geodetic 0.3.2 → 0.5.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 +84 -2
- data/README.md +121 -8
- data/docs/coordinate-systems/gars.md +2 -2
- data/docs/coordinate-systems/georef.md +2 -2
- data/docs/coordinate-systems/gh.md +2 -2
- data/docs/coordinate-systems/gh36.md +2 -2
- data/docs/coordinate-systems/h3.md +2 -2
- data/docs/coordinate-systems/ham.md +2 -2
- data/docs/coordinate-systems/olc.md +2 -2
- data/docs/index.md +4 -2
- data/docs/reference/areas.md +140 -14
- data/docs/reference/arithmetic.md +368 -0
- data/docs/reference/feature.md +2 -2
- data/docs/reference/path.md +3 -3
- data/docs/reference/segment.md +181 -0
- data/docs/reference/vector.md +256 -0
- data/examples/02_all_coordinate_systems.rb +6 -6
- data/examples/06_path_operations.rb +2 -4
- data/examples/07_segments_and_shapes.rb +258 -0
- data/examples/08_geodetic_arithmetic.rb +393 -0
- data/examples/README.md +35 -1
- data/lib/geodetic/areas/bounding_box.rb +56 -0
- data/lib/geodetic/areas/circle.rb +8 -0
- data/lib/geodetic/areas/hexagon.rb +11 -0
- data/lib/geodetic/areas/octagon.rb +11 -0
- data/lib/geodetic/areas/pentagon.rb +11 -0
- data/lib/geodetic/areas/polygon.rb +64 -14
- data/lib/geodetic/areas/rectangle.rb +85 -35
- data/lib/geodetic/areas/regular_polygon.rb +59 -0
- data/lib/geodetic/areas/triangle.rb +180 -0
- data/lib/geodetic/areas.rb +6 -0
- data/lib/geodetic/coordinate/gh36.rb +1 -1
- data/lib/geodetic/coordinate/h3.rb +1 -1
- data/lib/geodetic/coordinate/spatial_hash.rb +2 -2
- data/lib/geodetic/coordinate.rb +26 -1
- data/lib/geodetic/distance.rb +5 -1
- data/lib/geodetic/path.rb +85 -153
- data/lib/geodetic/segment.rb +193 -0
- data/lib/geodetic/vector.rb +242 -0
- data/lib/geodetic/version.rb +1 -1
- data/lib/geodetic.rb +2 -0
- data/mkdocs.yml +1 -0
- metadata +14 -1
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# Geodetic Arithmetic Reference
|
|
2
|
+
|
|
3
|
+
Geodetic provides operator overloading that lets you compose geometric objects naturally. The `+` operator builds geometry from parts, while `*` (and its alias `translate`) applies a vector displacement to shift objects.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Design Principles
|
|
8
|
+
|
|
9
|
+
1. **Type determines result** — the types of the operands, not their values, determine the return type. `Coordinate + Coordinate` always returns a Segment, never conditionally a different type.
|
|
10
|
+
|
|
11
|
+
2. **Left operand is the anchor** — in asymmetric operations, the left operand provides the reference point. `P1 + V` creates a segment starting at P1; `V + P1` creates a segment ending at P1.
|
|
12
|
+
|
|
13
|
+
3. **Consistent translation** — `*` always means "translate by this vector," returning the same type as the receiver. A translated Segment is still a Segment; a translated Path is still a Path.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## The + Operator: Building Geometry
|
|
18
|
+
|
|
19
|
+
The `+` operator composes smaller geometric objects into larger ones. The result type depends on the combination of operand types.
|
|
20
|
+
|
|
21
|
+
### Coordinate + Coordinate → Segment
|
|
22
|
+
|
|
23
|
+
Two points define a directed line segment.
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
seattle = Geodetic::Coordinate::LLA.new(lat: 47.62, lng: -122.35, alt: 0)
|
|
27
|
+
portland = Geodetic::Coordinate::LLA.new(lat: 45.52, lng: -122.68, alt: 0)
|
|
28
|
+
|
|
29
|
+
seg = seattle + portland # => Geodetic::Segment
|
|
30
|
+
seg.start_point # => seattle
|
|
31
|
+
seg.end_point # => portland
|
|
32
|
+
seg.length # => Distance (~235 km)
|
|
33
|
+
seg.bearing # => Bearing (~188°)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Works across any coordinate system — the Segment converts both points to LLA internally:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
utm = portland.to_utm
|
|
40
|
+
seg = seattle + utm # => Segment (seattle → portland via LLA)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Order matters: `seattle + portland` is a different segment than `portland + seattle`.
|
|
44
|
+
|
|
45
|
+
### Coordinate + Coordinate + Coordinate → Path
|
|
46
|
+
|
|
47
|
+
Chaining builds a path. The first `+` produces a Segment; the second `+` extends it into a Path.
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
path = seattle + portland + sf # => Geodetic::Path (3 points)
|
|
51
|
+
path.size # => 3
|
|
52
|
+
path.first # => seattle
|
|
53
|
+
path.last # => sf
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Further chaining continues to extend the Path:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
path = seattle + portland + sf + la + nyc # => Path (5 points)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Coordinate + Segment → Path
|
|
63
|
+
|
|
64
|
+
A point plus a segment produces a three-point path: the point, then the segment's endpoints.
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
seg = portland + sf
|
|
68
|
+
path = seattle + seg # => Path: seattle → portland → sf
|
|
69
|
+
path.size # => 3
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Segment + Coordinate → Path
|
|
73
|
+
|
|
74
|
+
Extending a segment with a point:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
seg = seattle + portland
|
|
78
|
+
path = seg + sf # => Path: seattle → portland → sf
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Segment + Segment → Path
|
|
82
|
+
|
|
83
|
+
Two segments concatenate into a four-point path:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
seg1 = seattle + portland
|
|
87
|
+
seg2 = sf + la
|
|
88
|
+
path = seg1 + seg2 # => Path: seattle → portland → sf → la
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Coordinate + Distance → Circle
|
|
92
|
+
|
|
93
|
+
A point plus a distance defines a circle.
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
radius = Geodetic::Distance.km(5)
|
|
97
|
+
circle = seattle + radius
|
|
98
|
+
# => Areas::Circle centered at seattle, 5000m radius
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Distance + Coordinate → Circle
|
|
102
|
+
|
|
103
|
+
Commutative — same result:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
circle = Geodetic::Distance.km(5) + seattle
|
|
107
|
+
# => Areas::Circle centered at seattle, 5000m radius
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Coordinate + Vector → Segment
|
|
111
|
+
|
|
112
|
+
A point plus a vector solves the Vincenty direct problem, producing a segment from the origin to the destination.
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
v = Geodetic::Vector.new(distance: 100_000, bearing: 45.0)
|
|
116
|
+
seg = seattle + v
|
|
117
|
+
# => Segment from seattle to a point 100km northeast
|
|
118
|
+
seg.length_meters # => ~100000.0
|
|
119
|
+
seg.bearing # => ~45°
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
This is different from translation (`*`): `+` gives you the journey (a Segment), while `*` gives you just the destination (a Coordinate).
|
|
123
|
+
|
|
124
|
+
### Vector + Coordinate → Segment
|
|
125
|
+
|
|
126
|
+
The vector reversed determines the start point; the coordinate is the endpoint.
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
v = Geodetic::Vector.new(distance: 10_000, bearing: 90.0)
|
|
130
|
+
seg = v + seattle
|
|
131
|
+
# => Segment from (10km west of seattle) to seattle
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Segment + Vector → Path
|
|
135
|
+
|
|
136
|
+
Extends the segment from its endpoint in the vector's direction.
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
seg = seattle + portland
|
|
140
|
+
v = Geodetic::Vector.new(distance: 50_000, bearing: 180.0)
|
|
141
|
+
path = seg + v
|
|
142
|
+
# => Path: seattle → portland → (50km south of portland)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Vector + Segment → Path
|
|
146
|
+
|
|
147
|
+
Prepends a new start point. The vector is reversed from the segment's start to find it.
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
v = Geodetic::Vector.new(distance: 50_000, bearing: 90.0)
|
|
151
|
+
seg = seattle + portland
|
|
152
|
+
path = v + seg
|
|
153
|
+
# => Path: (50km west of seattle) → seattle → portland
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Path + Vector → Path
|
|
157
|
+
|
|
158
|
+
Extends the path from its last point in the vector's direction.
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
path = seattle + portland + sf
|
|
162
|
+
v = Geodetic::Vector.new(distance: 100_000, bearing: 180.0)
|
|
163
|
+
path2 = path + v
|
|
164
|
+
# => Path: seattle → portland → sf → (100km south of sf)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Path + Coordinate → Path
|
|
168
|
+
|
|
169
|
+
Appends a waypoint (already existed before arithmetic was added):
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
path = seattle + portland
|
|
173
|
+
path2 = path + sf # => Path: seattle → portland → sf
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Path + Path → Path
|
|
177
|
+
|
|
178
|
+
Concatenates two paths (already existed):
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
west_coast = seattle + portland + sf
|
|
182
|
+
east_coast = nyc + dc
|
|
183
|
+
cross_country = west_coast + east_coast
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Complete + Operator Table
|
|
189
|
+
|
|
190
|
+
| Left | + Right | Result | Description |
|
|
191
|
+
|------|---------|--------|-------------|
|
|
192
|
+
| Coordinate | Coordinate | Segment | Two-point directed segment |
|
|
193
|
+
| Coordinate | Vector | Segment | Origin to Vincenty destination |
|
|
194
|
+
| Coordinate | Distance | Circle | Point + radius |
|
|
195
|
+
| Coordinate | Segment | Path | Point then segment endpoints |
|
|
196
|
+
| Segment | Coordinate | Path | Extend with waypoint |
|
|
197
|
+
| Segment | Segment | Path | Concatenate segments |
|
|
198
|
+
| Segment | Vector | Path | Extend from endpoint |
|
|
199
|
+
| Vector | Coordinate | Segment | Reverse start to coordinate |
|
|
200
|
+
| Vector | Segment | Path | Prepend via reverse |
|
|
201
|
+
| Vector | Vector | Vector | Component-wise addition |
|
|
202
|
+
| Distance | Coordinate | Circle | Radius + center |
|
|
203
|
+
| Path | Coordinate | Path | Append waypoint |
|
|
204
|
+
| Path | Path | Path | Concatenate |
|
|
205
|
+
| Path | Segment | Path | Append segment points |
|
|
206
|
+
| Path | Vector | Path | Extend from last point |
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## The * Operator: Translation
|
|
211
|
+
|
|
212
|
+
The `*` operator translates (shifts) a geometric object by a vector displacement. Every point in the object is moved by the same vector. The result is always the same type as the receiver.
|
|
213
|
+
|
|
214
|
+
The named method `translate` is an alias for `*`.
|
|
215
|
+
|
|
216
|
+
### Coordinate * Vector → Coordinate
|
|
217
|
+
|
|
218
|
+
Returns the destination point — the pure result of moving the point.
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
v = Geodetic::Vector.new(distance: 10_000, bearing: 0.0)
|
|
222
|
+
p2 = seattle * v # => LLA (10km north of seattle)
|
|
223
|
+
p2 = seattle.translate(v) # => same
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Compare with `+`: `seattle + v` returns a **Segment** (the journey); `seattle * v` returns a **Coordinate** (just the destination).
|
|
227
|
+
|
|
228
|
+
### Segment * Vector → Segment
|
|
229
|
+
|
|
230
|
+
Both endpoints are translated by the same vector. Length and bearing are preserved.
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
seg = Geodetic::Segment.new(seattle, portland)
|
|
234
|
+
v = Geodetic::Vector.new(distance: 100_000, bearing: 90.0)
|
|
235
|
+
|
|
236
|
+
shifted = seg * v
|
|
237
|
+
shifted = seg.translate(v)
|
|
238
|
+
|
|
239
|
+
shifted.length_meters # => same as original
|
|
240
|
+
shifted.start_point # => 100km east of seattle
|
|
241
|
+
shifted.end_point # => 100km east of portland
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Path * Vector → Path
|
|
245
|
+
|
|
246
|
+
All waypoints are translated. The shape and distances between points are preserved.
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
route = seattle + portland + sf
|
|
250
|
+
v = Geodetic::Vector.new(distance: 50_000, bearing: 0.0)
|
|
251
|
+
|
|
252
|
+
shifted = route * v # => Path shifted 50km north
|
|
253
|
+
shifted = route.translate(v) # => same
|
|
254
|
+
shifted.size # => 3
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Circle * Vector → Circle
|
|
258
|
+
|
|
259
|
+
The centroid is translated. The radius is preserved.
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
circle = Geodetic::Areas::Circle.new(centroid: seattle, radius: 5000)
|
|
263
|
+
v = Geodetic::Vector.new(distance: 10_000, bearing: 180.0)
|
|
264
|
+
|
|
265
|
+
shifted = circle * v # => Circle 10km south, same 5km radius
|
|
266
|
+
shifted = circle.translate(v) # => same
|
|
267
|
+
shifted.radius # => 5000.0
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Polygon * Vector → Polygon
|
|
271
|
+
|
|
272
|
+
All boundary vertices are translated. The shape is preserved.
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
a = LLA.new(lat: 40.0, lng: -74.0, alt: 0)
|
|
276
|
+
b = LLA.new(lat: 40.0, lng: -73.0, alt: 0)
|
|
277
|
+
c = LLA.new(lat: 41.0, lng: -73.5, alt: 0)
|
|
278
|
+
poly = Geodetic::Areas::Polygon.new(boundary: [a, b, c])
|
|
279
|
+
|
|
280
|
+
v = Geodetic::Vector.new(distance: 100_000, bearing: 0.0)
|
|
281
|
+
shifted = poly * v # => Polygon shifted 100km north
|
|
282
|
+
shifted = poly.translate(v) # => same
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Complete * Operator Table
|
|
288
|
+
|
|
289
|
+
| Object | * Vector | Result | Effect |
|
|
290
|
+
|--------|----------|--------|--------|
|
|
291
|
+
| Coordinate | Vector | Coordinate | Translate point |
|
|
292
|
+
| Segment | Vector | Segment | Translate both endpoints |
|
|
293
|
+
| Path | Vector | Path | Translate all waypoints |
|
|
294
|
+
| Circle | Vector | Circle | Translate centroid, preserve radius |
|
|
295
|
+
| Polygon | Vector | Polygon | Translate all vertices |
|
|
296
|
+
|
|
297
|
+
The `*` operator only accepts a `Vector` on the right side. Any other type raises `ArgumentError`.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Corridors
|
|
302
|
+
|
|
303
|
+
`Path#to_corridor(width:)` converts a path into a polygon by offsetting each waypoint perpendicular to the path bearing on both sides.
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
route = seattle + portland + sf
|
|
307
|
+
corridor = route.to_corridor(width: 1000) # 1km wide
|
|
308
|
+
corridor = route.to_corridor(width: Distance.km(1)) # also accepts Distance
|
|
309
|
+
# => Areas::Polygon with 2*N boundary vertices
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
At interior waypoints, the perpendicular direction uses the mean bearing of the two adjacent segments to avoid self-intersection at bends.
|
|
313
|
+
|
|
314
|
+
Requires at least 2 coordinates. The `width:` parameter accepts meters (Numeric) or a `Distance` object.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Combining + and *
|
|
319
|
+
|
|
320
|
+
The operators compose naturally:
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
# Build a route, then shift it
|
|
324
|
+
v = Geodetic::Vector.new(distance: 50_000, bearing: 90.0)
|
|
325
|
+
route = (seattle + portland + sf) * v # shifted 50km east
|
|
326
|
+
|
|
327
|
+
# Build a circle, then translate it
|
|
328
|
+
circle = (seattle + Distance.km(5)) * v
|
|
329
|
+
|
|
330
|
+
# Chain: point + vector gives segment, then extend
|
|
331
|
+
seg = seattle + Geodetic::Vector.new(distance: 100_000, bearing: 45.0)
|
|
332
|
+
path = seg + portland # 3-point path
|
|
333
|
+
|
|
334
|
+
# Translate a corridor
|
|
335
|
+
corridor = route.to_corridor(width: 500)
|
|
336
|
+
shifted_corridor = corridor * v
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Key Distinctions
|
|
342
|
+
|
|
343
|
+
### + vs * with Vector
|
|
344
|
+
|
|
345
|
+
This is the most important distinction to understand:
|
|
346
|
+
|
|
347
|
+
| Expression | Result | Meaning |
|
|
348
|
+
|-----------|--------|---------|
|
|
349
|
+
| `P + V` | Segment | The **journey** — where you started and where you arrived |
|
|
350
|
+
| `P * V` | Coordinate | The **destination** — just where you end up |
|
|
351
|
+
|
|
352
|
+
`P + V` gives you a Segment because building geometry is the purpose of `+`. The segment records both the origin and the destination.
|
|
353
|
+
|
|
354
|
+
`P * V` gives you a Coordinate because translation is the purpose of `*`. You're moving the point, not creating a composite object.
|
|
355
|
+
|
|
356
|
+
### Commutativity
|
|
357
|
+
|
|
358
|
+
Most `+` operations are **not** commutative — order determines the structure:
|
|
359
|
+
|
|
360
|
+
- `P1 + P2` starts at P1; `P2 + P1` starts at P2
|
|
361
|
+
- `P + V` creates a segment starting at P; `V + P` creates a segment ending at P
|
|
362
|
+
|
|
363
|
+
The exceptions are:
|
|
364
|
+
|
|
365
|
+
- `P + Distance` and `Distance + P` both produce the same Circle
|
|
366
|
+
- `V1 + V2` and `V2 + V1` produce the same Vector (component addition is commutative)
|
|
367
|
+
|
|
368
|
+
Translation (`*`) is always `object * vector` — the vector must be on the right.
|
data/docs/reference/feature.md
CHANGED
|
@@ -14,7 +14,7 @@ Feature.new(
|
|
|
14
14
|
)
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
The `geometry` parameter accepts any coordinate class, any area class (`Circle`, `Polygon`, `
|
|
17
|
+
The `geometry` parameter accepts any coordinate class, any area class (`Circle`, `Polygon`, `BoundingBox`), or a `Path`. The `metadata` hash is optional and defaults to `{}`.
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -35,7 +35,7 @@ All three attributes have both reader and writer methods.
|
|
|
35
35
|
A Feature's geometry can be any of:
|
|
36
36
|
|
|
37
37
|
- **Coordinate** — any of the 18 coordinate classes (`LLA`, `ECEF`, `UTM`, etc.)
|
|
38
|
-
- **Area** — `Areas::Circle`, `Areas::Polygon`, or `Areas::
|
|
38
|
+
- **Area** — `Areas::Circle`, `Areas::Polygon`, or `Areas::BoundingBox`
|
|
39
39
|
- **Path** — a `Geodetic::Path` representing a route or trail
|
|
40
40
|
|
|
41
41
|
When the geometry is an area, `distance_to` and `bearing_to` use the area's `centroid` as the reference point. When the geometry is a Path, `distance_to` and `bearing_to` use geometric projection to find the closest approach point on the path.
|
data/docs/reference/path.md
CHANGED
|
@@ -40,7 +40,7 @@ Raises `ArgumentError` if any coordinate appears more than once.
|
|
|
40
40
|
| `prev(coordinate)` | Waypoint before the given one, or `nil` at start |
|
|
41
41
|
| `size` | Integer | Number of waypoints |
|
|
42
42
|
| `empty?` | Boolean | True if the path has no waypoints |
|
|
43
|
-
| `segments` | Array |
|
|
43
|
+
| `segments` | Array | Array of `Segment` objects for each consecutive pair |
|
|
44
44
|
|
|
45
45
|
---
|
|
46
46
|
|
|
@@ -94,7 +94,7 @@ The `other` parameter for `distance_to`, `bearing_to`, and `closest_coordinate_t
|
|
|
94
94
|
}
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
Accepts `Areas::Circle`, `Areas::Polygon`, `Areas::
|
|
97
|
+
Accepts `Areas::Circle`, `Areas::Polygon`, `Areas::BoundingBox`, or another `Path`.
|
|
98
98
|
|
|
99
99
|
---
|
|
100
100
|
|
|
@@ -149,7 +149,7 @@ Returns the last coordinate if the distance exceeds the total path length.
|
|
|
149
149
|
|
|
150
150
|
### `bounds`
|
|
151
151
|
|
|
152
|
-
Returns an `Areas::
|
|
152
|
+
Returns an `Areas::BoundingBox` representing the axis-aligned bounding box of all waypoints.
|
|
153
153
|
|
|
154
154
|
```ruby
|
|
155
155
|
bbox = route.bounds
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Segment Reference
|
|
2
|
+
|
|
3
|
+
`Geodetic::Segment` represents a directed line segment between two points on the Earth's surface. It is the fundamental geometric primitive underlying `Path` segments, `Polygon` edges, and closest-approach calculations.
|
|
4
|
+
|
|
5
|
+
A Segment has a `start_point` and an `end_point`, both stored as `Coordinate::LLA`. Properties like `length`, `bearing`, and `midpoint` are computed lazily and cached.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Great Circle Arcs
|
|
10
|
+
|
|
11
|
+
On a sphere, any two points that are not antipodal (diametrically opposite) define a great circle, and that great circle produces two arcs between them: the **minor arc** (the shorter path) and the **major arc** (the longer way around the globe).
|
|
12
|
+
|
|
13
|
+
Segment always uses the **minor arc**. All operations — `length`, `bearing`, `interpolate`, `project`, `contains?` — follow the shortest path between the two endpoints via Vincenty geodesic calculations.
|
|
14
|
+
|
|
15
|
+
For **antipodal points** (exactly opposite sides of the Earth, roughly 20,000 km apart), the great circle is degenerate: there are infinitely many paths of equal length and the bearing is undefined.
|
|
16
|
+
|
|
17
|
+
In practice, real-world segments rarely approach even a quarter of the Earth's circumference (~10,000 km), so the minor arc assumption holds for virtually all use cases.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Constructor
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
a = Geodetic::Coordinate::LLA.new(lat: 40.7484, lng: -73.9857, alt: 0)
|
|
25
|
+
b = Geodetic::Coordinate::LLA.new(lat: 40.7580, lng: -73.9855, alt: 0)
|
|
26
|
+
|
|
27
|
+
seg = Geodetic::Segment.new(a, b)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Accepts any coordinate type that responds to `to_lla`. Endpoints are converted to LLA on construction.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Attributes
|
|
35
|
+
|
|
36
|
+
| Attribute | Type | Description |
|
|
37
|
+
|---------------|------|-------------|
|
|
38
|
+
| `start_point` | LLA | The starting point of the segment |
|
|
39
|
+
| `end_point` | LLA | The ending point of the segment |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Properties
|
|
44
|
+
|
|
45
|
+
All properties are lazily computed and cached after first access.
|
|
46
|
+
|
|
47
|
+
| Method | Returns | Description |
|
|
48
|
+
|----------------|----------|-------------|
|
|
49
|
+
| `length` | Distance | Great-circle distance between endpoints |
|
|
50
|
+
| `distance` | Distance | Alias for `length` |
|
|
51
|
+
| `length_meters`| Float | Length in meters (convenience accessor) |
|
|
52
|
+
| `bearing` | Bearing | Forward azimuth from start to end |
|
|
53
|
+
| `midpoint` | LLA | Point at the halfway mark |
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
seg.length # => #<Geodetic::Distance 1067.45 m>
|
|
57
|
+
seg.distance # => #<Geodetic::Distance 1067.45 m> (alias)
|
|
58
|
+
seg.length_meters # => 1067.45
|
|
59
|
+
seg.bearing # => #<Geodetic::Bearing 1.0°>
|
|
60
|
+
seg.midpoint # => LLA at the midpoint
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Geometry
|
|
66
|
+
|
|
67
|
+
### `reverse`
|
|
68
|
+
|
|
69
|
+
Returns a new Segment with start and end points swapped.
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
rev = seg.reverse
|
|
73
|
+
rev.start_point == seg.end_point # => true
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `interpolate(fraction)`
|
|
77
|
+
|
|
78
|
+
Returns the LLA coordinate at a given fraction (0.0 to 1.0) along the segment.
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
seg.interpolate(0.0) # => start_point
|
|
82
|
+
seg.interpolate(0.5) # => midpoint
|
|
83
|
+
seg.interpolate(1.0) # => end_point
|
|
84
|
+
seg.interpolate(0.25) # => quarter-way along
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Projection
|
|
90
|
+
|
|
91
|
+
### `project(point)`
|
|
92
|
+
|
|
93
|
+
Projects a point onto the segment, returning the closest point on the segment and the distance in meters.
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
foot, distance_m = seg.project(target_point)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- If the perpendicular foot falls within the segment, returns the foot and the perpendicular distance.
|
|
100
|
+
- If the foot falls before the start, returns `start_point`.
|
|
101
|
+
- If the foot falls past the end, returns `end_point`.
|
|
102
|
+
- Handles zero-length segments and target-at-endpoint edge cases.
|
|
103
|
+
|
|
104
|
+
This is the core geometric operation used by `Path#closest_coordinate_to`, `Path#closest_points_to`, and `Path#at_distance`.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Membership
|
|
109
|
+
|
|
110
|
+
| Method | Description |
|
|
111
|
+
|--------|-------------|
|
|
112
|
+
| `includes?(point)` | True if the point is a vertex (start or end point) |
|
|
113
|
+
| `contains?(point, tolerance: 10.0)` | True if the point lies on the segment within tolerance (meters) |
|
|
114
|
+
| `excludes?(point, tolerance: 10.0)` | Opposite of `contains?` |
|
|
115
|
+
|
|
116
|
+
`includes?` checks vertices only. `contains?` checks whether a point lies anywhere along the line between the two endpoints using a bearing comparison with a tolerance derived from the segment length.
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
seg.includes?(seg.start_point) # => true
|
|
120
|
+
seg.includes?(seg.midpoint) # => false
|
|
121
|
+
|
|
122
|
+
seg.contains?(seg.midpoint) # => true
|
|
123
|
+
seg.contains?(far_away_point) # => false
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Intersection
|
|
129
|
+
|
|
130
|
+
### `intersects?(other_segment)`
|
|
131
|
+
|
|
132
|
+
Tests whether two segments cross each other using cross-product orientation tests on a flat lat/lng approximation. Handles both proper intersections and collinear overlap.
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
seg1 = Geodetic::Segment.new(a, b)
|
|
136
|
+
seg2 = Geodetic::Segment.new(c, d)
|
|
137
|
+
|
|
138
|
+
seg1.intersects?(seg2) # => true/false
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Used internally by `Path#intersects?` and `Path#to_polygon` for self-intersection validation.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Conversion
|
|
146
|
+
|
|
147
|
+
| Method | Returns | Description |
|
|
148
|
+
|----------|---------|-------------|
|
|
149
|
+
| `to_path`| Path | A two-point Path from start to end |
|
|
150
|
+
| `to_a` | Array | `[start_point, end_point]` |
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
seg.to_path # => #<Geodetic::Path size=2 ...>
|
|
154
|
+
seg.to_a # => [start_point, end_point]
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Equality and Display
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
seg1 == seg2 # true if same start and end points
|
|
163
|
+
|
|
164
|
+
seg.to_s # => "Segment(40.748400, ... -> 40.758000, ...)"
|
|
165
|
+
seg.inspect # => "#<Geodetic::Segment start=... end=... length=1067.45 m>"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Two segments are equal if they have the same start and end points. Direction matters: `Segment.new(a, b) != Segment.new(b, a)`.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Relationship to Path and Polygon
|
|
173
|
+
|
|
174
|
+
`Path#segments` returns an array of Segment objects:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
route = Path.new(coordinates: [a, b, c, d])
|
|
178
|
+
route.segments # => [Segment(a→b), Segment(b→c), Segment(c→d)]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Polygon edges are implicit segments formed by consecutive boundary points. Segment's `project`, `intersects?`, and `contains?` methods power the geometric operations in both Path and Polygon.
|