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
data/lib/gd/gis/projection.rb
CHANGED
|
@@ -1,36 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module GD
|
|
2
4
|
module GIS
|
|
5
|
+
# Projection helpers for Web Mercator–based maps.
|
|
6
|
+
#
|
|
7
|
+
# This module provides low-level projection utilities used
|
|
8
|
+
# throughout the rendering pipeline to convert geographic
|
|
9
|
+
# coordinates (longitude, latitude) into projected and pixel
|
|
10
|
+
# coordinates.
|
|
11
|
+
#
|
|
12
|
+
# All longitude and latitude values are assumed to be in
|
|
13
|
+
# WGS84 (EPSG:4326).
|
|
14
|
+
#
|
|
3
15
|
module Projection
|
|
16
|
+
# Earth radius used for Web Mercator (meters)
|
|
4
17
|
R = 6378137.0
|
|
5
18
|
|
|
19
|
+
# Converts longitude to Web Mercator X coordinate.
|
|
20
|
+
#
|
|
21
|
+
# @param lon [Float] longitude in degrees
|
|
22
|
+
# @return [Float] X coordinate in meters
|
|
6
23
|
def self.mercator_x(lon)
|
|
7
24
|
lon * Math::PI / 180.0 * R
|
|
8
25
|
end
|
|
9
26
|
|
|
27
|
+
# Converts latitude to Web Mercator Y coordinate.
|
|
28
|
+
#
|
|
29
|
+
# @param lat [Float] latitude in degrees
|
|
30
|
+
# @return [Float] Y coordinate in meters
|
|
10
31
|
def self.mercator_y(lat)
|
|
11
|
-
Math.log(Math.tan(Math::PI/4 + lat * Math::PI / 360.0)) * R
|
|
32
|
+
Math.log(Math.tan((Math::PI / 4) + (lat * Math::PI / 360.0))) * R
|
|
12
33
|
end
|
|
13
34
|
|
|
35
|
+
# Projects geographic coordinates into pixel space
|
|
36
|
+
# relative to a bounding box.
|
|
37
|
+
#
|
|
38
|
+
# This method is typically used for viewport-based rendering
|
|
39
|
+
# where a fixed image size is mapped to a geographic extent.
|
|
40
|
+
#
|
|
41
|
+
# @param lon [Float] longitude in degrees
|
|
42
|
+
# @param lat [Float] latitude in degrees
|
|
43
|
+
# @param min_x [Float] minimum Web Mercator X (meters)
|
|
44
|
+
# @param max_x [Float] maximum Web Mercator X (meters)
|
|
45
|
+
# @param min_y [Float] minimum Web Mercator Y (meters)
|
|
46
|
+
# @param max_y [Float] maximum Web Mercator Y (meters)
|
|
47
|
+
# @param width [Integer] image width in pixels
|
|
48
|
+
# @param height [Integer] image height in pixels
|
|
49
|
+
#
|
|
50
|
+
# @return [Array<Integer>] pixel coordinates [x, y]
|
|
14
51
|
def self.lonlat_to_pixel(lon, lat, min_x, max_x, min_y, max_y, width, height)
|
|
15
52
|
x = mercator_x(lon)
|
|
16
53
|
y = mercator_y(lat)
|
|
17
54
|
|
|
18
55
|
px = (x - min_x) / (max_x - min_x) * width
|
|
19
|
-
py = height - (y - min_y) / (max_y - min_y) * height
|
|
56
|
+
py = height - ((y - min_y) / (max_y - min_y) * height)
|
|
20
57
|
|
|
21
58
|
[px.to_i, py.to_i]
|
|
22
59
|
end
|
|
23
60
|
|
|
61
|
+
# Web Mercator tile size in pixels
|
|
24
62
|
TILE_SIZE = 256
|
|
25
63
|
|
|
64
|
+
# Converts geographic coordinates to global pixel coordinates.
|
|
65
|
+
#
|
|
66
|
+
# This method implements the standard XYZ / Web Mercator
|
|
67
|
+
# tiling scheme used by most web map providers.
|
|
68
|
+
#
|
|
69
|
+
# Latitude values are clamped to the valid Web Mercator range.
|
|
70
|
+
#
|
|
71
|
+
# @param lon [Float] longitude in degrees
|
|
72
|
+
# @param lat [Float] latitude in degrees
|
|
73
|
+
# @param zoom [Integer] zoom level
|
|
74
|
+
#
|
|
75
|
+
# @return [Array<Float>] global pixel coordinates [x, y]
|
|
26
76
|
def self.lonlat_to_global_px(lon, lat, zoom)
|
|
27
|
-
lat =
|
|
28
|
-
n = 2.0
|
|
77
|
+
lat = lat.clamp(-85.05112878, 85.05112878)
|
|
78
|
+
n = 2.0**zoom
|
|
29
79
|
|
|
30
80
|
x = (lon + 180.0) / 360.0 * n * TILE_SIZE
|
|
31
81
|
|
|
32
82
|
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
|
|
83
|
+
y = (1.0 - (Math.log(Math.tan(lat_rad) + (1.0 / Math.cos(lat_rad))) / Math::PI)) / 2.0 * n * TILE_SIZE
|
|
34
84
|
|
|
35
85
|
[x, y]
|
|
36
86
|
end
|
data/lib/gd/gis/style.rb
CHANGED
|
@@ -1,10 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "yaml"
|
|
2
4
|
|
|
3
5
|
module GD
|
|
4
6
|
module GIS
|
|
7
|
+
# Defines visual styling rules for map rendering.
|
|
8
|
+
#
|
|
9
|
+
# A Style object encapsulates all visual configuration used
|
|
10
|
+
# during rendering, including colors, stroke widths, fonts,
|
|
11
|
+
# and layer ordering.
|
|
12
|
+
#
|
|
13
|
+
# Styles are typically loaded from YAML files and applied
|
|
14
|
+
# to a {GD::GIS::Map} instance before rendering.
|
|
15
|
+
#
|
|
5
16
|
class Style
|
|
6
|
-
|
|
17
|
+
# @return [Hash] road styling rules
|
|
18
|
+
attr_reader :roads
|
|
19
|
+
|
|
20
|
+
# @return [Hash] rail styling rules
|
|
21
|
+
attr_reader :rails
|
|
22
|
+
|
|
23
|
+
# @return [Hash] water styling rules
|
|
24
|
+
attr_reader :water
|
|
25
|
+
|
|
26
|
+
# @return [Hash] park styling rules
|
|
27
|
+
attr_reader :parks
|
|
28
|
+
|
|
29
|
+
# @return [Hash] point styling rules
|
|
30
|
+
attr_reader :points
|
|
31
|
+
|
|
32
|
+
# @return [Array<Symbol>] drawing order of semantic layers
|
|
33
|
+
attr_reader :order
|
|
7
34
|
|
|
35
|
+
# Creates a new style from a definition hash.
|
|
36
|
+
#
|
|
37
|
+
# @param definition [Hash]
|
|
38
|
+
# style definition with optional sections:
|
|
39
|
+
# :roads, :rails, :water, :parks, :points, :order
|
|
8
40
|
def initialize(definition)
|
|
9
41
|
@roads = definition[:roads] || {}
|
|
10
42
|
@rails = definition[:rails] || {}
|
|
@@ -14,6 +46,22 @@ module GD
|
|
|
14
46
|
@order = definition[:order] || []
|
|
15
47
|
end
|
|
16
48
|
|
|
49
|
+
# Loads a style definition from a YAML file.
|
|
50
|
+
#
|
|
51
|
+
# The file name is resolved as:
|
|
52
|
+
#
|
|
53
|
+
# <from>/<name>.yml
|
|
54
|
+
#
|
|
55
|
+
# All keys are deep-symbolized on load.
|
|
56
|
+
#
|
|
57
|
+
# @param name [String, Symbol]
|
|
58
|
+
# style name (without extension)
|
|
59
|
+
# @param from [String]
|
|
60
|
+
# directory containing style files
|
|
61
|
+
#
|
|
62
|
+
# @return [Style]
|
|
63
|
+
# @raise [RuntimeError] if the style file does not exist
|
|
64
|
+
# @raise [Psych::SyntaxError] if the YAML is invalid
|
|
17
65
|
def self.load(name, from: "styles")
|
|
18
66
|
path = File.join(from, "#{name}.yml")
|
|
19
67
|
raise "Style not found: #{path}" unless File.exist?(path)
|
|
@@ -31,6 +79,10 @@ module GD
|
|
|
31
79
|
)
|
|
32
80
|
end
|
|
33
81
|
|
|
82
|
+
# Recursively converts hash keys to symbols.
|
|
83
|
+
#
|
|
84
|
+
# @param obj [Object]
|
|
85
|
+
# @return [Object]
|
|
34
86
|
def self.deep_symbolize(obj)
|
|
35
87
|
case obj
|
|
36
88
|
when Hash
|
|
@@ -43,6 +95,17 @@ module GD
|
|
|
43
95
|
end
|
|
44
96
|
end
|
|
45
97
|
|
|
98
|
+
# Normalizes a color definition into a GD::Color.
|
|
99
|
+
#
|
|
100
|
+
# Accepted formats:
|
|
101
|
+
# - GD::Color instance
|
|
102
|
+
# - [r, g, b]
|
|
103
|
+
# - [r, g, b, a]
|
|
104
|
+
# - nil (generates a random vivid color)
|
|
105
|
+
#
|
|
106
|
+
# @param color [GD::Color, Array<Integer>, nil]
|
|
107
|
+
# @return [GD::Color]
|
|
108
|
+
# @raise [ArgumentError] if the format is invalid
|
|
46
109
|
def normalize_color(color)
|
|
47
110
|
case color
|
|
48
111
|
when GD::Color
|
|
@@ -56,7 +119,7 @@ module GD
|
|
|
56
119
|
GD::Color.rgba(*color)
|
|
57
120
|
else
|
|
58
121
|
raise ArgumentError,
|
|
59
|
-
|
|
122
|
+
"Style error: color array must be [r,g,b] or [r,g,b,a]"
|
|
60
123
|
end
|
|
61
124
|
|
|
62
125
|
when nil
|
|
@@ -64,7 +127,7 @@ module GD
|
|
|
64
127
|
|
|
65
128
|
else
|
|
66
129
|
raise ArgumentError,
|
|
67
|
-
|
|
130
|
+
"Style error: invalid color format (#{color.inspect})"
|
|
68
131
|
end
|
|
69
132
|
end
|
|
70
133
|
end
|
data/lib/gd/gis.rb
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "gd"
|
|
2
4
|
|
|
5
|
+
# LibGD::GIS provides high-level GIS rendering primitives
|
|
6
|
+
# built on top of the GD graphics library.
|
|
7
|
+
#
|
|
8
|
+
# The library is focused on rendering geographic data
|
|
9
|
+
# (points, lines, polygons, and GeoJSON) into raster images
|
|
10
|
+
# using a layered map model.
|
|
11
|
+
#
|
|
12
|
+
# ## Core concepts
|
|
13
|
+
#
|
|
14
|
+
# - {LibGD::GIS::Map} — rendering surface and orchestration
|
|
15
|
+
# - {LibGD::GIS::Layer} — drawable data layers
|
|
16
|
+
# - {LibGD::GIS::Geometry} — geometric primitives
|
|
17
|
+
# - {LibGD::GIS::Projection} — coordinate transformations
|
|
18
|
+
#
|
|
19
|
+
# ## Typical usage
|
|
20
|
+
#
|
|
21
|
+
# map = LibGD::GIS::Map.new(
|
|
22
|
+
# bbox: bbox,
|
|
23
|
+
# zoom: zoom,
|
|
24
|
+
# basemap: :nasa_goes_geocolor
|
|
25
|
+
# )
|
|
26
|
+
# map.style = GD::GIS::Style.load(style_name.yml)
|
|
27
|
+
# map.render
|
|
28
|
+
# map.save("map_name.png")
|
|
29
|
+
#
|
|
3
30
|
require_relative "gis/color_helpers"
|
|
31
|
+
require_relative "gis/font_helper"
|
|
4
32
|
require_relative "gis/style"
|
|
5
33
|
require_relative "gis/classifier"
|
|
6
34
|
|
data/lib/libgd_gis.rb
CHANGED
|
@@ -1,44 +1,74 @@
|
|
|
1
1
|
require "libgd_gis"
|
|
2
|
-
|
|
3
2
|
require "open-uri"
|
|
4
3
|
require "tempfile"
|
|
5
4
|
require "gd"
|
|
6
5
|
|
|
6
|
+
# Namespace for LibGD extensions
|
|
7
7
|
module LibGD
|
|
8
8
|
module GIS
|
|
9
|
+
# Represents a single map tile fetched from a remote tile provider.
|
|
9
10
|
class Tile
|
|
10
|
-
|
|
11
|
+
# @return [Integer] zoom level
|
|
12
|
+
attr_reader :z
|
|
13
|
+
|
|
14
|
+
# @return [Integer] tile X coordinate
|
|
15
|
+
attr_reader :x
|
|
16
|
+
|
|
17
|
+
# @return [Integer] tile Y coordinate
|
|
18
|
+
attr_reader :y
|
|
19
|
+
|
|
20
|
+
# @return [GD::Image, nil] rendered image
|
|
21
|
+
attr_reader :image
|
|
11
22
|
|
|
12
|
-
|
|
23
|
+
# Builds a tile using an OSM-compatible XYZ source
|
|
24
|
+
#
|
|
25
|
+
# @param z [Integer] zoom level
|
|
26
|
+
# @param x [Integer] X coordinate
|
|
27
|
+
# @param y [Integer] Y coordinate
|
|
28
|
+
# @return [Tile]
|
|
29
|
+
def self.osm(z:, x:, y:)
|
|
13
30
|
new(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
31
|
+
z: z,
|
|
32
|
+
x: x,
|
|
33
|
+
y: y,
|
|
34
|
+
source: "https://api.maptiler.com/maps/basic/#{z}/#{x}/#{y}.png?key=GetYourOwnKey"
|
|
18
35
|
)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param z [Integer]
|
|
39
|
+
# @param x [Integer]
|
|
40
|
+
# @param y [Integer]
|
|
41
|
+
# @param source [String] remote image URL
|
|
42
|
+
def initialize(z:, x:, y:, source:)
|
|
43
|
+
@z = z
|
|
44
|
+
@x = x
|
|
45
|
+
@y = y
|
|
46
|
+
@source = source
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Downloads and renders the tile image
|
|
50
|
+
#
|
|
51
|
+
# @return [GD::Image]
|
|
52
|
+
def render
|
|
53
|
+
tmp = Tempfile.new(["tile", ".png"])
|
|
54
|
+
tmp.binmode
|
|
55
|
+
uri = URI(@source)
|
|
56
|
+
response = Net::HTTP.get(uri)
|
|
57
|
+
tmp.write(response)
|
|
58
|
+
tmp.flush
|
|
59
|
+
|
|
60
|
+
@image = GD::Image.open(tmp.path)
|
|
61
|
+
ensure
|
|
62
|
+
tmp.close
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Saves the rendered image to disk
|
|
66
|
+
#
|
|
67
|
+
# @param path [String]
|
|
68
|
+
# @return [void]
|
|
69
|
+
def save(path)
|
|
70
|
+
@image.save(path)
|
|
71
|
+
end
|
|
42
72
|
end
|
|
43
73
|
end
|
|
44
74
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: libgd-gis
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Germán Alberto Giménez Silva
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-01-
|
|
11
|
+
date: 2026-01-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ruby-libgd
|
|
@@ -30,6 +30,34 @@ dependencies:
|
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
32
|
version: 0.2.3
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: rubocop
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.60'
|
|
40
|
+
type: :development
|
|
41
|
+
prerelease: false
|
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.60'
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: rubocop-performance
|
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.20'
|
|
54
|
+
type: :development
|
|
55
|
+
prerelease: false
|
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.20'
|
|
33
61
|
description: A native GIS raster engine for Ruby built on libgd. Render maps, GeoJSON,
|
|
34
62
|
heatmaps and tiles.
|
|
35
63
|
email:
|
|
@@ -45,11 +73,8 @@ files:
|
|
|
45
73
|
- lib/gd/gis/color_helpers.rb
|
|
46
74
|
- lib/gd/gis/crs_normalizer.rb
|
|
47
75
|
- lib/gd/gis/feature.rb
|
|
76
|
+
- lib/gd/gis/font_helper.rb
|
|
48
77
|
- lib/gd/gis/geometry.rb
|
|
49
|
-
- lib/gd/gis/input/detector.rb
|
|
50
|
-
- lib/gd/gis/input/geojson.rb
|
|
51
|
-
- lib/gd/gis/input/kml.rb
|
|
52
|
-
- lib/gd/gis/input/shapefile.rb
|
|
53
78
|
- lib/gd/gis/layer_geojson.rb
|
|
54
79
|
- lib/gd/gis/layer_lines.rb
|
|
55
80
|
- lib/gd/gis/layer_points.rb
|
|
@@ -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/input/geojson.rb
DELETED
|
File without changes
|
data/lib/gd/gis/input/kml.rb
DELETED
|
File without changes
|
|
File without changes
|