rtp-connect 1.6 → 1.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -16,37 +16,52 @@ module RTP
16
16
  def leaf_boundaries(nr_leaves)
17
17
  case nr_leaves
18
18
  when 29
19
- [-200, -135, -125, -115, -105, -95, -85, -75, -65, -55, -45, -35, -25,
20
- -15, -5, 5, 15, 25, 35, 45, 55, 65, 75, 85, 95, 105, 115, 125, 135, 200
21
- ]
19
+ leaf_boundaries_odd(29)
22
20
  when 40
23
- Array.new(nr_leaves+1) {|i| (i * 400 / nr_leaves.to_f - 200).to_i}
21
+ leaf_boundaries_even(40)
24
22
  when 41
25
- [-200, -195, -185, -175, -165, -155, -145, -135, -125, -115,
26
- -105, -95, -85, -75, -65, -55, -45, -35, -25, -15, -5, 5, 15, 25, 35, 45,
27
- 55, 65, 75, 85, 95, 105, 115, 125, 135, 145, 155, 165, 175, 185, 195, 200
28
- ]
23
+ leaf_boundaries_odd(41)
29
24
  when 60
30
- [-200, -190, -180, -170, -160, -150, -140, -130, -120, -110,
31
- -100, -95, -90, -85, -80, -75, -70, -65, -60, -55, -50, -45, -40, -35, -30,
32
- -25, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65,
33
- 70, 75, 80, 85, 90, 95, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200
34
- ]
25
+ Array.new(10) {|i| (i * 10 - 200).to_i}
26
+ .concat(Array.new(41) {|i| (i * 5 - 100).to_i})
27
+ .concat(Array.new(10) {|i| (i * 10 + 110).to_i})
35
28
  when 80
36
- Array.new(nr_leaves+1) {|i| (i * 400 / nr_leaves.to_f - 200).to_i}
29
+ leaf_boundaries_even(80)
37
30
  else
38
31
  raise ArgumentError, "Unsupported number of leaves: #{nr_leaves}"
39
32
  end
40
33
  end
41
34
 
35
+ # Gives an array of MLC leaf position boundaries for ordinary even numbered
36
+ # multi leaf collimators.
37
+ #
38
+ # @param [Fixnum] nr_leaves the number of leaves (in one leaf bank)
39
+ # @return [Array<Fixnum>] the leaf boundary positions
40
+ #
41
+ def leaf_boundaries_even(nr_leaves)
42
+ Array.new(nr_leaves+1) {|i| (i * 400 / nr_leaves.to_f - 200).to_i}
43
+ end
44
+
45
+ # Gives an array of MLC leaf position boundaries for ordinary odd numbered
46
+ # multi leaf collimators.
47
+ #
48
+ # @param [Fixnum] nr_leaves the number of leaves (in one leaf bank)
49
+ # @return [Array<Fixnum>] the leaf boundary positions
50
+ #
51
+ def leaf_boundaries_odd(nr_leaves)
52
+ Array.new(nr_leaves-1) {|i| (10 * (i - (0.5 * nr_leaves - 1))).to_i}.unshift(-200).push(200)
53
+ end
54
+
42
55
  # Computes the CRC checksum of the given line and verifies that
43
56
  # this value corresponds with the checksum given at the end of the line.
44
57
  #
45
58
  # @param [String] line a single line string from an RTPConnect ascii file
59
+ # @param [Hash] options the options to use for verifying the RTP record
60
+ # @option options [Boolean] :ignore_crc if true, the verification method will return true even if the checksum is invalid
46
61
  # @return [Boolean] true
47
62
  # @raise [ArgumentError] if an invalid line/record is given or the string contains an invalid checksum
48
63
  #
49
- def verify(line)
64
+ def verify(line, options={})
50
65
  last_comma_pos = line.rindex(',')
51
66
  raise ArgumentError, "Invalid line encountered; No comma present in the string: #{line}" unless last_comma_pos
52
67
  string_to_check = line[0..last_comma_pos]
