Index: /trunk/BNC/src/bncantex.cpp
===================================================================
--- /trunk/BNC/src/bncantex.cpp	(revision 11078)
+++ /trunk/BNC/src/bncantex.cpp	(revision 11079)
@@ -778,5 +778,9 @@
                                  const ColumnVector& xSat,
                                  const ColumnVector& vSat, ColumnVector& dx,
-                                 e_attMode mode, double externalYaw) {
+                                 e_attMode mode, double externalYaw,
+                                 const QString& stateKey) {
+
+  // separate yaw model states per caller (stateKey), each called in time order
+  QString yawKey = stateKey.isEmpty() ? prn : stateKey + ":" + prn;
 
   t_frequency::type frqType = t_frequency::dummy;
@@ -836,20 +840,20 @@
 
       if (mode == ATT_COMPUTED && prn[0] == 'R' && vSat.size() == 3) {
-        psiEff = glonassYawAngle(prn, Mjd, xSat, vSat, xSun);
+        psiEff = glonassYawAngle(yawKey, Mjd, xSat, vSat, xSun);
         useVelocityFrame = true;
       }
       else if (mode == ATT_COMPUTED && prn[0] == 'G' && vSat.size() == 3
                && !map->blockType.isEmpty()) {
-        psiEff = gpsYawAngle(prn, map->blockType, Mjd, xSat, vSat, xSun);
+        psiEff = gpsYawAngle(yawKey, map->blockType, Mjd, xSat, vSat, xSun);
         useVelocityFrame = true;
       }
       else if (mode == ATT_COMPUTED && prn[0] == 'E' && vSat.size() == 3
                && !map->blockType.isEmpty()) {
-        psiEff = galileoYawAngle(prn, map->blockType, Mjd, xSat, vSat, xSun);
+        psiEff = galileoYawAngle(yawKey, map->blockType, Mjd, xSat, vSat, xSun);
         useVelocityFrame = true;
       }
       else if (mode == ATT_COMPUTED && prn[0] == 'C' && vSat.size() == 3
                && !map->blockType.isEmpty()) {
-        psiEff = bdsYawAngle(prn, map->blockType, Mjd, xSat, vSat, xSun);
+        psiEff = bdsYawAngle(yawKey, map->blockType, Mjd, xSat, vSat, xSun);
         useVelocityFrame = true;
       }
Index: /trunk/BNC/src/bncantex.h
===================================================================
--- /trunk/BNC/src/bncantex.h	(revision 11078)
+++ /trunk/BNC/src/bncantex.h	(revision 11079)
@@ -66,5 +66,6 @@
                            ColumnVector& dx,
                            e_attMode mode = ATT_COMPUTED,
-                           double externalYaw = 0.0);
+                           double externalYaw = 0.0,
+                           const QString& stateKey = QString());
 
   // Drain and return diagnostic logs accumulated by the yaw models.
Index: /trunk/BNC/src/bnchelp.html
===================================================================
--- /trunk/BNC/src/bnchelp.html	(revision 11078)
+++ /trunk/BNC/src/bnchelp.html	(revision 11079)
@@ -6323,4 +6323,10 @@
   </p>
   <p>
+    Whether the orbit corrections refer to the satellite's Antenna Phase Center (APC) or to its Center of Mass (CoM) is
+    derived from the name of the correction stream or file: names beginning with 'SSRA' refer to the APC, the combined
+    corrections 'INTERNAL' always refer to the APC as well; all others are taken as CoM. For APC corrections, only the
+    differences of the satellite antenna offsets to that of the reference frequency are applied.
+  </p>
+  <p>
     Default value for 'ANTEX file' is an empty option field, meaning that you do not want to correct observations for
     Antenna Phase Center offsets and variations.
@@ -8096,4 +8102,17 @@
     - despite the fact, that the delivered code biases of an AC may contain contributions from other biases, also phase
     biases.
