| 1 | // Part of BNC, a utility for retrieving decoding and
|
|---|
| 2 | // converting GNSS data streams from NTRIP broadcasters.
|
|---|
| 3 | //
|
|---|
| 4 | // Copyright (C) 2007
|
|---|
| 5 | // German Federal Agency for Cartography and Geodesy (BKG)
|
|---|
| 6 | // http://www.bkg.bund.de
|
|---|
| 7 | // Czech Technical University Prague, Department of Geodesy
|
|---|
| 8 | // http://www.fsv.cvut.cz
|
|---|
| 9 | //
|
|---|
| 10 | // Email: euref-ip@bkg.bund.de
|
|---|
| 11 | //
|
|---|
| 12 | // This program is free software; you can redistribute it and/or
|
|---|
| 13 | // modify it under the terms of the GNU General Public License
|
|---|
| 14 | // as published by the Free Software Foundation, version 2.
|
|---|
| 15 | //
|
|---|
| 16 | // This program is distributed in the hope that it will be useful,
|
|---|
| 17 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|---|
| 18 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|---|
| 19 | // GNU General Public License for more details.
|
|---|
| 20 | //
|
|---|
| 21 | // You should have received a copy of the GNU General Public License
|
|---|
| 22 | // along with this program; if not, write to the Free Software
|
|---|
| 23 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|---|
| 24 |
|
|---|
| 25 | /* -------------------------------------------------------------------------
|
|---|
| 26 | * BKG NTRIP Client
|
|---|
| 27 | * -------------------------------------------------------------------------
|
|---|
| 28 | *
|
|---|
| 29 | * Class: bncAntex
|
|---|
| 30 | *
|
|---|
| 31 | * Purpose: Antenna Phase Centers and Variations from ANTEX File
|
|---|
| 32 | *
|
|---|
| 33 | * Author: L. Mervart
|
|---|
| 34 | *
|
|---|
| 35 | * Created: 26-Jan-2011
|
|---|
| 36 | *
|
|---|
| 37 | * Changes:
|
|---|
| 38 | *
|
|---|
| 39 | * -----------------------------------------------------------------------*/
|
|---|
| 40 |
|
|---|
| 41 | #include <iostream>
|
|---|
| 42 | #include <cmath>
|
|---|
| 43 | #include <newmatio.h>
|
|---|
| 44 |
|
|---|
| 45 | #include "bncantex.h"
|
|---|
| 46 | #include "pppModel.h"
|
|---|
| 47 |
|
|---|
| 48 | using namespace std;
|
|---|
| 49 |
|
|---|
| 50 | // Constructor
|
|---|
| 51 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 52 | bncAntex::bncAntex() {
|
|---|
| 53 | }
|
|---|
| 54 |
|
|---|
| 55 | // Constructor
|
|---|
| 56 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 57 | bncAntex::bncAntex(const char* fileName) {
|
|---|
| 58 | readFile(QString(fileName));//print();
|
|---|
| 59 | }
|
|---|
| 60 |
|
|---|
| 61 | // Destructor
|
|---|
| 62 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 63 | bncAntex::~bncAntex() {
|
|---|
| 64 | QMapIterator<QString, t_antMap*> it(_maps);
|
|---|
| 65 | while (it.hasNext()) {
|
|---|
| 66 | it.next();
|
|---|
| 67 | delete it.value();
|
|---|
| 68 | }
|
|---|
| 69 | _maps.clear();
|
|---|
| 70 | }
|
|---|
| 71 |
|
|---|
| 72 | // Print
|
|---|
| 73 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 74 | void bncAntex::print() const {
|
|---|
| 75 | QMapIterator<QString, t_antMap*> itAnt(_maps);
|
|---|
| 76 | while (itAnt.hasNext()) {
|
|---|
| 77 | itAnt.next();
|
|---|
| 78 | t_antMap* map = itAnt.value();
|
|---|
| 79 | cout << map->antName.toLatin1().data() << endl;
|
|---|
| 80 | cout << " " << map->zen1 << " " << map->zen2 << " " << map->dZen << endl;
|
|---|
| 81 | QMapIterator<t_frequency::type, t_frqMap*> itFrq(map->frqMap);
|
|---|
| 82 | while (itFrq.hasNext()) {
|
|---|
| 83 | itFrq.next();
|
|---|
| 84 | const t_frqMap* frqMap = itFrq.value();
|
|---|
| 85 | cout << t_frequency::toString(itFrq.key()) << ":\n"
|
|---|
| 86 | << frqMap->neu[0] << " "
|
|---|
| 87 | << frqMap->neu[1] << " "
|
|---|
| 88 | << frqMap->neu[2] << endl;
|
|---|
| 89 | cout << frqMap->pattern.t();
|
|---|
| 90 | }
|
|---|
| 91 | cout << endl;
|
|---|
| 92 | }
|
|---|
| 93 | }
|
|---|
| 94 |
|
|---|
| 95 | // Print
|
|---|
| 96 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 97 | QString bncAntex::pcoSinexString(const std::string& antName, t_frequency::type frqType) {
|
|---|
| 98 |
|
|---|
| 99 | if (antName.find("NULLANTENNA") != string::npos) {
|
|---|
| 100 | return QString(" ------ ------ ------");
|
|---|
| 101 | }
|
|---|
| 102 |
|
|---|
| 103 | QString antNameQ = antName.c_str();
|
|---|
| 104 | if (_maps.find(antNameQ) == _maps.end()) {
|
|---|
| 105 | return QString(" ------ ------ ------");
|
|---|
| 106 | }
|
|---|
| 107 |
|
|---|
| 108 | t_antMap* map = _maps[antNameQ];
|
|---|
| 109 | if (map->frqMap.find(frqType) == map->frqMap.end()) {
|
|---|
| 110 | return QString(" ------ ------ ------");
|
|---|
| 111 | }
|
|---|
| 112 |
|
|---|
| 113 | t_frqMap* frqMap = map->frqMap[frqType];
|
|---|
| 114 |
|
|---|
| 115 | QString u = QString().asprintf("%+6.4f" ,frqMap->neu[2]); if (u.mid(1,1) == "0") {u.remove(1,1);}
|
|---|
| 116 | QString n = QString().asprintf("%+6.4f" ,frqMap->neu[0]); if (n.mid(1,1) == "0") {n.remove(1,1);}
|
|---|
| 117 | QString e = QString().asprintf("%+6.4f" ,frqMap->neu[1]); if (e.mid(1,1) == "0") {e.remove(1,1);}
|
|---|
| 118 |
|
|---|
| 119 | return QString(" %1 %2 %3").arg(u).arg(n).arg(e);
|
|---|
| 120 | }
|
|---|
| 121 |
|
|---|
| 122 | // Print
|
|---|
| 123 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 124 | QString bncAntex::snxCodeSinexString(const std::string& antName) {
|
|---|
| 125 |
|
|---|
| 126 | if (antName.find("NULLANTENNA") != string::npos) {
|
|---|
| 127 | return QString(" ----------");
|
|---|
| 128 | }
|
|---|
| 129 |
|
|---|
| 130 | QString antNameQ = antName.c_str();
|
|---|
| 131 | if (_maps.find(antNameQ) == _maps.end()) {
|
|---|
| 132 | return QString(" ----------");
|
|---|
| 133 | }
|
|---|
| 134 | else {
|
|---|
| 135 | return QString(" %1").arg(_maps[antNameQ]->snxCode, 10, QLatin1Char(' '));
|
|---|
| 136 | }
|
|---|
| 137 | }
|
|---|
| 138 |
|
|---|
| 139 | // Read ANTEX File
|
|---|
| 140 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 141 | t_irc bncAntex::readFile(const QString& fileName) {
|
|---|
| 142 |
|
|---|
| 143 | QFile inFile(fileName);
|
|---|
| 144 | inFile.open(QIODevice::ReadOnly | QIODevice::Text);
|
|---|
| 145 |
|
|---|
| 146 | QTextStream in(&inFile);
|
|---|
| 147 |
|
|---|
| 148 | t_antMap* newAntMap = 0;
|
|---|
| 149 | t_frqMap* newFrqMap = 0;
|
|---|
| 150 |
|
|---|
| 151 | while ( !in.atEnd() ) {
|
|---|
| 152 | QString line = in.readLine();
|
|---|
| 153 |
|
|---|
| 154 | // Start of Antenna
|
|---|
| 155 | // ----------------
|
|---|
| 156 | if (line.indexOf("START OF ANTENNA") == 60) {
|
|---|
| 157 | if (newAntMap) {
|
|---|
| 158 | delete newAntMap;
|
|---|
| 159 | return failure;
|
|---|
| 160 | }
|
|---|
| 161 | else {
|
|---|
| 162 | delete newAntMap;
|
|---|
| 163 | newAntMap = new t_antMap();
|
|---|
| 164 | }
|
|---|
| 165 | }
|
|---|
| 166 |
|
|---|
| 167 | // End of Antenna
|
|---|
| 168 | // --------------
|
|---|
| 169 | else if (line.indexOf("END OF ANTENNA") == 60) {
|
|---|
| 170 | if (newAntMap) {
|
|---|
| 171 | if (_maps.contains(newAntMap->antName)) {
|
|---|
| 172 | delete _maps[newAntMap->antName];
|
|---|
| 173 | }
|
|---|
| 174 | _maps[newAntMap->antName] = newAntMap;
|
|---|
| 175 | newAntMap = 0;
|
|---|
| 176 | }
|
|---|
| 177 | else {
|
|---|
| 178 | delete newAntMap;
|
|---|
| 179 | return failure;
|
|---|
| 180 | }
|
|---|
| 181 | }
|
|---|
| 182 |
|
|---|
| 183 | // Antenna Reading in Progress
|
|---|
| 184 | // ---------------------------
|
|---|
| 185 | else if (newAntMap) {
|
|---|
| 186 | if (line.indexOf("TYPE / SERIAL NO") == 60) {
|
|---|
| 187 | if (line.indexOf("BLOCK I") == 0 ||
|
|---|
| 188 | line.indexOf("GLONASS") == 0 ||
|
|---|
| 189 | line.indexOf("QZSS") == 0 ||
|
|---|
| 190 | line.indexOf("BEIDOU") == 0 ||
|
|---|
| 191 | line.indexOf("GALILEO") == 0 ||
|
|---|
| 192 | line.indexOf("NavIC") == 0 ){
|
|---|
| 193 | newAntMap->antName = line.mid(20,3);
|
|---|
| 194 | if (line.indexOf("BLOCK I") == 0) {
|
|---|
| 195 | // Extract GPS block type: "BLOCK IIF " → "IIF"
|
|---|
| 196 | QString bt = line.mid(0, 20).trimmed();
|
|---|
| 197 | if (bt.startsWith("BLOCK "))
|
|---|
| 198 | newAntMap->blockType = bt.mid(6);
|
|---|
| 199 | }
|
|---|
| 200 | else if (line.indexOf("GALILEO") == 0) {
|
|---|
| 201 | // "GALILEO-1" → "1" (IOV), "GALILEO-2" → "2" (FOC)
|
|---|
| 202 | QString bt = line.mid(0, 20).trimmed();
|
|---|
| 203 | if (bt.startsWith("GALILEO-"))
|
|---|
| 204 | newAntMap->blockType = bt.mid(8);
|
|---|
| 205 | }
|
|---|
| 206 | else if (line.indexOf("BEIDOU") == 0) {
|
|---|
| 207 | // "BEIDOU-2M" → "2M", "BEIDOU-3M-CAST" → "3M-CAST", etc.
|
|---|
| 208 | QString bt = line.mid(0, 20).trimmed();
|
|---|
| 209 | if (bt.startsWith("BEIDOU-"))
|
|---|
| 210 | newAntMap->blockType = bt.mid(7);
|
|---|
| 211 | }
|
|---|
| 212 | }
|
|---|
| 213 | else {
|
|---|
| 214 | newAntMap->antName = line.mid(0,20);
|
|---|
| 215 | }
|
|---|
| 216 | }
|
|---|
| 217 | else if (line.indexOf("ZEN1 / ZEN2 / DZEN") == 60) {
|
|---|
| 218 | QTextStream inLine(&line, QIODevice::ReadOnly);
|
|---|
| 219 | inLine >> newAntMap->zen1 >> newAntMap->zen2 >> newAntMap->dZen;
|
|---|
| 220 | }
|
|---|
| 221 | else if (line.indexOf("SINEX CODE") == 60) {
|
|---|
| 222 | QTextStream inLine(&line, QIODevice::ReadOnly);
|
|---|
| 223 | inLine >> newAntMap->snxCode;
|
|---|
| 224 | }
|
|---|
| 225 |
|
|---|
| 226 | // Start of Frequency
|
|---|
| 227 | // ------------------
|
|---|
| 228 | else if (line.indexOf("START OF FREQUENCY") == 60) {
|
|---|
| 229 | if (newFrqMap) {
|
|---|
| 230 | delete newFrqMap;
|
|---|
| 231 | delete newAntMap;
|
|---|
| 232 | return failure;
|
|---|
| 233 | }
|
|---|
| 234 | else {
|
|---|
| 235 | newFrqMap = new t_frqMap();
|
|---|
| 236 | }
|
|---|
| 237 | }
|
|---|
| 238 |
|
|---|
| 239 | // End of Frequency
|
|---|
| 240 | // ----------------
|
|---|
| 241 | else if (line.indexOf("END OF FREQUENCY") == 60) {
|
|---|
| 242 | if (newFrqMap) {
|
|---|
| 243 | t_frequency::type frqType = t_frequency::dummy;
|
|---|
| 244 | // GPS
|
|---|
| 245 | if (line.indexOf("G01") == 3) {
|
|---|
| 246 | frqType = t_frequency::G1;
|
|---|
| 247 | }
|
|---|
| 248 | else if (line.indexOf("G02") == 3) {
|
|---|
| 249 | frqType = t_frequency::G2;
|
|---|
| 250 | }
|
|---|
| 251 | else if (line.indexOf("G05") == 3) {
|
|---|
| 252 | frqType = t_frequency::G5;
|
|---|
| 253 | }
|
|---|
| 254 | // GLONASS
|
|---|
| 255 | else if (line.indexOf("R01") == 3) {
|
|---|
| 256 | frqType = t_frequency::R1;
|
|---|
| 257 | }
|
|---|
| 258 | else if (line.indexOf("R02") == 3) {
|
|---|
| 259 | frqType = t_frequency::R2;
|
|---|
| 260 | }
|
|---|
| 261 | // Galileo
|
|---|
| 262 | else if (line.indexOf("E01") == 3) {
|
|---|
| 263 | frqType = t_frequency::E1;
|
|---|
| 264 | }
|
|---|
| 265 | else if (line.indexOf("E05") == 3) {
|
|---|
| 266 | frqType = t_frequency::E5;
|
|---|
| 267 | }
|
|---|
| 268 | else if (line.indexOf("E06") == 3) {
|
|---|
| 269 | frqType = t_frequency::E6;
|
|---|
| 270 | }
|
|---|
| 271 | else if (line.indexOf("E07") == 3) {
|
|---|
| 272 | frqType = t_frequency::E7;
|
|---|
| 273 | }
|
|---|
| 274 | else if (line.indexOf("E08") == 3) {
|
|---|
| 275 | frqType = t_frequency::E8;
|
|---|
| 276 | }
|
|---|
| 277 | // QZSS
|
|---|
| 278 | else if (line.indexOf("J01") == 3) {
|
|---|
| 279 | frqType = t_frequency::J1;
|
|---|
| 280 | }
|
|---|
| 281 | else if (line.indexOf("J02") == 3) {
|
|---|
| 282 | frqType = t_frequency::J2;
|
|---|
| 283 | }
|
|---|
| 284 | else if (line.indexOf("J05") == 3) {
|
|---|
| 285 | frqType = t_frequency::J5;
|
|---|
| 286 | }
|
|---|
| 287 | else if (line.indexOf("J06") == 3) {
|
|---|
| 288 | frqType = t_frequency::J6;
|
|---|
| 289 | }
|
|---|
| 290 | // BDS
|
|---|
| 291 | else if (line.indexOf("C01") == 3) {
|
|---|
| 292 | frqType = t_frequency::C1;
|
|---|
| 293 | }
|
|---|
| 294 | else if (line.indexOf("C02") == 3) {
|
|---|
| 295 | frqType = t_frequency::C2;
|
|---|
| 296 | }
|
|---|
| 297 | else if (line.indexOf("C06") == 3) {
|
|---|
| 298 | frqType = t_frequency::C6;
|
|---|
| 299 | }
|
|---|
| 300 | else if (line.indexOf("C07") == 3) {
|
|---|
| 301 | frqType = t_frequency::C7;
|
|---|
| 302 | }
|
|---|
| 303 | if (frqType != t_frequency::dummy) {
|
|---|
| 304 | if (newAntMap->frqMap.find(frqType) != newAntMap->frqMap.end()) {
|
|---|
| 305 | delete newAntMap->frqMap[frqType];
|
|---|
| 306 | }
|
|---|
| 307 | newAntMap->frqMap[frqType] = newFrqMap;
|
|---|
| 308 | }
|
|---|
| 309 | else {
|
|---|
| 310 | delete newFrqMap;
|
|---|
| 311 | }
|
|---|
| 312 | newFrqMap = 0;
|
|---|
| 313 | }
|
|---|
| 314 | else {
|
|---|
| 315 | delete newAntMap;
|
|---|
| 316 | return failure;
|
|---|
| 317 | }
|
|---|
| 318 | }
|
|---|
| 319 |
|
|---|
| 320 | // Frequency Reading in Progress
|
|---|
| 321 | // -----------------------------
|
|---|
| 322 | else if (newFrqMap) {
|
|---|
| 323 | if (line.indexOf("NORTH / EAST / UP") == 60) {
|
|---|
| 324 | QTextStream inLine(&line, QIODevice::ReadOnly);
|
|---|
| 325 | inLine >> newFrqMap->neu[0] >> newFrqMap->neu[1] >> newFrqMap->neu[2];
|
|---|
| 326 | newFrqMap->neu[0] *= 1e-3;
|
|---|
| 327 | newFrqMap->neu[1] *= 1e-3;
|
|---|
| 328 | newFrqMap->neu[2] *= 1e-3;
|
|---|
| 329 | }
|
|---|
| 330 | else if (line.indexOf("NOAZI") == 3) {
|
|---|
| 331 | QTextStream inLine(&line, QIODevice::ReadOnly);
|
|---|
| 332 | int nPat = int((newAntMap->zen2-newAntMap->zen1)/newAntMap->dZen) + 1;
|
|---|
| 333 | newFrqMap->pattern.ReSize(nPat);
|
|---|
| 334 | QString dummy;
|
|---|
| 335 | inLine >> dummy;
|
|---|
| 336 | for (int ii = 0; ii < nPat; ii++) {
|
|---|
| 337 | inLine >> newFrqMap->pattern[ii];
|
|---|
| 338 | }
|
|---|
| 339 | newFrqMap->pattern *= 1e-3;
|
|---|
| 340 | }
|
|---|
| 341 | }
|
|---|
| 342 | }
|
|---|
| 343 | }
|
|---|
| 344 | inFile.close();
|
|---|
| 345 | delete newFrqMap;
|
|---|
| 346 | delete newAntMap;
|
|---|
| 347 |
|
|---|
| 348 | return success;
|
|---|
| 349 | }
|
|---|
| 350 |
|
|---|
| 351 | // GLONASS Yaw Angle (Sun-pointing law overridden near noon/midnight when
|
|---|
| 352 | // the satellite cannot mechanically keep up, following the GLONASS-M
|
|---|
| 353 | // "yaw-fixed" behaviour described in Dilssner, Springer, Flohrer, Dow
|
|---|
| 354 | // (2011), "The GLONASS-M satellite yaw-attitude model", Advances in Space
|
|---|
| 355 | // Research 47(1), 160-171.
|
|---|
| 356 | //
|
|---|
| 357 | // Unlike GPS/Galileo/BeiDou, which are commonly approximated by the
|
|---|
| 358 | // nominal Sun-pointing yaw-steering law of Bar-Sever (1996) at all times,
|
|---|
| 359 | // GLONASS-M has been found to stop tracking that law and hold a constant
|
|---|
| 360 | // (frozen) yaw angle whenever the required yaw rate would exceed the
|
|---|
| 361 | // satellite's slew capability - which happens close to the orbit
|
|---|
| 362 | // noon/midnight points whenever the Sun's elevation above the orbital
|
|---|
| 363 | // plane (the "beta" angle) is small. The maximum yaw rate used below
|
|---|
| 364 | // (0.25 deg/s) and the general approach follow that paper; satellite
|
|---|
| 365 | // telemetry was not available to validate the exact rate against this
|
|---|
| 366 | // installation's GLONASS satellites, so it should be checked against
|
|---|
| 367 | // independently-known attitude/orbit residuals if high accuracy matters.
|
|---|
| 368 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 369 | double bncAntex::glonassYawAngle(const QString& prn, double Mjd,
|
|---|
| 370 | const ColumnVector& xSat,
|
|---|
| 371 | const ColumnVector& vSat,
|
|---|
| 372 | const ColumnVector& xSun) {
|
|---|
| 373 |
|
|---|
| 374 | const double MAX_YAW_RATE = 0.25 * M_PI / 180.0; // [rad/s], approximate
|
|---|
| 375 |
|
|---|
| 376 | // This bounds the gap BETWEEN CONSECUTIVE CALLS for a given satellite
|
|---|
| 377 | // (not how long the freeze itself has lasted - a real low-beta passage
|
|---|
| 378 | // can legitimately stay frozen far longer than this). If there was no
|
|---|
| 379 | // call for longer than this, something else interrupted normal epoch-by-
|
|---|
| 380 | // epoch processing (a stream outage, a maneuver-flagged-unhealthy
|
|---|
| 381 | // period, ...), so the stored frozen value is more likely stale than a
|
|---|
| 382 | // still-valid freeze, and we fall back to the current nominal value
|
|---|
| 383 | // instead of trusting it indefinitely.
|
|---|
| 384 | const double MAX_CALL_GAP = 1800.0 / 86400.0; // 30 minutes, in days
|
|---|
| 385 |
|
|---|
| 386 | // Inertial-consistent velocity (xSat, vSat are Earth-fixed; remove the
|
|---|
| 387 | // Earth-rotation contribution so that the orbit normal below is not
|
|---|
| 388 | // contaminated by it)
|
|---|
| 389 | // -------------------------------------------------------------------
|
|---|
| 390 | ColumnVector Omega(3); Omega(1) = 0.0; Omega(2) = 0.0; Omega(3) = t_CST::omega;
|
|---|
| 391 | ColumnVector vInert = vSat + crossproduct(Omega, xSat);
|
|---|
| 392 |
|
|---|
| 393 | // Orbit normal and instantaneous orbital rate from r x v (exact, valid
|
|---|
| 394 | // for any Keplerian orbit, not just circular ones)
|
|---|
| 395 | // ---------------------------------------------------------------------
|
|---|
| 396 | ColumnVector h = crossproduct(xSat, vInert);
|
|---|
| 397 | double hNorm = sqrt(DotProduct(h, h));
|
|---|
| 398 | ColumnVector orbNormal = h / hNorm;
|
|---|
| 399 | double r = sqrt(DotProduct(xSat, xSat));
|
|---|
| 400 | double nRate = hNorm / (r * r); // [rad/s]
|
|---|
| 401 |
|
|---|
| 402 | // Beta angle (Sun elevation above the orbital plane)
|
|---|
| 403 | // ----------------------------------------------------
|
|---|
| 404 | double beta = asin(DotProduct(orbNormal, xSun));
|
|---|
| 405 |
|
|---|
| 406 | // Orbit angle mu, measured from the orbit midnight point, increasing in
|
|---|
| 407 | // the direction of satellite motion
|
|---|
| 408 | // -----------------------------------------------------------------------
|
|---|
| 409 | ColumnVector sunProj = xSun - DotProduct(xSun, orbNormal) * orbNormal;
|
|---|
| 410 | sunProj /= sqrt(DotProduct(sunProj, sunProj));
|
|---|
| 411 | ColumnVector eX = -1.0 * sunProj; // midnight direction, mu = 0
|
|---|
| 412 | ColumnVector eY = crossproduct(orbNormal, eX);
|
|---|
| 413 | ColumnVector rHat = xSat / r;
|
|---|
| 414 | double mu = atan2(DotProduct(rHat, eY), DotProduct(rHat, eX));
|
|---|
| 415 |
|
|---|
| 416 | // Nominal Sun-pointing yaw angle and its rate (Bar-Sever 1996)
|
|---|
| 417 | // ----------------------------------------------------------------
|
|---|
| 418 | double tanBeta = tan(beta);
|
|---|
| 419 | double sinMu = sin(mu);
|
|---|
| 420 | double psiNom = atan2(-tanBeta, sinMu);
|
|---|
| 421 | double denom = tanBeta * tanBeta + sinMu * sinMu;
|
|---|
| 422 | double psiRate = (denom > 1e-12)
|
|---|
| 423 | ? nRate * fabs(tanBeta * cos(mu)) / denom
|
|---|
| 424 | : 1e9;
|
|---|
| 425 |
|
|---|
| 426 | t_glonassYaw& st = _glonassYaw[prn];
|
|---|
| 427 | double callGap = st.valid ? (Mjd - st.lastCallMjd) : 0.0;
|
|---|
| 428 | double psiEff;
|
|---|
| 429 | if (psiRate > MAX_YAW_RATE) {
|
|---|
| 430 | if (!st.valid || callGap > MAX_CALL_GAP) {
|
|---|
| 431 | if (st.valid) { // i.e. it was a genuine gap, not cold-start
|
|---|
| 432 | _glonassYawLog += QString().asprintf(
|
|---|
| 433 | "%s glonassYaw STALE-RESET Mjd=%.6f beta=%6.3f mu=%7.2f rate=%6.3f"
|
|---|
| 434 | " old=%7.2f new=%7.2f gap=%.1fmin\n",
|
|---|
| 435 | prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI,
|
|---|
| 436 | psiRate*180.0/M_PI, st.yaw*180.0/M_PI, psiNom*180.0/M_PI,
|
|---|
| 437 | callGap*1440.0);
|
|---|
| 438 | }
|
|---|
| 439 | st.yaw = psiNom; // no prior history, or the gap is too long to trust it
|
|---|
| 440 | }
|
|---|
| 441 | if (!st.fixed) {
|
|---|
| 442 | _glonassYawLog += QString().asprintf(
|
|---|
| 443 | "%s glonassYaw FIXED-ENTER Mjd=%.6f beta=%6.3f mu=%7.2f rate=%6.3f yaw=%7.2f\n",
|
|---|
| 444 | prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI,
|
|---|
| 445 | psiRate*180.0/M_PI, st.yaw*180.0/M_PI);
|
|---|
| 446 | st.fixed = true;
|
|---|
| 447 | }
|
|---|
| 448 | psiEff = st.yaw; // hold the frozen yaw angle
|
|---|
| 449 | }
|
|---|
| 450 | else {
|
|---|
| 451 | if (st.fixed) {
|
|---|
| 452 | _glonassYawLog += QString().asprintf(
|
|---|
| 453 | "%s glonassYaw FIXED-EXIT Mjd=%.6f beta=%6.3f mu=%7.2f rate=%6.3f"
|
|---|
| 454 | " frozen=%7.2f nominal=%7.2f\n",
|
|---|
| 455 | prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI,
|
|---|
| 456 | psiRate*180.0/M_PI, st.yaw*180.0/M_PI, psiNom*180.0/M_PI);
|
|---|
| 457 | st.fixed = false;
|
|---|
| 458 | }
|
|---|
| 459 | st.yaw = psiNom;
|
|---|
| 460 | psiEff = psiNom;
|
|---|
| 461 | }
|
|---|
| 462 | st.lastCallMjd = Mjd; // update on every call, fixed or not, to detect gaps
|
|---|
| 463 | st.valid = true;
|
|---|
| 464 |
|
|---|
| 465 | return psiEff;
|
|---|
| 466 | }
|
|---|
| 467 |
|
|---|
| 468 | // GPS satellite yaw angle during nominal tracking and noon/midnight turns.
|
|---|
| 469 | //
|
|---|
| 470 | // GPS satellites perform a "noon turn" (and, for some blocks, a "midnight
|
|---|
| 471 | // turn") when the Sun's elevation angle above the orbital plane (beta) is
|
|---|
| 472 | // small: the required nominal yaw rate then exceeds the satellite's
|
|---|
| 473 | // mechanical maximum, so the satellite yaws at that maximum rate until it
|
|---|
| 474 | // catches back up to the nominal Sun-pointing orientation (Kouba 2009/2015,
|
|---|
| 475 | // Bar-Sever 1996).
|
|---|
| 476 | //
|
|---|
| 477 | // The max yaw rates below are the best published estimates per block type:
|
|---|
| 478 | // IIA: 0.12 °/s (Kouba 2009)
|
|---|
| 479 | // IIR: 0.20 °/s (Bar-Sever 1996)
|
|---|
| 480 | // IIR-M: 0.20 °/s
|
|---|
| 481 | // IIF: 0.11 °/s (Kouba 2015)
|
|---|
| 482 | // IIIA: 0.15 °/s (tentative)
|
|---|
| 483 | //
|
|---|
| 484 | // Returns the effective yaw angle [rad] in the velocity-referenced frame,
|
|---|
| 485 | // for use with the same Rodrigues rotation as the GLONASS model. During
|
|---|
| 486 | // nominal tracking this equals psiNom and the result is identical to the
|
|---|
| 487 | // simple sz×xSun formula.
|
|---|
| 488 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 489 | double bncAntex::gpsYawAngle(const QString& prn, const QString& blockType,
|
|---|
| 490 | double Mjd,
|
|---|
| 491 | const ColumnVector& xSat,
|
|---|
| 492 | const ColumnVector& vSat,
|
|---|
| 493 | const ColumnVector& xSun) {
|
|---|
| 494 |
|
|---|
| 495 | // Max yaw rate [rad/s] by GPS block type
|
|---|
| 496 | double psiDotMax;
|
|---|
| 497 | if (blockType == "IIA") psiDotMax = 0.12 * M_PI / 180.0;
|
|---|
| 498 | else if (blockType == "IIR") psiDotMax = 0.20 * M_PI / 180.0;
|
|---|
| 499 | else if (blockType == "IIR-M") psiDotMax = 0.20 * M_PI / 180.0;
|
|---|
| 500 | else if (blockType == "IIF") psiDotMax = 0.11 * M_PI / 180.0;
|
|---|
| 501 | else if (blockType == "IIIA") psiDotMax = 0.15 * M_PI / 180.0;
|
|---|
| 502 | else return 0.0; // unknown block: caller uses simple Sun-pointing
|
|---|
| 503 |
|
|---|
| 504 | const double MAX_CALL_GAP = 1800.0 / 86400.0; // 30 min in days
|
|---|
| 505 |
|
|---|
| 506 | // Inertial velocity
|
|---|
| 507 | ColumnVector Omega(3); Omega(1) = 0.0; Omega(2) = 0.0; Omega(3) = t_CST::omega;
|
|---|
| 508 | ColumnVector vInert = vSat + crossproduct(Omega, xSat);
|
|---|
| 509 |
|
|---|
| 510 | // Orbital angular momentum vector → orbital rate
|
|---|
| 511 | ColumnVector h = crossproduct(xSat, vInert);
|
|---|
| 512 | double hNorm = sqrt(DotProduct(h, h));
|
|---|
| 513 | ColumnVector orbNormal = h / hNorm;
|
|---|
| 514 | double r = sqrt(DotProduct(xSat, xSat));
|
|---|
| 515 | double nRate = hNorm / (r * r); // [rad/s]
|
|---|
| 516 |
|
|---|
| 517 | // Beta angle
|
|---|
| 518 | double beta = asin(DotProduct(orbNormal, xSun));
|
|---|
| 519 |
|
|---|
| 520 | // Mu: orbit angle from midnight (same geometry as GLONASS)
|
|---|
| 521 | ColumnVector sunProj = xSun - DotProduct(xSun, orbNormal) * orbNormal;
|
|---|
| 522 | sunProj /= sqrt(DotProduct(sunProj, sunProj));
|
|---|
| 523 | ColumnVector eX = -1.0 * sunProj; // midnight direction
|
|---|
| 524 | ColumnVector eY = crossproduct(orbNormal, eX);
|
|---|
| 525 | ColumnVector rHat = xSat / r;
|
|---|
| 526 | double mu = atan2(DotProduct(rHat, eY), DotProduct(rHat, eX));
|
|---|
| 527 |
|
|---|
| 528 | // Nominal yaw and its rate
|
|---|
| 529 | double tanBeta = tan(beta);
|
|---|
| 530 | double sinMu = sin(mu);
|
|---|
| 531 | double psiNom = atan2(-tanBeta, sinMu);
|
|---|
| 532 | double denom = tanBeta * tanBeta + sinMu * sinMu;
|
|---|
| 533 | // |dPsi/dt| (always non-negative, sign comes from sign of tanBeta*cos(mu))
|
|---|
| 534 | double psiDotNomAbs = (denom > 1e-12)
|
|---|
| 535 | ? nRate * fabs(tanBeta * cos(mu)) / denom
|
|---|
| 536 | : 1e9;
|
|---|
| 537 | // Sign of the nominal yaw rate: dPsi/dt = nRate * tanBeta * cos(mu) / denom
|
|---|
| 538 | double psiDotNomSign = (tanBeta * cos(mu) >= 0.0) ? 1.0 : -1.0;
|
|---|
| 539 |
|
|---|
| 540 | t_gpsYaw& st = _gpsYaw[prn];
|
|---|
| 541 | double callGap = st.valid ? (Mjd - st.lastCallMjd) : 0.0;
|
|---|
| 542 |
|
|---|
| 543 | // Stale state: reset if there has been a gap in calls
|
|---|
| 544 | if (st.valid && callGap > MAX_CALL_GAP) {
|
|---|
| 545 | _gpsYawLog += QString().asprintf(
|
|---|
| 546 | "%s gpsYaw STALE-RESET Mjd=%.6f beta=%6.3f mu=%7.2f"
|
|---|
| 547 | " old=%7.2f new=%7.2f gap=%.1fmin\n",
|
|---|
| 548 | prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI,
|
|---|
| 549 | st.yaw*180.0/M_PI, psiNom*180.0/M_PI, callGap*1440.0);
|
|---|
| 550 | st.valid = false;
|
|---|
| 551 | st.inTurn = false;
|
|---|
| 552 | }
|
|---|
| 553 |
|
|---|
| 554 | double psiEff;
|
|---|
| 555 | if (psiDotNomAbs > psiDotMax) {
|
|---|
| 556 | // Constrained: satellite yaws at psiDotMax in the nominal direction
|
|---|
| 557 | if (!st.valid || !st.inTurn) {
|
|---|
| 558 | // Entering a noon/midnight turn
|
|---|
| 559 | psiEff = st.valid ? st.yaw : psiNom;
|
|---|
| 560 | _gpsYawLog += QString().asprintf(
|
|---|
| 561 | "%s gpsYaw TURN-ENTER Mjd=%.6f beta=%6.3f mu=%7.2f"
|
|---|
| 562 | " psiNom=%7.2f psiEff=%7.2f psiDotNom=%6.3f max=%5.3f\n",
|
|---|
| 563 | prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI,
|
|---|
| 564 | psiNom*180.0/M_PI, psiEff*180.0/M_PI,
|
|---|
| 565 | psiDotNomAbs*psiDotNomSign*180.0/M_PI, psiDotMax*180.0/M_PI);
|
|---|
| 566 | st.inTurn = true;
|
|---|
| 567 | } else {
|
|---|
| 568 | // Continuing the turn: integrate at constrained rate
|
|---|
| 569 | double dt = callGap * 86400.0; // [s]
|
|---|
| 570 | psiEff = st.yaw + psiDotNomSign * psiDotMax * dt;
|
|---|
| 571 | }
|
|---|
| 572 | // Wrap to [-pi, pi]
|
|---|
| 573 | while (psiEff > M_PI) psiEff -= 2.0 * M_PI;
|
|---|
| 574 | while (psiEff < -M_PI) psiEff += 2.0 * M_PI;
|
|---|
| 575 | }
|
|---|
| 576 | else {
|
|---|
| 577 | // Nominal tracking
|
|---|
| 578 | psiEff = psiNom;
|
|---|
| 579 | if (st.inTurn) {
|
|---|
| 580 | _gpsYawLog += QString().asprintf(
|
|---|
| 581 | "%s gpsYaw TURN-EXIT Mjd=%.6f beta=%6.3f mu=%7.2f"
|
|---|
| 582 | " frozen=%7.2f nominal=%7.2f\n",
|
|---|
| 583 | prn.toLatin1().data(), Mjd, beta*180.0/M_PI, mu*180.0/M_PI,
|
|---|
| 584 | st.yaw*180.0/M_PI, psiNom*180.0/M_PI);
|
|---|
| 585 | st.inTurn = false;
|
|---|
| 586 | }
|
|---|
| 587 | st.yaw = psiNom;
|
|---|
| 588 | }
|
|---|
| 589 |
|
|---|
| 590 | st.yaw = psiEff;
|
|---|
| 591 | st.lastCallMjd = Mjd;
|
|---|
| 592 | st.valid = true;
|
|---|
| 593 |
|
|---|
| 594 | return psiEff;
|
|---|
| 595 | }
|
|---|
| 596 |
|
|---|
| 597 | //
|
|---|
| 598 | // Orbit-Normal Mode Yaw Angle — shared model for Galileo and BDS
|
|---|
| 599 | //
|
|---|
| 600 | // When |beta| < betaThr the satellite rotates toward yaw = 0 (orbit-normal)
|
|---|
| 601 | // at the block-specific maximum yaw rate. When |beta| >= betaThr it returns
|
|---|
| 602 | // to nominal yaw-steering (psiNom). Transitions are rate-limited in both
|
|---|
| 603 | // directions to match physical satellite behaviour.
|
|---|
| 604 | //
|
|---|
| 605 | // References:
|
|---|
| 606 | // Galileo IOV/FOC: Kouba (2017), Steigenberger et al. (2018)
|
|---|
| 607 | // BDS MEO/IGSO: Dai et al. (2015), Wang et al. (2018)
|
|---|
| 608 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 609 | double bncAntex::onModeYawAngle(const QString& prn,
|
|---|
| 610 | double betaThr, double psiDotMax, double Mjd,
|
|---|
| 611 | const ColumnVector& xSat,
|
|---|
| 612 | const ColumnVector& vSat,
|
|---|
| 613 | const ColumnVector& xSun) {
|
|---|
| 614 |
|
|---|
| 615 | const double MAX_CALL_GAP = 1800.0 / 86400.0; // 30 min [days]
|
|---|
| 616 |
|
|---|
| 617 | // Inertial velocity
|
|---|
| 618 | ColumnVector Omega(3); Omega(1) = 0.0; Omega(2) = 0.0; Omega(3) = t_CST::omega;
|
|---|
| 619 | ColumnVector vInert = vSat + crossproduct(Omega, xSat);
|
|---|
| 620 |
|
|---|
| 621 | // Orbit geometry
|
|---|
| 622 | ColumnVector h = crossproduct(xSat, vInert);
|
|---|
| 623 | double hNorm = sqrt(DotProduct(h, h));
|
|---|
| 624 | ColumnVector orbNormal = h / hNorm;
|
|---|
| 625 | double r = sqrt(DotProduct(xSat, xSat));
|
|---|
| 626 |
|
|---|
| 627 | double beta = asin(DotProduct(orbNormal, xSun));
|
|---|
| 628 |
|
|---|
| 629 | ColumnVector sunProj = xSun - DotProduct(xSun, orbNormal) * orbNormal;
|
|---|
| 630 | sunProj /= sqrt(DotProduct(sunProj, sunProj));
|
|---|
| 631 | ColumnVector eX = -1.0 * sunProj;
|
|---|
| 632 | ColumnVector eY = crossproduct(orbNormal, eX);
|
|---|
| 633 | ColumnVector rHat = xSat / r;
|
|---|
| 634 | double mu = atan2(DotProduct(rHat, eY), DotProduct(rHat, eX));
|
|---|
| 635 | double psiNom = atan2(-tan(beta), sin(mu));
|
|---|
| 636 |
|
|---|
| 637 | // Target yaw: orbit-normal (0) when below threshold, nominal otherwise
|
|---|
| 638 | bool wantsON = (fabs(beta) < betaThr);
|
|---|
| 639 | double psiTarget = wantsON ? 0.0 : psiNom;
|
|---|
| 640 |
|
|---|
| 641 | t_onYaw& st = _onYaw[prn];
|
|---|
| 642 | double callGap = st.valid ? (Mjd - st.lastCallMjd) : 0.0;
|
|---|
| 643 |
|
|---|
| 644 | if (st.valid && callGap > MAX_CALL_GAP) {
|
|---|
| 645 | _onYawLog += QString().asprintf(
|
|---|
| 646 | "%s onYaw STALE-RESET Mjd=%.6f beta=%5.2f gap=%.1fmin\n",
|
|---|
| 647 | prn.toLatin1().data(), Mjd, beta*180.0/M_PI, callGap*1440.0);
|
|---|
| 648 | st.valid = false;
|
|---|
| 649 | }
|
|---|
| 650 |
|
|---|
| 651 | double psiEff;
|
|---|
| 652 | if (!st.valid) {
|
|---|
| 653 | // Cold start: initialise at psiNom regardless of mode
|
|---|
| 654 | psiEff = psiNom;
|
|---|
| 655 | st.inON = false;
|
|---|
| 656 | }
|
|---|
| 657 | else {
|
|---|
| 658 | double dt = callGap * 86400.0; // [s]
|
|---|
| 659 | double dpsi = psiTarget - st.yaw;
|
|---|
| 660 | while (dpsi > M_PI) dpsi -= 2.0 * M_PI;
|
|---|
| 661 | while (dpsi < -M_PI) dpsi += 2.0 * M_PI;
|
|---|
| 662 | double maxDpsi = psiDotMax * dt;
|
|---|
| 663 |
|
|---|
| 664 | if (fabs(dpsi) <= maxDpsi) {
|
|---|
| 665 | // Target reached this step
|
|---|
| 666 | psiEff = psiTarget;
|
|---|
| 667 | bool wasON = st.inON;
|
|---|
| 668 | st.inON = wantsON;
|
|---|
| 669 | if (!wasON && wantsON && fabs(st.yaw) < 0.5*M_PI/180.0) {
|
|---|
| 670 | _onYawLog += QString().asprintf(
|
|---|
| 671 | "%s onYaw ON-ENTER Mjd=%.6f beta=%5.2f psiNom=%7.2f\n",
|
|---|
| 672 | prn.toLatin1().data(), Mjd, beta*180.0/M_PI, psiNom*180.0/M_PI);
|
|---|
| 673 | }
|
|---|
| 674 | else if (wasON && !wantsON) {
|
|---|
| 675 | _onYawLog += QString().asprintf(
|
|---|
| 676 | "%s onYaw ON-EXIT Mjd=%.6f beta=%5.2f psiNom=%7.2f\n",
|
|---|
| 677 | prn.toLatin1().data(), Mjd, beta*180.0/M_PI, psiNom*180.0/M_PI);
|
|---|
| 678 | st.inON = false;
|
|---|
| 679 | }
|
|---|
| 680 | }
|
|---|
| 681 | else {
|
|---|
| 682 | // Still rotating toward target
|
|---|
| 683 | psiEff = st.yaw + (dpsi > 0.0 ? 1.0 : -1.0) * maxDpsi;
|
|---|
| 684 | }
|
|---|
| 685 | }
|
|---|
| 686 |
|
|---|
| 687 | while (psiEff > M_PI) psiEff -= 2.0 * M_PI;
|
|---|
| 688 | while (psiEff < -M_PI) psiEff += 2.0 * M_PI;
|
|---|
| 689 |
|
|---|
| 690 | st.yaw = psiEff;
|
|---|
| 691 | st.lastCallMjd = Mjd;
|
|---|
| 692 | st.valid = true;
|
|---|
| 693 |
|
|---|
| 694 | return psiEff;
|
|---|
| 695 | }
|
|---|
| 696 |
|
|---|
| 697 | //
|
|---|
| 698 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 699 | double bncAntex::galileoYawAngle(const QString& prn, const QString& blockType,
|
|---|
| 700 | double Mjd,
|
|---|
| 701 | const ColumnVector& xSat,
|
|---|
| 702 | const ColumnVector& vSat,
|
|---|
| 703 | const ColumnVector& xSun) {
|
|---|
| 704 | // IOV (type "1") and FOC (type "2"): orbit-normal for |beta| < 2 deg.
|
|---|
| 705 | // Max yaw rate 0.20 deg/s applies to both generations.
|
|---|
| 706 | // Kouba (2017), Steigenberger et al. (2018)
|
|---|
| 707 | Q_UNUSED(blockType);
|
|---|
| 708 | const double betaThr = 2.0 * M_PI / 180.0;
|
|---|
| 709 | const double psiDotMax = 0.20 * M_PI / 180.0;
|
|---|
| 710 | return onModeYawAngle(prn, betaThr, psiDotMax, Mjd, xSat, vSat, xSun);
|
|---|
| 711 | }
|
|---|
| 712 |
|
|---|
| 713 | //
|
|---|
| 714 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 715 | double bncAntex::bdsYawAngle(const QString& prn, const QString& blockType,
|
|---|
| 716 | double Mjd,
|
|---|
| 717 | const ColumnVector& xSat,
|
|---|
| 718 | const ColumnVector& vSat,
|
|---|
| 719 | const ColumnVector& xSun) {
|
|---|
| 720 | // GEO satellites are always in orbit-normal mode (yaw = 0).
|
|---|
| 721 | if (blockType == "2G" || blockType == "3G-CAST")
|
|---|
| 722 | return 0.0;
|
|---|
| 723 |
|
|---|
| 724 | double betaThr, psiDotMax;
|
|---|
| 725 | if (blockType == "2M" || blockType == "2I") {
|
|---|
| 726 | // BDS-2 MEO / IGSO: orbit-normal for |beta| < 4 deg (Dai et al. 2015)
|
|---|
| 727 | betaThr = 4.0 * M_PI / 180.0;
|
|---|
| 728 | psiDotMax = 0.10 * M_PI / 180.0;
|
|---|
| 729 | }
|
|---|
| 730 | else {
|
|---|
| 731 | // BDS-3 (CAST and SECM MEO/IGSO): orbit-normal for |beta| < 3 deg
|
|---|
| 732 | betaThr = 3.0 * M_PI / 180.0;
|
|---|
| 733 | psiDotMax = 0.15 * M_PI / 180.0;
|
|---|
| 734 | }
|
|---|
| 735 | return onModeYawAngle(prn, betaThr, psiDotMax, Mjd, xSat, vSat, xSun);
|
|---|
| 736 | }
|
|---|
| 737 |
|
|---|
| 738 | //
|
|---|
| 739 | // Satellite Antenna Offset
|
|---|
| 740 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 741 | t_irc bncAntex::satCoMcorrection(const QString& prn, double Mjd,
|
|---|
| 742 | const ColumnVector& xSat,
|
|---|
| 743 | const ColumnVector& vSat, ColumnVector& dx,
|
|---|
| 744 | e_attMode mode, double externalYaw) {
|
|---|
| 745 |
|
|---|
| 746 | t_frequency::type frqType = t_frequency::dummy;
|
|---|
| 747 |
|
|---|
| 748 | if (prn[0] == 'G') {
|
|---|
| 749 | frqType = t_frequency::G1;
|
|---|
| 750 | }
|
|---|
| 751 | else if (prn[0] == 'R') {
|
|---|
| 752 | frqType = t_frequency::R1;
|
|---|
| 753 | }
|
|---|
| 754 | else if (prn[0] == 'E') {
|
|---|
| 755 | frqType = t_frequency::E1;
|
|---|
| 756 | }
|
|---|
| 757 | else if (prn[0] == 'C') {
|
|---|
| 758 | frqType = t_frequency::C2;
|
|---|
| 759 | }
|
|---|
| 760 | else if (prn[0] == 'S') {
|
|---|
| 761 | frqType = t_frequency::S1;
|
|---|
| 762 | }
|
|---|
| 763 | else if (prn[0] == 'J') {
|
|---|
| 764 | frqType = t_frequency::J1;
|
|---|
| 765 | }
|
|---|
| 766 | else if (prn[0] == 'I') {
|
|---|
| 767 | frqType = t_frequency::I5;
|
|---|
| 768 | }
|
|---|
| 769 |
|
|---|
| 770 | QMap<QString, t_antMap*>::const_iterator it = _maps.find(prn.mid(0,3));
|
|---|
| 771 | if (it != _maps.end()) {
|
|---|
| 772 | t_antMap* map = it.value();
|
|---|
| 773 | if (map->frqMap.find(frqType) != map->frqMap.end()) {
|
|---|
| 774 |
|
|---|
| 775 | double* neu = map->frqMap[frqType]->neu;
|
|---|
| 776 |
|
|---|
| 777 | // Unit Vectors sz, sy, sx
|
|---|
| 778 | // -----------------------
|
|---|
| 779 | ColumnVector sz = -xSat;
|
|---|
| 780 | sz /= sqrt(DotProduct(sz,sz));
|
|---|
| 781 |
|
|---|
| 782 | ColumnVector xSun = BNC_PPP::t_astro::Sun(Mjd);
|
|---|
| 783 | xSun /= sqrt(DotProduct(xSun,xSun));
|
|---|
| 784 |
|
|---|
| 785 | ColumnVector sy, sx;
|
|---|
| 786 |
|
|---|
| 787 | // Determine the satellite body frame orientation.
|
|---|
| 788 | //
|
|---|
| 789 | // ATT_NOMINAL: simple nominal Sun-pointing for all systems.
|
|---|
| 790 | // ATT_COMPUTED or ATT_EXTERNAL: use per-system attitude models.
|
|---|
| 791 | // GLONASS → yaw-fixed model (Dilssner et al. 2011)
|
|---|
| 792 | // GPS → noon/midnight turn model (Kouba 2009/2015)
|
|---|
| 793 | // others → simple nominal Sun-pointing
|
|---|
| 794 | // ATT_EXTERNAL: caller supplies the yaw angle [rad] directly
|
|---|
| 795 | // (velocity-referenced frame, same convention as GLONASS/GPS models).
|
|---|
| 796 | // -----------------------------------------------------------------------
|
|---|
| 797 | bool useVelocityFrame = false;
|
|---|
| 798 | double psiEff = 0.0;
|
|---|
| 799 |
|
|---|
| 800 | if (mode == ATT_COMPUTED && prn[0] == 'R' && vSat.size() == 3) {
|
|---|
| 801 | psiEff = glonassYawAngle(prn, Mjd, xSat, vSat, xSun);
|
|---|
| 802 | useVelocityFrame = true;
|
|---|
| 803 | }
|
|---|
| 804 | else if (mode == ATT_COMPUTED && prn[0] == 'G' && vSat.size() == 3
|
|---|
| 805 | && !map->blockType.isEmpty()) {
|
|---|
| 806 | psiEff = gpsYawAngle(prn, map->blockType, Mjd, xSat, vSat, xSun);
|
|---|
| 807 | useVelocityFrame = true;
|
|---|
| 808 | }
|
|---|
| 809 | else if (mode == ATT_COMPUTED && prn[0] == 'E' && vSat.size() == 3
|
|---|
| 810 | && !map->blockType.isEmpty()) {
|
|---|
| 811 | psiEff = galileoYawAngle(prn, map->blockType, Mjd, xSat, vSat, xSun);
|
|---|
| 812 | useVelocityFrame = true;
|
|---|
| 813 | }
|
|---|
| 814 | else if (mode == ATT_COMPUTED && prn[0] == 'C' && vSat.size() == 3
|
|---|
| 815 | && !map->blockType.isEmpty()) {
|
|---|
| 816 | psiEff = bdsYawAngle(prn, map->blockType, Mjd, xSat, vSat, xSun);
|
|---|
| 817 | useVelocityFrame = true;
|
|---|
| 818 | }
|
|---|
| 819 | else if (mode == ATT_EXTERNAL && vSat.size() == 3) {
|
|---|
| 820 | psiEff = externalYaw;
|
|---|
| 821 | useVelocityFrame = true;
|
|---|
| 822 | }
|
|---|
| 823 |
|
|---|
| 824 | if (useVelocityFrame) {
|
|---|
| 825 | ColumnVector Omega(3); Omega(1) = 0.0; Omega(2) = 0.0; Omega(3) = t_CST::omega;
|
|---|
| 826 | ColumnVector vInert = vSat + crossproduct(Omega, xSat);
|
|---|
| 827 |
|
|---|
| 828 | ColumnVector sy0 = crossproduct(sz, vInert);
|
|---|
| 829 | sy0 /= sqrt(DotProduct(sy0,sy0));
|
|---|
| 830 | ColumnVector sx0 = crossproduct(sy0, sz);
|
|---|
| 831 |
|
|---|
| 832 | // Rodrigues rotation of (sx0, sy0) around sz by psiEff
|
|---|
| 833 | double cosY = cos(psiEff);
|
|---|
| 834 | double sinY = sin(psiEff);
|
|---|
| 835 | sx = sx0 * cosY + crossproduct(sz, sx0) * sinY;
|
|---|
| 836 | sy = sy0 * cosY + crossproduct(sz, sy0) * sinY;
|
|---|
| 837 | }
|
|---|
| 838 | else {
|
|---|
| 839 | // Nominal Sun-pointing: direct formula (ATT_NOMINAL, or computed
|
|---|
| 840 | // with no block-type-specific model available)
|
|---|
| 841 | sy = crossproduct(sz, xSun);
|
|---|
| 842 | sy /= sqrt(DotProduct(sy,sy));
|
|---|
| 843 | sx = crossproduct(sy, sz);
|
|---|
| 844 | }
|
|---|
| 845 |
|
|---|
| 846 | dx[0] = sx[0] * neu[0] + sy[0] * neu[1] + sz[0] * neu[2];
|
|---|
| 847 | dx[1] = sx[1] * neu[0] + sy[1] * neu[1] + sz[1] * neu[2];
|
|---|
| 848 | dx[2] = sx[2] * neu[0] + sy[2] * neu[1] + sz[2] * neu[2];
|
|---|
| 849 |
|
|---|
| 850 | return success;
|
|---|
| 851 | }
|
|---|
| 852 | }
|
|---|
| 853 |
|
|---|
| 854 | return failure;
|
|---|
| 855 | }
|
|---|
| 856 |
|
|---|
| 857 | //
|
|---|
| 858 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 859 | double bncAntex::satCorr(const QString& prn, t_frequency::type frqType,
|
|---|
| 860 | double elTx, double azTx, bool& found) const {
|
|---|
| 861 |
|
|---|
| 862 | if (_maps.find(prn.mid(0,3)) == _maps.end()) {
|
|---|
| 863 | found = false;
|
|---|
| 864 | return 0.0;
|
|---|
| 865 | };
|
|---|
| 866 |
|
|---|
| 867 | t_antMap* map = _maps[prn.mid(0,3)];
|
|---|
| 868 |
|
|---|
| 869 | if (map->frqMap.find(frqType) == map->frqMap.end()) {
|
|---|
| 870 | found = false;
|
|---|
| 871 | return 0.0;
|
|---|
| 872 | };
|
|---|
| 873 |
|
|---|
| 874 | t_frqMap* frqMap = map->frqMap[frqType];
|
|---|
| 875 |
|
|---|
| 876 | double var = 0.0;
|
|---|
| 877 | if (frqMap->pattern.ncols() > 0) {
|
|---|
| 878 | double zenDiff = 999.999;
|
|---|
| 879 | double zenTx = 90.0 - elTx * 180.0 / M_PI;
|
|---|
| 880 | unsigned iZen = 0;
|
|---|
| 881 | for (double zen = map->zen1; zen <= map->zen2; zen += map->dZen) {
|
|---|
| 882 | iZen += 1;
|
|---|
| 883 | double newZenDiff = fabs(zen - zenTx);
|
|---|
| 884 | if (newZenDiff < zenDiff) {
|
|---|
| 885 | zenDiff = newZenDiff;
|
|---|
| 886 | var = frqMap->pattern(iZen);
|
|---|
| 887 | }
|
|---|
| 888 | }
|
|---|
| 889 | }
|
|---|
| 890 |
|
|---|
| 891 | found = true;
|
|---|
| 892 | return var - frqMap->neu[0] * cos(azTx)*cos(elTx)
|
|---|
| 893 | - frqMap->neu[1] * sin(azTx)*cos(elTx)
|
|---|
| 894 | - frqMap->neu[2] * sin(elTx);
|
|---|
| 895 |
|
|---|
| 896 | }
|
|---|
| 897 |
|
|---|
| 898 | //
|
|---|
| 899 | ////////////////////////////////////////////////////////////////////////////
|
|---|
| 900 | double bncAntex::rcvCorr(const string& antName, t_frequency::type frqType,
|
|---|
| 901 | double eleSat, double azSat, bool& found) const {
|
|---|
| 902 |
|
|---|
| 903 | if (antName.find("NULLANTENNA") != string::npos) {
|
|---|
| 904 | found = true;
|
|---|
| 905 | return 0.0;
|
|---|
| 906 | }
|
|---|
| 907 |
|
|---|
| 908 | QString antNameQ = antName.c_str();
|
|---|
| 909 |
|
|---|
| 910 | if (_maps.find(antNameQ) == _maps.end()) {
|
|---|
| 911 | found = false;
|
|---|
| 912 | return 0.0;
|
|---|
| 913 | }
|
|---|
| 914 |
|
|---|
| 915 | t_antMap* map = _maps[antNameQ];
|
|---|
| 916 | if (map->frqMap.find(frqType) == map->frqMap.end()) {
|
|---|
| 917 | found = false;
|
|---|
| 918 | return 0.0;
|
|---|
| 919 | }
|
|---|
| 920 |
|
|---|
| 921 | t_frqMap* frqMap = map->frqMap[frqType];
|
|---|
| 922 |
|
|---|
| 923 | double var = 0.0;
|
|---|
| 924 | if (frqMap->pattern.ncols() > 0) {
|
|---|
| 925 | double zenDiff = 999.999;
|
|---|
| 926 | double zenSat = 90.0 - eleSat * 180.0 / M_PI;
|
|---|
| 927 | unsigned iZen = 0;
|
|---|
| 928 | for (double zen = map->zen1; zen <= map->zen2; zen += map->dZen) {
|
|---|
| 929 | iZen += 1;
|
|---|
| 930 | double newZenDiff = fabs(zen - zenSat);
|
|---|
| 931 | if (newZenDiff < zenDiff) {
|
|---|
| 932 | zenDiff = newZenDiff;
|
|---|
| 933 | var = frqMap->pattern(iZen);
|
|---|
| 934 | }
|
|---|
| 935 | }
|
|---|
| 936 | }
|
|---|
| 937 |
|
|---|
| 938 | found = true;
|
|---|
| 939 | return var - frqMap->neu[0] * cos(azSat)*cos(eleSat)
|
|---|
| 940 | - frqMap->neu[1] * sin(azSat)*cos(eleSat)
|
|---|
| 941 | - frqMap->neu[2] * sin(eleSat);
|
|---|
| 942 |
|
|---|
| 943 | }
|
|---|