hexagonly 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +24 -0
- data/.rspec +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +265 -0
- data/Rakefile +7 -0
- data/hexagonly.gemspec +31 -0
- data/lib/hexagonly/geo_json.rb +34 -0
- data/lib/hexagonly/hexagon.rb +212 -0
- data/lib/hexagonly/point.rb +113 -0
- data/lib/hexagonly/polygon.rb +94 -0
- data/lib/hexagonly/space.rb +37 -0
- data/lib/hexagonly/version.rb +3 -0
- data/lib/hexagonly.rb +11 -0
- data/localities.rb +17 -0
- data/spec/factories.rb +12 -0
- data/spec/fixtures/localities.csv +5009 -0
- data/spec/lib/hexagonly/hexagon_spec.rb +208 -0
- data/spec/lib/hexagonly/point_spec.rb +71 -0
- data/spec/lib/hexagonly/polygon_spec.rb +96 -0
- data/spec/lib/hexagonly/space_spec.rb +31 -0
- data/spec/lib/hexagonly_spec.rb +5 -0
- data/spec/spec_helper.rb +8 -0
- metadata +163 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
YzNjMDVmZmQwZTZkNzQzODUzMjczNWNlZjBiYjkyYjZlOTRkZTUyMg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NTYxM2RlOTQ2M2FiYTQ1N2QzYjBiNDdkZWI4MjY3MDY0ZWIxNWVkYQ==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MDY1NzlkNDgyZDhkZTFmYzkyMTE1M2UxODI5YzA0NjU0YmU4YTQ3MzA1YjNl
|
10
|
+
ZGE2MTYwMjk4NTE1ODdjNGE2NWEwZTM2ZGExYmJlOGFjODg2YzE0YjgwNTFk
|
11
|
+
MDNhYWU0NTJhMmVjZGRiMDc3ODI2MjZhMTBlZTk3ZmMzMDQyMjA=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
YjM3MzAyMTVlNWNiODNiNzc0ZjUyZmUyZmQ5YmE5MTI0ODVlOWM2NGFmMDE0
|
14
|
+
NDZmMTYwNzdhNGRkYjQ3M2E1ZTY2YjlmNTgwNGIzZTU0M2ZkYTA0MTY0NTYw
|
15
|
+
OTI3YjhiYjIyYTcxNzUzZjRmYzAyNjMwODYzMzAxNDQ5NzEyMzk=
|
data/.gitignore
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
|
19
|
+
# Ruby version
|
20
|
+
.ruby-gemset
|
21
|
+
.ruby-version
|
22
|
+
|
23
|
+
# Roadmap
|
24
|
+
roadmap.txt
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
hexagonly
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-1.9.3-p448
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 lipanski
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,265 @@
|
|
1
|
+
# Hexagonly
|
2
|
+
|
3
|
+
Provides helper classes for performing flat-topped hexagonal tiling and other polygon-related operations.
|
4
|
+
|
5
|
+
## Motivation
|
6
|
+
|
7
|
+
A personal project required me to **group geographical coordinates** on a map, compute different stats for individual groups
|
8
|
+
and display those stats in such a way that made them visually identify their groups.
|
9
|
+
Squares and rectangles didn't really work for me, because different points on the boundries aren't equally distanced to the center.
|
10
|
+
Circles would have been a good option for grouping objects in a 2-dimensional space, but then again
|
11
|
+
circles are not really *tileable*.
|
12
|
+
**Hexagons**, on the other hand, combine properties of the two shapes: they are easily tileable and have (sort of) a radius.
|
13
|
+
|
14
|
+
I've tested this on my map project, but I guess it should work with **2D games** as well, or anything else that requires tiling or polygon-related operations.
|
15
|
+
|
16
|
+
## Features
|
17
|
+
|
18
|
+
- Currently supported shapes: Point, Polygon, Hexagon.
|
19
|
+
- Define a **Polygon** and check whether a Point lies within its boundries (*crossing count* algorithm).
|
20
|
+
- Define a **Hexagon** and determine its boundries, based on the hexagon center and size. All **Polygon** methods apply to this shape as well.
|
21
|
+
- Generate **neighbouring Hexagons** for a given Hexagon.
|
22
|
+
- **Hexagonal tiling**: generate hexagons to fill up a space, based on its boundries (two points suffice) and the hexagon size.
|
23
|
+
- **Hexagonal tiling & collecting objects on the way**: generate hexagons to match the boundries of a given collection of Points (a *space*), then store contained Points (or custom objects) for every Hexagon.
|
24
|
+
- Convert shapes (Polygon, Hexagon, Point) and mixed collections of shapes to **GeoJson**.
|
25
|
+
- For every defined shape you can either use pre-defined classes or use your own custom classes, by including the appropriate **Hexagonly shape module**.
|
26
|
+
|
27
|
+
The gem currently supports *flat-topped* hexagons only. For *pointy-topped* hexagons just place a bug request and I'll look into it.
|
28
|
+
|
29
|
+
## Installation
|
30
|
+
|
31
|
+
Add this line to your application's Gemfile:
|
32
|
+
|
33
|
+
gem 'hexagonly'
|
34
|
+
|
35
|
+
And then execute:
|
36
|
+
|
37
|
+
$ bundle install
|
38
|
+
|
39
|
+
Or install it yourself as:
|
40
|
+
|
41
|
+
$ gem install hexagonly
|
42
|
+
|
43
|
+
## Usage
|
44
|
+
|
45
|
+
### Points
|
46
|
+
|
47
|
+
There are 2 ways for defining Point objects:
|
48
|
+
|
49
|
+
1. By using the pre-defined ``Hexagonly::Point`` class:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
point = Hexagonly::Point.new(1, 2)
|
53
|
+
|
54
|
+
puts point.x_coord # => 1
|
55
|
+
puts point.y_coord # => 2
|
56
|
+
```
|
57
|
+
|
58
|
+
2. By using your custom class (e.g. think ActiveRecord) and including ``Hexagonly::Point::Methods``
|
59
|
+
inside your class definition. Then you would assign your own accessors as coordinate getters and setters.
|
60
|
+
This is accomplished via the class method ``x_y_coord_methods`` which takes two arguments: the names of the x and y coordinate accessors.
|
61
|
+
The ``x_y_coord_methods`` defaults to ``:x`` and ``:y``.
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
class MyCustomPoint
|
65
|
+
include Hexagonly::Point::Methods
|
66
|
+
|
67
|
+
# Sets accessors :a and :b as coordinate accessors
|
68
|
+
x_y_coord_methods :a, :b
|
69
|
+
|
70
|
+
attr_accessor :a, :b
|
71
|
+
def initialize(a, b)
|
72
|
+
@a, @b = a, b
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
point = MyCustomPoint.new(1, 2)
|
77
|
+
|
78
|
+
puts point.x_coord # => 1
|
79
|
+
puts point.y_coord # => 2
|
80
|
+
```
|
81
|
+
|
82
|
+
### Poylgons
|
83
|
+
|
84
|
+
The same 2 ways of instanciating apply to Polygons as well:
|
85
|
+
|
86
|
+
1. By using the pre-defined ``Hexagonly::Polygon`` class:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
corners = [ Hexagonly::Point.new(2, 1), Hexagonly::Point.new(5, 5), Hexagonly::Point.new(6, 1), ... ]
|
90
|
+
poly = Hexagonly::Polygon(corners)
|
91
|
+
|
92
|
+
puts poly.poly_points # => corners...
|
93
|
+
puts poly.contains?(Hexagonly::Point(4, 2)) # => true
|
94
|
+
```
|
95
|
+
|
96
|
+
2. By using custom classes, which include ``Hexagonly::Polygon::Methods`` and assigning custom corner accessor, set via the class method ``poly_points_method``.
|
97
|
+
The ``poly_points_method`` method defaults to ``:poly_points``.
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
class MyCustomPolygon
|
101
|
+
include Hexagonly::Polygon::Methods
|
102
|
+
|
103
|
+
# Sets :corners as the polygon corners accessor
|
104
|
+
poly_points_method :corners
|
105
|
+
|
106
|
+
attr_accessor :corners
|
107
|
+
def initialize(corners)
|
108
|
+
@corners = corners
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
corners = [ Hexagonly::Point.new(2, 1), Hexagonly::Point.new(5, 5), Hexagonly::Point.new(6, 1), ... ]
|
113
|
+
poly = MyCustomPolygon.new(corners)
|
114
|
+
|
115
|
+
puts poly.poly_points # => corners...
|
116
|
+
puts poly.contains?(Hexagonly::Point(4, 2)) # => true
|
117
|
+
```
|
118
|
+
|
119
|
+
### Hexagons
|
120
|
+
|
121
|
+
Hexagons inherit all methods of Polygons. There are 2 ways of creating new Hexagons:
|
122
|
+
|
123
|
+
1. By using the pre-defined ``Hexagonly::Hexagon`` class:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
center = Hexagonly::Point.new(4, 4)
|
127
|
+
hexagon = Hexagonly::Hexagon.new(center, 1.5)
|
128
|
+
|
129
|
+
puts hexagon.hex_corners # => corners...
|
130
|
+
puts hexagon.contains?(center) # => true
|
131
|
+
```
|
132
|
+
|
133
|
+
2. By using a custom class, which includes ``Hexagonly::Hexagon::Methods``, and calling ``setup_hex`` on your instance:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
class MyCustomHexagon
|
137
|
+
include Hexagonly::Hexagon::Methods
|
138
|
+
|
139
|
+
def initialize(center, size)
|
140
|
+
setup_hex(center, size)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
center = Hexagonly::Point.new(4, 4)
|
145
|
+
hexagon = MyCustomHexagon.new(center, 1.5)
|
146
|
+
|
147
|
+
puts hexagon.hex_center # => center
|
148
|
+
puts hexagon.hex_v_size # => distance from center to the top / bottom borders
|
149
|
+
puts hexagon.hex_corners # => corners...
|
150
|
+
puts hexagon.contains?(center) # => true
|
151
|
+
```
|
152
|
+
|
153
|
+
### Hexagonal tiling
|
154
|
+
|
155
|
+
You start by defining your boundries. Boundries are basically a collection of 2 or more Hexagonaly::Point objects or objects including ``Hexagonly::Point::Methods``:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
boundries = [
|
159
|
+
Hexagonly::Point.new(1, 2),
|
160
|
+
Hexagonly::Point.new(4, 5),
|
161
|
+
...
|
162
|
+
]
|
163
|
+
|
164
|
+
# Or you can use your custom Point class...
|
165
|
+
boundries = [
|
166
|
+
MyCustomPoint.new(1, 2),
|
167
|
+
MyCustomPoint.new(4, 5),
|
168
|
+
...
|
169
|
+
]
|
170
|
+
```
|
171
|
+
|
172
|
+
Once you've defined your boundries, you can pass them to the ``Hexagonly::Hexagon.pack`` method. This takes 3 arguments:
|
173
|
+
- the *boundries* or *points* that mark your hexagon field
|
174
|
+
- half of the desired width of your hexagons (the distance from the center to the left / right boundries)
|
175
|
+
- a *Hash* of additional parameters:
|
176
|
+
- ``:hexagon_class``: the class used to instanciate new Hexagons. Defaults to ``Hexagonly::Hexagon``. If you are
|
177
|
+
using custom Hexagon classes, you should include your class here.
|
178
|
+
- ``:point_class``: the class used to instanciate Hexagon center points. Defaults to ``Hexagonly::Point``.
|
179
|
+
- ``:grab_points``: a boolean, determining whether the first argument (points / boundries) will also be used to collect
|
180
|
+
contained points for every generated hexagon (see next category).
|
181
|
+
- ``:reject_empty``: a boolean, determining whether generated hexagons with no collected points should be removed from
|
182
|
+
the result. Only works if ``:grab_points`` is enabled (see next category).
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
# Generated Hexagons will be Hexagonly::Hexagon instances
|
186
|
+
hexagons = Hexagonly::Hexagon.pack(boundries, 0.3)
|
187
|
+
|
188
|
+
# Generated Hexagons will be MyCustomHexagon instances
|
189
|
+
hexagons = Hexagonly::Hexagon.pack(boundries, 0.3, { :hexagon_class => MyCustomHexagon })
|
190
|
+
```
|
191
|
+
|
192
|
+
### Hexagonal tiling & collecting objects on the way
|
193
|
+
|
194
|
+
While generating your hexagons from a collection of Point objects, you might want to **store all contained points within individual hexagons**.
|
195
|
+
This is accomplished by the same ``Hexagonly::Hexagon.pack`` method, only you'll need to enable the additional ``:grab_points`` parameter.
|
196
|
+
Enabling the``:reject_empty`` parameter will remove all empty hexagons from the results Array.
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
points = [
|
200
|
+
Hexagonly::Point.new(1, 1),
|
201
|
+
Hexagonly::Point.new(2, 2),
|
202
|
+
Hexagonly::Point.new(2, 3),
|
203
|
+
Hexagonly::Point.new(7, 1),
|
204
|
+
...
|
205
|
+
]
|
206
|
+
|
207
|
+
hexagons = Hexagonly::Hexagon.pack(points, 0.25, { :grab_points => true, :reject_empty => true })
|
208
|
+
|
209
|
+
puts hexagons[0].collected_points # => all objects from the points variable contained within this hexagon
|
210
|
+
puts hexagons[0].collected_points[0].class # => Hexagonly::Point or your custom point class
|
211
|
+
```
|
212
|
+
|
213
|
+
### More examples
|
214
|
+
|
215
|
+
While tiling and grabbing objects, you can also mix classes in your ``points`` collection, as long as they are
|
216
|
+
compatible with ``Hexagonly::Point`` methods:
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
class Salami
|
220
|
+
include Hexagonly::Point::Methods
|
221
|
+
|
222
|
+
# .x_y_coord_methods defaults to :x and :y
|
223
|
+
attr_accessor :x, :y
|
224
|
+
def initialize(x, y)
|
225
|
+
@x, @y = x, y
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
class Cheese
|
230
|
+
include Hexagonly::Point::Methods
|
231
|
+
|
232
|
+
attr_accessor :x, :y
|
233
|
+
def initialize(x, y)
|
234
|
+
@x, @y = x, y
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
class Pizza
|
239
|
+
include Hexagonly::Hexagon::Methods
|
240
|
+
end
|
241
|
+
|
242
|
+
# We have a couple of ingredients layed out on the table
|
243
|
+
ingredients = [
|
244
|
+
Salami.new(1, 1),
|
245
|
+
Cheese.new(2, 2),
|
246
|
+
Cheese.new(3, 3),
|
247
|
+
Salami.new(4, 4),
|
248
|
+
...
|
249
|
+
]
|
250
|
+
|
251
|
+
# And we want to create hexagonal pizzas out of them, by just laying the dough on top and removing the extra dough
|
252
|
+
pizza_size = 1.0
|
253
|
+
pizzas = Hexagonly::Hexagon.pack(ingredients, pizza_size, { :hexagon_class => Pizza, :grab_points => true, :reject_empty => true })
|
254
|
+
|
255
|
+
puts pizzas[0].class # => Pizza
|
256
|
+
puts pizzas[0].collected_points[0].class # => Salami or Cheese
|
257
|
+
```
|
258
|
+
|
259
|
+
## Contributing
|
260
|
+
|
261
|
+
1. Fork it
|
262
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
263
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
264
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
265
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/hexagonly.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'hexagonly/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "hexagonly"
|
8
|
+
spec.version = Hexagonly::VERSION
|
9
|
+
spec.authors = ["Florin Lipan"]
|
10
|
+
spec.email = ["lipanski@gmail.com"]
|
11
|
+
spec.description = %q{Hexagonal tiling in Ruby.}
|
12
|
+
spec.summary = %q{Provides helper classes for performing regular, flat-topped hexagonal tiling and other polygon-related operations.}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# General
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
23
|
+
spec.add_development_dependency "rake"
|
24
|
+
# Testing
|
25
|
+
spec.add_development_dependency "rspec"
|
26
|
+
spec.add_development_dependency "factory_girl"
|
27
|
+
# Docs
|
28
|
+
spec.add_development_dependency "yard"
|
29
|
+
spec.add_development_dependency "redcarpet"
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Hexagonly
|
4
|
+
class GeoJson
|
5
|
+
|
6
|
+
attr_reader :features
|
7
|
+
|
8
|
+
# @param [Array] features an array of objects that support the #to_geojson method
|
9
|
+
def initialize(features = nil)
|
10
|
+
add_features(features) unless features.nil?
|
11
|
+
end
|
12
|
+
|
13
|
+
# Adds features (Points, Spaces, Hexagons) that support the #to_geojson method.
|
14
|
+
#
|
15
|
+
# @param [Array] features an array of objects that support the #to_geojson method
|
16
|
+
def add_features(features)
|
17
|
+
@features ||= []
|
18
|
+
features.each do |feat|
|
19
|
+
@features << feat.to_geojson
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Outputs the GeoJson string.
|
24
|
+
#
|
25
|
+
# @return [String] a valid GeoJSON string
|
26
|
+
def to_json
|
27
|
+
{
|
28
|
+
:type => "FeatureCollection",
|
29
|
+
:features => @features
|
30
|
+
}.to_json
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
module Hexagonly
|
2
|
+
class Hexagon
|
3
|
+
|
4
|
+
# Adds Hexagon methods to an object.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# class MyHexagon
|
8
|
+
#
|
9
|
+
# include Hexagonly::Hexagon::Methods
|
10
|
+
#
|
11
|
+
# def initialize(center, size)
|
12
|
+
# setup_hex(center, size)
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# hex = MyHexagon.new(Hexagonly::Point.new(1, 2), 0.5)
|
18
|
+
# hex.hex_corners # => an array containing all 6 corners of the hexagon
|
19
|
+
# hex.contains?(Hexagonly::Point.new(1, 2)) # => true
|
20
|
+
# hex.contains?(Hexagonly::Point.new(3, 3)) # => false
|
21
|
+
module Methods
|
22
|
+
|
23
|
+
include Hexagonly::Polygon::Methods
|
24
|
+
|
25
|
+
def self.included(base)
|
26
|
+
base.extend(Hexagonly::Polygon::ClassMethods)
|
27
|
+
base.poly_points_method :hex_corners
|
28
|
+
|
29
|
+
base.extend(ClassMethods)
|
30
|
+
end
|
31
|
+
|
32
|
+
module ClassMethods
|
33
|
+
|
34
|
+
# Starting from the north-east corner of the given Points array and going east,
|
35
|
+
# then repeating for every other row to the south, the method generates an
|
36
|
+
# array of hexagons. If params[:grab_points] is true, every hexagon created
|
37
|
+
# on the way grabs all the points contained within.
|
38
|
+
#
|
39
|
+
# @param points [Array<Hexagonly::Point>] the boundries for the generated hexagons
|
40
|
+
# or an Array of points that will be "grabbed" by the generated hexagons
|
41
|
+
# (see @params[:grap_points])
|
42
|
+
# @param hex_size [Float] the size of the generated hexagons (width / 2)
|
43
|
+
# @param [Hash] params additional parameters
|
44
|
+
# @option params [false, true] :grab_points whether to "grab" points -
|
45
|
+
# stores included points unter hex#collected_points and excluded points
|
46
|
+
# under hex#rejected_points
|
47
|
+
# @option params [false, true] :reject_empty when :grab_points is enabled
|
48
|
+
# settings this option to true removes empty hexagons from the results
|
49
|
+
# @option params [Class] :hexagon_class the class used to instanciate new hexagons
|
50
|
+
# @option params [Class] :point_class the class used to instanciate hexagon center points
|
51
|
+
#
|
52
|
+
# @return [Array<Hexagonly::Hexagon] an array of hexagons
|
53
|
+
def pack(points, hex_size, params = {})
|
54
|
+
return [] if points.nil? || points.empty? || points.size < 2
|
55
|
+
|
56
|
+
hexagons = []
|
57
|
+
space = Hexagonly::Space.new(points)
|
58
|
+
|
59
|
+
h_hexagon_count = ((space.east.x_coord - space.west.x_coord) / (hex_size * 1.5)).abs.ceil
|
60
|
+
v_hexagon_count = ((space.north.y_coord - space.south.y_coord) / (hex_size * Math.cos(Math::PI / 6) * 2.0)).abs.ceil + 1
|
61
|
+
|
62
|
+
point_class = params.fetch(:point_class, Hexagonly::Point)
|
63
|
+
hexagon_class = params.fetch(:hexagon_class, Hexagonly::Hexagon)
|
64
|
+
grab_points = params.fetch(:grab_points, false)
|
65
|
+
reject_empty = params.fetch(:reject_empty, false)
|
66
|
+
|
67
|
+
north_west = point_class.new.tap{ |p| p.set_coords(space.west.x_coord, space.north.y_coord) }
|
68
|
+
hexagons << hex = hexagon_class.new.tap{ |h| h.setup_hex(north_west, hex_size); h.grab(points) if grab_points }
|
69
|
+
v_hexagon_count.times do |i|
|
70
|
+
first_hex = hex
|
71
|
+
h_hexagon_count.times do |j|
|
72
|
+
if j % 2 == 0
|
73
|
+
hexagons << hex = hex.north_east_hexagon(params)
|
74
|
+
else
|
75
|
+
hexagons << hex = hex.south_east_hexagon(params)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
hexagons << hex = first_hex.south_hexagon(params) if i < v_hexagon_count - 1
|
80
|
+
end
|
81
|
+
|
82
|
+
if grab_points && reject_empty
|
83
|
+
hexagons.reject!{ |hex| hex.collected_points.empty? }
|
84
|
+
end
|
85
|
+
|
86
|
+
hexagons
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
attr_accessor :hex_center, :hex_size
|
92
|
+
|
93
|
+
# Initializes the hexagon.
|
94
|
+
#
|
95
|
+
# @param hex_center [Hexagonly::Point] the center of the hexagon
|
96
|
+
# @param hex_size [Float] the size of the hexagon (horizontal width / 2)
|
97
|
+
def setup_hex(hex_center, hex_size)
|
98
|
+
@hex_center, @hex_size = hex_center, hex_size
|
99
|
+
end
|
100
|
+
|
101
|
+
# The distance from the center of the hexagon to the top / bottom edges.
|
102
|
+
#
|
103
|
+
# @return [Float]
|
104
|
+
def hex_v_size
|
105
|
+
raise "hex_size not defined!" if @hex_size.nil?
|
106
|
+
|
107
|
+
@hex_size * Math.cos(Math::PI / 6)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Checks whether the given point lies within the hexagon.
|
111
|
+
#
|
112
|
+
# @return [true|false]
|
113
|
+
def contains?(point)
|
114
|
+
loosely_contains?(point) ? super : false
|
115
|
+
end
|
116
|
+
|
117
|
+
# Checks whether the given point lies within the bounding box of the hexagon.
|
118
|
+
#
|
119
|
+
# @return [true|false]
|
120
|
+
def loosely_contains?(point)
|
121
|
+
raise "Call #setup_hex first!" if @hex_center.nil? || @hex_size.nil?
|
122
|
+
|
123
|
+
((point.x_coord - @hex_center.x_coord).abs <= @hex_size) && ((point.y_coord - @hex_center.y_coord).abs <= hex_v_size)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns a hexagon to the north-east with the same radius.
|
127
|
+
def north_east_hexagon(params = {})
|
128
|
+
raise "Call #setup_hex first!" if @hex_center.nil? || @hex_size.nil?
|
129
|
+
|
130
|
+
grab_points = params.fetch(:grab_points, false)
|
131
|
+
|
132
|
+
north_east_center = hex_point_class.new(@hex_center.x_coord + @hex_size * 1.5, @hex_center.y_coord + hex_v_size)
|
133
|
+
self.class.new.tap do |hex|
|
134
|
+
hex.setup_hex(north_east_center, @hex_size)
|
135
|
+
hex.grab(@rejected_points) if grab_points
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Returns a hexagon to the south-east with the same radius.
|
140
|
+
def south_east_hexagon(params = {})
|
141
|
+
raise "Call #setup_hex first!" if @hex_center.nil? || @hex_size.nil?
|
142
|
+
|
143
|
+
grab_points = params.fetch(:grab_points, false)
|
144
|
+
|
145
|
+
south_east_center = hex_point_class.new(@hex_center.x_coord + @hex_size * 1.5, @hex_center.y_coord - hex_v_size)
|
146
|
+
self.class.new.tap do |hex|
|
147
|
+
hex.setup_hex(south_east_center, @hex_size)
|
148
|
+
hex.grab(@rejected_points) if grab_points
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Returns a hexagon to the south with the same radius.
|
153
|
+
def south_hexagon(params = {})
|
154
|
+
raise "Call #setup_hex first!" if @hex_center.nil? || @hex_size.nil?
|
155
|
+
|
156
|
+
grab_points = params.fetch(:grab_points, false)
|
157
|
+
|
158
|
+
south_center = hex_point_class.new(@hex_center.x_coord, @hex_center.y_coord - hex_v_size * 2.0)
|
159
|
+
self.class.new.tap do |hex|
|
160
|
+
hex.setup_hex(south_center, @hex_size)
|
161
|
+
hex.grab(@rejected_points) if grab_points
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Returns an array of the 6 points defining the corners of the hexagon.
|
166
|
+
#
|
167
|
+
# @return [Array<HexagonTiling::Point>] an array of points, coresponding to
|
168
|
+
# the 6 corners of the hexagon
|
169
|
+
def hex_corners
|
170
|
+
raise "Call #setup_hex first!" if @hex_center.nil? || @hex_size.nil?
|
171
|
+
|
172
|
+
corners = []
|
173
|
+
(0..5).each do |i|
|
174
|
+
angle = 2 * Math::PI / 6 * i
|
175
|
+
corner_x = @hex_center.x_coord + @hex_size * Math.cos(angle)
|
176
|
+
corner_y = @hex_center.y_coord + @hex_size * Math.sin(angle)
|
177
|
+
corners << hex_point_class.new(corner_x, corner_y)
|
178
|
+
end
|
179
|
+
|
180
|
+
corners
|
181
|
+
end
|
182
|
+
|
183
|
+
def to_geojson
|
184
|
+
corner_points = hex_corners.map{ |p| [p.x_coord, p.y_coord] }
|
185
|
+
corner_points << corner_points.last
|
186
|
+
{
|
187
|
+
:type => "Feature",
|
188
|
+
:geometry => {
|
189
|
+
:type => "Polygon",
|
190
|
+
:coordinates => [corner_points]
|
191
|
+
},
|
192
|
+
:properties => nil
|
193
|
+
}
|
194
|
+
end
|
195
|
+
|
196
|
+
private
|
197
|
+
|
198
|
+
def hex_point_class
|
199
|
+
@hex_center.nil? ? Hexagonly::Point : @hex_center.class
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
203
|
+
|
204
|
+
include Methods
|
205
|
+
|
206
|
+
# (see #setup_hex)
|
207
|
+
def initialize(*params)
|
208
|
+
setup_hex(*params) if params.size == 2
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
end
|