rtp-connect 1.6 → 1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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