blob: 8e8af6cf9a360adb90ace9671f3e57ecf4d41c09 [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
evansiroky63d35e12016-06-16 10:08:15 -070095var downloadOsmBoundary = function(boundaryId, boundaryCallback) {
96 var cfg = osmBoundarySources[boundaryId],
97 query = '[out:json][timeout:60];',
98 boundaryFilename = './downloads/' + cfg.type,
99 debug = 'getting data for '
100
101 if(cfg.type === 'ISO3166-1') {
102 query += '(relation["boundary"="administrative"]' +
103 '["admin_level"="2"]' +
104 '["ISO3166-1"="' + cfg.code + '"]);' +
105 'out body;>;out meta qt;'
106 boundaryFilename += '_' + cfg.code
107 debug += 'country: ' + cfg.code
evansiroky6f9d8f72016-06-21 16:27:54 -0700108 } else if(cfg.type === 'ISO3166-2') {
109 query += '(relation["boundary"="administrative"]' +
110 '["admin_level"="4"]' +
111 '["ISO3166-2"="' + cfg.code + '"]);' +
112 'out body;>;out meta qt;'
113 boundaryFilename += '_' + cfg.code
114 debug += 'state/province: ' + cfg.code
evansiroky5d008132016-06-17 08:37:51 -0700115 } else if(cfg.type === 'city') {
116 query += '(relation["boundary"="administrative"]' +
117 '["admin_level"="8"]' +
118 '["name"="' + cfg.name + '"]);' +
119 'out body;>;out meta qt;'
120 boundaryFilename += '_' + cfg.name
121 debug += 'city: ' + cfg.name
evansiroky63d35e12016-06-16 10:08:15 -0700122 }
123
evansiroky4be1c7a2016-06-16 18:23:34 -0700124 boundaryFilename += '.json'
125
evansiroky63d35e12016-06-16 10:08:15 -0700126 console.log(debug)
127
128 async.auto({
evansiroky5d008132016-06-17 08:37:51 -0700129 downloadFromOverpass: function(cb) {
evansiroky50216c62016-06-16 17:41:47 -0700130 console.log('downloading from overpass')
evansiroky4be1c7a2016-06-16 18:23:34 -0700131 fetchIfNeeded(boundaryFilename, boundaryCallback, function() {
evansiroky50216c62016-06-16 17:41:47 -0700132 overpass(query, cb, { flatProperties: true })
evansiroky63d35e12016-06-16 10:08:15 -0700133 })
134 },
evansiroky63d35e12016-06-16 10:08:15 -0700135 validateOverpassResult: ['downloadFromOverpass', function(results, cb) {
136 var data = results.downloadFromOverpass
137 if(!data.features || data.features.length == 0) {
138 err = new Error('Invalid geojson for boundary: ' + boundaryId)
139 return cb(err)
140 }
141 cb()
142 }],
143 saveSingleMultiPolygon: ['validateOverpassResult', function(results, cb) {
144 var data = results.downloadFromOverpass,
145 combined
146
147 // union all multi-polygons / polygons into one
148 for (var i = data.features.length - 1; i >= 0; i--) {
149 var curGeom = data.features[i].geometry
150 if(curGeom.type === 'Polygon' || curGeom.type === 'MultiPolygon') {
151 console.log('combining border')
152 if(!combined) {
153 combined = curGeom
154 } else {
155 combined = union(curGeom, combined)
156 }
157 }
158 }
159 fs.writeFile(boundaryFilename, JSON.stringify(combined, null, 2), cb)
160 }]
161 }, boundaryCallback)
162}
evansirokyd401c892016-06-16 00:05:14 -0700163
evansiroky4be1c7a2016-06-16 18:23:34 -0700164var getDataSource = function(source) {
evansirokybecb56e2016-07-06 12:42:35 -0700165 var geoJson
evansiroky4be1c7a2016-06-16 18:23:34 -0700166 if(source.source === 'efele') {
evansirokybecb56e2016-07-06 12:42:35 -0700167 geoJson = efeleGeoms[efeleLookup[source.id]].geometry
evansiroky4be1c7a2016-06-16 18:23:34 -0700168 } else if(source.source === 'overpass') {
evansirokybecb56e2016-07-06 12:42:35 -0700169 geoJson = require('./downloads/' + source.id + '.json')
evansiroky35f64342016-06-16 22:17:04 -0700170 } else if(source.source === 'manual-polygon') {
evansirokybecb56e2016-07-06 12:42:35 -0700171 geoJson = polygon(source.data).geometry
evansiroky4be1c7a2016-06-16 18:23:34 -0700172 } else {
173 var err = new Error('unknown source: ' + source.source)
174 throw err
175 }
evansirokybecb56e2016-07-06 12:42:35 -0700176 return geoJsonReader.read(JSON.stringify(geoJson))
evansiroky4be1c7a2016-06-16 18:23:34 -0700177}
178
179var makeTimezoneBoundary = function(tzid, callback) {
evansiroky35f64342016-06-16 22:17:04 -0700180 console.log('makeTimezoneBoundary for', tzid)
181
evansiroky4be1c7a2016-06-16 18:23:34 -0700182 var ops = zoneCfg[tzid],
183 geom
184
185 async.eachSeries(ops, function(task, cb) {
186 var taskData = getDataSource(task)
evansiroky6f9d8f72016-06-21 16:27:54 -0700187 console.log('-', task.op, task.id)
evansiroky4be1c7a2016-06-16 18:23:34 -0700188 if(task.op === 'init') {
189 geom = taskData
190 } else if(task.op === 'intersect') {
evansiroky6f9d8f72016-06-21 16:27:54 -0700191 geom = debugGeo('intersection', geom, taskData)
evansiroky4be1c7a2016-06-16 18:23:34 -0700192 } else if(task.op === 'difference') {
evansiroky6f9d8f72016-06-21 16:27:54 -0700193 geom = debugGeo('diff', geom, taskData)
evansiroky6e45be62016-06-17 08:46:28 -0700194 } else if(task.op === 'union') {
evansiroky6f9d8f72016-06-21 16:27:54 -0700195 geom = debugGeo('union', geom, taskData)
evansiroky4be1c7a2016-06-16 18:23:34 -0700196 }
evansiroky35f64342016-06-16 22:17:04 -0700197 cb()
evansiroky4be1c7a2016-06-16 18:23:34 -0700198 }, function(err) {
199 if(err) { return callback(err) }
evansirokybecb56e2016-07-06 12:42:35 -0700200 fs.writeFile('./dist/' + tzid.replace(/\//g, '__') + '.json',
201 JSON.stringify(geoJsonWriter.write(geom)),
202 callback)
evansiroky4be1c7a2016-06-16 18:23:34 -0700203 })
204}
205
evansirokyd401c892016-06-16 00:05:14 -0700206async.auto({
207 makeDownloadsDir: function(cb) {
208 console.log('creating downloads dir')
evansiroky4be1c7a2016-06-16 18:23:34 -0700209 safeMkdir('./downloads', cb)
210 },
211 makeDistDir: function(cb) {
212 console.log('createing dist dir')
213 safeMkdir('./dist', cb)
evansirokyd401c892016-06-16 00:05:14 -0700214 },
215 getEfeleShapefile: ['makeDownloadsDir', function(results, cb) {
evansiroky50216c62016-06-16 17:41:47 -0700216 console.log('download efele.net shapefile')
217 var efeleFilename = './downloads/tz_world_mp.zip'
218 fetchIfNeeded(efeleFilename, cb, function() {
219 var file = fs.createWriteStream(efeleFilename)
220 http.get('http://efele.net/maps/tz/world/tz_world_mp.zip', function(response) {
221 response.pipe(file)
222 file
223 .on('finish', function() {
224 file.close(cb)
225 })
226 .on('error', cb)
227 })
evansirokyd401c892016-06-16 00:05:14 -0700228 })
229 }],
evansirokyd401c892016-06-16 00:05:14 -0700230 getOsmBoundaries: ['makeDownloadsDir', function(results, cb) {
231 console.log('downloading osm boundaries')
evansiroky63d35e12016-06-16 10:08:15 -0700232 async.eachSeries(Object.keys(osmBoundarySources), downloadOsmBoundary, cb)
233 }],
234 extractEfeleNetShapefile: ['getOsmBoundaries', function(results, cb) {
evansiroky63d35e12016-06-16 10:08:15 -0700235 console.log('extracting efele.net shapefile')
236 extractToGeoJson(cb)
evansiroky50216c62016-06-16 17:41:47 -0700237 }],
238 dictifyEfeleNetData: ['extractEfeleNetShapefile', function(results, cb) {
evansiroky35f64342016-06-16 22:17:04 -0700239 console.log('dictify efele.net')
evansiroky4be1c7a2016-06-16 18:23:34 -0700240 efeleGeoms = results.extractEfeleNetShapefile.features
241 for (var i = efeleGeoms.length - 1; i >= 0; i--) {
242 var curTz = efeleGeoms[i]
243 efeleLookup[curTz.properties.TZID] = i
evansiroky50216c62016-06-16 17:41:47 -0700244 }
evansiroky4be1c7a2016-06-16 18:23:34 -0700245 cb()
evansiroky50216c62016-06-16 17:41:47 -0700246 }],
evansiroky4be1c7a2016-06-16 18:23:34 -0700247 createZones: ['makeDistDir', 'dictifyEfeleNetData', function(results, cb) {
evansiroky35f64342016-06-16 22:17:04 -0700248 console.log('createZones')
evansiroky50216c62016-06-16 17:41:47 -0700249 async.each(Object.keys(zoneCfg), makeTimezoneBoundary, cb)
250 }],
251 mergeZones: ['createZones', function(results, cb) {
evansiroky4be1c7a2016-06-16 18:23:34 -0700252 cb()
evansirokyd401c892016-06-16 00:05:14 -0700253 }]
evansiroky50216c62016-06-16 17:41:47 -0700254}, function(err, results) {
evansirokyd401c892016-06-16 00:05:14 -0700255 console.log('done')
256 if(err) {
257 console.log('error!', err)
258 return
259 }
evansirokyd401c892016-06-16 00:05:14 -0700260})