2017-07-29 05:01:33 +00:00
|
|
|
// SPDX-License-Identifier: GPL-2.0
|
|
|
|
#include <QApplication>
|
|
|
|
#include <QClipboard>
|
|
|
|
#include <QDebug>
|
2017-08-06 23:04:23 +00:00
|
|
|
#include <QVector>
|
2017-07-29 05:01:33 +00:00
|
|
|
|
|
|
|
#include "qmlmapwidgethelper.h"
|
2024-06-07 08:25:09 +00:00
|
|
|
#include "core/divefilter.h"
|
|
|
|
#include "core/divelist.h"
|
|
|
|
#include "core/divelog.h"
|
2017-07-29 05:01:33 +00:00
|
|
|
#include "core/divesite.h"
|
2018-06-03 20:15:19 +00:00
|
|
|
#include "core/qthelper.h"
|
2024-06-07 08:25:09 +00:00
|
|
|
#include "core/range.h"
|
2017-07-29 05:01:33 +00:00
|
|
|
#include "qt-models/maplocationmodel.h"
|
2019-05-09 19:33:01 +00:00
|
|
|
#include "qt-models/divelocationmodel.h"
|
2019-05-03 21:16:40 +00:00
|
|
|
#ifndef SUBSURFACE_MOBILE
|
2019-08-30 13:25:59 +00:00
|
|
|
#include "desktop-widgets/mapwidget.h"
|
2019-05-03 21:16:40 +00:00
|
|
|
#endif
|
2017-07-29 05:01:33 +00:00
|
|
|
|
|
|
|
#define SMALL_CIRCLE_RADIUS_PX 26.0
|
|
|
|
|
|
|
|
MapWidgetHelper::MapWidgetHelper(QObject *parent) : QObject(parent)
|
|
|
|
{
|
|
|
|
m_mapLocationModel = new MapLocationModel(this);
|
2017-12-28 11:20:34 +00:00
|
|
|
m_smallCircleRadius = SMALL_CIRCLE_RADIUS_PX;
|
|
|
|
m_map = nullptr;
|
|
|
|
m_editMode = false;
|
2019-05-09 19:33:01 +00:00
|
|
|
connect(&diveListNotifier, &DiveListNotifier::diveSiteChanged, this, &MapWidgetHelper::diveSiteChanged);
|
2017-07-29 05:01:33 +00:00
|
|
|
}
|
|
|
|
|
2018-10-28 21:04:56 +00:00
|
|
|
QGeoCoordinate MapWidgetHelper::getCoordinates(struct dive_site *ds)
|
2018-04-02 19:48:23 +00:00
|
|
|
{
|
2024-06-30 15:38:36 +00:00
|
|
|
if (!ds || !ds->has_gps_location())
|
2018-04-02 19:48:23 +00:00
|
|
|
return QGeoCoordinate(0.0, 0.0);
|
2018-10-20 18:12:15 +00:00
|
|
|
return QGeoCoordinate(ds->location.lat.udeg * 0.000001, ds->location.lon.udeg * 0.000001);
|
2018-04-02 19:48:23 +00:00
|
|
|
}
|
|
|
|
|
2017-07-29 05:01:33 +00:00
|
|
|
void MapWidgetHelper::centerOnDiveSite(struct dive_site *ds)
|
2018-10-08 19:16:40 +00:00
|
|
|
{
|
2024-06-30 15:38:36 +00:00
|
|
|
if (!ds || !ds->has_gps_location()) {
|
2018-10-08 19:16:40 +00:00
|
|
|
// dive site with no GPS
|
2019-08-30 10:38:25 +00:00
|
|
|
m_mapLocationModel->setSelected(ds);
|
2018-10-08 19:16:40 +00:00
|
|
|
QMetaObject::invokeMethod(m_map, "deselectMapLocation");
|
|
|
|
} else {
|
|
|
|
// dive site with GPS
|
2019-08-30 10:38:25 +00:00
|
|
|
m_mapLocationModel->setSelected(ds);
|
2018-10-20 18:12:15 +00:00
|
|
|
QGeoCoordinate dsCoord (ds->location.lat.udeg * 0.000001, ds->location.lon.udeg * 0.000001);
|
2018-10-08 19:16:40 +00:00
|
|
|
QMetaObject::invokeMethod(m_map, "centerOnCoordinate", Q_ARG(QVariant, QVariant::fromValue(dsCoord)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-11 11:21:53 +00:00
|
|
|
void MapWidgetHelper::setSelected(const std::vector<dive_site *> divesites)
|
2019-08-30 15:38:54 +00:00
|
|
|
{
|
2024-05-11 11:21:53 +00:00
|
|
|
m_mapLocationModel->setSelected(std::move(divesites));
|
2022-09-03 21:00:03 +00:00
|
|
|
m_mapLocationModel->selectionChanged();
|
|
|
|
updateEditMode();
|
2019-08-30 15:38:54 +00:00
|
|
|
}
|
|
|
|
|
2018-10-08 19:16:40 +00:00
|
|
|
void MapWidgetHelper::centerOnSelectedDiveSite()
|
2017-07-29 05:01:33 +00:00
|
|
|
{
|
2024-05-11 11:21:53 +00:00
|
|
|
std::vector<struct dive_site *> selDS = m_mapLocationModel->selectedDs();
|
2017-08-06 23:04:23 +00:00
|
|
|
|
2024-05-11 11:21:53 +00:00
|
|
|
if (selDS.empty()) {
|
2018-10-08 19:16:40 +00:00
|
|
|
// no selected dives with GPS coordinates
|
2017-07-29 05:01:33 +00:00
|
|
|
QMetaObject::invokeMethod(m_map, "deselectMapLocation");
|
2019-05-08 20:26:28 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// find the most top-left and bottom-right dive sites on the map coordinate system.
|
|
|
|
qreal minLat = 0.0, minLon = 0.0, maxLat = 0.0, maxLon = 0.0;
|
|
|
|
int count = 0;
|
|
|
|
for(struct dive_site *dss: selDS) {
|
|
|
|
if (!has_location(&dss->location))
|
|
|
|
continue;
|
|
|
|
qreal lat = dss->location.lat.udeg * 0.000001;
|
|
|
|
qreal lon = dss->location.lon.udeg * 0.000001;
|
|
|
|
if (++count == 1) {
|
|
|
|
minLat = maxLat = lat;
|
|
|
|
minLon = maxLon = lon;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (lat < minLat)
|
|
|
|
minLat = lat;
|
|
|
|
else if (lat > maxLat)
|
|
|
|
maxLat = lat;
|
|
|
|
if (lon < minLon)
|
|
|
|
minLon = lon;
|
|
|
|
else if (lon > maxLon)
|
|
|
|
maxLon = lon;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Pass coordinates to QML, either as a point or as a rectangle.
|
|
|
|
// If we didn't find any coordinates, do nothing.
|
|
|
|
if (count == 1) {
|
2019-05-02 19:44:05 +00:00
|
|
|
QGeoCoordinate dsCoord (selDS[0]->location.lat.udeg * 0.000001, selDS[0]->location.lon.udeg * 0.000001);
|
|
|
|
QMetaObject::invokeMethod(m_map, "centerOnCoordinate", Q_ARG(QVariant, QVariant::fromValue(dsCoord)));
|
2019-05-08 20:26:28 +00:00
|
|
|
} else if (count > 1) {
|
2017-08-06 23:04:23 +00:00
|
|
|
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",
|
2019-05-08 20:26:28 +00:00
|
|
|
Q_ARG(QVariant, QVariant::fromValue(coordTopLeft)),
|
|
|
|
Q_ARG(QVariant, QVariant::fromValue(coordBottomRight)),
|
|
|
|
Q_ARG(QVariant, QVariant::fromValue(coordCenter)));
|
2017-07-29 05:01:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-30 20:19:23 +00:00
|
|
|
void MapWidgetHelper::updateEditMode()
|
2017-07-29 05:01:33 +00:00
|
|
|
{
|
2019-05-03 21:16:40 +00:00
|
|
|
#ifndef SUBSURFACE_MOBILE
|
|
|
|
// The filter being set to dive site is the signal that we are in dive site edit mode.
|
|
|
|
// This is the case when either the dive site edit tab or the dive site list tab are active.
|
2019-08-30 20:19:23 +00:00
|
|
|
bool old = m_editMode;
|
2019-11-17 17:13:55 +00:00
|
|
|
m_editMode = DiveFilter::instance()->diveSiteMode();
|
2019-08-30 20:19:23 +00:00
|
|
|
if (old != m_editMode)
|
|
|
|
emit editModeChanged();
|
2019-05-03 21:16:40 +00:00
|
|
|
#endif
|
2019-08-30 20:19:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void MapWidgetHelper::reloadMapLocations()
|
|
|
|
{
|
|
|
|
updateEditMode();
|
2019-05-08 20:15:01 +00:00
|
|
|
m_mapLocationModel->reload(m_map);
|
2017-07-29 05:01:33 +00:00
|
|
|
}
|
|
|
|
|
2019-08-30 10:38:25 +00:00
|
|
|
void MapWidgetHelper::selectedLocationChanged(struct dive_site *ds_in)
|
2017-07-29 05:01:33 +00:00
|
|
|
{
|
2019-05-02 20:41:24 +00:00
|
|
|
QList<int> selectedDiveIds;
|
2019-08-30 10:38:25 +00:00
|
|
|
|
|
|
|
if (!ds_in)
|
|
|
|
return;
|
2024-05-11 11:21:53 +00:00
|
|
|
const MapLocation *location = m_mapLocationModel->getMapLocation(ds_in);
|
2019-08-30 10:38:25 +00:00
|
|
|
if (!location)
|
|
|
|
return;
|
2019-08-31 22:18:15 +00:00
|
|
|
QGeoCoordinate locationCoord = location->coordinate;
|
2019-08-30 10:38:25 +00:00
|
|
|
|
2024-06-07 08:25:09 +00:00
|
|
|
for (auto [idx, dive]: enumerated_range(divelog.dives)) {
|
2024-06-30 16:36:29 +00:00
|
|
|
struct dive_site *ds = dive->dive_site;
|
2024-06-30 15:38:36 +00:00
|
|
|
if (!ds || !ds->has_gps_location())
|
2017-07-29 05:01:33 +00:00
|
|
|
continue;
|
2018-03-08 19:43:23 +00:00
|
|
|
#ifndef SUBSURFACE_MOBILE
|
2018-10-20 18:12:15 +00:00
|
|
|
const qreal latitude = ds->location.lat.udeg * 0.000001;
|
|
|
|
const qreal longitude = ds->location.lon.udeg * 0.000001;
|
2017-07-29 05:01:33 +00:00
|
|
|
QGeoCoordinate dsCoord(latitude, longitude);
|
|
|
|
if (locationCoord.distanceTo(dsCoord) < m_smallCircleRadius)
|
2019-05-02 20:41:24 +00:00
|
|
|
selectedDiveIds.append(idx);
|
2017-07-29 05:01:33 +00:00
|
|
|
}
|
2018-03-08 19:43:23 +00:00
|
|
|
#else // the mobile version doesn't support multi-dive selection
|
2019-08-31 22:18:15 +00:00
|
|
|
if (ds == location->divesite)
|
2019-05-02 20:41:24 +00:00
|
|
|
selectedDiveIds.append(dive->id); // use id here instead of index
|
2018-03-08 19:43:23 +00:00
|
|
|
}
|
|
|
|
int last; // get latest dive chronologically
|
2019-05-02 20:41:24 +00:00
|
|
|
if (!selectedDiveIds.isEmpty()) {
|
|
|
|
last = selectedDiveIds.last();
|
|
|
|
selectedDiveIds.clear();
|
|
|
|
selectedDiveIds.append(last);
|
2018-03-08 19:43:23 +00:00
|
|
|
}
|
|
|
|
#endif
|
2019-05-02 20:41:24 +00:00
|
|
|
emit selectedDivesChanged(selectedDiveIds);
|
2017-07-29 05:01:33 +00:00
|
|
|
}
|
|
|
|
|
2017-08-06 23:58:19 +00:00
|
|
|
void MapWidgetHelper::selectVisibleLocations()
|
|
|
|
{
|
2019-05-02 20:41:24 +00:00
|
|
|
QList<int> selectedDiveIds;
|
2024-06-07 08:25:09 +00:00
|
|
|
for (auto [idx, dive]: enumerated_range(divelog.dives)) {
|
2024-06-30 16:36:29 +00:00
|
|
|
struct dive_site *ds = dive->dive_site;
|
2024-06-30 15:38:36 +00:00
|
|
|
if (!ds || ds->has_gps_location())
|
2017-08-06 23:58:19 +00:00
|
|
|
continue;
|
2018-10-20 18:12:15 +00:00
|
|
|
const qreal latitude = ds->location.lat.udeg * 0.000001;
|
|
|
|
const qreal longitude = ds->location.lon.udeg * 0.000001;
|
2017-08-08 21:36:33 +00:00
|
|
|
QGeoCoordinate dsCoord(latitude, longitude);
|
|
|
|
QPointF point;
|
|
|
|
QMetaObject::invokeMethod(m_map, "fromCoordinate", Q_RETURN_ARG(QPointF, point),
|
|
|
|
Q_ARG(QGeoCoordinate, dsCoord));
|
2019-05-02 20:22:52 +00:00
|
|
|
if (!qIsNaN(point.x()))
|
2020-03-11 10:30:51 +00:00
|
|
|
#ifndef SUBSURFACE_MOBILE // indices on desktop
|
2019-05-02 20:41:24 +00:00
|
|
|
selectedDiveIds.append(idx);
|
2017-08-06 23:58:19 +00:00
|
|
|
}
|
2018-03-08 19:43:23 +00:00
|
|
|
#else // use id on mobile instead of index
|
2019-05-02 20:41:24 +00:00
|
|
|
selectedDiveIds.append(dive->id);
|
2018-03-08 19:43:23 +00:00
|
|
|
}
|
|
|
|
int last; // get latest dive chronologically
|
2019-05-02 20:41:24 +00:00
|
|
|
if (!selectedDiveIds.isEmpty()) {
|
|
|
|
last = selectedDiveIds.last();
|
|
|
|
selectedDiveIds.clear();
|
|
|
|
selectedDiveIds.append(last);
|
2018-03-08 19:43:23 +00:00
|
|
|
}
|
|
|
|
#endif
|
2019-05-02 20:41:24 +00:00
|
|
|
emit selectedDivesChanged(selectedDiveIds);
|
2017-08-06 23:58:19 +00:00
|
|
|
}
|
|
|
|
|
2017-07-29 05:01:33 +00:00
|
|
|
/*
|
|
|
|
* 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),
|
2017-09-16 21:25:22 +00:00
|
|
|
Q_ARG(QGeoCoordinate, coord));
|
2017-07-29 05:01:33 +00:00
|
|
|
QPointF point2(point.x() + SMALL_CIRCLE_RADIUS_PX, point.y());
|
|
|
|
QGeoCoordinate coord2;
|
|
|
|
QMetaObject::invokeMethod(m_map, "toCoordinate", Q_RETURN_ARG(QGeoCoordinate, coord2),
|
2017-09-16 21:25:22 +00:00
|
|
|
Q_ARG(QPointF, point2));
|
2017-07-29 05:01:33 +00:00
|
|
|
m_smallCircleRadius = coord2.distanceTo(coord);
|
|
|
|
}
|
|
|
|
|
2018-10-20 18:12:15 +00:00
|
|
|
static location_t mk_location(QGeoCoordinate coord)
|
|
|
|
{
|
|
|
|
return create_location(coord.latitude(), coord.longitude());
|
|
|
|
}
|
|
|
|
|
2017-07-29 05:01:33 +00:00
|
|
|
void MapWidgetHelper::copyToClipboardCoordinates(QGeoCoordinate coord, bool formatTraditional)
|
|
|
|
{
|
|
|
|
bool savep = prefs.coordinates_traditional;
|
|
|
|
prefs.coordinates_traditional = formatTraditional;
|
2018-10-20 18:12:15 +00:00
|
|
|
location_t location = mk_location(coord);
|
2019-03-25 08:05:47 +00:00
|
|
|
QApplication::clipboard()->setText(printGPSCoords(&location), QClipboard::Clipboard);
|
2017-07-29 05:01:33 +00:00
|
|
|
|
|
|
|
prefs.coordinates_traditional = savep;
|
|
|
|
}
|
|
|
|
|
2018-10-26 15:03:54 +00:00
|
|
|
void MapWidgetHelper::updateCurrentDiveSiteCoordinatesFromMap(struct dive_site *ds, QGeoCoordinate coord)
|
2017-07-29 05:01:33 +00:00
|
|
|
{
|
2018-10-26 15:03:54 +00:00
|
|
|
MapLocation *loc = m_mapLocationModel->getMapLocation(ds);
|
2017-07-29 05:01:33 +00:00
|
|
|
if (loc)
|
2019-08-31 22:18:15 +00:00
|
|
|
loc->coordinate = coord;
|
2018-10-20 18:12:15 +00:00
|
|
|
location_t location = mk_location(coord);
|
2019-03-14 22:28:45 +00:00
|
|
|
emit coordinatesChanged(ds, location);
|
2017-07-29 05:01:33 +00:00
|
|
|
}
|
|
|
|
|
2019-05-09 19:33:01 +00:00
|
|
|
void MapWidgetHelper::diveSiteChanged(struct dive_site *ds, int field)
|
2017-11-09 16:43:21 +00:00
|
|
|
{
|
2019-05-09 19:33:01 +00:00
|
|
|
centerOnDiveSite(ds);
|
2017-11-09 16:43:21 +00:00
|
|
|
}
|
|
|
|
|
2019-08-30 13:25:59 +00:00
|
|
|
bool MapWidgetHelper::editMode() const
|
|
|
|
{
|
|
|
|
return m_editMode;
|
|
|
|
}
|
|
|
|
|
2017-08-12 00:58:26 +00:00
|
|
|
QString MapWidgetHelper::pluginObject()
|
|
|
|
{
|
2020-03-22 15:43:21 +00:00
|
|
|
QString lang = getUiLanguage().replace('_', '-');
|
2024-06-13 20:59:32 +00:00
|
|
|
QString cacheFolder = QString::fromStdString(system_default_directory() + "/googlemaps").replace("\\", "/");
|
2019-05-09 19:43:18 +00:00
|
|
|
return QStringLiteral("import QtQuick 2.0;"
|
|
|
|
"import QtLocation 5.3;"
|
|
|
|
"Plugin {"
|
|
|
|
" id: mapPlugin;"
|
|
|
|
" name: 'googlemaps';"
|
|
|
|
" PluginParameter { name: 'googlemaps.maps.language'; value: '%1' }"
|
|
|
|
" PluginParameter { name: 'googlemaps.cachefolder'; value: '%2' }"
|
|
|
|
" Component.onCompleted: {"
|
|
|
|
" if (availableServiceProviders.indexOf(name) === -1) {"
|
|
|
|
" console.warn('MapWidget.qml: cannot find a plugin named: ' + name);"
|
|
|
|
" }"
|
|
|
|
" }"
|
|
|
|
"}").arg(lang, cacheFolder);
|
2017-08-12 00:58:26 +00:00
|
|
|
}
|