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 +4 -4
- data/lib/gd/gis/crs_normalizer.rb +67 -0
- data/lib/gd/gis/legend.rb +20 -0
- data/lib/gd/gis/map.rb +112 -0
- metadata +3 -3
- data/lib/gd/gis/middleware.rb +0 -152
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ff2a73817209bdd6e71f77b83cd0f7ffaee27327e95c199e01bf9eb879b2ff53
|
|
4
|
+
data.tar.gz: d5fe87d75561eba31a1cae4c46902a16dfcd9a70b525f8d6772682f05f1f9c56
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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: []
|
data/lib/gd/gis/middleware.rb
DELETED
|
@@ -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
|