Skip to content

Commit 5401e90

Browse files
nvkelsobdon
andauthoredAug 24, 2023
add NE <> OSM data join for places layer (#93)
* stub out NE <> OSM data join * order of operations is important, cleanups * formatting --------- Co-authored-by: Brandon Liu <bdon@bdon.org>
1 parent c45b167 commit 5401e90

File tree

2 files changed

+80
-9
lines changed

2 files changed

+80
-9
lines changed
 

‎tiles/src/main/java/com/protomaps/basemap/Basemap.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,11 @@ static void run(Arguments args) throws Exception {
120120

121121
var planetiler = Planetiler.create(args)
122122
.setProfile(new Basemap(earthWaterBounds))
123-
.addOsmSource("osm", Path.of("data", "sources", area + ".osm.pbf"), "geofabrik:" + area)
123+
// (nvkelso 20230817) Order of operations matters here so all NE places can be added to RTree indexes
124+
// before OSM uses them for data joins
124125
.addNaturalEarthSource("ne", sourcesDir.resolve("natural_earth_vector.sqlite.zip"),
125126
"https://naciscdn.org/naturalearth/packages/natural_earth_vector.sqlite.zip")
127+
.addOsmSource("osm", Path.of("data", "sources", area + ".osm.pbf"), "geofabrik:" + area)
126128
.setOutput(Path.of(area + ".pmtiles"));
127129
planetiler.addShapefileSource("osm_water", sourcesDir.resolve("water-polygons-split-3857.zip"),
128130
"https://osmdata.openstreetmap.de/download/water-polygons-split-3857.zip")

‎tiles/src/main/java/com/protomaps/basemap/layers/Places.java

+77-8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import com.onthegomap.planetiler.FeatureCollector;
66
import com.onthegomap.planetiler.ForwardingProfile;
77
import com.onthegomap.planetiler.VectorTile;
8+
import com.onthegomap.planetiler.geo.GeoUtils;
9+
import com.onthegomap.planetiler.geo.GeometryException;
10+
import com.onthegomap.planetiler.geo.PointIndex;
811
import com.onthegomap.planetiler.reader.SourceFeature;
912
import com.onthegomap.planetiler.util.SortKey;
1013
import com.onthegomap.planetiler.util.ZoomFunction;
@@ -16,6 +19,7 @@
1619
import java.util.List;
1720
import java.util.Map;
1821
import java.util.concurrent.atomic.AtomicInteger;
22+
import org.locationtech.jts.geom.Point;
1923

2024
public class Places implements ForwardingProfile.FeatureProcessor, ForwardingProfile.FeaturePostProcessor {
2125

@@ -26,6 +30,17 @@ public String name() {
2630

2731
private final AtomicInteger placeNumber = new AtomicInteger(0);
2832

33+
// To have consistent min_zoom and other attr between Natural Earth (low zooms) and OpenStreetMap (high zooms)
34+
// we need to store the NE places with a spatial indexes for later joins with OSM
35+
private PointIndex<NaturalEarthPlace> ne_populated_places = PointIndex.create();
36+
37+
// Data structure for any Natural Earth place (eg for data joins between NE and OSM)
38+
private record NaturalEarthPlace(String name, String wikidataId, float minZoom, int populationRank) {}
39+
40+
// Search window size for NE <> OSM data joins
41+
private static final double LOCALITY_JOIN_DISTANCE = GeoUtils.metersToPixelAtEquator(0, 50_000) / 256d;
42+
43+
2944
// Evaluates place layer sort ordering of inputs into an integer for the sort-key field.
3045
static int getSortKey(float minZoom, int kindRank, int populationRank, long population, String name) {
3146
return SortKey
@@ -44,6 +59,11 @@ static int getSortKey(float minZoom, int kindRank, int populationRank, long popu
4459
.get();
4560
}
4661

62+
@Override
63+
public void release() {
64+
ne_populated_places = null;
65+
}
66+
4767
private static final ZoomFunction<Number> LOCALITY_GRID_SIZE_ZOOM_FUNCTION =
4868
ZoomFunction.fromMaxZoomThresholds(Map.of(
4969
6, 32,
@@ -66,6 +86,29 @@ public void processNe(SourceFeature sf, FeatureCollector features) {
6686
if (!sourceLayer.equals("ne_10m_populated_places")) {
6787
return;
6888
}
89+
90+
// Setup for high zoom content
91+
// Collect Natural Earth populated places for use later in OSM data joins
92+
try {
93+
ne_populated_places.put(sf.worldGeometry(), new NaturalEarthPlace(
94+
sf.getString("name"),
95+
sf.getString("wikidataid"),
96+
// Offset by 1 here because of 256 versus 512 pixel tile sizes
97+
// and how the OSM processing assumes 512 tile size (while NE is 256)
98+
(float) Double.parseDouble(sf.getString("min_zoom")) - 1,
99+
(int) Double.parseDouble(sf.getString("rank_max"))
100+
));
101+
} catch (GeometryException e) {
102+
e.log("Geometry exception in NE populated places setup");
103+
}
104+
105+
// (nvkelso 20230817) We could omit the rest of this function and rely solely on
106+
// OSM for features (but using the NE attributes above.
107+
// We don't do that because OSM has too many names which
108+
// would bloat low zoom file size. Once OSM name localization
109+
// is configurable the below logic should be removed.
110+
111+
// Setup low zoom content
69112
var kind = "";
70113
var kindDetail = "";
71114

@@ -114,21 +157,18 @@ public void processNe(SourceFeature sf, FeatureCollector features) {
114157
.setAttr("pmap:kind_detail", kindDetail)
115158
.setAttr("population", population)
116159
.setAttr("pmap:population_rank", populationRank)
117-
.setAttr("wikidata_id", sf.getString("wikidata"))
118160
// Server sort features so client label collisions are pre-sorted
119161
// we also set the sort keys so the label grid can be sorted predictably (bonus: tile features also sorted)
120162
// since all these are locality, we hard code kindRank to 2 (needs to match OSM section below)
121163
.setSortKey(getSortKey(minZoom, 2, populationRank, population, sf.getString("name")));
122164

123-
// We set the sort keys so the label grid can be sorted predictably (bonus: tile features also sorted)
124165
// NOTE: The buffer needs to be consistent with the innteral grid pixel sizes
125-
//feat.setPointLabelGridSizeAndLimit(13, 64, 4); // each cell in the 4x4 grid can have 4 items
126166
feat.setPointLabelGridPixelSize(LOCALITY_GRID_SIZE_ZOOM_FUNCTION)
127167
.setPointLabelGridLimit(LOCALITY_GRID_LIMIT_ZOOM_FUNCTION)
128168
.setBufferPixels(64);
129169

130-
if (sf.hasTag("wikidata")) {
131-
feat.setAttr("wikidata", sf.getString("wikidata"));
170+
if (sf.hasTag("wikidataid")) {
171+
feat.setAttr("wikidata", sf.getString("wikidataid"));
132172
}
133173

134174
NeNames.setNeNames(feat, sf, 0);
@@ -182,7 +222,7 @@ public void processFeature(SourceFeature sf, FeatureCollector features) {
182222
case "city":
183223
case "town":
184224
kind = "locality";
185-
// TODO: these should be from data join to Natural Earth, and if fail data join then default to 8
225+
// This minZoom can be changed to smaller value in the NE data join step below
186226
minZoom = 7.0f;
187227
maxZoom = 15.0f;
188228
kindRank = 2;
@@ -196,7 +236,7 @@ public void processFeature(SourceFeature sf, FeatureCollector features) {
196236
break;
197237
case "village":
198238
kind = "locality";
199-
// TODO: these should be from data join to Natural Earth, and if fail data join then default to 8
239+
// This minZoom can be changed to smaller value in the NE data join step below
200240
minZoom = 10.0f;
201241
maxZoom = 15.0f;
202242
kindRank = 3;
@@ -251,6 +291,33 @@ public void processFeature(SourceFeature sf, FeatureCollector features) {
251291
}
252292
}
253293

294+
// Join OSM locality with nearby NE localities based on Wikidata ID and
295+
// harvest the min_zoom to achieve consistent label collisions at zoom 7+
296+
// By this zoom we get OSM points centered in feature better for area labels
297+
// While NE earlier aspires to be more the downtown area
298+
//
299+
// First scope down the NE <> OSM data join (to speed up total build time)
300+
if (kind.equals("locality")) {
301+
try {
302+
Point point = sf.worldGeometry().getCentroid();
303+
List<NaturalEarthPlace> neLocalities = ne_populated_places.getWithin(point, LOCALITY_JOIN_DISTANCE);
304+
String wikidata = sf.getString("wikidata", "");
305+
for (NaturalEarthPlace neLocality : neLocalities) {
306+
// We could add more fallback equivelancy tests here, but 98% of NE places have a Wikidata ID
307+
if (wikidata.equals(neLocality.wikidataId)) {
308+
minZoom = neLocality.minZoom;
309+
// (nvkelso 20230815) We could set the population value here, too
310+
// But by the OSM zooms the value should be the incorporated value
311+
// While symbology should be for the metro population value
312+
populationRank = neLocality.populationRank;
313+
break;
314+
}
315+
}
316+
} catch (GeometryException e) {
317+
e.log("Geometry exception in NE <> OSM data joins");
318+
}
319+
}
320+
254321
var feat = features.point(this.name())
255322
.setId(FeatureId.create(sf))
256323
// Core Tilezen schema properties
@@ -264,7 +331,9 @@ public void processFeature(SourceFeature sf, FeatureCollector features) {
264331
// DEPRECATION WARNING: Marked for deprecation in v4 schema, do not use these for styling
265332
// If an explicate value is needed it should be a kind, or included in kind_detail
266333
.setAttr("place", sf.getString("place"))
267-
.setZoomRange((int) minZoom, (int) maxZoom);
334+
// Generally we use NE and low zooms, and OSM at high zooms
335+
// With exceptions for country and region labels
336+
.setZoomRange(Math.max((int) minZoom, themeMinZoom), (int) maxZoom);
268337

269338
// Instead of exporting ISO country_code_iso3166_1_alpha_2 (which are sparse), we export Wikidata IDs
270339
if (sf.hasTag("wikidata")) {

0 commit comments

Comments
 (0)