1var exec = require('child_process').exec 2var fs = require('fs') 3 4var area = require('@mapbox/geojson-area') 5var geojsonhint = require('@mapbox/geojsonhint') 6var bbox = require('@turf/bbox').default 7var helpers = require('@turf/helpers') 8var multiPolygon = helpers.multiPolygon 9var polygon = helpers.polygon 10var asynclib = require('async') 11var jsts = require('jsts') 12var rimraf = require('rimraf') 13var overpass = require('query-overpass') 14 15const ProgressStats = require('./progressStats') 16 17var osmBoundarySources = require('./osmBoundarySources.json') 18var zoneCfg = require('./timezones.json') 19var expectedZoneOverlaps = require('./expectedZoneOverlaps.json') 20 21// allow building of only a specified zones 22var filteredIndex = process.argv.indexOf('--filtered-zones') 23let filteredZones = [] 24if (filteredIndex > -1 && process.argv[filteredIndex + 1]) { 25 filteredZones = process.argv[filteredIndex + 1].split(',') 26 var newZoneCfg = {} 27 filteredZones.forEach((zoneName) => { 28 newZoneCfg[zoneName] = zoneCfg[zoneName] 29 }) 30 zoneCfg = newZoneCfg 31 32 // filter out unneccessary downloads 33 var newOsmBoundarySources = {} 34 Object.keys(zoneCfg).forEach((zoneName) => { 35 zoneCfg[zoneName].forEach((op) => { 36 if (op.source === 'overpass') { 37 newOsmBoundarySources[op.id] = osmBoundarySources[op.id] 38 } 39 }) 40 }) 41 42 osmBoundarySources = newOsmBoundarySources 43} 44 45var geoJsonReader = new jsts.io.GeoJSONReader() 46var geoJsonWriter = new jsts.io.GeoJSONWriter() 47var precisionModel = new jsts.geom.PrecisionModel(1000000) 48var precisionReducer = new jsts.precision.GeometryPrecisionReducer(precisionModel) 49var distZones = {} 50var minRequestGap = 4 51var curRequestGap = 4 52 53var safeMkdir = function (dirname, callback) { 54 fs.mkdir(dirname, function (err) { 55 if (err && err.code === 'EEXIST') { 56 callback() 57 } else { 58 callback(err) 59 } 60 }) 61} 62 63var debugGeo = function (op, a, b, reducePrecision) { 64 var result 65 66 if (reducePrecision) { 67 a = precisionReducer.reduce(a) 68 b = precisionReducer.reduce(b) 69 } 70 71 try { 72 switch (op) { 73 case 'union': 74 result = a.union(b) 75 break 76 case 'intersection': 77 result = a.intersection(b) 78 break 79 case 'intersects': 80 result = a.intersects(b) 81 break 82 case 'diff': 83 result = a.difference(b) 84 break 85 default: 86 var err = new Error('invalid op: ' + op) 87 throw err 88 } 89 } catch (e) { 90 if (e.name === 'TopologyException') { 91 console.log('Encountered TopologyException, retry with GeometryPrecisionReducer') 92 return debugGeo(op, a, b, true) 93 } 94 console.log('op err') 95 console.log(e) 96 console.log(e.stack) 97 fs.writeFileSync('debug_' + op + '_a.json', JSON.stringify(geoJsonWriter.write(a))) 98 fs.writeFileSync('debug_' + op + '_b.json', JSON.stringify(geoJsonWriter.write(b))) 99 throw e 100 } 101 102 return result 103} 104 105var fetchIfNeeded = function (file, superCallback, downloadCallback, fetchFn) { 106 // check for file that got downloaded 107 fs.stat(file, function (err) { 108 if (!err) { 109 // file found, skip download steps 110 return superCallback() 111 } 112 // check for manual file that got fixed and needs validation 113 var fixedFile = file.replace('.json', '_fixed.json') 114 fs.stat(fixedFile, function (err) { 115 if (!err) { 116 // file found, return fixed file 117 return downloadCallback(null, require(fixedFile)) 118 } 119 // no manual fixed file found, download from overpass 120 fetchFn() 121 }) 122 }) 123} 124 125var geoJsonToGeom = function (geoJson) { 126 try { 127 return geoJsonReader.read(JSON.stringify(geoJson)) 128 } catch (e) { 129 console.error('error converting geojson to geometry') 130 fs.writeFileSync('debug_geojson_read_error.json', JSON.stringify(geoJson)) 131 throw e 132 } 133} 134 135var geomToGeoJson = function (geom) { 136 return geoJsonWriter.write(geom) 137} 138 139var geomToGeoJsonString = function (geom) { 140 return JSON.stringify(geoJsonWriter.write(geom)) 141} 142 143const downloadProgress = new ProgressStats( 144 'Downloading', 145 Object.keys(osmBoundarySources).length 146) 147 148var downloadOsmBoundary = function (boundaryId, boundaryCallback) { 149 var cfg = osmBoundarySources[boundaryId] 150 var query = '[out:json][timeout:60];(' 151 if (cfg.way) { 152 query += 'way' 153 } else { 154 query += 'relation' 155 } 156 var boundaryFilename = './downloads/' + boundaryId + '.json' 157 var debug = 'getting data for ' + boundaryId 158 var queryKeys = Object.keys(cfg) 159 160 for (var i = queryKeys.length - 1; i >= 0; i--) { 161 var k = queryKeys[i] 162 if (k === 'way') continue 163 var v = cfg[k] 164 165 query += '["' + k + '"="' + v + '"]' 166 } 167 168 query += ';);out body;>;out meta qt;' 169 170 downloadProgress.beginTask(debug, true) 171 172 asynclib.auto({ 173 downloadFromOverpass: function (cb) { 174 console.log('downloading from overpass') 175 fetchIfNeeded(boundaryFilename, boundaryCallback, cb, function () { 176 var overpassResponseHandler = function (err, data) { 177 if (err) { 178 console.log(err) 179 console.log('Increasing overpass request gap') 180 curRequestGap *= 2 181 makeQuery() 182 } else { 183 console.log('Success, decreasing overpass request gap') 184 curRequestGap = Math.max(minRequestGap, curRequestGap / 2) 185 cb(null, data) 186 } 187 } 188 var makeQuery = function () { 189 console.log('waiting ' + curRequestGap + ' seconds') 190 setTimeout(function () { 191 overpass(query, overpassResponseHandler, { flatProperties: true }) 192 }, curRequestGap * 1000) 193 } 194 makeQuery() 195 }) 196 }, 197 validateOverpassResult: ['downloadFromOverpass', function (results, cb) { 198 var data = results.downloadFromOverpass 199 if (!data.features) { 200 var err = new Error('Invalid geojson for boundary: ' + boundaryId) 201 return cb(err) 202 } 203 if (data.features.length === 0) { 204 console.error('No data for the following query:') 205 console.error(query) 206 console.error('To read more about this error, please visit https://git.io/vxKQL') 207 return cb(new Error('No data found for from overpass query')) 208 } 209 cb() 210 }], 211 saveSingleMultiPolygon: ['validateOverpassResult', function (results, cb) { 212 var data = results.downloadFromOverpass 213 var combined 214 215 // union all multi-polygons / polygons into one 216 for (var i = data.features.length - 1; i >= 0; i--) { 217 var curOsmGeom = data.features[i].geometry 218 const curOsmProps = data.features[i].properties 219 if ( 220 (curOsmGeom.type === 'Polygon' || curOsmGeom.type === 'MultiPolygon') && 221 curOsmProps.type === 'boundary' // need to make sure enclaves aren't unioned 222 ) { 223 console.log('combining border') 224 let errors = geojsonhint.hint(curOsmGeom) 225 if (errors && errors.length > 0) { 226 const stringifiedGeojson = JSON.stringify(curOsmGeom, null, 2) 227 errors = geojsonhint.hint(stringifiedGeojson) 228 console.error('Invalid geojson received in Overpass Result') 229 console.error('Overpass query: ' + query) 230 const problemFilename = boundaryId + '_convert_to_geom_error.json' 231 fs.writeFileSync(problemFilename, stringifiedGeojson) 232 console.error('saved problem file to ' + problemFilename) 233 console.error('To read more about this error, please visit https://git.io/vxKQq') 234 return cb(errors) 235 } 236 try { 237 var curGeom = geoJsonToGeom(curOsmGeom) 238 } catch (e) { 239 console.error('error converting overpass result to geojson') 240 console.error(e) 241 242 fs.writeFileSync(boundaryId + '_convert_to_geom_error-all-features.json', JSON.stringify(data)) 243 return cb(e) 244 } 245 if (!combined) { 246 combined = curGeom 247 } else { 248 combined = debugGeo('union', curGeom, combined) 249 } 250 } 251 } 252 try { 253 fs.writeFile(boundaryFilename, geomToGeoJsonString(combined), cb) 254 } catch (e) { 255 console.error('error writing combined border to geojson') 256 fs.writeFileSync(boundaryId + '_combined_border_convert_to_geom_error.json', JSON.stringify(data)) 257 return cb(e) 258 } 259 }] 260 }, boundaryCallback) 261} 262 263var getTzDistFilename = function (tzid) { 264 return './dist/' + tzid.replace(/\//g, '__') + '.json' 265} 266 267/** 268 * Get the geometry of the requested source data 269 * 270 * @return {Object} geom The geometry of the source 271 * @param {Object} source An object representing the data source 272 * must have `source` key and then either: 273 * - `id` if from a file 274 * - `id` if from a file 275 */ 276var getDataSource = function (source) { 277 var geoJson 278 if (source.source === 'overpass') { 279 geoJson = require('./downloads/' + source.id + '.json') 280 } else if (source.source === 'manual-polygon') { 281 geoJson = polygon(source.data).geometry 282 } else if (source.source === 'manual-multipolygon') { 283 geoJson = multiPolygon(source.data).geometry 284 } else if (source.source === 'dist') { 285 geoJson = require(getTzDistFilename(source.id)) 286 } else { 287 var err = new Error('unknown source: ' + source.source) 288 throw err 289 } 290 return geoJsonToGeom(geoJson) 291} 292 293/** 294 * Post process created timezone boundary. 295 * - remove small holes and exclaves 296 * - reduce geometry precision 297 * 298 * @param {Geometry} geom The jsts geometry of the timezone 299 * @param {boolean} returnAsObject if true, return as object, otherwise return stringified 300 * @return {Object|String} geojson as object or stringified 301 */ 302var postProcessZone = function (geom, returnAsObject) { 303 // reduce precision of geometry 304 const geojson = geomToGeoJson(precisionReducer.reduce(geom)) 305 306 // iterate through all polygons 307 const filteredPolygons = [] 308 let allPolygons = geojson.coordinates 309 if (geojson.type === 'Polygon') { 310 allPolygons = [geojson.coordinates] 311 } 312 313 allPolygons.forEach((curPolygon, idx) => { 314 // remove any polygon with very small area 315 const polygonFeature = polygon(curPolygon) 316 const polygonArea = area.geometry(polygonFeature.geometry) 317 318 if (polygonArea < 1) return 319 320 // find all holes 321 const filteredLinearRings = [] 322 323 curPolygon.forEach((curLinearRing, lrIdx) => { 324 if (lrIdx === 0) { 325 // always keep first linearRing 326 filteredLinearRings.push(curLinearRing) 327 } else { 328 const polygonFromLinearRing = polygon([curLinearRing]) 329 const linearRingArea = area.geometry(polygonFromLinearRing.geometry) 330 331 // only include holes with relevant area 332 if (linearRingArea > 1) { 333 filteredLinearRings.push(curLinearRing) 334 } 335 } 336 }) 337 338 filteredPolygons.push(filteredLinearRings) 339 }) 340 341 // recompile to geojson string 342 const newGeojson = { 343 type: geojson.type 344 } 345 346 if (geojson.type === 'Polygon') { 347 newGeojson.coordinates = filteredPolygons[0] 348 } else { 349 newGeojson.coordinates = filteredPolygons 350 } 351 352 return returnAsObject ? newGeojson : JSON.stringify(newGeojson) 353} 354 355const buildingProgress = new ProgressStats( 356 'Building', 357 Object.keys(zoneCfg).length 358) 359 360var makeTimezoneBoundary = function (tzid, callback) { 361 buildingProgress.beginTask(`makeTimezoneBoundary for ${tzid}`, true) 362 363 var ops = zoneCfg[tzid] 364 var geom 365 366 asynclib.eachSeries(ops, function (task, cb) { 367 var taskData = getDataSource(task) 368 console.log('-', task.op, task.id) 369 if (task.op === 'init') { 370 geom = taskData 371 } else if (task.op === 'intersect') { 372 geom = debugGeo('intersection', geom, taskData) 373 } else if (task.op === 'difference') { 374 geom = debugGeo('diff', geom, taskData) 375 } else if (task.op === 'difference-reverse-order') { 376 geom = debugGeo('diff', taskData, geom) 377 } else if (task.op === 'union') { 378 geom = debugGeo('union', geom, taskData) 379 } else { 380 var err = new Error('unknown op: ' + task.op) 381 return cb(err) 382 } 383 cb() 384 }, 385 function (err) { 386 if (err) { return callback(err) } 387 fs.writeFile(getTzDistFilename(tzid), 388 postProcessZone(geom), 389 callback) 390 }) 391} 392 393var loadDistZonesIntoMemory = function () { 394 console.log('load zones into memory') 395 var zones = Object.keys(zoneCfg) 396 var tzid 397 398 for (var i = 0; i < zones.length; i++) { 399 tzid = zones[i] 400 distZones[tzid] = getDataSource({ source: 'dist', id: tzid }) 401 } 402} 403 404var getDistZoneGeom = function (tzid) { 405 return distZones[tzid] 406} 407 408var roundDownToTenth = function (n) { 409 return Math.floor(n * 10) / 10 410} 411 412var roundUpToTenth = function (n) { 413 return Math.ceil(n * 10) / 10 414} 415 416var formatBounds = function (bounds) { 417 let boundsStr = '[' 418 boundsStr += roundDownToTenth(bounds[0]) + ', ' 419 boundsStr += roundDownToTenth(bounds[1]) + ', ' 420 boundsStr += roundUpToTenth(bounds[2]) + ', ' 421 boundsStr += roundUpToTenth(bounds[3]) + ']' 422 return boundsStr 423} 424 425var validateTimezoneBoundaries = function () { 426 const numZones = Object.keys(zoneCfg).length 427 const validationProgress = new ProgressStats( 428 'Validation', 429 numZones * (numZones + 1) / 2 430 ) 431 432 console.log('do validation... this may take a few minutes') 433 var allZonesOk = true 434 var zones = Object.keys(zoneCfg) 435 var lastPct = 0 436 var compareTzid, tzid, zoneGeom 437 438 for (var i = 0; i < zones.length; i++) { 439 tzid = zones[i] 440 zoneGeom = getDistZoneGeom(tzid) 441 442 for (var j = i + 1; j < zones.length; j++) { 443 const curPct = Math.floor(validationProgress.getPercentage()) 444 if (curPct % 10 === 0 && curPct !== lastPct) { 445 validationProgress.printStats('Validating zones', true) 446 lastPct = curPct 447 } 448 compareTzid = zones[j] 449 450 var compareZoneGeom = getDistZoneGeom(compareTzid) 451 452 var intersects = false 453 try { 454 intersects = debugGeo('intersects', zoneGeom, compareZoneGeom) 455 } catch (e) { 456 console.warn('warning, encountered intersection error with zone ' + tzid + ' and ' + compareTzid) 457 } 458 if (intersects) { 459 var intersectedGeom = debugGeo('intersection', zoneGeom, compareZoneGeom) 460 var intersectedArea = intersectedGeom.getArea() 461 462 if (intersectedArea > 0.0001) { 463 // check if the intersected area(s) are one of the expected areas of overlap 464 const allowedOverlapBounds = expectedZoneOverlaps[`${tzid}-${compareTzid}`] || expectedZoneOverlaps[`${compareTzid}-${tzid}`] 465 const overlapsGeoJson = geoJsonWriter.write(intersectedGeom) 466 467 // these zones are allowed to overlap in certain places, make sure the 468 // found overlap(s) all fit within the expected areas of overlap 469 if (allowedOverlapBounds) { 470 // if the overlaps are a multipolygon, make sure each individual 471 // polygon of overlap fits within at least one of the expected 472 // overlaps 473 let overlapsPolygons 474 switch (overlapsGeoJson.type) { 475 case 'MultiPolygon': 476 overlapsPolygons = overlapsGeoJson.coordinates.map( 477 polygonCoords => ({ 478 coordinates: polygonCoords, 479 type: 'Polygon' 480 }) 481 ) 482 break 483 case 'Polygon': 484 overlapsPolygons = [overlapsGeoJson] 485 break 486 case 'GeometryCollection': 487 overlapsPolygons = [] 488 overlapsGeoJson.geometries.forEach(geom => { 489 if (geom.type === 'Polygon') { 490 overlapsPolygons.push(geom) 491 } else if (geom.type === 'MultiPolygon') { 492 geom.coordinates.forEach(polygonCoords => { 493 overlapsPolygons.push({ 494 coordinates: polygonCoords, 495 type: 'Polygon' 496 }) 497 }) 498 } 499 }) 500 break 501 default: 502 console.error('unexpected geojson overlap type') 503 console.log(overlapsGeoJson) 504 break 505 } 506 507 let allOverlapsOk = true 508 overlapsPolygons.forEach((polygon, idx) => { 509 const bounds = bbox(polygon) 510 const polygonArea = area.geometry(polygon) 511 if ( 512 polygonArea > 10 && // ignore small polygons 513 !allowedOverlapBounds.some(allowedBounds => 514 allowedBounds.bounds[0] <= bounds[0] && // minX 515 allowedBounds.bounds[1] <= bounds[1] && // minY 516 allowedBounds.bounds[2] >= bounds[2] && // maxX 517 allowedBounds.bounds[3] >= bounds[3] // maxY 518 ) 519 ) { 520 console.error(`Unexpected intersection (${polygonArea} area) with bounds: ${formatBounds(bounds)}`) 521 allOverlapsOk = false 522 } 523 }) 524 525 if (allOverlapsOk) continue 526 } 527 528 // at least one unexpected overlap found, output an error and write debug file 529 console.error('Validation error: ' + tzid + ' intersects ' + compareTzid + ' area: ' + intersectedArea) 530 const debugFilename = tzid.replace(/\//g, '-') + '-' + compareTzid.replace(/\//g, '-') + '-overlap.json' 531 fs.writeFileSync( 532 debugFilename, 533 JSON.stringify(overlapsGeoJson) 534 ) 535 console.error('wrote overlap area as file ' + debugFilename) 536 console.error('To read more about this error, please visit https://git.io/vx6nx') 537 allZonesOk = false 538 } 539 } 540 validationProgress.logNext() 541 } 542 } 543 544 return allZonesOk ? null : 'Zone validation unsuccessful' 545} 546 547let oceanZoneBoundaries 548let oceanZones = [ 549 { tzid: 'Etc/GMT-12', left: 172.5, right: 180 }, 550 { tzid: 'Etc/GMT-11', left: 157.5, right: 172.5 }, 551 { tzid: 'Etc/GMT-10', left: 142.5, right: 157.5 }, 552 { tzid: 'Etc/GMT-9', left: 127.5, right: 142.5 }, 553 { tzid: 'Etc/GMT-8', left: 112.5, right: 127.5 }, 554 { tzid: 'Etc/GMT-7', left: 97.5, right: 112.5 }, 555 { tzid: 'Etc/GMT-6', left: 82.5, right: 97.5 }, 556 { tzid: 'Etc/GMT-5', left: 67.5, right: 82.5 }, 557 { tzid: 'Etc/GMT-4', left: 52.5, right: 67.5 }, 558 { tzid: 'Etc/GMT-3', left: 37.5, right: 52.5 }, 559 { tzid: 'Etc/GMT-2', left: 22.5, right: 37.5 }, 560 { tzid: 'Etc/GMT-1', left: 7.5, right: 22.5 }, 561 { tzid: 'Etc/GMT', left: -7.5, right: 7.5 }, 562 { tzid: 'Etc/GMT+1', left: -22.5, right: -7.5 }, 563 { tzid: 'Etc/GMT+2', left: -37.5, right: -22.5 }, 564 { tzid: 'Etc/GMT+3', left: -52.5, right: -37.5 }, 565 { tzid: 'Etc/GMT+4', left: -67.5, right: -52.5 }, 566 { tzid: 'Etc/GMT+5', left: -82.5, right: -67.5 }, 567 { tzid: 'Etc/GMT+6', left: -97.5, right: -82.5 }, 568 { tzid: 'Etc/GMT+7', left: -112.5, right: -97.5 }, 569 { tzid: 'Etc/GMT+8', left: -127.5, right: -112.5 }, 570 { tzid: 'Etc/GMT+9', left: -142.5, right: -127.5 }, 571 { tzid: 'Etc/GMT+10', left: -157.5, right: -142.5 }, 572 { tzid: 'Etc/GMT+11', left: -172.5, right: -157.5 }, 573 { tzid: 'Etc/GMT+12', left: -180, right: -172.5 } 574] 575 576if (filteredZones.length > 0) { 577 oceanZones = oceanZones.filter(oceanZone => filteredZones.indexOf(oceanZone) > -1) 578} 579 580var addOceans = function (callback) { 581 console.log('adding ocean boundaries') 582 const zones = Object.keys(zoneCfg) 583 584 const oceanProgress = new ProgressStats( 585 'Oceans', 586 oceanZones.length 587 ) 588 589 oceanZoneBoundaries = oceanZones.map(zone => { 590 oceanProgress.beginTask(zone.tzid, true) 591 const geoJson = polygon([[ 592 [zone.left, 90], 593 [zone.left, -90], 594 [zone.right, -90], 595 [zone.right, 90], 596 [zone.left, 90] 597 ]]).geometry 598 599 let geom = geoJsonToGeom(geoJson) 600 601 // diff against every zone 602 zones.forEach(distZone => { 603 geom = debugGeo('diff', geom, getDistZoneGeom(distZone)) 604 }) 605 606 return { 607 geom: postProcessZone(geom, true), 608 tzid: zone.tzid 609 } 610 }) 611 612 callback() 613} 614 615var combineAndWriteZones = function (callback) { 616 var stream = fs.createWriteStream('./dist/combined.json') 617 var streamWithOceans = fs.createWriteStream('./dist/combined-with-oceans.json') 618 var zones = Object.keys(zoneCfg) 619 620 stream.write('{"type":"FeatureCollection","features":[') 621 streamWithOceans.write('{"type":"FeatureCollection","features":[') 622 623 for (var i = 0; i < zones.length; i++) { 624 if (i > 0) { 625 stream.write(',') 626 streamWithOceans.write(',') 627 } 628 var feature = { 629 type: 'Feature', 630 properties: { tzid: zones[i] }, 631 geometry: geomToGeoJson(getDistZoneGeom(zones[i])) 632 } 633 const stringified = JSON.stringify(feature) 634 stream.write(stringified) 635 streamWithOceans.write(stringified) 636 } 637 oceanZoneBoundaries.forEach(boundary => { 638 streamWithOceans.write(',') 639 var feature = { 640 type: 'Feature', 641 properties: { tzid: boundary.tzid }, 642 geometry: boundary.geom 643 } 644 streamWithOceans.write(JSON.stringify(feature)) 645 }) 646 asynclib.parallel([ 647 cb => { 648 stream.end(']}', cb) 649 }, 650 cb => { 651 streamWithOceans.end(']}', cb) 652 } 653 ], callback) 654} 655 656const autoScript = { 657 makeDownloadsDir: function (cb) { 658 overallProgress.beginTask('Creating downloads dir') 659 safeMkdir('./downloads', cb) 660 }, 661 makeDistDir: function (cb) { 662 overallProgress.beginTask('Creating dist dir') 663 safeMkdir('./dist', cb) 664 }, 665 getOsmBoundaries: ['makeDownloadsDir', function (results, cb) { 666 overallProgress.beginTask('Downloading osm boundaries') 667 asynclib.eachSeries(Object.keys(osmBoundarySources), downloadOsmBoundary, cb) 668 }], 669 createZones: ['makeDistDir', 'getOsmBoundaries', function (results, cb) { 670 overallProgress.beginTask('Creating timezone boundaries') 671 asynclib.each(Object.keys(zoneCfg), makeTimezoneBoundary, cb) 672 }], 673 validateZones: ['createZones', function (results, cb) { 674 overallProgress.beginTask('Validating timezone boundaries') 675 loadDistZonesIntoMemory() 676 if (process.argv.indexOf('no-validation') > -1) { 677 console.warn('WARNING: Skipping validation!') 678 cb() 679 } else { 680 cb(validateTimezoneBoundaries()) 681 } 682 }], 683 addOceans: ['validateZones', function (results, cb) { 684 overallProgress.beginTask('Adding oceans') 685 addOceans(cb) 686 }], 687 mergeZones: ['addOceans', function (results, cb) { 688 overallProgress.beginTask('Merging zones') 689 combineAndWriteZones(cb) 690 }], 691 zipGeoJson: ['mergeZones', function (results, cb) { 692 overallProgress.beginTask('Zipping geojson') 693 exec('zip dist/timezones.geojson.zip dist/combined.json', cb) 694 }], 695 zipGeoJsonWithOceans: ['mergeZones', function (results, cb) { 696 overallProgress.beginTask('Zipping geojson with oceans') 697 exec('zip dist/timezones-with-oceans.geojson.zip dist/combined-with-oceans.json', cb) 698 }], 699 makeShapefile: ['mergeZones', function (results, cb) { 700 overallProgress.beginTask('Converting from geojson to shapefile') 701 rimraf.sync('dist/combined-shapefile.*') 702 exec( 703 'ogr2ogr -f "ESRI Shapefile" dist/combined-shapefile.shp dist/combined.json', 704 function (err, stdout, stderr) { 705 if (err) { return cb(err) } 706 exec('zip dist/timezones.shapefile.zip dist/combined-shapefile.*', cb) 707 } 708 ) 709 }], 710 makeShapefileWithOceans: ['mergeZones', function (results, cb) { 711 overallProgress.beginTask('Converting from geojson with oceans to shapefile') 712 rimraf.sync('dist/combined-shapefile-with-oceans.*') 713 exec( 714 'ogr2ogr -f "ESRI Shapefile" dist/combined-shapefile-with-oceans.shp dist/combined-with-oceans.json', 715 function (err, stdout, stderr) { 716 if (err) { return cb(err) } 717 exec('zip dist/timezones-with-oceans.shapefile.zip dist/combined-shapefile-with-oceans.*', cb) 718 } 719 ) 720 }], 721 makeListOfTimeZoneNames: function (cb) { 722 overallProgress.beginTask('Writing timezone names to file') 723 let zoneNames = Object.keys(zoneCfg) 724 oceanZones.forEach(oceanZone => { 725 zoneNames.push(oceanZone.tzid) 726 }) 727 if (filteredZones.length > 0) { 728 zoneNames = zoneNames.filter(zoneName => filteredZones.indexOf(zoneName) > -1) 729 } 730 fs.writeFile( 731 'dist/timezone-names.json', 732 JSON.stringify(zoneNames), 733 cb 734 ) 735 } 736} 737 738const overallProgress = new ProgressStats('Overall', Object.keys(autoScript).length) 739 740asynclib.auto(autoScript, function (err, results) { 741 console.log('done') 742 if (err) { 743 console.log('error!', err) 744 } 745}) 746