Skip to content

Commit

Permalink
feat(lookup): add option to load features in memory
Browse files Browse the repository at this point in the history
Add options object and feature providers, with preloaded provider
  • Loading branch information
evansiroky authored Feb 11, 2017
2 parents 07fbbb7 + 0bfa103 commit 3610440
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 46 deletions.
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ A node.js module to find the timezone at specific gps coordinates

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 specifcTime = geoTz.tzMoment(47.650499, -122.350070, '2016-03-30T01:23:45Z') // moment-timezone obj
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

## API Docs:

Expand All @@ -25,6 +25,18 @@ Returns timezone name found at `lat`, `lon`. Returns null if timezone could not

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/).

## 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


## 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.
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ var find = require('./lib/find.js')

module.exports = {
tz: find.timezone,
tzMoment: find.timezoneMoment
tzMoment: find.timezoneMoment,
createPreloadedFeatureProvider: find.createPreloadedFeatureProvider
}
53 changes: 44 additions & 9 deletions lib/find.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,46 @@ var point = require('turf-point')

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

var getTimezone = function (lat, lon) {

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 geoJson = geobuf.decode(data)
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)
})
}
}
preloadFeaturesRecursive(tzData.lookup, '')

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

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

var err

Expand Down Expand Up @@ -82,11 +119,8 @@ var getTimezone = function (lat, lon) {
// no timezone in this quad
return null
} else if (curTzData === 'f') {
// 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 geoJson = geobuf.decode(data)
// get exact boundaries
var geoJson = options.featureProvider(quadPos)

for (var i = 0; i < geoJson.features.length; i++) {
if (inside(pt, geoJson.features[i])) {
Expand All @@ -113,8 +147,8 @@ var getTimezone = function (lat, lon) {

module.exports = {
timezone: getTimezone,
timezoneMoment: function (lat, lon, timeString) {
var tzName = getTimezone(lat, lon)
timezoneMoment: function (lat, lon, timeString, options) {
var tzName = getTimezone(lat, lon, options)
if (!tzName) {
return tzName
}
Expand All @@ -123,5 +157,6 @@ module.exports = {
} else {
return moment().tz(tzName)
}
}
},
createPreloadedFeatureProvider: createPreloadedFeatureProvider
}
105 changes: 72 additions & 33 deletions tests/find.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,45 +8,84 @@ var issueCoords = require('./fixtures/issues.json')
process.chdir('/tmp')

describe('find tests', 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')
})

it('should find the timezone name for a valid coordinate via subfile examination', function () {
var tz = geoTz.tz(1.44, 104.04)
assert.isString(tz)
assert.equal(tz, 'Asia/Singapore')
})
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')
})
});

it('should return null timezone name for coordinate in ocean', function () {
var tz = geoTz.tz(0, 0)
assert.isNull(tz)
})
describe('with options object', function() {
var featureProviders = [
{ name: 'unspecified', provider: undefined },
{ name: 'preloaded', provider: geoTz.createPreloadedFeatureProvider() }
];

it('should return a moment-timezone', function () {
var tzMoment = geoTz.tzMoment(47.650499, -122.350070)
assert.isObject(tzMoment)
assert.deepPropertyVal(tzMoment, '_z.name', 'America/Los_Angeles')
})
featureProviders.forEach(function(featureProvider) {
var options = { featureProvider: featureProvider.provider };

it('should return null timezone moment for coordinate in ocean', function () {
var tz = geoTz.tzMoment(0, 0)
assert.isNull(tz)
})
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 parse time correctly', function () {
var tzMoment = geoTz.tzMoment(47.650499, -122.350070, '2016-03-30T01:23:45Z')
assert.equal(tzMoment.format('LLLL'), 'Tuesday, March 29, 2016 6:23 PM')
})
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, options)
assert.isObject(tzMoment)
assert.deepPropertyVal(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)
})
})
})

describe('performance aspects', function() {
var europeTopLeft = [56.432158, -11.9263934]
var europeBottomRight = [39.8602076, 34.9127951]
var count = 2000

describe('issue cases', function () {
issueCoords.forEach(function (spot) {
it('should find ' + spot.zid + ' (' + spot.description + ')', function () {
var tz = geoTz.tz(spot.lat, spot.lon)
assert.isString(tz)
assert.equal(tz, spot.zid)
it('should find timezone of ' + count + ' random european positions')
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);
})
})
})
})
Expand Down

0 comments on commit 3610440

Please sign in to comment.