Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
Tiles 4.14.3
------
- Fix OSM place selection regression at zoom=7 [#576]

Tiles 4.14.2
------
- Upgrade to planetiler 0.10.1 with workaround for JTS polygon negative buffer bug [#538]
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 @@ -133,7 +133,7 @@ public String description() {

@Override
public String version() {
return "4.14.2";
return "4.14.3";
}

@Override
Expand Down
76 changes: 53 additions & 23 deletions tiles/src/main/java/com/protomaps/basemap/layers/Places.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@
rule(with("place", "state", "province"), with("pm:country", "US", "CA", "BR", "IN", "CN", "AU"),
use("pm:kind", "region")),
rule(with("place", "city", "town"), use("pm:kind", "locality"), use("pm:kindDetail", fromTag("place"))),
rule(with("place", "city"), without("population"), use("pm:population", 5000)),
rule(with("place", "town"), without("population"), use("pm:population", 10000)),
rule(with("place", "city"), without("population"), use("pm:populationFallback", 5000)),
rule(with("place", "town"), without("population"), use("pm:populationFallback", 10000)),

// Neighborhood-scale places

Expand All @@ -87,17 +87,17 @@
// Smaller places detailed in OSM but not fully tested for Overture

rule(with("place", "village"), use("pm:kind", "locality"), use("pm:kindDetail", fromTag("place"))),
rule(with("place", "village"), without("population"), use("pm:population", 2000)),
rule(with("place", "village"), without("population"), use("pm:populationFallback", 2000)),
rule(with("place", "locality"), use("pm:kind", "locality")),
rule(with("place", "locality"), without("population"), use("pm:population", 1000)),
rule(with("place", "locality"), without("population"), use("pm:populationFallback", 1000)),
rule(with("place", "hamlet"), use("pm:kind", "locality")),
rule(with("place", "hamlet"), without("population"), use("pm:population", 200)),
rule(with("place", "hamlet"), without("population"), use("pm:populationFallback", 200)),
rule(with("place", "isolated_dwelling"), use("pm:kind", "locality")),
rule(with("place", "isolated_dwelling"), without("population"), use("pm:population", 100)),
rule(with("place", "isolated_dwelling"), without("population"), use("pm:populationFallback", 100)),
rule(with("place", "farm"), use("pm:kind", "locality")),
rule(with("place", "farm"), without("population"), use("pm:population", 50)),
rule(with("place", "farm"), without("population"), use("pm:populationFallback", 50)),
rule(with("place", "allotments"), use("pm:kind", "locality")),
rule(with("place", "allotments"), without("population"), use("pm:population", 1000))
rule(with("place", "allotments"), without("population"), use("pm:populationFallback", 1000))

)).index();

Expand Down Expand Up @@ -126,10 +126,14 @@
rule(with("pm:kind", "region"), use("pm:minzoom", 8), use("pm:maxzoom", 11)),
rule(with("pm:kind", "region"), with("pm:country", "US", "CA", "BR", "IN", "CN", "AU"), use("pm:kindRank", 1)),

rule(with("pm:kind", "locality"), use("pm:kindRank", 4), use("pm:minzoom", 7), use("pm:maxzoom", 15)),
rule(with("pm:kind", "locality"), atLeast("pm:population", 1000), use("pm:minzoom", 12)),
rule(with("pm:kind", "locality"), with("pm:kindDetail", "city"), use("pm:kindRank", 2), use("pm:minzoom", 8)),
rule(with("pm:kind", "locality"), with("pm:kindDetail", "town"), use("pm:kindRank", 2), use("pm:minzoom", 9)),
rule(with("pm:kind", "locality"), use("pm:kindRank", 4), use("pm:minzoom", 11), use("pm:maxzoom", 15)),
rule(with("pm:kind", "locality"), with("pm:populationFallback"), use("pm:minzoom", 12)),
rule(with("pm:kind", "locality"), with("pm:kindDetail", "city"), use("pm:kindRank", 2), use("pm:minzoom", 7)),
rule(with("pm:kind", "locality"), with("pm:kindDetail", "city"), with("pm:populationFallback"),
use("pm:minzoom", 8)),
rule(with("pm:kind", "locality"), with("pm:kindDetail", "town"), use("pm:kindRank", 2), use("pm:minzoom", 7)),
rule(with("pm:kind", "locality"), with("pm:kindDetail", "town"), with("pm:populationFallback"),
use("pm:minzoom", 9)),
rule(with("pm:kind", "locality"), with("pm:kindDetail", "village"), use("pm:kindRank", 3), use("pm:minzoom", 10)),
rule(with("pm:kind", "locality"), with("pm:kindDetail", "village"), atLeast("pm:population", 2000),
use("pm:minzoom", 11)),
Expand Down Expand Up @@ -226,7 +230,21 @@
15, 3
), 0);

private Map<String, Object> makeTagMap(String kind, String kindDetail, Integer population,
Integer populationFallback) {
Map<String, Object> computedTags = new HashMap<>();

computedTags.put("pm:kind", kind);
computedTags.put("pm:kindDetail", kindDetail);
computedTags.put("pm:population", population);
if (populationFallback > 0) {
computedTags.put("pm:populationFallback", populationFallback);
}

return computedTags;
}

