geo_hex 3.1.1
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.
- 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: []
|