diff --git a/.gitignore b/.gitignore index a9305609fd2a51a9899e8a2ef024a70646f496d8..c94bfb4e2fb82925be8231893229c1e294f40b68 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ pkg/* rdoc/* *.gem .bundle +Gemfile.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000000000000000000000000000000000..f642106070e28720590bd6fa0040ef049ebb6dfd --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +rvm: + - 1.8.7 + - 1.9.2 + - 1.9.3 +gemfile: + - Gemfile + - gemfiles/Gemfile.mongoid-2.4.x +env: SSL_CERT_DIR=/etc/ssl/certs +matrix: + exclude: + - rvm: 1.8.7 + gemfile: Gemfile + env: SSL_CERT_DIR=/etc/ssl/certs + - rvm: 1.9.2 + gemfile: Gemfile + env: SSL_CERT_DIR=/etc/ssl/certs + - rvm: 1.9.3 + gemfile: gemfiles/Gemfile.mongoid-2.4.x + env: SSL_CERT_DIR=/etc/ssl/certs diff --git a/CHANGELOG.rdoc b/CHANGELOG.rdoc index 80b266ed33d129406bc1a24d91396e0261b3d663..f4628e246835a91ef842a7a35ed618aab35444e2 100644 --- a/CHANGELOG.rdoc +++ b/CHANGELOG.rdoc @@ -2,6 +2,17 @@ Per-release changes to Geocoder. +== 1.1.2 (2012 May 24) + +* Add ability to specify default units and distance calculation method (thanks github.com/abravalheri). +* Add new (optional) configuration syntax (thanks github.com/abravalheri). +* Add support for cache stores that provide :get and :set methods. +* Add support for custom HTTP request headers (thanks github.com/robotmay). +* Add Result#cache_hit attribute (thanks github.com/s01ipsist). +* Fix: rake geocode:all wasn't properly loading namespaced classes. +* Fix: properly recognize IP addresses with ::ffff: prefix (thanks github.com/brian-ewell). +* Fix: avoid exception during calculations when coordinates not known (thanks github.com/flori). + == 1.1.1 (2012 Feb 16) * Add distance_from_sql class method to geocoded class (thanks github.com/dwilkie). diff --git a/Gemfile b/Gemfile index c80ee3697054566d1a4247d80be78ec3ddfde295..b114997e63766aaffb45ad5066df769b59516390 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,15 @@ source "http://rubygems.org" gemspec + +group :development, :test do + gem 'rake' + gem 'mongoid' + gem 'bson_ext', :platforms => :ruby + + gem 'rails' + + platforms :jruby do + gem 'jruby-openssl' + end +end diff --git a/README.rdoc b/README.rdoc index 883788e5d89a199436bd6e90be123ce9f86b1a77..2d9fcb61287c53693f00223b588e8b57950c214f 100644 --- a/README.rdoc +++ b/README.rdoc @@ -83,6 +83,13 @@ Be sure to read <i>Latitude/Longitude Order</i> in the <i>Notes on MongoDB</i> s MongoMapper is very similar to Mongoid, just be sure to include <tt>Geocoder::Model::MongoMapper</tt>. +=== Mongo Indices + +By default, the methods <tt>geocoded_by</tt> and <tt>reverse_geocoded_by</tt> create a geospatial index. You can avoid index creation with the <tt>:skip_index option</tt>, for example: + + include Geocoder::Model::Mongoid + geocoded_by :address, :skip_index => true + === Bulk Geocoding If you have just added geocoding to an existing application with a lot of objects you can use this Rake task to geocode them all: @@ -191,7 +198,21 @@ If your model has +street+, +city+, +state+, and +country+ attributes you might For reverse geocoding you can also specify an alternate name attribute where the address will be stored, for example: reverse_geocoded_by :latitude, :longitude, :address => :location # ActiveRecord - reverse_geocoded_by :coordinates, :address => :loc # MongoDB + reverse_geocoded_by :coordinates, :address => :loc # MongoDB + + +== Advanced Querying + +When querying for objects (if you're using ActiveRecord) you can also look within a square rather than a radius (circle) by using the <tt>within_bounding_box</tt> scope: + + distance = 20 + center_point = [40.71, 100.23] + box = Geocoder::Calculations.bounding_box(center_point, distance) + Venue.within_bounding_box(box, distance) + +This can also dramatically improve query performance, especially when used in conjunction with indexes on the latitude/longitude columns. Note, however, that returned results do not include +distance+ and +bearing+ attributes. If you want to improve performance AND have access to distance and bearing info, use both scopes: + + Venue.near(center_point, distance).within_bounding_box(box, distance) == Advanced Geocoding @@ -225,32 +246,30 @@ If you're familiar with the results returned by the geocoding service you're usi == Geocoding Services -By default Geocoder uses Google's geocoding API to fetch coordinates and street addresses (FreeGeoIP is used for IP address info). However there are several other APIs supported, as well as a variety of settings. Please see the listing and comparison below for details on specific geocoding services (not all settings are supported by all services). The configuration options are: +By default Geocoder uses Google's geocoding API to fetch coordinates and street addresses (FreeGeoIP is used for IP address info). However there are several other APIs supported, as well as a variety of settings. Please see the listing and comparison below for details on specific geocoding services (not all settings are supported by all services). Some common configuration options are: # config/initializers/geocoder.rb + Geocoder.configure do |config| - # geocoding service (see below for supported options): - Geocoder::Configuration.lookup = :yahoo + # geocoding service (see below for supported options): + config.lookup = :yahoo - # to use an API key: - Geocoder::Configuration.api_key = "..." + # to use an API key: + config.api_key = "..." - # geocoding service request timeout, in seconds (default 3): - Geocoder::Configuration.timeout = 5 + # geocoding service request timeout, in seconds (default 3): + config.timeout = 5 - # use HTTPS for geocoding service connections: - Geocoder::Configuration.use_https = true + # set default units to kilometers: + config.units = :km - # language to use (for search queries and reverse geocoding): - Geocoder::Configuration.language = :de + # caching (see below for details): + config.cache = Redis.new + config.cache_prefix = "..." - # use a proxy to access the service: - Geocoder::Configuration.http_proxy = "127.4.4.1" - Geocoder::Configuration.https_proxy = "127.4.4.2" # only if HTTPS is needed + end - # caching (see below for details) - Geocoder::Configuration.cache = Redis.new - Geocoder::Configuration.cache_prefix = "..." +Please see lib/geocoder/configuration.rb for a complete list of configuration options. === Listing and Comparison @@ -259,7 +278,7 @@ The following is a comparison of the supported geocoding APIs. The "Limitations" ==== Google (<tt>:google</tt>) -API key:: optional (required for Premier) +API key:: required for Premier (do NOT use a key for the free version) Key signup:: http://code.google.com/apis/maps/signup.html Quota:: 2,500 requests/day, 100,000 with Google Maps API Premier Region:: world @@ -307,9 +326,8 @@ Limitations:: Please limit request rate to 1 per second and include your contact ==== Yandex (<tt>:yandex</tt>) -API key:: required -Key signup:: http://api.yandex.ru/maps/intro/concepts/intro.xml#apikey -Quota:: ? +API key:: none +Quota:: 25000 requests / day Region:: Russia SSL support:: no Languages:: Russian, Belarusian, and Ukrainian @@ -331,7 +349,7 @@ Limitations:: "Under no circumstances can our data be re-distributed or re-sold ==== FreeGeoIP API key:: none -Quota:: ? +Quota:: 1000 requests per hour. After reaching the hourly quota, all of your requests will result in HTTP 403 (Forbidden) until it clears up on the next roll over. Region:: world SSL support:: no Languages:: English @@ -436,7 +454,7 @@ Mongo document classes (Mongoid and MongoMapper) have a built-in +near+ scope, b === Latitude/Longitude Order -Coordinates are generally printed and spoken as latitude, then logitude ([lat,lon]). Geocoder respects this convention and always expects method arguments to be given in [lat,lon] order. However, MongoDB requires that coordinates be stored in [lon,lat] order as per the GeoJSON spec (http://geojson.org/geojson-spec.html#positions), so internally they are stored "backwards." However, this does not affect order of arguments to methods when using Mongoid or MongoMapper. +Coordinates are generally printed and spoken as latitude, then longitude ([lat,lon]). Geocoder respects this convention and always expects method arguments to be given in [lat,lon] order. However, MongoDB requires that coordinates be stored in [lon,lat] order as per the GeoJSON spec (http://geojson.org/geojson-spec.html#positions), so internally they are stored "backwards." However, this does not affect order of arguments to methods when using Mongoid or MongoMapper. To access an object's coordinates in the conventional order, use the <tt>to_coordinates</tt> instance method provided by Geocoder. For example: @@ -491,4 +509,4 @@ You cannot use the +near+ scope with another scope that provides an +includes+ o If anyone has a more elegant solution to this problem I am very interested in seeing it. -Copyright (c) 2009-11 Alex Reisner, released under the MIT license +Copyright (c) 2009-12 Alex Reisner, released under the MIT license diff --git a/gemfiles/Gemfile.mongoid-2.4.x b/gemfiles/Gemfile.mongoid-2.4.x new file mode 100644 index 0000000000000000000000000000000000000000..e3fe031df45723a395a423308638b979b3ccac30 --- /dev/null +++ b/gemfiles/Gemfile.mongoid-2.4.x @@ -0,0 +1,15 @@ +source "http://rubygems.org" + +gemspec :path => '..' + +group :development, :test do + gem 'rake' + gem 'mongoid', '2.4.11' + gem 'bson_ext', :platforms => :ruby + + gem 'rails' + + platforms :jruby do + gem 'jruby-openssl' + end +end diff --git a/lib/generators/geocoder/config/config_generator.rb b/lib/generators/geocoder/config/config_generator.rb new file mode 100644 index 0000000000000000000000000000000000000000..3fe3b3c17fecaeb026c87013e0a63f24c05333a8 --- /dev/null +++ b/lib/generators/geocoder/config/config_generator.rb @@ -0,0 +1,14 @@ +require 'rails/generators' + +module Geocoder + class ConfigGenerator < Rails::Generators::Base + source_root File.expand_path("../templates", __FILE__) + + desc "This generator creates an initializer file at config/initializers, " + + "with the default configuration options for Geocoder." + def add_initializer + template "initializer.rb", "config/initializers/geocoder.rb" + end + end +end + diff --git a/lib/generators/geocoder/config/templates/initializer.rb b/lib/generators/geocoder/config/templates/initializer.rb new file mode 100644 index 0000000000000000000000000000000000000000..078cea8b47698710866279e9f43274b7ff4e40be --- /dev/null +++ b/lib/generators/geocoder/config/templates/initializer.rb @@ -0,0 +1,25 @@ +Geocoder.configure do |config| + ## Configurable parameters: if you wish to change some configurable + ## behaviour in Geocoder, feel free to uncomment the following lines + ## and provide custom parameters. + + # config.timeout = 3 # geocoding service timeout (secs) + # config.lookup = :google # name of geocoding service (symbol) + # config.language = :en # ISO-639 language code + # config.use_https = false # use HTTPS for lookup requests? (if supported) + # config.http_proxy = nil # HTTP proxy server (user:pass@host:port) + # config.https_proxy = nil # HTTPS proxy server (user:pass@host:port) + # config.api_key = nil # API key for geocoding service + # config.cache = nil # cache object (must respond to #[], #[]=, and #keys) + # config.cache_prefix = "geocoder:" # prefix (string) to use for all cache keys + + ## exceptions that should not be rescued by default + ## (if you want to implement custom error handling); + ## supports SocketError and TimeoutError + # config.always_raise = [] + + ## Calculation options + # config.units = :mi # :km for kilometers or :mi for miles + # config.distances = :linear # :spherical or :linear +end + diff --git a/lib/geocoder.rb b/lib/geocoder.rb index f5b9d8afb0bba9e35bfafcf6154465cbe4354a00..be59577ad0c5d2006ee3f46c0aa24133b46f6b75 100644 --- a/lib/geocoder.rb +++ b/lib/geocoder.rb @@ -3,9 +3,9 @@ require "geocoder/calculations" require "geocoder/exceptions" require "geocoder/cache" require "geocoder/request" -require "geocoder/models/active_record" -require "geocoder/models/mongoid" -require "geocoder/models/mongo_mapper" +require "geocoder/models/active_record" if defined?(::ActiveRecord) +require "geocoder/models/mongoid" if defined?(::Mongoid) +require "geocoder/models/mongo_mapper" if defined?(::MongoMapper) module Geocoder extend self @@ -115,7 +115,7 @@ module Geocoder # dot-delimited numbers. # def ip_address?(value) - !!value.to_s.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/) + !!value.to_s.match(/^(::ffff:)?(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/) end ## diff --git a/lib/geocoder/cache.rb b/lib/geocoder/cache.rb index 2465d29f43edb82c504fce6bf202387099fb6720..d4cb1ac1c9b35d31a75fc8842b451c497b472dd3 100644 --- a/lib/geocoder/cache.rb +++ b/lib/geocoder/cache.rb @@ -10,14 +10,24 @@ module Geocoder # Read from the Cache. # def [](url) - interpret store[key_for(url)] + interpret case + when store.respond_to?(:[]) + store[key_for(url)] + when store.respond_to?(:get) + store.get key_for(url) + end end ## # Write to the Cache. # def []=(url, value) - store[key_for(url)] = value + case + when store.respond_to?(:[]=) + store[key_for(url)] = value + when store.respond_to?(:set) + store.set key_for(url), value + end end ## diff --git a/lib/geocoder/calculations.rb b/lib/geocoder/calculations.rb index abc8b94740fdc7bd4a6f080ce4d4d956a14d4063..a3f1ff987c66dc5ae78bdf34eb6b940bf7edbc8d 100644 --- a/lib/geocoder/calculations.rb +++ b/lib/geocoder/calculations.rb @@ -21,10 +21,26 @@ module Geocoder # KM_IN_MI = 0.621371192 + # Not a number constant + NAN = defined?(::Float::NAN) ? ::Float::NAN : 0 / 0.0 + + ## + # Returns true if all given arguments are valid latitude/longitude values. + # + def coordinates_present?(*args) + args.each do |a| + # note that Float::NAN != Float::NAN + # still, this could probably be improved: + return false if (!a.is_a?(Numeric) or a.to_s == "NaN") + end + true + end + ## # Distance spanned by one degree of latitude in the given units. # - def latitude_degree_distance(units = :mi) + def latitude_degree_distance(units = nil) + units ||= Geocoder::Configuration.units 2 * Math::PI * earth_radius(units) / 360 end @@ -32,7 +48,8 @@ module Geocoder # Distance spanned by one degree of longitude at the given latitude. # This ranges from around 69 miles at the equator to zero at the poles. # - def longitude_degree_distance(latitude, units = :mi) + def longitude_degree_distance(latitude, units = nil) + units ||= Geocoder::Configuration.units latitude_degree_distance(units) * Math.cos(to_radians(latitude)) end @@ -49,12 +66,13 @@ module Geocoder # # The options hash supports: # - # * <tt>:units</tt> - <tt>:mi</tt> (default) or <tt>:km</tt> + # * <tt>:units</tt> - <tt>:mi</tt> or <tt>:km</tt> + # See Geocoder::Configuration to know how configure default units. # def distance_between(point1, point2, options = {}) # set default options - options[:units] ||= :mi + options[:units] ||= Geocoder::Configuration.units # convert to coordinate arrays point1 = extract_coordinates(point1) @@ -81,17 +99,18 @@ module Geocoder # See Geocoder::Calculations.distance_between for # ways of specifying the points. Also accepts an options hash: # - # * <tt>:method</tt> - <tt>:linear</tt> (default) or <tt>:spherical</tt>; + # * <tt>:method</tt> - <tt>:linear</tt> or <tt>:spherical</tt>; # the spherical method is "correct" in that it returns the shortest path - # (one along a great circle) but the linear method is the default as it - # is less confusing (returns due east or west when given two points with - # the same latitude) + # (one along a great circle) but the linear method is less confusing + # (returns due east or west when given two points with the same latitude). + # See Geocoder::Configuration to know how configure default method. # # Based on: http://www.movable-type.co.uk/scripts/latlong.html # def bearing_between(point1, point2, options = {}) # set default options + options[:method] ||= Geocoder::Configuration.distances options[:method] = :linear unless options[:method] == :spherical # convert to coordinate arrays @@ -177,12 +196,13 @@ module Geocoder # See Geocoder::Calculations.distance_between for # ways of specifying the point. Also accepts an options hash: # - # * <tt>:units</tt> - <tt>:mi</tt> (default) or <tt>:km</tt> + # * <tt>:units</tt> - <tt>:mi</tt> or <tt>:km</tt>. + # See Geocoder::Configuration to know how configure default units. # def bounding_box(point, radius, options = {}) lat,lon = extract_coordinates(point) radius = radius.to_f - units = options[:units] || :mi + units = options[:units] || Geocoder::Configuration.units [ lat - (radius / latitude_degree_distance(units)), lon - (radius / longitude_degree_distance(lat, units)), @@ -219,11 +239,13 @@ module Geocoder end end - def distance_to_radians(distance, units = :mi) + def distance_to_radians(distance, units = nil) + units ||= Geocoder::Configuration.units distance.to_f / earth_radius(units) end - def radians_to_distance(radians, units = :mi) + def radians_to_distance(radians, units = nil) + units ||= Geocoder::Configuration.units radians * earth_radius(units) end @@ -242,9 +264,11 @@ module Geocoder end ## - # Radius of the Earth in the given units (:mi or :km). Default is :mi. + # Radius of the Earth in the given units (:mi or :km). + # See Geocoder::Configuration to know how configure default units. # - def earth_radius(units = :mi) + def earth_radius(units = nil) + units ||= Geocoder::Configuration.units units == :km ? EARTH_RADIUS : to_miles(EARTH_RADIUS) end @@ -270,10 +294,26 @@ module Geocoder # def extract_coordinates(point) case point - when Array; point - when String; Geocoder.coordinates(point) - else point.to_coordinates + when Array + if point.size == 2 + lat, lon = point + if !lat.nil? && lat.respond_to?(:to_f) and + !lon.nil? && lon.respond_to?(:to_f) + then + return [ lat.to_f, lon.to_f ] + end + end + when String + point = Geocoder.coordinates(point) and return point + else + if point.respond_to?(:to_coordinates) + if Array === array = point.to_coordinates + return extract_coordinates(array) + end + end end + [ NAN, NAN ] end end end + diff --git a/lib/geocoder/configuration.rb b/lib/geocoder/configuration.rb index 44baabeebf4547d063ee0798ce643a2d1aebfa86..49c4c5449000a46f19da1f1dcf5a92b1ff5d59d1 100644 --- a/lib/geocoder/configuration.rb +++ b/lib/geocoder/configuration.rb @@ -1,58 +1,103 @@ -module Geocoder - class Configuration - - def self.options_and_defaults - [ - # geocoding service timeout (secs) - [:timeout, 3], +require 'singleton' - # name of geocoding service (symbol) - [:lookup, :google], +module Geocoder - # ISO-639 language code - [:language, :en], + ## + # Provides convenient access to the Configuration singleton. + # + def self.configure(&block) + if block_given? + block.call(Configuration.instance) + else + Configuration.instance + end + end - # use HTTPS for lookup requests? (if supported) - [:use_https, false], + ## + # This class handles geocoder Geocoder configuration + # (geocoding service provider, caching, units of measurement, etc). + # Configuration can be done in two ways: + # + # 1) Using Geocoder.configure and passing a block + # (useful for configuring multiple things at once): + # + # Geocoder.configure do |config| + # config.timeout = 5 + # config.lookup = :yahoo + # config.api_key = "2a9fsa983jaslfj982fjasd" + # config.units = :km + # end + # + # 2) Using the Geocoder::Configuration singleton directly: + # + # Geocoder::Configuration.language = 'pt-BR' + # + # Default values are defined in Configuration#set_defaults. + # + class Configuration + include Singleton - # HTTP proxy server (user:pass@host:port) - [:http_proxy, nil], + OPTIONS = [ + :timeout, + :lookup, + :language, + :http_headers, + :use_https, + :http_proxy, + :https_proxy, + :api_key, + :cache, + :cache_prefix, + :always_raise, + :units, + :distances + ] - # HTTPS proxy server (user:pass@host:port) - [:https_proxy, nil], + attr_accessor *OPTIONS - # API key for geocoding service - # for Google Premier use a 3-element array: [key, client, channel] - [:api_key, nil], + def initialize # :nodoc + set_defaults + end - # cache object (must respond to #[], #[]=, and #keys) - [:cache, nil], + def set_defaults + @timeout = 3 # geocoding service timeout (secs) + @lookup = :google # name of geocoding service (symbol) + @language = :en # ISO-639 language code + @http_headers = {} # HTTP headers for lookup + @use_https = false # use HTTPS for lookup requests? (if supported) + @http_proxy = nil # HTTP proxy server (user:pass@host:port) + @https_proxy = nil # HTTPS proxy server (user:pass@host:port) + @api_key = nil # API key for geocoding service + @cache = nil # cache object (must respond to #[], #[]=, and #keys) + @cache_prefix = "geocoder:" # prefix (string) to use for all cache keys - # prefix (string) to use for all cache keys - [:cache_prefix, "geocoder:"], + # exceptions that should not be rescued by default + # (if you want to implement custom error handling); + # supports SocketError and TimeoutError + @always_raise = [] - # exceptions that should not be rescued by default - # (if you want to implement custom error handling); - # supports SocketError and TimeoutError - [:always_raise, []] - ] + # calculation options + @units = :mi # :mi or :km + @distances = :linear # :linear or :spherical end - # define getters and setters for all configuration settings - self.options_and_defaults.each do |option, default| - class_eval(<<-END, __FILE__, __LINE__ + 1) - - @@#{option} = default unless defined? @@#{option} - - def self.#{option} - @@#{option} - end + instance_eval(OPTIONS.map do |option| + o = option.to_s + <<-EOS + def #{o} + instance.#{o} + end - def self.#{option}=(obj) - @@#{option} = obj - end + def #{o}=(value) + instance.#{o} = value + end + EOS + end.join("\n\n")) - END + class << self + def set_defaults + instance.set_defaults + end end end end diff --git a/lib/geocoder/lookups/base.rb b/lib/geocoder/lookups/base.rb index 838f633c527d76ebac3c9674785752167ec74a9b..fddf8a0d4f3dcbe5fe64c2ae530e164c18c3337c 100644 --- a/lib/geocoder/lookups/base.rb +++ b/lib/geocoder/lookups/base.rb @@ -1,4 +1,5 @@ require 'net/http' +require 'net/https' require 'uri' unless defined?(ActiveSupport::JSON) @@ -33,7 +34,11 @@ module Geocoder else reverse = false end - results(query, reverse).map{ |r| result_class.new(r) } + results(query, reverse).map{ |r| + result = result_class.new(r) + result.cache_hit = @cache_hit if cache + result + } end ## @@ -95,7 +100,7 @@ module Geocoder # Return false if exception not raised. # def raise_error(error, message = nil) - if Geocoder::Configuration.always_raise.include?(error.class) + if Geocoder::Configuration.always_raise.include?( error.is_a?(Class) ? error : error.class ) raise error, message else false @@ -106,29 +111,25 @@ module Geocoder # Returns a parsed search result (Ruby hash). # def fetch_data(query, reverse = false) - begin - parse_raw_data fetch_raw_data(query, reverse) - rescue SocketError => err - raise_error(err) or warn "Geocoding API connection cannot be established." - rescue TimeoutError => err - raise_error(err) or warn "Geocoding API not responding fast enough " + - "(see Geocoder::Configuration.timeout to set limit)." - end + parse_raw_data fetch_raw_data(query, reverse) + rescue SocketError => err + raise_error(err) or warn "Geocoding API connection cannot be established." + rescue TimeoutError => err + raise_error(err) or warn "Geocoding API not responding fast enough " + + "(see Geocoder::Configuration.timeout to set limit)." end ## # Parses a raw search result (returns hash or array). # def parse_raw_data(raw_data) - begin - if defined?(ActiveSupport::JSON) - ActiveSupport::JSON.decode(raw_data) - else - JSON.parse(raw_data) - end - rescue - warn "Geocoding API's response was not valid JSON." + if defined?(ActiveSupport::JSON) + ActiveSupport::JSON.decode(raw_data) + else + JSON.parse(raw_data) end + rescue + warn "Geocoding API's response was not valid JSON." end ## @@ -146,14 +147,17 @@ module Geocoder timeout(Geocoder::Configuration.timeout) do url = query_url(query, reverse) uri = URI.parse(url) - unless cache and body = cache[url] + if cache and body = cache[url] + @cache_hit = true + else client = http_client.new(uri.host, uri.port) client.use_ssl = true if Geocoder::Configuration.use_https - response = client.get(uri.request_uri) + response = client.get(uri.request_uri, Geocoder::Configuration.http_headers) body = response.body if cache and (200..399).include?(response.code.to_i) cache[url] = body end + @cache_hit = false end body end diff --git a/lib/geocoder/lookups/freegeoip.rb b/lib/geocoder/lookups/freegeoip.rb index 63599a487cf768bf7d56033dcd108da17752793f..a5526667cd343808446934a627d3d8f6c0dee102 100644 --- a/lib/geocoder/lookups/freegeoip.rb +++ b/lib/geocoder/lookups/freegeoip.rb @@ -6,6 +6,10 @@ module Geocoder::Lookup private # --------------------------------------------------------------- + def parse_raw_data(raw_data) + raw_data.match(/^<html><title>404/) ? nil : super(raw_data) + end + def results(query, reverse = false) # don't look up a loopback address, just return the stored result return [reserved_result(query)] if loopback_address?(query) diff --git a/lib/geocoder/lookups/geocoder_ca.rb b/lib/geocoder/lookups/geocoder_ca.rb index afaf147caad696e9501e2c80ca5d7a4a69820253..32ea293ab6af7388b0484fd1676ee3af9f57f6ab 100644 --- a/lib/geocoder/lookups/geocoder_ca.rb +++ b/lib/geocoder/lookups/geocoder_ca.rb @@ -33,6 +33,7 @@ module Geocoder::Lookup params[:reverse] = 1 else params[:locate] = query + params[:showpostal] = 1 end "http://geocoder.ca/?" + hash_to_query(params) end diff --git a/lib/geocoder/models/active_record.rb b/lib/geocoder/models/active_record.rb index 67a17106accb7e66632091aac450bed5ceb2f699..29bbe5f406d5534ffc674599c28d4c86340f10ce 100644 --- a/lib/geocoder/models/active_record.rb +++ b/lib/geocoder/models/active_record.rb @@ -14,7 +14,9 @@ module Geocoder :user_address => address_attr, :latitude => options[:latitude] || :latitude, :longitude => options[:longitude] || :longitude, - :geocode_block => block + :geocode_block => block, + :units => options[:units], + :method => options[:method] ) end @@ -27,7 +29,9 @@ module Geocoder :fetched_address => options[:address] || :address, :latitude => latitude_attr, :longitude => longitude_attr, - :reverse_block => block + :reverse_block => block, + :units => options[:units], + :method => options[:method] ) end @@ -39,3 +43,4 @@ module Geocoder end end end + diff --git a/lib/geocoder/models/base.rb b/lib/geocoder/models/base.rb index 836233162770542dc063784b4d448c8324792708..34853a5b7af77a6bb1b692d2d053858b7e7784d1 100644 --- a/lib/geocoder/models/base.rb +++ b/lib/geocoder/models/base.rb @@ -12,7 +12,9 @@ module Geocoder if defined?(@geocoder_options) @geocoder_options elsif superclass.respond_to?(:geocoder_options) - superclass.geocoder_options + superclass.geocoder_options || { } + else + { } end end @@ -24,7 +26,6 @@ module Geocoder fail end - private # ---------------------------------------------------------------- def geocoder_init(options) @@ -38,3 +39,4 @@ module Geocoder end end end + diff --git a/lib/geocoder/models/mongo_base.rb b/lib/geocoder/models/mongo_base.rb index 15bbbabe848b51c99247c2339dae34b764f45b1a..fe7a0e1f5fe968b1178aaed8b655b2087816cc42 100644 --- a/lib/geocoder/models/mongo_base.rb +++ b/lib/geocoder/models/mongo_base.rb @@ -16,7 +16,10 @@ module Geocoder :geocode => true, :user_address => address_attr, :coordinates => options[:coordinates] || :coordinates, - :geocode_block => block + :geocode_block => block, + :units => options[:units], + :method => options[:method], + :skip_index => options[:skip_index] || false ) end @@ -28,7 +31,10 @@ module Geocoder :reverse_geocode => true, :fetched_address => options[:address] || :address, :coordinates => coordinates_attr, - :reverse_block => block + :reverse_block => block, + :units => options[:units], + :method => options[:method], + :skip_index => options[:skip_index] || false ) end @@ -36,7 +42,7 @@ module Geocoder def geocoder_init(options) unless geocoder_initialized? - @geocoder_options = {} + @geocoder_options = { } require "geocoder/stores/#{geocoder_file_name}" include Geocoder::Store.const_get(geocoder_module_name) end @@ -44,12 +50,11 @@ module Geocoder end def geocoder_initialized? - begin - included_modules.include? Geocoder::Store.const_get(geocoder_module_name) - rescue NameError - false - end + included_modules.include? Geocoder::Store.const_get(geocoder_module_name) + rescue NameError + false end end end end + diff --git a/lib/geocoder/models/mongo_mapper.rb b/lib/geocoder/models/mongo_mapper.rb index a093f6bf056a1418ce422c38d7c80994ae3a1011..17d83402e71e78fe65a4e97386d0582e163509fb 100644 --- a/lib/geocoder/models/mongo_mapper.rb +++ b/lib/geocoder/models/mongo_mapper.rb @@ -16,8 +16,10 @@ module Geocoder def geocoder_init(options) super(options) - ensure_index [[ geocoder_options[:coordinates], Mongo::GEO2D ]], - :min => -180, :max => 180 # create 2d index + if options[:skip_index] == false + ensure_index [[ geocoder_options[:coordinates], Mongo::GEO2D ]], + :min => -180, :max => 180 # create 2d index + end end end end diff --git a/lib/geocoder/models/mongoid.rb b/lib/geocoder/models/mongoid.rb index e3ca53a3254c38ca9145f1db221727a2727c5486..9e0b9fd832127dcf2439a06786d78658d7fc9e40 100644 --- a/lib/geocoder/models/mongoid.rb +++ b/lib/geocoder/models/mongoid.rb @@ -16,8 +16,16 @@ module Geocoder def geocoder_init(options) super(options) - index [[ geocoder_options[:coordinates], Mongo::GEO2D ]], - :min => -180, :max => 180 # create 2d index + if options[:skip_index] == false + # create 2d index + if (::Mongoid::VERSION >= "3") + index({ geocoder_options[:coordinates].to_sym => '2d' }, + {:min => -180, :max => 180}) + else + index [[ geocoder_options[:coordinates], '2d' ]], + :min => -180, :max => 180 + end + end end end end diff --git a/lib/geocoder/results/base.rb b/lib/geocoder/results/base.rb index 645e3c2a29e1400f4e25b567aaafed4eba3040de..8a42413f229dff258629fb64785518eee212f3e2 100644 --- a/lib/geocoder/results/base.rb +++ b/lib/geocoder/results/base.rb @@ -1,13 +1,14 @@ module Geocoder module Result class Base - attr_accessor :data + attr_accessor :data, :cache_hit ## # Takes a hash of result data from a parsed Google result document. # def initialize(data) @data = data + @cache_hit = nil end ## diff --git a/lib/geocoder/results/yandex.rb b/lib/geocoder/results/yandex.rb index 841da9887e1c074699960caffa886b2ca0e76f70..bbcf9974b319374ffc368bfc584a873b9e2f51e3 100644 --- a/lib/geocoder/results/yandex.rb +++ b/lib/geocoder/results/yandex.rb @@ -57,6 +57,10 @@ module Geocoder::Result address_details['Locality']['Premise']['PremiseName'] end + def precision + @data['GeoObject']['metaDataProperty']['GeocoderMetaData']['precision'] + end + private # ---------------------------------------------------------------- def address_details diff --git a/lib/geocoder/stores/active_record.rb b/lib/geocoder/stores/active_record.rb index 819e987c4480e622f079ce599df26df26151d81e..782458f524372f95cd2ac9158dcec8115589e494 100644 --- a/lib/geocoder/stores/active_record.rb +++ b/lib/geocoder/stores/active_record.rb @@ -33,10 +33,10 @@ module Geocoder::Store # scope :near, lambda{ |location, *args| latitude, longitude = Geocoder::Calculations.extract_coordinates(location) - if latitude and longitude + if Geocoder::Calculations.coordinates_present?(latitude, longitude) near_scope_options(latitude, longitude, *args) else - where(:id => false) # no results if no lat/lon given + where(false_condition) # no results if no lat/lon given end } @@ -49,7 +49,7 @@ module Geocoder::Store # scope :within_bounding_box, lambda{ |bounds| sw_lat, sw_lng, ne_lat, ne_lng = bounds.flatten if bounds - return where(:id => false) unless sw_lat && sw_lng && ne_lat && ne_lng + return where(false_condition) unless sw_lat && sw_lng && ne_lat && ne_lng spans = "#{geocoder_options[:latitude]} BETWEEN #{sw_lat} AND #{ne_lat} AND " spans << if sw_lng > ne_lng # Handle a box that spans 180 "#{geocoder_options[:longitude]} BETWEEN #{sw_lng} AND 180 OR #{geocoder_options[:longitude]} BETWEEN -180 AND #{ne_lng}" @@ -68,29 +68,33 @@ module Geocoder::Store def distance_from_sql(location, *args) latitude, longitude = Geocoder::Calculations.extract_coordinates(location) - distance_from_sql_options(latitude, longitude, *args) if latitude and longitude + if Geocoder::Calculations.coordinates_present?(latitude, longitude) + distance_from_sql_options(latitude, longitude, *args) + end end private # ---------------------------------------------------------------- ## # Get options hash suitable for passing to ActiveRecord.find to get - # records within a radius (in miles) of the given point. + # records within a radius (in kilometers) of the given point. # Options hash may include: # - # * +:units+ - <tt>:mi</tt> (default) or <tt>:km</tt>; to be used + # * +:units+ - <tt>:mi</tt> or <tt>:km</tt>; to be used. # for interpreting radius as well as the +distance+ attribute which - # is added to each found nearby object - # * +:bearing+ - <tt>:linear</tt> (default) or <tt>:spherical</tt>; + # is added to each found nearby object. + # See Geocoder::Configuration to know how configure default units. + # * +:bearing+ - <tt>:linear</tt> or <tt>:spherical</tt>. # the method to be used for calculating the bearing (direction) # between the given point and each found nearby point; - # set to false for no bearing calculation + # set to false for no bearing calculation. + # See Geocoder::Configuration to know how configure default method. # * +:select+ - string with the SELECT SQL fragment (e.g. “id, nameâ€) # * +:order+ - column(s) for ORDER BY SQL clause; default is distance # * +:exclude+ - an object to exclude (used by the +nearbys+ method) # def near_scope_options(latitude, longitude, radius = 20, options = {}) - if connection.adapter_name.match /sqlite/i + if using_sqlite? approx_near_scope_options(latitude, longitude, radius, options) else full_near_scope_options(latitude, longitude, radius, options) @@ -98,7 +102,7 @@ module Geocoder::Store end def distance_from_sql_options(latitude, longitude, options = {}) - if connection.adapter_name.match /sqlite/i + if using_sqlite? approx_distance_from_sql(latitude, longitude, options) else full_distance_from_sql(latitude, longitude, options) @@ -116,33 +120,35 @@ module Geocoder::Store def full_near_scope_options(latitude, longitude, radius, options) lat_attr = geocoder_options[:latitude] lon_attr = geocoder_options[:longitude] - options[:bearing] = :linear unless options.include?(:bearing) + options[:bearing] ||= (options[:method] || + geocoder_options[:method] || + Geocoder::Configuration.distances) bearing = case options[:bearing] when :linear "CAST(" + "DEGREES(ATAN2( " + - "RADIANS(#{lon_attr} - #{longitude}), " + - "RADIANS(#{lat_attr} - #{latitude})" + + "RADIANS(#{full_column_name(lon_attr)} - #{longitude}), " + + "RADIANS(#{full_column_name(lat_attr)} - #{latitude})" + ")) + 360 " + "AS decimal) % 360" when :spherical "CAST(" + "DEGREES(ATAN2( " + - "SIN(RADIANS(#{lon_attr} - #{longitude})) * " + - "COS(RADIANS(#{lat_attr})), (" + - "COS(RADIANS(#{latitude})) * SIN(RADIANS(#{lat_attr}))" + + "SIN(RADIANS(#{full_column_name(lon_attr)} - #{longitude})) * " + + "COS(RADIANS(#{full_column_name(lat_attr)})), (" + + "COS(RADIANS(#{latitude})) * SIN(RADIANS(#{full_column_name(lat_attr)}))" + ") - (" + - "SIN(RADIANS(#{latitude})) * COS(RADIANS(#{lat_attr})) * " + - "COS(RADIANS(#{lon_attr} - #{longitude}))" + + "SIN(RADIANS(#{latitude})) * COS(RADIANS(#{full_column_name(lat_attr)})) * " + + "COS(RADIANS(#{full_column_name(lon_attr)} - #{longitude}))" + ")" + ")) + 360 " + "AS decimal) % 360" end - + options[:units] ||= (geocoder_options[:units] || Geocoder::Configuration.units) distance = full_distance_from_sql(latitude, longitude, options) conditions = ["#{distance} <= ?", radius] default_near_scope_options(latitude, longitude, radius, options).merge( - :select => "#{options[:select] || "#{table_name}.*"}, " + + :select => "#{options[:select] || full_column_name("*")}, " + "#{distance} AS distance" + (bearing ? ", #{bearing} AS bearing" : ""), :conditions => add_exclude_condition(conditions, options[:exclude]) @@ -160,9 +166,9 @@ module Geocoder::Store earth = Geocoder::Calculations.earth_radius(options[:units] || :mi) "#{earth} * 2 * ASIN(SQRT(" + - "POWER(SIN((#{latitude} - #{table_name}.#{lat_attr}) * PI() / 180 / 2), 2) + " + - "COS(#{latitude} * PI() / 180) * COS(#{table_name}.#{lat_attr} * PI() / 180) * " + - "POWER(SIN((#{longitude} - #{table_name}.#{lon_attr}) * PI() / 180 / 2), 2) ))" + "POWER(SIN((#{latitude} - #{full_column_name(lat_attr)}) * PI() / 180 / 2), 2) + " + + "COS(#{latitude} * PI() / 180) * COS(#{full_column_name(lat_attr)} * PI() / 180) * " + + "POWER(SIN((#{longitude} - #{full_column_name(lon_attr)}) * PI() / 180 / 2), 2) ))" end def approx_distance_from_sql(latitude, longitude, options) @@ -175,8 +181,8 @@ module Geocoder::Store # sin of 45 degrees = average x or y component of vector factor = Math.sin(Math::PI / 4) - "(#{dy} * ABS(#{table_name}.#{lat_attr} - #{latitude}) * #{factor}) + " + - "(#{dx} * ABS(#{table_name}.#{lon_attr} - #{longitude}) * #{factor})" + "(#{dy} * ABS(#{full_column_name(lat_attr)} - #{latitude}) * #{factor}) + " + + "(#{dx} * ABS(#{full_column_name(lon_attr)} - #{longitude}) * #{factor})" end ## @@ -191,27 +197,32 @@ module Geocoder::Store def approx_near_scope_options(latitude, longitude, radius, options) lat_attr = geocoder_options[:latitude] lon_attr = geocoder_options[:longitude] - options[:bearing] = :linear unless options.include?(:bearing) + unless options.include?(:bearing) + options[:bearing] = (options[:method] || \ + geocoder_options[:method] || \ + Geocoder::Configuration.distances) + end if options[:bearing] bearing = "CASE " + - "WHEN (#{lat_attr} >= #{latitude} AND #{lon_attr} >= #{longitude}) THEN 45.0 " + - "WHEN (#{lat_attr} < #{latitude} AND #{lon_attr} >= #{longitude}) THEN 135.0 " + - "WHEN (#{lat_attr} < #{latitude} AND #{lon_attr} < #{longitude}) THEN 225.0 " + - "WHEN (#{lat_attr} >= #{latitude} AND #{lon_attr} < #{longitude}) THEN 315.0 " + + "WHEN (#{full_column_name(lat_attr)} >= #{latitude} AND #{full_column_name(lon_attr)} >= #{longitude}) THEN 45.0 " + + "WHEN (#{full_column_name(lat_attr)} < #{latitude} AND #{full_column_name(lon_attr)} >= #{longitude}) THEN 135.0 " + + "WHEN (#{full_column_name(lat_attr)} < #{latitude} AND #{full_column_name(lon_attr)} < #{longitude}) THEN 225.0 " + + "WHEN (#{full_column_name(lat_attr)} >= #{latitude} AND #{full_column_name(lon_attr)} < #{longitude}) THEN 315.0 " + "END" else bearing = false end distance = approx_distance_from_sql(latitude, longitude, options) + options[:units] ||= (geocoder_options[:units] || Geocoder::Configuration.units) b = Geocoder::Calculations.bounding_box([latitude, longitude], radius, options) conditions = [ - "#{lat_attr} BETWEEN ? AND ? AND #{lon_attr} BETWEEN ? AND ?"] + + "#{full_column_name(lat_attr)} BETWEEN ? AND ? AND #{full_column_name(lon_attr)} BETWEEN ? AND ?"] + [b[0], b[2], b[1], b[3] ] default_near_scope_options(latitude, longitude, radius, options).merge( - :select => "#{options[:select] || "#{table_name}.*"}, " + + :select => "#{options[:select] || full_column_name("*")}, " + "#{distance} AS distance" + (bearing ? ", #{bearing} AS bearing" : ""), :conditions => add_exclude_condition(conditions, options[:exclude]) @@ -235,11 +246,30 @@ module Geocoder::Store # def add_exclude_condition(conditions, exclude) if exclude - conditions[0] << " AND #{table_name}.id != ?" + conditions[0] << " AND #{full_column_name(:id)} != ?" conditions << exclude.id end conditions end + + def using_sqlite? + connection.adapter_name.match /sqlite/i + end + + ## + # Value which can be passed to where() to produce no results. + # + def false_condition + using_sqlite? ? 0 : "false" + end + + ## + # Prepend table name if column name doesn't already contain one. + # + def full_column_name(column) + column = column.to_s + column.include?(".") ? column : [table_name, column].join(".") + end end ## @@ -278,3 +308,4 @@ module Geocoder::Store alias_method :fetch_address, :reverse_geocode end end + diff --git a/lib/geocoder/stores/base.rb b/lib/geocoder/stores/base.rb index f7726c645153738566b4d8468e2e506720a6704b..a9d578f4f55ab521aa0074342f912bb694d9477f 100644 --- a/lib/geocoder/stores/base.rb +++ b/lib/geocoder/stores/base.rb @@ -20,9 +20,10 @@ module Geocoder # Calculate the distance from the object to an arbitrary point. # See Geocoder::Calculations.distance_between for ways of specifying # the point. Also takes a symbol specifying the units - # (:mi or :km; default is :mi). + # (:mi or :km; can be specified in Geocoder configuration). # - def distance_to(point, units = :mi) + def distance_to(point, units = nil) + units ||= self.class.geocoder_options[:units] return nil unless geocoded? Geocoder::Calculations.distance_between( to_coordinates, point, :units => units) @@ -36,6 +37,7 @@ module Geocoder # ways of specifying the point. # def bearing_to(point, options = {}) + options[:method] ||= self.class.geocoder_options[:method] return nil unless geocoded? Geocoder::Calculations.bearing_between( to_coordinates, point, options) @@ -47,6 +49,7 @@ module Geocoder # ways of specifying the point. # def bearing_from(point, options = {}) + options[:method] ||= self.class.geocoder_options[:method] return nil unless geocoded? Geocoder::Calculations.bearing_between( point, to_coordinates, options) @@ -78,7 +81,6 @@ module Geocoder fail end - private # -------------------------------------------------------------- ## @@ -114,3 +116,4 @@ module Geocoder end end end + diff --git a/lib/geocoder/stores/mongo_base.rb b/lib/geocoder/stores/mongo_base.rb index 29054e486f25987c56cfa2dcda63d91c71bd6fc5..13ae12fd02dc5eebd08378a14b28a0440e958c43 100644 --- a/lib/geocoder/stores/mongo_base.rb +++ b/lib/geocoder/stores/mongo_base.rb @@ -20,6 +20,7 @@ module Geocoder::Store radius = args.size > 0 ? args.shift : 20 options = args.size > 0 ? args.shift : {} + options[:units] ||= geocoder_options[:units] # Use BSON::OrderedHash if Ruby's hashes are unordered. # Conditions must be in order required by indexes (see mongo gem). @@ -30,7 +31,7 @@ module Geocoder::Store conds[field] = empty.clone conds[field]["$nearSphere"] = coords.reverse conds[field]["$maxDistance"] = \ - Geocoder::Calculations.distance_to_radians(radius, options[:units] || :mi) + Geocoder::Calculations.distance_to_radians(radius, options[:units]) if obj = options[:exclude] conds[:_id.ne] = obj.id @@ -81,3 +82,4 @@ module Geocoder::Store end end end + diff --git a/lib/geocoder/version.rb b/lib/geocoder/version.rb index ef1e30beed4e1e726c0400949ebac4e75df50952..beb3d4f30633e6ce12db90b77a93841eaee27340 100644 --- a/lib/geocoder/version.rb +++ b/lib/geocoder/version.rb @@ -1,3 +1,3 @@ module Geocoder - VERSION = "1.1.1" + VERSION = "1.1.2" end diff --git a/lib/tasks/geocoder.rake b/lib/tasks/geocoder.rake index f235e1b51b2cb047021042a5d4719f3e4233ab40..33ed4829e1ed76c624106d0fae5deefef82378f7 100644 --- a/lib/tasks/geocoder.rake +++ b/lib/tasks/geocoder.rake @@ -3,10 +3,23 @@ namespace :geocode do task :all => :environment do class_name = ENV['CLASS'] || ENV['class'] raise "Please specify a CLASS (model)" unless class_name - klass = Object.const_get(class_name) + klass = class_from_string(class_name) klass.not_geocoded.each do |obj| obj.geocode; obj.save end end end + +## +# Get a class object from the string given in the shell environment. +# Similar to ActiveSupport's +constantize+ method. +# +def class_from_string(class_name) + parts = class_name.split("::") + constant = Object + parts.each do |part| + constant = constant.const_get(part) + end + constant +end diff --git a/test/calculations_test.rb b/test/calculations_test.rb index 25343cb7a5f3ae3f53153e9bf3fafe57fa1bcdde..4f9fef0df46df8131fb82d67bd7f9b93e61f68d8 100644 --- a/test/calculations_test.rb +++ b/test/calculations_test.rb @@ -2,7 +2,12 @@ require 'test_helper' class CalculationsTest < Test::Unit::TestCase - + def setup + Geocoder.configure do |config| + config.units = :mi + config.distances = :linear + end + end # --- degree distance --- @@ -144,4 +149,40 @@ class CalculationsTest < Test::Unit::TestCase l = Landmark.new(*landmark_params(:msg)) assert_equal l.bearing_from([50,-86.1]), l.bearing_to([50,-86.1]) - 180 end + + def test_extract_coordinates + result = Geocoder::Calculations.extract_coordinates([ nil, nil ]) + assert is_nan_coordinates?(result) + + result = Geocoder::Calculations.extract_coordinates([ 1.0 / 3, 2.0 / 3 ]) + assert_in_delta 1.0 / 3, result.first, 1E-5 + assert_in_delta 2.0 / 3, result.last, 1E-5 + + result = Geocoder::Calculations.extract_coordinates(nil) + assert is_nan_coordinates?(result) + + result = Geocoder::Calculations.extract_coordinates('') + assert is_nan_coordinates?(result) + + result = Geocoder::Calculations.extract_coordinates([ 'nix' ]) + assert is_nan_coordinates?(result) + + o = Object.new + result = Geocoder::Calculations.extract_coordinates(o) + assert is_nan_coordinates?(result) + + def o.to_coordinates + [ 1.0 / 3, 2.0 / 3 ] + end + result = Geocoder::Calculations.extract_coordinates(o) + assert_in_delta 1.0 / 3, result.first, 1E-5 + assert_in_delta 2.0 / 3, result.last, 1E-5 + end + + def test_coordinates_present + assert Geocoder::Calculations.coordinates_present?(3.23) + assert !Geocoder::Calculations.coordinates_present?(nil) + assert !Geocoder::Calculations.coordinates_present?(Geocoder::Calculations::NAN) + assert !Geocoder::Calculations.coordinates_present?(3.23, nil) + end end diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 4c4e668186218874e10611f24cd2f899f667ab79..4137fbf44bf2a8fc6f7e8561f17234944f70e107 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -2,6 +2,9 @@ require 'test_helper' class ConfigurationTest < Test::Unit::TestCase + def setup + Geocoder::Configuration.set_defaults + end def test_exception_raised_on_bad_lookup_config Geocoder::Configuration.lookup = :stoopid @@ -10,4 +13,91 @@ class ConfigurationTest < Test::Unit::TestCase end end + # --- class method configuration --- + def test_configurated_by_class_method + Geocoder::Configuration.units = :mi + distance = Geocoder::Calculations.distance_between([0,0], [0,1]).round + assert_not_equal 111, distance + assert_equal 69, distance + + Geocoder::Configuration.units = :km + distance = Geocoder::Calculations.distance_between([0,0], [0,1]).round + assert_equal 111, distance + assert_not_equal 69, distance + + Geocoder::Configuration.distances = :spherical + angle = Geocoder::Calculations.bearing_between([50,-85], [40.750354, -73.993371]).round + assert_equal 136, angle + assert_not_equal 130, angle + + Geocoder::Configuration.distances = :linear + angle = Geocoder::Calculations.bearing_between([50,-85], [40.750354, -73.993371]).round + assert_not_equal 136, angle + assert_equal 130, angle + end + + # --- Geocoder#configure distances configuration --- + def test_geocoder_configuration + # DSL + Geocoder.configure do |config| + config.units = :mi + config.distances = :linear + end + + assert_equal Geocoder::Configuration.units, :mi + distance = Geocoder::Calculations.distance_between([0,0], [0,1]).round + assert_not_equal 111, distance + assert_equal 69, distance + + assert_equal Geocoder::Configuration.distances, :linear + angle = Geocoder::Calculations.bearing_between([50,-85], [40.750354, -73.993371]).round + assert_not_equal 136, angle + assert_equal 130, angle + + # Direct + Geocoder.configure.units = :km + Geocoder.configure.distances = :spherical + + assert_equal Geocoder::Configuration.units, :km + distance = Geocoder::Calculations.distance_between([0,0], [0,1]).round + assert_equal 111, distance + assert_not_equal 69, distance + + assert_equal Geocoder::Configuration.distances, :spherical + angle = Geocoder::Calculations.bearing_between([50,-85], [40.750354, -73.993371]).round + assert_equal 136, angle + assert_not_equal 130, angle + end + + # Geocoder per-model configuration + def test_model_configuration + Landmark.reverse_geocoded_by :latitude, :longitude, :method => :spherical, :units => :km + assert_equal :km, Landmark.geocoder_options[:units] + assert_equal :spherical, Landmark.geocoder_options[:method] + + v = Landmark.new(*landmark_params(:msg)) + v.latitude = 0 + v.longitude = 0 + assert_equal 111, v.distance_to([0,1]).round + v.latitude = 40.750354 + v.longitude = -73.993371 + assert_equal 136, v.bearing_from([50,-85]).round + end + + def test_configuration_chain + v = Landmark.new(*landmark_params(:msg)) + v.latitude = 0 + v.longitude = 0 + + # method option > global configuration + Geocoder.configure.units = :km + assert_equal 69, v.distance_to([0,1], :mi).round + + # per-model configuration > global configuration + Landmark.reverse_geocoded_by :latitude, :longitude, :method => :spherical, :units => :mi + assert_equal 69, v.distance_to([0,1]).round + + # method option > per-model configuration + assert_equal 111, v.distance_to([0,1], :km).round + end end diff --git a/test/custom_block_test.rb b/test/custom_block_test.rb index 8fab62e819aa6ff05a1b706e185ab7aba92008fc..0f4789d163dc80ab40016016571848f623525499 100644 --- a/test/custom_block_test.rb +++ b/test/custom_block_test.rb @@ -29,3 +29,4 @@ class CustomBlockTest < Test::Unit::TestCase assert_nil e.address end end + diff --git a/test/input_handling_test.rb b/test/input_handling_test.rb index 9d453b97b562c615bc82178495dba36c05e9d9a7..f5da1f13c9df941fdbf08a157464a0dbb0738d7c 100644 --- a/test/input_handling_test.rb +++ b/test/input_handling_test.rb @@ -6,8 +6,10 @@ class InputHandlingTest < Test::Unit::TestCase def test_ip_address_detection assert Geocoder.send(:ip_address?, "232.65.123.94") assert Geocoder.send(:ip_address?, "666.65.123.94") # technically invalid + assert Geocoder.send(:ip_address?, "::ffff:12.34.56.78") assert !Geocoder.send(:ip_address?, "232.65.123.94.43") assert !Geocoder.send(:ip_address?, "232.65.123") + assert !Geocoder.send(:ip_address?, "::ffff:123.456.789") end def test_blank_query_detection diff --git a/test/lookup_test.rb b/test/lookup_test.rb index 3c72d05b84294fce0a7ce0394ee9645d649edf81..0b627c71cd8f4ed12cf878b837b838a8e19b1bc2 100644 --- a/test/lookup_test.rb +++ b/test/lookup_test.rb @@ -27,4 +27,11 @@ class LookupTest < Test::Unit::TestCase g = Geocoder::Lookup::Yahoo.new assert_match "appid=MY_KEY", g.send(:query_url, "Madison Square Garden, New York, NY 10001, United States") end + + def test_geocoder_ca_showpostal + Geocoder::Configuration.api_key = "MY_KEY" + g = Geocoder::Lookup::GeocoderCa.new + assert_match "showpostal=1", g.send(:query_url, "Madison Square Garden, New York, NY 10001, United States") + end + end diff --git a/test/mongoid_test.rb b/test/mongoid_test.rb index 44b4ca9a0798538974177cfb79332657d9908e3b..1af5e9cd56e64d617b0e0fcee676de9b14925e16 100644 --- a/test/mongoid_test.rb +++ b/test/mongoid_test.rb @@ -1,12 +1,7 @@ # encoding: utf-8 -require 'test_helper' - -begin -require 'mongoid' require 'mongoid_test_helper' class MongoidTest < Test::Unit::TestCase - def test_geocoded_check p = Place.new(*venue_params(:msg)) p.location = [40.750354, -73.993371] @@ -22,10 +17,23 @@ class MongoidTest < Test::Unit::TestCase def test_custom_coordinate_field_near_scope location = [40.750354, -73.993371] p = Place.near(location) - assert_equal p.selector[:location]['$nearSphere'], location.reverse + key = Mongoid::VERSION >= "3" ? "location" : :location + assert_equal p.selector[key]['$nearSphere'], location.reverse end -end -rescue LoadError => crash - warn 'Mongoid not installed, not tested.' + def test_model_configuration + p = Place.new(*venue_params(:msg)) + p.location = [0, 0] + + Place.geocoded_by :address, :coordinates => :location, :units => :km + assert_equal 111, p.distance_to([0,1]).round + + Place.geocoded_by :address, :coordinates => :location, :units => :mi + assert_equal 69, p.distance_to([0,1]).round + end + + def test_index_is_skipped_if_skip_option_flag + result = PlaceWithoutIndex.index_options.keys.flatten[0] == :coordinates + assert !result + end end diff --git a/test/mongoid_test_helper.rb b/test/mongoid_test_helper.rb index 69eb2fd044c593898780e3e73fb2f8463905e207..cf37bb01c236fb9b6df27b81f39bcbed5884e1a2 100644 --- a/test/mongoid_test_helper.rb +++ b/test/mongoid_test_helper.rb @@ -1,11 +1,18 @@ require 'rubygems' require 'test/unit' +require 'test_helper' +require 'mongoid' +require 'geocoder/models/mongoid' $LOAD_PATH.unshift(File.dirname(__FILE__)) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) -Mongoid.configure do |config| - config.logger = Logger.new($stderr, :debug) +if (::Mongoid::VERSION >= "3") + Mongoid.logger = Logger.new($stderr, :debug) +else + Mongoid.configure do |config| + config.logger = Logger.new($stderr, :debug) + end end ## @@ -26,3 +33,11 @@ class Place write_attribute :address, address end end + +class PlaceWithoutIndex + include Mongoid::Document + include Geocoder::Model::Mongoid + + field :location, :type => Array + geocoded_by :location, :skip_index => true +end diff --git a/test/test_helper.rb b/test/test_helper.rb index d34b63c0359256e40432bea7c691d83042dc94ad..61b1d058fdf2e6031d4c1087b5b62e2173f50d2f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -150,7 +150,7 @@ module Geocoder end end - class Nominatim < Base + class Nominatim < Base private #----------------------------------------------------------------- def fetch_raw_data(query, reverse = false) raise TimeoutError if query == "timeout" @@ -271,4 +271,11 @@ class Test::Unit::TestCase def street_lookups all_lookups - [:freegeoip] end + + def is_nan_coordinates?(coordinates) + return false unless coordinates.respond_to? :size # Should be an array + return false unless coordinates.size == 2 # Should have dimension 2 + coordinates[0].nan? && coordinates[1].nan? # Both coordinates should be NaN + end end +