libgd-gis 0.2.9 → 0.3.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.
@@ -1,20 +1,56 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module GD
2
4
  module GIS
5
+ # Coordinate Reference System (CRS) helpers and constants.
6
+ #
7
+ # This namespace defines common CRS identifiers and utilities
8
+ # used for normalizing coordinate order and projection.
9
+ #
3
10
  module CRS
11
+ # OGC CRS84 (longitude, latitude)
4
12
  CRS84 = "urn:ogc:def:crs:OGC:1.3:CRS84"
13
+
14
+ # EPSG:4326 (latitude, longitude axis order)
5
15
  EPSG4326 = "EPSG:4326"
16
+
17
+ # EPSG:3857 (Web Mercator)
6
18
  EPSG3857 = "EPSG:3857"
7
19
 
20
+ # Normalizes coordinates from different CRS definitions
21
+ # into a consistent [longitude, latitude] order.
22
+ #
23
+ # This class handles:
24
+ # - Axis order normalization (e.g. EPSG:4326)
25
+ # - Web Mercator (EPSG:3857) to WGS84 conversion
26
+ # - Flexible input formats
27
+ #
28
+ # The output is always expressed as:
29
+ # [longitude, latitude] in degrees
30
+ #
8
31
  class Normalizer
32
+ # Creates a new CRS normalizer.
33
+ #
34
+ # @param crs [String, Symbol, nil]
35
+ # CRS identifier (e.g. "EPSG:4326", "EPSG:3857", CRS84)
9
36
  def initialize(crs)
10
37
  @crs = normalize_name(crs)
11
38
  end
12
39
 
13
- # Accepts:
14
- # normalize(lon,lat)
15
- # normalize(lon,lat,z)
16
- # normalize([lon,lat])
17
- # normalize([lon,lat,z])
40
+ # Normalizes coordinates into [longitude, latitude].
41
+ #
42
+ # Accepted input forms:
43
+ #
44
+ # normalize(lon, lat)
45
+ # normalize(lon, lat, z)
46
+ # normalize([lon, lat])
47
+ # normalize([lon, lat, z])
48
+ #
49
+ # Extra dimensions (e.g. Z) are ignored.
50
+ #
51
+ # @param args [Array<Float, Array<Float>>]
52
+ # @return [Array<Float>, nil]
53
+ # normalized [lon, lat] or nil if input is invalid
18
54
  def normalize(*args)
19
55
  lon, lat = args.flatten
20
56
  return nil if lon.nil? || lat.nil?
@@ -34,21 +70,31 @@ module GD
34
70
  mercator_to_wgs84(lon, lat)
35
71
 
36
72
  else
37
- [lon, lat]
73
+ raise ArgumentError, "Unsupported CRS: #{@crs}"
38
74
  end
39
75
  end
40
76
 
41
77
  private
42
78
 
79
+ # Normalizes a CRS name into a comparable string.
80
+ #
81
+ # @param name [Object]
82
+ # @return [String, nil]
43
83
  def normalize_name(name)
44
84
  return nil if name.nil?
85
+
45
86
  name.to_s.strip
46
87
  end
47
88
 
89
+ # Converts Web Mercator coordinates to WGS84.
90
+ #
91
+ # @param x [Float] X coordinate (meters)
92
+ # @param y [Float] Y coordinate (meters)
93
+ # @return [Array<Float>] [longitude, latitude] in degrees
48
94
  def mercator_to_wgs84(x, y)
49
95
  r = 6378137.0
50
96
  lon = (x / r) * 180.0 / Math::PI
51
- lat = (2 * Math.atan(Math.exp(y / r)) - Math::PI / 2) * 180.0 / Math::PI
97
+ lat = ((2 * Math.atan(Math.exp(y / r))) - (Math::PI / 2)) * 180.0 / Math::PI
52
98
  [lon, lat]
53
99
  end
54
100
  end
