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.
- 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
@@ -0,0 +1,65 @@
|
|
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 'curve'
|
22
|
+
|
23
|
+
require 'pry'
|
24
|
+
|
25
|
+
module Geospatial
|
26
|
+
module Hilbert
|
27
|
+
class Curve
|
28
|
+
def traverse(&block)
|
29
|
+
return to_enum(:traverse) unless block_given?
|
30
|
+
|
31
|
+
traverse_recurse(@order-1, 0, 0, self.origin, self.size, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def bit_width
|
35
|
+
@dimensions.count
|
36
|
+
end
|
37
|
+
|
38
|
+
# Traversal enumerates all regions of a curve, top-down.
|
39
|
+
def traverse_recurse(order, mask, value, origin, size, &block)
|
40
|
+
half_size = size.collect{|value| value * 0.5}.freeze
|
41
|
+
prefix_mask = (1 << order) | mask
|
42
|
+
|
43
|
+
(2**bit_width).times do |prefix|
|
44
|
+
# These both do the same thing, not sure which one is faster:
|
45
|
+
child_value = (value << @dimensions.count) | prefix
|
46
|
+
prefix = child_value << (order*bit_width)
|
47
|
+
|
48
|
+
index = HilbertIndex.from_integral(prefix, bit_width, @order).to_ordinal
|
49
|
+
|
50
|
+
index = index & prefix_mask
|
51
|
+
|
52
|
+
child_origin = @dimensions.unmap(index.axes).freeze
|
53
|
+
|
54
|
+
# puts "yield(#{child_origin}, #{half_size}, #{prefix}, #{order})"
|
55
|
+
# We avoid calling traverse_recurse simply to hit the callback on the leaf nodes:
|
56
|
+
result = yield child_origin, half_size, prefix, order
|
57
|
+
|
58
|
+
if order > 0 and result != :skip
|
59
|
+
self.traverse_recurse(order - 1, prefix_mask, child_value, child_origin, half_size, &block)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/geospatial/hilbert.rb
CHANGED
@@ -1,106 +1,127 @@
|
|
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.
|
1
20
|
|
2
21
|
module Geospatial
|
3
|
-
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
22
|
+
module Hilbert
|
23
|
+
# Convert between Hilbert index and N-dimensional points.
|
24
|
+
#
|
25
|
+
# The Hilbert index is expressed as an array of transposed bits.
|
26
|
+
#
|
27
|
+
# Example: 5 bits for each of n=3 coordinates.
|
28
|
+
# 15-bit Hilbert integer = A B C D E F G H I J K L M N O is stored
|
29
|
+
# as its Transpose ^
|
30
|
+
# X[0] = A D G J M X[2]| 7
|
31
|
+
# X[1] = B E H K N <-------> | /X[1]
|
32
|
+
# X[2] = C F I L O axes |/
|
33
|
+
# high low 0------> X[0]
|
34
|
+
#
|
35
|
+
# This algorithm is derived from work done by John Skilling and published in "Programming the Hilbert curve".
|
10
36
|
|
11
|
-
#
|
12
|
-
|
13
|
-
#
|
14
|
-
#
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
C = 2 # RIGHT
|
21
|
-
# | 1 | 0 |
|
22
|
-
# | 2 | 3 |
|
23
|
-
|
24
|
-
D = 3 # UP
|
25
|
-
# | 3 | 0 |
|
26
|
-
# | 2 | 1 |
|
27
|
-
|
28
|
-
# This maps the identity rotation/quadrants into their prefix quadrant. The prefix quadrant is the 2 bit number (0 to 3) which identifies along the curve which quadrant the value falls into. This can be computed by looking at how the curve for a given rotation and looking at the correspondence between the identity quadrants and the curve's traversal.
|
29
|
-
ROTATE = [
|
30
|
-
[A, B, C, D], # A is the identity
|
31
|
-
[A, D, C, B], # Map A onto B.
|
32
|
-
[C, D, A, B], # Map A onto C.
|
33
|
-
[C, B, A, D], # Map A onto D.
|
34
|
-
].freeze
|
35
|
-
|
36
|
-
# Rotate quadrant by rotation. The provided quadrant is with respect to the Up rotation.
|
37
|
-
# Note that this function is self-inverting in the sense that rotate(r, rotate(r, x)) == x.
|
38
|
-
def self.rotate(rotation, quadrant)
|
39
|
-
ROTATE[rotation][quadrant]
|
40
|
-
end
|
41
|
-
|
42
|
-
# These prefixes are generated by the following graph:
|
43
|
-
# Rotation | 0 1 2 3 (Prefix)
|
44
|
-
# A | B A A D
|
45
|
-
# B | A B B C
|
46
|
-
# C | D C C B
|
47
|
-
# D | C D D A
|
48
|
-
# We can compute this matrix by looking how the given prefix quadrant maps onto a curve one level down the tree, given the current rotation. We identify that colums 1 and 2 are the same as the input so we take advantage of this by mapping only the columns which are different, i.e. for prefix 0 and 3.
|
49
|
-
|
50
|
-
PREFIX0 = [B, A, D, C].freeze
|
51
|
-
PREFIX3 = [D, C, B, A].freeze
|
52
|
-
|
53
|
-
# Given the current rotation and the prefix quadrant, compute the next rotation one level down the tree.
|
54
|
-
def self.next_rotation(rotation, prefix)
|
55
|
-
if prefix == 0
|
56
|
-
PREFIX0[rotation]
|
57
|
-
elsif prefix == 3
|
58
|
-
PREFIX3[rotation]
|
59
|
-
else
|
60
|
-
rotation
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
# Compute which quadrant this bit is in.
|
65
|
-
def self.normalized_quadrant(x, y, bit_offset)
|
66
|
-
mask = 1 << bit_offset
|
37
|
+
# Convert the Hilbert index into an N-dimensional point expressed as a vector of uints.
|
38
|
+
# @param transposed_index The Hilbert index stored in transposed form.
|
39
|
+
# @param bits Number of bits per coordinate.
|
40
|
+
# @return Coordinate vector.
|
41
|
+
def self.unmap(transposed_index, bits)
|
42
|
+
x = transposed_index.dup #var X = (uint[])transposedIndex.Clone();
|
43
|
+
n = x.length # n: Number of dimensions
|
44
|
+
m = 1 << bits
|
67
45
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
46
|
+
# Gray decode by H ^ (H/2)
|
47
|
+
t = x[n-1] >> 1 # t = X[n - 1] >> 1;
|
48
|
+
|
49
|
+
(n-1).downto(1) {|i| x[i] ^= x[i-1]}
|
50
|
+
x[0] ^= t
|
51
|
+
|
52
|
+
# Undo excess work
|
53
|
+
q = 2
|
54
|
+
while q != m
|
55
|
+
p = q - 1
|
56
|
+
|
57
|
+
i = n - 1
|
58
|
+
while i >= 0
|
59
|
+
if x[i] & q != 0
|
60
|
+
x[0] ^= p # invert
|
61
|
+
else
|
62
|
+
t = (x[0] ^ x[i]) & p;
|
63
|
+
x[0] ^= t;
|
64
|
+
x[i] ^= t;
|
65
|
+
end
|
66
|
+
|
67
|
+
i -= 1
|
79
68
|
end
|
69
|
+
|
70
|
+
q <<= 1
|
80
71
|
end
|
72
|
+
|
73
|
+
return x
|
81
74
|
end
|
82
75
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
76
|
+
# Given the coordinates of a point in N-Dimensional space, find the distance to that point along the Hilbert curve.
|
77
|
+
# That distance will be transposed; broken into pieces and distributed into an array.
|
78
|
+
#
|
79
|
+
# The number of dimensions is the length of the hilbert_index array.
|
80
|
+
# @param hilbert_index Point in N-space.
|
81
|
+
# @param bits Depth of the Hilbert curve. If bits is one, this is the top-level Hilbert curve.
|
82
|
+
# @return The Hilbert distance (or index) as a transposed Hilbert index.
|
83
|
+
def self.map(hilbert_axes, bits)
|
84
|
+
x = hilbert_axes.dup
|
85
|
+
n = x.length # n: Number of dimensions
|
86
|
+
m = 1 << (bits - 1)
|
87
87
|
|
88
|
-
|
89
|
-
|
90
|
-
|
88
|
+
# Inverse undo
|
89
|
+
q = m
|
90
|
+
while q > 1
|
91
|
+
p = q - 1
|
92
|
+
i = 0
|
91
93
|
|
92
|
-
|
93
|
-
|
94
|
+
while i < n
|
95
|
+
if (x[i] & q) != 0
|
96
|
+
x[0] ^= p # invert
|
97
|
+
else
|
98
|
+
t = (x[0] ^ x[i]) & p;
|
99
|
+
x[0] ^= t;
|
100
|
+
x[i] ^= t;
|
101
|
+
end
|
102
|
+
|
103
|
+
i += 1
|
104
|
+
end
|
94
105
|
|
95
|
-
|
96
|
-
|
97
|
-
|
106
|
+
q >>= 1
|
107
|
+
end # exchange
|
108
|
+
|
109
|
+
# Gray encode
|
110
|
+
1.upto(n-1) {|i| x[i] ^= x[i-1]}
|
111
|
+
|
112
|
+
t = 0
|
113
|
+
q = m
|
114
|
+
while q > 1
|
115
|
+
if x[n-1] & q != 0
|
116
|
+
t ^= q - 1
|
117
|
+
end
|
98
118
|
|
99
|
-
|
100
|
-
rotation = next_rotation(rotation, prefix)
|
119
|
+
q >>= 1
|
101
120
|
end
|
102
121
|
|
103
|
-
|
122
|
+
0.upto(n-1) {|i| x[i] ^= t}
|
123
|
+
|
124
|
+
return x
|
104
125
|
end
|
105
126
|
end
|
106
|
-
end
|
127
|
+
end
|
@@ -0,0 +1,80 @@
|
|
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 'interleave'
|
22
|
+
require_relative 'hilbert'
|
23
|
+
|
24
|
+
module Geospatial
|
25
|
+
# This class represents a n-dimentional index.
|
26
|
+
# @param axes Point in n-space, e.g. [3, 7].
|
27
|
+
# @param bits Number of bits to use for each axis, e.g. 8.
|
28
|
+
class Index
|
29
|
+
def initialize(axes, bits)
|
30
|
+
@axes = axes
|
31
|
+
@bits = bits
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.from_integral(integral, width, bits)
|
35
|
+
self.new(Interleave.unmap(integral, width), bits)
|
36
|
+
end
|
37
|
+
|
38
|
+
attr :axes
|
39
|
+
attr :bits
|
40
|
+
|
41
|
+
def & mask
|
42
|
+
self.class.new(axes.collect{|axis| axis & mask}, @bits)
|
43
|
+
end
|
44
|
+
|
45
|
+
def hash
|
46
|
+
@axes.hash
|
47
|
+
end
|
48
|
+
|
49
|
+
def eql?(other)
|
50
|
+
self.class.eql?(other.class) and @axes.eql?(other.axes) and @bits.eql?(other.bits)
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_i
|
54
|
+
Interleave.map(@axes, @bits)
|
55
|
+
end
|
56
|
+
|
57
|
+
def bit_length
|
58
|
+
@axes.size * @bits
|
59
|
+
end
|
60
|
+
|
61
|
+
def inspect
|
62
|
+
i = self.to_i
|
63
|
+
"\#<#{self.class}[#{@bits}] 0b#{i.to_s(2).rjust(bit_length, '0')} (#{i}) #{@axes.inspect}>"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Represents an index on the hilbert curve.
|
68
|
+
class HilbertIndex < Index
|
69
|
+
def to_ordinal
|
70
|
+
OrdinalIndex.new(Hilbert.unmap(@axes, @bits), @bits)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Represents an index in ordinal space.
|
75
|
+
class OrdinalIndex < Index
|
76
|
+
def to_hilbert
|
77
|
+
HilbertIndex.new(Hilbert.map(@axes, @bits), @bits)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,69 @@
|
|
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
|
+
module Interleave
|
23
|
+
# Convert a Tranpose Hilbert index into a Hilbert integer
|
24
|
+
# 15-bit Hilbert integer = A B C D E F G H I J K L M N O is stored
|
25
|
+
# as its Transpose:
|
26
|
+
# x[0] = A D G J M
|
27
|
+
# x[1] = B E H K N
|
28
|
+
# x[2] = C F I L O
|
29
|
+
# |--bits-|
|
30
|
+
def self.map(index, bits)
|
31
|
+
result = 0
|
32
|
+
|
33
|
+
index.each_with_index do |x, i|
|
34
|
+
offset = index.size - (i+1)
|
35
|
+
|
36
|
+
bits.times do |j|
|
37
|
+
result |= (x & 1) << (j*index.size+offset)
|
38
|
+
|
39
|
+
x >>= 1
|
40
|
+
|
41
|
+
break if x == 0
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
return result
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.unmap(integral, width)
|
49
|
+
result = [0] * width
|
50
|
+
mask = (1 << width) - 1
|
51
|
+
offset = 0
|
52
|
+
|
53
|
+
while integral != 0
|
54
|
+
# N times, look at each bit and append
|
55
|
+
width.times do |i|
|
56
|
+
bit = (integral >> i) & 1
|
57
|
+
result[-1-i] |= bit << offset
|
58
|
+
end
|
59
|
+
|
60
|
+
# Pop first n bits
|
61
|
+
integral >>= width
|
62
|
+
|
63
|
+
offset += 1
|
64
|
+
end
|
65
|
+
|
66
|
+
return result
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/geospatial/location.rb
CHANGED
@@ -1,3 +1,22 @@
|
|
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.
|
1
20
|
|
2
21
|
module Geospatial
|
3
22
|
# This location is specifically relating to a WGS84 coordinate on Earth.
|
@@ -16,59 +35,61 @@ module Geospatial
|
|
16
35
|
R2D = (180.0 / Math::PI)
|
17
36
|
D2R = (Math::PI / 180.0)
|
18
37
|
|
19
|
-
MIN_LATITUDE = -90.0 * D2R
|
20
|
-
MAX_LATITUDE = 90 * D2R
|
21
|
-
VALID_LATITUDE = MIN_LATITUDE...MAX_LATITUDE
|
22
|
-
|
23
38
|
MIN_LONGITUDE = -180 * D2R
|
24
39
|
MAX_LONGITUDE = 180 * D2R
|
25
40
|
VALID_LONGITUDE = MIN_LONGITUDE...MAX_LONGITUDE
|
26
|
-
|
27
|
-
|
41
|
+
|
42
|
+
MIN_LATITUDE = -90.0 * D2R
|
43
|
+
MAX_LATITUDE = 90 * D2R
|
44
|
+
VALID_LATITUDE = MIN_LATITUDE...MAX_LATITUDE
|
45
|
+
|
46
|
+
def initialize(longitude, latitude)
|
28
47
|
@latitude = latitude
|
29
48
|
@longitude = longitude
|
30
|
-
@altitude = altitude
|
31
49
|
end
|
32
50
|
|
33
51
|
def valid?
|
34
|
-
|
52
|
+
VALID_LONGITUDE.include?(longitude) and VALID_LATITUDE.include?(latitude)
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_a
|
56
|
+
[@longitude, @latitude]
|
35
57
|
end
|
36
58
|
|
37
59
|
def to_s
|
38
|
-
"#<Location
|
60
|
+
"#<Location longitude=#{@longitude.to_f} latitude=#{@latitude}>"
|
39
61
|
end
|
40
62
|
|
41
63
|
alias inspect to_s
|
42
64
|
|
43
|
-
attr :
|
44
|
-
attr :
|
45
|
-
attr :altitude
|
65
|
+
attr :longitude # -180 -> 180 (equivalent to x)
|
66
|
+
attr :latitude # -90 -> 90 (equivalent to y)
|
46
67
|
|
47
68
|
# http://janmatuschek.de/LatitudeLongitudeBoundingCoordinates
|
48
69
|
def bounding_box(distance, radius = EARTH_RADIUS)
|
49
70
|
raise ArgumentError.new("Invalid distance or radius") if distance < 0 or radius < 0
|
50
71
|
|
51
72
|
# angular distance in radians on a great circle
|
52
|
-
angular_distance = distance /
|
73
|
+
angular_distance = distance / radius
|
53
74
|
|
54
75
|
min_latitude = (self.latitude * D2R) - angular_distance
|
55
76
|
max_latitude = (self.latitude * D2R) + angular_distance
|
56
77
|
|
57
|
-
if min_latitude >
|
78
|
+
if min_latitude > MIN_LATITUDE and max_latitude < MAX_LATITUDE
|
58
79
|
longitude_delta = Math::asin(Math::sin(angular_distance) / Math::cos(self.latitude * D2R))
|
59
80
|
|
60
81
|
min_longitude = (self.longitude * D2R) - longitude_delta
|
61
|
-
min_longitude += 2.0 * Math::PI if (min_longitude <
|
82
|
+
min_longitude += 2.0 * Math::PI if (min_longitude < MIN_LONGITUDE)
|
62
83
|
|
63
84
|
max_longitude = (self.longitude * D2R) + longitude_delta;
|
64
|
-
max_longitude -= 2.0 * Math::PI if (max_longitude >
|
85
|
+
max_longitude -= 2.0 * Math::PI if (max_longitude > MAX_LONGITUDE)
|
65
86
|
else
|
66
87
|
# a pole is within the distance
|
67
|
-
min_latitude = [min_latitude,
|
68
|
-
max_latitude = [max_latitude,
|
88
|
+
min_latitude = [min_latitude, MIN_LATITUDE].max
|
89
|
+
max_latitude = [max_latitude, MAX_LATITUDE].min
|
69
90
|
|
70
|
-
min_longitude =
|
71
|
-
max_longitude =
|
91
|
+
min_longitude = MIN_LONGITUDE
|
92
|
+
max_longitude = MAX_LONGITUDE
|
72
93
|
end
|
73
94
|
|
74
95
|
return {
|
@@ -78,17 +99,17 @@ module Geospatial
|
|
78
99
|
end
|
79
100
|
|
80
101
|
# Converts latitude, longitude to ECEF coordinate system
|
81
|
-
def to_ecef
|
82
|
-
clat = Math::cos(lat * D2R)
|
83
|
-
slat = Math::sin(lat * D2R)
|
102
|
+
def to_ecef
|
84
103
|
clon = Math::cos(lon * D2R)
|
85
104
|
slon = Math::sin(lon * D2R)
|
86
|
-
|
105
|
+
clat = Math::cos(lat * D2R)
|
106
|
+
slat = Math::sin(lat * D2R)
|
107
|
+
|
87
108
|
n = WGS84_A / Math::sqrt(1.0 - WGS84_E * WGS84_E * slat * slat)
|
88
109
|
|
89
|
-
x =
|
90
|
-
y =
|
91
|
-
z =
|
110
|
+
x = n * clat * clon
|
111
|
+
y = n * clat * slon
|
112
|
+
z = n * (1.0 - WGS84_E * WGS84_E) * slat
|
92
113
|
|
93
114
|
return x, y, z
|
94
115
|
end
|
@@ -108,18 +129,18 @@ module Geospatial
|
|
108
129
|
lat = Math::atan2((z+ep*ep*b*(Math::sin(th) ** 3)), (p-e*e*a*(Math::cos(th)**3)))
|
109
130
|
|
110
131
|
n = a / Math::sqrt(1.0-e*e*(Math::sin(lat) ** 2))
|
111
|
-
alt = p / Math::cos(lat)-n
|
112
|
-
|
113
|
-
return self.new(lat*R2D, lon*R2D
|
132
|
+
# alt = p / Math::cos(lat)-n
|
133
|
+
|
134
|
+
return self.new(lat*R2D, lon*R2D)
|
114
135
|
end
|
115
136
|
|
116
137
|
# calculate distance in metres between us and something else
|
117
138
|
# ref: http://codingandweb.blogspot.co.nz/2012/04/calculating-distance-between-two-points.html
|
118
139
|
def distance_from(other_position)
|
119
|
-
rlat1 = self.latitude * D2R
|
120
140
|
rlong1 = self.longitude * D2R
|
121
|
-
|
141
|
+
rlat1 = self.latitude * D2R
|
122
142
|
rlong2 = other_position.longitude * D2R
|
143
|
+
rlat2 = other_position.latitude * D2R
|
123
144
|
|
124
145
|
dlon = rlong1 - rlong2
|
125
146
|
dlat = rlat1 - rlat2
|
@@ -0,0 +1,57 @@
|
|
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 '../map'
|
22
|
+
|
23
|
+
module Geospatial
|
24
|
+
class Map
|
25
|
+
# Uses dependency injection to generate a class to `load` and `dump` a serialized column.
|
26
|
+
class Index
|
27
|
+
class << self
|
28
|
+
attr_accessor :map
|
29
|
+
|
30
|
+
def load(hash)
|
31
|
+
if hash
|
32
|
+
map.point_for_hash(hash)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def dump(point)
|
37
|
+
if point.is_a?(Point)
|
38
|
+
point.hash
|
39
|
+
elsif point.respond_to?(:to_a)
|
40
|
+
map.hash_for_coordinates(point.to_a)
|
41
|
+
elsif !point.nil?
|
42
|
+
raise ArgumentError.new("Could not convert #{point} on #{map}!")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# serialize :point, Map.for_earth.index
|
49
|
+
def index
|
50
|
+
klass = Class.new(Index)
|
51
|
+
|
52
|
+
klass.map = self
|
53
|
+
|
54
|
+
return klass
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|