KOSMIndoorMap

maploader.cpp
1/*
2 SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include <config-kosmindoormap.h>
8
9#include "maploader.h"
10#include "boundarysearch_p.h"
11#include "logging.h"
12#include "mapdata.h"
13#include "marblegeometryassembler_p.h"
14#include "tilecache_p.h"
15
16#include "network/useragent_p.h"
17
18#include <osm/datatypes.h>
19#include <osm/datasetmergebuffer.h>
20#include <osm/element.h>
21#include <osm/o5mparser.h>
22#include <osm/io.h>
23
24#include <QDateTime>
25#include <QElapsedTimer>
26#include <QFile>
27#include <QNetworkReply>
28#include <QNetworkRequest>
29#include <QRect>
30#include <QUrl>
31
32#include <deque>
33
34using namespace Qt::Literals::StringLiterals;
35
36enum {
37 TileZoomLevel = 17
38};
39
40inline void initResources() // needs to be outside of a namespace
41{
42#if !BUILD_TOOLS_ONLY
43 Q_INIT_RESOURCE(assets);
44#endif
45}
46
47namespace KOSMIndoorMap {
48class MapLoaderPrivate {
49public:
50 NetworkAccessManagerFactory m_nam = KOSMIndoorMap::defaultNetworkAccessManagerFactory; // TODO make externally configurable
51 OSM::DataSet m_dataSet;
52 OSM::DataSetMergeBuffer m_mergeBuffer;
53 MarbleGeometryAssembler m_marbleMerger;
54 MapData m_data;
55 TileCache m_tileCache{m_nam};
56 OSM::BoundingBox m_tileBbox;
57 OSM::BoundingBox m_targetBbox;
58 QRect m_loadedTiles;
59 std::vector<Tile> m_pendingTiles;
60 std::unique_ptr<BoundarySearch> m_boundarySearcher;
61 QDateTime m_ttl;
62 std::deque<QUrl> m_pendingChangeSets;
63
64 QString m_errorMessage;
65};
66}
67
68using namespace KOSMIndoorMap;
69
70MapLoader::MapLoader(QObject *parent)
71 : QObject(parent)
72 , d(new MapLoaderPrivate)
73{
74 initResources();
75 connect(&d->m_tileCache, &TileCache::tileLoaded, this, &MapLoader::downloadFinished);
76 connect(&d->m_tileCache, &TileCache::tileError, this, &MapLoader::downloadFailed);
77 d->m_tileCache.expire();
78}
79
80MapLoader::~MapLoader() = default;
81
82void MapLoader::loadFromFile(const QString &fileName)
83{
84 QElapsedTimer loadTime;
85 loadTime.start();
86
87 d->m_errorMessage.clear();
88 QFile f(fileName.contains(QLatin1Char(':')) ? QUrl::fromUserInput(fileName).toLocalFile() : fileName);
89 if (!f.open(QFile::ReadOnly)) {
90 qCritical() << f.fileName() << f.errorString();
91 return;
92 }
93 const auto data = f.map(0, f.size());
94
95 auto reader = OSM::IO::readerForFileName(fileName, &d->m_dataSet);
96 if (!reader) {
97 qCWarning(Log) << "no file reader for" << fileName;
98 return;
99 }
100 reader->read(data, f.size());
101 d->m_data = MapData();
102 qCDebug(Log) << "o5m loading took" << loadTime.elapsed() << "ms";
103 QMetaObject::invokeMethod(this, &MapLoader::applyNextChangeSet, Qt::QueuedConnection);
104}
105
106void MapLoader::loadForCoordinate(double lat, double lon)
107{
108 loadForCoordinate(lat, lon, {});
109}
110
111void MapLoader::loadForCoordinate(double lat, double lon, const QDateTime &ttl)
112{
113 d->m_ttl = ttl;
114 d->m_tileBbox = {};
115 d->m_targetBbox = {};
116 d->m_pendingTiles.clear();
117 d->m_boundarySearcher = std::make_unique<BoundarySearch>();
118 d->m_boundarySearcher->init(OSM::Coordinate(lat, lon));
119 d->m_errorMessage.clear();
120 d->m_marbleMerger.setDataSet(&d->m_dataSet);
121 d->m_data = MapData();
122
123 auto tile = Tile::fromCoordinate(lat, lon, TileZoomLevel);
124 d->m_loadedTiles = QRect(tile.x, tile.y, 1, 1);
125 d->m_pendingTiles.push_back(std::move(tile));
126 downloadTiles();
127}
128
130{
131 d->m_ttl = {};
132 d->m_tileBbox = box;
133 d->m_targetBbox = box;
134 d->m_pendingTiles.clear();
135 d->m_errorMessage.clear();
136 d->m_marbleMerger.setDataSet(&d->m_dataSet);
137 d->m_data = MapData();
138
139 const auto topLeftTile = Tile::fromCoordinate(box.min.latF(), box.min.lonF(), TileZoomLevel);
140 const auto bottomRightTile = Tile::fromCoordinate(box.max.latF(), box.max.lonF(), TileZoomLevel);
141 for (auto x = topLeftTile.x; x <= bottomRightTile.x; ++x) {
142 for (auto y = bottomRightTile.y; y <= topLeftTile.y; ++y) {
143 d->m_pendingTiles.push_back(makeTile(x, y));
144 }
145 }
146 downloadTiles();
147}
148
149void MapLoader::loadForBoundingBox(double minLat, double minLon, double maxLat, double maxLon)
150{
151 loadForBoundingBox(OSM::BoundingBox(OSM::Coordinate{minLat, minLon}, OSM::Coordinate{maxLat, maxLon}));
152}
153
155{
156 d->m_ttl = {};
157 d->m_tileBbox = tile.boundingBox();
158 d->m_targetBbox = {};
159 d->m_pendingTiles.clear();
160 d->m_errorMessage.clear();
161 d->m_marbleMerger.setDataSet(&d->m_dataSet);
162 d->m_data = MapData();
163
164 if (tile.z >= TileZoomLevel) {
165 d->m_pendingTiles.push_back(std::move(tile));
166 } else {
167 const auto start = tile.topLeftAtZ(TileZoomLevel);
168 const auto end = tile.bottomRightAtZ(TileZoomLevel);
169 for (auto x = start.x; x <= end.x; ++x) {
170 for (auto y = start.y; y <= end.y; ++y) {
171 d->m_pendingTiles.push_back(makeTile(x, y));
172 }
173 }
174 }
175
176 downloadTiles();
177}
178
180{
181 d->m_pendingChangeSets.push_back(url);
182}
183
185{
186 return std::move(d->m_data);
187}
188
189void MapLoader::downloadTiles()
190{
191 for (const auto &tile : d->m_pendingTiles) {
192 d->m_tileCache.ensureCached(tile);
193 }
194 if (d->m_tileCache.pendingDownloads() == 0) {
195 // still go through the event loop when having everything cached already
196 // this makes outside behavior more identical in both cases, and avoids
197 // signal connection races etc.
198 QMetaObject::invokeMethod(this, &MapLoader::loadTiles, Qt::QueuedConnection);
199 } else {
200 Q_EMIT isLoadingChanged();
201 }
202}
203
204void MapLoader::downloadFinished()
205{
206 if (d->m_tileCache.pendingDownloads() > 0) {
207 return;
208 }
209 loadTiles();
210}
211
212void MapLoader::loadTiles()
213{
214 QElapsedTimer loadTime;
215 loadTime.start();
216
217 OSM::O5mParser p(&d->m_dataSet);
218 p.setMergeBuffer(&d->m_mergeBuffer);
219 for (const auto &tile : d->m_pendingTiles) {
220 const auto fileName = d->m_tileCache.cachedTile(tile);
221 qCDebug(Log) << "loading tile" << fileName;
222 QFile f(fileName);
223 if (!f.open(QFile::ReadOnly)) {
224 qWarning() << "Failed to open tile!" << f.fileName() << f.errorString();
225 continue;
226 }
227
228 const auto data = f.map(0, f.size());
229 if (!data) {
230 qCritical() << "Failed to mmap tile!" << f.fileName() << f.size() << f.errorString();
231 continue;
232 }
233
234 p.read(data, f.size());
235 d->m_marbleMerger.merge(&d->m_mergeBuffer);
236
237 d->m_tileBbox = OSM::unite(d->m_tileBbox, tile.boundingBox());
238 }
239 d->m_pendingTiles.clear();
240
241 if (d->m_boundarySearcher) {
242 const auto bbox = d->m_boundarySearcher->boundingBox(d->m_dataSet);
243 qCDebug(Log) << "needed bbox:" << bbox << "got:" << d->m_tileBbox << d->m_loadedTiles;
244
245 // expand left and right
246 if (bbox.min.longitude < d->m_tileBbox.min.longitude) {
247 d->m_loadedTiles.setLeft(d->m_loadedTiles.left() - 1);
248 for (int y = d->m_loadedTiles.top(); y <= d->m_loadedTiles.bottom(); ++y) {
249 d->m_pendingTiles.push_back(makeTile(d->m_loadedTiles.left(), y));
250 }
251 }
252 if (bbox.max.longitude > d->m_tileBbox.max.longitude) {
253 d->m_loadedTiles.setRight(d->m_loadedTiles.right() + 1);
254 for (int y = d->m_loadedTiles.top(); y <= d->m_loadedTiles.bottom(); ++y) {
255 d->m_pendingTiles.push_back(makeTile(d->m_loadedTiles.right(), y));
256 }
257 }
258
259 // expand top/bottom: note that geographics and slippy map tile coordinates have a different understanding on what is "top"
260 if (bbox.max.latitude > d->m_tileBbox.max.latitude) {
261 d->m_loadedTiles.setTop(d->m_loadedTiles.top() - 1);
262 for (int x = d->m_loadedTiles.left(); x <= d->m_loadedTiles.right(); ++x) {
263 d->m_pendingTiles.push_back(makeTile(x, d->m_loadedTiles.top()));
264 }
265 }
266 if (bbox.min.latitude < d->m_tileBbox.min.latitude) {
267 d->m_loadedTiles.setBottom(d->m_loadedTiles.bottom() + 1);
268 for (int x = d->m_loadedTiles.left(); x <= d->m_loadedTiles.right(); ++x) {
269 d->m_pendingTiles.push_back(makeTile(x, d->m_loadedTiles.bottom()));
270 }
271 }
272
273 if (!d->m_pendingTiles.empty()) {
274 downloadTiles();
275 return;
276 }
277 d->m_targetBbox = bbox;
278 }
279
280 d->m_marbleMerger.finalize();
281 d->m_boundarySearcher.reset();
282
283 qCDebug(Log) << "o5m loading took" << loadTime.elapsed() << "ms";
284 applyNextChangeSet();
285}
286
287Tile MapLoader::makeTile(uint32_t x, uint32_t y) const
288{
289 auto tile = Tile(x, y, TileZoomLevel);
290 tile.ttl = d->m_ttl;
291 return tile;
292}
293
294void MapLoader::downloadFailed(Tile tile, const QString& errorMessage)
295{
296 Q_UNUSED(tile);
297 d->m_errorMessage = errorMessage;
298 d->m_tileCache.cancelPending();
299 Q_EMIT isLoadingChanged();
300 Q_EMIT done();
301}
302
303bool MapLoader::isLoading() const
304{
305 return d->m_tileCache.pendingDownloads() > 0 || !d->m_pendingChangeSets.empty();
306}
307
308bool MapLoader::hasError() const
309{
310 return !d->m_errorMessage.isEmpty();
311}
312
313QString MapLoader::errorMessage() const
314{
315 return d->m_errorMessage;
316}
317
318void MapLoader::applyNextChangeSet()
319{
320 if (d->m_pendingChangeSets.empty() || hasError()) {
321 d->m_data.setDataSet(std::move(d->m_dataSet));
322 if (d->m_targetBbox.isValid()) {
323 d->m_data.setBoundingBox(d->m_targetBbox);
324 }
325
326 Q_EMIT isLoadingChanged();
327 Q_EMIT done();
328 return;
329 }
330
331 const auto &url = d->m_pendingChangeSets.front();
332 if (url.isLocalFile()) {
333 QFile f(url.toLocalFile());
334 if (!f.open(QFile::ReadOnly)) {
335 qCWarning(Log) << f.fileName() << f.errorString();
336 d->m_errorMessage = f.errorString();
337 } else {
338 applyChangeSet(url, &f);
339 }
340 } else if (url.scheme() == "https"_L1) {
341 QNetworkRequest req(url);
342 req.setHeader(QNetworkRequest::UserAgentHeader, KOSMIndoorMap::userAgent());
343 auto reply = d->m_nam()->get(req);
344 connect(reply, &QNetworkReply::finished, this, [this, reply, url]() {
345 reply->deleteLater();
346
347 if (reply->error() != QNetworkReply::NoError) {
348 d->m_errorMessage = reply->errorString();
349 } else {
350 applyChangeSet(url, reply);
351 }
352
353 d->m_pendingChangeSets.pop_front();
354 applyNextChangeSet();
355 });
356 return;
357 }
358
359 d->m_pendingChangeSets.pop_front();
360 applyNextChangeSet();
361}
362
363void MapLoader::applyChangeSet(const QUrl &url, QIODevice *io)
364{
365 auto reader = OSM::IO::readerForFileName(url.fileName(), &d->m_dataSet);
366 if (!reader) {
367 qCWarning(Log) << "unable to find reader for" << url;
368 return;
369 }
370
371 reader->read(io);
372 if (reader->hasError()) {
373 d->m_errorMessage = reader->errorString();
374 }
375}
376
377#include "moc_maploader.cpp"
Raw OSM map data, separated by levels.
Definition mapdata.h:60
Q_INVOKABLE void loadFromFile(const QString &fileName)
Load a single O5M or OSM PBF file.
Definition maploader.cpp:82
void done()
Emitted when the requested data has been loaded.
bool isLoading
Indicates we are downloading content.
Definition maploader.h:32
void loadForBoundingBox(OSM::BoundingBox box)
Load map data for the given bounding box, without applying the boundary search.
Q_INVOKABLE void loadForCoordinate(double lat, double lon)
Load map for the given coordinates.
Q_INVOKABLE void addChangeSet(const QUrl &url)
Add a changeset to be applied on top of the data loaded by any of the load() methods.
MapData && takeData()
Take out the completely loaded result.
void loadForTile(Tile tile)
Load map data for the given tile.
Bounding box, ie.
Definition datatypes.h:95
Coordinate, stored as 1e7 * degree to avoid floating point precision issues, and offset to unsigned v...
Definition datatypes.h:37
Holds OSM elements produced by a parser prior to merging into OSM::DataSet.
A set of nodes, ways and relations.
Definition datatypes.h:343
Zero-copy parser of O5M binary files.
Definition o5mparser.h:28
Q_SCRIPTABLE Q_NOREPLY void start()
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
OSM-based multi-floor indoor maps for buildings.
KOSMINDOORMAP_EXPORT QNetworkAccessManager * defaultNetworkAccessManagerFactory()
Default implementation if not using an application-specific QNetworkAccessManager instance.
KOSM_EXPORT std::unique_ptr< AbstractReader > readerForFileName(QStringView fileName, OSM::DataSet *dataSet)
Returns a suitable reader for the given file name.
Definition io.cpp:42
qint64 elapsed() const const
virtual QString fileName() const const override
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QueuedConnection
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QString fileName(ComponentFormattingOptions options) const const
QUrl fromUserInput(const QString &userInput, const QString &workingDirectory, UserInputResolutionOptions options)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:57:46 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.