libgd-gis 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 72c8b1d43f60ad8615302e22054c3d5e00be6bd746c757f00d980e781f760b0c
4
- data.tar.gz: 74d09fb46fe02d7d53c75f4c2a9acb6bbab24383b51ef9bd9ec3ad8c89e0253f
3
+ metadata.gz: e54f40ea70b79fc866db311f7ac994ca267483f62de518b2f6f4f5989957f5e6
4
+ data.tar.gz: c8825bd11685f5a0254bd5fdaa5b654cee53dff608a990ac91bc4bacfcba4f31
5
5
  SHA512:
6
- metadata.gz: e09c62bf8ee7218019a2e69676cc9b7461982568842bb8afac861f38f290d7fec658ee15269915ec5a3ad925de4d5ef4e6dcdce0415d7b97d438d91795d022a5
7
- data.tar.gz: e7a1a6f47da4bbf9db7e18de9c3184e4af3498f636f51ba8b8ee9aa25b1c57c9532be24e71ee76e2201f5f7a240cddc62e33f16d886a1673062d845163831664
6
+ metadata.gz: 7bf12c61f036e85b93aa1b7617fb2aa63f9c0fb788f3ad53e1bf313259b5847658e3c1c1cf2682227fdc9d542cabf86489cb94d9a79c5cf15949519dfe08be69
7
+ data.tar.gz: bdc13c7e7aa1e99f4328c3ea69699b4e8f548ea5aeb975b317a81fe136d8dc94b1e64655987f51a0fcf4db0af030ffd8e5a125ae8cd3191a34c8814575cffa0e
@@ -1,5 +1,30 @@
1
1
  module GD
2
2
  module GIS
3
+ # Provides helper methods for discovering and selecting font files
4
+ # available on the local system.
5
+ #
6
+ # This module is primarily used to supply font paths to text-rendering
7
+ # components (such as PointsLayer labels) in environments where font
8
+ # availability is system-dependent.
9
+ #
10
+ # Font discovery is performed by scanning a set of well-known directories
11
+ # for TrueType and OpenType font files. The results are cached for the
12
+ # lifetime of the process.
13
+ #
14
+ # Supported font formats:
15
+ # - TrueType (.ttf)
16
+ # - OpenType (.otf)
17
+ # - TrueType Collection (.ttc)
18
+ #
19
+ # @example Select a random system font
20
+ # font_path = GD::GIS::FontHelper.random
21
+ #
22
+ # @example Find a font by name fragment
23
+ # font_path = GD::GIS::FontHelper.find("Noto")
24
+ #
25
+ # @note
26
+ # This helper does not validate glyph coverage (e.g. CJK support).
27
+ # It only locates font files present on the system.
3
28
  module FontHelper
4
29
  PATHS = [
5
30
  "/usr/share/fonts",
@@ -9,6 +34,16 @@ module GD
9
34
 
10
35
  EXTENSIONS = %w[ttf otf ttc].freeze
11
36
 
37
+ # Returns the list of all font files discovered on the system.
38
+ #
39
+ # The search is performed once and cached. Subsequent calls return
40
+ # the cached result.
41
+ #
42
+ # Font files are discovered by recursively scanning the directories
43
+ # defined in {PATHS} for files matching the extensions in {EXTENSIONS}.
44
+ #
45
+ # @return [Array<String>]
46
+ # An array of absolute file paths to font files.
12
47
  def self.all
13
48
  @all ||= PATHS.flat_map do |path|
14
49
  next [] unless Dir.exist?(path)
@@ -19,10 +54,34 @@ module GD
19
54
  end.compact.uniq
20
55
  end
21
56
 
57
+ # Returns a randomly selected font file from the system.
58
+ #
59
+ # This is primarily intended as a fallback mechanism when no explicit
60
+ # font is configured by the caller.
61
+ #
62
+ # @return [String]
63
+ # Absolute path to a font file.
64
+ #
65
+ # @raise [RuntimeError]
66
+ # If no font files are found on the system.
67
+ #
68
+ # @note
69
+ # The selected font is not guaranteed to support any particular
70
+ # character set.
22
71
  def self.random
23
72
  all.sample or raise "GD::GIS::FontHelper: no fonts found on system"
24
73
  end
25
74
 
75
+ # Finds a font file whose filename includes the given name fragment.
76
+ #
77
+ # The match is case-insensitive and performed against the basename
78
+ # of each discovered font file.
79
+ #
80
+ # @param name [String]
81
+ # A substring to search for in font filenames (e.g. "Noto", "DejaVu").
82
+ #
83
+ # @return [String, nil]
84
+ # The path to the first matching font file, or nil if no match is found.
26
85
  def self.find(name)
27
86
  all.find do |f|
28
87
  File.basename(f).downcase.include?(name.downcase)
@@ -38,26 +38,34 @@ module GD
38
38
  label: nil,
39
39
  font: nil,
40
40
  size: 12,
41
- color: [0, 0, 0]
41
+ color: [0, 0, 0],
42
+ font_color: nil,
43
+ count: 0
42
44
  )
