diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Roads.java b/tiles/src/main/java/com/protomaps/basemap/layers/Roads.java index 096866b0..164b3112 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Roads.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Roads.java @@ -8,6 +8,8 @@ import com.onthegomap.planetiler.VectorTile; import com.onthegomap.planetiler.geo.GeometryException; import com.onthegomap.planetiler.reader.SourceFeature; +import com.onthegomap.planetiler.reader.osm.OsmElement; +import com.onthegomap.planetiler.reader.osm.OsmRelationInfo; import com.protomaps.basemap.feature.CountryCoder; import com.protomaps.basemap.feature.FeatureId; import com.protomaps.basemap.locales.CartographicLocale; @@ -15,7 +17,7 @@ import com.protomaps.basemap.names.OsmNames; import java.util.*; -public class Roads implements ForwardingProfile.LayerPostProcesser { +public class Roads implements ForwardingProfile.LayerPostProcessor, ForwardingProfile.OsmRelationPreprocessor { private CountryCoder countryCoder; @@ -33,6 +35,22 @@ public String name() { public record Shield(String text, String network) {} + private record RouteRelationInfo( + @Override long id, + String network + ) implements OsmRelationInfo {} + + @Override + public List preprocessOsmRelation(OsmElement.Relation relation) { + if (relation.hasTag("type", "route") && relation.hasTag("route", "road")) { + return List.of(new RouteRelationInfo( + relation.id(), + relation.getString("network") + )); + } + return null; + } + public void processOsm(SourceFeature sf, FeatureCollector features) { if (sf.canBeLine() && sf.hasTag("highway") && !(sf.hasTag("highway", "proposed", "abandoned", "razed", "demolished", "removed", "construction", "elevator"))) { @@ -49,11 +67,35 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { Shield shield = locale.getShield(sf); Integer shieldTextLength = shield.text() == null ? null : shield.text().length(); + List relationNetworks = new ArrayList<>(); + + for (var routeInfo : sf.relationInfo(RouteRelationInfo.class)) { + RouteRelationInfo relation = routeInfo.relation(); + if (relation.network != null) { + relationNetworks.add(relation.network); + } + } + + boolean ARoad = false; + boolean BRoad = false; + + // United States + ARoad |= relationNetworks.contains("US:I"); + BRoad |= relationNetworks.contains("US:US"); + + boolean hasOverride = false; + try { + var code = countryCoder.getCountryCode(sf.latLonGeometry()).orElse(""); + hasOverride |= code.equals("US"); // United States + } catch (Exception e) { + // do nothing + } + if (highway.equals("motorway") || highway.equals("motorway_link")) { // TODO: (nvkelso 20230622) Use Natural Earth for low zoom roads at zoom 5 and earlier // as normally OSM roads would start at 6, but we start at 3 to match Protomaps v2 kind = "highway"; - minZoom = 3; + minZoom = hasOverride ? 7 : 3; if (highway.equals("motorway")) { minZoomShieldText = 7; @@ -69,7 +111,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { if (highway.equals("trunk")) { // Just trunk earlier zoom, otherwise road network looks choppy just with motorways then - minZoom = 6; + minZoom = hasOverride ? 7 : 6; minZoomShieldText = 8; } else if (highway.equals("primary")) { minZoomShieldText = 10; @@ -138,6 +180,15 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { minZoomNames = 14; } + if (hasOverride) { + if (BRoad) { + minZoom = 6; + } + if (ARoad) { + minZoom = 3; + } + } + var feat = features.line("roads") .setId(FeatureId.create(sf)) .setAttr("kind", kind) @@ -154,12 +205,6 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { .setPixelTolerance(0) .setZoomRange(minZoom, maxZoom); - try { - var code = countryCoder.getCountryCode(sf.latLonGeometry()); - } catch (Exception e) { - // do logic based on country code - } - if (!kindDetail.isEmpty()) { feat.setAttr("kind_detail", kindDetail); } else { diff --git a/tiles/src/test/java/com/protomaps/basemap/layers/LayerTest.java b/tiles/src/test/java/com/protomaps/basemap/layers/LayerTest.java index 5bfd56e3..cb38c2f9 100644 --- a/tiles/src/test/java/com/protomaps/basemap/layers/LayerTest.java +++ b/tiles/src/test/java/com/protomaps/basemap/layers/LayerTest.java @@ -9,6 +9,7 @@ import com.onthegomap.planetiler.reader.SourceFeature; import com.onthegomap.planetiler.stats.Stats; import com.protomaps.basemap.Basemap; +import com.protomaps.basemap.feature.CountryCoder; import com.protomaps.basemap.feature.NaturalEarthDb; import java.util.List; import java.util.Map; @@ -24,7 +25,11 @@ abstract class LayerTest { List.of(new NaturalEarthDb.NeAdmin1StateProvince("California", "US-CA", "Q2", 5.0, 8.0)), List.of(new NaturalEarthDb.NePopulatedPlace("San Francisco", "Q3", 9.0, 2)) ); - final Basemap profile = new Basemap(naturalEarthDb, null, null, null); + final CountryCoder countryCoder = CountryCoder.fromJsonString( + "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"properties\":{\"iso1A2\":\"US\",\"nameEn\":\"United States\"},\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[-124,47],[-124,25],[-71,25],[-71,47],[-124,47]]]]}}]}"); + + // US [-124,47],[-124,25],[-71,25],[-71,47],[-124,47] + final Basemap profile = new Basemap(naturalEarthDb, null, countryCoder, null); static void assertFeatures(int zoom, List> expected, Iterable actual) { var expectedList = expected.stream().toList(); diff --git a/tiles/src/test/java/com/protomaps/basemap/layers/RoadsTest.java b/tiles/src/test/java/com/protomaps/basemap/layers/RoadsTest.java index 4a48c835..feb14a23 100644 --- a/tiles/src/test/java/com/protomaps/basemap/layers/RoadsTest.java +++ b/tiles/src/test/java/com/protomaps/basemap/layers/RoadsTest.java @@ -2,7 +2,10 @@ import static com.onthegomap.planetiler.TestUtils.newLineString; +import com.onthegomap.planetiler.FeatureCollector; import com.onthegomap.planetiler.reader.SimpleFeature; +import com.onthegomap.planetiler.reader.osm.OsmElement; +import com.onthegomap.planetiler.reader.osm.OsmReader; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -26,4 +29,117 @@ void simple() { 0 ))); } + + @Test + void relation1() { + // highway=motorway is part of a US Interstate relation and is located in the US -> minzoom should be 3 + var relationResult = profile.preprocessOsmRelation(new OsmElement.Relation(1, Map.of( + "type", "route", + "route", "road", + "network", "US:I" + ), List.of( + new OsmElement.Relation.Member(OsmElement.Type.WAY, 2, "role") + ))); + + FeatureCollector features = process(SimpleFeature.createFakeOsmFeature( + newLineString(-104.97235, 39.73867, -105.260503, 40.010771), // Denver - Boulder + new HashMap<>(Map.of("highway", "motorway")), + "osm", + null, + 2, + relationResult.stream().map(info -> new OsmReader.RelationMember<>("role", info)).toList() + )); + + assertFeatures(0, + List.of(Map.of( + "_minzoom", 3 + )), + features + ); + } + + @Test + void relation2() { + // highway=motorway is part of US State network and is located in the US -> minzoom should be 6 + var relationResult = profile.preprocessOsmRelation(new OsmElement.Relation(1, Map.of( + "type", "route", + "route", "road", + "network", "US:US" + ), List.of( + new OsmElement.Relation.Member(OsmElement.Type.WAY, 2, "role") + ))); + + FeatureCollector features = process(SimpleFeature.createFakeOsmFeature( + newLineString(-104.97235, 39.73867, -105.260503, 40.010771), // Denver - Boulder + new HashMap<>(Map.of("highway", "motorway")), + "osm", + null, + 2, + relationResult.stream().map(info -> new OsmReader.RelationMember<>("role", info)).toList() + )); + + assertFeatures(0, + List.of(Map.of( + "_minzoom", 6 + )), + features + ); + } + + @Test + void relation3() { + // highway=motorway is not part of US Interstate/State network and is located in the US -> minzoom should be 7 + var relationResult = profile.preprocessOsmRelation(new OsmElement.Relation(1, Map.of( + "type", "route", + "route", "road", + "network", "some:network" + ), List.of( + new OsmElement.Relation.Member(OsmElement.Type.WAY, 2, "role") + ))); + + FeatureCollector features = process(SimpleFeature.createFakeOsmFeature( + newLineString(-104.97235, 39.73867, -105.260503, 40.010771), // Denver - Boulder + new HashMap<>(Map.of("highway", "motorway")), + "osm", + null, + 2, + relationResult.stream().map(info -> new OsmReader.RelationMember<>("role", info)).toList() + )); + + assertFeatures(0, + List.of(Map.of( + "_minzoom", 7 + )), + features + ); + } + + @Test + void relation4() { + // highway=motorway is part of US State network and is located ouside of the US -> minzoom should be 3 + var relationResult = profile.preprocessOsmRelation(new OsmElement.Relation(1, Map.of( + "type", "route", + "route", "road", + "network", "US:US" + ), List.of( + new OsmElement.Relation.Member(OsmElement.Type.WAY, 2, "role") + ))); + + FeatureCollector features = process(SimpleFeature.createFakeOsmFeature( + newLineString(2.424, 48.832, 8.52332, 47.36919), // Paris - Zurich + new HashMap<>(Map.of("highway", "motorway")), + "osm", + null, + 2, + relationResult.stream().map(info -> new OsmReader.RelationMember<>("role", info)).toList() + )); + + assertFeatures(0, + List.of(Map.of( + "_minzoom", 3 + )), + features + ); + } + }