dicom 0.8 → 0.9

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.
@@ -1,16 +1,15 @@
1
- # Copyright 2008-2010 Christoffer Lervag
2
- #
3
- # === Notes
4
- #
5
- # In addition to reading files that are compliant to DICOM 3 Part 10, the philosophy of the Ruby DICOM library is to feature maximum
6
- # compatibility, and as such it will also successfully read many types of 'DICOM' files that deviate in some way from the standard.
7
-
8
1
  module DICOM
9
2
 
10
3
  # The DRead class parses the DICOM data from a binary string.
11
4
  #
12
5
  # The source of this binary string is typically either a DICOM file or a DICOM network transmission.
13
6
  #
7
+ # === Notes
8
+ #
9
+ # In addition to reading files that are compliant to DICOM 3 Part 10, the philosophy of the
10
+ # Ruby DICOM library is to feature maximum compatibility, and as such it will also
11
+ # successfully read many types of 'DICOM' files that deviate in some way from the standard.
12
+ #
14
13
  class DRead
15
14
 
16
15
  # A boolean which reports the explicitness of the DICOM string, true if explicit and false if implicit.
@@ -18,7 +17,7 @@ module DICOM
18
17
  # A boolean which reports the endianness of the post-meta group part of the DICOM string (true for big endian, false for little endian).
19
18
  attr_reader :file_endian
20
19
  # An array which records any status messages that are generated while parsing the DICOM string.
21
- attr_reader :msg
20
+ attr_reader :msg
22
21
  # A DObject instance which the parsed data elements will be connected to.
23
22
  attr_reader :obj
24
23
  # A boolean which records whether the DICOM string contained the proper DICOM header signature of 128 bytes + 'DICM'.
@@ -38,13 +37,16 @@ module DICOM
38
37
  # === Options
39
38
  #
40
39
  # * <tt>:syntax</tt> -- String. If specified, the decoding of the DICOM string will be forced to use this transfer syntax.
41
- # * <tt>:bin</tt> -- Boolean. If set to true, string parameter will be interpreted as a binary DICOM string, and not a path string, which is the default behaviour.
42
- #
40
+ # * <tt>:bin</tt> -- Boolean. If true, the string parameter will be interpreted as a binary DICOM string instead of a path string.
41
+ #
43
42
  def initialize(obj, string=nil, options={})
44
43
  # Set the DICOM object as an instance variable:
45
44
  @obj = obj
46
- # Some of the options need to be transferred to instance variables:
47
- @transfer_syntax = options[:syntax]
45
+ # If a transfer syntax has been specified as an option for a DICOM object, make sure that it makes it into the object:
46
+ if options[:syntax]
47
+ @transfer_syntax = options[:syntax]
48
+ obj.add(Element.new("0002,0010", options[:syntax])) if obj.is_a?(DObject)
49
+ end
48
50
  # Initiate the variables that are used during file reading:
49
51
  init_variables
50
52
  # Are we going to read from a file, or read from a binary string?
@@ -72,7 +74,7 @@ module DICOM
72
74
  # Read and verify the DICOM header:
73
75
  header = check_header
74
76
  # If the file didnt have the expected header, we will attempt to read
75
- # data elements from the very start file:
77
+ # data elements from the very start of the file:
76
78
  if header == false
77
79
  @stream.skip(-132)
78
80
  elsif header == nil
@@ -88,9 +90,10 @@ module DICOM
88
90
  begin
89
91
  # Extracting Data element information (nil is returned if end of file is encountered in a normal way).
90
92
  data_element = process_data_element
91
- rescue
93
+ rescue Exception => msg
92
94
  # The parse algorithm crashed. Set data_element to false to break the loop and toggle the success boolean to indicate failure.
93
- @msg << "Error! Failed to process a Data Element. This is probably the result of invalid or corrupt DICOM data."
95
+ @msg << msg
96
+ @msg << "Error: Failed to parse Data Elements. This was probably an invalid/corrupt DICOM file."
94
97
  @success = false
95
98
  data_element = false
96
99
  end
@@ -218,12 +221,9 @@ module DICOM
218
221
  @current_parent = @current_parent.parent
219
222
  else
220
223
  # Create an ordinary Data Element:
