geodetic 0.3.2 → 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: f295df82e85611a9a7a7dd56593ae029d6ad096cc5c96021d5b59b4f4b001cc2
4
- data.tar.gz: 6da1bc3461fd4f372b53f5458d322f8c14d1feb8a2afa93a2375cb5641674674
3
+ metadata.gz: f85da37953a9e974422502d62fbf82279fcf4f0ff71594ada2bb150e07cc75d1
4
+ data.tar.gz: a986282c09df583d9de453034f36e1a15c0d29cc016f586f09239f4f642d6fae
5
5
  SHA512:
6
- metadata.gz: 1a0b72935abf28bbcb41182f800d1bdb52466530565f13d75fec5f3eec14c0dac7680f8220f09c51cf4d60ed3b87fc53a3675ceb972f1cec17ca06cbd096d717
7
- data.tar.gz: 754e5b64d8e1d333dafd270a6e21434d46102ea57e43fbba16b83bcecd9eb69eb4e834a51554c10eb59433f6d434930bf2664fca0d9c22b37cf7378e90cc106e
6
+ metadata.gz: 2d72a9b0e9e1a0688f25659dbff3d35e8b1c001c72468d2d2bd5c64ffe9fd2f16315dbbe4ec429c545115268e7486db81b26c4f6c9d625de3a8089b82cbcf8df
7
+ data.tar.gz: 031eb9604c27d9683c74bf3483b913a5ddb065ed78bd5375f23297f39b923f5a6212498c236982a310afe25fa5ca300df0dfaaa60d8632ad02950c75c2662e15
data/CHANGELOG.md CHANGED
@@ -11,6 +11,45 @@ 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
+
14
53
  ## [0.3.2] - 2026-03-09
15
54
 
16
55
  ### Added
@@ -19,11 +58,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
19
58
  - **Navigation**: `first`, `last`, `next`, `prev`, `segments`, `size`, `empty?`
20
59
  - **Membership**: `include?`/`includes?` (waypoint check), `contains?`/`inside?` (on-segment check with configurable tolerance)
21
60
  - **Spatial**: `nearest_waypoint`, `closest_coordinate_to`, `distance_to`, `bearing_to` using geometric projection onto segments
22
- - **Closest points**: `closest_points_to` for Path-to-Path, Path-to-Polygon, Path-to-Rectangle, and Path-to-Circle
61
+ - **Closest points**: `closest_points_to` for Path-to-Path, Path-to-Polygon, Path-to-BoundingBox, and Path-to-Circle
23
62
  - **Computed**: `total_distance`, `segment_distances`, `segment_bearings`, `reverse`
24
63
  - **Subpath/split**: `between(from, to)` extracts a subpath; `split_at(coord)` divides into two paths sharing the split point
25
64
  - **Interpolation**: `at_distance(distance)` finds the coordinate at a given distance along the path
26
- - **Bounding box**: `bounds` returns an `Areas::Rectangle`
65
+ - **Bounding box**: `bounds` returns an `Areas::BoundingBox`
27
66
  - **Polygon conversion**: `to_polygon` closes the path (validates no self-intersection)
28
67
  - **Intersection**: `intersects?(other_path)` detects crossing segments
29
68
  - **Equality**: `==` compares coordinates in order
data/README.md CHANGED
@@ -18,7 +18,8 @@
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>
22
23
  - <strong>Paths</strong> - Directed coordinate sequences with navigation, interpolation, closest approach, intersection, and area conversion<br>
23
24
  - <strong>Features</strong> - Named geometry wrapper with metadata and delegated distance/bearing<br>
24
25
  - <strong>Validated Setters</strong> - Type coercion and range validation on all coordinate attributes<br>
@@ -426,7 +427,7 @@ lla = gh36.to_lla
426
427
  gh36.neighbors # => { N: GH36, S: GH36, E: GH36, W: GH36, NE: ..., NW: ..., SE: ..., SW: ... }
427
428
 
428
429
  # Bounding rectangle of the geohash cell
429
- area = gh36.to_area # => Areas::Rectangle
430
+ area = gh36.to_area # => Areas::BoundingBox
430
431
  area.includes?(gh36.to_lla) # => true
