INSTINCT Code Coverage Report


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