@@ -1,17 +1,62 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module GD
2
4
  module GIS
5
+ # Represents a single geographic feature with geometry and properties.
6
+ #
7
+ # A Feature acts as the rendering bridge between:
8
+ # - GeoJSON-like geometry data
9
+ # - Attribute properties
10
+ # - GD drawing primitives
11
+ #
12
+ # Features are responsible for drawing themselves onto a GD image
13
+ # using a provided projection and styling information.
14
+ #
3
15
  class Feature
4
- attr_reader :geometry, :properties, :layer
5
-
16
+ # @return [Hash] GeoJSON geometry object
17
+ attr_reader :geometry
18
+
19
+ # @return [Hash] feature properties (tags)
20
+ attr_reader :properties
21
+
22
+ # @return [Symbol, nil] logical layer identifier
23
+ attr_reader :layer
24
+
25
+ # Creates a new feature.
26
+ #
27
+ # @param geometry [Hash]
28
+ # GeoJSON-like geometry hash (type + coordinates)
29
+ # @param properties [Hash, nil]
30
+ # feature attributes / tags
31
+ # @param layer [Symbol, nil]
32
+ # optional logical layer identifier
6
33
  def initialize(geometry, properties, layer = nil)
7
34
  @geometry = geometry
8
35
  @properties = properties || {}
9
36
  @layer = layer
10
37
  end
11
38
 
12
- # -------------------------------------------------
13
- # Main draw entry point
14
- # -------------------------------------------------
39
+ # Draws the feature onto a GD image.
40
+ #
41
+ # This is the main rendering entry point and dispatches
42
+ # to the appropriate drawing method based on geometry type
43
+ # and layer styling.
44
+ #
45
+ # Supported geometry types:
46
+ # - Polygon
47
+ # - MultiPolygon
48
+ # - LineString
49
+ # - MultiLineString
50
+ #
51
+ # @param img [GD::Image] target image
52
+ # @param projection [#call]
53
+ # callable object converting (lon, lat) → (x, y)
54
+ # @param color [GD::Color, nil] base color
55
+ # @param width [Integer] stroke width
56
+ # @param layer [Symbol, Hash, nil]
57
+ # layer identifier or style hash
58
+ #
59
+ # @return [void]
15
60
  def draw(img, projection, color, width, layer = nil)
16
61
  case geometry["type"]
17
62
  when "Polygon"
@@ -37,43 +82,58 @@ module GD
37
82
  end
38
83
  end
39
84
 
40
- # -------------------------------------------------
41
- # Styled polygon rendering (fill + stroke)
42
- # -------------------------------------------------
85
+ # Draws a polygon with fill and stroke styling.
86
+ #
87
+ # @param img [GD::Image]
88
+ # @param projection [#call]
89
+ # @param rings [Array]
90
+ # polygon rings (GeoJSON format)
91
+ # @param style [Hash]
92
+ # style hash with :fill and/or :stroke RGB arrays
93
+ #
94
+ # @return [void]
43
95
  def draw_polygon_styled(img, projection, rings, style)
44
96
  fill = style[:fill] ? GD::Color.rgb(*style[:fill]) : nil
45
97
  stroke = style[:stroke] ? GD::Color.rgb(*style[:stroke]) : nil
46
98
 
47
99
  rings.each do |ring|
48
- pts = ring.map do |lon,lat|
49
- x,y = projection.call(lon,lat)
100
+ pts = ring.filter_map do |lon, lat|
101
+ x, y = projection.call(lon, lat)
50
102
  next if x.nil? || y.nil?
103
+
51
104
  [x.to_i, y.to_i]
52
- end.compact
105
+ end
53
106
 
54
- pts = pts.chunk_while { |a,b| a == b }.map(&:first)
107
+ pts = pts.chunk_while { |a, b| a == b }.map(&:first)
55
108
  next if pts.length < 3
56
109
 
