From 0b76962f2a2b850d2ec19915e482fb6d875db11f Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Thu, 15 Jul 2021 21:48:58 -0400 Subject: [PATCH] poi tests --- .../flatmap/openmaptiles/layers/Poi.java | 160 +++++++++++++++++- .../flatmap/openmaptiles/layers/PoiTest.java | 156 +++++++++++++++++ 2 files changed, 310 insertions(+), 6 deletions(-) create mode 100644 openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/PoiTest.java diff --git a/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/layers/Poi.java b/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/layers/Poi.java index bb0ba276..51229507 100644 --- a/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/layers/Poi.java +++ b/openmaptiles/src/main/java/com/onthegomap/flatmap/openmaptiles/layers/Poi.java @@ -1,14 +1,162 @@ 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.openmaptiles.Utils.coalesce; +import static com.onthegomap.flatmap.openmaptiles.Utils.nullIf; +import static com.onthegomap.flatmap.openmaptiles.Utils.nullIfEmpty; +import static com.onthegomap.flatmap.openmaptiles.Utils.nullOrEmpty; +import static java.util.Map.entry; -public class Poi implements OpenMapTilesSchema.Poi { +import com.carrotsearch.hppc.LongIntHashMap; +import com.carrotsearch.hppc.LongIntMap; +import com.onthegomap.flatmap.Arguments; +import com.onthegomap.flatmap.FeatureCollector; +import com.onthegomap.flatmap.Parse; +import com.onthegomap.flatmap.Translations; +import com.onthegomap.flatmap.VectorTileEncoder; +import com.onthegomap.flatmap.geo.GeometryException; +import com.onthegomap.flatmap.monitoring.Stats; +import com.onthegomap.flatmap.openmaptiles.LanguageUtils; +import com.onthegomap.flatmap.openmaptiles.MultiExpression; +import com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile; +import com.onthegomap.flatmap.openmaptiles.generated.OpenMapTilesSchema; +import com.onthegomap.flatmap.openmaptiles.generated.Tables; +import java.util.List; +import java.util.Map; + +public class Poi implements OpenMapTilesSchema.Poi, + Tables.OsmPoiPoint.Handler, + Tables.OsmPoiPolygon.Handler, + OpenMapTilesProfile.FeaturePostProcessor { + + private final MultiExpression.MultiExpressionIndex classMapping; + private final Translations translations; public Poi(Translations translations, Arguments args, Stats stats) { + this.classMapping = FieldMappings.Class.index(); + this.translations = translations; } - // TODO implement + private String poiClass(String subclass, String mappingKey) { + subclass = coalesce(subclass, ""); + return classMapping.getOrElse(Map.of( + "subclass", subclass, + "mapping_key", coalesce(mappingKey, "") + ), subclass); + } + + private static final Map CLASS_RANKS = Map.ofEntries( + entry(FieldValues.CLASS_HOSPITAL, 20), + entry(FieldValues.CLASS_RAILWAY, 40), + entry(FieldValues.CLASS_BUS, 50), + entry(FieldValues.CLASS_ATTRACTION, 70), + entry(FieldValues.CLASS_HARBOR, 75), + entry(FieldValues.CLASS_COLLEGE, 80), + entry(FieldValues.CLASS_SCHOOL, 85), + entry(FieldValues.CLASS_STADIUM, 90), + entry("zoo", 95), + entry(FieldValues.CLASS_TOWN_HALL, 100), + entry(FieldValues.CLASS_CAMPSITE, 110), + entry(FieldValues.CLASS_CEMETERY, 115), + entry(FieldValues.CLASS_PARK, 120), + entry(FieldValues.CLASS_LIBRARY, 130), + entry("police", 135), + entry(FieldValues.CLASS_POST, 140), + entry(FieldValues.CLASS_GOLF, 150), + entry(FieldValues.CLASS_SHOP, 400), + entry(FieldValues.CLASS_GROCERY, 500), + entry(FieldValues.CLASS_FAST_FOOD, 600), + entry(FieldValues.CLASS_CLOTHING_STORE, 700), + entry(FieldValues.CLASS_BAR, 800) + ); + + private static int poiClassRank(String clazz) { + return CLASS_RANKS.getOrDefault(clazz, 1_000); + } + + private int minzoom(String subclass, String mappingKey) { + boolean lowZoom = ("station".equals(subclass) && "railway".equals(mappingKey)) || + "halt".equals(subclass) || "ferry_terminal".equals(subclass); + return lowZoom ? 12 : 14; + } + + @Override + public void process(Tables.OsmPoiPoint element, FeatureCollector features) { + String rawSubclass = element.subclass(); + if ("station".equals(rawSubclass) && "subway".equals(element.station())) { + rawSubclass = "subway"; + } + if ("station".equals(rawSubclass) && "yes".equals(element.funicular())) { + rawSubclass = "halt"; + } + + String subclass = switch (rawSubclass) { + case "information" -> nullIfEmpty(element.information()); + case "place_of_worship" -> nullIfEmpty(element.religion()); + case "pitch" -> nullIfEmpty(element.sport()); + default -> rawSubclass; + }; + String poiClass = poiClass(rawSubclass, element.mappingKey()); + int poiClassRank = poiClassRank(poiClass); + int rankOrder = poiClassRank + ((nullOrEmpty(element.name())) ? 2000 : 0); + + features.point(LAYER_NAME).setBufferPixels(BUFFER_SIZE) + .setAttr(Fields.CLASS, poiClass) + .setAttr(Fields.SUBCLASS, subclass) + .setAttr(Fields.LAYER, nullIf(element.layer(), 0)) + .setAttr(Fields.LEVEL, Parse.parseLongOrNull(element.source().getTag("level"))) + .setAttr(Fields.INDOOR, element.indoor() ? 1 : null) + .setAttrs(LanguageUtils.getNames(element.source().properties(), translations)) + .setLabelGridPixelSize(14, 64) + .setZorder(-rankOrder) + .setZoomRange(minzoom(element.subclass(), element.mappingKey()), 14); + } + + @Override + public void process(Tables.OsmPoiPolygon element, FeatureCollector features) { + // TODO duplicate code + String rawSubclass = element.subclass(); + if ("station".equals(rawSubclass) && "subway".equals(element.station())) { + rawSubclass = "subway"; + } + if ("station".equals(rawSubclass) && "yes".equals(element.funicular())) { + rawSubclass = "halt"; + } + + String subclass = switch (rawSubclass) { + case "information" -> nullIfEmpty(element.information()); + case "place_of_worship" -> nullIfEmpty(element.religion()); + case "pitch" -> nullIfEmpty(element.sport()); + default -> rawSubclass; + }; + String poiClass = poiClass(rawSubclass, element.mappingKey()); + int poiClassRank = poiClassRank(poiClass); + int rankOrder = poiClassRank + ((nullOrEmpty(element.name())) ? 2000 : 0); + + // TODO pointOnSurface if not convex + features.centroid(LAYER_NAME).setBufferPixels(BUFFER_SIZE) + .setAttr(Fields.CLASS, poiClass) + .setAttr(Fields.SUBCLASS, subclass) + .setAttr(Fields.LAYER, nullIf(element.layer(), 0)) + .setAttr(Fields.LEVEL, Parse.parseLongOrNull(element.source().getTag("level"))) + .setAttr(Fields.INDOOR, element.indoor() ? 1 : null) + .setAttrs(LanguageUtils.getNames(element.source().properties(), translations)) + .setLabelGridPixelSize(14, 64) + .setZorder(-rankOrder) + .setZoomRange(minzoom(element.subclass(), element.mappingKey()), 14); + } + + @Override + public List postProcess(int zoom, + List items) throws GeometryException { + LongIntMap groupCounts = new LongIntHashMap(); + for (int i = items.size() - 1; i >= 0; i--) { + VectorTileEncoder.Feature feature = items.get(i); + int gridrank = groupCounts.getOrDefault(feature.group(), 1); + groupCounts.put(feature.group(), gridrank + 1); + if (!feature.attrs().containsKey(Fields.RANK)) { + feature.attrs().put(Fields.RANK, gridrank); + } + } + return items; + } } diff --git a/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/PoiTest.java b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/PoiTest.java new file mode 100644 index 00000000..dc355b57 --- /dev/null +++ b/openmaptiles/src/test/java/com/onthegomap/flatmap/openmaptiles/layers/PoiTest.java @@ -0,0 +1,156 @@ +package com.onthegomap.flatmap.openmaptiles.layers; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.onthegomap.flatmap.SourceFeature; +import com.onthegomap.flatmap.geo.GeometryException; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class PoiTest extends AbstractLayerTest { + + private SourceFeature feature(boolean area, Map tags) { + return area ? polygonFeature(tags) : pointFeature(tags); + } + + @Test + public void testFenwayPark() { + assertFeatures(7, List.of(Map.of( + "_layer", "poi", + "class", "stadium", + "subclass", "stadium", + "name", "Fenway Park", + "rank", "", + "_minzoom", 14, + "_labelgrid_size", 64d + )), process(pointFeature(Map.of( + "leisure", "stadium", + "name", "Fenway Park" + )))); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void testFunicularHalt(boolean area) { + assertFeatures(7, List.of(Map.of( + "_layer", "poi", + "class", "railway", + "subclass", "halt", + "rank", "" + )), process(feature(area, Map.of( + "railway", "station", + "funicular", "yes", + "name", "station" + )))); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void testSubway(boolean area) { + assertFeatures(7, List.of(Map.of( + "_layer", "poi", + "class", "railway", + "subclass", "subway", + "rank", "" + )), process(feature(area, Map.of( + "railway", "station", + "station", "subway", + "name", "station" + )))); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void testPlaceOfWorshipFromReligionTag(boolean area) { + assertFeatures(7, List.of(Map.of( + "_layer", "poi", + "class", "place_of_worship", + "subclass", "religion value", + "rank", "" + )), process(feature(area, Map.of( + "amenity", "place_of_worship", + "religion", "religion value", + "name", "station" + )))); + } + + @Test + public void testPitchFromSportTag() { + assertFeatures(7, List.of(Map.of( + "_layer", "poi", + "class", "pitch", + "subclass", "soccer", + "rank", "" + )), process(pointFeature(Map.of( + "leisure", "pitch", + "sport", "soccer", + "name", "station" + )))); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void testInformation(boolean area) { + assertFeatures(7, List.of(Map.of( + "_layer", "poi", + "class", "information", + "subclass", "infotype", + "rank", "" + )), process(feature(area, Map.of( + "tourism", "information", + "information", "infotype", + "name", "station" + )))); + } + + @Test + public void testGridRank() throws GeometryException { + var layerName = Poi.LAYER_NAME; + assertEquals(List.of(), profile.postProcessLayerFeatures(layerName, 13, List.of())); + + assertEquals(List.of(pointFeature( + layerName, + Map.of("rank", 1), + 1 + )), profile.postProcessLayerFeatures(layerName, 14, List.of(pointFeature( + layerName, + Map.of(), + 1 + )))); + + assertEquals(List.of( + pointFeature( + layerName, + Map.of("rank", 2, "name", "a"), + 1 + ), pointFeature( + layerName, + Map.of("rank", 1, "name", "b"), + 1 + ), pointFeature( + layerName, + Map.of("rank", 1, "name", "c"), + 2 + ) + ), profile.postProcessLayerFeatures(layerName, 14, List.of( + pointFeature( + layerName, + Map.of("name", "a"), + 1 + ), + pointFeature( + layerName, + Map.of("name", "b"), + 1 + ), + pointFeature( + layerName, + Map.of("name", "c"), + 2 + ) + ))); + } +}