geokit 1.8.5 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.travis.yml +0 -1
  3. data/CHANGELOG.md +7 -0
  4. data/README.markdown +31 -14
  5. data/fixtures/vcr_cassettes/geonames_geocode.yml +1 -1
  6. data/fixtures/vcr_cassettes/geonames_geocode_premium.yml +42 -0
  7. data/fixtures/vcr_cassettes/google_country_code_biased_result_orly.yml +160 -0
  8. data/fixtures/vcr_cassettes/google_country_code_biased_result_toledo.yml +111 -0
  9. data/fixtures/vcr_cassettes/google_result_toledo_default_bias.yml +275 -0
  10. data/fixtures/vcr_cassettes/google_sublocality.yml +126 -0
  11. data/fixtures/vcr_cassettes/mapbox_forward_geocode.yml +59 -0
  12. data/fixtures/vcr_cassettes/mapbox_reverse_geocode.yml +63 -0
  13. data/fixtures/vcr_cassettes/opencage_city.yml +51 -0
  14. data/fixtures/vcr_cassettes/opencage_full.yml +65 -0
  15. data/fixtures/vcr_cassettes/opencage_language_response_es.yml +66 -0
  16. data/fixtures/vcr_cassettes/opencage_reverse_madrid.yml +51 -0
  17. data/fixtures/vcr_cassettes/opencage_reverse_prilep.yml +53 -0
  18. data/geokit.gemspec +5 -3
  19. data/lib/geokit/bounds.rb +40 -31
  20. data/lib/geokit/core_ext.rb +1 -1
  21. data/lib/geokit/geo_loc.rb +63 -41
  22. data/lib/geokit/geocoders.rb +13 -13
  23. data/lib/geokit/geocoders/bing.rb +9 -9
  24. data/lib/geokit/geocoders/ca_geocoder.rb +29 -29
  25. data/lib/geokit/geocoders/fcc.rb +4 -4
  26. data/lib/geokit/geocoders/free_geo_ip.rb +6 -7
  27. data/lib/geokit/geocoders/geo_plugin.rb +5 -6
  28. data/lib/geokit/geocoders/geocodio.rb +2 -2
  29. data/lib/geokit/geocoders/geonames.rb +18 -12
  30. data/lib/geokit/geocoders/google.rb +31 -30
  31. data/lib/geokit/geocoders/ip.rb +3 -4
  32. data/lib/geokit/geocoders/mapbox.rb +94 -0
  33. data/lib/geokit/geocoders/mapquest.rb +5 -5
  34. data/lib/geokit/geocoders/opencage.rb +93 -0
  35. data/lib/geokit/geocoders/openstreetmap.rb +9 -6
  36. data/lib/geokit/geocoders/ripe.rb +3 -3
  37. data/lib/geokit/geocoders/us_geocoder.rb +10 -9
  38. data/lib/geokit/geocoders/yahoo.rb +33 -34
  39. data/lib/geokit/geocoders/yandex.rb +17 -17
  40. data/lib/geokit/inflectors.rb +4 -10
  41. data/lib/geokit/lat_lng.rb +50 -26
  42. data/lib/geokit/mappable.rb +83 -83
  43. data/lib/geokit/multi_geocoder.rb +25 -20
  44. data/lib/geokit/net_adapter/net_http.rb +7 -4
  45. data/lib/geokit/polygon.rb +36 -4
  46. data/lib/geokit/version.rb +1 -1
  47. data/test/helper.rb +15 -13
  48. data/test/test_base_geocoder.rb +6 -7
  49. data/test/test_bing_geocoder.rb +20 -21
  50. data/test/test_bounds.rb +26 -28
  51. data/test/test_ca_geocoder.rb +9 -10
  52. data/test/test_fcc_geocoder.rb +1 -1
  53. data/test/test_free_geo_ip_geocoder.rb +1 -1
  54. data/test/test_geo_plugin_geocoder.rb +9 -9
  55. data/test/test_geoloc.rb +7 -6
  56. data/test/test_geonames_geocoder.rb +28 -6
  57. data/test/test_google_geocoder.rb +210 -176
  58. data/test/test_inflector.rb +0 -1
  59. data/test/test_ipgeocoder.rb +17 -18
  60. data/test/test_latlng.rb +105 -85
  61. data/test/test_map_quest.rb +18 -21
  62. data/test/test_mapbox_geocoder.rb +31 -0
  63. data/test/test_mappable.rb +46 -0
  64. data/test/test_maxmind_geocoder.rb +1 -3
  65. data/test/test_multi_geocoder.rb +8 -9
  66. data/test/test_multi_ip_geocoder.rb +3 -5
  67. data/test/test_net_adapter.rb +4 -4
  68. data/test/test_opencage_geocoder.rb +108 -0
  69. data/test/test_openstreetmap_geocoder.rb +62 -44
  70. data/test/{test_polygon_contains.rb → test_polygon.rb} +30 -20
  71. data/test/test_ripe_geocoder.rb +2 -0
  72. data/test/test_us_geocoder.rb +7 -8
  73. data/test/test_yahoo_geocoder.rb +20 -21
  74. data/test/test_yandex_geocoder.rb +34 -35
  75. metadata +79 -56
  76. data/fixtures/vcr_cassettes/google_country_code_biased_result.yml +0 -401