57
110
  img.filled_polygon(pts, fill) if fill
58
111
 
59
112
  if stroke
60
- pts.each_cons(2) { |a,b| img.line(a[0],a[1], b[0],b[1], stroke) }
113
+ pts.each_cons(2) { |a, b| img.line(a[0], a[1], b[0], b[1], stroke) }
61
114
  img.line(pts.last[0], pts.last[1], pts.first[0], pts.first[1], stroke)
62
115
  end
63
116
  end
64
117
  end
65
118
 
66
- # -------------------------------------------------
67
- # Polygon outline (used for water)
68
- # -------------------------------------------------
119
+ # Draws only the outline of a polygon.
120
+ #
121
+ # Used primarily for water bodies.
122
+ #
123
+ # @param img [GD::Image]
124
+ # @param projection [#call]
125
+ # @param rings [Array]
126
+ # @param color [GD::Color]
127
+ # @param width [Integer]
128
+ # @return [void]
69
129
  def draw_polygon_outline(img, projection, rings, color, width)
70
130
  return if color.nil?
71
131
 
72
132
  rings.each do |ring|
73
- pts = ring.map do |lon, lat|
133
+ pts = ring.filter_map do |lon, lat|
74
134
  x, y = projection.call(lon, lat)
75
135
  [x.to_i, y.to_i] if x && y
76
- end.compact
136
+ end
77
137
 
78
138
  next if pts.size < 2
79
139
 
@@ -81,29 +141,39 @@ module GD
81
141
  end
82
142
  end
83
143
 
84
- # -------------------------------------------------
85
- # Legacy filled polygon (single color)
86
- # -------------------------------------------------
144
+ # Draws a filled polygon using a single color.
145
+ #
146
+ # @param img [GD::Image]
147
+ # @param projection [#call]
148
+ # @param rings [Array]
149
+ # @param color [GD::Color]
150
+ # @return [void]
87
151
  def draw_polygon(img, projection, rings, color)
88
152
  return if color.nil?
89
153
 
90
154
  rings.each do |ring|
91
- pts = ring.map do |lon,lat|
92
- x,y = projection.call(lon,lat)
155
+ pts = ring.filter_map do |lon, lat|
156
+ x, y = projection.call(lon, lat)
93
157
  next if x.nil? || y.nil?
158
+
94
159
  [x.to_i, y.to_i]
95
- end.compact
160
+ end
96
161
 
97
- pts = pts.chunk_while { |a,b| a == b }.map(&:first)
162
+ pts = pts.chunk_while { |a, b| a == b }.map(&:first)
98
163
  next if pts.length < 3
99
164
 
100
165
  img.filled_polygon(pts, color)
101
166
  end
102
167
  end
103
168
 
104
- # -------------------------------------------------
105
- # Lines
106
- # -------------------------------------------------
169
+ # Draws line or multiline geometries.
170
+ #
171
+ # @param img [GD::Image]
172
+ # @param projection [#call]
173
+ # @param coords [Array]
174
+ # @param color [GD::Color]
175
+ # @param width [Integer]
176
+ # @return [void]
107
177
  def draw_lines(img, projection, coords, color, width)
108
178
  return if color.nil?
109
179
 
@@ -114,23 +184,36 @@ module GD
114
184
  end
115
185
  end
116
186
 
187
+ # Draws a single line geometry.
188
+ #
189
+ # @param img [GD::Image]
190
+ # @param projection [#call]
191
+ # @param coords [Array]
192
+ # @param color [GD::Color]
193
+ # @param width [Integer]
194
+ # @return [void]
117
195
  def draw_line(img, projection, coords, color, width)
118
196
  return if color.nil?
119
197
 