431
432
 
432
433
  # Precision info
@@ -453,7 +454,7 @@ lla = gh.to_lla
453
454
  gh.neighbors # => { N: GH, S: GH, E: GH, W: GH, NE: ..., NW: ..., SE: ..., SW: ... }
454
455
 
455
456
  # Bounding rectangle of the geohash cell
456
- area = gh.to_area # => Areas::Rectangle
457
+ area = gh.to_area # => Areas::BoundingBox
457
458
  area.includes?(gh.to_lla) # => true
458
459
 
459
460
  # Precision info
@@ -480,7 +481,7 @@ lla = ham.to_lla
480
481
  ham.neighbors # => { N: HAM, S: HAM, E: HAM, W: HAM, NE: ..., NW: ..., SE: ..., SW: ... }
481
482
 
482
483
  # Bounding rectangle of the grid square
483
- area = ham.to_area # => Areas::Rectangle
484
+ area = ham.to_area # => Areas::BoundingBox
484
485
  area.includes?(ham.to_lla) # => true
485
486
 
486
487
  # Precision info
@@ -507,7 +508,7 @@ lla = olc.to_lla
507
508
  olc.neighbors # => { N: OLC, S: OLC, E: OLC, W: OLC, NE: ..., NW: ..., SE: ..., SW: ... }
508
509
 
509
510
  # Bounding rectangle of the plus code cell
510
- area = olc.to_area # => Areas::Rectangle
511
+ area = olc.to_area # => Areas::BoundingBox
511
512
  area.includes?(olc.to_lla) # => true
512
513
 
513
514
  # Precision info
@@ -532,16 +533,52 @@ points = [
532
533
  polygon = Areas::Polygon.new(boundary: points)
533
534
  polygon.centroid # => computed centroid as LLA
534
535
 
535
- # Rectangle area (accepts any coordinate type)
536
+ # BoundingBox area (accepts any coordinate type)
536
537
  nw = Coordinates::LLA.new(lat: 41.0, lng: -75.0)
537
538
  se = Coordinates::LLA.new(lat: 40.0, lng: -74.0)
538
- rect = Areas::Rectangle.new(nw: nw, se: se)
539
+ rect = Areas::BoundingBox.new(nw: nw, se: se)
539
540
  rect.centroid # => LLA at center
540
541
  rect.ne # => computed NE corner
541
542
  rect.sw # => computed SW corner
542
543
  rect.includes?(point) # => true/false
543
544
  ```
544
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
+
545
582
  ### Paths
546
583
 
547
584
  `Path` is a directed, ordered sequence of unique coordinates representing routes, trails, or boundaries.
@@ -572,7 +609,7 @@ route.closest_points_to(other_path) # path-to-path
572
609
  sub = route.between(a, b) # extract subpath
573
610
  left, right = route.split_at(c) # split at waypoint
574
611
  route.at_distance(Distance.km(2)) # interpolate along path
575
- route.bounds # => Areas::Rectangle
612
+ route.bounds # => Areas::BoundingBox
576
613
  route.to_polygon # close into polygon
577
614
  route.intersects?(other_path) # crossing detection
578
615
  route.contains?(point) # on-segment check
@@ -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,8 @@
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>
18
19
  <li><strong>Paths</strong> - Directed coordinate sequences with navigation, interpolation, closest approach, intersection, and area conversion<br>
19
20
  <li><strong>Features</strong> - Named geometry wrapper with metadata and delegated distance/bearing<br>
20
21
  <li><strong>Validated Setters</strong> - Type coercion and range validation on all coordinate attributes<br>
@@ -57,7 +58,8 @@ Geodetic supports full bidirectional conversion between all 18 coordinate system
57
58
 
58
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.
59
60
  - **Geoid height calculations** -- Convert between ellipsoidal and orthometric heights using models such as EGM96, EGM2008, GEOID18, and GEOID12B.
60
- - **Geographic areas** -- `Geodetic::Areas::Circle`, `Geodetic::Areas::Polygon`, and `Geodetic::Areas::Rectangle` for point-in-area testing.
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.
61
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.
62
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.
63
65
 
@@ -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, 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