File indexing completed on 2025-01-05 03:58:35

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2006-09-19
0007  * Description : Track file reader
0008  *
0009  * SPDX-FileCopyrightText: 2006-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0010  * SPDX-FileCopyrightText: 2010-2014 by Michael G. Hansen <mike at mghansen dot de>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "trackreader.h"
0017 
0018 // Qt includes
0019 
0020 #include <QFile>
0021 #include <QXmlStreamAttributes>
0022 
0023 // KDE includes
0024 
0025 #include <klocalizedstring.h>
0026 
0027 namespace Digikam
0028 {
0029 
0030 class Q_DECL_HIDDEN TrackReader::Private
0031 {
0032 public:
0033 
0034     explicit Private()
0035         : fileData (nullptr)
0036     {
0037     }
0038 
0039     TrackReadResult* fileData;
0040 };
0041 
0042 TrackReader::TrackReader(TrackReadResult* const dataTarget)
0043     : QObject(nullptr),
0044       d      (new Private)
0045 {
0046     d->fileData = dataTarget;
0047 }
0048 
0049 TrackReader::~TrackReader()
0050 {
0051 }
0052 
0053 QDateTime TrackReader::ParseTime(const QString& tstring)
0054 {
0055     QString timeString = tstring;
0056 
0057     if (timeString.isEmpty())
0058     {
0059         return QDateTime();
0060     }
0061 
0062     // we want to be able to parse these formats:
0063     // "2010-01-14T09:26:02.287-02:00" <-- here we have to cut off the -02:00 and replace it with 'Z'
0064     // "2010-01-14T09:26:02.287+02:00" <-- here we have to cut off the +02:00 and replace it with 'Z'
0065     // "2009-03-11T13:39:55.622Z"
0066 
0067     const int timeStringLength      = timeString.length();
0068     const int timeZoneSignPosition  = timeStringLength-6;
0069 
0070     // does the string contain a timezone offset?
0071 
0072     int timeZoneOffsetSeconds       = 0;
0073     const int timeZonePlusPosition  = timeString.lastIndexOf(QLatin1Char('+'));
0074     const int timeZoneMinusPosition = timeString.lastIndexOf(QLatin1Char('-'));
0075 
0076     if ((timeZonePlusPosition  == timeZoneSignPosition)   ||
0077         (timeZoneMinusPosition == timeZoneSignPosition))
0078     {
0079         const int timeZoneSign       = (timeZonePlusPosition == timeZoneSignPosition) ? +1 : -1;
0080 
0081         // cut off the last digits:
0082 
0083         const QString timeZoneString = timeString.right(6);
0084         timeString.chop(6);
0085         timeString                  += QLatin1Char('Z');
0086 
0087         // determine the time zone offset:
0088 
0089         bool okayHour                = false;
0090         bool okayMinute              = false;
0091         const int hourOffset         = timeZoneString.mid(1, 2).toInt(&okayHour);
0092         const int minuteOffset       = timeZoneString.mid(4, 2).toInt(&okayMinute);
0093 
0094         if (okayHour&&okayMinute)
0095         {
0096             timeZoneOffsetSeconds  = hourOffset*3600 + minuteOffset*60;
0097             timeZoneOffsetSeconds *= timeZoneSign;
0098         }
0099     }
0100 
0101     QDateTime theTime = QDateTime::fromString(timeString, Qt::ISODate);
0102     theTime           = theTime.addSecs(-timeZoneOffsetSeconds);
0103 
0104     return theTime;
0105 }
0106 
0107 void TrackReader::parseTrack(QXmlStreamReader& xml)
0108 {
0109     /* check that really getting a track. */
0110 
0111     if ((xml.tokenType() != QXmlStreamReader::StartElement) &&
0112         (xml.name()      == QLatin1String("trkpt")))
0113     {
0114         return;
0115     }
0116 
0117     TrackManager::TrackPoint currentDataPoint;
0118 
0119     /* get first the attributes for track points */
0120 
0121     QXmlStreamAttributes attributes = xml.attributes();
0122 
0123     /* check that track has lat or lon attribute. */
0124 
0125     if (attributes.hasAttribute(QLatin1String("lat")) &&
0126         attributes.hasAttribute(QLatin1String("lon")))
0127     {
0128         currentDataPoint.coordinates.setLatLon(attributes.value(QLatin1String("lat")).toDouble(),
0129                                                attributes.value(QLatin1String("lon")).toDouble());
0130     }
0131 
0132     /* Next element... */
0133 
0134     xml.readNext();
0135 
0136     while (!((xml.tokenType() == QXmlStreamReader::EndElement) &&
0137              (xml.name()      == QLatin1String("trkpt")))      && !xml.hasError())
0138     {
0139         if (xml.tokenType() == QXmlStreamReader::StartElement)
0140         {
0141             QString eText = xml.readElementText(QXmlStreamReader::IncludeChildElements);
0142 
0143             if      (xml.name() == QLatin1String("time"))
0144             {
0145                 currentDataPoint.dateTime = ParseTime(eText.trimmed());
0146             }
0147             else if (xml.name() == QLatin1String("sat"))
0148             {
0149                 bool okay       = false;
0150                 int nSatellites = eText.toInt(&okay);
0151 
0152                 if (okay && (nSatellites >= 0))
0153                 {
0154                     currentDataPoint.nSatellites = nSatellites;
0155                 }
0156             }
0157             else if (xml.name() == QLatin1String("hdop"))
0158             {
0159                 bool okay  = false;
0160                 qreal hDop = eText.toDouble(&okay);
0161 
0162                 if (okay)
0163                 {
0164                     currentDataPoint.hDop = hDop;
0165                 }
0166             }
0167             else if (xml.name() == QLatin1String("pdop"))
0168             {
0169                 bool okay  = false;
0170                 qreal pDop = eText.toDouble(&okay);
0171 
0172                 if (okay)
0173                 {
0174                     currentDataPoint.pDop = pDop;
0175                 }
0176             }
0177             else if (xml.name() == QLatin1String("fix"))
0178             {
0179                 if      (eText == QLatin1String("2d"))
0180                 {
0181                     currentDataPoint.fixType = 2;
0182                 }
0183                 else if (eText == QLatin1String("3d"))
0184                 {
0185                     currentDataPoint.fixType = 3;
0186                 }
0187             }
0188             else if (xml.name() == QLatin1String("ele"))
0189             {
0190                 bool haveAltitude = false;
0191                 const qreal alt   = eText.toDouble(&haveAltitude);
0192 
0193                 if (haveAltitude)
0194                 {
0195                     currentDataPoint.coordinates.setAlt(alt);
0196                 }
0197             }
0198             else if (xml.name() == QLatin1String("speed"))
0199             {
0200                 bool haveSpeed    = false;
0201                 const qreal speed = eText.toDouble(&haveSpeed);
0202 
0203                 if (haveSpeed)
0204                 {
0205                     currentDataPoint.speed = speed;
0206                 }
0207             }
0208         }
0209 
0210         /* ...and next... */
0211 
0212         xml.readNext();
0213     }
0214 
0215     if (currentDataPoint.dateTime.isValid() && currentDataPoint.coordinates.hasCoordinates())
0216     {
0217         d->fileData->track.points << currentDataPoint;
0218     }
0219 }
0220 
0221 TrackReader::TrackReadResult TrackReader::loadTrackFile(const QUrl& url)
0222 {
0223     // TODO: store some kind of error message
0224 
0225     TrackReadResult parsedData;
0226     TrackReader trackReader(&parsedData);
0227     parsedData.track.url = url;
0228     parsedData.isValid   = false;
0229 
0230     QFile file(url.toLocalFile());
0231 
0232     if (!file.open(QFile::ReadOnly | QFile::Text))
0233     {
0234         parsedData.loadError = i18n("Could not open: %1", file.errorString());
0235 
0236         return parsedData;
0237     }
0238 
0239     if (file.size() == 0)
0240     {
0241         parsedData.loadError = i18n("File is empty.");
0242 
0243         return parsedData;
0244     }
0245 
0246     QXmlStreamReader XmlReader(&file);
0247 
0248     while (!XmlReader.atEnd() && !XmlReader.hasError() )
0249     {
0250         QXmlStreamReader::TokenType token = XmlReader.readNext();
0251 
0252         if (token == QXmlStreamReader::StartElement)
0253         {
0254             if (XmlReader.name().toString() != QLatin1String("trkpt"))
0255             {
0256                 continue;
0257             }
0258             else
0259             {
0260                 trackReader.parseTrack(XmlReader);
0261             }
0262         }
0263     }
0264 
0265     // error not well formed XML file
0266 
0267     parsedData.isValid = !XmlReader.hasError() ;
0268 
0269     if (!parsedData.isValid)
0270     {
0271         if (XmlReader.error() == QXmlStreamReader::NotWellFormedError)
0272         {
0273             parsedData.loadError = i18n("Probably not a GPX file.");
0274         }
0275 
0276         return parsedData;
0277     }
0278 
0279     // error no track points
0280 
0281     parsedData.isValid = !parsedData.track.points.isEmpty();
0282 
0283     if (!parsedData.isValid)
0284     {
0285         parsedData.loadError = i18n("File is a GPX file, but no track points "
0286                                     "with valid timestamps were found.");
0287 
0288         return parsedData;
0289     }
0290 
0291     // The correlation algorithm relies on sorted data, therefore sort now
0292 
0293     std::sort(parsedData.track.points.begin(),
0294               parsedData.track.points.end(),
0295               TrackManager::TrackPoint::EarlierThan);
0296 
0297     return parsedData;
0298 }
0299 
0300 } // namespace Digikam
0301 
0302 #include "moc_trackreader.cpp"