1// SPDX-License-Identifier: LGPL-2.1-or-later 2// 3// SPDX-FileCopyrightText: 2015 Gábor Péterffy <peterffy95@gmail.com> 4// SPDX-FileCopyrightText: 2015 Dennis Nienhüser <nienhueser@kde.org> 5// SPDX-FileCopyrightText: 2015 Mikhail Ivchenko <ematirov@gmail.com> 6// 7 8import QtQuick 2.7 9import QtQuick.Controls 2.2 10import QtQuick.Window 2.2 11import QtQuick.Controls.Material 2.0 12 13 14import org.kde.marble 0.20 15import org.kde.kirigami 2.0 as Kirigami 16 17Kirigami.AbstractApplicationWindow { 18 id: app 19 title: qsTr("Marble Maps") 20 visible: true 21 22 width: 600 23 height: 400 24 25 Material.theme: Material.Light 26 Material.accent: Material.Blue 27 28 color: "#f9f9f9" // Keep the background white while no dialog is loaded 29 30 property alias state: stateTracker.state 31 32 property var selectedPlacemark 33 property bool showOsmTags: false 34 property int currentWaypointIndex: 0 35 36 property real animatedMargin: app.state === "none" ? 0 : -dialogLoader.height 37 property bool dialogExpanded: animatedMargin === -dialogLoader.height 38 property real mapOffset: !dialogExpanded ? animatedMargin / 2 : 0 39 40 Behavior on animatedMargin { 41 NumberAnimation { 42 id: dialogAnimation 43 duration: 200 44 easing.type: Easing.OutQuart 45 } 46 } 47 48 onSelectedPlacemarkChanged: { 49 if (!selectedPlacemark) { 50 app.state = "none" 51 } 52 else { 53 bookmarkButton.bookmark = bookmarks.isBookmark(selectedPlacemark.longitude, selectedPlacemark.latitude) 54 } 55 } 56 57 SystemPalette{ 58 id: palette 59 colorGroup: SystemPalette.Active 60 } 61 62 Settings { 63 id: settings 64 } 65 66 67 property bool aboutToQuit: false 68 69 onClosing: { 70 if (app.aboutToQuit || Qt.platform.os !== "android") { 71 close.accepted = true // we will quit 72 return 73 } else if (sidePanel.drawerOpen) { 74 sidePanel.close() 75 } else if (pageStack.depth > 1) { 76 pageStack.pop() 77 } else if (navigationManager.visible) { 78 navigationManager.visible = false 79 } else if (app.state !== "none") { 80 app.state = "none" 81 } else if(search.searchResultsVisible.visible){ 82 search.searchResultsVisible = false 83 } 84 else { 85 if(search.searchResultsVisible){ 86 search.searchResultsVisible = false 87 } 88 app.aboutToQuit = true 89 quitHelper.visible = true 90 } 91 close.accepted = false 92 } 93 94 globalDrawer: Kirigami.GlobalDrawer { 95 id: sidePanel 96 title: qsTr("Settings") 97 98 handleVisible: false 99 property alias showAccessibility: accessibilityAction.checked 100 101 Settings { 102 id: sidePanelSettings 103 property bool showUpdateInfo: Number(value("MarbleMaps", "updateInfoVersion", "0")) < 1 104 Component.onDestruction: { 105 sidePanelSettings.setValue("MarbleMaps", "showAccessibility", accessibilityAction.checked ? "true" : "false") 106 } 107 } 108 109 actions: [ 110 Kirigami.Action { 111 id: publicTransportAction 112 text: qsTr("Public Transport") 113 checkable: true 114 checked: marbleMaps.showPublicTransport 115 iconName: "qrc:///material/directions-bus.svg" 116 visible: true 117 onTriggered: { 118 sidePanel.close() 119 marbleMaps.showPublicTransport = checked 120 publicTransportDialog.open() 121 } 122 }, 123 Kirigami.Action { 124 id: outdoorActivitiesAction 125 checkable: true 126 checked: marbleMaps.showOutdoorActivities 127 text: qsTr("Outdoor Activities") 128 visible: true 129 iconName: "qrc:///material/directions-run.svg" 130 onTriggered: { 131 sidePanel.close() 132 marbleMaps.showOutdoorActivities = checked 133 } 134 }, 135 Kirigami.Action { 136 id: accessibilityAction 137 checkable: true 138 checked: settings.value("MarbleMaps", "showAccessibility", "false") === "true" 139 text: qsTr("Accessibility") 140 visible: true 141 iconName: "qrc:///material/wheelchair.svg" 142 onTriggered: { 143 sidePanelSettings.value("MarbleMaps", "showAccessibility", "false") === "true" 144 } 145 }, 146 Kirigami.Action{ enabled: false}, 147 Kirigami.Action { 148 text: qsTr("About") 149 iconName: "qrc:///marble.svg" 150 visible: true 151 onTriggered: { 152 app.state = "about" 153 sidePanel.close() 154 source = "" 155 app.pageStack.push("qrc:///AboutDialog.qml") 156 } 157 }, 158 Kirigami.Action { 159 text: qsTr("Bookmarks") 160 iconName: "qrc:///material/star.svg" 161 onTriggered: { 162 app.state = "bookmarks" 163 sidePanel.close() 164 app.pageStack.push("qrc:///Bookmarks.qml") 165 } 166 }, 167 Kirigami.Action { 168 text: qsTr("Layer Options") 169 iconName: "qrc:///settings.png" 170 onTriggered: { 171 app.state = "options" 172 sidePanel.close() 173 app.pageStack.push("qrc:///Options.qml") 174 } 175 }, 176 Kirigami.Action { 177 text: qsTr("Routing") 178 iconName: "qrc:///material/directions.svg" 179 onTriggered: { 180 app.state = "route" 181 } 182 } 183 ] 184 185 Binding { 186 target: pageStack.currentItem 187 property: "marbleQuickItem" 188 value: marbleMaps 189 when: app.state === "bookmarks" 190 } 191 } 192 193 pageStack: StackView { 194 anchors.fill: parent 195 initialItem: page 196 } 197 198 Kirigami.Page { 199 id: page 200 padding: 0 201 topPadding: 0 202 leftPadding: 0 203 rightPadding: 0 204 bottomPadding: 0 205 title: qsTr("Marble Maps") 206 207 Item { 208 id: mapItem 209 210 width: parent.width 211 height: parent.height - dialogLoader.height - bottomMenu.height 212 213 PinchArea { 214 anchors.fill: parent 215 enabled: true 216 217 onPinchStarted: marbleMaps.handlePinchStarted(pinch.center) 218 onPinchFinished: marbleMaps.handlePinchFinished(pinch.center) 219 onPinchUpdated: marbleMaps.handlePinchUpdated(pinch.center, pinch.scale); 220 221 MarbleMaps { 222 id: marbleMaps 223 224 property string currentPositionProvider: "QtPositioning" 225 property bool wlanOnly: false 226 property bool smallZoom : radius < 2 * Math.max(app.width, app.height) 227 228 anchors.fill: parent 229 visible: true 230 231 // Theme settings. 232 projection: smallZoom ? MarbleItem.Spherical : MarbleItem.Mercator 233 mapThemeId: settings.value("MarbleMaps", "mapThemeId", "earth/vectorosm/vectorosm.dgml") 234 235 // Visibility of layers/plugins. 236 showFrameRate: false 237 showAtmosphere: smallZoom 238 showCompass: false 239 showClouds: false 240 showCrosshairs: false 241 showGrid: smallZoom 242 showOverviewMap: false 243 showOtherPlaces: false 244 showScaleBar: false 245 showBackground: smallZoom 246 showPublicTransport: settings.value("MarbleMaps", "showPublicTransport", "false") === "true" 247 positionProvider: suspended ? "" : currentPositionProvider 248 keepScreenOn: !suspended && navigationManager.guidanceModeEnabled 249 showPositionMarker: false 250 animationViewContext: dialogAnimation.running 251 252 placemarkDelegate: Image { 253 id: balloon 254 property int xPos: 0 255 property int yPos: 0 256 property real animationOffset: 0 257 property var placemark: null 258 x: xPos - 0.5 * width 259 y: yPos - height - 30 * Screen.pixelDensity * animationOffset 260 opacity: 1.0 - animationOffset 261 262 Connections { 263 target: app 264 onSelectedPlacemarkChanged: balloonAnimation.restart() 265 } 266 267 NumberAnimation { 268 id: balloonAnimation 269 target: balloon 270 property: "animationOffset" 271 from: 1 272 to: 0 273 duration: 1000 274 easing.type: Easing.OutBounce 275 } 276 277 278 width: Screen.pixelDensity*6 279 height: width 280 source: "qrc:///ic_place.png" 281 onPlacemarkChanged: { 282 app.selectedPlacemark = placemark 283 if (placemark) { 284 app.state = "place" 285 } else { 286 app.state = "none" 287 } 288 } 289 } 290 291 onPositionAvailableChanged: { 292 updateIndicator(); 293 } 294 onPositionVisibleChanged: { 295 updateIndicator(); 296 } 297 onVisibleLatLonAltBoxChanged: { 298 !panningDetectionTimer.restart(); 299 updateIndicator(); 300 } 301 onCurrentPositionChanged: { 302 updateIndicator(); 303 } 304 305 onZoomChanged: { 306 zoomDetectionTimer.restart() 307 } 308 309 Component.onCompleted: { 310 setPluginSetting("coordinate-grid", "gridColor", "#999999"); 311 setPluginSetting("coordinate-grid", "tropicsColor", "#888888"); 312 setPluginSetting("coordinate-grid", "equatorColor", "#777777"); 313 setPluginSetting("coordinate-grid", "primaryLabels", "false"); 314 setPluginSetting("coordinate-grid", "secondaryLabels", "false"); 315 marbleMaps.loadSettings() 316 } 317 Component.onDestruction: marbleMaps.writeSettings() 318 319 Connections { 320 target: Qt.application 321 onStateChanged: { 322 if (Qt.application.state === Qt.ApplicationInactive || Qt.application.state === Qt.ApplicationSuspended) { 323 marbleMaps.writeSettings() 324 } 325 } 326 } 327 328 function updateIndicator() { 329 if ( !positionVisible && positionAvailable ) { 330 zoomToPositionButton.updateIndicator(); 331 } 332 } 333 334 RoutingManager { 335 id: routingManager 336 anchors.fill: parent 337 marbleItem: marbleMaps 338 visible: hasRoute 339 340 function addToRoute() { 341 ensureRouteHasDeparture() 342 routingManager.addViaByPlacemarkAtIndex(routingManager.waypointCount(), selectedPlacemark) 343 routingManager.clearSearchResultPlacemarks() 344 selectedPlacemark = null 345 app.state = "route" 346 } 347 function ensureRouteHasDeparture() { 348 if (routingManager.routeRequestModel.count === 0) { 349 if (marbleMaps.positionAvailable) { 350 routingManager.addViaByPlacemark(marbleMaps.currentPosition) 351 } 352 } 353 } 354 355 } 356 357 Timer { 358 id: zoomDetectionTimer 359 interval: 1000 360 } 361 Timer { 362 id: panningDetectionTimer 363 interval: 1000 364 } 365 366 PositionMarker { 367 id: positionMarker 368 x: navigationManager.snappedPositionMarkerScreenPosition.x - positionMarker.width / 2 369 y: navigationManager.snappedPositionMarkerScreenPosition.y - positionMarker.height / 2 370 angle: marbleMaps.angle 371 visible: marbleMaps.positionAvailable && marbleMaps.positionVisible 372 radius: navigationManager.screenAccuracy / 2 373 showAccuracy: navigationManager.deviated 374 allowRadiusAnimation: !zoomDetectionTimer.running 375 allowPositionAnimation: !panningDetectionTimer.running 376 speed: marbleMaps.speed 377 378 MouseArea { 379 anchors.fill: parent 380 onPressed: app.state = "position" 381 } 382 } 383 384 MouseArea { 385 anchors.fill: parent 386 propagateComposedEvents: true 387 onPressed: { 388 marbleMaps.focus = true; 389 mouse.accepted = false; 390 } 391 } 392 } 393 394 NavigationManager { 395 id: navigationManager 396 width: parent.width 397 height: parent.height 398 visible: false 399 marbleItem: marbleMaps 400 hasRoute: routingManager.hasRoute 401 } 402 } 403 404 BoxedText { 405 id: distanceIndicator 406 text: qsTr("%1 km").arg(zoomToPositionButton.distance < 10 ? zoomToPositionButton.distance.toFixed(1) : zoomToPositionButton.distance.toFixed(0)) 407 anchors { 408 bottom: zoomToPositionButton.top 409 horizontalCenter: zoomToPositionButton.horizontalCenter 410 } 411 412 visible: marbleMaps.positionAvailable && !marbleMaps.positionVisible 413 } 414 415 PositionButton { 416 id: zoomToPositionButton 417 anchors { 418 right: parent.right 419 rightMargin: Screen.pixelDensity * 1 420 bottom: mapItem.bottom 421 bottomMargin: 10 422 } 423 424 enabled: marbleMaps.positionAvailable 425 426 iconSource: marbleMaps.positionAvailable ? "qrc:///gps_fixed.png" : "qrc:///gps_not_fixed.png" 427 428 onClicked: marbleMaps.centerOnCurrentPosition() 429 430 property real distance: 0 431 432 function updateIndicator() { 433 var point = marbleMaps.mapFromItem(zoomToPositionButton, diameter * 0.5, diameter * 0.5); 434 distance = 0.001 * marbleMaps.distanceFromPointToCurrentLocation(point); 435 angle = marbleMaps.angleFromPointToCurrentLocation(point); 436 } 437 438 showDirection: marbleMaps.positionAvailable && !marbleMaps.positionVisible 439 } 440 } 441 442 443 Row { 444 id: bottomMenu 445 anchors.left: parent.left 446 anchors.right: parent.right 447 anchors.bottom: dialogLoader.top 448 width: parent.width 449 height: bottomMenu.visible ? routeEditorButton.height + Screen.pixelDensity * 2 : 0 450 anchors.topMargin: app.animatedMargin 451 visible: app.state === "place" || app.state === "route" 452 453 onVisibleChanged: bottomMenuAnimation.start() 454 455 NumberAnimation { 456 id: bottomMenuAnimation 457 target: bottomMenu 458 property: "y" 459 from: app.height - bottomMenu.height 460 to: 0 461 duration: 500 462 easing.type: Easing.InExpo 463 } 464 465 Item { 466 id: bottomMenuBackground 467 anchors.fill: parent 468 Rectangle { 469 color: Material.accent 470 anchors.fill : parent 471 } 472 } 473 474 Row { 475 anchors.centerIn: parent 476 spacing: Kirigami.Units.gridUnit * 2 477 478 FlatButton { 479 id: routeEditorButton 480 property string currentProfileIcon: "qrc:///material/directions-car.svg" 481 height: Screen.pixelDensity * 6 482 width: height 483 enabled: app.state !== "route" || routingManager.hasRoute 484 imageSource: "qrc:///material/directions.svg" 485 486 onClicked: { 487 if (app.state === "route") { 488 app.state = "none" 489 navigationManager.visible = true 490 } else if (app.state === "place") { 491 app.state = "route" 492 routingManager.addToRoute() 493 } else { 494 app.state = "route" 495 navigationManager.visible = false 496 } 497 } 498 states: [ 499 State { 500 name: "" 501 PropertyChanges { target: routeEditorButton; imageSource: "qrc:///material/directions.svg"; } 502 }, 503 State { 504 name: "routingAction" 505 when: app.state === "route" 506 PropertyChanges { target: routeEditorButton; imageSource: "qrc:///material/navigation.svg"; } 507 }, 508 State { 509 name: "placeAction" 510 when: app.state === "place" 511 PropertyChanges { target: routeEditorButton; imageSource: "qrc:///material/directions.svg" } 512 } 513 ] 514 } 515 516 FlatButton { 517 id: bookmarkButton 518 anchors.verticalCenter: parent.verticalCenter 519 height: Screen.pixelDensity * 6 520 width: height 521 property bool bookmark: bookmarks.isBookmark(app.selectedPlacemark.longitude, app.selectedPlacemark.latitude) 522 enabled: app.state === "place" 523 visible: app.state === "place" 524 imageSource: bookmark ? "qrc:///material/star.svg" : "qrc:///material/star_border.svg" 525 onClicked: { 526 if (bookmarkButton.bookmark) { 527 bookmarks.removeBookmark(app.selectedPlacemark.longitude, app.selectedPlacemark.latitude) 528 } else { 529 bookmarks.addBookmark(app.selectedPlacemark, "Default") 530 } 531 bookmarkButton.bookmark = !bookmarkButton.bookmark 532 } 533 } 534 } 535 } 536 537 538 BorderImage { 539 anchors.top: mapItem.bottom 540 anchors.bottom: dialogLoader.bottom 541 anchors.right: parent.right 542 anchors.left: parent.left 543 anchors.margins: -14 544 border { top: 14; left: 14; right: 14; bottom: 14 } 545 source: "qrc:///border_shadow.png" 546 } 547 548 Search { 549 id: search 550 anchors.fill: parent 551 marbleQuickItem: marbleMaps 552 visible: !navigationManager.visible 553 554 onItemSelected: { 555 if (routingManager) { 556 routingManager.addSearchResultAsPlacemark(suggestedPlacemark); 557 } 558 app.selectedPlacemark = suggestedPlacemark; 559 app.state = "place" 560 } 561 onMenuButtonClicked: sidePanel.open() 562 } 563 564 Loader { 565 id: dialogLoader 566 focus: true 567 width: childrenRect.width 568 height : childrenRect.height 569 570 anchors { 571 left: parent.left 572 right: parent.right 573 top: parent.bottom 574 bottom: bottomMenu.top 575 topMargin: app.animatedMargin 576 bottomMargin: Kirigami.Units.gridUnits * 10 577 } 578 579 NumberAnimation { 580 id: loaderAnimation 581 target: dialogLoader.item 582 property: "y" 583 from: dialogLoader.height === 0 ? app.height : app.height - dialogLoader.item.height 584 to: 0 585 duration: 500 586 easing.type: Easing.InExpo 587 } 588 589 onLoaded: { 590 app.state != "none" ? loaderAnimation.running = true : loaderAnimation.running = false 591 if (app.state === "place") { 592 dialogLoader.item.map = marbleMaps 593 dialogLoader.item.placemark = app.selectedPlacemark 594 dialogLoader.item.showOsmTags = app.showOsmTags 595 dialogLoader.item.showAccessibility = sidePanel.showAccessibility 596 } else if (app.state === "route") { 597 item.routingManager = routingManager 598 item.routingProfile = routingManager.routingProfile 599 item.currentIndex = Qt.binding(function() { return app.currentWaypointIndex }) 600 } else if (app.state == "position") { 601 dialogLoader.item.map = marbleMaps 602 dialogLoader.item.navigationManager = navigationManager 603 } else if (app.state == "none"){ 604 dialogLoader.height = 0 605 } 606 } 607 608 Connections { 609 target: dialogLoader.item 610 onCurrentProfileIconChanged: routeEditorButton.currentProfileIcon = dialogLoader.item.currentProfileIcon 611 ignoreUnknownSignals: true 612 } 613 } 614 615 Rectangle { 616 width: parent.width 617 color: Kirigami.Theme.textColor 618 opacity: 0.4 619 height: 1 620 anchors.bottom: dialogLoader.top 621 } 622 623 Item { 624 id: stateTracker 625 state: "none" 626 627 states: [ 628 State { 629 name: "none" 630 PropertyChanges { target: dialogLoader; source: "" } 631 }, 632 State { 633 name: "position" 634 PropertyChanges { target: dialogLoader; source: "CurrentPosition.qml" } 635 }, 636 State { 637 name: "route" 638 PropertyChanges { target: dialogLoader; source: "RouteEditor.qml" } 639 }, 640 State { 641 name: "place" 642 PropertyChanges { target: dialogLoader; source: "PlacemarkDialog.qml" } 643 }, 644 State { 645 name: "about" 646 PropertyChanges { target: dialogLoader; source: "" } 647 }, 648 State { 649 name: "settings" 650 PropertyChanges { target: dialogLoader; source: "SettingsDialog.qml" } 651 }, 652 State { 653 name: "developer" 654 PropertyChanges { target: dialogLoader; source: "DeveloperDialog.qml" } 655 }, 656 State { 657 name: "options" 658 PropertyChanges { target: dialogLoader; source: "" } 659 }, 660 State { 661 name: "bookmarks" 662 PropertyChanges { target: dialogLoader; source: "" } 663 } 664 ] 665 } 666 667 BoxedText { 668 id: quitHelper 669 visible: false 670 text: qsTr("Press again to close.") 671 anchors.bottom: parent.bottom 672 anchors.bottomMargin: Screen.pixelDensity * 5 673 anchors.horizontalCenter: parent.horizontalCenter 674 onVisibleChanged: { 675 if (visible) { 676 quitTimer.restart() 677 } 678 } 679 680 Timer { 681 id: quitTimer 682 interval: 3000; 683 running: false; 684 repeat: false 685 onTriggered: { 686 app.aboutToQuit = false 687 quitHelper.visible = false 688 } 689 } 690 } 691 692 Bookmarks { 693 id: bookmarks 694 map: marbleMaps 695 } 696 } 697} 698 699