Index: trunk/BNC/src/PPP/pppSatObs.cpp
===================================================================
--- trunk/BNC/src/PPP/pppSatObs.cpp	(revision 11042)
+++ trunk/BNC/src/PPP/pppSatObs.cpp	(revision 11047)
@@ -400,13 +400,44 @@
 
 
-// A system's Satellite Antenna message is only trustworthy once the
-// provider has confirmed it via Metadata: SatelliteAntennaIOD (DF+010) must
-// be non-zero, and must match the Data IOD (DF+069) of a Metadata entry for
-// model-correction type 1 (satellite antenna PCV) or type 2 (GDV) - either
-// entry matching is sufficient. Metadata carries this IOD once globally, not
-// per system (DF+010 is GNSS-specific, DF+069 is not), so the same Metadata
-// entries are reused as the trust anchor for every system's check - the
-// provider is relied on to keep DF+010 synchronized with DF+069 across all
-// systems for this to work.
+// Satellite Antenna IOD referenced by the SSR Metadata: the Data IOD (DF+069)
+// of the model-correction entry of type 1 (satellite antenna PCV) or type 2
+// (GDV). refIOD = 0 if Metadata references no antenna information (no
+// Metadata, no such entry, no Data IOD, or a zero Data IOD). A provider's
+// solution is relative to one Satellite Antenna information only, so two
+// different non-zero IODs are inconsistent: returns false in that case.
+//
+// A non-zero reference means the provider's biases, and orbits/clocks, are
+// relative to exactly that Satellite Antenna information, so the client
+// must apply it and nothing else (see t_pppSatObs::cmpModel()).
+////////////////////////////////////////////////////////////////////////////
+static bool ssrSatAntennaRefIOD(const t_metaData* metaData, unsigned int& refIOD) {
+  refIOD = 0;
+  if (!metaData) {
+    return true;
+  }
+  for (unsigned ii = 0; ii < metaData->_entries.size(); ii++) {
+    const t_metaDataEntry& entry = metaData->_entries[ii];
+    if ((entry._typeIndicator == 1 || entry._typeIndicator == 2) && // PCV or GDV
+        entry._dataIODIndicator && entry._dataIOD != 0) {
+      if (refIOD != 0 && refIOD != entry._dataIOD) {
+        return false;
+      }
+      refIOD = entry._dataIOD;
+    }
+  }
+  return true;
+}
+
+// Whether cached Satellite Antenna data is the information referenced by
+// Metadata (refIOD, from ssrSatAntennaRefIOD()). SatelliteAntennaIOD (DF+010)
+// must be non-zero and equal refIOD. Metadata carries this IOD once globally,
+// not per system (DF+010 is GNSS-specific, DF+069 is not), so the same
+// Metadata entries are reused for every system's check - the provider is
+// relied on to keep DF+010 synchronized with DF+069 across all systems.
+//
+// The IOD is also unique only per SSR Provider ID (DF414), so the Metadata
+// must come from the same provider as the Antenna message - antenna data may
+// be taken from the bias mountpoint while Metadata comes from the
+// corrections mountpoint (see pppRun.cpp).
 //
 // Separately, providers only guarantee SatelliteAntennaIOD uniqueness within
@@ -414,12 +445,16 @@
 // has been without a fresh Antenna message for that system for 64 days or
 // more must treat any cached data as unverifiable and stop using it, even
