diff options
author | Lubomir I. Ivanov <neolit123@gmail.com> | 2017-11-04 21:23:37 +0200 |
---|---|---|
committer | Dirk Hohndel <dirk@hohndel.org> | 2017-11-05 14:48:56 -0800 |
commit | 956b45ddfda060fcd818659ee05618ed2e4bfcab (patch) | |
tree | 1acb6ff91cba57e059e7eac8868ecd066539b230 /map-widget | |
parent | 6ce4239884780fafdf641fa52a2a8c7a0a02450d (diff) | |
download | subsurface-956b45ddfda060fcd818659ee05618ed2e4bfcab.tar.gz |
map-widget: move the widget and its resources to 'map-widget'
Move all the map widget platform agnostic files to the
<subsurface-root>/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 <neolit123@gmail.com>
Diffstat (limited to 'map-widget')
-rw-r--r-- | map-widget/qml/MapWidget.qml | 355 | ||||
-rw-r--r-- | map-widget/qml/MapWidgetContextMenu.qml | 124 | ||||
-rw-r--r-- | map-widget/qml/MapWidgetError.qml | 13 | ||||
-rw-r--r-- | map-widget/qml/icons/mapwidget-context-menu.png | bin | 0 -> 242 bytes | |||
-rw-r--r-- | map-widget/qml/icons/mapwidget-marker-gray.png | bin | 0 -> 2033 bytes | |||
-rw-r--r-- | map-widget/qml/icons/mapwidget-marker-selected.png | bin | 0 -> 1995 bytes | |||
-rw-r--r-- | map-widget/qml/icons/mapwidget-marker.png | bin | 0 -> 1801 bytes | |||
-rw-r--r-- | map-widget/qml/icons/mapwidget-toggle-satellite.png | bin | 0 -> 6288 bytes | |||
-rw-r--r-- | map-widget/qml/icons/mapwidget-toggle-street.png | bin | 0 -> 5916 bytes | |||
-rw-r--r-- | map-widget/qml/icons/mapwidget-zoom-in.png | bin | 0 -> 256 bytes | |||
-rw-r--r-- | map-widget/qml/icons/mapwidget-zoom-out.png | bin | 0 -> 242 bytes | |||
-rw-r--r-- | map-widget/qmlmapwidgethelper.cpp | 264 | ||||
-rw-r--r-- | map-widget/qmlmapwidgethelper.h | 53 |
13 files changed, 809 insertions, 0 deletions
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 Binary files differnew file mode 100644 index 000000000..6ab7cf77d --- /dev/null +++ b/map-widget/qml/icons/mapwidget-context-menu.png diff --git a/map-widget/qml/icons/mapwidget-marker-gray.png b/map-widget/qml/icons/mapwidget-marker-gray.png Binary files differnew file mode 100644 index 000000000..856db9f5b --- /dev/null +++ b/map-widget/qml/icons/mapwidget-marker-gray.png diff --git a/map-widget/qml/icons/mapwidget-marker-selected.png b/map-widget/qml/icons/mapwidget-marker-selected.png Binary files differnew file mode 100644 index 000000000..57f4efa27 --- /dev/null +++ b/map-widget/qml/icons/mapwidget-marker-selected.png diff --git a/map-widget/qml/icons/mapwidget-marker.png b/map-widget/qml/icons/mapwidget-marker.png Binary files differnew file mode 100644 index 000000000..a1be73866 --- /dev/null +++ b/map-widget/qml/icons/mapwidget-marker.png diff --git a/map-widget/qml/icons/mapwidget-toggle-satellite.png b/map-widget/qml/icons/mapwidget-toggle-satellite.png Binary files differnew file mode 100644 index 000000000..7ee536929 --- /dev/null +++ b/map-widget/qml/icons/mapwidget-toggle-satellite.png diff --git a/map-widget/qml/icons/mapwidget-toggle-street.png b/map-widget/qml/icons/mapwidget-toggle-street.png Binary files differnew file mode 100644 index 000000000..04a668c3f --- /dev/null +++ b/map-widget/qml/icons/mapwidget-toggle-street.png diff --git a/map-widget/qml/icons/mapwidget-zoom-in.png b/map-widget/qml/icons/mapwidget-zoom-in.png Binary files differnew file mode 100644 index 000000000..8c2521c3e --- /dev/null +++ b/map-widget/qml/icons/mapwidget-zoom-in.png diff --git a/map-widget/qml/icons/mapwidget-zoom-out.png b/map-widget/qml/icons/mapwidget-zoom-out.png Binary files differnew file mode 100644 index 000000000..bd372f17d --- /dev/null +++ b/map-widget/qml/icons/mapwidget-zoom-out.png 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 <QApplication> +#include <QClipboard> +#include <QGeoCoordinate> +#include <QDebug> +#include <QVector> + +#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<struct dive_site *> selDS; + QVector<QGeoCoordinate> 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<QString, MapLocation *> locationNameMap; + m_mapLocationModel->clear(); + MapLocation *location; + QVector<MapLocation *> 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<QGeoCoordinate>(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 <QObject> + +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<int> m_selectedDiveIds; + bool m_editMode; + +private slots: + void selectedLocationChanged(MapLocation *); + +signals: + void modelChanged(); + void editModeChanged(); + void selectedDivesChanged(QList<int> list); + void coordinatesChanged(); + void pluginObjectChanged(); +}; + +extern "C" const char *printGPSCoords(int lat, int lon); + +#endif |