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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +84 -2
  3. data/README.md +121 -8
  4. data/docs/coordinate-systems/gars.md +2 -2
  5. data/docs/coordinate-systems/georef.md +2 -2
  6. data/docs/coordinate-systems/gh.md +2 -2
  7. data/docs/coordinate-systems/gh36.md +2 -2
  8. data/docs/coordinate-systems/h3.md +2 -2
  9. data/docs/coordinate-systems/ham.md +2 -2
  10. data/docs/coordinate-systems/olc.md +2 -2
  11. data/docs/index.md +4 -2
  12. data/docs/reference/areas.md +140 -14
  13. data/docs/reference/arithmetic.md +368 -0
  14. data/docs/reference/feature.md +2 -2
  15. data/docs/reference/path.md +3 -3
  16. data/docs/reference/segment.md +181 -0
  17. data/docs/reference/vector.md +256 -0
  18. data/examples/02_all_coordinate_systems.rb +6 -6
  19. data/examples/06_path_operations.rb +2 -4
  20. data/examples/07_segments_and_shapes.rb +258 -0
  21. data/examples/08_geodetic_arithmetic.rb +393 -0
  22. data/examples/README.md +35 -1
  23. data/lib/geodetic/areas/bounding_box.rb +56 -0
  24. data/lib/geodetic/areas/circle.rb +8 -0
  25. data/lib/geodetic/areas/hexagon.rb +11 -0
  26. data/lib/geodetic/areas/octagon.rb +11 -0
  27. data/lib/geodetic/areas/pentagon.rb +11 -0
  28. data/lib/geodetic/areas/polygon.rb +64 -14
  29. data/lib/geodetic/areas/rectangle.rb +85 -35
  30. data/lib/geodetic/areas/regular_polygon.rb +59 -0
  31. data/lib/geodetic/areas/triangle.rb +180 -0
  32. data/lib/geodetic/areas.rb +6 -0
  33. data/lib/geodetic/coordinate/gh36.rb +1 -1
  34. data/lib/geodetic/coordinate/h3.rb +1 -1
  35. data/lib/geodetic/coordinate/spatial_hash.rb +2 -2
  36. data/lib/geodetic/coordinate.rb +26 -1
  37. data/lib/geodetic/distance.rb +5 -1
  38. data/lib/geodetic/path.rb +85 -153
  39. data/lib/geodetic/segment.rb +193 -0
  40. data/lib/geodetic/vector.rb +242 -0
  41. data/lib/geodetic/version.rb +1 -1
  42. data/lib/geodetic.rb +2 -0
  43. data/mkdocs.yml +1 -0
  44. 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.
@@ -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`, `Rectangle`), or a `Path`. The `metadata` hash is optional and defaults to `{}`.
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::Rectangle`
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.
@@ -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 | Pairs of consecutive coordinates `[[a,b], [b,c], ...]` |
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::Rectangle`, or another `Path`.
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::Rectangle` representing the axis-aligned bounding box of all waypoints.
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.