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 <willemferguson@zoology.up.ac.za>
Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
This commit is contained in:
Willem Ferguson 2020-01-19 12:06:50 -08:00 committed by Dirk Hohndel
parent 1f51251f1b
commit 1ecd5065a0
11 changed files with 977 additions and 12 deletions

View file

@ -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 "<xxx " or "<xxx>" 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:
// <trkpt lat="-26.84" lon="32.88"><ele>-53.7</ele><time>2017-08-06T04:56:42Z</time></trkpt>
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 </trk> 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 <time> element
break; // on EOF
if (getSubstring(&f1, &buf, '<')) // Read the string containing date/time
break; // on EOF
bool ok;
tm1.tm_year = buf.left(4).toInt(&ok, 10); // Extract the different substrings:
tm1.tm_mon = buf.mid(5,2).toInt(&ok,10) - 1;
tm1.tm_mday = buf.mid(8,2).toInt(&ok,10);
tm1.tm_hour = buf.mid(11,2).toInt(&ok,10);
tm1.tm_min = buf.mid(14,2).toInt(&ok,10);
tm1.tm_sec = buf.mid(17,2).toInt(&ok,10);
when = utc_mktime(&tm1) + time_offset;
if (first_line) {
first_line = false;
coords.start_track = when; // Local time of start of GPS track
}
if ((when > divetime && found == false)) { // This GPS local time corresponds to the start time of the dive
coords.lon = lon; // save the coordinates
coords.lat = lat;
found = true;
}
#ifdef GPSDEBUG
utc_mkdate(when, &time); // print time and coordinates of each of the trkpt elements of the GPX file
fprintf(stderr, " %02d: lat=%f lon=%f timestamp=%ld (%ld) %02d/%02d/%02d %02d:%02d dt=%ld %02d/%02d/%02d %02d:%02d\n", line, lat,
lon, when, time_offset, time.tm_year, time.tm_mon+1, time.tm_mday, time.tm_hour, time.tm_min, divetime, dyr, dmon+1, dday,dhr, dmin );
#endif
} while (true); // This loop executes until EOF causes a break out of the loop
coords.end_track = when; // This is the local time of the end of the GPS track
f1.close();
return 0;
}
// Fill the visual elements of the synchronisation panel with information
void ImportGPS::updateUI()
{
struct tm time;
int dive_day, gps_day;
char datestr[50];
QString problemString = "";
utc_mkdate(coords.start_track, &time); // Display GPS date and local start and end times of track:
gps_day = time.tm_mday;
datestr[0] = 0x0;
strftime(datestr, sizeof(datestr), "%A %d %B ", &time); // GPS date
ui.trackDateLabel->setText("GPS date = " + QString(datestr) + QString::number(time.tm_year));
ui.startTimeLabel->setText(QString::number(time.tm_hour) + ":" + QString("%1").arg(time.tm_min, 2, 10, QChar('0'))); // track start time
utc_mkdate(coords.end_track, &time);
ui.endTimeLabel->setText(QString::number(time.tm_hour) + ":" + QString("%1").arg(time.tm_min, 2, 10, QChar('0'))); // track end time
utc_mkdate(coords.start_dive, &time); // Display dive date and start and end times of dive:
dive_day = time.tm_mday;
datestr[0] = 0x0;
strftime(datestr, sizeof(datestr), "%A %d %B ", localtime(&(coords.start_dive))); // dive date
ui.diveDateLabel->setText("Dive date = " + QString(datestr) + QString::number(time.tm_year));
ui.startTimeSyncLabel->setText( QString::number(time.tm_hour) + ":" + QString("%1").arg(time.tm_min, 2, 10, QChar('0'))); // dive start time
utc_mkdate(coords.end_dive, &time);
ui.endTimeSyncLabel->setText(QString::number(time.tm_hour) + ":" + QString("%1").arg(time.tm_min, 2, 10, QChar('0'))); // dive end time
// This section implements extensive warnings in case there is not synchronisation between dive and GPS data:
if (gps_day != dive_day)
problemString = "(different dates)";
// Create 3 icons to indicate the quality of the synchrinisation between dive and GPS
QPixmap goodResultIcon (":gps_good_result-icon");
ui.goodIconLabel->setPixmap(goodResultIcon.scaled(pixmapSize,pixmapSize,Qt::KeepAspectRatio));
ui.goodIconLabel->setVisible(false);
QPixmap warningResultIcon (":gps_warning_result-icon");
ui.warningIconLabel->setPixmap(warningResultIcon.scaled(pixmapSize,pixmapSize,Qt::KeepAspectRatio));
ui.warningIconLabel->setVisible(false);
QPixmap badResultIcon (":gps_bad_result-icon");
ui.badIconLabel->setPixmap(badResultIcon.scaled(pixmapSize,pixmapSize,Qt::KeepAspectRatio));
ui.badIconLabel->setVisible(false);
// Show information or warning message as well as synch quality icon
if (coords.start_dive < coords.start_track) {
ui.resultLabel->setStyleSheet("QLabel { color: red;} ");
ui.resultLabel->setText("PROBLEM: Dive started before the GPS track "+ problemString);
ui.badIconLabel->setVisible(true);
} else {
if (coords.start_dive > coords.end_track) {
ui.resultLabel->setStyleSheet("QLabel { color: red;} ");
ui.resultLabel->setText("PROBLEM: Dive started after the GPS track " + problemString);
ui.badIconLabel->setVisible(true);
} else {
if (coords.end_dive > coords.end_track) {
ui.resultLabel->setStyleSheet("QLabel { color: red;} ");
ui.resultLabel->setText("WARNING: Dive ended after the GPS track " + problemString);
ui.warningIconLabel->setVisible(true);
} else {
ui.resultLabel->setStyleSheet("QLabel { color: darkgreen;} ");
ui.resultLabel->setText("Dive coordinates: "+ QString::number(coords.lat) + "S, " + QString::number(coords.lon) + "E");
ui.goodIconLabel->setVisible(true);
}
}
}
}
void ImportGPS::changeZoneForward()
{
coords.timeZone_offset = abs(coords.timeZone_offset);
getCoordsFromFile(); // If any of the time controls are changed
updateUI(); // .. then recalculate the synchronisation
}
void ImportGPS::changeZoneBackwards()
{
if (coords.timeZone_offset > 0)
coords.timeZone_offset = 0 - coords.timeZone_offset;
getCoordsFromFile();
updateUI();
}
void ImportGPS::changeDiffForward()
{
coords.settingsDiff_offset = abs(coords.settingsDiff_offset);
getCoordsFromFile();
updateUI();
}
void ImportGPS::changeDiffBackwards()
{
if (coords.settingsDiff_offset > 0)
coords.settingsDiff_offset = 0 - coords.settingsDiff_offset;
getCoordsFromFile();
updateUI();
}
void ImportGPS::timeDiffEditChanged()
{
coords.settingsDiff_offset = ui.timeDiffEdit->time().hour() * 3600 + ui.timeDiffEdit->time().minute() * 60;
if (ui.diff_backwards->isChecked())
coords.settingsDiff_offset = 0 - coords.settingsDiff_offset;
getCoordsFromFile();
updateUI();
}
void ImportGPS::timeZoneEditChanged()
{
coords.timeZone_offset = ui.timeZoneEdit->time().hour() * 3600;
if (ui.timezone_backwards->isChecked())
coords.timeZone_offset = 0 - coords.timeZone_offset;
getCoordsFromFile();
updateUI();
}