43
45
  @data = data
44
46
  @lon = lon
45
47
  @lat = lat
48
+ @color = color
46
49
 
47
50
  if icon.is_a?(Array) || icon.nil?
48
51
  fill, stroke = icon || [GD::GIS::ColorHelpers.random_rgb, GD::GIS::ColorHelpers.random_rgb]
49
52
  @icon = build_default_marker(fill, stroke)
53
+ elsif icon == "numeric" || icon == "alphabetic"
54
+ @icon = icon
55
+ @font_color = font_color
50
56
  else
51
57
  @icon = GD::Image.open(icon)
58
+ @icon.alpha_blending = true
59
+ @icon.save_alpha = true
52
60
  end
53
61
 
54
62
  @label = label
55
63
  @font = font
56
64
  @size = size
57
- @color = color
65
+ @r, @g, @b, @a = color
66
+ @a = 0 if @a.nil?
67
+ @count = count
58
68
 
59
- @icon.alpha_blending = true
60
- @icon.save_alpha = true
61
69
  end
62
70
 
63
71
  # Builds a default circular marker icon.
@@ -92,8 +100,22 @@ module GD
92
100
  #
93
101
  # @return [void]
94
102
  def render!(img, projector)
95
- w = @icon.width
96
- h = @icon.height
103
+
104
+ case @icon
105
+ when "numeric"
106
+ value = @count
107
+ when "alphabetic"
108
+ value = (@count + 96).chr
109
+ else
110
+ value = "*"
111
+ end
112
+
113
+ if @icon.is_a?(GD::Image)
114
+ w = @icon.width
115
+ h = @icon.height
116
+ else
117
+ w = radius_from_text(img, value, font: @font, size: @size) * 2
118
+ end
97
119
 
98
120
  @data.each do |row|
99
121
  lon = @lon.call(row)
@@ -101,25 +123,89 @@ module GD
101
123
 
102
124
  x, y = projector.call(lon, lat)
103
125
 
104
- # icono
105
- img.copy(@icon, x - (w / 2), y - (h / 2), 0, 0, w, h)
106
-
107
- # etiqueta opcional
108
126
  next unless @label && @font
109
127
 
110
128
  text = @label.call(row)
111
129
  next if text.nil? || text.strip.empty?
112
-
113
130
  font_h = @size * 1.1
114
131
 
132
+ if @icon == "numeric" || @icon == "alphabetic"
133
+
134
+ draw_symbol_circle!(
135
+ img: img,
136
+ x: x,
137
+ y: y,
138
+ symbol: value,
139
+ radius: 12,
140
+ bg_color: @color,
141
+ font_color: @font_color,
142
+ font: @font,
143
+ font_size: @size
144
+ )
145
+ else
146
+ img.copy(@icon, x - (w / 2), y - (h / 2), 0, 0, w, h)
147
+ end
148
+
115
149
  img.text(text,
116
- x: x + (w / 2) + 4,
117
- y: y + (font_h / 2),
118
- size: @size,
119
- color: @color,
120
- font: @font)
150
+ x: x + (w / 2) + 4,
151
+ y: y + (font_h / 2),
152
+ size: @size,
153
+ color: GD::Color.rgba(@r, @g, @b, @a),
154
+ font: @font)
121
155
  end
122
156
  end
