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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f295df82e85611a9a7a7dd56593ae029d6ad096cc5c96021d5b59b4f4b001cc2
4
- data.tar.gz: 6da1bc3461fd4f372b53f5458d322f8c14d1feb8a2afa93a2375cb5641674674
3
+ metadata.gz: 495ba0272532d5591ecbb37869857675af00014d6863849c79c6fbef35e590c3
4
+ data.tar.gz: dcb68a4385f7a888fbfb37b421d2f82c66aae077bda8557ce2a7b60c57e38b35
5
5
  SHA512:
6
- metadata.gz: 1a0b72935abf28bbcb41182f800d1bdb52466530565f13d75fec5f3eec14c0dac7680f8220f09c51cf4d60ed3b87fc53a3675ceb972f1cec17ca06cbd096d717
7
- data.tar.gz: 754e5b64d8e1d333dafd270a6e21434d46102ea57e43fbba16b83bcecd9eb69eb4e834a51554c10eb59433f6d434930bf2664fca0d9c22b37cf7378e90cc106e
6
+ metadata.gz: 8f276d8924aad177efc94aee0037f1fbd4e1cb655668ed54ff5ba0e471c16dd91687966577505057e31a144ea545adf3d8843535ca0705fca69fc5e5bf9a3637
7
+ data.tar.gz: f2d8102549b1502099d149453f5b18d8143b5799e3d3f5c2c4f947c86b7e2de1355e74916ae3b0d7376652fdb547ebd48488bff2eba18d60211da4ff0ff030a3
data/CHANGELOG.md CHANGED
@@ -11,6 +11,88 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
  ## [Unreleased]
12
12
 
13
13
 
