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 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: []