221
- @current_element = DataElement.new(tag, value, :bin => bin, :name => name, :parent => @current_parent, :vr => vr)
224
+ @current_element = Element.new(tag, value, :bin => bin, :name => name, :parent => @current_parent, :vr => vr)
222
225
  # Check that the data stream didnt end abruptly:
223
- if length != @current_element.bin.length
224
- set_abrupt_error
225
- return false # (Failed)
226
- end
226
+ raise "Error: The actual length of the binary (#{@current_element.bin.length}) does not match the specified length (#{length}) for Data Element #{@current_element.tag}." if length != @current_element.bin.length
227
227
  end
228
228
  # Return true to indicate success:
229
229
  return true
@@ -290,11 +290,8 @@ module DICOM
290
290
  else
291
291
  length = @stream.decode(bytes, "SL") # (4)
292
292
  end
293
- if length%2 > 0 and length > 0
294
- # According to the DICOM standard, all data element lengths should be an even number.
295
- # If it is not, it may indicate a file that is not standards compliant or it might even not be a DICOM file.
296
- @msg << "Warning: Odd number of bytes in data element's length occured. This is a violation of the DICOM standard, but program will attempt to read the rest of the file anyway."
297
- end
293
+ # Check that length is valid (according to the DICOM standard, it must be even):
294
+ raise "Error: Encountered a Data Element (#{tag}) with an invalid (odd) value length." if length%2 == 1 and length > 0
298
295
  return vr, length
299
296
  end
300
297
 
@@ -341,12 +338,29 @@ module DICOM
341
338
  #
342
339
  # === Parameters
343
340
  #
344
- # * <tt>file</tt> -- A path/file string.
341
+ # * <tt>file</tt> -- A path/file string. The string may point to a local file or a http location.
345
342
  #
346
343
  def open_file(file)
347
- if File.exist?(file)
344
+ if file.index('http')==0
345
+ # Try to open the remote file using open-uri:
346
+ @retrials = 0
347
+ begin
348
+ @file = open(file, 'rb') # binary encoding (ASCII-8BIT)
349
+ rescue Exception => e
350
+ if @retrials>3
351
+ @retrials = 0
352
+ raise "Unable to read the file. File does not exist?"
353
+ else
354
+ puts "Warning: Exception in ruby-dicom when loading a dicom file from: #{file}"
355
+ puts "Retrying... #{@retrials}"
356
+ @retrials+=1
357
+ retry
358
+ end
359
+ end
360
+ elsif File.exist?(file)
361
+ # Try to read the file on the local file system:
348
362
  if File.readable?(file)
349
- if not File.directory?(file)
363
+ if !File.directory?(file)
350
364
  if File.size(file) > 8
351
365
  @file = File.new(file, "rb")
352
366
  else
@@ -363,14 +377,6 @@ module DICOM
363
377
  end
364
378
  end
365
379
 
366
- # Registers an unexpected error by toggling a success boolean and recording an error message.
367
- # The DICOM string ended abruptly because the data element's value was shorter than expected.
368
- #
369
- def set_abrupt_error
370
- @msg << "Error! The parsed data of the last data element #{@current_element.tag} does not match its specified length value. This is probably the result of invalid or corrupt DICOM data."
371
- @success = false
372
- end
373
-
374
380
  # Changes encoding variables as the file reading proceeds past the initial meta group part (0002,xxxx) of the DICOM file.
375
381
  #
376
382
  def switch_syntax
@@ -1,5 +1,3 @@
1
- # Copyright 2009-2010 Christoffer Lervag
2
-
3
1
  module DICOM
4
2
 
5
3
  # This class contains code for setting up a Service Class Provider (SCP),
@@ -20,9 +18,9 @@ module DICOM
20
18
  # require 'dicom'
21
19
  # require 'my_file_handler'
22
20
  # include DICOM
23
- # DServer.run(104, 'c:/temp/') do
24
- # timeout = 100
25
- # file_handler = MyFileHandler
21
+ # DServer.run(104, 'c:/temp/') do |s|
22
+ # s.timeout = 100
23
+ # s.file_handler = MyFileHandler
26
24
  # end
27
25
  #
28
26
  def self.run(port=104, path='./received/', &block)
@@ -35,7 +33,7 @@ module DICOM
35
33
  attr_accessor :file_handler
36
34
  # The name of the server (application entity).
