KOSMIndoorMap

marblegeometryassembler.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 "marblegeometryassembler_p.h"
8#include "reassembly-logging.h"
9
10#include <cassert>
11
12using namespace KOSMIndoorMap;
13
14enum { NodeMatchDistance = 2 }; // in 1e7th of a degree, 46 for the old Marble data, very small for the new one
15
16/** Compare two coordinate while accounting for floating point noise. */
17static bool fuzzyEquals(OSM::Coordinate lhs, OSM::Coordinate rhs)
18{
19 return std::abs((int32_t)lhs.latitude - (int32_t)rhs.latitude) <= NodeMatchDistance && std::abs((int32_t)lhs.longitude - (int32_t)rhs.longitude) <= NodeMatchDistance;
20}
21
22OSM::Id MarbleGeometryAssembler::s_nextInternalId = -(1ll << 32); // try to stay out of the way of the number space used by Marble tiles
23
24MarbleGeometryAssembler::MarbleGeometryAssembler() = default;
25MarbleGeometryAssembler::~MarbleGeometryAssembler() = default;
26
27void MarbleGeometryAssembler::setDataSet(OSM::DataSet* dataSet)
28{
29 assert(dataSet);
30 m_dataSet = dataSet;
31 m_mxoidKey = m_dataSet->makeTagKey("mx:oid");
32 m_typeKey = m_dataSet->makeTagKey("type");
33}
34
35void MarbleGeometryAssembler::merge(OSM::DataSetMergeBuffer *mergeBuffer)
36{
37 assert(m_dataSet);
38 m_nodeIdMap.clear();
39 m_wayIdMap.clear();
40 m_relIdMap.clear();
41
42 std::vector<OSM::Way> prevPendingWays;
43 std::swap(m_pendingWays, prevPendingWays);
44
45 mergeNodes(mergeBuffer);
46 deduplicateWays(mergeBuffer->ways);
47 remapWayNodes(mergeBuffer->ways);
48 mergeWays(mergeBuffer->ways);
49 mergeWays(prevPendingWays);
50 mergeRelations(mergeBuffer);
51
52 mergeBuffer->clear();
53}
54
55void MarbleGeometryAssembler::finalize()
56{
57 m_dataSet->ways.reserve(m_dataSet->ways.size() + m_pendingWays.size());
58 for (auto &way : m_pendingWays) {
59 if (!std::binary_search(m_dataSet->ways.begin(), m_dataSet->ways.end(), way)) {
60 m_dataSet->ways.push_back(std::move(way));
61 }
62 }
63 std::sort(m_dataSet->ways.begin(), m_dataSet->ways.end());
64}
65
66void MarbleGeometryAssembler::mergeNodes(OSM::DataSetMergeBuffer *mergeBuffer)
67{
68 // nodes of the first batch are just taken as-is
69 if (m_dataSet->nodes.empty()) {
70 m_dataSet->nodes = std::move(mergeBuffer->nodes);
71 std::sort(m_dataSet->nodes.begin(), m_dataSet->nodes.end());
72 return;
73 }
74
75 // find nodes we already know
76 // - for synthetic nodes those are collisions we need to remap
77 // - for normal nodes, those are real duplicates and can be omitted
78 // we do this in-place in the merge buffer, to avoid O(n^2) removal or insertion ops
79 // (which matters here as this grows with the number of tiles we load)
80 for (auto &node : mergeBuffer->nodes) {
81 const auto it = std::lower_bound(m_dataSet->nodes.begin(), m_dataSet->nodes.end(), node);
82 if (it != m_dataSet->nodes.end() && (*it).id == node.id) {
83 if (node.id < 0) { // synthetic id collision, remap that
84 node.id = s_nextInternalId++;
85 m_nodeIdMap[(*it).id] = node.id;
86 } else {
87 node.id = 0;
88 }
89 }
90 }
91
92 // append the new nodes (those not marked with id == 0), then sort the result
93 m_dataSet->nodes.reserve(m_dataSet->nodes.size() + mergeBuffer->nodes.size());
94 for (auto &node : mergeBuffer->nodes) {
95 if (node.id) {
96 m_dataSet->nodes.push_back(std::move(node));
97 }
98 }
99 std::sort(m_dataSet->nodes.begin(), m_dataSet->nodes.end());
100}
101
102void MarbleGeometryAssembler::mergeWays(std::vector<OSM::Way> &ways)
103{
104 // for ways we do:
105 // 1. restore the original id
106 // 2. if a way with that id already exists, we merge with the geometry of the existing one
107
108 for (auto &way : ways) {
109 if (way.id > 0 || way.nodes.empty()) { // not a synthetic id
110 m_dataSet->addWay(std::move(way));
111 continue;
112 }
113
114 const OSM::Id mxoid = takeMxOid(way);
115 if (mxoid <= 0) { // shouldn't happen?
116 m_dataSet->addWay(std::move(way));
117 continue;
118 }
119
120 const auto syntheticId = way.id;
121 way.id = mxoid;
122
123 const auto it = std::lower_bound(m_dataSet->ways.begin(), m_dataSet->ways.end(), way);
124 if (it != m_dataSet->ways.end() && (*it).id == way.id) {
125 mergeWay(*it, way);
126
127 if (way.nodes.empty()) {
128 // way was fully merged
129 m_wayIdMap[syntheticId] = mxoid;
130 } else {
131 // defer to later (ie. more tiles loaded)
132 way.id = syntheticId;
133 OSM::setTagValue(way, m_mxoidKey, QByteArray::number((qlonglong)mxoid));
134 m_pendingWays.push_back(std::move(way));
135 }
136
137 } else {
138 m_wayIdMap[syntheticId] = mxoid;
139 m_dataSet->ways.insert(it, std::move(way));
140 }
141 }
142}
143
144static bool isDuplicateWay(const OSM::Way &lhs, const OSM::Way &rhs)
145{
146 if (lhs.nodes.size() != rhs.nodes.size()) {
147 return false;
148 }
149 for (std::size_t i = 0; i < lhs.nodes.size(); ++i) {
150 if (lhs.nodes[i] != rhs.nodes[i] && (lhs.nodes[i] > 0 || rhs.nodes[i] > 0)) {
151 return false;
152 }
153 }
154 return true;
155}
156
157void MarbleGeometryAssembler::deduplicateWays(std::vector<OSM::Way>& ways)
158{
159 for (auto it = ways.begin(); it != ways.end();) {
160 if ((*it).id > 0) {
161 ++it;
162 continue;
163 }
164 const OSM::Id mxoid = OSM::tagValue(*it, m_mxoidKey).toLongLong();
165 if (mxoid == 0) {
166 ++it;
167 continue;
168 }
169
170 const auto duplIt = m_duplicateWays.find(mxoid);
171 if (duplIt != m_duplicateWays.end()) {
172 bool found = false;
173 for (auto it2 = (*duplIt).second.begin(); it2 != (*duplIt).second.end(); ++it2) {
174 if (isDuplicateWay(*it, ways[*it2])) {
175 m_wayIdMap[(*it).id] = mxoid;
176 qCDebug(ReassemblyLog) << "removing duplicate way:" << (*it).id << (*it).url();
177 it = ways.erase(it);
178 found = true;
179 break;
180 }
181 }
182 if (!found) {
183 m_duplicateWays[mxoid].push_back(std::distance(ways.begin(), it));
184 ++it;
185 }
186 } else {
187 m_duplicateWays[mxoid] = {(std::size_t)std::distance(ways.begin(), it)};
188 ++it;
189 }
190 }
191 m_duplicateWays.clear();
192}
193
194void MarbleGeometryAssembler::remapWayNodes(std::vector<OSM::Way> &ways) const
195{
196 if (m_nodeIdMap.empty()) {
197 return;
198 }
199
200 for (auto &way : ways) {
201 for (auto &id : way.nodes) {
202 if (id > 0) {
203 continue;
204 }
205 const auto it = m_nodeIdMap.find(id);
206 if (it != m_nodeIdMap.end()) {
207 id = (*it).second;
208 }
209 }
210 }
211}
212
213void MarbleGeometryAssembler::mergeWay(OSM::Way &way, OSM::Way &otherWay) const
214{
215 // for merging two ways:
216 // - non-synthetic nodes remain unchanged, ways can only be merged on synthetic nodes
217 // - synthetic nodes are duplicated in both sets, we need to merge them by coordinate comparison
218 // - synthetic nodes can be removed, and so can edges between two adjacent synthetic nodes
219 // - closed polygons at least have one shared edge (possibly multiple adjacent or non-adjacent ones)
220 // - lines can only be merged at the beginning or the end, a line crossing the same boundary multiple times would be split at every boundary intersection
221 // - we can assume polygon orientation is preserved by the splitting
222
223 // in some cases the Marble tile generator produces both ways and areas for the same polygon
224 // e.g. for something way-like itself used in a multipolygon relations, with both being tagged
225 // we ignore the way in that case and rely solely on area merging
226
227 if (!way.isClosed() && !otherWay.isClosed()) {
228 mergeLine(way, otherWay);
229 } else if (way.isClosed() && !otherWay.isClosed()) {
230 return;
231 } else if (!way.isClosed() && otherWay.isClosed()) {
232 std::swap(way, otherWay);
233 return;
234 } else {
235 way.nodes = mergeArea(way, otherWay);
236 }
237}
238
239void MarbleGeometryAssembler::mergeLine(OSM::Way &way, OSM::Way &otherWay) const
240{
241 const auto begin1 = m_dataSet->node(way.nodes.front());
242 const auto end1 = m_dataSet->node(way.nodes.back());
243 const auto begin2 = m_dataSet->node(otherWay.nodes.front());
244 const auto end2 = m_dataSet->node(otherWay.nodes.back());
245 if (!begin1 || !end1 || !begin2 || !end2) {
246 qDebug() << "failed to find way nodes!?" << begin1 << end1 << begin2 << end2;;
247 return;
248 }
249
250 // only merge ways on synthetic nodes (the < 0 checks)
251 // it is possible to end up here with mergable non-synthetic nodes, in case of a circular
252 // path that the Marble tile generator classified as a line rather than an area
253 // in this case we do not want to merge the nodes as that would eliminate that node
254
255 way.nodes.reserve(way.nodes.size() + otherWay.nodes.size() - 2);
256 if (end1->id < 0 && begin2->id < 0 && fuzzyEquals(end1->coordinate, begin2->coordinate)) {
257 way.nodes.pop_back();
258 std::copy(std::next(otherWay.nodes.begin()), otherWay.nodes.end(), std::back_inserter(way.nodes));
259 } else if (end1->id < 0 && end2->id < 0 && fuzzyEquals(end1->coordinate, end2->coordinate)) {
260 way.nodes.pop_back();
261 std::copy(std::next(otherWay.nodes.rbegin()), otherWay.nodes.rend(), std::back_inserter(way.nodes));
262 } else if (begin1->id < 0 && end2->id < 0 && fuzzyEquals(begin1->coordinate, end2->coordinate)) {
263 way.nodes.erase(way.nodes.begin());
264 way.nodes.insert(way.nodes.begin(), otherWay.nodes.begin(), std::prev(otherWay.nodes.end()));
265 } else if (begin1->id < 0 && begin2->id < 0 && fuzzyEquals(begin1->coordinate, begin2->coordinate)) {
266 way.nodes.erase(way.nodes.begin());
267 way.nodes.insert(way.nodes.begin(), otherWay.nodes.rbegin(), std::prev(otherWay.nodes.rend()));
268 } else {
269 //qDebug() << "unable to merge line:" << begin1->coordinate << end1->coordinate << begin2->coordinate << end2->coordinate;
270 return;
271 }
272
273 otherWay.nodes.clear(); // for the caller to notice we succeeded here
274}
275
276std::vector<OSM::Id> MarbleGeometryAssembler::mergeArea(OSM::Way &way, OSM::Way &otherWay) const
277{
278 // sanity checks for assumptions below
279 if (way.nodes.size() < 3 || otherWay.nodes.size() < 3 || !way.isClosed() || !otherWay.isClosed()) {
280 qCWarning(ReassemblyLog()) << "got invalid polygons!" << way.url() << way.nodes.size() << way.isClosed() << otherWay.url() << otherWay.nodes.size() << otherWay.isClosed();
281 return way.nodes.empty() ? std::move(way.nodes) : std::move(otherWay.nodes);
282 }
283
284 // "open" the closed polygons (otherwise we have to special-case the closing edges in both ways below)
285 way.nodes.pop_back();
286 otherWay.nodes.pop_back();
287
288 std::vector<OSM::Id> nodes;
289 mergeAreaSection(nodes, way.nodes, way.nodes.begin(), otherWay.nodes);
290
291 // re-close the polygon
292 if (!nodes.empty()) {
293 nodes.push_back(nodes.front());
294 }
295
296 // if otherWay is still populated we failed to find a connecting node
297 // this can happen in a number of cases, such as the same area entering a tile
298 // twice but its connecting part still being in an not yet loaded tile
299 // we defer processing those to later, so just re-close the polygon here
300 if (!otherWay.nodes.empty()) {
301 otherWay.nodes.push_back(otherWay.nodes.front());
302 }
303 return nodes;
304}
305
306bool MarbleGeometryAssembler::mergeAreaSection(std::vector<OSM::Id> &assembledPath, std::vector<OSM::Id> &path, const std::vector<OSM::Id>::iterator &pathBegin, std::vector<OSM::Id> &otherPath) const
307{
308 for (auto nodeIt = pathBegin; nodeIt != path.end(); ++nodeIt) {
309 if ((*nodeIt) >= 0) { // not synthetic
310 continue;
311 }
312 const auto node = m_dataSet->node(*nodeIt);
313 if (!node) { // should not happen?
314 qCDebug(ReassemblyLog) << "could not find node" << (*nodeIt);
315 continue;
316 }
317
318 // TODO orientation change?
319 // synthetic node, find a matching one in the other way and splice in that way
320 for (auto otherNodeIt = otherPath.begin(); otherNodeIt != otherPath.end(); ++otherNodeIt) {
321 if ((*otherNodeIt) >= 0) {
322 continue;
323 }
324
325 const auto otherNode = m_dataSet->node(*otherNodeIt);
326 if (!otherNode) {
327 qCDebug(ReassemblyLog) << "could not find node" << (*otherNodeIt);
328 continue;
329 }
330
331 if (!fuzzyEquals(node->coordinate, otherNode->coordinate)) {
332 continue;
333 }
334
335 // found a matching synthetic node, continue in the other path
336 std::copy(pathBegin, nodeIt, std::back_inserter(assembledPath));
337 nodeIt = path.erase(pathBegin, ++nodeIt);
338 if (nodeIt == path.end()) {
339 nodeIt = path.begin();
340 }
341 otherNodeIt = otherPath.erase(otherNodeIt);
342 if (otherNodeIt == otherPath.end()) {
343 otherNodeIt = otherPath.begin();
344 }
345 // if otherPath was completely consumed, it would have switched back to us with its closing edge
346 // so add the remaining part of path here
347 if (mergeAreaSection(assembledPath, otherPath, otherNodeIt, path)) {
348// qDebug() << " processing trailing path";
349 std::copy(nodeIt, path.end(), std::back_inserter(assembledPath));
350 std::copy(path.begin(), nodeIt, std::back_inserter(assembledPath));
351 path.clear();
352 }
353 return false;
354 }
355
356 }
357
358 // copy the final segment
359 std::copy(pathBegin, path.end(), std::back_inserter(assembledPath));
360 path.erase(pathBegin, path.end());
361
362 // wrap around when starting in the middle (can happen on the secondary path)
363 if (!path.empty()) {
364 return mergeAreaSection(assembledPath, path, path.begin(), otherPath);
365 }
366
367 return path.empty();
368}
369
370void MarbleGeometryAssembler::mergeRelations(OSM::DataSetMergeBuffer *mergeBuffer)
371{
372 // for relations we do:
373 // 1. restore the original id
374 // 2. replace all member ids with the restored ids for ways/relations
375 // 3. if a relations with the restored id already exists, merge with its content
376
377 for (auto &rel : mergeBuffer->relations) {
378 const OSM::Id mxoid = takeMxOid(rel);
379 if (mxoid <= 0) { // shouldn't happen?
380 m_dataSet->addRelation(std::move(rel));
381 continue;
382 }
383
384 m_relIdMap[rel.id] = mxoid;
385 rel.id = mxoid;
386
387 for (auto &member : rel.members) {
388 if (member.id >= 0) { // not a synthetic id
389 continue;
390 }
391
392 if (member.type() == OSM::Type::Node) {
393 const auto it = m_nodeIdMap.find(member.id);
394 if (it != m_nodeIdMap.end()) {
395 member.id = (*it).second;
396 }
397 } else if (member.type() == OSM::Type::Way) {
398 const auto it = m_wayIdMap.find(member.id);
399 if (it != m_wayIdMap.end()) {
400 member.id = (*it).second;
401 }
402 } else if (member.type() == OSM::Type::Relation) {
403 const auto it = m_relIdMap.find(member.id);
404 if (it != m_relIdMap.end()) {
405 member.id = (*it).second;
406 }
407 }
408 }
409
410 const auto it = std::lower_bound(m_dataSet->relations.begin(), m_dataSet->relations.end(), rel);
411 if (it != m_dataSet->relations.end() && (*it).id == rel.id) {
412 mergeRelation(*it, rel);
413 } else {
414 m_dataSet->relations.insert(it, std::move(rel));
415 }
416 }
417}
418
419void MarbleGeometryAssembler::mergeRelation(OSM::Relation& relation, const OSM::Relation& otherRelation) const
420{
421 for (auto &otherMember : otherRelation.members) {
422 const auto it = std::find(relation.members.begin(), relation.members.end(), otherMember);
423 if (it == relation.members.end()) {
424 relation.members.push_back(otherMember);
425 }
426 }
427
428 // multi-polygons can exist entirely out of synthetic polygons, e.g. if the source was a set of lines rather than closed polygons
429 // merging those would not have happened before (as we wouldn't know what to merge it with), so we need to do this here
430 if (OSM::tagValue(relation, m_typeKey) == "multipolygon") {
431 for (auto it = relation.members.begin(); it != relation.members.end();) {
432 if ((*it).id > 0 || (*it).type() != OSM::Type::Way) {
433 ++it;
434 continue;
435 }
436 // TODO look up role names once via DataSet::role
437 if (std::strcmp((*it).role().name(), "outer") != 0 && std::strcmp((*it).role().name(), "inner") != 0) {
438 ++it;
439 continue;
440 }
441
442 auto way = m_dataSet->way((*it).id);
443 if (!way || !way->isClosed()) {
444 ++it;
445 continue;
446 }
447
448 bool merged = false;
449 for (auto it2 = std::next(it); it2 != relation.members.end(); ++it2) {
450 if ((*it2).id > 0 || (*it2).type() != OSM::Type::Way || (*it2).role() != (*it).role()) {
451 continue;
452 }
453
454 auto otherWay = m_dataSet->way((*it2).id);
455 if (!otherWay || !otherWay->isClosed()) {
456 continue;
457 }
458
459 way->nodes = mergeArea(*way, *otherWay);
460 if (otherWay->nodes.empty()) {
461 relation.members.erase(it2);
462 merged = true;
463 break;
464 }
465 }
466 if (!merged) {
467 ++it;
468 }
469 }
470 }
471}
472
473template<typename Elem>
474OSM::Id MarbleGeometryAssembler::takeMxOid(Elem &elem) const
475{
476 const auto it = std::lower_bound(elem.tags.begin(), elem.tags.end(), m_mxoidKey, [](const auto &lhs, const auto &rhs) { return lhs.key < rhs; });
477 if (it != elem.tags.end() && (*it).key == m_mxoidKey) {
478 bool result = false;
479 const OSM::Id id = (*it).value.toLongLong(&result);
480 if (result) {
481 elem.tags.erase(it);
482 return id;
483 }
484 }
485 return {};
486}
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:340
TagKey makeTagKey(const char *keyName, StringMemory keyMemOpt=StringMemory::Transient)
Create a tag key for the given tag name.
Definition datatypes.cpp:17
An OSM relation.
Definition datatypes.h:311
An OSM way.
Definition datatypes.h:231
KIOCORE_EXPORT CopyJob * move(const QList< QUrl > &src, const QUrl &dest, JobFlags flags=DefaultFlags)
QString path(const QString &relativePath)
OSM-based multi-floor indoor maps for buildings.
void setTagValue(Elem &elem, TagKey key, QByteArray &&value)
Inserts a new tag, or updates an existing one.
Definition datatypes.h:484
int64_t Id
OSM element identifier.
Definition datatypes.h:30
QByteArray tagValue(const Elem &elem, TagKey key)
Returns the tag value for key of elem.
Definition datatypes.h:410
QByteArray number(double n, char format, int precision)
qlonglong toLongLong(bool *ok, int base) const const
iterator begin()
void clear()
iterator end()
iterator erase(const_iterator first, const_iterator last)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:20:03 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.