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
@@ -76,12 +76,12 @@ module Geodetic
76
76
  VALID_LENGTHS.include?(@code.length) && valid_characters?(@code)
77
77
  end
78
78
 
79
+ protected
80
+
79
81
  def code_value
80
82
  @code
81
83
  end
82
84
 
83
- protected
84
-
85
85
  def normalize(string)
86
86
  result = String.new(capacity: string.length)
87
87
  string.each_char.with_index do |ch, i|
@@ -227,7 +227,6 @@ module Geodetic
227
227
  end
228
228
 
229
229
  register_hash_system(:gars, self, default_precision: 7)
230
- Coordinate.register_class(self)
231
230
  end
232
231
  end
233
232
  end
@@ -55,12 +55,12 @@ module Geodetic
55
55
  VALID_LENGTHS.include?(@code.length) && valid_characters?(@code)
56
56
  end
57
57
 
58
+ protected
59
+
58
60
  def code_value
59
61
  @code
60
62
  end
61
63
 
62
- protected
63
-
64
64
  def normalize(string)
65
65
  string.upcase
66
66
  end
@@ -198,7 +198,6 @@ module Geodetic
198
198
  end
199
199
 
200
200
  register_hash_system(:georef, self, default_precision: 8)
201
- Coordinate.register_class(self)
202
201
  end
203
202
  end
204
203
  end
@@ -39,13 +39,12 @@ module Geodetic
39
39
  @geohash.length > 0 && @geohash.each_char.all? { |c| CHAR_INDEX.key?(c) }
40
40
  end
41
41
 
42
- # Expose code_value for base class equality and other shared methods
42
+ protected
43
+
43
44
  def code_value
44
45
  @geohash
45
46
  end
46
47
 
47
- protected
48
-
49
48
  def normalize(string)
50
49
  string.downcase
51
50
  end
@@ -155,7 +154,6 @@ module Geodetic
155
154
  alias_method :validate_code!, :validate_geohash!
156
155
 
157
156
  register_hash_system(:gh, self, default_precision: 12)
158
- Coordinate.register_class(self)
159
157
  end
160
158
  end
161
159
  end
@@ -74,10 +74,6 @@ module Geodetic
74
74
  @geohash.length > 0 && @geohash.each_char.all? { |c| VALID_CHARS_SET.include?(c) }
75
75
  end
76
76
 
77
- def code_value
78
- @geohash
79
- end
80
-
81
77
  # --- GH36-specific overrides (matrix-based algorithms) ---
82
78
 
83
79
  # Uses recursive matrix-based neighbor calculation instead of bounds-based
@@ -105,6 +101,10 @@ module Geodetic
105
101
 
106
102
  protected
107
103
 
104
+ def code_value
105
+ @geohash
106
+ end
107
+
108
108
  def set_code(value)
109
109
  @geohash = value
110
110
  end
@@ -243,7 +243,6 @@ module Geodetic
243
243
  alias_method :validate_code!, :validate_geohash!
244
244
 
245
245
  register_hash_system(:gh36, self, default_precision: 10)
246
- Coordinate.register_class(self)
247
246
  end
248
247
  end
249
248
  end
@@ -358,12 +358,12 @@ module Geodetic
358
358
  end
359
359
  end
360
360
 
361
+ protected
362
+
361
363
  def code_value
362
364
  @code
363
365
  end
364
366
 
365
- protected
366
-
367
367
  def normalize(string)
368
368
  string.downcase.delete_prefix('0x')
369
369
  end
@@ -407,7 +407,6 @@ module Geodetic
407
407
  alias_method :validate_code!, :validate_h3!
408
408
 
409
409
  register_hash_system(:h3, self, default_precision: 7)
410
- Coordinate.register_class(self)
411
410
  end
412
411
  end
413
412
  end
@@ -74,12 +74,12 @@ module Geodetic
74
74
  valid_characters?(@locator)
75
75
  end
76
76
 
77
+ protected
78
+
77
79
  def code_value
78
80
  @locator
79
81
  end
80
82
 
81
- protected
82
-
83
83
  def normalize(string)
84
84
  result = String.new(capacity: string.length)
85
85
  string.each_char.with_index do |ch, i|
@@ -220,7 +220,6 @@ module Geodetic
220
220
  end
221
221
 
222
222
  register_hash_system(:ham, self, default_precision: 6)
