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/wkt.rb
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Geodetic
|
|
4
|
+
module WKT
|
|
5
|
+
# --- Export helpers ---
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def position(lla, precision:, use_z:)
|
|
9
|
+
if use_z
|
|
10
|
+
"#{lla.lng.round(precision)} #{lla.lat.round(precision)} #{lla.alt.round(precision)}"
|
|
11
|
+
else
|
|
12
|
+
"#{lla.lng.round(precision)} #{lla.lat.round(precision)}"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def has_altitude?(points)
|
|
17
|
+
points.any? { |p| p.alt != 0.0 }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def srid_prefix(srid)
|
|
21
|
+
srid ? "SRID=#{srid};" : ""
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def point_wkt(lla, precision: 6, srid: nil)
|
|
25
|
+
z = lla.alt != 0.0
|
|
26
|
+
type = z ? "POINT Z" : "POINT"
|
|
27
|
+
"#{srid_prefix(srid)}#{type}(#{position(lla, precision: precision, use_z: z)})"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def line_string_wkt(points, precision: 6, srid: nil)
|
|
31
|
+
z = has_altitude?(points)
|
|
32
|
+
type = z ? "LINESTRING Z" : "LINESTRING"
|
|
33
|
+
coords = points.map { |p| position(p, precision: precision, use_z: z) }.join(", ")
|
|
34
|
+
"#{srid_prefix(srid)}#{type}(#{coords})"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def polygon_wkt(rings, precision: 6, srid: nil)
|
|
38
|
+
z = rings.any? { |ring| has_altitude?(ring) }
|
|
39
|
+
type = z ? "POLYGON Z" : "POLYGON"
|
|
40
|
+
ring_strs = rings.map do |ring|
|
|
41
|
+
coords = ring.map { |p| position(p, precision: precision, use_z: z) }.join(", ")
|
|
42
|
+
"(#{coords})"
|
|
43
|
+
end
|
|
44
|
+
"#{srid_prefix(srid)}#{type}(#{ring_strs.join(', ')})"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# --- File I/O ---
|
|
48
|
+
|
|
49
|
+
def save!(path, *objects, srid: nil, precision: 6)
|
|
50
|
+
list = objects.length == 1 && objects[0].is_a?(Array) ? objects[0] : objects
|
|
51
|
+
File.open(path, "w") do |f|
|
|
52
|
+
list.each { |obj| f.puts obj.to_wkt(precision: precision, srid: srid) }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def load(path)
|
|
57
|
+
File.readlines(path, chomp: true)
|
|
58
|
+
.reject { |line| line.strip.empty? }
|
|
59
|
+
.map { |line| parse(line) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# --- Import ---
|
|
63
|
+
|
|
64
|
+
def parse(wkt_string)
|
|
65
|
+
str = wkt_string.strip
|
|
66
|
+
# Strip SRID prefix if present
|
|
67
|
+
str = str.sub(/\ASRID=\d+;/i, "")
|
|
68
|
+
parse_geometry(str)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def parse_with_srid(wkt_string)
|
|
72
|
+
str = wkt_string.strip
|
|
73
|
+
srid = nil
|
|
74
|
+
if str =~ /\ASRID=(\d+);/i
|
|
75
|
+
srid = $1.to_i
|
|
76
|
+
str = $'
|
|
77
|
+
end
|
|
78
|
+
[parse_geometry(str), srid]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def parse_geometry(str)
|
|
84
|
+
str = str.strip
|
|
85
|
+
case str
|
|
86
|
+
when /\APOINT\s*Z\s*\((.+)\)\z/im
|
|
87
|
+
parse_point($1, has_z: true)
|
|
88
|
+
when /\APOINT\s*\((.+)\)\z/im
|
|
89
|
+
parse_point($1, has_z: false)
|
|
90
|
+
when /\ALINESTRING\s*Z\s*\((.+)\)\z/im
|
|
91
|
+
parse_line_string($1, has_z: true)
|
|
92
|
+
when /\ALINESTRING\s*\((.+)\)\z/im
|
|
93
|
+
parse_line_string($1, has_z: false)
|
|
94
|
+
when /\APOLYGON\s*Z\s*\((.+)\)\z/im
|
|
95
|
+
parse_polygon($1, has_z: true)
|
|
96
|
+
when /\APOLYGON\s*\((.+)\)\z/im
|
|
97
|
+
parse_polygon($1, has_z: false)
|
|
98
|
+
when /\AMULTIPOINT\s*(Z?)\s*\((.+)\)\z/im
|
|
99
|
+
parse_multi_point($2, has_z: !$1.empty?)
|
|
100
|
+
when /\AMULTILINESTRING\s*(Z?)\s*\((.+)\)\z/im
|
|
101
|
+
parse_multi_line_string($2, has_z: !$1.empty?)
|
|
102
|
+
when /\AMULTIPOLYGON\s*(Z?)\s*\((.+)\)\z/im
|
|
103
|
+
parse_multi_polygon($2, has_z: !$1.empty?)
|
|
104
|
+
when /\AGEOMETRYCOLLECTION\s*Z?\s*\((.+)\)\z/im
|
|
105
|
+
parse_geometry_collection($1)
|
|
106
|
+
|
|
107
|
+
else
|
|
108
|
+
raise ArgumentError, "unknown WKT type: #{str[0..30]}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def parse_point(coords_str, has_z:)
|
|
113
|
+
parts = coords_str.strip.split(/\s+/)
|
|
114
|
+
lng = parts[0].to_f
|
|
115
|
+
lat = parts[1].to_f
|
|
116
|
+
alt = has_z && parts[2] ? parts[2].to_f : 0.0
|
|
117
|
+
Coordinate::LLA.new(lat: lat, lng: lng, alt: alt)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def parse_line_string(coords_str, has_z:)
|
|
121
|
+
points = coords_str.split(",").map { |c| parse_point(c, has_z: has_z) }
|
|
122
|
+
if points.length == 2
|
|
123
|
+
Segment.new(points[0], points[1])
|
|
124
|
+
else
|
|
125
|
+
Path.new(coordinates: points)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def parse_polygon(rings_str, has_z:)
|
|
130
|
+
rings = split_rings(rings_str)
|
|
131
|
+
outer = rings[0].split(",").map { |c| parse_point(c, has_z: has_z) }
|
|
132
|
+
# Remove closing point if it duplicates the first
|
|
133
|
+
outer.pop if outer.length > 1 && outer.first == outer.last
|
|
134
|
+
Areas::Polygon.new(boundary: outer)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def parse_multi_point(coords_str, has_z:)
|
|
138
|
+
# MULTIPOINT can be ((x y), (x y)) or (x y, x y)
|
|
139
|
+
if coords_str.include?("(")
|
|
140
|
+
coords_str.scan(/\(([^)]+)\)/).map { |m| parse_point(m[0], has_z: has_z) }
|
|
141
|
+
else
|
|
142
|
+
coords_str.split(",").map { |c| parse_point(c, has_z: has_z) }
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def parse_multi_line_string(coords_str, has_z:)
|
|
147
|
+
split_rings(coords_str).map { |ring| parse_line_string(ring, has_z: has_z) }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def parse_multi_polygon(coords_str, has_z:)
|
|
151
|
+
polygons = split_top_level(coords_str)
|
|
152
|
+
polygons.map { |poly_str| parse_polygon(poly_str, has_z: has_z) }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def parse_geometry_collection(inner)
|
|
156
|
+
geometries = split_geometries(inner)
|
|
157
|
+
geometries.map { |g| parse_geometry(g) }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Split "(a, b), (c, d)" into ["a, b", "c, d"]
|
|
161
|
+
def split_rings(str)
|
|
162
|
+
str.scan(/\(([^()]+)\)/).map(&:first)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Split top-level parenthesized groups: "((a),(b)), ((c),(d))" → ["(a),(b)", "(c),(d)"]
|
|
166
|
+
def split_top_level(str)
|
|
167
|
+
results = []
|
|
168
|
+
depth = 0
|
|
169
|
+
current = +""
|
|
170
|
+
str.each_char do |ch|
|
|
171
|
+
case ch
|
|
172
|
+
when "("
|
|
173
|
+
depth += 1
|
|
174
|
+
if depth == 1
|
|
175
|
+
current = +""
|
|
176
|
+
else
|
|
177
|
+
current << ch
|
|
178
|
+
end
|
|
179
|
+
when ")"
|
|
180
|
+
depth -= 1
|
|
181
|
+
if depth == 0
|
|
182
|
+
results << current
|
|
183
|
+
else
|
|
184
|
+
current << ch
|
|
185
|
+
end
|
|
186
|
+
when ","
|
|
187
|
+
current << ch if depth > 0
|
|
188
|
+
when " "
|
|
189
|
+
current << ch if depth > 0
|
|
190
|
+
else
|
|
191
|
+
current << ch
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
results
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Split geometry collection: "POINT(1 2), LINESTRING(1 2, 3 4)"
|
|
198
|
+
# Must handle nested parens in each geometry
|
|
199
|
+
def split_geometries(str)
|
|
200
|
+
results = []
|
|
201
|
+
depth = 0
|
|
202
|
+
current = +""
|
|
203
|
+
str.each_char do |ch|
|
|
204
|
+
case ch
|
|
205
|
+
when "("
|
|
206
|
+
depth += 1
|
|
207
|
+
current << ch
|
|
208
|
+
when ")"
|
|
209
|
+
depth -= 1
|
|
210
|
+
current << ch
|
|
211
|
+
when ","
|
|
212
|
+
if depth == 0
|
|
213
|
+
results << current.strip
|
|
214
|
+
current = +""
|
|
215
|
+
else
|
|
216
|
+
current << ch
|
|
217
|
+
end
|
|
218
|
+
else
|
|
219
|
+
current << ch
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
results << current.strip unless current.strip.empty?
|
|
223
|
+
results
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------------------
|
|
228
|
+
# Mixin for coordinate classes
|
|
229
|
+
# ---------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
module CoordinateMethods
|
|
232
|
+
def to_wkt(precision: 6, srid: nil)
|
|
233
|
+
if is_a?(Coordinate::ENU) || is_a?(Coordinate::NED)
|
|
234
|
+
raise ArgumentError,
|
|
235
|
+
"#{self.class.name.split('::').last} is a relative coordinate system " \
|
|
236
|
+
"and cannot be exported to WKT without a reference point. " \
|
|
237
|
+
"Convert to an absolute system (e.g., LLA) first."
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
lla = is_a?(Coordinate::LLA) ? self : to_lla
|
|
241
|
+
WKT.point_wkt(lla, precision: precision, srid: srid)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# ---------------------------------------------------------------
|
|
248
|
+
# Add to_wkt to non-coordinate geometry types
|
|
249
|
+
# ---------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
module Geodetic
|
|
252
|
+
class Segment
|
|
253
|
+
def to_wkt(precision: 6, srid: nil)
|
|
254
|
+
WKT.line_string_wkt([@start_point, @end_point], precision: precision, srid: srid)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
class Path
|
|
259
|
+
def to_wkt(as: :line_string, precision: 6, srid: nil)
|
|
260
|
+
raise ArgumentError, "path is empty" if empty?
|
|
261
|
+
|
|
262
|
+
if as == :polygon
|
|
263
|
+
raise ArgumentError, "need at least 3 coordinates for a polygon" if size < 3
|
|
264
|
+
ring = @coordinates.dup
|
|
265
|
+
ring << ring.first unless ring.first == ring.last
|
|
266
|
+
WKT.polygon_wkt([ring], precision: precision, srid: srid)
|
|
267
|
+
else
|
|
268
|
+
raise ArgumentError, "need at least 2 coordinates for a line string" if size < 2
|
|
269
|
+
WKT.line_string_wkt(@coordinates, precision: precision, srid: srid)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
class Feature
|
|
275
|
+
def to_wkt(precision: 6, srid: nil)
|
|
276
|
+
@geometry.to_wkt(precision: precision, srid: srid)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
module Areas
|
|
281
|
+
class Polygon
|
|
282
|
+
def to_wkt(precision: 6, srid: nil)
|
|
283
|
+
WKT.polygon_wkt([@boundary], precision: precision, srid: srid)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
class Circle
|
|
288
|
+
def to_wkt(segments: 32, precision: 6, srid: nil)
|
|
289
|
+
step = 360.0 / segments
|
|
290
|
+
ring = segments.times.map do |i|
|
|
291
|
+
Vector.new(distance: @radius, bearing: step * i).destination_from(@centroid)
|
|
292
|
+
end
|
|
293
|
+
ring << ring.first
|
|
294
|
+
WKT.polygon_wkt([ring], precision: precision, srid: srid)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
class BoundingBox
|
|
299
|
+
def to_wkt(precision: 6, srid: nil)
|
|
300
|
+
ring = [nw, ne, @se, sw, nw]
|
|
301
|
+
WKT.polygon_wkt([ring], precision: precision, srid: srid)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# ---------------------------------------------------------------
|
|
308
|
+
# Apply coordinate mixin to all registered coordinate classes
|
|
309
|
+
# ---------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
Geodetic::Coordinate.systems.each do |klass|
|
|
312
|
+
klass.include(Geodetic::WKT::CoordinateMethods)
|
|
313
|
+
end
|
data/lib/geodetic.rb
CHANGED
data/mkdocs.yml
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: geodetic
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -57,11 +57,14 @@ files:
|
|
|
57
57
|
- docs/reference/feature.md
|
|
58
58
|
- docs/reference/geoid-height.md
|
|
59
59
|
- docs/reference/geojson.md
|
|
60
|
+
- docs/reference/geos-acceleration.md
|
|
60
61
|
- docs/reference/map-rendering.md
|
|
61
62
|
- docs/reference/path.md
|
|
62
63
|
- docs/reference/segment.md
|
|
63
64
|
- docs/reference/serialization.md
|
|
64
65
|
- docs/reference/vector.md
|
|
66
|
+
- docs/reference/wkb.md
|
|
67
|
+
- docs/reference/wkt.md
|
|
65
68
|
- examples/01_basic_conversions.rb
|
|
66
69
|
- examples/02_all_coordinate_systems.rb
|
|
67
70
|
- examples/03_distance_calculations.rb
|
|
@@ -78,8 +81,17 @@ files:
|
|
|
78
81
|
- examples/07_segments_and_shapes.rb
|
|
79
82
|
- examples/08_geodetic_arithmetic.rb
|
|
80
83
|
- examples/09_geojson_export.rb
|
|
84
|
+
- examples/10_wkt_serialization.rb
|
|
85
|
+
- examples/11_wkb_serialization.rb
|
|
86
|
+
- examples/12_geos_benchmark.rb
|
|
87
|
+
- examples/13_geos_operations.rb
|
|
81
88
|
- examples/README.md
|
|
82
89
|
- examples/geodetic_demo.geojson
|
|
90
|
+
- examples/geodetic_demo.wkb
|
|
91
|
+
- examples/geodetic_demo.wkt
|
|
92
|
+
- examples/geodetic_demo_output.wkb.hex
|
|
93
|
+
- examples/sample_geometries.wkb
|
|
94
|
+
- examples/sample_geometries.wkb.hex
|
|
83
95
|
- fiddle_pointer_buffer_pool.md
|
|
84
96
|
- lib/geodetic.rb
|
|
85
97
|
- lib/geodetic/areas.rb
|
|
@@ -118,10 +130,13 @@ files:
|
|
|
118
130
|
- lib/geodetic/feature.rb
|
|
119
131
|
- lib/geodetic/geoid_height.rb
|
|
120
132
|
- lib/geodetic/geojson.rb
|
|
133
|
+
- lib/geodetic/geos.rb
|
|
121
134
|
- lib/geodetic/path.rb
|
|
122
135
|
- lib/geodetic/segment.rb
|
|
123
136
|
- lib/geodetic/vector.rb
|
|
124
137
|
- lib/geodetic/version.rb
|
|
138
|
+
- lib/geodetic/wkb.rb
|
|
139
|
+
- lib/geodetic/wkt.rb
|
|
125
140
|
- mkdocs.yml
|
|
126
141
|
- sig/geodetic.rbs
|
|
127
142
|
- spatial_hash_idea.md
|