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"
|
|
|
|
#include "core/divesite.h"
|
2018-06-03 20:15:19 +00:00
|
|
|
#include "core/qthelper.h"
|
2017-07-29 05:01:33 +00:00
|
|
|
#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);
|
2017-12-28 11:20:34 +00:00
|
|
|
m_smallCircleRadius = SMALL_CIRCLE_RADIUS_PX;
|
|
|
|
m_map = nullptr;
|
|
|
|
m_editMode = false;
|
2017-07-29 05:01:33 +00:00
|
|
|
connect(m_mapLocationModel, SIGNAL(selectedLocationChanged(MapLocation *)),
|
|
|
|
this, SLOT(selectedLocationChanged(MapLocation *)));
|
|
|
|
}
|
|
|
|
|
2018-10-25 18:46:12 +00:00
|
|
|
QGeoCoordinate MapWidgetHelper::getCoordinates(QVariant dive_site)
|
2018-04-02 19:48:23 +00:00
|
|
|
{
|
2018-10-25 18:46:12 +00:00
|
|
|
struct dive_site *ds = (struct dive_site *)dive_site.value<uintptr_t>();
|
2018-04-02 19:48:23 +00:00
|
|
|
if (!ds || !dive_site_has_gps_location(ds))
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2018-10-25 18:46:12 +00:00
|
|
|
void MapWidgetHelper::centerOnDiveSite(QVariant dive_site)
|
2018-03-08 19:38:14 +00:00
|
|
|
{
|
2018-10-25 18:46:12 +00:00
|
|
|
struct dive_site *ds = (struct dive_site *)dive_site.value<uintptr_t>();
|
2018-03-08 19:38:14 +00:00
|
|
|
if (ds)
|
|
|
|
centerOnDiveSite(ds);
|
|
|
|
}
|
|
|
|
|
2017-07-29 05:01:33 +00:00
|
|
|
void MapWidgetHelper::centerOnDiveSite(struct dive_site *ds)
|
2018-10-08 19:16:40 +00:00
|
|
|
{
|
|
|
|
if (!ds || !dive_site_has_gps_location(ds)) {
|
|
|
|
// dive site with no GPS
|
2018-10-26 15:03:54 +00:00
|
|
|
m_mapLocationModel->setSelected(ds, false);
|
2018-10-08 19:16:40 +00:00
|
|
|
QMetaObject::invokeMethod(m_map, "deselectMapLocation");
|
|
|
|
} else {
|
|
|
|
// dive site with GPS
|
2018-10-26 15:03:54 +00:00
|
|
|
m_mapLocationModel->setSelected(ds, false);
|
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)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MapWidgetHelper::centerOnSelectedDiveSite()
|
2017-07-29 05:01:33 +00:00
|
|
|
{
|
2017-08-06 23:04:23 +00:00
|
|
|
QVector<struct dive_site *> selDS;
|
|
|
|
QVector<QGeoCoordinate> selGC;
|
|
|
|
|
2018-03-08 19:43:23 +00:00
|
|
|
int idx;
|
|
|
|
struct dive *dive;
|
2017-08-06 23:04:23 +00:00
|
|
|
for_each_dive (idx, dive) {
|
2018-10-08 19:16:40 +00:00
|
|
|
if (!dive->selected)
|
|
|
|
continue;
|
2017-08-06 23:04:23 +00:00
|
|
|
struct dive_site *dss = get_dive_site_for_dive(dive);
|
2018-10-08 19:16:40 +00:00
|
|
|
if (!dive_site_has_gps_location(dss))
|
2017-08-06 23:04:23 +00:00
|
|
|
continue;
|
|
|
|
// only store dive sites with GPS
|
|
|
|
selDS.append(dss);
|
2018-10-20 18:12:15 +00:00
|
|
|
selGC.append(QGeoCoordinate(dss->location.lat.udeg * 0.000001,
|
|
|
|
dss->location.lon.udeg * 0.000001));
|
2017-08-06 23:04:23 +00:00
|
|
|
}
|
2018-10-08 19:16:40 +00:00
|
|
|
|
|
|
|
if (selDS.isEmpty()) {
|
|
|
|
// no selected dives with GPS coordinates
|
2018-10-26 15:03:54 +00:00
|
|
|
m_mapLocationModel->setSelected(nullptr, false);
|
2017-07-29 05:01:33 +00:00
|
|
|
QMetaObject::invokeMethod(m_map, "deselectMapLocation");
|
2017-08-06 23:04:23 +00:00
|
|
|
} else if (selDS.size() == 1) {
|
2018-10-08 19:16:40 +00:00
|
|
|
centerOnDiveSite(selDS[0]);
|
2017-08-06 23:04:23 +00:00
|
|
|
} 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. */
|
2018-10-26 15:03:54 +00:00
|
|
|
m_mapLocationModel->setSelected(selDS[0], false);
|
2017-08-06 23:04:23 +00:00
|
|
|
qreal minLat = 0.0, minLon = 0.0, maxLat = 0.0, maxLon = 0.0;
|
|
|
|
bool start = true;
|
2018-10-08 19:16:40 +00:00
|
|
|
for(struct dive_site *dss: selDS) {
|
2018-10-20 18:12:15 +00:00
|
|
|
qreal lat = dss->location.lat.udeg * 0.000001;
|
|
|
|
qreal lon = dss->location.lon.udeg * 0.000001;
|
2017-08-06 23:04:23 +00:00
|
|
|
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)));
|
2017-07-29 05:01:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MapWidgetHelper::reloadMapLocations()
|
|
|
|
{
|
|
|
|
int idx;
|
2018-08-17 17:37:30 +00:00
|
|
|
struct dive *dive;
|
2017-07-29 05:01:33 +00:00
|
|
|
QMap<QString, MapLocation *> locationNameMap;
|
|
|
|
m_mapLocationModel->clear();
|
|
|
|
MapLocation *location;
|
|
|
|
QVector<MapLocation *> locationList;
|
2018-10-25 18:58:54 +00:00
|
|
|
QVector<struct dive_site *> locations;
|
2017-07-29 05:01:33 +00:00
|
|
|
qreal latitude, longitude;
|
|
|
|
|
2018-08-17 17:37:30 +00:00
|
|
|
for_each_dive(idx, dive) {
|
2018-10-08 14:24:41 +00:00
|
|
|
// Don't show dive sites of hidden dives, unless this is the currently
|
|
|
|
// displayed (edited) dive.
|
|
|
|
if (dive->hidden_by_filter && dive != current_dive)
|
2018-08-17 17:37:30 +00:00
|
|
|
continue;
|
|
|
|
struct dive_site *ds = get_dive_site_for_dive(dive);
|
2018-10-25 18:58:54 +00:00
|
|
|
if (!dive_site_has_gps_location(ds) || locations.contains(ds))
|
2017-07-29 05:01:33 +00:00
|
|
|
continue;
|
2018-10-20 18:12:15 +00:00
|
|
|
latitude = ds->location.lat.udeg * 0.000001;
|
|
|
|
longitude = ds->location.lon.udeg * 0.000001;
|
2017-07-29 05:01:33 +00:00
|
|
|
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
|
2017-11-03 16:44:09 +00:00
|
|
|
if (locationNameMap.contains(name)) {
|
2017-07-29 05:01:33 +00:00
|
|
|
MapLocation *existingLocation = locationNameMap[name];
|
2017-11-25 21:21:12 +00:00
|
|
|
QGeoCoordinate coord = existingLocation->coordinate();
|
2017-07-29 05:01:33 +00:00
|
|
|
if (dsCoord.distanceTo(coord) < MIN_DISTANCE_BETWEEN_DIVE_SITES_M)
|
|
|
|
continue;
|
|
|
|
}
|
2018-10-26 15:03:54 +00:00
|
|
|
location = new MapLocation(ds, dsCoord, name);
|
2017-07-29 05:01:33 +00:00
|
|
|
locationList.append(location);
|
2018-10-25 18:58:54 +00:00
|
|
|
locations.append(ds);
|
2017-07-29 05:01:33 +00:00
|
|
|
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;
|
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)
|
|
|
|
m_selectedDiveIds.append(idx);
|
|
|
|
}
|
2018-03-08 19:43:23 +00:00
|
|
|
#else // the mobile version doesn't support multi-dive selection
|
2018-10-26 15:03:54 +00:00
|
|
|
if (ds == location->divesite())
|
2018-03-08 19:43:23 +00:00
|
|
|
m_selectedDiveIds.append(dive->id); // use id here instead of index
|
|
|
|
}
|
|
|
|
int last; // get latest dive chronologically
|
|
|
|
if (!m_selectedDiveIds.isEmpty()) {
|
|
|
|
last = m_selectedDiveIds.last();
|
|
|
|
m_selectedDiveIds.clear();
|
|
|
|
m_selectedDiveIds.append(last);
|
|
|
|
}
|
|
|
|
#endif
|
2017-07-29 05:01:33 +00:00
|
|
|
emit selectedDivesChanged(m_selectedDiveIds);
|
|
|
|
}
|
|
|
|
|
2017-08-06 23:58:19 +00:00
|
|
|
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;
|
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));
|
|
|
|
if (!qIsNaN(point.x())) {
|
|
|
|
if (!selectedFirst) {
|
2018-10-26 15:03:54 +00:00
|
|
|
m_mapLocationModel->setSelected(ds, false);
|
2017-08-08 21:36:33 +00:00
|
|
|
selectedFirst = true;
|
2017-08-06 23:58:19 +00:00
|
|
|
}
|
2018-03-08 19:43:23 +00:00
|
|
|
#ifndef SUBSURFACE_MOBILE // indexes on desktop
|
2017-08-08 21:36:33 +00:00
|
|
|
m_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
|
|
|
|
m_selectedDiveIds.append(dive->id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
int last; // get latest dive chronologically
|
|
|
|
if (!m_selectedDiveIds.isEmpty()) {
|
|
|
|
last = m_selectedDiveIds.last();
|
|
|
|
m_selectedDiveIds.clear();
|
|
|
|
m_selectedDiveIds.append(last);
|
|
|
|
}
|
|
|
|
#endif
|
2017-08-06 23:58:19 +00:00
|
|
|
emit selectedDivesChanged(m_selectedDiveIds);
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
const char *coordinates = printGPSCoords(&location);
|
2017-07-29 05:01:33 +00:00
|
|
|
QApplication::clipboard()->setText(QString(coordinates), QClipboard::Clipboard);
|
|
|
|
|
|
|
|
free((void *)coordinates);
|
|
|
|
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)
|
|
|
|
loc->setCoordinate(coord);
|
2018-10-20 18:12:15 +00:00
|
|
|
location_t location = mk_location(coord);
|
|
|
|
emit coordinatesChanged(location);
|
2017-07-29 05:01:33 +00:00
|
|
|
}
|
|
|
|
|
2018-10-25 10:23:19 +00:00
|
|
|
void MapWidgetHelper::updateDiveSiteCoordinates(struct dive_site *ds, const location_t &location)
|
2017-11-09 16:43:21 +00:00
|
|
|
{
|
2018-10-25 10:23:19 +00:00
|
|
|
if (!ds)
|
|
|
|
return;
|
2018-10-20 18:12:15 +00:00
|
|
|
const qreal latitude_r = location.lat.udeg * 0.000001;
|
|
|
|
const qreal longitude_r = location.lon.udeg * 0.000001;
|
2018-10-08 10:57:46 +00:00
|
|
|
QGeoCoordinate coord(latitude_r, longitude_r);
|
2018-10-26 15:03:54 +00:00
|
|
|
m_mapLocationModel->updateMapLocationCoordinates(ds, coord);
|
2017-11-25 21:04:33 +00:00
|
|
|
QMetaObject::invokeMethod(m_map, "centerOnCoordinate", Q_ARG(QVariant, QVariant::fromValue(coord)));
|
2017-11-09 16:43:21 +00:00
|
|
|
}
|
|
|
|
|
2018-10-08 13:00:12 +00:00
|
|
|
void MapWidgetHelper::exitEditMode()
|
2017-07-29 05:01:33 +00:00
|
|
|
{
|
2018-10-08 13:00:12 +00:00
|
|
|
m_editMode = false;
|
|
|
|
emit editModeChanged();
|
2017-07-29 05:01:33 +00:00
|
|
|
}
|
|
|
|
|
2018-10-25 08:07:45 +00:00
|
|
|
void MapWidgetHelper::enterEditMode(struct dive_site *ds)
|
2017-07-29 05:01:33 +00:00
|
|
|
{
|
2018-10-08 13:43:06 +00:00
|
|
|
// We don't support editing of a dive site that doesn't exist
|
|
|
|
if (!ds)
|
|
|
|
return;
|
|
|
|
|
2018-10-08 13:00:12 +00:00
|
|
|
m_editMode = true;
|
2018-10-26 15:03:54 +00:00
|
|
|
MapLocation *exists = m_mapLocationModel->getMapLocation(ds);
|
2018-10-08 13:00:12 +00:00
|
|
|
QGeoCoordinate coord;
|
2018-10-08 13:43:06 +00:00
|
|
|
// if divesite doesn't exist in the model, add a new MapLocation.
|
2018-10-08 13:00:12 +00:00
|
|
|
if (!exists) {
|
|
|
|
coord = m_map->property("center").value<QGeoCoordinate>();
|
2018-10-26 15:03:54 +00:00
|
|
|
m_mapLocationModel->add(new MapLocation(ds, coord, QString(ds->name)));
|
2018-10-08 13:00:12 +00:00
|
|
|
} else {
|
|
|
|
coord = exists->coordinate();
|
2017-07-29 05:01:33 +00:00
|
|
|
}
|
2018-10-25 08:07:45 +00:00
|
|
|
centerOnDiveSite(ds);
|
2018-10-20 18:12:15 +00:00
|
|
|
location_t location = mk_location(coord);
|
|
|
|
emit coordinatesChanged(location);
|
2017-07-29 05:01:33 +00:00
|
|
|
emit editModeChanged();
|
|
|
|
}
|
2017-08-12 00:58:26 +00:00
|
|
|
|
|
|
|
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%' }";
|
2017-08-12 15:14:32 +00:00
|
|
|
str += " PluginParameter { name: 'googlemaps.cachefolder'; value: '%cacheFolder%' }";
|
2017-08-12 00:58:26 +00:00
|
|
|
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);
|
2017-08-12 15:14:32 +00:00
|
|
|
QString cacheFolder = QString(system_default_directory()).append("/googlemaps");
|
|
|
|
str.replace("%cacheFolder%", cacheFolder.replace("\\", "/"));
|
2017-08-12 00:58:26 +00:00
|
|
|
return str;
|
|
|
|
}
|