geodetic 0.3.0 → 0.3.2

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -8
  3. data/README.md +108 -25
  4. data/docs/coordinate-systems/gars.md +0 -4
  5. data/docs/coordinate-systems/georef.md +0 -4
  6. data/docs/coordinate-systems/gh.md +0 -4
  7. data/docs/coordinate-systems/gh36.md +0 -4
  8. data/docs/coordinate-systems/h3.md +0 -4
  9. data/docs/coordinate-systems/ham.md +0 -4
  10. data/docs/coordinate-systems/index.md +2 -2
  11. data/docs/coordinate-systems/olc.md +0 -4
  12. data/docs/index.md +5 -0
  13. data/docs/reference/conversions.md +15 -15
  14. data/docs/reference/feature.md +117 -0
  15. data/docs/reference/map-rendering.md +32 -0
  16. data/docs/reference/path.md +269 -0
  17. data/docs/reference/serialization.md +4 -4
  18. data/examples/02_all_coordinate_systems.rb +0 -3
  19. data/examples/03_distance_calculations.rb +1 -0
  20. data/examples/04_bearing_calculations.rb +1 -0
  21. data/examples/05_map_rendering/.gitignore +2 -0
  22. data/examples/05_map_rendering/demo.rb +264 -0
  23. data/examples/05_map_rendering/icons/bridge.png +0 -0
  24. data/examples/05_map_rendering/icons/building.png +0 -0
  25. data/examples/05_map_rendering/icons/landmark.png +0 -0
  26. data/examples/05_map_rendering/icons/monument.png +0 -0
  27. data/examples/05_map_rendering/icons/park.png +0 -0
  28. data/examples/05_map_rendering/nyc_landmarks.png +0 -0
  29. data/examples/06_path_operations.rb +368 -0
  30. data/examples/README.md +85 -0
  31. data/fiddle_pointer_buffer_pool.md +119 -0
  32. data/lib/geodetic/coordinate/bng.rb +14 -33
  33. data/lib/geodetic/coordinate/ecef.rb +5 -1
  34. data/lib/geodetic/coordinate/enu.rb +4 -0
  35. data/lib/geodetic/coordinate/gars.rb +2 -3
  36. data/lib/geodetic/coordinate/georef.rb +2 -3
  37. data/lib/geodetic/coordinate/gh.rb +2 -4
  38. data/lib/geodetic/coordinate/gh36.rb +4 -5
  39. data/lib/geodetic/coordinate/h3.rb +2 -3
  40. data/lib/geodetic/coordinate/ham.rb +2 -3
  41. data/lib/geodetic/coordinate/lla.rb +7 -1
  42. data/lib/geodetic/coordinate/mgrs.rb +1 -1
  43. data/lib/geodetic/coordinate/ned.rb +4 -0
  44. data/lib/geodetic/coordinate/olc.rb +0 -1
  45. data/lib/geodetic/coordinate/spatial_hash.rb +2 -2
  46. data/lib/geodetic/coordinate/ups.rb +1 -1
  47. data/lib/geodetic/coordinate/usng.rb +1 -1
  48. data/lib/geodetic/coordinate/utm.rb +1 -1
  49. data/lib/geodetic/coordinate/web_mercator.rb +1 -1
  50. data/lib/geodetic/coordinate.rb +30 -26
  51. data/lib/geodetic/feature.rb +52 -0
  52. data/lib/geodetic/geoid_height.rb +11 -6
  53. data/lib/geodetic/path.rb +599 -0
  54. data/lib/geodetic/version.rb +1 -1
  55. data/lib/geodetic.rb +2 -0
  56. data/mkdocs.yml +3 -0
  57. metadata +17 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d81eb7c69d9aff7d512562f54e2413c91c8ff10b2361c9143a8afd0c1af0ca0
4
- data.tar.gz: e7fb09dd1f7d4d97f57ea48f9cacb0e6afbd30abedab1aa04cbf1153c2fe4373
3
+ metadata.gz: f295df82e85611a9a7a7dd56593ae029d6ad096cc5c96021d5b59b4f4b001cc2
4
+ data.tar.gz: 6da1bc3461fd4f372b53f5458d322f8c14d1feb8a2afa93a2375cb5641674674
5
5
  SHA512:
6
- metadata.gz: 90a2e1b8bbc1c6bdf59c7e326d4965f8ec85a16682bccba948fd1f104d7818a5fa34ee179fa00de0221cbfd0e9d9afbd5c667aac46c1a6c9c070b338618b461a
7
- data.tar.gz: 85b40d6e961ec4782a9748232eed4191df1dd8a5f22df77afe68c63d64b6b23df0e8ff5ec4ae927038d60919a7825e09c2429bbd4aa8d4447b1737ff81aa4dfd
6
+ metadata.gz: 1a0b72935abf28bbcb41182f800d1bdb52466530565f13d75fec5f3eec14c0dac7680f8220f09c51cf4d60ed3b87fc53a3675ceb972f1cec17ca06cbd096d717
7
+ data.tar.gz: 754e5b64d8e1d333dafd270a6e21434d46102ea57e43fbba16b83bcecd9eb69eb4e834a51554c10eb59433f6d434930bf2664fca0d9c22b37cf7378e90cc106e
data/CHANGELOG.md CHANGED
@@ -11,21 +11,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
  ## [Unreleased]
