Skip to content

Commit

Permalink
fix(data): update to 2018g data
Browse files Browse the repository at this point in the history
Also improve speed of geo-indexing.

Fixes #81
Fixes #83

BREAKING CHANGE:

The timezone lookup API call now returns an array of timezone names.
  • Loading branch information
evansiroky committed Nov 19, 2018
1 parent 05ea092 commit bbb894e
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 142 deletions.
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,22 @@ The most up-to-date and accurate node.js geographical timezone lookup package.

## Usage

```javascript
var geoTz = require('geo-tz')

geoTz(47.650499, -122.350070) // 'America/Los_Angeles'
geoTz(47.650499, -122.350070) // ['America/Los_Angeles']
geoTz(43.839319, 87.526148) // ['Asia/Shanghai', 'Asia/Urumqi']
```

## API Docs:

As of Version 4, there is now only one API call and no dependency on moment-timezone.
As of Version 5, the API now returns a list of possible timezones as there are certain coordinates where the timekeeping method will depend on the person you ask.

### geoTz(lat, lon)

Returns the timezone name found at `lat`, `lon`. The timezone name will be a timezone identifier as defined in the [timezone database](https://www.iana.org/time-zones). The underlying geographic data is obtained from the [timezone-boudary-builder](https://github.com/evansiroky/timezone-boundary-builder) project.
Returns the timezone names found at `lat`, `lon`. The timezone names will be the timezone identifiers as defined in the [timezone database](https://www.iana.org/time-zones). The underlying geographic data is obtained from the [timezone-boudary-builder](https://github.com/evansiroky/timezone-boundary-builder) project.

This library does an exact geographic lookup which has tradeoffs. It is perhaps a little bit slower that other libraries, has a large installation size on disk and cannot be used in the browser. However, the results are more accurate than other libraries that compromise by approximating the lookup of the data.
This library does an exact geographic lookup which has tradeoffs. It is perhaps a little bit slower that other libraries, has a larger installation size on disk and cannot be used in the browser. However, the results are more accurate than other libraries that compromise by approximating the lookup of the data.

The data is indexed for fast analysis with automatic caching (with time expiration) of subregions of geographic data for when a precise lookup is needed.

Expand Down
Binary file modified data.zip
Binary file not shown.
26 changes: 17 additions & 9 deletions lib/find.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
var fs = require('fs')
var path = require('path')

var geobuf = require('geobuf')
var inside = require('@turf/boolean-point-in-polygon').default
var Cache = require( "timed-cache" )
var Cache = require('timed-cache')
var Pbf = require('pbf')
var point = require('@turf/helpers').point

var tzData = require('../data/index.json')

const featureCache = new Cache()

var loadFeatures = function(quadPos) {
var loadFeatures = function (quadPos) {
// exact boundaries saved in file
// parse geojson for exact boundaries
var filepath = quadPos.split('').join('/')
var data = new Pbf(fs.readFileSync(__dirname + '/../data/' + filepath + '/geo.buf'))
var data = new Pbf(fs.readFileSync(
path.join(__dirname, '/../data/', filepath, '/geo.buf'))
)
var geoJson = geobuf.decode(data)
return geoJson;
return geoJson
}

const oceanZones = [
Expand Down Expand Up @@ -137,17 +140,22 @@ var getTimezone = function (lat, lon) {
featureCache.put(quadPos, geoJson)
}

var timezonesContainingPoint = []

for (var i = 0; i < geoJson.features.length; i++) {
if (inside(pt, geoJson.features[i])) {
return geoJson.features[i].properties.tzid
timezonesContainingPoint.push(geoJson.features[i].properties.tzid)
}
}

// not within subarea, therefore must be timezone at sea
return getTimezoneAtSea(lon)
} else if (typeof curTzData === 'number') {
// if at least one timezone contained the point, return those timezones,
// otherwise must be timezone at sea
return timezonesContainingPoint.length > 0
? timezonesContainingPoint
: getTimezoneAtSea(lon)
} else if (curTzData.length > 0) {
// exact match found
return tzData.timezones[curTzData]
return curTzData.map(idx => tzData.timezones[idx])
} else if (typeof curTzData !== 'object') {
// not another nested quad index, throw error
err = new Error('Unexpected data type')
Expand Down
232 changes: 129 additions & 103 deletions lib/geo-index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,6 @@ 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')

Expand All @@ -54,37 +21,96 @@ module.exports = function (tzGeojson, dataDir, targetIndexPercent, callback) {
lookup: {}
}

var inspectZones = function (timezonesToInspect, curBoundsGeoJson) {
const timezoneGeometries = tzGeojson.features.map(feature =>
geoJsonReader.read(JSON.stringify(feature.geometry))
)

var getIntersectingGeojson = function (tzIdx, curBoundsGeometry) {
// console.log('intersecting', tzGeojson.features[tzIdx].properties)
var intersectedGeometry = timezoneGeometries[tzIdx].intersection(curBoundsGeometry)
var intersectedGeoJson = geoJsonWriter.write(intersectedGeometry)

if (
intersectedGeoJson.type === 'GeometryCollection' &&
intersectedGeoJson.geometries.length === 0
) {
return undefined
} else {
return {
type: 'Feature',
properties: {},
geometry: intersectedGeoJson
}
}
}

/**
* Check if certain timezones fall within a specified bounds geometry.
* Also, check if an exact match is found (ie, the bounds are fully contained
* within a particular zone).
*
* @param {Array<number>} timezonesToInspect An array of indexes referencing
* a particular timezone as noted in the tzGeojson.features array.
* @param {Geometry} curBoundsGeometry The geometry to check
*/
var inspectZones = function (timezonesToInspect, curBoundsGeometry) {
var intersectedZones = []
var foundExactMatch = false
var numberOfZonesThatContainBounds = 0

for (var j = timezonesToInspect.length - 1; j >= 0; j--) {
var curZoneIdx = timezonesToInspect[j]
var curZoneGeoJson = tzGeojson.features[curZoneIdx].geometry
var curZoneGeometry = timezoneGeometries[curZoneIdx]

if (curZoneGeometry.intersects(curBoundsGeometry)) {
// bounds and timezone intersect, add to intersected zones
intersectedZones.push(curZoneIdx)

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)
if (curZoneGeometry.contains(curBoundsGeometry)) {
// bounds fully within tz
numberOfZonesThatContainBounds += 1
}
}
}

// console.log('found', intersectedZones.length, 'intersecting zones')
return {
foundExactMatch: foundExactMatch,
intersectedZones: intersectedZones
intersectedZones,
numberOfZonesThatContainBounds
}
}

var i, j

// analyze each unindexable area in a queue, otherwise the program may run out
// of memory
var unindexableAreaAnalyzingQueue = async.queue(
function (unindexableData, cb) {
var features = []
// calculate intersected area for each intersected zone
for (j = unindexableData.intersectedZones.length - 1; j >= 0; j--) {
var tzIdx = unindexableData.intersectedZones[j]
var intersectedGeoJson = getIntersectingGeojson(
tzIdx,
unindexableData.curBoundsGeometry
)

if (intersectedGeoJson) {
intersectedGeoJson.properties.tzid = data.timezones[tzIdx]
features.push(intersectedGeoJson)
}
}

var areaGeoJson = featureCollection(features)
var path = dataDir + '/' + unindexableData.curZone.id.replace(/\./g, '/')

fileWritingQueue.push(
{ folder: path, filename: 'geo.buf', data: areaGeoJson },
cb
)
},
10
)

var fileWritingQueue = async.queue(
function (data, cb) {
// console.log(data.folder)
Expand Down Expand Up @@ -132,7 +158,7 @@ module.exports = function (tzGeojson, dataDir, targetIndexPercent, callback) {
bounds: [0, -89.9999, 179.9999, 0]
}
]
var printMod, curZone, curBounds, curBoundsGeoJson
var printMod, curZone, curBounds, curBoundsGeometry

while (curPctIndexed < targetIndexPercent) {
var nextZones = []
Expand All @@ -150,17 +176,21 @@ module.exports = function (tzGeojson, dataDir, targetIndexPercent, callback) {

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
curBoundsGeometry = geoJsonReader.read(
JSON.stringify(
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 = []
Expand All @@ -175,15 +205,18 @@ module.exports = function (tzGeojson, dataDir, targetIndexPercent, callback) {
}
}

var result = inspectZones(timezonesToInspect, curBoundsGeoJson)
var result = inspectZones(timezonesToInspect, curBoundsGeometry)
var intersectedZones = result.intersectedZones
var foundExactMatch = result.foundExactMatch
var zoneResult = -1 // defaults to no zones found
var numberOfZonesThatContainBounds = result.numberOfZonesThatContainBounds
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]
if (
intersectedZones.length === numberOfZonesThatContainBounds &&
numberOfZonesThatContainBounds > 0
) {
// analysis zones can fit completely within current quad
zoneResult = intersectedZones
} else if (intersectedZones.length > 0) {
// further analysis needed
var topRight = {
Expand Down Expand Up @@ -271,50 +304,43 @@ module.exports = function (tzGeojson, dataDir, targetIndexPercent, callback) {

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
curBoundsGeometry = geoJsonReader.read(
JSON.stringify(
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)
result = inspectZones(curZone.tzs, curBoundsGeometry)
intersectedZones = result.intersectedZones
foundExactMatch = result.foundExactMatch
numberOfZonesThatContainBounds = result.numberOfZonesThatContainBounds

// console.log('intersectedZones', intersectedZones.length, 'exact:', foundExactMatch)
zoneResult = -1 // defaults to no zones found
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]
if (
intersectedZones.length === numberOfZonesThatContainBounds &&
numberOfZonesThatContainBounds > 0
) {
// analysis zones can fit completely within current quad
zoneResult = intersectedZones
} 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 })

unindexableAreaAnalyzingQueue.push({
curBoundsGeometry,
curZone,
intersectedZones
})
zoneResult = 'f'
}

Expand Down
Loading

0 comments on commit bbb894e

Please sign in to comment.