map_view 0.0.1a → 0.1.0.pre.alpha
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/README.md +0 -188
- data/Rakefile +7 -0
- data/app/controllers/map_view/maps_controller.rb +20 -0
- data/config/initializers/map_view.rb +4 -0
- data/config/routes.rb +4 -0
- data/lib/generators/map_view/install_generator.rb +65 -0
- data/lib/generators/map_view/templates/default_styles.yml +45 -0
- data/lib/generators/map_view/templates/initializer.rb +14 -0
- data/lib/generators/map_view/templates/maps_controller.rb +15 -0
- data/lib/map_view/config.rb +67 -0
- data/lib/map_view/engine.rb +39 -0
- data/lib/map_view/version.rb +4 -0
- data/lib/map_view/view_helper.rb +116 -0
- data/lib/map_view.rb +24 -0
- metadata +83 -39
- data/lib/gd/gis/basemap.rb +0 -178
- data/lib/gd/gis/classifier.rb +0 -57
- data/lib/gd/gis/color_helpers.rb +0 -65
- data/lib/gd/gis/crs_normalizer.rb +0 -57
- data/lib/gd/gis/feature.rb +0 -153
- data/lib/gd/gis/geometry.rb +0 -235
- data/lib/gd/gis/input/detector.rb +0 -34
- data/lib/gd/gis/layer_geojson.rb +0 -66
- data/lib/gd/gis/layer_lines.rb +0 -44
- data/lib/gd/gis/layer_points.rb +0 -78
- data/lib/gd/gis/layer_polygons.rb +0 -54
- data/lib/gd/gis/map.rb +0 -370
- data/lib/gd/gis/middleware.rb +0 -89
- data/lib/gd/gis/ontology.rb +0 -26
- data/lib/gd/gis/ontology.yml +0 -28
- data/lib/gd/gis/projection.rb +0 -39
- data/lib/gd/gis/style.rb +0 -45
- data/lib/gd/gis.rb +0 -15
- data/lib/libgd_gis.rb +0 -44
- /data/{lib/gd/gis/input/geojson.rb → MIT-LICENSE} +0 -0
- /data/{lib/gd/gis/input/kml.rb → app/assets/stylesheets/map_view.css} +0 -0
- /data/{lib/gd/gis/input/shapefile.rb → app/views/map_view/_map.html.erb} +0 -0
data/lib/gd/gis/geometry.rb
DELETED
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
module GD
|
|
2
|
-
module GIS
|
|
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
|
-
#
|
|
92
|
-
def self.buffer_line(coords, meters)
|
|
93
|
-
validate_coords!(coords)
|
|
94
|
-
|
|
95
|
-
left = []
|
|
96
|
-
right = []
|
|
97
|
-
|
|
98
|
-
coords.each_cons(2) do |a, b|
|
|
99
|
-
x1, y1 = a
|
|
100
|
-
x2, y2 = b
|
|
101
|
-
|
|
102
|
-
dx = x2 - x1
|
|
103
|
-
dy = y2 - y1
|
|
104
|
-
|
|
105
|
-
len = Math.sqrt(dx * dx + dy * dy)
|
|
106
|
-
next if len.zero?
|
|
107
|
-
|
|
108
|
-
nx = -dy / len
|
|
109
|
-
ny = dx / len
|
|
110
|
-
|
|
111
|
-
off = meters / 111_320.0
|
|
112
|
-
|
|
113
|
-
left << [x1 + nx * off, y1 + ny * off]
|
|
114
|
-
right << [x1 - nx * off, y1 - ny * off]
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
x2, y2 = coords.last
|
|
118
|
-
left << [x2, y2]
|
|
119
|
-
right << [x2, y2]
|
|
120
|
-
|
|
121
|
-
left + right.reverse
|
|
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
|
-
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
|
|
@@ -1,34 +0,0 @@
|
|
|
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
|
data/lib/gd/gis/layer_geojson.rb
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
require_relative "feature"
|
|
3
|
-
require_relative "crs_normalizer"
|
|
4
|
-
require_relative "ontology"
|
|
5
|
-
|
|
6
|
-
module GD
|
|
7
|
-
module GIS
|
|
8
|
-
class LayerGeoJSON
|
|
9
|
-
|
|
10
|
-
def self.load(path)
|
|
11
|
-
data = JSON.parse(File.read(path))
|
|
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)
|
|
25
|
-
end
|
|
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
|
-
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
data/lib/gd/gis/layer_lines.rb
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
module GD
|
|
2
|
-
module GIS
|
|
3
|
-
class LinesLayer
|
|
4
|
-
attr_accessor :debug
|
|
5
|
-
|
|
6
|
-
def initialize(lines, stroke:, width:)
|
|
7
|
-
@lines = lines
|
|
8
|
-
@stroke = stroke
|
|
9
|
-
@width = width
|
|
10
|
-
@debug = false
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def render!(img, projection)
|
|
14
|
-
@lines.each do |line|
|
|
15
|
-
raise "Invalid line: #{line.inspect}" unless valid_line?(line)
|
|
16
|
-
|
|
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
|
-
)
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
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
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
data/lib/gd/gis/layer_points.rb
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
module GD
|
|
2
|
-
module GIS
|
|
3
|
-
class PointsLayer
|
|
4
|
-
|
|
5
|
-
def initialize(data, lon:, lat:, icon:, label: nil, font: nil, size: 12, color: [0,0,0])
|
|
6
|
-
@data = data
|
|
7
|
-
@lon = lon
|
|
8
|
-
@lat = lat
|
|
9
|
-
|
|
10
|
-
if icon
|
|
11
|
-
@icon = GD::Image.open(icon)
|
|
12
|
-
else
|
|
13
|
-
@icon = build_default_marker
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
@label = label
|
|
17
|
-
@font = font
|
|
18
|
-
@size = size
|
|
19
|
-
@color = color
|
|
20
|
-
|
|
21
|
-
@icon.alpha_blending = true
|
|
22
|
-
@icon.save_alpha = true
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def build_default_marker
|
|
26
|
-
size = 32
|
|
27
|
-
img = GD::Image.new(size, size)
|
|
28
|
-
img.antialias = true
|
|
29
|
-
|
|
30
|
-
white = GD::Color.rgb(255,255,255)
|
|
31
|
-
black = GD::Color.rgb(0,0,0)
|
|
32
|
-
|
|
33
|
-
cx = size / 2
|
|
34
|
-
cy = size / 2
|
|
35
|
-
r = 12
|
|
36
|
-
|
|
37
|
-
# borde blanco
|
|
38
|
-
img.arc(cx, cy, r*2+4, r*2+4, 0, 360, white)
|
|
39
|
-
|
|
40
|
-
# relleno negro
|
|
41
|
-
img.filled_arc(cx, cy, r*2, r*2, 0, 360, black)
|
|
42
|
-
|
|
43
|
-
img
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def render!(img, projector)
|
|
47
|
-
w = @icon.width
|
|
48
|
-
h = @icon.height
|
|
49
|
-
|
|
50
|
-
@data.each do |row|
|
|
51
|
-
lon = @lon.call(row)
|
|
52
|
-
lat = @lat.call(row)
|
|
53
|
-
|
|
54
|
-
x,y = projector.call(lon,lat)
|
|
55
|
-
|
|
56
|
-
# icono
|
|
57
|
-
img.copy(@icon, x - w/2, y - h/2, 0,0,w,h)
|
|
58
|
-
|
|
59
|
-
# etiqueta opcional
|
|
60
|
-
if @label && @font
|
|
61
|
-
text = @label.call(row)
|
|
62
|
-
unless text.nil? || text.strip.empty?
|
|
63
|
-
font_h = @size * 1.1
|
|
64
|
-
|
|
65
|
-
img.text(text,
|
|
66
|
-
x: x + w/2 + 4,
|
|
67
|
-
y: y + font_h/2,
|
|
68
|
-
size: @size,
|
|
69
|
-
color: @color,
|
|
70
|
-
font: @font
|
|
71
|
-
)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
module GD
|
|
2
|
-
module GIS
|
|
3
|
-
class PolygonsLayer
|
|
4
|
-
attr_accessor :debug
|
|
5
|
-
|
|
6
|
-
def initialize(polygons, fill:, stroke: nil, width: nil)
|
|
7
|
-
@polygons = polygons
|
|
8
|
-
@fill = fill
|
|
9
|
-
@stroke = stroke
|
|
10
|
-
@width = width
|
|
11
|
-
|
|
12
|
-
@debug = false
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.from_lines(features, stroke:, fill:, width:)
|
|
16
|
-
polys = []
|
|
17
|
-
|
|
18
|
-
features.each do |f|
|
|
19
|
-
coords = f["geometry"]["coordinates"]
|
|
20
|
-
poly = Geometry.buffer_line(coords, width)
|
|
21
|
-
polys << poly
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
new(polys, fill: fill, stroke: stroke)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def render!(img, projection)
|
|
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
|
|
34
|
-
|
|
35
|
-
@stroke = GD::GIS::ColorHelpers.random_vivid if @debug
|
|
36
|
-
@fill = GD::GIS::ColorHelpers.random_vivid if @debug
|
|
37
|
-
|
|
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
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|