223
- Coordinate.register_class(self)
224
223
  end
225
224
  end
226
225
  end
@@ -305,6 +305,12 @@ module Geodetic
305
305
  new(lat: parts[0].to_f, lng: parts[1].to_f, alt: parts[2].to_f)
306
306
  end
307
307
 
308
+ def valid?
309
+ @lat.finite? && @lng.finite? && @alt.finite? &&
310
+ @lat >= -90 && @lat <= 90 &&
311
+ @lng >= -180 && @lng <= 180
312
+ end
313
+
308
314
  def ==(other)
309
315
  return false unless other.is_a?(LLA)
310
316
 
@@ -325,7 +331,7 @@ module Geodetic
325
331
  raise ArgumentError, "Longitude must be between -180 and 180 degrees" if @lng < -180 || @lng > 180
326
332
  end
327
333
 
328
- Coordinate.register_class(self)
334
+ Coordinate.register_class(self, hash_conversion_style: :no_datum)
329
335
  end
330
336
  end
331
337
  end
@@ -305,7 +305,7 @@ module Geodetic
305
305
  bands[index]
306
306
  end
307
307
 
308
- Coordinate.register_class(self)
308
+ Coordinate.register_class(self, hash_conversion_style: :with_datum_and_precision)
309
309
  end
310
310
  end
311
311
  end
@@ -224,6 +224,10 @@ module Geodetic
224
224
  new(n: parts[0].to_f, e: parts[1].to_f, d: parts[2].to_f)
225
225
  end
226
226
 
227
+ def valid?
228
+ @n.finite? && @e.finite? && @d.finite?
229
+ end
230
+
227
231
  def ==(other)
228
232
  return false unless other.is_a?(NED)
229
233
 
@@ -267,7 +267,6 @@ module Geodetic
267
267
  end
268
268
 
269
269
  register_hash_system(:olc, self, default_precision: 10)
270
- Coordinate.register_class(self)
271
270
  end
272
271
  end
273
272
  end
@@ -12,7 +12,7 @@
12
12
  # decode_bounds(code_string) → { min_lat:, max_lat:, min_lng:, max_lng: }
13
13
  # validate_code!(string) → raises ArgumentError or nil
14
14
  # set_code(normalized_string) → sets the internal ivar (@geohash, @code, etc.)
15
- # code_value → returns the internal ivar
15
+ # code_value → returns the internal ivar (protected)
16
16
  # self.default_precision → Integer
17
17
  # self.hash_system_name → Symbol (:gh, :gh36, :ham, :olc)
18
18
  #
@@ -52,6 +52,7 @@ module Geodetic
52
52
  class_name: klass.name.split('::').last,
53
53
  default_precision: default_precision
54
54
  }
55
+ Coordinate.register_class(klass)
55
56
  end
56
57
 
57
58
  # Called once after all spatial hash subclasses are loaded.
@@ -285,7 +286,6 @@ module Geodetic
285
286
  code_value == other.code_value
286
287
  end
287
288
 
288
- alias_method :to_slug, :to_s
289
289
 
290
290
  # Returns all 8 neighboring cells
291
291
  # Keys: :N, :S, :E, :W, :NE, :NW, :SE, :SW
@@ -312,7 +312,7 @@ module Geodetic
312
312
  end
313
313
  end
314
314
 
315
- Coordinate.register_class(self)
315
+ Coordinate.register_class(self, hash_conversion_style: :with_datum)
316
316
  end
317
317
  end
318
318
  end
@@ -262,7 +262,7 @@ module Geodetic
262
262
  end
263
263
  end
264
264
 
265
- Coordinate.register_class(self)
265
+ Coordinate.register_class(self, hash_conversion_style: :with_datum_and_precision)
266
266
  end
267
267
  end
268
268
  end
@@ -249,7 +249,7 @@ module Geodetic
249
249
  raise ArgumentError, "Northing must be positive" if @northing < 0
250
250
  end
251
251
 
252
- Coordinate.register_class(self)
252
+ Coordinate.register_class(self, hash_conversion_style: :no_datum)
253
253
  end
254
254
  end
255
255
  end
@@ -230,7 +230,7 @@ module Geodetic
230
230
  }
231
231
  end
232
232
 
233
- Coordinate.register_class(self)
233
+ Coordinate.register_class(self, hash_conversion_style: :no_datum)
234
234
  end
