bw-geocoder 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +6 -0
- data/.travis.yml +31 -0
- data/CHANGELOG.md +377 -0
- data/LICENSE +20 -0
- data/README.md +1041 -0
- data/Rakefile +25 -0
- data/bin/geocode +5 -0
- data/examples/autoexpire_cache_dalli.rb +62 -0
- data/examples/autoexpire_cache_redis.rb +28 -0
- data/examples/cache_bypass.rb +48 -0
- data/gemfiles/Gemfile.mongoid-2.4.x +16 -0
- data/lib/generators/geocoder/config/config_generator.rb +14 -0
- data/lib/generators/geocoder/config/templates/initializer.rb +21 -0
- data/lib/generators/geocoder/maxmind/geolite_city_generator.rb +28 -0
- data/lib/generators/geocoder/maxmind/geolite_country_generator.rb +28 -0
- data/lib/generators/geocoder/maxmind/templates/migration/geolite_city.rb +30 -0
- data/lib/generators/geocoder/maxmind/templates/migration/geolite_country.rb +17 -0
- data/lib/geocoder/cache.rb +90 -0
- data/lib/geocoder/calculations.rb +428 -0
- data/lib/geocoder/cli.rb +121 -0
- data/lib/geocoder/configuration.rb +124 -0
- data/lib/geocoder/configuration_hash.rb +11 -0
- data/lib/geocoder/exceptions.rb +21 -0
- data/lib/geocoder/ip_address.rb +21 -0
- data/lib/geocoder/lookup.rb +100 -0
- data/lib/geocoder/lookups/baidu.rb +55 -0
- data/lib/geocoder/lookups/baidu_ip.rb +54 -0
- data/lib/geocoder/lookups/base.rb +302 -0
- data/lib/geocoder/lookups/bing.rb +59 -0
- data/lib/geocoder/lookups/dstk.rb +20 -0
- data/lib/geocoder/lookups/esri.rb +48 -0
- data/lib/geocoder/lookups/freegeoip.rb +47 -0
- data/lib/geocoder/lookups/geocoder_ca.rb +54 -0
- data/lib/geocoder/lookups/geocoder_us.rb +39 -0
- data/lib/geocoder/lookups/geocodio.rb +42 -0
- data/lib/geocoder/lookups/google.rb +67 -0
- data/lib/geocoder/lookups/google_places_details.rb +50 -0
- data/lib/geocoder/lookups/google_premier.rb +47 -0
- data/lib/geocoder/lookups/here.rb +62 -0
- data/lib/geocoder/lookups/ip_address_labs.rb +43 -0
- data/lib/geocoder/lookups/mapquest.rb +60 -0
- data/lib/geocoder/lookups/maxmind.rb +90 -0
- data/lib/geocoder/lookups/maxmind_local.rb +58 -0
- data/lib/geocoder/lookups/nominatim.rb +52 -0
- data/lib/geocoder/lookups/okf.rb +43 -0
- data/lib/geocoder/lookups/opencagedata.rb +58 -0
- data/lib/geocoder/lookups/ovi.rb +62 -0
- data/lib/geocoder/lookups/pointpin.rb +68 -0
- data/lib/geocoder/lookups/smarty_streets.rb +45 -0
- data/lib/geocoder/lookups/telize.rb +40 -0
- data/lib/geocoder/lookups/test.rb +44 -0
- data/lib/geocoder/lookups/yahoo.rb +88 -0
- data/lib/geocoder/lookups/yandex.rb +54 -0
- data/lib/geocoder/models/active_record.rb +50 -0
- data/lib/geocoder/models/base.rb +39 -0
- data/lib/geocoder/models/mongo_base.rb +64 -0
- data/lib/geocoder/models/mongo_mapper.rb +26 -0
- data/lib/geocoder/models/mongoid.rb +32 -0
- data/lib/geocoder/query.rb +111 -0
- data/lib/geocoder/railtie.rb +26 -0
- data/lib/geocoder/request.rb +25 -0
- data/lib/geocoder/results/baidu.rb +79 -0
- data/lib/geocoder/results/baidu_ip.rb +62 -0
- data/lib/geocoder/results/base.rb +67 -0
- data/lib/geocoder/results/bing.rb +48 -0
- data/lib/geocoder/results/dstk.rb +6 -0
- data/lib/geocoder/results/esri.rb +51 -0
- data/lib/geocoder/results/freegeoip.rb +45 -0
- data/lib/geocoder/results/geocoder_ca.rb +60 -0
- data/lib/geocoder/results/geocoder_us.rb +39 -0
- data/lib/geocoder/results/geocodio.rb +66 -0
- data/lib/geocoder/results/google.rb +124 -0
- data/lib/geocoder/results/google_places_details.rb +35 -0
- data/lib/geocoder/results/google_premier.rb +6 -0
- data/lib/geocoder/results/here.rb +62 -0
- data/lib/geocoder/results/ip_address_labs.rb +78 -0
- data/lib/geocoder/results/mapquest.rb +51 -0
- data/lib/geocoder/results/maxmind.rb +135 -0
- data/lib/geocoder/results/maxmind_local.rb +49 -0
- data/lib/geocoder/results/nominatim.rb +94 -0
- data/lib/geocoder/results/okf.rb +106 -0
- data/lib/geocoder/results/opencagedata.rb +82 -0
- data/lib/geocoder/results/ovi.rb +62 -0
- data/lib/geocoder/results/pointpin.rb +44 -0
- data/lib/geocoder/results/smarty_streets.rb +106 -0
- data/lib/geocoder/results/telize.rb +45 -0
- data/lib/geocoder/results/test.rb +33 -0
- data/lib/geocoder/results/yahoo.rb +55 -0
- data/lib/geocoder/results/yandex.rb +84 -0
- data/lib/geocoder/sql.rb +107 -0
- data/lib/geocoder/stores/active_record.rb +278 -0
- data/lib/geocoder/stores/base.rb +127 -0
- data/lib/geocoder/stores/mongo_base.rb +89 -0
- data/lib/geocoder/stores/mongo_mapper.rb +13 -0
- data/lib/geocoder/stores/mongoid.rb +13 -0
- data/lib/geocoder/version.rb +3 -0
- data/lib/geocoder.rb +47 -0
- data/lib/hash_recursive_merge.rb +74 -0
- data/lib/maxmind_database.rb +109 -0
- data/lib/oauth_util.rb +112 -0
- data/lib/tasks/geocoder.rake +29 -0
- data/lib/tasks/maxmind.rake +73 -0
- data/test/fixtures/baidu_invalid_key +1 -0
- data/test/fixtures/baidu_ip_202_198_16_3 +19 -0
- data/test/fixtures/baidu_ip_invalid_key +1 -0
- data/test/fixtures/baidu_ip_no_results +1 -0
- data/test/fixtures/baidu_no_results +1 -0
- data/test/fixtures/baidu_reverse +1 -0
- data/test/fixtures/baidu_shanghai_pearl_tower +12 -0
- data/test/fixtures/bing_invalid_key +1 -0
- data/test/fixtures/bing_madison_square_garden +40 -0
- data/test/fixtures/bing_no_results +16 -0
- data/test/fixtures/bing_reverse +42 -0
- data/test/fixtures/cloudmade_invalid_key +1 -0
- data/test/fixtures/cloudmade_madison_square_garden +1 -0
- data/test/fixtures/cloudmade_no_results +1 -0
- data/test/fixtures/esri_madison_square_garden +59 -0
- data/test/fixtures/esri_no_results +8 -0
- data/test/fixtures/esri_reverse +21 -0
- data/test/fixtures/freegeoip_74_200_247_59 +12 -0
- data/test/fixtures/freegeoip_no_results +1 -0
- data/test/fixtures/geocoder_ca_madison_square_garden +1 -0
- data/test/fixtures/geocoder_ca_no_results +1 -0
- data/test/fixtures/geocoder_ca_reverse +34 -0
- data/test/fixtures/geocoder_us_madison_square_garden +1 -0
- data/test/fixtures/geocoder_us_no_results +1 -0
- data/test/fixtures/geocodio_1101_pennsylvania_ave +1 -0
- data/test/fixtures/geocodio_bad_api_key +3 -0
- data/test/fixtures/geocodio_invalid +4 -0
- data/test/fixtures/geocodio_no_results +1 -0
- data/test/fixtures/geocodio_over_query_limit +4 -0
- data/test/fixtures/google_garbage +456 -0
- data/test/fixtures/google_madison_square_garden +57 -0
- data/test/fixtures/google_no_city_data +44 -0
- data/test/fixtures/google_no_locality +51 -0
- data/test/fixtures/google_no_results +4 -0
- data/test/fixtures/google_over_limit +4 -0
- data/test/fixtures/google_places_details_invalid_request +4 -0
- data/test/fixtures/google_places_details_madison_square_garden +120 -0
- data/test/fixtures/google_places_details_no_results +4 -0
- data/test/fixtures/google_places_details_no_reviews +60 -0
- data/test/fixtures/google_places_details_no_types +66 -0
- data/test/fixtures/here_madison_square_garden +72 -0
- data/test/fixtures/here_no_results +8 -0
- data/test/fixtures/mapquest_error +16 -0
- data/test/fixtures/mapquest_invalid_api_key +16 -0
- data/test/fixtures/mapquest_invalid_request +16 -0
- data/test/fixtures/mapquest_madison_square_garden +52 -0
- data/test/fixtures/mapquest_no_results +16 -0
- data/test/fixtures/maxmind_24_24_24_21 +1 -0
- data/test/fixtures/maxmind_24_24_24_22 +1 -0
- data/test/fixtures/maxmind_24_24_24_23 +1 -0
- data/test/fixtures/maxmind_24_24_24_24 +1 -0
- data/test/fixtures/maxmind_74_200_247_59 +1 -0
- data/test/fixtures/maxmind_invalid_key +1 -0
- data/test/fixtures/maxmind_no_results +1 -0
- data/test/fixtures/nominatim_madison_square_garden +150 -0
- data/test/fixtures/nominatim_no_results +1 -0
- data/test/fixtures/nominatim_over_limit +1 -0
- data/test/fixtures/okf_kirstinmaki +67 -0
- data/test/fixtures/okf_no_results +4 -0
- data/test/fixtures/opencagedata_invalid_api_key +25 -0
- data/test/fixtures/opencagedata_invalid_request +26 -0
- data/test/fixtures/opencagedata_madison_square_garden +73 -0
- data/test/fixtures/opencagedata_no_results +29 -0
- data/test/fixtures/opencagedata_over_limit +31 -0
- data/test/fixtures/ovi_madison_square_garden +72 -0
- data/test/fixtures/ovi_no_results +8 -0
- data/test/fixtures/pointpin_10_10_10_10 +1 -0
- data/test/fixtures/pointpin_555_555_555_555 +1 -0
- data/test/fixtures/pointpin_80_111_555_555 +1 -0
- data/test/fixtures/pointpin_no_results +1 -0
- data/test/fixtures/smarty_streets_11211 +1 -0
- data/test/fixtures/smarty_streets_madison_square_garden +47 -0
- data/test/fixtures/smarty_streets_no_results +1 -0
- data/test/fixtures/telize_10_10_10_10 +1 -0
- data/test/fixtures/telize_555_555_555_555 +4 -0
- data/test/fixtures/telize_74_200_247_59 +1 -0
- data/test/fixtures/telize_no_results +1 -0
- data/test/fixtures/yahoo_error +1 -0
- data/test/fixtures/yahoo_invalid_key +2 -0
- data/test/fixtures/yahoo_madison_square_garden +52 -0
- data/test/fixtures/yahoo_no_results +10 -0
- data/test/fixtures/yahoo_over_limit +2 -0
- data/test/fixtures/yandex_canada_rue_dupuis_14 +446 -0
- data/test/fixtures/yandex_invalid_key +1 -0
- data/test/fixtures/yandex_kremlin +48 -0
- data/test/fixtures/yandex_new_york +1 -0
- data/test/fixtures/yandex_no_city_and_town +112 -0
- data/test/fixtures/yandex_no_results +16 -0
- data/test/integration/http_client_test.rb +31 -0
- data/test/mongoid_test_helper.rb +43 -0
- data/test/test_helper.rb +386 -0
- data/test/unit/active_record_test.rb +16 -0
- data/test/unit/cache_test.rb +37 -0
- data/test/unit/calculations_test.rb +220 -0
- data/test/unit/configuration_test.rb +55 -0
- data/test/unit/error_handling_test.rb +56 -0
- data/test/unit/geocoder_test.rb +78 -0
- data/test/unit/https_test.rb +17 -0
- data/test/unit/ip_address_test.rb +27 -0
- data/test/unit/lookup_test.rb +153 -0
- data/test/unit/lookups/bing_test.rb +68 -0
- data/test/unit/lookups/dstk_test.rb +26 -0
- data/test/unit/lookups/esri_test.rb +48 -0
- data/test/unit/lookups/freegeoip_test.rb +27 -0
- data/test/unit/lookups/geocoder_ca_test.rb +17 -0
- data/test/unit/lookups/geocodio_test.rb +55 -0
- data/test/unit/lookups/google_places_details_test.rb +122 -0
- data/test/unit/lookups/google_premier_test.rb +22 -0
- data/test/unit/lookups/google_test.rb +84 -0
- data/test/unit/lookups/mapquest_test.rb +60 -0
- data/test/unit/lookups/maxmind_local_test.rb +28 -0
- data/test/unit/lookups/maxmind_test.rb +63 -0
- data/test/unit/lookups/nominatim_test.rb +31 -0
- data/test/unit/lookups/okf_test.rb +38 -0
- data/test/unit/lookups/opencagedata_test.rb +64 -0
- data/test/unit/lookups/pointpin_test.rb +30 -0
- data/test/unit/lookups/smarty_streets_test.rb +71 -0
- data/test/unit/lookups/telize_test.rb +36 -0
- data/test/unit/lookups/yahoo_test.rb +35 -0
- data/test/unit/method_aliases_test.rb +26 -0
- data/test/unit/model_test.rb +38 -0
- data/test/unit/mongoid_test.rb +47 -0
- data/test/unit/near_test.rb +87 -0
- data/test/unit/oauth_util_test.rb +31 -0
- data/test/unit/proxy_test.rb +37 -0
- data/test/unit/query_test.rb +52 -0
- data/test/unit/rake_task_test.rb +21 -0
- data/test/unit/request_test.rb +35 -0
- data/test/unit/result_test.rb +72 -0
- data/test/unit/test_mode_test.rb +70 -0
- metadata +281 -0
data/lib/geocoder/sql.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
module Geocoder
|
2
|
+
module Sql
|
3
|
+
extend self
|
4
|
+
|
5
|
+
##
|
6
|
+
# Distance calculation for use with a database that supports POWER(),
|
7
|
+
# SQRT(), PI(), and trigonometric functions SIN(), COS(), ASIN(),
|
8
|
+
# ATAN2(), DEGREES(), and RADIANS().
|
9
|
+
#
|
10
|
+
# Based on the excellent tutorial at:
|
11
|
+
# http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
|
12
|
+
#
|
13
|
+
def full_distance(latitude, longitude, lat_attr, lon_attr, options = {})
|
14
|
+
units = options[:units] || Geocoder.config.units
|
15
|
+
earth = Geocoder::Calculations.earth_radius(units)
|
16
|
+
|
17
|
+
"#{earth} * 2 * ASIN(SQRT(" +
|
18
|
+
"POWER(SIN((#{latitude.to_f} - #{lat_attr}) * PI() / 180 / 2), 2) + " +
|
19
|
+
"COS(#{latitude.to_f} * PI() / 180) * COS(#{lat_attr} * PI() / 180) * " +
|
20
|
+
"POWER(SIN((#{longitude.to_f} - #{lon_attr}) * PI() / 180 / 2), 2)" +
|
21
|
+
"))"
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# Distance calculation for use with a database without trigonometric
|
26
|
+
# functions, like SQLite. Approach is to find objects within a square
|
27
|
+
# rather than a circle, so results are very approximate (will include
|
28
|
+
# objects outside the given radius).
|
29
|
+
#
|
30
|
+
# Distance and bearing calculations are *extremely inaccurate*. To be
|
31
|
+
# clear: this only exists to provide interface consistency. Results
|
32
|
+
# are not intended for use in production!
|
33
|
+
#
|
34
|
+
def approx_distance(latitude, longitude, lat_attr, lon_attr, options = {})
|
35
|
+
units = options[:units] || Geocoder.config.units
|
36
|
+
dx = Geocoder::Calculations.longitude_degree_distance(30, units)
|
37
|
+
dy = Geocoder::Calculations.latitude_degree_distance(units)
|
38
|
+
|
39
|
+
# sin of 45 degrees = average x or y component of vector
|
40
|
+
factor = Math.sin(Math::PI / 4)
|
41
|
+
|
42
|
+
"(#{dy} * ABS(#{lat_attr} - #{latitude.to_f}) * #{factor}) + " +
|
43
|
+
"(#{dx} * ABS(#{lon_attr} - #{longitude.to_f}) * #{factor})"
|
44
|
+
end
|
45
|
+
|
46
|
+
def within_bounding_box(sw_lat, sw_lng, ne_lat, ne_lng, lat_attr, lon_attr)
|
47
|
+
spans = "#{lat_attr} BETWEEN #{sw_lat} AND #{ne_lat} AND "
|
48
|
+
# handle box that spans 180 longitude
|
49
|
+
if sw_lng.to_f > ne_lng.to_f
|
50
|
+
spans + "#{lon_attr} BETWEEN #{sw_lng} AND 180 OR " +
|
51
|
+
"#{lon_attr} BETWEEN -180 AND #{ne_lng}"
|
52
|
+
else
|
53
|
+
spans + "#{lon_attr} BETWEEN #{sw_lng} AND #{ne_lng}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# Fairly accurate bearing calculation. Takes a latitude, longitude,
|
59
|
+
# and an options hash which must include a :bearing value
|
60
|
+
# (:linear or :spherical).
|
61
|
+
#
|
62
|
+
# Based on:
|
63
|
+
# http://www.beginningspatial.com/calculating_bearing_one_point_another
|
64
|
+
#
|
65
|
+
def full_bearing(latitude, longitude, lat_attr, lon_attr, options = {})
|
66
|
+
degrees_per_radian = Geocoder::Calculations::DEGREES_PER_RADIAN
|
67
|
+
case options[:bearing] || Geocoder.config.distances
|
68
|
+
when :linear
|
69
|
+
"MOD(CAST(" +
|
70
|
+
"(ATAN2( " +
|
71
|
+
"((#{lon_attr} - #{longitude.to_f}) / #{degrees_per_radian}), " +
|
72
|
+
"((#{lat_attr} - #{latitude.to_f}) / #{degrees_per_radian})" +
|
73
|
+
") * #{degrees_per_radian}) + 360 " +
|
74
|
+
"AS decimal), 360)"
|
75
|
+
when :spherical
|
76
|
+
"MOD(CAST(" +
|
77
|
+
"(ATAN2( " +
|
78
|
+
"SIN( (#{lon_attr} - #{longitude.to_f}) / #{degrees_per_radian} ) * " +
|
79
|
+
"COS( (#{lat_attr}) / #{degrees_per_radian} ), (" +
|
80
|
+
"COS( (#{latitude.to_f}) / #{degrees_per_radian} ) * SIN( (#{lat_attr}) / #{degrees_per_radian})" +
|
81
|
+
") - (" +
|
82
|
+
"SIN( (#{latitude.to_f}) / #{degrees_per_radian}) * COS((#{lat_attr}) / #{degrees_per_radian}) * " +
|
83
|
+
"COS( (#{lon_attr} - #{longitude.to_f}) / #{degrees_per_radian})" +
|
84
|
+
")" +
|
85
|
+
") * #{degrees_per_radian}) + 360 " +
|
86
|
+
"AS decimal), 360)"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# Totally lame bearing calculation. Basically useless except that it
|
92
|
+
# returns *something* in databases without trig functions.
|
93
|
+
#
|
94
|
+
def approx_bearing(latitude, longitude, lat_attr, lon_attr, options = {})
|
95
|
+
"CASE " +
|
96
|
+
"WHEN (#{lat_attr} >= #{latitude.to_f} AND " +
|
97
|
+
"#{lon_attr} >= #{longitude.to_f}) THEN 45.0 " +
|
98
|
+
"WHEN (#{lat_attr} < #{latitude.to_f} AND " +
|
99
|
+
"#{lon_attr} >= #{longitude.to_f}) THEN 135.0 " +
|
100
|
+
"WHEN (#{lat_attr} < #{latitude.to_f} AND " +
|
101
|
+
"#{lon_attr} < #{longitude.to_f}) THEN 225.0 " +
|
102
|
+
"WHEN (#{lat_attr} >= #{latitude.to_f} AND " +
|
103
|
+
"#{lon_attr} < #{longitude.to_f}) THEN 315.0 " +
|
104
|
+
"END"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,278 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'geocoder/sql'
|
3
|
+
require 'geocoder/stores/base'
|
4
|
+
|
5
|
+
##
|
6
|
+
# Add geocoding functionality to any ActiveRecord object.
|
7
|
+
#
|
8
|
+
module Geocoder::Store
|
9
|
+
module ActiveRecord
|
10
|
+
include Base
|
11
|
+
|
12
|
+
##
|
13
|
+
# Implementation of 'included' hook method.
|
14
|
+
#
|
15
|
+
def self.included(base)
|
16
|
+
base.extend ClassMethods
|
17
|
+
base.class_eval do
|
18
|
+
|
19
|
+
# scope: geocoded objects
|
20
|
+
scope :geocoded, lambda {
|
21
|
+
where("#{geocoder_options[:latitude]} IS NOT NULL " +
|
22
|
+
"AND #{geocoder_options[:longitude]} IS NOT NULL")
|
23
|
+
}
|
24
|
+
|
25
|
+
# scope: not-geocoded objects
|
26
|
+
scope :not_geocoded, lambda {
|
27
|
+
where("#{geocoder_options[:latitude]} IS NULL " +
|
28
|
+
"OR #{geocoder_options[:longitude]} IS NULL")
|
29
|
+
}
|
30
|
+
|
31
|
+
##
|
32
|
+
# Find all objects within a radius of the given location.
|
33
|
+
# Location may be either a string to geocode or an array of
|
34
|
+
# coordinates (<tt>[lat,lon]</tt>). Also takes an options hash
|
35
|
+
# (see Geocoder::Store::ActiveRecord::ClassMethods.near_scope_options
|
36
|
+
# for details).
|
37
|
+
#
|
38
|
+
scope :near, lambda{ |location, *args|
|
39
|
+
latitude, longitude = Geocoder::Calculations.extract_coordinates(location)
|
40
|
+
if Geocoder::Calculations.coordinates_present?(latitude, longitude)
|
41
|
+
options = near_scope_options(latitude, longitude, *args)
|
42
|
+
select(options[:select]).where(options[:conditions]).
|
43
|
+
order(options[:order])
|
44
|
+
else
|
45
|
+
# If no lat/lon given we don't want any results, but we still
|
46
|
+
# need distance and bearing columns so you can add, for example:
|
47
|
+
# .order("distance")
|
48
|
+
select(select_clause(nil, "NULL", "NULL")).where(false_condition)
|
49
|
+
end
|
50
|
+
}
|
51
|
+
|
52
|
+
##
|
53
|
+
# Find all objects within the area of a given bounding box.
|
54
|
+
# Bounds must be an array of locations specifying the southwest
|
55
|
+
# corner followed by the northeast corner of the box
|
56
|
+
# (<tt>[[sw_lat, sw_lon], [ne_lat, ne_lon]]</tt>).
|
57
|
+
#
|
58
|
+
scope :within_bounding_box, lambda{ |bounds|
|
59
|
+
sw_lat, sw_lng, ne_lat, ne_lng = bounds.flatten if bounds
|
60
|
+
if sw_lat && sw_lng && ne_lat && ne_lng
|
61
|
+
where(Geocoder::Sql.within_bounding_box(
|
62
|
+
sw_lat, sw_lng, ne_lat, ne_lng,
|
63
|
+
full_column_name(geocoder_options[:latitude]),
|
64
|
+
full_column_name(geocoder_options[:longitude])
|
65
|
+
))
|
66
|
+
else
|
67
|
+
select(select_clause(nil, "NULL", "NULL")).where(false_condition)
|
68
|
+
end
|
69
|
+
}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Methods which will be class methods of the including class.
|
75
|
+
#
|
76
|
+
module ClassMethods
|
77
|
+
|
78
|
+
def distance_from_sql(location, *args)
|
79
|
+
latitude, longitude = Geocoder::Calculations.extract_coordinates(location)
|
80
|
+
if Geocoder::Calculations.coordinates_present?(latitude, longitude)
|
81
|
+
distance_sql(latitude, longitude, *args)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private # ----------------------------------------------------------------
|
86
|
+
|
87
|
+
##
|
88
|
+
# Get options hash suitable for passing to ActiveRecord.find to get
|
89
|
+
# records within a radius (in kilometers) of the given point.
|
90
|
+
# Options hash may include:
|
91
|
+
#
|
92
|
+
# * +:units+ - <tt>:mi</tt> or <tt>:km</tt>; to be used.
|
93
|
+
# for interpreting radius as well as the +distance+ attribute which
|
94
|
+
# is added to each found nearby object.
|
95
|
+
# Use Geocoder.configure[:units] to configure default units.
|
96
|
+
# * +:bearing+ - <tt>:linear</tt> or <tt>:spherical</tt>.
|
97
|
+
# the method to be used for calculating the bearing (direction)
|
98
|
+
# between the given point and each found nearby point;
|
99
|
+
# set to false for no bearing calculation. Use
|
100
|
+
# Geocoder.configure[:distances] to configure default calculation method.
|
101
|
+
# * +:select+ - string with the SELECT SQL fragment (e.g. “id, name”)
|
102
|
+
# * +:select_distance+ - whether to include the distance alias in the
|
103
|
+
# SELECT SQL fragment (e.g. <formula> AS distance)
|
104
|
+
# * +:select_bearing+ - like +:select_distance+ but for bearing.
|
105
|
+
# * +:order+ - column(s) for ORDER BY SQL clause; default is distance;
|
106
|
+
# set to false or nil to omit the ORDER BY clause
|
107
|
+
# * +:exclude+ - an object to exclude (used by the +nearbys+ method)
|
108
|
+
# * +:distance_column+ - used to set the column name of the calculated distance.
|
109
|
+
# * +:bearing_column+ - used to set the column name of the calculated bearing.
|
110
|
+
# * +:min_radius+ - the value to use as the minimum radius.
|
111
|
+
# ignored if database is sqlite.
|
112
|
+
# default is 0.0
|
113
|
+
#
|
114
|
+
def near_scope_options(latitude, longitude, radius = 20, options = {})
|
115
|
+
if options[:units]
|
116
|
+
options[:units] = options[:units].to_sym
|
117
|
+
end
|
118
|
+
latitude_attribute = options[:latitude] || geocoder_options[:latitude]
|
119
|
+
longitude_attribute = options[:longitude] || geocoder_options[:longitude]
|
120
|
+
options[:units] ||= (geocoder_options[:units] || Geocoder.config.units)
|
121
|
+
select_distance = options.fetch(:select_distance, true)
|
122
|
+
options[:order] = "" if !select_distance && !options.include?(:order)
|
123
|
+
select_bearing = options.fetch(:select_bearing, true)
|
124
|
+
bearing = bearing_sql(latitude, longitude, options)
|
125
|
+
distance = distance_sql(latitude, longitude, options)
|
126
|
+
distance_column = options.fetch(:distance_column, 'distance')
|
127
|
+
bearing_column = options.fetch(:bearing_column, 'bearing')
|
128
|
+
|
129
|
+
b = Geocoder::Calculations.bounding_box([latitude, longitude], radius, options)
|
130
|
+
args = b + [
|
131
|
+
full_column_name(latitude_attribute),
|
132
|
+
full_column_name(longitude_attribute)
|
133
|
+
]
|
134
|
+
bounding_box_conditions = Geocoder::Sql.within_bounding_box(*args)
|
135
|
+
|
136
|
+
if using_sqlite?
|
137
|
+
conditions = bounding_box_conditions
|
138
|
+
else
|
139
|
+
min_radius = options.fetch(:min_radius, 0).to_f
|
140
|
+
conditions = [bounding_box_conditions + " AND (#{distance}) BETWEEN ? AND ?", min_radius, radius]
|
141
|
+
end
|
142
|
+
{
|
143
|
+
:select => select_clause(options[:select],
|
144
|
+
select_distance ? distance : nil,
|
145
|
+
select_bearing ? bearing : nil,
|
146
|
+
distance_column,
|
147
|
+
bearing_column),
|
148
|
+
:conditions => add_exclude_condition(conditions, options[:exclude]),
|
149
|
+
:order => options.include?(:order) ? options[:order] : "#{distance_column} ASC"
|
150
|
+
}
|
151
|
+
end
|
152
|
+
|
153
|
+
##
|
154
|
+
# SQL for calculating distance based on the current database's
|
155
|
+
# capabilities (trig functions?).
|
156
|
+
#
|
157
|
+
def distance_sql(latitude, longitude, options = {})
|
158
|
+
method_prefix = using_sqlite? ? "approx" : "full"
|
159
|
+
Geocoder::Sql.send(
|
160
|
+
method_prefix + "_distance",
|
161
|
+
latitude, longitude,
|
162
|
+
full_column_name(options[:latitude] || geocoder_options[:latitude]),
|
163
|
+
full_column_name(options[:longitude]|| geocoder_options[:longitude]),
|
164
|
+
options
|
165
|
+
)
|
166
|
+
end
|
167
|
+
|
168
|
+
##
|
169
|
+
# SQL for calculating bearing based on the current database's
|
170
|
+
# capabilities (trig functions?).
|
171
|
+
#
|
172
|
+
def bearing_sql(latitude, longitude, options = {})
|
173
|
+
if !options.include?(:bearing)
|
174
|
+
options[:bearing] = Geocoder.config.distances
|
175
|
+
end
|
176
|
+
if options[:bearing]
|
177
|
+
method_prefix = using_sqlite? ? "approx" : "full"
|
178
|
+
Geocoder::Sql.send(
|
179
|
+
method_prefix + "_bearing",
|
180
|
+
latitude, longitude,
|
181
|
+
full_column_name(options[:latitude] || geocoder_options[:latitude]),
|
182
|
+
full_column_name(options[:longitude]|| geocoder_options[:longitude]),
|
183
|
+
options
|
184
|
+
)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
##
|
189
|
+
# Generate the SELECT clause.
|
190
|
+
#
|
191
|
+
def select_clause(columns, distance = nil, bearing = nil, distance_column = 'distance', bearing_column = 'bearing')
|
192
|
+
if columns == :id_only
|
193
|
+
return full_column_name(primary_key)
|
194
|
+
elsif columns == :geo_only
|
195
|
+
clause = ""
|
196
|
+
else
|
197
|
+
clause = (columns || full_column_name("*"))
|
198
|
+
end
|
199
|
+
if distance
|
200
|
+
clause += ", " unless clause.empty?
|
201
|
+
clause += "#{distance} AS #{distance_column}"
|
202
|
+
end
|
203
|
+
if bearing
|
204
|
+
clause += ", " unless clause.empty?
|
205
|
+
clause += "#{bearing} AS #{bearing_column}"
|
206
|
+
end
|
207
|
+
clause
|
208
|
+
end
|
209
|
+
|
210
|
+
##
|
211
|
+
# Adds a condition to exclude a given object by ID.
|
212
|
+
# Expects conditions as an array or string. Returns array.
|
213
|
+
#
|
214
|
+
def add_exclude_condition(conditions, exclude)
|
215
|
+
conditions = [conditions] if conditions.is_a?(String)
|
216
|
+
if exclude
|
217
|
+
conditions[0] << " AND #{full_column_name(primary_key)} != ?"
|
218
|
+
conditions << exclude.id
|
219
|
+
end
|
220
|
+
conditions
|
221
|
+
end
|
222
|
+
|
223
|
+
def using_sqlite?
|
224
|
+
connection.adapter_name.match(/sqlite/i)
|
225
|
+
end
|
226
|
+
|
227
|
+
##
|
228
|
+
# Value which can be passed to where() to produce no results.
|
229
|
+
#
|
230
|
+
def false_condition
|
231
|
+
using_sqlite? ? 0 : "false"
|
232
|
+
end
|
233
|
+
|
234
|
+
##
|
235
|
+
# Prepend table name if column name doesn't already contain one.
|
236
|
+
#
|
237
|
+
def full_column_name(column)
|
238
|
+
column = column.to_s
|
239
|
+
column.include?(".") ? column : [table_name, column].join(".")
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
##
|
244
|
+
# Look up coordinates and assign to +latitude+ and +longitude+ attributes
|
245
|
+
# (or other as specified in +geocoded_by+). Returns coordinates (array).
|
246
|
+
#
|
247
|
+
def geocode
|
248
|
+
do_lookup(false) do |o,rs|
|
249
|
+
if r = rs.first
|
250
|
+
unless r.latitude.nil? or r.longitude.nil?
|
251
|
+
o.__send__ "#{self.class.geocoder_options[:latitude]}=", r.latitude
|
252
|
+
o.__send__ "#{self.class.geocoder_options[:longitude]}=", r.longitude
|
253
|
+
end
|
254
|
+
r.coordinates
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
alias_method :fetch_coordinates, :geocode
|
260
|
+
|
261
|
+
##
|
262
|
+
# Look up address and assign to +address+ attribute (or other as specified
|
263
|
+
# in +reverse_geocoded_by+). Returns address (string).
|
264
|
+
#
|
265
|
+
def reverse_geocode
|
266
|
+
do_lookup(true) do |o,rs|
|
267
|
+
if r = rs.first
|
268
|
+
unless r.address.nil?
|
269
|
+
o.__send__ "#{self.class.geocoder_options[:fetched_address]}=", r.address
|
270
|
+
end
|
271
|
+
r.address
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
alias_method :fetch_address, :reverse_geocode
|
277
|
+
end
|
278
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Geocoder
|
2
|
+
module Store
|
3
|
+
module Base
|
4
|
+
|
5
|
+
##
|
6
|
+
# Is this object geocoded? (Does it have latitude and longitude?)
|
7
|
+
#
|
8
|
+
def geocoded?
|
9
|
+
to_coordinates.compact.size > 0
|
10
|
+
end
|
11
|
+
|
12
|
+
##
|
13
|
+
# Coordinates [lat,lon] of the object.
|
14
|
+
#
|
15
|
+
def to_coordinates
|
16
|
+
[:latitude, :longitude].map{ |i| send self.class.geocoder_options[i] }
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Calculate the distance from the object to an arbitrary point.
|
21
|
+
# See Geocoder::Calculations.distance_between for ways of specifying
|
22
|
+
# the point. Also takes a symbol specifying the units
|
23
|
+
# (:mi or :km; can be specified in Geocoder configuration).
|
24
|
+
#
|
25
|
+
def distance_to(point, units = nil)
|
26
|
+
units ||= self.class.geocoder_options[:units]
|
27
|
+
return nil unless geocoded?
|
28
|
+
Geocoder::Calculations.distance_between(
|
29
|
+
to_coordinates, point, :units => units)
|
30
|
+
end
|
31
|
+
|
32
|
+
alias_method :distance_from, :distance_to
|
33
|
+
|
34
|
+
##
|
35
|
+
# Calculate the bearing from the object to another point.
|
36
|
+
# See Geocoder::Calculations.distance_between for
|
37
|
+
# ways of specifying the point.
|
38
|
+
#
|
39
|
+
def bearing_to(point, options = {})
|
40
|
+
options[:method] ||= self.class.geocoder_options[:method]
|
41
|
+
return nil unless geocoded?
|
42
|
+
Geocoder::Calculations.bearing_between(
|
43
|
+
to_coordinates, point, options)
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Calculate the bearing from another point to the object.
|
48
|
+
# See Geocoder::Calculations.distance_between for
|
49
|
+
# ways of specifying the point.
|
50
|
+
#
|
51
|
+
def bearing_from(point, options = {})
|
52
|
+
options[:method] ||= self.class.geocoder_options[:method]
|
53
|
+
return nil unless geocoded?
|
54
|
+
Geocoder::Calculations.bearing_between(
|
55
|
+
point, to_coordinates, options)
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Get nearby geocoded objects.
|
60
|
+
# Takes the same options hash as the near class method (scope).
|
61
|
+
# Returns nil if the object is not geocoded.
|
62
|
+
#
|
63
|
+
def nearbys(radius = 20, options = {})
|
64
|
+
return nil unless geocoded?
|
65
|
+
options.merge!(:exclude => self) unless send(self.class.primary_key).nil?
|
66
|
+
self.class.near(self, radius, options)
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Look up coordinates and assign to +latitude+ and +longitude+ attributes
|
71
|
+
# (or other as specified in +geocoded_by+). Returns coordinates (array).
|
72
|
+
#
|
73
|
+
def geocode
|
74
|
+
fail
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Look up address and assign to +address+ attribute (or other as specified
|
79
|
+
# in +reverse_geocoded_by+). Returns address (string).
|
80
|
+
#
|
81
|
+
def reverse_geocode
|
82
|
+
fail
|
83
|
+
end
|
84
|
+
|
85
|
+
private # --------------------------------------------------------------
|
86
|
+
|
87
|
+
##
|
88
|
+
# Look up geographic data based on object attributes (configured in
|
89
|
+
# geocoded_by or reverse_geocoded_by) and handle the results with the
|
90
|
+
# block (given to geocoded_by or reverse_geocoded_by). The block is
|
91
|
+
# given two-arguments: the object being geocoded and an array of
|
92
|
+
# Geocoder::Result objects).
|
93
|
+
#
|
94
|
+
def do_lookup(reverse = false)
|
95
|
+
options = self.class.geocoder_options
|
96
|
+
if reverse and options[:reverse_geocode]
|
97
|
+
query = to_coordinates
|
98
|
+
elsif !reverse and options[:geocode]
|
99
|
+
query = send(options[:user_address])
|
100
|
+
else
|
101
|
+
return
|
102
|
+
end
|
103
|
+
|
104
|
+
query_options = [:lookup, :ip_lookup, :language].inject({}) do |hash, key|
|
105
|
+
if options.has_key?(key)
|
106
|
+
val = options[key]
|
107
|
+
hash[key] = val.respond_to?(:call) ? val.call(self) : val
|
108
|
+
end
|
109
|
+
hash
|
110
|
+
end
|
111
|
+
results = Geocoder.search(query, query_options)
|
112
|
+
|
113
|
+
# execute custom block, if specified in configuration
|
114
|
+
block_key = reverse ? :reverse_block : :geocode_block
|
115
|
+
if custom_block = options[block_key]
|
116
|
+
custom_block.call(self, results)
|
117
|
+
|
118
|
+
# else execute block passed directly to this method,
|
119
|
+
# which generally performs the "auto-assigns"
|
120
|
+
elsif block_given?
|
121
|
+
yield(self, results)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Geocoder::Store
|
2
|
+
module MongoBase
|
3
|
+
|
4
|
+
def self.included_by_model(base)
|
5
|
+
base.class_eval do
|
6
|
+
|
7
|
+
scope :geocoded, lambda {
|
8
|
+
where(geocoder_options[:coordinates].ne => nil)
|
9
|
+
}
|
10
|
+
|
11
|
+
scope :not_geocoded, lambda {
|
12
|
+
where(geocoder_options[:coordinates] => nil)
|
13
|
+
}
|
14
|
+
|
15
|
+
scope :near, lambda{ |location, *args|
|
16
|
+
coords = Geocoder::Calculations.extract_coordinates(location)
|
17
|
+
|
18
|
+
# no results if no lat/lon given
|
19
|
+
return where(:id => false) unless coords.is_a?(Array)
|
20
|
+
|
21
|
+
radius = args.size > 0 ? args.shift : 20
|
22
|
+
options = args.size > 0 ? args.shift : {}
|
23
|
+
options[:units] ||= geocoder_options[:units]
|
24
|
+
|
25
|
+
# Use BSON::OrderedHash if Ruby's hashes are unordered.
|
26
|
+
# Conditions must be in order required by indexes (see mongo gem).
|
27
|
+
version = RUBY_VERSION.split('.').map { |i| i.to_i }
|
28
|
+
empty = version[0] < 2 && version[1] < 9 ? BSON::OrderedHash.new : {}
|
29
|
+
|
30
|
+
conds = empty.clone
|
31
|
+
field = geocoder_options[:coordinates]
|
32
|
+
conds[field] = empty.clone
|
33
|
+
conds[field]["$nearSphere"] = coords.reverse
|
34
|
+
|
35
|
+
if radius
|
36
|
+
conds[field]["$maxDistance"] = \
|
37
|
+
Geocoder::Calculations.distance_to_radians(radius, options[:units])
|
38
|
+
end
|
39
|
+
|
40
|
+
if obj = options[:exclude]
|
41
|
+
conds[:_id.ne] = obj.id
|
42
|
+
end
|
43
|
+
where(conds)
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Coordinates [lat,lon] of the object.
|
50
|
+
# This method always returns coordinates in lat,lon order,
|
51
|
+
# even though internally they are stored in the opposite order.
|
52
|
+
#
|
53
|
+
def to_coordinates
|
54
|
+
coords = send(self.class.geocoder_options[:coordinates])
|
55
|
+
coords.is_a?(Array) ? coords.reverse : []
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Look up coordinates and assign to +latitude+ and +longitude+ attributes
|
60
|
+
# (or other as specified in +geocoded_by+). Returns coordinates (array).
|
61
|
+
#
|
62
|
+
def geocode
|
63
|
+
do_lookup(false) do |o,rs|
|
64
|
+
if r = rs.first
|
65
|
+
unless r.coordinates.nil?
|
66
|
+
o.__send__ "#{self.class.geocoder_options[:coordinates]}=", r.coordinates.reverse
|
67
|
+
end
|
68
|
+
r.coordinates
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Look up address and assign to +address+ attribute (or other as specified
|
75
|
+
# in +reverse_geocoded_by+). Returns address (string).
|
76
|
+
#
|
77
|
+
def reverse_geocode
|
78
|
+
do_lookup(true) do |o,rs|
|
79
|
+
if r = rs.first
|
80
|
+
unless r.address.nil?
|
81
|
+
o.__send__ "#{self.class.geocoder_options[:fetched_address]}=", r.address
|
82
|
+
end
|
83
|
+
r.address
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
data/lib/geocoder.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require "geocoder/configuration"
|
2
|
+
require "geocoder/query"
|
3
|
+
require "geocoder/calculations"
|
4
|
+
require "geocoder/exceptions"
|
5
|
+
require "geocoder/cache"
|
6
|
+
require "geocoder/request"
|
7
|
+
require "geocoder/lookup"
|
8
|
+
require "geocoder/ip_address"
|
9
|
+
require "geocoder/models/active_record" if defined?(::ActiveRecord)
|
10
|
+
require "geocoder/models/mongoid" if defined?(::Mongoid)
|
11
|
+
require "geocoder/models/mongo_mapper" if defined?(::MongoMapper)
|
12
|
+
|
13
|
+
module Geocoder
|
14
|
+
|
15
|
+
##
|
16
|
+
# Search for information about an address or a set of coordinates.
|
17
|
+
#
|
18
|
+
def self.search(query, options = {})
|
19
|
+
query = Geocoder::Query.new(query, options) unless query.is_a?(Geocoder::Query)
|
20
|
+
query.blank? ? [] : query.execute
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Look up the coordinates of the given street or IP address.
|
25
|
+
#
|
26
|
+
def self.coordinates(address, options = {})
|
27
|
+
if (results = search(address, options)).size > 0
|
28
|
+
results.first.coordinates
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Look up the address of the given coordinates ([lat,lon])
|
34
|
+
# or IP address (string).
|
35
|
+
#
|
36
|
+
def self.address(query, options = {})
|
37
|
+
if (results = search(query, options)).size > 0
|
38
|
+
results.first.address
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# load Railtie if Rails exists
|
44
|
+
if defined?(Rails)
|
45
|
+
require "geocoder/railtie"
|
46
|
+
Geocoder::Railtie.insert
|
47
|
+
end
|