libgd-gis 0.4.1 → 0.4.3

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: ff2a73817209bdd6e71f77b83cd0f7ffaee27327e95c199e01bf9eb879b2ff53
4
- data.tar.gz: d5fe87d75561eba31a1cae4c46902a16dfcd9a70b525f8d6772682f05f1f9c56
3
+ metadata.gz: 8fdfcd8c25a4c2d5be9f69153957d4d3c5fbd2b82e26167554be17c7e0ff058c
4
+ data.tar.gz: 9b5b504f06e11d6a0ac465747237e8832986234bee3ded81f0360a5c6749d29f
5
5
  SHA512:
6
- metadata.gz: a9e41b51e0e1365eb60905131e2766981de954d74b92b16ba8abb45224e3c6df9215a96628cf23f7c90c6675d2acf98dbafe326c569ee45440dcedd1502f5d7d
7
- data.tar.gz: d2598e4392ee4c400a0cf2982d91916119222f9ec4dd869b4851d0b994a0a241f39ef483e720d66121fd070af3b4ba5c46068d2194d8a09b9ed4b61cfa259192
6
+ metadata.gz: 7ded6603953b5144db8794841f6c898aea15819960b8d1a3b89c46e7122e74bf7c0e344814a983d5fbede7f4472080dc39ea360743d6d73d24c82d31371e5dcc
7
+ data.tar.gz: 2a2b7f255b78b338e128d9a2a0bd293db0936da7a30cfdb94e710c541251937c798504b9f5c20fd5ccabb8845c10fc29a47878d886a723e0a66f3ce25effd4aa
data/README.md CHANGED
@@ -202,6 +202,35 @@ This design ensures predictable rendering and makes all visual decisions explici
202
202
  and reproducible.
203
203
 
204
204
 
205
+ ---
206
+
207
+
208
+ ## Named geographic extents
209
+
210
+ LibGD-GIS includes a global dataset of predefined geographic areas.
211
+ You can use them directly as the `bbox` parameter.
212
+
213
+ ### Example
214
+
215
+ ```ruby
216
+ map = GD::GIS::Map.new(
217
+ bbox: :argentina,
218
+ zoom: 5,
219
+ width: 800,
220
+ height: 600,
221
+ basemap: :osm
222
+ )
223
+ ```
224
+ You can also use continents or regions:
225
+
226
+ ```
227
+ bbox: :world
228
+ bbox: :europe
229
+ bbox: :south_america
230
+ bbox: :north_america
231
+ bbox: :asia
232
+ ```
233
+
205
234
  ---
206
235
 