235
235
  end
236
236
  end
@@ -3,13 +3,39 @@
3
3
  module Geodetic
4
4
  module Coordinate
5
5
  # Registry for coordinate classes — each class registers itself at load time
6
+ # Each entry is [klass, options_hash] where options may include :hash_conversion_style
6
7
  @registered_classes = []
8
+ @finalized = false
7
9
 
8
10
  class << self
9
11
  attr_reader :registered_classes
10
12
 
11
- def register_class(klass)
12
- @registered_classes << klass
13
+ def systems
14
+ @registered_classes.map(&:first).freeze
15
+ end
16
+
17
+ def register_class(klass, hash_conversion_style: nil)
18
+ @registered_classes << [klass, { hash_conversion_style: hash_conversion_style }]
19
+ end
20
+
21
+ def finalize!
22
+ raise "Geodetic::Coordinate already finalized!" if @finalized
23
+ @finalized = true
24
+
25
+ # Phase 1: Generate cross-hash conversion methods between spatial hash subclasses
26
+ SpatialHash.finalize_cross_hash_conversions!
27
+
28
+ # Phase 2: Generate hash conversion methods on non-hash coordinate classes
29
+ @registered_classes.each do |klass, opts|
30
+ next unless opts[:hash_conversion_style]
31
+ SpatialHash.generate_hash_conversions_for(klass, style: opts[:hash_conversion_style])
32
+ end
33
+
34
+ # Phase 3: Include distance/bearing mixins in all registered coordinate classes
35
+ @registered_classes.each do |klass, _opts|
36
+ klass.include(DistanceMethods)
37
+ klass.include(BearingMethods)
38
+ end
13
39
  end
14
40
  end
15
41
  end
@@ -257,27 +283,5 @@ module Geodetic
257
283
  end
258
284
  end
259
285
 
260
- # Generate cross-hash conversion methods (to_gh, to_ham, etc.) between all spatial hash subclasses
261
- Geodetic::Coordinate::SpatialHash.finalize_cross_hash_conversions!
262
-
263
- # Generate hash conversion methods (to_gh, from_gh, etc.) on non-hash coordinate classes
264
- sh = Geodetic::Coordinate::SpatialHash
265
- sh.generate_hash_conversions_for(Geodetic::Coordinate::LLA, style: :no_datum)
266
- sh.generate_hash_conversions_for(Geodetic::Coordinate::ECEF, style: :no_datum)
267
- sh.generate_hash_conversions_for(Geodetic::Coordinate::UTM, style: :no_datum)
268
- sh.generate_hash_conversions_for(Geodetic::Coordinate::WebMercator, style: :no_datum)
269
- sh.generate_hash_conversions_for(Geodetic::Coordinate::BNG, style: :with_datum)
270
- sh.generate_hash_conversions_for(Geodetic::Coordinate::UPS, style: :with_datum)
271
- sh.generate_hash_conversions_for(Geodetic::Coordinate::MGRS, style: :with_datum_and_precision)
272
- sh.generate_hash_conversions_for(Geodetic::Coordinate::USNG, style: :with_datum_and_precision)
273
-
274
- # Include distance/bearing mixins in all registered coordinate classes
275
- ALL_COORD_CLASSES = Geodetic::Coordinate.registered_classes.freeze
276
-
277
- ALL_COORD_CLASSES.each do |klass|
278
- klass.include(Geodetic::Coordinate::DistanceMethods)
279
- klass.include(Geodetic::Coordinate::BearingMethods)
280
- end
281
-
282
- # GCS is a convenience alias for Geodetic::Coordinate, available after require "geodetic"
283
- GCS = Geodetic::Coordinate
286
+ # All classes loaded and registered finalize conversions and mixins
287
+ Geodetic::Coordinate.finalize!
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geodetic
4
+ class Feature
5
+ attr_accessor :label, :geometry, :metadata
6
+
7
+ def initialize(label:, geometry:, metadata: {})
8
+ @label = label
9
+ @geometry = geometry
10
+ @metadata = metadata
11
+ end
12
+
13
+ def distance_to(other)
14
+ if @geometry.is_a?(Path)
15
+ @geometry.distance_to(other)
16
+ else
17
+ resolve_point.distance_to(resolve_point_from(other))
18
+ end
19
+ end
20
+
21
+ def bearing_to(other)
22
+ if @geometry.is_a?(Path)
23
+ @geometry.bearing_to(other)
24
+ else
25
+ resolve_point.bearing_to(resolve_point_from(other))
26
+ end
27
+ end
28
+
29
+ def to_s
30
+ "#{@label} (#{@geometry})"
31
+ end
32
+
33
+ def inspect
34
+ "#<Geodetic::Feature name=#{@label.inspect} geometry=#{@geometry.inspect} metadata=#{@metadata.inspect}>"
35
+ end
36
+
37
+ private
38
+
39
+ def resolve_point
40
+ @geometry.respond_to?(:centroid) ? @geometry.centroid : @geometry
41
+ end
42
+
43
+ def resolve_point_from(other)
44
+ case other
45
+ when Feature
46
+ other.send(:resolve_point)
47
+ else
48
+ other.respond_to?(:centroid) ? other.centroid : other
49
+ end
50
+ end
51
+ end
52
+ end
@@ -107,14 +107,14 @@ module Geodetic
107
107
  raise ArgumentError, "Unknown vertical datum: #{to_datum}" unless to_info
