libgd-gis 0.4.0 → 0.4.1

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: 74dd1161f2cc225b81f304e90a5e60c2bf249da1606c7a84798367f3d1d1c4bb
4
- data.tar.gz: 51a6d88d8f49e6e14a363c68fbd9a0837aae91aef028b456f4df20865831fce8
3
+ metadata.gz: ff2a73817209bdd6e71f77b83cd0f7ffaee27327e95c199e01bf9eb879b2ff53
4
+ data.tar.gz: d5fe87d75561eba31a1cae4c46902a16dfcd9a70b525f8d6772682f05f1f9c56
5
5
  SHA512:
6
- metadata.gz: 78987b17ccf96eb35607632956a97add64353d9500a5ec9b208028a7064a11501ee5d0b19ce0b1134e41062dd35387b891eb1d67316737a0f7c83279223d1e2c
7
- data.tar.gz: e2a9b4e8149839b5b9d7b834757e304979a81e3879a4332f97159e75236be79ea4c4e179a54ed3efd94231c24d290edc83812793b2962ffdbd40ae12549bf40a
6
+ metadata.gz: a9e41b51e0e1365eb60905131e2766981de954d74b92b16ba8abb45224e3c6df9215a96628cf23f7c90c6675d2acf98dbafe326c569ee45440dcedd1502f5d7d
7
+ data.tar.gz: d2598e4392ee4c400a0cf2982d91916119222f9ec4dd869b4851d0b994a0a241f39ef483e720d66121fd070af3b4ba5c46068d2194d8a09b9ed4b61cfa259192
@@ -69,6 +69,9 @@ module GD
69
69
  when EPSG3857
70
70
  mercator_to_wgs84(lon, lat)
71
71
 
72
+ when "EPSG:22195"
73
+ gk_to_wgs84(lon, lat)
74
+
72
75
  else
73
76
  raise ArgumentError, "Unsupported CRS: #{@crs}"
74
77
  end
@@ -97,6 +100,70 @@ module GD
97
100
  lat = ((2 * Math.atan(Math.exp(y / r))) - (Math::PI / 2)) * 180.0 / Math::PI
98
101
  [lon, lat]
99
102
  end
103
+
104
+ # Converts Gauss–Krüger (GK) projected coordinates to WGS84.
105
+ #
106
+ # This method converts easting/northing coordinates from
107
+ # Gauss–Krüger Argentina Zone 5 (EPSG:22195) into
108
+ # WGS84 longitude/latitude (degrees).
109
+ #
110
+ # The implementation is intended for cartographic rendering
111
+ # and visualization purposes, not for high-precision geodesy.
112
+ #
113
+ # @param easting [Numeric]
114
+ # Easting value in meters.
115
+ #
116
+ # @param northing [Numeric]
117
+ # Northing value in meters.
118
+ #
119
+ # @return [Array<Float>]
120
+ # A `[longitude, latitude]` pair in decimal degrees (WGS84).
121
+ #
122
+ # @example Convert Gauss–Krüger coordinates
123
+ # gk_to_wgs84(580_000, 6_176_000)
124
+ # # => [longitude, latitude]
125
+ #
126
+ # @note
127
+ # This method assumes:
128
+ # - Central meridian: −60°
129
+ # - False easting: 500,000 m
130
+ # - WGS84-compatible ellipsoid
131
+ #
132
+ # @see https://epsg.io/22195
133
+
134
+ def gk_to_wgs84(easting, northing)
135
+ a = 6378137.0
136
+ f = 1 / 298.257223563
137
+ e2 = (2 * f) - (f * f)
138
+ lon0 = -60.0 * Math::PI / 180.0
139
+
140
+ x = easting - 500_000.0
141
+ y = northing - 10_000_000.0
142
+
143
+ m = y
144
+ mu = m / (a * (1 - (e2 / 4) - (3 * e2 * e2 / 64)))
145
+
146
+ e1 = (1 - Math.sqrt(1 - e2)) / (1 + Math.sqrt(1 - e2))
147
+
148
+ j1 = (3 * e1 / 2) - (27 * (e1**3) / 32)
149
+ j2 = (21 * (e1**2) / 16) - (55 * (e1**4) / 32)
150
+
151
+ fp = mu + (j1 * Math.sin(2 * mu)) + (j2 * Math.sin(4 * mu))
152
+
153
+ c1 = e2 * (Math.cos(fp)**2)
154
+ t1 = Math.tan(fp)**2
155
+ r1 = a * (1 - e2) / ((1 - (e2 * (Math.sin(fp)**2)))**1.5)
156
+ n1 = a / Math.sqrt(1 - (e2 * (Math.sin(fp)**2)))
157
+
158
+ d = x / n1
159
+
160
+ lat = fp - ((n1 * Math.tan(fp) / r1) *
161
+ (((d**2) / 2) - ((5 + (3 * t1) + (10 * c1)) * (d**4) / 24)))
162
+
163
+ lon = lon0 + ((d - ((1 + (2 * t1) + c1) * (d**3) / 6)) / Math.cos(fp))
164
+
165
+ [lon * 180.0 / Math::PI, lat * 180.0 / Math::PI]
166
+ end
100
167
  end
