Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip multiexpression #109

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion app/src/examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,29 @@
{
"name": "zurich-bridges",
"description": "Pedestrian bridge tagged as area",
"tags": ["bridge", "ch"],
"tags": ["landuse", "bridge", "ch"],
"center": [8.54252, 47.376925],
"zoom": 17
},
{
"name": "sfo-tarmac",
"description": "San Francisco International Airpot tarmac tagged with only routable aeroway=",
"tags": ["landuse", "airport", "aeroway"],
"center": [-122.37868, 37.61983],
"zoom": 15
},
{
"name": "ams-tarmac",
"description": "Amsterdam Schiphol airport tagged with area:aeroway= in addition to aeroway=",
"tags": ["landuse", "airport", "area:aeroway"],
"center": [4.73503, 52.31107],
"zoom": 14
},
{
"name": "us-national-parks",
"description": "landuse featueres for US national parks distinguished by operator tags",
"tags": ["us", "landuse"],
"center": [-119.256, 37.579],
"zoom": 8
}
]
226 changes: 69 additions & 157 deletions tiles/src/main/java/com/protomaps/basemap/layers/Landuse.java
Original file line number Diff line number Diff line change
@@ -1,178 +1,90 @@
package com.protomaps.basemap.layers;

import static com.onthegomap.planetiler.expression.Expression.*;
import static com.onthegomap.planetiler.expression.MultiExpression.entry;

import com.onthegomap.planetiler.FeatureCollector;
import com.onthegomap.planetiler.FeatureMerge;
import com.onthegomap.planetiler.ForwardingProfile;
import com.onthegomap.planetiler.VectorTile;
import com.onthegomap.planetiler.expression.MultiExpression;
import com.onthegomap.planetiler.geo.GeometryException;
import com.onthegomap.planetiler.reader.SourceFeature;
import com.protomaps.basemap.feature.FeatureId;
import com.protomaps.basemap.postprocess.Area;
import java.util.List;
import java.util.stream.Stream;