public void processOsm(SourceFeature sf, FeatureCollector features) {

Check failure on line 247 in tiles/src/main/java/com/protomaps/basemap/layers/Places.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=protomaps_basemaps&issues=AZzVmuIhEH4qK027DrAW&open=AZzVmuIhEH4qK027DrAW&pullRequest=576
if (!sf.isPoint() || !sf.hasTag("name") || !sf.hasTag("place")) {
return;
}
Expand All @@ -245,6 +263,7 @@
String kind = getString(sf, matches, "pm:kind", "pm:undefined");
String kindDetail = getString(sf, matches, "pm:kindDetail", "");
Integer population = getInteger(sf, matches, "pm:population", 0);
Integer populationFallback = getInteger(sf, matches, "pm:populationFallback", 0);

if ("pm:undefined".equals(kind)) {
return;
Expand All @@ -254,9 +273,15 @@
Integer maxZoom;
Integer kindRank;

var sf2 = new Matcher.SourceFeatureWithComputedTags(sf, Map.of("pm:kind", kind, "pm:kindDetail", kindDetail));
var computedTags = makeTagMap(kind, kindDetail, population, populationFallback);
var sf2 = new Matcher.SourceFeatureWithComputedTags(sf, computedTags);
var zoomMatches = zoomsIndex.getMatches(sf2);

// Use populationFallback for sorting if no real population
if (population == 0 && populationFallback > 0) {
population = populationFallback;
}

minZoom = getInteger(sf2, zoomMatches, "pm:minzoom", 99);
maxZoom = getInteger(sf2, zoomMatches, "pm:maxzoom", 99);
kindRank = getInteger(sf2, zoomMatches, "pm:kindRank", 99);
Expand Down Expand Up @@ -345,11 +370,25 @@
return;
}

// Extract population (if available)
Integer population = 0;
if (sf.hasTag("population")) {
Object popValue = sf.getTag("population");
if (popValue instanceof Number number) {
population = number.intValue();
}
}

// Overture always uses populationFallback for zoom calculations to get consistent behavior
// This ensures Overture places get the higher minzoom levels (8 for city, 9 for town, etc)
Integer populationFallback = 1; // Marker value to trigger fallback zoom levels

Integer minZoom;
Integer maxZoom;
Integer kindRank;

var sf2 = new Matcher.SourceFeatureWithComputedTags(sf, Map.of("pm:kind", kind, "pm:kindDetail", kindDetail));
var computedTags = makeTagMap(kind, kindDetail, population, populationFallback);
var sf2 = new Matcher.SourceFeatureWithComputedTags(sf, computedTags);
var zoomMatches = zoomsIndex.getMatches(sf2);

minZoom = getInteger(sf2, zoomMatches, "pm:minzoom", 99);
Expand All @@ -359,15 +398,6 @@
// Extract name
String name = sf.getString("names.primary");

// Extract population (if available)
Integer population = 0;
if (sf.hasTag("population")) {
Object popValue = sf.getTag("population");
if (popValue instanceof Number number) {
population = number.intValue();
}
}

int populationRank = 0;

for (int i = 0; i < popBreaks.length; i++) {
Expand Down
83 changes: 83 additions & 0 deletions tiles/src/test/java/com/protomaps/basemap/layers/PlacesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,89 @@ void testAllotmentsOsm() {
0
)));
}

@Test
void testLocalityNoPopulationHasCorrectMinZoom() {
// Place Mahiouindo - OSM node/4535788658
// place=locality without population should have _minzoom=12 (not 7)
assertFeatures(7,
List.of(Map.of("_minzoom", 12, "_maxzoom", 14, "kind", "locality", "kind_detail", "locality")),
process(SimpleFeature.create(
newPoint(2.5892, 7.3321),
new HashMap<>(Map.of("place", "locality", "name", "Place Mahiouindo")),
"osm",
null,
0
)));
}

@Test
void testLocalityNoPopulationMinZoom() {
// Place Mahiouindo - should have min_zoom=13 (minzoom=12 + 1)
assertFeatures(12,
List.of(Map.of("kind", "locality",
"kind_detail", "locality",
"min_zoom", 13,
"population", 1000)),
process(SimpleFeature.create(
newPoint(2.5892, 7.3321),
new HashMap<>(Map.of("place", "locality", "name", "Place Mahiouindo")),
"osm",
null,
0
)));
}

@Test
void testCityWithPopulationVisibleAtZoom7() {
// Kétou - OSM node/2313302870
// place=city with population=160000 should be visible at zoom 7 (minzoom=7, min_zoom=8)
assertFeatures(7,
List.of(Map.of("kind", "locality",
"kind_detail", "city",
"min_zoom", 8,
"population", 160000)),
process(SimpleFeature.create(
newPoint(2.5892, 7.3632),
new HashMap<>(Map.of("place", "city", "name", "Kétou", "population", "160000")),
"osm",
null,
0
)));
}

@Test
void testCityWithoutPopulationHasCorrectMinZoom() {
// Cities without population tags get populationFallback=5000 and should have _minZoom=8
assertFeatures(8,
List.of(Map.of("_minzoom", 8, "kind", "locality", "kind_detail", "city", "population", 5000)),
process(SimpleFeature.create(
newPoint(2.5892, 7.3632),
new HashMap<>(Map.of("place", "city", "name", "Some City")),
"osm",
null,
0
)));
}

@Test
void testOuidahWithWikidataVisibleAtZoom6() {
// Ouidah - OSM node/313015821, wikidata Q850031
// Has wikidata entry in places.csv: Q850031,6,-1,8
// Should be visible at zoom 6 (wikidata override)
assertFeatures(6,
List.of(Map.of("kind", "locality",
"kind_detail", "city",
"population", 160000)),
process(SimpleFeature.create(
newPoint(2.0854, 6.3616),
new HashMap<>(Map.of("place", "city", "name", "Ouidah", "population", "160000", "wikidata", "Q850031")),
"osm",
null,
0
)));
}

}


Expand Down
Loading