157
+
158
+ # Draws a filled circle (bullet) with a centered numeric label.
159
+ #
160
+ # - x, y: circle center in pixels
161
+ # - y for text() is BASELINE (not top). We compute baseline to center the text.
162
+ def draw_symbol_circle!(img:, x:, y:, symbol:, radius:, bg_color:, font_color:, font:, font_size:, angle: 0.0)
163
+ diameter = radius_from_text(img, symbol, font: font, size: font_size) * 2
164
+
165
+ # 1) Bullet background
166
+ img.filled_ellipse(x, y, diameter, diameter, bg_color)
167
+
168
+ # 2) Measure text in pixels (matches rendering)
169
+ text = symbol.to_s
170
+ w, h = img.text_bbox(text, font: font, size: font_size, angle: angle)
171
+
172
+ # 3) Compute centered position:
173
+ # text() uses baseline Y, so:
174
+ # top_y = y - h/2
175
+ # baseline = top_y + h = y + h/2
176
+ text_x = (x - (w / 2.0)).round
177
+ text_y = (y + (h / 2.0)).round
178
+
179
+ # 4) Draw number
180
+ img.text(
181
+ text,
182
+ x: text_x,
183
+ y: text_y,
184
+ font: font,
185
+ size: font_size,
186
+ color: font_color
187
+ )
188
+ end
189
+
190
+ # Calculates a circle radius that fully contains the rendered text.
191
+ #
192
+ # img : GD::Image
193
+ # text : String (number, letters, etc.)
194
+ # font : path to .ttf
195
+ # size : font size in points
196
+ # padding : extra pixels around text (visual breathing room)
197
+ #
198
+ def radius_from_text(img, text, font:, size:, padding: 4)
199
+ w, h = img.text_bbox(
200
+ text.to_s,
201
+ font: font,
202
+ size: size
203
+ )
204
+
205
+ # Use the larger dimension to ensure the text fits
206
+ (([w, h].max / 2.0).ceil) + padding
207
+ end
208
+
123
209
  end
124
210
  end
125
211
  end
data/lib/gd/gis/map.rb CHANGED
@@ -113,6 +113,7 @@ module GD
113
113
  primary: [],
114
114
  secondary: [],
115
115
  street: [],
116
+ track: [],
116
117
  minor: [],
117
118
  rail: [],
118
119
  water: [],
@@ -132,6 +133,7 @@ module GD
132
133
 
133
134
  @debug = false
134
135
  @used_labels = {}
136
+ @count = 1
135
137
  end
136
138
 
137
139
  # Returns all features belonging to a given semantic layer.
@@ -155,7 +157,37 @@ module GD
155
157
  end
156
158
  end
157
159
 
160
+ # Creates a single text label for a named linear feature (LineString or
161
+ # MultiLineString), avoiding duplicate labels for the same named entity.
162
+ #
163
+ # Many datasets (especially OSM) split a single logical entity
164
+ # (rivers, streets, railways, etc.) into multiple line features that
165
+ # all share the same name. This method ensures that:
166
+ #
167
+ # - Only one label is created per unique entity name
168
+ # - The label is placed on a representative segment of the geometry
169
+ # - The logic is independent of the feature's semantic layer (water, road, rail)
170
+ #
171
+ # Labels are rendered using a PointsLayer because libgd-gis does not
172
+ # support text rendering directly on line geometries.
173
+ #
174
+ # The label position is chosen as the midpoint of the line coordinates.
175
+ # This is a simple heuristic that provides a reasonable placement without
176
+ # requiring geometry merging or topological analysis.
177
+ #
178
+ # @param feature [GD::GIS::Feature]
179
+ # A feature with a linear geometry and a "name" property.
180
+ #
181
+ # @return [void]
182
+ # Adds a PointsLayer to @points_layers if a label is created.
183
+ #
184
+ # @note
185
+ # This method must be called during feature loading (add_geojson),
186
+ # before rendering. It intentionally does not depend on map style
187
+ # configuration, which is applied later during rendering.
158
188
  def maybe_create_line_label(feature)
189
+ return true if @style.global[:label] == false || @style.global[:label].nil?
190
+
159
191
  geom = feature.geometry
160
192
  return unless LINE_GEOMS.include?(geom["type"])
161
193
 
@@ -171,17 +203,15 @@ module GD
171
203
 
172
204
  lon, lat = coords[coords.size / 2]
173
205
 
174
- puts @style
175
-
176
206
  @points_layers << GD::GIS::PointsLayer.new(
177
207
  [feature],
178
208
  lon: ->(_) { lon },
179
209
  lat: ->(_) { lat },
180
- icon: nil,
210
+ icon: @style.global[:label][:icon],
181
211
  label: ->(_) { name },
182
- font: GD::GIS::FontHelper.random,
183
- size: 10,
184
- color: GD::Color.rgb(0, 0, 0)
212
+ font: @style.global[:label][:font] || GD::GIS::FontHelper.random,
213
+ size: @style.global[:label][:size] || (6..20).to_a.sample,
214
+ color: @style.global[:label][:color] || GD::GIS::ColorHelpers.random_rgba
185
215
  )
