1+ import rewind from '@mapbox/geojson-rewind'
12import { geoIdentity , geoPath } from 'd3-geo' ;
2- import { geoStitch } from 'd3-geo-projection'
33import fs from 'fs' ;
44import mapshaper from 'mapshaper' ;
55import path from 'path' ;
6- import config , { getNEFilename } from './config.mjs' ;
7- import { topology } from 'topojson-server' ;
86import topojsonLib from 'topojson' ;
9- import rewind from '@mapbox/geojson-rewind'
7+ import config , { getNEFilename } from './config.mjs' ;
108
119const { filters, inputDir, layers, resolutions, scopes, unFilename, vectors } = config ;
1210
@@ -32,19 +30,6 @@ function getJsonFile(filename) {
3230 }
3331}
3432
35- async function createCountriesLayer ( { bounds, filter, name, resolution, source } ) {
36- const inputFilePath = `${ outputDirGeojson } /${ unFilename } _${ resolution } m/${ source } .geojson` ;
37- const outputFilePath = `${ outputDirGeojson } /${ name } _${ resolution } m/countries.geojson` ;
38- const commands = [
39- inputFilePath ,
40- bounds . length ? `-clip bbox=${ bounds . join ( ',' ) } ` : '' ,
41- filter ? `-filter '${ filter } '` : '' ,
42- `-o ${ outputFilePath } `
43- ] . join ( ' ' ) ;
44- await mapshaper . runCommands ( commands ) ;
45- addCentroidsToGeojson ( outputFilePath ) ;
46- }
47-
4833function addCentroidsToGeojson ( geojsonPath ) {
4934 const geojson = getJsonFile ( geojsonPath ) ;
5035 if ( ! geojson . features ) return ;
@@ -59,6 +44,100 @@ function addCentroidsToGeojson(geojsonPath) {
5944 fs . writeFileSync ( geojsonPath , JSON . stringify ( { ...geojson , features } ) ) ;
6045}
6146
47+ // Wind the polygon rings in the correct direction to indicate what is solid and what is whole
48+ const rewindGeojson = ( geojson , clockwise = true ) => rewind ( geojson , clockwise )
49+
50+ // Snap x-coordinates that are close to be on the antimeridian
51+ function snapToAntimeridian ( inputFilepath , outputFilepath ) {
52+ outputFilepath ||= inputFilepath
53+ const jsonString = fs . readFileSync ( inputFilepath , 'utf8' )
54+ const updatedString = jsonString
55+ . replaceAll ( / 1 7 9 \. 9 9 \d + , / g, '180,' )
56+ . replaceAll ( / 1 8 0 \. 0 0 \d + , / g, '180,' )
57+
58+ fs . writeFileSync ( outputFilepath , updatedString ) ;
59+ }
60+
61+ function pruneProperties ( topojson ) {
62+ for ( const layer in topojson . objects ) {
63+ switch ( layer ) {
64+ case 'countries' :
65+ topojson . objects [ layer ] . geometries = topojson . objects [ layer ] . geometries . map ( ( geometry ) => {
66+ const { properties } = geometry ;
67+ if ( properties ) {
68+ geometry . id = properties . iso3cd ;
69+ geometry . properties = {
70+ ct : properties . ct
71+ } ;
72+ }
73+
74+ return geometry ;
75+ } ) ;
76+ break ;
77+ case 'subunits' :
78+ topojson . objects [ layer ] . geometries = topojson . objects [ layer ] . geometries . map ( ( geometry ) => {
79+ const { properties } = geometry ;
80+ if ( properties ) {
81+ geometry . id = properties . postal ;
82+ geometry . properties = {
83+ ct : properties . ct ,
84+ gu : properties . gu_a3
85+ } ;
86+ }
87+
88+ return geometry ;
89+ } ) ;
90+
91+ break ;
92+ default :
93+ topojson . objects [ layer ] . geometries = topojson . objects [ layer ] . geometries . map ( ( geometry ) => {
94+ delete geometry . id ;
95+ delete geometry . properties ;
96+
97+ return geometry ;
98+ } ) ;
99+
100+ break ;
101+ }
102+ }
103+
104+ return topojson ;
105+ }
106+
107+ function getCentroid ( feature ) {
108+ const { type } = feature . geometry ;
109+ const projection = geoIdentity ( ) ;
110+ const path = geoPath ( projection ) ;
111+
112+ if ( type === 'MultiPolygon' ) {
113+ let maxArea = - Infinity ;
114+
115+ for ( const coordinates of feature . geometry . coordinates ) {
116+ const polygon = { type : 'Polygon' , coordinates } ;
117+ const area = path . area ( polygon ) ;
118+ if ( area > maxArea ) {
119+ maxArea = area ;
120+ feature = polygon ;
121+ }
122+ }
123+ }
124+
125+ return path . centroid ( feature ) . map ( ( coord ) => + coord . toFixed ( 2 ) ) ;
126+ }
127+
128+ async function createCountriesLayer ( { bounds, filter, name, resolution, source } ) {
129+ const inputFilePath = `${ outputDirGeojson } /${ unFilename } _${ resolution } m/${ source } .geojson` ;
130+ const outputFilePath = `${ outputDirGeojson } /${ name } _${ resolution } m/countries.geojson` ;
131+ const commands = [
132+ inputFilePath ,
133+ bounds . length ? `-clip bbox=${ bounds . join ( ',' ) } ` : '' ,
134+ filter ? `-filter '${ filter } '` : '' ,
135+ `-o ${ outputFilePath } `
136+ ] . join ( ' ' ) ;
137+ await mapshaper . runCommands ( commands ) ;
138+ addCentroidsToGeojson ( outputFilePath ) ;
139+ }
140+
62141async function createLandLayer ( { bounds, name, resolution, source } ) {
63142 const inputFilePath = `${ outputDirGeojson } /${ name } _${ resolution } m/countries.geojson` ;
64143 const outputFilePath = `${ outputDirGeojson } /${ name } _${ resolution } m/land.geojson` ;
@@ -80,9 +159,12 @@ async function createCoastlinesLayer({ bounds, name, resolution, source }) {
80159 '-dissolve' ,
81160 '-lines' ,
82161 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' : '' ,
83164 `-o ${ outputFilePath } `
84165 ] . join ( ' ' ) ;
85166 await mapshaper . runCommands ( commands ) ;
167+ if ( [ 'antarctica' , 'world' ] . includes ( name ) ) snapToAntimeridian ( outputFilePath )
86168}
87169
88170async function createOceanLayer ( { bounds, name, resolution, source } ) {
@@ -133,96 +215,19 @@ async function createSubunitsLayer({ name, resolution, source }) {
133215 addCentroidsToGeojson ( outputFilePath ) ;
134216}
135217
136- function pruneProperties ( topojson ) {
137- for ( const layer in topojson . objects ) {
138- switch ( layer ) {
139- case 'countries' :
140- topojson . objects [ layer ] . geometries = topojson . objects [ layer ] . geometries . map ( ( geometry ) => {
141- const { properties } = geometry ;
142- if ( properties ) {
143- geometry . id = properties . iso3cd ;
144- geometry . properties = {
145- ct : properties . ct
146- } ;
147- }
148-
149- return geometry ;
150- } ) ;
151- break ;
152- case 'subunits' :
153- topojson . objects [ layer ] . geometries = topojson . objects [ layer ] . geometries . map ( ( geometry ) => {
154- const { properties } = geometry ;
155- if ( properties ) {
156- geometry . id = properties . postal ;
157- geometry . properties = {
158- ct : properties . ct ,
159- gu : properties . gu_a3
160- } ;
161- }
162-
163- return geometry ;
164- } ) ;
165-
166- break ;
167- default :
168- topojson . objects [ layer ] . geometries = topojson . objects [ layer ] . geometries . map ( ( geometry ) => {
169- delete geometry . id ;
170- delete geometry . properties ;
171-
172- return geometry ;
173- } ) ;
174-
175- break ;
176- }
177- }
178-
179- return topojson ;
180- }
181-
182- function getCentroid ( feature ) {
183- const { type } = feature . geometry ;
184- const projection = geoIdentity ( ) ;
185- const path = geoPath ( projection ) ;
186-
187- if ( type === 'MultiPolygon' ) {
188- let maxArea = - Infinity ;
189-
190- for ( const coordinates of feature . geometry . coordinates ) {
191- const polygon = { type : 'Polygon' , coordinates } ;
192- const area = path . area ( polygon ) ;
193- if ( area > maxArea ) {
194- maxArea = area ;
195- feature = polygon ;
196- }
197- }
198- }
199-
200- return path . centroid ( feature ) . map ( ( coord ) => + coord . toFixed ( 2 ) ) ;
201- }
202-
203218async function convertLayersToTopojson ( { name, resolution } ) {
204219 const regionDir = path . join ( outputDirGeojson , `${ name } _${ resolution } m` ) ;
205220 if ( ! fs . existsSync ( regionDir ) ) return ;
206221
207222 const outputFile = `${ outputDirTopojson } /${ name } _${ resolution } m.json` ;
223+ // Scopes with polygons that cross the antimeridian need to be stitched (via the topology call)
208224 if ( [ "antarctica" , "world" ] . includes ( name ) ) {
209- // if (false) {
210- const files = fs . readdirSync ( regionDir )
211225 const geojsonObjects = { }
212- for ( const file of files ) {
213- const filePath = path . join ( regionDir , file )
214- const layer = file . split ( "." ) [ 0 ]
215- let stitchedGeojson = geoStitch ( getJsonFile ( filePath ) )
216- // stitchedGeojson = rewind(stitchedGeojson, true)
217- // fs.writeFileSync(filePath, JSON.stringify(stitchedGeojson));
218- geojsonObjects [ layer ] = stitchedGeojson
219- // geojsonObjects[layer] = getJsonFile(filePath)
226+ for ( const layer of Object . keys ( config . layers ) ) {
227+ const filePath = path . join ( regionDir , `${ layer } .geojson` )
228+ geojsonObjects [ layer ] = rewindGeojson ( getJsonFile ( filePath ) )
220229 }
221- const topojsonTopology = topology ( geojsonObjects )
222- // const topojsonTopology = topojsonLib.topology(geojsonObjects, {
223- // verbose: true,
224- // 'property-transform': f => f.properties
225- // })
230+ const topojsonTopology = topojsonLib . topology ( geojsonObjects , { 'property-transform' : f => f . properties } )
226231 fs . writeFileSync ( outputFile , JSON . stringify ( topojsonTopology ) ) ;
227232 } else {
228233 // Layer names default to file names
@@ -231,28 +236,17 @@ async function convertLayersToTopojson({ name, resolution }) {
231236 }
232237
233238 // Remove extra information from features
234- // const topojson = getJsonFile(outputFile);
235- // const prunedTopojson = pruneProperties(topojson);
236- // fs.writeFileSync(outputFile, JSON.stringify(prunedTopojson));
239+ const topojson = getJsonFile ( outputFile ) ;
240+ const prunedTopojson = pruneProperties ( topojson ) ;
241+ fs . writeFileSync ( outputFile , JSON . stringify ( prunedTopojson ) ) ;
237242}
238243
239244// Get polygon features from UN GeoJSON and patch Antarctica gap
240245const inputFilePathUNGeojson = `${ inputDir } /${ unFilename } .geojson` ;
241246const inputFilePathUNGeojsonCleaned = `${ inputDir } /${ unFilename } _cleaned.geojson` ;
242- // TODO: Update all x-coords close to 180 to be exactly 180
243- function snapToAntimeridian ( inputFilepath , outputFilepath ) {
244- const jsonString = fs . readFileSync ( inputFilepath , 'utf8' )
245- const updatedString = jsonString
246- . replaceAll ( / 1 7 9 \. 9 9 \d + , / g, '180,' )
247- . replaceAll ( / 1 8 0 \. 0 0 \d + , / g, '180,' )
248-
249- fs . writeFileSync ( outputFilepath , updatedString ) ;
250- }
251247snapToAntimeridian ( inputFilePathUNGeojson , inputFilePathUNGeojsonCleaned )
252248const commandsAllFeaturesCommon = [
253- // TODO: Should I use the cleaned data or leave as is?
254249 inputFilePathUNGeojsonCleaned ,
255- // inputFilePathUNGeojson,
256250 `-filter 'iso3cd === "ATA"' target=1 + name=antarctica` ,
257251 // Use 'snap-interval' to patch gap in Antarctica
258252 '-clean snap-interval=0.015 target=antarctica' ,
@@ -269,7 +263,8 @@ const commandsAllFeaturesCommon = [
269263 '-erase source=caspian_sea target=all_features' ,
270264 // Update country codes for disputed territories at Egypt/Sudan border: https://en.wikipedia.org/wiki/Egypt%E2%80%93Sudan_border
271265 `-each 'if (globalid === "{CA12D116-7A19-41D1-9622-17C12CCC720D}") iso3cd = "XHT"'` , // Halaib Triangle
272- `-each 'if (globalid === "{9FD54A50-0BFB-4385-B342-1C3BDEE5ED9B}") iso3cd = "XBT"'` // Bir Tawil
266+ `-each 'if (globalid === "{9FD54A50-0BFB-4385-B342-1C3BDEE5ED9B}") iso3cd = "XBT"'` , // Bir Tawil
267+ `-each 'FID = iso3cd'`
273268]
274269
275270// Process 50m UN geodata
@@ -305,13 +300,11 @@ await mapshaper.runCommands(commandsLand50m);
305300const inputFilePath110m = outputFilePath50m ;
306301const outputFilePath110m = `${ outputDirGeojson } /${ unFilename } _110m/all_features.geojson` ;
307302const commandsAllFeatures110m = [
308- // ...commandsAllFeaturesCommon,
309303 inputFilePath110m ,
310- '-simplify 10% rdp ' ,
304+ '-simplify 20% ' ,
311305 `-o target=1 ${ outputFilePath110m } `
312306] . join ( " " )
313307await mapshaper . runCommands ( commandsAllFeatures110m ) ;
314- console . log ( commandsAllFeatures110m )
315308
316309// Get countries from all polygon features
317310const inputFilePathCountries110m = outputFilePath110m ;
0 commit comments