geodetic 0.3.1 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13b2b50f8ff03c2093abf1a2454f3db38942f9c32918eb23aab30b6c45a4146b
4
- data.tar.gz: 2b3d5a7f325e2766196d2997e9c1781f0425abe016edeeb2fe4f155fc6a43f13
3
+ metadata.gz: f85da37953a9e974422502d62fbf82279fcf4f0ff71594ada2bb150e07cc75d1
4
+ data.tar.gz: a986282c09df583d9de453034f36e1a15c0d29cc016f586f09239f4f642d6fae
5
5
  SHA512:
6
- metadata.gz: 1d0378c9df0e04f9bb3de688303abae35857c4fbc594a26094a983417baa55004ef7f7f4cef7c8db7511b8fc769d0a368420e7601cb04e12eaf887b324e51e61
7
- data.tar.gz: c20dd6fa952579d8d035e1afd146d7337caa087efd71df4db17dcc3918b8cb4c561237f1cb5820167d9f84a64209da48a1dd38d0b46a5bccf0e3ce6959aa06d3
6
+ metadata.gz: 2d72a9b0e9e1a0688f25659dbff3d35e8b1c001c72468d2d2bd5c64ffe9fd2f16315dbbe4ec429c545115268e7486db81b26c4f6c9d625de3a8089b82cbcf8df
7
+ data.tar.gz: 031eb9604c27d9683c74bf3483b913a5ddb065ed78bd5375f23297f39b923f5a6212498c236982a310afe25fa5ca300df0dfaaa60d8632ad02950c75c2662e15
data/CHANGELOG.md CHANGED
@@ -11,6 +11,72 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
  ## [Unreleased]
12
12
 
13
13
 
