mirror of
https://github.com/subsurface/subsurface.git
synced 2024-11-30 14:10:19 +00:00
f21ae69f7b
The QML Map has a couple of methods - toCoordinate(someQPointF) and fromCoordinate(someQGeoCoordinate). Ideally, if one passes a point to toCoordinate() and then attempts to convert the resulted coordinates back to the point, the same point in the Map view port with minimal error should be retrieved. That's not always the case - e.g. near 47.400200 -123.142066, which means that the methods are not exactly *reliable* and there might be Map class bugs at hand. The new zoom-in and zoom-out improvements try to work around the above: - for both centerOnRectangle() and centerOnCoordinate(), if no good zoom-out level is found (newZoomOut), the zoom-out is set to the default value of defaultZoomOut. In practice, this prevents the case where the map does not zoom-out at all when going from one place to another - centerOnRectangle() now uses rectangle diagonals to estimate a fit, instead of checking if 2 points (top-left and bottom-right) are visible in the viewport. The usage of fromCoordinate() was giving bad results in this case and comparing distances (diagonals) is more reliable but more expensive on the CPU. Due to the inconsistencies of toCoordinate() and fromCoordinate() the dynamic zoom-in and zoom-out are still not ideal, but the current implementation is somewhat usable with decent accuracy. Signed-off-by: Lubomir I. Ivanov <neolit123@gmail.com>
359 lines
11 KiB
QML
359 lines
11 KiB
QML
// 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 {
|
|
property int nSelectedDives: 0
|
|
|
|
Plugin {
|
|
id: mapPlugin
|
|
name: "googlemaps"
|
|
Component.onCompleted: {
|
|
if (availableServiceProviders.indexOf(name) === -1)
|
|
console.warn("MapWidget.qml: cannot find a plugin with the name '" + name + "'")
|
|
}
|
|
}
|
|
|
|
MapWidgetHelper {
|
|
id: mapHelper
|
|
map: map
|
|
editMode: false
|
|
onSelectedDivesChanged: nSelectedDives = list.length
|
|
onEditModeChanged: editMessage.isVisible = editMode === true ? 1 : 0
|
|
onCoordinatesChanged: {}
|
|
}
|
|
|
|
Map {
|
|
id: map
|
|
anchors.fill: parent
|
|
plugin: mapPlugin
|
|
zoomLevel: 1
|
|
|
|
readonly property var mapType: { "STREET": supportedMapTypes[0], "SATELLITE": supportedMapTypes[1] }
|
|
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)
|
|
|
|
Component.onCompleted: activeMapType = mapType.SATELLITE
|
|
onZoomLevelChanged: 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: Item {
|
|
Image {
|
|
id: mapItemImage
|
|
source: "qrc:///mapwidget-marker" + (mapHelper.model.selectedUuid === model.uuid ? "-selected" : "")
|
|
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()
|
|
newCenter = coord
|
|
if (coord.latitude === 0.0 && coord.longitude === 0.0) {
|
|
newZoom = 2.6
|
|
newZoomOut = newZoom
|
|
} else {
|
|
var newZoomOutFound = false
|
|
var zoomStored = zoomLevel
|
|
newZoomOut = zoomLevel
|
|
while (zoomLevel > minimumZoomLevel) {
|
|
var pt = fromCoordinate(coord)
|
|
if (pointIsVisible(pt)) {
|
|
newZoomOut = zoomLevel
|
|
newZoomOutFound = true
|
|
break
|
|
}
|
|
zoomLevel--
|
|
}
|
|
if (!newZoomOutFound)
|
|
newZoomOut = defaultZoomOut
|
|
zoomLevel = zoomStored
|
|
newZoom = defaultZoomIn
|
|
}
|
|
mapAnimationZoomIn.restart()
|
|
mapAnimationZoomOut.stop()
|
|
}
|
|
|
|
function centerOnRectangle(topLeft, bottomRight, centerRect) {
|
|
stopZoomAnimations()
|
|
newCenter = centerRect
|
|
if (newCenter.latitude === 0.0 && newCenter.longitude === 0.0) {
|
|
newZoom = 2.6
|
|
newZoomOut = newZoom
|
|
} else {
|
|
var centerStored = QtPositioning.coordinate(center.latitude, center.longitude)
|
|
var zoomStored = zoomLevel
|
|
var newZoomOutFound = false
|
|
// 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() {
|
|
animateMapZoomOut()
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|