@@ -54,7 +69,7 @@ module RTP
54
69
  raise ArgumentError, "Invalid line encountered; Valid checksum missing at end of string: #{string_remaining}" unless string_remaining.length >= 3
55
70
  checksum_extracted = string_remaining.value.to_i
56
71
  checksum_computed = string_to_check.checksum
57
- raise ArgumentError, "Invalid line encountered: Specified checskum #{checksum_extracted} deviates from the computed checksum #{checksum_computed}." if checksum_extracted != checksum_computed
72
+ raise ArgumentError, "Invalid line encountered: Specified checksum #{checksum_extracted} deviates from the computed checksum #{checksum_computed}." if checksum_extracted != checksum_computed && !options[:ignore_crc]
58
73
  return true
59
74
  end
60
75
 
@@ -1,4 +1,4 @@
1
- # Copyright 2011-2013 Christoffer Lervag
1
+ # Copyright 2011-2014 Christoffer Lervag
2
2
  #
3
3
  # This program is free software: you can redistribute it and/or modify
4
4
  # it under the terms of the GNU General Public License as published by
@@ -63,70 +63,48 @@ module RTP
63
63
  # @note This method does not perform crc verification on the given string.
64
64
  # If such verification is desired, use methods ::parse or ::read instead.
65
65
  # @param [#to_s] string the plan definition record string line
66
+ # @param [Hash] options the options to use for loading the plan definition string
67
+ # @option options [Boolean] :repair if true, a record containing invalid CSV will be attempted fixed and loaded
66
68
  # @return [Plan] the created Plan instance
67
69
  # @raise [ArgumentError] if given a string containing an invalid number of elements
68
70
  #
69
- def self.load(string)
70
- # Get the quote-less values:
71
- values = string.to_s.values
72
- low_limit = 10
73
- high_limit = 28
74
- raise ArgumentError, "Invalid argument 'string': Expected at least #{low_limit} elements, got #{values.length}." if values.length < low_limit
75
- RTP.logger.warn "The number of elements (#{values.length}) for this Plan record exceeds the known number of data items for this record (#{high_limit}). This may indicate an invalid record or that the RTP format has recently been expanded with new items." if values.length > high_limit
71
+ def self.load(string, options={})
76
72
  rtp = self.new
77
- # Assign the values to attributes:
78
- rtp.keyword = values[0]
79
- rtp.patient_id = values[1]
80
- rtp.patient_last_name = values[2]
81
- rtp.patient_first_name = values[3]
82
- rtp.patient_middle_initial = values[4]
83
- rtp.plan_id = values[5]
84
- rtp.plan_date = values[6]
85
- rtp.plan_time = values[7]
86
- rtp.course_id = values[8]
87
- rtp.diagnosis = values[9]
88
- rtp.md_last_name = values[10]
89
- rtp.md_first_name = values[11]
90
- rtp.md_middle_initial = values[12]
91
- rtp.md_approve_last_name = values[13]
92
- rtp.md_approve_first_name = values[14]
93
- rtp.md_approve_middle_initial = values[15]
94
- rtp.phy_approve_last_name = values[16]
95
- rtp.phy_approve_first_name = values[17]
96
- rtp.phy_approve_middle_initial = values[18]
97
- rtp.author_last_name = values[19]
98
- rtp.author_first_name = values[20]
99
- rtp.author_middle_initial = values[21]
100
- rtp.rtp_mfg = values[22]
101
- rtp.rtp_model = values[23]
102
- rtp.rtp_version = values[24]
103
- rtp.rtp_if_protocol = values[25]
104
- rtp.rtp_if_version = values[26]
105
- rtp.crc = values[-1]
106
- return rtp
73
+ rtp.load(string, options)
107
74
  end
108
75
 
109
76
  # Creates a Plan instance by parsing an RTPConnect string.
110
77
  #
111
78
  # @param [#to_s] string an RTPConnect ascii string (with single or multiple lines/records)
