module Geokit::ActsAsMappable::ClassMethods
Class methods included in models when acts_as_mappable
is called
Public Instance Methods
# File lib/geokit-rails/acts_as_mappable.rb, line 130 def beyond(distance, options = {}) options[:beyond] = distance #geo_scope(options) where(distance_conditions(options)) end
# File lib/geokit-rails/acts_as_mappable.rb, line 152 def by_distance(options = {}) origin = extract_origin_from_options(options) units = extract_units_from_options(options) formula = extract_formula_from_options(options) distance_column_name = distance_sql(origin, units, formula) with_latlng.order( Arel.sql(distance_column_name).send(options[:reverse] ? 'desc' : 'asc') ) end
# File lib/geokit-rails/acts_as_mappable.rb, line 166 def closest(options = {}) by_distance(options).limit(1) end
Returns the distance calculation to be used as a display column or a condition. This is provide for anyone wanting access to the raw SQL.
# File lib/geokit-rails/acts_as_mappable.rb, line 213 def distance_sql(origin, units=default_units, formula=default_formula) case formula when :sphere sql = sphere_distance_sql(origin, units) when :flat sql = flat_distance_sql(origin, units) end sql end
# File lib/geokit-rails/acts_as_mappable.rb, line 171 def farthest(options = {}) by_distance({:reverse => true}.merge(options)).limit(1) end
A proxy to an instance of a finder adapter, inferred from the connection's adapter.
# File lib/geokit-rails/acts_as_mappable.rb, line 97 def geokit_finder_adapter @geokit_finder_adapter ||= begin unless Adapters.const_defined?(connection.adapter_name.camelcase) filename = connection.adapter_name.downcase require File.join("geokit-rails", "adapters", filename) end klass = Adapters.const_get(connection.adapter_name.camelcase) if klass.class == Module # For some reason Mysql2 adapter was defined in Adapters.constants but was Module instead of a Class filename = connection.adapter_name.downcase require File.join("geokit-rails", "adapters", filename) # Re-init the klass after require klass = Adapters.const_get(connection.adapter_name.camelcase) end klass.load(self) unless klass.loaded || skip_loading klass.new(self) rescue LoadError raise UnsupportedAdapter, "`#{connection.adapter_name.downcase}` is not a supported adapter." end end
# File lib/geokit-rails/acts_as_mappable.rb, line 143 def in_bounds(bounds, options = {}) inclusive = options.delete(:inclusive) || false options[:bounds] = bounds #geo_scope(options) #where(distance_conditions(options)) bounds = extract_bounds_from_options(options) where(bound_conditions(bounds, inclusive)) end
# File lib/geokit-rails/acts_as_mappable.rb, line 137 def in_range(range, options = {}) options[:range] = range #geo_scope(options) where(distance_conditions(options)) end
# File lib/geokit-rails/acts_as_mappable.rb, line 162 def with_latlng where("#{qualified_lat_column_name} IS NOT NULL AND #{qualified_lng_column_name} IS NOT NULL") end
# File lib/geokit-rails/acts_as_mappable.rb, line 118 def within(distance, options = {}) options[:within] = distance # Add bounding box to speed up SQL request. bounds = formulate_bounds_from_distance( options, normalize_point_to_lat_lng(options[:origin]), options[:units] || default_units) with_latlng.where(bound_conditions(bounds)). where(distance_conditions(options)) end
Private Instance Methods
# File lib/geokit-rails/acts_as_mappable.rb, line 267 def bound_conditions(bounds, inclusive = false) return nil unless bounds if inclusive lt_operator = :lteq gt_operator = :gteq else lt_operator = :lt gt_operator = :gt end sw,ne = bounds.sw, bounds.ne lat, lng = Arel.sql(qualified_lat_column_name), Arel.sql(qualified_lng_column_name) lat.send(gt_operator, sw.lat).and(lat.send(lt_operator, ne.lat)).and( if bounds.crosses_meridian? lng.send(lt_operator, ne.lng).or(lng.send(gt_operator, sw.lng)) else lng.send(gt_operator, sw.lng).and(lng.send(lt_operator, ne.lng)) end ) end
# File lib/geokit-rails/acts_as_mappable.rb, line 246 def distance_conditions(options) origin = extract_origin_from_options(options) units = extract_units_from_options(options) formula = extract_formula_from_options(options) distance_column_name = distance_sql(origin, units, formula) if options.has_key?(:within) Arel.sql(distance_column_name).lteq(options[:within]) elsif options.has_key?(:beyond) Arel.sql(distance_column_name).gt(options[:beyond]) elsif options.has_key?(:range) min_condition = Arel.sql(distance_column_name).gteq(options[:range].begin) max_condition = if options[:range].exclude_end? Arel.sql(distance_column_name).lt(options[:range].end) else Arel.sql(distance_column_name).lteq(options[:range].end) end min_condition.and(max_condition) end end
# File lib/geokit-rails/acts_as_mappable.rb, line 315 def extract_bounds_from_options(options) bounds = options.delete(:bounds) bounds = Geokit::Bounds.normalize(bounds) if bounds end
Extract the formula out of the options if it exists and returns it. If there is no :formula key, it uses the default. The side effect of the method is to remove the :formula key from the options hash.
# File lib/geokit-rails/acts_as_mappable.rb, line 309 def extract_formula_from_options(options) formula = options[:formula] || default_formula options.delete(:formula) formula end
Extracts the origin instance out of the options if it exists and returns it. If there is no origin, looks for latitude and longitude values to create an origin. The side-effect of the method is to remove these option keys from the hash.
# File lib/geokit-rails/acts_as_mappable.rb, line 291 def extract_origin_from_options(options) origin = options.delete(:origin) res = normalize_point_to_lat_lng(origin) if origin res end
Extract the units out of the options if it exists and returns it. If there is no :units key, it uses the default. The side effect of the method is to remove the :units key from the options hash.
# File lib/geokit-rails/acts_as_mappable.rb, line 300 def extract_units_from_options(options) units = options[:units] || default_units options.delete(:units) units end
Returns the distance SQL using the flat-world formula (Phythagorean Theory). The SQL is tuned to the database in use.
# File lib/geokit-rails/acts_as_mappable.rb, line 368 def flat_distance_sql(origin, units) lat_degree_units = units_per_latitude_degree(units) lng_degree_units = units_per_longitude_degree(get_lat(origin), units) geokit_finder_adapter.flat_distance_sql(origin, lat_degree_units, lng_degree_units) end
If it's a :within query, add a bounding box to improve performance. This only gets called if a :bounds argument is not otherwise supplied.
# File lib/geokit-rails/acts_as_mappable.rb, line 236 def formulate_bounds_from_distance(options, origin, units) distance = options[:within] if options.has_key?(:within) && options[:within].is_a?(Numeric) distance = options[:range].last-(options[:range].exclude_end?? 1 : 0) if options.has_key?(:range) if distance Geokit::Bounds.from_point_and_radius(origin,distance,:units=>units) else nil end end
Geocode IP address.
# File lib/geokit-rails/acts_as_mappable.rb, line 321 def geocode_ip_address(origin) geo_location = Geokit::Geocoders::MultiGeocoder.geocode(origin) return geo_location if geo_location.success raise Geokit::Geocoders::GeocodeError end
# File lib/geokit-rails/acts_as_mappable.rb, line 374 def get_lat(origin) origin.respond_to?(:lat) ? origin.lat \ : origin.send(:"#{lat_column_name}") end
# File lib/geokit-rails/acts_as_mappable.rb, line 379 def get_lng(origin) origin.respond_to?(:lng) ? origin.lng \ : origin.send(:"#{lng_column_name}") end
Given a point in a variety of (an address to geocode, an array of [lat,lng], or an object with appropriate lat/lng methods, an IP addres) this method will normalize it into a Geokit::LatLng instance. The only thing this method adds on top of LatLng#normalize is handling of IP addresses
# File lib/geokit-rails/acts_as_mappable.rb, line 331 def normalize_point_to_lat_lng(point) res = geocode_ip_address(point) if point.is_a?(String) && /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(point) res = Geokit::LatLng.normalize(point) unless res res end
Returns the distance SQL using the spherical world formula (Haversine). The SQL is tuned to the database in use.
# File lib/geokit-rails/acts_as_mappable.rb, line 356 def sphere_distance_sql(origin, units) # "origin" can be a Geokit::LatLng (with :lat and :lng methods), e.g. # when using geo_scope or it can be an ActsAsMappable with customized # latitude and longitude methods, e.g. when using distance_sql. lat = deg2rad(get_lat(origin)) lng = deg2rad(get_lng(origin)) multiplier = units_sphere_multiplier(units) geokit_finder_adapter.sphere_distance_sql(lat, lng, multiplier) if geokit_finder_adapter end
Looks for the distance column and replaces it with the distance sql. If an origin was not passed in and the distance column exists, we leave it to be flagged as bad SQL by the database. Conditions are either a string or an array. In the case of an array, the first entry contains the condition.
# File lib/geokit-rails/acts_as_mappable.rb, line 341 def substitute_distance_in_where_values(arel, origin, units=default_units, formula=default_formula) pattern = Regexp.new("\\b#{distance_column_name}\\b") value = distance_sql(origin, units, formula) arel.where_values.map! do |where_value| if where_value.is_a?(String) where_value.gsub(pattern, value) else where_value end end arel end