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 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
@@ -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
@@ -0,0 +1,3 @@
1
+ module GeoHex
2
+ VERSION = "3.1.1".freeze
3
+ end
@@ -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: []