@@ -1,44 +1,43 @@
1
- #require 'forwardable'
1
+ # require 'forwardable'
2
2
 
3
3
  module Geokit
4
- # Contains class and instance methods providing distance calcuation services. This
5
- # module is meant to be mixed into classes containing lat and lng attributes where
6
- # distance calculation is desired.
4
+ # Contains class and instance methods providing distance calcuation services.
5
+ # This module is meant to be mixed into classes containing lat and lng
6
+ # attributes where distance calculation is desired.
7
7
  #
8
8
  # At present, two forms of distance calculations are provided:
9
9
  #
10
- # * Pythagorean Theory (flat Earth) - which assumes the world is flat and loses accuracy over long distances.
10
+ # * Pythagorean Theory (flat Earth) - which assumes the world is flat and
11
+ # loses accuracy over long distances.
11
12
  # * Haversine (sphere) - which is fairly accurate, but at a performance cost.
12
13
  #
13
14
  # Distance units supported are :miles, :kms, and :nms.
14
15
  module Mappable
15
- PI_DIV_RAD = 0.0174
16
- KMS_PER_MILE = 1.609
17
- NMS_PER_MILE = 0.868976242
18
- EARTH_RADIUS_IN_MILES = 3963.19
19
- EARTH_RADIUS_IN_KMS = EARTH_RADIUS_IN_MILES * KMS_PER_MILE
20
- EARTH_RADIUS_IN_NMS = EARTH_RADIUS_IN_MILES * NMS_PER_MILE
21
- MILES_PER_LATITUDE_DEGREE = 69.1
22
- KMS_PER_LATITUDE_DEGREE = MILES_PER_LATITUDE_DEGREE * KMS_PER_MILE
23
- NMS_PER_LATITUDE_DEGREE = MILES_PER_LATITUDE_DEGREE * NMS_PER_MILE
24
- LATITUDE_DEGREES = EARTH_RADIUS_IN_MILES / MILES_PER_LATITUDE_DEGREE
25
-
26
16
  # Mix below class methods into the includer.
27
17
  def self.included(receiver) # :nodoc:
28
18
  receiver.extend ClassMethods
29
19
  end
30
20
 
31
21
  module ClassMethods #:nodoc:
22
+ PI_DIV_RAD = Math::PI / 180
23
+ EARTH_RADIUS_IN_METERS = 6376772.71
24
+ METERS_PER_LATITUDE_DEGREE = 111181.9
25
+
26
+ EARTH_RADIUS = {}
27
+ PER_LATITUDE_DEGREE = {}
28
+
32
29
  # Returns the distance between two points. The from and to parameters are
33
30
  # required to have lat and lng attributes. Valid options are:
34
- # :units - valid values are :miles, :kms, :nms (Geokit::default_units is the default)
35
- # :formula - valid values are :flat or :sphere (Geokit::default_formula is the default)
36
- def distance_between(from, to, options={})
37
- from=Geokit::LatLng.normalize(from)
38
- to=Geokit::LatLng.normalize(to)
31
+ # :units - valid values are :miles, :kms, :nms
32
+ # (Geokit::default_units is the default)
33
+ # :formula - valid values are :flat or :sphere
34
+ # (Geokit::default_formula is the default)
35
+ def distance_between(from, to, options = {})
36
+ from = Geokit::LatLng.normalize(from)
37
+ to = Geokit::LatLng.normalize(to)
39
38
  return 0.0 if from == to # fixes a "zero-distance" bug
