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.
- checksums.yaml +15 -0
- data/{CHANGELOG.rdoc → CHANGELOG.md} +29 -14
- data/Gemfile.lock +27 -21
- data/{README.rdoc → README.md} +62 -50
- data/lib/rtp-connect.rb +1 -0
- data/lib/rtp-connect/constants.rb +1 -0
- data/lib/rtp-connect/control_point.rb +142 -100
- data/lib/rtp-connect/dose_tracking.rb +31 -36
- data/lib/rtp-connect/extended_field.rb +15 -51
- data/lib/rtp-connect/extended_plan.rb +133 -0
- data/lib/rtp-connect/field.rb +101 -128
- data/lib/rtp-connect/methods.rb +31 -16
- data/lib/rtp-connect/plan.rb +80 -98
- data/lib/rtp-connect/plan_to_dcm.rb +68 -106
- data/lib/rtp-connect/prescription.rb +18 -56
- data/lib/rtp-connect/record.rb +62 -1
- data/lib/rtp-connect/ruby_extensions.rb +34 -3
- data/lib/rtp-connect/simulation_field.rb +58 -136
- data/lib/rtp-connect/site_setup.rb +51 -62
- data/lib/rtp-connect/version.rb +1 -1
- data/rakefile.rb +0 -1
- data/rtp-connect.gemspec +7 -7
- metadata +51 -41
@@ -36,28 +36,8 @@ module RTP
|
|
36
36
|
# @raise [ArgumentError] if given a string containing an invalid number of elements
|
37
37
|
#
|
38
38
|
def self.load(string, parent)
|
39
|
-
# Get the quote-less values:
|
40
|
-
values = string.to_s.values
|
41
|
-
low_limit = 4
|
42
|
-
high_limit = 13
|
43
|
-
raise ArgumentError, "Invalid argument 'string': Expected at least #{low_limit} elements, got #{values.length}." if values.length < low_limit
|
44
|
-
RTP.logger.warn "The number of elements (#{values.length}) for this Prescription 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
|
45
39
|
p = self.new(parent)
|
46
|
-
|
47
|
-
p.keyword = values[0]
|
48
|
-
p.course_id = values[1]
|
49
|
-
p.rx_site_name = values[2]
|
50
|
-
p.technique = values[3]
|
51
|
-
p.modality = values[4]
|
52
|
-
p.dose_spec = values[5]
|
53
|
-
p.rx_depth = values[6]
|
54
|
-
p.dose_ttl = values[7]
|
55
|
-
p.dose_tx = values[8]
|
56
|
-
p.pattern = values[9]
|
57
|
-
p.rx_note = values[10]
|
58
|
-
p.number_of_fields = values[11]
|
59
|
-
p.crc = values[-1]
|
60
|
-
return p
|
40
|
+
p.load(string)
|
61
41
|
end
|
62
42
|
|
63
43
|
# Creates a new Prescription site.
|
@@ -65,6 +45,7 @@ module RTP
|
|
65
45
|
# @param [Record] parent a record which is used to determine the proper parent of this instance
|
66
46
|
#
|
67
47
|
def initialize(parent)
|
48
|
+
super('RX_DEF', 4, 13)
|
68
49
|
# Child objects:
|
69
50
|
@site_setup = nil
|
70
51
|
@fields = Array.new
|
@@ -72,7 +53,22 @@ module RTP
|
|
72
53
|
# Parent relation (may get more than one type of record here):
|
73
54
|
@parent = get_parent(parent.to_record, Plan)
|
74
55
|
@parent.add_prescription(self)
|
75
|
-
@
|
56
|
+
@attributes = [
|
57
|
+
# Required:
|
58
|
+
:keyword,
|
59
|
+
:course_id,
|
60
|
+
:rx_site_name,
|
61
|
+
# Optional:
|
62
|
+
:technique,
|
63
|
+
:modality,
|
64
|
+
:dose_spec,
|
65
|
+
:rx_depth,
|
66
|
+
:dose_ttl,
|
67
|
+
:dose_tx,
|
68
|
+
:pattern,
|
69
|
+
:rx_note,
|
70
|
+
:number_of_fields
|
71
|
+
]
|
76
72
|
end
|
77
73
|
|
78
74
|
# Checks for equality.
|
@@ -133,28 +129,6 @@ module RTP
|
|
133
129
|
state.hash
|
134
130
|
end
|
135
131
|
|
136
|
-
# Collects the values (attributes) of this instance.
|
137
|
-
#
|
138
|
-
# @note The CRC is not considered part of the actual values and is excluded.
|
139
|
-
# @return [Array<String>] an array of attributes (in the same order as they appear in the RTP string)
|
140
|
-
#
|
141
|
-
def values
|
142
|
-
return [
|
143
|
-
@keyword,
|
144
|
-
@course_id,
|
145
|
-
@rx_site_name,
|
146
|
-
@technique,
|
147
|
-
@modality,
|
148
|
-
@dose_spec,
|
149
|
-
@rx_depth,
|
150
|
-
@dose_ttl,
|
151
|
-
@dose_tx,
|
152
|
-
@pattern,
|
153
|
-
@rx_note,
|
154
|
-
@number_of_fields
|
155
|
-
]
|
156
|
-
end
|
157
|
-
|
158
132
|
# Returns self.
|
159
133
|
#
|
160
134
|
# @return [Prescription] self
|
@@ -180,18 +154,6 @@ module RTP
|
|
180
154
|
|
181
155
|
alias :to_str :to_s
|
182
156
|
|
183
|
-
# Sets the keyword attribute.
|
184
|
-
#
|
185
|
-
# @note Since only a specific string is accepted, this is more of an argument check than a traditional setter method
|
186
|
-
# @param [#to_s] value the new attribute value
|
187
|
-
# @raise [ArgumentError] if given an unexpected keyword
|
188
|
-
#
|
189
|
-
def keyword=(value)
|
190
|
-
value = value.to_s.upcase
|
191
|
-
raise ArgumentError, "Invalid keyword. Expected 'RX_DEF', got #{value}." unless value == "RX_DEF"
|
192
|
-
@keyword = value
|
193
|
-
end
|
194
|
-
|
195
157
|
# Sets the course_id attribute.
|
196
158
|
#
|
197
159
|
# @param [nil, #to_s] value the new attribute value
|
data/lib/rtp-connect/record.rb
CHANGED
@@ -10,6 +10,18 @@ module RTP
|
|
10
10
|
# The CRC is used to validate the integrity of the content of the RTP string line.
|
11
11
|
attr_reader :crc
|
12
12
|
|
13
|
+
# Creates a new Record.
|
14
|
+
#
|
15
|
+
# @param [String] keyword the keyword which identifies this record
|
16
|
+
# @param [Integer] min_elements the minimum number of data elements required for this record
|
17
|
+
# @param [Integer] max_elements the maximum supported number of data elements for this record
|
18
|
+
#
|
19
|
+
def initialize(keyword, min_elements, max_elements)
|
20
|
+
@keyword = keyword
|
21
|
+
@min_elements = min_elements
|
22
|
+
@max_elements = max_elements
|
23
|
+
end
|
24
|
+
|
13
25
|
# Sets the crc (checksum) attribute.
|
14
26
|
#
|
15
27
|
# @note This value is not used when creating an RTP string from a record (a new crc is calculated)
|
@@ -26,7 +38,8 @@ module RTP
|
|
26
38
|
# @return [String] a proper RTPConnect type CSV string
|
27
39
|
#
|
28
40
|
def encode
|
29
|
-
|
41
|
+
encoded_values = values.collect {|v| v && v.encode('ISO8859-1')}
|
42
|
+
content = CSV.generate_line(encoded_values, force_quotes: true, row_sep: '') + ","
|
30
43
|
checksum = content.checksum
|
31
44
|
# Complete string is content + checksum (in double quotes) + carriage return + line feed
|
32
45
|
return (content + checksum.to_s.wrap + "\r\n").encode('ISO8859-1')
|
@@ -45,6 +58,32 @@ module RTP
|
|
45
58
|
end
|
46
59
|
end
|
47
60
|
|
61
|
+
# Verifies a proposed keyword attribute.
|
62
|
+
#
|
63
|
+
# @note Since only a specific string is accepted, this is more of an argument check than a traditional setter method.
|
64
|
+
# @param [#to_s] value the proposed keyword attribute
|
65
|
+
# @raise [ArgumentError] if given an unexpected keyword
|
66
|
+
#
|
67
|
+
def keyword=(value)
|
68
|
+
value = value.to_s.upcase
|
69
|
+
raise ArgumentError, "Invalid keyword. Expected '#{@keyword}', got #{value}." unless value == @keyword
|
70
|
+
end
|
71
|
+
|
72
|
+
# Sets up a record by parsing a RTPConnect string line.
|
73
|
+
#
|
74
|
+
# @param [#to_s] string the extended treatment field definition record string line
|
75
|
+
# @return [Record] the updated Record instance
|
76
|
+
# @raise [ArgumentError] if given a string containing an invalid number of elements
|
77
|
+
#
|
78
|
+
def load(string, options={})
|
79
|
+
# Extract processed values:
|
80
|
+
values = string.to_s.values(options[:repair])
|
81
|
+
raise ArgumentError, "Invalid argument 'string': Expected at least #{@min_elements} elements for #{@keyword}, got #{values.length}." if values.length < @min_elements
|
82
|
+
RTP.logger.warn "The number of given elements (#{values.length}) exceeds the known number of data elements for this record (#{@max_elements}). This may indicate an invalid string record or that the RTP format has recently been expanded with new elements." if values.length > @max_elements
|
83
|
+
self.send(:set_attributes, values)
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
48
87
|
# Returns self.
|
49
88
|
#
|
50
89
|
# @return [Record] self
|
@@ -53,6 +92,28 @@ module RTP
|
|
53
92
|
self
|
54
93
|
end
|
55
94
|
|
95
|
+
# Collects the values (attributes) of this instance.
|
96
|
+
#
|
97
|
+
# @note The CRC is not considered part of the actual values and is excluded.
|
98
|
+
# @return [Array<String>] an array of attributes (in the same order as they appear in the RTP string)
|
99
|
+
#
|
100
|
+
def values
|
101
|
+
@attributes.collect {|attribute| self.send(attribute)}
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
|
108
|
+
# Sets the attributes of the record instance.
|
109
|
+
#
|
110
|
+
# @param [Array<String>] values the record attributes (as parsed from a record string)
|
111
|
+
#
|
112
|
+
def set_attributes(values)
|
113
|
+
@attributes.each_index {|i| self.send("#{@attributes[i]}=", values[i])}
|
114
|
+
@crc = values[-1]
|
115
|
+
end
|
116
|
+
|
56
117
|
end
|
57
118
|
|
58
119
|
end
|
@@ -26,6 +26,17 @@ class String
|
|
26
26
|
self.split(',')
|
27
27
|
end
|
28
28
|
|
29
|
+
# Reformats a string, attempting to fix broken CSV format. Note that this
|
30
|
+
# method attempts to fix the CSV in a rather primitive, crude way: Any attributes
|
31
|
+
# containing a " character, will have these characters simply removed.
|
32
|
+
#
|
33
|
+
# @return [String] the processed string
|
34
|
+
#
|
35
|
+
def repair_csv
|
36
|
+
arr = self[1..-2].split('","')
|
37
|
+
"\"#{arr.collect{|e| e.gsub('"', '')}.join('","')}\""
|
38
|
+
end
|
39
|
+
|
29
40
|
# Removes leading & trailing double quotes from a string.
|
30
41
|
#
|
31
42
|
# @return [String] the processed string
|
@@ -38,14 +49,24 @@ class String
|
|
38
49
|
# quotation (leading and trailing double-quote characters) from the extracted
|
39
50
|
# string elements.
|
40
51
|
#
|
52
|
+
# @param [Boolean] repair if true, the method will attempt to repair a string that fails CSV processing, and then try to process it a second time
|
41
53
|
# @return [Array<String>] an array of the comma separated values
|
42
54
|
#
|
43
|
-
def values
|
55
|
+
def values(repair=false)
|
44
56
|
begin
|
45
57
|
CSV.parse(self).first
|
46
58
|
rescue StandardError => e
|
47
|
-
|
48
|
-
|
59
|
+
if repair
|
60
|
+
RTP.logger.warn("CSV processing failed. Will attempt to reformat and reprocess the string record.")
|
61
|
+
begin
|
62
|
+
CSV.parse(self.repair_csv).first
|
63
|
+
rescue StandardError => e
|
64
|
+
RTP.logger.error("Unable to parse the given string record. Probably the CSV format is invalid and beyond repair: #{self}")
|
65
|
+
end
|
66
|
+
else
|
67
|
+
RTP.logger.error("Unable to parse the given string record. Probably invalid CSV format: #{self}")
|
68
|
+
raise e
|
69
|
+
end
|
49
70
|
end
|
50
71
|
end
|
51
72
|
|
@@ -76,6 +97,16 @@ class Array
|
|
76
97
|
return wrapped.join(',')
|
77
98
|
end
|
78
99
|
|
100
|
+
# Validates the number of elements in an array and converts all elements
|
101
|
+
# to strings.
|
102
|
+
#
|
103
|
+
# @param [Integer] nr the required number of elements in the array
|
104
|
+
#
|
105
|
+
def validate_and_process(nr)
|
106
|
+
raise ArgumentError, "Invalid array length. Expected exactly #{nr} elements, got #{self.length}." unless self.length == nr
|
107
|
+
self.collect {|e| e && e.to_s.strip}
|
108
|
+
end
|
109
|
+
|
79
110
|
end
|
80
111
|
|
81
112
|
# An extension to the NilClass, facilitating a transformation from nil to
|
@@ -70,68 +70,8 @@ module RTP
|
|
70
70
|
# @raise [ArgumentError] if given a string containing an invalid number of elements
|
71
71
|
#
|
72
72
|
def self.load(string, parent)
|
73
|
-
# Get the quote-less values:
|
74
|
-
values = string.to_s.values
|
75
|
-
low_limit = 17
|
76
|
-
high_limit = 53
|
77
|
-
raise ArgumentError, "Invalid argument 'string': Expected at least #{low_limit} elements, got #{values.length}." if values.length < low_limit
|
78
|
-
RTP.logger.warn "The number of elements (#{values.length}) for this Simulation Field 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
|
79
73
|
sf = self.new(parent)
|
80
|
-
|
81
|
-
sf.keyword = values[0]
|
82
|
-
sf.rx_site_name = values[1]
|
83
|
-
sf.field_name = values[2]
|
84
|
-
sf.field_id = values[3]
|
85
|
-
sf.field_note = values[4]
|
86
|
-
sf.treatment_machine = values[5]
|
87
|
-
sf.gantry_angle = values[6]
|
88
|
-
sf.collimator_angle = values[7]
|
89
|
-
sf.field_x_mode = values[8]
|
90
|
-
sf.field_x = values[9]
|
91
|
-
sf.collimator_x1 = values[10]
|
92
|
-
sf.collimator_x2 = values[11]
|
93
|
-
sf.field_y_mode = values[12]
|
94
|
-
sf.field_y = values[13]
|
95
|
-
sf.collimator_y1 = values[14]
|
96
|
-
sf.collimator_y2 = values[15]
|
97
|
-
sf.couch_vertical = values[16]
|
98
|
-
sf.couch_lateral = values[17]
|
99
|
-
sf.couch_longitudinal = values[18]
|
100
|
-
sf.couch_angle = values[19]
|
101
|
-
sf.couch_pedestal = values[20]
|
102
|
-
sf.sad = values[21]
|
103
|
-
sf.ap_separation = values[22]
|
104
|
-
sf.pa_separation = values[23]
|
105
|
-
sf.lateral_separation = values[24]
|
106
|
-
sf.tangential_separation = values[25]
|
107
|
-
sf.other_label_1 = values[26]
|
108
|
-
sf.ssd_1 = values[27]
|
109
|
-
sf.sfd_1 = values[28]
|
110
|
-
sf.other_label_2 = values[29]
|
111
|
-
sf.other_measurement_1 = values[30]
|
112
|
-
sf.other_measurement_2 = values[31]
|
113
|
-
sf.other_label_3 = values[32]
|
114
|
-
sf.other_measurement_3 = values[33]
|
115
|
-
sf.other_measurement_4 = values[34]
|
116
|
-
sf.other_label_4 = values[35]
|
117
|
-
sf.other_measurement_5 = values[36]
|
118
|
-
sf.other_measurement_6 = values[37]
|
119
|
-
sf.blade_x_mode = values[38]
|
120
|
-
sf.blade_x = values[39]
|
121
|
-
sf.blade_x1 = values[40]
|
122
|
-
sf.blade_x2 = values[41]
|
123
|
-
sf.blade_y_mode = values[42]
|
124
|
-
sf.blade_y = values[43]
|
125
|
-
sf.blade_y1 = values[44]
|
126
|
-
sf.blade_y2 = values[45]
|
127
|
-
sf.ii_lateral = values[46]
|
128
|
-
sf.ii_longitudinal = values[47]
|
129
|
-
sf.ii_vertical = values[48]
|
130
|
-
sf.kvp = values[49]
|
131
|
-
sf.ma = values[50]
|
132
|
-
sf.seconds = values[51]
|
133
|
-
sf.crc = values[-1]
|
134
|
-
return sf
|
74
|
+
sf.load(string)
|
135
75
|
end
|
136
76
|
|
137
77
|
# Creates a new SimulationField.
|
@@ -139,10 +79,66 @@ module RTP
|
|
139
79
|
# @param [Record] parent a record which is used to determine the proper parent of this instance
|
140
80
|
#
|
141
81
|
def initialize(parent)
|
82
|
+
super('SIM_DEF', 17, 53)
|
142
83
|
# Parent relation (may get more than one type of record here):
|
143
84
|
@parent = get_parent(parent.to_record, Prescription)
|
144
85
|
@parent.add_simulation_field(self)
|
145
|
-
@
|
86
|
+
@attributes = [
|
87
|
+
# Required:
|
88
|
+
:keyword,
|
89
|
+
:rx_site_name,
|
90
|
+
:field_name,
|
91
|
+
:field_id,
|
92
|
+
:field_note,
|
93
|
+
:treatment_machine,
|
94
|
+
:gantry_angle,
|
95
|
+
:collimator_angle,
|
96
|
+
:field_x_mode,
|
97
|
+
:field_x,
|
98
|
+
:collimator_x1,
|
99
|
+
:collimator_x2,
|
100
|
+
:field_y_mode,
|
101
|
+
:field_y,
|
102
|
+
:collimator_y1,
|
103
|
+
:collimator_y2,
|
104
|
+
# Optional:
|
105
|
+
:couch_vertical,
|
106
|
+
:couch_lateral,
|
107
|
+
:couch_longitudinal,
|
108
|
+
:couch_angle,
|
109
|
+
:couch_pedestal,
|
110
|
+
:sad,
|
111
|
+
:ap_separation,
|
112
|
+
:pa_separation,
|
113
|
+
:lateral_separation,
|
114
|
+
:tangential_separation,
|
115
|
+
:other_label_1,
|
116
|
+
:ssd_1,
|
117
|
+
:sfd_1,
|
118
|
+
:other_label_2,
|
119
|
+
:other_measurement_1,
|
120
|
+
:other_measurement_2,
|
121
|
+
:other_label_3,
|
122
|
+
:other_measurement_3,
|
123
|
+
:other_measurement_4,
|
124
|
+
:other_label_4,
|
125
|
+
:other_measurement_5,
|
126
|
+
:other_measurement_6,
|
127
|
+
:blade_x_mode,
|
128
|
+
:blade_x,
|
129
|
+
:blade_x1,
|
130
|
+
:blade_x2,
|
131
|
+
:blade_y_mode,
|
132
|
+
:blade_y,
|
133
|
+
:blade_y1,
|
134
|
+
:blade_y2,
|
135
|
+
:ii_lateral,
|
136
|
+
:ii_longitudinal,
|
137
|
+
:ii_vertical,
|
138
|
+
:kvp,
|
139
|
+
:ma,
|
140
|
+
:seconds
|
141
|
+
]
|
146
142
|
end
|
147
143
|
|
148
144
|
# Checks for equality.
|
@@ -179,68 +175,6 @@ module RTP
|
|
179
175
|
state.hash
|
180
176
|
end
|
181
177
|
|
182
|
-
# Collects the values (attributes) of this instance.
|
183
|
-
#
|
184
|
-
# @note The CRC is not considered part of the actual values and is excluded.
|
185
|
-
# @return [Array<String>] an array of attributes (in the same order as they appear in the RTP string)
|
186
|
-
#
|
187
|
-
def values
|
188
|
-
return [
|
189
|
-
@keyword,
|
190
|
-
@rx_site_name,
|
191
|
-
@field_name,
|
192
|
-
@field_id,
|
193
|
-
@field_note,
|
194
|
-
@treatment_machine,
|
195
|
-
@gantry_angle,
|
196
|
-
@collimator_angle,
|
197
|
-
@field_x_mode,
|
198
|
-
@field_x,
|
199
|
-
@collimator_x1,
|
200
|
-
@collimator_x2,
|
201
|
-
@field_y_mode,
|
202
|
-
@field_y,
|
203
|
-
@collimator_y1,
|
204
|
-
@collimator_y2,
|
205
|
-
@couch_vertical,
|
206
|
-
@couch_lateral,
|
207
|
-
@couch_longitudinal,
|
208
|
-
@couch_angle,
|
209
|
-
@couch_pedestal,
|
210
|
-
@sad,
|
211
|
-
@ap_separation,
|
212
|
-
@pa_separation,
|
213
|
-
@lateral_separation,
|
214
|
-
@tangential_separation,
|
215
|
-
@other_label_1,
|
216
|
-
@ssd_1,
|
217
|
-
@sfd_1,
|
218
|
-
@other_label_2,
|
219
|
-
@other_measurement_1,
|
220
|
-
@other_measurement_2,
|
221
|
-
@other_label_3,
|
222
|
-
@other_measurement_3,
|
223
|
-
@other_measurement_4,
|
224
|
-
@other_label_4,
|
225
|
-
@other_measurement_5,
|
226
|
-
@other_measurement_6,
|
227
|
-
@blade_x_mode,
|
228
|
-
@blade_x,
|
229
|
-
@blade_x1,
|
230
|
-
@blade_x2,
|
231
|
-
@blade_y_mode,
|
232
|
-
@blade_y,
|
233
|
-
@blade_y1,
|
234
|
-
@blade_y2,
|
235
|
-
@ii_lateral,
|
236
|
-
@ii_longitudinal,
|
237
|
-
@ii_vertical,
|
238
|
-
@kvp,
|
239
|
-
@ma,
|
240
|
-
@seconds
|
241
|
-
]
|
242
|
-
end
|
243
|
-
|
244
178
|
# Returns self.
|
245
179
|
#
|
246
180
|
# @return [SimulationField] self
|
@@ -266,18 +200,6 @@ module RTP
|
|
266
200
|
|
267
201
|
alias :to_str :to_s
|
268
202
|
|
269
|
-
# Sets the keyword attribute.
|
270
|
-
#
|
271
|
-
# @note Since only a specific string is accepted, this is more of an argument check than a traditional setter method
|
272
|
-
# @param [#to_s] value the new attribute value
|
273
|
-
# @raise [ArgumentError] if given an unexpected keyword
|
274
|
-
#
|
275
|
-
def keyword=(value)
|
276
|
-
value = value.to_s.upcase
|
277
|
-
raise ArgumentError, "Invalid keyword. Expected 'SIM_DEF', got #{value}." unless value == "SIM_DEF"
|
278
|
-
@keyword = value
|
279
|
-
end
|
280
|
-
|
281
203
|
# Sets the rx_site_name attribute.
|
282
204
|
#
|
283
205
|
# @param [nil, #to_s] value the new attribute value
|