geokit-rails 2.0.1 → 2.3.2

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.

Potentially problematic release.


This version of geokit-rails might be problematic. Click here for more details.

@@ -0,0 +1,17 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rdoc/task'
5
+ Rake::RDocTask.new do |rdoc|
6
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
7
+
8
+ rdoc.rdoc_dir = 'rdoc'
9
+ rdoc.title = "geokit-rails #{version}"
10
+ rdoc.rdoc_files.include('README*')
11
+ rdoc.rdoc_files.include('lib/**/*.rb')
12
+ end
13
+
14
+ load 'test/tasks.rake'
15
+
16
+ desc 'Default: run unit tests.'
17
+ task :default => :test
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ group :development, :test do
4
+ gem 'activerecord', '~> 3.2.0'
5
+ end
6
+
7
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ group :development, :test do
4
+ gem 'activerecord', '~> 4.0'
5
+ gem 'test-unit'
6
+ end
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ group :development, :test do
4
+ gem 'activerecord', '~> 5.0'
5
+ gem 'test-unit'
6
+ end
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path("../lib/geokit-rails/version", __FILE__)
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = "geokit-rails"
6
+ spec.version = GeokitRails::VERSION
7
+ spec.authors = ["Michael Noack", "Andre Lewis", "Bill Eisenhauer", "Jeremy Lecour"]
8
+ spec.email = ["michael+geokit@noack.com.au", "andre@earthcode.com", "bill_eisenhauer@yahoo.com", "jeremy.lecour@gmail.com"]
9
+ spec.summary = "Integrate Geokit with Rails"
10
+ spec.description = "Official Geokit plugin for Rails/ActiveRecord. Provides location-based goodness for your Rails app. Requires the Geokit gem."
11
+ spec.summary = "Geokit helpers for rails apps."
12
+ spec.homepage = "http://github.com/geokit/geokit-rails"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency 'rails', '>= 3.0'
21
+ spec.add_dependency 'geokit', '~> 1.5'
22
+ spec.add_development_dependency "bundler", "> 1.0"
23
+ spec.add_development_dependency "simplecov", "~> 0.16.1"
24
+ spec.add_development_dependency "simplecov-rcov"
25
+ spec.add_development_dependency 'rake'
26
+ spec.add_development_dependency 'test-unit'
27
+ spec.add_development_dependency "mocha", "~> 0.9"
28
+ spec.add_development_dependency 'coveralls'
29
+ spec.add_development_dependency "mysql2", "~> 0.2"
30
+ spec.add_development_dependency "activerecord-mysql2spatial-adapter"
31
+ spec.add_development_dependency "pg", "~> 0.10"
32
+ spec.add_development_dependency "sqlite3"
33
+ end
@@ -0,0 +1,13 @@
1
+ module GeokitRails
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("../../templates", __FILE__)
5
+
6
+ desc "Creates a sample Geokit initializer."
7
+
8
+ def copy_initializer
9
+ copy_file "geokit_config.rb", "config/initializers/geokit_config.rb"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,100 @@
1
+ # These defaults are used in Geokit::Mappable.distance_to and acts_as_mappable
2
+ Geokit::default_units = :miles # others :kms, :nms, :meters
3
+ Geokit::default_formula = :sphere
4
+
5
+ # This is the timeout value in seconds to be used for calls to the geocoder web
6
+ # services. For no timeout at all, comment out the setting. The timeout unit
7
+ # is in seconds.
8
+ Geokit::Geocoders::request_timeout = 3
9
+
10
+ # This setting can be used if web service calls must be routed through a proxy.
11
+ # These setting can be nil if not needed, otherwise, a valid URI must be
12
+ # filled in at a minimum. If the proxy requires authentication, the username
13
+ # and password can be provided as well.
14
+ # Geokit::Geocoders::proxy = 'https://user:password@host:port'
15
+
16
+ # This is your yahoo application key for the Yahoo Geocoder.
17
+ # See http://developer.yahoo.com/faq/index.html#appid
18
+ # and http://developer.yahoo.com/maps/rest/V1/geocode.html
19
+ # Geokit::Geocoders::YahooGeocoder.key = 'REPLACE_WITH_YOUR_YAHOO_KEY'
20
+ # Geokit::Geocoders::YahooGeocoder.secret = 'REPLACE_WITH_YOUR_YAHOO_SECRET'
21
+
22
+ # This is your Google Maps geocoder keys (all optional).
23
+ # See http://www.google.com/apis/maps/signup.html
24
+ # and http://www.google.com/apis/maps/documentation/#Geocoding_Examples
25
+ # Geokit::Geocoders::GoogleGeocoder.client_id = ''
26
+ # Geokit::Geocoders::GoogleGeocoder.cryptographic_key = ''
27
+ # Geokit::Geocoders::GoogleGeocoder.channel = ''
28
+
29
+ # You can also use the free API key instead of signed requests
30
+ # See https://developers.google.com/maps/documentation/geocoding/#api_key
31
+ # Geokit::Geocoders::GoogleGeocoder.api_key = ''
32
+
33
+ # You can also set multiple API KEYS for different domains that may be directed
34
+ # to this same application.
35
+ # The domain from which the current user is being directed will automatically
36
+ # be updated for Geokit via
37
+ # the GeocoderControl class, which gets it's begin filter mixed
38
+ # into the ActionController.
39
+ # You define these keys with a Hash as follows:
40
+ # Geokit::Geocoders::google = {
41
+ # 'rubyonrails.org' => 'RUBY_ON_RAILS_API_KEY',
42
+ # ' ruby-docs.org' => 'RUBY_DOCS_API_KEY' }
43
+
44
+ # This is your username and password for geocoder.us.
45
+ # To use the free service, the value can be set to nil or false. For
46
+ # usage tied to an account, the value should be set to username:password.
47
+ # See http://geocoder.us
48
+ # and http://geocoder.us/user/signup
49
+ # Geokit::Geocoders::UsGeocoder.key = 'username:password'
50
+
51
+ # This is your authorization key for geocoder.ca.
52
+ # To use the free service, the value can be set to nil or false. For
53
+ # usage tied to an account, set the value to the key obtained from
54
+ # Geocoder.ca.
55
+ # See http://geocoder.ca
56
+ # and http://geocoder.ca/?register=1
57
+ # Geokit::Geocoders::CaGeocoder.key = 'KEY'
58
+
59
+ # This is your username key for geonames.
60
+ # To use this service either free or premium, you must register a key.
61
+ # See http://www.geonames.org
62
+ # Geokit::Geocoders::GeonamesGeocoder.key = 'KEY'
63
+
64
+ # Most other geocoders need either no setup or a key
65
+ # Geokit::Geocoders::BingGeocoder.key = ''
66
+ # Geokit::Geocoders::MapQuestGeocoder.key = ''
67
+ # Geokit::Geocoders::YandexGeocoder.key = ''
68
+ # Geokit::Geocoders::MapboxGeocoder.key = 'ACCESS_TOKEN'
69
+ # Geokit::Geocoders::OpencageGeocoder.key = 'some_api_key'
70
+
71
+ # Geonames has a free service and a premium service, each using a different URL
72
+ # GeonamesGeocoder.premium = true will use http://ws.geonames.net (premium)
73
+ # GeonamesGeocoder.premium = false will use http://api.geonames.org (free)
74
+ # Geokit::Geocoders::GeonamesGeocoder.premium = false
75
+
76
+ # require "external_geocoder.rb"
77
+ # Please see the section "writing your own geocoders" for more information.
78
+ # Geokit::Geocoders::external_key = 'REPLACE_WITH_YOUR_API_KEY'
79
+
80
+ # This is the order in which the geocoders are called in a failover scenario
81
+ # If you only want to use a single geocoder, put a single symbol in the array.
82
+ # Valid symbols are :google, :yahoo, :us, and :ca.
83
+ # Be aware that there are Terms of Use restrictions on how you can use the
84
+ # various geocoders. Make sure you read up on relevant Terms of Use for each
85
+ # geocoder you are going to use.
86
+ # Geokit::Geocoders::provider_order = [:google,:us]
87
+
88
+ # The IP provider order. Valid symbols are :ip,:geo_plugin.
89
+ # As before, make sure you read up on relevant Terms of Use for each.
90
+ # Geokit::Geocoders::ip_provider_order = [:external,:geo_plugin,:ip]
91
+
92
+ # Disable HTTPS globally. This option can also be set on individual
93
+ # geocoder classes.
94
+ # Geokit::Geocoders::secure = false
95
+
96
+ # Control verification of the server certificate for geocoders using HTTPS
97
+ # Geokit::Geocoders::ssl_verify_mode = OpenSSL::SSL::VERIFY_(PEER/NONE)
98
+ # Setting this to VERIFY_NONE may be needed on systems that don't have
99
+ # a complete or up to date root certificate store. Only applies to
100
+ # the Net::HTTP adapter.
@@ -3,7 +3,6 @@ require 'geokit'
3
3
  require 'geokit-rails/railtie'
