libgd-gis 0.2.6 → 0.2.8
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/color_helpers.rb +65 -0
- data/lib/gd/gis/crs_normalizer.rb +57 -0
- data/lib/gd/gis/feature.rb +3 -2
- data/lib/gd/gis/geometry.rb +211 -16
- 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 +53 -3
- data/lib/gd/gis/layer_lines.rb +27 -19
- data/lib/gd/gis/layer_points.rb +3 -6
- data/lib/gd/gis/layer_polygons.rb +22 -6
- data/lib/gd/gis/map.rb +215 -69
- 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.rb +1 -0
- metadata +19 -10
- data/lib/gd/gis/style/dark.rb +0 -49
- data/lib/gd/gis/style/light.rb +0 -49
- data/lib/gd/gis/style/solarized.rb +0 -49
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78e53ca4b47b3bd61cdc1a52268598fefbfa1797f2781a5296e4be856523787e
|
|
4
|
+
data.tar.gz: 2cbf66d159688eb784f2801527ba1856fc836c58211187c772ed1060ed702e36
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 55886e495283ecac661a25773d87fc98420cd38abaf8ca0f39fc9896b42c58d7e3da40f0aeaecb977def3c9d0eb721fc2520f7eb7d07fe6b5de89506cc1fae50
|
|
7
|
+
data.tar.gz: 90e030366511f4cc4c8aa8de17ac0ce17a129c56d71c2c3f46d2fb6a03a882c8408870c9ec5bc132c837d4a3a8bf4681385c77862cfd0fdde4b5f03378688c0e
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module GD
|
|
2
|
+
module GIS
|
|
3
|
+
module ColorHelpers
|
|
4
|
+
# --------------------------------------------------
|
|
5
|
+
# Random RGB color
|
|
6
|
+
# --------------------------------------------------
|
|
7
|
+
def self.random_rgb(min: 0, max: 255)
|
|
8
|
+
GD::Color.rgb(
|
|
9
|
+
rand(min..max),
|
|
10
|
+
rand(min..max),
|
|
11
|
+
rand(min..max)
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# --------------------------------------------------
|
|
16
|
+
# Random RGBA color
|
|
17
|
+
# --------------------------------------------------
|
|
18
|
+
def self.random_rgba(min: 0, max: 255, alpha: nil)
|
|
19
|
+
GD::Color.rgba(
|
|
20
|
+
rand(min..max),
|
|
21
|
+
rand(min..max),
|
|
22
|
+
rand(min..max),
|
|
23
|
+
alpha || rand(50..255)
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# --------------------------------------------------
|
|
28
|
+
# Random vivid color (avoid gray/mud)
|
|
29
|
+
# --------------------------------------------------
|
|
30
|
+
def self.random_vivid
|
|
31
|
+
h = rand
|
|
32
|
+
s = rand(0.6..1.0)
|
|
33
|
+
v = rand(0.7..1.0)
|
|
34
|
+
|
|
35
|
+
r, g, b = hsv_to_rgb(h, s, v)
|
|
36
|
+
GD::Color.rgb(r, g, b)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# --------------------------------------------------
|
|
40
|
+
# HSV → RGB
|
|
41
|
+
# --------------------------------------------------
|
|
42
|
+
def self.hsv_to_rgb(h, s, v)
|
|
43
|
+
i = (h * 6).floor
|
|
44
|
+
f = h * 6 - i
|
|
45
|
+
p = v * (1 - s)
|
|
46
|
+
q = v * (1 - f * s)
|
|
47
|
+
t = v * (1 - (1 - f) * s)
|
|
48
|
+
|
|
49
|
+
r, g, b =
|
|
50
|
+
case i % 6
|
|
51
|
+
when 0 then [v, t, p]
|
|
52
|
+
when 1 then [q, v, p]
|
|
53
|
+
when 2 then [p, v, t]
|
|
54
|
+
when 3 then [p, q, v]
|
|
55
|
+
when 4 then [t, p, v]
|
|
56
|
+
when 5 then [v, p, q]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
[(r * 255).to_i, (g * 255).to_i, (b * 255).to_i]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
|
|
8
|
+
class Normalizer
|
|
9
|
+
def initialize(crs)
|
|
10
|
+
@crs = normalize_name(crs)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Accepts:
|
|
14
|
+
# normalize(lon,lat)
|
|
15
|
+
# normalize(lon,lat,z)
|
|
16
|
+
# normalize([lon,lat])
|
|
17
|
+
# normalize([lon,lat,z])
|
|
18
|
+
def normalize(*args)
|
|
19
|
+
lon, lat = args.flatten
|
|
20
|
+
return nil if lon.nil? || lat.nil?
|
|
21
|
+
|
|
22
|
+
lon = lon.to_f
|
|
23
|
+
lat = lat.to_f
|
|
24
|
+
|
|
25
|
+
case @crs
|
|
26
|
+
when CRS84, nil
|
|
27
|
+
[lon, lat]
|
|
28
|
+
|
|
29
|
+
when EPSG4326
|
|
30
|
+
# axis order lat,lon → lon,lat
|
|
31
|
+
[lat, lon]
|
|
32
|
+
|
|
33
|
+
when EPSG3857
|
|
34
|
+
mercator_to_wgs84(lon, lat)
|
|
35
|
+
|
|
36
|
+
else
|
|
37
|
+
[lon, lat]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def normalize_name(name)
|
|
44
|
+
return nil if name.nil?
|
|
45
|
+
name.to_s.strip
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def mercator_to_wgs84(x, y)
|
|
49
|
+
r = 6378137.0
|
|
50
|
+
lon = (x / r) * 180.0 / Math::PI
|
|
51
|
+
lat = (2 * Math.atan(Math.exp(y / r)) - Math::PI / 2) * 180.0 / Math::PI
|
|
52
|
+
[lon, lat]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/gd/gis/feature.rb
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
module GD
|
|
2
2
|
module GIS
|
|
3
3
|
class Feature
|
|
4
|
-
attr_reader :geometry, :properties
|
|
4
|
+
attr_reader :geometry, :properties, :layer
|
|
5
5
|
|
|
6
|
-
def initialize(geometry, properties)
|
|
6
|
+
def initialize(geometry, properties, layer = nil)
|
|
7
7
|
@geometry = geometry
|
|
8
8
|
@properties = properties || {}
|
|
9
|
+
@layer = layer
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
# -------------------------------------------------
|
data/lib/gd/gis/geometry.rb
CHANGED
|
@@ -1,40 +1,235 @@
|
|
|
1
1
|
module GD
|
|
2
2
|
module GIS
|
|
3
3
|
module Geometry
|
|
4
|
+
|
|
5
|
+
TILE_SIZE = 256.0
|
|
6
|
+
MAX_LAT = 85.05112878
|
|
7
|
+
|
|
8
|
+
# --------------------------------------------------
|
|
9
|
+
# Validation
|
|
10
|
+
# --------------------------------------------------
|
|
11
|
+
|
|
12
|
+
def self.validate_bbox!(bbox)
|
|
13
|
+
unless bbox.is_a?(Array) && bbox.size == 4
|
|
14
|
+
raise ArgumentError, "bbox must be [min_lng, min_lat, max_lng, max_lat]"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.validate_coords!(coords)
|
|
19
|
+
unless coords.is_a?(Array) && coords.size >= 2
|
|
20
|
+
raise ArgumentError, "coords must be an Array of at least 2 points"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# --------------------------------------------------
|
|
25
|
+
# Web Mercator Projection
|
|
26
|
+
# --------------------------------------------------
|
|
27
|
+
|
|
28
|
+
def self.lng_to_x(lng, zoom)
|
|
29
|
+
((lng + 180.0) / 360.0) * TILE_SIZE * (2**zoom)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.lat_to_y(lat, zoom)
|
|
33
|
+
lat = [[lat, MAX_LAT].min, -MAX_LAT].max
|
|
34
|
+
lat_rad = lat * Math::PI / 180.0
|
|
35
|
+
n = Math.log(Math.tan(Math::PI / 4.0 + lat_rad / 2.0))
|
|
36
|
+
(1.0 - n / Math::PI) / 2.0 * TILE_SIZE * (2**zoom)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.x_to_lng(x, zoom)
|
|
40
|
+
(x / (TILE_SIZE * (2**zoom))) * 360.0 - 180.0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.y_to_lat(y, zoom)
|
|
44
|
+
n = Math::PI - 2.0 * Math::PI * y / (TILE_SIZE * (2**zoom))
|
|
45
|
+
180.0 / Math::PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# --------------------------------------------------
|
|
49
|
+
# Viewport
|
|
50
|
+
# --------------------------------------------------
|
|
51
|
+
|
|
52
|
+
def self.viewport_bbox(bbox:, zoom:, width:, height:)
|
|
53
|
+
validate_bbox!(bbox)
|
|
54
|
+
|
|
55
|
+
min_lng, min_lat, max_lng, max_lat = bbox
|
|
56
|
+
|
|
57
|
+
center_lng = (min_lng + max_lng) / 2.0
|
|
58
|
+
center_lat = (min_lat + max_lat) / 2.0
|
|
59
|
+
|
|
60
|
+
center_x = lng_to_x(center_lng, zoom)
|
|
61
|
+
center_y = lat_to_y(center_lat, zoom)
|
|
62
|
+
|
|
63
|
+
half_w = width / 2.0
|
|
64
|
+
half_h = height / 2.0
|
|
65
|
+
|
|
66
|
+
min_x = center_x - half_w
|
|
67
|
+
max_x = center_x + half_w
|
|
68
|
+
min_y = center_y - half_h
|
|
69
|
+
max_y = center_y + half_h
|
|
70
|
+
|
|
71
|
+
[
|
|
72
|
+
x_to_lng(min_x, zoom),
|
|
73
|
+
y_to_lat(max_y, zoom),
|
|
74
|
+
x_to_lng(max_x, zoom),
|
|
75
|
+
y_to_lat(min_y, zoom)
|
|
76
|
+
]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# --------------------------------------------------
|
|
80
|
+
# Geometry helpers
|
|
81
|
+
# --------------------------------------------------
|
|
82
|
+
|
|
83
|
+
# buffer_line(coords, meters)
|
|
84
|
+
#
|
|
85
|
+
# coords :: Array<[lng, lat]> in WGS84
|
|
86
|
+
# meters :: Numeric (approximate)
|
|
87
|
+
#
|
|
88
|
+
# NOTE:
|
|
89
|
+
# - Naive meters-to-degrees conversion
|
|
90
|
+
# - Suitable for visualization, not analysis
|
|
91
|
+
#
|
|
4
92
|
def self.buffer_line(coords, meters)
|
|
5
|
-
|
|
93
|
+
validate_coords!(coords)
|
|
94
|
+
|
|
95
|
+
left = []
|
|
6
96
|
right = []
|
|
7
97
|
|
|
8
|
-
coords.each_cons(2) do |a,b|
|
|
9
|
-
x1,y1 = a
|
|
10
|
-
x2,y2 = b
|
|
98
|
+
coords.each_cons(2) do |a, b|
|
|
99
|
+
x1, y1 = a
|
|
100
|
+
x2, y2 = b
|
|
11
101
|
|
|
12
102
|
dx = x2 - x1
|
|
13
103
|
dy = y2 - y1
|
|
14
104
|
|
|
15
|
-
len = Math.sqrt(dx*dx + dy*dy)
|
|
16
|
-
next if len
|
|
105
|
+
len = Math.sqrt(dx * dx + dy * dy)
|
|
106
|
+
next if len.zero?
|
|
17
107
|
|
|
18
|
-
# normal vector
|
|
19
108
|
nx = -dy / len
|
|
20
|
-
ny =
|
|
109
|
+
ny = dx / len
|
|
21
110
|
|
|
22
|
-
# offset in degrees (approx)
|
|
23
|
-
# 1 meter ≈ 1 / 111_320 degrees
|
|
24
111
|
off = meters / 111_320.0
|
|
25
112
|
|
|
26
|
-
left << [x1 + nx*off, y1 + ny*off]
|
|
27
|
-
right << [x1 - nx*off, y1 - ny*off]
|
|
113
|
+
left << [x1 + nx * off, y1 + ny * off]
|
|
114
|
+
right << [x1 - nx * off, y1 - ny * off]
|
|
28
115
|
end
|
|
29
116
|
|
|
30
|
-
|
|
31
|
-
x2,y2 = coords.last
|
|
117
|
+
x2, y2 = coords.last
|
|
32
118
|
left << [x2, y2]
|
|
33
119
|
right << [x2, y2]
|
|
34
120
|
|
|
35
|
-
|
|
36
|
-
polygon
|
|
121
|
+
left + right.reverse
|
|
37
122
|
end
|
|
123
|
+
|
|
124
|
+
def self.project(lng, lat, bbox, zoom)
|
|
125
|
+
min_lng, _min_lat, _max_lng, max_lat = bbox
|
|
126
|
+
|
|
127
|
+
world_x = lng_to_x(lng, zoom)
|
|
128
|
+
world_y = lat_to_y(lat, zoom)
|
|
129
|
+
|
|
130
|
+
offset_x = lng_to_x(min_lng, zoom)
|
|
131
|
+
offset_y = lat_to_y(max_lat, zoom)
|
|
132
|
+
|
|
133
|
+
[
|
|
134
|
+
world_x - offset_x,
|
|
135
|
+
world_y - offset_y
|
|
136
|
+
]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.bbox_for_image(path, zoom:, width:, height:, padding_px: 80)
|
|
140
|
+
data = JSON.parse(File.read(path))
|
|
141
|
+
points = []
|
|
142
|
+
|
|
143
|
+
data["features"].each do |f|
|
|
144
|
+
geom = f["geometry"]
|
|
145
|
+
next unless geom
|
|
146
|
+
collect_points(geom, points)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
raise "No coordinates found in GeoJSON" if points.empty?
|
|
150
|
+
|
|
151
|
+
# --------------------------------------------------
|
|
152
|
+
# 1. Project to pixel space
|
|
153
|
+
# --------------------------------------------------
|
|
154
|
+
xs = []
|
|
155
|
+
ys = []
|
|
156
|
+
|
|
157
|
+
points.each do |lon, lat|
|
|
158
|
+
xs << lng_to_x(lon, zoom)
|
|
159
|
+
ys << lat_to_y(lat, zoom)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
min_x = xs.min - padding_px
|
|
163
|
+
max_x = xs.max + padding_px
|
|
164
|
+
min_y = ys.min - padding_px
|
|
165
|
+
max_y = ys.max + padding_px
|
|
166
|
+
|
|
167
|
+
# --------------------------------------------------
|
|
168
|
+
# 2. Fit bbox to image aspect ratio
|
|
169
|
+
# --------------------------------------------------
|
|
170
|
+
target_ratio = width.to_f / height.to_f
|
|
171
|
+
current_ratio = (max_x - min_x) / (max_y - min_y)
|
|
172
|
+
|
|
173
|
+
if current_ratio > target_ratio
|
|
174
|
+
# too wide → expand vertically
|
|
175
|
+
new_h = (max_x - min_x) / target_ratio
|
|
176
|
+
delta = (new_h - (max_y - min_y)) / 2.0
|
|
177
|
+
min_y -= delta
|
|
178
|
+
max_y += delta
|
|
179
|
+
else
|
|
180
|
+
# too tall → expand horizontally
|
|
181
|
+
new_w = (max_y - min_y) * target_ratio
|
|
182
|
+
delta = (new_w - (max_x - min_x)) / 2.0
|
|
183
|
+
min_x -= delta
|
|
184
|
+
max_x += delta
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# --------------------------------------------------
|
|
188
|
+
# 3. Convert back to lon/lat
|
|
189
|
+
# --------------------------------------------------
|
|
190
|
+
[
|
|
191
|
+
x_to_lng(min_x, zoom),
|
|
192
|
+
y_to_lat(max_y, zoom),
|
|
193
|
+
x_to_lng(max_x, zoom),
|
|
194
|
+
y_to_lat(min_y, zoom)
|
|
195
|
+
]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def self.collect_points(geom, points)
|
|
199
|
+
case geom["type"]
|
|
200
|
+
when "Point"
|
|
201
|
+
points << geom["coordinates"]
|
|
202
|
+
|
|
203
|
+
when "MultiPoint", "LineString"
|
|
204
|
+
geom["coordinates"].each { |c| points << c }
|
|
205
|
+
|
|
206
|
+
when "MultiLineString", "Polygon"
|
|
207
|
+
geom["coordinates"].each do |line|
|
|
208
|
+
line.each { |c| points << c }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
when "MultiPolygon"
|
|
212
|
+
geom["coordinates"].each do |poly|
|
|
213
|
+
poly.each do |ring|
|
|
214
|
+
ring.each { |c| points << c }
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def self.bbox_around_point(lon, lat, radius_km:)
|
|
221
|
+
delta_lat = radius_km / 111.0
|
|
222
|
+
delta_lon = radius_km / (111.0 * Math.cos(lat * Math::PI / 180.0))
|
|
223
|
+
|
|
224
|
+
[
|
|
225
|
+
lon - delta_lon,
|
|
226
|
+
lat - delta_lat,
|
|
227
|
+
lon + delta_lon,
|
|
228
|
+
lat + delta_lat
|
|
229
|
+
]
|
|
230
|
+
end
|
|
231
|
+
|
|
38
232
|
end
|
|
39
233
|
end
|
|
40
234
|
end
|
|
235
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module GD
|
|
2
|
+
module GIS
|
|
3
|
+
module Input
|
|
4
|
+
module Detector
|
|
5
|
+
def self.detect(path)
|
|
6
|
+
return :geojson if geojson?(path)
|
|
7
|
+
return :kml if kml?(path)
|
|
8
|
+
return :shapefile if shapefile?(path)
|
|
9
|
+
return :osm_pbf if pbf?(path)
|
|
10
|
+
:unknown
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.geojson?(path)
|
|
14
|
+
File.open(path) do |f|
|
|
15
|
+
head = f.read(2048)
|
|
16
|
+
head.include?('"FeatureCollection"') || head.include?('"GeometryCollection"')
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.kml?(path)
|
|
21
|
+
File.open(path) { |f| f.read(512).include?("<kml") }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.shapefile?(path)
|
|
25
|
+
File.open(path, "rb") { |f| f.read(4) == "\x00\x00\x27\x0A" }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.pbf?(path)
|
|
29
|
+
File.open(path, "rb") { |f| f.read(2) == "\x1f\x8b" }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
data/lib/gd/gis/layer_geojson.rb
CHANGED
|
@@ -1,16 +1,66 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
require_relative "feature"
|
|
3
|
+
require_relative "crs_normalizer"
|
|
4
|
+
require_relative "ontology"
|
|
3
5
|
|
|
4
6
|
module GD
|
|
5
7
|
module GIS
|
|
6
8
|
class LayerGeoJSON
|
|
9
|
+
|
|
7
10
|
def self.load(path)
|
|
8
11
|
data = JSON.parse(File.read(path))
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
|
|
13
|
+
# 1) Detect CRS
|
|
14
|
+
crs_name = data["crs"]&.dig("properties", "name")
|
|
15
|
+
normalizer = CRS::Normalizer.new(crs_name)
|
|
16
|
+
|
|
17
|
+
# 2) Load ontology
|
|
18
|
+
ontology = Ontology.new
|
|
19
|
+
|
|
20
|
+
# 3) Normalize geometries + classify
|
|
21
|
+
data["features"].map do |f|
|
|
22
|
+
normalize_geometry!(f["geometry"], normalizer)
|
|
23
|
+
layer = ontology.classify(f["properties"] || {})
|
|
24
|
+
Feature.new(f["geometry"], f["properties"], layer)
|
|
12
25
|
end
|
|
13
26
|
end
|
|
27
|
+
|
|
28
|
+
# --------------------------------------------
|
|
29
|
+
# CRS normalization (2D + 3D safe)
|
|
30
|
+
# --------------------------------------------
|
|
31
|
+
def self.normalize_geometry!(geometry, normalizer)
|
|
32
|
+
case geometry["type"]
|
|
33
|
+
|
|
34
|
+
when "Point"
|
|
35
|
+
geometry["coordinates"] =
|
|
36
|
+
normalizer.normalize(geometry["coordinates"])
|
|
37
|
+
|
|
38
|
+
when "LineString"
|
|
39
|
+
geometry["coordinates"] =
|
|
40
|
+
geometry["coordinates"].map { |c| normalizer.normalize(c) }
|
|
41
|
+
|
|
42
|
+
when "MultiLineString"
|
|
43
|
+
geometry["coordinates"] =
|
|
44
|
+
geometry["coordinates"].map do |line|
|
|
45
|
+
line.map { |c| normalizer.normalize(c) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
when "Polygon"
|
|
49
|
+
geometry["coordinates"] =
|
|
50
|
+
geometry["coordinates"].map do |ring|
|
|
51
|
+
ring.map { |c| normalizer.normalize(c) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
when "MultiPolygon"
|
|
55
|
+
geometry["coordinates"] =
|
|
56
|
+
geometry["coordinates"].map do |poly|
|
|
57
|
+
poly.map do |ring|
|
|
58
|
+
ring.map { |c| normalizer.normalize(c) }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
14
64
|
end
|
|
15
65
|
end
|
|
16
66
|
end
|
data/lib/gd/gis/layer_lines.rb
CHANGED
|
@@ -1,35 +1,43 @@
|
|
|
1
1
|
module GD
|
|
2
2
|
module GIS
|
|
3
3
|
class LinesLayer
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
attr_accessor :debug
|
|
5
|
+
|
|
6
|
+
def initialize(lines, stroke:, width:)
|
|
7
|
+
@lines = lines
|
|
6
8
|
@stroke = stroke
|
|
7
|
-
@width
|
|
9
|
+
@width = width
|
|
10
|
+
@debug = false
|
|
8
11
|
end
|
|
9
12
|
|
|
10
13
|
def render!(img, projection)
|
|
11
|
-
@
|
|
12
|
-
|
|
14
|
+
@lines.each do |line|
|
|
15
|
+
raise "Invalid line: #{line.inspect}" unless valid_line?(line)
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
pts = line.map do |lng, lat|
|
|
18
|
+
projection.call(lng, lat)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
color = @debug ? ColorHelpers.random_vivid : @stroke
|
|
22
|
+
|
|
23
|
+
pts.each_cons(2) do |a, b|
|
|
24
|
+
img.line(
|
|
25
|
+
a[0], a[1],
|
|
26
|
+
b[0], b[1],
|
|
27
|
+
color,
|
|
28
|
+
thickness: @width
|
|
29
|
+
)
|
|
21
30
|
end
|
|
22
31
|
end
|
|
23
32
|
end
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
pts = coords.map { |p| projection.call(p[0], p[1]) }
|
|
34
|
+
private
|
|
27
35
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
def valid_line?(line)
|
|
37
|
+
line.is_a?(Array) &&
|
|
38
|
+
line.size >= 2 &&
|
|
39
|
+
line.first.is_a?(Array) &&
|
|
40
|
+
line.first.size == 2
|
|
33
41
|
end
|
|
34
42
|
end
|
|
35
43
|
end
|
data/lib/gd/gis/layer_points.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module GD
|
|
2
2
|
module GIS
|
|
3
3
|
class PointsLayer
|
|
4
|
+
|
|
4
5
|
def initialize(data, lon:, lat:, icon:, label: nil, font: nil, size: 12, color: [0,0,0])
|
|
5
6
|
@data = data
|
|
6
7
|
@lon = lon
|
|
@@ -24,12 +25,8 @@ module GD
|
|
|
24
25
|
def build_default_marker
|
|
25
26
|
size = 32
|
|
26
27
|
img = GD::Image.new(size, size)
|
|
27
|
-
img.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
transparent = GD::Color.rgba(0,0,0,0)
|
|
31
|
-
img.filled_rectangle(0,0,size,size,transparent)
|
|
32
|
-
|
|
28
|
+
img.antialias = true
|
|
29
|
+
|
|
33
30
|
white = GD::Color.rgb(255,255,255)
|
|
34
31
|
black = GD::Color.rgb(0,0,0)
|
|
35
32
|
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
module GD
|
|
2
2
|
module GIS
|
|
3
3
|
class PolygonsLayer
|
|
4
|
+
attr_accessor :debug
|
|
5
|
+
|
|
4
6
|
def initialize(polygons, fill:, stroke: nil, width: nil)
|
|
5
7
|
@polygons = polygons
|
|
6
8
|
@fill = fill
|
|
7
9
|
@stroke = stroke
|
|
8
10
|
@width = width
|
|
11
|
+
|
|
12
|
+
@debug = false
|
|
9
13
|
end
|
|
10
14
|
|
|
11
15
|
def self.from_lines(features, stroke:, fill:, width:)
|
|
@@ -21,18 +25,30 @@ module GD
|
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
def render!(img, projection)
|
|
24
|
-
@polygons.each do |
|
|
25
|
-
|
|
28
|
+
@polygons.each do |polygon|
|
|
29
|
+
# polygon = [ ring, ring, ... ]
|
|
30
|
+
polygon.each_with_index do |ring, idx|
|
|
31
|
+
pts = ring.map do |lng, lat|
|
|
32
|
+
projection.call(lng, lat)
|
|
33
|
+
end
|
|
26
34
|
|
|
27
|
-
|
|
35
|
+
@stroke = GD::GIS::ColorHelpers.random_vivid if @debug
|
|
36
|
+
@fill = GD::GIS::ColorHelpers.random_vivid if @debug
|
|
28
37
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
img.
|
|
38
|
+
if idx == 0
|
|
39
|
+
# ring exterior
|
|
40
|
+
img.filled_polygon(pts, @fill)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if @stroke
|
|
44
|
+
pts.each_cons(2) do |a, b|
|
|
45
|
+
img.line(a[0], a[1], b[0], b[1], @stroke, thickness: (@width || 1))
|
|
46
|
+
end
|
|
32
47
|
end
|
|
33
48
|
end
|
|
34
49
|
end
|
|
35
50
|
end
|
|
51
|
+
|
|
36
52
|
end
|
|
37
53
|
end
|
|
38
54
|
end
|