101
168
  end
102
169
  end
@@ -0,0 +1,20 @@
1
+ # lib/libgd/gis/legend.rb
2
+
3
+ module GD
4
+ module GIS
5
+ LegendItem = Struct.new(:color, :label)
6
+
7
+ class Legend
8
+ attr_reader :items, :position
9
+
10
+ def initialize(position: :bottom_right)
11
+ @position = position
12
+ @items = []
13
+ end
14
+
15
+ def add(color, label)
16
+ @items << LegendItem.new(color, label)
17
+ end
18
+ end
19
+ end
20
+ end
data/lib/gd/gis/map.rb CHANGED
@@ -7,6 +7,7 @@ require_relative "layer_geojson"
7
7
  require_relative "layer_points"
8
8
  require_relative "layer_lines"
9
9
  require_relative "layer_polygons"
10
+ require_relative "legend"
10
11
 
11
12
  LINE_GEOMS = %w[LineString MultiLineString].freeze
12
13
  POLY_GEOMS = %w[Polygon MultiPolygon].freeze
@@ -42,6 +43,8 @@ module GD
42
43
  # @return [Boolean] enables debug rendering
43
44
  attr_reader :debug
44
45
 
46
+ attr_reader :legend
47
+
45
48
  # Creates a new map.
46
49
  #
47
50
  # @param bbox [Array<Float>]
@@ -217,6 +220,111 @@ module GD
217
220
  @used_labels[key] = true
218
221
  end
219
222
 
223
+ def legend(position: :bottom_right)
224
+ @legend = Legend.new(position: position)
225
+ yield @legend
226
+ end
227
+
228
+ def legend_from_layers(position: :bottom_right)
229
+ @legend = Legend.new(position: position)
230
+
231
+ layers.each do |layer|
232
+ next unless layer.respond_to?(:color)
233
+ @legend.add(layer.color, layer.name)
234
+ end
235
+ end
236
+
237
+ def draw_legend
238
+ return unless @legend
239
+ return unless @image
240
+ return unless @style
241
+ return unless @style.global
242
+ return if @style.global[:label] == false
243
+
244
+ label_style = @style.global[:label] || {}
245
+
246
+ padding = 10
247
+ box_size = 12
248
+ line_height = 18
249
+ margin = 15
250
+
251
+ # --- font (from style) -----------------------------------
252
+
253
+ font_path =
254
+ case label_style[:font]
255
+ when nil, "default"
256
+ GD::GIS::FontHelper.random
257
+ else
258
+ label_style[:font]
259
+ end
260
+
261
+ font_size = label_style[:size] || 10
262
+ font_color = GD::Color.rgba(*(label_style[:color] || [0, 0, 0, 0]))
263
+
264
+ # --- measure text (CORRECT API) ---------------------------
265
+
266
+ text_widths = @legend.items.map do |i|
267
+ w, = @image.text_bbox(
268
+ i.label,
269
+ font: font_path,
270
+ size: font_size
271
+ )
272
+ w
273
+ end
274
+
275
+ width = (text_widths.max || 0) + box_size + padding * 3
276
+ height = @legend.items.size * line_height + padding * 2
277
+
278
+ # --- position --------------------------------------------
279
+
280
+ x, y =
281
+ case @legend.position
282
+ when :bottom_right
283
+ [@image.width - width - margin, @image.height - height - margin]
284
+ when :bottom_left
285
+ [margin, @image.height - height - margin]
286
+ when :top_right
287
+ [@image.width - width - margin, margin]
288
+ when :top_left
289
+ [margin, margin]
290
+ else
291
+ [margin, margin]
292
+ end
293
+
294
+ # --- background ------------------------------------------
295
+
296
+ bg = GD::Color.rgba(255, 255, 255, 80)
297
+ border = GD::Color.rgb(200, 200, 200)
298
+
299
+ @image.filled_rectangle(x, y, x + width, y + height, bg)
300
+ @image.rectangle(x, y, x + width, y + height, border)
301
+
302
+ # --- items -----------------------------------------------
303
+
304
+ @legend.items.each_with_index do |item, idx|
305
+ iy = y + padding + idx * line_height
306
+
307
+ # color box
308
+ @image.filled_rectangle(
309
+ x + padding,
310
+ iy,
311
+ x + padding + box_size,
312
+ iy + box_size,
313
+ GD::Color.rgba(*item.color)
314
+ )
315
+
316
+ # label text
317
+ @image.text_ft(
318
+ item.label,
319
+ x: x + padding + box_size + 8,
320
+ y: iy + box_size,
321
+ font: font_path,
322
+ size: font_size,
323
+ color: font_color
324
+ )
325
+ end
326
+ end
327
+
220
328
  # Loads features from a GeoJSON file.
221
329
  #
222
330
  # This method:
@@ -477,6 +585,8 @@ module GD
477
585
  @polygons_layers.each { |l| l.render!(@image, projection) }