40
- units = options[:units] || Geokit::default_units
41
- formula = options[:formula] || Geokit::default_formula
39
+ units = options[:units] || Geokit.default_units
40
+ formula = options[:formula] || Geokit.default_formula
42
41
  case formula
43
42
  when :sphere then distance_between_sphere(from, to, units)
44
43
  when :flat then distance_between_flat(from, to, units)
@@ -56,8 +55,9 @@ module Geokit
56
55
 
57
56
  def distance_between_flat(from, to, units)
58
57
  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)
58
+ lng_length = units_per_longitude_degree(from.lat, units) *
59
+ (from.lng - to.lng)
60
+ Math.sqrt(lat_length**2 + lng_length**2)
61
61
  end
62
62
 
63
63
  # Ruby 1.9 raises {Math::DomainError}, but it is not defined in Ruby 1.8
@@ -67,25 +67,26 @@ module Geokit
67
67
  end
68
68
 
69
69
  # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc)
70
- # from the first point to the second point. Typicaly, the instance methods will be used
71
- # instead of this method.
72
- def heading_between(from,to)
73
- from=Geokit::LatLng.normalize(from)
74
- to=Geokit::LatLng.normalize(to)
75
-
76
- d_lng=deg2rad(to.lng-from.lng)
77
- from_lat=deg2rad(from.lat)
78
- to_lat=deg2rad(to.lat)
79
- y=Math.sin(d_lng) * Math.cos(to_lat)
80
- x=Math.cos(from_lat)*Math.sin(to_lat)-Math.sin(from_lat)*Math.cos(to_lat)*Math.cos(d_lng)
81
- heading=to_heading(Math.atan2(y,x))
70
+ # from the first point to the second point. Typicaly, the instance methods
71
+ # will be used instead of this method.
72
+ def heading_between(from, to)
73
+ from = Geokit::LatLng.normalize(from)
74
+ to = Geokit::LatLng.normalize(to)
75
+
76
+ d_lng = deg2rad(to.lng - from.lng)
77
+ from_lat = deg2rad(from.lat)
78
+ to_lat = deg2rad(to.lat)
79
+ y = Math.sin(d_lng) * Math.cos(to_lat)
80
+ x = Math.cos(from_lat) * Math.sin(to_lat) -
81
+ Math.sin(from_lat) * Math.cos(to_lat) * Math.cos(d_lng)
82
+ to_heading(Math.atan2(y, x))
82
83
  end
83
84
 
84
85
  # Given a start point, distance, and heading (in degrees), provides
85
86
  # an endpoint. Returns a LatLng instance. Typically, the instance method
86
87
  # will be used instead of this method.
87
- def endpoint(start,heading, distance, options={})
88
- units = options[:units] || Geokit::default_units
88
+ def endpoint(start, heading, distance, options = {})
89
+ units = options[:units] || Geokit.default_units
89
90
  ratio = distance.to_f / units_sphere_multiplier(units)
90
91
  start = Geokit::LatLng.normalize(start)
91
92
  lat = deg2rad(start.lat)
@@ -103,21 +104,20 @@ module Geokit
103
104
  end_lng = lng + Math.atan2(Math.sin(heading) * sin_ratio * cos_lat,
104
105
  cos_ratio - sin_lat * Math.sin(end_lat))
105
106
 
106
- LatLng.new(rad2deg(end_lat),rad2deg(end_lng))
107
+ LatLng.new(rad2deg(end_lat), rad2deg(end_lng))
107
108
  end
108
109
 
109
110
  # Returns the midpoint, given two points. Returns a LatLng.
110
111
  # Typically, the instance method will be used instead of this method.
111
112
  # Valid option:
112
- # :units - valid values are :miles, :kms, or :nms (:miles is the default)
113
- def midpoint_between(from,to,options={})
114
- from=Geokit::LatLng.normalize(from)
115
-
116
- units = options[:units] || Geokit::default_units
117
-
118
- heading=from.heading_to(to)
119
- distance=from.distance_to(to,options)
120
- midpoint=from.endpoint(heading,distance/2,options)
113
+ # :units - valid values are :miles, :kms, or :nms
114
+ # (:miles is the default)
115
+ def midpoint_between(from, to, options = {})
116
+ from = Geokit::LatLng.normalize(from)
117
+
118
+ heading = from.heading_to(to)
119
+ distance = from.distance_to(to, options)
120
+ from.endpoint(heading, distance / 2, options)
121
121
  end
122
122
 
123
123
  # Geocodes a location using the multi geocoder.
