geokit 1.7.1 → 1.8.0
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.
- 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
|