79
+ # @param [Hash] options the options to use for parsing the RTP string
80
+ # @option options [Boolean] :ignore_crc if true, the RTP records will be successfully loaded even if their checksums are invalid
81
+ # @option options [Boolean] :repair if true, any RTP records containing invalid CSV will be attempted fixed and loaded
82
+ # @option options [Boolean] :skip_unknown if true, unknown records will be skipped, and record instances will be built from the remaining recognized string records
112
83
  # @return [Plan] the created Plan instance
113
84
  # @raise [ArgumentError] if given an invalid string record
114
85
  #
115
- def self.parse(string)
86
+ def self.parse(string, options={})
116
87
  lines = string.to_s.split("\r\n")
117
88
  # Create the Plan object:
118
89
  line = lines.first
119
- RTP::verify(line)
120
- rtp = self.load(line)
90
+ RTP::verify(line, options)
91
+ rtp = self.load(line, options)
121
92
  lines[1..-1].each do |line|
122
93
  # Validate, determine type, and process the line accordingly to
123
94
  # build the hierarchy of records:
124
- RTP::verify(line)
125
- values = line.values
95
+ RTP::verify(line, options)
96
+ values = line.values(options[:repair])
126
97
  keyword = values.first
127
98
  method = RTP::PARSE_METHOD[keyword]
128
- raise ArgumentError, "Unknown keyword #{keyword} extracted from string." unless method
129
- rtp.send(method, line)
99
+ if method
100
+ rtp.send(method, line)
101
+ else
102
+ if options[:skip_unknown]
103
+ logger.warn("Skipped unknown record definition: #{keyword}")
104
+ else
105
+ raise ArgumentError, "Unknown keyword #{keyword} extracted from string."
106
+ end
107
+ end
130
108
  end
131
109
  return rtp
132
110
  end
@@ -134,10 +112,14 @@ module RTP
134
112
  # Creates an Plan instance by reading and parsing an RTPConnect file.
135
113
  #
136
114
  # @param [String] file a string which specifies the path of the RTPConnect file to be loaded
115
+ # @param [Hash] options the options to use for reading the RTP file
116
+ # @option options [Boolean] :ignore_crc if true, the RTP records will be successfully loaded even if their checksums are invalid
117
+ # @option options [Boolean] :repair if true, any RTP records containing invalid CSV will be attempted fixed and loaded
118
+ # @option options [Boolean] :skip_unknown if true, unknown records will be skipped, and record instances will be built from the remaining recognized string records
137
119
  # @return [Plan] the created Plan instance
138
120
  # @raise [ArgumentError] if given an invalid file or the file given contains an invalid record
139
121
  #
140
- def self.read(file)
122
+ def self.read(file, options={})
141
123
  raise ArgumentError, "Invalid argument 'file'. Expected String, got #{file.class}." unless file.is_a?(String)
142
124
  # Read the file content:
143
125
  str = nil
@@ -160,7 +142,7 @@ module RTP
160
142
  end
161
143
  # Parse the file contents and create the RTP::Connect object:
162
144
  if str
163
- rtp = self.parse(str)
145
+ rtp = self.parse(str, options)
164
146
  else
165
147
  raise "An RTP::Plan object could not be created from the specified file. Check the log for more details."
166
148
  end
@@ -170,13 +152,45 @@ module RTP
170
152
  # Creates a new Plan.
171
153
  #
172
154
  def initialize
155
+ super('PLAN_DEF', 10, 28)
173
156
  @current_parent = self
174
157
  # Child records:
158
+ @extended_plan = nil
175
159
  @prescriptions = Array.new
176
160
  @dose_trackings = Array.new
177
161
  # No parent (by definition) for the Plan record:
178
162
  @parent = nil
