Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
Tiles 4.13.5
------
- Translate POI kind= assignments to MultiExpression rules [#537]

Tiles 4.13.4
------
- remove kind=protected_area which made historical city centers appear as parks [#531]
Expand Down
2 changes: 1 addition & 1 deletion tiles/src/main/java/com/protomaps/basemap/Basemap.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public String description() {

@Override
public String version() {
return "4.13.4";
return "4.13.5";
}

@Override
Expand Down
215 changes: 118 additions & 97 deletions tiles/src/main/java/com/protomaps/basemap/layers/Pois.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package com.protomaps.basemap.layers;

import static com.onthegomap.planetiler.util.Parse.parseDoubleOrNull;
import static com.protomaps.basemap.feature.Matcher.fromTag;
import static com.protomaps.basemap.feature.Matcher.getString;
import static com.protomaps.basemap.feature.Matcher.rule;
import static com.protomaps.basemap.feature.Matcher.use;
import static com.protomaps.basemap.feature.Matcher.with;
import static com.protomaps.basemap.feature.Matcher.without;

import com.onthegomap.planetiler.FeatureCollector;
import com.onthegomap.planetiler.ForwardingProfile;
import com.onthegomap.planetiler.VectorTile;
import com.onthegomap.planetiler.expression.Expression;
import com.onthegomap.planetiler.expression.MultiExpression;
import com.onthegomap.planetiler.geo.GeoUtils;
import com.onthegomap.planetiler.geo.GeometryException;
import com.onthegomap.planetiler.reader.SourceFeature;
Expand Down Expand Up @@ -35,15 +43,125 @@ public Pois(QrankDb qrankDb) {

public static final String LAYER_NAME = "pois";

private static final Expression WITH_OPERATOR_USFS = with("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");

private static final MultiExpression.Index<Map<String, Object>> index = MultiExpression.of(List.of(

// Everything is "other"/"" at first
rule(use("kind", "other"), use("kindDetail", "")),

// Boundary is most generic, so place early else we lose out
// on nature_reserve detail versus all the protected_area
rule(with("boundary"), use("kind", fromTag("boundary"))),

// More specific kinds

rule(with("historic"), without("historic", "yes"), use("kind", fromTag("historic"))),
rule(with("tourism"), use("kind", fromTag("tourism"))),
rule(with("shop"), use("kind", fromTag("shop"))),
rule(with("highway"), use("kind", fromTag("highway"))),
rule(with("railway"), use("kind", fromTag("railway"))),
rule(with("natural"), use("kind", fromTag("natural"))),
rule(with("leisure"), use("kind", fromTag("leisure"))),
rule(with("landuse"), use("kind", fromTag("landuse"))),
rule(with("aeroway"), use("kind", fromTag("aeroway"))),
rule(with("craft"), use("kind", fromTag("craft"))),
rule(with("attraction"), use("kind", fromTag("attraction"))),
rule(with("amenity"), use("kind", fromTag("amenity"))),

// National forests

rule(
Expression.or(
with("landuse", "forest"),
Expression.and(with("boundary", "national_park"), WITH_OPERATOR_USFS),
Expression.and(
with("boundary", "national_park"),
with("protect_class", "6"),
with("protection_title", "National Forest")
),
Expression.and(
with("boundary", "protected_area"),
with("protect_class", "6"),
WITH_OPERATOR_USFS
)
),
use("kind", "forest")
),

// National parks

rule(with("boundary", "national_park"), use("kind", "park")),
rule(
with("boundary", "national_park"),
Expression.not(WITH_OPERATOR_USFS),
without("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"),
Expression.or(
with("protect_class", "2", "3"),
with("operator", "United States National Park Service", "National Park Service", "US National Park Service",
"U.S. National Park Service", "US National Park service"),
with("operator:en", "Parks Canada"),
with("designation", "national_park"),
with("protection_title", "National Park")
),
use("kind", "national_park")
),

// Remaining things

rule(with("natural", "peak"), use("kind", fromTag("natural"))),
rule(with("highway", "bus_stop"), use("kind", fromTag("highway"))),
rule(with("tourism", "attraction", "camp_site", "hotel"), use("kind", fromTag("tourism"))),
rule(with("shop", "grocery", "supermarket"), use("kind", fromTag("shop"))),
rule(with("leisure", "golf_course", "marina", "stadium", "park"), use("kind", fromTag("leisure"))),

rule(with("landuse", "military"), use("kind", "military")),
rule(
with("landuse", "military"),
with("military", "naval_base", "airfield"),
use("kind", fromTag("military"))
),

rule(with("landuse", "cemetery"), use("kind", fromTag("landuse"))),

rule(
with("aeroway", "aerodrome"),
use("kind", "aerodrome"),
use("kindDetail", fromTag("aerodrome"))
),

// Additional details for certain classes of POI

rule(with("sport"), use("kindDetail", fromTag("sport"))),
rule(with("religion"), use("kindDetail", fromTag("religion"))),
rule(with("cuisine"), use("kindDetail", fromTag("cuisine")))

)).index();

@Override
public String name() {
return LAYER_NAME;
}

// ~= pow((sqrt(70k) / (40m / 256)) / 256, 2) ~= 4.4e-11
private static final double WORLD_AREA_FOR_70K_SQUARE_METERS =
Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d, 2);

public void processOsm(SourceFeature sf, FeatureCollector features) {
var matches = index.getMatches(sf);
if (matches.isEmpty()) {
return;
}

String kind = getString(sf, matches, "kind", "undefined");
String kindDetail = getString(sf, matches, "kindDetail", "undefined");

if ((sf.isPoint() || sf.canBePolygon()) && (sf.hasTag("aeroway", "aerodrome") ||
sf.hasTag("amenity") ||
sf.hasTag("attraction") ||
Expand All @@ -59,8 +177,6 @@ public void processOsm(SourceFeature sf, FeatureCollector features) {
sf.hasTag("shop") ||
sf.hasTag("tourism") &&
(!sf.hasTag("historic", "district")))) {
String kind = "other";
String kindDetail = "";
Integer minZoom = 15;
long qrank = 0;

Expand All @@ -70,123 +186,39 @@ public void processOsm(SourceFeature sf, FeatureCollector features) {
}

if (sf.hasTag("aeroway", "aerodrome")) {
kind = sf.getString("aeroway");
minZoom = 13;

// Emphasize large international airports earlier
if (kind.equals("aerodrome") && sf.hasTag("iata")) {
minZoom -= 2;
}

if (sf.hasTag("aerodrome")) {
kindDetail = sf.getString("aerodrome");
}
} else if (sf.hasTag("amenity", "university", "college")) {
kind = sf.getString("amenity");
// One would think University should be earlier, but there are lots of dinky node only places
// So if the university has a large area, it'll naturally improve it's zoom in the next section...
minZoom = 14;
} else if (sf.hasTag("amenity", "hospital")) {
kind = sf.getString("amenity");
minZoom = 12;
} else if (sf.hasTag("amenity", "library", "post_office", "townhall")) {
kind = sf.getString("amenity");
minZoom = 13;
} else if (sf.hasTag("amenity", "school")) {
kind = sf.getString("amenity");
minZoom = 15;
} else if (sf.hasTag("amenity", "cafe")) {
kind = sf.getString("amenity");
minZoom = 15;
} else if (sf.hasTag("landuse", "cemetery")) {
kind = sf.getString("landuse");
minZoom = 14;
} else if (sf.hasTag("landuse", "military")) {
kind = "military";
if (sf.hasTag("military", "naval_base", "airfield")) {
kind = sf.getString("military");
}
} else if (sf.hasTag("leisure", "park")) {
kind = "park";
// Lots of pocket parks and NODE parks, show those later than rest of leisure
minZoom = 14;
} else if (sf.hasTag("leisure", "golf_course", "marina", "stadium")) {
kind = sf.getString("leisure");
minZoom = 13;
} else if (sf.hasTag("shop", "grocery", "supermarket")) {
kind = sf.getString("shop");
minZoom = 14;
} else if (sf.hasTag("tourism", "attraction", "camp_site", "hotel")) {
kind = sf.getString("tourism");
minZoom = 15;
} else if (sf.hasTag("highway", "bus_stop")) {
kind = sf.getString("highway");
minZoom = 17;
} else if (sf.hasTag("natural", "peak")) {
kind = sf.getString("natural");
minZoom = 13;
} 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("attraction")) {
kind = sf.getString("attraction");
} else if (sf.hasTag("craft")) {
kind = sf.getString("craft");
} else if (sf.hasTag("aeroway")) {
kind = sf.getString("aeroway");
} else if (sf.hasTag("landuse")) {
kind = sf.getString("landuse");
} else if (sf.hasTag("leisure")) {
kind = sf.getString("leisure");
} else if (sf.hasTag("natural")) {
kind = sf.getString("natural");
} else if (sf.hasTag("railway")) {
kind = sf.getString("railway");
} else if (sf.hasTag("highway")) {
kind = sf.getString("highway");
} 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("historic") && !sf.hasTag("historic", "yes")) {
kind = sf.getString("historic");
} else if (sf.hasTag("boundary")) {
kind = sf.getString("boundary");
}
}

// 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";
}

// National parks
Expand All @@ -205,21 +237,10 @@ public void processOsm(SourceFeature sf, FeatureCollector features) {
sf.hasTag("operator:en", "Parks Canada") ||
sf.hasTag("designation", "national_park") ||
sf.hasTag("protection_title", "National Park"))) {
kind = "national_park";
minZoom = 11;
} else {
kind = "park";
}
}

if (sf.hasTag("cuisine")) {
kindDetail = sf.getString("cuisine");
} else if (sf.hasTag("religion")) {
kindDetail = sf.getString("religion");
} else if (sf.hasTag("sport")) {
kindDetail = sf.getString("sport");
}

// try first for polygon -> point representations
if (sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null) {
Double wayArea = 0.0;
Expand Down
Loading