207
236
  ## CRS Support
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GD
4
+ module GIS
5
+ # Resolves bounding box inputs into a normalized WGS84 bbox array.
6
+ #
7
+ # Accepts:
8
+ # - Symbol or String referencing a named extent (e.g. :world, :argentina)
9
+ # - Array in the form [min_lng, min_lat, max_lng, max_lat]
10
+ #
11
+ # Returns a 4-element array of Float values.
12
+ #
13
+ # @example Using a named extent
14
+ # BBoxResolver.resolve(:europe)
15
+ #
16
+ # @example Using a raw bbox
17
+ # BBoxResolver.resolve([-10, 35, 5, 45])
18
+ module BBoxResolver
19
+ def self.resolve(bbox)
20
+ case bbox
21
+ when Symbol, String
22
+ Extents.fetch(bbox)
23
+
24
+ when Array
25
+ validate!(bbox)
26
+ bbox.map(&:to_f)
27
+
28
+ else
29
+ raise ArgumentError,
30
+ "bbox must be Symbol, String or [min_lng, min_lat, max_lng, max_lat]"
31
+ end
32
+ end
33
+
34
+ def self.validate!(bbox)
35
+ return if bbox.is_a?(Array) && bbox.size == 4
36
+
37
+ raise ArgumentError,
38
+ "bbox must be [min_lng, min_lat, max_lng, max_lat]"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,95 @@
1
+ {
2
+ "world": [-180.0, -90.0, 180.0, 90.0],
3
+
4
+ "northern_hemisphere": [-180.0, 0.0, 180.0, 90.0],
5
+ "southern_hemisphere": [-180.0, -90.0, 180.0, 0.0],
6
+ "eastern_hemisphere": [0.0, -90.0, 180.0, 90.0],
7
+ "western_hemisphere": [-180.0, -90.0, 0.0, 90.0],
8
+
9
+ "africa": [-25.0, -35.0, 51.0, 38.0],
10
+ "antarctica": [-180.0, -90.0, 180.0, -60.0],
11
+ "asia": [26.0, -11.0, 169.0, 78.0],
12
+ "europe": [-31.0, 34.0, 39.0, 72.0],
13
+ "north_america": [-168.0, 7.0, -52.0, 83.0],
14
+ "south_america": [-82.0, -56.0, -34.0, 13.0],
15
+ "oceania": [110.0, -50.0, 180.0, 0.0],
16
+
17
+ "argentina": [-73.6, -55.1, -53.6, -21.7],
18
+ "bolivia": [-69.7, -22.9, -57.5, -9.7],
19
+ "brazil": [-74.0, -34.0, -34.0, 5.3],
20
+ "chile": [-75.6, -56.0, -66.4, -17.5],
21
+ "colombia": [-79.0, -4.2, -66.9, 13.5],
22
+ "ecuador": [-81.0, -5.0, -75.0, 1.5],
23
+ "guyana": [-61.4, 1.2, -56.5, 8.6],
24
+ "paraguay": [-62.6, -27.6, -54.3, -19.3],
25
+ "peru": [-81.4, -18.4, -68.7, -0.0],
26
+ "suriname": [-58.1, 1.8, -53.9, 6.0],
27
+ "uruguay": [-58.4, -35.0, -53.1, -30.1],
28
+ "venezuela": [-73.4, 0.6, -59.8, 12.2],
29
+
30
+ "canada": [-141.0, 41.0, -52.6, 83.1],
31
+ "united_states": [-125.0, 24.0, -66.9, 49.4],
32
+ "mexico": [-118.4, 14.5, -86.7, 32.7],
33
+ "greenland": [-73.0, 59.8, -12.2, 83.6],
34
+
35
+ "iceland": [-24.5, 63.3, -13.5, 66.6],
36
+ "ireland": [-10.5, 51.4, -6.0, 55.5],
37
+ "united_kingdom": [-8.6, 49.8, 1.8, 60.9],
38
+ "portugal": [-9.6, 36.9, -6.2, 42.2],
39
+ "spain": [-9.5, 35.9, 3.3, 43.8],
40
+ "france": [-5.2, 41.3, 9.6, 51.1],
41
+ "belgium": [2.5, 49.5, 6.4, 51.5],
42
+ "netherlands": [3.3, 50.7, 7.2, 53.7],
43
+ "germany": [5.9, 47.3, 15.0, 55.1],
44
+ "switzerland": [5.9, 45.8, 10.5, 47.8],
45
+ "italy": [6.6, 36.6, 18.5, 47.1],
46
+ "austria": [9.5, 46.4, 17.2, 49.0],
47
+ "poland": [14.1, 49.0, 24.1, 54.8],
48
+ "czechia": [12.1, 48.5, 18.9, 51.1],
49
+ "hungary": [16.1, 45.7, 22.9, 48.6],
50
+ "greece": [19.4, 34.8, 28.2, 41.8],
51
+ "norway": [4.9, 58.0, 31.3, 71.2],
52
+ "sweden": [11.1, 55.3, 24.2, 69.1],
53
+ "finland": [20.6, 59.8, 31.6, 70.1],
54
+ "denmark": [8.0, 54.5, 12.7, 57.8],
55
+
56
+ "turkey": [26.0, 36.0, 45.0, 42.1],
57
+ "russia": [19.0, 41.0, 180.0, 82.0],
58
+
59
+ "saudi_arabia": [34.5, 16.3, 55.7, 32.2],
60
+ "iran": [44.0, 25.0, 63.3, 39.8],
61
+ "iraq": [38.8, 29.0, 48.6, 37.4],
62
+ "israel": [34.2, 29.5, 35.9, 33.3],
63
+ "jordan": [34.9, 29.2, 39.3, 33.4],
64
+ "syria": [35.7, 32.3, 42.4, 37.3],
65
+
66
+ "india": [68.1, 6.5, 97.4, 35.5],
67
+ "pakistan": [60.9, 23.7, 77.8, 37.1],
68
+ "bangladesh": [88.0, 20.7, 92.7, 26.6],
69
+ "china": [73.5, 18.1, 135.1, 53.6],
70
+ "mongolia": [87.7, 41.6, 119.9, 52.1],
71
+ "japan": [129.4, 31.0, 145.8, 45.5],
72
+ "south_korea": [125.0, 33.1, 131.9, 38.6],
73
+ "north_korea": [124.0, 37.7, 130.7, 43.0],
74
+
75
+ "thailand": [97.3, 5.6, 105.6, 20.5],
76
+ "vietnam": [102.1, 8.2, 109.5, 23.4],
77
+ "malaysia": [99.6, 0.8, 119.3, 7.4],
78
+ "indonesia": [95.0, -11.0, 141.0, 6.0],
79
+ "philippines": [116.9, 4.6, 126.6, 21.2],
80
+
81
+ "egypt": [24.7, 22.0, 36.9, 31.7],
82
+ "libya": [9.3, 19.5, 25.1, 33.2],
83
+ "algeria": [-8.7, 19.0, 12.0, 37.1],
84
+ "morocco": [-13.2, 27.7, -1.0, 35.9],
85
+ "tunisia": [7.5, 30.2, 11.6, 37.3],
86
+
87
+ "nigeria": [2.7, 4.2, 14.7, 13.9],
88
+ "ethiopia": [32.9, 3.4, 47.9, 14.9],
89
+ "kenya": [33.9, -4.7, 41.9, 5.0],
90
+ "south_africa": [16.4, -34.8, 32.9, -22.1],
91
+
92
+ "australia": [112.9, -43.7, 153.6, -10.7],
93
+ "new_zealand": [166.4, -47.3, 178.6, -34.4],
94
+ "papua_new_guinea": [140.8, -11.7, 156.0, -1.4]
95
+ }
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module GD
6
+ module GIS
7
+ # Provides access to predefined geographic extents loaded from
8
+ # a JSON dataset.
9
+ #
10
+ # Extents are WGS84 bounding boxes defined as:
11
+ # [min_lng, min_lat, max_lng, max_lat]
12
+ #
13
+ # Supports lookup by symbolic name.
14
+ #
15
+ # @example Fetch extent
16
+ # Extents.fetch(:world)
17
+ #
18
+ # @example Using bracket syntax
19
+ # Extents[:argentina]
20
+ #
21
+ # @note Bounding boxes are approximate and intended for visualization.
22
+ module Extents
23
+ DATA_PATH = File.expand_path(
24
+ "data/extents_global.json",
25
+ __dir__
26
+ )
27
+
28
+ @extents = nil
29
+
30
+ class << self
31
+ def fetch(name)
32
+ load_data!
33
+ @extents.fetch(name.to_s.downcase) do
34
+ raise ArgumentError, "Unknown extent: #{name}"
35
+ end
36
+ end
37
+
38
+ def [](name)
39
+ fetch(name)
40
+ end
41
+
42
+ def all
43
+ load_data!
44
+ @extents.keys.map(&:to_sym)
45
+ end
46
+
47
+ private
48
+
49
+ def load_data!
50
+ return if @extents
51
+
52
+ @extents = JSON.parse(File.read(DATA_PATH))
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -101,16 +101,15 @@ module GD
101
101
  #
