libgd-gis 0.2.2 → 0.2.3
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 +2 -1
- data/lib/gd/gis/basemap.rb +2 -2
- data/lib/gd/gis/classifier.rb +57 -0
- data/lib/gd/gis/dump.sh +19 -0
- data/lib/gd/gis/feature.rb +152 -0
- data/lib/gd/gis/layer_geojson.rb +6 -66
- data/lib/gd/gis/map.rb +137 -54
- data/lib/gd/gis/style/dark.rb +49 -0
- data/lib/gd/gis/style/light.rb +49 -0
- data/lib/gd/gis/style/solarized.rb +49 -0
- data/lib/gd/gis/style.rb +45 -0
- data/lib/gd/gis.rb +12 -1
- metadata +14 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9112994f7952782be773dc2a7bb8659d96769903b6e9c81b28a563cb2caa80eb
|
|
4
|
+
data.tar.gz: 569e8be0ac09110cf30b3b888317a5079fc975176988ee8051b7f3419587b721
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 85d38d16fdbc475ed5938c32a94819dabbdee7b4d706c369f8a84faec9753bf3765bcd5b4134a47ba62775ead533dad0c42a40cad46085d520f1333c0aacdb1c
|
|
7
|
+
data.tar.gz: ea38df19e492b85550edcebeb1e85157422e619bb7922cf4e49750bd9dc1978afc58e7a6163773de769aabfe60240803f062037c545774bf29f79bbae5324d4a
|
data/README.md
CHANGED
|
@@ -32,10 +32,11 @@
|
|
|
32
32
|
|
|
33
33
|
| Examples | Examples | Examples |
|
|
34
34
|
| :----: | :----: | :--: |
|
|
35
|
+
| <img src="examples/nyc/nyc.png" height="250"> | <img src="examples/tokyo/tokyo.png" height="250"> | <img src="examples/parana/parana.png" height="250"> |
|
|
35
36
|
| <img src="docs/examples/america.png" height="250"> | <img src="docs/examples/argentina_museum.png" height="250"> | <img src="docs/examples/museos_parana.png" height="250"> |
|
|
36
37
|
| <img src="docs/examples/asia.png" height="250"> | <img src="docs/examples/europe.png" height="250"> | <img src="docs/examples/icecream_parana.png" height="250"> |
|
|
37
38
|
| <img src="docs/examples/argentina_cities.png" height="250"> | <img src="docs/examples/tanzania_hydro.png" height="250"> | <img src="docs/examples/parana_polygon.png" height="250"> |
|
|
38
|
-
| <img src="docs/examples/parana_carto_dark.png" height="250"> | <img src="docs/examples/ramirez_avenue.png" height="250"> | |
|
|
39
|
+
| <img src="docs/examples/parana_carto_dark.png" height="250"> | <img src="docs/examples/ramirez_avenue.png" height="250"> | <img src="examples/paris/paris.png" height="250"> |
|
|
39
40
|
|
|
40
41
|
---
|
|
41
42
|
|
data/lib/gd/gis/basemap.rb
CHANGED
|
@@ -59,7 +59,7 @@ module GD
|
|
|
59
59
|
"https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/#{z}/#{y}/#{x}"
|
|
60
60
|
|
|
61
61
|
# ==============================
|
|
62
|
-
# STAMEN
|
|
62
|
+
# STAMEN 503
|
|
63
63
|
# ==============================
|
|
64
64
|
when :stamen_toner
|
|
65
65
|
"https://stamen-tiles.a.ssl.fastly.net/toner/#{z}/#{x}/#{y}.png"
|
|
@@ -80,7 +80,7 @@ module GD
|
|
|
80
80
|
"https://a.tile.opentopomap.org/#{z}/#{x}/#{y}.png"
|
|
81
81
|
|
|
82
82
|
# ==============================
|
|
83
|
-
# Wikimedia
|
|
83
|
+
# Wikimedia 403
|
|
84
84
|
# ==============================
|
|
85
85
|
when :wikimedia
|
|
86
86
|
"https://maps.wikimedia.org/osm-intl/#{z}/#{x}/#{y}.png"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module GD
|
|
2
|
+
module GIS
|
|
3
|
+
class Classifier
|
|
4
|
+
def self.road(feature)
|
|
5
|
+
tags = feature.properties || {}
|
|
6
|
+
|
|
7
|
+
case tags["highway"]
|
|
8
|
+
when "motorway", "trunk"
|
|
9
|
+
:motorway
|
|
10
|
+
when "primary", "primary_link"
|
|
11
|
+
:primary
|
|
12
|
+
when "secondary", "secondary_link"
|
|
13
|
+
:secondary
|
|
14
|
+
when "tertiary"
|
|
15
|
+
:street
|
|
16
|
+
when "residential", "living_street"
|
|
17
|
+
:street
|
|
18
|
+
when "service", "track"
|
|
19
|
+
:minor
|
|
20
|
+
else
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.water?(feature)
|
|
26
|
+
p = feature.properties
|
|
27
|
+
|
|
28
|
+
p["waterway"] ||
|
|
29
|
+
p["natural"] == "water" ||
|
|
30
|
+
p["fclass"] == "river" ||
|
|
31
|
+
p["fclass"] == "stream"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.rail?(feature)
|
|
35
|
+
tags = feature.properties || {}
|
|
36
|
+
tags["railway"]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.park?(feature)
|
|
40
|
+
tags = feature.properties || {}
|
|
41
|
+
%w[park recreation_ground garden].include?(tags["leisure"]) ||
|
|
42
|
+
%w[park grass forest].include?(tags["landuse"])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.water_kind(feature)
|
|
46
|
+
p = feature.properties
|
|
47
|
+
|
|
48
|
+
case p["waterway"] || p["fclass"]
|
|
49
|
+
when "river" then :river
|
|
50
|
+
when "stream" then :stream
|
|
51
|
+
else :minor
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/gd/gis/dump.sh
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
OUT="all_ruby_sources.txt"
|
|
6
|
+
> "$OUT"
|
|
7
|
+
|
|
8
|
+
echo "### Ruby sources dump ($(date))" >> "$OUT"
|
|
9
|
+
echo >> "$OUT"
|
|
10
|
+
|
|
11
|
+
find . -type f -name "*.rb" | sort | while read -r file; do
|
|
12
|
+
echo "===== FILE: $file =====" >> "$OUT"
|
|
13
|
+
echo >> "$OUT"
|
|
14
|
+
cat "$file" >> "$OUT"
|
|
15
|
+
echo >> "$OUT"
|
|
16
|
+
echo >> "$OUT"
|
|
17
|
+
done
|
|
18
|
+
|
|
19
|
+
echo "Wrote $OUT"
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
module GD
|
|
2
|
+
module GIS
|
|
3
|
+
class Feature
|
|
4
|
+
attr_reader :geometry, :properties
|
|
5
|
+
|
|
6
|
+
def initialize(geometry, properties)
|
|
7
|
+
@geometry = geometry
|
|
8
|
+
@properties = properties || {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# -------------------------------------------------
|
|
12
|
+
# Main draw entry point
|
|
13
|
+
# -------------------------------------------------
|
|
14
|
+
def draw(img, projection, color, width, layer = nil)
|
|
15
|
+
case geometry["type"]
|
|
16
|
+
when "Polygon"
|
|
17
|
+
if layer == :water
|
|
18
|
+
draw_polygon_outline(img, projection, geometry["coordinates"], color, width)
|
|
19
|
+
elsif layer.is_a?(Hash)
|
|
20
|
+
draw_polygon_styled(img, projection, geometry["coordinates"], layer)
|
|
21
|
+
else
|
|
22
|
+
draw_polygon(img, projection, geometry["coordinates"], color)
|
|
23
|
+
end
|
|
24
|
+
when "MultiPolygon"
|
|
25
|
+
geometry["coordinates"].each do |poly|
|
|
26
|
+
if layer == :water
|
|
27
|
+
draw_polygon_outline(img, projection, poly, color, width)
|
|
28
|
+
elsif layer.is_a?(Hash)
|
|
29
|
+
draw_polygon_styled(img, projection, poly, layer)
|
|
30
|
+
else
|
|
31
|
+
draw_polygon(img, projection, poly, color)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
when "LineString", "MultiLineString"
|
|
35
|
+
draw_lines(img, projection, geometry["coordinates"], color, width)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# -------------------------------------------------
|
|
40
|
+
# Styled polygon rendering (fill + stroke)
|
|
41
|
+
# -------------------------------------------------
|
|
42
|
+
def draw_polygon_styled(img, projection, rings, style)
|
|
43
|
+
fill = style[:fill] ? GD::Color.rgb(*style[:fill]) : nil
|
|
44
|
+
stroke = style[:stroke] ? GD::Color.rgb(*style[:stroke]) : nil
|
|
45
|
+
|
|
46
|
+
rings.each do |ring|
|
|
47
|
+
pts = ring.map do |lon,lat|
|
|
48
|
+
x,y = projection.call(lon,lat)
|
|
49
|
+
next if x.nil? || y.nil?
|
|
50
|
+
[x.to_i, y.to_i]
|
|
51
|
+
end.compact
|
|
52
|
+
|
|
53
|
+
pts = pts.chunk_while { |a,b| a == b }.map(&:first)
|
|
54
|
+
next if pts.length < 3
|
|
55
|
+
|
|
56
|
+
img.filled_polygon(pts, fill) if fill
|
|
57
|
+
|
|
58
|
+
if stroke
|
|
59
|
+
pts.each_cons(2) { |a,b| img.line(a[0],a[1], b[0],b[1], stroke) }
|
|
60
|
+
img.line(pts.last[0], pts.last[1], pts.first[0], pts.first[1], stroke)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# -------------------------------------------------
|
|
66
|
+
# Polygon outline (used for water)
|
|
67
|
+
# -------------------------------------------------
|
|
68
|
+
def draw_polygon_outline(img, projection, rings, color, width)
|
|
69
|
+
return if color.nil?
|
|
70
|
+
|
|
71
|
+
rings.each do |ring|
|
|
72
|
+
pts = ring.map do |lon, lat|
|
|
73
|
+
x, y = projection.call(lon, lat)
|
|
74
|
+
[x.to_i, y.to_i] if x && y
|
|
75
|
+
end.compact
|
|
76
|
+
|
|
77
|
+
next if pts.size < 2
|
|
78
|
+
|
|
79
|
+
img.lines(pts, color, width)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# -------------------------------------------------
|
|
84
|
+
# Legacy filled polygon (single color)
|
|
85
|
+
# -------------------------------------------------
|
|
86
|
+
def draw_polygon(img, projection, rings, color)
|
|
87
|
+
return if color.nil?
|
|
88
|
+
|
|
89
|
+
rings.each do |ring|
|
|
90
|
+
pts = ring.map do |lon,lat|
|
|
91
|
+
x,y = projection.call(lon,lat)
|
|
92
|
+
next if x.nil? || y.nil?
|
|
93
|
+
[x.to_i, y.to_i]
|
|
94
|
+
end.compact
|
|
95
|
+
|
|
96
|
+
pts = pts.chunk_while { |a,b| a == b }.map(&:first)
|
|
97
|
+
next if pts.length < 3
|
|
98
|
+
|
|
99
|
+
img.filled_polygon(pts, color)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# -------------------------------------------------
|
|
104
|
+
# Lines
|
|
105
|
+
# -------------------------------------------------
|
|
106
|
+
def draw_lines(img, projection, coords, color, width)
|
|
107
|
+
return if color.nil?
|
|
108
|
+
|
|
109
|
+
if coords.first.is_a?(Array) && coords.first.first.is_a?(Array)
|
|
110
|
+
coords.each { |line| draw_line(img, projection, line, color, width) }
|
|
111
|
+
else
|
|
112
|
+
draw_line(img, projection, coords, color, width)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def draw_line(img, projection, coords, color, width)
|
|
117
|
+
return if color.nil?
|
|
118
|
+
|
|
119
|
+
coords.each_cons(2) do |(lon1,lat1),(lon2,lat2)|
|
|
120
|
+
x1,y1 = projection.call(lon1,lat1)
|
|
121
|
+
x2,y2 = projection.call(lon2,lat2)
|
|
122
|
+
img.line(x1, y1, x2, y2, color, thickness: width)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# -------------------------------------------------
|
|
127
|
+
# Metadata helpers
|
|
128
|
+
# -------------------------------------------------
|
|
129
|
+
def label
|
|
130
|
+
properties["name:ja"] || properties["name"]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def centroid
|
|
134
|
+
pts = []
|
|
135
|
+
|
|
136
|
+
case geometry["type"]
|
|
137
|
+
when "LineString"
|
|
138
|
+
pts = geometry["coordinates"]
|
|
139
|
+
when "MultiLineString"
|
|
140
|
+
pts = geometry["coordinates"].flatten(1)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
return nil if pts.empty?
|
|
144
|
+
|
|
145
|
+
lon = pts.map(&:first).sum / pts.size
|
|
146
|
+
lat = pts.map(&:last).sum / pts.size
|
|
147
|
+
|
|
148
|
+
[lon, lat]
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
data/lib/gd/gis/layer_geojson.rb
CHANGED
|
@@ -1,76 +1,16 @@
|
|
|
1
1
|
require "json"
|
|
2
|
+
require_relative "feature"
|
|
2
3
|
|
|
3
4
|
module GD
|
|
4
5
|
module GIS
|
|
5
6
|
class LayerGeoJSON
|
|
6
|
-
def
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@font = options[:font]
|
|
12
|
-
@size = options[:size] || 10
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def render!(img, projection)
|
|
16
|
-
features = @geojson["features"] || []
|
|
17
|
-
|
|
18
|
-
features.each do |f|
|
|
19
|
-
geom = f["geometry"]
|
|
20
|
-
next unless geom
|
|
21
|
-
|
|
22
|
-
type = geom["type"]
|
|
23
|
-
coords = geom["coordinates"]
|
|
24
|
-
|
|
25
|
-
case type
|
|
26
|
-
when "Point"
|
|
27
|
-
draw_points(img, projection, [coords], f["properties"])
|
|
28
|
-
when "MultiPoint"
|
|
29
|
-
draw_points(img, projection, coords, f["properties"])
|
|
30
|
-
when "LineString"
|
|
31
|
-
draw_lines(img, projection, [coords])
|
|
32
|
-
when "MultiLineString"
|
|
33
|
-
coords.each { |l| draw_lines(img, projection, [l]) }
|
|
34
|
-
when "Polygon"
|
|
35
|
-
draw_polygons(img, projection, coords)
|
|
36
|
-
when "MultiPolygon"
|
|
37
|
-
coords.each { |p| draw_polygons(img, projection, p) }
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
def draw_lines(img, projection, lines)
|
|
45
|
-
color = @color || [120,120,120]
|
|
46
|
-
|
|
47
|
-
lines.each do |line|
|
|
48
|
-
pts = line.map { |p| projection.call(p[0], p[1]) }
|
|
49
|
-
|
|
50
|
-
pts.each_cons(2) do |(x1, y1), (x2, y2)|
|
|
51
|
-
img.line(x1, y1, x2, y2, color)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def draw_polygons(img, projection, rings)
|
|
57
|
-
fill = @color || [200,200,200]
|
|
58
|
-
stroke = @color || [120,120,120]
|
|
59
|
-
|
|
60
|
-
rings.each do |ring|
|
|
61
|
-
pts = ring.map { |p| projection.call(p[0], p[1]) }
|
|
62
|
-
|
|
63
|
-
# fill
|
|
64
|
-
img.polygon(pts, fill)
|
|
65
|
-
|
|
66
|
-
# stroke
|
|
67
|
-
pts.each_cons(2) do |(x1, y1), (x2, y2)|
|
|
68
|
-
img.line(x1, y1, x2, y2, stroke)
|
|
7
|
+
def self.load(path)
|
|
8
|
+
data = JSON.parse(File.read(path))
|
|
9
|
+
features = data["features"]
|
|
10
|
+
features.map do |f|
|
|
11
|
+
Feature.new(f["geometry"], f["properties"])
|
|
69
12
|
end
|
|
70
13
|
end
|
|
71
14
|
end
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
end
|
|
75
15
|
end
|
|
76
16
|
end
|
data/lib/gd/gis/map.rb
CHANGED
|
@@ -1,58 +1,91 @@
|
|
|
1
|
-
require "gd"
|
|
2
1
|
require_relative "basemap"
|
|
3
2
|
require_relative "projection"
|
|
4
|
-
require_relative "
|
|
3
|
+
require_relative "classifier"
|
|
4
|
+
require_relative "layer_geojson"
|
|
5
5
|
require_relative "layer_points"
|
|
6
6
|
require_relative "layer_lines"
|
|
7
7
|
require_relative "layer_polygons"
|
|
8
|
-
require_relative "layer_geojson"
|
|
9
8
|
|
|
10
9
|
module GD
|
|
11
10
|
module GIS
|
|
12
11
|
class Map
|
|
13
12
|
TILE_SIZE = 256
|
|
14
13
|
|
|
14
|
+
attr_reader :image
|
|
15
|
+
attr_accessor :style
|
|
16
|
+
|
|
15
17
|
def initialize(bbox:, zoom:, basemap:)
|
|
16
|
-
@bbox
|
|
17
|
-
@zoom
|
|
18
|
-
@basemap
|
|
19
|
-
|
|
18
|
+
@bbox = bbox
|
|
19
|
+
@zoom = zoom
|
|
20
|
+
@basemap = Basemap.new(zoom, bbox, basemap)
|
|
21
|
+
|
|
22
|
+
# 🔒 DO NOT CHANGE — this is the working GeoJSON pipeline
|
|
23
|
+
@layers = {
|
|
24
|
+
motorway: [],
|
|
25
|
+
primary: [],
|
|
26
|
+
secondary: [],
|
|
27
|
+
street: [],
|
|
28
|
+
minor: [],
|
|
29
|
+
rail: [],
|
|
30
|
+
water: [],
|
|
31
|
+
park: []
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# 🆕 overlay layers
|
|
35
|
+
@points_layers = []
|
|
36
|
+
@lines_layers = []
|
|
37
|
+
@polygons_layers = []
|
|
38
|
+
|
|
39
|
+
@style = nil
|
|
20
40
|
end
|
|
21
41
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
def
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
# -----------------------------------
|
|
43
|
+
# GeoJSON input (unchanged)
|
|
44
|
+
# -----------------------------------
|
|
45
|
+
|
|
46
|
+
def add_geojson(path)
|
|
47
|
+
features = LayerGeoJSON.load(path)
|
|
48
|
+
|
|
49
|
+
features.each do |feature|
|
|
50
|
+
if Classifier.water?(feature)
|
|
51
|
+
kind = Classifier.water_kind(feature)
|
|
52
|
+
@layers[:water] << [kind, feature]
|
|
53
|
+
|
|
54
|
+
elsif Classifier.park?(feature)
|
|
55
|
+
@layers[:park] << feature
|
|
56
|
+
|
|
57
|
+
elsif Classifier.rail?(feature)
|
|
58
|
+
@layers[:rail] << feature
|
|
59
|
+
|
|
60
|
+
elsif type = Classifier.road(feature)
|
|
61
|
+
@layers[type] << feature
|
|
62
|
+
end
|
|
63
|
+
end
|
|
37
64
|
end
|
|
38
65
|
|
|
39
|
-
|
|
40
|
-
|
|
66
|
+
# -----------------------------------
|
|
67
|
+
# Overlay layers
|
|
68
|
+
# -----------------------------------
|
|
69
|
+
|
|
70
|
+
def add_points(data, **opts)
|
|
71
|
+
@points_layers << GD::GIS::PointsLayer.new(data, **opts)
|
|
41
72
|
end
|
|
42
73
|
|
|
43
|
-
def
|
|
44
|
-
@
|
|
74
|
+
def add_lines(features, **opts)
|
|
75
|
+
@lines_layers << GD::GIS::LinesLayer.new(features, **opts)
|
|
45
76
|
end
|
|
46
77
|
|
|
47
|
-
def
|
|
48
|
-
@
|
|
78
|
+
def add_polygons(polygons, **opts)
|
|
79
|
+
@polygons_layers << GD::GIS::PolygonsLayer.new(polygons, **opts)
|
|
49
80
|
end
|
|
50
81
|
|
|
51
|
-
#
|
|
82
|
+
# -----------------------------------
|
|
52
83
|
# Rendering
|
|
53
|
-
#
|
|
84
|
+
# -----------------------------------
|
|
85
|
+
|
|
86
|
+
def render
|
|
87
|
+
raise "map.style must be set" unless @style
|
|
54
88
|
|
|
55
|
-
def render(path = "map.png")
|
|
56
89
|
tiles, x_min, y_min = @basemap.fetch_tiles
|
|
57
90
|
|
|
58
91
|
xs = tiles.map { |t| t[0] }
|
|
@@ -67,42 +100,92 @@ module GD
|
|
|
67
100
|
origin_x = x_min * TILE_SIZE
|
|
68
101
|
origin_y = y_min * TILE_SIZE
|
|
69
102
|
|
|
70
|
-
@
|
|
71
|
-
@
|
|
72
|
-
@img.save_alpha = true
|
|
103
|
+
@image = GD::Image.new(width, height)
|
|
104
|
+
@image.antialias(false)
|
|
73
105
|
|
|
74
|
-
#
|
|
106
|
+
# Basemap
|
|
75
107
|
tiles.each do |x, y, file|
|
|
76
108
|
tile = GD::Image.open(file)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
109
|
+
@image.copy(
|
|
110
|
+
tile,
|
|
111
|
+
(x - x_min) * TILE_SIZE,
|
|
112
|
+
(y - y_min) * TILE_SIZE,
|
|
113
|
+
0, 0, TILE_SIZE, TILE_SIZE
|
|
114
|
+
)
|
|
80
115
|
end
|
|
81
116
|
|
|
82
|
-
# WebMercator projection → local pixels
|
|
83
|
-
scale = TILE_SIZE * (2 ** @zoom)
|
|
84
|
-
|
|
85
117
|
projection = lambda do |lon, lat|
|
|
86
|
-
x = (lon
|
|
87
|
-
|
|
88
|
-
lat_rad = lat * Math::PI / 180.0
|
|
89
|
-
y = (1.0 - Math.log(Math.tan(lat_rad) + 1.0 / Math.cos(lat_rad)) / Math::PI) / 2.0 * scale
|
|
90
|
-
|
|
91
|
-
[
|
|
92
|
-
(x - origin_x).round,
|
|
93
|
-
(y - origin_y).round
|
|
94
|
-
]
|
|
118
|
+
x, y = GD::GIS::Projection.lonlat_to_global_px(lon, lat, @zoom)
|
|
119
|
+
[(x - origin_x).round, (y - origin_y).round]
|
|
95
120
|
end
|
|
96
121
|
|
|
97
|
-
#
|
|
98
|
-
@
|
|
99
|
-
|
|
122
|
+
# 1️⃣ Semantic GeoJSON layers (this is what was working)
|
|
123
|
+
@style.order.each do |kind|
|
|
124
|
+
draw_layer(kind, projection)
|
|
100
125
|
end
|
|
126
|
+
|
|
127
|
+
# 2️⃣ Generic overlays
|
|
128
|
+
@polygons_layers.each { |l| l.render!(@image, projection) }
|
|
129
|
+
@lines_layers.each { |l| l.render!(@image, projection) }
|
|
130
|
+
@points_layers.each { |l| l.render!(@image, projection) }
|
|
101
131
|
end
|
|
102
132
|
|
|
103
133
|
def save(path)
|
|
104
|
-
@
|
|
134
|
+
@image.save(path)
|
|
105
135
|
end
|
|
136
|
+
|
|
137
|
+
def draw_layer(kind, projection)
|
|
138
|
+
items = @layers[kind]
|
|
139
|
+
return if items.nil? || items.empty?
|
|
140
|
+
|
|
141
|
+
style =
|
|
142
|
+
case kind
|
|
143
|
+
when :street, :primary, :motorway, :secondary, :minor
|
|
144
|
+
@style.roads[kind]
|
|
145
|
+
when :rail then @style.rails
|
|
146
|
+
when :water then @style.water
|
|
147
|
+
when :park then @style.parks
|
|
148
|
+
else
|
|
149
|
+
@style.extra[kind] if @style.respond_to?(:extra)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
return if style.nil?
|
|
153
|
+
|
|
154
|
+
items.each do |item|
|
|
155
|
+
if kind == :water
|
|
156
|
+
water_kind, f = item
|
|
157
|
+
|
|
158
|
+
width =
|
|
159
|
+
case water_kind
|
|
160
|
+
when :river then 2.5
|
|
161
|
+
when :stream then 1.5
|
|
162
|
+
else 1
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
if style[:stroke]
|
|
166
|
+
color = GD::Color.rgb(*style[:stroke])
|
|
167
|
+
f.draw(@image, projection, color, width, :water)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
else
|
|
171
|
+
f = item
|
|
172
|
+
geom = f.geometry["type"]
|
|
173
|
+
|
|
174
|
+
if geom == "Polygon" || geom == "MultiPolygon"
|
|
175
|
+
# THIS is the critical fix
|
|
176
|
+
f.draw(@image, projection, nil, nil, style)
|
|
177
|
+
else
|
|
178
|
+
if style[:stroke]
|
|
179
|
+
color = GD::Color.rgb(*style[:stroke])
|
|
180
|
+
width = style[:stroke_width] || 1
|
|
181
|
+
f.draw(@image, projection, color, width)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
|
|
106
189
|
end
|
|
107
190
|
end
|
|
108
191
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module GD
|
|
2
|
+
module GIS
|
|
3
|
+
class Style
|
|
4
|
+
DARK = Style.new(
|
|
5
|
+
roads: {
|
|
6
|
+
motorway: {
|
|
7
|
+
stroke: [255,255,255],
|
|
8
|
+
stroke_width: 10,
|
|
9
|
+
fill: [60,60,60],
|
|
10
|
+
fill_width: 6
|
|
11
|
+
},
|
|
12
|
+
primary: {
|
|
13
|
+
stroke: [200,200,200],
|
|
14
|
+
stroke_width: 7,
|
|
15
|
+
fill: [80,80,80],
|
|
16
|
+
fill_width: 4
|
|
17
|
+
},
|
|
18
|
+
street: {
|
|
19
|
+
stroke: [120,120,120],
|
|
20
|
+
stroke_width: 1
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
rail: {
|
|
24
|
+
stroke: [255,255,255],
|
|
25
|
+
stroke_width: 6,
|
|
26
|
+
fill: [220,50,50],
|
|
27
|
+
fill_width: 4,
|
|
28
|
+
center: [255,255,255],
|
|
29
|
+
center_width: 1
|
|
30
|
+
},
|
|
31
|
+
water: {
|
|
32
|
+
fill: [40,80,120],
|
|
33
|
+
stroke: [100,160,220]
|
|
34
|
+
},
|
|
35
|
+
park: {
|
|
36
|
+
fill: [40,80,40]
|
|
37
|
+
},
|
|
38
|
+
order: [
|
|
39
|
+
:water,
|
|
40
|
+
:park,
|
|
41
|
+
:street,
|
|
42
|
+
:primary,
|
|
43
|
+
:motorway,
|
|
44
|
+
:rail
|
|
45
|
+
]
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module GD
|
|
2
|
+
module GIS
|
|
3
|
+
class Style
|
|
4
|
+
LIGHT = Style.new(
|
|
5
|
+
roads: {
|
|
6
|
+
motorway: {
|
|
7
|
+
stroke: [100,100,100],
|
|
8
|
+
stroke_width: 10,
|
|
9
|
+
fill: [245,245,245],
|
|
10
|
+
fill_width: 6
|
|
11
|
+
},
|
|
12
|
+
primary: {
|
|
13
|
+
stroke: [140,140,140],
|
|
14
|
+
stroke_width: 7,
|
|
15
|
+
fill: [240,240,240],
|
|
16
|
+
fill_width: 4
|
|
17
|
+
},
|
|
18
|
+
street: {
|
|
19
|
+
stroke: [220,220,220],
|
|
20
|
+
stroke_width: 1
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
rail: {
|
|
24
|
+
stroke: [80,80,80],
|
|
25
|
+
stroke_width: 6,
|
|
26
|
+
fill: [230,70,70],
|
|
27
|
+
fill_width: 4,
|
|
28
|
+
center: [255,255,255],
|
|
29
|
+
center_width: 1
|
|
30
|
+
},
|
|
31
|
+
water: {
|
|
32
|
+
fill: [168,208,255],
|
|
33
|
+
stroke: [120,180,240]
|
|
34
|
+
},
|
|
35
|
+
park: {
|
|
36
|
+
fill: [205,238,203]
|
|
37
|
+
},
|
|
38
|
+
order: [
|
|
39
|
+
:water,
|
|
40
|
+
:park,
|
|
41
|
+
:street,
|
|
42
|
+
:primary,
|
|
43
|
+
:motorway,
|
|
44
|
+
:rail
|
|
45
|
+
]
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module GD
|
|
2
|
+
module GIS
|
|
3
|
+
class Style
|
|
4
|
+
SOLARIZED = Style.new(
|
|
5
|
+
roads: {
|
|
6
|
+
motorway: {
|
|
7
|
+
stroke: [7, 54, 66], # Base02
|
|
8
|
+
stroke_width: 10,
|
|
9
|
+
fill: [131, 148, 150], # Base0
|
|
10
|
+
fill_width: 6
|
|
11
|
+
},
|
|
12
|
+
primary: {
|
|
13
|
+
stroke: [88, 110, 117], # Base01
|
|
14
|
+
stroke_width: 7,
|
|
15
|
+
fill: [147, 161, 161], # Base1
|
|
16
|
+
fill_width: 4
|
|
17
|
+
},
|
|
18
|
+
street: {
|
|
19
|
+
stroke: [147, 161, 161], # Base1
|
|
20
|
+
stroke_width: 1
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
rails: {
|
|
24
|
+
stroke: [203, 75, 22], # Orange
|
|
25
|
+
stroke_width: 6,
|
|
26
|
+
fill: [220, 50, 47], # Red
|
|
27
|
+
fill_width: 4,
|
|
28
|
+
center: [253, 246, 227], # Base3
|
|
29
|
+
center_width: 1
|
|
30
|
+
},
|
|
31
|
+
water: {
|
|
32
|
+
fill: [38, 139, 210], # Blue
|
|
33
|
+
stroke: [42, 161, 152] # Cyan
|
|
34
|
+
},
|
|
35
|
+
parks: {
|
|
36
|
+
fill: [133, 153, 0] # Green
|
|
37
|
+
},
|
|
38
|
+
order: [
|
|
39
|
+
:water,
|
|
40
|
+
:park,
|
|
41
|
+
:street,
|
|
42
|
+
:primary,
|
|
43
|
+
:motorway,
|
|
44
|
+
:rail
|
|
45
|
+
]
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
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
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
-
# lib/gd/gis.rb
|
|
2
1
|
require "gd"
|
|
2
|
+
|
|
3
|
+
require_relative "gis/style"
|
|
4
|
+
require_relative "gis/classifier"
|
|
5
|
+
|
|
6
|
+
require_relative "gis/feature"
|
|
3
7
|
require_relative "gis/map"
|
|
8
|
+
require_relative "gis/basemap"
|
|
9
|
+
require_relative "gis/projection"
|
|
10
|
+
require_relative "gis/geometry"
|
|
11
|
+
require_relative "gis/layer_points"
|
|
12
|
+
require_relative "gis/layer_lines"
|
|
13
|
+
require_relative "gis/layer_polygons"
|
|
14
|
+
require_relative "gis/layer_geojson"
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: libgd-gis
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Germán Alberto Giménez Silva
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: ruby-libgd
|
|
@@ -16,20 +15,20 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - "~>"
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 0.1
|
|
18
|
+
version: 0.2.1
|
|
20
19
|
- - ">="
|
|
21
20
|
- !ruby/object:Gem::Version
|
|
22
|
-
version: 0.1
|
|
21
|
+
version: 0.2.1
|
|
23
22
|
type: :runtime
|
|
24
23
|
prerelease: false
|
|
25
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
26
25
|
requirements:
|
|
27
26
|
- - "~>"
|
|
28
27
|
- !ruby/object:Gem::Version
|
|
29
|
-
version: 0.1
|
|
28
|
+
version: 0.2.1
|
|
30
29
|
- - ">="
|
|
31
30
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.1
|
|
31
|
+
version: 0.2.1
|
|
33
32
|
description: A native GIS raster engine for Ruby built on libgd. Render maps, GeoJSON,
|
|
34
33
|
heatmaps and tiles.
|
|
35
34
|
email:
|
|
@@ -41,6 +40,9 @@ files:
|
|
|
41
40
|
- README.md
|
|
42
41
|
- lib/gd/gis.rb
|
|
43
42
|
- lib/gd/gis/basemap.rb
|
|
43
|
+
- lib/gd/gis/classifier.rb
|
|
44
|
+
- lib/gd/gis/dump.sh
|
|
45
|
+
- lib/gd/gis/feature.rb
|
|
44
46
|
- lib/gd/gis/geometry.rb
|
|
45
47
|
- lib/gd/gis/layer_geojson.rb
|
|
46
48
|
- lib/gd/gis/layer_lines.rb
|
|
@@ -48,12 +50,15 @@ files:
|
|
|
48
50
|
- lib/gd/gis/layer_polygons.rb
|
|
49
51
|
- lib/gd/gis/map.rb
|
|
50
52
|
- lib/gd/gis/projection.rb
|
|
53
|
+
- lib/gd/gis/style.rb
|
|
54
|
+
- lib/gd/gis/style/dark.rb
|
|
55
|
+
- lib/gd/gis/style/light.rb
|
|
56
|
+
- lib/gd/gis/style/solarized.rb
|
|
51
57
|
- lib/libgd_gis.rb
|
|
52
58
|
homepage: https://github.com/ggerman/libgd-gis
|
|
53
59
|
licenses:
|
|
54
60
|
- MIT
|
|
55
61
|
metadata: {}
|
|
56
|
-
post_install_message:
|
|
57
62
|
rdoc_options: []
|
|
58
63
|
require_paths:
|
|
59
64
|
- lib
|
|
@@ -68,8 +73,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
68
73
|
- !ruby/object:Gem::Version
|
|
69
74
|
version: '0'
|
|
70
75
|
requirements: []
|
|
71
|
-
rubygems_version:
|
|
72
|
-
signing_key:
|
|
76
|
+
rubygems_version: 4.0.3
|
|
73
77
|
specification_version: 4
|
|
74
78
|
summary: Geospatial raster rendering for Ruby using libgd
|
|
75
79
|
test_files: []
|