blob: 36d424e94a814118498e2e38179330768498da9e [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'),
evansirokyd401c892016-06-16 00:05:14 -07006 overpass = require('query-overpass'),
evansiroky4be1c7a2016-06-16 18:23:34 -07007 polygon = require('turf-polygon'),
evansirokyd401c892016-06-16 00:05:14 -07008 shp = require('shpjs')
9
10
evansiroky63d35e12016-06-16 10:08:15 -070011var osmBoundarySources = require('./osmBoundarySources.json'),
evansiroky50216c62016-06-16 17:41:47 -070012 zoneCfg = require('./timezones.json'),
evansiroky63d35e12016-06-16 10:08:15 -070013 geoJsonReader = new jsts.io.GeoJSONReader(),
evansiroky4be1c7a2016-06-16 18:23:34 -070014 geoJsonWriter = new jsts.io.GeoJSONWriter(),
evansiroky35f64342016-06-16 22:17:04 -070015 efeleGeoms, efeleLookup = {}
evansiroky63d35e12016-06-16 10:08:15 -070016
evansiroky4be1c7a2016-06-16 18:23:34 -070017var safeMkdir = function(dirname, callback) {
18 fs.mkdir(dirname, function(err) {
19 if(err && err.code === 'EEXIST') {
20 callback()
21 } else {
22 callback(err)
23 }
24 })
25}
26
evansirokyd401c892016-06-16 00:05:14 -070027var toArrayBuffer = function(buffer) {
28 var ab = new ArrayBuffer(buffer.length)
29 var view = new Uint8Array(ab)
30 for (var i = 0; i < buffer.length; ++i) {
31 view[i] = buffer[i]
32 }
33 return view
34}
35
36var extractToGeoJson = function(callback) {
37 shp(toArrayBuffer(fs.readFileSync('./downloads/tz_world_mp.zip')))
evansiroky35f64342016-06-16 22:17:04 -070038 .then(function(geojson) { console.log('extract success'); return callback(null, geojson) })
evansirokyd401c892016-06-16 00:05:14 -070039 .catch(function(e){ console.log('extract err', e); callback(e) })
40}
41
evansiroky6f9d8f72016-06-21 16:27:54 -070042debugGeo = function(op, a, b) {
evansirokybecb56e2016-07-06 12:42:35 -070043
44 var result
45
evansiroky6f9d8f72016-06-21 16:27:54 -070046 try {
47 switch(op) {
48 case 'union':
evansirokybecb56e2016-07-06 12:42:35 -070049 result = a.union(b)
evansiroky6f9d8f72016-06-21 16:27:54 -070050 break
51 case 'intersection':
evansirokybecb56e2016-07-06 12:42:35 -070052 result = a.intersection(b)
evansiroky6f9d8f72016-06-21 16:27:54 -070053 break
54 case 'diff':
evansirokybecb56e2016-07-06 12:42:35 -070055 try {
56 result = a.difference(b)
57 } catch(e) {
58 if(e.name === 'TopologyException') {
59 console.log('retry with GeometryPrecisionReducer')
Evan Siroky783532d2016-07-07 16:44:01 -070060 var precisionModel = new jsts.geom.PrecisionModel(10000),
evansirokybecb56e2016-07-06 12:42:35 -070061 precisionReducer = new jsts.precision.GeometryPrecisionReducer(precisionModel)
62
63 a = precisionReducer.reduce(a)
64 b = precisionReducer.reduce(b)
65
66 result = a.difference(b)
67 } else {
68 throw e
69 }
70 }
evansiroky6f9d8f72016-06-21 16:27:54 -070071 break
72 default:
73 var err = new Error('invalid op: ' + op)
74 throw err
75 }
76 } catch(e) {
77 console.log('op err')
evansirokybecb56e2016-07-06 12:42:35 -070078 console.log(e)
79 console.log(e.stack)
80 fs.writeFileSync('debug_' + op + '_a.json', JSON.stringify(geoJsonWriter.write(a)))
81 fs.writeFileSync('debug_' + op + '_b.json', JSON.stringify(geoJsonWriter.write(b)))
evansiroky6f9d8f72016-06-21 16:27:54 -070082 throw e
83 }
evansiroky6f9d8f72016-06-21 16:27:54 -070084
evansirokybecb56e2016-07-06 12:42:35 -070085 return result
evansiroky4be1c7a2016-06-16 18:23:34 -070086}
87
evansiroky50216c62016-06-16 17:41:47 -070088var fetchIfNeeded = function(file, superCallback, fetchFn) {
89 fs.stat(file, function(err) {
90 if(!err) { return superCallback() }
91 fetchFn()
92 })
93}
94
Evan Siroky5669adc2016-07-07 17:25:31 -070095var geoJsonToGeom = function(geoJson) {
96 return geoJsonReader.read(JSON.stringify(geoJson))
97}
98
99var geomToGeoJsonString = function(geom) {
100 return JSON.stringify(geoJsonWriter.write(geom))
101}
102
evansiroky63d35e12016-06-16 10:08:15 -0700103var downloadOsmBoundary = function(boundaryId, boundaryCallback) {
104 var cfg = osmBoundarySources[boundaryId],
Evan Siroky5669adc2016-07-07 17:25:31 -0700105 query = '[out:json][timeout:60];(relation',
106 boundaryFilename = './downloads/' + boundaryId + '.json',
107 debug = 'getting data for ' + boundaryId,
108 queryKeys = Object.keys(cfg)
evansiroky63d35e12016-06-16 10:08:15 -0700109
Evan Siroky5669adc2016-07-07 17:25:31 -0700110 for (var i = queryKeys.length - 1; i >= 0; i--) {
111 var k = queryKeys[i],
112 v = cfg[k]
113
114 query += '["' + k + '"="' + v + '"]'
115
evansiroky63d35e12016-06-16 10:08:15 -0700116 }
117
Evan Siroky5669adc2016-07-07 17:25:31 -0700118 query += ');out body;>;out meta qt;'
evansiroky4be1c7a2016-06-16 18:23:34 -0700119
evansiroky63d35e12016-06-16 10:08:15 -0700120 console.log(debug)
121
122 async.auto({
evansiroky5d008132016-06-17 08:37:51 -0700123 downloadFromOverpass: function(cb) {
evansiroky50216c62016-06-16 17:41:47 -0700124 console.log('downloading from overpass')
evansiroky4be1c7a2016-06-16 18:23:34 -0700125 fetchIfNeeded(boundaryFilename, boundaryCallback, function() {
evansiroky50216c62016-06-16 17:41:47 -0700126 overpass(query, cb, { flatProperties: true })
evansiroky63d35e12016-06-16 10:08:15 -0700127 })
128 },
evansiroky63d35e12016-06-16 10:08:15 -0700129 validateOverpassResult: ['downloadFromOverpass', function(results, cb) {
130 var data = results.downloadFromOverpass
131 if(!data.features || data.features.length == 0) {
132 err = new Error('Invalid geojson for boundary: ' + boundaryId)
133 return cb(err)
134 }
135 cb()
136 }],
137 saveSingleMultiPolygon: ['validateOverpassResult', function(results, cb) {
138 var data = results.downloadFromOverpass,
139 combined
140
141 // union all multi-polygons / polygons into one
142 for (var i = data.features.length - 1; i >= 0; i--) {
Evan Siroky5669adc2016-07-07 17:25:31 -0700143 var curOsmGeom = data.features[i].geometry
144 if(curOsmGeom.type === 'Polygon' || curOsmGeom.type === 'MultiPolygon') {
evansiroky63d35e12016-06-16 10:08:15 -0700145 console.log('combining border')
Evan Siroky5669adc2016-07-07 17:25:31 -0700146 var curGeom = geoJsonToGeom(curOsmGeom)
evansiroky63d35e12016-06-16 10:08:15 -0700147 if(!combined) {
148 combined = curGeom
149 } else {
Evan Siroky5669adc2016-07-07 17:25:31 -0700150 combined = debugGeo('union', curGeom, combined)
evansiroky63d35e12016-06-16 10:08:15 -0700151 }
152 }
153 }
Evan Siroky5669adc2016-07-07 17:25:31 -0700154 fs.writeFile(boundaryFilename, geomToGeoJsonString(combined), cb)
evansiroky63d35e12016-06-16 10:08:15 -0700155 }]
156 }, boundaryCallback)
157}
evansirokyd401c892016-06-16 00:05:14 -0700158
evansiroky4be1c7a2016-06-16 18:23:34 -0700159var getDataSource = function(source) {
evansirokybecb56e2016-07-06 12:42:35 -0700160 var geoJson
evansiroky4be1c7a2016-06-16 18:23:34 -0700161 if(source.source === 'efele') {
evansirokybecb56e2016-07-06 12:42:35 -0700162 geoJson = efeleGeoms[efeleLookup[source.id]].geometry
evansiroky4be1c7a2016-06-16 18:23:34 -0700163 } else if(source.source === 'overpass') {
evansirokybecb56e2016-07-06 12:42:35 -0700164 geoJson = require('./downloads/' + source.id + '.json')
evansiroky35f64342016-06-16 22:17:04 -0700165 } else if(source.source === 'manual-polygon') {
evansirokybecb56e2016-07-06 12:42:35 -0700166 geoJson = polygon(source.data).geometry
evansiroky4be1c7a2016-06-16 18:23:34 -0700167 } else {
168 var err = new Error('unknown source: ' + source.source)
169 throw err
170 }
Evan Siroky5669adc2016-07-07 17:25:31 -0700171 return geoJsonToGeom(geoJson)
evansiroky4be1c7a2016-06-16 18:23:34 -0700172}
173
174var makeTimezoneBoundary = function(tzid, callback) {
evansiroky35f64342016-06-16 22:17:04 -0700175 console.log('makeTimezoneBoundary for', tzid)
176
evansiroky4be1c7a2016-06-16 18:23:34 -0700177 var ops = zoneCfg[tzid],
178 geom
179
180 async.eachSeries(ops, function(task, cb) {
181 var taskData = getDataSource(task)
evansiroky6f9d8f72016-06-21 16:27:54 -0700182 console.log('-', task.op, task.id)
evansiroky4be1c7a2016-06-16 18:23:34 -0700183 if(task.op === 'init') {
184 geom = taskData
185 } else if(task.op === 'intersect') {
evansiroky6f9d8f72016-06-21 16:27:54 -0700186 geom = debugGeo('intersection', geom, taskData)
evansiroky4be1c7a2016-06-16 18:23:34 -0700187 } else if(task.op === 'difference') {
evansiroky6f9d8f72016-06-21 16:27:54 -0700188 geom = debugGeo('diff', geom, taskData)
evansiroky6e45be62016-06-17 08:46:28 -0700189 } else if(task.op === 'union') {
evansiroky6f9d8f72016-06-21 16:27:54 -0700190 geom = debugGeo('union', geom, taskData)
evansiroky4be1c7a2016-06-16 18:23:34 -0700191 }
evansiroky35f64342016-06-16 22:17:04 -0700192 cb()
evansiroky4be1c7a2016-06-16 18:23:34 -0700193 }, function(err) {
194 if(err) { return callback(err) }
evansirokybecb56e2016-07-06 12:42:35 -0700195 fs.writeFile('./dist/' + tzid.replace(/\//g, '__') + '.json',
Evan Siroky5669adc2016-07-07 17:25:31 -0700196 geomToGeoJsonString(geom),
evansirokybecb56e2016-07-06 12:42:35 -0700197 callback)
evansiroky4be1c7a2016-06-16 18:23:34 -0700198 })
199}
200
evansirokyd401c892016-06-16 00:05:14 -0700201async.auto({
202 makeDownloadsDir: function(cb) {
203 console.log('creating downloads dir')
evansiroky4be1c7a2016-06-16 18:23:34 -0700204 safeMkdir('./downloads', cb)
205 },
206 makeDistDir: function(cb) {
207 console.log('createing dist dir')
208 safeMkdir('./dist', cb)
evansirokyd401c892016-06-16 00:05:14 -0700209 },
210 getEfeleShapefile: ['makeDownloadsDir', function(results, cb) {
evansiroky50216c62016-06-16 17:41:47 -0700211 console.log('download efele.net shapefile')
212 var efeleFilename = './downloads/tz_world_mp.zip'
213 fetchIfNeeded(efeleFilename, cb, function() {
214 var file = fs.createWriteStream(efeleFilename)
215 http.get('http://efele.net/maps/tz/world/tz_world_mp.zip', function(response) {
216 response.pipe(file)
217 file
218 .on('finish', function() {
219 file.close(cb)
220 })
221 .on('error', cb)
222 })
evansirokyd401c892016-06-16 00:05:14 -0700223 })
224 }],
evansirokyd401c892016-06-16 00:05:14 -0700225 getOsmBoundaries: ['makeDownloadsDir', function(results, cb) {
226 console.log('downloading osm boundaries')
evansiroky63d35e12016-06-16 10:08:15 -0700227 async.eachSeries(Object.keys(osmBoundarySources), downloadOsmBoundary, cb)
228 }],
229 extractEfeleNetShapefile: ['getOsmBoundaries', function(results, cb) {
evansiroky63d35e12016-06-16 10:08:15 -0700230 console.log('extracting efele.net shapefile')
231 extractToGeoJson(cb)
evansiroky50216c62016-06-16 17:41:47 -0700232 }],
233 dictifyEfeleNetData: ['extractEfeleNetShapefile', function(results, cb) {
evansiroky35f64342016-06-16 22:17:04 -0700234 console.log('dictify efele.net')
evansiroky4be1c7a2016-06-16 18:23:34 -0700235 efeleGeoms = results.extractEfeleNetShapefile.features
236 for (var i = efeleGeoms.length - 1; i >= 0; i--) {
237 var curTz = efeleGeoms[i]
238 efeleLookup[curTz.properties.TZID] = i
evansiroky50216c62016-06-16 17:41:47 -0700239 }
evansiroky4be1c7a2016-06-16 18:23:34 -0700240 cb()
evansiroky50216c62016-06-16 17:41:47 -0700241 }],
evansiroky4be1c7a2016-06-16 18:23:34 -0700242 createZones: ['makeDistDir', 'dictifyEfeleNetData', function(results, cb) {
evansiroky35f64342016-06-16 22:17:04 -0700243 console.log('createZones')
evansiroky50216c62016-06-16 17:41:47 -0700244 async.each(Object.keys(zoneCfg), makeTimezoneBoundary, cb)
245 }],
246 mergeZones: ['createZones', function(results, cb) {
evansiroky4be1c7a2016-06-16 18:23:34 -0700247 cb()
evansirokyd401c892016-06-16 00:05:14 -0700248 }]
evansiroky50216c62016-06-16 17:41:47 -0700249}, function(err, results) {
evansirokyd401c892016-06-16 00:05:14 -0700250 console.log('done')
251 if(err) {
252 console.log('error!', err)
253 return
254 }
evansirokyd401c892016-06-16 00:05:14 -0700255})