geospatial 0.0.1 → 1.0.0

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