179
- @keyword = 'PLAN_DEF'
163
+ @attributes = [
164
+ # Required:
165
+ :keyword,
166
+ :patient_id,
167
+ :patient_last_name,
168
+ :patient_first_name,
169
+ :patient_middle_initial,
170
+ :plan_id,
171
+ :plan_date,
172
+ :plan_time,
173
+ :course_id,
174
+ # Optional:
175
+ :diagnosis,
176
+ :md_last_name,
177
+ :md_first_name,
178
+ :md_middle_initial,
179
+ :md_approve_last_name,
180
+ :md_approve_first_name,
181
+ :md_approve_middle_initial,
182
+ :phy_approve_last_name,
183
+ :phy_approve_first_name,
184
+ :phy_approve_middle_initial,
185
+ :author_last_name,
186
+ :author_first_name,
187
+ :author_middle_initial,
188
+ :rtp_mfg,
189
+ :rtp_model,
190
+ :rtp_version,
191
+ :rtp_if_protocol,
192
+ :rtp_if_version
193
+ ]
180
194
  end
181
195
 
182
196
  # Checks for equality.
@@ -203,6 +217,14 @@ module RTP
203
217
  @dose_trackings << child.to_dose_tracking
204
218
  end
205
219
 
220
+ # Adds an extended plan record to this instance.
221
+ #
222
+ # @param [ExtendedPlan] child an ExtendedPlan instance which is to be associated with self
223
+ #
224
+ def add_extended_plan(child)
225
+ @extended_plan = child.to_extended_plan
226
+ end
227
+
206
228
  # Adds a prescription site record to this instance.
207
229
  #
208
230
  # @param [Prescription] child a Prescription instance which is to be associated with self
@@ -216,7 +238,7 @@ module RTP
216
238
  # @return [Array<Prescription, DoseTracking>] a sorted array of self's child records
217
239
  #
218
240
  def children
219
- return [@prescriptions, @dose_trackings].flatten.compact
241
+ return [@extended_plan, @prescriptions, @dose_trackings].flatten.compact
220
242
  end
221
243
 
222
244
  # Computes a hash code for this object.
@@ -229,43 +251,6 @@ module RTP
229
251
  state.hash
230
252
  end
231
253
 
232
- # Collects the values (attributes) of this instance.
233
- #
234
- # @note The CRC is not considered part of the actual values and is excluded.
235
- # @return [Array<String>] an array of attributes (in the same order as they appear in the RTP string)
236
- #
237
- def values
238
- return [
239
- @keyword,
240
- @patient_id,
241
- @patient_last_name,
242
- @patient_first_name,
243
- @patient_middle_initial,
244
- @plan_id,
245
- @plan_date,
246
- @plan_time,
247
- @course_id,
248
- @diagnosis,
249
- @md_last_name,
250
- @md_first_name,
251
- @md_middle_initial,
252
- @md_approve_last_name,
253
- @md_approve_first_name,
254
- @md_approve_middle_initial,
255
- @phy_approve_last_name,
256
- @phy_approve_first_name,
257
- @phy_approve_middle_initial,
258
- @author_last_name,
259
- @author_first_name,
260
- @author_middle_initial,
261
- @rtp_mfg,
262
- @rtp_model,
263
- @rtp_version,
264
- @rtp_if_protocol,
265
- @rtp_if_version
266
- ]
267
- end
268
-
269
254
  # Returns self.
270
255
  #
271
256
  # @return [Plan] self
@@ -308,18 +293,6 @@ module RTP
308
293
  f.close
309
294
  end
310
295
 
311
- # Sets the keyword attribute.
312
- #
313
- # @note Since only a specific string is accepted, this is more of an argument check than a traditional setter method
314
- # @param [#to_s] value the new attribute value
315
- # @raise [ArgumentError] if given an unexpected keyword
316
- #
317
- def keyword=(value)
318
- value = value.to_s.upcase
319
- raise ArgumentError, "Invalid keyword. Expected 'PLAN_DEF', got #{value}." unless value == "PLAN_DEF"
320
- @keyword = value
321
- end
322
-
323
296
  # Sets the patient_id attribute.
324
297
  #
325
298
  # @param [nil, #to_s] value the new attribute value
@@ -548,6 +521,15 @@ module RTP
548
521
  @current_parent = dt
549
522
  end
