INSTINCT Code Coverage Report


Directory: src/
File: Nodes/DataLogger/General/CsvLogger.cpp
Date: 2025-06-02 15:19:59
Exec Total Coverage
Lines: 91 213 42.7%
Functions: 16 23 69.6%
Branches: 79 418 18.9%

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 #include "CsvLogger.hpp"
10
11 #include "NodeData/NodeData.hpp"
12
13 #include "util/Logger.hpp"
14
15 #include <iomanip> // std::setprecision
16 #include <regex>
17
18 #include "internal/NodeManager.hpp"
19 namespace nm = NAV::NodeManager;
20 #include "internal/FlowManager.hpp"
21 #include "NodeRegistry.hpp"
22 #include "internal/gui/NodeEditorApplication.hpp"
23 #include "util/StringUtil.hpp"
24
25 114 NAV::CsvLogger::CsvLogger()
26
4/8
✓ Branch 1 taken 114 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 114 times.
✗ Branch 5 not taken.
✓ Branch 8 taken 114 times.
✗ Branch 9 not taken.
✓ Branch 11 taken 114 times.
✗ Branch 12 not taken.
114 : Node(typeStatic())
27 {
28 LOG_TRACE("{}: called", name);
29
30 114 _fileType = FileType::ASCII;
31
32 114 _hasConfig = true;
33 114 _guiConfigDefaultWindowSize = { 380, 70 };
34
35
4/8
✓ Branch 1 taken 114 times.
✗ Branch 2 not taken.
✓ Branch 5 taken 114 times.
✗ Branch 6 not taken.
✓ Branch 8 taken 114 times.
✓ Branch 9 taken 114 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
342 nm::CreateInputPin(this, "writeObservation", Pin::Type::Flow,
36 { NodeData::type() },
37 &CsvLogger::writeObservation);
38 228 }
39
40 232 NAV::CsvLogger::~CsvLogger()
41 {
42 LOG_TRACE("{}: called", nameId());
43 232 }
44
45 226 std::string NAV::CsvLogger::typeStatic()
46 {
47
1/2
✓ Branch 1 taken 226 times.
✗ Branch 2 not taken.
452 return "CsvLogger";
48 }
49
50 std::string NAV::CsvLogger::type() const
51 {
52 return typeStatic();
53 }
54
55 112 std::string NAV::CsvLogger::category()
56 {
57
1/2
✓ Branch 1 taken 112 times.
✗ Branch 2 not taken.
224 return "Data Logger";
58 }
59
60 void NAV::CsvLogger::guiConfig()
61 {
62 if (FileWriter::guiConfig(".csv", { ".csv" }, size_t(id), nameId()))
63 {
64 flow::ApplyChanges();
65 doDeinitialize();
66 }
67
68 if (CommonLog::ShowOriginInput(nameId().c_str()))
69 {
70 flow::ApplyChanges();
71 }
72
73 if (ImGui::Button(fmt::format("Clear header##{}", size_t(id)).c_str()))
74 {
75 _headerLogging.clear();
76 }
77 ImGui::SameLine();
78 if (ImGui::Button(fmt::format("Select all##{}", size_t(id)).c_str()))
79 {
80 for (auto& header : _headerLogging) { header.second = true; }
81 flow::ApplyChanges();
82 }
83 ImGui::SameLine();
84 if (ImGui::Button(fmt::format("Deselect all##{}", size_t(id)).c_str()))
85 {
86 for (auto& header : _headerLogging) { header.second = false; }
87 flow::ApplyChanges();
88 }
89 ImGui::SameLine();
90 if (ImGui::Checkbox(fmt::format("Default for new##{}", size_t(id)).c_str(), &_headerLoggingDefault))
91 {
92 flow::ApplyChanges();
93 }
94 ImGui::SameLine();
95 if (ImGui::Checkbox(fmt::format("Sort headers in GUI##{}", size_t(id)).c_str(), &_headerLoggingSortGui))
96 {
97 flow::ApplyChanges();
98 }
99
100 if (_headerLoggingRegex.empty()) { ImGui::BeginDisabled(); }
101 std::optional<bool> regexSelect;
102 if (ImGui::Button(fmt::format("Select regex##{}", size_t(id)).c_str()))
103 {
104 regexSelect = true;
105 }
106 ImGui::SameLine();
107 if (ImGui::Button(fmt::format("Deselect regex##{}", size_t(id)).c_str()))
108 {
109 regexSelect = false;
110 }
111 if (regexSelect.has_value())
112 {
113 bool anyChanged = false;
114 try
115 {
116 for (auto& [desc, checked] : _headerLogging)
117 {
118 std::regex self_regex(_headerLoggingRegex,
119 std::regex_constants::ECMAScript | std::regex_constants::icase);
120 if (std::regex_search(desc, self_regex) && checked != *regexSelect)
121 {
122 anyChanged = true;
123 checked = *regexSelect;
124 }
125 }
126 }
127 catch (const std::regex_error& e)
128 {
129 LOG_ERROR("Regex could not be parsed: {}", e.what());
130 }
131 if (anyChanged)
132 {
133 flow::ApplyChanges();
134 }
135 }
136 if (_headerLoggingRegex.empty()) { ImGui::EndDisabled(); }
137 ImGui::SameLine();
138 ImGui::SetNextItemWidth(300.0F * gui::NodeEditorApplication::windowFontRatio());
139 if (ImGui::InputText(fmt::format("##Select Regex {}", size_t(id)).c_str(), &_headerLoggingRegex))
140 {
141 flow::ApplyChanges();
142 }
143
144 if (!_headerLogging.empty())
145 {
146 auto* headerLogging = &_headerLogging;
147 decltype(_headerLogging) sortedHeaderLogging;
148 if (_headerLoggingSortGui)
149 {
150 sortedHeaderLogging = _headerLogging;
151 std::ranges::sort(sortedHeaderLogging);
152 headerLogging = &sortedHeaderLogging;
153 }
154 int nCols = std::min((static_cast<int>(headerLogging->size()) - 1) / 5 + 1, 3);
155 if (ImGui::BeginChild(fmt::format("Headers Scrolling {}", size_t(id)).c_str(), ImGui::GetContentRegionAvail(), false))
156 {
157 if (ImGui::BeginTable(fmt::format("Logging headers##{}", size_t(id)).c_str(), nCols, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoHostExtendX, ImVec2(0.0F, 0.0F)))
158 {
159 for (auto& [desc, checked] : *headerLogging)
160 {
161 ImGui::TableNextColumn();
162 if (ImGui::Checkbox(fmt::format("{}##{}", desc, size_t(id)).c_str(), &checked))
163 {
164 if (_headerLoggingSortGui)
165 {
166 if (auto iter = std::ranges::find_if(_headerLogging, [&](const std::pair<std::string, bool>& header) {
167 return desc == header.first; // NOLINT(clang-analyzer-core.CallAndMessage)
168 });
169 iter != _headerLogging.end())
170 {
171 iter->second = checked;
172 }
173 }
174 flow::ApplyChanges();
175 }
176 }
177
178 ImGui::EndTable();
179 }
180 }
181 ImGui::EndChild();
182 }
183 else
184 {
185 ImGui::TextUnformatted("Please run the flow to collect information about the available data.");
186 }
187 }
188
189 [[nodiscard]] json NAV::CsvLogger::save() const
190 {
191 LOG_TRACE("{}: called", nameId());
192
193 return {
194 { "FileWriter", FileWriter::save() },
195 { "header", _headerLogging },
196 { "lastConnectedType", _lastConnectedType },
197 { "headerLoggingRegex", _headerLoggingRegex },
198 { "headerLoggingDefault", _headerLoggingDefault },
199 { "headerLoggingSortGui", _headerLoggingSortGui },
200 };
201 }
202
203 2 void NAV::CsvLogger::restore(json const& j)
204 {
205 LOG_TRACE("{}: called", nameId());
206
207
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
2 if (j.contains("FileWriter")) { FileWriter::restore(j.at("FileWriter")); }
208
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
2 if (j.contains("header")) { j.at("header").get_to(_headerLogging); }
209
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
2 if (j.contains("lastConnectedType")) { j.at("lastConnectedType").get_to(_lastConnectedType); }
210
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
2 if (j.contains("headerLoggingRegex")) { j.at("headerLoggingRegex").get_to(_headerLoggingRegex); }
211
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
2 if (j.contains("headerLoggingDefault")) { j.at("headerLoggingDefault").get_to(_headerLoggingDefault); }
212 2 }
213
214 2 void NAV::CsvLogger::flush()
215 {
216 2 _filestream.flush();
217 2 }
218
219 2 bool NAV::CsvLogger::onCreateLink([[maybe_unused]] OutputPin& startPin, [[maybe_unused]] InputPin& endPin)
220 {
221 LOG_TRACE("{}: called for {} ==> {}", nameId(), size_t(startPin.id), size_t(endPin.id));
222
223
1/2
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 if (_lastConnectedType != startPin.dataIdentifier.front())
224 {
225 LOG_DEBUG("{}: [{} ==> {}] Dropping headers because last type [{}] and new type [{}]", nameId(), size_t(startPin.id), size_t(endPin.id), _lastConnectedType, startPin.dataIdentifier.front());
226 _headerLogging.clear();
227 }
228 2 _lastConnectedType = startPin.dataIdentifier.front();
229
230 2 return true;
231 }
232
233 6 bool NAV::CsvLogger::initialize()
234 {
235 LOG_TRACE("{}: called", nameId());
236
237
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
6 if (!FileWriter::initialize())
238 {
239 return false;
240 }
241
242 6 CommonLog::initialize();
243
244 6 _headerWritten = false;
245 6 _dynamicHeader.clear();
246
247 6 return true;
248 }
249
250 2 void NAV::CsvLogger::deinitialize()
251 {
252 LOG_TRACE("{}: called", nameId());
253
254 2 FileWriter::deinitialize();
255 2 }
256
257 2 void NAV::CsvLogger::writeHeader()
258 {
259 2 _filestream << "Time [s],GpsCycle,GpsWeek,GpsToW [s]";
260
261 #if LOG_LEVEL <= LOG_LEVEL_TRACE
262 std::string headers = "Time [s],GpsCycle,GpsWeek,GpsToW [s]";
263 #endif
264
265 2 _headerLoggingCount = 0;
266
2/2
✓ Branch 7 taken 183 times.
✓ Branch 8 taken 2 times.
185 for (const auto& [desc, enabled] : _headerLogging)
267 {
268
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 183 times.
183 if (!enabled) { continue; }
269 183 _headerLoggingCount++;
270
271
4/8
✓ Branch 1 taken 183 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 183 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 183 times.
✗ Branch 8 not taken.
✓ Branch 10 taken 183 times.
✗ Branch 11 not taken.
549 auto header = str::replaceAll_copy(desc, ",", "_");
272 #if LOG_LEVEL <= LOG_LEVEL_TRACE
273 headers += "," + header;
274 #endif
275
2/4
✓ Branch 1 taken 183 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 183 times.
✗ Branch 5 not taken.
183 _filestream << "," << header;
276 183 }
277 2 _filestream << std::endl; // NOLINT(performance-avoid-endl)
278
279 #if LOG_LEVEL <= LOG_LEVEL_TRACE
280 LOG_TRACE("{}: Header written:\n{}", nameId(), headers);
281 #endif
282
283 2 _headerWritten = true;
284 2 }
285
286 void NAV::CsvLogger::rewriteData(size_t oldSize, size_t newSize)
287 {
288 LOG_TRACE("{}: Rewriting header, because {} new elements", nameId(), newSize - oldSize);
289 FileWriter::deinitialize();
290 auto tmpFilePath = getFilepath().concat("_temp");
291 std::filesystem::rename(getFilepath(), tmpFilePath);
292 FileWriter::initialize();
293 writeHeader();
294
295 std::ifstream tmpFilestream(tmpFilePath, std::ios_base::in | std::ios_base::binary);
296 if (tmpFilestream.good())
297 {
298 std::string delimiterEnd(newSize - oldSize, ',');
299 std::string line;
300 std::getline(tmpFilestream, line); // Old header
301 while (std::getline(tmpFilestream, line) && !tmpFilestream.eof())
302 {
303 _filestream << line << delimiterEnd << '\n';
304 }
305 }
306 if (tmpFilestream.is_open()) { tmpFilestream.close(); }
307 tmpFilestream.clear();
308 std::filesystem::remove(tmpFilePath);
309 }
310
311 30 void NAV::CsvLogger::writeObservation(NAV::InputPin::NodeDataQueue& queue, size_t /* pinIdx */)
312 {
313
1/2
✓ Branch 1 taken 30 times.
✗ Branch 2 not taken.
30 auto obs = queue.extract_front();
314
315
1/2
✓ Branch 1 taken 30 times.
✗ Branch 2 not taken.
2514 auto oldHeaderLength = static_cast<size_t>(std::ranges::count_if(_headerLogging, [](const auto& header) { return header.second; }));
316
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 28 times.
30 if (!_headerWritten)
317 {
318
2/4
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 2 times.
2 for (const auto& desc : obs->staticDataDescriptors())
319 {
320 if (auto iter = std::ranges::find_if(_headerLogging, [&](const std::pair<std::string, bool>& header) {
321 return desc == header.first;
322 });
323 iter == _headerLogging.end())
324 {
325 _headerLogging.emplace_back(desc, _headerLoggingDefault);
326 flow::ApplyChanges();
327 }
328 2 }
329 }
330
3/4
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 9 taken 2489 times.
✓ Branch 10 taken 30 times.
2518 for (const auto& desc : obs->dynamicDataDescriptors())
331 {
332
3/4
✓ Branch 1 taken 2488 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 183 times.
✓ Branch 4 taken 2305 times.
131558 if (std::ranges::none_of(_dynamicHeader, [&](const auto& header) { return header == desc; }))
333 {
334 LOG_DATA("{}: Adding new dynamic header: {}", nameId(), desc);
335
1/2
✓ Branch 1 taken 183 times.
✗ Branch 2 not taken.
183 _dynamicHeader.push_back(desc);
336
1/2
✓ Branch 1 taken 183 times.
✗ Branch 2 not taken.
183 if (auto iter = std::ranges::find_if(_headerLogging, [&](const std::pair<std::string, bool>& header) {
337 10250 return desc == header.first;
338 });
339
1/2
✗ Branch 2 not taken.
✓ Branch 3 taken 183 times.
183 iter == _headerLogging.end())
340 {
341 _headerLogging.emplace_back(desc, _headerLoggingDefault);
342 flow::ApplyChanges();
343 }
344 }
345 30 }
346
347
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 28 times.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
30 if (!_headerWritten) { writeHeader(); }
348
2/4
✓ Branch 1 taken 28 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 28 times.
2335 else if (auto newHeaderLength = static_cast<size_t>(std::ranges::count_if(_headerLogging, [](const auto& header) { return header.second; }));
349 oldHeaderLength != newHeaderLength)
350 {
351 rewriteData(oldHeaderLength, newHeaderLength);
352 }
353
354 30 constexpr int gpsCyclePrecision = 3;
355 30 constexpr int gpsTimePrecision = 12;
356 30 constexpr int valuePrecision = 15;
357
358
1/2
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 if (!obs->insTime.empty())
359 {
360
2/4
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 30 times.
✗ Branch 8 not taken.
30 _filestream << std::setprecision(valuePrecision) << std::round(calcTimeIntoRun(obs->insTime) * 1e9) / 1e9;
361 }
362
1/2
✓ Branch 1 taken 30 times.
✗ Branch 2 not taken.
30 _filestream << ",";
363
1/2
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 if (!obs->insTime.empty())
364 {
365
3/6
✓ Branch 1 taken 30 times.
✗ Branch 2 not taken.
✓ Branch 8 taken 30 times.
✗ Branch 9 not taken.
✓ Branch 11 taken 30 times.
✗ Branch 12 not taken.
30 _filestream << std::fixed << std::setprecision(gpsCyclePrecision) << obs->insTime.toGPSweekTow().gpsCycle;
366 }
367
1/2
✓ Branch 1 taken 30 times.
✗ Branch 2 not taken.
30 _filestream << ",";
368
1/2
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 if (!obs->insTime.empty())
369 {
370
3/6
✓ Branch 1 taken 30 times.
✗ Branch 2 not taken.
✓ Branch 8 taken 30 times.
✗ Branch 9 not taken.
✓ Branch 11 taken 30 times.
✗ Branch 12 not taken.
30 _filestream << std::defaultfloat << std::setprecision(gpsTimePrecision) << obs->insTime.toGPSweekTow().gpsWeek;
371 }
372
1/2
✓ Branch 1 taken 30 times.
✗ Branch 2 not taken.
30 _filestream << ",";
373
1/2
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 if (!obs->insTime.empty())
374 {
375
3/6
✓ Branch 1 taken 30 times.
✗ Branch 2 not taken.
✓ Branch 8 taken 30 times.
✗ Branch 9 not taken.
✓ Branch 11 taken 30 times.
✗ Branch 12 not taken.
30 _filestream << std::defaultfloat << std::setprecision(gpsTimePrecision) << obs->insTime.toGPSweekTow().tow;
376 }
377 30 _filestream << std::setprecision(valuePrecision);
378
379 30 size_t dataLogged = 0;
380
1/2
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 const auto staticDataDescriptors = obs->staticDataDescriptors();
381
2/4
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 30 times.
30 for (size_t i = 0; i < obs->staticDescriptorCount(); ++i)
382 {
383 const auto& desc = staticDataDescriptors.at(i);
384 if (auto iter = std::ranges::find_if(_headerLogging, [&](const std::pair<std::string, bool>& header) {
385 return desc == header.first;
386 });
387 iter != _headerLogging.end() && !iter->second)
388 {
389 continue;
390 }
391 dataLogged++;
392 _filestream << ',';
393 if (auto val = obs->getValueAt(i)) { _filestream << *val; }
394 }
395
396
2/2
✓ Branch 5 taken 2486 times.
✓ Branch 6 taken 30 times.
2518 for (const auto& desc : _dynamicHeader)
397 {
398
1/2
✓ Branch 1 taken 2489 times.
✗ Branch 2 not taken.
2486 if (auto iter = std::ranges::find_if(_headerLogging, [&](const std::pair<std::string, bool>& header) {
399 129179 return desc == header.first;
400 });
401
3/6
✓ Branch 2 taken 2489 times.
✗ Branch 3 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 2489 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2488 times.
2489 iter != _headerLogging.end() && !iter->second)
402 {
403 continue;
404 }
405 2488 dataLogged++;
406
1/2
✓ Branch 1 taken 2489 times.
✗ Branch 2 not taken.
2488 _filestream << ',';
407
3/6
✓ Branch 2 taken 2490 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 2487 times.
✗ Branch 6 not taken.
✓ Branch 9 taken 2488 times.
✗ Branch 10 not taken.
2489 if (auto val = obs->getDynamicDataAt(desc)) { _filestream << *val; }
408 }
409
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 30 times.
30 for (size_t i = dataLogged; i < _headerLoggingCount; i++)
410 {
411 _filestream << ',';
412 }
413
414
1/2
✓ Branch 1 taken 30 times.
✗ Branch 2 not taken.
30 _filestream << '\n';
415 30 }
416