fast-timezone
Version:
Fast timezone lookup service.
338 lines (284 loc) • 9.8 kB
JavaScript
var fs = require('fs')
var helpers = require('@turf/helpers')
var async = require('async')
var geobuf = require('geobuf')
var jsts = require('jsts')
var _ = require('lodash')
var mkdirp = require('mkdirp')
var Pbf = require('pbf')
var featureCollection = helpers.featureCollection
var polygon = helpers.polygon
var geoJsonReader = new jsts.io.GeoJSONReader()
var geoJsonWriter = new jsts.io.GeoJSONWriter()
var within = function (outer, inner) {
var a = geoJsonReader.read(JSON.stringify(outer))
var b = geoJsonReader.read(JSON.stringify(inner))
return a.contains(b)
}
var intersects = function (a, b) {
var _a = geoJsonReader.read(JSON.stringify(a))
var _b = geoJsonReader.read(JSON.stringify(b))
return _a.intersects(_b)
}
// copied and modified from turf-intersect
var intersection = function (a, b) {
var _a = geoJsonReader.read(JSON.stringify(a))
var _b = geoJsonReader.read(JSON.stringify(b))
var result = _a.intersection(_b)
result = geoJsonWriter.write(result)
if (result.type === 'GeometryCollection' && result.geometries.length === 0) {
return undefined
} else {
return {
type: 'Feature',
properties: {},
geometry: result
}
}
}
module.exports = function (tzGeojson, dataDir, targetIndexPercent, callback) {
console.log('indexing')
var data = {
timezones: [],
lookup: {}
}
var inspectZones = function (timezonesToInspect, curBoundsGeoJson) {
var intersectedZones = []
var foundExactMatch = false
for (var j = timezonesToInspect.length - 1; j >= 0; j--) {
var curZoneIdx = timezonesToInspect[j]
var curZoneGeoJson = tzGeojson.features[curZoneIdx].geometry
if (intersects(curZoneGeoJson, curBoundsGeoJson)) {
// bounds and timezone intersect
// check if tz fully contains bounds
if (within(curZoneGeoJson, curBoundsGeoJson)) {
// bounds fully within tz, note in index
intersectedZones = [curZoneIdx]
foundExactMatch = true
break
} else {
// bounds not fully within tz, add to intersected zones
intersectedZones.push(curZoneIdx)
}
}
}
// console.log('found', intersectedZones.length, 'intersecting zones')
return {
foundExactMatch: foundExactMatch,
intersectedZones: intersectedZones
}
}
var i, j
var fileWritingQueue = async.queue(
function (data, cb) {
// console.log(data.folder)
mkdirp(data.folder, function (err) {
if (err) { return cb(err) }
var filePath = data.folder + '/' + data.filename
var writeStream = fs.createWriteStream(filePath)
var output
if (filePath.split('.').pop() === 'buf') {
output = Buffer.from(geobuf.encode(data.data, new Pbf()))
} else {
output = JSON.stringify(data.data)
}
writeStream.end(output, function (err) {
if (err) { return cb(err) }
cb()
})
})
}, 10)
// create array and index lookup of timezone names
for (i = 0; i < tzGeojson.features.length; i++) {
data.timezones.push(tzGeojson.features[i].properties.tzid)
}
// recursively generate index until 99% of planet is indexed exactly
var curPctIndexed = 0
var curLevel = 1
var expectedAtLevel = 4
var curZones = [
{
id: 'a',
bounds: [0, 0, 179.9999, 89.9999]
}, {
id: 'b',
bounds: [-179.9999, 0, 0, 89.9999]
}, {
id: 'c',
bounds: [-179.9999, -89.9999, 0, 0]
}, {
id: 'd',
bounds: [0, -89.9999, 179.9999, 0]
}
]
var printMod, curZone, curBounds, curBoundsGeoJson
while (curPctIndexed < targetIndexPercent) {
var nextZones = []
console.log('*********************************************')
console.log('level', curLevel, ' pct Indexed: ', curPctIndexed)
console.log('*********************************************')
printMod = Math.round(curZones.length / 5)
for (i = curZones.length - 1; i >= 0; i--) {
if (i % printMod === 0) {
console.log('inspecting index area ', curZones.length - i, ' of ', curZones.length)
}
curZone = curZones[i]
curBounds = curZone.bounds
curBoundsGeoJson = polygon(
[
[
[curBounds[0], curBounds[1]],
[curBounds[0], curBounds[3]],
[curBounds[2], curBounds[3]],
[curBounds[2], curBounds[1]],
[curBounds[0], curBounds[1]]
]
]
).geometry
// calculate intersection with timezone boundaries
var timezonesToInspect = []
if (curZone.tzs) {
// only examine confirmed timezones found in last iteration
timezonesToInspect = curZone.tzs
} else {
// first iteration, find all intersections in world
for (j = tzGeojson.features.length - 1; j >= 0; j--) {
timezonesToInspect.push(j)
}
}
var result = inspectZones(timezonesToInspect, curBoundsGeoJson)
var intersectedZones = result.intersectedZones
var foundExactMatch = result.foundExactMatch
var zoneResult = -1 // defaults to no zones found
// check the results
if (intersectedZones.length === 1 && foundExactMatch) {
// analysis zone can fit completely within timezone
zoneResult = intersectedZones[0]
} else if (intersectedZones.length > 0) {
// further analysis needed
var topRight = {
id: curZone.id + '.a',
tzs: intersectedZones,
bounds: [
(curBounds[0] + curBounds[2]) / 2,
(curBounds[1] + curBounds[3]) / 2,
curBounds[2],
curBounds[3]
]
}
var topLeft = {
id: curZone.id + '.b',
tzs: intersectedZones,
bounds: [
curBounds[0],
(curBounds[1] + curBounds[3]) / 2,
(curBounds[0] + curBounds[2]) / 2,
curBounds[3]
]
}
var bottomLeft = {
id: curZone.id + '.c',
tzs: intersectedZones,
bounds: [
curBounds[0],
curBounds[1],
(curBounds[0] + curBounds[2]) / 2,
(curBounds[1] + curBounds[3]) / 2
]
}
var bottomRight = {
id: curZone.id + '.d',
tzs: intersectedZones,
bounds: [
(curBounds[0] + curBounds[2]) / 2,
curBounds[1],
curBounds[2],
(curBounds[1] + curBounds[3]) / 2
]
}
nextZones.push(topRight)
nextZones.push(topLeft)
nextZones.push(bottomLeft)
nextZones.push(bottomRight)
zoneResult = {
a: intersectedZones,
b: intersectedZones,
c: intersectedZones,
d: intersectedZones
}
}
if (zoneResult !== -1) {
_.set(data.lookup, curZone.id, zoneResult)
} else {
_.unset(data.lookup, curZone.id)
}
}
// recalculate pct indexed after this round
expectedAtLevel = Math.pow(4, curLevel + 1)
curPctIndexed = (expectedAtLevel - nextZones.length) / expectedAtLevel
curZones = nextZones
curLevel++
}
console.log('*********************************************')
console.log('reached target index: ', curPctIndexed)
console.log('writing unindexable zone data')
printMod = Math.round(curZones.length / 5)
// process remaining zones and write out individual geojson for each small region
for (i = curZones.length - 1; i >= 0; i--) {
if (i % printMod === 0) {
console.log('inspecting unindexable area ', curZones.length - i, ' of ', curZones.length)
}
curZone = curZones[i]
curBounds = curZone.bounds
curBoundsGeoJson = polygon(
[
[
[curBounds[0], curBounds[1]],
[curBounds[0], curBounds[3]],
[curBounds[2], curBounds[3]],
[curBounds[2], curBounds[1]],
[curBounds[0], curBounds[1]]
]
]
).geometry
// console.log('writing zone data `', curZone.id, '`', i ,'of', curZones.length)
result = inspectZones(curZone.tzs, curBoundsGeoJson)
intersectedZones = result.intersectedZones
foundExactMatch = result.foundExactMatch
// console.log('intersectedZones', intersectedZones.length, 'exact:', foundExactMatch)
zoneResult = -1 // defaults to no zones found
// check the results
if (intersectedZones.length === 1 && foundExactMatch) {
// analysis zone can fit completely within timezone
zoneResult = intersectedZones[0]
} else if (intersectedZones.length > 0) {
var features = []
// calculate intersected area for each intersected zone
for (j = intersectedZones.length - 1; j >= 0; j--) {
var tzIdx = intersectedZones[j]
// console.log('intersecting', tzGeojson.features[tzIdx].properties)
var intersectedArea = intersection(tzGeojson.features[tzIdx].geometry, curBoundsGeoJson)
if (intersectedArea) {
intersectedArea.properties.tzid = data.timezones[tzIdx]
features.push(intersectedArea)
}
}
var areaGeoJson = featureCollection(features)
var path = dataDir + '/' + curZone.id.replace(/\./g, '/')
fileWritingQueue.push({ folder: path, filename: 'geo.buf', data: areaGeoJson })
zoneResult = 'f'
}
if (zoneResult !== -1) {
_.set(data.lookup, curZone.id, zoneResult)
} else {
_.unset(data.lookup, curZone.id)
}
}
console.log('writing index file')
fileWritingQueue.drain = function (err) {
console.log('done indexing')
callback(err)
}
// write index data to file
fileWritingQueue.push({ folder: dataDir, filename: 'index.json', data: data })
}