map_view 0.0.1a
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 +7 -0
- data/README.md +188 -0
- data/lib/gd/gis/basemap.rb +178 -0
- data/lib/gd/gis/classifier.rb +57 -0
- data/lib/gd/gis/color_helpers.rb +65 -0
- data/lib/gd/gis/crs_normalizer.rb +57 -0
- data/lib/gd/gis/feature.rb +153 -0
- data/lib/gd/gis/geometry.rb +235 -0
- data/lib/gd/gis/input/detector.rb +34 -0
- data/lib/gd/gis/input/geojson.rb +0 -0
- data/lib/gd/gis/input/kml.rb +0 -0
- data/lib/gd/gis/input/shapefile.rb +0 -0
- data/lib/gd/gis/layer_geojson.rb +66 -0
- data/lib/gd/gis/layer_lines.rb +44 -0
- data/lib/gd/gis/layer_points.rb +78 -0
- data/lib/gd/gis/layer_polygons.rb +54 -0
- data/lib/gd/gis/map.rb +370 -0
- data/lib/gd/gis/middleware.rb +89 -0
- data/lib/gd/gis/ontology.rb +26 -0
- data/lib/gd/gis/ontology.yml +28 -0
- data/lib/gd/gis/projection.rb +39 -0
- data/lib/gd/gis/style.rb +45 -0
- data/lib/gd/gis.rb +15 -0
- data/lib/libgd_gis.rb +44 -0
- metadata +87 -0
data/lib/gd/gis/map.rb
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
require_relative "basemap"
|
|
2
|
+
require_relative "projection"
|
|
3
|
+
require_relative "classifier"
|
|
4
|
+
require_relative "layer_geojson"
|
|
5
|
+
require_relative "layer_points"
|
|
6
|
+
require_relative "layer_lines"
|
|
7
|
+
require_relative "layer_polygons"
|
|
8
|
+
|
|
9
|
+
module GD
|
|
10
|
+
module GIS
|
|
11
|
+
attr_accessor :debug
|
|
12
|
+
|
|
13
|
+
class Map
|
|
14
|
+
TILE_SIZE = 256
|
|
15
|
+
|
|
16
|
+
attr_reader :image
|
|
17
|
+
attr_reader :layers
|
|
18
|
+
attr_accessor :style
|
|
19
|
+
|
|
20
|
+
def initialize(
|
|
21
|
+
bbox:,
|
|
22
|
+
zoom:,
|
|
23
|
+
basemap:,
|
|
24
|
+
width: nil,
|
|
25
|
+
height: nil,
|
|
26
|
+
crs: nil,
|
|
27
|
+
fitted_bbox: false
|
|
28
|
+
)
|
|
29
|
+
# --------------------------------------------------
|
|
30
|
+
# 1. Basic input validation
|
|
31
|
+
# --------------------------------------------------
|
|
32
|
+
raise ArgumentError, "bbox must be [min_lng, min_lat, max_lng, max_lat]" unless
|
|
33
|
+
bbox.is_a?(Array) && bbox.size == 4
|
|
34
|
+
|
|
35
|
+
raise ArgumentError, "zoom must be an Integer" unless zoom.is_a?(Integer)
|
|
36
|
+
|
|
37
|
+
if (width && !height) || (!width && height)
|
|
38
|
+
raise ArgumentError, "width and height must be provided together"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@zoom = zoom
|
|
42
|
+
@width = width
|
|
43
|
+
@height = height
|
|
44
|
+
|
|
45
|
+
# --------------------------------------------------
|
|
46
|
+
# 2. CRS normalization (input → WGS84 lon/lat)
|
|
47
|
+
# --------------------------------------------------
|
|
48
|
+
if crs
|
|
49
|
+
normalizer = GD::GIS::CRS::Normalizer.new(crs)
|
|
50
|
+
|
|
51
|
+
min_lng, min_lat = normalizer.normalize(bbox[0], bbox[1])
|
|
52
|
+
max_lng, max_lat = normalizer.normalize(bbox[2], bbox[3])
|
|
53
|
+
|
|
54
|
+
bbox = [min_lng, min_lat, max_lng, max_lat]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# --------------------------------------------------
|
|
58
|
+
# 3. Final bbox (viewport-aware if width/height)
|
|
59
|
+
# --------------------------------------------------
|
|
60
|
+
@bbox =
|
|
61
|
+
if width && height && !fitted_bbox
|
|
62
|
+
GD::GIS::Geometry.viewport_bbox(
|
|
63
|
+
bbox: bbox,
|
|
64
|
+
zoom: zoom,
|
|
65
|
+
width: width,
|
|
66
|
+
height: height
|
|
67
|
+
)
|
|
68
|
+
else
|
|
69
|
+
bbox
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# --------------------------------------------------
|
|
73
|
+
# 4. Basemap (uses FINAL bbox)
|
|
74
|
+
# --------------------------------------------------
|
|
75
|
+
@basemap = GD::GIS::Basemap.new(zoom, @bbox, basemap)
|
|
76
|
+
|
|
77
|
+
# --------------------------------------------------
|
|
78
|
+
# 5. Legacy semantic layers (REQUIRED by render)
|
|
79
|
+
# --------------------------------------------------
|
|
80
|
+
@layers = {
|
|
81
|
+
motorway: [],
|
|
82
|
+
primary: [],
|
|
83
|
+
secondary: [],
|
|
84
|
+
street: [],
|
|
85
|
+
minor: [],
|
|
86
|
+
rail: [],
|
|
87
|
+
water: [],
|
|
88
|
+
park: []
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Optional alias (semantic clarity, no behavior change)
|
|
92
|
+
@road_layers = @layers
|
|
93
|
+
|
|
94
|
+
# --------------------------------------------------
|
|
95
|
+
# 6. Overlay layers (generic)
|
|
96
|
+
# --------------------------------------------------
|
|
97
|
+
@points_layers = []
|
|
98
|
+
@lines_layers = []
|
|
99
|
+
@polygons_layers = []
|
|
100
|
+
|
|
101
|
+
# --------------------------------------------------
|
|
102
|
+
# 7. Style
|
|
103
|
+
# --------------------------------------------------
|
|
104
|
+
@style = nil
|
|
105
|
+
|
|
106
|
+
@debug = false
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def features_by_layer(layer)
|
|
110
|
+
return [] unless @layers[layer]
|
|
111
|
+
|
|
112
|
+
@layers[layer].map do |item|
|
|
113
|
+
item.is_a?(Array) ? item.last : item
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def features
|
|
118
|
+
@layers.values.flatten.map do |item|
|
|
119
|
+
item.is_a?(Array) ? item.last : item
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# -----------------------------------
|
|
124
|
+
# GeoJSON input (unchanged behavior)
|
|
125
|
+
# -----------------------------------
|
|
126
|
+
def add_geojson(path)
|
|
127
|
+
features = LayerGeoJSON.load(path)
|
|
128
|
+
|
|
129
|
+
features.each do |feature|
|
|
130
|
+
case feature.layer
|
|
131
|
+
when :water
|
|
132
|
+
kind =
|
|
133
|
+
case (feature.properties["objeto"] || feature.properties["waterway"]).to_s.downcase
|
|
134
|
+
when /river|río/ then :river
|
|
135
|
+
when /stream|arroyo/ then :stream
|
|
136
|
+
else :minor
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
@layers[:water] << [kind, feature]
|
|
140
|
+
|
|
141
|
+
when :roads
|
|
142
|
+
@layers[:street] << feature
|
|
143
|
+
|
|
144
|
+
when :parks
|
|
145
|
+
@layers[:park] << feature
|
|
146
|
+
|
|
147
|
+
when :track
|
|
148
|
+
# elegí una:
|
|
149
|
+
@layers[:minor] << feature
|
|
150
|
+
# o @layers[:street] << feature
|
|
151
|
+
else
|
|
152
|
+
geom_type = feature.geometry["type"]
|
|
153
|
+
if geom_type == "LineString" || geom_type == "MultiLineString"
|
|
154
|
+
@layers[:minor] << feature
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# -----------------------------------
|
|
161
|
+
# Overlay layers
|
|
162
|
+
# -----------------------------------
|
|
163
|
+
def add_points(data, **opts)
|
|
164
|
+
@points_layers << GD::GIS::PointsLayer.new(data, **opts)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def add_lines(features, **opts)
|
|
168
|
+
@lines_layers << GD::GIS::LinesLayer.new(features, **opts)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def add_polygons(polygons, **opts)
|
|
172
|
+
@polygons_layers << GD::GIS::PolygonsLayer.new(polygons, **opts)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# -----------------------------------
|
|
176
|
+
# Rendering (LEGACY, UNCHANGED)
|
|
177
|
+
# -----------------------------------
|
|
178
|
+
def render
|
|
179
|
+
raise "map.style must be set" unless @style
|
|
180
|
+
|
|
181
|
+
if @width && @height
|
|
182
|
+
render_viewport
|
|
183
|
+
else
|
|
184
|
+
render_tiles
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def render_tiles
|
|
189
|
+
raise "map.style must be set" unless @style
|
|
190
|
+
|
|
191
|
+
tiles, x_min, y_min = @basemap.fetch_tiles
|
|
192
|
+
|
|
193
|
+
xs = tiles.map { |t| t[0] }
|
|
194
|
+
ys = tiles.map { |t| t[1] }
|
|
195
|
+
|
|
196
|
+
cols = xs.max - xs.min + 1
|
|
197
|
+
rows = ys.max - ys.min + 1
|
|
198
|
+
|
|
199
|
+
width = cols * TILE_SIZE
|
|
200
|
+
height = rows * TILE_SIZE
|
|
201
|
+
|
|
202
|
+
origin_x = x_min * TILE_SIZE
|
|
203
|
+
origin_y = y_min * TILE_SIZE
|
|
204
|
+
|
|
205
|
+
@image = GD::Image.new(width, height)
|
|
206
|
+
@image.antialias = false
|
|
207
|
+
|
|
208
|
+
# Basemap
|
|
209
|
+
tiles.each do |x, y, file|
|
|
210
|
+
tile = GD::Image.open(file)
|
|
211
|
+
@image.copy(
|
|
212
|
+
tile,
|
|
213
|
+
(x - x_min) * TILE_SIZE,
|
|
214
|
+
(y - y_min) * TILE_SIZE,
|
|
215
|
+
0, 0, TILE_SIZE, TILE_SIZE
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
projection = lambda do |lon, lat|
|
|
220
|
+
x, y = GD::GIS::Projection.lonlat_to_global_px(lon, lat, @zoom)
|
|
221
|
+
[(x - origin_x).round, (y - origin_y).round]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# 1️⃣ GeoJSON semantic layers
|
|
225
|
+
@style.order.each do |kind|
|
|
226
|
+
draw_layer(kind, projection)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# 2️⃣ Generic overlays
|
|
230
|
+
@polygons_layers.each { |l| l.render!(@image, projection) }
|
|
231
|
+
@lines_layers.each { |l| l.render!(@image, projection) }
|
|
232
|
+
@points_layers.each { |l| l.render!(@image, projection) }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def render_viewport
|
|
236
|
+
raise "map.style must be set" unless @style
|
|
237
|
+
|
|
238
|
+
@image = GD::Image.new(@width, @height)
|
|
239
|
+
@image.antialias = false
|
|
240
|
+
|
|
241
|
+
# --------------------------------------------------
|
|
242
|
+
# 1. Compute global pixel bbox
|
|
243
|
+
# --------------------------------------------------
|
|
244
|
+
min_lng, min_lat, max_lng, max_lat = @bbox
|
|
245
|
+
|
|
246
|
+
x1, y1 = GD::GIS::Projection.lonlat_to_global_px(min_lng, max_lat, @zoom)
|
|
247
|
+
x2, y2 = GD::GIS::Projection.lonlat_to_global_px(max_lng, min_lat, @zoom)
|
|
248
|
+
|
|
249
|
+
# --------------------------------------------------
|
|
250
|
+
# 2. Fetch tiles
|
|
251
|
+
# --------------------------------------------------
|
|
252
|
+
tiles, = @basemap.fetch_tiles
|
|
253
|
+
|
|
254
|
+
# --------------------------------------------------
|
|
255
|
+
# 3. Draw tiles clipped to viewport
|
|
256
|
+
# --------------------------------------------------
|
|
257
|
+
tiles.each do |x, y, file|
|
|
258
|
+
tile = GD::Image.open(file)
|
|
259
|
+
|
|
260
|
+
tile_x = x * TILE_SIZE
|
|
261
|
+
tile_y = y * TILE_SIZE
|
|
262
|
+
|
|
263
|
+
dst_x = tile_x - x1
|
|
264
|
+
dst_y = tile_y - y1
|
|
265
|
+
|
|
266
|
+
src_x = [0, -dst_x].max
|
|
267
|
+
src_y = [0, -dst_y].max
|
|
268
|
+
|
|
269
|
+
draw_w = [TILE_SIZE - src_x, @width - dst_x - src_x].min
|
|
270
|
+
draw_h = [TILE_SIZE - src_y, @height - dst_y - src_y].min
|
|
271
|
+
|
|
272
|
+
next if draw_w <= 0 || draw_h <= 0
|
|
273
|
+
|
|
274
|
+
@image.copy(
|
|
275
|
+
tile,
|
|
276
|
+
dst_x + src_x,
|
|
277
|
+
dst_y + src_y,
|
|
278
|
+
src_x,
|
|
279
|
+
src_y,
|
|
280
|
+
draw_w,
|
|
281
|
+
draw_h
|
|
282
|
+
)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# --------------------------------------------------
|
|
286
|
+
# 4. Projection (viewport version)
|
|
287
|
+
# --------------------------------------------------
|
|
288
|
+
projection = lambda do |lon, lat|
|
|
289
|
+
GD::GIS::Geometry.project(lon, lat, @bbox, @zoom)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# --------------------------------------------------
|
|
293
|
+
# 5. REUSE the same render pipeline
|
|
294
|
+
# --------------------------------------------------
|
|
295
|
+
@style.order.each do |kind|
|
|
296
|
+
draw_layer(kind, projection)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
@polygons_layers.each { |l| l.render!(@image, projection) }
|
|
300
|
+
@lines_layers.each { |l| l.render!(@image, projection) }
|
|
301
|
+
@points_layers.each { |l| l.render!(@image, projection) }
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def save(path)
|
|
305
|
+
@image.save(path)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def draw_layer(kind, projection)
|
|
309
|
+
items = @layers[kind]
|
|
310
|
+
return if items.nil? || items.empty?
|
|
311
|
+
|
|
312
|
+
style =
|
|
313
|
+
case kind
|
|
314
|
+
when :street, :primary, :motorway, :secondary, :minor
|
|
315
|
+
@style.roads[kind]
|
|
316
|
+
when :rail
|
|
317
|
+
@style.rails
|
|
318
|
+
when :water
|
|
319
|
+
@style.water
|
|
320
|
+
when :park
|
|
321
|
+
@style.parks
|
|
322
|
+
else
|
|
323
|
+
@style.extra[kind] if @style.respond_to?(:extra)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
return if style.nil?
|
|
327
|
+
|
|
328
|
+
items.each do |item|
|
|
329
|
+
if kind == :water
|
|
330
|
+
water_kind, f = item
|
|
331
|
+
|
|
332
|
+
width =
|
|
333
|
+
case water_kind
|
|
334
|
+
when :river then 2.5
|
|
335
|
+
when :stream then 1.5
|
|
336
|
+
else 1
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
if style[:stroke]
|
|
340
|
+
color = GD::Color.rgb(*style[:stroke])
|
|
341
|
+
|
|
342
|
+
color = GD::GIS::ColorHelpers.random_vivid if @debug
|
|
343
|
+
|
|
344
|
+
f.draw(@image, projection, color, width, :water)
|
|
345
|
+
end
|
|
346
|
+
else
|
|
347
|
+
f = item
|
|
348
|
+
geom = f.geometry["type"]
|
|
349
|
+
|
|
350
|
+
if geom == "Polygon" || geom == "MultiPolygon"
|
|
351
|
+
f.draw(@image, projection, nil, nil, style)
|
|
352
|
+
else
|
|
353
|
+
if style[:stroke]
|
|
354
|
+
color = GD::Color.rgb(*style[:stroke])
|
|
355
|
+
|
|
356
|
+
color = GD::GIS::ColorHelpers.random_vivid if @debug
|
|
357
|
+
|
|
358
|
+
width = style[:stroke_width] ? style[:stroke_width].round : 1
|
|
359
|
+
width = 1 if width < 1
|
|
360
|
+
f.draw(@image, projection, color, width)
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
module GD
|
|
2
|
+
module GIS
|
|
3
|
+
module CRS
|
|
4
|
+
CRS84 = "urn:ogc:def:crs:OGC:1.3:CRS84"
|
|
5
|
+
EPSG4326 = "EPSG:4326"
|
|
6
|
+
EPSG3857 = "EPSG:3857"
|
|
7
|
+
GK_ARGENTINA = "EPSG:22195" # Gauss–Krüger Argentina (zone 5 example)
|
|
8
|
+
|
|
9
|
+
# Normalize any CRS → CRS84 (lon,lat in degrees)
|
|
10
|
+
class Normalizer
|
|
11
|
+
def initialize(crs)
|
|
12
|
+
@crs = normalize_name(crs)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def normalize(lon, lat)
|
|
16
|
+
case @crs
|
|
17
|
+
when CRS84
|
|
18
|
+
[lon, lat]
|
|
19
|
+
|
|
20
|
+
when EPSG4326
|
|
21
|
+
# EPSG:4326 uses (lat, lon)
|
|
22
|
+
[lat, lon]
|
|
23
|
+
|
|
24
|
+
when GK_ARGENTINA
|
|
25
|
+
gk_to_wgs84(lon, lat)
|
|
26
|
+
|
|
27
|
+
when EPSG3857
|
|
28
|
+
mercator_to_wgs84(lon, lat)
|
|
29
|
+
|
|
30
|
+
else
|
|
31
|
+
raise "Unsupported CRS: #{@crs}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def normalize_name(name)
|
|
38
|
+
return CRS84 if name.nil?
|
|
39
|
+
name.to_s.strip
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Web Mercator → WGS84
|
|
43
|
+
def mercator_to_wgs84(x, y)
|
|
44
|
+
r = 6378137.0
|
|
45
|
+
lon = (x / r) * 180.0 / Math::PI
|
|
46
|
+
lat = (2 * Math.atan(Math.exp(y / r)) - Math::PI / 2) * 180.0 / Math::PI
|
|
47
|
+
[lon, lat]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Gauss–Krüger Argentina (Zone 5) → WGS84
|
|
51
|
+
# This is enough precision for mapping
|
|
52
|
+
def gk_to_wgs84(easting, northing)
|
|
53
|
+
# Parameters for Argentina GK Zone 5
|
|
54
|
+
a = 6378137.0
|
|
55
|
+
f = 1 / 298.257223563
|
|
56
|
+
e2 = 2*f - f*f
|
|
57
|
+
lon0 = -60.0 * Math::PI / 180.0 # central meridian zone 5
|
|
58
|
+
|
|
59
|
+
x = easting - 500000.0
|
|
60
|
+
y = northing
|
|
61
|
+
|
|
62
|
+
m = y
|
|
63
|
+
mu = m / (a * (1 - e2/4 - 3*e2*e2/64))
|
|
64
|
+
|
|
65
|
+
e1 = (1 - Math.sqrt(1 - e2)) / (1 + Math.sqrt(1 - e2))
|
|
66
|
+
|
|
67
|
+
j1 = 3*e1/2 - 27*e1**3/32
|
|
68
|
+
j2 = 21*e1**2/16 - 55*e1**4/32
|
|
69
|
+
|
|
70
|
+
fp = mu + j1*Math.sin(2*mu) + j2*Math.sin(4*mu)
|
|
71
|
+
|
|
72
|
+
c1 = e2 * Math.cos(fp)**2
|
|
73
|
+
t1 = Math.tan(fp)**2
|
|
74
|
+
r1 = a * (1 - e2) / (1 - e2 * Math.sin(fp)**2)**1.5
|
|
75
|
+
n1 = a / Math.sqrt(1 - e2 * Math.sin(fp)**2)
|
|
76
|
+
|
|
77
|
+
d = x / n1
|
|
78
|
+
|
|
79
|
+
lat = fp - (n1*Math.tan(fp)/r1) *
|
|
80
|
+
(d**2/2 - (5 + 3*t1 + 10*c1)*d**4/24)
|
|
81
|
+
|
|
82
|
+
lon = lon0 + (d - (1 + 2*t1 + c1)*d**3/6) / Math.cos(fp)
|
|
83
|
+
|
|
84
|
+
[lon * 180.0 / Math::PI, lat * 180.0 / Math::PI]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
|
|
3
|
+
module GD
|
|
4
|
+
module GIS
|
|
5
|
+
class Ontology
|
|
6
|
+
def initialize(path = nil)
|
|
7
|
+
path ||= File.expand_path("ontology.yml", __dir__)
|
|
8
|
+
@rules = YAML.load_file(path)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def classify(properties)
|
|
12
|
+
@rules.each do |layer, sources|
|
|
13
|
+
sources.each do |source, rules|
|
|
14
|
+
rules.each do |key, values|
|
|
15
|
+
v = (properties[key.to_s] || properties[key.to_sym]).to_s.strip.downcase
|
|
16
|
+
values = values.map { |x| x.to_s.downcase }
|
|
17
|
+
|
|
18
|
+
return layer.to_sym if values.any? { |x| v.include?(x) }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
water:
|
|
2
|
+
ign:
|
|
3
|
+
objeto:
|
|
4
|
+
- canal
|
|
5
|
+
- río
|
|
6
|
+
- arroyo
|
|
7
|
+
- embalse
|
|
8
|
+
- laguna
|
|
9
|
+
- dique
|
|
10
|
+
- represa
|
|
11
|
+
gna:
|
|
12
|
+
- canal
|
|
13
|
+
- río
|
|
14
|
+
- arroyo
|
|
15
|
+
- embalse
|
|
16
|
+
- laguna
|
|
17
|
+
|
|
18
|
+
natural_earth:
|
|
19
|
+
featurecla:
|
|
20
|
+
- river
|
|
21
|
+
- lake
|
|
22
|
+
- reservoir
|
|
23
|
+
- riverbank
|
|
24
|
+
|
|
25
|
+
track:
|
|
26
|
+
gps:
|
|
27
|
+
name:
|
|
28
|
+
- track
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module GD
|
|
2
|
+
module GIS
|
|
3
|
+
module Projection
|
|
4
|
+
R = 6378137.0
|
|
5
|
+
|
|
6
|
+
def self.mercator_x(lon)
|
|
7
|
+
lon * Math::PI / 180.0 * R
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.mercator_y(lat)
|
|
11
|
+
Math.log(Math.tan(Math::PI/4 + lat * Math::PI / 360.0)) * R
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.lonlat_to_pixel(lon, lat, min_x, max_x, min_y, max_y, width, height)
|
|
15
|
+
x = mercator_x(lon)
|
|
16
|
+
y = mercator_y(lat)
|
|
17
|
+
|
|
18
|
+
px = (x - min_x) / (max_x - min_x) * width
|
|
19
|
+
py = height - (y - min_y) / (max_y - min_y) * height
|
|
20
|
+
|
|
21
|
+
[px.to_i, py.to_i]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
TILE_SIZE = 256
|
|
25
|
+
|
|
26
|
+
def self.lonlat_to_global_px(lon, lat, zoom)
|
|
27
|
+
lat = [[lat, 85.05112878].min, -85.05112878].max
|
|
28
|
+
n = 2.0 ** zoom
|
|
29
|
+
|
|
30
|
+
x = (lon + 180.0) / 360.0 * n * TILE_SIZE
|
|
31
|
+
|
|
32
|
+
lat_rad = lat * Math::PI / 180.0
|
|
33
|
+
y = (1.0 - Math.log(Math.tan(lat_rad) + 1.0 / Math.cos(lat_rad)) / Math::PI) / 2.0 * n * TILE_SIZE
|
|
34
|
+
|
|
35
|
+
[x, y]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/gd/gis/style.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
|
|
3
|
+
module GD
|
|
4
|
+
module GIS
|
|
5
|
+
class Style
|
|
6
|
+
attr_reader :roads, :rails, :water, :parks, :order
|
|
7
|
+
|
|
8
|
+
def initialize(definition)
|
|
9
|
+
@roads = definition[:roads] || {}
|
|
10
|
+
@rails = definition[:rails] || {}
|
|
11
|
+
@water = definition[:water] || {}
|
|
12
|
+
@parks = definition[:parks] || {}
|
|
13
|
+
@order = definition[:order] || []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.load(name, from: "styles")
|
|
17
|
+
path = File.join(from, "#{name}.yml")
|
|
18
|
+
raise "Style not found: #{path}" unless File.exist?(path)
|
|
19
|
+
|
|
20
|
+
data = YAML.load_file(path)
|
|
21
|
+
data = deep_symbolize(data)
|
|
22
|
+
|
|
23
|
+
new(
|
|
24
|
+
roads: data[:roads],
|
|
25
|
+
rails: data[:rail] || data[:rails],
|
|
26
|
+
water: data[:water],
|
|
27
|
+
parks: data[:park] || data[:parks],
|
|
28
|
+
order: (data[:order] || []).map(&:to_sym)
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.deep_symbolize(obj)
|
|
33
|
+
case obj
|
|
34
|
+
when Hash
|
|
35
|
+
obj.transform_keys(&:to_sym)
|
|
36
|
+
.transform_values { |v| deep_symbolize(v) }
|
|
37
|
+
when Array
|
|
38
|
+
obj.map { |v| deep_symbolize(v) }
|
|
39
|
+
else
|
|
40
|
+
obj
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/gd/gis.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require "gd"
|
|
2
|
+
|
|
3
|
+
require_relative "gis/color_helpers"
|
|
4
|
+
require_relative "gis/style"
|
|
5
|
+
require_relative "gis/classifier"
|
|
6
|
+
|
|
7
|
+
require_relative "gis/feature"
|
|
8
|
+
require_relative "gis/map"
|
|
9
|
+
require_relative "gis/basemap"
|
|
10
|
+
require_relative "gis/projection"
|
|
11
|
+
require_relative "gis/geometry"
|
|
12
|
+
require_relative "gis/layer_points"
|
|
13
|
+
require_relative "gis/layer_lines"
|
|
14
|
+
require_relative "gis/layer_polygons"
|
|
15
|
+
require_relative "gis/layer_geojson"
|
data/lib/libgd_gis.rb
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require "libgd_gis"
|
|
2
|
+
|
|
3
|
+
require "open-uri"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
require "gd"
|
|
6
|
+
|
|
7
|
+
module LibGD
|
|
8
|
+
module GIS
|
|
9
|
+
class Tile
|
|
10
|
+
attr_reader :z, :x, :y, :image
|
|
11
|
+
|
|
12
|
+
def self.osm(z:, x:, y:)
|
|
13
|
+
new(
|
|
14
|
+
z: z,
|
|
15
|
+
x: x,
|
|
16
|
+
y: y,
|
|
17
|
+
source: "https://api.maptiler.com/maps/basic/#{z}/#{x}/#{y}.png?key=GetYourOwnKey"
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(z:, x:, y:, source:)
|
|
22
|
+
@z = z
|
|
23
|
+
@x = x
|
|
24
|
+
@y = y
|
|
25
|
+
@source = source
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def render
|
|
29
|
+
tmp = Tempfile.new(["tile", ".png"])
|
|
30
|
+
tmp.binmode
|
|
31
|
+
tmp.write URI.open(@source).read
|
|
32
|
+
tmp.flush
|
|
33
|
+
|
|
34
|
+
@image = GD::Image.open(tmp.path)
|
|
35
|
+
ensure
|
|
36
|
+
tmp.close
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def save(path)
|
|
40
|
+
@image.save(path)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|