dm-geokit 0.10.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.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Matt King
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,51 @@
1
+ dm-geokit
2
+ =========
3
+
4
+ A mixin for Datamapper models that enables geographic functionality.
5
+
6
+ * Search for content via DataMapper-style query methods, e.g. Location.all(:address.near => {:origin => 'Portland, OR', :distance => 5.mi})
7
+ * Sort by distance easily: Location.all(:address.near => {:origin => 'Portland, OR', :distance => 5.mi}, :order => [:address_distance.desc])
8
+ * Ability to specify multiple fields that are geocodable (mostly)
9
+
10
+ Usage
11
+ =====
12
+
13
+ Basic Class Definition:
14
+
15
+ class Location
16
+ include DataMapper::Resource
17
+ property :id, Serial
18
+ has_geographic_location :address
19
+ end
20
+
21
+ This will automatically generate fields and methods for use with the DM Object, prefixed with the field name specified.
22
+ Since the above example used the field :address, the following fields would be generated:
23
+
24
+ * address_street_address
25
+ * address_city
26
+ * address_state
27
+ * address_zip
28
+ * address_country_code
29
+ * address_full_address
30
+ * address_lat
31
+ * address_lng
32
+
33
+ You can either reference those fields directly, or use the proxy object returned by calling .address on your object:
34
+
35
+ l = Location.all(:address.near => {:origin => 'Portland, OR', :distance => 5.mi})
36
+
37
+ l.each do |loc|
38
+ puts loc.address # .to_s yields string representation of full address, e.g. "12345 My St. Portland, OR USA"
39
+ puts loc.address.inspect # the proxy object, GeographicLocation, with matching methods for each property
40
+ puts loc.address.street_address # getting the street_address from the proxy object
41
+ puts loc.address_street_address # directly access the SQL column
42
+ end
43
+
44
+ The GeographicLocation proxy object is a convenience to allow you to compare and sort results in Ruby.
45
+
46
+ Requirements
47
+ ===========
48
+
49
+ * geokit >= 1.5.0
50
+ * dm-core >= 0.10.1
51
+ * dm-aggregates >= 0.10.1
@@ -0,0 +1,20 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |s|
4
+ s.name = 'dm-geokit'
5
+ s.summary = "DataMapper plugin for geokit stuff forked from Foy Savas's project. Now relies on the geokit gem rather than Foy's gem."
6
+ s.authors = ['Foy Savas', 'Daniel Higginbotham', 'Matt King']
7
+ s.email = 'matt@mattking.org'
8
+ s.homepage = "http://github.com/mattking17/dm-geokit/tree/master"
9
+ s.description = "Simple and opinionated helper for creating Rubygem projects on GitHub"
10
+ s.files = FileList["[A-Z]*", "{bin,generators,lib,test}/**/*", 'lib/jeweler/templates/.gitignore']
11
+ s.require_path = 'lib'
12
+ s.has_rdoc = true
13
+ s.platform = Gem::Platform::RUBY
14
+ s.extra_rdoc_files = %w[ README LICENSE TODO ]
15
+ s.add_dependency 'dm-core'
16
+ s.add_dependency 'andre-geokit'
17
+ end
18
+ rescue LoadError
19
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
20
+ end
data/TODO ADDED
@@ -0,0 +1,8 @@
1
+ TODO
2
+ ====
3
+
4
+
5
+
6
+ ---
7
+ TODO tickets may also be found in the DataMapper Issue Tracker:
8
+ http://wm.lighthouseapp.com/projects/4819-datamapper/overview
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 10
4
+ :patch: 1
@@ -0,0 +1,5 @@
1
+ %w(rubygems geokit dm-core dm-aggregates).each{|f| require f}
2
+ %w(distance_measurement distance_support symbol integer float).each{|f|
3
+ require File.join(File.dirname(__FILE__),'dm-geokit','support',f)
4
+ }
5
+ require File.join(File.dirname(__FILE__),'dm-geokit','resource')
@@ -0,0 +1,35 @@
1
+ require 'yaml'
2
+
3
+ module GeoKit
4
+ # Contains a class method geocode_ip_address which can be used to enable automatic geocoding
5
+ # for request IP addresses. The geocoded information is stored in a cookie and in the
6
+ # session to minimize web service calls. The point of the helper is to enable location-based
7
+ # websites to have a best-guess for new visitors.
8
+ module IpGeocodeLookup
9
+
10
+ private
11
+
12
+ # Places the IP address' geocode location into the session if it
13
+ # can be found. Otherwise, looks for a geo location cookie and
14
+ # uses that value. The last resort is to call the web service to
15
+ # get the value.
16
+ def store_ip_location
17
+ session[:geo_location] ||= retrieve_location_from_cookie_or_service
18
+ cookies[:geo_location] = { :value => session[:geo_location].to_yaml, :expires => 30.days.from_now } if session[:geo_location]
19
+ end
20
+
21
+ # Uses the stored location value from the cookie if it exists. If
22
+ # no cookie exists, calls out to the web service to get the location.
23
+ def retrieve_location_from_cookie_or_service
24
+ return YAML.load(cookies[:geo_location]) if cookies[:geo_location]
25
+ location = Geocoders::IpGeocoder.geocode(get_ip_address)
26
+ return location.success ? location : nil
27
+ end
28
+
29
+ # Returns the real ip address, though this could be the localhost ip
30
+ # address. No special handling here anymore.
31
+ def get_ip_address
32
+ request.remote_ip
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,237 @@
1
+ module DataMapper
2
+ module GeoKit
3
+ PROPERTY_NAMES = %w(lat lng street_address city state zip country_code full_address)
4
+
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ def has_geographic_location(name, options = {})
11
+ return if self.included_modules.include?(DataMapper::GeoKit::InstanceMethods)
12
+ send :include, InstanceMethods
13
+ send :include, ::GeoKit::Mappable
14
+
15
+ property name.to_sym, String, :length => 255
16
+ property "#{name}_distance".to_sym, Float
17
+
18
+ PROPERTY_NAMES.each do |p|
19
+ if p.match(/l(at|ng)/)
20
+ property "#{name}_#{p}".to_sym, Float, :precision => 15, :scale => 12, :index => true
21
+ else
22
+ property "#{name}_#{p}".to_sym, String, :length => 255
23
+ end
24
+ end
25
+
26
+ DataMapper.auto_upgrade!
27
+
28
+ if options[:auto_geocode] == true or options[:auto_geocode].nil?
29
+ define_method :auto_geocode? do
30
+ true
31
+ end
32
+ else
33
+ define_method :auto_geocode? do
34
+ false
35
+ end
36
+ end
37
+
38
+ define_method "#{name}" do
39
+ if attribute_get(name.to_sym).nil?
40
+ nil
41
+ else
42
+ GeographicLocation.new(name, self)
43
+ end
44
+ end
45
+
46
+ define_method "#{name}=" do |value|
47
+ if value.nil?
48
+ nil
49
+ elsif value.is_a?(String)
50
+ if auto_geocode?
51
+ geo = ::GeoKit::Geocoders::MultiGeocoder.geocode(value)
52
+ if geo.success?
53
+ attribute_set(name.to_sym, geo.full_address)
54
+ PROPERTY_NAMES.each do |p|
55
+ attribute_set("#{name}_#{p}".to_sym, geo.send(p.to_sym))
56
+ end
57
+ end
58
+ else
59
+ attribute_set(name.to_sym, value)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ alias acts_as_mappable has_geographic_location
65
+ end
66
+
67
+ module InstanceMethods
68
+ def self.included(base) # :nodoc:
69
+ base.extend SingletonMethods
70
+ end
71
+
72
+ module SingletonMethods # :nodoc:
73
+ def all(query = {})
74
+ super(prepare_query(query))
75
+ end
76
+
77
+ def first(query = {})
78
+ super(prepare_query(query))
79
+ end
80
+
81
+ # Required dm-aggregates to work
82
+ def count(query = {})
83
+ super(prepare_query(query))
84
+ end
85
+
86
+ private
87
+
88
+ # Looks in the query for keys that are a DistanceOperator, then extracts the keys/values and turns them into conditions
89
+ def prepare_query(query)
90
+ query.each_pair do |k,v|
91
+ next if not k.is_a?(DistanceOperator)
92
+ field = k.target
93
+ origin = v[:origin].is_a?(String) ? ::GeoKit::Geocoders::MultiGeocoder.geocode(v[:origin]) : v[:origin]
94
+ distance = v[:distance]
95
+ query[:conditions] = expand_conditions(query[:conditions], "#{sphere_distance_sql(field, origin, distance.measurement)}", distance.to_f)
96
+ query[:conditions] = apply_bounds_conditions(query[:conditions], field, bounds_from_distance(distance.to_f, origin, distance.measurement))
97
+ query[:fields] = expand_fields(query[:fields], field, "#{sphere_distance_sql(field, origin, distance.measurement)}")
98
+ query.delete(k)
99
+ end
100
+ query
101
+ end
102
+
103
+ # Spherical distance sql
104
+ def sphere_distance_sql(field, origin, units)
105
+ lat = deg2rad(origin.lat)
106
+ lng = deg2rad(origin.lng)
107
+ qualified_lat_column = "`#{storage_name}`.`#{field}_lat`"
108
+ qualified_lng_column = "`#{storage_name}`.`#{field}_lng`"
109
+ "(ACOS(least(1,COS(#{lat})*COS(#{lng})*COS(RADIANS(#{qualified_lat_column}))*COS(RADIANS(#{qualified_lng_column}))+COS(#{lat})*SIN(#{lng})*COS(RADIANS(#{qualified_lat_column}))*SIN(RADIANS(#{qualified_lng_column}))+SIN(#{lat})*SIN(RADIANS(#{qualified_lat_column}))))*#{units_sphere_multiplier(units)})"
110
+ end
111
+
112
+ # in case conditions were altered by other means
113
+ def expand_conditions(conditions, sql, value)
114
+ if conditions.is_a?(Hash)
115
+ [conditions.keys.inject(''){|m,k|
116
+ m << "#{k} = ?"
117
+ } << " AND #{sql} <= ?"] + ([conditions.values] << value)
118
+ elsif conditions.is_a?(Array)
119
+ if conditions.size == 1
120
+ ["#{conditions[0]} AND #{sql} <= ?", value]
121
+ else
122
+ conditions[0] = "#{conditions[0]} AND #{sql} <= ?"
123
+ conditions << value
124
+ conditions
125
+ end
126
+ else
127
+ ["#{sql} <= ?", value]
128
+ end
129
+ end
130
+
131
+ # Hack in the distance field by adding the :fields option to the query
132
+ def expand_fields(fields, distance_field, sql)
133
+ f = DataMapper::Property.new(self, "#{distance_field}_distance".to_sym, DataMapper::Types::Distance, :field => "#{sql} as #{distance_field}_distance")
134
+ if fields.is_a?(Array) # user specified fields, just tack this onto the end
135
+ fields + [f]
136
+ else # otherwise since we specify :fields, we have to add back in the original fields it would have selected
137
+ self.properties(repository.name).defaults + [f]
138
+ end
139
+ end
140
+
141
+ def bounds_from_distance(distance, origin, units)
142
+ if distance
143
+ ::GeoKit::Bounds.from_point_and_radius(origin,distance,:units=>units)
144
+ else
145
+ nil
146
+ end
147
+ end
148
+
149
+ def apply_bounds_conditions(conditions, field, bounds)
150
+ qualified_lat_column = "`#{storage_name}`.`#{field}_lat`"
151
+ qualified_lng_column = "`#{storage_name}`.`#{field}_lng`"
152
+ sw, ne = bounds.sw, bounds.ne
153
+ lng_sql = bounds.crosses_meridian? ? "(#{qualified_lng_column}<=#{sw.lng} OR #{qualified_lng_column}>=#{ne.lng})" : "#{qualified_lng_column}>=#{sw.lng} AND #{qualified_lng_column}<=#{ne.lng}"
154
+ bounds_sql = "#{qualified_lat_column}>=#{sw.lat} AND #{qualified_lat_column}<=#{ne.lat} AND #{lng_sql}"
155
+ conditions[0] << " AND (#{bounds_sql})"
156
+ conditions
157
+ end
158
+
159
+ end
160
+ end
161
+
162
+ class GeographicLocation
163
+ attr_accessor :full_address, :lat, :lng, :street_address, :city, :state, :zip, :country_code, :distance
164
+ def initialize(field, obj)
165
+ PROPERTY_NAMES.each do |p|
166
+ instance_variable_set("@#{p}",obj.send("#{field}_#{p}"))
167
+ end
168
+ @distance = obj.send("#{field}_distance") if obj.respond_to?("#{field}_distance".to_sym)
169
+ end
170
+ def to_s
171
+ @full_address
172
+ end
173
+ def to_lat_lng
174
+ ::GeoKit::LatLng.new(@lat,@lng)
175
+ end
176
+ end
177
+
178
+ class DistanceOperator < DataMapper::Query::Operator
179
+ end
180
+
181
+ module DataObjectsAdapter
182
+ def self.included(base)
183
+ base.send(:include, SQL)
184
+ end
185
+ module SQL
186
+ def self.included(base)
187
+ base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
188
+ # FIXME: figure out a cleaner approach than AMC
189
+ alias property_to_column_name_without_distance property_to_column_name
190
+ alias property_to_column_name property_to_column_name_with_distance
191
+ RUBY
192
+ end
193
+
194
+ def property_to_column_name_with_distance(property, qualify)
195
+ if property.is_a?(DataMapper::Property) and property.type == DataMapper::Types::Distance
196
+ property.field
197
+ else
198
+ property_to_column_name_without_distance(property, qualify)
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
204
+
205
+ module Adapters
206
+ extendable do
207
+ # TODO: document
208
+ # @api private
209
+ def const_added(const_name)
210
+ if DataMapper::GeoKit.const_defined?(const_name)
211
+ adapter = const_get(const_name)
212
+ adapter.send(:include, DataMapper::GeoKit.const_get(const_name))
213
+ end
214
+ super
215
+ end
216
+ end
217
+ end
218
+
219
+ module Types
220
+ class Distance < DataMapper::Type
221
+ primitive Float
222
+ end
223
+ end
224
+
225
+ module Aggregates
226
+ module Model
227
+ def size
228
+ count
229
+ end
230
+ end
231
+ module Collection
232
+ def size
233
+ loaded? ? super : count
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,16 @@
1
+ class DistanceMeasurement
2
+ attr_accessor :measurement
3
+ def initialize(value,measurement)
4
+ @value = value.to_f
5
+ @measurement = measurement
6
+ end
7
+ def to_s
8
+ @value.to_s
9
+ end
10
+ def to_i
11
+ @value.to_i
12
+ end
13
+ def to_f
14
+ @value.to_f
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ module DistanceSupport
2
+
3
+ def mi
4
+ DistanceMeasurement.new(self, :miles)
5
+ end
6
+
7
+ def km
8
+ DistanceMeasurement.new(self, :kms)
9
+ end
10
+
11
+ end
@@ -0,0 +1 @@
1
+ Float.send(:include, DistanceSupport)
@@ -0,0 +1 @@
1
+ Integer.send(:include, DistanceSupport)
@@ -0,0 +1,11 @@
1
+ class Symbol
2
+
3
+ def near
4
+ DataMapper::GeoKit::DistanceOperator.new(self, :near)
5
+ end
6
+
7
+ def outside
8
+ DataMapper::GeoKit::DistanceOperator.new(self, :outside)
9
+ end
10
+
11
+ end
File without changes
@@ -0,0 +1,50 @@
1
+ # These defaults are used in GeoKit::Mappable.distance_to and in acts_as_mappable
2
+ GeoKit::default_units = :miles
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::timeout = 3
9
+
10
+ # These settings are used if web service calls must be routed through a proxy.
11
+ # These setting can be nil if not needed, otherwise, addr and port 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_addr = nil
15
+ GeoKit::Geocoders::proxy_port = nil
16
+ GeoKit::Geocoders::proxy_user = nil
17
+ GeoKit::Geocoders::proxy_pass = nil
18
+
19
+ # This is your yahoo application key for the Yahoo Geocoder.
20
+ # See http://developer.yahoo.com/faq/index.html#appid
21
+ # and http://developer.yahoo.com/maps/rest/V1/geocode.html
22
+ GeoKit::Geocoders::yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY'
23
+
24
+ # This is your Google Maps geocoder key.
25
+ # See http://www.google.com/apis/maps/signup.html
26
+ # and http://www.google.com/apis/maps/documentation/#Geocoding_Examples
27
+ GeoKit::Geocoders::google = 'REPLACE_WITH_YOUR_GOOGLE_KEY'
28
+
29
+ # This is your username and password for geocoder.us.
30
+ # To use the free service, the value can be set to nil or false. For
31
+ # usage tied to an account, the value should be set to username:password.
32
+ # See http://geocoder.us
33
+ # and http://geocoder.us/user/signup
34
+ GeoKit::Geocoders::geocoder_us = false
35
+
36
+ # This is your authorization key for geocoder.ca.
37
+ # To use the free service, the value can be set to nil or false. For
38
+ # usage tied to an account, set the value to the key obtained from
39
+ # Geocoder.ca.
40
+ # See http://geocoder.ca
41
+ # and http://geocoder.ca/?register=1
42
+ GeoKit::Geocoders::geocoder_ca = false
43
+
44
+ # This is the order in which the geocoders are called in a failover scenario
45
+ # If you only want to use a single geocoder, put a single symbol in the array.
46
+ # Valid symbols are :google, :yahoo, :us, and :ca.
47
+ # Be aware that there are Terms of Use restrictions on how you can use the
48
+ # various geocoders. Make sure you read up on relevant Terms of Use for each
49
+ # geocoder you are going to use.
50
+ GeoKit::Geocoders::provider_order = [:google,:us]
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dm-geokit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.10.1
5
+ platform: ruby
6
+ authors:
7
+ - Matt King
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-25 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: dm-core
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.10.1
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: geokit
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.5.0
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: dm-aggregates
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 0.10.1
44
+ version:
45
+ description: Adds geographic functionality to DataMapper objects
46
+ email: matt@mattking.org
47
+ executables: []
48
+
49
+ extensions: []
50
+
51
+ extra_rdoc_files:
52
+ - README
53
+ - LICENSE
54
+ - TODO
55
+ files:
56
+ - LICENSE
57
+ - Rakefile
58
+ - README
59
+ - TODO
60
+ - VERSION.yml
61
+ - lib/dm-geokit
62
+ - lib/dm-geokit/ip_geocode_lookup.rb
63
+ - lib/dm-geokit/resource.rb
64
+ - lib/dm-geokit.rb
65
+ - lib/skeleton
66
+ - lib/skeleton/api_keys_template
67
+ - lib/jeweler/templates/.gitignore
68
+ - lib/dm-geokit/support/distance_measurement.rb
69
+ - lib/dm-geokit/support/distance_support.rb
70
+ - lib/dm-geokit/support/float.rb
71
+ - lib/dm-geokit/support/integer.rb
72
+ - lib/dm-geokit/support/symbol.rb
73
+ has_rdoc: true
74
+ homepage: http://github.com/mattking17/dm-geokit/tree/master
75
+ post_install_message:
76
+ rdoc_options:
77
+ - --inline-source
78
+ - --charset=UTF-8
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: "0"
86
+ version:
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: "0"
92
+ version:
93
+ requirements: []
94
+
95
+ rubyforge_project: dm-geokit
96
+ rubygems_version: 1.3.1
97
+ signing_key:
98
+ specification_version: 2
99
+ summary: Adds geographic functionality to DataMapper objects, relying on the Geokit gem for geocoding and searching by geographic location.
100
+ test_files: []
101
+