INSTINCT Code Coverage Report


Directory: src/
File: Nodes/Plotting/Plot.cpp
Date: 2025-02-07 16:54:41
Exec Total Coverage
Lines: 170 1143 14.9%
Functions: 19 53 35.8%
Branches: 154 2386 6.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 "Plot.hpp"
10 #include <array>
11 #include <cmath>
12 #include <cstddef>
13 #include <cstdint>
14 #include <imgui.h>
15 #include <imgui_internal.h>
16 #include <implot.h>
17 #include <utility>
18
19 #include "NodeData/IMU/ImuObsWDelta.hpp"
20 #include "util/Assert.h"
21 #include "util/Container/ScrollingBuffer.hpp"
22 #include "util/Logger.hpp"
23
24 #include "internal/NodeManager.hpp"
25 #include "util/Plot/PlotTooltip.hpp"
26 #include <fmt/core.h>
27 namespace nm = NAV::NodeManager;
28 #include "internal/FlowManager.hpp"
29 #include "internal/ConfigManager.hpp"
30 #include "NodeRegistry.hpp"
31
32 #include "internal/gui/NodeEditorApplication.hpp"
33 #include "internal/gui/widgets/HelpMarker.hpp"
34 #include "internal/gui/widgets/Splitter.hpp"
35 #include "internal/gui/widgets/imgui_ex.hpp"
36 #include "internal/gui/windows/ImPlotStyleEditor.hpp"
37 #include "internal/gui/windows/Screenshotter.hpp"
38 #include "util/ImPlot.hpp"
39 #include "util/Json.hpp"
40 #include "util/StringUtil.hpp"
41 #include "util/Container/STL.hpp"
42
43 #include "util/Container/Vector.hpp"
44
45 #include "util/Time/TimeBase.hpp"
46 #include "Navigation/Ellipsoid/Ellipsoid.hpp"
47 #include "Navigation/Transformations/CoordinateFrames.hpp"
48 #include "Navigation/Transformations/Units.hpp"
49
50 #include <implot_internal.h>
51 #include <muParser.h>
52
53 #include <algorithm>
54 #include <regex>
55
56 namespace NAV
57 {
58 /// @brief Write info to a json object
59 /// @param[out] j Json output
60 /// @param[in] data Object to read info from
61 static void to_json(json& j, const Plot::PinData::PlotData& data) // NOLINT(misc-use-anonymous-namespace)
62 {
63 j = json{
64 { "displayName", data.displayName },
65 };
66 }
67 /// @brief Read info from a json object
68 /// @param[in] j Json variable to read info from
69 /// @param[out] data Output object
70 3951 static void from_json(const json& j, Plot::PinData::PlotData& data) // NOLINT(misc-use-anonymous-namespace)
71 {
72
1/2
✓ Branch 1 taken 3951 times.
✗ Branch 2 not taken.
3951 if (j.contains("displayName")) { j.at("displayName").get_to(data.displayName); }
73 3951 }
74
75 /// @brief Write info to a json object
76 /// @param[out] j Json output
77 /// @param[in] data Object to read info from
78 static void to_json(json& j, const std::vector<Plot::PinData::PlotData>& data) // NOLINT(misc-use-anonymous-namespace)
79 {
80 for (const auto& i : data)
81 {
82 if (i.isDynamic) { continue; }
83 j.push_back(i);
84 }
85 }
86
87 /// @brief Write info to a json object
88 /// @param[out] j Json output
89 /// @param[in] data Object to read info from
90 static void to_json(json& j, const Plot::PinData& data) // NOLINT(misc-use-anonymous-namespace)
91 {
92 j = json{
93 { "dataIdentifier", data.dataIdentifier },
94 { "size", data.size },
95 { "plotData", data.plotData },
96 { "pinType", data.pinType },
97 { "stride", data.stride },
98 };
99 }
100 /// @brief Read info from a json object
101 /// @param[in] j Json variable to read info from
102 /// @param[out] data Output object
103 78 static void from_json(const json& j, Plot::PinData& data) // NOLINT(misc-use-anonymous-namespace)
104 {
105
1/2
✓ Branch 1 taken 78 times.
✗ Branch 2 not taken.
78 if (j.contains("dataIdentifier")) { j.at("dataIdentifier").get_to(data.dataIdentifier); }
106
1/2
✓ Branch 1 taken 78 times.
✗ Branch 2 not taken.
78 if (j.contains("size"))
107 {
108 78 j.at("size").get_to(data.size);
109 78 data.rawNodeData.resize(static_cast<size_t>(data.size));
110 }
111
5/6
✓ Branch 1 taken 78 times.
✗ Branch 2 not taken.
✓ Branch 5 taken 74 times.
✓ Branch 6 taken 4 times.
✓ Branch 7 taken 74 times.
✓ Branch 8 taken 4 times.
78 if (j.contains("plotData") && j.at("plotData").is_array())
112 {
113 74 j.at("plotData").get_to(data.plotData);
114
2/2
✓ Branch 4 taken 3951 times.
✓ Branch 5 taken 74 times.
4025 for (auto& plotData : data.plotData)
115 {
116
1/2
✓ Branch 1 taken 3951 times.
✗ Branch 2 not taken.
3951 plotData.buffer = ScrollingBuffer<double>(static_cast<size_t>(data.size));
117 }
118 }
119
1/2
✓ Branch 1 taken 78 times.
✗ Branch 2 not taken.
78 if (j.contains("pinType")) { j.at("pinType").get_to(data.pinType); }
120
1/2
✓ Branch 1 taken 78 times.
✗ Branch 2 not taken.
78 if (j.contains("stride")) { j.at("stride").get_to(data.stride); }
121 78 }
122
123 /// @brief Write info to a json object
124 /// @param[out] j Json output
125 /// @param[in] data Object to read info from
126 static void to_json(json& j, const Plot::PlotInfo::PlotItem& data) // NOLINT(misc-use-anonymous-namespace)
127 {
128 j = json{
129 { "pinIndex", data.pinIndex },
130 { "dataIndex", data.dataIndex },
131 { "displayName", data.displayName },
132 { "axis", data.axis },
133 { "style", data.style },
134 };
135 }
136 /// @brief Read info from a json object
137 /// @param[in] j Json variable to read info from
138 /// @param[out] data Output object
139 404 static void from_json(const json& j, Plot::PlotInfo::PlotItem& data) // NOLINT(misc-use-anonymous-namespace)
140 {
141
1/2
✓ Branch 1 taken 404 times.
✗ Branch 2 not taken.
404 if (j.contains("pinIndex")) { j.at("pinIndex").get_to(data.pinIndex); }
142
1/2
✓ Branch 1 taken 404 times.
✗ Branch 2 not taken.
404 if (j.contains("dataIndex")) { j.at("dataIndex").get_to(data.dataIndex); }
143
1/2
✓ Branch 1 taken 404 times.
✗ Branch 2 not taken.
404 if (j.contains("displayName")) { j.at("displayName").get_to(data.displayName); }
144
1/2
✓ Branch 1 taken 404 times.
✗ Branch 2 not taken.
404 if (j.contains("axis")) { j.at("axis").get_to(data.axis); }
145
1/2
✓ Branch 1 taken 404 times.
✗ Branch 2 not taken.
404 if (j.contains("style")) { j.at("style").get_to(data.style); }
146 404 }
147
148 /// @brief Write info to a json object
149 /// @param[out] j Json output
150 /// @param[in] data Object to read info from
151 static void to_json(json& j, const Plot::PlotInfo& data) // NOLINT(misc-use-anonymous-namespace)
152 {
153 j = json{
154 { "size", data.size },
155 { "xAxisFlags", data.xAxisFlags },
156 { "yAxisFlags", data.yAxisFlags },
157 { "xAxisScale", data.xAxisScale },
158 { "yAxesScale", data.yAxesScale },
159 { "lineFlags", data.lineFlags },
160 { "headerText", data.headerText },
161 { "leftPaneWidth", data.leftPaneWidth },
162 { "plotFlags", data.plotFlags },
163 { "rightPaneWidth", data.rightPaneWidth },
164 { "selectedPin", data.selectedPin },
165 { "selectedXdata", data.selectedXdata },
166 { "plotItems", data.plotItems },
167 { "title", data.title },
168 { "overrideXAxisLabel", data.overrideXAxisLabel },
169 { "xAxisLabel", data.xAxisLabel },
170 { "y1AxisLabel", data.y1AxisLabel },
171 { "y2AxisLabel", data.y2AxisLabel },
172 { "y3AxisLabel", data.y3AxisLabel },
173 };
174 }
175 /// @brief Read info from a json object
176 /// @param[in] j Json variable to read info from
177 /// @param[out] data Output object
178 161 static void from_json(const json& j, Plot::PlotInfo& data) // NOLINT(misc-use-anonymous-namespace)
179 {
180
1/2
✓ Branch 1 taken 161 times.
✗ Branch 2 not taken.
161 if (j.contains("size")) { j.at("size").get_to(data.size); }
181
1/2
✓ Branch 1 taken 161 times.
✗ Branch 2 not taken.
161 if (j.contains("xAxisFlags")) { j.at("xAxisFlags").get_to(data.xAxisFlags); }
182
1/2
✓ Branch 1 taken 161 times.
✗ Branch 2 not taken.
161 if (j.contains("yAxisFlags")) { j.at("yAxisFlags").get_to(data.yAxisFlags); }
183
1/2
✓ Branch 1 taken 161 times.
✗ Branch 2 not taken.
161 if (j.contains("xAxisScale")) { j.at("xAxisScale").get_to(data.xAxisScale); }
184
1/2
✓ Branch 1 taken 161 times.
✗ Branch 2 not taken.
161 if (j.contains("yAxesScale")) { j.at("yAxesScale").get_to(data.yAxesScale); }
185
1/2
✓ Branch 1 taken 161 times.
✗ Branch 2 not taken.
161 if (j.contains("lineFlags")) { j.at("lineFlags").get_to(data.lineFlags); }
186
1/2
✓ Branch 1 taken 161 times.
✗ Branch 2 not taken.
161 if (j.contains("headerText")) { j.at("headerText").get_to(data.headerText); }
187
1/2
✓ Branch 1 taken 161 times.
✗ Branch 2 not taken.
161 if (j.contains("leftPaneWidth")) { j.at("leftPaneWidth").get_to(data.leftPaneWidth); }
188
1/2
✓ Branch 1 taken 161 times.
✗ Branch 2 not taken.
161 if (j.contains("plotFlags")) { j.at("plotFlags").get_to(data.plotFlags); }
189
1/2
✓ Branch 1 taken 161 times.
✗ Branch 2 not taken.
161 if (j.contains("rightPaneWidth")) { j.at("rightPaneWidth").get_to(data.rightPaneWidth); }
190
1/2
✓ Branch 1 taken 161 times.
✗ Branch 2 not taken.
161 if (j.contains("selectedPin")) { j.at("selectedPin").get_to(data.selectedPin); }
191
1/2
✓ Branch 1 taken 161 times.
✗ Branch 2 not taken.
161 if (j.contains("selectedXdata")) { j.at("selectedXdata").get_to(data.selectedXdata); }
192
1/2
✓ Branch 1 taken 161 times.
✗ Branch 2 not taken.
161 if (j.contains("plotItems")) { j.at("plotItems").get_to(data.plotItems); }
193
1/2
✓ Branch 1 taken 161 times.
✗ Branch 2 not taken.
161 if (j.contains("title")) { j.at("title").get_to(data.title); }
194
1/2
✓ Branch 1 taken 161 times.
✗ Branch 2 not taken.
161 if (j.contains("overrideXAxisLabel")) { j.at("overrideXAxisLabel").get_to(data.overrideXAxisLabel); }
195
1/2
✓ Branch 1 taken 161 times.
✗ Branch 2 not taken.
161 if (j.contains("xAxisLabel")) { j.at("xAxisLabel").get_to(data.xAxisLabel); }
196
1/2
✓ Branch 1 taken 161 times.
✗ Branch 2 not taken.
161 if (j.contains("y1AxisLabel")) { j.at("y1AxisLabel").get_to(data.y1AxisLabel); }
197
1/2
✓ Branch 1 taken 161 times.
✗ Branch 2 not taken.
161 if (j.contains("y2AxisLabel")) { j.at("y2AxisLabel").get_to(data.y2AxisLabel); }
198
1/2
✓ Branch 1 taken 161 times.
✗ Branch 2 not taken.
161 if (j.contains("y3AxisLabel")) { j.at("y3AxisLabel").get_to(data.y3AxisLabel); }
199 161 }
200
201 } // namespace NAV
202
203 1178 NAV::Plot::PinData::PlotData::PlotData(std::string displayName, size_t size)
204
1/2
✓ Branch 3 taken 1178 times.
✗ Branch 4 not taken.
1178 : displayName(std::move(displayName)), buffer(size) {}
205
206 NAV::Plot::PinData::PinData(const PinData& other)
207 : size(other.size),
208 dataIdentifier(other.dataIdentifier),
209 plotData(other.plotData),
210 rawNodeData(other.rawNodeData),
211 pinType(other.pinType),
212 stride(other.stride) {}
213
214 127 NAV::Plot::PinData::PinData(PinData&& other) noexcept
215 127 : size(other.size),
216 127 dataIdentifier(std::move(other.dataIdentifier)),
217 127 plotData(std::move(other.plotData)),
218 127 rawNodeData(std::move(other.rawNodeData)),
219 127 pinType(other.pinType),
220 127 stride(other.stride) {}
221
222 NAV::Plot::PinData& NAV::Plot::PinData::operator=(const PinData& rhs)
223 {
224 if (&rhs != this)
225 {
226 size = rhs.size;
227 dataIdentifier = rhs.dataIdentifier;
228 plotData = rhs.plotData;
229 rawNodeData = rhs.rawNodeData;
230 pinType = rhs.pinType;
231 stride = rhs.stride;
232 }
233
234 return *this;
235 }
236
237 NAV::Plot::PinData& NAV::Plot::PinData::operator=(PinData&& rhs) noexcept
238 {
239 if (&rhs != this)
240 {
241 size = rhs.size;
242 dataIdentifier = std::move(rhs.dataIdentifier);
243 plotData = std::move(rhs.plotData);
244 rawNodeData = std::move(rhs.rawNodeData);
245 pinType = rhs.pinType;
246 stride = rhs.stride;
247 }
248
249 return *this;
250 }
251
252 4771 void NAV::Plot::PinData::addPlotDataItem(size_t dataIndex, const std::string& displayName)
253 {
254
1/2
✓ Branch 1 taken 4771 times.
✗ Branch 2 not taken.
4771 if (plotData.size() > dataIndex)
255 {
256
3/4
✓ Branch 1 taken 4771 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 3497 times.
✓ Branch 5 taken 1274 times.
4771 if (plotData.at(dataIndex).displayName == displayName) // Item was restored already at this position
257 {
258
1/2
✓ Branch 1 taken 3497 times.
✗ Branch 2 not taken.
3497 plotData.at(dataIndex).markedForDelete = false;
259 3497 return;
260 }
261
262 // Some other item was restored at this position
263
3/4
✓ Branch 1 taken 1274 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1144 times.
✓ Branch 4 taken 130 times.
1274 if (!plotData.at(dataIndex).markedForDelete)
264 {
265
3/6
✓ Branch 1 taken 1144 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 1144 times.
✗ Branch 5 not taken.
✓ Branch 8 taken 1144 times.
✗ Branch 9 not taken.
1144 LOG_WARN("Adding PlotData item '{}' at position {}, but at this position exists already the item '{}'. Reordering the items to match the data. Consider resaving the flow file.",
266 displayName, dataIndex, plotData.at(dataIndex).displayName);
267 }
268
2/4
✓ Branch 1 taken 1274 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 1274 times.
✗ Branch 5 not taken.
78775 auto searchIter = std::ranges::find_if(plotData, [displayName](const PlotData& plotData) { return plotData.displayName == displayName; });
269 1274 auto iter = plotData.begin();
270 1274 std::advance(iter, dataIndex);
271 1274 iter->markedForDelete = false;
272
2/2
✓ Branch 2 taken 1178 times.
✓ Branch 3 taken 96 times.
1274 if (searchIter == plotData.end()) // Item does not exist yet. Developer added a new item to the list
273 {
274
3/6
✓ Branch 1 taken 1178 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 1178 times.
✗ Branch 5 not taken.
✓ Branch 8 taken 1178 times.
✗ Branch 9 not taken.
1178 plotData.insert(iter, PlotData{ displayName, static_cast<size_t>(size) });
275 }
276 else // Item exists already. Developer reordered the items in the list
277 {
278
1/2
✓ Branch 3 taken 96 times.
✗ Branch 4 not taken.
96 move(plotData, static_cast<size_t>(searchIter - plotData.begin()), dataIndex);
279 }
280 }
281 else if (std::ranges::find_if(plotData, [displayName](const PlotData& plotData) { return plotData.displayName == displayName; })
282 != plotData.end())
283 {
284 LOG_ERROR("Adding the PlotData item {} at position {}, but this plot item was found at another position already",
285 displayName, dataIndex);
286 }
287 else // Item not there yet. Add to the end of the list
288 {
289 plotData.emplace_back(displayName, static_cast<size_t>(size));
290 }
291 }
292
293 // ###########################################################################################################
294
295 145 NAV::Plot::Plot()
296
7/14
✓ Branch 1 taken 145 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 145 times.
✗ Branch 5 not taken.
✓ Branch 8 taken 145 times.
✗ Branch 9 not taken.
✓ Branch 13 taken 145 times.
✗ Branch 14 not taken.
✓ Branch 15 taken 2320 times.
✓ Branch 16 taken 145 times.
✓ Branch 23 taken 145 times.
✗ Branch 24 not taken.
✗ Branch 29 not taken.
✗ Branch 30 not taken.
2755 : Node(typeStatic())
297 {
298 LOG_TRACE("{}: called", name);
299
300 145 _hasConfig = true;
301 145 _lockConfigDuringRun = false;
302 145 _guiConfigDefaultWindowSize = { 750, 650 };
303
304 // PinData::PinType::Flow:
305
1/2
✓ Branch 1 taken 145 times.
✗ Branch 2 not taken.
145 _pinData.at(0).pinType = PinData::PinType::Flow;
306
1/2
✓ Branch 1 taken 145 times.
✗ Branch 2 not taken.
145 inputPins.at(0).type = Pin::Type::Flow;
307
2/4
✓ Branch 1 taken 145 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 145 times.
✗ Branch 5 not taken.
145 inputPins.at(0).dataIdentifier = _dataIdentifier;
308
1/2
✓ Branch 1 taken 145 times.
✗ Branch 2 not taken.
145 inputPins.at(0).callback = static_cast<InputPin::FlowFirableCallbackFunc>(&Plot::plotFlowData);
309 // // PinData::PinType::Bool:
310 // _pinData.at(1).pinType = PinData::PinType::Bool;
311 // inputPins.at(1).type = Pin::Type::Bool;
312 // inputPins.at(1).dataIdentifier.clear();
313 // inputPins.at(1).callback = static_cast<InputPin::DataChangedNotifyFunc>(&Plot::plotBoolean);
314 // // PinData::PinType::Int:
315 // _pinData.at(2).pinType = PinData::PinType::Int;
316 // inputPins.at(2).type = Pin::Type::Int;
317 // inputPins.at(2).dataIdentifier.clear();
318 // inputPins.at(2).callback = static_cast<InputPin::DataChangedNotifyFunc>(&Plot::plotInteger);
319 // // PinData::PinType::Float:
320 // _pinData.at(3).pinType = PinData::PinType::Float;
321 // inputPins.at(3).type = Pin::Type::Float;
322 // inputPins.at(3).dataIdentifier.clear();
323 // inputPins.at(3).callback = static_cast<InputPin::DataChangedNotifyFunc>(&Plot::plotFloat);
324 // // PinData::PinType::Matrix:
325 // _pinData.at(4).pinType = PinData::PinType::Matrix;
326 // inputPins.at(4).type = Pin::Type::Matrix;
327 // inputPins.at(4).dataIdentifier = { "Eigen::MatrixXd", "Eigen::VectorXd" };
328 // inputPins.at(4).callback = static_cast<InputPin::DataChangedNotifyFunc>(&Plot::plotMatrix);
329 290 }
330
331 356 NAV::Plot::~Plot()
332 {
333 LOG_TRACE("{}: called", nameId());
334 356 }
335
336 257 std::string NAV::Plot::typeStatic()
337 {
338
1/2
✓ Branch 1 taken 257 times.
✗ Branch 2 not taken.
514 return "Plot";
339 }
340
341 std::string NAV::Plot::type() const
342 {
343 return typeStatic();
344 }
345
346 112 std::string NAV::Plot::category()
347 {
348
1/2
✓ Branch 1 taken 112 times.
✗ Branch 2 not taken.
224 return "Plot";
349 }
350
351 void NAV::Plot::guiConfig()
352 {
353 ImGui::SetNextItemOpen(false, ImGuiCond_FirstUseEver);
354 if (ImGui::CollapsingHeader(fmt::format("Options##{}", size_t(id)).c_str()))
355 {
356 auto columnContentPinType = [&](size_t pinIndex) -> bool {
357 auto& pinData = _pinData.at(pinIndex);
358 ImGui::SetNextItemWidth(100.0F * gui::NodeEditorApplication::windowFontRatio());
359 if (auto pinType = static_cast<int>(pinData.pinType);
360 ImGui::Combo(fmt::format("##Pin Type for Pin {} - {}", pinIndex + 1, size_t(id)).c_str(),
361 &pinType, "Flow\0Bool\0Int\0Float\0Matrix\0\0"))
362 {
363 pinData.pinType = static_cast<decltype(pinData.pinType)>(pinType);
364 if (inputPins.at(pinIndex).isPinLinked())
365 {
366 inputPins.at(pinIndex).deleteLink();
367 }
368
369 switch (pinData.pinType)
370 {
371 case PinData::PinType::Flow:
372 inputPins.at(pinIndex).type = Pin::Type::Flow;
373 inputPins.at(pinIndex).dataIdentifier = _dataIdentifier;
374 inputPins.at(pinIndex).callback = static_cast<InputPin::FlowFirableCallbackFunc>(&Plot::plotFlowData);
375 break;
376 case PinData::PinType::Bool:
377 inputPins.at(pinIndex).type = Pin::Type::Bool;
378 inputPins.at(pinIndex).dataIdentifier.clear();
379 inputPins.at(pinIndex).callback = static_cast<InputPin::DataChangedNotifyFunc>(&Plot::plotBoolean);
380 break;
381 case PinData::PinType::Int:
382 inputPins.at(pinIndex).type = Pin::Type::Int;
383 inputPins.at(pinIndex).dataIdentifier.clear();
384 inputPins.at(pinIndex).callback = static_cast<InputPin::DataChangedNotifyFunc>(&Plot::plotInteger);
385 break;
386 case PinData::PinType::Float:
387 inputPins.at(pinIndex).type = Pin::Type::Float;
388 inputPins.at(pinIndex).dataIdentifier.clear();
389 inputPins.at(pinIndex).callback = static_cast<InputPin::DataChangedNotifyFunc>(&Plot::plotFloat);
390 break;
391 case PinData::PinType::Matrix:
392 inputPins.at(pinIndex).type = Pin::Type::Matrix;
393 inputPins.at(pinIndex).dataIdentifier = { "Eigen::MatrixXd", "Eigen::VectorXd" };
394 inputPins.at(pinIndex).callback = static_cast<InputPin::DataChangedNotifyFunc>(&Plot::plotMatrix);
395 break;
396 }
397
398 return true;
399 }
400 return false;
401 };
402 auto columnContentDataPoints = [&](size_t pinIndex) -> bool {
403 bool changed = false;
404 auto& pinData = _pinData.at(pinIndex);
405 ImGui::SetNextItemWidth(100.0F * gui::NodeEditorApplication::windowFontRatio());
406 if (ImGui::DragInt(fmt::format("##Data Points {} - {}", size_t(id), pinIndex + 1).c_str(),
407 &pinData.size, 10.0F, 0, INT32_MAX / 2))
408 {
409 pinData.size = std::max(pinData.size, 0);
410 std::scoped_lock<std::mutex> guard(pinData.mutex);
411 for (auto& plotData : pinData.plotData)
412 {
413 changed = true;
414 plotData.buffer.resize(static_cast<size_t>(pinData.size));
415 }
416 }
417 if (ImGui::IsItemHovered())
418 {
419 ImGui::SetTooltip("The amount of data which should be stored before the buffer gets reused.\nEnter 0 to show all data.");
420 }
421 return changed;
422 };
423 auto columnContentStride = [&](size_t pinIndex) -> bool {
424 bool changed = false;
425 auto& pinData = _pinData.at(pinIndex);
426 ImGui::SetNextItemWidth(100.0F * gui::NodeEditorApplication::windowFontRatio());
427 if (ImGui::InputInt(fmt::format("##Stride {} - {}", size_t(id), pinIndex + 1).c_str(),
428 &pinData.stride))
429 {
430 pinData.stride = std::max(pinData.stride, 1);
431 changed = true;
432 }
433 if (ImGui::IsItemHovered())
434 {
435 ImGui::SetTooltip("The amount of points to skip when plotting. This greatly reduces lag when plotting");
436 }
437 return changed;
438 };
439
440 if (_dynamicInputPins.ShowGuiWidgets(size_t(id), inputPins, this,
441 { { .header = "Pin Type", .content = columnContentPinType },
442 { .header = "# Data Points", .content = columnContentDataPoints },
443 { .header = "Stride", .content = columnContentStride } }))
444 {
445 flow::ApplyChanges();
446 }
447
448 if (ImGui::Checkbox(fmt::format("Override local position origin (North/East)##{}", size_t(id)).c_str(), &_overridePositionStartValues))
449 {
450 flow::ApplyChanges();
451 LOG_DEBUG("{}: overridePositionStartValues changed to {}", nameId(), _overridePositionStartValues);
452 if (!_originPosition) { _originPosition = gui::widgets::PositionWithFrame(); }
453 }
454 if (_overridePositionStartValues)
455 {
456 ImGui::Indent();
457 if (gui::widgets::PositionInput(fmt::format("Origin##{}", size_t(id)).c_str(), _originPosition.value(), gui::widgets::PositionInputLayout::SINGLE_ROW))
458 {
459 flow::ApplyChanges();
460 }
461 ImGui::Unindent();
462 }
463 if (!_overridePositionStartValues)
464 {
465 if (CommonLog::ShowOriginInput(nameId().c_str()))
466 {
467 flow::ApplyChanges();
468 }
469 }
470 }
471 ImGui::BulletText("Tipp: Ctrl + Hover = Tooltip (+ LClick = Tooltip window)");
472
473 // Used to reset the member variabel _dragAndDropHeaderIndex in case no plot does a drag and drop action
474 bool dragAndDropHeaderStillInProgress = false;
475
476 auto showDragDropTargetHeader = [this](size_t plotIdxTarget) {
477 ImGui::Dummy(ImVec2(-1.F, 2.F));
478
479 bool selectableSelectedDummy = true;
480 ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, ImVec2(0.5F, 0.5F));
481 ImGui::PushStyleColor(ImGuiCol_Header, IM_COL32(16, 173, 44, 79));
482 ImGui::Selectable(fmt::format("[drop here]").c_str(), &selectableSelectedDummy, ImGuiSelectableFlags_None, ImVec2(ImGui::GetWindowContentRegionWidth(), 20.F));
483 ImGui::PopStyleColor();
484 ImGui::PopStyleVar();
485
486 if (ImGui::BeginDragDropTarget())
487 {
488 if (const ImGuiPayload* payloadData = ImGui::AcceptDragDropPayload(fmt::format("DND ColHead {}", size_t(id)).c_str()))
489 {
490 auto plotIdxSource = *static_cast<size_t*>(payloadData->Data);
491
492 if (plotIdxSource < plotIdxTarget)
493 {
494 --plotIdxTarget;
495 }
496
497 move(_plots, plotIdxSource, plotIdxTarget);
498 flow::ApplyChanges();
499 }
500 ImGui::EndDragDropTarget();
501 }
502 ImGui::Dummy(ImVec2(-1.F, 2.F));
503 };
504
505 if (_dragAndDropHeaderIndex > 0)
506 {
507 showDragDropTargetHeader(0);
508 }
509
510 for (size_t plotIdx = 0; plotIdx < _plots.size(); plotIdx++)
511 {
512 auto& plot = _plots.at(plotIdx);
513
514 size_t plotElementIdx = 0;
515
516 if (!plot.visible) // In the previous frame the x was pressed on the plot
517 {
518 LOG_DEBUG("{}: # Plot '{}' at index {} was deleted", nameId(), plot.headerText, plotIdx);
519 _plots.erase(_plots.begin() + static_cast<int64_t>(plotIdx));
520 _nPlots -= 1;
521 flow::ApplyChanges();
522 continue;
523 }
524
525 ImGui::SetNextItemOpen(true, ImGuiCond_Once);
526 if (ImGui::CollapsingHeader(fmt::format("{}##Plot Header {} - {}", plot.headerText, size_t(id), plotIdx).c_str(), &plot.visible))
527 {
528 if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None))
529 {
530 dragAndDropHeaderStillInProgress = true;
531 _dragAndDropHeaderIndex = static_cast<int>(plotIdx);
532 // Data is copied into heap inside the drag and drop
533 ImGui::SetDragDropPayload(fmt::format("DND ColHead {}", size_t(id)).c_str(),
534 &plotIdx, sizeof(plotIdx));
535 ImGui::Dummy(ImVec2(ImGui::CalcTextSize(plot.headerText.c_str()).x + 60.F, -1.F));
536 bool dnd_display_close = true;
537 ImGui::CollapsingHeader(fmt::format("{}##Plot DND Header {} - {}", plot.headerText, size_t(id), plotIdx).c_str(), &dnd_display_close);
538 ImGui::EndDragDropSource();
539 }
540
541 bool saveForceXaxisRange = false;
542 ImGui::SetNextItemOpen(false, ImGuiCond_FirstUseEver);
543 auto optionsCursorPos = ImGui::GetCursorPos();
544 if (ImGui::TreeNode(fmt::format("Options##{} - {}", size_t(id), plotIdx).c_str()))
545 {
546 std::string headerTitle = plot.headerText;
547 ImGui::InputText(fmt::format("Header Title##{} - {}", size_t(id), plotIdx).c_str(), &headerTitle);
548 if (plot.headerText != headerTitle && !ImGui::IsItemActive())
549 {
550 plot.headerText = headerTitle;
551 flow::ApplyChanges();
552 LOG_DEBUG("{}: Header changed to {}", nameId(), plot.headerText);
553 }
554 if (ImGui::InputText(fmt::format("Plot Title##{} - {}", size_t(id), plotIdx).c_str(), &plot.title))
555 {
556 flow::ApplyChanges();
557 LOG_DEBUG("{}: Plot Title changed to {}", nameId(), plot.title);
558 }
559 bool plotWidthAutomatic = plot.size.x == -1.0;
560 float checkBoxStartX = ImGui::GetCursorPosX();
561 if (ImGui::Checkbox(fmt::format("{}Automatic Plot Width##{} - {}", plotWidthAutomatic ? "" : "##", size_t(id), plotIdx).c_str(), &plotWidthAutomatic))
562 {
563 if (plotWidthAutomatic) { plot.size.x = -1.0; }
564 else { plot.size.x = ImGui::GetWindowContentRegionWidth() - plot.leftPaneWidth - ImGui::GetStyle().ItemSpacing.x; }
565 flow::ApplyChanges();
566 }
567 if (!plotWidthAutomatic)
568 {
569 ImGui::SameLine();
570 ImGui::SetNextItemWidth(ImGui::CalcItemWidth() - (ImGui::GetCursorPosX() - checkBoxStartX));
571 if (ImGui::SliderFloat(fmt::format("Plot Width##{} - {}", size_t(id), plotIdx).c_str(), &plot.size.x, 1.0F, 2000, "%.0f"))
572 {
573 flow::ApplyChanges();
574 }
575 }
576 if (ImGui::SliderFloat(fmt::format("Plot Height##{} - {}", size_t(id), plotIdx).c_str(), &plot.size.y, 0.0F, 1000, "%.0f"))
577 {
578 flow::ApplyChanges();
579 }
580 if (ImGui::Checkbox(fmt::format("Override X Axis Label##{} - {}", size_t(id), plotIdx).c_str(), &plot.overrideXAxisLabel))
581 {
582 flow::ApplyChanges();
583 }
584 if (plot.overrideXAxisLabel)
585 {
586 if (ImGui::InputText(fmt::format("X Axis Label##{} - {}", size_t(id), plotIdx).c_str(), &plot.xAxisLabel))
587 {
588 flow::ApplyChanges();
589 }
590 }
591 if (ImGui::InputText(fmt::format("Y1 Axis Label##{} - {}", size_t(id), plotIdx).c_str(), &plot.y1AxisLabel))
592 {
593 flow::ApplyChanges();
594 }
595 if (plot.plotFlags & ImPlotFlags_YAxis2)
596 {
597 if (ImGui::InputText(fmt::format("Y2 Axis Label##{} - {}", size_t(id), plotIdx).c_str(), &plot.y2AxisLabel))
598 {
599 flow::ApplyChanges();
600 }
601 }
602 if (plot.plotFlags & ImPlotFlags_YAxis3)
603 {
604 if (ImGui::InputText(fmt::format("Y3 Axis Label##{} - {}", size_t(id), plotIdx).c_str(), &plot.y3AxisLabel))
605 {
606 flow::ApplyChanges();
607 }
608 }
609 if (ImGui::BeginTable(fmt::format("Pin Settings##{} - {}", size_t(id), plotIdx).c_str(), 2,
610 ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoHostExtendX, ImVec2(0.0F, 0.0F)))
611 {
612 ImGui::TableSetupColumn("Pin");
613 ImGui::TableSetupColumn("X Data");
614 ImGui::TableHeadersRow();
615
616 for (size_t pinIndex = 0; pinIndex < _pinData.size(); pinIndex++)
617 {
618 auto& pinData = _pinData.at(pinIndex);
619
620 ImGui::TableNextRow();
621 ImGui::TableNextColumn(); // Pin
622 ImGui::Text("%zu - %s", pinIndex + 1, pinData.dataIdentifier.c_str());
623
624 ImGui::TableNextColumn(); // X Data
625 if (!pinData.plotData.empty())
626 {
627 ImGui::SetNextItemWidth(200.0F * gui::NodeEditorApplication::windowFontRatio());
628 if (ImGui::BeginCombo(fmt::format("##X Data for Pin {} - {} - {}", pinIndex + 1, size_t(id), plotIdx).c_str(),
629 pinData.plotData.at(plot.selectedXdata.at(pinIndex)).displayName.c_str()))
630 {
631 for (size_t plotDataIndex = 0; plotDataIndex < pinData.plotData.size(); plotDataIndex++)
632 {
633 auto& plotData = pinData.plotData.at(plotDataIndex);
634
635 if (!plotData.hasData)
636 {
637 ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5F);
638 }
639 const bool is_selected = (plot.selectedXdata.at(pinIndex) == plotDataIndex);
640 if (ImGui::Selectable(pinData.plotData.at(plotDataIndex).displayName.c_str(), is_selected))
641 {
642 flow::ApplyChanges();
643 plot.selectedXdata.at(pinIndex) = plotDataIndex;
644 if (plotDataIndex == GPST_PLOT_IDX) // GPST Time
645 {
646 // Set all data to plot over GPST Time
647 for (auto& selectedX : plot.selectedXdata)
648 {
649 selectedX = plotDataIndex;
650 }
651 }
652 else
653 {
654 // Remove all GPST Time on the x axis
655 for (auto& selectedX : plot.selectedXdata)
656 {
657 if (selectedX == GPST_PLOT_IDX) { selectedX = 0; }
658 }
659 }
660
661 for (auto& plotItem : plot.plotItems)
662 {
663 plotItem.eventMarker.clear();
664 plotItem.eventTooltips.clear();
665 }
666 }
667 if (!plotData.hasData)
668 {
669 ImGui::PopStyleVar();
670 }
671
672 // Set the initial focus when opening the combo (scrolling + keyboard navigation focus)
673 if (is_selected)
674 {
675 ImGui::SetItemDefaultFocus();
676 }
677 }
678 ImGui::EndCombo();
679 }
680 }
681 }
682
683 ImGui::EndTable();
684 }
685
686 if (ImGui::CheckboxFlags(fmt::format("Y-Axis 2##{} - {}", size_t(id), plotIdx).c_str(),
687 &plot.plotFlags, ImPlotFlags_YAxis2))
688 {
689 flow::ApplyChanges();
690 }
691 ImGui::SameLine();
692 if (ImGui::CheckboxFlags(fmt::format("Y-Axis 3##{} - {}", size_t(id), plotIdx).c_str(),
693 &plot.plotFlags, ImPlotFlags_YAxis3))
694 {
695 flow::ApplyChanges();
696 }
697 ImGui::SameLine();
698
699 if (ImGui::CheckboxFlags(fmt::format("Auto Limit X-Axis##{} - {}", size_t(id), plotIdx).c_str(),
700 &plot.xAxisFlags, ImPlotAxisFlags_AutoFit))
701 {
702 flow::ApplyChanges();
703 }
704 ImGui::SameLine();
705 if (ImGui::CheckboxFlags(fmt::format("Auto Limit Y-Axis##{} - {}", size_t(id), plotIdx).c_str(),
706 &plot.yAxisFlags, ImPlotAxisFlags_AutoFit))
707 {
708 flow::ApplyChanges();
709 }
710 ImGui::SameLine();
711 if (ImGui::Button(fmt::format("Same X range all plots##{} - {}", size_t(id), plotIdx).c_str()))
712 {
713 saveForceXaxisRange = true;
714 _forceXaxisRange.first.clear();
715 size_t pinIdx = plot.plotItems.empty() ? 0 : plot.plotItems.front().pinIndex;
716 const auto& xName = _pinData.at(pinIdx).plotData.at(plot.selectedXdata.at(pinIdx)).displayName;
717 for (size_t p = 0; p < _plots.size(); p++)
718 {
719 if (p == plotIdx) { continue; }
720 auto& plot = _plots.at(p);
721 size_t pinIdx = plot.plotItems.empty() ? 0 : plot.plotItems.front().pinIndex;
722 const auto& dispName = _pinData.at(pinIdx).plotData.at(plot.selectedXdata.at(pinIdx)).displayName;
723 if (xName == dispName) { _forceXaxisRange.first.insert(p); }
724 }
725 }
726
727 auto axisScaleCombo = [&](const char* label, ImPlotScale& axisScale) {
728 auto getImPlotScaleString = [](ImPlotScale scale) {
729 switch (scale)
730 {
731 case ImPlotScale_Linear: // default linear scale
732 return "Linear";
733 // case ImPlotScale_Time: // date/time scale
734 // return "Time";
735 case ImPlotScale_Log10: // base 10 logartithmic scale
736 return "Log10";
737 case ImPlotScale_SymLog: // symmetric log scale
738 return "SymLog";
739 default:
740 return "-";
741 }
742 return "-";
743 };
744
745 ImGui::SetNextItemWidth(100.0F);
746 if (ImGui::BeginCombo(fmt::format("{}-Axis Scale##{} - {}", label, size_t(id), plotIdx).c_str(), getImPlotScaleString(axisScale)))
747 {
748 for (size_t n = 0; n < 4; ++n)
749 {
750 if (n == ImPlotScale_Time) { continue; }
751 const bool is_selected = (static_cast<size_t>(axisScale) == n);
752 if (ImGui::Selectable(getImPlotScaleString(static_cast<ImPlotScale>(n)), is_selected))
753 {
754 axisScale = static_cast<ImPlotScale>(n);
755 flow::ApplyChanges();
756 }
757 if (is_selected) { ImGui::SetItemDefaultFocus(); } // Set the initial focus when opening the combo
758 }
759 ImGui::EndCombo();
760 }
761 };
762 axisScaleCombo("X", plot.xAxisScale);
763 ImGui::SameLine();
764 axisScaleCombo("Y1", plot.yAxesScale[0]);
765 if (plot.plotFlags & ImPlotFlags_YAxis2)
766 {
767 ImGui::SameLine();
768 axisScaleCombo("Y2", plot.yAxesScale[1]);
769 }
770 if (plot.plotFlags & ImPlotFlags_YAxis3)
771 {
772 ImGui::SameLine();
773 axisScaleCombo("Y3", plot.yAxesScale[2]);
774 }
775
776 ImGui::SameLine();
777 if (ImGui::CheckboxFlags(fmt::format("NoClip##LineFlags {} - {}", size_t(id), plotIdx).c_str(), &plot.lineFlags, ImPlotLineFlags_NoClip))
778 {
779 flow::ApplyChanges();
780 }
781 if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Markers (if displayed) on the edge of a plot will not be clipped"); }
782 ImGui::SameLine();
783 if (ImGui::CheckboxFlags(fmt::format("SkipNaN##LineFlags {} - {}", size_t(id), plotIdx).c_str(), &plot.lineFlags, ImPlotLineFlags_SkipNaN))
784 {
785 flow::ApplyChanges();
786 }
787 if (ImGui::IsItemHovered()) { ImGui::SetTooltip("NaNs values will be skipped instead of rendered as missing data"); }
788 ImGui::SameLine();
789 if (ImGui::CheckboxFlags(fmt::format("Loop##LineFlags {} - {}", size_t(id), plotIdx).c_str(), &plot.lineFlags, ImPlotLineFlags_Loop))
790 {
791 flow::ApplyChanges();
792 }
793 if (ImGui::IsItemHovered()) { ImGui::SetTooltip("The last and first point will be connected to form a closed loop"); }
794
795 ImGui::TreePop();
796 }
797
798 #ifdef IMGUI_IMPL_OPENGL_LOADER_GL3W
799 {
800 auto afterOptionsCursorPos = ImGui::GetCursorPos();
801 float buttonSize = 0.6F * gui::NodeEditorApplication::windowFontRatio() * 24.0F;
802
803 ImGui::SetCursorPos(ImVec2(ImGui::GetWindowContentRegionWidth() - buttonSize - ImGui::GetStyle().ItemInnerSpacing.x, optionsCursorPos.y));
804 ImGui::PushID(fmt::format("{}{}", size_t(id), plotIdx).c_str());
805 if (ImGui::ImageButton(gui::NodeEditorApplication::m_SaveButtonImage, ImVec2(buttonSize, buttonSize)))
806 {
807 if (_screenshotFrameCnt == 0)
808 {
809 _screenShotPlotIdx = plotIdx;
810 _screenshotFrameCnt = 1;
811 }
812 }
813 ImGui::PopID();
814 ImGui::SetCursorPos(afterOptionsCursorPos);
815 }
816 #endif
817
818 auto plotSize = plot.size;
819 if (gui::NodeEditorApplication::isUsingBigWindowFont()) { plotSize.y *= 0.8F * gui::NodeEditorApplication::windowFontRatio(); }
820
821 gui::widgets::Splitter(fmt::format("Splitter {} - {}", size_t(id), plotIdx).c_str(),
822 true, 4.0F, &plot.leftPaneWidth, &plot.rightPaneWidth, 3.0F, 80.0F, plotSize.y);
823
824 ImGui::SetNextItemWidth(plot.leftPaneWidth - 2.0F);
825
826 ImGui::BeginGroup();
827 {
828 if (ImGui::BeginCombo(fmt::format("##Data source pin selection{} - {}", size_t(id), plotIdx).c_str(),
829 inputPins.at(plot.selectedPin).name.c_str()))
830 {
831 for (size_t n = 0; n < inputPins.size(); ++n)
832 {
833 const bool is_selected = (plot.selectedPin == n);
834 if (ImGui::Selectable(inputPins.at(n).name.c_str(), is_selected, 0))
835 {
836 plot.selectedPin = n;
837 }
838
839 // Set the initial focus when opening the combo (scrolling + keyboard navigation focus)
840 if (is_selected)
841 {
842 ImGui::SetItemDefaultFocus();
843 }
844 }
845 ImGui::EndCombo();
846 }
847 auto comboBoxSize = ImGui::GetItemRectSize();
848 if (ImGui::Button(fmt::format("Clear##{} - {}", size_t(id), plotIdx).c_str(), ImVec2(plot.leftPaneWidth - 2.0F, 0)))
849 {
850 plot.plotItems.clear();
851 flow::ApplyChanges();
852 }
853 if (ImGui::BeginDragDropTarget())
854 {
855 if (const ImGuiPayload* payloadData = ImGui::AcceptDragDropPayload(fmt::format("DND PlotItem {} - {}", size_t(id), plotIdx).c_str()))
856 {
857 auto [pinIndex, dataIndex, displayName] = *static_cast<std::tuple<size_t, size_t, std::string*>*>(payloadData->Data);
858
859 auto iter = std::ranges::find(plot.plotItems, PlotInfo::PlotItem{ pinIndex, dataIndex, *displayName });
860 if (iter != plot.plotItems.end())
861 {
862 plot.plotItems.erase(iter);
863 flow::ApplyChanges();
864 }
865 }
866 ImGui::EndDragDropTarget();
867 }
868 auto buttonSize = ImGui::GetItemRectSize();
869 ImGui::BeginChild(fmt::format("Data Drag{} - {}", size_t(id), plotIdx).c_str(),
870 ImVec2(plot.leftPaneWidth - 2.0F, plotSize.y - comboBoxSize.y - buttonSize.y - 2 * ImGui::GetStyle().ItemSpacing.y),
871 true);
872 {
873 // Left Data Selectables
874 for (size_t dataIndex = 0; dataIndex < _pinData.at(plot.selectedPin).plotData.size(); ++dataIndex)
875 {
876 auto& plotData = _pinData.at(plot.selectedPin).plotData.at(dataIndex);
877 auto plotDataHasData = plotData.hasData;
878 if (!plotDataHasData)
879 {
880 ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5F);
881 }
882 std::string label = plotData.displayName;
883
884 if (auto iter = std::ranges::find(plot.plotItems, PlotInfo::PlotItem{ plot.selectedPin, dataIndex, plotData.displayName });
885 iter != plot.plotItems.end())
886 {
887 label += fmt::format(" (Y{})", iter->axis + 1 - 3);
888 }
889
890 if (!label.empty())
891 {
892 ImGui::Selectable(label.c_str(), false, 0);
893 if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None))
894 {
895 // Data is copied into heap inside the drag and drop
896 auto pinAndDataIndex = std::make_tuple(plot.selectedPin, dataIndex, &plotData.displayName);
897 ImGui::SetDragDropPayload(fmt::format("DND PlotItem {} - {}", size_t(id), plotIdx).c_str(),
898 &pinAndDataIndex, sizeof(pinAndDataIndex));
899 ImGui::TextUnformatted(label.c_str());
900 ImGui::EndDragDropSource();
901 }
902 }
903
904 if (!plotDataHasData)
905 {
906 ImGui::PopStyleVar();
907 }
908 }
909
910 ImGui::EndChild();
911 }
912 ImGui::EndGroup();
913 }
914
915 ImGui::SameLine();
916
917 const char* xLabel = plot.overrideXAxisLabel ? (!plot.xAxisLabel.empty() ? plot.xAxisLabel.c_str() : nullptr)
918 : (!_pinData.at(0).plotData.empty() ? _pinData.at(0).plotData.at(plot.selectedXdata.at(0)).displayName.c_str() : nullptr);
919
920 const char* y1Label = !plot.y1AxisLabel.empty() ? plot.y1AxisLabel.c_str() : nullptr;
921 const char* y2Label = (plot.plotFlags & ImPlotFlags_YAxis2) && !plot.y2AxisLabel.empty() ? plot.y2AxisLabel.c_str() : nullptr;
922 const char* y3Label = (plot.plotFlags & ImPlotFlags_YAxis3) && !plot.y3AxisLabel.empty() ? plot.y3AxisLabel.c_str() : nullptr;
923
924 bool timeScaleXaxis = false;
925 std::array<double, 2> timeAxisMinMax = { std::numeric_limits<double>::infinity(), -std::numeric_limits<double>::infinity() };
926 for (auto& plotItem : plot.plotItems)
927 {
928 if (plot.selectedXdata.at(plotItem.pinIndex) == GPST_PLOT_IDX)
929 {
930 auto& pinData = _pinData.at(plotItem.pinIndex);
931 const auto& plotDataX = pinData.plotData.at(plot.selectedXdata.at(plotItem.pinIndex));
932
933 // Lock the buffer so no data can be inserted
934 std::scoped_lock<std::mutex> guard(pinData.mutex);
935 if (!plotDataX.buffer.empty())
936 {
937 timeScaleXaxis = true;
938 timeAxisMinMax[0] = std::min(timeAxisMinMax[0], plotDataX.buffer.front());
939 timeAxisMinMax[1] = std::max(timeAxisMinMax[1], plotDataX.buffer.back());
940 }
941 }
942 }
943
944 if (ImPlot::BeginPlot(fmt::format("{}##{} - {}", plot.title, size_t(id), plotIdx).c_str(), plotSize, plot.plotFlags))
945 {
946 #ifdef IMGUI_IMPL_OPENGL_LOADER_GL3W
947 if (_screenShotPlotIdx == plotIdx)
948 {
949 // can be static as its impossible to trigger multiple screenshots in different nodes at once
950 static ImVec4 windowBgColor = ImGui::GetStyle().Colors[ImGuiCol_WindowBg];
951 static json imPlotStyle = ImPlot::GetStyle();
952 if (_screenshotFrameCnt == 1)
953 {
954 windowBgColor = ImGui::GetStyle().Colors[ImGuiCol_WindowBg];
955 imPlotStyle = ImPlot::GetStyle();
956 loadImPlotStyleFromConfigFile(gui::windows::plotScreenshotImPlotStyleFile.c_str(), ImPlot::GetStyle());
957 if (!ImPlot::IsColorAuto(ImPlot::GetStyle().Colors[ImPlotCol_FrameBg])
958 && ImPlot::GetStyle().Colors[ImPlotCol_FrameBg].w == 1.0F)
959 {
960 ImGui::GetStyle().Colors[ImGuiCol_WindowBg] = ImPlot::GetStyle().Colors[ImPlotCol_FrameBg];
961 }
962 ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = 1.0;
963 _screenshotFrameCnt++;
964 }
965 else if (_screenshotFrameCnt == 4) // Need to delay a few frames in order for the background color to apply
966 {
967 ImRect CaptureRect = ImPlot::GetCurrentPlot()->FrameRect;
968 ImGuiIO& io = ImGui::GetIO();
969 ImGuiScreenshotImageBuf Output(static_cast<int>(CaptureRect.Min.x),
970 static_cast<int>(io.DisplaySize.y) - static_cast<int>(CaptureRect.Max.y),
971 static_cast<size_t>(CaptureRect.GetWidth()),
972 static_cast<size_t>(CaptureRect.GetHeight()));
973 auto savePath = flow::GetOutputPath() / fmt::format("Plot-{}_{}.png", size_t(id), plotIdx);
974 Output.SaveFile(savePath.c_str());
975 LOG_INFO("{}: Plot image saved as: {}", nameId(), savePath);
976 if (gui::windows::copyScreenshotsToClipboard)
977 {
978 gui::windows::CopyFileToClipboard(savePath.c_str());
979 }
980
981 ImGui::GetStyle().Colors[ImGuiCol_WindowBg] = windowBgColor;
982 imPlotStyle.get_to(ImPlot::GetStyle());
983 _screenshotFrameCnt = 0;
984 }
985 else if (_screenshotFrameCnt != 0) { _screenshotFrameCnt++; } // Increment frame counter
986 }
987 #endif
988
989 if (saveForceXaxisRange)
990 {
991 std::get<ImPlotRange>(_forceXaxisRange) = ImPlot::GetCurrentPlot()->XAxis(ImAxis_X1).Range;
992 }
993 else if (_forceXaxisRange.first.contains(plotIdx))
994 {
995 if (plot.xAxisFlags & ImPlotAxisFlags_AutoFit)
996 {
997 plot.xAxisFlags &= ~ImPlotAxisFlags_AutoFit;
998 flow::ApplyChanges();
999 }
1000 ImPlot::GetCurrentPlot()->XAxis(ImAxis_X1).SetRange(std::get<ImPlotRange>(_forceXaxisRange));
1001 _forceXaxisRange.first.erase(plotIdx);
1002 }
1003
1004 ImPlot::SetupAxis(ImAxis_X1, xLabel, plot.xAxisFlags);
1005 ImPlot::SetupAxisScale(ImAxis_X1, timeScaleXaxis ? ImPlotScale_Time : plot.xAxisScale);
1006 ImPlot::SetupAxis(ImAxis_Y1, y1Label, plot.yAxisFlags);
1007 ImPlot::SetupAxisScale(ImAxis_Y1, plot.yAxesScale[0]);
1008 if (plot.plotFlags & ImPlotFlags_YAxis2)
1009 {
1010 ImPlot::SetupAxis(ImAxis_Y2, y2Label, plot.yAxisFlags | ImPlotAxisFlags_NoGridLines | ImPlotAxisFlags_Opposite);
1011 ImPlot::SetupAxisScale(ImAxis_Y2, plot.yAxesScale[1]);
1012 }
1013 if (plot.plotFlags & ImPlotFlags_YAxis3)
1014 {
1015 ImPlot::SetupAxis(ImAxis_Y3, y3Label, plot.yAxisFlags | ImPlotAxisFlags_NoGridLines | ImPlotAxisFlags_Opposite);
1016 ImPlot::SetupAxisScale(ImAxis_Y3, plot.yAxesScale[2]);
1017 }
1018
1019 std::vector<PlotInfo::PlotItem> plotItemsToRemove;
1020 bool hoverTooltipShown = false;
1021 for (size_t plotItemIdx = 0; plotItemIdx < plot.plotItems.size(); plotItemIdx++)
1022 {
1023 auto& plotItem = plot.plotItems.at(plotItemIdx);
1024 auto& pinData = _pinData.at(plotItem.pinIndex);
1025
1026 // Lock the buffer so no data can be inserted till plotting finishes
1027 std::scoped_lock<std::mutex> guard(pinData.mutex);
1028 // The next line needs already be locked, otherwise we have a data race
1029
1030 if (pinData.plotData.size() <= plotItem.dataIndex) { continue; } // Dynamic data can not be available yet
1031 auto& plotData = pinData.plotData.at(plotItem.dataIndex);
1032 const auto& plotDataX = pinData.plotData.at(plot.selectedXdata.at(plotItem.pinIndex));
1033 if (plotData.displayName != plotItem.displayName)
1034 {
1035 if (plotItem.displayName.empty())
1036 {
1037 plotItem.displayName = plotData.displayName; // old flow file where it was not set yet
1038 }
1039 else
1040 {
1041 plotItemsToRemove.push_back(plotItem);
1042 continue;
1043 }
1044 }
1045
1046 if (plotData.hasData
1047 && (plotItem.axis == ImAxis_Y1
1048 || (plotItem.axis == ImAxis_Y2 && (plot.plotFlags & ImPlotFlags_YAxis2))
1049 || (plotItem.axis == ImAxis_Y3 && (plot.plotFlags & ImPlotFlags_YAxis3))))
1050 {
1051 ImPlot::SetAxis(plotItem.axis);
1052
1053 if (plotItem.style.colormapMask.first != ColormapMaskType::None)
1054 {
1055 if (const auto& cmap = ColormapSearch(plotItem.style.colormapMask.first, plotItem.style.colormapMask.second))
1056 {
1057 if (plotItem.colormapMaskVersion != cmap->get().version) { plotItem.colormapMaskColors.clear(); }
1058 if (plotItem.colormapMaskColors.size() != plotData.buffer.size()
1059 && plotItem.style.colormapMaskDataCmpIdx < pinData.plotData.size())
1060 {
1061 plotItem.colormapMaskVersion = cmap->get().version;
1062 const auto& cmpData = pinData.plotData.at(plotItem.style.colormapMaskDataCmpIdx);
1063
1064 auto color = plotItem.style.lineType == PlotItemStyle::LineType::Line
1065 ? (ImPlot::IsColorAuto(plotItem.style.color) ? ImPlot::GetColormapColor(static_cast<int>(plotElementIdx)) : plotItem.style.color)
1066 : (ImPlot::IsColorAuto(plotItem.style.markerFillColor) ? ImPlot::GetColormapColor(static_cast<int>(plotElementIdx)) : plotItem.style.markerFillColor);
1067 plotItem.colormapMaskColors.reserve(plotData.buffer.size());
1068 for (size_t i = plotItem.colormapMaskColors.size(); i < plotData.buffer.size(); i++)
1069 {
1070 plotItem.colormapMaskColors.push_back(i >= cmpData.buffer.size() ? ImColor(color) : cmap->get().getColor(cmpData.buffer.at(i), color));
1071 }
1072 }
1073 }
1074 else
1075 {
1076 plotItem.style.colormapMask.first = ColormapMaskType::None;
1077 plotItem.style.colormapMask.second = -1;
1078 plotItem.colormapMaskColors.clear();
1079 }
1080 }
1081 if (plotItem.style.markers && plotItem.style.markerColormapMask.first != ColormapMaskType::None)
1082 {
1083 if (const auto& cmap = ColormapSearch(plotItem.style.markerColormapMask.first, plotItem.style.markerColormapMask.second))
1084 {
1085 if (plotItem.markerColormapMaskVersion != cmap->get().version) { plotItem.markerColormapMaskColors.clear(); }
1086 if (plotItem.markerColormapMaskColors.size() != plotData.buffer.size()
1087 && plotItem.style.markerColormapMaskDataCmpIdx < pinData.plotData.size())
1088 {
1089 plotItem.markerColormapMaskVersion = cmap->get().version;
1090 const auto& cmpData = pinData.plotData.at(plotItem.style.markerColormapMaskDataCmpIdx);
1091
1092 auto color = plotItem.style.lineType == PlotItemStyle::LineType::Line
1093 ? (ImPlot::IsColorAuto(plotItem.style.color) ? ImPlot::GetColormapColor(static_cast<int>(plotElementIdx)) : plotItem.style.color)
1094 : (ImPlot::IsColorAuto(plotItem.style.markerFillColor) ? ImPlot::GetColormapColor(static_cast<int>(plotElementIdx)) : plotItem.style.markerFillColor);
1095 plotItem.markerColormapMaskColors.reserve(plotData.buffer.size());
1096 for (size_t i = plotItem.markerColormapMaskColors.size(); i < plotData.buffer.size(); i++)
1097 {
1098 plotItem.markerColormapMaskColors.push_back(i >= cmpData.buffer.size() ? ImColor(color) : cmap->get().getColor(cmpData.buffer.at(i), color));
1099 }
1100 }
1101 }
1102 else
1103 {
1104 plotItem.style.markerColormapMask.first = ColormapMaskType::None;
1105 plotItem.style.markerColormapMask.second = -1;
1106 plotItem.markerColormapMaskColors.clear();
1107 }
1108 }
1109 if (plotItem.style.errorBoundsEnabled)
1110 {
1111 if (plotItem.errorBoundsData[0].size() != plotData.buffer.size()
1112 && plotItem.style.errorBoundsDataIdx < pinData.plotData.size())
1113 {
1114 const auto& errorData = pinData.plotData.at(plotItem.style.errorBoundsDataIdx);
1115 for (size_t i = plotItem.errorBoundsData[0].size(); i < plotData.buffer.size(); i++)
1116 {
1117 double errorValue = errorData.buffer.at(i);
1118 if (!plotItem.style.errorBoundsModifierExpression.empty())
1119 {
1120 try
1121 {
1122 mu::Parser p;
1123 double x = errorValue;
1124 p.DefineVar("x", &x);
1125 p.SetExpr(plotItem.style.errorBoundsModifierExpression);
1126 errorValue = p.Eval();
1127 }
1128 catch (mu::Parser::exception_type& e)
1129 {
1130 LOG_ERROR("{}: Error bound modifier parse error on '{}': {}", nameId(), plotItem.style.legendName, e.GetMsg());
1131 }
1132 }
1133 plotItem.errorBoundsData[0].push_back(plotData.buffer.at(i) - errorValue);
1134 plotItem.errorBoundsData[1].push_back(plotData.buffer.at(i) + errorValue);
1135 }
1136 }
1137 }
1138 if (plotItem.style.eventsEnabled)
1139 {
1140 if (plotItem.eventMarker.size() != plotData.buffer.size())
1141 {
1142 auto& plotDataRelTime = pinData.plotData.at(0);
1143 for (size_t i = plotItem.eventMarker.size(); i < plotData.buffer.size(); i++)
1144 {
1145 double relTime = plotDataRelTime.buffer.at(i);
1146 PlotEventTooltip tooltip;
1147 try
1148 {
1149 std::regex filter(plotItem.style.eventTooltipFilterRegex,
1150 std::regex_constants::ECMAScript | std::regex_constants::icase);
1151 for (const auto& e : pinData.events)
1152 {
1153 if (std::abs(std::get<0>(e) - relTime) <= 1e-6)
1154 {
1155 tooltip.time = std::get<1>(e);
1156 if ((std::get<3>(e) == -1 || static_cast<size_t>(std::get<3>(e)) == plotItem.dataIndex)
1157 && (plotItem.style.eventTooltipFilterRegex.empty() || std::regex_search(std::get<2>(e), filter)))
1158 {
1159 tooltip.texts.push_back(std::get<2>(e));
1160 }
1161 }
1162 }
1163 }
1164 catch (...) // NOLINT(bugprone-empty-catch)
1165 {}
1166
1167 if (!tooltip.texts.empty())
1168 {
1169 plotItem.eventMarker.push_back(plotData.buffer.at(i));
1170 plotItem.eventTooltips.emplace_back(plotDataX.buffer.at(i), plotData.buffer.at(i), tooltip);
1171 }
1172 else
1173 {
1174 plotItem.eventMarker.push_back(std::nan(""));
1175 }
1176 }
1177 }
1178 }
1179
1180 if (plotItem.style.legendName.empty())
1181 {
1182 plotItem.style.legendName = fmt::format("{} ({})", plotData.displayName, inputPins.at(plotItem.pinIndex).name);
1183 }
1184 std::string plotName = fmt::format("{}##{} - {} - {}", plotItem.style.legendName, size_t(id), plotItem.pinIndex + 1, plotData.displayName);
1185
1186 plotItem.style.plotData(plotName.c_str(),
1187 plotDataX.buffer,
1188 plotData.buffer,
1189 static_cast<int>(plotElementIdx),
1190 pinData.stride,
1191 plot.lineFlags,
1192 &plotItem.colormapMaskColors,
1193 &plotItem.markerColormapMaskColors,
1194 &plotItem.errorBoundsData);
1195
1196 // ----------------------------------- Tooltips --------------------------------------
1197 ImGuiWindow* plotWindow = ImGui::GetCurrentWindow();
1198
1199 ShowPlotTooltipWindows(
1200 plot.tooltips,
1201 plotItemIdx,
1202 plotItem.style.legendName,
1203 fmt::format("{} {} {}", size_t(id), plotItem.pinIndex, plotItem.displayName),
1204 { reinterpret_cast<int*>(plotWindow) },
1205 [&](size_t dataIdx) { return pinData.rawNodeData.at(dataIdx)->insTime; },
1206 [&](size_t dataIdx, const char* tooltipUID) {
1207 const auto& nodeData = pinData.rawNodeData.at(dataIdx);
1208 auto nEvents = nodeData->events().size();
1209 if (nEvents > 0)
1210 {
1211 ImGui::SetNextItemOpen(false, ImGuiCond_Once);
1212 if (ImGui::TreeNode(fmt::format("Events: {}", nEvents).c_str()))
1213 {
1214 for (const auto& text : nodeData->events())
1215 {
1216 ImGui::BulletText("%s", text.c_str());
1217 }
1218 ImGui::TreePop();
1219 }
1220 }
1221 else { ImGui::BulletText("Events: 0"); }
1222 if (nodeData->hasTooltip())
1223 {
1224 nodeData->guiTooltip(true, false, plotItem.displayName.c_str(),
1225 tooltipUID, reinterpret_cast<int*>(plotWindow));
1226 }
1227 });
1228
1229 hoverTooltipShown |= ShowPlotTooltip(
1230 plot.tooltips, plotItemIdx,
1231 plotName, plotItem.axis,
1232 plotDataX.buffer, plotData.buffer,
1233 hoverTooltipShown,
1234 [&](size_t dataIdx) {
1235 const auto& nodeData = pinData.rawNodeData.at(dataIdx);
1236 ImGui::TextUnformatted(fmt::format("{} - {}", plotItem.style.legendName,
1237 nodeData->insTime.toYMDHMS(GPST))
1238 .c_str());
1239 ImGui::Separator();
1240
1241 auto nEvents = nodeData->events().size();
1242 if (nEvents > 0)
1243 {
1244 ImGui::SetNextItemOpen(false, ImGuiCond_Always);
1245 if (ImGui::TreeNode(fmt::format("Events: {}", nEvents).c_str())) { ImGui::TreePop(); }
1246 }
1247 else { ImGui::BulletText("Events: 0"); }
1248
1249 if (pinData.rawNodeData.at(dataIdx)->hasTooltip())
1250 {
1251 auto tooltipUID = fmt::format("{} {} {} {}", size_t(id), plotItem.pinIndex, plotItem.displayName, dataIdx);
1252 pinData.rawNodeData.at(dataIdx)->guiTooltip(ImGui::IsKeyDown(ImGuiKey_ModShift), true,
1253 plotItem.displayName.c_str(), tooltipUID.c_str(),
1254 reinterpret_cast<int*>(plotWindow));
1255 }
1256 });
1257
1258 // ###################################################################################
1259
1260 if (plotItem.style.eventsEnabled)
1261 {
1262 if (const auto* item = ImPlot::GetCurrentPlot()->Items.GetItem(plotName.c_str());
1263 item && item->Show)
1264 {
1265 auto stride = plotItem.style.stride ? plotItem.style.stride
1266 : pinData.stride;
1267 auto dataPointCount = static_cast<int>(std::ceil(static_cast<double>(plotData.buffer.size())
1268 / static_cast<double>(stride)));
1269
1270 ImPlot::SetNextMarkerStyle(plotItem.style.eventMarkerStyle,
1271 plotItem.style.eventMarkerSize,
1272 ImPlot::IsColorAuto(plotItem.style.eventMarkerFillColor) ? ImPlot::GetColormapColor(static_cast<int>(plotElementIdx)) : plotItem.style.eventMarkerFillColor,
1273 plotItem.style.eventMarkerWeight,
1274 ImPlot::IsColorAuto(plotItem.style.eventMarkerOutlineColor) ? ImPlot::GetColormapColor(static_cast<int>(plotElementIdx)) : plotItem.style.eventMarkerOutlineColor);
1275 ImPlot::PlotScatter(fmt::format("##{} - Events", plotName).c_str(),
1276 plotDataX.buffer.data(),
1277 plotItem.eventMarker.data(),
1278 dataPointCount,
1279 ImPlotScatterFlags_None,
1280 static_cast<int>(std::ceil(static_cast<double>(plotItem.eventMarker.offset()) / static_cast<double>(stride))),
1281 stride * static_cast<int>(sizeof(double)));
1282
1283 if (ImPlot::IsPlotHovered())
1284 {
1285 constexpr double HOVER_PIXEL_SIZE = 5.0;
1286 auto limits = ImPlot::GetPlotLimits(IMPLOT_AUTO, plotItem.axis);
1287
1288 ImVec2 scaling = ImVec2(static_cast<float>(HOVER_PIXEL_SIZE * (limits.X.Max - limits.X.Min) / ImPlot::GetCurrentPlot()->PlotRect.GetWidth()),
1289 static_cast<float>(HOVER_PIXEL_SIZE * (limits.Y.Max - limits.Y.Min) / ImPlot::GetCurrentPlot()->PlotRect.GetHeight()));
1290 ImPlotPoint mouse = ImPlot::GetPlotMousePos();
1291
1292 std::vector<PlotEventTooltip> tooltips;
1293 for (const auto& e : plotItem.eventTooltips)
1294 {
1295 if (std::abs(mouse.x - std::get<0>(e)) < scaling.x
1296 && std::abs(mouse.y - std::get<1>(e)) < scaling.y)
1297 {
1298 tooltips.push_back(std::get<2>(e));
1299 }
1300 }
1301 if (!tooltips.empty())
1302 {
1303 ImGui::BeginTooltip();
1304 ImGui::PushFont(Application::MonoFont());
1305 for (size_t i = 0; i < tooltips.size(); i++)
1306 {
1307 ImGui::SetNextItemOpen(true, ImGuiCond_Always);
1308 if (ImGui::TreeNode(fmt::format("{} GPST", tooltips.at(i).time.toYMDHMS(GPST)).c_str()))
1309 {
1310 for (const auto& text : tooltips.at(i).texts)
1311 {
1312 ImGui::BulletText("%s", text.c_str());
1313 }
1314 ImGui::TreePop();
1315 }
1316 if (i != tooltips.size() - 1) { ImGui::Separator(); }
1317 }
1318 ImGui::PopFont();
1319 ImGui::EndTooltip();
1320 }
1321 }
1322 }
1323 }
1324
1325 // allow legend item labels to be DND sources
1326 if (ImPlot::BeginDragDropSourceItem(plotName.c_str()))
1327 {
1328 // Data is copied into heap inside the drag and drop
1329 auto pinAndDataIndex = std::make_tuple(plotItem.pinIndex, plotItem.dataIndex, &plotItem.displayName);
1330 ImGui::SetDragDropPayload(fmt::format("DND PlotItem {} - {}", size_t(id), plotIdx).c_str(), &pinAndDataIndex, sizeof(pinAndDataIndex));
1331 ImGui::TextUnformatted(plotData.displayName.c_str());
1332 ImPlot::EndDragDropSource();
1333 }
1334
1335 auto ShowDataReferenceChooser = [&](size_t& dataIdx, const char* label = "") -> bool {
1336 bool changed = false;
1337 const char* preview = dataIdx < pinData.plotData.size()
1338 ? pinData.plotData.at(dataIdx).displayName.c_str()
1339 : "";
1340 if (ImGui::BeginCombo(label, preview))
1341 {
1342 for (size_t plotDataIndex = 0; plotDataIndex < pinData.plotData.size(); plotDataIndex++)
1343 {
1344 auto& plotData = pinData.plotData.at(plotDataIndex);
1345
1346 if (!plotData.hasData) { ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5F); }
1347 const bool is_selected = (dataIdx == plotDataIndex);
1348 if (ImGui::Selectable(pinData.plotData.at(plotDataIndex).displayName.c_str(), is_selected))
1349 {
1350 changed = true;
1351 dataIdx = plotDataIndex;
1352 }
1353 if (!plotData.hasData) { ImGui::PopStyleVar(); }
1354
1355 // Set the initial focus when opening the combo (scrolling + keyboard navigation focus)
1356 if (is_selected) { ImGui::SetItemDefaultFocus(); }
1357 }
1358 ImGui::EndCombo();
1359 }
1360 return changed;
1361 };
1362
1363 if (auto legendReturn = plotItem.style.showLegendPopup(
1364 plotName.c_str(),
1365 fmt::format("Pin {} - {}: {}", plotItem.pinIndex + 1,
1366 pinData.dataIdentifier, plotData.displayName)
1367 .c_str(),
1368 static_cast<int>(plotData.buffer.size()),
1369 static_cast<int>(plotElementIdx),
1370 nameId().c_str(),
1371 plot.lineFlags,
1372 &plotItem.colormapMaskColors,
1373 &plotItem.markerColormapMaskColors,
1374 ShowDataReferenceChooser,
1375 &plotItem.eventMarker,
1376 &plotItem.eventTooltips);
1377 legendReturn.changed)
1378 {
1379 flow::ApplyChanges();
1380
1381 if (legendReturn.errorBoundsReCalcNeeded)
1382 {
1383 for (auto& data : plotItem.errorBoundsData) { data.clear(); }
1384 }
1385 }
1386
1387 plotElementIdx++;
1388 }
1389 }
1390
1391 for (const auto& plotItem : plotItemsToRemove)
1392 {
1393 LOG_WARN("{}: Erasing plot item '{}' from plot '{}', because it does not match the order the data came in",
1394 nameId(), plotItem.displayName, plot.headerText);
1395 std::erase(plot.plotItems, plotItem);
1396 flow::ApplyChanges();
1397 }
1398
1399 auto addDragDropPlotToAxis = [this, plotIdx, &plot](ImAxis dragDropAxis) {
1400 if (const ImGuiPayload* payloadData = ImGui::AcceptDragDropPayload(fmt::format("DND PlotItem {} - {}", size_t(id), plotIdx).c_str()))
1401 {
1402 auto [pinIndex, dataIndex, displayName] = *static_cast<std::tuple<size_t, size_t, std::string*>*>(payloadData->Data);
1403
1404 auto iter = std::ranges::find(plot.plotItems, PlotInfo::PlotItem{ pinIndex, dataIndex, *displayName });
1405 if (iter != plot.plotItems.end()) // Item gets dragged from one axis to another
1406 {
1407 iter->axis = dragDropAxis;
1408 }
1409 else
1410 {
1411 plot.plotItems.emplace_back(pinIndex, dataIndex, *displayName, dragDropAxis);
1412 }
1413 flow::ApplyChanges();
1414 }
1415 };
1416
1417 // allow the main plot area to be a DND target
1418 if (ImPlot::BeginDragDropTargetPlot())
1419 {
1420 addDragDropPlotToAxis(ImAxis_Y1);
1421 ImPlot::EndDragDropTarget();
1422 }
1423 // allow each y-axis to be a DND target
1424 for (ImAxis y = ImAxis_Y1; y <= ImAxis_Y3; ++y)
1425 {
1426 if ((y == ImAxis_Y2 && !(plot.plotFlags & ImPlotFlags_YAxis2))
1427 || (y == ImAxis_Y3 && !(plot.plotFlags & ImPlotFlags_YAxis3)))
1428 {
1429 continue;
1430 }
1431 if (ImPlot::BeginDragDropTargetAxis(y))
1432 {
1433 addDragDropPlotToAxis(y);
1434 ImPlot::EndDragDropTarget();
1435 }
1436 }
1437
1438 ImPlot::EndPlot();
1439 }
1440 }
1441
1442 if (_dragAndDropHeaderIndex >= 0
1443 && plotIdx != static_cast<size_t>(_dragAndDropHeaderIndex - 1)
1444 && plotIdx != static_cast<size_t>(_dragAndDropHeaderIndex))
1445 {
1446 showDragDropTargetHeader(plotIdx + 1);
1447 }
1448 }
1449
1450 if (!dragAndDropHeaderStillInProgress)
1451 {
1452 _dragAndDropHeaderIndex = -1;
1453 }
1454
1455 ImGui::Separator();
1456 if (ImGui::Button(fmt::format("Add Plot##{}", size_t(id)).c_str()))
1457 {
1458 ++_nPlots;
1459 LOG_DEBUG("{}: # Plots changed to {}", nameId(), _nPlots);
1460 flow::ApplyChanges();
1461 updateNumberOfPlots();
1462 }
1463 }
1464
1465 [[nodiscard]] json NAV::Plot::save() const
1466 {
1467 LOG_TRACE("{}: called", nameId());
1468
1469 json j;
1470
1471 j["dynamicInputPins"] = _dynamicInputPins;
1472 j["nPlots"] = _nPlots;
1473 j["pinData"] = _pinData;
1474 j["plots"] = _plots;
1475 j["overridePositionStartValues"] = _overridePositionStartValues;
1476 if (_overridePositionStartValues && _originPosition)
1477 {
1478 j["originPosition"] = _originPosition.value();
1479 }
1480
1481 return j;
1482 }
1483
1484 33 void NAV::Plot::restore(json const& j)
1485 {
1486 LOG_TRACE("{}: called", nameId());
1487
1488
2/2
✓ Branch 1 taken 27 times.
✓ Branch 2 taken 6 times.
33 if (j.contains("dynamicInputPins"))
1489 {
1490 27 NAV::gui::widgets::from_json(j.at("dynamicInputPins"), _dynamicInputPins, this);
1491 }
1492
1/2
✓ Branch 1 taken 33 times.
✗ Branch 2 not taken.
33 if (j.contains("nPlots"))
1493 {
1494 33 j.at("nPlots").get_to(_nPlots);
1495 33 updateNumberOfPlots();
1496 }
1497
1/2
✓ Branch 1 taken 33 times.
✗ Branch 2 not taken.
33 if (j.contains("pinData"))
1498 {
1499 33 j.at("pinData").get_to(_pinData);
1500
1501
2/2
✓ Branch 1 taken 74 times.
✓ Branch 2 taken 33 times.
107 for (size_t inputPinIndex = 0; inputPinIndex < inputPins.size(); inputPinIndex++)
1502 {
1503
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 74 times.
74 if (inputPinIndex >= _pinData.size()) { break; }
1504
1/6
✓ Branch 1 taken 74 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
74 switch (_pinData.at(inputPinIndex).pinType)
1505 {
1506 74 case PinData::PinType::Flow:
1507 74 inputPins.at(inputPinIndex).type = Pin::Type::Flow;
1508 74 inputPins.at(inputPinIndex).dataIdentifier = _dataIdentifier;
1509
1/2
✓ Branch 1 taken 74 times.
✗ Branch 2 not taken.
74 inputPins.at(inputPinIndex).callback = static_cast<InputPin::FlowFirableCallbackFunc>(&Plot::plotFlowData);
1510 74 break;
1511 case Plot::PinData::PinType::Bool:
1512 inputPins.at(inputPinIndex).type = Pin::Type::Bool;
1513 inputPins.at(inputPinIndex).dataIdentifier.clear();
1514 inputPins.at(inputPinIndex).callback = static_cast<InputPin::DataChangedNotifyFunc>(&Plot::plotBoolean);
1515 break;
1516 case Plot::PinData::PinType::Int:
1517 inputPins.at(inputPinIndex).type = Pin::Type::Int;
1518 inputPins.at(inputPinIndex).dataIdentifier.clear();
1519 inputPins.at(inputPinIndex).callback = static_cast<InputPin::DataChangedNotifyFunc>(&Plot::plotInteger);
1520 break;
1521 case Plot::PinData::PinType::Float:
1522 inputPins.at(inputPinIndex).type = Pin::Type::Float;
1523 inputPins.at(inputPinIndex).dataIdentifier.clear();
1524 inputPins.at(inputPinIndex).callback = static_cast<InputPin::DataChangedNotifyFunc>(&Plot::plotFloat);
1525 break;
1526 case Plot::PinData::PinType::Matrix:
1527 inputPins.at(inputPinIndex).type = Pin::Type::Matrix;
1528 inputPins.at(inputPinIndex).dataIdentifier = { "Eigen::MatrixXd", "Eigen::VectorXd" };
1529 inputPins.at(inputPinIndex).callback = static_cast<InputPin::DataChangedNotifyFunc>(&Plot::plotMatrix);
1530 break;
1531 default:
1532 break;
1533 }
1534 }
1535 }
1536
1/2
✓ Branch 1 taken 33 times.
✗ Branch 2 not taken.
33 if (j.contains("plots"))
1537 {
1538 33 j.at("plots").get_to(_plots);
1539 }
1540
1/2
✓ Branch 1 taken 33 times.
✗ Branch 2 not taken.
33 if (j.contains("overridePositionStartValues"))
1541 {
1542 33 j.at("overridePositionStartValues").get_to(_overridePositionStartValues);
1543 }
1544
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 32 times.
33 if (_overridePositionStartValues)
1545 {
1546
1/2
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
1 if (j.contains("originPosition"))
1547 {
1548
2/4
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 _originPosition = j.at("originPosition").get<gui::widgets::PositionWithFrame>();
1549 }
1550 else
1551 {
1552 _originPosition = gui::widgets::PositionWithFrame();
1553 }
1554 }
1555 33 }
1556
1557 99 bool NAV::Plot::initialize()
1558 {
1559 LOG_TRACE("{}: called", nameId());
1560
1561 99 CommonLog::initialize();
1562
1563 99 _startTime.reset();
1564
2/2
✓ Branch 0 taken 96 times.
✓ Branch 1 taken 3 times.
99 if (!_overridePositionStartValues) { _originPosition.reset(); }
1565
1566
2/2
✓ Branch 5 taken 483 times.
✓ Branch 6 taken 99 times.
582 for (auto& plot : _plots)
1567 {
1568 483 plot.tooltips.clear();
1569
2/2
✓ Branch 5 taken 1212 times.
✓ Branch 6 taken 483 times.
1694 for (auto& plotItem : plot.plotItems)
1570 {
1571
1/2
✓ Branch 1 taken 1212 times.
✗ Branch 2 not taken.
1212 plotItem.colormapMaskColors.clear();
1572
1/2
✓ Branch 1 taken 1212 times.
✗ Branch 2 not taken.
1212 plotItem.markerColormapMaskColors.clear();
1573
3/4
✓ Branch 1 taken 2423 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2424 times.
✓ Branch 4 taken 1211 times.
3635 for (auto& data : plotItem.errorBoundsData) { data.clear(); }
1574
1/2
✓ Branch 1 taken 1212 times.
✗ Branch 2 not taken.
1211 plotItem.eventMarker.clear();
1575 1212 plotItem.eventTooltips.clear();
1576 }
1577 }
1578
2/2
✓ Branch 5 taken 234 times.
✓ Branch 6 taken 99 times.
333 for (auto& pinData : _pinData)
1579 {
1580
1/2
✓ Branch 1 taken 234 times.
✗ Branch 2 not taken.
234 std::scoped_lock<std::mutex> guard(pinData.mutex); // Lock the buffer for multithreaded access
1581
1582
1/2
✓ Branch 1 taken 234 times.
✗ Branch 2 not taken.
234 pinData.rawNodeData.clear();
1583
1584
2/2
✓ Branch 5 taken 14135 times.
✓ Branch 6 taken 234 times.
14371 for (auto& plotData : pinData.plotData)
1585 {
1586 14138 plotData.hasData = false;
1587
1/2
✓ Branch 1 taken 14137 times.
✗ Branch 2 not taken.
14138 plotData.buffer.clear();
1588 }
1589
5/6
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 222 times.
✓ Branch 3 taken 12 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 12 times.
✓ Branch 6 taken 222 times.
234 if (pinData.dynamicDataStartIndex != -1 && static_cast<int>(pinData.plotData.size()) >= pinData.dynamicDataStartIndex) // Erase all dynamic data
1590 {
1591
1/2
✓ Branch 6 taken 12 times.
✗ Branch 7 not taken.
12 pinData.plotData.erase(pinData.plotData.begin() + pinData.dynamicDataStartIndex, pinData.plotData.end());
1592 }
1593 234 pinData.events.clear();
1594 234 }
1595
1596 99 return true;
1597 }
1598
1599 33 void NAV::Plot::deinitialize()
1600 {
1601 LOG_TRACE("{}: called", nameId());
1602 33 }
1603
1604 73 void NAV::Plot::afterCreateLink(OutputPin& startPin, InputPin& endPin)
1605 {
1606 LOG_TRACE("{}: called for {} ==> {}", nameId(), size_t(startPin.id), size_t(endPin.id));
1607
1608 73 size_t pinIndex = inputPinIndexFromId(endPin.id);
1609
1610
3/4
✓ Branch 1 taken 73 times.
✗ Branch 2 not taken.
✓ Branch 7 taken 3921 times.
✓ Branch 8 taken 73 times.
3994 for (auto& plotData : _pinData.at(pinIndex).plotData) // Mark all plot data for deletion
1611 {
1612 3921 plotData.markedForDelete = true;
1613 }
1614
1615 73 size_t i = 0;
1616
1617
2/4
✓ Branch 1 taken 73 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 73 times.
✗ Branch 5 not taken.
73 if (inputPins.at(pinIndex).type == Pin::Type::Flow)
1618 {
1619
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 73 times.
73 if (startPin.dataIdentifier.size() > 1)
1620 {
1621 // Happens if connected Node supports multiple output values which can be chosen, but the node did not load yet.
1622 // But it will and then recreate the link
1623 return;
1624 }
1625
1/2
✗ Branch 3 not taken.
✓ Branch 4 taken 73 times.
73 if (_pinData.at(pinIndex).dataIdentifier != startPin.dataIdentifier.front())
1626 {
1627 _pinData.at(pinIndex).plotData.clear();
1628 for (auto& plot : _plots)
1629 {
1630 while (true)
1631 {
1632 auto plotItemIter = std::ranges::find_if(plot.plotItems,
1633 [pinIndex](const PlotInfo::PlotItem& plotItem) { return plotItem.pinIndex == pinIndex; });
1634 if (plotItemIter != plot.plotItems.end())
1635 {
1636 plot.plotItems.erase(plotItemIter);
1637 }
1638 else
1639 {
1640 break;
1641 }
1642 }
1643 }
1644 }
1645
1646 73 _pinData.at(pinIndex).dataIdentifier = startPin.dataIdentifier.front();
1647
1648 // NodeData
1649
2/4
✓ Branch 2 taken 73 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 73 times.
✗ Branch 6 not taken.
146 _pinData.at(pinIndex).addPlotDataItem(i++, "Time [s]");
1650
2/4
✓ Branch 2 taken 73 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 73 times.
✗ Branch 6 not taken.
146 _pinData.at(pinIndex).addPlotDataItem(i++, "GPST Time");
1651
2/4
✓ Branch 2 taken 73 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 73 times.
✗ Branch 6 not taken.
146 _pinData.at(pinIndex).addPlotDataItem(i++, "GPS time of week [s]");
1652
1653
3/4
✓ Branch 1 taken 73 times.
✗ Branch 2 not taken.
✓ Branch 8 taken 4552 times.
✓ Branch 9 taken 73 times.
4625 for (const auto& desc : NAV::NodeRegistry::GetStaticDataDescriptors(startPin.dataIdentifier))
1654 {
1655
2/4
✓ Branch 1 taken 4552 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 4552 times.
✗ Branch 5 not taken.
4552 _pinData.at(pinIndex).addPlotDataItem(i++, desc);
1656 73 }
1657
2/2
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 69 times.
73 if (NAV::NodeRegistry::TypeHasDynamicData(startPin.dataIdentifier.front()))
1658 {
1659 4 _pinData.at(pinIndex).dynamicDataStartIndex = static_cast<int>(i);
1660 }
1661 }
1662 else
1663 {
1664 _pinData.at(pinIndex).dataIdentifier = startPin.name;
1665
1666 // NodeData
1667 _pinData.at(pinIndex).addPlotDataItem(i++, "Time [s]");
1668 _pinData.at(pinIndex).addPlotDataItem(i++, "GPST Time");
1669 _pinData.at(pinIndex).addPlotDataItem(i++, "GPS time of week [s]");
1670
1671 if (inputPins.at(pinIndex).type == Pin::Type::Bool)
1672 {
1673 _pinData.at(pinIndex).addPlotDataItem(i++, "Boolean");
1674 }
1675 else if (inputPins.at(pinIndex).type == Pin::Type::Int)
1676 {
1677 _pinData.at(pinIndex).addPlotDataItem(i++, "Integer");
1678 }
1679 else if (inputPins.at(pinIndex).type == Pin::Type::Float)
1680 {
1681 _pinData.at(pinIndex).addPlotDataItem(i++, "Float");
1682 }
1683 else if (inputPins.at(pinIndex).type == Pin::Type::Matrix)
1684 {
1685 if (startPin.dataIdentifier.front() == "Eigen::MatrixXd")
1686 {
1687 if (auto matrix = getInputValue<Eigen::MatrixXd>(pinIndex))
1688 {
1689 for (int row = 0; row < matrix->v->rows(); row++)
1690 {
1691 for (int col = 0; col < matrix->v->cols(); col++)
1692 {
1693 _pinData.at(pinIndex).addPlotDataItem(i++, fmt::format("{}, {}", row, col));
1694 }
1695 }
1696 }
1697 }
1698 else if (startPin.dataIdentifier.front() == "Eigen::VectorXd")
1699 {
1700 if (auto matrix = getInputValue<Eigen::VectorXd>(pinIndex))
1701 {
1702 for (int row = 0; row < matrix->v->rows(); row++)
1703 {
1704 _pinData.at(pinIndex).addPlotDataItem(i++, fmt::format("{}", row));
1705 }
1706 }
1707 }
1708
1709 for (size_t dataIndex = i; dataIndex < _pinData.at(pinIndex).plotData.size(); dataIndex++)
1710 {
1711 const auto& displayName = _pinData.at(pinIndex).plotData.at(dataIndex).displayName;
1712 for (auto& plot : _plots)
1713 {
1714 auto plotItemIter = std::ranges::find(plot.plotItems, PlotInfo::PlotItem{ pinIndex, dataIndex, displayName });
1715 if (plotItemIter != plot.plotItems.end())
1716 {
1717 plot.plotItems.erase(plotItemIter);
1718 }
1719 }
1720 }
1721 }
1722 }
1723
1724
2/2
✓ Branch 2 taken 5099 times.
✓ Branch 3 taken 73 times.
5172 for (size_t dataIndex = 0; dataIndex < _pinData.at(pinIndex).plotData.size(); ++dataIndex)
1725 {
1726
1/2
✓ Branch 1 taken 5099 times.
✗ Branch 2 not taken.
5099 auto iter = _pinData.at(pinIndex).plotData.begin();
1727 std::advance(iter, dataIndex);
1728
2/2
✓ Branch 1 taken 400 times.
✓ Branch 2 taken 4699 times.
5099 if (iter->markedForDelete)
1729 {
1730
2/4
✓ Branch 1 taken 400 times.
✗ Branch 2 not taken.
✓ Branch 5 taken 400 times.
✗ Branch 6 not taken.
400 _pinData.at(pinIndex).plotData.erase(iter);
1731 400 --dataIndex;
1732 }
1733 }
1734
1735
2/2
✓ Branch 5 taken 415 times.
✓ Branch 6 taken 73 times.
488 for (auto& plot : _plots)
1736 {
1737
3/6
✓ Branch 1 taken 415 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 415 times.
✗ Branch 5 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 415 times.
415 if (plot.selectedXdata.at(pinIndex) > _pinData.at(pinIndex).plotData.size())
1738 {
1739 plot.selectedXdata.at(pinIndex) = 1;
1740 }
1741 }
1742 }
1743
1744 33 void NAV::Plot::updateNumberOfPlots()
1745 {
1746
2/2
✓ Branch 1 taken 161 times.
✓ Branch 2 taken 33 times.
194 while (_nPlots > _plots.size())
1747 {
1748
1/2
✓ Branch 4 taken 161 times.
✗ Branch 5 not taken.
322 _plots.emplace_back(fmt::format("Plot {}", _plots.size() + 1), inputPins.size());
1749 }
1750
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 33 times.
33 while (_nPlots < _plots.size())
1751 {
1752 _plots.pop_back();
1753 }
1754 33 }
1755
1756 186 void NAV::Plot::pinAddCallback(Node* node)
1757 {
1758 186 auto* plotNode = static_cast<Plot*>(node); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
1759
1760
1/2
✓ Branch 5 taken 186 times.
✗ Branch 6 not taken.
372 nm::CreateInputPin(node, fmt::format("Pin {}", node->inputPins.size() + 1).c_str(), Pin::Type::Flow, plotNode->_dataIdentifier, &Plot::plotFlowData);
1761 186 plotNode->_pinData.emplace_back();
1762
1/2
✗ Branch 4 not taken.
✓ Branch 5 taken 186 times.
186 for (auto& plot : plotNode->_plots)
1763 {
1764 plot.selectedXdata.emplace_back(1);
1765 }
1766 186 }
1767
1768 void NAV::Plot::pinDeleteCallback(Node* node, size_t pinIdx)
1769 {
1770 auto* plotNode = static_cast<Plot*>(node); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
1771
1772 for (auto& plot : plotNode->_plots)
1773 {
1774 if (plot.selectedPin >= pinIdx && plot.selectedPin > 0)
1775 {
1776 plot.selectedPin -= 1;
1777 }
1778 for (int64_t plotItemIdx = 0; plotItemIdx < static_cast<int64_t>(plot.plotItems.size()); ++plotItemIdx)
1779 {
1780 auto& plotItem = plot.plotItems.at(static_cast<size_t>(plotItemIdx));
1781
1782 if (plotItem.pinIndex == pinIdx) // The index we want to delete
1783 {
1784 plot.plotItems.erase(plot.plotItems.begin() + plotItemIdx);
1785 plotItemIdx -= 1;
1786 }
1787 else if (plotItem.pinIndex > pinIdx) // Index higher -> Decrement
1788 {
1789 plotItem.pinIndex -= 1;
1790 }
1791 }
1792
1793 plot.selectedXdata.erase(std::next(plot.selectedXdata.begin(), static_cast<int64_t>(pinIdx)));
1794 }
1795
1796 nm::DeleteInputPin(node->inputPins.at(pinIdx));
1797 plotNode->_pinData.erase(std::next(plotNode->_pinData.begin(), static_cast<int64_t>(pinIdx)));
1798 }
1799
1800 void NAV::Plot::addEvent(size_t pinIndex, InsTime insTime, const std::string& text, int32_t dataIndex)
1801 {
1802 if (!insTime.empty() && !_startTime.empty())
1803 {
1804 double relTime = static_cast<double>((insTime - _startTime).count());
1805 _pinData.at(pinIndex).events.emplace_back(relTime, insTime, text, dataIndex);
1806 }
1807 }
1808
1809 void NAV::Plot::addData(size_t pinIndex, size_t dataIndex, double value)
1810 {
1811 auto& plotData = _pinData.at(pinIndex).plotData.at(dataIndex);
1812
1813 plotData.buffer.push_back(value);
1814 if (!std::isnan(value))
1815 {
1816 plotData.hasData = true;
1817 }
1818 }
1819
1820 size_t NAV::Plot::addData(size_t pinIndex, std::string displayName, double value)
1821 {
1822 auto& pinData = _pinData.at(pinIndex);
1823
1824 auto plotData = std::ranges::find_if(pinData.plotData, [&](const auto& data) {
1825 return data.displayName == displayName;
1826 });
1827 if (plotData == pinData.plotData.end()) // Item is new
1828 {
1829 pinData.addPlotDataItem(pinData.plotData.size(), displayName);
1830 plotData = pinData.plotData.end() - 1;
1831 plotData->isDynamic = true;
1832
1833 // We assume, there is a static item at the front (the time)
1834 for (size_t i = plotData->buffer.size(); i < pinData.plotData.front().buffer.size() - 1; i++) // Add empty NaN values to shift it to the correct start point
1835 {
1836 plotData->buffer.push_back(std::nan(""));
1837 }
1838 }
1839 else
1840 {
1841 // Item was there, but it could have been missing and came again
1842 for (size_t i = plotData->buffer.size(); i < pinData.plotData.front().buffer.size() - 1; i++) // Add empty NaN values to shift it to the correct start point
1843 {
1844 plotData->buffer.push_back(std::nan(""));
1845 }
1846 }
1847 auto dataIndex = static_cast<size_t>(plotData - pinData.plotData.begin());
1848 addData(pinIndex, dataIndex, value);
1849 return dataIndex;
1850 }
1851
1852 NAV::CommonLog::LocalPosition NAV::Plot::calcLocalPosition(const Eigen::Vector3d& lla_position)
1853 {
1854 if (!_originPosition)
1855 {
1856 _originPosition = { .frame = gui::widgets::PositionWithFrame::ReferenceFrame::ECEF,
1857 .e_position = trafo::lla2ecef_WGS84(lla_position) };
1858 }
1859
1860 int sign = lla_position.x() > _originPosition->latitude() ? 1 : -1;
1861 // North/South deviation [m]
1862 double northSouth = calcGeographicalDistance(lla_position.x(), lla_position.y(),
1863 _originPosition->latitude(), lla_position.y())
1864 * sign;
1865
1866 sign = lla_position.y() > _originPosition->longitude() ? 1 : -1;
1867 // East/West deviation [m]
1868 double eastWest = calcGeographicalDistance(lla_position.x(), lla_position.y(),
1869 lla_position.x(), _originPosition->longitude())
1870 * sign;
1871
1872 return { .northSouth = northSouth, .eastWest = eastWest };
1873 }
1874
1875 void NAV::Plot::plotBoolean(const InsTime& insTime, size_t pinIdx)
1876 {
1877 LOG_DATA("{}: Plotting boolean on pin '{}' with time {}", nameId(), inputPins[pinIdx].name, insTime.toYMDHMS());
1878 if (ConfigManager::Get<bool>("nogui"))
1879 {
1880 releaseInputValue(pinIdx);
1881 return;
1882 }
1883
1884 if (auto value = getInputValue<bool>(pinIdx);
1885 value && !insTime.empty())
1886 {
1887 if (_startTime.empty()) { _startTime = insTime; }
1888 size_t i = 0;
1889
1890 std::scoped_lock<std::mutex> guard(_pinData.at(pinIdx).mutex);
1891
1892 // NodeData
1893 addData(pinIdx, i++, static_cast<double>((insTime - _startTime).count()));
1894 addData(pinIdx, i++, static_cast<double>(insTime.toUnixTime() + insTime.differenceToUTC(GPST)));
1895 addData(pinIdx, i++, static_cast<double>(insTime.toGPSweekTow().tow));
1896 // Boolean
1897 addData(pinIdx, i++, static_cast<double>(*value->v));
1898 }
1899 }
1900
1901 void NAV::Plot::plotInteger(const InsTime& insTime, size_t pinIdx)
1902 {
1903 LOG_DATA("{}: Plotting integer on pin '{}' with time {}", nameId(), inputPins[pinIdx].name, insTime.toYMDHMS());
1904 if (ConfigManager::Get<bool>("nogui"))
1905 {
1906 releaseInputValue(pinIdx);
1907 return;
1908 }
1909
1910 if (auto value = getInputValue<int>(pinIdx);
1911 value && !insTime.empty())
1912 {
1913 if (_startTime.empty()) { _startTime = insTime; }
1914 size_t i = 0;
1915
1916 std::scoped_lock<std::mutex> guard(_pinData.at(pinIdx).mutex);
1917
1918 // NodeData
1919 addData(pinIdx, i++, static_cast<double>((insTime - _startTime).count()));
1920 addData(pinIdx, i++, static_cast<double>(insTime.toUnixTime() + insTime.differenceToUTC(GPST)));
1921 addData(pinIdx, i++, static_cast<double>(insTime.toGPSweekTow().tow));
1922 // Integer
1923 addData(pinIdx, i++, static_cast<double>(*value->v));
1924 }
1925 }
1926
1927 void NAV::Plot::plotFloat(const InsTime& insTime, size_t pinIdx)
1928 {
1929 LOG_DATA("{}: Plotting float on pin '{}' with time {}", nameId(), inputPins[pinIdx].name, insTime.toYMDHMS());
1930 if (ConfigManager::Get<bool>("nogui"))
1931 {
1932 releaseInputValue(pinIdx);
1933 return;
1934 }
1935
1936 if (auto value = getInputValue<double>(pinIdx);
1937 value && !insTime.empty())
1938 {
1939 if (_startTime.empty()) { _startTime = insTime; }
1940 size_t i = 0;
1941
1942 std::scoped_lock<std::mutex> guard(_pinData.at(pinIdx).mutex);
1943
1944 // NodeData
1945 addData(pinIdx, i++, static_cast<double>((insTime - _startTime).count()));
1946 addData(pinIdx, i++, static_cast<double>(insTime.toUnixTime() + insTime.differenceToUTC(GPST)));
1947 addData(pinIdx, i++, static_cast<double>(insTime.toGPSweekTow().tow));
1948 // Double
1949 addData(pinIdx, i++, *value->v);
1950 }
1951 }
1952
1953 void NAV::Plot::plotMatrix(const InsTime& insTime, size_t pinIdx)
1954 {
1955 LOG_DATA("{}: Plotting matrix on pin '{}' with time {}", nameId(), inputPins[pinIdx].name, insTime.toYMDHMS());
1956 if (ConfigManager::Get<bool>("nogui"))
1957 {
1958 releaseInputValue(pinIdx);
1959 return;
1960 }
1961
1962 if (auto* sourcePin = inputPins.at(pinIdx).link.getConnectedPin())
1963 {
1964 if (sourcePin->dataIdentifier.front() == "Eigen::MatrixXd")
1965 {
1966 if (auto value = getInputValue<Eigen::MatrixXd>(pinIdx);
1967 value && !insTime.empty())
1968 {
1969 if (_startTime.empty()) { _startTime = insTime; }
1970 size_t i = 0;
1971
1972 std::scoped_lock<std::mutex> guard(_pinData.at(pinIdx).mutex);
1973
1974 // NodeData
1975 addData(pinIdx, i++, static_cast<double>((insTime - _startTime).count()));
1976 addData(pinIdx, i++, static_cast<double>(insTime.toUnixTime() + insTime.differenceToUTC(GPST)));
1977 addData(pinIdx, i++, static_cast<double>(insTime.toGPSweekTow().tow));
1978 // Matrix
1979 for (int row = 0; row < value->v->rows(); row++)
1980 {
1981 for (int col = 0; col < value->v->cols(); col++)
1982 {
1983 addData(pinIdx, i++, (*value->v)(row, col));
1984 }
1985 }
1986 }
1987 }
1988 else if (sourcePin->dataIdentifier.front() == "Eigen::VectorXd")
1989 {
1990 if (auto value = getInputValue<Eigen::VectorXd>(pinIdx);
1991 value && !insTime.empty())
1992 {
1993 if (_startTime.empty()) { _startTime = insTime; }
1994 size_t i = 0;
1995
1996 std::scoped_lock<std::mutex> guard(_pinData.at(pinIdx).mutex);
1997
1998 // NodeData
1999 addData(pinIdx, i++, static_cast<double>((insTime - _startTime).count()));
2000 addData(pinIdx, i++, static_cast<double>(insTime.toUnixTime() + insTime.differenceToUTC(GPST)));
2001 addData(pinIdx, i++, static_cast<double>(insTime.toGPSweekTow().tow));
2002 // Vector
2003 for (int row = 0; row < value->v->rows(); row++)
2004 {
2005 addData(pinIdx, i++, (*value->v)(row));
2006 }
2007 }
2008 }
2009 else
2010 {
2011 releaseInputValue(pinIdx);
2012 }
2013 }
2014 }
2015
2016 209540 void NAV::Plot::plotFlowData(NAV::InputPin::NodeDataQueue& queue, size_t pinIdx)
2017 {
2018
1/2
✓ Branch 1 taken 208844 times.
✗ Branch 2 not taken.
209540 auto nodeData = queue.extract_front();
2019
2020
3/6
✓ Branch 1 taken 209045 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 209584 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 209193 times.
✗ Branch 8 not taken.
417663 if (ConfigManager::Get<bool>("nogui")) { return; }
2021
2022 LOG_DATA("{}: Plotting data on pin '{}' with time {} GPST", nameId(), inputPins[pinIdx].name, nodeData->insTime.toYMDHMS(GPST));
2023
2024 std::scoped_lock<std::mutex> guard(_pinData.at(pinIdx).mutex);
2025 _pinData.at(pinIdx).rawNodeData.push_back(nodeData);
2026
2027 // NodeData
2028 size_t i = 0;
2029 addData(pinIdx, i++, CommonLog::calcTimeIntoRun(nodeData->insTime));
2030 addData(pinIdx, i++, static_cast<double>(nodeData->insTime.toUnixTime() + nodeData->insTime.differenceToUTC(GPST)));
2031 addData(pinIdx, i++, static_cast<double>(nodeData->insTime.toGPSweekTow(GPST).tow));
2032
2033 if (auto* sourcePin = inputPins.at(pinIdx).link.getConnectedPin())
2034 {
2035 LOG_DATA("{}: Connected Pin data identifier: [{}]", nameId(), joinToString(sourcePin->dataIdentifier));
2036 // -------------------------------------------- General ----------------------------------------------
2037 if (sourcePin->dataIdentifier.front() == DynamicData::type())
2038 {
2039 plotData(std::static_pointer_cast<const DynamicData>(nodeData), pinIdx, i);
2040 }
2041 // --------------------------------------------- GNSS ------------------------------------------------
2042 else if (NAV::NodeRegistry::NodeDataTypeAnyIsChildOf(sourcePin->dataIdentifier, { GnssCombination::type() }))
2043 {
2044 plotData(std::static_pointer_cast<const GnssCombination>(nodeData), pinIdx, i);
2045 }
2046 else if (NAV::NodeRegistry::NodeDataTypeAnyIsChildOf(sourcePin->dataIdentifier, { GnssObs::type() }))
2047 {
2048 plotData(std::static_pointer_cast<const GnssObs>(nodeData), pinIdx, i);
2049 }
2050 // ---------------------------------------------- IMU ------------------------------------------------
2051 else if (NAV::NodeRegistry::NodeDataTypeAnyIsChildOf(sourcePin->dataIdentifier, { ImuObsSimulated::type() }))
2052 {
2053 plotData(std::static_pointer_cast<const ImuObsSimulated>(nodeData), pinIdx, i);
2054 }
2055 else if (NAV::NodeRegistry::NodeDataTypeAnyIsChildOf(sourcePin->dataIdentifier, { ImuObsWDelta::type() }))
2056 {
2057 plotData(std::static_pointer_cast<const ImuObsWDelta>(nodeData), pinIdx, i);
2058 }
2059 else if (NAV::NodeRegistry::NodeDataTypeAnyIsChildOf(sourcePin->dataIdentifier, { KvhObs::type() }))
2060 {
2061 plotData(std::static_pointer_cast<const KvhObs>(nodeData), pinIdx, i);
2062 }
2063 else if (NAV::NodeRegistry::NodeDataTypeAnyIsChildOf(sourcePin->dataIdentifier, { ImuObs::type() }))
2064 {
2065 plotData(std::static_pointer_cast<const ImuObs>(nodeData), pinIdx, i);
2066 }
2067 else if (NAV::NodeRegistry::NodeDataTypeAnyIsChildOf(sourcePin->dataIdentifier, { VectorNavBinaryOutput::type() }))
2068 {
2069 plotData(std::static_pointer_cast<const VectorNavBinaryOutput>(nodeData), pinIdx, i);
2070 }
2071 // --------------------------------------------- State -----------------------------------------------
2072 else if (NAV::NodeRegistry::NodeDataTypeAnyIsChildOf(sourcePin->dataIdentifier, { Pos::type() }))
2073 {
2074 auto obs = std::static_pointer_cast<const Pos>(nodeData);
2075 auto localPosition = calcLocalPosition(obs->lla_position());
2076
2077 for (size_t j = 0; j < Pos::GetStaticDescriptorCount(); ++j)
2078 {
2079 if (j == 3) { addData(pinIdx, i++, localPosition.northSouth); }
2080 else if (j == 4) { addData(pinIdx, i++, localPosition.eastWest); }
2081 else { addData(pinIdx, i++, obs->getValueAtOrNaN(j)); }
2082 }
2083
2084 if (NAV::NodeRegistry::NodeDataTypeAnyIsChildOf(sourcePin->dataIdentifier, { InsGnssTCKFSolution::type() }))
2085 {
2086 plotData(std::static_pointer_cast<const InsGnssTCKFSolution>(nodeData), pinIdx, i, Pos::GetStaticDescriptorCount());
2087 }
2088 else if (NAV::NodeRegistry::NodeDataTypeAnyIsChildOf(sourcePin->dataIdentifier, { InsGnssLCKFSolution::type() }))
2089 {
2090 plotData(std::static_pointer_cast<const InsGnssLCKFSolution>(nodeData), pinIdx, i, Pos::GetStaticDescriptorCount());
2091 }
2092 else if (NAV::NodeRegistry::NodeDataTypeAnyIsChildOf(sourcePin->dataIdentifier, { PosVelAtt::type() }))
2093 {
2094 plotData(std::static_pointer_cast<const PosVelAtt>(nodeData), pinIdx, i, Pos::GetStaticDescriptorCount());
2095 }
2096 else if (NAV::NodeRegistry::NodeDataTypeAnyIsChildOf(sourcePin->dataIdentifier, { SppSolution::type() }))
2097 {
2098 plotData(std::static_pointer_cast<const SppSolution>(nodeData), pinIdx, i, Pos::GetStaticDescriptorCount());
2099 }
2100 else if (NAV::NodeRegistry::NodeDataTypeAnyIsChildOf(sourcePin->dataIdentifier, { RtklibPosObs::type() }))
2101 {
2102 plotData(std::static_pointer_cast<const RtklibPosObs>(nodeData), pinIdx, i, Pos::GetStaticDescriptorCount());
2103 }
2104 else if (NAV::NodeRegistry::NodeDataTypeAnyIsChildOf(sourcePin->dataIdentifier, { PosVel::type() }))
2105 {
2106 plotData(std::static_pointer_cast<const PosVel>(nodeData), pinIdx, i, Pos::GetStaticDescriptorCount());
2107 }
2108 }
2109
2110 for (const auto& [displayName, value] : nodeData->getDynamicData())
2111 {
2112 addData(pinIdx, displayName, value);
2113 }
2114
2115 for (const auto& event : nodeData->events())
2116 {
2117 addEvent(pinIdx, nodeData->insTime, event, -1);
2118 }
2119 }
2120
2121 size_t s = 0;
2122 for (auto& plotData : _pinData.at(pinIdx).plotData)
2123 {
2124 if (s == 0) { s = plotData.buffer.size(); }
2125 if (s != plotData.buffer.size())
2126 {
2127 plotData.buffer.push_back(std::nan(""));
2128 LOG_DATA("{}: [{}] Adding NaN to pin '{} (idx {})' data buffer for '{}' to make all buffers same size", nameId(), nodeData->insTime.toYMDHMS(GPST),
2129 inputPins.at(pinIdx).name, pinIdx, plotData.displayName);
2130 }
2131 INS_ASSERT_USER_ERROR(s == plotData.buffer.size(), "All buffers should have the same size");
2132 }
2133 209193 }
2134