libgd-gis 0.2.9 → 0.3.1

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,16 +1,59 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "yaml"
2
4
 
3
5
  module GD
4
6
  module GIS
7
+ # Classifies features into semantic layers using rule-based matching.
8
+ #
9
+ # An Ontology maps feature properties to logical layer identifiers
10
+ # (e.g. :water, :road, :park) based on a YAML rule definition.
11
+ #
12
+ # The ontology is intentionally simple and heuristic-based:
13
+ # - Rules are evaluated in order
14
+ # - The first matching rule wins
15
+ # - Matching is case-insensitive and substring-based
16
+ #
17
+ # This design favors robustness and flexibility over strict
18
+ # schema enforcement.
19
+ #
5
20
  class Ontology
21
+ # Creates a new ontology.
22
+ #
23
+ # @param path [String, nil]
24
+ # path to a YAML ontology file; defaults to `ontology.yml`
25
+ # shipped with the gem
26
+ #
27
+ # @raise [Errno::ENOENT] if the file does not exist
28
+ # @raise [Psych::SyntaxError] if the YAML is invalid
6
29
  def initialize(path = nil)
7
30
  path ||= File.expand_path("ontology.yml", __dir__)
8
31
  @rules = YAML.load_file(path)
9
32
  end
10
33
 
34
+ # Classifies a feature into a semantic layer.
35
+ #
36
+ # Properties are matched against ontology rules using
37
+ # case-insensitive substring comparison.
38
+ #
39
+ # @param properties [Hash]
40
+ # feature attributes / tags
41
+ # @param geometry_type [String, nil]
42
+ # GeoJSON geometry type
43
+ #
44
+ # @return [Symbol, nil]
45
+ # semantic layer identifier, or nil if no rule matches
46
+ #
47
+ # @example
48
+ # ontology.classify({ "waterway" => "river" })
49
+ # #=> :water
50
+ #
51
+ # @example
52
+ # ontology.classify({}, geometry_type: "Point")
53
+ # #=> :points
11
54
  def classify(properties, geometry_type: nil)
12
55
  @rules.each do |layer, sources|
13
- sources.each do |source, rules|
56
+ sources.each_value do |rules|
14
57
  rules.each do |key, values|
15
58
  v = (properties[key.to_s] || properties[key.to_sym]).to_s.strip.downcase
16
59
  values = values.map { |x| x.to_s.downcase }
@@ -20,11 +63,11 @@ module GD
20
63
  end
21
64
  end
22
65
 
66
+ # Fallback classification
23
67
  return :points if geometry_type == "Point"
24
68
 
25
69
  nil
26
70
  end
27
-
28
71
  end
29
72
  end
30
73
  end
@@ -1,4 +1,10 @@
1
1
  water:
2
+ osm:
3
+ waterway:
4
+ - river
5
+ - canal
6
+ - stream
7
+
2
8
  ign:
3
9
  objeto:
4
10
  - canal
@@ -26,3 +32,4 @@ track:
26
32
  gps:
27
33
  name:
28
34
  - track
35
+ - Track
@@ -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,19 +1,75 @@
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] global styling rules
18
+ attr_reader :global
19
+
20
+ # @return [Hash] road styling rules
21
+ attr_reader :roads
22
+
23
+ # @return [Hash] rail styling rules
24
+ attr_reader :rails
25
+
26
+ # @return [Hash] water styling rules
27
+ attr_reader :water
28
+
29
+ # @return [Hash] park styling rules
30
+ attr_reader :parks
31
+
32
+ # @return [Hash] point styling rules
33
+ attr_reader :points
34
+
35
+ # @return [Hash] track styling rules
36
+ attr_reader :track
37
+
38
+ # @return [Array<Symbol>] drawing order of semantic layers
39
+ attr_reader :order
7
40
 
41
+ # Creates a new style from a definition hash.
42
+ #
43
+ # @param definition [Hash]
44
+ # style definition with optional sections:
45
+ # :global, :roads, :rails, :water, :parks, :points, :order, :track
8
46
  def initialize(definition)
47
+ @global = definition[:global] || {}
9
48
  @roads = definition[:roads] || {}
10
49
  @rails = definition[:rails] || {}
11
50
  @water = definition[:water] || {}
12
51
  @parks = definition[:parks] || {}
13
52
  @points = definition[:points] || {}
53
+ @track = definition[:track] || {}
14
54
  @order = definition[:order] || []
15
55
  end
16
56
 
