INSTINCT Code Coverage Report


Directory: src/
File: Nodes/Plotting/Plot.cpp
Date: 2025-11-25 23:34:18
Exec Total Coverage
Lines: 168 1149 14.6%
Functions: 19 53 35.8%
Branches: 149 2422 6.2%

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