@@ -139,8 +139,6 @@ module Geokit
139
139
  (seconds % 60) # seconds as positive float
140
140
  ]
141
141
  end
142
-
143
- protected
144
142
 
145
143
  def deg2rad(degrees)
146
144
  degrees.to_f / 180.0 * Math::PI
@@ -151,35 +149,34 @@ module Geokit
151
149
  end
152
150
 
153
151
  def to_heading(rad)
154
- (rad2deg(rad)+360)%360
152
+ (rad2deg(rad) + 360) % 360
153
+ end
154
+
155
+ def self.register_unit(key, in_meters)
156
+ EARTH_RADIUS[key] = EARTH_RADIUS_IN_METERS * in_meters
157
+ PER_LATITUDE_DEGREE[key] = METERS_PER_LATITUDE_DEGREE * in_meters
155
158
  end
156
159
 
160
+ register_unit :meters, 1
161
+ register_unit :kms, 1 / 1000.0
162
+ register_unit :miles, 1 / 1609.0
163
+ register_unit :nms, 0.0005400722448725917
164
+
157
165
  # Returns the multiplier used to obtain the correct distance units.
166
+ # TODO make more accurate by coping
167
+ # http://msi.nga.mil/MSISiteContent/StaticFiles/Calculators/degree.html
158
168
  def units_sphere_multiplier(units)
159
- case units
160
- when :kms; EARTH_RADIUS_IN_KMS
161
- when :nms; EARTH_RADIUS_IN_NMS
162
- else EARTH_RADIUS_IN_MILES
163
- end
169
+ EARTH_RADIUS[units]
164
170
  end
165
171
 
166
172
  # Returns the number of units per latitude degree.
167
173
  def units_per_latitude_degree(units)
168
- case units
169
- when :kms; KMS_PER_LATITUDE_DEGREE
170
- when :nms; NMS_PER_LATITUDE_DEGREE
171
- else MILES_PER_LATITUDE_DEGREE
172
- end
174
+ PER_LATITUDE_DEGREE[units]
173
175
  end
174
176
 
175
177
  # Returns the number units per longitude degree.
176
178
  def units_per_longitude_degree(lat, units)
177
- miles_per_longitude_degree = (LATITUDE_DEGREES * Math.cos(lat * PI_DIV_RAD)).abs
178
- case units
179
- when :kms; miles_per_longitude_degree * KMS_PER_MILE
180
- when :nms; miles_per_longitude_degree * NMS_PER_MILE
181
- else miles_per_longitude_degree
182
- end
179
+ units_sphere_multiplier(units) * Math.cos(lat * PI_DIV_RAD) * PI_DIV_RAD
183
180
  end
184
181
  end
185
182
 
@@ -189,46 +186,49 @@ module Geokit
189
186
 
190
187
  # Extracts a LatLng instance. Use with models that are acts_as_mappable
191
188
  def to_lat_lng
192
- return self if instance_of?(Geokit::LatLng) || instance_of?(Geokit::GeoLoc)
193
- return LatLng.new(send(self.class.lat_column_name), send(self.class.lng_column_name))
194
- nil
189
+ if instance_of?(Geokit::LatLng) || instance_of?(Geokit::GeoLoc)
190
+ return self
191
+ end
192
+ lat = send(self.class.lat_column_name)
193
+ lng = send(self.class.lng_column_name)
194
+ LatLng.new(lat, lng)
195
195
  end
196
196
 
197
197
  # Returns the distance from another point. The other point parameter is
198
198
  # required to have lat and lng attributes. Valid options are:
199
199
  # :units - valid values are :miles, :kms, :or :nms (:miles is the default)
200
200
  # :formula - valid values are :flat or :sphere (:sphere is the default)
201
- def distance_to(other, options={})
201
+ def distance_to(other, options = {})
202
202
  self.class.distance_between(self, other, options)
203
203
  end
204
204
  alias distance_from distance_to
205
205
 
206
- # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc)
207
- # to the given point. The given point can be a LatLng or a string to be Geocoded
206
+ # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc) to
207
+ # the given point. The given point can be a LatLng or a string to be
208
+ # Geocoded
208
209
  def heading_to(other)
209
- self.class.heading_between(self,other)
210
+ self.class.heading_between(self, other)
210
211
  end
211
212
 
212
213
  # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc)
213
- # FROM the given point. The given point can be a LatLng or a string to be Geocoded
214
+ # FROM the given point. The given point can be a LatLng or a string to be
215
+ # Geocoded
214
216
  def heading_from(other)
