Skip to content

Commit

Permalink
Merge pull request #76 from evansiroky/2018d
Browse files Browse the repository at this point in the history
2018d
  • Loading branch information
evansiroky authored Apr 13, 2018
2 parents 7db8c2a + d0f78ac commit 67f042d
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 152 deletions.
26 changes: 7 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,20 @@ The most up-to-date and accurate node.js geographical timezone lookup package.

var geoTz = require('geo-tz')

var name = geoTz.tz(47.650499, -122.350070) // 'America/Los_Angeles'
var now = geoTz.tzMoment(47.650499, -122.350070) // moment-timezone obj
var specificTime = geoTz.tzMoment(47.650499, -122.350070, '2016-03-30T01:23:45Z') // moment-timezone obj
geoTz.tz(47.650499, -122.350070) // 'America/Los_Angeles'

## API Docs:

### .tz(lat, lon, [options])
As of Version 4, there is now only one API call and no dependency on moment-timezone.

Returns timezone name found at `lat`, `lon`. Returns null if timezone could not be found at coordinate. See the Advanced Usage section for documentaiton of the `options` argument.
### geoTz(lat, lon)

### .tzMoment(lat, lon, [dateTime], [options])
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 a moment-timezone object found at `lat`, `lon`. Returns null if timezone could not be found at coordinate. If `dateTime` is omitted, the moment-timezone will have the current time set. If `dateTime` is provided, moment-timezone will be set to the time provided according to the timezone found. `dateTime` can be any single-argument parameter that will get passed to the [`moment()` parser](http://momentjs.com/docs/#/parsing/). If providing the `options` argument, you must also send an argument for `dateTime` (send "null" to get the current time).

## Advanced usage:

### .createPreloadedFeatureProvider()

By default, to keep memory usage low, the library loads geographic feature files on-demand when determining timezone. This behavior has performance implications and can be changed by specifying a different feature provider in an options object. `geoTz.createPreloadedFeatureProvider()` creates a feature provider that loads all geographic features into memory. This tends to make the `tz()` and `tzMoment()` calls 20-30 times faster, but also consumes about 900 MB of [memory](https://futurestud.io/tutorials/node-js-increase-the-memory-limit-for-your-process). Make sure to not create such a provider on every timezone lookup. The preloaded feature provider should be created on application startup and reused. Usage example:

var featureProvider = geoTz.createPreloadedFeatureProvider()
var options = { featureProvider: featureProvider }
var name = geoTz.tz(47.650499, -122.350070, options)
var specificTime = geoTz.tzMoment(47.650499, -122.350070, '2016-03-30T01:23:45Z', options) // moment-timezone obj
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.

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.

## An Important Note About Maintenance

Due to the ever-changing nature of timezone data, it is critical that you always use the latest version of this package. Any releases to this project's dependency of moment-timezone will also cause a new release in this package. If you use old versions, there will be a few edge cases where the calculated time is wrong. If you use greenkeeper, please be sure to specify an exact target version so you will always get PR's for even patch-level releases.
Due to the ever-changing nature of timezone data, it is critical that you always use the latest version of this package. If you use old versions, there will be a few edge cases where the calculated time is wrong. If you use greenkeeper, please be sure to specify an exact target version so you will always get PR's for even patch-level releases.
Binary file modified data.zip
Binary file not shown.
6 changes: 1 addition & 5 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
var find = require('./lib/find.js')

module.exports = {
tz: find.timezone,
tzMoment: find.timezoneMoment,
createPreloadedFeatureProvider: find.createPreloadedFeatureProvider
}
module.exports = find
93 changes: 47 additions & 46 deletions lib/find.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
var fs = require('fs')

var geobuf = require('geobuf')
var inside = require('@turf/inside')
var moment = require('moment-timezone')
var inside = require('@turf/boolean-point-in-polygon').default
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) {
// exact boundaries saved in file
Expand All @@ -18,36 +19,46 @@ var loadFeatures = function(quadPos) {
return geoJson;
}

var onDemandFeatureProvider = function(quadPos) {
return loadFeatures(quadPos)
}

var createPreloadedFeatureProvider = function() {
var preloadedFeatures = {}
var preloadFeaturesRecursive = function(curTzData, quadPos) {
if (!curTzData) {
} else if (curTzData === 'f') {
var geoJson = loadFeatures(quadPos)
preloadedFeatures[quadPos] = geoJson
} else if (typeof curTzData === 'number') {
} else {
Object.getOwnPropertyNames(curTzData).forEach(function(value, index) {
preloadFeaturesRecursive(curTzData[value], quadPos + value)
})
const oceanZones = [
{ tzid: 'Etc/GMT-12', left: 172.5, right: 180 },
{ tzid: 'Etc/GMT-11', left: 157.5, right: 172.5 },
{ tzid: 'Etc/GMT-10', left: 142.5, right: 157.5 },
{ tzid: 'Etc/GMT-9', left: 127.5, right: 142.5 },
{ tzid: 'Etc/GMT-8', left: 112.5, right: 127.5 },
{ tzid: 'Etc/GMT-7', left: 97.5, right: 112.5 },
{ tzid: 'Etc/GMT-6', left: 82.5, right: 97.5 },
{ tzid: 'Etc/GMT-5', left: 67.5, right: 82.5 },
{ tzid: 'Etc/GMT-4', left: 52.5, right: 67.5 },
{ tzid: 'Etc/GMT-3', left: 37.5, right: 52.5 },
{ tzid: 'Etc/GMT-2', left: 22.5, right: 37.5 },
{ tzid: 'Etc/GMT-1', left: 7.5, right: 22.5 },
{ tzid: 'Etc/GMT', left: -7.5, right: 7.5 },
{ tzid: 'Etc/GMT+1', left: -22.5, right: -7.5 },
{ tzid: 'Etc/GMT+2', left: -37.5, right: -22.5 },
{ tzid: 'Etc/GMT+3', left: -52.5, right: -37.5 },
{ tzid: 'Etc/GMT+4', left: -67.5, right: -52.5 },
{ tzid: 'Etc/GMT+5', left: -82.5, right: -67.5 },
{ tzid: 'Etc/GMT+6', left: -97.5, right: -82.5 },
{ tzid: 'Etc/GMT+7', left: -112.5, right: -97.5 },
{ tzid: 'Etc/GMT+8', left: -127.5, right: -112.5 },
{ tzid: 'Etc/GMT+9', left: -142.5, right: -127.5 },
{ tzid: 'Etc/GMT+10', left: -157.5, right: -142.5 },
{ tzid: 'Etc/GMT+11', left: -172.5, right: -157.5 },
{ tzid: 'Etc/GMT+12', left: -180, right: -172.5 }
]

var getTimezoneAtSea = function (lon) {
for (var i = 0; i < oceanZones.length; i++) {
var z = oceanZones[i]
if (z.left <= lon && z.right >= lon) {
return z.tzid
}
}
preloadFeaturesRecursive(tzData.lookup, '')

return function(quadPos) {
return preloadedFeatures[quadPos]
}
}

var getTimezone = function (lat, lon, options) {
var getTimezone = function (lat, lon) {
lat = parseFloat(lat)
lon = parseFloat(lon)
options = options || {}
options.featureProvider = options.featureProvider || onDemandFeatureProvider

var err

Expand Down Expand Up @@ -116,20 +127,24 @@ var getTimezone = function (lat, lon, options) {

// analyze result of current depth
if (!curTzData) {
// no timezone in this quad
return null
// no timezone in this quad, therefore must be timezone at sea
return getTimezoneAtSea(lon)
} else if (curTzData === 'f') {
// get exact boundaries
var geoJson = options.featureProvider(quadPos)
var geoJson = featureCache.get(quadPos)
if (!geoJson) {
geoJson = loadFeatures(quadPos)
featureCache.put(quadPos, geoJson)
}

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

// not within subarea, therefore no valid timezone
return null
// not within subarea, therefore must be timezone at sea
return getTimezoneAtSea(lon)
} else if (typeof curTzData === 'number') {
// exact match found
return tzData.timezones[curTzData]
Expand All @@ -145,18 +160,4 @@ var getTimezone = function (lat, lon, options) {
}
}

module.exports = {
timezone: getTimezone,
timezoneMoment: function (lat, lon, timeString, options) {
var tzName = getTimezone(lat, lon, options)
if (!tzName) {
return tzName
}
if (timeString) {
return moment(timeString).tz(tzName)
} else {
return moment().tz(tzName)
}
},
createPreloadedFeatureProvider: createPreloadedFeatureProvider
}
module.exports = getTimezone
2 changes: 1 addition & 1 deletion lib/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ var downloadLatest = function (callback) {
res.on('end', function () {
data = JSON.parse(data)
for (var i = 0; i < data.assets.length; i++) {
data.assets[i].browser_download_url.indexOf('geojson') > -1
data.assets[i].browser_download_url.indexOf('timezones.geojson') > -1
return cb(null, data.assets[i].browser_download_url)
}
cb('geojson not found')
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@
"yazl": "^2.4.2"
},
"dependencies": {
"@turf/helpers": "^5.0.0",
"@turf/inside": "^5.0.0",
"@turf/boolean-point-in-polygon": "^6.0.1",
"@turf/helpers": "^6.1.3",
"geobuf": "^3.0.0",
"moment-timezone": "0.5.14",
"pbf": "^3.0.5"
"pbf": "^3.0.5",
"timed-cache": "^1.1.0"
},
"config": {
"commitizen": {
Expand Down
117 changes: 40 additions & 77 deletions tests/find.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,87 +9,50 @@ process.chdir('/tmp')

describe('find tests', function () {

describe('without options object', function() {
it('should find the timezone name for a valid coordinate', function () {
var tz = geoTz.tz(47.650499, -122.350070)
assert.isString(tz)
assert.equal(tz, 'America/Los_Angeles')
})
});

describe('with options object', function() {
var featureProviders = [
{ name: 'unspecified', provider: undefined },
{ name: 'preloaded', provider: geoTz.createPreloadedFeatureProvider() }
];

featureProviders.forEach(function(featureProvider) {
var options = { featureProvider: featureProvider.provider };

describe('with ' + featureProvider.name + ' feature provider', function() {
it('should find the timezone name for a valid coordinate', function () {
var tz = geoTz.tz(47.650499, -122.350070, options)
assert.isString(tz)
assert.equal(tz, 'America/Los_Angeles')
})

it('should find the timezone name for a valid coordinate via subfile examination', function () {
var tz = geoTz.tz(1.44, 104.04, options)
assert.isString(tz)
assert.equal(tz, 'Asia/Singapore')
})

it('should return null timezone name for coordinate in ocean', function () {
var tz = geoTz.tz(0, 0, options)
assert.isNull(tz)
})

it('should return a moment-timezone', function () {
var tzMoment = geoTz.tzMoment(47.650499, -122.350070, null, options)
assert.isObject(tzMoment)
assert.equal(tzMoment._z.name, 'America/Los_Angeles')
})

it('should return null timezone moment for coordinate in ocean', function () {
var tz = geoTz.tzMoment(0, 0, options)
assert.isNull(tz)
})

it('should parse time correctly', function () {
var tzMoment = geoTz.tzMoment(47.650499, -122.350070, '2016-03-30T01:23:45Z', options)
assert.equal(tzMoment.format('LLLL'), 'Tuesday, March 29, 2016 6:23 PM')
})

describe('issue cases', function () {
issueCoords.forEach(function (spot) {
it('should find ' + spot.zid + ' (' + spot.description + ')', function () {
var tz = geoTz.tz(spot.lat, spot.lon, options)
assert.isString(tz)
assert.equal(tz, spot.zid)
})
})
})
it('should find the timezone name for a valid coordinate', function () {
var tz = geoTz(47.650499, -122.350070)
assert.isString(tz)
assert.equal(tz, 'America/Los_Angeles')
})

describe('performance aspects', function() {
this.timeout(20000)
it('should find the timezone name for a valid coordinate via subfile examination', function () {
var tz = geoTz(1.44, 104.04)
assert.isString(tz)
assert.equal(tz, 'Asia/Singapore')
})

var europeTopLeft = [56.432158, -11.9263934]
var europeBottomRight = [39.8602076, 34.9127951]
var count = 2000
it('should return null timezone name for coordinate in ocean', function () {
var tz = geoTz(0, 0)
assert.equal(tz, 'Etc/GMT')
})

it('should find timezone of ' + count + ' random european positions', function () {
var timingStr = 'find tz of ' + count + ' random european positions with ' + featureProvider.name
console.time(timingStr)
for(var i=0; i<count; i++) {
geoTz.tz(
europeTopLeft[0] + Math.random() * (europeBottomRight[0] - europeTopLeft[0]),
europeTopLeft[1] + Math.random() * (europeBottomRight[1] - europeTopLeft[1]),
options)
}
console.timeEnd(timingStr);
})
})
describe('issue cases', function () {
issueCoords.forEach(function (spot) {
it('should find ' + spot.zid + ' (' + spot.description + ')', function () {
var tz = geoTz(spot.lat, spot.lon)
assert.isString(tz)
assert.equal(tz, spot.zid)
})
})
})

describe('performance aspects', function() {
this.timeout(20000)

var europeTopLeft = [56.432158, -11.9263934]
var europeBottomRight = [39.8602076, 34.9127951]
var count = 2000

it('should find timezone of ' + count + ' random european positions', function () {
var timingStr = 'find tz of ' + count + ' random european positions'
console.time(timingStr)
for(var i=0; i<count; i++) {
geoTz(
europeTopLeft[0] + Math.random() * (europeBottomRight[0] - europeTopLeft[0]),
europeTopLeft[1] + Math.random() * (europeBottomRight[1] - europeTopLeft[1])
)
}
console.timeEnd(timingStr);
})
})
})

0 comments on commit 67f042d

Please sign in to comment.