-// if the IOD/Metadata check above would otherwise pass - the same IOD value
-// could by then legitimately mean something else. epoTime is the current
-// processing epoch, compared against t_satAntenna::_time (when this data
-// was actually decoded, not a wire epoch - the message carries none).
-////////////////////////////////////////////////////////////////////////////
-static bool ssrSatAntennaTrusted(const t_satAntenna* satAntenna, const bncTime& epoTime) {
+// if the IOD check above would otherwise pass - the same IOD value could by
+// then legitimately mean something else. epoTime is the current processing
+// epoch, compared against t_satAntenna::_time (when this data was last
+// received, to within the decoder's re-emit interval - not a wire epoch,
+// the message carries none; see RTCM3coDecoder::sendResults()).
+////////////////////////////////////////////////////////////////////////////
+static bool ssrSatAntennaTrusted(const t_satAntenna* satAntenna, const t_metaData* metaData,
+                                 unsigned int refIOD, const bncTime& epoTime) {
   const double MAX_AGE_SEC = 64.0 * 86400.0;
-  if (!satAntenna || satAntenna->_satelliteAntennaIOD == 0) {
+  if (!satAntenna || !metaData || refIOD == 0 ||
+      satAntenna->_satelliteAntennaIOD != refIOD ||
+      satAntenna->_ssrProviderID != metaData->_providerID) {
     return false;
   }
@@ -428,24 +463,11 @@
     return false;
   }
-  const t_metaData* metaData = PPP_CLIENT->obsPool()->metaData();
-  if (!metaData) {
-    return false;
-  }
-  for (unsigned ii = 0; ii < metaData->_entries.size(); ii++) {
-    const t_metaDataEntry& entry = metaData->_entries[ii];
-    if ((entry._typeIndicator == 1 || entry._typeIndicator == 2) && // PCV or GDV
-        entry._dataIODIndicator &&
-        entry._dataIOD == satAntenna->_satelliteAntennaIOD) {
-      return true;
-    }
-  }
-  return false;
+  return true;
 }
 
 // Satellite antenna correction (offset + nadir-angle-dependent PCV) from a
 // live SSR Satellite Antenna message, in the same additive sense as
-// bncAntex::satCorr(). Returns false (corr untouched) if no trusted SSR data
-// is available for this PRN/frequency (see ssrSatAntennaTrusted), so the
-// caller falls back to the static ANTEX file.
+// bncAntex::satCorr(). Returns false (corr untouched) if the data contains
+// no entry for this frequency.
 ////////////////////////////////////////////////////////////////////////////
 static bool ssrSatAntennaCorr(const t_satAntenna* satAntenna, const string& frqStr,
@@ -472,4 +494,7 @@
       else if (idx >= (int)frq._nadirAngleCorrection.size()) {
         idx = (int)frq._nadirAngleCorrection.size() - 1;
+      }
+      if (std::isnan(frq._nadirAngleCorrection[idx])) {
+        return false; // invalid value (DF+021 = -2^(n-1)) at this nadir angle
       }
       corr += frq._nadirAngleCorrection[idx];
@@ -555,9 +580,35 @@
   // Antenna Phase Center Offsets and Variations
   // -------------------------------------------
+  // If SSR Metadata references Satellite Antenna information (non-zero IOD),
+  // the provider's biases are relative to exactly that information, so it
+  // is the only satellite antenna model applied - never mixed with ANTEX:
+  // phaseranges get the SSR correction if the Phase Center indicator
+  // (DF+011) is set, otherwise zero; pseudoranges likewise with the Group
+  // Delay indicator (DF+012). Zero values are part of that information
+  // (explicitly encoded, or DF+019/DF+013 not set); omitting a satellite or
+  // frequency within a constellation is not a valid way to signal zero. So
+  // if the referenced information is not (yet) available for this satellite,
+  // or lacks a frequency the PPP linear combinations need, the satellite is
+  // excluded rather than corrected with ANTEX. Without such a reference,
+  // the static ANTEX file is used for both observables.
   if (PPP_CLIENT->antex()) {
+    const t_metaData*   metaData   = PPP_CLIENT->obsPool()->metaData();
     const t_satAntenna* satAntenna = PPP_CLIENT->obsPool()->satAntenna(_prn);
-    if (!ssrSatAntennaTrusted(satAntenna, _time)) {
-      satAntenna = 0; // untrusted (IOD zero, or not confirmed via Metadata) - fall back to ANTEX
-    }
+    unsigned int refIOD;
+    if (!ssrSatAntennaRefIOD(metaData, refIOD)) {
+      LOG << "SSR Metadata references different Satellite Antenna IODs (PCV/GDV) - "
+          << _prn.toString() << " excluded" << endl;
+      return failure;
+    }
+    bool useSsr = (refIOD != 0);
+    if (useSsr && !ssrSatAntennaTrusted(satAntenna, metaData, refIOD, _time)) {
+      LOG << "SSR Satellite Antenna IOD " << refIOD << " referenced by Metadata not available for "
+          << _prn.toString() << " - satellite excluded" << endl;
+      return failure;
+    }
+
+    bool phaseMissing[t_frequency::max] = {};
+    bool codeMissing[t_frequency::max]  = {};
+
     for (unsigned ii = 0; ii < t_frequency::max; ii++) {
       t_frequency::type frqType = static_cast<t_frequency::type>(ii);
@@ -568,21 +619,54 @@
       _model._antPCO[ii] = PPP_CLIENT->antex()->rcvCorr(station->antName(), frqType, _model._eleSat, _model._azSat, found);
 
-      // Prefer a live, trusted SSR Satellite Antenna correction over the
-      // static ANTEX file when available for this satellite/frequency,
-      // mirroring how SSR orbit/clock corrections already take precedence
-      // over broadcast ephemerides elsewhere in this codebase. Unlike
-      // ANTEX's satCorr() (a single geometric PCO/PCV value shared by code
-      // and phase alike), the SSR message's DF+011/DF+012 indicators say
-      // explicitly which observable(s) the correction applies to, so it is
-      // routed into the dedicated phase-only/code-only fields instead of
-      // the shared _antPCO - both may fire for the same ssrCorr value if
-      // both indicators are set.
-      double ssrCorr;
-      if (ssrSatAntennaCorr(satAntenna, frqStr, _model._elTx, ssrCorr)) {
-        if (satAntenna->_phaseCenterInfoInd) {
-          _model._antPCV[ii] += ssrCorr;
-        }
-        if (satAntenna->_groupDelayInfoInd) {
-          _model._antGDV[ii] += ssrCorr;
+      if (useSsr) {
+        // Unlike ANTEX's satCorr() (a single geometric PCO/PCV value shared
+        // by code and phase alike), DF+011/DF+012 say explicitly which
+        // observable(s) the correction applies to, so it is routed into the
+        // dedicated phase-only/code-only fields instead of the shared _antPCO.
+        // BDS 1207.14 MHz: B2 (BDS-2, "C7") or B2b (BDS-3, "C7b") - a
+        // satellite transmits only one of them. The entry of the satellite's
+        // own generation is preferred, taken from the ANTEX block type valid
+        // at this epoch (PRNs have been reassigned from BDS-2 to BDS-3).
+        // Since antenna corrections depend on the carrier, not the signal,
+        // the other entry is used as fallback if the own one is missing; if
+        // the generation is unknown, an entry is used only if unambiguous
+        // (just one present, or both equal).
+        // PRELIMINARY - pending clarification by RTCM SC-104 whether B2 and
+        // B2b entries of one satellite are meant to be identical; to revert
+        // to the strict selection, drop the fallback branches below.
+        double ssrCorr = 0.0;
+        bool   ssrOK   = false;
+        if (frqStr == "C7") {
+          double corrB2 = 0.0, corrB2b = 0.0;
+          bool   hasB2  = ssrSatAntennaCorr(satAntenna, "C7",  _model._elTx, corrB2);
+          bool   hasB2b = ssrSatAntennaCorr(satAntenna, "C7b", _model._elTx, corrB2b);
+          QString blockType = PPP_CLIENT->antex()->satBlockType(prn, _time);
+          if      (blockType.startsWith("3")) {       // BDS-3: B2b, fallback B2
+            ssrOK = hasB2b || hasB2;
+            ssrCorr = hasB2b ? corrB2b : corrB2;
+          }
+          else if (blockType.startsWith("2")) {       // BDS-2: B2, fallback B2b
+            ssrOK = hasB2 || hasB2b;
+            ssrCorr = hasB2 ? corrB2 : corrB2b;
+          }
+          else if (hasB2 != hasB2b || (hasB2 && corrB2 == corrB2b)) { // generation unknown
+            ssrOK = true;
+            ssrCorr = hasB2 ? corrB2 : corrB2b;
+          }
+        }
+        else {
+          ssrOK = ssrSatAntennaCorr(satAntenna, frqStr, _model._elTx, ssrCorr);
+        }
+        if (ssrOK) {
+          if (satAntenna->_phaseCenterInfoInd) {
+            _model._antPCV[ii] += ssrCorr;
+          }
+          if (satAntenna->_groupDelayInfoInd) {
+            _model._antGDV[ii] += ssrCorr;
+          }
+        }
+        else {
+          phaseMissing[ii] = satAntenna->_phaseCenterInfoInd;
+          codeMissing[ii]  = satAntenna->_groupDelayInfoInd;
         }
       }
@@ -604,4 +688,50 @@
             _model._antPCO[ii] -= PPP_CLIENT->antex()->satCorr(prn, t_frequency::C2, _model._elTx, _model._azTx, found);
           }
+        }
+      }
+    }
+
+    // If the SSR antenna information applies to both observables (both
+    // DF+011 and DF+012 set), the code and phase biases must come from the
+    // same SSR solution (same SSR IOD and Provider ID), otherwise they are
+    // not consistent with each other with respect to it. Only checked if
+    // phase biases are used and both IODs are known.
+    if (useSsr && satAntenna->_phaseCenterInfoInd && satAntenna->_groupDelayInfoInd &&
+        OPT->arSystem(_prn.system())) {
+      const t_satCodeBias*  satCodeBias  = PPP_CLIENT->obsPool()->satCodeBias(_prn);
+      const t_satPhaseBias* satPhaseBias = PPP_CLIENT->obsPool()->satPhaseBias(_prn);
+      if (satCodeBias && satPhaseBias &&
+          satCodeBias->_ssrIOD >= 0 && satPhaseBias->_ssrIOD >= 0 &&
+          (satCodeBias->_ssrIOD        != satPhaseBias->_ssrIOD ||
+           satCodeBias->_ssrProviderID != satPhaseBias->_ssrProviderID)) {
+        LOG << "SSR code bias (IOD " << satCodeBias->_ssrIOD << ", provider " << satCodeBias->_ssrProviderID
+            << ") and phase bias (IOD " << satPhaseBias->_ssrIOD << ", provider " << satPhaseBias->_ssrProviderID
+            << ") inconsistent for " << _prn.toString() << " with shared SSR Satellite Antenna information"
+            << " - satellite excluded" << endl;
+        return failure;
+      }
+    }
+
+    // Exclude the satellite if the referenced antenna information omits a
+    // frequency actually used by the linear combinations of this system
+    const vector<t_lc>& LCs = OPT->LCs(_prn.system());
+    for (unsigned iLC = 0; iLC < LCs.size(); iLC++) {
+      map<t_frequency::type, double> codeCoeff;
+      map<t_frequency::type, double> phaseCoeff;
+      map<t_frequency::type, double> ionoCoeff;
+      lcCoeff(LCs[iLC], codeCoeff, phaseCoeff, ionoCoeff);
+      map<t_frequency::type, double>::const_iterator it;
+      for (it = phaseCoeff.begin(); it != phaseCoeff.end(); it++) {
+        if (phaseMissing[it->first]) {
+          LOG << "SSR Satellite Antenna IOD " << refIOD << " has no valid entry for "
+              << _prn.toString() << ' ' << t_frequency::toString(it->first) << " (phase) - satellite excluded" << endl;
+          return failure;
+        }
+      }
+      for (it = codeCoeff.begin(); it != codeCoeff.end(); it++) {
+        if (codeMissing[it->first]) {
+          LOG << "SSR Satellite Antenna IOD " << refIOD << " has no valid entry for "
+              << _prn.toString() << ' ' << t_frequency::toString(it->first) << " (code) - satellite excluded" << endl;
+          return failure;
         }
       }
