Changeset 11192 in ntrip


Ignore:
Timestamp:
Nov 10, 2026, 12:13:23 PM (less than one hour ago)
Author:
stuerze
Message:

rinex version 4.02 now fully supported

Location:
trunk/BNC
Files:
24 added
14 edited

Legend:

Unmodified
Added
Removed
  • trunk/BNC/CHANGELOG.md

    r11190 r11192  
    11# Changelog
    22## 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
    412- FIXED: Build with BNC_DEBUG_BCE: undeclared message number in the debug messages of the Galileo ephemeris decoder
    513- 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  
    28322832    <li>'stream_format': 'decoder' (RTCM2, RTCM3, ZERO, ZERO2FILE, RTNET) and 'format' (format entry of the stream),</li>
    28332833    <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>
    28372840    <li>'stream_outage_begin' (warning), 'stream_corrupted_begin' (warning): 'since' (UTC);
    28382841        'stream_outage_end', 'stream_corrupted_end': 'ended' and 'since' (UTC),</li>
  • trunk/BNC/src/ephemeris.cpp

    r11158 r11192  
    11991199  }
    12001200
    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) {
    12021205    nLines += 1;
    12031206  } else {
    … …  
    12661269      _TOC = _TOC + _gps_utc;
    12671270      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;
    12681274      if (   readDbl(line, pos[1], fieldLen, _tau)
    12691275          || readDbl(line, pos[2], fieldLen, _gamma)
    1270           || readDbl(line, pos[3], fieldLen, _tki)) {
     1276          || readDbl(line, pos[3], fieldLen, third)) {
    12711277        _checkState = bad;
    12721278        return;
    12731279      }
    1274       _tki -= nd * 86400.0;
     1280      if (type() != t_eph::L1OC && type() != t_eph::L3OC) {
     1281        _tki -= nd * 86400.0;
     1282      }
    12751283      _tau = -_tau;
    12761284    }
    … …  
    15881596  QTextStream out(&rnxStr);
    15891597
     1598  bool cdma = (type() == t_eph::L1OC || type() == t_eph::L3OC);
    15901599  out
    15911600      << QString("%1%2%3\n")
    15921601      .arg(-_tau,               19, 'e', 12)
    15931602      .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);
    15951604
    15961605  QString fmt = version < 3.0 ? "   %1%2%3%4\n" : "    %1%2%3%4\n";
    … …  
    17361745          .arg(_sat_type  ,         19, 'e', 12)
    17371746          .arg(double(sourceflags), 19, 'e', 12)
    1738           .arg(_ET,                 19, 'e', 12)
    1739           .arg(_EE,                 19, 'e', 12);
     1747          .arg(_EE,                 19, 'e', 12)  // AODE
     1748          .arg(_ET,                 19, 'e', 12); // AODC
    17401749    }
    17411750    // =====================
  • trunk/BNC/src/ephemeris.h

    r11137 r11192  
    244244    _Tin     = 0.0;
    245245    _gamma   = 0.0;
     246    _beta    = 0.0;
    246247    _tki     = 0.0;
    247248    _x_pos   = 0.0;
    … …  
    321322
    322323  double  _gamma;              // [-]
     324  double  _beta;               // clock drift rate [s/s^2] (L1OC, L3OC)
    323325  mutable double  _tki;        // message frame time
    324326
  • trunk/BNC/src/rinex/reqcedit.cpp

    r11158 r11192  
    357357        addRnxConversionDetails(obsFile, txtMap);
    358358      }
    359       outObsFile.header().write(outObsFile.stream(), &txtMap);
     359      // header with TIME OF FIRST/LAST OBS etc. of the written epochs
     360      outObsFile.writeHeaderOnClose(txtMap);
    360361    }
    361362    t_rnxObsFile::t_rnxEpo* epo = 0;
    362363    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;
    363370      while ( (epo = obsFile->nextEpoch()) != 0) {
     371        collectEvents(epo->events, pendingEvents);
     372        if (epo->flag == 1) {
     373          pendingPowerFail = true;
     374        }
    364375        if (_begTime.valid() && epo->tt < _begTime) {
    365376          continue;
    366377        }
    367378        if (_endTime.valid() && epo->tt > _endTime) {
     379          endReached = true;
    368380          break;
    369381        }
    370382
    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) {
    373387          applyLLI(obsFile, epo);
    374388          if (_minEle > 0.0) {
    375389            applyElevationMask(obsFile, epo);
    376390          }
     391          epo->events = pendingEvents;
     392          pendingEvents.clear();
     393          if (pendingPowerFail) {
     394            epo->flag = 1;
     395            pendingPowerFail = false;
     396          }
    377397          outObsFile.writeEpoch(epo);
    378398        }
    … …  
    380400          rememberLLI(obsFile, epo);
    381401        }
     402      }
     403      // events after the last epoch of the file
     404      if (!endReached) {
     405        collectEvents(obsFile->pendingEvents(), pendingEvents);
     406        outObsFile.writeEvents(pendingEvents);
    382407      }
    383408    }
    … …  
    400425      return;
    401426    }
     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////////////////////////////////////////////////////////////////////////////
     434void 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);
    402446  }
    403447}
  • trunk/BNC/src/rinex/reqcedit.h

    r11057 r11192  
    6666  void editEphemerides();
    6767  void editRnxObsHeader(t_rnxObsFile& obsFile);
     68  void collectEvents(const QList<t_rnxObsFile::t_rnxEvent>& events,
     69                     QList<t_rnxObsFile::t_rnxEvent>& pendingEvents) const;
    6870  void rememberLLI(const t_rnxObsFile* obsFile, const t_rnxObsFile::t_rnxEpo* epo);
    6971  void applyLLI(const t_rnxObsFile* obsFile, t_rnxObsFile::t_rnxEpo* epo);
  • trunk/BNC/src/rinex/rnxnavfile.cpp

    r11158 r11192  
    152152  QString navType;
    153153  QString prn;
     154  bool    ephPending = false; // RINEX 4: "> EPH" line read, data lines follow
    154155
    155156  while (stream->status() == QTextStream::Ok && !stream->atEnd()) {
    156     char sys;
     157    char sys = 0;
    157158    QString line = stream->readLine();
    158159    if (line.isEmpty()) {
    … …  
    176177      navType = "";
    177178    }
    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    }
    179190    else if (version() >= 4.0 && firstStr == ">") {
     191      ephPending = false;
     192      if (hlp.size() < 4) {
     193        continue;
     194      }
    180195      QString key = hlp.at(1);
    181196      prn = hlp.at(2);
    182197      sys = prn[0].toLatin1();
    183198      navType = hlp.at(3);
     199      ephPending = (key == "EPH");
    184200
    185201      // ALL Non-EPH messages are currently ignored
    … …  
    206222          if ((sys == 'I' && navType == "L1NV") || // I: KLOB, NEQN
    207223              (sys == 'J' && navType == "CNVX")) { // J: WIDE, JAPN
    208             QString navSubType = hlp.at(4);
     224            QString navSubType = hlp.value(4); // RINEX >= 4.02 only
    209225            if      (navSubType == "KLOB") {
    210226              lines2skip += 1;
    … …  
    244260      if (version() >= 3.05) {
    245261        nLines += 1;
     262      }
     263      if (navType == "L1OC" || navType == "L3OC") {
     264        nLines = 9;
    246265      }
    247266      for (int ii = 1; ii < nLines; ii++) {
  • trunk/BNC/src/rinex/rnxobsfile.cpp

    r11057 r11192  
    5959  _version  = 0.0;
    6060  _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;
    6168  for (unsigned iPrn = 1; iPrn <= t_prn::MAXPRN_GPS; iPrn++) {
    6269    _wlFactorsL1[iPrn] = 1;
    … …  
    93100  int numLines = 0;
    94101
     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
    95113  while ( stream->status() == QTextStream::Ok && !stream->atEnd() ) {
    96114    QString line = stream->readLine(); ++ numLines;
    … …  
    109127      QTextStream in(value.toLatin1(), QIODevice::ReadOnly);
    110128      in >> _version;
     129      _otherRecordsVersion = _version;
     130      QChar satSystem = line.length() > 40 ? line[40] : QChar(' ');
     131      _satSystem = (satSystem == ' ') ? 'G' : satSystem.toLatin1(); // blank: GPS (RINEX 2)
    111132    }
    112133    else if (key == "PGM / RUN BY / DATE") {
    … …  
    177198      QTextStream in(value.toLatin1(), QIODevice::ReadOnly);
    178199      in >> _antXYZ[0] >> _antXYZ[1] >> _antXYZ[2];
     200      if (fullHeader) {
     201        _otherRecords << line;
     202      }
    179203    }
    180204    else if (key == "ANTENNA: B.SIGHT XYZ") {
    181205      QTextStream in(value.toLatin1(), QIODevice::ReadOnly);
    182206      in >> _antBSG[0] >> _antBSG[1] >> _antBSG[2];
     207      if (fullHeader) {
     208        _otherRecords << line;
     209      }
    183210    }
    184211    else if (key == "DOI") {
    185212      _digitalObjectId = line.mid(0,60).trimmed();
     213      if (fullHeader) {
     214        _otherRecords << line;
     215      }
    186216    }
    187217    else if (key == "LICENSE OF USE") {
    188218      _licenseOfUse.append(line.mid(0,60).trimmed());
     219      if (fullHeader) {
     220        _otherRecords << line;
     221      }
    189222    }
    190223    else if (key == "STATION INFORMATION") {
    191224     _stationInformation.append(line.mid(0,60).trimmed());
     225      if (fullHeader) {
     226        _otherRecords << line;
     227      }
    192228    }
    193229    else if (key == "# / TYPES OF OBSERV") {
    … …  
    245281      in >> year >> month >> day >> hour >> min >> sec;
    246282      _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      }
    247322    }
    248323    else if (key == "SYS / PHASE SHIFT"){
    … …  
    298373      }
    299374      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;
    300380    }
    301381    if (maxLines > 0 && numLines == maxLines) {
    … …  
    412492  _receiverVersion = header._receiverVersion;
    413493  _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;
    414503  _usedSystems     = header._usedSystems;
    415504  _comments        = header._comments;
    … …  
    638727  }
    639728
    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")
    641741    .arg(_version, 9, 'f', 2)
     742    .arg(QChar(satSystem))
    642743    .leftJustified(60)
    643744           << "RINEX VERSION / TYPE\n";
    … …  
    720821           << "ANTENNA: DELTA H/E/N\n";
    721822
     823  if (_rcvClkOffsAppl >= 0) {
     824    *stream << QString("%1")
     825      .arg(_rcvClkOffsAppl, 6)
     826      .leftJustified(60)
     827             << "RCV CLOCK OFFS APPL\n";
     828  }
     829
    722830  if (_version < 3.0) {
    723831    int defaultWlFact1 = _wlFactorsL1[1];
    … …  
    731839
    732840  *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  }
    733877
    734878  if (_interval > 0) {
    … …  
    750894    .arg(min, 6)
    751895    .arg(sec, 13, 'f', 7)
    752     .arg("GPS", 8)
     896    .arg(_timeSystem, 8)
    753897    .leftJustified(60)
    754898           << "TIME OF FIRST OBS\n";
    755899
    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) {
    757919    if (_phaseShifts.empty()) {
    758920      QMap<char, QStringList>::const_iterator it;
    … …  
    8751037  }
    8761038
     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
    8771069  *stream << QString()
    8781070    .leftJustified(60)
    8791071           << "END OF HEADER\n";
     1072}
     1073
     1074// Scale factor of an observation type (SYS / SCALE FACTOR, RINEX >= 3)
     1075////////////////////////////////////////////////////////////////////////////
     1076int 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////////////////////////////////////////////////////////////////////////////
     1091QString 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;
    8801147}
    8811148
    … …  
    10411308  _inpOut       = inpOut;
    10421309  _stream       = 0;
     1310  _file         = 0;
    10431311  _flgPowerFail = false;
     1312  _epoFile      = 0;
     1313  _epoStream    = 0;
    10441314  if (_inpOut == input) {
    10451315    openRead(fileName);
    … …  
    10811351    _stream->seek(0);
    10821352    _header.read(_stream);
     1353    _pendingEvents.clear();
     1354    _flgPowerFail = false;
    10831355  }
    10841356
    … …  
    10931365    _stream->seek(0);
    10941366    _header.read(_stream);
     1367    _pendingEvents.clear();
     1368    _flgPowerFail = false;
    10951369  }
    10961370}
    … …  
    11161390////////////////////////////////////////////////////////////////////////////
    11171391void t_rnxObsFile::close() {
     1392  if (_epoStream) {
     1393    writeDeferredHeader();
     1394  }
    11181395  delete _stream; _stream = 0;
    11191396  delete _file;   _file = 0;
    11201397}
    11211398
    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////////////////////////////////////////////////////////////////////////////
     1403void 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////////////////////////////////////////////////////////////////////////////
     1419void 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////////////////////////////////////////////////////////////////////////////
     1446void 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);
    11471484    }
    11481485    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();
    11501495    }
    11511496    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////////////////////////////////////////////////////////////////////////////
     1508QList<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////////////////////////////////////////////////////////////////////////////
     1516void 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////////////////////////////////////////////////////////////////////////////
     1548void 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    }
    11661603  }
    11671604}
    … …  
    11931630    int flag = 0;
    11941631    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;
    12021640
    12031641    QTextStream in(line.mid(1).toLatin1(), QIODevice::ReadOnly);
    … …  
    12151653    readInt(line, 32, 3, numSat);
    12161654
    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    }
    12181670
    12191671    // Observations
    12201672    // ------------
    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);
    12461674
    12471675    _flgPowerFail = false;
     1676
     1677    _currEpo.events = _pendingEvents;
     1678    _pendingEvents.clear();
    12481679
    12491680    return &_currEpo;
    … …  
    12671698    int flag = 0;
    12681699    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;
    12761708
    12771709    QTextStream in(line.toLatin1(), QIODevice::ReadOnly);
    … …  
    12951727    readInt(line, 29, 3, numSat);
    12961728
    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);
    13511741
    13521742    _flgPowerFail = false;
     1743
     1744    _currEpo.events = _pendingEvents;
     1745    _pendingEvents.clear();
    13531746
    13541747    return &_currEpo;
    … …  
    13651758  }
    13661759  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////////////////////////////////////////////////////////////////////////////
     1786std::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.
    13721793          if (rnxSat.prn.system() != 'G' && rnxSat.prn.system() != 'R' &&
    13731794              rnxSat.prn.system() != 'E' && rnxSat.prn.system() != 'S' &&
    … …  
    13761797          }
    13771798      }
    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////////////////////////////////////////////////////////////////////////////
     1809static 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////////////////////////////////////////////////////////////////////////////
     1827static 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////////////////////////////////////////////////////////////////////////////
     1843void 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    }
    13881899  }
    13891900}
    … …  
    13921903////////////////////////////////////////////////////////////////////////////
    13931904void 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);
    14121911  for (unsigned iSat = 0; iSat < epo->rnxSat.size(); iSat++) {
    14131912    const t_rnxSat& rnxSat = epo->rnxSat[iSat];
    14141913    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;
    14201928
    14211929  for (unsigned iSat = 0; iSat < epo->rnxSat.size(); iSat++) {
    14221930    const t_rnxSat& rnxSat = epo->rnxSat[iSat];
    14231931    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    }
    14241937    for (int iTypeV2 = 0; iTypeV2 < header.nTypes(sys); iTypeV2++) {
    14251938      if (iTypeV2 > 0 && iTypeV2 % 5 == 0) {
    … …  
    14581971              else {
    14591972                *stream << QString("%1").arg(rnxObs.value, 14, 'f', 3);
     1973                if (counts) {
     1974                  (*counts)[iTypeV2]++;
     1975                }
    14601976                if (rnxObs.lli != 0.0) {
    14611977                  *stream << QString("%1").arg(rnxObs.lli,1);
    … …  
    14872003////////////////////////////////////////////////////////////////////////////
    14882004void 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";
    15072029
    15082030  for (unsigned iSat = 0; iSat < epo->rnxSat.size(); iSat++) {
    … …  
    15612083    if (header.nTypes(sys)) {
    15622084      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      }
    15632090      for (int iTypeV3 = 0; iTypeV3 < header.nTypes(sys); iTypeV3++) {
    15642091        const t_rnxObs* rnxObs = hlp[iTypeV3];
    … …  
    15672094        }
    15682095        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          }
    15702102          if (rnxObs->lli != 0.0) {
    15712103            record += QString("%1").arg(rnxObs->lli, 1);
  • trunk/BNC/src/rinex/rnxobsfile.h

    r11023 r11192  
    7373  void        write(QTextStream* stream, const QMap<QString, QString>* txtMap = 0) const;
    7474  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);
    7578  void        setStartTime(const bncTime& startTime) {_startTime = startTime;}
    7679
    … …  
    107110  int                     _wlFactorsL2[t_prn::MAXPRN_GPS+1];
    108111  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
    109126  bool                    _writeRinexOnlyWithSklObsTypes;
    110127
    … …  
    136153  };
    137154
     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
    138168  class t_rnxEpo {
    139169   public:
    … …  
    142172      tt.reset();
    143173      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)
    146181    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
    147189  };
    148190
    … …  
    233275    }
    234276    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;
    236282    for (unsigned ii = 0; ii < epo->rnxSat.size(); ii++) {
    237283      const t_rnxSat& rnxSat = epo->rnxSat[ii];
    … …  
    248294    std::stable_sort(epoLocal.rnxSat.begin(), epoLocal.rnxSat.end(), t_rnxSat::prnSort);
    249295
     296    writeEvents(stream, header, epo->events);
    250297    if (header.version() >= 3.0) {
    251298      writeEpochV3(stream, header, &epoLocal);
    … …  
    256303  }
    257304
     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
    258319 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);
    261325  t_rnxObsFile() {};
    262326  void openRead(const QString& fileName);
    … …  
    265329  t_rnxEpo* nextEpochV2();
    266330  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();
    268337
    269338  e_inpOut       _inpOut;
    … …  
    274343  t_rnxEpo       _currEpo;
    275344  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;
    276353};
    277354
  • trunk/BNC/tests/README.md

    r11187 r11192  
    2626| `cases/<name>/expected/*.gz` | normalised reference outputs |
    2727| `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) |
    2830| `tools/fakecaster.py` | local Ntrip caster recording an upload as a BNC raw file (case key `fakecaster`); optionally TLS and serving a stream |
    2931| `tools/bncraw.py` | `info` (streams, time span, RTCM3 message types), `slice` and `crs` (decode 1300/1301/1302) for raw captures |
    … …  
    4951* `rinex_edit_minele`: Edit/Concatenate with a 10 deg elevation mask (1 h BRUX
    5052  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.
    5164* Stream replays of the real-time examples (`replay_*`), all from the 5 min
    5265  slice `data/wtzr_cnes_5min.raw` (BCEP00BKG0, WTZR00DEU0, SSRA00CNE1):
Note: See TracChangeset for help on using the changeset viewer.