geodetic 0.4.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +76 -0
- data/docs/reference/arithmetic.md +368 -0
- data/docs/reference/vector.md +256 -0
- data/examples/08_geodetic_arithmetic.rb +393 -0
- data/examples/README.md +16 -0
- data/lib/geodetic/areas/circle.rb +8 -0
- data/lib/geodetic/areas/polygon.rb +10 -0
- data/lib/geodetic/coordinate.rb +26 -1
- data/lib/geodetic/distance.rb +5 -1
- data/lib/geodetic/path.rb +59 -0
- data/lib/geodetic/segment.rb +21 -0
- data/lib/geodetic/vector.rb +242 -0
- data/lib/geodetic/version.rb +1 -1
- data/lib/geodetic.rb +1 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 495ba0272532d5591ecbb37869857675af00014d6863849c79c6fbef35e590c3
|
|
4
|
+
data.tar.gz: dcb68a4385f7a888fbfb37b421d2f82c66aae077bda8557ce2a7b60c57e38b35
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8f276d8924aad177efc94aee0037f1fbd4e1cb655668ed54ff5ba0e471c16dd91687966577505057e31a144ea545adf3d8843535ca0705fca69fc5e5bf9a3637
|
|
7
|
+
data.tar.gz: f2d8102549b1502099d149453f5b18d8143b5799e3d3f5c2c4f947c86b7e2de1355e74916ae3b0d7376652fdb547ebd48488bff2eba18d60211da4ff0ff030a3
|
data/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,49 @@ 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
|
+
|
|
14
57
|
## [0.4.0] - 2026-03-10
|
|
15
58
|
|
|
16
59
|
### Added
|
data/README.md
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
- <strong>Segments</strong> - Directed two-point line segments with projection, intersection, and interpolation<br>
|
|
23
23
|
- <strong>Paths</strong> - Directed coordinate sequences with navigation, interpolation, closest approach, intersection, and area conversion<br>
|
|
24
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>
|
|
25
27
|
- <strong>Validated Setters</strong> - Type coercion and range validation on all coordinate attributes<br>
|
|
26
28
|
- <strong>Serialization</strong> - to_s(precision), to_a, from_string, from_array, DMS format<br>
|
|
27
29
|
- <strong>Multiple Datums</strong> - WGS84, Clarke 1866, GRS 1980, Airy 1830, and more<br>
|
|
@@ -649,6 +651,78 @@ park.distance_to(liberty).to_km # => "12.47 km"
|
|
|
649
651
|
|
|
650
652
|
All three attributes (`label`, `geometry`, `metadata`) are mutable.
|
|
651
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
|
+
|
|
652
726
|
### Web Mercator Tile Coordinates
|
|
653
727
|
|
|
654
728
|
```ruby
|
|
@@ -679,6 +753,8 @@ The [`examples/`](examples/) directory contains runnable demo scripts showing pr
|
|
|
679
753
|
| [`04_bearing_calculations.rb`](examples/04_bearing_calculations.rb) | Bearing class, compass directions, elevation angles, and chain bearings |
|
|
680
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) |
|
|
681
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 |
|
|
682
758
|
|
|
683
759
|
Run any example with:
|
|
684
760
|
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# Geodetic Arithmetic Reference
|
|
2
|
+
|
|
3
|
+
Geodetic provides operator overloading that lets you compose geometric objects naturally. The `+` operator builds geometry from parts, while `*` (and its alias `translate`) applies a vector displacement to shift objects.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Design Principles
|
|
8
|
+
|
|
9
|
+
1. **Type determines result** — the types of the operands, not their values, determine the return type. `Coordinate + Coordinate` always returns a Segment, never conditionally a different type.
|
|
10
|
+
|
|
11
|
+
2. **Left operand is the anchor** — in asymmetric operations, the left operand provides the reference point. `P1 + V` creates a segment starting at P1; `V + P1` creates a segment ending at P1.
|
|
12
|
+
|
|
13
|
+
3. **Consistent translation** — `*` always means "translate by this vector," returning the same type as the receiver. A translated Segment is still a Segment; a translated Path is still a Path.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## The + Operator: Building Geometry
|
|
18
|
+
|
|
19
|
+
The `+` operator composes smaller geometric objects into larger ones. The result type depends on the combination of operand types.
|
|
20
|
+
|
|
21
|
+
### Coordinate + Coordinate → Segment
|
|
22
|
+
|
|
23
|
+
Two points define a directed line segment.
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
seattle = Geodetic::Coordinate::LLA.new(lat: 47.62, lng: -122.35, alt: 0)
|
|
27
|
+
portland = Geodetic::Coordinate::LLA.new(lat: 45.52, lng: -122.68, alt: 0)
|
|
28
|
+
|
|
29
|
+
seg = seattle + portland # => Geodetic::Segment
|
|
30
|
+
seg.start_point # => seattle
|
|
31
|
+
seg.end_point # => portland
|
|
32
|
+
seg.length # => Distance (~235 km)
|
|
33
|
+
seg.bearing # => Bearing (~188°)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Works across any coordinate system — the Segment converts both points to LLA internally:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
utm = portland.to_utm
|
|
40
|
+
seg = seattle + utm # => Segment (seattle → portland via LLA)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Order matters: `seattle + portland` is a different segment than `portland + seattle`.
|
|
44
|
+
|
|
45
|
+
### Coordinate + Coordinate + Coordinate → Path
|
|
46
|
+
|
|
47
|
+
Chaining builds a path. The first `+` produces a Segment; the second `+` extends it into a Path.
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
path = seattle + portland + sf # => Geodetic::Path (3 points)
|
|
51
|
+
path.size # => 3
|
|
52
|
+
path.first # => seattle
|
|
53
|
+
path.last # => sf
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Further chaining continues to extend the Path:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
path = seattle + portland + sf + la + nyc # => Path (5 points)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Coordinate + Segment → Path
|
|
63
|
+
|
|
64
|
+
A point plus a segment produces a three-point path: the point, then the segment's endpoints.
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
seg = portland + sf
|
|
68
|
+
path = seattle + seg # => Path: seattle → portland → sf
|
|
69
|
+
path.size # => 3
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Segment + Coordinate → Path
|
|
73
|
+
|
|
74
|
+
Extending a segment with a point:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
seg = seattle + portland
|
|
78
|
+
path = seg + sf # => Path: seattle → portland → sf
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Segment + Segment → Path
|
|
82
|
+
|
|
83
|
+
Two segments concatenate into a four-point path:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
seg1 = seattle + portland
|
|
87
|
+
seg2 = sf + la
|
|
88
|
+
path = seg1 + seg2 # => Path: seattle → portland → sf → la
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Coordinate + Distance → Circle
|
|
92
|
+
|
|
93
|
+
A point plus a distance defines a circle.
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
radius = Geodetic::Distance.km(5)
|
|
97
|
+
circle = seattle + radius
|
|
98
|
+
# => Areas::Circle centered at seattle, 5000m radius
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Distance + Coordinate → Circle
|
|
102
|
+
|
|
103
|
+
Commutative — same result:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
circle = Geodetic::Distance.km(5) + seattle
|
|
107
|
+
# => Areas::Circle centered at seattle, 5000m radius
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Coordinate + Vector → Segment
|
|
111
|
+
|
|
112
|
+
A point plus a vector solves the Vincenty direct problem, producing a segment from the origin to the destination.
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
v = Geodetic::Vector.new(distance: 100_000, bearing: 45.0)
|
|
116
|
+
seg = seattle + v
|
|
117
|
+
# => Segment from seattle to a point 100km northeast
|
|
118
|
+
seg.length_meters # => ~100000.0
|
|
119
|
+
seg.bearing # => ~45°
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
This is different from translation (`*`): `+` gives you the journey (a Segment), while `*` gives you just the destination (a Coordinate).
|
|
123
|
+
|
|
124
|
+
### Vector + Coordinate → Segment
|
|
125
|
+
|
|
126
|
+
The vector reversed determines the start point; the coordinate is the endpoint.
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
v = Geodetic::Vector.new(distance: 10_000, bearing: 90.0)
|
|
130
|
+
seg = v + seattle
|
|
131
|
+
# => Segment from (10km west of seattle) to seattle
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Segment + Vector → Path
|
|
135
|
+
|
|
136
|
+
Extends the segment from its endpoint in the vector's direction.
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
seg = seattle + portland
|
|
140
|
+
v = Geodetic::Vector.new(distance: 50_000, bearing: 180.0)
|
|
141
|
+
path = seg + v
|
|
142
|
+
# => Path: seattle → portland → (50km south of portland)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Vector + Segment → Path
|
|
146
|
+
|
|
147
|
+
Prepends a new start point. The vector is reversed from the segment's start to find it.
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
v = Geodetic::Vector.new(distance: 50_000, bearing: 90.0)
|
|
151
|
+
seg = seattle + portland
|
|
152
|
+
path = v + seg
|
|
153
|
+
# => Path: (50km west of seattle) → seattle → portland
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Path + Vector → Path
|
|
157
|
+
|
|
158
|
+
Extends the path from its last point in the vector's direction.
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
path = seattle + portland + sf
|
|
162
|
+
v = Geodetic::Vector.new(distance: 100_000, bearing: 180.0)
|
|
163
|
+
path2 = path + v
|
|
164
|
+
# => Path: seattle → portland → sf → (100km south of sf)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Path + Coordinate → Path
|
|
168
|
+
|
|
169
|
+
Appends a waypoint (already existed before arithmetic was added):
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
path = seattle + portland
|
|
173
|
+
path2 = path + sf # => Path: seattle → portland → sf
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Path + Path → Path
|
|
177
|
+
|
|
178
|
+
Concatenates two paths (already existed):
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
west_coast = seattle + portland + sf
|
|
182
|
+
east_coast = nyc + dc
|
|
183
|
+
cross_country = west_coast + east_coast
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Complete + Operator Table
|
|
189
|
+
|
|
190
|
+
| Left | + Right | Result | Description |
|
|
191
|
+
|------|---------|--------|-------------|
|
|
192
|
+
| Coordinate | Coordinate | Segment | Two-point directed segment |
|
|
193
|
+
| Coordinate | Vector | Segment | Origin to Vincenty destination |
|
|
194
|
+
| Coordinate | Distance | Circle | Point + radius |
|
|
195
|
+
| Coordinate | Segment | Path | Point then segment endpoints |
|
|
196
|
+
| Segment | Coordinate | Path | Extend with waypoint |
|
|
197
|
+
| Segment | Segment | Path | Concatenate segments |
|
|
198
|
+
| Segment | Vector | Path | Extend from endpoint |
|
|
199
|
+
| Vector | Coordinate | Segment | Reverse start to coordinate |
|
|
200
|
+
| Vector | Segment | Path | Prepend via reverse |
|
|
201
|
+
| Vector | Vector | Vector | Component-wise addition |
|
|
202
|
+
| Distance | Coordinate | Circle | Radius + center |
|
|
203
|
+
| Path | Coordinate | Path | Append waypoint |
|
|
204
|
+
| Path | Path | Path | Concatenate |
|
|
205
|
+
| Path | Segment | Path | Append segment points |
|
|
206
|
+
| Path | Vector | Path | Extend from last point |
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## The * Operator: Translation
|
|
211
|
+
|
|
212
|
+
The `*` operator translates (shifts) a geometric object by a vector displacement. Every point in the object is moved by the same vector. The result is always the same type as the receiver.
|
|
213
|
+
|
|
214
|
+
The named method `translate` is an alias for `*`.
|
|
215
|
+
|
|
216
|
+
### Coordinate * Vector → Coordinate
|
|
217
|
+
|
|
218
|
+
Returns the destination point — the pure result of moving the point.
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
v = Geodetic::Vector.new(distance: 10_000, bearing: 0.0)
|
|
222
|
+
p2 = seattle * v # => LLA (10km north of seattle)
|
|
223
|
+
p2 = seattle.translate(v) # => same
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Compare with `+`: `seattle + v` returns a **Segment** (the journey); `seattle * v` returns a **Coordinate** (just the destination).
|
|
227
|
+
|
|
228
|
+
### Segment * Vector → Segment
|
|
229
|
+
|
|
230
|
+
Both endpoints are translated by the same vector. Length and bearing are preserved.
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
seg = Geodetic::Segment.new(seattle, portland)
|
|
234
|
+
v = Geodetic::Vector.new(distance: 100_000, bearing: 90.0)
|
|
235
|
+
|
|
236
|
+
shifted = seg * v
|
|
237
|
+
shifted = seg.translate(v)
|
|
238
|
+
|
|
239
|
+
shifted.length_meters # => same as original
|
|
240
|
+
shifted.start_point # => 100km east of seattle
|
|
241
|
+
shifted.end_point # => 100km east of portland
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Path * Vector → Path
|
|
245
|
+
|
|
246
|
+
All waypoints are translated. The shape and distances between points are preserved.
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
route = seattle + portland + sf
|
|
250
|
+
v = Geodetic::Vector.new(distance: 50_000, bearing: 0.0)
|
|
251
|
+
|
|
252
|
+
shifted = route * v # => Path shifted 50km north
|
|
253
|
+
shifted = route.translate(v) # => same
|
|
254
|
+
shifted.size # => 3
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Circle * Vector → Circle
|
|
258
|
+
|
|
259
|
+
The centroid is translated. The radius is preserved.
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
circle = Geodetic::Areas::Circle.new(centroid: seattle, radius: 5000)
|
|
263
|
+
v = Geodetic::Vector.new(distance: 10_000, bearing: 180.0)
|
|
264
|
+
|
|
265
|
+
shifted = circle * v # => Circle 10km south, same 5km radius
|
|
266
|
+
shifted = circle.translate(v) # => same
|
|
267
|
+
shifted.radius # => 5000.0
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Polygon * Vector → Polygon
|
|
271
|
+
|
|
272
|
+
All boundary vertices are translated. The shape is preserved.
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
a = LLA.new(lat: 40.0, lng: -74.0, alt: 0)
|
|
276
|
+
b = LLA.new(lat: 40.0, lng: -73.0, alt: 0)
|
|
277
|
+
c = LLA.new(lat: 41.0, lng: -73.5, alt: 0)
|
|
278
|
+
poly = Geodetic::Areas::Polygon.new(boundary: [a, b, c])
|
|
279
|
+
|
|
280
|
+
v = Geodetic::Vector.new(distance: 100_000, bearing: 0.0)
|
|
281
|
+
shifted = poly * v # => Polygon shifted 100km north
|
|
282
|
+
shifted = poly.translate(v) # => same
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Complete * Operator Table
|
|
288
|
+
|
|
289
|
+
| Object | * Vector | Result | Effect |
|
|
290
|
+
|--------|----------|--------|--------|
|
|
291
|
+
| Coordinate | Vector | Coordinate | Translate point |
|
|
292
|
+
| Segment | Vector | Segment | Translate both endpoints |
|
|
293
|
+
| Path | Vector | Path | Translate all waypoints |
|
|
294
|
+
| Circle | Vector | Circle | Translate centroid, preserve radius |
|
|
295
|
+
| Polygon | Vector | Polygon | Translate all vertices |
|
|
296
|
+
|
|
297
|
+
The `*` operator only accepts a `Vector` on the right side. Any other type raises `ArgumentError`.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Corridors
|
|
302
|
+
|
|
303
|
+
`Path#to_corridor(width:)` converts a path into a polygon by offsetting each waypoint perpendicular to the path bearing on both sides.
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
route = seattle + portland + sf
|
|
307
|
+
corridor = route.to_corridor(width: 1000) # 1km wide
|
|
308
|
+
corridor = route.to_corridor(width: Distance.km(1)) # also accepts Distance
|
|
309
|
+
# => Areas::Polygon with 2*N boundary vertices
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
At interior waypoints, the perpendicular direction uses the mean bearing of the two adjacent segments to avoid self-intersection at bends.
|
|
313
|
+
|
|
314
|
+
Requires at least 2 coordinates. The `width:` parameter accepts meters (Numeric) or a `Distance` object.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Combining + and *
|
|
319
|
+
|
|
320
|
+
The operators compose naturally:
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
# Build a route, then shift it
|
|
324
|
+
v = Geodetic::Vector.new(distance: 50_000, bearing: 90.0)
|
|
325
|
+
route = (seattle + portland + sf) * v # shifted 50km east
|
|
326
|
+
|
|
327
|
+
# Build a circle, then translate it
|
|
328
|
+
circle = (seattle + Distance.km(5)) * v
|
|
329
|
+
|
|
330
|
+
# Chain: point + vector gives segment, then extend
|
|
331
|
+
seg = seattle + Geodetic::Vector.new(distance: 100_000, bearing: 45.0)
|
|
332
|
+
path = seg + portland # 3-point path
|
|
333
|
+
|
|
334
|
+
# Translate a corridor
|
|
335
|
+
corridor = route.to_corridor(width: 500)
|
|
336
|
+
shifted_corridor = corridor * v
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Key Distinctions
|
|
342
|
+
|
|
343
|
+
### + vs * with Vector
|
|
344
|
+
|
|
345
|
+
This is the most important distinction to understand:
|
|
346
|
+
|
|
347
|
+
| Expression | Result | Meaning |
|
|
348
|
+
|-----------|--------|---------|
|
|
349
|
+
| `P + V` | Segment | The **journey** — where you started and where you arrived |
|
|
350
|
+
| `P * V` | Coordinate | The **destination** — just where you end up |
|
|
351
|
+
|
|
352
|
+
`P + V` gives you a Segment because building geometry is the purpose of `+`. The segment records both the origin and the destination.
|
|
353
|
+
|
|
354
|
+
`P * V` gives you a Coordinate because translation is the purpose of `*`. You're moving the point, not creating a composite object.
|
|
355
|
+
|
|
356
|
+
### Commutativity
|
|
357
|
+
|
|
358
|
+
Most `+` operations are **not** commutative — order determines the structure:
|
|
359
|
+
|
|
360
|
+
- `P1 + P2` starts at P1; `P2 + P1` starts at P2
|
|
361
|
+
- `P + V` creates a segment starting at P; `V + P` creates a segment ending at P
|
|
362
|
+
|
|
363
|
+
The exceptions are:
|
|
364
|
+
|
|
365
|
+
- `P + Distance` and `Distance + P` both produce the same Circle
|
|
366
|
+
- `V1 + V2` and `V2 + V1` produce the same Vector (component addition is commutative)
|
|
367
|
+
|
|
368
|
+
Translation (`*`) is always `object * vector` — the vector must be on the right.
|