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
@@ -0,0 +1,113 @@
|
|
1
|
+
module Hexagonly
|
2
|
+
class Point
|
3
|
+
|
4
|
+
# Adds Point methods to an object. Any Point bears x and y coordinates,
|
5
|
+
# returned by the #x and #y methods. You can either override these methods
|
6
|
+
# to output your desired coordinates or you can map coordinate readers to
|
7
|
+
# different method names by using the #x_y_coord_methods, #x_coord_method or
|
8
|
+
# #y_coord_method methods.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# class MyPoint
|
12
|
+
#
|
13
|
+
# include Hexagonly::Point::Methods
|
14
|
+
# # The x coordinate will be read from #a and the y coordinate will be read from #b
|
15
|
+
# x_y_coord_methods :a, :b
|
16
|
+
#
|
17
|
+
# attr_accessor :a, :b
|
18
|
+
# def initialize(a, b); @a, @b = a, b; end
|
19
|
+
#
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# p = MyPoint.new(1, 2)
|
23
|
+
# p.x_coord # => 1
|
24
|
+
# p.y_coord # => 2
|
25
|
+
module Methods
|
26
|
+
|
27
|
+
include Comparable
|
28
|
+
|
29
|
+
def self.included(base)
|
30
|
+
base.extend(ClassMethods)
|
31
|
+
end
|
32
|
+
|
33
|
+
module ClassMethods
|
34
|
+
attr_accessor :x_coord_method_name, :y_coord_method_name
|
35
|
+
|
36
|
+
def x_coord_method(x_method)
|
37
|
+
self.x_coord_method_name = x_method.to_sym
|
38
|
+
end
|
39
|
+
|
40
|
+
def y_coord_method(y_method)
|
41
|
+
self.y_coord_method_name = y_method.to_sym
|
42
|
+
end
|
43
|
+
|
44
|
+
def x_y_coord_methods(x_method, y_method)
|
45
|
+
x_coord_method(x_method)
|
46
|
+
y_coord_method(y_method)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def x_coord
|
51
|
+
send(self.class.x_coord_method_name || 'x')
|
52
|
+
end
|
53
|
+
|
54
|
+
def x_coord=(value)
|
55
|
+
send("#{self.class.x_coord_method_name || 'x'}=", value)
|
56
|
+
end
|
57
|
+
|
58
|
+
def y_coord
|
59
|
+
send(self.class.y_coord_method_name || 'y')
|
60
|
+
end
|
61
|
+
|
62
|
+
def y_coord=(value)
|
63
|
+
send("#{self.class.y_coord_method_name || 'y'}=", value)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Sets the coordinates for the current Point.
|
67
|
+
#
|
68
|
+
# @param x [Float]
|
69
|
+
# @param y [Float]
|
70
|
+
def set_coords(x, y)
|
71
|
+
self.x_coord = x
|
72
|
+
self.y_coord = y
|
73
|
+
end
|
74
|
+
|
75
|
+
def <=>(another_point)
|
76
|
+
if x_coord == another_point.x_coord && y_coord == another_point.y_coord
|
77
|
+
0
|
78
|
+
elsif x_coord > another_point.x_coord
|
79
|
+
1
|
80
|
+
else
|
81
|
+
-1
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Enable implicit splat.
|
86
|
+
# def to_ary
|
87
|
+
# [x_coord, y_coord]
|
88
|
+
# end
|
89
|
+
|
90
|
+
def to_geojson
|
91
|
+
{
|
92
|
+
:type => "Feature",
|
93
|
+
:geometry => {
|
94
|
+
:type => "Point",
|
95
|
+
:coordinates => [x_coord, y_coord]
|
96
|
+
},
|
97
|
+
:properties => nil
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
include Methods
|
104
|
+
|
105
|
+
attr_accessor :x, :y
|
106
|
+
|
107
|
+
# (see #set_coords)
|
108
|
+
def initialize(*coords)
|
109
|
+
set_coords(*coords) if coords.size == 2
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Hexagonly
|
2
|
+
class Polygon
|
3
|
+
|
4
|
+
# Adds Polygon methods to an object. The Polygon corners are read via
|
5
|
+
# the #poly_points method. You can override this method or use the
|
6
|
+
# #poly_points_method class method to set a method name for reading
|
7
|
+
# polygon corners.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# class MyPolygon
|
11
|
+
#
|
12
|
+
# include Hexagonly::Polygon::Methods
|
13
|
+
# poly_points_method :corners
|
14
|
+
#
|
15
|
+
# attr_reader :corners
|
16
|
+
# def initialize(corners); @corners = corners; end
|
17
|
+
#
|
18
|
+
# end
|
19
|
+
module Methods
|
20
|
+
|
21
|
+
def self.included(base)
|
22
|
+
base.extend(ClassMethods)
|
23
|
+
end
|
24
|
+
|
25
|
+
module ClassMethods
|
26
|
+
attr_accessor :poly_points_method_name
|
27
|
+
|
28
|
+
def poly_points_method(points_method)
|
29
|
+
self.poly_points_method_name = points_method.to_sym
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_accessor :collected_points, :rejected_points
|
34
|
+
|
35
|
+
def poly_points
|
36
|
+
raise NoMethodError if self.class.poly_points_method_name.nil?
|
37
|
+
|
38
|
+
send(self.class.poly_points_method_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Crossing count algorithm for determining whether a point lies within a
|
42
|
+
# polygon. Ported from http://www.visibone.com/inpoly/inpoly.c.txt
|
43
|
+
# (original C code by Bob Stein & Craig Yap).
|
44
|
+
def contains?(point)
|
45
|
+
raise "Not a valid polygon!" if poly_points.nil? || poly_points.size < 3
|
46
|
+
|
47
|
+
is_inside = false
|
48
|
+
old_p = poly_points.last
|
49
|
+
poly_points.each do |new_p|
|
50
|
+
if new_p.x_coord > old_p.x_coord
|
51
|
+
first_p = old_p
|
52
|
+
second_p = new_p
|
53
|
+
else
|
54
|
+
first_p = new_p
|
55
|
+
second_p = old_p
|
56
|
+
end
|
57
|
+
if ((new_p.x_coord < point.x_coord) == (point.x_coord <= old_p.x_coord)) && ((point.y_coord - first_p.y_coord) * (second_p.x_coord - first_p.x_coord) < (second_p.y_coord - first_p.y_coord) * (point.x_coord - first_p.x_coord))
|
58
|
+
is_inside = ! is_inside
|
59
|
+
end
|
60
|
+
old_p = new_p
|
61
|
+
end
|
62
|
+
|
63
|
+
is_inside
|
64
|
+
end
|
65
|
+
|
66
|
+
# Grabs all points within the polygon boundries from an array of Points
|
67
|
+
# and appends them to @collected_points. All rejected Points are stored
|
68
|
+
# under @rejected_points (if you want to pass the to other objects).
|
69
|
+
#
|
70
|
+
# @param points [Array<Hexagonly::Point>]
|
71
|
+
#
|
72
|
+
# @return [Array<Hexagonly::Point] the grabed points
|
73
|
+
def grab(points)
|
74
|
+
parts = points.partition{ |p| contains?(p) }
|
75
|
+
@collected_points ||= []
|
76
|
+
@collected_points += parts[0]
|
77
|
+
@rejected_points = parts[1]
|
78
|
+
|
79
|
+
parts[0]
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
include Methods
|
85
|
+
|
86
|
+
attr_accessor :poly_points
|
87
|
+
|
88
|
+
# @param [Array<Hexagonly::Point>] poly_points the points that make up the polygon
|
89
|
+
def initialize(poly_points)
|
90
|
+
@poly_points = poly_points
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Hexagonly
|
2
|
+
class Space
|
3
|
+
|
4
|
+
attr_reader :points, :north, :west, :south, :east, :height, :width, :center
|
5
|
+
|
6
|
+
# @param [Array<Hexagonly::Point>] points an array of points that make up the space
|
7
|
+
def initialize(points)
|
8
|
+
@points = points
|
9
|
+
refresh
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def refresh
|
15
|
+
compute_boundries
|
16
|
+
compute_center
|
17
|
+
end
|
18
|
+
|
19
|
+
def compute_boundries
|
20
|
+
@north, @west, @south, @east = nil
|
21
|
+
@points.each do |p|
|
22
|
+
@north = p if @north.nil? || @north.y_coord < p.y_coord
|
23
|
+
@west = p if @west.nil? || @west.x_coord > p.x_coord
|
24
|
+
@south = p if @south.nil? || @south.y_coord > p.y_coord
|
25
|
+
@east = p if @east.nil? || @east.x_coord < p.x_coord
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def compute_center
|
30
|
+
compute_boundries if @north.nil? || @west.nil? || @south.nil? || @east.nil?
|
31
|
+
@height = @north.y_coord - @south.y_coord
|
32
|
+
@width = @east.x_coord - @west.x_coord
|
33
|
+
@center = Hexagonly::Point.new(@width / 2 + @west.x_coord, @height / 2 + @south.y_coord)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
data/lib/hexagonly.rb
ADDED
data/localities.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# bundle exec ruby localities.rb | xclip -selection clipboard
|
2
|
+
|
3
|
+
require 'hexagonly'
|
4
|
+
require 'csv'
|
5
|
+
|
6
|
+
localities_csv = CSV.read(File.expand_path('../spec/fixtures/localities.csv', __FILE__))
|
7
|
+
|
8
|
+
points = []
|
9
|
+
localities_csv.first(1000).each do |row|
|
10
|
+
points << Hexagonly::Point.new(row[2].to_f, row[1].to_f)
|
11
|
+
end
|
12
|
+
|
13
|
+
hexagons = Hexagonly::Hexagon.pack(points, 0.4, { grab_points: true, reject_empty: true })
|
14
|
+
|
15
|
+
geo = Hexagonly::GeoJson.new(hexagons)
|
16
|
+
# geo.add_features(points)
|
17
|
+
puts geo.to_json
|
data/spec/factories.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
FactoryGirl.define do
|
4
|
+
localities_csv = CSV.read(File.expand_path('../fixtures/localities.csv', __FILE__))
|
5
|
+
|
6
|
+
sequence(:x) { |n| localities_csv.size >= n ? localities_csv[n][2].to_f : nil }
|
7
|
+
sequence(:y) { |n| localities_csv.size >= n ? localities_csv[n][1].to_f : nil }
|
8
|
+
|
9
|
+
factory :point, :class => Hexagonly::Point do
|
10
|
+
initialize_with { new(generate(:x), generate(:y)) }
|
11
|
+
end
|
12
|
+
end
|