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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2a5d010173b1e91d60be3e2c07e0e89a95ccb000
4
- data.tar.gz: 5df99174a2639bc48dca1b84f26a149e2c8b3e92
3
+ metadata.gz: 2f5c7adeba4a6c4c4773d3f05ce924da0287e1b4
4
+ data.tar.gz: f927b7952f48e67dd30df8c6076aa4d590d8f5cc
5
5
  SHA512:
6
- metadata.gz: c4eb89aa1b47b77313d8c68abaa95d2d83660b040dead60be19823d69fc5aeb0b4c67d529caf573e677ab4eba7496673c3859797bafc2f0ba0cec3fd52bc31c9
7
- data.tar.gz: 3177ebab37d176ec50b98db396ce0cb964a071e5f8994ae2d14f1da00d0ca4023a7aac13126cfaca96c5870cbae02d5b0544f321b79bde38793521edaf2057f8
6
+ metadata.gz: bb6ad414765df8a2c66706c1b157b51355b0e717fd1a9e779455bffc10c8d8a338c0f9c27171ca5023d78d5824414fe2c919be315c277380f4bebb73e6bda4c2
7
+ data.tar.gz: 4f1f8d46e132ec47cbb07ee5c361b5215088e816514e254d85f40405e8fb5fa0f77fd32cc1d696f73ac0c0a424a2dd7ebc1db6f5fb4c4136a3b10dd20289990a
data/.gitignore CHANGED
@@ -20,3 +20,4 @@ tmp
20
20
  *.o
21
21
  *.a
22
22
  mkmf.log
23
+ .tags*
data/.travis.yml CHANGED
@@ -1,5 +1,14 @@
1
1
  language: ruby
2
+ sudo: false
2
3
  rvm:
3
- - "2.0"
4
- - "2.1"
4
+ - 2.1.8
5
+ - 2.2.4
6
+ - 2.3.0
7
+ - ruby-head
8
+ - rbx-2
5
9
  env: COVERAGE=true
10
+ matrix:
11
+ fast_finish: true
12
+ allow_failures:
13
+ - rvm: "ruby-head"
14
+ - rvm: "rbx-2"
data/Gemfile CHANGED
@@ -1,4 +1,15 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in geospatial.gemspec
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
- Geospatial provides abstractions for dealing with geographical locations efficiently.
3
+ ![Australia Hilbert Curve](australia.png?raw=true "Australia Hilbert Curve Visualisation")
4
4
 
5
- [![Build Status](https://secure.travis-ci.org/ioquatix/geospatial.png)](http://travis-ci.org/ioquatix/geospatial)
6
- [![Code Climate](https://codeclimate.com/github/ioquatix/geospatial.png)](https://codeclimate.com/github/ioquatix/geospatial)
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
- gem 'geospatial'
21
+ gem 'geospatial'
14
22
 
15
23
  And then execute:
16
24
 
17
- $ bundle
25
+ $ bundle
18
26
 
19
27
  Or install it yourself as:
20
28
 
21
- $ gem install geospatial
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, 2015, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
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.1.0"
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