~ubuntu-branches/ubuntu/oneiric/ctioga2/oneiric

« back to all changes in this revision

Viewing changes to lib/ctioga2/graphics/styles/factory.rb

  • Committer: Bazaar Package Importer
  • Author(s): Vincent Fourmond
  • Date: 2011-01-24 21:36:06 UTC
  • Revision ID: james.westby@ubuntu.com-20110124213606-9ettx0ugl83z0bzp
Tags: upstream-0.1
ImportĀ upstreamĀ versionĀ 0.1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# factory.rb: an object in charge of generating the style for Curves
 
2
# copyright (c) 2009 by Vincent Fourmond
 
3
 
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (at your option) any later version.
 
8
 
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details (in the COPYING file).
 
13
 
 
14
require 'ctioga2/utils'
 
15
require 'ctioga2/log'
 
16
 
 
17
# This module contains all the classes used by ctioga
 
18
module CTioga2
 
19
 
 
20
  Version::register_svn_info('$Revision: 217 $', '$Date: 2010-12-31 16:18:20 +0100 (Fri, 31 Dec 2010) $')
 
21
 
 
22
  module Graphics
 
23
 
 
24
    module Styles
 
25
 
 
26
      # This object is in charge of the generation of the CurveStyle
 
27
      # object for the next curve to be drawn.
 
28
      class CurveStyleFactory
 
29
 
 
30
        include Log
 
31
 
 
32
        # A private class that defines a parameter for the Factory
 
33
        class CurveStyleFactoryParameter
 
34
 
 
35
          # The code-like name of the parameter
 
36
          attr_accessor :name
 
37
 
 
38
          # The Commands::CommandType of the parameter
 
39
          attr_accessor :type
 
40
          
 
41
          # The pre-defined sets available to use with that
 
42
          # parameter. It is a hash. 
 
43
          attr_accessor :sets
 
44
 
 
45
          # The description of the parameter.
 
46
          attr_accessor :description
 
47
          
 
48
          # The short option for setting the parameter directly from
 
49
          # the command-line.
 
50
          attr_accessor :short_option
 
51
 
 
52
          # The MetaBuilder::Type object that can convert a String to
 
53
          # an Array suitable for use with CircularArray.
 
54
          attr_accessor :sets_type
 
55
 
 
56
          # If this attribute is on, then CurveStyleFactory will not
 
57
          # generate commands for this parameter, only the option.
 
58
          attr_accessor :disable_commands
 
59
          
 
60
 
 
61
          # Creates a new CurveStyleFactoryParameter object.
 
62
          def initialize(name, type, sets, description, 
 
63
                         short_option = nil, disable_cmds = false)
 
64
            @name = name
 
65
            @type = Commands::CommandType.get_type(type)
 
66
            @sets = sets
 
67
            @description = description
 
68
            @short_option = short_option
 
69
            @disable_commands = disable_cmds
 
70
            
 
71
            ## \todo it is not very satisfying to mix CommandTypes and
 
72
            # MetaBuilder::Type on the same level.
 
73
            if @sets
 
74
              @sets_type = 
 
75
                MetaBuilder::Type.get_type({
 
76
                                             :type => :set,
 
77
                                             :subtype => @type.type,
 
78
                                             :shortcuts => @sets
 
79
                                           })
 
80
            end
 
81
          end
 
82
 
 
83
          # Returns a suitable default set for the given object.
 
84
          def default_set
 
85
            return nil unless @sets
 
86
            if @sets.key? 'default'
 
87
              return @sets['default']
 
88
            else
 
89
              @sets.each do |k,v|
 
90
                return v
 
91
              end
 
92
            end
 
93
          end
 
94
 
 
95
        end
 
96
 
 
97
        # Switch some parameter back to automatic
 
98
        AutoRE = /auto/i
 
99
 
 
100
        # Sets some parameter to _false_.
 
101
        DisableRE = /no(ne)?|off/i
 
102
 
 
103
        # If that matches, we use the value as a link to other values.
 
104
        LinkRE = /(?:=|->)(\S+)/
 
105
 
 
106
 
 
107
        # Creates a new parameter for the style factory.
 
108
        #
 
109
        # \todo add a way to add some more text to the description;
 
110
        # possibly a self.describe_parameter function ?
 
111
        def self.define_parameter(target, name, type, sets, description, 
 
112
                                  short_option = nil, disable_cmds = false)
 
113
          # We define two new types:
 
114
          # - first, the color-or-auto type:
 
115
          base_type = Commands::CommandType.get_type(type)
 
116
 
 
117
          if ! Commands::Interpreter.type("#{base_type.name}-or-auto")
 