14
+ ## [0.4.0] - 2026-03-10
15
+
16
+ ### Added
17
+
18
+ - **`Geodetic::Segment` class** — directed two-point line segment, the fundamental geometric primitive underlying Path and Polygon
19
+ - **Properties**: `length`/`distance` (returns Distance), `length_meters`, `bearing` (returns Bearing), `midpoint`/`centroid` (returns LLA) — all lazily computed and cached
20
+ - **Projection**: `project(point)` returns the closest point on the segment and perpendicular distance
21
+ - **Interpolation**: `interpolate(fraction)` returns the LLA at any fraction along the segment
22
+ - **Membership**: `includes?(point)` (vertex-only check), `contains?(point, tolerance:)` (on-segment check), `excludes?`
23
+ - **Intersection**: `intersects?(other_segment)` using cross-product orientation tests
24
+ - **Conversion**: `reverse`, `to_path`, `to_a`, `==`, `to_s`, `inspect`
25
+ - **`Geodetic::Areas::Triangle`** — polygon subclass with four construction modes
26
+ - Isosceles: `Triangle.new(center:, width:, height:, bearing:)`
27
+ - Equilateral by circumradius: `Triangle.new(center:, radius:, bearing:)`
28
+ - Equilateral by side length: `Triangle.new(center:, side:, bearing:)`
29
+ - Arbitrary vertices: `Triangle.new(vertices: [p1, p2, p3])`
30
+ - Predicates: `equilateral?`, `isosceles?`, `scalene?` based on actual side lengths (5m tolerance)
31
+ - Methods: `vertices`, `side_lengths`, `base`, `to_bounding_box`
32
+ - **`Geodetic::Areas::Rectangle`** — polygon subclass defined by a centerline Segment and perpendicular width
33
+ - `Rectangle.new(segment:, width:)` — accepts a Segment object or a two-element array of coordinates
34
+ - `width:` accepts numeric (meters) or a Distance instance
35
+ - Derived properties: `center`, `height`, `bearing` from the centerline; `corners`, `square?`, `to_bounding_box`
36
+ - **`Geodetic::Areas::Pentagon`**, **`Hexagon`**, **`Octagon`** — regular polygon subclasses from center + radius + bearing
37
+ - **Polygon self-intersection validation** — `Polygon.new` validates that no edge crosses another; pass `validate: false` to skip (used by subclasses with generated geometry)
38
+ - **Polygon `segments` method** — returns `Array<Segment>` for each edge; `edges` and `border` are aliases
39
+ - **Segments and shapes example** (`examples/07_segments_and_shapes.rb`) — 10-section demo covering Segment operations, Triangle/Rectangle construction and predicates, regular polygons, containment, bounding boxes, and Feature integration
40
+ - Documentation: `docs/reference/segment.md` (Segment reference with Great Circle Arcs section), updated `docs/reference/areas.md` with all polygon subclasses
41
+
42
+ ### Changed
43
+
44
+ - **Refactored `Path`** to use Segment objects — removed ~140 lines of private segment methods (`project_onto_segment`, `on_segment?`, `segments_intersect?`, `cross_sign`, `on_collinear?`, `to_flat`); all segment operations now delegate to Segment
45
+ - **Refactored `Polygon`** — `segments` is now the primary method (was `edges`); `edges` and `border` are aliases
46
+ - **Updated `Feature`** to support Segment as a geometry type via `centroid` (alias for `midpoint`)
47
+ - Updated README, `docs/index.md`, `docs/reference/areas.md`, `docs/reference/segment.md`, `examples/README.md`, and mkdocs nav
48
+
49
+ ### Removed
50
+
51
+ - `Areas::Rectangle = Areas::BoundingBox` alias — Rectangle is now its own class (Polygon subclass)
52
+
53
+ ## [0.3.2] - 2026-03-09
54
+
55
+ ### Added
56
+
57
+ - **`Geodetic::Path` class** — directed, ordered sequence of unique coordinates for modeling routes, trails, and boundaries
58
+ - **Navigation**: `first`, `last`, `next`, `prev`, `segments`, `size`, `empty?`
59
+ - **Membership**: `include?`/`includes?` (waypoint check), `contains?`/`inside?` (on-segment check with configurable tolerance)
60
+ - **Spatial**: `nearest_waypoint`, `closest_coordinate_to`, `distance_to`, `bearing_to` using geometric projection onto segments
61
+ - **Closest points**: `closest_points_to` for Path-to-Path, Path-to-Polygon, Path-to-BoundingBox, and Path-to-Circle
62
+ - **Computed**: `total_distance`, `segment_distances`, `segment_bearings`, `reverse`
63
+ - **Subpath/split**: `between(from, to)` extracts a subpath; `split_at(coord)` divides into two paths sharing the split point
64
+ - **Interpolation**: `at_distance(distance)` finds the coordinate at a given distance along the path
65
+ - **Bounding box**: `bounds` returns an `Areas::BoundingBox`
66
+ - **Polygon conversion**: `to_polygon` closes the path (validates no self-intersection)
67
+ - **Intersection**: `intersects?(other_path)` detects crossing segments
68
+ - **Equality**: `==` compares coordinates in order
69
+ - **Enumerable**: includes `Enumerable` via `each` — supports `map`, `select`, `any?`, `to_a`, etc.
70
+ - **Non-mutating operators**: `+` and `-` accept both coordinates and paths
71
+ - **Mutating operators**: `<<`, `>>`, `prepend`, `insert(after:/before:)`, `delete`/`remove` — all accept paths as well as coordinates
72
+ - **Path operations example** (`examples/06_path_operations.rb`) — 19-section demo covering all Path capabilities with a Manhattan walking route
73
+ - Documentation: `docs/reference/path.md` (Path reference)
74
+
75
+ ### Changed
76
+
77
+ - Updated `Geodetic::Feature` to support Path as a geometry type — delegates `distance_to` and `bearing_to` using geometric projection
78
+ - Updated README, `docs/index.md`, `docs/reference/feature.md`, `examples/README.md`, and mkdocs nav to include Path class
79
+
14
80
  ## [0.3.1] - 2026-03-09
15
81
 
16
82
  ### Added
data/README.md CHANGED
@@ -18,7 +18,9 @@
18
18
  - <strong>Distance Calculations</strong> - Vincenty great-circle and straight-line with unit tracking<br>
19
19
  - <strong>Bearing Calculations</strong> - Forward azimuth, back azimuth, compass directions, elevation angles<br>
20
20
  - <strong>Geoid Height Support</strong> - EGM96, EGM2008, GEOID18, GEOID12B models<br>
21
- - <strong>Geographic Areas</strong> - Circle, Polygon, and Rectangle with point-in-area tests<br>
21
+ - <strong>Geographic Areas</strong> - Circle, Polygon, BoundingBox, Triangle, Rectangle, Pentagon, Hexagon, Octagon<br>
22
+ - <strong>Segments</strong> - Directed two-point line segments with projection, intersection, and interpolation<br>
23
+ - <strong>Paths</strong> - Directed coordinate sequences with navigation, interpolation, closest approach, intersection, and area conversion<br>
22
24
  - <strong>Features</strong> - Named geometry wrapper with metadata and delegated distance/bearing<br>
23
25
  - <strong>Validated Setters</strong> - Type coercion and range validation on all coordinate attributes<br>
24
26
  - <strong>Serialization</strong> - to_s(precision), to_a, from_string, from_array, DMS format<br>
@@ -425,7 +427,7 @@ lla = gh36.to_lla
425
427
  gh36.neighbors # => { N: GH36, S: GH36, E: GH36, W: GH36, NE: ..., NW: ..., SE: ..., SW: ... }
