geospatial 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +11 -2
- data/Gemfile +12 -1
- data/README.md +37 -8
- data/Rakefile +11 -0
- data/australia.png +0 -0
- data/geospatial.gemspec +1 -1
- data/lib/geospatial/box.rb +126 -0
- data/lib/geospatial/circle.rb +109 -0
- data/lib/geospatial/dimensions.rb +130 -0
- data/lib/geospatial/filter.rb +82 -0
- data/lib/geospatial/hilbert/curve.rb +79 -0
- data/lib/geospatial/hilbert/traverse.rb +65 -0
- data/lib/geospatial/hilbert.rb +111 -90
- data/lib/geospatial/index.rb +80 -0
- data/lib/geospatial/interleave.rb +69 -0
- data/lib/geospatial/location.rb +53 -32
- data/lib/geospatial/map/index.rb +57 -0
- data/lib/geospatial/map.rb +153 -0
- data/lib/geospatial/polygon.rb +115 -0
- data/lib/geospatial/version.rb +21 -1
- data/lib/geospatial.rb +22 -3
- data/spec/geospatial/box_spec.rb +42 -0
- data/spec/geospatial/circle_spec.rb +76 -0
- data/spec/geospatial/dimensions_spec.rb +35 -0
- data/spec/geospatial/hilbert_curve_spec.rb +59 -0
- data/spec/geospatial/hilbert_traverse_spec.rb +63 -0
- data/spec/geospatial/index_spec.rb +113 -0
- data/spec/{geoquery → geospatial}/location_spec.rb +4 -4
- data/spec/geospatial/map_index_spec.rb +41 -0
- data/spec/geospatial/map_spec.rb +71 -0
- data/spec/geospatial/polygon_spec.rb +96 -0
- data/spec/geospatial/sorted.rb +6 -0
- data/spec/geospatial/visualization.rb +46 -0
- data/spec/geospatial/world.png +0 -0
- metadata +43 -9
- data/spec/geoquery/hilbert_spec.rb +0 -72
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2f5c7adeba4a6c4c4773d3f05ce924da0287e1b4
|
4
|
+
data.tar.gz: f927b7952f48e67dd30df8c6076aa4d590d8f5cc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb6ad414765df8a2c66706c1b157b51355b0e717fd1a9e779455bffc10c8d8a338c0f9c27171ca5023d78d5824414fe2c919be315c277380f4bebb73e6bda4c2
|
7
|
+
data.tar.gz: 4f1f8d46e132ec47cbb07ee5c361b5215088e816514e254d85f40405e8fb5fa0f77fd32cc1d696f73ac0c0a424a2dd7ebc1db6f5fb4c4136a3b10dd20289990a
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
@@ -1,4 +1,15 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
|
-
# Specify your gem's dependencies in
|
3
|
+
# Specify your gem's dependencies in build.gemspec
|
4
4
|
gemspec
|
5
|
+
|
6
|
+
group :development do
|
7
|
+
gem 'pry'
|
8
|
+
end
|
9
|
+
|
10
|
+
group :test do
|
11
|
+
gem 'simplecov'
|
12
|
+
gem 'coveralls', require: false
|
13
|
+
|
14
|
+
gem "prawn"
|
15
|
+
end
|
data/README.md
CHANGED
@@ -1,28 +1,57 @@
|
|
1
1
|
# Geospatial
|
2
2
|
|
3
|
-
|
3
|
+
![Australia Hilbert Curve](australia.png?raw=true "Australia Hilbert Curve Visualisation")
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
Geospatial provides abstractions for dealing with geographical locations efficiently. It is not a generic point/line/polygon handling library like [RGeo](https://github.com/rgeo/rgeo), but a specially crafted library to deal with querying for points on a map efficiently.
|
6
|
+
|
7
|
+
[![Build Status](https://secure.travis-ci.org/ioquatix/geospatial.svg)](http://travis-ci.org/ioquatix/geospatial)
|
8
|
+
[![Code Climate](https://codeclimate.com/github/ioquatix/geospatial.svg)](https://codeclimate.com/github/ioquatix/geospatial)
|
7
9
|
[![Coverage Status](https://coveralls.io/repos/ioquatix/geospatial/badge.svg)](https://coveralls.io/r/ioquatix/geospatial)
|
8
10
|
|
11
|
+
## Motivation
|
12
|
+
|
13
|
+
We had a need to query a database of places efficiently using SQLite. We did some investigation and found that SQLite (at least at the time) couldn't use composite indexes efficiently. Our testing revealed that MySQL also didn't really do well with large amounts of data. We had a table with 5Gb of data, and 15Gb of indexes. Crazy.
|
14
|
+
|
15
|
+
After researching geospatial hashing algorithms, I found [this blog post](http://blog.notdot.net/2009/11/Damn-Cool-Algorithms-Spatial-indexing-with-Quadtrees-and-Hilbert-Curves) and decided to implement a geospatial hash using the Hilbert curve. This library exposes a fast indexing and querying mechanism based on Hilbert curves, for points on a map, which can be integrated into a database or other systems as required.
|
16
|
+
|
9
17
|
## Installation
|
10
18
|
|
11
19
|
Add this line to your application's Gemfile:
|
12
20
|
|
13
|
-
|
21
|
+
gem 'geospatial'
|
14
22
|
|
15
23
|
And then execute:
|
16
24
|
|
17
|
-
|
25
|
+
$ bundle
|
18
26
|
|
19
27
|
Or install it yourself as:
|
20
28
|
|
21
|
-
|
29
|
+
$ gem install geospatial
|
22
30
|
|
23
31
|
## Usage
|
24
32
|
|
25
|
-
|
33
|
+
The simplest way to use this library is to use the built in `Map`:
|
34
|
+
|
35
|
+
map = Geospatial::Map.new
|
36
|
+
map << Geospatial::Location.new(170.53, -43.89) # Lake Tekapo, New Zealand.
|
37
|
+
map << Geospatial::Location.new(170.45, -43.94) # Lake Alex, New Zealand.
|
38
|
+
map << Geospatial::Location.new(151.21, -33.85) # Sydney, Australia.
|
39
|
+
|
40
|
+
map.sort! # or assume an ordered database index.
|
41
|
+
|
42
|
+
new_zealand = Geospatial::Box.from_bounds(Vector[166.0, -48.0], Vector[180.0, -34.0])
|
43
|
+
|
44
|
+
points = subject.query(new_zealand)
|
45
|
+
expect(points).to include(lake_tekapo, lake_alex)
|
46
|
+
expect(points).to_not include(sydney)
|
47
|
+
|
48
|
+
At a lower level you can use the method in the `Geospatial::Hilbert` module to `map`, `unmap` and `traverse` the Hilbert mapping.
|
49
|
+
|
50
|
+
### Geotemporal Indexes
|
51
|
+
|
52
|
+
The Hilbert curve is multi-dimensional and therefore can represent multi-dimensional data, e.g. latitude, longitude and time, in a single index. The curve expands uniformly in all dimensions, so you can't control the precision of the dimensions independently.
|
53
|
+
|
54
|
+
Mathematically speaking, it's possible to compose curves together to form curves of different precision/properties. However, how these fit together generally is a bit more complex, especially in terms of exploring the curve via traversal.
|
26
55
|
|
27
56
|
## Contributing
|
28
57
|
|
@@ -36,7 +65,7 @@ Or install it yourself as:
|
|
36
65
|
|
37
66
|
Released under the MIT license.
|
38
67
|
|
39
|
-
Copyright,
|
68
|
+
Copyright, 2016, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
|
40
69
|
|
41
70
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
42
71
|
of this software and associated documentation files (the "Software"), to deal
|
data/Rakefile
CHANGED
@@ -6,3 +6,14 @@ RSpec::Core::RakeTask.new(:spec) do |task|
|
|
6
6
|
end
|
7
7
|
|
8
8
|
task :default => :spec
|
9
|
+
|
10
|
+
task :console do
|
11
|
+
require 'pry'
|
12
|
+
|
13
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
14
|
+
|
15
|
+
require 'geospatial'
|
16
|
+
require 'geospatial/hilbert'
|
17
|
+
|
18
|
+
Pry.start
|
19
|
+
end
|
data/australia.png
ADDED
Binary file
|
data/geospatial.gemspec
CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
18
|
spec.require_paths = ["lib"]
|
19
19
|
|
20
|
-
spec.add_development_dependency "rspec", "~> 3.
|
20
|
+
spec.add_development_dependency "rspec", "~> 3.4"
|
21
21
|
|
22
22
|
spec.add_development_dependency "bundler", "~> 1.6"
|
23
23
|
spec.add_development_dependency "rake"
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# Copyright, 2015, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'matrix'
|
22
|
+
|
23
|
+
module Geospatial
|
24
|
+
class Box
|
25
|
+
class << self
|
26
|
+
def from_bounds(min, max)
|
27
|
+
self.new(min, max-min, max)
|
28
|
+
end
|
29
|
+
|
30
|
+
alias [] from_bounds
|
31
|
+
|
32
|
+
def enclosing_points(points)
|
33
|
+
return nil unless points.any?
|
34
|
+
|
35
|
+
min = points.first.to_a
|
36
|
+
max = points.first.to_a
|
37
|
+
|
38
|
+
points.each do |point|
|
39
|
+
point.each_with_index do |value, index|
|
40
|
+
if value < min[index]
|
41
|
+
min[index] = value
|
42
|
+
elsif value > max[index]
|
43
|
+
max[index] = value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
return self.from_bounds(Vector.elements(min), Vector.elements(max))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(origin, size, max = nil)
|
53
|
+
@origin = origin
|
54
|
+
@size = size
|
55
|
+
@max = max
|
56
|
+
end
|
57
|
+
|
58
|
+
def freeze
|
59
|
+
self.max
|
60
|
+
|
61
|
+
super
|
62
|
+
end
|
63
|
+
|
64
|
+
attr :origin
|
65
|
+
attr :size
|
66
|
+
|
67
|
+
def to_s
|
68
|
+
"#{self.class}[#{min.inspect}, #{max.inspect}]"
|
69
|
+
end
|
70
|
+
|
71
|
+
def min
|
72
|
+
@origin
|
73
|
+
end
|
74
|
+
|
75
|
+
def max
|
76
|
+
@max ||= @origin + @size
|
77
|
+
end
|
78
|
+
|
79
|
+
# This yields the four corners of the box.
|
80
|
+
def corners
|
81
|
+
return to_enum(:corners) unless block_given?
|
82
|
+
|
83
|
+
yield(@origin)
|
84
|
+
|
85
|
+
max = self.max
|
86
|
+
yield(Vector[max[0], @origin[1]])
|
87
|
+
yield(max)
|
88
|
+
yield(Vector[@origin[0], max[1]])
|
89
|
+
end
|
90
|
+
|
91
|
+
# This yields the midpoints of the four sides of the box.
|
92
|
+
def midpoints
|
93
|
+
return to_enum(:midpoints) unless block_given?
|
94
|
+
|
95
|
+
size = self.size
|
96
|
+
|
97
|
+
yield(Vector[@origin[0] + size[0] / 2, @origin[1]])
|
98
|
+
yield(Vector[@origin[0] + size[0], @origin[1] + size[1] / 2])
|
99
|
+
yield(Vector[@origin[0] + size[0] / 2, @origin[1] + size[1]])
|
100
|
+
yield(Vector[@origin[0], @origin[1] + size[1] / 2])
|
101
|
+
end
|
102
|
+
|
103
|
+
def include_point?(point)
|
104
|
+
2.times do |i|
|
105
|
+
return false if point[i] < min[i] or point[i] >= max[i]
|
106
|
+
end
|
107
|
+
|
108
|
+
return true
|
109
|
+
end
|
110
|
+
|
111
|
+
def include?(other)
|
112
|
+
include_point?(other.min) && include_point?(other.max)
|
113
|
+
end
|
114
|
+
|
115
|
+
def intersect?(other)
|
116
|
+
2.times do |i|
|
117
|
+
# Separating axis theorm, if the minimum of the other is past the maximum of self, or the maximum of other is less than the minimum of self, an intersection cannot occur.
|
118
|
+
if other.min[i] > self.max[i] or other.max[i] < self.min[i]
|
119
|
+
return false
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
return true
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# Copyright, 2015, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'matrix'
|
22
|
+
|
23
|
+
module Geospatial
|
24
|
+
# A circle is a geometric primative where the center is a location and the radius is in meters.
|
25
|
+
class Circle
|
26
|
+
class << self
|
27
|
+
alias [] new
|
28
|
+
end
|
29
|
+
|
30
|
+
# Center must be a vector, radius must be a numeric value.
|
31
|
+
def initialize(center, radius)
|
32
|
+
@center = center
|
33
|
+
@radius = radius
|
34
|
+
end
|
35
|
+
|
36
|
+
attr :center
|
37
|
+
attr :radius
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
"#{self.class}[#{@center}, #{@radius}]"
|
41
|
+
end
|
42
|
+
|
43
|
+
def distance_from(point)
|
44
|
+
Location.new(point[0], point[1]).distance_from(@center)
|
45
|
+
end
|
46
|
+
|
47
|
+
def include_point?(point, radius = @radius)
|
48
|
+
distance_from(point) <= radius
|
49
|
+
end
|
50
|
+
|
51
|
+
def include_box?(other)
|
52
|
+
# We must contain the for corners of the other box:
|
53
|
+
other.corners do |corner|
|
54
|
+
return false unless include_point?(corner)
|
55
|
+
end
|
56
|
+
|
57
|
+
return true
|
58
|
+
end
|
59
|
+
|
60
|
+
def include_circle?(other)
|
61
|
+
# We must be big enough to contain the other point:
|
62
|
+
@radius >= other.radius && include_point?(other.center.to_a, @radius - other.radius)
|
63
|
+
end
|
64
|
+
|
65
|
+
def include?(other)
|
66
|
+
case other
|
67
|
+
when Box
|
68
|
+
include_box?(other)
|
69
|
+
when Circle
|
70
|
+
include_circle?(other)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def intersect?(other)
|
75
|
+
case other
|
76
|
+
when Box
|
77
|
+
intersect_with_box?(other)
|
78
|
+
when Circle
|
79
|
+
intersect_with_circle?(other)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def midpoints
|
84
|
+
@bounds ||= @center.bounding_box(@radius)
|
85
|
+
|
86
|
+
yield([@bounds[:longitude].begin, @center.latitude])
|
87
|
+
yield([@bounds[:longitude].end, @center.latitude])
|
88
|
+
yield([@center.longitude, @bounds[:latitude].begin])
|
89
|
+
yield([@center.longitude, @bounds[:latitude].end])
|
90
|
+
end
|
91
|
+
|
92
|
+
def intersect_with_box?(other)
|
93
|
+
# If we contain any of the four corners:
|
94
|
+
other.corners do |corner|
|
95
|
+
return true if include_point?(corner)
|
96
|
+
end
|
97
|
+
|
98
|
+
midpoints do |midpoint|
|
99
|
+
return true if other.include_point?(midpoint)
|
100
|
+
end
|
101
|
+
|
102
|
+
return false
|
103
|
+
end
|
104
|
+
|
105
|
+
def intersect_with_circle?(other)
|
106
|
+
include_point?(other.center.to_a, @radius + other.radius)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'matrix'
|
22
|
+
|
23
|
+
module Geospatial
|
24
|
+
# An integral dimension which maps a continuous space into an integral space. The scale is the maximum integral unit.
|
25
|
+
class Dimension
|
26
|
+
def initialize(origin, size, scale = 1.0)
|
27
|
+
@origin = origin
|
28
|
+
@size = size
|
29
|
+
@scale = scale
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
if @scale != 1.0
|
34
|
+
"(#{min}..#{max} * #{@scale})"
|
35
|
+
else
|
36
|
+
"(#{min}..#{max})"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def * factor
|
41
|
+
self.class.new(@origin, @size, @scale * factor)
|
42
|
+
end
|
43
|
+
|
44
|
+
attr :origin
|
45
|
+
attr :size
|
46
|
+
attr :scale
|
47
|
+
|
48
|
+
def min
|
49
|
+
@origin
|
50
|
+
end
|
51
|
+
|
52
|
+
def max
|
53
|
+
@origin + @size
|
54
|
+
end
|
55
|
+
|
56
|
+
# Normalize the value into the range 0..1 and then multiply by scale.
|
57
|
+
def map(value)
|
58
|
+
((value - @origin).to_f / @size) * @scale
|
59
|
+
end
|
60
|
+
|
61
|
+
def unmap(value)
|
62
|
+
@origin + (value / @scale) * @size
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class Dimensions
|
67
|
+
def initialize(dimensions)
|
68
|
+
@dimensions = dimensions
|
69
|
+
end
|
70
|
+
|
71
|
+
attr :dimensions
|
72
|
+
|
73
|
+
def to_s
|
74
|
+
"[#{@dimensions.join(', ')}]"
|
75
|
+
end
|
76
|
+
|
77
|
+
def freeze
|
78
|
+
@dimensions.freeze
|
79
|
+
|
80
|
+
super
|
81
|
+
end
|
82
|
+
|
83
|
+
def count
|
84
|
+
return @dimensions.count
|
85
|
+
end
|
86
|
+
|
87
|
+
def * factor
|
88
|
+
self.class.new(@dimensions.collect{|dimension| dimension * factor})
|
89
|
+
end
|
90
|
+
|
91
|
+
def origin
|
92
|
+
@dimensions.collect(&:origin)
|
93
|
+
end
|
94
|
+
|
95
|
+
def size
|
96
|
+
@dimensions.collect(&:size)
|
97
|
+
end
|
98
|
+
|
99
|
+
def scale
|
100
|
+
@dimensions.colect(&:scale)
|
101
|
+
end
|
102
|
+
|
103
|
+
def min
|
104
|
+
@dimensions.collect(&:min)
|
105
|
+
end
|
106
|
+
|
107
|
+
def max
|
108
|
+
@dimensions.collect(&:max)
|
109
|
+
end
|
110
|
+
|
111
|
+
def map(values)
|
112
|
+
@dimensions.zip(values).collect{|d,v| d.map(v)}
|
113
|
+
end
|
114
|
+
|
115
|
+
def unmap(values)
|
116
|
+
@dimensions.zip(values).collect{|d,v| d.unmap(v)}
|
117
|
+
end
|
118
|
+
|
119
|
+
LATITUDE = Dimension.new(-90.0, 180.0).freeze
|
120
|
+
LONGITUDE = Dimension.new(-180.0, 360.0).freeze
|
121
|
+
|
122
|
+
def self.for_earth
|
123
|
+
@for_earth ||= self.new([LONGITUDE, LATITUDE]).freeze
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.from_ranges(*ranges)
|
127
|
+
self.new ranges.collect{|range| Dimension.new(range.min, range.max - range.min)}
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
module Geospatial
|
22
|
+
class Filter
|
23
|
+
class Range
|
24
|
+
def initialize(prefix, order)
|
25
|
+
@min = prefix
|
26
|
+
update_max(prefix, order)
|
27
|
+
end
|
28
|
+
|
29
|
+
attr :min
|
30
|
+
attr :max
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
"#{min.to_s(2)}..#{max.to_s(2)}"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the new max if expansion was possible, or nil otherwise.
|
37
|
+
def expand!(prefix, order)
|
38
|
+
if @max < prefix and prefix == @max+1
|
39
|
+
update_max(prefix, order)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def include?(hash)
|
44
|
+
hash >= min and hash <= max
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def update_max(prefix, order)
|
50
|
+
# We set the RHS of the prefix to 1s, which is the maximum:
|
51
|
+
@max = prefix | ((1 << (order*2)) - 1)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def initialize
|
56
|
+
@ranges = []
|
57
|
+
end
|
58
|
+
|
59
|
+
attr :ranges
|
60
|
+
|
61
|
+
def add(prefix, order)
|
62
|
+
if last = @ranges.last
|
63
|
+
raise ArgumentError.new("Cannot add non-sequential prefix") unless prefix > last.max
|
64
|
+
end
|
65
|
+
|
66
|
+
unless last = @ranges.last and last.expand!(prefix, order)
|
67
|
+
@ranges << Range.new(prefix, order)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def apply(points)
|
72
|
+
# This is a poor implementation.
|
73
|
+
points.select{|point| @ranges.any?{|range| range.include?(point.hash)}}
|
74
|
+
end
|
75
|
+
|
76
|
+
alias & apply
|
77
|
+
|
78
|
+
def include?(point)
|
79
|
+
@ranges.any?{|range| range.include?(point.hash)}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# Copyright, 2015, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative '../dimensions'
|
22
|
+
require_relative '../index'
|
23
|
+
|
24
|
+
module Geospatial
|
25
|
+
module Hilbert
|
26
|
+
class Curve
|
27
|
+
def initialize(dimensions, order = 30)
|
28
|
+
raise ArgumentError("Order #{order} must be positive integer!") unless order >= 1
|
29
|
+
|
30
|
+
# Order is the number of levels of the curve, which is equivalent to the number of bits per dimension.
|
31
|
+
@order = order
|
32
|
+
@scale = 2**@order
|
33
|
+
|
34
|
+
@dimensions = dimensions * @scale
|
35
|
+
@origin = dimensions.origin
|
36
|
+
@size = dimensions.size
|
37
|
+
end
|
38
|
+
|
39
|
+
attr :order
|
40
|
+
attr :scale
|
41
|
+
attr :dimensions
|
42
|
+
|
43
|
+
def origin
|
44
|
+
@dimensions.origin
|
45
|
+
end
|
46
|
+
|
47
|
+
def size
|
48
|
+
@dimensions.size
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_s
|
52
|
+
"\#<#{self.class} order=#{@order} dimensions=#{@dimensions}>"
|
53
|
+
end
|
54
|
+
|
55
|
+
# This is a helper entry point for traversing Hilbert space.
|
56
|
+
def traverse(&block)
|
57
|
+
if block_given?
|
58
|
+
self.class.traverse_recurse(@order, @rotation, 0, @origin, @size, &block)
|
59
|
+
else
|
60
|
+
self.class.to_enum(:traverse_recurse, @order, @rotation, 0, @origin, @size)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def map(coordinates)
|
65
|
+
axes = @dimensions.map(coordinates).map(&:floor)
|
66
|
+
|
67
|
+
index = OrdinalIndex.new(axes, @order)
|
68
|
+
|
69
|
+
return index.to_hilbert.to_i
|
70
|
+
end
|
71
|
+
|
72
|
+
def unmap(value)
|
73
|
+
index = HilbertIndex.from_integral(value, @dimensions.count, @order)
|
74
|
+
|
75
|
+
return @dimensions.unmap(index.to_ordinal.axes)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|