geomodel 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +192 -0
- data/Rakefile +22 -0
- data/geomodel.gemspec +35 -0
- data/lib/geomodel.rb +158 -0
- data/lib/geomodel/geocell.rb +467 -0
- data/lib/geomodel/geomath.rb +28 -0
- data/lib/geomodel/geotypes.rb +109 -0
- data/lib/geomodel/util.rb +51 -0
- data/lib/geomodel/version.rb +3 -0
- data/spec/geocell_spec.rb +154 -0
- data/spec/geomath_spec.rb +55 -0
- data/spec/geomodel_spec.rb +89 -0
- data/spec/geotypes_spec.rb +65 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/util_spec.rb +19 -0
- metadata +229 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b8914e3d585af5281290b7a854cb1eec6d7a566c
|
4
|
+
data.tar.gz: 30b52b4cb9989bf752dbe9fb426f71607ef5858c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b170a8372a165c3bdad452588f7ffad1174996b448076f8a139bd0ee48bd83924ef1a4420734d9c98f8106239ce2fb601d0cd525350a4b692662f68847d61e80
|
7
|
+
data.tar.gz: 13298af29d84898c152475a74ffc11d917507224a5f2a36b5e2cd3bc0c6e4e0e18fbca99366c8c545771c0076e5daf02421d53d2200d92e2ec2d1a63e6cda41f
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
geomodel
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.0.0
|
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Integrallis Software & Brian Sam-Bodden
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
# Geomodel
|
2
|
+
|
3
|
+
[![Build Status](https://secure.travis-ci.org/integrallis/geomodel.png?branch=master)](http://travis-ci.org/integrallis/geomodel)
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/geomodel.png)](http://badge.fury.io/rb/geomodel)
|
5
|
+
[![Dependency Status](https://gemnasium.com/integrallis/geomodel.png)](https://gemnasium.com/integrallis/geomodel)
|
6
|
+
[![Code Climate](https://codeclimate.com/github/integrallis/geomodel.png)](https://codeclimate.com/github/integrallis/geomodel)
|
7
|
+
[![Coverage Status](https://coveralls.io/repos/integrallis/geomodel/badge.png?branch=master)](https://coveralls.io/r/integrallis/geomodel?branch=master)
|
8
|
+
|
9
|
+
Geomodel aims to provide a generalized solution for performing basic indexing
|
10
|
+
and querying of geospatial data in non-relation environments. At the core, this
|
11
|
+
solution utilizes geohash-like objects called geocells.
|
12
|
+
|
13
|
+
A geocell is a hexadecimal string that defines a two dimensional rectangular
|
14
|
+
region inside the [-90,90] x [-180,180] latitude/longitude space. A geocell's
|
15
|
+
'resolution' is its length. For most practical purposes, at high resolutions,
|
16
|
+
geocells can be treated as single points.
|
17
|
+
|
18
|
+
Much like geohashes (see http://en.wikipedia.org/wiki/Geohash), geocells are
|
19
|
+
hierarchical, in that any prefix of a geocell is considered its ancestor, with
|
20
|
+
geocell[:-1] being geocell's immediate parent cell.
|
21
|
+
|
22
|
+
To calculate the rectangle of a given geocell string, first divide the
|
23
|
+
[-90,90] x [-180,180] latitude/longitude space evenly into a 4x4 grid like so:
|
24
|
+
|
25
|
+
<pre>
|
26
|
+
+---+---+---+---+ (90, 180)
|
27
|
+
| a | b | e | f |
|
28
|
+
+---+---+---+---+
|
29
|
+
| 8 | 9 | c | d |
|
30
|
+
+---+---+---+---+
|
31
|
+
| 2 | 3 | 6 | 7 |
|
32
|
+
+---+---+---+---+
|
33
|
+
| 0 | 1 | 4 | 5 |
|
34
|
+
(-90,-180) +---+---+---+---+
|
35
|
+
</pre>
|
36
|
+
|
37
|
+
NOTE: The point (0, 0) is at the intersection of grid cells 3, 6, 9 and c. And,
|
38
|
+
for example, cell 7 should be the sub-rectangle from (-45, 90) to (0, 180).
|
39
|
+
|
40
|
+
Calculate the sub-rectangle for the first character of the geocell string and
|
41
|
+
re-divide this sub-rectangle into another 4x4 grid. For example, if the geocell
|
42
|
+
string is '78a', we will re-divide the sub-rectangle like so:
|
43
|
+
|
44
|
+
<pre>
|
45
|
+
. .
|
46
|
+
. .
|
47
|
+
. . +----+----+----+----+ (0, 180)
|
48
|
+
| 7a | 7b | 7e | 7f |
|
49
|
+
+----+----+----+----+
|
50
|
+
| 78 | 79 | 7c | 7d |
|
51
|
+
+----+----+----+----+
|
52
|
+
| 72 | 73 | 76 | 77 |
|
53
|
+
+----+----+----+----+
|
54
|
+
| 70 | 71 | 74 | 75 |
|
55
|
+
. . (-45,90) +----+----+----+----+
|
56
|
+
. .
|
57
|
+
. .
|
58
|
+
</pre>
|
59
|
+
|
60
|
+
Continue to re-divide into sub-rectangles and 4x4 grids until the entire
|
61
|
+
geocell string has been exhausted. The final sub-rectangle is the rectangular
|
62
|
+
region for the geocell.
|
63
|
+
|
64
|
+
A geocell can be associated with a single geographic point and subsequently
|
65
|
+
indexed and filtered by either conformance to a bounding box or by proximity
|
66
|
+
(nearest-n) to a search center point.
|
67
|
+
|
68
|
+
# Approach
|
69
|
+
|
70
|
+
This Ruby implementation of GeoModel is based on the Python, Java and JavaScript implementations.
|
71
|
+
It's implemented as class level methods contained modules and a few datatype classes. So the 'model'
|
72
|
+
part isn't quite there and I don't really see a need for it. Since the library is meant to be use in
|
73
|
+
Non-Relational/Non-ORM environmets, binding the functions/methods to a model does not make much sense.
|
74
|
+
The model part was mostly implemented in the other libraries to bind directly to Google App Engine.
|
75
|
+
|
76
|
+
# References
|
77
|
+
|
78
|
+
- http://code.google.com/p/javageomodel/
|
79
|
+
- http://code.google.com/p/geomodel/
|
80
|
+
- https://github.com/danieldkim/geomodel
|
81
|
+
|
82
|
+
## Installation
|
83
|
+
|
84
|
+
Add this line to your application's Gemfile:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
gem 'geomodel'
|
88
|
+
```
|
89
|
+
|
90
|
+
And then execute:
|
91
|
+
|
92
|
+
$ bundle
|
93
|
+
|
94
|
+
Or install it yourself as:
|
95
|
+
|
96
|
+
$ gem install geomodel
|
97
|
+
|
98
|
+
## Usage
|
99
|
+
|
100
|
+
Currently, only single-point entities and two types of basic geospatial queries
|
101
|
+
on those entities are supported.
|
102
|
+
|
103
|
+
### Representing your locations
|
104
|
+
|
105
|
+
You'll need a class to hold a geolocation. It assumes that an "entity" has a unique
|
106
|
+
"id" (specific field can be configure), a latitude/longitude combination stored in
|
107
|
+
a "location" field (a Geomodel::Types::Point) and a collection of "geocells".
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
class Entity
|
111
|
+
attr_accessor :id, :location, :geocells
|
112
|
+
|
113
|
+
def to_s
|
114
|
+
self.id
|
115
|
+
end
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
An instance of one of these entities can be instantiated as shown next. Let's say we
|
120
|
+
wanted to create an entity for the Frank Lloyd Wright Iconic Desert Spire in Scottsdale, AZ
|
121
|
+
(http://livebetterinscottsdale.com/2012/02/things-to-see-in-scottsdale-az-the-frank-lloyd-wright-spire/):
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
flw_spire = Entity.new
|
125
|
+
flw_spire.id = 'Flatiron'
|
126
|
+
flw_spire.location = Geomodel::Types::Point.new(33.633406, -111.916803)
|
127
|
+
flw_spire.geocells = Geomodel::GeoCell.generate_geocells(flw_spire.location)
|
128
|
+
```
|
129
|
+
|
130
|
+
### Bounding Box Queries
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
# compute a geocell for the location using a resolution of 14
|
134
|
+
cell = Geomodel::GeoCell.compute(flw_spire.location, 14)
|
135
|
+
|
136
|
+
# create a bounding box for the cell
|
137
|
+
bounding_box = Geomodel::GeoCell.compute_box(cell)
|
138
|
+
|
139
|
+
# get a list of geocells for the given bounding box
|
140
|
+
geocells = Geomodel.geocells_for_bounding_box(bounding_box)
|
141
|
+
|
142
|
+
# use the bounding box geocells to do a key lookup in your database assuming that there is
|
143
|
+
# a location_geocell 'column' and you can do an IN query like:
|
144
|
+
result_set = my_db.query('SELECT * WHERE location_geocells IN (?)', query_geocells)
|
145
|
+
|
146
|
+
# the results then can be filtered by whether they fall inside the bounding box using:
|
147
|
+
matches = Geomodel.filter_result_set_by_bounding_box(bounding_box, result_set)
|
148
|
+
```
|
149
|
+
|
150
|
+
### proximity (nearest-n) queries
|
151
|
+
|
152
|
+
Find nearby locations given a location (lat & lon) and a radius in meters:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
|
156
|
+
# a list of places (instance of Entity or object that responds to :id, :location, :geocells)
|
157
|
+
places = [place1, place2, place3, ...]
|
158
|
+
|
159
|
+
# a function that can query your database. It takes as a parameter an array of geocells (strings)
|
160
|
+
# that are used to filter the query (below is an in-memory implementation using the 'places' array
|
161
|
+
# as our datasource)
|
162
|
+
query_runner = lambda do |geocells|
|
163
|
+
result = places.reject do |o|
|
164
|
+
(o.geocells & geocells).length < 0
|
165
|
+
end
|
166
|
+
|
167
|
+
result
|
168
|
+
end
|
169
|
+
|
170
|
+
# query for a maximum of 20 results, 15 miles (~24140 meters) from the Frank Lloyd Wright Spire
|
171
|
+
# results are tuples (2 element arrays) with the matching entity and its distance from the location
|
172
|
+
results = Geomodel.proximity_fetch(flw_spire.location, query_runner, 20, 24140)
|
173
|
+
|
174
|
+
# extract the matching places
|
175
|
+
places = results.map(&:first)
|
176
|
+
|
177
|
+
# extract the distances
|
178
|
+
distances = results.map(&:last)
|
179
|
+
|
180
|
+
```
|
181
|
+
|
182
|
+
## Contributing
|
183
|
+
|
184
|
+
1. Fork it
|
185
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
186
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
187
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
188
|
+
5. Create new Pull Request
|
189
|
+
|
190
|
+
## License
|
191
|
+
|
192
|
+
MIT License
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
#--
|
4
|
+
# Copyright &169;2001-2014 Integrallis Software, LLC.
|
5
|
+
# All Rights Reserved.
|
6
|
+
#
|
7
|
+
# Permission is granted for use, copying, modification, distribution,
|
8
|
+
# and distribution of modified versions of this work as long as the
|
9
|
+
# above copyright notice is included.
|
10
|
+
#++
|
11
|
+
|
12
|
+
# encoding: utf-8
|
13
|
+
|
14
|
+
# --------------------------------------------------------------------
|
15
|
+
require 'rubygems'
|
16
|
+
require 'bundler/setup'
|
17
|
+
require 'bundler/gem_tasks'
|
18
|
+
require 'rspec/core/rake_task'
|
19
|
+
|
20
|
+
RSpec::Core::RakeTask.new(:spec)
|
21
|
+
|
22
|
+
task default: :spec
|
data/geomodel.gemspec
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'geomodel/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'geomodel'
|
8
|
+
spec.version = Geomodel::VERSION
|
9
|
+
spec.authors = ['Brian Sam-Bodden']
|
10
|
+
spec.email = ['bsbodden@integrallis.com']
|
11
|
+
spec.description = %q{A Ruby implementation of the Geomodel concept}
|
12
|
+
spec.summary = %q{Geomodel aims to provide a generalized solution for performing basic indexing
|
13
|
+
and querying of geospatial data in non-relation environments. At the core, this
|
14
|
+
solution utilizes geohash-like objects called geocells.}
|
15
|
+
spec.homepage = 'https://github.com/integrallis/geomodel'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.files = `git ls-files`.split($/)
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.add_dependency 'geocoder', '~> 1.1.9'
|
24
|
+
spec.add_dependency 'hashie', '~> 2.0.5'
|
25
|
+
|
26
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
27
|
+
spec.add_development_dependency 'rake'
|
28
|
+
spec.add_development_dependency 'rspec'
|
29
|
+
spec.add_development_dependency 'simplecov'
|
30
|
+
spec.add_development_dependency 'hashie'
|
31
|
+
spec.add_development_dependency 'debugger'
|
32
|
+
spec.add_development_dependency 'launchy'
|
33
|
+
spec.add_development_dependency 'pry'
|
34
|
+
spec.add_development_dependency 'coveralls'
|
35
|
+
end
|
data/lib/geomodel.rb
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
require "geomodel/version"
|
2
|
+
require "geomodel/geomath"
|
3
|
+
require "geomodel/geotypes"
|
4
|
+
require "geomodel/geocell"
|
5
|
+
require "geomodel/util"
|
6
|
+
require 'set'
|
7
|
+
|
8
|
+
module Geomodel
|
9
|
+
|
10
|
+
# The default cost function, used if none is provided
|
11
|
+
DEFAULT_COST_FUNCTION = lambda do |num_cells, resolution|
|
12
|
+
num_cells > (Geomodel::GeoCell::GEOCELL_GRID_SIZE ** 2) ? 1e10000 : 0
|
13
|
+
end
|
14
|
+
|
15
|
+
# Retrieve the geocells to be used in a bounding box query
|
16
|
+
# Something like geocells IN (...)
|
17
|
+
#
|
18
|
+
# Args:
|
19
|
+
#
|
20
|
+
# bbox: A geotypes.Box indicating the bounding box to filter entities by.
|
21
|
+
# cost_function: An optional function that accepts two arguments:
|
22
|
+
# * num_cells: the number of cells to search
|
23
|
+
# * resolution: the resolution of each cell to search
|
24
|
+
# and returns the 'cost' of querying against this number of cells
|
25
|
+
# at the given resolution.
|
26
|
+
def self.geocells_for_bounding_box(bounding_box, cost_function = nil)
|
27
|
+
cost_function = DEFAULT_COST_FUNCTION if cost_function.nil?
|
28
|
+
Geomodel::GeoCell.best_bbox_search_cells(bounding_box, cost_function)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Given a result set from your datastore (a query you filtered with
|
32
|
+
# #geocells_for_bounding_box) it will filter the records that land outside
|
33
|
+
# of the given bounding box (generally you'll use the same bounding box used
|
34
|
+
# in #geocells_for_bounding_box)
|
35
|
+
def self.filter_result_set_by_bounding_box(bounding_box, result_set)
|
36
|
+
result_set.select do |row|
|
37
|
+
row.latitude >= bounding_box.south &&
|
38
|
+
row.latitude <= bounding_box.north &&
|
39
|
+
row.longitude >= bounding_box.west &&
|
40
|
+
row.longitude <= bounding_box.east
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# center: A geotypes.Point or db.GeoPt indicating the center point around
|
45
|
+
# which to search for matching entities.
|
46
|
+
# max_results: An int indicating the maximum number of desired results.
|
47
|
+
# The default is 10, and the larger this number, the longer the fetch
|
48
|
+
# will take.
|
49
|
+
# max_distance: An optional number indicating the maximum distance to
|
50
|
+
# search, in meters.
|
51
|
+
def self.proximity_fetch(center, query_runner, max_results = 10, max_distance = 0)
|
52
|
+
results = []
|
53
|
+
|
54
|
+
searched_cells = Set.new
|
55
|
+
|
56
|
+
# The current search geocell containing the lat,lon.
|
57
|
+
cur_containing_geocell = Geomodel::GeoCell.compute(center)
|
58
|
+
|
59
|
+
# The currently-being-searched geocells.
|
60
|
+
# NOTES:
|
61
|
+
# * Start with max possible.
|
62
|
+
# * Must always be of the same resolution.
|
63
|
+
# * Must always form a rectangular region.
|
64
|
+
# * One of these must be equal to the cur_containing_geocell.
|
65
|
+
cur_geocells = [cur_containing_geocell]
|
66
|
+
|
67
|
+
closest_possible_next_result_dist = 0
|
68
|
+
|
69
|
+
# Assumes both a and b are lists of (entity, dist) tuples, *sorted by dist*.
|
70
|
+
# NOTE: This is an in-place merge, and there are guaranteed
|
71
|
+
# no duplicates in the resulting list.
|
72
|
+
|
73
|
+
cmp_fn = lambda do |x, y|
|
74
|
+
(!x.empty? && !y.empty?) ? x[1] <=> y[1] : 0
|
75
|
+
end
|
76
|
+
|
77
|
+
dup_fn = lambda do |x|
|
78
|
+
(x.nil? || x.empty?) ? nil : x[0].id
|
79
|
+
end # assuming the the element responds to #id
|
80
|
+
|
81
|
+
sorted_edges = [[0,0]]
|
82
|
+
sorted_edge_distances = [0]
|
83
|
+
|
84
|
+
while !cur_geocells.empty?
|
85
|
+
closest_possible_next_result_dist = sorted_edge_distances[0]
|
86
|
+
|
87
|
+
next if max_distance and closest_possible_next_result_dist > max_distance
|
88
|
+
|
89
|
+
cur_geocells_unique = cur_geocells - searched_cells.to_a
|
90
|
+
|
91
|
+
# Run query on the next set of geocells.
|
92
|
+
cur_resolution = cur_geocells[0].size
|
93
|
+
|
94
|
+
# Update results and sort.
|
95
|
+
new_results = query_runner.call(cur_geocells_unique)
|
96
|
+
|
97
|
+
searched_cells.merge(cur_geocells)
|
98
|
+
|
99
|
+
# Begin storing distance from the search result entity to the
|
100
|
+
# search center along with the search result itself, in a tuple.
|
101
|
+
new_results = new_results.map { |entity| [entity, Geomodel::Math.distance(center, entity.location)] }
|
102
|
+
new_results.sort! { |x, y| (!x.empty? && !y.empty?) ? x[1] <=> y[1] : 0 }
|
103
|
+
new_results = new_results[0...max_results]
|
104
|
+
|
105
|
+
# Merge new_results into results or the other way around, depending on
|
106
|
+
# which is larger.
|
107
|
+
if results.size > new_results.size
|
108
|
+
Geomodel::Util.merge_in_place(results, [new_results], dup_fn, cmp_fn)
|
109
|
+
else
|
110
|
+
Geomodel::Util.merge_in_place(new_results, [results], dup_fn, cmp_fn)
|
111
|
+
results = new_results
|
112
|
+
end
|
113
|
+
|
114
|
+
results = results[0...max_results]
|
115
|
+
|
116
|
+
sorted_edges, sorted_edge_distances = Geomodel::Util.distance_sorted_edges(cur_geocells, center)
|
117
|
+
|
118
|
+
if results.empty? || cur_geocells.size == 4
|
119
|
+
# Either no results (in which case we optimize by not looking at adjacents, go straight to the parent)
|
120
|
+
# or we've searched 4 adjacent geocells, in which case we should now search the parents of those
|
121
|
+
# geocells.
|
122
|
+
cur_containing_geocell = cur_containing_geocell[0...-1]
|
123
|
+
cur_geocells = cur_geocells.map { |cell| cell[0...-1] }
|
124
|
+
break if !cur_geocells.empty? || !cur_geocells[0] # Done with search, we've searched everywhere.
|
125
|
+
elsif cur_geocells.size == 1
|
126
|
+
# Get adjacent in one direction.
|
127
|
+
# TODO(romannurik): Watch for +/- 90 degree latitude edge case geocells.
|
128
|
+
nearest_edge = sorted_edges[0]
|
129
|
+
cur_geocells << Geomodel::GeoCell.adjacent(cur_geocells[0], nearest_edge)
|
130
|
+
elsif cur_geocells.size == 2
|
131
|
+
# Get adjacents in perpendicular direction.
|
132
|
+
nearest_edge = Geomodel::Util.distance_sorted_edges([cur_containing_geocell], center)[0][0]
|
133
|
+
if nearest_edge[0] == 0
|
134
|
+
# Was vertical, perpendicular is horizontal.
|
135
|
+
perpendicular_nearest_edge = sorted_edges.keep_if { |x| x[0] != 0 }.first
|
136
|
+
else
|
137
|
+
# Was horizontal, perpendicular is vertical.
|
138
|
+
perpendicular_nearest_edge = sorted_edges.keep_if { |x| x[0] == 0 }.first
|
139
|
+
end
|
140
|
+
|
141
|
+
cur_geocells.concat(
|
142
|
+
cur_geocells.map { |cell| Geomodel::GeoCell.adjacent(cell, perpendicular_nearest_edge) }
|
143
|
+
)
|
144
|
+
end
|
145
|
+
|
146
|
+
# We don't have enough items yet, keep searching.
|
147
|
+
next if results.size < max_results
|
148
|
+
|
149
|
+
# If the currently max_results'th closest item is closer than any
|
150
|
+
# of the next test geocells, we're done searching.
|
151
|
+
current_farthest_returnable_result_dist = Geomodel::Math.distance(center, results[max_results - 1][0].location)
|
152
|
+
break if (closest_possible_next_result_dist >= current_farthest_returnable_result_dist)
|
153
|
+
end
|
154
|
+
|
155
|
+
results[0...max_results].keep_if { |result| max_distance == 0 || result.last < max_distance }
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|