1*f0df9794SNeil Fullervar exec = require('child_process').exec 2*f0df9794SNeil Fullervar fs = require('fs') 3*f0df9794SNeil Fullervar path = require('path') 4*f0df9794SNeil Fuller 5*f0df9794SNeil Fullervar area = require('@mapbox/geojson-area') 6*f0df9794SNeil Fullervar geojsonhint = require('@mapbox/geojsonhint') 7*f0df9794SNeil Fullervar bbox = require('@turf/bbox').default 8*f0df9794SNeil Fullervar helpers = require('@turf/helpers') 9*f0df9794SNeil Fullervar multiPolygon = helpers.multiPolygon 10*f0df9794SNeil Fullervar polygon = helpers.polygon 11*f0df9794SNeil Fullervar asynclib = require('async') 12*f0df9794SNeil Fullervar https = require('follow-redirects').https 13*f0df9794SNeil Fullervar jsts = require('jsts') 14*f0df9794SNeil Fullervar rimraf = require('rimraf') 15*f0df9794SNeil Fullervar overpass = require('query-overpass') 16*f0df9794SNeil Fullervar yargs = require('yargs') 17*f0df9794SNeil Fuller 18*f0df9794SNeil Fullerconst FeatureWriterStream = require('./util/featureWriterStream') 19*f0df9794SNeil Fullerconst ProgressStats = require('./util/progressStats') 20*f0df9794SNeil Fuller 21*f0df9794SNeil Fullervar osmBoundarySources = require('./osmBoundarySources.json') 22*f0df9794SNeil Fullervar zoneCfg = require('./timezones.json') 23*f0df9794SNeil Fullervar expectedZoneOverlaps = require('./expectedZoneOverlaps.json') 24*f0df9794SNeil Fuller 25*f0df9794SNeil Fullerconst argv = yargs 26*f0df9794SNeil Fuller .option('downloads_dir', { 27*f0df9794SNeil Fuller description: 'Set the download location', 28*f0df9794SNeil Fuller default: './downloads', 29*f0df9794SNeil Fuller type: 'string' 30*f0df9794SNeil Fuller }) 31*f0df9794SNeil Fuller .option('dist_dir', { 32*f0df9794SNeil Fuller description: 'Set the dist location', 33*f0df9794SNeil Fuller default: './dist', 34*f0df9794SNeil Fuller type: 'string' 35*f0df9794SNeil Fuller }) 36*f0df9794SNeil Fuller .option('excluded_zones', { 37*f0df9794SNeil Fuller description: 'Exclude specified zones', 38*f0df9794SNeil Fuller type: 'array' 39*f0df9794SNeil Fuller }) 40*f0df9794SNeil Fuller .option('included_zones', { 41*f0df9794SNeil Fuller description: 'Include specified zones', 42*f0df9794SNeil Fuller type: 'array' 43*f0df9794SNeil Fuller }) 44*f0df9794SNeil Fuller .option('skip_analyze_diffs', { 45*f0df9794SNeil Fuller description: 'Skip analysis of diffs between versions', 46*f0df9794SNeil Fuller type: 'boolean' 47*f0df9794SNeil Fuller }) 48*f0df9794SNeil Fuller .option('skip_shapefile', { 49*f0df9794SNeil Fuller description: 'Skip shapefile creation', 50*f0df9794SNeil Fuller type: 'boolean' 51*f0df9794SNeil Fuller }) 52*f0df9794SNeil Fuller .option('skip_validation', { 53*f0df9794SNeil Fuller description: 'Skip validation', 54*f0df9794SNeil Fuller type: 'boolean' 55*f0df9794SNeil Fuller }) 56*f0df9794SNeil Fuller .option('skip_zip', { 57*f0df9794SNeil Fuller description: 'Skip zip creation', 58*f0df9794SNeil Fuller type: 'boolean' 59*f0df9794SNeil Fuller }) 60*f0df9794SNeil Fuller .help() 61*f0df9794SNeil Fuller .strict() 62*f0df9794SNeil Fuller .alias('help', 'h') 63*f0df9794SNeil Fuller .argv 64*f0df9794SNeil Fuller 65*f0df9794SNeil Fuller// Resolve the arguments with paths so relative paths become absolute. 66*f0df9794SNeil Fullerconst downloadsDir = path.resolve(argv.downloads_dir) 67*f0df9794SNeil Fullerconst distDir = path.resolve(argv.dist_dir) 68*f0df9794SNeil Fuller 69*f0df9794SNeil Fuller// allow building of only a specified zones 70*f0df9794SNeil Fullerlet includedZones = [] 71*f0df9794SNeil Fullerlet excludedZones = [] 72*f0df9794SNeil Fullerif (argv.included_zones || argv.excluded_zones) { 73*f0df9794SNeil Fuller if (argv.included_zones) { 74*f0df9794SNeil Fuller const newZoneCfg = {} 75*f0df9794SNeil Fuller includedZones = argv.included_zones 76*f0df9794SNeil Fuller includedZones.forEach((zoneName) => { 77*f0df9794SNeil Fuller newZoneCfg[zoneName] = zoneCfg[zoneName] 78*f0df9794SNeil Fuller }) 79*f0df9794SNeil Fuller zoneCfg = newZoneCfg 80*f0df9794SNeil Fuller } 81*f0df9794SNeil Fuller if (argv.excluded_zones) { 82*f0df9794SNeil Fuller const newZoneCfg = {} 83*f0df9794SNeil Fuller excludedZones = argv.excluded_zones 84*f0df9794SNeil Fuller Object.keys(zoneCfg).forEach((zoneName) => { 85*f0df9794SNeil Fuller if (!excludedZones.includes(zoneName)) { 86*f0df9794SNeil Fuller newZoneCfg[zoneName] = zoneCfg[zoneName] 87*f0df9794SNeil Fuller } 88*f0df9794SNeil Fuller }) 89*f0df9794SNeil Fuller zoneCfg = newZoneCfg 90*f0df9794SNeil Fuller } 91*f0df9794SNeil Fuller 92*f0df9794SNeil Fuller // filter out unneccessary downloads 93*f0df9794SNeil Fuller var newOsmBoundarySources = {} 94*f0df9794SNeil Fuller Object.keys(zoneCfg).forEach((zoneName) => { 95*f0df9794SNeil Fuller zoneCfg[zoneName].forEach((op) => { 96*f0df9794SNeil Fuller if (op.source === 'overpass') { 97*f0df9794SNeil Fuller newOsmBoundarySources[op.id] = osmBoundarySources[op.id] 98*f0df9794SNeil Fuller } 99*f0df9794SNeil Fuller }) 100*f0df9794SNeil Fuller }) 101*f0df9794SNeil Fuller 102*f0df9794SNeil Fuller osmBoundarySources = newOsmBoundarySources 103*f0df9794SNeil Fuller} 104*f0df9794SNeil Fuller 105*f0df9794SNeil Fullervar geoJsonReader = new jsts.io.GeoJSONReader() 106*f0df9794SNeil Fullervar geoJsonWriter = new jsts.io.GeoJSONWriter() 107*f0df9794SNeil Fullervar precisionModel = new jsts.geom.PrecisionModel(1000000) 108*f0df9794SNeil Fullervar precisionReducer = new jsts.precision.GeometryPrecisionReducer(precisionModel) 109*f0df9794SNeil Fullervar distZones = {} 110*f0df9794SNeil Fullervar lastReleaseJSONfile 111*f0df9794SNeil Fullervar minRequestGap = 4 112*f0df9794SNeil Fullervar curRequestGap = 4 113*f0df9794SNeil Fullerconst bufferDistance = 0.01 114*f0df9794SNeil Fuller 115*f0df9794SNeil Fullervar safeMkdir = function (dirname, callback) { 116*f0df9794SNeil Fuller fs.mkdir(dirname, function (err) { 117*f0df9794SNeil Fuller if (err && err.code === 'EEXIST') { 118*f0df9794SNeil Fuller callback() 119*f0df9794SNeil Fuller } else { 120*f0df9794SNeil Fuller callback(err) 121*f0df9794SNeil Fuller } 122*f0df9794SNeil Fuller }) 123*f0df9794SNeil Fuller} 124*f0df9794SNeil Fuller 125*f0df9794SNeil Fullervar debugGeo = function ( 126*f0df9794SNeil Fuller op, 127*f0df9794SNeil Fuller a, 128*f0df9794SNeil Fuller b, 129*f0df9794SNeil Fuller reducePrecision, 130*f0df9794SNeil Fuller bufferAfterPrecisionReduction 131*f0df9794SNeil Fuller) { 132*f0df9794SNeil Fuller var result 133*f0df9794SNeil Fuller 134*f0df9794SNeil Fuller if (reducePrecision) { 135*f0df9794SNeil Fuller a = precisionReducer.reduce(a) 136*f0df9794SNeil Fuller b = precisionReducer.reduce(b) 137*f0df9794SNeil Fuller } 138*f0df9794SNeil Fuller 139*f0df9794SNeil Fuller try { 140*f0df9794SNeil Fuller switch (op) { 141*f0df9794SNeil Fuller case 'union': 142*f0df9794SNeil Fuller result = a.union(b) 143*f0df9794SNeil Fuller break 144*f0df9794SNeil Fuller case 'intersection': 145*f0df9794SNeil Fuller result = a.intersection(b) 146*f0df9794SNeil Fuller break 147*f0df9794SNeil Fuller case 'intersects': 148*f0df9794SNeil Fuller result = a.intersects(b) 149*f0df9794SNeil Fuller break 150*f0df9794SNeil Fuller case 'diff': 151*f0df9794SNeil Fuller result = a.difference(b) 152*f0df9794SNeil Fuller break 153*f0df9794SNeil Fuller default: 154*f0df9794SNeil Fuller var err = new Error('invalid op: ' + op) 155*f0df9794SNeil Fuller throw err 156*f0df9794SNeil Fuller } 157*f0df9794SNeil Fuller } catch (e) { 158*f0df9794SNeil Fuller if (e.name === 'TopologyException') { 159*f0df9794SNeil Fuller if (reducePrecision) { 160*f0df9794SNeil Fuller if (bufferAfterPrecisionReduction) { 161*f0df9794SNeil Fuller console.log('Encountered TopologyException, retry with buffer increase') 162*f0df9794SNeil Fuller return debugGeo( 163*f0df9794SNeil Fuller op, 164*f0df9794SNeil Fuller a.buffer(bufferDistance), 165*f0df9794SNeil Fuller b.buffer(bufferDistance), 166*f0df9794SNeil Fuller true, 167*f0df9794SNeil Fuller bufferAfterPrecisionReduction 168*f0df9794SNeil Fuller ) 169*f0df9794SNeil Fuller } else { 170*f0df9794SNeil Fuller throw new Error('Encountered TopologyException after reducing precision') 171*f0df9794SNeil Fuller } 172*f0df9794SNeil Fuller } else { 173*f0df9794SNeil Fuller console.log('Encountered TopologyException, retry with GeometryPrecisionReducer') 174*f0df9794SNeil Fuller return debugGeo(op, a, b, true, bufferAfterPrecisionReduction) 175*f0df9794SNeil Fuller } 176*f0df9794SNeil Fuller } 177*f0df9794SNeil Fuller console.log('op err') 178*f0df9794SNeil Fuller console.log(e) 179*f0df9794SNeil Fuller console.log(e.stack) 180*f0df9794SNeil Fuller fs.writeFileSync('debug_' + op + '_a.json', JSON.stringify(geoJsonWriter.write(a))) 181*f0df9794SNeil Fuller fs.writeFileSync('debug_' + op + '_b.json', JSON.stringify(geoJsonWriter.write(b))) 182*f0df9794SNeil Fuller throw e 183*f0df9794SNeil Fuller } 184*f0df9794SNeil Fuller 185*f0df9794SNeil Fuller return result 186*f0df9794SNeil Fuller} 187*f0df9794SNeil Fuller 188*f0df9794SNeil Fullervar fetchIfNeeded = function (file, superCallback, downloadCallback, fetchFn) { 189*f0df9794SNeil Fuller // check for file that got downloaded 190*f0df9794SNeil Fuller fs.stat(file, function (err) { 191*f0df9794SNeil Fuller if (!err) { 192*f0df9794SNeil Fuller // file found, skip download steps 193*f0df9794SNeil Fuller return superCallback() 194*f0df9794SNeil Fuller } 195*f0df9794SNeil Fuller // check for manual file that got fixed and needs validation 196*f0df9794SNeil Fuller var fixedFile = file.replace('.json', '_fixed.json') 197*f0df9794SNeil Fuller fs.stat(fixedFile, function (err) { 198*f0df9794SNeil Fuller if (!err) { 199*f0df9794SNeil Fuller // file found, return fixed file 200*f0df9794SNeil Fuller return downloadCallback(null, require(fixedFile)) 201*f0df9794SNeil Fuller } 202*f0df9794SNeil Fuller // no manual fixed file found, download from overpass 203*f0df9794SNeil Fuller fetchFn() 204*f0df9794SNeil Fuller }) 205*f0df9794SNeil Fuller }) 206*f0df9794SNeil Fuller} 207*f0df9794SNeil Fuller 208*f0df9794SNeil Fullervar geoJsonToGeom = function (geoJson) { 209*f0df9794SNeil Fuller try { 210*f0df9794SNeil Fuller return geoJsonReader.read(JSON.stringify(geoJson)) 211*f0df9794SNeil Fuller } catch (e) { 212*f0df9794SNeil Fuller console.error('error converting geojson to geometry') 213*f0df9794SNeil Fuller fs.writeFileSync('debug_geojson_read_error.json', JSON.stringify(geoJson)) 214*f0df9794SNeil Fuller throw e 215*f0df9794SNeil Fuller } 216*f0df9794SNeil Fuller} 217*f0df9794SNeil Fuller 218*f0df9794SNeil Fullervar geomToGeoJson = function (geom) { 219*f0df9794SNeil Fuller return geoJsonWriter.write(geom) 220*f0df9794SNeil Fuller} 221*f0df9794SNeil Fuller 222*f0df9794SNeil Fullervar geomToGeoJsonString = function (geom) { 223*f0df9794SNeil Fuller return JSON.stringify(geoJsonWriter.write(geom)) 224*f0df9794SNeil Fuller} 225*f0df9794SNeil Fuller 226*f0df9794SNeil Fullerconst downloadProgress = new ProgressStats( 227*f0df9794SNeil Fuller 'Downloading', 228*f0df9794SNeil Fuller Object.keys(osmBoundarySources).length 229*f0df9794SNeil Fuller) 230*f0df9794SNeil Fuller 231*f0df9794SNeil Fullervar downloadOsmBoundary = function (boundaryId, boundaryCallback) { 232*f0df9794SNeil Fuller var cfg = osmBoundarySources[boundaryId] 233*f0df9794SNeil Fuller var query = '[out:json][timeout:60];(' 234*f0df9794SNeil Fuller if (cfg.way) { 235*f0df9794SNeil Fuller query += 'way' 236*f0df9794SNeil Fuller } else { 237*f0df9794SNeil Fuller query += 'relation' 238*f0df9794SNeil Fuller } 239*f0df9794SNeil Fuller var boundaryFilename = downloadsDir + '/' + boundaryId + '.json' 240*f0df9794SNeil Fuller var debug = 'getting data for ' + boundaryId 241*f0df9794SNeil Fuller var queryKeys = Object.keys(cfg) 242*f0df9794SNeil Fuller 243*f0df9794SNeil Fuller for (var i = queryKeys.length - 1; i >= 0; i--) { 244*f0df9794SNeil Fuller var k = queryKeys[i] 245*f0df9794SNeil Fuller if (k === 'way') continue 246*f0df9794SNeil Fuller var v = cfg[k] 247*f0df9794SNeil Fuller 248*f0df9794SNeil Fuller query += '["' + k + '"="' + v + '"]' 249*f0df9794SNeil Fuller } 250*f0df9794SNeil Fuller 251*f0df9794SNeil Fuller query += ';);out body;>;out meta qt;' 252*f0df9794SNeil Fuller 253*f0df9794SNeil Fuller downloadProgress.beginTask(debug, true) 254*f0df9794SNeil Fuller 255*f0df9794SNeil Fuller asynclib.auto({ 256*f0df9794SNeil Fuller downloadFromOverpass: function (cb) { 257*f0df9794SNeil Fuller console.log('downloading from overpass') 258*f0df9794SNeil Fuller fetchIfNeeded(boundaryFilename, boundaryCallback, cb, function () { 259*f0df9794SNeil Fuller var overpassResponseHandler = function (err, data) { 260*f0df9794SNeil Fuller if (err) { 261*f0df9794SNeil Fuller console.log(err) 262*f0df9794SNeil Fuller console.log('Increasing overpass request gap') 263*f0df9794SNeil Fuller curRequestGap *= 2 264*f0df9794SNeil Fuller makeQuery() 265*f0df9794SNeil Fuller } else { 266*f0df9794SNeil Fuller console.log('Success, decreasing overpass request gap') 267*f0df9794SNeil Fuller curRequestGap = Math.max(minRequestGap, curRequestGap / 2) 268*f0df9794SNeil Fuller cb(null, data) 269*f0df9794SNeil Fuller } 270*f0df9794SNeil Fuller } 271*f0df9794SNeil Fuller var makeQuery = function () { 272*f0df9794SNeil Fuller console.log('waiting ' + curRequestGap + ' seconds') 273*f0df9794SNeil Fuller setTimeout(function () { 274*f0df9794SNeil Fuller overpass(query, overpassResponseHandler, { flatProperties: true }) 275*f0df9794SNeil Fuller }, curRequestGap * 1000) 276*f0df9794SNeil Fuller } 277*f0df9794SNeil Fuller makeQuery() 278*f0df9794SNeil Fuller }) 279*f0df9794SNeil Fuller }, 280*f0df9794SNeil Fuller validateOverpassResult: ['downloadFromOverpass', function (results, cb) { 281*f0df9794SNeil Fuller var data = results.downloadFromOverpass 282*f0df9794SNeil Fuller if (!data.features) { 283*f0df9794SNeil Fuller var err = new Error('Invalid geojson for boundary: ' + boundaryId) 284*f0df9794SNeil Fuller return cb(err) 285*f0df9794SNeil Fuller } 286*f0df9794SNeil Fuller if (data.features.length === 0) { 287*f0df9794SNeil Fuller console.error('No data for the following query:') 288*f0df9794SNeil Fuller console.error(query) 289*f0df9794SNeil Fuller console.error('To read more about this error, please visit https://git.io/vxKQL') 290*f0df9794SNeil Fuller return cb(new Error('No data found for from overpass query')) 291*f0df9794SNeil Fuller } 292*f0df9794SNeil Fuller cb() 293*f0df9794SNeil Fuller }], 294*f0df9794SNeil Fuller saveSingleMultiPolygon: ['validateOverpassResult', function (results, cb) { 295*f0df9794SNeil Fuller var data = results.downloadFromOverpass 296*f0df9794SNeil Fuller var combined 297*f0df9794SNeil Fuller 298*f0df9794SNeil Fuller // union all multi-polygons / polygons into one 299*f0df9794SNeil Fuller for (var i = data.features.length - 1; i >= 0; i--) { 300*f0df9794SNeil Fuller var curOsmGeom = data.features[i].geometry 301*f0df9794SNeil Fuller const curOsmProps = data.features[i].properties 302*f0df9794SNeil Fuller if ( 303*f0df9794SNeil Fuller (curOsmGeom.type === 'Polygon' || curOsmGeom.type === 'MultiPolygon') && 304*f0df9794SNeil Fuller curOsmProps.type === 'boundary' // need to make sure enclaves aren't unioned 305*f0df9794SNeil Fuller ) { 306*f0df9794SNeil Fuller console.log('combining border') 307*f0df9794SNeil Fuller let errors = geojsonhint.hint(curOsmGeom) 308*f0df9794SNeil Fuller if (errors && errors.length > 0) { 309*f0df9794SNeil Fuller const stringifiedGeojson = JSON.stringify(curOsmGeom, null, 2) 310*f0df9794SNeil Fuller errors = geojsonhint.hint(stringifiedGeojson) 311*f0df9794SNeil Fuller console.error('Invalid geojson received in Overpass Result') 312*f0df9794SNeil Fuller console.error('Overpass query: ' + query) 313*f0df9794SNeil Fuller const problemFilename = boundaryId + '_convert_to_geom_error.json' 314*f0df9794SNeil Fuller fs.writeFileSync(problemFilename, stringifiedGeojson) 315*f0df9794SNeil Fuller console.error('saved problem file to ' + problemFilename) 316*f0df9794SNeil Fuller console.error('To read more about this error, please visit https://git.io/vxKQq') 317*f0df9794SNeil Fuller return cb(errors) 318*f0df9794SNeil Fuller } 319*f0df9794SNeil Fuller try { 320*f0df9794SNeil Fuller var curGeom = geoJsonToGeom(curOsmGeom) 321*f0df9794SNeil Fuller } catch (e) { 322*f0df9794SNeil Fuller console.error('error converting overpass result to geojson') 323*f0df9794SNeil Fuller console.error(e) 324*f0df9794SNeil Fuller 325*f0df9794SNeil Fuller fs.writeFileSync(boundaryId + '_convert_to_geom_error-all-features.json', JSON.stringify(data)) 326*f0df9794SNeil Fuller return cb(e) 327*f0df9794SNeil Fuller } 328*f0df9794SNeil Fuller if (!combined) { 329*f0df9794SNeil Fuller combined = curGeom 330*f0df9794SNeil Fuller } else { 331*f0df9794SNeil Fuller combined = debugGeo('union', curGeom, combined) 332*f0df9794SNeil Fuller } 333*f0df9794SNeil Fuller } 334*f0df9794SNeil Fuller } 335*f0df9794SNeil Fuller try { 336*f0df9794SNeil Fuller fs.writeFile(boundaryFilename, geomToGeoJsonString(combined), cb) 337*f0df9794SNeil Fuller } catch (e) { 338*f0df9794SNeil Fuller console.error('error writing combined border to geojson') 339*f0df9794SNeil Fuller fs.writeFileSync(boundaryId + '_combined_border_convert_to_geom_error.json', JSON.stringify(data)) 340*f0df9794SNeil Fuller return cb(e) 341*f0df9794SNeil Fuller } 342*f0df9794SNeil Fuller }] 343*f0df9794SNeil Fuller }, boundaryCallback) 344*f0df9794SNeil Fuller} 345*f0df9794SNeil Fuller 346*f0df9794SNeil Fullervar getTzDistFilename = function (tzid) { 347*f0df9794SNeil Fuller return distDir + '/' + tzid.replace(/\//g, '__') + '.json' 348*f0df9794SNeil Fuller} 349*f0df9794SNeil Fuller 350*f0df9794SNeil Fuller/** 351*f0df9794SNeil Fuller * Get the geometry of the requested source data 352*f0df9794SNeil Fuller * 353*f0df9794SNeil Fuller * @return {Object} geom The geometry of the source 354*f0df9794SNeil Fuller * @param {Object} source An object representing the data source 355*f0df9794SNeil Fuller * must have `source` key and then either: 356*f0df9794SNeil Fuller * - `id` if from a file 357*f0df9794SNeil Fuller * - `id` if from a file 358*f0df9794SNeil Fuller */ 359*f0df9794SNeil Fullervar getDataSource = function (source) { 360*f0df9794SNeil Fuller var geoJson 361*f0df9794SNeil Fuller if (source.source === 'overpass') { 362*f0df9794SNeil Fuller geoJson = require(downloadsDir + '/' + source.id + '.json') 363*f0df9794SNeil Fuller } else if (source.source === 'manual-polygon') { 364*f0df9794SNeil Fuller geoJson = polygon(source.data).geometry 365*f0df9794SNeil Fuller } else if (source.source === 'manual-multipolygon') { 366*f0df9794SNeil Fuller geoJson = multiPolygon(source.data).geometry 367*f0df9794SNeil Fuller } else if (source.source === 'dist') { 368*f0df9794SNeil Fuller geoJson = require(getTzDistFilename(source.id)) 369*f0df9794SNeil Fuller } else { 370*f0df9794SNeil Fuller var err = new Error('unknown source: ' + source.source) 371*f0df9794SNeil Fuller throw err 372*f0df9794SNeil Fuller } 373*f0df9794SNeil Fuller return geoJsonToGeom(geoJson) 374*f0df9794SNeil Fuller} 375*f0df9794SNeil Fuller 376*f0df9794SNeil Fuller/** 377*f0df9794SNeil Fuller * Post process created timezone boundary. 378*f0df9794SNeil Fuller * - remove small holes and exclaves 379*f0df9794SNeil Fuller * - reduce geometry precision 380*f0df9794SNeil Fuller * 381*f0df9794SNeil Fuller * @param {Geometry} geom The jsts geometry of the timezone 382*f0df9794SNeil Fuller * @param {boolean} returnAsObject if true, return as object, otherwise return stringified 383*f0df9794SNeil Fuller * @return {Object|String} geojson as object or stringified 384*f0df9794SNeil Fuller */ 385*f0df9794SNeil Fullervar postProcessZone = function (geom, returnAsObject) { 386*f0df9794SNeil Fuller // reduce precision of geometry 387*f0df9794SNeil Fuller const geojson = geomToGeoJson(precisionReducer.reduce(geom)) 388*f0df9794SNeil Fuller 389*f0df9794SNeil Fuller // iterate through all polygons 390*f0df9794SNeil Fuller const filteredPolygons = [] 391*f0df9794SNeil Fuller let allPolygons = geojson.coordinates 392*f0df9794SNeil Fuller if (geojson.type === 'Polygon') { 393*f0df9794SNeil Fuller allPolygons = [geojson.coordinates] 394*f0df9794SNeil Fuller } 395*f0df9794SNeil Fuller 396*f0df9794SNeil Fuller allPolygons.forEach((curPolygon, idx) => { 397*f0df9794SNeil Fuller // remove any polygon with very small area 398*f0df9794SNeil Fuller const polygonFeature = polygon(curPolygon) 399*f0df9794SNeil Fuller const polygonArea = area.geometry(polygonFeature.geometry) 400*f0df9794SNeil Fuller 401*f0df9794SNeil Fuller if (polygonArea < 1) return 402*f0df9794SNeil Fuller 403*f0df9794SNeil Fuller // find all holes 404*f0df9794SNeil Fuller const filteredLinearRings = [] 405*f0df9794SNeil Fuller 406*f0df9794SNeil Fuller curPolygon.forEach((curLinearRing, lrIdx) => { 407*f0df9794SNeil Fuller if (lrIdx === 0) { 408*f0df9794SNeil Fuller // always keep first linearRing 409*f0df9794SNeil Fuller filteredLinearRings.push(curLinearRing) 410*f0df9794SNeil Fuller } else { 411*f0df9794SNeil Fuller const polygonFromLinearRing = polygon([curLinearRing]) 412*f0df9794SNeil Fuller const linearRingArea = area.geometry(polygonFromLinearRing.geometry) 413*f0df9794SNeil Fuller 414*f0df9794SNeil Fuller // only include holes with relevant area 415*f0df9794SNeil Fuller if (linearRingArea > 1) { 416*f0df9794SNeil Fuller filteredLinearRings.push(curLinearRing) 417*f0df9794SNeil Fuller } 418*f0df9794SNeil Fuller } 419*f0df9794SNeil Fuller }) 420*f0df9794SNeil Fuller 421*f0df9794SNeil Fuller filteredPolygons.push(filteredLinearRings) 422*f0df9794SNeil Fuller }) 423*f0df9794SNeil Fuller 424*f0df9794SNeil Fuller // recompile to geojson string 425*f0df9794SNeil Fuller const newGeojson = { 426*f0df9794SNeil Fuller type: geojson.type 427*f0df9794SNeil Fuller } 428*f0df9794SNeil Fuller 429*f0df9794SNeil Fuller if (geojson.type === 'Polygon') { 430*f0df9794SNeil Fuller newGeojson.coordinates = filteredPolygons[0] 431*f0df9794SNeil Fuller } else { 432*f0df9794SNeil Fuller newGeojson.coordinates = filteredPolygons 433*f0df9794SNeil Fuller } 434*f0df9794SNeil Fuller 435*f0df9794SNeil Fuller return returnAsObject ? newGeojson : JSON.stringify(newGeojson) 436*f0df9794SNeil Fuller} 437*f0df9794SNeil Fuller 438*f0df9794SNeil Fullerconst buildingProgress = new ProgressStats( 439*f0df9794SNeil Fuller 'Building', 440*f0df9794SNeil Fuller Object.keys(zoneCfg).length 441*f0df9794SNeil Fuller) 442*f0df9794SNeil Fuller 443*f0df9794SNeil Fullervar makeTimezoneBoundary = function (tzid, callback) { 444*f0df9794SNeil Fuller buildingProgress.beginTask(`makeTimezoneBoundary for ${tzid}`, true) 445*f0df9794SNeil Fuller 446*f0df9794SNeil Fuller var ops = zoneCfg[tzid] 447*f0df9794SNeil Fuller var geom 448*f0df9794SNeil Fuller 449*f0df9794SNeil Fuller asynclib.eachSeries(ops, function (task, cb) { 450*f0df9794SNeil Fuller var taskData = getDataSource(task) 451*f0df9794SNeil Fuller console.log('-', task.op, task.id) 452*f0df9794SNeil Fuller if (task.op === 'init') { 453*f0df9794SNeil Fuller geom = taskData 454*f0df9794SNeil Fuller } else if (task.op === 'intersect') { 455*f0df9794SNeil Fuller geom = debugGeo('intersection', geom, taskData) 456*f0df9794SNeil Fuller } else if (task.op === 'difference') { 457*f0df9794SNeil Fuller geom = debugGeo('diff', geom, taskData) 458*f0df9794SNeil Fuller } else if (task.op === 'difference-reverse-order') { 459*f0df9794SNeil Fuller geom = debugGeo('diff', taskData, geom) 460*f0df9794SNeil Fuller } else if (task.op === 'union') { 461*f0df9794SNeil Fuller geom = debugGeo('union', geom, taskData) 462*f0df9794SNeil Fuller } else { 463*f0df9794SNeil Fuller var err = new Error('unknown op: ' + task.op) 464*f0df9794SNeil Fuller return cb(err) 465*f0df9794SNeil Fuller } 466*f0df9794SNeil Fuller cb() 467*f0df9794SNeil Fuller }, 468*f0df9794SNeil Fuller function (err) { 469*f0df9794SNeil Fuller if (err) { return callback(err) } 470*f0df9794SNeil Fuller fs.writeFile(getTzDistFilename(tzid), 471*f0df9794SNeil Fuller postProcessZone(geom), 472*f0df9794SNeil Fuller callback) 473*f0df9794SNeil Fuller }) 474*f0df9794SNeil Fuller} 475*f0df9794SNeil Fuller 476*f0df9794SNeil Fullervar loadDistZonesIntoMemory = function () { 477*f0df9794SNeil Fuller console.log('load zones into memory') 478*f0df9794SNeil Fuller var zones = Object.keys(zoneCfg) 479*f0df9794SNeil Fuller var tzid 480*f0df9794SNeil Fuller 481*f0df9794SNeil Fuller for (var i = 0; i < zones.length; i++) { 482*f0df9794SNeil Fuller tzid = zones[i] 483*f0df9794SNeil Fuller distZones[tzid] = getDataSource({ source: 'dist', id: tzid }) 484*f0df9794SNeil Fuller } 485*f0df9794SNeil Fuller} 486*f0df9794SNeil Fuller 487*f0df9794SNeil Fullervar getDistZoneGeom = function (tzid) { 488*f0df9794SNeil Fuller return distZones[tzid] 489*f0df9794SNeil Fuller} 490*f0df9794SNeil Fuller 491*f0df9794SNeil Fullervar roundDownToTenth = function (n) { 492*f0df9794SNeil Fuller return Math.floor(n * 10) / 10 493*f0df9794SNeil Fuller} 494*f0df9794SNeil Fuller 495*f0df9794SNeil Fullervar roundUpToTenth = function (n) { 496*f0df9794SNeil Fuller return Math.ceil(n * 10) / 10 497*f0df9794SNeil Fuller} 498*f0df9794SNeil Fuller 499*f0df9794SNeil Fullervar formatBounds = function (bounds) { 500*f0df9794SNeil Fuller let boundsStr = '[' 501*f0df9794SNeil Fuller boundsStr += roundDownToTenth(bounds[0]) + ', ' 502*f0df9794SNeil Fuller boundsStr += roundDownToTenth(bounds[1]) + ', ' 503*f0df9794SNeil Fuller boundsStr += roundUpToTenth(bounds[2]) + ', ' 504*f0df9794SNeil Fuller boundsStr += roundUpToTenth(bounds[3]) + ']' 505*f0df9794SNeil Fuller return boundsStr 506*f0df9794SNeil Fuller} 507*f0df9794SNeil Fuller 508*f0df9794SNeil Fullervar validateTimezoneBoundaries = function () { 509*f0df9794SNeil Fuller const numZones = Object.keys(zoneCfg).length 510*f0df9794SNeil Fuller const validationProgress = new ProgressStats( 511*f0df9794SNeil Fuller 'Validation', 512*f0df9794SNeil Fuller numZones * (numZones + 1) / 2 513*f0df9794SNeil Fuller ) 514*f0df9794SNeil Fuller 515*f0df9794SNeil Fuller console.log('do validation... this may take a few minutes') 516*f0df9794SNeil Fuller var allZonesOk = true 517*f0df9794SNeil Fuller var zones = Object.keys(zoneCfg) 518*f0df9794SNeil Fuller var lastPct = 0 519*f0df9794SNeil Fuller var compareTzid, tzid, zoneGeom 520*f0df9794SNeil Fuller 521*f0df9794SNeil Fuller for (var i = 0; i < zones.length; i++) { 522*f0df9794SNeil Fuller tzid = zones[i] 523*f0df9794SNeil Fuller zoneGeom = getDistZoneGeom(tzid) 524*f0df9794SNeil Fuller 525*f0df9794SNeil Fuller for (var j = i + 1; j < zones.length; j++) { 526*f0df9794SNeil Fuller const curPct = Math.floor(validationProgress.getPercentage()) 527*f0df9794SNeil Fuller if (curPct % 10 === 0 && curPct !== lastPct) { 528*f0df9794SNeil Fuller validationProgress.printStats('Validating zones', true) 529*f0df9794SNeil Fuller lastPct = curPct 530*f0df9794SNeil Fuller } 531*f0df9794SNeil Fuller compareTzid = zones[j] 532*f0df9794SNeil Fuller 533*f0df9794SNeil Fuller var compareZoneGeom = getDistZoneGeom(compareTzid) 534*f0df9794SNeil Fuller 535*f0df9794SNeil Fuller var intersects = false 536*f0df9794SNeil Fuller try { 537*f0df9794SNeil Fuller intersects = debugGeo('intersects', zoneGeom, compareZoneGeom) 538*f0df9794SNeil Fuller } catch (e) { 539*f0df9794SNeil Fuller console.warn('warning, encountered intersection error with zone ' + tzid + ' and ' + compareTzid) 540*f0df9794SNeil Fuller } 541*f0df9794SNeil Fuller if (intersects) { 542*f0df9794SNeil Fuller var intersectedGeom = debugGeo('intersection', zoneGeom, compareZoneGeom) 543*f0df9794SNeil Fuller var intersectedArea = intersectedGeom.getArea() 544*f0df9794SNeil Fuller 545*f0df9794SNeil Fuller if (intersectedArea > 0.0001) { 546*f0df9794SNeil Fuller // check if the intersected area(s) are one of the expected areas of overlap 547*f0df9794SNeil Fuller const allowedOverlapBounds = expectedZoneOverlaps[`${tzid}-${compareTzid}`] || expectedZoneOverlaps[`${compareTzid}-${tzid}`] 548*f0df9794SNeil Fuller const overlapsGeoJson = geoJsonWriter.write(intersectedGeom) 549*f0df9794SNeil Fuller 550*f0df9794SNeil Fuller // these zones are allowed to overlap in certain places, make sure the 551*f0df9794SNeil Fuller // found overlap(s) all fit within the expected areas of overlap 552*f0df9794SNeil Fuller if (allowedOverlapBounds) { 553*f0df9794SNeil Fuller // if the overlaps are a multipolygon, make sure each individual 554*f0df9794SNeil Fuller // polygon of overlap fits within at least one of the expected 555*f0df9794SNeil Fuller // overlaps 556*f0df9794SNeil Fuller let overlapsPolygons 557*f0df9794SNeil Fuller switch (overlapsGeoJson.type) { 558*f0df9794SNeil Fuller case 'MultiPolygon': 559*f0df9794SNeil Fuller overlapsPolygons = overlapsGeoJson.coordinates.map( 560*f0df9794SNeil Fuller polygonCoords => ({ 561*f0df9794SNeil Fuller coordinates: polygonCoords, 562*f0df9794SNeil Fuller type: 'Polygon' 563*f0df9794SNeil Fuller }) 564*f0df9794SNeil Fuller ) 565*f0df9794SNeil Fuller break 566*f0df9794SNeil Fuller case 'Polygon': 567*f0df9794SNeil Fuller overlapsPolygons = [overlapsGeoJson] 568*f0df9794SNeil Fuller break 569*f0df9794SNeil Fuller case 'GeometryCollection': 570*f0df9794SNeil Fuller overlapsPolygons = [] 571*f0df9794SNeil Fuller overlapsGeoJson.geometries.forEach(geom => { 572*f0df9794SNeil Fuller if (geom.type === 'Polygon') { 573*f0df9794SNeil Fuller overlapsPolygons.push(geom) 574*f0df9794SNeil Fuller } else if (geom.type === 'MultiPolygon') { 575*f0df9794SNeil Fuller geom.coordinates.forEach(polygonCoords => { 576*f0df9794SNeil Fuller overlapsPolygons.push({ 577*f0df9794SNeil Fuller coordinates: polygonCoords, 578*f0df9794SNeil Fuller type: 'Polygon' 579*f0df9794SNeil Fuller }) 580*f0df9794SNeil Fuller }) 581*f0df9794SNeil Fuller } 582*f0df9794SNeil Fuller }) 583*f0df9794SNeil Fuller break 584*f0df9794SNeil Fuller default: 585*f0df9794SNeil Fuller console.error('unexpected geojson overlap type') 586*f0df9794SNeil Fuller console.log(overlapsGeoJson) 587*f0df9794SNeil Fuller break 588*f0df9794SNeil Fuller } 589*f0df9794SNeil Fuller 590*f0df9794SNeil Fuller let allOverlapsOk = true 591*f0df9794SNeil Fuller overlapsPolygons.forEach((polygon, idx) => { 592*f0df9794SNeil Fuller const bounds = bbox(polygon) 593*f0df9794SNeil Fuller const polygonArea = area.geometry(polygon) 594*f0df9794SNeil Fuller if ( 595*f0df9794SNeil Fuller polygonArea > 10 && // ignore small polygons 596*f0df9794SNeil Fuller !allowedOverlapBounds.some(allowedBounds => 597*f0df9794SNeil Fuller allowedBounds.bounds[0] <= bounds[0] && // minX 598*f0df9794SNeil Fuller allowedBounds.bounds[1] <= bounds[1] && // minY 599*f0df9794SNeil Fuller allowedBounds.bounds[2] >= bounds[2] && // maxX 600*f0df9794SNeil Fuller allowedBounds.bounds[3] >= bounds[3] // maxY 601*f0df9794SNeil Fuller ) 602*f0df9794SNeil Fuller ) { 603*f0df9794SNeil Fuller console.error(`Unexpected intersection (${polygonArea} area) with bounds: ${formatBounds(bounds)}`) 604*f0df9794SNeil Fuller allOverlapsOk = false 605*f0df9794SNeil Fuller } 606*f0df9794SNeil Fuller }) 607*f0df9794SNeil Fuller 608*f0df9794SNeil Fuller if (allOverlapsOk) continue 609*f0df9794SNeil Fuller } 610*f0df9794SNeil Fuller 611*f0df9794SNeil Fuller // at least one unexpected overlap found, output an error and write debug file 612*f0df9794SNeil Fuller console.error('Validation error: ' + tzid + ' intersects ' + compareTzid + ' area: ' + intersectedArea) 613*f0df9794SNeil Fuller const debugFilename = tzid.replace(/\//g, '-') + '-' + compareTzid.replace(/\//g, '-') + '-overlap.json' 614*f0df9794SNeil Fuller fs.writeFileSync( 615*f0df9794SNeil Fuller debugFilename, 616*f0df9794SNeil Fuller JSON.stringify(overlapsGeoJson) 617*f0df9794SNeil Fuller ) 618*f0df9794SNeil Fuller console.error('wrote overlap area as file ' + debugFilename) 619*f0df9794SNeil Fuller console.error('To read more about this error, please visit https://git.io/vx6nx') 620*f0df9794SNeil Fuller allZonesOk = false 621*f0df9794SNeil Fuller } 622*f0df9794SNeil Fuller } 623*f0df9794SNeil Fuller validationProgress.logNext() 624*f0df9794SNeil Fuller } 625*f0df9794SNeil Fuller } 626*f0df9794SNeil Fuller 627*f0df9794SNeil Fuller return allZonesOk ? null : 'Zone validation unsuccessful' 628*f0df9794SNeil Fuller} 629*f0df9794SNeil Fuller 630*f0df9794SNeil Fullerlet oceanZoneBoundaries 631*f0df9794SNeil Fullerlet oceanZones = [ 632*f0df9794SNeil Fuller { tzid: 'Etc/GMT-12', left: 172.5, right: 180 }, 633*f0df9794SNeil Fuller { tzid: 'Etc/GMT-11', left: 157.5, right: 172.5 }, 634*f0df9794SNeil Fuller { tzid: 'Etc/GMT-10', left: 142.5, right: 157.5 }, 635*f0df9794SNeil Fuller { tzid: 'Etc/GMT-9', left: 127.5, right: 142.5 }, 636*f0df9794SNeil Fuller { tzid: 'Etc/GMT-8', left: 112.5, right: 127.5 }, 637*f0df9794SNeil Fuller { tzid: 'Etc/GMT-7', left: 97.5, right: 112.5 }, 638*f0df9794SNeil Fuller { tzid: 'Etc/GMT-6', left: 82.5, right: 97.5 }, 639*f0df9794SNeil Fuller { tzid: 'Etc/GMT-5', left: 67.5, right: 82.5 }, 640*f0df9794SNeil Fuller { tzid: 'Etc/GMT-4', left: 52.5, right: 67.5 }, 641*f0df9794SNeil Fuller { tzid: 'Etc/GMT-3', left: 37.5, right: 52.5 }, 642*f0df9794SNeil Fuller { tzid: 'Etc/GMT-2', left: 22.5, right: 37.5 }, 643*f0df9794SNeil Fuller { tzid: 'Etc/GMT-1', left: 7.5, right: 22.5 }, 644*f0df9794SNeil Fuller { tzid: 'Etc/GMT', left: -7.5, right: 7.5 }, 645*f0df9794SNeil Fuller { tzid: 'Etc/GMT+1', left: -22.5, right: -7.5 }, 646*f0df9794SNeil Fuller { tzid: 'Etc/GMT+2', left: -37.5, right: -22.5 }, 647*f0df9794SNeil Fuller { tzid: 'Etc/GMT+3', left: -52.5, right: -37.5 }, 648*f0df9794SNeil Fuller { tzid: 'Etc/GMT+4', left: -67.5, right: -52.5 }, 649*f0df9794SNeil Fuller { tzid: 'Etc/GMT+5', left: -82.5, right: -67.5 }, 650*f0df9794SNeil Fuller { tzid: 'Etc/GMT+6', left: -97.5, right: -82.5 }, 651*f0df9794SNeil Fuller { tzid: 'Etc/GMT+7', left: -112.5, right: -97.5 }, 652*f0df9794SNeil Fuller { tzid: 'Etc/GMT+8', left: -127.5, right: -112.5 }, 653*f0df9794SNeil Fuller { tzid: 'Etc/GMT+9', left: -142.5, right: -127.5 }, 654*f0df9794SNeil Fuller { tzid: 'Etc/GMT+10', left: -157.5, right: -142.5 }, 655*f0df9794SNeil Fuller { tzid: 'Etc/GMT+11', left: -172.5, right: -157.5 }, 656*f0df9794SNeil Fuller { tzid: 'Etc/GMT+12', left: -180, right: -172.5 } 657*f0df9794SNeil Fuller] 658*f0df9794SNeil Fuller 659*f0df9794SNeil Fullerif (includedZones.length > 0) { 660*f0df9794SNeil Fuller oceanZones = oceanZones.filter(oceanZone => includedZones.indexOf(oceanZone) > -1) 661*f0df9794SNeil Fuller} 662*f0df9794SNeil Fullerif (excludedZones.length > 0) { 663*f0df9794SNeil Fuller oceanZones = oceanZones.filter(oceanZone => excludedZones.indexOf(oceanZone) === -1) 664*f0df9794SNeil Fuller} 665*f0df9794SNeil Fuller 666*f0df9794SNeil Fullervar addOceans = function (callback) { 667*f0df9794SNeil Fuller console.log('adding ocean boundaries') 668*f0df9794SNeil Fuller const zones = Object.keys(zoneCfg) 669*f0df9794SNeil Fuller 670*f0df9794SNeil Fuller const oceanProgress = new ProgressStats( 671*f0df9794SNeil Fuller 'Oceans', 672*f0df9794SNeil Fuller oceanZones.length 673*f0df9794SNeil Fuller ) 674*f0df9794SNeil Fuller 675*f0df9794SNeil Fuller oceanZoneBoundaries = oceanZones.map(zone => { 676*f0df9794SNeil Fuller oceanProgress.beginTask(zone.tzid, true) 677*f0df9794SNeil Fuller const geoJson = polygon([[ 678*f0df9794SNeil Fuller [zone.left, 90], 679*f0df9794SNeil Fuller [zone.left, -90], 680*f0df9794SNeil Fuller [zone.right, -90], 681*f0df9794SNeil Fuller [zone.right, 90], 682*f0df9794SNeil Fuller [zone.left, 90] 683*f0df9794SNeil Fuller ]]).geometry 684*f0df9794SNeil Fuller 685*f0df9794SNeil Fuller let geom = geoJsonToGeom(geoJson) 686*f0df9794SNeil Fuller 687*f0df9794SNeil Fuller // diff against every zone 688*f0df9794SNeil Fuller zones.forEach(distZone => { 689*f0df9794SNeil Fuller geom = debugGeo('diff', geom, getDistZoneGeom(distZone)) 690*f0df9794SNeil Fuller }) 691*f0df9794SNeil Fuller 692*f0df9794SNeil Fuller return { 693*f0df9794SNeil Fuller geom: postProcessZone(geom, true), 694*f0df9794SNeil Fuller tzid: zone.tzid 695*f0df9794SNeil Fuller } 696*f0df9794SNeil Fuller }) 697*f0df9794SNeil Fuller 698*f0df9794SNeil Fuller callback() 699*f0df9794SNeil Fuller} 700*f0df9794SNeil Fuller 701*f0df9794SNeil Fullervar combineAndWriteZones = function (callback) { 702*f0df9794SNeil Fuller const regularWriter = new FeatureWriterStream(distDir + '/combined.json') 703*f0df9794SNeil Fuller const oceanWriter = new FeatureWriterStream(distDir + '/combined-with-oceans.json') 704*f0df9794SNeil Fuller var zones = Object.keys(zoneCfg) 705*f0df9794SNeil Fuller 706*f0df9794SNeil Fuller zones.forEach(zoneName => { 707*f0df9794SNeil Fuller const feature = { 708*f0df9794SNeil Fuller type: 'Feature', 709*f0df9794SNeil Fuller properties: { tzid: zoneName }, 710*f0df9794SNeil Fuller geometry: geomToGeoJson(getDistZoneGeom(zoneName)) 711*f0df9794SNeil Fuller } 712*f0df9794SNeil Fuller const stringified = JSON.stringify(feature) 713*f0df9794SNeil Fuller regularWriter.add(stringified) 714*f0df9794SNeil Fuller oceanWriter.add(stringified) 715*f0df9794SNeil Fuller }) 716*f0df9794SNeil Fuller oceanZoneBoundaries.forEach(boundary => { 717*f0df9794SNeil Fuller var feature = { 718*f0df9794SNeil Fuller type: 'Feature', 719*f0df9794SNeil Fuller properties: { tzid: boundary.tzid }, 720*f0df9794SNeil Fuller geometry: boundary.geom 721*f0df9794SNeil Fuller } 722*f0df9794SNeil Fuller oceanWriter.add(JSON.stringify(feature)) 723*f0df9794SNeil Fuller }) 724*f0df9794SNeil Fuller asynclib.parallel([ 725*f0df9794SNeil Fuller cb => regularWriter.end(cb), 726*f0df9794SNeil Fuller cb => oceanWriter.end(cb) 727*f0df9794SNeil Fuller ], callback) 728*f0df9794SNeil Fuller} 729*f0df9794SNeil Fuller 730*f0df9794SNeil Fullervar downloadLastRelease = function (cb) { 731*f0df9794SNeil Fuller // download latest release info 732*f0df9794SNeil Fuller https.get( 733*f0df9794SNeil Fuller { 734*f0df9794SNeil Fuller headers: { 'user-agent': 'timezone-boundary-builder' }, 735*f0df9794SNeil Fuller host: 'api.github.com', 736*f0df9794SNeil Fuller path: '/repos/evansiroky/timezone-boundary-builder/releases/latest' 737*f0df9794SNeil Fuller }, 738*f0df9794SNeil Fuller function (res) { 739*f0df9794SNeil Fuller var data = '' 740*f0df9794SNeil Fuller res.on('data', function (chunk) { 741*f0df9794SNeil Fuller data += chunk 742*f0df9794SNeil Fuller }) 743*f0df9794SNeil Fuller res.on('end', function () { 744*f0df9794SNeil Fuller data = JSON.parse(data) 745*f0df9794SNeil Fuller // determine last release version name and download link 746*f0df9794SNeil Fuller const lastReleaseName = data.name 747*f0df9794SNeil Fuller lastReleaseJSONfile = `./dist/${lastReleaseName}.json` 748*f0df9794SNeil Fuller let lastReleaseDownloadUrl 749*f0df9794SNeil Fuller for (var i = 0; i < data.assets.length; i++) { 750*f0df9794SNeil Fuller if (data.assets[i].browser_download_url.indexOf('timezones-with-oceans.geojson') > -1) { 751*f0df9794SNeil Fuller lastReleaseDownloadUrl = data.assets[i].browser_download_url 752*f0df9794SNeil Fuller } 753*f0df9794SNeil Fuller } 754*f0df9794SNeil Fuller if (!lastReleaseDownloadUrl) { 755*f0df9794SNeil Fuller return cb(new Error('geojson not found')) 756*f0df9794SNeil Fuller } 757*f0df9794SNeil Fuller 758*f0df9794SNeil Fuller // check for file that got downloaded 759*f0df9794SNeil Fuller fs.stat(lastReleaseJSONfile, function (err) { 760*f0df9794SNeil Fuller if (!err) { 761*f0df9794SNeil Fuller // file found, skip download steps 762*f0df9794SNeil Fuller return cb() 763*f0df9794SNeil Fuller } 764*f0df9794SNeil Fuller // file not found, download 765*f0df9794SNeil Fuller console.log(`Downloading latest release to ${lastReleaseJSONfile}.zip`) 766*f0df9794SNeil Fuller https.get({ 767*f0df9794SNeil Fuller headers: { 'user-agent': 'timezone-boundary-builder' }, 768*f0df9794SNeil Fuller host: 'github.com', 769*f0df9794SNeil Fuller path: lastReleaseDownloadUrl.replace('https://github.com', '') 770*f0df9794SNeil Fuller }, function (response) { 771*f0df9794SNeil Fuller var file = fs.createWriteStream(`${lastReleaseJSONfile}.zip`) 772*f0df9794SNeil Fuller response.pipe(file) 773*f0df9794SNeil Fuller file.on('finish', function () { 774*f0df9794SNeil Fuller file.close((err) => { 775*f0df9794SNeil Fuller if (err) return cb(err) 776*f0df9794SNeil Fuller // unzip file 777*f0df9794SNeil Fuller console.log('unzipping latest release') 778*f0df9794SNeil Fuller exec( 779*f0df9794SNeil Fuller `unzip -o ${lastReleaseJSONfile} -d dist`, 780*f0df9794SNeil Fuller err => { 781*f0df9794SNeil Fuller if (err) { return cb(err) } 782*f0df9794SNeil Fuller console.log('unzipped file') 783*f0df9794SNeil Fuller console.log('moving unzipped file') 784*f0df9794SNeil Fuller // might need to change this after changes to how files are 785*f0df9794SNeil Fuller // zipped after 2020a 786*f0df9794SNeil Fuller fs.copyFile( 787*f0df9794SNeil Fuller path.join( 788*f0df9794SNeil Fuller 'dist', 789*f0df9794SNeil Fuller 'dist', 790*f0df9794SNeil Fuller 'combined-with-oceans.json' 791*f0df9794SNeil Fuller ), 792*f0df9794SNeil Fuller lastReleaseJSONfile, 793*f0df9794SNeil Fuller cb 794*f0df9794SNeil Fuller ) 795*f0df9794SNeil Fuller } 796*f0df9794SNeil Fuller ) 797*f0df9794SNeil Fuller }) 798*f0df9794SNeil Fuller }) 799*f0df9794SNeil Fuller }).on('error', cb) 800*f0df9794SNeil Fuller }) 801*f0df9794SNeil Fuller }) 802*f0df9794SNeil Fuller } 803*f0df9794SNeil Fuller ) 804*f0df9794SNeil Fuller} 805*f0df9794SNeil Fuller 806*f0df9794SNeil Fullervar analyzeChangesFromLastRelease = function (cb) { 807*f0df9794SNeil Fuller // load last release data into memory 808*f0df9794SNeil Fuller console.log('loading previous release into memory') 809*f0df9794SNeil Fuller const lastReleaseData = require(lastReleaseJSONfile) 810*f0df9794SNeil Fuller 811*f0df9794SNeil Fuller // load each feature's geojson into JSTS format and then organized by tzid 812*f0df9794SNeil Fuller const lastReleaseZones = {} 813*f0df9794SNeil Fuller lastReleaseData.features.forEach( 814*f0df9794SNeil Fuller feature => { 815*f0df9794SNeil Fuller lastReleaseZones[feature.properties.tzid] = feature 816*f0df9794SNeil Fuller } 817*f0df9794SNeil Fuller ) 818*f0df9794SNeil Fuller 819*f0df9794SNeil Fuller // generate set of keys from last release and current 820*f0df9794SNeil Fuller const zoneNames = new Set() 821*f0df9794SNeil Fuller Object.keys(distZones).forEach(zoneName => zoneNames.add(zoneName)) 822*f0df9794SNeil Fuller Object.keys(lastReleaseZones).forEach(zoneName => zoneNames.add(zoneName)) 823*f0df9794SNeil Fuller 824*f0df9794SNeil Fuller // create diff for each zone 825*f0df9794SNeil Fuller const analysisProgress = new ProgressStats( 826*f0df9794SNeil Fuller 'Analyzing diffs', 827*f0df9794SNeil Fuller zoneNames.size 828*f0df9794SNeil Fuller ) 829*f0df9794SNeil Fuller const additionsWriter = new FeatureWriterStream(distDir + '/additions.json') 830*f0df9794SNeil Fuller const removalsWriter = new FeatureWriterStream(distDir + '/removals.json') 831*f0df9794SNeil Fuller zoneNames.forEach(zoneName => { 832*f0df9794SNeil Fuller analysisProgress.beginTask(zoneName, true) 833*f0df9794SNeil Fuller if (distZones[zoneName] && lastReleaseZones[zoneName]) { 834*f0df9794SNeil Fuller // some zones take forever to diff unless they are buffered, so buffer by 835*f0df9794SNeil Fuller // just a small amount 836*f0df9794SNeil Fuller const lastReleaseGeom = geoJsonToGeom( 837*f0df9794SNeil Fuller lastReleaseZones[zoneName].geometry 838*f0df9794SNeil Fuller ).buffer(bufferDistance) 839*f0df9794SNeil Fuller const curDataGeom = getDistZoneGeom(zoneName).buffer(bufferDistance) 840*f0df9794SNeil Fuller 841*f0df9794SNeil Fuller // don't diff equal geometries 842*f0df9794SNeil Fuller if (curDataGeom.equals(lastReleaseGeom)) return 843*f0df9794SNeil Fuller 844*f0df9794SNeil Fuller // diff current - last = additions 845*f0df9794SNeil Fuller const addition = debugGeo( 846*f0df9794SNeil Fuller 'diff', 847*f0df9794SNeil Fuller curDataGeom, 848*f0df9794SNeil Fuller lastReleaseGeom, 849*f0df9794SNeil Fuller false, 850*f0df9794SNeil Fuller true 851*f0df9794SNeil Fuller ) 852*f0df9794SNeil Fuller if (addition.getArea() > 0.0001) { 853*f0df9794SNeil Fuller additionsWriter.add(JSON.stringify({ 854*f0df9794SNeil Fuller type: 'Feature', 855*f0df9794SNeil Fuller properties: { tzid: zoneName }, 856*f0df9794SNeil Fuller geometry: geomToGeoJson(addition) 857*f0df9794SNeil Fuller })) 858*f0df9794SNeil Fuller } 859*f0df9794SNeil Fuller 860*f0df9794SNeil Fuller // diff last - current = removals 861*f0df9794SNeil Fuller const removal = debugGeo( 862*f0df9794SNeil Fuller 'diff', 863*f0df9794SNeil Fuller lastReleaseGeom, 864*f0df9794SNeil Fuller curDataGeom, 865*f0df9794SNeil Fuller false, 866*f0df9794SNeil Fuller true 867*f0df9794SNeil Fuller ) 868*f0df9794SNeil Fuller if (removal.getArea() > 0.0001) { 869*f0df9794SNeil Fuller removalsWriter.add(JSON.stringify({ 870*f0df9794SNeil Fuller type: 'Feature', 871*f0df9794SNeil Fuller properties: { tzid: zoneName }, 872*f0df9794SNeil Fuller geometry: geomToGeoJson(removal) 873*f0df9794SNeil Fuller })) 874*f0df9794SNeil Fuller } 875*f0df9794SNeil Fuller } else if (distZones[zoneName]) { 876*f0df9794SNeil Fuller additionsWriter.add(JSON.stringify({ 877*f0df9794SNeil Fuller type: 'Feature', 878*f0df9794SNeil Fuller properties: { tzid: zoneName }, 879*f0df9794SNeil Fuller geometry: geomToGeoJson(getDistZoneGeom(zoneName)) 880*f0df9794SNeil Fuller })) 881*f0df9794SNeil Fuller } else { 882*f0df9794SNeil Fuller removalsWriter.add(JSON.stringify(lastReleaseZones[zoneName])) 883*f0df9794SNeil Fuller } 884*f0df9794SNeil Fuller }) 885*f0df9794SNeil Fuller 886*f0df9794SNeil Fuller // write files 887*f0df9794SNeil Fuller asynclib.parallel([ 888*f0df9794SNeil Fuller wcb => additionsWriter.end(wcb), 889*f0df9794SNeil Fuller wcb => removalsWriter.end(wcb) 890*f0df9794SNeil Fuller ], cb) 891*f0df9794SNeil Fuller} 892*f0df9794SNeil Fuller 893*f0df9794SNeil Fullerconst autoScript = { 894*f0df9794SNeil Fuller makeDownloadsDir: function (cb) { 895*f0df9794SNeil Fuller overallProgress.beginTask('Creating downloads dir') 896*f0df9794SNeil Fuller safeMkdir(downloadsDir, cb) 897*f0df9794SNeil Fuller }, 898*f0df9794SNeil Fuller makeDistDir: function (cb) { 899*f0df9794SNeil Fuller overallProgress.beginTask('Creating dist dir') 900*f0df9794SNeil Fuller safeMkdir(distDir, cb) 901*f0df9794SNeil Fuller }, 902*f0df9794SNeil Fuller getOsmBoundaries: ['makeDownloadsDir', function (results, cb) { 903*f0df9794SNeil Fuller overallProgress.beginTask('Downloading osm boundaries') 904*f0df9794SNeil Fuller asynclib.eachSeries(Object.keys(osmBoundarySources), downloadOsmBoundary, cb) 905*f0df9794SNeil Fuller }], 906*f0df9794SNeil Fuller cleanDownloadFolder: ['makeDistDir', 'getOsmBoundaries', function (results, cb) { 907*f0df9794SNeil Fuller overallProgress.beginTask('cleanDownloadFolder') 908*f0df9794SNeil Fuller const downloadedFilenames = Object.keys(osmBoundarySources).map(name => `${name}.json`) 909*f0df9794SNeil Fuller fs.readdir(downloadsDir, (err, files) => { 910*f0df9794SNeil Fuller if (err) return cb(err) 911*f0df9794SNeil Fuller asynclib.each( 912*f0df9794SNeil Fuller files, 913*f0df9794SNeil Fuller (file, fileCb) => { 914*f0df9794SNeil Fuller if (downloadedFilenames.indexOf(file) === -1) { 915*f0df9794SNeil Fuller return fs.unlink(path.join(downloadsDir, file), fileCb) 916*f0df9794SNeil Fuller } 917*f0df9794SNeil Fuller fileCb() 918*f0df9794SNeil Fuller }, 919*f0df9794SNeil Fuller cb 920*f0df9794SNeil Fuller ) 921*f0df9794SNeil Fuller }) 922*f0df9794SNeil Fuller }], 923*f0df9794SNeil Fuller zipInputData: ['cleanDownloadFolder', function (results, cb) { 924*f0df9794SNeil Fuller overallProgress.beginTask('Zipping up input data') 925*f0df9794SNeil Fuller exec('zip -j ' + distDir + '/input-data.zip ' + downloadsDir + 926*f0df9794SNeil Fuller '/* timezones.json osmBoundarySources.json expectedZoneOverlaps.json', cb) 927*f0df9794SNeil Fuller }], 928*f0df9794SNeil Fuller downloadLastRelease: ['makeDistDir', function (results, cb) { 929*f0df9794SNeil Fuller if (argv.skip_analyze_diffs) { 930*f0df9794SNeil Fuller overallProgress.beginTask('WARNING: Skipping download of last release for analysis!') 931*f0df9794SNeil Fuller cb() 932*f0df9794SNeil Fuller } else { 933*f0df9794SNeil Fuller overallProgress.beginTask('Downloading last release for analysis') 934*f0df9794SNeil Fuller downloadLastRelease(cb) 935*f0df9794SNeil Fuller } 936*f0df9794SNeil Fuller }], 937*f0df9794SNeil Fuller createZones: ['makeDistDir', 'getOsmBoundaries', function (results, cb) { 938*f0df9794SNeil Fuller overallProgress.beginTask('Creating timezone boundaries') 939*f0df9794SNeil Fuller asynclib.each(Object.keys(zoneCfg), makeTimezoneBoundary, cb) 940*f0df9794SNeil Fuller }], 941*f0df9794SNeil Fuller validateZones: ['createZones', function (results, cb) { 942*f0df9794SNeil Fuller overallProgress.beginTask('Validating timezone boundaries') 943*f0df9794SNeil Fuller loadDistZonesIntoMemory() 944*f0df9794SNeil Fuller if (argv.skip_validation) { 945*f0df9794SNeil Fuller console.warn('WARNING: Skipping validation!') 946*f0df9794SNeil Fuller cb() 947*f0df9794SNeil Fuller } else { 948*f0df9794SNeil Fuller cb(validateTimezoneBoundaries()) 949*f0df9794SNeil Fuller } 950*f0df9794SNeil Fuller }], 951*f0df9794SNeil Fuller addOceans: ['validateZones', function (results, cb) { 952*f0df9794SNeil Fuller overallProgress.beginTask('Adding oceans') 953*f0df9794SNeil Fuller addOceans(cb) 954*f0df9794SNeil Fuller }], 955*f0df9794SNeil Fuller mergeZones: ['addOceans', function (results, cb) { 956*f0df9794SNeil Fuller overallProgress.beginTask('Merging zones') 957*f0df9794SNeil Fuller combineAndWriteZones(cb) 958*f0df9794SNeil Fuller }], 959*f0df9794SNeil Fuller zipGeoJson: ['mergeZones', function (results, cb) { 960*f0df9794SNeil Fuller if (argv.skip_zip) { 961*f0df9794SNeil Fuller overallProgress.beginTask('Skipping zip') 962*f0df9794SNeil Fuller return cb() 963*f0df9794SNeil Fuller } 964*f0df9794SNeil Fuller overallProgress.beginTask('Zipping geojson') 965*f0df9794SNeil Fuller const zipFile = distDir + '/timezones.geojson.zip' 966*f0df9794SNeil Fuller const jsonFile = distDir + '/combined.json' 967*f0df9794SNeil Fuller exec('zip -j ' + zipFile + ' ' + jsonFile, cb) 968*f0df9794SNeil Fuller }], 969*f0df9794SNeil Fuller zipGeoJsonWithOceans: ['mergeZones', function (results, cb) { 970*f0df9794SNeil Fuller if (argv.skip_zip) { 971*f0df9794SNeil Fuller overallProgress.beginTask('Skipping with oceans zip') 972*f0df9794SNeil Fuller return cb() 973*f0df9794SNeil Fuller } 974*f0df9794SNeil Fuller overallProgress.beginTask('Zipping geojson with oceans') 975*f0df9794SNeil Fuller const zipFile = distDir + '/timezones-with-oceans.geojson.zip' 976*f0df9794SNeil Fuller const jsonFile = distDir + '/combined-with-oceans.json' 977*f0df9794SNeil Fuller exec('zip -j ' + zipFile + ' ' + jsonFile, cb) 978*f0df9794SNeil Fuller }], 979*f0df9794SNeil Fuller makeShapefile: ['mergeZones', function (results, cb) { 980*f0df9794SNeil Fuller if (argv.skip_shapefile) { 981*f0df9794SNeil Fuller overallProgress.beginTask('Skipping shapefile creation') 982*f0df9794SNeil Fuller return cb() 983*f0df9794SNeil Fuller } 984*f0df9794SNeil Fuller overallProgress.beginTask('Converting from geojson to shapefile') 985*f0df9794SNeil Fuller const shapeFileGlob = distDir + '/combined-shapefile.*' 986*f0df9794SNeil Fuller rimraf.sync(shapeFileGlob) 987*f0df9794SNeil Fuller const shapeFile = distDir + '/combined-shapefile.shp' 988*f0df9794SNeil Fuller const jsonFile = distDir + '/combined.json' 989*f0df9794SNeil Fuller exec( 990*f0df9794SNeil Fuller 'ogr2ogr -f "ESRI Shapefile" ' + shapeFile + ' ' + jsonFile, 991*f0df9794SNeil Fuller function (err, stdout, stderr) { 992*f0df9794SNeil Fuller if (err) { return cb(err) } 993*f0df9794SNeil Fuller const shapeFileZip = distDir + '/timezones.shapefile.zip' 994*f0df9794SNeil Fuller exec('zip -j ' + shapeFileZip + ' ' + shapeFileGlob, cb) 995*f0df9794SNeil Fuller } 996*f0df9794SNeil Fuller ) 997*f0df9794SNeil Fuller }], 998*f0df9794SNeil Fuller makeShapefileWithOceans: ['mergeZones', function (results, cb) { 999*f0df9794SNeil Fuller if (argv.skip_shapefile) { 1000*f0df9794SNeil Fuller overallProgress.beginTask('Skipping with oceans shapefile creation') 1001*f0df9794SNeil Fuller return cb() 1002*f0df9794SNeil Fuller } 1003*f0df9794SNeil Fuller overallProgress.beginTask('Converting from geojson with oceans to shapefile') 1004*f0df9794SNeil Fuller const shapeFileGlob = distDir + '/combined-shapefile-with-oceans.*' 1005*f0df9794SNeil Fuller rimraf.sync(shapeFileGlob) 1006*f0df9794SNeil Fuller const shapeFile = distDir + '/combined-shapefile-with-oceans.shp' 1007*f0df9794SNeil Fuller const jsonFile = distDir + '/combined-with-oceans.json' 1008*f0df9794SNeil Fuller exec( 1009*f0df9794SNeil Fuller 'ogr2ogr -f "ESRI Shapefile" ' + shapeFile + ' ' + jsonFile, 1010*f0df9794SNeil Fuller function (err, stdout, stderr) { 1011*f0df9794SNeil Fuller if (err) { return cb(err) } 1012*f0df9794SNeil Fuller const shapeFileZip = distDir + '/timezones-with-oceans.shapefile.zip' 1013*f0df9794SNeil Fuller exec('zip -j ' + shapeFileZip + ' ' + shapeFileGlob, cb) 1014*f0df9794SNeil Fuller } 1015*f0df9794SNeil Fuller ) 1016*f0df9794SNeil Fuller }], 1017*f0df9794SNeil Fuller makeListOfTimeZoneNames: function (cb) { 1018*f0df9794SNeil Fuller overallProgress.beginTask('Writing timezone names to file') 1019*f0df9794SNeil Fuller let zoneNames = Object.keys(zoneCfg) 1020*f0df9794SNeil Fuller oceanZones.forEach(oceanZone => { 1021*f0df9794SNeil Fuller zoneNames.push(oceanZone.tzid) 1022*f0df9794SNeil Fuller }) 1023*f0df9794SNeil Fuller if (includedZones.length > 0) { 1024*f0df9794SNeil Fuller zoneNames = zoneNames.filter(zoneName => includedZones.indexOf(zoneName) > -1) 1025*f0df9794SNeil Fuller } 1026*f0df9794SNeil Fuller if (excludedZones.length > 0) { 1027*f0df9794SNeil Fuller zoneNames = zoneNames.filter(zoneName => excludedZones.indexOf(zoneName) === -1) 1028*f0df9794SNeil Fuller } 1029*f0df9794SNeil Fuller fs.writeFile( 1030*f0df9794SNeil Fuller distDir + '/timezone-names.json', 1031*f0df9794SNeil Fuller JSON.stringify(zoneNames), 1032*f0df9794SNeil Fuller cb 1033*f0df9794SNeil Fuller ) 1034*f0df9794SNeil Fuller }, 1035*f0df9794SNeil Fuller analyzeChangesFromLastRelease: ['downloadLastRelease', 'mergeZones', function (results, cb) { 1036*f0df9794SNeil Fuller if (argv.skip_analyze_diffs) { 1037*f0df9794SNeil Fuller overallProgress.beginTask('WARNING: Skipping analysis of changes from last release!') 1038*f0df9794SNeil Fuller cb() 1039*f0df9794SNeil Fuller } else { 1040*f0df9794SNeil Fuller overallProgress.beginTask('Analyzing changes from last release') 1041*f0df9794SNeil Fuller analyzeChangesFromLastRelease(cb) 1042*f0df9794SNeil Fuller } 1043*f0df9794SNeil Fuller }] 1044*f0df9794SNeil Fuller} 1045*f0df9794SNeil Fuller 1046*f0df9794SNeil Fullerconst overallProgress = new ProgressStats('Overall', Object.keys(autoScript).length) 1047*f0df9794SNeil Fuller 1048*f0df9794SNeil Fullerasynclib.auto(autoScript, function (err, results) { 1049*f0df9794SNeil Fuller console.log('done') 1050*f0df9794SNeil Fuller if (err) { 1051*f0df9794SNeil Fuller console.log('error!', err) 1052*f0df9794SNeil Fuller } 1053*f0df9794SNeil Fuller}) 1054