186
216
 
187
217
  @used_labels[key] = true
@@ -207,7 +237,7 @@ module GD
207
237
  kind =
208
238
  case (feature.properties["objeto"] || feature.properties["waterway"]).to_s.downcase
209
239
  when /river|río|canal/ then :river
210
- when /stream|arroyo/ then :stream
240
+ when /stream|arroyo/ then :stream
211
241
  else :minor
212
242
  end
213
243
 
@@ -220,9 +250,7 @@ module GD
220
250
  @layers[:park] << feature
221
251
 
222
252
  when :track
223
- # elegí una:
224
- @layers[:minor] << feature
225
- # o @layers[:street] << feature
253
+ @layers[:track] << feature
226
254
  else
227
255
  geom_type = feature.geometry["type"]
228
256
 
@@ -231,20 +259,20 @@ module GD
231
259
  warn "Style error: missing 'points' section"
232
260
  end
233
261
 
234
- font = points_style[:font] || begin
262
+ font = @style.points[:font] || begin
235
263
  warn "[libgd-gis] points.font not defined in style, using random system font"
236
264
  GD::GIS::FontHelper.random
237
265
  end
238
266
 
239
- size = points_style[:size] || begin
267
+ size = @style.points[:size] || begin
240
268
  warn "[libgd-gis] points.font size not defined in style, using random system font size"
241
269
  (6..14).to_a.sample
242
270
  end
243
271
 
244
- raw_color = points_style[:color]
245
- color = @style.normalize_color(raw_color)
272
+ color = @style.points[:color] ? @style.normalize_color(@style.points[:color]) : GD::GIS::ColorHelpers.random_vivid
273
+ font_color = @style.points[:font_color] ? @style.normalize_color(@style.points[:font_color]) : [250, 250, 250, 0]
246
274
 
247
- icon = if points_style.key?(:icon_fill) && points_style.key?(:icon_stroke)
275
+ icon = if @style.points.key?(:icon_fill) && @style.points.key?(:icon_stroke)
248
276
  [points_style[:icon_stroke],
249
277
  points_style[:icon_stroke]]
250
278
  end
@@ -258,8 +286,11 @@ module GD
258
286
  label: ->(f) { f.properties["name"] },
259
287
  font: font,
260
288
  size: size,
261
- color: color
289
+ color: color,
290
+ font_color: font_color,
291
+ count: @count
262
292
  )
293
+ @count += 1
263
294
  elsif LINE_GEOMS.include?(geom_type)
264
295
  @layers[:minor] << feature
265
296
  end
@@ -272,8 +303,8 @@ module GD
272
303
  # @param data [Enumerable]
273
304
  # @param opts [Hash]
274
305
  # @return [void]
275
- def add_points(data, **opts)
276
- @points_layers << GD::GIS::PointsLayer.new(data, **opts)
306
+ def add_points(data, **)
307
+ @points_layers << GD::GIS::PointsLayer.new(data, **)
277
308
  end
278
309
 
279
310
  # Adds a generic lines overlay layer.
@@ -281,8 +312,8 @@ module GD
281
312
  # @param features [Array]
282
313
  # @param opts [Hash]
283
314
  # @return [void]
284
- def add_lines(features, **opts)
285
- @lines_layers << GD::GIS::LinesLayer.new(features, **opts)
315
+ def add_lines(features, **)
316
+ @lines_layers << GD::GIS::LinesLayer.new(features, **)
286
317
  end
287
318
 
288
319
  # Adds a generic polygons overlay layer.
@@ -290,8 +321,8 @@ module GD
290
321
  # @param polygons [Array]
291
322
  # @param opts [Hash]
292
323
  # @return [void]
293
- def add_polygons(polygons, **opts)
294
- @polygons_layers << GD::GIS::PolygonsLayer.new(polygons, **opts)
324
+ def add_polygons(polygons, **)
325
+ @polygons_layers << GD::GIS::PolygonsLayer.new(polygons, **)
295
326
  end
296
327
 
297
328
  # Renders the map.
@@ -438,6 +469,8 @@ module GD
438
469
  case kind
439
470
  when :street, :primary, :motorway, :secondary, :minor
440
471
  @style.roads[kind]
472
+ when :track
473
+ @style.track[kind]
441
474
  when :rail
442
475
  @style.rails
443
476
  when :water
@@ -475,7 +508,9 @@ module GD
475
508
  if POLY_GEOMS.include?(geom)
476
509
  f.draw(@image, projection, nil, nil, style)