102
102
  # @return [void]
103
103
  def render!(img, projector)
104
- value = case @icon
105
- when "numeric"
106
- @symbol
107
- when "alphabetic"
108
- (@symbol + 96).chr
109
- when "symbol"
110
- @symbol
111
- else
112
- @icon
113
- end
104
+ value =
105
+ case @icon
106
+ when "numeric", "symbol"
107
+ @symbol
108
+ when "alphabetic"
109
+ (@symbol + 96).chr
110
+ else
111
+ @icon
112
+ end
114
113
 
115
114
  if @icon.is_a?(GD::Image)
116
115
  w = @icon.width
@@ -176,7 +175,7 @@ module GD
176
175
  # baseline = top_y + h = y + h/2
177
176
  text_x = (x - (w / 2.0)).round
178
177
  text_y = (y + (h / 2.0)).round
179
-
178
+
180
179
  # 4) Draw number
181
180
  img.text(
182
181
  text,
data/lib/gd/gis/legend.rb CHANGED
@@ -1,9 +1,27 @@
1
- # lib/libgd/gis/legend.rb
1
+ # lib/gd/gis/legend.rb
2
2
 
3
3
  module GD
4
4
  module GIS
5
5
  LegendItem = Struct.new(:color, :label)
6
6
 
7
+ # Represents a map legend rendered as part of the final image.
8
+ #
9
+ # A Legend provides visual context for map elements by associating
10
+ # colors or symbols with human-readable labels.
11
+ #
12
+ # Legends are rendered server-side and embedded directly into the
13
+ # resulting map image, allowing the map to be self-explanatory
14
+ # without relying on external UI components.
15
+ #
16
+ # A Legend is typically created and configured via {Map#legend}
17
+ # and rendered automatically during the map rendering pipeline.
18
+ #
19
+ # @example Creating a legend
20
+ # map.legend do |l|
21
+ # l.add [76, 175, 80, 0], "Delivered"
22
+ # l.add [255, 193, 7, 0], "In transit"
23
+ # l.add [244, 67, 54, 0], "Delayed"
24
+ #
7
25
  class Legend
8
26
  attr_reader :items, :position
9
27
 
data/lib/gd/gis/map.rb CHANGED
@@ -43,8 +43,6 @@ module GD
43
43
  # @return [Boolean] enables debug rendering
44
44
  attr_reader :debug
45
45
 
46
- attr_reader :legend
47
-
48
46
  # Creates a new map.
49
47
  #
50
48
  # @param bbox [Array<Float>]
@@ -72,6 +70,9 @@ module GD
72
70
  crs: nil,
73
71
  fitted_bbox: false
74
72
  )
