QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
LogViewerBaseChart.qml
Go to the documentation of this file.
1import QtQuick
2import QtQuick.Layouts
3import QtGraphs
4
5import QGroundControl
6import QGroundControl.Controls
7
8/// Shared chart base for the Log Viewer.
9///
10/// Provides: GraphsView with QGC-themed GraphsTheme, a HH:MM:SS x-axis
11/// labelDelegate, zoom/cursor interaction, position marker line, and
12/// LogViewerCursorPopup.
13///
14/// Derived charts (LogViewerChart, LogViewerAltChart) embed this as an Item,
15/// connect to zoomRangeSet / cursorPositionSet to refresh their series data,
16/// and supply popup extra rows as declarative children (forwarded via the
17/// default property alias to the popup's extraContent).
18Item {
19 id: control
20
21 // -------------------------------------------------------------------------
22 // Required
23 // -------------------------------------------------------------------------
24 required property var logParser
25
26 // -------------------------------------------------------------------------
27 // Optional
28 // -------------------------------------------------------------------------
29 property bool xAxisShowLocalTime: false
30 property string yAxisTitle: ""
31 property real popupYOffset: 0 ///< added to popup y (alt chart offsets slightly)
32
33 // -------------------------------------------------------------------------
34 // Read-only aliases to child items — derived charts access these
35 // -------------------------------------------------------------------------
36 readonly property alias graphsView: _chart
37 readonly property alias xAxis: _xAxis
38 readonly property alias yAxis: _yAxis
39 readonly property alias colorBlockWidth: _popup.colorBlockWidth
40 readonly property real popupWidth: _popup.implicitWidth
41
42 // -------------------------------------------------------------------------
43 // Default property — children declared inside LogViewerBaseChart { } go
44 // into the popup's extraContent column (chart-specific popup rows).
45 // -------------------------------------------------------------------------
46 default property alias content: _popup.extraContent
47
48 // -------------------------------------------------------------------------
49 // Signals
50 // -------------------------------------------------------------------------
51
52 /// Emitted only on user-driven cursor interaction.
53 /// Do NOT re-emit this when handling an external sync (setSharedCursor).
54 signal cursorMoved(real t)
55
56 /// Emitted only on user-driven zoom.
57 /// Do NOT re-emit this when handling an external sync (setSharedZoom).
58 signal zoomApplied(real minX, real maxX)
59
60 /// Emitted whenever the zoom range changes (user-driven or external sync).
61 /// Derived charts connect here to refresh their series data.
62 signal zoomRangeSet(real minX, real maxX)
63
64 /// Emitted whenever the cursor position changes (user-driven or external sync).
65 /// Derived charts connect here to update popup values (e.g. _markerAltValue).
66 signal cursorPositionSet(real t)
67
68 // -------------------------------------------------------------------------
69 // State — writable by base, readable by derived charts
70 // -------------------------------------------------------------------------
71 property bool markerVisible: false
72 property real markerPixelX: 0
73 property real markerXValue: 0
74 property real fullMinX: 0
75 property real fullMaxX: 1
76 property real zoomMinX: 0
77 property real zoomMaxX: 1
78
79 // -------------------------------------------------------------------------
80 // Public API
81 // -------------------------------------------------------------------------
82
83 /// Set the full and zoom range (called on log load).
84 /// Emits zoomRangeSet so derived charts can refresh their series.
85 function initRange(minX, maxX) {
86 // Small margin each side so data doesn't touch the edges; timestamps are time since boot so never below 0
87 const pad = (maxX - minX) * 0.025
88 const paddedMinX = Math.max(0, minX - pad)
89 const paddedMaxX = maxX + pad
90 fullMinX = paddedMinX
91 fullMaxX = paddedMaxX
92 zoomMinX = paddedMinX
93 zoomMaxX = paddedMaxX
94 _xAxis.min = paddedMinX
95 _xAxis.max = paddedMaxX
96 zoomRangeSet(paddedMinX, paddedMaxX)
97 }
98
99 /// User-driven zoom — also emits zoomApplied for cross-chart sync.
100 function applyZoomRange(minX, maxX) {
101 _applyZoomInternal(minX, maxX)
102 zoomApplied(minX, maxX)
103 }
104
105 /// External zoom sync — does not re-emit zoomApplied.
106 function setSharedZoom(minX, maxX) {
107 _applyZoomInternal(minX, maxX)
108 }
109
110 /// Place the cursor at axis value t and emit cursorPositionSet.
111 /// Does NOT emit cursorMoved — use for both external sync and initial placement.
112 function setCursor(t) {
113 if (_xAxis.max <= _xAxis.min) return
114 markerXValue = t
115 markerPixelX = _axisXToPixel(t)
116 markerVisible = true
117 cursorPositionSet(t)
118 }
119
120 /// External cursor sync — delegates to setCursor (no cursorMoved emitted).
121 function setSharedCursor(t) {
122 setCursor(t)
123 }
124
125 function resetZoom() {
126 applyZoomRange(fullMinX, fullMaxX)
127 }
128
129 function clearCursor() {
130 markerVisible = false
131 }
132
133 // -------------------------------------------------------------------------
134 // Internal helpers
135 // -------------------------------------------------------------------------
136 function _pixelToAxisX(pixelX) {
137 const plotX = _chart.plotArea.x
138 const plotW = _chart.plotArea.width
139 if (plotW <= 0 || _xAxis.max <= _xAxis.min) return _xAxis.min
140 const ratio = (Math.max(plotX, Math.min(plotX + plotW, pixelX)) - plotX) / plotW
141 return _xAxis.min + ratio * (_xAxis.max - _xAxis.min)
142 }
143
144 function _axisXToPixel(axisX) {
145 const plotX = _chart.plotArea.x
146 const plotW = _chart.plotArea.width
147 if (plotW <= 0 || _xAxis.max <= _xAxis.min) return plotX
148 const ratio = (axisX - _xAxis.min) / (_xAxis.max - _xAxis.min)
149 return plotX + Math.max(0, Math.min(plotW, ratio * plotW))
150 }
151
152 function _applyZoomInternal(minX, maxX) {
153 if (maxX <= minX) return
154 zoomMinX = minX
155 zoomMaxX = maxX
156 _xAxis.min = minX
157 _xAxis.max = maxX
158 if (markerVisible && (markerXValue < minX || markerXValue > maxX)) {
159 markerXValue = (minX + maxX) / 2
160 }
161 markerPixelX = _axisXToPixel(markerXValue)
162 zoomRangeSet(minX, maxX)
163 }
164
165 function _updateCursorFromInteraction(pixelX) {
166 if (_xAxis.max <= _xAxis.min) return
167 const plotX = _chart.plotArea.x
168 const plotW = _chart.plotArea.width
169 markerVisible = true
170 markerPixelX = Math.max(plotX, Math.min(plotX + plotW, pixelX))
171 markerXValue = _pixelToAxisX(markerPixelX)
172 cursorPositionSet(markerXValue)
173 cursorMoved(markerXValue)
174 }
175
176 function _popupX() {
177 const plotMidX = _chart.plotArea.x + _chart.plotArea.width / 2
178 if (markerPixelX < plotMidX) {
179 const rightX = _chart.plotArea.x + _chart.plotArea.width - _popup.width
180 return Math.max(0, Math.min(rightX, control.width - _popup.width))
181 } else {
182 return _chart.plotArea.x + _popup._margin
183 }
184 }
185
186 // -------------------------------------------------------------------------
187 // Visual tree
188 // -------------------------------------------------------------------------
189 QGCPalette { id: qgcPal }
190
191 GraphsView {
192 id: _chart
193 anchors.fill: parent
194 marginTop: 0
195 marginRight: 0
196 marginBottom: 0
197 marginLeft: 0
198
199 theme: GraphsTheme {
200 colorScheme: qgcPal.globalTheme === QGCPalette.Light ? GraphsTheme.ColorScheme.Light : GraphsTheme.ColorScheme.Dark
201 backgroundColor: qgcPal.windowShadeDark
202 backgroundVisible: true
203 plotAreaBackgroundColor: qgcPal.windowShadeDark
204 grid.mainColor: Qt.rgba(qgcPal.text.r, qgcPal.text.g, qgcPal.text.b, 0.25)
205 grid.subColor: Qt.rgba(qgcPal.text.r, qgcPal.text.g, qgcPal.text.b, 0.12)
206 grid.mainWidth: 1
207 labelBackgroundVisible: false
208 labelTextColor: qgcPal.text
209 axisXLabelFont.family: ScreenTools.fixedFontFamily
210 axisXLabelFont.pointSize: ScreenTools.smallFontPointSize
211 axisYLabelFont.family: ScreenTools.fixedFontFamily
212 axisYLabelFont.pointSize: ScreenTools.smallFontPointSize
213 }
214
215 axisX: ValueAxis {
216 id: _xAxis
217 titleText: xAxisShowLocalTime ? qsTr("Time (local)") : qsTr("Elapsed")
218 labelFormat: "%.3f"
219 min: 0
220 max: 1
221
222 labelDelegate: Component {
223 Item {
224 property string text: "" // raw seconds value, assigned by axis
225
226 Text {
227 anchors.centerIn: parent
228 color: _chart.theme.labelTextColor
229 font: _chart.theme.axisXLabelFont
230 horizontalAlignment: Text.AlignHCenter
231 text: {
232 const secs = parseFloat(parent.text)
233 if (isNaN(secs)) { return parent.text }
234 if (xAxisShowLocalTime) {
235 const st = logParser.startTime
236 if (st && !isNaN(st.getTime()) && st.getTime() > 0) {
237 const use12h = Qt.locale().timeFormat(Locale.ShortFormat).indexOf("a") >= 0
238 || Qt.locale().timeFormat(Locale.ShortFormat).indexOf("A") >= 0
239 return Qt.formatTime(new Date(st.getTime() + secs * 1000),
240 use12h ? "h:mm:ss AP" : "HH:mm:ss")
241 }
242 }
243 const wholeSecs = Math.floor(secs)
244 const hh = Math.floor(wholeSecs / 3600)
245 const mm = Math.floor((wholeSecs % 3600) / 60)
246 const ss = wholeSecs % 60
247 if (hh > 0) {
248 return hh + ":" + String(mm).padStart(2, "0") + ":" + String(ss).padStart(2, "0")
249 } else if (mm > 0) {
250 return mm + ":" + String(ss).padStart(2, "0")
251 }
252 return ss + "s"
253 }
254 }
255 }
256 }
257 }
258
259 axisY: ValueAxis {
260 id: _yAxis
261 titleText: yAxisTitle
262 min: 0
263 max: 1
264 }
265 }
266
267 // Recalculate marker pixel position when the chart is resized or laid out.
268 Connections {
269 target: _chart
270 function onPlotAreaChanged() {
271 if (control.markerVisible) {
272 control.markerPixelX = control._axisXToPixel(control.markerXValue)
273 }
274 }
275 }
276
277 // Zoom selection rectangle
278 Rectangle {
279 id: _zoomRect
280 visible: false
281 color: Qt.rgba(1, 1, 1, 0.2)
282 border.color: qgcPal.buttonHighlight
283 border.width: 1
284 z: 10
285 }
286
287 MouseArea {
288 anchors.fill: parent
289 acceptedButtons: Qt.LeftButton
290 enabled: _xAxis.max > _xAxis.min
291 z: 11
292
293 property real _dragStartX: 0
294 property bool _isDraggingCursor: false
295 property bool _isDraggingZoom: false
296 property bool _wasDoubleClick: false
297
298 onPressed: (mouse) => {
299 _isDraggingCursor = false
300 _isDraggingZoom = false
301 _wasDoubleClick = false
302 if (mouse.modifiers & Qt.ShiftModifier) {
303 // Shift pressed — start cursor drag
304 _isDraggingCursor = true
305 control._updateCursorFromInteraction(mouse.x)
306 } else {
307 // Plain press — start zoom selection
308 _dragStartX = mouse.x
309 _zoomRect.x = mouse.x
310 _zoomRect.y = _chart.plotArea.y
311 _zoomRect.width = 0
312 _zoomRect.height = _chart.plotArea.height
313 _zoomRect.visible = false // only show once drag is meaningful
314 }
315 }
316
317 onPositionChanged: (mouse) => {
318 if (!pressed) return
319 if (_isDraggingCursor) {
320 control._updateCursorFromInteraction(mouse.x)
321 } else {
322 // Plain drag — update zoom selection rect
323 const left = Math.min(_dragStartX, mouse.x)
324 const right = Math.max(_dragStartX, mouse.x)
325 _zoomRect.x = left
326 _zoomRect.width = Math.max(0, right - left)
327 if (_zoomRect.width >= ScreenTools.defaultFontPixelWidth * 0.5) {
328 _isDraggingZoom = true
329 _zoomRect.visible = true
330 }
331 }
332 }
333
334 onReleased: (mouse) => {
335 if (_isDraggingCursor) {
336 _isDraggingCursor = false
337 return
338 }
339 _zoomRect.visible = false
340 if (_isDraggingZoom) {
341 _isDraggingZoom = false
342 const leftX = control._pixelToAxisX(_zoomRect.x)
343 const rightX = control._pixelToAxisX(_zoomRect.x + _zoomRect.width)
344 control.applyZoomRange(Math.min(leftX, rightX), Math.max(leftX, rightX))
345 } else if (!_wasDoubleClick) {
346 // Tap — place cursor (suppressed on the second click of a double-click)
347 control._updateCursorFromInteraction(mouse.x)
348 }
349 }
350
351 onDoubleClicked: (mouse) => {
352 // onDoubleClicked fires before onReleased for the second click,
353 // so this flag prevents onReleased from repositioning the cursor.
354 _wasDoubleClick = true
355 control.resetZoom()
356 const centerT = (control.fullMinX + control.fullMaxX) / 2
357 control.setCursor(centerT)
358 control.cursorMoved(centerT)
359 }
360 }
361
362 // Position marker line
363 Rectangle {
364 visible: markerVisible
365 x: markerPixelX
366 y: _chart.plotArea.y
367 width: 1
368 height: _chart.plotArea.height
369 color: qgcPal.text
370 z: 12
371 }
372
373 // Cursor popup — chart-specific rows are injected via default property alias
374 LogViewerCursorPopup {
375 id: _popup
376 visible: markerVisible
377 x: _popupX()
378 y: _chart.plotArea.y + popupYOffset
379 z: 13
380 cursorXValue: markerXValue
381 logParser: control.logParser
382 xAxisShowLocalTime: control.xAxisShowLocalTime
383 zoomMinX: control.zoomMinX
384 zoomMaxX: control.zoomMaxX
385 plotAreaWidth: _chart.plotArea.width
386 }
387}