rtkit 0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,213 @@
1
+ module RTKIT
2
+
3
+ # Contains DICOM data and methods related to a Contour.
4
+ # A set of Contours in a set of Slices defines a ROI.
5
+ #
6
+ # === Relations
7
+ #
8
+ # * The Contour belongs to a Slice.
9
+ # * A Contour has many Coordinates.
10
+ #
11
+ # === Resources
12
+ #
13
+ # * ROI Contour Module: PS 3.3, C.8.8.6
14
+ # * Patient Based Coordinate System: PS 3.3, C.7.6.2.1.1
15
+ #
16
+ class Contour
17
+
18
+ # An array of Coordinates (x,y,z - triplets).
19
+ attr_reader :coordinates
20
+ # Contour Number.
21
+ attr_reader :number
22
+ # The Slice that the Contour belongs to.
23
+ attr_reader :slice
24
+ # Contour Geometric Type.
25
+ attr_reader :type
26
+
27
+ # Creates a new Contour instance from x, y and z coordinate arrays.
28
+ # This method also creates and connects any child Coordinates as indicated by the coordinate arrays.
29
+ # Returns the Contour instance.
30
+ #
31
+ # === Parameters
32
+ #
33
+ # * <tt>x</tt> -- An array of x coordinates (Floats).
34
+ # * <tt>y</tt> -- An array of y coordinates (Floats).
35
+ # * <tt>z</tt> -- An array of z coordinates (Floats).
36
+ # * <tt>slice</tt> -- The Slice instance that this Contour belongs to.
37
+ #
38
+ def self.create_from_coordinates(x, y, z, slice)
39
+ raise ArgumentError, "Invalid argument 'x'. Expected Array, got #{x.class}." unless x.is_a?(Array)
40
+ raise ArgumentError, "Invalid argument 'y'. Expected Array, got #{y.class}." unless y.is_a?(Array)
41
+ raise ArgumentError, "Invalid argument 'z'. Expected Array, got #{z.class}." unless z.is_a?(Array)
42
+ raise ArgumentError, "Invalid argument 'slice'. Expected Slice, got #{slice.class}." unless slice.is_a?(Slice)
43
+ raise ArgumentError, "The coordinate arrays are of unequal length [#{x.length}, #{y.length}, #{z.length}]." unless [x.length, y.length, z.length].uniq.length == 1
44
+ number = slice.roi.num_contours + 1
45
+ # Create the Contour:
46
+ c = self.new(slice, :number => number)
47
+ # Create the Coordinates belonging to this Contour:
48
+ x.each_index do |i|
49
+ Coordinate.new(x[i], y[i], z[i], c)
50
+ end
51
+ return c
52
+ end
53
+
54
+ # Creates a new Contour instance from a contour item.
55
+ # This method also creates and connects any Coordinates as indicated by the item.
56
+ # Returns the Contour instance.
57
+ #
58
+ # === Parameters
59
+ #
60
+ # * <tt>contour_item</tt> -- An array of contour items from the Contour Sequence in ROI Contour Sequence, belonging to the same slice.
61
+ # * <tt>slice</tt> -- The Slice instance that this Contour belongs to.
62
+ #
63
+ def self.create_from_item(contour_item, slice)
64
+ raise ArgumentError, "Invalid argument 'contour_item'. Expected Item, got #{contour_item.class}." unless contour_item.is_a?(DICOM::Item)
65
+ raise ArgumentError, "Invalid argument 'slice'. Expected Slice, got #{slice.class}." unless slice.is_a?(Slice)
66
+ raise ArgumentError, "Invalid argument 'contour_item'. The specified Item does not contain a Contour Data Value (Element '3006,0050')." unless contour_item.value(CONTOUR_DATA)
67
+ number = (contour_item.value(CONTOUR_NUMBER) ? contour_item.value(CONTOUR_NUMBER).to_i : nil)
68
+ type = contour_item.value(CONTOUR_GEO_TYPE)
69
+ #size = contour_item.value(NR_CONTOUR_POINTS) # May be used for QA of the content of the item, but not needed in the Contour object.
70
+ # Create the Contour:
71
+ c = self.new(slice, :type => type, :number => number)
72
+ # Create the Coordinates belonging to this Contour:
73
+ c.create_coordinates(contour_item.value(CONTOUR_DATA))
74
+ return c
75
+ end
76
+
77
+ # Creates a new Contour instance.
78
+ #
79
+ # === Parameters
80
+ #
81
+ # * <tt>slice</tt> -- The Slice instance that this Contour belongs to.
82
+ # * <tt>options</tt> -- A hash of parameters.
83
+ #
84
+ # === Options
85
+ #
86
+ # * <tt>:number</tt> -- Integer. The Contour Number.
87
+ # * <tt>:type</tt> -- String. The Contour Geometric Type. Defaults to 'CLOSED_PLANAR'.
88
+ #
89
+ def initialize(slice, options={})
90
+ raise ArgumentError, "Invalid argument 'slice'. Expected Slice, got #{slice.class}." unless slice.is_a?(Slice)
91
+ raise ArgumentError, "Invalid option :number. Expected Integer, got #{options[:number].class}." if options[:number] && !options[:number].is_a?(Integer)
92
+ raise ArgumentError, "Invalid option :type. Expected String, got #{options[:type].class}." if options[:type] && !options[:type].is_a?(String)
93
+ # Key attributes:
94
+ @coordinates = Array.new
95
+ @slice = slice
96
+ @type = options[:type] || 'CLOSED_PLANAR'
97
+ @number = options[:number] # don't need a default value for this attribute
98
+ # Register ourselves with the Slice:
99
+ @slice.add_contour(self)
100
+ end
101
+
102
+ # Returns true if the argument is an instance with attributes equal to self.
103
+ #
104
+ def ==(other)
105
+ if other.respond_to?(:to_contour)
106
+ other.send(:state) == state
107
+ end
108
+ end
109
+
110
+ alias_method :eql?, :==
111
+
112
+ # Adds a Coordinate instance to this Contour.
113
+ #
114
+ def add_coordinate(coordinate)
115
+ raise ArgumentError, "Invalid argument 'coordinate'. Expected Coordinate, got #{coordinate.class}." unless coordinate.is_a?(Coordinate)
116
+ @coordinates << coordinate unless @coordinates.include?(coordinate)
117
+ end
118
+
119
+ # Returns all Coordinates of this Contour, packed to a string in the format
120
+ # used in the Contour Data DICOM Element (3006,0050).
121
+ # Returns an empty string if the Contour contains no coordinates.
122
+ #
123
+ def contour_data
124
+ x, y, z = coords
125
+ return [x, y, z].transpose.flatten.join("\\")
126
+ end
127
+
128
+ # Returns all Coordinates of this Contour, in arrays of x, y and z coordinates.
129
+ #
130
+ def coords
131
+ x, y, z = Array.new, Array.new, Array.new
132
+ @coordinates.each do |coord|
133
+ x << coord.x
134
+ y << coord.y
135
+ z << coord.z
136
+ end
137
+ return x, y, z
138
+ end
139
+
140
+ # Creates and connects Coordinate instances with this Contour instance
141
+ # by processing the value of the Contour Data element.
142
+ #
143
+ # === Parameters
144
+ #
145
+ # * <tt>contour_data</tt> -- The value of the Contour Data Element (A String of backslash-separated of xyz coordinate triplets).
146
+ #
147
+ def create_coordinates(contour_data)
148
+ raise ArgumentError, "Invalid argument 'contour_data'. Expected String (or nil), got #{contour_data.class}." unless [String, NilClass].include?(contour_data.class)
149
+ if contour_data && contour_data != ""
150
+ # Split the number strings, sperated by a '\', into an array:
151
+ string_values = contour_data.split("\\")
152
+ size = string_values.length/3
153
+ # Extract every third value of the string array as x, y, and z, respectively, and collect them as floats instead of strings:
154
+ x = string_values.values_at(*(Array.new(size){|i| i*3 })).collect{|val| val.to_f}
155
+ y = string_values.values_at(*(Array.new(size){|i| i*3+1})).collect{|val| val.to_f}
156
+ z = string_values.values_at(*(Array.new(size){|i| i*3+2})).collect{|val| val.to_f}
157
+ x.each_index do |i|
158
+ Coordinate.new(x[i], y[i], z[i], self)
159
+ end
160
+ end
161
+ end
162
+
163
+ # Generates a Fixnum hash value for this instance.
164
+ #
165
+ def hash
166
+ state.hash
167
+ end
168
+
169
+ =begin
170
+ # Returns the number of Coordinates (Corner Points) belonging to this Contour.
171
+ #
172
+ def length
173
+ @coordinates.length
174
+ end
175
+
176
+ alias_method :size, :length
177
+ =end
178
+
179
+ # Returns self.
180
+ #
181
+ def to_contour
182
+ self
183
+ end
184
+
185
+ # Creates and returns a Contour Sequence Item from the attributes of the Contour.
186
+ #
187
+ def to_item
188
+ # FIXME: We need to decide on how to principally handle the situation when an image series has not been
189
+ # loaded, and how to set up the ROI slices. A possible solution is to create Image instances if they hasn't been loaded.
190
+ item = DICOM::Item.new
191
+ item.add(DICOM::Sequence.new(CONTOUR_IMAGE_SQ))
192
+ item[CONTOUR_IMAGE_SQ].add_item
193
+ item[CONTOUR_IMAGE_SQ][0].add(DICOM::Element.new(REF_SOP_CLASS_UID, @slice.image ? @slice.image.series.class_uid : '1.2.840.10008.5.1.4.1.1.2')) # Deafult to CT if image ref. doesn't exist.
194
+ item[CONTOUR_IMAGE_SQ][0].add(DICOM::Element.new(REF_SOP_UID, @slice.uid))
195
+ item.add(DICOM::Element.new(CONTOUR_GEO_TYPE, @type))
196
+ item.add(DICOM::Element.new(NR_CONTOUR_POINTS, @coordinates.length.to_s))
197
+ item.add(DICOM::Element.new(CONTOUR_NUMBER, @number.to_s))
198
+ item.add(DICOM::Element.new(CONTOUR_DATA, contour_data))
199
+ return item
200
+ end
201
+
202
+
203
+ private
204
+
205
+
206
+ # Returns the attributes of this instance in an array (for comparison purposes).
207
+ #
208
+ def state
209
+ [@coordinates, @number, @type]
210
+ end
211
+
212
+ end
213
+ end
@@ -0,0 +1,371 @@
1
+ module RTKIT
2
+
3
+ # Contains DICOM data and methods related to a ControlPoint.
4
+ #
5
+ # === Notes
6
+ #
7
+ # The first control point in a given beam defines the intial setup, and contains
8
+ # all applicable parameters. The rest of the control points contains the parameters
9
+ # which change at any control point.
10
+ #
11
+ # === Relations
12
+ #
13
+ # * A Beam has many ControlPoints.
14
+ # * A ControlPoint has many Collimators.
15
+ #
16
+ class ControlPoint
17
+
18
+
19
+ # The Beam that the ControlPoint is defined in.
20
+ attr_reader :beam
21
+ # Collimator (beam limiting device) angle (float).
22
+ attr_reader :collimator_angle
23
+ # Collimator (beam limiting device) rotation direction (string).
24
+ attr_reader :collimator_direction
25
+ # An array containing the ControlPoint's collimators.
26
+ attr_reader :collimators
27
+ # Cumulative meterset weight (float).
28
+ attr_reader :cum_meterset
29
+ # Nominal beam energy (float).
30
+ attr_reader :energy
31
+ # Gantry angle (float).
32
+ attr_reader :gantry_angle
33
+ # Gantry rotation direction (string).
34
+ attr_reader :gantry_direction
35
+ # Control point index (integer).
36
+ attr_reader :index
37
+ # Isosenter position (a coordinate triplet of positions x, y, z).
38
+ attr_reader :iso
39
+ # Pedestal angle (float).
40
+ attr_reader :pedestal_angle
41
+ # Pedestal rotation direction (string).
42
+ attr_reader :pedestal_direction
43
+ # Source to surface distance (float).
44
+ attr_reader :ssd
45
+ # Table top angle (float).
46
+ attr_reader :table_top_angle
47
+ # Table top rotation direction (string).
48
+ attr_reader :table_top_direction
49
+ # Table top lateral position (float).
50
+ attr_reader :table_top_lateral
51
+ # Table top longitudinal position (float).
52
+ attr_reader :table_top_longitudinal
53
+ # Table top vertical position (float).
54
+ attr_reader :table_top_vertical
55
+
56
+ # Creates a new control point instance from a Control Point Sequence Item (from an RTPlan file).
57
+ # Returns the ControlPoint instance.
58
+ #
59
+ # === Parameters
60
+ #
61
+ # * <tt>cp_item</tt> -- An item from the Control Point Sequence in the DObject of a RTPlan file.
62
+ # * <tt>beam</tt> -- The Beam instance that this ControlPoint belongs to.
63
+ #
64
+ def self.create_from_item(cp_item, beam)
65
+ raise ArgumentError, "Invalid argument 'cp_item'. Expected DICOM::Item, got #{cp_item.class}." unless cp_item.is_a?(DICOM::Item)
66
+ raise ArgumentError, "Invalid argument 'beam'. Expected Beam, got #{beam.class}." unless beam.is_a?(Beam)
67
+ # Values from the Structure Set ROI Sequence Item:
68
+ index = cp_item.value(CONTROL_POINT_INDEX)
69
+ cum_meterset = cp_item.value(CUM_METERSET_WEIGHT)
70
+ # Create the Beam instance:
71
+ cp = self.new(index, cum_meterset, beam)
72
+ # Set optional values:
73
+ cp.collimator_angle = cp_item.value(COLL_ANGLE)
74
+ cp.collimator_direction = cp_item.value(COLL_DIRECTION)
75
+ cp.energy = cp_item.value(BEAM_ENERGY)
76
+ cp.gantry_angle = cp_item.value(GANTRY_ANGLE)
77
+ cp.gantry_direction = cp_item.value(GANTRY_DIRECTION)
78
+ cp.iso = cp_item.value(ISO_POS)
79
+ cp.pedestal_angle = cp_item.value(PEDESTAL_ANGLE)
80
+ cp.pedestal_direction = cp_item.value(PEDESTAL_DIRECTION)
81
+ cp.ssd = cp_item.value(SSD).to_f if cp_item.exists?(SSD)
82
+ cp.table_top_angle = cp_item.value(TABLE_TOP_ANGLE)
83
+ cp.table_top_direction = cp_item.value(TABLE_TOP_DIRECTION)
84
+ cp.table_top_lateral = cp_item.value(TABLE_TOP_LATERAL)
85
+ cp.table_top_vertical = cp_item.value(TABLE_TOP_VERTICAL)
86
+ cp.table_top_longitudinal = cp_item.value(TABLE_TOP_LONGITUDINAL)
87
+ # Iterate the beam limiting device position items and create Collimator instances:
88
+ if cp_item.exists?(COLL_POS_SQ)
89
+ cp_item[COLL_POS_SQ].each do |coll_item|
90
+ CollimatorSetup.create_from_item(coll_item, cp)
91
+ end
92
+ end
93
+ return cp
94
+ end
95
+
96
+ # Creates a new ControlPoint instance.
97
+ #
98
+ # === Parameters
99
+ #
100
+ # * <tt>index</tt> -- Integer. The control point index.
101
+ # * <tt>meterset</tt> -- The control point's cumulative meterset weight.
102
+ # * <tt>beam</tt> -- The Beam instance that this ControlPoint belongs to.
103
+ # * <tt>options</tt> -- A hash of parameters.
104
+ #
105
+ # === Options
106
+ #
107
+ # * <tt>:type</tt> -- String. Beam type. Defaults to 'STATIC'.
108
+ # * <tt>:delivery_type</tt> -- String. Treatment delivery type. Defaults to 'TREATMENT'.
109
+ # * <tt>:description</tt> -- String. Beam description. Defaults to the 'name' attribute.
110
+ # * <tt>:rad_type</tt> -- String. Radiation type. Defaults to 'PHOTON'.
111
+ # * <tt>:sad</tt> -- Float. Source-axis distance. Defaults to 1000.0.
112
+ # * <tt>:unit</tt> -- String. The primary dosimeter unit. Defaults to 'MU'.
113
+ #
114
+ def initialize(index, cum_meterset, beam)
115
+ raise ArgumentError, "Invalid argument 'beam'. Expected Beam, got #{beam.class}." unless beam.is_a?(Beam)
116
+ # Set values:
117
+ @collimators = Array.new
118
+ @associated_collimators = Hash.new
119
+ self.index = index
120
+ self.cum_meterset = cum_meterset
121
+ # Set references:
122
+ @beam = beam
123
+ # Register ourselves with the Beam:
124
+ @beam.add_control_point(self)
125
+ end
126
+
127
+ # Returns true if the argument is an instance with attributes equal to self.
128
+ #
129
+ def ==(other)
130
+ if other.respond_to?(:to_control_point)
131
+ other.send(:state) == state
132
+ end
133
+ end
134
+
135
+ alias_method :eql?, :==
136
+
137
+ # Adds a CollimatorSetup instance to this ControlPoint.
138
+ #
139
+ def add_collimator(coll)
140
+ raise ArgumentError, "Invalid argument 'coll'. Expected CollimatorSetup, got #{coll.class}." unless coll.is_a?(CollimatorSetup)
141
+ @collimators << coll unless @associated_collimators[coll.type]
142
+ @associated_collimators[coll.type] = coll
143
+ end
144
+
145
+ # Returns the CollimatorSetup instance mathcing the specified device type string (if an argument is used).
146
+ # If a specified type doesn't match, nil is returned.
147
+ # If no argument is passed, the first CollimatorSetup instance associated with the ControlPoint is returned.
148
+ #
149
+ # === Parameters
150
+ #
151
+ # * <tt>type</tt> -- String. The RT Beam Limiting Device Type value.
152
+ #
153
+ def collimator(*args)
154
+ raise ArgumentError, "Expected one or none arguments, got #{args.length}." unless [0, 1].include?(args.length)
155
+ if args.length == 1
156
+ return @associated_collimators[args.first && args.first.to_s]
157
+ else
158
+ # No argument used, therefore we return the first CollimatorSetup instance:
159
+ return @collimators.first
160
+ end
161
+ end
162
+
163
+ # Sets a new beam limiting device angle.
164
+ #
165
+ # === Parameters
166
+ #
167
+ # * <tt>value</tt> -- Float. The beam limiting device angle (300A,0120).
168
+ #
169
+ def collimator_angle=(value)
170
+ @collimator_angle = value && value.to_f
171
+ end
172
+
173
+ # Sets a new beam limiting device rotation direction.
174
+ #
175
+ # === Parameters
176
+ #
177
+ # * <tt>value</tt> -- String. The beam limiting device rotation direction (300A,0121).
178
+ #
179
+ def collimator_direction=(value)
180
+ @collimator_direction = value && value.to_s
181
+ end
182
+
183
+ # Sets a new cumulative meterset weight.
184
+ #
185
+ # === Parameters
186
+ #
187
+ # * <tt>value</tt> -- Float. The cumulative meterset weight (300A,0134).
188
+ #
189
+ def cum_meterset=(value)
190
+ raise ArgumentError, "Argument 'value' must be defined (got #{value.class})." unless value
191
+ @cum_meterset = value && value.to_f
192
+ end
193
+
194
+ # Sets a new nominal beam energy.
195
+ #
196
+ # === Parameters
197
+ #
198
+ # * <tt>value</tt> -- Float. The nominal beam energy (300A,0114).
199
+ #
200
+ def energy=(value)
201
+ @energy = value && value.to_f
202
+ end
203
+
204
+ # Sets a new gantry angle.
205
+ #
206
+ # === Parameters
207
+ #
208
+ # * <tt>value</tt> -- Float. The gantry angle (300A,011E).
209
+ #
210
+ def gantry_angle=(value)
211
+ @gantry_angle = value && value.to_f
212
+ end
213
+
214
+ # Sets a new gantry rotation direction.
215
+ #
216
+ # === Parameters
217
+ #
218
+ # * <tt>value</tt> -- String. The gantry rotation direction (300A,011F).
219
+ #
220
+ def gantry_direction=(value)
221
+ @gantry_direction = value && value.to_s
222
+ end
223
+
224
+ # Generates a Fixnum hash value for this instance.
225
+ #
226
+ def hash
227
+ state.hash
228
+ end
229
+
230
+ # Sets a new control point index.
231
+ #
232
+ # === Parameters
233
+ #
234
+ # * <tt>value</tt> -- Integer. The control point index (300A,0112).
235
+ #
236
+ def index=(value)
237
+ raise ArgumentError, "Argument 'value' must be defined (got #{value.class})." unless value
238
+ @index = value.to_i
239
+ end
240
+
241
+ # Sets a new isosenter position (a coordinate triplet of positions x, y, z).
242
+ #
243
+ # === Parameters
244
+ #
245
+ # * <tt>value</tt> -- Coordinate/String. The isocenter position (300A,0112).
246
+ #
247
+ def iso=(value)
248
+ @iso = value && value.to_coordinate
249
+ end
250
+
251
+ # Sets a new patient support angle.
252
+ #
253
+ # === Parameters
254
+ #
255
+ # * <tt>value</tt> -- Float. The patient support angle (300A,0122).
256
+ #
257
+ def pedestal_angle=(value)
258
+ @pedestal_angle = value && value.to_f
259
+ end
260
+
261
+ # Sets a new patient support rotation direction.
262
+ #
263
+ # === Parameters
264
+ #
265
+ # * <tt>value</tt> -- String. The patient support rotation direction (300A,0123).
266
+ #
267
+ def pedestal_direction=(value)
268
+ @pedestal_direction = value && value.to_s
269
+ end
270
+
271
+ # Sets a new source to surface distance.
272
+ #
273
+ # === Parameters
274
+ #
275
+ # * <tt>value</tt> -- Float. The source to surface distance (300A,012C).
276
+ #
277
+ def ssd=(value)
278
+ @ssd = value && value.to_f
279
+ end
280
+
281
+ # Sets a new table top eccentric angle.
282
+ #
283
+ # === Parameters
284
+ #
285
+ # * <tt>value</tt> -- Float. The table top eccentric angle (300A,0125).
286
+ #
287
+ def table_top_angle=(value)
288
+ @table_top_angle = value && value.to_f
289
+ end
290
+
291
+ # Sets a new table top eccentric rotation direction.
292
+ #
293
+ # === Parameters
294
+ #
295
+ # * <tt>value</tt> -- String. The table top eccentric rotation direction (300A,0126).
296
+ #
297
+ def table_top_direction=(value)
298
+ @table_top_direction = value && value.to_s
299
+ end
300
+
301
+ # Sets a new table top lateral position.
302
+ #
303
+ # === Parameters
304
+ #
305
+ # * <tt>value</tt> -- Float. The table top lateral position (300A,0125).
306
+ #
307
+ def table_top_lateral=(value)
308
+ @table_top_lateral = value && value.to_f
309
+ end
310
+
311
+ # Sets a new table top longitudinal position.
312
+ #
313
+ # === Parameters
314
+ #
315
+ # * <tt>value</tt> -- Float. The table top longitudinal position (300A,0125).
316
+ #
317
+ def table_top_longitudinal=(value)
318
+ @table_top_longitudinal = value && value.to_f
319
+ end
320
+
321
+ # Sets a new table top vertical position.
322
+ #
323
+ # === Parameters
324
+ #
325
+ # * <tt>value</tt> -- Float. The table top vertical position (300A,0125).
326
+ #
327
+ def table_top_vertical=(value)
328
+ @table_top_vertical = value && value.to_f
329
+ end
330
+
331
+ # Returns self.
332
+ #
333
+ def to_control_point
334
+ self
335
+ end
336
+
337
+ # Creates and returns a Control Point Sequence Item from the attributes of the ControlPoint.
338
+ #
339
+ def to_item
340
+ item = DICOM::Item.new
341
+ item.add(DICOM::Element.new(ROI_COLOR, @color))
342
+ s = DICOM::Sequence.new(CONTOUR_SQ)
343
+ item.add(s)
344
+ item.add(DICOM::Element.new(REF_ROI_NUMBER, @number.to_s))
345
+ # Add Contour items to the Contour Sequence (one or several items per Slice):
346
+ @slices.each do |slice|
347
+ slice.contours.each do |contour|
348
+ s.add_item(contour.to_item)
349
+ end
350
+ end
351
+ return item
352
+ end
353
+
354
+
355
+ private
356
+
357
+
358
+ # Returns the attributes of this instance in an array (for comparison purposes).
359
+ #
360
+ def state
361
+ [@collimators, @collimator_angle, @collimator_direction, @cum_meterset, @energy,
362
+ @gantry_angle, @gantry_direction, @index, @iso, @pedestal_angle, @pedestal_direction,
363
+ @ssd, @table_top_angle, @table_top_direction, @table_top_lateral,
364
+ @table_top_longitudinal, @table_top_vertical
365
+ ]
366
+ end
367
+
368
+
369
+ end
370
+
371
+ end