blob: ff74a96516c59488c4af3433cd571ce67a41035d [file] [log] [blame]
evansirokyd401c892016-06-16 00:05:14 -07001var fs = require('fs'),
2 http = require('http')
3
4var async = require('async'),
evansiroky63d35e12016-06-16 10:08:15 -07005 jsts = require('jsts'),
Evan Siroky8e30a2e2016-08-06 19:55:35 -07006 multiPolygon = require('turf-multipolygon'),
evansirokyd401c892016-06-16 00:05:14 -07007 overpass = require('query-overpass'),
evansiroky4be1c7a2016-06-16 18:23:34 -07008 polygon = require('turf-polygon'),
evansirokyd401c892016-06-16 00:05:14 -07009 shp = require('shpjs')
10
11
evansiroky63d35e12016-06-16 10:08:15 -070012var osmBoundarySources = require('./osmBoundarySources.json'),
evansiroky50216c62016-06-16 17:41:47 -070013 zoneCfg = require('./timezones.json'),
evansiroky63d35e12016-06-16 10:08:15 -070014 geoJsonReader = new jsts.io.GeoJSONReader(),
evansiroky4be1c7a2016-06-16 18:23:34 -070015 geoJsonWriter = new jsts.io.GeoJSONWriter(),
Evan Siroky4fc596c2016-09-25 19:52:30 -070016 distZones = {}
evansiroky63d35e12016-06-16 10:08:15 -070017
evansiroky4be1c7a2016-06-16 18:23:34 -070018var safeMkdir = function(dirname, callback) {
19 fs.mkdir(dirname, function(err) {
20 if(err && err.code === 'EEXIST') {
21 callback()
22 } else {
23 callback(err)
24 }
25 })
26}
27
evansiroky6f9d8f72016-06-21 16:27:54 -070028debugGeo = function(op, a, b) {
evansirokybecb56e2016-07-06 12:42:35 -070029
30 var result
31
evansiroky6f9d8f72016-06-21 16:27:54 -070032 try {
33 switch(op) {
34 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)
43 } catch(e) {
44 if(e.name === 'TopologyException') {
45 console.log('retry with GeometryPrecisionReducer')
Evan Siroky783532d2016-07-07 16:44:01 -070046 var precisionModel = new jsts.geom.PrecisionModel(10000),
evansirokybecb56e2016-07-06 12:42:35 -070047 precisionReducer = new jsts.precision.GeometryPrecisionReducer(precisionModel)
48
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 }
62 } catch(e) {
63 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
evansiroky50216c62016-06-16 17:41:47 -070074var fetchIfNeeded = function(file, superCallback, fetchFn) {
75 fs.stat(file, function(err) {
76 if(!err) { return superCallback() }
77 fetchFn()
78 })
79}
80
Evan Siroky5669adc2016-07-07 17:25:31 -070081var geoJsonToGeom = function(geoJson) {
82 return geoJsonReader.read(JSON.stringify(geoJson))
83}
84
85var geomToGeoJsonString = function(geom) {
86 return JSON.stringify(geoJsonWriter.write(geom))
87}
88
evansiroky63d35e12016-06-16 10:08:15 -070089var downloadOsmBoundary = function(boundaryId, boundaryCallback) {
90 var cfg = osmBoundarySources[boundaryId],
Evan Siroky5669adc2016-07-07 17:25:31 -070091 query = '[out:json][timeout:60];(relation',
92 boundaryFilename = './downloads/' + boundaryId + '.json',
93 debug = 'getting data for ' + boundaryId,
94 queryKeys = Object.keys(cfg)
evansiroky63d35e12016-06-16 10:08:15 -070095
Evan Siroky5669adc2016-07-07 17:25:31 -070096 for (var i = queryKeys.length - 1; i >= 0; i--) {
97 var k = queryKeys[i],
98 v = cfg[k]
99
100 query += '["' + k + '"="' + v + '"]'
101
evansiroky63d35e12016-06-16 10:08:15 -0700102 }
103
Evan Siroky5669adc2016-07-07 17:25:31 -0700104 query += ');out body;>;out meta qt;'
evansiroky4be1c7a2016-06-16 18:23:34 -0700105
evansiroky63d35e12016-06-16 10:08:15 -0700106 console.log(debug)
107
108 async.auto({
evansiroky5d008132016-06-17 08:37:51 -0700109 downloadFromOverpass: function(cb) {
evansiroky50216c62016-06-16 17:41:47 -0700110 console.log('downloading from overpass')
evansiroky4be1c7a2016-06-16 18:23:34 -0700111 fetchIfNeeded(boundaryFilename, boundaryCallback, function() {
evansiroky50216c62016-06-16 17:41:47 -0700112 overpass(query, cb, { flatProperties: true })
evansiroky63d35e12016-06-16 10:08:15 -0700113 })
114 },
evansiroky63d35e12016-06-16 10:08:15 -0700115 validateOverpassResult: ['downloadFromOverpass', function(results, cb) {
116 var data = results.downloadFromOverpass
117 if(!data.features || data.features.length == 0) {
118 err = new Error('Invalid geojson for boundary: ' + boundaryId)
119 return cb(err)
120 }
121 cb()
122 }],
123 saveSingleMultiPolygon: ['validateOverpassResult', function(results, cb) {
124 var data = results.downloadFromOverpass,
125 combined
126
127 // union all multi-polygons / polygons into one
128 for (var i = data.features.length - 1; i >= 0; i--) {
Evan Siroky5669adc2016-07-07 17:25:31 -0700129 var curOsmGeom = data.features[i].geometry
130 if(curOsmGeom.type === 'Polygon' || curOsmGeom.type === 'MultiPolygon') {
evansiroky63d35e12016-06-16 10:08:15 -0700131 console.log('combining border')
Evan Siroky5669adc2016-07-07 17:25:31 -0700132 var curGeom = geoJsonToGeom(curOsmGeom)
evansiroky63d35e12016-06-16 10:08:15 -0700133 if(!combined) {
134 combined = curGeom
135 } else {
Evan Siroky5669adc2016-07-07 17:25:31 -0700136 combined = debugGeo('union', curGeom, combined)
evansiroky63d35e12016-06-16 10:08:15 -0700137 }
138 }
139 }
Evan Siroky5669adc2016-07-07 17:25:31 -0700140 fs.writeFile(boundaryFilename, geomToGeoJsonString(combined), cb)
evansiroky63d35e12016-06-16 10:08:15 -0700141 }]
142 }, boundaryCallback)
143}
evansirokyd401c892016-06-16 00:05:14 -0700144
Evan Siroky4fc596c2016-09-25 19:52:30 -0700145var getTzDistFilename = function (tzid) {
146 return './dist/' + tzid.replace(/\//g, '__') + '.json'
147}
148
149/**
150 * Get the geometry of the requested source data
151 *
152 * @return {Object} geom The geometry of the source
153 * @param {Object} source An object representing the data source
154 * must have `source` key and then either:
155 * - `id` if from a file
156 * - `id` if from a file
157 */
evansiroky4be1c7a2016-06-16 18:23:34 -0700158var getDataSource = function(source) {
evansirokybecb56e2016-07-06 12:42:35 -0700159 var geoJson
Evan Siroky4fc596c2016-09-25 19:52:30 -0700160 if(source.source === 'overpass') {
evansirokybecb56e2016-07-06 12:42:35 -0700161 geoJson = require('./downloads/' + source.id + '.json')
evansiroky35f64342016-06-16 22:17:04 -0700162 } else if(source.source === 'manual-polygon') {
evansirokybecb56e2016-07-06 12:42:35 -0700163 geoJson = polygon(source.data).geometry
Evan Siroky8e30a2e2016-08-06 19:55:35 -0700164 } else if(source.source === 'manual-multipolygon') {
165 geoJson = multiPolygon(source.data).geometry
Evan Siroky4fc596c2016-09-25 19:52:30 -0700166 } else if(source.source === 'dist') {
167 geoJson = require(getTzDistFilename(source.id))
evansiroky4be1c7a2016-06-16 18:23:34 -0700168 } else {
169 var err = new Error('unknown source: ' + source.source)
170 throw err
171 }
Evan Siroky5669adc2016-07-07 17:25:31 -0700172 return geoJsonToGeom(geoJson)
evansiroky4be1c7a2016-06-16 18:23:34 -0700173}
174
175var makeTimezoneBoundary = function(tzid, callback) {
evansiroky35f64342016-06-16 22:17:04 -0700176 console.log('makeTimezoneBoundary for', tzid)
177
evansiroky4be1c7a2016-06-16 18:23:34 -0700178 var ops = zoneCfg[tzid],
179 geom
180
181 async.eachSeries(ops, function(task, cb) {
182 var taskData = getDataSource(task)
evansiroky6f9d8f72016-06-21 16:27:54 -0700183 console.log('-', task.op, task.id)
evansiroky4be1c7a2016-06-16 18:23:34 -0700184 if(task.op === 'init') {
185 geom = taskData
186 } else if(task.op === 'intersect') {
evansiroky6f9d8f72016-06-21 16:27:54 -0700187 geom = debugGeo('intersection', geom, taskData)
evansiroky4be1c7a2016-06-16 18:23:34 -0700188 } else if(task.op === 'difference') {
evansiroky6f9d8f72016-06-21 16:27:54 -0700189 geom = debugGeo('diff', geom, taskData)
Evan Siroky8ccaf0b2016-09-03 11:36:13 -0700190 } else if(task.op === 'difference-reverse-order') {
191 geom = debugGeo('diff', taskData, geom)
evansiroky6e45be62016-06-17 08:46:28 -0700192 } else if(task.op === 'union') {
evansiroky6f9d8f72016-06-21 16:27:54 -0700193 geom = debugGeo('union', geom, taskData)
Evan Siroky8ccaf0b2016-09-03 11:36:13 -0700194 } else {
195 var err = new Error('unknown op: ' + task.op)
196 return cb(err)
evansiroky4be1c7a2016-06-16 18:23:34 -0700197 }
evansiroky35f64342016-06-16 22:17:04 -0700198 cb()
Evan Siroky4fc596c2016-09-25 19:52:30 -0700199 },
200 function(err) {
evansiroky4be1c7a2016-06-16 18:23:34 -0700201 if(err) { return callback(err) }
Evan Siroky4fc596c2016-09-25 19:52:30 -0700202 fs.writeFile(getTzDistFilename(tzid),
203 geomToGeoJsonString(geom),
evansirokybecb56e2016-07-06 12:42:35 -0700204 callback)
evansiroky4be1c7a2016-06-16 18:23:34 -0700205 })
206}
207
Evan Siroky4fc596c2016-09-25 19:52:30 -0700208var loadDistZonesIntoMemory = function () {
209 console.log('load zones into memory')
210 var zones = Object.keys(zoneCfg),
211 tzid
212
213 for (var i = 0; i < zones.length; i++) {
214 tzid = zones[i]
215 distZones[tzid] = getDataSource({ source: 'dist', id: tzid })
216 }
217}
218
219var getDistZoneGeom = function (tzid) {
220 return distZones[tzid]
221}
222
223var validateTimezoneBoundaries = function () {
224 console.log('do validation')
225 var allZonesOk = true,
226 zones = Object.keys(zoneCfg),
227 compareTzid, tzid, zoneGeom
228
229 for (var i = 0; i < zones.length; i++) {
230 tzid = zones[i]
231 zoneGeom = getDistZoneGeom(tzid)
232
233 for (var j = i + 1; j < zones.length; j++) {
234 compareTzid = zones[j]
235
236 var compareZoneGeom = getDistZoneGeom(compareTzid)
237 if(zoneGeom.intersects(compareZoneGeom)) {
238 var intersectedGeom = debugGeo('intersection', zoneGeom, compareZoneGeom),
239 intersectedArea = intersectedGeom.getArea()
240
241 if(intersectedArea > 0.0001) {
242 console.log('Validation error: ' + tzid + ' intersects ' + compareTzid + ' area: ' + intersectedArea)
243 allZonesOk = false
244 }
245 }
246 }
247 }
248
249 return allZonesOk ? null : 'Zone validation unsuccessful'
250
251}
252
evansirokyd401c892016-06-16 00:05:14 -0700253async.auto({
254 makeDownloadsDir: function(cb) {
255 console.log('creating downloads dir')
evansiroky4be1c7a2016-06-16 18:23:34 -0700256 safeMkdir('./downloads', cb)
257 },
258 makeDistDir: function(cb) {
259 console.log('createing dist dir')
260 safeMkdir('./dist', cb)
evansirokyd401c892016-06-16 00:05:14 -0700261 },
evansirokyd401c892016-06-16 00:05:14 -0700262 getOsmBoundaries: ['makeDownloadsDir', function(results, cb) {
263 console.log('downloading osm boundaries')
evansiroky63d35e12016-06-16 10:08:15 -0700264 async.eachSeries(Object.keys(osmBoundarySources), downloadOsmBoundary, cb)
265 }],
Evan Siroky4fc596c2016-09-25 19:52:30 -0700266 createZones: ['makeDistDir', 'getOsmBoundaries', function(results, cb) {
evansiroky35f64342016-06-16 22:17:04 -0700267 console.log('createZones')
evansiroky50216c62016-06-16 17:41:47 -0700268 async.each(Object.keys(zoneCfg), makeTimezoneBoundary, cb)
269 }],
Evan Siroky4fc596c2016-09-25 19:52:30 -0700270 validateZones: ['createZones', function(results, cb) {
271 console.log('validating zones')
272 loadDistZonesIntoMemory()
273 cb(validateTimezoneBoundaries())
274 }],
275 mergeZones: ['validateZones', function(results, cb) {
276 // TODO: merge zones into single geojson file
evansiroky4be1c7a2016-06-16 18:23:34 -0700277 cb()
evansirokyd401c892016-06-16 00:05:14 -0700278 }]
evansiroky50216c62016-06-16 17:41:47 -0700279}, function(err, results) {
evansirokyd401c892016-06-16 00:05:14 -0700280 console.log('done')
281 if(err) {
282 console.log('error!', err)
283 return
284 }
Evan Siroky4fc596c2016-09-25 19:52:30 -0700285})