geomodel 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.
@@ -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
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --format documentation
@@ -0,0 +1 @@
1
+ geomodel
@@ -0,0 +1 @@
1
+ 2.0.0
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ notifications:
6
+ email:
7
+ - bsbodden@integrallis.com
@@ -0,0 +1,2 @@
1
+ ### 0.0.1 (January 2, 2014)
2
+ * Initial release - Straight port from Python, Java and JS implementations
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in geomodel.gemspec
4
+ gemspec
@@ -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.
@@ -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
@@ -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
@@ -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
@@ -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