summaryrefslogtreecommitdiffstats
path: root/desktop-widgets/importgps.cpp
blob: 5e48459511a1d1d3542860d216a1cf203b013a3b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
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();
}