120
- coords.each_cons(2) do |(lon1,lat1),(lon2,lat2)|
121
- x1,y1 = projection.call(lon1,lat1)
122
- x2,y2 = projection.call(lon2,lat2)
198
+ coords.each_cons(2) do |(lon1, lat1), (lon2, lat2)|
199
+ x1, y1 = projection.call(lon1, lat1)
200
+ x2, y2 = projection.call(lon2, lat2)
123
201
  img.line(x1, y1, x2, y2, color, thickness: width)
124
202
  end
125
203
  end
126
204
 
127
- # -------------------------------------------------
128
- # Metadata helpers
129
- # -------------------------------------------------
205
+ # Returns the display label for the feature.
206
+ #
207
+ # Prefers Japanese names if present.
208
+ #
209
+ # @return [String, nil]
130
210
  def label
131
211
  properties["name:ja"] || properties["name"]
132
212
  end
133
213
 
214
+ # Computes a simple centroid for line-based geometries.
215
+ #
216
+ # @return [Array<Float>, nil] [lon, lat] or nil
134
217
  def centroid
135
218
  pts = []
136
219
 
@@ -143,8 +226,8 @@ module GD
143
226
 
144
227
  return nil if pts.empty?
145
228
 
146
- lon = pts.map(&:first).sum / pts.size
147
- lat = pts.map(&:last).sum / pts.size
229
+ lon = pts.sum(&:first) / pts.size
230
+ lat = pts.sum(&:last) / pts.size
148
231
 
149
232
  [lon, lat]
150
233
  end
@@ -0,0 +1,33 @@
1
+ module GD
2
+ module GIS
3
+ module FontHelper
4
+ PATHS = [
5
+ "/usr/share/fonts",
6
+ "/usr/local/share/fonts",
7
+ File.expand_path("~/.fonts")
8
+ ].freeze
9
+
10
+ EXTENSIONS = %w[ttf otf ttc].freeze
11
+
12
+ def self.all
13
+ @all ||= PATHS.flat_map do |path|
14
+ next [] unless Dir.exist?(path)
15
+
16
+ EXTENSIONS.flat_map do |ext|
17
+ Dir.glob("#{path}/**/*.#{ext}")
18
+ end
19
+ end.compact.uniq
20
+ end
21
+
22
+ def self.random
23
+ all.sample or raise "GD::GIS::FontHelper: no fonts found on system"
24
+ end
25
+
26
+ def self.find(name)
27
+ all.find do |f|
28
+ File.basename(f).downcase.include?(name.downcase)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,54 +1,101 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module GD
2
4
  module GIS
5
+ # Geometry and projection helpers for Web Mercator maps.
6
+ #
7
+ # This module provides low-level utilities for:
8
+ # - Web Mercator (EPSG:3857) projection math
9
+ # - Bounding box manipulation
10
+ # - Viewport fitting
11
+ # - Simple geometric operations for visualization
12
+ #
13
+ # All longitude/latitude values are assumed to be in WGS84
14
+ # (EPSG:4326), unless explicitly stated otherwise.
15
+ #
16
+ # ⚠️ These helpers are intended for *rendering and visualization*,
17
+ # not for precise geospatial analysis.
18
+ #
3
19
  module Geometry
4
-
20
+ # Web Mercator tile size in pixels
5
21
  TILE_SIZE = 256.0
6
- MAX_LAT = 85.05112878
7
22
 
8
- # --------------------------------------------------
9
- # Validation
10
- # --------------------------------------------------
23
+ # Maximum latitude supported by Web Mercator
24
+ MAX_LAT = 85.05112878
11
25
 
26
+ # Validates a bounding box.
27
+ #
28
+ # @param bbox [Array<Float>]
29
+ # [min_lng, min_lat, max_lng, max_lat]
30
+ # @raise [ArgumentError] if the bbox is invalid
31
+ # @return [void]
12
32
  def self.validate_bbox!(bbox)