215
- self.class.heading_between(other,self)
217
+ self.class.heading_between(other, self)
216
218
  end
217
219
 
218
220
  # Returns the endpoint, given a heading (in degrees) and distance.
219
221
  # Valid option:
220
222
  # :units - valid values are :miles, :kms, or :nms (:miles is the default)
221
- def endpoint(heading,distance,options={})
222
- self.class.endpoint(self,heading,distance,options)
223
+ def endpoint(heading, distance, options = {})
224
+ self.class.endpoint(self, heading, distance, options)
223
225
  end
224
226
 
225
227
  # Returns the midpoint, given another point on the map.
226
228
  # Valid option:
227
229
  # :units - valid values are :miles, :kms, or :nms (:miles is the default)
228
- def midpoint_to(other, options={})
229
- self.class.midpoint_between(self,other,options)
230
+ def midpoint_to(other, options = {})
231
+ self.class.midpoint_between(self, other, options)
230
232
  end
231
-
232
233
  end
233
-
234
234
  end
@@ -1,26 +1,29 @@
1
1
  module Geokit
2
2
  module Geocoders
3
- # -------------------------------------------------------------------------------------------
3
+ # --------------------------------------------------------------------------
4
4
  # The Multi Geocoder
5
- # -------------------------------------------------------------------------------------------
5
+ # --------------------------------------------------------------------------
6
6
 
7
- # Provides methods to geocode with a variety of geocoding service providers, plus failover
8
- # among providers in the order you configure. When 2nd parameter is set 'true', perform
9
- # ip location lookup with 'address' as the ip address.
7
+ # Provides methods to geocode with a variety of geocoding service providers,
8
+ # plus failover among providers in the order you configure. When 2nd
9
+ # parameter is set 'true', perform ip location lookup with 'address' as the
10
+ # ip address.
10
11
  #
11
12
  # Goal:
12
13
  # - homogenize the results of multiple geocoders
13
14
  #
14
15
  # Limitations:
15
- # - currently only provides the first result. Sometimes geocoders will return multiple results.
16
+ # - currently only provides the first result. Sometimes geocoders will
17
+ # return multiple results.
16
18
  # - currently discards the "accuracy" component of the geocoding calls
17
19
  class MultiGeocoder < Geocoder
18
-
19
20
  private
20
- # This method will call one or more geocoders in the order specified in the
21
- # configuration until one of the geocoders work.
21
+
22
+ # This method will call one or more geocoders in the order specified in
23
+ # the configuration until one of the geocoders work.
22
24
  #
23
- # The failover approach is crucial for production-grade apps, but is rarely used.
25
+ # The failover approach is crucial for production-grade apps, but is
26
+ # rarely used.
24
27
  # 98% of your geocoding calls will be successful with the first call
25
28
  def self.do_geocode(address, *args)
26
29
  provider_order = provider_order_for(address, args)
@@ -31,24 +34,26 @@ module Geokit
31
34
  res = klass.send :geocode, address, *args
32
35
  return res if res.success?
33
36
  rescue => e
34
- logger.error("An error has occurred during geocoding: #{e}\nAddress: #{address}. Provider: #{provider}")
37
+ logger.error("An error has occurred during geocoding: #{e}\n" +
38
+ "Address: #{address}. Provider: #{provider}")
35
39
  end
36
40
  end
37
41
  # If we get here, we failed completely.
38
42
  GeoLoc.new
39
43
  end
40
44
 
41
- # This method will call one or more geocoders in the order specified in the
42
- # configuration until one of the geocoders work, only this time it's going
43
- # to try to reverse geocode a geographical point.
45
+ # This method will call one or more geocoders in the order specified in
46
+ # the configuration until one of the geocoders work, only this time it's
47
+ # going to try to reverse geocode a geographical point.
44
48
  def self.do_reverse_geocode(latlng)
45
- Geokit::Geocoders::provider_order.each do |provider|
49
+ Geokit::Geocoders.provider_order.each do |provider|
46
50
  klass = geocoder(provider)
47
51
  begin
48
52
  res = klass.send :reverse_geocode, latlng
49
53
  return res if res.success?
50
54
  rescue => e
51
- logger.error("An error has occurred during geocoding: #{e}\nLatlng: #{latlng}. Provider: #{provider}")
55
+ logger.error("An error has occurred during geocoding: #{e}\n" +
56
+ "Latlng: #{latlng}. Provider: #{provider}")
52
57
  end
