File indexing completed on 2024-06-02 04:45:46
0001 /* 0002 SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "tilecache_p.h" 0008 #include "logging.h" 0009 0010 #include <osm/datatypes.h> 0011 #include <osm/geomath.h> 0012 0013 #include <QCoreApplication> 0014 #include <QDir> 0015 #include <QDirIterator> 0016 #include <QFile> 0017 #include <QFileInfo> 0018 #include <QNetworkAccessManager> 0019 #include <QNetworkReply> 0020 #include <QStandardPaths> 0021 #include <QUrl> 0022 0023 #include <cmath> 0024 0025 using namespace KOSMIndoorMap; 0026 0027 enum { 0028 DefaultCacheDays = 14, 0029 }; 0030 0031 Tile Tile::fromCoordinate(double lat, double lon, uint8_t z) 0032 { 0033 Tile t; 0034 t.x = std::floor((lon + 180.0) / 360.0 * (1 << z)); 0035 const auto latrad = OSM::degToRad(lat); 0036 t.y = std::floor((1.0 - std::asinh(std::tan(latrad)) / M_PI) / 2.0 * (1 << z)); 0037 t.z = z; 0038 return t; 0039 } 0040 0041 OSM::Coordinate Tile::topLeft() const 0042 { 0043 const auto lon = x / (double)(1 << z) * 360.0 - 180.0; 0044 0045 const auto n = M_PI - 2.0 * M_PI * y / (double)(1 << z); 0046 const auto lat = OSM::radToDeg(std::atan(0.5 * (std::exp(n) - std::exp(-n)))); 0047 0048 return OSM::Coordinate(lat, lon); 0049 } 0050 0051 OSM::BoundingBox Tile::boundingBox() const 0052 { 0053 Tile bottomRight = *this; 0054 ++bottomRight.x; 0055 ++bottomRight.y; 0056 0057 const auto tl = topLeft(); 0058 const auto br = bottomRight.topLeft(); 0059 0060 return OSM::BoundingBox(OSM::Coordinate(br.latitude, tl.longitude), OSM::Coordinate(tl.latitude, br.longitude)); 0061 } 0062 0063 Tile Tile::topLeftAtZ(uint8_t z) const 0064 { 0065 if (z == this->z) { 0066 return *this; 0067 } 0068 if (z < this->z) { 0069 return Tile{ x / (1 << (this->z - z)), y / (1 << (this->z - z)), z}; 0070 } 0071 return Tile{ x * (1 << (z - this->z )), y * (1 << (z - this->z)), z}; 0072 } 0073 0074 Tile Tile::bottomRightAtZ(uint8_t z) const 0075 { 0076 if (z <= this->z) { 0077 return topLeftAtZ(z); 0078 } 0079 const auto deltaZ = z - this->z; 0080 const auto deltaWidth = 1 << deltaZ; 0081 return Tile{ x * deltaWidth + deltaWidth - 1, y * deltaWidth + deltaWidth - 1, z}; 0082 } 0083 0084 TileCache::TileCache(QObject *parent) 0085 : QObject(parent) 0086 , m_nam(new QNetworkAccessManager(this)) 0087 { 0088 m_nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); 0089 m_nam->enableStrictTransportSecurityStore(true, QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/org.kde.osm/hsts/")); 0090 m_nam->setStrictTransportSecurityEnabled(true); 0091 } 0092 0093 TileCache::~TileCache() = default; 0094 0095 QString TileCache::cachedTile(Tile tile) const 0096 { 0097 const auto p = cachePath(tile); 0098 if (QFile::exists(p)) { 0099 return p; 0100 } 0101 return {}; 0102 } 0103 0104 void TileCache::ensureCached(Tile tile) 0105 { 0106 const auto t = cachedTile(tile); 0107 if (t.isEmpty()) { 0108 downloadTile(tile); 0109 return; 0110 } 0111 0112 if (tile.ttl.isValid()) { 0113 updateTtl(t, tile.ttl); 0114 } 0115 } 0116 0117 void TileCache::downloadTile(Tile tile) 0118 { 0119 m_pendingDownloads.push_back(tile); 0120 downloadNext(); 0121 } 0122 0123 QString TileCache::cachePath(Tile tile) const 0124 { 0125 QString base; 0126 if (!qEnvironmentVariableIsSet("KOSMINDOORMAP_CACHE_PATH")) { 0127 base = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) 0128 + QLatin1String("/org.kde.osm/vectorosm/"); 0129 } else { 0130 base = qEnvironmentVariable("KOSMINDOORMAP_CACHE_PATH"); 0131 } 0132 0133 return base 0134 + QString::number(tile.z) + QLatin1Char('/') 0135 + QString::number(tile.x) + QLatin1Char('/') 0136 + QString::number(tile.y) + QLatin1String(".o5m"); 0137 } 0138 0139 void TileCache::downloadNext() 0140 { 0141 if (m_output.isOpen() || m_pendingDownloads.empty()) { 0142 return; 0143 } 0144 0145 const auto tile = m_pendingDownloads.front(); 0146 m_pendingDownloads.pop_front(); 0147 0148 QFileInfo fi(cachePath(tile)); 0149 QDir().mkpath(fi.absolutePath()); 0150 m_output.setFileName(fi.absoluteFilePath() + QLatin1String(".part")); 0151 if (!m_output.open(QFile::WriteOnly)) { 0152 qCWarning(Log) << m_output.fileName() << m_output.errorString(); 0153 return; 0154 } 0155 0156 QUrl url; 0157 if (qEnvironmentVariableIsSet("KOSMINDOORMAP_TILESERVER")) { 0158 url = QUrl(qEnvironmentVariable("KOSMINDOORMAP_TILESERVER")); 0159 } else { 0160 url.setScheme(QStringLiteral("https")); 0161 url.setHost(QStringLiteral("maps.kde.org")); 0162 url.setPath(QStringLiteral("/earth/vectorosm/v1/")); 0163 } 0164 0165 url.setPath(url.path() + QString::number(tile.z) + QLatin1Char('/') 0166 + QString::number(tile.x) + QLatin1Char('/') 0167 + QString::number(tile.y) + QLatin1String(".o5m")); 0168 0169 QNetworkRequest req(url); 0170 req.setAttribute(QNetworkRequest::Http2AllowedAttribute, true); 0171 req.setHeader(QNetworkRequest::UserAgentHeader, (QCoreApplication::applicationName() + QLatin1Char('/') + QCoreApplication::applicationVersion()).toUtf8()); 0172 auto reply = m_nam->get(req); 0173 connect(reply, &QNetworkReply::readyRead, this, [this, reply]() { dataReceived(reply); }); 0174 connect(reply, &QNetworkReply::finished, this, [this, reply, tile]() { downloadFinished(reply, tile); }); 0175 connect(reply, &QNetworkReply::sslErrors, this, [reply](const auto &sslErrors) { reply->setProperty("_ssl_errors", QVariant::fromValue(sslErrors)); }); 0176 } 0177 0178 void TileCache::dataReceived(QNetworkReply *reply) 0179 { 0180 m_output.write(reply->read(reply->bytesAvailable())); 0181 } 0182 0183 void TileCache::downloadFinished(QNetworkReply* reply, Tile tile) 0184 { 0185 reply->deleteLater(); 0186 m_output.close(); 0187 0188 if (reply->error() != QNetworkReply::NoError) { 0189 qCWarning(Log) << reply->errorString() << reply->url(); 0190 m_output.remove(); 0191 if (reply->error() == QNetworkReply::SslHandshakeFailedError) { 0192 const auto sslErrors = reply->property("_ssl_errors").value<QList<QSslError>>(); 0193 QStringList errorStrings; 0194 errorStrings.reserve(sslErrors.size()); 0195 std::transform(sslErrors.begin(), sslErrors.end(), std::back_inserter(errorStrings), [](const auto &e) { return e.errorString(); }); 0196 qCWarning(Log) << errorStrings; 0197 Q_EMIT tileError(tile, reply->errorString() + QLatin1String(" (") + errorStrings.join(QLatin1String(", ")) + QLatin1Char(')')); 0198 } else { 0199 Q_EMIT tileError(tile, reply->errorString()); 0200 } 0201 downloadNext(); 0202 return; 0203 } 0204 0205 const auto t = cachePath(tile); 0206 m_output.rename(t); 0207 if (tile.ttl.isValid()) { 0208 updateTtl(t, std::max(QDateTime::currentDateTimeUtc().addDays(1), tile.ttl)); 0209 } else { 0210 updateTtl(t, QDateTime::currentDateTimeUtc().addDays(DefaultCacheDays)); 0211 } 0212 0213 Q_EMIT tileLoaded(tile); 0214 downloadNext(); 0215 } 0216 0217 int TileCache::pendingDownloads() const 0218 { 0219 return m_pendingDownloads.size() + (m_output.isOpen() ? 1 : 0); 0220 } 0221 0222 void TileCache::cancelPending() 0223 { 0224 m_pendingDownloads.clear(); 0225 } 0226 0227 static void expireRecursive(const QString &path) 0228 { 0229 QDirIterator it(path, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks); 0230 while (it.hasNext()) { 0231 it.next(); 0232 0233 if (it.fileInfo().isDir()) { 0234 expireRecursive(it.filePath()); 0235 if (QDir(it.filePath()).isEmpty()) { 0236 qCDebug(Log) << "removing empty tile directory" << it.fileName(); 0237 QDir(path).rmdir(it.filePath()); 0238 } 0239 } else if (it.fileInfo().lastModified() < QDateTime::currentDateTimeUtc()) { 0240 qCDebug(Log) << "removing expired tile" << it.filePath(); 0241 QDir(path).remove(it.filePath()); 0242 } 0243 } 0244 } 0245 void TileCache::expire() 0246 { 0247 const QString base = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/org.kde.osm/vectorosm/"); 0248 expireRecursive(base); 0249 } 0250 0251 void TileCache::updateTtl(const QString &filePath, const QDateTime &ttl) 0252 { 0253 QFile f(filePath); 0254 f.open(QFile::WriteOnly | QFile::Append); 0255 f.setFileTime(std::max(f.fileTime(QFileDevice::FileModificationTime), ttl), QFile::FileModificationTime); 0256 } 0257 0258 #include "moc_tilecache_p.cpp"