550
523
 
524
+ # Creates an extended plan record from the given string.
525
+ #
526
+ # @param [String] string a string line containing an extended plan definition
527
+ #
528
+ def extended_plan(string)
529
+ ep = ExtendedPlan.load(string, @current_parent)
530
+ @current_parent = ep
531
+ end
532
+
551
533
  # Creates an extended treatment field record from the given string.
552
534
  #
553
535
  # @param [String] string a string line containing an extended treatment field definition
@@ -2,6 +2,14 @@ module RTP
2
2
 
3
3
  class Plan < Record
4
4
 
5
+ attr_accessor :current_gantry
6
+ attr_accessor :current_collimator
7
+ attr_accessor :current_couch_angle
8
+ attr_accessor :current_couch_pedestal
9
+ attr_accessor :current_couch_lateral
10
+ attr_accessor :current_couch_longitudinal
11
+ attr_accessor :current_couch_vertical
12
+
5
13
  # Converts the Plan (and child) records to a
6
14
  # DICOM::DObject of modality RTPLAN.
7
15
  #
@@ -13,6 +21,7 @@ module RTP
13
21
  # @option options [Boolean] :dose_ref if set, Dose Reference & Referenced Dose Reference sequences will be included in the generated DICOM file
14
22
  # @option options [String] :manufacturer the value used for the manufacturer tag (0008,0070) in the beam sequence
15
23
  # @option options [String] :model the value used for the manufacturer's model name tag (0008,1090) in the beam sequence
24
+ # @option options [Symbol] :scale if set, relevant device parameters are converted from native readout format to IEC1217 (supported values are :elekta & :varian)
16
25
  # @option options [String] :serial_number the value used for the device serial number tag (0018,1000) in the beam sequence
17
26
  # @return [DICOM::DObject] the converted DICOM object
18
27
  #
@@ -323,7 +332,11 @@ module RTP
323
332
  # Cumulative Meterset Weight:
324
333
  DICOM::Element.new('300A,0134', '0', :parent => cp_item)
325
334
  # Beam Limiting Device Position Sequence:
326
- create_beam_limiting_device_positions(cp_item, field.control_points.first)
335
+ if field.control_points.length > 0
336
+ create_beam_limiting_device_positions(cp_item, field.control_points.first, options)
337
+ else
338
+ create_beam_limiting_device_positions_from_field(cp_item, field, options)
339
+ end
327
340
  # Referenced Dose Reference Sequence:
328
341
  create_referenced_dose_reference(cp_item) if options[:dose_ref]
329
342
  # Second CP:
@@ -355,93 +368,39 @@ module RTP
355
368
  private
356
369
 
357
370
 
358
- # Adds Collimator Angle elements to a Control Point Item.
371
+ # Adds an angular type value to a Control Point Item, by creating the
372
+ # necessary DICOM elements.
359
373
  # Note that the element is only added if there is no 'current' attribute
360
374
  # defined, or the given value is different form the current attribute.
361
375
  #
362
- # @param [String, NilClass] value1 the collimator angle attribute
363
- # @param [String, NilClass] value2 the collimator rotation direction attribute
364
- # @param [DICOM::Item] item the DICOM control point item in which to create an element
365
- #
366
- def add_collimator(value1, value2, item)
367
- if !@current_collimator || value1 != @current_collimator
368
- @current_collimator = value1
369
- DICOM::Element.new('300A,0120', value1, :parent => item)
370
- DICOM::Element.new('300A,0121', (value2.empty? ? 'NONE' : value2), :parent => item)
376
+ # @param [DICOM::Item] item the DICOM control point item in which to create the elements
377
+ # @param [String] angle_tag the DICOM tag of the angle element
378
+ # @param [String] direction_tag the DICOM tag of the direction element
379
+ # @param [String, NilClass] angle the collimator angle attribute
380
+ # @param [String, NilClass] direction the collimator rotation direction attribute
381
+ # @param [Symbol] current_angle the instance variable that keeps track of the current value of this attribute
382
+ #
383
+ def add_angle(item, angle_tag, direction_tag, angle, direction, current_angle)
384
+ if !self.send(current_angle) || angle != self.send(current_angle)
385
+ self.send("#{current_angle}=", angle)
386
+ DICOM::Element.new(angle_tag, angle, :parent => item)
387
+ DICOM::Element.new(direction_tag, (direction.empty? ? 'NONE' : direction), :parent => item)
371
388
  end
