source: trunk/MagicSoft/Mars/mfileio/MReadTree.cc@ 1541

Last change on this file since 1541 was 1527, checked in by tbretz, 23 years ago
*** empty log message ***
File size: 24.8 KB
Line 
1/* ======================================================================== *\
2!
3! *
4! * This file is part of MARS, the MAGIC Analysis and Reconstruction
5! * Software. It is distributed to you in the hope that it can be a useful
6! * and timesaving tool in analysing Data of imaging Cerenkov telescopes.
7! * It is distributed WITHOUT ANY WARRANTY.
8! *
9! * Permission to use, copy, modify and distribute this software and its
10! * documentation for any purpose is hereby granted without fee,
11! * provided that the above copyright notice appear in all copies and
12! * that both that copyright notice and this permission notice appear
13! * in supporting documentation. It is provided "as is" without express
14! * or implied warranty.
15! *
16!
17!
18! Author(s): Thomas Bretz 12/2000 <mailto:tbretz@uni-sw.gwdg.de>
19!
20! Copyright: MAGIC Software Development, 2000-2002
21!
22!
23\* ======================================================================== */
24
25/////////////////////////////////////////////////////////////////////////////
26// //
27// MReadTree //
28// //
29// This tasks opens all branches in a specified tree and creates the //
30// corresponding parameter containers if not already existing in the //
31// parameter list. //
32// //
33// The Process function reads one events from the tree. To go through the //
34// events of one tree make sure that the event number is increased from //
35// outside. It makes also possible to go back by decreasing the number. //
36// //
37// If you don't want to start reading the first event you have to call //
38// MReadTree::SetEventNum after instantiating your MReadTree-object. //
39// //
40// To make reading much faster (up to a factor of 10 to 20) you can //
41// ensure that only the data you are really processing is enabled by //
42// calling MReadTree::UseLeaf. //
43// //
44// If the chain switches from one file to another file all //
45// TObject::Notify() functions are called of TObject objects which were //
46// added to the Notifier list view MReadTree::AddNotify. If MReadTree //
47// is the owner (viw MReadTree::SetOwner) all this objects are deleted //
48// by the destructor of MReadTree //
49// //
50/////////////////////////////////////////////////////////////////////////////
51#include "MReadTree.h"
52
53#include <fstream.h>
54
55#include <TFile.h> // TFile::GetName
56#include <TSystem.h> // gSystem->ExpandPath
57#include <TGProgressBar.h>
58#include <TChainElement.h>
59#include <TOrdCollection.h>
60
61#include "MLog.h"
62#include "MLogManip.h"
63
64#include "MChain.h"
65#include "MFilter.h"
66#include "MParList.h"
67#include "MTaskList.h"
68
69ClassImp(MReadTree);
70
71// --------------------------------------------------------------------------
72//
73// Default constructor. Don't use it.
74//
75MReadTree::MReadTree()
76 : fNumEntry(0), fBranchChoosing(kFALSE), fAutoEnable(kTRUE), fProgress(NULL)
77{
78 fName = "MReadTree";
79 fTitle = "Task to loop over all events in one single tree";
80
81 fVetoList = NULL;
82 fNotify = NULL;
83
84 fChain = NULL;
85}
86
87// --------------------------------------------------------------------------
88//
89// Constructor. It creates an TChain instance which represents the
90// the Tree you want to read and adds the given file (if you gave one).
91// More files can be added using MReadTree::AddFile.
92// Also an empty veto list is created. This list is used if you want to
93// veto (disable or "don't enable") a branch in the tree, it vetos also
94// the creation of the corresponding object.
95// An empty list of TObjects are also created. This objects are called
96// at any time the TChain starts to read from another file.
97//
98MReadTree::MReadTree(const char *tname, const char *fname,
99 const char *name, const char *title)
100 : fNumEntry(0), fBranchChoosing(kFALSE), fAutoEnable(kTRUE), fProgress(NULL)
101{
102 fName = name ? name : "MReadTree";
103 fTitle = title ? title : "Task to loop over all events in one single tree";
104
105 fVetoList = new TList;
106 fVetoList->SetOwner();
107
108 fNotify = new TList;
109
110 //
111 // open the input stream
112 //
113 fChain = new MChain(tname);
114
115 // root 3.02:
116 // In TChain::Addfile remove the limitation that the file name must contain
117 // the string ".root". ".root" is necessary only in case one wants to specify
118 // a Tree in a subdirectory of a Root file with eg, the format:
119
120 if (fname)
121 fChain->Add(fname);
122}
123
124// --------------------------------------------------------------------------
125//
126// Destructor. It deletes the TChain and veto list object
127//
128MReadTree::~MReadTree()
129{
130 //
131 // Delete all the pointers to pointers to the objects where the
132 // branche data gets stored.
133 //
134 TIter Next(fChain->GetStatus());
135
136 TChainElement *element = NULL;
137 while ((element=(TChainElement*)Next()))
138 delete (MParContainer**)element->GetBaddress();
139
140 //
141 // Delete the chain and the veto list
142 //
143#if ROOT_VERSION_CODE < ROOT_VERSION(3,03,00)
144 if (fChain->GetFile())
145 delete fChain->GetFile();
146#endif
147 delete fChain;
148
149 delete fNotify;
150 delete fVetoList;
151}
152
153// --------------------------------------------------------------------------
154//
155// If the owner flag is set all TObjects which are scheduled via
156// AddNotify are deleted by the destructor of MReadTree
157//
158void MReadTree::SetOwner(Bool_t flag)
159{
160 flag ? fNotify->SetBit(kIsOwner) : fNotify->ResetBit(kIsOwner);
161}
162
163// --------------------------------------------------------------------------
164//
165// This function is called each time MReadTree changes the file to read
166// from. It calls all TObject::Notify() functions which are scheduled
167// via AddNotify.
168//
169Bool_t MReadTree::Notify()
170{
171 *fLog << inf << GetDescriptor() << ": Notify '" << fChain->GetName();
172 *fLog << "' (before processing event #" << GetEventNum()-1 << ")" << endl;
173
174 //fNotify->Notify();
175
176 return kTRUE;
177}
178
179// --------------------------------------------------------------------------
180//
181// If you want to read the given tree over several files you must add
182// the files here before PreProcess is called. Be careful: If the tree
183// doesn't have the same contents (branches) it may confuse your
184// program (trees which are are not existing in later files are not read
185// anymore, tree wich are not existing in the first file are never read)
186//
187// Name may use the wildcarding notation, eg "xxx*.root" means all files
188// starting with xxx in the current file system directory.
189//
190// AddFile returns the number of files added to the chain.
191//
192Int_t MReadTree::AddFile(const char *fname)
193{
194 //
195 // FIXME! A check is missing whether the file already exists or not.
196 //
197 //
198 // returns the number of file which were added
199 //
200 return fChain->Add(fname);
201}
202
203// --------------------------------------------------------------------------
204//
205// Adds all files from another MReadTree to this instance
206//
207// Returns the number of file which were added
208//
209Int_t MReadTree::AddFiles(const MReadTree &read)
210{
211 Int_t rc = 0;
212
213 TIter Next(read.fChain->GetListOfFiles());
214 TObject *obj = NULL;
215 while ((obj=Next()))
216 rc += AddFile(obj->GetTitle());
217
218 return rc;
219}
220
221// --------------------------------------------------------------------------
222//
223// This function is called if Branch choosing method should get enabled.
224// Branch choosing means, that only the enabled branches are read into
225// memory. To use an enableing scheme we have to disable all branches first.
226// This is done, if this function is called the first time.
227//
228void MReadTree::EnableBranchChoosing()
229{
230 if (fBranchChoosing)
231 return;
232
233 *fLog << inf << GetDescriptor() << ": Branch choosing method enabled (only enabled branches are read)." << endl;
234 fChain->SetBranchStatus("*", kFALSE);
235 fBranchChoosing = kTRUE;
236}
237
238// --------------------------------------------------------------------------
239//
240// The first time this function is called all branches are disabled.
241// The given branch is enabled. By enabling only the branches you
242// are processing you can speed up your calculation many times (up to
243// a factor of 10 or 20)
244//
245void MReadTree::EnableBranch(const char *name)
246{
247 EnableBranchChoosing();
248
249 TNamed branch(name, "");
250 SetBranchStatus(&branch, kTRUE);
251}
252
253// --------------------------------------------------------------------------
254//
255// Set branch status of branch name
256//
257void MReadTree::SetBranchStatus(const char *name, Bool_t status)
258{
259 fChain->SetBranchStatus(name, status);
260
261 *fLog << inf << (status ? "Enabled" : "Disabled");
262 *fLog << " subbranch '" << name << "'." << endl;
263}
264
265// --------------------------------------------------------------------------
266//
267// Checks whether a branch with the given name exists in the chain
268// and sets the branch status of this branch corresponding to status.
269//
270void MReadTree::SetBranchStatus(TObject *branch, Bool_t status)
271{
272 //
273 // Get branch name
274 //
275 const char *name = branch->GetName();
276
277 //
278 // Check whether this branch really exists
279 //
280 if (fChain->GetBranch(name))
281 SetBranchStatus(name, status);
282
283 //
284 // Remove trailing '.' if one and try to enable the subbranch without
285 // the master branch name. This is to be compatible with older mars
286 // and camera files.
287 //
288 const char *dot = strrchr(name, '.');
289 if (!dot)
290 return;
291
292 if (fChain->GetBranch(dot+1))
293 SetBranchStatus(dot+1, status);
294}
295
296// --------------------------------------------------------------------------
297//
298// Set the status of all branches in the list to status.
299//
300void MReadTree::SetBranchStatus(const TList *list, Bool_t status)
301{
302 //
303 // Loop over all subbranches in this master branch
304 //
305 TIter Next(list);
306
307 TObject *obj;
308 while ((obj=Next()))
309 SetBranchStatus(obj, status);
310}
311
312// --------------------------------------------------------------------------
313//
314// This is the implementation of the Auto Enabling Scheme.
315// For more information see MTask::AddBranchToList.
316// This function loops over all tasks and its filters in the tasklist
317// and enables all branches which are requested by the tasks and its
318// filters.
319//
320// To enable 'unknown' branches which are not in the branchlist of
321// the tasks you can call EnableBranch
322//
323void MReadTree::EnableBranches(MParList *plist)
324{
325 //
326 // check whether branch choosing must be switched on
327 //
328 EnableBranchChoosing();
329
330 //
331 // request the tasklist from the parameter list.
332 // FIXME: Tasklist can have a different name
333 //
334 const MTaskList *tlist = (MTaskList*)plist->FindObject("MTaskList");
335 if (!tlist)
336 {
337 *fLog << warn << GetDescriptor() << "Cannot use auto enabeling scheme for branches. 'MTaskList' not found." << endl;
338 return;
339 }
340
341 //
342 // This loop is not necessary. We could do it like in the commented
343 // loop below. But this loop makes sure, that we don't try to enable
344 // one branch several times. This would not harm, but we would get
345 // an output for each attempt. To have several outputs for one subbranch
346 // may confuse the user, this we don't want.
347 // This loop creates a new list of subbranches and for each branch
348 // which is added we check before whether it already exists or not.
349 //
350 TList list;
351
352 MTask *task;
353 TIter NextTask(tlist->GetList());
354 while ((task=(MTask*)NextTask()))
355 {
356 TObject *obj;
357
358 TIter NextTBranch(task->GetListOfBranches());
359 while ((obj=NextTBranch()))
360 if (!list.FindObject(obj->GetName()))
361 list.Add(obj);
362
363 const MFilter *filter = task->GetFilter();
364
365 if (!filter)
366 continue;
367
368 TIter NextFBranch(filter->GetListOfBranches());
369 while ((obj=NextFBranch()))
370 if (!list.FindObject(obj->GetName()))
371 list.Add(obj);
372 }
373
374 SetBranchStatus(&list, kTRUE);
375/*
376 //
377 // Loop over all tasks iand its filters n the task list.
378 //
379 MTask *task;
380 TIter NextTask(tlist->GetList());
381 while ((task=(MTask*)NextTask()))
382 {
383 SetBranchStatus(task->GetListOfBranches(), kTRUE);
384
385 const MFilter *filter = task->GetFilter();
386 if (!filter)
387 continue;
388
389 SetBranchStatus(filter->GetListOfBranches(), kTRUE);
390
391 }
392*/
393}
394
395// --------------------------------------------------------------------------
396//
397// The disables all subbranches of the given master branch.
398//
399void MReadTree::DisableSubBranches(TBranch *branch)
400{
401 //
402 // This is not necessary, it would work without. But the output
403 // may confuse the user...
404 //
405 if (fAutoEnable || fBranchChoosing)
406 return;
407
408 SetBranchStatus(branch->GetListOfBranches(), kFALSE);
409}
410
411// --------------------------------------------------------------------------
412//
413// The PreProcess loops (till now) over the branches in the given tree.
414// It checks if the corresponding containers (containers with the same
415// name than the branch name) are existing in the Parameter Container List.
416// If not, a container of objec type 'branch-name' is created (everything
417// after the last semicolon in the branch name is stripped). Only
418// branches which don't have a veto (see VetoBranch) are enabled If the
419// object isn't found in the root dictionary (a list of classes known by the
420// root environment) the branch is skipped and an error message is printed
421// out.
422//
423Bool_t MReadTree::PreProcess(MParList *pList)
424{
425 //
426 // Make sure, that all the following calls doesn't result in
427 // Notifications. This may be dangerous, because the notified
428 // tasks are not preprocessed.
429 //
430 fChain->SetNotify(NULL);
431
432 //
433 // get number of events in this tree
434 //
435 fNumEntries = (UInt_t)fChain->GetEntries();
436
437 if (!fNumEntries)
438 {
439 *fLog << warn << dbginf << "No entries found in file(s)" << endl;
440 return kFALSE;
441 }
442
443 //
444 // output logging information
445 //
446 *fLog << inf << fNumEntries << " entries found in file(s)." << endl;
447
448 //
449 // Get all branches of this tree and
450 // create the Iterator to loop over all branches
451 //
452 TIter Next(fChain->GetListOfBranches());
453 TBranch *branch=NULL;
454
455 Int_t num=0;
456 //
457 // loop over all tasks for processing
458 //
459 while ( (branch=(TBranch*)Next()) )
460 {
461 //
462 // Get Name of Branch and Object
463 //
464 const char *bname = branch->GetName();
465
466 TString oname(bname);
467 if (oname.EndsWith("."))
468 oname.Remove(oname.Length()-1);
469
470 //
471 // Check if enabeling the branch is allowed
472 //
473 if (fVetoList->FindObject(oname))
474 {
475 *fLog << inf << "Master branch " << bname << " has veto... skipped." << endl;
476 DisableSubBranches(branch);
477 continue;
478 }
479
480 //
481 // Create a pointer to the pointer to the object in which the
482 // branch data is stored. The pointers are stored in the TChain
483 // object and we get the pointers from there to delete it.
484 //
485 MParContainer **pcont= new MParContainer*;
486
487#if ROOT_VERSION_CODE < ROOT_VERSION(3,02,06)
488 const char *classname = oname;
489#else
490 const char *classname = branch->GetClassName();
491#endif
492
493 //
494 // check if object is existing in the list
495 //
496 *pcont=pList->FindCreateObj(classname, oname);
497
498 if (!*pcont)
499 {
500 //
501 // if class is not existing in the (root) environment
502 // we cannot proceed reading this branch
503 //
504 *fLog << warn << dbginf << "Warning: Class '" << classname;
505 *fLog << "' for " << oname << " not existing in dictionary. Branch skipped." << endl;
506 DisableSubBranches(branch);
507 continue;
508 }
509
510 //
511 // Check whether a Pointer to a pointer already exists, if
512 // we created one already delete it.
513 //
514 TChainElement *element = (TChainElement*)fChain->GetStatus()->FindObject(bname);
515 if (element)
516 delete (MParContainer**)element->GetBaddress();
517
518 //
519 // here pcont is a pointer the to container in which the data from
520 // the actual branch should be stored - enable branch.
521 //
522 fChain->SetBranchAddress(bname, pcont);
523
524 *fLog << inf << "Master branch address " << bname << " [";
525 *fLog << classname << "] setup for reading." << endl;
526
527 //*fLog << "Branch " << bname << " autodel: " << (int)branch->IsAutoDelete() << endl;
528 //branch->SetAutoDelete();
529
530 num++;
531 }
532
533 *fLog << inf << GetDescriptor() << " setup " << num << " master branches addresses." << endl;
534
535 //
536 // If auto enabling scheme isn't disabled, do auto enabling
537 //
538 if (fAutoEnable)
539 EnableBranches(pList);
540
541 //
542 // If a progress bar is given set its range.
543 //
544 if (fProgress)
545 fProgress->SetRange(0, fNumEntries);
546
547 //
548 // Now we can start notifying. Reset tree makes sure, that TChain thinks
549 // that the correct file is not yet initialized and reinitilizes it
550 // as soon as the first event is read. This is necessary to call
551 // the notifiers when the first event is read, but after the
552 // PreProcess-function.
553 //
554 fChain->ResetTree();
555 fChain->SetNotify(this);
556
557 return kTRUE;
558}
559
560// --------------------------------------------------------------------------
561//
562// Set the ready to save flag of all containers which branchaddresses are
563// set for. This is necessary to copy data.
564//
565void MReadTree::SetReadyToSave(Bool_t flag)
566{
567 TIter Next(fChain->GetStatus());
568
569 TChainElement *element = NULL;
570 while ((element=(TChainElement*)Next()))
571 {
572 //
573 // Check whether the branch is enabled
574 //
575 if (!element->GetStatus())
576 continue;
577
578 //
579 // Get the pointer to the pointer of the corresponding container
580 //
581 MParContainer **pcont = (MParContainer**)element->GetBaddress();
582
583 //
584 // Check whether the pointer is not NULL
585 //
586 if (!pcont || !*pcont)
587 continue;
588
589 //
590 // Set the ready to save status of the container.
591 //
592 (*pcont)->SetReadyToSave(flag);
593 }
594
595 //
596 // Set the ready to save status of this task (used?), too
597 //
598 MTask::SetReadyToSave(flag);
599}
600
601// --------------------------------------------------------------------------
602//
603// The Process-function reads one event from the tree (this contains all
604// enabled branches) and increases the position in the file by one event.
605// (Remark: The position can also be set by some member functions
606// If the end of the file is reached the Eventloop is stopped.
607//
608#if ROOT_VERSION_CODE < ROOT_VERSION(3,02,06)
609#include "MRawEvtData.h"
610#endif
611Bool_t MReadTree::Process()
612{
613 //
614 // This is necessary due to a bug in TChain::LoadTree in root.
615 // will be fixed in 3.03
616 //
617#if ROOT_VERSION_CODE < ROOT_VERSION(3,03,01)
618 if (fNumEntry >= fNumEntries)
619 return kFALSE;
620#endif
621
622#if ROOT_VERSION_CODE < ROOT_VERSION(3,02,06)
623 //
624 // This fixes 99.9% of a memory leak using a root version prior
625 // to 3.02/??
626 //
627 TChainElement *element=NULL;
628 TIter Next(fChain->GetStatus());
629 while ((element=(TChainElement*)Next()))
630 {
631 MParContainer **c = (MParContainer**)element->GetBaddress();
632 if (!c) continue;
633 if ((*c)->InheritsFrom(MRawEvtData::Class()))
634 ((MRawEvtData*)(*c))->DeletePixels(kFALSE);
635
636 }
637#endif
638
639 Bool_t rc = fChain->GetEntry(fNumEntry++) != 0;
640
641 if (rc)
642 SetReadyToSave();
643
644 return rc;
645}
646
647// --------------------------------------------------------------------------
648//
649// Get the Event with the current EventNumber fNumEntry
650//
651Bool_t MReadTree::GetEvent()
652{
653 Bool_t rc = fChain->GetEntry(fNumEntry) != 0;
654
655 if (rc)
656 SetReadyToSave();
657
658 return rc;
659}
660
661// --------------------------------------------------------------------------
662//
663// Decrease the number of the event which is read by Process() next
664// by one or more
665//
666Bool_t MReadTree::DecEventNum(UInt_t dec)
667{
668 if (fNumEntry-dec >= fNumEntries)
669 {
670 *fLog << warn << GetDescriptor() << ": DecEventNum, WARNING - Event " << fNumEntry << "-";
671 *fLog << dec << "=" << (Int_t)fNumEntry-dec << " out of Range." << endl;
672 return kFALSE;
673 }
674
675 fNumEntry -= dec;
676 return kTRUE;
677}
678
679// --------------------------------------------------------------------------
680//
681// Increase the number of the event which is read by Process() next
682// by one or more
683//
684Bool_t MReadTree::IncEventNum(UInt_t inc)
685{
686 if (fNumEntry+inc >= fNumEntries)
687 {
688 *fLog << warn << GetDescriptor() << ": IncEventNum, WARNING - Event " << fNumEntry << "+";
689 *fLog << inc << "=" << (Int_t)fNumEntry+inc << " out of Range." << endl;
690 return kFALSE;
691 }
692
693 fNumEntry += inc;
694 return kTRUE;
695}
696
697// --------------------------------------------------------------------------
698//
699// This function makes Process() read event number nr next
700//
701// Remark: You can use this function after instatiating you MReadTree-object
702// to set the event number from which you want to start reading.
703//
704Bool_t MReadTree::SetEventNum(UInt_t nr)
705{
706 if (nr >= fNumEntries)
707 {
708 *fLog << warn << GetDescriptor() << ": SetEventNum, WARNING - " << nr << " out of Range." << endl;
709 return kFALSE;
710 }
711
712 fNumEntry = nr;
713 return kTRUE;
714}
715
716// --------------------------------------------------------------------------
717//
718// For the branch with the given name:
719// 1) no object is automatically created
720// 2) the branch address for this branch is not set
721// (because we lack the object, see 1)
722// 3) The whole branch (exactly: all its subbranches) are disabled
723// this means are not read in memory by TTree:GetEntry
724//
725void MReadTree::VetoBranch(const char *name)
726{
727 fVetoList->Add(new TNamed(name, ""));
728}
729
730// --------------------------------------------------------------------------
731//
732// Return the name of the file we are actually reading from.
733//
734TString MReadTree::GetFileName() const
735{
736 const TFile *file = fChain->GetFile();
737
738 if (!file)
739 return TString("<unknown>");
740
741 TString name(file->GetName());
742 name.Remove(0, name.Last('/')+1);
743 return name;
744}
745
746// --------------------------------------------------------------------------
747//
748// Return the number of the file in the chain, -1 in case of an error
749//
750Int_t MReadTree::GetFileIndex() const
751{
752 return fChain->GetTreeNumber();
753 /*
754 const TString filename = fChain->GetFile()->GetName();
755
756 int i=0;
757 TObject *file = NULL;
758
759 TIter Next(fChain->GetListOfFiles());
760 while ((file=Next()))
761 {
762 if (filename==gSystem->ExpandPathName(file->GetTitle()))
763 return i;
764 i++;
765 }
766 return -1;
767 */
768}
769
770// --------------------------------------------------------------------------
771//
772// This schedules a TObject which Notify(9 function is called in case
773// of MReadTree (TChain) switches from one file in the chain to another
774// one.
775//
776void MReadTree::AddNotify(TObject *obj)
777{
778 fNotify->Add(obj);
779}
780
781void MReadTree::Print(Option_t *o) const
782{
783 *fLog << all << GetDescriptor() << dec << endl;
784 *fLog << setfill('-') << setw(strlen(GetDescriptor())) << "" << endl;
785 *fLog << " Files [Tree]:" << endl;
786
787 int i = 0;
788 TIter Next(fChain->GetListOfFiles());
789 TObject *obj = NULL;
790 while ((obj=Next()))
791 *fLog << " " << i++ << ") " << obj->GetTitle() << " [" << obj->GetName() << "]" << endl;
792
793 *fLog << " Total Number of Entries: " << fNumEntries << endl;
794 *fLog << " Next Entry to read: " << fNumEntry << endl;
795}
796
797// --------------------------------------------------------------------------
798//
799// Implementation of SavePrimitive. Used to write the call to a constructor
800// to a macro. In the original root implementation it is used to write
801// gui elements to a macro-file.
802//
803void MReadTree::StreamPrimitive(ofstream &out) const
804{
805 out << " " << ClassName() << " " << GetUniqueName() << "(\"";
806 out << fChain->GetName() << "\", \"" << fName << "\", \"" << fTitle << "\");" << endl;
807
808 TIter Next(fChain->GetListOfFiles());
809 TObject *obj = NULL;
810 while ((obj=Next()))
811 out << " " << GetUniqueName() << ".AddFile(\"" << obj->GetTitle() << "\");" << endl;
812
813 if (!fAutoEnable)
814 out << " " << GetUniqueName() << ".DisableAutoScheme();" << endl;
815
816 if (fNumEntry!=0)
817 out << " " << GetUniqueName() << ".SetEventNum(" << fNumEntry << ");" << endl;
818
819
820}
Note: See TracBrowser for help on using the repository browser.