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 fullMinX = minX
87 fullMaxX = maxX
88 zoomMinX = minX
89 zoomMaxX = maxX
90 _xAxis.min = minX
91 _xAxis.max = maxX
92 zoomRangeSet(minX, maxX)
93 }
94
95 /// User-driven zoom — also emits zoomApplied for cross-chart sync.
96 function applyZoomRange(minX, maxX) {
97 _applyZoomInternal(minX, maxX)
98 zoomApplied(minX, maxX)
99 }
100
101 /// External zoom sync — does not re-emit zoomApplied.
102 function setSharedZoom(minX, maxX) {
103 _applyZoomInternal(minX, maxX)
104 }
105
106 /// Place the cursor at axis value t and emit cursorPositionSet.
107 /// Does NOT emit cursorMoved — use for both external sync and initial placement.
108 function setCursor(t) {
109 if (_xAxis.max <= _xAxis.min) return
110 markerXValue = t
111 markerPixelX = _axisXToPixel(t)
112 markerVisible = true
113 cursorPositionSet(t)
114 }
115
116 /// External cursor sync — delegates to setCursor (no cursorMoved emitted).
117 function setSharedCursor(t) {
118 setCursor(t)
119 }
120
121 function resetZoom() {
122 applyZoomRange(fullMinX, fullMaxX)
123 }
124
125 function clearCursor() {
126 markerVisible = false
127 }
128
129 // -------------------------------------------------------------------------
130 // Internal helpers
131 // -------------------------------------------------------------------------
132 function _pixelToAxisX(pixelX) {
133 const plotX = _chart.plotArea.x
134 const plotW = _chart.plotArea.width
135 if (plotW <= 0 || _xAxis.max <= _xAxis.min) return _xAxis.min
136 const ratio = (Math.max(plotX, Math.min(plotX + plotW, pixelX)) - plotX) / plotW
137 return _xAxis.min + ratio * (_xAxis.max - _xAxis.min)
138 }
139
140 function _axisXToPixel(axisX) {
141 const plotX = _chart.plotArea.x
142 const plotW = _chart.plotArea.width
143 if (plotW <= 0 || _xAxis.max <= _xAxis.min) return plotX
144 const ratio = (axisX - _xAxis.min) / (_xAxis.max - _xAxis.min)
145 return plotX + Math.max(0, Math.min(plotW, ratio * plotW))
146 }
147
148 function _applyZoomInternal(minX, maxX) {
149 if (maxX <= minX) return
150 zoomMinX = minX
151 zoomMaxX = maxX
152 _xAxis.min = minX
153 _xAxis.max = maxX
154 if (markerVisible && (markerXValue < minX || markerXValue > maxX)) {
155 markerXValue = (minX + maxX) / 2
156 }
157 markerPixelX = _axisXToPixel(markerXValue)
158 zoomRangeSet(minX, maxX)
159 }
160
161 function _updateCursorFromInteraction(pixelX) {
162 if (_xAxis.max <= _xAxis.min) return
163 const plotX = _chart.plotArea.x
164 const plotW = _chart.plotArea.width
165 markerVisible = true
166 markerPixelX = Math.max(plotX, Math.min(plotX + plotW, pixelX))
167 markerXValue = _pixelToAxisX(markerPixelX)
168 cursorPositionSet(markerXValue)
169 cursorMoved(markerXValue)
170 }
171
172 function _popupX() {
173 const plotMidX = _chart.plotArea.x + _chart.plotArea.width / 2
174 if (markerPixelX < plotMidX) {
175 const rightX = _chart.plotArea.x + _chart.plotArea.width - _popup.width
176 return Math.max(0, Math.min(rightX, control.width - _popup.width))
177 } else {
178 return _chart.plotArea.x + _popup._margin
179 }
180 }
181
182 // -------------------------------------------------------------------------
183 // Visual tree
184 // -------------------------------------------------------------------------
185 QGCPalette { id: qgcPal }
186
187 GraphsView {
188 id: _chart
189 anchors.fill: parent
190 marginTop: 0
191 marginRight: 0
192 marginBottom: 0
193 marginLeft: 0
194
195 theme: GraphsTheme {
196 colorScheme: qgcPal.globalTheme === QGCPalette.Light ? GraphsTheme.ColorScheme.Light : GraphsTheme.ColorScheme.Dark
197 backgroundColor: qgcPal.windowShadeDark
198 backgroundVisible: true
199 plotAreaBackgroundColor: qgcPal.windowShadeDark
200 grid.mainColor: Qt.rgba(qgcPal.text.r, qgcPal.text.g, qgcPal.text.b, 0.25)
201 grid.subColor: Qt.rgba(qgcPal.text.r, qgcPal.text.g, qgcPal.text.b, 0.12)
202 grid.mainWidth: 1
203 labelBackgroundVisible: false
204 labelTextColor: qgcPal.text
205 axisXLabelFont.family: ScreenTools.fixedFontFamily
206 axisXLabelFont.pointSize: ScreenTools.smallFontPointSize
207 axisYLabelFont.family: ScreenTools.fixedFontFamily
208 axisYLabelFont.pointSize: ScreenTools.smallFontPointSize
209 }
210
211 axisX: ValueAxis {
212 id: _xAxis
213 titleText: xAxisShowLocalTime ? qsTr("Time (local)") : qsTr("Elapsed")
214 labelFormat: "%.3f"
215 min: 0
216 max: 1
217
218 labelDelegate: Component {
219 Item {
220 property string text: "" // raw seconds value, assigned by axis
221
222 Text {
223 anchors.centerIn: parent
224 color: _chart.theme.labelTextColor
225 font: _chart.theme.axisXLabelFont
226 horizontalAlignment: Text.AlignHCenter
227 text: {
228 const secs = parseFloat(parent.text)
229 if (isNaN(secs)) { return parent.text }
230 if (xAxisShowLocalTime) {
231 const st = logParser.startTime
232 if (st && !isNaN(st.getTime()) && st.getTime() > 0) {
233 const use12h = Qt.locale().timeFormat(Locale.ShortFormat).indexOf("a") >= 0
234 || Qt.locale().timeFormat(Locale.ShortFormat).indexOf("A") >= 0
235 return Qt.formatTime(new Date(st.getTime() + secs * 1000),
236 use12h ? "h:mm:ss AP" : "HH:mm:ss")
237 }
238 }
239 const wholeSecs = Math.floor(secs)
240 const hh = Math.floor(wholeSecs / 3600)
241 const mm = Math.floor((wholeSecs % 3600) / 60)
242 const ss = wholeSecs % 60
243 if (hh > 0) {
244 return hh + ":" + String(mm).padStart(2, "0") + ":" + String(ss).padStart(2, "0")
245 } else if (mm > 0) {
246 return mm + ":" + String(ss).padStart(2, "0")
247 }
248 return ss + "s"
249 }
250 }
251 }
252 }
253 }
254
255 axisY: ValueAxis {
256 id: _yAxis
257 titleText: yAxisTitle
258 min: 0
259 max: 1
260 }
261 }
262
263 // Recalculate marker pixel position when the chart is resized or laid out.
264 Connections {
265 target: _chart
266 function onPlotAreaChanged() {
267 if (control.markerVisible) {
268 control.markerPixelX = control._axisXToPixel(control.markerXValue)
269 }
270 }
271 }
272
273 // Zoom selection rectangle
274 Rectangle {
275 id: _zoomRect
276 visible: false
277 color: Qt.rgba(1, 1, 1, 0.2)
278 border.color: qgcPal.buttonHighlight
279 border.width: 1
280 z: 10
281 }
282
283 MouseArea {
284 anchors.fill: parent
285 acceptedButtons: Qt.LeftButton
286 enabled: _xAxis.max > _xAxis.min
287 z: 11
288
289 property real _dragStartX: 0
290 property bool _isDraggingCursor: false
291 property bool _isDraggingZoom: false
292 property bool _wasDoubleClick: false
293
294 onPressed: (mouse) => {
295 _isDraggingCursor = false
296 _isDraggingZoom = false
297 _wasDoubleClick = false
298 if (mouse.modifiers & Qt.ShiftModifier) {
299 // Shift pressed — start cursor drag
300 _isDraggingCursor = true
301 control._updateCursorFromInteraction(mouse.x)
302 } else {
303 // Plain press — start zoom selection
304 _dragStartX = mouse.x
305 _zoomRect.x = mouse.x
306 _zoomRect.y = _chart.plotArea.y
307 _zoomRect.width = 0
308 _zoomRect.height = _chart.plotArea.height
309 _zoomRect.visible = false // only show once drag is meaningful
310 }
311 }
312
313 onPositionChanged: (mouse) => {
314 if (!pressed) return
315 if (_isDraggingCursor) {
316 control._updateCursorFromInteraction(mouse.x)
317 } else {
318 // Plain drag — update zoom selection rect
319 const left = Math.min(_dragStartX, mouse.x)
320 const right = Math.max(_dragStartX, mouse.x)
321 _zoomRect.x = left
322 _zoomRect.width = Math.max(0, right - left)
323 if (_zoomRect.width >= ScreenTools.defaultFontPixelWidth * 0.5) {
324 _isDraggingZoom = true
325 _zoomRect.visible = true
326 }
327 }
328 }
329
330 onReleased: (mouse) => {
331 if (_isDraggingCursor) {
332 _isDraggingCursor = false
333 return
334 }
335 _zoomRect.visible = false
336 if (_isDraggingZoom) {
337 _isDraggingZoom = false
338 const leftX = control._pixelToAxisX(_zoomRect.x)
339 const rightX = control._pixelToAxisX(_zoomRect.x + _zoomRect.width)
340 control.applyZoomRange(Math.min(leftX, rightX), Math.max(leftX, rightX))
341 } else if (!_wasDoubleClick) {
342 // Tap — place cursor (suppressed on the second click of a double-click)
343 control._updateCursorFromInteraction(mouse.x)
344 }
345 }
346
347 onDoubleClicked: (mouse) => {
348 // onDoubleClicked fires before onReleased for the second click,
349 // so this flag prevents onReleased from repositioning the cursor.
350 _wasDoubleClick = true
351 control.resetZoom()
352 const centerT = (control.fullMinX + control.fullMaxX) / 2
353 control.setCursor(centerT)
354 control.cursorMoved(centerT)
355 }
356 }
357
358 // Position marker line
359 Rectangle {
360 visible: markerVisible
361 x: markerPixelX
362 y: _chart.plotArea.y
363 width: 1
364 height: _chart.plotArea.height
365 color: qgcPal.text
366 z: 12
367 }
368
369 // Cursor popup — chart-specific rows are injected via default property alias
370 LogViewerCursorPopup {
371 id: _popup
372 visible: markerVisible
373 x: _popupX()
374 y: _chart.plotArea.y + popupYOffset
375 z: 13
376 cursorXValue: markerXValue
377 logParser: control.logParser
378 xAxisShowLocalTime: control.xAxisShowLocalTime
379 zoomMinX: control.zoomMinX
380 zoomMaxX: control.zoomMaxX
381 plotAreaWidth: _chart.plotArea.width
382 }
383}