File indexing completed on 2025-03-09 03:50:39

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2006-09-19
0007  * Description : GPS data file parser.
0008  *               (GPX format https://www.topografix.com/gpx.asp).
0009  *
0010  * SPDX-FileCopyrightText: 2006-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "geodataparser.h"
0017 #include "geodataparser_time.h"
0018 
0019 // C++ includes
0020 
0021 #include <cmath>
0022 #include <cstdlib>
0023 
0024 // Qt includes
0025 
0026 #include <QDomDocument>
0027 #include <QFile>
0028 #include <QString>
0029 #include <QIODevice>
0030 
0031 // Local includes
0032 
0033 #include "digikam_debug.h"
0034 
0035 namespace DigikamGenericGeolocationEditPlugin
0036 {
0037 
0038 GeoDataParser::GeoDataParser()
0039 {
0040     clear();
0041 }
0042 
0043 void GeoDataParser::clear()
0044 {
0045     m_GeoDataMap.clear();
0046 }
0047 
0048 int GeoDataParser::numPoints() const
0049 {
0050     return m_GeoDataMap.count();
0051 }
0052 
0053 bool GeoDataParser::matchDate(const QDateTime& photoDateTime, int maxGapTime, int secondsOffset,
0054                               bool offsetContainsTimeZone,
0055                               bool interpolate, int interpolationDstTime,
0056                               GeoDataContainer* const gpsData)
0057 {
0058     // GPS device are sync in time by satellite using GMT time.
0059 
0060     QDateTime cameraGMTDateTime = photoDateTime.addSecs(secondsOffset*(-1));
0061 
0062     if (offsetContainsTimeZone)
0063     {
0064         cameraGMTDateTime.setTimeSpec(Qt::UTC);
0065     }
0066 
0067     qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "    photoDateTime: " << photoDateTime << photoDateTime.timeSpec();
0068     qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "cameraGMTDateTime: " << cameraGMTDateTime << cameraGMTDateTime.timeSpec();
0069 
0070     // We are trying to find the right date in the GPS points list.
0071 
0072     bool findItem = false;
0073     int nbSecItem = maxGapTime;
0074     int nbSecs;
0075 
0076     for (GeoDataMap::ConstIterator it = m_GeoDataMap.constBegin() ; it != m_GeoDataMap.constEnd() ; ++it )
0077     {
0078         // Here we check a possible accuracy in seconds between the
0079         // Camera GMT time and the GPS device GMT time.
0080 
0081         nbSecs = qAbs(cameraGMTDateTime.secsTo( it.key() ));
0082 /*
0083         qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << it.key() << cameraGMTDateTime << nbSecs;
0084         qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << it.key().timeSpec() << cameraGMTDateTime.timeSpec() << nbSecs;
0085 */
0086         // We're trying to find the minimal accuracy.
0087 
0088         if ((nbSecs < maxGapTime) && (nbSecs < nbSecItem))
0089         {
0090             if (gpsData)
0091             {
0092                 *gpsData = m_GeoDataMap[it.key()];
0093             }
0094 
0095             findItem  = true;
0096             nbSecItem = nbSecs;
0097         }
0098     }
0099 
0100     if (findItem)
0101         return true;
0102 
0103     // If we can't find it, we will trying to interpolate the GPS point.
0104 
0105     if (interpolate)
0106     {
0107         // The interpolate GPS point will be separate by at the maximum of 'interpolationDstTime'
0108         // seconds before and after the next and previous real GPS point found.
0109 
0110         QDateTime prevDateTime = findPrevDate(cameraGMTDateTime, interpolationDstTime);
0111         QDateTime nextDateTime = findNextDate(cameraGMTDateTime, interpolationDstTime);
0112 
0113         if (!nextDateTime.isNull() && !prevDateTime.isNull())
0114         {
0115             GeoDataContainer prevGPSPoint = m_GeoDataMap[prevDateTime];
0116             GeoDataContainer nextGPSPoint = m_GeoDataMap[nextDateTime];
0117 
0118             double alt1                   = prevGPSPoint.altitude();
0119             double lon1                   = prevGPSPoint.longitude();
0120             double lat1                   = prevGPSPoint.latitude();
0121             uint   t1                     = prevDateTime.toSecsSinceEpoch();
0122             double alt2                   = nextGPSPoint.altitude();
0123             double lon2                   = nextGPSPoint.longitude();
0124             double lat2                   = nextGPSPoint.latitude();
0125             uint   t2                     = nextDateTime.toSecsSinceEpoch();
0126             uint   tCor                   = cameraGMTDateTime.toSecsSinceEpoch();
0127 
0128             if (tCor-t1 != 0)
0129             {
0130                 if (gpsData)
0131                 {
0132                     gpsData->setAltitude(alt1  + (alt2-alt1) * (tCor-t1)/(t2-t1));
0133                     gpsData->setLatitude(lat1  + (lat2-lat1) * (tCor-t1)/(t2-t1));
0134                     gpsData->setLongitude(lon1 + (lon2-lon1) * (tCor-t1)/(t2-t1));
0135                     gpsData->setInterpolated(true);
0136                 }
0137 
0138                 return true;
0139             }
0140         }
0141     }
0142 
0143     return false;
0144 }
0145 
0146 QDateTime GeoDataParser::findNextDate(const QDateTime& dateTime, int secs)
0147 {
0148     // We will find the item in GPS data list where the time is
0149     // at the maximum bigger than 'secs' mn of the value to match.
0150 
0151     QDateTime itemFound = dateTime.addSecs(secs);
0152     bool found          = false;
0153 
0154     for (GeoDataMap::ConstIterator it = m_GeoDataMap.constBegin() ; it != m_GeoDataMap.constEnd() ; ++it)
0155     {
0156         if (it.key() > dateTime)
0157         {
0158             if (it.key() < itemFound)
0159             {
0160                 itemFound = it.key();
0161                 found = true;
0162             }
0163         }
0164     }
0165 
0166     if (found)
0167     {
0168         return itemFound;
0169     }
0170 
0171     return QDateTime();
0172 }
0173 
0174 QDateTime GeoDataParser::findPrevDate(const QDateTime& dateTime, int secs)
0175 {
0176     // We will find the item in GPS data list where the time is
0177     // at the maximum smaller than 'secs' mn of the value to match.
0178 
0179     QDateTime itemFound = dateTime.addSecs((-1)*secs);
0180     bool found          = false;
0181 
0182     for (GeoDataMap::ConstIterator it = m_GeoDataMap.constBegin() ; it != m_GeoDataMap.constEnd() ; ++it)
0183     {
0184         if (it.key() < dateTime)
0185         {
0186             if (it.key() > itemFound)
0187             {
0188                 itemFound = it.key();
0189                 found     = true;
0190             }
0191         }
0192     }
0193 
0194     if (found)
0195     {
0196         return itemFound;
0197     }
0198 
0199     return QDateTime();
0200 }
0201 
0202 bool GeoDataParser::loadGPXFile(const QUrl& url)
0203 {
0204     QFile gpxfile(url.toLocalFile());
0205 
0206     if (!gpxfile.open(QIODevice::ReadOnly))
0207     {
0208         return false;
0209     }
0210 
0211     QDomDocument gpxDoc(QLatin1String("gpx"));
0212 
0213     if (!gpxDoc.setContent(&gpxfile))
0214     {
0215         gpxfile.close();
0216         return false;
0217     }
0218 
0219     QDomElement gpxDocElem = gpxDoc.documentElement();
0220 
0221     if (gpxDocElem.tagName() != QLatin1String("gpx"))
0222     {
0223         gpxfile.close();
0224         return false;
0225     }
0226 
0227     for (QDomNode nTrk = gpxDocElem.firstChild(); !nTrk.isNull(); nTrk = nTrk.nextSibling())
0228     {
0229         QDomElement trkElem = nTrk.toElement();
0230 
0231         if (trkElem.isNull())
0232             continue;
0233 
0234         if (trkElem.tagName() != QLatin1String("trk"))
0235             continue;
0236 
0237         for (QDomNode nTrkseg = trkElem.firstChild() ; !nTrkseg.isNull() ; nTrkseg = nTrkseg.nextSibling())
0238         {
0239             QDomElement trksegElem = nTrkseg.toElement();
0240 
0241             if (trksegElem.isNull())
0242                 continue;
0243 
0244             if (trksegElem.tagName() != QLatin1String("trkseg"))
0245                 continue;
0246 
0247             for (QDomNode nTrkpt = trksegElem.firstChild() ; !nTrkpt.isNull() ; nTrkpt = nTrkpt.nextSibling())
0248             {
0249                 QDomElement trkptElem = nTrkpt.toElement();
0250 
0251                 if (trkptElem.isNull())
0252                     continue;
0253 
0254                 if (trkptElem.tagName() != QLatin1String("trkpt"))
0255                     continue;
0256 
0257                 QDateTime ptDateTime;
0258                 double    ptAltitude  = 0.0;
0259                 double    ptLatitude  = 0.0;
0260                 double    ptLongitude = 0.0;
0261 
0262                 // Get GPS position. If not available continue to next point.
0263 
0264                 QString lat = trkptElem.attribute(QLatin1String("lat"));
0265                 QString lon = trkptElem.attribute(QLatin1String("lon"));
0266 
0267                 if (lat.isEmpty() || lon.isEmpty())
0268                     continue;
0269 
0270                 ptLatitude  = lat.toDouble();
0271                 ptLongitude = lon.toDouble();
0272 
0273                 // Get metadata of track point (altitude and time stamp)
0274 
0275                 for (QDomNode nTrkptMeta = trkptElem.firstChild() ; !nTrkptMeta.isNull() ; nTrkptMeta = nTrkptMeta.nextSibling())
0276                 {
0277                     QDomElement trkptMetaElem = nTrkptMeta.toElement();
0278 
0279                     if (trkptMetaElem.isNull())
0280                         continue;
0281 
0282                     if (trkptMetaElem.tagName() == QLatin1String("time"))
0283                     {
0284                         // Get GPS point time stamp. If not available continue to next point.
0285 
0286                         const QString time = trkptMetaElem.text();
0287 
0288                         if (time.isEmpty())
0289                             continue;
0290 
0291                         ptDateTime = GeoDataParserParseTime(time);
0292                     }
0293 
0294                     if (trkptMetaElem.tagName() == QLatin1String("ele"))
0295                     {
0296                         // Get GPS point altitude. If not available continue to next point.
0297 
0298                         QString ele = trkptMetaElem.text();
0299 
0300                         if (!ele.isEmpty())
0301                         {
0302                             ptAltitude  = ele.toDouble();
0303                         }
0304                     }
0305                 }
0306 
0307                 if (ptDateTime.isNull())
0308                 {
0309                     continue;
0310                 }
0311 
0312                 GeoDataContainer gpsData(ptAltitude, ptLatitude, ptLongitude, false);
0313                 m_GeoDataMap.insert( ptDateTime, gpsData );
0314             }
0315         }
0316     }
0317 /*
0318     qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "GPX File " << url.fileName()
0319                                          << " parsed with " << numPoints()
0320                                          << " points extracted" ;
0321 */
0322     gpxfile.close();
0323 
0324     return true;
0325 }
0326 
0327 } // namespace DigikamGenericGeolocationEditPlugin