rtp-connect 1.6 → 1.11

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,3 +1,3 @@
1
- source "http://www.rubygems.org"
2
-
1
+ source "http://www.rubygems.org"
2
+
3
3
  gemspec
@@ -1,35 +1,45 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rtp-connect (1.6)
4
+ rtp-connect (1.11)
5
5
 
6
6
  GEM
7
7
  remote: http://www.rubygems.org/
8
8
  specs:
9
- dicom (0.9.5)
10
- diff-lcs (1.2.1)
11
- metaclass (0.0.1)
12
- mocha (0.13.2)
9
+ dicom (0.9.8)
10
+ diff-lcs (1.2.5)
11
+ metaclass (0.0.4)
12
+ mocha (1.1.0)
13
13
  metaclass (~> 0.0.1)
14
- rake (0.9.6)
15
- rspec (2.13.0)
16
- rspec-core (~> 2.13.0)
17
- rspec-expectations (~> 2.13.0)
18
- rspec-mocks (~> 2.13.0)
19
- rspec-core (2.13.0)
20
- rspec-expectations (2.13.0)
21
- diff-lcs (>= 1.1.3, < 2.0)
22
- rspec-mocks (2.13.0)
23
- yard (0.8.5)
14
+ rake (12.3.1)
15
+ redcarpet (3.4.0)
16
+ rspec (3.7.0)
17
+ rspec-core (~> 3.7.0)
18
+ rspec-expectations (~> 3.7.0)
19
+ rspec-mocks (~> 3.7.0)
20
+ rspec-core (3.7.1)
21
+ rspec-support (~> 3.7.0)
22
+ rspec-expectations (3.7.0)
23
+ diff-lcs (>= 1.2.0, < 2.0)
24
+ rspec-support (~> 3.7.0)
25
+ rspec-mocks (3.7.0)
26
+ diff-lcs (>= 1.2.0, < 2.0)
27
+ rspec-support (~> 3.7.0)
28
+ rspec-support (3.7.1)
29
+ yard (0.9.12)
24
30
 
25
31
  PLATFORMS
26
32
  x86-mingw32
27
33
 
28
34
  DEPENDENCIES
29
- bundler (~> 1.3)
30
- dicom (~> 0.9.5)
31
- mocha (~> 0.13)
32
- rake (~> 0.9.6)
33
- rspec (~> 2.13)
35
+ bundler (~> 1.11)
36
+ dicom (~> 0.9, >= 0.9.8)
37
+ mocha (~> 1.1)
38
+ rake (~> 12.3)
39
+ redcarpet (~> 3.4)
40
+ rspec (~> 3.7)
34
41
  rtp-connect!
