rtkit 0.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.
@@ -0,0 +1,237 @@
1
+ module RTKIT
2
+
3
+ # The RTDose class contains methods that are specific for this modality (RTDOSE).
4
+ #
5
+ # === Inheritance
6
+ #
7
+ # * RTDose inherits all methods and attributes from the Series class.
8
+ #
9
+ class RTDose < Series
10
+
11
+ # The Plan which this RTDose series belongs to.
12
+ attr_reader :plan
13
+ # An array of dose Volume instances associated with this RTDose series.
14
+ attr_accessor :volumes
15
+
16
+ # Creates a new RTDose instance by loading the relevant information from the specified DICOM object.
17
+ # The Series Instance UID string value is used to uniquely identify a RTDose instance.
18
+ #
19
+ # === Parameters
20
+ #
21
+ # * <tt>dcm</tt> -- An instance of a DICOM object (DICOM::DObject) with modality 'RTDOSE'.
22
+ # * <tt>study</tt> -- The Study instance that this RTDose belongs to.
23
+ #
24
+ def self.load(dcm, study)
25
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
26
+ raise ArgumentError, "Invalid argument 'study'. Expected Study, got #{study.class}." unless study.is_a?(Study)
27
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject with modality 'RTDOSE', got #{dcm.value(MODALITY)}." unless dcm.value(MODALITY) == 'RTDOSE'
28
+ # Required attributes:
29
+ series_uid = dcm.value(SERIES_UID)
30
+ # Optional attributes:
31
+ class_uid = dcm.value(SOP_CLASS)
32
+ date = dcm.value(SERIES_DATE)
33
+ time = dcm.value(SERIES_TIME)
34
+ description = dcm.value(SERIES_DESCR)
35
+ series_uid = dcm.value(SERIES_UID)
36
+ # Get the corresponding Plan:
37
+ plan = self.plan(dcm, study)
38
+ # Create the RTDose instance:
39
+ dose = self.new(series_uid, plan, :class_uid => class_uid, :date => date, :time => time, :description => description)
40
+ dose.add(dcm)
41
+ return dose
42
+ end
43
+
44
+ # Identifies the Plan that the RTDose object belongs to.
45
+ # If the referenced instances (Plan, StructureSet, ImageSeries & Frame) does not exist, they are created by this method.
46
+ #
47
+ def self.plan(dcm, study)
48
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
49
+ raise ArgumentError, "Invalid argument 'study'. Expected Study, got #{study.class}." unless study.is_a?(Study)
50
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject with modality 'RTDOSE', got #{dcm.value(MODALITY)}." unless dcm.value(MODALITY) == 'RTDOSE'
51
+ # Extract the Frame of Reference UID:
52
+ begin
53
+ frame_of_ref = dcm.value(FRAME_OF_REF)
54
+ rescue
55
+ frame_of_ref = nil
56
+ end
57
+ # Extract referenced Plan SOP Instance UID:
58
+ begin
59
+ ref_plan_uid = dcm[REF_PLAN_SQ][0].value(REF_SOP_UID)
60
+ rescue
61
+ ref_plan_uid = nil
62
+ end
63
+ # Create the Frame if it doesn't exist:
64
+ f = study.patient.dataset.frame(frame_of_ref)
65
+ f = Frame.new(frame_of_ref, study.patient) unless f
66
+ # Create the Plan, StructureSet & ImageSeries if the referenced Plan doesn't exist:
67
+ plan = study.fseries(ref_plan_uid)
68
+ unless plan
69
+ # Create ImageSeries (assuming modality CT):
70
+ is = ImageSeries.new(RTKIT.series_uid, 'CT', f, study)
71
+ study.add_series(is)
72
+ # Create StructureSet:
73
+ struct = StructureSet.new(RTKIT.sop_uid, is)
74
+ study.add_series(struct)
75
+ # Create Plan:
76
+ plan = Plan.new(ref_plan_uid, struct)
77
+ study.add_series(plan)
78
+ end
79
+ return plan
80
+ end
81
+
82
+ # Creates a new RTDose instance.
83
+ #
84
+ # === Parameters
85
+ #
86
+ # * <tt>series_uid</tt> -- The Series Instance UID string.
87
+ # * <tt>plan</tt> -- The Plan instance that this RTDose series belongs to.
88
+ # * <tt>options</tt> -- A hash of parameters.
89
+ #
90
+ # === Options
91
+ #
92
+ # * <tt>:date</tt> -- String. The Series Date (DICOM tag '0008,0021').
93
+ # * <tt>:time</tt> -- String. The Series Time (DICOM tag '0008,0031').
94
+ # * <tt>:description</tt> -- String. The Series Description (DICOM tag '0008,103E').
95
+ #
96
+ def initialize(series_uid, plan, options={})
97
+ raise ArgumentError, "Invalid argument 'series_uid'. Expected String, got #{series_uid.class}." unless series_uid.is_a?(String)
98
+ raise ArgumentError, "Invalid argument 'plan'. Expected Plan, got #{plan.class}." unless plan.is_a?(Plan)
99
+ # Pass attributes to Series initialization:
100
+ options[:class_uid] = '1.2.840.10008.5.1.4.1.1.481.2' # RT Dose Storage
101
+ super(series_uid, 'RTDOSE', plan.study, options)
102
+ @plan = plan
103
+ # Default attributes:
104
+ @volumes = Array.new
105
+ @associated_volumes = Hash.new
106
+ # Register ourselves with the Plan:
107
+ @plan.add_rt_dose(self)
108
+ end
109
+
110
+ # Returns true if the argument is an instance with attributes equal to self.
111
+ #
112
+ def ==(other)
113
+ if other.respond_to?(:to_rt_dose)
114
+ other.send(:state) == state
115
+ end
116
+ end
117
+
118
+ alias_method :eql?, :==
119
+
120
+ # Registers a DICOM Object to the RTDose series, and processes it
121
+ # to create (and reference) a DoseVolume instance linked to this RTDose series.
122
+ #
123
+ def add(dcm)
124
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
125
+ DoseVolume.load(dcm, self)
126
+ end
127
+
128
+ # Adds a DoseVolume instance to this RTDose series.
129
+ #
130
+ def add_volume(volume)
131
+ raise ArgumentError, "Invalid argument 'volume'. Expected DoseVolume, got #{volume.class}." unless volume.is_a?(DoseVolume)
132
+ @volumes << volume unless @associated_volumes[volume.uid]
133
+ @associated_volumes[volume.uid] = volume
134
+ end
135
+
136
+ # Generates a Fixnum hash value for this instance.
137
+ #
138
+ def hash
139
+ state.hash
140
+ end
141
+
142
+ # Returns a DoseVolume which is the sum of the volumes of this instance.
143
+ # With the individual DoseVolumes corresponding to the dose for a particular
144
+ # beam, the sum DoseVolume corresponds to the summed dose of the entire
145
+ # treatment plan.
146
+ #
147
+ def sum
148
+ if @sum
149
+ # If the sum volume has already been created, return it instead of recreating:
150
+ return @sum
151
+ else
152
+ if @volumes.length > 0
153
+ nr_frames = @volumes.first.images.length
154
+ # Create the sum DoseVolume instance:
155
+ sop_uid = @volumes.first.sop_uid + ".1"
156
+ @sum = DoseVolume.new(sop_uid, @volumes.first.frame, @volumes.first.dose_series, :sum => true)
157
+ # Sum the dose of the various DoseVolumes:
158
+ dose_sum = NArray.int(nr_frames, @volumes.first.images.first.columns, @volumes.first.images.first.rows)
159
+ @volumes.each { |volume| dose_sum += volume.dose_arr }
160
+ # Convert dose float array to integer pixel values of a suitable range,
161
+ # along with a corresponding scaling factor:
162
+ sum_scaling_coeff = dose_sum.max / 65000.0
163
+ if sum_scaling_coeff == 0.0
164
+ pixel_values = NArray.int(nr_frames, @volumes.first.images.first.columns, @volumes.first.images.first.rows)
165
+ else
166
+ pixel_values = dose_sum * (1 / sum_scaling_coeff)
167
+ end
168
+ # Set the scaling coeffecient:
169
+ @sum.scaling = sum_scaling_coeff
170
+ # Collect the rest of the image information needed to create new dose images:
171
+ sop_uids = RTKIT.sop_uids(nr_frames)
172
+ slice_positions = @volumes.first.images.collect {|img| img.pos_slice}
173
+ columns = @volumes.first.images.first.columns
174
+ rows = @volumes.first.images.first.rows
175
+ pos_x = @volumes.first.images.first.pos_x
176
+ pos_y = @volumes.first.images.first.pos_y
177
+ col_spacing = @volumes.first.images.first.col_spacing
178
+ row_spacing = @volumes.first.images.first.row_spacing
179
+ cosines = @volumes.first.images.first.cosines
180
+ # Create dose images for our sum dose volume:
181
+ nr_frames.times do |i|
182
+ img = Image.new(sop_uids[i], @sum)
183
+ # Fill in image information:
184
+ img.columns = columns
185
+ img.rows = rows
186
+ img.pos_x = pos_x
187
+ img.pos_y = pos_y
188
+ img.pos_slice = slice_positions[i]
189
+ img.col_spacing = col_spacing
190
+ img.row_spacing = row_spacing
191
+ img.cosines = cosines
192
+ # Fill in the pixel frame data:
193
+ img.narray = pixel_values[i, true, true]
194
+ end
195
+ return @sum
196
+ end
197
+ end
198
+ end
199
+
200
+ # Returns self.
201
+ #
202
+ def to_rt_dose
203
+ self
204
+ end
205
+
206
+ # Returns the Volume instance mathcing the specified SOP Instance UID (if an argument is used).
207
+ # If a specified UID doesn't match, nil is returned.
208
+ # If no argument is passed, the first Volume instance associated with the RTDose is returned.
209
+ #
210
+ # === Parameters
211
+ #
212
+ # * <tt>uid</tt> -- String. The value of the SOP Instance UID element.
213
+ #
214
+ def volume(*args)
215
+ raise ArgumentError, "Expected one or none arguments, got #{args.length}." unless [0, 1].include?(args.length)
216
+ if args.length == 1
217
+ raise ArgumentError, "Expected String (or nil), got #{args.first.class}." unless [String, NilClass].include?(args.first.class)
218
+ return @associated_volumes[args.first]
219
+ else
220
+ # No argument used, therefore we return the first Volume instance:
221
+ return @volumes.first
222
+ end
223
+ end
224
+
225
+
226
+ private
227
+
228
+
229
+ # Returns the attributes of this instance in an array (for comparison purposes).
230
+ #
231
+ def state
232
+ [@series_uid, @volumes]
233
+ end
234
+
235
+ end
236
+
237
+ end
@@ -0,0 +1,179 @@
1
+ module RTKIT
2
+
3
+ # The RTImage class contains methods that are specific for this modality (RTIMAGE).
4
+ #
5
+ # === Inheritance
6
+ #
7
+ # * RTImage inherits all methods and attributes from the Series class.
8
+ #
9
+ class RTImage < Series
10
+
11
+ # An array of Plan Verification Images associated with this RTImage Series.
12
+ attr_reader :images
13
+ # The Plan which this RTImage Series belongs to.
14
+ attr_reader :plan
15
+
16
+ # Creates a new RTImage (series) instance by loading the relevant information from the specified DICOM object.
17
+ # The Series Instance UID string value is used to uniquely identify a RTImage (series) instance.
18
+ #
19
+ # === Parameters
20
+ #
21
+ # * <tt>dcm</tt> -- An instance of a DICOM object (DICOM::DObject) with modality 'RTIMAGE'.
22
+ # * <tt>study</tt> -- The Study instance that this RTImage belongs to.
23
+ #
24
+ def self.load(dcm, study)
25
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
26
+ raise ArgumentError, "Invalid argument 'study'. Expected Study, got #{study.class}." unless study.is_a?(Study)
27
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject with modality 'RTIMAGE', got #{dcm.value(MODALITY)}." unless dcm.value(MODALITY) == 'RTIMAGE'
28
+ # Required attributes:
29
+ series_uid = dcm.value(SERIES_UID)
30
+ # Optional attributes:
31
+ class_uid = dcm.value(SOP_CLASS)
32
+ date = dcm.value(SERIES_DATE)
33
+ time = dcm.value(SERIES_TIME)
34
+ description = dcm.value(SERIES_DESCR)
35
+ series_uid = dcm.value(SERIES_UID)
36
+ # Get the corresponding Plan:
37
+ plan = self.plan(dcm, study)
38
+ # Create the RTImage instance:
39
+ rtimage = self.new(series_uid, plan, :class_uid => class_uid, :date => date, :time => time, :description => description)
40
+ rtimage.add(dcm)
41
+ return rtimage
42
+ end
43
+
44
+ # Identifies the Plan that the RTImage object belongs to.
45
+ # If the referenced instances (Plan, StructureSet, ImageSeries & Frame) does not exist, they are created by this method.
46
+ #
47
+ def self.plan(dcm, study)
48
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
49
+ raise ArgumentError, "Invalid argument 'study'. Expected Study, got #{study.class}." unless study.is_a?(Study)
50
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject with modality 'RTIMAGE', got #{dcm.value(MODALITY)}." unless dcm.value(MODALITY) == 'RTIMAGE'
51
+ # Extract the Frame of Reference UID:
52
+ begin
53
+ frame_of_ref = dcm.value(FRAME_OF_REF)
54
+ rescue
55
+ frame_of_ref = nil
56
+ end
57
+ # Extract referenced Plan SOP Instance UID:
58
+ begin
59
+ ref_plan_uid = dcm[REF_PLAN_SQ][0].value(REF_SOP_UID)
60
+ rescue
61
+ ref_plan_uid = nil
62
+ end
63
+ # Create the Frame if it doesn't exist:
64
+ f = study.patient.dataset.frame(frame_of_ref)
65
+ f = Frame.new(frame_of_ref, study.patient) unless f
66
+ # Create the Plan, StructureSet & ImageSeries if the referenced Plan doesn't exist:
67
+ plan = study.fseries(ref_plan_uid)
68
+ unless plan
69
+ # Create ImageSeries (assuming modality CT):
70
+ is = ImageSeries.new(RTKIT.series_uid, 'CT', f, study)
71
+ study.add_series(is)
72
+ # Create StructureSet:
73
+ struct = StructureSet.new(RTKIT.sop_uid, is)
74
+ study.add_series(struct)
75
+ # Create Plan:
76
+ plan = Plan.new(ref_plan_uid, struct)
77
+ study.add_series(plan)
78
+ end
79
+ return plan
80
+ end
81
+
82
+ # Creates a new RTImage instance.
83
+ #
84
+ # === Parameters
85
+ #
86
+ # * <tt>series_uid</tt> -- The Series Instance UID string.
87
+ # * <tt>plan</tt> -- The Plan that this RTImage series belongs to.
88
+ # * <tt>options</tt> -- A hash of parameters.
89
+ #
90
+ # === Options
91
+ #
92
+ # * <tt>:date</tt> -- String. The Series Date (DICOM tag '0008,0021').
93
+ # * <tt>:time</tt> -- String. The Series Time (DICOM tag '0008,0031').
94
+ # * <tt>:description</tt> -- String. The Series Description (DICOM tag '0008,103E').
95
+ # * <tt>:series_uid</tt> -- String. The Series Instance UID (DICOM tag '0020,000E').
96
+ #
97
+ def initialize(series_uid, plan, options={})
98
+ raise ArgumentError, "Invalid argument 'series_uid'. Expected String, got #{series_uid.class}." unless series_uid.is_a?(String)
99
+ raise ArgumentError, "Invalid argument 'plan'. Expected Plan, got #{plan.class}." unless plan.is_a?(Plan)
100
+ # Pass attributes to Series initialization:
101
+ options[:class_uid] = '1.2.840.10008.5.1.4.1.1.481.1' # RT Image Storage
102
+ super(series_uid, 'RTIMAGE', plan.struct.study, options)
103
+ @plan = plan
104
+ # Default attributes:
105
+ @images = Array.new
106
+ @associated_images = Hash.new
107
+ # Register ourselves with the Plan:
108
+ @plan.add_rt_image(self)
109
+ end
110
+
111
+ # Returns true if the argument is an instance with attributes equal to self.
112
+ #
113
+ def ==(other)
114
+ if other.respond_to?(:to_rt_image)
115
+ other.send(:state) == state
116
+ end
117
+ end
118
+
119
+ alias_method :eql?, :==
120
+
121
+ # Registers a DICOM Object to the RTImage series, and processes it
122
+ # to create (and reference) an (RT) Image instance linked to this RTImage series.
123
+ #
124
+ def add(dcm)
125
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
126
+ Image.load(dcm, self)
127
+ #load_patient_setup
128
+ #load_fields
129
+ end
130
+
131
+ # Adds an Image to this RTImage series.
132
+ #
133
+ def add_image(image)
134
+ raise ArgumentError, "Invalid argument 'image'. Expected Image, got #{image.class}." unless image.is_a?(Image)
135
+ @images << image unless @associated_images[image.uid]
136
+ @associated_images[image.uid] = image
137
+ end
138
+
139
+ # Generates a Fixnum hash value for this instance.
140
+ #
141
+ def hash
142
+ state.hash
143
+ end
144
+
145
+ # Returns self.
146
+ #
147
+ def to_rt_image
148
+ self
149
+ end
150
+
151
+
152
+ private
153
+
154
+
155
+ =begin
156
+ # Registers this RTImage series instance with the Plan that it references.
157
+ #
158
+ def connect_to_plan(dcm)
159
+ # Find out which Plan is referenced:
160
+ dcm[REF_PLAN_SQ].each do |plan_item|
161
+ ref_sop_uid = plan_item.value(REF_SOP_UID)
162
+ matched_plan = @study.associated_instance_uids[ref_sop_uid]
163
+ if matched_plan
164
+ # The referenced series exists in our dataset. Proceed with setting up the references:
165
+ matched_plan.add_rtimage(self)
166
+ @plan = matched_plan
167
+ end
168
+ end
169
+ end
170
+ =end
171
+
172
+ # Returns the attributes of this instance in an array (for comparison purposes).
173
+ #
174
+ def state
175
+ [@series_uid, @images]
176
+ end
177
+
178
+ end
179
+ end
@@ -0,0 +1,165 @@
1
+ # Array extensions used by RTKIT.
2
+ #
3
+ class Array
4
+
5
+ # Rearranges the array (self) so that its elements appear in exactly
6
+ # the same sequence as another array (the argument).
7
+ # If the two arrays do not contain the same set of elements, an error is raised.
8
+ #
9
+ # === Examples
10
+ #
11
+ # a = [5, 2, 10, 1]
12
+ # b = [10, 2, 1, 5]
13
+ # b.assimilate!(a)
14
+ # b
15
+ # => [5, 2, 10, 1]
16
+ #
17
+ # NB! Not in use atm!
18
+ #
19
+ def assimilate!(other)
20
+ if self.length != other.length
21
+ raise ArgumentError, "Arrays 'self' and 'other' are of unequal length. Unable to compare."
22
+ else
23
+ # Validate equality:
24
+ self.each_index do |i|
25
+ index = other.index(self[i])
26
+ if index
27
+ self[i] = other[index]
28
+ else
29
+ raise "An element (index #{i}) in self was not found in the other array. Unable to assimilate."
30
+ end
31
+ end
32
+ end
33
+ return self
34
+ end
35
+
36
+ # Compares an array (self) with a target array to determine an array of indices which can be
37
+ # used to extract the elements of self in order to create an array which shares the exact
38
+ # order of elements as the target array.
39
+ # Naturally, for this comparison to make sense, the target array and self must
40
+ # contain the same set of elements.
41
+ # Raises an error if self and other are not of equal length.
42
+ #
43
+ # === Restrictions
44
+ #
45
+ # * Behaviour may be incorrect if the array contains multiple identical objects.
46
+ #
47
+ # === Examples
48
+ #
49
+ # a = ['hi', 2, dcm]
50
+ # b = [2, dcm, 'hi']
51
+ # order = b.compare_with(a)
52
+ # => [2, 0, 1]
53
+ #
54
+ def compare_with(other)
55
+ raise ArgumentError, "Arrays 'self' and 'other' are of unequal length. Unable to compare." if self.length != other.length
56
+ if self.length > 0
57
+ order = Array.new
58
+ other.each do |item|
59
+ index = self.index(item)
60
+ if index
61
+ order << index
62
+ else
63
+ raise "An element (#{item}) from the other array was not found in self. Unable to complete comparison."
64
+ end
65
+ end
66
+ end
67
+ return order
68
+ end
69
+
70
+ # Returns the most common value in the array.
71
+ #
72
+ def most_common_value
73
+ self.group_by do |e|
74
+ e
75
+ end.values.max_by(&:size).first
76
+ end
77
+
78
+ # Returns an array where the elements of the original array are extracted
79
+ # according to the indices given in the argument array.
80
+ #
81
+ # === Examples
82
+ #
83
+ # a = [5, 2, 10, 1]
84
+ # i = a.sort_order
85
+ # a.sort_by_order(i)
86
+ # => [1, 2, 5, 10]
87
+ #
88
+ #
89
+ def sort_by_order(order=[])
90
+ if self.length != order.length
91
+ return nil
92
+ else
93
+ return self.values_at(*order)
94
+ end
95
+ end
96
+
97
+ # Rearranges an array (self) so that it's original elements in the
98
+ # order specified by the indices given in the argument array.
99
+ #
100
+ # === Examples
101
+ #
102
+ # a = [5, 2, 10, 1]
103
+ # a.sort_by_order!([3, 1, 0, 2])
104
+ # a
105
+ # => [1, 2, 5, 10]
106
+ #
107
+ def sort_by_order!(order=[])
108
+ raise ArgumentError, "Invalid argument 'order'. Expected length equal to self.length (#{self.length}), got #{order.length}." if self.length != order.length
109
+ # It only makes sense to sort if length is 2 or more:
110
+ if self.length > 1
111
+ copy = self.dup
112
+ self.each_index do |i|
113
+ self[i] = copy[order[i]]
114
+ end
115
+ end
116
+ return self
117
+ end
118
+
119
+ # Returns an ordered array of indices, where each element contains the index in the original array
120
+ # which needs to be extracted to produce a sorted array.
121
+ # This method is useful if you wish to sort multiple arrays depending on the sequence of elements in a specific array.
122
+ #
123
+ # === Examples
124
+ #
125
+ # a = [5, 2, 10, 1]
126
+ # a.sort_order
127
+ # => [3, 1, 0, 2]
128
+ #
129
+ def sort_order
130
+ d=[]
131
+ self.each_with_index{|x,i| d[i]=[x,i]}
132
+ if block_given?
133
+ return d.sort {|x,y| yield x[0],y[0]}.collect{|x| x[1]}
134
+ else
135
+ return d.sort.collect{|x| x[1]}
136
+ end
137
+ end
138
+
139
+ end
140
+
141
+
142
+ class NArray
143
+
144
+ # Checks if an image array is segmented. Returns true if it is, and false if not.
145
+ # We define the image to be segmented if it contains at least positive 3 pixel values.
146
+ #
147
+ def segmented?
148
+ (self.gt 0).where.length > 2 ? true : false
149
+ end
150
+
151
+ end
152
+
153
+
154
+ class String
155
+
156
+ # Converts a string (containing a '\' separated x,y,z coordinate triplet)
157
+ # to a Coordinate instance.
158
+ #
159
+ def to_coordinate
160
+ values = self.split("\\").collect {|str| str.to_f}
161
+ raise ArgumentError, "Unable to create coordinate. Expected a string containing 3 values, got #{values.length}" unless values.length >= 3
162
+ return RTKIT::Coordinate.new(values[0], values[1], values[2])
163
+ end
164
+
165
+ end