public class Landuse implements ForwardingProfile.FeatureProcessor, ForwardingProfile.FeaturePostProcessor {

@Override
public void processFeature(SourceFeature sf, FeatureCollector features) {
if (sf.canBePolygon() && (sf.hasTag("aeroway", "aerodrome", "runway") ||
sf.hasTag("area:aeroway", "taxiway", "runway") ||
sf.hasTag("amenity", "hospital", "school", "kindergarten", "university", "college") ||
sf.hasTag("boundary", "national_park", "protected_area") ||
sf.hasTag("landuse", "recreation_ground", "industrial", "brownfield", "railway", "cemetery", "commercial",
"grass", "orchard", "farmland", "farmyard", "residential", "military") ||
sf.hasTag("leisure", "park", "garden", "golf_course", "dog_park", "playground", "pitch", "nature_reserve") ||
sf.hasTag("man_made", "pier", "bridge") ||
sf.hasTag("natural", "beach") ||
// TODO: (nvkelso 20230622) This use of the place tag here is dubious, though paired with "residential"
sf.hasTag("place", "neighbourhood") ||
sf.hasTag("railway", "platform") ||
sf.hasTag("tourism", "zoo") ||
(sf.hasTag("area", "yes") &&
(sf.hasTag("highway", "pedestrian", "footway"))))) {
String kind = "other";
if (sf.hasTag("aeroway", "aerodrome")) {
kind = sf.getString("aeroway");
} else if (sf.hasTag("amenity", "university", "college", "hospital", "library", "post_office", "school",
"townhall")) {
kind = sf.getString("amenity");
} else if (sf.hasTag("amenity", "cafe")) {
kind = sf.getString("amenity");
} else if (sf.hasTag("highway")) {
kind = "pedestrian";
} else if (sf.hasTag("landuse", "cemetery")) {
kind = sf.getString("landuse");
} else if (sf.hasTag("landuse", "orchard", "farmland", "farmyard")) {
kind = "farmland";
} else if (sf.hasTag("landuse", "residential")) {
kind = "residential";
} else if (sf.hasTag("landuse", "industrial", "brownfield")) {
kind = "industrial";
} else if (sf.hasTag("landuse", "military")) {
kind = "military";
if (sf.hasTag("military", "naval_base", "airfield")) {
kind = sf.getString("military");
}
} else if (sf.hasTag("leisure", "golf_course", "marina", "park", "stadium")) {
kind = sf.getString("leisure");
} else if (sf.hasTag("man_made", "bridge")) {
kind = "pedestrian";
} else if (sf.hasTag("man_made", "pier")) {
kind = "pier";
} else if (sf.hasTag("shop", "grocery", "supermarket")) {
kind = sf.getString("shop");
} else if (sf.hasTag("tourism", "attraction", "camp_site", "hotel")) {
kind = sf.getString("tourism");
} else {
// Avoid problem of too many "other" kinds
// All these will default to min_zoom of 15
// If a more specific min_zoom is needed (or sanitize kind values)
// then add new logic in section above
if (sf.hasTag("amenity")) {
kind = sf.getString("amenity");
} else if (sf.hasTag("craft")) {
kind = sf.getString("craft");
} else if (sf.hasTag("aeroway")) {
kind = sf.getString("aeroway");
} else if (sf.hasTag("historic")) {
kind = sf.getString("historic");
} else if (sf.hasTag("landuse")) {
kind = sf.getString("landuse");
} else if (sf.hasTag("leisure")) {
kind = sf.getString("leisure");
} else if (sf.hasTag("man_made")) {
kind = sf.getString("man_made");
} else if (sf.hasTag("natural")) {
kind = sf.getString("natural");
} else if (sf.hasTag("railway")) {
kind = sf.getString("railway");
} else if (sf.hasTag("shop")) {
kind = sf.getString("shop");
} else if (sf.hasTag("tourism")) {
kind = sf.getString("tourism");
// Boundary is most generic, so place last else we loose out
// on nature_reserve detail versus all the protected_area
} else if (sf.hasTag("boundary")) {
kind = sf.getString("boundary");
}
}
public static Stream<MultiExpression.Entry<String>> valueEntries(String field, String... values) {
return Stream.of(values).map(v -> entry(v, matchAny(field, v)));
}

public static MultiExpression.Index<String> compose(Stream<MultiExpression.Entry<String>>... values) {
return MultiExpression
.of(Stream.of(values).reduce(Stream::concat).orElseGet(Stream::empty).toList()).index();
}

// National forests
if (sf.hasTag("boundary", "national_park") &&
sf.hasTag("operator", "United States Forest Service", "US Forest Service", "U.S. Forest Service",
"USDA Forest Service", "United States Department of Agriculture", "US National Forest Service",
"United State Forest Service", "U.S. National Forest Service")) {
kind = "forest";
} else if (sf.hasTag("boundary", "national_park") &&
sf.hasTag("protect_class", "6") &&
sf.hasTag("protection_title", "National Forest")) {
kind = "forest";
} else if (sf.hasTag("landuse", "forest") &&
sf.hasTag("protect_class", "6")) {
kind = "forest";
} else if (sf.hasTag("landuse", "forest") &&
sf.hasTag("operator", "United States Forest Service", "US Forest Service", "U.S. Forest Service",
"USDA Forest Service", "United States Department of Agriculture", "US National Forest Service",
"United State Forest Service", "U.S. National Forest Service")) {
kind = "forest";
} else if (sf.hasTag("landuse", "forest")) {
kind = "forest";
} else if (sf.hasTag("boundary", "protected_area") &&
sf.hasTag("protect_class", "6") &&
sf.hasTag("operator", "United States Forest Service", "US Forest Service", "U.S. Forest Service",
"USDA Forest Service", "United States Department of Agriculture", "US National Forest Service",
"United State Forest Service", "U.S. National Forest Service")) {
kind = "forest";
}
public static final String LANDUSE_KEY = "landuse";
public static final String PEDESTRIAN = "pedestrian";
// TODO craft and historic
private static final MultiExpression.Index<String> LANDUSE_KIND = compose(
valueEntries("aeroway", "aerodrome", "runway"),
bdon marked this conversation as resolved.
Show resolved Hide resolved
valueEntries("area:aeroway", "taxiway", "runway"),
valueEntries("amenity", "university", "college", "hospital", "library", "school"), // townhall? post_office?
Stream.of(entry(PEDESTRIAN, and(
matchAny("area", "yes"),
matchAny("highway", PEDESTRIAN, "footway")
))),
valueEntries(LANDUSE_KEY, "cemetery"),
Stream.of(entry("farmland", matchAny(LANDUSE_KEY, "orchard", "farmland", "farmyard"))),
valueEntries(LANDUSE_KEY, "residential"),
Stream.of(entry("industrial", matchAny(LANDUSE_KEY, "industrial", "brownfield"))),
valueEntries(LANDUSE_KEY, "military"),
valueEntries("military", "naval_base", "airfield"),
valueEntries("leisure", "golf_course", "park", "stadium", "garden", "dog_park", "playground", "pitch",
"nature_reserve"),
Stream.of(entry(PEDESTRIAN, matchAny("man_made", "bridge"))),
valueEntries("man_made", "pier", "bridge"),
valueEntries("natural", "beach"),
valueEntries("shop", "grocery", "supermarket"), // not in tilezen?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nvkelso to research

valueEntries("tourism", "attraction", "camp_site", "hotel", "zoo"),
valueEntries("railway", "platform"),
// Boundary is most generic, so place last else we loose out
// on nature_reserve detail versus all the protected_area
valueEntries("boundary", "national_park", "protected_area"),
valueEntries(LANDUSE_KEY, "recreation_ground", "railway", "commercial", "grass")
);

// National parks
if (sf.hasTag("boundary", "national_park")) {
if (!(sf.hasTag("operator", "United States Forest Service", "US Forest Service", "U.S. Forest Service",
"USDA Forest Service", "United States Department of Agriculture", "US National Forest Service",
"United State Forest Service", "U.S. National Forest Service") ||
sf.hasTag("protection_title", "Conservation Area", "Conservation Park", "Environmental use", "Forest Reserve",
"National Forest", "National Wildlife Refuge", "Nature Refuge", "Nature Reserve", "Protected Site",
"Provincial Park", "Public Access Land", "Regional Reserve", "Resources Reserve", "State Forest",
"State Game Land", "State Park", "Watershed Recreation Unit", "Wild Forest", "Wilderness Area",
"Wilderness Study Area", "Wildlife Management", "Wildlife Management Area", "Wildlife Sanctuary")) &&
(sf.hasTag("protect_class", "2", "3") ||
sf.hasTag("operator", "United States National Park Service", "National Park Service",
"US National Park Service", "U.S. National Park Service", "US National Park service") ||
sf.hasTag("operator:en", "Parks Canada") ||
sf.hasTag("designation", "national_park") ||
sf.hasTag("protection_title", "National Park"))) {
kind = "national_park";
} else {
kind = "park";
}
}
static final MatchAny US_OPERATOR =
matchAny("operator", "United States Forest Service", "US Forest Service", "U.S. Forest Service",
"USDA Forest Service", "United States Department of Agriculture", "US National Forest Service",
"United State Forest Service", "U.S. National Forest Service");

features.polygon(this.name())
.setId(FeatureId.create(sf))
// Core Tilezen schema properties
.setAttr("pmap:kind", kind)
// Core OSM tags for different kinds of places
// DEPRECATION WARNING: Marked for deprecation in v4 schema, do not use these for styling
// If an explicate value is needed it should bea kind, or included in kind_detail
.setAttr("aeroway", sf.getString("aeroway"))
.setAttr("amenity", sf.getString("amenity"))
.setAttr("area:aeroway", sf.getString("area:aeroway"))
.setAttr("boundary", sf.getString("boundary"))
.setAttr("highway", sf.getString("highway"))
.setAttr("landuse", sf.getString("landuse"))
.setAttr("leisure", sf.getString("leisure"))
.setAttr("man_made", sf.getString("man_made"))
.setAttr("natural", sf.getString("natural"))
.setAttr("place", sf.getString("place"))
.setAttr("railway", sf.getString("railway"))
.setAttr("sport", sf.getString("sport"))
// NOTE: (nvkelso 20230622) Consider zoom 5 instead...
// But to match Protomaps v2 we do earlier
.setZoomRange(2, 15)
.setMinPixelSize(2.0);
static final MatchAny PROTECTION_TITLE =
matchAny("protection_title", "Conservation Area", "Conservation Park", "Environmental use", "Forest Reserve",
"National Forest", "National Wildlife Refuge", "Nature Refuge", "Nature Reserve", "Protected Site",
"Provincial Park", "Public Access Land", "Regional Reserve", "Resources Reserve", "State Forest",
"State Game Land", "State Park", "Watershed Recreation Unit", "Wild Forest", "Wilderness Area",
"Wilderness Study Area", "Wildlife Management", "Wildlife Management Area", "Wildlife Sanctuary");

// NOTE: (nvkelso 20230622) landuse labels for polygons are found in the pois layer
//OsmNames.setOsmNames(poly, sf, 0);
}
@Override
public void processFeature(SourceFeature sf, FeatureCollector features) {
if (!sf.canBePolygon())
return;
List<String> matches = LANDUSE_KIND.getMatches(sf);
if (matches.isEmpty())
return;
String kind = matches.get(0);

features.polygon(this.name())
.setId(FeatureId.create(sf))
.setAttr("pmap:kind", kind)
// NOTE: (nvkelso 20230622) Consider zoom 5 instead...
// But to match Protomaps v2 we do earlier
.setZoomRange(2, 15)
.setMinPixelSize(2.0);
}

@Override
Expand Down
62 changes: 62 additions & 0 deletions tiles/src/test/java/com/protomaps/basemap/layers/LanduseTest.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.protomaps.basemap.layers;

import static com.onthegomap.planetiler.TestUtils.newPolygon;
import static org.junit.jupiter.api.Assertions.assertEquals;

import com.onthegomap.planetiler.reader.SimpleFeature;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.StreamSupport;
import org.junit.jupiter.api.Test;

class LanduseTest extends LayerTest {
Expand All @@ -21,4 +23,64 @@ void simple() {
0
)));
}

