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