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