File indexing completed on 2024-04-28 15:39:05
0001 // SPDX-FileCopyrightText: 2020-2022 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 #include "ElevationEngine.h" 0007 #include "Settings.h" 0008 #include "Coordinates.h" 0009 0010 // KDE includes 0011 #include <KLocalizedString> 0012 0013 // Qt includes 0014 #include <QNetworkAccessManager> 0015 #include <QNetworkReply> 0016 #include <QDebug> 0017 #include <QJsonDocument> 0018 #include <QJsonObject> 0019 #include <QJsonArray> 0020 #include <QTimer> 0021 0022 // C++ includes 0023 #include <functional> 0024 0025 // opentopodata.org query restrictions 0026 static constexpr int s_maximumLocations = 100; 0027 static constexpr int s_msToNextRequest = 1000; 0028 0029 ElevationEngine::ElevationEngine(QObject *parent, Settings *settings) 0030 : QObject(parent), 0031 m_settings(settings) 0032 { 0033 m_manager = new QNetworkAccessManager(this); 0034 connect(m_manager, &QNetworkAccessManager::finished, this, &ElevationEngine::processReply); 0035 0036 m_requestTimer = new QTimer(this); 0037 m_requestTimer->setSingleShot(true); 0038 m_requestTimer->setInterval(s_msToNextRequest); 0039 connect(m_requestTimer, &QTimer::timeout, this, &ElevationEngine::processNextRequest); 0040 } 0041 0042 void ElevationEngine::request(ElevationEngine::Target target, const QVector<QString> &ids, 0043 const QVector<Coordinates> &coordinates) 0044 { 0045 // Check if we want to lookup different coordinates 0046 bool identicalCoordinates = true; 0047 if (coordinates.count() > 1) { 0048 const auto &firstCoordinates = coordinates.first(); 0049 for (int i = 1; i < coordinates.count(); i++) { 0050 if (coordinates.at(i) != firstCoordinates) { 0051 identicalCoordinates = false; 0052 break; 0053 } 0054 } 0055 } 0056 0057 if (identicalCoordinates) { 0058 // Add a request for one location 0059 m_queuedTargets.append(target); 0060 m_queuedIds.append(ids); 0061 const auto &firstCoordinates = coordinates.first(); 0062 m_queuedLocations.append(QStringLiteral("%1,%2").arg( 0063 QString::number(firstCoordinates.lat()), 0064 QString::number(firstCoordinates.lon()))); 0065 } else { 0066 // Create clusters of locations with at most s_maximumLocations locations per cluster 0067 0068 QStringList locations; 0069 for (const auto &singleCoordinate : coordinates) { 0070 locations.append(QStringLiteral("%1,%2").arg(QString::number(singleCoordinate.lat()), 0071 QString::number(singleCoordinate.lon()))); 0072 } 0073 0074 // Group all requested coordinates to groups with at most s_maximumLocations entries 0075 int start = 0; 0076 while (start < ids.count()) { 0077 m_queuedTargets.append(target); 0078 m_queuedIds.append(ids.mid(start, s_maximumLocations)); 0079 m_queuedLocations.append( 0080 locations.mid(start, s_maximumLocations).join(QLatin1String("|"))); 0081 start += s_maximumLocations; 0082 } 0083 } 0084 0085 processNextRequest(); 0086 } 0087 0088 void ElevationEngine::processNextRequest() 0089 { 0090 if (m_queuedTargets.isEmpty()) { 0091 // Nothing to do. 0092 // This happens because m_requestTimer always calls this after being finished. 0093 return; 0094 } 0095 0096 if (m_requestTimer->isActive()) { 0097 // Pending request, we can't currently post another one. 0098 // m_requestTimer will call this again after having waited for s_msToNextRequest. 0099 return; 0100 } 0101 0102 auto *reply = m_manager->get(QNetworkRequest(QUrl( 0103 QStringLiteral("https://api.opentopodata.org/v1/%1?locations=%2").arg( 0104 m_settings->elevationDataset(), m_queuedLocations.takeFirst())))); 0105 m_requests.insert(reply, { m_queuedTargets.takeFirst(), m_queuedIds.takeFirst() }); 0106 QTimer::singleShot(3000, this, std::bind(&ElevationEngine::cleanUpRequest, this, reply)); 0107 0108 // Block the next request (checking) 0109 m_requestTimer->start(); 0110 } 0111 0112 void ElevationEngine::removeRequest(QNetworkReply *request) 0113 { 0114 m_requests.remove(request); 0115 request->deleteLater(); 0116 } 0117 0118 void ElevationEngine::cleanUpRequest(QNetworkReply *request) 0119 { 0120 if (m_requests.contains(request)) { 0121 request->abort(); 0122 Q_EMIT lookupFailed(i18n("The request timed out")); 0123 m_requests.remove(request); 0124 } 0125 } 0126 0127 void ElevationEngine::processReply(QNetworkReply *request) 0128 { 0129 if (! request->isOpen()) { 0130 // This happens if the request has been aborted by the cleanup timer 0131 return; 0132 } 0133 0134 const auto [ target, ids ] = m_requests.value(request); 0135 removeRequest(request); 0136 0137 const auto requestData = request->readAll(); 0138 QJsonParseError error; 0139 const auto json = QJsonDocument::fromJson(requestData, &error); 0140 if (error.error != QJsonParseError::NoError || ! json.isObject()) { 0141 Q_EMIT lookupFailed(i18n("Could not parse the server's response: Failed to create a JSON " 0142 "document.</p>" 0143 "<p>The error's description was: %1</p>" 0144 "<p>The literal response was:</p>" 0145 "<p><kbd>%2</kbd>", error.errorString(), 0146 QString::fromLocal8Bit(requestData))); 0147 return; 0148 } 0149 0150 const auto object = json.object(); 0151 const auto statusValue = object.value(QStringLiteral("status")); 0152 if (statusValue.isUndefined()) { 0153 Q_EMIT lookupFailed(i18n("Could not parse the server's response: Could not read the status " 0154 "value")); 0155 } 0156 0157 const auto statusString = statusValue.toString(); 0158 if (statusString != QStringLiteral("OK")) { 0159 const auto errorValue = object.value(QStringLiteral("error")); 0160 const auto errorString = errorValue.isUndefined() 0161 ? i18n("Could not read error description") : errorValue.toString(); 0162 Q_EMIT lookupFailed(i18nc("A server error status followed by the error description", 0163 "%1: %2", statusString, errorString)); 0164 return; 0165 } 0166 0167 const auto resultsValue = object.value(QStringLiteral("results")); 0168 if (! resultsValue.isArray()) { 0169 Q_EMIT lookupFailed(i18n("Could not parse the server's response: Could not read the " 0170 "results array")); 0171 return; 0172 } 0173 0174 const auto resultsArray = resultsValue.toArray(); 0175 bool allPresent = true; 0176 QVector<double> elevations; 0177 for (const auto &result : resultsArray) { 0178 const auto elevation = result.toObject().value(QStringLiteral("elevation")); 0179 if (elevation.isUndefined()) { 0180 Q_EMIT lookupFailed(i18n("Could not parse the server's response: Could not read the " 0181 "elevation value")); 0182 return; 0183 } else if (elevation.isNull()) { 0184 allPresent = false; 0185 } 0186 elevations.append(elevation.toDouble()); 0187 } 0188 0189 const int originalElevationsCount = elevations.count(); 0190 if (ids.count() > 1 && originalElevationsCount == 1) { 0191 // Same coordinates requested multiple times 0192 const auto coordinates = elevations.first(); 0193 for (int i = 0; i < ids.count() - 1; i++) { 0194 elevations.append(coordinates); 0195 } 0196 } 0197 0198 Q_EMIT elevationProcessed(target, ids, elevations); 0199 0200 if (! allPresent) { 0201 Q_EMIT notAllPresent(ids.count(), originalElevationsCount); 0202 } 0203 }