diff --git a/src/main/java/org/openmaptiles/layers/Transportation.java b/src/main/java/org/openmaptiles/layers/Transportation.java index 299d3f7e..c051141c 100644 --- a/src/main/java/org/openmaptiles/layers/Transportation.java +++ b/src/main/java/org/openmaptiles/layers/Transportation.java @@ -168,6 +168,8 @@ public class Transportation implements .put(6, 100) .put(5, 500) .put(4, 1_000); + private static final ZoomFunction.MeterToPixelThresholds TRUNK_UPGRADE_LENGTH = ZoomFunction.meterThresholds() + .put(5, 1_000); // ORDER BY network_type, network, LENGTH(ref), ref) private static final Comparator RELATION_ORDERING = Comparator .comparingInt( @@ -285,6 +287,22 @@ private static boolean isTrunkForZ5(String highway, List routeRel .anyMatch(Z5_TRUNK_BY_NETWORK::contains); } + /** + * Checks if a trunk segment is small enough to be processed at z5 so it can merge with surrounding motorways. + * + * @param element the highway linestring element to check + * @return true if the trunk segment length is less than the threshold, false otherwise + */ + private boolean isTrunkZ5MergeableLength(Tables.OsmHighwayLinestring element) { + try { + return element.source().length() < TRUNK_UPGRADE_LENGTH.apply(5).doubleValue(); + } catch (GeometryException e) { + e.log(stats, "omt_transportation_trunk_length", + "Unable to get feature length for trunk upgrade: " + element.source().id()); + return false; + } + } + private static boolean isMotorwayWithNetworkForZ4(List routeRelations) { // All roads in network included in osm_national_network except gb-trunk and us-highway return routeRelations.stream() @@ -570,6 +588,12 @@ int getMinzoom(Tables.OsmHighwayLinestring element, String highwayClass) { (z13Paths || !nullOrEmpty(element.name()) || routeRank <= 2 || !nullOrEmpty(element.sacScale())) ? 13 : 14; case FieldValues.CLASS_TRUNK -> { boolean z5trunk = isTrunkForZ5(highway, routeRelations); + + //Allow small trunk segments to be processed at z5 so they can merge with surrounding motorways + if (isTrunkZ5MergeableLength(element)) { + z5trunk = true; + } + // and if it is good for Z5, it may be good also for Z4 (see CLASS_MOTORWAY bellow): String clazz = FieldValues.CLASS_TRUNK; if (z5trunk && isMotorwayWithNetworkForZ4(routeRelations)) { @@ -702,6 +726,19 @@ public List postProcess(int zoom, List i } } + //Upgrade small trunk segments at z5 so they can merge with surrounding motorways + if (zoom == 5) { + for (var item : items) { + var highway = item.tags().get(Fields.CLASS); + if (highway instanceof String highwayStr && highwayStr.equals(FieldValues.CLASS_TRUNK)) { + item.tags().put(Fields.CLASS, FieldValues.CLASS_MOTORWAY); + } + if (highway instanceof String highwayStr && highwayStr.equals(FieldValues.CLASS_TRUNK_CONSTRUCTION)) { + item.tags().put(Fields.CLASS, FieldValues.CLASS_MOTORWAY_CONSTRUCTION); + } + } + } + var merged = FeatureMerge.mergeLineStrings(items, minLength, tolerance, BUFFER_SIZE); for (var item : merged) { diff --git a/src/test/java/org/openmaptiles/layers/TransportationTest.java b/src/test/java/org/openmaptiles/layers/TransportationTest.java index 3e0f6b4d..d648ab02 100644 --- a/src/test/java/org/openmaptiles/layers/TransportationTest.java +++ b/src/test/java/org/openmaptiles/layers/TransportationTest.java @@ -4,9 +4,11 @@ import static com.onthegomap.planetiler.TestUtils.newPoint; import static com.onthegomap.planetiler.TestUtils.rectangle; import static java.util.Map.entry; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import com.onthegomap.planetiler.FeatureCollector; +import com.onthegomap.planetiler.VectorTile; import com.onthegomap.planetiler.config.Arguments; import com.onthegomap.planetiler.config.PlanetilerConfig; import com.onthegomap.planetiler.geo.GeometryException; @@ -16,6 +18,7 @@ import com.onthegomap.planetiler.reader.osm.OsmRelationInfo; import com.onthegomap.planetiler.stats.Stats; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Stream; @@ -396,7 +399,7 @@ void testDuplicateRoute() { "_layer", "transportation", "class", "trunk", "network", "us-state", - "_minzoom", 6 + "_minzoom", 5 ), Map.of( "_layer", "transportation_name", "class", "trunk", @@ -1217,7 +1220,7 @@ void testTransCanadaProvincialCaOnPrimaryRefOther() { "_layer", "transportation", "class", "trunk", "network", "ca-provincial", - "_minzoom", 6 + "_minzoom", 5 ), Map.of( "_layer", "transportation_name", "class", "trunk", @@ -1271,7 +1274,7 @@ void testTransCanadaProvincialCaMbPthRefOther() { "_layer", "transportation", "class", "trunk", "network", "ca-provincial", - "_minzoom", 6 + "_minzoom", 5 ), Map.of( "_layer", "transportation_name", "class", "trunk", @@ -1325,7 +1328,7 @@ void testTransCanadaProvincialCaAbPrimaryRefOther() { "_layer", "transportation", "class", "trunk", "network", "ca-provincial", - "_minzoom", 6 + "_minzoom", 5 ), Map.of( "_layer", "transportation_name", "class", "trunk", @@ -1379,7 +1382,7 @@ void testTransCanadaProvincialCaBcRefOther() { "_layer", "transportation", "class", "trunk", "network", "ca-provincial", - "_minzoom", 6 + "_minzoom", 5 ), Map.of( "_layer", "transportation_name", "class", "trunk", @@ -1404,7 +1407,7 @@ void testTransCanadaProvincialCaOther() { assertFeatures(13, List.of(Map.of( "_layer", "transportation", "class", "trunk", - "_minzoom", 6 + "_minzoom", 5 )), features); boolean caProvPresent = StreamSupport.stream(features.spliterator(), false) .flatMap(f -> f.getAttrsAtZoom(13).entrySet().stream()) @@ -2192,7 +2195,7 @@ void testARoad() { assertFeatures(13, List.of(Map.of( "_layer", "transportation", "class", "trunk", - "_minzoom", 6 + "_minzoom", 5 ), Map.of( "_layer", "transportation_name", "class", "trunk", @@ -2227,4 +2230,55 @@ void testERoad() { "route_1_ref", "E 77" )), features); } + + @Test + void testShortTrunkMerge() throws GeometryException { + var layer = Transportation.LAYER_NAME; + + var motorwayZ6 = new VectorTile.Feature( + layer, + 1, + VectorTile.encodeGeometry(newLineString(0, 0, 10, 10)), + new HashMap<>(Map.of("class", "motorway")), + 0 + ); + var trunkZ6 = new VectorTile.Feature( + layer, + 2, + VectorTile.encodeGeometry(newLineString(10, 10, 10, 11)), + new HashMap<>(Map.of("class", "trunk")), + 0 + ); + var motorwayZ5 = new VectorTile.Feature( + layer, + 1, + VectorTile.encodeGeometry(newLineString(0, 0, 10, 10)), + new HashMap<>(Map.of("class", "motorway")), + 0 + ); + var trunkZ5 = new VectorTile.Feature( + layer, + 2, + VectorTile.encodeGeometry(newLineString(10, 10, 10, 11)), + new HashMap<>(Map.of("class", "trunk")), + 0 + ); + + List inputZ6 = List.of(motorwayZ6, trunkZ6); + List inputZ5 = List.of(motorwayZ5, trunkZ5); + + List resultZ6 = profile.postProcessLayerFeatures(layer, 6, inputZ6); + List resultZ5 = profile.postProcessLayerFeatures(layer, 5, inputZ5); + + assertEquals(2, resultZ6.size(), "Should be separate features at zoom 6"); + assertEquals(1, resultZ5.size(), "Should merge into a single feature at zoom 5"); + + VectorTile.Feature mergedFeatureZ5 = resultZ5.get(0); + assertEquals("motorway", mergedFeatureZ5.tags().get("class"), "Merged feature should be motorway class at zoom 5"); + + List classesZ6 = resultZ6.stream() + .map(f -> (String) f.tags().get("class")) + .toList(); + assertEquals(List.of("motorway", "trunk"), classesZ6, "At zoom 6, should have motorway and trunk classes"); + } }