Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Near timezone search for (lat, lon). #14

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 104 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ var EXCLUDE_REGIONS = [];
var timezoneNamesToPolygons = null;
var timezoneLongitudeShortcuts = null;
var timezoneLatitudeShortcuts = null;
// Later initialized as array [lat][lon], where lat and lon are Math.floor
// product for given coords - contains just polygon points having same coords
// after applying Math.floor() to point coords.
var polyPointsShortcuts = null;
var currentTzWorld;
var constructedShortcutFilePath = path.join(__dirname, 'shortcuts.json');

Expand All @@ -49,6 +53,21 @@ var init = function(tzWorldFile){
}
};

var addPolyPointShortcut = function (tzname, pointObj) {
var latFloor = Math.floor(pointObj.lat);
var lngFloor = Math.floor(pointObj.lng);
if (polyPointsShortcuts[latFloor] === undefined) {
polyPointsShortcuts[latFloor] = [];
}
if (polyPointsShortcuts[latFloor][lngFloor] === undefined) {
polyPointsShortcuts[latFloor][lngFloor] = {};
}
if (polyPointsShortcuts[latFloor][lngFloor][tzname] === undefined) {
polyPointsShortcuts[latFloor][lngFloor][tzname] = [];
}
polyPointsShortcuts[latFloor][lngFloor][tzname].push(pointObj);
};

var constructShortcuts = function (tzWorldFile) {
// Construct once
if ((timezoneNamesToPolygons === null) || (timezoneLongitudeShortcuts === null)) {
Expand All @@ -59,6 +78,7 @@ var constructShortcuts = function (tzWorldFile) {
var now = Date.now();
var featureCollection = JSON.parse(fs.readFileSync(tzWorldFile, 'utf-8'));
timezoneNamesToPolygons = {};
polyPointsShortcuts = [];
for (var featureIndex in featureCollection['features']) {
var tzname = featureCollection['features'][featureIndex]['properties']['TZID'];
var region = tzname.split('/')[0];
Expand All @@ -73,7 +93,10 @@ var constructShortcuts = function (tzWorldFile) {
// Our data is in WPS84. Convert to an explicit format which geolib likes.
var poly = [];
for (var pointIndex in polys[polyIndex]) {
poly.push({'lat': polys[polyIndex][pointIndex][1], 'lng': polys[polyIndex][pointIndex][0]});
var point = {'lat': polys[polyIndex][pointIndex][1], 'lng': polys[polyIndex][pointIndex][0]};
poly.push(point);
// create polygon members shortcuts
addPolyPointShortcut(tzname, point);
}
timezoneNamesToPolygons[tzname].push(poly);
}
Expand Down Expand Up @@ -232,6 +255,85 @@ var tzOffsetAt = function () {
return null;
};

// Search for some points near (latFl, lonFl) = latitude and longitude floor.
// Do not use if precisely nearest point has to be searched.
//
// Requires polyPointsShortcuts to be filled in (array of timezone polygon points
// present in <(latFl, lonFl), (latFl + 1, lonFl + 1)) interval).
//
// Search is stopped when first data field is encountered, althrough it is not
// the nearest possible point. Search is done in clockwise iteration around input
// point, starting by 1 step left, 1 step up, 2 steps right, 2 steps down,
// 3 steps left, 3 steps up and so forth and so on.
//
// Return object with timezones and its polygon points for (latFl, lonFl).
// {tzname: [points for latFl, lonFl]}
var searchNeighbours = function(latFl, lonFl, maxDist) {
// maximum sides to process - one "circle" is 4 sides
// thus maxRectSides/4 is approx distance in degrees to search.
// (Very inaccurate, because corners are 1.4 * more distant then line centers)
var maxRectSides = maxDist * 4;
var y = latFl, x = lonFl;
// (lat, lon) diffs for [left, up, right, down]
var dirDiff = [[0, -1], [-1, 0], [0, 1], [1, 0]];
var dir = 0, step = 1, stepInc = true;

// process sides up to limit
for (var i = 0; i < maxRectSides; i++) {
// process whole direction
for (var j = 0; j < step; j++) {
x += dirDiff[dir][1];
y += dirDiff[dir][0];
// out of latitude bounds?
if (! polyPointsShortcuts[y]) continue;
// return first occurence, if there are some timezones on this tile
if (polyPointsShortcuts[y][x]) return polyPointsShortcuts[y][x];
}
// increment step every second iteration
stepInc = !stepInc;
if (stepInc) step++;
// change direction after each processed side
dir = (dir + 1) % 4;
}
return undefined;
}

// Search for roughly nearest timezone at given coords. If no timezone polygon
// border points are present for 1 degree rounding of (lat, lon), surrounding
// points are searched up to distance approx maxDistance degrees.
// Distance is measured to timezone polygons border points only, so precision
// should be very low for two very distant polygon points connecting straight
// border.
//
// Return object with properties:
// .tz Timezone name.
// .data Polygon border point marked as nearest to input coords:
// .distance Distance from input coords in meters.
// .latitude Point lat.
// .longitude Point lon.
var tzNameNear = function (latitude, longitude, maxDistance) {
var maxDistance = typeof maxDistance === 'undefined' ? 10 : maxDistance;
var latFl = Math.floor(latitude);
var lngFl = Math.floor(longitude);
var latShortcuts = polyPointsShortcuts[latFl];
if (! latShortcuts) return undefined;
var points = latShortcuts[lngFl];
var minDist = Infinity;
var minTz = {};
// search for near points, if there is nothing on this tile
if (points === undefined) points = searchNeighbours(latFl, lngFl, maxDistance);
// tile could have more timezones, search for nearest polygon point
for (var tzname in points) {
var near = geolib.findNearest({'lat': latitude, 'lng': longitude}, points[tzname]);
if (near.distance < minDist) {
minDist = near.distance;
minTz.data = near;
minTz.tz = tzname;
}
}
return minTz;
}

// Allows you to call
// tzwhere.tzoffset(lat, long, function (error, offset) {
// console.log(error ? error : offset);
Expand Down Expand Up @@ -267,4 +369,5 @@ module.exports = {
'dateAt': wrap(dateAt),
'dateIn': wrap(dateIn),
'tzOffsetAt': wrap(tzOffsetAt),
'tzNameNear': wrap(tzNameNear),
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
],
"name": "tzwhere",
"description": "Determine timezone from lat/long",
"version": "1.0.0",
"version": "1.0.0-beta.0",
"main": "lib/index.js",
"directories" : {
"lib": "lib",
Expand Down
16 changes: 16 additions & 0 deletions test/readme.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,22 @@ describe('Readme example', function () {
return done();
});

it('should determine nearest time zone for the sea coordinate', function (done) {
var seaPlace = {'lat': 56.1460, 'lng': 5.1120};
tzwhere.tzNameAt(seaPlace['lat'], seaPlace['lng'], function (error, result) {
if (error) {
return done(error);
}
// tzNameAt should find nothing
assert(result === null);
// tzNameNear should find nearest place
var result = tzwhere.tzNameNear(seaPlace['lat'], seaPlace['lng']);
assert(result['tz'] === 'Europe/Oslo');
console.log(result['tz'] + ' ' + result['data']['distance']);
return done();
});
});

after(function () {
console.log(util.inspect(process.memoryUsage()));
});
Expand Down