INSTINCT Code Coverage Report


Directory: src/
File: NodeData/GNSS/RtkSolution.cpp
Date: 2025-11-25 23:34:18
Exec Total Coverage
Lines: 0 348 0.0%
Functions: 0 38 0.0%
Branches: 0 791 0.0%

Line Branch Exec Source
1 // This file is part of INSTINCT, the INS Toolkit for Integrated
2 // Navigation Concepts and Training by the Institute of Navigation of
3 // the University of Stuttgart, Germany.
4 //
5 // This Source Code Form is subject to the terms of the Mozilla Public
6 // License, v. 2.0. If a copy of the MPL was not distributed with this
7 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
8
9 /// @file RtkSolution.cpp
10 /// @brief RTK Solution data
11 /// @author T. Topp (topp@ins.uni-stuttgart.de)
12 /// @date 2024-07-17
13
14 #include "RtkSolution.hpp"
15 #include <algorithm>
16 #include <cstddef>
17 #include <imgui.h>
18 #include <numeric>
19 #include <set>
20 #include <string>
21 #include <unordered_set>
22 #include <utility>
23 #include <variant>
24 #include "Navigation/GNSS/Ambiguity/CycleSlipDetector.hpp"
25 #include "Navigation/GNSS/Core/Code.hpp"
26 #include "Navigation/GNSS/Core/Frequency.hpp"
27 #include "Navigation/GNSS/Core/SatelliteIdentifier.hpp"
28 #include "Navigation/GNSS/Core/SatelliteSystem.hpp"
29 #include "Navigation/GNSS/Positioning/RTK/Keys.hpp"
30 #include "Navigation/Transformations/Units.hpp"
31 #include "NodeData/GNSS/GnssObs.hpp"
32 #include <fmt/core.h>
33
34 void NAV::RtkSolution::guiTooltipSatellites(const std::map<SatelliteSystem, std::unordered_set<SatId>>& satsReceived, const char* id) const
35 {
36 if (ImGui::BeginTable(fmt::format("Satellites {}", id).c_str(), 2,
37 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit
38 | ImGuiTableFlags_NoHostExtendX))
39 {
40 for (const auto& [satSys, satsRecv] : satsReceived)
41 {
42 ImGui::TableNextColumn();
43 ImGui::TextUnformatted(fmt::format("{:<4}", satSys).c_str());
44 ImGui::SameLine();
45 ImGui::TextUnformatted(fmt::format("(# {:2})", satsRecv.size()).c_str());
46
47 ImGui::TableNextColumn();
48 size_t printed = 0;
49 for (const SatId& satId : satsRecv)
50 {
51 if (satId.satSys != satSys) { continue; }
52
53 bool sameLine = printed != 0 && (printed % 7) != 0;
54 if (sameLine)
55 {
56 ImGui::SameLine();
57 ImGui::SetCursorPosX(ImGui::GetCursorPosX() - ImGui::GetStyle().ItemSpacing.x);
58 ImGui::TextUnformatted(", ");
59 ImGui::SameLine();
60 ImGui::SetCursorPosX(ImGui::GetCursorPosX() - ImGui::GetStyle().ItemSpacing.x);
61 }
62 auto sat = std::ranges::find_if(satData, [&](const std::pair<SatId, RtkSolution::SatData>& satData) {
63 return satData.first == satId;
64 });
65 bool isUnused = std::ranges::find_if(observableUsed, [&](const Observable& used) {
66 return satId == used.satSigId.toSatId();
67 })
68 == observableUsed.end();
69 bool isFiltered = std::ranges::find_if(observableFiltered, [&](const Observable& used) {
70 return satId == used.satSigId.toSatId();
71 })
72 == observableFiltered.end();
73
74 if (isFiltered) { ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); } // Gray
75 else if (isUnused) { ImGui::PushStyleColor(ImGuiCol_Text, ImColor(115, 147, 179).Value); } // Blue Gray
76
77 ImGui::TextUnformatted(fmt::format("{} ({:2.0f}°)", satId, rad2deg(sat->second.satElevation)).c_str());
78
79 if (isFiltered || isUnused) { ImGui::PopStyleColor(); }
80
81 if (ImGui::IsItemHovered())
82 {
83 if (isFiltered)
84 {
85 ImGui::BeginTooltip();
86 if (std::ranges::any_of(filtered.frequencyFilter,
87 [&satId](const SatSigId& satSigId) {
88 return satSigId.toSatId() == satId;
89 })) { ImGui::TextUnformatted("Signals excluded due to Frequency filter"); }
90 if (std::ranges::any_of(filtered.codeFilter,
91 [&satId](const SatSigId& satSigId) {
92 return satSigId.toSatId() == satId;
93 })) { ImGui::TextUnformatted("Signals excluded due to Code filter"); }
94 if (std::ranges::any_of(filtered.excludedSatellites,
95 [&satId](const SatSigId& satSigId) {
96 return satSigId.toSatId() == satId;
97 })) { ImGui::TextUnformatted("Satellite excluded due to satellite exclusion list"); }
98 if (std::ranges::any_of(filtered.tempExcludedSignal,
99 [&satId](const SatSigId& satSigId) {
100 return satSigId.toSatId() == satId;
101 })) { ImGui::TextUnformatted("Signals temporarily excluded"); }
102 if (std::ranges::any_of(filtered.notAllReceiversObserved,
103 [&satId](const SatSigId& satSigId) {
104 return satSigId.toSatId() == satId;
105 })) { ImGui::TextUnformatted("Signals not observed by all receivers"); }
106 if (std::ranges::any_of(filtered.singleObservation,
107 [&satId](const SatSigId& satSigId) {
108 return satSigId.toSatId() == satId;
109 })) { ImGui::TextUnformatted("No second signal for double difference."); }
110 if (std::ranges::any_of(filtered.noPseudorangeMeasurement,
111 [&satId](const SatSigId& satSigId) {
112 return satSigId.toSatId() == satId;
113 })) { ImGui::TextUnformatted("Signals without pseudorange measurement"); }
114 if (std::ranges::any_of(filtered.navigationDataMissing,
115 [&satId](const SatSigId& satSigId) {
116 return satSigId.toSatId() == satId;
117 })) { ImGui::TextUnformatted("Satellite without navigation data"); }
118 if (std::ranges::any_of(filtered.elevationMaskTriggered,
119 [&satId](const std::pair<SatSigId, double>& satSigId) {
120 return satSigId.first.toSatId() == satId;
121 })) { ImGui::TextUnformatted("Satellite triggered elevation mask"); }
122 if (std::ranges::any_of(filtered.snrMaskTriggered,
123 [&satId](const std::pair<SatSigId, double>& satSigId) {
124 return satSigId.first.toSatId() == satId;
125 })) { ImGui::TextUnformatted("Signals triggered SNR mask"); }
126 ImGui::EndTooltip();
127 }
128 else if (isUnused) { ImGui::SetTooltip("Removed due to outlier check"); }
129 }
130
131 printed++;
132 }
133 }
134
135 ImGui::EndTable();
136 }
137 }
138
139 void NAV::RtkSolution::guiTooltipObservationTable(const std::multiset<RtkSolution::Observable>& observables,
140 bool showSatCounts,
141 bool colorPivots,
142 bool colorNotUsed,
143 bool colorCycleSlips,
144 bool colorPivotChanges,
145 const char* id) const
146 {
147 std::array<bool, GnssObs::ObservationType_COUNT> hasObsType{};
148 std::map<SatelliteSystem, size_t> obsCount;
149 std::map<SatelliteSystem, std::set<SatId>> satellites;
150 for (const RtkSolution::Observable& obs : observables)
151 {
152 hasObsType.at(obs.obsType) = true;
153 auto satId = obs.satSigId.toSatId();
154 obsCount[satId.satSys]++;
155 satellites[satId.satSys].insert(satId);
156 }
157 auto obsTypeCount = static_cast<int>(std::ranges::count(hasObsType, true));
158
159 if (ImGui::BeginTable(fmt::format("Pivot sats {}", id).c_str(), obsTypeCount + 1,
160 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit
161 | ImGuiTableFlags_NoHostExtendX))
162 {
163 ImGui::TableSetupColumn("");
164 auto headerColumn = [&](const char* desc, const GnssObs::ObservationType& obsType) {
165 if (hasObsType.at(obsType))
166 {
167 if (showSatCounts)
168 {
169 ImGui::TableSetupColumn(fmt::format("{} #{} ({} sats)", desc, nObservations.at(obsType),
170 nObservationsUniqueSatellite.at(obsType))
171 .c_str());
172 }
173 else { ImGui::TableSetupColumn(desc); }
174 }
175 };
176 headerColumn("Pseudorange", GnssObs::Pseudorange);
177 headerColumn("Carrier", GnssObs::Carrier);
178 headerColumn("Doppler", GnssObs::Doppler);
179
180 ImGui::TableHeadersRow();
181
182 for (const auto& [satSys, sats] : satellites)
183 {
184 ImGui::TableNextRow();
185 ImGui::TableNextColumn();
186 ImGui::TextUnformatted(fmt::format("{}", satSys).c_str());
187
188 if (showSatCounts)
189 {
190 ImGui::TextUnformatted(fmt::format("#{:3}", obsCount.at(satSys)).c_str());
191 if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Number of signals"); }
192
193 ImGui::TextUnformatted(fmt::format("S{:3}", sats.size()).c_str());
194 if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Number of satellites"); }
195 }
196
197 for (size_t o = 0; o < GnssObs::ObservationType_COUNT; o++)
198 {
199 if (!hasObsType.at(o)) { continue; }
200 ImGui::TableSetColumnIndex(static_cast<int>(o + 1));
201
202 size_t printed = 0;
203 Frequency lastFreq = Freq_None;
204 for (const auto& code : Code::GetAll())
205 {
206 if (code.getFrequency().getSatSys() != satSys) { continue; }
207
208 for (const RtkSolution::Observable& obs : observables)
209 {
210 if (obs.satSigId.code != code || obs.obsType != o) { continue; }
211
212 bool sameLine = printed != 0 && (printed % 2) != 0;
213 if (sameLine)
214 {
215 ImGui::SameLine();
216 ImGui::SetCursorPosX(ImGui::GetCursorPosX() - ImGui::GetStyle().ItemSpacing.x);
217 if (lastFreq != code.getFrequency()) { ImGui::PushStyleColor(ImGuiCol_Text, ImColor(243, 156, 18).Value); } // Orange
218 ImGui::TextUnformatted(", ");
219 if (lastFreq != code.getFrequency()) { ImGui::PopStyleColor(); }
220 ImGui::SameLine();
221 ImGui::SetCursorPosX(ImGui::GetCursorPosX() - ImGui::GetStyle().ItemSpacing.x);
222 }
223 auto sat = std::ranges::find_if(satData, [&](const std::pair<SatId, RtkSolution::SatData>& satData) {
224 return satData.first == obs.satSigId.toSatId();
225 });
226
227 bool isPivot = std::ranges::find_if(pivots, [&](const Observable& piv) {
228 return piv.satSigId == obs.satSigId && piv.obsType == obs.obsType;
229 })
230 != pivots.end();
231 bool isFiltered = std::ranges::find_if(observableFiltered, [&](const Observable& used) {
232 return obs.satSigId == used.satSigId && obs.obsType == used.obsType;
233 })
234 == observableFiltered.end();
235 bool isUnused = std::ranges::find_if(observableUsed, [&](const Observable& used) {
236 return obs.satSigId == used.satSigId && obs.obsType == used.obsType;
237 })
238 == observableUsed.end();
239 bool isNewlyEstimated = std::ranges::find_if(newEstimatedAmbiguity, [&](const SatSigId& satSigId) {
240 return obs.satSigId == satSigId && obs.obsType == GnssObs::Carrier;
241 })
242 != newEstimatedAmbiguity.end();
243
244 auto cycleSlip = std::ranges::find_if(cycleSlipDetectorResult,
245 [&obs](const std::pair<CycleSlipDetector::Result, std::string>& cycleSlip) {
246 if (const auto* s = std::get_if<CycleSlipDetector::CycleSlipLossOfLockIndicator>(&cycleSlip.first))
247 {
248 return s->signal == obs.satSigId;
249 }
250 if (const auto* s = std::get_if<CycleSlipDetector::CycleSlipSingleFrequency>(&cycleSlip.first))
251 {
252 return s->signal == obs.satSigId;
253 }
254 if (const auto* s = std::get_if<CycleSlipDetector::CycleSlipDualFrequency>(&cycleSlip.first))
255 {
256 return s->signals.front() == obs.satSigId || s->signals.back() == obs.satSigId;
257 }
258 return false;
259 });
260 bool isCycleSlip = obs.obsType == GnssObs::Carrier && cycleSlip != cycleSlipDetectorResult.end();
261
262 bool pivChanged = isPivot && changedPivotSatellites.contains(std::make_pair(code, obs.obsType));
263
264 if (colorNotUsed && isFiltered) { ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); } // Gray
265 else if (colorNotUsed && isUnused) { ImGui::PushStyleColor(ImGuiCol_Text, ImColor(115, 147, 179).Value); } // Blue Gray
266 else if (colorCycleSlips && isCycleSlip) { ImGui::PushStyleColor(ImGuiCol_Text, ImColor(255, 191, 0).Value); } // Yellow
267 else if (colorCycleSlips && isNewlyEstimated) { ImGui::PushStyleColor(ImGuiCol_Text, ImColor(245, 245, 220).Value); } // Beige
268 else if (colorPivotChanges && pivChanged) { ImGui::PushStyleColor(ImGuiCol_Text, ImColor(255, 191, 0).Value); } // Yellow
269 else if (colorPivots && isPivot) { ImGui::PushStyleColor(ImGuiCol_Text, ImColor(80, 200, 120).Value); } // Green
270
271 ImGui::TextUnformatted(fmt::format("{} ({:2.0f}°)", obs.satSigId, rad2deg(sat->second.satElevation)).c_str());
272
273 if ((colorPivots && isPivot)
274 || (colorNotUsed && isFiltered)
275 || (colorNotUsed && isUnused)
276 || (colorCycleSlips && (isCycleSlip || isNewlyEstimated))
277 || (colorPivotChanges && pivChanged)) { ImGui::PopStyleColor(); }
278
279 if (ImGui::IsItemHovered())
280 {
281 if (isPivot && !colorPivotChanges)
282 {
283 ImGui::BeginTooltip();
284 ImGui::TextUnformatted("Pivot");
285 ImGui::EndTooltip();
286 }
287 if (isFiltered)
288 {
289 ImGui::BeginTooltip();
290 if (std::ranges::any_of(filtered.frequencyFilter,
291 [&obs](const SatSigId& satSigId) {
292 return satSigId == obs.satSigId;
293 })) { ImGui::TextUnformatted("Signal excluded due to Frequency filter"); }
294 else if (std::ranges::any_of(filtered.codeFilter,
295 [&obs](const SatSigId& satSigId) {
296 return satSigId == obs.satSigId;
297 })) { ImGui::TextUnformatted("Signal excluded due to Code filter"); }
298 else if (std::ranges::any_of(filtered.excludedSatellites,
299 [&obs](const SatSigId& satSigId) {
300 return satSigId == obs.satSigId;
301 })) { ImGui::TextUnformatted("Signal excluded due to satellite exclusion list"); }
302 else if (std::ranges::any_of(filtered.tempExcludedSignal,
303 [&obs](const SatSigId& satSigId) {
304 return satSigId == obs.satSigId;
305 })) { ImGui::TextUnformatted("Signal temporarily excluded"); }
306 else if (std::ranges::any_of(filtered.notAllReceiversObserved,
307 [&obs](const SatSigId& satSigId) {
308 return satSigId == obs.satSigId;
309 })) { ImGui::TextUnformatted("Signal not observed by all receivers"); }
310 else if (std::ranges::any_of(filtered.singleObservation,
311 [&obs](const SatSigId& satSigId) {
312 return satSigId == obs.satSigId;
313 })) { ImGui::TextUnformatted("No second signal for double difference."); }
314 else if (std::ranges::any_of(filtered.noPseudorangeMeasurement,
315 [&obs](const SatSigId& satSigId) {
316 return satSigId == obs.satSigId;
317 })) { ImGui::TextUnformatted("Signal excluded because no pseudorange measurement to calculate satellite position"); }
318 else if (std::ranges::any_of(filtered.navigationDataMissing,
319 [&obs](const SatSigId& satSigId) {
320 return satSigId == obs.satSigId;
321 })) { ImGui::TextUnformatted("Signal excluded because no navigation data"); }
322 else if (auto sig = std::ranges::find_if(filtered.elevationMaskTriggered,
323 [&obs](const std::pair<SatSigId, double>& satSigId) {
324 return satSigId.first == obs.satSigId;
325 });
326 sig != filtered.elevationMaskTriggered.end())
327 {
328 ImGui::TextUnformatted(fmt::format("Satellite triggered elevation mask (elevation {:2.0f}°)", rad2deg(sig->second)).c_str());
329 }
330 else if (auto sig = std::ranges::find_if(filtered.snrMaskTriggered,
331 [&obs](const std::pair<SatSigId, double>& satSigId) {
332 return satSigId.first == obs.satSigId;
333 });
334 sig != filtered.snrMaskTriggered.end())
335 {
336 ImGui::TextUnformatted(fmt::format("Signal triggered SNR mask (SNR {:.0f} [dBHz])", sig->second).c_str());
337 }
338 ImGui::EndTooltip();
339 }
340 else if (isUnused)
341 {
342 if (auto outlier = std::ranges::find_if(outliers, [&obs](const Outlier& outlier) {
343 return outlier.satSigId == obs.satSigId && outlier.obsType == obs.obsType;
344 });
345 outlier != outliers.end())
346 {
347 ImGui::BeginTooltip();
348 ImGui::TextUnformatted(fmt::format("Outlier: {}", outlier->type).c_str());
349 ImGui::EndTooltip();
350 }
351 else
352 {
353 ImGui::BeginTooltip();
354 ImGui::TextUnformatted("Cannot calc double difference with single observation");
355 ImGui::EndTooltip();
356 }
357 }
358 if (isCycleSlip)
359 {
360 ImGui::BeginTooltip();
361 ImGui::TextUnformatted(fmt::format("{}: {}", cycleSlip->second, cycleSlip->first).c_str());
362 ImGui::EndTooltip();
363 }
364 else if (isNewlyEstimated)
365 {
366 ImGui::BeginTooltip();
367 ImGui::TextUnformatted("Signal is newly estimated this epoch");
368 ImGui::EndTooltip();
369 }
370 if (pivChanged)
371 {
372 ImGui::BeginTooltip();
373 ImGui::TextUnformatted(fmt::format("{}", changedPivotSatellites.at(std::make_pair(code, obs.obsType))).c_str());
374 ImGui::EndTooltip();
375 }
376 RTK::Meas::MeasKeyTypes key;
377 switch (obs.obsType)
378 {
379 case GnssObs::Pseudorange:
380 key = RTK::Meas::PsrDD{ obs.satSigId };
381 break;
382 case GnssObs::Carrier:
383 key = RTK::Meas::CarrierDD{ obs.satSigId };
384 break;
385 case GnssObs::Doppler:
386 key = RTK::Meas::DopplerDD{ obs.satSigId };
387 break;
388 case GnssObs::ObservationType_COUNT:
389 break;
390 }
391 if (measInnovation.hasRow(key))
392 {
393 ImGui::BeginTooltip();
394 ImGui::TextUnformatted(fmt::format("Meas. innovation: {:.2g}", measInnovation(key)).c_str());
395 ImGui::EndTooltip();
396 }
397 }
398 printed++;
399 lastFreq = code.getFrequency();
400 }
401 }
402 }
403 }
404
405 ImGui::EndTable();
406 }
407 }
408
409 void NAV::RtkSolution::guiTooltipAmbiguities(const char* id) const
410 {
411 std::set<SatSigId> pivotSats;
412 for (const auto& amb : ambiguityDD_br)
413 {
414 pivotSats.insert(amb.pivotSatSigId);
415 }
416
417 if (ImGui::BeginTable(fmt::format("Ambiguities {}", id).c_str(), static_cast<int>(pivotSats.size()),
418 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit
419 | ImGuiTableFlags_NoHostExtendX))
420 {
421 for (const auto& pivotSatSigId : pivotSats)
422 {
423 ImGui::TableSetupColumn(fmt::format("{}", pivotSatSigId).c_str());
424 }
425 ImGui::TableHeadersRow();
426
427 bool addedEntry = true;
428 for (size_t row = 0; addedEntry; row++)
429 {
430 addedEntry = false;
431 int p = 0;
432 for (const auto& pivotSatSigId : pivotSats)
433 {
434 size_t i = 0;
435 for (const auto& amb : ambiguityDD_br)
436 {
437 if (amb.pivotSatSigId != pivotSatSigId) { continue; }
438 if (i++ < row) { continue; } // Skip entries to be in the correct row
439
440 if (!addedEntry) { ImGui::TableNextRow(); }
441 ImGui::TableSetColumnIndex(p);
442 ImGui::TextUnformatted(fmt::format("{}", amb.satSigId).c_str());
443
444 ImGui::TextUnformatted(fmt::format("V {:7.1f}", amb.value.value).c_str());
445 if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Value [cycles]"); }
446
447 ImGui::TextUnformatted(fmt::format("S {:7.1e}", amb.value.stdDev).c_str());
448 if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Standard deviation [cycles]"); }
449
450 addedEntry = true;
451 break;
452 }
453 p++;
454 }
455 }
456 ImGui::EndTable();
457 }
458 }
459
460 void NAV::RtkSolution::guiTooltip(bool detailView, bool firstOpen, const char* /* displayName */, const char* id, int* /* rootWindow */) const
461 {
462 if (nAmbiguitiesFixed && *nAmbiguitiesFixed - 1 < ambiguityDD_br.size())
463 {
464 ImGui::BulletText("%s", fmt::format("Solution Type: {}Partial Fix {} / {}",
465 solType == SolutionType::RTK_Float ? "" : fmt::format("{} - ", solType),
466 *nAmbiguitiesFixed - 1,
467 ambiguityDD_br.size())
468 .c_str());
469 }
470 else
471 {
472 ImGui::BulletText("%s", fmt::format("Solution Type: {}", solType).c_str());
473 }
474
475 ImGui::BulletText("Rover obs time: %s", fmt::format("{}", insTime.toYMDHMS(GPST)).c_str());
476 ImGui::BulletText("Base obs time: %s", fmt::format("{}", baseTime.toYMDHMS(GPST)).c_str());
477
478 std::map<SatelliteSystem, std::unordered_set<SatId>> satsReceived;
479 for (const auto& obs : observableReceived)
480 {
481 satsReceived[obs.satSigId.toSatId().satSys].insert(obs.satSigId.toSatId());
482 }
483 size_t nSatellitesReceived = 0;
484 for (const auto& satRecv : satsReceived) { nSatellitesReceived += satRecv.second.size(); }
485
486 ImGui::SetNextItemOpen(detailView, firstOpen ? ImGuiCond_Always : ImGuiCond_Once);
487 if (ImGui::TreeNode(fmt::format("Satellites: used {:3d} / {:3d} received", nSatellites, nSatellitesReceived).c_str()))
488 {
489 guiTooltipSatellites(satsReceived, id);
490 ImGui::TreePop();
491 }
492
493 ImGui::SetNextItemOpen(detailView, firstOpen ? ImGuiCond_Always : ImGuiCond_Once);
494 if (ImGui::TreeNode(fmt::format("Observables: used {:3d} / {:3d} received", observableUsed.size(), observableReceived.size()).c_str()))
495 {
496 guiTooltipObservationTable(observableReceived, true, true, true, true, false, id);
497 ImGui::TreePop();
498 }
499
500 if (ImGui::TreeNode(fmt::format("Pivots: {} ({} changed)", pivots.size(), changedPivotSatellites.size()).c_str()))
501 {
502 guiTooltipObservationTable(pivots, false, false, true, false, true, id);
503 ImGui::TreePop();
504 }
505
506 if (nisResultInitial)
507 {
508 if (nisResultInitial->triggered)
509 {
510 ImGui::SetNextItemOpen(detailView, firstOpen ? ImGuiCond_Always : ImGuiCond_Once);
511 if (ImGui::TreeNode(fmt::format("NIS Outlier check: {} observables removed", nisRemovedCnt).c_str()))
512 {
513 ImGui::BulletText("%s", fmt::format("Initial NIS {:.2g} > {:.2g} r2", nisResultInitial->NIS, nisResultInitial->r2).c_str());
514 ImGui::BulletText("%s", fmt::format("Final NIS {:.2g} {} {:.2g} r2",
515 nisResultFinal->NIS, nisResultFinal->NIS < nisResultFinal->r2 ? "<" : ">", nisResultFinal->r2)
516 .c_str());
517 ImGui::TreePop();
518 }
519 }
520 else
521 {
522 ImGui::BulletText("NIS Outlier check not triggered");
523 }
524 }
525
526 ImGui::SetNextItemOpen(detailView, firstOpen ? ImGuiCond_Always : ImGuiCond_Once);
527 if (ImGui::TreeNode(fmt::format("Measurement innovation: Largest {:.2g}, Mean {:.2g}",
528 measInnovation.rows() ? measInnovation(all).cwiseAbs().maxCoeff() : std::nan(""),
529 measInnovation.rows() ? measInnovation(all).mean() : std::nan(""))
530 .c_str()))
531 {
532 auto showLargestAndMean = [&]<typename T>(const char* desc, const char* unit) {
533 if (std::any_of(measInnovation.rowKeys().begin(), measInnovation.rowKeys().end(), [](const auto& key) {
534 return std::holds_alternative<T>(key);
535 }))
536 {
537 std::vector<RTK::Meas::MeasKeyTypes> keys;
538 for (const auto& key : measInnovation.rowKeys())
539 {
540 if (std::holds_alternative<T>(key)) { keys.push_back(key); }
541 }
542 ImGui::BulletText("%s", fmt::format("{} Largest {:.2g}, Mean {:.2g} [{}]",
543 desc,
544 measInnovation(keys).rows() ? measInnovation(keys).cwiseAbs().maxCoeff() : std::nan(""),
545 measInnovation(keys).rows() ? measInnovation(keys).mean() : std::nan(""),
546 unit)
547 .c_str());
548 }
549 };
550 showLargestAndMean.operator()<RTK::Meas::PsrDD>("Pseudorange:", "m");
551 showLargestAndMean.operator()<RTK::Meas::CarrierDD>("Carrier: ", "m");
552 showLargestAndMean.operator()<RTK::Meas::DopplerDD>("Doppler: ", "m/s");
553
554 if (ImGui::TreeNode("Vector"))
555 {
556 gui::widgets::KeyedVectorView(fmt::format("Measurement innovation##{}", id).c_str(), &measInnovation, 300.0F);
557 ImGui::TreePop();
558 }
559 ImGui::TreePop();
560 }
561
562 if (ImGui::TreeNode(fmt::format("Ambiguities: {}", ambiguityDD_br.size()).c_str()))
563 {
564 guiTooltipAmbiguities(id);
565 ImGui::TreePop();
566 }
567 }
568