13
- unless bbox.is_a?(Array) && bbox.size == 4
14
- raise ArgumentError, "bbox must be [min_lng, min_lat, max_lng, max_lat]"
15
- end
33
+ return if bbox.is_a?(Array) && bbox.size == 4
34
+
35
+ raise ArgumentError, "bbox must be [min_lng, min_lat, max_lng, max_lat]"
16
36
  end
17
37
 
38
+ # Validates a coordinate array.
39
+ #
40
+ # @param coords [Array<Array<Float>>]
41
+ # @raise [ArgumentError] if the coordinates are invalid
42
+ # @return [void]
18
43
  def self.validate_coords!(coords)
19
- unless coords.is_a?(Array) && coords.size >= 2
20
- raise ArgumentError, "coords must be an Array of at least 2 points"
21
- end
22
- end
44
+ return if coords.is_a?(Array) && coords.size >= 2
23
45
 
24
- # --------------------------------------------------
25
- # Web Mercator Projection
26
- # --------------------------------------------------
46
+ raise ArgumentError, "coords must be an Array of at least 2 points"
47
+ end
27
48
 
49
+ # Converts longitude to Web Mercator X coordinate.
50
+ #
51
+ # @param lng [Float] longitude in degrees
52
+ # @param zoom [Integer] zoom level
53
+ # @return [Float] X coordinate in pixels
28
54
  def self.lng_to_x(lng, zoom)
29
55
  ((lng + 180.0) / 360.0) * TILE_SIZE * (2**zoom)
30
56
  end
31
57
 
58
+ # Converts latitude to Web Mercator Y coordinate.
59
+ #
60
+ # Latitude values are clamped to the valid Web Mercator range.
61
+ #
62
+ # @param lat [Float] latitude in degrees
63
+ # @param zoom [Integer] zoom level
64
+ # @return [Float] Y coordinate in pixels
32
65
  def self.lat_to_y(lat, zoom)
33
- lat = [[lat, MAX_LAT].min, -MAX_LAT].max
66
+ lat = lat.clamp(-MAX_LAT, MAX_LAT)
34
67
  lat_rad = lat * Math::PI / 180.0
35
- n = Math.log(Math.tan(Math::PI / 4.0 + lat_rad / 2.0))
36
- (1.0 - n / Math::PI) / 2.0 * TILE_SIZE * (2**zoom)
68
+ n = Math.log(Math.tan((Math::PI / 4.0) + (lat_rad / 2.0)))
69
+ (1.0 - (n / Math::PI)) / 2.0 * TILE_SIZE * (2**zoom)
37
70
  end
38
71
 
72
+ # Converts Web Mercator X coordinate to longitude.
73
+ #
74
+ # @param x [Float] X coordinate in pixels
75
+ # @param zoom [Integer] zoom level
76
+ # @return [Float] longitude in degrees
39
77
  def self.x_to_lng(x, zoom)
40
- (x / (TILE_SIZE * (2**zoom))) * 360.0 - 180.0
78
+ ((x / (TILE_SIZE * (2**zoom))) * 360.0) - 180.0
41
79
  end
42
80
 
81
+ # Converts Web Mercator Y coordinate to latitude.
82
+ #
83
+ # @param y [Float] Y coordinate in pixels
84
+ # @param zoom [Integer] zoom level
85
+ # @return [Float] latitude in degrees
43
86
  def self.y_to_lat(y, zoom)
44
- n = Math::PI - 2.0 * Math::PI * y / (TILE_SIZE * (2**zoom))
87
+ n = Math::PI - (2.0 * Math::PI * y / (TILE_SIZE * (2**zoom)))
45
88
  180.0 / Math::PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)))
46
89
  end
47
90
 
48
- # --------------------------------------------------
49
- # Viewport
50
- # --------------------------------------------------
51
-
91
+ # Computes a viewport bounding box fitted to an image size.
92
+ #
93
+ # @param bbox [Array<Float>]
94
+ # input bounding box [min_lng, min_lat, max_lng, max_lat]
95
+ # @param zoom [Integer] zoom level
96
+ # @param width [Integer] image width in pixels
97
+ # @param height [Integer] image height in pixels
98
+ # @return [Array<Float>] fitted bounding box
52
99
  def self.viewport_bbox(bbox:, zoom:, width:, height:)