53
58
  end
54
59
  # If we get here, we failed completely.
@@ -56,7 +61,8 @@ module Geokit
56
61
  end
57
62
 
58
63
  def self.geocoder(provider)
59
- Geokit::Geocoders.const_get "#{Geokit::Inflector::camelize(provider.to_s)}Geocoder"
64
+ class_name = "#{Geokit::Inflector.camelize(provider.to_s)}Geocoder"
65
+ Geokit::Geocoders.const_get class_name
60
66
  end
61
67
 
62
68
  def self.provider_order_for(address, args)
@@ -64,13 +70,12 @@ module Geokit
64
70
  args.last.delete(:provider_order)
65
71
  else
66
72
  if /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.match(address)
67
- Geokit::Geocoders::ip_provider_order
73
+ Geokit::Geocoders.ip_provider_order
68
74
  else
69
- Geokit::Geocoders::provider_order
75
+ Geokit::Geocoders.provider_order
70
76
  end
71
77
  end
72
78
  end
73
79
  end
74
80
  end
75
81
  end
76
-
@@ -6,16 +6,19 @@ module Geokit
6
6
  req = Net::HTTP::Get.new(url)
7
7
  req.basic_auth(uri.user, uri.password) if uri.userinfo
8
8
  net_http_args = [uri.host, uri.port]
9
- if (proxy_uri_string = Geokit::Geocoders::proxy)
9
+ if (proxy_uri_string = Geokit::Geocoders.proxy)
10
10
  proxy_uri = URI.parse(proxy_uri_string)
11
- net_http_args += [proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password]
11
+ net_http_args += [proxy_uri.host,
12
+ proxy_uri.port,
13
+ proxy_uri.user,
14
+ proxy_uri.password]
12
15
  end
13
- http = Net::HTTP::new(*net_http_args)
16
+ http = Net::HTTP.new(*net_http_args)
14
17
  if uri.scheme == 'https'
15
18
  http.use_ssl = true
16
19
  http.verify_mode = Geokit::Geocoders.ssl_verify_mode
17
20
  end
18
- http.start { |http| http.request(req) }
21
+ http.start { |h| h.request(req) }
19
22
  end
20
23
 
21
24
  def self.success?(response)
@@ -1,13 +1,12 @@
1
1
  module Geokit
2
2
  # A complex polygon made of multiple points. End point must equal start point to close the poly.
3
3
  class Polygon
4
-
5
4
  attr_accessor :points
6
-
5
+
7
6
  # Pass in an array of Geokit::LatLng
8
7
  def initialize(points)
9
8
  @points = points
10
-
9
+
11
10
  # A Polygon must be 'closed', the last point equal to the first point
12
11
  # Append the first point to the array to close the polygon
13
12
  @points << points[0] if points[0] != points[-1]
@@ -30,11 +29,44 @@ module Geokit
30
29
  oddNodes = !oddNodes
31
30
  end
32
31
  end
33
-
32
+
34
33
  last_point = p
35
34
  end
36
35
 
37
36
  oddNodes
38
37
  end # contains?
38
+
39
+ # A polygon is static and can not be updated with new points, as a result
40
+ # calculate the centroid once and store it when requested.
41
+ def centroid
42
+ @centroid ||= calculate_centroid
43
+ end # end centroid
44
+
45
+ private
46
+
47
+ def calculate_centroid
48
+ centroid_lat = 0.0
49
+ centroid_lng = 0.0
50
+ signed_area = 0.0
51
+
52
+ # Iterate over each element in the list but the last item as it's
53
+ # calculated by the i+1 logic
54
+ @points[0...-1].each_index do |i|
55
+ x0 = @points[i].lat
56
+ y0 = @points[i].lng
57
+ x1 = @points[i + 1].lat
58
+ y1 = @points[i + 1].lng
59
+ a = (x0 * y1) - (x1 * y0)
60
+ signed_area += a
61
+ centroid_lat += (x0 + x1) * a
62
+ centroid_lng += (y0 + y1) * a
63
+ end
64
+
65
+ signed_area *= 0.5
66
+ centroid_lat /= (6.0 * signed_area)
67
+ centroid_lng /= (6.0 * signed_area)
68
+
69
+ Geokit::LatLng.new(centroid_lat, centroid_lng)
70
+ end # end calculate_centroid
39
71
  end # class Polygon
40
72
  end