diff --git a/README.md b/README.md index fb933dd9..4f207f4c 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ var geocoder = NodeGeocoder({ * `google` : GoogleGeocoder. Supports address geocoding and reverse geocoding. Use `options.clientId`and `options.apiKey`(privateKey) for business licence. You can also use `options.language` and `options.region` to specify language and region, respectively. Note that 'https' is required when using an apiKey * `here` : HereGeocoder. Supports address geocoding and reverse geocoding. You must specify `options.appId` and `options.appCode` with your license keys. You can also use `options.language`, `options.politicalView` ([read about political views here](https://developer.here.com/rest-apis/documentation/geocoder/topics/political-views.html)), `options.country`, and `options.state`. +* `here with suggestions`: HereGeocoderWithSuggestions. The here geocoder with suggestions to a given search term. * `freegeoip` : FreegeoipGeocoder. Supports IP geocoding * `datasciencetoolkit` : DataScienceToolkitGeocoder. Supports IPv4 geocoding and address geocoding. Use `options.host` to specify a local instance * `openstreetmap` : OpenStreetMapGeocoder. Supports address geocoding and reverse geocoding. You can use `options.language` and `options.email` to specify a language and a contact email address. diff --git a/lib/geocoder/heregeocoder.js b/lib/geocoder/heregeocoder.js index 600981cf..35ad0c18 100644 --- a/lib/geocoder/heregeocoder.js +++ b/lib/geocoder/heregeocoder.js @@ -53,6 +53,9 @@ HereGeocoder.prototype._geocode = function (value, callback) { params.postalcode = value.zipcode; } params.searchtext = value.address; + } + else if (value.locationId) { + params.locationId = value.locationId; } else { params.searchtext = value; } diff --git a/lib/geocoder/heregeocoderwithsuggestions.js b/lib/geocoder/heregeocoderwithsuggestions.js new file mode 100644 index 00000000..5711a007 --- /dev/null +++ b/lib/geocoder/heregeocoderwithsuggestions.js @@ -0,0 +1,80 @@ +/** + * Created by caliskan on 15.02.17. + */ + +var util = require('util'), + HereGeocoder = require('./heregeocoder'); + +/** + * Constructor + * @param httpAdapter Http Adapter + * @param options Options (appId, appCode, language, politicalView, country, state) + */ +var HereGeocoderWithSuggestions = function HereGeocoderWithSuggestions(httpAdapter, options) { + this.options = ['appId', 'appCode', 'language', 'politicalView', 'country', 'state']; + + HereGeocoderWithSuggestions.super_.call(this, httpAdapter, options); +}; + +util.inherits(HereGeocoderWithSuggestions, HereGeocoder); + +// Here geocoder suggestion API endpoint +HereGeocoderWithSuggestions.prototype._suggestionsEndpoint = 'https://autocomplete.geocoder.api.here.com/6.2/suggest.json'; + +/** + * Geocode + * @param value Value to geocode + * @param callback Callback method + */ +HereGeocoderWithSuggestions.prototype._geocode = function (value, callback) { + var params = this._prepareQueryString(); + + if (value.query) { + params.query = value.query; + if (value.country) { + params.country = value.country; + } + } else { + params.query = value; + } + + var self = this; + var geocodeResults = []; + geocodeResults.raw = []; + var suggestionsLength = 0; + + // first get some suggestions + this.httpAdapter.get(this._suggestionsEndpoint, params, getSuggestions); + + function getSuggestions (err, result) { + if (err) { + return callback(err, result); + } else { + suggestionsLength = result.suggestions.length; + + if (0 === suggestionsLength) { + return callback(false, geocodeResults); + } + + // geocode each suggestion with its locationId, so that lat/lng is available + result.suggestions.forEach(function (item) { + HereGeocoder.prototype._geocode.call(self, {locationId: item.locationId}, formatSuggestions); + }); + } + } + + function formatSuggestions (err, result) { + if (err) { + return callback(err, result); + } else { + geocodeResults.push(result[0]); + geocodeResults.raw.push(result.raw); + + if (geocodeResults.length === suggestionsLength) { + return callback(false, geocodeResults); + } + } + } +} + +module.exports = HereGeocoderWithSuggestions; diff --git a/lib/geocoder/locationiqgeocoder.js b/lib/geocoder/locationiqgeocoder.js index 066c5c43..3874b9ba 100644 --- a/lib/geocoder/locationiqgeocoder.js +++ b/lib/geocoder/locationiqgeocoder.js @@ -69,11 +69,14 @@ LocationIQGeocoder.prototype._geocode = function(value, callback) { // when there’s no err thrown here the resulting array object always // seemes to be defined but empty so no need to check for // responseData.error for now - - var results = responseData.map(this._formatResult).filter(function(result) { - return result.longitude && result.latitude; - }); - results.raw = responseData; + // add check if the array is not empty, as it returns an empty array from time to time + var results = []; + if (responseData.length && responseData.length > 0) { + results = responseData.map(this._formatResult).filter(function(result) { + return result.longitude && result.latitude; + }); + results.raw = responseData; + } callback(false, results); }.bind(this)); diff --git a/lib/geocoderfactory.js b/lib/geocoderfactory.js index 6f3b4f87..1d238c8e 100644 --- a/lib/geocoderfactory.js +++ b/lib/geocoderfactory.js @@ -9,6 +9,7 @@ var RequestAdapter = require('./httpadapter/requestadapter.js'); var GoogleGeocoder = require('./geocoder/googlegeocoder.js'); var HereGeocoder = require('./geocoder/heregeocoder.js'); +var HereGeocoderWithSuggestions = require('./geocoder/heregeocoderwithsuggestions.js'); var AGOLGeocoder = require('./geocoder/agolgeocoder.js'); var FreegeoipGeocoder = require('./geocoder/freegeoipgeocoder.js'); var DataScienceToolkitGeocoder = require('./geocoder/datasciencetoolkitgeocoder.js'); @@ -58,6 +59,9 @@ var GeocoderFactory = { if (geocoderName === 'here') { return new HereGeocoder(adapter, {appId: extra.appId, appCode: extra.appCode, language: extra.language, politicalView: extra.politicalView, country: extra.country, state: extra.state}); } + if (geocoderName === 'herewithsuggestions') { + return new HereGeocoderWithSuggestions(adapter, {appId: extra.appId, appCode: extra.appCode, language: extra.language, politicalView: extra.politicalView, country: extra.country, state: extra.state}); + } if (geocoderName === 'agol') { return new AGOLGeocoder(adapter, {client_id: extra.client_id, client_secret: extra.client_secret}); } diff --git a/test/geocoder/heregeocoderwithsuggestions.js b/test/geocoder/heregeocoderwithsuggestions.js new file mode 100644 index 00000000..0c0bb4e5 --- /dev/null +++ b/test/geocoder/heregeocoderwithsuggestions.js @@ -0,0 +1,385 @@ +/** + * Created by caliskan on 22.02.17. + */ + +(function() { + var chai = require('chai'), + should = chai.should(), + expect = chai.expect, + sinon = require('sinon'); + + var HereGeocoderWithSuggestions = require('../../lib/geocoder/heregeocoderwithsuggestions.js'); + var HttpAdapter = require('../../lib/httpadapter/httpadapter.js'); + + var mockedHttpAdapter = { + get: function() { + return {}; + }, + supportsHttps: function() { + return true; + } + }; + + describe('HereGeocoderWithSuggestions', function() { + + describe('#constructor' , function() { + it('an http adapter must be set', function() { + expect(function() {new HereGeocoderWithSuggestions();}).to.throw(Error, 'HereGeocoderWithSuggestions need an httpAdapter'); + }); + + it('requires appId and appCode to be specified', function() { + expect(function() {new HereGeocoderWithSuggestions(mockedHttpAdapter, {});}).to.throw(Error, 'You must specify appId and appCode to use Here Geocoder'); + expect(function() {new HereGeocoderWithSuggestions(mockedHttpAdapter, {appId: 'APP_ID'});}).to.throw(Error, 'You must specify appId and appCode to use Here Geocoder'); + expect(function() {new HereGeocoderWithSuggestions(mockedHttpAdapter, {appCode: 'APP_CODE'});}).to.throw(Error, 'You must specify appId and appCode to use Here Geocoder'); + }); + + it('Should be an instance of HereGeocoderWithSuggestions if an http adapter, appId, and appCode are provided', function() { + var hereAdapter = new HereGeocoderWithSuggestions(mockedHttpAdapter, {appId: 'APP_ID', appCode: 'APP_CODE'}); + + hereAdapter.should.be.instanceof(HereGeocoderWithSuggestions); + }); + }); + + describe('#geocode' , function() { + it('Should not accept IPv4', function () { + + var hereAdapter = new HereGeocoderWithSuggestions(mockedHttpAdapter, {appId: 'APP_ID', appCode: 'APP_CODE'}); + + expect(function () { + hereAdapter.geocode('127.0.0.1'); + }).to.throw(Error, 'HereGeocoderWithSuggestions does not support geocoding IPv4'); + + }); + + it('Should not accept IPv6', function () { + + var hereAdapter = new HereGeocoderWithSuggestions(mockedHttpAdapter, {appId: 'APP_ID', appCode: 'APP_CODE'}); + + expect(function () { + hereAdapter.geocode('2001:0db8:0000:85a3:0000:0000:ac1f:8001'); + }).to.throw(Error, 'HereGeocoderWithSuggestions does not support geocoding IPv6'); + + }); + + it('Should call httpAdapter get method', function () { + var mock = sinon.mock(mockedHttpAdapter); + mock.expects('get').withArgs('https://autocomplete.geocoder.cit.api.here.com/6.2/suggest.json', { + query: "1 champs élysée Paris", + app_code: "APP_CODE", + app_id: "APP_ID", + additionaldata: "Country2,true", + gen: 8 + }).once().returns({ + then: function () { + } + }); + + var hereAdapter = new HereGeocoderWithSuggestions(mockedHttpAdapter, {appId: 'APP_ID', appCode: 'APP_CODE'}); + + hereAdapter.geocode('1 champs élysée Paris'); + + mock.verify(); + }); + + it('Should call httpAdapter get method with language if specified', function () { + var mock = sinon.mock(mockedHttpAdapter); + mock.expects('get').withArgs('https://autocomplete.geocoder.cit.api.here.com/6.2/suggest.json', { + query: "1 champs élysée Paris", + language: "en", + app_code: "APP_CODE", + app_id: "APP_ID", + additionaldata: "Country2,true", + gen: 8 + }).once().returns({ + then: function () { + } + }); + + var hereAdapter = new HereGeocoderWithSuggestions(mockedHttpAdapter, {appId: 'APP_ID', appCode: 'APP_CODE', language: 'en'}); + + hereAdapter.geocode('1 champs élysée Paris'); + + mock.verify(); + }); + + it('Should call httpAdapter get method with politicalView if specified', function () { + var mock = sinon.mock(mockedHttpAdapter); + mock.expects('get').withArgs('https://autocomplete.geocoder.cit.api.here.com/6.2/suggest.json', { + query: "1 champs élysée Paris", + politicalview: "GRE", + app_code: "APP_CODE", + app_id: "APP_ID", + additionaldata: "Country2,true", + gen: 8 + }).once().returns({ + then: function () { + } + }); + + var hereAdapter = new HereGeocoderWithSuggestions(mockedHttpAdapter, { + appId: 'APP_ID', + appCode: 'APP_CODE', + politicalView: 'GRE' + }); + + hereAdapter.geocode('1 champs élysée Paris'); + + mock.verify(); + }); + + it('Should call httpAdapter get method with country if specified', function () { + var mock = sinon.mock(mockedHttpAdapter); + mock.expects('get').withArgs('https://autocomplete.geocoder.cit.api.here.com/6.2/suggest.json', { + query: "1 champs élysée Paris", + country: "FR", + app_code: "APP_CODE", + app_id: "APP_ID", + additionaldata: "Country2,true", + gen: 8 + }).once().returns({ + then: function () { + } + }); + + var hereAdapter = new HereGeocoderWithSuggestions(mockedHttpAdapter, {appId: 'APP_ID', appCode: 'APP_CODE', country: 'FR'}); + + hereAdapter.geocode('1 champs élysée Paris'); + + mock.verify(); + }); + + it('Should call httpAdapter get method with state if specified', function () { + var mock = sinon.mock(mockedHttpAdapter); + mock.expects('get').withArgs('https://autocomplete.geocoder.cit.api.here.com/6.2/suggest.json', { + query: "1 champs élysée Paris", + state: "Île-de-France", + app_code: "APP_CODE", + app_id: "APP_ID", + additionaldata: "Country2,true", + gen: 8 + }).once().returns({ + then: function () { + } + }); + + var hereAdapter = new HereGeocoderWithSuggestions(mockedHttpAdapter, { + appId: 'APP_ID', + appCode: 'APP_CODE', + state: 'Île-de-France' + }); + + hereAdapter.geocode('1 champs élysée Paris'); + + mock.verify(); + }); + + it('Should call httpAdapter get method with changed country if called with object containing country', function () { + var mock = sinon.mock(mockedHttpAdapter); + mock.expects('get').withArgs('https://autocomplete.geocoder.cit.api.here.com/6.2/suggest.json', { + query: "Kaiserswerther Str 10, Berlin", + country: "DE", + app_code: "APP_CODE", + app_id: "APP_ID", + additionaldata: "Country2,true", + gen: 8 + }).once().returns({ + then: function () { + } + }); + + var hereAdapter = new HereGeocoderWithSuggestions(mockedHttpAdapter, { + appId: 'APP_ID', + appCode: 'APP_CODE', + country: 'FR' + }); + + hereAdapter.geocode({ + query: 'Kaiserswerther Str 10, Berlin', + country: 'DE' + }); + + mock.verify(); + }); + + it('Should return geocoded address', function (done) { + var mock = sinon.mock(mockedHttpAdapter); + mock.expects('get').thrice().callsArgWith(2, false, { suggestions: + [ { label: 'Deutschland, Bremen', + language: 'de', + countryCode: 'DEU', + locationId: 'NT_SOWuYQhksDF8rxc6HnhIMA', + address: [Object], + matchLevel: 'state' }, + { label: 'Deutschland, Bremen, Bremen, Bremen', + language: 'de', + countryCode: 'DEU', + locationId: 'NT_p0jRnQzY27JW0p7z9sutaA', + address: [Object], + matchLevel: 'city' } + ] } + ).onCall(1).callsArgWith(2, false, { Response: + { MetaInfo: { Timestamp: '2015-08-21T07:53:51.042+0000' }, + View: + [ { _type: 'SearchResultsViewType', + ViewId: 0, + Result: + [ { Relevance: 1, + MatchLevel: 'houseNumber', + MatchQuality: + { City: 1, + Street: [ 1 ], + HouseNumber: 1 }, + MatchType: 'pointAddress', + Location: + { LocationId: 'NT_l-pW8M-6wY8Ylp8zHdjc7C_xAD', + LocationType: 'address', + DisplayPosition: { Latitude: 52.44841, Longitude: 13.28755 }, + NavigationPosition: [ { Latitude: 52.44854, Longitude: 13.2874 } ], + MapView: + { TopLeft: + { Latitude: 52.4495342, + Longitude: 13.2857055 }, + BottomRight: + { Latitude: 52.4472858, + Longitude: 13.2893945 } }, + Address: + { Label: 'Kaiserswerther Straße 10, 14195 Berlin, Deutschland', + Country: 'DEU', + State: 'Berlin', + County: 'Berlin', + City: 'Berlin', + District: 'Dahlem', + Street: 'Kaiserswerther Straße', + HouseNumber: '10', + PostalCode: '14195', + AdditionalData: + [ { value: 'DE', key: 'Country2' }, + { value: 'Deutschland', key: 'CountryName' }, + { value: 'Berlin', key: 'StateName' }, + { value: 'Berlin', key: 'CountyName' } ] } } } ] } ] } } + ).onCall(2).callsArgWith(2, false, { Response: + { MetaInfo: { Timestamp: '2015-08-21T07:53:51.042+0000' }, + View: + [ { _type: 'SearchResultsViewType', + ViewId: 0, + Result: + [ { Relevance: 1, + MatchLevel: 'houseNumber', + MatchQuality: + { City: 1, + Street: [ 1 ], + HouseNumber: 1 }, + MatchType: 'pointAddress', + Location: + { LocationId: 'NT_l-pW8M-6wY8Ylp8zHdjc7C_xAD', + LocationType: 'address', + DisplayPosition: { Latitude: 52.44841, Longitude: 13.28755 }, + NavigationPosition: [ { Latitude: 52.44854, Longitude: 13.2874 } ], + MapView: + { TopLeft: + { Latitude: 52.4495342, + Longitude: 13.2857055 }, + BottomRight: + { Latitude: 52.4472858, + Longitude: 13.2893945 } }, + Address: + { Label: 'Kaiserswerther Straße 10, 14195 Berlin, Deutschland', + Country: 'DEU', + State: 'Berlin', + County: 'Berlin', + City: 'Berlin', + District: 'Dahlem', + Street: 'Kaiserswerther Straße', + HouseNumber: '10', + PostalCode: '14195', + AdditionalData: + [ { value: 'DE', key: 'Country2' }, + { value: 'Deutschland', key: 'CountryName' }, + { value: 'Berlin', key: 'StateName' }, + { value: 'Berlin', key: 'CountyName' } ] } } } ] } ] } } + ); + + var hereAdapter = new HereGeocoderWithSuggestions(mockedHttpAdapter, {appId: 'APP_ID', appCode: 'APP_CODE'}); + + hereAdapter.geocode('Kaiserswerther Str 10, Berlin', function (err, results) { + err.should.to.equal(false); + + for (var index = 0; index < results.raw.length; ++index) { + results[ index ].should.to.deep.equal({ + formattedAddress: 'Kaiserswerther Straße 10, 14195 Berlin, Deutschland', + latitude: 52.44841, + longitude: 13.28755, + country: 'Deutschland', + countryCode: 'DE', + state: 'Berlin', + county: 'Berlin', + city: 'Berlin', + zipcode: '14195', + district: 'Dahlem', + streetName: 'Kaiserswerther Straße', + streetNumber: '10', + building: null, + extra: { herePlaceId: 'NT_l-pW8M-6wY8Ylp8zHdjc7C_xAD', confidence: 1 }, + administrativeLevels: { level1long: 'Berlin', level2long: 'Berlin' } + }); + + results.raw[ index ].should.deep.equal({ + Response: { + MetaInfo: {Timestamp: '2015-08-21T07:53:51.042+0000'}, + View: [{ + _type: 'SearchResultsViewType', + ViewId: 0, + Result: [{ + Relevance: 1, + MatchLevel: 'houseNumber', + MatchQuality: { + City: 1, + Street: [1], + HouseNumber: 1 + }, + MatchType: 'pointAddress', + Location: { + LocationId: 'NT_l-pW8M-6wY8Ylp8zHdjc7C_xAD', + LocationType: 'address', + DisplayPosition: {Latitude: 52.44841, Longitude: 13.28755}, + NavigationPosition: [{Latitude: 52.44854, Longitude: 13.2874}], + MapView: { + TopLeft: { + Latitude: 52.4495342, + Longitude: 13.2857055 + }, + BottomRight: { + Latitude: 52.4472858, + Longitude: 13.2893945 + } + }, + Address: { + Label: 'Kaiserswerther Straße 10, 14195 Berlin, Deutschland', + Country: 'DEU', + State: 'Berlin', + County: 'Berlin', + City: 'Berlin', + District: 'Dahlem', + Street: 'Kaiserswerther Straße', + HouseNumber: '10', + PostalCode: '14195', + AdditionalData: [{value: 'DE', key: 'Country2'}, + {value: 'Deutschland', key: 'CountryName'}, + {value: 'Berlin', key: 'StateName'}, + {value: 'Berlin', key: 'CountyName'}] + } + } + }] + }] + } + } + ); + } + + mock.verify(); + done(); + }); + }); + }); + }); +})();