blob: e85cf04ca760e640b16133adcceff4f1c27ec52f [file] [log] [blame]
Evan Siroky8b47abe2016-10-02 12:28:52 -07001var exec = require('child_process').exec,
2 fs = require('fs'),
evansirokyd401c892016-06-16 00:05:14 -07003 http = require('http')
4
5var async = require('async'),
evansiroky63d35e12016-06-16 10:08:15 -07006 jsts = require('jsts'),
Evan Siroky8e30a2e2016-08-06 19:55:35 -07007 multiPolygon = require('turf-multipolygon'),
evansirokyd401c892016-06-16 00:05:14 -07008 overpass = require('query-overpass'),
evansiroky4be1c7a2016-06-16 18:23:34 -07009 polygon = require('turf-polygon'),
evansirokyd401c892016-06-16 00:05:14 -070010 shp = require('shpjs')
11
12
evansiroky63d35e12016-06-16 10:08:15 -070013var osmBoundarySources = require('./osmBoundarySources.json'),
evansiroky50216c62016-06-16 17:41:47 -070014 zoneCfg = require('./timezones.json'),
evansiroky63d35e12016-06-16 10:08:15 -070015 geoJsonReader = new jsts.io.GeoJSONReader(),
evansiroky4be1c7a2016-06-16 18:23:34 -070016 geoJsonWriter = new jsts.io.GeoJSONWriter(),
Evan Siroky4fc596c2016-09-25 19:52:30 -070017 distZones = {}
evansiroky63d35e12016-06-16 10:08:15 -070018
evansiroky4be1c7a2016-06-16 18:23:34 -070019var safeMkdir = function(dirname, callback) {
20 fs.mkdir(dirname, function(err) {
21 if(err && err.code === 'EEXIST') {
22 callback()
23 } else {
24 callback(err)
25 }
26 })
27}
28
evansiroky6f9d8f72016-06-21 16:27:54 -070029debugGeo = function(op, a, b) {
evansirokybecb56e2016-07-06 12:42:35 -070030
31 var result
32
evansiroky6f9d8f72016-06-21 16:27:54 -070033 try {
34 switch(op) {
35 case 'union':
evansirokybecb56e2016-07-06 12:42:35 -070036 result = a.union(b)
evansiroky6f9d8f72016-06-21 16:27:54 -070037 break
38 case 'intersection':
evansirokybecb56e2016-07-06 12:42:35 -070039 result = a.intersection(b)
evansiroky6f9d8f72016-06-21 16:27:54 -070040 break
41 case 'diff':
evansirokybecb56e2016-07-06 12:42:35 -070042 try {
43 result = a.difference(b)
44 } catch(e) {
45 if(e.name === 'TopologyException') {
46 console.log('retry with GeometryPrecisionReducer')
Evan Siroky783532d2016-07-07 16:44:01 -070047 var precisionModel = new jsts.geom.PrecisionModel(10000),
evansirokybecb56e2016-07-06 12:42:35 -070048 precisionReducer = new jsts.precision.GeometryPrecisionReducer(precisionModel)
49
50 a = precisionReducer.reduce(a)
51 b = precisionReducer.reduce(b)
52
53 result = a.difference(b)
54 } else {
55 throw e
56 }
57 }
evansiroky6f9d8f72016-06-21 16:27:54 -070058 break
59 default:
60 var err = new Error('invalid op: ' + op)
61 throw err
62 }
63 } catch(e) {
64 console.log('op err')
evansirokybecb56e2016-07-06 12:42:35 -070065 console.log(e)
66 console.log(e.stack)
67 fs.writeFileSync('debug_' + op + '_a.json', JSON.stringify(geoJsonWriter.write(a)))
68 fs.writeFileSync('debug_' + op + '_b.json', JSON.stringify(geoJsonWriter.write(b)))
evansiroky6f9d8f72016-06-21 16:27:54 -070069 throw e
70 }
evansiroky6f9d8f72016-06-21 16:27:54 -070071
evansirokybecb56e2016-07-06 12:42:35 -070072 return result
evansiroky4be1c7a2016-06-16 18:23:34 -070073}
74
evansiroky50216c62016-06-16 17:41:47 -070075var fetchIfNeeded = function(file, superCallback, fetchFn) {
76 fs.stat(file, function(err) {
77 if(!err) { return superCallback() }
78 fetchFn()
79 })
80}
81
Evan Siroky5669adc2016-07-07 17:25:31 -070082var geoJsonToGeom = function(geoJson) {
83 return geoJsonReader.read(JSON.stringify(geoJson))
84}
85
Evan Siroky8b47abe2016-10-02 12:28:52 -070086var geomToGeoJson = function (geom) {
87 return geoJsonWriter.write(geom)
88}
89
Evan Siroky5669adc2016-07-07 17:25:31 -070090var geomToGeoJsonString = function(geom) {
91 return JSON.stringify(geoJsonWriter.write(geom))
92}
93
evansiroky63d35e12016-06-16 10:08:15 -070094var downloadOsmBoundary = function(boundaryId, boundaryCallback) {
95 var cfg = osmBoundarySources[boundaryId],
Evan Siroky5669adc2016-07-07 17:25:31 -070096 query = '[out:json][timeout:60];(relation',
97 boundaryFilename = './downloads/' + boundaryId + '.json',
98 debug = 'getting data for ' + boundaryId,
99 queryKeys = Object.keys(cfg)
evansiroky63d35e12016-06-16 10:08:15 -0700100
Evan Siroky5669adc2016-07-07 17:25:31 -0700101 for (var i = queryKeys.length - 1; i >= 0; i--) {
102 var k = queryKeys[i],
103 v = cfg[k]
104
105 query += '["' + k + '"="' + v + '"]'
106
evansiroky63d35e12016-06-16 10:08:15 -0700107 }
108
Evan Siroky5669adc2016-07-07 17:25:31 -0700109 query += ');out body;>;out meta qt;'
evansiroky4be1c7a2016-06-16 18:23:34 -0700110
evansiroky63d35e12016-06-16 10:08:15 -0700111 console.log(debug)
112
113 async.auto({
evansiroky5d008132016-06-17 08:37:51 -0700114 downloadFromOverpass: function(cb) {
evansiroky50216c62016-06-16 17:41:47 -0700115 console.log('downloading from overpass')
evansiroky4be1c7a2016-06-16 18:23:34 -0700116 fetchIfNeeded(boundaryFilename, boundaryCallback, function() {
evansiroky50216c62016-06-16 17:41:47 -0700117 overpass(query, cb, { flatProperties: true })
evansiroky63d35e12016-06-16 10:08:15 -0700118 })
119 },
evansiroky63d35e12016-06-16 10:08:15 -0700120 validateOverpassResult: ['downloadFromOverpass', function(results, cb) {
121 var data = results.downloadFromOverpass
122 if(!data.features || data.features.length == 0) {
123 err = new Error('Invalid geojson for boundary: ' + boundaryId)
124 return cb(err)
125 }
126 cb()
127 }],
128 saveSingleMultiPolygon: ['validateOverpassResult', function(results, cb) {
129 var data = results.downloadFromOverpass,
130 combined
131
132 // union all multi-polygons / polygons into one
133 for (var i = data.features.length - 1; i >= 0; i--) {
Evan Siroky5669adc2016-07-07 17:25:31 -0700134 var curOsmGeom = data.features[i].geometry
135 if(curOsmGeom.type === 'Polygon' || curOsmGeom.type === 'MultiPolygon') {
evansiroky63d35e12016-06-16 10:08:15 -0700136 console.log('combining border')
Evan Siroky5669adc2016-07-07 17:25:31 -0700137 var curGeom = geoJsonToGeom(curOsmGeom)
evansiroky63d35e12016-06-16 10:08:15 -0700138 if(!combined) {
139 combined = curGeom
140 } else {
Evan Siroky5669adc2016-07-07 17:25:31 -0700141 combined = debugGeo('union', curGeom, combined)
evansiroky63d35e12016-06-16 10:08:15 -0700142 }
143 }
144 }
Evan Siroky5669adc2016-07-07 17:25:31 -0700145 fs.writeFile(boundaryFilename, geomToGeoJsonString(combined), cb)
evansiroky63d35e12016-06-16 10:08:15 -0700146 }]
147 }, boundaryCallback)
148}
evansirokyd401c892016-06-16 00:05:14 -0700149
Evan Siroky4fc596c2016-09-25 19:52:30 -0700150var getTzDistFilename = function (tzid) {
151 return './dist/' + tzid.replace(/\//g, '__') + '.json'
152}
153
154/**
155 * Get the geometry of the requested source data
156 *
157 * @return {Object} geom The geometry of the source
158 * @param {Object} source An object representing the data source
159 * must have `source` key and then either:
160 * - `id` if from a file
161 * - `id` if from a file
162 */
evansiroky4be1c7a2016-06-16 18:23:34 -0700163var getDataSource = function(source) {
evansirokybecb56e2016-07-06 12:42:35 -0700164 var geoJson
Evan Siroky4fc596c2016-09-25 19:52:30 -0700165 if(source.source === 'overpass') {
evansirokybecb56e2016-07-06 12:42:35 -0700166 geoJson = require('./downloads/' + source.id + '.json')
evansiroky35f64342016-06-16 22:17:04 -0700167 } else if(source.source === 'manual-polygon') {
evansirokybecb56e2016-07-06 12:42:35 -0700168 geoJson = polygon(source.data).geometry
Evan Siroky8e30a2e2016-08-06 19:55:35 -0700169 } else if(source.source === 'manual-multipolygon') {
170 geoJson = multiPolygon(source.data).geometry
Evan Siroky4fc596c2016-09-25 19:52:30 -0700171 } else if(source.source === 'dist') {
172 geoJson = require(getTzDistFilename(source.id))
evansiroky4be1c7a2016-06-16 18:23:34 -0700173 } else {
174 var err = new Error('unknown source: ' + source.source)
175 throw err
176 }
Evan Siroky5669adc2016-07-07 17:25:31 -0700177 return geoJsonToGeom(geoJson)
evansiroky4be1c7a2016-06-16 18:23:34 -0700178}
179
180var makeTimezoneBoundary = function(tzid, callback) {
evansiroky35f64342016-06-16 22:17:04 -0700181 console.log('makeTimezoneBoundary for', tzid)
182
evansiroky4be1c7a2016-06-16 18:23:34 -0700183 var ops = zoneCfg[tzid],
184 geom
185
186 async.eachSeries(ops, function(task, cb) {
187 var taskData = getDataSource(task)
evansiroky6f9d8f72016-06-21 16:27:54 -0700188 console.log('-', task.op, task.id)
evansiroky4be1c7a2016-06-16 18:23:34 -0700189 if(task.op === 'init') {
190 geom = taskData
191 } else if(task.op === 'intersect') {
evansiroky6f9d8f72016-06-21 16:27:54 -0700192 geom = debugGeo('intersection', geom, taskData)
evansiroky4be1c7a2016-06-16 18:23:34 -0700193 } else if(task.op === 'difference') {
evansiroky6f9d8f72016-06-21 16:27:54 -0700194 geom = debugGeo('diff', geom, taskData)
Evan Siroky8ccaf0b2016-09-03 11:36:13 -0700195 } else if(task.op === 'difference-reverse-order') {
196 geom = debugGeo('diff', taskData, geom)
evansiroky6e45be62016-06-17 08:46:28 -0700197 } else if(task.op === 'union') {
evansiroky6f9d8f72016-06-21 16:27:54 -0700198 geom = debugGeo('union', geom, taskData)
Evan Siroky8ccaf0b2016-09-03 11:36:13 -0700199 } else {
200 var err = new Error('unknown op: ' + task.op)
201 return cb(err)
evansiroky4be1c7a2016-06-16 18:23:34 -0700202 }
evansiroky35f64342016-06-16 22:17:04 -0700203 cb()
Evan Siroky4fc596c2016-09-25 19:52:30 -0700204 },
205 function(err) {
evansiroky4be1c7a2016-06-16 18:23:34 -0700206 if(err) { return callback(err) }
Evan Siroky4fc596c2016-09-25 19:52:30 -0700207 fs.writeFile(getTzDistFilename(tzid),
208 geomToGeoJsonString(geom),
evansirokybecb56e2016-07-06 12:42:35 -0700209 callback)
evansiroky4be1c7a2016-06-16 18:23:34 -0700210 })
211}
212
Evan Siroky4fc596c2016-09-25 19:52:30 -0700213var loadDistZonesIntoMemory = function () {
214 console.log('load zones into memory')
215 var zones = Object.keys(zoneCfg),
216 tzid
217
218 for (var i = 0; i < zones.length; i++) {
219 tzid = zones[i]
220 distZones[tzid] = getDataSource({ source: 'dist', id: tzid })
221 }
222}
223
224var getDistZoneGeom = function (tzid) {
225 return distZones[tzid]
226}
227
228var validateTimezoneBoundaries = function () {
229 console.log('do validation')
230 var allZonesOk = true,
231 zones = Object.keys(zoneCfg),
232 compareTzid, tzid, zoneGeom
233
234 for (var i = 0; i < zones.length; i++) {
235 tzid = zones[i]
236 zoneGeom = getDistZoneGeom(tzid)
237
238 for (var j = i + 1; j < zones.length; j++) {
239 compareTzid = zones[j]
240
241 var compareZoneGeom = getDistZoneGeom(compareTzid)
242 if(zoneGeom.intersects(compareZoneGeom)) {
243 var intersectedGeom = debugGeo('intersection', zoneGeom, compareZoneGeom),
244 intersectedArea = intersectedGeom.getArea()
245
246 if(intersectedArea > 0.0001) {
247 console.log('Validation error: ' + tzid + ' intersects ' + compareTzid + ' area: ' + intersectedArea)
248 allZonesOk = false
249 }
250 }
251 }
252 }
253
254 return allZonesOk ? null : 'Zone validation unsuccessful'
255
256}
257
Evan Siroky8b47abe2016-10-02 12:28:52 -0700258var combineAndWriteZones = function(callback) {
259 var stream = fs.createWriteStream('./dist/combined.json')
260 var zones = Object.keys(zoneCfg)
261
262 stream.write('{"type":"FeatureCollection","features":[')
263
264 for (var i = 0; i < zones.length; i++) {
265 if(i > 0) {
266 stream.write(',')
267 }
268 var feature = {
269 type: 'Feature',
270 properties: { tzid: zones[i] },
271 geometry: geomToGeoJson(getDistZoneGeom(zones[i]))
272 }
273 stream.write(JSON.stringify(feature))
274 }
275 stream.end(']}', callback)
276}
277
evansirokyd401c892016-06-16 00:05:14 -0700278async.auto({
279 makeDownloadsDir: function(cb) {
280 console.log('creating downloads dir')
evansiroky4be1c7a2016-06-16 18:23:34 -0700281 safeMkdir('./downloads', cb)
282 },
283 makeDistDir: function(cb) {
284 console.log('createing dist dir')
285 safeMkdir('./dist', cb)
evansirokyd401c892016-06-16 00:05:14 -0700286 },
evansirokyd401c892016-06-16 00:05:14 -0700287 getOsmBoundaries: ['makeDownloadsDir', function(results, cb) {
288 console.log('downloading osm boundaries')
evansiroky63d35e12016-06-16 10:08:15 -0700289 async.eachSeries(Object.keys(osmBoundarySources), downloadOsmBoundary, cb)
290 }],
Evan Siroky4fc596c2016-09-25 19:52:30 -0700291 createZones: ['makeDistDir', 'getOsmBoundaries', function(results, cb) {
evansiroky35f64342016-06-16 22:17:04 -0700292 console.log('createZones')
evansiroky50216c62016-06-16 17:41:47 -0700293 async.each(Object.keys(zoneCfg), makeTimezoneBoundary, cb)
294 }],
Evan Siroky4fc596c2016-09-25 19:52:30 -0700295 validateZones: ['createZones', function(results, cb) {
296 console.log('validating zones')
297 loadDistZonesIntoMemory()
298 cb(validateTimezoneBoundaries())
299 }],
300 mergeZones: ['validateZones', function(results, cb) {
Evan Siroky8b47abe2016-10-02 12:28:52 -0700301 console.log('merge zones')
302 combineAndWriteZones(cb)
303 }],
304 zipGeoJson: ['mergeZones', function (results, cb) {
305 console.log('zip geojson')
306 exec('zip dist/timezones.geojson.zip dist/combined.json', cb)
307 }],
308 makeShapefile: ['mergeZones', function (results, cb) {
309 console.log('convert from geojson to shapefile')
310 exec('ogr2ogr -nlt MULTIPOLYGON dist/combined_shapefile.shp dist/combined.json OGRGeoJSON', function (err, stdout, stderr) {
311 if(err) { return cb(err) }
312 exec('zip dist/timezones.shapefile.zip dist/combined_shapefile.*', cb)
313 })
evansirokyd401c892016-06-16 00:05:14 -0700314 }]
evansiroky50216c62016-06-16 17:41:47 -0700315}, function(err, results) {
evansirokyd401c892016-06-16 00:05:14 -0700316 console.log('done')
317 if(err) {
318 console.log('error!', err)
319 return
320 }
Evan Siroky4fc596c2016-09-25 19:52:30 -0700321})