Program Listing for File antex_loader.cc¶
↰ Return to documentation for file (interfaces/antex_loader.cc)
#include "lupnt/interfaces/antex_loader.h"
#include <algorithm>
#include <cctype>
#include <fstream>
#include <iomanip>
#include <regex>
#include <sstream>
#include "lupnt/conversions/time_conversions.h"
#include "lupnt/core/error.h"
#include "lupnt/core/file.h"
#include "lupnt/data/kernels.h"
namespace lupnt {
namespace {
constexpr int kLabelCol = 60; // ANTEX label starts at column 61 (1-based)
std::string GetLabel(const std::string& line) {
if (line.size() <= kLabelCol) return "";
std::string label = line.substr(kLabelCol);
// rstrip
label.erase(label.find_last_not_of(" \t\r\n") + 1);
return label;
}
std::string GetData(const std::string& line) {
return line.substr(0, std::min(line.size(), static_cast<size_t>(kLabelCol)));
}
std::string ToUpper(const std::string& s) {
std::string out = s;
std::transform(out.begin(), out.end(), out.begin(),
[](unsigned char c) { return std::toupper(c); });
return out;
}
std::string Trim(const std::string& s) {
size_t b = s.find_first_not_of(" \t\r\n");
if (b == std::string::npos) return "";
size_t e = s.find_last_not_of(" \t\r\n");
return s.substr(b, e - b + 1);
}
bool ParseAntexDatetimeTai(const std::string& data, double& t_tai) {
std::istringstream ss(data);
int y, mo, d;
int hh = 0, mm = 0;
double ssec = 0.0;
if (!(ss >> y >> mo >> d)) return false;
ss >> hh >> mm >> ssec; // optional fields default to 0
Real t = ConvertTime(GregorianToTime(y, mo, d, hh, mm, ssec), Time::GPS, Time::TAI);
t_tai = t.val();
return true;
}
bool LooksLikePrnToken(const std::string& tok) {
static const std::regex re("^[A-Z][0-9]{2}$");
return std::regex_match(tok, re);
}
inline Vec3d ToVec3d(const Vec3& v) { return Vec3d(v(0).val(), v(1).val(), v(2).val()); }
} // namespace
// Constructors ***************************************************************
AntexLoader::AntexLoader(const std::filesystem::path& filepath) { LoadFile(filepath); }
// Loading ********************************************************************
void AntexLoader::LoadFile(const std::filesystem::path& filepath) { ParseFile(filepath); }
void AntexLoader::ParseFile(const std::filesystem::path& filepath) {
LUPNT_CHECK(std::filesystem::exists(filepath), "ANTEX file not found: " + filepath.string(),
"AntexLoader");
std::ifstream file = OpenFile<std::ifstream>(filepath);
SatAntennaEntry cur_entry;
bool have_entry = false;
std::string cur_freq;
bool have_freq = false;
std::string raw;
while (std::getline(file, raw)) {
// strip trailing CR (Windows line endings)
if (!raw.empty() && raw.back() == '\r') raw.pop_back();
std::string label = GetLabel(raw);
std::string data = GetData(raw);
if (label == "START OF ANTENNA") {
cur_entry = SatAntennaEntry{};
have_entry = true;
cur_freq.clear();
have_freq = false;
continue;
}
if (label == "END OF ANTENNA") {
if (have_entry && !cur_entry.sat_id.empty()) {
sat_index_[cur_entry.sat_id].push_back(cur_entry);
}
have_entry = false;
cur_freq.clear();
have_freq = false;
continue;
}
if (!have_entry) continue;
if (label == "TYPE / SERIAL NO") {
std::istringstream ss(data);
std::string tok;
std::string prn_token;
while (ss >> tok) {
if (LooksLikePrnToken(tok)) {
prn_token = tok;
break;
}
}
cur_entry.sat_id = prn_token; // empty -> receiver block, not indexed
continue;
}
if (label == "VALID FROM") {
double t;
if (ParseAntexDatetimeTai(data, t)) {
cur_entry.has_valid_from = true;
cur_entry.valid_from_tai = t;
}
continue;
}
if (label == "VALID UNTIL") {
double t;
if (ParseAntexDatetimeTai(data, t)) {
cur_entry.has_valid_until = true;
cur_entry.valid_until_tai = t;
}
continue;
}
if (label == "START OF FREQUENCY") {
if (cur_entry.sat_id.empty()) {
have_freq = false;
continue;
}
std::istringstream ss(data);
std::string tok;
if (!(ss >> tok)) continue;
cur_freq = Trim(tok);
have_freq = true;
cur_entry.freqs.emplace(cur_freq, FreqPattern{cur_freq});
continue;
}
if (label == "END OF FREQUENCY") {
cur_freq.clear();
have_freq = false;
continue;
}
if (cur_entry.sat_id.empty() || !have_freq) continue;
if (label == "NORTH / EAST / UP") {
std::istringstream ss(data);
double n, e, u;
if (ss >> n >> e >> u) {
FreqPattern& pat = cur_entry.freqs.at(cur_freq);
pat.has_pco = true;
// ANTEX stores PCO in millimeters
pat.pco_neu_m = Vec3d(n, e, u) / 1000.0;
}
continue;
}
}
}
// Queries ********************************************************************
std::string AntexLoader::GnssLetter(GnssConst gnss_const) {
switch (gnss_const) {
case GnssConst::GPS: return "G";
case GnssConst::GLONASS: return "R";
case GnssConst::GALILEO: return "E";
case GnssConst::BEIDOU: return "C";
case GnssConst::QZSS: return "J";
default: return "G";
}
}
std::string AntexLoader::SatId(GnssConst gnss_const, int prn) {
std::ostringstream ss;
ss << GnssLetter(gnss_const) << std::setfill('0') << std::setw(2) << prn;
return ss.str();
}
std::string AntexLoader::FreqToAntexCode(const std::string& gnss_letter,
const std::string& freq_name) {
std::string c = ToUpper(gnss_letter);
std::string f = ToUpper(freq_name);
if (c == "G") {
if (f == "L1") return "G01";
if (f == "L2") return "G02";
if (f == "L5") return "G05";
} else if (c == "E") {
if (f == "E1") return "E01";
if (f == "E5A" || f == "E5a") return "E05";
if (f == "E5B") return "E07";
if (f == "E5") return "E08";
if (f == "E6") return "E06";
} else if (c == "R") {
if (f == "G1") return "R01";
if (f == "G2") return "R02";
if (f == "G3") return "R03";
} else if (c == "J") {
if (f == "L1") return "J01";
if (f == "L2") return "J02";
if (f == "L5") return "J05";
if (f == "L6") return "J06";
} else if (c == "C") {
if (f == "B1I") return "C02";
if (f == "B1C") return "C01";
if (f == "B2A") return "C05";
if (f == "B2I") return "C07";
if (f == "B3I") return "C06";
}
return f; // fallback: assume `freq_name` is already an ANTEX code
}
bool AntexLoader::HasSatellite(GnssConst gnss_const, int prn) const {
return sat_index_.find(SatId(gnss_const, prn)) != sat_index_.end();
}
const AntexLoader::SatAntennaEntry& AntexLoader::SelectEntry(const std::string& sat_id,
double t_tai) const {
auto it = sat_index_.find(sat_id);
LUPNT_CHECK(it != sat_index_.end(), "Satellite '" + sat_id + "' not found in ANTEX file",
"AntexLoader");
const SatAntennaEntry* best = nullptr;
for (const auto& e : it->second) {
bool ok_from = !e.has_valid_from || t_tai >= e.valid_from_tai;
bool ok_until = !e.has_valid_until || t_tai <= e.valid_until_tai;
if (ok_from && ok_until) {
// Prefer the entry with the latest `valid_from` (mirrors `_select_entry`'s
// descending sort by `valid_from`).
if (best == nullptr
|| (e.has_valid_from
&& (!best->has_valid_from || e.valid_from_tai > best->valid_from_tai))) {
best = &e;
}
}
}
LUPNT_CHECK(best != nullptr,
"No valid ANTEX entry for satellite '" + sat_id + "' at the requested epoch",
"AntexLoader");
return *best;
}
std::vector<std::string> AntexLoader::GetAvailableFreqCodes(GnssConst gnss_const, int prn,
Real t_tai) const {
const SatAntennaEntry& entry = SelectEntry(SatId(gnss_const, prn), t_tai.val());
std::vector<std::string> codes;
codes.reserve(entry.freqs.size());
for (const auto& [code, _] : entry.freqs) codes.push_back(code);
std::sort(codes.begin(), codes.end());
return codes;
}
Vec3d AntexLoader::GetPco(const std::string& gnss_letter, int prn,
const std::string& antex_freq_code, Real t_tai) const {
std::ostringstream ss;
ss << ToUpper(gnss_letter) << std::setfill('0') << std::setw(2) << prn;
std::string sat_id = ss.str();
const SatAntennaEntry& entry = SelectEntry(sat_id, t_tai.val());
std::string code = ToUpper(antex_freq_code);
auto it = entry.freqs.find(code);
LUPNT_CHECK(it != entry.freqs.end() && it->second.has_pco,
"PCO not found for satellite '" + sat_id + "', frequency code '" + code + "'",
"AntexLoader");
return it->second.pco_neu_m;
}
Vec3d AntexLoader::GetPco(GnssConst gnss_const, int prn, GnssFreq freq, Real t_tai) const {
std::string gnss_letter = GnssLetter(gnss_const);
std::string freq_name = ToUpper(std::string(enum_name(freq)));
std::string code = FreqToAntexCode(gnss_letter, freq_name);
return GetPco(gnss_letter, prn, code, t_tai);
}
// PCO -> ECEF correction *****************************************************
Mat3d AntexLoader::ComputeIjkToEcefRotation(Real t_tai, const Vec3d& r_sat_ecef) {
constexpr double kEps = 1e-12;
Vec3d kvec = -r_sat_ecef.normalized();
LUPNT_CHECK(r_sat_ecef.norm() > kEps, "Cannot normalize near-zero satellite position",
"AntexLoader");
Real t_tdb = ConvertTime(t_tai, Time::TAI, Time::TDB);
Vec3d r_sun_ecef = ToVec3d(GetBodyPos(t_tdb, BodyId::EARTH, BodyId::SUN, Frame::ECEF));
Vec3d to_sun = r_sun_ecef - r_sat_ecef;
LUPNT_CHECK(to_sun.norm() > kEps, "Cannot normalize near-zero Sun direction", "AntexLoader");
Vec3d jvec = to_sun.normalized();
Vec3d ivec = jvec.cross(kvec);
Mat3d Cijk;
Cijk.col(0) = ivec;
Cijk.col(1) = jvec;
Cijk.col(2) = kvec;
return Cijk;
}
Vec3d AntexLoader::ApplyPcoCorrectionEcef(Real t_tai, const Vec3d& pos_sp3_ecef,
const Vec3d& pco_neu_m) {
Mat3d Cijk = ComputeIjkToEcefRotation(t_tai, pos_sp3_ecef);
Vec3d pco_ecef = Cijk * pco_neu_m;
return pos_sp3_ecef + pco_ecef;
}
} // namespace lupnt