372
389
  end
373
390
 
374
- # Adds Table Top Eccentric Angle elements to a Control Point Item.
391
+ # Adds a Table Top Position element to a Control Point Item.
375
392
  # Note that the element is only added if there is no 'current' attribute
376
393
  # defined, or the given value is different form the current attribute.
377
394
  #
378
- # @param [String, NilClass] value1 the table top eccentric angle attribute
379
- # @param [String, NilClass] value2 the table top eccentric rotation direction attribute
380
- # @param [DICOM::Item] item the DICOM control point item in which to create an element
395
+ # @param [DICOM::Item] item the DICOM control point item in which to create the element
396
+ # @param [String] tag the DICOM tag of the couch position element
397
+ # @param [String, NilClass] value the couch position
398
+ # @param [Symbol] current the instance variable that keeps track of the current value of this attribute
381
399
  #
382
- def add_couch_angle(value1, value2, item)
383
- if !@current_couch_angle || value1 != @current_couch_angle
384
- @current_couch_angle = value1
385
- DICOM::Element.new('300A,0125', value1, :parent => item)
386
- DICOM::Element.new('300A,0126', (value2.empty? ? 'NONE' : value2), :parent => item)
387
- end
388
- end
389
-
390
- # Adds a Table Top Lateral Position element to a Control Point Item.
391
- # Note that the element is only added if there is no 'current' attribute
392
- # defined, or the given value is different form the current attribute.
393
- #
394
- # @param [String, NilClass] value the couch lateral attribute
395
- # @param [DICOM::Item] item the DICOM control point item in which to create an element
396
- #
397
- def add_couch_lateral(value, item)
398
- if !@current_couch_lateral || value != @current_couch_lateral
399
- @current_couch_lateral = value
400
- DICOM::Element.new('300A,012A', (value.empty? ? '' : value.to_f * 10), :parent => item)
401
- end
402
- end
403
-
404
- # Adds a Table Top Longitudinal Position element to a Control Point Item.
405
- # Note that the element is only added if there is no 'current' attribute
406
- # defined, or the given value is different form the current attribute.
407
- #
408
- # @param [String, NilClass] value the couch longitudinal attribute
409
- # @param [DICOM::Item] item the DICOM control point item in which to create an element
410
- #
411
- def add_couch_longitudinal(value, item)
412
- if !@current_couch_longitudinal || value != @current_couch_longitudinal
413
- @current_couch_longitudinal = value
414
- DICOM::Element.new('300A,0129', (value.empty? ? '' : value.to_f * 10), :parent => item)
415
- end
416
- end
417
-
418
- # Adds Patient Support Angle elements to a Control Point Item.
419
- # Note that the element is only added if there is no 'current' attribute
420
- # defined, or the given value is different form the current attribute.
421
- #
422
- # @param [String, NilClass] value1 the patient support angle attribute
423
- # @param [String, NilClass] value2 the patient support rotation direction attribute
424
- # @param [DICOM::Item] item the DICOM control point item in which to create an element
425
- #
426
- def add_couch_pedestal(value1, value2, item)
427
- if !@current_couch_pedestal || value1 != @current_couch_pedestal
428
- @current_couch_pedestal = value1
429
- DICOM::Element.new('300A,0122', value1, :parent => item)
430
- DICOM::Element.new('300A,0123', (value2.empty? ? 'NONE' : value2), :parent => item)
431
- end
432
- end
433
-
434
- # Adds a Table Top Vertical Position element to a Control Point Item.
435
- # Note that the element is only added if there is no 'current' attribute
436
- # defined, or the given value is different form the current attribute.
437
- #
438
- # @param [String, NilClass] value the couch vertical attribute
439
- # @param [DICOM::Item] item the DICOM control point item in which to create an element
440
- #
441
- def add_couch_vertical(value, item)
442
- if !@current_couch_vertical || value != @current_couch_vertical
443
- @current_couch_vertical = value
444
- DICOM::Element.new('300A,0128', (value.empty? ? '' : value.to_f * 10), :parent => item)
400
+ def add_couch_position(item, tag, value, current)
401
+ if !self.send(current) || value != self.send(current)
402
+ self.send("#{current}=", value)
403
+ DICOM::Element.new(tag, (value.empty? ? '' : value.to_f * 10), :parent => item)
445
404
  end