void assertKind(String expected, Map<String, String> keys) {
assertFeatures(15,
List.of(Map.of("pmap:kind", expected)),
process(SimpleFeature.create(
newPolygon(0, 0, 0, 1, 1, 1, 0, 0),
new HashMap<>(keys),
"osm",
null,
0
)));
}

void assertNone(Map<String, String> keys) {
assertEquals(0,
StreamSupport.stream(process(SimpleFeature.create(
newPolygon(0, 0, 0, 1, 1, 1, 0, 0),
new HashMap<>(keys),
"osm",
null,
0
)).spliterator(), false).toList().size());
}


@Test
void kinds() {
assertKind("aerodrome", Map.of("aeroway", "aerodrome"));
assertKind("runway", Map.of("aeroway", "runway"));
assertKind("taxiway", Map.of("area:aeroway", "taxiway"));
assertKind("runway", Map.of("area:aeroway", "runway"));
assertKind("university", Map.of("amenity", "university"));
assertKind("college", Map.of("amenity", "college"));
// assertKind("townhall", Map.of("amenity", "townhall"));
assertKind("pedestrian", Map.of("highway", "pedestrian", "area", "yes"));
assertNone(Map.of("highway", "pedestrian"));
assertKind("pedestrian", Map.of("highway", "footway", "area", "yes"));
assertNone(Map.of("highway", "footway"));
assertKind("cemetery", Map.of("landuse", "cemetery"));
assertKind("farmland", Map.of("landuse", "orchard"));
assertKind("farmland", Map.of("landuse", "farmland"));
assertKind("farmland", Map.of("landuse", "farmyard"));
assertKind("residential", Map.of("landuse", "residential"));
assertKind("industrial", Map.of("landuse", "industrial"));
assertKind("industrial", Map.of("landuse", "brownfield"));
assertKind("military", Map.of("landuse", "military"));
// assertKind("naval_base", Map.of("military", "naval_base"));
// assertKind("airfield", Map.of("military", "airfield"));
assertKind("golf_course", Map.of("leisure", "golf_course"));
// assertKind("marina", Map.of("leisure", "marina"));
assertKind("park", Map.of("leisure", "park"));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To test the order of operations, there should at least be one test that includes other tags than just the expected minimum tags... eg add a boundary key-value in.

// assertKind("stadium", Map.of("leisure", "stadium"));
// assertKind("pedestrian", Map.of("man_made", "bridge"));
assertKind("pier", Map.of("man_made", "pier"));
// assertKind("grocery", Map.of("shop", "grocery"));
// assertKind("supermarket", Map.of("shop", "supermarket"));
assertKind("attraction", Map.of("tourism", "attraction"));
assertKind("camp_site", Map.of("tourism", "camp_site"));
assertKind("hotel", Map.of("tourism", "hotel"));
}
}
Loading