KOSMIndoorMap

navmeshbuilder.cpp
1/*
2 SPDX-FileCopyrightText: 2024 Volker Krause <vkrause@kde.org>
3 SPDX-License-Identifier: LGPL-2.0-or-later
4*/
5
6#include "navmeshbuilder.h"
7
8#include "navmesh.h"
9#include "navmesh_p.h"
10#include "navmeshtransform.h"
11#include "recastnav_p.h"
12// #include "recastnavdebug_p.h"
13#include "recastnavsettings_p.h"
14#include "routingarea.h"
15#include <logging.h>
16
17#include <KOSMIndoorMap/MapData>
18#include <KOSMIndoorMap/MapCSSParser>
19#include <KOSMIndoorMap/MapCSSResult>
20#include <KOSMIndoorMap/MapCSSStyle>
21#include <KOSMIndoorMap/OverlaySource>
22
23#include <loader/levelparser_p.h>
24#include <qloggingcategory.h>
25#include <scene/penwidthutil_p.h>
26#include <scene/scenegraphitem.h>
27#include <style/mapcssdeclaration_p.h>
28#include <style/mapcssstate_p.h>
29
30#include <QFile>
31#include <QPolygonF>
32#include <QPainterPath>
33#include <QThreadPool>
34
35#include <private/qtriangulator_p.h>
36#include <private/qtriangulatingstroker_p.h>
37
38#if HAVE_RECAST
39#include <DetourNavMeshBuilder.h>
40#endif
41
42#include <cmath>
43#include <unordered_map>
44#include <unordered_set>
45
46class rcContext;
47
48namespace KOSMIndoorRouting {
49
50enum class LinkDirection { Forward, Backward, Bidirectional };
51
52// ordered by priority, must match m_areaClassKeys array below
53struct {
54 const char *name;
55 AreaType area;
56} constexpr inline const routing_area_map[] = {
57 { "escalator", AreaType::Escalator },
58 { "movingWalkway", AreaType::MovingWalkway },
59 { "stairs", AreaType::Stairs },
60 { "elevator", AreaType::Elevator },
61 { "tactilePaving", AreaType::TactilePaving },
62 { "streetCrossing", AreaType::StreetCrossing },
63 { "ramp", AreaType::Ramp },
64 { "room", AreaType::Room },
65};
66
67class NavMeshBuilderPrivate
68{
69public:
70 [[nodiscard]] std::optional<LinkDirection> linkDirection(KOSMIndoorMap::LayerSelectorKey layerKey) const;
71 [[nodiscard]] AreaType areaType(const KOSMIndoorMap::MapCSSResultLayer &result) const;
72
73 /** Look up level for a given node id. */
74 [[nodiscard]] int levelForNode(OSM::Id nodeId) const;
75 void addNodeToLevelIndex(OSM::Id nodeId, int level);
76 void indexNodeLevels();
77
78 void processElement(OSM::Element elem, int floorLevel);
79 void processGeometry(OSM::Element elem, int floorLevel, const KOSMIndoorMap::MapCSSResultLayer &res);
80 void processLink(OSM::Element elem, int floorLevel, LinkDirection linkDir, const KOSMIndoorMap::MapCSSResultLayer &res);
81
82 /** 0 if @p node is not a door, the width of the door when specified, otherwise a default assumption. */
83 [[nodiscard]] double doorWidth(const OSM::Node *node);
84 void extrudeWall(const std::vector<const OSM::Node*> &way, int floorLevel);
85
86 void addVertex(float x, float y, float z);
87 void addFace(std::size_t i, std::size_t j, std::size_t k, AreaType areaType);
88 void addOffMeshConnection(float x1, float y1, float z1, float x2, float y2, float z2, LinkDirection linkDir, AreaType areaType);
89
90 void buildNavMesh();
91 [[nodiscard]] bool buildNavMeshTile(rcContext *ctx, int tx, int ty, NavMeshPrivate *result);
92
93 void writeGsetFile();
94 void writeObjFile();
95
98 KOSMIndoorMap::MapCSSResult m_filterResult;
99 KOSMIndoorMap::MapCSSResult m_filterResultL2;
100
101 std::array<KOSMIndoorMap::LayerSelectorKey, 3> m_linkKeys;
102 std::array<KOSMIndoorMap::ClassSelectorKey, std::size(routing_area_map)> m_areaClassKeys;
103
104 NavMeshTransform m_transform;
105
106 std::unordered_map<OSM::Id, int> m_nodeLevelMap;
107 KOSMIndoorMap::AbstractOverlaySource *m_equipmentModel = nullptr;
108
109 std::unordered_set<OSM::Element> m_processedLinks;
110
111 // triangle data
112 std::vector<float> m_verts;
113 inline int numVerts() const { return (int)m_verts.size() / 3; }
114 std::vector<int> m_tris;
115 inline int numTris() const { return (int)m_tris.size() / 3; }
116 std::vector<uint8_t> m_triAreaIds;
117
118 // off mesh connection data
119 struct {
120 std::vector<float> verts;
121 std::vector<float> rads;
122 std::vector<uint16_t> flags;
123 std::vector<uint8_t> areas;
124 std::vector<uint8_t> dir;
125 std::vector<uint32_t> userId;
126 } m_offMeshCon;
127 inline int offMeshCount() const { return (int) m_offMeshCon.rads.size(); }
128
129 NavMesh m_navMesh;
130
131 struct {
132 OSM::TagKey door;
133 OSM::TagKey entrance;
134 OSM::TagKey indoor;
135 OSM::TagKey highway;
136 OSM::TagKey room;
137 OSM::TagKey stairs;
138 } m_tagKeys;
139
140 // diganostic obj output
141 QString m_gsetFileName;
142 QString m_objFileName;
143 qsizetype m_vertexOffset = 0;
144};
145}
146
147using namespace KOSMIndoorRouting;
148using namespace Qt::Literals::StringLiterals;
149
150//BEGIN TODO largely copied from SceneController, refactor/unify?
151static QPolygonF createPolygon(const OSM::DataSet &dataSet, OSM::Element e)
152{
153 const auto path = e.outerPath(dataSet);
154 if (path.empty()) {
155 return {};
156 }
157
158 QPolygonF poly;
159 // Element::outerPath takes care of re-assembling broken up line segments
160 // the below takes care of properly merging broken up polygons
161 for (auto it = path.begin(); it != path.end();) {
162 QPolygonF subPoly;
163 subPoly.reserve(path.size());
164 OSM::Id pathBegin = (*it)->id;
165
166 auto subIt = it;
167 for (; subIt != path.end(); ++subIt) {
168 subPoly.push_back(QPointF((*subIt)->coordinate.lonF(), (*subIt)->coordinate.latF()));
169 if ((*subIt)->id == pathBegin && subIt != it && subIt != std::prev(path.end())) {
170 ++subIt;
171 break;
172 }
173 }
174 it = subIt;
175 poly = poly.isEmpty() ? std::move(subPoly) : poly.united(subPoly);
176 }
177 return poly;
178}
179
180// @see https://wiki.openstreetmap.org/wiki/Relation:multipolygon
181static QPainterPath createPath(const OSM::DataSet &dataSet, const OSM::Element e)
182{
183 assert(e.type() == OSM::Type::Relation);
184 QPolygonF outerPath = createPolygon(dataSet, e); // TODO this is actually not correct for the multiple outer polygon case
186 path.setFillRule(Qt::OddEvenFill);
187
188 for (const auto &mem : e.relation()->members) {
189 const bool isInner = std::strcmp(mem.role().name(), "inner") == 0;
190 const bool isOuter = std::strcmp(mem.role().name(), "outer") == 0;
191 if (mem.type() != OSM::Type::Way || (!isInner && !isOuter)) {
192 continue;
193 }
194 if (auto way = dataSet.way(mem.id)) {
195 const auto subPoly = createPolygon(dataSet, OSM::Element(way));
196 if (subPoly.isEmpty()) {
197 continue;
198 }
199 path.addPolygon(subPoly);
200 path.closeSubpath();
201 }
202 }
203
204 return path;
205}
206//END
207
208NavMeshBuilder::NavMeshBuilder(QObject *parent)
209 : QObject(parent)
210 , d(std::make_unique<NavMeshBuilderPrivate>())
211{
212}
213
214NavMeshBuilder::~NavMeshBuilder() = default;
215
216void NavMeshBuilder::setMapData(const KOSMIndoorMap::MapData &mapData)
217{
218 d->m_data = mapData;
219
220 if (d->m_style.isEmpty()) {
222 d->m_style = p.parse(QStringLiteral(":/org.kde.kosmindoorrouting/navmesh-filter.mapcss"));
223 if (p.hasError()) {
224 qCWarning(Log) << p.errorMessage();
225 return;
226 }
227
228 // resolve layer and class keys
229 constexpr const char *link_direction_keys[] = { "link_forward", "link_backward", "link" };
230 for (auto dir : {LinkDirection::Forward, LinkDirection::Backward, LinkDirection::Bidirectional}) {
231 d->m_linkKeys[qToUnderlying(dir)] = d->m_style.layerKey(link_direction_keys[qToUnderlying(dir)]);
232 }
233
234 for (std::size_t i = 0; i < std::size(routing_area_map); ++i) {
235 d->m_areaClassKeys[i] = d->m_style.classKey(routing_area_map[i].name);
236 if (d->m_areaClassKeys[i].isNull()) {
237 qCWarning(Log) << "area class key not found:" << routing_area_map[i].name;
238 }
239 }
240 }
241
242 if (!d->m_data.isEmpty()) {
243 d->m_style.compile(d->m_data.dataSet());
244
245 d->m_tagKeys.door = d->m_data.dataSet().tagKey("door");
246 d->m_tagKeys.entrance = d->m_data.dataSet().tagKey("entrance");
247 d->m_tagKeys.indoor = d->m_data.dataSet().tagKey("indoor");
248 d->m_tagKeys.highway = d->m_data.dataSet().tagKey("highway");
249 d->m_tagKeys.room = d->m_data.dataSet().tagKey("room");
250 d->m_tagKeys.stairs = d->m_data.dataSet().tagKey("stairs");
251 }
252}
253
254void NavMeshBuilder::setEquipmentModel(KOSMIndoorMap::AbstractOverlaySource *equipmentModel)
255{
256 d->m_equipmentModel = equipmentModel;
257 // TODO can we do incremental updates when a realtime elevator status changes?
258}
259
260void NavMeshBuilder::writeDebugNavMesh(const QString &gsetFile, const QString &objFile)
261{
262 d->m_gsetFileName = gsetFile;
263 d->m_objFileName = objFile;
264}
265
266std::optional<LinkDirection> NavMeshBuilderPrivate::linkDirection(KOSMIndoorMap::LayerSelectorKey layerKey) const
267{
268 for (auto dir : {LinkDirection::Forward, LinkDirection::Backward, LinkDirection::Bidirectional}) {
269 if (!m_linkKeys[qToUnderlying(dir)].isNull() && m_linkKeys[qToUnderlying(dir)] == layerKey) {
270 return dir;
271 }
272 }
273 return {};
274}
275
276AreaType NavMeshBuilderPrivate::areaType(const KOSMIndoorMap::MapCSSResultLayer &res) const
277{
278 for (std::size_t i = 0; i < m_areaClassKeys.size(); ++i) {
279 if (!m_areaClassKeys[i].isNull() && res.hasClass(m_areaClassKeys[i])) {
280 return routing_area_map[i].area;
281 }
282 }
283
284 return AreaType::Walkable;
285}
286
287int NavMeshBuilderPrivate::levelForNode(OSM::Id nodeId) const
288{
289 const auto it = m_nodeLevelMap.find(nodeId);
290 return it != m_nodeLevelMap.end() ? (*it).second : 0;
291}
292
293void NavMeshBuilderPrivate::addNodeToLevelIndex(OSM::Id nodeId, int level)
294{
295 auto it = m_nodeLevelMap.find(nodeId);
296 if (it == m_nodeLevelMap.end()) {
297 m_nodeLevelMap[nodeId] = level;
298 return;
299 }
300 if ((*it).second != level) {
301 (*it).second = std::numeric_limits<int>::min();
302 }
303}
304
305void NavMeshBuilderPrivate::indexNodeLevels()
306{
307 for (const auto &level : m_data.levelMap()) {
308 if (level.first.numericLevel() == 0) {
309 continue;
310 }
311 for (const auto elem : level.second) {
312 switch (elem.type()) {
313 case OSM::Type::Null:
314 Q_UNREACHABLE();
315 case OSM::Type::Node:
316 continue;
317 case OSM::Type::Way:
318 {
319 // TODO ignore multi-level ways
320 const auto lvl = elem.tagValue("level");
321 if (lvl.isEmpty() || lvl.contains(';')) {
322 break;
323 }
324 for (OSM::Id nodeId : elem.way()->nodes) {
325 addNodeToLevelIndex(nodeId, level.first.numericLevel());
326 }
327 break;
328 }
329 case OSM::Type::Relation:
330 // TODO
331 break;
332 }
333 }
334 }
335}
336
337void NavMeshBuilder::start()
338{
339 // the first half of this where we access m_data runs in the main thread (as MapData isn't prepared for multi-threaded access)
340 qCDebug(Log) << QThread::currentThread();
341
342 d->m_transform.initialize(d->m_data.boundingBox());
343 d->indexNodeLevels();
344
345 std::vector<OSM::Element> hiddenElements;
346 if (d->m_equipmentModel) {
347 d->m_equipmentModel->hiddenElements(hiddenElements);
348 }
349 std::sort(hiddenElements.begin(), hiddenElements.end());
350
351 for (const auto &level : d->m_data.levelMap()) {
352 for (const auto &elem : level.second) {
353 if (std::binary_search(hiddenElements.begin(), hiddenElements.end(), elem)) {
354 continue;
355 }
356 d->processElement(elem, level.first.numericLevel());
357 }
358
359 if (level.first.numericLevel() % 10 || !d->m_equipmentModel) {
360 continue;
361 }
362 d->m_equipmentModel->forEach(level.first.numericLevel(), [this](OSM::Element elem, int floorLevel) {
363 d->processElement(elem, floorLevel);
364 });
365 }
366
367 [[unlikely]] if (!d->m_gsetFileName.isEmpty()) {
368 d->writeGsetFile();
369 d->writeObjFile();
370 }
371
372 qCDebug(Log) << "Vertex data size:" << d->m_verts.size() * sizeof(float);
373 qCDebug(Log) << "Triangle index size:" << d->m_tris.size() * sizeof(int);
374 qCDebug(Log) << "Triangle area size:" << d->m_triAreaIds.size();
375 qCDebug(Log) << "Off-mesh data size:" << d->offMeshCount() * 16;
376
377 // the second half of this (which takes the majority of the time) runs in a secondary thread
379 d->buildNavMesh();
380 QMetaObject::invokeMethod(this, &NavMeshBuilder::finished, Qt::QueuedConnection);
381 });
382}
383
384void NavMeshBuilderPrivate::processElement(OSM::Element elem, int floorLevel)
385{
386 if (!elem.hasTags()) {
387 return;
388 }
389
390 KOSMIndoorMap::MapCSSState filterState;
391 filterState.element = elem;
392 m_style.initializeState(filterState);
393 m_style.evaluate(filterState, m_filterResult);
394
395 for (const auto &res : m_filterResult.results()) {
396 if (res.layerSelector().isNull()) {
397 processGeometry(elem, floorLevel, res);
398 } else if (const auto linkDir = linkDirection(res.layerSelector()); linkDir) {
399 processLink(elem, floorLevel, *linkDir, res);
400 }
401 }
402}
403
404void NavMeshBuilderPrivate::processGeometry(OSM::Element elem, int floorLevel, const KOSMIndoorMap::MapCSSResultLayer &res)
405{
406 if (res.hasAreaProperties()) {
408 if (prop && prop->doubleValue() > 0.0) {
410 if (elem.type() == OSM::Type::Relation) {
411 path = createPath(m_data.dataSet(), elem);
412 } else {
413 path.addPolygon(createPolygon(m_data.dataSet(), elem));
414 }
415
416 QPainterPath p;
417 const auto triSet = qTriangulate(m_transform.mapGeoToNav(path));
418 qCDebug(Log) << "A" << elem.url() << m_transform.mapGeoToNav(path).boundingRect() << path.elementCount() << triSet.indices.size() << triSet.vertices.size() << m_vertexOffset << floorLevel;
419
420 for (qsizetype i = 0; i < triSet.vertices.size(); i += 2) {
421 addVertex(triSet.vertices[i], m_transform.mapHeightToNav(floorLevel), triSet.vertices[i + 1]);
422 }
423 if (triSet.indices.type() == QVertexIndexVector::UnsignedShort) {
424 for (qsizetype i = 0; i <triSet.indices.size(); i += 3) {
425 addFace(*(reinterpret_cast<const uint16_t*>(triSet.indices.data()) + i) + m_vertexOffset,
426 *(reinterpret_cast<const uint16_t*>(triSet.indices.data()) + i + 1) + m_vertexOffset,
427 *(reinterpret_cast<const uint16_t*>(triSet.indices.data()) + i + 2) + m_vertexOffset,
428 areaType(res));
429 }
430 } else if (triSet.indices.type() == QVertexIndexVector::UnsignedInt) {
431 for (qsizetype i = 0; i <triSet.indices.size(); i += 3) {
432 addFace(*(reinterpret_cast<const uint32_t*>(triSet.indices.data()) + i) + m_vertexOffset,
433 *(reinterpret_cast<const uint32_t*>(triSet.indices.data()) + i + 1) + m_vertexOffset,
434 *(reinterpret_cast<const uint32_t*>(triSet.indices.data()) + i + 2) + m_vertexOffset,
435 areaType(res));
436 }
437 }
438 m_vertexOffset += triSet.vertices.size() / 2;
439 }
440 }
441
442 if (res.hasLineProperties()) {
444 KOSMIndoorMap::Unit dummyUnit;
445 if (const auto penWidth = prop ? KOSMIndoorMap::PenWidthUtil::penWidth(elem, prop, dummyUnit) : 0.0; penWidth > 0.0) {
446 QPolygonF poly = m_transform.mapGeoToNav(createPolygon(m_data.dataSet(), elem));
448 path.addPolygon(poly);
449 QPen pen;
450 // TODO join/cap styles
452 pen.setWidthF(penWidth);
453
454 QTriangulatingStroker stroker;
455 stroker.process(qtVectorPathForPath(path), pen, {}, {});
456 qCDebug(Log) << "L" << elem.url() << stroker.vertexCount() << pen.widthF();
457
458 for (int i = 0; i < stroker.vertexCount(); i += 2) {
459 auto l = floorLevel;
460 if (poly.size() == 2 && elem.type() == OSM::Type::Way) { // TODO can we generalize this?
461 const auto way = elem.way();
462 const auto l1 = levelForNode(way->nodes.at(0));
463 const auto l2 = levelForNode(way->nodes.at(1));
464 qCDebug(Log) << " S" << elem.url() << floorLevel << l1 << l2;
465 if (l1 != l2 && l1 != std::numeric_limits<int>::min() && l2 != std::numeric_limits<int>::min()) {
466 QPointF p(*(stroker.vertices() + i), *(stroker.vertices() + i + 1));
467 l = QLineF(poly.at(0), p).length() < QLineF(poly.at(1), p).length() ? l1 : l2;
468 }
469 }
470 addVertex(*(stroker.vertices() + i), m_transform.mapHeightToNav(l), *(stroker.vertices() + i + 1));
471 }
472 for (int i = 0; i < stroker.vertexCount() / 2 - 2; ++i) {
473 // GL_TRIANGLE_STRIP winding order
474 if (i % 2) {
475 addFace(m_vertexOffset + i, m_vertexOffset + i + 1, m_vertexOffset + i + 2, areaType(res));
476 } else {
477 addFace(m_vertexOffset + i + 1, m_vertexOffset + i, m_vertexOffset + i + 2, areaType(res));
478 }
479 }
480 m_vertexOffset += stroker.vertexCount() / 2;
481 }
482 }
483
484 if (res.hasExtrudeProperties()) {
486 if (prop && prop->doubleValue() > 0.0) {
487 // TODO support multi polygons
488 const auto way = elem.outerPath(m_data.dataSet());
489
490 // ### HACK work around staircases and elevators that are mapped with walls but without doors
491 // this is fairly common in train stations in one way or the other
492 const auto highway = elem.tagValue(m_tagKeys.highway);
493 const auto indoor = elem.tagValue(m_tagKeys.indoor);
494 const auto room = elem.tagValue(m_tagKeys.room);
495 if (highway == "elevator" || room == "stairs" || room == "steps" ||
496 (indoor == "room" && elem.tagValue(m_tagKeys.stairs) == "yes") ||
497 (indoor == "yes" && highway == "steps")) {
498 const auto isDoor = [this](const OSM::Node *node) {
499 return !OSM::tagValue(*node, m_tagKeys.door).isEmpty() || !OSM::tagValue(*node, m_tagKeys.entrance).isEmpty();
500 };
501 if (std::none_of(way.begin(), way.end(), isDoor)) {
502 qCDebug(Log) << "skipping walls for door-less room" << elem.url();
503 return;
504 }
505 }
506 extrudeWall(way, floorLevel);
507 }
508 }
509}
510
511void NavMeshBuilderPrivate::processLink(OSM::Element elem, int floorLevel, LinkDirection linkDir, const KOSMIndoorMap::MapCSSResultLayer &res)
512{
513 if (m_processedLinks.contains(elem)) {
514 return;
515 }
516
517 if (res.hasAreaProperties()) {
519 if (prop && prop->doubleValue() <= 0.0) {
520 return;
521 }
522
523 std::vector<int> levels;
524 KOSMIndoorMap::LevelParser::parse(elem.tagValue("level"), elem, [&levels](int level, auto) { levels.push_back(level); });
525 if (levels.size() > 1) {
526 qDebug() << "E" << elem.url() << levels;
527 // TODO doesn't work for concave polygons!
528 const QPointF p = m_transform.mapGeoToNav(elem.center());
529 for (std::size_t i = 0; i < levels.size() - 1; ++i) {
530 addOffMeshConnection(p.x(), m_transform.mapHeightToNav(levels[i]), p.y(), p.x(), m_transform.mapHeightToNav(levels[i + 1]), p.y(), LinkDirection::Bidirectional, areaType(res));
531 }
532 m_processedLinks.insert(elem);
533 }
534 }
535 if (res.hasLineProperties() && elem.type() == OSM::Type::Way) {
537 KOSMIndoorMap::Unit dummyUnit;
538 if (const auto penWidth = prop ? KOSMIndoorMap::PenWidthUtil::penWidth(elem, prop, dummyUnit) : 0.0; penWidth <= 0.0) {
539 return;
540 }
541
542 const auto way = elem.way();
543 if (way->nodes.size() == 2) {
544 auto l1 = levelForNode(way->nodes.at(0));
545 auto l2 = levelForNode(way->nodes.at(1));
546
547 std::vector<int> levels;
548 KOSMIndoorMap::LevelParser::parse(elem.tagValue("level"), elem, [&levels](int level, auto) { levels.push_back(level); });
549 if (levels.size() == 2) {
550 auto l1b = levels[0];
551 auto l2b = levels[1];
552 if (l1 == l2b) {
553 std::swap(l1b, l2b);
554 }
555 if (l1 == l1b && l2 != l2b && (l2 == 0 || l2 == std::numeric_limits<int>::min())) {
556 l2 = l2b;
557 } else if (l2 == l2b && l1 != l1b && (l1 == 0 || l1 == std::numeric_limits<int>::min())) {
558 l1 = l1b;
559 }
560 }
561
562 if (l1 != l2 && l1 != std::numeric_limits<int>::min() && l2 != std::numeric_limits<int>::min()) {
563 qCDebug(Log) << " LINK" << elem.url() << floorLevel << l1 << l2 << levels;
564 const auto poly = createPolygon(m_data.dataSet(), elem);
565 const auto p1 = m_transform.mapGeoToNav(poly.at(0));
566 const auto p2 = m_transform.mapGeoToNav(poly.at(1));
567 addOffMeshConnection(p1.x(), m_transform.mapHeightToNav(l1), p1.y(), p2.x(), m_transform.mapHeightToNav(l2), p2.y(), linkDir, areaType(res));
568 } else {
569 qCDebug(Log) << " failed to determin levels for link" << elem.url() <<floorLevel << l1 << l2 << levels;
570 }
571 m_processedLinks.insert(elem);
572 }
573 }
574}
575
576double NavMeshBuilderPrivate::doorWidth(const OSM::Node *node)
577{
578 if (node->tags.empty()) {
579 return 0.0;
580 }
581
582 KOSMIndoorMap::MapCSSState state;
583 state.element = node;
584 m_filterResultL2.clear();
585 m_style.initializeState(state);
586 m_style.evaluate(state, m_filterResultL2);
587
588 const auto &layer = m_filterResultL2[{}];
589 auto prop = layer.declaration(KOSMIndoorMap::MapCSSProperty::Opacity);
590 if (!prop || prop->doubleValue() <= 0.0) {
591 return 0.0;
592 }
593 prop = layer.declaration(KOSMIndoorMap::MapCSSProperty::Width);
594 return prop ? std::max(prop->doubleValue(), 0.0) : 1.0;
595}
596
597void NavMeshBuilderPrivate::extrudeWall(const std::vector<const OSM::Node*> &way, int floorLevel)
598{
599 bool reuseEdge = false;
600 for (std::size_t i = 0; i < way.size() - 1; ++i) {
601 auto p1 = m_transform.mapGeoToNav(way[i]->coordinate);
602 auto p2 = m_transform.mapGeoToNav(way[i + 1]->coordinate);
603
604 QLineF line(p1, p2);
605 if (const auto w = doorWidth(way[i]); w >= 0.0) {
606 reuseEdge = false;
607 if (line.length() < w) {
608 continue;
609 }
610 QLineF reverseLine(p2, p1);
611 reverseLine.setLength(line.length() - w);
612 p1 = reverseLine.p2();
613 }
614
615 if (auto w = doorWidth(way[i + 1]); w >= 0.0) {
616 reuseEdge = false;
617 if (line.length() < w) {
618 continue;
619 }
620 line.setLength(line.length() - w);
621 p2 = line.p2();
622 }
623
624 qsizetype offset = m_vertexOffset;
625 if (!reuseEdge) {
626 addVertex((float)p1.x(), m_transform.mapHeightToNav(floorLevel), (float)p1.y());
627 addVertex((float)p1.x(), m_transform.mapHeightToNav(floorLevel + 10), (float)p1.y());
628 m_vertexOffset += 2;
629 reuseEdge = true;
630 } else {
631 assert(m_vertexOffset >= 2);
632 offset -= 2;
633 }
634
635 addVertex((float)p2.x(), m_transform.mapHeightToNav(floorLevel), (float)p2.y());
636 addVertex((float)p2.x(), m_transform.mapHeightToNav(floorLevel + 10), (float)p2.y());
637 m_vertexOffset += 2;
638
639 addFace(offset, offset + 2, offset + 1, AreaType::Unwalkable);
640 addFace(offset + 2, offset + 3, offset + 1, AreaType::Unwalkable);
641 }
642}
643
644void NavMeshBuilderPrivate::addVertex(float x, float y, float z)
645{
646 for (const auto v : {x, y, z}) {
647 m_verts.push_back(v);
648 }
649}
650
651void NavMeshBuilderPrivate::addFace(std::size_t i, std::size_t j, std::size_t k, AreaType areaType)
652{
653 for (const auto v : {i, j, k}) {
654 m_tris.push_back((int)v);
655 }
656 m_triAreaIds.push_back(qToUnderlying(areaType));
657}
658
659void NavMeshBuilderPrivate::addOffMeshConnection(float x1, float y1, float z1, float x2, float y2, float z2, LinkDirection linkDir, AreaType areaType)
660{
661 if (linkDir == LinkDirection::Backward) {
662 std::swap(x1, x2);
663 std::swap(y1, y2);
664 std::swap(z1, z2);
665 linkDir = LinkDirection::Forward;
666 }
667
668 for (const auto v : { x1, y1, z1, x2, y2, z2 }) {
669 m_offMeshCon.verts.push_back(v);
670 }
671 m_offMeshCon.rads.push_back(0.6); // ???
672 m_offMeshCon.flags.push_back(flagsForAreaType(areaType));
673 m_offMeshCon.areas.push_back(qToUnderlying(areaType));
674 m_offMeshCon.dir.push_back(linkDir == LinkDirection::Bidirectional ? 1 : 0);
675 m_offMeshCon.userId.push_back(0); // ???
676}
677
678void NavMeshBuilderPrivate::writeGsetFile()
679{
680 QFile f(m_gsetFileName);
681 f.open(QFile::WriteOnly);
682 f.write("f ");
683 f.write(m_objFileName.toUtf8());
684 f.write("\n");
685
686 f.write("s ");
687 f.write(QByteArray::number(RECAST_CELL_SIZE));
688 f.write(" ");
689 f.write(QByteArray::number(RECAST_CELL_HEIGHT));
690 f.write(" ");
691
692 f.write(QByteArray::number(RECAST_AGENT_HEIGHT));
693 f.write(" ");
694 f.write(QByteArray::number(RECAST_AGENT_RADIUS));
695 f.write(" ");
696 f.write(QByteArray::number(RECAST_AGENT_MAX_CLIMB));
697 f.write(" ");
698 f.write(QByteArray::number(RECAST_AGENT_MAX_SLOPE));
699 f.write(" ");
700
701 f.write(QByteArray::number(RECAST_REGION_MIN_AREA));
702 f.write(" ");
703 f.write(QByteArray::number(RECAST_REGION_MERGE_AREA));
704 f.write(" ");
705 f.write(QByteArray::number(RECAST_MAX_EDGE_LEN));
706 f.write(" ");
707 f.write(QByteArray::number(RECAST_MAX_SIMPLIFICATION_ERROR));
708 f.write(" 6 "); // vertex per polygon
709 f.write(QByteArray::number(RECAST_DETAIL_SAMPLE_DIST));
710 f.write(" ");
711 f.write(QByteArray::number(RECAST_DETAIL_SAMPLE_MAX_ERROR));
712 f.write(" ");
713 f.write(QByteArray::number(qToUnderlying(RECAST_PARTITION_TYPE))); // partition type
714 f.write(" ");
715
716 // bbox min
717 auto p = m_transform.mapGeoToNav(m_data.boundingBox().min);
718 f.write(QByteArray::number(p.x()));
719 f.write(" ");
720 f.write(QByteArray::number(std::prev(m_data.levelMap().end())->first.numericLevel()));
721 f.write(" ");
722 f.write(QByteArray::number(p.y()));
723 f.write(" ");
724
725 // bbox max
726 p = m_transform.mapGeoToNav(m_data.boundingBox().max);
727 f.write(QByteArray::number(p.x()));
728 f.write(" ");
729 f.write(QByteArray::number(m_data.levelMap().begin()->first.numericLevel()));
730 f.write(" ");
731 f.write(QByteArray::number(p.y()));
732 f.write(" ");
733
734 f.write("0\n"); // tile size?
735
736 for (int i = 0; i < offMeshCount(); ++i) {
737 f.write("c ");
738 for (int j = 0; j < 6; ++j) {
739 f.write(QByteArray::number(m_offMeshCon.verts[i * 6 + j]));
740 f.write(" ");
741 }
742 f.write(QByteArray::number(m_offMeshCon.rads[i]));
743 f.write(" ");
744 f.write(QByteArray::number(m_offMeshCon.dir[i]));
745 f.write(" ");
746 f.write(QByteArray::number(m_offMeshCon.areas[i]));
747 f.write(" ");
748 f.write(QByteArray::number(m_offMeshCon.flags[i]));
749 f.write("\n");
750 }
751}
752
753void NavMeshBuilderPrivate::writeObjFile()
754{
755 QFile f(m_objFileName);
756 f.open(QFile::WriteOnly);
757
758 for (std::size_t i = 0; i < m_verts.size(); i += 3) {
759 f.write("v ");
760 f.write(QByteArray::number(m_verts[i]));
761 f.write(" ");
762 f.write(QByteArray::number(m_verts[i+1]));
763 f.write(" ");
764 f.write(QByteArray::number(m_verts[i+2]));
765 f.write("\n");
766 }
767
768 for (std::size_t i = 0; i < m_tris.size(); i += 3) {
769 f.write("f ");
770 f.write(QByteArray::number(m_tris[i] + 1));
771 f.write(" ");
772 f.write(QByteArray::number(m_tris[i+1] + 1));
773 f.write(" ");
774 f.write(QByteArray::number(m_tris[i+2] + 1));
775 f.write("\n");
776 }
777}
778
779void NavMeshBuilderPrivate::buildNavMesh()
780{
781 qCDebug(Log) << QThread::currentThread();
782
783 const auto bmin = m_transform.mapGeoHeightToNav(m_data.boundingBox().min, std::prev(m_data.levelMap().end())->first.numericLevel());
784 const auto bmax = m_transform.mapGeoHeightToNav(m_data.boundingBox().max, m_data.levelMap().begin()->first.numericLevel());
785
786 NavMesh resultData;
787 const auto result = NavMeshPrivate::create(resultData);
788 result->m_transform = m_transform;
789 result->m_updateSignal = QObject::connect(m_equipmentModel, &KOSMIndoorMap::AbstractOverlaySource::update, m_equipmentModel, [result]() {
790 result->m_dirty = true;
791 qCDebug(Log) << "nav mesh invalidated";
793
794 // steps as defined in the Recast demo app
795#if HAVE_RECAST
796 // step 1: setup
797 rcContext ctx;
798 int width = 0;
799 int height = 0;
800 rcCalcGridSize(bmin, bmax, RECAST_CELL_SIZE, &width, &height);
801 qCDebug(Log) << width << "x" << height << "cells";
802
803 const auto tileWidth = (width + RECAST_TILE_SIZE - 1) / RECAST_TILE_SIZE;
804 const auto tileHeight = (height + RECAST_TILE_SIZE - 1) / RECAST_TILE_SIZE;
805 qCDebug(Log) << tileWidth << "x" << tileHeight << "tiles";
806
807 // from Sample_TileMesh.cpp of Recast:
808 // Max tiles and max polys affect how the tile IDs are caculated.
809 // There are 22 bits available for identifying a tile and a polygon.
810 const auto tileBits = std::min<int>(std::ceil(std::log2(tileWidth * tileHeight)), 14);
811 const auto polyBits = 22 - tileBits;
812 qCDebug(Log) << "using" << tileBits << "bits for tiles and" << polyBits << "bits for polygons";
813
814 result->m_navMesh.reset(dtAllocNavMesh());
815 {
816 dtNavMeshParams params;
817 rcVcopy(params.orig, bmin);
818 params.tileWidth = RECAST_TILE_SIZE * RECAST_CELL_SIZE;
819 params.tileHeight = RECAST_TILE_SIZE * RECAST_CELL_SIZE;
820 params.maxTiles = (1 << tileBits);
821 params.maxPolys = (1 << polyBits);
822
823 result->m_navMesh->init(&params);
824 }
825
826 for (int tx = 0; tx < tileWidth; ++tx) {
827 for (int ty = 0; ty < tileHeight; ++ty) {
828 if (!buildNavMeshTile(&ctx, tx, ty, result)) {
829 return;
830 }
831 }
832 }
833
834 result->m_navMeshQuery.reset(dtAllocNavMeshQuery());
835 const auto status = result->m_navMeshQuery->init(result->m_navMesh.get(), RECAST_NAV_QUERY_MAX_NODES);
836 if (dtStatusFailed(status)) {
837 qCWarning(Log) << "Failed to init dtNavMeshQuery";
838 return;
839 }
840
841 m_navMesh = std::move(resultData);
842 qCDebug(Log) << "done";
843#endif
844}
845
846bool NavMeshBuilderPrivate::buildNavMeshTile(rcContext *ctx, int tx, int ty, NavMeshPrivate *result)
847{
848 qCDebug(Log) << " building tile" << tx << ty;
849
850 const auto walkableHeight = (int)std::ceil(RECAST_AGENT_HEIGHT / RECAST_CELL_HEIGHT);
851 const auto walkableClimb = (int)std::floor(RECAST_AGENT_MAX_CLIMB/ RECAST_CELL_HEIGHT);
852 const auto walkableRadius = (int)std::ceil(RECAST_AGENT_RADIUS / RECAST_CELL_SIZE);
853 const auto borderSize = (float)walkableRadius + 3.0f;
854
855 // tile boundaries
856 auto bmin = m_transform.mapGeoHeightToNav(m_data.boundingBox().min, std::prev(m_data.levelMap().end())->first.numericLevel());
857 bmin.x += (float)tx * RECAST_TILE_SIZE * RECAST_CELL_SIZE;
858 bmin.z += (float)ty * RECAST_TILE_SIZE * RECAST_CELL_SIZE;
859
860 auto bmax = m_transform.mapGeoHeightToNav(m_data.boundingBox().max, m_data.levelMap().begin()->first.numericLevel());
861 bmax.x = std::min(bmax.x, bmin.x + RECAST_TILE_SIZE * RECAST_CELL_SIZE);
862 bmax.z = std::min(bmax.z, bmin.z + RECAST_TILE_SIZE * RECAST_CELL_SIZE);
863
864 // expand tile to slightly overlap with neighboring tiles for get things to connect properly
865 bmin.x -= borderSize * RECAST_CELL_SIZE;
866 bmin.z -= borderSize * RECAST_CELL_SIZE;
867 bmax.x += borderSize * RECAST_CELL_SIZE;
868 bmax.z += borderSize * RECAST_CELL_SIZE;
869
870#if HAVE_RECAST
871 // step 1: setup
872 int width = 0;
873 int height = 0;
874 rcCalcGridSize(bmin, bmax, RECAST_CELL_SIZE, &width, &height);
875
876 // step 2: build input polygons
877 rcHeightfieldPtr solid(rcAllocHeightfield());
878 if (!rcCreateHeightfield(ctx, *solid, width, height, bmin, bmax, RECAST_CELL_SIZE, RECAST_CELL_HEIGHT)) {
879 qCWarning(Log) << "Failed to create solid heightfield.";
880 return false;
881 }
882
883 if (!rcRasterizeTriangles(ctx, m_verts.data(), numVerts(), m_tris.data(), m_triAreaIds.data(), numTris(), *solid, walkableClimb)) {
884 qCWarning(Log) << "Failed to rasterize triangles";
885 return false;
886 }
887
888 // step 3: filter walkable sufaces
889
890 rcFilterLowHangingWalkableObstacles(ctx, walkableClimb, *solid);
891 rcFilterLedgeSpans(ctx, walkableHeight, walkableClimb, *solid);
892 rcFilterWalkableLowHeightSpans(ctx, walkableHeight, *solid);
893
894 // step 4: partition surface into regions
895 rcCompactHeightfieldPtr chf(rcAllocCompactHeightfield());
896 if (!rcBuildCompactHeightfield(ctx, walkableHeight, walkableClimb, *solid, *chf)) {
897 qCWarning(Log) << "Failed to build compact height field.";
898 return false;
899 }
900 solid.reset();
901
902 if (!rcErodeWalkableArea(ctx, walkableRadius, *chf)) {
903 qCWarning(Log) << "Failed to erode walkable area";
904 return false;
905 }
906
907 if constexpr (RECAST_PARTITION_TYPE == RecastPartitionType::Monotone) {
908 if (!rcBuildRegionsMonotone(ctx, *chf, (int)borderSize, (int)std::pow(RECAST_REGION_MIN_AREA, 2.0), (int)std::pow(RECAST_REGION_MERGE_AREA, 2.0))) {
909 qCWarning(Log) << "Failed to build monotone regions";
910 return false;
911 }
912 } else if constexpr (RECAST_PARTITION_TYPE == RecastPartitionType::Watershed) {
913 if (!rcBuildDistanceField(ctx, *chf)) {
914 qCWarning(Log) << "Failed to build distance field.";
915 return false;
916 }
917 if (!rcBuildRegions(ctx, *chf, (int)borderSize, (int)std::pow(RECAST_REGION_MIN_AREA, 2.0), (int)std::pow(RECAST_REGION_MERGE_AREA, 2.0))) {
918 qCWarning(Log) << "Failed to build watershed regions.";
919 return false;
920 }
921 } else {
922 static_assert("partition type not yet implemented");
923 }
924
925 // step 5: create contours
926 rcContourSetPtr cset(rcAllocContourSet());
927 if (!rcBuildContours(ctx, *chf, RECAST_MAX_SIMPLIFICATION_ERROR, RECAST_MAX_EDGE_LEN / RECAST_CELL_SIZE, *cset)) {
928 qCWarning(Log) << "Failed to create contours.";
929 return false;
930 }
931
932 // step 6: create polygon mesh from countours
933 rcPolyMeshPtr pmesh(rcAllocPolyMesh());
934 if (!rcBuildPolyMesh(ctx, *cset, DT_VERTS_PER_POLYGON, *pmesh)) {
935 qCWarning(Log) << "Failed to triangulate contours";
936 return false;
937 }
938 if (pmesh->npolys == 0) {
939 qCDebug(Log) << "skipping empty tile"; // TODO can we do this even earlier?
940 return true;
941 }
942
943#if 0
944 QFile f(u"pmesh.obj"_s);
945 f.open(QFile::WriteOnly);
946 RecastDebugIoAdapter adapter(&f);
947 duDumpPolyMeshToObj(*pmesh, &adapter);
948#endif
949
950 // step 7: create detail mesh
951 rcPolyMeshDetailPtr dmesh(rcAllocPolyMeshDetail());
952 if (!rcBuildPolyMeshDetail(ctx, *pmesh, *chf, RECAST_DETAIL_SAMPLE_DIST * RECAST_CELL_SIZE, RECAST_DETAIL_SAMPLE_MAX_ERROR * RECAST_CELL_HEIGHT, *dmesh)) {
953 qCWarning(Log) << "Failed to build detail mesh";
954 return false;
955 }
956 chf.reset();
957 cset.reset();
958
959 // step 8 create detour data
960 uint8_t *navData = nullptr;
961 int navDataSize = 0;
962
963 for (int i = 0; i < pmesh->npolys; ++i) {
964 pmesh->flags[i] = flagsForAreaType(static_cast<AreaType>(pmesh->areas[i]));
965 }
966
967 dtNavMeshCreateParams params;
968 std::memset(&params, 0, sizeof(params));
969 params.verts = pmesh->verts;
970 params.vertCount = pmesh->nverts;
971 params.polys = pmesh->polys;
972 params.polyAreas = pmesh->areas;
973 params.polyFlags = pmesh->flags;
974 params.polyCount = pmesh->npolys;
975 params.nvp = pmesh->nvp;
976 params.detailMeshes = dmesh->meshes;
977 params.detailVerts = dmesh->verts;
978 params.detailVertsCount = dmesh->nverts;
979 params.detailTris = dmesh->tris;
980 params.detailTriCount = dmesh->ntris;
981 params.offMeshConVerts = m_offMeshCon.verts.data();
982 params.offMeshConRad = m_offMeshCon.rads.data();
983 params.offMeshConDir = m_offMeshCon.dir.data();
984 params.offMeshConAreas = m_offMeshCon.areas.data();
985 params.offMeshConFlags = m_offMeshCon.flags.data();
986 params.offMeshConUserID = m_offMeshCon.userId.data();
987 params.offMeshConCount = offMeshCount();
988 params.walkableHeight = RECAST_AGENT_HEIGHT;
989 params.walkableRadius = RECAST_AGENT_RADIUS;
990 params.walkableClimb = RECAST_AGENT_MAX_CLIMB;
991 params.tileX = tx;
992 params.tileY = ty;
993 params.tileLayer = 0;
994 rcVcopy(params.bmin, pmesh->bmin);
995 rcVcopy(params.bmax, pmesh->bmax);
996 params.cs = RECAST_CELL_SIZE;
997 params.ch = RECAST_CELL_HEIGHT;
998 params.buildBvTree = true;
999
1000 if (!dtCreateNavMeshData(&params, &navData, &navDataSize)) {
1001 qCWarning(Log) << "dtCreateNavMeshData failed"; // TODO error propagation
1002 return false;
1003 }
1004 std::unique_ptr<uint8_t> navDataPtr(navData);
1005
1006 result->m_navMesh->removeTile(result->m_navMesh->getTileRefAt(tx, ty, 0), {}, nullptr);
1007 auto status = result->m_navMesh->addTile(navData, navDataSize, DT_TILE_FREE_DATA, {}, nullptr);
1008 if (dtStatusFailed(status)) {
1009 qCWarning(Log) << "Failed to add tile to dtNavMesh";
1010 return false;
1011 }
1012
1013 (void)navDataPtr.release(); // managed by navMesh now
1014#endif
1015 return true;
1016}
1017
1018NavMesh NavMeshBuilder::navMesh() const
1019{
1020 return d->m_navMesh;
1021}
A source for overlay elements, drawn on top of the static map data.
void update()
Trigger map re-rendering when the source changes.
bool hasError() const
Returns true if an error occured during parsing and the returned style is invalid.
Result of MapCSS stylesheet evaluation for a single layer selector.
bool hasClass(ClassSelectorKey cls) const
Check whether this result layer has class cls set.
LayerSelectorKey layerSelector() const
The layer selector for this result.
bool hasLineProperties() const
Returns true if a way/line needs to be drawn.
const MapCSSDeclaration * declaration(MapCSSProperty prop) const
Returns the declaration for property @prop, or nullptr is this property isn't set.
bool hasExtrudeProperties() const
Returns true if a 3D extrusion is requested.
bool hasAreaProperties() const
Returns true if an area/polygon needs to be drawn.
Result of MapCSS stylesheet evaluation for all layer selectors.
void clear()
Reset result state from a previous evaluation, while retaining previously allocated resource for reus...
const std::vector< MapCSSResultLayer > & results() const
Results for all layer selectors.
A parsed MapCSS style sheet.
Definition mapcssstyle.h:33
void evaluate(const MapCSSState &state, MapCSSResult &result) const
Evaluates the style sheet for a given state state (OSM element, view state, element state,...
void initializeState(MapCSSState &state) const
Initializes the evaluation state.
Raw OSM map data, separated by levels.
Definition mapdata.h:60
Compiled nav mesh for routing.
Definition navmesh.h:22
A set of nodes, ways and relations.
Definition datatypes.h:346
const Way * way(Id id) const
Find a way by its id.
Definition datatypes.cpp:61
A reference to any of OSM::Node/OSM::Way/OSM::Relation.
Definition element.h:24
bool hasTags() const
Returns whether or not this element has any tags set.
Definition element.h:53
std::vector< const Node * > outerPath(const DataSet &dataSet) const
Returns all nodes belonging to the outer path of this element.
Definition element.cpp:166
An OSM node.
Definition datatypes.h:204
A key of an OSM tag.
Definition datatypes.h:179
Q_SCRIPTABLE CaptureState status()
QString path(const QString &relativePath)
Unit
Unit for geometry sizes.
@ FillOpacity
area fill color
@ Extrude
rounded or rectangular
QStringView level(QStringView ifopt)
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:420
bool isEmpty() const const
QByteArray number(double n, char format, int precision)
qreal length() const const
const_reference at(qsizetype i) const const
bool isEmpty() const const
void push_back(parameter_type value)
void reserve(qsizetype size)
qsizetype size() const const
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void setCapStyle(Qt::PenCapStyle style)
void setWidthF(qreal width)
qreal widthF() const const
qreal x() const const
qreal y() const const
iterator begin()
iterator end()
qsizetype size() const const
QByteArray toUtf8() const const
QChar first() const const
QueuedConnection
OddEvenFill
QThread * currentThread()
QThreadPool * globalInstance()
void start(Callable &&callableToRun, int priority)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 17:06:15 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.