File indexing completed on 2024-04-28 15:39:05
0001 // SPDX-FileCopyrightText: 2020-2023 Tobias Leupold <tl at stonemx dot de> 0002 // 0003 // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0004 0005 // Local includes 0006 0007 #include "GpxEngine.h" 0008 #include "GeoDataModel.h" 0009 #include "Logging.h" 0010 0011 #include "debugMode.h" 0012 0013 // Marble includes 0014 #include <marble/GeoDataCoordinates.h> 0015 0016 // Qt includes 0017 0018 #include <QDebug> 0019 #include <QFile> 0020 #include <QXmlStreamReader> 0021 #include <QJsonDocument> 0022 #include <QStandardPaths> 0023 #include <QFile> 0024 #include <QLoggingCategory> 0025 #include <QTimeZone> 0026 0027 // C++ includes 0028 #include <cmath> 0029 0030 static const auto s_gpx = QStringLiteral("gpx"); 0031 static const auto s_trk = QStringLiteral("trk"); 0032 static const auto s_trkpt = QStringLiteral("trkpt"); 0033 static const auto s_lon = QStringLiteral("lon"); 0034 static const auto s_lat = QStringLiteral("lat"); 0035 static const auto s_ele = QStringLiteral("ele"); 0036 static const auto s_time = QStringLiteral("time"); 0037 static const auto s_trkseg = QStringLiteral("trkseg"); 0038 0039 GpxEngine::GpxEngine(QObject *parent, GeoDataModel *geoDataModel) 0040 : QObject(parent), 0041 m_geoDataModel(geoDataModel) 0042 { 0043 // Load the timezone map image 0044 const auto timezoneMapFile = QStandardPaths::locate(QStandardPaths::AppDataLocation, 0045 QStringLiteral("timezones.png")); 0046 if (! timezoneMapFile.isEmpty()) { 0047 m_timezoneMap = QImage(timezoneMapFile); 0048 if (! m_timezoneMap.isNull()) { 0049 m_timezoneMapWidth = m_timezoneMap.size().width(); 0050 m_timezoneMapHeight = m_timezoneMap.size().height(); 0051 } 0052 qCDebug(KGeoTagLog) << "Loaded the timezones map from" << timezoneMapFile; 0053 } else { 0054 // This should not happen 0055 qCDebug(KGeoTagLog) << "Failed to load the timezones map file!"; 0056 } 0057 0058 // Load the color-timezone mapping 0059 const auto timezoneMappingFile = QStandardPaths::locate(QStandardPaths::AppDataLocation, 0060 QStringLiteral("timezones.json")); 0061 if (! timezoneMappingFile.isEmpty()) { 0062 QFile jsonData(timezoneMappingFile); 0063 if (jsonData.open(QIODevice::ReadOnly | QIODevice::Text)) { 0064 const auto jsonDocument = QJsonDocument::fromJson(jsonData.readAll()); 0065 jsonData.close(); 0066 m_timezoneMapping = jsonDocument.object(); 0067 } 0068 qCDebug(KGeoTagLog) << "Loaded the timezone mapping data from" << timezoneMappingFile; 0069 } else { 0070 // This should not happen 0071 qCDebug(KGeoTagLog) << "Failed to load the timezone mapping data file!"; 0072 } 0073 0074 // Check if all listed timezones are valid 0075 0076 if (! m_timezoneMapping.isEmpty()) { 0077 const auto allTimeZones = QTimeZone::availableTimeZoneIds(); 0078 const auto keys = m_timezoneMapping.keys(); 0079 0080 QVector<QByteArray> invalidIds; 0081 0082 for (const auto &key : keys) { 0083 const auto timeZoneId = m_timezoneMapping.value(key).toString().toUtf8(); 0084 if (! allTimeZones.contains(timeZoneId)) { 0085 invalidIds.append(timeZoneId); 0086 } 0087 } 0088 0089 qCDebug(KGeoTagLog) << "Processed" << m_timezoneMapping.count() << "timezone IDs"; 0090 0091 if (invalidIds.count() > 0) { 0092 qCWarning(KGeoTagLog) << "Found" << invalidIds.count() << "unusable timezone ID(s)!"; 0093 qCWarning(KGeoTagLog) << " The following IDs are not represented in " 0094 << "QTimeZone::availableTimeZoneIds():"; 0095 for (const auto &id : invalidIds) { 0096 qCWarning(KGeoTagLog) << " " << id; 0097 } 0098 } 0099 0100 } else { 0101 // This should not happen 0102 qCWarning(KGeoTagLog) << "Could not load any timezone IDs!"; 0103 } 0104 } 0105 0106 GpxEngine::LoadInfo GpxEngine::load(const QString &path) 0107 { 0108 if (m_geoDataModel->contains(path)) { 0109 return { LoadResult::AlreadyLoaded }; 0110 } 0111 0112 QFile gpxFile(path); 0113 0114 if (! gpxFile.open(QIODevice::ReadOnly | QIODevice::Text)) { 0115 return { LoadResult::OpenFailed }; 0116 } 0117 0118 QXmlStreamReader xml(&gpxFile); 0119 0120 double lon = 0.0; 0121 double lat = 0.0; 0122 double alt = 0.0; 0123 QDateTime time; 0124 0125 QVector<QDateTime> segmentTimes; 0126 QVector<Coordinates> segmentCoordinates; 0127 0128 QVector<QVector<Coordinates>> allSegments; 0129 QVector<QVector<QDateTime>> allSegmentTimes; 0130 0131 bool gpxFound = false; 0132 bool trackStartFound = false; 0133 0134 int tracks = 0; 0135 int segments = 0; 0136 int points = 0; 0137 0138 while (! xml.atEnd()) { 0139 if (xml.hasError()) { 0140 return { LoadResult::XmlError }; 0141 } 0142 0143 const QXmlStreamReader::TokenType token = xml.readNext(); 0144 const QStringRef name = xml.name(); 0145 0146 if (token == QXmlStreamReader::StartElement) { 0147 if (! gpxFound) { 0148 if (name != s_gpx) { 0149 continue; 0150 } else { 0151 gpxFound = true; 0152 } 0153 } 0154 0155 if (! trackStartFound) { 0156 if (name != s_trk) { 0157 continue; 0158 } else { 0159 trackStartFound = true; 0160 tracks++; 0161 } 0162 } 0163 0164 if (name == s_trkseg) { 0165 segments++; 0166 0167 } else if (name == s_trkpt) { 0168 QXmlStreamAttributes attributes = xml.attributes(); 0169 lon = attributes.value(s_lon).toDouble(); 0170 lat = attributes.value(s_lat).toDouble(); 0171 points++; 0172 0173 } else if (name == s_ele) { 0174 xml.readNext(); 0175 alt = xml.text().toDouble(); 0176 0177 } else if (name == s_time) { 0178 xml.readNext(); 0179 time = QDateTime::fromString(xml.text().toString(), Qt::ISODate); 0180 0181 // Strip out milliseconds if the GPX provides them to allow seconds-exact matching 0182 const auto msec = time.time().msec(); 0183 if (msec != 0) { 0184 time = time.addMSecs(msec * -1); 0185 } 0186 } 0187 0188 } else if (token == QXmlStreamReader::EndElement) { 0189 if (name == s_trkpt) { 0190 segmentTimes.append(time); 0191 segmentCoordinates.append(Coordinates(lon, lat, alt, true)); 0192 alt = 0.0; 0193 time = QDateTime(); 0194 0195 } else if (name == s_trkseg && ! segmentCoordinates.isEmpty()) { 0196 allSegmentTimes.append(segmentTimes); 0197 allSegments.append(segmentCoordinates); 0198 segmentTimes.clear(); 0199 segmentCoordinates.clear(); 0200 0201 } else if (name == s_trk) { 0202 trackStartFound = false; 0203 } 0204 } 0205 } 0206 0207 if (! gpxFound) { 0208 return { LoadResult::NoGpxElement, tracks, segments, points }; 0209 } 0210 0211 if (points == 0) { 0212 return { LoadResult::NoGeoData, tracks, segments, points }; 0213 } 0214 0215 // All okay :-) 0216 0217 // Pass the loaded data to the GeoDataModel 0218 m_geoDataModel->addTrack(path, allSegmentTimes, allSegments); 0219 0220 // Detect the presumable timezone the corresponding photos were taken in 0221 0222 // Get the loaded path's bounding box's center point 0223 const auto trackCenter = m_geoDataModel->trackBoxCenter(path); 0224 0225 // Scale the coordinates to the image size, relative to the image center 0226 int mappedLon = std::round(trackCenter.lon() / 180.0 * (m_timezoneMapWidth / 2.0)); 0227 int mappedLat = std::round(trackCenter.lat() / 90.0 * (m_timezoneMapHeight / 2.0)); 0228 0229 // Move the mapped coordinates to the left lower edge 0230 mappedLon = m_timezoneMapWidth / 2 + mappedLon; 0231 mappedLat = m_timezoneMapHeight - (m_timezoneMapHeight / 2 + mappedLat); 0232 0233 // Get the respective pixel's color 0234 const auto timezoneColor = m_timezoneMap.pixelColor(mappedLon, mappedLat).name(); 0235 0236 // Lookup the corresponding timezone 0237 const auto timezoneId = m_timezoneMapping.value(timezoneColor); 0238 if (timezoneId.isString()) { 0239 m_lastDetectedTimeZoneId = timezoneId.toString().toUtf8(); 0240 } else { 0241 m_lastDetectedTimeZoneId.clear(); 0242 } 0243 0244 return { LoadResult::Okay, tracks, segments, points }; 0245 } 0246 0247 void GpxEngine::setMatchParameters(int exactMatchTolerance, int maximumInterpolationInterval, 0248 int maximumInterpolationDistance) 0249 { 0250 m_exactMatchTolerance = exactMatchTolerance; 0251 m_maximumInterpolationInterval = maximumInterpolationInterval; 0252 m_maximumInterpolationDistance = maximumInterpolationDistance; 0253 } 0254 0255 Coordinates GpxEngine::findExactCoordinates(const QDateTime &time, int deviation) const 0256 { 0257 if (deviation == 0) { 0258 return findExactCoordinates(time); 0259 } else { 0260 const QDateTime fixedTime = time.addSecs(deviation); 0261 return findExactCoordinates(fixedTime); 0262 } 0263 } 0264 0265 Coordinates GpxEngine::findExactCoordinates(const QDateTime &time) const 0266 { 0267 // Iterate over all loaded files we have 0268 for (const auto &trackPoints : m_geoDataModel->trackPoints()) { 0269 // Check for an exact match 0270 if (trackPoints.contains(time)) { 0271 return trackPoints.value(time); 0272 } 0273 0274 // Check for a match with +/- the maximum tolerable deviation 0275 for (int i = 1; i <= m_exactMatchTolerance; i++) { 0276 const auto timeBefore = time.addSecs(i * -1); 0277 if (trackPoints.contains(timeBefore)) { 0278 return trackPoints.value(timeBefore); 0279 } 0280 const auto timeAfter = time.addSecs(i); 0281 if (trackPoints.contains(timeAfter)) { 0282 return trackPoints.value(timeAfter); 0283 } 0284 } 0285 } 0286 0287 // No match found 0288 return Coordinates(); 0289 } 0290 0291 Coordinates GpxEngine::findInterpolatedCoordinates(const QDateTime &time, int deviation) const 0292 { 0293 if (deviation == 0) { 0294 return findInterpolatedCoordinates(time); 0295 } else { 0296 const QDateTime fixedTime = time.addSecs(deviation); 0297 return findInterpolatedCoordinates(fixedTime); 0298 } 0299 } 0300 0301 Coordinates GpxEngine::findInterpolatedCoordinates(const QDateTime &time) const 0302 { 0303 // Iterate over all loaded files we have 0304 for (int i = 0; i < m_geoDataModel->dateTimes().count(); i++) { 0305 const auto &dateTimes = m_geoDataModel->dateTimes().at(i); 0306 const auto &trackPoints = m_geoDataModel->trackPoints().at(i); 0307 0308 // This only works if we at least have at least 2 points ;-) 0309 if (dateTimes.count() < 2) { 0310 continue; 0311 } 0312 0313 // If the image's date is before the first or after the last point we have, 0314 // it can't be assigned. 0315 if (time < dateTimes.first() || time > dateTimes.last()) { 0316 continue; 0317 } 0318 0319 // Check for an exact match (without tolerance) 0320 // This also eliminates the case that the time could be the first one. 0321 // We thus can be sure the first entry in dateTimes is earlier than the time requested. 0322 if (dateTimes.contains(time)) { 0323 return trackPoints.value(time); 0324 } 0325 0326 // Search for the first time earlier than the image's 0327 0328 int start = 0; 0329 int end = dateTimes.count() - 1; 0330 int index = 0; 0331 int lastIndex = -1; 0332 0333 while (true) { 0334 index = start + (end - start) / 2; 0335 if (index == lastIndex) { 0336 break; 0337 } 0338 0339 if (dateTimes.at(index) > time) { 0340 end = index; 0341 } else { 0342 start = index; 0343 } 0344 0345 lastIndex = index; 0346 } 0347 0348 // If the found point is the last one, we can't interpolate and use it directly 0349 const auto &closestBefore = dateTimes.at(index); 0350 if (closestBefore == dateTimes.last()) { 0351 return trackPoints.value(closestBefore); 0352 } 0353 0354 // Interpolate between the two coordinates 0355 0356 const auto &closestAfter = dateTimes.at(index + 1); 0357 0358 // Check for a maximum time interval between the points if requested 0359 if (m_maximumInterpolationInterval != -1 0360 && closestBefore.secsTo(closestAfter) > m_maximumInterpolationInterval) { 0361 0362 continue; 0363 } 0364 0365 // Create Marble coordinates from the cache for further calculations 0366 const auto &pointBefore = trackPoints[closestBefore]; 0367 const auto &pointAfter = trackPoints[closestAfter]; 0368 const auto coordinatesBefore = Marble::GeoDataCoordinates( 0369 pointBefore.lon(), pointBefore.lat(), pointBefore.alt(), 0370 Marble::GeoDataCoordinates::Degree); 0371 const auto coordinatesAfter = Marble::GeoDataCoordinates( 0372 pointAfter.lon(), pointAfter.lat(), pointAfter.alt(), 0373 Marble::GeoDataCoordinates::Degree); 0374 0375 // Check for a maximum distance between the points if requested 0376 0377 if (m_maximumInterpolationDistance != -1 0378 && coordinatesBefore.sphericalDistanceTo(coordinatesAfter) * KGeoTag::earthRadius 0379 > m_maximumInterpolationDistance) { 0380 0381 continue; 0382 } 0383 0384 // Calculate an interpolated position between the coordinates 0385 0386 const int secondsBefore = closestBefore.secsTo(time); 0387 const double fraction = double(secondsBefore) 0388 / double(secondsBefore + time.secsTo(closestAfter)); 0389 const auto interpolated = coordinatesBefore.interpolate(coordinatesAfter, fraction); 0390 0391 return Coordinates(interpolated.longitude(Marble::GeoDataCoordinates::Degree), 0392 interpolated.latitude(Marble::GeoDataCoordinates::Degree), 0393 interpolated.altitude(), 0394 true); 0395 } 0396 0397 // No match found 0398 return Coordinates(); 0399 } 0400 0401 QByteArray GpxEngine::lastDetectedTimeZoneId() const 0402 { 0403 return m_lastDetectedTimeZoneId; 0404 } 0405 0406 bool GpxEngine::timeZoneDataLoaded() const 0407 { 0408 return ! m_timezoneMap.isNull() && ! m_timezoneMapping.isEmpty(); 0409 } 0410 0411 QPair<Coordinates, QDateTime> GpxEngine::findClosestTrackPoint(QDateTime time, 0412 int cameraClockDeviation) const 0413 { 0414 if (cameraClockDeviation != 0) { 0415 time = time.addSecs(cameraClockDeviation); 0416 } 0417 0418 auto coordinates = Coordinates(); 0419 auto pointTime = QDateTime(); 0420 auto deviation = -1; 0421 0422 // Iterate over all loaded files we have 0423 for (const auto &trackPoints : m_geoDataModel->trackPoints()) { 0424 if (deviation == -1) { 0425 // Initialize the deviation with the first point 0426 const auto it = trackPoints.constBegin(); 0427 deviation = std::abs(it.key().secsTo(time)); 0428 pointTime = it.key(); 0429 coordinates = it.value(); 0430 } 0431 0432 // Check for an exact match 0433 if (trackPoints.contains(time)) { 0434 return QPair<Coordinates, QDateTime>(trackPoints.value(time), time); 0435 } 0436 0437 // Check for the time deviation of each point 0438 QHash<QDateTime, Coordinates>::const_iterator it; 0439 for (it = trackPoints.constBegin(); it != trackPoints.constEnd(); it++) { 0440 const auto currentDeviaton = std::abs(it.key().secsTo(time)); 0441 if (currentDeviaton < deviation) { 0442 deviation = currentDeviaton; 0443 pointTime = it.key(); 0444 coordinates = it.value(); 0445 } 0446 } 0447 } 0448 0449 return QPair<Coordinates, QDateTime>(coordinates, pointTime); 0450 }