4
4
  require 'geokit-rails/core_extensions'
5
5
 
6
- require 'geokit-rails/defaults'
7
6
  require 'geokit-rails/adapters/abstract'
8
7
  require 'geokit-rails/acts_as_mappable'
9
8
  require 'geokit-rails/geocoder_control'
@@ -11,6 +11,8 @@ module Geokit
11
11
  extend ActiveSupport::Concern
12
12
 
13
13
  module ClassMethods # :nodoc:
14
+ OPTION_SYMBOLS = [ :distance_column_name, :default_units, :default_formula, :lat_column_name, :lng_column_name, :qualified_lat_column_name, :qualified_lng_column_name, :skip_loading ]
15
+
14
16
  def acts_as_mappable(options = {})
15
17
  metaclass = (class << self; self; end)
16
18
 
@@ -21,20 +23,21 @@ module Geokit
21
23
 
22
24
  if reflection = Geokit::ActsAsMappable.end_of_reflection_chain(self.through, self)
23
25
  metaclass.instance_eval do
24
- [ :distance_column_name, :default_units, :default_formula, :lat_column_name, :lng_column_name, :qualified_lat_column_name, :qualified_lng_column_name ].each do |method_name|
26
+ OPTION_SYMBOLS.each do |method_name|
25
27
  define_method method_name do
