blob: 0caf598d5a457d5d41ce6e953c41df5c4504f187 [file] [log] [blame]
Evan Siroky7891a6e2016-11-05 11:50:50 -07001var exec = require('child_process').exec
2var fs = require('fs')
evansirokyf3bcf052020-10-17 23:10:15 -07003var path = require('path')
evansirokyd401c892016-06-16 00:05:14 -07004
Evan Siroky477ece62017-08-01 07:08:51 -07005var area = require('@mapbox/geojson-area')
evansiroky70b35fe2018-04-01 21:06:36 -07006var geojsonhint = require('@mapbox/geojsonhint')
evansiroky0ea1d1e2018-10-30 22:30:51 -07007var bbox = require('@turf/bbox').default
Evan Siroky8326cf02017-03-02 08:27:55 -08008var helpers = require('@turf/helpers')
Evan Siroky070bbb92017-03-07 23:48:29 -08009var multiPolygon = helpers.multiPolygon
Evan Siroky8326cf02017-03-02 08:27:55 -080010var polygon = helpers.polygon
Evan Siroky7891a6e2016-11-05 11:50:50 -070011var asynclib = require('async')
evansiroky62278f02020-10-25 14:27:29 -070012var https = require('follow-redirects').https
Evan Siroky7891a6e2016-11-05 11:50:50 -070013var jsts = require('jsts')
Evan Sirokyb57a5b92016-11-07 10:22:34 -080014var rimraf = require('rimraf')
Evan Siroky7891a6e2016-11-05 11:50:50 -070015var overpass = require('query-overpass')
Neil Fullerc4ae49b2020-04-30 18:08:43 +010016var yargs = require('yargs')
evansirokyd401c892016-06-16 00:05:14 -070017
evansiroky62278f02020-10-25 14:27:29 -070018const FeatureWriterStream = require('./util/featureWriterStream')
19const ProgressStats = require('./util/progressStats')
evansiroky5348a6f2019-01-05 15:39:28 -080020
Evan Siroky7891a6e2016-11-05 11:50:50 -070021var osmBoundarySources = require('./osmBoundarySources.json')
22var zoneCfg = require('./timezones.json')
evansiroky0ea1d1e2018-10-30 22:30:51 -070023var expectedZoneOverlaps = require('./expectedZoneOverlaps.json')
Evan Siroky081648a2017-07-04 09:53:36 -070024
Neil Fullerc4ae49b2020-04-30 18:08:43 +010025const argv = yargs
Neil Fullera4e73272020-04-30 18:25:44 +010026 .option('downloads_dir', {
27 description: 'Set the download location',
28 default: './downloads',
29 type: 'string'
30 })
31 .option('dist_dir', {
32 description: 'Set the dist location',
33 default: './dist',
34 type: 'string'
35 })
evansiroky62278f02020-10-25 14:27:29 -070036 .option('excluded_zones', {
37 description: 'Exclude specified zones',
38 type: 'array'
39 })
40 .option('included_zones', {
41 description: 'Include specified zones',
42 type: 'array'
43 })
44 .option('skip_analyze_diffs', {
45 description: 'Skip analysis of diffs between versions',
46 type: 'boolean'
47 })
48 .option('skip_shapefile', {
49 description: 'Skip shapefile creation',
50 type: 'boolean'
51 })
52 .option('skip_validation', {
Neil Fullerc4ae49b2020-04-30 18:08:43 +010053 description: 'Skip validation',
54 type: 'boolean'
55 })
Neil Fullera83cc6d2020-04-30 18:37:08 +010056 .option('skip_zip', {
57 description: 'Skip zip creation',
58 type: 'boolean'
59 })
Neil Fullerc4ae49b2020-04-30 18:08:43 +010060 .help()
61 .strict()
62 .alias('help', 'h')
63 .argv
64
Neil Fuller01803192020-08-14 11:19:42 +010065// Resolve the arguments with paths so relative paths become absolute.
evansirokyb7d9d792020-10-17 16:50:14 -070066const downloadsDir = path.resolve(argv.downloads_dir)
67const distDir = path.resolve(argv.dist_dir)
Neil Fuller01803192020-08-14 11:19:42 +010068
Evan Siroky081648a2017-07-04 09:53:36 -070069// allow building of only a specified zones
Neil Fullerc4ae49b2020-04-30 18:08:43 +010070let includedZones = []
Neil Fuller2b4b80a2020-04-30 18:20:56 +010071let excludedZones = []
72if (argv.included_zones || argv.excluded_zones) {
73 if (argv.included_zones) {
evansirokyb7d9d792020-10-17 16:50:14 -070074 const newZoneCfg = {}
Neil Fuller2b4b80a2020-04-30 18:20:56 +010075 includedZones = argv.included_zones
76 includedZones.forEach((zoneName) => {
77 newZoneCfg[zoneName] = zoneCfg[zoneName]
78 })
79 zoneCfg = newZoneCfg
80 }
81 if (argv.excluded_zones) {
evansirokyb7d9d792020-10-17 16:50:14 -070082 const newZoneCfg = {}
Neil Fuller2b4b80a2020-04-30 18:20:56 +010083 excludedZones = argv.excluded_zones
84 Object.keys(zoneCfg).forEach((zoneName) => {
85 if (!excludedZones.includes(zoneName)) {
86 newZoneCfg[zoneName] = zoneCfg[zoneName]
87 }
88 })
89 zoneCfg = newZoneCfg
90 }
Evan Siroky081648a2017-07-04 09:53:36 -070091
92 // filter out unneccessary downloads
93 var newOsmBoundarySources = {}
94 Object.keys(zoneCfg).forEach((zoneName) => {
95 zoneCfg[zoneName].forEach((op) => {
96 if (op.source === 'overpass') {
97 newOsmBoundarySources[op.id] = osmBoundarySources[op.id]
98 }
99 })
100 })
101
102 osmBoundarySources = newOsmBoundarySources
103}
104
Evan Siroky7891a6e2016-11-05 11:50:50 -0700105var geoJsonReader = new jsts.io.GeoJSONReader()
106var geoJsonWriter = new jsts.io.GeoJSONWriter()
Evan Siroky477ece62017-08-01 07:08:51 -0700107var precisionModel = new jsts.geom.PrecisionModel(1000000)
108var precisionReducer = new jsts.precision.GeometryPrecisionReducer(precisionModel)
Evan Siroky7891a6e2016-11-05 11:50:50 -0700109var distZones = {}
evansiroky96dfadc2020-04-28 01:31:38 -0700110var lastReleaseJSONfile
Evan Sirokyb57a5b92016-11-07 10:22:34 -0800111var minRequestGap = 4
112var curRequestGap = 4
evansiroky62278f02020-10-25 14:27:29 -0700113const bufferDistance = 0.01
evansirokyd401c892016-06-16 00:05:14 -0700114
Evan Siroky7891a6e2016-11-05 11:50:50 -0700115var safeMkdir = function (dirname, callback) {
116 fs.mkdir(dirname, function (err) {
117 if (err && err.code === 'EEXIST') {
evansiroky4be1c7a2016-06-16 18:23:34 -0700118 callback()
119 } else {
120 callback(err)
121 }
122 })
123}
124
evansiroky62278f02020-10-25 14:27:29 -0700125var debugGeo = function (
126 op,
127 a,
128 b,
129 reducePrecision,
130 bufferAfterPrecisionReduction
131) {
evansirokybecb56e2016-07-06 12:42:35 -0700132 var result
133
Evan Sirokyb173fd42017-03-08 15:16:27 -0800134 if (reducePrecision) {
Evan Sirokyb173fd42017-03-08 15:16:27 -0800135 a = precisionReducer.reduce(a)
136 b = precisionReducer.reduce(b)
137 }
138
evansiroky6f9d8f72016-06-21 16:27:54 -0700139 try {
Evan Siroky7891a6e2016-11-05 11:50:50 -0700140 switch (op) {
evansiroky6f9d8f72016-06-21 16:27:54 -0700141 case 'union':
evansirokybecb56e2016-07-06 12:42:35 -0700142 result = a.union(b)
evansiroky6f9d8f72016-06-21 16:27:54 -0700143 break
144 case 'intersection':
evansirokybecb56e2016-07-06 12:42:35 -0700145 result = a.intersection(b)
evansiroky6f9d8f72016-06-21 16:27:54 -0700146 break
Evan Siroky070bbb92017-03-07 23:48:29 -0800147 case 'intersects':
148 result = a.intersects(b)
149 break
evansiroky6f9d8f72016-06-21 16:27:54 -0700150 case 'diff':
Evan Sirokyb173fd42017-03-08 15:16:27 -0800151 result = a.difference(b)
evansiroky6f9d8f72016-06-21 16:27:54 -0700152 break
153 default:
154 var err = new Error('invalid op: ' + op)
155 throw err
156 }
Evan Siroky7891a6e2016-11-05 11:50:50 -0700157 } catch (e) {
Evan Sirokyb173fd42017-03-08 15:16:27 -0800158 if (e.name === 'TopologyException') {
evansiroky62278f02020-10-25 14:27:29 -0700159 if (reducePrecision) {
160 if (bufferAfterPrecisionReduction) {
161 console.log('Encountered TopologyException, retry with buffer increase')
162 return debugGeo(
163 op,
164 a.buffer(bufferDistance),
165 b.buffer(bufferDistance),
166 true,
167 bufferAfterPrecisionReduction
168 )
169 } else {
170 throw new Error('Encountered TopologyException after reducing precision')
171 }
172 } else {
173 console.log('Encountered TopologyException, retry with GeometryPrecisionReducer')
174 return debugGeo(op, a, b, true, bufferAfterPrecisionReduction)
175 }
Evan Sirokyb173fd42017-03-08 15:16:27 -0800176 }
evansiroky6f9d8f72016-06-21 16:27:54 -0700177 console.log('op err')
evansirokybecb56e2016-07-06 12:42:35 -0700178 console.log(e)
179 console.log(e.stack)
180 fs.writeFileSync('debug_' + op + '_a.json', JSON.stringify(geoJsonWriter.write(a)))
181 fs.writeFileSync('debug_' + op + '_b.json', JSON.stringify(geoJsonWriter.write(b)))
evansiroky6f9d8f72016-06-21 16:27:54 -0700182 throw e
183 }
evansiroky6f9d8f72016-06-21 16:27:54 -0700184
evansirokybecb56e2016-07-06 12:42:35 -0700185 return result
evansiroky4be1c7a2016-06-16 18:23:34 -0700186}
187
Evan Siroky070bbb92017-03-07 23:48:29 -0800188var fetchIfNeeded = function (file, superCallback, downloadCallback, fetchFn) {
189 // check for file that got downloaded
Evan Siroky7891a6e2016-11-05 11:50:50 -0700190 fs.stat(file, function (err) {
Evan Siroky070bbb92017-03-07 23:48:29 -0800191 if (!err) {
192 // file found, skip download steps
193 return superCallback()
194 }
195 // check for manual file that got fixed and needs validation
196 var fixedFile = file.replace('.json', '_fixed.json')
197 fs.stat(fixedFile, function (err) {
198 if (!err) {
199 // file found, return fixed file
200 return downloadCallback(null, require(fixedFile))
201 }
202 // no manual fixed file found, download from overpass
203 fetchFn()
204 })
evansiroky50216c62016-06-16 17:41:47 -0700205 })
206}
207
Evan Siroky7891a6e2016-11-05 11:50:50 -0700208var geoJsonToGeom = function (geoJson) {
Evan Siroky8326cf02017-03-02 08:27:55 -0800209 try {
210 return geoJsonReader.read(JSON.stringify(geoJson))
211 } catch (e) {
212 console.error('error converting geojson to geometry')
213 fs.writeFileSync('debug_geojson_read_error.json', JSON.stringify(geoJson))
214 throw e
215 }
Evan Siroky5669adc2016-07-07 17:25:31 -0700216}
217
Evan Siroky8b47abe2016-10-02 12:28:52 -0700218var geomToGeoJson = function (geom) {
219 return geoJsonWriter.write(geom)
220}
221
Evan Siroky7891a6e2016-11-05 11:50:50 -0700222var geomToGeoJsonString = function (geom) {
Evan Siroky5669adc2016-07-07 17:25:31 -0700223 return JSON.stringify(geoJsonWriter.write(geom))
224}
225
evansiroky5348a6f2019-01-05 15:39:28 -0800226const downloadProgress = new ProgressStats(
227 'Downloading',
228 Object.keys(osmBoundarySources).length
229)
230
Evan Siroky7891a6e2016-11-05 11:50:50 -0700231var downloadOsmBoundary = function (boundaryId, boundaryCallback) {
232 var cfg = osmBoundarySources[boundaryId]
Evan Siroky1bcd4772017-10-14 23:47:21 -0700233 var query = '[out:json][timeout:60];('
234 if (cfg.way) {
235 query += 'way'
236 } else {
237 query += 'relation'
238 }
Neil Fullerdce1d042020-10-14 09:53:00 +0100239 var boundaryFilename = downloadsDir + '/' + boundaryId + '.json'
Evan Siroky7891a6e2016-11-05 11:50:50 -0700240 var debug = 'getting data for ' + boundaryId
241 var queryKeys = Object.keys(cfg)
evansiroky63d35e12016-06-16 10:08:15 -0700242
Evan Siroky5669adc2016-07-07 17:25:31 -0700243 for (var i = queryKeys.length - 1; i >= 0; i--) {
Evan Siroky7891a6e2016-11-05 11:50:50 -0700244 var k = queryKeys[i]
Evan Siroky1bcd4772017-10-14 23:47:21 -0700245 if (k === 'way') continue
Evan Siroky7891a6e2016-11-05 11:50:50 -0700246 var v = cfg[k]
Evan Siroky5669adc2016-07-07 17:25:31 -0700247
248 query += '["' + k + '"="' + v + '"]'
evansiroky63d35e12016-06-16 10:08:15 -0700249 }
250
evansiroky283ebbc2018-07-16 15:13:07 -0700251 query += ';);out body;>;out meta qt;'
evansiroky4be1c7a2016-06-16 18:23:34 -0700252
evansiroky5348a6f2019-01-05 15:39:28 -0800253 downloadProgress.beginTask(debug, true)
evansiroky63d35e12016-06-16 10:08:15 -0700254
Evan Siroky7891a6e2016-11-05 11:50:50 -0700255 asynclib.auto({
256 downloadFromOverpass: function (cb) {
evansiroky5348a6f2019-01-05 15:39:28 -0800257 console.log('downloading from overpass')
Evan Siroky070bbb92017-03-07 23:48:29 -0800258 fetchIfNeeded(boundaryFilename, boundaryCallback, cb, function () {
Evan Sirokyb57a5b92016-11-07 10:22:34 -0800259 var overpassResponseHandler = function (err, data) {
260 if (err) {
261 console.log(err)
262 console.log('Increasing overpass request gap')
263 curRequestGap *= 2
264 makeQuery()
265 } else {
266 console.log('Success, decreasing overpass request gap')
267 curRequestGap = Math.max(minRequestGap, curRequestGap / 2)
268 cb(null, data)
269 }
270 }
271 var makeQuery = function () {
272 console.log('waiting ' + curRequestGap + ' seconds')
273 setTimeout(function () {
274 overpass(query, overpassResponseHandler, { flatProperties: true })
275 }, curRequestGap * 1000)
276 }
277 makeQuery()
evansiroky63d35e12016-06-16 10:08:15 -0700278 })
279 },
Evan Siroky7891a6e2016-11-05 11:50:50 -0700280 validateOverpassResult: ['downloadFromOverpass', function (results, cb) {
evansiroky63d35e12016-06-16 10:08:15 -0700281 var data = results.downloadFromOverpass
evansiroky70b35fe2018-04-01 21:06:36 -0700282 if (!data.features) {
Evan Siroky7891a6e2016-11-05 11:50:50 -0700283 var err = new Error('Invalid geojson for boundary: ' + boundaryId)
evansiroky63d35e12016-06-16 10:08:15 -0700284 return cb(err)
285 }
evansiroky70b35fe2018-04-01 21:06:36 -0700286 if (data.features.length === 0) {
287 console.error('No data for the following query:')
288 console.error(query)
289 console.error('To read more about this error, please visit https://git.io/vxKQL')
evansiroky0ea1d1e2018-10-30 22:30:51 -0700290 return cb(new Error('No data found for from overpass query'))
evansiroky70b35fe2018-04-01 21:06:36 -0700291 }
evansiroky63d35e12016-06-16 10:08:15 -0700292 cb()
293 }],
Evan Siroky7891a6e2016-11-05 11:50:50 -0700294 saveSingleMultiPolygon: ['validateOverpassResult', function (results, cb) {
295 var data = results.downloadFromOverpass
296 var combined
evansiroky63d35e12016-06-16 10:08:15 -0700297
298 // union all multi-polygons / polygons into one
299 for (var i = data.features.length - 1; i >= 0; i--) {
Evan Siroky5669adc2016-07-07 17:25:31 -0700300 var curOsmGeom = data.features[i].geometry
evansiroky92c15c42018-11-15 20:58:18 -0800301 const curOsmProps = data.features[i].properties
302 if (
303 (curOsmGeom.type === 'Polygon' || curOsmGeom.type === 'MultiPolygon') &&
304 curOsmProps.type === 'boundary' // need to make sure enclaves aren't unioned
305 ) {
evansiroky63d35e12016-06-16 10:08:15 -0700306 console.log('combining border')
evansiroky70b35fe2018-04-01 21:06:36 -0700307 let errors = geojsonhint.hint(curOsmGeom)
308 if (errors && errors.length > 0) {
309 const stringifiedGeojson = JSON.stringify(curOsmGeom, null, 2)
310 errors = geojsonhint.hint(stringifiedGeojson)
311 console.error('Invalid geojson received in Overpass Result')
312 console.error('Overpass query: ' + query)
313 const problemFilename = boundaryId + '_convert_to_geom_error.json'
314 fs.writeFileSync(problemFilename, stringifiedGeojson)
315 console.error('saved problem file to ' + problemFilename)
316 console.error('To read more about this error, please visit https://git.io/vxKQq')
317 return cb(errors)
318 }
Evan Siroky070bbb92017-03-07 23:48:29 -0800319 try {
320 var curGeom = geoJsonToGeom(curOsmGeom)
321 } catch (e) {
322 console.error('error converting overpass result to geojson')
Evan Siroky477ece62017-08-01 07:08:51 -0700323 console.error(e)
evansiroky70b35fe2018-04-01 21:06:36 -0700324
Evan Siroky1bcd4772017-10-14 23:47:21 -0700325 fs.writeFileSync(boundaryId + '_convert_to_geom_error-all-features.json', JSON.stringify(data))
evansiroky70b35fe2018-04-01 21:06:36 -0700326 return cb(e)
Evan Siroky070bbb92017-03-07 23:48:29 -0800327 }
Evan Siroky7891a6e2016-11-05 11:50:50 -0700328 if (!combined) {
evansiroky63d35e12016-06-16 10:08:15 -0700329 combined = curGeom
330 } else {
Evan Siroky5669adc2016-07-07 17:25:31 -0700331 combined = debugGeo('union', curGeom, combined)
evansiroky63d35e12016-06-16 10:08:15 -0700332 }
333 }
334 }
Evan Siroky081c8e42017-05-29 14:53:52 -0700335 try {
336 fs.writeFile(boundaryFilename, geomToGeoJsonString(combined), cb)
337 } catch (e) {
338 console.error('error writing combined border to geojson')
339 fs.writeFileSync(boundaryId + '_combined_border_convert_to_geom_error.json', JSON.stringify(data))
evansiroky70b35fe2018-04-01 21:06:36 -0700340 return cb(e)
Evan Siroky081c8e42017-05-29 14:53:52 -0700341 }
evansiroky63d35e12016-06-16 10:08:15 -0700342 }]
343 }, boundaryCallback)
344}
evansirokyd401c892016-06-16 00:05:14 -0700345
Evan Siroky4fc596c2016-09-25 19:52:30 -0700346var getTzDistFilename = function (tzid) {
Neil Fullerdce1d042020-10-14 09:53:00 +0100347 return distDir + '/' + tzid.replace(/\//g, '__') + '.json'
Evan Siroky4fc596c2016-09-25 19:52:30 -0700348}
349
350/**
351 * Get the geometry of the requested source data
352 *
353 * @return {Object} geom The geometry of the source
354 * @param {Object} source An object representing the data source
355 * must have `source` key and then either:
356 * - `id` if from a file
357 * - `id` if from a file
358 */
Evan Siroky7891a6e2016-11-05 11:50:50 -0700359var getDataSource = function (source) {
evansirokybecb56e2016-07-06 12:42:35 -0700360 var geoJson
Evan Siroky7891a6e2016-11-05 11:50:50 -0700361 if (source.source === 'overpass') {
Neil Fullerdce1d042020-10-14 09:53:00 +0100362 geoJson = require(downloadsDir + '/' + source.id + '.json')
Evan Siroky7891a6e2016-11-05 11:50:50 -0700363 } else if (source.source === 'manual-polygon') {
evansirokybecb56e2016-07-06 12:42:35 -0700364 geoJson = polygon(source.data).geometry
Evan Siroky7891a6e2016-11-05 11:50:50 -0700365 } else if (source.source === 'manual-multipolygon') {
Evan Siroky8e30a2e2016-08-06 19:55:35 -0700366 geoJson = multiPolygon(source.data).geometry
Evan Siroky7891a6e2016-11-05 11:50:50 -0700367 } else if (source.source === 'dist') {
Evan Siroky4fc596c2016-09-25 19:52:30 -0700368 geoJson = require(getTzDistFilename(source.id))
evansiroky4be1c7a2016-06-16 18:23:34 -0700369 } else {
370 var err = new Error('unknown source: ' + source.source)
371 throw err
372 }
Evan Siroky5669adc2016-07-07 17:25:31 -0700373 return geoJsonToGeom(geoJson)
evansiroky4be1c7a2016-06-16 18:23:34 -0700374}
375
Evan Siroky477ece62017-08-01 07:08:51 -0700376/**
377 * Post process created timezone boundary.
378 * - remove small holes and exclaves
379 * - reduce geometry precision
380 *
381 * @param {Geometry} geom The jsts geometry of the timezone
evansiroky26325842018-04-03 14:10:42 -0700382 * @param {boolean} returnAsObject if true, return as object, otherwise return stringified
383 * @return {Object|String} geojson as object or stringified
Evan Siroky477ece62017-08-01 07:08:51 -0700384 */
evansiroky26325842018-04-03 14:10:42 -0700385var postProcessZone = function (geom, returnAsObject) {
Evan Siroky477ece62017-08-01 07:08:51 -0700386 // reduce precision of geometry
387 const geojson = geomToGeoJson(precisionReducer.reduce(geom))
388
389 // iterate through all polygons
390 const filteredPolygons = []
391 let allPolygons = geojson.coordinates
392 if (geojson.type === 'Polygon') {
393 allPolygons = [geojson.coordinates]
394 }
395
396 allPolygons.forEach((curPolygon, idx) => {
397 // remove any polygon with very small area
398 const polygonFeature = polygon(curPolygon)
399 const polygonArea = area.geometry(polygonFeature.geometry)
400
401 if (polygonArea < 1) return
402
403 // find all holes
404 const filteredLinearRings = []
405
406 curPolygon.forEach((curLinearRing, lrIdx) => {
407 if (lrIdx === 0) {
408 // always keep first linearRing
409 filteredLinearRings.push(curLinearRing)
410 } else {
411 const polygonFromLinearRing = polygon([curLinearRing])
412 const linearRingArea = area.geometry(polygonFromLinearRing.geometry)
413
414 // only include holes with relevant area
415 if (linearRingArea > 1) {
416 filteredLinearRings.push(curLinearRing)
417 }
418 }
419 })
420
421 filteredPolygons.push(filteredLinearRings)
422 })
423
424 // recompile to geojson string
425 const newGeojson = {
426 type: geojson.type
427 }
428
429 if (geojson.type === 'Polygon') {
430 newGeojson.coordinates = filteredPolygons[0]
431 } else {
432 newGeojson.coordinates = filteredPolygons
433 }
434
evansiroky26325842018-04-03 14:10:42 -0700435 return returnAsObject ? newGeojson : JSON.stringify(newGeojson)
Evan Siroky477ece62017-08-01 07:08:51 -0700436}
437
evansiroky5348a6f2019-01-05 15:39:28 -0800438const buildingProgress = new ProgressStats(
439 'Building',
440 Object.keys(zoneCfg).length
441)
442
Evan Siroky7891a6e2016-11-05 11:50:50 -0700443var makeTimezoneBoundary = function (tzid, callback) {
evansiroky3046a3d2019-01-05 21:19:14 -0800444 buildingProgress.beginTask(`makeTimezoneBoundary for ${tzid}`, true)
evansiroky35f64342016-06-16 22:17:04 -0700445
Evan Siroky7891a6e2016-11-05 11:50:50 -0700446 var ops = zoneCfg[tzid]
447 var geom
evansiroky4be1c7a2016-06-16 18:23:34 -0700448
Evan Siroky7891a6e2016-11-05 11:50:50 -0700449 asynclib.eachSeries(ops, function (task, cb) {
evansiroky4be1c7a2016-06-16 18:23:34 -0700450 var taskData = getDataSource(task)
evansiroky6f9d8f72016-06-21 16:27:54 -0700451 console.log('-', task.op, task.id)
Evan Siroky7891a6e2016-11-05 11:50:50 -0700452 if (task.op === 'init') {
evansiroky4be1c7a2016-06-16 18:23:34 -0700453 geom = taskData
Evan Siroky7891a6e2016-11-05 11:50:50 -0700454 } else if (task.op === 'intersect') {
evansiroky6f9d8f72016-06-21 16:27:54 -0700455 geom = debugGeo('intersection', geom, taskData)
Evan Siroky7891a6e2016-11-05 11:50:50 -0700456 } else if (task.op === 'difference') {
evansiroky6f9d8f72016-06-21 16:27:54 -0700457 geom = debugGeo('diff', geom, taskData)
Evan Siroky7891a6e2016-11-05 11:50:50 -0700458 } else if (task.op === 'difference-reverse-order') {
Evan Siroky8ccaf0b2016-09-03 11:36:13 -0700459 geom = debugGeo('diff', taskData, geom)
Evan Siroky7891a6e2016-11-05 11:50:50 -0700460 } else if (task.op === 'union') {
evansiroky6f9d8f72016-06-21 16:27:54 -0700461 geom = debugGeo('union', geom, taskData)
Evan Siroky8ccaf0b2016-09-03 11:36:13 -0700462 } else {
463 var err = new Error('unknown op: ' + task.op)
464 return cb(err)
evansiroky4be1c7a2016-06-16 18:23:34 -0700465 }
evansiroky35f64342016-06-16 22:17:04 -0700466 cb()
Evan Siroky4fc596c2016-09-25 19:52:30 -0700467 },
Evan Siroky7891a6e2016-11-05 11:50:50 -0700468 function (err) {
469 if (err) { return callback(err) }
Evan Siroky4fc596c2016-09-25 19:52:30 -0700470 fs.writeFile(getTzDistFilename(tzid),
Evan Siroky477ece62017-08-01 07:08:51 -0700471 postProcessZone(geom),
evansirokybecb56e2016-07-06 12:42:35 -0700472 callback)
evansiroky4be1c7a2016-06-16 18:23:34 -0700473 })
474}
475
Evan Siroky4fc596c2016-09-25 19:52:30 -0700476var loadDistZonesIntoMemory = function () {
477 console.log('load zones into memory')
Evan Siroky7891a6e2016-11-05 11:50:50 -0700478 var zones = Object.keys(zoneCfg)
479 var tzid
Evan Siroky4fc596c2016-09-25 19:52:30 -0700480
481 for (var i = 0; i < zones.length; i++) {
482 tzid = zones[i]
483 distZones[tzid] = getDataSource({ source: 'dist', id: tzid })
484 }
485}
486
487var getDistZoneGeom = function (tzid) {
488 return distZones[tzid]
489}
490
evansiroky92c15c42018-11-15 20:58:18 -0800491var roundDownToTenth = function (n) {
492 return Math.floor(n * 10) / 10
493}
494
495var roundUpToTenth = function (n) {
496 return Math.ceil(n * 10) / 10
497}
498
499var formatBounds = function (bounds) {
500 let boundsStr = '['
501 boundsStr += roundDownToTenth(bounds[0]) + ', '
502 boundsStr += roundDownToTenth(bounds[1]) + ', '
503 boundsStr += roundUpToTenth(bounds[2]) + ', '
504 boundsStr += roundUpToTenth(bounds[3]) + ']'
505 return boundsStr
506}
507
Evan Siroky4fc596c2016-09-25 19:52:30 -0700508var validateTimezoneBoundaries = function () {
evansiroky5348a6f2019-01-05 15:39:28 -0800509 const numZones = Object.keys(zoneCfg).length
510 const validationProgress = new ProgressStats(
511 'Validation',
512 numZones * (numZones + 1) / 2
513 )
514
evansiroky26325842018-04-03 14:10:42 -0700515 console.log('do validation... this may take a few minutes')
Evan Siroky7891a6e2016-11-05 11:50:50 -0700516 var allZonesOk = true
517 var zones = Object.keys(zoneCfg)
evansiroky3046a3d2019-01-05 21:19:14 -0800518 var lastPct = 0
Evan Siroky7891a6e2016-11-05 11:50:50 -0700519 var compareTzid, tzid, zoneGeom
Evan Siroky4fc596c2016-09-25 19:52:30 -0700520
521 for (var i = 0; i < zones.length; i++) {
522 tzid = zones[i]
523 zoneGeom = getDistZoneGeom(tzid)
524
525 for (var j = i + 1; j < zones.length; j++) {
evansiroky3046a3d2019-01-05 21:19:14 -0800526 const curPct = Math.floor(validationProgress.getPercentage())
527 if (curPct % 10 === 0 && curPct !== lastPct) {
evansiroky5348a6f2019-01-05 15:39:28 -0800528 validationProgress.printStats('Validating zones', true)
evansiroky3046a3d2019-01-05 21:19:14 -0800529 lastPct = curPct
evansiroky5348a6f2019-01-05 15:39:28 -0800530 }
Evan Siroky4fc596c2016-09-25 19:52:30 -0700531 compareTzid = zones[j]
532
533 var compareZoneGeom = getDistZoneGeom(compareTzid)
Evan Siroky070bbb92017-03-07 23:48:29 -0800534
535 var intersects = false
536 try {
537 intersects = debugGeo('intersects', zoneGeom, compareZoneGeom)
538 } catch (e) {
539 console.warn('warning, encountered intersection error with zone ' + tzid + ' and ' + compareTzid)
540 }
541 if (intersects) {
Evan Siroky7891a6e2016-11-05 11:50:50 -0700542 var intersectedGeom = debugGeo('intersection', zoneGeom, compareZoneGeom)
543 var intersectedArea = intersectedGeom.getArea()
Evan Siroky4fc596c2016-09-25 19:52:30 -0700544
Evan Siroky7891a6e2016-11-05 11:50:50 -0700545 if (intersectedArea > 0.0001) {
evansiroky0ea1d1e2018-10-30 22:30:51 -0700546 // check if the intersected area(s) are one of the expected areas of overlap
547 const allowedOverlapBounds = expectedZoneOverlaps[`${tzid}-${compareTzid}`] || expectedZoneOverlaps[`${compareTzid}-${tzid}`]
548 const overlapsGeoJson = geoJsonWriter.write(intersectedGeom)
549
550 // these zones are allowed to overlap in certain places, make sure the
551 // found overlap(s) all fit within the expected areas of overlap
552 if (allowedOverlapBounds) {
553 // if the overlaps are a multipolygon, make sure each individual
554 // polygon of overlap fits within at least one of the expected
555 // overlaps
556 let overlapsPolygons
557 switch (overlapsGeoJson.type) {
evansiroky92c15c42018-11-15 20:58:18 -0800558 case 'MultiPolygon':
559 overlapsPolygons = overlapsGeoJson.coordinates.map(
560 polygonCoords => ({
561 coordinates: polygonCoords,
562 type: 'Polygon'
563 })
564 )
565 break
evansiroky0ea1d1e2018-10-30 22:30:51 -0700566 case 'Polygon':
567 overlapsPolygons = [overlapsGeoJson]
568 break
evansiroky92c15c42018-11-15 20:58:18 -0800569 case 'GeometryCollection':
570 overlapsPolygons = []
571 overlapsGeoJson.geometries.forEach(geom => {
572 if (geom.type === 'Polygon') {
573 overlapsPolygons.push(geom)
574 } else if (geom.type === 'MultiPolygon') {
575 geom.coordinates.forEach(polygonCoords => {
576 overlapsPolygons.push({
577 coordinates: polygonCoords,
578 type: 'Polygon'
579 })
580 })
581 }
582 })
583 break
evansiroky0ea1d1e2018-10-30 22:30:51 -0700584 default:
evansiroky92c15c42018-11-15 20:58:18 -0800585 console.error('unexpected geojson overlap type')
586 console.log(overlapsGeoJson)
evansiroky0ea1d1e2018-10-30 22:30:51 -0700587 break
588 }
589
590 let allOverlapsOk = true
591 overlapsPolygons.forEach((polygon, idx) => {
592 const bounds = bbox(polygon)
evansiroky92c15c42018-11-15 20:58:18 -0800593 const polygonArea = area.geometry(polygon)
evansiroky0ea1d1e2018-10-30 22:30:51 -0700594 if (
evansiroky92c15c42018-11-15 20:58:18 -0800595 polygonArea > 10 && // ignore small polygons
evansiroky0ea1d1e2018-10-30 22:30:51 -0700596 !allowedOverlapBounds.some(allowedBounds =>
evansiroky92c15c42018-11-15 20:58:18 -0800597 allowedBounds.bounds[0] <= bounds[0] && // minX
598 allowedBounds.bounds[1] <= bounds[1] && // minY
599 allowedBounds.bounds[2] >= bounds[2] && // maxX
600 allowedBounds.bounds[3] >= bounds[3] // maxY
evansiroky0ea1d1e2018-10-30 22:30:51 -0700601 )
602 ) {
evansiroky92c15c42018-11-15 20:58:18 -0800603 console.error(`Unexpected intersection (${polygonArea} area) with bounds: ${formatBounds(bounds)}`)
evansiroky0ea1d1e2018-10-30 22:30:51 -0700604 allOverlapsOk = false
605 }
606 })
607
608 if (allOverlapsOk) continue
609 }
610
evansiroky92c15c42018-11-15 20:58:18 -0800611 // at least one unexpected overlap found, output an error and write debug file
evansiroky70b35fe2018-04-01 21:06:36 -0700612 console.error('Validation error: ' + tzid + ' intersects ' + compareTzid + ' area: ' + intersectedArea)
evansiroky92c15c42018-11-15 20:58:18 -0800613 const debugFilename = tzid.replace(/\//g, '-') + '-' + compareTzid.replace(/\//g, '-') + '-overlap.json'
evansiroky70b35fe2018-04-01 21:06:36 -0700614 fs.writeFileSync(
615 debugFilename,
evansiroky0ea1d1e2018-10-30 22:30:51 -0700616 JSON.stringify(overlapsGeoJson)
evansiroky70b35fe2018-04-01 21:06:36 -0700617 )
618 console.error('wrote overlap area as file ' + debugFilename)
619 console.error('To read more about this error, please visit https://git.io/vx6nx')
Evan Siroky4fc596c2016-09-25 19:52:30 -0700620 allZonesOk = false
621 }
622 }
evansiroky5348a6f2019-01-05 15:39:28 -0800623 validationProgress.logNext()
Evan Siroky4fc596c2016-09-25 19:52:30 -0700624 }
625 }
626
627 return allZonesOk ? null : 'Zone validation unsuccessful'
Evan Siroky4fc596c2016-09-25 19:52:30 -0700628}
629
evansiroky26325842018-04-03 14:10:42 -0700630let oceanZoneBoundaries
evansiroky9fd50512019-07-07 12:06:28 -0700631let oceanZones = [
632 { tzid: 'Etc/GMT-12', left: 172.5, right: 180 },
633 { tzid: 'Etc/GMT-11', left: 157.5, right: 172.5 },
634 { tzid: 'Etc/GMT-10', left: 142.5, right: 157.5 },
635 { tzid: 'Etc/GMT-9', left: 127.5, right: 142.5 },
636 { tzid: 'Etc/GMT-8', left: 112.5, right: 127.5 },
637 { tzid: 'Etc/GMT-7', left: 97.5, right: 112.5 },
638 { tzid: 'Etc/GMT-6', left: 82.5, right: 97.5 },
639 { tzid: 'Etc/GMT-5', left: 67.5, right: 82.5 },
640 { tzid: 'Etc/GMT-4', left: 52.5, right: 67.5 },
641 { tzid: 'Etc/GMT-3', left: 37.5, right: 52.5 },
642 { tzid: 'Etc/GMT-2', left: 22.5, right: 37.5 },
643 { tzid: 'Etc/GMT-1', left: 7.5, right: 22.5 },
644 { tzid: 'Etc/GMT', left: -7.5, right: 7.5 },
645 { tzid: 'Etc/GMT+1', left: -22.5, right: -7.5 },
646 { tzid: 'Etc/GMT+2', left: -37.5, right: -22.5 },
647 { tzid: 'Etc/GMT+3', left: -52.5, right: -37.5 },
648 { tzid: 'Etc/GMT+4', left: -67.5, right: -52.5 },
649 { tzid: 'Etc/GMT+5', left: -82.5, right: -67.5 },
650 { tzid: 'Etc/GMT+6', left: -97.5, right: -82.5 },
651 { tzid: 'Etc/GMT+7', left: -112.5, right: -97.5 },
652 { tzid: 'Etc/GMT+8', left: -127.5, right: -112.5 },
653 { tzid: 'Etc/GMT+9', left: -142.5, right: -127.5 },
654 { tzid: 'Etc/GMT+10', left: -157.5, right: -142.5 },
655 { tzid: 'Etc/GMT+11', left: -172.5, right: -157.5 },
656 { tzid: 'Etc/GMT+12', left: -180, right: -172.5 }
657]
658
Neil Fullerc4ae49b2020-04-30 18:08:43 +0100659if (includedZones.length > 0) {
660 oceanZones = oceanZones.filter(oceanZone => includedZones.indexOf(oceanZone) > -1)
evansiroky9fd50512019-07-07 12:06:28 -0700661}
Neil Fuller2b4b80a2020-04-30 18:20:56 +0100662if (excludedZones.length > 0) {
663 oceanZones = oceanZones.filter(oceanZone => excludedZones.indexOf(oceanZone) === -1)
664}
evansiroky26325842018-04-03 14:10:42 -0700665
666var addOceans = function (callback) {
667 console.log('adding ocean boundaries')
evansiroky26325842018-04-03 14:10:42 -0700668 const zones = Object.keys(zoneCfg)
669
evansiroky3046a3d2019-01-05 21:19:14 -0800670 const oceanProgress = new ProgressStats(
671 'Oceans',
672 oceanZones.length
673 )
674
evansiroky26325842018-04-03 14:10:42 -0700675 oceanZoneBoundaries = oceanZones.map(zone => {
evansiroky3046a3d2019-01-05 21:19:14 -0800676 oceanProgress.beginTask(zone.tzid, true)
evansiroky26325842018-04-03 14:10:42 -0700677 const geoJson = polygon([[
678 [zone.left, 90],
evansiroky0ea1d1e2018-10-30 22:30:51 -0700679 [zone.left, -90],
680 [zone.right, -90],
681 [zone.right, 90],
evansiroky26325842018-04-03 14:10:42 -0700682 [zone.left, 90]
683 ]]).geometry
684
685 let geom = geoJsonToGeom(geoJson)
686
687 // diff against every zone
688 zones.forEach(distZone => {
689 geom = debugGeo('diff', geom, getDistZoneGeom(distZone))
690 })
691
692 return {
693 geom: postProcessZone(geom, true),
694 tzid: zone.tzid
695 }
696 })
697
698 callback()
699}
700
Evan Siroky7891a6e2016-11-05 11:50:50 -0700701var combineAndWriteZones = function (callback) {
evansiroky62278f02020-10-25 14:27:29 -0700702 const regularWriter = new FeatureWriterStream(distDir + '/combined.json')
703 const oceanWriter = new FeatureWriterStream(distDir + '/combined-with-oceans.json')
Evan Siroky8b47abe2016-10-02 12:28:52 -0700704 var zones = Object.keys(zoneCfg)
705
evansiroky62278f02020-10-25 14:27:29 -0700706 zones.forEach(zoneName => {
707 const feature = {
Evan Siroky8b47abe2016-10-02 12:28:52 -0700708 type: 'Feature',
evansiroky62278f02020-10-25 14:27:29 -0700709 properties: { tzid: zoneName },
710 geometry: geomToGeoJson(getDistZoneGeom(zoneName))
Evan Siroky8b47abe2016-10-02 12:28:52 -0700711 }
evansiroky26325842018-04-03 14:10:42 -0700712 const stringified = JSON.stringify(feature)
evansiroky62278f02020-10-25 14:27:29 -0700713 regularWriter.add(stringified)
714 oceanWriter.add(stringified)
715 })
evansiroky26325842018-04-03 14:10:42 -0700716 oceanZoneBoundaries.forEach(boundary => {
evansiroky26325842018-04-03 14:10:42 -0700717 var feature = {
718 type: 'Feature',
719 properties: { tzid: boundary.tzid },
720 geometry: boundary.geom
721 }
evansiroky62278f02020-10-25 14:27:29 -0700722 oceanWriter.add(JSON.stringify(feature))
evansiroky26325842018-04-03 14:10:42 -0700723 })
724 asynclib.parallel([
evansiroky62278f02020-10-25 14:27:29 -0700725 cb => regularWriter.end(cb),
726 cb => oceanWriter.end(cb)
evansiroky26325842018-04-03 14:10:42 -0700727 ], callback)
Evan Siroky8b47abe2016-10-02 12:28:52 -0700728}
729
evansiroky96dfadc2020-04-28 01:31:38 -0700730var downloadLastRelease = function (cb) {
evansiroky96dfadc2020-04-28 01:31:38 -0700731 // download latest release info
evansiroky62278f02020-10-25 14:27:29 -0700732 https.get(
733 {
734 headers: { 'user-agent': 'timezone-boundary-builder' },
735 host: 'api.github.com',
736 path: '/repos/evansiroky/timezone-boundary-builder/releases/latest'
737 },
738 function (res) {
739 var data = ''
740 res.on('data', function (chunk) {
741 data += chunk
742 })
743 res.on('end', function () {
744 data = JSON.parse(data)
745 // determine last release version name and download link
746 const lastReleaseName = data.name
747 lastReleaseJSONfile = `./dist/${lastReleaseName}.json`
748 let lastReleaseDownloadUrl
749 for (var i = 0; i < data.assets.length; i++) {
750 if (data.assets[i].browser_download_url.indexOf('timezones-with-oceans.geojson') > -1) {
751 lastReleaseDownloadUrl = data.assets[i].browser_download_url
752 }
753 }
754 if (!lastReleaseDownloadUrl) {
755 return cb(new Error('geojson not found'))
756 }
evansiroky96dfadc2020-04-28 01:31:38 -0700757
evansiroky62278f02020-10-25 14:27:29 -0700758 // check for file that got downloaded
759 fs.stat(lastReleaseJSONfile, function (err) {
760 if (!err) {
761 // file found, skip download steps
762 return cb()
763 }
764 // file not found, download
765 console.log(`Downloading latest release to ${lastReleaseJSONfile}.zip`)
766 https.get({
767 headers: { 'user-agent': 'timezone-boundary-builder' },
768 host: 'github.com',
769 path: lastReleaseDownloadUrl.replace('https://github.com', '')
770 }, function (response) {
771 var file = fs.createWriteStream(`${lastReleaseJSONfile}.zip`)
772 response.pipe(file)
773 file.on('finish', function () {
774 file.close((err) => {
775 if (err) return cb(err)
776 // unzip file
777 console.log('unzipping latest release')
778 exec(
779 `unzip -o ${lastReleaseJSONfile} -d dist`,
780 err => {
781 if (err) { return cb(err) }
782 console.log('unzipped file')
783 console.log('moving unzipped file')
784 // might need to change this after changes to how files are
785 // zipped after 2020a
786 fs.copyFile(
787 path.join(
788 'dist',
789 'dist',
790 'combined-with-oceans.json'
791 ),
792 lastReleaseJSONfile,
793 cb
794 )
795 }
796 )
797 })
798 })
799 }).on('error', cb)
800 })
801 })
802 }
803 )
evansiroky96dfadc2020-04-28 01:31:38 -0700804}
805
806var analyzeChangesFromLastRelease = function (cb) {
evansiroky96dfadc2020-04-28 01:31:38 -0700807 // load last release data into memory
evansiroky62278f02020-10-25 14:27:29 -0700808 console.log('loading previous release into memory')
809 const lastReleaseData = require(lastReleaseJSONfile)
810
811 // load each feature's geojson into JSTS format and then organized by tzid
812 const lastReleaseZones = {}
813 lastReleaseData.features.forEach(
814 feature => {
815 lastReleaseZones[feature.properties.tzid] = feature
816 }
817 )
evansiroky96dfadc2020-04-28 01:31:38 -0700818
819 // generate set of keys from last release and current
evansiroky62278f02020-10-25 14:27:29 -0700820 const zoneNames = new Set()
821 Object.keys(distZones).forEach(zoneName => zoneNames.add(zoneName))
822 Object.keys(lastReleaseZones).forEach(zoneName => zoneNames.add(zoneName))
evansiroky96dfadc2020-04-28 01:31:38 -0700823
evansiroky62278f02020-10-25 14:27:29 -0700824 // create diff for each zone
825 const analysisProgress = new ProgressStats(
826 'Analyzing diffs',
827 zoneNames.size
828 )
829 const additionsWriter = new FeatureWriterStream(distDir + '/additions.json')
830 const removalsWriter = new FeatureWriterStream(distDir + '/removals.json')
831 zoneNames.forEach(zoneName => {
832 analysisProgress.beginTask(zoneName, true)
833 if (distZones[zoneName] && lastReleaseZones[zoneName]) {
834 // some zones take forever to diff unless they are buffered, so buffer by
835 // just a small amount
836 const lastReleaseGeom = geoJsonToGeom(
837 lastReleaseZones[zoneName].geometry
838 ).buffer(bufferDistance)
839 const curDataGeom = getDistZoneGeom(zoneName).buffer(bufferDistance)
evansiroky96dfadc2020-04-28 01:31:38 -0700840
evansiroky62278f02020-10-25 14:27:29 -0700841 // don't diff equal geometries
842 if (curDataGeom.equals(lastReleaseGeom)) return
843
844 // diff current - last = additions
845 const addition = debugGeo(
846 'diff',
847 curDataGeom,
848 lastReleaseGeom,
849 false,
850 true
851 )
852 if (addition.getArea() > 0.0001) {
853 additionsWriter.add(JSON.stringify({
854 type: 'Feature',
855 properties: { tzid: zoneName },
856 geometry: geomToGeoJson(addition)
857 }))
858 }
859
860 // diff last - current = removals
861 const removal = debugGeo(
862 'diff',
863 lastReleaseGeom,
864 curDataGeom,
865 false,
866 true
867 )
868 if (removal.getArea() > 0.0001) {
869 removalsWriter.add(JSON.stringify({
870 type: 'Feature',
871 properties: { tzid: zoneName },
872 geometry: geomToGeoJson(removal)
873 }))
874 }
875 } else if (distZones[zoneName]) {
876 additionsWriter.add(JSON.stringify({
877 type: 'Feature',
878 properties: { tzid: zoneName },
879 geometry: geomToGeoJson(getDistZoneGeom(zoneName))
880 }))
881 } else {
882 removalsWriter.add(JSON.stringify(lastReleaseZones[zoneName]))
883 }
884 })
885
886 // write files
887 asynclib.parallel([
888 wcb => additionsWriter.end(wcb),
889 wcb => removalsWriter.end(wcb)
890 ], cb)
evansiroky96dfadc2020-04-28 01:31:38 -0700891}
892
evansiroky5348a6f2019-01-05 15:39:28 -0800893const autoScript = {
Evan Siroky7891a6e2016-11-05 11:50:50 -0700894 makeDownloadsDir: function (cb) {
evansiroky5348a6f2019-01-05 15:39:28 -0800895 overallProgress.beginTask('Creating downloads dir')
Neil Fullerdce1d042020-10-14 09:53:00 +0100896 safeMkdir(downloadsDir, cb)
evansiroky4be1c7a2016-06-16 18:23:34 -0700897 },
Evan Siroky7891a6e2016-11-05 11:50:50 -0700898 makeDistDir: function (cb) {
evansiroky5348a6f2019-01-05 15:39:28 -0800899 overallProgress.beginTask('Creating dist dir')
Neil Fullerdce1d042020-10-14 09:53:00 +0100900 safeMkdir(distDir, cb)
evansirokyd401c892016-06-16 00:05:14 -0700901 },
Evan Siroky7891a6e2016-11-05 11:50:50 -0700902 getOsmBoundaries: ['makeDownloadsDir', function (results, cb) {
evansiroky5348a6f2019-01-05 15:39:28 -0800903 overallProgress.beginTask('Downloading osm boundaries')
Evan Siroky7891a6e2016-11-05 11:50:50 -0700904 asynclib.eachSeries(Object.keys(osmBoundarySources), downloadOsmBoundary, cb)
evansiroky63d35e12016-06-16 10:08:15 -0700905 }],
evansirokyf3bcf052020-10-17 23:10:15 -0700906 cleanDownloadFolder: ['makeDistDir', 'getOsmBoundaries', function (results, cb) {
907 overallProgress.beginTask('cleanDownloadFolder')
908 const downloadedFilenames = Object.keys(osmBoundarySources).map(name => `${name}.json`)
evansirokyb0442ca2020-10-18 17:49:45 -0700909 fs.readdir(downloadsDir, (err, files) => {
evansirokyf3bcf052020-10-17 23:10:15 -0700910 if (err) return cb(err)
911 asynclib.each(
912 files,
913 (file, fileCb) => {
914 if (downloadedFilenames.indexOf(file) === -1) {
evansirokyb0442ca2020-10-18 17:49:45 -0700915 return fs.unlink(path.join(downloadsDir, file), fileCb)
evansirokyf3bcf052020-10-17 23:10:15 -0700916 }
917 fileCb()
918 },
919 cb
920 )
921 })
922 }],
923 zipInputData: ['cleanDownloadFolder', function (results, cb) {
evansirokya48b3922020-04-27 15:29:06 -0700924 overallProgress.beginTask('Zipping up input data')
evansiroky7452c9b2020-10-18 00:27:06 -0700925 exec('zip -j ' + distDir + '/input-data.zip ' + downloadsDir +
Neil Fullerdce1d042020-10-14 09:53:00 +0100926 '/* timezones.json osmBoundarySources.json expectedZoneOverlaps.json', cb)
evansirokya48b3922020-04-27 15:29:06 -0700927 }],
evansiroky96dfadc2020-04-28 01:31:38 -0700928 downloadLastRelease: ['makeDistDir', function (results, cb) {
evansiroky62278f02020-10-25 14:27:29 -0700929 if (argv.skip_analyze_diffs) {
930 overallProgress.beginTask('WARNING: Skipping download of last release for analysis!')
931 cb()
932 } else {
evansiroky96dfadc2020-04-28 01:31:38 -0700933 overallProgress.beginTask('Downloading last release for analysis')
934 downloadLastRelease(cb)
evansiroky96dfadc2020-04-28 01:31:38 -0700935 }
936 }],
Evan Siroky7891a6e2016-11-05 11:50:50 -0700937 createZones: ['makeDistDir', 'getOsmBoundaries', function (results, cb) {
evansiroky5348a6f2019-01-05 15:39:28 -0800938 overallProgress.beginTask('Creating timezone boundaries')
Evan Siroky7891a6e2016-11-05 11:50:50 -0700939 asynclib.each(Object.keys(zoneCfg), makeTimezoneBoundary, cb)
evansiroky50216c62016-06-16 17:41:47 -0700940 }],
Evan Siroky7891a6e2016-11-05 11:50:50 -0700941 validateZones: ['createZones', function (results, cb) {
evansiroky5348a6f2019-01-05 15:39:28 -0800942 overallProgress.beginTask('Validating timezone boundaries')
Evan Siroky4fc596c2016-09-25 19:52:30 -0700943 loadDistZonesIntoMemory()
evansiroky62278f02020-10-25 14:27:29 -0700944 if (argv.skip_validation) {
Evan Siroky081648a2017-07-04 09:53:36 -0700945 console.warn('WARNING: Skipping validation!')
946 cb()
947 } else {
948 cb(validateTimezoneBoundaries())
949 }
Evan Siroky4fc596c2016-09-25 19:52:30 -0700950 }],
evansiroky26325842018-04-03 14:10:42 -0700951 addOceans: ['validateZones', function (results, cb) {
evansiroky5348a6f2019-01-05 15:39:28 -0800952 overallProgress.beginTask('Adding oceans')
evansiroky26325842018-04-03 14:10:42 -0700953 addOceans(cb)
954 }],
955 mergeZones: ['addOceans', function (results, cb) {
evansiroky5348a6f2019-01-05 15:39:28 -0800956 overallProgress.beginTask('Merging zones')
Evan Siroky8b47abe2016-10-02 12:28:52 -0700957 combineAndWriteZones(cb)
958 }],
959 zipGeoJson: ['mergeZones', function (results, cb) {
Neil Fullera83cc6d2020-04-30 18:37:08 +0100960 if (argv.skip_zip) {
961 overallProgress.beginTask('Skipping zip')
Neil Fuller61287092020-07-29 14:46:16 +0100962 return cb()
Neil Fullera83cc6d2020-04-30 18:37:08 +0100963 }
Neil Fuller61287092020-07-29 14:46:16 +0100964 overallProgress.beginTask('Zipping geojson')
evansirokyb7d9d792020-10-17 16:50:14 -0700965 const zipFile = distDir + '/timezones.geojson.zip'
966 const jsonFile = distDir + '/combined.json'
evansiroky7452c9b2020-10-18 00:27:06 -0700967 exec('zip -j ' + zipFile + ' ' + jsonFile, cb)
Evan Siroky8b47abe2016-10-02 12:28:52 -0700968 }],
evansiroky26325842018-04-03 14:10:42 -0700969 zipGeoJsonWithOceans: ['mergeZones', function (results, cb) {
Neil Fullera83cc6d2020-04-30 18:37:08 +0100970 if (argv.skip_zip) {
971 overallProgress.beginTask('Skipping with oceans zip')
Neil Fuller61287092020-07-29 14:46:16 +0100972 return cb()
Neil Fullera83cc6d2020-04-30 18:37:08 +0100973 }
Neil Fuller61287092020-07-29 14:46:16 +0100974 overallProgress.beginTask('Zipping geojson with oceans')
evansirokyb7d9d792020-10-17 16:50:14 -0700975 const zipFile = distDir + '/timezones-with-oceans.geojson.zip'
976 const jsonFile = distDir + '/combined-with-oceans.json'
evansiroky7452c9b2020-10-18 00:27:06 -0700977 exec('zip -j ' + zipFile + ' ' + jsonFile, cb)
evansiroky26325842018-04-03 14:10:42 -0700978 }],
Evan Siroky8b47abe2016-10-02 12:28:52 -0700979 makeShapefile: ['mergeZones', function (results, cb) {
Neil Fullera83cc6d2020-04-30 18:37:08 +0100980 if (argv.skip_shapefile) {
981 overallProgress.beginTask('Skipping shapefile creation')
Neil Fuller61287092020-07-29 14:46:16 +0100982 return cb()
Neil Fullera83cc6d2020-04-30 18:37:08 +0100983 }
Neil Fuller61287092020-07-29 14:46:16 +0100984 overallProgress.beginTask('Converting from geojson to shapefile')
evansirokyb7d9d792020-10-17 16:50:14 -0700985 const shapeFileGlob = distDir + '/combined-shapefile.*'
Neil Fuller61287092020-07-29 14:46:16 +0100986 rimraf.sync(shapeFileGlob)
evansirokyb7d9d792020-10-17 16:50:14 -0700987 const shapeFile = distDir + '/combined-shapefile.shp'
988 const jsonFile = distDir + '/combined.json'
Neil Fuller61287092020-07-29 14:46:16 +0100989 exec(
990 'ogr2ogr -f "ESRI Shapefile" ' + shapeFile + ' ' + jsonFile,
991 function (err, stdout, stderr) {
992 if (err) { return cb(err) }
evansirokyb7d9d792020-10-17 16:50:14 -0700993 const shapeFileZip = distDir + '/timezones.shapefile.zip'
evansiroky7452c9b2020-10-18 00:27:06 -0700994 exec('zip -j ' + shapeFileZip + ' ' + shapeFileGlob, cb)
Neil Fuller61287092020-07-29 14:46:16 +0100995 }
996 )
evansiroky26325842018-04-03 14:10:42 -0700997 }],
998 makeShapefileWithOceans: ['mergeZones', function (results, cb) {
Neil Fullera83cc6d2020-04-30 18:37:08 +0100999 if (argv.skip_shapefile) {
1000 overallProgress.beginTask('Skipping with oceans shapefile creation')
Neil Fuller61287092020-07-29 14:46:16 +01001001 return cb()
Neil Fullera83cc6d2020-04-30 18:37:08 +01001002 }
Neil Fuller61287092020-07-29 14:46:16 +01001003 overallProgress.beginTask('Converting from geojson with oceans to shapefile')
evansirokyb7d9d792020-10-17 16:50:14 -07001004 const shapeFileGlob = distDir + '/combined-shapefile-with-oceans.*'
Neil Fuller61287092020-07-29 14:46:16 +01001005 rimraf.sync(shapeFileGlob)
evansirokyb7d9d792020-10-17 16:50:14 -07001006 const shapeFile = distDir + '/combined-shapefile-with-oceans.shp'
1007 const jsonFile = distDir + '/combined-with-oceans.json'
Neil Fuller61287092020-07-29 14:46:16 +01001008 exec(
1009 'ogr2ogr -f "ESRI Shapefile" ' + shapeFile + ' ' + jsonFile,
1010 function (err, stdout, stderr) {
1011 if (err) { return cb(err) }
evansirokyb7d9d792020-10-17 16:50:14 -07001012 const shapeFileZip = distDir + '/timezones-with-oceans.shapefile.zip'
evansiroky7452c9b2020-10-18 00:27:06 -07001013 exec('zip -j ' + shapeFileZip + ' ' + shapeFileGlob, cb)
Neil Fuller61287092020-07-29 14:46:16 +01001014 }
1015 )
evansiroky9fd50512019-07-07 12:06:28 -07001016 }],
1017 makeListOfTimeZoneNames: function (cb) {
1018 overallProgress.beginTask('Writing timezone names to file')
1019 let zoneNames = Object.keys(zoneCfg)
1020 oceanZones.forEach(oceanZone => {
1021 zoneNames.push(oceanZone.tzid)
1022 })
Neil Fullerc4ae49b2020-04-30 18:08:43 +01001023 if (includedZones.length > 0) {
1024 zoneNames = zoneNames.filter(zoneName => includedZones.indexOf(zoneName) > -1)
evansiroky9fd50512019-07-07 12:06:28 -07001025 }
Neil Fuller2b4b80a2020-04-30 18:20:56 +01001026 if (excludedZones.length > 0) {
1027 zoneNames = zoneNames.filter(zoneName => excludedZones.indexOf(zoneName) === -1)
1028 }
evansiroky9fd50512019-07-07 12:06:28 -07001029 fs.writeFile(
Neil Fullerdce1d042020-10-14 09:53:00 +01001030 distDir + '/timezone-names.json',
evansiroky9fd50512019-07-07 12:06:28 -07001031 JSON.stringify(zoneNames),
1032 cb
1033 )
evansiroky96dfadc2020-04-28 01:31:38 -07001034 },
1035 analyzeChangesFromLastRelease: ['downloadLastRelease', 'mergeZones', function (results, cb) {
evansiroky62278f02020-10-25 14:27:29 -07001036 if (argv.skip_analyze_diffs) {
1037 overallProgress.beginTask('WARNING: Skipping analysis of changes from last release!')
1038 cb()
1039 } else {
evansiroky96dfadc2020-04-28 01:31:38 -07001040 overallProgress.beginTask('Analyzing changes from last release')
1041 analyzeChangesFromLastRelease(cb)
evansiroky96dfadc2020-04-28 01:31:38 -07001042 }
1043 }]
evansiroky5348a6f2019-01-05 15:39:28 -08001044}
1045
1046const overallProgress = new ProgressStats('Overall', Object.keys(autoScript).length)
1047
1048asynclib.auto(autoScript, function (err, results) {
evansirokyd401c892016-06-16 00:05:14 -07001049 console.log('done')
Evan Siroky7891a6e2016-11-05 11:50:50 -07001050 if (err) {
evansirokyd401c892016-06-16 00:05:14 -07001051 console.log('error!', err)
evansirokyd401c892016-06-16 00:05:14 -07001052 }
Evan Siroky4fc596c2016-09-25 19:52:30 -07001053})