Class: Pippa::Map

Inherits:
Object
  • Object
show all
Includes:
Magick
Defined in:
lib/pippa.rb

Overview

An image-based map class that can be overlain with dots of given area and location given by pixel coordinates, lat/lon, or zipcode (courtesy of federalgovernmentzipcodes.us).

Constant Summary collapse

TWO_SQRT_1_PI =
2 * Math.sqrt(1 / Math::PI)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name = 'World') ⇒ Map

Make a new map with given name. See the file maps/_info or call Pippa#map_names for all possible.



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/pippa.rb', line 143

def initialize(name = 'World')

  # Set up drawing standards.
  @point_size = 1
  @fill = 'DarkRed'
  @stroke = 'gray25'
  @fill_opacity = 0.85
  @stroke_width = 1
  @anti_alias = false
  @dot_kind = :square
  @merge = false
  @dots = []

  # Look up global info or return if none.
  return unless @map_info = Map.info[:map][name]
  @image = Image.read("#{File.dirname(__FILE__)}/pippa/maps/#{@map_info[0]}").first
  @width, @height = @image.columns, @image.rows

  # Look up projection info, if any.
  @projection_info = Map.info[:projection][name]
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(sym, *args, &block) ⇒ Object

Handle special cases of missing converters, writers, and flushing attribute setters.



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/pippa.rb', line 306

def method_missing(sym, *args, &block) # :nodoc:

  # Handle graphic attribute setters. flushing with render first.
  if GRAPHIC_ATTRIBUTE_SETTERS.include?(sym)
    iv_name = "@#{sym.to_s[0..-2]}"
    old_val = instance_variable_get(iv_name)
    return old_val if args[0] == old_val
    render
    return instance_variable_set(iv_name, args[0])
  end

  # Handle to_??? format converters, again flushing with render.
  fmt = conversion_to_format(sym)
  if fmt
    render
    @image.format = fmt
    return @image.to_blob
  end

  # Handle write_??? file writers, again flushing with render
  fmt = writer_to_format(sym)
  if fmt
    render
    @image.format = fmt
    return @image.write(args[0])
  end

  # Punt on everything else.
  super
end

Instance Attribute Details

#dot_kindObject

Dot shape. Either :square (default) or :circle



74
75
76
# File 'lib/pippa.rb', line 74

def dot_kind
  @dot_kind
end

#fillObject (readonly)

Dot fill color



93
94
95
# File 'lib/pippa.rb', line 93

def fill
  @fill
end

#fill_opacityObject (readonly)

Dot fill opacity



99
100
101
# File 'lib/pippa.rb', line 99

def fill_opacity
  @fill_opacity
end

#heightObject (readonly)

Height of the map image in pixels



83
84
85
# File 'lib/pippa.rb', line 83

def height
  @height
end

#imageObject (readonly)

RMagick image for direct manipulation, for example drawing lines and labels



117
118
119
# File 'lib/pippa.rb', line 117

def image
  @image
end

#mergeObject

Boolean saying whether to merge markers to eliminate overlaps during rendering.



77
78
79
# File 'lib/pippa.rb', line 77

def merge
  @merge
end

#point_sizeObject (readonly)

Base size of dot edges in pixels; defaults to 1. Therefore a unit area is one pixel.



87
88
89
# File 'lib/pippa.rb', line 87

def point_size
  @point_size
end

#strokeObject (readonly)

Dot border stroke color name



105
106
107
# File 'lib/pippa.rb', line 105

def stroke
  @stroke
end

#stroke_widthObject (readonly)

Dot border stroke width



111
112
113
# File 'lib/pippa.rb', line 111

def stroke_width
  @stroke_width
end

#widthObject (readonly)

Width of the map image in pixels



80
81
82
# File 'lib/pippa.rb', line 80

def width
  @width
end

Class Method Details

.infoObject

Return global map and projection information from config file. See maps/_info for format. This is not generally very useful.



137
138
139
# File 'lib/pippa.rb', line 137

def self.info # :nodoc:
  @@info ||= info_from_file
end

.write_zipcode_maps(dot_kind = :circle) ⇒ Object

Write the test map produced by zipcode_map as png and jpg files.



356
357
358
359
360
# File 'lib/pippa.rb', line 356

def self.write_zipcode_maps(dot_kind = :circle)
  m = zipcode_map(dot_kind)
  File.open('spec/data/zipcodes.png', 'wb') { |f| f.write(m.to_png) }
  m.write_jpg('spec/data/zipcodes.jpg')
end

.zipcode_map(dot_kind = :circle) ⇒ Object

Make a map showing all the zip codes in the USA with dots of fixed area. Also a couple of additional dots.



339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/pippa.rb', line 339

def self.zipcode_map(dot_kind = :circle)
  generator = Random.new(42) # Force same on every run for testing.
  m = Map.new('USA')
  m.point_size = 1.5
  m.dot_kind = dot_kind
  m.merge = true
  Pippa.zips.each_key.each do |zip|
    m.add_at_zip(zip, 1)
  end
  m.fill = 'red'
  m.fill_opacity = 1
  m.add_at_lat_lon(41, -74, 300) # West Point, NY
  m.add_at_lat_lon(38, -122, 300) # Berkeley, CA
  m
