geodetic 0.5.2 → 0.7.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 +4 -4
- data/CHANGELOG.md +75 -0
- data/README.md +94 -0
- data/docs/index.md +4 -0
- data/docs/reference/geos-acceleration.md +170 -0
- data/docs/reference/wkb.md +385 -0
- data/docs/reference/wkt.md +354 -0
- data/examples/10_wkt_serialization.rb +248 -0
- data/examples/11_wkb_serialization.rb +261 -0
- data/examples/12_geos_benchmark.rb +258 -0
- data/examples/13_geos_operations.rb +538 -0
- data/examples/README.md +82 -0
- data/examples/geodetic_demo.wkb +0 -0
- data/examples/geodetic_demo.wkt +5 -0
- data/examples/geodetic_demo_output.wkb.hex +5 -0
- data/examples/sample_geometries.wkb +0 -0
- data/examples/sample_geometries.wkb.hex +60 -0
- data/lib/geodetic/areas/polygon.rb +66 -10
- data/lib/geodetic/geos.rb +547 -0
- data/lib/geodetic/path.rb +20 -8
- data/lib/geodetic/version.rb +1 -1
- data/lib/geodetic/wkb.rb +360 -0
- data/lib/geodetic/wkt.rb +313 -0
- data/lib/geodetic.rb +3 -0
- data/mkdocs.yml +2 -0
- metadata +16 -1
data/lib/geodetic/path.rb
CHANGED
|
@@ -187,15 +187,11 @@ module Geodetic
|
|
|
187
187
|
def intersects?(other_path)
|
|
188
188
|
raise ArgumentError, "expected a Path" unless other_path.is_a?(Path)
|
|
189
189
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
other_path
|
|
194
|
-
return true if seg1.intersects?(seg2)
|
|
195
|
-
end
|
|
190
|
+
if Geos.available?
|
|
191
|
+
geos_intersects?(other_path)
|
|
192
|
+
else
|
|
193
|
+
ruby_intersects?(other_path)
|
|
196
194
|
end
|
|
197
|
-
|
|
198
|
-
false
|
|
199
195
|
end
|
|
200
196
|
|
|
201
197
|
def total_distance
|
|
@@ -524,5 +520,21 @@ module Geodetic
|
|
|
524
520
|
Math.cos(r1) + Math.cos(r2)
|
|
525
521
|
) * DEG_PER_RAD
|
|
526
522
|
end
|
|
523
|
+
|
|
524
|
+
def geos_intersects?(other_path)
|
|
525
|
+
Geos.intersects?(self, other_path)
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def ruby_intersects?(other_path)
|
|
529
|
+
return false unless bounds_overlap?(bounds, other_path.bounds)
|
|
530
|
+
|
|
531
|
+
segments.each do |seg1|
|
|
532
|
+
other_path.segments.each do |seg2|
|
|
533
|
+
return true if seg1.intersects?(seg2)
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
false
|
|
538
|
+
end
|
|
527
539
|
end
|
|
528
540
|
end
|
data/lib/geodetic/version.rb
CHANGED
data/lib/geodetic/wkb.rb
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Geodetic
|
|
4
|
+
module WKB
|
|
5
|
+
# Type codes
|
|
6
|
+
TYPE_POINT = 1
|
|
7
|
+
TYPE_LINE_STRING = 2
|
|
8
|
+
TYPE_POLYGON = 3
|
|
9
|
+
TYPE_MULTI_POINT = 4
|
|
10
|
+
TYPE_MULTI_LINE_STRING = 5
|
|
11
|
+
TYPE_MULTI_POLYGON = 6
|
|
12
|
+
TYPE_GEOMETRY_COLLECTION = 7
|
|
13
|
+
|
|
14
|
+
# Z offset (ISO WKB)
|
|
15
|
+
ISO_Z_OFFSET = 1000
|
|
16
|
+
|
|
17
|
+
# EWKB flags
|
|
18
|
+
EWKB_SRID_FLAG = 0x20000000
|
|
19
|
+
EWKB_Z_FLAG = 0x80000000
|
|
20
|
+
|
|
21
|
+
# Output is always little-endian (NDR). This matches PostGIS, GEOS, RGeo,
|
|
22
|
+
# Shapely, and virtually all modern GIS tools. Big-endian (XDR) input is
|
|
23
|
+
# fully supported by the parser.
|
|
24
|
+
BYTE_ORDER_LE = 0x01
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# --- Export helpers (private, used by to_wkb on each type) ---
|
|
28
|
+
|
|
29
|
+
def encode_point(lla, srid: nil)
|
|
30
|
+
has_z = lla.alt != 0.0
|
|
31
|
+
pack_header(TYPE_POINT, srid: srid, has_z: has_z) +
|
|
32
|
+
pack_coords(lla, has_z: has_z)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def encode_line_string(points, srid: nil)
|
|
36
|
+
has_z = has_altitude?(points)
|
|
37
|
+
pack_header(TYPE_LINE_STRING, srid: srid, has_z: has_z) +
|
|
38
|
+
[points.length].pack("V") +
|
|
39
|
+
points.map { |p| pack_coords(p, has_z: has_z) }.join
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def encode_polygon(rings, srid: nil)
|
|
43
|
+
has_z = rings.any? { |ring| has_altitude?(ring) }
|
|
44
|
+
pack_header(TYPE_POLYGON, srid: srid, has_z: has_z) +
|
|
45
|
+
[rings.length].pack("V") +
|
|
46
|
+
rings.map { |ring|
|
|
47
|
+
[ring.length].pack("V") +
|
|
48
|
+
ring.map { |p| pack_coords(p, has_z: has_z) }.join
|
|
49
|
+
}.join
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# --- File I/O ---
|
|
53
|
+
|
|
54
|
+
def save!(path, *objects, srid: nil)
|
|
55
|
+
list = objects.length == 1 && objects[0].is_a?(Array) ? objects[0] : objects
|
|
56
|
+
File.open(path, "wb") do |f|
|
|
57
|
+
f.write([list.length].pack("V"))
|
|
58
|
+
list.each do |obj|
|
|
59
|
+
bytes = obj.to_wkb(srid: srid)
|
|
60
|
+
f.write([bytes.bytesize].pack("V"))
|
|
61
|
+
f.write(bytes)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def load(path)
|
|
67
|
+
data = File.binread(path)
|
|
68
|
+
count = data[0, 4].unpack1("V")
|
|
69
|
+
pos = 4
|
|
70
|
+
count.times.map do
|
|
71
|
+
size = data[pos, 4].unpack1("V")
|
|
72
|
+
pos += 4
|
|
73
|
+
geom_bytes = data[pos, size]
|
|
74
|
+
pos += size
|
|
75
|
+
parse(geom_bytes)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def save_hex!(path, *objects, srid: nil)
|
|
80
|
+
list = objects.length == 1 && objects[0].is_a?(Array) ? objects[0] : objects
|
|
81
|
+
File.open(path, "w") do |f|
|
|
82
|
+
list.each { |obj| f.puts obj.to_wkb_hex(srid: srid) }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def load_hex(path)
|
|
87
|
+
File.readlines(path, chomp: true)
|
|
88
|
+
.reject { |line| line.strip.empty? || line.strip.start_with?("#") }
|
|
89
|
+
.map { |line| parse(line.strip) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# --- Import ---
|
|
93
|
+
|
|
94
|
+
def parse(input)
|
|
95
|
+
reader = Reader.new(to_binary(input))
|
|
96
|
+
reader.read_geometry
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def parse_with_srid(input)
|
|
100
|
+
reader = Reader.new(to_binary(input))
|
|
101
|
+
geom = reader.read_geometry
|
|
102
|
+
[geom, reader.srid]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def has_altitude?(points)
|
|
108
|
+
points.any? { |p| p.alt != 0.0 }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def pack_header(type, srid: nil, has_z: false)
|
|
112
|
+
type_int = has_z ? type + ISO_Z_OFFSET : type
|
|
113
|
+
type_int |= EWKB_SRID_FLAG if srid
|
|
114
|
+
result = [BYTE_ORDER_LE].pack("C") + [type_int].pack("V")
|
|
115
|
+
result += [srid].pack("V") if srid
|
|
116
|
+
result
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def pack_coords(lla, has_z:)
|
|
120
|
+
if has_z
|
|
121
|
+
[lla.lng, lla.lat, lla.alt].pack("E3")
|
|
122
|
+
else
|
|
123
|
+
[lla.lng, lla.lat].pack("E2")
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def to_binary(input)
|
|
128
|
+
if input.encoding == Encoding::ASCII_8BIT
|
|
129
|
+
# Could be binary or hex that happens to be ASCII-8BIT
|
|
130
|
+
if input.bytes.any? { |b| b > 127 } || input.bytesize < 5
|
|
131
|
+
return input
|
|
132
|
+
end
|
|
133
|
+
# Check if it looks like hex
|
|
134
|
+
if input.match?(/\A[0-9a-fA-F]+\z/)
|
|
135
|
+
return [input].pack("H*")
|
|
136
|
+
end
|
|
137
|
+
input
|
|
138
|
+
else
|
|
139
|
+
# String encoding — treat as hex
|
|
140
|
+
stripped = input.strip
|
|
141
|
+
if stripped.match?(/\A[0-9a-fA-F]+\z/) && stripped.length.even?
|
|
142
|
+
[stripped].pack("H*")
|
|
143
|
+
else
|
|
144
|
+
stripped.b
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------
|
|
151
|
+
# Reader for parsing WKB binary data
|
|
152
|
+
# ---------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
class Reader
|
|
155
|
+
attr_reader :srid
|
|
156
|
+
|
|
157
|
+
def initialize(data)
|
|
158
|
+
@data = data.b
|
|
159
|
+
@pos = 0
|
|
160
|
+
@srid = nil
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def read_geometry
|
|
164
|
+
byte_order = read_bytes(1).ord
|
|
165
|
+
@le = byte_order == 0x01
|
|
166
|
+
|
|
167
|
+
type_int = read_uint32
|
|
168
|
+
has_srid = (type_int & EWKB_SRID_FLAG) != 0
|
|
169
|
+
has_z_ewkb = (type_int & EWKB_Z_FLAG) != 0
|
|
170
|
+
|
|
171
|
+
type_int &= ~EWKB_SRID_FLAG & ~EWKB_Z_FLAG
|
|
172
|
+
|
|
173
|
+
@srid = read_uint32 if has_srid
|
|
174
|
+
|
|
175
|
+
has_z = has_z_ewkb
|
|
176
|
+
if type_int >= ISO_Z_OFFSET && type_int < 2000
|
|
177
|
+
has_z = true
|
|
178
|
+
type_int -= ISO_Z_OFFSET
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
case type_int
|
|
182
|
+
when TYPE_POINT then read_point(has_z)
|
|
183
|
+
when TYPE_LINE_STRING then read_line_string(has_z)
|
|
184
|
+
when TYPE_POLYGON then read_polygon(has_z)
|
|
185
|
+
when TYPE_MULTI_POINT then read_multi
|
|
186
|
+
when TYPE_MULTI_LINE_STRING then read_multi
|
|
187
|
+
when TYPE_MULTI_POLYGON then read_multi
|
|
188
|
+
when TYPE_GEOMETRY_COLLECTION then read_multi
|
|
189
|
+
else
|
|
190
|
+
raise ArgumentError, "unknown WKB type code: #{type_int}"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
private
|
|
195
|
+
|
|
196
|
+
def read_bytes(n)
|
|
197
|
+
result = @data[@pos, n]
|
|
198
|
+
@pos += n
|
|
199
|
+
result
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def read_uint32
|
|
203
|
+
bytes = read_bytes(4)
|
|
204
|
+
bytes.unpack1(@le ? "V" : "N")
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def read_double
|
|
208
|
+
bytes = read_bytes(8)
|
|
209
|
+
bytes.unpack1(@le ? "E" : "G")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def read_point(has_z)
|
|
213
|
+
lng = read_double
|
|
214
|
+
lat = read_double
|
|
215
|
+
alt = has_z ? read_double : 0.0
|
|
216
|
+
Coordinate::LLA.new(lat: lat, lng: lng, alt: alt)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def read_line_string(has_z)
|
|
220
|
+
count = read_uint32
|
|
221
|
+
points = count.times.map { read_point(has_z) }
|
|
222
|
+
if points.length == 2
|
|
223
|
+
Segment.new(points[0], points[1])
|
|
224
|
+
else
|
|
225
|
+
Path.new(coordinates: points)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def read_polygon(has_z)
|
|
230
|
+
ring_count = read_uint32
|
|
231
|
+
rings = ring_count.times.map do
|
|
232
|
+
point_count = read_uint32
|
|
233
|
+
point_count.times.map { read_point(has_z) }
|
|
234
|
+
end
|
|
235
|
+
outer = rings[0]
|
|
236
|
+
outer.pop if outer.length > 1 && outer.first == outer.last
|
|
237
|
+
Areas::Polygon.new(boundary: outer)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def read_multi
|
|
241
|
+
count = read_uint32
|
|
242
|
+
count.times.map { read_geometry }
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# ---------------------------------------------------------------
|
|
247
|
+
# Mixin for coordinate classes
|
|
248
|
+
# ---------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
module CoordinateMethods
|
|
251
|
+
def to_wkb(srid: nil)
|
|
252
|
+
if is_a?(Coordinate::ENU) || is_a?(Coordinate::NED)
|
|
253
|
+
raise ArgumentError,
|
|
254
|
+
"#{self.class.name.split('::').last} is a relative coordinate system " \
|
|
255
|
+
"and cannot be exported to WKB without a reference point. " \
|
|
256
|
+
"Convert to an absolute system (e.g., LLA) first."
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
lla = is_a?(Coordinate::LLA) ? self : to_lla
|
|
260
|
+
WKB.encode_point(lla, srid: srid)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def to_wkb_hex(srid: nil)
|
|
264
|
+
to_wkb(srid: srid).unpack1("H*")
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# ---------------------------------------------------------------
|
|
271
|
+
# Add to_wkb / to_wkb_hex to non-coordinate geometry types
|
|
272
|
+
# ---------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
module Geodetic
|
|
275
|
+
class Segment
|
|
276
|
+
def to_wkb(srid: nil)
|
|
277
|
+
WKB.encode_line_string([@start_point, @end_point], srid: srid)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def to_wkb_hex(srid: nil)
|
|
281
|
+
to_wkb(srid: srid).unpack1("H*")
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
class Path
|
|
286
|
+
def to_wkb(as: :line_string, srid: nil)
|
|
287
|
+
raise ArgumentError, "path is empty" if empty?
|
|
288
|
+
|
|
289
|
+
if as == :polygon
|
|
290
|
+
raise ArgumentError, "need at least 3 coordinates for a polygon" if size < 3
|
|
291
|
+
ring = @coordinates.dup
|
|
292
|
+
ring << ring.first unless ring.first == ring.last
|
|
293
|
+
WKB.encode_polygon([ring], srid: srid)
|
|
294
|
+
else
|
|
295
|
+
raise ArgumentError, "need at least 2 coordinates for a line string" if size < 2
|
|
296
|
+
WKB.encode_line_string(@coordinates, srid: srid)
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def to_wkb_hex(as: :line_string, srid: nil)
|
|
301
|
+
to_wkb(as: as, srid: srid).unpack1("H*")
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
class Feature
|
|
306
|
+
def to_wkb(srid: nil)
|
|
307
|
+
@geometry.to_wkb(srid: srid)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def to_wkb_hex(srid: nil)
|
|
311
|
+
to_wkb(srid: srid).unpack1("H*")
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
module Areas
|
|
316
|
+
class Polygon
|
|
317
|
+
def to_wkb(srid: nil)
|
|
318
|
+
WKB.encode_polygon([@boundary], srid: srid)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def to_wkb_hex(srid: nil)
|
|
322
|
+
to_wkb(srid: srid).unpack1("H*")
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
class Circle
|
|
327
|
+
def to_wkb(segments: 32, srid: nil)
|
|
328
|
+
step = 360.0 / segments
|
|
329
|
+
ring = segments.times.map do |i|
|
|
330
|
+
Vector.new(distance: @radius, bearing: step * i).destination_from(@centroid)
|
|
331
|
+
end
|
|
332
|
+
ring << ring.first
|
|
333
|
+
WKB.encode_polygon([ring], srid: srid)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def to_wkb_hex(segments: 32, srid: nil)
|
|
337
|
+
to_wkb(segments: segments, srid: srid).unpack1("H*")
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
class BoundingBox
|
|
342
|
+
def to_wkb(srid: nil)
|
|
343
|
+
ring = [nw, ne, @se, sw, nw]
|
|
344
|
+
WKB.encode_polygon([ring], srid: srid)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def to_wkb_hex(srid: nil)
|
|
348
|
+
to_wkb(srid: srid).unpack1("H*")
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# ---------------------------------------------------------------
|
|
355
|
+
# Apply coordinate mixin to all registered coordinate classes
|
|
356
|
+
# ---------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
Geodetic::Coordinate.systems.each do |klass|
|
|
359
|
+
klass.include(Geodetic::WKB::CoordinateMethods)
|
|
360
|
+
end
|