118
            mb_type = base_type.type.dup
 
119
            mb_type.re_shortcuts = (mb_type.re_shortcuts ? 
 
120
                                        mb_type.re_shortcuts.dup : {}) 
 
121
            
 
122
            mb_type.re_shortcuts[AutoRE] = 'auto'
 
123
            mb_type.re_shortcuts[DisableRE] = false
 
124
 
 
125
            # Add passthrough for expressions such as =color...
 
126
            mb_type.passthrough = LinkRE
 
127
 
 
128
            # Now, register a type for the type or automatic.
 
129
            CmdType.new("#{base_type.name}-or-auto", mb_type,
 
130
                        "Same thing as {type:#{base_type.name}}, or @auto@ to let the style factory handle automatically.")
 
131
 
 
132
          end
 
133
 
 
134
          if sets and ! Commands::Interpreter.type("#{base_type.name}-set")
 
135
            # Now, register a type for the type or automatic.
 
136
            CmdType.new("#{base_type.name}-set",{
 
137
                          :type => :set,
 
138
                          :subtype => base_type.type,
 
139
                          :shortcuts => sets
 
140
                        } ,
 
141
                        "Sets of {type: #{base_type.name}}")
 
142
          end
 
143
          param = 
 
144
            CurveStyleFactoryParameter.new(name, type, sets, 
 
145
                                           description, short_option, 
 
146
                                           disable_cmds)
 
147
          @parameters ||= {}
 
148
          @parameters[target] = param
 
149
 
 
150
          @name_to_target ||= {}
 
151
          @name_to_target[name] = target
 
152
        end
 
153
 
 
154
        # Returns the Hash containing the class parameters.
 
155
        def self.parameters
 
156
          return @parameters || {}
 
157
        end
 
158
 
 
159
 
 
160
        # Returns the Hash containing the class parameters.
 
161
        def self.name_to_target
 
162
          return @name_to_target
 
163
        end
 
164
 
 
165
        # The CmdGroup for stylistic information about
 
166
        # curves.
 
167
        CurveStyleGroup = 
 
168
          CmdGroup.new('curve-style', "Curves styles", 
 
169
                       "Set stylistic details of curves or other object drawn from data", 1)
 
170
 
 
171
 
 
172
        # Creates two commands for each parameter of the object:
 
173
        # * a command to set the override
 
174
        # * a command to choose the sets.
 
175
        def self.create_commands
 
176
          parameters.each do |target, param|
 
177
            next if param.disable_commands
 
178
            override_cmd = 
 
179
              Cmd.new("#{param.name}",
 
180
                      param.short_option,
 
181
                      "--#{param.name}", 
 
182
                      [
 
183
                       CmdArg.new("#{param.type.name}-or-auto") 
 
184
                      ], {},
 
185
                      "Sets the #{param.description} for subsequent curves",
 
186
                      "Sets the #{param.description} for subsequent curves, until cancelled with @auto@ as argument.", CurveStyleGroup) do |plotmaker, value|
 
187
              plotmaker.curve_generator.style_factory.
 
188
                set_parameter_override(target, value)
 
189
            end
 
190
 
 
191
            if param.sets
 
192
              next if param.disable_commands
 
193
              set_cmd = 
 
194
                Cmd.new("#{param.name}-set",
 
195
                        nil,
 
196
                        "--#{param.name}-set", 
 
197
                        [
 
198
                         CmdArg.new("#{param.type.name}-set")
 
199
                        ], {},
 
200
                        "Chooses a set for the #{param.description} of subsequent curves",
 
201
                        "Chooses a set for the #{param.description} of subsequent curves. Also sets {command: #{param.name}} to @auto@, so that the set takes effect immediately", 
 
202
                        CurveStyleGroup) do |plotmaker, value|
 
203
                plotmaker.curve_generator.style_factory.
 
204
                  set_parameter_set(target, value)
 
205
                plotmaker.curve_generator.style_factory.
 
206
                  set_parameter_override(target, 'auto')
 
207
              end
 
208
            end
 
209
          end
 
210
        end
 
211
 
 
212
        # This function returns a hash suitable for use with the plot
 
213
        # command as optional arguments, that will end up as the
 
214
        # _one_time_ hash in #next.
 
215
        def self.plot_optional_arguments
 
216
          args = {}
 
217
          for option_name, param in @parameters
 
218
            args[param.name] = 
 
219
              CmdArg.new(param.type)
 
220
          end
 
221
 
 
222
          # Here, we add the support for a /legend= option
 
223
          args['legend'] = CmdArg.new('text')
 