end

Instance Method Details

#add_at_lat_lon(lat, lon, area = 0) ⇒ Object Also known as: add_dot_at_lat_lon

Add a dot on the map at given latitude and longitude with given area.

Attributes

  • lat - Dot latitude

  • lon - Dot longitude

  • area - Optional area, defaults to single pixel

Examples

Make a map and put a dot at West Point, NY.

map = Map.new('USA')
map.add_at_lat_lon(41, -74, 100)
map.write_png('map.png')


217
218
219
# File 'lib/pippa.rb', line 217

def add_at_lat_lon(lat, lon, area = 0)
  add_dot(*lat_lon_to_xy(lat, lon), area)
end

#add_at_zip(zip, area = 0) ⇒ Object Also known as: add_dot_at_zip

Add a dot on the map at given 5-digit zip code.

Attributes

  • zip - Zipcode

  • area - Optional area, defaults to single pixel

Examples

Make a map and put a dot at West Point, NY.

map = Map.new('USA')
map.add_at_zip('10996', 100)
map.write_png('map.png')


236
237
238
239
# File 'lib/pippa.rb', line 236

def add_at_zip(zip, area = 0)
  data = Pippa.zips[zip]
  add_at_lat_lon(data[:lat], data[:lon], area) if data
end

#add_dot(x, y, area = 0) ⇒ Object

Add a dot of given area at the given pixel coordinates.

Attributes

  • x - Dot x-pixel coordinate

  • y - Dot y-pixel coordinate

  • area - Optional area, defaults to single pixel

Examples

Make a map and put a dot in the middle.

map = Map.new('USA')
map.add_dot(map.width/2, map.height/2, 100)
map.write_png('map.png')


180
181
182
# File 'lib/pippa.rb', line 180

def add_dot(x, y, area = 0)
  @dots << [x, y, area]
end

#anti_alias=(val) ⇒ Object

Render if we’re making a change and then set a flag indicating whether anti-aliasing will be performed in next render. Default is false. We don’t handle this in method_missing because we need to detect boolean equivalence, not equality.



123
124
125
126
127
128
# File 'lib/pippa.rb', line 123

def anti_alias=(val) # :nodoc:
  val = !!val
  return val if val == @anti_alias
  render
  @anti_alias = val
end

#anti_alias?Boolean

Return flag indicating whether anti-aliasing will be performed in next render.

Returns:

  • (Boolean)


131
132
133
# File 'lib/pippa.rb', line 131

def anti_alias? # :nodoc:
  @anti_alias
end

#lat_lon_to_xy(lat, lon) ⇒ Object

Return the pixel-xy coordinate on this map of a given latitude and longitude.

Attributes

  • lat - Given latitude

  • lon - Given longitude

Examples

Get the pixel coordinate of West Point, NY.

map = Map.new('USA')
x, y = map.lat_lon_to_xy(41, -74)


197
198
199
200
# File 'lib/pippa.rb', line 197

def lat_lon_to_xy(lat, lon)
  set_projection unless @lat_lon_to_xy
  @lat_lon_to_xy.call(lat, lon)
end

#merged_dotsObject



244
245
246
247
248
249
250
251
# File 'lib/pippa.rb', line 244

def merged_dots
  require 'lulu'
  list = Lulu::MarkerList.new
  list.set_info(@dot_kind, @point_size)
  @dots.each {|dot| list.add(*dot) }
  list.merge
  list.markers
end

#renderObject

Force rendering of all dots added so far onto the map. Then forget them so they’re never rendered again.



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/pippa.rb', line 255

def render
  return if @image.nil? || @dots.empty?
  if @merge
    @dots = merged_dots
  else
    @dots.sort! {|a, b| b[2] <=> a[2] } # by area, smallest last
  end
  gc = new_gc
  @dots.each do |x, y, area|
    diam = @point_size * Math.sqrt(area)
    diam *= TWO_SQRT_1_PI if @dot_kind == :circle
    x, y, diam = x.round, y.round, diam.round unless @anti_alias
    if diam <= 1
      gc.point(x, y)
    else
      if @dot_kind == :circle
        gc.circle(x, y, x + diam / 2, y)
      else
        h = diam / 2
        x1 = x - h
        y1 = y - h
        gc.rectangle(x1, y1, x1 + diam, y1 + diam)
      end
    end
  end
  gc.draw(@image)
  @dots = []
end

#respond_to?(sym, include_private = false) ⇒ Boolean

Return true iff we respond to given method. Takes care of to_??? and write_???? converters and writers of graphic formats.

Returns:

  • (Boolean)


286
287
288
# File 'lib/pippa.rb', line 286

def respond_to? (sym, include_private = false)
  conversion_to_format(sym) || writer_to_format(sym) ? true : super
end