geospatial 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +11 -2
- data/Gemfile +12 -1
- data/README.md +37 -8
- data/Rakefile +11 -0
- data/australia.png +0 -0
- data/geospatial.gemspec +1 -1
- data/lib/geospatial/box.rb +126 -0
- data/lib/geospatial/circle.rb +109 -0
- data/lib/geospatial/dimensions.rb +130 -0
- data/lib/geospatial/filter.rb +82 -0
- data/lib/geospatial/hilbert/curve.rb +79 -0
- data/lib/geospatial/hilbert/traverse.rb +65 -0
- data/lib/geospatial/hilbert.rb +111 -90
- data/lib/geospatial/index.rb +80 -0
- data/lib/geospatial/interleave.rb +69 -0
- data/lib/geospatial/location.rb +53 -32
- data/lib/geospatial/map/index.rb +57 -0
- data/lib/geospatial/map.rb +153 -0
- data/lib/geospatial/polygon.rb +115 -0
- data/lib/geospatial/version.rb +21 -1
- data/lib/geospatial.rb +22 -3
- data/spec/geospatial/box_spec.rb +42 -0
- data/spec/geospatial/circle_spec.rb +76 -0
- data/spec/geospatial/dimensions_spec.rb +35 -0
- data/spec/geospatial/hilbert_curve_spec.rb +59 -0
- data/spec/geospatial/hilbert_traverse_spec.rb +63 -0
- data/spec/geospatial/index_spec.rb +113 -0
- data/spec/{geoquery → geospatial}/location_spec.rb +4 -4
- data/spec/geospatial/map_index_spec.rb +41 -0
- data/spec/geospatial/map_spec.rb +71 -0
- data/spec/geospatial/polygon_spec.rb +96 -0
- data/spec/geospatial/sorted.rb +6 -0
- data/spec/geospatial/visualization.rb +46 -0
- data/spec/geospatial/world.png +0 -0
- metadata +43 -9
- data/spec/geoquery/hilbert_spec.rb +0 -72
@@ -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
|