From 1ecd5065a0f821bb9406b5f651ebf6db5d6c2040 Mon Sep 17 00:00:00 2001 From: Willem Ferguson Date: Sun, 19 Jan 2020 12:06:50 -0800 Subject: Desktop: Import dive coordinates directly from GPS This allows Subsurface to obtain the coordinates of a dive directly from a GPS track. It parses a GPX file (GPX V1.0 or V1.1) from a GPS to locate the trackpoint immediatedly after the start of a dive. There is an additional "Use GPS file" button in the Edit Dive Site panel that is selected from the Notes tab. Image: This allows one to select a GPX file, bringing up the Import GPS dialog. There is extensive provision for cross-checking that the dive track synchronises with the dive start and end. If the Save button in the dialog is pressed the dive coordinates are copied into the Dive Coordinates text box in the Edit Dive Site panel. The map moves to indicate the location of the dive site. The bulk of the work is done in importgps.cpp. The code is pretty intergrated: I tried to break it up in smaller commits but that was not feasible. The code includes responses to the comments by @neolit123 and @bstoeger. The C-based file input was replaced with Qt-based code using QChar, QString and QFile. [Dirk Hohndel: fixed several small issues in the .ui file, removed various headers includes that weren't needed and fixed printing of minutes as zero padded] Signed-off-by: willemferguson Signed-off-by: Dirk Hohndel --- desktop-widgets/CMakeLists.txt | 3 + desktop-widgets/importgps.cpp | 294 ++++++++++++++++ desktop-widgets/importgps.h | 49 +++ desktop-widgets/importgps.ui | 575 ++++++++++++++++++++++++++++++++ desktop-widgets/locationinformation.cpp | 24 +- desktop-widgets/locationinformation.h | 4 +- desktop-widgets/locationinformation.ui | 37 +- icons/resultgreen.png | Bin 0 -> 596 bytes icons/resultred.png | Bin 0 -> 595 bytes icons/resultyellow.png | Bin 0 -> 599 bytes subsurface.qrc | 3 + 11 files changed, 977 insertions(+), 12 deletions(-) create mode 100644 desktop-widgets/importgps.cpp create mode 100644 desktop-widgets/importgps.h create mode 100644 desktop-widgets/importgps.ui create mode 100644 icons/resultgreen.png create mode 100644 icons/resultred.png create mode 100644 icons/resultyellow.png diff --git a/desktop-widgets/CMakeLists.txt b/desktop-widgets/CMakeLists.txt index 14c829526..82a81321e 100644 --- a/desktop-widgets/CMakeLists.txt +++ b/desktop-widgets/CMakeLists.txt @@ -28,6 +28,7 @@ set (SUBSURFACE_UI downloadfromdivecomputer.ui filterwidget2.ui findmovedimagesdialog.ui + importgps.ui listfilter.ui locationinformation.ui mainwindow.ui @@ -83,6 +84,8 @@ set(SUBSURFACE_INTERFACE findmovedimagesdialog.h groupedlineedit.cpp groupedlineedit.h + importgps.cpp + importgps.h kmessagewidget.cpp kmessagewidget.h locationinformation.cpp diff --git a/desktop-widgets/importgps.cpp b/desktop-widgets/importgps.cpp new file mode 100644 index 000000000..5e4845951 --- /dev/null +++ b/desktop-widgets/importgps.cpp @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: GPL-2.0 +#include "desktop-widgets/importgps.h" + +/* Import dive coordinates from a GPS device and synchronise them with the dive profile information + of a dive computer. This file contains the infrastructure to: + 1) Read a .GPX file from a GPS system. + 2) Find the first gpx trackpoint that follows after the start of the dive. + 3) Allow modification of the coordinates dependent on international time zone and + on differences in local time settings between the GPS and the dive computer. + 4) Saving the coordinates into the Coordinates text box in the Dive Site Edit panel + and which which causes the map to show the location of the dive site. + The structure coords is used to store critical information. */ +ImportGPS::ImportGPS(QWidget *parent, QString fileName, class Ui::LocationInformation *LocationUI) : QDialog(parent), + fileName(fileName), LocationUI(LocationUI) +{ + ui.setupUi(this); + connect(ui.timeDiffEdit, &QTimeEdit::timeChanged, this, &ImportGPS::timeDiffEditChanged); + connect(ui.timeZoneEdit, &QTimeEdit::timeChanged, this, &ImportGPS::timeZoneEditChanged); + connect(ui.timezone_backwards, &QRadioButton::toggled, this, &ImportGPS::changeZoneBackwards); + connect(ui.timezone_forward, &QRadioButton::toggled, this, &ImportGPS::changeZoneForward); + connect(ui.diff_backwards, &QRadioButton::toggled, this, &ImportGPS::changeDiffBackwards); + connect(ui.diff_forward, &QRadioButton::toggled, this, &ImportGPS::changeDiffForward); + connect(ui.GPSbuttonBox, &QDialogButtonBox::clicked, this, &ImportGPS::buttonClicked); + coords.settingsDiff_offset = 0; + coords.timeZone_offset = 0; + coords.lat = 0; + coords.lon = 0; + pixmapSize = (int) (ui.diveDateLabel->height() / 2); +} + +void ImportGPS::buttonClicked(QAbstractButton *button) +{ + if (ui.GPSbuttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { + // Write the coordinates in decimal degree format to the Coordinates QLineEdit of the LocationaInformationWidget UI: + LocationUI->diveSiteCoordinates->setText(QString::number(coords.lat) + ", " + QString::number(coords.lon)); + LocationUI->diveSiteCoordinates->editingFinished(); + } else { + close(); + } +} + +// Read text from the present position in the file until +// the first 'delim' character is encountered. +int ImportGPS::getSubstring(QFile *file, QString *bufptr, char delim) +{ + char c; + bufptr->clear(); + do { + if (file->read(&c, 1) <= 0) // EOF + return 1; + if (c == delim) break; + bufptr->append(QChar(c)); + } while (c != delim); + return 0; +} + +// Find the next occurence of a specified target GPX element in the file, +// characerised by a "" character sequence. +// 'target' specifies the name of the element searched for. +// termc is the ending character of the element name search = '>' or ' '. +int ImportGPS::findXmlElement(QFile *fileptr, QString target, QString *bufptr, char termc) +{ + bool match = false; + char c; + char skipdelim = (termc == ' ') ? '>' : ' '; + do { // Skip input until first start new of element (<) is encountered: + if (getSubstring(fileptr, bufptr, '<')) + return 1; // EOF + bufptr->clear(); + do { // Read name of following element and store it in buf + if (fileptr->read(&c, 1) <= 0) // EOF encountered + return 1; + if ((c == '>') || (c == ' ')) // found a valid delimiter + break; + bufptr->append(QChar(c)); + } while ((c != '>') && (c != ' ')); + if (*bufptr == "/trk") // End of GPS track found: return EOF + return 1; + if (c == skipdelim) + continue; // if inappropriate delimiter was found, redo from start + if (*bufptr == target) // Compare xml element name from gpx file with the + match = true; // the target element searched for. + } while (match == false); + return 0; +} + +// Find the coordinates at the time specified in coords.start_dive +// by searching the gpx file "fileName". Here is a typical trkpt element in GPX: +// -53.7 +int ImportGPS::getCoordsFromFile() +{ + struct tm tm1; + time_t when = 0; + double lon, lat; + int line = 0; + int64_t time_offset = coords.settingsDiff_offset + coords.timeZone_offset; + time_t divetime; + bool first_line = true; + bool found = false; + divetime = coords.start_dive; + QString buf; + QFile f1; + f1.setFileName(fileName); + if (!f1.open(QIODevice::ReadOnly | QIODevice::Text)) { + QByteArray local8bitBAString1 = fileName.toLocal8Bit(); + char *fname = local8bitBAString1.data(); // convert QString to a C string fileName + fprintf(stderr, "GPS file open error: file name = %s\n", fname); + return 1; + } + +#ifdef GPSDEBUG + struct tm time; // decode the time of start of dive: + utc_mkdate(divetime, &time); + int dyr,dmon,dday,dhr,dmin; + dyr = time.tm_year; + dmon = time.tm_mon; + dday = time.tm_mday; + dhr = time.tm_hour; + dmin = time.tm_min; +#endif + + do { + line++; // this is the sequence number of the trkpt xml element processed + // Find next trkpt xml element (This function also detects that signals EOF): + if (findXmlElement(&f1, QString("trkpt"), &buf, ' ')) // This is the normal exit point + break; // for this routine + // == Get coordinates: == + if (getSubstring(&f1, &buf, '"')) // read up to the end of the "lat=" label + break; // on EOF + if (buf != "lat=") { + fprintf(stderr, "GPX parse error: cannot find latitude (trkpt #%d)\n", line); + return 1; + } + if (getSubstring(&f1, &buf, '"')) // read up to the end of the latitude value + break; // on EOF + lat = buf.toDouble(); // Convert lat to decimal + if (getSubstring(&f1, &buf, ' ')) // Read past space char + break; // on EOF + if (getSubstring(&f1, &buf, '"')) // Read up to end of "lon=" label + break; // on EOF + if (buf != "lon=") { + fprintf(stderr, "GPX parse error: cannot find longitude (trkpt #%d)\n", line); + return 1; + } + if (getSubstring(&f1, &buf, '"')) // get string with longitude + break; // on EOF + lon = buf.toDouble(); // Convert longitude to decimal + // == get time: == + if (findXmlElement(&f1, QString("time"), &buf, '>')) // Find the