INSTINCT Code Coverage Report


Directory: src/
File: Nodes/DataLogger/General/CsvLogger.cpp
Date: 2025-02-07 16:54:41
Exec Total Coverage
Lines: 13 208 6.2%
Functions: 4 23 17.4%
Branches: 10 404 2.5%

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