Skip to content

Commit

Permalink
Merge pull request #51 from evansiroky/dev
Browse files Browse the repository at this point in the history
merge dev into master
  • Loading branch information
evansiroky authored Feb 11, 2017
2 parents d477889 + 9e742af commit f4905b0
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 59 deletions.
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# node-geo-tz
[![npm version](https://badge.fury.io/js/geo-tz.svg)](http://badge.fury.io/js/geo-tz) [![Build Status](https://travis-ci.org/evansiroky/node-geo-tz.svg?branch=master)](https://travis-ci.org/evansiroky/node-geo-tz) [![Dependency Status](https://david-dm.org/evansiroky/node-geo-tz.svg)](https://david-dm.org/evansiroky/node-geo-tz) [![Test Coverage](https://codeclimate.com/github/evansiroky/node-geo-tz/badges/coverage.svg)](https://codeclimate.com/github/evansiroky/node-geo-tz/coverage)

A node.js module to find the timezone at specific gps coordinates
The most up-to-date and accurate node.js geographical timezone lookup package. It's fast too!

## Install

Expand All @@ -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
}
57 changes: 46 additions & 11 deletions lib/find.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,53 @@
var fs = require('fs')

var geobuf = require('geobuf')
var inside = require('turf-inside')
var inside = require('@turf/inside')
var moment = require('moment-timezone')
var Pbf = require('pbf')
var point = require('turf-point')
var point = require('@turf/helpers').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
}
11 changes: 6 additions & 5 deletions lib/geo-index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
var fs = require('fs')

var _ = require('lodash')
var helpers = require('@turf/helpers')
var async = require('async')
var geobuf = require('geobuf')
var Pbf = require('pbf')
var featurecollection = require('turf-featurecollection')
var jsts = require('jsts')
var _ = require('lodash')
var mkdirp = require('mkdirp')
var polygon = require('turf-polygon')
var Pbf = require('pbf')

var featureCollection = helpers.featureCollection
var polygon = helpers.polygon
var geoJsonReader = new jsts.io.GeoJSONReader()
var geoJsonWriter = new jsts.io.GeoJSONWriter()

Expand Down Expand Up @@ -309,7 +310,7 @@ module.exports = function (tzGeojson, dataDir, targetIndexPercent, callback) {
}
}

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

fileWritingQueue.push({ folder: path, filename: 'geo.buf', data: areaGeoJson })
Expand Down
8 changes: 3 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,15 @@
"nock": "^9.0.5",
"rimraf": "^2.5.2",
"semantic-release": "^6.3.2",
"turf-featurecollection": "^1.0.1",
"turf-polygon": "^1.0.3",
"yauzl": "^2.6.0",
"yazl": "^2.4.2"
},
"dependencies": {
"@turf/helpers": "^3.7.5",
"@turf/inside": "^3.7.5",
"geobuf": "^3.0.0",
"moment-timezone": "0.5.11",
"pbf": "^3.0.5",
"turf-inside": "^3.0.5",
"turf-point": "^2.0.0"
"pbf": "^3.0.5"
},
"config": {
"commitizen": {
Expand Down
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 f4905b0

Please sign in to comment.