geomodel 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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