108
108
 
109
109
  if from_info[:type] == 'orthometric'
110
- geoid_model = GeoidHeight.new(geoid_model: from_info[:reference_geoid])
110
+ geoid_model = GeoidHeight.for(from_info[:reference_geoid])
111
111
  ellipsoidal_height = geoid_model.orthometric_to_ellipsoidal(lat, lng, height)
112
112
  else
113
113
  ellipsoidal_height = height
114
114
  end
115
115
 
116
116
  if to_info[:type] == 'orthometric'
117
- geoid_model = GeoidHeight.new(geoid_model: to_info[:reference_geoid])
117
+ geoid_model = GeoidHeight.for(to_info[:reference_geoid])
118
118
  geoid_model.ellipsoidal_to_orthometric(lat, lng, ellipsoidal_height)
119
119
  else
120
120
  ellipsoidal_height
@@ -175,6 +175,11 @@ module Geodetic
175
175
  end
176
176
  end
177
177
 
178
+ def self.for(geoid_model = 'EGM2008')
179
+ @instances ||= {}
180
+ @instances[geoid_model] ||= new(geoid_model: geoid_model)
181
+ end
182
+
178
183
  def self.available_models
179
184
  GEOID_MODELS.keys
180
185
  end
@@ -271,7 +276,7 @@ module Geodetic
271
276
  def convert_height_datum(from_datum, to_datum, geoid_model = 'EGM2008')
272
277
  return self unless respond_to?(:lat) && respond_to?(:lng) && respond_to?(:alt)
273
278
 
274
- geoid = GeoidHeight.new(geoid_model: geoid_model)
279
+ geoid = GeoidHeight.for(geoid_model)
275
280
  new_height = geoid.convert_vertical_datum(self.lat, self.lng, self.alt, from_datum, to_datum)
276
281
 
277
282
  self.class.new(lat: self.lat, lng: self.lng, alt: new_height)
@@ -280,19 +285,19 @@ module Geodetic
280
285
  def geoid_height(geoid_model = 'EGM2008')
281
286
  return nil unless respond_to?(:lat) && respond_to?(:lng)
282
287
 
283
- geoid = GeoidHeight.new(geoid_model: geoid_model)
288
+ geoid = GeoidHeight.for(geoid_model)
284
289
  geoid.geoid_height_at(self.lat, self.lng)
285
290
  end
286
291
 
287
292
  def orthometric_height(geoid_model = 'EGM2008')
288
293
  return nil unless respond_to?(:alt) && respond_to?(:lat) && respond_to?(:lng)
289
294
 
290
- geoid = GeoidHeight.new(geoid_model: geoid_model)
295
+ geoid = GeoidHeight.for(geoid_model)
291
296
  geoid.ellipsoidal_to_orthometric(self.lat, self.lng, self.alt)
292
297
  end
293
298
 
294
299
  def self.from_orthometric_height(lat, lng, orthometric_height, geoid_model = 'EGM2008')
295
- geoid = GeoidHeight.new(geoid_model: geoid_model)
300
+ geoid = GeoidHeight.for(geoid_model)
296
301
  geoid.orthometric_to_ellipsoidal(lat, lng, orthometric_height)
297
302
  end
298
303
  end