geospatial 0.0.1 → 1.0.0

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,153 @@
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_relative 'location'
22
+ require_relative 'box'
23
+ require_relative 'filter'
24
+ require_relative 'hilbert/traverse'
25
+
26
+ module Geospatial
27
+ # A point is a location on a map with a specific hash representation based on the map. A point might store multi-dimentional data (e.g. longitude, latitude, time) which is hashed to a single column.
28
+ class Point
29
+ def initialize(map, coordinates, object = nil)
30
+ @map = map
31
+ @coordinates = coordinates
32
+ @object = object
33
+ end
34
+
35
+ attr :object
36
+
37
+ def [] index
38
+ @coordinates[index]
39
+ end
40
+
41
+ def []= index, value
42
+ @coordinates[index] = value
43
+ end
44
+
45
+ attr :coordinates
46
+
47
+ def eql?(other)
48
+ self.class.eql?(other.class) and @coordinates.eql?(other.coordinates)
49
+ end
50
+
51
+ def hash
52
+ @hash ||= @map.hash_for_coordinates(@coordinates).freeze
53
+ end
54
+ end
55
+
56
+ class Map
57
+ def self.for_earth(order = 20)
58
+ self.new(Hilbert::Curve.new(Dimensions.for_earth, order))
59
+ end
60
+
61
+ def initialize(curve)
62
+ @curve = curve
63
+ @points = []
64
+ @bounds = nil
65
+ end
66
+
67
+ def order
68
+ @curve.order
69
+ end
70
+
71
+ def bounds
72
+ unless @bounds
73
+ origin = @curve.origin
74
+ size = @curve.size
75
+
76
+ @bounds = Box.new(origin, size)
77
+ end
78
+
79
+ return @bounds
80
+ end
81
+
82
+ attr :points
83
+
84
+ def hash_for_coordinates(coordinates)
85
+ @curve.map(coordinates)
86
+ end
87
+
88
+ def point_for_hash(hash)
89
+ Point.new(self, @curve.unmap(hash))
90
+ end
91
+
92
+ def point_for_coordinates(coordinates, object = nil)
93
+ Point.new(self, coordinates, object)
94
+ end
95
+
96
+ def point_for_object(object)
97
+ Point.new(self, object.to_a, object)
98
+ end
99
+
100
+ def << object
101
+ @points << point_for_coordinates(object.to_a, object)
102
+
103
+ return self
104
+ end
105
+
106
+ def count
107
+ @points.count
108
+ end
109
+
110
+ def sort!
111
+ @points.sort_by!(&:hash)
112
+ end
113
+
114
+ def query(region, **options)
115
+ filter = filter_for(region, **options)
116
+
117
+ return filter.apply(@points).map(&:object)
118
+ end
119
+
120
+ def traverse(region, depth: 0)
121
+ @curve.traverse do |child_origin, child_size, prefix, order|
122
+ child = Box.new(Vector.elements(child_origin), Vector.elements(child_size))
123
+
124
+ # puts "Considering (order=#{order}) #{child.inspect}..."
125
+
126
+ if region.intersect?(child)
127
+ if order == depth # at bottom
128
+ # puts "at bottom -> found prefix #{prefix.to_s(2)} (#{child.inspect})"
129
+ yield(child, prefix, order); :skip
130
+ elsif region.include?(child)
131
+ # puts "include child -> found prefix #{prefix.to_s(2)} (#{child.inspect})"
132
+ yield(child, prefix, order); :skip
133
+ else
134
+ # puts "going deeper..."
135
+ end
136
+ else
137
+ # puts "out of bounds."
138
+ :skip
139
+ end
140
+ end
141
+ end
142
+
143
+ def filter_for(region, **options)
144
+ filter = Filter.new
145
+
146
+ traverse(region, **options) do |child, prefix, order|
147
+ filter.add(prefix, order)
148
+ end
149
+
150
+ return filter
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,115 @@
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_relative 'box'
22
+
23
+ module Geospatial
24
+ class Polygon
25
+ def self.[] *points
26
+ self.new(points)
27
+ end
28
+
29
+ def initialize(points)
30
+ @points = points
31
+ @box = nil
32
+ end
33
+
34
+ def to_s
35
+ "#{self.class}#{@points.inspect}"
36
+ end
37
+
38
+ def bounding_box
39
+ @bounding_box ||= Box.enclosing_points(@points)
40
+ end
41
+
42
+ def freeze
43
+ @points.freeze
44
+ bounding_box.freeze
45
+
46
+ super
47
+ end
48
+
49
+ def edges
50
+ return to_enum(:edges) unless block_given?
51
+
52
+ previous = @points.last
53
+ @points.each do |point|
54
+ yield previous, point
55
+ previous = point
56
+ end
57
+ end
58
+
59
+ def self.is_left(p0, p1, p2)
60
+ a = p1 - p0
61
+ b = p2 - p0
62
+
63
+ return (a[0] * b[1]) - (b[0] * a[1])
64
+ end
65
+
66
+ # Test a 2D point for inclusion in the polygon.
67
+ # @param [Vector] p The point to test.
68
+ # @return [Number] The number of times the polygon winds around the point (0 if outside).
69
+ def winding_number(p)
70
+ count = 0
71
+
72
+ edges.each do |pa, pb|
73
+ if pa[1] <= p[1]
74
+ if pb[1] >= p[1] and Polygon.is_left(pa, pb, p) > 0
75
+ count += 1
76
+ end
77
+ else
78
+ if pb[1] <= p[1] and Polygon.is_left(pa, pb, p) < 0
79
+ count -= 1
80
+ end
81
+ end
82
+
83
+ end
84
+
85
+ return count
86
+ end
87
+
88
+ def include_point?(point)
89
+ return false unless bounding_box.include_point?(point)
90
+
91
+ self.winding_number(point) == 1
92
+ end
93
+
94
+ def intersect_with_box?(other)
95
+ return true if @points.any?{|point| other.include_point?(point)}
96
+
97
+ return true if other.corners.any?{|corner| self.include_point?(corner)}
98
+
99
+ return false
100
+ end
101
+
102
+ def intersect?(other)
103
+ case other
104
+ when Box
105
+ intersect_with_box?(other)
106
+ when Circle
107
+ intersect_with_circle?(other)
108
+ end
109
+ end
110
+
111
+ def include?(other)
112
+ other.corners.all?{|corner| self.include_point?(corner)}
113
+ end
114
+ end
115
+ end
@@ -1,3 +1,23 @@
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
+
1
21
  module Geospatial
