1
+ import rewind from '@mapbox/geojson-rewind'
1
2
import { geoIdentity , geoPath } from 'd3-geo' ;
2
- import { geoStitch } from 'd3-geo-projection'
3
3
import fs from 'fs' ;
4
4
import mapshaper from 'mapshaper' ;
5
5
import path from 'path' ;
6
- import config , { getNEFilename } from './config.mjs' ;
7
- import { topology } from 'topojson-server' ;
8
6
import topojsonLib from 'topojson' ;
9
- import rewind from '@mapbox/geojson-rewind'
7
+ import config , { getNEFilename } from './config.mjs' ;
10
8
11
9
const { filters, inputDir, layers, resolutions, scopes, unFilename, vectors } = config ;
12
10
@@ -32,19 +30,6 @@ function getJsonFile(filename) {
32
30
}
33
31
}
34
32
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
-
48
33
function addCentroidsToGeojson ( geojsonPath ) {
49
34
const geojson = getJsonFile ( geojsonPath ) ;
50
35
if ( ! geojson . features ) return ;
@@ -59,6 +44,100 @@ function addCentroidsToGeojson(geojsonPath) {
59
44
fs . writeFileSync ( geojsonPath , JSON . stringify ( { ...geojson , features } ) ) ;
60
45
}
61
46
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
+
62
141
async function createLandLayer ( { bounds, name, resolution, source } ) {
63
142
const inputFilePath = `${ outputDirGeojson } /${ name } _${ resolution } m/countries.geojson` ;
64
143
const outputFilePath = `${ outputDirGeojson } /${ name } _${ resolution } m/land.geojson` ;
@@ -80,9 +159,12 @@ async function createCoastlinesLayer({ bounds, name, resolution, source }) {
80
159
'-dissolve' ,
81
160
'-lines' ,
82
161
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' : '' ,
83
164
`-o ${ outputFilePath } `
84
165
] . join ( ' ' ) ;
85
166
await mapshaper . runCommands ( commands ) ;
167
+ if ( [ 'antarctica' , 'world' ] . includes ( name ) ) snapToAntimeridian ( outputFilePath )
86
168
}
87
169
88
170
async function createOceanLayer ( { bounds, name, resolution, source } ) {
@@ -133,96 +215,19 @@ async function createSubunitsLayer({ name, resolution, source }) {
133
215
addCentroidsToGeojson ( outputFilePath ) ;
134
216
}
135
217
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
-
203
218
async function convertLayersToTopojson ( { name, resolution } ) {
204
219
const regionDir = path . join ( outputDirGeojson , `${ name } _${ resolution } m` ) ;
205
220
if ( ! fs . existsSync ( regionDir ) ) return ;
206
221
207
222
const outputFile = `${ outputDirTopojson } /${ name } _${ resolution } m.json` ;
223
+ // Scopes with polygons that cross the antimeridian need to be stitched (via the topology call)
208
224
if ( [ "antarctica" , "world" ] . includes ( name ) ) {
209
- // if (false) {
210
- const files = fs . readdirSync ( regionDir )
211
225
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 ) )
220
229
}
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 } )
226
231
fs . writeFileSync ( outputFile , JSON . stringify ( topojsonTopology ) ) ;
227
232
} else {
228
233
// Layer names default to file names
@@ -231,28 +236,17 @@ async function convertLayersToTopojson({ name, resolution }) {
231
236
}
232
237
233
238
// 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 ) ) ;
237
242
}
238
243
239
244
// Get polygon features from UN GeoJSON and patch Antarctica gap
240
245
const inputFilePathUNGeojson = `${ inputDir } /${ unFilename } .geojson` ;
241
246
const 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
- }
251
247
snapToAntimeridian ( inputFilePathUNGeojson , inputFilePathUNGeojsonCleaned )
252
248
const commandsAllFeaturesCommon = [
253
- // TODO: Should I use the cleaned data or leave as is?
254
249
inputFilePathUNGeojsonCleaned ,
255
- // inputFilePathUNGeojson,
256
250
`-filter 'iso3cd === "ATA"' target=1 + name=antarctica` ,
257
251
// Use 'snap-interval' to patch gap in Antarctica
258
252
'-clean snap-interval=0.015 target=antarctica' ,
@@ -269,7 +263,8 @@ const commandsAllFeaturesCommon = [
269
263
'-erase source=caspian_sea target=all_features' ,
270
264
// Update country codes for disputed territories at Egypt/Sudan border: https://en.wikipedia.org/wiki/Egypt%E2%80%93Sudan_border
271
265
`-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'`
273
268
]
274
269
275
270
// Process 50m UN geodata
@@ -305,13 +300,11 @@ await mapshaper.runCommands(commandsLand50m);
305
300
const inputFilePath110m = outputFilePath50m ;
306
301
const outputFilePath110m = `${ outputDirGeojson } /${ unFilename } _110m/all_features.geojson` ;
307
302
const commandsAllFeatures110m = [
308
- // ...commandsAllFeaturesCommon,
309
303
inputFilePath110m ,
310
- '-simplify 10% rdp ' ,
304
+ '-simplify 20% ' ,
311
305
`-o target=1 ${ outputFilePath110m } `
312
306
] . join ( " " )
313
307
await mapshaper . runCommands ( commandsAllFeatures110m ) ;
314
- console . log ( commandsAllFeatures110m )
315
308
316
309
// Get countries from all polygon features
317
310
const inputFilePathCountries110m = outputFilePath110m ;
0 commit comments