libgd-gis 0.2.8 → 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.
data/lib/gd/gis/map.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "basemap"
2
4
  require_relative "projection"
3
5
  require_relative "classifier"
@@ -6,17 +8,58 @@ require_relative "layer_points"
6
8
  require_relative "layer_lines"
7
9
  require_relative "layer_polygons"
8
10
 
11
+ LINE_GEOMS = %w[LineString MultiLineString].freeze
12
+ POLY_GEOMS = %w[Polygon MultiPolygon].freeze
13
+
9
14
  module GD
10
15
  module GIS
11
- attr_accessor :debug
12
-
16
+ # Represents a complete renderable map.
17
+ #
18
+ # A Map is responsible for:
19
+ # - Managing the geographic extent (bounding box + zoom)
20
+ # - Fetching and compositing basemap tiles
21
+ # - Managing semantic feature layers
22
+ # - Managing generic overlay layers (points, lines, polygons)
23
+ # - Executing the rendering pipeline
24
+ #
25
+ # The map can render either:
26
+ # - A full tile-based image, or
27
+ # - A fixed-size viewport clipped from the basemap
28
+ #
13
29
  class Map
30
+ # Tile size in pixels (Web Mercator standard)
14
31
  TILE_SIZE = 256
15
32
 
33
+ # @return [GD::Image, nil] rendered image
16
34
  attr_reader :image
35
+
36
+ # @return [Hash<Symbol, Array>] semantic feature layers
17
37
  attr_reader :layers
38
+
39
+ # @return [Object, nil] style object
18
40
  attr_accessor :style
19
41
 
42
+ # @return [Boolean] enables debug rendering
43
+ attr_reader :debug
44
+
45
+ # Creates a new map.
46
+ #
47
+ # @param bbox [Array<Float>]
48
+ # bounding box [min_lng, min_lat, max_lng, max_lat]
49
+ # @param zoom [Integer]
50
+ # zoom level
51
+ # @param basemap [Symbol]
52
+ # basemap provider identifier
53
+ # @param width [Integer, nil]
54
+ # viewport width in pixels
55
+ # @param height [Integer, nil]
56
+ # viewport height in pixels
57
+ # @param crs [String, Symbol, nil]
58
+ # input CRS identifier
59
+ # @param fitted_bbox [Boolean]
60
+ # whether the provided bbox is already viewport-fitted
61
+ #
62
+ # @raise [ArgumentError] if parameters are invalid
20
63
  def initialize(
21
64
  bbox:,
22
65
  zoom:,
@@ -24,27 +67,21 @@ module GD
24
67
  width: nil,
25
68
  height: nil,
26
69
  crs: nil,
27
- fitted_bbox: false
70
+ fitted_bbox: false
28
71
  )
29
- # --------------------------------------------------
30
72
  # 1. Basic input validation
31
- # --------------------------------------------------
32
73
  raise ArgumentError, "bbox must be [min_lng, min_lat, max_lng, max_lat]" unless
33
74
  bbox.is_a?(Array) && bbox.size == 4
34
75
 
35
76
  raise ArgumentError, "zoom must be an Integer" unless zoom.is_a?(Integer)
36
77
 
37
- if (width && !height) || (!width && height)
38
- raise ArgumentError, "width and height must be provided together"
39
- end
78
+ raise ArgumentError, "width and height must be provided together" if (width && !height) || (!width && height)
40
79
 
41
80
  @zoom = zoom
42
81
  @width = width
43
82
  @height = height
44
83
 
45
- # --------------------------------------------------
46
84
  # 2. CRS normalization (input → WGS84 lon/lat)
47
- # --------------------------------------------------
48
85
  if crs
49
86
  normalizer = GD::GIS::CRS::Normalizer.new(crs)
50
87
 
@@ -54,9 +91,7 @@ module GD
54
91
  bbox = [min_lng, min_lat, max_lng, max_lat]
55
92
  end
56
93
 
57
- # --------------------------------------------------
58
94
  # 3. Final bbox (viewport-aware if width/height)
59
- # --------------------------------------------------
60
95
  @bbox =
61
96
  if width && height && !fitted_bbox
