11import rewind from '@mapbox/geojson-rewind'
22import { geoIdentity , geoPath } from 'd3-geo' ;
3+ import { geoStitch } from "d3-geo-projection"
34import fs from 'fs' ;
45import mapshaper from 'mapshaper' ;
56import path from 'path' ;
6- import topojsonLib from 'topojson' ;
7+ import { topology } from 'topojson-server'
78import config , { getNEFilename } from './config.mjs' ;
89
910const { filters, inputDir, layers, resolutions, scopes, unFilename, vectors } = config ;
@@ -47,13 +48,13 @@ function addCentroidsToGeojson(geojsonPath) {
4748// Wind the polygon rings in the correct direction to indicate what is solid and what is whole
4849const rewindGeojson = ( geojson , clockwise = true ) => rewind ( geojson , clockwise )
4950
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 ) {
5253 outputFilepath ||= inputFilepath
5354 const jsonString = fs . readFileSync ( inputFilepath , 'utf8' )
5455 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,' )
5758
5859 fs . writeFileSync ( outputFilepath , updatedString ) ;
5960}
@@ -156,15 +157,15 @@ async function createCoastlinesLayer({ bounds, name, resolution, source }) {
156157 const outputFilePath = `${ outputDirGeojson } /${ name } _${ resolution } m/coastlines.geojson` ;
157158 const commands = [
158159 inputFilePath ,
159- '-dissolve ' ,
160+ '-dissolve2 ' ,
160161 '-lines' ,
161162 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 ',
164165 `-o ${ outputFilePath } `
165166 ] . join ( ' ' ) ;
166167 await mapshaper . runCommands ( commands ) ;
167- if ( [ 'antarctica' , 'world' ] . includes ( name ) ) snapToAntimeridian ( outputFilePath )
168+ clampToAntimeridian ( outputFilePath )
168169}
169170
170171async function createOceanLayer ( { bounds, name, resolution, source } ) {
@@ -220,17 +221,18 @@ async function convertLayersToTopojson({ name, resolution }) {
220221 if ( ! fs . existsSync ( regionDir ) ) return ;
221222
222223 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
224225 if ( [ "antarctica" , "world" ] . includes ( name ) ) {
225226 const geojsonObjects = { }
226227 for ( const layer of Object . keys ( config . layers ) ) {
227228 const filePath = path . join ( regionDir , `${ layer } .geojson` )
228- geojsonObjects [ layer ] = rewindGeojson ( getJsonFile ( filePath ) )
229+ geojsonObjects [ layer ] = geoStitch ( rewindGeojson ( getJsonFile ( filePath ) ) )
229230 }
230- const topojsonTopology = topojsonLib . topology ( geojsonObjects , { 'property-transform' : f => f . properties } )
231+ // Convert geojson to topojson
232+ const topojsonTopology = topology ( geojsonObjects , 1000000 )
231233 fs . writeFileSync ( outputFile , JSON . stringify ( topojsonTopology ) ) ;
232234 } else {
233- // Layer names default to file names
235+ // In Mapshaper, layer names default to file names
234236 const commands = [ `${ regionDir } /*.geojson combine-files` , `-o format=topojson ${ outputFile } ` ] . join ( ' ' ) ;
235237 await mapshaper . runCommands ( commands ) ;
236238 }
@@ -241,50 +243,130 @@ async function convertLayersToTopojson({ name, resolution }) {
241243 fs . writeFileSync ( outputFile , JSON . stringify ( prunedTopojson ) ) ;
242244}
243245
244- // Get polygon features from UN GeoJSON and patch Antarctica gap
246+ // Get required polygon features from UN GeoJSON
245247const 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 ,
251263 // Use 'snap-interval' to patch gap in Antarctica
252264 '-clean snap-interval=0.015 target=antarctica' ,
253265 // Add rectangle to extend Antarctica to bottom of world
254266 '-rectangle bbox=-180,-90,180,-89 name=antarctica_rectangle' ,
255267 '-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 )
269346
270347// 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 ) ;
277348
278349// Get countries from all polygon features
279- const inputFilePathCountries50m = outputFilePath50m ;
280350const outputFilePathCountries50m = `${ outputDirGeojson } /${ unFilename } _50m/countries.geojson` ;
281351const 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()'` ,
283364 `-filter '${ filters . countries } '` ,
284365 '-clean' ,
285366 `-o ${ outputFilePathCountries50m } `
286367] . join ( ' ' ) ;
287368await mapshaper . runCommands ( commandsCountries50m ) ;
369+ clampToAntimeridian ( outputFilePathCountries50m )
288370
289371// Get land from all polygon features
290372const inputFilePathLand50m = outputFilePathCountries50m ;
@@ -296,23 +378,15 @@ const commandsLand50m = [
296378] . join ( ' ' ) ;
297379await mapshaper . runCommands ( commandsLand50m ) ;
298380
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
308382
309383// Get countries from all polygon features
310- const inputFilePathCountries110m = outputFilePath110m ;
384+ const inputFilePathCountries110m = outputFilePathCountries50m ;
311385const outputFilePathCountries110m = `${ outputDirGeojson } /${ unFilename } _110m/countries.geojson` ;
312386const commandsCountries110m = [
313387 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
316390 '-clean snap-interval=0.015' ,
317391 `-o ${ outputFilePathCountries110m } `
318392] . join ( ' ' ) ;
0 commit comments