73
+ # resolve symbolic bbox
74
+ bbox = GD::GIS::BBoxResolver.resolve(bbox)
75
+
75
76
  # 1. Basic input validation
76
77
  raise ArgumentError, "bbox must be [min_lng, min_lat, max_lng, max_lat]" unless
77
78
  bbox.is_a?(Array) && bbox.size == 4
@@ -131,9 +132,6 @@ module GD
131
132
  @lines_layers = []
132
133
  @polygons_layers = []
133
134
 
134
- # 7. Style
135
- @style = nil
136
-
137
135
  @debug = false
138
136
  @used_labels = {}
139
137
  @count = 1
@@ -189,6 +187,7 @@ module GD
189
187
  # before rendering. It intentionally does not depend on map style
190
188
  # configuration, which is applied later during rendering.
191
189
  def maybe_create_line_label(feature)
190
+ @style ||= GD::GIS::Style.default
192
191
  return true if @style.global[:label] == false || @style.global[:label].nil?
193
192
 
194
193
  geom = feature.geometry
@@ -214,7 +213,8 @@ module GD
214
213
  label: ->(_) { name },
215
214
  font: @style.global[:label][:font] || GD::GIS::FontHelper.random,
216
215
  size: @style.global[:label][:size] || (6..20).to_a.sample,
217
- color: @style.global[:label][:color] || GD::GIS::ColorHelpers.random_rgba
216
+ color: @style.global[:label][:color] || GD::GIS::ColorHelpers.random_rgba,
217
+ font_color: @style.global[:label][:color] || GD::GIS::ColorHelpers.random_rgba
218
218
  )
219
219
 
220
220
  @used_labels[key] = true
@@ -230,11 +230,13 @@ module GD
230
230
 
231
231
  layers.each do |layer|
232
232
  next unless layer.respond_to?(:color)
233
+
233
234
  @legend.add(layer.color, layer.name)
