Class: GD::GIS::PointsLayer

Inherits:
Object
  • Object
show all
Defined in:
lib/gd/gis/layer_points.rb

Overview

Renders point data as icons (markers) with optional labels.

A PointsLayer draws markers for arbitrary data records. Longitude and latitude values are extracted using callables, allowing the layer to work with any data structure.

Optionally, text labels can be rendered next to each marker.

Instance Method Summary collapse

Constructor Details

#initialize(data, lon:, lat:, icon:, label: nil, font: nil, size: 12, color: [0, 0, 0], font_color: nil, symbol: 0) ⇒ PointsLayer

Creates a new points layer.

Parameters:

  • data (Enumerable)

    collection of data records

  • lon (#call)

    callable extracting longitude from a data record

  • lat (#call)

    callable extracting latitude from a data record

  • icon (String, Array<GD::Color>, nil)

    path to an icon image, or [fill, stroke] colors, or nil to generate a random marker

  • label (#call, nil) (defaults to: nil)

    callable extracting label text from a data record

  • font (String, nil) (defaults to: nil)

    font path used for labels

  • size (Integer) (defaults to: 12)

    font size in pixels

  • color (Array<Integer>) (defaults to: [0, 0, 0])

    label color as RGB array



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/gd/gis/layer_points.rb', line 33

def initialize(
  data,
  lon:,
  lat:,
  icon:,
  label: nil,
  font: nil,
  size: 12,
  color: [0, 0, 0],
  font_color: nil,
  symbol: 0
)
  @data = data
  @lon = lon
  @lat = lat
  @color = color
  @font_color = font_color

  if icon.is_a?(Array) || icon.nil?
    fill, stroke = icon || [GD::GIS::ColorHelpers.random_rgb, GD::GIS::ColorHelpers.random_rgb]
    @icon = build_default_marker(fill, stroke)
  elsif %w[numeric alphabetic symbol].include?(icon)
    @icon = icon
  else
    @icon = GD::Image.open(icon)
    @icon.alpha_blending = true
    @icon.save_alpha = true
  end

  @label = label
  @font  = font
  @size  = size

  @r, @g, @b, @a = color
  @a = 0 if @a.nil?

  @symbol = symbol
end

Instance Method Details

#build_default_marker(fill, stroke) ⇒ GD::Image

Builds a default circular marker icon.

Parameters:

  • fill (GD::Color)
  • stroke (GD::Color)

Returns:

  • (GD::Image)


77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/gd/gis/layer_points.rb', line 77

def build_default_marker(fill, stroke)
  size = 32

  img = GD::Image.new(size, size)
  img.antialias = true

  cx = size / 2
  cy = size / 2
  r  = 5

  # stroke
  img.arc(cx, cy, (r * 2) + 4, (r * 2) + 4, 0, 360, stroke)

  # fill
  img.filled_arc(cx, cy, r * 2, r * 2, 0, 360, fill)

  img
end

#draw_symbol_circle!(img:, x:, y:, symbol:, bg_color:, font_color:, font:, font_size:, angle: 0.0) ⇒ Object

Draws a filled circle (bullet) with a centered numeric label.

  • x, y: circle center in pixels

  • y for text() is BASELINE (not top). We compute baseline to center the text.



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/gd/gis/layer_points.rb', line 162

def draw_symbol_circle!(img:, x:, y:, symbol:, bg_color:, font_color:, font:, font_size:, angle: 0.0)
  diameter = radius_from_text(img, symbol, font: font, size: font_size) * 2

  # 1) Bullet background
  img.filled_ellipse(x, y, diameter, diameter, bg_color)

  # 2) Measure text in pixels (matches rendering)
  text = symbol.to_s
  w, h = img.text_bbox(text, font: font, size: font_size, angle: angle)

  # 3) Compute centered position:
  # text() uses baseline Y, so:
  # top_y     = y - h/2
  # baseline  = top_y + h = y + h/2
  text_x = (x - (w / 2.0)).round
  text_y = (y + (h / 2.0)).round

  # 4) Draw number
  img.text(
    text,
    x: text_x,
    y: text_y,
    font: font,
    size: font_size,
    color: font_color
  )
end

#radius_from_text(img, text, font:, size:, padding: 4) ⇒ Object

Calculates a circle radius that fully contains the rendered text.

img : GD::Image text : String (number, letters, etc.) font : path to .ttf size : font size in points padding : extra pixels around text (visual breathing room)



198
199
200
201
202
203
204
205
206
207
# File 'lib/gd/gis/layer_points.rb', line 198

def radius_from_text(img, text, font:, size:, padding: 4)
  w, h = img.text_bbox(
    text.to_s,
    font: font,
    size: size
  )

  # Use the larger dimension to ensure the text fits
  ([w, h].max / 2.0).ceil + padding
end

#render!(img, projector) ⇒ void

This method returns an undefined value.

Renders all points onto the image.

Parameters:

  • img (GD::Image)

    target image

  • projector (#call)

    callable converting (lon, lat) → (x, y)



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/gd/gis/layer_points.rb', line 103

def render!(img, projector)
  value =
    case @icon
    when "numeric", "symbol"
      @symbol
    when "alphabetic"
      (@symbol + 96).chr
    else
      @icon
    end

  if @icon.is_a?(GD::Image)
    w = @icon.width
    h = @icon.height
  else
    w = radius_from_text(img, value, font: @font, size: @size) * 2
  end

  @data.each do |row|
    lon = @lon.call(row)
    lat = @lat.call(row)

    x, y = projector.call(lon, lat)

    next unless @label && @font

    text = @label.call(row)
    next if text.nil? || text.strip.empty?

    font_h = @size * 1.1

    if @icon == "numeric" || @icon == "alphabetic" || @icon == "symbol"
      draw_symbol_circle!(
        img: img,
        x: x,
        y: y,
        symbol: value,
        bg_color: @color,
        font_color: @font_color,
        font: @font,
        font_size: @size
      )
    else
      img.copy(@icon, x - (w / 2), y - (h / 2), 0, 0, w, h)
    end

    img.text(text,
             x: x + (w / 2) + 4,
             y: y + (font_h / 2),
             size: @size,
             color: @font_color,
             font: @font)
  end
end