26
28
  reflection.klass.send(method_name)
27
29
  end
28
30
  end
29
31
  end
30
32
  else
31
- cattr_accessor :distance_column_name, :default_units, :default_formula, :lat_column_name, :lng_column_name, :qualified_lat_column_name, :qualified_lng_column_name
33
+ cattr_accessor *OPTION_SYMBOLS
32
34
 
33
35
  self.distance_column_name = options[:distance_column_name] || 'distance'
34
36
  self.default_units = options[:default_units] || Geokit::default_units
35
37
  self.default_formula = options[:default_formula] || Geokit::default_formula
36
38
  self.lat_column_name = options[:lat_column_name] || 'lat'
37
39
  self.lng_column_name = options[:lng_column_name] || 'lng'
40
+ self.skip_loading = options[:skip_loading]
38
41
  self.qualified_lat_column_name = "#{table_name}.#{lat_column_name}"
39
42
  self.qualified_lng_column_name = "#{table_name}.#{lng_column_name}"
40
43
 
@@ -91,11 +94,21 @@ module Geokit
91
94
  module ClassMethods
92
95
 
93
96
  # A proxy to an instance of a finder adapter, inferred from the connection's adapter.
94
- def adapter
95
- @adapter ||= begin
96
- require File.join(File.dirname(__FILE__), 'adapters', connection.adapter_name.downcase)
97
+ def geokit_finder_adapter
98
+ @geokit_finder_adapter ||= begin
99
+ unless Adapters.const_defined?(connection.adapter_name.camelcase)
100
+ filename = connection.adapter_name.downcase
101
+ require File.join("geokit-rails", "adapters", filename)
102
+ end
97
103
  klass = Adapters.const_get(connection.adapter_name.camelcase)
