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/CSmlProject.h"
22 #include "gis/suunto/ISuuntoProject.h"
23 #include "gis/trk/CGisItemTrk.h"
24 #include <QtWidgets>
25
26
27 const QList<extension_t> CSmlProject::extensions =
28 {
29 {"Latitude", RAD_TO_DEG, 0.0, ASSIGN_VALUE(lat, NIL)} // unit [°]
30 , {"Longitude", RAD_TO_DEG, 0.0, ASSIGN_VALUE(lon, NIL)} // unit [°]
31 , {"Altitude", 1.0, 0.0, ASSIGN_VALUE(ele, NIL)} // unit [m]
32 , {"VerticalSpeed", 1.0, 0.0, ASSIGN_VALUE(extensions["gpxdata:verticalSpeed"], NIL)} // unit [m/h]
33 , {"HR", 60.0, 0.0, ASSIGN_VALUE(extensions["gpxtpx:TrackPointExtension|gpxtpx:hr"], qRound)} // unit [bpm]
34 , {"Cadence", 60.0, 0.0, ASSIGN_VALUE(extensions["gpxdata:cadence"], NIL)} // unit [bpm]
35 , {"Temperature", 1.0, -273.15, ASSIGN_VALUE(extensions["gpxdata:temp"], NIL)} // unit [°C]
36 , {"SeaLevelPressure", 0.01, 0.0, ASSIGN_VALUE(extensions["gpxdata:seaLevelPressure"], NIL)} // unit [hPa]
37 , {"Speed", 1.0, 0.0, ASSIGN_VALUE(extensions["gpxdata:speed"], NIL)} // unit [m/s]
38 , {"EnergyConsumption", 60.0 / 4184.0, 0.0, ASSIGN_VALUE(extensions["gpxdata:energy"], NIL)} // unit [kCal/min]
39 };
40
41
42
CSmlProject(const QString & filename,CGisListWks * parent)43 CSmlProject::CSmlProject(const QString& filename, CGisListWks* parent)
44 : ISuuntoProject(eTypeSml, filename, parent)
45 {
46 setIcon(CGisListWks::eColumnIcon, QIcon("://icons/32x32/SmlProject.png"));
47 blockUpdateItems(true);
48 loadSml(filename);
49 blockUpdateItems(false);
50 setupName(QFileInfo(filename).completeBaseName().replace("_", " "));
51 }
52
53
loadSml(const QString & filename)54 void CSmlProject::loadSml(const QString& filename)
55 {
56 try
57 {
58 loadSml(filename, this);
59 }
60 catch(QString& errormsg)
61 {
62 QMessageBox::critical(CMainWindow::getBestWidgetForParent(),
63 tr("Failed to load file %1...").arg(filename), errormsg, QMessageBox::Abort);
64 valid = false;
65 }
66 }
67
68
loadSml(const QString & filename,CSmlProject * project)69 void CSmlProject::loadSml(const QString& filename, CSmlProject* project)
70 {
71 QFile file(filename);
72
73 // if the file does not exist, the filename is assumed to be a name for a new project
74 if (!file.exists() || QFileInfo(filename).suffix().toLower() != "sml")
75 {
76 project->filename.clear();
77 project->setupName(filename);
78 project->setToolTip(CGisListWks::eColumnName, project->getInfo());
79 project->valid = true;
80 return;
81 }
82
83 if (!file.open(QIODevice::ReadOnly))
84 {
85 throw tr("Failed to open %1").arg(filename);
86 }
87
88 // load file content to xml document
89 QDomDocument xml;
90 QString msg;
91 int line;
92 int column;
93 if (!xml.setContent(&file, false, &msg, &line, &column))
94 {
95 file.close();
96 throw tr("Failed to read: %1\nline %2, column %3:\n %4").arg(filename).arg(line).arg(column).arg(msg);
97 }
98 file.close();
99
100 QDomElement xmlSml = xml.documentElement();
101 if (xmlSml.tagName() != "sml")
102 {
103 throw tr("Not an sml file: %1").arg(filename);
104 }
105
106 if(xmlSml.namedItem("DeviceLog").isElement())
107 {
108 CTrackData trk;
109 QDateTime time0; // start time of the track
110
111 const QDomNode& xmlDeviceLog = xmlSml.namedItem("DeviceLog");
112 if(xmlDeviceLog.namedItem("Header").isElement())
113 {
114 const QDomNode& xmlHeader = xmlDeviceLog.namedItem("Header");
115 if(xmlHeader.namedItem("DateTime").isElement())
116 {
117 QString dateTimeStr = xmlHeader.namedItem("DateTime").toElement().text();
118 trk.name = dateTimeStr; // date (in local time) of beginning of recording is chosen as track name
119 IUnit::parseTimestamp(dateTimeStr, time0); // as local time
120 }
121
122 if(xmlHeader.namedItem("Activity").isElement())
123 {
124 trk.desc = xmlHeader.namedItem("Activity").toElement().text();
125 }
126
127 if(xmlHeader.namedItem("RecoveryTime").isElement())
128 {
129 trk.cmt = tr("Recovery time: %1 h<br/>").arg(xmlHeader.namedItem("RecoveryTime").toElement().text().toInt() / 3600);
130 }
131
132 if(xmlHeader.namedItem("PeakTrainingEffect").isElement())
133 {
134 trk.cmt += tr("Peak Training Effect: %1<br/>").arg(xmlHeader.namedItem("PeakTrainingEffect").toElement().text().toDouble());
135 }
136
137 if(xmlHeader.namedItem("Energy").isElement())
138 {
139 trk.cmt += tr("Energy: %1 kCal<br/>").arg((int)xmlHeader.namedItem("Energy").toElement().text().toDouble() / 4184);
140 }
141
142
143 if(xmlHeader.namedItem("BatteryChargeAtStart").isElement() &&
144 xmlHeader.namedItem("BatteryCharge").isElement() &&
145 xmlHeader.namedItem("Duration").isElement() )
146
147 {
148 trk.cmt += tr("Battery usage: %1 %/hour")
149 .arg( 100 * (xmlHeader.namedItem("BatteryChargeAtStart").toElement().text().toDouble()
150 - xmlHeader.namedItem("BatteryCharge").toElement().text().toDouble())
151 / (xmlHeader.namedItem("Duration").toElement().text().toDouble() / 3600), 0, 'f', 1);
152 }
153 }
154
155 if(xmlDeviceLog.namedItem("Device").isElement())
156 {
157 const QDomNode& xmlDevice = xmlDeviceLog.namedItem("Device");
158 if(xmlDevice.namedItem("Name").isElement())
159 {
160 trk.cmt = tr("Device: %1<br/>").arg(xmlDevice.namedItem("Name").toElement().text()) + trk.cmt;
161 }
162 }
163
164 if(xmlDeviceLog.namedItem("Samples").isElement())
165 {
166 const QDomNode& xmlSamples = xmlDeviceLog.namedItem("Samples");
167 const QDomNodeList& xmlSampleList = xmlSamples.toElement().elementsByTagName("Sample");
168
169 if (xmlSampleList.count() > 0)
170 {
171 bool UTCtimeFound = false;
172 for (int i = 0; i < xmlSampleList.count(); i++) // browse XML samples
173 { //look for samples with UTC timestamp
174 const QDomNode& xmlSample = xmlSampleList.item(i);
175
176 if (xmlSample.namedItem("UTC").isElement())
177 {
178 QString timeStr = xmlSample.namedItem("UTC").toElement().text();
179
180 if (timeStr.indexOf("Z") != NOIDX) // "Z" means "UTC timestamp" ; note the even this element is <UTC>, this does not mean that time is expressed as UTC
181 {
182 if(xmlSample.namedItem("Time").isElement())
183 {
184 IUnit::parseTimestamp(timeStr, time0);
185 time0 = time0.addMSecs(-xmlSample.namedItem("Time").toElement().text().toDouble() * 1000.0 ); // substract current sample time to get start time
186 UTCtimeFound = true;
187 break;
188 }
189 }
190 }
191 }
192
193 if ( !UTCtimeFound)
194 {
195 QMessageBox::warning(CMainWindow::getBestWidgetForParent(), tr("Use of local time...")
196 , tr("No UTC time has been found in file %1. "
197 "Local computer time will be used. "
198 "You can adjust time using a time filter if needed.").arg(filename)
199 , QMessageBox::Ok);
200 }
201
202 bool sampleWithPositionFound = false;
203 QList<sample_t> samplesList;
204 QList<QDateTime> lapsList;
205
206 for (int i = 0; i < xmlSampleList.count(); i++) // browse XML samples
207 {
208 sample_t sample;
209 const QDomNode& xmlSample = xmlSampleList.item(i);
210
211 if(xmlSample.namedItem("Latitude").isElement())
212 {
213 sampleWithPositionFound = true;
214 }
215
216 if(xmlSample.namedItem("Time").isElement())
217 {
218 sample.time = time0.addMSecs(xmlSample.namedItem("Time").toElement().text().toDouble() * 1000.0);
219 }
220
221 if(xmlSample.namedItem("Events").isElement())
222 {
223 const QDomNode& xmlEvents = xmlSample.namedItem("Events");
224 if(xmlEvents.namedItem("Lap").isElement())
225 {
226 lapsList << sample.time; // stores timestamps of the samples where the the "Lap" button has been pressed
227 }
228 }
229 else // samples without "Events" are the ones containing position, heart rate, etc... that we want to store
230 {
231 for (const extension_t& ext : extensions)
232 {
233 if (xmlSample.namedItem(ext.tag).isElement())
234 {
235 const QDomNode& xmlSampleData = xmlSample.namedItem(ext.tag);
236 sample[ext.tag] = xmlSampleData.toElement().text().toDouble() * ext.scale + ext.offset;
237 }
238 }
239 samplesList << sample;
240 }
241 }
242
243 if (!sampleWithPositionFound)
244 {
245 throw tr("This SML 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
251 new CGisItemTrk(trk, project);
252
253 project->sortItems();
254 project->setupName(QFileInfo(filename).completeBaseName().replace("_", " "));
255 project->setToolTip(CGisListWks::eColumnName, project->getInfo());
256 project->valid = true;
257 }
258 }
259 }
260 }
261