From 5b8a9a31d2877452060eddcd32ec727cd8df4105 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Fri, 29 Jul 2016 19:44:45 +0200 Subject: [PATCH 1/3] wrap coordinates at poles --- sanitiser/_geo_common.js | 8 ++++ sanitiser/wrap.js | 36 +++++++++++++++ test/unit/run.js | 1 + test/unit/sanitiser/_geo_common.js | 58 ++++++++++++++++++++++++ test/unit/sanitiser/reverse.js | 2 - test/unit/sanitiser/search.js | 2 - test/unit/sanitiser/wrap.js | 71 ++++++++++++++++++++++++++++++ 7 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 sanitiser/wrap.js create mode 100644 test/unit/sanitiser/wrap.js diff --git a/sanitiser/_geo_common.js b/sanitiser/_geo_common.js index 3287bc31..1992a50d 100644 --- a/sanitiser/_geo_common.js +++ b/sanitiser/_geo_common.js @@ -4,6 +4,7 @@ var groups = require('./_groups'), util = require('util'), check = require('check-types'), + wrap = require('./wrap'), _ = require('lodash'); /** @@ -68,6 +69,7 @@ function sanitize_circle( key_prefix, clean, raw, circle_is_required ) { * @param {bool} point_is_required */ function sanitize_point( key_prefix, clean, raw, point_is_required ) { + // calculate full property names from the key_prefix var properties = [ 'lat', 'lon'].map(function(prop) { return key_prefix + '.' + prop; @@ -91,6 +93,11 @@ function sanitize_point( key_prefix, clean, raw, point_is_required ) { properties.forEach(function(prop) { sanitize_coord(prop, clean, raw, true); }); + + // normalize co-ordinates by wrapping around the poles + var normalized = wrap(clean[properties[0]], clean[properties[1]]); + clean[properties[0]] = normalized.lat; + clean[properties[1]] = normalized.lon; } /** @@ -103,6 +110,7 @@ function sanitize_point( key_prefix, clean, raw, point_is_required ) { */ function sanitize_coord( key, clean, raw, latlon_is_required ) { var parsedValue = parseFloat( raw[key] ); + if ( _.isFinite( parsedValue ) ) { clean[key] = parsedValue; } diff --git a/sanitiser/wrap.js b/sanitiser/wrap.js new file mode 100644 index 00000000..f1a482e5 --- /dev/null +++ b/sanitiser/wrap.js @@ -0,0 +1,36 @@ + +/** + normalize co-ordinates that lie outside of the normal ranges. +**/ + +function wrap( lat, lon ){ + + var flip = false; + var point = { lat: lat, lon: lon }; + + // north pole + if( point.lat > 90 ){ + point.lat = 90 - point.lat % 90; + point.lon += 180; + } + + // south pole + else if( point.lat < -90 ){ + point.lat = -90 - point.lat % 90; + point.lon += 180; + } + + // reduce lon + while( point.lon > 180 ){ + point.lon -= 360; + } + + // increase lon + while( point.lon < -180 ){ + point.lon += 360; + } + + return point; +} + +module.exports = wrap; diff --git a/test/unit/run.js b/test/unit/run.js index 62516bc9..2a89b161 100644 --- a/test/unit/run.js +++ b/test/unit/run.js @@ -56,6 +56,7 @@ var tests = [ require('./sanitiser/place'), require('./sanitiser/reverse'), require('./sanitiser/search'), + require('./sanitiser/wrap'), require('./service/mget'), require('./service/search'), ]; diff --git a/test/unit/sanitiser/_geo_common.js b/test/unit/sanitiser/_geo_common.js index f8a27a8b..d8e174d3 100644 --- a/test/unit/sanitiser/_geo_common.js +++ b/test/unit/sanitiser/_geo_common.js @@ -12,6 +12,64 @@ module.exports.tests.interface = function(test, common) { }); }; +module.exports.tests.wrapping = function(test, common) { + test('control - no wrapping required', function (t) { + var clean = {}; + var params = { + 'point.lat': 40.7, + 'point.lon': -73.9 + }; + sanitize.sanitize_point( 'point', clean, params, false ); + t.equal(clean['point.lat'], params['point.lat']); + t.equal(clean['point.lon'], params['point.lon']); + t.end(); + }); + test('positive longitude wrapping', function (t) { + var clean = {}; + var params = { + 'point.lat': 40.7, + 'point.lon': 195.1 + }; + sanitize.sanitize_point( 'point', clean, params, false ); + t.equal(clean['point.lat'], 40.7); + t.equal(clean['point.lon'], -164.9); + t.end(); + }); + test('negative longitude wrapping', function (t) { + var clean = {}; + var params = { + 'point.lat': 40.7, + 'point.lon': -195.1 + }; + sanitize.sanitize_point( 'point', clean, params, false ); + t.equal(clean['point.lat'], 40.7); + t.equal(clean['point.lon'], 164.9); + t.end(); + }); + test('positive latitudinal wrapping', function (t) { + var clean = {}; + var params = { + 'point.lat': 98.1, + 'point.lon': -73.9 + }; + sanitize.sanitize_point( 'point', clean, params, false ); + t.equal(clean['point.lat'], 81.9); + t.equal(clean['point.lon'], 106.1); + t.end(); + }); + test('negative latitudinal wrapping', function (t) { + var clean = {}; + var params = { + 'point.lat': -98.1, + 'point.lon': -73.9 + }; + sanitize.sanitize_point( 'point', clean, params, false ); + t.equal(clean['point.lat'], -81.9); + t.equal(clean['point.lon'], 106.1); + t.end(); + }); +}; + module.exports.tests.coord = function(test, common) { test('valid coord', function (t) { var clean = {}; diff --git a/test/unit/sanitiser/reverse.js b/test/unit/sanitiser/reverse.js index fcce8226..089e1aaf 100644 --- a/test/unit/sanitiser/reverse.js +++ b/test/unit/sanitiser/reverse.js @@ -65,7 +65,6 @@ module.exports.tests.sanitize_lat = function(test, common) { sanitize(req, function(){ var expected_lat = parseFloat( lat ); t.deepEqual(req.errors, [], 'no errors'); - t.equal(req.clean['point.lat'], expected_lat, 'clean set correctly (' + lat + ')'); }); }); t.end(); @@ -93,7 +92,6 @@ module.exports.tests.sanitize_lon = function(test, common) { sanitize(req, function(){ var expected_lon = parseFloat( lon ); t.deepEqual(req.errors, [], 'no errors'); - t.equal(req.clean['point.lon'], expected_lon, 'clean set correctly (' + lon + ')'); }); }); t.end(); diff --git a/test/unit/sanitiser/search.js b/test/unit/sanitiser/search.js index 864195a8..53bdb6af 100644 --- a/test/unit/sanitiser/search.js +++ b/test/unit/sanitiser/search.js @@ -129,7 +129,6 @@ module.exports.tests.sanitize_lat = function(test, common) { sanitize(req, function(){ var expected_lat = parseFloat( lat ); t.equal(req.errors[0], undefined, 'no error'); - t.equal(req.clean['focus.point.lat'], expected_lat, 'clean lat set correctly (' + lat + ')'); }); }); t.end(); @@ -146,7 +145,6 @@ module.exports.tests.sanitize_lon = function(test, common) { sanitize( req, function(){ var expected_lon = parseFloat( lon ); t.equal(req.errors[0], undefined, 'no error'); - t.deepEqual(req.clean['focus.point.lon'], expected_lon, 'clean set correctly (' + lon + ')'); }); }); t.end(); diff --git a/test/unit/sanitiser/wrap.js b/test/unit/sanitiser/wrap.js new file mode 100644 index 00000000..73e2ec0e --- /dev/null +++ b/test/unit/sanitiser/wrap.js @@ -0,0 +1,71 @@ + +var wrap = require('../../../sanitiser/wrap'); + +module.exports.tests = {}; + +module.exports.tests.wrapping = function(test, common) { + test('control - no wrapping required', function (t) { + var norm = wrap(55.555, 22.222); + t.equal(norm.lat, 55.555); + t.equal(norm.lon, 22.222); + t.end(); + }); + test('positive longitude wrapping', function (t) { + var norm = wrap(0, 181); + t.equal(norm.lat, 0); + t.equal(norm.lon, -179); + t.end(); + }); + test('positive longitude wrapping (double)', function (t) { + var norm = wrap(0, 541); + t.equal(norm.lat, 0); + t.equal(norm.lon, -179); + t.end(); + }); + test('negative longitude wrapping', function (t) { + var norm = wrap(0, -181); + t.equal(norm.lat, 0); + t.equal(norm.lon, 179); + t.end(); + }); + test('negative longitude wrapping (double)', function (t) { + var norm = wrap(0, -541); + t.equal(norm.lat, 0); + t.equal(norm.lon, 179); + t.end(); + }); + test('positive latitudinal wrapping', function (t) { + var norm = wrap(91, 0); + t.equal(norm.lat, 89); + t.equal(norm.lon, 180); + t.end(); + }); + test('positive latitudinal wrapping (double)', function (t) { + var norm = wrap(271, 0); + t.equal(norm.lat, 89); + t.equal(norm.lon, 180); + t.end(); + }); + test('negative latitudinal wrapping', function (t) { + var norm = wrap(-91, 0); + t.equal(norm.lat, -89); + t.equal(norm.lon, 180); + t.end(); + }); + test('negative latitudinal wrapping (double)', function (t) { + var norm = wrap(-271, 0); + t.equal(norm.lat, -89); + t.equal(norm.lon, 180); + t.end(); + }); +}; + +module.exports.all = function (tape, common) { + function test(name, testFunction) { + return tape('SANTIZE wrap ' + name, testFunction); + } + + for( var testCase in module.exports.tests ){ + module.exports.tests[testCase](test, common); + } +}; From c317d4f3215213f3f782418bf74de8a031f66d49 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Tue, 2 Aug 2016 12:45:58 +0200 Subject: [PATCH 2/3] fix/improve coordinate wrapping function, added more tests --- sanitiser/wrap.js | 38 +++++-- test/unit/sanitiser/_geo_common.js | 41 +++---- test/unit/sanitiser/wrap.js | 172 ++++++++++++++++++++++++----- 3 files changed, 193 insertions(+), 58 deletions(-) diff --git a/sanitiser/wrap.js b/sanitiser/wrap.js index f1a482e5..c89088e7 100644 --- a/sanitiser/wrap.js +++ b/sanitiser/wrap.js @@ -1,23 +1,37 @@ /** normalize co-ordinates that lie outside of the normal ranges. + + longitude wrapping simply requires adding +- 360 to the value until it comes + in to range. + + for the latitude values we need to flip the longitude whenever the latitude + crosses a pole. **/ + function wrap( lat, lon ){ - var flip = false; var point = { lat: lat, lon: lon }; - - // north pole - if( point.lat > 90 ){ - point.lat = 90 - point.lat % 90; - point.lon += 180; - } - - // south pole - else if( point.lat < -90 ){ - point.lat = -90 - point.lat % 90; - point.lon += 180; + var quadrant = Math.floor( Math.abs(lat) / 90) % 4; + var pole = ( lat > 0 ) ? 90 : -90; + var offset = lat % 90; + + switch( quadrant ){ + case 0: + point.lat = offset; + break; + case 1: + point.lat = pole - offset; + point.lon += 180; + break; + case 2: + point.lat = -offset; + point.lon += 180; + break; + case 3: + point.lat = -pole + offset; + break; } // reduce lon diff --git a/test/unit/sanitiser/_geo_common.js b/test/unit/sanitiser/_geo_common.js index d8e174d3..93ec5d11 100644 --- a/test/unit/sanitiser/_geo_common.js +++ b/test/unit/sanitiser/_geo_common.js @@ -12,60 +12,61 @@ module.exports.tests.interface = function(test, common) { }); }; +// @note: for better coverage see unit tests for 'wrap.js'. module.exports.tests.wrapping = function(test, common) { test('control - no wrapping required', function (t) { var clean = {}; var params = { - 'point.lat': 40.7, - 'point.lon': -73.9 + 'point.lat': +1.1, + 'point.lon': -1.1 }; sanitize.sanitize_point( 'point', clean, params, false ); - t.equal(clean['point.lat'], params['point.lat']); - t.equal(clean['point.lon'], params['point.lon']); + t.equal(clean['point.lat'], +1.1, 'not changed'); + t.equal(clean['point.lon'], -1.1, 'not changed'); t.end(); }); test('positive longitude wrapping', function (t) { var clean = {}; var params = { - 'point.lat': 40.7, - 'point.lon': 195.1 + 'point.lat': +1.1, + 'point.lon': +181.1 }; sanitize.sanitize_point( 'point', clean, params, false ); - t.equal(clean['point.lat'], 40.7); - t.equal(clean['point.lon'], -164.9); + t.equal(clean['point.lat'], +1.1, 'not changed'); + t.equal(clean['point.lon'], -178.9, 'equal to (-180 + 1.1)'); t.end(); }); test('negative longitude wrapping', function (t) { var clean = {}; var params = { - 'point.lat': 40.7, - 'point.lon': -195.1 + 'point.lat': -1.1, + 'point.lon': -181.1 }; sanitize.sanitize_point( 'point', clean, params, false ); - t.equal(clean['point.lat'], 40.7); - t.equal(clean['point.lon'], 164.9); + t.equal(clean['point.lat'], -1.1, 'not changed'); + t.equal(clean['point.lon'], +178.9, 'equal to (+180 - 1.1)'); t.end(); }); test('positive latitudinal wrapping', function (t) { var clean = {}; var params = { - 'point.lat': 98.1, - 'point.lon': -73.9 + 'point.lat': 91.1, + 'point.lon': 1.1 }; sanitize.sanitize_point( 'point', clean, params, false ); - t.equal(clean['point.lat'], 81.9); - t.equal(clean['point.lon'], 106.1); + t.equal(clean['point.lat'], +88.9, 'equal to (+90 - 1.1)'); + t.equal(clean['point.lon'], -178.9, 'equal to (-180 - 1.1)'); // polar flip t.end(); }); test('negative latitudinal wrapping', function (t) { var clean = {}; var params = { - 'point.lat': -98.1, - 'point.lon': -73.9 + 'point.lat': -91.1, + 'point.lon': -1.1 }; sanitize.sanitize_point( 'point', clean, params, false ); - t.equal(clean['point.lat'], -81.9); - t.equal(clean['point.lon'], 106.1); + t.equal(clean['point.lat'], -88.9, 'equal to (-90 + 1.1)'); + t.equal(clean['point.lon'], +178.9, 'equal to (+180 - 1.1)'); // polar flip t.end(); }); }; diff --git a/test/unit/sanitiser/wrap.js b/test/unit/sanitiser/wrap.js index 73e2ec0e..ea9a75a5 100644 --- a/test/unit/sanitiser/wrap.js +++ b/test/unit/sanitiser/wrap.js @@ -3,59 +3,179 @@ var wrap = require('../../../sanitiser/wrap'); module.exports.tests = {}; -module.exports.tests.wrapping = function(test, common) { +module.exports.tests.control = function(test, common) { test('control - no wrapping required', function (t) { var norm = wrap(55.555, 22.222); t.equal(norm.lat, 55.555); t.equal(norm.lon, 22.222); t.end(); }); - test('positive longitude wrapping', function (t) { +}; + +module.exports.tests.latitude_positive = function(test, common) { + test('positive latitude wrapping - 1 degree', function (t) { + var norm = wrap(1, 0); + t.equal(norm.lat, 1); + t.equal(norm.lon, 0); + t.end(); + }); + test('positive latitude wrapping - 91 degrees', function (t) { + var norm = wrap(91, 0); + t.equal(norm.lat, 89); + t.equal(norm.lon, 180); + t.end(); + }); + test('positive latitude wrapping - 181 degrees', function (t) { + var norm = wrap(181, 0); + t.equal(norm.lat, -1); + t.equal(norm.lon, 180); + t.end(); + }); + test('positive latitude wrapping - 271 degrees', function (t) { + var norm = wrap(271, 0); + t.equal(norm.lat, -89); + t.equal(norm.lon, 0); + t.end(); + }); + test('positive latitude wrapping - 361 degrees', function (t) { + var norm = wrap(361, 0); + t.equal(norm.lat, 1); + t.equal(norm.lon, 0); + t.end(); + }); + test('positive latitude wrapping - 631 degrees', function (t) { + var norm = wrap(631, 0); + t.equal(norm.lat, -89); + t.equal(norm.lon, 0); + t.end(); + }); + test('positive latitude wrapping - 721 degrees', function (t) { + var norm = wrap(721, 0); + t.equal(norm.lat, 1); + t.equal(norm.lon, 0); + t.end(); + }); +}; + +module.exports.tests.latitude_negative = function(test, common) { + test('negative latitude wrapping - 1 degree', function (t) { + var norm = wrap(-1, 0); + t.equal(norm.lat, -1); + t.equal(norm.lon, 0); + t.end(); + }); + test('negative latitude wrapping - 91 degrees', function (t) { + var norm = wrap(-91, 0); + t.equal(norm.lat, -89); + t.equal(norm.lon, 180); + t.end(); + }); + test('negative latitude wrapping - 181 degrees', function (t) { + var norm = wrap(-181, 0); + t.equal(norm.lat, 1); + t.equal(norm.lon, 180); + t.end(); + }); + test('negative latitude wrapping - 271 degrees', function (t) { + var norm = wrap(-271, 0); + t.equal(norm.lat, 89); + t.equal(norm.lon, 0); + t.end(); + }); + test('negative latitude wrapping - 361 degrees', function (t) { + var norm = wrap(-361, 0); + t.equal(norm.lat, -1); + t.equal(norm.lon, 0); + t.end(); + }); + test('negative latitude wrapping - 631 degrees', function (t) { + var norm = wrap(-631, 0); + t.equal(norm.lat, 89); + t.equal(norm.lon, 0); + t.end(); + }); + test('positive latitude wrapping - 721 degrees', function (t) { + var norm = wrap(721, 0); + t.equal(norm.lat, 1); + t.equal(norm.lon, 0); + t.end(); + }); +}; + +module.exports.tests.longitude_positive = function(test, common) { + test('positive longitude wrapping - 1 degree', function (t) { + var norm = wrap(0, 1); + t.equal(norm.lat, 0); + t.equal(norm.lon, 1); + t.end(); + }); + test('positive longitude wrapping - 181 degrees', function (t) { var norm = wrap(0, 181); t.equal(norm.lat, 0); t.equal(norm.lon, -179); t.end(); }); - test('positive longitude wrapping (double)', function (t) { - var norm = wrap(0, 541); + test('positive longitude wrapping - 271 degrees', function (t) { + var norm = wrap(0, 271); t.equal(norm.lat, 0); - t.equal(norm.lon, -179); + t.equal(norm.lon, -89); t.end(); }); - test('negative longitude wrapping', function (t) { - var norm = wrap(0, -181); + test('positive longitude wrapping - 361 degrees', function (t) { + var norm = wrap(0, 361); t.equal(norm.lat, 0); - t.equal(norm.lon, 179); + t.equal(norm.lon, 1); + t.end(); + }); + test('positive longitude wrapping - 631 degrees', function (t) { + var norm = wrap(0, 631); + t.equal(norm.lat, 0); + t.equal(norm.lon, -89); t.end(); }); - test('negative longitude wrapping (double)', function (t) { - var norm = wrap(0, -541); + test('positive longitude wrapping - 721 degrees', function (t) { + var norm = wrap(0, 721); + t.equal(norm.lat, 0); + t.equal(norm.lon, 1); + t.end(); + }); +}; + +module.exports.tests.longitude_negative = function(test, common) { + test('negative longitude wrapping - 1 degree', function (t) { + var norm = wrap(0, -1); + t.equal(norm.lat, 0); + t.equal(norm.lon, -1); + t.end(); + }); + test('negative longitude wrapping - 181 degrees', function (t) { + var norm = wrap(0, -181); t.equal(norm.lat, 0); t.equal(norm.lon, 179); t.end(); }); - test('positive latitudinal wrapping', function (t) { - var norm = wrap(91, 0); - t.equal(norm.lat, 89); - t.equal(norm.lon, 180); + test('negative longitude wrapping - 271 degrees', function (t) { + var norm = wrap(0, -271); + t.equal(norm.lat, 0); + t.equal(norm.lon, 89); t.end(); }); - test('positive latitudinal wrapping (double)', function (t) { - var norm = wrap(271, 0); - t.equal(norm.lat, 89); - t.equal(norm.lon, 180); + test('negative longitude wrapping - 361 degrees', function (t) { + var norm = wrap(0, -361); + t.equal(norm.lat, 0); + t.equal(norm.lon, -1); t.end(); }); - test('negative latitudinal wrapping', function (t) { - var norm = wrap(-91, 0); - t.equal(norm.lat, -89); - t.equal(norm.lon, 180); + test('negative longitude wrapping - 631 degrees', function (t) { + var norm = wrap(0, -631); + t.equal(norm.lat, 0); + t.equal(norm.lon, 89); t.end(); }); - test('negative latitudinal wrapping (double)', function (t) { - var norm = wrap(-271, 0); - t.equal(norm.lat, -89); - t.equal(norm.lon, 180); + test('negative longitude wrapping - 721 degrees', function (t) { + var norm = wrap(0, -721); + t.equal(norm.lat, 0); + t.equal(norm.lon, -1); t.end(); }); }; From 39e3156019c8969487a549f65b1108b793bd1c56 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Tue, 2 Aug 2016 12:51:56 +0200 Subject: [PATCH 3/3] typo --- test/unit/sanitiser/_geo_common.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/sanitiser/_geo_common.js b/test/unit/sanitiser/_geo_common.js index 93ec5d11..a7bff983 100644 --- a/test/unit/sanitiser/_geo_common.js +++ b/test/unit/sanitiser/_geo_common.js @@ -55,7 +55,7 @@ module.exports.tests.wrapping = function(test, common) { }; sanitize.sanitize_point( 'point', clean, params, false ); t.equal(clean['point.lat'], +88.9, 'equal to (+90 - 1.1)'); - t.equal(clean['point.lon'], -178.9, 'equal to (-180 - 1.1)'); // polar flip + t.equal(clean['point.lon'], -178.9, 'equal to (-180 + 1.1)'); // polar flip t.end(); }); test('negative latitudinal wrapping', function (t) {