37
35
  attr_accessor :host_ae
38
- # The maximum allowed size of network packages (in bytes).
36
+ # The maximum allowed size of network packages (in bytes).
39
37
  attr_accessor :max_package_size
40
38
  # The network port to be used.
41
39
  attr_accessor :port
@@ -43,7 +41,7 @@ module DICOM
43
41
  attr_accessor :timeout
44
42
  # A boolean which defines if notices/warnings/errors will be printed to the screen (true) or not (false).
45
43
  attr_accessor :verbose
46
-
44
+
47
45
  # A hash containing the abstract syntaxes that will be accepted.
48
46
  attr_reader :accepted_abstract_syntaxes
49
47
  # A hash containing the transfer syntaxes that will be accepted.
@@ -64,7 +62,7 @@ module DICOM
64
62
  #
65
63
  # * <tt>:file_handler</tt> -- A customized FileHandler class to use instead of the default FileHandler.
66
64
  # * <tt>:host_ae</tt> -- String. The name of the server (application entity).
67
- # * <tt>:max_package_size</tt> -- Fixnum. The maximum allowed size of network packages (in bytes).
65
+ # * <tt>:max_package_size</tt> -- Fixnum. The maximum allowed size of network packages (in bytes).
68
66
  # * <tt>:timeout</tt> -- Fixnum. The maximum period the server will wait on an answer from a client before aborting the communication.
69
67
  # * <tt>:verbose</tt> -- Boolean. If set to false, the DServer instance will run silently and not output warnings and error messages to the screen. Defaults to true.
70
68
  #
@@ -107,7 +105,7 @@ module DICOM
107
105
  #
108
106
  def add_abstract_syntax(uid)
109
107
  if uid.is_a?(String)
110
- name = LIBRARY.get_syntax_description(uid) || "Unknown UID"
108
+ name = LIBRARY.get_syntax_description(uid) || "Unknown UID"
111
109
  @accepted_abstract_syntaxes[uid] = name
112
110
  else
113
111
  raise "Invalid type of UID. Expected String, got #{uid.class}!"
@@ -123,7 +121,7 @@ module DICOM
123
121
  #
124
122
  def add_transfer_syntax(uid)
125
123
  if uid.is_a?(String)
126
- name = LIBRARY.get_syntax_description(uid) || "Unknown UID"
124
+ name = LIBRARY.get_syntax_description(uid) || "Unknown UID"
127
125
  @accepted_transfer_syntaxes[uid] = name
128
126
  else
129
127
  raise "Invalid type of UID. Expected String, got #{uid.class}!"
@@ -1,23 +1,21 @@
1
- # Copyright 2008-2010 Christoffer Lervag
2
- #
3
- # === Notes
4
- #
5
- # The philosophy of the Ruby DICOM library is to feature maximum conformance to the DICOM standard.
6
- # As such, the class which writes DICOM files may manipulate the meta group, remove/change group lengths and add a header signature.
7
- #
8
- # Therefore, the file that is written may not be an exact bitwise copy of the file that was read,
9
- # even if no DObject manipulation has been done on the part of the user.
10
- #
11
- # Remember: If this behaviour for some reason is not wanted, it is easy to modify the source code to avoid it.
12
- #
13
- # It is important to note, that while the goal is to be fully DICOM compliant, no guarantees are given
14
- # that this is actually achieved. You are encouraged to thouroughly test your files for compatibility after creation.
15
-
16
1
  module DICOM
17
2
 
18
3
  # The DWrite class handles the encoding of a DObject instance to a valid DICOM string.
19
4
  # The String is either written to file or returned in segments to be used for network transmission.
20
5
  #
6
+ # === Notes
7
+ #
8
+ # The philosophy of the Ruby DICOM library is to feature maximum conformance to the DICOM standard.
9
+ # As such, the class which writes DICOM files may manipulate the meta group, remove/change group lengths and add a header signature.
10
+ #
11
+ # Therefore, the file that is written may not be an exact bitwise copy of the file that was read,
12
+ # even if no DObject manipulation has been done on the part of the user.
13
+ #
14
+ # Remember: If this behaviour for some reason is not wanted, it is easy to modify the source code to avoid it.
15
+ #
16
+ # It is important to note, that while the goal is to be fully DICOM compliant, no guarantees are given
17
+ # that this is actually achieved. You are encouraged to thouroughly test your files for compatibility after creation.
18
+ #
21
19
  class DWrite
