source: trunk/Mars/mcore/ofits.h@ 13782

Last change on this file since 13782 was 13115, checked in by lyard, 13 years ago
moved EXTNAME keyword further down for compatibility with fverify
File size: 23.1 KB
Line 
1#ifndef MARS_ofits
2#define MARS_ofits
3
4#include <string>
5#include <string.h>
6#include <algorithm>
7#include <sstream>
8#include <iostream>
9#include <fstream>
10#include <iomanip>
11#include <vector>
12#include <inttypes.h>
13#include <algorithm>
14
15#ifdef __EXCEPTIONS
16#include <stdexcept>
17#endif
18
19#include "checksum.h"
20
21namespace std
22{
23// Sloppy: allow / <--- left
24// allow all characters (see specs for what is possible)
25
26// units: m kg s rad sr K A mol cd Hz J W V N Pa C Ohm S F Wb T Hlm lx
27
28class ofits : public ofstream
29{
30 struct Key
31 {
32 string key;
33 bool delim;
34 string value;
35 string comment;
36
37 off_t offset; // offset in file
38
39 bool changed; // For closing the file
40
41 Key(const string &k="") : key(k), delim(false), offset(0), changed(true) { }
42
43 string Trim(const string &str)
44 {
45 // Trim Both leading and trailing spaces
46 const size_t first = str.find_first_not_of(' '); // Find the first character position after excluding leading blank spaces
47 const size_t last = str.find_last_not_of(' '); // Find the first character position from reverse af
48
49 // if all spaces or empty return an empty string
50 if (string::npos==first || string::npos==last)
51 return string();
52
53 return str.substr(first, last-first+1);
54 }
55
56 bool FormatKey()
57 {
58 key = Trim(key);
59 if (key.size()==0)
60 {
61#ifdef __EXCEPTIONS
62 throw runtime_error("Key name empty.");
63#else
64 gLog << ___err___ << "ERROR - Key name empty." << endl;
65 return false;
66#endif
67 }
68 if (key.size()>8)
69 {
70 ostringstream sout;
71 sout << "Key '" << key << "' exceeds 8 bytes.";
72#ifdef __EXCEPTIONS
73 throw runtime_error(sout.str());
74#else
75 gLog << ___err___ << "ERROR - " << sout.str() << endl;
76 return false;
77#endif
78 }
79
80 //transform(key.begin(), key.end(), key.begin(), toupper);
81
82 for (string::const_iterator c=key.begin(); c<key.end(); c++)
83 if ((*c<'A' || *c>'Z') && (*c<'0' || *c>'9') && *c!='-' && *c!='_')
84 {
85 ostringstream sout;
86 sout << "Invalid character '" << *c << "' found in key '" << key << "'";
87#ifdef __EXCEPTIONS
88 throw runtime_error(sout.str());
89#else
90 gLog << ___err___ << "ERROR - " << sout.str() << endl;
91 return false;
92#endif
93 }
94
95 return true;
96 }
97
98 bool FormatComment()
99 {
100 comment = Trim(comment);
101
102 for (string::const_iterator c=key.begin(); c<key.end(); c++)
103 if (*c<32 || *c>126)
104 {
105 ostringstream sout;
106 sout << "Invalid character '" << *c << "' [" << int(*c) << "] found in comment '" << comment << "'";
107#ifdef __EXCEPTIONS
108 throw runtime_error(sout.str());
109#else
110 gLog << ___err___ << "ERROR - " << sout.str() << endl;
111 return false;
112#endif
113 }
114
115 return true;
116 }
117
118 bool check()
119 {
120 if (!FormatKey())
121 return false;
122
123 if (!FormatComment())
124 return false;
125
126 const size_t sz = CalcSize();
127 if (sz>80)
128 {
129 ostringstream sout;
130 sout << "Size " << sz << " of entry for key '" << key << "' exceeds 80 characters.";
131#ifdef __EXCEPTIONS
132 throw runtime_error(sout.str());
133#else
134 gLog << ___err___ << "ERROR - " << sout.str() << endl;
135 return false;
136#endif
137 }
138
139 return true;
140 }
141
142 size_t CalcSize() const
143 {
144 if (!delim)
145 return 10+comment.size();
146
147 return 10 + (value.size()<20?20:value.size()) + 3 + comment.size();
148 }
149
150 string Compile()
151 {
152 ostringstream sout;
153 sout << std::left << setw(8) << key;
154
155 if (!delim)
156 {
157 sout << " " << comment;
158 return sout.str();
159 }
160
161 sout << "= ";
162 sout << (value[0]=='\''?std::left:std::right);
163 sout << setw(20) << value << std::left;
164
165 if (comment.size()>0)
166 sout << " / " << comment;
167
168 return sout.str();
169 }
170
171 Checksum checksum;
172
173 void Out(ofstream &fout)
174 {
175 if (!changed)
176 return;
177
178 string str = Compile();
179 str.insert(str.end(), 80-str.size(), ' ');
180
181 if (offset==0)
182 offset = fout.tellp();
183
184 //cout << "Write[" << offset << "]: " << key << "/" << value << endl;
185
186 fout.seekp(offset);
187 fout << str;
188
189 checksum.reset();
190 checksum.add(str.c_str(), 80);
191
192 changed = false;
193 }
194 /*
195 void Out(ostream &out)
196 {
197 string str = Compile();
198
199 str.insert(str.end(), 80-str.size(), ' ');
200
201 out << str;
202 changed = false;
203 }*/
204 };
205
206 vector<Key> fKeys;
207
208 vector<Key>::iterator findkey(const string &key)
209 {
210 for (auto it=fKeys.begin(); it!=fKeys.end(); it++)
211 if (key==it->key)
212 return it;
213
214 return fKeys.end();
215 }
216
217 bool Set(const string &key="", bool delim=false, const string &value="", const string &comment="")
218 {
219 // If no delimit add the row no matter if it alread exists
220 if (delim)
221 {
222 // if the row already exists: update it
223 auto it = findkey(key);
224 if (it!=fKeys.end())
225 {
226 it->value = value;
227 it->changed = true;
228 return true;
229 }
230 }
231
232 if (fTable.num_rows>0)
233 {
234 ostringstream sout;
235 sout << "No new header key can be defined, rows were already written to the file... ignoring new key '" << key << "'";
236#ifdef __EXCEPTIONS
237 throw runtime_error(sout.str());
238#else
239 gLog << ___err___ << "ERROR - " << sout.str() << endl;
240 return false;
241#endif
242 }
243
244 Key entry;
245
246 entry.key = key;
247 entry.delim = delim;
248 entry.value = value;
249 entry.comment = comment;
250 entry.offset = 0;
251 entry.changed = true;
252
253 if (!entry.check())
254 return false;
255
256 fKeys.push_back(entry);
257 return true;
258 }
259
260 struct Table
261 {
262 off_t offset;
263
264 size_t bytes_per_row;
265 size_t num_rows;
266 size_t num_cols;
267
268 struct Column
269 {
270 string name;
271 size_t offset;
272 size_t num;
273 size_t size;
274 char type;
275 };
276
277 vector<Column> cols;
278
279 Table() : offset(0), bytes_per_row(0), num_rows(0), num_cols(0)
280 {
281 }
282 };
283
284
285 Table fTable;
286
287 vector<char> fOutputBuffer;
288
289 vector<Table::Column>::iterator findcol(const string &name)
290 {
291 for (auto it=fTable.cols.begin(); it!=fTable.cols.end(); it++)
292 if (name==it->name)
293 return it;
294
295 return fTable.cols.end();
296 }
297
298 Checksum fDataSum;
299 Checksum fHeaderSum;
300
301public:
302 ofits()
303 {
304 }
305 ofits(const char *fname) : ofstream()
306 {
307 this->open(fname);
308 }
309 ~ofits() { close(); }
310
311 void open(const char * filename)
312 {
313 fDataSum = 0;
314 fHeaderSum = 0;
315
316 fTable = Table();
317 fKeys.clear();
318
319 SetStr("XTENSION", "BINTABLE", "binary table extension");
320 SetInt("BITPIX", 8, "8-bit bytes");
321 SetInt("NAXIS", 2, "2-dimensional binary table");
322 SetInt("NAXIS1", 0, "width of table in bytes");
323 SetInt("NAXIS2", 0, "number of rows in table");
324 SetInt("PCOUNT", 0, "size of special data area");
325 SetInt("GCOUNT", 1, "one data group (required keyword)");
326 SetInt("TFIELDS", 0, "number of fields in each row");
327 SetStr("EXTNAME", "", "name of extension table");
328 SetStr("CHECKSUM", "{0000000000000000}", "Checksum for the whole file");
329 SetInt("DATASUM", 0, "Checksum for the data block");
330
331 ofstream::open(filename);
332 }
333
334
335 bool SetBool(const string &key, bool b, const string &comment="")
336 {
337 return Set(key, true, b?"T":"F", comment);
338 }
339
340 bool AddEmpty(const string &key, const string &comment="")
341 {
342 return Set(key, true, "", comment);
343 }
344
345 bool SetStr(const string &key, string s, const string &comment="")
346 {
347 for (string::iterator c=s.begin(); c<s.end(); c++)
348 if (*c=='\'')
349 s.insert(c++, '\'');
350
351 return Set(key, true, "'"+s+"'", comment);
352 }
353
354 bool SetInt(const string &key, int64_t i, const string &comment="")
355 {
356 ostringstream sout;
357 sout << i;
358
359 return Set(key, true, sout.str(), comment);
360 }
361
362 bool SetFloat(const string &key, double f, int p, const string &comment="")
363 {
364 ostringstream sout;
365
366 if (p<0)
367 sout << setprecision(-p) << fixed;
368 if (p>0)
369 sout << setprecision(p);
370 if (p==0)
371 sout << setprecision(f>1e-100 && f<1e100 ? 15 : 14);
372
373 sout << f;
374
375 string str = sout.str();
376
377 replace(str.begin(), str.end(), 'e', 'E');
378
379 if (str.find_first_of('E')==string::npos && str.find_first_of('.')==string::npos)
380 str += ".";
381
382 return Set(key, true, str, comment);
383 }
384
385 bool SetFloat(const string &key, double f, const string &comment="")
386 {
387 return SetFloat(key, f, 0, comment);
388 }
389
390 bool SetHex(const string &key, uint64_t i, const string &comment="")
391 {
392 ostringstream sout;
393 sout << std::hex << "0x" << i;
394 return SetStr(key, sout.str(), comment);
395 }
396
397 bool AddComment(const string &comment)
398 {
399 return Set("COMMENT", false, "", comment);
400 }
401
402 bool AddHistory(const string &comment)
403 {
404 return Set("HISTORY", false, "", comment);
405 }
406
407 void End()
408 {
409 Set("END");
410 while (fKeys.size()%36!=0)
411 fKeys.push_back(Key());
412 }
413
414 bool AddColumn(uint32_t cnt, char typechar, const string &name, const string &unit, const string &comment="")
415 {
416 if (tellp()<0)
417 {
418 ostringstream sout;
419 sout << "File not open... ignoring column '" << name << "'";
420#ifdef __EXCEPTIONS
421 throw runtime_error(sout.str());
422#else
423 gLog << ___err___ << "ERROR - " << sout.str() << endl;
424 return false;
425#endif
426 }
427
428 if (tellp()>0)
429 {
430 ostringstream sout;
431 sout << "Header already writtenm, no new column can be defined... ignoring column '" << name << "'";
432#ifdef __EXCEPTIONS
433 throw runtime_error(sout.str());
434#else
435 gLog << ___err___ << "ERROR - " << sout.str() << endl;
436 return false;
437#endif
438 }
439
440 if (findcol(name)!=fTable.cols.end())
441 {
442 ostringstream sout;
443 sout << "A column with the name '" << name << "' already exists.";
444#ifdef __EXCEPTIONS
445 throw runtime_error(sout.str());
446#else
447 gLog << ___err___ << "ERROR - " << sout.str() << endl;
448 return false;
449#endif
450 }
451
452 typechar = toupper(typechar);
453
454 static const string allow("LABIJKED");
455 if (std::find(allow.begin(), allow.end(), typechar)==allow.end())
456 {
457 ostringstream sout;
458 sout << "Column type '" << typechar << "' not supported.";
459#ifdef __EXCEPTIONS
460 throw runtime_error(sout.str());
461#else
462 gLog << ___err___ << "ERROR - " << sout.str() << endl;
463 return false;
464#endif
465 }
466
467 ostringstream type;
468 type << cnt << typechar;
469
470 fTable.num_cols++;
471
472 ostringstream typekey, formkey, unitkey, unitcom, typecom;
473 typekey << "TTYPE" << fTable.num_cols;
474 formkey << "TFORM" << fTable.num_cols;
475 unitkey << "TUNIT" << fTable.num_cols;
476 unitcom << "unit of " << name;
477
478 typecom << "format of " << name << " [";
479
480 switch (typechar)
481 {
482 case 'L': typecom << "1-byte BOOL]"; break;
483 case 'A': typecom << "1-byte CHAR]"; break;
484 case 'B': typecom << "1-byte BOOL]"; break;
485 case 'I': typecom << "2-byte INT]"; break;
486 case 'J': typecom << "4-byte INT]"; break;
487 case 'K': typecom << "8-byte INT]"; break;
488 case 'E': typecom << "4-byte FLOAT]"; break;
489 case 'D': typecom << "8-byte FLOAT]"; break;
490 }
491
492 SetStr(formkey.str(), type.str(), typecom.str());
493 SetStr(typekey.str(), name, comment);
494
495 if (!unit.empty())
496 SetStr(unitkey.str(), unit, unitcom.str());
497
498 size_t size = 0;
499
500 switch (typechar)
501 {
502 case 'L': size = 1; break;
503 case 'A': size = 1; break;
504 case 'B': size = 1; break;
505 case 'I': size = 2; break;
506 case 'J': size = 4; break;
507 case 'K': size = 8; break;
508 case 'E': size = 4; break;
509 case 'D': size = 8; break;
510 }
511
512 Table::Column col;
513 col.name = name;
514 col.type = typechar;
515 col.num = cnt;
516 col.size = size;
517 col.offset = fTable.bytes_per_row;
518
519 fTable.cols.push_back(col);
520
521 fTable.bytes_per_row += size*cnt;
522
523 // Align to four bytes
524 fOutputBuffer.resize(fTable.bytes_per_row + 4-fTable.bytes_per_row%4);
525
526 return true;
527 }
528
529 bool AddColumnShort(uint32_t cnt, const string &name, const string &unit="", const string &comment="")
530 { return AddColumn(cnt, 'I', name, unit, comment); }
531 bool AddColumnInt(uint32_t cnt, const string &name, const string &unit="", const string &comment="")
532 { return AddColumn(cnt, 'J', name, unit, comment); }
533 bool AddColumnLong(uint32_t cnt, const string &name, const string &unit="", const string &comment="")
534 { return AddColumn(cnt, 'K', name, unit, comment); }
535 bool AddColumnFloat(uint32_t cnt, const string &name, const string &unit="", const string &comment="")
536 { return AddColumn(cnt, 'E', name, unit, comment); }
537 bool AddColumnDouble(uint32_t cnt, const string &name, const string &unit="", const string &comment="")
538 { return AddColumn(cnt, 'D', name, unit, comment); }
539 bool AddColumnChar(uint32_t cnt, const string &name, const string &unit="", const string &comment="")
540 { return AddColumn(cnt, 'A', name, unit, comment); }
541 bool AddColumnByte(uint32_t cnt, const string &name, const string &unit="", const string &comment="")
542 { return AddColumn(cnt, 'B', name, unit, comment); }
543 bool AddColumnBool(uint32_t cnt, const string &name, const string &unit="", const string &comment="")
544 { return AddColumn(cnt, 'L', name, unit, comment); }
545
546 bool AddColumnShort(const string &name, const string &unit="", const string &comment="")
547 { return AddColumn(1, 'I', name, unit, comment); }
548 bool AddColumnInt(const string &name, const string &unit="", const string &comment="")
549 { return AddColumn(1, 'J', name, unit, comment); }
550 bool AddColumnLong(const string &name, const string &unit="", const string &comment="")
551 { return AddColumn(1, 'K', name, unit, comment); }
552 bool AddColumnFloat(const string &name, const string &unit="", const string &comment="")
553 { return AddColumn(1, 'E', name, unit, comment); }
554 bool AddColumnDouble(const string &name, const string &unit="", const string &comment="")
555 { return AddColumn(1, 'D', name, unit, comment); }
556 bool AddColumnChar(const string &name, const string &unit="", const string &comment="")
557 { return AddColumn(1, 'A', name, unit, comment); }
558 bool AddColumnByte(const string &name, const string &unit="", const string &comment="")
559 { return AddColumn(1, 'B', name, unit, comment); }
560 bool AddColumnBool(const string &name, const string &unit="", const string &comment="")
561 { return AddColumn(1, 'L', name, unit, comment); }
562
563 /*
564 bool AddKey(string key, double d, const string &comment)
565 {
566 ostringstream out;
567 out << d;
568
569 string s = out.str();
570
571 replace(s.begin(), s.end(), "e", "E");
572
573 return AddKey(key, s, comment);
574 }*/
575
576
577 Checksum WriteHeader(ofstream &fout)
578 {
579 Checksum sum;
580 for (auto it=fKeys.begin(); it!=fKeys.end(); it++)
581 {
582 it->Out(fout);
583 sum += it->checksum;
584 }
585 fout.flush();
586
587 return sum;
588 }
589
590 Checksum WriteHeader()
591 {
592 return WriteHeader(*this);
593 }
594
595 void FlushHeader()
596 {
597 const off_t pos = tellp();
598 WriteHeader();
599 seekp(pos);
600 }
601
602 Checksum WriteFitsHeader()
603 {
604 ofits h;
605
606 h.SetBool("SIMPLE", true, "file does conform to FITS standard");
607 h.SetInt ("BITPIX", 8, "number of bits per data pixel");
608 h.SetInt ("NAXIS", 0, "number of data axes");
609 h.SetBool("EXTEND", true, "FITS dataset may contain extensions");
610
611 h.AddComment("FITS (Flexible Image Transport System) format is defined in 'Astronomy");
612 h.AddComment("and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H");
613 h.End();
614
615 return h.WriteHeader(*this);
616 }
617
618 bool WriteTableHeader(const char *name="DATA")
619 {
620 if (tellp()>0)
621 {
622#ifdef __EXCEPTIONS
623 throw runtime_error("File not empty anymore.");
624#else
625 gLog << ___err___ << "ERROR - File not empty anymore." << endl;
626 return false;
627#endif
628 }
629
630 fHeaderSum = WriteFitsHeader();
631
632 SetStr("EXTNAME", name);
633 SetInt("NAXIS1", fTable.bytes_per_row);
634 SetInt("TFIELDS", fTable.cols.size());
635
636 End();
637
638 WriteHeader();
639
640 return good();
641 }
642
643 template<size_t N>
644 void revcpy(char *dest, const char *src, int num)
645 {
646 const char *pend = src + num*N;
647 for (const char *ptr = src; ptr<pend; ptr+=N, dest+=N)
648 reverse_copy(ptr, ptr+N, dest);
649 }
650
651 uint32_t GetBytesPerRow() const { return fTable.bytes_per_row; }
652
653 bool WriteRow(const void *ptr, size_t cnt, bool byte_swap=true)
654 {
655 // FIXME: Make sure that header was already written
656 // or write header now!
657 if (cnt!=fTable.bytes_per_row)
658 {
659 ostringstream sout;
660 sout << "WriteRow - Size " << cnt << " does not match expected size " << fTable.bytes_per_row;
661#ifdef __EXCEPTIONS
662 throw runtime_error(sout.str());
663#else
664 gLog << ___err___ << "ERROR - " << sout.str() << endl;
665 return false;
666#endif
667 }
668
669 // For the checksum we need everything to be correctly aligned
670 const uint8_t offset = fTable.offset%4;
671
672 char *buffer = fOutputBuffer.data() + offset;
673
674 auto ib = fOutputBuffer.begin();
675 auto ie = fOutputBuffer.end();
676 *ib++ = 0;
677 *ib++ = 0;
678 *ib++ = 0;
679 *ib = 0;
680
681 *--ie = 0;
682 *--ie = 0;
683 *--ie = 0;
684 *--ie = 0;
685
686 if (!byte_swap)
687 memcpy(buffer, ptr, cnt);
688 else
689 {
690 for (auto it=fTable.cols.begin(); it!=fTable.cols.end(); it++)
691 {
692 const char *src = reinterpret_cast<const char*>(ptr) + it->offset;
693 char *dest = buffer + it->offset;
694
695 // Let the compiler do some optimization by
696 // knowing the we only have 1, 2, 4 and 8
697 switch (it->size)
698 {
699 case 1: memcpy (dest, src, it->num*it->size); break;
700 case 2: revcpy<2>(dest, src, it->num); break;
701 case 4: revcpy<4>(dest, src, it->num); break;
702 case 8: revcpy<8>(dest, src, it->num); break;
703 }
704 }
705 }
706
707 write(buffer, cnt);
708 fDataSum.add(fOutputBuffer);
709
710 fTable.num_rows++;
711 fTable.offset += cnt;
712 return good();
713 }
714
715 template<typename N>
716 bool WriteRow(const vector<N> &vec)
717 {
718 return WriteRow(vec.data(), vec.size()*sizeof(N));
719 }
720
721 // Flushes the number of rows to the header on disk
722 void FlushNumRows()
723 {
724 SetInt("NAXIS2", fTable.num_rows);
725 FlushHeader();
726 }
727
728 bool close()
729 {
730 if (tellp()<0)
731 return false;
732
733 if (tellp()%(80*36)>0)
734 {
735 //cout << "fill" << endl;
736 const vector<char> filler(80*36-tellp()%(80*36));
737 write(filler.data(), filler.size());
738 }
739
740 // We don't hav eto jump back to the end of the file
741 SetInt("NAXIS2", fTable.num_rows);
742 SetInt("DATASUM", fDataSum.val());
743
744 const Checksum sum = WriteHeader();
745 //sum += headersum;
746
747 SetStr("CHECKSUM", "{"+(sum+fDataSum).str()+"}");
748
749 const Checksum chk = WriteHeader();
750
751 ofstream::close();
752
753 if ((chk+fDataSum).valid())
754 return true;
755
756 ostringstream sout;
757 sout << "Checksum (" << std::hex << chk.val() << ") invalid.";
758#ifdef __EXCEPTIONS
759 throw runtime_error(sout.str());
760#else
761 gLog << ___err___ << "ERROR - " << sout.str() << endl;
762 return false;
763#endif
764 }
765};
766
767}
768
769#if 0
770#include "fits.h"
771
772int main()
773{
774 using namespace std;
775
776 ofits h2("delme.fits");
777
778 h2.SetInt("KEY1", 1, "comment 1");
779 h2.AddColumnInt(2, "testcol1", "counts", "My comment");
780 h2.AddColumnInt("testcol2", "counts", "My comment");
781 //h2.AddColumnInt("testcol2", "counts", "My comment");
782 h2.SetInt("KEY2", 2, "comment 2");
783
784 /*
785 AddFloat("X0", 0.000123456, "number of fields in each row");
786 AddFloat("X1", 0, "number of fields in each row");
787 AddFloat("X2", 12345, "number of fields in each row");
788 AddFloat("X3", 123456.67890, "number of fields in each row");
789 AddFloat("X4", 1234567890123456789.12345678901234567890, "number of fields in each row");
790 AddFloat("X5", 1234567890.1234567890e20, "number of fields in each row");
791 AddFloat("X6", 1234567890.1234567890e-20, "number of fields in each row");
792 AddFloat("XB", 1234567890.1234567890e-111, "number of fields in each row");
793 AddFloat("X7", 1e-5, "number of fields in each row");
794 AddFloat("X8", 1e-6, "number of fields in each row");
795 //AddStr("12345678", "123456789012345678", "12345678901234567890123456789012345678901234567");
796 */
797 // -
798
799 h2.WriteTableHeader("TABLE_NAME");
800
801 for (int i=0; i<10; i++)
802 {
803 int j[3] = { i+10, i*10, i*100 };
804 h2.WriteRow(j, 3*sizeof(i));
805 }
806
807 //h2.AddColumnInt("testcol2", "counts", "My comment");
808 //h2.SetInt("KEY3", 2, "comment 2");
809 h2.SetInt("KEY2", 2, "comment 2xxx");
810 h2.SetInt("KEY1", 11);
811
812 h2.close();
813
814 cout << "---" << endl;
815
816 fits f("delme.fits");
817 if (!f)
818 throw runtime_error("xxx");
819
820 cout << "Header is valid: " << f.IsHeaderOk() << endl;
821
822 while (f.GetNextRow());
823
824 cout << "File is valid: " << f.IsFileOk() << endl;
825
826 cout << "---" << endl;
827
828 return 0;
829}
830#endif
831
832#endif
Note: See TracBrowser for help on using the repository browser.