diff --git a/lib/geocoder/lookup.rb b/lib/geocoder/lookup.rb index e915b7d4d..70bcc9b34 100644 --- a/lib/geocoder/lookup.rb +++ b/lib/geocoder/lookup.rb @@ -38,6 +38,7 @@ def street_services :google_premier, :google_places_details, :google_places_search, + :google_places_details_new, :bing, :geocoder_ca, :yandex, diff --git a/lib/geocoder/lookups/google.rb b/lib/geocoder/lookups/google.rb index 81df16437..a79362e30 100644 --- a/lib/geocoder/lookups/google.rb +++ b/lib/geocoder/lookups/google.rb @@ -1,8 +1,8 @@ -require 'geocoder/lookups/base' +require 'geocoder/lookups/google_base' require "geocoder/results/google" module Geocoder::Lookup - class Google < Base + class Google < GoogleBase def name "Google" @@ -12,32 +12,12 @@ def map_link_url(coordinates) "http://maps.google.com/maps?q=#{coordinates.join(',')}" end - def supported_protocols - # Google requires HTTPS if an API key is used. - if configuration.api_key - [:https] - else - [:http, :https] - end - end - private # --------------------------------------------------------------- def base_query_url(query) "#{protocol}://maps.googleapis.com/maps/api/geocode/json?" end - def configure_ssl!(client) - client.instance_eval { - @ssl_context = OpenSSL::SSL::SSLContext.new - options = OpenSSL::SSL::OP_NO_SSLv2 | OpenSSL::SSL::OP_NO_SSLv3 - if OpenSSL::SSL.const_defined?('OP_NO_COMPRESSION') - options |= OpenSSL::SSL::OP_NO_COMPRESSION - end - @ssl_context.set_params({options: options}) - } - end - def valid_response?(response) json = parse_json(response.body) status = json["status"] if json diff --git a/lib/geocoder/lookups/google_base.rb b/lib/geocoder/lookups/google_base.rb new file mode 100644 index 000000000..5b6a922b7 --- /dev/null +++ b/lib/geocoder/lookups/google_base.rb @@ -0,0 +1,28 @@ +require 'geocoder/lookups/base' + +module Geocoder::Lookup + class GoogleBase < Base + def supported_protocols + # Google requires HTTPS if an API key is used. + if configuration.api_key + [:https] + else + [:http, :https] + end + end + + private # --------------------------------------------------------------- + + def configure_ssl!(client) + client.instance_eval { + @ssl_context = OpenSSL::SSL::SSLContext.new + options = OpenSSL::SSL::OP_NO_SSLv2 | OpenSSL::SSL::OP_NO_SSLv3 + if OpenSSL::SSL.const_defined?('OP_NO_COMPRESSION') + options |= OpenSSL::SSL::OP_NO_COMPRESSION + end + @ssl_context.set_params({options: options}) + } + end + end +end + diff --git a/lib/geocoder/lookups/google_new.rb b/lib/geocoder/lookups/google_new.rb new file mode 100644 index 000000000..265953b95 --- /dev/null +++ b/lib/geocoder/lookups/google_new.rb @@ -0,0 +1,52 @@ +require 'geocoder/lookups/google_base' +require 'geocoder/results/google_new' + +module Geocoder::Lookup + class GoogleNew < GoogleBase + def required_api_key_parts + ["key"] + end + + private + + def base_url + "#{protocol}://places.googleapis.com/v1/places" + end + + def valid_response?(response) + json = parse_json(response.body) + error_status = json.dig('errpr', 'status') if json + + super(response) and error_status.nil? + end + + def results(query) + return [] unless doc = fetch_data(query) + + error = doc['error'] + return doc if error.nil? + + case error['status'] + when 'PERMISSION_DENIED' + raise_error(Geocoder::RequestDenied, error['message']) || + Geocoder.log(:warn, "#{name} API error: request denied (#{error['message']}).") + when 'INVALID_ARGUMENT' + raise_error(Geocoder::InvalidRequest, error['message']) || + Geocoder.log(:warn, "#{name} API error: invalid request (#{error['message']}).") + end + + return [] + end + + def query_url_google_params(query) + {} + end + + def query_url_params(query) + query_url_google_params(query).merge( + :key => configuration.api_key + ).merge(super) + end + end +end + diff --git a/lib/geocoder/lookups/google_places_details_new.rb b/lib/geocoder/lookups/google_places_details_new.rb new file mode 100644 index 000000000..39b0af1f8 --- /dev/null +++ b/lib/geocoder/lookups/google_places_details_new.rb @@ -0,0 +1,50 @@ +require 'geocoder/lookups/google_new' +require 'geocoder/results/google_places_details_new' + +module Geocoder::Lookup + class GooglePlacesDetailsNew < GoogleNew + def name + 'Google Placees Details (New)' + end + + private + + def base_query_url(query) + "#{base_url}/#{query.text}?" + end + + def results(query) + result = super(query) + return [result] unless result.is_a? Array + + result + end + + def fields(query) + if query.options.has_key?(:fields) + return format_fields(query.options[:fields]) + end + + if configuration.has_key?(:fields) + return format_fields(configuration[:fields]) + end + + # Google discourage the use of the wildcard field mask so you probably do NOT want to use it + '*' + end + + def format_fields(*fields) + flattened = fields.flatten.compact + return if flattened.empty? + + flattened.join(',') + end + + def query_url_google_params(query) + { + fields: fields(query), + languageCode: query.language || configuration.language + }.merge(super) + end + end +end diff --git a/lib/geocoder/results/google_new.rb b/lib/geocoder/results/google_new.rb new file mode 100644 index 000000000..8d6526a10 --- /dev/null +++ b/lib/geocoder/results/google_new.rb @@ -0,0 +1,122 @@ +require 'geocoder/results/base' + +module Geocoder::Result + class GoogleNew < Base + + def coordinates + ['latitude', 'longitude'].map{ |i| @data['location'][i] } + end + + def address(format = :full) + formatted_address + end + + def neighborhood + if neighborhood = address_components_of_type(:neighborhood).first + neighborhood['longText'] + end + end + + def city + fields = [:locality, :sublocality, + :administrative_area_level_3, + :administrative_area_level_2] + fields.each do |f| + if entity = address_components_of_type(f).first + return entity['longText'] + end + end + + return nil # no appropriate components found + end + + def state + if state = address_components_of_type(:administrative_area_level_1).first + state['longText'] + end + end + + def state_code + if state = address_components_of_type(:administrative_area_level_1).first + state['shortText'] + end + end + + def sub_state + if state = address_components_of_type(:administrative_area_level_2).first + state['longText'] + end + end + + def sub_state_code + if state = address_components_of_type(:administrative_area_level_2).first + state['shortText'] + end + end + + def country + if country = address_components_of_type(:country).first + country['longText'] + end + end + + def country_code + if country = address_components_of_type(:country).first + country['shortText'] + end + end + + def postal_code + if postal = address_components_of_type(:postal_code).first + postal['longText'] + end + end + + def route + if route = address_components_of_type(:route).first + route['longText'] + end + end + + def street_number + if street_number = address_components_of_type(:street_number).first + street_number['longText'] + end + end + + def street_address + [street_number, route].compact.join(' ') + end + + def types + @data['types'] + end + + def formatted_address + @data['formattedAddress'] + end + + def address_components + @data['addressComponents'] + end + + ## + # Get address components of a given type. Valid types are defined in + # Google's Geocoding API documentation and include (among others): + # + # :street_number + # :locality + # :neighborhood + # :route + # :postal_code + # + def address_components_of_type(type) + address_components.select{ |c| c['types'].include?(type.to_s) } + end + + def place_id + @data['id'] + end + end +end + diff --git a/lib/geocoder/results/google_places_details_new.rb b/lib/geocoder/results/google_places_details_new.rb new file mode 100644 index 000000000..55b9db1dc --- /dev/null +++ b/lib/geocoder/results/google_places_details_new.rb @@ -0,0 +1,38 @@ +require 'geocoder/results/google_new' + +module Geocoder::Result + class GooglePlacesDetailsNew < GoogleNew + def types + @data['types'] || [] + end + + def primary_type + @data['primaryType'] + end + + def reviews + @data['reviews'] || [] + end + + def rating + @data['rating'] + end + + def rating_count + @data['userRatingCount'] + end + + def phone_number + @data['internationalPhoneNumber'] + end + + def website + @data['websiteUri'] + end + + def photos + @data['photos'] || [] + end + end +end + diff --git a/test/fixtures/google_places_details_new_invalid_request b/test/fixtures/google_places_details_new_invalid_request new file mode 100644 index 000000000..1d3436934 --- /dev/null +++ b/test/fixtures/google_places_details_new_invalid_request @@ -0,0 +1,6 @@ +{ + "error" : { + "code": 400, + "status": "INVALID_ARGUMENT" + } +} diff --git a/test/fixtures/google_places_details_new_madison_square_garden b/test/fixtures/google_places_details_new_madison_square_garden new file mode 100644 index 000000000..995fd48c3 --- /dev/null +++ b/test/fixtures/google_places_details_new_madison_square_garden @@ -0,0 +1,123 @@ +{ + "addressComponents" : [ + { + "longText" : "4", + "shortText" : "4", + "types" : [ "street_number" ] + }, + { + "longText" : "Pennsylvania Plaza", + "shortText" : "Pennsylvania Plaza", + "types" : [ "route" ] + }, + { + "longText" : "Chelsea", + "shortText" : "Chelsea", + "types" : [ "neighborhood", "political" ] + }, + { + "longText" : "Manhattan", + "shortText" : "Manhattan", + "types" : [ "sublocality_level_1", "sublocality", "political" ] + }, + { + "longText" : "New York", + "shortText" : "New York", + "types" : [ "locality", "political" ] + }, + { + "longText" : "New York County", + "shortText" : "New York County", + "types" : [ "administrative_area_level_2", "political" ] + }, + { + "longText" : "NY", + "shortText" : "NY", + "types" : [ "administrative_area_level_1", "political" ] + }, + { + "longText" : "United States", + "shortText" : "US", + "types" : [ "country", "political" ] + }, + { + "longText" : "10001", + "shortText" : "10001", + "types" : [ "postal_code" ] + } + ], + "adrFormatAddress" : "\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", + "formattedAddress" : "4 Pennsylvania Plaza, New York, NY, United States", + "nationalPhoneNumber" : "(212) 465-6741", + "location" : { + "latitude" : 40.750504, + "longitude" : -73.993439 + }, + "name" : "places/ChIJhRwB-yFawokR5Phil-QQ3zM", + "internationalPhoneNumber" : "+1 212-465-6741", + "displayName" : { + "text" : "Madison Square Garden", + "languageCode" : "en" + }, + "photos" : [ + { + "name" : "places/ChIJhRwB-yFawokR5Phil-QQ3zM/photos/AcJnMuEPJtKLwJrGZto0VDHpUPP2Im3iSmmoH10JehqT6DYfnvNuNrpEqAgwFWtBkVf1fdh-IW14iVRm-GU_KBy3_msqziCqoLnKhpdHv9DipZxl7t_u6zL8LkcnzxS3Jsm1TID6Sy7JXvSFJ4Y3SGEM8bfbIv0XE2mCckw", + "widthPx" : 400, + "heightPx" : 267, + "authorAttributions" : [{ + "displayName" : "John Smith", + "uri" : "//maps.google.com/maps/contrib/110012216588911435300", + "photoUri" : "//lh3.googleusercontent.com/a-/ALV-UjVtRt_FObWkBxnWSXMuXwjUuL8QgL8rE6K-u36kpSIeMFIJ=s100-p-k-no-mo" + }] + }, + { + "name" : "places/ChIJhRwB-yFawokR5Phil-QQ3zM/photos/AcJnMuEPJtKLwJrGZto0VDHpUPP2Im3iSmmoH10JehqT6DYfnvNuNrpEqAgwFWtBkVf1fdh-IW14iVRm-GU_KBy3_msqziCqoLnKhpdHv9DipZxl7t_u6zL8LkcnzxS3Jsm1TID6Sy7JXvSFJ4Y3SGEM8bfbIv0XE2mCckw", + "widthPx" : 400, + "heightPx" : 267, + "authorAttributions" : [{ + "displayName" : "John Smith", + "uri" : "//maps.google.com/maps/contrib/110012216588911435300", + "photoUri" : "//lh3.googleusercontent.com/a-/ALV-UjVtRt_FObWkBxnWSXMuXwjUuL8QgL8rE6K-u36kpSIeMFIJ=s100-p-k-no-mo" + }] + } + ], + "id" : "ChIJhRwB-yFawokR5Phil-QQ3zM", + "rating" : 4.4, + "reviews" : [ + { + "name" : "places/ChIJhRwB-yFawokR5Phil-QQ3zM/reviews/ChdDSUhNMG9nS0VJQ0FnSUNaN2Zpbjl3RRAB", + "authorAttribution" : { + "displayName" : "John Smith", + "uri" : "//maps.google.com/maps/contrib/110012216588911435300", + "photoUri" : "//lh3.googleusercontent.com/a-/ALV-UjVtRt_FObWkBxnWSXMuXwjUuL8QgL8rE6K-u36kpSIeMFIJ=s100-p-k-no-mo" + }, + "rating" : 5, + "text" : { + "text" : "It's nice.", + "languageCode": "en" + }, + "publishTime" : "2023-09-15T20:20:40Z" + }, + { + "name" : "places/ChIJhRwB-yFawokR5Phil-QQ3zM/reviews/ChdDSUhNMG9nS0VJQ0FnSUNaN2Zpbjl3RRAB", + "authorAttribution" : { + "displayName" : "John Smith", + "uri" : "//maps.google.com/maps/contrib/110012216588911435300", + "photoUri" : "//lh3.googleusercontent.com/a-/ALV-UjVtRt_FObWkBxnWSXMuXwjUuL8QgL8rE6K-u36kpSIeMFIJ=s100-p-k-no-mo" + }, + "rating" : 2, + "text" : { + "text" : "Not so nice.", + "languageCode": "en" + }, + "publishTime" : "2023-09-15T20:15:40Z" + } + ], + "types" : [ "tourist_attraction", "stadium", "sports_complex", "event_venue", "sports_club", "point_of_interest", "establishment" ], + "primaryType" : "event_venue", + "googleMapsUri" : "https://plus.google.com/112180896421099179463/about?hl=en-US", + "userRatingCount" : 382, + "utcOffsetMinutes" : -240, + "shortFormattedAddress" : "4 Pennsylvania Plaza, New York", + "websiteUri" : "http://www.thegarden.com/" +} diff --git a/test/test_helper.rb b/test/test_helper.rb index 83d049a68..2f204a655 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -196,6 +196,14 @@ def fixture_prefix end end + require 'geocoder/lookups/google_places_details_new' + class GooglePlacesDetailsNew + private + def fixture_prefix + "google_places_details_new" + end + end + require 'geocoder/lookups/dstk' class Dstk private diff --git a/test/unit/lookup_test.rb b/test/unit/lookup_test.rb index ab4dc0b5c..dc13f084a 100644 --- a/test/unit/lookup_test.rb +++ b/test/unit/lookup_test.rb @@ -18,7 +18,8 @@ def test_search_returns_empty_array_when_no_results :ipqualityscore, :melissa_street, :nationaal_georegister_nl, - :twogis + :twogis, + :google_places_details_new ].include?(l) # lookups that always return a result lookup = Geocoder::Lookup.get(l) diff --git a/test/unit/lookups/google_places_details_new_test.rb b/test/unit/lookups/google_places_details_new_test.rb new file mode 100644 index 000000000..4d0dfa187 --- /dev/null +++ b/test/unit/lookups/google_places_details_new_test.rb @@ -0,0 +1,121 @@ +# encoding: utf-8 +require 'test_helper' + +class GooglePlacesDetailsNewTest < GeocoderTestCase + + def setup + super + Geocoder.configure(lookup: :google_places_details_new) + set_api_key!(:google_places_details_new) + end + + def test_google_places_details_new_result_components + assert_equal "Manhattan", madison_square_garden.address_components_of_type(:sublocality).first["longText"] + end + + def test_google_places_details_new_result_components_contains_route + assert_equal "Pennsylvania Plaza", madison_square_garden.address_components_of_type(:route).first["longText"] + end + + def test_google_places_details_new_result_components_contains_street_number + assert_equal "4", madison_square_garden.address_components_of_type(:street_number).first["longText"] + end + + def test_google_places_details_new_street_address_returns_formatted_street_address + assert_equal "4 Pennsylvania Plaza", madison_square_garden.street_address + end + + def test_google_places_details_new_result_contains_place_id + assert_equal "ChIJhRwB-yFawokR5Phil-QQ3zM", madison_square_garden.place_id + end + + def test_google_places_details_new_result_contains_latitude + assert_equal madison_square_garden.latitude, 40.750504 + end + + def test_google_places_details_new_result_contains_longitude + assert_equal madison_square_garden.longitude, -73.993439 + end + + def test_google_places_details_new_result_contains_rating + assert_equal 4.4, madison_square_garden.rating + end + + def test_google_places_details_new_result_contains_rating_count + assert_equal 382, madison_square_garden.rating_count + end + + def test_google_places_details_new_result_contains_reviews + reviews = madison_square_garden.reviews + + assert_equal 2, reviews.size + assert_equal "John Smith", reviews.first["authorAttribution"]["displayName"] + assert_equal 5, reviews.first["rating"] + assert_equal "It's nice.", reviews.first["text"]["text"] + assert_equal "en", reviews.first["text"]["languageCode"] + end + + def test_google_places_details_new_result_contains_types + assert_equal madison_square_garden.types, %w(tourist_attraction stadium sports_complex event_venue sports_club point_of_interest establishment) + end + + def test_google_places_details_new_result_contains_primary_type + assert_equal madison_square_garden.primary_type, "event_venue" + end + + def test_google_places_details_new_result_contains_website + assert_equal madison_square_garden.website, "http://www.thegarden.com/" + end + + def test_google_places_details_new_result_contains_phone_number + assert_equal madison_square_garden.phone_number, "+1 212-465-6741" + end + + def test_google_places_details_new_query_url_contains_language + url = lookup.query_url(Geocoder::Query.new("some-place-id", language: "de")) + assert_match(/languageCode=de/, url) + end + + def test_google_places_details_new_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_new_query_url_contains_specific_fields_when_given + fields = %w[formattedAddress id] + url = lookup.query_url(Geocoder::Query.new("some-place-id", fields: fields)) + assert_match(/fields=#{fields.join('%2C')}/, url) + end + + def test_google_places_details_new_query_url_contains_specific_fields_when_configured + fields = %w[businessStatus photos] + Geocoder.configure(google_places_details_new: {fields: fields}) + url = lookup.query_url(Geocoder::Query.new("some-place-id")) + assert_match(/fields=#{fields.join('%2C')}/, url) + Geocoder.configure(google_places_details_new: {}) + end + + def test_google_places_details_new_result_with_invalid_place_id_empty + silence_warnings do + assert_equal Geocoder.search("invalid request"), [] + end + 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::GooglePlacesDetailsNew.new + end + + def madison_square_garden + Geocoder.search("ChIJhRwB-yFawokR5Phil-QQ3zM").first + end + +end