source: Sophya/trunk/SophyaPI/PIext/pintuple.cc@ 4072

Last change on this file since 4072 was 4071, checked in by ansari, 13 years ago

Trace en coordonnees polaires (2D avec barres d'erreur) classe PINTuple

et introduction des attributs graphiques polarplot, polaraxes ...

Reza, 9 Mai 2012

File size: 17.6 KB
Line 
1// Peida Interactive - PI R. Ansari 97-99
2// Traceur (Drawer) pour NTupleInterface
3// LAL (Orsay) / IN2P3-CNRS DAPNIA/SPP (Saclay) / CEA
4
5#include <stdio.h>
6#include <stdlib.h>
7#include <iostream>
8#include <math.h>
9#include "sopnamsp.h"
10#include "pintuple.h"
11
12//++
13// Class PINTuple
14// Lib PIext
15// include pintuple.h
16//
17// Classe de traceur 2D (dans un plan) à partir des données
18// d'un objet implémentant l'interface *NTupleInterface*.
19// Les objets "PINTuple" peuvent tracer des signes (markers)
20// éventuellement avec des barres d'erreur et une étiquette
21// pour chaque point. Si un nom de colonne poids est spécifié,
22// la taille et/ou la couleur des signes (markers) sera fonction
23// de la valeur de poids.
24//--
25//++
26// Links Parents
27// PIDrawer
28//--
29//++
30// Links Voir aussi
31// NTupleInterface
32// PINTuple3D
33//--
34
35//++
36// Titre Constructeur
37//--
38//++
39// PINTuple(NTupleInterface* nt, bool ad)
40// Constructeur. Si "ad == true", l'objet "nt" est détruit par
41// le destructeur de l'objet "PINTuple"
42// Note : "nt" doit être créé par new dans ce cas.
43//--
44
45/* --Methode-- */
46PINTuple::PINTuple(NTupleInterface* nt, bool ad)
47: PIDrawer()
48{
49 mNT = nt;
50 mAdDO = ad;
51 SetStats(true);
52 SetStatPosOffset();
53 ConnectPoints(false);
54 UseSizeScale(true, 5);
55 UseColorScale(true);
56 SelectXY(NULL, NULL);
57 SelectWt(NULL);
58 SelectErrBar();
59 SelectLabel(NULL);
60 SelectColorByName(NULL);
61 SelectColorByIndex(NULL);
62 SelectPolar(NULL,NULL);
63 SetName("NTupleDrw");
64 NptDraw=0;
65 polarPlot=false;
66}
67
68PINTuple::~PINTuple()
69{
70 if (mAdDO && mNT) delete mNT;
71}
72
73//++
74// Titre Méthodes
75//--
76//++
77// void SelectXY(const char* px, const char* py)
78// Choix des noms de colonnes X,Y définissant les coordonnées des points.
79// Ces deux colonnes doivent être spécifiées pour obtenir un tracé.
80// void SelectErrBar(const char* erbx=NULL, const char* erby=NULL)
81// Choix des noms de colonnes pour le tracé des barres d'erreur.
82// void SelectWt(const char* pw=NULL, int nbins=10)
83// Choix du nom de colonne poids. Dans ce cas, la taille du signe
84// (marker) sera proportionnel à la valeur de cette colonne pour
85// chaque point.
86// void SelectLabel(const char* plabel=NULL)
87// Choix du nom de colonne correspondant à l'étiquette.
88// void SelectPolar(const char* pradius, const char* pangle, double angconv=1., double angoffser=0.)
89// Choix des noms de colonnes rayon/angle pour un trace en coordonnees polaires
90//--
91
92/* --Methode-- */
93void PINTuple::SelectXY(const char* px, const char* py)
94{
95string name;
96if (mNT == NULL) xK = yK = -1;
97if (px == NULL) xK = -1;
98else { name = px; xK = mNT->ColumnIndex(name); }
99if (py == NULL) yK = -1;
100else { name = py; yK = mNT->ColumnIndex(name); }
101polarPlot=false;
102}
103
104/* --Methode-- */
105void PINTuple::SelectWt(const char* pw)
106{
107if (pw == NULL) wK = -1;
108else { string name = pw; wK = mNT->ColumnIndex(name); }
109
110if (wK >= 0) mNT->GetMinMax(wK, wMin, wMax);
111else { wMin = 0.; wMax = 1.; }
112}
113
114/* --Methode-- */
115void PINTuple::SelectLabel(const char* plabel)
116{
117if (plabel == NULL) lK = -1;
118else { string name = plabel; lK = mNT->ColumnIndex(name); }
119}
120
121/* --Methode-- */
122void PINTuple::SelectColorByName(const char* pcolnm)
123{
124if (pcolnm == NULL) cnK = -1;
125else { string name = pcolnm; cnK = mNT->ColumnIndex(name); }
126if (cnK >= 0) ciK = -1;
127}
128
129/* --Methode-- */
130void PINTuple::SelectColorByIndex(const char* pcolidx)
131{
132if (pcolidx == NULL) ciK = -1;
133else { string name = pcolidx; ciK = mNT->ColumnIndex(name); }
134if (ciK >= 0) cnK = -1;
135}
136
137
138/* --Methode-- */
139void PINTuple::SelectErrBar(const char* erbx, const char* erby)
140{
141string name;
142if (mNT == NULL) xebK = yebK = -1;
143if (erbx == NULL) xebK = -1;
144else { name = erbx; xebK = mNT->ColumnIndex(name); }
145if (erby == NULL) yebK = -1;
146else { name = erby; yebK = mNT->ColumnIndex(name); }
147}
148
149/* --Methode-- */
150void PINTuple::SelectPolar(const char* pangle, const char* pradius, double angconv, double angoff)
151{
152string name;
153if (mNT == NULL) xK = yK = -1;
154if (pangle == NULL) xK = -1;
155else { name = pangle; xK = mNT->ColumnIndex(name); }
156if (pradius == NULL) yK = -1;
157else { name = pradius; yK = mNT->ColumnIndex(name); }
158polarPlot=true;
159angConvFactor=angconv;
160angOffset=angoff;
161}
162
163/* --Methode-- */
164void PINTuple::UpdateLimits()
165{
166 if (!mNT) return;
167 if (mNT->NbLines() <= 0) return;
168 if ( (xK < 0) || (yK < 0) ) return;
169
170 // Commencer par trouver nos limites
171 double xmin, xmax, ymin, ymax;
172 if (polarPlot) {
173 double rmin,rmax;
174 mNT->GetMinMax(yK, rmin, rmax);
175 if (rmax<=0.) rmax=1.;
176 xmin=ymin=-rmax;
177 xmax=ymax=rmax;
178 }
179 else {
180 xmin = ymin = 9.e19;
181 xmax = ymax = -9.e19;
182 mNT->GetMinMax(xK, xmin, xmax);
183 mNT->GetMinMax(yK, ymin, ymax);
184 PIAxes::ReSizeMinMax(isLogScaleX(),xmin,xmax);
185 PIAxes::ReSizeMinMax(isLogScaleY(),ymin,ymax);
186 }
187 SetLimits(xmin,xmax,ymin,ymax);
188// SetAxesFlags(kBoxAxes | kExtTicks | kLabels); Ne pas faire - Reza 11/99
189}
190
191
192/* --Methode-- */
193#define NMXMULTP_LOCAL 30 // Pour multipoint sans new
194void PINTuple::Draw(PIGraphicUC* g, double xmin, double ymin, double xmax, double ymax)
195{
196double xp,yp,xer,yer,wp;
197double ray,ang;
198double xl,yl;
199long nok;
200int npolyg;
201
202if (!mNT) return;
203if (axesFlags != kAxesNone) DrawAxes(g);
204if ( (xK < 0) || (yK < 0) ) return;
205if (GetGraphicAtt().GetLineAtt() == PI_NotDefLineAtt) g->SelLine(PI_ThinLine);
206
207// Pour tracer des markers avec taille fonction de Wt (poids)
208double dw = (wMax-wMin)/nWbins;
209if (dw < 1.e-19) dw = 1.e-19;
210
211// La couleur par defaut (pour le trace avec couleur specifie par nom de colonne (06/2007)
212PIColors defcol = GetGraphicAtt().GetColor();
213if (defcol == PI_NotDefColor) defcol = PI_Black;
214
215// Pour tracer des markers avec couleur en fonction de Wt (poids)
216PIColorMap * cmap = NULL;
217double dwc = 1.;
218int nwc = 1;
219bool revcmap;
220CMapId mcmapid = GetGraphicAtt().GetColMapId(revcmap);
221if( colorScale && (wK >= 0) && (mcmapid != CMAP_OTHER) ) {
222 cmap = new PIColorMap(mcmapid);
223 cmap->ReverseColorIndex(revcmap);
224 nwc = cmap->NCol();
225 dwc = (wMax-wMin)/(double)nwc;
226}
227
228int msz,sz;
229
230PIMarker mmrk = GetGraphicAtt().GetMarker();
231PIMarker mrk;
232if (wK >= 0) mrk = (mmrk != PI_NotDefMarker) ? mmrk : PI_CircleMarker;
233else mrk = (mmrk != PI_NotDefMarker) ? mmrk : PI_DotMarker;
234msz = GetGraphicAtt().GetMarkerSz();
235if (msz < 1) msz = 1;
236g->SelMarker(msz, mrk);
237
238PIGrCoord uxmin, uxmax, uymin, uymax;
239g->GetGrSpace(uxmin, uxmax, uymin, uymax);
240double xmin2 = uxmin;
241double ymin2 = uymin;
242double xmax2 = uxmax;
243double ymax2 = uymax;
244
245nok = 0;
246xp = yp = xl = yl = 0;
247PIGrCoord xpolyg[NMXMULTP_LOCAL], ypolyg[NMXMULTP_LOCAL];
248npolyg = 0;
249NptDraw = 0;
250
251// Mai 2005: Correction bug trace des lignes avec le pts en dehors de la zone (Reza)
252// flags pour la gestion des points/lignes a tracer
253bool fgokcurpt = true; // Point courant dans la zone a tracer
254bool fgoklastpt = true; // Le dernier point etait dans la zone a tracer
255for (long i=0; i<(long)mNT->NbLines(); i++) {
256 xl = xp; yl = yp; fgoklastpt = fgokcurpt;
257 if (polarPlot) {
258 ang = mNT->GetCell(i, xK)*angConvFactor+angOffset;
259 ray = mNT->GetCell(i, yK);
260 xp=cos(ang)*ray;
261 yp=sin(ang)*ray;
262 }
263 else {
264 xp = mNT->GetCell(i, xK);
265 yp = mNT->GetCell(i, yK);
266 }
267 // Comptage du nombre de pts dans la zone graphique du widget
268 if ( !((xp < xmin2) || (xp > xmax2) || (yp < ymin2) || (yp > ymax2)) ) nok++;
269 // Gestion des limites a tracer
270 fgokcurpt = true;
271 if ( (xp < xmin) || (xp > xmax) || (yp < ymin) || (yp > ymax) ) fgokcurpt = false;
272 else NptDraw++;
273
274// Taille - couleur de marker en fonction du poids
275 if ( fgokcurpt &&(wK >= 0) ) wp = mNT->GetCell(i, wK);
276 if ( fgokcurpt && mrkSzScale && (wK >= 0) ) { // Changement de taille
277 sz = (int)((wp-wMin)/dw);
278 if (sz < 0) sz = 0;
279 if (sz > nWbins) sz = nWbins;
280 sz += msz;
281 if (sz < 2) g->SelMarker(sz, PI_DotMarker);
282 else g->SelMarker(sz, mrk);
283 }
284// Couleur du marker en fonction du poids
285 if( fgokcurpt && colorScale && (wK >= 0) && cmap ) {
286 int cid = (int)((wp-wMin)/dwc);
287 if (cid < 0) cid = 0;
288 if (cid >= nwc) cid = nwc-1;
289 g->SelForeground(*cmap, cid);
290 }
291
292// Si on a specifie un nom de colonne pour la couleur (06/2007)
293 if ( (cnK >= 0) || (ciK >= 0) ) {
294 PIColors scol = defcol;
295 if (cnK > 0)
296 scol = PIGraphicAtt::ColNameToColor(mNT->GetCelltoString(i, cnK));
297 else
298 scol = PIGraphicAtt::ColIndexToColor(mNT->GetCell(i, ciK));
299 if (scol == PI_NotDefColor) scol = defcol;
300 g->SelForeground(scol);
301 }
302
303 // Trace d'une ligne reliant les points
304 if( connectPts ) {
305 // On initialise le polygone avec le dernier point, si ce dernier ok
306 if((npolyg==0) && (i>0) && fgokcurpt) {xpolyg[0]=xl; ypolyg[0]=yl; npolyg=1;}
307 // On ajoute le point courant au polygone - sauf si celui-ci est vide
308 if((npolyg<NMXMULTP_LOCAL) && (npolyg>0))
309 {xpolyg[npolyg]=xp; ypolyg[npolyg]=yp; npolyg++;}
310 // On trace le polygone s'il est plein - ou le point courant en dehors
311 // (point courant en dehors -> discontinuite de la ligne
312 if( !fgokcurpt || (npolyg==NMXMULTP_LOCAL) )
313 {g->DrawPolygon(xpolyg,ypolyg,npolyg,false); npolyg=0;}
314 }
315
316 // Plus rien a faire si point en dehors -- on s'en va
317 if (!fgokcurpt) continue;
318
319 // Trace des erreurs selon X et Y
320 if (xebK >= 0) {
321 xer = mNT->GetCell(i, xebK);
322 if(xer>0.) {
323 if (polarPlot) {
324 g->DrawLine(xp,yp,ray*cos(ang-yer*angConvFactor),ray*sin(ang-yer*angConvFactor));
325 g->DrawLine(xp,yp,ray*cos(ang+yer*angConvFactor),ray*sin(ang+yer*angConvFactor));
326 }
327 else g->DrawLine(xp-xer, yp, xp+xer, yp);
328 }
329 }
330 if (yebK >= 0) {
331 yer = mNT->GetCell(i, yebK);
332 if(yer>0.) {
333 if (polarPlot) {
334 double ray1=ray-xer; if (ray1<0.) ray1=0.;
335 double ray2=ray+xer; if (ray2<0.) ray2=0.;
336 g->DrawLine(ray1*cos(ang), ray1*sin(ang), ray2*cos(ang), ray2*sin(ang));
337 }
338 else g->DrawLine(xp, yp-yer, xp, yp+yer);
339 }
340 }
341
342 // Trace du marker
343 if ((wK >= 0)||(lK < 0)||(mmrk != PI_NotDefMarker)) g->DrawMarker(xp, yp);
344
345 // Trace eventuel du label
346 if (lK >= 0) g->DrawString(xp, yp, mNT->GetCelltoString(i, lK).c_str());
347
348}
349
350// Fin du trace d'une ligne reliant les points si necessaire
351if( connectPts && npolyg>1 )
352 {g->DrawPolygon(xpolyg,ypolyg,npolyg,false); npolyg=0;}
353
354if (stats) { // Trace de stats
355 g->SelForeground(defcol);
356// Reza(12/2008): utilisation de flag et fraction pour selection automatique taille de fonte
357 if (mAFSz) {
358 double fsz = (YMax() - YMin())*mFontSzF;
359 g->SelFontSz(fsz); // au lieu de 1/30*(ymax-ymin)
360 }
361 // La hauteur de la cellule
362 PIGrCoord a,d;
363 double cH = (double)g->GetFontHeight(a,d);
364 double cellHeight = 1.2 * cH;
365 // Les labels et leurs longueurs -> largeur de la cellule
366 char label[64];
367 sprintf(label, "N=%ld (/%ld)", (long)nok, (long)mNT->NbLines());
368 double cellWidth = 1.1 * (double)g->CalcStringWidth(label);
369 double xu, yu, cw;
370 double ofpx = spoX*(XMax()-XMin());
371 double ofpy = spoY*(YMax()-YMin());
372 // Les limites du cadre
373 xu = g->DeltaUCX(XMax(), - cellWidth);
374 yu = g->DeltaUCY(YMax(), - cellHeight);
375 double recw = XMax()-xu;
376 double rech = YMax()-yu;
377 xu += ofpx; yu += ofpy;
378 g->DrawBox(xu, yu, recw, rech);
379 // L'ecriture des labels (attention aux inversions possibles des axes!)
380 cw = (g->isAxeXDirRtoL()) ? -0.05*cellWidth : -0.95*cellWidth;
381 xu = g->DeltaUCX(XMax(),cw);
382 cw = (g->isAxeYDirUpDown()) ? -0.1*cH : -1.1*cH;
383 yu = g->DeltaUCY(YMax(),cw);
384 xu += ofpx; yu += ofpy;
385 g->DrawString(xu,yu,label);
386}
387
388if (cmap) delete cmap;
389return;
390}
391#undef NMXMULTP_LOCAL
392
393/* --Methode-- */
394void PINTuple::AppendTextInfo(string& info, double xmin, double ymin, double xmax, double ymax)
395{
396if (!mNT) return;
397if ( (xK < 0) || (yK < 0) ) return;
398
399long ncnt = 0;
400double xp,yp;
401char buff[128];
402sprintf(buff,"PINTuple: NLines= %ld NCol= %ld \n", (long)mNT->NbLines(),
403 (long)mNT->NbColumns());
404info += buff;
405info += mNT->LineHeaderToString();
406for(long i=0; i<(long)mNT->NbLines(); i++) {
407 xp = mNT->GetCell(i, xK);
408 yp = mNT->GetCell(i, yK);
409 if ( (xp < xmin) || (xp > xmax) || (yp < ymin) || (yp > ymax) ) continue;
410 ncnt++;
411 if (ncnt > 101) continue;
412 info += mNT->LineToString(i);
413 }
414if (ncnt >= 101) info += " .... \n";
415sprintf(buff," %ld points inside selected region \n", (long)ncnt);
416info += buff;
417// printf("PINTuple::AppendTextInfo()-DBG %g %g %g %g - %d\n", xmin, ymin, xmax, ymax, ncnt);
418return;
419}
420
421/* La methode DecodeOptionString permet de decoder un ensemble d'options
422 et de parametre d'affichage specifie sous forme d'un vecteur de string.
423 Si rmdecopt == true, les options decodees sont supprimees du vecteur
424 de string fourni en entree - ce qui permet l'enchainement eventuel
425 de plusieurs decodages de string.
426 Les options peuvent etre sous forme de flag : "stat" "nostat"
427 ou plus complexes, par exemple "dynamic=-3,3"
428 Rc: La methode renvoie le nombre d'options decodees
429*/
430
431/* --Methode-- */
432int PINTuple::DecodeOptionString(vector<string> & opt, bool rmdecopt)
433{
434 int optsz1 = opt.size();
435 if(optsz1<1) return(0);
436 // On appelle d'abord le decodage de la classe PIDrawer de laquelle
437 // on herite. (Pas obligatoire) on decode donc ici les attributs de
438 // couleur, fontes ...
439 int ndec1 = PIDrawer::DecodeOptionString(opt, rmdecopt);
440 if(optsz1-ndec1<1) return(ndec1); // si tout a ete decode
441
442 vector<string> udopt; // On gardera ici les options non decodees
443 unsigned int k = 0;
444 int ndec = opt.size();
445 for( k=0; k<opt.size(); k++ ) {
446 string opts = opt[k];
447 if(opts=="sta" || opts=="stat" || opts=="stats") SetStats(true);
448 else if( opts=="nsta" || opts=="nstat"
449 || opts=="nostat" || opts=="nostats") SetStats(false);
450 else if(opts.substr(0,11) == "statposoff=") {
451 double xo=0., yo=0.;
452 sscanf(opts.substr(11).c_str(),"%lf,%lf",&xo, &yo);
453 SetStatPosOffset(xo, yo);
454 }
455 else if (opts == "connectpoints") ConnectPoints(true);
456 else if (opts == "cpts") ConnectPoints(true);
457 else if (opts == "noconnectpoints") ConnectPoints(false);
458 else if (opts == "colorscale") UseColorScale(true);
459 else if (opts == "nocolorscale") UseColorScale(false);
460 else if (opts == "sizescale") UseSizeScale(true);
461 else if (opts == "nosizescale") UseSizeScale(false);
462 else if (opts.substr(0,10) == "sizescale=") {
463 int nbn = atoi(opts.substr(10).c_str());
464 UseSizeScale(true, nbn);
465 }
466 // gestion trace en coordonnees polaires
467 else if (opts == "polarplot") { polarPlot=true; }
468 else if (opts == "polarangledeg") { angConvFactor=M_PI/180.; }
469 else if (opts.substr(0,11) == "polarangle=") {
470 double aconv=1., aoff=0.;
471 sscanf(opts.substr(11).c_str(),"%lf,%lf",&aconv, &aoff);
472 angConvFactor=aconv; angOffset=aoff;
473 }
474 else {
475 // Si option non decode
476 ndec--;
477 // S'il faut supprimer les options decodees
478 if (rmdecopt) udopt.push_back(opts);
479 }
480 }
481 // S'il faut supprimer les options decodees, on remplace l'argument opt
482 // par le vecteur des options non decodees.
483 if (rmdecopt) opt = udopt;
484 return(ndec+ndec1);
485}
486
487int PINTuple::OptionToString(vector<string> & opt) const
488{
489 PIDrawer::OptionToString(opt);
490
491 char str[256];
492
493 if(stats) opt.push_back("stat"); else opt.push_back("nstat");
494
495 sprintf(str,"statposoff=%lf,%lf",spoX,spoY); opt.push_back(str);
496
497 if(connectPts) opt.push_back("connectpoints");
498 else opt.push_back("noconnectpoints");
499
500 if(colorScale) opt.push_back("colorscale");
501 else opt.push_back("nocolorscale");
502
503 if(mrkSzScale) {
504 if(nWbins>0) {sprintf(str,"sizescale=%d",nWbins); opt.push_back(str);}
505 else opt.push_back("sizescale");
506 } else opt.push_back("nosizescale");
507
508 if (polarPlot) {
509 sprintf(str,"polarplot polarangle==%g,%g",angConvFactor,angOffset); opt.push_back(str);
510 }
511
512 return 1;
513}
514
515/* La methode GetOptionsHelpInfo(string& info) renvoie une chaine
516 avec la description des options comprises par ce drawer
517 Note: Il est preferable de ne pas initialiser la chaine
518 string info au depart, afin de permettre de mettre bout a
519 bout les aides de differents Drawer */
520
521/* --Methode-- */
522void PINTuple::GetOptionsHelpInfo(string& info)
523{
524info += " ---- PINTuple options help info : \n" ;
525info += " sta,stat,stats: activate statistic display\n";
526info += " nsta,nstat,nostat,nostats: deactivate statistic display\n";
527info += " statposoff=OffsetX,OffsetY : Position offset for Stats drawing \n";
528info += " as a fraction of total size \n";
529info += " connectpoints: The points are connected by a line \n";
530info += " noconnectpoints (this is the default) \n";
531info += " colorscale/nocolorscale (Use color scale for weight) \n";
532info += " sizescale/sizescale=nbins/nosizescale (Use marker size for weight) \n";
533info += " polarplot: select plot in polar coordinates \n";
534info += " polarangle=ConvFactorToRadian,AngOffsetinRadian Polar angle conversion \n";
535info += " polarangledeg: set ConvFactorToRadian for input angles in degree \n";
536info += " (and usual color/line/marker/... attribute decoding) \n";
537// On recupere ensuite la chaine info de la classe de base
538PIDrawer::GetOptionsHelpInfo(info);
539return;
540}
541
542
543/* --Methode-- */
544double PINTuple::GetDistanceToPoint(double x, double y)
545{
546 if(!mNT) return 1.e+9;
547 if( xK<0 || yK<0 ) return 1.e+9;
548
549 const int nessai = 100;
550 long inc = (NptDraw>nessai) ? (long)(NptDraw/nessai)+1 : 1;
551
552 double dist = -1.e+18;
553 long n = 0;
554 for(long i=0; i<(long)mNT->NbLines(); i++) {
555 double xp=mNT->GetCell(i,xK);
556 if(xp<XMin() || xp>XMax()) continue;
557 double yp=mNT->GetCell(i,yK);
558 if(yp<YMin() || yp>YMax()) continue;
559 if(n%inc==0) {
560 xp = (xp-x)/(XMax()-XMin())/0.5;
561 yp = (yp-y)/(YMax()-YMin())/0.5;
562 xp = xp*xp+yp*yp;
563 if(dist<0. || xp<dist) dist = xp;
564 }
565 n++;
566 }
567 dist=sqrt(fabs(dist));
568 //cout<<"PINTuple: xlim="<<XMin()<<","<<XMax()<<" ylim="<<YMin()<<","<<YMax()
569 // <<" NbLines="<<mNT->NbLines()<<" inc="<<inc<<endl;
570 //cout<<"....d="<<dist<<" x="<<x<<" y="<<y<<" NptDraw="<<NptDraw<<endl;
571
572 return dist;
573}
Note: See TracBrowser for help on using the repository browser.