53
100
  validate_bbox!(bbox)
54
101
 
@@ -76,19 +123,15 @@ module GD
76
123
  ]
77
124
  end
78
125
 
79
- # --------------------------------------------------
80
- # Geometry helpers
81
- # --------------------------------------------------
82
-
83
- # buffer_line(coords, meters)
126
+ # Creates a naive buffer polygon around a line.
84
127
  #
85
- # coords :: Array<[lng, lat]> in WGS84
86
- # meters :: Numeric (approximate)
87
- #
88
- # NOTE:
89
- # - Naive meters-to-degrees conversion
90
- # - Suitable for visualization, not analysis
128
+ # ⚠️ This uses an approximate meters-to-degrees conversion
129
+ # and is intended for visualization only.
91
130
  #
131
+ # @param coords [Array<Array<Float>>]
132
+ # array of [lng, lat] points
133
+ # @param meters [Numeric] buffer distance (approximate)
134
+ # @return [Array<Array<Float>>] polygon coordinates
92
135
  def self.buffer_line(coords, meters)
93
136
  validate_coords!(coords)
94
137
 
@@ -102,7 +145,7 @@ module GD
102
145
  dx = x2 - x1
103
146
  dy = y2 - y1
104
147
 
105
- len = Math.sqrt(dx * dx + dy * dy)
148
+ len = Math.sqrt((dx * dx) + (dy * dy))
106
149
  next if len.zero?
107
150
 
108
151
  nx = -dy / len
@@ -110,8 +153,8 @@ module GD
110
153
 
111
154
  off = meters / 111_320.0
112
155
 
113
- left << [x1 + nx * off, y1 + ny * off]
114
- right << [x1 - nx * off, y1 - ny * off]
156
+ left << [x1 + (nx * off), y1 + (ny * off)]
157
+ right << [x1 - (nx * off), y1 - (ny * off)]
115
158
  end
116
159
 
117
160
  x2, y2 = coords.last
@@ -120,7 +163,14 @@ module GD
120
163
 
121
164
  left + right.reverse
122
165
  end
123
-
166
+
167
+ # Projects geographic coordinates into pixel space relative to a bbox.
168
+ #
169
+ # @param lng [Float] longitude
170
+ # @param lat [Float] latitude
171
+ # @param bbox [Array<Float>] reference bounding box
172
+ # @param zoom [Integer] zoom level
173
+ # @return [Array<Float>] [x, y] pixel coordinates
124
174
  def self.project(lng, lat, bbox, zoom)
125
175
  min_lng, _min_lat, _max_lng, max_lat = bbox
126
176
 
@@ -136,6 +186,18 @@ module GD
136
186
  ]
137
187
  end
138
188
 
189
+ # Computes a bounding box that fits all features in a GeoJSON file.
190
+ #
191
+ # The resulting bbox is padded and adjusted to match the
192
+ # requested image aspect ratio.
193
+ #
194
+ # @param path [String] path to GeoJSON file
195
+ # @param zoom [Integer] zoom level
196
+ # @param width [Integer] image width in pixels
197
+ # @param height [Integer] image height in pixels
198
+ # @param padding_px [Integer] padding in pixels
199
+ # @return [Array<Float>] bounding box
200
+ # @raise [RuntimeError] if no coordinates are found
139
201
  def self.bbox_for_image(path, zoom:, width:, height:, padding_px: 80)
140
202
  data = JSON.parse(File.read(path))
141
203
  points = []
@@ -143,6 +205,7 @@ module GD
143
205
  data["features"].each do |f|
144
206
  geom = f["geometry"]
145
207
  next unless geom
