From ed5aeb1d2df1f3dd7ef78067745c0db58dd1465d Mon Sep 17 00:00:00 2001 From: Simo Moujami Date: Wed, 10 Dec 2014 16:30:07 -0500 Subject: [PATCH 1/8] Fixed the haversine formula to calculate distance between 2 points properly --- lib/geo.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/geo.js b/lib/geo.js index b5b8cd3b..71334af7 100644 --- a/lib/geo.js +++ b/lib/geo.js @@ -239,17 +239,22 @@ var EARTH_RADIUS = { }; function geoDistance(x1, y1, x2, y2, options) { + + var type = (options && options.type) || 'miles'; + // Convert to radians x1 = x1 * DEG2RAD; y1 = y1 * DEG2RAD; x2 = x2 * DEG2RAD; y2 = y2 * DEG2RAD; - var a = Math.pow(Math.sin(( y2 - y1 ) / 2.0), 2); - var b = Math.pow(Math.sin(( x2 - x1 ) / 2.0), 2); - var c = Math.sqrt(a + Math.cos(y2) * Math.cos(y1) * b); + // use the haversine formula to calculate distance for any 2 points on a sphere. + // ref http://en.wikipedia.org/wiki/Haversine_formula + var haversine = function(a) { + return Math.pow(Math.sin(a / 2.0), 2) + } - var type = (options && options.type) || 'miles'; + var c = Math.sqrt(haversine(x2 - x1) + Math.cos(x2) * Math.cos(x1) * haversine(y2 - y1)); return 2 * Math.asin(c) * EARTH_RADIUS[type]; } From 45fb633c3ad22f8d539f7e279e110e86a6e66545 Mon Sep 17 00:00:00 2001 From: Simo Moujami Date: Wed, 10 Dec 2014 17:08:56 -0500 Subject: [PATCH 2/8] renamed intermediary variable --- lib/geo.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/geo.js b/lib/geo.js index 71334af7..998ed933 100644 --- a/lib/geo.js +++ b/lib/geo.js @@ -254,8 +254,8 @@ function geoDistance(x1, y1, x2, y2, options) { return Math.pow(Math.sin(a / 2.0), 2) } - var c = Math.sqrt(haversine(x2 - x1) + Math.cos(x2) * Math.cos(x1) * haversine(y2 - y1)); + var f = Math.sqrt(haversine(x2 - x1) + Math.cos(x2) * Math.cos(x1) * haversine(y2 - y1)); - return 2 * Math.asin(c) * EARTH_RADIUS[type]; + return 2 * Math.asin(f) * EARTH_RADIUS[type]; } From 62c6ea0ddddb8090c33ffa40f94a3d4c4a259466 Mon Sep 17 00:00:00 2001 From: Simo Moujami Date: Wed, 10 Dec 2014 17:41:49 -0500 Subject: [PATCH 3/8] Added mocha tests for GeoPoint --- test/geo.test.js | 57 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 test/geo.test.js diff --git a/test/geo.test.js b/test/geo.test.js new file mode 100644 index 00000000..9dbf52d0 --- /dev/null +++ b/test/geo.test.js @@ -0,0 +1,57 @@ +/*global describe,it*/ +/*jshint expr:true*/ + +require('should'); + +var GeoPoint = require('../lib/geo').GeoPoint; + +describe('GeoPoint', function () { + describe('distance calculation between two points', function () { + + var here = new GeoPoint({ lat: 40.77492964101182, lng: -73.90950187151662 }); + var there = new GeoPoint({ lat: 40.7753227, lng: -73.909217 }); + + it('should return value in miles by default', function () { + + var distance = GeoPoint.distanceBetween(here, there); + distance.should.be.a.Number; + distance.should.equal(0.03097916611592679); + }); + + it('should return value using specified unit', function () { + + /* Supported units: + * - `radians` + * - `kilometers` + * - `meters` + * - `miles` + * - `feet` + * - `degrees` + */ + + var distance = here.distanceTo(there, { type: 'radians'}); + distance.should.be.a.Number; + distance.should.equal(0.000007825491914348416); + + distance = here.distanceTo(there, { type: 'kilometers'}); + distance.should.be.a.Number; + distance.should.equal(0.04985613511367009); + + distance = here.distanceTo(there, { type: 'meters'}); + distance.should.be.a.Number; + distance.should.equal(49.856135113670085); + + distance = here.distanceTo(there, { type: 'miles'}); + distance.should.be.a.Number; + distance.should.equal(0.03097916611592679); + + distance = here.distanceTo(there, { type: 'feet'}); + distance.should.be.a.Number; + distance.should.equal(163.56999709209347); + + distance = here.distanceTo(there, { type: 'degrees'}); + distance.should.be.a.Number; + distance.should.equal(0.0004483676593058972); + }); + }); +}); From 87027b49f88a096a42d2b15071f4687b5c70cfc9 Mon Sep 17 00:00:00 2001 From: Simo Moujami Date: Wed, 10 Dec 2014 17:47:32 -0500 Subject: [PATCH 4/8] Fixed indentation --- test/geo.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/geo.test.js b/test/geo.test.js index 9dbf52d0..562c12dd 100644 --- a/test/geo.test.js +++ b/test/geo.test.js @@ -6,16 +6,16 @@ require('should'); var GeoPoint = require('../lib/geo').GeoPoint; describe('GeoPoint', function () { - describe('distance calculation between two points', function () { + describe('distance calculation between two points', function () { var here = new GeoPoint({ lat: 40.77492964101182, lng: -73.90950187151662 }); var there = new GeoPoint({ lat: 40.7753227, lng: -73.909217 }); - it('should return value in miles by default', function () { + it('should return value in miles by default', function () { - var distance = GeoPoint.distanceBetween(here, there); + var distance = GeoPoint.distanceBetween(here, there); distance.should.be.a.Number; - distance.should.equal(0.03097916611592679); + distance.should.equal(0.03097916611592679); }); it('should return value using specified unit', function () { @@ -53,5 +53,5 @@ describe('GeoPoint', function () { distance.should.be.a.Number; distance.should.equal(0.0004483676593058972); }); - }); + }); }); From beaa97f6f4f9b9f96dea1d2fa2f850014d6e3093 Mon Sep 17 00:00:00 2001 From: Simo Moujami Date: Wed, 10 Dec 2014 19:36:02 -0500 Subject: [PATCH 5/8] Fixed constructor parameters and added bdd tests for constructor validation --- lib/geo.js | 26 +++++++++++++------------- test/geo.test.js | 46 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/lib/geo.js b/lib/geo.js index 998ed933..d9c0ab26 100644 --- a/lib/geo.js +++ b/lib/geo.js @@ -111,7 +111,7 @@ exports.GeoPoint = GeoPoint; * * @options {Object} Options Object with two Number properties: lat and long. * @property {Number} lat The latitude point in degrees. Range: -90 to 90. - * @property {Number} lng The longitude point in degrees. Range: -90 to 90. + * @property {Number} lng The longitude point in degrees. Range: -180 to 180. */ function GeoPoint(data) { @@ -119,14 +119,17 @@ function GeoPoint(data) { return new GeoPoint(data); } + assert(typeof data === 'object' || typeof data === 'string', 'must provide valid geo-coordinates array [lat, lng] or object or a "lat, lng" string'); + + if (typeof data === 'string') { data = data.split(/,\s*/); - assert(data.length === 2, 'must provide a string "lng,lat" creating a GeoPoint with a string'); + assert(data.length === 2, 'must provide a string "lat,lng" creating a GeoPoint with a string'); } if (Array.isArray(data)) { data = { - lng: Number(data[0]), - lat: Number(data[1]) + lat: Number(data[0]), + lng: Number(data[1]) }; } else { data.lng = Number(data.lng); @@ -177,7 +180,7 @@ GeoPoint.distanceBetween = function distanceBetween(a, b, options) { var y2 = b.lng; return geoDistance(x1, y1, x2, y2, options); -} +}; /** * Determine the spherical distance to the given point. @@ -202,15 +205,15 @@ GeoPoint.distanceBetween = function distanceBetween(a, b, options) { GeoPoint.prototype.distanceTo = function (point, options) { return GeoPoint.distanceBetween(this, point, options); -} +}; /** * Simple serialization. */ GeoPoint.prototype.toString = function () { - return this.lng + ',' + this.lat; -} + return this.lat + ',' + this.lng; +}; /** * @property {Number} PI - Ratio of a circle's circumference to its diameter. @@ -219,9 +222,6 @@ GeoPoint.prototype.toString = function () { * @property {Object} EARTH_RADIUS - Radius of the earth. */ -// ratio of a circle's circumference to its diameter -var PI = 3.1415926535897932384626433832795; - // factor to convert degrees to radians var DEG2RAD = 0.01745329252; @@ -251,8 +251,8 @@ function geoDistance(x1, y1, x2, y2, options) { // use the haversine formula to calculate distance for any 2 points on a sphere. // ref http://en.wikipedia.org/wiki/Haversine_formula var haversine = function(a) { - return Math.pow(Math.sin(a / 2.0), 2) - } + return Math.pow(Math.sin(a / 2.0), 2); + }; var f = Math.sqrt(haversine(x2 - x1) + Math.cos(x2) * Math.cos(x1) * haversine(y2 - y1)); diff --git a/test/geo.test.js b/test/geo.test.js index 562c12dd..81504186 100644 --- a/test/geo.test.js +++ b/test/geo.test.js @@ -1,11 +1,55 @@ /*global describe,it*/ -/*jshint expr:true*/ +/*jshint expr:true */ require('should'); var GeoPoint = require('../lib/geo').GeoPoint; describe('GeoPoint', function () { + + describe('constructor', function() { + + it('should support a valid array', function () { + var point = new GeoPoint([-34, 150]); + + point.lat.should.equal(-34); + point.lng.should.equal(150); + }); + + it('should support a valid object', function () { + var point = new GeoPoint({ lat: -34, lng: 150 }); + + point.lat.should.equal(-34); + point.lng.should.equal(150); + }); + + it('should support valid string geo coordinates', function () { + var point = new GeoPoint('-34,150'); + + point.lat.should.equal(-34); + point.lng.should.equal(150); + }); + + it('should reject invalid parameters', function () { + /*jshint -W024 */ + var fn = function() { + new GeoPoint('150,-34'); + }; + fn.should.throw(); + + fn = function() { + new GeoPoint('150,-34'); + }; + fn.should.throw(); + + fn = function() { + new GeoPoint(); + }; + fn.should.throw(); + }); + + }); + describe('distance calculation between two points', function () { var here = new GeoPoint({ lat: 40.77492964101182, lng: -73.90950187151662 }); From 8ba0c3f87df65b757c4039fa6f4b17e227fa66f8 Mon Sep 17 00:00:00 2001 From: Simo Moujami Date: Wed, 10 Dec 2014 19:38:39 -0500 Subject: [PATCH 6/8] Additional formatting --- lib/geo.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/geo.js b/lib/geo.js index d9c0ab26..af7f5d19 100644 --- a/lib/geo.js +++ b/lib/geo.js @@ -121,7 +121,6 @@ function GeoPoint(data) { assert(typeof data === 'object' || typeof data === 'string', 'must provide valid geo-coordinates array [lat, lng] or object or a "lat, lng" string'); - if (typeof data === 'string') { data = data.split(/,\s*/); assert(data.length === 2, 'must provide a string "lat,lng" creating a GeoPoint with a string'); @@ -216,7 +215,6 @@ GeoPoint.prototype.toString = function () { }; /** - * @property {Number} PI - Ratio of a circle's circumference to its diameter. * @property {Number} DEG2RAD - Factor to convert degrees to radians. * @property {Number} RAD2DEG - Factor to convert radians to degrees. * @property {Object} EARTH_RADIUS - Radius of the earth. From b990afc910087395e283e586b206b9df1c8924bb Mon Sep 17 00:00:00 2001 From: Simo Moujami Date: Wed, 10 Dec 2014 19:43:49 -0500 Subject: [PATCH 7/8] Added test for toString() --- test/geo.test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/geo.test.js b/test/geo.test.js index 81504186..4956131f 100644 --- a/test/geo.test.js +++ b/test/geo.test.js @@ -50,6 +50,16 @@ describe('GeoPoint', function () { }); + describe('toString()', function() { + + it('should return a string in the form "lat,lng"', function() { + + var point = new GeoPoint({ lat: -34, lng: 150 }); + point.toString().should.equal('-34,150'); + }); + + }); + describe('distance calculation between two points', function () { var here = new GeoPoint({ lat: 40.77492964101182, lng: -73.90950187151662 }); @@ -97,5 +107,7 @@ describe('GeoPoint', function () { distance.should.be.a.Number; distance.should.equal(0.0004483676593058972); }); + }); + }); From e1a60f146eab2aad606d5bc6584dda995e0826c3 Mon Sep 17 00:00:00 2001 From: Simo Moujami Date: Thu, 18 Dec 2014 10:47:06 -0500 Subject: [PATCH 8/8] Added support for inline parameters like: new GeoPoint(-34, 150) --- lib/geo.js | 13 ++++++++++++- test/geo.test.js | 27 ++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/geo.js b/lib/geo.js index af7f5d19..e0fb6964 100644 --- a/lib/geo.js +++ b/lib/geo.js @@ -112,6 +112,10 @@ exports.GeoPoint = GeoPoint; * @options {Object} Options Object with two Number properties: lat and long. * @property {Number} lat The latitude point in degrees. Range: -90 to 90. * @property {Number} lng The longitude point in degrees. Range: -180 to 180. + * + * @options {Array} Options Array with two Number entries: [lat,long]. + * @property {Number} lat The latitude point in degrees. Range: -90 to 90. + * @property {Number} lng The longitude point in degrees. Range: -180 to 180. */ function GeoPoint(data) { @@ -119,7 +123,14 @@ function GeoPoint(data) { return new GeoPoint(data); } - assert(typeof data === 'object' || typeof data === 'string', 'must provide valid geo-coordinates array [lat, lng] or object or a "lat, lng" string'); + if(arguments.length === 2) { + data = { + lat: arguments[0], + lng: arguments[1] + }; + } + + assert(Array.isArray(data) || typeof data === 'object' || typeof data === 'string', 'must provide valid geo-coordinates array [lat, lng] or object or a "lat, lng" string'); if (typeof data === 'string') { data = data.split(/,\s*/); diff --git a/test/geo.test.js b/test/geo.test.js index 4956131f..831caf25 100644 --- a/test/geo.test.js +++ b/test/geo.test.js @@ -30,6 +30,13 @@ describe('GeoPoint', function () { point.lng.should.equal(150); }); + it('should support coordinates as inline parameters', function () { + var point = new GeoPoint(-34, 150); + + point.lat.should.equal(-34); + point.lng.should.equal(150); + }); + it('should reject invalid parameters', function () { /*jshint -W024 */ var fn = function() { @@ -38,7 +45,25 @@ describe('GeoPoint', function () { fn.should.throw(); fn = function() { - new GeoPoint('150,-34'); + new GeoPoint('invalid_string'); + }; + fn.should.throw(); + + fn = function() { + new GeoPoint([150, -34]); + }; + fn.should.throw(); + + fn = function() { + new GeoPoint({ + lat: 150, + lng: null + }); + }; + fn.should.throw(); + + fn = function() { + new GeoPoint(150, -34); }; fn.should.throw();