diff --git a/README.md b/README.md index e34fef2e8d31c7b58936a3816d39d913989acafe..2de8db822a7f8b43d6b2b7850dcc5313a13919d6 100644 --- a/README.md +++ b/README.md @@ -396,6 +396,27 @@ The following is a comparison of the supported geocoding APIs. The "Limitations" * **Limitations**: "You must not use or display the Content without a corresponding Google map, unless you are explicitly permitted to do so in the Maps APIs Documentation, or through written permission from Google." "You must not pre-fetch, cache, or store any Content, except that you may store: (i) limited amounts of Content for the purpose of improving the performance of your Maps API Implementation..." * **Notes**: To use Google Premier set `Geocoder.configure(:lookup => :google_premier, :api_key => [key, client, channel])`. +#### Google Places Details (`:google_places_details`) + +The [Google Places Details API](https://developers.google.com/places/documentation/details) extends results provided by the +[Google Places Autocomplete API](https://developers.google.com/places/documentation/autocomplete) with address information, +coordinates, ratings and reviews. It can be used as a replacement to the Google Geocoding API, because some information +provided by the Autocomplete API is not geocodable over the Geocoding API - like hotels, restaurants, bars. + +The Google Places Details search requires a valid `place_id` - instead of the usual search query. This `place_id` can be +obtained over the Google Places Autocomplete API and should be passed to Geocoder as the first search argument: +`Geocoder.search("ChIJhRwB-yFawokR5Phil-QQ3zM", :lookup => :google_places_details)`. + +* **API key**: required +* **Key signup**: https://code.google.com/apis/console/ +* **Quota**: 1,000 request/day, 100,000 after credit card authentication +* **Region**: world +* **SSL support**: yes +* **Languages**: ar, eu, bg, bn, ca, cs, da, de, el, en, en-AU, en-GB, es, eu, fa, fi, fil, fr, gl, gu, hi, hr, hu, id, it, iw, ja, kn, ko, lt, lv, ml, mr, nl, no, pl, pt, pt-BR, pt-PT, ro, ru, sk, sl, sr, sv, tl, ta, te, th, tr, uk, vi, zh-CN, zh-TW (see http://spreadsheets.google.com/pub?key=p9pdwsai2hDMsLkXsoM05KQ&gid=1) +* **Documentation**: https://developers.google.com/places/documentation/details +* **Terms of Service**: https://developers.google.com/places/policies +* **Limitations**: "If your application displays Places API data on a page or view that does not also display a Google Map, you must show a "Powered by Google" logo with that data." + #### Yahoo BOSS (`:yahoo`) * **API key**: requires OAuth consumer key and secret (set `Geocoder.configure(:api_key => [key, secret])`) diff --git a/lib/geocoder/lookup.rb b/lib/geocoder/lookup.rb index b2dcd136bcbb61004db4867011bee5b976d2f00d..d6728b51f6c913d590bd83c1ac506022887d4987 100644 --- a/lib/geocoder/lookup.rb +++ b/lib/geocoder/lookup.rb @@ -25,6 +25,7 @@ module Geocoder :esri, :google, :google_premier, + :google_places_details, :yahoo, :bing, :geocoder_ca, diff --git a/lib/geocoder/lookups/google_places_details.rb b/lib/geocoder/lookups/google_places_details.rb new file mode 100644 index 0000000000000000000000000000000000000000..b6c44e63e215e8273a5b6db0f52e3d97d87e5a11 --- /dev/null +++ b/lib/geocoder/lookups/google_places_details.rb @@ -0,0 +1,50 @@ +require "geocoder/lookups/google" +require "geocoder/results/google_places_details" + +module Geocoder + module Lookup + class GooglePlacesDetails < Google + def name + "Google Places Details" + end + + def required_api_key_parts + ["key"] + end + + def use_ssl? + true + end + + def query_url(query) + "#{protocol}://maps.googleapis.com/maps/api/place/details/json?#{url_query_string(query)}" + end + + private + + def results(query) + return [] unless doc = fetch_data(query) + + case doc["status"] + when "OK" + return [doc["result"]] + when "OVER_QUERY_LIMIT" + raise_error(Geocoder::OverQueryLimitError) || warn("Google Places Details API error: over query limit.") + when "REQUEST_DENIED" + raise_error(Geocoder::RequestDenied) || warn("Google Places Details API error: request denied.") + when "INVALID_REQUEST" + raise_error(Geocoder::InvalidRequest) || warn("Google Places Details API error: invalid request.") + end + + [] + end + + def query_url_google_params(query) + { + placeid: query.text, + language: query.language || configuration.language + } + end + end + end +end diff --git a/lib/geocoder/results/google_places_details.rb b/lib/geocoder/results/google_places_details.rb new file mode 100644 index 0000000000000000000000000000000000000000..cd10eb6c4244fcec9b99c80032429a5fd90e0a70 --- /dev/null +++ b/lib/geocoder/results/google_places_details.rb @@ -0,0 +1,35 @@ +require "geocoder/results/google" + +module Geocoder + module Result + class GooglePlacesDetails < Google + def place_id + @data["place_id"] + end + + def types + @data["types"] || [] + end + + def reviews + @data["reviews"] || [] + end + + def rating + @data["rating"] + end + + def rating_count + @data["user_ratings_total"] + end + + def phone_number + @data["international_phone_number"] + end + + def website + @data["website"] + end + end + end +end diff --git a/test/fixtures/google_places_details_invalid_request b/test/fixtures/google_places_details_invalid_request new file mode 100644 index 0000000000000000000000000000000000000000..51fd509625336a55d3015241776ae95733f0aa45 --- /dev/null +++ b/test/fixtures/google_places_details_invalid_request @@ -0,0 +1,4 @@ +{ + "html_attributions" : [], + "status" : "INVALID_REQUEST" +} diff --git a/test/fixtures/google_places_details_madison_square_garden b/test/fixtures/google_places_details_madison_square_garden new file mode 100644 index 0000000000000000000000000000000000000000..147975427d184d906e5133ef2fc836f7774bbc0c --- /dev/null +++ b/test/fixtures/google_places_details_madison_square_garden @@ -0,0 +1,120 @@ +{ + "html_attributions" : [], + "result" : { + "address_components" : [ + { + "long_name" : "4", + "short_name" : "4", + "types" : [ "street_number" ] + }, + { + "long_name" : "Pennsylvania Plaza", + "short_name" : "Pennsylvania Plaza", + "types" : [ "route" ] + }, + { + "long_name" : "Chelsea", + "short_name" : "Chelsea", + "types" : [ "neighborhood", "political" ] + }, + { + "long_name" : "Manhattan", + "short_name" : "Manhattan", + "types" : [ "sublocality_level_1", "sublocality", "political" ] + }, + { + "long_name" : "New York", + "short_name" : "New York", + "types" : [ "locality", "political" ] + }, + { + "long_name" : "New York County", + "short_name" : "New York County", + "types" : [ "administrative_area_level_2", "political" ] + }, + { + "long_name" : "NY", + "short_name" : "NY", + "types" : [ "administrative_area_level_1", "political" ] + }, + { + "long_name" : "United States", + "short_name" : "US", + "types" : [ "country", "political" ] + }, + { + "long_name" : "10001", + "short_name" : "10001", + "types" : [ "postal_code" ] + } + ], + "adr_address" : "\u003cspan class=\"street-address\"\u003e4 Pennsylvania Plaza\u003c/span\u003e, \u003cspan class=\"locality\"\u003eNew York\u003c/span\u003e, \u003cspan class=\"region\"\u003eNY\u003c/span\u003e \u003cspan class=\"postal-code\"\u003e10001\u003c/span\u003e, \u003cspan class=\"country-name\"\u003eUnited States\u003c/span\u003e", + "formatted_address" : "4 Pennsylvania Plaza, New York, NY, United States", + "formatted_phone_number" : "(212) 465-6741", + "geometry" : { + "location" : { + "lat" : 40.750504, + "lng" : -73.993439 + } + }, + "icon" : "http://maps.gstatic.com/mapfiles/place_api/icons/stadium-71.png", + "id" : "55e3174d410b31da010030a7dfc0c9819027445a", + "international_phone_number" : "+1 212-465-6741", + "name" : "Madison Square Garden", + "photos" : [ + { + "height" : 267, + "html_attributions" : [ "Someone" ], + "photo_reference" : "CnRoAAAAB1lz_vOkA1Ffaf7vJ1xcjzVsII-873Z_f8_v3EyW4XbEvlR3VkW_HvNfwF6AwDA0U-ont7fIUqEMyDcovCTWN8RSYN3ibEhqgJPvXxBjeYVi0cc-t-3KfkB_LpV7chPAqhOsdMG56kyzTKIo4lISHxIQqru7QXV6xU11jLaLpi4FNRoUhutg9aq027NghW1d-o9GGE8N3yM", + "width" : 400 + }, + { + "height" : 732, + "html_attributions" : [ "Someone else" ], + "photo_reference" : "CoQBfwAAADbYj554hM1BgVbBOs6rDjO9UtQ63ecU1P8tI3PfaHame3MB4ipgcNRlv9N2iUa-tXoMq9iXAaDStWgCJ3f48WIuIRbz-Xv-HzSJ0hMCrFiZMRs7kLgaBO7eDJwioTySWcoyAwojretq4mZKF_AcRSUhkqOokBhOstmshWWpSAyyEhAdpL2yp42xDNq2YRiFrx4DGhRZIdt7mochSwZcgSdjESea27Qy9Q", + "width" : 929 + } + ], + "place_id" : "ChIJhRwB-yFawokR5Phil-QQ3zM", + "rating" : 4.4, + "reference" : "CnRuAAAArSB-mFxXRjm0ERZIC4c_1gbXwxqmyxUrxws_PQUrz9Y3xZstEwm7_8dLw7zM8hV3vWkPF4RkkZQQ_X01ikDALx2VgV60YwiSX7k3TXxkWnzyFcX0fnHCQLerlYttk_usL7UALQQgMeuf25_eFx3SnhIQxN95Ek3bQVuLVM16yb99ehoU7djcL3cjllohLZ6oJ6X3Tzb9MJQ", + "reviews" : [ + { + "aspects" : [ + { + "rating" : 5, + "type" : "overall" + } + ], + "author_name" : "John Smith", + "author_url" : "https://plus.google.com/john.smith", + "language" : "en", + "rating" : 5, + "text" : "It's nice.", + "time" : 1407266916 + }, + { + "aspects" : [ + { + "rating" : 2, + "type" : "overall" + } + ], + "author_name" : "Jane Smith", + "author_url" : "https://plus.google.com/jane.smith", + "language" : "en", + "rating" : 2, + "text" : "Not so nice.", + "time" : 1398779079 + } + ], + "scope" : "GOOGLE", + "types" : [ "stadium", "establishment" ], + "url" : "https://plus.google.com/112180896421099179463/about?hl=en-US", + "user_ratings_total" : 382, + "utc_offset" : -240, + "vicinity" : "4 Pennsylvania Plaza, New York", + "website" : "http://www.thegarden.com/" + }, + "status" : "OK" +} diff --git a/test/fixtures/google_places_details_no_results b/test/fixtures/google_places_details_no_results new file mode 100644 index 0000000000000000000000000000000000000000..cc4747f3638d7a84512e76f0c7f4313f109fd20a --- /dev/null +++ b/test/fixtures/google_places_details_no_results @@ -0,0 +1,4 @@ +{ + "html_attributions" : [], + "result": {} +} diff --git a/test/fixtures/google_places_details_no_reviews b/test/fixtures/google_places_details_no_reviews new file mode 100644 index 0000000000000000000000000000000000000000..3d8527b1b1e1878eded50a3cfc87ed28a6ca1ebb --- /dev/null +++ b/test/fixtures/google_places_details_no_reviews @@ -0,0 +1,60 @@ +{ + "html_attributions" : [], + "result" : { + "address_components" : [ + { + "long_name" : "25", + "short_name" : "25", + "types" : [ "street_number" ] + }, + { + "long_name" : "Oranienstraße", + "short_name" : "Oranienstraße", + "types" : [ "route" ] + }, + { + "long_name" : "Friedrichshain-Kreuzberg", + "short_name" : "Friedrichshain-Kreuzberg", + "types" : [ "sublocality_level_1", "sublocality", "political" ] + }, + { + "long_name" : "Berlin", + "short_name" : "Berlin", + "types" : [ "locality", "political" ] + }, + { + "long_name" : "Berlin", + "short_name" : "Berlin", + "types" : [ "administrative_area_level_1", "political" ] + }, + { + "long_name" : "Germany", + "short_name" : "DE", + "types" : [ "country", "political" ] + }, + { + "long_name" : "10999", + "short_name" : "10999", + "types" : [ "postal_code" ] + } + ], + "adr_address" : "\u003cspan class=\"street-address\"\u003eOranienstraße 25\u003c/span\u003e, \u003cspan class=\"postal-code\"\u003e10999\u003c/span\u003e \u003cspan class=\"locality\"\u003eBerlin\u003c/span\u003e, \u003cspan class=\"country-name\"\u003eGermany\u003c/span\u003e", + "formatted_address" : "Oranienstraße 25, 10999 Berlin, Germany", + "geometry" : { + "location" : { + "lat" : 52.5010652, + "lng" : 13.4206563 + } + }, + "icon" : "http://maps.gstatic.com/mapfiles/place_api/icons/geocode-71.png", + "id" : "b49e917abd41c247425645a6ac3e5a6756ad80f8", + "name" : "Oranienstraße 25", + "place_id" : "ChIJQ8-HeTROqEcRGdCTErYy5D0", + "reference" : "CpQBjAAAAKrbnaAglhQLI6KPs3EbRp1lVh5UkB4xLEyzOmvvGmtpmwD2EAnlokKcx1VU5PvvB7moRGw6lHTlMTScpGL3GTCC_WM2pzDxgeaAtoB-SR4YQ7PRhHkT2eqxACbaP_70Z9Wyb2J31tG66xBrYASAuBzXgEXYaFpo8dhRBY4xMTfexvMziw6rHs01SxLhCFh-uBIQBQcGVz70_HtaG_g6ZZ5omhoUdIDDSVApRiVoIh61NiCzStYa-x0", + "scope" : "GOOGLE", + "types" : [ "street_address" ], + "url" : "https://maps.google.com/maps/place?q=Oranienstra%C3%9Fe+25,+10999+Berlin,+Germany&ftid=0x47a84e347987cf43:0x3de432b61293d019", + "vicinity" : "Friedrichshain-Kreuzberg" + }, + "status" : "OK" +} diff --git a/test/fixtures/google_places_details_no_types b/test/fixtures/google_places_details_no_types new file mode 100644 index 0000000000000000000000000000000000000000..96903c5aabd5054eca9edef5bdd5397f2ee39927 --- /dev/null +++ b/test/fixtures/google_places_details_no_types @@ -0,0 +1,66 @@ +{ + "html_attributions" : [], + "result" : { + "address_components" : [ + { + "long_name" : "6", + "short_name" : "6", + "types" : [ "street_number" ] + }, + { + "long_name" : "Graniczna", + "short_name" : "Graniczna", + "types" : [ "route" ] + }, + { + "long_name" : "Gdynia", + "short_name" : "Gdynia", + "types" : [ "locality", "political" ] + }, + { + "long_name" : "Gdynia", + "short_name" : "Gdynia", + "types" : [ "administrative_area_level_3", "political" ] + }, + { + "long_name" : "Gdynia", + "short_name" : "Gdynia", + "types" : [ "administrative_area_level_2", "political" ] + }, + { + "long_name" : "Pomeranian Voivodeship", + "short_name" : "Pomeranian Voivodeship", + "types" : [ "administrative_area_level_1", "political" ] + }, + { + "long_name" : "Poland", + "short_name" : "PL", + "types" : [ "country", "political" ] + }, + { + "long_name" : "81-626", + "short_name" : "81-626", + "types" : [ "postal_code" ] + } + ], + "adr_address" : "\u003cspan class=\"street-address\"\u003eGraniczna 6\u003c/span\u003e, \u003cspan class=\"postal-code\"\u003e81-626\u003c/span\u003e \u003cspan class=\"locality\"\u003eGdynia\u003c/span\u003e, \u003cspan class=\"country-name\"\u003ePoland\u003c/span\u003e", + "formatted_address" : "Graniczna 6, Gdynia, Poland", + "formatted_phone_number" : "795 085 050", + "geometry" : { + "location" : { + "lat" : 54.493608, + "lng" : 18.508983 + } + }, + "id" : "aed47c412bce35bb385c86edef64f8f7860b1cf8", + "international_phone_number" : "+48 795 085 050", + "name" : "Taxi Gdynia", + "place_id" : "ChIJgT2H0tig_UYRD9iM2NSOo4Y", + "reference" : "CnRlAAAA6bzgakKCXzwjtLBSMLvj0fvv3OSwGp2WsA54VwQELEupFzqtFuUmxMyMSNYt745EukJR0Ui6Ih9WX3AdL--HXjQprE8xHSb_6qQh2eauKWFIoHzIvbMrkjDIcUPxPDMdJc2XkwpOr_EUimZplCy-gBIQb_UHtRVaswAMjdHHtDqzURoU3tGbILe-zhN3oISBAvc3AyoyznE", + "scope" : "GOOGLE", + "url" : "https://plus.google.com/100174651414421384172/about?hl=en-US", + "utc_offset" : 120, + "vicinity" : "Graniczna 6, Gdynia" + }, + "status" : "OK" +} diff --git a/test/test_helper.rb b/test/test_helper.rb index 0faf3f113d02c505d7ffcce1063881250697025b..ce5ad195739a93e83f74c6b9f42bece6e6d46eed 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -122,6 +122,13 @@ module Geocoder end end + class GooglePlacesDetails + private + def fixture_prefix + "google_places_details" + end + end + class Dstk private def fixture_prefix diff --git a/test/unit/lookups/google_places_details_test.rb b/test/unit/lookups/google_places_details_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..1132aea9ceee7a7bb730b5c2bf93081422a41b06 --- /dev/null +++ b/test/unit/lookups/google_places_details_test.rb @@ -0,0 +1,122 @@ +# encoding: utf-8 +$: << File.join(File.dirname(__FILE__), "..", "..") +require 'test_helper' + +class GooglePlacesDetailsTest < GeocoderTestCase + + def setup + Geocoder.configure(lookup: :google_places_details) + set_api_key!(:google_places_details) + end + + def test_google_places_details_result_components + assert_equal "Manhattan", madison_square_garden.address_components_of_type(:sublocality).first["long_name"] + end + + def test_google_places_details_result_components_contains_route + assert_equal "Pennsylvania Plaza", madison_square_garden.address_components_of_type(:route).first["long_name"] + end + + def test_google_places_details_result_components_contains_street_number + assert_equal "4", madison_square_garden.address_components_of_type(:street_number).first["long_name"] + end + + def test_google_places_details_street_address_returns_formatted_street_address + assert_equal "4 Pennsylvania Plaza", madison_square_garden.street_address + end + + def test_google_places_details_result_contains_place_id + assert_equal "ChIJhRwB-yFawokR5Phil-QQ3zM", madison_square_garden.place_id + end + + def test_google_places_details_result_contains_latitude + assert_equal madison_square_garden.latitude, 40.750504 + end + + def test_google_places_details_result_contains_longitude + assert_equal madison_square_garden.longitude, -73.993439 + end + + def test_google_places_details_result_contains_rating + assert_equal 4.4, madison_square_garden.rating + end + + def test_google_places_details_result_contains_rating_count + assert_equal 382, madison_square_garden.rating_count + end + + def test_google_places_details_result_contains_reviews + reviews = madison_square_garden.reviews + + assert_equal 2, reviews.size + assert_equal "John Smith", reviews.first["author_name"] + assert_equal 5, reviews.first["rating"] + assert_equal "It's nice.", reviews.first["text"] + assert_equal "en", reviews.first["language"] + end + + def test_google_places_details_result_contains_types + assert_equal madison_square_garden.types, %w(stadium establishment) + end + + def test_google_places_details_result_contains_website + assert_equal madison_square_garden.website, "http://www.thegarden.com/" + end + + def test_google_places_details_result_contains_phone_number + assert_equal madison_square_garden.phone_number, "+1 212-465-6741" + end + + def test_google_places_details_query_url_contains_placeid + url = lookup.query_url(Geocoder::Query.new("some-place-id")) + assert_match(/placeid=some-place-id/, url) + end + + def test_google_places_details_query_url_contains_language + url = lookup.query_url(Geocoder::Query.new("some-place-id", language: "de")) + assert_match(/language=de/, url) + end + + def test_google_places_details_query_url_always_uses_https + url = lookup.query_url(Geocoder::Query.new("some-place-id")) + assert_match(%r(^https://), url) + end + + def test_google_places_details_result_with_no_reviews_shows_empty_reviews + assert_equal no_reviews_result.reviews, [] + end + + def test_google_places_details_result_with_no_types_shows_empty_types + assert_equal no_types_result.types, [] + end + + def test_google_places_details_result_with_invalid_place_id_empty + assert_equal Geocoder.search("invalid request"), [] + end + + def test_raises_exception_on_google_places_details_invalid_request + Geocoder.configure(always_raise: [Geocoder::InvalidRequest]) + assert_raises Geocoder::InvalidRequest do + Geocoder.search("invalid request") + end + end + + private + + def lookup + Geocoder::Lookup::GooglePlacesDetails.new + end + + def madison_square_garden + Geocoder.search("ChIJhRwB-yFawokR5Phil-QQ3zM").first + end + + def no_reviews_result + Geocoder.search("no reviews").first + end + + def no_types_result + Geocoder.search("no types").first + end + +end