62
97
  GD::GIS::Geometry.viewport_bbox(
@@ -69,14 +104,10 @@ module GD
69
104
  bbox
70
105
  end
71
106
 
72
- # --------------------------------------------------
73
107
  # 4. Basemap (uses FINAL bbox)
74
- # --------------------------------------------------
75
108
  @basemap = GD::GIS::Basemap.new(zoom, @bbox, basemap)
76
109
 
77
- # --------------------------------------------------
78
110
  # 5. Legacy semantic layers (REQUIRED by render)
79
- # --------------------------------------------------
80
111
  @layers = {
81
112
  motorway: [],
82
113
  primary: [],
@@ -91,21 +122,22 @@ module GD
91
122
  # Optional alias (semantic clarity, no behavior change)
92
123
  @road_layers = @layers
93
124
 
94
- # --------------------------------------------------
95
125
  # 6. Overlay layers (generic)
96
- # --------------------------------------------------
97
126
  @points_layers = []
98
127
  @lines_layers = []
99
128
  @polygons_layers = []
100
129
 
101
- # --------------------------------------------------
102
130
  # 7. Style
103
- # --------------------------------------------------
104
131
  @style = nil
105
132
 
106
- @debug = false
133
+ @debug = false
134
+ @used_labels = {}
107
135
  end
108
136
 
137
+ # Returns all features belonging to a given semantic layer.
138
+ #
139
+ # @param layer [Symbol]
140
+ # @return [Array<Feature>]
109
141
  def features_by_layer(layer)
110
142
  return [] unless @layers[layer]
111
143
 
@@ -114,25 +146,68 @@ module GD
114
146
  end
115
147
  end
116
148
 
149
+ # Returns all features in the map.
150
+ #
151
+ # @return [Array<Feature>]
117
152
  def features
118
153
  @layers.values.flatten.map do |item|
119
154
  item.is_a?(Array) ? item.last : item
120
155
  end
121
156
  end
122
157
 
123
- # -----------------------------------
124
- # GeoJSON input (unchanged behavior)
125
- # -----------------------------------
158
+ def maybe_create_line_label(feature)
159
+ geom = feature.geometry
160
+ return unless LINE_GEOMS.include?(geom["type"])
161
+
162
+ name = feature.properties["name"]
163
+ return if name.nil? || name.empty?
164
+
165
+ key = feature.properties["wikidata"] || name
166
+ return if @used_labels[key]
167
+
168
+ coords = geom["coordinates"]
169
+ coords = coords.flatten(1) if geom["type"] == "MultiLineString"
170
+ return if coords.size < 2
171
+
172
+ lon, lat = coords[coords.size / 2]
173
+
174
+ puts @style
175
+
176
+ @points_layers << GD::GIS::PointsLayer.new(
177
+ [feature],
178
+ lon: ->(_) { lon },
179
+ lat: ->(_) { lat },
180
+ icon: nil,
181
+ label: ->(_) { name },
182
+ font: GD::GIS::FontHelper.random,
183
+ size: 10,
184
+ color: GD::Color.rgb(0, 0, 0)
185
+ )
186
+
187
+ @used_labels[key] = true
188
+ end
189
+
190
+ # Loads features from a GeoJSON file.
191
+ #
192
+ # This method:
193
+ # - Normalizes CRS
194
+ # - Classifies features into semantic layers
195
+ # - Creates overlay layers when needed (points)
196
+ #
197
+ # @param path [String] path to GeoJSON file
198
+ # @return [void]
126
199
  def add_geojson(path)
127
200
  features = LayerGeoJSON.load(path)
128
201
 
129
202
  features.each do |feature|
203
+ maybe_create_line_label(feature)
204
+
130
205
  case feature.layer
131
206
  when :water
132
207
  kind =
133
208
  case (feature.properties["objeto"] || feature.properties["waterway"]).to_s.downcase
134
- when /river|río/ then :river
135
- when /stream|arroyo/ then :stream
209
+ when /river|río|canal/ then :river
210
+ when /stream|arroyo/ then :stream
136
211
  else :minor
137
212
  end
138
213
 
@@ -146,35 +221,86 @@ module GD
146
221
 
147
222
  when :track
148
223
  # elegí una:
149
- @layers[:minor] << feature
224
+ @layers[:minor] << feature
150
225
  # o @layers[:street] << feature
151
226
  else
152
227
  geom_type = feature.geometry["type"]
153
- if geom_type == "LineString" || geom_type == "MultiLineString"
228
+
229
+ if geom_type == "Point"
230
+ points_style = @style.points || begin
231
+ warn "Style error: missing 'points' section"
232
+ end
233
+
234
+ font = points_style[:font] || begin
235
+ warn "[libgd-gis] points.font not defined in style, using random system font"
236
+ GD::GIS::FontHelper.random
237
+ end
238
+
239
+ size = points_style[:size] || begin
240
+ warn "[libgd-gis] points.font size not defined in style, using random system font size"
241
+ (6..14).to_a.sample
242
+ end
243
+
244
+ raw_color = points_style[:color]
245
+ color = @style.normalize_color(raw_color)
246
+
247
+ icon = if points_style.key?(:icon_fill) && points_style.key?(:icon_stroke)
248
+ [points_style[:icon_stroke],
249
+ points_style[:icon_stroke]]
250
+ end
251
+ icon = points_style.key?(:icon) ? points_style[:icon] : nil if icon.nil?
252
+
253
+ @points_layers << GD::GIS::PointsLayer.new(
254
+ [feature],
255
+ lon: ->(f) { f.geometry["coordinates"][0] },
256
+ lat: ->(f) { f.geometry["coordinates"][1] },
257
+ icon: icon,
258
+ label: ->(f) { f.properties["name"] },
259
+ font: font,
260
+ size: size,
261
+ color: color
262
+ )
263
+ elsif LINE_GEOMS.include?(geom_type)
154
264
  @layers[:minor] << feature
155
265
  end
156
266
  end
157
267
  end
158
268
  end
159
269
 
160
- # -----------------------------------
161
- # Overlay layers
162
- # -----------------------------------
270
+ # Adds a generic points overlay layer.
271
+ #
272
+ # @param data [Enumerable]
273
+ # @param opts [Hash]
274
+ # @return [void]
163
275
  def add_points(data, **opts)
164
276
  @points_layers << GD::GIS::PointsLayer.new(data, **opts)
165
277
  end
166
278
 
279
+ # Adds a generic lines overlay layer.
280
+ #
281
+ # @param features [Array]
282
+ # @param opts [Hash]
283
+ # @return [void]
167
284
  def add_lines(features, **opts)
168
285
  @lines_layers << GD::GIS::LinesLayer.new(features, **opts)
169
286
  end
170
287
 
288
+ # Adds a generic polygons overlay layer.
289
+ #
290
+ # @param polygons [Array]
291
+ # @param opts [Hash]
292
+ # @return [void]
171
293
  def add_polygons(polygons, **opts)
172
294
  @polygons_layers << GD::GIS::PolygonsLayer.new(polygons, **opts)
173
295
  end
174
296
 
175
- # -----------------------------------
176
- # Rendering (LEGACY, UNCHANGED)
177
- # -----------------------------------
297
+ # Renders the map.
298
+ #
299
+ # Chooses between tile rendering and viewport rendering
300
+ # depending on whether width and height are set.
301
+ #
302
+ # @return [void]
303
+ # @raise [RuntimeError] if style is not set
178
304
  def render
179
305
  raise "map.style must be set" unless @style
180
306
 
@@ -184,7 +310,7 @@ module GD
184
310
  render_tiles
185
311
  end
186
312
  end
187
-
313
+
188
314
  def render_tiles
189
315
  raise "map.style must be set" unless @style
190
316
 
@@ -221,12 +347,12 @@ module GD
221
347
  [(x - origin_x).round, (y - origin_y).round]
222
348
  end
223
349
 
224
- # 1️⃣ GeoJSON semantic layers
350
+ # 1. GeoJSON semantic layers
225
351
  @style.order.each do |kind|
226
352
  draw_layer(kind, projection)
227
353
  end
228
354
 
229
- # 2️⃣ Generic overlays
355
+ # 2. Generic overlays
230
356
  @polygons_layers.each { |l| l.render!(@image, projection) }
231
357
  @lines_layers.each { |l| l.render!(@image, projection) }
232
358
  @points_layers.each { |l| l.render!(@image, projection) }
@@ -238,22 +364,16 @@ module GD
238
364
  @image = GD::Image.new(@width, @height)
239
365
  @image.antialias = false
240
366
 
241
- # --------------------------------------------------
242
367
  # 1. Compute global pixel bbox
243
- # --------------------------------------------------
244
368
  min_lng, min_lat, max_lng, max_lat = @bbox
245
369
 
246
370
  x1, y1 = GD::GIS::Projection.lonlat_to_global_px(min_lng, max_lat, @zoom)
247
- x2, y2 = GD::GIS::Projection.lonlat_to_global_px(max_lng, min_lat, @zoom)
371
+ GD::GIS::Projection.lonlat_to_global_px(max_lng, min_lat, @zoom)
248
372
 
249
- # --------------------------------------------------
250
373
  # 2. Fetch tiles
251
- # --------------------------------------------------
252
374
  tiles, = @basemap.fetch_tiles
253
375
 
254
- # --------------------------------------------------
255
376
  # 3. Draw tiles clipped to viewport
256
- # --------------------------------------------------
257
377
  tiles.each do |x, y, file|
258
378
  tile = GD::Image.open(file)
259
379
 
@@ -282,16 +402,12 @@ module GD
282
402
  )
283
403
  end
284
404
 
285
- # --------------------------------------------------
286
405
  # 4. Projection (viewport version)
287
- # --------------------------------------------------
288
406
  projection = lambda do |lon, lat|
289
407
  GD::GIS::Geometry.project(lon, lat, @bbox, @zoom)
290
408
  end
291
409
 
292
- # --------------------------------------------------
293
410
  # 5. REUSE the same render pipeline
294
- # --------------------------------------------------
295
411
  @style.order.each do |kind|
296
412
  draw_layer(kind, projection)
297
413
  end
@@ -301,10 +417,19 @@ module GD
301
417
  @points_layers.each { |l| l.render!(@image, projection) }
302
418
  end
303
419
 
420
+ # Saves the rendered image to disk.
421
+ #
422
+ # @param path [String]
423
+ # @return [void]
304
424
  def save(path)
305
425
  @image.save(path)
306
426
  end
307
427
 
428
+ # Draws a semantic feature layer.
429
+ #
430
+ # @param kind [Symbol]
431
+ # @param projection [#call]
432
+ # @return [void]
308
433
  def draw_layer(kind, projection)
309
434
  items = @layers[kind]
310
435
  return if items.nil? || items.empty?
@@ -347,24 +472,20 @@ module GD
347
472
  f = item
348
473
  geom = f.geometry["type"]
349
474
 
350
- if geom == "Polygon" || geom == "MultiPolygon"
475
+ if POLY_GEOMS.include?(geom)
351
476
  f.draw(@image, projection, nil, nil, style)
352
- else
353
- if style[:stroke]
354
- color = GD::Color.rgb(*style[:stroke])
477
+ elsif style[:stroke]
478
+ color = GD::Color.rgb(*style[:stroke])
355
479
 
356
- color = GD::GIS::ColorHelpers.random_vivid if @debug
480
+ color = GD::GIS::ColorHelpers.random_vivid if @debug
357
481
 
358
- width = style[:stroke_width] ? style[:stroke_width].round : 1
359
- width = 1 if width < 1
360
- f.draw(@image, projection, color, width)
361
- end
482
+ width = style[:stroke_width] ? style[:stroke_width].round : 1
483
+ width = 1 if width < 1
484
+ f.draw(@image, projection, color, width)
362
485
  end
363
486
  end
364
487
  end
365
488
  end
366
-
367
489
  end
368
490
  end
369
491
  end
370
-
@@ -1,17 +1,65 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module GD
2
4
  module GIS
5
+ # Coordinate Reference System (CRS) helpers and normalizers.
6
+ #
7
+ # This module defines commonly used CRS identifiers and
8
+ # utilities for normalizing coordinates into a single,
9
+ # consistent representation.
10
+ #
3
11
  module CRS
12
+ # OGC CRS84 (longitude, latitude)
4
13
  CRS84 = "urn:ogc:def:crs:OGC:1.3:CRS84"
14
+
15
+ # EPSG:4326 (latitude, longitude axis order)
5
16
  EPSG4326 = "EPSG:4326"
17
+
18
+ # EPSG:3857 (Web Mercator)
6
19
  EPSG3857 = "EPSG:3857"
7
- GK_ARGENTINA = "EPSG:22195" # Gauss–Krüger Argentina (zone 5 example)
8
20
 
9
- # Normalize any CRS → CRS84 (lon,lat in degrees)
21
+ # Gauss–Krüger Argentina, zone 5
22
+ #
23
+ # Note: This constant represents a *specific* GK zone
24
+ # and is not a generic Gauss–Krüger definition.
25
+ GK_ARGENTINA = "EPSG:22195"
26
+
27
+ # Normalizes coordinates from supported CRS definitions
28
+ # into CRS84 (longitude, latitude in degrees).
29
+ #
30
+ # Supported input CRS:
31
+ # - CRS84
32
+ # - EPSG:4326 (axis order normalization)
33
+ # - EPSG:3857 (Web Mercator)
34
+ # - EPSG:22195 (Gauss–Krüger Argentina, zone 5)
35
+ #
36
+ # All outputs are returned as:
37
+ # [longitude, latitude] in degrees
38
+ #
39
+ # ⚠️ Projection conversions are intended for mapping
40
+ # and visualization, not for high-precision geodesy.
41
+ #
10
42
  class Normalizer
43
+ # Creates a new CRS normalizer.
44
+ #
45
+ # @param crs [String, Symbol, nil]
46
+ # CRS identifier; defaults to CRS84 if nil
11
47
  def initialize(crs)
12
48
  @crs = normalize_name(crs)
13
49
  end
14
50
 
51
+ # Normalizes a coordinate pair into CRS84.
52
+ #
53
+ # @param lon [Numeric]
54
+ # first coordinate (meaning depends on input CRS)
55
+ # @param lat [Numeric]
56
+ # second coordinate (meaning depends on input CRS)
57
+ #
58
+ # @return [Array<Float>]
59
+ # normalized [longitude, latitude] in degrees
60
+ #
61
+ # @raise [RuntimeError]
62
+ # if the CRS is not supported
15
63
  def normalize(lon, lat)
16
64
  case @crs
17
65
  when CRS84
@@ -34,52 +82,67 @@ module GD
34
82
 
35
83
  private
36
84
 
85
+ # Normalizes a CRS name into a comparable string.
86
+ #
87
+ # @param name [Object]
88
+ # @return [String]
37
89
  def normalize_name(name)
38
90
  return CRS84 if name.nil?
91
+
39
92
  name.to_s.strip
40
93
  end
41
94
 
42
- # Web Mercator WGS84
95
+ # Converts Web Mercator coordinates to WGS84.
96
+ #
97
+ # @param x [Numeric] X coordinate in meters
98
+ # @param y [Numeric] Y coordinate in meters
99
+ # @return [Array<Float>] [longitude, latitude] in degrees
43
100
  def mercator_to_wgs84(x, y)
44
101
  r = 6378137.0
45
102
  lon = (x / r) * 180.0 / Math::PI
46
- lat = (2 * Math.atan(Math.exp(y / r)) - Math::PI / 2) * 180.0 / Math::PI
103
+ lat = ((2 * Math.atan(Math.exp(y / r))) - (Math::PI / 2)) * 180.0 / Math::PI
47
104
  [lon, lat]
48
105
  end
49
106
 
50
- # Gauss–Krüger Argentina (Zone 5) WGS84
51
- # This is enough precision for mapping
107
+ # Converts Gauss–Krüger Argentina (zone 5) coordinates to WGS84.
108
+ #
109
+ # This implementation provides sufficient accuracy for
110
+ # cartographic rendering and visualization.
111
+ #
112
+ # @param easting [Numeric] easting (meters)
113
+ # @param northing [Numeric] northing (meters)
114
+ # @return [Array<Float>] [longitude, latitude] in degrees
52
115
  def gk_to_wgs84(easting, northing)
53
116
  # Parameters for Argentina GK Zone 5
54
117
  a = 6378137.0
55
118
  f = 1 / 298.257223563
56
- e2 = 2*f - f*f
57
- lon0 = -60.0 * Math::PI / 180.0 # central meridian zone 5
119
+ e2 = (2 * f) - (f * f)
120
+ lon0 = -60.0 * Math::PI / 180.0 # central meridian zone 5
58
121
 
59
122
  x = easting - 500000.0
60
123
  y = northing
61
124
 
62
125
  m = y
63
- mu = m / (a * (1 - e2/4 - 3*e2*e2/64))
126
+ mu = m / (a * (1 - (e2 / 4) - (3 * e2 * e2 / 64)))
64
127
 
65
128
  e1 = (1 - Math.sqrt(1 - e2)) / (1 + Math.sqrt(1 - e2))
66
129
 
67
- j1 = 3*e1/2 - 27*e1**3/32
68
- j2 = 21*e1**2/16 - 55*e1**4/32
130
+ j1 = (3 * e1 / 2) - (27 * (e1**3) / 32)
131
+ j2 = (21 * (e1**2) / 16) - (55 * (e1**4) / 32)
69
132
 
70
- fp = mu + j1*Math.sin(2*mu) + j2*Math.sin(4*mu)
133
+ fp = mu + (j1 * Math.sin(2 * mu)) + (j2 * Math.sin(4 * mu))
71
134
 
72
- c1 = e2 * Math.cos(fp)**2
135
+ c1 = e2 * (Math.cos(fp)**2)
73
136
  t1 = Math.tan(fp)**2
74
- r1 = a * (1 - e2) / (1 - e2 * Math.sin(fp)**2)**1.5
75
- n1 = a / Math.sqrt(1 - e2 * Math.sin(fp)**2)
137
+ r1 = a * (1 - e2) / ((1 - (e2 * (Math.sin(fp)**2)))**1.5)
138
+ n1 = a / Math.sqrt(1 - (e2 * (Math.sin(fp)**2)))
76
139
 
77
140
  d = x / n1
78
141
 
79
- lat = fp - (n1*Math.tan(fp)/r1) *
80
- (d**2/2 - (5 + 3*t1 + 10*c1)*d**4/24)
142
+ lat = fp - ((n1 * Math.tan(fp) / r1) *
143
+ (((d**2) / 2) - ((5 + (3 * t1) + (10 * c1)) * (d**4) / 24)))
81
144
 
82
- lon = lon0 + (d - (1 + 2*t1 + c1)*d**3/6) / Math.cos(fp)
145
+ lon = lon0 + ((d - ((1 + (2 * t1) + c1) * (d**3) / 6)) / Math.cos(fp))
83
146
 
84
147
  [lon * 180.0 / Math::PI, lat * 180.0 / Math::PI]
85
148
  end
@@ -1,16 +1,59 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "yaml"
2
4
 
3
5
  module GD
4
6
  module GIS
7
+ # Classifies features into semantic layers using rule-based matching.
8
+ #
9
+ # An Ontology maps feature properties to logical layer identifiers
10
+ # (e.g. :water, :road, :park) based on a YAML rule definition.
11
+ #
12
+ # The ontology is intentionally simple and heuristic-based:
13
+ # - Rules are evaluated in order
14
+ # - The first matching rule wins
15
+ # - Matching is case-insensitive and substring-based
16
+ #
17
+ # This design favors robustness and flexibility over strict
18
+ # schema enforcement.
19
+ #
5
20
  class Ontology
21
+ # Creates a new ontology.
22
+ #
23
+ # @param path [String, nil]
24
+ # path to a YAML ontology file; defaults to `ontology.yml`
25
+ # shipped with the gem
26
+ #
27
+ # @raise [Errno::ENOENT] if the file does not exist
28
+ # @raise [Psych::SyntaxError] if the YAML is invalid
6
29
  def initialize(path = nil)
7
30
  path ||= File.expand_path("ontology.yml", __dir__)
8
31
  @rules = YAML.load_file(path)
9
32
  end
10
33
 
11
- def classify(properties)
34
+ # Classifies a feature into a semantic layer.
35
+ #
36
+ # Properties are matched against ontology rules using
37
+ # case-insensitive substring comparison.
38
+ #
39
+ # @param properties [Hash]
40
+ # feature attributes / tags
41
+ # @param geometry_type [String, nil]
42
+ # GeoJSON geometry type
43
+ #
44
+ # @return [Symbol, nil]
45
+ # semantic layer identifier, or nil if no rule matches
46
+ #
47
+ # @example
48
+ # ontology.classify({ "waterway" => "river" })
49
+ # #=> :water
50
+ #
51
+ # @example
52
+ # ontology.classify({}, geometry_type: "Point")
53
+ # #=> :points
54
+ def classify(properties, geometry_type: nil)
12
55
  @rules.each do |layer, sources|
13
- sources.each do |source, rules|
56
+ sources.each_value do |rules|
14
57
  rules.each do |key, values|
15
58
  v = (properties[key.to_s] || properties[key.to_sym]).to_s.strip.downcase
16
59
  values = values.map { |x| x.to_s.downcase }
@@ -19,6 +62,10 @@ module GD
19
62
  end
20
63
  end
21
64
  end
65
+
66
+ # Fallback classification
67
+ return :points if geometry_type == "Point"
68
+
22
69
  nil
23
70
  end
24
71
  end
@@ -1,4 +1,10 @@
1
1
  water:
2
+ osm:
3
+ waterway:
4
+ - river
5
+ - canal
6
+ - stream
7
+
2
8
  ign:
3
9
  objeto:
4
10
  - canal
@@ -26,3 +32,5 @@ track:
26
32
  gps:
27
33
  name:
28
34
  - track
35
+
36
+