5
5
import com .onthegomap .planetiler .FeatureCollector ;
6
6
import com .onthegomap .planetiler .ForwardingProfile ;
7
7
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 ;
8
11
import com .onthegomap .planetiler .reader .SourceFeature ;
9
12
import com .onthegomap .planetiler .util .SortKey ;
10
13
import com .onthegomap .planetiler .util .ZoomFunction ;
16
19
import java .util .List ;
17
20
import java .util .Map ;
18
21
import java .util .concurrent .atomic .AtomicInteger ;
22
+ import org .locationtech .jts .geom .Point ;
19
23
20
24
public class Places implements ForwardingProfile .FeatureProcessor , ForwardingProfile .FeaturePostProcessor {
21
25
@@ -26,6 +30,17 @@ public String name() {
26
30
27
31
private final AtomicInteger placeNumber = new AtomicInteger (0 );
28
32
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
+
29
44
// Evaluates place layer sort ordering of inputs into an integer for the sort-key field.
30
45
static int getSortKey (float minZoom , int kindRank , int populationRank , long population , String name ) {
31
46
return SortKey
@@ -44,6 +59,11 @@ static int getSortKey(float minZoom, int kindRank, int populationRank, long popu
44
59
.get ();
45
60
}
46
61
62
+ @ Override
63
+ public void release () {
64
+ ne_populated_places = null ;
65
+ }
66
+
47
67
private static final ZoomFunction <Number > LOCALITY_GRID_SIZE_ZOOM_FUNCTION =
48
68
ZoomFunction .fromMaxZoomThresholds (Map .of (
49
69
6 , 32 ,
@@ -66,6 +86,29 @@ public void processNe(SourceFeature sf, FeatureCollector features) {
66
86
if (!sourceLayer .equals ("ne_10m_populated_places" )) {
67
87
return ;
68
88
}
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
69
112
var kind = "" ;
70
113
var kindDetail = "" ;
71
114
@@ -114,21 +157,18 @@ public void processNe(SourceFeature sf, FeatureCollector features) {
114
157
.setAttr ("pmap:kind_detail" , kindDetail )
115
158
.setAttr ("population" , population )
116
159
.setAttr ("pmap:population_rank" , populationRank )
117
- .setAttr ("wikidata_id" , sf .getString ("wikidata" ))
118
160
// Server sort features so client label collisions are pre-sorted
119
161
// we also set the sort keys so the label grid can be sorted predictably (bonus: tile features also sorted)
120
162
// since all these are locality, we hard code kindRank to 2 (needs to match OSM section below)
121
163
.setSortKey (getSortKey (minZoom , 2 , populationRank , population , sf .getString ("name" )));
122
164
123
- // We set the sort keys so the label grid can be sorted predictably (bonus: tile features also sorted)
124
165
// 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
126
166
feat .setPointLabelGridPixelSize (LOCALITY_GRID_SIZE_ZOOM_FUNCTION )
127
167
.setPointLabelGridLimit (LOCALITY_GRID_LIMIT_ZOOM_FUNCTION )
128
168
.setBufferPixels (64 );
129
169
130
- if (sf .hasTag ("wikidata " )) {
131
- feat .setAttr ("wikidata" , sf .getString ("wikidata " ));
170
+ if (sf .hasTag ("wikidataid " )) {
171
+ feat .setAttr ("wikidata" , sf .getString ("wikidataid " ));
132
172
}
133
173
134
174
NeNames .setNeNames (feat , sf , 0 );
@@ -182,7 +222,7 @@ public void processFeature(SourceFeature sf, FeatureCollector features) {
182
222
case "city" :
183
223
case "town" :
184
224
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
186
226
minZoom = 7.0f ;
187
227
maxZoom = 15.0f ;
188
228
kindRank = 2 ;
@@ -196,7 +236,7 @@ public void processFeature(SourceFeature sf, FeatureCollector features) {
196
236
break ;
197
237
case "village" :
198
238
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
200
240
minZoom = 10.0f ;
201
241
maxZoom = 15.0f ;
202
242
kindRank = 3 ;
@@ -251,6 +291,33 @@ public void processFeature(SourceFeature sf, FeatureCollector features) {
251
291
}
252
292
}
253
293
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
+
254
321
var feat = features .point (this .name ())
255
322
.setId (FeatureId .create (sf ))
256
323
// Core Tilezen schema properties
@@ -264,7 +331,9 @@ public void processFeature(SourceFeature sf, FeatureCollector features) {
264
331
// DEPRECATION WARNING: Marked for deprecation in v4 schema, do not use these for styling
265
332
// If an explicate value is needed it should be a kind, or included in kind_detail
266
333
.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 );
268
337
269
338
// Instead of exporting ISO country_code_iso3166_1_alpha_2 (which are sparse), we export Wikidata IDs
270
339
if (sf .hasTag ("wikidata" )) {
0 commit comments