ctioga 1.11.1
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +340 -0
- data/ctioga/bin/ctable +28 -0
- data/ctioga/bin/ctioga +37 -0
- data/ctioga/doc/ctable.1 +156 -0
- data/ctioga/doc/ctioga.1 +2363 -0
- data/ctioga/examples/README +46 -0
- data/ctioga/examples/ctioga.gnuplot +4 -0
- data/ctioga/examples/ctioga_within_tioga.rb +53 -0
- data/ctioga/examples/ctiogarc.rb +24 -0
- data/ctioga/examples/include_1.rb +15 -0
- data/ctioga/examples/noise.dat +100 -0
- data/ctioga/examples/noise.rb +13 -0
- data/ctioga/examples/trig.csv +100 -0
- data/ctioga/examples/trig.dat +100 -0
- data/ctioga/examples/trig.rb +14 -0
- data/ctioga/examples/trigh.dat +100 -0
- data/ctioga/examples/trigh.rb +10 -0
- data/ctioga/examples/tutorial +763 -0
- data/ctioga/examples/tutorial.sh +269 -0
- data/ctioga/tests/README +14 -0
- data/ctioga/tests/axes.sh +40 -0
- data/ctioga/tests/basic.sh +11 -0
- data/ctioga/tests/draw.sh +24 -0
- data/ctioga/tests/histograms.sh +14 -0
- data/ctioga/tests/insets.sh +41 -0
- data/ctioga/tests/layouts.sh +29 -0
- data/ctioga/tests/legends.sh +113 -0
- data/ctioga/tests/styles.sh +43 -0
- data/ctioga/tests/test_style.sh +8 -0
- data/ctioga/tests/tests.sh +24 -0
- data/ctioga/tests/text_backend.sh +83 -0
- data/ctioga/tests/tioga_defaults.rb +18 -0
- data/lib/CTioga/axes.rb +904 -0
- data/lib/CTioga/backends.rb +88 -0
- data/lib/CTioga/boundaries.rb +224 -0
- data/lib/CTioga/ctable.rb +134 -0
- data/lib/CTioga/curve_style.rb +246 -0
- data/lib/CTioga/debug.rb +199 -0
- data/lib/CTioga/dimension.rb +133 -0
- data/lib/CTioga/elements.rb +17 -0
- data/lib/CTioga/elements/base.rb +84 -0
- data/lib/CTioga/elements/containers.rb +578 -0
- data/lib/CTioga/elements/curves.rb +368 -0
- data/lib/CTioga/elements/tioga_primitives.rb +440 -0
- data/lib/CTioga/layout.rb +595 -0
- data/lib/CTioga/legends.rb +29 -0
- data/lib/CTioga/legends/cmdline.rb +187 -0
- data/lib/CTioga/legends/item.rb +164 -0
- data/lib/CTioga/legends/style.rb +257 -0
- data/lib/CTioga/log.rb +73 -0
- data/lib/CTioga/movingarrays.rb +131 -0
- data/lib/CTioga/partition.rb +271 -0
- data/lib/CTioga/plot_style.rb +230 -0
- data/lib/CTioga/plotmaker.rb +1677 -0
- data/lib/CTioga/shortcuts.rb +69 -0
- data/lib/CTioga/structures.rb +82 -0
- data/lib/CTioga/styles.rb +140 -0
- data/lib/CTioga/themes.rb +581 -0
- data/lib/CTioga/themes/classical.rb +82 -0
- data/lib/CTioga/themes/demo.rb +63 -0
- data/lib/CTioga/themes/fits.rb +91 -0
- data/lib/CTioga/themes/mono.rb +33 -0
- data/lib/CTioga/tioga.rb +32 -0
- data/lib/CTioga/utils.rb +173 -0
- data/lib/MetaBuilder/Parameters/dates.rb +38 -0
- data/lib/MetaBuilder/Parameters/lists.rb +132 -0
- data/lib/MetaBuilder/Parameters/numbers.rb +69 -0
- data/lib/MetaBuilder/Parameters/strings.rb +86 -0
- data/lib/MetaBuilder/Parameters/styles.rb +75 -0
- data/lib/MetaBuilder/Qt4/Parameters/dates.rb +51 -0
- data/lib/MetaBuilder/Qt4/Parameters/numbers.rb +65 -0
- data/lib/MetaBuilder/Qt4/Parameters/strings.rb +106 -0
- data/lib/MetaBuilder/Qt4/parameter.rb +172 -0
- data/lib/MetaBuilder/Qt4/parameters.rb +9 -0
- data/lib/MetaBuilder/descriptions.rb +603 -0
- data/lib/MetaBuilder/factory.rb +101 -0
- data/lib/MetaBuilder/group.rb +57 -0
- data/lib/MetaBuilder/metabuilder.rb +10 -0
- data/lib/MetaBuilder/parameter.rb +374 -0
- data/lib/MetaBuilder/parameters.rb +11 -0
- data/lib/MetaBuilder/qt4.rb +8 -0
- data/lib/SciYAG/Backends/backend.rb +379 -0
- data/lib/SciYAG/Backends/binner.rb +168 -0
- data/lib/SciYAG/Backends/cache.rb +102 -0
- data/lib/SciYAG/Backends/dataset.rb +158 -0
- data/lib/SciYAG/Backends/descriptions.rb +469 -0
- data/lib/SciYAG/Backends/filters.rb +25 -0
- data/lib/SciYAG/Backends/filters/average.rb +134 -0
- data/lib/SciYAG/Backends/filters/cumulate.rb +37 -0
- data/lib/SciYAG/Backends/filters/filter.rb +70 -0
- data/lib/SciYAG/Backends/filters/norm.rb +39 -0
- data/lib/SciYAG/Backends/filters/smooth.rb +63 -0
- data/lib/SciYAG/Backends/filters/sort.rb +43 -0
- data/lib/SciYAG/Backends/filters/strip.rb +34 -0
- data/lib/SciYAG/Backends/filters/trim.rb +64 -0
- data/lib/SciYAG/Backends/gnuplot.rb +131 -0
- data/lib/SciYAG/Backends/math.rb +108 -0
- data/lib/SciYAG/Backends/mdb.rb +462 -0
- data/lib/SciYAG/Backends/multitext.rb +96 -0
- data/lib/SciYAG/Backends/source.rb +64 -0
- data/lib/SciYAG/Backends/text.rb +339 -0
- data/lib/SciYAG/backends.rb +16 -0
- metadata +191 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
# numbers.rb : Qt methods for numbers
|
2
|
+
# Copyright (C) 2006 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.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program; if not, write to the Free Software
|
16
|
+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
17
|
+
|
18
|
+
|
19
|
+
module MetaBuilder
|
20
|
+
|
21
|
+
# The module ParameterTypes should be used for all subclasses of
|
22
|
+
# ParameterType, to keep the place clean and tidy.
|
23
|
+
module ParameterTypes
|
24
|
+
|
25
|
+
# An integer
|
26
|
+
class IntegerParameter
|
27
|
+
|
28
|
+
def qt4_create_input_widget(parent = nil, target = nil, style = nil)
|
29
|
+
return Qt4MB::IntegerInputWidget.new(parent, self)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
module Qt4MB
|
36
|
+
|
37
|
+
# A spinbox to choose Integers.
|
38
|
+
class IntegerInputWidget < Qt::SpinBox
|
39
|
+
|
40
|
+
# Emitted when the value is changed. Connected to
|
41
|
+
# editingFinished().
|
42
|
+
signals 'value_changed()'
|
43
|
+
|
44
|
+
# Creates a Generic widget. +type+ is the ParameterType child
|
45
|
+
# instance for whom we should do conversion.
|
46
|
+
def initialize(parent, type)
|
47
|
+
super(parent)
|
48
|
+
@type = type
|
49
|
+
connect(self, SIGNAL('valueChanged(int)'),
|
50
|
+
SIGNAL('value_changed()'))
|
51
|
+
set_range(-(2**30), 2**30)
|
52
|
+
end
|
53
|
+
|
54
|
+
def text
|
55
|
+
return @type.type_to_string(value)
|
56
|
+
end
|
57
|
+
|
58
|
+
def text=(t)
|
59
|
+
self.value=(@type.string_to_type(t))
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# numbers.rb : Qt methods for strings
|
2
|
+
# Copyright (C) 2006 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.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program; if not, write to the Free Software
|
16
|
+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
17
|
+
|
18
|
+
|
19
|
+
module MetaBuilder
|
20
|
+
|
21
|
+
# The module ParameterTypes should be used for all subclasses of
|
22
|
+
# ParameterType, to keep the place clean and tidy.
|
23
|
+
module ParameterTypes
|
24
|
+
|
25
|
+
# An integer
|
26
|
+
class FileParameter
|
27
|
+
|
28
|
+
def qt4_create_input_widget(parent = nil, target = nil, style = nil)
|
29
|
+
return Qt4MB::FileInputWidget.new(parent, self)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Directly wraps to Qt::FileDialog.get_open_file_name;
|
33
|
+
# the _title_ is therefore ignored...
|
34
|
+
def qt4_get(parent, title, label, default, target = nil)
|
35
|
+
|
36
|
+
file = Qt::FileDialog.get_open_file_name(parent, label,
|
37
|
+
File.dirname(default),
|
38
|
+
filter)
|
39
|
+
if file.nil?
|
40
|
+
raise CancelInput
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
module Qt4MB
|
49
|
+
|
50
|
+
# A widget with a nice Browse box to choose
|
51
|
+
class FileInputWidget < Qt::Widget
|
52
|
+
|
53
|
+
# Emitted when the value is changed. Connected to
|
54
|
+
# editingFinished().
|
55
|
+
signals 'value_changed()'
|
56
|
+
|
57
|
+
# When the button is clicked
|
58
|
+
slots 'button_clicked()'
|
59
|
+
|
60
|
+
# Creates a Generic widget. _type_ is the FileParameter
|
61
|
+
# instance for whom we should do conversion.
|
62
|
+
def initialize(parent, type)
|
63
|
+
super(parent)
|
64
|
+
@type = type
|
65
|
+
@layout = Qt::HBoxLayout.new(self)
|
66
|
+
|
67
|
+
@line_edit = Qt::LineEdit.new
|
68
|
+
@layout.add_widget(@line_edit)
|
69
|
+
connect(@line_edit, SIGNAL('editingFinished()'),
|
70
|
+
SIGNAL('value_changed()'))
|
71
|
+
|
72
|
+
@button = Qt::PushButton.new("Browse")
|
73
|
+
@layout.add_widget(@button)
|
74
|
+
connect(@button, SIGNAL('clicked()'),
|
75
|
+
SLOT('button_clicked()'))
|
76
|
+
end
|
77
|
+
|
78
|
+
def button_clicked
|
79
|
+
file = @type.qt4_get(parent, "dummy",
|
80
|
+
"Select file", text)
|
81
|
+
self.text=file
|
82
|
+
rescue
|
83
|
+
# Nothing, just don't set the current value
|
84
|
+
end
|
85
|
+
|
86
|
+
def text
|
87
|
+
return @line_edit.text
|
88
|
+
end
|
89
|
+
|
90
|
+
def text=(t)
|
91
|
+
@line_edit.setText(t)
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
def value
|
96
|
+
return text
|
97
|
+
end
|
98
|
+
|
99
|
+
def value=(v)
|
100
|
+
self.text=v
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# parameter.rb : Qt support for parameters.
|
2
|
+
# Copyright (C) 2006 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.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program; if not, write to the Free Software
|
16
|
+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
module MetaBuilder
|
22
|
+
|
23
|
+
# An exception raised when the user cancels an input dialog box.
|
24
|
+
class CancelInput < Exception
|
25
|
+
end
|
26
|
+
|
27
|
+
class ParameterType
|
28
|
+
|
29
|
+
# Creates a widget suitable for editing the value and returns it.
|
30
|
+
# The widget will be a child of _parent_ (can be nil).
|
31
|
+
# The _target_ is provided for the Parameter system. It should contain
|
32
|
+
# the value of the target whose parameter we should set. This can be
|
33
|
+
# used for instance when the value has dynamic content depending on
|
34
|
+
# some internals of _target_. Reimplementations should not
|
35
|
+
# fail if it is +nil+. The _style_ parameter
|
36
|
+
# is for now a placeholder.
|
37
|
+
#
|
38
|
+
# The created widget must provide the following:
|
39
|
+
# * <tt>text=|text</tt>: a writer|reader pair for the string value;
|
40
|
+
# * <tt>value=|value</tt>: a writer|reader pair for the real value;
|
41
|
+
# * <tt>value_changed()</tt> : a signal for when the value has changed.
|
42
|
+
# should be used in the same way as Qt::LineEdit::editingFinished
|
43
|
+
# in the sense that it should only trigger on complete output.
|
44
|
+
# * <tt>label=</tt>: a way to set the label. Label could be basically
|
45
|
+
# anything accepted by Qt; if it is +nil+ or +false+, the display is
|
46
|
+
# switched off. The label display must be switched off by default.
|
47
|
+
#
|
48
|
+
def qt4_create_input_widget(parent = nil, target = nil, style = nil)
|
49
|
+
return Qt4MB::GenericInputWidget.new(parent, self)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Pops up a Qt4 dialog box to ask for the value of the parameter.
|
53
|
+
# * _parent_ is the parent widget;
|
54
|
+
# * _title_ is the title of the Dialog Box
|
55
|
+
# * _label_ is the label of the widget used for querying
|
56
|
+
# * _default_ is the default value (of the appropriate type).
|
57
|
+
# * _target_ is the target for the parameters system. Defaults
|
58
|
+
# to +nil+.
|
59
|
+
#
|
60
|
+
# Returns directly a value in the right type. The default
|
61
|
+
# implementation uses the #qt4_create_input_widget function.
|
62
|
+
# As +false+ or +nil+ are appropriate return values in some
|
63
|
+
# cases, #qt4_get raises a CancelInput exception. You *must*
|
64
|
+
# handle it if you don't want nasty surprises...
|
65
|
+
|
66
|
+
def qt4_get(parent, title, label, default, target = nil)
|
67
|
+
return Qt4MB::GenericDialog.get_type(parent, self,
|
68
|
+
title, label, default, target)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Converts from the type to a QVariant
|
72
|
+
def type_to_variant(value)
|
73
|
+
return Qt::Variant.new(value)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Converts from a QVariant to the appropriate (Ruby) type.
|
77
|
+
def variant_to_type(variant)
|
78
|
+
return variant.value
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
class Parameter
|
84
|
+
end
|
85
|
+
|
86
|
+
# This module holds all the classes descending from the Qt4 classes.
|
87
|
+
module Qt4MB
|
88
|
+
|
89
|
+
|
90
|
+
# THIS MUST BE REWRITTEN TO COMPLY WITH THE NEW SPECS !!!
|
91
|
+
# A generic input for the types. Based on Qt::LineEdit.
|
92
|
+
class GenericInputWidget < Qt::LineEdit
|
93
|
+
|
94
|
+
# Emitted when the value is changed. Connected to
|
95
|
+
# editingFinished().
|
96
|
+
signals 'value_changed()'
|
97
|
+
|
98
|
+
# Creates a Generic widget. _type_ is the ParameterType child
|
99
|
+
# instance for whom we should do conversion.
|
100
|
+
def initialize(parent, type)
|
101
|
+
super(parent)
|
102
|
+
@type = type
|
103
|
+
connect(self, SIGNAL('editingFinished()'),
|
104
|
+
SIGNAL('value_changed()'))
|
105
|
+
end
|
106
|
+
|
107
|
+
def text=(t)
|
108
|
+
setText(t)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns the value currently displayed
|
112
|
+
def value
|
113
|
+
return @type.string_to_type(text)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Sets the display.
|
117
|
+
def value=(val)
|
118
|
+
self.text = @type.type_to_string(val)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class GenericDialog < Qt::Dialog
|
123
|
+
|
124
|
+
# Creates a generic input dialog for the type _type_.
|
125
|
+
def initialize(parent, type, title, label, val, target)
|
126
|
+
super(parent)
|
127
|
+
self.window_title = title
|
128
|
+
vbox = Qt::VBoxLayout.new(self)
|
129
|
+
|
130
|
+
hbox = Qt::HBoxLayout.new
|
131
|
+
@label = Qt::Label.new(label)
|
132
|
+
hbox.add_widget(@label)
|
133
|
+
@widget = type.qt4_create_input_widget(nil, target)
|
134
|
+
hbox.add_widget(@widget)
|
135
|
+
@widget.value = val
|
136
|
+
vbox.add_layout(hbox)
|
137
|
+
|
138
|
+
hbox = Qt::HBoxLayout.new
|
139
|
+
# OK and Cancel buttons
|
140
|
+
button = Qt::PushButton.new("OK")
|
141
|
+
connect(button, SIGNAL('clicked()'),
|
142
|
+
SLOT('accept()'))
|
143
|
+
hbox.add_widget(button)
|
144
|
+
button = Qt::PushButton.new("Cancel")
|
145
|
+
connect(button, SIGNAL('clicked()'),
|
146
|
+
SLOT('reject()'))
|
147
|
+
hbox.add_widget(button)
|
148
|
+
vbox.add_layout(hbox)
|
149
|
+
# and that's all !
|
150
|
+
end
|
151
|
+
|
152
|
+
def exec
|
153
|
+
res = super
|
154
|
+
if res > 0
|
155
|
+
return @widget.value
|
156
|
+
else
|
157
|
+
raise CancelInput, "User cancelled input"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# A convenience wrapper around the functionnality. Creates a
|
162
|
+
# GenericDialog and executes it.
|
163
|
+
def self.get_type(parent, type, title, label, val, target)
|
164
|
+
dialog = new(parent, type, title, label, val, target)
|
165
|
+
return dialog.exec
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# parameters.rb: A file grouping all the require necessary to have a
|
2
|
+
# working parameter system.
|
3
|
+
# This file is copyright 2006 by Vincent Fourmond, but you can do whatever
|
4
|
+
# you want with it.
|
5
|
+
|
6
|
+
require 'MetaBuilder/Qt4/parameter'
|
7
|
+
require 'MetaBuilder/Qt4/Parameters/numbers'
|
8
|
+
require 'MetaBuilder/Qt4/Parameters/strings'
|
9
|
+
require 'MetaBuilder/Qt4/Parameters/dates'
|
@@ -0,0 +1,603 @@
|
|
1
|
+
# descriptions.rb : The basics of the MetaBuilder system
|
2
|
+
# Copyright (C) 2006 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.
|
13
|
+
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program; if not, write to the Free Software
|
16
|
+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
17
|
+
|
18
|
+
module MetaBuilder
|
19
|
+
|
20
|
+
|
21
|
+
# The Description class is a meta-information class that records several
|
22
|
+
# informations about the class:
|
23
|
+
#
|
24
|
+
# * a basic name, code-like, which is used mainly for internal
|
25
|
+
# purposes;
|
26
|
+
# * a long name, more explanatory, in proper English (or any
|
27
|
+
# other language. By the way, it should definitely be
|
28
|
+
# translated in a production environment);
|
29
|
+
# * a description itself, some small text describing the nature
|
30
|
+
# of the class;
|
31
|
+
# * a list of all the Parameters that are important for the class,
|
32
|
+
# and enough to recreate the state of an object.
|
33
|
+
#
|
34
|
+
# This class is fairly general, and can be subclassed to fit specific
|
35
|
+
# needs. An example is in the SciYAG/Backend system, where the description
|
36
|
+
# of a backend is derived from Description.
|
37
|
+
#
|
38
|
+
# To make use of the Description system for a class, use the following:
|
39
|
+
#
|
40
|
+
# class SomeClass
|
41
|
+
# extend MetaBuilder::DescriptionExtend
|
42
|
+
# include MetaBuilder::DescriptionInclude
|
43
|
+
#
|
44
|
+
# describe 'someclass', 'Some nice class', <<EOD
|
45
|
+
# The description of a nice class
|
46
|
+
# EOD
|
47
|
+
#
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# Descriptions can be used in two completely different manners:
|
51
|
+
#
|
52
|
+
# * you can use a single Description to facilitate the interface
|
53
|
+
# with the user to query|save parameters;
|
54
|
+
# * you can use the Description system to describe a whole set of
|
55
|
+
# classes providing similar functions and sharing a base ancestor,
|
56
|
+
# such as series of input|output plugins, database formats,
|
57
|
+
# themes... In this case, the Description system can act as a real
|
58
|
+
# plugin factory, recording every new subclass of the base one
|
59
|
+
# and providing facilities to prompt the user.
|
60
|
+
#
|
61
|
+
# Please note that if you want to use the facilities to dynamically
|
62
|
+
# create objects at run-time, the classes used by describe
|
63
|
+
# *should not need any parameter for #initialize*.
|
64
|
+
#
|
65
|
+
#
|
66
|
+
# Description provides the following facilities:
|
67
|
+
#
|
68
|
+
# * a #save_state system, returning a hash containing the current
|
69
|
+
# parameters of an object; it's state can be returned to using
|
70
|
+
# #restore_state. Please note that the parameters will
|
71
|
+
# be set in the same order as they appear in the class. Moreover,
|
72
|
+
# if you want the #restore_state to work properly, you must ensure
|
73
|
+
# that setting all the parameters restore the state, i.e. you didn't
|
74
|
+
# forget anything.
|
75
|
+
# * facilities to prepare OptionParser to work on a described class (see
|
76
|
+
# #option_parser_fill, #option_parser_banner, #option_parser_options)
|
77
|
+
# * facilities to create widgets, dialog boxes and so on to query and
|
78
|
+
# modify the contents of a described class instance.
|
79
|
+
|
80
|
+
|
81
|
+
class Description
|
82
|
+
# The Class to instantiate.
|
83
|
+
attr_accessor :object_class
|
84
|
+
|
85
|
+
# The name of the class (short, code-like)
|
86
|
+
attr_accessor :name
|
87
|
+
|
88
|
+
# (text) description !
|
89
|
+
attr_accessor :description
|
90
|
+
|
91
|
+
# Long name, the one for public display
|
92
|
+
attr_accessor :long_name
|
93
|
+
|
94
|
+
# The parameter list. The parameters are added when they are found
|
95
|
+
# in the class description, and will be used in the order found
|
96
|
+
# in this list to recreate the state; beware if one parameter is
|
97
|
+
# depending on another one.
|
98
|
+
attr_reader :param_list
|
99
|
+
|
100
|
+
# A hash index on the (short) name and the symbols of the parameters,
|
101
|
+
# for quick access. None of those should overlap.
|
102
|
+
attr_reader :param_hash
|
103
|
+
|
104
|
+
|
105
|
+
# The list of groups
|
106
|
+
attr_accessor :group_list
|
107
|
+
|
108
|
+
# A hash indexing the groups by their name
|
109
|
+
attr_accessor :group_hash
|
110
|
+
|
111
|
+
# Initializes a Description
|
112
|
+
def initialize(cls, name, long_name, description = "")
|
113
|
+
@object_class = cls
|
114
|
+
@name = name
|
115
|
+
@long_name = long_name
|
116
|
+
@description = description
|
117
|
+
@param_list = []
|
118
|
+
@param_hash = {}
|
119
|
+
@init_param_list = []
|
120
|
+
|
121
|
+
@group_list = []
|
122
|
+
@group_hash = {}
|
123
|
+
|
124
|
+
# There is not default group.
|
125
|
+
@current_group = nil
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
# Adds a group to the list, and switches to it
|
130
|
+
def add_group(group)
|
131
|
+
@group_list << group
|
132
|
+
@group_hash[group.name] = group
|
133
|
+
@current_group = group
|
134
|
+
end
|
135
|
+
|
136
|
+
# Switches to the named group
|
137
|
+
def switch_to_group(name)
|
138
|
+
@current_group = @group_hash.fetch(name)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Adds a new parameter to the description
|
142
|
+
def add_param(param)
|
143
|
+
@param_list << param
|
144
|
+
|
145
|
+
# Update the parameter hash, for easy access.
|
146
|
+
@param_hash[param.reader_symbol] = param
|
147
|
+
@param_hash[param.writer_symbol] = param
|
148
|
+
@param_hash[param.name] = param
|
149
|
+
|
150
|
+
# Update the current group, if necessary
|
151
|
+
@current_group.add_parameter(param) unless @current_group.nil?
|
152
|
+
end
|
153
|
+
|
154
|
+
# Small set of functions dealing with OptionParsers
|
155
|
+
|
156
|
+
# Fills an OptionParser with all the parameters the of the class.
|
157
|
+
# _instance_ is the instance of the class we want to
|
158
|
+
# parametrize, _parser_ is an OptionParser, and if _uniquify_ is set,
|
159
|
+
# the description name is prefixed to the parameter name.
|
160
|
+
def option_parser_fill(parser, instance, uniquify = true, groups = false)
|
161
|
+
option_parser_banner(parser, instance)
|
162
|
+
option_parser_options(parser, instance, uniquify, groups)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Fills a parser with options for all parameters. _parser_, _instance_
|
166
|
+
# and _uniquify_ have the same meaning as in #option_parser_option.
|
167
|
+
# If _grouping_ is set to _true_, the options are organized according
|
168
|
+
# to group listing.
|
169
|
+
def option_parser_options(parser, instance, uniquify = true,
|
170
|
+
grouping = false)
|
171
|
+
if grouping
|
172
|
+
for group in @group_list
|
173
|
+
parser.separator group.long_name
|
174
|
+
for param in group.parameter_list
|
175
|
+
option_parser_option(parser, param, instance, uniquify)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
else
|
179
|
+
for param in @param_list
|
180
|
+
option_parser_option(parser, param, instance, uniquify)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Adds an option for the parameter _param_ to the OptionParser _parser_.
|
186
|
+
# The corresponding parameter will be set in _instance_. If _uniquify_
|
187
|
+
# is set, the description name if prefixed to the option.
|
188
|
+
# _param_ can be either a Parameter or a string|symbol registered
|
189
|
+
# in #param_hash.
|
190
|
+
def option_parser_option(parser, param, instance, uniquify = true)
|
191
|
+
raise "The instance is not of the right class" unless
|
192
|
+
instance.is_a? @object_class
|
193
|
+
if not param.is_a?(Parameter)
|
194
|
+
param = param_hash.fetch(param)
|
195
|
+
end
|
196
|
+
if uniquify
|
197
|
+
param_name = "#{@name}-#{param.name}"
|
198
|
+
else
|
199
|
+
param_name = "#{param.name}"
|
200
|
+
end
|
201
|
+
param.option_parser_option(parser, param_name, instance)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Subclass Description and reimplement this method to have your own banner.
|
205
|
+
def banner(instance)
|
206
|
+
return false
|
207
|
+
end
|
208
|
+
|
209
|
+
# Ouputs an OptionParser banner based on the return value
|
210
|
+
# of the #banner method.
|
211
|
+
def option_parser_banner(parser, instance)
|
212
|
+
if b = banner(instance)
|
213
|
+
parser.separator b
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Creates a hash representing the state of an instance of the
|
218
|
+
# described class at a given time. It can be fed to #restore_state
|
219
|
+
# and #recreate_state to restore the state to an already existing
|
220
|
+
# instance or to create a new one from scratch.
|
221
|
+
#
|
222
|
+
# This function saves the raw parameters, as there is not much
|
223
|
+
# interest in converting to String and back from it.
|
224
|
+
#
|
225
|
+
# It saves a duplicate of the values, not the values themselves,
|
226
|
+
# to make sure it really represents the state of the objects at
|
227
|
+
# a given time.
|
228
|
+
def save_state(instance)
|
229
|
+
ret = {}
|
230
|
+
for param in @param_list
|
231
|
+
val = param.get_raw(instance)
|
232
|
+
ret[param.name] = begin # Necessary as some classes can't be duped.
|
233
|
+
val.dup
|
234
|
+
rescue
|
235
|
+
val # Typical case: nil, which still is a
|
236
|
+
# valid value
|
237
|
+
end
|
238
|
+
end
|
239
|
+
return ret
|
240
|
+
end
|
241
|
+
|
242
|
+
# Puts the _target_ into the _state_ returned by #save_state.
|
243
|
+
# The parameters are set in the order in which they are found
|
244
|
+
# in the class declaration.
|
245
|
+
def restore_state(state, target)
|
246
|
+
for param in @param_list
|
247
|
+
if state.has_key?(param.name)
|
248
|
+
param.set_raw(target, state[param.name])
|
249
|
+
end
|
250
|
+
end
|
251
|
+
return target
|
252
|
+
end
|
253
|
+
|
254
|
+
# Returns the default state of an freshly created instance.
|
255
|
+
# This way, you can create a well-defined state of one
|
256
|
+
# instance via parameters set thus:
|
257
|
+
#
|
258
|
+
# state = desc.default_state.update({'param' -> "My Value"})
|
259
|
+
def default_state
|
260
|
+
return save_state(instantiate)
|
261
|
+
end
|
262
|
+
|
263
|
+
# Creates a new (default) instance of the #object_class associated
|
264
|
+
# with this Description. Although this method takes additional
|
265
|
+
# parameters that are fed to the new method of the class, its
|
266
|
+
# use is strongly discouraged.
|
267
|
+
def instantiate(*a)
|
268
|
+
return @object_class.new(*a)
|
269
|
+
end
|
270
|
+
|
271
|
+
# Creates a new instance of the #object_class associated with
|
272
|
+
# this description and fills its state
|
273
|
+
def recreate_state(state)
|
274
|
+
obj = instantiate
|
275
|
+
restore_state(state, obj)
|
276
|
+
return obj
|
277
|
+
end
|
278
|
+
|
279
|
+
end
|
280
|
+
|
281
|
+
# This module should be used in conjunction with DescriptionExtend to
|
282
|
+
# provide Descriptions to a class or a family of classes:
|
283
|
+
#
|
284
|
+
# class NiceClass
|
285
|
+
# extend DescriptionExtend
|
286
|
+
# include DescriptionInclude
|
287
|
+
# # real code now
|
288
|
+
# end
|
289
|
+
#
|
290
|
+
# The DescriptionInclude in itself provide some small facilities: a mehod
|
291
|
+
# to access directly the #description of an object (and its #long_name),
|
292
|
+
# a series of methods
|
293
|
+
# to set|get parameters based on their name (#parameter, #get_param,
|
294
|
+
# #get_param_raw, #set_param, #set_param_raw) and some methods to fill
|
295
|
+
# an OptionParser (#option_parser_fill, #option_parser_options,
|
296
|
+
# #option_parser_banner)
|
297
|
+
|
298
|
+
module DescriptionInclude
|
299
|
+
|
300
|
+
# Returns the description associated with the class.
|
301
|
+
def description
|
302
|
+
return self.class.description
|
303
|
+
end
|
304
|
+
|
305
|
+
|
306
|
+
# Returns the long name of the class
|
307
|
+
def long_name
|
308
|
+
return description.long_name
|
309
|
+
end
|
310
|
+
|
311
|
+
# Small functions to deal with the Parameter objects associated with
|
312
|
+
# the class. They are absolutely not necessary to actually access the
|
313
|
+
# data, but can come in really useful at some points.
|
314
|
+
|
315
|
+
# Returns the Parameter object associated with the named parameter
|
316
|
+
def parameter(param)
|
317
|
+
return description.param_hash.fetch(param)
|
318
|
+
end
|
319
|
+
|
320
|
+
# Query the value of a parameter, returns a String
|
321
|
+
def get_param(p)
|
322
|
+
return parameter(p).get(self)
|
323
|
+
end
|
324
|
+
|
325
|
+
# Query the value of a parameter, returns the actual value
|
326
|
+
def get_param_raw(p)
|
327
|
+
return parameter(p).get_raw(self)
|
328
|
+
end
|
329
|
+
|
330
|
+
# Sets the value of a paramete according to a String
|
331
|
+
def set_param(param, str)
|
332
|
+
return parameter(param).set(self, str)
|
333
|
+
end
|
334
|
+
|
335
|
+
# Sets directly the value of a parameter
|
336
|
+
def set_param_raw(param, val)
|
337
|
+
return parameter(param).set_raw(self, val)
|
338
|
+
end
|
339
|
+
|
340
|
+
# OptionParser related methods.
|
341
|
+
|
342
|
+
# Fills an OptionParser with their parameters. Most probably, the
|
343
|
+
# default implementation should do for most cases. _uniquify_ asks
|
344
|
+
# if we should try to make the command-line options as unique as
|
345
|
+
# reasonable to do. If _groups_ is true, organize parameters
|
346
|
+
# in groups.
|
347
|
+
def option_parser_fill(parser, uniquify = true, groups = false)
|
348
|
+
description.option_parser_fill(parser, self,
|
349
|
+
uniquify, groups)
|
350
|
+
end
|
351
|
+
|
352
|
+
# Provides only a banner for the current class to the OptionParser
|
353
|
+
# _parser_.
|
354
|
+
def option_parser_banner(parser)
|
355
|
+
description.parser_banner(parser, self)
|
356
|
+
end
|
357
|
+
|
358
|
+
# Prepares an option parser for this instance. See
|
359
|
+
# Description#option_parser_options
|
360
|
+
def option_parser_options(parser, uniquify = true, groups = false)
|
361
|
+
description.option_parser_options(parser, self, uniquify, groups)
|
362
|
+
end
|
363
|
+
|
364
|
+
# SaveState and the like:
|
365
|
+
|
366
|
+
# Calls Description#save_state on this object
|
367
|
+
def save_state
|
368
|
+
return description.save_state(self)
|
369
|
+
end
|
370
|
+
|
371
|
+
# Restores a previously saved state. (Well, it doesn't need to
|
372
|
+
# be previously saved, you can make it up if you like).
|
373
|
+
def restore_state(state)
|
374
|
+
description.restore_state(state, self)
|
375
|
+
end
|
376
|
+
|
377
|
+
end
|
378
|
+
|
379
|
+
# This module should be used with +extend+ to provide the class with
|
380
|
+
# descriptions functionnalities. You also need to +include+
|
381
|
+
# DescriptionInclude. Please not that all the *instance* methods
|
382
|
+
# defined here will become *class* methods in the class you extend.
|
383
|
+
#
|
384
|
+
# This module defines several methods to add a description (#describe) to a
|
385
|
+
# class, to add parameters (#param, #param_noaccess) and to import
|
386
|
+
# parameters from parents (#inherit_parameters).
|
387
|
+
#
|
388
|
+
# Factories can be created using the #craete_factory statement.
|
389
|
+
# This makes the
|
390
|
+
# current class the factory repository for all the subclasses. It creates
|
391
|
+
# a factory class method returning the base factory.
|
392
|
+
# You can use #register_class to register the current class into the
|
393
|
+
# base factory class.
|
394
|
+
module DescriptionExtend
|
395
|
+
|
396
|
+
# The functions for factory handling.
|
397
|
+
|
398
|
+
# Makes this class the factory class for all subclasses.
|
399
|
+
# It creates four class methods: base_factory, that always
|
400
|
+
# points to the closest factory in the hierarchy
|
401
|
+
# and three methods used internally.
|
402
|
+
#
|
403
|
+
# If _auto_ is true, the subclasses are all automatically
|
404
|
+
# registered to the factory. If _register_self_ is true
|
405
|
+
# the class itself is registered. It is probably not a good
|
406
|
+
# idea, so it is off by default.
|
407
|
+
def create_factory(auto = true, register_self = false)
|
408
|
+
cls = self
|
409
|
+
# we create a temporary module so that we can use
|
410
|
+
# define_method with a block and extend this class with it
|
411
|
+
mod = Module.new
|
412
|
+
mod.send(:define_method, :factory) do
|
413
|
+
return cls
|
414
|
+
end
|
415
|
+
mod.send(:define_method, :private_description_list) do
|
416
|
+
return @registered_descriptions
|
417
|
+
end
|
418
|
+
mod.send(:define_method, :private_description_hash) do
|
419
|
+
return @registered_descriptions_hash
|
420
|
+
end
|
421
|
+
# Creates an accessor for the factory class
|
422
|
+
mod.send(:define_method, :private_auto_register) do
|
423
|
+
@auto_register_subclasses
|
424
|
+
end
|
425
|
+
self.extend(mod)
|
426
|
+
|
427
|
+
# Creates the necessary arrays|hashes to handle the registered
|
428
|
+
# classes:
|
429
|
+
@registered_descriptions = []
|
430
|
+
@registered_descriptions_hash = {}
|
431
|
+
|
432
|
+
@auto_register_subclasses = auto
|
433
|
+
end
|
434
|
+
|
435
|
+
# Checks if the class has a factory
|
436
|
+
def has_factory?
|
437
|
+
return self.respond_to?(:factory)
|
438
|
+
end
|
439
|
+
|
440
|
+
# Returns the base description if there is one, or nil if there isn't
|
441
|
+
def base_description
|
442
|
+
if has_factory?
|
443
|
+
return factory.description
|
444
|
+
else
|
445
|
+
return nil
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
# Returns the description list of the factory. Raises an exception if
|
450
|
+
# there is no factory
|
451
|
+
def factory_description_list
|
452
|
+
raise "Must have a factory" unless has_factory?
|
453
|
+
return factory.private_description_list
|
454
|
+
end
|
455
|
+
|
456
|
+
# Returns the description hash of the factory. Raises an exception if
|
457
|
+
# there is no factory
|
458
|
+
def factory_description_hash
|
459
|
+
raise "Must have a factory" unless has_factory?
|
460
|
+
return factory.private_description_hash
|
461
|
+
end
|
462
|
+
|
463
|
+
# Returns the factory description with the given name
|
464
|
+
def factory_description(name)
|
465
|
+
raise "Must have a factory" unless has_factory?
|
466
|
+
return factory_description_hash.fetch(name)
|
467
|
+
end
|
468
|
+
|
469
|
+
# Returns the Class object associated with the given
|
470
|
+
# name in the factory
|
471
|
+
def factory_class(name)
|
472
|
+
return factory_description(name).object_class
|
473
|
+
end
|
474
|
+
|
475
|
+
# Registers the given description to the factory. If no description
|
476
|
+
# is given, the current class is registered.
|
477
|
+
def register_class(desc = nil)
|
478
|
+
raise "One of the superclasses should have a 'factory' statement" unless
|
479
|
+
has_factory?
|
480
|
+
desc = description if desc.nil?
|
481
|
+
factory_description_list << desc
|
482
|
+
factory_description_hash[desc.name] = desc
|
483
|
+
end
|
484
|
+
|
485
|
+
# Returns the Description of the class.
|
486
|
+
def description
|
487
|
+
return @description
|
488
|
+
end
|
489
|
+
|
490
|
+
# Sets the description of the class. It is probably way better to use
|
491
|
+
# #describe, or write your own class method in the base class in the
|
492
|
+
# case of a family of classes.
|
493
|
+
def set_description(desc)
|
494
|
+
@description = desc
|
495
|
+
if has_factory? and factory.private_auto_register
|
496
|
+
register_class
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
|
501
|
+
# Registers an new parameter, with the following properties:
|
502
|
+
# * _writer_ is the name of the method used to write that parameter;
|
503
|
+
# * _reader_ the name of the method that returns its current value;
|
504
|
+
# * _name_ is a short code-like name of the parameter (typically
|
505
|
+
# lowercase);
|
506
|
+
# * _long_name_ is a more descriptive name, properly capitalized and
|
507
|
+
# localized if possible;
|
508
|
+
# * _type_ is it's type. Please see the MetaBuilder::ParameterType
|
509
|
+
# for a better description of what is a type;
|
510
|
+
# * _desc_ is a proper (short) description of the parameter,
|
511
|
+
# something that would fit on a What's this box, for instance.
|
512
|
+
# * _attrs_ are optional parameters that may come useful, see
|
513
|
+
# Parameter#attributes documentation.
|
514
|
+
#
|
515
|
+
# You might want to use the #param_reader, #param_writer, and
|
516
|
+
# #param_accessor facilities that create the respective accessors
|
517
|
+
# in addition. A typical example would be:
|
518
|
+
#
|
519
|
+
# param :set_size, :size, 'size', "Size", {:type => :integer},
|
520
|
+
# "The size !!"
|
521
|
+
#
|
522
|
+
def param(writer, reader, name, long_name, type,
|
523
|
+
desc = "", attrs = {})
|
524
|
+
raise "Use describe first" if description.nil?
|
525
|
+
param = MetaBuilder::Parameter.new(name, writer, reader,
|
526
|
+
long_name,
|
527
|
+
type, desc, attrs)
|
528
|
+
description.add_param(param)
|
529
|
+
return param
|
530
|
+
end
|
531
|
+
|
532
|
+
# The same as #param, but creates a attr_reader in addition
|
533
|
+
def param_reader(writer, reader, name, long_name, type,
|
534
|
+
desc = "", attrs = {})
|
535
|
+
p = param(writer, reader, name, long_name, type, desc, attrs)
|
536
|
+
attr_reader reader
|
537
|
+
return p
|
538
|
+
end
|
539
|
+
|
540
|
+
# The same as #param, except that _writer_ is made from _symbol_ by
|
541
|
+
# appending a = at the end. An attr_writer is created for the _symbol_.
|
542
|
+
def param_writer(symbol, name, long_name, type,
|
543
|
+
desc = "", attrs = {})
|
544
|
+
writer = (symbol.to_s + '=').to_sym
|
545
|
+
p = param(writer, symbol, name, long_name, type, desc, attrs)
|
546
|
+
attr_writer symbol
|
547
|
+
return p
|
548
|
+
end
|
549
|
+
|
550
|
+
# The same as #param_writer, except that an attr_writer is created
|
551
|
+
# for the _symbol_ instead of only a attr_writer. The most useful of
|
552
|
+
# the four methods. Typical use:
|
553
|
+
#
|
554
|
+
# param_accessor :name, 'name', "Object name", {:type => :string},
|
555
|
+
# "The name of the object"
|
556
|
+
def param_accessor(symbol, name, long_name, type,
|
557
|
+
desc = "", attrs = {})
|
558
|
+
writer = (symbol.to_s + '=').to_sym
|
559
|
+
p = param(writer, symbol, name, long_name, type, desc, attrs)
|
560
|
+
attr_accessor symbol
|
561
|
+
return p
|
562
|
+
end
|
563
|
+
|
564
|
+
# Creates a description attached to the current class. It needs to be
|
565
|
+
# used before anything else.
|
566
|
+
def describe(name, longname = name, desc = "")
|
567
|
+
d = Description.new(self, name, longname, desc)
|
568
|
+
set_description(d)
|
569
|
+
end
|
570
|
+
|
571
|
+
|
572
|
+
# Imports the given parameters directly from the parent class.
|
573
|
+
# This function is quite naive and will not look further than
|
574
|
+
# the direct #superclass.
|
575
|
+
def inherit_parameters(*names)
|
576
|
+
if self.superclass.respond_to?(:description)
|
577
|
+
parents_params = self.superclass.description.param_hash
|
578
|
+
for n in names
|
579
|
+
if parents_params.key?(n)
|
580
|
+
description.add_param(parents_params[n])
|
581
|
+
else
|
582
|
+
warn "Param #{n} not found"
|
583
|
+
end
|
584
|
+
end
|
585
|
+
else
|
586
|
+
warn "The parent class has no description"
|
587
|
+
end
|
588
|
+
end
|
589
|
+
|
590
|
+
# Switches to the given ParameterGroup. If it doesn't exist, it is
|
591
|
+
# created with the given parameters.
|
592
|
+
def group(name, long_name = name, desc = nil)
|
593
|
+
if not description.group_hash.has_key?(name)
|
594
|
+
group = ParameterGroup.new(name, long_name, desc)
|
595
|
+
description.add_group(group)
|
596
|
+
end
|
597
|
+
description.switch_to_group(name)
|
598
|
+
end
|
599
|
+
|
600
|
+
end
|
601
|
+
|
602
|
+
end
|
603
|
+
|