lib/geokit/geocoders.rb
require 'geokit/net_adapter/net_http'
require 'geokit/net_adapter/typhoeus'
require 'ipaddr'
require 'json'
require 'logger'
require 'net/http'
require 'openssl'
require 'timeout'
require 'yaml'
module Geokit
require File.join(File.dirname(__FILE__), 'inflectors')
# Contains a range of geocoders:
#
# ### "regular" address geocoders
# * Yahoo Geocoder - requires an API key.
# * Geocoder.ca - for Canada; may require authentication as well.
# * Geonames - a free geocoder
#
# ### address geocoders that also provide reverse geocoding
# * Google Geocoder - requires an API key.
#
# ### IP address geocoders
# * IP Geocoder - geocodes an IP address using hostip.info's web service.
# * Geoplugin.net -- another IP address geocoder
# * IP-API.com -- another IP address geocoder
#
# ### The Multigeocoder
# * Multi Geocoder - provides failover for the physical location geocoders.
#
# Some of these geocoders require configuration. You don't have to provide it here. See the README.
module Geocoders
@@proxy = nil
@@useragent = nil
@@request_timeout = nil
@@provider_order = [:google, :us]
@@ip_provider_order = [:geo_plugin, :ip]
@@logger = Logger.new(STDOUT)
@@logger.level = Logger::INFO
@@host = nil
@@domain = nil
@@net_adapter = Geokit::NetAdapter::NetHttp
@@secure = true
@@ssl_verify_mode = OpenSSL::SSL::VERIFY_PEER
def self.__define_accessors
class_variables.each do |v|
sym = v.to_s.delete('@').to_sym
next if self.respond_to? sym
module_eval <<-EOS, __FILE__, __LINE__
def self.#{sym}
value = if defined?(#{sym.to_s.upcase})
#{sym.to_s.upcase}
else
@@#{sym}
end
if value.is_a?(Hash)
value = (self.domain.nil? ? nil : value[self.domain]) || value.values.first
end
value
end
def self.#{sym}=(obj)
@@#{sym} = obj
end
EOS
end
end
__define_accessors
# Error which is thrown in the event a geocoding error occurs.
class GeocodeError < StandardError; end
class TooManyQueriesError < StandardError; end
class AccessDeniedError < StandardError; end
class NoSuchGeocoderError < StandardError; end
# -------------------------------------------------------------------------------------------
# Geocoder Base class -- every geocoder should inherit from this
# -------------------------------------------------------------------------------------------
# The Geocoder base class which defines the interface to be used by all
# other geocoders.
class Geocoder
# Main method which calls the do_geocode template method which subclasses
# are responsible for implementing. Returns a populated GeoLoc or an
# empty one with a failed success code.
def self.geocode(address, *args)
logger.debug "#{provider_name} geocoding. address: #{address}, args #{args}"
do_geocode(address, *args) || GeoLoc.new
rescue TooManyQueriesError, GeocodeError, AccessDeniedError, NoSuchGeocoderError
raise
rescue => e
logger.error "Caught an error during #{provider_name} geocoding call: #{$!}"
logger.error e.backtrace.join("\n")
GeoLoc.new
end
# Main method which calls the do_reverse_geocode template method which subclasses
# are responsible for implementing. Returns a populated GeoLoc or an
# empty one with a failed success code.
def self.reverse_geocode(latlng, *args)
logger.debug "#{provider_name} geocoding. latlng: #{latlng}, args #{args}"
do_reverse_geocode(latlng, *args) || GeoLoc.new
end
protected
def self.logger
Geokit::Geocoders.logger
end
private
def self.config(*attrs)
attrs.each do |attr|
class_eval <<-METHOD
@@#{attr} = nil
def self.#{attr}=(value)
@@#{attr} = value
end
def self.#{attr}
@@#{attr}
end
METHOD
end
end
def self.inherited(base)
base.config :secure
end
def self.new_loc
loc = GeoLoc.new
loc.provider = Geokit::Inflector.underscore(provider_name)
loc
end
# Call the geocoder service using the timeout if configured.
def self.call_geocoder_service(url)
Timeout.timeout(Geokit::Geocoders.request_timeout) { return do_get(url) } if Geokit::Geocoders.request_timeout
do_get(url)
rescue Timeout::Error
nil
end
# Not all geocoders can do reverse geocoding. So, unless the subclass explicitly overrides this method,
# a call to reverse_geocode will return an empty GeoLoc. If you happen to be using MultiGeocoder,
# this will cause it to failover to the next geocoder, which will hopefully be one which supports reverse geocoding.
def self.do_reverse_geocode(_latlng)
GeoLoc.new
end
def self.use_https?
secure && Geokit::Geocoders.secure
end
def self.protocol
use_https? ? 'https' : 'http'
end
# Wraps the geocoder call around a proxy if necessary.
def self.do_get(url)
net_adapter.do_get(url)
end
def self.net_adapter
Geokit::Geocoders.net_adapter
end
def self.provider_name
name.split('::').last.gsub(/Geocoder$/, '')
end
def self.parse(format, body, *args)
logger.debug "#{provider_name} geocoding. Result: #{CGI.escape(body)}"
if format == :xml
begin
require 'rexml/document'
rescue LoadError
logger.error "REXML load error, if using Ruby 3.0 add 'rexml' to your Gemfile"
raise
end
end
case format
when :json then parse_json(JSON.load(body), *args)
when :xml then parse_xml(REXML::Document.new(body), *args)
when :yaml then parse_yaml(YAML.load(body), *args)
when :csv then parse_csv(body.chomp.split(','), *args)
end
end
def self.set_mappings(loc, xml, mappings)
mappings.each_pair do |field, xml_field|
loc.send("#{field}=", xml.elements[xml_field].try(:text))
end
end
def self.process(format, url, *args)
res = call_geocoder_service(url)
return GeoLoc.new unless net_adapter.success?(res)
parse format, res.body, *args
end
end
# -------------------------------------------------------------------------------------------
# "Regular" Address geocoders
# -------------------------------------------------------------------------------------------
require File.join(File.dirname(__FILE__), 'geocoders/base_ip')
Dir[File.join(File.dirname(__FILE__), '/geocoders/*.rb')].each { |f| require f }
require File.join(File.dirname(__FILE__), 'multi_geocoder')
end
end