Skip to content

Commit

Permalink
Road relations (#386)
Browse files Browse the repository at this point in the history
* Improve minzoom logic for roads based on relations [#221, #213]
* Only keep network from route
* Only test minzoom
  • Loading branch information
wipfli authored Feb 24, 2025
1 parent 450155a commit a974fe9
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 10 deletions.
63 changes: 54 additions & 9 deletions tiles/src/main/java/com/protomaps/basemap/layers/Roads.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@
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;
import com.protomaps.basemap.locales.US;
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;

Expand All @@ -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<OsmRelationInfo> 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"))) {
Expand All @@ -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<String> 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;
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Map<String, Object>> expected, Iterable<FeatureCollector.Feature> actual) {
var expectedList = expected.stream().toList();
Expand Down
116 changes: 116 additions & 0 deletions tiles/src/test/java/com/protomaps/basemap/layers/RoadsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
);
}

}

0 comments on commit a974fe9

Please sign in to comment.