98
- klass.load(self) unless klass.loaded
104
+ if klass.class == Module
105
+ # For some reason Mysql2 adapter was defined in Adapters.constants but was Module instead of a Class
106
+ filename = connection.adapter_name.downcase
107
+ require File.join("geokit-rails", "adapters", filename)
108
+ # Re-init the klass after require
109
+ klass = Adapters.const_get(connection.adapter_name.camelcase)
110
+ end
111
+ klass.load(self) unless klass.loaded || skip_loading
99
112
  klass.new(self)
100
113
  rescue LoadError
101
114
  raise UnsupportedAdapter, "`#{connection.adapter_name.downcase}` is not a supported adapter."
@@ -104,8 +117,13 @@ module Geokit
104
117
 
105
118
  def within(distance, options = {})
106
119
  options[:within] = distance
107
- #geo_scope(options)
108
- where(distance_conditions(options))
120
+ # Add bounding box to speed up SQL request.
121
+ bounds = formulate_bounds_from_distance(
122
+ options,
123
+ normalize_point_to_lat_lng(options[:origin]),
124
+ options[:units] || default_units)
125
+ with_latlng.where(bound_conditions(bounds)).
126
+ where(distance_conditions(options))
109
127
  end
110
128
  alias inside within
111
129
 
@@ -123,30 +141,35 @@ module Geokit
123
141
  end
124
142
 
125
143
  def in_bounds(bounds, options = {})
144
+ inclusive = options.delete(:inclusive) || false
126
145
  options[:bounds] = bounds
127
146
  #geo_scope(options)
128
147
  #where(distance_conditions(options))
129
148
  bounds = extract_bounds_from_options(options)
130
- where(bound_conditions(bounds))
149
+ where(bound_conditions(bounds, inclusive))
131
150
  end
132
151
 
133
152
  def by_distance(options = {})
134
153
  origin = extract_origin_from_options(options)
135
154
  units = extract_units_from_options(options)
136
155
  formula = extract_formula_from_options(options)
137
- bounds = extract_bounds_from_options(options)
138
156
  distance_column_name = distance_sql(origin, units, formula)
139
- #geo_scope(options).order("#{distance_column_name} asc")
140
- order("#{distance_column_name} asc")
157
+ with_latlng.order(
158
+ Arel.sql(distance_column_name).send(options[:reverse] ? 'desc' : 'asc')
159
+ )
160
+ end
161
+
162
+ def with_latlng
163
+ where("#{qualified_lat_column_name} IS NOT NULL AND #{qualified_lng_column_name} IS NOT NULL")
141
164
  end
142
165
 
143
166
  def closest(options = {})
144
- by_distance(options).first(1)
167
+ by_distance(options).limit(1)
145
168
  end
146
169
  alias nearest closest
147
170
 
148
171
  def farthest(options = {})
149
- by_distance(options).last(1)
172
+ by_distance({:reverse => true}.merge(options)).limit(1)
150
173
  end
151
174
 
152
175
  #def geo_scope(options = {})
@@ -162,9 +185,9 @@ module Geokit
162
185
 
163
186
  # if origin
164
187
  # arel.distance_formula = distance_sql(origin, units, formula)
165
- #
188
+ #
166
189
  # if arel.select_values.blank?
167
- # star_select = Arel::SqlLiteral.new(arel.quoted_table_name + '.*')
190
+ # star_select = Arel::Nodes::SqlLiteral.new(arel.quoted_table_name + '.*')
168
191
  # arel = arel.select(star_select)
169
192
  # end
170
193
  # end
@@ -211,10 +234,10 @@ module Geokit
211
234
  # If it's a :within query, add a bounding box to improve performance.