+  </p>
+  <p>
+    <b>APC and CoM streams:</b> The orbits of Broadcast Correction streams refer either to the satellite's Antenna Phase
+    Center (APC, stream names 'SSRA...', reference frequency of the IGS-SSR and RTCM-SSR conventions) or to its Center
+    of Mass (CoM, 'SSRC...'). If streams of both kinds are combined and an ANTEX file is specified ('Upload
+    Corrections' panel), the combination is done in the CoM frame: the orbits of the APC streams are converted to CoM
+    with the satellite antenna offsets of the ANTEX file and the attitude model selected for the AC ('Computed', 'SSR'
+    or 'Nominal'). The result is converted to the APC for upload streams referring to the APC and for the combined
+    corrections passed on to BNC's PPP ('INTERNAL'), which always refer to the APC. As most ACs provide
+    both kinds of streams, combining CoM streams is recommended: the combined SP3 and CoM results then do not depend on
+    BNC's attitude model and ANTEX file, only the uploaded APC streams do. Without an ANTEX file, or if all streams
+    are of one kind, the combination is done in the frame of the master AC; streams of the other kind are not
+    combined, which is reported in the logfile at most once per hour per AC and system.
   </p>
   <p>
Index: /trunk/BNC/src/combination/bnccomb.cpp
===================================================================
--- /trunk/BNC/src/combination/bnccomb.cpp	(revision 11078)
+++ /trunk/BNC/src/combination/bnccomb.cpp	(revision 11079)
@@ -296,4 +296,31 @@
   }
 
+  // Reference point of the combination: with ACs of both kinds (APC streams
+  // 'SSRA...', CoM streams 'SSRC...') and an ANTEX file, the combination is
+  // done in the CoM frame and the orbits of the APC ACs are converted to
+  // CoM; otherwise in the frame of the master AC (ACs of the other kind are
+  // not used)
+  // ------------------------------------------------------------------------
+  bool hasAPC = false, hasCoM = false;
+  for (const cmbAC* AC : _ACs) {
+    if (AC->isAPC) hasAPC = true; else hasCoM = true;
+  }
+  _cmbCoM = (_antex && hasAPC && hasCoM);
+  if (_cmbCoM) {
+    for (QMap<char, bool>::iterator it = _masterIsAPC.begin(); it != _masterIsAPC.end(); ++it) {
+      it.value() = false;
+    }
+    emit newMessage("bncComb: APC and CoM streams configured - combination in the CoM frame, "
+                    "orbits of the APC streams converted to CoM", true);
+  }
+  else if (hasAPC && hasCoM) {
+    emit newMessage("bncComb: APC and CoM streams configured, but no ANTEX file - only the streams "
+                    "with the reference point of the master AC are combined", true);
+  }
+  if (!_antex && hasCoM) {
+    emit newMessage("bncComb: CoM streams configured, but no ANTEX file - the combined orbits (INTERNAL) "
+                    "may refer to the CoM, while the PPP assumes the APC", true);
+  }
+
 
   // Bias SINEX File
@@ -610,7 +637,16 @@
       }
     }
-    if (acName.isEmpty() ||
-        isAPC != _masterIsAPC[sys] ||
-        excludeSat(newClk._prn, excludeSats)) {
+    if (acName.isEmpty() || excludeSat(newClk._prn, excludeSats)) {
+      continue;
+    }
+    if (!_cmbCoM && isAPC != _masterIsAPC[sys]) {
+      bncTime& warned = _refPointWarned[acName + sys];
+      if (!warned.valid() || newClk._time - warned >= 3600.0) {
+        warned = newClk._time;
+        emit newMessage("bncComb: " + acName.toLatin1() + " " + QByteArray(1, sys) + " not combined: its orbits refer to the " +
+                        (isAPC ? "APC" : "CoM") + ", those of the master AC " + _masterOrbitAC[sys].toLatin1() +
+                        " to the " + (isAPC ? "CoM" : "APC") +
+                        " (an ANTEX file allows to combine both in the CoM frame)", true);
+      }
       continue;
     }
@@ -949,4 +985,20 @@
     }
 
+    // Orbits of an APC AC in a combination in the CoM frame
+    // ------------------------------------------------------
+    if (_cmbCoM) {
+      const cmbAC* AC = 0;
+      for (const cmbAC* ac : _ACs) {
+        if (ac->name == acName) {
+          AC = ac;
+          break;
+        }
+      }
+      if (AC && AC->isAPC && convertApcToCoM(_newCorr, AC) != success) {
+        delete _newCorr; _newCorr = 0;
+        continue;
+      }
+    }
+
     // Store correction into the buffer
     // --------------------------------
