geokit 1.7.1 → 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +6 -14
- data/.travis.yml +1 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +2 -1
- data/MIT-LICENSE +20 -0
- data/README.markdown +44 -39
- data/Rakefile +15 -0
- data/fixtures/vcr_cassettes/bing_full.yml +102 -0
- data/fixtures/vcr_cassettes/bing_full_au.yml +91 -0
- data/fixtures/vcr_cassettes/bing_full_de.yml +91 -0
- data/fixtures/vcr_cassettes/fcc_reverse_geocode.yml +37 -0
- data/fixtures/vcr_cassettes/free_geo_ip_geocode.yml +36 -0
- data/fixtures/vcr_cassettes/geo_plugin_geocode.yml +38 -0
- data/fixtures/vcr_cassettes/geonames_geocode.yml +304 -0
- data/fixtures/vcr_cassettes/{google3_city.yml → google_city.yml} +0 -0
- data/fixtures/vcr_cassettes/{google3_country_code_biased_result.yml → google_country_code_biased_result.yml} +0 -0
- data/fixtures/vcr_cassettes/{google3_full.yml → google_full.yml} +0 -0
- data/fixtures/vcr_cassettes/{google3_full_short.yml → google_full_short.yml} +0 -0
- data/fixtures/vcr_cassettes/{google3_language_response_fr.yml → google_language_response_fr.yml} +0 -0
- data/fixtures/vcr_cassettes/{google3_multi.yml → google_multi.yml} +0 -0
- data/fixtures/vcr_cassettes/{google3_reverse_madrid.yml → google_reverse_madrid.yml} +0 -0
- data/fixtures/vcr_cassettes/ripe_geocode.yml +66 -0
- data/fixtures/vcr_cassettes/ripe_geocode_au.yml +66 -0
- data/geokit.gemspec +1 -1
- data/lib/geokit.rb +5 -0
- data/lib/geokit/bounds.rb +96 -0
- data/lib/geokit/core_ext.rb +17 -0
- data/lib/geokit/geo_loc.rb +134 -0
- data/lib/geokit/geocoders.rb +48 -35
- data/lib/geokit/geocoders/base_ip.rb +43 -0
- data/lib/geokit/geocoders/bing.rb +101 -0
- data/lib/geokit/geocoders/ca_geocoder.rb +50 -0
- data/lib/geokit/{services → geocoders}/fcc.rb +17 -20
- data/lib/geokit/geocoders/free_geo_ip.rb +34 -0
- data/lib/geokit/geocoders/geo_plugin.rb +33 -0
- data/lib/geokit/geocoders/geonames.rb +53 -0
- data/lib/geokit/{services/google3.rb → geocoders/google.rb} +59 -57
- data/lib/geokit/geocoders/ip.rb +69 -0
- data/lib/geokit/geocoders/mapquest.rb +72 -0
- data/lib/geokit/geocoders/maxmind.rb +29 -0
- data/lib/geokit/geocoders/openstreetmap.rb +119 -0
- data/lib/geokit/geocoders/ripe.rb +41 -0
- data/lib/geokit/{services → geocoders}/us_geocoder.rb +15 -20
- data/lib/geokit/{services → geocoders}/yahoo.rb +52 -55
- data/lib/geokit/geocoders/yandex.rb +61 -0
- data/lib/geokit/inflectors.rb +1 -2
- data/lib/geokit/lat_lng.rb +129 -0
- data/lib/geokit/mappable.rb +41 -424
- data/lib/geokit/multi_geocoder.rb +6 -2
- data/lib/geokit/polygon.rb +46 -0
- data/lib/geokit/version.rb +1 -1
- data/test/helper.rb +2 -12
- data/test/test_base_geocoder.rb +0 -10
- data/test/test_bing_geocoder.rb +60 -0
- data/test/test_fcc_geocoder.rb +23 -0
- data/test/test_free_geo_ip_geocoder.rb +23 -0
- data/test/test_geo_plugin_geocoder.rb +23 -0
- data/test/test_geonames_geocoder.rb +23 -0
- data/test/test_google_geocoder.rb +208 -235
- data/test/test_maxmind_geocoder.rb +35 -4
- data/test/test_multi_geocoder.rb +3 -1
- data/test/test_ripe_geocoder.rb +35 -0
- data/test/test_yahoo_geocoder.rb +0 -12
- metadata +78 -52
- data/LICENSE +0 -25
- data/Manifest.txt +0 -21
- data/data/GeoLiteCity.dat +0 -0
- data/lib/geokit/services/ca_geocoder.rb +0 -55
- data/lib/geokit/services/geo_plugin.rb +0 -31
- data/lib/geokit/services/geonames.rb +0 -53
- data/lib/geokit/services/google.rb +0 -158
- data/lib/geokit/services/ip.rb +0 -103
- data/lib/geokit/services/maxmind.rb +0 -39
- data/lib/geokit/services/openstreetmap.rb +0 -119
- data/lib/geokit/services/ripe.rb +0 -32
- data/lib/geokit/services/yandex.rb +0 -51
- data/test/test_google_geocoder3.rb +0 -238
- data/test/test_google_reverse_geocoder.rb +0 -49
@@ -0,0 +1,61 @@
|
|
1
|
+
module Geokit
|
2
|
+
module Geocoders
|
3
|
+
# Yandex geocoder implementation. Expects the Geokit::Geocoders::YANDEX variable to
|
4
|
+
# contain a Yandex API key (optional). Conforms to the interface set by the Geocoder class.
|
5
|
+
class YandexGeocoder < Geocoder
|
6
|
+
private
|
7
|
+
|
8
|
+
# Template method which does the geocode lookup.
|
9
|
+
def self.do_geocode(address)
|
10
|
+
address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
|
11
|
+
url = submit_url(address_str)
|
12
|
+
res = call_geocoder_service(url)
|
13
|
+
return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
|
14
|
+
parse :json, res.body
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.submit_url(address_str)
|
18
|
+
url = "http://geocode-maps.yandex.ru/1.x/?geocode=#{Geokit::Inflector::url_escape(address_str)}&format=json"
|
19
|
+
url += "&key=#{Geokit::Geocoders::yandex}" if Geokit::Geocoders::yandex
|
20
|
+
url
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.parse_json(result)
|
24
|
+
loc = GeoLoc.new
|
25
|
+
|
26
|
+
coll = result["response"]["GeoObjectCollection"]
|
27
|
+
return loc unless coll["metaDataProperty"]["GeocoderResponseMetaData"]["found"].to_i > 0
|
28
|
+
|
29
|
+
l = coll["featureMember"][0]["GeoObject"]
|
30
|
+
|
31
|
+
loc.success = true
|
32
|
+
loc.provider = "yandex"
|
33
|
+
loc.lng = l["Point"]["pos"].split(" ").first
|
34
|
+
loc.lat = l["Point"]["pos"].split(" ").last
|
35
|
+
|
36
|
+
country = l["metaDataProperty"]["GeocoderMetaData"]["AddressDetails"]["Country"]
|
37
|
+
locality = country["Locality"] || country["AdministrativeArea"]["Locality"] || country["AdministrativeArea"]["SubAdministrativeArea"]["Locality"] rescue nil
|
38
|
+
set_address_components(loc, l, country, locality)
|
39
|
+
set_precision(loc, l, locality)
|
40
|
+
|
41
|
+
loc
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.set_address_components(loc, l, country, locality)
|
45
|
+
loc.country_code = country["CountryNameCode"]
|
46
|
+
loc.full_address = country["AddressLine"]
|
47
|
+
loc.street_address = l["name"]
|
48
|
+
loc.street_number = locality["Thoroughfare"]["Premise"]["PremiseNumber"] rescue nil
|
49
|
+
loc.street_name = locality["Thoroughfare"]["ThoroughfareName"] rescue nil
|
50
|
+
loc.city = locality["LocalityName"] rescue nil
|
51
|
+
loc.state = country["AdministrativeArea"]["AdministrativeAreaName"] rescue nil
|
52
|
+
loc.state ||= country["Locality"]["LocalityName"] rescue nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.set_precision(loc, l, locality)
|
56
|
+
loc.precision = l["metaDataProperty"]["GeocoderMetaData"]["precision"].sub(/exact/, "building").sub(/number|near/, "address").sub(/other/, "city")
|
57
|
+
loc.precision = "country" unless locality
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/geokit/inflectors.rb
CHANGED
@@ -0,0 +1,129 @@
|
|
1
|
+
module Geokit
|
2
|
+
class LatLng
|
3
|
+
include Mappable
|
4
|
+
|
5
|
+
attr_accessor :lat, :lng
|
6
|
+
|
7
|
+
# Accepts latitude and longitude or instantiates an empty instance
|
8
|
+
# if lat and lng are not provided. Converted to floats if provided
|
9
|
+
def initialize(lat=nil, lng=nil)
|
10
|
+
lat = lat.to_f if lat && !lat.is_a?(Numeric)
|
11
|
+
lng = lng.to_f if lng && !lng.is_a?(Numeric)
|
12
|
+
@lat = lat
|
13
|
+
@lng = lng
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.from_json(json)
|
17
|
+
new(json['lat'], json['lng'])
|
18
|
+
end
|
19
|
+
|
20
|
+
# Latitude attribute setter; stored as a float.
|
21
|
+
def lat=(lat)
|
22
|
+
@lat = lat.to_f if lat
|
23
|
+
end
|
24
|
+
|
25
|
+
# Longitude attribute setter; stored as a float;
|
26
|
+
def lng=(lng)
|
27
|
+
@lng=lng.to_f if lng
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns the lat and lng attributes as a comma-separated string.
|
31
|
+
def ll
|
32
|
+
"#{lat},#{lng}"
|
33
|
+
end
|
34
|
+
|
35
|
+
#returns a string with comma-separated lat,lng values
|
36
|
+
def to_s
|
37
|
+
ll
|
38
|
+
end
|
39
|
+
|
40
|
+
#returns a two-element array
|
41
|
+
def to_a
|
42
|
+
[lat,lng]
|
43
|
+
end
|
44
|
+
# Returns true if the candidate object is logically equal. Logical equivalence
|
45
|
+
# is true if the lat and lng attributes are the same for both objects.
|
46
|
+
def ==(other)
|
47
|
+
return false unless other.is_a?(LatLng)
|
48
|
+
lat == other.lat && lng == other.lng
|
49
|
+
end
|
50
|
+
|
51
|
+
def hash
|
52
|
+
lat.hash + lng.hash
|
53
|
+
end
|
54
|
+
|
55
|
+
def eql?(other)
|
56
|
+
self == other
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns true if both lat and lng attributes are defined
|
60
|
+
def valid?
|
61
|
+
lat && lng
|
62
|
+
end
|
63
|
+
|
64
|
+
# A *class* method to take anything which can be inferred as a point and generate
|
65
|
+
# a LatLng from it. You should use this anything you're not sure what the input is,
|
66
|
+
# and want to deal with it as a LatLng if at all possible. Can take:
|
67
|
+
# 1) two arguments (lat,lng)
|
68
|
+
# 2) a string in the format "37.1234,-129.1234" or "37.1234 -129.1234"
|
69
|
+
# 3) a string which can be geocoded on the fly
|
70
|
+
# 4) an array in the format [37.1234,-129.1234]
|
71
|
+
# 5) a LatLng or GeoLoc (which is just passed through as-is)
|
72
|
+
# 6) anything responding to to_lat_lng -- a LatLng will be extracted from it
|
73
|
+
def self.normalize(thing,other=nil)
|
74
|
+
return Geokit::LatLng.new(thing, other) if other
|
75
|
+
|
76
|
+
case thing
|
77
|
+
when String
|
78
|
+
from_string(thing)
|
79
|
+
when Array
|
80
|
+
thing.size == 2 or raise ArgumentError.new("Must initialize with an Array with both latitude and longitude")
|
81
|
+
Geokit::LatLng.new(thing[0],thing[1])
|
82
|
+
when LatLng # will also be true for GeoLocs
|
83
|
+
thing
|
84
|
+
else
|
85
|
+
if thing.respond_to? :to_lat_lng
|
86
|
+
thing.to_lat_lng
|
87
|
+
else
|
88
|
+
raise ArgumentError.new("#{thing} (#{thing.class}) cannot be normalized to a LatLng. We tried interpreting it as an array, string, etc., but no dice.")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.from_string(thing)
|
94
|
+
thing.strip!
|
95
|
+
if match=thing.match(/(\-?\d+\.?\d*)[, ] ?(\-?\d+\.?\d*)$/)
|
96
|
+
Geokit::LatLng.new(match[1],match[2])
|
97
|
+
else
|
98
|
+
res = Geokit::Geocoders::MultiGeocoder.geocode(thing)
|
99
|
+
return res if res.success?
|
100
|
+
raise Geokit::Geocoders::GeocodeError
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Reverse geocodes a LatLng object using the MultiGeocoder (default), or optionally
|
105
|
+
# using a geocoder of your choosing. Returns a new Geokit::GeoLoc object
|
106
|
+
#
|
107
|
+
# ==== Options
|
108
|
+
# * :using - Specifies the geocoder to use for reverse geocoding. Defaults to
|
109
|
+
# MultiGeocoder. Can be either the geocoder class (or any class that
|
110
|
+
# implements do_reverse_geocode for that matter), or the name of
|
111
|
+
# the class without the "Geocoder" part (e.g. :google)
|
112
|
+
#
|
113
|
+
# ==== Examples
|
114
|
+
# LatLng.new(51.4578329, 7.0166848).reverse_geocode # => #<Geokit::GeoLoc:0x12dac20 @state...>
|
115
|
+
# LatLng.new(51.4578329, 7.0166848).reverse_geocode(:using => :google) # => #<Geokit::GeoLoc:0x12dac20 @state...>
|
116
|
+
# LatLng.new(51.4578329, 7.0166848).reverse_geocode(:using => Geokit::Geocoders::GoogleGeocoder) # => #<Geokit::GeoLoc:0x12dac20 @state...>
|
117
|
+
def reverse_geocode(options = { :using => Geokit::Geocoders::MultiGeocoder })
|
118
|
+
if options[:using].is_a?(String) || options[:using].is_a?(Symbol)
|
119
|
+
provider = Geokit::Geocoders.const_get("#{Geokit::Inflector::camelize(options[:using].to_s)}Geocoder")
|
120
|
+
elsif options[:using].respond_to?(:do_reverse_geocode)
|
121
|
+
provider = options[:using]
|
122
|
+
else
|
123
|
+
raise ArgumentError.new("#{options[:using]} is not a valid geocoder.")
|
124
|
+
end
|
125
|
+
|
126
|
+
provider.send(:reverse_geocode, self)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
data/lib/geokit/mappable.rb
CHANGED
@@ -40,27 +40,34 @@ module Geokit
|
|
40
40
|
units = options[:units] || Geokit::default_units
|
41
41
|
formula = options[:formula] || Geokit::default_formula
|
42
42
|
case formula
|
43
|
-
when :sphere
|
44
|
-
|
45
|
-
|
46
|
-
# Ruby 1.9 raises {Math::DomainError}, but it is not defined in Ruby
|
47
|
-
# 1.8. Backwards-compatibly rescue both errors.
|
48
|
-
error_classes << Math::DomainError if defined?(Math::DomainError)
|
49
|
-
|
50
|
-
begin
|
51
|
-
units_sphere_multiplier(units) *
|
52
|
-
Math.acos( Math.sin(deg2rad(from.lat)) * Math.sin(deg2rad(to.lat)) +
|
53
|
-
Math.cos(deg2rad(from.lat)) * Math.cos(deg2rad(to.lat)) *
|
54
|
-
Math.cos(deg2rad(to.lng) - deg2rad(from.lng)))
|
55
|
-
rescue *error_classes
|
56
|
-
0.0
|
57
|
-
end
|
58
|
-
when :flat
|
59
|
-
Math.sqrt((units_per_latitude_degree(units)*(from.lat-to.lat))**2 +
|
60
|
-
(units_per_longitude_degree(from.lat, units)*(from.lng-to.lng))**2)
|
43
|
+
when :sphere then distance_between_sphere(from, to, units)
|
44
|
+
when :flat then distance_between_flat(from, to, units)
|
61
45
|
end
|
62
46
|
end
|
63
47
|
|
48
|
+
def distance_between_sphere(from, to, units)
|
49
|
+
lat_sin = Math.sin(deg2rad(from.lat)) * Math.sin(deg2rad(to.lat))
|
50
|
+
lat_cos = Math.cos(deg2rad(from.lat)) * Math.cos(deg2rad(to.lat))
|
51
|
+
lng_cos = Math.cos(deg2rad(to.lng) - deg2rad(from.lng))
|
52
|
+
units_sphere_multiplier(units) * Math.acos(lat_sin + lat_cos * lng_cos)
|
53
|
+
rescue *math_error_classes
|
54
|
+
0.0
|
55
|
+
end
|
56
|
+
|
57
|
+
def distance_between_flat(from, to, units)
|
58
|
+
lat_length = units_per_latitude_degree(units) * (from.lat - to.lat)
|
59
|
+
lng_length = units_per_longitude_degree(from.lat, units) * (from.lng - to.lng)
|
60
|
+
Math.sqrt(lat_length ** 2 + lng_length ** 2)
|
61
|
+
end
|
62
|
+
|
63
|
+
def math_error_classes
|
64
|
+
error_classes = [Errno::EDOM]
|
65
|
+
|
66
|
+
# Ruby 1.9 raises {Math::DomainError}, but it is not defined in Ruby
|
67
|
+
# 1.8. Backwards-compatibly rescue both errors.
|
68
|
+
error_classes << Math::DomainError if defined?(Math::DomainError)
|
69
|
+
end
|
70
|
+
|
64
71
|
# Returns heading in degrees (0 is north, 90 is east, 180 is south, etc)
|
65
72
|
# from the first point to the second point. Typicaly, the instance methods will be used
|
66
73
|
# instead of this method.
|
@@ -80,23 +87,23 @@ module Geokit
|
|
80
87
|
# an endpoint. Returns a LatLng instance. Typically, the instance method
|
81
88
|
# will be used instead of this method.
|
82
89
|
def endpoint(start,heading, distance, options={})
|
83
|
-
units
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
90
|
+
units = options[:units] || Geokit::default_units
|
91
|
+
ratio = distance.to_f / units_sphere_multiplier(units)
|
92
|
+
start = Geokit::LatLng.normalize(start)
|
93
|
+
lat = deg2rad(start.lat)
|
94
|
+
lng = deg2rad(start.lng)
|
95
|
+
heading = deg2rad(heading)
|
96
|
+
|
97
|
+
sin_ratio = Math.sin(ratio)
|
98
|
+
cos_ratio = Math.cos(ratio)
|
99
|
+
sin_lat = Math.sin(lat)
|
100
|
+
cos_lat = Math.cos(lat)
|
94
101
|
|
95
|
-
end_lat=Math.asin(
|
96
|
-
|
102
|
+
end_lat = Math.asin(sin_lat * cos_ratio +
|
103
|
+
cos_lat * sin_ratio * Math.cos(heading))
|
97
104
|
|
98
|
-
end_lng=lng+Math.atan2(Math.sin(heading)*
|
99
|
-
|
105
|
+
end_lng = lng + Math.atan2(Math.sin(heading) * sin_ratio * cos_lat,
|
106
|
+
cos_ratio - sin_lat * Math.sin(end_lat))
|
100
107
|
|
101
108
|
LatLng.new(rad2deg(end_lat),rad2deg(end_lng))
|
102
109
|
end
|
@@ -172,7 +179,7 @@ module Geokit
|
|
172
179
|
# Extracts a LatLng instance. Use with models that are acts_as_mappable
|
173
180
|
def to_lat_lng
|
174
181
|
return self if instance_of?(Geokit::LatLng) || instance_of?(Geokit::GeoLoc)
|
175
|
-
return LatLng.new(send(self.class.lat_column_name),send(self.class.lng_column_name))
|
182
|
+
return LatLng.new(send(self.class.lat_column_name), send(self.class.lng_column_name))
|
176
183
|
nil
|
177
184
|
end
|
178
185
|
|
@@ -213,394 +220,4 @@ module Geokit
|
|
213
220
|
|
214
221
|
end
|
215
222
|
|
216
|
-
class LatLng
|
217
|
-
include Mappable
|
218
|
-
|
219
|
-
attr_accessor :lat, :lng
|
220
|
-
|
221
|
-
# Accepts latitude and longitude or instantiates an empty instance
|
222
|
-
# if lat and lng are not provided. Converted to floats if provided
|
223
|
-
def initialize(lat=nil, lng=nil)
|
224
|
-
lat = lat.to_f if lat && !lat.is_a?(Numeric)
|
225
|
-
lng = lng.to_f if lng && !lng.is_a?(Numeric)
|
226
|
-
@lat = lat
|
227
|
-
@lng = lng
|
228
|
-
end
|
229
|
-
|
230
|
-
# Latitude attribute setter; stored as a float.
|
231
|
-
def lat=(lat)
|
232
|
-
@lat = lat.to_f if lat
|
233
|
-
end
|
234
|
-
|
235
|
-
# Longitude attribute setter; stored as a float;
|
236
|
-
def lng=(lng)
|
237
|
-
@lng=lng.to_f if lng
|
238
|
-
end
|
239
|
-
|
240
|
-
# Returns the lat and lng attributes as a comma-separated string.
|
241
|
-
def ll
|
242
|
-
"#{lat},#{lng}"
|
243
|
-
end
|
244
|
-
|
245
|
-
#returns a string with comma-separated lat,lng values
|
246
|
-
def to_s
|
247
|
-
ll
|
248
|
-
end
|
249
|
-
|
250
|
-
#returns a two-element array
|
251
|
-
def to_a
|
252
|
-
[lat,lng]
|
253
|
-
end
|
254
|
-
# Returns true if the candidate object is logically equal. Logical equivalence
|
255
|
-
# is true if the lat and lng attributes are the same for both objects.
|
256
|
-
def ==(other)
|
257
|
-
other.is_a?(LatLng) ? self.lat == other.lat && self.lng == other.lng : false
|
258
|
-
end
|
259
|
-
|
260
|
-
def hash
|
261
|
-
lat.hash + lng.hash
|
262
|
-
end
|
263
|
-
|
264
|
-
def eql?(other)
|
265
|
-
self == other
|
266
|
-
end
|
267
|
-
|
268
|
-
# Returns true if both lat and lng attributes are defined
|
269
|
-
def valid?
|
270
|
-
self.lat and self.lng
|
271
|
-
end
|
272
|
-
|
273
|
-
# A *class* method to take anything which can be inferred as a point and generate
|
274
|
-
# a LatLng from it. You should use this anything you're not sure what the input is,
|
275
|
-
# and want to deal with it as a LatLng if at all possible. Can take:
|
276
|
-
# 1) two arguments (lat,lng)
|
277
|
-
# 2) a string in the format "37.1234,-129.1234" or "37.1234 -129.1234"
|
278
|
-
# 3) a string which can be geocoded on the fly
|
279
|
-
# 4) an array in the format [37.1234,-129.1234]
|
280
|
-
# 5) a LatLng or GeoLoc (which is just passed through as-is)
|
281
|
-
# 6) anything which acts_as_mappable -- a LatLng will be extracted from it
|
282
|
-
def self.normalize(thing,other=nil)
|
283
|
-
# if an 'other' thing is supplied, normalize the input by creating an array of two elements
|
284
|
-
thing=[thing,other] if other
|
285
|
-
|
286
|
-
if thing.is_a?(String)
|
287
|
-
thing.strip!
|
288
|
-
if match=thing.match(/(\-?\d+\.?\d*)[, ] ?(\-?\d+\.?\d*)$/)
|
289
|
-
return Geokit::LatLng.new(match[1],match[2])
|
290
|
-
else
|
291
|
-
res = Geokit::Geocoders::MultiGeocoder.geocode(thing)
|
292
|
-
return res if res.success?
|
293
|
-
raise Geokit::Geocoders::GeocodeError
|
294
|
-
end
|
295
|
-
elsif thing.is_a?(Array) && thing.size==2
|
296
|
-
return Geokit::LatLng.new(thing[0],thing[1])
|
297
|
-
elsif thing.is_a?(LatLng) # will also be true for GeoLocs
|
298
|
-
return thing
|
299
|
-
elsif thing.class.respond_to?(:acts_as_mappable) && thing.class.respond_to?(:distance_column_name)
|
300
|
-
return thing.to_lat_lng
|
301
|
-
elsif thing.respond_to? :to_lat_lng
|
302
|
-
return thing.to_lat_lng
|
303
|
-
end
|
304
|
-
|
305
|
-
raise ArgumentError.new("#{thing} (#{thing.class}) cannot be normalized to a LatLng. We tried interpreting it as an array, string, Mappable, etc., but no dice.")
|
306
|
-
end
|
307
|
-
|
308
|
-
# Reverse geocodes a LatLng object using the MultiGeocoder (default), or optionally
|
309
|
-
# using a geocoder of your choosing. Returns a new Geokit::GeoLoc object
|
310
|
-
#
|
311
|
-
# ==== Options
|
312
|
-
# * :using - Specifies the geocoder to use for reverse geocoding. Defaults to
|
313
|
-
# MultiGeocoder. Can be either the geocoder class (or any class that
|
314
|
-
# implements do_reverse_geocode for that matter), or the name of
|
315
|
-
# the class without the "Geocoder" part (e.g. :google)
|
316
|
-
#
|
317
|
-
# ==== Examples
|
318
|
-
# LatLng.new(51.4578329, 7.0166848).reverse_geocode # => #<Geokit::GeoLoc:0x12dac20 @state...>
|
319
|
-
# LatLng.new(51.4578329, 7.0166848).reverse_geocode(:using => :google) # => #<Geokit::GeoLoc:0x12dac20 @state...>
|
320
|
-
# LatLng.new(51.4578329, 7.0166848).reverse_geocode(:using => Geokit::Geocoders::GoogleGeocoder) # => #<Geokit::GeoLoc:0x12dac20 @state...>
|
321
|
-
def reverse_geocode(options = { :using => Geokit::Geocoders::MultiGeocoder })
|
322
|
-
if options[:using].is_a?(String) or options[:using].is_a?(Symbol)
|
323
|
-
provider = Geokit::Geocoders.const_get("#{Geokit::Inflector::camelize(options[:using].to_s)}Geocoder")
|
324
|
-
elsif options[:using].respond_to?(:do_reverse_geocode)
|
325
|
-
provider = options[:using]
|
326
|
-
else
|
327
|
-
raise ArgumentError.new("#{options[:using]} is not a valid geocoder.")
|
328
|
-
end
|
329
|
-
|
330
|
-
provider.send(:reverse_geocode, self)
|
331
|
-
end
|
332
|
-
end
|
333
|
-
|
334
|
-
# This class encapsulates the result of a geocoding call.
|
335
|
-
# It's primary purpose is to homogenize the results of multiple
|
336
|
-
# geocoding providers. It also provides some additional functionality, such as
|
337
|
-
# the "full address" method for geocoders that do not provide a
|
338
|
-
# full address in their results (for example, Yahoo), and the "is_us" method.
|
339
|
-
#
|
340
|
-
# Some geocoders can return multple results. Geoloc can capture multiple results through
|
341
|
-
# its "all" method.
|
342
|
-
#
|
343
|
-
# For the geocoder setting the results, it would look something like this:
|
344
|
-
# geo=GeoLoc.new(first_result)
|
345
|
-
# geo.all.push(second_result)
|
346
|
-
# geo.all.push(third_result)
|
347
|
-
#
|
348
|
-
# Then, for the user of the result:
|
349
|
-
#
|
350
|
-
# puts geo.full_address # just like usual
|
351
|
-
# puts geo.all.size => 3 # there's three results total
|
352
|
-
# puts geo.all.first # all is just an array or additional geolocs,
|
353
|
-
# so do what you want with it
|
354
|
-
class GeoLoc < LatLng
|
355
|
-
|
356
|
-
# Location attributes. Full address is a concatenation of all values. For example:
|
357
|
-
# 100 Spear St, San Francisco, CA, 94101, US
|
358
|
-
# Street number and street name are extracted from the street address attribute if they don't exist
|
359
|
-
attr_accessor :street_number, :street_name, :street_address, :city, :state, :zip, :country_code, :country
|
360
|
-
attr_accessor :full_address, :all, :district, :province, :sub_premise, :neighborhood
|
361
|
-
# Attributes set upon return from geocoding. Success will be true for successful
|
362
|
-
# geocode lookups. The provider will be set to the name of the providing geocoder.
|
363
|
-
# Finally, precision is an indicator of the accuracy of the geocoding.
|
364
|
-
attr_accessor :success, :provider, :precision, :suggested_bounds
|
365
|
-
# accuracy is set for Yahoo and Google geocoders, it is a numeric value of the
|
366
|
-
# precision. see http://code.google.com/apis/maps/documentation/geocoding/#GeocodingAccuracy
|
367
|
-
attr_accessor :accuracy
|
368
|
-
# FCC Attributes
|
369
|
-
attr_accessor :district_fips, :state_fips, :block_fips
|
370
|
-
|
371
|
-
|
372
|
-
# Constructor expects a hash of symbols to correspond with attributes.
|
373
|
-
def initialize(h={})
|
374
|
-
@all = [self]
|
375
|
-
|
376
|
-
@street_address=h[:street_address]
|
377
|
-
@sub_premise=nil
|
378
|
-
@street_number=nil
|
379
|
-
@street_name=nil
|
380
|
-
@city=h[:city]
|
381
|
-
@state=h[:state]
|
382
|
-
@zip=h[:zip]
|
383
|
-
@country_code=h[:country_code]
|
384
|
-
@province = h[:province]
|
385
|
-
@success=false
|
386
|
-
@precision='unknown'
|
387
|
-
@full_address=nil
|
388
|
-
super(h[:lat],h[:lng])
|
389
|
-
end
|
390
|
-
|
391
|
-
# Returns true if geocoded to the United States.
|
392
|
-
def is_us?
|
393
|
-
country_code == 'US'
|
394
|
-
end
|
395
|
-
|
396
|
-
def success?
|
397
|
-
success == true
|
398
|
-
end
|
399
|
-
|
400
|
-
# full_address is provided by google but not by yahoo. It is intended that the google
|
401
|
-
# geocoding method will provide the full address, whereas for yahoo it will be derived
|
402
|
-
# from the parts of the address we do have.
|
403
|
-
def full_address
|
404
|
-
@full_address ? @full_address : to_geocodeable_s
|
405
|
-
end
|
406
|
-
|
407
|
-
# Extracts the street number from the street address where possible.
|
408
|
-
def street_number
|
409
|
-
@street_number ||= street_address[/(\d*)/] if street_address
|
410
|
-
@street_number
|
411
|
-
end
|
412
|
-
|
413
|
-
# Returns the street name portion of the street address where possible
|
414
|
-
def street_name
|
415
|
-
@street_name||=street_address[street_number.length, street_address.length].strip if street_address
|
416
|
-
@street_name
|
417
|
-
end
|
418
|
-
|
419
|
-
# gives you all the important fields as key-value pairs
|
420
|
-
def hash
|
421
|
-
res={}
|
422
|
-
[:success, :lat, :lng, :country_code, :city, :state, :zip, :street_address, :province,
|
423
|
-
:district, :provider, :full_address, :is_us?, :ll, :precision, :district_fips, :state_fips,
|
424
|
-
:block_fips, :sub_premise].each { |s| res[s] = self.send(s.to_s) }
|
425
|
-
res
|
426
|
-
end
|
427
|
-
alias to_hash hash
|
428
|
-
|
429
|
-
# Sets the city after capitalizing each word within the city name.
|
430
|
-
def city=(city)
|
431
|
-
@city = Geokit::Inflector::titleize(city) if city
|
432
|
-
end
|
433
|
-
|
434
|
-
# Sets the street address after capitalizing each word within the street address.
|
435
|
-
def street_address=(address)
|
436
|
-
if address and not ['google','google3'].include?(self.provider)
|
437
|
-
@street_address = Geokit::Inflector::titleize(address)
|
438
|
-
else
|
439
|
-
@street_address = address
|
440
|
-
end
|
441
|
-
end
|
442
|
-
|
443
|
-
# Returns a comma-delimited string consisting of the street address, city, state,
|
444
|
-
# zip, and country code. Only includes those attributes that are non-blank.
|
445
|
-
def to_geocodeable_s
|
446
|
-
a=[street_address, district, city, province, state, zip, country_code].compact
|
447
|
-
a.delete_if { |e| !e || e == '' }
|
448
|
-
a.join(', ')
|
449
|
-
end
|
450
|
-
|
451
|
-
def to_yaml_properties
|
452
|
-
(instance_variables - ['@all', :@all]).sort
|
453
|
-
end
|
454
|
-
|
455
|
-
def encode_with(coder)
|
456
|
-
to_yaml_properties.each do |name|
|
457
|
-
coder[name[1..-1].to_s] = instance_variable_get(name.to_s)
|
458
|
-
end
|
459
|
-
end
|
460
|
-
|
461
|
-
# Returns a string representation of the instance.
|
462
|
-
def to_s
|
463
|
-
"Provider: #{provider}\nStreet: #{street_address}\nCity: #{city}\nState: #{state}\nZip: #{zip}\nLatitude: #{lat}\nLongitude: #{lng}\nCountry: #{country_code}\nSuccess: #{success}"
|
464
|
-
end
|
465
|
-
end
|
466
|
-
|
467
|
-
# Bounds represents a rectangular bounds, defined by the SW and NE corners
|
468
|
-
class Bounds
|
469
|
-
# sw and ne are LatLng objects
|
470
|
-
attr_accessor :sw, :ne
|
471
|
-
|
472
|
-
# provide sw and ne to instantiate a new Bounds instance
|
473
|
-
def initialize(sw,ne)
|
474
|
-
raise ArgumentError if !(sw.is_a?(Geokit::LatLng) && ne.is_a?(Geokit::LatLng))
|
475
|
-
@sw,@ne=sw,ne
|
476
|
-
end
|
477
|
-
|
478
|
-
#returns the a single point which is the center of the rectangular bounds
|
479
|
-
def center
|
480
|
-
@sw.midpoint_to(@ne)
|
481
|
-
end
|
482
|
-
|
483
|
-
# a simple string representation:sw,ne
|
484
|
-
def to_s
|
485
|
-
"#{@sw.to_s},#{@ne.to_s}"
|
486
|
-
end
|
487
|
-
|
488
|
-
# a two-element array of two-element arrays: sw,ne
|
489
|
-
def to_a
|
490
|
-
[@sw.to_a, @ne.to_a]
|
491
|
-
end
|
492
|
-
|
493
|
-
# Returns true if the bounds contain the passed point.
|
494
|
-
# allows for bounds which cross the meridian
|
495
|
-
def contains?(point)
|
496
|
-
point=Geokit::LatLng.normalize(point)
|
497
|
-
res = point.lat > @sw.lat && point.lat < @ne.lat
|
498
|
-
if crosses_meridian?
|
499
|
-
res &= point.lng < @ne.lng || point.lng > @sw.lng
|
500
|
-
else
|
501
|
-
res &= point.lng < @ne.lng && point.lng > @sw.lng
|
502
|
-
end
|
503
|
-
res
|
504
|
-
end
|
505
|
-
|
506
|
-
# returns true if the bounds crosses the international dateline
|
507
|
-
def crosses_meridian?
|
508
|
-
@sw.lng > @ne.lng
|
509
|
-
end
|
510
|
-
|
511
|
-
# Returns true if the candidate object is logically equal. Logical equivalence
|
512
|
-
# is true if the lat and lng attributes are the same for both objects.
|
513
|
-
def ==(other)
|
514
|
-
other.is_a?(Bounds) ? self.sw == other.sw && self.ne == other.ne : false
|
515
|
-
end
|
516
|
-
|
517
|
-
# Equivalent to Google Maps API's .toSpan() method on GLatLng's.
|
518
|
-
#
|
519
|
-
# Returns a LatLng object, whose coordinates represent the size of a rectangle
|
520
|
-
# defined by these bounds.
|
521
|
-
def to_span
|
522
|
-
lat_span = (@ne.lat - @sw.lat).abs
|
523
|
-
lng_span = (crosses_meridian? ? 360 + @ne.lng - @sw.lng : @ne.lng - @sw.lng).abs
|
524
|
-
Geokit::LatLng.new(lat_span, lng_span)
|
525
|
-
end
|
526
|
-
|
527
|
-
class <<self
|
528
|
-
|
529
|
-
# returns an instance of bounds which completely encompases the given circle
|
530
|
-
def from_point_and_radius(point,radius,options={})
|
531
|
-
point=LatLng.normalize(point)
|
532
|
-
p0=point.endpoint(0,radius,options)
|
533
|
-
p90=point.endpoint(90,radius,options)
|
534
|
-
p180=point.endpoint(180,radius,options)
|
535
|
-
p270=point.endpoint(270,radius,options)
|
536
|
-
sw=Geokit::LatLng.new(p180.lat,p270.lng)
|
537
|
-
ne=Geokit::LatLng.new(p0.lat,p90.lng)
|
538
|
-
Geokit::Bounds.new(sw,ne)
|
539
|
-
end
|
540
|
-
|
541
|
-
# Takes two main combinations of arguments to create a bounds:
|
542
|
-
# point,point (this is the only one which takes two arguments
|
543
|
-
# [point,point]
|
544
|
-
# . . . where a point is anything LatLng#normalize can handle (which is quite a lot)
|
545
|
-
#
|
546
|
-
# NOTE: everything combination is assumed to pass points in the order sw, ne
|
547
|
-
def normalize (thing,other=nil)
|
548
|
-
# maybe this will be simple -- an actual bounds object is passed, and we can all go home
|
549
|
-
return thing if thing.is_a? Bounds
|
550
|
-
|
551
|
-
# no? OK, if there's no "other," the thing better be a two-element array
|
552
|
-
thing,other=thing if !other && thing.is_a?(Array) && thing.size==2
|
553
|
-
|
554
|
-
# Now that we're set with a thing and another thing, let LatLng do the heavy lifting.
|
555
|
-
# Exceptions may be thrown
|
556
|
-
Bounds.new(Geokit::LatLng.normalize(thing),Geokit::LatLng.normalize(other))
|
557
|
-
end
|
558
|
-
end
|
559
|
-
end
|
560
|
-
|
561
|
-
# A complex polygon made of multiple points. End point must equal start point to close the poly.
|
562
|
-
class Polygon
|
563
|
-
|
564
|
-
attr_accessor :poly_y, :poly_x
|
565
|
-
|
566
|
-
def initialize(points)
|
567
|
-
# Pass in an array of Geokit::LatLng
|
568
|
-
@poly_x = []
|
569
|
-
@poly_y = []
|
570
|
-
|
571
|
-
points.each do |point|
|
572
|
-
@poly_x << point.lng
|
573
|
-
@poly_y << point.lat
|
574
|
-
end
|
575
|
-
|
576
|
-
# A Polygon must be 'closed', the last point equal to the first point
|
577
|
-
if not @poly_x[0] == @poly_x[-1] or not @poly_y[0] == @poly_y[-1]
|
578
|
-
# Append the first point to the array to close the polygon
|
579
|
-
@poly_x << @poly_x[0]
|
580
|
-
@poly_y << @poly_y[0]
|
581
|
-
end
|
582
|
-
|
583
|
-
end
|
584
|
-
|
585
|
-
def contains?(point)
|
586
|
-
j = @poly_x.length - 1
|
587
|
-
oddNodes = false
|
588
|
-
x = point.lng
|
589
|
-
y = point.lat
|
590
|
-
|
591
|
-
for i in (0..j)
|
592
|
-
if (@poly_y[i] < y && @poly_y[j] >= y ||
|
593
|
-
@poly_y[j] < y && @poly_y[i] >= y)
|
594
|
-
if (@poly_x[i] + (y - @poly_y[i]) / (@poly_y[j] - @poly_y[i]) * (@poly_x[j] - @poly_x[i]) < x)
|
595
|
-
oddNodes = !oddNodes
|
596
|
-
end
|
597
|
-
end
|
598
|
-
|
599
|
-
j=i
|
600
|
-
end
|
601
|
-
|
602
|
-
oddNodes
|
603
|
-
end # contains?
|
604
|
-
end # class Polygon
|
605
|
-
|
606
223
|
end
|