1 /**********************************************************************************************
2 Copyright (C) 2017 Michel Durand <zero@cms123.fr>
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17 **********************************************************************************************/
18
19 #include "CMainWindow.h"
20 #include "gis/CGisListWks.h"
21 #include "gis/suunto/CLogProject.h"
22 #include "gis/suunto/ISuuntoProject.h"
23 #include "gis/trk/CGisItemTrk.h"
24 #include <QtWidgets>
25
26
27 const QList<extension_t> CLogProject::extensions =
28 {
29 {"Latitude", 0.0000001, 0.0, ASSIGN_VALUE(lat, NIL)} // unit [°]
30 , {"Longitude", 0.0000001, 0.0, ASSIGN_VALUE(lon, NIL)} // unit [°]
31 , {"Altitude", 1.0, 0.0, ASSIGN_VALUE(ele, NIL)} // unit [m]
32 , {"VerticalSpeed", 0.01, 0.0, ASSIGN_VALUE(extensions["gpxdata:verticalSpeed"], NIL)} // unit [m/h]
33 , {"HR", 1.0, 0.0, ASSIGN_VALUE(extensions["gpxtpx:TrackPointExtension|gpxtpx:hr"], qRound)} // unit [bpm]
34 , {"Cadence", 1.0, 0.0, ASSIGN_VALUE(extensions["gpxdata:cadence"], NIL)} // unit [bpm]
35 , {"Temperature", 0.1, 0.0, ASSIGN_VALUE(extensions["gpxdata:temp"], NIL)} // unit [°C]
36 , {"SeaLevelPressure", 0.1, 0.0, ASSIGN_VALUE(extensions["gpxdata:seaLevelPressure"], NIL)} // unit [hPa]
37 , {"Speed", 0.01, 0.0, ASSIGN_VALUE(extensions["gpxdata:speed"], NIL)} // unit [m/s]
38 , {"EnergyConsumption", 0.1, 0.0, ASSIGN_VALUE(extensions["gpxdata:energy"], NIL)} // unit [kCal/min]
39 };
40
41
CLogProject(const QString & filename,CGisListWks * parent)42 CLogProject::CLogProject(const QString& filename, CGisListWks* parent)
43 : ISuuntoProject(eTypeLog, filename, parent)
44 {
45 setIcon(CGisListWks::eColumnIcon, QIcon("://icons/32x32/LogProject.png"));
46 blockUpdateItems(true);
47 loadLog(filename);
48 blockUpdateItems(false);
49 setupName(QFileInfo(filename).completeBaseName().replace("_", " "));
50 }
51
52
loadLog(const QString & filename)53 void CLogProject::loadLog(const QString& filename)
54 {
55 try
56 {
57 loadLog(filename, this);
58 }
59 catch(QString& errormsg)
60 {
61 QMessageBox::critical(CMainWindow::getBestWidgetForParent(),
62 tr("Failed to load file %1...").arg(filename), errormsg, QMessageBox::Abort);
63 valid = false;
64 }
65 }
66
67
loadLog(const QString & filename,CLogProject * project)68 void CLogProject::loadLog(const QString& filename, CLogProject* project)
69 {
70 QFile file(filename);
71
72 // if the file does not exist, the filename is assumed to be a name for a new project
73 if (!file.exists() || QFileInfo(filename).suffix().toLower() != "log")
74 {
75 project->filename.clear();
76 project->setupName(filename);
77 project->setToolTip(CGisListWks::eColumnName, project->getInfo());
78 project->valid = true;
79 return;
80 }
81
82 if (!file.open(QIODevice::ReadOnly))
83 {
84 throw tr("Failed to open %1").arg(filename);
85 }
86
87 // load file content to xml document
88 QDomDocument xml;
89 QString msg;
90 int line;
91 int column;
92 if (!xml.setContent(&file, false, &msg, &line, &column))
93 {
94 file.close();
95 throw tr("Failed to read: %1\nline %2, column %3:\n %4").arg(filename).arg(line).arg(column).arg(msg);
96 }
97 file.close();
98
99 QDomElement xmlOpenambitlog = xml.documentElement();
100 if (xmlOpenambitlog.tagName() != "openambitlog")
101 {
102 throw tr("Not an Openambit log file: %1").arg(filename);
103 }
104
105 CTrackData trk;
106
107
108 if(xmlOpenambitlog.namedItem("DeviceInfo").isElement())
109 {
110 const QDomNode& xmlDeviceInfo = xmlOpenambitlog.namedItem("DeviceInfo");
111 if(xmlDeviceInfo.namedItem("Name").isElement())
112 {
113 trk.cmt = tr("Device: %1<br/>").arg(xmlDeviceInfo.namedItem("Name").toElement().text());
114 }
115 }
116
117 if(xmlOpenambitlog.namedItem("Log").isElement())
118 {
119 QDateTime time0; // start time of the track
120
121 const QDomNode& xmlLog = xmlOpenambitlog.namedItem("Log");
122 if(xmlLog.namedItem("Header").isElement())
123 {
124 const QDomNode& xmlHeader = xmlLog.namedItem("Header");
125
126 if(xmlHeader.namedItem("DateTime").isElement())
127 {
128 QString dateTimeStr = xmlHeader.namedItem("DateTime").toElement().text();
129 trk.name = dateTimeStr; // date of beginning of recording is chosen as track name
130 IUnit::parseTimestamp(dateTimeStr, time0); // as local time
131 }
132
133 if(xmlHeader.namedItem("Activity").isElement())
134 {
135 trk.desc = xmlHeader.namedItem("Activity").toElement().text();
136 }
137
138 if(xmlHeader.namedItem("RecoveryTime").isElement())
139 {
140 trk.cmt += tr("Recovery time: %1 h<br/>").arg(xmlHeader.namedItem("RecoveryTime").toElement().text().toInt() / 3600000);
141 }
142
143 if(xmlHeader.namedItem("PeakTrainingEffect").isElement())
144 {
145 trk.cmt += tr("Peak Training Effect: %1<br/>").arg(xmlHeader.namedItem("PeakTrainingEffect").toElement().text().toDouble() / 10.0);
146 }
147
148 if(xmlHeader.namedItem("Energy").isElement())
149 {
150 trk.cmt += tr("Energy: %1 kCal<br/>").arg((int)xmlHeader.namedItem("Energy").toElement().text().toDouble() );
151 }
152 }
153
154 if(xmlLog.namedItem("Samples").isElement())
155 {
156 const QDomNode& xmlSamples = xmlLog.namedItem("Samples");
157 const QDomNodeList& xmlSampleList = xmlSamples.toElement().elementsByTagName("Sample");
158
159 if (xmlSampleList.count() > 0)
160 {
161 bool UTCtimeFound = false;
162 for (int i = 0; i < xmlSampleList.count(); i++) // browse XML samples
163 { //look for samples with UTC timestamp
164 const QDomNode& xmlSample = xmlSampleList.item(i);
165
166 if (xmlSample.namedItem("UTCReference").isElement())
167 {
168 QString timeStr = xmlSample.namedItem("UTCReference").toElement().text();
169
170 if(xmlSample.namedItem("Time").isElement())
171 {
172 IUnit::parseTimestamp(timeStr, time0);
173 time0 = time0.addMSecs(-xmlSample.namedItem("Time").toElement().text().toDouble() ); // substract current sample time to get start time
174 UTCtimeFound = true;
175 break;
176 }
177 }
178 }
179
180 if ( !UTCtimeFound)
181 {
182 QMessageBox::warning(CMainWindow::getBestWidgetForParent(), tr("Use of local time...")
183 , tr("No UTC time has been found in file %1. "
184 "Local computer time will be used. "
185 "You can adjust time using a time filter if needed.").arg(filename)
186 , QMessageBox::Ok);
187 }
188
189 bool sampleWithPositionFound = false;
190 QList<sample_t> samplesList;
191 QList<QDateTime> lapsList;
192
193 for (int i = 0; i < xmlSampleList.count(); i++) // browse XML samples
194 {
195 sample_t sample;
196 const QDomNode& xmlSample = xmlSampleList.item(i);
197
198 if(xmlSample.namedItem("Latitude").isElement())
199 {
200 sampleWithPositionFound = true;
201 }
202
203 if(xmlSample.namedItem("Time").isElement())
204 {
205 sample.time = time0.addMSecs(xmlSample.namedItem("Time").toElement().text().toDouble() );
206 }
207
208 if(xmlSample.namedItem("Type").isElement())
209 {
210 if ( xmlSample.namedItem("Type").toElement().text() == "lap-info" )
211 {
212 if ( xmlSample.namedItem("Lap").isElement() )
213 {
214 const QDomNode& xmlLap = xmlSample.namedItem("Lap");
215 if(xmlLap.namedItem("Type").isElement())
216 {
217 if (xmlLap.namedItem("Type").toElement().text() == "Manual")
218 {
219 lapsList << sample.time; // stores timestamps of the samples where the the "Lap" button has been pressed
220 }
221 }
222 }
223 }
224 else if (xmlSample.namedItem("Type").toElement().text() == "gps-small"
225 || xmlSample.namedItem("Type").toElement().text() == "gps-base"
226 || xmlSample.namedItem("Type").toElement().text() == "gps-tiny"
227 || xmlSample.namedItem("Type").toElement().text() == "position"
228 || xmlSample.namedItem("Type").toElement().text() == "periodic")
229 {
230 for (const extension_t& ext : extensions)
231 {
232 if (xmlSample.namedItem(ext.tag).isElement())
233 {
234 const QDomNode& xmlSampleData = xmlSample.namedItem(ext.tag);
235 sample[ext.tag] = xmlSampleData.toElement().text().toDouble() * ext.scale + ext.offset;
236 }
237 }
238 samplesList << sample;
239 }
240 }
241 }
242
243 if (!sampleWithPositionFound)
244 {
245 throw tr("This LOG file does not contain any position data and can not be displayed by QMapShack: %1").arg(filename);
246 }
247
248 fillTrackPointsFromSamples(samplesList, lapsList, trk, extensions);
249
250 new CGisItemTrk(trk, project);
251
252 project->sortItems();
253 project->setupName(QFileInfo(filename).completeBaseName().replace("_", " "));
254 project->setToolTip(CGisListWks::eColumnName, project->getInfo());
255 project->valid = true;
256 }
257 }
258 }
259 }
260