Skip to content

Instantly share code, notes, and snippets.

@shouse
Last active October 28, 2017 21:23
Show Gist options
  • Save shouse/0a18d9109f45f3345280864d1bb1682d to your computer and use it in GitHub Desktop.
Save shouse/0a18d9109f45f3345280864d1bb1682d to your computer and use it in GitHub Desktop.
Ti.Geohash: Encoding/decoding and associated functions

geohash

Functions to convert a geohash to/from a latitude/longitude point, and to determine bounds of a geohash cell and find neighbours of a geohash.

Methods summary:

  • encode: latitude/longitude point to geohash
  • decode: geohash to latitude/longitude
  • bounds of a geohash cell
  • adjacent neighbours of a geohash

Install

in browser

Download the JavaScript source and reference in HTML page using:

<script src="js/latlon-geohash.js"></script>

from npm

npm install --save latlon-geohash

Usage

  • Geohash.encode(lat, lon, [precision]): encode latitude/longitude point to geohash of given precision (number of characters in resulting geohash); if precision is not specified, it is inferred from precision of latitude/longitude values.
  • Geohash.decode(geohash): return { lat, lon } of centre of given geohash, to appropriate precision.
  • Geohash.bounds(geohash): return { sw, ne } bounds of given geohash.
  • Geohash.adjacent(geohash, direction): return adjacent cell to given geohash in specified direction (N/S/E/W).
  • Geohash.neighbours(geohash): return all 8 adjacent cells (n/ne/e/se/s/sw/w/nw) to given geohash.

Note to obtain neighbours as an array, you can use

var neighboursObj = Geohash.neighbours(geohash);
var neighboursArr = Object.keys(neighboursObj).map(function(n) { return neighboursObj[n]; });

Note that the parent of a geocode is simply geocode.slice(0, -1).

Import within node.js

var Geohash = require('latlon-geohash');

Further details

More information (with interactive conversion) at www.movable-type.co.uk/scripts/geohash.html.

Full JsDoc at www.movable-type.co.uk/scripts/js/latlon-geohash/docs/Geohash.html.

