blob: 78b2f80e5b1d777a69c99c3abb0332a36aa4e2db [file] [log] [blame]
Evan Siroky7891a6e2016-11-05 11:50:50 -07001var exec = require('child_process').exec
2var fs = require('fs')
evansirokyd401c892016-06-16 00:05:14 -07003
Evan Siroky7891a6e2016-11-05 11:50:50 -07004var asynclib = require('async')
5var jsts = require('jsts')
Evan Sirokyb57a5b92016-11-07 10:22:34 -08006var rimraf = require('rimraf')
Evan Siroky7891a6e2016-11-05 11:50:50 -07007var multiPolygon = require('turf-multipolygon')
8var overpass = require('query-overpass')
9var polygon = require('turf-polygon')
evansirokyd401c892016-06-16 00:05:14 -070010
Evan Siroky7891a6e2016-11-05 11:50:50 -070011var osmBoundarySources = require('./osmBoundarySources.json')
12var zoneCfg = require('./timezones.json')
13var geoJsonReader = new jsts.io.GeoJSONReader()
14var geoJsonWriter = new jsts.io.GeoJSONWriter()
15var distZones = {}
Evan Sirokyb57a5b92016-11-07 10:22:34 -080016var minRequestGap = 4
17var curRequestGap = 4
evansirokyd401c892016-06-16 00:05:14 -070018
Evan Siroky7891a6e2016-11-05 11:50:50 -070019var safeMkdir = function (dirname, callback) {
20 fs.mkdir(dirname, function (err) {
21 if (err && err.code === 'EEXIST') {
evansiroky4be1c7a2016-06-16 18:23:34 -070022 callback()
23 } else {
24 callback(err)
25 }
26 })
27}
28
Evan Siroky7891a6e2016-11-05 11:50:50 -070029var debugGeo = function (op, a, b) {
evansirokybecb56e2016-07-06 12:42:35 -070030 var result
31
evansiroky6f9d8f72016-06-21 16:27:54 -070032 try {
Evan Siroky7891a6e2016-11-05 11:50:50 -070033 switch (op) {
evansiroky6f9d8f72016-06-21 16:27:54 -070034 case 'union':
evansirokybecb56e2016-07-06 12:42:35 -070035 result = a.union(b)
evansiroky6f9d8f72016-06-21 16:27:54 -070036 break
37 case 'intersection':
evansirokybecb56e2016-07-06 12:42:35 -070038 result = a.intersection(b)
evansiroky6f9d8f72016-06-21 16:27:54 -070039 break
40 case 'diff':
evansirokybecb56e2016-07-06 12:42:35 -070041 try {
42 result = a.difference(b)
Evan Siroky7891a6e2016-11-05 11:50:50 -070043 } catch (e) {
44 if (e.name === 'TopologyException') {
evansirokybecb56e2016-07-06 12:42:35 -070045 console.log('retry with GeometryPrecisionReducer')
Evan Siroky7891a6e2016-11-05 11:50:50 -070046 var precisionModel = new jsts.geom.PrecisionModel(10000)
47 var precisionReducer = new jsts.precision.GeometryPrecisionReducer(precisionModel)
evansirokybecb56e2016-07-06 12:42:35 -070048
49 a = precisionReducer.reduce(a)
50 b = precisionReducer.reduce(b)
51
52 result = a.difference(b)
53 } else {
54 throw e
55 }
56 }
evansiroky6f9d8f72016-06-21 16:27:54 -070057 break
58 default:
59 var err = new Error('invalid op: ' + op)
60 throw err
61 }
Evan Siroky7891a6e2016-11-05 11:50:50 -070062 } catch (e) {
evansiroky6f9d8f72016-06-21 16:27:54 -070063 console.log('op err')
evansirokybecb56e2016-07-06 12:42:35 -070064 console.log(e)
65 console.log(e.stack)
66 fs.writeFileSync('debug_' + op + '_a.json', JSON.stringify(geoJsonWriter.write(a)))
67 fs.writeFileSync('debug_' + op + '_b.json', JSON.stringify(geoJsonWriter.write(b)))
evansiroky6f9d8f72016-06-21 16:27:54 -070068 throw e
69 }
evansiroky6f9d8f72016-06-21 16:27:54 -070070
evansirokybecb56e2016-07-06 12:42:35 -070071 return result
evansiroky4be1c7a2016-06-16 18:23:34 -070072}
73
Evan Siroky7891a6e2016-11-05 11:50:50 -070074var fetchIfNeeded = function (file, superCallback, fetchFn) {
75 fs.stat(file, function (err) {
76 if (!err) { return superCallback() }
evansiroky50216c62016-06-16 17:41:47 -070077 fetchFn()
78 })
79}
80
Evan Siroky7891a6e2016-11-05 11:50:50 -070081var geoJsonToGeom = function (geoJson) {
Evan Siroky5669adc2016-07-07 17:25:31 -070082 return geoJsonReader.read(JSON.stringify(geoJson))
83}
84
Evan Siroky8b47abe2016-10-02 12:28:52 -070085var geomToGeoJson = function (geom) {
86 return geoJsonWriter.write(geom)
87}
88
Evan Siroky7891a6e2016-11-05 11:50:50 -070089var geomToGeoJsonString = function (geom) {
Evan Siroky5669adc2016-07-07 17:25:31 -070090 return JSON.stringify(geoJsonWriter.write(geom))
91}
92
Evan Siroky7891a6e2016-11-05 11:50:50 -070093var downloadOsmBoundary = function (boundaryId, boundaryCallback) {
94 var cfg = osmBoundarySources[boundaryId]
95 var query = '[out:json][timeout:60];(relation'
96 var boundaryFilename = './downloads/' + boundaryId + '.json'
97 var debug = 'getting data for ' + boundaryId
98 var queryKeys = Object.keys(cfg)
evansiroky63d35e12016-06-16 10:08:15 -070099
Evan Siroky5669adc2016-07-07 17:25:31 -0700100 for (var i = queryKeys.length - 1; i >= 0; i--) {
Evan Siroky7891a6e2016-11-05 11:50:50 -0700101 var k = queryKeys[i]
102 var v = cfg[k]
Evan Siroky5669adc2016-07-07 17:25:31 -0700103
104 query += '["' + k + '"="' + v + '"]'
evansiroky63d35e12016-06-16 10:08:15 -0700105 }
106
Evan Siroky5669adc2016-07-07 17:25:31 -0700107 query += ');out body;>;out meta qt;'
evansiroky4be1c7a2016-06-16 18:23:34 -0700108
evansiroky63d35e12016-06-16 10:08:15 -0700109 console.log(debug)
110
Evan Siroky7891a6e2016-11-05 11:50:50 -0700111 asynclib.auto({
112 downloadFromOverpass: function (cb) {
evansiroky50216c62016-06-16 17:41:47 -0700113 console.log('downloading from overpass')
Evan Siroky7891a6e2016-11-05 11:50:50 -0700114 fetchIfNeeded(boundaryFilename, boundaryCallback, function () {
Evan Sirokyb57a5b92016-11-07 10:22:34 -0800115 var overpassResponseHandler = function (err, data) {
116 if (err) {
117 console.log(err)
118 console.log('Increasing overpass request gap')
119 curRequestGap *= 2
120 makeQuery()
121 } else {
122 console.log('Success, decreasing overpass request gap')
123 curRequestGap = Math.max(minRequestGap, curRequestGap / 2)
124 cb(null, data)
125 }
126 }
127 var makeQuery = function () {
128 console.log('waiting ' + curRequestGap + ' seconds')
129 setTimeout(function () {
130 overpass(query, overpassResponseHandler, { flatProperties: true })
131 }, curRequestGap * 1000)
132 }
133 makeQuery()
evansiroky63d35e12016-06-16 10:08:15 -0700134 })
135 },
Evan Siroky7891a6e2016-11-05 11:50:50 -0700136 validateOverpassResult: ['downloadFromOverpass', function (results, cb) {
evansiroky63d35e12016-06-16 10:08:15 -0700137 var data = results.downloadFromOverpass
Evan Siroky7891a6e2016-11-05 11:50:50 -0700138 if (!data.features || data.features.length === 0) {
139 var err = new Error('Invalid geojson for boundary: ' + boundaryId)
evansiroky63d35e12016-06-16 10:08:15 -0700140 return cb(err)
141 }
142 cb()
143 }],
Evan Siroky7891a6e2016-11-05 11:50:50 -0700144 saveSingleMultiPolygon: ['validateOverpassResult', function (results, cb) {
145 var data = results.downloadFromOverpass
146 var combined
evansiroky63d35e12016-06-16 10:08:15 -0700147
148 // union all multi-polygons / polygons into one
149 for (var i = data.features.length - 1; i >= 0; i--) {
Evan Siroky5669adc2016-07-07 17:25:31 -0700150 var curOsmGeom = data.features[i].geometry
Evan Siroky7891a6e2016-11-05 11:50:50 -0700151 if (curOsmGeom.type === 'Polygon' || curOsmGeom.type === 'MultiPolygon') {
evansiroky63d35e12016-06-16 10:08:15 -0700152 console.log('combining border')
Evan Siroky5669adc2016-07-07 17:25:31 -0700153 var curGeom = geoJsonToGeom(curOsmGeom)
Evan Siroky7891a6e2016-11-05 11:50:50 -0700154 if (!combined) {
evansiroky63d35e12016-06-16 10:08:15 -0700155 combined = curGeom
156 } else {
Evan Siroky5669adc2016-07-07 17:25:31 -0700157 combined = debugGeo('union', curGeom, combined)
evansiroky63d35e12016-06-16 10:08:15 -0700158 }
159 }
160 }
Evan Siroky5669adc2016-07-07 17:25:31 -0700161 fs.writeFile(boundaryFilename, geomToGeoJsonString(combined), cb)
evansiroky63d35e12016-06-16 10:08:15 -0700162 }]
163 }, boundaryCallback)
164}
evansirokyd401c892016-06-16 00:05:14 -0700165
Evan Siroky4fc596c2016-09-25 19:52:30 -0700166var getTzDistFilename = function (tzid) {
167 return './dist/' + tzid.replace(/\//g, '__') + '.json'
168}
169
170/**
171 * Get the geometry of the requested source data
172 *
173 * @return {Object} geom The geometry of the source
174 * @param {Object} source An object representing the data source
175 * must have `source` key and then either:
176 * - `id` if from a file
177 * - `id` if from a file
178 */
Evan Siroky7891a6e2016-11-05 11:50:50 -0700179var getDataSource = function (source) {
evansirokybecb56e2016-07-06 12:42:35 -0700180 var geoJson
Evan Siroky7891a6e2016-11-05 11:50:50 -0700181 if (source.source === 'overpass') {
evansirokybecb56e2016-07-06 12:42:35 -0700182 geoJson = require('./downloads/' + source.id + '.json')
Evan Siroky7891a6e2016-11-05 11:50:50 -0700183 } else if (source.source === 'manual-polygon') {
evansirokybecb56e2016-07-06 12:42:35 -0700184 geoJson = polygon(source.data).geometry
Evan Siroky7891a6e2016-11-05 11:50:50 -0700185 } else if (source.source === 'manual-multipolygon') {
Evan Siroky8e30a2e2016-08-06 19:55:35 -0700186 geoJson = multiPolygon(source.data).geometry
Evan Siroky7891a6e2016-11-05 11:50:50 -0700187 } else if (source.source === 'dist') {
Evan Siroky4fc596c2016-09-25 19:52:30 -0700188 geoJson = require(getTzDistFilename(source.id))
evansiroky4be1c7a2016-06-16 18:23:34 -0700189 } else {
190 var err = new Error('unknown source: ' + source.source)
191 throw err
192 }
Evan Siroky5669adc2016-07-07 17:25:31 -0700193 return geoJsonToGeom(geoJson)
evansiroky4be1c7a2016-06-16 18:23:34 -0700194}
195
Evan Siroky7891a6e2016-11-05 11:50:50 -0700196var makeTimezoneBoundary = function (tzid, callback) {
evansiroky35f64342016-06-16 22:17:04 -0700197 console.log('makeTimezoneBoundary for', tzid)
198
Evan Siroky7891a6e2016-11-05 11:50:50 -0700199 var ops = zoneCfg[tzid]
200 var geom
evansiroky4be1c7a2016-06-16 18:23:34 -0700201
Evan Siroky7891a6e2016-11-05 11:50:50 -0700202 asynclib.eachSeries(ops, function (task, cb) {
evansiroky4be1c7a2016-06-16 18:23:34 -0700203 var taskData = getDataSource(task)
evansiroky6f9d8f72016-06-21 16:27:54 -0700204 console.log('-', task.op, task.id)
Evan Siroky7891a6e2016-11-05 11:50:50 -0700205 if (task.op === 'init') {
evansiroky4be1c7a2016-06-16 18:23:34 -0700206 geom = taskData
Evan Siroky7891a6e2016-11-05 11:50:50 -0700207 } else if (task.op === 'intersect') {
evansiroky6f9d8f72016-06-21 16:27:54 -0700208 geom = debugGeo('intersection', geom, taskData)
Evan Siroky7891a6e2016-11-05 11:50:50 -0700209 } else if (task.op === 'difference') {
evansiroky6f9d8f72016-06-21 16:27:54 -0700210 geom = debugGeo('diff', geom, taskData)
Evan Siroky7891a6e2016-11-05 11:50:50 -0700211 } else if (task.op === 'difference-reverse-order') {
Evan Siroky8ccaf0b2016-09-03 11:36:13 -0700212 geom = debugGeo('diff', taskData, geom)
Evan Siroky7891a6e2016-11-05 11:50:50 -0700213 } else if (task.op === 'union') {
evansiroky6f9d8f72016-06-21 16:27:54 -0700214 geom = debugGeo('union', geom, taskData)
Evan Siroky8ccaf0b2016-09-03 11:36:13 -0700215 } else {
216 var err = new Error('unknown op: ' + task.op)
217 return cb(err)
evansiroky4be1c7a2016-06-16 18:23:34 -0700218 }
evansiroky35f64342016-06-16 22:17:04 -0700219 cb()
Evan Siroky4fc596c2016-09-25 19:52:30 -0700220 },
Evan Siroky7891a6e2016-11-05 11:50:50 -0700221 function (err) {
222 if (err) { return callback(err) }
Evan Siroky4fc596c2016-09-25 19:52:30 -0700223 fs.writeFile(getTzDistFilename(tzid),
224 geomToGeoJsonString(geom),
evansirokybecb56e2016-07-06 12:42:35 -0700225 callback)
evansiroky4be1c7a2016-06-16 18:23:34 -0700226 })
227}
228
Evan Siroky4fc596c2016-09-25 19:52:30 -0700229var loadDistZonesIntoMemory = function () {
230 console.log('load zones into memory')
Evan Siroky7891a6e2016-11-05 11:50:50 -0700231 var zones = Object.keys(zoneCfg)
232 var tzid
Evan Siroky4fc596c2016-09-25 19:52:30 -0700233
234 for (var i = 0; i < zones.length; i++) {
235 tzid = zones[i]
236 distZones[tzid] = getDataSource({ source: 'dist', id: tzid })
237 }
238}
239
240var getDistZoneGeom = function (tzid) {
241 return distZones[tzid]
242}
243
244var validateTimezoneBoundaries = function () {
245 console.log('do validation')
Evan Siroky7891a6e2016-11-05 11:50:50 -0700246 var allZonesOk = true
247 var zones = Object.keys(zoneCfg)
248 var compareTzid, tzid, zoneGeom
Evan Siroky4fc596c2016-09-25 19:52:30 -0700249
250 for (var i = 0; i < zones.length; i++) {
251 tzid = zones[i]
252 zoneGeom = getDistZoneGeom(tzid)
253
254 for (var j = i + 1; j < zones.length; j++) {
255 compareTzid = zones[j]
256
257 var compareZoneGeom = getDistZoneGeom(compareTzid)
Evan Siroky7891a6e2016-11-05 11:50:50 -0700258 if (zoneGeom.intersects(compareZoneGeom)) {
259 var intersectedGeom = debugGeo('intersection', zoneGeom, compareZoneGeom)
260 var intersectedArea = intersectedGeom.getArea()
Evan Siroky4fc596c2016-09-25 19:52:30 -0700261
Evan Siroky7891a6e2016-11-05 11:50:50 -0700262 if (intersectedArea > 0.0001) {
Evan Siroky4fc596c2016-09-25 19:52:30 -0700263 console.log('Validation error: ' + tzid + ' intersects ' + compareTzid + ' area: ' + intersectedArea)
264 allZonesOk = false
265 }
266 }
267 }
268 }
269
270 return allZonesOk ? null : 'Zone validation unsuccessful'
Evan Siroky4fc596c2016-09-25 19:52:30 -0700271}
272
Evan Siroky7891a6e2016-11-05 11:50:50 -0700273var combineAndWriteZones = function (callback) {
Evan Siroky8b47abe2016-10-02 12:28:52 -0700274 var stream = fs.createWriteStream('./dist/combined.json')
275 var zones = Object.keys(zoneCfg)
276
277 stream.write('{"type":"FeatureCollection","features":[')
278
279 for (var i = 0; i < zones.length; i++) {
Evan Siroky7891a6e2016-11-05 11:50:50 -0700280 if (i > 0) {
Evan Siroky8b47abe2016-10-02 12:28:52 -0700281 stream.write(',')
282 }
283 var feature = {
284 type: 'Feature',
285 properties: { tzid: zones[i] },
286 geometry: geomToGeoJson(getDistZoneGeom(zones[i]))
287 }
288 stream.write(JSON.stringify(feature))
289 }
290 stream.end(']}', callback)
291}
292
Evan Siroky7891a6e2016-11-05 11:50:50 -0700293asynclib.auto({
294 makeDownloadsDir: function (cb) {
evansirokyd401c892016-06-16 00:05:14 -0700295 console.log('creating downloads dir')
evansiroky4be1c7a2016-06-16 18:23:34 -0700296 safeMkdir('./downloads', cb)
297 },
Evan Siroky7891a6e2016-11-05 11:50:50 -0700298 makeDistDir: function (cb) {
evansiroky4be1c7a2016-06-16 18:23:34 -0700299 console.log('createing dist dir')
300 safeMkdir('./dist', cb)
evansirokyd401c892016-06-16 00:05:14 -0700301 },
Evan Siroky7891a6e2016-11-05 11:50:50 -0700302 getOsmBoundaries: ['makeDownloadsDir', function (results, cb) {
evansirokyd401c892016-06-16 00:05:14 -0700303 console.log('downloading osm boundaries')
Evan Siroky7891a6e2016-11-05 11:50:50 -0700304 asynclib.eachSeries(Object.keys(osmBoundarySources), downloadOsmBoundary, cb)
evansiroky63d35e12016-06-16 10:08:15 -0700305 }],
Evan Siroky7891a6e2016-11-05 11:50:50 -0700306 createZones: ['makeDistDir', 'getOsmBoundaries', function (results, cb) {
evansiroky35f64342016-06-16 22:17:04 -0700307 console.log('createZones')
Evan Siroky7891a6e2016-11-05 11:50:50 -0700308 asynclib.each(Object.keys(zoneCfg), makeTimezoneBoundary, cb)
evansiroky50216c62016-06-16 17:41:47 -0700309 }],
Evan Siroky7891a6e2016-11-05 11:50:50 -0700310 validateZones: ['createZones', function (results, cb) {
Evan Siroky4fc596c2016-09-25 19:52:30 -0700311 console.log('validating zones')
312 loadDistZonesIntoMemory()
313 cb(validateTimezoneBoundaries())
314 }],
Evan Siroky7891a6e2016-11-05 11:50:50 -0700315 mergeZones: ['validateZones', function (results, cb) {
Evan Siroky8b47abe2016-10-02 12:28:52 -0700316 console.log('merge zones')
317 combineAndWriteZones(cb)
318 }],
319 zipGeoJson: ['mergeZones', function (results, cb) {
320 console.log('zip geojson')
321 exec('zip dist/timezones.geojson.zip dist/combined.json', cb)
322 }],
323 makeShapefile: ['mergeZones', function (results, cb) {
324 console.log('convert from geojson to shapefile')
Evan Sirokyb57a5b92016-11-07 10:22:34 -0800325 rimraf.sync('dist/dist')
326 rimraf.sync('dist/combined_shapefile.*')
Evan Siroky8b47abe2016-10-02 12:28:52 -0700327 exec('ogr2ogr -nlt MULTIPOLYGON dist/combined_shapefile.shp dist/combined.json OGRGeoJSON', function (err, stdout, stderr) {
Evan Siroky7891a6e2016-11-05 11:50:50 -0700328 if (err) { return cb(err) }
Evan Siroky8b47abe2016-10-02 12:28:52 -0700329 exec('zip dist/timezones.shapefile.zip dist/combined_shapefile.*', cb)
330 })
evansirokyd401c892016-06-16 00:05:14 -0700331 }]
Evan Siroky7891a6e2016-11-05 11:50:50 -0700332}, function (err, results) {
evansirokyd401c892016-06-16 00:05:14 -0700333 console.log('done')
Evan Siroky7891a6e2016-11-05 11:50:50 -0700334 if (err) {
evansirokyd401c892016-06-16 00:05:14 -0700335 console.log('error!', err)
336 return
337 }
Evan Siroky4fc596c2016-09-25 19:52:30 -0700338})