1 /*
2 SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7 #include "maploader.h"
8 #include "boundarysearch_p.h"
9 #include "logging.h"
10 #include "mapdata.h"
11 #include "marblegeometryassembler_p.h"
12 #include "tilecache_p.h"
13
14 #include <osm/datatypes.h>
15 #include <osm/datasetmergebuffer.h>
16 #include <osm/element.h>
17 #include <osm/o5mparser.h>
18 #include <osm/osmpbfparser.h>
19 #include <osm/xmlparser.h>
20
21 #include <QDateTime>
22 #include <QElapsedTimer>
23 #include <QFile>
24 #include <QRect>
25 #include <QUrl>
26
27 enum {
28 TileZoomLevel = 17
29 };
30
initResources()31 inline void initResources() // needs to be outside of a namespace
32 {
33 Q_INIT_RESOURCE(assets);
34 }
35
36 namespace KOSMIndoorMap {
37 class MapLoaderPrivate {
38 public:
39 OSM::DataSet m_dataSet;
40 OSM::DataSetMergeBuffer m_mergeBuffer;
41 MarbleGeometryAssembler m_marbleMerger;
42 MapData m_data;
43 TileCache m_tileCache;
44 OSM::BoundingBox m_tileBbox;
45 QRect m_loadedTiles;
46 std::vector<Tile> m_pendingTiles;
47 BoundarySearch m_boundarySearcher;
48 QDateTime m_ttl;
49
50 QString m_errorMessage;
51 };
52 }
53
54 using namespace KOSMIndoorMap;
55
MapLoader(QObject * parent)56 MapLoader::MapLoader(QObject *parent)
57 : QObject(parent)
58 , d(new MapLoaderPrivate)
59 {
60 initResources();
61 connect(&d->m_tileCache, &TileCache::tileLoaded, this, &MapLoader::downloadFinished);
62 connect(&d->m_tileCache, &TileCache::tileError, this, &MapLoader::downloadFailed);
63 d->m_tileCache.expire();
64 }
65
66 MapLoader::~MapLoader() = default;
67
loadFromFile(const QString & fileName)68 void MapLoader::loadFromFile(const QString &fileName)
69 {
70 QElapsedTimer loadTime;
71 loadTime.start();
72
73 d->m_errorMessage.clear();
74 QFile f(fileName.contains(QLatin1Char(':')) ? QUrl::fromUserInput(fileName).toLocalFile() : fileName);
75 if (!f.open(QFile::ReadOnly)) {
76 qCritical() << f.fileName() << f.errorString();
77 return;
78 }
79 const auto data = f.map(0, f.size());
80
81 OSM::DataSet ds;
82 if (fileName.endsWith(QLatin1String(".osm.pbf"))) {
83 OSM::OsmPbfParser p(&ds);
84 p.parse(data, f.size());
85 } else if (fileName.endsWith(QLatin1String(".osm"))) {
86 qDebug() << fileName << f.pos() <<f.size();
87 OSM::XmlParser p(&ds);
88 p.parse(&f);
89 } else {
90 OSM::O5mParser p(&ds);
91 p.parse(data, f.size());
92 }
93 d->m_data = MapData();
94 d->m_data.setDataSet(std::move(ds));
95 qCDebug(Log) << "o5m loading took" << loadTime.elapsed() << "ms";
96 Q_EMIT done();
97 }
98
loadForCoordinate(double lat,double lon)99 void MapLoader::loadForCoordinate(double lat, double lon)
100 {
101 loadForCoordinate(lat, lon, {});
102 }
103
loadForCoordinate(double lat,double lon,const QDateTime & ttl)104 void MapLoader::loadForCoordinate(double lat, double lon, const QDateTime &ttl)
105 {
106 d->m_ttl = ttl;
107 d->m_tileBbox = {};
108 d->m_pendingTiles.clear();
109 d->m_boundarySearcher.init(OSM::Coordinate(lat, lon));
110 d->m_errorMessage.clear();
111 d->m_marbleMerger.setDataSet(&d->m_dataSet);
112 d->m_data = MapData();
113
114 auto tile = Tile::fromCoordinate(lat, lon, TileZoomLevel);
115 d->m_loadedTiles = QRect(tile.x, tile.y, 1, 1);
116 d->m_pendingTiles.push_back(std::move(tile));
117 downloadTiles();
118 }
119
takeData()120 MapData&& MapLoader::takeData()
121 {
122 return std::move(d->m_data);
123 }
124
downloadTiles()125 void MapLoader::downloadTiles()
126 {
127 for (const auto &tile : d->m_pendingTiles) {
128 d->m_tileCache.ensureCached(tile);
129 }
130 if (d->m_tileCache.pendingDownloads() == 0) {
131 // still go through the event loop when having everything cached already
132 // this makes outside behavior more identical in both cases, and avoids
133 // signal connection races etc.
134 QMetaObject::invokeMethod(this, &MapLoader::loadTiles, Qt::QueuedConnection);
135 } else {
136 Q_EMIT isLoadingChanged();
137 }
138 }
139
downloadFinished()140 void MapLoader::downloadFinished()
141 {
142 if (d->m_tileCache.pendingDownloads() > 0) {
143 return;
144 }
145 loadTiles();
146 }
147
loadTiles()148 void MapLoader::loadTiles()
149 {
150 QElapsedTimer loadTime;
151 loadTime.start();
152
153 OSM::O5mParser p(&d->m_dataSet);
154 p.setMergeBuffer(&d->m_mergeBuffer);
155 for (const auto &tile : d->m_pendingTiles) {
156 const auto fileName = d->m_tileCache.cachedTile(tile);
157 qCDebug(Log) << "loading tile" << fileName;
158 QFile f(fileName);
159 if (!f.open(QFile::ReadOnly)) {
160 qWarning() << f.fileName() << f.errorString();
161 break;
162 }
163 const auto data = f.map(0, f.size());
164 p.parse(data, f.size());
165 d->m_marbleMerger.merge(&d->m_mergeBuffer);
166
167 d->m_tileBbox = OSM::unite(d->m_tileBbox, tile.boundingBox());
168 }
169 d->m_pendingTiles.clear();
170
171 const auto bbox = d->m_boundarySearcher.boundingBox(d->m_dataSet);
172 qCDebug(Log) << "needed bbox:" << bbox << "got:" << d->m_tileBbox << d->m_loadedTiles;
173
174 // expand left and right
175 if (bbox.min.longitude < d->m_tileBbox.min.longitude) {
176 d->m_loadedTiles.setLeft(d->m_loadedTiles.left() - 1);
177 for (int y = d->m_loadedTiles.top(); y <= d->m_loadedTiles.bottom(); ++y) {
178 d->m_pendingTiles.push_back(makeTile(d->m_loadedTiles.left(), y));
179 }
180 }
181 if (bbox.max.longitude > d->m_tileBbox.max.longitude) {
182 d->m_loadedTiles.setRight(d->m_loadedTiles.right() + 1);
183 for (int y = d->m_loadedTiles.top(); y <= d->m_loadedTiles.bottom(); ++y) {
184 d->m_pendingTiles.push_back(makeTile(d->m_loadedTiles.right(), y));
185 }
186 }
187
188 // expand top/bottom: note that geographics and slippy map tile coordinates have a different understanding on what is "top"
189 if (bbox.max.latitude > d->m_tileBbox.max.latitude) {
190 d->m_loadedTiles.setTop(d->m_loadedTiles.top() - 1);
191 for (int x = d->m_loadedTiles.left(); x <= d->m_loadedTiles.right(); ++x) {
192 d->m_pendingTiles.push_back(makeTile(x, d->m_loadedTiles.top()));
193 }
194 }
195 if (bbox.min.latitude < d->m_tileBbox.min.latitude) {
196 d->m_loadedTiles.setBottom(d->m_loadedTiles.bottom() + 1);
197 for (int x = d->m_loadedTiles.left(); x <= d->m_loadedTiles.right(); ++x) {
198 d->m_pendingTiles.push_back(makeTile(x, d->m_loadedTiles.bottom()));
199 }
200 }
201
202 if (!d->m_pendingTiles.empty()) {
203 downloadTiles();
204 return;
205 }
206
207 d->m_marbleMerger.finalize();
208 d->m_data.setDataSet(std::move(d->m_dataSet));
209 d->m_data.setBoundingBox(bbox);
210
211 qCDebug(Log) << "o5m loading took" << loadTime.elapsed() << "ms";
212 Q_EMIT isLoadingChanged();
213 Q_EMIT done();
214 }
215
makeTile(uint32_t x,uint32_t y) const216 Tile MapLoader::makeTile(uint32_t x, uint32_t y) const
217 {
218 auto tile = Tile(x, y, TileZoomLevel);
219 tile.ttl = d->m_ttl;
220 return tile;
221 }
222
downloadFailed(Tile tile,const QString & errorMessage)223 void MapLoader::downloadFailed(Tile tile, const QString& errorMessage)
224 {
225 Q_UNUSED(tile);
226 d->m_errorMessage = errorMessage;
227 d->m_tileCache.cancelPending();
228 Q_EMIT isLoadingChanged();
229 Q_EMIT done();
230 }
231
isLoading() const232 bool MapLoader::isLoading() const
233 {
234 return d->m_tileCache.pendingDownloads() > 0;
235 }
236
hasError() const237 bool MapLoader::hasError() const
238 {
239 return !d->m_errorMessage.isEmpty();
240 }
241
errorMessage() const242 QString MapLoader::errorMessage() const
243 {
244 return d->m_errorMessage;
245 }
246