geocoder 1.1.3 → 1.1.6
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/.travis.yml +4 -0
- data/{CHANGELOG.rdoc → CHANGELOG.md} +109 -35
- data/README.md +645 -0
- data/examples/autoexpire_cache.rb +28 -0
- data/lib/generators/geocoder/config/templates/initializer.rb +18 -22
- data/lib/geocoder/cache.rb +2 -1
- data/lib/geocoder/calculations.rb +13 -13
- data/lib/geocoder/cli.rb +17 -19
- data/lib/geocoder/configuration.rb +68 -42
- data/lib/geocoder/configuration_hash.rb +11 -0
- data/lib/geocoder/exceptions.rb +3 -0
- data/lib/geocoder/lookup.rb +81 -0
- data/lib/geocoder/lookups/base.rb +106 -56
- data/lib/geocoder/lookups/bing.rb +25 -11
- data/lib/geocoder/lookups/freegeoip.rb +11 -7
- data/lib/geocoder/lookups/geocoder_ca.rb +17 -9
- data/lib/geocoder/lookups/google.rb +29 -8
- data/lib/geocoder/lookups/google_premier.rb +20 -11
- data/lib/geocoder/lookups/mapquest.rb +29 -19
- data/lib/geocoder/lookups/maxmind.rb +72 -0
- data/lib/geocoder/lookups/nominatim.rb +21 -13
- data/lib/geocoder/lookups/test.rb +12 -6
- data/lib/geocoder/lookups/yahoo.rb +63 -13
- data/lib/geocoder/lookups/yandex.rb +26 -11
- data/lib/geocoder/models/mongoid.rb +1 -2
- data/lib/geocoder/query.rb +103 -0
- data/lib/geocoder/request.rb +7 -1
- data/lib/geocoder/results/base.rb +8 -2
- data/lib/geocoder/results/google.rb +6 -0
- data/lib/geocoder/results/mapquest.rb +19 -34
- data/lib/geocoder/results/maxmind.rb +136 -0
- data/lib/geocoder/results/nominatim.rb +14 -3
- data/lib/geocoder/results/yahoo.rb +9 -2
- data/lib/geocoder/sql.rb +106 -0
- data/lib/geocoder/stores/active_record.rb +75 -142
- data/lib/geocoder/stores/base.rb +3 -2
- data/lib/geocoder/stores/mongo_base.rb +2 -2
- data/lib/geocoder/version.rb +1 -1
- data/lib/geocoder.rb +11 -89
- data/lib/hash_recursive_merge.rb +74 -0
- data/lib/oauth_util.rb +112 -0
- data/test/active_record_test.rb +15 -0
- data/test/cache_test.rb +19 -0
- data/test/calculations_test.rb +11 -4
- data/test/configuration_test.rb +26 -51
- data/test/error_handling_test.rb +15 -11
- data/test/fixtures/bing_invalid_key +1 -0
- data/test/fixtures/mapquest_madison_square_garden +52 -0
- data/test/fixtures/mapquest_no_results +7 -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/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/https_test.rb +3 -3
- data/test/integration/smoke_test.rb +8 -6
- data/test/lookup_test.rb +92 -13
- data/test/near_test.rb +11 -0
- data/test/oauth_util_test.rb +30 -0
- data/test/proxy_test.rb +2 -2
- data/test/query_test.rb +42 -0
- data/test/result_test.rb +4 -3
- data/test/services_test.rb +164 -26
- data/test/test_helper.rb +93 -112
- data/test/test_mode_test.rb +3 -4
- metadata +52 -35
- data/README.rdoc +0 -552
- data/test/fixtures/mapquest_madison_square_garden.json +0 -27
- data/test/fixtures/mapquest_no_results.json +0 -1
- data/test/fixtures/yahoo_garbage.json +0 -50
- data/test/fixtures/yahoo_madison_square_garden.json +0 -46
- data/test/fixtures/yahoo_no_results.json +0 -10
- data/test/input_handling_test.rb +0 -43
- /data/test/fixtures/{bing_madison_square_garden.json → bing_madison_square_garden} +0 -0
- /data/test/fixtures/{bing_no_results.json → bing_no_results} +0 -0
- /data/test/fixtures/{bing_reverse.json → bing_reverse} +0 -0
- /data/test/fixtures/{freegeoip_74_200_247_59.json → freegeoip_74_200_247_59} +0 -0
- /data/test/fixtures/{freegeoip_no_results.json → freegeoip_no_results} +0 -0
- /data/test/fixtures/{geocoder_ca_madison_square_garden.json → geocoder_ca_madison_square_garden} +0 -0
- /data/test/fixtures/{geocoder_ca_no_results.json → geocoder_ca_no_results} +0 -0
- /data/test/fixtures/{geocoder_ca_reverse.json → geocoder_ca_reverse} +0 -0
- /data/test/fixtures/{google_garbage.json → google_garbage} +0 -0
- /data/test/fixtures/{google_madison_square_garden.json → google_madison_square_garden} +0 -0
- /data/test/fixtures/{google_no_city_data.json → google_no_city_data} +0 -0
- /data/test/fixtures/{google_no_locality.json → google_no_locality} +0 -0
- /data/test/fixtures/{google_no_results.json → google_no_results} +0 -0
- /data/test/fixtures/{nominatim_madison_square_garden.json → nominatim_madison_square_garden} +0 -0
- /data/test/fixtures/{nominatim_no_results.json → nominatim_no_results} +0 -0
- /data/test/fixtures/{yandex_invalid_key.json → yandex_invalid_key} +0 -0
- /data/test/fixtures/{yandex_kremlin.json → yandex_kremlin} +0 -0
- /data/test/fixtures/{yandex_no_results.json → yandex_no_results} +0 -0
data/lib/geocoder/sql.rb
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
case options[:bearing] || Geocoder.config.distances
|
|
67
|
+
when :linear
|
|
68
|
+
"CAST(" +
|
|
69
|
+
"DEGREES(ATAN2( " +
|
|
70
|
+
"RADIANS(#{lon_attr} - #{longitude.to_f}), " +
|
|
71
|
+
"RADIANS(#{lat_attr} - #{latitude.to_f})" +
|
|
72
|
+
")) + 360 " +
|
|
73
|
+
"AS decimal) % 360"
|
|
74
|
+
when :spherical
|
|
75
|
+
"CAST(" +
|
|
76
|
+
"DEGREES(ATAN2( " +
|
|
77
|
+
"SIN(RADIANS(#{lon_attr} - #{longitude.to_f})) * " +
|
|
78
|
+
"COS(RADIANS(#{lat_attr})), (" +
|
|
79
|
+
"COS(RADIANS(#{latitude.to_f})) * SIN(RADIANS(#{lat_attr}))" +
|
|
80
|
+
") - (" +
|
|
81
|
+
"SIN(RADIANS(#{latitude.to_f})) * COS(RADIANS(#{lat_attr})) * " +
|
|
82
|
+
"COS(RADIANS(#{lon_attr} - #{longitude.to_f}))" +
|
|
83
|
+
")" +
|
|
84
|
+
")) + 360 " +
|
|
85
|
+
"AS decimal) % 360"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
##
|
|
90
|
+
# Totally lame bearing calculation. Basically useless except that it
|
|
91
|
+
# returns *something* in databases without trig functions.
|
|
92
|
+
#
|
|
93
|
+
def approx_bearing(latitude, longitude, lat_attr, lon_attr, options = {})
|
|
94
|
+
"CASE " +
|
|
95
|
+
"WHEN (#{lat_attr} >= #{latitude.to_f} AND " +
|
|
96
|
+
"#{lon_attr} >= #{longitude.to_f}) THEN 45.0 " +
|
|
97
|
+
"WHEN (#{lat_attr} < #{latitude.to_f} AND " +
|
|
98
|
+
"#{lon_attr} >= #{longitude.to_f}) THEN 135.0 " +
|
|
99
|
+
"WHEN (#{lat_attr} < #{latitude.to_f} AND " +
|
|
100
|
+
"#{lon_attr} < #{longitude.to_f}) THEN 225.0 " +
|
|
101
|
+
"WHEN (#{lat_attr} >= #{latitude.to_f} AND " +
|
|
102
|
+
"#{lon_attr} < #{longitude.to_f}) THEN 315.0 " +
|
|
103
|
+
"END"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require 'geocoder/sql'
|
|
1
2
|
require 'geocoder/stores/base'
|
|
2
3
|
|
|
3
4
|
##
|
|
@@ -36,11 +37,13 @@ module Geocoder::Store
|
|
|
36
37
|
if Geocoder::Calculations.coordinates_present?(latitude, longitude)
|
|
37
38
|
near_scope_options(latitude, longitude, *args)
|
|
38
39
|
else
|
|
39
|
-
|
|
40
|
+
# If no lat/lon given we don't want any results, but we still
|
|
41
|
+
# need distance and bearing columns so you can add, for example:
|
|
42
|
+
# .order("distance")
|
|
43
|
+
select(select_clause(nil, "NULL", "NULL")).where(false_condition)
|
|
40
44
|
end
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
|
|
44
47
|
##
|
|
45
48
|
# Find all objects within the area of a given bounding box.
|
|
46
49
|
# Bounds must be an array of locations specifying the southwest
|
|
@@ -49,14 +52,15 @@ module Geocoder::Store
|
|
|
49
52
|
#
|
|
50
53
|
scope :within_bounding_box, lambda{ |bounds|
|
|
51
54
|
sw_lat, sw_lng, ne_lat, ne_lng = bounds.flatten if bounds
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
if sw_lat && sw_lng && ne_lat && ne_lng
|
|
56
|
+
{:conditions => Geocoder::Sql.within_bounding_box(
|
|
57
|
+
sw_lat, sw_lng, ne_lat, ne_lng,
|
|
58
|
+
full_column_name(geocoder_options[:latitude]),
|
|
59
|
+
full_column_name(geocoder_options[:longitude])
|
|
60
|
+
)}
|
|
56
61
|
else
|
|
57
|
-
"
|
|
62
|
+
select(select_clause(nil, "NULL", "NULL")).where(false_condition)
|
|
58
63
|
end
|
|
59
|
-
{ :conditions => spans }
|
|
60
64
|
}
|
|
61
65
|
end
|
|
62
66
|
end
|
|
@@ -69,7 +73,7 @@ module Geocoder::Store
|
|
|
69
73
|
def distance_from_sql(location, *args)
|
|
70
74
|
latitude, longitude = Geocoder::Calculations.extract_coordinates(location)
|
|
71
75
|
if Geocoder::Calculations.coordinates_present?(latitude, longitude)
|
|
72
|
-
|
|
76
|
+
distance_sql(latitude, longitude, *args)
|
|
73
77
|
end
|
|
74
78
|
end
|
|
75
79
|
|
|
@@ -83,170 +87,99 @@ module Geocoder::Store
|
|
|
83
87
|
# * +:units+ - <tt>:mi</tt> or <tt>:km</tt>; to be used.
|
|
84
88
|
# for interpreting radius as well as the +distance+ attribute which
|
|
85
89
|
# is added to each found nearby object.
|
|
86
|
-
#
|
|
90
|
+
# Use Geocoder.configure[:units] to configure default units.
|
|
87
91
|
# * +:bearing+ - <tt>:linear</tt> or <tt>:spherical</tt>.
|
|
88
92
|
# the method to be used for calculating the bearing (direction)
|
|
89
93
|
# between the given point and each found nearby point;
|
|
90
|
-
# set to false for no bearing calculation.
|
|
91
|
-
#
|
|
94
|
+
# set to false for no bearing calculation. Use
|
|
95
|
+
# Geocoder.configure[:distances] to configure default calculation method.
|
|
92
96
|
# * +:select+ - string with the SELECT SQL fragment (e.g. “id, name”)
|
|
93
|
-
# * +:order+ - column(s) for ORDER BY SQL clause; default is distance
|
|
97
|
+
# * +:order+ - column(s) for ORDER BY SQL clause; default is distance;
|
|
98
|
+
# set to false or nil to omit the ORDER BY clause
|
|
94
99
|
# * +:exclude+ - an object to exclude (used by the +nearbys+ method)
|
|
95
100
|
#
|
|
96
101
|
def near_scope_options(latitude, longitude, radius = 20, options = {})
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
options[:units] ||= (geocoder_options[:units] || Geocoder.config.units)
|
|
103
|
+
bearing = bearing_sql(latitude, longitude, options)
|
|
104
|
+
distance = distance_sql(latitude, longitude, options)
|
|
105
|
+
|
|
106
|
+
b = Geocoder::Calculations.bounding_box([latitude, longitude], radius, options)
|
|
107
|
+
args = b + [
|
|
108
|
+
full_column_name(geocoder_options[:latitude]),
|
|
109
|
+
full_column_name(geocoder_options[:longitude])
|
|
110
|
+
]
|
|
111
|
+
bounding_box_conditions = Geocoder::Sql.within_bounding_box(*args)
|
|
103
112
|
|
|
104
|
-
def distance_from_sql_options(latitude, longitude, options = {})
|
|
105
113
|
if using_sqlite?
|
|
106
|
-
|
|
114
|
+
conditions = bounding_box_conditions
|
|
107
115
|
else
|
|
108
|
-
|
|
116
|
+
conditions = [bounding_box_conditions + " AND #{distance} <= ?", radius]
|
|
109
117
|
end
|
|
118
|
+
{
|
|
119
|
+
:select => select_clause(options[:select], distance, bearing),
|
|
120
|
+
:conditions => add_exclude_condition(conditions, options[:exclude]),
|
|
121
|
+
:order => options.include?(:order) ? options[:order] : "distance ASC"
|
|
122
|
+
}
|
|
110
123
|
end
|
|
111
124
|
|
|
112
125
|
##
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
# ATAN2(), DEGREES(), and RADIANS().
|
|
116
|
-
#
|
|
117
|
-
# Bearing calculation based on:
|
|
118
|
-
# http://www.beginningspatial.com/calculating_bearing_one_point_another
|
|
126
|
+
# SQL for calculating distance based on the current database's
|
|
127
|
+
# capabilities (trig functions?).
|
|
119
128
|
#
|
|
120
|
-
def
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
"CAST(" +
|
|
129
|
-
"DEGREES(ATAN2( " +
|
|
130
|
-
"RADIANS(#{full_column_name(lon_attr)} - #{longitude}), " +
|
|
131
|
-
"RADIANS(#{full_column_name(lat_attr)} - #{latitude})" +
|
|
132
|
-
")) + 360 " +
|
|
133
|
-
"AS decimal) % 360"
|
|
134
|
-
when :spherical
|
|
135
|
-
"CAST(" +
|
|
136
|
-
"DEGREES(ATAN2( " +
|
|
137
|
-
"SIN(RADIANS(#{full_column_name(lon_attr)} - #{longitude})) * " +
|
|
138
|
-
"COS(RADIANS(#{full_column_name(lat_attr)})), (" +
|
|
139
|
-
"COS(RADIANS(#{latitude})) * SIN(RADIANS(#{full_column_name(lat_attr)}))" +
|
|
140
|
-
") - (" +
|
|
141
|
-
"SIN(RADIANS(#{latitude})) * COS(RADIANS(#{full_column_name(lat_attr)})) * " +
|
|
142
|
-
"COS(RADIANS(#{full_column_name(lon_attr)} - #{longitude}))" +
|
|
143
|
-
")" +
|
|
144
|
-
")) + 360 " +
|
|
145
|
-
"AS decimal) % 360"
|
|
146
|
-
end
|
|
147
|
-
options[:units] ||= (geocoder_options[:units] || Geocoder::Configuration.units)
|
|
148
|
-
distance = full_distance_from_sql(latitude, longitude, options)
|
|
149
|
-
conditions = ["#{distance} <= ?", radius]
|
|
150
|
-
default_near_scope_options(latitude, longitude, radius, options).merge(
|
|
151
|
-
:select => "#{options[:select] || full_column_name("*")}, " +
|
|
152
|
-
"#{distance} AS distance" +
|
|
153
|
-
(bearing ? ", #{bearing} AS bearing" : ""),
|
|
154
|
-
:conditions => add_exclude_condition(conditions, options[:exclude])
|
|
129
|
+
def distance_sql(latitude, longitude, options = {})
|
|
130
|
+
method_prefix = using_sqlite? ? "approx" : "full"
|
|
131
|
+
Geocoder::Sql.send(
|
|
132
|
+
method_prefix + "_distance",
|
|
133
|
+
latitude, longitude,
|
|
134
|
+
full_column_name(geocoder_options[:latitude]),
|
|
135
|
+
full_column_name(geocoder_options[:longitude]),
|
|
136
|
+
options
|
|
155
137
|
)
|
|
156
138
|
end
|
|
157
139
|
|
|
158
|
-
|
|
159
|
-
# Distance calculations based on the excellent tutorial at:
|
|
160
|
-
# http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
|
|
161
|
-
|
|
162
|
-
def full_distance_from_sql(latitude, longitude, options)
|
|
163
|
-
lat_attr = geocoder_options[:latitude]
|
|
164
|
-
lon_attr = geocoder_options[:longitude]
|
|
165
|
-
|
|
166
|
-
earth = Geocoder::Calculations.earth_radius(options[:units] || :mi)
|
|
167
|
-
|
|
168
|
-
"#{earth} * 2 * ASIN(SQRT(" +
|
|
169
|
-
"POWER(SIN((#{latitude} - #{full_column_name(lat_attr)}) * PI() / 180 / 2), 2) + " +
|
|
170
|
-
"COS(#{latitude} * PI() / 180) * COS(#{full_column_name(lat_attr)} * PI() / 180) * " +
|
|
171
|
-
"POWER(SIN((#{longitude} - #{full_column_name(lon_attr)}) * PI() / 180 / 2), 2) ))"
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def approx_distance_from_sql(latitude, longitude, options)
|
|
175
|
-
lat_attr = geocoder_options[:latitude]
|
|
176
|
-
lon_attr = geocoder_options[:longitude]
|
|
177
|
-
|
|
178
|
-
dx = Geocoder::Calculations.longitude_degree_distance(30, options[:units] || :mi)
|
|
179
|
-
dy = Geocoder::Calculations.latitude_degree_distance(options[:units] || :mi)
|
|
180
|
-
|
|
181
|
-
# sin of 45 degrees = average x or y component of vector
|
|
182
|
-
factor = Math.sin(Math::PI / 4)
|
|
183
|
-
|
|
184
|
-
"(#{dy} * ABS(#{full_column_name(lat_attr)} - #{latitude}) * #{factor}) + " +
|
|
185
|
-
"(#{dx} * ABS(#{full_column_name(lon_attr)} - #{longitude}) * #{factor})"
|
|
186
|
-
end
|
|
187
|
-
|
|
188
140
|
##
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
# rather than a circle, so results are very approximate (will include
|
|
192
|
-
# objects outside the given radius).
|
|
193
|
-
#
|
|
194
|
-
# Distance and bearing calculations are *extremely inaccurate*. They
|
|
195
|
-
# only exist for interface consistency--not intended for production!
|
|
141
|
+
# SQL for calculating bearing based on the current database's
|
|
142
|
+
# capabilities (trig functions?).
|
|
196
143
|
#
|
|
197
|
-
def
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
unless options.include?(:bearing)
|
|
201
|
-
options[:bearing] = (options[:method] || \
|
|
202
|
-
geocoder_options[:method] || \
|
|
203
|
-
Geocoder::Configuration.distances)
|
|
144
|
+
def bearing_sql(latitude, longitude, options = {})
|
|
145
|
+
if !options.include?(:bearing)
|
|
146
|
+
options[:bearing] = Geocoder.config.distances
|
|
204
147
|
end
|
|
205
148
|
if options[:bearing]
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
149
|
+
method_prefix = using_sqlite? ? "approx" : "full"
|
|
150
|
+
Geocoder::Sql.send(
|
|
151
|
+
method_prefix + "_bearing",
|
|
152
|
+
latitude, longitude,
|
|
153
|
+
full_column_name(geocoder_options[:latitude]),
|
|
154
|
+
full_column_name(geocoder_options[:longitude]),
|
|
155
|
+
options
|
|
156
|
+
)
|
|
214
157
|
end
|
|
215
|
-
|
|
216
|
-
options[:units] ||= (geocoder_options[:units] || Geocoder::Configuration.units)
|
|
217
|
-
distance = approx_distance_from_sql(latitude, longitude, options)
|
|
218
|
-
|
|
219
|
-
b = Geocoder::Calculations.bounding_box([latitude, longitude], radius, options)
|
|
220
|
-
conditions = [
|
|
221
|
-
"#{full_column_name(lat_attr)} BETWEEN ? AND ? AND #{full_column_name(lon_attr)} BETWEEN ? AND ?"] +
|
|
222
|
-
[b[0], b[2], b[1], b[3]
|
|
223
|
-
]
|
|
224
|
-
default_near_scope_options(latitude, longitude, radius, options).merge(
|
|
225
|
-
:select => "#{options[:select] || full_column_name("*")}, " +
|
|
226
|
-
"#{distance} AS distance" +
|
|
227
|
-
(bearing ? ", #{bearing} AS bearing" : ""),
|
|
228
|
-
:conditions => add_exclude_condition(conditions, options[:exclude])
|
|
229
|
-
)
|
|
230
158
|
end
|
|
231
159
|
|
|
232
160
|
##
|
|
233
|
-
#
|
|
161
|
+
# Generate the SELECT clause.
|
|
234
162
|
#
|
|
235
|
-
def
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
163
|
+
def select_clause(columns, distance = nil, bearing = nil)
|
|
164
|
+
if columns == :id_only
|
|
165
|
+
return full_column_name(primary_key)
|
|
166
|
+
elsif columns == :geo_only
|
|
167
|
+
clause = ""
|
|
168
|
+
else
|
|
169
|
+
clause = (columns || full_column_name("*")) + ", "
|
|
170
|
+
end
|
|
171
|
+
clause + "#{distance} AS distance" +
|
|
172
|
+
(bearing ? ", #{bearing} AS bearing" : "")
|
|
241
173
|
end
|
|
242
174
|
|
|
243
175
|
##
|
|
244
176
|
# Adds a condition to exclude a given object by ID.
|
|
245
|
-
#
|
|
177
|
+
# Expects conditions as an array or string. Returns array.
|
|
246
178
|
#
|
|
247
179
|
def add_exclude_condition(conditions, exclude)
|
|
180
|
+
conditions = [conditions] if conditions.is_a?(String)
|
|
248
181
|
if exclude
|
|
249
|
-
conditions[0] << " AND #{full_column_name(
|
|
182
|
+
conditions[0] << " AND #{full_column_name(primary_key)} != ?"
|
|
250
183
|
conditions << exclude.id
|
|
251
184
|
end
|
|
252
185
|
conditions
|
|
@@ -280,8 +213,8 @@ module Geocoder::Store
|
|
|
280
213
|
do_lookup(false) do |o,rs|
|
|
281
214
|
if r = rs.first
|
|
282
215
|
unless r.latitude.nil? or r.longitude.nil?
|
|
283
|
-
o.
|
|
284
|
-
o.
|
|
216
|
+
o.__send__ "#{self.class.geocoder_options[:latitude]}=", r.latitude
|
|
217
|
+
o.__send__ "#{self.class.geocoder_options[:longitude]}=", r.longitude
|
|
285
218
|
end
|
|
286
219
|
r.coordinates
|
|
287
220
|
end
|
|
@@ -298,7 +231,7 @@ module Geocoder::Store
|
|
|
298
231
|
do_lookup(true) do |o,rs|
|
|
299
232
|
if r = rs.first
|
|
300
233
|
unless r.address.nil?
|
|
301
|
-
o.
|
|
234
|
+
o.__send__ "#{self.class.geocoder_options[:fetched_address]}=", r.address
|
|
302
235
|
end
|
|
303
236
|
r.address
|
|
304
237
|
end
|
data/lib/geocoder/stores/base.rb
CHANGED
|
@@ -58,10 +58,11 @@ module Geocoder
|
|
|
58
58
|
##
|
|
59
59
|
# Get nearby geocoded objects.
|
|
60
60
|
# Takes the same options hash as the near class method (scope).
|
|
61
|
+
# Returns nil if the object is not geocoded.
|
|
61
62
|
#
|
|
62
63
|
def nearbys(radius = 20, options = {})
|
|
63
|
-
return
|
|
64
|
-
options.merge!(:exclude => self)
|
|
64
|
+
return nil unless geocoded?
|
|
65
|
+
options.merge!(:exclude => self) unless send(self.class.primary_key).nil?
|
|
65
66
|
self.class.near(self, radius, options)
|
|
66
67
|
end
|
|
67
68
|
|
|
@@ -59,7 +59,7 @@ module Geocoder::Store
|
|
|
59
59
|
do_lookup(false) do |o,rs|
|
|
60
60
|
if r = rs.first
|
|
61
61
|
unless r.coordinates.nil?
|
|
62
|
-
o.
|
|
62
|
+
o.__send__ "#{self.class.geocoder_options[:coordinates]}=", r.coordinates.reverse
|
|
63
63
|
end
|
|
64
64
|
r.coordinates
|
|
65
65
|
end
|
|
@@ -74,7 +74,7 @@ module Geocoder::Store
|
|
|
74
74
|
do_lookup(true) do |o,rs|
|
|
75
75
|
if r = rs.first
|
|
76
76
|
unless r.address.nil?
|
|
77
|
-
o.
|
|
77
|
+
o.__send__ "#{self.class.geocoder_options[:fetched_address]}=", r.address
|
|
78
78
|
end
|
|
79
79
|
r.address
|
|
80
80
|
end
|
data/lib/geocoder/version.rb
CHANGED
data/lib/geocoder.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
require "geocoder/configuration"
|
|
2
|
+
require "geocoder/query"
|
|
2
3
|
require "geocoder/calculations"
|
|
3
4
|
require "geocoder/exceptions"
|
|
4
5
|
require "geocoder/cache"
|
|
5
6
|
require "geocoder/request"
|
|
7
|
+
require "geocoder/lookup"
|
|
6
8
|
require "geocoder/models/active_record" if defined?(::ActiveRecord)
|
|
7
9
|
require "geocoder/models/mongoid" if defined?(::Mongoid)
|
|
8
10
|
require "geocoder/models/mongo_mapper" if defined?(::MongoMapper)
|
|
@@ -13,15 +15,16 @@ module Geocoder
|
|
|
13
15
|
##
|
|
14
16
|
# Search for information about an address or a set of coordinates.
|
|
15
17
|
#
|
|
16
|
-
def search(query)
|
|
17
|
-
|
|
18
|
+
def search(query, options = {})
|
|
19
|
+
query = Geocoder::Query.new(query, options) unless query.is_a?(Geocoder::Query)
|
|
20
|
+
query.blank? ? [] : query.execute
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
##
|
|
21
24
|
# Look up the coordinates of the given street or IP address.
|
|
22
25
|
#
|
|
23
|
-
def coordinates(address)
|
|
24
|
-
if (results = search(address)).size > 0
|
|
26
|
+
def coordinates(address, options = {})
|
|
27
|
+
if (results = search(address, options)).size > 0
|
|
25
28
|
results.first.coordinates
|
|
26
29
|
end
|
|
27
30
|
end
|
|
@@ -30,8 +33,8 @@ module Geocoder
|
|
|
30
33
|
# Look up the address of the given coordinates ([lat,lon])
|
|
31
34
|
# or IP address (string).
|
|
32
35
|
#
|
|
33
|
-
def address(query)
|
|
34
|
-
if (results = search(query)).size > 0
|
|
36
|
+
def address(query, options = {})
|
|
37
|
+
if (results = search(query, options)).size > 0
|
|
35
38
|
results.first.address
|
|
36
39
|
end
|
|
37
40
|
end
|
|
@@ -40,89 +43,8 @@ module Geocoder
|
|
|
40
43
|
# The working Cache object, or +nil+ if none configured.
|
|
41
44
|
#
|
|
42
45
|
def cache
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
end
|
|
46
|
-
@cache
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
##
|
|
50
|
-
# Array of valid Lookup names.
|
|
51
|
-
#
|
|
52
|
-
def valid_lookups
|
|
53
|
-
street_lookups + ip_lookups
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
##
|
|
57
|
-
# All street address lookups, default first.
|
|
58
|
-
#
|
|
59
|
-
def street_lookups
|
|
60
|
-
[:google, :google_premier, :yahoo, :bing, :geocoder_ca, :yandex, :nominatim, :mapquest, :test]
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
##
|
|
64
|
-
# All IP address lookups, default first.
|
|
65
|
-
#
|
|
66
|
-
def ip_lookups
|
|
67
|
-
[:freegeoip]
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
private # -----------------------------------------------------------------
|
|
72
|
-
|
|
73
|
-
##
|
|
74
|
-
# Get a Lookup object (which communicates with the remote geocoding API).
|
|
75
|
-
# Takes a search query and returns an IP or street address Lookup
|
|
76
|
-
# depending on the query contents.
|
|
77
|
-
#
|
|
78
|
-
def lookup(query)
|
|
79
|
-
if ip_address?(query)
|
|
80
|
-
get_lookup(ip_lookups.first)
|
|
81
|
-
else
|
|
82
|
-
get_lookup(Configuration.lookup || street_lookups.first)
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
##
|
|
87
|
-
# Retrieve a Lookup object from the store.
|
|
88
|
-
#
|
|
89
|
-
def get_lookup(name)
|
|
90
|
-
@lookups = {} unless defined?(@lookups)
|
|
91
|
-
@lookups[name] = spawn_lookup(name) unless @lookups.include?(name)
|
|
92
|
-
@lookups[name]
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
##
|
|
96
|
-
# Spawn a Lookup of the given name.
|
|
97
|
-
#
|
|
98
|
-
def spawn_lookup(name)
|
|
99
|
-
if valid_lookups.include?(name)
|
|
100
|
-
name = name.to_s
|
|
101
|
-
require "geocoder/lookups/#{name}"
|
|
102
|
-
klass = name.split("_").map{ |i| i[0...1].upcase + i[1..-1] }.join
|
|
103
|
-
Geocoder::Lookup.const_get(klass).new
|
|
104
|
-
else
|
|
105
|
-
valids = valid_lookups.map(&:inspect).join(", ")
|
|
106
|
-
raise ConfigurationError, "Please specify a valid lookup for Geocoder " +
|
|
107
|
-
"(#{name.inspect} is not one of: #{valids})."
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
##
|
|
112
|
-
# Does the given value look like an IP address?
|
|
113
|
-
#
|
|
114
|
-
# Does not check for actual validity, just the appearance of four
|
|
115
|
-
# dot-delimited numbers.
|
|
116
|
-
#
|
|
117
|
-
def ip_address?(value)
|
|
118
|
-
!!value.to_s.match(/^(::ffff:)?(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
##
|
|
122
|
-
# Is the given search query blank? (ie, should we not bother searching?)
|
|
123
|
-
#
|
|
124
|
-
def blank_query?(value)
|
|
125
|
-
!!value.to_s.match(/^\s*$/)
|
|
46
|
+
warn "WARNING: Calling Geocoder.cache is DEPRECATED. The #cache method now belongs to the Geocoder::Lookup object."
|
|
47
|
+
Geocoder::Lookup.get(Geocoder.config.lookup).send(:configuration).cache
|
|
126
48
|
end
|
|
127
49
|
end
|
|
128
50
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#
|
|
2
|
+
# = Hash Recursive Merge
|
|
3
|
+
#
|
|
4
|
+
# Merges a Ruby Hash recursively, Also known as deep merge.
|
|
5
|
+
# Recursive version of Hash#merge and Hash#merge!.
|
|
6
|
+
#
|
|
7
|
+
# Category:: Ruby
|
|
8
|
+
# Package:: Hash
|
|
9
|
+
# Author:: Simone Carletti <weppos@weppos.net>
|
|
10
|
+
# Copyright:: 2007-2008 The Authors
|
|
11
|
+
# License:: MIT License
|
|
12
|
+
# Link:: http://www.simonecarletti.com/
|
|
13
|
+
# Source:: http://gist.github.com/gists/6391/
|
|
14
|
+
#
|
|
15
|
+
module HashRecursiveMerge
|
|
16
|
+
|
|
17
|
+
#
|
|
18
|
+
# Recursive version of Hash#merge!
|
|
19
|
+
#
|
|
20
|
+
# Adds the contents of +other_hash+ to +hsh+,
|
|
21
|
+
# merging entries in +hsh+ with duplicate keys with those from +other_hash+.
|
|
22
|
+
#
|
|
23
|
+
# Compared with Hash#merge!, this method supports nested hashes.
|
|
24
|
+
# When both +hsh+ and +other_hash+ contains an entry with the same key,
|
|
25
|
+
# it merges and returns the values from both arrays.
|
|
26
|
+
#
|
|
27
|
+
# h1 = {"a" => 100, "b" => 200, "c" => {"c1" => 12, "c2" => 14}}
|
|
28
|
+
# h2 = {"b" => 254, "c" => {"c1" => 16, "c3" => 94}}
|
|
29
|
+
# h1.rmerge!(h2) #=> {"a" => 100, "b" => 254, "c" => {"c1" => 16, "c2" => 14, "c3" => 94}}
|
|
30
|
+
#
|
|
31
|
+
# Simply using Hash#merge! would return
|
|
32
|
+
#
|
|
33
|
+
# h1.merge!(h2) #=> {"a" => 100, "b" = >254, "c" => {"c1" => 16, "c3" => 94}}
|
|
34
|
+
#
|
|
35
|
+
def rmerge!(other_hash)
|
|
36
|
+
merge!(other_hash) do |key, oldval, newval|
|
|
37
|
+
oldval.class == self.class ? oldval.rmerge!(newval) : newval
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
#
|
|
42
|
+
# Recursive version of Hash#merge
|
|
43
|
+
#
|
|
44
|
+
# Compared with Hash#merge!, this method supports nested hashes.
|
|
45
|
+
# When both +hsh+ and +other_hash+ contains an entry with the same key,
|
|
46
|
+
# it merges and returns the values from both arrays.
|
|
47
|
+
#
|
|
48
|
+
# Compared with Hash#merge, this method provides a different approch
|
|
49
|
+
# for merging nasted hashes.
|
|
50
|
+
# If the value of a given key is an Hash and both +other_hash+ abd +hsh
|
|
51
|
+
# includes the same key, the value is merged instead replaced with
|
|
52
|
+
# +other_hash+ value.
|
|
53
|
+
#
|
|
54
|
+
# h1 = {"a" => 100, "b" => 200, "c" => {"c1" => 12, "c2" => 14}}
|
|
55
|
+
# h2 = {"b" => 254, "c" => {"c1" => 16, "c3" => 94}}
|
|
56
|
+
# h1.rmerge(h2) #=> {"a" => 100, "b" => 254, "c" => {"c1" => 16, "c2" => 14, "c3" => 94}}
|
|
57
|
+
#
|
|
58
|
+
# Simply using Hash#merge would return
|
|
59
|
+
#
|
|
60
|
+
# h1.merge(h2) #=> {"a" => 100, "b" = >254, "c" => {"c1" => 16, "c3" => 94}}
|
|
61
|
+
#
|
|
62
|
+
def rmerge(other_hash)
|
|
63
|
+
r = {}
|
|
64
|
+
merge(other_hash) do |key, oldval, newval|
|
|
65
|
+
r[key] = oldval.class == self.class ? oldval.rmerge(newval) : newval
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Hash
|
|
73
|
+
include HashRecursiveMerge
|
|
74
|
+
end
|