224
          @name_to_target['legend'] = 'legend'
 
225
 
 
226
          return args
 
227
        end
 
228
 
 
229
 
 
230
        # A hash containing values that override default ones derived
 
231
        # from the CircularArray objects.
 
232
        attr_accessor :override_parameters
 
233
 
 
234
        # A hash of CircularArray objects.
 
235
        attr_accessor :parameter_carrays
 
236
 
 
237
 
 
238
        # Creates a new CurveStyleFactory.
 
239
        def initialize
 
240
          # Overrides as in the first ctioga
 
241
          @override_parameters = {
 
242
            'line_style' => LineStyles::Solid,
 
243
            'marker_marker' => false,
 
244
            'marker_scale' => 0.5 
 
245
          }
 
246
          @parameters_carrays = {}
 
247
          for target, param in self.class.parameters
 
248
            set = param.default_set
 
249
            if set
 
250
              @parameters_carrays[target] = CircularArray.new(set)
 
251
            end
 
252
          end
 
253
        end
 
254
 
 
255
        # Gets the style for the next curve. The _one_time_ hash
 
256
        # contains values 'parameter name' (name, and not target) =>
 
257
        # value that are used for this time only.
 
258
        def next(one_time = {})
 
259
          base = {}
 
260
          for target, array in @parameters_carrays
 
261
            base[target] = array.next
 
262
          end
 
263
          base.merge!(@override_parameters)
 
264
          base.merge!(hash_name_to_target(one_time))
 
265
          return CurveStyle.from_hash(resolve_links(base))
 
266
        end
 
267
 
 
268
 
 
269
 
 
270
        # Sets the override for the given parameter. This corresponds
 
271
        # to fixing manually the corresponding element until the
 
272
        # override is removed, by a call with a _value_ that matches
 
273
        # AutoRE.
 
274
        #
 
275
        # The _value_ should ideally be a String that is further
 
276
        # converted to the appropriate type. Non-string objects will
 
277
        # be left untouched.
 
278
        def set_parameter_override(target, value)
 
279
          param = get_parameter(target)
 
280
          # Perform automatic type conversion only on strings.
 
281
          if value.is_a? String 
 
282
            if value =~ AutoRE
 
283
              @override_parameters.delete(target)
 
284
              return
 
285
            elsif value =~ LinkRE
 
286
              t = $1
 
287
              convert = self.class.name_to_target
 
288
              if convert.key?(t)
 
289
                value = "=#{convert[t]}".to_sym
 
290
              else
 
291
                warn { "No known key: #{t}, treating as auto" }
 
292
                @override_parameters.delete(target)
 
293
                return
 
294
              end
 
295
 
 
296
            elsif value =~ DisableRE
 
297
              value = false
 
298
            else
 
299
              value = param.type.string_to_type(value)
 
300
            end
 
301
          end
 
302
 
 
303
          @override_parameters[target] = value
 
304
        end
 
305
 
 
306
        # Sets the CircularArray set corresponding to the named
 
307
        def set_parameter_set(target, value)
 
308
          param = get_parameter(target)
 
309
          # Perform automatic type conversion only on strings.
 
310
          if value.is_a? String 
 
311
            value = param.sets_type.string_to_type(value)
 
312
          end
 
313
          @parameters_carrays[target].set = value
 
314
        end
 
315
 
 
316
        # Now, the parameters:
 
317
 
 
318
        # Lines:
 
319
        define_parameter 'line_color', 'color', 'color',
 
320
        Sets::ColorSets, "color", "-c"
 
321
 
 
322
        define_parameter 'line_width', 'line-width', 'float',
 
323
        Sets::LineWidthSets, "line width", nil
 
324
 
 
325
        define_parameter 'line_style', 'line-style', 'line-style',
 
326
        Sets::LineStyleSets, "line style", nil
 
327
 
 
328
        # Markers
 
329
        define_parameter 'marker_marker', 'marker', 'marker',
 
330
        Sets::MarkerSets, "marker", '-m'
 
331
 
 
332
        define_parameter 'marker_color', 'marker-color', 'color',
 
333
        Sets::ColorSets, "marker color", nil
 
334
 
 
335
        define_parameter 'marker_scale', 'marker-scale', 'float',
 
336
        Sets::LineWidthSets, "marker scale", nil
 
337
 
 
338
        # Error bars:
 
339
        define_parameter 'error_bar_color', 'error-bar-color', 'color',
 
340
        Sets::ColorSets, "error bar color", nil
 
341
 
 
342
        # Location:
 