35
- yard (~> 0.8.5)
42
+ yard (~> 0.9, >= 0.9.12)
43
+
44
+ BUNDLED WITH
45
+ 1.16.5
@@ -0,0 +1,161 @@
1
+ [![DOI](https://zenodo.org/badge/doi/10.5281/zenodo.11752.png)](http://dx.doi.org/10.5281/zenodo.11752)
2
+
3
+ # RTPConnect
4
+
5
+ The RTPConnect library allows you to read, edit and write RTPConnect files in Ruby.
6
+ RTPConnect is a file format used in radiotherapy (e.g. Mosaiq) for export & import
7
+ of treatment planning data. The library is written entirely in Ruby and has no
8
+ external dependencies.
9
+
10
+
11
+ ## INSTALLATION
12
+
13
+ gem install rtp-connect
14
+
15
+
16
+ ## REQUIREMENTS
17
+
18
+ * Ruby 1.9.3 (or higher)
19
+
20
+
21
+ ## BASIC USAGE
22
+
23
+ ### Load & Include
24
+
25
+ require 'rtp-connect'
26
+ include RTP
27
+
28
+ ### Read, modify and write
29
+
30
+ # Read file:
31
+ rtp = Plan.read('some_file.rtp')
32
+ # Extract the Patient's Name:
33
+ name = rtp.patient_last_name
34
+ # Modify the Patient's Name:
35
+ rtp.patient_last_name = 'Anonymous'
36
+ # Write to file:
37
+ rtp.write('new_file.rtp')
38
+
39
+ ### Create a new Plan Definition Record from scratch
40
+
41
+ # Create the instance:
42
+ rtp = Plan.new
43
+ # Set the Patient's ID attribute:
44
+ rtp.patient_id = '12345'
45
+ # Export the instance to an RTP string (with CRC):
46
+ output = rtp.to_s
47
+
48
+ ### Fix invalid RTP files:
49
+
50
+ # Read an RTP file containing invalid checksum(s):
51
+ rtp = Plan.read('invalid_crc.rtp', ignore_crc: true)
52
+ # Read an RTP file containing unknown record type(s):
53
+ rtp = Plan.read('custom.rtp', skip_unknown: true)
54
+ # Read an RTP file containing invalid CSV format:
55
+ rtp = Plan.read('invalid_csv.rtp', repair: true)
56
+ # Write a corrected RTP file:
57
+ rtp.write('valid.rtp')
58
+
59
+ ### Write RTP files for specific Mosaiq versions:
60
+
61
+ # Mosaiq 2.4:
62
+ rtp.write('treatment_plan.rtp', version: 2.4)
63
+ # Mosaiq 2.5:
64
+ rtp.write('treatment_plan.rtp', version: 2.5)
65
+ # Mosaiq 2.6 (and 2.62):
66
+ rtp.write('treatment_plan.rtp', version: 2.6)
67
+ # By default files are outputted at the latest supported version (currently 2.64) when omitting the version parameter:
68
+ rtp.write('treatment_plan.rtp')
69
+
70
+ ### Convert an RTP file to DICOM:
71
+
72
+ p = Plan.read('some_file.rtp')
73
+ dcm = p.to_dcm
74
+ dcm.write('rtplan.dcm')
75
+
76
+ ### Log settings
77
+
78
+ # Change the log level so that only error messages are displayed:
79
+ RTP.logger.level = Logger::ERROR
80
+ # Setting up a simple file log:
81
+ l = Logger.new('my_logfile.log')
82
+ RTP.logger = l
83
+ # Create a logger which ages logfile daily/monthly:
84
+ RTP.logger = Logger.new('foo.log', 'daily')
85
+ RTP.logger = Logger.new('foo.log', 'monthly')
86
+
87
+ ### Scripts
88
+
89
+ For more comprehensive and useful examples, check out the scripts folder
90
+ which contains various Ruby scripts that intends to show off real world
91
+ usage scenarios of the RTPConnect library.
92
+
93
+ ### IRB Tip
94
+
95
+ When working with the RTPConnect library in irb, you may be annoyed with all
96
+ the information that is printed to screen, regardless of your log level.
97
+ This is because in irb every variable loaded in the program is
98
+ automatically printed to the screen. A useful hack to avoid this effect is
99
+ to append ";0" after a command.
100
+
101
+ Example:
102
+
103
+ rtp = Plan.read('some_file.rtp') ;0
104
+
105
+
106
+ ## RESOURCES
107
+
108
+ * [Rubygems download](https://rubygems.org/gems/rtp-connect)
109
+ * [Documentation](http://rubydoc.info/gems/rtp-connect/frames)
110
+ * [Source code repository](https://github.com/dicom/rtp-connect)
111
+
112
+
113
+ ## RESTRICTIONS
114
+
115
+ ### Supported records
116
+
117
+ * Plan definition [PLAN_DEF]
118
+ * Extended plan definition [EXTENDED_PLAN_DEF]
119
+ * Prescription site [RX_DEF]
120
+ * Site setup [SITE_SETUP_DEF]
121
+ * Simulation field [SIM_DEF]
122
+ * Treatment field [FIELD_DEF]
123
+ * Extended treatment field [EXTENDED_FIELD_DEF]
124
+ * Control point record [CONTROL_PT_DEF]
125
+ * Dose tracking record [DOSE_DEF]
126
+
127
+ ### Unsupported records
128
+
129
+ * Document based treatment field [PDF_FIELD_DEF]
130
+ * Multileaf collimator [MLC_DEF]
131
+ * MLC shape [MLC_SHAPE_DEF]
132
+ * Dose action points [DOSE_ACTION]
133
+
134
+ If you encounter an RTP file with an unsupported record type, please contact me.
135
+
136
+
137
+ ## COPYRIGHT
138
+
139
+ Copyright 2011-2020 Christoffer Lervåg
140
+
141
+ This program is free software: you can redistribute it and/or modify
142
+ it under the terms of the GNU General Public License as published by
143
+ the Free Software Foundation, either version 3 of the License, or
144
+ (at your option) any later version.
145
+
146
+ This program is distributed in the hope that it will be useful,
147
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
148
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
149
+ GNU General Public License for more details.
150
+
151
+ You should have received a copy of the GNU General Public License
152
+ along with this program. If not, see http://www.gnu.org/licenses/ .
153
+
154
+
155
+ ## ABOUT THE AUTHOR
156
+
157
+ * Name: Christoffer Lervåg
158
+ * Location: Norway
159
+ * Email: chris.lervag [@nospam.com] @gmail.com
160
+
161
+ Please don't hesitate to email me if you have any feedback related to this project!
@@ -6,6 +6,7 @@ require_relative 'rtp-connect/logging'
6
6
  require_relative 'rtp-connect/record'
7
7
  # Core library:
8
8
  require_relative 'rtp-connect/plan'
9
+ require_relative 'rtp-connect/extended_plan'
9
10
  require_relative 'rtp-connect/plan_to_dcm'
10
11
  require_relative 'rtp-connect/prescription'
11
12
  require_relative 'rtp-connect/site_setup'
@@ -1,58 +1,59 @@
1
- module RTP
2
-
3
- # The seed value used in the RTPConnect implementation of the CCITT algorithm.
4
- CRC_SEED = 0x0521
5
-
6
- # The table & values used in the RTPConnect implementation of the CCITT algorithm.
7
- CRC_TABLE = [
8
- 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
9
- 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
10
- 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
11
- 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
12
- 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
13
- 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
14
- 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
15
- 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
16
- 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
17
- 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
18
- 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
19
- 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
20
- 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
21
- 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
22
- 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
23
- 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
24
- 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
25
- 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
26
- 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
27
- 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
28
- 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
29
- 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
30
- 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
31
- 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
32
- 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
33
- 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
34
- 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
35
- 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
36
- 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
37
- 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
38
- 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
39
- 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040
40
- ]
41
-
42
- # Pairs of RTPConnect keywords and parse method names.
43
- PARSE_METHOD = {
44
- "PLAN_DEF" => :plan_definition,
45
- "RX_DEF" => :prescription_site,
46
- "SITE_SETUP_DEF" => :site_setup,
47
- "SIM_DEF" => :simulation_field,
48
- "FIELD_DEF" => :treatment_field,
49
- "EXTENDED_FIELD_DEF" => :extended_treatment_field,
50
- "PDF_FIELD_DEF" => :document_based_treatment_field,
51
- "MLC_DEF" => :multileaf_collimator,
52
- "CONTROL_PT_DEF" => :control_point,
53
- "MLC_SHAPE_DEF" => :mlc_shape,
54
- "DOSE_DEF" => :dose_tracking,
55
- "DOSE_ACTION" => :dose_action,
56
- }
57
-
1
+ module RTP
2
+
3
+ # The seed value used in the RTPConnect implementation of the CCITT algorithm.
4
+ CRC_SEED = 0x0521
5
+
6
+ # The table & values used in the RTPConnect implementation of the CCITT algorithm.
7
+ CRC_TABLE = [
8
+ 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
9
+ 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
10
+ 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
11
+ 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
12
+ 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
13
+ 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
14
+ 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
15
+ 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
16
+ 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
17
+ 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
18
+ 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
19
+ 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
20
+ 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
21
+ 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
22
+ 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
23
+ 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
24
+ 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
25
+ 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
26
+ 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
27
+ 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
28
+ 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
29
+ 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
30
+ 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
31
+ 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
32
+ 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
33
+ 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
34
+ 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
35
+ 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
36
+ 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
37
+ 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
38
+ 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
39
+ 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040
40
+ ]
41
+
42
+ # Pairs of RTPConnect keywords and parse method names.
43
+ PARSE_METHOD = {
44
+ "PLAN_DEF" => :plan_definition,
45
+ "EXTENDED_PLAN_DEF" => :extended_plan_def,
46
+ "RX_DEF" => :prescription_site,
47
+ "SITE_SETUP_DEF" => :site_setup,
48
+ "SIM_DEF" => :simulation_field,
49
+ "FIELD_DEF" => :treatment_field,
50
+ "EXTENDED_FIELD_DEF" => :extended_treatment_field,
51
+ "PDF_FIELD_DEF" => :document_based_treatment_field,
52
+ "MLC_DEF" => :multileaf_collimator,
53
+ "CONTROL_PT_DEF" => :control_point,
54
+ "MLC_SHAPE_DEF" => :mlc_shape,
55
+ "DOSE_DEF" => :dose_tracking,
56
+ "DOSE_ACTION" => :dose_action,
57
+ }
58
+
58
59
  end
@@ -8,8 +8,11 @@ module RTP
8
8
  #
9
9
  class ControlPoint < Record
10
10
 
11
+ # The number of attributes not having their own variable for this record (200 - 2).
12
+ NR_SURPLUS_ATTRIBUTES = 198
13
+
11
14
  # The Record which this instance belongs to.
12
- attr_reader :parent
15
+ attr_accessor :parent
13
16
  # The MLC shape record (if any) that belongs to this ControlPoint.
14
17
  attr_reader :mlc_shape
15
18
  attr_reader :field_id
@@ -43,6 +46,9 @@ module RTP
43
46
  attr_reader :couch_dir
44
47
  attr_reader :couch_pedestal
45
48
  attr_reader :couch_ped_dir
49
+ attr_reader :iso_pos_x
50
+ attr_reader :iso_pos_y
51
+ attr_reader :iso_pos_z
46
52
  # Note: This attribute contains an array of all MLC LP A values (leaves 1..100).
47
53
  attr_reader :mlc_lp_a
48
54
  # Note: This attribute contains an array of all MLC LP B values (leaves 1..100).
@@ -56,50 +62,8 @@ module RTP
56
62
  # @raise [ArgumentError] if given a string containing an invalid number of elements
57
63
  #
58
64
  def self.load(string, parent)
59
- # Get the quote-less values:
60
- values = string.to_s.values
61
- low_limit = 233
62
- high_limit = 233
63
- raise ArgumentError, "Invalid argument 'string': Expected at least #{low_limit} elements, got #{values.length}." if values.length < low_limit
64
- RTP.logger.warn "The number of elements (#{values.length}) for this ControlPoint 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
65
65
  cp = self.new(parent)
66
- # Assign the values to attributes:
67
- cp.keyword = values[0]
68
- cp.field_id = values[1]
69
- cp.mlc_type = values[2]
70
- cp.mlc_leaves = values[3]
71
- cp.total_control_points = values[4]
72
- cp.control_pt_number = values[5]
73
- cp.mu_convention = values[6]
74
- cp.monitor_units = values[7]
75
- cp.wedge_position = values[8]
76
- cp.energy = values[9]
77
- cp.doserate = values[10]
78
- cp.ssd = values[11]
79
- cp.scale_convention = values[12]
80
- cp.gantry_angle = values[13]
81
- cp.gantry_dir = values[14]
82
- cp.collimator_angle = values[15]
83
- cp.collimator_dir = values[16]
84
- cp.field_x_mode = values[17]
85
- cp.field_x = values[18]
86
- cp.collimator_x1 = values[19]
87
- cp.collimator_x2 = values[20]
88
- cp.field_y_mode = values[21]
89
- cp.field_y = values[22]
90
- cp.collimator_y1 = values[23]
91
- cp.collimator_y2 = values[24]
92
- cp.couch_vertical = values[25]
93
- cp.couch_lateral = values[26]
94
- cp.couch_longitudinal = values[27]
95
- cp.couch_angle = values[28]
96
- cp.couch_dir = values[29]
97
- cp.couch_pedestal = values[30]
98
- cp.couch_ped_dir = values[31]
99
- cp.mlc_lp_a = [*values[32..131]]
100
- cp.mlc_lp_b = [*values[132..231]]
101
- cp.crc = values[-1]
102
- return cp
66
+ cp.load(string)
103
67
  end
104
68
 
105
69
  # Creates a new ControlPoint.
@@ -107,14 +71,54 @@ module RTP
107
71
  # @param [Record] parent a record which is used to determine the proper parent of this instance
108
72
  #
109
73
  def initialize(parent)
74
+ super('CONTROL_PT_DEF', 233, 236)
110
75
  # Child:
111
76
  @mlc_shape = nil
112
77
  # Parent relation (may get more than one type of record here):
113
78
  @parent = get_parent(parent.to_record, Field)
114
79
  @parent.add_control_point(self)
115
- @keyword = 'CONTROL_PT_DEF'
116
80
  @mlc_lp_a = Array.new(100)
117
81
  @mlc_lp_b = Array.new(100)
82
+ @attributes = [
83
+ # Required:
84
+ :keyword,
85
+ :field_id,
86
+ :mlc_type,
87
+ :mlc_leaves,
88
+ :total_control_points,
89
+ :control_pt_number,
90
+ :mu_convention,
91
+ :monitor_units,
92
+ :wedge_position,
93
+ :energy,
94
+ :doserate,
95
+ :ssd,
96
+ :scale_convention,
97
+ :gantry_angle,
98
+ :gantry_dir,
99
+ :collimator_angle,
100
+ :collimator_dir,
101
+ :field_x_mode,
102
+ :field_x,
103
+ :collimator_x1,
104
+ :collimator_x2,
105
+ :field_y_mode,
106
+ :field_y,
107
+ :collimator_y1,
108
+ :collimator_y2,
109
+ :couch_vertical,
110
+ :couch_lateral,
111
+ :couch_longitudinal,
112
+ :couch_angle,
113
+ :couch_dir,
114
+ :couch_pedestal,
115
+ :couch_ped_dir,
116
+ :iso_pos_x,
117
+ :iso_pos_y,
118
+ :iso_pos_z,
119
+ :mlc_lp_a,
120
+ :mlc_lp_b
121
+ ]
118
122
  end
119
123
 
120
124
  # Checks for equality.
@@ -145,51 +149,51 @@ module RTP
145
149
 
146
150
  # Converts the collimator_x1 attribute to proper DICOM format.
147
151
  #
152
+ # @param [Symbol] scale if set, relevant device parameters are converted from a native readout format to IEC1217 (supported values are :elekta & :varian)
148
153
  # @return [Float] the DICOM-formatted collimator_x1 attribute
149
154
  #
150
- def dcm_collimator_x1
151
- attribute = (scale_convertion? ? :collimator_y1 : :collimator_x1)
152
- target = (@field_x_mode && !@field_x_mode.empty? ? self : @parent)
153
- target.send(attribute).to_f * 10 * scale_factor
155
+ def dcm_collimator_x1(scale=nil)
156
+ dcm_collimator_1(scale, default_axis=:x)
154
157
  end
155
158
 
156
159
  # Converts the collimator_x2 attribute to proper DICOM format.
157
160
  #
161
+ # @param [Symbol] scale if set, relevant device parameters are converted from native readout format to IEC1217 (supported values are :elekta & :varian)
158
162
  # @return [Float] the DICOM-formatted collimator_x2 attribute
159
163
  #
160
- def dcm_collimator_x2
161
- attribute = (scale_convertion? ? :collimator_y2 : :collimator_x2)
162
- target = (@field_x_mode && !@field_x_mode.empty? ? self : @parent)
163
- target.send(attribute).to_f * 10
164
+ def dcm_collimator_x2(scale=nil)
165
+ axis = (scale == :elekta ? :y : :x)
166
+ dcm_collimator(axis, coeff=1, side=2)
164
167
  end
165
168
 
166
169
  # Converts the collimator_y1 attribute to proper DICOM format.
167
170
  #
171
+ # @param [Symbol] scale if set, relevant device parameters are converted from native readout format to IEC1217 (supported values are :elekta & :varian)
168
172
  # @return [Float] the DICOM-formatted collimator_y1 attribute
169
173
  #
170
- def dcm_collimator_y1
171
- attribute = (scale_convertion? ? :collimator_x1 : :collimator_y1)
172
- target = (@field_y_mode && !@field_y_mode.empty? ? self : @parent)
173
- target.send(attribute).to_f * 10 * scale_factor
174
+ def dcm_collimator_y1(scale=nil)
175
+ dcm_collimator_1(scale, default_axis=:y)
174
176
  end
175
177
 
176
178
  # Converts the collimator_y2 attribute to proper DICOM format.
177
179
  #
180
+ # @param [Symbol] scale if set, relevant device parameters are converted from native readout format to IEC1217 (supported values are :elekta & :varian)
178
181
  # @return [Float] the DICOM-formatted collimator_y2 attribute
179
182
  #
180
- def dcm_collimator_y2
181
- attribute = (scale_convertion? ? :collimator_x2 : :collimator_y2)
182
- target = (@field_y_mode && !@field_y_mode.empty? ? self : @parent)
183
- target.send(attribute).to_f * 10
183
+ def dcm_collimator_y2(scale=nil)
184
+ axis = (scale == :elekta ? :x : :y)
185
+ dcm_collimator(axis, coeff=1, side=2)
184
186
  end
185
187
 
186
188
  # Converts the mlc_lp_a & mlc_lp_b attributes to a proper DICOM formatted string.
187
189
  #
190
+ # @param [Symbol] scale if set, relevant device parameters are converted from native readout format to IEC1217 (supported values are :elekta & :varian)
188
191
  # @return [String] the DICOM-formatted leaf pair positions
189
192
  #
190
- def dcm_mlc_positions
193
+ def dcm_mlc_positions(scale=nil)
194
+ coeff = (scale == :elekta ? -1 : 1)
191
195
  # As with the collimators, the first side (1/a) may need scale invertion:
192
- pos_a = @mlc_lp_a.collect{|p| (p.to_f * 10 * scale_factor).round(1) unless p.empty?}.compact
196
+ pos_a = @mlc_lp_a.collect{|p| (p.to_f * 10 * coeff).round(1) unless p.empty?}.compact
193
197
  pos_b = @mlc_lp_b.collect{|p| (p.to_f * 10).round(1) unless p.empty?}.compact
194
198
  (pos_a + pos_b).join("\\")
195
199
  end
@@ -219,7 +223,7 @@ module RTP
219
223
  # @return [Array<String>] an array of attributes (in the same order as they appear in the RTP string)
220
224
  #
221
225
  def values
222
- return [
226
+ [
223
227
  @keyword,
224
228
  @field_id,
225
229
  @mlc_type,
@@ -252,6 +256,9 @@ module RTP
252
256
  @couch_dir,
253
257
  @couch_pedestal,
254
258
  @couch_ped_dir,
259
+ @iso_pos_x,
260
+ @iso_pos_y,
261
+ @iso_pos_z,
255
262
  *@mlc_lp_a,
256
263
  *@mlc_lp_b
257
264
  ]
@@ -265,23 +272,6 @@ module RTP
265
272
  self
266
273
  end
267
274
 
268
- # Encodes the ControlPoint object + any hiearchy of child objects,
269
- # to a properly formatted RTPConnect ascii string.
270
- #
271
- # @return [String] an RTP string with a single or multiple lines/records
272
- #
273
- def to_s
274
- str = encode
275
- if children
276
- children.each do |child|
277
- str += child.to_s
278
- end
279
- end
280
- return str
281
- end
282
-
283
- alias :to_str :to_s
284
-
285
275
  # Sets the mlc_lp_a attribute.
286
276
  #
287
277
  # @note As opposed to the ordinary (string) attributes, this attribute
@@ -289,9 +279,7 @@ module RTP
289
279
  # @param [Array<nil, #to_s>] array the new attribute values
290
280
  #
291
281
  def mlc_lp_a=(array)
292
- array = array.to_a
293
- raise ArgumentError, "Invalid argument 'array'. Expected length 100, got #{array.length}." unless array.length == 100
294
- @mlc_lp_a = array.collect! {|e| e && e.to_s.strip}
282
+ @mlc_lp_a = array.to_a.validate_and_process(100)
295
283
  end
296
284
 
297
285
  # Sets the mlc_lp_b attribute.
@@ -301,21 +289,7 @@ module RTP
301
289
  # @param [Array<nil, #to_s>] array the new attribute values
302
290
  #
303
291
  def mlc_lp_b=(array)
304
- array = array.to_a
305
- raise ArgumentError, "Invalid argument 'array'. Expected length 100, got #{array.length}." unless array.length == 100
306
- @mlc_lp_b = array.collect! {|e| e && e.to_s.strip}
307
- end
308
-
309
- # Sets the keyword attribute.
310
- #
311
- # @note Since only a specific string is accepted, this is more of an argument check than a traditional setter method
312
- # @param [#to_s] value the new attribute value
313
- # @raise [ArgumentError] if given an unexpected keyword
314
- #
315
- def keyword=(value)
316
- value = value.to_s.upcase
317
- raise ArgumentError, "Invalid keyword. Expected 'CONTROL_PT_DEF', got #{value}." unless value == "CONTROL_PT_DEF"
318
- @keyword = value
292
+ @mlc_lp_b = array.to_a.validate_and_process(100)
319
293
  end
320
294
 
321
295
  # Sets the field_id attribute.
@@ -566,6 +540,30 @@ module RTP
566
540
  @couch_ped_dir = value && value.to_s
567
541
  end
568
542
 
543
+ # Sets the iso_pos_x attribute.
544
+ #
545
+ # @param [nil, #to_s] value the new attribute value
546
+ #
547
+ def iso_pos_x=(value)
548
+ @iso_pos_x = value && value.to_s.strip
549
+ end
550
+
551
+ # Sets the iso_pos_y attribute.
552
+ #
553
+ # @param [nil, #to_s] value the new attribute value
554
+ #
555
+ def iso_pos_y=(value)
556
+ @iso_pos_y = value && value.to_s.strip
557
+ end
558
+
559
+ # Sets the iso_pos_z attribute.
560
+ #
561
+ # @param [nil, #to_s] value the new attribute value
562
+ #
563
+ def iso_pos_z=(value)
564
+ @iso_pos_z = value && value.to_s.strip
565
+ end
566
+
569
567
 
570
568
  private
571
569
 
@@ -577,31 +575,73 @@ module RTP
577
575
  #
578
576
  alias_method :state, :values
579
577
 
580
- # Checks whether the contents of the this record indicates that scale
581
- # convertion is to be applied. This convertion entails converting a value
582
- # from IEC1217 format to the target machine's native readout format.
583
- # Note that the scope of this scale conversion is not precisely known (the
584
- # current implementation is based on a few observations made from a single
585
- # RTP file).
578
+ # Converts the collimator attribute to proper DICOM format.
586
579
  #
587
- # @return [Boolean] true if the scale convention attribute indicates scale convertion
580
+ # @param [Symbol] axis a representation for the axis of interest (x or y)
581
+ # @param [Integer] coeff a coeffecient (of -1 or 1) which the attribute is multiplied with
582
+ # @param [Integer] nr collimator side/index (1 or 2)
583
+ # @return [Float] the DICOM-formatted collimator attribute
588
584
  #
589
- def scale_convertion?
590
- # A scale convention of 1 means that geometric parameters are represented
591
- # in the target machine's native readout format, as opposed to the IEC 1217
592
- # convention. The consequences of this is not totally clear, but at least for
593
- # an Elekta device, there are a number of convertions which seems to be indicated.
594
- @scale_convention.to_i == 1 ? true : false
585
+ def dcm_collimator(axis, coeff, nr)
586
+ mode = self.send("field_#{axis}_mode")
587
+ if mode && !mode.empty?
588
+ target = self
589
+ else
590
+ target = @parent
591
+ end
592
+ target.send("collimator_#{axis}#{nr}").to_f * 10 * coeff
595
593
  end
596
594
 
597
- # Gives a factor used for scale convertion, which depends on the
598
- # 'scale_convention' attribute.
595
+ # Converts the collimator1 attribute to proper DICOM format.
599
596
  #
600
- # @param [Numerical] value the value to process
601
- # @return [Numerical] the scale converted value
597
+ # @param [Symbol] scale if set, relevant device parameters are converted from a native readout format to IEC1217 (supported values are :elekta & :varian)
598
+ # @return [Float] the DICOM-formatted collimator_x1 attribute
602
599
  #
603
- def scale_factor
604
- scale_convertion? ? -1 : 1
600
+ def dcm_collimator_1(scale=nil, axis)
601
+ coeff = 1
602
+ if scale == :elekta
603
+ axis = (axis == :x ? :y : :x)
604
+ coeff = -1
605
+ elsif scale == :varian
606
+ coeff = -1
607
+ end
608
+ dcm_collimator(axis, coeff, side=1)
609
+ end
610
+
611
+ # Gives an array of indices indicating where the attributes of this record gets its
612
+ # values from in the comma separated string which the instance is created from.
613
+ #
614
+ # @param [Integer] length the number of elements to create in the indices array
615
+ #
616
+ def import_indices(length)
617
+ # Note that this method is defined in the parent Record class, where it is
618
+ # used for most record types. However, because this record has two attributes
619
+ # which contain an array of values, we use a custom import_indices method.
620
+ #
621
+ # Furthermore, as of Mosaiq version 2.64, the RTP ControlPoint record includes
622
+ # 3 new attributes: iso_pos_x/y/z. Since these (unfortunately) are not placed
623
+ # at the end of the record (which is the norm), but rather inserted before the
624
+ # MLC leaf positions, we have to take special care here to make sure that this
625
+ # gets right for records where these are included or excluded.
626
+ #
627
+ # Override length:
628
+ applied_length = 235
629
+ ind = Array.new(applied_length - NR_SURPLUS_ATTRIBUTES) { |i| [i] }
630
+ # Override indices for mlc_pl_a and mlc_lp_b:
631
+ # Allocation here is dependent on the RTP file version:
632
+ # For 2.62 and earlier, where length is 232, we dont have the 3 iso_pos_x/y/z values preceeding the mlc arrays leaf position arrays.
633
+ # For 2.64 (and later), where length is 235, we have the 3 iso_pos_x/y/z values preceeding the mlc leaf position arrays.
634
+ if length == 232
635
+ ind[32] = nil
636
+ ind[33] = nil
637
+ ind[34] = nil
638
+ ind[35] = (32..131).to_a
639
+ ind[36] = (132..231).to_a
640
+ else # (length = 235)
641
+ ind[35] = (35..134).to_a
642
+ ind[36] = (135..234).to_a
643
+ end
644
+ ind
605
645
  end
606
646
 
607
647
  end