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.
@@ -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
@@ -0,0 +1,3 @@
1
+ module Hexagonly
2
+ VERSION = "0.1.0"
3
+ end
data/lib/hexagonly.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'hexagonly/version'
2
+
3
+ require 'hexagonly/point'
4
+ require 'hexagonly/space'
5
+ require 'hexagonly/polygon'
6
+ require 'hexagonly/hexagon'
7
+ require 'hexagonly/geo_json'
8
+
9
+ module Hexagonly
10
+
11
+ end
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