2
- VERSION = "0.0.1"
22
+ VERSION = "1.0.0"
3
23
  end
data/lib/geospatial.rb CHANGED
@@ -1,4 +1,23 @@
1
- require "geospatial/version"
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.
2
20
 
3
- module Geospatial
4
- end
21
+ require_relative "geospatial/version"
22
+
23
+ require_relative "geospatial/location"
@@ -0,0 +1,42 @@
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 'geospatial/map'
22
+
23
+ module Geospatial::BoxSpec
24
+ describe Geospatial::Box do
25
+ let(:square) {Geospatial::Box.new(Vector[-1, -1], Vector[2, 2])}
26
+
27
+ # it "convert to integral" do
28
+ # expect(square.to_integral([-1, -1], 4)).to be == [0, 0]
29
+ # end
30
+ #
31
+ # it "convert from integral" do
32
+ # expect(square.from_integral([0, 0], 4)).to be == [-1, -1]
33
+ # end
34
+
35
+ let(:new_zealand) {Geospatial::Box.from_bounds(Vector[166.0, -48.0], Vector[180.0, -34.0])}
36
+ let(:child) {Geospatial::Box.new(Vector[135.0, -67.5], Vector[45.0, 22.5])}
37
+
38
+ it "should intersect" do
39
+ expect(new_zealand).to be_intersect(child)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,76 @@
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 'geospatial/circle'
22
+ require_relative 'visualization'
23
+
24
+ RSpec.describe Geospatial::Circle do
25
+ let(:lake_tekapo) {Geospatial::Location.new(170.53, -43.89)}
26
+ let(:lake_alex) {Geospatial::Location.new(170.45, -43.94)}
27
+ let(:sydney) {Geospatial::Location.new(151.21, -33.85)}
28
+
29
+ let(:new_zealand) {Geospatial::Box.from_bounds(Vector[166.0, -48.0], Vector[180.0, -34.0])}
30
+ let(:australia) {Geospatial::Box.from_bounds(Vector[112.0, -45.0], Vector[155.0, -10.0])}
31
+
32
+ let(:circle_lake_tekapo) {Geospatial::Circle.new(lake_tekapo, 10_000)}
33
+ let(:circle_lake_alex) {Geospatial::Circle.new(lake_tekapo, 10_000)}
34
+ let(:circle_sydney) {Geospatial::Circle.new(sydney, 10_000)}
35
+ let(:circle_new_zealand) {Geospatial::Circle.new(lake_tekapo, 1_000_000)}
36
+
37
+ it "should intersect circles" do
38
+ expect(circle_lake_tekapo).to be_intersect(circle_lake_alex)
39
+ end
40
+
41
+ it "should not intersect distant circles" do
42
+ expect(circle_lake_tekapo).to_not be_intersect(circle_sydney)
43
+ end
44
+
45
+ it "should include circles" do
46
+ expect(circle_new_zealand).to be_include(circle_lake_tekapo)
47
+ expect(circle_new_zealand).to be_include(circle_lake_alex)
48
+ end
49
+
50
+ it "should not include distant circles" do
51
+ expect(circle_new_zealand).to_not be_include(circle_sydney)
52
+ end
53
+
54
+ it "can generate visualisation" do
55
+ map = Geospatial::Map.for_earth
56
+
57
+ map << lake_tekapo
58
+
59
+ circle = Geospatial::Circle.new(lake_tekapo, 100_000)
60
+
61
+ Geospatial::Visualization.for_map(map) do |pdf, origin|
62
+ #count = 0
63
+ map.traverse(circle, depth: map.order - 10) do |child, prefix, order|
64
+ #count += 1
65
+ size = child.size
66
+ top_left = (origin + child.min) + Vector[0, size[1]]
67
+ pdf.rectangle(top_left.to_a, *size.to_a)
68
+ # puts "#{top_left.to_a} #{size.to_a}"
69
+ end
70
+
71
+ #puts "count=#{count}"
72
+
73
+ pdf.fill_and_stroke
74
+ end.render_file "circle.pdf"
75
+ end
76
+ end
@@ -0,0 +1,35 @@
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 'geospatial/dimensions'
22
+
23
+ RSpec.describe Geospatial::Dimension.new(-180.0, 360.0) do
24
+ it "should map values" do
25
+ expect(subject.map(-180)).to be == 0.0
26
+ expect(subject.map(0)).to be == 0.5
27
+ expect(subject.map(180)).to be == 1.0
28
+ end
29
+
30
+ it "should unmap values" do
31
+ expect(subject.unmap(0.0)).to be == -180
32
+ expect(subject.unmap(0.5)).to be == 0
33
+ expect(subject.unmap(1.0)).to be == 180
34
+ end
35
+ end
@@ -0,0 +1,59 @@
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 'geospatial/location'
22
+ require 'geospatial/hilbert/curve'
23
+
24
+ RSpec.shared_examples_for "invertible function" do |input|
25
+ it "should round-trip coordinates" do
26
+ mapped = subject.map(input)
27
+ output = subject.unmap(mapped)
28
+
29
+ input.each_with_index do |value, index|
30
+ expect(output[index]).to be_within(tolerance).of(value)
31
+ end
32
+ end
33
+ end
34
+
35
+ RSpec.shared_context "earth invertible curve" do
36
+ it_behaves_like "invertible function", [-180, -90]
37
+ it_behaves_like "invertible function", [0, 0]
38
+ it_behaves_like "invertible function", [179, 89]
39
+
40
+ it_behaves_like "invertible function", [170.53, -43.89]
41
+ end
42
+
43
+ RSpec.describe Geospatial::Hilbert::Curve.new(Geospatial::Dimensions.for_earth, 6) do
44
+ let(:tolerance) {10.0}
45
+
46
+ include_context "earth invertible curve"
47
+ end
48
+
49
+ RSpec.describe Geospatial::Hilbert::Curve.new(Geospatial::Dimensions.for_earth, 8) do
50
+ let(:tolerance) {1.0}
51
+
52
+ include_context "earth invertible curve"
53
+ end
54
+
55
+ RSpec.describe Geospatial::Hilbert::Curve.new(Geospatial::Dimensions.for_earth, 32) do
56
+ let(:tolerance) {0.000001}
57
+
58
+ include_context "earth invertible curve"
59
+ end
@@ -0,0 +1,63 @@
1
+
2
+ require 'geospatial/hilbert/traverse'
3
+
4
+ require_relative 'sorted'
5
+
6
+ RSpec.describe Geospatial::Hilbert::Curve do
7
+ let(:dimensions) {Geospatial::Dimensions.from_ranges(0..4, 0..4)}
8
+ subject {Geospatial::Hilbert::Curve.new(dimensions, 2)}
9
+
10
+ it "traverses and generates all prefixes" do
11
+ items = subject.traverse.to_a
12
+
13
+ # 4 items of order 1, 16 items of order 0.
14
+ expect(items.size).to be == (4 + 16)
15
+
16
+ expect(items[0]).to be == [[0, 0], [2, 2], 0b0000, 1]
17
+ expect(items[1]).to be == [[0, 0], [1, 1], 0b0000, 0]
18
+ expect(items[2]).to be == [[1, 0], [1, 1], 0b0001, 0]
19
+ expect(items[3]).to be == [[1, 1], [1, 1], 0b0010, 0]
20
+ expect(items[4]).to be == [[0, 1], [1, 1], 0b0011, 0]
21
+
22
+ expect(items[5]).to be == [[0, 2], [2, 2], 0b0100, 1]
23
+ expect(items[6]).to be == [[0, 2], [1, 1], 0b0100, 0]
24
+ expect(items[7]).to be == [[0, 3], [1, 1], 0b0101, 0]
25
+ expect(items[8]).to be == [[1, 3], [1, 1], 0b0110, 0]
26
+ expect(items[9]).to be == [[1, 2], [1, 1], 0b0111, 0]
27
+
28
+ expect(items[10]).to be == [[2, 2], [2, 2], 0b1000, 1]
29
+ expect(items[11]).to be == [[2, 2], [1, 1], 0b1000, 0]
30
+ expect(items[12]).to be == [[2, 3], [1, 1], 0b1001, 0]
31
+ expect(items[13]).to be == [[3, 3], [1, 1], 0b1010, 0]
32
+ expect(items[14]).to be == [[3, 2], [1, 1], 0b1011, 0]
33
+
34
+ # This result here might seem a bit unexpected, but it's correct. For a 2-dimentional curve, the origin of the quadrant isn't always the same as the origin of the curve in that region. If you draw a 2D curve of order 2, you will see that the origin of the first 3 sub-curves is in the bottom left, but the last sub-curve has it's origin in the top right.
35
+ expect(items[15]).to be == [[2, 0], [2, 2], 0b1100, 1]
36
+ expect(items[16]).to be == [[3, 1], [1, 1], 0b1100, 0]
37
+ expect(items[17]).to be == [[2, 1], [1, 1], 0b1101, 0]
38
+ expect(items[18]).to be == [[2, 0], [1, 1], 0b1110, 0]
39
+ expect(items[19]).to be == [[3, 0], [1, 1], 0b1111, 0]
40
+
41
+ prefixes = items.collect{|item| item[2]}
42
+ expect(prefixes).to be_sorted
43
+ end
44
+ end
45
+
46
+ RSpec.describe Geospatial::Hilbert::Curve do
47
+ let(:order) {4}
48
+ let(:divisions) {2**order}
49
+ let(:dimensions) {Geospatial::Dimensions.from_ranges(0..divisions, 0..divisions)}
50
+ subject {Geospatial::Hilbert::Curve.new(dimensions, order)}
51
+
52
+ it "traverses and generates valid matching hashes" do
53
+ subject.traverse do |origin, size, prefix, depth|
54
+ if depth == 0
55
+ index = Geospatial::OrdinalIndex.new(origin.collect(&:to_i), order).to_hilbert
56
+
57
+ # puts "Child origin=#{origin.inspect} prefix=#{prefix.to_s(2)} order=#{order}"
58
+
59
+ expect(prefix).to be == index.to_i
60
+ end
61
+ end
62
+ end
63
+ end