208
+
146
209
  collect_points(geom, points)
147
210
  end
148
211
 
@@ -167,7 +230,7 @@ module GD
167
230
  # --------------------------------------------------
168
231
  # 2. Fit bbox to image aspect ratio
169
232
  # --------------------------------------------------
170
- target_ratio = width.to_f / height.to_f
233
+ target_ratio = width.to_f / height
171
234
  current_ratio = (max_x - min_x) / (max_y - min_y)
172
235
 
173
236
  if current_ratio > target_ratio
@@ -195,6 +258,11 @@ module GD
195
258
  ]
196
259
  end
197
260
 
261
+ # Collects all coordinate points from a GeoJSON geometry.
262
+ #
263
+ # @param geom [Hash] GeoJSON geometry
264
+ # @param points [Array] accumulator array
265
+ # @return [void]
198
266
  def self.collect_points(geom, points)
199
267
  case geom["type"]
200
268
  when "Point"
@@ -217,6 +285,14 @@ module GD
217
285
  end
218
286
  end
219
287
 
288
+ # Builds a bounding box around a point using a radius.
289
+ #
290
+ # Uses a simple spherical approximation.
291
+ #
292
+ # @param lon [Float] longitude
293
+ # @param lat [Float] latitude
294
+ # @param radius_km [Numeric] radius in kilometers
295
+ # @return [Array<Float>] bounding box
220
296
  def self.bbox_around_point(lon, lat, radius_km:)
221
297
  delta_lat = radius_km / 111.0
222
298
  delta_lon = radius_km / (111.0 * Math.cos(lat * Math::PI / 180.0))
@@ -228,8 +304,6 @@ module GD
228
304
  lat + delta_lat
229
305
  ]
230
306
  end
231
-
232
307
  end
233
308
  end
234
309
  end
235
-
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "json"
2
4
  require_relative "feature"
3
5
  require_relative "crs_normalizer"
@@ -5,7 +7,24 @@ require_relative "ontology"
5
7
 
6
8
  module GD
7
9
  module GIS
10
+ # Loads GeoJSON files into renderable Feature objects.
11
+ #
12
+ # This class is responsible for:
13
+ # - Parsing GeoJSON files
14
+ # - Normalizing coordinates across CRS definitions
15
+ # - Classifying features using an ontology
16
+ # - Producing {GD::GIS::Feature} instances
17
+ #
18
+ # All coordinates are normalized to WGS84
19
+ # in [longitude, latitude] order.
20
+ #
8
21
  class LayerGeoJSON
22
+ # Loads a GeoJSON file and returns normalized features.
23
+ #
24
+ # @param path [String] path to GeoJSON file
25
+ # @return [Array<GD::GIS::Feature>]
26
+ # @raise [JSON::ParserError] if the file is invalid JSON
27
+ # @raise [Errno::ENOENT] if the file does not exist
9
28
  def self.load(path)
10
29
  data = JSON.parse(File.read(path))
11
30
 
@@ -27,9 +46,15 @@ module GD
27
46
  end
28
47
  end
29
48
 
30
- # --------------------------------------------
31
- # CRS normalization (2D + 3D safe)
32
- # --------------------------------------------
49
+ # Normalizes a GeoJSON geometry in-place.
50
+ #
51
+ # Supports 2D and 3D coordinate arrays.
52
+ # Any additional dimensions (e.g. Z) are preserved or ignored
53
+ # depending on the CRS normalizer.
54
+ #
55
+ # @param geometry [Hash] GeoJSON geometry object
56
+ # @param normalizer [CRS::Normalizer]
57
+ # @return [void]
33
58
  def self.normalize_geometry!(geometry, normalizer)
34
59
  case geometry["type"]
35
60
 
@@ -62,7 +87,6 @@ module GD
62
87
  end
63
88
  end
64
89
  end
65
-
66
90
  end
67
91
  end
68
92
  end