212
235
  # This only gets called if a :bounds argument is not otherwise supplied.
213
236
  def formulate_bounds_from_distance(options, origin, units)
214
- distance = options[:within] if options.has_key?(:within)
237
+ distance = options[:within] if options.has_key?(:within) && options[:within].is_a?(Numeric)
215
238
  distance = options[:range].last-(options[:range].exclude_end?? 1 : 0) if options.has_key?(:range)
216
239
  if distance
217
- res=Geokit::Bounds.from_point_and_radius(origin,distance,:units=>units)
240
+ Geokit::Bounds.from_point_and_radius(origin,distance,:units=>units)
218
241
  else
219
242
  nil
220
243
  end
@@ -224,25 +247,41 @@ module Geokit
224
247
  origin = extract_origin_from_options(options)
225
248
  units = extract_units_from_options(options)
226
249
  formula = extract_formula_from_options(options)
227
- bounds = extract_bounds_from_options(options)
228
250
  distance_column_name = distance_sql(origin, units, formula)
229
251
 
230
- res = if options.has_key?(:within)
231
- "#{distance_column_name} <= #{options[:within]}"
252
+ if options.has_key?(:within)
253
+ Arel.sql(distance_column_name).lteq(options[:within])
232
254
  elsif options.has_key?(:beyond)
233
- "#{distance_column_name} > #{options[:beyond]}"
255
+ Arel.sql(distance_column_name).gt(options[:beyond])
234
256
  elsif options.has_key?(:range)
235
- "#{distance_column_name} >= #{options[:range].first} AND #{distance_column_name} <#{'=' unless options[:range].exclude_end?} #{options[:range].last}"
257
+ min_condition = Arel.sql(distance_column_name).gteq(options[:range].begin)
258
+ max_condition = if options[:range].exclude_end?
259
+ Arel.sql(distance_column_name).lt(options[:range].end)
260
+ else
261
+ Arel.sql(distance_column_name).lteq(options[:range].end)
262
+ end
263
+ min_condition.and(max_condition)
236
264
  end
237
- Arel::SqlLiteral.new("(#{res})") if res.present?
238
265
  end
239
266
 
240
- def bound_conditions(bounds)
267
+ def bound_conditions(bounds, inclusive = false)
268
+ return nil unless bounds
269
+ if inclusive
270
+ lt_operator = :lteq
271
+ gt_operator = :gteq
272
+ else
273
+ lt_operator = :lt
274
+ gt_operator = :gt
275
+ end
241
276
  sw,ne = bounds.sw, bounds.ne
242
- lng_sql = bounds.crosses_meridian? ? "(#{qualified_lng_column_name}<#{ne.lng} OR #{qualified_lng_column_name}>#{sw.lng})" : "#{qualified_lng_column_name}>#{sw.lng} AND #{qualified_lng_column_name}<#{ne.lng}"
243
- res = "#{qualified_lat_column_name}>#{sw.lat} AND #{qualified_lat_column_name}<#{ne.lat} AND #{lng_sql}"
244
- #Arel::SqlLiteral.new("(#{res})") if res.present?
245
- res if res.present?
277
+ lat, lng = Arel.sql(qualified_lat_column_name), Arel.sql(qualified_lng_column_name)
278
+ lat.send(gt_operator, sw.lat).and(lat.send(lt_operator, ne.lat)).and(
279
+ if bounds.crosses_meridian?
280
+ lng.send(lt_operator, ne.lng).or(lng.send(gt_operator, sw.lng))
281
+ else
282
+ lng.send(gt_operator, sw.lng).and(lng.send(lt_operator, ne.lng))
283
+ end
284
+ )
246
285
  end
247
286
 
248
287
  # Extracts the origin instance out of the options if it exists and returns
@@ -315,20 +354,31 @@ module Geokit
315
354
  # Returns the distance SQL using the spherical world formula (Haversine). The SQL is tuned
316
355
  # to the database in use.
