geodetic 0.5.0 → 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +62 -3
- data/README.md +95 -48
- data/docs/coordinate-systems/index.md +18 -18
- data/docs/index.md +49 -27
- data/docs/reference/arithmetic.md +1 -1
- data/docs/reference/conversions.md +3 -3
- data/docs/reference/feature.md +1 -1
- data/docs/reference/geojson.md +388 -0
- data/examples/09_geojson_export.rb +255 -0
- data/examples/README.md +14 -0
- data/examples/geodetic_demo.geojson +305 -0
- data/lib/geodetic/areas/regular_polygon.rb +1 -21
- data/lib/geodetic/coordinate/bng.rb +26 -26
- data/lib/geodetic/coordinate/ecef.rb +12 -12
- data/lib/geodetic/coordinate/enu.rb +26 -26
- data/lib/geodetic/coordinate/lla.rb +16 -16
- data/lib/geodetic/coordinate/mgrs.rb +26 -26
- data/lib/geodetic/coordinate/ned.rb +26 -26
- data/lib/geodetic/coordinate/spatial_hash.rb +22 -22
- data/lib/geodetic/coordinate/state_plane.rb +42 -42
- data/lib/geodetic/coordinate/ups.rb +21 -21
- data/lib/geodetic/coordinate/usng.rb +25 -25
- data/lib/geodetic/coordinate/utm.rb +12 -12
- data/lib/geodetic/coordinate/web_mercator.rb +21 -21
- data/lib/geodetic/coordinate.rb +3 -2
- data/lib/geodetic/geojson.rb +303 -0
- data/lib/geodetic/path.rb +12 -15
- data/lib/geodetic/segment.rb +2 -18
- data/lib/geodetic/version.rb +1 -1
- data/lib/geodetic.rb +5 -4
- data/mkdocs.yml +10 -0
- metadata +5 -1
|
@@ -250,7 +250,7 @@ portland = Geodetic::Coordinate::LLA.new(lat: 45.5152, lng: -122.6784, alt: 0.0)
|
|
|
250
250
|
sf = Geodetic::Coordinate::LLA.new(lat: 37.7749, lng: -122.4194, alt: 0.0)
|
|
251
251
|
|
|
252
252
|
# Radial distances from receiver
|
|
253
|
-
seattle.distance_to(portland) # => Distance (
|
|
253
|
+
seattle.distance_to(portland) # => Distance (235385.71 m)
|
|
254
254
|
seattle.distance_to(portland, sf) # => [Distance, Distance] (Array)
|
|
255
255
|
|
|
256
256
|
# Consecutive chain distances
|
|
@@ -274,7 +274,7 @@ Both `distance_to` and `straight_line_distance_to` accept any coordinate type. C
|
|
|
274
274
|
```ruby
|
|
275
275
|
utm = seattle.to_utm
|
|
276
276
|
mgrs = Geodetic::Coordinate::MGRS.from_lla(portland)
|
|
277
|
-
utm.distance_to(mgrs) # => Distance (
|
|
277
|
+
utm.distance_to(mgrs) # => Distance (235385.71 m)
|
|
278
278
|
```
|
|
279
279
|
|
|
280
280
|
### ENU and NED (Relative Systems)
|
|
@@ -308,7 +308,7 @@ 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
|
|
311
|
-
b.degrees # =>
|
|
311
|
+
b.degrees # => 186.25...
|
|
312
312
|
b.to_compass(points: 8) # => "S"
|
|
313
313
|
b.reverse # => Bearing (back azimuth)
|
|
314
314
|
|
data/docs/reference/feature.md
CHANGED
|
@@ -65,7 +65,7 @@ liberty.distance_to(LLA.new(lat: 40.7484, lng: -73.9857, alt: 0))
|
|
|
65
65
|
Returns a `Geodetic::Bearing` from this feature to another feature, coordinate, or area. Uses the same centroid resolution as `distance_to`.
|
|
66
66
|
|
|
67
67
|
```ruby
|
|
68
|
-
liberty.bearing_to(empire).degrees # => 36.
|
|
68
|
+
liberty.bearing_to(empire).degrees # => 36.95
|
|
69
69
|
liberty.bearing_to(empire).to_compass # => "NE"
|
|
70
70
|
```
|
|
71
71
|
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
# GeoJSON Export Reference
|
|
2
|
+
|
|
3
|
+
`Geodetic::GeoJSON` builds a [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) FeatureCollection from any combination of Geodetic objects. It provides an accumulator pattern for collecting geometry, then exporting as a Hash, JSON string, or file.
|
|
4
|
+
|
|
5
|
+
Every geometry type also gains a `to_geojson` instance method that returns a GeoJSON-compatible Ruby Hash.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## GeoJSON Class
|
|
10
|
+
|
|
11
|
+
### Constructor
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gj = Geodetic::GeoJSON.new # empty collection
|
|
15
|
+
gj = Geodetic::GeoJSON.new(seattle, portland) # initialize with objects
|
|
16
|
+
gj = Geodetic::GeoJSON.new([seattle, portland]) # also accepts an array
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Accumulating Objects
|
|
20
|
+
|
|
21
|
+
Use `<<` to add a single object or an array of objects. Returns `self` for chaining.
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
gj = Geodetic::GeoJSON.new
|
|
25
|
+
|
|
26
|
+
gj << seattle # single coordinate
|
|
27
|
+
gj << [portland, sf, la] # array of coordinates
|
|
28
|
+
gj << Geodetic::Segment.new(a, b) # segment
|
|
29
|
+
gj << route # path
|
|
30
|
+
gj << polygon # area
|
|
31
|
+
gj << circle # circle (approximated)
|
|
32
|
+
gj << bbox # bounding box
|
|
33
|
+
gj << feature # feature (preserves properties)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Any Geodetic object with a `to_geojson` method can be added. Non-Feature objects are auto-wrapped as GeoJSON Features with empty `properties`. Feature objects carry their `label` and `metadata` into the GeoJSON output.
|
|
37
|
+
|
|
38
|
+
### Query
|
|
39
|
+
|
|
40
|
+
| Method | Returns | Description |
|
|
41
|
+
|----------|---------|-------------|
|
|
42
|
+
| `size` | Integer | Number of collected objects |
|
|
43
|
+
| `length` | Integer | Alias for `size` |
|
|
44
|
+
| `empty?` | Boolean | True if no objects collected |
|
|
45
|
+
| `each` | Enumerator | Iterate over collected objects |
|
|
46
|
+
|
|
47
|
+
`GeoJSON` includes `Enumerable`, so `map`, `select`, `reject`, `count`, `to_a`, and all other Enumerable methods are available.
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
gj.size # => 5
|
|
51
|
+
gj.empty? # => false
|
|
52
|
+
gj.map { |obj| obj.class } # => [LLA, LLA, Path, ...]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Removing Objects
|
|
56
|
+
|
|
57
|
+
| Method | Description |
|
|
58
|
+
|---------------|-------------|
|
|
59
|
+
| `delete(obj)` | Remove a specific object from the collection |
|
|
60
|
+
| `clear` | Remove all objects |
|
|
61
|
+
|
|
62
|
+
Both return `self` for chaining.
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
gj.delete(portland)
|
|
66
|
+
gj.clear
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Export
|
|
70
|
+
|
|
71
|
+
| Method | Returns | Description |
|
|
72
|
+
|----------------------------|---------|-------------|
|
|
73
|
+
| `to_h` | Hash | GeoJSON FeatureCollection as a Ruby Hash |
|
|
74
|
+
| `to_json` | String | Compact JSON string |
|
|
75
|
+
| `to_json(pretty: true)` | String | Pretty-printed JSON string |
|
|
76
|
+
| `save(path)` | nil | Write compact JSON to file |
|
|
77
|
+
| `save(path, pretty: true)` | nil | Write pretty-printed JSON to file |
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
gj.to_h
|
|
81
|
+
# => {"type" => "FeatureCollection", "features" => [...]}
|
|
82
|
+
|
|
83
|
+
gj.to_json
|
|
84
|
+
# => '{"type":"FeatureCollection","features":[...]}'
|
|
85
|
+
|
|
86
|
+
gj.to_json(pretty: true)
|
|
87
|
+
# => formatted JSON with indentation
|
|
88
|
+
|
|
89
|
+
gj.save("my_map.geojson")
|
|
90
|
+
gj.save("my_map.geojson", pretty: true)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The `to_json` and `save` methods use Ruby's built-in `json` library (a default gem, always available). No external dependencies are required.
|
|
94
|
+
|
|
95
|
+
### Display
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
gj.to_s # => "GeoJSON::FeatureCollection(5 features)"
|
|
99
|
+
gj.inspect # => "#<Geodetic::GeoJSON size=5>"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Import
|
|
103
|
+
|
|
104
|
+
| Method | Returns | Description |
|
|
105
|
+
|--------|---------|-------------|
|
|
106
|
+
| `GeoJSON.load(path)` | Array | Read a GeoJSON file and return an Array of Geodetic objects |
|
|
107
|
+
| `GeoJSON.parse(hash)` | Array | Parse a GeoJSON Hash and return an Array of Geodetic objects |
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
objects = Geodetic::GeoJSON.load("west_coast.geojson")
|
|
111
|
+
# => [Feature("Seattle", LLA), Feature("Portland", LLA), Segment, Polygon, ...]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`parse` accepts a Ruby Hash (useful when you already have parsed JSON):
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
data = JSON.parse(File.read("west_coast.geojson"))
|
|
118
|
+
objects = Geodetic::GeoJSON.parse(data)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**GeoJSON → Geodetic type mapping:**
|
|
122
|
+
|
|
123
|
+
| GeoJSON type | Geodetic type |
|
|
124
|
+
|--------------|---------------|
|
|
125
|
+
| Point | `Coordinate::LLA` |
|
|
126
|
+
| LineString (2 points) | `Segment` |
|
|
127
|
+
| LineString (3+ points) | `Path` |
|
|
128
|
+
| Polygon | `Areas::Polygon` (outer ring only; holes are dropped) |
|
|
129
|
+
| MultiPoint | Multiple `Coordinate::LLA` |
|
|
130
|
+
| MultiLineString | Multiple `Segment` or `Path` |
|
|
131
|
+
| MultiPolygon | Multiple `Areas::Polygon` |
|
|
132
|
+
| GeometryCollection | Flattened into individual geometries |
|
|
133
|
+
|
|
134
|
+
**Feature handling:**
|
|
135
|
+
|
|
136
|
+
- A GeoJSON Feature with a `"name"` property or any non-empty properties becomes a `Geodetic::Feature`. The `"name"` property maps to `label`, and remaining properties become `metadata` with symbol keys.
|
|
137
|
+
- A GeoJSON Feature with empty properties (`{}`) returns the raw geometry with no Feature wrapper.
|
|
138
|
+
|
|
139
|
+
This means a save/load roundtrip preserves Feature labels, metadata, and geometry types:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
# Save
|
|
143
|
+
gj = Geodetic::GeoJSON.new
|
|
144
|
+
gj << Geodetic::Feature.new(label: "Seattle", geometry: seattle, metadata: { state: "WA" })
|
|
145
|
+
gj << portland # raw coordinate, no Feature
|
|
146
|
+
gj.save("cities.geojson")
|
|
147
|
+
|
|
148
|
+
# Load
|
|
149
|
+
objects = Geodetic::GeoJSON.load("cities.geojson")
|
|
150
|
+
objects[0] # => Feature (label: "Seattle", metadata: {state: "WA"})
|
|
151
|
+
objects[0].label # => "Seattle"
|
|
152
|
+
objects[0].metadata # => {state: "WA"}
|
|
153
|
+
objects[1] # => LLA (raw coordinate, no Feature wrapper)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Geometry Mapping
|
|
159
|
+
|
|
160
|
+
Each Geodetic geometry type maps to a specific GeoJSON geometry type:
|
|
161
|
+
|
|
162
|
+
| Geodetic Type | GeoJSON Type | Notes |
|
|
163
|
+
|---------------|-------------|-------|
|
|
164
|
+
| Any coordinate (LLA, UTM, ECEF, ...) | Point | Converts through LLA |
|
|
165
|
+
| `Segment` | LineString | 2 positions |
|
|
166
|
+
| `Path` | LineString | N positions (default) |
|
|
167
|
+
| `Path` (with `as: :polygon`) | Polygon | Auto-closes the ring |
|
|
168
|
+
| `Areas::Polygon` (and subclasses) | Polygon | Boundary ring already closed |
|
|
169
|
+
| `Areas::Circle` | Polygon | Approximated as N-gon (default 32) |
|
|
170
|
+
| `Areas::BoundingBox` | Polygon | 4 corners, closed |
|
|
171
|
+
| `Feature` | Feature | Geometry + properties |
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Individual `to_geojson` Methods
|
|
176
|
+
|
|
177
|
+
### Coordinates
|
|
178
|
+
|
|
179
|
+
All 18 coordinate classes gain a `to_geojson` method. It converts the coordinate to LLA and returns a GeoJSON Point.
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
seattle = Geodetic::Coordinate::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
|
|
183
|
+
seattle.to_geojson
|
|
184
|
+
# => {"type" => "Point", "coordinates" => [-122.3493, 47.6205]}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Altitude handling:** The GeoJSON position array is `[lng, lat]` when altitude is zero, and `[lng, lat, alt]` when altitude is non-zero.
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
Geodetic::Coordinate::LLA.new(lat: 47.62, lng: -122.35, alt: 184.0).to_geojson
|
|
191
|
+
# => {"type" => "Point", "coordinates" => [-122.35, 47.62, 184.0]}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Cross-system:** Any coordinate type works — UTM, ECEF, MGRS, GH, etc. are converted to LLA internally.
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
seattle.to_utm.to_geojson # => {"type" => "Point", ...}
|
|
198
|
+
seattle.to_mgrs.to_geojson # => {"type" => "Point", ...}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**ENU and NED:** These are relative coordinate systems with no absolute position. Calling `to_geojson` raises `ArgumentError` with a message explaining that conversion to an absolute system is required first.
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
enu = Geodetic::Coordinate::ENU.new(e: 100, n: 200, u: 10)
|
|
205
|
+
enu.to_geojson # => ArgumentError
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
### Segment
|
|
211
|
+
|
|
212
|
+
Returns a GeoJSON LineString with two positions.
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
seg = Geodetic::Segment.new(seattle, portland)
|
|
216
|
+
seg.to_geojson
|
|
217
|
+
# => {"type" => "LineString", "coordinates" => [[-122.3493, 47.6205], [-122.6784, 45.5152]]}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
### Path
|
|
223
|
+
|
|
224
|
+
Returns a GeoJSON LineString by default. Requires at least 2 coordinates.
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
route = Geodetic::Path.new(coordinates: [seattle, portland, sf])
|
|
228
|
+
route.to_geojson
|
|
229
|
+
# => {"type" => "LineString", "coordinates" => [[...], [...], [...]]}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**As polygon:** Pass `as: :polygon` to export as a closed GeoJSON Polygon. Requires at least 3 coordinates. The ring is auto-closed if the first and last coordinates differ.
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
route.to_geojson(as: :polygon)
|
|
236
|
+
# => {"type" => "Polygon", "coordinates" => [[[...], [...], [...], [...]]]}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Edge cases:**
|
|
240
|
+
|
|
241
|
+
| Condition | Behavior |
|
|
242
|
+
|-----------|----------|
|
|
243
|
+
| Empty path | Raises `ArgumentError` |
|
|
244
|
+
| 1 coordinate (line_string) | Raises `ArgumentError` |
|
|
245
|
+
| 2 coordinates (polygon) | Raises `ArgumentError` |
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
### Areas::Polygon
|
|
250
|
+
|
|
251
|
+
Returns a GeoJSON Polygon. The boundary ring is already closed by `Polygon#initialize`.
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
poly = Geodetic::Areas::Polygon.new(boundary: [a, b, c])
|
|
255
|
+
poly.to_geojson
|
|
256
|
+
# => {"type" => "Polygon", "coordinates" => [[[...], [...], [...], [...]]]}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
All Polygon subclasses (`Triangle`, `Rectangle`, `Pentagon`, `Hexagon`, `Octagon`) inherit this method.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
### Areas::Circle
|
|
264
|
+
|
|
265
|
+
Returns a GeoJSON Polygon approximating the circle as a regular N-gon. Default is 32 segments.
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
circle = Geodetic::Areas::Circle.new(centroid: seattle, radius: 10_000)
|
|
269
|
+
|
|
270
|
+
circle.to_geojson # => 32-gon (33 positions including closing)
|
|
271
|
+
circle.to_geojson(segments: 64) # => 64-gon (65 positions including closing)
|
|
272
|
+
circle.to_geojson(segments: 8) # => 8-gon (9 positions including closing)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The vertices are computed using `Geodetic::Vector#destination_from` (Vincenty direct), so the approximation is geodetically accurate.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
### Areas::BoundingBox
|
|
280
|
+
|
|
281
|
+
Returns a GeoJSON Polygon with 4 corners plus the closing point (5 positions total). Ring order follows the GeoJSON right-hand rule: NW → NE → SE → SW → NW.
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
bbox = Geodetic::Areas::BoundingBox.new(
|
|
285
|
+
nw: Geodetic::Coordinate::LLA.new(lat: 48.0, lng: -123.0, alt: 0),
|
|
286
|
+
se: Geodetic::Coordinate::LLA.new(lat: 46.0, lng: -121.0, alt: 0)
|
|
287
|
+
)
|
|
288
|
+
bbox.to_geojson
|
|
289
|
+
# => {"type" => "Polygon", "coordinates" => [[[-123.0, 48.0], [-121.0, 48.0], [-121.0, 46.0], [-123.0, 46.0], [-123.0, 48.0]]]}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
### Feature
|
|
295
|
+
|
|
296
|
+
Returns a GeoJSON Feature. The `label` is mapped to the `"name"` property. The `metadata` hash is merged into `properties` with keys converted to strings.
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
f = Geodetic::Feature.new(
|
|
300
|
+
label: "Seattle",
|
|
301
|
+
geometry: seattle,
|
|
302
|
+
metadata: { state: "WA", population: 750_000 }
|
|
303
|
+
)
|
|
304
|
+
f.to_geojson
|
|
305
|
+
# => {
|
|
306
|
+
# "type" => "Feature",
|
|
307
|
+
# "geometry" => {"type" => "Point", "coordinates" => [-122.3493, 47.6205]},
|
|
308
|
+
# "properties" => {"name" => "Seattle", "state" => "WA", "population" => 750000}
|
|
309
|
+
# }
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
| Feature field | GeoJSON mapping |
|
|
313
|
+
|---------------|-----------------|
|
|
314
|
+
| `label` | `properties["name"]` (omitted if `nil`) |
|
|
315
|
+
| `metadata` | Merged into `properties` (symbol keys stringified) |
|
|
316
|
+
| `geometry` | Delegates to the geometry's `to_geojson` |
|
|
317
|
+
|
|
318
|
+
The geometry can be any type: coordinate, segment, path, polygon, circle, or bounding box.
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
# Feature wrapping a Path
|
|
322
|
+
route_feature = Geodetic::Feature.new(
|
|
323
|
+
label: "West Coast Route",
|
|
324
|
+
geometry: route,
|
|
325
|
+
metadata: { mode: "driving" }
|
|
326
|
+
)
|
|
327
|
+
route_feature.to_geojson
|
|
328
|
+
# => {"type" => "Feature", "geometry" => {"type" => "LineString", ...}, "properties" => {...}}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Complete Example
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
require "geodetic"
|
|
337
|
+
|
|
338
|
+
seattle = Geodetic::Coordinate::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0)
|
|
339
|
+
portland = Geodetic::Coordinate::LLA.new(lat: 45.5152, lng: -122.6784, alt: 0)
|
|
340
|
+
sf = Geodetic::Coordinate::LLA.new(lat: 37.7749, lng: -122.4194, alt: 0)
|
|
341
|
+
|
|
342
|
+
# Build a collection
|
|
343
|
+
gj = Geodetic::GeoJSON.new
|
|
344
|
+
|
|
345
|
+
# Add cities as features with metadata
|
|
346
|
+
gj << Geodetic::Feature.new(label: "Seattle", geometry: seattle, metadata: { pop: 750_000 })
|
|
347
|
+
gj << Geodetic::Feature.new(label: "Portland", geometry: portland, metadata: { pop: 650_000 })
|
|
348
|
+
gj << Geodetic::Feature.new(label: "San Francisco", geometry: sf, metadata: { pop: 870_000 })
|
|
349
|
+
|
|
350
|
+
# Add the route connecting them
|
|
351
|
+
route = Geodetic::Path.new(coordinates: [seattle, portland, sf])
|
|
352
|
+
gj << Geodetic::Feature.new(label: "West Coast Route", geometry: route)
|
|
353
|
+
|
|
354
|
+
# Add a 50km radius around Seattle
|
|
355
|
+
gj << Geodetic::Feature.new(
|
|
356
|
+
label: "Seattle Metro",
|
|
357
|
+
geometry: Geodetic::Areas::Circle.new(centroid: seattle, radius: 50_000)
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Export
|
|
361
|
+
gj.save("west_coast.geojson", pretty: true)
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
The output file can be opened directly in [geojson.io](https://geojson.io), QGIS, Mapbox, Leaflet, or any other GeoJSON-compatible tool.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## GeoJSON Specification Notes
|
|
369
|
+
|
|
370
|
+
- **Coordinate order** is `[longitude, latitude]` (not `[lat, lng]`), per [RFC 7946 Section 3.1.1](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.1).
|
|
371
|
+
- **Altitude** is optional. Included as the third element when non-zero.
|
|
372
|
+
- **Polygon rings** follow the right-hand rule: exterior rings are counterclockwise. BoundingBox uses NW → NE → SE → SW → NW.
|
|
373
|
+
- **String keys** are used throughout (`"type"`, `"coordinates"`, `"properties"`, etc.) per JSON convention.
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## Visualizing GeoJSON
|
|
378
|
+
|
|
379
|
+
The easiest way to verify your exported GeoJSON is [geojson.io](https://geojson.io). It renders points, lines, and polygons on an interactive map with property inspection.
|
|
380
|
+
|
|
381
|
+
To use it:
|
|
382
|
+
|
|
383
|
+
1. Export your collection: `gj.save("my_map.geojson", pretty: true)`
|
|
384
|
+
2. Open [geojson.io](https://geojson.io) in a browser
|
|
385
|
+
3. Drag and drop the `.geojson` file onto the map, or paste the JSON into the editor panel
|
|
386
|
+
|
|
387
|
+
Feature properties (name, metadata) appear in popups when you click on a rendered geometry. This makes it a quick way to confirm that coordinates, shapes, and metadata are correct before integrating with QGIS, Mapbox, Leaflet, or other GIS tools.
|
|
388
|
+
- Output is a Ruby Hash. Call `.to_json` or `JSON.generate(hash)` to produce a JSON string.
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Demonstration of GeoJSON Export
|
|
4
|
+
# Shows how to build a GeoJSON FeatureCollection from mixed Geodetic objects
|
|
5
|
+
# and save it to a file that can be opened in any GeoJSON viewer.
|
|
6
|
+
|
|
7
|
+
require_relative "../lib/geodetic"
|
|
8
|
+
|
|
9
|
+
include Geodetic
|
|
10
|
+
|
|
11
|
+
LLA = Coordinate::LLA
|
|
12
|
+
Distance = Geodetic::Distance
|
|
13
|
+
Vector = Geodetic::Vector
|
|
14
|
+
|
|
15
|
+
puts "=== GeoJSON Export Demo ==="
|
|
16
|
+
puts
|
|
17
|
+
|
|
18
|
+
# ── 1. Individual to_geojson on each geometry type ─────────────────
|
|
19
|
+
|
|
20
|
+
puts "--- 1. Coordinate → GeoJSON Point ---"
|
|
21
|
+
puts
|
|
22
|
+
|
|
23
|
+
seattle = LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
|
|
24
|
+
puts " seattle.to_geojson"
|
|
25
|
+
puts " => #{seattle.to_geojson}"
|
|
26
|
+
puts
|
|
27
|
+
|
|
28
|
+
# Works from any coordinate system — converts through LLA
|
|
29
|
+
utm = seattle.to_utm
|
|
30
|
+
puts " seattle.to_utm.to_geojson"
|
|
31
|
+
puts " => #{utm.to_geojson}"
|
|
32
|
+
puts
|
|
33
|
+
|
|
34
|
+
# Altitude included when non-zero
|
|
35
|
+
space_needle = LLA.new(lat: 47.6205, lng: -122.3493, alt: 184.0)
|
|
36
|
+
puts " With altitude: #{space_needle.to_geojson}"
|
|
37
|
+
puts
|
|
38
|
+
|
|
39
|
+
# ── 2. Segment → LineString ───────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
puts "--- 2. Segment → GeoJSON LineString ---"
|
|
42
|
+
puts
|
|
43
|
+
|
|
44
|
+
portland = LLA.new(lat: 45.5152, lng: -122.6784, alt: 0.0)
|
|
45
|
+
seg = Segment.new(seattle, portland)
|
|
46
|
+
puts " Segment(seattle → portland).to_geojson"
|
|
47
|
+
puts " => #{seg.to_geojson}"
|
|
48
|
+
puts
|
|
49
|
+
|
|
50
|
+
# ── 3. Path → LineString or Polygon ───────────────────────────────
|
|
51
|
+
|
|
52
|
+
puts "--- 3. Path → GeoJSON LineString ---"
|
|
53
|
+
puts
|
|
54
|
+
|
|
55
|
+
sf = LLA.new(lat: 37.7749, lng: -122.4194, alt: 0.0)
|
|
56
|
+
la = LLA.new(lat: 34.0522, lng: -118.2437, alt: 0.0)
|
|
57
|
+
route = Path.new(coordinates: [seattle, portland, sf, la])
|
|
58
|
+
|
|
59
|
+
geojson = route.to_geojson
|
|
60
|
+
puts " Path(4 waypoints).to_geojson"
|
|
61
|
+
puts " type: #{geojson["type"]}, points: #{geojson["coordinates"].length}"
|
|
62
|
+
puts
|
|
63
|
+
|
|
64
|
+
# Path can also export as a polygon
|
|
65
|
+
triangle_path = Path.new(coordinates: [
|
|
66
|
+
LLA.new(lat: 47.0, lng: -122.5, alt: 0),
|
|
67
|
+
LLA.new(lat: 46.0, lng: -121.0, alt: 0),
|
|
68
|
+
LLA.new(lat: 46.0, lng: -123.0, alt: 0)
|
|
69
|
+
])
|
|
70
|
+
poly_geojson = triangle_path.to_geojson(as: :polygon)
|
|
71
|
+
puts " Path(3 points).to_geojson(as: :polygon)"
|
|
72
|
+
puts " type: #{poly_geojson["type"]}, ring closed: #{poly_geojson["coordinates"][0].first == poly_geojson["coordinates"][0].last}"
|
|
73
|
+
puts
|
|
74
|
+
|
|
75
|
+
# ── 4. Areas → Polygon ────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
puts "--- 4. Areas → GeoJSON Polygon ---"
|
|
78
|
+
puts
|
|
79
|
+
|
|
80
|
+
# Polygon
|
|
81
|
+
a = LLA.new(lat: 47.7, lng: -122.5, alt: 0)
|
|
82
|
+
b = LLA.new(lat: 47.5, lng: -122.1, alt: 0)
|
|
83
|
+
c = LLA.new(lat: 47.3, lng: -122.4, alt: 0)
|
|
84
|
+
poly = Areas::Polygon.new(boundary: [a, b, c])
|
|
85
|
+
puts " Polygon(3 vertices).to_geojson"
|
|
86
|
+
puts " type: #{poly.to_geojson["type"]}"
|
|
87
|
+
|
|
88
|
+
# Circle (approximated as 32-gon)
|
|
89
|
+
circle = Areas::Circle.new(centroid: seattle, radius: 10_000)
|
|
90
|
+
circle_gj = circle.to_geojson
|
|
91
|
+
ring_size = circle_gj["coordinates"][0].length
|
|
92
|
+
puts " Circle(10km).to_geojson → #{ring_size - 1}-gon (#{ring_size} points including closing)"
|
|
93
|
+
|
|
94
|
+
# Circle with custom resolution
|
|
95
|
+
circle_gj_64 = circle.to_geojson(segments: 64)
|
|
96
|
+
puts " Circle(10km).to_geojson(segments: 64) → #{circle_gj_64["coordinates"][0].length - 1}-gon"
|
|
97
|
+
|
|
98
|
+
# BoundingBox
|
|
99
|
+
bbox = Areas::BoundingBox.new(
|
|
100
|
+
nw: LLA.new(lat: 48.0, lng: -123.0, alt: 0),
|
|
101
|
+
se: LLA.new(lat: 46.0, lng: -121.0, alt: 0)
|
|
102
|
+
)
|
|
103
|
+
puts " BoundingBox.to_geojson → #{bbox.to_geojson["coordinates"][0].length} points (4 corners + closing)"
|
|
104
|
+
puts
|
|
105
|
+
|
|
106
|
+
# ── 5. Feature → GeoJSON Feature with properties ──────────────────
|
|
107
|
+
|
|
108
|
+
puts "--- 5. Feature → GeoJSON Feature ---"
|
|
109
|
+
puts
|
|
110
|
+
|
|
111
|
+
city = Feature.new(
|
|
112
|
+
label: "Seattle",
|
|
113
|
+
geometry: seattle,
|
|
114
|
+
metadata: { state: "WA", population: 750_000, timezone: "PST" }
|
|
115
|
+
)
|
|
116
|
+
feature_gj = city.to_geojson
|
|
117
|
+
puts " Feature('Seattle').to_geojson"
|
|
118
|
+
puts " type: #{feature_gj["type"]}"
|
|
119
|
+
puts " geometry: #{feature_gj["geometry"]["type"]}"
|
|
120
|
+
puts " properties: #{feature_gj["properties"]}"
|
|
121
|
+
puts
|
|
122
|
+
|
|
123
|
+
# Feature wrapping a polygon
|
|
124
|
+
park = Feature.new(
|
|
125
|
+
label: "Triangle Park",
|
|
126
|
+
geometry: poly,
|
|
127
|
+
metadata: { type: "park", area_sqkm: 12.5 }
|
|
128
|
+
)
|
|
129
|
+
park_gj = park.to_geojson
|
|
130
|
+
puts " Feature('Triangle Park', polygon).to_geojson"
|
|
131
|
+
puts " geometry: #{park_gj["geometry"]["type"]}"
|
|
132
|
+
puts " properties: #{park_gj["properties"]}"
|
|
133
|
+
puts
|
|
134
|
+
|
|
135
|
+
# ── 6. Building a FeatureCollection ───────────────────────────────
|
|
136
|
+
|
|
137
|
+
puts "--- 6. GeoJSON FeatureCollection ---"
|
|
138
|
+
puts
|
|
139
|
+
|
|
140
|
+
gj = GeoJSON.new
|
|
141
|
+
puts " gj = GeoJSON.new"
|
|
142
|
+
puts " gj.size: #{gj.size}, empty? #{gj.empty?}"
|
|
143
|
+
puts
|
|
144
|
+
|
|
145
|
+
# Add cities as features
|
|
146
|
+
cities = {
|
|
147
|
+
"Seattle" => LLA.new(lat: 47.6205, lng: -122.3493, alt: 0),
|
|
148
|
+
"Portland" => LLA.new(lat: 45.5152, lng: -122.6784, alt: 0),
|
|
149
|
+
"San Francisco" => LLA.new(lat: 37.7749, lng: -122.4194, alt: 0),
|
|
150
|
+
"Los Angeles" => LLA.new(lat: 34.0522, lng: -118.2437, alt: 0),
|
|
151
|
+
"New York" => LLA.new(lat: 40.7128, lng: -74.0060, alt: 0)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
cities.each do |name, coord|
|
|
155
|
+
gj << Feature.new(label: name, geometry: coord, metadata: { type: "city" })
|
|
156
|
+
end
|
|
157
|
+
puts " Added #{cities.size} cities as Features"
|
|
158
|
+
|
|
159
|
+
# Add a route as a path
|
|
160
|
+
west_coast = Path.new(coordinates: cities.values_at("Seattle", "Portland", "San Francisco", "Los Angeles"))
|
|
161
|
+
gj << Feature.new(label: "West Coast Route", geometry: west_coast, metadata: { mode: "driving" })
|
|
162
|
+
puts " Added West Coast route (Path with 4 waypoints)"
|
|
163
|
+
|
|
164
|
+
# Add an array of raw coordinates (auto-wrapped as Features)
|
|
165
|
+
landmarks = [
|
|
166
|
+
LLA.new(lat: 48.8566, lng: 2.3522, alt: 0), # Paris
|
|
167
|
+
LLA.new(lat: 51.5074, lng: -0.1278, alt: 0) # London
|
|
168
|
+
]
|
|
169
|
+
gj << landmarks
|
|
170
|
+
puts " Added 2 landmarks via << [array]"
|
|
171
|
+
|
|
172
|
+
# Add a bounding box and a circle
|
|
173
|
+
gj << Feature.new(label: "Pacific NW", geometry: bbox, metadata: { region: true })
|
|
174
|
+
gj << Feature.new(label: "Seattle Metro", geometry: circle, metadata: { radius_km: 10 })
|
|
175
|
+
puts " Added bounding box and circle"
|
|
176
|
+
|
|
177
|
+
puts
|
|
178
|
+
puts " Total items: #{gj.size}"
|
|
179
|
+
puts " to_s: #{gj}"
|
|
180
|
+
puts
|
|
181
|
+
|
|
182
|
+
# ── 7. Initialize with objects ────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
puts "--- 7. Initialize with objects ---"
|
|
185
|
+
puts
|
|
186
|
+
|
|
187
|
+
gj2 = GeoJSON.new(seattle, portland, sf)
|
|
188
|
+
puts " GeoJSON.new(seattle, portland, sf) → size: #{gj2.size}"
|
|
189
|
+
|
|
190
|
+
gj3 = GeoJSON.new([seattle, portland])
|
|
191
|
+
puts " GeoJSON.new([seattle, portland]) → size: #{gj3.size}"
|
|
192
|
+
puts
|
|
193
|
+
|
|
194
|
+
# ── 8. Delete and Clear ───────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
puts "--- 8. Delete and Clear ---"
|
|
197
|
+
puts
|
|
198
|
+
|
|
199
|
+
gj4 = GeoJSON.new(seattle, portland, sf)
|
|
200
|
+
puts " Before delete: #{gj4.size}"
|
|
201
|
+
gj4.delete(portland)
|
|
202
|
+
puts " After delete(portland): #{gj4.size}"
|
|
203
|
+
gj4.clear
|
|
204
|
+
puts " After clear: #{gj4.size}, empty? #{gj4.empty?}"
|
|
205
|
+
puts
|
|
206
|
+
|
|
207
|
+
# ── 9. Enumerable ─────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
puts "--- 9. Enumerable ---"
|
|
210
|
+
puts
|
|
211
|
+
|
|
212
|
+
gj5 = GeoJSON.new(seattle, portland, sf)
|
|
213
|
+
puts " Iterating:"
|
|
214
|
+
gj5.each { |obj| puts " #{obj.class}: #{obj.to_s(4)}" }
|
|
215
|
+
puts " map to classes: #{gj5.map(&:class).map(&:name)}"
|
|
216
|
+
puts
|
|
217
|
+
|
|
218
|
+
# ── 10. Export to Hash, JSON, and File ─────────────────────────────
|
|
219
|
+
|
|
220
|
+
puts "--- 10. Export ---"
|
|
221
|
+
puts
|
|
222
|
+
|
|
223
|
+
# to_h
|
|
224
|
+
h = gj.to_h
|
|
225
|
+
puts " to_h:"
|
|
226
|
+
puts " type: #{h["type"]}"
|
|
227
|
+
puts " features: #{h["features"].length}"
|
|
228
|
+
puts " geometry types: #{h["features"].map { |f| f["geometry"]["type"] }.tally}"
|
|
229
|
+
puts
|
|
230
|
+
|
|
231
|
+
# to_json (compact)
|
|
232
|
+
json = gj.to_json
|
|
233
|
+
puts " to_json (compact): #{json.length} bytes"
|
|
234
|
+
|
|
235
|
+
# to_json (pretty)
|
|
236
|
+
pretty = gj.to_json(pretty: true)
|
|
237
|
+
puts " to_json(pretty: true): #{pretty.length} bytes, #{pretty.lines.count} lines"
|
|
238
|
+
puts
|
|
239
|
+
|
|
240
|
+
# save to file
|
|
241
|
+
output_path = File.join(__dir__, "geodetic_demo.geojson")
|
|
242
|
+
gj.save(output_path, pretty: true)
|
|
243
|
+
puts " Saved to: #{output_path}"
|
|
244
|
+
puts " File size: #{File.size(output_path)} bytes"
|
|
245
|
+
puts
|
|
246
|
+
|
|
247
|
+
# Show first few lines of the pretty-printed output
|
|
248
|
+
puts " Preview (first 15 lines):"
|
|
249
|
+
pretty.lines.first(15).each { |line| puts " #{line}" }
|
|
250
|
+
puts " ..."
|
|
251
|
+
puts
|
|
252
|
+
|
|
253
|
+
puts "=== Done ==="
|
|
254
|
+
puts
|
|
255
|
+
puts "Open #{output_path} in https://geojson.io to visualize the data."
|
data/examples/README.md
CHANGED
|
@@ -117,3 +117,17 @@ Demonstrates the operator-based geometry system and the `Geodetic::Vector` class
|
|
|
117
117
|
- **Key distinction: + vs \*** where `P + V` returns a Segment (the journey) and `P * V` returns a Coordinate (the destination)
|
|
118
118
|
- **Path corridors** with `to_corridor(width:)` converting a path into a polygon, and translating the corridor
|
|
119
119
|
- **Composing operations** chaining arithmetic, vector math, and corridors in single expressions
|
|
120
|
+
|
|
121
|
+
## 09 - GeoJSON Export
|
|
122
|
+
|
|
123
|
+
Demonstrates the `Geodetic::GeoJSON` class for building and exporting GeoJSON FeatureCollections. Covers:
|
|
124
|
+
|
|
125
|
+
- **Coordinate → Point** with `to_geojson` on any coordinate system, including altitude handling
|
|
126
|
+
- **Segment → LineString** exporting two-point directed segments
|
|
127
|
+
- **Path → LineString** and optional `to_geojson(as: :polygon)` for closed paths
|
|
128
|
+
- **Areas → Polygon** for `Polygon`, `Circle` (32-gon approximation with configurable `segments:`), and `BoundingBox`
|
|
129
|
+
- **Feature → GeoJSON Feature** with `label` mapped to `"name"` and `metadata` merged into `properties`
|
|
130
|
+
- **FeatureCollection building** with `GeoJSON.new`, `<<` for single objects and arrays, and `GeoJSON.new(obj, ...)` initialization
|
|
131
|
+
- **Delete and clear** removing individual objects or emptying the collection
|
|
132
|
+
- **Enumerable** iteration over collected objects
|
|
133
|
+
- **Export** via `to_h` (Ruby Hash), `to_json`/`to_json(pretty: true)` (JSON string), and `save(path, pretty:)` (file output)
|