Added a new bar graph, that counts how many points the borda system of
[selectricity-live] / vendor / plugins / gruff / lib / gruff / scene.rb
1
2 require "observer"
3 require File.dirname(__FILE__) + '/base'
4
5 ##
6 # A scene is a non-linear graph that assembles layers together to tell a story.
7 # Layers are folders with appropriately named files (see below). You can group 
8 # layers and control them together or just set their values individually.
9 #
10 # Examples:
11 #
12 # * A city scene that changes with the time of day and the weather conditions.
13 # * A traffic map that shows red lines on streets that are crowded and green on free-flowing ones.
14 #
15 # Usage:
16
17 #  g = Gruff::Scene.new("500x100", "artwork/city_scene")
18 #  g.layers = %w(background haze sky clouds)
19 #  g.weather_group = %w(clouds)
20 #  g.time_group = %w(background sky)
21 #  g.weather = "cloudy"
22 #  g.time = Time.now
23 #  g.haze = true
24 #  g.write "hazy_daytime_city_scene.png"
25 #
26 #
27 #
28 # If there is a file named 'default.png', it will be selected (unless other values are provided to override it).
29 #
30 class Gruff::Scene < Gruff::Base
31     
32   # An array listing the foldernames that will be rendered, from back to front.
33   #
34   #  g.layers = %w(sky clouds buildings street people)
35   #
36   attr_reader :layers
37
38   def initialize(target_width, base_dir)
39     @base_dir = base_dir
40     @groups = {}
41     @layers = []    
42     super target_width
43   end
44
45   def draw
46     # Join all the custom paths and filter out the empty ones
47     image_paths = @layers.map { |layer| layer.path }.select { |path| !path.empty? }
48     images = Magick::ImageList.new(*image_paths)
49     @base_image = images.flatten_images
50   end
51
52   def layers=(ordered_list)
53     ordered_list.each do |layer_name|
54       @layers << Gruff::Layer.new(@base_dir, layer_name)
55     end
56   end
57
58   # Group layers to input values
59   #
60   #  g.weather_group = ["sky", "sea", "clouds"]
61   #
62   # Set input values
63   #
64   #  g.weather = "cloudy"
65   #
66   def method_missing(method_name, *args)
67     case method_name.to_s
68     when /^(\w+)_group=$/
69       add_group $1, *args
70       return
71     when /^(\w+)=$/
72       set_input $1, args.first
73       return
74     end
75     super
76   end
77
78 private
79
80   def add_group(input_name, layer_names)
81     @groups[input_name] = Gruff::Group.new(input_name, @layers.select { |layer| layer_names.include?(layer.name) })
82   end
83
84   def set_input(input_name, input_value)
85     if not @groups[input_name].nil?
86       @groups[input_name].send_updates(input_value)
87     else
88       if chosen_layer = @layers.detect { |layer| layer.name == input_name }
89         chosen_layer.update input_value
90       end
91     end
92   end
93   
94 end
95
96
97 class Gruff::Group
98
99   include Observable
100   attr_reader :name
101
102   def initialize(folder_name, layers)
103     @name = folder_name
104     layers.each do |layer|
105       layer.observe self
106     end
107   end
108   
109   def send_updates(value)
110     changed
111     notify_observers value
112   end
113   
114 end
115
116
117 class Gruff::Layer
118   
119   attr_reader :name
120   
121   def initialize(base_dir, folder_name)
122     @base_dir = base_dir.to_s
123     @name = folder_name.to_s
124     @filenames = Dir.open(File.join(base_dir, folder_name)).entries.select { |file| file =~ /^[^.]+\.png$/ }
125     @selected_filename = select_default
126   end
127   
128   # Register this layer so it receives updates from the group
129   def observe(obj)
130     obj.add_observer self
131   end
132   
133   # Choose the appropriate filename for this layer, based on the input
134   def update(value)
135     @selected_filename =  case value.to_s
136                           when /^(true|false)$/
137                             select_boolean value
138                           when /^(\w|\s)+$/
139                             select_string value
140                           when /^-?(\d+\.)?\d+$/
141                             select_numeric value
142                           when /(\d\d):(\d\d):\d\d/
143                             select_time "#{$1}#{$2}"
144                           else
145                             select_default
146                           end
147     # Finally, try to use 'default' if we're still blank
148     @selected_filename ||= select_default
149   end
150
151   # Returns the full path to the selected image, or a blank string
152   def path
153     unless @selected_filename.nil? || @selected_filename.empty?
154       return File.join(@base_dir, @name, @selected_filename)
155     end
156     ''
157   end
158
159 private
160
161   # Match "true.png" or "false.png"
162   def select_boolean(value)
163     file_exists_or_blank value.to_s
164   end
165
166   # Match -5 to _5.png
167   def select_numeric(value)
168     file_exists_or_blank value.to_s.gsub('-', '_')
169   end
170   
171   def select_time(value)
172     times = @filenames.map { |filename| filename.gsub('.png', '') }
173     times.each_with_index do |time, index|
174       if (time > value) && (index > 0)
175         return "#{times[index - 1]}.png"
176       end
177     end
178     return "#{times.last}.png"
179   end
180   
181   # Match "partly cloudy" to "partly_cloudy.png"
182   def select_string(value)
183     file_exists_or_blank value.to_s.gsub(' ', '_')
184   end
185   
186   def select_default
187     @filenames.include?("default.png") ? "default.png" : ''
188   end
189
190   # Returns the string "#{filename}.png", if it exists.
191   #
192   # Failing that, it returns default.png, or '' if that doesn't exist.
193   def file_exists_or_blank(filename)
194     @filenames.include?("#{filename}.png") ? "#{filename}.png" : select_default
195   end
196   
197 end

Benjamin Mako Hill || Want to submit a patch?