22
20
 
23
21
  # An array which records any status messages that are generated while encoding/writing the DICOM string.
@@ -103,14 +101,10 @@ module DICOM
103
101
  @max_size = max_size
104
102
  @segments = Array.new
105
103
  elements = @obj.children
106
- # When sending a DICOM file across the network, no header or meta information is needed.
107
- # We must therefore find the position of the first tag which is not a meta information tag.
108
- first_pos = first_non_meta(elements)
109
- selected_elements = elements[first_pos..-1]
110
104
  # Create a Stream instance to handle the encoding of content to
111
105
  # the binary string that will eventually be saved to file:
112
106
  @stream = Stream.new(nil, @file_endian)
113
- write_data_elements(selected_elements)
107
+ write_data_elements(elements)
114
108
  # Extract the remaining string in our stream instance to our array of strings:
115
109
  @segments << @stream.export
116
110
  # Mark this write session as successful:
@@ -134,19 +128,21 @@ module DICOM
134
128
  else
135
129
  # As the encoded DICOM string will be cut in multiple, smaller pieces, we need to monitor the length of our encoded strings:
136
130
  if (string.length + @stream.length) > @max_size
137
- append = string.slice!(0, @max_size-@stream.length)
131
+ # Duplicate the string as not to ruin the binary of the data element with our slicing:
132
+ segment = string.dup
133
+ append = segment.slice!(0, @max_size-@stream.length)
138
134
  # Join these strings together and add them to the segments:
139
135
  @segments << @stream.export + append
140
- if (30 + string.length) > @max_size
136
+ if (30 + segment.length) > @max_size
141
137
  # The remaining part of the string is bigger than the max limit, fill up more segments:
142
138
  # How many full segments will this string fill?
143
- number = (string.length/@max_size.to_f).floor
144
- number.times {@segments << string.slice!(0, @max_size)}
139
+ number = (segment.length/@max_size.to_f).floor
140
+ number.times {@segments << segment.slice!(0, @max_size)}
145
141
  # The remaining part is added to the stream:
146
- @stream.add_last(string)
142
+ @stream.add_last(segment)
147
143
  else
148
144
  # The rest of the string is small enough that it can be added to the stream:
149
- @stream.add_last(string)
145
+ @stream.add_last(segment)
150
146
  end
151
147
  elsif (30 + @stream.length) > @max_size
152
148
  # End the current segment, and start on a new segment for this string.
@@ -209,7 +205,7 @@ module DICOM
209
205
  # Among group length elements, only write the meta group element (the others have been retired in the DICOM standard):
210
206
  write_data_element(element) if element.tag == "0002,0000"
211
207
  else
212
- write_data_element(element)
208
+ write_data_element(element)
213
209
  end
214
210
  end
215
211
  end
@@ -315,7 +311,7 @@ module DICOM
315
311
  #
316
312
  def write_value(bin)
317
313
  # This is pretty straightforward, just dump the binary data to the file/string:
318
- add(bin)
314
+ add(bin) if bin
319
315
  end
320
316
 
321
317
 
@@ -347,7 +343,7 @@ module DICOM
347
343
  unless File.directory?(path)
348
344
  # We need to create (parts of) this path:
349
345
  require 'fileutils'
350
- FileUtils.mkdir_p path
346
+ FileUtils.mkdir_p(path)
351
347
  end
352
348
  end
353
349
  # The path to this non-existing file is verified, and we can proceed to create the file:
@@ -384,23 +380,6 @@ module DICOM
384
380
  @stream.endian = @rest_endian
385
381
  end
386
382
 
387
- # Identifies and returns the index of the first data element that does not have a meta group ("0002,xxxx") tag.
388
- #
389
- # === Parameters
390
- #
391
- # * <tt>elements</tt> -- An array of data elements.
392
- #
393
- def first_non_meta(elements)
394
- non_meta_index = 0
395
- elements.each_index do |i|
396
- if elements[i].tag.group != META_GROUP
397
- non_meta_index = i
398
- break
399
- end
400
- end
401
- return non_meta_index
402
- end
403
-
404
383
  # Creates various variables used when encoding the DICOM string.
405
384
  #