57
+ # Loads a style definition from a YAML file.
58
+ #
59
+ # The file name is resolved as:
60
+ #
61
+ # <from>/<name>.yml
62
+ #
63
+ # All keys are deep-symbolized on load.
64
+ #
65
+ # @param name [String, Symbol]
66
+ # style name (without extension)
67
+ # @param from [String]
68
+ # directory containing style files
69
+ #
70
+ # @return [Style]
71
+ # @raise [RuntimeError] if the style file does not exist
72
+ # @raise [Psych::SyntaxError] if the YAML is invalid
17
73
  def self.load(name, from: "styles")
18
74
  path = File.join(from, "#{name}.yml")
19
75
  raise "Style not found: #{path}" unless File.exist?(path)
@@ -22,8 +78,10 @@ module GD
22
78
  data = deep_symbolize(data)
23
79
 
24
80
  new(
81
+ global: data[:global],
25
82
  roads: data[:roads],
26
83
  rails: data[:rail] || data[:rails],
84
+ track: data[:track],
27
85
  water: data[:water],
28
86
  parks: data[:park] || data[:parks],
29
87
  points: data[:points],
@@ -31,6 +89,10 @@ module GD
31
89
  )
32
90
  end
33
91
 
92
+ # Recursively converts hash keys to symbols.
93
+ #
94
+ # @param obj [Object]
95
+ # @return [Object]
34
96
  def self.deep_symbolize(obj)
35
97
  case obj
36
98
  when Hash
@@ -43,6 +105,17 @@ module GD
43
105
  end
44
106
  end
45
107
 
108
+ # Normalizes a color definition into a GD::Color.
109
+ #
110
+ # Accepted formats:
111
+ # - GD::Color instance
112
+ # - [r, g, b]
113
+ # - [r, g, b, a]
114
+ # - nil (generates a random vivid color)
115
+ #
116
+ # @param color [GD::Color, Array<Integer>, nil]
117
+ # @return [GD::Color]
118
+ # @raise [ArgumentError] if the format is invalid
46
119
  def normalize_color(color)
47
120
  case color
48
121
  when GD::Color
@@ -56,7 +129,7 @@ module GD
56
129
  GD::Color.rgba(*color)
57
130
  else
58
131
  raise ArgumentError,
59
- "Style error: color array must be [r,g,b] or [r,g,b,a]"
132
+ "Style error: color array must be [r,g,b] or [r,g,b,a]"
60
133
  end
61
134
 
62
135
  when nil
@@ -64,7 +137,7 @@ module GD
64
137
 
65
138
  else
66
139
  raise ArgumentError,
67
- "Style error: invalid color format (#{color.inspect})"
140
+ "Style error: invalid color format (#{color.inspect})"
68
141
  end
69
142
  end
70
143
  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
data/lib/test.rb ADDED
@@ -0,0 +1,4 @@
1
+ def foo
2
+ end
3
+
4
+ foo(1, 2)
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.9
4
+ version: 0.3.1
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: 2026-01-22 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: ruby-libgd
@@ -45,11 +44,8 @@ files:
45
44
  - lib/gd/gis/color_helpers.rb
46
45
  - lib/gd/gis/crs_normalizer.rb
47
46
  - lib/gd/gis/feature.rb
47
+ - lib/gd/gis/font_helper.rb
48
48
  - 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
49
  - lib/gd/gis/layer_geojson.rb
54
50
  - lib/gd/gis/layer_lines.rb
55
51
  - lib/gd/gis/layer_points.rb
@@ -61,11 +57,12 @@ files:
61
57
  - lib/gd/gis/projection.rb
62
58
  - lib/gd/gis/style.rb
63
59
  - lib/libgd_gis.rb
60
+ - lib/test.rb
64
61
  homepage: https://github.com/ggerman/libgd-gis
65
62
  licenses:
66
63
  - MIT
67
- metadata: {}
68
- post_install_message:
64
+ metadata:
65
+ rubygems_mfa_required: 'true'
69
66
  rdoc_options: []
70
67
  require_paths:
71
68
  - lib
@@ -80,8 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
80
77
  - !ruby/object:Gem::Version
81
78
  version: '0'
82
79
  requirements: []
83
- rubygems_version: 3.5.22
84
- signing_key:
80
+ rubygems_version: 4.0.4
85
81
  specification_version: 4
86
82
  summary: Geospatial raster rendering for Ruby using libgd
87
83
  test_files: []
@@ -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