|
|
|
require 'geocoder'
|
|
|
|
require 'geocoder/railtie'
|
|
|
|
require 'geocoder/calculations'
|
|
|
|
|
|
|
|
Geocoder::Railtie.insert
|
|
|
|
|
|
|
|
class City < ActiveRecord::Base
|
|
|
|
geocoded_by :address
|
|
|
|
translates :city
|
|
|
|
|
|
|
|
reverse_geocoded_by :latitude, :longitude, :address => :full_address
|
|
|
|
after_validation :geocode, if: ->(obj){ obj.country_changed? or obj.territory_changed? or obj.city_changed? or obj.latitude.blank? or obj.longitude.blank? }
|
|
|
|
|
|
|
|
def address
|
|
|
|
([city!, territory, country] - [nil, '']).join(', ')
|
|
|
|
end
|
|
|
|
|
|
|
|
def get_translation(locale)
|
|
|
|
location = Geocoder.search(address, language: locale.to_s).first
|
|
|
|
|
|
|
|
# if the service lets us down, return nil
|
|
|
|
return nil unless location.present?
|
|
|
|
|
|
|
|
searched_component = false
|
|
|
|
location.data['address_components'].each do | component |
|
|
|
|
# city is usually labeled a 'locality' but sometimes this is missing and only 'colloquial_area' is present
|
|
|
|
if component['types'].first == 'locality'
|
|
|
|
return component['short_name']
|
|
|
|
end
|
|
|
|
|
|
|
|
if component['types'] == location.data['types']
|
|
|
|
searched_component = component['short_name']
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# return the type we searched for but it's still possible that it will be false
|
|
|
|
searched_component
|
|
|
|
end
|
|
|
|
|
|
|
|
# this method will get called automatically if a translation is asked for but not found
|
|
|
|
def translate_city(locale)
|
|
|
|
translation = get_translation(locale)
|
|
|
|
|
|
|
|
# if we found it, set it
|
|
|
|
if translation.present?
|
|
|
|
set_column_for_locale(:city, locale, translation)
|
|
|
|
save!
|
|
|
|
end
|
|
|
|
|
|
|
|
return translation
|
|
|
|
end
|
|
|
|
|
|
|
|
def to_s
|
|
|
|
([
|
|
|
|
city,
|
|
|
|
territory.present? && country.present? ? I18n.t("geography.subregions.#{country}.#{territory}") : '',
|
|
|
|
country.present? ? I18n.t("geography.countries.#{country}") : ''
|
|
|
|
] - ['', nil]).join(', ')
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.search(str)
|
|
|
|
cache = CityCache.search(str)
|
|
|
|
|
|
|
|
# return the city if this search is in our cache
|
|
|
|
return cache.city if cache.present?
|
|
|
|
|
|
|
|
# look up the city in the geocoder
|
|
|
|
location = Geocoder.search(str, language: 'en').first
|
|
|
|
|
|
|
|
# return nil to indicate that the service is down
|
|
|
|
return nil unless location.present?
|
|
|
|
# see if the city is already present in our database
|
|
|
|
city = City.find_by_place_id(location.data['place_id'])
|
|
|
|
|
|
|
|
# if we didn't find a match by place id, collect the city, territory, and country from the result
|
|
|
|
unless city.present?
|
|
|
|
# google names things differently than we do, we'll look for these items
|
|
|
|
component_alises = {
|
|
|
|
'locality' => :city,
|
|
|
|
'administrative_area_level_1' => :territory,
|
|
|
|
'country' => :country
|
|
|
|
}
|
|
|
|
|
|
|
|
# and populate this map to eventually create the city if we need to
|
|
|
|
city_data = {
|
|
|
|
locale: :en,
|
|
|
|
latitude: location.data['geometry']['location']['lat'],
|
|
|
|
longitude: location.data['geometry']['location']['lng'],
|
|
|
|
place_id: location.data['place_id']
|
|
|
|
}
|
|
|
|
|
|
|
|
# these things are definitely not cities, make sure we don't think they're one
|
|
|
|
not_a_city = [
|
|
|
|
'administrative_area_level_1',
|
|
|
|
'country',
|
|
|
|
'street_address',
|
|
|
|
'street_number',
|
|
|
|
'postal_code',
|
|
|
|
'postal_code_prefix',
|
|
|
|
'route',
|
|
|
|
'intersection',
|
|
|
|
'premise',
|
|
|
|
'subpremise',
|
|
|
|
'natural_feature',
|
|
|
|
'airport',
|
|
|
|
'park',
|
|
|
|
'point_of_interest',
|
|
|
|
'bus_station',
|
|
|
|
'train_station',
|
|
|
|
'transit_station',
|
|
|
|
'room',
|
|
|
|
'post_box',
|
|
|
|
'parking',
|
|
|
|
'establishment',
|
|
|
|
'floor'
|
|
|
|
]
|
|
|
|
|
|
|
|
searched_component = nil
|
|
|
|
location.data['address_components'].each do | component |
|
|
|
|
property = component_alises[component['types'].first]
|
|
|
|
city_data[property] = component['short_name'] if property.present?
|
|
|
|
|
|
|
|
# ideally we will find the component that is labeled a locality but
|
|
|
|
# if that fails we will select what was searched for, hopefully they searched for a city
|
|
|
|
# and not an address or country
|
|
|
|
# some places are not labeled 'locality', search for 'Halifax NS' for example and you will
|
|
|
|
# get 'administrative_area_level_2' since Halifax is a municipality
|
|
|
|
if component['types'] == location.data['types'] && !not_a_city.include?(component['types'].first)
|
|
|
|
searched_component = component['short_name']
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# fall back to the searched component
|
|
|
|
city_data[:city] ||= searched_component
|
|
|
|
|
|
|
|
# we need to have the city and country at least
|
|
|
|
return false unless city_data[:city].present? && city_data[:country].present?
|
|
|
|
|
|
|
|
# one last attempt to make sure we don't already have a record of this city
|
|
|
|
city = City.where(city: city_data[:city], territory: city_data[:territory], country: city_data[:country]).first
|
|
|
|
|
|
|
|
# only if we still can't find the city, then save it as a new one
|
|
|
|
unless city.present?
|
|
|
|
city = City.new(city_data)
|
|
|
|
# if we found exactly what we were looking for, keep these location details
|
|
|
|
# otherwise we may have searched for 'The Bronx' and set the sity the 'New York' but these details will be about The Bronx
|
|
|
|
# so if we try to show New York on a map it will always point to The Bronx, not very fair to those from Staten Island
|
|
|
|
unless city_data[:city] == searched_component
|
|
|
|
new_location = Geocoder.search(str, language: 'en').first
|
|
|
|
city.latitude = new_location.data['geometry']['location']['lat']
|
|
|
|
city.longitude = new_location.data['geometry']['location']['lng']
|
|
|
|
city.place_id = new_location.data['place_id']
|
|
|
|
end
|
|
|
|
|
|
|
|
# and create the new city
|
|
|
|
city.save!
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# save this to our cache
|
|
|
|
CityCache.cache(str, city.id)
|
|
|
|
|
|
|
|
# and return it
|
|
|
|
return city
|
|
|
|
end
|
|
|
|
end
|