diff --git a/middleware/confidenceScoreReverse.js b/middleware/confidenceScoreReverse.js new file mode 100644 index 00000000..462ea9d9 --- /dev/null +++ b/middleware/confidenceScoreReverse.js @@ -0,0 +1,68 @@ +var logger = require('pelias-logger').get('api'); +var _ = require('lodash'); + +// these are subjective terms, but wanted to add shortcuts to denote something +// about importance +var SCORES = { + EXACT: 1.0, + EXCELLENT: 0.9, + GOOD: 0.8, + OKAY: 0.7, + POOR: 0.6, + NONE: 0.5, + INVALID: 0.0 +}; + +var BUCKETS = { + _1_METER: 1, + _10_METERS: 10, + _100_METERS: 100, + _250_METERS: 250, + _1_KILOMETER: 1000 +}; + +function setup() { + return computeScores; +} + +function computeScores(req, res, next) { + // do nothing if no result data set + if (!res.data || !res.data) { + return next(); + } + + // loop through data items and determine confidence scores + res.data = res.data.map(computeConfidenceScore); + + next(); +} + +function computeConfidenceScore(hit) { + // non-number or invalid distance should be given confidence 0.0 + if (!_.isFinite(hit.distance) || hit.distance < 0) { + hit.confidence = SCORES.INVALID; + return hit; + } + + var distance = hit.distance * 1000.0; + + // figure out which range the distance lies in and assign confidence accordingly + if (distance < BUCKETS._1_METER) { + hit.confidence = SCORES.EXACT; + } else if (distance < BUCKETS._10_METERS) { + hit.confidence = SCORES.EXCELLENT; + } else if (distance < BUCKETS._100_METERS) { + hit.confidence = SCORES.GOOD; + } else if (distance < BUCKETS._250_METERS) { + hit.confidence = SCORES.OKAY; + } else if (distance < BUCKETS._1_KILOMETER) { + hit.confidence = SCORES.POOR; + } else { + hit.confidence = SCORES.NONE; + } + + return hit; + +} + +module.exports = setup; diff --git a/middleware/distance.js b/middleware/distance.js index 389b724b..eb1d7c21 100644 --- a/middleware/distance.js +++ b/middleware/distance.js @@ -1,4 +1,5 @@ var geolib = require('geolib'); +var check = require('check-types'); function setup() { @@ -13,14 +14,20 @@ function computeDistances(req, res, next) { return next(); } - if ( !(req.clean.hasOwnProperty('lat') && req.clean.hasOwnProperty('lon')) ) { + if (!(check.number(req.clean['point.lat']) && + check.number(req.clean['point.lon']))) { return next(); } + var point = { + latitude: req.clean['point.lat'], + longitude: req.clean['point.lon'] + }; + res.data.forEach(function (place) { // the result of getDistance is in meters, so convert to kilometers place.distance = geolib.getDistance( - { latitude: req.clean.lat, longitude: req.clean.lon }, + point, { latitude: place.center_point.lat, longitude: place.center_point.lon } ) / 1000; }); diff --git a/routes/v1.js b/routes/v1.js index a9b61c3d..215f1752 100644 --- a/routes/v1.js +++ b/routes/v1.js @@ -29,6 +29,7 @@ var controllers = { var postProc = { distances: require('../middleware/distance'), confidenceScores: require('../middleware/confidenceScore'), + confidenceScoresReverse: require('../middleware/confidenceScoreReverse'), renamePlacenames: require('../middleware/renamePlacenames'), geocodeJSON: require('../middleware/geocodeJSON'), sendJSON: require('../middleware/sendJSON') @@ -75,8 +76,10 @@ function addRoutes(app, peliasConfig) { sanitisers.reverse.middleware, middleware.types, controllers.search(undefined, reverseQuery), - // TODO: add confidence scores postProc.distances(), + // reverse confidence scoring depends on distance from origin + // so it must be calculated first + postProc.confidenceScoresReverse(), postProc.renamePlacenames(), postProc.geocodeJSON(peliasConfig, base), postProc.sendJSON diff --git a/test/unit/middleware/confidenceScoreReverse.js b/test/unit/middleware/confidenceScoreReverse.js new file mode 100644 index 00000000..450fd0db --- /dev/null +++ b/test/unit/middleware/confidenceScoreReverse.js @@ -0,0 +1,178 @@ +var confidenceScoreReverse = require('../../../middleware/confidenceScoreReverse')(); + +module.exports.tests = {}; + +module.exports.tests.confidenceScoreReverse = function(test, common) { + test('res without results should not throw exception', function(t) { + var res = {}; + + try { + confidenceScoreReverse(null, res, function() {}); + } + catch (e) { + t.fail('an exception should not have been thrown'); + } + finally { + t.end(); + } + + }); + + test('res.results without data should not throw exception', function(t) { + var res = { + results: {} + }; + + try { + confidenceScoreReverse(null, res, function() {}); + } + catch (e) { + t.fail('an exception should not have been thrown'); + } + finally { + t.end(); + } + + }); + + test('0m <= distance < 1m should be given score 1.0', function(t) { + var res = { + data: [ + { distance: 0.0000 / 1000.0 }, + { distance: 0.9999 / 1000.0 } + ] + }; + + confidenceScoreReverse(null, res, function() { + t.equal(res.data[0].confidence, 1.0, 'score should be exact confidence'); + t.equal(res.data[1].confidence, 1.0, 'score should be exact confidence'); + t.end(); + }); + + }); + + test('1m <= distance < 10m should be given score 0.9', function(t) { + var res = { + data: [ + { distance: 1.0000 / 1000.0 }, + { distance: 9.9999 / 1000.0 } + ] + }; + + confidenceScoreReverse(null, res, function() { + t.equal(res.data[0].confidence, 0.9, 'score should be excellent confidence'); + t.equal(res.data[1].confidence, 0.9, 'score should be excellent confidence'); + t.end(); + }); + + }); + + test('10m <= distance < 100m should be given score 0.8', function(t) { + var res = { + data: [ + { distance: 10.0000 / 1000.0 }, + { distance: 99.9999 / 1000.0 } + ] + }; + + confidenceScoreReverse(null, res, function() { + t.equal(res.data[0].confidence, 0.8, 'score should be good confidence'); + t.equal(res.data[1].confidence, 0.8, 'score should be good confidence'); + t.end(); + }); + + }); + + test('100m <= distance < 250m should be given score 0.7', function(t) { + var res = { + data: [ + { distance: 100.0000 / 1000.0 }, + { distance: 249.9999 / 1000.0 } + ] + }; + + confidenceScoreReverse(null, res, function() { + t.equal(res.data[0].confidence, 0.7, 'score should be okay confidence'); + t.equal(res.data[1].confidence, 0.7, 'score should be okay confidence'); + t.end(); + }); + + }); + + test('250m <= distance < 1000m should be given score 0.6', function(t) { + var res = { + data: [ + {distance: 250.0000 / 1000.0}, + {distance: 999.9999 / 1000.0} + ] + }; + + confidenceScoreReverse(null, res, function() { + t.equal(res.data[0].confidence, 0.6, 'score should be poor confidence'); + t.equal(res.data[1].confidence, 0.6, 'score should be poor confidence'); + t.end(); + }); + + }); + + test('distance >= 1000m should be given score 0.5', function(t) { + var res = { + data: [ + {distance: 1000.0 / 1000.0}, + {distance: 2000.0 / 1000.0} + ] + }; + + confidenceScoreReverse(null, res, function() { + t.equal(res.data[0].confidence, 0.5, 'score should be least confidence'); + t.equal(res.data[1].confidence, 0.5, 'score should be least confidence'); + t.end(); + }); + + }); + + test('distance < 0 (invalid) should be given score 0.0', function(t) { + var res = { + data: [ + { distance: -1.0000 / 1000.0 } + ] + }; + + confidenceScoreReverse(null, res, function() { + t.equal(res.data[0].confidence, 0.0, 'score should be 0.0 confidence'); + t.end(); + }); + + }); + + test('non-number-type (invalid) distance should be given score 0.0', function(t) { + var res = { + data: [ + {}, + {distance: []}, + {distance: {}}, + {distance: 'this is not a number'} + ] + }; + + confidenceScoreReverse(null, res, function() { + t.equal(res.data[0].confidence, 0.0, 'score should be 0.0 confidence'); + t.equal(res.data[1].confidence, 0.0, 'score should be 0.0 confidence'); + t.equal(res.data[2].confidence, 0.0, 'score should be 0.0 confidence'); + t.equal(res.data[3].confidence, 0.0, 'score should be 0.0 confidence'); + t.end(); + }); + + }); + +}; + +module.exports.all = function (tape, common) { + function test(name, testFunction) { + return tape('[middleware] confidenceScoreReverse: ' + name, testFunction); + } + + for( var testCase in module.exports.tests ){ + module.exports.tests[testCase](test, common); + } +}; diff --git a/test/unit/middleware/distance.js b/test/unit/middleware/distance.js index 81cf6871..da868e45 100644 --- a/test/unit/middleware/distance.js +++ b/test/unit/middleware/distance.js @@ -6,8 +6,8 @@ module.exports.tests.computeDistance = function(test, common) { test('valid lat/lon and results', function(t) { var req = { clean: { - lat: 45, - lon: -77 + 'point.lat': 45, + 'point.lon': -77 } }; var res = { diff --git a/test/unit/run.js b/test/unit/run.js index 88590e4f..efa0c4de 100644 --- a/test/unit/run.js +++ b/test/unit/run.js @@ -28,6 +28,7 @@ var tests = [ require('./helper/types'), require('./sanitiser/_geo_common'), require('./middleware/distance'), + require('./middleware/confidenceScoreReverse'), require('./sanitiser/_size'), ];