406
385
  def init_variables
@@ -1,6 +1,4 @@
1
- # coding: ISO-8859-1
2
- #
3
- # Copyright 2008-2010 Christoffer Lervag
1
+ # encoding: UTF-8
4
2
 
5
3
  module DICOM
6
4
 
@@ -1,22 +1,20 @@
1
- # Copyright 2010 Christoffer Lervag
2
-
3
1
  module DICOM
4
2
 
5
- # The DataElement class handles information related to ordinary (non-parent) data elements.
3
+ # The Element class handles information related to ordinary (non-parent) elementals (data elements).
6
4
  #
7
- class DataElement
5
+ class Element
8
6
 
9
- # Include the Elements mix-in module:
10
- include Elements
7
+ # Include the Elemental mix-in module:
8
+ include Elemental
11
9
 
12
10
  # The (decoded) value of the data element.
13
11
  attr_reader :value
14
12
 
15
- # Creates a DataElement instance.
13
+ # Creates a Element instance.
16
14
  #
17
15
  # === Notes
18
16
  #
19
- # * In the case where the DataElement is given a binary instead of value, the DataElement will not have a formatted value (value = nil).
17
+ # * In the case where the Element is given a binary instead of value, the Element will not have a formatted value (value = nil).
20
18
  # * Private data elements will have their names listed as "Private".
21
19
  # * Non-private data elements that are not found in the dictionary will be listed as "Unknown".
22
20
  #
@@ -30,20 +28,21 @@ module DICOM
30
28
  #
31
29
  # * <tt>:bin</tt> -- String. If you already have the value pre-encoded to a binary string, the string can be supplied with this option to avoid it being encoded a second time.
32
30
  # * <tt>:encoded</tt> -- Boolean. If the value parameter contains a pre-encoded binary, this boolean must to be set as true.
33
- # * <tt>:name</tt> - String. The name of the DataElement may be specified upon creation. If it is not, the name will be retrieved from the dictionary.
34
- # * <tt>:parent</tt> - Item or DObject instance which the DataElement instance shall belong to.
35
- # * <tt>:vr</tt> -- String. If a private DataElement is created with a custom value, this must be specified to enable the encoding of the value. If it is not specified, the vr will be retrieved from the dictionary.
31
+ # * <tt>:name</tt> - String. The name of the Element may be specified upon creation. If it is not, the name will be retrieved from the dictionary.
32
+ # * <tt>:parent</tt> - Item or DObject instance which the Element instance shall belong to.
33
+ # * <tt>:vr</tt> -- String. If a private Element is created with a custom value, this must be specified to enable the encoding of the value. If it is not specified, the vr will be retrieved from the dictionary.
36
34
  #
37
35
  # === Examples
38
36
  #
39
37
  # # Create a new data element and connect it to a DObject instance:
40
- # patient_name = DataElement.new("0010,0010", "John Doe", :parent => obj)
38
+ # patient_name = Element.new("0010,0010", "John Doe", :parent => obj)
41
39
  # # Create a "Pixel Data" element and insert image data that you have already encoded elsewhere:
42
- # pixel_data = DataElement.new("7FE0,0010", processed_pixel_data, :encoded => true, :parent => obj)
40
+ # pixel_data = Element.new("7FE0,0010", processed_pixel_data, :encoded => true, :parent => obj)
43
41
  # # Create a private data element:
44
- # private_data = DataElement.new("0011,2102", some_data, :parent => obj, :vr => "LO")
42
+ # private_data = Element.new("0011,2102", some_data, :parent => obj, :vr => "LO")
45
43
  #
46
44
  def initialize(tag, value, options={})
45
+ raise ArgumentError, "The supplied tag (#{tag}) is not valid. The tag must be a string of the form 'GGGG,EEEE'." unless tag.is_a?(String) && tag.tag?
47
46
  # Set instance variables:
48
47
  @tag = tag
49
48
  # We may beed to retrieve name and vr from the library:
@@ -78,11 +77,11 @@ module DICOM
78
77
  # Manage the parent relation if specified:
79
78
  if options[:parent]
80
79
  @parent = options[:parent]
81
- @parent.add(self)
80
+ @parent.add(self, :no_follow => true)
82
81
  end
83
82
  end
84
83
 
