dse-driver 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,181 @@
1
+ # encoding: utf-8
2
+
3
+ #--
4
+ # Copyright (C) 2016 DataStax Inc.
5
+ #
6
+ # This software can be used solely with DataStax Enterprise. Please consult the license at
7
+ # http://www.datastax.com/terms/datastax-dse-driver-license-terms
8
+ #++
9
+
10
+ module Dse
11
+ module Geometry
12
+ # Encapsulates a set of lines, characterized by a sequence of {Point}s in the xy-plane. It corresponds to the
13
+ # `org.apache.cassandra.db.marshal.LineStringType` column type in DSE.
14
+ #
15
+ # @see https://en.wikipedia.org/wiki/Well-known_text Wikipedia article on Well Known Text
16
+ class LineString
17
+ include Cassandra::CustomData
18
+
19
+ # @return [Array<Point>] collection of points that make up this line-string.
20
+ attr_reader :points
21
+
22
+ # @private
23
+ WKT_RE = /^LINESTRING\s*\(\s*([^)]+)\s*\)$/
24
+ # @private
25
+ POINT_SEPARATOR_RE = /\s*,\s*/
26
+
27
+ # @param args [Array<Point>,Array<String>] varargs-style arguments in two forms:
28
+ # <ul><li>ordered collection of points that make up this line-string.
29
+ # Must be empty or have at least two points.</li>
30
+ # <li>one-element string array with the wkt representation.</li></ul>
31
+ #
32
+ # @example Construct an empty LineString
33
+ # line = LineString.new
34
+ # @example Construct a LineString with Point objects.
35
+ # line = LineString.new(Point.new(1.0, 2.0), Point.new(3.0, 4.0))
36
+ # @example Construct a LineString with a wkt string.
37
+ # line = LineString.new('LINESTRING (1.0 2.0, 3.0 4.0)')
38
+ def initialize(*args)
39
+ # The constructor has two forms:
40
+ # 1. 0, 2, or more Point objects in the order in which one connects them to make a line-string.
41
+ # 2. one String arg as the wkt representation.
42
+
43
+ if args.size == 1
44
+ wkt = args.first
45
+ Cassandra::Util.assert_instance_of(String, wkt)
46
+
47
+ if wkt == 'LINESTRING EMPTY'
48
+ @points = [].freeze
49
+ else
50
+ match = wkt.match(WKT_RE)
51
+ raise ArgumentError, "#{wkt.inspect} is not a valid WKT representation of a line-string" unless match
52
+ @points = self.class.parse_wkt_internal(match[1])
53
+ end
54
+ else
55
+ @points = args.freeze
56
+ @points.each do |p|
57
+ Cassandra::Util.assert_instance_of(Point, p, "#{p.inspect} is not a Point")
58
+ end
59
+ end
60
+ end
61
+
62
+ # @private
63
+ def self.parse_wkt_internal(line_str)
64
+ point_strings = line_str.split(POINT_SEPARATOR_RE)
65
+ points = []
66
+ point_strings.each do |ps|
67
+ next if ps.empty?
68
+ points << Point.new(*Point.parse_wkt_internal(ps))
69
+ end
70
+ points.freeze
71
+ end
72
+
73
+ # @return [String] well-known-text representation of this line-string.
74
+ def wkt
75
+ @points.empty? ? 'LINESTRING EMPTY' : "LINESTRING (#{wkt_internal})"
76
+ end
77
+
78
+ # @return [String] a human-readable English string describing this {LineString}.
79
+ def to_s
80
+ @points.join(' to ')
81
+ end
82
+
83
+ # @private
84
+ def wkt_internal
85
+ # This is a helper used to embed point coords into some container (e.g. polygon)
86
+ @points.map(&:wkt_internal).join(', ')
87
+ end
88
+
89
+ # @private
90
+ def eql?(other)
91
+ other.is_a?(LineString) && \
92
+ @points == other.points
93
+ end
94
+ alias == eql?
95
+
96
+ # @private
97
+ def hash
98
+ @hash ||= 31 * 17 + @points.hash
99
+ end
100
+
101
+ # @private
102
+ def inspect
103
+ "#<LineString:0x#{object_id.to_s(16)} " \
104
+ "@points=#{@points.inspect}>"
105
+ end
106
+
107
+ # methods related to serializing/deserializing.
108
+
109
+ # @private
110
+ TYPE = Cassandra::Types::Custom.new('org.apache.cassandra.db.marshal.LineStringType')
111
+
112
+ # @return [Cassandra::Types::Custom] type of column that is processed by this domain object class.
113
+ def self.type
114
+ TYPE
115
+ end
116
+
117
+ # Deserialize the given data into an instance of this domain object class.
118
+ # @param data [String] byte-array representation of a column value of this custom type.
119
+ # @return [LineString]
120
+ # @raise [Cassandra::Errors::DecodingError] upon failure.
121
+ def self.deserialize(data)
122
+ buffer = Cassandra::Protocol::CqlByteBuffer.new(data)
123
+ little_endian = buffer.read(1) != "\x00"
124
+
125
+ # Depending on the endian-ness of the data, we want to read it differently. Wrap the buffer
126
+ # with an "endian-aware" reader that reads the desired way.
127
+ buffer = Dse::Util::EndianBuffer.new(buffer, little_endian)
128
+
129
+ type = buffer.read_unsigned
130
+ raise Cassandra::Errors::DecodingError, "LineString data-type value should be 2, but was #{type}" if type != 2
131
+
132
+ deserialize_raw(buffer)
133
+ end
134
+
135
+ # This is a helper function to deserialize the meat of the data (after we've accounted for endianness
136
+ # and other metadata)
137
+ # @private
138
+ def self.deserialize_raw(buffer)
139
+ # Now comes the number of points in the line-string.
140
+ num_points = buffer.read_unsigned
141
+
142
+ # Read that many x,y coords from the buffer.
143
+ points = []
144
+ num_points.times do
145
+ points << Point.deserialize_raw(buffer)
146
+ end
147
+ LineString.new(*points)
148
+ end
149
+
150
+ # Serialize this domain object into a byte array to send to DSE.
151
+ # @return [String] byte-array representation of this domain object.
152
+ def serialize
153
+ buffer = Cassandra::Protocol::CqlByteBuffer.new
154
+
155
+ # We serialize in little-endian form.
156
+
157
+ buffer << "\x01"
158
+
159
+ # This is a line-string.
160
+ buffer.append([2].pack(Cassandra::Protocol::Formats::INT_FORMAT_LE))
161
+
162
+ # Write out the count of how many points we have.
163
+ serialize_raw(buffer)
164
+
165
+ buffer
166
+ end
167
+
168
+ # This is a helper function to serialize the meat of the data (after we've accounted for endianness
169
+ # and other metadata)
170
+ # @private
171
+ def serialize_raw(buffer)
172
+ buffer.append([@points.size].pack(Cassandra::Protocol::Formats::INT_FORMAT_LE))
173
+
174
+ # Now write out x and y for each point.
175
+ @points.each do |point|
176
+ point.serialize_raw(buffer)
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,179 @@
1
+ # encoding: utf-8
2
+
3
+ #--
4
+ # Copyright (C) 2016 DataStax Inc.
5
+ #
6
+ # This software can be used solely with DataStax Enterprise. Please consult the license at
7
+ # http://www.datastax.com/terms/datastax-dse-driver-license-terms
8
+ #++
9
+
10
+ module Dse
11
+ module Geometry
12
+ # Encapsulates a 2D point with x,y coordinates. It corresponds to the `org.apache.cassandra.db.marshal.PointType`
13
+ # column type in DSE.
14
+ #
15
+ # @see https://en.wikipedia.org/wiki/Well-known_text Wikipedia article on Well Known Text
16
+ class Point
17
+ include Cassandra::CustomData
18
+
19
+ # @return [Float] the x coordinate of the point.
20
+ attr_reader :x
21
+ # @return [Float] the y coordinate of the point.
22
+ attr_reader :y
23
+
24
+ # @private
25
+ WKT_RE = /^POINT\s*\(\s*([^)]+?)\s*\)$/
26
+ # @private
27
+ POINT_SPEC_RE = /^([0-9\-\.]+)\s+([0-9\-\.]+)$/
28
+ # @private
29
+ EOL_RE = /[\r\n]/
30
+
31
+ # @param args [Array<Numeric>,Array<String>] varargs-style arguments in two forms:
32
+ # <ul><li>two-element numeric array representing x,y coordinates.</li>
33
+ # <li>one-element string array with the wkt representation.</li></ul>
34
+ #
35
+ # @example Construct a Point with numeric arguments.
36
+ # point = Point.new(3, 4)
37
+ # @example Construct a Point with a wkt string.
38
+ # point = Point.new('POINT (3.0 4.0)')
39
+ def initialize(*args)
40
+ # The constructor has two forms:
41
+ # 1. two numeric args (x,y)
42
+ # 2. one String arg as the wkt representation.
43
+
44
+ case args.size
45
+ when 2
46
+ x, y = args
47
+ Cassandra::Util.assert_instance_of(::Numeric, x)
48
+ Cassandra::Util.assert_instance_of(::Numeric, y)
49
+ Cassandra::Util.assert(!x.nan?, 'x cannot be Float::NAN') if x.is_a?(Float)
50
+ Cassandra::Util.assert(!y.nan?, 'y cannot be Float::NAN') if y.is_a?(Float)
51
+ @x = x.to_f
52
+ @y = y.to_f
53
+ when 1
54
+ wkt = args.first
55
+ Cassandra::Util.assert_instance_of(String, wkt)
56
+ # subsitute eol chars in the string with a space.
57
+ wkt.gsub!(EOL_RE, ' ')
58
+ match = wkt.match(WKT_RE)
59
+ raise ArgumentError, "#{wkt.inspect} is not a valid WKT representation of a point" unless match
60
+ @x, @y = self.class.parse_wkt_internal(match[1])
61
+ else
62
+ raise ArgumentError,
63
+ 'wrong number of arguments: use one string argument (wkt) or two numeric arguments (x,y)'
64
+ end
65
+ end
66
+
67
+ # @return [String] well-known-text representation of this point.
68
+ def wkt
69
+ "POINT (#{wkt_internal})"
70
+ end
71
+
72
+ # @return [String] a human-readable English string describing this {Point}.
73
+ def to_s
74
+ "#{@x},#{@y}"
75
+ end
76
+
77
+ # @private
78
+ def wkt_internal
79
+ # This is a helper used to embed point coords into some container (e.g. line-string, polygon)
80
+ "#{@x} #{@y}"
81
+ end
82
+
83
+ # @private
84
+ def self.parse_wkt_internal(point_str)
85
+ point_str.rstrip!
86
+ match = point_str.match(POINT_SPEC_RE)
87
+ raise ArgumentError, "#{point_str.inspect} is not a valid WKT representation of a point" unless match
88
+ [match[1].to_f, match[2].to_f]
89
+ end
90
+
91
+ # @private
92
+ def eql?(other)
93
+ other.is_a?(Point) && \
94
+ @x == other.x && \
95
+ @y == other.y
96
+ end
97
+ alias == eql?
98
+
99
+ # @private
100
+ def hash
101
+ @hash ||= begin
102
+ h = 17
103
+ h = 31 * h + @x.hash
104
+ h = 31 * h + @y.hash
105
+ h
106
+ end
107
+ end
108
+
109
+ # @private
110
+ def inspect
111
+ "#<Point:0x#{object_id.to_s(16)} " \
112
+ "@x=#{@x.inspect}, " \
113
+ "@y=#{@y.inspect}>"
114
+ end
115
+
116
+ # methods related to serializing/deserializing.
117
+
118
+ # @private
119
+ TYPE = Cassandra::Types::Custom.new('org.apache.cassandra.db.marshal.PointType')
120
+
121
+ # @return [Cassandra::Types::Custom] type of column that is processed by this domain object class.
122
+ def self.type
123
+ TYPE
124
+ end
125
+
126
+ # Deserialize the given data into an instance of this domain object class.
127
+ # @param data [String] byte-array representation of a column value of this custom type.
128
+ # @return [Point]
129
+ # @raise [Cassandra::Errors::DecodingError] upon failure.
130
+ def self.deserialize(data)
131
+ buffer = Cassandra::Protocol::CqlByteBuffer.new(data)
132
+ little_endian = buffer.read(1) != "\x00"
133
+
134
+ # Depending on the endian-ness of the data, we want to read it differently. Wrap the buffer
135
+ # with an "endian-aware" reader that reads the desired way.
136
+ buffer = Dse::Util::EndianBuffer.new(buffer, little_endian)
137
+
138
+ type = buffer.read_unsigned
139
+ raise Cassandra::Errors::DecodingError, "Point data-type value should be 1, but was #{type}" if type != 1
140
+ deserialize_raw(buffer)
141
+ end
142
+
143
+ # This is a helper function to deserialize the meat of the data (after we've accounted for endianness
144
+ # and other metadata)
145
+ # @private
146
+ def self.deserialize_raw(buffer)
147
+ x = buffer.read_double
148
+ y = buffer.read_double
149
+ Point.new(x, y)
150
+ end
151
+
152
+ # Serialize this domain object into a byte array to send to DSE.
153
+ # @return [String] byte-array representation of this domain object.
154
+ def serialize
155
+ buffer = Cassandra::Protocol::CqlByteBuffer.new
156
+
157
+ # Serialize little-endian.
158
+
159
+ buffer << "\x01"
160
+
161
+ # This is a point.
162
+ buffer.append([1].pack(Cassandra::Protocol::Formats::INT_FORMAT_LE))
163
+
164
+ # Write out x and y.
165
+ serialize_raw(buffer)
166
+
167
+ buffer
168
+ end
169
+
170
+ # This is a helper function to serialize the meat of the data (after we've accounted for endianness
171
+ # and other metadata)
172
+ # @private
173
+ def serialize_raw(buffer)
174
+ buffer.append([@x].pack(Cassandra::Protocol::Formats::DOUBLE_FORMAT_LE))
175
+ buffer.append([@y].pack(Cassandra::Protocol::Formats::DOUBLE_FORMAT_LE))
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,196 @@
1
+ # encoding: utf-8
2
+
3
+ #--
4
+ # Copyright (C) 2016 DataStax Inc.
5
+ #
6
+ # This software can be used solely with DataStax Enterprise. Please consult the license at
7
+ # http://www.datastax.com/terms/datastax-dse-driver-license-terms
8
+ #++
9
+
10
+ module Dse
11
+ module Geometry
12
+ # Encapsulates a polygon consisting of a set of linear-rings in the xy-plane. It corresponds to the
13
+ # `org.apache.cassandra.db.marshal.PolygonType` column type in DSE.
14
+ #
15
+ # A linear-ring is a {LineString} whose last point is the same as its first point. The first ring specified
16
+ # in a polygon defines the outer edges of the polygon and is called the _exterior ring_. A polygon may also have
17
+ # _holes_ within it, specified by other linear-rings, and those holes may contain linear-rings indicating
18
+ # _islands_. All such rings are called _interior rings_.
19
+ #
20
+ # @see https://en.wikipedia.org/wiki/Well-known_text Wikipedia article on Well Known Text
21
+ class Polygon
22
+ include Cassandra::CustomData
23
+
24
+ # @private
25
+ WKT_RE = /^POLYGON\s*\(\s*(.+?)\s*\)$/
26
+ # @private
27
+ LINESTRING_SEPARATOR_RE = /\),?/
28
+ # @private
29
+ EOL_RE = /[\r\n]/
30
+
31
+ # @param args [Array<LineString>,Array<String>] varargs-style arguments in two forms:
32
+ # <ul><li>ordered collection of linear-rings that make up this polygon. Can be empty.</li>
33
+ # <li>one-element string array with the wkt representation.</li></ul>
34
+ #
35
+ # @example Construct an empty Polygon
36
+ # polygon = Polygon.new
37
+ # @example Construct a Polygon with LineString objects.
38
+ # exterior_ring = LineString.new(Point.new(0, 0), Point.new(10, 0), Point.new(10, 10), Point.new(0, 0))
39
+ # interior_ring = LineString.new(Point.new(1, 1), Point.new(1, 5), Point.new(5, 1), Point.new(1, 1))
40
+ # polygon = Polygon.new(exterior_ring, interior_ring)
41
+ # @example Construct a line-string with a wkt string.
42
+ # polygon = Polygon.new('POLYGON ((0.0 0.0, 10.0 0.0, 10.0 10.0, 0.0 0.0), ' \
43
+ # '(1.0 1.0, 1.0 5.0, 5.0 1.0, 1.0 1.0))')
44
+ def initialize(*args)
45
+ # The constructor has two forms:
46
+ # 1. 0 or more LineString objects where the first is the exterior ring and the rest are interior rings.
47
+ # 2. one String arg as the wkt representation.
48
+
49
+ if args.size == 1 && args.first.is_a?(String)
50
+ # subsitute eol chars in the string with a space.
51
+ wkt = args.first.gsub(EOL_RE, ' ')
52
+ # Consolidate whitespace before/after commas and parens.
53
+ wkt.gsub!(/\s*([,\(\)])\s*/, '\1')
54
+ if wkt == 'POLYGON EMPTY'
55
+ @rings = [].freeze
56
+ else
57
+ match = wkt.match(WKT_RE)
58
+ raise ArgumentError, "#{wkt.inspect} is not a valid WKT representation of a polygon" unless match
59
+ @rings = parse_wkt_internal(match[1])
60
+ end
61
+ else
62
+ @rings = args.freeze
63
+ @rings.each do |ring|
64
+ Cassandra::Util.assert_instance_of(LineString, ring, "#{ring.inspect} is not a LineString")
65
+ end
66
+ end
67
+ end
68
+
69
+ # @private
70
+ def parse_wkt_internal(poly_str)
71
+ # poly_str is everything inside the outer parens. Example:
72
+ # original: POLYGON ( (1 2, 3 4, 5 6, 1 2), (9 8, 7 6, 5 4, 9 8) )
73
+ # poly_str: (1 2, 3 4, 5 6, 1 2), (9 8, 7 6, 5 4, 9 8)
74
+ line_string_strings = poly_str.split(LINESTRING_SEPARATOR_RE)
75
+ line_strings = []
76
+ line_string_strings.each do |ls|
77
+ # ls still has a leading open paren and possibly spaces.
78
+ ls.sub!(/\s*\(\s*/, '')
79
+ line_strings << LineString.new(*LineString.parse_wkt_internal(ls))
80
+ end
81
+ line_strings.freeze
82
+ end
83
+
84
+ # @return [LineString] linear-ring characterizing the exterior of the polygon. `nil` for empty polygon.
85
+ def exterior_ring
86
+ @rings.first
87
+ end
88
+
89
+ # @return [Array<LineString>] ordered collection of linear-rings that make up the interior of this polygon.
90
+ # Empty if there are no interior rings.
91
+ def interior_rings
92
+ @interior_rings ||= (@rings[1..-1] || []).freeze
93
+ end
94
+
95
+ # @return [String] well-known-text representation of this polygon.
96
+ def wkt
97
+ return 'POLYGON EMPTY' if @rings.empty?
98
+
99
+ result = 'POLYGON ('
100
+ first = true
101
+ @rings.each do |ring|
102
+ result += ', ' unless first
103
+ first = false
104
+ result += "(#{ring.wkt_internal})"
105
+ end
106
+ result += ')'
107
+ result
108
+ end
109
+
110
+ # @return [String] a human-readable English string describing this {Polygon}.
111
+ def to_s
112
+ "Exterior ring: #{@rings.first}\n" \
113
+ "Interior rings:\n " +
114
+ interior_rings.join("\n ")
115
+ end
116
+
117
+ # @private
118
+ def eql?(other)
119
+ other.is_a?(Polygon) && \
120
+ @rings == other.instance_variable_get(:@rings)
121
+ end
122
+ alias == eql?
123
+
124
+ # @private
125
+ def hash
126
+ @hash ||= 31 * 17 + @rings.hash
127
+ end
128
+
129
+ # @private
130
+ def inspect
131
+ "#<Polygon:0x#{object_id.to_s(16)} " \
132
+ "@exterior_ring=#{@rings.first.inspect}, " \
133
+ "@interior_rings=#{interior_rings.inspect}>"
134
+ end
135
+
136
+ # methods related to serializing/deserializing.
137
+
138
+ # @private
139
+ TYPE = Cassandra::Types::Custom.new('org.apache.cassandra.db.marshal.PolygonType')
140
+
141
+ # @return [Cassandra::Types::Custom] type of column that is processed by this domain object class.
142
+ def self.type
143
+ TYPE
144
+ end
145
+
146
+ # Deserialize the given data into an instance of this domain object class.
147
+ # @param data [String] byte-array representation of a column value of this custom type.
148
+ # @return [Polygon]
149
+ # @raise [Cassandra::Errors::DecodingError] upon failure.
150
+ def self.deserialize(data)
151
+ buffer = Cassandra::Protocol::CqlByteBuffer.new(data)
152
+ little_endian = buffer.read(1) != "\x00"
153
+
154
+ # Depending on the endian-ness of the data, we want to read it differently. Wrap the buffer
155
+ # with an "endian-aware" reader that reads the desired way.
156
+ buffer = Dse::Util::EndianBuffer.new(buffer, little_endian)
157
+
158
+ type = buffer.read_unsigned
159
+ raise Cassandra::Errors::DecodingError, "LineString data-type value should be 3, but was #{type}" if type != 3
160
+
161
+ # Now comes the number of rings.
162
+ num_rings = buffer.read_unsigned
163
+
164
+ # Read that many line-string's (rings) from the buffer.
165
+ rings = []
166
+ num_rings.times do
167
+ rings << LineString.deserialize_raw(buffer)
168
+ end
169
+ Polygon.new(*rings)
170
+ end
171
+
172
+ # Serialize this domain object into a byte array to send to DSE.
173
+ # @return [String] byte-array representation of this domain object.
174
+ def serialize
175
+ buffer = Cassandra::Protocol::CqlByteBuffer.new
176
+
177
+ # We serialize in little-endian form.
178
+
179
+ buffer << "\x01"
180
+
181
+ # This is a polygon.
182
+ buffer.append([3].pack(Cassandra::Protocol::Formats::INT_FORMAT_LE))
183
+
184
+ # Write out the count of how many rings we have.
185
+ buffer.append([@rings.size].pack(Cassandra::Protocol::Formats::INT_FORMAT_LE))
186
+
187
+ # Now write out the raw serialization of each ring (e.g. linestring).
188
+ @rings.each do |ring|
189
+ ring.serialize_raw(buffer)
190
+ end
191
+
192
+ buffer
193
+ end
194
+ end
195
+ end
196
+ end