From 956b45ddfda060fcd818659ee05618ed2e4bfcab Mon Sep 17 00:00:00 2001 From: "Lubomir I. Ivanov" Date: Sat, 4 Nov 2017 21:23:37 +0200 Subject: map-widget: move the widget and its resources to 'map-widget' Move all the map widget platform agnostic files to the /map-widget folder. This avoids the confusion about the desktop version of subsurface using mobile components. The map widget is planned as a shared component between the mobile and desktop versions. desktop-widgets/mapwidget[.h/.cpp] still remain as those are specific to the desktop version. Signed-off-by: Lubomir I. Ivanov --- desktop-widgets/CMakeLists.txt | 2 +- desktop-widgets/mapwidget.cpp | 2 +- map-widget/qml/MapWidget.qml | 355 +++++++++++++++++++++ map-widget/qml/MapWidgetContextMenu.qml | 124 +++++++ map-widget/qml/MapWidgetError.qml | 13 + map-widget/qml/icons/mapwidget-context-menu.png | Bin 0 -> 242 bytes map-widget/qml/icons/mapwidget-marker-gray.png | Bin 0 -> 2033 bytes map-widget/qml/icons/mapwidget-marker-selected.png | Bin 0 -> 1995 bytes map-widget/qml/icons/mapwidget-marker.png | Bin 0 -> 1801 bytes .../qml/icons/mapwidget-toggle-satellite.png | Bin 0 -> 6288 bytes map-widget/qml/icons/mapwidget-toggle-street.png | Bin 0 -> 5916 bytes map-widget/qml/icons/mapwidget-zoom-in.png | Bin 0 -> 256 bytes map-widget/qml/icons/mapwidget-zoom-out.png | Bin 0 -> 242 bytes map-widget/qmlmapwidgethelper.cpp | 264 +++++++++++++++ map-widget/qmlmapwidgethelper.h | 53 +++ mobile-widgets/qml/MapWidget.qml | 355 --------------------- mobile-widgets/qml/MapWidgetContextMenu.qml | 124 ------- mobile-widgets/qml/MapWidgetError.qml | 13 - .../qml/icons/mapwidget-context-menu.png | Bin 242 -> 0 bytes mobile-widgets/qml/icons/mapwidget-marker-gray.png | Bin 2033 -> 0 bytes .../qml/icons/mapwidget-marker-selected.png | Bin 1995 -> 0 bytes mobile-widgets/qml/icons/mapwidget-marker.png | Bin 1801 -> 0 bytes .../qml/icons/mapwidget-toggle-satellite.png | Bin 6288 -> 0 bytes .../qml/icons/mapwidget-toggle-street.png | Bin 5916 -> 0 bytes mobile-widgets/qml/icons/mapwidget-zoom-in.png | Bin 256 -> 0 bytes mobile-widgets/qml/icons/mapwidget-zoom-out.png | Bin 242 -> 0 bytes mobile-widgets/qmlmapwidgethelper.cpp | 264 --------------- mobile-widgets/qmlmapwidgethelper.h | 53 --- subsurface.qrc | 22 +- 29 files changed, 822 insertions(+), 822 deletions(-) create mode 100644 map-widget/qml/MapWidget.qml create mode 100644 map-widget/qml/MapWidgetContextMenu.qml create mode 100644 map-widget/qml/MapWidgetError.qml create mode 100644 map-widget/qml/icons/mapwidget-context-menu.png create mode 100644 map-widget/qml/icons/mapwidget-marker-gray.png create mode 100644 map-widget/qml/icons/mapwidget-marker-selected.png create mode 100644 map-widget/qml/icons/mapwidget-marker.png create mode 100644 map-widget/qml/icons/mapwidget-toggle-satellite.png create mode 100644 map-widget/qml/icons/mapwidget-toggle-street.png create mode 100644 map-widget/qml/icons/mapwidget-zoom-in.png create mode 100644 map-widget/qml/icons/mapwidget-zoom-out.png create mode 100644 map-widget/qmlmapwidgethelper.cpp create mode 100644 map-widget/qmlmapwidgethelper.h delete mode 100644 mobile-widgets/qml/MapWidget.qml delete mode 100644 mobile-widgets/qml/MapWidgetContextMenu.qml delete mode 100644 mobile-widgets/qml/MapWidgetError.qml delete mode 100644 mobile-widgets/qml/icons/mapwidget-context-menu.png delete mode 100644 mobile-widgets/qml/icons/mapwidget-marker-gray.png delete mode 100644 mobile-widgets/qml/icons/mapwidget-marker-selected.png delete mode 100644 mobile-widgets/qml/icons/mapwidget-marker.png delete mode 100644 mobile-widgets/qml/icons/mapwidget-toggle-satellite.png delete mode 100644 mobile-widgets/qml/icons/mapwidget-toggle-street.png delete mode 100644 mobile-widgets/qml/icons/mapwidget-zoom-in.png delete mode 100644 mobile-widgets/qml/icons/mapwidget-zoom-out.png delete mode 100644 mobile-widgets/qmlmapwidgethelper.cpp delete mode 100644 mobile-widgets/qmlmapwidgethelper.h diff --git a/desktop-widgets/CMakeLists.txt b/desktop-widgets/CMakeLists.txt index 6a7e1313b..b94b840d1 100644 --- a/desktop-widgets/CMakeLists.txt +++ b/desktop-widgets/CMakeLists.txt @@ -69,7 +69,7 @@ set(SUBSURFACE_INTERFACE kmessagewidget.cpp mainwindow.cpp mapwidget.cpp - ../mobile-widgets/qmlmapwidgethelper.cpp + ../map-widget/qmlmapwidgethelper.cpp modeldelegates.cpp notificationwidget.cpp simplewidgets.cpp diff --git a/desktop-widgets/mapwidget.cpp b/desktop-widgets/mapwidget.cpp index 806b82c2d..aefda2a32 100644 --- a/desktop-widgets/mapwidget.cpp +++ b/desktop-widgets/mapwidget.cpp @@ -7,7 +7,7 @@ #include "mapwidget.h" #include "core/dive.h" #include "core/divesite.h" -#include "mobile-widgets/qmlmapwidgethelper.h" +#include "map-widget/qmlmapwidgethelper.h" #include "qt-models/maplocationmodel.h" #include "mainwindow.h" #include "divelistview.h" diff --git a/map-widget/qml/MapWidget.qml b/map-widget/qml/MapWidget.qml new file mode 100644 index 000000000..2a3283482 --- /dev/null +++ b/map-widget/qml/MapWidget.qml @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: GPL-2.0 +import QtQuick 2.0 +import QtLocation 5.3 +import QtPositioning 5.3 +import org.subsurfacedivelog.mobile 1.0 + +Item { + id: rootItem + property int nSelectedDives: 0 + + MapWidgetHelper { + id: mapHelper + map: map + editMode: false + onSelectedDivesChanged: nSelectedDives = list.length + onEditModeChanged: editMessage.isVisible = editMode === true ? 1 : 0 + onCoordinatesChanged: {} + Component.onCompleted: { + map.plugin = Qt.createQmlObject(pluginObject, rootItem) + map.mapType = { "STREET": map.supportedMapTypes[0], "SATELLITE": map.supportedMapTypes[1] } + map.activeMapType = map.mapType.SATELLITE + } + } + + Map { + id: map + anchors.fill: parent + zoomLevel: defaultZoomIn + + property var mapType + readonly property var defaultCenter: QtPositioning.coordinate(0, 0) + readonly property real defaultZoomIn: 12.0 + readonly property real defaultZoomOut: 1.0 + readonly property real textVisibleZoom: 11.0 + readonly property real zoomStep: 2.0 + property var newCenter: defaultCenter + property real newZoom: 1.0 + property real newZoomOut: 1.0 + property var clickCoord: QtPositioning.coordinate(0, 0) + property bool isReady: false + + Component.onCompleted: isReady = true + onZoomLevelChanged: { + if (isReady) + mapHelper.calculateSmallCircleRadius(map.center) + } + + MapItemView { + id: mapItemView + model: mapHelper.model + delegate: MapQuickItem { + id: mapItem + anchorPoint.x: 0 + anchorPoint.y: mapItemImage.height + coordinate: model.coordinate + z: mapHelper.model.selectedUuid === model.uuid ? mapHelper.model.count - 1 : 0 + sourceItem: Image { + id: mapItemImage + source: "qrc:///mapwidget-marker" + (mapHelper.model.selectedUuid === model.uuid ? "-selected" : (mapHelper.editMode ? "-gray" : "")) + SequentialAnimation { + id: mapItemImageAnimation + PropertyAnimation { target: mapItemImage; property: "scale"; from: 1.0; to: 0.7; duration: 120 } + PropertyAnimation { target: mapItemImage; property: "scale"; from: 0.7; to: 1.0; duration: 80 } + } + MouseArea { + drag.target: (mapHelper.editMode && mapHelper.model.selectedUuid === model.uuid) ? mapItem : undefined + anchors.fill: parent + onClicked: { + if (!mapHelper.editMode) + mapHelper.model.setSelectedUuid(model.uuid, true) + } + onDoubleClicked: map.doubleClickHandler(mapItem.coordinate) + onReleased: { + if (mapHelper.editMode && mapHelper.model.selectedUuid === model.uuid) { + mapHelper.updateCurrentDiveSiteCoordinates(mapHelper.model.selectedUuid, mapItem.coordinate) + } + } + } + Item { + // Text with a duplicate for shadow. DropShadow as layer effect is kind of slow here. + y: mapItemImage.y + mapItemImage.height + visible: map.zoomLevel >= map.textVisibleZoom + Text { + id: mapItemTextShadow + x: mapItemText.x + 2; y: mapItemText.y + 2 + text: mapItemText.text + font.pointSize: mapItemText.font.pointSize + color: "black" + } + Text { + id: mapItemText + text: model.name + font.pointSize: 11.0 + color: mapHelper.model.selectedUuid === model.uuid ? "white" : "lightgrey" + } + } + } + } + } + + SequentialAnimation { + id: mapAnimationZoomIn + NumberAnimation { + target: map; property: "zoomLevel"; to: map.newZoomOut; duration: Math.abs(map.newZoomOut - map.zoomLevel) * 200 + } + ParallelAnimation { + CoordinateAnimation { target: map; property: "center"; to: map.newCenter; duration: 1000 } + NumberAnimation { + target: map; property: "zoomLevel"; to: map.newZoom ; duration: 2000; easing.type: Easing.InCubic + } + } + } + + ParallelAnimation { + id: mapAnimationZoomOut + NumberAnimation { target: map; property: "zoomLevel"; from: map.zoomLevel; to: map.newZoom; duration: 3000 } + SequentialAnimation { + PauseAnimation { duration: 2000 } + CoordinateAnimation { target: map; property: "center"; to: map.newCenter; duration: 2000 } + } + } + + ParallelAnimation { + id: mapAnimationClick + CoordinateAnimation { target: map; property: "center"; to: map.newCenter; duration: 500 } + NumberAnimation { target: map; property: "zoomLevel"; to: map.newZoom; duration: 500 } + } + + MouseArea { + anchors.fill: parent + onDoubleClicked: map.doubleClickHandler(map.toCoordinate(Qt.point(mouseX, mouseY))) + } + + function doubleClickHandler(coord) { + newCenter = coord + newZoom = zoomLevel + zoomStep + if (newZoom > maximumZoomLevel) + newZoom = maximumZoomLevel + mapAnimationClick.restart() + } + + function animateMapZoomOut() { + newCenter = defaultCenter + newZoom = defaultZoomOut + mapAnimationZoomIn.stop() + mapAnimationZoomOut.restart() + } + + function pointIsVisible(pt) { + return !isNaN(pt.x) + } + + function stopZoomAnimations() { + mapAnimationZoomIn.stop() + mapAnimationZoomOut.stop() + } + + function centerOnCoordinate(coord) { + stopZoomAnimations() + if (coord.latitude === 0.0 && coord.longitude === 0.0) { + // Do nothing + } else { + var newZoomOutFound = false + var zoomStored = zoomLevel + newZoomOut = zoomLevel + newCenter = coord + while (zoomLevel > minimumZoomLevel) { + var pt = fromCoordinate(coord) + if (pointIsVisible(pt)) { + newZoomOut = zoomLevel + newZoomOutFound = true + break + } + zoomLevel-- + } + if (!newZoomOutFound) + newZoomOut = defaultZoomOut + zoomLevel = zoomStored + newZoom = zoomStored + mapAnimationZoomIn.restart() + mapAnimationZoomOut.stop() + } + } + + function centerOnRectangle(topLeft, bottomRight, centerRect) { + stopZoomAnimations() + if (newCenter.latitude === 0.0 && newCenter.longitude === 0.0) { + // Do nothing + } else { + var centerStored = QtPositioning.coordinate(center.latitude, center.longitude) + var zoomStored = zoomLevel + var newZoomOutFound = false + newCenter = centerRect + // calculate zoom out + newZoomOut = zoomLevel + while (zoomLevel > minimumZoomLevel) { + var ptCenter = fromCoordinate(centerStored) + var ptCenterRect = fromCoordinate(centerRect) + if (pointIsVisible(ptCenter) && pointIsVisible(ptCenterRect)) { + newZoomOut = zoomLevel + newZoomOutFound = true + break + } + zoomLevel-- + } + if (!newZoomOutFound) + newZoomOut = defaultZoomOut + // calculate zoom in + center = newCenter + zoomLevel = maximumZoomLevel + var diagonalRect = topLeft.distanceTo(bottomRight) + while (zoomLevel > minimumZoomLevel) { + var c0 = toCoordinate(Qt.point(0.0, 0.0)) + var c1 = toCoordinate(Qt.point(width, height)) + if (c0.distanceTo(c1) > diagonalRect) { + newZoom = zoomLevel - 2.0 + break + } + zoomLevel-- + } + if (newZoom > defaultZoomIn) + newZoom = defaultZoomIn + zoomLevel = zoomStored + center = centerStored + mapAnimationZoomIn.restart() + mapAnimationZoomOut.stop() + } + } + + function deselectMapLocation() { + stopZoomAnimations() + } + } + + Rectangle { + id: editMessage + radius: padding + color: "#b08000" + border.color: "white" + x: (map.width - width) * 0.5; y: padding + width: editMessageText.width + padding * 2.0 + height: editMessageText.height + padding * 2.0 + visible: false + opacity: 0.0 + property int isVisible: -1 + property real padding: 10.0 + onOpacityChanged: visible = opacity != 0.0 + states: [ + State { when: editMessage.isVisible === 1; PropertyChanges { target: editMessage; opacity: 1.0 }}, + State { when: editMessage.isVisible === 0; PropertyChanges { target: editMessage; opacity: 0.0 }} + ] + transitions: Transition { NumberAnimation { properties: "opacity"; easing.type: Easing.InOutQuad }} + Text { + id: editMessageText + y: editMessage.padding; x: editMessage.padding + verticalAlignment: Text.AlignVCenter + color: "white" + font.pointSize: 11.0 + text: qsTr("Drag the selected dive location") + } + } + + Image { + id: toggleImage + x: 10; y: x + width: 40 + height: 40 + source: "qrc:///mapwidget-toggle-" + (map.activeMapType === map.mapType.SATELLITE ? "street" : "satellite") + SequentialAnimation { + id: toggleImageAnimation + PropertyAnimation { target: toggleImage; property: "scale"; from: 1.0; to: 0.8; duration: 120 } + PropertyAnimation { target: toggleImage; property: "scale"; from: 0.8; to: 1.0; duration: 80 } + } + MouseArea { + anchors.fill: parent + onClicked: { + map.activeMapType = map.activeMapType === map.mapType.SATELLITE ? map.mapType.STREET : map.mapType.SATELLITE + toggleImageAnimation.restart() + } + } + } + + Image { + id: imageZoomIn + x: 10 + (toggleImage.width - imageZoomIn.width) * 0.5; y: toggleImage.y + toggleImage.height + 10 + width: 20 + height: 20 + source: "qrc:///mapwidget-zoom-in" + SequentialAnimation { + id: imageZoomInAnimation + PropertyAnimation { target: imageZoomIn; property: "scale"; from: 1.0; to: 0.8; duration: 120 } + PropertyAnimation { target: imageZoomIn; property: "scale"; from: 0.8; to: 1.0; duration: 80 } + } + MouseArea { + anchors.fill: parent + onClicked: { + map.newCenter = map.center + map.newZoom = map.zoomLevel + map.zoomStep + if (map.newZoom > map.maximumZoomLevel) + map.newZoom = map.maximumZoomLevel + mapAnimationClick.restart() + imageZoomInAnimation.restart() + } + } + } + + Image { + id: imageZoomOut + x: imageZoomIn.x; y: imageZoomIn.y + imageZoomIn.height + 10 + source: "qrc:///mapwidget-zoom-out" + width: 20 + height: 20 + SequentialAnimation { + id: imageZoomOutAnimation + PropertyAnimation { target: imageZoomOut; property: "scale"; from: 1.0; to: 0.8; duration: 120 } + PropertyAnimation { target: imageZoomOut; property: "scale"; from: 0.8; to: 1.0; duration: 80 } + } + MouseArea { + anchors.fill: parent + onClicked: { + map.newCenter = map.center + map.newZoom = map.zoomLevel - map.zoomStep + mapAnimationClick.restart() + imageZoomOutAnimation.restart() + } + } + } + + function openLocationInGoogleMaps(latitude, longitude) { + var loc = latitude + " " + longitude + var url = "https://www.google.com/maps/place/" + loc + "/@" + loc + ",5000m/data=!3m1!1e3!4m2!3m1!1s0x0:0x0" + Qt.openUrlExternally(url) + } + + MapWidgetContextMenu { + id: contextMenu + y: 10; x: map.width - y + onActionSelected: { + switch (action) { + case contextMenu.actions.OPEN_LOCATION_IN_GOOGLE_MAPS: + openLocationInGoogleMaps(map.center.latitude, map.center.longitude) + break + case contextMenu.actions.COPY_LOCATION_DECIMAL: + mapHelper.copyToClipboardCoordinates(map.center, false) + break + case contextMenu.actions.COPY_LOCATION_SEXAGESIMAL: + mapHelper.copyToClipboardCoordinates(map.center, true) + break + case contextMenu.actions.SELECT_VISIBLE_LOCATIONS: + mapHelper.selectVisibleLocations() + break + } + } + } +} diff --git a/map-widget/qml/MapWidgetContextMenu.qml b/map-widget/qml/MapWidgetContextMenu.qml new file mode 100644 index 000000000..17450a729 --- /dev/null +++ b/map-widget/qml/MapWidgetContextMenu.qml @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-2.0 +import QtQuick 2.0 + +Item { + id: container + signal actionSelected(int action) + + readonly property var actions: { + "OPEN_LOCATION_IN_GOOGLE_MAPS": 0, + "COPY_LOCATION_DECIMAL": 1, + "COPY_LOCATION_SEXAGESIMAL": 2, + "SELECT_VISIBLE_LOCATIONS": 3 + } + readonly property var menuItemData: [ + { idx: actions.OPEN_LOCATION_IN_GOOGLE_MAPS, itemText: qsTr("Open location in Google Maps") }, + { idx: actions.COPY_LOCATION_DECIMAL, itemText: qsTr("Copy location to clipboard (decimal)") }, + { idx: actions.COPY_LOCATION_SEXAGESIMAL, itemText: qsTr("Copy location to clipboard (sexagesimal)") }, + { idx: actions.SELECT_VISIBLE_LOCATIONS, itemText: qsTr("Select visible dive locations") } + ] + readonly property real itemTextPadding: 10.0 + readonly property real itemHeight: 34.0 + readonly property int itemAnimationDuration: 100 + readonly property color colorItemBackground: "#dedede" + readonly property color colorItemBackgroundSelected: "grey" + readonly property color colorItemText: "black" + readonly property color colorItemTextSelected: "#dedede" + readonly property color colorItemBorder: "black" + property int listViewIsVisible: -1 + property real maxItemWidth: 0.0 + + Image { + id: contextMenuImage + x: -width + source: "qrc:///mapwidget-context-menu" + + SequentialAnimation { + id:contextMenuImageAnimation + PropertyAnimation { target: contextMenuImage; property: "scale"; from: 1.0; to: 0.8; duration: 80 } + PropertyAnimation { target: contextMenuImage; property: "scale"; from: 0.8; to: 1.0; duration: 60 } + } + + MouseArea { + anchors.fill: parent + onClicked: { + contextMenuImageAnimation.restart() + listViewIsVisible = (listViewIsVisible !== 1) ? 1 : 0 + } + } + } + + ListModel { + id: listModel + property int selectedIdx: -1 + Component.onCompleted: { + for (var i = 0; i < menuItemData.length; i++) + append(menuItemData[i]); + } + } + + Component { + id: listItemDelegate + Rectangle { + color: model.idx === listModel.selectedIdx ? colorItemBackgroundSelected : colorItemBackground + width: maxItemWidth + height: itemHeight + border.color: colorItemBorder + Text { + x: itemTextPadding + height: itemHeight + verticalAlignment: Text.AlignVCenter + text: model.itemText + font.pointSize: 10.0 + color: model.idx === listModel.selectedIdx ? colorItemTextSelected : colorItemText + onWidthChanged: { + if (width + itemTextPadding * 2.0 > maxItemWidth) + maxItemWidth = width + itemTextPadding * 2.0 + } + Behavior on color { ColorAnimation { duration: itemAnimationDuration }} + } + Behavior on color { ColorAnimation { duration: itemAnimationDuration }} + } + } + + ListView { + id: listView + y: contextMenuImage.y + contextMenuImage.height + 10; + width: maxItemWidth; + height: listModel.count * itemHeight + visible: false + opacity: 0.0 + interactive: false + model: listModel + delegate: listItemDelegate + + onCountChanged: x = -maxItemWidth + onVisibleChanged: listModel.selectedIdx = -1 + onOpacityChanged: visible = opacity != 0.0 + + Timer { + id: timerListViewVisible + running: false + repeat: false + interval: itemAnimationDuration + 50 + onTriggered: listViewIsVisible = 0 + } + + MouseArea { + anchors.fill: parent + onClicked: { + if (opacity < 1.0) + return; + var idx = listView.indexAt(mouseX, mouseY) + listModel.selectedIdx = idx + container.actionSelected(idx) + timerListViewVisible.restart() + } + } + states: [ + State { when: listViewIsVisible === 1; PropertyChanges { target: listView; opacity: 1.0 }}, + State { when: listViewIsVisible === 0; PropertyChanges { target: listView; opacity: 0.0 }} + ] + transitions: Transition { NumberAnimation { properties: "opacity"; easing.type: Easing.InOutQuad }} + } +} diff --git a/map-widget/qml/MapWidgetError.qml b/map-widget/qml/MapWidgetError.qml new file mode 100644 index 000000000..346e95f07 --- /dev/null +++ b/map-widget/qml/MapWidgetError.qml @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0 +import QtQuick 2.0 + +Item { + Text { + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: "red" + text: qsTr("MapWidget.qml failed to load! +The QML modules QtPositioning and QtLocation could be missing!") + } +} diff --git a/map-widget/qml/icons/mapwidget-context-menu.png b/map-widget/qml/icons/mapwidget-context-menu.png new file mode 100644 index 000000000..6ab7cf77d Binary files /dev/null and b/map-widget/qml/icons/mapwidget-context-menu.png differ diff --git a/map-widget/qml/icons/mapwidget-marker-gray.png b/map-widget/qml/icons/mapwidget-marker-gray.png new file mode 100644 index 000000000..856db9f5b Binary files /dev/null and b/map-widget/qml/icons/mapwidget-marker-gray.png differ diff --git a/map-widget/qml/icons/mapwidget-marker-selected.png b/map-widget/qml/icons/mapwidget-marker-selected.png new file mode 100644 index 000000000..57f4efa27 Binary files /dev/null and b/map-widget/qml/icons/mapwidget-marker-selected.png differ diff --git a/map-widget/qml/icons/mapwidget-marker.png b/map-widget/qml/icons/mapwidget-marker.png new file mode 100644 index 000000000..a1be73866 Binary files /dev/null and b/map-widget/qml/icons/mapwidget-marker.png differ diff --git a/map-widget/qml/icons/mapwidget-toggle-satellite.png b/map-widget/qml/icons/mapwidget-toggle-satellite.png new file mode 100644 index 000000000..7ee536929 Binary files /dev/null and b/map-widget/qml/icons/mapwidget-toggle-satellite.png differ diff --git a/map-widget/qml/icons/mapwidget-toggle-street.png b/map-widget/qml/icons/mapwidget-toggle-street.png new file mode 100644 index 000000000..04a668c3f Binary files /dev/null and b/map-widget/qml/icons/mapwidget-toggle-street.png differ diff --git a/map-widget/qml/icons/mapwidget-zoom-in.png b/map-widget/qml/icons/mapwidget-zoom-in.png new file mode 100644 index 000000000..8c2521c3e Binary files /dev/null and b/map-widget/qml/icons/mapwidget-zoom-in.png differ diff --git a/map-widget/qml/icons/mapwidget-zoom-out.png b/map-widget/qml/icons/mapwidget-zoom-out.png new file mode 100644 index 000000000..bd372f17d Binary files /dev/null and b/map-widget/qml/icons/mapwidget-zoom-out.png differ diff --git a/map-widget/qmlmapwidgethelper.cpp b/map-widget/qmlmapwidgethelper.cpp new file mode 100644 index 000000000..ba197f63f --- /dev/null +++ b/map-widget/qmlmapwidgethelper.cpp @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: GPL-2.0 +#include +#include +#include +#include +#include + +#include "qmlmapwidgethelper.h" +#include "core/dive.h" +#include "core/divesite.h" +#include "core/helpers.h" +#include "qt-models/maplocationmodel.h" + +#define MIN_DISTANCE_BETWEEN_DIVE_SITES_M 50.0 +#define SMALL_CIRCLE_RADIUS_PX 26.0 + +MapWidgetHelper::MapWidgetHelper(QObject *parent) : QObject(parent) +{ + m_mapLocationModel = new MapLocationModel(this); + connect(m_mapLocationModel, SIGNAL(selectedLocationChanged(MapLocation *)), + this, SLOT(selectedLocationChanged(MapLocation *))); +} + +void MapWidgetHelper::centerOnDiveSite(struct dive_site *ds) +{ + int idx; + struct dive *dive; + QVector selDS; + QVector selGC; + QGeoCoordinate dsCoord; + + for_each_dive (idx, dive) { + struct dive_site *dss = get_dive_site_for_dive(dive); + if (!dive_site_has_gps_location(dss) || !dive->selected) + continue; + // only store dive sites with GPS + selDS.append(dss); + selGC.append(QGeoCoordinate(dss->latitude.udeg * 0.000001, + dss->longitude.udeg * 0.000001)); + } + if (!dive_site_has_gps_location(ds) && !selDS.size()) { + // only a single dive site with no GPS selected + m_mapLocationModel->setSelectedUuid(ds ? ds->uuid : 0, false); + QMetaObject::invokeMethod(m_map, "deselectMapLocation"); + + } else if (selDS.size() == 1) { + // a single dive site with GPS selected + ds = selDS.at(0); + m_mapLocationModel->setSelectedUuid(ds->uuid, false); + dsCoord.setLatitude(ds->latitude.udeg * 0.000001); + dsCoord.setLongitude(ds->longitude.udeg * 0.000001); + QMetaObject::invokeMethod(m_map, "centerOnCoordinate", Q_ARG(QVariant, QVariant::fromValue(dsCoord))); + } else if (selDS.size() > 1) { + /* more than one dive sites with GPS selected. + * find the most top-left and bottom-right dive sites on the map coordinate system. */ + ds = selDS.at(0); + m_mapLocationModel->setSelectedUuid(ds->uuid, false); + qreal minLat = 0.0, minLon = 0.0, maxLat = 0.0, maxLon = 0.0; + bool start = true; + foreach(QGeoCoordinate gc, selGC) { + qreal lat = gc.latitude(); + qreal lon = gc.longitude(); + if (start) { + minLat = maxLat = lat; + minLon = maxLon = lon; + start = false; + continue; + } + if (lat < minLat) + minLat = lat; + else if (lat > maxLat) + maxLat = lat; + if (lon < minLon) + minLon = lon; + else if (lon > maxLon) + maxLon = lon; + } + // pass rectangle coordinates to QML + QGeoCoordinate coordTopLeft(minLat, minLon); + QGeoCoordinate coordBottomRight(maxLat, maxLon); + QGeoCoordinate coordCenter(minLat + (maxLat - minLat) * 0.5, minLon + (maxLon - minLon) * 0.5); + QMetaObject::invokeMethod(m_map, "centerOnRectangle", + Q_ARG(QVariant, QVariant::fromValue(coordTopLeft)), + Q_ARG(QVariant, QVariant::fromValue(coordBottomRight)), + Q_ARG(QVariant, QVariant::fromValue(coordCenter))); + } +} + +void MapWidgetHelper::reloadMapLocations() +{ + struct dive_site *ds; + int idx; + QMap locationNameMap; + m_mapLocationModel->clear(); + MapLocation *location; + QVector locationList; + qreal latitude, longitude; + + if (displayed_dive_site.uuid && dive_site_has_gps_location(&displayed_dive_site)) { + latitude = displayed_dive_site.latitude.udeg * 0.000001; + longitude = displayed_dive_site.longitude.udeg * 0.000001; + location = new MapLocation(displayed_dive_site.uuid, QGeoCoordinate(latitude, longitude), + QString(displayed_dive_site.name)); + locationList.append(location); + locationNameMap[QString(displayed_dive_site.name)] = location; + } + for_each_dive_site(idx, ds) { + if (!dive_site_has_gps_location(ds) || ds->uuid == displayed_dive_site.uuid) + continue; + latitude = ds->latitude.udeg * 0.000001; + longitude = ds->longitude.udeg * 0.000001; + QGeoCoordinate dsCoord(latitude, longitude); + QString name(ds->name); + // don't add dive locations with the same name, unless they are + // at least MIN_DISTANCE_BETWEEN_DIVE_SITES_M apart + if (locationNameMap.contains(name)) { + MapLocation *existingLocation = locationNameMap[name]; + QGeoCoordinate coord = qvariant_cast(existingLocation->getRole(MapLocation::Roles::RoleCoordinate)); + if (dsCoord.distanceTo(coord) < MIN_DISTANCE_BETWEEN_DIVE_SITES_M) + continue; + } + location = new MapLocation(ds->uuid, dsCoord, name); + locationList.append(location); + locationNameMap[name] = location; + } + m_mapLocationModel->addList(locationList); +} + +void MapWidgetHelper::selectedLocationChanged(MapLocation *location) +{ + int idx; + struct dive *dive; + m_selectedDiveIds.clear(); + QGeoCoordinate locationCoord = location->coordinate(); + for_each_dive (idx, dive) { + struct dive_site *ds = get_dive_site_for_dive(dive); + if (!dive_site_has_gps_location(ds)) + continue; + const qreal latitude = ds->latitude.udeg * 0.000001; + const qreal longitude = ds->longitude.udeg * 0.000001; + QGeoCoordinate dsCoord(latitude, longitude); + if (locationCoord.distanceTo(dsCoord) < m_smallCircleRadius) + m_selectedDiveIds.append(idx); + } + emit selectedDivesChanged(m_selectedDiveIds); +} + +void MapWidgetHelper::selectVisibleLocations() +{ + int idx; + struct dive *dive; + bool selectedFirst = false; + m_selectedDiveIds.clear(); + for_each_dive (idx, dive) { + struct dive_site *ds = get_dive_site_for_dive(dive); + if (!dive_site_has_gps_location(ds)) + continue; + const qreal latitude = ds->latitude.udeg * 0.000001; + const qreal longitude = ds->longitude.udeg * 0.000001; + QGeoCoordinate dsCoord(latitude, longitude); + QPointF point; + QMetaObject::invokeMethod(m_map, "fromCoordinate", Q_RETURN_ARG(QPointF, point), + Q_ARG(QGeoCoordinate, dsCoord)); + if (!qIsNaN(point.x())) { + if (!selectedFirst) { + m_mapLocationModel->setSelectedUuid(ds->uuid, false); + selectedFirst = true; + } + m_selectedDiveIds.append(idx); + } + } + emit selectedDivesChanged(m_selectedDiveIds); +} + +/* + * Based on a 2D Map widget circle with center "coord" and radius SMALL_CIRCLE_RADIUS_PX, + * obtain a "small circle" with radius m_smallCircleRadius in meters: + * https://en.wikipedia.org/wiki/Circle_of_a_sphere + * + * The idea behind this circle is to be able to select multiple nearby dives, when clicking on + * the map. This code can be in QML, but it is in C++ instead for performance reasons. + * + * This can be made faster with an exponential regression [a * exp(b * x)], with a pretty + * decent R-squared, but it becomes bound to map provider zoom level mappings and the + * SMALL_CIRCLE_RADIUS_PX value, which makes the code hard to maintain. + */ +void MapWidgetHelper::calculateSmallCircleRadius(QGeoCoordinate coord) +{ + QPointF point; + QMetaObject::invokeMethod(m_map, "fromCoordinate", Q_RETURN_ARG(QPointF, point), + Q_ARG(QGeoCoordinate, coord)); + QPointF point2(point.x() + SMALL_CIRCLE_RADIUS_PX, point.y()); + QGeoCoordinate coord2; + QMetaObject::invokeMethod(m_map, "toCoordinate", Q_RETURN_ARG(QGeoCoordinate, coord2), + Q_ARG(QPointF, point2)); + m_smallCircleRadius = coord2.distanceTo(coord); +} + +void MapWidgetHelper::copyToClipboardCoordinates(QGeoCoordinate coord, bool formatTraditional) +{ + bool savep = prefs.coordinates_traditional; + prefs.coordinates_traditional = formatTraditional; + + const int lat = lrint(1000000.0 * coord.latitude()); + const int lon = lrint(1000000.0 * coord.longitude()); + const char *coordinates = printGPSCoords(lat, lon); + QApplication::clipboard()->setText(QString(coordinates), QClipboard::Clipboard); + + free((void *)coordinates); + prefs.coordinates_traditional = savep; +} + +void MapWidgetHelper::updateCurrentDiveSiteCoordinates(quint32 uuid, QGeoCoordinate coord) +{ + MapLocation *loc = m_mapLocationModel->getMapLocationForUuid(uuid); + if (loc) + loc->setCoordinate(coord); + displayed_dive_site.latitude.udeg = lrint(coord.latitude() * 1000000.0); + displayed_dive_site.longitude.udeg = lrint(coord.longitude() * 1000000.0); + emit coordinatesChanged(); +} + +bool MapWidgetHelper::editMode() +{ + return m_editMode; +} + +void MapWidgetHelper::setEditMode(bool editMode) +{ + m_editMode = editMode; + MapLocation *exists = m_mapLocationModel->getMapLocationForUuid(displayed_dive_site.uuid); + // if divesite uuid doesn't exist in the model, add a new MapLocation. + if (editMode && !exists) { + QGeoCoordinate coord(0.0, 0.0); + m_mapLocationModel->add(new MapLocation(displayed_dive_site.uuid, coord, + QString(displayed_dive_site.name))); + QMetaObject::invokeMethod(m_map, "centerOnCoordinate", + Q_ARG(QVariant, QVariant::fromValue(coord))); + } + emit editModeChanged(); +} + +QString MapWidgetHelper::pluginObject() +{ + QString str; + str += "import QtQuick 2.0;"; + str += "import QtLocation 5.3;"; + str += "Plugin {"; + str += " id: mapPlugin;"; + str += " name: 'googlemaps';"; + str += " PluginParameter { name: 'googlemaps.maps.language'; value: '%lang%' }"; + str += " PluginParameter { name: 'googlemaps.cachefolder'; value: '%cacheFolder%' }"; + str += " Component.onCompleted: {"; + str += " if (availableServiceProviders.indexOf(name) === -1) {"; + str += " console.warn('MapWidget.qml: cannot find a plugin named: ' + name);"; + str += " }"; + str += " }"; + str += "}"; + QString lang = uiLanguage(NULL).replace('_', '-'); + str.replace("%lang%", lang); + QString cacheFolder = QString(system_default_directory()).append("/googlemaps"); + str.replace("%cacheFolder%", cacheFolder.replace("\\", "/")); + return str; +} diff --git a/map-widget/qmlmapwidgethelper.h b/map-widget/qmlmapwidgethelper.h new file mode 100644 index 000000000..36d25d178 --- /dev/null +++ b/map-widget/qmlmapwidgethelper.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0 +#ifndef QMLMAPWIDGETHELPER_H +#define QMLMAPWIDGETHELPER_H + +#include + +class QGeoCoordinate; +class MapLocationModel; +class MapLocation; +struct dive_site; + +class MapWidgetHelper : public QObject { + + Q_OBJECT + Q_PROPERTY(QObject *map MEMBER m_map) + Q_PROPERTY(MapLocationModel *model MEMBER m_mapLocationModel NOTIFY modelChanged) + Q_PROPERTY(bool editMode READ editMode WRITE setEditMode NOTIFY editModeChanged) + Q_PROPERTY(QString pluginObject READ pluginObject NOTIFY pluginObjectChanged) + +public: + explicit MapWidgetHelper(QObject *parent = NULL); + + void centerOnDiveSite(struct dive_site *); + void reloadMapLocations(); + Q_INVOKABLE void copyToClipboardCoordinates(QGeoCoordinate coord, bool formatTraditional); + Q_INVOKABLE void calculateSmallCircleRadius(QGeoCoordinate coord); + Q_INVOKABLE void updateCurrentDiveSiteCoordinates(quint32 uuid, QGeoCoordinate coord); + Q_INVOKABLE void selectVisibleLocations(); + bool editMode(); + void setEditMode(bool editMode); + QString pluginObject(); + +private: + QObject *m_map; + MapLocationModel *m_mapLocationModel; + qreal m_smallCircleRadius; + QList m_selectedDiveIds; + bool m_editMode; + +private slots: + void selectedLocationChanged(MapLocation *); + +signals: + void modelChanged(); + void editModeChanged(); + void selectedDivesChanged(QList list); + void coordinatesChanged(); + void pluginObjectChanged(); +}; + +extern "C" const char *printGPSCoords(int lat, int lon); + +#endif diff --git a/mobile-widgets/qml/MapWidget.qml b/mobile-widgets/qml/MapWidget.qml deleted file mode 100644 index 2a3283482..000000000 --- a/mobile-widgets/qml/MapWidget.qml +++ /dev/null @@ -1,355 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0 -import QtQuick 2.0 -import QtLocation 5.3 -import QtPositioning 5.3 -import org.subsurfacedivelog.mobile 1.0 - -Item { - id: rootItem - property int nSelectedDives: 0 - - MapWidgetHelper { - id: mapHelper - map: map - editMode: false - onSelectedDivesChanged: nSelectedDives = list.length - onEditModeChanged: editMessage.isVisible = editMode === true ? 1 : 0 - onCoordinatesChanged: {} - Component.onCompleted: { - map.plugin = Qt.createQmlObject(pluginObject, rootItem) - map.mapType = { "STREET": map.supportedMapTypes[0], "SATELLITE": map.supportedMapTypes[1] } - map.activeMapType = map.mapType.SATELLITE - } - } - - Map { - id: map - anchors.fill: parent - zoomLevel: defaultZoomIn - - property var mapType - readonly property var defaultCenter: QtPositioning.coordinate(0, 0) - readonly property real defaultZoomIn: 12.0 - readonly property real defaultZoomOut: 1.0 - readonly property real textVisibleZoom: 11.0 - readonly property real zoomStep: 2.0 - property var newCenter: defaultCenter - property real newZoom: 1.0 - property real newZoomOut: 1.0 - property var clickCoord: QtPositioning.coordinate(0, 0) - property bool isReady: false - - Component.onCompleted: isReady = true - onZoomLevelChanged: { - if (isReady) - mapHelper.calculateSmallCircleRadius(map.center) - } - - MapItemView { - id: mapItemView - model: mapHelper.model - delegate: MapQuickItem { - id: mapItem - anchorPoint.x: 0 - anchorPoint.y: mapItemImage.height - coordinate: model.coordinate - z: mapHelper.model.selectedUuid === model.uuid ? mapHelper.model.count - 1 : 0 - sourceItem: Image { - id: mapItemImage - source: "qrc:///mapwidget-marker" + (mapHelper.model.selectedUuid === model.uuid ? "-selected" : (mapHelper.editMode ? "-gray" : "")) - SequentialAnimation { - id: mapItemImageAnimation - PropertyAnimation { target: mapItemImage; property: "scale"; from: 1.0; to: 0.7; duration: 120 } - PropertyAnimation { target: mapItemImage; property: "scale"; from: 0.7; to: 1.0; duration: 80 } - } - MouseArea { - drag.target: (mapHelper.editMode && mapHelper.model.selectedUuid === model.uuid) ? mapItem : undefined - anchors.fill: parent - onClicked: { - if (!mapHelper.editMode) - mapHelper.model.setSelectedUuid(model.uuid, true) - } - onDoubleClicked: map.doubleClickHandler(mapItem.coordinate) - onReleased: { - if (mapHelper.editMode && mapHelper.model.selectedUuid === model.uuid) { - mapHelper.updateCurrentDiveSiteCoordinates(mapHelper.model.selectedUuid, mapItem.coordinate) - } - } - } - Item { - // Text with a duplicate for shadow. DropShadow as layer effect is kind of slow here. - y: mapItemImage.y + mapItemImage.height - visible: map.zoomLevel >= map.textVisibleZoom - Text { - id: mapItemTextShadow - x: mapItemText.x + 2; y: mapItemText.y + 2 - text: mapItemText.text - font.pointSize: mapItemText.font.pointSize - color: "black" - } - Text { - id: mapItemText - text: model.name - font.pointSize: 11.0 - color: mapHelper.model.selectedUuid === model.uuid ? "white" : "lightgrey" - } - } - } - } - } - - SequentialAnimation { - id: mapAnimationZoomIn - NumberAnimation { - target: map; property: "zoomLevel"; to: map.newZoomOut; duration: Math.abs(map.newZoomOut - map.zoomLevel) * 200 - } - ParallelAnimation { - CoordinateAnimation { target: map; property: "center"; to: map.newCenter; duration: 1000 } - NumberAnimation { - target: map; property: "zoomLevel"; to: map.newZoom ; duration: 2000; easing.type: Easing.InCubic - } - } - } - - ParallelAnimation { - id: mapAnimationZoomOut - NumberAnimation { target: map; property: "zoomLevel"; from: map.zoomLevel; to: map.newZoom; duration: 3000 } - SequentialAnimation { - PauseAnimation { duration: 2000 } - CoordinateAnimation { target: map; property: "center"; to: map.newCenter; duration: 2000 } - } - } - - ParallelAnimation { - id: mapAnimationClick - CoordinateAnimation { target: map; property: "center"; to: map.newCenter; duration: 500 } - NumberAnimation { target: map; property: "zoomLevel"; to: map.newZoom; duration: 500 } - } - - MouseArea { - anchors.fill: parent - onDoubleClicked: map.doubleClickHandler(map.toCoordinate(Qt.point(mouseX, mouseY))) - } - - function doubleClickHandler(coord) { - newCenter = coord - newZoom = zoomLevel + zoomStep - if (newZoom > maximumZoomLevel) - newZoom = maximumZoomLevel - mapAnimationClick.restart() - } - - function animateMapZoomOut() { - newCenter = defaultCenter - newZoom = defaultZoomOut - mapAnimationZoomIn.stop() - mapAnimationZoomOut.restart() - } - - function pointIsVisible(pt) { - return !isNaN(pt.x) - } - - function stopZoomAnimations() { - mapAnimationZoomIn.stop() - mapAnimationZoomOut.stop() - } - - function centerOnCoordinate(coord) { - stopZoomAnimations() - if (coord.latitude === 0.0 && coord.longitude === 0.0) { - // Do nothing - } else { - var newZoomOutFound = false - var zoomStored = zoomLevel - newZoomOut = zoomLevel - newCenter = coord - while (zoomLevel > minimumZoomLevel) { - var pt = fromCoordinate(coord) - if (pointIsVisible(pt)) { - newZoomOut = zoomLevel - newZoomOutFound = true - break - } - zoomLevel-- - } - if (!newZoomOutFound) - newZoomOut = defaultZoomOut - zoomLevel = zoomStored - newZoom = zoomStored - mapAnimationZoomIn.restart() - mapAnimationZoomOut.stop() - } - } - - function centerOnRectangle(topLeft, bottomRight, centerRect) { - stopZoomAnimations() - if (newCenter.latitude === 0.0 && newCenter.longitude === 0.0) { - // Do nothing - } else { - var centerStored = QtPositioning.coordinate(center.latitude, center.longitude) - var zoomStored = zoomLevel - var newZoomOutFound = false - newCenter = centerRect - // calculate zoom out - newZoomOut = zoomLevel - while (zoomLevel > minimumZoomLevel) { - var ptCenter = fromCoordinate(centerStored) - var ptCenterRect = fromCoordinate(centerRect) - if (pointIsVisible(ptCenter) && pointIsVisible(ptCenterRect)) { - newZoomOut = zoomLevel - newZoomOutFound = true - break - } - zoomLevel-- - } - if (!newZoomOutFound) - newZoomOut = defaultZoomOut - // calculate zoom in - center = newCenter - zoomLevel = maximumZoomLevel - var diagonalRect = topLeft.distanceTo(bottomRight) - while (zoomLevel > minimumZoomLevel) { - var c0 = toCoordinate(Qt.point(0.0, 0.0)) - var c1 = toCoordinate(Qt.point(width, height)) - if (c0.distanceTo(c1) > diagonalRect) { - newZoom = zoomLevel - 2.0 - break - } - zoomLevel-- - } - if (newZoom > defaultZoomIn) - newZoom = defaultZoomIn - zoomLevel = zoomStored - center = centerStored - mapAnimationZoomIn.restart() - mapAnimationZoomOut.stop() - } - } - - function deselectMapLocation() { - stopZoomAnimations() - } - } - - Rectangle { - id: editMessage - radius: padding - color: "#b08000" - border.color: "white" - x: (map.width - width) * 0.5; y: padding - width: editMessageText.width + padding * 2.0 - height: editMessageText.height + padding * 2.0 - visible: false - opacity: 0.0 - property int isVisible: -1 - property real padding: 10.0 - onOpacityChanged: visible = opacity != 0.0 - states: [ - State { when: editMessage.isVisible === 1; PropertyChanges { target: editMessage; opacity: 1.0 }}, - State { when: editMessage.isVisible === 0; PropertyChanges { target: editMessage; opacity: 0.0 }} - ] - transitions: Transition { NumberAnimation { properties: "opacity"; easing.type: Easing.InOutQuad }} - Text { - id: editMessageText - y: editMessage.padding; x: editMessage.padding - verticalAlignment: Text.AlignVCenter - color: "white" - font.pointSize: 11.0 - text: qsTr("Drag the selected dive location") - } - } - - Image { - id: toggleImage - x: 10; y: x - width: 40 - height: 40 - source: "qrc:///mapwidget-toggle-" + (map.activeMapType === map.mapType.SATELLITE ? "street" : "satellite") - SequentialAnimation { - id: toggleImageAnimation - PropertyAnimation { target: toggleImage; property: "scale"; from: 1.0; to: 0.8; duration: 120 } - PropertyAnimation { target: toggleImage; property: "scale"; from: 0.8; to: 1.0; duration: 80 } - } - MouseArea { - anchors.fill: parent - onClicked: { - map.activeMapType = map.activeMapType === map.mapType.SATELLITE ? map.mapType.STREET : map.mapType.SATELLITE - toggleImageAnimation.restart() - } - } - } - - Image { - id: imageZoomIn - x: 10 + (toggleImage.width - imageZoomIn.width) * 0.5; y: toggleImage.y + toggleImage.height + 10 - width: 20 - height: 20 - source: "qrc:///mapwidget-zoom-in" - SequentialAnimation { - id: imageZoomInAnimation - PropertyAnimation { target: imageZoomIn; property: "scale"; from: 1.0; to: 0.8; duration: 120 } - PropertyAnimation { target: imageZoomIn; property: "scale"; from: 0.8; to: 1.0; duration: 80 } - } - MouseArea { - anchors.fill: parent - onClicked: { - map.newCenter = map.center - map.newZoom = map.zoomLevel + map.zoomStep - if (map.newZoom > map.maximumZoomLevel) - map.newZoom = map.maximumZoomLevel - mapAnimationClick.restart() - imageZoomInAnimation.restart() - } - } - } - - Image { - id: imageZoomOut - x: imageZoomIn.x; y: imageZoomIn.y + imageZoomIn.height + 10 - source: "qrc:///mapwidget-zoom-out" - width: 20 - height: 20 - SequentialAnimation { - id: imageZoomOutAnimation - PropertyAnimation { target: imageZoomOut; property: "scale"; from: 1.0; to: 0.8; duration: 120 } - PropertyAnimation { target: imageZoomOut; property: "scale"; from: 0.8; to: 1.0; duration: 80 } - } - MouseArea { - anchors.fill: parent - onClicked: { - map.newCenter = map.center - map.newZoom = map.zoomLevel - map.zoomStep - mapAnimationClick.restart() - imageZoomOutAnimation.restart() - } - } - } - - function openLocationInGoogleMaps(latitude, longitude) { - var loc = latitude + " " + longitude - var url = "https://www.google.com/maps/place/" + loc + "/@" + loc + ",5000m/data=!3m1!1e3!4m2!3m1!1s0x0:0x0" - Qt.openUrlExternally(url) - } - - MapWidgetContextMenu { - id: contextMenu - y: 10; x: map.width - y - onActionSelected: { - switch (action) { - case contextMenu.actions.OPEN_LOCATION_IN_GOOGLE_MAPS: - openLocationInGoogleMaps(map.center.latitude, map.center.longitude) - break - case contextMenu.actions.COPY_LOCATION_DECIMAL: - mapHelper.copyToClipboardCoordinates(map.center, false) - break - case contextMenu.actions.COPY_LOCATION_SEXAGESIMAL: - mapHelper.copyToClipboardCoordinates(map.center, true) - break - case contextMenu.actions.SELECT_VISIBLE_LOCATIONS: - mapHelper.selectVisibleLocations() - break - } - } - } -} diff --git a/mobile-widgets/qml/MapWidgetContextMenu.qml b/mobile-widgets/qml/MapWidgetContextMenu.qml deleted file mode 100644 index 17450a729..000000000 --- a/mobile-widgets/qml/MapWidgetContextMenu.qml +++ /dev/null @@ -1,124 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0 -import QtQuick 2.0 - -Item { - id: container - signal actionSelected(int action) - - readonly property var actions: { - "OPEN_LOCATION_IN_GOOGLE_MAPS": 0, - "COPY_LOCATION_DECIMAL": 1, - "COPY_LOCATION_SEXAGESIMAL": 2, - "SELECT_VISIBLE_LOCATIONS": 3 - } - readonly property var menuItemData: [ - { idx: actions.OPEN_LOCATION_IN_GOOGLE_MAPS, itemText: qsTr("Open location in Google Maps") }, - { idx: actions.COPY_LOCATION_DECIMAL, itemText: qsTr("Copy location to clipboard (decimal)") }, - { idx: actions.COPY_LOCATION_SEXAGESIMAL, itemText: qsTr("Copy location to clipboard (sexagesimal)") }, - { idx: actions.SELECT_VISIBLE_LOCATIONS, itemText: qsTr("Select visible dive locations") } - ] - readonly property real itemTextPadding: 10.0 - readonly property real itemHeight: 34.0 - readonly property int itemAnimationDuration: 100 - readonly property color colorItemBackground: "#dedede" - readonly property color colorItemBackgroundSelected: "grey" - readonly property color colorItemText: "black" - readonly property color colorItemTextSelected: "#dedede" - readonly property color colorItemBorder: "black" - property int listViewIsVisible: -1 - property real maxItemWidth: 0.0 - - Image { - id: contextMenuImage - x: -width - source: "qrc:///mapwidget-context-menu" - - SequentialAnimation { - id:contextMenuImageAnimation - PropertyAnimation { target: contextMenuImage; property: "scale"; from: 1.0; to: 0.8; duration: 80 } - PropertyAnimation { target: contextMenuImage; property: "scale"; from: 0.8; to: 1.0; duration: 60 } - } - - MouseArea { - anchors.fill: parent - onClicked: { - contextMenuImageAnimation.restart() - listViewIsVisible = (listViewIsVisible !== 1) ? 1 : 0 - } - } - } - - ListModel { - id: listModel - property int selectedIdx: -1 - Component.onCompleted: { - for (var i = 0; i < menuItemData.length; i++) - append(menuItemData[i]); - } - } - - Component { - id: listItemDelegate - Rectangle { - color: model.idx === listModel.selectedIdx ? colorItemBackgroundSelected : colorItemBackground - width: maxItemWidth - height: itemHeight - border.color: colorItemBorder - Text { - x: itemTextPadding - height: itemHeight - verticalAlignment: Text.AlignVCenter - text: model.itemText - font.pointSize: 10.0 - color: model.idx === listModel.selectedIdx ? colorItemTextSelected : colorItemText - onWidthChanged: { - if (width + itemTextPadding * 2.0 > maxItemWidth) - maxItemWidth = width + itemTextPadding * 2.0 - } - Behavior on color { ColorAnimation { duration: itemAnimationDuration }} - } - Behavior on color { ColorAnimation { duration: itemAnimationDuration }} - } - } - - ListView { - id: listView - y: contextMenuImage.y + contextMenuImage.height + 10; - width: maxItemWidth; - height: listModel.count * itemHeight - visible: false - opacity: 0.0 - interactive: false - model: listModel - delegate: listItemDelegate - - onCountChanged: x = -maxItemWidth - onVisibleChanged: listModel.selectedIdx = -1 - onOpacityChanged: visible = opacity != 0.0 - - Timer { - id: timerListViewVisible - running: false - repeat: false - interval: itemAnimationDuration + 50 - onTriggered: listViewIsVisible = 0 - } - - MouseArea { - anchors.fill: parent - onClicked: { - if (opacity < 1.0) - return; - var idx = listView.indexAt(mouseX, mouseY) - listModel.selectedIdx = idx - container.actionSelected(idx) - timerListViewVisible.restart() - } - } - states: [ - State { when: listViewIsVisible === 1; PropertyChanges { target: listView; opacity: 1.0 }}, - State { when: listViewIsVisible === 0; PropertyChanges { target: listView; opacity: 0.0 }} - ] - transitions: Transition { NumberAnimation { properties: "opacity"; easing.type: Easing.InOutQuad }} - } -} diff --git a/mobile-widgets/qml/MapWidgetError.qml b/mobile-widgets/qml/MapWidgetError.qml deleted file mode 100644 index 346e95f07..000000000 --- a/mobile-widgets/qml/MapWidgetError.qml +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0 -import QtQuick 2.0 - -Item { - Text { - anchors.fill: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: "red" - text: qsTr("MapWidget.qml failed to load! -The QML modules QtPositioning and QtLocation could be missing!") - } -} diff --git a/mobile-widgets/qml/icons/mapwidget-context-menu.png b/mobile-widgets/qml/icons/mapwidget-context-menu.png deleted file mode 100644 index 6ab7cf77d..000000000 Binary files a/mobile-widgets/qml/icons/mapwidget-context-menu.png and /dev/null differ diff --git a/mobile-widgets/qml/icons/mapwidget-marker-gray.png b/mobile-widgets/qml/icons/mapwidget-marker-gray.png deleted file mode 100644 index 856db9f5b..000000000 Binary files a/mobile-widgets/qml/icons/mapwidget-marker-gray.png and /dev/null differ diff --git a/mobile-widgets/qml/icons/mapwidget-marker-selected.png b/mobile-widgets/qml/icons/mapwidget-marker-selected.png deleted file mode 100644 index 57f4efa27..000000000 Binary files a/mobile-widgets/qml/icons/mapwidget-marker-selected.png and /dev/null differ diff --git a/mobile-widgets/qml/icons/mapwidget-marker.png b/mobile-widgets/qml/icons/mapwidget-marker.png deleted file mode 100644 index a1be73866..000000000 Binary files a/mobile-widgets/qml/icons/mapwidget-marker.png and /dev/null differ diff --git a/mobile-widgets/qml/icons/mapwidget-toggle-satellite.png b/mobile-widgets/qml/icons/mapwidget-toggle-satellite.png deleted file mode 100644 index 7ee536929..000000000 Binary files a/mobile-widgets/qml/icons/mapwidget-toggle-satellite.png and /dev/null differ diff --git a/mobile-widgets/qml/icons/mapwidget-toggle-street.png b/mobile-widgets/qml/icons/mapwidget-toggle-street.png deleted file mode 100644 index 04a668c3f..000000000 Binary files a/mobile-widgets/qml/icons/mapwidget-toggle-street.png and /dev/null differ diff --git a/mobile-widgets/qml/icons/mapwidget-zoom-in.png b/mobile-widgets/qml/icons/mapwidget-zoom-in.png deleted file mode 100644 index 8c2521c3e..000000000 Binary files a/mobile-widgets/qml/icons/mapwidget-zoom-in.png and /dev/null differ diff --git a/mobile-widgets/qml/icons/mapwidget-zoom-out.png b/mobile-widgets/qml/icons/mapwidget-zoom-out.png deleted file mode 100644 index bd372f17d..000000000 Binary files a/mobile-widgets/qml/icons/mapwidget-zoom-out.png and /dev/null differ diff --git a/mobile-widgets/qmlmapwidgethelper.cpp b/mobile-widgets/qmlmapwidgethelper.cpp deleted file mode 100644 index ba197f63f..000000000 --- a/mobile-widgets/qmlmapwidgethelper.cpp +++ /dev/null @@ -1,264 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0 -#include -#include -#include -#include -#include - -#include "qmlmapwidgethelper.h" -#include "core/dive.h" -#include "core/divesite.h" -#include "core/helpers.h" -#include "qt-models/maplocationmodel.h" - -#define MIN_DISTANCE_BETWEEN_DIVE_SITES_M 50.0 -#define SMALL_CIRCLE_RADIUS_PX 26.0 - -MapWidgetHelper::MapWidgetHelper(QObject *parent) : QObject(parent) -{ - m_mapLocationModel = new MapLocationModel(this); - connect(m_mapLocationModel, SIGNAL(selectedLocationChanged(MapLocation *)), - this, SLOT(selectedLocationChanged(MapLocation *))); -} - -void MapWidgetHelper::centerOnDiveSite(struct dive_site *ds) -{ - int idx; - struct dive *dive; - QVector selDS; - QVector selGC; - QGeoCoordinate dsCoord; - - for_each_dive (idx, dive) { - struct dive_site *dss = get_dive_site_for_dive(dive); - if (!dive_site_has_gps_location(dss) || !dive->selected) - continue; - // only store dive sites with GPS - selDS.append(dss); - selGC.append(QGeoCoordinate(dss->latitude.udeg * 0.000001, - dss->longitude.udeg * 0.000001)); - } - if (!dive_site_has_gps_location(ds) && !selDS.size()) { - // only a single dive site with no GPS selected - m_mapLocationModel->setSelectedUuid(ds ? ds->uuid : 0, false); - QMetaObject::invokeMethod(m_map, "deselectMapLocation"); - - } else if (selDS.size() == 1) { - // a single dive site with GPS selected - ds = selDS.at(0); - m_mapLocationModel->setSelectedUuid(ds->uuid, false); - dsCoord.setLatitude(ds->latitude.udeg * 0.000001); - dsCoord.setLongitude(ds->longitude.udeg * 0.000001); - QMetaObject::invokeMethod(m_map, "centerOnCoordinate", Q_ARG(QVariant, QVariant::fromValue(dsCoord))); - } else if (selDS.size() > 1) { - /* more than one dive sites with GPS selected. - * find the most top-left and bottom-right dive sites on the map coordinate system. */ - ds = selDS.at(0); - m_mapLocationModel->setSelectedUuid(ds->uuid, false); - qreal minLat = 0.0, minLon = 0.0, maxLat = 0.0, maxLon = 0.0; - bool start = true; - foreach(QGeoCoordinate gc, selGC) { - qreal lat = gc.latitude(); - qreal lon = gc.longitude(); - if (start) { - minLat = maxLat = lat; - minLon = maxLon = lon; - start = false; - continue; - } - if (lat < minLat) - minLat = lat; - else if (lat > maxLat) - maxLat = lat; - if (lon < minLon) - minLon = lon; - else if (lon > maxLon) - maxLon = lon; - } - // pass rectangle coordinates to QML - QGeoCoordinate coordTopLeft(minLat, minLon); - QGeoCoordinate coordBottomRight(maxLat, maxLon); - QGeoCoordinate coordCenter(minLat + (maxLat - minLat) * 0.5, minLon + (maxLon - minLon) * 0.5); - QMetaObject::invokeMethod(m_map, "centerOnRectangle", - Q_ARG(QVariant, QVariant::fromValue(coordTopLeft)), - Q_ARG(QVariant, QVariant::fromValue(coordBottomRight)), - Q_ARG(QVariant, QVariant::fromValue(coordCenter))); - } -} - -void MapWidgetHelper::reloadMapLocations() -{ - struct dive_site *ds; - int idx; - QMap locationNameMap; - m_mapLocationModel->clear(); - MapLocation *location; - QVector locationList; - qreal latitude, longitude; - - if (displayed_dive_site.uuid && dive_site_has_gps_location(&displayed_dive_site)) { - latitude = displayed_dive_site.latitude.udeg * 0.000001; - longitude = displayed_dive_site.longitude.udeg * 0.000001; - location = new MapLocation(displayed_dive_site.uuid, QGeoCoordinate(latitude, longitude), - QString(displayed_dive_site.name)); - locationList.append(location); - locationNameMap[QString(displayed_dive_site.name)] = location; - } - for_each_dive_site(idx, ds) { - if (!dive_site_has_gps_location(ds) || ds->uuid == displayed_dive_site.uuid) - continue; - latitude = ds->latitude.udeg * 0.000001; - longitude = ds->longitude.udeg * 0.000001; - QGeoCoordinate dsCoord(latitude, longitude); - QString name(ds->name); - // don't add dive locations with the same name, unless they are - // at least MIN_DISTANCE_BETWEEN_DIVE_SITES_M apart - if (locationNameMap.contains(name)) { - MapLocation *existingLocation = locationNameMap[name]; - QGeoCoordinate coord = qvariant_cast(existingLocation->getRole(MapLocation::Roles::RoleCoordinate)); - if (dsCoord.distanceTo(coord) < MIN_DISTANCE_BETWEEN_DIVE_SITES_M) - continue; - } - location = new MapLocation(ds->uuid, dsCoord, name); - locationList.append(location); - locationNameMap[name] = location; - } - m_mapLocationModel->addList(locationList); -} - -void MapWidgetHelper::selectedLocationChanged(MapLocation *location) -{ - int idx; - struct dive *dive; - m_selectedDiveIds.clear(); - QGeoCoordinate locationCoord = location->coordinate(); - for_each_dive (idx, dive) { - struct dive_site *ds = get_dive_site_for_dive(dive); - if (!dive_site_has_gps_location(ds)) - continue; - const qreal latitude = ds->latitude.udeg * 0.000001; - const qreal longitude = ds->longitude.udeg * 0.000001; - QGeoCoordinate dsCoord(latitude, longitude); - if (locationCoord.distanceTo(dsCoord) < m_smallCircleRadius) - m_selectedDiveIds.append(idx); - } - emit selectedDivesChanged(m_selectedDiveIds); -} - -void MapWidgetHelper::selectVisibleLocations() -{ - int idx; - struct dive *dive; - bool selectedFirst = false; - m_selectedDiveIds.clear(); - for_each_dive (idx, dive) { - struct dive_site *ds = get_dive_site_for_dive(dive); - if (!dive_site_has_gps_location(ds)) - continue; - const qreal latitude = ds->latitude.udeg * 0.000001; - const qreal longitude = ds->longitude.udeg * 0.000001; - QGeoCoordinate dsCoord(latitude, longitude); - QPointF point; - QMetaObject::invokeMethod(m_map, "fromCoordinate", Q_RETURN_ARG(QPointF, point), - Q_ARG(QGeoCoordinate, dsCoord)); - if (!qIsNaN(point.x())) { - if (!selectedFirst) { - m_mapLocationModel->setSelectedUuid(ds->uuid, false); - selectedFirst = true; - } - m_selectedDiveIds.append(idx); - } - } - emit selectedDivesChanged(m_selectedDiveIds); -} - -/* - * Based on a 2D Map widget circle with center "coord" and radius SMALL_CIRCLE_RADIUS_PX, - * obtain a "small circle" with radius m_smallCircleRadius in meters: - * https://en.wikipedia.org/wiki/Circle_of_a_sphere - * - * The idea behind this circle is to be able to select multiple nearby dives, when clicking on - * the map. This code can be in QML, but it is in C++ instead for performance reasons. - * - * This can be made faster with an exponential regression [a * exp(b * x)], with a pretty - * decent R-squared, but it becomes bound to map provider zoom level mappings and the - * SMALL_CIRCLE_RADIUS_PX value, which makes the code hard to maintain. - */ -void MapWidgetHelper::calculateSmallCircleRadius(QGeoCoordinate coord) -{ - QPointF point; - QMetaObject::invokeMethod(m_map, "fromCoordinate", Q_RETURN_ARG(QPointF, point), - Q_ARG(QGeoCoordinate, coord)); - QPointF point2(point.x() + SMALL_CIRCLE_RADIUS_PX, point.y()); - QGeoCoordinate coord2; - QMetaObject::invokeMethod(m_map, "toCoordinate", Q_RETURN_ARG(QGeoCoordinate, coord2), - Q_ARG(QPointF, point2)); - m_smallCircleRadius = coord2.distanceTo(coord); -} - -void MapWidgetHelper::copyToClipboardCoordinates(QGeoCoordinate coord, bool formatTraditional) -{ - bool savep = prefs.coordinates_traditional; - prefs.coordinates_traditional = formatTraditional; - - const int lat = lrint(1000000.0 * coord.latitude()); - const int lon = lrint(1000000.0 * coord.longitude()); - const char *coordinates = printGPSCoords(lat, lon); - QApplication::clipboard()->setText(QString(coordinates), QClipboard::Clipboard); - - free((void *)coordinates); - prefs.coordinates_traditional = savep; -} - -void MapWidgetHelper::updateCurrentDiveSiteCoordinates(quint32 uuid, QGeoCoordinate coord) -{ - MapLocation *loc = m_mapLocationModel->getMapLocationForUuid(uuid); - if (loc) - loc->setCoordinate(coord); - displayed_dive_site.latitude.udeg = lrint(coord.latitude() * 1000000.0); - displayed_dive_site.longitude.udeg = lrint(coord.longitude() * 1000000.0); - emit coordinatesChanged(); -} - -bool MapWidgetHelper::editMode() -{ - return m_editMode; -} - -void MapWidgetHelper::setEditMode(bool editMode) -{ - m_editMode = editMode; - MapLocation *exists = m_mapLocationModel->getMapLocationForUuid(displayed_dive_site.uuid); - // if divesite uuid doesn't exist in the model, add a new MapLocation. - if (editMode && !exists) { - QGeoCoordinate coord(0.0, 0.0); - m_mapLocationModel->add(new MapLocation(displayed_dive_site.uuid, coord, - QString(displayed_dive_site.name))); - QMetaObject::invokeMethod(m_map, "centerOnCoordinate", - Q_ARG(QVariant, QVariant::fromValue(coord))); - } - emit editModeChanged(); -} - -QString MapWidgetHelper::pluginObject() -{ - QString str; - str += "import QtQuick 2.0;"; - str += "import QtLocation 5.3;"; - str += "Plugin {"; - str += " id: mapPlugin;"; - str += " name: 'googlemaps';"; - str += " PluginParameter { name: 'googlemaps.maps.language'; value: '%lang%' }"; - str += " PluginParameter { name: 'googlemaps.cachefolder'; value: '%cacheFolder%' }"; - str += " Component.onCompleted: {"; - str += " if (availableServiceProviders.indexOf(name) === -1) {"; - str += " console.warn('MapWidget.qml: cannot find a plugin named: ' + name);"; - str += " }"; - str += " }"; - str += "}"; - QString lang = uiLanguage(NULL).replace('_', '-'); - str.replace("%lang%", lang); - QString cacheFolder = QString(system_default_directory()).append("/googlemaps"); - str.replace("%cacheFolder%", cacheFolder.replace("\\", "/")); - return str; -} diff --git a/mobile-widgets/qmlmapwidgethelper.h b/mobile-widgets/qmlmapwidgethelper.h deleted file mode 100644 index 36d25d178..000000000 --- a/mobile-widgets/qmlmapwidgethelper.h +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0 -#ifndef QMLMAPWIDGETHELPER_H -#define QMLMAPWIDGETHELPER_H - -#include - -class QGeoCoordinate; -class MapLocationModel; -class MapLocation; -struct dive_site; - -class MapWidgetHelper : public QObject { - - Q_OBJECT - Q_PROPERTY(QObject *map MEMBER m_map) - Q_PROPERTY(MapLocationModel *model MEMBER m_mapLocationModel NOTIFY modelChanged) - Q_PROPERTY(bool editMode READ editMode WRITE setEditMode NOTIFY editModeChanged) - Q_PROPERTY(QString pluginObject READ pluginObject NOTIFY pluginObjectChanged) - -public: - explicit MapWidgetHelper(QObject *parent = NULL); - - void centerOnDiveSite(struct dive_site *); - void reloadMapLocations(); - Q_INVOKABLE void copyToClipboardCoordinates(QGeoCoordinate coord, bool formatTraditional); - Q_INVOKABLE void calculateSmallCircleRadius(QGeoCoordinate coord); - Q_INVOKABLE void updateCurrentDiveSiteCoordinates(quint32 uuid, QGeoCoordinate coord); - Q_INVOKABLE void selectVisibleLocations(); - bool editMode(); - void setEditMode(bool editMode); - QString pluginObject(); - -private: - QObject *m_map; - MapLocationModel *m_mapLocationModel; - qreal m_smallCircleRadius; - QList m_selectedDiveIds; - bool m_editMode; - -private slots: - void selectedLocationChanged(MapLocation *); - -signals: - void modelChanged(); - void editModeChanged(); - void selectedDivesChanged(QList list); - void coordinatesChanged(); - void pluginObjectChanged(); -}; - -extern "C" const char *printGPSCoords(int lat, int lon); - -#endif diff --git a/subsurface.qrc b/subsurface.qrc index 6bfb7785d..83b93cc29 100644 --- a/subsurface.qrc +++ b/subsurface.qrc @@ -1,16 +1,16 @@ - mobile-widgets/qml/MapWidget.qml - mobile-widgets/qml/MapWidgetError.qml - mobile-widgets/qml/MapWidgetContextMenu.qml - mobile-widgets/qml/icons/mapwidget-marker.png - mobile-widgets/qml/icons/mapwidget-marker-gray.png - mobile-widgets/qml/icons/mapwidget-marker-selected.png - mobile-widgets/qml/icons/mapwidget-toggle-satellite.png - mobile-widgets/qml/icons/mapwidget-toggle-street.png - mobile-widgets/qml/icons/mapwidget-context-menu.png - mobile-widgets/qml/icons/mapwidget-zoom-in.png - mobile-widgets/qml/icons/mapwidget-zoom-out.png + map-widget/qml/MapWidget.qml + map-widget/qml/MapWidgetError.qml + map-widget/qml/MapWidgetContextMenu.qml + map-widget/qml/icons/mapwidget-marker.png + map-widget/qml/icons/mapwidget-marker-gray.png + map-widget/qml/icons/mapwidget-marker-selected.png + map-widget/qml/icons/mapwidget-toggle-satellite.png + map-widget/qml/icons/mapwidget-toggle-street.png + map-widget/qml/icons/mapwidget-context-menu.png + map-widget/qml/icons/mapwidget-zoom-in.png + map-widget/qml/icons/mapwidget-zoom-out.png icons/satellite.svg icons/star.svg icons/subsurface-icon.png -- cgit v1.2.3-70-g09d2