234
235
  end
235
236
  end
236
237
 
237
238
  def draw_legend
239
+ @style ||= GD::GIS::Style.default
238
240
  return unless @legend
239
241
  return unless @image
240
242
  return unless @style
@@ -272,8 +274,8 @@ module GD
272
274
  w
273
275
  end
274
276
 
275
- width = (text_widths.max || 0) + box_size + padding * 3
276
- height = @legend.items.size * line_height + padding * 2
277
+ width = (text_widths.max || 0) + box_size + (padding * 3)
278
+ height = (@legend.items.size * line_height) + (padding * 2)
277
279
 
278
280
  # --- position --------------------------------------------
279
281
 
@@ -285,8 +287,6 @@ module GD
285
287
  [margin, @image.height - height - margin]
286
288
  when :top_right
287
289
  [@image.width - width - margin, margin]
288
- when :top_left
289
- [margin, margin]
290
290
  else
291
291
  [margin, margin]
292
292
  end
@@ -302,7 +302,7 @@ module GD
302
302
  # --- items -----------------------------------------------
303
303
 
304
304
  @legend.items.each_with_index do |item, idx|
305
- iy = y + padding + idx * line_height
305
+ iy = y + padding + (idx * line_height)
306
306
 
307
307
  # color box
308
308
  @image.filled_rectangle(
@@ -335,6 +335,7 @@ module GD
335
335
  # @param path [String] path to GeoJSON file
336
336
  # @return [void]
337
337
  def add_geojson(path)
338
+ @style ||= GD::GIS::Style.default
338
339
  features = LayerGeoJSON.load(path)
339
340
 
340
341
  features.each do |feature|
@@ -482,12 +483,14 @@ module GD
482
483
  label: label
483
484
  }
484
485
 
