diff --git a/README.md b/README.md index f94a3910..c13e9275 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/data.zip b/data.zip index 7556f267..5d1818fe 100644 Binary files a/data.zip and b/data.zip differ diff --git a/index.js b/index.js index 361afc71..d592833e 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,3 @@ var find = require('./lib/find.js') -module.exports = { - tz: find.timezone, - tzMoment: find.timezoneMoment, - createPreloadedFeatureProvider: find.createPreloadedFeatureProvider -} +module.exports = find diff --git a/lib/find.js b/lib/find.js index 41fed3fa..d6cbbc98 100644 --- a/lib/find.js +++ b/lib/find.js @@ -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 @@ -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 @@ -116,11 +127,15 @@ 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])) { @@ -128,8 +143,8 @@ var getTimezone = function (lat, lon, options) { } } - // 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] @@ -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 diff --git a/lib/update.js b/lib/update.js index 52210bcd..18f565ef 100644 --- a/lib/update.js +++ b/lib/update.js @@ -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') diff --git a/package.json b/package.json index 2f571f15..525f341b 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/tests/find.test.js b/tests/find.test.js index 07fb2459..253f7197 100644 --- a/tests/find.test.js +++ b/tests/find.test.js @@ -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