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