libgd-gis 0.4.0 → 0.4.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 +4 -4
- data/lib/gd/gis/bbox_resolver.rb +27 -0
- data/lib/gd/gis/crs_normalizer.rb +67 -0
- data/lib/gd/gis/data/extents_global.json +95 -0
- data/lib/gd/gis/extents.rb +39 -0
- data/lib/gd/gis/layer_points.rb +10 -11
- data/lib/gd/gis/legend.rb +38 -0
- data/lib/gd/gis/map.rb +115 -3
- data/lib/gd/gis.rb +3 -0
- metadata +6 -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: fa4dd3c210385e6b89998eeccbc88c4e82d5c45b6081be260b98843b4bd01124
|
|
4
|
+
data.tar.gz: c0e6f00da6d017e7d30f8ccc6c14b4a16ee3a5e7799d5cbf8c4a4b72b137bde6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd5c30cbcdae3f214a9f0fb9605edd1244ca161774fc6e60fabcff70688b9b3712bc7444be326fd88c5517d470bfa8c06d8bc0522ca470edc86e5608235a26e9
|
|
7
|
+
data.tar.gz: 1cfd799c2c1abfb457ad761de7b293a97b5f880ddd55cd4e02298820c91f344977ee176abf8d7da9b77594247a1c8c1a1f34db951f69f15ff00ed7748d3c4da5
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module GD
|
|
2
|
+
module GIS
|
|
3
|
+
module BBoxResolver
|
|
4
|
+
def self.resolve(bbox)
|
|
5
|
+
case bbox
|
|
6
|
+
when Symbol, String
|
|
7
|
+
Extents.fetch(bbox)
|
|
8
|
+
|
|
9
|
+
when Array
|
|
10
|
+
validate!(bbox)
|
|
11
|
+
bbox.map(&:to_f)
|
|
12
|
+
|
|
13
|
+
else
|
|
14
|
+
raise ArgumentError,
|
|
15
|
+
"bbox must be Symbol, String or [min_lng, min_lat, max_lng, max_lat]"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.validate!(bbox)
|
|
20
|
+
unless bbox.is_a?(Array) && bbox.size == 4
|
|
21
|
+
raise ArgumentError,
|
|
22
|
+
"bbox must be [min_lng, min_lat, max_lng, max_lat]"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -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,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,39 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module GD
|
|
4
|
+
module GIS
|
|
5
|
+
module Extents
|
|
6
|
+
DATA_PATH = File.expand_path(
|
|
7
|
+
"data/extents_global.json",
|
|
8
|
+
__dir__
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
@extents = nil
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def fetch(name)
|
|
15
|
+
load_data!
|
|
16
|
+
@extents.fetch(name.to_s.downcase) do
|
|
17
|
+
raise ArgumentError, "Unknown extent: #{name}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def [](name)
|
|
22
|
+
fetch(name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def all
|
|
26
|
+
load_data!
|
|
27
|
+
@extents.keys.map(&:to_sym)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def load_data!
|
|
33
|
+
return if @extents
|
|
34
|
+
@extents = JSON.parse(File.read(DATA_PATH))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/gd/gis/layer_points.rb
CHANGED
|
@@ -101,16 +101,15 @@ module GD
|
|
|
101
101
|
#
|
|
102
102
|
# @return [void]
|
|
103
103
|
def render!(img, projector)
|
|
104
|
-
value =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# lib/gd/gis/legend.rb
|
|
2
|
+
|
|
3
|
+
module GD
|
|
4
|
+
module GIS
|
|
5
|
+
LegendItem = Struct.new(:color, :label)
|
|
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
|
+
#
|
|
25
|
+
class Legend
|
|
26
|
+
attr_reader :items, :position
|
|
27
|
+
|
|
28
|
+
def initialize(position: :bottom_right)
|
|
29
|
+
@position = position
|
|
30
|
+
@items = []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def add(color, label)
|
|
34
|
+
@items << LegendItem.new(color, label)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
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
|
|
@@ -69,6 +70,9 @@ module GD
|
|
|
69
70
|
crs: nil,
|
|
70
71
|
fitted_bbox: false
|
|
71
72
|
)
|
|
73
|
+
# resolve symbolic bbox
|
|
74
|
+
bbox = GD::GIS::BBoxResolver.resolve(bbox)
|
|
75
|
+
|
|
72
76
|
# 1. Basic input validation
|
|
73
77
|
raise ArgumentError, "bbox must be [min_lng, min_lat, max_lng, max_lat]" unless
|
|
74
78
|
bbox.is_a?(Array) && bbox.size == 4
|
|
@@ -217,6 +221,110 @@ module GD
|
|
|
217
221
|
@used_labels[key] = true
|
|
218
222
|
end
|
|
219
223
|
|
|
224
|
+
def legend(position: :bottom_right)
|
|
225
|
+
@legend = Legend.new(position: position)
|
|
226
|
+
yield @legend
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def legend_from_layers(position: :bottom_right)
|
|
230
|
+
@legend = Legend.new(position: position)
|
|
231
|
+
|
|
232
|
+
layers.each do |layer|
|
|
233
|
+
next unless layer.respond_to?(:color)
|
|
234
|
+
|
|
235
|
+
@legend.add(layer.color, layer.name)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def draw_legend
|
|
240
|
+
return unless @legend
|
|
241
|
+
return unless @image
|
|
242
|
+
return unless @style
|
|
243
|
+
return unless @style.global
|
|
244
|
+
return if @style.global[:label] == false
|
|
245
|
+
|
|
246
|
+
label_style = @style.global[:label] || {}
|
|
247
|
+
|
|
248
|
+
padding = 10
|
|
249
|
+
box_size = 12
|
|
250
|
+
line_height = 18
|
|
251
|
+
margin = 15
|
|
252
|
+
|
|
253
|
+
# --- font (from style) -----------------------------------
|
|
254
|
+
|
|
255
|
+
font_path =
|
|
256
|
+
case label_style[:font]
|
|
257
|
+
when nil, "default"
|
|
258
|
+
GD::GIS::FontHelper.random
|
|
259
|
+
else
|
|
260
|
+
label_style[:font]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
font_size = label_style[:size] || 10
|
|
264
|
+
font_color = GD::Color.rgba(*(label_style[:color] || [0, 0, 0, 0]))
|
|
265
|
+
|
|
266
|
+
# --- measure text (CORRECT API) ---------------------------
|
|
267
|
+
|
|
268
|
+
text_widths = @legend.items.map do |i|
|
|
269
|
+
w, = @image.text_bbox(
|
|
270
|
+
i.label,
|
|
271
|
+
font: font_path,
|
|
272
|
+
size: font_size
|
|
273
|
+
)
|
|
274
|
+
w
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
width = (text_widths.max || 0) + box_size + (padding * 3)
|
|
278
|
+
height = (@legend.items.size * line_height) + (padding * 2)
|
|
279
|
+
|
|
280
|
+
# --- position --------------------------------------------
|
|
281
|
+
|
|
282
|
+
x, y =
|
|
283
|
+
case @legend.position
|
|
284
|
+
when :bottom_right
|
|
285
|
+
[@image.width - width - margin, @image.height - height - margin]
|
|
286
|
+
when :bottom_left
|
|
287
|
+
[margin, @image.height - height - margin]
|
|
288
|
+
when :top_right
|
|
289
|
+
[@image.width - width - 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:
|
|
@@ -376,10 +484,10 @@ module GD
|
|
|
376
484
|
|
|
377
485
|
@points_layers << GD::GIS::PointsLayer.new(
|
|
378
486
|
[row],
|
|
379
|
-
lon: ->
|
|
380
|
-
lat: ->
|
|
487
|
+
lon: ->(r) { r[:lon] },
|
|
488
|
+
lat: ->(r) { r[:lat] },
|
|
381
489
|
icon: icon || @style.point[:icon],
|
|
382
|
-
label: label ? ->
|
|
490
|
+
label: label ? ->(r) { r[:label] } : nil,
|
|
383
491
|
font: font || @style.point[:font],
|
|
384
492
|
size: size || @style.point[:size],
|
|
385
493
|
color: color || @style.point[:color],
|
|
@@ -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.
|
data/lib/gd/gis.rb
CHANGED
|
@@ -32,6 +32,9 @@ require_relative "gis/font_helper"
|
|
|
32
32
|
require_relative "gis/style"
|
|
33
33
|
require_relative "gis/classifier"
|
|
34
34
|
|
|
35
|
+
require_relative "gis/extents"
|
|
36
|
+
require_relative "gis/bbox_resolver"
|
|
37
|
+
|
|
35
38
|
require_relative "gis/feature"
|
|
36
39
|
require_relative "gis/map"
|
|
37
40
|
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.
|
|
4
|
+
version: 0.4.2
|
|
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
|
|
@@ -50,8 +53,8 @@ files:
|
|
|
50
53
|
- lib/gd/gis/layer_lines.rb
|
|
51
54
|
- lib/gd/gis/layer_points.rb
|
|
52
55
|
- lib/gd/gis/layer_polygons.rb
|
|
56
|
+
- lib/gd/gis/legend.rb
|
|
53
57
|
- lib/gd/gis/map.rb
|
|
54
|
-
- lib/gd/gis/middleware.rb
|
|
55
58
|
- lib/gd/gis/ontology.rb
|
|
56
59
|
- lib/gd/gis/ontology.yml
|
|
57
60
|
- lib/gd/gis/projection.rb
|
|
@@ -76,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
76
79
|
- !ruby/object:Gem::Version
|
|
77
80
|
version: '0'
|
|
78
81
|
requirements: []
|
|
79
|
-
rubygems_version: 4.0.
|
|
82
|
+
rubygems_version: 4.0.6
|
|
80
83
|
specification_version: 4
|
|
81
84
|
summary: Geospatial raster rendering for Ruby using libgd
|
|
82
85
|
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
|