var geohash = require('geohash').encode(_location.latitude, _location.longitude, 6);
/***
* _____ _____ ____________
* / ___// _/ | / / ____/ _/
* \__ \ / / | | / / / / /
* ___/ // / | |/ / /____/ /
* /____/___/ |___/\____/___/
*
* @class Lib.TiGeoHash
* This library is a helper
*
* @docauthor Steven House <[email protected]>
*
* Origonally from https://github.com/chrisveness/latlon-geohash
* Geohash encoding/decoding and associated functions (c) Chris Veness 2014-2016 / MIT Licence
*/
'use strict';
/**
* Geohash encode, decode, bounds, neighbours.
*
* @namespace
*/
var Geohash = {};
/* (Geohash-specific) Base32 map */
Geohash.base32 = '0123456789bcdefghjkmnpqrstuvwxyz';
/**
* Encodes latitude/longitude to geohash, either to specified precision or to automatically
* evaluated precision.
*
* @param {number} lat - Latitude in degrees.
* @param {number} lon - Longitude in degrees.
* @param {number} [precision] - Number of characters in resulting geohash.
* @returns {string} Geohash of supplied latitude/longitude.
* @throws Invalid geohash.
*
* @example
* var geohash = Geohash.encode(52.205, 0.119, 7); // geohash: 'u120fxw'
*/
Geohash.encode = function(lat, lon, precision) {
// infer precision?
if (typeof precision == 'undefined') {
// refine geohash until it matches precision of supplied lat/lon
for (var p=1; p<=12; p++) {
var hash = Geohash.encode(lat, lon, p);
var posn = Geohash.decode(hash);
if (posn.lat==lat && posn.lon==lon) return hash;
}
precision = 12; // set to maximum
}
lat = Number(lat);
lon = Number(lon);
precision = Number(precision);
if (isNaN(lat) || isNaN(lon) || isNaN(precision)) throw new Error('Invalid geohash');
var idx = 0; // index into base32 map
var bit = 0; // each char holds 5 bits
var evenBit = true;
var geohash = '';
var latMin = -90, latMax = 90;
var lonMin = -180, lonMax = 180;
while (geohash.length < precision) {
if (evenBit) {
// bisect E-W longitude
var lonMid = (lonMin + lonMax) / 2;
if (lon >= lonMid) {
idx = idx*2 + 1;
lonMin = lonMid;
} else {
idx = idx*2;
lonMax = lonMid;
}
} else {
// bisect N-S latitude
var latMid = (latMin + latMax) / 2;
if (lat >= latMid) {
idx = idx*2 + 1;
latMin = latMid;
} else {
idx = idx*2;
latMax = latMid;
}
}
evenBit = !evenBit;
if (++bit == 5) {
// 5 bits gives us a character: append it and start over
geohash += Geohash.base32.charAt(idx);
bit = 0;
idx = 0;
}
}
return geohash;
};
/**
* Decode geohash to latitude/longitude (location is approximate centre of geohash cell,
* to reasonable precision).
*
* @param {string} geohash - Geohash string to be converted to latitude/longitude.
* @returns {{lat:number, lon:number}} (Center of) geohashed location.
* @throws Invalid geohash.
*
* @example
* var latlon = Geohash.decode('u120fxw'); // latlon: { lat: 52.205, lon: 0.1188 }
*/
Geohash.decode = function(geohash) {
var bounds = Geohash.bounds(geohash); // <-- the hard work
// now just determine the centre of the cell...
var latMin = bounds.sw.lat, lonMin = bounds.sw.lon;
var latMax = bounds.ne.lat, lonMax = bounds.ne.lon;
// cell centre
var lat = (latMin + latMax)/2;
var lon = (lonMin + lonMax)/2;
// round to close to centre without excessive precision: ⌊2-log10(Δ°)⌋ decimal places
lat = lat.toFixed(Math.floor(2-Math.log(latMax-latMin)/Math.LN10));
lon = lon.toFixed(Math.floor(2-Math.log(lonMax-lonMin)/Math.LN10));
return { lat: Number(lat), lon: Number(lon) };
};
/**
* Returns SW/NE latitude/longitude bounds of specified geohash.
*
* @param {string} geohash - Cell that bounds are required of.
* @returns {{sw: {lat: number, lon: number}, ne: {lat: number, lon: number}}}
* @throws Invalid geohash.
*/
Geohash.bounds = function(geohash) {
if (geohash.length === 0) throw new Error('Invalid geohash');
geohash = geohash.toLowerCase();
var evenBit = true;
var latMin = -90, latMax = 90;
var lonMin = -180, lonMax = 180;
for (var i=0; i<geohash.length; i++) {
var chr = geohash.charAt(i);
var idx = Geohash.base32.indexOf(chr);
if (idx == -1) throw new Error('Invalid geohash');
for (var n=4; n>=0; n--) {
var bitN = idx >> n & 1;
if (evenBit) {
// longitude
var lonMid = (lonMin+lonMax) / 2;
if (bitN == 1) {
lonMin = lonMid;
} else {
lonMax = lonMid;
}
} else {
// latitude
var latMid = (latMin+latMax) / 2;
if (bitN == 1) {
latMin = latMid;
} else {
latMax = latMid;
}
}
evenBit = !evenBit;
}
}
var bounds = {
sw: { lat: latMin, lon: lonMin },
ne: { lat: latMax, lon: lonMax },
};
return bounds;
};
/**
* Determines adjacent cell in given direction.
*
* @param geohash - Cell to which adjacent cell is required.
* @param direction - Direction from geohash (N/S/E/W).
* @returns {string} Geocode of adjacent cell.
* @throws Invalid geohash.
*/
Geohash.adjacent = function(geohash, direction) {
// based on github.com/davetroy/geohash-js
geohash = geohash.toLowerCase();
direction = direction.toLowerCase();
if (geohash.length === 0) throw new Error('Invalid geohash');
if ('nsew'.indexOf(direction) == -1) throw new Error('Invalid direction');
var neighbour = {
n: [ 'p0r21436x8zb9dcf5h7kjnmqesgutwvy', 'bc01fg45238967deuvhjyznpkmstqrwx' ],
s: [ '14365h7k9dcfesgujnmqp0r2twvyx8zb', '238967debc01fg45kmstqrwxuvhjyznp' ],
e: [ 'bc01fg45238967deuvhjyznpkmstqrwx', 'p0r21436x8zb9dcf5h7kjnmqesgutwvy' ],
w: [ '238967debc01fg45kmstqrwxuvhjyznp', '14365h7k9dcfesgujnmqp0r2twvyx8zb' ],
};
var border = {
n: [ 'prxz', 'bcfguvyz' ],
s: [ '028b', '0145hjnp' ],
e: [ 'bcfguvyz', 'prxz' ],
w: [ '0145hjnp', '028b' ],
};
var lastCh = geohash.slice(-1); // last character of hash
var parent = geohash.slice(0, -1); // hash without last character
var type = geohash.length % 2;
// check for edge-cases which don't share common prefix
if (border[direction][type].indexOf(lastCh) != -1 && parent !== '') {
parent = Geohash.adjacent(parent, direction);
}
// append letter for direction to parent
return parent + Geohash.base32.charAt(neighbour[direction][type].indexOf(lastCh));
};
/**
* Returns all 8 adjacent cells to specified geohash.
*
* @param {string} geohash - Geohash neighbours are required of.
* @returns {{n,ne,e,se,s,sw,w,nw: string}}
* @throws Invalid geohash.
*/
Geohash.neighbours = function(geohash) {
return {
'n': Geohash.adjacent(geohash, 'n'),
'ne': Geohash.adjacent(Geohash.adjacent(geohash, 'n'), 'e'),
'e': Geohash.adjacent(geohash, 'e'),
'se': Geohash.adjacent(Geohash.adjacent(geohash, 's'), 'e'),
's': Geohash.adjacent(geohash, 's'),
'sw': Geohash.adjacent(Geohash.adjacent(geohash, 's'), 'w'),
'w': Geohash.adjacent(geohash, 'w'),
'nw': Geohash.adjacent(Geohash.adjacent(geohash, 'n'), 'w'),
};
};
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
if (typeof module != 'undefined' && module.exports) module.exports = Geohash; // CommonJS, node.js
/***
* _____ _____ ____________
* / ___// _/ | / / ____/ _/
* \__ \ / / | | / / / / /
* ___/ // / | |/ / /____/ /
* /____/___/ |___/\____/___/
*
* @class Lib.TiGeo
* This library is a helper
*
* @author Steven House <[email protected]>
* @example
var geo = require('ti.geolocation.helper');
function success(_location) {
console.warn("location callback success");
console.info(JSON.stringify(_location));
}
function error(_error) {
console.error("Location error: " + _error);
}
geo.getLocation({success: success, error: error});
*/
exports.handlePermissions = handlePermissions;
exports.getLocation = getLocation;
exports.getCompass = getCompass;
exports.getLocationUpdates = getLocationUpdates;
exports.forwardGeocode = forwardGeocode;
exports.reverseGeocode = reverseGeocode;
/**
* @function handlePermissions
* @summary Handle permissions for geolocation
* @returns {void}
*/
function handlePermissions(_success) {
// The first argument is required on iOS and ignored on other platforms
var hasLocationPermissions = Ti.Geolocation.hasLocationPermissions(Ti.Geolocation.AUTHORIZATION_ALWAYS);
log.info('[Ti.Geo] Has Location Permissions: ' + hasLocationPermissions, hasLocationPermissions);
if (hasLocationPermissions) {
_success();
} else {
Ti.Geolocation.requestLocationPermissions(Ti.Geolocation.AUTHORIZATION_WHEN_IN_USE, function(_e) {
log.debug('[Ti.Geo] Request Location Permissions', _e);
if (_e.success) {
_success();
} else if (OS_ANDROID) {
log.warn('[Ti.Geo] You denied permission for now, forever or the dialog did not show at all because it you denied forever before.');
} else {
// We already check AUTHORIZATION_DENIED earlier so we can be sure it was denied now and not before
Ti.UI.createAlertDialog({
title: 'You denied permission.',
// We also end up here if the NSLocationAlwaysUsageDescription is missing from tiapp.xml in which case e.error will say so
message: _e.error
}).show();
}
});
}
}
/**
* @function getLocation
* @summary Start the geolocation
* @param {object} _args
* @param {function} _args.success Success callback
* @param {function} _args.error Error callback
* @returns {object} Returns something similar to:
{
"accuracy": 100,
"altitude": 0,
"altitudeAccuracy": null,
"heading": 0,
"latitude": 40.493781233333333,
"longitude": -80.056671
"speed": 0,
"timestamp": 1318426498331
}
*/
function getLocation(_args) {
handlePermissions(getLoc);
function getLoc() {
if (Ti.Geolocation.locationServicesEnabled) {
Titanium.Geolocation.purpose = 'Get Current Location';
Titanium.Geolocation.getCurrentPosition(function(_e) {
if (_e.error) {
log.warn('[Ti.Geo] Error', _e.error)
_args.error && _args.error(_e.error);
} else {
log.info('[Ti.Geo] Success', _e.coords);
_args.success && _args.success(_e.coords);
}
});
} else {
log.info("[Ti.Geo] GeoLocation services are turned off");
alert('Location services are not enabled');
}
}
}
/**
* Get the compass
* @param {Object} _args
* @param {Function} _args.success Success callback
* @param {Function} _args.error Error callback
* @return {Object} The console output of your program will contain the heading information, which will be sent continuously from the heading event. The data for each heading entry will be structured in the following manner.
{
"accuracy": 3,
"magneticHeading": 34.421875, // degrees east of magnetic north
"timestamp": 1318447443692,
"trueHeading": 43.595027923583984, // degrees east of true north
"type": "heading",
"x": 34.421875,
"y": -69.296875,
"z": -1.140625
}
*
*/
function getCompass(_args) {
handlePermissions(getComp);
function getComp() {
// Use the compass
if (Ti.Geolocation.locationServicesEnabled) {
Ti.Geolocation.purpose = 'Get Current Heading';
// make a single request for the current heading
Ti.Geolocation.getCurrentHeading(function(_e) {
log.info("[Ti.Geo]", _e.heading);
_args.success && _args.success(_e.heading)
});
// Set 'heading' event for continual monitoring
Ti.Geolocation.addEventListener('heading', function(_e) {
if (_e.error) {
log.warn('[Ti.Geo] Compass Error', _e.error);
_args.error && _args.error(_e.error);
} else {
_args.success && _args.success(_e.heading);
log.info(_e.heading);
}
});
} else {
alert('Please enable location services');
}
}
}
/**
* Get Location Updates
* @param {Object} _args
* @param {String} _args.purpose
* @param {Const} _args.accuracy
* @param {Number} _args.distanceFilter
* @param {Const} _args.preferredProvider
* @param {Function} _args.callback
*/
function getLocationUpdates(_args) {
handlePermissions(getLocUpdates);
function getLocUpdates() {
// Configure Location Service Properties
if (Ti.Geolocation.locationServicesEnabled) {
Ti.Geolocation.purpose = _args.purpose ? _args.purpose : 'Get Current Location';
Ti.Geolocation.accuracy = _args.accuracy ? _args.accuracy : Ti.Geolocation.ACCURACY_BEST;
Ti.Geolocation.distanceFilter = _args.distanceFilter ? _args.distanceFilter : 10;
Ti.Geolocation.preferredProvider = _args.preferredProvider ? _args.preferredProvider : Ti.Geolocation.PROVIDER_GPS;
Ti.Geolocation.addEventListener('location', function(e) {
if (e.error) {
log.error('[Geo] Error: ' + e.error);
_args.callback && _args.callback(e);
} else {
log.error('[Geo] Error: ' + e.error);
_args.callback && _args.callback(e);
}
});
} else {
alert('Location services are not enabled');
}
}
}
/**
* @function forwardGeocode
* @summary Forward Geocode
* @param {String} _address
* @param {Object} _args
* @param {Function} _args.success Success callback
* @param {Function} _args.error Error callback
* @returns {Object}
{ "accuracy": 1, "latitude": 37.389071, "longitude": -122.050156, "success": 1 }
*/
function forwardGeocode(_address, _args) {
handlePermissions(getLocUpdates);
function fwdGeo() {
if (!_address || !_callback) { log.error('[Geo] You must provide address and callback.'); return false; }
// Forward and Reverse Geocoding
Ti.Geolocation.forwardGeocoder(_address, _args.success);
}
}
/**
* Reverse Geocode
* @param {String} _lat
* @param {String} _lng
* @param {Function} _args
* @return {Object}
{
"places": [{
"address": ", 418020 Dzhany-Kuduk, , Kazakhstan", "city": "Oral", "country": "Kazakhstan",
"country_code": "KZ", "latitude": 50.0, "longitude": 50.0, "street": "", "zipcode": 418020
}],
"success": 1
}
*/
function reverseGeocode(_lat, _lng, _callback) {
handlePermissions(getLocUpdates);
function revGeo() {
if (!_lat || !_lng || !_callback) {
log.error('[Geo] You must provide address and callback.'); return false;
}
Ti.Geolocation.reverseGeocoder(_lat, _lng, _callback);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment