geodetic 0.5.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 82e74456b38ec9c888ee70e68de3e626f0333b9f791c8de7779f5aa544cbbbc7
4
- data.tar.gz: a76e9a726d4a503f0e2331f321de25a3b7c155e8712a27a18c53ba8e5ab27953
3
+ metadata.gz: 44b5221a45f63382e712aa88d033d1cb0b8165f8804833a1fd8523a9d7d8ce32
4
+ data.tar.gz: 787dc88782b277262e15dde4fa1f61801894d1d34eff304dc63965147a3b10e8
5
5
  SHA512:
6
- metadata.gz: 4bd3b6a3c7664f9f0b20c7bd5c9cee52d80528fe91f97d5a9d2a8a48c9f8314d34027f50ba3b091581650965721e8275b268564ac6fb203fdc896c1f7fa25fa4
7
- data.tar.gz: f7d83378049a016a388e2f8b4130179e7166704e20b10525a6f5c5a88a12a8812b703fe484dbfd8c40d7af7d8e6ddda7a903052575ea52b48a169c1ebff3fd8d
6
+ metadata.gz: ffb91f043c3c0c9a2bb6b44fad7952936121a09b18879350398cf260fd2be1f594f33c405df7cea0027f59c0e06679f1d4f718192d7626032a4b5fab195626f9
7
+ data.tar.gz: dd922521aa23db80a5d9fdf56dbb98dcdfab811f72a126978d9b8dffe248f72f3dd83c3cae1e41d3ee2a1cf11455d70120d6c81fff7523f3aac798397250fbca
data/CHANGELOG.md CHANGED
@@ -8,6 +8,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  ## [Unreleased]
9
9
 
10
10
 
11
+ ## [0.5.2] - 2026-03-10
12
+
13
+ ### Added
14
+
15
+ - **`Geodetic::GeoJSON.load(path)`** — read a GeoJSON file and return an Array of Geodetic objects
16
+ - **`Geodetic::GeoJSON.parse(hash)`** — same as `load` but accepts an already-parsed Ruby Hash
17
+ - Roundtrip-safe import: Features with `"name"` or non-empty properties restore as `Geodetic::Feature` (label from `"name"`, remaining properties as `metadata` with symbol keys); Features with empty properties restore as raw geometry
18
+ - GeoJSON → Geodetic type mapping:
19
+ - Point → `Coordinate::LLA` (altitude preserved when present)
20
+ - LineString (2 points) → `Segment`
21
+ - LineString (3+ points) → `Path`
22
+ - Polygon → `Areas::Polygon` (outer ring; holes dropped)
23
+ - MultiPoint, MultiLineString, MultiPolygon → flattened into multiple objects
24
+ - GeometryCollection → flattened into individual geometries
25
+ - 17 new tests covering load, parse, roundtrip, multi-geometries, and edge cases
26
+
27
+ ### Changed
28
+
29
+ - Updated README with `GeoJSON.load` usage example
30
+ - Updated `docs/reference/geojson.md` with Import section, type mapping table, and roundtrip example
31
+ - Updated `mkdocs.yml` nav to include all 7 missing coordinate systems (GH36, GH, HAM, OLC, GEOREF, GARS, H3) and 3 missing reference pages (Vector, Arithmetic, GeoJSON Export)
32
+ - GeoJSON demo (`examples/09_geojson_export.rb`) now saves output to `examples/` directory instead of system temp
33
+
11
34
  ## [0.5.1] - 2026-03-10
12
35
 
13
36
  ### Added
data/README.md CHANGED
@@ -760,6 +760,15 @@ feature.to_geojson # => {"type" => "Feature", ...}
760
760
 
761
761
  Features carry their `label` as `"name"` and `metadata` as `properties` in the GeoJSON output. Non-Feature objects added to the collection are auto-wrapped as Features with empty properties.
762
762
 
763
+ **Loading GeoJSON files:**
764
+
765
+ ```ruby
766
+ objects = Geodetic::GeoJSON.load("map.geojson")
767
+ # => [Feature("Seattle", LLA), Segment, Path, Polygon, LLA, ...]
768
+ ```
769
+
770
+ `load` returns an Array of Geodetic objects. Features with a `"name"` or non-empty properties round-trip as `Feature` objects; bare geometries with empty properties return as raw coordinates, segments, paths, or polygons. `GeoJSON.parse(hash)` does the same from an already-parsed Hash.
771
+
763
772
  ### Web Mercator Tile Coordinates
764
773
 
765
774
  ```ruby
@@ -99,6 +99,60 @@ gj.to_s # => "GeoJSON::FeatureCollection(5 features)"
99
99
  gj.inspect # => "#<Geodetic::GeoJSON size=5>"
100
100
  ```