14
+ ## [0.5.0] - 2026-03-10
15
+
16
+ ### Added
17
+
18
+ - **`Geodetic::Vector` class** — geodetic displacement pairing a Distance (magnitude) with a Bearing (direction)
19
+ - **Construction**: `Vector.new(distance:, bearing:)` with automatic coercion from numeric values
20
+ - **Components**: `north`, `east` — decomposed meters; `magnitude` — distance in meters
21
+ - **Factory methods**: `Vector.from_components(north:, east:)`, `Vector.from_segment(segment)`
22
+ - **Vincenty direct**: `destination_from(origin)` solves the direct geodetic problem on the WGS84 ellipsoid
23
+ - **Arithmetic**: `+`, `-` (component-wise), `*`, `/` (scalar), `-@` (unary minus); `Numeric * Vector` via coerce
24
+ - **Products**: `dot(other)`, `cross(other)`, `angle_between(other)`
25
+ - **Properties**: `zero?`, `normalize`, `reverse`/`inverse`
26
+ - **Comparable**: ordered by distance (magnitude)
27
+ - Near-zero results (< 1e-9 m) snap to clean zero vector
28
+ - **Geodetic arithmetic with `+` operator** — build geometry from coordinates, vectors, and distances:
29
+ - `Coordinate + Coordinate` → Segment
30
+ - `Coordinate + Coordinate + Coordinate` → Path (via Segment + Coordinate → Path)
31
+ - `Coordinate + Segment` → Path
32
+ - `Segment + Coordinate` → Path
33
+ - `Segment + Segment` → Path
34
+ - `Coordinate + Distance` → Circle
35
+ - `Distance + Coordinate` → Circle (commutative)
36
+ - `Coordinate + Vector` → Segment (Vincenty direct)
37
+ - `Vector + Coordinate` → Segment (reverse start to coordinate)
38
+ - `Segment + Vector` → Path (extend from endpoint)
39
+ - `Vector + Segment` → Path (prepend via reverse)
40
+ - `Path + Vector` → Path (extend from last point)
41
+ - **Translation with `*` operator and `translate` method** — uniform displacement across all geometric types:
42
+ - `Coordinate * Vector` → Coordinate (translated point)
43
+ - `Segment * Vector` → Segment (translated endpoints)
44
+ - `Path * Vector` → Path (translated waypoints)
45
+ - `Circle * Vector` → Circle (translated centroid, preserved radius)
46
+ - `Polygon * Vector` → Polygon (translated vertices)
47
+ - **`Segment#to_vector`** — extract a Vector from a Segment's length and bearing
48
+ - **`Path#to_corridor(width:)`** — convert a path into a Polygon corridor of a given width; uses mean bearing at interior waypoints to avoid self-intersection; accepts meters or a Distance object
49
+ - **Geodetic arithmetic example** (`examples/08_geodetic_arithmetic.rb`) — 11-section demo covering all arithmetic operators, Vector class, translation, corridors, and composed operations
50
+ - Documentation: `docs/reference/vector.md` (Vector reference), `docs/reference/arithmetic.md` (Geodetic Arithmetic reference)
51
+
52
+ ### Changed
53
+
54
+ - Updated README with Vector, Geodetic Arithmetic, and Corridors sections; added to key features list
55
+ - Updated `examples/README.md` with example 08 description
56
+
57
+ ## [0.4.0] - 2026-03-10
58
+
59
+ ### Added
60
+
61
+ - **`Geodetic::Segment` class** — directed two-point line segment, the fundamental geometric primitive underlying Path and Polygon
62
+ - **Properties**: `length`/`distance` (returns Distance), `length_meters`, `bearing` (returns Bearing), `midpoint`/`centroid` (returns LLA) — all lazily computed and cached
63
+ - **Projection**: `project(point)` returns the closest point on the segment and perpendicular distance
64
+ - **Interpolation**: `interpolate(fraction)` returns the LLA at any fraction along the segment
65
+ - **Membership**: `includes?(point)` (vertex-only check), `contains?(point, tolerance:)` (on-segment check), `excludes?`
66
+ - **Intersection**: `intersects?(other_segment)` using cross-product orientation tests
67
+ - **Conversion**: `reverse`, `to_path`, `to_a`, `==`, `to_s`, `inspect`
68
+ - **`Geodetic::Areas::Triangle`** — polygon subclass with four construction modes
69
+ - Isosceles: `Triangle.new(center:, width:, height:, bearing:)`
70
+ - Equilateral by circumradius: `Triangle.new(center:, radius:, bearing:)`
71
+ - Equilateral by side length: `Triangle.new(center:, side:, bearing:)`
72
+ - Arbitrary vertices: `Triangle.new(vertices: [p1, p2, p3])`
73
+ - Predicates: `equilateral?`, `isosceles?`, `scalene?` based on actual side lengths (5m tolerance)
74
+ - Methods: `vertices`, `side_lengths`, `base`, `to_bounding_box`
75
+ - **`Geodetic::Areas::Rectangle`** — polygon subclass defined by a centerline Segment and perpendicular width
76
+ - `Rectangle.new(segment:, width:)` — accepts a Segment object or a two-element array of coordinates
77
+ - `width:` accepts numeric (meters) or a Distance instance
78
+ - Derived properties: `center`, `height`, `bearing` from the centerline; `corners`, `square?`, `to_bounding_box`
79
+ - **`Geodetic::Areas::Pentagon`**, **`Hexagon`**, **`Octagon`** — regular polygon subclasses from center + radius + bearing
80
+ - **Polygon self-intersection validation** — `Polygon.new` validates that no edge crosses another; pass `validate: false` to skip (used by subclasses with generated geometry)
81
+ - **Polygon `segments` method** — returns `Array<Segment>` for each edge; `edges` and `border` are aliases
82
+ - **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
83
+ - Documentation: `docs/reference/segment.md` (Segment reference with Great Circle Arcs section), updated `docs/reference/areas.md` with all polygon subclasses
84
+
85
+ ### Changed
86
+
87
+ - **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
88
+ - **Refactored `Polygon`** — `segments` is now the primary method (was `edges`); `edges` and `border` are aliases
89
+ - **Updated `Feature`** to support Segment as a geometry type via `centroid` (alias for `midpoint`)
90
+ - Updated README, `docs/index.md`, `docs/reference/areas.md`, `docs/reference/segment.md`, `examples/README.md`, and mkdocs nav
91
+
92
+ ### Removed
93
+
94
+ - `Areas::Rectangle = Areas::BoundingBox` alias — Rectangle is now its own class (Polygon subclass)
95
+
14
96
  ## [0.3.2] - 2026-03-09