85
- # Sets the binary string of a DataElement.
84
+ # Sets the binary string of a Element.
86
85
  #
87
86
  # === Notes
88
87
  #
@@ -94,46 +93,74 @@ module DICOM
94
93
  # * <tt>new_bin</tt> -- A binary string of encoded data.
95
94
  #
96
95
  def bin=(new_bin)
97
- if new_bin.is_a?(String)
98
- # Add a zero byte at the end if the length of the binary is odd:
99
- if new_bin.length[0] == 1
100
- @bin = new_bin + stream.pad_byte[@vr]
101
- else
102
- @bin = new_bin
103
- end
104
- @value = nil
105
- @length = @bin.length
96
+ raise ArgumentError, "Expected String, got #{new_bin.class}." unless new_bin.is_a?(String)
97
+ # Add a zero byte at the end if the length of the binary is odd:
98
+ if new_bin.length[0] == 1
99
+ @bin = new_bin + stream.pad_byte[@vr]
106
100
  else
107
- raise "Invalid parameter type. String was expected, got #{new_bin.class}."
101
+ @bin = new_bin
108
102
  end
103
+ @value = nil
104
+ @length = @bin.length
109
105
  end
110
106
 
111
- # Checks if an element actually has any child elements.
112
- # Returns false, as DataElement instances can not have children.
107
+ # Checks if the Element actually has any child elementals.
108
+ # Returns false, as Element instances by definition can not have children.
113
109
  #
114
110
  def children?
115
111
  return false
116
112
  end
117
113
 
118
- # Checks if an element is a parent.
119
- # Returns false, as DataElement instance can not be parents.
114
+ # Returns the endianness of the encoded binary value of this data element.
115
+ # Returns false if little endian, true if big endian.
116
+ #
117
+ def endian
118
+ return stream.str_endian
119
+ end
120
+
121
+ # Returns a string containing a human-readable hash representation of the Element.
122
+ #
123
+ def inspect
124
+ to_hash.inspect
125
+ end
126
+
127
+ # Checks if the Element is a parent.
128
+ # Returns false, as Element instances by definition can not be parents.
120
129
  #
121
130
  def is_parent?
122
131
  return false
123
132
  end
124
133
 
125
- # Sets the value of the DataElement instance.
134
+ # Returns the value of the elemental (used as value in the parent's hash representation).
135
+ #
136
+ def to_hash
137
+ return {self.send(DICOM.key_representation) => value}
138
+ end
139
+
140
+ # Returns a json string containing a human-readable representation of the Element.
141
+ #
142
+ def to_json
143
+ to_hash.to_json
144
+ end
145
+
146
+ # Returns a yaml string containing a human-readable representation of the Element.
147
+ #
148
+ def to_yaml
149
+ to_hash.to_yaml
150
+ end
151
+
152
+ # Sets the value of the Element instance.
126
153
  #
127
154
  # === Notes
128
155
  #
129
156
  # In addition to updating the value attribute, the specified value is encoded and used to
130
- # update both the DataElement's binary and length attributes too.
157
+ # update both the Element's binary and length attributes too.
131
158
  #
132
- # The specified value must be of a type that is compatible with the DataElement's value representation (vr).
159
+ # The specified value must be of a type that is compatible with the Element's value representation (vr).
133
160
  #
134
161
  # === Parameters
135
162
  #
136
- # * <tt>new_value</tt> -- A custom value (String, Fixnum, etc..) that is assigned to the DataElement.
163
+ # * <tt>new_value</tt> -- A custom value (String, Fixnum, etc..) that is assigned to the Element.
137
164
  #
138
165
  def value=(new_value)
139
166
  @bin = encode(new_value)
@@ -156,20 +183,5 @@ module DICOM
156
183
  return stream.encode_value(formatted_value, @vr)
157
184
  end
158
185
 
159
- # Returns a Stream instance which can be used for encoding a value to binary.
160
- #
161
- # === Notes
162
- #
163
- # * Retrieves the Stream instance of the top parent DObject instance.
164
- # If this fails, a new Stream instance is created (with Little Endian encoding assumed).
165
- #
166
- def stream
167
- if top_parent.is_a?(DObject)
168
- return top_parent.stream
169
- else
170
- return Stream.new(nil, file_endian=false)
171
- end
172
- end
173
-
174
186
  end
175
187
  end