101
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
+
102
156
  ---
103
157
 
104
158
  ## Geometry Mapping
@@ -317,4 +371,18 @@ The output file can be opened directly in [geojson.io](https://geojson.io), QGIS
317
371
  - **Altitude** is optional. Included as the third element when non-zero.
318
372
  - **Polygon rings** follow the right-hand rule: exterior rings are counterclockwise. BoundingBox uses NW → NE → SE → SW → NW.
319
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.
320
388
  - Output is a Ruby Hash. Call `.to_json` or `JSON.generate(hash)` to produce a JSON string.
@@ -5,7 +5,6 @@
5
5
  # and save it to a file that can be opened in any GeoJSON viewer.
6
6
 
7
7
  require_relative "../lib/geodetic"
8
- require "tmpdir"
9
8
 
10
9
  include Geodetic
11
10
 
@@ -239,7 +238,7 @@ puts " to_json(pretty: true): #{pretty.length} bytes, #{pretty.lines.count} lin
239
238
  puts
240
239
 
241
240
  # save to file
242
- output_path = File.join(Dir.tmpdir, "geodetic_demo.geojson")
241
+ output_path = File.join(__dir__, "geodetic_demo.geojson")
243
242
  gj.save(output_path, pretty: true)
244
243
  puts " Saved to: #{output_path}"
245
244
  puts " File size: #{File.size(output_path)} bytes"
@@ -0,0 +1,305 @@
1
+ {
2
+ "type": "FeatureCollection",
3
+ "features": [
4
+ {
5
+ "type": "Feature",
6
+ "geometry": {
7
+ "type": "Point",
8
+ "coordinates": [
9
+ -122.3493,
10
+ 47.6205
11
+ ]
12
+ },
13
+ "properties": {
14
+ "name": "Seattle",
15
+ "type": "city"
16
+ }
17
+ },
18
+ {
19
+ "type": "Feature",
20
+ "geometry": {
21
+ "type": "Point",
22
+ "coordinates": [
23
+ -122.6784,
24
+ 45.5152
25
+ ]
26
+ },
27
+ "properties": {
28
+ "name": "Portland",
29
+ "type": "city"
30
+ }
31
+ },
32
+ {
33
+ "type": "Feature",
34
+ "geometry": {
35
+ "type": "Point",
36
+ "coordinates": [
37
+ -122.4194,
38
+ 37.7749
39
+ ]
40
+ },
41
+ "properties": {
42
+ "name": "San Francisco",
43
+ "type": "city"
44
+ }
45
+ },
46
+ {
47
+ "type": "Feature",
48
+ "geometry": {
49
+ "type": "Point",
50
+ "coordinates": [
51
+ -118.2437,
52
+ 34.0522
53
+ ]
54
+ },
55
+ "properties": {
56
+ "name": "Los Angeles",
57
+ "type": "city"
58
+ }
59
+ },
60
+ {
61
+ "type": "Feature",
62
+ "geometry": {
63
+ "type": "Point",
64
+ "coordinates": [
65
+ -74.006,
66
+ 40.7128
67
+ ]
68
+ },
69
+ "properties": {
70
+ "name": "New York",
71
+ "type": "city"
72
+ }
73
+ },
74
+ {
75
+ "type": "Feature",
76
+ "geometry": {
77
+ "type": "LineString",
78
+ "coordinates": [
79
+ [
80
+ -122.3493,
81
+ 47.6205
82
+ ],
83
+ [
84
+ -122.6784,
85
+ 45.5152
86
+ ],
87
+ [
88
+ -122.4194,
89
+ 37.7749
90
+ ],
91
+ [
92
+ -118.2437,
93
+ 34.0522
94
+ ]
95
+ ]
96
+ },
97
+ "properties": {
98
+ "name": "West Coast Route",
99
+ "mode": "driving"
100
+ }
101
+ },
102
+ {
103
+ "type": "Feature",
104
+ "geometry": {
105
+ "type": "Point",
106
+ "coordinates": [
107
+ 2.3522,
108
+ 48.8566
109
+ ]
110
+ },
111
+ "properties": {}
112
+ },
113
+ {
114
+ "type": "Feature",
115
+ "geometry": {
116
+ "type": "Point",
117
+ "coordinates": [
118
+ -0.1278,
119
+ 51.5074
120
+ ]
121
+ },
122
+ "properties": {}
123
+ },
124
+ {
125
+ "type": "Feature",
126
+ "geometry": {
127
+ "type": "Polygon",
128
+ "coordinates": [
129
+ [
130
+ [
131
+ -123.0,
132
+ 48.0
133
+ ],
134
+ [
135
+ -121.0,
136
+ 48.0
137
+ ],
138
+ [
139
+ -121.0,
140
+ 46.0
141
+ ],
142
+ [
143
+ -123.0,
144
+ 46.0
145
+ ],
146
+ [
147
+ -123.0,
148
+ 48.0
149
+ ]
150
+ ]
151
+ ]
152
+ },
153
+ "properties": {
154
+ "name": "Pacific NW",
155
+ "region": true
156
+ }
157
+ },
158
+ {
159
+ "type": "Feature",
160
+ "geometry": {
161
+ "type": "Polygon",
162
+ "coordinates": [
163
+ [
164
+ [
165
+ -122.3493,
166
+ 47.710441151017214
167
+ ],
168
+ [
169
+ -122.32330338127136,
170
+ 47.708710028492575
171
+ ],
172
+ [
173
+ -122.29831079292721,
174
+ 47.70358352014371
175
+ ],
176
+ [
177
+ -122.27528691572327,
178
+ 47.69525958316883
179
+ ],
180
+ [
181
+ -122.25511936150583,
182
+ 47.684059519927274
183
+ ],
184
+ [
185
+ -122.23858413342101,
186
+ 47.67041541322468
187
+ ],
188
+ [
189
+ -122.22631565848305,
190
+ 47.654853266080394
191
+ ],
192
+ [
193
+ -122.21878256554382,
194
+ 47.63797253321124
195
+ ],
196
+ [
197
+ -122.21627011382594,
198
+ 47.62042286978415
199
+ ],
200
+ [
201
+ -122.21886987894307,
202
+ 47.602879023582524
203
+ ],
204
+ [
205
+ -122.2264769927538,
206
+ 47.586014856783315
207
+ ],
208
+ [
209
+ -122.23879492708923,
210
+ 47.57047750255195
211
+ ],
212
+ [
213
+ -122.25534752329217,
214
+ 47.5568626411414
215
+ ],
216
+ [
217
+ -122.27549771006404,
218
+ 47.54569182325386
219
+ ],
220
+ [
221
+ -122.2984721281491,
222
+ 47.53739267934147
223
+ ],
224
+ [
225
+ -122.32339069534318,
226
+ 47.532282737212164
227
+ ],
228
+ [
229
+ -122.3493,
230
+ 47.53055743197494
231
+ ],
232
+ [
233
+ -122.37520930465682,
234
+ 47.532282737212164
235
+ ],
236
+ [
237
+ -122.4001278718509,
238
+ 47.53739267934147
239
+ ],
240
+ [
241
+ -122.42310228993595,
242
+ 47.54569182325386
243
+ ],
244
+ [
245
+ -122.44325247670783,
246
+ 47.55686264114141
247
+ ],
248
+ [
249
+ -122.45980507291077,
250
+ 47.57047750255195
251
+ ],
252
+ [
253
+ -122.4721230072462,
254
+ 47.58601485678331
255
+ ],
256
+ [
257
+ -122.47973012105693,
258
+ 47.60287902358252
259
+ ],
260
+ [
261
+ -122.48232988617406,
262
+ 47.62042286978415
263
+ ],
264
+ [
265
+ -122.47981743445618,
266
+ 47.63797253321123
267
+ ],
268
+ [
269
+ -122.47228434151695,
270
+ 47.6548532660804
271
+ ],
272
+ [
273
+ -122.46001586657898,
274
+ 47.670415413224674
275
+ ],
276
+ [
277
+ -122.44348063849417,
278
+ 47.684059519927274
279
+ ],
280
+ [
281
+ -122.42331308427673,
282
+ 47.69525958316883
283
+ ],
284
+ [
285
+ -122.40028920707279,
286
+ 47.70358352014371
287
+ ],
288
+ [
289
+ -122.37529661872864,
290
+ 47.708710028492575
291
+ ],
292
+ [
293
+ -122.3493,
294
+ 47.710441151017214
295
+ ]
296
+ ]
297
+ ]
298
+ },
299
+ "properties": {
300
+ "name": "Seattle Metro",
301
+ "radius_km": 10
302
+ }
303
+ }
304
+ ]
305
+ }
@@ -101,6 +101,8 @@ module Geodetic
101
101
  # ---------------------------------------------------------------
102
102
 
103
103
  class << self
104
+ # --- Export helpers ---
105
+
104
106
  def position(lla)
105
107
  if lla.alt != 0.0
106
108
  [lla.lng, lla.lat, lla.alt]
@@ -120,6 +122,93 @@ module Geodetic
120
122
  def polygon_hash(rings)
121
123
  { "type" => "Polygon", "coordinates" => rings.map { |ring| ring.map { |p| position(p) } } }
122
124
  end
125
+
126
+ # --- Import ---
127
+
128
+ def load(path)
129
+ data = JSON.parse(File.read(path))
130
+ parse(data)
131
+ end
132
+
133
+ def parse(data)
134
+ case data["type"]
135
+ when "FeatureCollection"
136
+ data["features"].flat_map { |f| parse(f) }
137
+ when "Feature"
138
+ parse_feature(data)
139
+ when "GeometryCollection"
140
+ data["geometries"].flat_map { |g| parse_geometry(g) }
141
+ else
142
+ [parse_geometry(data)]
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ def parse_feature(data)
149
+ geometry = parse_geometry(data["geometry"])
150
+ properties = data["properties"] || {}
151
+
152
+ results = geometry.is_a?(Array) ? geometry : [geometry]
153
+
154
+ results.map do |geom|
155
+ name = properties["name"]
156
+ metadata = properties.reject { |k, _| k == "name" }
157
+
158
+ if name || !metadata.empty?
159
+ Feature.new(
160
+ label: name,
161
+ geometry: geom,
162
+ metadata: metadata.transform_keys(&:to_sym)
163
+ )
164
+ else
165
+ geom
166
+ end
167
+ end
168
+ end
169
+
170
+ def parse_geometry(data)
171
+ case data["type"]
172
+ when "Point"
173
+ parse_point(data["coordinates"])
174
+ when "LineString"
175
+ parse_line_string(data["coordinates"])
176
+ when "Polygon"
177
+ parse_polygon(data["coordinates"])
178
+ when "MultiPoint"
179
+ data["coordinates"].map { |pos| parse_point(pos) }
180
+ when "MultiLineString"
181
+ data["coordinates"].map { |coords| parse_line_string(coords) }
182
+ when "MultiPolygon"
183
+ data["coordinates"].map { |rings| parse_polygon(rings) }
184
+ when "GeometryCollection"
185
+ data["geometries"].flat_map { |g| parse_geometry(g) }
186
+ else
187
+ raise ArgumentError, "unknown GeoJSON geometry type: #{data["type"]}"
188
+ end
189
+ end
190
+
191
+ def parse_point(coords)
192
+ lng, lat = coords[0], coords[1]
193
+ alt = coords[2] || 0.0
194
+ Coordinate::LLA.new(lat: lat, lng: lng, alt: alt)
195
+ end
196
+
197
+ def parse_line_string(coords)
198
+ points = coords.map { |pos| parse_point(pos) }
199
+ if points.length == 2
200
+ Segment.new(points[0], points[1])
201
+ else
202
+ Path.new(coordinates: points)
203
+ end
204
+ end
205
+
206
+ def parse_polygon(rings)
207
+ outer = rings[0].map { |pos| parse_point(pos) }
208
+ # Remove closing point if it duplicates the first (Polygon#initialize adds it)
209
+ outer.pop if outer.length > 1 && outer.first == outer.last
210
+ Areas::Polygon.new(boundary: outer)
211
+ end
123
212
  end
124
213
 
125
214
  # ---------------------------------------------------------------
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Geodetic
4
- VERSION = "0.5.1"
4
+ VERSION = "0.5.2"
5
5
  end
data/mkdocs.yml CHANGED
@@ -132,6 +132,13 @@ nav:
132
132
  - UPS: coordinate-systems/ups.md
133
133
  - State Plane: coordinate-systems/state-plane.md
134
134
  - BNG: coordinate-systems/bng.md
135
+ - GH36: coordinate-systems/gh36.md
136
+ - GH: coordinate-systems/gh.md
137
+ - HAM: coordinate-systems/ham.md
138
+ - OLC: coordinate-systems/olc.md
139
+ - GEOREF: coordinate-systems/georef.md
140
+ - GARS: coordinate-systems/gars.md
141
+ - H3: coordinate-systems/h3.md
135
142
  - Reference:
136
143
  - Datums: reference/datums.md
137
144
  - Geoid Height: reference/geoid-height.md
@@ -141,4 +148,7 @@ nav:
141
148
  - Feature: reference/feature.md
142
149
  - Serialization: reference/serialization.md
143
150
  - Conversions: reference/conversions.md
151
+ - Vector: reference/vector.md
152
+ - Arithmetic: reference/arithmetic.md
153
+ - GeoJSON Export: reference/geojson.md
144
154
  - Map Rendering: reference/map-rendering.md
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: geodetic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -79,6 +79,7 @@ files:
79
79
  - examples/08_geodetic_arithmetic.rb
80
80
  - examples/09_geojson_export.rb
81
81
  - examples/README.md
82
+ - examples/geodetic_demo.geojson
82
83
  - fiddle_pointer_buffer_pool.md
83
84
  - lib/geodetic.rb
84
85
  - lib/geodetic/areas.rb