geo-calculator 0.0.1
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 +7 -0
- data/.gitignore +18 -0
- data/.rbenv-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +48 -0
- data/Rakefile +3 -0
- data/geo-calculator.gemspec +27 -0
- data/lib/geo-calculator.rb +6 -0
- data/lib/geo-calculator/calculations.rb +428 -0
- data/lib/geo-calculator/configuration.rb +124 -0
- data/lib/geo-calculator/configuration_hash.rb +11 -0
- data/lib/geo-calculator/hash_recursive_merge.rb +74 -0
- data/lib/geo-calculator/sql.rb +107 -0
- data/lib/geo-calculator/store/active_record.rb +290 -0
- data/lib/geo-calculator/store/base.rb +126 -0
- data/lib/geo-calculator/version.rb +3 -0
- data/spec/includes_spec.rb +21 -0
- data/spec/spec_helper.rb +6 -0
- metadata +149 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
require 'singleton'
|
|
2
|
+
require 'geo-calculator/configuration_hash'
|
|
3
|
+
|
|
4
|
+
module Geocoder
|
|
5
|
+
|
|
6
|
+
##
|
|
7
|
+
# Configuration options should be set by passing a hash:
|
|
8
|
+
#
|
|
9
|
+
# Geocoder.configure(
|
|
10
|
+
# :timeout => 5,
|
|
11
|
+
# :lookup => :yandex,
|
|
12
|
+
# :api_key => "2a9fsa983jaslfj982fjasd",
|
|
13
|
+
# :units => :km
|
|
14
|
+
# )
|
|
15
|
+
#
|
|
16
|
+
def self.configure(options = nil, &block)
|
|
17
|
+
if !options.nil?
|
|
18
|
+
Configuration.instance.configure(options)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
##
|
|
23
|
+
# Read-only access to the singleton's config data.
|
|
24
|
+
#
|
|
25
|
+
def self.config
|
|
26
|
+
Configuration.instance.data
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
##
|
|
30
|
+
# Read-only access to lookup-specific config data.
|
|
31
|
+
#
|
|
32
|
+
def self.config_for_lookup(lookup_name)
|
|
33
|
+
data = config.clone
|
|
34
|
+
data.reject!{ |key,value| !Configuration::OPTIONS.include?(key) }
|
|
35
|
+
if config.has_key?(lookup_name)
|
|
36
|
+
data.merge!(config[lookup_name])
|
|
37
|
+
end
|
|
38
|
+
data
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class Configuration
|
|
42
|
+
include Singleton
|
|
43
|
+
|
|
44
|
+
OPTIONS = [
|
|
45
|
+
:timeout,
|
|
46
|
+
:lookup,
|
|
47
|
+
:ip_lookup,
|
|
48
|
+
:language,
|
|
49
|
+
:http_headers,
|
|
50
|
+
:use_https,
|
|
51
|
+
:http_proxy,
|
|
52
|
+
:https_proxy,
|
|
53
|
+
:api_key,
|
|
54
|
+
:cache,
|
|
55
|
+
:cache_prefix,
|
|
56
|
+
:always_raise,
|
|
57
|
+
:units,
|
|
58
|
+
:distances
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
attr_accessor :data
|
|
62
|
+
|
|
63
|
+
def self.set_defaults
|
|
64
|
+
instance.set_defaults
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
OPTIONS.each do |o|
|
|
68
|
+
define_method o do
|
|
69
|
+
@data[o]
|
|
70
|
+
end
|
|
71
|
+
define_method "#{o}=" do |value|
|
|
72
|
+
@data[o] = value
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def configure(options)
|
|
77
|
+
@data.rmerge!(options)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def initialize # :nodoc
|
|
81
|
+
@data = Geocoder::ConfigurationHash.new
|
|
82
|
+
set_defaults
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def set_defaults
|
|
86
|
+
|
|
87
|
+
# geocoding options
|
|
88
|
+
@data[:timeout] = 3 # geocoding service timeout (secs)
|
|
89
|
+
@data[:lookup] = :google # name of street address geocoding service (symbol)
|
|
90
|
+
@data[:ip_lookup] = :freegeoip # name of IP address geocoding service (symbol)
|
|
91
|
+
@data[:language] = :en # ISO-639 language code
|
|
92
|
+
@data[:http_headers] = {} # HTTP headers for lookup
|
|
93
|
+
@data[:use_https] = false # use HTTPS for lookup requests? (if supported)
|
|
94
|
+
@data[:http_proxy] = nil # HTTP proxy server (user:pass@host:port)
|
|
95
|
+
@data[:https_proxy] = nil # HTTPS proxy server (user:pass@host:port)
|
|
96
|
+
@data[:api_key] = nil # API key for geocoding service
|
|
97
|
+
@data[:cache] = nil # cache object (must respond to #[], #[]=, and #keys)
|
|
98
|
+
@data[:cache_prefix] = "geocoder:" # prefix (string) to use for all cache keys
|
|
99
|
+
|
|
100
|
+
# exceptions that should not be rescued by default
|
|
101
|
+
# (if you want to implement custom error handling);
|
|
102
|
+
# supports SocketError and TimeoutError
|
|
103
|
+
@data[:always_raise] = []
|
|
104
|
+
|
|
105
|
+
# calculation options
|
|
106
|
+
@data[:units] = :mi # :mi or :km
|
|
107
|
+
@data[:distances] = :linear # :linear or :spherical
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
instance_eval(OPTIONS.map do |option|
|
|
111
|
+
o = option.to_s
|
|
112
|
+
<<-EOS
|
|
113
|
+
def #{o}
|
|
114
|
+
instance.data[:#{o}]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def #{o}=(value)
|
|
118
|
+
instance.data[:#{o}] = value
|
|
119
|
+
end
|
|
120
|
+
EOS
|
|
121
|
+
end.join("\n\n"))
|
|
122
|
+
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#
|
|
2
|
+
# = Hash Recursive Merge
|
|
3
|
+
#
|
|
4
|
+
# Merges a Ruby Hash recursively, Also known as deep merge.
|
|
5
|
+
# Recursive version of Hash#merge and Hash#merge!.
|
|
6
|
+
#
|
|
7
|
+
# Category:: Ruby
|
|
8
|
+
# Package:: Hash
|
|
9
|
+
# Author:: Simone Carletti <weppos@weppos.net>
|
|
10
|
+
# Copyright:: 2007-2008 The Authors
|
|
11
|
+
# License:: MIT License
|
|
12
|
+
# Link:: http://www.simonecarletti.com/
|
|
13
|
+
# Source:: http://gist.github.com/gists/6391/
|
|
14
|
+
#
|
|
15
|
+
module HashRecursiveMerge
|
|
16
|
+
|
|
17
|
+
#
|
|
18
|
+
# Recursive version of Hash#merge!
|
|
19
|
+
#
|
|
20
|
+
# Adds the contents of +other_hash+ to +hsh+,
|
|
21
|
+
# merging entries in +hsh+ with duplicate keys with those from +other_hash+.
|
|
22
|
+
#
|
|
23
|
+
# Compared with Hash#merge!, this method supports nested hashes.
|
|
24
|
+
# When both +hsh+ and +other_hash+ contains an entry with the same key,
|
|
25
|
+
# it merges and returns the values from both arrays.
|
|
26
|
+
#
|
|
27
|
+
# h1 = {"a" => 100, "b" => 200, "c" => {"c1" => 12, "c2" => 14}}
|
|
28
|
+
# h2 = {"b" => 254, "c" => {"c1" => 16, "c3" => 94}}
|
|
29
|
+
# h1.rmerge!(h2) #=> {"a" => 100, "b" => 254, "c" => {"c1" => 16, "c2" => 14, "c3" => 94}}
|
|
30
|
+
#
|
|
31
|
+
# Simply using Hash#merge! would return
|
|
32
|
+
#
|
|
33
|
+
# h1.merge!(h2) #=> {"a" => 100, "b" = >254, "c" => {"c1" => 16, "c3" => 94}}
|
|
34
|
+
#
|
|
35
|
+
def rmerge!(other_hash)
|
|
36
|
+
merge!(other_hash) do |key, oldval, newval|
|
|
37
|
+
oldval.class == self.class ? oldval.rmerge!(newval) : newval
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
#
|
|
42
|
+
# Recursive version of Hash#merge
|
|
43
|
+
#
|
|
44
|
+
# Compared with Hash#merge!, this method supports nested hashes.
|
|
45
|
+
# When both +hsh+ and +other_hash+ contains an entry with the same key,
|
|
46
|
+
# it merges and returns the values from both arrays.
|
|
47
|
+
#
|
|
48
|
+
# Compared with Hash#merge, this method provides a different approch
|
|
49
|
+
# for merging nasted hashes.
|
|
50
|
+
# If the value of a given key is an Hash and both +other_hash+ abd +hsh
|
|
51
|
+
# includes the same key, the value is merged instead replaced with
|
|
52
|
+
# +other_hash+ value.
|
|
53
|
+
#
|
|
54
|
+
# h1 = {"a" => 100, "b" => 200, "c" => {"c1" => 12, "c2" => 14}}
|
|
55
|
+
# h2 = {"b" => 254, "c" => {"c1" => 16, "c3" => 94}}
|
|
56
|
+
# h1.rmerge(h2) #=> {"a" => 100, "b" => 254, "c" => {"c1" => 16, "c2" => 14, "c3" => 94}}
|
|
57
|
+
#
|
|
58
|
+
# Simply using Hash#merge would return
|
|
59
|
+
#
|
|
60
|
+
# h1.merge(h2) #=> {"a" => 100, "b" = >254, "c" => {"c1" => 16, "c3" => 94}}
|
|
61
|
+
#
|
|
62
|
+
def rmerge(other_hash)
|
|
63
|
+
r = {}
|
|
64
|
+
merge(other_hash) do |key, oldval, newval|
|
|
65
|
+
r[key] = oldval.class == self.class ? oldval.rmerge(newval) : newval
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Hash
|
|
73
|
+
include HashRecursiveMerge
|
|
74
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
module Geocoder
|
|
2
|
+
module Sql
|
|
3
|
+
extend self
|
|
4
|
+
|
|
5
|
+
##
|
|
6
|
+
# Distance calculation for use with a database that supports POWER(),
|
|
7
|
+
# SQRT(), PI(), and trigonometric functions SIN(), COS(), ASIN(),
|
|
8
|
+
# ATAN2(), DEGREES(), and RADIANS().
|
|
9
|
+
#
|
|
10
|
+
# Based on the excellent tutorial at:
|
|
11
|
+
# http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
|
|
12
|
+
#
|
|
13
|
+
def full_distance(latitude, longitude, lat_attr, lon_attr, options = {})
|
|
14
|
+
units = options[:units] || Geocoder.config.units
|
|
15
|
+
earth = Geocoder::Calculations.earth_radius(units)
|
|
16
|
+
|
|
17
|
+
"#{earth} * 2 * ASIN(SQRT(" +
|
|
18
|
+
"POWER(SIN((#{latitude.to_f} - #{lat_attr}) * PI() / 180 / 2), 2) + " +
|
|
19
|
+
"COS(#{latitude.to_f} * PI() / 180) * COS(#{lat_attr} * PI() / 180) * " +
|
|
20
|
+
"POWER(SIN((#{longitude.to_f} - #{lon_attr}) * PI() / 180 / 2), 2)" +
|
|
21
|
+
"))"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# Distance calculation for use with a database without trigonometric
|
|
26
|
+
# functions, like SQLite. Approach is to find objects within a square
|
|
27
|
+
# rather than a circle, so results are very approximate (will include
|
|
28
|
+
# objects outside the given radius).
|
|
29
|
+
#
|
|
30
|
+
# Distance and bearing calculations are *extremely inaccurate*. To be
|
|
31
|
+
# clear: this only exists to provide interface consistency. Results
|
|
32
|
+
# are not intended for use in production!
|
|
33
|
+
#
|
|
34
|
+
def approx_distance(latitude, longitude, lat_attr, lon_attr, options = {})
|
|
35
|
+
units = options[:units] || Geocoder.config.units
|
|
36
|
+
dx = Geocoder::Calculations.longitude_degree_distance(30, units)
|
|
37
|
+
dy = Geocoder::Calculations.latitude_degree_distance(units)
|
|
38
|
+
|
|
39
|
+
# sin of 45 degrees = average x or y component of vector
|
|
40
|
+
factor = Math.sin(Math::PI / 4)
|
|
41
|
+
|
|
42
|
+
"(#{dy} * ABS(#{lat_attr} - #{latitude.to_f}) * #{factor}) + " +
|
|
43
|
+
"(#{dx} * ABS(#{lon_attr} - #{longitude.to_f}) * #{factor})"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def within_bounding_box(sw_lat, sw_lng, ne_lat, ne_lng, lat_attr, lon_attr)
|
|
47
|
+
spans = "#{lat_attr} BETWEEN #{sw_lat} AND #{ne_lat} AND "
|
|
48
|
+
# handle box that spans 180 longitude
|
|
49
|
+
if sw_lng.to_f > ne_lng.to_f
|
|
50
|
+
spans + "#{lon_attr} BETWEEN #{sw_lng} AND 180 OR " +
|
|
51
|
+
"#{lon_attr} BETWEEN -180 AND #{ne_lng}"
|
|
52
|
+
else
|
|
53
|
+
spans + "#{lon_attr} BETWEEN #{sw_lng} AND #{ne_lng}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Fairly accurate bearing calculation. Takes a latitude, longitude,
|
|
59
|
+
# and an options hash which must include a :bearing value
|
|
60
|
+
# (:linear or :spherical).
|
|
61
|
+
#
|
|
62
|
+
# Based on:
|
|
63
|
+
# http://www.beginningspatial.com/calculating_bearing_one_point_another
|
|
64
|
+
#
|
|
65
|
+
def full_bearing(latitude, longitude, lat_attr, lon_attr, options = {})
|
|
66
|
+
degrees_per_radian = Geocoder::Calculations::DEGREES_PER_RADIAN
|
|
67
|
+
case options[:bearing] || Geocoder.config.distances
|
|
68
|
+
when :linear
|
|
69
|
+
"MOD(CAST(" +
|
|
70
|
+
"(ATAN2( " +
|
|
71
|
+
"((#{lon_attr} - #{longitude.to_f}) / #{degrees_per_radian}), " +
|
|
72
|
+
"((#{lat_attr} - #{latitude.to_f}) / #{degrees_per_radian})" +
|
|
73
|
+
") * #{degrees_per_radian}) + 360 " +
|
|
74
|
+
"AS decimal), 360)"
|
|
75
|
+
when :spherical
|
|
76
|
+
"MOD(CAST(" +
|
|
77
|
+
"(ATAN2( " +
|
|
78
|
+
"SIN( (#{lon_attr} - #{longitude.to_f}) / #{degrees_per_radian} ) * " +
|
|
79
|
+
"COS( (#{lat_attr}) / #{degrees_per_radian} ), (" +
|
|
80
|
+
"COS( (#{latitude.to_f}) / #{degrees_per_radian} ) * SIN( (#{lat_attr}) / #{degrees_per_radian})" +
|
|
81
|
+
") - (" +
|
|
82
|
+
"SIN( (#{latitude.to_f}) / #{degrees_per_radian}) * COS((#{lat_attr}) / #{degrees_per_radian}) * " +
|
|
83
|
+
"COS( (#{lon_attr} - #{longitude.to_f}) / #{degrees_per_radian})" +
|
|
84
|
+
")" +
|
|
85
|
+
") * #{degrees_per_radian}) + 360 " +
|
|
86
|
+
"AS decimal), 360)"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
##
|
|
91
|
+
# Totally lame bearing calculation. Basically useless except that it
|
|
92
|
+
# returns *something* in databases without trig functions.
|
|
93
|
+
#
|
|
94
|
+
def approx_bearing(latitude, longitude, lat_attr, lon_attr, options = {})
|
|
95
|
+
"CASE " +
|
|
96
|
+
"WHEN (#{lat_attr} >= #{latitude.to_f} AND " +
|
|
97
|
+
"#{lon_attr} >= #{longitude.to_f}) THEN 45.0 " +
|
|
98
|
+
"WHEN (#{lat_attr} < #{latitude.to_f} AND " +
|
|
99
|
+
"#{lon_attr} >= #{longitude.to_f}) THEN 135.0 " +
|
|
100
|
+
"WHEN (#{lat_attr} < #{latitude.to_f} AND " +
|
|
101
|
+
"#{lon_attr} < #{longitude.to_f}) THEN 225.0 " +
|
|
102
|
+
"WHEN (#{lat_attr} >= #{latitude.to_f} AND " +
|
|
103
|
+
"#{lon_attr} < #{longitude.to_f}) THEN 315.0 " +
|
|
104
|
+
"END"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
require 'geo-calculator/sql'
|
|
3
|
+
require 'geo-calculator/store/base'
|
|
4
|
+
require 'byebug'
|
|
5
|
+
|
|
6
|
+
##
|
|
7
|
+
# Add geocoding functionality to any ActiveRecord object.
|
|
8
|
+
#
|
|
9
|
+
module Geocoder::Store
|
|
10
|
+
module ActiveRecord
|
|
11
|
+
include Base
|
|
12
|
+
|
|
13
|
+
##
|
|
14
|
+
# Implementation of 'included' hook method.
|
|
15
|
+
#
|
|
16
|
+
def self.included(base)
|
|
17
|
+
base.extend ClassMethods
|
|
18
|
+
base.class_eval do
|
|
19
|
+
|
|
20
|
+
# scope: geocoded objects
|
|
21
|
+
scope :geocoded, lambda {
|
|
22
|
+
where("#{table_name}.#{geocoder_options[:latitude]} IS NOT NULL " +
|
|
23
|
+
"AND #{table_name}.#{geocoder_options[:longitude]} IS NOT NULL")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# scope: not-geocoded objects
|
|
27
|
+
scope :not_geocoded, lambda {
|
|
28
|
+
where("#{table_name}.#{geocoder_options[:latitude]} IS NULL " +
|
|
29
|
+
"OR #{table_name}.#{geocoder_options[:longitude]} IS NULL")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# Find all objects within a radius of the given location.
|
|
34
|
+
# Location may be either a string to geocode or an array of
|
|
35
|
+
# coordinates (<tt>[lat,lon]</tt>). Also takes an options hash
|
|
36
|
+
# (see Geocoder::Store::ActiveRecord::ClassMethods.near_scope_options
|
|
37
|
+
# for details).
|
|
38
|
+
#
|
|
39
|
+
scope :near, lambda{ |location, *args|
|
|
40
|
+
latitude, longitude = Geocoder::Calculations.extract_coordinates(location)
|
|
41
|
+
if Geocoder::Calculations.coordinates_present?(latitude, longitude)
|
|
42
|
+
options = near_scope_options(latitude, longitude, *args)
|
|
43
|
+
select(options[:select]).where(options[:conditions]).
|
|
44
|
+
order(options[:order])
|
|
45
|
+
else
|
|
46
|
+
# If no lat/lon given we don't want any results, but we still
|
|
47
|
+
# need distance and bearing columns so you can add, for example:
|
|
48
|
+
# .order("distance")
|
|
49
|
+
select(select_clause(nil, null_value, null_value)).where(false_condition)
|
|
50
|
+
end
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# Find all objects within the area of a given bounding box.
|
|
55
|
+
# Bounds must be an array of locations specifying the southwest
|
|
56
|
+
# corner followed by the northeast corner of the box
|
|
57
|
+
# (<tt>[[sw_lat, sw_lon], [ne_lat, ne_lon]]</tt>).
|
|
58
|
+
#
|
|
59
|
+
scope :within_bounding_box, lambda{ |bounds|
|
|
60
|
+
sw_lat, sw_lng, ne_lat, ne_lng = bounds.flatten if bounds
|
|
61
|
+
if sw_lat && sw_lng && ne_lat && ne_lng
|
|
62
|
+
where(Geocoder::Sql.within_bounding_box(
|
|
63
|
+
sw_lat, sw_lng, ne_lat, ne_lng,
|
|
64
|
+
full_column_name(geocoder_options[:latitude]),
|
|
65
|
+
full_column_name(geocoder_options[:longitude])
|
|
66
|
+
))
|
|
67
|
+
else
|
|
68
|
+
select(select_clause(nil, null_value, null_value)).where(false_condition)
|
|
69
|
+
end
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
##
|
|
75
|
+
# Methods which will be class methods of the including class.
|
|
76
|
+
#
|
|
77
|
+
module ClassMethods
|
|
78
|
+
|
|
79
|
+
def distance_from_sql(location, *args)
|
|
80
|
+
latitude, longitude = Geocoder::Calculations.extract_coordinates(location)
|
|
81
|
+
if Geocoder::Calculations.coordinates_present?(latitude, longitude)
|
|
82
|
+
distance_sql(latitude, longitude, *args)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private # ----------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
##
|
|
89
|
+
# Get options hash suitable for passing to ActiveRecord.find to get
|
|
90
|
+
# records within a radius (in kilometers) of the given point.
|
|
91
|
+
# Options hash may include:
|
|
92
|
+
#
|
|
93
|
+
# * +:units+ - <tt>:mi</tt> or <tt>:km</tt>; to be used.
|
|
94
|
+
# for interpreting radius as well as the +distance+ attribute which
|
|
95
|
+
# is added to each found nearby object.
|
|
96
|
+
# Use Geocoder.configure[:units] to configure default units.
|
|
97
|
+
# * +:bearing+ - <tt>:linear</tt> or <tt>:spherical</tt>.
|
|
98
|
+
# the method to be used for calculating the bearing (direction)
|
|
99
|
+
# between the given point and each found nearby point;
|
|
100
|
+
# set to false for no bearing calculation. Use
|
|
101
|
+
# Geocoder.configure[:distances] to configure default calculation method.
|
|
102
|
+
# * +:select+ - string with the SELECT SQL fragment (e.g. “id, name”)
|
|
103
|
+
# * +:select_distance+ - whether to include the distance alias in the
|
|
104
|
+
# SELECT SQL fragment (e.g. <formula> AS distance)
|
|
105
|
+
# * +:select_bearing+ - like +:select_distance+ but for bearing.
|
|
106
|
+
# * +:order+ - column(s) for ORDER BY SQL clause; default is distance;
|
|
107
|
+
# set to false or nil to omit the ORDER BY clause
|
|
108
|
+
# * +:exclude+ - an object to exclude (used by the +nearbys+ method)
|
|
109
|
+
# * +:distance_column+ - used to set the column name of the calculated distance.
|
|
110
|
+
# * +:bearing_column+ - used to set the column name of the calculated bearing.
|
|
111
|
+
# * +:min_radius+ - the value to use as the minimum radius.
|
|
112
|
+
# ignored if database is sqlite.
|
|
113
|
+
# default is 0.0
|
|
114
|
+
#
|
|
115
|
+
def near_scope_options(latitude, longitude, radius = 20, options = {})
|
|
116
|
+
if options[:units]
|
|
117
|
+
options[:units] = options[:units].to_sym
|
|
118
|
+
end
|
|
119
|
+
latitude_attribute = options[:latitude] || geocoder_options[:latitude]
|
|
120
|
+
longitude_attribute = options[:longitude] || geocoder_options[:longitude]
|
|
121
|
+
options[:units] ||= (geocoder_options[:units] || Geocoder.config.units)
|
|
122
|
+
select_distance = options.fetch(:select_distance) { true }
|
|
123
|
+
options[:order] = "" if !select_distance && !options.include?(:order)
|
|
124
|
+
select_bearing = options.fetch(:select_bearing) { true }
|
|
125
|
+
bearing = bearing_sql(latitude, longitude, options)
|
|
126
|
+
distance = distance_sql(latitude, longitude, options)
|
|
127
|
+
distance_column = options.fetch(:distance_column) { 'distance' }
|
|
128
|
+
bearing_column = options.fetch(:bearing_column) { 'bearing' }
|
|
129
|
+
|
|
130
|
+
b = Geocoder::Calculations.bounding_box([latitude, longitude], radius, options)
|
|
131
|
+
args = b + [
|
|
132
|
+
full_column_name(latitude_attribute),
|
|
133
|
+
full_column_name(longitude_attribute)
|
|
134
|
+
]
|
|
135
|
+
bounding_box_conditions = Geocoder::Sql.within_bounding_box(*args)
|
|
136
|
+
|
|
137
|
+
if using_sqlite?
|
|
138
|
+
conditions = bounding_box_conditions
|
|
139
|
+
else
|
|
140
|
+
min_radius = options.fetch(:min_radius, 0).to_f
|
|
141
|
+
conditions = [bounding_box_conditions + " AND (#{distance}) BETWEEN ? AND ?", min_radius, radius]
|
|
142
|
+
end
|
|
143
|
+
{
|
|
144
|
+
:select => select_clause(options[:select],
|
|
145
|
+
select_distance ? distance : nil,
|
|
146
|
+
select_bearing ? bearing : nil,
|
|
147
|
+
distance_column,
|
|
148
|
+
bearing_column),
|
|
149
|
+
:conditions => add_exclude_condition(conditions, options[:exclude]),
|
|
150
|
+
:order => options.include?(:order) ? options[:order] : "#{distance_column} ASC"
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
##
|
|
155
|
+
# SQL for calculating distance based on the current database's
|
|
156
|
+
# capabilities (trig functions?).
|
|
157
|
+
#
|
|
158
|
+
def distance_sql(latitude, longitude, options = {})
|
|
159
|
+
method_prefix = using_sqlite? ? "approx" : "full"
|
|
160
|
+
Geocoder::Sql.send(
|
|
161
|
+
method_prefix + "_distance",
|
|
162
|
+
latitude, longitude,
|
|
163
|
+
full_column_name(options[:latitude] || geocoder_options[:latitude]),
|
|
164
|
+
full_column_name(options[:longitude]|| geocoder_options[:longitude]),
|
|
165
|
+
options
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
##
|
|
170
|
+
# SQL for calculating bearing based on the current database's
|
|
171
|
+
# capabilities (trig functions?).
|
|
172
|
+
#
|
|
173
|
+
def bearing_sql(latitude, longitude, options = {})
|
|
174
|
+
if !options.include?(:bearing)
|
|
175
|
+
options[:bearing] = Geocoder.config.distances
|
|
176
|
+
end
|
|
177
|
+
if options[:bearing]
|
|
178
|
+
method_prefix = using_sqlite? ? "approx" : "full"
|
|
179
|
+
Geocoder::Sql.send(
|
|
180
|
+
method_prefix + "_bearing",
|
|
181
|
+
latitude, longitude,
|
|
182
|
+
full_column_name(options[:latitude] || geocoder_options[:latitude]),
|
|
183
|
+
full_column_name(options[:longitude]|| geocoder_options[:longitude]),
|
|
184
|
+
options
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
##
|
|
190
|
+
# Generate the SELECT clause.
|
|
191
|
+
#
|
|
192
|
+
def select_clause(columns, distance = nil, bearing = nil, distance_column = 'distance', bearing_column = 'bearing')
|
|
193
|
+
if columns == :id_only
|
|
194
|
+
return full_column_name(primary_key)
|
|
195
|
+
elsif columns == :geo_only
|
|
196
|
+
clause = ""
|
|
197
|
+
else
|
|
198
|
+
clause = (columns || full_column_name("*"))
|
|
199
|
+
end
|
|
200
|
+
if distance
|
|
201
|
+
clause += ", " unless clause.empty?
|
|
202
|
+
clause += "#{distance} AS #{distance_column}"
|
|
203
|
+
end
|
|
204
|
+
if bearing
|
|
205
|
+
clause += ", " unless clause.empty?
|
|
206
|
+
clause += "#{bearing} AS #{bearing_column}"
|
|
207
|
+
end
|
|
208
|
+
clause
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
##
|
|
212
|
+
# Adds a condition to exclude a given object by ID.
|
|
213
|
+
# Expects conditions as an array or string. Returns array.
|
|
214
|
+
#
|
|
215
|
+
def add_exclude_condition(conditions, exclude)
|
|
216
|
+
conditions = [conditions] if conditions.is_a?(String)
|
|
217
|
+
if exclude
|
|
218
|
+
conditions[0] << " AND #{full_column_name(primary_key)} != ?"
|
|
219
|
+
conditions << exclude.id
|
|
220
|
+
end
|
|
221
|
+
conditions
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def using_sqlite?
|
|
225
|
+
connection.adapter_name.match(/sqlite/i)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def using_postgres?
|
|
229
|
+
connection.adapter_name.match(/postgres/i)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
##
|
|
233
|
+
# Use OID type when running in PosgreSQL
|
|
234
|
+
#
|
|
235
|
+
def null_value
|
|
236
|
+
using_postgres? ? 'NULL::text' : 'NULL'
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
##
|
|
240
|
+
# Value which can be passed to where() to produce no results.
|
|
241
|
+
#
|
|
242
|
+
def false_condition
|
|
243
|
+
using_sqlite? ? 0 : "false"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
##
|
|
247
|
+
# Prepend table name if column name doesn't already contain one.
|
|
248
|
+
#
|
|
249
|
+
def full_column_name(column)
|
|
250
|
+
column = column.to_s
|
|
251
|
+
column.include?(".") ? column : [table_name, column].join(".")
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
##
|
|
256
|
+
# Look up coordinates and assign to +latitude+ and +longitude+ attributes
|
|
257
|
+
# (or other as specified in +geocoded_by+). Returns coordinates (array).
|
|
258
|
+
#
|
|
259
|
+
def geocode
|
|
260
|
+
do_lookup(false) do |o,rs|
|
|
261
|
+
if r = rs.first
|
|
262
|
+
unless r.latitude.nil? or r.longitude.nil?
|
|
263
|
+
o.__send__ "#{self.class.geocoder_options[:latitude]}=", r.latitude
|
|
264
|
+
o.__send__ "#{self.class.geocoder_options[:longitude]}=", r.longitude
|
|
265
|
+
end
|
|
266
|
+
r.coordinates
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
alias_method :fetch_coordinates, :geocode
|
|
272
|
+
|
|
273
|
+
##
|
|
274
|
+
# Look up address and assign to +address+ attribute (or other as specified
|
|
275
|
+
# in +reverse_geocoded_by+). Returns address (string).
|
|
276
|
+
#
|
|
277
|
+
def reverse_geocode
|
|
278
|
+
do_lookup(true) do |o,rs|
|
|
279
|
+
if r = rs.first
|
|
280
|
+
unless r.address.nil?
|
|
281
|
+
o.__send__ "#{self.class.geocoder_options[:fetched_address]}=", r.address
|
|
282
|
+
end
|
|
283
|
+
r.address
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
alias_method :fetch_address, :reverse_geocode
|
|
289
|
+
end
|
|
290
|
+
end
|