1
1
import rewind from '@mapbox/geojson-rewind'
2
2
import { geoIdentity , geoPath } from 'd3-geo' ;
3
+ import { geoStitch } from "d3-geo-projection"
3
4
import fs from 'fs' ;
4
5
import mapshaper from 'mapshaper' ;
5
6
import path from 'path' ;
6
- import topojsonLib from 'topojson' ;
7
+ import { topology } from 'topojson-server'
7
8
import config , { getNEFilename } from './config.mjs' ;
8
9
9
10
const { filters, inputDir, layers, resolutions, scopes, unFilename, vectors } = config ;
@@ -47,13 +48,13 @@ function addCentroidsToGeojson(geojsonPath) {
47
48
// Wind the polygon rings in the correct direction to indicate what is solid and what is whole
48
49
const rewindGeojson = ( geojson , clockwise = true ) => rewind ( geojson , clockwise )
49
50
50
- // Snap x-coordinates that are close to be on the antimeridian
51
- function snapToAntimeridian ( inputFilepath , outputFilepath ) {
51
+ // Clamp x-coordinates to the antimeridian
52
+ function clampToAntimeridian ( inputFilepath , outputFilepath ) {
52
53
outputFilepath ||= inputFilepath
53
54
const jsonString = fs . readFileSync ( inputFilepath , 'utf8' )
54
55
const updatedString = jsonString
55
- . replaceAll ( / 1 7 9 \. 9 9 \d + , / g, '180,' )
56
- . replaceAll ( / 1 8 0 \. 0 0 \d + , / g, '180,' )
56
+ . replaceAll ( / 1 7 9 \. 9 9 9 9 \d + , / g, '180,' )
57
+ . replaceAll ( / 1 8 0 \. 0 0 0 0 \d + , / g, '180,' )
57
58
58
59
fs . writeFileSync ( outputFilepath , updatedString ) ;
59
60
}
@@ -156,15 +157,15 @@ async function createCoastlinesLayer({ bounds, name, resolution, source }) {
156
157
const outputFilePath = `${ outputDirGeojson } /${ name } _${ resolution } m/coastlines.geojson` ;
157
158
const commands = [
158
159
inputFilePath ,
159
- '-dissolve ' ,
160
+ '-dissolve2 ' ,
160
161
'-lines' ,
161
162
bounds . length ? `-clip bbox=${ bounds . join ( ',' ) } ` : '' ,
162
- // Erase outer lines to avoid unpleasant lines through polygons crossing the antimeridian
163
- [ 'antarctica' , 'world' ] . includes ( name ) ? ' -clip bbox=-179.999 ,-89.999 ,179.999 ,89.999' : ' ',
163
+ // Erase world border to avoid unpleasant lines through polygons crossing the border.
164
+ ' -clip bbox=-179.99999 ,-89.99999 ,179.99999 ,89.99999 ',
164
165
`-o ${ outputFilePath } `
165
166
] . join ( ' ' ) ;
166
167
await mapshaper . runCommands ( commands ) ;
167
- if ( [ 'antarctica' , 'world' ] . includes ( name ) ) snapToAntimeridian ( outputFilePath )
168
+ clampToAntimeridian ( outputFilePath )
168
169
}
169
170
170
171
async function createOceanLayer ( { bounds, name, resolution, source } ) {
@@ -220,17 +221,18 @@ async function convertLayersToTopojson({ name, resolution }) {
220
221
if ( ! fs . existsSync ( regionDir ) ) return ;
221
222
222
223
const outputFile = `${ outputDirTopojson } /${ name } _${ resolution } m.json` ;
223
- // Scopes with polygons that cross the antimeridian need to be stitched (via the topology call)
224
+ // Scopes with polygons that cross the antimeridian need to be stitched
224
225
if ( [ "antarctica" , "world" ] . includes ( name ) ) {
225
226
const geojsonObjects = { }
226
227
for ( const layer of Object . keys ( config . layers ) ) {
227
228
const filePath = path . join ( regionDir , `${ layer } .geojson` )
228
- geojsonObjects [ layer ] = rewindGeojson ( getJsonFile ( filePath ) )
229
+ geojsonObjects [ layer ] = geoStitch ( rewindGeojson ( getJsonFile ( filePath ) ) )
229
230
}
230
- const topojsonTopology = topojsonLib . topology ( geojsonObjects , { 'property-transform' : f => f . properties } )
231
+ // Convert geojson to topojson
232
+ const topojsonTopology = topology ( geojsonObjects , 1000000 )
231
233
fs . writeFileSync ( outputFile , JSON . stringify ( topojsonTopology ) ) ;
232
234
} else {
233
- // Layer names default to file names
235
+ // In Mapshaper, layer names default to file names
234
236
const commands = [ `${ regionDir } /*.geojson combine-files` , `-o format=topojson ${ outputFile } ` ] . join ( ' ' ) ;
235
237
await mapshaper . runCommands ( commands ) ;
236
238
}
@@ -241,50 +243,130 @@ async function convertLayersToTopojson({ name, resolution }) {
241
243
fs . writeFileSync ( outputFile , JSON . stringify ( prunedTopojson ) ) ;
242
244
}
243
245
244
- // Get polygon features from UN GeoJSON and patch Antarctica gap
246
+ // Get required polygon features from UN GeoJSON
245
247
const inputFilePathUNGeojson = `${ inputDir } /${ unFilename } .geojson` ;
246
- const inputFilePathUNGeojsonCleaned = `${ inputDir } /${ unFilename } _cleaned.geojson` ;
247
- snapToAntimeridian ( inputFilePathUNGeojson , inputFilePathUNGeojsonCleaned )
248
- const commandsAllFeaturesCommon = [
249
- inputFilePathUNGeojsonCleaned ,
250
- `-filter 'iso3cd === "ATA"' target=1 + name=antarctica` ,
248
+ // await mapshaper.runCommands(`${inputFilePathUNGeojson} -filter 'stscod !== undefined' target=1 -clean target=1 -o force ${inputFilePathUNGeojson}`)
249
+ const outputFilePathAntarctica50m = `${ outputDirGeojson } /${ unFilename } _50m/antarctica.geojson` ;
250
+ const outputFilePathFiji50m = `${ outputDirGeojson } /${ unFilename } _50m/fiji.geojson` ;
251
+ const outputFilePathFijiAntimeridian50m = `${ outputDirGeojson } /${ unFilename } _50m/fiji_antimeridian.geojson` ;
252
+ const outputFilePathRussia50m = `${ outputDirGeojson } /${ unFilename } _50m/russia.geojson` ;
253
+ const outputFilePathRussiaAntimeridian50m = `${ outputDirGeojson } /${ unFilename } _50m/russia_antimeridian.geojson` ;
254
+ const copyFieldsList = "objectid,iso3cd,m49_cd,nam_en,lbl_en,georeg,geo_cd,sub_cd,int_cd,subreg,intreg,iso2cd,lbl_fr,name_fr,globalid,stscod,isoclr,ct,FID"
255
+ // The following fix up code is necessary to isolate/join/cut the polygons that cross the antimeridian.
256
+ // This is necessary for two reasons: the UN geojson is poor around the antimeridian and Mapshaper
257
+ // doesn't handle antimeridian cutting.
258
+
259
+ // Fix up Antarctica polygons
260
+ await mapshaper . runCommands ( `${ inputFilePathUNGeojson } -filter 'iso3cd === "ATA"' target=1 -o ${ outputFilePathAntarctica50m } ` )
261
+ const commandsAntarctica = [
262
+ outputFilePathAntarctica50m ,
251
263
// Use 'snap-interval' to patch gap in Antarctica
252
264
'-clean snap-interval=0.015 target=antarctica' ,
253
265
// Add rectangle to extend Antarctica to bottom of world
254
266
'-rectangle bbox=-180,-90,180,-89 name=antarctica_rectangle' ,
255
267
'-merge-layers target=antarctica,antarctica_rectangle force' ,
256
- '-dissolve2 target=antarctica copy-fields=objectid,iso3cd,m49_cd,nam_en,lbl_en,georeg,geo_cd,sub_cd,int_cd,subreg,intreg,iso2cd,lbl_fr,name_fr,globalid,stscod,isoclr,ct,FID' ,
257
- // Remove unpatched Antarctica
258
- `-filter 'georeg !== "ANT"' target=1` ,
259
- // Merge patched Antarctica
260
- '-merge-layers target=1,antarctica force name=all_features' ,
261
- // Erase Caspian Sea
262
- `-filter 'globalid === "{BBBEF27F-A6F4-4FBC-9729-77B3A8739409}"' target=all_features + name=caspian_sea` ,
263
- '-erase source=caspian_sea target=all_features' ,
264
- // Update country codes for disputed territories at Egypt/Sudan border: https://en.wikipedia.org/wiki/Egypt%E2%80%93Sudan_border
265
- `-each 'if (globalid === "{CA12D116-7A19-41D1-9622-17C12CCC720D}") iso3cd = "XHT"'` , // Halaib Triangle
266
- `-each 'if (globalid === "{9FD54A50-0BFB-4385-B342-1C3BDEE5ED9B}") iso3cd = "XBT"'` , // Bir Tawil
267
- `-each 'FID = iso3cd'`
268
- ]
268
+ `-dissolve2 target=antarctica copy-fields=${ copyFieldsList } ` ,
269
+ `-o force target=antarctica ${ outputFilePathAntarctica50m } `
270
+ ] . join ( " " )
271
+ await mapshaper . runCommands ( commandsAntarctica )
272
+
273
+ // Fix up Fiji polygons
274
+ await mapshaper . runCommands ( `${ inputFilePathUNGeojson } -filter 'iso3cd === "FJI"' target=1 -o ${ outputFilePathFiji50m } ` )
275
+ const commandsIsolateFijiAntimeridian = [
276
+ outputFilePathFiji50m ,
277
+ '-explode' ,
278
+ `-each 'id = this.id'` ,
279
+ `-filter '[31, 36, 39, 40].includes(id)' target=fiji + name=fiji_antimeridian` ,
280
+ `-o target=fiji_antimeridian ${ outputFilePathFijiAntimeridian50m } `
281
+ ] . join ( " " )
282
+ await mapshaper . runCommands ( commandsIsolateFijiAntimeridian )
283
+
284
+ const commandsFixFijiAntimeridian = [
285
+ outputFilePathFijiAntimeridian50m ,
286
+ '-proj +proj=eck4 +lon_0=11 +datum=WGS84' ,
287
+ `-dissolve2 copy-fields=${ copyFieldsList } ` ,
288
+ '-clean snap-interval=951' ,
289
+ `-proj +proj=webmerc +datum=WGS84 +lon_0=11` ,
290
+ '-erase bbox=18812993.94,-22000000,20000000,16500000 target=1 + name=east' ,
291
+ '-erase bbox=972000,-22000000,18812993.95,16500000 target=1 + name=west' ,
292
+ '-merge-layers target=east,west name=complete' ,
293
+ `-dissolve2 target=complete copy-fields=${ copyFieldsList } ` ,
294
+ '-proj wgs84' ,
295
+ `-o force target=complete ${ outputFilePathFijiAntimeridian50m } `
296
+ ] . join ( " " )
297
+ await mapshaper . runCommands ( commandsFixFijiAntimeridian )
298
+
299
+ const commandsFiji = [
300
+ `-i combine-files ${ outputFilePathFiji50m } ${ outputFilePathFijiAntimeridian50m } ` ,
301
+ '-explode target=fiji' ,
302
+ `-each 'id = this.id' target=fiji` ,
303
+ `-filter '![31, 36, 39, 40].includes(id)' target=fiji` ,
304
+ '-merge-layers target=fiji,fiji_antimeridian force name=fiji' ,
305
+ `-dissolve2 target=fiji copy-fields=${ copyFieldsList } ` ,
306
+ `-o force target=fiji ${ outputFilePathFiji50m } `
307
+ ] . join ( " " )
308
+ await mapshaper . runCommands ( commandsFiji )
309
+
310
+ // Fix up Russia polygons
311
+ await mapshaper . runCommands ( `${ inputFilePathUNGeojson } -filter 'iso3cd === "RUS"' target=1 -o ${ outputFilePathRussia50m } ` )
312
+ const commandsIsolateRussiaAntimeridian = [
313
+ outputFilePathRussia50m ,
314
+ '-explode' ,
315
+ `-each 'id = this.id'` ,
316
+ `-filter '[13, 15].includes(id)' target=russia + name=russia_antimeridian` ,
317
+ `-o target=russia_antimeridian ${ outputFilePathRussiaAntimeridian50m } `
318
+ ] . join ( " " )
319
+ await mapshaper . runCommands ( commandsIsolateRussiaAntimeridian )
320
+
321
+ const commandsFixRussiaAntimeridian = [
322
+ outputFilePathRussiaAntimeridian50m ,
323
+ '-proj +proj=eck4 +lon_0=11 +datum=WGS84' ,
324
+ `-dissolve2 copy-fields=${ copyFieldsList } ` ,
325
+ '-clean snap-interval=257' ,
326
+ `-proj +proj=webmerc +datum=WGS84 +lon_0=11` ,
327
+ '-erase bbox=18812993.94,-22000000,20000000,16500000 target=1 + name=east' ,
328
+ '-erase bbox=972000,-22000000,18812993.95,16500000 target=1 + name=west' ,
329
+ '-merge-layers target=east,west name=complete' ,
330
+ `-dissolve2 target=complete copy-fields=${ copyFieldsList } ` ,
331
+ '-proj wgs84' ,
332
+ `-o force target=complete ${ outputFilePathRussiaAntimeridian50m } `
333
+ ] . join ( " " )
334
+ await mapshaper . runCommands ( commandsFixRussiaAntimeridian )
335
+
336
+ const commandsRussia = [
337
+ `-i combine-files ${ outputFilePathRussia50m } ${ outputFilePathRussiaAntimeridian50m } ` ,
338
+ '-explode target=russia' ,
339
+ `-each 'id = this.id' target=russia` ,
340
+ `-filter '![13, 15].includes(id)' target=russia` ,
341
+ '-merge-layers target=russia,russia_antimeridian force name=russia' ,
342
+ `-dissolve2 target=russia copy-fields=${ copyFieldsList } ` ,
343
+ `-o force target=russia ${ outputFilePathRussia50m } `
344
+ ] . join ( " " )
345
+ await mapshaper . runCommands ( commandsRussia )
269
346
270
347
// Process 50m UN geodata
271
- const outputFilePath50m = `${ outputDirGeojson } /${ unFilename } _50m/all_features.geojson` ;
272
- const commandsAllFeatures50m = [
273
- ...commandsAllFeaturesCommon ,
274
- `-o target=1 ${ outputFilePath50m } `
275
- ] . join ( " " )
276
- await mapshaper . runCommands ( commandsAllFeatures50m ) ;
277
348
278
349
// Get countries from all polygon features
279
- const inputFilePathCountries50m = outputFilePath50m ;
280
350
const outputFilePathCountries50m = `${ outputDirGeojson } /${ unFilename } _50m/countries.geojson` ;
281
351
const commandsCountries50m = [
282
- inputFilePathCountries50m ,
352
+ `-i combine-files ${ inputFilePathUNGeojson } ${ outputFilePathAntarctica50m } ${ outputFilePathFiji50m } ${ outputFilePathRussia50m } ` ,
353
+ `-rename-layers un_polygons,un_polylines,antarctica,fiji,russia` ,
354
+ // Remove country polygons with bad geometry
355
+ `-filter '!["ATA", "FJI", "RUS"].includes(iso3cd)' target=un_polygons` ,
356
+ '-merge-layers target=un_polygons,antarctica,fiji,russia force name=all_features' ,
357
+ // Erase Caspian Sea
358
+ `-filter 'globalid === "{BBBEF27F-A6F4-4FBC-9729-77B3A8739409}"' target=all_features + name=caspian_sea` ,
359
+ '-erase source=caspian_sea target=all_features' ,
360
+ // Update country codes for disputed territories at Egypt/Sudan border: https://en.wikipedia.org/wiki/Egypt%E2%80%93Sudan_border
361
+ `-each 'if (globalid === "{CA12D116-7A19-41D1-9622-17C12CCC720D}") iso3cd = "XHT"'` , // Halaib Triangle
362
+ `-each 'if (globalid === "{9FD54A50-0BFB-4385-B342-1C3BDEE5ED9B}") iso3cd = "XBT"'` , // Bir Tawil
363
+ `-each 'if (iso3cd) iso3cd = iso3cd.toUpperCase()'` ,
283
364
`-filter '${ filters . countries } '` ,
284
365
'-clean' ,
285
366
`-o ${ outputFilePathCountries50m } `
286
367
] . join ( ' ' ) ;
287
368
await mapshaper . runCommands ( commandsCountries50m ) ;
369
+ clampToAntimeridian ( outputFilePathCountries50m )
288
370
289
371
// Get land from all polygon features
290
372
const inputFilePathLand50m = outputFilePathCountries50m ;
@@ -296,23 +378,15 @@ const commandsLand50m = [
296
378
] . join ( ' ' ) ;
297
379
await mapshaper . runCommands ( commandsLand50m ) ;
298
380
299
- // Create 110m geodata
300
- const inputFilePath110m = outputFilePath50m ;
301
- const outputFilePath110m = `${ outputDirGeojson } /${ unFilename } _110m/all_features.geojson` ;
302
- const commandsAllFeatures110m = [
303
- inputFilePath110m ,
304
- '-simplify 20%' ,
305
- `-o target=1 ${ outputFilePath110m } `
306
- ] . join ( " " )
307
- await mapshaper . runCommands ( commandsAllFeatures110m ) ;
381
+ // Process 50m UN geodata
308
382
309
383
// Get countries from all polygon features
310
- const inputFilePathCountries110m = outputFilePath110m ;
384
+ const inputFilePathCountries110m = outputFilePathCountries50m ;
311
385
const outputFilePathCountries110m = `${ outputDirGeojson } /${ unFilename } _110m/countries.geojson` ;
312
386
const commandsCountries110m = [
313
387
inputFilePathCountries110m ,
314
- `-filter ' ${ filters . countries } '` ,
315
- // Use 'snap-interval' to fix alignment issues with USA and Alaska, Mexico
388
+ '-simplify 20%' ,
389
+ // Use 'snap-interval' to fix alignment issues with continental USA, Alaska, and Mexico
316
390
'-clean snap-interval=0.015' ,
317
391
`-o ${ outputFilePathCountries110m } `
318
392
] . join ( ' ' ) ;
0 commit comments