hexagonly 0.1.0
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.
- 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
|