446
405
  end
447
406
 
@@ -473,22 +432,6 @@ module RTP
473
432
  end
474
433
  end
475
434
 
476
- # Adds Gantry Angle elements to a Control Point Item.
477
- # Note that the element is only added if there is no 'current' attribute
478
- # defined, or the given value is different form the current attribute.
479
- #
480
- # @param [String, NilClass] value1 the gantry angle attribute
481
- # @param [String, NilClass] value2 the gantry rotation direction attribute
482
- # @param [DICOM::Item] item the DICOM control point item in which to create an element
483
- #
484
- def add_gantry(value1, value2, item)
485
- if !@current_gantry || value1 != @current_gantry
486
- @current_gantry = value1
487
- DICOM::Element.new('300A,011E', value1, :parent => item)
488
- DICOM::Element.new('300A,011F', (value2.empty? ? 'NONE' : value2), :parent => item)
489
- end
490
- end
491
-
492
435
  # Adds an Isosenter element to a Control Point Item.
493
436
  # Note that the element is only added if there is a Site Setup record present,
494
437
  # and it contains a real (non-empty) value. Also, the element is only added if there
@@ -542,7 +485,7 @@ module RTP
542
485
  # Control Point Index:
543
486
  DICOM::Element.new('300A,0112', "#{cp.index}", :parent => cp_item)
544
487
  # Beam Limiting Device Position Sequence:
545
- create_beam_limiting_device_positions(cp_item, cp)
488
+ create_beam_limiting_device_positions(cp_item, cp, options)
546
489
  # Source to Surface Distance:
547
490
  add_ssd(cp.ssd, cp_item)
548
491
  # Cumulative Meterset Weight:
@@ -555,19 +498,19 @@ module RTP
555
498
  # Dose Rate Set:
556
499
  add_doserate(cp.doserate, cp_item)
557
500
  # Gantry Angle & Rotation Direction:
558
- add_gantry(cp.gantry_angle, cp.gantry_dir, cp_item)
501
+ add_angle(cp_item, '300A,011E', '300A,011F', cp.gantry_angle, cp.gantry_dir, :current_gantry)
559
502
  # Beam Limiting Device Angle & Rotation Direction:
560
- add_collimator(cp.collimator_angle, cp.collimator_dir, cp_item)
503
+ add_angle(cp_item, '300A,0120', '300A,0121', cp.collimator_angle, cp.collimator_dir, :current_collimator)
561
504
  # Patient Support Angle & Rotation Direction:
562
- add_couch_pedestal(cp.couch_pedestal, cp.couch_ped_dir, cp_item)
505
+ add_angle(cp_item, '300A,0122', '300A,0123', cp.couch_pedestal, cp.couch_ped_dir, :current_couch_pedestal)
563
506
  # Table Top Eccentric Angle & Rotation Direction:
564
- add_couch_angle(cp.couch_angle, cp.couch_dir, cp_item)
507
+ add_angle(cp_item, '300A,0125', '300A,0126', cp.couch_angle, cp.couch_dir, :current_couch_angle)
565
508
  # Table Top Vertical Position:
566
- add_couch_vertical(cp.couch_vertical, cp_item)
509
+ add_couch_position(cp_item, '300A,0128', cp.couch_vertical, :current_couch_vertical)
567
510
  # Table Top Longitudinal Position:
568
- add_couch_longitudinal(cp.couch_vertical, cp_item)
511
+ add_couch_position(cp_item, '300A,0129', cp.couch_longitudinal, :current_couch_longitudinal)
569
512
  # Table Top Lateral Position:
570
- add_couch_lateral(cp.couch_vertical, cp_item)
513
+ add_couch_position(cp_item, '300A,012A', cp.couch_lateral, :current_couch_lateral)
571
514
  # Isocenter Position (x\y\z):
572
515
  add_isosenter(cp.parent.parent.site_setup, cp_item)
573
516
  cp_item
@@ -613,26 +556,45 @@ module RTP
613
556
  # @param [ControlPoint] cp the RTP control point to fetch device parameters from
614
557
  # @return [DICOM::Sequence] the constructed beam limiting device positions sequence
615
558
  #
616
- def create_beam_limiting_device_positions(cp_item, cp)
559
+ def create_beam_limiting_device_positions(cp_item, cp, options={})
617
560
  dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item)
618
561
  # The ASYMX item ('backup jaws') doesn't exist on all models:
619
562
  if ['SYM', 'ASY'].include?(cp.parent.field_x_mode.upcase)
620
563
  dp_item_x = DICOM::Item.new(:parent => dp_seq)
621
564
  DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
622
- DICOM::Element.new('300A,011C', "#{cp.dcm_collimator_x1}\\#{cp.dcm_collimator_x2}", :parent => dp_item_x)
565
+ DICOM::Element.new('300A,011C', "#{cp.dcm_collimator_x1(options[:scale])}\\#{cp.dcm_collimator_x2(options[:scale])}", :parent => dp_item_x)
623
566
  end
624
567
  # Always create one ASYMY item:
625
568
  dp_item_y = DICOM::Item.new(:parent => dp_seq)
626
569
  # RT Beam Limiting Device Type:
627
570
  DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
628
571
  # Leaf/Jaw Positions:
629
- DICOM::Element.new('300A,011C', "#{cp.dcm_collimator_y1}\\#{cp.dcm_collimator_y2}", :parent => dp_item_y)
572
+ DICOM::Element.new('300A,011C', "#{cp.dcm_collimator_y1(options[:scale])}\\#{cp.dcm_collimator_y2(options[:scale])}", :parent => dp_item_y)
630
573
  # MLCX:
631
574
  dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
632
575
  # RT Beam Limiting Device Type:
633
576
  DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
634
577
  # Leaf/Jaw Positions:
635
- DICOM::Element.new('300A,011C', cp.dcm_mlc_positions, :parent => dp_item_mlcx)
578
+ DICOM::Element.new('300A,011C', cp.dcm_mlc_positions(options[:scale]), :parent => dp_item_mlcx)
579
+ dp_seq
580
+ end
581
+
582
+ # Creates a beam limiting device positions sequence in the given DICOM object.
583
+ #
584
+ # @param [DICOM::Item] cp_item the DICOM control point item in which to insert the sequence
585
+ # @param [Field] field the RTP treatment field to fetch device parameters from
586
+ # @return [DICOM::Sequence] the constructed beam limiting device positions sequence
587
+ #
588
+ def create_beam_limiting_device_positions_from_field(cp_item, field, options={})
589
+ dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item)
590
+ # ASYMX:
591
+ dp_item_x = DICOM::Item.new(:parent => dp_seq)
592
+ DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
593
+ DICOM::Element.new('300A,011C', "#{field.dcm_collimator_x1}\\#{field.dcm_collimator_x2}", :parent => dp_item_x)
594
+ # ASYMY:
595
+ dp_item_y = DICOM::Item.new(:parent => dp_seq)
596
+ DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
597
+ DICOM::Element.new('300A,011C', "#{field.dcm_collimator_y1}\\#{field.dcm_collimator_y2}", :parent => dp_item_y)
636
598
  dp_seq
637
599
  end
638
600