477
510
  elsif style[:stroke]
478
- color = GD::Color.rgb(*style[:stroke])
511
+ r, g, b, a = style[:stroke]
512
+ a = 0 if a.nil?
513
+ color = GD::Color.rgba(r, g, b, a)
479
514
 
480
515
  color = GD::GIS::ColorHelpers.random_vivid if @debug
481
516
 
@@ -32,5 +32,4 @@ track:
32
32
  gps:
33
33
  name:
34
34
  - track
35
-
36
-
35
+ - Track
data/lib/gd/gis/style.rb CHANGED
@@ -14,6 +14,9 @@ module GD
14
14
  # to a {GD::GIS::Map} instance before rendering.
15
15
  #
16
16
  class Style
17
+ # @return [Hash] global styling rules
18
+ attr_reader :global
19
+
17
20
  # @return [Hash] road styling rules
18
21
  attr_reader :roads
19
22
 
@@ -29,6 +32,9 @@ module GD
29
32
  # @return [Hash] point styling rules
30
33
  attr_reader :points
31
34
 
35
+ # @return [Hash] track styling rules
36
+ attr_reader :track
37
+
32
38
  # @return [Array<Symbol>] drawing order of semantic layers
33
39
  attr_reader :order
34
40
 
@@ -36,13 +42,15 @@ module GD
36
42
  #
37
43
  # @param definition [Hash]
38
44
  # style definition with optional sections:
39
- # :roads, :rails, :water, :parks, :points, :order
45
+ # :global, :roads, :rails, :water, :parks, :points, :order, :track
40
46
  def initialize(definition)
47
+ @global = definition[:global] || {}
41
48
  @roads = definition[:roads] || {}
42
49
  @rails = definition[:rails] || {}
43
50
  @water = definition[:water] || {}
44
51
  @parks = definition[:parks] || {}
45
52
  @points = definition[:points] || {}
53
+ @track = definition[:track] || {}
46
54
  @order = definition[:order] || []
47
55
  end
48
56
 
@@ -70,8 +78,10 @@ module GD
70
78
  data = deep_symbolize(data)
71
79
 
72
80
  new(
81
+ global: data[:global],
73
82
  roads: data[:roads],
74
83
  rails: data[:rail] || data[:rails],
84
+ track: data[:track],
75
85
  water: data[:water],
76
86
  parks: data[:park] || data[:parks],
77
87
  points: data[:points],
data/lib/test.rb ADDED
@@ -0,0 +1,4 @@
1
+ def foo
2
+ end
3
+
4
+ foo(1, 2)
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: libgd-gis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Germán Alberto Giménez Silva
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-01-23 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: ruby-libgd
@@ -30,34 +29,6 @@ dependencies:
30
29
  - - ">="
31
30
  - !ruby/object:Gem::Version
32
31
  version: 0.2.3
33
- - !ruby/object:Gem::Dependency
34
- name: rubocop
35
- requirement: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '1.60'
40
- type: :development
41
- prerelease: false
42
- version_requirements: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - "~>"
45
- - !ruby/object:Gem::Version
46
- version: '1.60'
47
- - !ruby/object:Gem::Dependency
48
- name: rubocop-performance
49
- requirement: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - "~>"
52
- - !ruby/object:Gem::Version
53
- version: '1.20'
54
- type: :development
55
- prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - "~>"
59
- - !ruby/object:Gem::Version
60
- version: '1.20'
61
32
  description: A native GIS raster engine for Ruby built on libgd. Render maps, GeoJSON,
62
33
  heatmaps and tiles.
63
34
  email:
@@ -86,11 +57,12 @@ files:
86
57
  - lib/gd/gis/projection.rb
87
58
  - lib/gd/gis/style.rb
88
59
  - lib/libgd_gis.rb
60
+ - lib/test.rb
89
61
  homepage: https://github.com/ggerman/libgd-gis
90
62
  licenses:
91
63
  - MIT
92
- metadata: {}
93
- post_install_message:
64
+ metadata:
65
+ rubygems_mfa_required: 'true'
94
66
  rdoc_options: []
95
67
  require_paths:
96
68
  - lib
@@ -105,8 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
105
77
  - !ruby/object:Gem::Version
106
78
  version: '0'
107
79
  requirements: []
108
- rubygems_version: 3.5.22
109
- signing_key:
80
+ rubygems_version: 4.0.4
110
81
  specification_version: 4
111
82
  summary: Geospatial raster rendering for Ruby using libgd
112
83
  test_files: []