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