diff --git a/index.js b/index.js index c86efa6..ece135f 100644 --- a/index.js +++ b/index.js @@ -10,7 +10,7 @@ * Version */ -var version = '0.2.1'; +var version = '0.2.3'; /** @@ -57,23 +57,22 @@ Geocoder.prototype = { * @api public */ - geocode: function ( loc, cbk, opts ) { + geocode: function ( loc, opts, cbk ) { if ( ! loc ) { return cbk( new Error( "Geocoder.geocode requires a location.") ); } - return this.providerObj.geocode(this.providerOpts, loc, cbk, opts); + return this.providerObj.geocode(this.providerOpts, loc, opts, cbk); }, - reverseGeocode: function ( lat, lng, cbk, opts ) { + reverseGeocode: function ( lat, lng, opts, cbk ) { if ( !lat || !lng ) { return cbk( new Error( "Geocoder.reverseGeocode requires a latitude and longitude." ) ); } - return this.providerObj.reverseGeocode(this.providerOpts, lat, lng, cbk, opts ); - + return this.providerObj.reverseGeocode(this.providerOpts, lat, lng, opts, cbk); }, /** diff --git a/package.json b/package.json index 2b4a34f..3555c0c 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,24 @@ { "name": "geocoder", - "description": "Geocoding through Google's Developer API", - "version": "0.2.2", - "main": "./index.js", "description": "node wrapper around google's geocoder api", + "version": "0.2.4", + "main": "./index.js", "author": "Stephen Wyatt Bush ", - "repository" : "git://github.com/wyattdanger/geocoder", - "homepage" : "https://github.com/wyattdanger/geocoder", - "keywords" : [ "google", "geocode", "geonames", "reverse geocode" ], + "repository": "git://github.com/wyattdanger/geocoder", + "homepage": "https://github.com/wyattdanger/geocoder", + "keywords": [ + "google", + "geocode", + "geonames", + "reverse geocode" + ], "license": { "type": "Apachev2", "url": "http://www.apache.org/licenses/LICENSE-2.0" }, - "dependencies" : { - "underscore" : "1.3.3", - "request":"2.11.1" + "dependencies": { + "extend": "^3.0.0", + "request": "2.11.1" }, "optionalDependencies": { "xml2js": "0.2.0" diff --git a/providers/geonames.js b/providers/geonames.js index 6336fd1..5ab604f 100644 --- a/providers/geonames.js +++ b/providers/geonames.js @@ -2,11 +2,11 @@ // xml2js is optional because only needed for geonames support var xml2js = require("xml2js"); var request = require("request"); -var _ = require('underscore'); +var extend = require('extend'); -exports.geocode = function ( providerOpts, loc, cbk, opts ) { +exports.geocode = function ( providerOpts, loc, opts, cbk ) { - var options = _.extend({q: loc, maxRows: 10, username:providerOpts.username||"demo" }, opts || {}); + var options = extend({q: loc, maxRows: 10, username:providerOpts.username||"demo" }, opts || {}); request({ uri:"http://api.geonames.org/searchJSON", @@ -24,9 +24,9 @@ exports.geocode = function ( providerOpts, loc, cbk, opts ) { }); }; -exports.reverseGeocode = function ( providerOpts, lat, lng, cbk, opts ) { +exports.reverseGeocode = function ( providerOpts, lat, lng, opts, cbk ) { - var options = _.extend({lat:lat, lng:lng, username:providerOpts.username||"demo" }, opts || {}); + var options = extend({lat:lat, lng:lng, username:providerOpts.username||"demo" }, opts || {}); request({ uri:"http://api.geonames.org/extendedFindNearby", @@ -101,11 +101,11 @@ exports.reverseGeocode = function ( providerOpts, lat, lng, cbk, opts ) { "types":[ "country" ] }); - if (a.lat && typeof a.lat[0]=="string") + /*if (a.lat && typeof a.lat[0]=="string") googlejson.results[0].geometry.location = { "lat":parseFloat(a.lat[0]), "lng":parseFloat(a.lng[0]) - } + }*/ } if (result.geonames.geoname) { diff --git a/providers/google.js b/providers/google.js index 80f8931..85455a2 100644 --- a/providers/google.js +++ b/providers/google.js @@ -1,10 +1,10 @@ var request = require("request"); -var _ = require('underscore'); +var extend = require('extend'); -exports.geocode = function ( providerOpts, loc, cbk, opts ) { +exports.geocode = function ( providerOpts, loc, opts, cbk ) { - var options = _.extend({sensor: false, address: loc}, opts || {}); - var uri = "http" + ( options.key ? "s" : "" ) + "://maps.googleapis.com/maps/api/geocode/json" + var options = extend({address: loc}, opts || {}); + var uri = "http" + ( options.key ? "s" : "" ) + "://maps.googleapis.com/maps/api/geocode/json"; request({ uri: uri, qs:options @@ -21,10 +21,10 @@ exports.geocode = function ( providerOpts, loc, cbk, opts ) { }); }; -exports.reverseGeocode = function ( providerOpts, lat, lng, cbk, opts ) { +exports.reverseGeocode = function ( providerOpts, lat, lng, opts, cbk ) { - var options = _.extend({sensor: false, latlng: lat + ',' + lng}, opts || {}); - var uri = "http" + ( options.key ? "s" : "" ) + "://maps.googleapis.com/maps/api/geocode/json" + var options = extend({latlng: lat + ',' + lng}, opts || {}); + var uri = "http" + ( options.key ? "s" : "" ) + "://maps.googleapis.com/maps/api/geocode/json"; request({ uri:uri, @@ -38,6 +38,12 @@ exports.reverseGeocode = function ( providerOpts, lat, lng, cbk, opts ) { cbk(err); return; } + + if(Array.isArray(result.results) && result.results.length > 0 && result.results[0].geometry && result.results[0].geometry.location) { + result.results[0].geometry.location.lat = parseFloat(lat); + result.results[0].geometry.location.lng = parseFloat(lng); + } + cbk(null,result); }); diff --git a/providers/here.js b/providers/here.js new file mode 100644 index 0000000..d56cf1d --- /dev/null +++ b/providers/here.js @@ -0,0 +1,144 @@ +var request = require("request"); +var extend = require('extend'); + +exports.geocode = function ( providerOpts, loc, opts, cbk ) { + + var options = extend({searchtext: loc, gen:"9", app_id:providerOpts.appid||"[yourappidhere]", app_code: providerOpts.appcode||"[yourappcodehere]" }, opts || {}); + + request({ + uri:"http://geocoder.api.here.com/6.2/geocode.json", + qs:options + }, function(err,resp,body) { + if (err) return cbk(err); + var result; + try { + result = JSON.parse(body); + } catch (err) { + cbk(err); + return; + } + cbk(null,result); + }); +}; + +// Here api https://developer.here.com/rest-apis/documentation/geocoder +exports.reverseGeocode = function ( providerOpts, lat, lng, opts, cbk ) { + + var options = extend({pos:lat+","+lng, mode:"trackPosition", gen:"9", app_id:providerOpts.appid||"[yourappidhere]", app_code: providerOpts.appcode||"[yourappcodehere]"}, opts || {}); + + request({ + uri:"http://reverse.geocoder.api.here.com/6.2/reversegeocode.json", + qs:options, + headers: { + 'User-Agent': 'request' + } + }, function(err,resp,body) { + + // console.log("[GEOCODER Here API] uri:", "http://reverse.geocoder.api.here.com/6.2/reversegeocode.json"); + // console.log("[GEOCODER Here API] options:", JSON.stringify(options)); + // console.log("[GEOCODER Here API] body:", body); + + if (err) return cbk(err); + + var result; + try { + result = JSON.parse(body); + } catch (err) { + cbk(err); + return; + } + + var view = result.Response.View[0]; + if(!view || !view.Result || !Array.isArray(view.Result) || !view.Result[0] || !view.Result[0].Location) { + cbk(true, result); + return; + } + + // Transform Here structure into something that looks like Google's JSON output + // https://developers.google.com/maps/documentation/geocoding/#JSON + var googlejson = { + "status":"OK", + "results":[ + { + "address_components":[], + "formatted_address":"", + "geometry":{ + "location":{ + "lat":lat, + "lng":lng + } + } + } + ] + }; + + var location = view.Result[0].Location; + + if(location.Address) { + + var a = location.Address; + + var additionalData = {}; + + a.AdditionalData.map(function(obj) { + additionalData[obj.key] = obj.value; + }); + + if (a.HouseNumber || a.Building) + googlejson.results[0].address_components.push({ + "long_name":a.HouseNumber || a.Building, + "short_name":a.HouseNumber || a.Building, + "types":["street_number"] + }); + + if (a.Street) + googlejson.results[0].address_components.push({ + "long_name":a.Street, + "short_name":a.Street, + "types":["route"] + }); + + if (a.City || a.District) + googlejson.results[0].address_components.push({ + "long_name": a.City || a.District, + "short_name": a.City || a.District, + "types":["locality", "political"] + }); + + if (a.State && typeof a.State=="string") + googlejson.results[0].address_components.push({ + "long_name":additionalData.StateName || a.State, + "short_name":a.State, + "types":[ "administrative_area_level_1", "political" ] + }); + + if (a.County && typeof a.County=="string") + googlejson.results[0].address_components.push({ + "long_name":additionalData.CountyName || a.County, + "short_name":a.County, + "types":[ "administrative_area_level_2", "political" ] + }); + + if (a.Country && typeof a.Country=="string") + googlejson.results[0].address_components.push({ + "long_name":additionalData.CountryName, + "short_name":(additionalData.Country2 || a.Country.substring(0,2)).toUpperCase(), + "types":[ "country", "political" ] + }); + + /*if (location.DisplayPosition.Latitude && typeof location.DisplayPosition.Latitude=="string") + googlejson.results[0].geometry.location = { + "lat":parseFloat(location.DisplayPosition.Latitude), + "lng":parseFloat(location.DisplayPosition.Longitude) + }*/ + + // Make a formatted address as well as we can + googlejson.results[0].formatted_address = a.Label; + } + + // console.log("[GEOCODER Here API], calling callback w/", JSON.stringify(googlejson)); + + cbk(null, googlejson); + }); + +}; diff --git a/providers/nominatim.js b/providers/nominatim.js new file mode 100644 index 0000000..2574937 --- /dev/null +++ b/providers/nominatim.js @@ -0,0 +1,133 @@ +var request = require("request"); +var extend = require('extend'); + +exports.geocode = function ( providerOpts, loc, opts, cbk ) { + + var options = extend({q: loc, format: "json", addressdetails:"1" }, opts || {}); + + request({ + uri:"http://nominatim.openstreetmap.org/search", + qs:options + }, function(err,resp,body) { + if (err) return cbk(err); + var result; + try { + result = JSON.parse(body); + } catch (err) { + cbk(err); + return; + } + cbk(null,result); + }); +}; + +// Nominatim api http://wiki.openstreetmap.org/wiki/Nominatim +exports.reverseGeocode = function ( providerOpts, lat, lng, opts, cbk ) { + + var options = extend({lat:lat, lon:lng, format:"json", addressdetails:"1"}, opts || {}); + + request({ + uri:"http://nominatim.openstreetmap.org/reverse", + qs:options, + headers: { + 'User-Agent': 'request' + } + }, function(err,resp,body) { + + // console.log("[GEOCODER Nominatim API] uri:", "http://nominatim.openstreetmap.org/reverse"); + // console.log("[GEOCODER Nominatim API] options:", JSON.stringify(options)); + // console.log("[GEOCODER Nominatim API] body:", body); + + if (err) return cbk(err); + + var result; + try { + result = JSON.parse(body); + } catch (err) { + cbk(err); + return; + } + + // Transform Nominatim structure into something that looks like Google's JSON output + // https://developers.google.com/maps/documentation/geocoding/#JSON + var googlejson = { + "status":"OK", + "results":[ + { + "address_components":[], + "formatted_address":"", + "geometry":{ + "location":{ + "lat":lat, + "lng":lng + } + }, + "place_id": "" + } + ] + }; + + if(result.address) { + var a = result.address; + + if (a.house_number || a.building) + googlejson.results[0].address_components.push({ + "long_name":a.house_number || a.building, + "short_name":a.house_number || a.building, + "types":["street_number"] + }); + + if (a.road || a.cycleway) + googlejson.results[0].address_components.push({ + "long_name":a.road || a.cycleway, + "short_name":a.road || a.cycleway, + "types":["route"] + }); + + if (a.city || a.town || a.village || a.hamlet) + googlejson.results[0].address_components.push({ + "long_name": a.city || a.town || a.village || a.hamlet, + "short_name": a.city || a.town || a.village || a.hamlet, + "types":["locality", "political"] + }); + + if (a.state && typeof a.state=="string") + googlejson.results[0].address_components.push({ + "long_name":a.state, + "short_name":a.state, + "types":[ "administrative_area_level_1", "political" ] + }); + + if (a.county && typeof a.county=="string") + googlejson.results[0].address_components.push({ + "long_name":a.county, + "short_name":a.county, + "types":[ "administrative_area_level_2", "political" ] + }); + + if (a.country && a.country_code && typeof a.country=="string") + googlejson.results[0].address_components.push({ + "long_name":a.country, + "short_name":a.country_code.toUpperCase(), + "types":[ "country", "political" ] + }); + + /*if (result.lat && typeof a.lat=="string") + googlejson.results[0].geometry.location = { + "lat":parseFloat(result.lat), + "lng":parseFloat(result.lon) + }*/ + } + + // Make a formatted address as well as we can + googlejson.results[0].formatted_address = result.display_name; + + // Set place id + googlejson.results[0].place_id = result.place_id; + + // console.log("[GEOCODER Nominatim API], calling callback w/", JSON.stringify(googlejson)); + + cbk(null, googlejson); + }); + +}; diff --git a/providers/yahoo.js b/providers/yahoo.js index 639af4f..796b5f3 100644 --- a/providers/yahoo.js +++ b/providers/yahoo.js @@ -1,11 +1,9 @@ -// xml2js is optional because only needed for geonames support -var xml2js = require("xml2js"); var request = require("request"); -var _ = require('underscore'); +var extend = require('extend'); -exports.geocode = function ( providerOpts, loc, cbk, opts ) { +exports.geocode = function ( providerOpts, loc, opts, cbk ) { - var options = _.extend({q: loc, flags: "J", appid:providerOpts.appid||"[yourappidhere]" }, opts || {}); + var options = extend({q: loc, flags: "J", appid:providerOpts.appid||"[yourappidhere]" }, opts || {}); request({ uri:"http://where.yahooapis.com/geocode", @@ -24,9 +22,9 @@ exports.geocode = function ( providerOpts, loc, cbk, opts ) { }; // yahoo placefinder api http://developer.yahoo.com/geo/placefinder/guide/ -exports.reverseGeocode = function ( providerOpts, lat, lng, cbk, opts ) { +exports.reverseGeocode = function ( providerOpts, lat, lng, opts, cbk ) { - var options = _.extend({q: lat+", "+lng, gflags:"R", flags: "J", appid:providerOpts.appid||"[yourappidhere]" }, opts || {}); + var options = extend({q: lat+", "+lng, gflags:"R", flags: "J", appid:providerOpts.appid||"[yourappidhere]" }, opts || {}); request({ uri:"http://where.yahooapis.com/geocode", @@ -133,11 +131,11 @@ exports.reverseGeocode = function ( providerOpts, lat, lng, cbk, opts ) { "types":[ "postal_code" ] }); - if (a.latitude) + /*if (a.latitude) googlejson.results[0].geometry.location = { "lat":parseFloat(a.latitude), "lng":parseFloat(a.longitude) - }; + };*/ // Make a formatted address as well as we can var formatted = []; diff --git a/test/geocoder-google-test.js b/test/geocoder-google-test.js index bae38fb..d6593af 100644 --- a/test/geocoder-google-test.js +++ b/test/geocoder-google-test.js @@ -17,7 +17,7 @@ module.exports = { testGeocode: function(test){ test.expect(3); - geocoder.geocode("Munich, Germany", function(err, result){ + geocoder.geocode("Munich, Germany", {}, function(err, result){ test.ok(!err); test.equals('OK', result.status); test.ok(result.results[0].formatted_address.match(/Munich/)); @@ -27,7 +27,7 @@ module.exports = { testReverseGeocode: function(test){ test.expect(7); - geocoder.reverseGeocode(49.101,6.1442, function(err, result){ + geocoder.reverseGeocode(49.101,6.1442, {}, function(err, result){ test.ok(!err); test.equals('OK', result.status); // console.error(result.results[0].formatted_address); @@ -50,7 +50,7 @@ module.exports = { testReverseGeocodeGoogleplex: function(test){ test.expect(9); - geocoder.reverseGeocode(37.42291810, -122.08542120, function(err, result){ + geocoder.reverseGeocode(37.42291810, -122.08542120, {}, function(err, result){ test.ok(!err); test.equals('OK', result.status); test.ok(result.results[0].formatted_address.match(/Mountain View/i)); @@ -76,7 +76,7 @@ module.exports = { testLanguage: function(test){ test.expect(3); - geocoder.geocode("Plattlinger Str. 10, 81479 München, Deutschland", function(err, result){ + geocoder.geocode("Plattlinger Str. 10, 81479 München, Deutschland", {}, function(err, result){ test.ok(!err); test.equals('OK', result.status); test.ok(result.results[0].formatted_address.match(/München/)); diff --git a/test/geocoder-here.js b/test/geocoder-here.js new file mode 100644 index 0000000..5ac2fdc --- /dev/null +++ b/test/geocoder-here.js @@ -0,0 +1,28 @@ +geocoder = require('../index.js'); + + + +module.exports = { + + setUp:function(cb) { + geocoder.selectProvider("here",{"appid": "sS9YNh3wSKTMMDlPvpdW", "appcode": "-junJXzl1ASWYAhDvLCfeg"}); + cb() + }, + + testExposeGeocodeFunction: function(test){ + test.equal(typeof geocoder.geocode, 'function'); + test.equal(geocoder.provider, 'here'); + test.done() + }, + + // Uses "here" + testReverseGeocode: function(test){ + return require('./geocoder-google-test.js').testReverseGeocode(test); + }, + + // Uses "address" + testReverseGeocodeGoogleplex: function(test){ + return require('./geocoder-google-test.js').testReverseGeocodeGoogleplex(test); + }, + +} diff --git a/test/geocoder-nominatim.js b/test/geocoder-nominatim.js new file mode 100644 index 0000000..83eeabf --- /dev/null +++ b/test/geocoder-nominatim.js @@ -0,0 +1,28 @@ +geocoder = require('../index.js'); + + + +module.exports = { + + setUp:function(cb) { + geocoder.selectProvider("nominatim",{"username":"npmunittests"}); + cb(); + }, + + testExposeGeocodeFunction: function(test){ + test.equal(typeof geocoder.geocode, 'function'); + test.equal(geocoder.provider, 'nominatim'); + test.done() + }, + + // Uses "nominatim" + testReverseGeocode: function(test){ + return require('./geocoder-google-test.js').testReverseGeocode(test); + }, + + // Uses "address" + /*testReverseGeocodeGoogleplex: function(test){ + return require('./geocoder-google-test.js').testReverseGeocodeGoogleplex(test); + }, +*/ +}