12
12
 
13
13
 
14
- ## [0.2.0] - 2026-03-08
14
+ ## [0.3.2] - 2026-03-09
15
15
 
16
16
  ### Added
17
17
 
18
- - **3 new coordinate systems** bringing the total from 15 to 18:
19
- - `Geodetic::Coordinate::GEOREF` World Geographic Reference System (aviation/military geocode with variable precision from 15-degree tiles to 0.001-minute resolution)
20
- - `Geodetic::Coordinate::GARS` Global Area Reference System (NGA standard with 30-minute cells, 15-minute quadrants, and 5-minute keypads)
21
- - `Geodetic::Coordinate::H3` Uber's H3 Hexagonal Hierarchical Index (16 resolution levels, hexagonal cells via `libh3` C library through `fiddle`)
22
- - Full cross-system conversions for GEOREF, GARS, and H3 — all 18 coordinate systems convert to/from every other system (324 conversion paths)
23
- - Spatial hash features for GEOREF, GARS, and H3: `neighbors`, `to_area`, `precision_in_meters`, `to_slug`, configurable precision
18
+ - **`Geodetic::Path` class** directed, ordered sequence of unique coordinates for modeling routes, trails, and boundaries
19
+ - **Navigation**: `first`, `last`, `next`, `prev`, `segments`, `size`, `empty?`
20
+ - **Membership**: `include?`/`includes?` (waypoint check), `contains?`/`inside?` (on-segment check with configurable tolerance)
21
+ - **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
23
+ - **Computed**: `total_distance`, `segment_distances`, `segment_bearings`, `reverse`
24
+ - **Subpath/split**: `between(from, to)` extracts a subpath; `split_at(coord)` divides into two paths sharing the split point
25
+ - **Interpolation**: `at_distance(distance)` finds the coordinate at a given distance along the path
26
+ - **Bounding box**: `bounds` returns an `Areas::Rectangle`
27
+ - **Polygon conversion**: `to_polygon` closes the path (validates no self-intersection)
28
+ - **Intersection**: `intersects?(other_path)` detects crossing segments
29
+ - **Equality**: `==` compares coordinates in order
30
+ - **Enumerable**: includes `Enumerable` via `each` — supports `map`, `select`, `any?`, `to_a`, etc.
31
+ - **Non-mutating operators**: `+` and `-` accept both coordinates and paths
32
+ - **Mutating operators**: `<<`, `>>`, `prepend`, `insert(after:/before:)`, `delete`/`remove` — all accept paths as well as coordinates
33
+ - **Path operations example** (`examples/06_path_operations.rb`) — 19-section demo covering all Path capabilities with a Manhattan walking route
34
+ - Documentation: `docs/reference/path.md` (Path reference)
35
+
36
+ ### Changed
37
+
38
+ - Updated `Geodetic::Feature` to support Path as a geometry type — delegates `distance_to` and `bearing_to` using geometric projection
39
+ - Updated README, `docs/index.md`, `docs/reference/feature.md`, `examples/README.md`, and mkdocs nav to include Path class
40
+
41
+ ## [0.3.1] - 2026-03-09
42
+
43
+ ### Added
44
+
45
+ - **`Geodetic::Feature` class** — wraps any coordinate or area geometry with a `label` and `metadata` hash; delegates `distance_to` and `bearing_to` to the underlying geometry, using the centroid for area geometries
46
+ - **Map rendering example** (`examples/05_map_rendering/`) — renders NYC landmarks on a raster map using [libgd-gis](https://rubygems.org/gems/libgd-gis), demonstrating Feature objects, polygon overlays, bearing arrows, icon compositing, and light/dark theme support
47
+ - `examples/README.md` describing all five example scripts
48
+ - Documentation: `docs/reference/feature.md` (Feature reference) and `docs/reference/map-rendering.md` (libgd-gis integration guide)
49
+
50
+ ### Changed
51
+
52
+ - Updated README, `docs/index.md`, and mkdocs nav to include Feature class and map rendering example
53
+
54
+ ## [0.3.0] - 2026-03-08
55
+
56
+ ### Added
57
+
58
+ - **H3 Hexagonal Hierarchical Index** (`Geodetic::Coordinate::H3`) — Uber's spatial indexing system, bringing total to 18 coordinate systems (324 conversion paths)
59
+ - H3 uses Ruby's `fiddle` to call the H3 v4 C API directly — no gem dependency beyond `fiddle`
24
60
  - H3-specific features: `grid_disk(k)`, `parent(res)`, `children(res)`, `pentagon?`, `cell_area`, `h3_index`, `resolution` (0-15)
25
61
  - H3 `to_area` returns `Areas::Polygon` (6 vertices for hexagons, 5 for pentagons) instead of `Areas::Rectangle`
26
62
  - H3 `neighbors` returns Array of 6 cells instead of directional Hash with 8 cardinal keys
27
63
  - Graceful degradation: H3 raises clear error with installation instructions if `libh3` is not found; all other coordinate systems work normally
28
- - Documentation pages: `docs/coordinate-systems/georef.md`, `docs/coordinate-systems/gars.md`, and `docs/coordinate-systems/h3.md`
64
+ - `H3.available?` class method to check for libh3 at runtime
65
+ - Documentation page: `docs/coordinate-systems/h3.md`
66
+
67
+ ### Changed
68
+
69
+ - Updated all documentation to reflect 18 coordinate systems (README, docs, gemspec, CLAUDE.md)
70
+
71
+ ## [0.2.0] - 2026-03-08
72
+
73
+ ### Added
74
+
75
+ - **2 new coordinate systems** bringing the total from 15 to 17:
76
+ - `Geodetic::Coordinate::GEOREF` — World Geographic Reference System (aviation/military geocode with variable precision from 15-degree tiles to 0.001-minute resolution)
77
+ - `Geodetic::Coordinate::GARS` — Global Area Reference System (NGA standard with 30-minute cells, 15-minute quadrants, and 5-minute keypads)
78
+ - Full cross-system conversions for GEOREF and GARS — all 17 coordinate systems convert to/from every other system (289 conversion paths)
79
+ - Spatial hash features for GEOREF and GARS: `neighbors`, `to_area`, `precision_in_meters`, `to_slug`, configurable precision
80
+ - Documentation pages: `docs/coordinate-systems/georef.md` and `docs/coordinate-systems/gars.md`
29
81
 
30
82
  ### Changed
31
83
 
data/README.md CHANGED
@@ -19,6 +19,8 @@
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
21
  - <strong>Geographic Areas</strong> - Circle, Polygon, and Rectangle with point-in-area tests<br>
22
+ - <strong>Paths</strong> - Directed coordinate sequences with navigation, interpolation, closest approach, intersection, and area conversion<br>
23
+ - <strong>Features</strong> - Named geometry wrapper with metadata and delegated distance/bearing<br>
22
24
  - <strong>Validated Setters</strong> - Type coercion and range validation on all coordinate attributes<br>
23
25
  - <strong>Serialization</strong> - to_s(precision), to_a, from_string, from_array, DMS format<br>
24
26
  - <strong>Multiple Datums</strong> - WGS84, Clarke 1866, GRS 1980, Airy 1830, and more<br>
@@ -77,14 +79,29 @@ ned = Coordinates::NED.new(n: 200.0, e: 100.0, d: -50.0)
77
79
 
78
80
  ### GCS Shorthand
79
81
 
80
- `GCS` is a top-level alias for `Geodetic::Coordinate`, providing a concise way to create and work with coordinates:
82
+ For convenience, you can define a short alias in your application:
81
83
 
82
84
  ```ruby
83
85
  require "geodetic"
84
86
 
85
- # Use GCS as a shorthand for Geodetic::Coordinate
86
- seattle = GCS::LLA.new(lat: 47.6205, lng: -122.3493, alt: 184.0)
87
- ecef = GCS::ECEF.new(x: -2304643.57, y: -3638650.07, z: 4688674.43)
87
+ GCS = Geodetic::Coordinate
88
+
89
+ seattle = Geodetic::Coordinate::LLA.new(lat: 47.6205, lng: -122.3493, alt: 184.0)
90
+ ecef = Geodetic::Coordinate::ECEF.new(x: -2304643.57, y: -3638650.07, z: 4688674.43)
91
+ ```
92
+
93
+ ### Discovering Coordinate Systems
94
+
95
+ List all available coordinate systems at runtime:
96
+
97
+ ```ruby
98
+ Geodetic::Coordinate.systems
99
+ # => [Geodetic::Coordinate::LLA, Geodetic::Coordinate::ECEF, Geodetic::Coordinate::UTM, ...]
100
+
101
+ # Get short names
102
+ Geodetic::Coordinate.systems.map { |c| c.name.split('::').last }
103
+ # => ["LLA", "ECEF", "UTM", "ENU", "NED", "MGRS", "USNG", "WebMercator",
104
+ # "UPS", "StatePlane", "BNG", "GH36", "GH", "HAM", "OLC", "GEOREF", "GARS", "H3"]
88
105
  ```
89
106
 
90
107
  ### Coordinate Conversions
@@ -185,9 +202,9 @@ Universal distance methods work across all coordinate types and return `Distance
185
202
  **Instance method `distance_to`** — Vincenty great-circle distance:
186
203
 
187
204
  ```ruby
188
- seattle = GCS::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
189
- portland = GCS::LLA.new(lat: 45.5152, lng: -122.6784, alt: 0.0)
190
- sf = GCS::LLA.new(lat: 37.7749, lng: -122.4194, alt: 0.0)
205
+ seattle = Geodetic::Coordinate::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
206
+ portland = Geodetic::Coordinate::LLA.new(lat: 45.5152, lng: -122.6784, alt: 0.0)
207
+ sf = Geodetic::Coordinate::LLA.new(lat: 37.7749, lng: -122.4194, alt: 0.0)
191
208
 
192
209
  d = seattle.distance_to(portland) # => Distance (meters)
193
210
  d.meters # => 235393.17
@@ -201,23 +218,23 @@ seattle.distance_to([portland, sf]) # => [Distance, Distance] (radial)
201
218
  **Class method `distance_between`** — consecutive chain distances:
202
219
 
203
220
  ```ruby
204
- GCS.distance_between(seattle, portland) # => Distance
205
- GCS.distance_between(seattle, portland, sf) # => [Distance, Distance] (chain)
206
- GCS.distance_between([seattle, portland, sf]) # => [Distance, Distance] (chain)
221
+ Geodetic::Coordinate.distance_between(seattle, portland) # => Distance
222
+ Geodetic::Coordinate.distance_between(seattle, portland, sf) # => [Distance, Distance] (chain)
223
+ Geodetic::Coordinate.distance_between([seattle, portland, sf]) # => [Distance, Distance] (chain)
207
224
  ```
208
225
 
209
226
  **Straight-line (ECEF Euclidean) versions:**
210
227
 
211
228
  ```ruby
212
229
  seattle.straight_line_distance_to(portland) # => Distance
213
- GCS.straight_line_distance_between(seattle, portland) # => Distance
230
+ Geodetic::Coordinate.straight_line_distance_between(seattle, portland) # => Distance
214
231
  ```
215
232
 
216
233
  **Cross-system distances** — works between any coordinate types:
217
234
 
218
235
  ```ruby
219
236
  utm = seattle.to_utm
220
- mgrs = GCS::MGRS.from_lla(portland)
237
+ mgrs = Geodetic::Coordinate::MGRS.from_lla(portland)
221
238
  utm.distance_to(mgrs) # => Distance
222
239
  ```
223
240
 
@@ -230,8 +247,8 @@ Universal bearing methods work across all coordinate types and return `Bearing`
230
247
  **Instance method `bearing_to`** — great-circle forward azimuth:
231
248
 
232
249
  ```ruby
233
- seattle = GCS::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
234
- portland = GCS::LLA.new(lat: 45.5152, lng: -122.6784, alt: 0.0)
250
+ seattle = Geodetic::Coordinate::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
251
+ portland = Geodetic::Coordinate::LLA.new(lat: 45.5152, lng: -122.6784, alt: 0.0)
235
252
 
236
253
  b = seattle.bearing_to(portland) # => Bearing
237
254
  b.degrees # => 188.2
@@ -245,8 +262,8 @@ b.to_s # => "188.2036°"
245
262
  **Instance method `elevation_to`** — vertical look angle:
246
263
 
247
264
  ```ruby
248
- a = GCS::LLA.new(lat: 47.62, lng: -122.35, alt: 0.0)
249
- b = GCS::LLA.new(lat: 47.62, lng: -122.35, alt: 5000.0)
265
+ a = Geodetic::Coordinate::LLA.new(lat: 47.62, lng: -122.35, alt: 0.0)
266
+ b = Geodetic::Coordinate::LLA.new(lat: 47.62, lng: -122.35, alt: 5000.0)
250
267
 
251
268
  a.elevation_to(b) # => 89.9... (degrees, nearly straight up)
252
269
  ```
@@ -254,15 +271,15 @@ a.elevation_to(b) # => 89.9... (degrees, nearly straight up)
254
271
  **Class method `bearing_between`** — consecutive chain bearings:
255
272
 
256
273
  ```ruby
257
- GCS.bearing_between(seattle, portland) # => Bearing
258
- GCS.bearing_between(seattle, portland, sf) # => [Bearing, Bearing] (chain)
274
+ Geodetic::Coordinate.bearing_between(seattle, portland) # => Bearing
275
+ Geodetic::Coordinate.bearing_between(seattle, portland, sf) # => [Bearing, Bearing] (chain)
259
276
  ```
260
277
 
261
278
  **Cross-system bearings** — works between any coordinate types:
262
279
 
263
280
  ```ruby
264
281
  utm = seattle.to_utm
265
- mgrs = GCS::MGRS.from_lla(portland)
282
+ mgrs = Geodetic::Coordinate::MGRS.from_lla(portland)
266
283
  utm.bearing_to(mgrs) # => Bearing
267
284
  ```
268
285
 
@@ -405,9 +422,6 @@ gh36 = lla.to_gh36(precision: 8)
405
422
  # Decode back to LLA
406
423
  lla = gh36.to_lla
407
424
 
408
- # URL slug (the hash itself is URL-safe)
409
- gh36.to_slug # => "bdrdC26BqH"
410
-
411
425
  # Neighbor cells
412
426
  gh36.neighbors # => { N: GH36, S: GH36, E: GH36, W: GH36, NE: ..., NW: ..., SE: ..., SW: ... }
413
427
 
@@ -435,9 +449,6 @@ gh = lla.to_gh(precision: 8)
435
449
  # Decode back to LLA
436
450
  lla = gh.to_lla
437
451
 
438
- # URL slug (the hash itself is URL-safe)
439
- gh.to_slug # => "dr5ru7"
440
-
441
452
  # Neighbor cells
442
453
  gh.neighbors # => { N: GH, S: GH, E: GH, W: GH, NE: ..., NW: ..., SE: ..., SW: ... }
443
454
 
@@ -531,6 +542,76 @@ rect.sw # => computed SW corner
531
542
  rect.includes?(point) # => true/false
532
543
  ```
533
544
 
545
+ ### Paths
546
+
547
+ `Path` is a directed, ordered sequence of unique coordinates representing routes, trails, or boundaries.
548
+
549
+ ```ruby
550
+ route = Path.new(coordinates: [battery_park, wall_street, brooklyn_bridge, city_hall])
551
+
552
+ # Navigation
553
+ route.first # => starting waypoint
554
+ route.next(wall_street) # => brooklyn_bridge
555
+ route.total_distance.to_km # => "3.42 km"
556
+
557
+ # Build incrementally
558
+ trail = Path.new
559
+ trail << start << middle << finish
560
+ trail >> new_start # prepend
561
+
562
+ # Combine paths
563
+ combined = downtown + uptown # concatenate
564
+ trimmed = combined - detour # remove coordinates
565
+
566
+ # Closest approach (geometric projection, not just waypoints)
567
+ route.closest_coordinate_to(off_path_point)
568
+ route.distance_to(target)
569
+ route.closest_points_to(other_path) # path-to-path
570
+
571
+ # Spatial operations
572
+ sub = route.between(a, b) # extract subpath
573
+ left, right = route.split_at(c) # split at waypoint
574
+ route.at_distance(Distance.km(2)) # interpolate along path
575
+ route.bounds # => Areas::Rectangle
576
+ route.to_polygon # close into polygon
577
+ route.intersects?(other_path) # crossing detection
578
+ route.contains?(point) # on-segment check
579
+
580
+ # Enumerable
581
+ route.map { |c| c.lat }
582
+ route.select { |c| c.lat > 40.72 }
583
+ ```
584
+
585
+ ### Features
586
+
587
+ `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.
588
+
589
+ ```ruby
590
+ liberty = Feature.new(
591
+ label: "Statue of Liberty",
592
+ geometry: Coordinates::LLA.new(lat: 40.6892, lng: -74.0445, alt: 0),
593
+ metadata: { category: "monument", year: 1886 }
594
+ )
595
+
596
+ empire = Feature.new(
597
+ label: "Empire State Building",
598
+ geometry: Coordinates::LLA.new(lat: 40.7484, lng: -73.9857, alt: 0),
599
+ metadata: { category: "building", floors: 102 }
600
+ )
601
+
602
+ liberty.distance_to(empire).to_km # => "8.24 km"
603
+ liberty.bearing_to(empire).degrees # => 36.99
604
+
605
+ # Area geometries use the centroid for distance/bearing
606
+ park = Feature.new(
607
+ label: "Central Park",
608
+ geometry: Areas::Polygon.new(boundary: [...])
609
+ )
610
+ park.distance_to(liberty).to_km # => "12.47 km"
611
+ ```
612
+
613
+ All three attributes (`label`, `geometry`, `metadata`) are mutable.
614
+
534
615
  ### Web Mercator Tile Coordinates
535
616
 
536
617
  ```ruby
@@ -559,6 +640,8 @@ The [`examples/`](examples/) directory contains runnable demo scripts showing pr
559
640
  | [`02_all_coordinate_systems.rb`](examples/02_all_coordinate_systems.rb) | All 18 coordinate systems, cross-system chains, and areas |
560
641
  | [`03_distance_calculations.rb`](examples/03_distance_calculations.rb) | Distance class features, unit conversions, and arithmetic |
561
642
  | [`04_bearing_calculations.rb`](examples/04_bearing_calculations.rb) | Bearing class, compass directions, elevation angles, and chain bearings |
643
+ | [`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
+ | [`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 |
562
645
 
563
646
  Run any example with:
564
647
 
@@ -141,10 +141,6 @@ coord.to_s(6) # => "006AG3"
141
141
  coord.to_s(5) # => "006AG"
142
142
  ```
143
143
 
144
- ### `to_slug`
145
-
146
- Alias for `to_s`. GARS codes are already URL-safe.
147
-
148
144
  ### `to_a`
149
145
 
150
146
  Returns `[lat, lng]` of the cell midpoint.
@@ -122,10 +122,6 @@ coord.to_s(4) # => "GJPJ"
122
122
  coord.to_s(2) # => "GJ"
123
123
  ```
124
124
 
125
- ### `to_slug`
126
-
127
- Alias for `to_s`. GEOREF codes are already URL-safe.
128
-
129
125
  ### `to_a`
130
126
 
131
127
  Returns `[lat, lng]` of the cell midpoint.
@@ -110,10 +110,6 @@ coord.to_s # => "dr5ru7c5g200"
110
110
  coord.to_s(6) # => "dr5ru7"
111
111
  ```
112
112
 
113
- ### `to_slug`
114
-
115
- Alias for `to_s`. The geohash is already URL-safe.
116
-
117
113
  ### `to_a`
118
114
 
119
115
  Returns `[lat, lng]` of the cell midpoint.
@@ -105,10 +105,6 @@ coord.to_s # => "bdrdC26BqH"
105
105
  coord.to_s(5) # => "bdrdC"
106
106
  ```
107
107
 
108
- ### `to_slug`
109
-
110
- Alias for `to_s`. The geohash is already URL-safe.
111
-
112
108
  ### `to_a`
113
109
 
114
110
  Returns `[lat, lng]` of the cell midpoint.
@@ -174,10 +174,6 @@ coord.to_s(:integer) # => 608693941536498687
174
174
  coord.h3_index # => 608693941536498687
175
175
  ```
176
176
 
177
- ### `to_slug`
178
-
179
- Alias for `to_s`. H3 hex strings are already URL-safe.
180
-
181
177
  ### `to_a`
182
178
 
183
179
  Returns `[lat, lng]` of the cell centroid.
@@ -118,10 +118,6 @@ coord.to_s(6) # => "FN31pr"
118
118
  coord.to_s(4) # => "FN31"
119
119
  ```
120
120
 
121
- ### `to_slug`
122
-
123
- Alias for `to_s`. The locator is already URL-safe.
124
-
125
121
  ### `to_a`
126
122
 
127
123
  Returns `[lat, lng]` of the cell midpoint.
@@ -84,9 +84,9 @@ Every coordinate system can convert to every other coordinate system. The table
84
84
 
85
85
  ## Universal Distance and Bearing Calculations
86
86
 
87
- All coordinate systems support universal distance calculations via `distance_to` (Vincenty great-circle) and `straight_line_distance_to` (ECEF Euclidean). These methods work across different coordinate types -- for example, computing the distance from a UTM coordinate to an MGRS coordinate. Class-level methods `GCS.distance_between` and `GCS.straight_line_distance_between` compute consecutive chain distances across a sequence of coordinates.
87
+ All coordinate systems support universal distance calculations via `distance_to` (Vincenty great-circle) and `straight_line_distance_to` (ECEF Euclidean). These methods work across different coordinate types -- for example, computing the distance from a UTM coordinate to an MGRS coordinate. Class-level methods `Geodetic::Coordinate.distance_between` and `Geodetic::Coordinate.straight_line_distance_between` compute consecutive chain distances across a sequence of coordinates.
88
88
 
89
- All coordinate systems also support universal bearing calculations via `bearing_to` (great-circle forward azimuth) and `elevation_to` (vertical look angle). These return `Bearing` and `Float` objects respectively. The class-level method `GCS.bearing_between` computes consecutive chain bearings.
89
+ All coordinate systems also support universal bearing calculations via `bearing_to` (great-circle forward azimuth) and `elevation_to` (vertical look angle). These return `Bearing` and `Float` objects respectively. The class-level method `Geodetic::Coordinate.bearing_between` computes consecutive chain bearings.
90
90
 
91
91
  See the [Conversions Reference](../reference/conversions.md#distance-calculations) for details on distances and [Bearing Calculations](../reference/conversions.md#bearing-calculations) for bearings.
92
92
 
@@ -123,10 +123,6 @@ coord.to_s(8) # => "849VCWC8+"
123
123
  coord.to_s(4) # => "84900000+"
124
124
  ```
125
125
 
126
- ### `to_slug`
127
-
128
- Alias for `to_s`. Plus codes are already URL-safe.
129
-
130
126
  ### `to_a`
131
127
 
132
128
  Returns `[lat, lng]` of the cell midpoint.
data/docs/index.md CHANGED
@@ -15,6 +15,8 @@
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
17
  <li><strong>Geographic Areas</strong> - Circle, Polygon, and Rectangle with point-in-area tests<br>
18
+ <li><strong>Paths</strong> - Directed coordinate sequences with navigation, interpolation, closest approach, intersection, and area conversion<br>
19
+ <li><strong>Features</strong> - Named geometry wrapper with metadata and delegated distance/bearing<br>
18
20
  <li><strong>Validated Setters</strong> - Type coercion and range validation on all coordinate attributes<br>
19
21
  <li><strong>Serialization</strong> - to_s(precision), to_a, from_string, from_array, DMS format<br>
20
22
  <li><strong>Multiple Datums</strong> - WGS84, Clarke 1866, GRS 1980, Airy 1830, and more<br>
@@ -56,12 +58,15 @@ Geodetic supports full bidirectional conversion between all 18 coordinate system
56
58
  - **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.
57
59
  - **Geoid height calculations** -- Convert between ellipsoidal and orthometric heights using models such as EGM96, EGM2008, GEOID18, and GEOID12B.
58
60
  - **Geographic areas** -- `Geodetic::Areas::Circle`, `Geodetic::Areas::Polygon`, and `Geodetic::Areas::Rectangle` for point-in-area testing.
61
+ - **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
+ - **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.
59
63
 
60
64
  ## Design Principles
61
65
 
62
66
  - All constructors use **keyword arguments** for clarity.
63
67
  - Every coordinate system supports **serialization** via `to_s` and `to_a`, and **deserialization** via `from_string` and `from_array`.
64
68
  - Conversions are available as instance methods (`to_ecef`, `to_utm`, etc.) and class-level factory methods (`from_ecef`, `from_utm`, etc.).
69
+ - All registered coordinate systems are discoverable at runtime via `Geodetic::Coordinate.systems`.
65
70
 
66
71
  ## Quick Example
67
72
 
@@ -240,31 +240,31 @@ Universal distance methods are available on all coordinate types and work across
240
240
  ### Great-Circle Distance (Vincenty)
241
241
 
242
242
  - **`distance_to(other, *others)`** — Instance method. Computes the Vincenty great-circle distance from the receiver to one or more target coordinates. Returns a `Distance` for a single target, or an Array of `Distance` objects for multiple targets (radial distances from the receiver).
243
- - **`GCS.distance_between(*coords)`** — Class method on `Geodetic::Coordinate` (aliased as `GCS`). Computes consecutive chain distances between an ordered sequence of coordinates. Returns a `Distance` for two coordinates, or an Array of `Distance` objects for three or more.
243
+ - **`Geodetic::Coordinate.distance_between(*coords)`** — Class method on `Geodetic::Coordinate`. Computes consecutive chain distances between an ordered sequence of coordinates. Returns a `Distance` for two coordinates, or an Array of `Distance` objects for three or more.
244
244
 
245
245
  > **`Distance` objects** wrap a distance value and provide unit-aware access. Call `.meters` to get the raw Float value in meters, or `.to_f` to get the value in the current display unit.
246
246
 
247
247
  ```ruby
248
- seattle = GCS::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
249
- portland = GCS::LLA.new(lat: 45.5152, lng: -122.6784, alt: 0.0)
250
- sf = GCS::LLA.new(lat: 37.7749, lng: -122.4194, alt: 0.0)
248
+ seattle = Geodetic::Coordinate::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
249
+ portland = Geodetic::Coordinate::LLA.new(lat: 45.5152, lng: -122.6784, alt: 0.0)
250
+ sf = Geodetic::Coordinate::LLA.new(lat: 37.7749, lng: -122.4194, alt: 0.0)
251
251
 
252
252
  # Radial distances from receiver
253
253
  seattle.distance_to(portland) # => Distance (235393.17 m)
254
254
  seattle.distance_to(portland, sf) # => [Distance, Distance] (Array)
255
255
 
256
256
  # Consecutive chain distances
257
- GCS.distance_between(seattle, portland, sf) # => [Distance, Distance] (Array)
257
+ Geodetic::Coordinate.distance_between(seattle, portland, sf) # => [Distance, Distance] (Array)
258
258
  ```
259
259
 
260
260
  ### Straight-Line Distance (ECEF Euclidean)
261
261
 
262
262
  - **`straight_line_distance_to(other, *others)`** — Instance method. Computes the Euclidean distance in ECEF (3D Cartesian) space. Returns a `Distance` for a single target, or an Array of `Distance` objects for multiple targets.
263
- - **`GCS.straight_line_distance_between(*coords)`** — Class method. Computes consecutive chain Euclidean distances.
263
+ - **`Geodetic::Coordinate.straight_line_distance_between(*coords)`** — Class method. Computes consecutive chain Euclidean distances.
264
264
 
265
265
  ```ruby
266
266
  seattle.straight_line_distance_to(portland) # => Distance
267
- GCS.straight_line_distance_between(seattle, portland) # => Distance
267
+ Geodetic::Coordinate.straight_line_distance_between(seattle, portland) # => Distance
268
268
  ```
269
269
 
270
270
  ### Cross-System Distances
@@ -273,7 +273,7 @@ Both `distance_to` and `straight_line_distance_to` accept any coordinate type. C
273
273
 
274
274
  ```ruby
275
275
  utm = seattle.to_utm
276
- mgrs = GCS::MGRS.from_lla(portland)
276
+ mgrs = Geodetic::Coordinate::MGRS.from_lla(portland)
277
277
  utm.distance_to(mgrs) # => Distance (235393.17 m)
278
278
  ```
279
279
 
@@ -282,7 +282,7 @@ utm.distance_to(mgrs) # => Distance (235393.17 m)
282
282
  ENU and NED are relative coordinate systems and do not support `distance_to` or `straight_line_distance_to` directly. Convert to an absolute system first:
283
283
 
284
284
  ```ruby
285
- ref = GCS::LLA.new(lat: 47.62, lng: -122.35, alt: 0.0)
285
+ ref = Geodetic::Coordinate::LLA.new(lat: 47.62, lng: -122.35, alt: 0.0)
286
286
  lla = enu.to_lla(ref)
287
287
  lla.distance_to(other_lla)
288
288
  ```
@@ -299,12 +299,12 @@ Universal bearing methods are available on all coordinate types and work across
299
299
 
300
300
  - **`bearing_to(other)`** — Instance method. Computes the great-circle forward azimuth from the receiver to the target coordinate. Returns a `Bearing` object.
301
301
  - **`elevation_to(other)`** — Instance method. Computes the vertical look angle (elevation) from the receiver to the target. Returns a Float in degrees (-90 to +90).
302
- - **`GCS.bearing_between(*coords)`** — Class method on `Geodetic::Coordinate` (aliased as `GCS`). Computes consecutive chain bearings between an ordered sequence of coordinates. Returns a `Bearing` for two coordinates, or an Array of `Bearing` objects for three or more.
302
+ - **`Geodetic::Coordinate.bearing_between(*coords)`** — Class method on `Geodetic::Coordinate`. Computes consecutive chain bearings between an ordered sequence of coordinates. Returns a `Bearing` for two coordinates, or an Array of `Bearing` objects for three or more.
303
303
 
304
304
  ```ruby
305
- seattle = GCS::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
306
- portland = GCS::LLA.new(lat: 45.5152, lng: -122.6784, alt: 0.0)
307
- sf = GCS::LLA.new(lat: 37.7749, lng: -122.4194, alt: 0.0)
305
+ seattle = Geodetic::Coordinate::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
306
+ portland = Geodetic::Coordinate::LLA.new(lat: 45.5152, lng: -122.6784, alt: 0.0)
307
+ sf = Geodetic::Coordinate::LLA.new(lat: 37.7749, lng: -122.4194, alt: 0.0)
308
308
 
309
309
  # Forward azimuth
310
310
  b = seattle.bearing_to(portland) # => Bearing
@@ -316,7 +316,7 @@ b.reverse # => Bearing (back azimuth)
316
316
  seattle.elevation_to(portland) # => Float (degrees)
317
317
 
318
318
  # Consecutive chain bearings
319
- GCS.bearing_between(seattle, portland, sf) # => [Bearing, Bearing]
319
+ Geodetic::Coordinate.bearing_between(seattle, portland, sf) # => [Bearing, Bearing]
320
320
  ```
321
321
 
322
322
  ### Cross-System Bearings
@@ -325,7 +325,7 @@ GCS.bearing_between(seattle, portland, sf) # => [Bearing, Bearing]
325
325
 
326
326
  ```ruby
327
327
  utm = seattle.to_utm
328
- mgrs = GCS::MGRS.from_lla(portland)
328
+ mgrs = Geodetic::Coordinate::MGRS.from_lla(portland)
329
329
  utm.bearing_to(mgrs) # => Bearing
330
330
  ```
331
331
 
@@ -0,0 +1,117 @@
1
+ # Feature Reference
2
+
3
+ `Geodetic::Feature` wraps a geometry with a human-readable label and an arbitrary metadata hash. It provides a single object that ties spatial data to application-level information like names, categories, and display properties.
4
+
5
+ ---
6
+
7
+ ## Constructor
8
+
9
+ ```ruby
10
+ Feature.new(
11
+ label: "Statue of Liberty",
12
+ geometry: Geodetic::Coordinate::LLA.new(lat: 40.6892, lng: -74.0445, alt: 0),
13
+ metadata: { category: "monument", year: 1886 }
14
+ )
15
+ ```
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 `{}`.
18
+
19
+ ---
20
+
21
+ ## Attributes
22
+
23
+ | Attribute | Type | Mutable | Description |
24
+ |------------|--------|---------|-------------|
25
+ | `label` | String | Yes | A display name for the feature |
26
+ | `geometry` | Object | Yes | A coordinate or area object |
27
+ | `metadata` | Hash | Yes | Arbitrary key-value pairs |
28
+
29
+ All three attributes have both reader and writer methods.
30
+
31
+ ---
32
+
33
+ ## Geometry Types
34
+
35
+ A Feature's geometry can be any of:
36
+
37
+ - **Coordinate** — any of the 18 coordinate classes (`LLA`, `ECEF`, `UTM`, etc.)
38
+ - **Area** — `Areas::Circle`, `Areas::Polygon`, or `Areas::Rectangle`
39
+ - **Path** — a `Geodetic::Path` representing a route or trail
40
+
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.
42
+
43
+ ---
44
+
45
+ ## Methods
46
+
47
+ ### `distance_to(other)`
48
+
49
+ Returns a `Geodetic::Distance` between this feature and another feature, coordinate, or area. When either side is an area geometry, its centroid is used.
50
+
51
+ The `other` parameter can be a `Feature`, a coordinate, or an area.
52
+
53
+ ```ruby
54
+ liberty = Feature.new(label: "Liberty", geometry: LLA.new(lat: 40.6892, lng: -74.0445, alt: 0))
55
+ empire = Feature.new(label: "Empire", geometry: LLA.new(lat: 40.7484, lng: -73.9857, alt: 0))
56
+
57
+ liberty.distance_to(empire).to_km # => "8.24 km"
58
+
59
+ # Also works with a raw coordinate
60
+ liberty.distance_to(LLA.new(lat: 40.7484, lng: -73.9857, alt: 0))
61
+ ```
62
+
63
+ ### `bearing_to(other)`
64
+
65
+ Returns a `Geodetic::Bearing` from this feature to another feature, coordinate, or area. Uses the same centroid resolution as `distance_to`.
66
+
67
+ ```ruby
68
+ liberty.bearing_to(empire).degrees # => 36.99
69
+ liberty.bearing_to(empire).to_compass # => "NE"
70
+ ```
71
+
72
+ ### `to_s`
73
+
74
+ Returns `"label (geometry.to_s)"`.
75
+
76
+ ```ruby
77
+ liberty.to_s # => "Liberty (40.689200, -74.044500, 0.00)"
78
+ ```
79
+
80
+ ### `inspect`
81
+
82
+ Returns a detailed string with label, geometry, and metadata.
83
+
84
+ ```ruby
85
+ liberty.inspect
86
+ # => "#<Geodetic::Feature name=\"Liberty\" geometry=#<Geodetic::Coordinate::LLA ...> metadata={}>"
87
+ ```
88
+
89
+ ---
90
+
91
+ ## Area Geometry Example
92
+
93
+ ```ruby
94
+ park_boundary = Areas::Polygon.new(boundary: [
95
+ LLA.new(lat: 40.7679, lng: -73.9818, alt: 0),
96
+ LLA.new(lat: 40.7649, lng: -73.9727, alt: 0),
97
+ LLA.new(lat: 40.8003, lng: -73.9494, alt: 0),
98
+ LLA.new(lat: 40.8008, lng: -73.9585, alt: 0),
99
+ ])
100
+
101
+ park = Feature.new(label: "Central Park", geometry: park_boundary)
102
+
103
+ # distance_to uses the polygon's centroid
104
+ park.distance_to(liberty) # => Distance
105
+ park.bearing_to(liberty) # => Bearing
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Centroid Resolution
111
+
112
+ When computing distances and bearings, Feature resolves the underlying point as follows:
113
+
114
+ - If the geometry responds to `centroid` (all area classes do), the centroid is used.
115
+ - Otherwise, the geometry itself is used directly (all coordinate classes).
116
+
117
+ This applies to both the source feature and the target. When the target is a Feature, its geometry is resolved the same way. When the target is a raw coordinate or area, the same logic applies.