pull/1/head
Mike Barry 2021-06-22 21:46:42 -04:00
rodzic aa0b622474
commit eea9d4ce86
30 zmienionych plików z 2689 dodań i 1148 usunięć

Wyświetl plik

@ -35,7 +35,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
}
public Feature geometry(String layer, Geometry geometry) {
Feature feature = new Feature(layer, geometry);
Feature feature = new Feature(layer, geometry, source.id());
output.add(feature);
return feature;
}
@ -49,7 +49,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
} catch (GeometryException e) {
stats.dataError("feature_point_" + e.stat());
LOGGER.warn("Error getting point geometry for " + source + ": " + e.getMessage());
return new Feature(layer, EMPTY_GEOM);
return new Feature(layer, EMPTY_GEOM, source.id());
}
}
@ -59,7 +59,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
} catch (GeometryException e) {
stats.dataError("feature_centroid_" + e.stat());
LOGGER.warn("Error getting centroid for " + source + ": " + e.getMessage());
return new Feature(layer, EMPTY_GEOM);
return new Feature(layer, EMPTY_GEOM, source.id());
}
}
@ -69,7 +69,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
} catch (GeometryException e) {
stats.dataError("feature_line_" + e.stat());
LOGGER.warn("Error constructing line for " + source + ": " + e.getMessage());
return new Feature(layer, EMPTY_GEOM);
return new Feature(layer, EMPTY_GEOM, source.id());
}
}
@ -79,7 +79,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
} catch (GeometryException e) {
stats.dataError("feature_polygon_" + e.stat());
LOGGER.warn("Error constructing polygon for " + source + ": " + e.getMessage());
return new Feature(layer, EMPTY_GEOM);
return new Feature(layer, EMPTY_GEOM, source.id());
}
}
@ -89,7 +89,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
} catch (GeometryException e) {
stats.dataError("feature_validated_polygon_" + e.stat());
LOGGER.warn("Error constructing validated polygon for " + source + ": " + e.getMessage());
return new Feature(layer, EMPTY_GEOM);
return new Feature(layer, EMPTY_GEOM, source.id());
}
}
@ -99,7 +99,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
} catch (GeometryException e) {
stats.dataError("feature_point_on_surface_" + e.stat());
LOGGER.warn("Error constructing point on surface for " + source + ": " + e.getMessage());
return new Feature(layer, EMPTY_GEOM);
return new Feature(layer, EMPTY_GEOM, source.id());
}
}
@ -118,6 +118,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
private final Geometry geom;
private final Map<String, Object> attrs = new TreeMap<>();
private final GeometryType geometryType;
private final long sourceId;
private int zOrder;
private int minzoom = config.minzoom();
private int maxzoom = config.maxzoom();
@ -134,11 +135,16 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
private double pixelToleranceAtMaxZoom = 256d / 4096;
private ZoomFunction<Double> pixelTolerance = null;
private Feature(String layer, Geometry geom) {
private Feature(String layer, Geometry geom, long sourceId) {
this.layer = layer;
this.geom = geom;
this.zOrder = 0;
this.geometryType = GeometryType.valueOf(geom);
this.sourceId = sourceId;
}
public long sourceId() {
return sourceId;
}
public int getZorder() {

Wyświetl plik

@ -109,4 +109,18 @@ public class Parse {
(Parse.boolInt(tags.get("bridge")) * 10L);
return Math.abs(z) < 10_000 ? (int) z : 0;
}
public static Double parseDoubleOrNull(Object value) {
if (value instanceof Number num) {
return num.doubleValue();
}
if (value == null) {
return null;
}
try {
return Double.parseDouble(value.toString());
} catch (NumberFormatException e) {
return null;
}
}
}

Wyświetl plik

@ -5,6 +5,7 @@ import com.graphhopper.reader.ReaderRelation;
import com.onthegomap.flatmap.geo.GeometryException;
import com.onthegomap.flatmap.read.OpenStreetMapReader;
import java.util.List;
import java.util.function.Consumer;
public interface Profile {
@ -44,6 +45,10 @@ public interface Profile {
return true;
}
default void finish(String sourceName, FeatureCollector.Factory featureCollectors,
Consumer<FeatureCollector.Feature> next) {
}
class NullProfile implements Profile {
@Override

Wyświetl plik

@ -14,11 +14,11 @@ public abstract class SourceFeature {
private final Map<String, Object> properties;
private final String source;
private final String sourceLayer;
private final List<OpenStreetMapReader.RelationMember<?>> relationInfos;
private final List<OpenStreetMapReader.RelationMember<OpenStreetMapReader.RelationInfo>> relationInfos;
private final long id;
protected SourceFeature(Map<String, Object> properties, String source, String sourceLayer,
List<OpenStreetMapReader.RelationMember<?>> relationInfos, long id) {
List<OpenStreetMapReader.RelationMember<OpenStreetMapReader.RelationInfo>> relationInfos, long id) {
this.properties = properties;
this.source = source;
this.sourceLayer = sourceLayer;

Wyświetl plik

@ -134,7 +134,7 @@ public class GeoUtils {
}
public static double decodeWorldX(long encoded) {
return ((double) (encoded >> 32)) / QUANTIZED_WORLD_SIZE;
return ((double) (encoded >>> 32)) / QUANTIZED_WORLD_SIZE;
}
public static double getZoomFromLonLatBounds(Envelope envelope) {

Wyświetl plik

@ -37,6 +37,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateList;
import org.locationtech.jts.geom.CoordinateSequence;
@ -169,12 +170,7 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima
ReaderElement readerElement;
var featureCollectors = new FeatureCollector.Factory(config, stats);
NodeLocationProvider nodeCache = newNodeGeometryCache();
var encoder = writer.newRenderedFeatureEncoder();
FeatureRenderer renderer = new FeatureRenderer(
config,
rendered -> next.accept(encoder.apply(rendered)),
stats
);
FeatureRenderer renderer = getFeatureRenderer(writer, config, next);
while ((readerElement = prev.get()) != null) {
SourceFeature feature = null;
if (readerElement instanceof ReaderNode node) {
@ -220,9 +216,23 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima
.addTopologyStats(topology);
topology.awaitAndLog(logger, config.logInterval());
profile.finish(name,
new FeatureCollector.Factory(config, stats),
getFeatureRenderer(writer, config, writer));
timer.stop();
}
private FeatureRenderer getFeatureRenderer(FeatureGroup writer, CommonParams config,
Consumer<FeatureSort.Entry> next) {
var encoder = writer.newRenderedFeatureEncoder();
return new FeatureRenderer(
config,
rendered -> next.accept(encoder.apply(rendered)),
stats
);
}
SourceFeature processRelationPass2(ReaderRelation rel, NodeLocationProvider nodeCache) {
return rel.hasTag("type", "multipolygon") ? new MultipolygonSourceFeature(rel, nodeCache) : null;
}
@ -237,7 +247,7 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima
boolean closed = nodes.size() > 1 && nodes.get(0) == nodes.get(nodes.size() - 1);
String area = way.getTag("area");
LongArrayList relationIds = wayToRelations.get(way.getId());
List<RelationMember<?>> rels = null;
List<RelationMember<RelationInfo>> rels = null;
if (!relationIds.isEmpty()) {
rels = new ArrayList<>(relationIds.size());
for (int r = 0; r < relationIds.size(); r++) {
@ -332,7 +342,7 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima
final boolean point;
public ProxyFeature(ReaderElement elem, boolean point, boolean line, boolean polygon,
List<RelationMember<?>> relationInfo) {
List<RelationMember<RelationInfo>> relationInfo) {
super(ReaderElementUtils.getProperties(elem), name, null, relationInfo, elem.getId());
this.point = point;
this.line = line;
@ -407,7 +417,7 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima
private final LongArrayList nodeIds;
public WaySourceFeature(ReaderWay way, boolean closed, String area, NodeLocationProvider nodeCache,
List<RelationMember<?>> relationInfo) {
List<RelationMember<RelationInfo>> relationInfo) {
super(way, false,
(!closed || !"yes".equals(area)) && way.getNodes().size() >= 2,
(closed && !"no".equals(area)) && way.getNodes().size() >= 4,

Wyświetl plik

@ -15,8 +15,10 @@ package com.onthegomap.flatmap.read;
import com.carrotsearch.hppc.LongArrayList;
import com.carrotsearch.hppc.LongObjectMap;
import com.carrotsearch.hppc.ObjectIntMap;
import com.carrotsearch.hppc.cursors.LongObjectCursor;
import com.graphhopper.coll.GHLongObjectHashMap;
import com.graphhopper.coll.GHObjectIntHashMap;
import com.onthegomap.flatmap.geo.GeoUtils;
import com.onthegomap.flatmap.geo.GeometryException;
import java.util.ArrayList;
@ -35,7 +37,7 @@ import org.locationtech.jts.geom.prep.PreparedPolygon;
* This class is ported to Java from https://github.com/omniscale/imposm3/blob/master/geom/multipolygon.go and
* https://github.com/omniscale/imposm3/blob/master/geom/ring.go
*/
class OsmMultipolygon {
public class OsmMultipolygon {
private static final double MIN_CLOSE_RING_GAP = 0.1 / GeoUtils.WORLD_CIRCUMFERENCE_METERS;
private static final Comparator<Ring> BY_AREA_DESCENDING = Comparator.comparingDouble(ring -> -ring.area);
@ -68,10 +70,39 @@ class OsmMultipolygon {
}
}
public static Geometry build(List<CoordinateSequence> rings) throws GeometryException {
ObjectIntMap<Coordinate> coordToId = new GHObjectIntHashMap<>();
List<Coordinate> idToCoord = new ArrayList<>();
int id = 0;
List<LongArrayList> idRings = new ArrayList<>(rings.size());
for (CoordinateSequence coords : rings) {
LongArrayList idRing = new LongArrayList(coords.size());
idRings.add(idRing);
for (Coordinate coord : coords.toCoordinateArray()) {
if (!coordToId.containsKey(coord)) {
coordToId.put(coord, id);
idToCoord.add(coord);
id++;
}
idRing.add(coordToId.get(coord));
}
}
return build(idRings, lookupId -> idToCoord.get((int) lookupId), 0, MIN_CLOSE_RING_GAP);
}
public static Geometry build(
List<LongArrayList> rings,
OpenStreetMapReader.NodeLocationProvider nodeCache,
long osmId
) throws GeometryException {
return build(rings, nodeCache, osmId, MIN_CLOSE_RING_GAP);
}
public static Geometry build(
List<LongArrayList> rings,
OpenStreetMapReader.NodeLocationProvider nodeCache,
long osmId,
double minGap
) throws GeometryException {
try {
if (rings.size() == 0) {
@ -83,7 +114,7 @@ class OsmMultipolygon {
for (LongArrayList segment : idSegments) {
int size = segment.size();
long firstId = segment.get(0), lastId = segment.get(size - 1);
if (firstId == lastId || tryClose(segment, nodeCache)) {
if (firstId == lastId || tryClose(segment, nodeCache, minGap)) {
CoordinateSequence coordinates = nodeCache.getWayGeometry(segment);
Polygon poly = GeoUtils.JTS_FACTORY.createPolygon(coordinates);
polygons.add(new Ring(poly));
@ -142,12 +173,13 @@ class OsmMultipolygon {
return shells;
}
private static boolean tryClose(LongArrayList segment, OpenStreetMapReader.NodeLocationProvider nodeCache) {
private static boolean tryClose(LongArrayList segment, OpenStreetMapReader.NodeLocationProvider nodeCache,
double minGap) {
int size = segment.size();
long firstId = segment.get(0);
Coordinate firstCoord = nodeCache.getCoordinate(firstId);
Coordinate lastCoord = nodeCache.getCoordinate(segment.get(size - 1));
if (firstCoord.distance(lastCoord) <= MIN_CLOSE_RING_GAP) {
if (firstCoord.distance(lastCoord) <= minGap) {
segment.set(size - 1, firstId);
return true;
}

Wyświetl plik

@ -12,6 +12,8 @@ import com.onthegomap.flatmap.render.FeatureRenderer;
import com.onthegomap.flatmap.worker.Topology;
import java.io.Closeable;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import org.jetbrains.annotations.NotNull;
import org.locationtech.jts.geom.Envelope;
public abstract class Reader implements Closeable {
@ -40,12 +42,7 @@ public abstract class Reader implements Closeable {
.<FeatureSort.Entry>addWorker("process", threads, (prev, next) -> {
SourceFeature sourceFeature;
var featureCollectors = new FeatureCollector.Factory(config, stats);
var encoder = writer.newRenderedFeatureEncoder();
FeatureRenderer renderer = new FeatureRenderer(
config,
rendered -> next.accept(encoder.apply(rendered)),
stats
);
FeatureRenderer renderer = getFeatureRenderer(writer, config, next);
while ((sourceFeature = prev.get()) != null) {
featuresRead.incrementAndGet();
FeatureCollector features = featureCollectors.get(sourceFeature);
@ -71,9 +68,25 @@ public abstract class Reader implements Closeable {
.addTopologyStats(topology);
topology.awaitAndLog(loggers, config.logInterval());
profile.finish(sourceName,
new FeatureCollector.Factory(config, stats),
getFeatureRenderer(writer, config, writer)
);
timer.stop();
}
@NotNull
private FeatureRenderer getFeatureRenderer(FeatureGroup writer, CommonParams config,
Consumer<FeatureSort.Entry> next) {
var encoder = writer.newRenderedFeatureEncoder();
return new FeatureRenderer(
config,
rendered -> next.accept(encoder.apply(rendered)),
stats
);
}
public abstract long getCount();
public abstract Topology.SourceStep<? extends SourceFeature> read();

Wyświetl plik

@ -3,6 +3,7 @@ package com.onthegomap.flatmap.read;
import com.onthegomap.flatmap.SourceFeature;
import com.onthegomap.flatmap.geo.GeoUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.locationtech.jts.geom.Geometry;
@ -21,15 +22,20 @@ public class ReaderFeature extends SourceFeature {
public ReaderFeature(Geometry latLonGeometry, Map<String, Object> properties, String source, String sourceLayer,
long id) {
super(properties, source, sourceLayer, null, id);
this.latLonGeometry = latLonGeometry;
this.properties = properties;
this(latLonGeometry, properties, source, sourceLayer, id, null);
}
public ReaderFeature(Geometry latLonGeometry, int numProperties, String source, String sourceLayer, long id) {
this(latLonGeometry, new HashMap<>(numProperties), source, sourceLayer, id);
}
public ReaderFeature(Geometry latLonGeometry, Map<String, Object> properties, String source, String sourceLayer,
long id, List<OpenStreetMapReader.RelationMember<OpenStreetMapReader.RelationInfo>> relations) {
super(properties, source, sourceLayer, relations, id);
this.latLonGeometry = latLonGeometry;
this.properties = properties;
}
@Override
public Geometry latLonGeometry() {
return latLonGeometry;

Wyświetl plik

@ -88,7 +88,7 @@ public class FeatureRenderer implements Consumer<FeatureCollector.Feature> {
double buffer = feature.getBufferPixelsAtZoom(zoom) / 256;
int tilesAtZoom = 1 << zoom;
TileExtents.ForZoom extents = config.extents().getForZoom(zoom);
TiledGeometry tiled = TiledGeometry.slicePointsIntoTiles(extents, buffer, zoom, coords);
TiledGeometry tiled = TiledGeometry.slicePointsIntoTiles(extents, buffer, zoom, coords, feature.sourceId());
RenderedFeature.Group groupInfo = null;
if (hasLabelGrid && coords.length == 1) {
@ -163,7 +163,7 @@ public class FeatureRenderer implements Consumer<FeatureCollector.Feature> {
List<List<CoordinateSequence>> groups = CoordinateSequenceExtractor.extractGroups(geom, minSize);
double buffer = feature.getBufferPixelsAtZoom(z) / 256;
TileExtents.ForZoom extents = config.extents().getForZoom(z);
TiledGeometry sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents);
TiledGeometry sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents, feature.sourceId());
writeTileFeatures(z, id, feature, sliced);
}

Wyświetl plik

@ -46,6 +46,7 @@ class TiledGeometry {
private static final double NEIGHBOR_BUFFER_EPS = 0.1d / 4096;
private final Map<TileCoord, List<List<CoordinateSequence>>> tileContents = new HashMap<>();
private final long id;
private Map<Integer, IntRange> filledRanges = null;
private final TileExtents.ForZoom extents;
private final double buffer;
@ -54,7 +55,8 @@ class TiledGeometry {
private final boolean area;
private final int max;
private TiledGeometry(TileExtents.ForZoom extents, double buffer, int z, boolean area) {
private TiledGeometry(TileExtents.ForZoom extents, double buffer, int z, boolean area, long id) {
this.id = id;
this.extents = extents;
this.buffer = buffer;
// make sure we inspect neighboring tiles when a line runs along an edge
@ -65,8 +67,8 @@ class TiledGeometry {
}
public static TiledGeometry slicePointsIntoTiles(TileExtents.ForZoom extents, double buffer, int z,
Coordinate[] coords) {
TiledGeometry result = new TiledGeometry(extents, buffer, z, false);
Coordinate[] coords, long id) {
TiledGeometry result = new TiledGeometry(extents, buffer, z, false, id);
for (Coordinate coord : coords) {
result.slicePoint(coord);
}
@ -107,9 +109,9 @@ class TiledGeometry {
}
public static TiledGeometry sliceIntoTiles(List<List<CoordinateSequence>> groups, double buffer, boolean area, int z,
TileExtents.ForZoom extents) {
TileExtents.ForZoom extents, long id) {
int worldExtent = 1 << z;
TiledGeometry result = new TiledGeometry(extents, buffer, z, area);
TiledGeometry result = new TiledGeometry(extents, buffer, z, area, id);
EnumSet<Direction> wrapResult = result.sliceWorldCopy(groups, 0);
if (wrapResult.contains(Direction.RIGHT)) {
result.sliceWorldCopy(groups, -worldExtent);
@ -176,7 +178,7 @@ class TiledGeometry {
boolean outer = i == 0;
IntObjectMap<List<MutableCoordinateSequence>> xSlices = sliceX(segment);
if (z >= 6 && xSlices.size() >= Math.pow(2, z) - 1) {
LOGGER.warn("Feature crosses world at z" + z + ": " + xSlices.size());
LOGGER.warn("Feature " + id + " crosses world at z" + z + ": " + xSlices.size());
}
for (IntObjectCursor<List<MutableCoordinateSequence>> xCursor : xSlices) {
int x = xCursor.key + xOffset;

Wyświetl plik

@ -27,6 +27,7 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@ -151,6 +152,18 @@ public class FlatMapTest {
);
}
private FlatMapResults runWithReaderFeaturesProfile(
Map<String, String> args,
List<ReaderFeature> features,
Profile profileToUse
) throws Exception {
return run(
args,
(featureGroup, profile, config) -> processReaderFeatures(featureGroup, profile, config, features),
profileToUse
);
}
private FlatMapResults runWithOsmElements(
Map<String, String> args,
List<ReaderElement> features,
@ -163,6 +176,18 @@ public class FlatMapTest {
);
}
private FlatMapResults runWithOsmElements(
Map<String, String> args,
List<ReaderElement> features,
Profile profileToUse
) throws Exception {
return run(
args,
(featureGroup, profile, config) -> processOsmFeatures(featureGroup, profile, config, features),
profileToUse
);
}
private FlatMapResults runWithOsmElements(
Map<String, String> args,
List<ReaderElement> features,
@ -1042,6 +1067,114 @@ public class FlatMapTest {
)), sortListValues(results.tiles));
}
@Test
public void testReaderProfileFinish() throws Exception {
double y = 0.5 + Z14_WIDTH / 2;
double lat = GeoUtils.getWorldLat(y);
double x1 = 0.5 + Z14_WIDTH / 4;
double lng1 = GeoUtils.getWorldLon(x1);
double lng2 = GeoUtils.getWorldLon(x1 + Z14_WIDTH * 10d / 256);
var results = runWithReaderFeaturesProfile(
Map.of("threads", "1"),
List.of(
newReaderFeature(newPoint(lng1, lat), Map.of("a", 1, "b", 2)),
newReaderFeature(newPoint(lng2, lat), Map.of("a", 3, "b", 4))
),
new Profile.NullProfile() {
private final List<SourceFeature> featureList = Collections.synchronizedList(new ArrayList<>());
@Override
public void processFeature(SourceFeature in, FeatureCollector features) {
featureList.add(in);
}
@Override
public void finish(String name, FeatureCollector.Factory featureCollectors,
Consumer<FeatureCollector.Feature> next) {
if ("test".equals(name)) {
for (SourceFeature in : featureList) {
var features = featureCollectors.get(in);
features.point("layer")
.setZoomRange(13, 14)
.inheritFromSource("a");
for (var feature : features) {
next.accept(feature);
}
}
}
}
}
);
assertSubmap(sortListValues(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
feature(newPoint(64, 128), Map.of("a", 1L)),
feature(newPoint(74, 128), Map.of("a", 3L))
),
TileCoord.ofXYZ(Z13_TILES / 2, Z13_TILES / 2, 13), List.of(
// merge 32->37 and 37->42 since they have same attrs
feature(newPoint(32, 64), Map.of("a", 1L)),
feature(newPoint(37, 64), Map.of("a", 3L))
)
)), sortListValues(results.tiles));
}
@Test
public void testOsmProfileFinish() throws Exception {
double y = 0.5 + Z14_WIDTH / 2;
double lat = GeoUtils.getWorldLat(y);
double x1 = 0.5 + Z14_WIDTH / 4;
double lng1 = GeoUtils.getWorldLon(x1);
double lng2 = GeoUtils.getWorldLon(x1 + Z14_WIDTH * 10d / 256);
var results = runWithOsmElements(
Map.of("threads", "1"),
List.of(
with(new ReaderNode(1, lat, lng1), t -> t.setTag("a", 1)),
with(new ReaderNode(2, lat, lng2), t -> t.setTag("a", 3))
),
new Profile.NullProfile() {
private final List<SourceFeature> featureList = Collections.synchronizedList(new ArrayList<>());
@Override
public void processFeature(SourceFeature in, FeatureCollector features) {
featureList.add(in);
}
@Override
public void finish(String name, FeatureCollector.Factory featureCollectors,
Consumer<FeatureCollector.Feature> next) {
if ("osm".equals(name)) {
for (SourceFeature in : featureList) {
var features = featureCollectors.get(in);
features.point("layer")
.setZoomRange(13, 14)
.inheritFromSource("a");
for (var feature : features) {
next.accept(feature);
}
}
}
}
}
);
assertSubmap(sortListValues(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
feature(newPoint(64, 128), Map.of("a", 1L)),
feature(newPoint(74, 128), Map.of("a", 3L))
),
TileCoord.ofXYZ(Z13_TILES / 2, Z13_TILES / 2, 13), List.of(
// merge 32->37 and 37->42 since they have same attrs
feature(newPoint(32, 64), Map.of("a", 1L)),
feature(newPoint(37, 64), Map.of("a", 3L))
)
)), sortListValues(results.tiles));
}
private <K extends Comparable<? super K>, V extends List<?>> Map<K, ?> sortListValues(Map<K, V> input) {
Map<K, List<?>> result = new TreeMap<>();
for (var entry : input.entrySet()) {

Wyświetl plik

@ -17,6 +17,7 @@ public class GeoUtilsTest {
@CsvSource({
"0,0, 0.5,0.5",
"0, -180, 0, 0.5",
"0, 180, 1, 0.5",
"0, " + (180 - 1e-7) + ", 1, 0.5",
"45, 0, 0.5, 0.359725",
"-45, 0, 0.5, " + (1 - 0.359725)

Wyświetl plik

@ -164,6 +164,15 @@ public class OsmMultipolygonTest {
);
}
@Test
public void testBuildMultipolygonFromGeometries() throws GeometryException {
Geometry actual = OsmMultipolygon.build(List.of(
newLineString(0.2, 0.2, 0.4, 0.2, 0.4, 0.4).getCoordinateSequence(),
newLineString(0.4, 0.4, 0.2, 0.4, 0.2, 0.2).getCoordinateSequence()
));
assertSameNormalizedFeature(rectangle(0.2, 0.4), actual);
}
@Test
public void testThrowWhenNoClosed() {
var node1 = node(0.5, 0.5);

Wyświetl plik

@ -23,6 +23,11 @@
<artifactId>snakeyaml</artifactId>
<version>1.29</version>
</dependency>
<dependency>
<groupId>org.commonmark</groupId>
<artifactId>commonmark</artifactId>
<version>0.17.2</version>
</dependency>
<dependency>
<groupId>com.ibm.icu</groupId>
<artifactId>icu4j</artifactId>

Wyświetl plik

@ -26,6 +26,9 @@ import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
import org.apache.commons.text.StringEscapeUtils;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.LoaderOptions;
@ -343,6 +346,7 @@ public class Generate {
import com.onthegomap.flatmap.Translations;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class OpenMapTilesSchema {
public static final String NAME = %s;
@ -415,6 +419,10 @@ public class Generate {
.formatted(name.toUpperCase(Locale.ROOT) + "_" + v.toUpperCase(Locale.ROOT).replace('-', '_'), quote(v)))
.collect(joining("\n")).indent(2).strip()
.indent(4));
fieldValues.append("public static final Set<String> %s = Set.of(%s);".formatted(
name.toUpperCase(Locale.ROOT) + "_VALUES",
values.stream().map(Generate::quote).collect(joining(", "))
).indent(4));
}
if (valuesNode != null && valuesNode.isObject()) {
@ -524,8 +532,12 @@ public class Generate {
return result;
}
private static final Parser parser = Parser.builder().build();
private static final HtmlRenderer renderer = HtmlRenderer.builder().build();
private static String escapeJavadoc(String description) {
return description.replaceAll("[\n\r*\\s]+", " ");
Node document = parser.parse(description);
return renderer.render(document).replaceAll("[\n\r*\\s]+", " ");
}
private static String getFieldDescription(JsonNode value) {

Wyświetl plik

@ -27,8 +27,8 @@ public class OpenMapTilesMain {
.setProfile(createProfileWithWikidataTranslations(runner))
.addShapefileSource("EPSG:3857", OpenMapTilesProfile.LAKE_CENTERLINE_SOURCE,
sourcesDir.resolve("lake_centerline.shp.zip"))
.addShapefileSource(OpenMapTilesProfile.WATER_POLYGON_SOURCE,
sourcesDir.resolve("water-polygons-split-3857.zip"))
// .addShapefileSource(OpenMapTilesProfile.WATER_POLYGON_SOURCE,
// sourcesDir.resolve("water-polygons-split-3857.zip"))
.addNaturalEarthSource(OpenMapTilesProfile.NATURAL_EARTH_SOURCE,
sourcesDir.resolve("natural_earth_vector.sqlite.zip"))
.addOsmSource(OpenMapTilesProfile.OSM_SOURCE, sourcesDir.resolve(fallbackOsmFile))

Wyświetl plik

@ -23,9 +23,14 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OpenMapTilesProfile implements Profile {
private static final Logger LOGGER = LoggerFactory.getLogger(OpenMapTilesProfile.class);
public static final String LAKE_CENTERLINE_SOURCE = "lake_centerlines";
public static final String WATER_POLYGON_SOURCE = "water_polygons";
public static final String NATURAL_EARTH_SOURCE = "natural_earth";
@ -41,6 +46,8 @@ public class OpenMapTilesProfile implements Profile {
private final List<OsmWaterPolygonProcessor> osmWaterProcessors;
private final List<LakeCenterlineProcessor> lakeCenterlineProcessors;
private final List<OsmAllProcessor> osmAllProcessors;
private final List<OsmRelationPreprocessor> osmRelationPreprocessors;
private final List<FinishHandler> finishHandlers;
private MultiExpression.MultiExpressionIndex<Tables.Constructor> indexForType(String type) {
return Tables.MAPPINGS
@ -73,6 +80,8 @@ public class OpenMapTilesProfile implements Profile {
lakeCenterlineProcessors = new ArrayList<>();
naturalEarthProcessors = new ArrayList<>();
osmWaterProcessors = new ArrayList<>();
osmRelationPreprocessors = new ArrayList<>();
finishHandlers = new ArrayList<>();
for (Layer layer : layers) {
if (layer instanceof FeaturePostProcessor postProcessor) {
postProcessors.put(layer.name(), postProcessor);
@ -89,6 +98,12 @@ public class OpenMapTilesProfile implements Profile {
if (layer instanceof NaturalEarthProcessor processor) {
naturalEarthProcessors.add(processor);
}
if (layer instanceof OsmRelationPreprocessor processor) {
osmRelationPreprocessors.add(processor);
}
if (layer instanceof FinishHandler processor) {
finishHandlers.add(processor);
}
}
}
@ -110,7 +125,19 @@ public class OpenMapTilesProfile implements Profile {
@Override
public List<OpenStreetMapReader.RelationInfo> preprocessOsmRelation(ReaderRelation relation) {
return null;
List<OpenStreetMapReader.RelationInfo> result = null;
for (int i = 0; i < osmRelationPreprocessors.size(); i++) {
List<OpenStreetMapReader.RelationInfo> thisResult = osmRelationPreprocessors.get(i)
.preprocessOsmRelation(relation);
if (thisResult != null) {
if (result == null) {
result = new ArrayList<>(thisResult);
} else {
result.addAll(thisResult);
}
}
}
return result;
}
@Override
@ -166,6 +193,14 @@ public class OpenMapTilesProfile implements Profile {
return result == null ? List.of() : result;
}
@Override
public void finish(String sourceName, FeatureCollector.Factory featureCollectors,
Consumer<FeatureCollector.Feature> next) {
for (var handler : finishHandlers) {
handler.finish(sourceName, featureCollectors, next);
}
}
public interface NaturalEarthProcessor {
void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features);
@ -186,6 +221,17 @@ public class OpenMapTilesProfile implements Profile {
void processAllOsm(SourceFeature feature, FeatureCollector features);
}
public interface FinishHandler {
void finish(String sourceName, FeatureCollector.Factory featureCollectors,
Consumer<FeatureCollector.Feature> next);
}
public interface OsmRelationPreprocessor {
List<OpenStreetMapReader.RelationInfo> preprocessOsmRelation(ReaderRelation relation);
}
public interface FeaturePostProcessor {
List<VectorTileEncoder.Feature> postProcess(int zoom, List<VectorTileEncoder.Feature> items)

Wyświetl plik

@ -1,14 +1,344 @@
package com.onthegomap.flatmap.openmaptiles.layers;
import com.onthegomap.flatmap.Arguments;
import com.onthegomap.flatmap.Translations;
import com.onthegomap.flatmap.monitoring.Stats;
import com.onthegomap.flatmap.openmaptiles.generated.OpenMapTilesSchema;
import static com.onthegomap.flatmap.geo.GeoUtils.JTS_FACTORY;
public class Boundary implements OpenMapTilesSchema.Boundary {
import com.carrotsearch.hppc.LongObjectMap;
import com.graphhopper.coll.GHLongObjectHashMap;
import com.graphhopper.reader.ReaderElementUtils;
import com.graphhopper.reader.ReaderRelation;
import com.onthegomap.flatmap.Arguments;
import com.onthegomap.flatmap.FeatureCollector;
import com.onthegomap.flatmap.FeatureMerge;
import com.onthegomap.flatmap.MemoryEstimator;
import com.onthegomap.flatmap.Parse;
import com.onthegomap.flatmap.SourceFeature;
import com.onthegomap.flatmap.Translations;
import com.onthegomap.flatmap.VectorTileEncoder;
import com.onthegomap.flatmap.geo.GeoUtils;
import com.onthegomap.flatmap.geo.GeometryException;
import com.onthegomap.flatmap.monitoring.Stats;
import com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile;
import com.onthegomap.flatmap.openmaptiles.generated.OpenMapTilesSchema;
import com.onthegomap.flatmap.read.OpenStreetMapReader;
import com.onthegomap.flatmap.read.OsmMultipolygon;
import com.onthegomap.flatmap.read.ReaderFeature;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryComponentFilter;
import org.locationtech.jts.geom.LineSegment;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.prep.PreparedGeometry;
import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
import org.locationtech.jts.operation.linemerge.LineMerger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Boundary implements
OpenMapTilesSchema.Boundary,
OpenMapTilesProfile.NaturalEarthProcessor,
OpenMapTilesProfile.OsmRelationPreprocessor,
OpenMapTilesProfile.OsmAllProcessor,
OpenMapTilesProfile.FeaturePostProcessor,
OpenMapTilesProfile.FinishHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(Boundary.class);
private static final double COUNTRY_TEST_OFFSET = GeoUtils.metersToPixelAtEquator(0, 100) / 256d;
private final Map<Long, String> regionNames = new HashMap<>();
private final Map<Long, List<Geometry>> regionGeometries = new HashMap<>();
private final Map<CountryBoundaryComponent, List<Geometry>> boundariesToMerge = new HashMap<>();
private final Stats stats;
public Boundary(Translations translations, Arguments args, Stats stats) {
this.stats = stats;
}
// TODO implement
private static boolean isDisputed(Map<String, Object> tags) {
return Parse.bool(tags.get("disputed")) ||
Parse.bool(tags.get("dispute")) ||
"dispute".equals(tags.get("border_status")) ||
tags.containsKey("disputed_by") ||
tags.containsKey("claimed_by");
}
private static String editName(String name) {
return name == null ? null : name.replace(" at ", "")
.replaceAll("\\s+", "")
.replace("Extentof", "");
}
@Override
public void release() {
regionGeometries.clear();
boundariesToMerge.clear();
regionNames.clear();
}
@Override
public void processNaturalEarth(String table, SourceFeature feature, FeatureCollector features) {
boolean disputed = feature.getString("featurecla", "").startsWith("Disputed");
record BoundaryInfo(int adminLevel, int minzoom, int maxzoom) {}
BoundaryInfo info = switch (table) {
case "ne_110m_admin_0_boundary_lines_land" -> new BoundaryInfo(2, 0, 0);
case "ne_50m_admin_0_boundary_lines_land" -> new BoundaryInfo(2, 1, 3);
case "ne_10m_admin_0_boundary_lines_land" -> feature.hasTag("featurecla", "Lease Limit") ? null
: new BoundaryInfo(2, 4, 4);
case "ne_10m_admin_1_states_provinces_lines" -> {
Double minZoom = Parse.parseDoubleOrNull(feature.getTag("min_zoom"));
yield minZoom != null && minZoom <= 7 ? new BoundaryInfo(4, 1, 4) : null;
}
default -> null;
};
if (info != null) {
features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
.setZoomRange(info.minzoom, info.maxzoom)
.setMinPixelSizeAtAllZooms(0)
.setAttr(Fields.ADMIN_LEVEL, info.adminLevel)
.setAttr(Fields.MARITIME, 0)
.setAttr(Fields.DISPUTED, disputed ? 1 : 0);
}
}
@Override
public List<VectorTileEncoder.Feature> postProcess(int zoom, List<VectorTileEncoder.Feature> items)
throws GeometryException {
double tolerance = zoom >= 14 ? 256d / 4096d : 0.1;
return FeatureMerge.mergeLineStrings(items, 1, tolerance, BUFFER_SIZE);
}
@Override
public void finish(String sourceName, FeatureCollector.Factory featureCollectors,
Consumer<FeatureCollector.Feature> next) {
if (OpenMapTilesProfile.OSM_SOURCE.equals(sourceName)) {
var timer = stats.startTimer("boundaries");
LOGGER.info("[boundaries] Creating polygons for " + regionGeometries.size() + " boundaries");
LongObjectMap<PreparedGeometry> countryBoundaries = new GHLongObjectHashMap<>();
for (var entry : regionGeometries.entrySet()) {
Long countryCode = entry.getKey();
List<CoordinateSequence> seqs = new ArrayList<>();
for (Geometry geometry : entry.getValue()) {
geometry.apply((GeometryComponentFilter) geom -> {
if (geom instanceof LineString lineString) {
seqs.add(lineString.getCoordinateSequence());
}
});
}
try {
countryBoundaries.put(countryCode, PreparedGeometryFactory.prepare(
GeoUtils.fixPolygon(
OsmMultipolygon.build(seqs)
)
));
} catch (GeometryException e) {
LOGGER.warn("[boundaries] Unable to build boundary polygon for " + countryCode + ": " + e.getMessage());
}
}
LOGGER.info("[boundaries] Finished creating polygons");
long number = 0;
for (var entry : boundariesToMerge.entrySet()) {
number++;
CountryBoundaryComponent key = entry.getKey();
LineMerger merger = new LineMerger();
for (Geometry geom : entry.getValue()) {
merger.add(geom);
}
entry.getValue().clear();
for (Object merged : merger.getMergedLineStrings()) {
if (merged instanceof LineString lineString) {
Long rightCountry = null, leftCountry = null;
int numPoints = lineString.getNumPoints();
int middle = Math.max(0, Math.min(numPoints - 2, numPoints / 2));
Coordinate a = lineString.getCoordinateN(middle);
Coordinate b = lineString.getCoordinateN(middle + 1);
LineSegment segment = new LineSegment(a, b);
Point right = JTS_FACTORY.createPoint(segment.pointAlongOffset(0.5, COUNTRY_TEST_OFFSET));
Point left = JTS_FACTORY.createPoint(segment.pointAlongOffset(0.5, -COUNTRY_TEST_OFFSET));
for (Long regionId : key.regions) {
PreparedGeometry geom = countryBoundaries.get(regionId);
if (geom != null) {
if (geom.contains(right)) {
rightCountry = regionId;
} else if (geom.contains(left)) {
leftCountry = regionId;
}
}
}
if (leftCountry == null && rightCountry == null) {
LOGGER.warn("[boundaries] no left or right country for " + key);
}
var features = featureCollectors.get(new ReaderFeature(
GeoUtils.worldToLatLonCoords(lineString),
Map.of(),
number
));
features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
.setAttr(Fields.ADMIN_LEVEL, key.adminLevel)
.setAttr(Fields.DISPUTED, key.disputed ? 1 : 0)
.setAttr(Fields.MARITIME, key.maritime ? 1 : 0)
.setAttr(Fields.CLAIMED_BY, key.claimedBy)
.setAttr(Fields.DISPUTED_NAME, key.disputed ? editName(key.name) : null)
.setAttr(Fields.ADM0_L, regionNames.get(leftCountry))
.setAttr(Fields.ADM0_R, regionNames.get(rightCountry))
.setMinPixelSizeAtAllZooms(0)
.setZoomRange(key.minzoom, 14);
for (var feature : features) {
next.accept(feature);
}
}
}
}
timer.stop();
}
}
@Override
public List<OpenStreetMapReader.RelationInfo> preprocessOsmRelation(ReaderRelation relation) {
String typeTag = relation.getTag("type");
if ("boundary".equals(typeTag) && relation.hasTag("admin_level") && relation.hasTag("boundary", "administrative")) {
Integer adminLevelValue = Parse.parseIntSubstring(relation.getTag("admin_level"));
String code = relation.getTag("ISO3166-1:alpha3");
if (adminLevelValue != null && adminLevelValue >= 2 && adminLevelValue <= 2) {
boolean disputed = isDisputed(ReaderElementUtils.getProperties(relation));
if (code != null) {
synchronized (regionNames) {
regionNames.put(relation.getId(), code);
}
}
return List.of(new BoundaryRelation(
relation.getId(),
adminLevelValue,
disputed,
relation.getTag("name"),
disputed ? relation.getTag("claimed_by") : null,
code
));
}
}
return null;
}
@Override
public void processAllOsm(SourceFeature feature, FeatureCollector features) {
if (!feature.canBeLine()) {
return;
}
var relationInfos = feature.relationInfo(BoundaryRelation.class);
if (!relationInfos.isEmpty()) {
int minAdminLevel = Integer.MAX_VALUE;
String disputedName = null, claimedBy = null;
Set<Long> regionIds = new HashSet<>();
boolean disputed = false;
for (var info : relationInfos) {
BoundaryRelation rel = info.relation();
disputed |= rel.disputed;
if (rel.adminLevel < minAdminLevel) {
minAdminLevel = rel.adminLevel;
}
if (rel.disputed) {
disputedName = disputedName == null ? rel.name : disputedName;
claimedBy = claimedBy == null ? rel.claimedBy : claimedBy;
}
if (minAdminLevel == 2 && regionNames.containsKey(info.relation().id)) {
regionIds.add(info.relation().id);
}
}
if (minAdminLevel <= 10) {
boolean wayIsDisputed = isDisputed(feature.properties());
disputed |= wayIsDisputed;
if (wayIsDisputed) {
disputedName = disputedName == null ? feature.getString("name") : disputedName;
claimedBy = claimedBy == null ? feature.getString("claimed_by") : claimedBy;
}
boolean maritime = feature.getBoolean("maritime") ||
feature.hasTag("natural", "coastline") ||
feature.hasTag("boundary_type", "maritime");
int minzoom =
(maritime && minAdminLevel == 2) ? 4 :
minAdminLevel <= 4 ? 5 :
minAdminLevel <= 6 ? 9 :
minAdminLevel <= 8 ? 11 : 12;
if (!regionIds.isEmpty()) {
// save for later
try {
CountryBoundaryComponent component = new CountryBoundaryComponent(
minAdminLevel,
disputed,
maritime,
minzoom,
feature.line(),
regionIds,
claimedBy,
disputedName
);
synchronized (regionGeometries) {
boundariesToMerge.computeIfAbsent(component.groupingKey(), key -> new ArrayList<>()).add(component.line);
for (var info : relationInfos) {
var rel = info.relation();
if (rel.adminLevel <= 2) {
regionGeometries.computeIfAbsent(rel.id, id -> new ArrayList<>()).add(component.line);
}
}
}
} catch (GeometryException e) {
LOGGER.warn("Cannot extract boundary line from " + feature);
}
} else {
features.line(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
.setAttr(Fields.ADMIN_LEVEL, minAdminLevel)
.setAttr(Fields.DISPUTED, disputed ? 1 : 0)
.setAttr(Fields.MARITIME, maritime ? 1 : 0)
.setMinPixelSizeAtAllZooms(0)
.setZoomRange(minzoom, 14)
.setAttr(Fields.CLAIMED_BY, claimedBy)
.setAttr(Fields.DISPUTED_NAME, editName(disputedName));
}
}
}
}
private static record BoundaryRelation(
long id,
int adminLevel,
boolean disputed,
String name,
String claimedBy,
String iso3166alpha3
) implements OpenStreetMapReader.RelationInfo {
@Override
public long estimateMemoryUsageBytes() {
return 29 + 8 + MemoryEstimator.size(name)
+ 8 + MemoryEstimator.size(claimedBy)
+ 8 + MemoryEstimator.size(iso3166alpha3);
}
}
private static record CountryBoundaryComponent(
int adminLevel,
boolean disputed,
boolean maritime,
int minzoom,
Geometry line,
Set<Long> regions,
String claimedBy,
String name
) {
CountryBoundaryComponent groupingKey() {
return new CountryBoundaryComponent(adminLevel, disputed, maritime, minzoom, null, regions, claimedBy, name);
}
}
}

Wyświetl plik

@ -1,859 +1,23 @@
package com.onthegomap.flatmap.openmaptiles;
import static com.onthegomap.flatmap.TestUtils.assertSubmap;
import static com.onthegomap.flatmap.TestUtils.newLineString;
import static com.onthegomap.flatmap.TestUtils.newPoint;
import static com.onthegomap.flatmap.TestUtils.rectangle;
import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.LAKE_CENTERLINE_SOURCE;
import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.NATURAL_EARTH_SOURCE;
import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.OSM_SOURCE;
import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.WATER_POLYGON_SOURCE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import com.graphhopper.reader.ReaderNode;
import com.onthegomap.flatmap.Arguments;
import com.onthegomap.flatmap.CommonParams;
import com.onthegomap.flatmap.FeatureCollector;
import com.onthegomap.flatmap.SourceFeature;
import com.onthegomap.flatmap.TestUtils;
import com.onthegomap.flatmap.Translations;
import com.onthegomap.flatmap.VectorTileEncoder;
import com.onthegomap.flatmap.Wikidata;
import com.onthegomap.flatmap.geo.GeoUtils;
import com.onthegomap.flatmap.geo.GeometryException;
import com.onthegomap.flatmap.monitoring.Stats;
import com.onthegomap.flatmap.openmaptiles.layers.MountainPeak;
import com.onthegomap.flatmap.openmaptiles.layers.Waterway;
import com.onthegomap.flatmap.read.ReaderFeature;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.StreamSupport;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
public class OpenMaptilesProfileTest {
private final Wikidata.WikidataTranslations wikidataTranslations = new Wikidata.WikidataTranslations();
private final Translations translations = Translations.defaultProvider(List.of("en", "es", "de"))
.addTranslationProvider(wikidataTranslations);
private final CommonParams params = CommonParams.defaults();
private final OpenMapTilesProfile profile = new OpenMapTilesProfile(translations, Arguments.of(),
new Stats.InMemory());
private final Stats stats = new Stats.InMemory();
private final FeatureCollector.Factory featureCollectorFactory = new FeatureCollector.Factory(params, stats);
private static void assertFeatures(int zoom, List<Map<String, Object>> expected, FeatureCollector actual) {
List<FeatureCollector.Feature> actualList = StreamSupport.stream(actual.spliterator(), false).toList();
assertEquals(expected.size(), actualList.size(), "size");
for (int i = 0; i < expected.size(); i++) {
assertSubmap(expected.get(i), TestUtils.toMap(actualList.get(i), zoom));
}
}
@TestFactory
public List<DynamicTest> mountainPeakProcessing() {
wikidataTranslations.put(123, "es", "es wd name");
return List.of(
dynamicTest("happy path", () -> {
var peak = process(pointFeature(Map.of(
"natural", "peak",
"name", "test",
"ele", "100",
"wikidata", "Q123"
)));
assertFeatures(14, List.of(Map.of(
"class", "peak",
"ele", 100,
"ele_ft", 328,
"_layer", "mountain_peak",
"_type", "point",
"_minzoom", 7,
"_maxzoom", 14,
"_buffer", 64d
)), peak);
assertFeatures(14, List.of(Map.of(
"name:latin", "test",
"name", "test",
"name:es", "es wd name"
)), peak);
}),
dynamicTest("labelgrid", () -> {
var peak = process(pointFeature(Map.of(
"natural", "peak",
"ele", "100"
)));
assertFeatures(14, List.of(Map.of(
"_labelgrid_limit", 0
)), peak);
assertFeatures(13, List.of(Map.of(
"_labelgrid_limit", 5,
"_labelgrid_size", 100d
)), peak);
}),
dynamicTest("volcano", () ->
assertFeatures(14, List.of(Map.of(
"class", "volcano"
)), process(pointFeature(Map.of(
"natural", "volcano",
"ele", "100"
))))),
dynamicTest("no elevation", () ->
assertFeatures(14, List.of(), process(pointFeature(Map.of(
"natural", "volcano"
))))),
dynamicTest("bogus elevation", () ->
assertFeatures(14, List.of(), process(pointFeature(Map.of(
"natural", "volcano",
"ele", "11000"
))))),
dynamicTest("ignore lines", () ->
assertFeatures(14, List.of(), process(lineFeature(Map.of(
"natural", "peak",
"name", "name",
"ele", "100"
))))),
dynamicTest("zorder", () -> {
assertFeatures(14, List.of(Map.of(
"_zorder", 100
)), process(pointFeature(Map.of(
"natural", "peak",
"ele", "100"
))));
assertFeatures(14, List.of(Map.of(
"_zorder", 10100
)), process(pointFeature(Map.of(
"natural", "peak",
"name", "name",
"ele", "100"
))));
assertFeatures(14, List.of(Map.of(
"_zorder", 20100
)), process(pointFeature(Map.of(
"natural", "peak",
"name", "name",
"wikipedia", "wikilink",
"ele", "100"
))));
})
);
}
@Test
public void testMountainPeakPostProcessing() throws GeometryException {
assertEquals(List.of(), profile.postProcessLayerFeatures(MountainPeak.LAYER_NAME, 13, List.of()));
assertEquals(List.of(pointFeature(
MountainPeak.LAYER_NAME,
Map.of("rank", 1),
1
)), profile.postProcessLayerFeatures(MountainPeak.LAYER_NAME, 13, List.of(pointFeature(
MountainPeak.LAYER_NAME,
Map.of(),
1
))));
assertEquals(List.of(
pointFeature(
MountainPeak.LAYER_NAME,
Map.of("rank", 2, "name", "a"),
1
), pointFeature(
MountainPeak.LAYER_NAME,
Map.of("rank", 1, "name", "b"),
1
), pointFeature(
MountainPeak.LAYER_NAME,
Map.of("rank", 1, "name", "c"),
2
)
), profile.postProcessLayerFeatures(MountainPeak.LAYER_NAME, 13, List.of(
pointFeature(
MountainPeak.LAYER_NAME,
Map.of("name", "a"),
1
),
pointFeature(
MountainPeak.LAYER_NAME,
Map.of("name", "b"),
1
),
pointFeature(
MountainPeak.LAYER_NAME,
Map.of("name", "c"),
2
)
)));
}
@TestFactory
public List<DynamicTest> aerodromeLabel() {
wikidataTranslations.put(123, "es", "es wd name");
return List.of(
dynamicTest("happy path point", () -> {
assertFeatures(14, List.of(Map.of(
"class", "international",
"ele", 100,
"ele_ft", 328,
"name", "osm name",
"name:es", "es wd name",
"_layer", "aerodrome_label",
"_type", "point",
"_minzoom", 10,
"_maxzoom", 14,
"_buffer", 64d
)), process(pointFeature(Map.of(
"aeroway", "aerodrome",
"name", "osm name",
"wikidata", "Q123",
"ele", "100",
"aerodrome", "international",
"iata", "123",
"icao", "1234"
))));
}),
dynamicTest("international", () -> {
assertFeatures(14, List.of(Map.of(
"class", "international",
"_layer", "aerodrome_label"
)), process(pointFeature(Map.of(
"aeroway", "aerodrome",
"aerodrome_type", "international"
))));
}),
dynamicTest("public", () -> {
assertFeatures(14, List.of(Map.of(
"class", "public",
"_layer", "aerodrome_label"
)), process(pointFeature(Map.of(
"aeroway", "aerodrome",
"aerodrome_type", "public airport"
))));
assertFeatures(14, List.of(Map.of(
"class", "public",
"_layer", "aerodrome_label"
)), process(pointFeature(Map.of(
"aeroway", "aerodrome",
"aerodrome_type", "civil"
))));
}),
dynamicTest("military", () -> {
assertFeatures(14, List.of(Map.of(
"class", "military",
"_layer", "aerodrome_label"
)), process(pointFeature(Map.of(
"aeroway", "aerodrome",
"aerodrome_type", "military airport"
))));
assertFeatures(14, List.of(Map.of(
"class", "military",
"_layer", "aerodrome_label"
)), process(pointFeature(Map.of(
"aeroway", "aerodrome",
"military", "airfield"
))));
}),
dynamicTest("private", () -> {
assertFeatures(14, List.of(Map.of(
"class", "private",
"_layer", "aerodrome_label"
)), process(pointFeature(Map.of(
"aeroway", "aerodrome",
"aerodrome_type", "private"
))));
assertFeatures(14, List.of(Map.of(
"class", "private",
"_layer", "aerodrome_label"
)), process(pointFeature(Map.of(
"aeroway", "aerodrome",
"aerodrome", "private"
))));
}),
dynamicTest("other", () -> {
assertFeatures(14, List.of(Map.of(
"class", "other",
"_layer", "aerodrome_label"
)), process(pointFeature(Map.of(
"aeroway", "aerodrome"
))));
}),
dynamicTest("ignore non-points", () -> {
assertFeatures(14, List.of(), process(lineFeature(Map.of(
"aeroway", "aerodrome"
))));
})
);
}
@Test
public void aerowayGate() {
assertFeatures(14, List.of(Map.of(
"class", "gate",
"ref", "123",
"_layer", "aeroway",
"_type", "point",
"_minzoom", 14,
"_maxzoom", 14,
"_buffer", 4d
)), process(pointFeature(Map.of(
"aeroway", "gate",
"ref", "123"
))));
assertFeatures(14, List.of(), process(lineFeature(Map.of(
"aeroway", "gate"
))));
assertFeatures(14, List.of(), process(polygonFeature(Map.of(
"aeroway", "gate"
))));
}
@Test
public void aerowayLine() {
assertFeatures(14, List.of(Map.of(
"class", "runway",
"ref", "123",
"_layer", "aeroway",
"_type", "line",
"_minzoom", 10,
"_maxzoom", 14,
"_buffer", 4d
)), process(lineFeature(Map.of(
"aeroway", "runway",
"ref", "123"
))));
assertFeatures(14, List.of(), process(pointFeature(Map.of(
"aeroway", "runway"
))));
}
@Test
public void aerowayPolygon() {
assertFeatures(14, List.of(Map.of(
"class", "runway",
"ref", "123",
"_layer", "aeroway",
"_type", "polygon",
"_minzoom", 10,
"_maxzoom", 14,
"_buffer", 4d
)), process(polygonFeature(Map.of(
"aeroway", "runway",
"ref", "123"
))));
assertFeatures(14, List.of(Map.of(
"class", "runway",
"ref", "123",
"_layer", "aeroway",
"_type", "polygon"
)), process(polygonFeature(Map.of(
"area:aeroway", "runway",
"ref", "123"
))));
assertFeatures(14, List.of(Map.of(
"class", "heliport",
"ref", "123",
"_layer", "aeroway",
"_type", "polygon"
)), process(polygonFeature(Map.of(
"aeroway", "heliport",
"ref", "123"
))));
assertFeatures(14, List.of(), process(lineFeature(Map.of(
"aeroway", "heliport"
))));
assertFeatures(14, List.of(), process(pointFeature(Map.of(
"aeroway", "heliport"
))));
}
@Test
public void testWaterwayImportantRiverProcess() {
var charlesRiver = process(lineFeature(Map.of(
"waterway", "river",
"name", "charles river",
"name:es", "es name"
)));
assertFeatures(14, List.of(Map.of(
"class", "river",
"name", "charles river",
"name:es", "es name",
"intermittent", 0,
"_layer", "waterway",
"_type", "line",
"_minzoom", 9,
"_maxzoom", 14,
"_buffer", 4d
)), charlesRiver);
assertFeatures(11, List.of(Map.of(
"class", "river",
"name", "charles river",
"name:es", "es name",
"intermittent", "<null>",
"_buffer", 13.082664546679323
)), charlesRiver);
assertFeatures(10, List.of(Map.of(
"class", "river",
"_buffer", 26.165329093358647
)), charlesRiver);
assertFeatures(9, List.of(Map.of(
"class", "river",
"_buffer", 26.165329093358647
)), charlesRiver);
}
@Test
public void testWaterwayImportantRiverPostProcess() throws GeometryException {
var line1 = new VectorTileEncoder.Feature(
Waterway.LAYER_NAME,
1,
VectorTileEncoder.encodeGeometry(newLineString(0, 0, 10, 0)),
Map.of("name", "river"),
0
);
var line2 = new VectorTileEncoder.Feature(
Waterway.LAYER_NAME,
1,
VectorTileEncoder.encodeGeometry(newLineString(10, 0, 20, 0)),
Map.of("name", "river"),
0
);
var connected = new VectorTileEncoder.Feature(
Waterway.LAYER_NAME,
1,
VectorTileEncoder.encodeGeometry(newLineString(00, 0, 20, 0)),
Map.of("name", "river"),
0
);
assertEquals(
List.of(),
profile.postProcessLayerFeatures(Waterway.LAYER_NAME, 11, List.of())
);
assertEquals(
List.of(line1, line2),
profile.postProcessLayerFeatures(Waterway.LAYER_NAME, 12, List.of(line1, line2))
);
assertEquals(
List.of(connected),
profile.postProcessLayerFeatures(Waterway.LAYER_NAME, 11, List.of(line1, line2))
);
}
@Test
public void testWaterwaySmaller() {
// river with no name is not important
assertFeatures(14, List.of(Map.of(
"class", "river",
"brunnel", "bridge",
"_layer", "waterway",
"_type", "line",
"_minzoom", 12
)), process(lineFeature(Map.of(
"waterway", "river",
"bridge", "1"
))));
assertFeatures(14, List.of(Map.of(
"class", "canal",
"_layer", "waterway",
"_type", "line",
"_minzoom", 12
)), process(lineFeature(Map.of(
"waterway", "canal",
"name", "name"
))));
assertFeatures(14, List.of(Map.of(
"class", "stream",
"_layer", "waterway",
"_type", "line",
"_minzoom", 13
)), process(lineFeature(Map.of(
"waterway", "stream",
"name", "name"
))));
}
@Test
public void testWaterwayNaturalEarth() {
assertFeatures(3, List.of(Map.of(
"class", "river",
"name", "<null>",
"intermittent", "<null>",
"_layer", "waterway",
"_type", "line",
"_minzoom", 3,
"_maxzoom", 3
)), process(new ReaderFeature(
newLineString(0, 0, 1, 1),
Map.of(
"featurecla", "River",
"name", "name"
),
NATURAL_EARTH_SOURCE,
"ne_110m_rivers_lake_centerlines",
0
)));
assertFeatures(6, List.of(Map.of(
"class", "river",
"intermittent", "<null>",
"_layer", "waterway",
"_type", "line",
"_minzoom", 4,
"_maxzoom", 5
)), process(new ReaderFeature(
newLineString(0, 0, 1, 1),
Map.of(
"featurecla", "River",
"name", "name"
),
NATURAL_EARTH_SOURCE,
"ne_50m_rivers_lake_centerlines",
0
)));
assertFeatures(6, List.of(Map.of(
"class", "river",
"intermittent", "<null>",
"_layer", "waterway",
"_type", "line",
"_minzoom", 6,
"_maxzoom", 8
)), process(new ReaderFeature(
newLineString(0, 0, 1, 1),
Map.of(
"featurecla", "River",
"name", "name"
),
NATURAL_EARTH_SOURCE,
"ne_10m_rivers_lake_centerlines",
0
)));
}
@Test
public void testWaterNaturalEarth() {
assertFeatures(0, List.of(Map.of(
"class", "lake",
"intermittent", "<null>",
"_layer", "water",
"_type", "polygon",
"_minzoom", 0
)), process(new ReaderFeature(
rectangle(0, 10),
Map.of(),
NATURAL_EARTH_SOURCE,
"ne_110m_lakes",
0
)));
assertFeatures(0, List.of(Map.of(
"class", "ocean",
"intermittent", "<null>",
"_layer", "water",
"_type", "polygon",
"_minzoom", 0
)), process(new ReaderFeature(
rectangle(0, 10),
Map.of(),
NATURAL_EARTH_SOURCE,
"ne_110m_ocean",
0
)));
assertFeatures(6, List.of(Map.of(
"class", "lake",
"_layer", "water",
"_type", "polygon",
"_maxzoom", 5
)), process(new ReaderFeature(
rectangle(0, 10),
Map.of(),
NATURAL_EARTH_SOURCE,
"ne_10m_lakes",
0
)));
assertFeatures(6, List.of(Map.of(
"class", "ocean",
"_layer", "water",
"_type", "polygon",
"_maxzoom", 5
)), process(new ReaderFeature(
rectangle(0, 10),
Map.of(),
NATURAL_EARTH_SOURCE,
"ne_10m_ocean",
0
)));
}
@Test
public void testWaterOsmWaterPolygon() {
assertFeatures(0, List.of(Map.of(
"class", "ocean",
"intermittent", "<null>",
"_layer", "water",
"_type", "polygon",
"_minzoom", 6,
"_maxzoom", 14
)), process(new ReaderFeature(
rectangle(0, 10),
Map.of(),
WATER_POLYGON_SOURCE,
null,
0
)));
}
@Test
public void testWater() {
assertFeatures(14, List.of(Map.of(
"class", "lake",
"_layer", "water",
"_type", "polygon",
"_minzoom", 6,
"_maxzoom", 14
)), process(polygonFeature(Map.of(
"natural", "water",
"water", "reservoir"
))));
assertFeatures(14, List.of(Map.of(
"class", "lake",
"_layer", "water",
"_type", "polygon",
"_minzoom", 6,
"_maxzoom", 14
)), process(polygonFeature(Map.of(
"leisure", "swimming_pool"
))));
assertFeatures(14, List.of(), process(polygonFeature(Map.of(
"natural", "bay"
))));
assertFeatures(14, List.of(Map.of()), process(polygonFeature(Map.of(
"natural", "water"
))));
assertFeatures(14, List.of(), process(polygonFeature(Map.of(
"natural", "water",
"covered", "yes"
))));
assertFeatures(14, List.of(Map.of(
"class", "river",
"brunnel", "bridge",
"intermittent", 1,
"_layer", "water",
"_type", "polygon",
"_minzoom", 6,
"_maxzoom", 14
)), process(polygonFeature(Map.of(
"waterway", "stream",
"bridge", "1",
"intermittent", "1"
))));
assertFeatures(11, List.of(Map.of(
"class", "lake",
"brunnel", "<null>",
"intermittent", 0,
"_layer", "water",
"_type", "polygon",
"_minzoom", 6,
"_maxzoom", 14,
"_minpixelsize", 2d
)), process(polygonFeature(Map.of(
"landuse", "salt_pond",
"bridge", "1"
))));
}
@Test
public void testWaterNamePoint() {
assertFeatures(11, List.of(Map.of(
"_layer", "water"
), Map.of(
"class", "lake",
"name", "waterway",
"name:es", "waterway es",
"intermittent", 1,
"_layer", "water_name",
"_type", "point",
"_minzoom", 9,
"_maxzoom", 14
)), process(polygonFeatureWithArea(1, Map.of(
"name", "waterway",
"name:es", "waterway es",
"natural", "water",
"water", "pond",
"intermittent", "1"
))));
double z11area = Math.pow((GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d), 2) * Math.pow(2, 20 - 11);
assertFeatures(10, List.of(Map.of(
"_layer", "water"
), Map.of(
"_layer", "water_name",
"_type", "point",
"_minzoom", 11,
"_maxzoom", 14
)), process(polygonFeatureWithArea(z11area, Map.of(
"name", "waterway",
"natural", "water",
"water", "pond"
))));
}
@Test
public void testWaterNameLakeline() {
assertFeatures(11, List.of(), process(new ReaderFeature(
newLineString(0, 0, 1, 1),
new HashMap<>(Map.<String, Object>of(
"OSM_ID", -10
)),
LAKE_CENTERLINE_SOURCE,
null,
0
)));
assertFeatures(10, List.of(Map.of(
"_layer", "water"
), Map.of(
"name", "waterway",
"name:es", "waterway es",
"_layer", "water_name",
"_type", "line",
"_geom", new TestUtils.NormGeometry(GeoUtils.latLonToWorldCoords(newLineString(0, 0, 1, 1))),
"_minzoom", 9,
"_maxzoom", 14,
"_minpixelsize", "waterway".length() * 6d
)), process(new ReaderFeature(
GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))),
new HashMap<>(Map.<String, Object>of(
"name", "waterway",
"name:es", "waterway es",
"natural", "water",
"water", "pond"
)),
OSM_SOURCE,
null,
10
)));
}
@Test
public void testMarinePoint() {
assertFeatures(11, List.of(), process(new ReaderFeature(
newLineString(0, 0, 1, 1),
new HashMap<>(Map.<String, Object>of(
"scalerank", 10,
"name", "pacific ocean"
)),
NATURAL_EARTH_SOURCE,
"ne_10m_geography_marine_polys",
0
)));
// name match - use scale rank from NE
assertFeatures(10, List.of(Map.of(
"name", "Pacific",
"name:es", "Pacific es",
"_layer", "water_name",
"_type", "point",
"_minzoom", 10,
"_maxzoom", 14
)), process(pointFeature(Map.of(
"rank", 9,
"name", "Pacific",
"name:es", "Pacific es",
"place", "sea"
))));
// name match but ocean - use min zoom=0
assertFeatures(10, List.of(Map.of(
"_layer", "water_name",
"_type", "point",
"_minzoom", 0,
"_maxzoom", 14
)), process(pointFeature(Map.of(
"rank", 9,
"name", "Pacific",
"place", "ocean"
))));
// no name match - use OSM rank
assertFeatures(10, List.of(Map.of(
"_layer", "water_name",
"_type", "point",
"_minzoom", 9,
"_maxzoom", 14
)), process(pointFeature(Map.of(
"rank", 9,
"name", "Atlantic",
"place", "sea"
))));
// no rank at all, default to 8
assertFeatures(10, List.of(Map.of(
"_layer", "water_name",
"_type", "point",
"_minzoom", 8,
"_maxzoom", 14
)), process(pointFeature(Map.of(
"name", "Atlantic",
"place", "sea"
))));
}
@Test
public void testHousenumber() {
assertFeatures(14, List.of(Map.of(
"_layer", "housenumber",
"_type", "point",
"_minzoom", 14,
"_maxzoom", 14,
"_buffer", 8d
)), process(pointFeature(Map.of(
"addr:housenumber", "10"
))));
assertFeatures(15, List.of(Map.of(
"_layer", "housenumber",
"_type", "point",
"_minzoom", 14,
"_maxzoom", 14,
"_buffer", 8d
)), process(polygonFeature(Map.of(
"addr:housenumber", "10"
))));
}
@Test
public void testCaresAboutWikidata() {
@ -864,54 +28,4 @@ public class OpenMaptilesProfileTest {
node.setTag("aeroway", "other");
assertFalse(profile.caresAboutWikidataTranslation(node));
}
private VectorTileEncoder.Feature pointFeature(String layer, Map<String, Object> map, int group) {
return new VectorTileEncoder.Feature(
layer,
1,
VectorTileEncoder.encodeGeometry(newPoint(0, 0)),
new HashMap<>(map),
group
);
}
private FeatureCollector process(SourceFeature feature) {
var collector = featureCollectorFactory.get(feature);
profile.processFeature(feature, collector);
return collector;
}
private SourceFeature pointFeature(Map<String, Object> props) {
return new ReaderFeature(
newPoint(0, 0),
new HashMap<>(props),
OSM_SOURCE,
null,
0
);
}
private SourceFeature lineFeature(Map<String, Object> props) {
return new ReaderFeature(
newLineString(0, 0, 1, 1),
new HashMap<>(props),
OSM_SOURCE,
null,
0
);
}
private SourceFeature polygonFeatureWithArea(double area, Map<String, Object> props) {
return new ReaderFeature(
GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(area))),
new HashMap<>(props),
OSM_SOURCE,
null,
0
);
}
private SourceFeature polygonFeature(Map<String, Object> props) {
return polygonFeatureWithArea(1, props);
}
}

Wyświetl plik

@ -0,0 +1,118 @@
package com.onthegomap.flatmap.openmaptiles.layers;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
public class AerodromeLabelTest extends BaseLayerTest {
@TestFactory
public List<DynamicTest> aerodromeLabel() {
wikidataTranslations.put(123, "es", "es wd name");
return List.of(
dynamicTest("happy path point", () -> {
assertFeatures(14, List.of(Map.of(
"class", "international",
"ele", 100,
"ele_ft", 328,
"name", "osm name",
"name:es", "es wd name",
"_layer", "aerodrome_label",
"_type", "point",
"_minzoom", 10,
"_maxzoom", 14,
"_buffer", 64d
)), process(pointFeature(Map.of(
"aeroway", "aerodrome",
"name", "osm name",
"wikidata", "Q123",
"ele", "100",
"aerodrome", "international",
"iata", "123",
"icao", "1234"
))));
}),
dynamicTest("international", () -> {
assertFeatures(14, List.of(Map.of(
"class", "international",
"_layer", "aerodrome_label"
)), process(pointFeature(Map.of(
"aeroway", "aerodrome",
"aerodrome_type", "international"
))));
}),
dynamicTest("public", () -> {
assertFeatures(14, List.of(Map.of(
"class", "public",
"_layer", "aerodrome_label"
)), process(pointFeature(Map.of(
"aeroway", "aerodrome",
"aerodrome_type", "public airport"
))));
assertFeatures(14, List.of(Map.of(
"class", "public",
"_layer", "aerodrome_label"
)), process(pointFeature(Map.of(
"aeroway", "aerodrome",
"aerodrome_type", "civil"
))));
}),
dynamicTest("military", () -> {
assertFeatures(14, List.of(Map.of(
"class", "military",
"_layer", "aerodrome_label"
)), process(pointFeature(Map.of(
"aeroway", "aerodrome",
"aerodrome_type", "military airport"
))));
assertFeatures(14, List.of(Map.of(
"class", "military",
"_layer", "aerodrome_label"
)), process(pointFeature(Map.of(
"aeroway", "aerodrome",
"military", "airfield"
))));
}),
dynamicTest("private", () -> {
assertFeatures(14, List.of(Map.of(
"class", "private",
"_layer", "aerodrome_label"
)), process(pointFeature(Map.of(
"aeroway", "aerodrome",
"aerodrome_type", "private"
))));
assertFeatures(14, List.of(Map.of(
"class", "private",
"_layer", "aerodrome_label"
)), process(pointFeature(Map.of(
"aeroway", "aerodrome",
"aerodrome", "private"
))));
}),
dynamicTest("other", () -> {
assertFeatures(14, List.of(Map.of(
"class", "other",
"_layer", "aerodrome_label"
)), process(pointFeature(Map.of(
"aeroway", "aerodrome"
))));
}),
dynamicTest("ignore non-points", () -> {
assertFeatures(14, List.of(), process(lineFeature(Map.of(
"aeroway", "aerodrome"
))));
})
);
}
}

Wyświetl plik

@ -0,0 +1,92 @@
package com.onthegomap.flatmap.openmaptiles.layers;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
public class AerowayTest extends BaseLayerTest {
@Test
public void aerowayGate() {
assertFeatures(14, List.of(Map.of(
"class", "gate",
"ref", "123",
"_layer", "aeroway",
"_type", "point",
"_minzoom", 14,
"_maxzoom", 14,
"_buffer", 4d
)), process(pointFeature(Map.of(
"aeroway", "gate",
"ref", "123"
))));
assertFeatures(14, List.of(), process(lineFeature(Map.of(
"aeroway", "gate"
))));
assertFeatures(14, List.of(), process(polygonFeature(Map.of(
"aeroway", "gate"
))));
}
@Test
public void aerowayLine() {
assertFeatures(14, List.of(Map.of(
"class", "runway",
"ref", "123",
"_layer", "aeroway",
"_type", "line",
"_minzoom", 10,
"_maxzoom", 14,
"_buffer", 4d
)), process(lineFeature(Map.of(
"aeroway", "runway",
"ref", "123"
))));
assertFeatures(14, List.of(), process(pointFeature(Map.of(
"aeroway", "runway"
))));
}
@Test
public void aerowayPolygon() {
assertFeatures(14, List.of(Map.of(
"class", "runway",
"ref", "123",
"_layer", "aeroway",
"_type", "polygon",
"_minzoom", 10,
"_maxzoom", 14,
"_buffer", 4d
)), process(polygonFeature(Map.of(
"aeroway", "runway",
"ref", "123"
))));
assertFeatures(14, List.of(Map.of(
"class", "runway",
"ref", "123",
"_layer", "aeroway",
"_type", "polygon"
)), process(polygonFeature(Map.of(
"area:aeroway", "runway",
"ref", "123"
))));
assertFeatures(14, List.of(Map.of(
"class", "heliport",
"ref", "123",
"_layer", "aeroway",
"_type", "polygon"
)), process(polygonFeature(Map.of(
"aeroway", "heliport",
"ref", "123"
))));
assertFeatures(14, List.of(), process(lineFeature(Map.of(
"aeroway", "heliport"
))));
assertFeatures(14, List.of(), process(pointFeature(Map.of(
"aeroway", "heliport"
))));
}
}

Wyświetl plik

@ -0,0 +1,125 @@
package com.onthegomap.flatmap.openmaptiles.layers;
import static com.onthegomap.flatmap.TestUtils.assertSubmap;
import static com.onthegomap.flatmap.TestUtils.newLineString;
import static com.onthegomap.flatmap.TestUtils.newPoint;
import static com.onthegomap.flatmap.TestUtils.rectangle;
import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.OSM_SOURCE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import com.onthegomap.flatmap.Arguments;
import com.onthegomap.flatmap.CommonParams;
import com.onthegomap.flatmap.FeatureCollector;
import com.onthegomap.flatmap.SourceFeature;
import com.onthegomap.flatmap.TestUtils;
import com.onthegomap.flatmap.Translations;
import com.onthegomap.flatmap.VectorTileEncoder;
import com.onthegomap.flatmap.Wikidata;
import com.onthegomap.flatmap.geo.GeoUtils;
import com.onthegomap.flatmap.monitoring.Stats;
import com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile;
import com.onthegomap.flatmap.read.ReaderFeature;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.StreamSupport;
public abstract class BaseLayerTest {
final Wikidata.WikidataTranslations wikidataTranslations = new Wikidata.WikidataTranslations();
final Translations translations = Translations.defaultProvider(List.of("en", "es", "de"))
.addTranslationProvider(wikidataTranslations);
final CommonParams params = CommonParams.defaults();
final OpenMapTilesProfile profile = new OpenMapTilesProfile(translations, Arguments.of(),
new Stats.InMemory());
final Stats stats = new Stats.InMemory();
final FeatureCollector.Factory featureCollectorFactory = new FeatureCollector.Factory(params, stats);
static void assertFeatures(int zoom, List<Map<String, Object>> expected, Iterable<FeatureCollector.Feature> actual) {
List<FeatureCollector.Feature> actualList = StreamSupport.stream(actual.spliterator(), false).toList();
assertEquals(expected.size(), actualList.size(), "size");
for (int i = 0; i < expected.size(); i++) {
assertSubmap(expected.get(i), TestUtils.toMap(actualList.get(i), zoom));
}
}
VectorTileEncoder.Feature pointFeature(String layer, Map<String, Object> map, int group) {
return new VectorTileEncoder.Feature(
layer,
1,
VectorTileEncoder.encodeGeometry(newPoint(0, 0)),
new HashMap<>(map),
group
);
}
FeatureCollector process(SourceFeature feature) {
var collector = featureCollectorFactory.get(feature);
profile.processFeature(feature, collector);
return collector;
}
void assertCoversZoomRange(int minzoom, int maxzoom, String layer, FeatureCollector... featureCollectors) {
Map<?, ?>[] zooms = new Map[Math.max(15, maxzoom + 1)];
for (var features : featureCollectors) {
for (var feature : features) {
if (feature.getLayer().equals(layer)) {
for (int zoom = feature.getMinZoom(); zoom <= feature.getMaxZoom(); zoom++) {
Map<String, Object> map = TestUtils.toMap(feature, zoom);
if (zooms[zoom] != null) {
fail("Multiple features at z" + zoom + ":\n" + zooms[zoom] + "\n" + map);
}
zooms[zoom] = map;
}
}
}
}
for (int zoom = 0; zoom <= 14; zoom++) {
if (zoom < minzoom || zoom > maxzoom) {
if (zooms[zoom] != null) {
fail("Expected nothing at z" + zoom + " but found: " + zooms[zoom]);
}
} else {
if (zooms[zoom] == null) {
fail("No feature at z" + zoom);
}
}
}
}
SourceFeature pointFeature(Map<String, Object> props) {
return new ReaderFeature(
newPoint(0, 0),
new HashMap<>(props),
OSM_SOURCE,
null,
0
);
}
SourceFeature lineFeature(Map<String, Object> props) {
return new ReaderFeature(
newLineString(0, 0, 1, 1),
new HashMap<>(props),
OSM_SOURCE,
null,
0
);
}
SourceFeature polygonFeatureWithArea(double area, Map<String, Object> props) {
return new ReaderFeature(
GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(area))),
new HashMap<>(props),
OSM_SOURCE,
null,
0
);
}
SourceFeature polygonFeature(Map<String, Object> props) {
return polygonFeatureWithArea(1, props);
}
}

Wyświetl plik

@ -0,0 +1,603 @@
package com.onthegomap.flatmap.openmaptiles.layers;
import static com.onthegomap.flatmap.TestUtils.newLineString;
import static com.onthegomap.flatmap.TestUtils.rectangle;
import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.NATURAL_EARTH_SOURCE;
import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.OSM_SOURCE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import com.graphhopper.reader.ReaderRelation;
import com.onthegomap.flatmap.FeatureCollector;
import com.onthegomap.flatmap.VectorTileEncoder;
import com.onthegomap.flatmap.geo.GeoUtils;
import com.onthegomap.flatmap.geo.GeometryException;
import com.onthegomap.flatmap.read.OpenStreetMapReader;
import com.onthegomap.flatmap.read.ReaderFeature;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
public class BoundaryTest extends BaseLayerTest {
@Test
public void testNaturalEarthCountryBoundaries() {
assertCoversZoomRange(
0, 4, "boundary",
process(new ReaderFeature(
newLineString(0, 0, 1, 1),
Map.of(),
NATURAL_EARTH_SOURCE,
"ne_110m_admin_0_boundary_lines_land",
0
)),
process(new ReaderFeature(
newLineString(0, 0, 1, 1),
Map.of(),
NATURAL_EARTH_SOURCE,
"ne_50m_admin_0_boundary_lines_land",
1
)),
process(new ReaderFeature(
newLineString(0, 0, 1, 1),
Map.of(),
NATURAL_EARTH_SOURCE,
"ne_10m_admin_0_boundary_lines_land",
2
))
);
assertFeatures(0, List.of(Map.of(
"_layer", "boundary",
"_type", "line",
"disputed", 0,
"maritime", 0,
"admin_level", 2,
"_minzoom", 0,
"_buffer", 4d
)), process(new ReaderFeature(
newLineString(0, 0, 1, 1),
Map.of(
"featurecla", "International boundary (verify)"
),
NATURAL_EARTH_SOURCE,
"ne_110m_admin_0_boundary_lines_land",
0
)));
assertFeatures(0, List.of(Map.of(
"_layer", "boundary",
"_type", "line",
"disputed", 1,
"maritime", 0,
"admin_level", 2,
"_buffer", 4d
)), process(new ReaderFeature(
newLineString(0, 0, 1, 1),
Map.of(
"featurecla", "Disputed (please verify)"
),
NATURAL_EARTH_SOURCE,
"ne_110m_admin_0_boundary_lines_land",
0
)));
assertFeatures(0, List.of(Map.of(
"_layer", "boundary",
"_type", "line",
"admin_level", 2
)), process(new ReaderFeature(
newLineString(0, 0, 1, 1),
Map.of(
"featurecla", "International boundary (verify)"
),
NATURAL_EARTH_SOURCE,
"ne_50m_admin_0_boundary_lines_land",
0
)));
assertFeatures(0, List.of(Map.of(
"_layer", "boundary",
"_type", "line",
"admin_level", 2
)), process(new ReaderFeature(
newLineString(0, 0, 1, 1),
Map.of(
"featurecla", "International boundary (verify)"
),
NATURAL_EARTH_SOURCE,
"ne_10m_admin_0_boundary_lines_land",
0
)));
assertFeatures(0, List.of(), process(new ReaderFeature(
newLineString(0, 0, 1, 1),
Map.of(
"featurecla", "Lease Limit"
),
NATURAL_EARTH_SOURCE,
"ne_10m_admin_0_boundary_lines_land",
0
)));
}
@Test
public void testNaturalEarthStateBoundaries() {
assertFeatures(0, List.of(Map.of(
"_layer", "boundary",
"_type", "line",
"disputed", 0,
"maritime", 0,
"admin_level", 4,
"_minzoom", 1,
"_maxzoom", 4,
"_buffer", 4d
)), process(new ReaderFeature(
newLineString(0, 0, 1, 1),
Map.of(
"min_zoom", 7d
),
NATURAL_EARTH_SOURCE,
"ne_10m_admin_1_states_provinces_lines",
0
)));
assertFeatures(0, List.of(), process(new ReaderFeature(
newLineString(0, 0, 1, 1),
Map.of(
"min_zoom", 7.1d
),
NATURAL_EARTH_SOURCE,
"ne_10m_admin_1_states_provinces_lines",
0
)));
assertFeatures(0, List.of(), process(new ReaderFeature(
newLineString(0, 0, 1, 1),
Map.of(),
NATURAL_EARTH_SOURCE,
"ne_10m_admin_1_states_provinces_lines",
0
)));
}
@Test
public void testMergesDisconnectedLineFeatures() throws GeometryException {
var line1 = new VectorTileEncoder.Feature(
Boundary.LAYER_NAME,
1,
VectorTileEncoder.encodeGeometry(newLineString(0, 0, 10, 0)),
Map.of("admin_level", 2),
0
);
var line2 = new VectorTileEncoder.Feature(
Boundary.LAYER_NAME,
1,
VectorTileEncoder.encodeGeometry(newLineString(10, 0, 20, 0)),
Map.of("admin_level", 2),
0
);
var connected = new VectorTileEncoder.Feature(
Boundary.LAYER_NAME,
1,
VectorTileEncoder.encodeGeometry(newLineString(00, 0, 20, 0)),
Map.of("admin_level", 2),
0
);
assertEquals(
List.of(connected),
profile.postProcessLayerFeatures(Boundary.LAYER_NAME, 14, List.of(line1, line2))
);
assertEquals(
List.of(connected),
profile.postProcessLayerFeatures(Boundary.LAYER_NAME, 13, List.of(line1, line2))
);
}
@Test
public void testOsmTownBoundary() {
var relation = new ReaderRelation(1);
relation.setTag("type", "boundary");
relation.setTag("admin_level", "10");
relation.setTag("boundary", "administrative");
assertFeatures(14, List.of(Map.of(
"_layer", "boundary",
"_type", "line",
"disputed", 0,
"maritime", 0,
"admin_level", 10,
"_minzoom", 12,
"_maxzoom", 14,
"_buffer", 4d,
"_minpixelsize", 0d
)), process(lineFeatureWithRelation(
profile.preprocessOsmRelation(relation),
Map.of())));
}
@Test
public void testOsmBoundaryTakesMinAdminLevel() {
var relation1 = new ReaderRelation(1);
relation1.setTag("type", "boundary");
relation1.setTag("admin_level", "10");
relation1.setTag("name", "Town");
relation1.setTag("boundary", "administrative");
var relation2 = new ReaderRelation(2);
relation2.setTag("type", "boundary");
relation2.setTag("admin_level", "4");
relation2.setTag("name", "State");
relation2.setTag("boundary", "administrative");
assertFeatures(14, List.of(Map.of(
"_layer", "boundary",
"_type", "line",
"disputed", 0,
"maritime", 0,
"admin_level", 4
)), process(lineFeatureWithRelation(
Stream.concat(
profile.preprocessOsmRelation(relation2).stream(),
profile.preprocessOsmRelation(relation1).stream()
).toList(),
Map.of())));
}
@Test
public void testOsmBoundarySetsMaritimeFromWay() {
var relation1 = new ReaderRelation(1);
relation1.setTag("type", "boundary");
relation1.setTag("admin_level", "10");
relation1.setTag("boundary", "administrative");
assertFeatures(14, List.of(Map.of(
"maritime", 1
)), process(lineFeatureWithRelation(
profile.preprocessOsmRelation(relation1),
Map.of(
"maritime", "yes"
))
));
assertFeatures(14, List.of(Map.of(
"maritime", 1
)), process(lineFeatureWithRelation(
profile.preprocessOsmRelation(relation1),
Map.of(
"natural", "coastline"
))
));
assertFeatures(14, List.of(Map.of(
"maritime", 1
)), process(lineFeatureWithRelation(
profile.preprocessOsmRelation(relation1),
Map.of(
"boundary_type", "maritime"
))
));
}
@Test
public void testIgnoresProtectedAreas() {
var relation1 = new ReaderRelation(1);
relation1.setTag("type", "boundary");
relation1.setTag("admin_level", "10");
relation1.setTag("boundary", "protected_area");
assertNull(profile.preprocessOsmRelation(relation1));
}
@Test
public void testIgnoresProtectedAdminLevelOver10() {
var relation1 = new ReaderRelation(1);
relation1.setTag("type", "boundary");
relation1.setTag("admin_level", "11");
relation1.setTag("boundary", "administrative");
assertNull(profile.preprocessOsmRelation(relation1));
}
@Test
public void testOsmBoundaryDisputed() {
var relation = new ReaderRelation(1);
relation.setTag("type", "boundary");
relation.setTag("admin_level", "5");
relation.setTag("boundary", "administrative");
relation.setTag("disputed", "yes");
relation.setTag("name", "Border A - B");
relation.setTag("claimed_by", "A");
assertFeatures(14, List.of(Map.of(
"_layer", "boundary",
"_type", "line",
"disputed_name", "BorderA-B",
"claimed_by", "A",
"disputed", 1,
"maritime", 0,
"admin_level", 5
)), process(lineFeatureWithRelation(
profile.preprocessOsmRelation(relation),
Map.of())
));
}
@Test
public void testOsmBoundaryDisputedFromWay() {
var relation = new ReaderRelation(1);
relation.setTag("type", "boundary");
relation.setTag("admin_level", "5");
relation.setTag("boundary", "administrative");
assertFeatures(14, List.of(Map.of(
"_layer", "boundary",
"_type", "line",
"disputed", 1,
"maritime", 0,
"admin_level", 5
)), process(lineFeatureWithRelation(
profile.preprocessOsmRelation(relation),
Map.of(
"disputed", "yes"
))
));
assertFeatures(14, List.of(Map.of(
"_layer", "boundary",
"_type", "line",
"disputed", 1,
"maritime", 0,
"admin_level", 5,
"claimed_by", "A",
"disputed_name", "AB"
)), process(lineFeatureWithRelation(
profile.preprocessOsmRelation(relation),
Map.of(
"disputed", "yes",
"claimed_by", "A",
"name", "AB"
))
));
}
@Test
public void testCountryBoundaryEmittedIfNoName() {
var relation = new ReaderRelation(1);
relation.setTag("type", "boundary");
relation.setTag("admin_level", "2");
relation.setTag("boundary", "administrative");
assertFeatures(14, List.of(Map.of(
"_layer", "boundary",
"_type", "line",
"disputed", 0,
"maritime", 0,
"admin_level", 2
)), process(lineFeatureWithRelation(
profile.preprocessOsmRelation(relation),
Map.of())
));
}
@Test
public void testCountryLeftRightName() {
var country1 = new ReaderRelation(1);
country1.setTag("type", "boundary");
country1.setTag("admin_level", "2");
country1.setTag("boundary", "administrative");
country1.setTag("ISO3166-1:alpha3", "C1");
var country2 = new ReaderRelation(2);
country2.setTag("type", "boundary");
country2.setTag("admin_level", "2");
country2.setTag("boundary", "administrative");
country2.setTag("ISO3166-1:alpha3", "C2");
// shared edge
assertFeatures(14, List.of(), process(new ReaderFeature(
newLineString(0, 0, 0, 10),
Map.of(),
OSM_SOURCE,
null,
3,
Stream.concat(
profile.preprocessOsmRelation(country1).stream(),
profile.preprocessOsmRelation(country2).stream()
).map(r -> new OpenStreetMapReader.RelationMember<>("", r)).toList()
)
));
// other 2 edges of country 1
assertFeatures(14, List.of(), process(new ReaderFeature(
newLineString(0, 0, 5, 10),
Map.of(),
OSM_SOURCE,
null,
4,
profile.preprocessOsmRelation(country1).stream().map(r -> new OpenStreetMapReader.RelationMember<>("", r))
.toList()
)
));
assertFeatures(14, List.of(), process(new ReaderFeature(
newLineString(0, 10, 5, 10),
Map.of(),
OSM_SOURCE,
null,
4,
profile.preprocessOsmRelation(country1).stream().map(r -> new OpenStreetMapReader.RelationMember<>("", r))
.toList()
)
));
// other 2 edges of country 2
assertFeatures(14, List.of(), process(new ReaderFeature(
newLineString(0, 0, -5, 10),
Map.of(),
OSM_SOURCE,
null,
4,
profile.preprocessOsmRelation(country2).stream().map(r -> new OpenStreetMapReader.RelationMember<>("", r))
.toList()
)
));
assertFeatures(14, List.of(), process(new ReaderFeature(
newLineString(0, 10, -5, 10),
Map.of(),
OSM_SOURCE,
null,
4,
profile.preprocessOsmRelation(country2).stream().map(r -> new OpenStreetMapReader.RelationMember<>("", r))
.toList()
)
));
List<FeatureCollector.Feature> features = new ArrayList<>();
profile.finish(OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add);
assertEquals(3, features.size());
// ensure shared edge has country labels on right sides
var sharedEdge = features.stream()
.filter(c -> c.getAttrsAtZoom(0).containsKey("adm0_l") && c.getAttrsAtZoom(0).containsKey("adm0_r")).findFirst()
.get();
if (sharedEdge.getGeometry().getCoordinate().y == 0.5) { // going up
assertEquals("C1", sharedEdge.getAttrsAtZoom(0).get("adm0_r"));
assertEquals("C2", sharedEdge.getAttrsAtZoom(0).get("adm0_l"));
} else { // going down
assertEquals("C2", sharedEdge.getAttrsAtZoom(0).get("adm0_r"));
assertEquals("C1", sharedEdge.getAttrsAtZoom(0).get("adm0_l"));
}
var c1 = features.stream()
.filter(c -> c.getGeometry().getEnvelopeInternal().getMaxX() > 0.5).findFirst()
.get();
if (c1.getGeometry().getCoordinate().y == 0.5) { // going up
assertEquals("C1", c1.getAttrsAtZoom(0).get("adm0_l"));
} else { // going down
assertEquals("C1", c1.getAttrsAtZoom(0).get("adm0_r"));
}
var c2 = features.stream()
.filter(c -> c.getGeometry().getEnvelopeInternal().getMinX() < 0.5).findFirst()
.get();
if (c2.getGeometry().getCoordinate().y == 0.5) { // going up
assertEquals("C2", c2.getAttrsAtZoom(0).get("adm0_r"));
} else { // going down
assertEquals("C2", c2.getAttrsAtZoom(0).get("adm0_l"));
}
}
@Test
public void testCountryBoundaryNotClosed() {
var country1 = new ReaderRelation(1);
country1.setTag("type", "boundary");
country1.setTag("admin_level", "2");
country1.setTag("boundary", "administrative");
country1.setTag("ISO3166-1:alpha3", "C1");
// shared edge
assertFeatures(14, List.of(), process(new ReaderFeature(
newLineString(0, 0, 0, 10, 5, 5),
Map.of(),
OSM_SOURCE,
null,
3,
profile.preprocessOsmRelation(country1).stream().map(r -> new OpenStreetMapReader.RelationMember<>("", r))
.toList()
)));
List<FeatureCollector.Feature> features = new ArrayList<>();
profile.finish(OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add);
assertFeatures(0, List.of(Map.of(
"adm0_r", "<null>",
"adm0_l", "<null>",
"maritime", 0,
"disputed", 0,
"admin_level", 2,
"_layer", "boundary"
)), features);
}
@Test
public void testNestedCountry() throws GeometryException {
var country1 = new ReaderRelation(1);
country1.setTag("type", "boundary");
country1.setTag("admin_level", "2");
country1.setTag("boundary", "administrative");
country1.setTag("ISO3166-1:alpha3", "C1");
assertFeatures(14, List.of(), process(new ReaderFeature(
GeoUtils.polygonToLineString(rectangle(0, 10)),
Map.of(),
OSM_SOURCE,
null,
3,
profile.preprocessOsmRelation(country1).stream().map(r -> new OpenStreetMapReader.RelationMember<>("", r))
.toList()
)));
assertFeatures(14, List.of(), process(new ReaderFeature(
GeoUtils.polygonToLineString(rectangle(1, 9)),
Map.of(),
OSM_SOURCE,
null,
3,
profile.preprocessOsmRelation(country1).stream().map(r -> new OpenStreetMapReader.RelationMember<>("", r))
.toList()
)));
List<FeatureCollector.Feature> features = new ArrayList<>();
profile.finish(OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add);
assertFeatures(0, List.of(Map.of(
"adm0_l", "C1",
"adm0_r", "<null>"
), Map.of(
"adm0_r", "C1",
"adm0_l", "<null>"
)), features);
}
@Test
public void testDontLabelBadPolygon() throws GeometryException {
var country1 = new ReaderRelation(1);
country1.setTag("type", "boundary");
country1.setTag("admin_level", "2");
country1.setTag("boundary", "administrative");
country1.setTag("ISO3166-1:alpha3", "C1");
assertFeatures(14, List.of(), process(new ReaderFeature(
GeoUtils.worldToLatLonCoords(newLineString(0, 0, 10, 0, 10, 10, 2, 10, 2, -2)),
Map.of(),
OSM_SOURCE,
null,
3,
profile.preprocessOsmRelation(country1).stream().map(r -> new OpenStreetMapReader.RelationMember<>("", r))
.toList()
)));
List<FeatureCollector.Feature> features = new ArrayList<>();
profile.finish(OSM_SOURCE, new FeatureCollector.Factory(params, stats), features::add);
assertFeatures(0, List.of(Map.of(
"adm0_l", "<null>",
"adm0_r", "<null>"
)), features);
}
@NotNull
private ReaderFeature lineFeatureWithRelation(List<OpenStreetMapReader.RelationInfo> relationInfos,
Map<String, Object> map) {
return new ReaderFeature(
newLineString(0, 0, 1, 1),
map,
OSM_SOURCE,
null,
0,
(relationInfos == null ? List.<OpenStreetMapReader.RelationInfo>of() : relationInfos).stream()
.map(r -> new OpenStreetMapReader.RelationMember<>("", r)).toList()
);
}
}

Wyświetl plik

@ -0,0 +1,30 @@
package com.onthegomap.flatmap.openmaptiles.layers;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
public class HousenumberTest extends BaseLayerTest {
@Test
public void testHousenumber() {
assertFeatures(14, List.of(Map.of(
"_layer", "housenumber",
"_type", "point",
"_minzoom", 14,
"_maxzoom", 14,
"_buffer", 8d
)), process(pointFeature(Map.of(
"addr:housenumber", "10"
))));
assertFeatures(14, List.of(Map.of(
"_layer", "housenumber",
"_type", "point",
"_minzoom", 14,
"_maxzoom", 14,
"_buffer", 8d
)), process(polygonFeature(Map.of(
"addr:housenumber", "10"
))));
}
}

Wyświetl plik

@ -0,0 +1,156 @@
package com.onthegomap.flatmap.openmaptiles.layers;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import com.onthegomap.flatmap.geo.GeometryException;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
public class MountainPeakTest extends BaseLayerTest {
@TestFactory
public List<DynamicTest> mountainPeakProcessing() {
wikidataTranslations.put(123, "es", "es wd name");
return List.of(
dynamicTest("happy path", () -> {
var peak = process(pointFeature(Map.of(
"natural", "peak",
"name", "test",
"ele", "100",
"wikidata", "Q123"
)));
assertFeatures(14, List.of(Map.of(
"class", "peak",
"ele", 100,
"ele_ft", 328,
"_layer", "mountain_peak",
"_type", "point",
"_minzoom", 7,
"_maxzoom", 14,
"_buffer", 64d
)), peak);
assertFeatures(14, List.of(Map.of(
"name:latin", "test",
"name", "test",
"name:es", "es wd name"
)), peak);
}),
dynamicTest("labelgrid", () -> {
var peak = process(pointFeature(Map.of(
"natural", "peak",
"ele", "100"
)));
assertFeatures(14, List.of(Map.of(
"_labelgrid_limit", 0
)), peak);
assertFeatures(13, List.of(Map.of(
"_labelgrid_limit", 5,
"_labelgrid_size", 100d
)), peak);
}),
dynamicTest("volcano", () ->
assertFeatures(14, List.of(Map.of(
"class", "volcano"
)), process(pointFeature(Map.of(
"natural", "volcano",
"ele", "100"
))))),
dynamicTest("no elevation", () ->
assertFeatures(14, List.of(), process(pointFeature(Map.of(
"natural", "volcano"
))))),
dynamicTest("bogus elevation", () ->
assertFeatures(14, List.of(), process(pointFeature(Map.of(
"natural", "volcano",
"ele", "11000"
))))),
dynamicTest("ignore lines", () ->
assertFeatures(14, List.of(), process(lineFeature(Map.of(
"natural", "peak",
"name", "name",
"ele", "100"
))))),
dynamicTest("zorder", () -> {
assertFeatures(14, List.of(Map.of(
"_zorder", 100
)), process(pointFeature(Map.of(
"natural", "peak",
"ele", "100"
))));
assertFeatures(14, List.of(Map.of(
"_zorder", 10100
)), process(pointFeature(Map.of(
"natural", "peak",
"name", "name",
"ele", "100"
))));
assertFeatures(14, List.of(Map.of(
"_zorder", 20100
)), process(pointFeature(Map.of(
"natural", "peak",
"name", "name",
"wikipedia", "wikilink",
"ele", "100"
))));
})
);
}
@Test
public void testMountainPeakPostProcessing() throws GeometryException {
assertEquals(List.of(), profile.postProcessLayerFeatures(MountainPeak.LAYER_NAME, 13, List.of()));
assertEquals(List.of(pointFeature(
MountainPeak.LAYER_NAME,
Map.of("rank", 1),
1
)), profile.postProcessLayerFeatures(MountainPeak.LAYER_NAME, 13, List.of(pointFeature(
MountainPeak.LAYER_NAME,
Map.of(),
1
))));
assertEquals(List.of(
pointFeature(
MountainPeak.LAYER_NAME,
Map.of("rank", 2, "name", "a"),
1
), pointFeature(
MountainPeak.LAYER_NAME,
Map.of("rank", 1, "name", "b"),
1
), pointFeature(
MountainPeak.LAYER_NAME,
Map.of("rank", 1, "name", "c"),
2
)
), profile.postProcessLayerFeatures(MountainPeak.LAYER_NAME, 13, List.of(
pointFeature(
MountainPeak.LAYER_NAME,
Map.of("name", "a"),
1
),
pointFeature(
MountainPeak.LAYER_NAME,
Map.of("name", "b"),
1
),
pointFeature(
MountainPeak.LAYER_NAME,
Map.of("name", "c"),
2
)
)));
}
}

Wyświetl plik

@ -0,0 +1,155 @@
package com.onthegomap.flatmap.openmaptiles.layers;
import static com.onthegomap.flatmap.TestUtils.newLineString;
import static com.onthegomap.flatmap.TestUtils.rectangle;
import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.LAKE_CENTERLINE_SOURCE;
import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.NATURAL_EARTH_SOURCE;
import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.OSM_SOURCE;
import com.onthegomap.flatmap.TestUtils;
import com.onthegomap.flatmap.geo.GeoUtils;
import com.onthegomap.flatmap.read.ReaderFeature;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
public class WaterNameTest extends BaseLayerTest {
@Test
public void testWaterNamePoint() {
assertFeatures(11, List.of(Map.of(
"_layer", "water"
), Map.of(
"class", "lake",
"name", "waterway",
"name:es", "waterway es",
"intermittent", 1,
"_layer", "water_name",
"_type", "point",
"_minzoom", 9,
"_maxzoom", 14
)), process(polygonFeatureWithArea(1, Map.of(
"name", "waterway",
"name:es", "waterway es",
"natural", "water",
"water", "pond",
"intermittent", "1"
))));
double z11area = Math.pow((GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d), 2) * Math.pow(2, 20 - 11);
assertFeatures(10, List.of(Map.of(
"_layer", "water"
), Map.of(
"_layer", "water_name",
"_type", "point",
"_minzoom", 11,
"_maxzoom", 14
)), process(polygonFeatureWithArea(z11area, Map.of(
"name", "waterway",
"natural", "water",
"water", "pond"
))));
}
@Test
public void testWaterNameLakeline() {
assertFeatures(11, List.of(), process(new ReaderFeature(
newLineString(0, 0, 1, 1),
new HashMap<>(Map.<String, Object>of(
"OSM_ID", -10
)),
LAKE_CENTERLINE_SOURCE,
null,
0
)));
assertFeatures(10, List.of(Map.of(
"_layer", "water"
), Map.of(
"name", "waterway",
"name:es", "waterway es",
"_layer", "water_name",
"_type", "line",
"_geom", new TestUtils.NormGeometry(GeoUtils.latLonToWorldCoords(newLineString(0, 0, 1, 1))),
"_minzoom", 9,
"_maxzoom", 14,
"_minpixelsize", "waterway".length() * 6d
)), process(new ReaderFeature(
GeoUtils.worldToLatLonCoords(rectangle(0, Math.sqrt(1))),
new HashMap<>(Map.<String, Object>of(
"name", "waterway",
"name:es", "waterway es",
"natural", "water",
"water", "pond"
)),
OSM_SOURCE,
null,
10
)));
}
@Test
public void testMarinePoint() {
assertFeatures(11, List.of(), process(new ReaderFeature(
newLineString(0, 0, 1, 1),
new HashMap<>(Map.<String, Object>of(
"scalerank", 10,
"name", "pacific ocean"
)),
NATURAL_EARTH_SOURCE,
"ne_10m_geography_marine_polys",
0
)));
// name match - use scale rank from NE
assertFeatures(10, List.of(Map.of(
"name", "Pacific",
"name:es", "Pacific es",
"_layer", "water_name",
"_type", "point",
"_minzoom", 10,
"_maxzoom", 14
)), process(pointFeature(Map.of(
"rank", 9,
"name", "Pacific",
"name:es", "Pacific es",
"place", "sea"
))));
// name match but ocean - use min zoom=0
assertFeatures(10, List.of(Map.of(
"_layer", "water_name",
"_type", "point",
"_minzoom", 0,
"_maxzoom", 14
)), process(pointFeature(Map.of(
"rank", 9,
"name", "Pacific",
"place", "ocean"
))));
// no name match - use OSM rank
assertFeatures(10, List.of(Map.of(
"_layer", "water_name",
"_type", "point",
"_minzoom", 9,
"_maxzoom", 14
)), process(pointFeature(Map.of(
"rank", 9,
"name", "Atlantic",
"place", "sea"
))));
// no rank at all, default to 8
assertFeatures(10, List.of(Map.of(
"_layer", "water_name",
"_type", "point",
"_minzoom", 8,
"_maxzoom", 14
)), process(pointFeature(Map.of(
"name", "Atlantic",
"place", "sea"
))));
}
}

Wyświetl plik

@ -0,0 +1,223 @@
package com.onthegomap.flatmap.openmaptiles.layers;
import static com.onthegomap.flatmap.TestUtils.rectangle;
import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.NATURAL_EARTH_SOURCE;
import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.OSM_SOURCE;
import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.WATER_POLYGON_SOURCE;
import com.onthegomap.flatmap.read.ReaderFeature;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
public class WaterTest extends BaseLayerTest {
@Test
public void testWaterNaturalEarth() {
assertFeatures(0, List.of(Map.of(
"class", "lake",
"intermittent", "<null>",
"_layer", "water",
"_type", "polygon",
"_minzoom", 0
)), process(new ReaderFeature(
rectangle(0, 10),
Map.of(),
NATURAL_EARTH_SOURCE,
"ne_110m_lakes",
0
)));
assertFeatures(0, List.of(Map.of(
"class", "ocean",
"intermittent", "<null>",
"_layer", "water",
"_type", "polygon",
"_minzoom", 0
)), process(new ReaderFeature(
rectangle(0, 10),
Map.of(),
NATURAL_EARTH_SOURCE,
"ne_110m_ocean",
0
)));
assertFeatures(6, List.of(Map.of(
"class", "lake",
"_layer", "water",
"_type", "polygon",
"_maxzoom", 5
)), process(new ReaderFeature(
rectangle(0, 10),
Map.of(),
NATURAL_EARTH_SOURCE,
"ne_10m_lakes",
0
)));
assertFeatures(6, List.of(Map.of(
"class", "ocean",
"_layer", "water",
"_type", "polygon",
"_maxzoom", 5
)), process(new ReaderFeature(
rectangle(0, 10),
Map.of(),
NATURAL_EARTH_SOURCE,
"ne_10m_ocean",
0
)));
}
@Test
public void testWaterOsmWaterPolygon() {
assertFeatures(0, List.of(Map.of(
"class", "ocean",
"intermittent", "<null>",
"_layer", "water",
"_type", "polygon",
"_minzoom", 6,
"_maxzoom", 14
)), process(new ReaderFeature(
rectangle(0, 10),
Map.of(),
WATER_POLYGON_SOURCE,
null,
0
)));
}
@Test
public void testWater() {
assertFeatures(14, List.of(Map.of(
"class", "lake",
"_layer", "water",
"_type", "polygon",
"_minzoom", 6,
"_maxzoom", 14
)), process(polygonFeature(Map.of(
"natural", "water",
"water", "reservoir"
))));
assertFeatures(14, List.of(Map.of(
"class", "lake",
"_layer", "water",
"_type", "polygon",
"_minzoom", 6,
"_maxzoom", 14
)), process(polygonFeature(Map.of(
"leisure", "swimming_pool"
))));
assertFeatures(14, List.of(), process(polygonFeature(Map.of(
"natural", "bay"
))));
assertFeatures(14, List.of(Map.of()), process(polygonFeature(Map.of(
"natural", "water"
))));
assertFeatures(14, List.of(), process(polygonFeature(Map.of(
"natural", "water",
"covered", "yes"
))));
assertFeatures(14, List.of(Map.of(
"class", "river",
"brunnel", "bridge",
"intermittent", 1,
"_layer", "water",
"_type", "polygon",
"_minzoom", 6,
"_maxzoom", 14
)), process(polygonFeature(Map.of(
"waterway", "stream",
"bridge", "1",
"intermittent", "1"
))));
assertFeatures(11, List.of(Map.of(
"class", "lake",
"brunnel", "<null>",
"intermittent", 0,
"_layer", "water",
"_type", "polygon",
"_minzoom", 6,
"_maxzoom", 14,
"_minpixelsize", 2d
)), process(polygonFeature(Map.of(
"landuse", "salt_pond",
"bridge", "1"
))));
}
@Test
public void testOceanZoomLevels() {
assertCoversZoomRange(0, 14, "water",
process(new ReaderFeature(
rectangle(0, 10),
Map.of(),
NATURAL_EARTH_SOURCE,
"ne_110m_ocean",
0
)),
process(new ReaderFeature(
rectangle(0, 10),
Map.of(),
NATURAL_EARTH_SOURCE,
"ne_50m_ocean",
0
)),
process(new ReaderFeature(
rectangle(0, 10),
Map.of(),
NATURAL_EARTH_SOURCE,
"ne_10m_ocean",
0
)),
process(new ReaderFeature(
rectangle(0, 10),
Map.of(),
WATER_POLYGON_SOURCE,
null,
0
))
);
}
@Test
public void testLakeZoomLevels() {
assertCoversZoomRange(0, 14, "water",
process(new ReaderFeature(
rectangle(0, 10),
Map.of(),
NATURAL_EARTH_SOURCE,
"ne_110m_lakes",
0
)),
process(new ReaderFeature(
rectangle(0, 10),
Map.of(),
NATURAL_EARTH_SOURCE,
"ne_50m_lakes",
0
)),
process(new ReaderFeature(
rectangle(0, 10),
Map.of(),
NATURAL_EARTH_SOURCE,
"ne_10m_lakes",
0
)),
process(new ReaderFeature(
rectangle(0, 10),
Map.of(
"natural", "water",
"water", "reservoir"
),
OSM_SOURCE,
null,
0
))
);
}
}

Wyświetl plik

@ -0,0 +1,186 @@
package com.onthegomap.flatmap.openmaptiles.layers;
import static com.onthegomap.flatmap.TestUtils.newLineString;
import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.NATURAL_EARTH_SOURCE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.onthegomap.flatmap.VectorTileEncoder;
import com.onthegomap.flatmap.geo.GeometryException;
import com.onthegomap.flatmap.read.ReaderFeature;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
public class WaterwayTest extends BaseLayerTest {
@Test
public void testWaterwayImportantRiverProcess() {
var charlesRiver = process(lineFeature(Map.of(
"waterway", "river",
"name", "charles river",
"name:es", "es name"
)));
assertFeatures(14, List.of(Map.of(
"class", "river",
"name", "charles river",
"name:es", "es name",
"intermittent", 0,
"_layer", "waterway",
"_type", "line",
"_minzoom", 9,
"_maxzoom", 14,
"_buffer", 4d
)), charlesRiver);
assertFeatures(11, List.of(Map.of(
"class", "river",
"name", "charles river",
"name:es", "es name",
"intermittent", "<null>",
"_buffer", 13.082664546679323
)), charlesRiver);
assertFeatures(10, List.of(Map.of(
"class", "river",
"_buffer", 26.165329093358647
)), charlesRiver);
assertFeatures(9, List.of(Map.of(
"class", "river",
"_buffer", 26.165329093358647
)), charlesRiver);
}
@Test
public void testWaterwayImportantRiverPostProcess() throws GeometryException {
var line1 = new VectorTileEncoder.Feature(
Waterway.LAYER_NAME,
1,
VectorTileEncoder.encodeGeometry(newLineString(0, 0, 10, 0)),
Map.of("name", "river"),
0
);
var line2 = new VectorTileEncoder.Feature(
Waterway.LAYER_NAME,
1,
VectorTileEncoder.encodeGeometry(newLineString(10, 0, 20, 0)),
Map.of("name", "river"),
0
);
var connected = new VectorTileEncoder.Feature(
Waterway.LAYER_NAME,
1,
VectorTileEncoder.encodeGeometry(newLineString(00, 0, 20, 0)),
Map.of("name", "river"),
0
);
assertEquals(
List.of(),
profile.postProcessLayerFeatures(Waterway.LAYER_NAME, 11, List.of())
);
assertEquals(
List.of(line1, line2),
profile.postProcessLayerFeatures(Waterway.LAYER_NAME, 12, List.of(line1, line2))
);
assertEquals(
List.of(connected),
profile.postProcessLayerFeatures(Waterway.LAYER_NAME, 11, List.of(line1, line2))
);
}
@Test
public void testWaterwaySmaller() {
// river with no name is not important
assertFeatures(14, List.of(Map.of(
"class", "river",
"brunnel", "bridge",
"_layer", "waterway",
"_type", "line",
"_minzoom", 12
)), process(lineFeature(Map.of(
"waterway", "river",
"bridge", "1"
))));
assertFeatures(14, List.of(Map.of(
"class", "canal",
"_layer", "waterway",
"_type", "line",
"_minzoom", 12
)), process(lineFeature(Map.of(
"waterway", "canal",
"name", "name"
))));
assertFeatures(14, List.of(Map.of(
"class", "stream",
"_layer", "waterway",
"_type", "line",
"_minzoom", 13
)), process(lineFeature(Map.of(
"waterway", "stream",
"name", "name"
))));
}
@Test
public void testWaterwayNaturalEarth() {
assertFeatures(3, List.of(Map.of(
"class", "river",
"name", "<null>",
"intermittent", "<null>",
"_layer", "waterway",
"_type", "line",
"_minzoom", 3,
"_maxzoom", 3
)), process(new ReaderFeature(
newLineString(0, 0, 1, 1),
Map.of(
"featurecla", "River",
"name", "name"
),
NATURAL_EARTH_SOURCE,
"ne_110m_rivers_lake_centerlines",
0
)));
assertFeatures(6, List.of(Map.of(
"class", "river",
"intermittent", "<null>",
"_layer", "waterway",
"_type", "line",
"_minzoom", 4,
"_maxzoom", 5
)), process(new ReaderFeature(
newLineString(0, 0, 1, 1),
Map.of(
"featurecla", "River",
"name", "name"
),
NATURAL_EARTH_SOURCE,
"ne_50m_rivers_lake_centerlines",
0
)));
assertFeatures(6, List.of(Map.of(
"class", "river",
"intermittent", "<null>",
"_layer", "waterway",
"_type", "line",
"_minzoom", 6,
"_maxzoom", 8
)), process(new ReaderFeature(
newLineString(0, 0, 1, 1),
Map.of(
"featurecla", "River",
"name", "name"
),
NATURAL_EARTH_SOURCE,
"ne_10m_rivers_lake_centerlines",
0
)));
}
}