QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
GeoTagPage.qml
Go to the documentation of this file.
1import QtQuick
2import QtQuick.Controls
3import QtQuick.Layouts
4
5import QGroundControl
6import QGroundControl.Controls
7
8AnalyzePage {
9 id: geoTagPage
10 pageComponent: pageComponent
11 pageDescription: qsTr("Tag images from a survey mission with GPS coordinates from your flight log.")
12
13 readonly property real _margin: ScreenTools.defaultFontPixelWidth
14
15 QGCPalette { id: qgcPal; colorGroupEnabled: true }
16
17 Component {
18 id: pageComponent
19
20 ColumnLayout {
21 spacing: _margin * 2
22 width: availableWidth
23
24 // Status Card
25 Rectangle {
26 Layout.fillWidth: true
27 Layout.preferredHeight: statusColumn.height + _margin * 2
28 color: qgcPal.windowShade
29 radius: ScreenTools.defaultFontPixelWidth / 2
30 visible: geoController.inProgress || geoController.errorMessage || geoController.taggedCount > 0
31
32 ColumnLayout {
33 id: statusColumn
34 anchors.left: parent.left
35 anchors.right: parent.right
36 anchors.top: parent.top
37 anchors.margins: _margin
38 spacing: _margin
39
40 RowLayout {
41 Layout.fillWidth: true
42 spacing: _margin
43 visible: geoController.inProgress
44
45 BusyIndicator {
46 Layout.preferredWidth: ScreenTools.defaultFontPixelHeight * 2
47 Layout.preferredHeight: ScreenTools.defaultFontPixelHeight * 2
48 running: geoController.inProgress
49 }
50
51 ColumnLayout {
52 Layout.fillWidth: true
53 spacing: _margin / 2
54
55 QGCLabel {
56 text: qsTr("Geotagging in progress...")
57 font.bold: true
58 }
59
60 ProgressBar {
61 Layout.fillWidth: true
62 from: 0
63 to: 100
64 value: geoController.progress
65 }
66 }
67 }
68
69 QGCLabel {
70 Layout.fillWidth: true
71 text: geoController.errorMessage
72 color: qgcPal.colorRed
73 font.bold: true
74 wrapMode: Text.WordWrap
75 horizontalAlignment: Text.AlignHCenter
76 visible: geoController.errorMessage && !geoController.inProgress
77 }
78
79 QGCLabel {
80 Layout.fillWidth: true
81 text: {
82 if (geoController.taggedCount > 0 && !geoController.inProgress) {
83 let msg = qsTr("Successfully tagged %1 images").arg(geoController.taggedCount)
84 let details = []
85 if (geoController.skippedCount > 0) {
86 details.push(qsTr("%1 skipped").arg(geoController.skippedCount))
87 }
88 if (geoController.failedCount > 0) {
89 details.push(qsTr("%1 failed").arg(geoController.failedCount))
90 }
91 if (details.length > 0) {
92 msg += " (" + details.join(", ") + ")"
93 }
94 return msg
95 }
96 return ""
97 }
98 color: geoController.failedCount > 0 ? qgcPal.colorOrange : qgcPal.colorGreen
99 font.bold: true
100 horizontalAlignment: Text.AlignHCenter
101 visible: geoController.taggedCount > 0 && !geoController.inProgress
102 }
103 }
104 }
105
106 // Step 1: Log File
107 Rectangle {
108 Layout.fillWidth: true
109 Layout.preferredHeight: step1Column.height + _margin * 2
110 color: qgcPal.windowShade
111 radius: ScreenTools.defaultFontPixelWidth / 2
112
113 ColumnLayout {
114 id: step1Column
115 anchors.left: parent.left
116 anchors.right: parent.right
117 anchors.top: parent.top
118 anchors.margins: _margin
119 spacing: _margin
120
121 RowLayout {
122 Layout.fillWidth: true
123 spacing: _margin
124
125 Rectangle {
126 Layout.preferredWidth: ScreenTools.defaultFontPixelHeight * 1.5
127 Layout.preferredHeight: ScreenTools.defaultFontPixelHeight * 1.5
128 radius: height / 2
129 color: geoController.logFile ? qgcPal.colorGreen : qgcPal.button
130
131 QGCLabel {
132 anchors.centerIn: parent
133 text: geoController.logFile ? "\u2713" : "1"
134 color: geoController.logFile ? "white" : qgcPal.buttonText
135 font.bold: true
136 }
137 }
138
139 QGCLabel {
140 text: qsTr("Select Flight Log")
141 font.bold: true
142 }
143 }
144
145 RowLayout {
146 Layout.fillWidth: true
147 spacing: _margin
148
149 QGCButton {
150 text: qsTr("Browse...")
151 enabled: !geoController.inProgress
152 onClicked: openLogFile.openForLoad()
153
154 QGCFileDialog {
155 id: openLogFile
156 title: qsTr("Select Flight Log")
157 nameFilters: [qsTr("Flight logs (*.ulg *.bin)"), qsTr("ULog (*.ulg)"), qsTr("DataFlash (*.bin)"), qsTr("All Files (*)")]
158 defaultSuffix: "ulg"
159 onAcceptedForLoad: (file) => {
160 geoController.logFile = file
161 close()
162 }
163 }
164 }
165
166 QGCLabel {
167 Layout.fillWidth: true
168 text: geoController.logFile ? geoController.logFile : qsTr("No file selected")
169 elide: Text.ElideMiddle
170 opacity: geoController.logFile ? 1.0 : 0.5
171 }
172 }
173 }
174 }
175
176 // Step 2: Image Directory
177 Rectangle {
178 Layout.fillWidth: true
179 Layout.preferredHeight: step2Column.height + _margin * 2
180 color: qgcPal.windowShade
181 radius: ScreenTools.defaultFontPixelWidth / 2
182
183 ColumnLayout {
184 id: step2Column
185 anchors.left: parent.left
186 anchors.right: parent.right
187 anchors.top: parent.top
188 anchors.margins: _margin
189 spacing: _margin
190
191 RowLayout {
192 Layout.fillWidth: true
193 spacing: _margin
194
195 Rectangle {
196 Layout.preferredWidth: ScreenTools.defaultFontPixelHeight * 1.5
197 Layout.preferredHeight: ScreenTools.defaultFontPixelHeight * 1.5
198 radius: height / 2
199 color: geoController.imageDirectory ? qgcPal.colorGreen : qgcPal.button
200
201 QGCLabel {
202 anchors.centerIn: parent
203 text: geoController.imageDirectory ? "\u2713" : "2"
204 color: geoController.imageDirectory ? "white" : qgcPal.buttonText
205 font.bold: true
206 }
207 }
208
209 QGCLabel {
210 text: qsTr("Select Image Folder")
211 font.bold: true
212 }
213 }
214
215 RowLayout {
216 Layout.fillWidth: true
217 spacing: _margin
218
219 QGCButton {
220 text: qsTr("Browse...")
221 enabled: !geoController.inProgress
222 onClicked: selectImageDir.openForLoad()
223
224 QGCFileDialog {
225 id: selectImageDir
226 title: qsTr("Select Image Folder")
227 selectFolder: true
228 onAcceptedForLoad: (file) => {
229 geoController.imageDirectory = file
230 close()
231 }
232 }
233 }
234
235 QGCLabel {
236 Layout.fillWidth: true
237 text: geoController.imageDirectory ? geoController.imageDirectory : qsTr("No folder selected")
238 elide: Text.ElideMiddle
239 opacity: geoController.imageDirectory ? 1.0 : 0.5
240 }
241 }
242 }
243 }
244
245 // Step 3: Save Directory (Optional)
246 Rectangle {
247 Layout.fillWidth: true
248 Layout.preferredHeight: step3Column.height + _margin * 2
249 color: qgcPal.windowShade
250 radius: ScreenTools.defaultFontPixelWidth / 2
251
252 ColumnLayout {
253 id: step3Column
254 anchors.left: parent.left
255 anchors.right: parent.right
256 anchors.top: parent.top
257 anchors.margins: _margin
258 spacing: _margin
259
260 RowLayout {
261 Layout.fillWidth: true
262 spacing: _margin
263
264 Rectangle {
265 Layout.preferredWidth: ScreenTools.defaultFontPixelHeight * 1.5
266 Layout.preferredHeight: ScreenTools.defaultFontPixelHeight * 1.5
267 radius: height / 2
268 color: qgcPal.button
269
270 QGCLabel {
271 anchors.centerIn: parent
272 text: "3"
273 color: qgcPal.buttonText
274 font.bold: true
275 }
276 }
277
278 QGCLabel {
279 text: qsTr("Output Folder (Optional)")
280 font.bold: true
281 }
282 }
283
284 RowLayout {
285 Layout.fillWidth: true
286 spacing: _margin
287
288 QGCButton {
289 text: qsTr("Browse...")
290 enabled: !geoController.inProgress
291 onClicked: selectDestDir.openForLoad()
292
293 QGCFileDialog {
294 id: selectDestDir
295 title: qsTr("Select Output Folder")
296 selectFolder: true
297 onAcceptedForLoad: (file) => {
298 geoController.saveDirectory = file
299 close()
300 }
301 }
302 }
303
304 QGCLabel {
305 Layout.fillWidth: true
306 text: {
307 if (geoController.saveDirectory) {
308 return geoController.saveDirectory
309 } else if (geoController.imageDirectory) {
310 return geoController.imageDirectory + "/TAGGED"
311 }
312 return qsTr("Default: /TAGGED subfolder")
313 }
314 elide: Text.ElideMiddle
315 opacity: geoController.saveDirectory ? 1.0 : 0.5
316 }
317 }
318 }
319 }
320
321 // Advanced Options
322 Rectangle {
323 Layout.fillWidth: true
324 Layout.preferredHeight: advancedColumn.height + _margin * 2
325 color: qgcPal.windowShade
326 radius: ScreenTools.defaultFontPixelWidth / 2
327
328 ColumnLayout {
329 id: advancedColumn
330 anchors.left: parent.left
331 anchors.right: parent.right
332 anchors.top: parent.top
333 anchors.margins: _margin
334 spacing: _margin
335
336 QGCLabel {
337 text: qsTr("Advanced Options")
338 font.bold: true
339 }
340
341 RowLayout {
342 Layout.fillWidth: true
343 spacing: _margin
344
345 QGCLabel {
346 text: qsTr("Time Offset (seconds):")
347 }
348
349 QGCTextField {
350 id: timeOffsetField
351 Layout.preferredWidth: ScreenTools.defaultFontPixelWidth * 10
352 text: geoController.timeOffsetSecs.toFixed(1)
353 enabled: !geoController.inProgress
354 inputMethodHints: Qt.ImhFormattedNumbersOnly
355 validator: DoubleValidator { bottom: -3600; top: 3600; decimals: 1 }
356 onEditingFinished: geoController.timeOffsetSecs = parseFloat(text) || 0
357 }
358
359 QGCLabel {
360 Layout.fillWidth: true
361 text: qsTr("Adjust if camera clock differs from flight log")
362 opacity: 0.7
363 font.pointSize: ScreenTools.smallFontPointSize
364 }
365 }
366
367 RowLayout {
368 Layout.fillWidth: true
369 spacing: _margin
370
371 QGCCheckBox {
372 id: previewCheckbox
373 text: qsTr("Preview mode (don't write files)")
374 checked: geoController.previewMode
375 enabled: !geoController.inProgress
376 onClicked: geoController.previewMode = checked
377 }
378
379 QGCLabel {
380 Layout.fillWidth: true
381 text: qsTr("Verify time offset before committing")
382 opacity: 0.7
383 font.pointSize: ScreenTools.smallFontPointSize
384 }
385 }
386 }
387 }
388
389 // Action Button
390 QGCButton {
391 Layout.alignment: Qt.AlignHCenter
392 Layout.preferredWidth: ScreenTools.defaultFontPixelWidth * 20
393 text: {
394 if (geoController.inProgress) {
395 return qsTr("Cancel")
396 } else if (geoController.previewMode) {
397 return qsTr("Preview")
398 } else {
399 return qsTr("Start Tagging")
400 }
401 }
402 enabled: (geoController.logFile && geoController.imageDirectory) || geoController.inProgress
403 onClicked: {
404 if (geoController.inProgress) {
405 geoController.cancelTagging()
406 } else {
407 geoController.startTagging()
408 }
409 }
410 }
411
412 // Image List
413 Rectangle {
414 Layout.fillWidth: true
415 Layout.fillHeight: true
416 Layout.minimumHeight: ScreenTools.defaultFontPixelHeight * 10
417 color: qgcPal.windowShade
418 radius: ScreenTools.defaultFontPixelWidth / 2
419 visible: geoController.imageModel.count > 0
420
421 ColumnLayout {
422 anchors.fill: parent
423 anchors.margins: _margin
424 spacing: _margin / 2
425
426 RowLayout {
427 Layout.fillWidth: true
428 spacing: _margin
429
430 QGCLabel {
431 text: qsTr("Images (%1)").arg(geoController.imageModel.count)
432 font.bold: true
433 }
434
435 Item { Layout.fillWidth: true }
436
437 // Legend
438 Row {
439 spacing: _margin
440
441 Row {
442 spacing: _margin / 4
443 Rectangle { width: 10; height: 10; radius: 2; color: qgcPal.text; opacity: 0.5 }
444 QGCLabel { text: qsTr("Pending"); font.pointSize: ScreenTools.smallFontPointSize }
445 }
446 Row {
447 spacing: _margin / 4
448 Rectangle { width: 10; height: 10; radius: 2; color: qgcPal.colorBlue }
449 QGCLabel { text: qsTr("Processing"); font.pointSize: ScreenTools.smallFontPointSize }
450 }
451 Row {
452 spacing: _margin / 4
453 Rectangle { width: 10; height: 10; radius: 2; color: qgcPal.colorGreen }
454 QGCLabel { text: qsTr("Tagged"); font.pointSize: ScreenTools.smallFontPointSize }
455 }
456 Row {
457 spacing: _margin / 4
458 Rectangle { width: 10; height: 10; radius: 2; color: qgcPal.colorOrange }
459 QGCLabel { text: qsTr("Skipped"); font.pointSize: ScreenTools.smallFontPointSize }
460 }
461 Row {
462 spacing: _margin / 4
463 Rectangle { width: 10; height: 10; radius: 2; color: qgcPal.colorRed }
464 QGCLabel { text: qsTr("Failed"); font.pointSize: ScreenTools.smallFontPointSize }
465 }
466 }
467 }
468
469 Rectangle {
470 Layout.fillWidth: true
471 height: 1
472 color: qgcPal.text
473 opacity: 0.2
474 }
475
476 QGCListView {
477 id: imageListView
478 Layout.fillWidth: true
479 Layout.fillHeight: true
480 model: geoController.imageModel
481
482 delegate: Rectangle {
483 width: imageListView.width
484 height: ScreenTools.defaultFontPixelHeight * 2
485 color: index % 2 === 0 ? "transparent" : qgcPal.window
486 radius: ScreenTools.defaultFontPixelWidth / 4
487
488 RowLayout {
489 anchors.fill: parent
490 anchors.leftMargin: _margin / 2
491 anchors.rightMargin: _margin / 2
492 spacing: _margin
493
494 // Status indicator
495 Rectangle {
496 Layout.preferredWidth: ScreenTools.defaultFontPixelHeight * 0.8
497 Layout.preferredHeight: ScreenTools.defaultFontPixelHeight * 0.8
498 radius: ScreenTools.defaultFontPixelWidth / 4
499 color: {
500 switch (model.status) {
501 case 0: return qgcPal.text // Pending
502 case 1: return qgcPal.colorBlue // Processing
503 case 2: return qgcPal.colorGreen // Tagged
504 case 3: return qgcPal.colorOrange // Skipped
505 case 4: return qgcPal.colorRed // Failed
506 default: return qgcPal.text
507 }
508 }
509 opacity: model.status === 0 ? 0.5 : 1.0
510
511 // Processing animation
512 SequentialAnimation on opacity {
513 running: model.status === 1
514 loops: Animation.Infinite
515 NumberAnimation { to: 0.3; duration: 500 }
516 NumberAnimation { to: 1.0; duration: 500 }
517 }
518 }
519
520 // File name
521 QGCLabel {
522 Layout.fillWidth: true
523 text: model.fileName
524 elide: Text.ElideMiddle
525 }
526
527 // Coordinate (if tagged)
528 QGCLabel {
529 Layout.preferredWidth: ScreenTools.defaultFontPixelWidth * 20
530 text: {
531 if (model.coordinate && model.coordinate.isValid) {
532 return model.coordinate.latitude.toFixed(6) + ", " + model.coordinate.longitude.toFixed(6)
533 }
534 return ""
535 }
536 font.pointSize: ScreenTools.smallFontPointSize
537 opacity: 0.7
538 visible: model.status === 2
539 }
540
541 // Status text or error
542 QGCLabel {
543 Layout.preferredWidth: ScreenTools.defaultFontPixelWidth * 12
544 text: model.errorMessage ? model.errorMessage : model.statusString
545 font.pointSize: ScreenTools.smallFontPointSize
546 color: model.status === 4 ? qgcPal.colorRed : (model.status === 3 ? qgcPal.colorOrange : qgcPal.text)
547 elide: Text.ElideRight
548 horizontalAlignment: Text.AlignRight
549 }
550 }
551 }
552 }
553 }
554 }
555 }
556 }
557}