426
428
 
427
429
  # Bounding rectangle of the geohash cell
428
- area = gh36.to_area # => Areas::Rectangle
430
+ area = gh36.to_area # => Areas::BoundingBox
429
431
  area.includes?(gh36.to_lla) # => true
430
432
 
431
433
  # Precision info
@@ -452,7 +454,7 @@ lla = gh.to_lla
452
454
  gh.neighbors # => { N: GH, S: GH, E: GH, W: GH, NE: ..., NW: ..., SE: ..., SW: ... }
453
455
 
454
456
  # Bounding rectangle of the geohash cell
455
- area = gh.to_area # => Areas::Rectangle
457
+ area = gh.to_area # => Areas::BoundingBox
456
458
  area.includes?(gh.to_lla) # => true
457
459
 
458
460
  # Precision info
@@ -479,7 +481,7 @@ lla = ham.to_lla
479
481
  ham.neighbors # => { N: HAM, S: HAM, E: HAM, W: HAM, NE: ..., NW: ..., SE: ..., SW: ... }
480
482
 
481
483
  # Bounding rectangle of the grid square
482
- area = ham.to_area # => Areas::Rectangle
484
+ area = ham.to_area # => Areas::BoundingBox
483
485
  area.includes?(ham.to_lla) # => true
484
486
 
485
487
  # Precision info
@@ -506,7 +508,7 @@ lla = olc.to_lla
506
508
  olc.neighbors # => { N: OLC, S: OLC, E: OLC, W: OLC, NE: ..., NW: ..., SE: ..., SW: ... }
507
509
 
508
510
  # Bounding rectangle of the plus code cell
509
- area = olc.to_area # => Areas::Rectangle
511
+ area = olc.to_area # => Areas::BoundingBox
510
512
  area.includes?(olc.to_lla) # => true
511
513
 
512
514
  # Precision info
