]> Creatis software - gdcm.git/blob - vtk/vtkGdcmReader.cxx
* FIX : distutilsWrapping.py : copy of .lib file for windows systems
[gdcm.git] / vtk / vtkGdcmReader.cxx
1 // vtkGdcmReader.cxx
2 //-----------------------------------------------------------------------------
3 // //////////////////////////////////////////////////////////////
4 // WARNING TODO CLENAME 
5 // Actual limitations of this code:
6 //
7 // /////// Redundant and unnecessary header parsing
8 // In it's current state this code actually parses three times the Dicom
9 // header of a file before the corresponding image gets loaded in the
10 // ad-hoc vtkData !
11 // Here is the process:
12 //  1/ First loading happens in ExecuteInformation which in order to
13 //     positionate the vtk extents calls CheckFileCoherence. The purpose
14 //     of CheckFileCoherence is to make sure all the images in the future
15 //     stack are "homogenous" (same size, same representation...). This
16 //     can only be achieved by parsing all the Dicom headers...
17 //  2/ ExecuteData is then responsible for the next two loadings:
18 //  2a/ ExecuteData calls AllocateOutputData that in turn seems to 
19 //      (indirectely call) ExecuteInformation which ends up in a second
20 //      header parsing
21 //  2b/ the core of ExecuteData then needs gdcmFile (which in turns
22 //      initialises gdcmHeader in the constructor) in order to access
23 //      the data-image.
24 //
25 // Possible solution:
26 // maintain a list of gdcmFiles (created by say ExecuteInformation) created
27 // once and for all accross the life of vtkGdcmHeader (it would only load
28 // new gdcmFile if the user changes the list). ExecuteData would then use 
29 // those gdcmFile and hence avoid calling the construtor:
30 //  - advantage: the header of the files would only be parser once.
31 //  - drawback: once execute information is called (i.e. on creation of
32 //              a vtkGdcmHeader) the gdcmFile structure is loaded in memory.
33 //              The average size of a gdcmHeader being of 100Ko, is one
34 //              loads 10 stacks of images with say 200 images each, you
35 //              end-up with a loss of 200Mo...
36 //
37 // /////// Never unallocated memory:
38 // ExecuteData allocates space for the pixel data [which will get pointed
39 // by the vtkPointData() through the call
40 // data->GetPointData()->GetScalars()->SetVoidArray(mem, StackNumPixels, 0);]
41 // This data is never "freed" neither in the destructor nor when the
42 // filename list is extended, ExecuteData is called a second (or third)
43 // time...
44 // //////////////////////////////////////////////////////////////
45
46 #include "gdcmFile.h"
47 #include "gdcmHeaderHelper.h"
48 #include "vtkGdcmReader.h"
49
50 //#include <stdio.h>
51 #include <vtkObjectFactory.h>
52 #include <vtkImageData.h>
53 #include <vtkPointData.h>
54 #include <vtkLookupTable.h>
55
56 //-----------------------------------------------------------------------------
57 // Constructor / Destructor
58 vtkGdcmReader::vtkGdcmReader()
59 {
60    this->LookupTable = NULL;
61 }
62
63 vtkGdcmReader::~vtkGdcmReader()
64 {
65    this->RemoveAllFileName();
66    this->InternalFileNameList.clear();
67    if(this->LookupTable) 
68       this->LookupTable->Delete();
69 }
70
71 //-----------------------------------------------------------------------------
72 // Print
73 void vtkGdcmReader::PrintSelf(ostream& os, vtkIndent indent)
74 {
75    vtkImageReader::PrintSelf(os,indent);
76    os << indent << "Filenames  : " << endl;
77    vtkIndent nextIndent = indent.GetNextIndent();
78    for (std::list<std::string>::iterator FileName  = FileNameList.begin();
79         FileName != FileNameList.end();
80         ++FileName)
81    {
82       os << nextIndent << FileName->c_str() << endl ;
83    }
84 }
85
86 //-----------------------------------------------------------------------------
87 // Public
88 /*
89  * Remove all files from the list of images to read.
90  */
91 void vtkGdcmReader::RemoveAllFileName(void)
92 {
93    this->FileNameList.clear();
94 }
95
96 /*
97  * Adds a file name to the list of images to read.
98  */
99 void vtkGdcmReader::AddFileName(const char* name)
100 {
101    // We need to bypass the const pointer [since list<>.push_bash() only
102    // takes a char* (but not a const char*)] by making a local copy:
103    char * LocalName = new char[strlen(name) + 1];
104    strcpy(LocalName, name);
105    this->FileNameList.push_back(LocalName);
106    this->Modified();
107    delete[] LocalName;
108 }
109
110 /*
111  * Sets up a filename to be read.
112  */
113 void vtkGdcmReader::SetFileName(const char *name) 
114 {
115    vtkImageReader2::SetFileName(name);
116    // Since we maintain a list of filenames, when building a volume,
117    // (see vtkGdcmReader::AddFileName), we additionaly need to purge
118    // this list when we manually positionate the filename.
119    this->FileNameList.clear();
120    this->Modified();
121 }
122
123 //-----------------------------------------------------------------------------
124 // Protected
125 /*
126  * Configure the output e.g. WholeExtent, spacing, origin, scalar type...
127  */
128 void vtkGdcmReader::ExecuteInformation()
129 {
130    this->TotalNumberOfPlanes = this->CheckFileCoherence();
131    if ( this->TotalNumberOfPlanes == 0)
132    {
133       vtkErrorMacro("File set is not coherent. Exiting...");
134       return;
135    }
136       
137    // if the user has not set the extent, but has set the VOI
138    // set the z axis extent to the VOI z axis
139    if (this->DataExtent[4]==0 && this->DataExtent[5] == 0 &&
140    (this->DataVOI[4] || this->DataVOI[5]))
141    {
142       this->DataExtent[4] = this->DataVOI[4];
143       this->DataExtent[5] = this->DataVOI[5];
144    }
145
146    // When the user has set the VOI, check it's coherence with the file content.
147    if (this->DataVOI[0] || this->DataVOI[1] || 
148    this->DataVOI[2] || this->DataVOI[3] ||
149    this->DataVOI[4] || this->DataVOI[5])
150    { 
151       if ((this->DataVOI[0] < 0) ||
152           (this->DataVOI[1] >= this->NumColumns) ||
153           (this->DataVOI[2] < 0) ||
154           (this->DataVOI[3] >= this->NumLines) ||
155           (this->DataVOI[4] < 0) ||
156           (this->DataVOI[5] >= this->TotalNumberOfPlanes ))
157       {
158          vtkWarningMacro("The requested VOI is larger than expected extent.");
159          this->DataVOI[0] = 0;
160          this->DataVOI[1] = this->NumColumns - 1;
161          this->DataVOI[2] = 0;
162          this->DataVOI[3] = this->NumLines - 1;
163          this->DataVOI[4] = 0;
164          this->DataVOI[5] = this->TotalNumberOfPlanes - 1;
165       }
166    }
167
168    // Positionate the Extent.
169    this->DataExtent[0] = 0;
170    this->DataExtent[1] = this->NumColumns - 1;
171    this->DataExtent[2] = 0;
172    this->DataExtent[3] = this->NumLines - 1;
173    this->DataExtent[4] = 0;
174    this->DataExtent[5] = this->TotalNumberOfPlanes - 1;
175   
176    // We don't need to positionate the Endian related stuff (by using
177    // this->SetDataByteOrderToBigEndian() or SetDataByteOrderToLittleEndian()
178    // since the reading of the file is done by gdcm.
179    // But we do need to set up the data type for downstream filters:
180    if      ( ImageType == "8U" )
181    {
182       vtkDebugMacro("8 bits unsigned image");
183       this->SetDataScalarTypeToUnsignedChar(); 
184    }
185    else if ( ImageType == "8S" )
186    {
187       vtkErrorMacro("Cannot handle 8 bit signed files");
188       return;
189    }
190    else if ( ImageType == "16U" )
191    {
192       vtkDebugMacro("16 bits unsigned image");
193       this->SetDataScalarTypeToUnsignedShort();
194    }
195    else if ( ImageType == "16S" )
196    {
197       vtkDebugMacro("16 bits signed image");
198       this->SetDataScalarTypeToShort();
199       //vtkErrorMacro("Cannot handle 16 bit signed files");
200    }
201    else if ( ImageType == "32U" )
202    {
203       vtkDebugMacro("32 bits unsigned image");
204       vtkDebugMacro("WARNING: forced to signed int !");
205       this->SetDataScalarTypeToInt();
206    }
207    else if ( ImageType == "32S" )
208    {
209       vtkDebugMacro("32 bits signed image");
210       this->SetDataScalarTypeToInt();
211    }
212
213    //Set number of scalar components:
214    this->SetNumberOfScalarComponents(this->NumComponents);
215
216    this->Superclass::ExecuteInformation();
217 }
218
219 /*
220  * Update => ouput->Update => UpdateData => Execute => ExecuteData 
221  * (see vtkSource.cxx for last step).
222  * This function (redefinition of vtkImageReader::ExecuteData, see 
223  * VTK/IO/vtkImageReader.cxx) reads a data from a file. The datas
224  * extent/axes are assumed to be the same as the file extent/order.
225  */
226 void vtkGdcmReader::ExecuteData(vtkDataObject *output)
227 {
228    if (this->InternalFileNameList.empty())
229    {
230       vtkErrorMacro("A least a valid FileName must be specified.");
231       return;
232    }
233
234    // FIXME : extraneous parsing of header is made when allocating OuputData
235    vtkImageData *data = this->AllocateOutputData(output);
236    data->SetExtent(this->DataExtent);
237    data->GetPointData()->GetScalars()->SetName("DicomImage-Volume");
238
239    // Test if output has valid extent
240    // Prevent memory errors
241    if((this->DataExtent[1]-this->DataExtent[0]>=0) &&
242       (this->DataExtent[3]-this->DataExtent[2]>=0) &&
243       (this->DataExtent[5]-this->DataExtent[4]>=0))
244    {
245       // The memory size for a full stack of images of course depends
246       // on the number of planes and the size of each image:
247       size_t StackNumPixels = this->NumColumns * this->NumLines
248                             * this->TotalNumberOfPlanes * this->NumComponents;
249       size_t stack_size = StackNumPixels * this->PixelSize;
250       // Allocate pixel data space itself.
251
252       // Variables for the UpdateProgress. We shall use 50 steps to signify
253       // the advance of the process:
254       unsigned long UpdateProgressTarget = (unsigned long) ceil (this->NumLines
255                                          * this->TotalNumberOfPlanes
256                                          / 50.0);
257       // The actual advance measure:
258       unsigned long UpdateProgressCount = 0;
259
260       // Feeling the allocated memory space with each image/volume:
261       unsigned char *Dest = (unsigned char *)data->GetPointData()->GetScalars()->GetVoidPointer(0);
262       for (std::list<std::string>::iterator FileName  = InternalFileNameList.begin();
263            FileName != InternalFileNameList.end();
264            ++FileName)
265       { 
266          // Images that were tagged as unreadable in CheckFileCoherence()
267          // are substituted with a black image to let the caller visually
268          // notice something wrong is going on:
269          if (*FileName != "GDCM_UNREADABLE")
270          {
271             // Update progress related for good files is made in LoadImageInMemory
272             Dest += this->LoadImageInMemory(*FileName, Dest,
273                                             UpdateProgressTarget,
274                                             UpdateProgressCount);
275          } 
276          else 
277          {
278             // We insert a black image in the stack for the user to be aware that
279             // this image/volume couldn't be loaded. We simply skip one image
280             // size:
281             Dest += this->NumColumns * this->NumLines * this->PixelSize;
282
283             // Update progress related for bad files:
284             UpdateProgressCount += this->NumLines;
285             if (UpdateProgressTarget > 0)
286             {
287                if (!(UpdateProgressCount%UpdateProgressTarget))
288                {
289                   this->UpdateProgress(UpdateProgressCount/(50.0*UpdateProgressTarget));
290                }
291             }
292          } // Else, file not loadable
293       } // Loop on files
294    }
295 }
296
297 /*
298  * vtkGdcmReader can have the file names specified through two ways:
299  * (1) by calling the vtkImageReader2::SetFileName(), SetFilePrefix() and
300  *     SetFilePattern()
301  * (2) By successive calls to vtkGdcmReader::AddFileName()
302  * When the first method was used by caller we need to update the local
303  * filename list
304  */
305 void vtkGdcmReader::BuildFileListFromPattern()
306 {
307    if ((! this->FileNameList.empty()) && this->FileName )
308    {
309       vtkErrorMacro("Both file patterns and AddFileName schemes were used");
310       vtkErrorMacro("Only the files specified with AddFileName shall be used");
311       return;
312    }
313
314    if (! this->FileNameList.empty()  )
315    {
316       vtkDebugMacro("Using the AddFileName specified files");
317       this->InternalFileNameList=this->FileNameList;
318       return;
319    }
320
321    if (!this->FileName && !this->FilePattern)
322    {
323       vtkErrorMacro("FileNames are not set. Either use AddFileName() or");
324       vtkErrorMacro("specify a FileName or FilePattern.");
325       return;
326    }
327
328    this->RemoveAllInternalFileName();
329    if( this->FileNameList.empty() )
330    {
331       //Multiframe case:
332       this->ComputeInternalFileName(this->DataExtent[4]);
333       vtkDebugMacro("Adding file " << this->InternalFileName);
334       this->AddInternalFileName(this->InternalFileName);
335    }
336    else
337    {
338       //stack of 2D dicom case:
339       for (int idx = this->DataExtent[4]; idx <= this->DataExtent[5]; ++idx)
340       {
341          this->ComputeInternalFileName(idx);
342          vtkDebugMacro("Adding file " << this->InternalFileName);
343          this->AddInternalFileName(this->InternalFileName);
344       }
345    }
346 }
347
348 /*
349  * When more than one filename is specified (i.e. we expect loading
350  * a stack or volume) we need to check that the corresponding images/volumes
351  * to be loaded are coherent i.e. to make sure:
352  *     - they all share the same X dimensions
353  *     - they all share the same Y dimensions
354  *     - they all share the same ImageType ( 8 bit signed, or unsigned...)
355  *
356  * Eventually, we emit a warning when all the files do NOT share the
357  * Z dimension, since we can still build a stack but the
358  * files are not coherent in Z, which is probably a source a trouble...
359  *   When files are not readable (either the file cannot be opened or
360  * because gdcm cannot parse it), they are flagged as "GDCM_UNREADABLE".  
361  *   This method returns the total number of planar images to be loaded
362  * (i.e. an image represents one plane, but a volume represents many planes)
363  */
364 int vtkGdcmReader::CheckFileCoherence()
365 {
366    int ReturnedTotalNumberOfPlanes = 0;   // The returned value.
367
368    this->BuildFileListFromPattern();
369    if (this->InternalFileNameList.empty())
370    {
371       vtkErrorMacro("FileNames are not set.");
372       return 0;
373    }
374
375    bool FoundReferenceFile = false;
376    int  ReferenceNZ = 0;
377
378    // Loop on the filenames:
379    // - check for their existence and gdcm "parsability"
380    // - get the coherence check done:
381    for (std::list<std::string>::iterator FileName = InternalFileNameList.begin();
382         FileName != InternalFileNameList.end();
383         ++FileName)
384    {
385       // The file is always added in the number of planes
386       //  - If file doesn't exist, it will be replaced by a black plane in the 
387       //    ExecuteData method
388       //  - If file has more than 1 plane, other planes will be added later to
389       //    to the ReturnedTotalNumberOfPlanes variable counter
390       ReturnedTotalNumberOfPlanes += 1;
391
392       /////// Stage 0: check for file name:
393       if(*FileName==std::string("GDCM_UNREADABLE"))
394          continue;
395
396       /////// Stage 1: check for file readability:
397       // Stage 1.1: check for file existence.
398       FILE *fp;
399       fp = fopen(FileName->c_str(),"rb");
400       if (!fp)
401       {
402          vtkErrorMacro("Unable to open file " << FileName->c_str());
403          vtkErrorMacro("Removing this file from readed files "
404                      << FileName->c_str());
405          *FileName = "GDCM_UNREADABLE";
406          continue;
407       }
408       fclose(fp);
409    
410       // Stage 1.2: check for Gdcm parsability
411       gdcmHeaderHelper GdcmHeader(FileName->c_str());
412       if (!GdcmHeader.IsReadable())
413       {
414          vtkErrorMacro("Gdcm cannot parse file " << FileName->c_str());
415          vtkErrorMacro("Removing this file from readed files "
416                         << FileName->c_str());
417          *FileName = "GDCM_UNREADABLE";
418          continue;
419       }
420
421       // Stage 1.3: further gdcm compatibility on PixelType
422       std::string type = GdcmHeader.GetPixelType();
423       if (   (type !=  "8U") && (type !=  "8S")
424       && (type != "16U") && (type != "16S")
425       && (type != "32U") && (type != "32S") )
426       {
427          vtkErrorMacro("Bad File Type for file" << FileName->c_str());
428          vtkErrorMacro("                      " << type.c_str());
429          vtkErrorMacro("Removing this file from readed files "
430                         << FileName->c_str());
431          *FileName = "GDCM_UNREADABLE";
432          continue;
433       }
434
435       // Stage 2: check coherence of the set of files
436       int NX = GdcmHeader.GetXSize();
437       int NY = GdcmHeader.GetYSize();
438       int NZ = GdcmHeader.GetZSize();
439       if (FoundReferenceFile) 
440       {
441          // Stage 2.1: mandatory coherence stage:
442          if (   ( NX   != this->NumColumns )
443          || ( NY   != this->NumLines )
444          || ( type != this->ImageType ) ) 
445          {
446             vtkErrorMacro("This file is not coherent with previous ones"
447                            << FileName->c_str());
448             vtkErrorMacro("Removing this file from readed files "
449                            << FileName->c_str());
450             *FileName = "GDCM_UNREADABLE";
451             continue;
452          }
453
454          // Stage 2.2: optional coherence stage
455          if ( NZ != ReferenceNZ )
456          {
457             vtkErrorMacro("File is not coherent in Z with previous ones"
458                            << FileName->c_str());
459          }
460          else
461          {
462             vtkDebugMacro("File is coherent with previous ones"
463                            << FileName->c_str());
464          }
465
466          // Stage 2.3: when the file contains a volume (as opposed to an image),
467          // notify the caller.
468          if (NZ > 1)
469          {
470             vtkErrorMacro("This file contains multiple planes (images)"
471                            << FileName->c_str());
472          }
473
474          // Eventually, this file can be added on the stack. Update the
475          // full size of the stack
476          vtkDebugMacro("Number of planes added to the stack: " << NZ);
477          ReturnedTotalNumberOfPlanes += NZ - 1; // First plane already added
478          continue;
479
480       } 
481       else 
482       {
483          // We didn't have a workable reference file yet. Set this one
484          // as the reference.
485          FoundReferenceFile = true;
486          vtkDebugMacro("This file taken as coherence reference:"
487                         << FileName->c_str());
488          vtkDebugMacro("Image dimension of reference file as read from Gdcm:" 
489                         << NX << " " << NY << " " << NZ);
490          vtkDebugMacro("Number of planes added to the stack: " << NZ);
491          // Set aside the size of the image
492          this->NumColumns = NX;
493          this->NumLines   = NY;
494          ReferenceNZ      = NZ;
495          ReturnedTotalNumberOfPlanes += NZ - 1; // First plane already added
496          this->ImageType = type;
497          this->PixelSize = GdcmHeader.GetPixelSize();
498
499          if( GdcmHeader.HasLUT() )
500          {
501             this->NumComponents = GdcmHeader.GetNumberOfScalarComponentsRaw();
502          }
503          else
504          {
505             this->NumComponents = GdcmHeader.GetNumberOfScalarComponents(); //rgb or mono
506          }
507        
508          //Set image spacing
509          this->DataSpacing[0] = GdcmHeader.GetXSpacing();
510          this->DataSpacing[1] = GdcmHeader.GetYSpacing();
511          this->DataSpacing[2] = GdcmHeader.GetZSpacing();
512
513          //Set image origin
514          this->DataOrigin[0] = GdcmHeader.GetXOrigin();
515          this->DataOrigin[1] = GdcmHeader.GetYOrigin();
516          this->DataOrigin[2] = GdcmHeader.GetZOrigin();
517
518       }
519    } // End of loop on FileName
520
521    ///////// The files we CANNOT load are flaged. On debugging purposes
522    // count the loadable number of files and display their number:
523    int NumberCoherentFiles = 0;
524    for (std::list<std::string>::iterator Filename = InternalFileNameList.begin();
525         Filename != InternalFileNameList.end();
526         ++Filename)
527    {
528      if (*Filename != "GDCM_UNREADABLE")
529         NumberCoherentFiles++;    
530    }
531    vtkDebugMacro("Number of coherent files: " << NumberCoherentFiles);
532
533    if (ReturnedTotalNumberOfPlanes == 0)
534    {
535       vtkErrorMacro("No loadable file.");
536    }
537
538    vtkDebugMacro("Total number of planes on the stack: "
539                   << ReturnedTotalNumberOfPlanes);
540    
541    return ReturnedTotalNumberOfPlanes;
542 }
543
544 //-----------------------------------------------------------------------------
545 // Private
546 /*
547  * Remove all file names to the internal list of images to read.
548  */
549 void vtkGdcmReader::RemoveAllInternalFileName(void)
550 {
551    this->InternalFileNameList.clear();
552 }
553
554 /*
555  * Adds a file name to the internal list of images to read.
556  */
557 void vtkGdcmReader::AddInternalFileName(const char* name)
558 {
559    char * LocalName = new char[strlen(name) + 1];
560    strcpy(LocalName, name);
561    this->InternalFileNameList.push_back(LocalName);
562    delete[] LocalName;
563 }
564
565 /*
566  * Loads the contents of the image/volume contained by Filename at
567  * the Dest memory address. Returns the size of the data loaded.
568  */
569 size_t vtkGdcmReader::LoadImageInMemory(
570              std::string FileName, 
571              unsigned char * Dest,
572              const unsigned long UpdateProgressTarget,
573              unsigned long & UpdateProgressCount)
574 {
575    vtkDebugMacro("Copying to memory image" << FileName.c_str());
576    gdcmFile GdcmFile(FileName.c_str());
577    size_t size;
578
579    // If the data structure of vtk for image/volume representation
580    // were straigthforwards the following would suffice:
581    //    GdcmFile.GetImageDataIntoVector((void*)Dest, size);
582    // But vtk chooses to invert the lines of an image, that is the last
583    // line comes first (for some axis related reasons?). Hence we need
584    // to load the image line by line, starting from the end.
585
586    int NumColumns = GdcmFile.GetHeader()->GetXSize();
587    int NumLines   = GdcmFile.GetHeader()->GetYSize();
588    int NumPlanes  = GdcmFile.GetHeader()->GetZSize();
589    int LineSize   = NumComponents * NumColumns * GdcmFile.GetHeader()->GetPixelSize();
590
591    unsigned char * Source;
592    if( GdcmFile.GetHeader()->HasLUT() )
593    {
594       size               = GdcmFile.GetImageDataSizeRaw();
595       Source             = (unsigned char*) GdcmFile.GetImageDataRaw();
596       unsigned char *Lut =                  GdcmFile.GetHeader()->GetLUTRGBA();
597
598       if(!this->LookupTable) 
599          this->LookupTable = vtkLookupTable::New();
600
601       this->LookupTable->SetNumberOfTableValues(256);
602       for (int tmp=0; tmp<256; tmp++)
603       {
604          this->LookupTable->SetTableValue(tmp,
605          (float)Lut[4*tmp+0]/255.0,
606          (float)Lut[4*tmp+1]/255.0,
607          (float)Lut[4*tmp+2]/255.0,
608          1);
609       }
610       this->LookupTable->SetRange(0,255);
611       vtkDataSetAttributes *a=this->GetOutput()->GetPointData();
612       a->GetScalars()->SetLookupTable(this->LookupTable);
613       free(Lut);
614    }
615    else
616    {
617       size        = GdcmFile.GetImageDataSize();
618       Source      = (unsigned char*)GdcmFile.GetImageData();
619    }
620    unsigned char * pSource     = Source; //pointer for later deletion
621    unsigned char * Destination = Dest + size - LineSize;
622
623    for (int plane = 0; plane < NumPlanes; plane++)
624    {
625       for (int line = 0; line < NumLines; line++)
626       {
627          // Copy one line at proper destination:
628          memcpy((void*)Destination, (void*)Source, LineSize);
629          Source      += LineSize;
630          Destination -= LineSize;
631          // Update progress related:
632          if (!(UpdateProgressCount%UpdateProgressTarget))
633          {
634             this->UpdateProgress(UpdateProgressCount/(50.0*UpdateProgressTarget));
635          }
636          UpdateProgressCount++;
637       }
638    }
639    //GetImageData allocate a (void*)malloc, remove it:
640    free(pSource);
641
642    return size;
643 }
644
645 //-----------------------------------------------------------------------------