geo_hex 3.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/geo_hex.rb +43 -0
- data/lib/geo_hex/ll.rb +61 -0
- data/lib/geo_hex/polygon.rb +90 -0
- data/lib/geo_hex/pp.rb +44 -0
- data/lib/geo_hex/unit.rb +56 -0
- data/lib/geo_hex/version.rb +3 -0
- data/lib/geo_hex/zone.rb +155 -0
- metadata +99 -0
data/lib/geo_hex.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
require 'geo_hex/version'
|
3
|
+
require 'geo_hex/ll'
|
4
|
+
require 'geo_hex/pp'
|
5
|
+
require 'geo_hex/zone'
|
6
|
+
require 'geo_hex/unit'
|
7
|
+
require 'geo_hex/polygon'
|
8
|
+
|
9
|
+
module GeoHex
|
10
|
+
H_KEY = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".freeze
|
11
|
+
H_BASE = 20037508.34
|
12
|
+
H_D2R = Math::PI / 180.0
|
13
|
+
H_K = Math.tan(H_D2R * 30)
|
14
|
+
H_ER = 6_371_007.2
|
15
|
+
|
16
|
+
# @param [Float] lat the latitude
|
17
|
+
# @param [Float] lon the longitude
|
18
|
+
# @param [Integer] level the level
|
19
|
+
# @return [GeoHex::Zone] the encoded zone
|
20
|
+
def self.encode(lat, lon, level = 7)
|
21
|
+
LL.new(lat, lon).to_zone(level)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param [String] code the GeoHex code
|
25
|
+
# @return [GeoHex::Zone] the decoded zone
|
26
|
+
def self.decode(code)
|
27
|
+
x, y = 0, 0
|
28
|
+
chars = code.size
|
29
|
+
string = "#{H_KEY.index(code[0]) * 30 + H_KEY.index(code[1])}#{code[2..-1]}"
|
30
|
+
string = string.rjust(chars+1, "0")
|
31
|
+
nums = string.chars.map {|c| c.to_i }
|
32
|
+
nums.each_with_index do |num, i|
|
33
|
+
pow = 3**(chars-i)
|
34
|
+
num = num.to_s(3).to_i
|
35
|
+
|
36
|
+
case (num / 10) when 0 then x -= pow when 2 then x += pow end
|
37
|
+
case (num % 10) when 0 then y -= pow when 2 then y += pow end
|
38
|
+
end
|
39
|
+
|
40
|
+
Zone.new(x, y, chars-2).send(:with_code, code)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
data/lib/geo_hex/ll.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
module GeoHex
|
2
|
+
|
3
|
+
# Lat/Lon coordinates
|
4
|
+
class LL
|
5
|
+
|
6
|
+
# @return [Float] longitude
|
7
|
+
def self.normalize(lon)
|
8
|
+
if lon < -180
|
9
|
+
lon += 360
|
10
|
+
elsif lon > 180
|
11
|
+
lon -= 360
|
12
|
+
else
|
13
|
+
lon
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :lat, :lon
|
18
|
+
|
19
|
+
# @param [Float] lat the latitude
|
20
|
+
# @param [Float] lon the longitude
|
21
|
+
def initialize(lat, lon)
|
22
|
+
@lat, @lon = lat, self.class.normalize(lon)
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [Float] mercator easting
|
26
|
+
def easting
|
27
|
+
@easting ||= lon * H_BASE / 180.0
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Float] mercator northing
|
31
|
+
def northing
|
32
|
+
@northing ||= Math.log(Math.tan((90 + lat) * H_D2R / 2)) / Math::PI * H_BASE
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [GeoHex::PP] the full projection point coordinates
|
36
|
+
def to_pp
|
37
|
+
GeoHex::PP.new(easting, northing)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Converts coordinates to Zone
|
41
|
+
# @param [Integer] level the level
|
42
|
+
# @return [GeoHex::Zone] the coordinates
|
43
|
+
def to_zone(level)
|
44
|
+
to_pp.to_zone(level)
|
45
|
+
end
|
46
|
+
|
47
|
+
# @param [GeoHex::PP] other coordinates
|
48
|
+
# @return [Float] distance in meters
|
49
|
+
def distance_to(other)
|
50
|
+
d_lat, d_lon = (other.lat - lat) * H_D2R / 2.0, (other.lon - lon) * H_D2R / 2.0
|
51
|
+
lat1, lat2 = lat * H_D2R, other.lat * H_D2R
|
52
|
+
|
53
|
+
a = Math.sin(d_lat) ** 2 +
|
54
|
+
Math.sin(d_lon) ** 2 * Math.cos(lat1) * Math.cos(lat2)
|
55
|
+
c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
|
56
|
+
|
57
|
+
H_ER * c
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module GeoHex
|
2
|
+
|
3
|
+
class Polygon < Struct.new(:easting, :northing, :size)
|
4
|
+
H_K = Math.tan(Math::PI / 180.0 * 60)
|
5
|
+
|
6
|
+
# @return [GeoHex::PP] The Centroid of the Polygon
|
7
|
+
def centroid
|
8
|
+
@centroid ||= PP.new(easting, northing)
|
9
|
+
end
|
10
|
+
alias_method :c, :centroid
|
11
|
+
|
12
|
+
# @return [GeoHex::PP] The Northeast point of the Polygon
|
13
|
+
def north_east
|
14
|
+
@north_east ||= PP.new(east_bound, north_bound)
|
15
|
+
end
|
16
|
+
alias_method :ne, :north_east
|
17
|
+
|
18
|
+
# @return [GeoHex::PP] The East point of the Polygon
|
19
|
+
def east
|
20
|
+
@east ||= PP.new(easting + 2 * size, northing)
|
21
|
+
end
|
22
|
+
alias_method :e, :east
|
23
|
+
|
24
|
+
# @return [GeoHex::PP] The Southeast point of the Polygon
|
25
|
+
def south_east
|
26
|
+
@south_east ||= PP.new(east_bound, south_bound)
|
27
|
+
end
|
28
|
+
alias_method :se, :south_east
|
29
|
+
|
30
|
+
# @return [GeoHex::PP] The Southwest point of the Polygon
|
31
|
+
def south_west
|
32
|
+
@south_west ||= PP.new(west_bound, south_bound)
|
33
|
+
end
|
34
|
+
alias_method :sw, :south_west
|
35
|
+
|
36
|
+
# @return [GeoHex::PP] The West point of the Polygon
|
37
|
+
def west
|
38
|
+
@west ||= PP.new(easting - 2 * size, northing)
|
39
|
+
end
|
40
|
+
alias_method :w, :west
|
41
|
+
|
42
|
+
# @return [GeoHex::PP] The Northwest point of the Polygon
|
43
|
+
def north_west
|
44
|
+
@north_west ||= PP.new(west_bound, north_bound)
|
45
|
+
end
|
46
|
+
alias_method :nw, :north_west
|
47
|
+
|
48
|
+
# @return [GeoHex::PP] Point in the middle of the northern polygon boundary
|
49
|
+
def north
|
50
|
+
@north ||= PP.new(easting, north_bound)
|
51
|
+
end
|
52
|
+
alias_method :n, :north
|
53
|
+
|
54
|
+
# @return [GeoHex::PP] Point in the middle of the southern polygon boundary
|
55
|
+
def south
|
56
|
+
@south ||= PP.new(easting, south_bound)
|
57
|
+
end
|
58
|
+
alias_method :s, :south
|
59
|
+
|
60
|
+
# @return [Array<GeoHex::PP>] All the points of the Polygon, ordered from Northeast round to Northwest
|
61
|
+
def points
|
62
|
+
@points ||= [ne, e, se, sw, w, nw]
|
63
|
+
end
|
64
|
+
alias_method :to_a, :points
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
# @return [Float] The northing of the Northern boundary of the Polygon
|
69
|
+
def north_bound
|
70
|
+
@north_bound ||= northing + H_K * size
|
71
|
+
end
|
72
|
+
|
73
|
+
# @return [Float] The easting of both Eastern corners of the Polygon
|
74
|
+
def east_bound
|
75
|
+
@east_bound ||= easting + size
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [Float] The northing of the Southern boundary of the Polygon
|
79
|
+
def south_bound
|
80
|
+
@south_bound ||= northing - H_K * size
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return [Float] The easting of both Western corners of the Polygon
|
84
|
+
def west_bound
|
85
|
+
@west_bound ||= easting - size
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
data/lib/geo_hex/pp.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module GeoHex
|
2
|
+
|
3
|
+
# Mercator projection point
|
4
|
+
class PP < Struct.new(:easting, :northing)
|
5
|
+
|
6
|
+
# @return [Float] longitude
|
7
|
+
def lon
|
8
|
+
@lon ||= LL.normalize(easting / H_BASE * 180.0)
|
9
|
+
end
|
10
|
+
|
11
|
+
# @return [Float] latitude
|
12
|
+
def lat
|
13
|
+
@lat ||= 180.0 / Math::PI * (2 * Math.atan(Math.exp(northing / H_BASE * 180.0 * H_D2R)) - Math::PI / 2.0)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [GeoHex::LL] lat/lon coordinates
|
17
|
+
def to_ll
|
18
|
+
LL.new(lat, lon)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Converts point coordinates into a Zone for a given `level`
|
22
|
+
# @param [Integer] level the level
|
23
|
+
# @return [GeoHex::Zone] the zone
|
24
|
+
def to_zone(level)
|
25
|
+
u = Unit[level]
|
26
|
+
x = (easting + northing / H_K) / u.width
|
27
|
+
y = (northing - H_K * easting) / u.height
|
28
|
+
|
29
|
+
x0, y0 = x.floor, y.floor
|
30
|
+
xq, yq = x - x0, y - y0
|
31
|
+
xn, yn = if yq > -xq + 1 && yq < 2 * xq && yq > 0.5 * xq
|
32
|
+
[x0 + 1, y0 + 1]
|
33
|
+
elsif yq < -xq + 1 && yq > 2 * xq - 1 && yq < 0.5 * xq + 0.5
|
34
|
+
[x0, y0]
|
35
|
+
else
|
36
|
+
[x.round, y.round]
|
37
|
+
end
|
38
|
+
|
39
|
+
Zone.new(xn, yn, level)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
data/lib/geo_hex/unit.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
module GeoHex
|
2
|
+
|
3
|
+
# A Unit is a class of Zones. It has a `level`, `width`, `height` and
|
4
|
+
# overall `size`. Dimensions vary for different levels.
|
5
|
+
class Unit
|
6
|
+
|
7
|
+
class << self
|
8
|
+
private :new
|
9
|
+
|
10
|
+
# @return [Boolean] true if unit caching is enabled, defaults to false
|
11
|
+
def cache?
|
12
|
+
@cache == true
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param [Boolean] value set to true to enable caching (recommended)
|
16
|
+
def cache=(value)
|
17
|
+
@cache = value
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Hash] cache store
|
21
|
+
def store
|
22
|
+
@store ||= {}
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param [Integer] level
|
26
|
+
# @return [GeoHex::Unit] for the given level
|
27
|
+
def [](level)
|
28
|
+
cache? ? store[level] ||= new(level) : new(level)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :level
|
34
|
+
|
35
|
+
# @param [Integer] level
|
36
|
+
def initialize(level)
|
37
|
+
@level = level
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Float] unit's mercator size
|
41
|
+
def size
|
42
|
+
@size ||= H_BASE / 3**(level+3)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [Float] unit's mercator width
|
46
|
+
def width
|
47
|
+
@width ||= 6.0 * size
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Float] unit's mercator height
|
51
|
+
def height
|
52
|
+
@height ||= width * H_K
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
data/lib/geo_hex/zone.rb
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
module GeoHex
|
2
|
+
|
3
|
+
# A positioned instance of a Unit, within a level-grid
|
4
|
+
class Zone
|
5
|
+
|
6
|
+
# @return [Integer] the zone coordinates within the grid
|
7
|
+
attr_reader :x, :y
|
8
|
+
|
9
|
+
# @return [GeoHex::Unit] the associated unit
|
10
|
+
attr_reader :unit
|
11
|
+
|
12
|
+
# @return [Float] the mercator northing
|
13
|
+
attr_reader :northing
|
14
|
+
|
15
|
+
# @return [Float] the mercator easting
|
16
|
+
attr_reader :easting
|
17
|
+
|
18
|
+
# @param [Integer] x the horizontal index
|
19
|
+
# @param [Integer] y the vertical index
|
20
|
+
# @param [Integer] level the level
|
21
|
+
def initialize(x, y, level)
|
22
|
+
@x, @y = x, y
|
23
|
+
@unit = Unit[level]
|
24
|
+
@northing = (H_K * @x * @unit.width + @y * @unit.height) / 2.0
|
25
|
+
@easting = (@northing - @y * @unit.height) / H_K
|
26
|
+
@x, @y = @y, @x if meridian_180?
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Integer] the level
|
30
|
+
def level
|
31
|
+
unit.level
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Float] the longitude coordinate
|
35
|
+
def lon
|
36
|
+
meridian_180? ? 180.0 : point.lon
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Float] the latitude coordinate
|
40
|
+
def lat
|
41
|
+
point.lat
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [String] GeoHex code
|
45
|
+
def code
|
46
|
+
@code ||= encode
|
47
|
+
end
|
48
|
+
alias_method :to_s, :code
|
49
|
+
|
50
|
+
# @return [GeoHex::PP] zone center, point coordinates
|
51
|
+
def point
|
52
|
+
@point ||= GeoHex::PP.new(easting, northing)
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [<GeoHex::Polygon>] Zone's NE, E, SE, SW, W and NW points
|
56
|
+
def polygon
|
57
|
+
@polygon ||= GeoHex::Polygon.new(easting, northing, unit.size)
|
58
|
+
end
|
59
|
+
|
60
|
+
# @param [Integer] range the number of zones to search within
|
61
|
+
# @return [Array<GeoHex::Zone>] the neighbouring zones
|
62
|
+
def neighbours(range)
|
63
|
+
zones = []
|
64
|
+
x0, xn = x - range, x + range
|
65
|
+
|
66
|
+
x0.upto(xn) do |xi|
|
67
|
+
zones << self.class.new(xi, y, level) unless xi == x
|
68
|
+
end
|
69
|
+
|
70
|
+
1.upto(range) do |i|
|
71
|
+
y+i % 2 == 1 ? xn-=1 : x0+=1
|
72
|
+
|
73
|
+
x0.upto(xn) do |xi|
|
74
|
+
zones << self.class.new(xi, y+i, level)
|
75
|
+
zones << self.class.new(xi, y-i, level)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
zones
|
80
|
+
end
|
81
|
+
alias_method :neighbors, :neighbours
|
82
|
+
|
83
|
+
# @param [Zone, String] other another Zone or a GeoHex code (String)
|
84
|
+
# @return [Boolean] true, if given Zone or GeoHex code (String) matches self
|
85
|
+
def ==(other)
|
86
|
+
case other
|
87
|
+
when String
|
88
|
+
to_s == other
|
89
|
+
when self.class
|
90
|
+
x == other.x && y == other.y && level == other.level
|
91
|
+
else
|
92
|
+
super
|
93
|
+
end
|
94
|
+
end
|
95
|
+
alias_method :eql?, :==
|
96
|
+
|
97
|
+
# @return [Fixnum] the object hash
|
98
|
+
def hash
|
99
|
+
[x, y, level].hash
|
100
|
+
end
|
101
|
+
|
102
|
+
protected
|
103
|
+
|
104
|
+
# @param [String] code the GeoHex code
|
105
|
+
# @return [GeoHex::Zone] GeoHex zone
|
106
|
+
def with_code(code)
|
107
|
+
@code = code
|
108
|
+
self
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
# @return [String] GeoHex code
|
114
|
+
def encode
|
115
|
+
code, mod_x, mod_y = "", self.x, self.y
|
116
|
+
|
117
|
+
(0..level+2).reverse_each do |i|
|
118
|
+
pow = 3 ** i
|
119
|
+
p2c = (pow / 2.0).ceil
|
120
|
+
|
121
|
+
c3_x = if mod_x >= p2c
|
122
|
+
mod_x -= pow
|
123
|
+
2
|
124
|
+
elsif mod_x <= -p2c
|
125
|
+
mod_x += pow
|
126
|
+
0
|
127
|
+
else
|
128
|
+
1
|
129
|
+
end
|
130
|
+
|
131
|
+
c3_y = if mod_y >= p2c
|
132
|
+
mod_y -= pow
|
133
|
+
2
|
134
|
+
elsif mod_y <= -p2c
|
135
|
+
mod_y += pow
|
136
|
+
0
|
137
|
+
else
|
138
|
+
1
|
139
|
+
end
|
140
|
+
|
141
|
+
code << Integer([c3_x, c3_y].join, 3).to_s
|
142
|
+
end
|
143
|
+
|
144
|
+
number = code[0..2].to_i
|
145
|
+
"#{H_KEY[number / 30]}#{H_KEY[number % 30]}#{code[3..-1]}"
|
146
|
+
end
|
147
|
+
|
148
|
+
# @return [Boolean] true if the zone is placed on the 180th meridian
|
149
|
+
def meridian_180?
|
150
|
+
return @meridian_180 if defined?(@meridian_180)
|
151
|
+
@meridian_180 = H_BASE - easting < unit.size
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
metadata
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: geo_hex
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 3.1.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Dimitrij Denissenko
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-06-01 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: bundler
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: Ruby implementation of GeoHex encoding algorithm
|
63
|
+
email: dimitrij@blacksquaremedia.com
|
64
|
+
executables: []
|
65
|
+
extensions: []
|
66
|
+
extra_rdoc_files: []
|
67
|
+
files:
|
68
|
+
- lib/geo_hex/unit.rb
|
69
|
+
- lib/geo_hex/ll.rb
|
70
|
+
- lib/geo_hex/polygon.rb
|
71
|
+
- lib/geo_hex/version.rb
|
72
|
+
- lib/geo_hex/pp.rb
|
73
|
+
- lib/geo_hex/zone.rb
|
74
|
+
- lib/geo_hex.rb
|
75
|
+
homepage: https://github.com/bsm/geo_hex
|
76
|
+
licenses: []
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options: []
|
79
|
+
require_paths:
|
80
|
+
- lib
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: 1.9.0
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: 1.3.6
|
93
|
+
requirements: []
|
94
|
+
rubyforge_project:
|
95
|
+
rubygems_version: 1.8.24
|
96
|
+
signing_key:
|
97
|
+
specification_version: 3
|
98
|
+
summary: GeoHex (V3)
|
99
|
+
test_files: []
|