@@ -1047,5 +1099,5 @@
               << AC->name.toLatin1().data()   << "\n";
           _masterOrbitAC[sys] = AC->name;
-          _masterIsAPC[sys]   = AC->isAPC;
+          _masterIsAPC[sys]   = _cmbCoM ? false : AC->isAPC;
           break;
         }
@@ -1320,4 +1372,43 @@
 }
 
+// Convert the orbit correction of an APC AC to the satellite's CoM: the
+// antenna offset (ANTEX, single reference frequency, attitude as configured
+// for the AC) is added to the correction in radial, along-track and
+// cross-track direction. The clock correction is not affected.
+////////////////////////////////////////////////////////////////////////////
+t_irc bncComb::convertApcToCoM(cmbCorr* corr, const cmbAC* AC) {
+  corr->_eph->setOrbCorr(&corr->_orbCorr);
+  corr->_eph->setClkCorr(&corr->_clkCorr);
+  ColumnVector xc(6), vv(3);
+  if (corr->_eph->getCrd(corr->_time, xc, vv, true) != success) {
+    return failure;
+  }
+  bncAntex::e_attMode attMode = bncAntex::ATT_COMPUTED;
+  double              extYaw  = 0.0;
+  if (AC->attitudeSource == "SSR" && corr->_satYawAngleValid) {
+    attMode = bncAntex::ATT_EXTERNAL;
+    extYaw  = corr->_satYawAngle;
+  }
+  else if (AC->attitudeSource == "Nominal") {
+    attMode = bncAntex::ATT_NOMINAL;
+  }
+  double Mjd = corr->_time.mjd() + corr->_time.daysec() / 86400.0;
+  ColumnVector dx(3); dx = 0.0;
+  if (_antex->satCoMcorrection(corr->_prn, Mjd, xc.Rows(1,3), vv, dx, attMode, extYaw, AC->name) != success) {
+    if (_antex->warnNoSatEntry(corr->_prn, corr->_time)) {
+      emit newMessage("bncComb: no satellite antenna entry valid at " +
+                      QByteArray(corr->_time.datestr().c_str()) + " " + QByteArray(corr->_time.timestr().c_str()) +
+                      " for " + corr->_prn.mid(0,3).toLatin1() +
+                      " in ANTEX file - APC orbit of " + AC->name.toLatin1() + " not converted to CoM, not combined", true);
+    }
+    return failure;
+  }
+  // X = X_brdc - RSW(xr): X_CoM = X_APC - dx  =>  xr_CoM = xr_APC + RSW(dx)
+  ColumnVector dRAO(3);
+  XYZ_to_RSW(xc.Rows(1,3), vv, dx, dRAO);
+  corr->_orbCorr._xr += dRAO;
+  return success;
+}
+
 // Report AC satellites rejected as outliers in most epochs
 ////////////////////////////////////////////////////////////////////////////
@@ -1544,4 +1635,12 @@
       apc(2) = xc(2)+dx(2);
       apc(3) = xc(3)+dx(3);
+    }
+
+    // The combined corrections passed on to the PPP (INTERNAL) always refer
+    // to the APC: orbits combined in the CoM frame are converted
+    if (!masterIsAPC && _antex) {
+      ColumnVector dRAO(3);
+      XYZ_to_RSW(xc.Rows(1,3), vv, dx, dRAO);
+      orbCorrections.back()._xr -= dRAO;
     }
 
Index: /trunk/BNC/src/combination/bnccomb.h
===================================================================
--- /trunk/BNC/src/combination/bnccomb.h	(revision 11078)
+++ /trunk/BNC/src/combination/bnccomb.h	(revision 11079)
@@ -280,4 +280,7 @@
   void checkBiasConsistency(bncTime epoTime, char sys, QTextStream& out);
   void checkPersistentOutliers(bncTime epoTime, char sys, QTextStream& out);
