libgd-gis 0.2.9 → 0.3.0
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 +132 -98
- data/lib/gd/gis/basemap.rb +69 -16
- data/lib/gd/gis/classifier.rb +48 -9
- data/lib/gd/gis/color_helpers.rb +39 -17
- data/lib/gd/gis/crs_normalizer.rb +53 -7
- data/lib/gd/gis/feature.rb +119 -36
- data/lib/gd/gis/font_helper.rb +33 -0
- data/lib/gd/gis/geometry.rb +116 -42
- data/lib/gd/gis/layer_geojson.rb +28 -4
- data/lib/gd/gis/layer_lines.rb +27 -0
- data/lib/gd/gis/layer_points.rb +69 -21
- data/lib/gd/gis/layer_polygons.rb +43 -8
- data/lib/gd/gis/map.rb +160 -66
- data/lib/gd/gis/middleware.rb +81 -18
- data/lib/gd/gis/ontology.rb +45 -2
- data/lib/gd/gis/ontology.yml +8 -0
- data/lib/gd/gis/projection.rb +55 -5
- data/lib/gd/gis/style.rb +66 -3
- data/lib/gd/gis.rb +28 -0
- data/lib/libgd_gis.rb +60 -30
- metadata +31 -6
- data/lib/gd/gis/input/detector.rb +0 -34
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 72c8b1d43f60ad8615302e22054c3d5e00be6bd746c757f00d980e781f760b0c
|
|
4
|
+
data.tar.gz: 74d09fb46fe02d7d53c75f4c2a9acb6bbab24383b51ef9bd9ec3ad8c89e0253f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e09c62bf8ee7218019a2e69676cc9b7461982568842bb8afac861f38f290d7fec658ee15269915ec5a3ad925de4d5ef4e6dcdce0415d7b97d438d91795d022a5
|
|
7
|
+
data.tar.gz: e7a1a6f47da4bbf9db7e18de9c3184e4af3498f636f51ba8b8ee9aa25b1c57c9532be24e71ee76e2201f5f7a240cddc62e33f16d886a1673062d845163831664
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# LibGD
|
|
1
|
+
# LibGD GIS
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<a href="https://rubystacknews.com/2026/01/07/ruby-can-now-draw-maps-and-i-started-with-ice-cream/">
|
|
@@ -33,156 +33,190 @@
|
|
|
33
33
|
[](https://coveralls.io/github/libgd-gis/libgd-gis?branch=master)
|
|
34
34
|
[](https://rubygems.org/gems/libgd-gis)
|
|
35
35
|
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
**libgd-gis** is a lightweight Ruby GIS rendering library built on top of **GD**.
|
|
39
|
+
It renders geographic data (GeoJSON, points, lines, polygons) into raster images using Web Mercator tiles and a simple, explicit rendering pipeline.
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
| :----: | :----: | :--: |
|
|
39
|
-
| <img src="docs/examples/parana.png" height="250"> | <img src="docs/examples/nyc.png" height="250"> | <img src="docs/examples/paris.png" height="250"> |
|
|
40
|
-
| <img src="examples/nyc/nyc.png" height="250"> | <img src="docs/examples/tokyo_solarized.png" height="250"> | <img src="examples/parana/parana.png" height="250"> |
|
|
41
|
-
| <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"> |
|
|
42
|
-
| <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"> |
|
|
43
|
-
| <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"> |
|
|
44
|
-
| <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"> |
|
|
41
|
+
This library is designed for **map visualization**, not for spatial analysis.
|
|
45
42
|
|
|
46
43
|
---
|
|
47
44
|
|
|
48
|
-
|
|
49
|
-
> Please report issues or ask for help — feedback is very welcome.
|
|
50
|
-
> https://github.com/ggerman/libgd-gis/issues or ggerman@gmail.com
|
|
45
|
+
## Features
|
|
51
46
|
|
|
52
|
-
|
|
47
|
+
- Web Mercator tile rendering (OSM, CARTO, ESRI, Stamen, etc.)
|
|
48
|
+
- CRS normalization (CRS84, EPSG:4326, EPSG:3857, Gauss–Krüger Argentina)
|
|
49
|
+
- Layered rendering pipeline
|
|
50
|
+
- YAML-based styling
|
|
51
|
+
- Rule-based semantic classification (ontology)
|
|
52
|
+
- Points, lines, and polygons support
|
|
53
|
+
- No heavy GIS dependencies
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
---
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
## Non-Goals
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
the ability to generate **maps, tiles, and GIS-grade visualizations natively**, without relying on external tools like QGIS, Mapnik, ImageMagick, or Mapbox.
|
|
59
|
+
libgd-gis intentionally does **not** aim to be:
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
- a spatial analysis engine
|
|
62
|
+
- a replacement for PostGIS / GEOS
|
|
63
|
+
- a full map server
|
|
64
|
+
- a vector tile generator
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
- Just Ruby, raster, and GIS.
|
|
66
|
+
If you need projections beyond Web Mercator or topological correctness,
|
|
67
|
+
use a full GIS stack.
|
|
66
68
|
|
|
67
69
|
---
|
|
68
70
|
|
|
69
|
-
##
|
|
71
|
+
## Installation
|
|
70
72
|
|
|
71
|
-
|
|
73
|
+
Add to your Gemfile:
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
```ruby
|
|
76
|
+
gem "libgd-gis"
|
|
77
|
+
```
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
- Fetch real basemap tiles
|
|
77
|
-
- Reproject WGS84 (lat/lon) into Web Mercator
|
|
78
|
-
- Render points, icons, and layers onto a raster map
|
|
79
|
-
- Generate PNG maps or map tiles
|
|
79
|
+
Then run:
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
```bash
|
|
82
|
+
bundle install
|
|
83
|
+
```
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
You must also have **GD** available on your system.
|
|
84
86
|
|
|
85
|
-
|
|
87
|
+
---
|
|
86
88
|
|
|
87
|
-
|
|
89
|
+
## Basic Usage
|
|
88
90
|
|
|
89
|
-
|
|
91
|
+
### Create a map
|
|
90
92
|
|
|
91
|
-
|
|
93
|
+
```ruby
|
|
94
|
+
require "gd/gis"
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
map = GD::GIS::Map.new(
|
|
97
|
+
bbox: [-58.45, -34.7, -58.35, -34.55],
|
|
98
|
+
zoom: 13,
|
|
99
|
+
basemap: :carto_light,
|
|
100
|
+
width: 1024,
|
|
101
|
+
height: 768
|
|
102
|
+
)
|
|
94
103
|
```
|
|
95
|
-
|
|
104
|
+
|
|
105
|
+
### Load a style
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
map.style = GD::GIS::Style.load("default", from: "styles")
|
|
96
109
|
```
|
|
97
110
|
|
|
98
|
-
|
|
111
|
+
### Load GeoJSON
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
map.add_geojson("data/roads.geojson")
|
|
115
|
+
map.add_geojson("data/water.geojson")
|
|
99
116
|
```
|
|
100
|
-
|
|
117
|
+
|
|
118
|
+
### Render
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
map.render
|
|
122
|
+
map.save("map.png")
|
|
101
123
|
```
|
|
102
124
|
|
|
103
125
|
---
|
|
104
126
|
|
|
105
|
-
|
|
127
|
+
## Styles Are Mandatory
|
|
106
128
|
|
|
107
|
-
|
|
108
|
-
gem install ruby-libgd
|
|
109
|
-
gem install libgd-gis
|
|
110
|
-
```
|
|
129
|
+
libgd-gis requires an explicit **style definition** in order to render a map.
|
|
111
130
|
|
|
112
|
-
|
|
131
|
+
A `GD::GIS::Map` instance **will not render without a style**, and calling
|
|
132
|
+
`map.render` before assigning one will raise an error.
|
|
133
|
+
|
|
134
|
+
This is intentional.
|
|
113
135
|
|
|
114
|
-
|
|
136
|
+
Styles define how semantic layers (roads, water, parks, points, etc.) are mapped
|
|
137
|
+
to visual properties such as colors, stroke widths, fills, and drawing order.
|
|
138
|
+
No implicit or default styling is applied.
|
|
115
139
|
|
|
116
|
-
|
|
140
|
+
### Example:
|
|
117
141
|
|
|
118
142
|
```ruby
|
|
119
|
-
require "json"
|
|
120
143
|
require "gd/gis"
|
|
121
144
|
|
|
122
|
-
# ---------------------------
|
|
123
|
-
# Bounding box mundial
|
|
124
|
-
# ---------------------------
|
|
125
|
-
AMERICA = [-170, -60, -30, 75]
|
|
126
|
-
|
|
127
|
-
# ---------------------------
|
|
128
|
-
# Crear mapa
|
|
129
|
-
# ---------------------------
|
|
130
145
|
map = GD::GIS::Map.new(
|
|
131
|
-
bbox:
|
|
132
|
-
zoom:
|
|
146
|
+
bbox: PARIS,
|
|
147
|
+
zoom: 13,
|
|
133
148
|
basemap: :carto_light
|
|
134
149
|
)
|
|
135
150
|
|
|
136
|
-
|
|
137
|
-
# ---------------------------
|
|
138
|
-
peaks = JSON.parse(File.read("picks.json"))
|
|
139
|
-
|
|
140
|
-
# ---------------------------
|
|
141
|
-
# Agregar capa de puntos
|
|
142
|
-
# ---------------------------
|
|
143
|
-
map.add_points(
|
|
144
|
-
peaks,
|
|
145
|
-
lon: ->(p) { p["longitude"] },
|
|
146
|
-
lat: ->(p) { p["latitude"] },
|
|
147
|
-
icon: "peak.png",
|
|
148
|
-
label: ->(p) { p["name"] },
|
|
149
|
-
font: "./fonts/DejaVuSans.ttf",
|
|
150
|
-
size: 10,
|
|
151
|
-
color: [0,0,0]
|
|
152
|
-
)
|
|
151
|
+
map.style = GD::GIS::Style.load("default")
|
|
153
152
|
|
|
154
|
-
# ---------------------------
|
|
155
|
-
# Renderizar y guardar
|
|
156
|
-
# ---------------------------
|
|
157
153
|
map.render
|
|
158
|
-
map.save("output/america.png")
|
|
159
|
-
|
|
160
|
-
puts "Saved output/america.png"
|
|
161
|
-
|
|
162
154
|
```
|
|
155
|
+
### Example:
|
|
156
|
+
|
|
157
|
+
```yml
|
|
158
|
+
# styles/default.yml
|
|
159
|
+
|
|
160
|
+
roads:
|
|
161
|
+
motorway:
|
|
162
|
+
stroke: [255, 255, 255]
|
|
163
|
+
stroke_width: 10
|
|
164
|
+
fill: [60, 60, 60]
|
|
165
|
+
fill_width: 6
|
|
166
|
+
|
|
167
|
+
primary:
|
|
168
|
+
stroke: [200, 200, 200]
|
|
169
|
+
stroke_width: 7
|
|
170
|
+
fill: [80, 80, 80]
|
|
171
|
+
fill_width: 4
|
|
172
|
+
|
|
173
|
+
street:
|
|
174
|
+
stroke: [120, 120, 120]
|
|
175
|
+
stroke_width: 1
|
|
176
|
+
|
|
177
|
+
rail:
|
|
178
|
+
stroke: [255, 255, 255]
|
|
179
|
+
stroke_width: 6
|
|
180
|
+
fill: [220, 50, 50]
|
|
181
|
+
fill_width: 4
|
|
182
|
+
center: [255, 255, 255]
|
|
183
|
+
center_width: 1
|
|
184
|
+
|
|
185
|
+
water:
|
|
186
|
+
fill: [120, 180, 255]
|
|
187
|
+
fill_width: 4
|
|
188
|
+
stroke: [80, 140, 220]
|
|
189
|
+
|
|
190
|
+
park:
|
|
191
|
+
fill: [40, 80, 40]
|
|
192
|
+
|
|
193
|
+
order:
|
|
194
|
+
- water
|
|
195
|
+
- park
|
|
196
|
+
- street
|
|
197
|
+
- primary
|
|
198
|
+
- motorway
|
|
199
|
+
- rail
|
|
200
|
+
```
|
|
201
|
+
This design ensures predictable rendering and makes all visual decisions explicit
|
|
202
|
+
and reproducible.
|
|
163
203
|
|
|
164
|
-
---
|
|
165
204
|
|
|
166
|
-
|
|
205
|
+
---
|
|
167
206
|
|
|
168
|
-
|
|
169
|
-
- WGS84 → Web Mercator projection
|
|
170
|
-
- GeoJSON point rendering
|
|
171
|
-
- CSV / JSON support
|
|
172
|
-
- Icon-based symbol layers
|
|
173
|
-
- Automatic bounding box fitting
|
|
174
|
-
- Raster output (PNG)
|
|
207
|
+
## CRS Support
|
|
175
208
|
|
|
176
|
-
|
|
209
|
+
Supported input CRS:
|
|
177
210
|
|
|
178
|
-
|
|
211
|
+
- CRS84
|
|
212
|
+
- EPSG:4326
|
|
213
|
+
- EPSG:3857
|
|
214
|
+
- EPSG:22195 (Gauss–Krüger Argentina, zone 5)
|
|
179
215
|
|
|
180
|
-
|
|
216
|
+
All coordinates are normalized internally to **CRS84 (lon, lat)**.
|
|
181
217
|
|
|
182
218
|
---
|
|
183
219
|
|
|
184
|
-
##
|
|
220
|
+
## License
|
|
185
221
|
|
|
186
|
-
|
|
187
|
-
https://github.com/ggerman
|
|
188
|
-
https://rubystacknews.com
|
|
222
|
+
MIT
|
data/lib/gd/gis/basemap.rb
CHANGED
|
@@ -1,18 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "net/http"
|
|
2
4
|
require "fileutils"
|
|
3
5
|
|
|
4
6
|
module GD
|
|
5
7
|
module GIS
|
|
8
|
+
# Fetches and manages raster basemap tiles using the XYZ
|
|
9
|
+
# Web Mercator tiling scheme.
|
|
10
|
+
#
|
|
11
|
+
# A Basemap is responsible for:
|
|
12
|
+
# - Converting geographic coordinates to tile coordinates
|
|
13
|
+
# - Downloading raster tiles from public tile providers
|
|
14
|
+
# - Caching tiles on disk
|
|
15
|
+
# - Exposing pixel origins for map composition
|
|
16
|
+
#
|
|
17
|
+
# All tiles are assumed to be 256×256 pixels.
|
|
18
|
+
#
|
|
19
|
+
# @example Fetch tiles for a bounding box
|
|
20
|
+
# bbox = [-3.8, 40.3, -3.6, 40.5] # west, south, east, north
|
|
21
|
+
# basemap = GD::GIS::Basemap.new(12, bbox, :carto_light)
|
|
22
|
+
# tiles, origin_x, origin_y = basemap.fetch_tiles
|
|
23
|
+
#
|
|
6
24
|
class Basemap
|
|
25
|
+
# Tile size in pixels (Web Mercator standard)
|
|
7
26
|
TILE_SIZE = 256
|
|
8
|
-
attr_reader :origin_x, :origin_y
|
|
9
27
|
|
|
10
|
-
|
|
28
|
+
# @return [Integer] pixel X origin of the basemap
|
|
29
|
+
attr_reader :origin_x
|
|
30
|
+
|
|
31
|
+
# @return [Integer] pixel Y origin of the basemap
|
|
32
|
+
attr_reader :origin_y
|
|
33
|
+
|
|
34
|
+
# Creates a new basemap tile source.
|
|
35
|
+
#
|
|
36
|
+
# @param zoom [Integer] zoom level
|
|
37
|
+
# @param bbox [Array<Float>] bounding box [west, south, east, north]
|
|
38
|
+
# @param provider [Symbol] tile provider/style
|
|
39
|
+
def initialize(zoom, bbox, provider = :carto_light)
|
|
11
40
|
@zoom = zoom
|
|
12
41
|
@bbox = bbox
|
|
13
42
|
@provider = provider
|
|
14
43
|
end
|
|
15
44
|
|
|
45
|
+
# Builds a tile URL for a given provider and tile coordinate.
|
|
46
|
+
#
|
|
47
|
+
# @param z [Integer] zoom level
|
|
48
|
+
# @param x [Integer] tile X coordinate
|
|
49
|
+
# @param y [Integer] tile Y coordinate
|
|
50
|
+
# @param style [Symbol] tile style/provider
|
|
51
|
+
# @return [String] tile URL
|
|
52
|
+
# @raise [RuntimeError] if the provider is unknown
|
|
16
53
|
def url(z, x, y, style = :osm)
|
|
17
54
|
case style
|
|
18
55
|
|
|
@@ -55,9 +92,6 @@ module GD
|
|
|
55
92
|
when :esri_terrain
|
|
56
93
|
"https://services.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/#{z}/#{y}/#{x}"
|
|
57
94
|
|
|
58
|
-
when :esri_hybrid
|
|
59
|
-
"https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/#{z}/#{y}/#{x}"
|
|
60
|
-
|
|
61
95
|
# ==============================
|
|
62
96
|
# STAMEN 503
|
|
63
97
|
# ==============================
|
|
@@ -102,15 +136,34 @@ module GD
|
|
|
102
136
|
end
|
|
103
137
|
end
|
|
104
138
|
|
|
139
|
+
# Converts longitude to tile X coordinate.
|
|
140
|
+
#
|
|
141
|
+
# @param lon [Float] longitude in degrees
|
|
142
|
+
# @return [Integer] tile X coordinate
|
|
105
143
|
def lon2tile(lon)
|
|
106
|
-
((lon + 180.0) / 360.0 * (2
|
|
144
|
+
((lon + 180.0) / 360.0 * (2**@zoom)).floor
|
|
107
145
|
end
|
|
108
146
|
|
|
147
|
+
# Converts latitude to tile Y coordinate.
|
|
148
|
+
#
|
|
149
|
+
# @param lat [Float] latitude in degrees
|
|
150
|
+
# @return [Integer] tile Y coordinate
|
|
109
151
|
def lat2tile(lat)
|
|
110
152
|
rad = lat * Math::PI / 180
|
|
111
|
-
((1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math::PI) / 2 * (2
|
|
153
|
+
((1 - (Math.log(Math.tan(rad) + (1 / Math.cos(rad))) / Math::PI)) / 2 * (2**@zoom)).floor
|
|
112
154
|
end
|
|
113
155
|
|
|
156
|
+
# Downloads all tiles required to cover the bounding box.
|
|
157
|
+
#
|
|
158
|
+
# Tiles are cached on disk under `tmp/tiles`.
|
|
159
|
+
#
|
|
160
|
+
# @return [Array]
|
|
161
|
+
# - tiles [Array<Array(Integer, Integer, String)>]
|
|
162
|
+
# list of [x, y, file_path]
|
|
163
|
+
# - origin_x [Integer] pixel X origin
|
|
164
|
+
# - origin_y [Integer] pixel Y origin
|
|
165
|
+
#
|
|
166
|
+
# @raise [RuntimeError] if a tile cannot be fetched
|
|
114
167
|
def fetch_tiles
|
|
115
168
|
west, south, east, north = @bbox
|
|
116
169
|
|
|
@@ -133,8 +186,14 @@ module GD
|
|
|
133
186
|
(y_min..y_max).each do |y|
|
|
134
187
|
path = nil
|
|
135
188
|
|
|
136
|
-
|
|
137
|
-
|
|
189
|
+
if File.exist?("tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.png") ||
|
|
190
|
+
File.exist?("tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.jpg")
|
|
191
|
+
path = if File.exist?("tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.png")
|
|
192
|
+
"tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.png"
|
|
193
|
+
else
|
|
194
|
+
"tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.jpg"
|
|
195
|
+
end
|
|
196
|
+
else
|
|
138
197
|
|
|
139
198
|
uri = URI(url(@zoom, x, y, @provider))
|
|
140
199
|
|
|
@@ -159,15 +218,9 @@ module GD
|
|
|
159
218
|
path = "tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.#{ext}"
|
|
160
219
|
File.binwrite(path, res.body)
|
|
161
220
|
end
|
|
162
|
-
else
|
|
163
|
-
if File.exist?("tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.png")
|
|
164
|
-
path = "tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.png"
|
|
165
|
-
else
|
|
166
|
-
path = "tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.jpg"
|
|
167
|
-
end
|
|
168
221
|
end
|
|
169
222
|
|
|
170
|
-
tiles << [x,y,path]
|
|
223
|
+
tiles << [x, y, path]
|
|
171
224
|
end
|
|
172
225
|
end
|
|
173
226
|
|
data/lib/gd/gis/classifier.rb
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module GD
|
|
2
4
|
module GIS
|
|
5
|
+
# Classifies geographic features based on their properties.
|
|
6
|
+
#
|
|
7
|
+
# This class provides a set of stateless helpers used to
|
|
8
|
+
# infer semantic categories (roads, water, parks, rails)
|
|
9
|
+
# from feature attribute tags, typically originating from
|
|
10
|
+
# OpenStreetMap or similar datasets.
|
|
11
|
+
#
|
|
12
|
+
# All methods are pure functions and return symbols or booleans
|
|
13
|
+
# suitable for styling or rendering decisions.
|
|
14
|
+
#
|
|
3
15
|
class Classifier
|
|
16
|
+
# Classifies a road feature into a road category.
|
|
17
|
+
#
|
|
18
|
+
# The classification is based on the `highway` tag.
|
|
19
|
+
#
|
|
20
|
+
# @param feature [GD::GIS::Feature]
|
|
21
|
+
# @return [Symbol, nil]
|
|
22
|
+
# one of:
|
|
23
|
+
# - :motorway
|
|
24
|
+
# - :primary
|
|
25
|
+
# - :secondary
|
|
26
|
+
# - :street
|
|
27
|
+
# - :minor
|
|
28
|
+
# - nil if the feature is not a road
|
|
4
29
|
def self.road(feature)
|
|
5
30
|
tags = feature.properties || {}
|
|
6
31
|
|
|
@@ -11,37 +36,52 @@ module GD
|
|
|
11
36
|
:primary
|
|
12
37
|
when "secondary", "secondary_link"
|
|
13
38
|
:secondary
|
|
14
|
-
when "tertiary"
|
|
15
|
-
:street
|
|
16
|
-
when "residential", "living_street"
|
|
39
|
+
when "tertiary", "residential", "living_street"
|
|
17
40
|
:street
|
|
18
41
|
when "service", "track"
|
|
19
42
|
:minor
|
|
20
|
-
else
|
|
21
|
-
nil
|
|
22
43
|
end
|
|
23
44
|
end
|
|
24
45
|
|
|
46
|
+
# Determines whether a feature represents water.
|
|
47
|
+
#
|
|
48
|
+
# @param feature [GD::GIS::Feature]
|
|
49
|
+
# @return [Boolean] true if the feature is water-related
|
|
25
50
|
def self.water?(feature)
|
|
26
51
|
p = feature.properties
|
|
27
52
|
|
|
28
53
|
p["waterway"] ||
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
54
|
+
p["natural"] == "water" ||
|
|
55
|
+
p["fclass"] == "river" ||
|
|
56
|
+
p["fclass"] == "stream"
|
|
32
57
|
end
|
|
33
58
|
|
|
59
|
+
# Determines whether a feature represents a railway.
|
|
60
|
+
#
|
|
61
|
+
# @param feature [GD::GIS::Feature]
|
|
62
|
+
# @return [Boolean] true if the feature is a rail feature
|
|
34
63
|
def self.rail?(feature)
|
|
35
64
|
tags = feature.properties || {}
|
|
36
65
|
tags["railway"]
|
|
37
66
|
end
|
|
38
67
|
|
|
68
|
+
# Determines whether a feature represents a park or green area.
|
|
69
|
+
#
|
|
70
|
+
# @param feature [GD::GIS::Feature]
|
|
71
|
+
# @return [Boolean] true if the feature is a park or green space
|
|
39
72
|
def self.park?(feature)
|
|
40
73
|
tags = feature.properties || {}
|
|
41
74
|
%w[park recreation_ground garden].include?(tags["leisure"]) ||
|
|
42
75
|
%w[park grass forest].include?(tags["landuse"])
|
|
43
76
|
end
|
|
44
77
|
|
|
78
|
+
# Classifies the type of water feature.
|
|
79
|
+
#
|
|
80
|
+
# @param feature [GD::GIS::Feature]
|
|
81
|
+
# @return [Symbol]
|
|
82
|
+
# - :river
|
|
83
|
+
# - :stream
|
|
84
|
+
# - :minor (default / fallback)
|
|
45
85
|
def self.water_kind(feature)
|
|
46
86
|
p = feature.properties
|
|
47
87
|
|
|
@@ -51,7 +91,6 @@ module GD
|
|
|
51
91
|
else :minor
|
|
52
92
|
end
|
|
53
93
|
end
|
|
54
|
-
|
|
55
94
|
end
|
|
56
95
|
end
|
|
57
96
|
end
|
data/lib/gd/gis/color_helpers.rb
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module GD
|
|
2
4
|
module GIS
|
|
5
|
+
# Utility helpers for generating colors compatible with GD.
|
|
6
|
+
#
|
|
7
|
+
# This module provides convenience methods for creating
|
|
8
|
+
# random RGB / RGBA colors and vivid colors suitable for
|
|
9
|
+
# map rendering and styling.
|
|
10
|
+
#
|
|
11
|
+
# All methods return instances of {GD::Color}.
|
|
12
|
+
#
|
|
3
13
|
module ColorHelpers
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
14
|
+
# Generates a random RGB color.
|
|
15
|
+
#
|
|
16
|
+
# @param min [Integer] minimum channel value (0–255)
|
|
17
|
+
# @param max [Integer] maximum channel value (0–255)
|
|
18
|
+
# @return [GD::Color]
|
|
7
19
|
def self.random_rgb(min: 0, max: 255)
|
|
8
20
|
GD::Color.rgb(
|
|
9
21
|
rand(min..max),
|
|
@@ -12,9 +24,12 @@ module GD
|
|
|
12
24
|
)
|
|
13
25
|
end
|
|
14
26
|
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
27
|
+
# Generates a random RGBA color.
|
|
28
|
+
#
|
|
29
|
+
# @param min [Integer] minimum channel value (0–255)
|
|
30
|
+
# @param max [Integer] maximum channel value (0–255)
|
|
31
|
+
# @param alpha [Integer, nil] alpha channel (0–255), random if nil
|
|
32
|
+
# @return [GD::Color]
|
|
18
33
|
def self.random_rgba(min: 0, max: 255, alpha: nil)
|
|
19
34
|
GD::Color.rgba(
|
|
20
35
|
rand(min..max),
|
|
@@ -24,9 +39,12 @@ module GD
|
|
|
24
39
|
)
|
|
25
40
|
end
|
|
26
41
|
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
42
|
+
# Generates a random vivid RGB color.
|
|
43
|
+
#
|
|
44
|
+
# Vivid colors avoid low saturation and brightness values,
|
|
45
|
+
# making them suitable for distinguishing map features.
|
|
46
|
+
#
|
|
47
|
+
# @return [GD::Color]
|
|
30
48
|
def self.random_vivid
|
|
31
49
|
h = rand
|
|
32
50
|
s = rand(0.6..1.0)
|
|
@@ -36,15 +54,21 @@ module GD
|
|
|
36
54
|
GD::Color.rgb(r, g, b)
|
|
37
55
|
end
|
|
38
56
|
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
57
|
+
# Converts HSV color values to RGB.
|
|
58
|
+
#
|
|
59
|
+
# Hue, saturation, and value are expected to be in the
|
|
60
|
+
# range 0.0–1.0.
|
|
61
|
+
#
|
|
62
|
+
# @param h [Float] hue
|
|
63
|
+
# @param s [Float] saturation
|
|
64
|
+
# @param v [Float] value
|
|
65
|
+
# @return [Array<Integer>] RGB values in the range 0–255
|
|
42
66
|
def self.hsv_to_rgb(h, s, v)
|
|
43
67
|
i = (h * 6).floor
|
|
44
|
-
f = h * 6 - i
|
|
68
|
+
f = (h * 6) - i
|
|
45
69
|
p = v * (1 - s)
|
|
46
|
-
q = v * (1 - f * s)
|
|
47
|
-
t = v * (1 - (1 - f) * s)
|
|
70
|
+
q = v * (1 - (f * s))
|
|
71
|
+
t = v * (1 - ((1 - f) * s))
|
|
48
72
|
|
|
49
73
|
r, g, b =
|
|
50
74
|
case i % 6
|
|
@@ -58,8 +82,6 @@ module GD
|
|
|
58
82
|
|
|
59
83
|
[(r * 255).to_i, (g * 255).to_i, (b * 255).to_i]
|
|
60
84
|
end
|
|
61
|
-
|
|
62
85
|
end
|
|
63
86
|
end
|
|
64
87
|
end
|
|
65
|
-
|