343
        define_parameter 'location_xaxis', 'xaxis', 'axis',
 
344
        nil, "X axis", nil, true
 
345
 
 
346
        define_parameter 'location_yaxis', 'yaxis', 'axis',
 
347
        nil, "Y axis", nil, true
 
348
 
 
349
        # Now, fill style
 
350
        define_parameter 'fill_y0', 'fill', 'fill-until',
 
351
        {}, "Fill until", nil
 
352
 
 
353
        define_parameter 'fill_color', 'fill-color', 'color',
 
354
        Sets::ColorSets, "fill color", nil
 
355
 
 
356
        define_parameter 'fill_transparency', 'fill-transparency', 'float',
 
357
        {}, "Fill transparency", nil
 
358
 
 
359
        # Region handling
 
360
        define_parameter 'region_position', 'region-side', 'region-side',
 
361
        {"default" => [:above, :below]}, "region side", nil
 
362
 
 
363
 
 
364
        define_parameter 'style', 'style', 'text',
 
365
        {}, "Path style", nil
 
366
 
 
367
        # Only for xyz-maps or xy-parametric
 
368
        define_parameter 'color_map', 'color-map', 'colormap',
 
369
        nil, "Color map", nil
 
370
 
 
371
 
 
372
        define_parameter 'zaxis', 'zaxis', 'text',
 
373
        nil, "Name for the Z axis", nil
 
374
 
 
375
        ## @todo For xy-parametric, there should be a way to specify
 
376
        ## to which z value the maps apply (ie lines = y2, marker =
 
377
        ## y3...). Although for readability, it is probably better
 
378
        ## to avoid that...
 
379
        define_parameter 'marker_color_map', 'marker-color-map', 'colormap',
 
380
        nil, "Marker color map", nil
 
381
 
 
382
        define_parameter 'split_on_nan', 'split-on-nan', 'boolean',
 
383
        nil, "Split on NaN", nil
 
384
 
 
385
 
 
386
        # And finally, we register all necessary commands...
 
387
        create_commands
 
388
 
 
389
        # A constant suitable for use as the optional arguments of the
 
390
        # plot command.
 
391
        PlotCommandOptions = plot_optional_arguments
 
392
 
 
393
        protected
 
394
 
 
395
        # Returns the CurveFactoryParameterType object corresponding
 
396
        # to the named parameter.
 
397
        def get_parameter(target)
 
398
          if ! parameters.key? target
 
399
            raise "Unkown parameter: #{target}"
 
400
          else
 
401
            return parameters[target]
 
402
          end
 
403
        end
 
404
 
 
405
        # Returns the class parameters hash
 
406
        def parameters
 
407
          return self.class.parameters
 
408
        end
 
409
 
 
410
        # Converts the one-time parameters, which is a hash whose keys
 
411
        # are the names of the parameters to targets.
 
412
        def hash_name_to_target(h)
 
413
          retval = {}
 
414
          convert = self.class.name_to_target
 
415
          for k,v in h
 
416
            if convert.key? k 
 
417
              retval[convert[k]] = v
 
418
            else
 
419
              warn { "Unkown key for hash_name_to_target: #{k}" }
 
420
            end
 
421
          end
 
422
          return retval
 
423
        end
 
424
 
 
425
        # Resolve potential links in the form of :=stuff within the
 
426
        # given hash, and returns a new version of the hash.
 
427
        #
 
428
        # \warning the _h_ parameter is completely destroyed in the
 
429
        # process
 
430
        def resolve_links(h)
 
431
          tv = {}
 
432
 
 
433
          # First, copy plain values
 
434
          for k,v in h
 
435
            if v.is_a?(Symbol) && v.to_s =~ /^(=|->)/
 
436
              # We keep for later
 
437
            else
 
438
              tv[k] = v
 
439
              h.delete(k)
 
440
            end
 
441
          end
 
442
          
 
443
          # Now, we will iterate over the remaining things; we will
 
444
          # stop with an error if the number of remaining keys does
 
445
          # not decrease after one step
 
446
          while h.size > 0
 
447
            pre_size = h.size
 
448
            for k,v in h
 
449
              v.to_s =~ /^(?:=|->)(\S+)/
 
450
              target = $1
 
451
              if tv.key? target
 
452
                tv[k] = tv[target]
 
453
                h.delete(k)
 
454
              end
 
455
            end
 
456
            if h.size >= pre_size
 
457
              raise "Error: infinite recursion loop while gathering styles"
 
458
            end
 
459
          end
 
460
          
 
461
          return tv
 
462
        end
 
463
      end
 
464
    end
 
465
  end
 
466
end
 
467