486
+ @style ||= GD::GIS::Style.default
487
+
485
488
  @points_layers << GD::GIS::PointsLayer.new(
486
489
  [row],
487
- lon: -> r { r[:lon] },
488
- lat: -> r { r[:lat] },
490
+ lon: ->(r) { r[:lon] },
491
+ lat: ->(r) { r[:lat] },
489
492
  icon: icon || @style.point[:icon],
490
- label: label ? -> r { r[:label] } : nil,
493
+ label: label ? ->(r) { r[:label] } : nil,
491
494
  font: font || @style.point[:font],
492
495
  size: size || @style.point[:size],
493
496
  color: color || @style.point[:color],
@@ -502,6 +505,7 @@ module GD
502
505
  # @param opts [Hash]
503
506
  # @return [void]
504
507
  def add_points(data, **)
508
+ @style ||= GD::GIS::Style.default
505
509
  @points_layers << GD::GIS::PointsLayer.new(data, **)
506
510
  end
507
511
 
@@ -511,6 +515,7 @@ module GD
511
515
  # @param opts [Hash]
512
516
  # @return [void]
513
517
  def add_lines(features, **)
518
+ @style ||= GD::GIS::Style.default
514
519
  @lines_layers << GD::GIS::LinesLayer.new(features, **)
515
520
  end
516
521
 
@@ -520,6 +525,7 @@ module GD
520
525
  # @param opts [Hash]
521
526
  # @return [void]
522
527
  def add_polygons(polygons, **)
528
+ @style ||= GD::GIS::Style.default
523
529
  @polygons_layers << GD::GIS::PolygonsLayer.new(polygons, **)
524
530
  end
525
531
 
@@ -531,6 +537,7 @@ module GD
531
537
  # @return [void]
532
538
  # @raise [RuntimeError] if style is not set
533
539
  def render
540
+ # Style assign DEFAULT Styles
534
541
  raise "map.style must be set" unless @style
535
542
 
536
543
  if @width && @height
@@ -585,7 +592,7 @@ module GD
585
592
  @polygons_layers.each { |l| l.render!(@image, projection) }
586
593
  @lines_layers.each { |l| l.render!(@image, projection) }
587
594
  @points_layers.each { |l| l.render!(@image, projection) }
588
-
595
+
589
596
  draw_legend
590
597
  end
591
598
 
@@ -646,7 +653,7 @@ module GD
646
653
  @polygons_layers.each { |l| l.render!(@image, projection) }
647
654
  @lines_layers.each { |l| l.render!(@image, projection) }
648
655
  @points_layers.each { |l| l.render!(@image, projection) }
649
-
656
+
650
657
  draw_legend
651
658
  end
652
659
 
data/lib/gd/gis/style.rb CHANGED
@@ -110,6 +110,80 @@ module GD
110
110
  end
111
111
  end
112
112
 
113
+ # Returns a built-in default style so Map can render even if no external style is set.
114
+ #
115
+ # @return [Style]
116
+ def self.default
117
+ new({
118
+ global: {
119
+ background: [15, 23, 42],
120
+ font: GD::GIS::FontHelper.find("DejaVuSans"),
121
+ font_color: [243, 244, 246],
122
+ label: {
123
+ color: [229, 231, 235],
124
+ font: GD::GIS::FontHelper.find("DejaVuSans"),
125
+ size: 12
126
+ }
127
+ },
128
+
129
+ label: {
130
+ label: { # ← envolver
131
+ color: [229, 231, 235],
132
+ font: GD::GIS::FontHelper.find("DejaVuSans"),
133
+ size: 12
134
+ }
135
+ },
136
+
137
+ roads: {
138
+ roads: { # ← envolver
139
+ color: [229, 231, 235],
140
+ font: GD::GIS::FontHelper.find("DejaVuSans"),
141
+ width: 6
142
+ }
143
+ },
144
+
145
+ rails: {
146
+ rails: {
147
+ color: [156, 163, 175],
148
+ font: GD::GIS::FontHelper.find("DejaVuSans"),
149
+ width: 5
150
+ }
151
+ },
152
+
153
+ water: {
154
+ water: {
155
+ color: [59, 130, 246]
156
+ }
157
+ },
158
+
159
+ parks: {
160
+ parks: {
161
+ color: [34, 197, 94]
162
+ }
163
+ },
164
+
165
+ points: {
166
+ points: {
167
+ color: [239, 68, 68],
168
+ radius: 4,
169
+ font: GD::GIS::FontHelper.find("DejaVuSans"),
170
+ font_color: [243, 244, 246]
171
+ }
172
+ },
173
+
174
+ track: {
175
+ track: { # ← CLAVE
176
+ stroke: [0, 85, 127, 250],
177
+ color: [250, 204, 21],
178
+ width: 2,
179
+ font_color: [243, 244, 246]
180
+ }
181
+ },
182
+
183
+ order: %i[water parks rails roads points track]
184
+ })
185
+ end
186
+
113
187
  # Normalizes a color definition into a GD::Color.
114
188
  #
115
189
  # Accepted formats:
data/lib/gd/gis.rb CHANGED
@@ -29,9 +29,11 @@ require "gd"
29
29
  #
30
30
  require_relative "gis/color_helpers"
31
31
  require_relative "gis/font_helper"
32
- require_relative "gis/style"
33
32
  require_relative "gis/classifier"
33
+ require_relative "gis/style"
34
34
 
35
+ require_relative "gis/extents"
36
+ require_relative "gis/bbox_resolver"
35
37
  require_relative "gis/feature"
36
38
  require_relative "gis/map"
37
39
  require_relative "gis/basemap"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: libgd-gis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Germán Alberto Giménez Silva
@@ -40,9 +40,12 @@ files:
40
40
  - README.md
41
41
  - lib/gd/gis.rb
42
42
  - lib/gd/gis/basemap.rb
43
+ - lib/gd/gis/bbox_resolver.rb
43
44
  - lib/gd/gis/classifier.rb
44
45
  - lib/gd/gis/color_helpers.rb
45
46
  - lib/gd/gis/crs_normalizer.rb
47
+ - lib/gd/gis/data/extents_global.json
48
+ - lib/gd/gis/extents.rb
46
49
  - lib/gd/gis/feature.rb
47
50
  - lib/gd/gis/font_helper.rb
48
51
  - lib/gd/gis/geometry.rb