@@ -531,19 +533,95 @@ points = [
531
533
  polygon = Areas::Polygon.new(boundary: points)
532
534
  polygon.centroid # => computed centroid as LLA
533
535
 
534
- # Rectangle area (accepts any coordinate type)
536
+ # BoundingBox area (accepts any coordinate type)
535
537
  nw = Coordinates::LLA.new(lat: 41.0, lng: -75.0)
536
538
  se = Coordinates::LLA.new(lat: 40.0, lng: -74.0)
537
- rect = Areas::Rectangle.new(nw: nw, se: se)
539
+ rect = Areas::BoundingBox.new(nw: nw, se: se)
538
540
  rect.centroid # => LLA at center
539
541
  rect.ne # => computed NE corner
540
542
  rect.sw # => computed SW corner
541
543
  rect.includes?(point) # => true/false
542
544
  ```
543
545
 
546
+ ### Segments
547
+
548
+ `Segment` represents a directed line segment between two points. It provides the geometric primitives that `Path` and `Polygon` build on.
549
+
550
+ ```ruby
551
+ a = Coordinate::LLA.new(lat: 40.7484, lng: -73.9857, alt: 0)
552
+ b = Coordinate::LLA.new(lat: 40.7580, lng: -73.9855, alt: 0)
553
+
554
+ seg = Segment.new(a, b)
555
+
556
+ # Properties (lazily computed, cached)
557
+ seg.length # => Distance
558
+ seg.distance # => Distance (alias for length)
559
+ seg.bearing # => Bearing
560
+ seg.midpoint # => LLA at halfway point
561
+
562
+ # Projection — closest point on segment to a target
563
+ foot, dist_m = seg.project(target_point)
564
+
565
+ # Interpolation — point at fraction along segment
566
+ seg.interpolate(0.25) # => LLA at quarter-way
567
+
568
+ # Membership
569
+ seg.includes?(a) # => true (vertex check only)
570
+ seg.includes?(seg.midpoint) # => false
571
+ seg.contains?(seg.midpoint) # => true (on-segment check)
572
+
573
+ # Intersection
574
+ seg.intersects?(other_seg) # => true/false
575
+
576
+ # Conversion
577
+ seg.reverse # => Segment with swapped endpoints
578
+ seg.to_path # => two-point Path
579
+ seg.to_a # => [start_point, end_point]
580
+ ```
581
+
582
+ ### Paths
583
+
584
+ `Path` is a directed, ordered sequence of unique coordinates representing routes, trails, or boundaries.
585
+
586
+ ```ruby
587
+ route = Path.new(coordinates: [battery_park, wall_street, brooklyn_bridge, city_hall])
588
+
589
+ # Navigation
590
+ route.first # => starting waypoint
591
+ route.next(wall_street) # => brooklyn_bridge
592
+ route.total_distance.to_km # => "3.42 km"
593
+
594
+ # Build incrementally
595
+ trail = Path.new
596
+ trail << start << middle << finish
597
+ trail >> new_start # prepend
598
+
599
+ # Combine paths
600
+ combined = downtown + uptown # concatenate
601
+ trimmed = combined - detour # remove coordinates
602
+
603
+ # Closest approach (geometric projection, not just waypoints)
604
+ route.closest_coordinate_to(off_path_point)
605
+ route.distance_to(target)
606
+ route.closest_points_to(other_path) # path-to-path
607
+
608
+ # Spatial operations
609
+ sub = route.between(a, b) # extract subpath
610
+ left, right = route.split_at(c) # split at waypoint
611
+ route.at_distance(Distance.km(2)) # interpolate along path
612
+ route.bounds # => Areas::BoundingBox
613
+ route.to_polygon # close into polygon
614
+ route.intersects?(other_path) # crossing detection
615
+ route.contains?(point) # on-segment check
616
+
617
+ # Enumerable
618
+ route.map { |c| c.lat }
619
+ route.select { |c| c.lat > 40.72 }
620
+ ```
621
+
544
622
  ### Features
545
623
 
546
- `Feature` wraps a geometry (any coordinate or area) with a label and a metadata hash. It delegates `distance_to` and `bearing_to` to its geometry, using the centroid for area geometries.
624
+ `Feature` wraps a geometry (any coordinate, area, or path) with a label and a metadata hash. It delegates `distance_to` and `bearing_to` to its geometry, using the centroid for area geometries.
547
625
 
548
626
  ```ruby
549
627
  liberty = Feature.new(
@@ -600,6 +678,7 @@ The [`examples/`](examples/) directory contains runnable demo scripts showing pr
600
678
  | [`03_distance_calculations.rb`](examples/03_distance_calculations.rb) | Distance class features, unit conversions, and arithmetic |
601
679
  | [`04_bearing_calculations.rb`](examples/04_bearing_calculations.rb) | Bearing class, compass directions, elevation angles, and chain bearings |
602
680
  | [`05_map_rendering/`](examples/05_map_rendering/) | Render landmarks on a raster map with Feature objects, polygon areas, bearing arrows, and icons using [libgd-gis](https://rubygems.org/gems/libgd-gis) |
681
+ | [`06_path_operations.rb`](examples/06_path_operations.rb) | Path class: construction, navigation, mutation, path arithmetic, closest approach, containment, Enumerable, equality, subpaths, split, interpolation, bounding boxes, polygon conversion, intersection, path-to-path/area closest points, and Feature integration |
603
682
 
604
683
  Run any example with:
605
684
 
@@ -173,11 +173,11 @@ Neighbors preserve the same precision as the original code. Latitude is clamped
173
173
 
174
174
  ## Area
175
175
 
176
- The `to_area` method returns the GARS cell as a `Geodetic::Areas::Rectangle`.
176
+ The `to_area` method returns the GARS cell as a `Geodetic::Areas::BoundingBox`.
177
177
 
178
178
  ```ruby
179
179
  area = coord.to_area
180
- # => Geodetic::Areas::Rectangle
180
+ # => Geodetic::Areas::BoundingBox
181
181
 
182
182
  area.includes?(coord.to_lla) # => true (midpoint is inside the cell)
183
183
  area.nw # => LLA (northwest corner)
@@ -154,11 +154,11 @@ Neighbors preserve the same precision as the original code. Latitude is clamped
154
154
 
155
155
  ## Area
156
156
 
157
- The `to_area` method returns the GEOREF cell as a `Geodetic::Areas::Rectangle`.
157
+ The `to_area` method returns the GEOREF cell as a `Geodetic::Areas::BoundingBox`.
158
158
 
159
159
  ```ruby
160
160
  area = coord.to_area
161
- # => Geodetic::Areas::Rectangle
161
+ # => Geodetic::Areas::BoundingBox
162
162
 
163
163
  area.includes?(coord.to_lla) # => true (midpoint is inside the cell)
164
164
  area.nw # => LLA (northwest corner)
@@ -142,11 +142,11 @@ Neighbors preserve the same precision as the original geohash. Longitude wraps c
142
142
 
143
143
  ## Area
144
144
 
145
- The `to_area` method returns the geohash cell as a `Geodetic::Areas::Rectangle`.
145
+ The `to_area` method returns the geohash cell as a `Geodetic::Areas::BoundingBox`.
146
146
 
147
147
  ```ruby
148
148
  area = coord.to_area
149
- # => Geodetic::Areas::Rectangle
149
+ # => Geodetic::Areas::BoundingBox
150
150
 
151
151
  area.includes?(coord.to_lla) # => true (midpoint is inside the cell)
152
152
  area.nw # => LLA (northwest corner)
@@ -137,11 +137,11 @@ Neighbor computation propagates carries when the adjustment wraps beyond the mat
137
137
 
138
138
  ## Area
139
139
 
140
- The `to_area` method returns the geohash cell as a `Geodetic::Areas::Rectangle`.
140
+ The `to_area` method returns the geohash cell as a `Geodetic::Areas::BoundingBox`.
141
141
 
142
142
  ```ruby
143
143
  area = coord.to_area
144
- # => Geodetic::Areas::Rectangle
144
+ # => Geodetic::Areas::BoundingBox
145
145
 
146
146
  area.includes?(coord.to_lla) # => true (midpoint is inside the cell)
147
147
  area.nw # => LLA (northwest corner)
@@ -36,8 +36,8 @@ Geodetic searches these paths automatically:
36
36
 
37
37
  | Feature | GH/OLC/GARS/GEOREF/HAM | H3 |
38
38
  |---------|------------------------|-----|
39
- | Cell shape | Rectangle | Hexagon (6 vertices) |
40
- | `to_area` returns | `Areas::Rectangle` | `Areas::Polygon` |
39
+ | Cell shape | BoundingBox | Hexagon (6 vertices) |
40
+ | `to_area` returns | `Areas::BoundingBox` | `Areas::Polygon` |
41
41
  | `neighbors` returns | Hash with 8 cardinal keys | Array of 6 cells |
42
42
  | Code format | String | 64-bit integer (hex string) |
43
43
  | Dependency | None (pure Ruby) | `libh3` (C library via fiddle) |
@@ -150,11 +150,11 @@ Neighbors preserve the same precision as the original locator. Latitude is clamp
150
150
 
151
151
  ## Area
152
152
 
153
- The `to_area` method returns the grid square as a `Geodetic::Areas::Rectangle`.
153
+ The `to_area` method returns the grid square as a `Geodetic::Areas::BoundingBox`.
154
154
 
155
155
  ```ruby
156
156
  area = coord.to_area
157
- # => Geodetic::Areas::Rectangle
157
+ # => Geodetic::Areas::BoundingBox
158
158
 
159
159
  area.includes?(coord.to_lla) # => true (midpoint is inside the cell)
160
160
  area.nw # => LLA (northwest corner)
@@ -155,11 +155,11 @@ Neighbors preserve the same precision as the original code. Latitude is clamped
155
155
 
156
156
  ## Area
157
157
 
158
- The `to_area` method returns the plus code cell as a `Geodetic::Areas::Rectangle`.
158
+ The `to_area` method returns the plus code cell as a `Geodetic::Areas::BoundingBox`.
159
159
 
160
160
  ```ruby
161
161
  area = coord.to_area
162
- # => Geodetic::Areas::Rectangle
162
+ # => Geodetic::Areas::BoundingBox
163
163
 
164
164
  area.includes?(coord.to_lla) # => true (midpoint is inside the cell)
165
165
  area.nw # => LLA (northwest corner)
data/docs/index.md CHANGED
@@ -14,7 +14,9 @@
14
14
  <li><strong>Distance Calculations</strong> - Vincenty great-circle and straight-line with unit tracking<br>
15
15
  <li><strong>Bearing Calculations</strong> - Forward azimuth, back azimuth, compass directions, elevation angles<br>
16
16
  <li><strong>Geoid Height Support</strong> - EGM96, EGM2008, GEOID18, GEOID12B models<br>
17
- <li><strong>Geographic Areas</strong> - Circle, Polygon, and Rectangle with point-in-area tests<br>
17
+ <li><strong>Geographic Areas</strong> - Circle, Polygon, BoundingBox, Triangle, Rectangle, Pentagon, Hexagon, Octagon<br>
18
+ <li><strong>Segments</strong> - Directed two-point line segments with projection, intersection, and interpolation<br>
19
+ <li><strong>Paths</strong> - Directed coordinate sequences with navigation, interpolation, closest approach, intersection, and area conversion<br>
18
20
  <li><strong>Features</strong> - Named geometry wrapper with metadata and delegated distance/bearing<br>
19
21
  <li><strong>Validated Setters</strong> - Type coercion and range validation on all coordinate attributes<br>
20
22
  <li><strong>Serialization</strong> - to_s(precision), to_a, from_string, from_array, DMS format<br>
@@ -56,8 +58,10 @@ Geodetic supports full bidirectional conversion between all 18 coordinate system
56
58
 
57
59
  - **16 geodetic datums** -- WGS84, GRS 1980, Clarke 1866, Airy 1830, Bessel 1841, and more. All conversion methods accept an optional datum parameter, defaulting to WGS84.
58
60
  - **Geoid height calculations** -- Convert between ellipsoidal and orthometric heights using models such as EGM96, EGM2008, GEOID18, and GEOID12B.
59
- - **Geographic areas** -- `Geodetic::Areas::Circle`, `Geodetic::Areas::Polygon`, and `Geodetic::Areas::Rectangle` for point-in-area testing.
60
- - **Features** -- `Geodetic::Feature` wraps any coordinate or area with a label and metadata hash, delegating `distance_to` and `bearing_to` to the underlying geometry.
61
+ - **Geographic areas** -- `Geodetic::Areas::Circle`, `Geodetic::Areas::Polygon`, `Geodetic::Areas::BoundingBox`, plus polygon subclasses (`Triangle`, `Rectangle`, `Pentagon`, `Hexagon`, `Octagon`) for point-in-area testing.
62
+ - **Segments** -- `Geodetic::Segment` is a directed two-point line segment with projection, intersection detection, interpolation, and membership testing. It is the geometric primitive underlying Path and Polygon operations.
63
+ - **Paths** -- `Geodetic::Path` is a directed, ordered sequence of unique coordinates supporting navigation, segment analysis, interpolation, closest approach (geometric projection), containment testing, bounding boxes, polygon conversion, and path intersection detection.
64
+ - **Features** -- `Geodetic::Feature` wraps any coordinate, area, or path with a label and metadata hash, delegating `distance_to` and `bearing_to` to the underlying geometry.
61
65
 
62
66
  ## Design Principles
63
67
 
@@ -1,6 +1,6 @@
1
1
  # Areas Reference
2
2
 
3
- The `Geodetic::Areas` module provides three geometric area classes for point-in-area testing: `Circle`, `Polygon`, and `Rectangle`. All operate on `Geodetic::Coordinate::LLA` points.
3
+ The `Geodetic::Areas` module provides geometric area classes for point-in-area testing: `Circle`, `Polygon`, `BoundingBox`, and polygon subclasses (`Triangle`, `Rectangle`, `Pentagon`, `Hexagon`, `Octagon`). All operate on `Geodetic::Coordinate::LLA` points.
4
4
 
5
5
  ---
6
6
 
@@ -107,18 +107,38 @@ Returns `true` if the given LLA point falls outside the polygon. The logical inv
107
107
  polygon.excludes?(point) # => true or false
108
108
  ```
109
109
 
110
+ #### `segments` / `edges` / `border`
111
+
112
+ Returns an array of `Segment` objects for each edge of the polygon.
113
+
114
+ ```ruby
115
+ polygon.segments # => [Segment(p1→p2), Segment(p2→p3), Segment(p3→p1)]
116
+ ```
117
+
118
+ ### Self-Intersection Validation
119
+
120
+ By default the constructor validates that no edge crosses another edge. Pass `validate: false` to skip this check (used by subclasses that generate known-good geometry).
121
+
122
+ ```ruby
123
+ Geodetic::Areas::Polygon.new(boundary: points) # validates
124
+ Geodetic::Areas::Polygon.new(boundary: points, validate: false) # skips
125
+ ```
126
+
110
127
  ### Alias Summary
111
128
 
112
129
  | Primary Method | Aliases |
113
130
  |---------------|---------|
114
131
  | `includes?` | `include?`, `inside?` |
115
132
  | `excludes?` | `exclude?`, `outside?` |
133
+ | `segments` | `edges`, `border` |
116
134
 
117
135
  ---
118
136
 
119
- ## Geodetic::Areas::Rectangle
137
+ ## Geodetic::Areas::BoundingBox
120
138
 
121
- Defines an axis-aligned rectangle by its northwest and southeast corners.
139
+ Defines an axis-aligned bounding box by its northwest and southeast corners. Edges are always oriented East-West and North-South.
140
+
141
+ > **Note:** `Rectangle` is a separate class (`Polygon` subclass) constructed from a `Segment` centerline. It is not related to `BoundingBox`.
122
142
 
123
143
  ### Constructor
124
144
 
@@ -126,7 +146,7 @@ Defines an axis-aligned rectangle by its northwest and southeast corners.
126
146
  nw = Geodetic::Coordinate::LLA.new(lat: 41.0, lng: -75.0)
127
147
  se = Geodetic::Coordinate::LLA.new(lat: 40.0, lng: -74.0)
128
148
 
129
- rectangle = Geodetic::Areas::Rectangle.new(nw: nw, se: se)
149
+ bbox = Geodetic::Areas::BoundingBox.new(nw: nw, se: se)
130
150
  ```
131
151
 
132
152
  The constructor accepts any coordinate type that responds to `to_lla` -- coordinates are automatically converted to LLA.
@@ -134,7 +154,7 @@ The constructor accepts any coordinate type that responds to `to_lla` -- coordin
134
154
  ```ruby
135
155
  nw_wm = Geodetic::Coordinate::WebMercator.from_lla(nw)
136
156
  se_wm = Geodetic::Coordinate::WebMercator.from_lla(se)
137
- rectangle = Geodetic::Areas::Rectangle.new(nw: nw_wm, se: se_wm)
157
+ bbox = Geodetic::Areas::BoundingBox.new(nw: nw_wm, se: se_wm)
138
158
  ```
139
159
 
140
160
  Raises `ArgumentError` if the NW corner has a lower latitude than the SE corner, or if the NW corner has a higher longitude than the SE corner.
@@ -152,27 +172,27 @@ All attributes are read-only.
152
172
  ### Computed Corners
153
173
 
154
174
  ```ruby
155
- rectangle.ne # => LLA (nw.lat, se.lng)
156
- rectangle.sw # => LLA (se.lat, nw.lng)
175
+ bbox.ne # => LLA (nw.lat, se.lng)
176
+ bbox.sw # => LLA (se.lat, nw.lng)
157
177
  ```
158
178
 
159
179
  ### Methods
160
180
 
161
181
  #### `includes?(a_point)` / `include?(a_point)` / `inside?(a_point)`
162
182
 
163
- Returns `true` if the given point falls within (or on the boundary of) the rectangle. Accepts any coordinate type that responds to `to_lla`.
183
+ Returns `true` if the given point falls within (or on the boundary of) the bounding box. Accepts any coordinate type that responds to `to_lla`.
164
184
 
165
185
  ```ruby
166
186
  point = Geodetic::Coordinate::LLA.new(lat: 40.5, lng: -74.5)
167
- rectangle.includes?(point) # => true
187
+ bbox.includes?(point) # => true
168
188
  ```
169
189
 
170
190
  #### `excludes?(a_point)` / `exclude?(a_point)` / `outside?(a_point)`
171
191
 
172
- Returns `true` if the given point falls outside the rectangle.
192
+ Returns `true` if the given point falls outside the bounding box.
173
193
 
174
194
  ```ruby
175
- rectangle.excludes?(point) # => true or false
195
+ bbox.excludes?(point) # => true or false
176
196
  ```
177
197
 
178
198
  ### Alias Summary
@@ -182,14 +202,120 @@ rectangle.excludes?(point) # => true or false
182
202
  | `includes?` | `include?`, `inside?` |
183
203
  | `excludes?` | `exclude?`, `outside?` |
184
204
 
185
- ### Integration with GH36
205
+ ### Integration with Spatial Hashes
186
206
 
187
- `Geodetic::Coordinate::GH36#to_area` returns a `Rectangle` representing the geohash cell's bounding box:
207
+ Spatial hash coordinate systems (`GH`, `GH36`, `HAM`, `OLC`, `GEOREF`, `GARS`) return a `BoundingBox` from `to_area`:
188
208
 
189
209
  ```ruby
190
210
  gh36 = Geodetic::Coordinate::GH36.new("bdrdC26BqH")
191
211
  area = gh36.to_area
192
- # => Geodetic::Areas::Rectangle
212
+ # => Geodetic::Areas::BoundingBox
193
213
 
194
214
  area.includes?(gh36.to_lla) # => true (midpoint is inside the cell)
195
215
  ```
216
+
217
+ ---
218
+
219
+ ## Polygon Subclasses
220
+
221
+ All polygon subclasses inherit from `Polygon` and share its `includes?`/`excludes?`, `segments`, `centroid`, and `boundary` methods. Each adds shape-specific constructors and attributes.
222
+
223
+ ### Geodetic::Areas::Triangle
224
+
225
+ A three-sided polygon with four construction modes.
226
+
227
+ #### Constructors
228
+
229
+ ```ruby
230
+ center = Geodetic::Coordinate::LLA.new(lat: 40.7484, lng: -73.9857, alt: 0)
231
+
232
+ # Isosceles — width and height from a center point
233
+ tri = Geodetic::Areas::Triangle.new(center: center, width: 400, height: 600, bearing: 0)
234
+
235
+ # Equilateral by circumradius
236
+ tri = Geodetic::Areas::Triangle.new(center: center, radius: 500, bearing: 45)
237
+
238
+ # Equilateral by side length
239
+ tri = Geodetic::Areas::Triangle.new(center: center, side: 600)
240
+
241
+ # Arbitrary 3 vertices
242
+ tri = Geodetic::Areas::Triangle.new(vertices: [p1, p2, p3])
243
+ ```
244
+
245
+ #### Attributes and Methods
246
+
247
+ | Method | Returns | Description |
248
+ |--------|---------|-------------|
249
+ | `sides` | Integer | Always 3 |
250
+ | `vertices` | Array<LLA> | The three vertices |
251
+ | `center` | LLA | Center point (or centroid for arbitrary vertices) |
252
+ | `width` | Float | Base width in meters (0 for arbitrary vertices) |
253
+ | `height` | Float | Height in meters (0 for arbitrary vertices) |
254
+ | `bearing` | Float | Bearing in degrees (0 for arbitrary vertices) |
255
+ | `base` | Float/nil | Same as width; nil for arbitrary vertices |
256
+ | `side_lengths` | Array<Float> | Three side lengths in meters |
257
+ | `equilateral?` | Boolean | All sides equal (within 5m tolerance) |
258
+ | `isosceles?` | Boolean | Exactly two sides equal |
259
+ | `scalene?` | Boolean | No two sides equal |
260
+ | `to_bounding_box` | BoundingBox | Axis-aligned bounding box enclosing the triangle |
261
+
262
+ ---
263
+
264
+ ### Geodetic::Areas::Rectangle
265
+
266
+ A four-sided polygon defined by a centerline `Segment` and a perpendicular width. The centerline is the fundamental representation — center, height, and bearing are all derived from it.
267
+
268
+ #### Constructor
269
+
270
+ ```ruby
271
+ a = Geodetic::Coordinate::LLA.new(lat: 40.7400, lng: -73.9900, alt: 0)
272
+ b = Geodetic::Coordinate::LLA.new(lat: 40.7500, lng: -73.9900, alt: 0)
273
+
274
+ # From a Segment object
275
+ seg = Geodetic::Segment.new(a, b)
276
+ rect = Geodetic::Areas::Rectangle.new(segment: seg, width: 200)
277
+
278
+ # From an array of two coordinates
279
+ rect = Geodetic::Areas::Rectangle.new(segment: [a, b], width: 200)
280
+
281
+ # Width accepts a Distance instance (converted to meters)
282
+ rect = Geodetic::Areas::Rectangle.new(segment: seg, width: seg.distance)
283
+ rect.square? #=> true
284
+ ```
285
+
286
+ The `segment:` parameter defines the centerline of the rectangle. Height equals the segment length, bearing equals the segment direction, and center equals the segment midpoint. Width is the perpendicular extent, specified in meters or as a `Distance` instance.
287
+
288
+ #### Attributes and Methods
289
+
290
+ | Method | Returns | Description |
291
+ |--------|---------|-------------|
292
+ | `sides` | Integer | Always 4 |
293
+ | `centerline` | Segment | The centerline segment |
294
+ | `width` | Float | Perpendicular width in meters |
295
+ | `center` | LLA | Midpoint of the centerline |
296
+ | `height` | Float | Length of the centerline in meters |
297
+ | `bearing` | Float | Direction of the centerline in degrees |
298
+ | `corners` | Array<LLA> | Four corners: front-left, front-right, back-right, back-left |
299
+ | `square?` | Boolean | True when width equals height |
300
+ | `to_bounding_box` | BoundingBox | Axis-aligned bounding box enclosing the rectangle |
301
+
302
+ ---
303
+
304
+ ### Geodetic::Areas::Pentagon / Hexagon / Octagon
305
+
306
+ Regular polygons constructed from a center point and circumradius.
307
+
308
+ ```ruby
309
+ center = Geodetic::Coordinate::LLA.new(lat: 40.7484, lng: -73.9857, alt: 0)
310
+
311
+ pent = Geodetic::Areas::Pentagon.new(center: center, radius: 500, bearing: 0)
312
+ hex = Geodetic::Areas::Hexagon.new(center: center, radius: 500, bearing: 0)
313
+ oct = Geodetic::Areas::Octagon.new(center: center, radius: 500, bearing: 0)
314
+ ```
315
+
316
+ | Method | Returns | Description |
317
+ |--------|---------|-------------|
318
+ | `sides` | Integer | 5, 6, or 8 respectively |
319
+ | `center` | LLA | Center point |
320
+ | `radius` | Float | Circumradius in meters |
321
+ | `bearing` | Float | Rotation bearing in degrees |
@@ -14,7 +14,7 @@ Feature.new(
14
14
  )
15
15
  ```
16
16
 
17
- The `geometry` parameter accepts any coordinate class or any area class (`Circle`, `Polygon`, `Rectangle`). 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,9 +35,10 @@ 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
+ - **Path** — a `Geodetic::Path` representing a route or trail
39
40
 
40
- When the geometry is an area, `distance_to` and `bearing_to` use the area's `centroid` as the reference point.
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.
41
42
 
42
43
  ---
43
44