317
356
  def sphere_distance_sql(origin, units)
318
- lat = deg2rad(origin.lat)
319
- lng = deg2rad(origin.lng)
357
+ # "origin" can be a Geokit::LatLng (with :lat and :lng methods), e.g.
358
+ # when using geo_scope or it can be an ActsAsMappable with customized
359
+ # latitude and longitude methods, e.g. when using distance_sql.
360
+ lat = deg2rad(get_lat(origin))
361
+ lng = deg2rad(get_lng(origin))
320
362
  multiplier = units_sphere_multiplier(units)
321
-
322
- adapter.sphere_distance_sql(lat, lng, multiplier) if adapter
363
+ geokit_finder_adapter.sphere_distance_sql(lat, lng, multiplier) if geokit_finder_adapter
323
364
  end
324
365
 
325
366
  # Returns the distance SQL using the flat-world formula (Phythagorean Theory). The SQL is tuned
326
367
  # to the database in use.
327
368
  def flat_distance_sql(origin, units)
328
369
  lat_degree_units = units_per_latitude_degree(units)
329
- lng_degree_units = units_per_longitude_degree(origin.lat, units)
370
+ lng_degree_units = units_per_longitude_degree(get_lat(origin), units)
371
+ geokit_finder_adapter.flat_distance_sql(origin, lat_degree_units, lng_degree_units)
372
+ end
373
+
374
+ def get_lat(origin)
375
+ origin.respond_to?(:lat) ? origin.lat \
376
+ : origin.send(:"#{lat_column_name}")
377
+ end
330
378
 
331
- adapter.flat_distance_sql(origin, lat_degree_units, lng_degree_units)
379
+ def get_lng(origin)
380
+ origin.respond_to?(:lng) ? origin.lng \
381
+ : origin.send(:"#{lng_column_name}")
332
382
  end
333
383
 
334
384
  end # ClassMethods
@@ -339,8 +389,8 @@ module Geokit
339
389
  geo=Geokit::Geocoders::MultiGeocoder.geocode(address)
340
390
 
341
391
  if geo.success
342
- self.send("#{lat_column_name}=", geo.lat)
343
- self.send("#{lng_column_name}=", geo.lng)
392
+ self.send("#{lat_column_name}=", geo.send(:"#{lat_column_name}"))
393
+ self.send("#{lng_column_name}=", geo.send(:"#{lng_column_name}"))
344
394
  else
345
395
  errors.add(auto_geocode_field, auto_geocode_error_message)
346
396
  end
@@ -0,0 +1,32 @@
1
+ module Geokit
2
+ module Adapters
3
+ class OracleEnhanced < Abstract
4
+ TO_DEGREES = Math::PI / 180
5
+ def sphere_distance_sql(lat, lng, multiplier)
6
+ %{
7
+ (
8
+ ACOS(
9
+ COS(#{lat}) * COS(#{lng}) *
10
+ COS(#{TO_DEGREES} * #{qualified_lat_column_name}) *
11
+ COS(#{TO_DEGREES} * #{qualified_lng_column_name}) +
12
+ COS(#{lat}) * SIN(#{lng}) *
13
+ COS(#{TO_DEGREES} * #{qualified_lat_column_name}) *
14
+ SIN(#{TO_DEGREES} * #{qualified_lng_column_name}) +
15
+ SIN(#{lat}) *
16
+ SIN(#{TO_DEGREES} * #{qualified_lat_column_name})
17
+ ) *
18
+ #{multiplier})
19
+ }
20
+ end
21
+
22
+ def flat_distance_sql(origin, lat_degree_units, lng_degree_units)
23
+ %{
24
+ SQRT(
25
+ POWER(#{lat_degree_units}*(#{origin.lat}-#{qualified_lat_column_name}), 2)
26
+ POWER(#{lng_degree_units}*(#{origin.lng}-#{qualified_lng_column_name}), 2)
27
+ )
28
+ }
29
+ end
30
+ end
31
+ end
32
+ end