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.
@@ -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 = [[lat, 85.05112878].min, -85.05112878].max
28
- n = 2.0 ** zoom
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
- attr_reader :roads, :rails, :water, :parks, :points, :order
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
- "Style error: color array must be [r,g,b] or [r,g,b,a]"
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
- "Style error: invalid color format (#{color.inspect})"
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
- attr_reader :z, :x, :y, :image
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
- def self.osm(z:, x:, y:)
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
- z: z,
15
- x: x,
16
- y: y,
17
- source: "https://api.maptiler.com/maps/basic/#{z}/#{x}/#{y}.png?key=GetYourOwnKey"
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
- end
20
-
21
- def initialize(z:, x:, y:, source:)
22
- @z = z
23
- @x = x
24
- @y = y
25
- @source = source
26
- end
27
-
28
- def render
29
- tmp = Tempfile.new(["tile", ".png"])
30
- tmp.binmode
31
- tmp.write URI.open(@source).read
32
- tmp.flush
33
-
34
- @image = GD::Image.open(tmp.path)
35
- ensure
36
- tmp.close
37
- end
38
-
39
- def save(path)
40
- @image.save(path)
41
- end
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.2.9
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-22 00:00:00.000000000 Z
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
File without changes
File without changes
File without changes