Skip to content

Commit 60171d7

Browse files
committed
Split out antimeridian fixes
This is necessary because Mapshaper applies changes to a child layer to the parent as well (for some commands).
1 parent eb1e966 commit 60171d7

File tree

1 file changed

+126
-52
lines changed

1 file changed

+126
-52
lines changed

tasks/topojson/process_geodata.mjs

Lines changed: 126 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import rewind from '@mapbox/geojson-rewind'
22
import { geoIdentity, geoPath } from 'd3-geo';
3+
import { geoStitch } from "d3-geo-projection"
34
import fs from 'fs';
45
import mapshaper from 'mapshaper';
56
import path from 'path';
6-
import topojsonLib from 'topojson';
7+
import { topology } from 'topojson-server'
78
import config, { getNEFilename } from './config.mjs';
89

910
const { 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
4849
const 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(/179\.99\d+,/g, '180,')
56-
.replaceAll(/180\.00\d+,/g, '180,')
56+
.replaceAll(/179\.9999\d+,/g, '180,')
57+
.replaceAll(/180\.0000\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

170171
async 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
245247
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,
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;
280350
const outputFilePathCountries50m = `${outputDirGeojson}/${unFilename}_50m/countries.geojson`;
281351
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()'`,
283364
`-filter '${filters.countries}'`,
284365
'-clean',
285366
`-o ${outputFilePathCountries50m}`
286367
].join(' ');
287368
await mapshaper.runCommands(commandsCountries50m);
369+
clampToAntimeridian(outputFilePathCountries50m)
288370

289371
// Get land from all polygon features
290372
const inputFilePathLand50m = outputFilePathCountries50m;
@@ -296,23 +378,15 @@ const commandsLand50m = [
296378
].join(' ');
297379
await 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;
311385
const outputFilePathCountries110m = `${outputDirGeojson}/${unFilename}_110m/countries.geojson`;
312386
const 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

Comments
 (0)