Changeset 11192 in ntrip
- Timestamp:
- Nov 10, 2026, 12:13:23 PM (less than one hour ago)
- Location:
- trunk/BNC
- Files:
-
- 24 added
- 14 edited
-
CHANGELOG.md (modified) (1 diff)
-
src/bnchelp.html (modified) (1 diff)
-
src/ephemeris.cpp (modified) (4 diffs)
-
src/ephemeris.h (modified) (2 diffs)
-
src/rinex/reqcedit.cpp (modified) (3 diffs)
-
src/rinex/reqcedit.h (modified) (1 diff)
-
src/rinex/rnxnavfile.cpp (modified) (4 diffs)
-
src/rinex/rnxobsfile.cpp (modified) (27 diffs)
-
src/rinex/rnxobsfile.h (modified) (9 diffs)
-
tests/README.md (modified) (2 diffs)
-
tests/cases/log_structured/expected/PPPQuickStart.jsonl.gz (modified) ( previous)
-
tests/cases/log_structured/expected/PPPQuickStart.logfmt.gz (modified) ( previous)
-
tests/cases/replay_rinex_obs/expected/WTZR00_S_20253211345_15M_01S_MO.rnx.gz (modified) ( previous)
-
tests/cases/rinex_edit_minele/expected/BRUX_MINELE10.rnx.gz (modified) ( previous)
-
tests/cases/rinex_edit_v402 (added)
-
tests/cases/rinex_edit_v402/case.json (added)
-
tests/cases/rinex_edit_v402/expected (added)
-
tests/cases/rinex_edit_v402/expected/RNX402_MN.rnx.gz (added)
-
tests/cases/rinex_edit_v402/expected/RNX402_MO.rnx.gz (added)
-
tests/cases/rinex_edit_v402/expected/RinexEdit.log.gz (added)
-
tests/cases/rinex_edit_v402_events (added)
-
tests/cases/rinex_edit_v402_events/case.json (added)
-
tests/cases/rinex_edit_v402_events/expected (added)
-
tests/cases/rinex_edit_v402_events/expected/RNX402_EVENTS_MO.rnx.gz (added)
-
tests/cases/rinex_edit_v402_events/expected/RinexEdit.log.gz (added)
-
tests/cases/rinex_edit_v402_events_to_v2 (added)
-
tests/cases/rinex_edit_v402_events_to_v2/case.json (added)
-
tests/cases/rinex_edit_v402_events_to_v2/expected (added)
-
tests/cases/rinex_edit_v402_events_to_v2/expected/RNX402_EVENTS.25O.gz (added)
-
tests/cases/rinex_edit_v402_events_to_v2/expected/RinexEdit.log.gz (added)
-
tests/cases/rinex_edit_v402_to_v2 (added)
-
tests/cases/rinex_edit_v402_to_v2/case.json (added)
-
tests/cases/rinex_edit_v402_to_v2/expected (added)
-
tests/cases/rinex_edit_v402_to_v2/expected/RNX402_V2.25O.gz (added)
-
tests/cases/rinex_edit_v402_to_v2/expected/RinexEdit.log.gz (added)
-
tests/data/rnx402_MN.rnx (added)
-
tests/data/rnx402_MO.rnx (added)
-
tests/data/rnx402_events_MO.rnx (added)
Legend:
- Unmodified
- Added
- Removed
-
trunk/BNC/CHANGELOG.md
r11190 r11192 1 1 # Changelog 2 2 ## 2.13.8 (2026-11-04) 3 - ADDED: **Structured logfile** (General options 'Structured logfile', 'Format' and 'Min. level', keys logFileStructured, logFormatStructured and logLevelStructured): in addition to the unchanged logfile, the same records as JSON Lines ('json', default) or key=value pairs ('logfmt'), one appended file without date for rotation by logrotate & co (reopened if moved or removed); each record has the time in UTC ('time'), for records of a data epoch its GPS Time ('epoch'), level, component, event, mountpoint ('mp'), the event's fields, the logfile text ('msg', without the leading mountpoint and trailing blanks, shortened where the fields carry the details, e.g. 'PPP position') and a version ('v'). Events with fields: ppp_position, ppp_epoch_failed, stream_format, stream_content (RTCM scan, with message_type and size of the received message types), stream_timeout, stream_outage_begin/end, stream_corrupted_begin/end, stream_latency; all other messages as event 'message' with the component of their origin (stream, ntrip, bce, ssr, ppp, cmb, upload, ssrqc, bnc) and a level (error: a function does not work; warning: data or configuration problem, BNC continues; info: status); messages of the BNC_DEBUG_BCE/OBS/CMB defines with level 'debug' and component bce/obs/cmb. Test case log_structured and check tests/tools/logcheck.py 3 - ADDED: **RINEX observation files: event records are kept** (RINEX Edit/Concatenate): epoch flag 1 (power failure) is written again, flags 2-5 with their special records (header records valid in the output version, without changes of the observation types fixed by the output header), flag 6 cycle slip records; events of epochs left out by the sampling are written with the next epoch, events after the last epoch at the end. Before, flag 5 records were skipped, flag 2 was written as an empty epoch and flag 6 stopped the editing ('unhandled flag') 4 - ADDED: **RINEX observation header records kept**: records not interpreted by BNC (e.g. SIGNAL STRENGTH UNIT, LEAP SECONDS, SYS / DCBS APPLIED, SYS / PCVS APPLIED, ANTENNA: PHASECENTER/ZERODIR/DELTA X/Y/Z/B.SIGHT XYZ, CENTER OF MASS: XYZ, DOI, LICENSE OF USE, STATION INFORMATION) are written again if they exist in the output version; SYS / SCALE FACTOR is applied when reading and writing (RINEX 2 output: unscaled); the satellite system of RINEX VERSION / TYPE is kept for single-system files (before always M) 5 - ADDED: RINEX Edit/Concatenate: TIME OF FIRST OBS, TIME OF LAST OBS, # OF SATELLITES and PRN / # OF OBS are given for the written epochs (the header is written when the file is closed); TIME OF LAST OBS and the satellite counts only if the input header has them 6 - ADDED: **RINEX 4.02 observation epochs: receiver clock offset and pico-sec digits** of the epoch record are read and written again (RINEX Edit/Concatenate), before both were dropped; the header record RCV CLOCK OFFS APPL and the system time of TIME OF FIRST OBS are kept (before always 'GPS'). The clock offset is also read and written in RINEX 2 (F12.9, columns 69-80) and 3; the pico-sec digits can only be written in RINEX 4.02 (otherwise the epoch is rounded to 0.1 microsec). An epoch rounded to 60.0000000 s is written as the next minute 7 - CHANGED: RINEX 4 observation files: no SYS / PHASE SHIFT records written any more (strongly deprecated in RINEX 4, to be ignored by encoders); before, records with 0.00000 were generated even when the input file had none 8 - FIXED: RINEX 4.02 navigation files: GLONASS L1OC/L3OC ephemerides were read with 5 instead of 9 lines (rest of the record lost, the L3OC record dropped), the clock drift rate Beta was read and written as message frame time, and AODE/AODC were written in the wrong order 9 - FIXED: RINEX 4 navigation files: crash at an ION J CNVX record without navigation message subtype (RINEX 4.00/4.01); lines not belonging to a record are skipped instead of being read with an undefined satellite system 10 - FIXED: RINEX Edit/Concatenate: crash (division by zero) with sampling '0.1 sec' 11 - ADDED: **Structured logfile** (General options 'Structured logfile', 'Format' and 'Min. level', keys logFileStructured, logFormatStructured and logLevelStructured): in addition to the unchanged logfile, the same records as JSON Lines ('json', default) or key=value pairs ('logfmt'), one appended file without date for rotation by logrotate & co (reopened if moved or removed); each record has the time in UTC ('time'), for records of a data epoch its GPS Time ('epoch'), level, component, event, mountpoint ('mp'), the event's fields, the logfile text ('msg', without the leading mountpoint and trailing blanks, shortened where the fields carry the details, e.g. 'PPP position') and a version ('v'). Events with fields: ppp_position, ppp_epoch_failed, stream_format, stream_content (RTCM scan, each record with message_type and size of the RTCM message carrying the information), stream_timeout, stream_outage_begin/end, stream_corrupted_begin/end, stream_latency; all other messages as event 'message' with the component of their origin (stream, ntrip, bce, ssr, ppp, cmb, upload, ssrqc, bnc) and a level (error: a function does not work; warning: data or configuration problem, BNC continues; info: status); messages of the BNC_DEBUG_BCE/OBS/CMB defines with level 'debug' and component bce/obs/cmb. Test case log_structured and check tests/tools/logcheck.py 4 12 - FIXED: Build with BNC_DEBUG_BCE: undeclared message number in the debug messages of the Galileo ephemeris decoder 5 13 - ADDED: **epoch/velocity fields to PPP coordinates file to allow BNC to propagate the reference coordinates** related to a dedicated reference epoch to the current processing epoch internally; coordinates can also be given as latitude/longitude/height -
trunk/BNC/src/bnchelp.html
r11190 r11192 2832 2832 <li>'stream_format': 'decoder' (RTCM2, RTCM3, ZERO, ZERO2FILE, RTNET) and 'format' (format entry of the stream),</li> 2833 2833 <li>'stream_timeout' (warning): data timeout, BNC reconnects,</li> 2834 <li>'stream_content': content of the stream found by the RTCM scan (see section <a href=#miscscan>2.12.3</a>), e.g. observation 2835 types, antenna and receiver descriptors, antenna coordinates, GLONASS slots, CRS and Helmert parameters; for the 2836 received message types with the fields 'message_type' and, if known, 'size' (bytes),</li> 2834 <li>'stream_content': content of the stream found by the RTCM scan (see section <a href=#miscscan>2.12.3</a>): the 2835 received message types, observation types, antenna and receiver descriptors, antenna coordinates, GLONASS 2836 slots and code-phase biases, CRS and Helmert parameters; each record with the fields 'message_type' and 'size' 2837 (bytes) of the RTCM message carrying the information (observation types and GLONASS slots: the message with 2838 the new entry); for RTCM 2 streams without 'size' and with 'message_type' only for the message types and the 2839 antenna information,</li> 2837 2840 <li>'stream_outage_begin' (warning), 'stream_corrupted_begin' (warning): 'since' (UTC); 2838 2841 'stream_outage_end', 'stream_corrupted_end': 'ended' and 'since' (UTC),</li> -
trunk/BNC/src/ephemeris.cpp
r11158 r11192 1199 1199 } 1200 1200 1201 if (rnxVersion >= 3.05) { 1201 if (type() == t_eph::L1OC || type() == t_eph::L3OC) { 1202 nLines = 9; // RINEX 4.02 Tables A16/A17 1203 } 1204 else if (rnxVersion >= 3.05) { 1202 1205 nLines += 1; 1203 1206 } else { … … 1266 1269 _TOC = _TOC + _gps_utc; 1267 1270 int nd = int((_TOC.gpssec())) / (24.0 * 60.0 * 60.0); 1271 // FDMA: -TauN, +GammaN, message frame time 1272 // L1OC/L3OC: -TauN, +GammaN, clock drift rate Beta (RINEX 4.02 A16/A17) 1273 double& third = (type() == t_eph::L1OC || type() == t_eph::L3OC) ? _beta : _tki; 1268 1274 if ( readDbl(line, pos[1], fieldLen, _tau) 1269 1275 || readDbl(line, pos[2], fieldLen, _gamma) 1270 || readDbl(line, pos[3], fieldLen, _tki)) {1276 || readDbl(line, pos[3], fieldLen, third)) { 1271 1277 _checkState = bad; 1272 1278 return; 1273 1279 } 1274 _tki -= nd * 86400.0; 1280 if (type() != t_eph::L1OC && type() != t_eph::L3OC) { 1281 _tki -= nd * 86400.0; 1282 } 1275 1283 _tau = -_tau; 1276 1284 } … … 1588 1596 QTextStream out(&rnxStr); 1589 1597 1598 bool cdma = (type() == t_eph::L1OC || type() == t_eph::L3OC); 1590 1599 out 1591 1600 << QString("%1%2%3\n") 1592 1601 .arg(-_tau, 19, 'e', 12) 1593 1602 .arg(_gamma, 19, 'e', 12) 1594 .arg(_tki + nd * 86400.0, 19, 'e', 12); 1603 .arg(cdma ? _beta : _tki + nd * 86400.0, 19, 'e', 12); 1595 1604 1596 1605 QString fmt = version < 3.0 ? " %1%2%3%4\n" : " %1%2%3%4\n"; … … 1736 1745 .arg(_sat_type , 19, 'e', 12) 1737 1746 .arg(double(sourceflags), 19, 'e', 12) 1738 .arg(_E T, 19, 'e', 12)1739 .arg(_E E, 19, 'e', 12);1747 .arg(_EE, 19, 'e', 12) // AODE 1748 .arg(_ET, 19, 'e', 12); // AODC 1740 1749 } 1741 1750 // ===================== -
trunk/BNC/src/ephemeris.h
r11137 r11192 244 244 _Tin = 0.0; 245 245 _gamma = 0.0; 246 _beta = 0.0; 246 247 _tki = 0.0; 247 248 _x_pos = 0.0; … … 321 322 322 323 double _gamma; // [-] 324 double _beta; // clock drift rate [s/s^2] (L1OC, L3OC) 323 325 mutable double _tki; // message frame time 324 326 -
trunk/BNC/src/rinex/reqcedit.cpp
r11158 r11192 357 357 addRnxConversionDetails(obsFile, txtMap); 358 358 } 359 outObsFile.header().write(outObsFile.stream(), &txtMap); 359 // header with TIME OF FIRST/LAST OBS etc. of the written epochs 360 outObsFile.writeHeaderOnClose(txtMap); 360 361 } 361 362 t_rnxObsFile::t_rnxEpo* epo = 0; 362 363 try { 364 // Events (epoch flags 2-6) of epochs not written (time window, 365 // sampling) are written with the next epoch; events with an epoch 366 // outside the time window are skipped 367 QList<t_rnxObsFile::t_rnxEvent> pendingEvents; 368 bool pendingPowerFail = false; 369 bool endReached = false; 363 370 while ( (epo = obsFile->nextEpoch()) != 0) { 371 collectEvents(epo->events, pendingEvents); 372 if (epo->flag == 1) { 373 pendingPowerFail = true; 374 } 364 375 if (_begTime.valid() && epo->tt < _begTime) { 365 376 continue; 366 377 } 367 378 if (_endTime.valid() && epo->tt > _endTime) { 379 endReached = true; 368 380 break; 369 381 } 370 382 371 int sec = int(nint(epo->tt.gpssec()*10)); 372 if (sec % (int(_samplingRate)*10) == 0) { 383 // sampling in units of 0.1 sec (int(0.1)*10 would be zero) 384 int sec = int(nint(epo->tt.gpssec()*10)); 385 int sampl10 = int(nint(_samplingRate*10)); 386 if (sampl10 <= 0 || sec % sampl10 == 0) { 373 387 applyLLI(obsFile, epo); 374 388 if (_minEle > 0.0) { 375 389 applyElevationMask(obsFile, epo); 376 390 } 391 epo->events = pendingEvents; 392 pendingEvents.clear(); 393 if (pendingPowerFail) { 394 epo->flag = 1; 395 pendingPowerFail = false; 396 } 377 397 outObsFile.writeEpoch(epo); 378 398 } … … 380 400 rememberLLI(obsFile, epo); 381 401 } 402 } 403 // events after the last epoch of the file 404 if (!endReached) { 405 collectEvents(obsFile->pendingEvents(), pendingEvents); 406 outObsFile.writeEvents(pendingEvents); 382 407 } 383 408 } … … 400 425 return; 401 426 } 427 } 428 } 429 430 // Events to be written: events after the time window are skipped, events 431 // before it only if they refer to their epoch (flag 5: external event, 432 // flag 6: cycle slips); flags 2-4 change the state of the following data 433 //////////////////////////////////////////////////////////////////////////// 434 void t_reqcEdit::collectEvents(const QList<t_rnxObsFile::t_rnxEvent>& events, 435 QList<t_rnxObsFile::t_rnxEvent>& pendingEvents) const { 436 for (int ii = 0; ii < events.size(); ii++) { 437 const t_rnxObsFile::t_rnxEvent& event = events[ii]; 438 if (event.epochValid && _endTime.valid() && event.tt > _endTime) { 439 continue; 440 } 441 if (event.epochValid && _begTime.valid() && event.tt < _begTime && 442 (event.flag == 5 || event.flag == 6)) { 443 continue; 444 } 445 pendingEvents.append(event); 402 446 } 403 447 } -
trunk/BNC/src/rinex/reqcedit.h
r11057 r11192 66 66 void editEphemerides(); 67 67 void editRnxObsHeader(t_rnxObsFile& obsFile); 68 void collectEvents(const QList<t_rnxObsFile::t_rnxEvent>& events, 69 QList<t_rnxObsFile::t_rnxEvent>& pendingEvents) const; 68 70 void rememberLLI(const t_rnxObsFile* obsFile, const t_rnxObsFile::t_rnxEpo* epo); 69 71 void applyLLI(const t_rnxObsFile* obsFile, t_rnxObsFile::t_rnxEpo* epo); -
trunk/BNC/src/rinex/rnxnavfile.cpp
r11158 r11192 152 152 QString navType; 153 153 QString prn; 154 bool ephPending = false; // RINEX 4: "> EPH" line read, data lines follow 154 155 155 156 while (stream->status() == QTextStream::Ok && !stream->atEnd()) { 156 char sys; 157 char sys = 0; 157 158 QString line = stream->readLine(); 158 159 if (line.isEmpty()) { … … 176 177 navType = ""; 177 178 } 178 // RINEX version 4 179 // RINEX version 4: data lines of an EPH record follow its "> EPH" line; 180 // anything else (e.g. the remainder of a record that could not be read) 181 // is skipped 182 else if (version() >= 4.0 && firstStr != ">") { 183 if (!ephPending || line.left(1) != prn.left(1)) { 184 ephPending = false; 185 continue; 186 } 187 ephPending = false; 188 sys = prn[0].toLatin1(); 189 } 179 190 else if (version() >= 4.0 && firstStr == ">") { 191 ephPending = false; 192 if (hlp.size() < 4) { 193 continue; 194 } 180 195 QString key = hlp.at(1); 181 196 prn = hlp.at(2); 182 197 sys = prn[0].toLatin1(); 183 198 navType = hlp.at(3); 199 ephPending = (key == "EPH"); 184 200 185 201 // ALL Non-EPH messages are currently ignored … … 206 222 if ((sys == 'I' && navType == "L1NV") || // I: KLOB, NEQN 207 223 (sys == 'J' && navType == "CNVX")) { // J: WIDE, JAPN 208 QString navSubType = hlp. at(4);224 QString navSubType = hlp.value(4); // RINEX >= 4.02 only 209 225 if (navSubType == "KLOB") { 210 226 lines2skip += 1; … … 244 260 if (version() >= 3.05) { 245 261 nLines += 1; 262 } 263 if (navType == "L1OC" || navType == "L3OC") { 264 nLines = 9; 246 265 } 247 266 for (int ii = 1; ii < nLines; ii++) { -
trunk/BNC/src/rinex/rnxobsfile.cpp
r11057 r11192 59 59 _version = 0.0; 60 60 _interval = 0.0; 61 _timeSystem = "GPS"; 62 _rcvClkOffsAppl = -1; 63 _satSystem = 'M'; 64 _otherRecordsVersion = 0.0; 65 _hasLastObs = false; 66 _hasObsCounts = false; 67 _hasPrnObs = false; 61 68 for (unsigned iPrn = 1; iPrn <= t_prn::MAXPRN_GPS; iPrn++) { 62 69 _wlFactorsL1[iPrn] = 1; … … 93 100 int numLines = 0; 94 101 102 // full header (not the header records of an event) 103 // ------------------------------------------------ 104 bool fullHeader = (maxLines == 0); 105 if (fullHeader) { 106 _otherRecords.clear(); 107 _scaleFactors.clear(); 108 _hasLastObs = false; 109 _hasObsCounts = false; 110 _hasPrnObs = false; 111 } 112 95 113 while ( stream->status() == QTextStream::Ok && !stream->atEnd() ) { 96 114 QString line = stream->readLine(); ++ numLines; … … 109 127 QTextStream in(value.toLatin1(), QIODevice::ReadOnly); 110 128 in >> _version; 129 _otherRecordsVersion = _version; 130 QChar satSystem = line.length() > 40 ? line[40] : QChar(' '); 131 _satSystem = (satSystem == ' ') ? 'G' : satSystem.toLatin1(); // blank: GPS (RINEX 2) 111 132 } 112 133 else if (key == "PGM / RUN BY / DATE") { … … 177 198 QTextStream in(value.toLatin1(), QIODevice::ReadOnly); 178 199 in >> _antXYZ[0] >> _antXYZ[1] >> _antXYZ[2]; 200 if (fullHeader) { 201 _otherRecords << line; 202 } 179 203 } 180 204 else if (key == "ANTENNA: B.SIGHT XYZ") { 181 205 QTextStream in(value.toLatin1(), QIODevice::ReadOnly); 182 206 in >> _antBSG[0] >> _antBSG[1] >> _antBSG[2]; 207 if (fullHeader) { 208 _otherRecords << line; 209 } 183 210 } 184 211 else if (key == "DOI") { 185 212 _digitalObjectId = line.mid(0,60).trimmed(); 213 if (fullHeader) { 214 _otherRecords << line; 215 } 186 216 } 187 217 else if (key == "LICENSE OF USE") { 188 218 _licenseOfUse.append(line.mid(0,60).trimmed()); 219 if (fullHeader) { 220 _otherRecords << line; 221 } 189 222 } 190 223 else if (key == "STATION INFORMATION") { 191 224 _stationInformation.append(line.mid(0,60).trimmed()); 225 if (fullHeader) { 226 _otherRecords << line; 227 } 192 228 } 193 229 else if (key == "# / TYPES OF OBSERV") { … … 245 281 in >> year >> month >> day >> hour >> min >> sec; 246 282 _startTime.set(year, month, day, hour, min, sec); 283 QString timeSystem = line.mid(48, 3).trimmed(); 284 if (!timeSystem.isEmpty()) { 285 _timeSystem = timeSystem; 286 } 287 } 288 else if (key == "RCV CLOCK OFFS APPL") { 289 readInt(line, 0, 6, _rcvClkOffsAppl); 290 } 291 else if (key == "TIME OF LAST OBS") { 292 _hasLastObs = true; 293 } 294 else if (key == "# OF SATELLITES") { 295 _hasObsCounts = true; 296 } 297 else if (key == "PRN / # OF OBS") { 298 _hasPrnObs = true; // recomputed for the written file 299 } 300 else if (key == "SYS / SCALE FACTOR") { 301 // A1,1X,I4,2X,I2,12(1X,A3), continuation lines 10X,12(1X,A3) 302 char sys = line.length() ? line[0].toLatin1() : ' '; 303 int factor = 1; 304 int nTypes = 0; 305 readInt(line, 2, 4, factor); 306 readInt(line, 8, 2, nTypes); 307 if (factor <= 0) { 308 factor = 1; 309 } 310 if (nTypes == 0) { 311 _scaleFactors[sys][""] = factor; 312 } 313 for (int ii = 0; ii < nTypes; ii++) { 314 if (ii > 0 && ii % 12 == 0) { 315 line = stream->readLine(); ++numLines; 316 } 317 QString type = line.mid(10 + 4*(ii % 12) + 1, 3).trimmed(); 318 if (!type.isEmpty()) { 319 _scaleFactors[sys][type] = factor; 320 } 321 } 247 322 } 248 323 else if (key == "SYS / PHASE SHIFT"){ … … 298 373 } 299 374 delete in; 375 } 376 else if (fullHeader && !key.isEmpty()) { 377 // kept verbatim, e.g. SIGNAL STRENGTH UNIT, LEAP SECONDS, 378 // SYS / DCBS APPLIED, SYS / PCVS APPLIED, ANTENNA: PHASECENTER 379 _otherRecords << line; 300 380 } 301 381 if (maxLines > 0 && numLines == maxLines) { … … 412 492 _receiverVersion = header._receiverVersion; 413 493 _startTime = header._startTime; 494 _timeSystem = header._timeSystem; 495 _rcvClkOffsAppl = header._rcvClkOffsAppl; 496 _satSystem = header._satSystem; 497 _scaleFactors = header._scaleFactors; 498 _otherRecords = header._otherRecords; 499 _otherRecordsVersion = header._version; 500 _hasLastObs = header._hasLastObs; 501 _hasObsCounts = header._hasObsCounts; 502 _hasPrnObs = header._hasPrnObs; 414 503 _usedSystems = header._usedSystems; 415 504 _comments = header._comments; … … 638 727 } 639 728 640 *stream << QString("%1 OBSERVATION DATA M") 729 // Satellite system: the single system of the observation types, the one 730 // of the input file (RINEX 2: one list of types for all systems) or M 731 // ----------------------------------------------------------------------- 732 char satSystem = 'M'; 733 if (_obsTypes.size() == 1) { 734 satSystem = _obsTypes.firstKey(); 735 } 736 else if (_version < 3.0 && _satSystem != 'M' && _satSystem != ' ' && 737 _obsTypes.contains(_satSystem)) { 738 satSystem = _satSystem; 739 } 740 *stream << QString("%1 OBSERVATION DATA %2") 641 741 .arg(_version, 9, 'f', 2) 742 .arg(QChar(satSystem)) 642 743 .leftJustified(60) 643 744 << "RINEX VERSION / TYPE\n"; … … 720 821 << "ANTENNA: DELTA H/E/N\n"; 721 822 823 if (_rcvClkOffsAppl >= 0) { 824 *stream << QString("%1") 825 .arg(_rcvClkOffsAppl, 6) 826 .leftJustified(60) 827 << "RCV CLOCK OFFS APPL\n"; 828 } 829 722 830 if (_version < 3.0) { 723 831 int defaultWlFact1 = _wlFactorsL1[1]; … … 731 839 732 840 *stream << obsTypesStrings().join(""); 841 842 // SYS / SCALE FACTOR (RINEX >= 3), for the observation types written 843 // ------------------------------------------------------------------- 844 if (_version >= 3.0) { 845 for (int iSys = 0; iSys < numSys(); iSys++) { 846 char sys = system(iSys); 847 if (!_scaleFactors.contains(sys)) { 848 continue; 849 } 850 QMap<int, QStringList> typesOfFactor; 851 for (int iType = 0; iType < nTypes(sys); iType++) { 852 QString type = obsType(sys, iType); 853 int factor = scaleFactor(sys, type); 854 if (factor > 1) { 855 typesOfFactor[factor] << type; 856 } 857 } 858 QMapIterator<int, QStringList> it(typesOfFactor); 859 while (it.hasNext()) { 860 it.next(); 861 QStringList types = it.value(); 862 if (types.size() == nTypes(sys)) { 863 types.clear(); // all observation types 864 } 865 QString hlp = QString("%1 %2 %3").arg(sys).arg(it.key(), 4).arg(types.size(), 2); 866 for (int ii = 0; ii < types.size(); ii++) { 867 if (ii > 0 && ii % 12 == 0) { 868 *stream << hlp.leftJustified(60) << "SYS / SCALE FACTOR\n"; 869 hlp = QString().leftJustified(10); 870 } 871 hlp += QString(" %1").arg(types[ii], -3); 872 } 873 *stream << hlp.leftJustified(60) << "SYS / SCALE FACTOR\n"; 874 } 875 } 876 } 733 877 734 878 if (_interval > 0) { … … 750 894 .arg(min, 6) 751 895 .arg(sec, 13, 'f', 7) 752 .arg( "GPS", 8)896 .arg(_timeSystem, 8) 753 897 .leftJustified(60) 754 898 << "TIME OF FIRST OBS\n"; 755 899 756 if (_version >= 3.0) { 900 if (_hasLastObs && _lastTime.valid()) { 901 _lastTime.civil_date(year, month, day); 902 _lastTime.civil_time(hour, min, sec); 903 *stream << QString("%1%2%3%4%5%6%7") 904 .arg(year, 6) 905 .arg(month, 6) 906 .arg(day, 6) 907 .arg(hour, 6) 908 .arg(min, 6) 909 .arg(sec, 13, 'f', 7) 910 .arg(_timeSystem, 8) 911 .leftJustified(60) 912 << "TIME OF LAST OBS\n"; 913 } 914 915 // SYS / PHASE SHIFT: RINEX 3 only; strongly deprecated in RINEX 4 916 // and to be ignored by encoders (RINEX 4.02, 5.2.12, Table A2) 917 // --------------------------------------------------------------- 918 if (_version >= 3.0 && _version < 4.0) { 757 919 if (_phaseShifts.empty()) { 758 920 QMap<char, QStringList>::const_iterator it; … … 875 1037 } 876 1038 1039 // Header records kept from the input file 1040 // --------------------------------------- 1041 for (int ii = 0; ii < _otherRecords.size(); ii++) { 1042 QString record = convertRecord(_otherRecords[ii], _version, _otherRecordsVersion); 1043 if (!record.isEmpty()) { 1044 *stream << record << "\n"; 1045 } 1046 } 1047 1048 // # OF SATELLITES and PRN / # OF OBS of the written file 1049 // ------------------------------------------------------ 1050 if ((_hasObsCounts || _hasPrnObs) && !_obsCounts.isEmpty()) { 1051 *stream << QString("%1").arg(_obsCounts.size(), 6).leftJustified(60) 1052 << "# OF SATELLITES\n"; 1053 QMapIterator<QString, QVector<int> > it(_obsCounts); 1054 while (_hasPrnObs && it.hasNext()) { 1055 it.next(); 1056 QString hlp = QString(" %1").arg(it.key(), 3); 1057 const QVector<int>& counts = it.value(); 1058 for (int ii = 0; ii < counts.size(); ii++) { 1059 if (ii > 0 && ii % 9 == 0) { 1060 *stream << hlp.leftJustified(60) << "PRN / # OF OBS\n"; 1061 hlp = QString().leftJustified(6); 1062 } 1063 hlp += QString("%1").arg(qMin(counts[ii], 99999), 6); 1064 } 1065 *stream << hlp.leftJustified(60) << "PRN / # OF OBS\n"; 1066 } 1067 } 1068 877 1069 *stream << QString() 878 1070 .leftJustified(60) 879 1071 << "END OF HEADER\n"; 1072 } 1073 1074 // Scale factor of an observation type (SYS / SCALE FACTOR, RINEX >= 3) 1075 //////////////////////////////////////////////////////////////////////////// 1076 int t_rnxObsHeader::scaleFactor(char sys, const QString& type) const { 1077 if (_version < 3.0 || !_scaleFactors.contains(sys)) { 1078 return 1; 1079 } 1080 const QMap<QString, int>& factors = _scaleFactors[sys]; 1081 if (factors.contains(type)) { 1082 return factors[type]; 1083 } 1084 return factors.value("", 1); 1085 } 1086 1087 // Header record (also from an event) converted to the output version; 1088 // empty if the record does not exist in the output version or defines 1089 // the layout of the observation records (given by the output header) 1090 //////////////////////////////////////////////////////////////////////////// 1091 QString t_rnxObsHeader::convertRecord(const QString& line, double outVersion, 1092 double inVersion) { 1093 QString value = line.left(60); 1094 QString key = line.mid(60).trimmed(); 1095 1096 static const QStringList layoutKeys = QStringList() 1097 << "RINEX VERSION / TYPE" << "END OF HEADER" << "SYS / # / OBS TYPES" 1098 << "# / TYPES OF OBSERV" << "SYS / SCALE FACTOR" << "TIME OF LAST OBS" 1099 << "# OF SATELLITES" << "PRN / # OF OBS"; 1100 static const QStringList v4Keys = QStringList() 1101 << "DOI" << "LICENSE OF USE" << "STATION INFORMATION"; 1102 static const QStringList v3Keys = QStringList() 1103 << "MARKER TYPE" << "SIGNAL STRENGTH UNIT" << "SYS / DCBS APPLIED" 1104 << "SYS / PCVS APPLIED" << "ANTENNA: PHASECENTER" << "ANTENNA: ZERODIR AZI" 1105 << "ANTENNA: ZERODIR XYZ" << "CENTER OF MASS: XYZ" << "ANTENNA: DELTA X/Y/Z" 1106 << "ANTENNA: B.SIGHT XYZ" << "GLONASS SLOT / FRQ #" << "GLONASS COD/PHS/BIS"; 1107 static const QStringList allKeys = QStringList() 1108 << "COMMENT" << "MARKER NAME" << "MARKER NUMBER" << "OBSERVER / AGENCY" 1109 << "REC # / TYPE / VERS" << "ANT # / TYPE" << "APPROX POSITION XYZ" 1110 << "ANTENNA: DELTA H/E/N" << "INTERVAL" << "TIME OF FIRST OBS" 1111 << "RCV CLOCK OFFS APPL" << "PGM / RUN BY / DATE"; 1112 1113 if (key.isEmpty() || layoutKeys.contains(key)) { 1114 return QString(); 1115 } 1116 else if (key == "SYS / PHASE SHIFT") { // RINEX 3 only 1117 if (outVersion < 3.0 || outVersion >= 4.0) { 1118 return QString(); 1119 } 1120 } 1121 else if (key == "WAVELENGTH FACT L1/2") { // RINEX 2 only 1122 if (outVersion >= 3.0) { 1123 return QString(); 1124 } 1125 } 1126 else if (key == "LEAP SECONDS") { // RINEX 2: I6 only 1127 if (outVersion < 3.0) { 1128 value = value.left(6); 1129 } 1130 } 1131 else if (v4Keys.contains(key)) { 1132 if (outVersion < 4.0) { 1133 return QString(); 1134 } 1135 } 1136 else if (v3Keys.contains(key)) { 1137 if (outVersion < 3.0) { 1138 return QString(); 1139 } 1140 } 1141 else if (!allKeys.contains(key)) { // not known to BNC 1142 if (inVersion > 0.0 && int(inVersion) != int(outVersion)) { 1143 return QString(); 1144 } 1145 } 1146 return value.leftJustified(60) + key; 880 1147 } 881 1148 … … 1041 1308 _inpOut = inpOut; 1042 1309 _stream = 0; 1310 _file = 0; 1043 1311 _flgPowerFail = false; 1312 _epoFile = 0; 1313 _epoStream = 0; 1044 1314 if (_inpOut == input) { 1045 1315 openRead(fileName); … … 1081 1351 _stream->seek(0); 1082 1352 _header.read(_stream); 1353 _pendingEvents.clear(); 1354 _flgPowerFail = false; 1083 1355 } 1084 1356 … … 1093 1365 _stream->seek(0); 1094 1366 _header.read(_stream); 1367 _pendingEvents.clear(); 1368 _flgPowerFail = false; 1095 1369 } 1096 1370 } … … 1116 1390 //////////////////////////////////////////////////////////////////////////// 1117 1391 void t_rnxObsFile::close() { 1392 if (_epoStream) { 1393 writeDeferredHeader(); 1394 } 1118 1395 delete _stream; _stream = 0; 1119 1396 delete _file; _file = 0; 1120 1397 } 1121 1398 1122 // Handle Special Epoch Flag 1123 //////////////////////////////////////////////////////////////////////////// 1124 void t_rnxObsFile::handleEpochFlag(int flag, const QString& line, 1125 bool& headerReRead) { 1126 1127 headerReRead = false; 1128 1129 // Power Failure 1130 // ------------- 1131 if (flag == 1) { 1132 _flgPowerFail = true; 1133 } 1134 1135 // Start moving antenna 1136 // -------------------- 1137 else if (flag == 2) { 1138 // no action 1139 } 1140 1141 // Re-Read Header 1142 // -------------- 1143 else if (flag == 3 || flag == 4 || flag == 5) { 1144 int numLines = 0; 1145 if (version() < 3.0) { 1146 readInt(line, 29, 3, numLines); 1399 // Header written on close: the epochs are written to a temporary file 1400 // first, so that the header can hold TIME OF FIRST/LAST OBS and 1401 // # OF SATELLITES / PRN / # OF OBS of the written epochs 1402 //////////////////////////////////////////////////////////////////////////// 1403 void t_rnxObsFile::writeHeaderOnClose(const QMap<QString, QString>& txtMap) { 1404 if (_inpOut != output || _epoStream) { 1405 return; 1406 } 1407 _txtMap = txtMap; 1408 _epoFile = new QTemporaryFile(_fileName + ".XXXXXX"); 1409 if (!_epoFile->open()) { 1410 delete _epoFile; _epoFile = 0; 1411 _header.write(_stream, &_txtMap); // no temporary file: header now 1412 return; 1413 } 1414 _epoStream = new QTextStream(_epoFile); 1415 } 1416 1417 // Write header and copy the epochs from the temporary file 1418 //////////////////////////////////////////////////////////////////////////// 1419 void t_rnxObsFile::writeDeferredHeader() { 1420 _epoStream->flush(); 1421 if (_firstEpoTime.valid()) { 1422 _header._startTime = _firstEpoTime; 1423 _header._lastTime = _lastEpoTime; 1424 } 1425 _header._obsCounts = _obsCounts; 1426 _header.write(_stream, &_txtMap); 1427 _stream->flush(); 1428 1429 _epoFile->seek(0); 1430 while (!_epoFile->atEnd()) { 1431 QByteArray buffer = _epoFile->read(1 << 20); 1432 if (buffer.isEmpty()) { 1433 break; 1434 } 1435 _file->write(buffer); 1436 } 1437 _file->flush(); 1438 1439 delete _epoStream; _epoStream = 0; 1440 delete _epoFile; _epoFile = 0; // removes the temporary file 1441 } 1442 1443 // Read Event Record (epoch flag 2-6); the event is kept until the next 1444 // observation epoch (or pendingEvents() at the end of the file) 1445 //////////////////////////////////////////////////////////////////////////// 1446 void t_rnxObsFile::readEvent(int flag, QString& line) { 1447 1448 t_rnxEvent event; 1449 event.flag = flag; 1450 event.version = version(); 1451 1452 bool v3 = (version() >= 3.0); 1453 1454 // Epoch (may be blank for events without significant epoch) 1455 // --------------------------------------------------------- 1456 QString epoStr = v3 ? line.mid(1, 28) : line.mid(0, 26); 1457 if (!epoStr.trimmed().isEmpty()) { 1458 QTextStream in(epoStr.toLatin1(), QIODevice::ReadOnly); 1459 int year = 0, month = 0, day = 0, hour = 0, min = 0; 1460 double sec = 0.0; 1461 in >> year >> month >> day >> hour >> min >> sec; 1462 if (!v3) { 1463 if (year < 80) {year += 2000;} 1464 else if (year < 100) {year += 1900;} 1465 } 1466 event.tt.set(year, month, day, hour, min, sec); 1467 event.epochValid = true; 1468 int psec; 1469 if (v3 && !line.mid(57, 5).trimmed().isEmpty() && 1470 readInt(line, 57, 5, psec) == 0 && psec >= 0 && psec <= 99999) { 1471 event.psec = psec; 1472 event.tt = event.tt + psec * 1.e-12; 1473 } 1474 } 1475 1476 int numRecords = 0; 1477 readInt(line, v3 ? 32 : 29, 3, numRecords); 1478 1479 // Cycle slip records (same format as observation records) 1480 // ------------------------------------------------------- 1481 if (flag == 6) { 1482 if (v3) { 1483 readSatRecordsV3(numRecords, event.slips); 1147 1484 } 1148 1485 else { 1149 readInt(line, 32, 3, numLines); 1486 readSatRecordsV2(line, numRecords, event.slips); 1487 } 1488 } 1489 1490 // Special records (header records) 1491 // -------------------------------- 1492 else { 1493 for (int ii = 0; ii < numRecords; ii++) { 1494 event.records << _stream->readLine(); 1150 1495 } 1151 1496 if (flag == 3 || flag == 4) { 1152 _header.read(_stream, numLines); 1153 headerReRead = true; 1154 } 1155 else { 1156 for (int ii = 0; ii < numLines; ii++) { 1157 _stream->readLine(); 1158 } 1159 } 1160 } 1161 1162 // Unhandled Flag 1163 // -------------- 1164 else { 1165 throw QString("t_rnxObsFile: unhandled flag\n" + line); 1497 QString hlp = event.records.join("\n"); 1498 QTextStream in(&hlp, QIODevice::ReadOnly); 1499 _header.read(&in, numRecords); 1500 } 1501 } 1502 1503 _pendingEvents.append(event); 1504 } 1505 1506 // Events read after the last observation epoch 1507 //////////////////////////////////////////////////////////////////////////// 1508 QList<t_rnxObsFile::t_rnxEvent> t_rnxObsFile::pendingEvents() { 1509 QList<t_rnxEvent> events = _pendingEvents; 1510 _pendingEvents.clear(); 1511 return events; 1512 } 1513 1514 // Read Satellite Records (RINEX Version 3) 1515 //////////////////////////////////////////////////////////////////////////// 1516 void t_rnxObsFile::readSatRecordsV3(int numSat, std::vector<t_rnxSat>& rnxSats) { 1517 rnxSats.resize(numSat); 1518 for (int iSat = 0; iSat < numSat; iSat++) { 1519 QString line = _stream->readLine(); 1520 t_prn prn; prn.set(line.left(3).toLatin1().data()); 1521 char sys = prn.system(); 1522 int num = prn.number(); 1523 int flag = t_corrSSR::getSsrNavTypeFlag(sys, num); 1524 prn.setFlag(flag); 1525 rnxSats[iSat].prn = prn; 1526 for (int iType = 0; iType < _header.nTypes(sys); iType++) { 1527 int pos = 3 + 16*iType; 1528 double obsValue = 0.0; 1529 int lli = 0; 1530 int snr = 0; 1531 readDbl(line, pos, 14, obsValue); 1532 readInt(line, pos + 14, 1, lli); 1533 readInt(line, pos + 15, 1, snr); 1534 if (_flgPowerFail) { 1535 lli |= 1; 1536 } 1537 QString type = obsType(sys, iType); 1538 rnxSats[iSat].obs[type].value = obsValue / _header.scaleFactor(sys, type); 1539 rnxSats[iSat].obs[type].lli = lli; 1540 rnxSats[iSat].obs[type].snr = snr; 1541 } 1542 } 1543 } 1544 1545 // Read Satellite List (epoch line and continuation lines) and Satellite 1546 // Records (RINEX Version 2) 1547 //////////////////////////////////////////////////////////////////////////// 1548 void t_rnxObsFile::readSatRecordsV2(QString& line, int numSat, 1549 std::vector<t_rnxSat>& rnxSats) { 1550 rnxSats.resize(numSat); 1551 1552 // Read Satellite Numbers 1553 // ---------------------- 1554 int pos = 32; 1555 for (int iSat = 0; iSat < numSat; iSat++) { 1556 if (iSat > 0 && iSat % 12 == 0) { 1557 line = _stream->readLine(); 1558 pos = 32; 1559 } 1560 1561 char sys = line.toLatin1()[pos]; 1562 if (sys == ' ') { 1563 sys = 'G'; 1564 } 1565 int num; readInt(line, pos + 1, 2, num); 1566 int flag = t_corrSSR::getSsrNavTypeFlag(sys, num); 1567 t_prn prn; 1568 prn.set(sys, num, flag); 1569 rnxSats[iSat].prn = prn; 1570 1571 pos += 3; 1572 } 1573 1574 // Read Observation Records 1575 // ------------------------ 1576 for (int iSat = 0; iSat < numSat; iSat++) { 1577 char sys = rnxSats[iSat].prn.system(); 1578 line = _stream->readLine(); 1579 pos = 0; 1580 for (int iType = 0; iType < _header.nTypes(sys); iType++) { 1581 if (iType > 0 && iType % 5 == 0) { 1582 line = _stream->readLine(); 1583 pos = 0; 1584 } 1585 double obsValue = 0.0; 1586 int lli = 0; 1587 int snr = 0; 1588 readDbl(line, pos, 14, obsValue); 1589 readInt(line, pos + 14, 1, lli); 1590 readInt(line, pos + 15, 1, snr); 1591 1592 if (_flgPowerFail) { 1593 lli |= 1; 1594 } 1595 1596 QString type = obsType(sys, iType); 1597 rnxSats[iSat].obs[type].value = obsValue; 1598 rnxSats[iSat].obs[type].lli = lli; 1599 rnxSats[iSat].obs[type].snr = snr; 1600 1601 pos += 16; 1602 } 1166 1603 } 1167 1604 } … … 1193 1630 int flag = 0; 1194 1631 readInt(line, 31, 1, flag); 1195 if (flag > 0) { 1196 bool headerReRead = false; 1197 handleEpochFlag(flag, line, headerReRead); 1198 if (headerReRead) { 1199 continue; 1200 } 1201 } 1632 if (flag > 1) { 1633 readEvent(flag, line); 1634 continue; 1635 } 1636 if (flag == 1) { // power failure between previous and current epoch 1637 _flgPowerFail = true; 1638 } 1639 _currEpo.flag = flag; 1202 1640 1203 1641 QTextStream in(line.mid(1).toLatin1(), QIODevice::ReadOnly); … … 1215 1653 readInt(line, 32, 3, numSat); 1216 1654 1217 _currEpo.rnxSat.resize(numSat); 1655 // Receiver Clock Offset (optional, 6X,F15.12) 1656 // and additional pico-sec digits (optional, RINEX >= 4.02, 1X,I5.5) 1657 // ----------------------------------------------------------------- 1658 double clkOffset; 1659 if (!line.mid(41, 15).trimmed().isEmpty() && 1660 readDbl(line, 41, 15, clkOffset) == 0) { 1661 _currEpo.clkOffset = clkOffset; 1662 _currEpo.clkOffsetValid = true; 1663 } 1664 int psec; 1665 if (!line.mid(57, 5).trimmed().isEmpty() && 1666 readInt(line, 57, 5, psec) == 0 && psec >= 0 && psec <= 99999) { 1667 _currEpo.psec = psec; 1668 _currEpo.tt = _currEpo.tt + psec * 1.e-12; 1669 } 1218 1670 1219 1671 // Observations 1220 1672 // ------------ 1221 for (int iSat = 0; iSat < numSat; iSat++) { 1222 line = _stream->readLine(); 1223 t_prn prn; prn.set(line.left(3).toLatin1().data()); 1224 char sys = prn.system(); 1225 int num = prn.number(); 1226 int flag = t_corrSSR::getSsrNavTypeFlag(sys, num); 1227 prn.setFlag(flag); 1228 _currEpo.rnxSat[iSat].prn = prn; 1229 for (int iType = 0; iType < _header.nTypes(sys); iType++) { 1230 int pos = 3 + 16*iType; 1231 double obsValue = 0.0; 1232 int lli = 0; 1233 int snr = 0; 1234 readDbl(line, pos, 14, obsValue); 1235 readInt(line, pos + 14, 1, lli); 1236 readInt(line, pos + 15, 1, snr); 1237 if (_flgPowerFail) { 1238 lli |= 1; 1239 } 1240 QString type = obsType(sys, iType); 1241 _currEpo.rnxSat[iSat].obs[type].value = obsValue; 1242 _currEpo.rnxSat[iSat].obs[type].lli = lli; 1243 _currEpo.rnxSat[iSat].obs[type].snr = snr; 1244 } 1245 } 1673 readSatRecordsV3(numSat, _currEpo.rnxSat); 1246 1674 1247 1675 _flgPowerFail = false; 1676 1677 _currEpo.events = _pendingEvents; 1678 _pendingEvents.clear(); 1248 1679 1249 1680 return &_currEpo; … … 1267 1698 int flag = 0; 1268 1699 readInt(line, 28, 1, flag); 1269 if (flag > 0) { 1270 bool headerReRead = false; 1271 handleEpochFlag(flag, line, headerReRead); 1272 if (headerReRead) { 1273 continue; 1274 } 1275 } 1700 if (flag > 1) { 1701 readEvent(flag, line); 1702 continue; 1703 } 1704 if (flag == 1) { // power failure between previous and current epoch 1705 _flgPowerFail = true; 1706 } 1707 _currEpo.flag = flag; 1276 1708 1277 1709 QTextStream in(line.toLatin1(), QIODevice::ReadOnly); … … 1295 1727 readInt(line, 29, 3, numSat); 1296 1728 1297 _currEpo.rnxSat.resize(numSat); 1298 1299 // Read Satellite Numbers 1300 // ---------------------- 1301 int pos = 32; 1302 for (int iSat = 0; iSat < numSat; iSat++) { 1303 if (iSat > 0 && iSat % 12 == 0) { 1304 line = _stream->readLine(); 1305 pos = 32; 1306 } 1307 1308 char sys = line.toLatin1()[pos]; 1309 if (sys == ' ') { 1310 sys = 'G'; 1311 } 1312 int num; readInt(line, pos + 1, 2, num); 1313 int flag = t_corrSSR::getSsrNavTypeFlag(sys, num); 1314 t_prn prn; 1315 prn.set(sys, num, flag); 1316 _currEpo.rnxSat[iSat].prn = prn; 1317 1318 pos += 3; 1319 } 1320 1321 // Read Observation Records 1322 // ------------------------ 1323 for (int iSat = 0; iSat < numSat; iSat++) { 1324 char sys = _currEpo.rnxSat[iSat].prn.system(); 1325 line = _stream->readLine(); 1326 pos = 0; 1327 for (int iType = 0; iType < _header.nTypes(sys); iType++) { 1328 if (iType > 0 && iType % 5 == 0) { 1329 line = _stream->readLine(); 1330 pos = 0; 1331 } 1332 double obsValue = 0.0; 1333 int lli = 0; 1334 int snr = 0; 1335 readDbl(line, pos, 14, obsValue); 1336 readInt(line, pos + 14, 1, lli); 1337 readInt(line, pos + 15, 1, snr); 1338 1339 if (_flgPowerFail) { 1340 lli |= 1; 1341 } 1342 1343 QString type = obsType(sys, iType); 1344 _currEpo.rnxSat[iSat].obs[type].value = obsValue; 1345 _currEpo.rnxSat[iSat].obs[type].lli = lli; 1346 _currEpo.rnxSat[iSat].obs[type].snr = snr; 1347 1348 pos += 16; 1349 } 1350 } 1729 // Receiver Clock Offset (optional, F12.9) 1730 // --------------------------------------- 1731 double clkOffset; 1732 if (!line.mid(68, 12).trimmed().isEmpty() && 1733 readDbl(line, 68, 12, clkOffset) == 0) { 1734 _currEpo.clkOffset = clkOffset; 1735 _currEpo.clkOffsetValid = true; 1736 } 1737 1738 // Satellite list and observations 1739 // ------------------------------- 1740 readSatRecordsV2(line, numSat, _currEpo.rnxSat); 1351 1741 1352 1742 _flgPowerFail = false; 1743 1744 _currEpo.events = _pendingEvents; 1745 _pendingEvents.clear(); 1353 1746 1354 1747 return &_currEpo; … … 1365 1758 } 1366 1759 t_rnxEpo epoLocal; 1367 epoLocal.tt = epo->tt; 1368 for (unsigned ii = 0; ii < epo->rnxSat.size(); ii++) { 1369 const t_rnxSat& rnxSat = epo->rnxSat[ii]; 1370 if (_header._obsTypes[rnxSat.prn.system()].size() > 0) { 1371 if (_header.version() < 3.0) { // exclude new GNSS such as BDS, QZSS, NavIC, etc. 1760 epoLocal.tt = epo->tt; 1761 epoLocal.clkOffset = epo->clkOffset; 1762 epoLocal.clkOffsetValid = epo->clkOffsetValid; 1763 epoLocal.psec = epo->psec; 1764 epoLocal.flag = epo->flag; 1765 epoLocal.rnxSat = selectSats(_header, epo->rnxSat); 1766 1767 QTextStream* stream = _epoStream ? _epoStream : _stream; 1768 1769 writeEvents(stream, _header, epo->events); 1770 1771 if (!_firstEpoTime.valid()) { 1772 _firstEpoTime = epo->tt; 1773 } 1774 _lastEpoTime = epo->tt; 1775 1776 if (version() < 3.0) { 1777 return writeEpochV2(stream, _header, &epoLocal, &_obsCounts); 1778 } 1779 else { 1780 return writeEpochV3(stream, _header, &epoLocal, &_obsCounts); 1781 } 1782 } 1783 1784 // Satellites with observation types in the header, sorted 1785 //////////////////////////////////////////////////////////////////////////// 1786 std::vector<t_rnxObsFile::t_rnxSat> t_rnxObsFile::selectSats(const t_rnxObsHeader& header, 1787 const std::vector<t_rnxSat>& rnxSats) { 1788 std::vector<t_rnxSat> selected; 1789 for (unsigned ii = 0; ii < rnxSats.size(); ii++) { 1790 const t_rnxSat& rnxSat = rnxSats[ii]; 1791 if (header._obsTypes.value(rnxSat.prn.system()).size() > 0) { 1792 if (header.version() < 3.0) { // exclude new GNSS such as BDS, QZSS, NavIC, etc. 1372 1793 if (rnxSat.prn.system() != 'G' && rnxSat.prn.system() != 'R' && 1373 1794 rnxSat.prn.system() != 'E' && rnxSat.prn.system() != 'S' && … … 1376 1797 } 1377 1798 } 1378 epoLocal.rnxSat.push_back(rnxSat); 1379 } 1380 } 1381 std::stable_sort(epoLocal.rnxSat.begin(), epoLocal.rnxSat.end(), t_rnxSat::prnSort); 1382 1383 if (version() < 3.0) { 1384 return writeEpochV2(_stream, _header, &epoLocal); 1385 } 1386 else { 1387 return writeEpochV3(_stream, _header, &epoLocal); 1799 selected.push_back(rnxSat); 1800 } 1801 } 1802 std::stable_sort(selected.begin(), selected.end(), t_rnxSat::prnSort); 1803 return selected; 1804 } 1805 1806 // Civil date and time of an epoch record (F11.7 seconds); seconds that 1807 // would be rounded to 60.0000000 are carried over into the next minute 1808 //////////////////////////////////////////////////////////////////////////// 1809 static void epochFields(const bncTime& tt, unsigned& year, unsigned& month, 1810 unsigned& day, unsigned& hour, unsigned& min, double& sec) { 1811 tt.civil_date(year, month, day); 1812 tt.civil_time(hour, min, sec); 1813 if (nint(sec * 1.e7) >= 600000000) { 1814 bncTime ttMin; 1815 ttMin.set(int(year), int(month), int(day), int(hour), int(min), 0.0); 1816 ttMin = ttMin + 60.0; 1817 ttMin.civil_date(year, month, day); 1818 ttMin.civil_time(hour, min, sec); 1819 sec = 0.0; 1820 } 1821 } 1822 1823 // Epoch fields of an epoch record: "> yyyy mm dd hh mm ss.sssssss" (V3) or 1824 // " yy mm dd hh mm ss.sssssss" (V2); with pico-sec digits (RINEX >= 4.02) 1825 // the seconds hold the epoch floored to 0.1 microsec 1826 //////////////////////////////////////////////////////////////////////////// 1827 static QString epochString(const bncTime& epoTime, int psec, bool writePsec, bool v3) { 1828 bncTime tt = writePsec ? epoTime - psec * 1.e-12 : epoTime; 1829 unsigned year, month, day, hour, min; 1830 double sec; 1831 epochFields(tt, year, month, day, hour, min, sec); 1832 return QString(v3 ? "> %1 %2 %3 %4 %5%6" : " %1 %2 %3 %4 %5%6") 1833 .arg(v3 ? int(year) : int(fmod(double(year), 100.0)), v3 ? 4 : 2, 10, QChar(v3 ? ' ' : '0')) 1834 .arg(month, 2, 10, QChar('0')) 1835 .arg(day, 2, 10, QChar('0')) 1836 .arg(hour, 2, 10, QChar('0')) 1837 .arg(min, 2, 10, QChar('0')) 1838 .arg(sec, 11, 'f', 7); 1839 } 1840 1841 // Write Events (epoch flags 2-6) 1842 //////////////////////////////////////////////////////////////////////////// 1843 void t_rnxObsFile::writeEvents(QTextStream* stream, const t_rnxObsHeader& header, 1844 const QList<t_rnxEvent>& events) { 1845 bool v3 = (header.version() >= 3.0); 1846 for (int iEv = 0; iEv < events.size(); iEv++) { 1847 const t_rnxEvent& event = events[iEv]; 1848 1849 // Cycle slip records: written like an observation epoch 1850 // ----------------------------------------------------- 1851 if (event.flag == 6) { 1852 t_rnxEpo epo; 1853 epo.tt = event.tt; 1854 epo.psec = event.psec; 1855 epo.flag = 6; 1856 epo.rnxSat = selectSats(header, event.slips); 1857 if (event.epochValid && !epo.rnxSat.empty()) { 1858 if (v3) { 1859 writeEpochV3(stream, header, &epo); 1860 } 1861 else { 1862 writeEpochV2(stream, header, &epo); 1863 } 1864 } 1865 continue; 1866 } 1867 1868 // Special records: header records valid in the output version, without 1869 // the ones defining the observation record layout (fixed by the output 1870 // header) 1871 // ------------------------------------------------------------------- 1872 QStringList records; 1873 for (int ii = 0; ii < event.records.size(); ii++) { 1874 QString record = t_rnxObsHeader::convertRecord(event.records[ii], header.version(), event.version); 1875 if (!record.isEmpty()) { 1876 records << record; 1877 } 1878 } 1879 if ((event.flag == 3 || event.flag == 4) && records.isEmpty()) { 1880 continue; 1881 } 1882 1883 bool writePsec = (event.epochValid && event.psec >= 0 && header.version() >= 4.02); 1884 QString epoLine; 1885 if (event.epochValid) { 1886 epoLine = epochString(event.tt, event.psec, writePsec, v3); 1887 } 1888 else { 1889 epoLine = v3 ? QString(">").leftJustified(29) : QString().leftJustified(26); 1890 } 1891 epoLine += QString("%1%2").arg(event.flag, 3).arg(records.size(), 3); 1892 if (writePsec) { 1893 epoLine += QString().leftJustified(21) + QString(" %1").arg(event.psec, 5, 10, QChar('0')); 1894 } 1895 *stream << epoLine << "\n"; 1896 for (int ii = 0; ii < records.size(); ii++) { 1897 *stream << records[ii] << "\n"; 1898 } 1388 1899 } 1389 1900 } … … 1392 1903 //////////////////////////////////////////////////////////////////////////// 1393 1904 void t_rnxObsFile::writeEpochV2(QTextStream* stream, const t_rnxObsHeader& header, 1394 const t_rnxEpo* epo) { 1395 1396 unsigned year, month, day, hour, min; 1397 double sec; 1398 epo->tt.civil_date(year, month, day); 1399 epo->tt.civil_time(hour, min, sec); 1400 1401 QString dateStr; 1402 QTextStream(&dateStr) << QString(" %1 %2 %3 %4 %5%6") 1403 .arg(int(fmod(double(year), 100.0)), 2, 10, QChar('0')) 1404 .arg(month, 2, 10, QChar('0')) 1405 .arg(day, 2, 10, QChar('0')) 1406 .arg(hour, 2, 10, QChar('0')) 1407 .arg(min, 2, 10, QChar('0')) 1408 .arg(sec, 11, 'f', 7); 1409 1410 int flag = 0; 1411 *stream << dateStr << QString("%1%2").arg(flag, 3).arg(epo->rnxSat.size(), 3); 1905 const t_rnxEpo* epo, t_obsCounts* obsCounts) { 1906 1907 QString dateStr = epochString(epo->tt, epo->psec, false, false); 1908 1909 int flag = epo->flag; 1910 QString epoLine = dateStr + QString("%1%2").arg(flag, 3).arg(epo->rnxSat.size(), 3); 1412 1911 for (unsigned iSat = 0; iSat < epo->rnxSat.size(); iSat++) { 1413 1912 const t_rnxSat& rnxSat = epo->rnxSat[iSat]; 1414 1913 if (iSat > 0 && iSat % 12 == 0) { 1415 *stream << Qt::endl << QString().leftJustified(32); 1416 } 1417 *stream << rnxSat.prn.toString().c_str(); 1418 } 1419 *stream << Qt::endl; 1914 if (iSat == 12 && epo->clkOffsetValid) { 1915 epoLine += QString("%1").arg(epo->clkOffset, 12, 'f', 9); 1916 } 1917 *stream << epoLine << Qt::endl; 1918 epoLine = QString().leftJustified(32); 1919 } 1920 epoLine += rnxSat.prn.toString().c_str(); 1921 } 1922 // Receiver clock offset (F12.9, columns 69-80 of the first line) 1923 // ------------------------------------------------------------- 1924 if (epo->rnxSat.size() <= 12 && epo->clkOffsetValid) { 1925 epoLine = epoLine.leftJustified(68) + QString("%1").arg(epo->clkOffset, 12, 'f', 9); 1926 } 1927 *stream << epoLine << Qt::endl; 1420 1928 1421 1929 for (unsigned iSat = 0; iSat < epo->rnxSat.size(); iSat++) { 1422 1930 const t_rnxSat& rnxSat = epo->rnxSat[iSat]; 1423 1931 char sys = rnxSat.prn.system(); 1932 QVector<int>* counts = 0; 1933 if (obsCounts) { 1934 counts = &(*obsCounts)[QString(rnxSat.prn.toString().c_str())]; 1935 counts->resize(header.nTypes(sys)); 1936 } 1424 1937 for (int iTypeV2 = 0; iTypeV2 < header.nTypes(sys); iTypeV2++) { 1425 1938 if (iTypeV2 > 0 && iTypeV2 % 5 == 0) { … … 1458 1971 else { 1459 1972 *stream << QString("%1").arg(rnxObs.value, 14, 'f', 3); 1973 if (counts) { 1974 (*counts)[iTypeV2]++; 1975 } 1460 1976 if (rnxObs.lli != 0.0) { 1461 1977 *stream << QString("%1").arg(rnxObs.lli,1); … … 1487 2003 //////////////////////////////////////////////////////////////////////////// 1488 2004 void t_rnxObsFile::writeEpochV3(QTextStream* stream, const t_rnxObsHeader& header, 1489 const t_rnxEpo* epo) { 1490 1491 unsigned year, month, day, hour, min; 1492 double sec; 1493 epo->tt.civil_date(year, month, day); 1494 epo->tt.civil_time(hour, min, sec); 1495 1496 QString dateStr; 1497 QTextStream(&dateStr) << QString("> %1 %2 %3 %4 %5%6") 1498 .arg(year, 4) 1499 .arg(month, 2, 10, QChar('0')) 1500 .arg(day, 2, 10, QChar('0')) 1501 .arg(hour, 2, 10, QChar('0')) 1502 .arg(min, 2, 10, QChar('0')) 1503 .arg(sec, 11, 'f', 7); 1504 1505 int flag = 0; 1506 *stream << dateStr << QString("%1%2\n").arg(flag, 3).arg(epo->rnxSat.size(), 3); 2005 const t_rnxEpo* epo, t_obsCounts* obsCounts) { 2006 2007 // Additional pico-sec digits (RINEX >= 4.02): the seconds field holds the 2008 // epoch floored to 0.1 microsec, the digits beyond are appended 2009 // (RINEX < 4.02: epoch rounded to 0.1 microsec) 2010 // ------------------------------------------------------------------------ 2011 bool writePsec = (epo->psec >= 0 && header.version() >= 4.02); 2012 QString dateStr = epochString(epo->tt, epo->psec, writePsec, true); 2013 2014 int flag = epo->flag; 2015 QString epoLine = dateStr + QString("%1%2").arg(flag, 3).arg(epo->rnxSat.size(), 3); 2016 if (epo->clkOffsetValid || writePsec) { 2017 epoLine += QString().leftJustified(6); 2018 if (epo->clkOffsetValid) { 2019 epoLine += QString("%1").arg(epo->clkOffset, 15, 'f', 12); 2020 } 2021 else { 2022 epoLine += QString().leftJustified(15); 2023 } 2024 if (writePsec) { 2025 epoLine += QString(" %1").arg(epo->psec, 5, 10, QChar('0')); 2026 } 2027 } 2028 *stream << epoLine << "\n"; 1507 2029 1508 2030 for (unsigned iSat = 0; iSat < epo->rnxSat.size(); iSat++) { … … 1561 2083 if (header.nTypes(sys)) { 1562 2084 QString record = rnxSat.prn.toString().c_str(); 2085 QVector<int>* counts = 0; 2086 if (obsCounts) { 2087 counts = &(*obsCounts)[record]; 2088 counts->resize(header.nTypes(sys)); 2089 } 1563 2090 for (int iTypeV3 = 0; iTypeV3 < header.nTypes(sys); iTypeV3++) { 1564 2091 const t_rnxObs* rnxObs = hlp[iTypeV3]; … … 1567 2094 } 1568 2095 else { 1569 record += QString("%1").arg(rnxObs->value, 14, 'f', 3); 2096 // SYS / SCALE FACTOR of the output header 2097 int scale = header.scaleFactor(sys, header.obsType(sys, iTypeV3)); 2098 record += QString("%1").arg(rnxObs->value * scale, 14, 'f', 3); 2099 if (counts) { 2100 (*counts)[iTypeV3]++; 2101 } 1570 2102 if (rnxObs->lli != 0.0) { 1571 2103 record += QString("%1").arg(rnxObs->lli, 1); -
trunk/BNC/src/rinex/rnxobsfile.h
r11023 r11192 73 73 void write(QTextStream* stream, const QMap<QString, QString>* txtMap = 0) const; 74 74 bncTime startTime() const {return _startTime;} 75 int rcvClkOffsAppl() const {return _rcvClkOffsAppl;} 76 int scaleFactor(char sys, const QString& type) const; 77 static QString convertRecord(const QString& line, double outVersion, double inVersion); 75 78 void setStartTime(const bncTime& startTime) {_startTime = startTime;} 76 79 … … 107 110 int _wlFactorsL2[t_prn::MAXPRN_GPS+1]; 108 111 bncTime _startTime; 112 QString _timeSystem; // TIME OF FIRST OBS system time 113 int _rcvClkOffsAppl; // RCV CLOCK OFFS APPL, -1: not given 114 char _satSystem; // RINEX VERSION / TYPE satellite system 115 QMap<char, QMap<QString, int> > _scaleFactors; // SYS / SCALE FACTOR, type "": all types 116 // Header records not interpreted (or not written) by BNC, kept verbatim 117 QStringList _otherRecords; 118 double _otherRecordsVersion; // RINEX version of _otherRecords 119 // TIME OF LAST OBS, # OF SATELLITES and PRN / # OF OBS: written if given 120 // in the input header, with the values of the written file 121 bool _hasLastObs; 122 bool _hasObsCounts; // # OF SATELLITES 123 bool _hasPrnObs; // PRN / # OF OBS 124 bncTime _lastTime; 125 QMap<QString, QVector<int> > _obsCounts; // per PRN and obs. type 109 126 bool _writeRinexOnlyWithSklObsTypes; 110 127 … … 136 153 }; 137 154 155 // Event (epoch flag 2-6) with its special records 156 class t_rnxEvent { 157 public: 158 t_rnxEvent() {flag = 0; epochValid = false; psec = -1; version = 0.0;} 159 int flag; 160 double version; // RINEX version of the input file 161 bool epochValid; // epoch fields not blank 162 bncTime tt; // incl. psec (if given) 163 int psec; 164 QStringList records; // flag 2-5: header records 165 std::vector<t_rnxSat> slips; // flag 6: cycle slip records 166 }; 167 138 168 class t_rnxEpo { 139 169 public: … … 142 172 tt.reset(); 143 173 rnxSat.clear(); 144 } 145 bncTime tt; 174 clkOffset = 0.0; 175 clkOffsetValid = false; 176 psec = -1; 177 flag = 0; 178 events.clear(); 179 } 180 bncTime tt; // incl. psec (if given) 146 181 std::vector<t_rnxSat> rnxSat; 182 double clkOffset; // receiver clock offset estimate [s] 183 bool clkOffsetValid; // clock offset given in epoch record 184 int psec; // additional pico-sec digits of the 185 // epoch (RINEX >= 4.02, 0..99999), 186 // -1: not given 187 int flag; // epoch flag 0 or 1 (power failure) 188 QList<t_rnxEvent> events; // events preceding this epoch 147 189 }; 148 190 … … 233 275 } 234 276 t_rnxEpo epoLocal; 235 epoLocal.tt = epo->tt; 277 epoLocal.tt = epo->tt; 278 epoLocal.clkOffset = epo->clkOffset; 279 epoLocal.clkOffsetValid = epo->clkOffsetValid; 280 epoLocal.psec = epo->psec; 281 epoLocal.flag = epo->flag; 236 282 for (unsigned ii = 0; ii < epo->rnxSat.size(); ii++) { 237 283 const t_rnxSat& rnxSat = epo->rnxSat[ii]; … … 248 294 std::stable_sort(epoLocal.rnxSat.begin(), epoLocal.rnxSat.end(), t_rnxSat::prnSort); 249 295 296 writeEvents(stream, header, epo->events); 250 297 if (header.version() >= 3.0) { 251 298 writeEpochV3(stream, header, &epoLocal); … … 256 303 } 257 304 305 // Write events (e.g. the ones after the last epoch, see pendingEvents()) 306 void writeEvents(const QList<t_rnxEvent>& events) { 307 writeEvents(_epoStream ? _epoStream : _stream, _header, events); 308 } 309 static void writeEvents(QTextStream* stream, const t_rnxObsHeader& header, 310 const QList<t_rnxEvent>& events); 311 312 // Events read after the last observation epoch 313 QList<t_rnxEvent> pendingEvents(); 314 315 // Output: header written on close(), with TIME OF FIRST/LAST OBS and 316 // PRN / # OF OBS of the written epochs; epochs go to a temporary file 317 void writeHeaderOnClose(const QMap<QString, QString>& txtMap); 318 258 319 private: 259 static void writeEpochV2(QTextStream* stream, const t_rnxObsHeader& header, const t_rnxEpo* epo); 260 static void writeEpochV3(QTextStream* stream, const t_rnxObsHeader& header, const t_rnxEpo* epo); 320 typedef QMap<QString, QVector<int> > t_obsCounts; 321 static void writeEpochV2(QTextStream* stream, const t_rnxObsHeader& header, const t_rnxEpo* epo, 322 t_obsCounts* obsCounts = 0); 323 static void writeEpochV3(QTextStream* stream, const t_rnxObsHeader& header, const t_rnxEpo* epo, 324 t_obsCounts* obsCounts = 0); 261 325 t_rnxObsFile() {}; 262 326 void openRead(const QString& fileName); … … 265 329 t_rnxEpo* nextEpochV2(); 266 330 t_rnxEpo* nextEpochV3(); 267 void handleEpochFlag(int flag, const QString& line, bool& headerReRead); 331 static std::vector<t_rnxSat> selectSats(const t_rnxObsHeader& header, 332 const std::vector<t_rnxSat>& rnxSats); 333 void readEvent(int flag, QString& line); 334 void readSatRecordsV3(int numSat, std::vector<t_rnxSat>& rnxSats); 335 void readSatRecordsV2(QString& line, int numSat, std::vector<t_rnxSat>& rnxSats); 336 void writeDeferredHeader(); 268 337 269 338 e_inpOut _inpOut; … … 274 343 t_rnxEpo _currEpo; 275 344 bool _flgPowerFail; 345 QList<t_rnxEvent> _pendingEvents; 346 // header written on close 347 QTemporaryFile* _epoFile; 348 QTextStream* _epoStream; 349 QMap<QString, QString> _txtMap; 350 bncTime _firstEpoTime; 351 bncTime _lastEpoTime; 352 t_obsCounts _obsCounts; 276 353 }; 277 354 -
trunk/BNC/tests/README.md
r11187 r11192 26 26 | `cases/<name>/expected/*.gz` | normalised reference outputs | 27 27 | `data/*.raw` | short raw captures (BNC `rawOutFile` format) replayed via `--file` | 28 | `data/rnx402_MO.rnx`, `data/rnx402_MN.rnx` | crafted RINEX 4.02 files: 3 BRUX epochs with receiver clock offset, pico-sec digits and `RCV CLOCK OFFS APPL`; GLONASS FDMA/L1OC/L3OC ephemerides and STO/EOP/ION examples of the RINEX 4.02 document (ION with subtypes KLOB/NEQN, ION J CNVX without subtype) | 29 | `data/rnx402_events_MO.rnx` | `data/rnx402_MO.rnx` with event records (flags 1-6, blank epochs, pico-sec event epoch, events after the last epoch) and further header records (SYS / SCALE FACTOR, DCBS/PCVS APPLIED, CENTER OF MASS: XYZ, PRN / # OF OBS, an unknown record) | 28 30 | `tools/fakecaster.py` | local Ntrip caster recording an upload as a BNC raw file (case key `fakecaster`); optionally TLS and serving a stream | 29 31 | `tools/bncraw.py` | `info` (streams, time span, RTCM3 message types), `slice` and `crs` (decode 1300/1301/1302) for raw captures | … … 49 51 * `rinex_edit_minele`: Edit/Concatenate with a 10 deg elevation mask (1 h BRUX 50 52 files), covering the ephemeris selection for the elevation mask. 53 * `rinex_edit_v402`: Edit/Concatenate of `data/rnx402_*` to RINEX 4.02 at 54 0.1 sec sampling: epoch clock offset and pico-sec digits kept unchanged, no 55 `SYS / PHASE SHIFT` records (deprecated in RINEX 4), L1OC/L3OC records 56 complete (incl. Beta, AODE/AODC order), non-EPH records skipped. 57 * `rinex_edit_v402_to_v2`: the same observations as RINEX 2.11 (clock offset 58 F12.9 in columns 69-80; the pico-sec digits cannot be written there). 59 * `rinex_edit_v402_events`, `rinex_edit_v402_events_to_v2`: Edit/Concatenate 60 of `data/rnx402_events_MO.rnx` to RINEX 4.02 and 2.11: events kept (in RINEX 2 61 without records not existing there, and without changes of the observation 62 types), scaled observations, header records kept, TIME OF LAST OBS, 63 # OF SATELLITES and PRN / # OF OBS of the written file. 51 64 * Stream replays of the real-time examples (`replay_*`), all from the 5 min 52 65 slice `data/wtzr_cnes_5min.raw` (BCEP00BKG0, WTZR00DEU0, SSRA00CNE1):
Note:
See TracChangeset
for help on using the changeset viewer.