+  t_irc convertApcToCoM(cmbCorr* corr, const cmbAC* AC);
+  bool                                       _cmbCoM;          // combination in the CoM frame, APC ACs converted
+  QMap<QString, bncTime>                     _refPointWarned;  // last 'APC/CoM differs from master' warning per AC
   struct t_outlierStat {
     t_outlierStat() : numEpo(0) {}
Index: /trunk/BNC/src/pppMain.cpp
===================================================================
--- /trunk/BNC/src/pppMain.cpp	(revision 11078)
+++ /trunk/BNC/src/pppMain.cpp	(revision 11079)
@@ -213,5 +213,6 @@
     if (_realTime) {
       opt->_corrMount.assign(settings.value("PPP/corrMount").toString().toStdString());
-      opt->_isAPC = (opt->_corrMount.substr(0,4)=="SSRA");
+      // the combined corrections (INTERNAL) always refer to the APC
+      opt->_isAPC = (opt->_corrMount.substr(0,4)=="SSRA" || opt->_corrMount == "INTERNAL");
       opt->_biasMount.assign(settings.value("PPP/biasMount").toString().toStdString());
       opt->_ionoMount.assign(settings.value("PPP/ionoMount").toString().toStdString());
Index: /trunk/BNC/src/src.pri
===================================================================
--- /trunk/BNC/src/src.pri	(revision 11078)
+++ /trunk/BNC/src/src.pri	(revision 11079)
@@ -47,5 +47,5 @@
               -lnewmat -lqwtpolar -lqwt
 
-HEADERS = bnchelp.html bncgetthread.h    bncwindow.h   bnctabledlg.h  \
+HEADERS = bnchelp.html bncgetthread.h ssrQc.h    bncwindow.h   bnctabledlg.h  \
           bnccaster.h bncrinex.h bnccore.h bncutils.h   bnchlpdlg.h   \
           bncconst.h bnchtml.h bnctableitem.h bnczerodecoder.h        \
@@ -89,5 +89,5 @@
 win32:HEADERS += serial/win_qextserialport.h
 
-SOURCES = bncgetthread.cpp  bncwindow.cpp bnctabledlg.cpp             \
+SOURCES = bncgetthread.cpp ssrQc.cpp  bncwindow.cpp bnctabledlg.cpp             \
           bnccaster.cpp bncrinex.cpp bnccore.cpp bncutils.cpp         \
           bncconst.cpp bnchtml.cpp bnchlpdlg.cpp bnctableitem.cpp     \
Index: /trunk/BNC/src/ssrQc.cpp
===================================================================
--- /trunk/BNC/src/ssrQc.cpp	(revision 11078)
+++ /trunk/BNC/src/ssrQc.cpp	(revision 11079)
@@ -106,9 +106,4 @@
   _dirName   = dirName;
   _reference = reference.isEmpty() ? QString("CONSENSUS") : reference;
-  bncSettings settings;
-  QStringList cmbStreams = settings.value("cmbStreams").toStringList();
-  if (!cmbStreams.isEmpty()) {
-    _cmbMaster = cmbStreams[0].split(' ', Qt::SkipEmptyParts).value(0);
-  }
   _interval = interval > 0.0 ? interval : 3600.0;
   for (const QString& sta : streams) {
@@ -398,9 +393,11 @@
 
 // Reference point of the orbits of a stream: APC (SSRA...), CoM (SSRC...);
-// the combined stream INTERNAL refers to that of its master AC (the first
-// AC of the combination)
+// the combined stream INTERNAL refers to the APC
 ////////////////////////////////////////////////////////////////////////////
 char t_ssrQc::refPoint(const QString& staID) const {
-  QString sta = (staID == "INTERNAL") ? _cmbMaster : staID;
+  if (staID == "INTERNAL") {
+    return 'A';  // the combined corrections always refer to the APC
+  }
+  const QString& sta = staID;
   if (sta.startsWith("SSRA")) return 'A';
   if (sta.startsWith("SSRC")) return 'C';
Index: /trunk/BNC/src/ssrQc.h
===================================================================
--- /trunk/BNC/src/ssrQc.h	(revision 11078)
+++ /trunk/BNC/src/ssrQc.h	(revision 11079)
@@ -107,5 +107,4 @@
   char   refPoint(const QString& staID) const;
   QString                                      _reference;   // "CONSENSUS" or stream name
-  QString                                      _cmbMaster;   // mountpoint of the combination's first AC
   bncTime                                      _nextCmpEpoch;
   QMap<QString, QMap<QString, QMap<double, t_orbCorr> > > _orbHist; // stream, PRN (internal), time