15
97
 
16
98
  ### Added
@@ -19,11 +101,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
19
101
  - **Navigation**: `first`, `last`, `next`, `prev`, `segments`, `size`, `empty?`
20
102
  - **Membership**: `include?`/`includes?` (waypoint check), `contains?`/`inside?` (on-segment check with configurable tolerance)
21
103
  - **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
104
+ - **Closest points**: `closest_points_to` for Path-to-Path, Path-to-Polygon, Path-to-BoundingBox, and Path-to-Circle
23
105
  - **Computed**: `total_distance`, `segment_distances`, `segment_bearings`, `reverse`
24
106
  - **Subpath/split**: `between(from, to)` extracts a subpath; `split_at(coord)` divides into two paths sharing the split point
25
107
  - **Interpolation**: `at_distance(distance)` finds the coordinate at a given distance along the path
26
- - **Bounding box**: `bounds` returns an `Areas::Rectangle`
108
+ - **Bounding box**: `bounds` returns an `Areas::BoundingBox`
27
109
  - **Polygon conversion**: `to_polygon` closes the path (validates no self-intersection)
28
110
  - **Intersection**: `intersects?(other_path)` detects crossing segments
29
111
  - **Equality**: `==` compares coordinates in order
data/README.md CHANGED
@@ -18,9 +18,12 @@
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>
25
+ - <strong>Vectors</strong> - Geodetic displacement (distance + bearing) with full arithmetic and Vincenty direct<br>
26
+ - <strong>Geodetic Arithmetic</strong> - Compose geometry with operators: P1 + P2 → Segment, + P3 → Path, + Distance → Circle, * Vector → translate<br>
24
27
  - <strong>Validated Setters</strong> - Type coercion and range validation on all coordinate attributes<br>
25
28
  - <strong>Serialization</strong> - to_s(precision), to_a, from_string, from_array, DMS format<br>
26
29
  - <strong>Multiple Datums</strong> - WGS84, Clarke 1866, GRS 1980, Airy 1830, and more<br>
@@ -426,7 +429,7 @@ lla = gh36.to_lla
426
429
  gh36.neighbors # => { N: GH36, S: GH36, E: GH36, W: GH36, NE: ..., NW: ..., SE: ..., SW: ... }
427
430
 
428
431
  # Bounding rectangle of the geohash cell
429
- area = gh36.to_area # => Areas::Rectangle
432
+ area = gh36.to_area # => Areas::BoundingBox
430
433
  area.includes?(gh36.to_lla) # => true
431
434
 
432
435
  # Precision info
@@ -453,7 +456,7 @@ lla = gh.to_lla
453
456
  gh.neighbors # => { N: GH, S: GH, E: GH, W: GH, NE: ..., NW: ..., SE: ..., SW: ... }
454
457
 
455
458
  # Bounding rectangle of the geohash cell
456
- area = gh.to_area # => Areas::Rectangle
459
+ area = gh.to_area # => Areas::BoundingBox
457
460
  area.includes?(gh.to_lla) # => true
458
461
 
459
462
  # Precision info
@@ -480,7 +483,7 @@ lla = ham.to_lla
480
483
  ham.neighbors # => { N: HAM, S: HAM, E: HAM, W: HAM, NE: ..., NW: ..., SE: ..., SW: ... }
481
484
 
482
485
  # Bounding rectangle of the grid square
483
- area = ham.to_area # => Areas::Rectangle
486
+ area = ham.to_area # => Areas::BoundingBox
484
487
  area.includes?(ham.to_lla) # => true
