1var exec = require('child_process').exec
2var fs = require('fs')
3
4var area = require('@mapbox/geojson-area')
5var geojsonhint = require('@mapbox/geojsonhint')
6var bbox = require('@turf/bbox').default
7var helpers = require('@turf/helpers')
8var multiPolygon = helpers.multiPolygon
9var polygon = helpers.polygon
10var asynclib = require('async')
11var jsts = require('jsts')
12var rimraf = require('rimraf')
13var overpass = require('query-overpass')
14
15const ProgressStats = require('./progressStats')
16
17var osmBoundarySources = require('./osmBoundarySources.json')
18var zoneCfg = require('./timezones.json')
19var expectedZoneOverlaps = require('./expectedZoneOverlaps.json')
20
21// allow building of only a specified zones
22var filteredIndex = process.argv.indexOf('--filtered-zones')
23let filteredZones = []
24if (filteredIndex > -1 && process.argv[filteredIndex + 1]) {
25  filteredZones = process.argv[filteredIndex + 1].split(',')
26  var newZoneCfg = {}
27  filteredZones.forEach((zoneName) => {
28    newZoneCfg[zoneName] = zoneCfg[zoneName]
29  })
30  zoneCfg = newZoneCfg
31
32  // filter out unneccessary downloads
33  var newOsmBoundarySources = {}
34  Object.keys(zoneCfg).forEach((zoneName) => {
35    zoneCfg[zoneName].forEach((op) => {
36      if (op.source === 'overpass') {
37        newOsmBoundarySources[op.id] = osmBoundarySources[op.id]
38      }
39    })
40  })
41
42  osmBoundarySources = newOsmBoundarySources
43}
44
45var geoJsonReader = new jsts.io.GeoJSONReader()
46var geoJsonWriter = new jsts.io.GeoJSONWriter()
47var precisionModel = new jsts.geom.PrecisionModel(1000000)
48var precisionReducer = new jsts.precision.GeometryPrecisionReducer(precisionModel)
49var distZones = {}
50var minRequestGap = 4
51var curRequestGap = 4
52
53var safeMkdir = function (dirname, callback) {
54  fs.mkdir(dirname, function (err) {
55    if (err && err.code === 'EEXIST') {
56      callback()
57    } else {
58      callback(err)
59    }
60  })
61}
62
63var debugGeo = function (op, a, b, reducePrecision) {
64  var result
65
66  if (reducePrecision) {
67    a = precisionReducer.reduce(a)
68    b = precisionReducer.reduce(b)
69  }
70
71  try {
72    switch (op) {
73      case 'union':
74        result = a.union(b)
75        break
76      case 'intersection':
77        result = a.intersection(b)
78        break
79      case 'intersects':
80        result = a.intersects(b)
81        break
82      case 'diff':
83        result = a.difference(b)
84        break
85      default:
86        var err = new Error('invalid op: ' + op)
87        throw err
88    }
89  } catch (e) {
90    if (e.name === 'TopologyException') {
91      console.log('Encountered TopologyException, retry with GeometryPrecisionReducer')
92      return debugGeo(op, a, b, true)
93    }
94    console.log('op err')
95    console.log(e)
96    console.log(e.stack)
97    fs.writeFileSync('debug_' + op + '_a.json', JSON.stringify(geoJsonWriter.write(a)))
98    fs.writeFileSync('debug_' + op + '_b.json', JSON.stringify(geoJsonWriter.write(b)))
99    throw e
100  }
101
102  return result
103}
104
105var fetchIfNeeded = function (file, superCallback, downloadCallback, fetchFn) {
106  // check for file that got downloaded
107  fs.stat(file, function (err) {
108    if (!err) {
109      // file found, skip download steps
110      return superCallback()
111    }
112    // check for manual file that got fixed and needs validation
113    var fixedFile = file.replace('.json', '_fixed.json')
114    fs.stat(fixedFile, function (err) {
115      if (!err) {
116        // file found, return fixed file
117        return downloadCallback(null, require(fixedFile))
118      }
119      // no manual fixed file found, download from overpass
120      fetchFn()
121    })
122  })
123}
124
125var geoJsonToGeom = function (geoJson) {
126  try {
127    return geoJsonReader.read(JSON.stringify(geoJson))
128  } catch (e) {
129    console.error('error converting geojson to geometry')
130    fs.writeFileSync('debug_geojson_read_error.json', JSON.stringify(geoJson))
131    throw e
132  }
133}
134
135var geomToGeoJson = function (geom) {
136  return geoJsonWriter.write(geom)
137}
138
139var geomToGeoJsonString = function (geom) {
140  return JSON.stringify(geoJsonWriter.write(geom))
141}
142
143const downloadProgress = new ProgressStats(
144  'Downloading',
145  Object.keys(osmBoundarySources).length
146)
147
148var downloadOsmBoundary = function (boundaryId, boundaryCallback) {
149  var cfg = osmBoundarySources[boundaryId]
150  var query = '[out:json][timeout:60];('
151  if (cfg.way) {
152    query += 'way'
153  } else {
154    query += 'relation'
155  }
156  var boundaryFilename = './downloads/' + boundaryId + '.json'
157  var debug = 'getting data for ' + boundaryId
158  var queryKeys = Object.keys(cfg)
159
160  for (var i = queryKeys.length - 1; i >= 0; i--) {
161    var k = queryKeys[i]
162    if (k === 'way') continue
163    var v = cfg[k]
164
165    query += '["' + k + '"="' + v + '"]'
166  }
167
168  query += ';);out body;>;out meta qt;'
169
170  downloadProgress.beginTask(debug, true)
171
172  asynclib.auto({
173    downloadFromOverpass: function (cb) {
174      console.log('downloading from overpass')
175      fetchIfNeeded(boundaryFilename, boundaryCallback, cb, function () {
176        var overpassResponseHandler = function (err, data) {
177          if (err) {
178            console.log(err)
179            console.log('Increasing overpass request gap')
180            curRequestGap *= 2
181            makeQuery()
182          } else {
183            console.log('Success, decreasing overpass request gap')
184            curRequestGap = Math.max(minRequestGap, curRequestGap / 2)
185            cb(null, data)
186          }
187        }
188        var makeQuery = function () {
189          console.log('waiting ' + curRequestGap + ' seconds')
190          setTimeout(function () {
191            overpass(query, overpassResponseHandler, { flatProperties: true })
192          }, curRequestGap * 1000)
193        }
194        makeQuery()
195      })
196    },
197    validateOverpassResult: ['downloadFromOverpass', function (results, cb) {
198      var data = results.downloadFromOverpass
199      if (!data.features) {
200        var err = new Error('Invalid geojson for boundary: ' + boundaryId)
201        return cb(err)
202      }
203      if (data.features.length === 0) {
204        console.error('No data for the following query:')
205        console.error(query)
206        console.error('To read more about this error, please visit https://git.io/vxKQL')
207        return cb(new Error('No data found for from overpass query'))
208      }
209      cb()
210    }],
211    saveSingleMultiPolygon: ['validateOverpassResult', function (results, cb) {
212      var data = results.downloadFromOverpass
213      var combined
214
215      // union all multi-polygons / polygons into one
216      for (var i = data.features.length - 1; i >= 0; i--) {
217        var curOsmGeom = data.features[i].geometry
218        const curOsmProps = data.features[i].properties
219        if (
220          (curOsmGeom.type === 'Polygon' || curOsmGeom.type === 'MultiPolygon') &&
221          curOsmProps.type === 'boundary' // need to make sure enclaves aren't unioned
222        ) {
223          console.log('combining border')
224          let errors = geojsonhint.hint(curOsmGeom)
225          if (errors && errors.length > 0) {
226            const stringifiedGeojson = JSON.stringify(curOsmGeom, null, 2)
227            errors = geojsonhint.hint(stringifiedGeojson)
228            console.error('Invalid geojson received in Overpass Result')
229            console.error('Overpass query: ' + query)
230            const problemFilename = boundaryId + '_convert_to_geom_error.json'
231            fs.writeFileSync(problemFilename, stringifiedGeojson)
232            console.error('saved problem file to ' + problemFilename)
233            console.error('To read more about this error, please visit https://git.io/vxKQq')
234            return cb(errors)
235          }
236          try {
237            var curGeom = geoJsonToGeom(curOsmGeom)
238          } catch (e) {
239            console.error('error converting overpass result to geojson')
240            console.error(e)
241
242            fs.writeFileSync(boundaryId + '_convert_to_geom_error-all-features.json', JSON.stringify(data))
243            return cb(e)
244          }
245          if (!combined) {
246            combined = curGeom
247          } else {
248            combined = debugGeo('union', curGeom, combined)
249          }
250        }
251      }
252      try {
253        fs.writeFile(boundaryFilename, geomToGeoJsonString(combined), cb)
254      } catch (e) {
255        console.error('error writing combined border to geojson')
256        fs.writeFileSync(boundaryId + '_combined_border_convert_to_geom_error.json', JSON.stringify(data))
257        return cb(e)
258      }
259    }]
260  }, boundaryCallback)
261}
262
263var getTzDistFilename = function (tzid) {
264  return './dist/' + tzid.replace(/\//g, '__') + '.json'
265}
266
267/**
268 * Get the geometry of the requested source data
269 *
270 * @return {Object} geom  The geometry of the source
271 * @param {Object} source  An object representing the data source
272 *   must have `source` key and then either:
273 *     - `id` if from a file
274 *     - `id` if from a file
275 */
276var getDataSource = function (source) {
277  var geoJson
278  if (source.source === 'overpass') {
279    geoJson = require('./downloads/' + source.id + '.json')
280  } else if (source.source === 'manual-polygon') {
281    geoJson = polygon(source.data).geometry
282  } else if (source.source === 'manual-multipolygon') {
283    geoJson = multiPolygon(source.data).geometry
284  } else if (source.source === 'dist') {
285    geoJson = require(getTzDistFilename(source.id))
286  } else {
287    var err = new Error('unknown source: ' + source.source)
288    throw err
289  }
290  return geoJsonToGeom(geoJson)
291}
292
293/**
294 * Post process created timezone boundary.
295 * - remove small holes and exclaves
296 * - reduce geometry precision
297 *
298 * @param  {Geometry} geom  The jsts geometry of the timezone
299 * @param  {boolean} returnAsObject if true, return as object, otherwise return stringified
300 * @return {Object|String}         geojson as object or stringified
301 */
302var postProcessZone = function (geom, returnAsObject) {
303  // reduce precision of geometry
304  const geojson = geomToGeoJson(precisionReducer.reduce(geom))
305
306  // iterate through all polygons
307  const filteredPolygons = []
308  let allPolygons = geojson.coordinates
309  if (geojson.type === 'Polygon') {
310    allPolygons = [geojson.coordinates]
311  }
312
313  allPolygons.forEach((curPolygon, idx) => {
314    // remove any polygon with very small area
315    const polygonFeature = polygon(curPolygon)
316    const polygonArea = area.geometry(polygonFeature.geometry)
317
318    if (polygonArea < 1) return
319
320    // find all holes
321    const filteredLinearRings = []
322
323    curPolygon.forEach((curLinearRing, lrIdx) => {
324      if (lrIdx === 0) {
325        // always keep first linearRing
326        filteredLinearRings.push(curLinearRing)
327      } else {
328        const polygonFromLinearRing = polygon([curLinearRing])
329        const linearRingArea = area.geometry(polygonFromLinearRing.geometry)
330
331        // only include holes with relevant area
332        if (linearRingArea > 1) {
333          filteredLinearRings.push(curLinearRing)
334        }
335      }
336    })
337
338    filteredPolygons.push(filteredLinearRings)
339  })
340
341  // recompile to geojson string
342  const newGeojson = {
343    type: geojson.type
344  }
345
346  if (geojson.type === 'Polygon') {
347    newGeojson.coordinates = filteredPolygons[0]
348  } else {
349    newGeojson.coordinates = filteredPolygons
350  }
351
352  return returnAsObject ? newGeojson : JSON.stringify(newGeojson)
353}
354
355const buildingProgress = new ProgressStats(
356  'Building',
357  Object.keys(zoneCfg).length
358)
359
360var makeTimezoneBoundary = function (tzid, callback) {
361  buildingProgress.beginTask(`makeTimezoneBoundary for ${tzid}`, true)
362
363  var ops = zoneCfg[tzid]
364  var geom
365
366  asynclib.eachSeries(ops, function (task, cb) {
367    var taskData = getDataSource(task)
368    console.log('-', task.op, task.id)
369    if (task.op === 'init') {
370      geom = taskData
371    } else if (task.op === 'intersect') {
372      geom = debugGeo('intersection', geom, taskData)
373    } else if (task.op === 'difference') {
374      geom = debugGeo('diff', geom, taskData)
375    } else if (task.op === 'difference-reverse-order') {
376      geom = debugGeo('diff', taskData, geom)
377    } else if (task.op === 'union') {
378      geom = debugGeo('union', geom, taskData)
379    } else {
380      var err = new Error('unknown op: ' + task.op)
381      return cb(err)
382    }
383    cb()
384  },
385  function (err) {
386    if (err) { return callback(err) }
387    fs.writeFile(getTzDistFilename(tzid),
388      postProcessZone(geom),
389      callback)
390  })
391}
392
393var loadDistZonesIntoMemory = function () {
394  console.log('load zones into memory')
395  var zones = Object.keys(zoneCfg)
396  var tzid
397
398  for (var i = 0; i < zones.length; i++) {
399    tzid = zones[i]
400    distZones[tzid] = getDataSource({ source: 'dist', id: tzid })
401  }
402}
403
404var getDistZoneGeom = function (tzid) {
405  return distZones[tzid]
406}
407
408var roundDownToTenth = function (n) {
409  return Math.floor(n * 10) / 10
410}
411
412var roundUpToTenth = function (n) {
413  return Math.ceil(n * 10) / 10
414}
415
416var formatBounds = function (bounds) {
417  let boundsStr = '['
418  boundsStr += roundDownToTenth(bounds[0]) + ', '
419  boundsStr += roundDownToTenth(bounds[1]) + ', '
420  boundsStr += roundUpToTenth(bounds[2]) + ', '
421  boundsStr += roundUpToTenth(bounds[3]) + ']'
422  return boundsStr
423}
424
425var validateTimezoneBoundaries = function () {
426  const numZones = Object.keys(zoneCfg).length
427  const validationProgress = new ProgressStats(
428    'Validation',
429    numZones * (numZones + 1) / 2
430  )
431
432  console.log('do validation... this may take a few minutes')
433  var allZonesOk = true
434  var zones = Object.keys(zoneCfg)
435  var lastPct = 0
436  var compareTzid, tzid, zoneGeom
437
438  for (var i = 0; i < zones.length; i++) {
439    tzid = zones[i]
440    zoneGeom = getDistZoneGeom(tzid)
441
442    for (var j = i + 1; j < zones.length; j++) {
443      const curPct = Math.floor(validationProgress.getPercentage())
444      if (curPct % 10 === 0 && curPct !== lastPct) {
445        validationProgress.printStats('Validating zones', true)
446        lastPct = curPct
447      }
448      compareTzid = zones[j]
449
450      var compareZoneGeom = getDistZoneGeom(compareTzid)
451
452      var intersects = false
453      try {
454        intersects = debugGeo('intersects', zoneGeom, compareZoneGeom)
455      } catch (e) {
456        console.warn('warning, encountered intersection error with zone ' + tzid + ' and ' + compareTzid)
457      }
458      if (intersects) {
459        var intersectedGeom = debugGeo('intersection', zoneGeom, compareZoneGeom)
460        var intersectedArea = intersectedGeom.getArea()
461
462        if (intersectedArea > 0.0001) {
463          // check if the intersected area(s) are one of the expected areas of overlap
464          const allowedOverlapBounds = expectedZoneOverlaps[`${tzid}-${compareTzid}`] || expectedZoneOverlaps[`${compareTzid}-${tzid}`]
465          const overlapsGeoJson = geoJsonWriter.write(intersectedGeom)
466
467          // these zones are allowed to overlap in certain places, make sure the
468          // found overlap(s) all fit within the expected areas of overlap
469          if (allowedOverlapBounds) {
470            // if the overlaps are a multipolygon, make sure each individual
471            // polygon of overlap fits within at least one of the expected
472            // overlaps
473            let overlapsPolygons
474            switch (overlapsGeoJson.type) {
475              case 'MultiPolygon':
476                overlapsPolygons = overlapsGeoJson.coordinates.map(
477                  polygonCoords => ({
478                    coordinates: polygonCoords,
479                    type: 'Polygon'
480                  })
481                )
482                break
483              case 'Polygon':
484                overlapsPolygons = [overlapsGeoJson]
485                break
486              case 'GeometryCollection':
487                overlapsPolygons = []
488                overlapsGeoJson.geometries.forEach(geom => {
489                  if (geom.type === 'Polygon') {
490                    overlapsPolygons.push(geom)
491                  } else if (geom.type === 'MultiPolygon') {
492                    geom.coordinates.forEach(polygonCoords => {
493                      overlapsPolygons.push({
494                        coordinates: polygonCoords,
495                        type: 'Polygon'
496                      })
497                    })
498                  }
499                })
500                break
501              default:
502                console.error('unexpected geojson overlap type')
503                console.log(overlapsGeoJson)
504                break
505            }
506
507            let allOverlapsOk = true
508            overlapsPolygons.forEach((polygon, idx) => {
509              const bounds = bbox(polygon)
510              const polygonArea = area.geometry(polygon)
511              if (
512                polygonArea > 10 && // ignore small polygons
513                !allowedOverlapBounds.some(allowedBounds =>
514                  allowedBounds.bounds[0] <= bounds[0] && // minX
515                    allowedBounds.bounds[1] <= bounds[1] && // minY
516                    allowedBounds.bounds[2] >= bounds[2] && // maxX
517                    allowedBounds.bounds[3] >= bounds[3] // maxY
518                )
519              ) {
520                console.error(`Unexpected intersection (${polygonArea} area) with bounds: ${formatBounds(bounds)}`)
521                allOverlapsOk = false
522              }
523            })
524
525            if (allOverlapsOk) continue
526          }
527
528          // at least one unexpected overlap found, output an error and write debug file
529          console.error('Validation error: ' + tzid + ' intersects ' + compareTzid + ' area: ' + intersectedArea)
530          const debugFilename = tzid.replace(/\//g, '-') + '-' + compareTzid.replace(/\//g, '-') + '-overlap.json'
531          fs.writeFileSync(
532            debugFilename,
533            JSON.stringify(overlapsGeoJson)
534          )
535          console.error('wrote overlap area as file ' + debugFilename)
536          console.error('To read more about this error, please visit https://git.io/vx6nx')
537          allZonesOk = false
538        }
539      }
540      validationProgress.logNext()
541    }
542  }
543
544  return allZonesOk ? null : 'Zone validation unsuccessful'
545}
546
547let oceanZoneBoundaries
548let oceanZones = [
549  { tzid: 'Etc/GMT-12', left: 172.5, right: 180 },
550  { tzid: 'Etc/GMT-11', left: 157.5, right: 172.5 },
551  { tzid: 'Etc/GMT-10', left: 142.5, right: 157.5 },
552  { tzid: 'Etc/GMT-9', left: 127.5, right: 142.5 },
553  { tzid: 'Etc/GMT-8', left: 112.5, right: 127.5 },
554  { tzid: 'Etc/GMT-7', left: 97.5, right: 112.5 },
555  { tzid: 'Etc/GMT-6', left: 82.5, right: 97.5 },
556  { tzid: 'Etc/GMT-5', left: 67.5, right: 82.5 },
557  { tzid: 'Etc/GMT-4', left: 52.5, right: 67.5 },
558  { tzid: 'Etc/GMT-3', left: 37.5, right: 52.5 },
559  { tzid: 'Etc/GMT-2', left: 22.5, right: 37.5 },
560  { tzid: 'Etc/GMT-1', left: 7.5, right: 22.5 },
561  { tzid: 'Etc/GMT', left: -7.5, right: 7.5 },
562  { tzid: 'Etc/GMT+1', left: -22.5, right: -7.5 },
563  { tzid: 'Etc/GMT+2', left: -37.5, right: -22.5 },
564  { tzid: 'Etc/GMT+3', left: -52.5, right: -37.5 },
565  { tzid: 'Etc/GMT+4', left: -67.5, right: -52.5 },
566  { tzid: 'Etc/GMT+5', left: -82.5, right: -67.5 },
567  { tzid: 'Etc/GMT+6', left: -97.5, right: -82.5 },
568  { tzid: 'Etc/GMT+7', left: -112.5, right: -97.5 },
569  { tzid: 'Etc/GMT+8', left: -127.5, right: -112.5 },
570  { tzid: 'Etc/GMT+9', left: -142.5, right: -127.5 },
571  { tzid: 'Etc/GMT+10', left: -157.5, right: -142.5 },
572  { tzid: 'Etc/GMT+11', left: -172.5, right: -157.5 },
573  { tzid: 'Etc/GMT+12', left: -180, right: -172.5 }
574]
575
576if (filteredZones.length > 0) {
577  oceanZones = oceanZones.filter(oceanZone => filteredZones.indexOf(oceanZone) > -1)
578}
579
580var addOceans = function (callback) {
581  console.log('adding ocean boundaries')
582  const zones = Object.keys(zoneCfg)
583
584  const oceanProgress = new ProgressStats(
585    'Oceans',
586    oceanZones.length
587  )
588
589  oceanZoneBoundaries = oceanZones.map(zone => {
590    oceanProgress.beginTask(zone.tzid, true)
591    const geoJson = polygon([[
592      [zone.left, 90],
593      [zone.left, -90],
594      [zone.right, -90],
595      [zone.right, 90],
596      [zone.left, 90]
597    ]]).geometry
598
599    let geom = geoJsonToGeom(geoJson)
600
601    // diff against every zone
602    zones.forEach(distZone => {
603      geom = debugGeo('diff', geom, getDistZoneGeom(distZone))
604    })
605
606    return {
607      geom: postProcessZone(geom, true),
608      tzid: zone.tzid
609    }
610  })
611
612  callback()
613}
614
615var combineAndWriteZones = function (callback) {
616  var stream = fs.createWriteStream('./dist/combined.json')
617  var streamWithOceans = fs.createWriteStream('./dist/combined-with-oceans.json')
618  var zones = Object.keys(zoneCfg)
619
620  stream.write('{"type":"FeatureCollection","features":[')
621  streamWithOceans.write('{"type":"FeatureCollection","features":[')
622
623  for (var i = 0; i < zones.length; i++) {
624    if (i > 0) {
625      stream.write(',')
626      streamWithOceans.write(',')
627    }
628    var feature = {
629      type: 'Feature',
630      properties: { tzid: zones[i] },
631      geometry: geomToGeoJson(getDistZoneGeom(zones[i]))
632    }
633    const stringified = JSON.stringify(feature)
634    stream.write(stringified)
635    streamWithOceans.write(stringified)
636  }
637  oceanZoneBoundaries.forEach(boundary => {
638    streamWithOceans.write(',')
639    var feature = {
640      type: 'Feature',
641      properties: { tzid: boundary.tzid },
642      geometry: boundary.geom
643    }
644    streamWithOceans.write(JSON.stringify(feature))
645  })
646  asynclib.parallel([
647    cb => {
648      stream.end(']}', cb)
649    },
650    cb => {
651      streamWithOceans.end(']}', cb)
652    }
653  ], callback)
654}
655
656const autoScript = {
657  makeDownloadsDir: function (cb) {
658    overallProgress.beginTask('Creating downloads dir')
659    safeMkdir('./downloads', cb)
660  },
661  makeDistDir: function (cb) {
662    overallProgress.beginTask('Creating dist dir')
663    safeMkdir('./dist', cb)
664  },
665  getOsmBoundaries: ['makeDownloadsDir', function (results, cb) {
666    overallProgress.beginTask('Downloading osm boundaries')
667    asynclib.eachSeries(Object.keys(osmBoundarySources), downloadOsmBoundary, cb)
668  }],
669  createZones: ['makeDistDir', 'getOsmBoundaries', function (results, cb) {
670    overallProgress.beginTask('Creating timezone boundaries')
671    asynclib.each(Object.keys(zoneCfg), makeTimezoneBoundary, cb)
672  }],
673  validateZones: ['createZones', function (results, cb) {
674    overallProgress.beginTask('Validating timezone boundaries')
675    loadDistZonesIntoMemory()
676    if (process.argv.indexOf('no-validation') > -1) {
677      console.warn('WARNING: Skipping validation!')
678      cb()
679    } else {
680      cb(validateTimezoneBoundaries())
681    }
682  }],
683  addOceans: ['validateZones', function (results, cb) {
684    overallProgress.beginTask('Adding oceans')
685    addOceans(cb)
686  }],
687  mergeZones: ['addOceans', function (results, cb) {
688    overallProgress.beginTask('Merging zones')
689    combineAndWriteZones(cb)
690  }],
691  zipGeoJson: ['mergeZones', function (results, cb) {
692    overallProgress.beginTask('Zipping geojson')
693    exec('zip dist/timezones.geojson.zip dist/combined.json', cb)
694  }],
695  zipGeoJsonWithOceans: ['mergeZones', function (results, cb) {
696    overallProgress.beginTask('Zipping geojson with oceans')
697    exec('zip dist/timezones-with-oceans.geojson.zip dist/combined-with-oceans.json', cb)
698  }],
699  makeShapefile: ['mergeZones', function (results, cb) {
700    overallProgress.beginTask('Converting from geojson to shapefile')
701    rimraf.sync('dist/combined-shapefile.*')
702    exec(
703      'ogr2ogr -f "ESRI Shapefile" dist/combined-shapefile.shp dist/combined.json',
704      function (err, stdout, stderr) {
705        if (err) { return cb(err) }
706        exec('zip dist/timezones.shapefile.zip dist/combined-shapefile.*', cb)
707      }
708    )
709  }],
710  makeShapefileWithOceans: ['mergeZones', function (results, cb) {
711    overallProgress.beginTask('Converting from geojson with oceans to shapefile')
712    rimraf.sync('dist/combined-shapefile-with-oceans.*')
713    exec(
714      'ogr2ogr -f "ESRI Shapefile" dist/combined-shapefile-with-oceans.shp dist/combined-with-oceans.json',
715      function (err, stdout, stderr) {
716        if (err) { return cb(err) }
717        exec('zip dist/timezones-with-oceans.shapefile.zip dist/combined-shapefile-with-oceans.*', cb)
718      }
719    )
720  }],
721  makeListOfTimeZoneNames: function (cb) {
722    overallProgress.beginTask('Writing timezone names to file')
723    let zoneNames = Object.keys(zoneCfg)
724    oceanZones.forEach(oceanZone => {
725      zoneNames.push(oceanZone.tzid)
726    })
727    if (filteredZones.length > 0) {
728      zoneNames = zoneNames.filter(zoneName => filteredZones.indexOf(zoneName) > -1)
729    }
730    fs.writeFile(
731      'dist/timezone-names.json',
732      JSON.stringify(zoneNames),
733      cb
734    )
735  }
736}
737
738const overallProgress = new ProgressStats('Overall', Object.keys(autoScript).length)
739
740asynclib.auto(autoScript, function (err, results) {
741  console.log('done')
742  if (err) {
743    console.log('error!', err)
744  }
745})
746