478
586
  @lines_layers.each { |l| l.render!(@image, projection) }
479
587
  @points_layers.each { |l| l.render!(@image, projection) }
588
+
589
+ draw_legend
480
590
  end
481
591
 
482
592
  def render_viewport
@@ -536,6 +646,8 @@ module GD
536
646
  @polygons_layers.each { |l| l.render!(@image, projection) }
537
647
  @lines_layers.each { |l| l.render!(@image, projection) }
538
648
  @points_layers.each { |l| l.render!(@image, projection) }
649
+
650
+ draw_legend
539
651
  end
540
652
 
541
653
  # Saves the rendered image to disk.
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.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Germán Alberto Giménez Silva
@@ -50,8 +50,8 @@ files:
50
50
  - lib/gd/gis/layer_lines.rb
51
51
  - lib/gd/gis/layer_points.rb
52
52
  - lib/gd/gis/layer_polygons.rb
53
+ - lib/gd/gis/legend.rb
53
54
  - lib/gd/gis/map.rb
54
- - lib/gd/gis/middleware.rb
55
55
  - lib/gd/gis/ontology.rb
56
56
  - lib/gd/gis/ontology.yml
57
57
  - lib/gd/gis/projection.rb
@@ -76,7 +76,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
76
  - !ruby/object:Gem::Version
77
77
  version: '0'
78
78
  requirements: []
79
- rubygems_version: 4.0.5
79
+ rubygems_version: 4.0.6
80
80
  specification_version: 4
81
81
  summary: Geospatial raster rendering for Ruby using libgd
82
82
  test_files: []
@@ -1,152 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GD
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
- #
11
- module CRS
12
- # OGC CRS84 (longitude, latitude)
13
- CRS84 = "urn:ogc:def:crs:OGC:1.3:CRS84"
14
-
15
- # EPSG:4326 (latitude, longitude axis order)
16
- EPSG4326 = "EPSG:4326"
17
-
18
- # EPSG:3857 (Web Mercator)
19
- EPSG3857 = "EPSG:3857"
20
-
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
- #
42
- class Normalizer
43
- # Creates a new CRS normalizer.
44
- #
45
- # @param crs [String, Symbol, nil]
46
- # CRS identifier; defaults to CRS84 if nil
47
- def initialize(crs)
48
- @crs = normalize_name(crs)
49
- end
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
63
- def normalize(lon, lat)
64
- case @crs
65
- when CRS84
66
- [lon, lat]
67
-
68
- when EPSG4326
69
- # EPSG:4326 uses (lat, lon)
70
- [lat, lon]
71
-
72
- when GK_ARGENTINA
73
- gk_to_wgs84(lon, lat)
74
-
75
- when EPSG3857
76
- mercator_to_wgs84(lon, lat)
77
-
78
- else
79
- raise "Unsupported CRS: #{@crs}"
80
- end
81
- end
82
-
83
- private
84
-
85
- # Normalizes a CRS name into a comparable string.
86
- #
87
- # @param name [Object]
88
- # @return [String]
89
- def normalize_name(name)
90
- return CRS84 if name.nil?
91
-
92
- name.to_s.strip
93
- end
94
-
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
100
- def mercator_to_wgs84(x, y)
101
- r = 6378137.0
102
- lon = (x / r) * 180.0 / Math::PI
103
- lat = ((2 * Math.atan(Math.exp(y / r))) - (Math::PI / 2)) * 180.0 / Math::PI
104
- [lon, lat]
105
- end
106
-
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
115
- def gk_to_wgs84(easting, northing)
116
- # Parameters for Argentina GK Zone 5
117
- a = 6378137.0
118
- f = 1 / 298.257223563
119
- e2 = (2 * f) - (f * f)
120
- lon0 = -60.0 * Math::PI / 180.0 # central meridian zone 5
121
-
122
- x = easting - 500000.0
123
- y = northing
124
-
125
- m = y
126
- mu = m / (a * (1 - (e2 / 4) - (3 * e2 * e2 / 64)))
127
-
128
- e1 = (1 - Math.sqrt(1 - e2)) / (1 + Math.sqrt(1 - e2))
129
-
130
- j1 = (3 * e1 / 2) - (27 * (e1**3) / 32)
131
- j2 = (21 * (e1**2) / 16) - (55 * (e1**4) / 32)
132
-
133
- fp = mu + (j1 * Math.sin(2 * mu)) + (j2 * Math.sin(4 * mu))
134
-
135
- c1 = e2 * (Math.cos(fp)**2)
136
- t1 = Math.tan(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)))
139
-
140
- d = x / n1
141
-
142
- lat = fp - ((n1 * Math.tan(fp) / r1) *
143
- (((d**2) / 2) - ((5 + (3 * t1) + (10 * c1)) * (d**4) / 24)))
144
-
145
- lon = lon0 + ((d - ((1 + (2 * t1) + c1) * (d**3) / 6)) / Math.cos(fp))
146
-
147
- [lon * 180.0 / Math::PI, lat * 180.0 / Math::PI]
148
- end
149
- end
150
- end
151
- end
152
- end