485
488
 
486
489
  # Precision info
@@ -507,7 +510,7 @@ lla = olc.to_lla
507
510
  olc.neighbors # => { N: OLC, S: OLC, E: OLC, W: OLC, NE: ..., NW: ..., SE: ..., SW: ... }
508
511
 
509
512
  # Bounding rectangle of the plus code cell
510
- area = olc.to_area # => Areas::Rectangle
513
+ area = olc.to_area # => Areas::BoundingBox
511
514
  area.includes?(olc.to_lla) # => true
512
515
 
513
516
  # Precision info
@@ -532,16 +535,52 @@ points = [
532
535
  polygon = Areas::Polygon.new(boundary: points)
533
536
  polygon.centroid # => computed centroid as LLA
534
537
 
535
- # Rectangle area (accepts any coordinate type)
538
+ # BoundingBox area (accepts any coordinate type)
536
539
  nw = Coordinates::LLA.new(lat: 41.0, lng: -75.0)
537
540
  se = Coordinates::LLA.new(lat: 40.0, lng: -74.0)
538
- rect = Areas::Rectangle.new(nw: nw, se: se)
541
+ rect = Areas::BoundingBox.new(nw: nw, se: se)
539
542
  rect.centroid # => LLA at center
540
543
  rect.ne # => computed NE corner
541
544
  rect.sw # => computed SW corner
542
545
  rect.includes?(point) # => true/false
543
546
  ```
544
547
 
548
+ ### Segments
549
+
550
+ `Segment` represents a directed line segment between two points. It provides the geometric primitives that `Path` and `Polygon` build on.
551
+
552
+ ```ruby
553
+ a = Coordinate::LLA.new(lat: 40.7484, lng: -73.9857, alt: 0)
554
+ b = Coordinate::LLA.new(lat: 40.7580, lng: -73.9855, alt: 0)
555
+
556
+ seg = Segment.new(a, b)
557
+
558
+ # Properties (lazily computed, cached)
559
+ seg.length # => Distance
560
+ seg.distance # => Distance (alias for length)
561
+ seg.bearing # => Bearing
562
+ seg.midpoint # => LLA at halfway point
563
+
564
+ # Projection — closest point on segment to a target
565
+ foot, dist_m = seg.project(target_point)
566
+
567
+ # Interpolation — point at fraction along segment
568
+ seg.interpolate(0.25) # => LLA at quarter-way
569
+
570
+ # Membership
571
+ seg.includes?(a) # => true (vertex check only)
572
+ seg.includes?(seg.midpoint) # => false
573
+ seg.contains?(seg.midpoint) # => true (on-segment check)
574
+
575
+ # Intersection
576
+ seg.intersects?(other_seg) # => true/false
577
+
578
+ # Conversion
579
+ seg.reverse # => Segment with swapped endpoints
580
+ seg.to_path # => two-point Path
581
+ seg.to_a # => [start_point, end_point]
582
+ ```
583
+
545
584
  ### Paths
546
585
 
547
586
  `Path` is a directed, ordered sequence of unique coordinates representing routes, trails, or boundaries.
@@ -572,7 +611,7 @@ route.closest_points_to(other_path) # path-to-path
572
611
  sub = route.between(a, b) # extract subpath
573
612
  left, right = route.split_at(c) # split at waypoint
574
613
  route.at_distance(Distance.km(2)) # interpolate along path
575
- route.bounds # => Areas::Rectangle
614
+ route.bounds # => Areas::BoundingBox
576
615
  route.to_polygon # close into polygon
577
616
  route.intersects?(other_path) # crossing detection
578
617
  route.contains?(point) # on-segment check
@@ -612,6 +651,78 @@ park.distance_to(liberty).to_km # => "12.47 km"
612
651
 
613
652
  All three attributes (`label`, `geometry`, `metadata`) are mutable.
614
653
 
654
+ ### Vectors
655
+
656
+ `Vector` pairs a `Distance` (magnitude) with a `Bearing` (direction) to represent a geodetic displacement. It solves the Vincenty direct problem to compute destination points.
657
+
658
+ ```ruby
659
+ v = Geodetic::Vector.new(distance: 10_000, bearing: 90.0)
660
+ v = Geodetic::Vector.new(distance: Distance.km(10), bearing: Bearing.new(90))
661
+
662
+ v.north # => north component in meters
663
+ v.east # => east component in meters
664
+ v.magnitude # => distance in meters
665
+ v.reverse # => same distance, opposite bearing
666
+ v.normalize # => unit vector (1 meter)
667
+ ```
668
+
669
+ **Vector arithmetic:**
670
+
671
+ ```ruby
672
+ v1 + v2 # => Vector (component-wise addition)
673
+ v1 - v2 # => Vector (component-wise subtraction)
674
+ v * 3 # => Vector (scale distance)
675
+ v / 2 # => Vector (scale distance)
676
+ -v # => Vector (reverse bearing)
677
+ v.dot(v2) # => Float (dot product)
678
+ v.cross(v2) # => Float (2D cross product)
679
+ ```
680
+
681
+ **Factory methods:**
682
+
683
+ ```ruby
684
+ Vector.from_components(north: 1000, east: 500)
685
+ Vector.from_segment(segment)
686
+ segment.to_vector
687
+ ```
688
+
689
+ ### Geodetic Arithmetic
690
+
691
+ Operators build geometry from coordinates, vectors, and distances:
692
+
693
+ ```ruby
694
+ # Building geometry with +
695
+ p1 + p2 # => Segment
696
+ p1 + p2 + p3 # => Path
697
+ p1 + segment # => Path
698
+ segment + p3 # => Path
699
+ segment + segment # => Path
700
+ p1 + distance # => Circle
701
+ p1 + vector # => Segment (to destination)
702
+ segment + vector # => Path (extend from endpoint)
703
+ vector + segment # => Path (prepend via reverse)
704
+ path + vector # => Path (extend from last point)
705
+ vector + coordinate # => Segment
706
+ distance + coordinate # => Circle
707
+
708
+ # Translation with * or .translate
709
+ p1 * vector # => Coordinate (translated point)
710
+ segment * vector # => Segment (translated endpoints)
711
+ path * vector # => Path (translated waypoints)
712
+ circle * vector # => Circle (translated centroid)
713
+ polygon * vector # => Polygon (translated vertices)
714
+ ```
715
+
716
+ ### Corridors
717
+
718
+ Convert a path into a polygon corridor of a given width:
719
+
720
+ ```ruby
721
+ route = seattle + portland + sf
722
+ corridor = route.to_corridor(width: 1000) # 1km wide polygon
723
+ corridor = route.to_corridor(width: Distance.km(1))
724
+ ```
725
+
615
726
  ### Web Mercator Tile Coordinates
616
727
 
617
728
  ```ruby
@@ -642,6 +753,8 @@ The [`examples/`](examples/) directory contains runnable demo scripts showing pr
642
753
  | [`04_bearing_calculations.rb`](examples/04_bearing_calculations.rb) | Bearing class, compass directions, elevation angles, and chain bearings |
643
754
  | [`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) |
644
755
  | [`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 |
756
+ | [`07_segments_and_shapes.rb`](examples/07_segments_and_shapes.rb) | Segment and polygon subclasses: Triangle, Rectangle, Pentagon, Hexagon, Octagon with containment, edges, and bounding boxes |
757
+ | [`08_geodetic_arithmetic.rb`](examples/08_geodetic_arithmetic.rb) | Geodetic arithmetic: building geometry with + (Segments, Paths, Circles), Vector class (Vincenty direct, components, arithmetic, dot/cross products), translation with * (Coordinates, Segments, Paths, Circles, Polygons), and corridors |
645
758
 
646
759
  Run any example with:
647
760
 
@@ -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 |