KOSMIndoorMap

scenecontroller.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 "scenecontroller.h"
8#include "logging.h"
9#include "render-logging.h"
10
11#include "iconloader_p.h"
12#include "penwidthutil_p.h"
13#include "poleofinaccessibilityfinder_p.h"
14#include "scenegeometry_p.h"
15#include "openinghourscache_p.h"
16#include "texturecache_p.h"
17#include "../style/mapcssdeclaration_p.h"
18#include "../style/mapcssexpressioncontext_p.h"
19#include "../style/mapcssstate_p.h"
20#include "../style/mapcssvalue_p.h"
21
22#include <KOSMIndoorMap/MapData>
23#include <KOSMIndoorMap/MapCSSResult>
24#include <KOSMIndoorMap/MapCSSStyle>
25#include <KOSMIndoorMap/OverlaySource>
26#include <KOSMIndoorMap/SceneGraph>
27#include <KOSMIndoorMap/View>
28
29#include <osm/element.h>
30#include <osm/datatypes.h>
31
32#include <QDebug>
33#include <QElapsedTimer>
34#include <QGuiApplication>
35#include <QPalette>
36#include <QScopedValueRollback>
37
38using namespace Qt::Literals::StringLiterals;
39
40namespace KOSMIndoorMap {
41class SceneControllerPrivate
42{
43public:
44 MapData m_data;
45 const MapCSSStyle *m_styleSheet = nullptr;
46 const View *m_view = nullptr;
47 std::vector<QPointer<AbstractOverlaySource>> m_overlaySources;
48 mutable std::vector<OSM::Element> m_hiddenElements;
49 OSM::Element m_hoverElement;
50
51 MapCSSResult m_styleResult;
52 QColor m_defaultTextColor;
53 QFont m_defaultFont;
54 QPolygonF m_labelPlacementPath;
55 TextureCache m_textureCache;
56 IconLoader m_iconLoader;
57 OpeningHoursCache m_openingHours;
58 PoleOfInaccessibilityFinder m_piaFinder;
59
60 OSM::TagKey m_layerTag;
61 OSM::TagKey m_typeTag;
62 OSM::Languages m_langs;
63
64 bool m_dirty = true;
65 bool m_overlay = false;
66};
67}
68
69using namespace KOSMIndoorMap;
70
71SceneController::SceneController() : d(new SceneControllerPrivate)
72{
74}
75SceneController::~SceneController() = default;
76
77void SceneController::setMapData(const MapData &data)
78{
79 d->m_data = data;
80 if (!d->m_data.isEmpty()) {
81 d->m_layerTag = data.dataSet().tagKey("layer");
82 d->m_typeTag = data.dataSet().tagKey("type");
83 d->m_openingHours.setMapData(data);
84 } else {
85 d->m_layerTag = {};
86 d->m_typeTag = {};
87 d->m_openingHours.setMapData(MapData());
88 }
89 d->m_dirty = true;
90}
91
92void SceneController::setStyleSheet(const MapCSSStyle *styleSheet)
93{
94 d->m_styleSheet = styleSheet;
95 d->m_dirty = true;
96}
97
98void SceneController::setView(const View *view)
99{
100 d->m_view = view;
101 QObject::connect(view, &View::timeChanged, view, [this]() { d->m_dirty = true; });
102 d->m_dirty = true;
103}
104
105void SceneController::setOverlaySources(std::vector<QPointer<AbstractOverlaySource>> &&overlays)
106{
107 d->m_overlaySources = std::move(overlays);
108 d->m_dirty = true;
109}
110
112{
113 // TODO we could potentially do this more fine-grained?
114 d->m_dirty = true;
115}
116
118{
119 QElapsedTimer sgUpdateTimer;
120 sgUpdateTimer.start();
121
122 // check if we are set up completely yet (we can't rely on a defined order with QML)
123 if (!d->m_view || !d->m_styleSheet) {
124 return;
125 }
126
127 // check if the scene is dirty at all
128 if (sg.zoomLevel() == (int)d->m_view->zoomLevel() && sg.currentFloorLevel() == d->m_view->level() && !d->m_dirty) {
129 return;
130 }
131 sg.setZoomLevel(d->m_view->zoomLevel());
132 sg.setCurrentFloorLevel(d->m_view->level());
133 d->m_openingHours.setTimeRange(d->m_view->beginTime(), d->m_view->endTime());
134 d->m_dirty = false;
135
136 sg.beginSwap();
137 std::for_each(d->m_overlaySources.begin(), d->m_overlaySources.end(), std::mem_fn(&AbstractOverlaySource::beginSwap));
138 updateCanvas(sg);
139
140 if (d->m_data.isEmpty()) { // if we don't have map data yet, we just need to get canvas styling here
141 sg.endSwap();
142 return;
143 }
144
145 // find all intermediate levels below or above the currently selected "full" level
146 auto it = d->m_data.levelMap().find(MapLevel(d->m_view->level()));
147 if (it == d->m_data.levelMap().end()) {
148 return;
149 }
150
151 auto beginIt = it;
152 if (beginIt != d->m_data.levelMap().begin()) {
153 do {
154 --beginIt;
155 } while (!(*beginIt).first.isFullLevel() && beginIt != d->m_data.levelMap().begin());
156 ++beginIt;
157 }
158
159 auto endIt = it;
160 for (++endIt; endIt != d->m_data.levelMap().end(); ++endIt) {
161 if ((*endIt).first.isFullLevel()) {
162 break;
163 }
164 }
165
166 // collect elements that the overlay want to hide
167 d->m_hiddenElements.clear();
168 for (const auto &overlaySource : d->m_overlaySources) {
169 overlaySource->hiddenElements(d->m_hiddenElements);
170 }
171 std::sort(d->m_hiddenElements.begin(), d->m_hiddenElements.end());
172
173 // for each level, update or create scene graph elements, after a some basic bounding box check
174 const auto geoBbox = d->m_view->mapSceneToGeo(d->m_view->sceneBoundingBox());
175 for (auto it = beginIt; it != endIt; ++it) {
176 for (auto e : (*it).second) {
177 if (OSM::intersects(geoBbox, e.boundingBox()) && !std::binary_search(d->m_hiddenElements.begin(), d->m_hiddenElements.end(), e)) {
178 updateElement(e, (*it).first.numericLevel(), sg);
179 }
180 }
181 }
182
183 // update overlay elements
184 d->m_overlay = true;
185 for (const auto &overlaySource : d->m_overlaySources) {
186 QScopedValueRollback tranientNodes(d->m_data.dataSet().transientNodes, overlaySource->transientNodes());
187 overlaySource->forEach(d->m_view->level(), [this, &geoBbox, &sg](OSM::Element e, int floorLevel) {
188 if (OSM::intersects(geoBbox, e.boundingBox()) && e.type() != OSM::Type::Null) {
189 updateElement(e, floorLevel, sg);
190 }
191 });
192 d->m_data.dataSet().transientNodes = nullptr;
193 }
194 d->m_overlay = false;
195
196 sg.zSort();
197 sg.endSwap();
198 std::for_each(d->m_overlaySources.begin(), d->m_overlaySources.end(), std::mem_fn(&AbstractOverlaySource::endSwap));
199
200 qCDebug(RenderLog) << "updated scenegraph took" << sgUpdateTimer.elapsed() << "ms";
201}
202
203void SceneController::updateCanvas(SceneGraph &sg) const
204{
205 sg.setBackgroundColor(QGuiApplication::palette().color(QPalette::Base));
206 d->m_defaultTextColor = QGuiApplication::palette().color(QPalette::Text);
207 d->m_defaultFont = QGuiApplication::font();
208
209 MapCSSState state;
210 state.zoomLevel = d->m_view->zoomLevel();
211 state.floorLevel = d->m_view->level();
212 d->m_styleSheet->evaluateCanvas(state, d->m_styleResult);
213 for (auto decl : d->m_styleResult[{}].declarations()) {
214 switch (decl->property()) {
216 sg.setBackgroundColor(decl->colorValue());
217 break;
219 d->m_defaultTextColor = decl->colorValue();
220 break;
221 default:
222 break;
223 }
224 }
225}
226
227void SceneController::updateElement(OSM::Element e, int level, SceneGraph &sg) const
228{
229 MapCSSState state;
230 state.element = e;
231 state.zoomLevel = d->m_view->zoomLevel();
232 state.floorLevel = d->m_view->level();
233 state.openingHours = &d->m_openingHours;
234 state.state = d->m_hoverElement == e ? MapCSSElementState::Hovered : MapCSSElementState::NoState;
235 d->m_styleSheet->initializeState(state);
236 d->m_styleSheet->evaluate(state, d->m_styleResult);
237 for (const auto &result : d->m_styleResult.results()) {
238 updateElement(state, level, sg, result);
239 }
240}
241
242[[nodiscard]] static bool canWordWrap(const QString &s)
243{
244 return std::any_of(s.begin(), s.end(), [](QChar c) { return !c.isLetter(); });
245}
246
247void SceneController::updateElement(const MapCSSState &state, int level, SceneGraph &sg, const MapCSSResultLayer &result) const
248{
249 if (result.hasAreaProperties()) {
250 PolygonBaseItem *item = nullptr;
251 std::unique_ptr<SceneGraphItemPayload> baseItem;
252 if (state.element.type() == OSM::Type::Relation && state.element.tagValue(d->m_typeTag) == "multipolygon") {
253 baseItem = sg.findOrCreatePayload<MultiPolygonItem>(state.element, level, result.layerSelector());
254 auto i = static_cast<MultiPolygonItem*>(baseItem.get());
255 if (i->path.isEmpty()) {
256 i->path = createPath(state.element, d->m_labelPlacementPath);
257 } else if (result.hasLabelProperties()) {
258 SceneGeometry::outerPolygonFromPath(i->path, d->m_labelPlacementPath);
259 }
260 item = i;
261 } else {
262 baseItem = sg.findOrCreatePayload<PolygonItem>(state.element, level, result.layerSelector());
263 auto i = static_cast<PolygonItem*>(baseItem.get());
264 if (i->polygon.isEmpty()) {
265 i->polygon = createPolygon(state.element);
266 }
267 d->m_labelPlacementPath = i->polygon;
268 item = i;
269 }
270
271 double lineOpacity = 1.0;
272 double casingOpacity = 1.0;
273 double fillOpacity = 1.0;
274 bool hasTexture = false;
275 item->z = 0;
276 initializePen(item->pen);
277 initializePen(item->casingPen);
278 for (auto decl : result.declarations()) {
279 applyGenericStyle(decl, item);
280 applyPenStyle(state.element, decl, item->pen, lineOpacity, item->penWidthUnit);
281 applyCasingPenStyle(state.element, decl, item->casingPen, casingOpacity, item->casingPenWidthUnit);
282 switch (decl->property()) {
284 item->fillBrush.setColor(decl->colorValue());
285 item->fillBrush.setStyle(Qt::SolidPattern);
286 break;
288 fillOpacity = decl->doubleValue();
289 break;
291 item->textureBrush.setTextureImage(d->m_textureCache.image(decl->stringValue()));
292 hasTexture = true;
293 break;
294 default:
295 break;
296 }
297 }
298 finalizePen(item->pen, lineOpacity);
299 finalizePen(item->casingPen, casingOpacity);
300 if (item->fillBrush.style() == Qt::SolidPattern && item->textureBrush.style() == Qt::NoBrush && fillOpacity < 1.0) {
301 auto c = item->fillBrush.color();
302 c.setAlphaF(c.alphaF() * fillOpacity);
303 item->fillBrush.setColor(c);
304 }
305 if (item->fillBrush.color().alphaF() == 0.0) {
306 item->fillBrush.setStyle(Qt::NoBrush);
307 }
308 if (hasTexture && item->textureBrush.style() != Qt::NoBrush && fillOpacity > 0.0) {
309 auto c = item->textureBrush.color();
310 c.setAlphaF(fillOpacity);
311 item->textureBrush.setColor(c);
312 } else {
313 item->textureBrush.setStyle(Qt::NoBrush);
314 }
315
316 addItem(sg, state, level, result, std::move(baseItem));
317 } else if (result.hasLineProperties()) {
318 auto baseItem = sg.findOrCreatePayload<PolylineItem>(state.element, level, result.layerSelector());
319 auto item = static_cast<PolylineItem*>(baseItem.get());
320 if (item->path.isEmpty()) {
321 item->path = createPolygon(state.element);
322 }
323
324 double lineOpacity = 1.0;
325 double casingOpacity = 1.0;
326 item->z = 0;
327 initializePen(item->pen);
328 initializePen(item->casingPen);
329 for (auto decl : result.declarations()) {
330 applyGenericStyle(decl, item);
331 applyPenStyle(state.element, decl, item->pen, lineOpacity, item->penWidthUnit);
332 applyCasingPenStyle(state.element, decl, item->casingPen, casingOpacity, item->casingPenWidthUnit);
333 }
334 finalizePen(item->pen, lineOpacity);
335 finalizePen(item->casingPen, casingOpacity);
336
337 d->m_labelPlacementPath = item->path;
338 addItem(sg, state, level, result, std::move(baseItem));
339 }
340
341 if (result.hasLabelProperties()) {
342 QString text;
343 auto textDecl = result.declaration(MapCSSProperty::Text);
344 if (!textDecl) {
345 textDecl = result.declaration(MapCSSProperty::ShieldText);
346 }
347
348 if (textDecl) {
349 if (textDecl->hasExpression()) {
350 text = QString::fromUtf8(textDecl->evaluateExpression({state, result}).asString());
351 } else if (!textDecl->keyValue().isEmpty()) {
352 text = QString::fromUtf8(state.element.tagValue(d->m_langs, textDecl->keyValue().constData()));
353 } else {
354 text = textDecl->stringValue();
355 }
356 }
357
358 const auto iconDecl = result.declaration(MapCSSProperty::IconImage);
359
360 if (!text.isEmpty() || iconDecl) {
361 auto baseItem = sg.findOrCreatePayload<LabelItem>(state.element, level, result.layerSelector());
362 auto item = static_cast<LabelItem*>(baseItem.get());
363 item->text.setText(text);
364 item->textIsSet = !text.isEmpty();
365 item->textOutputSizeCache = {};
366 item->font = d->m_defaultFont;
367 item->color = d->m_defaultTextColor;
368 item->iconSize = {};
369 item->textOffset = 0;
370 item->z = 0;
371
372 double textOpacity = 1.0;
373 double shieldOpacity = 1.0;
374 bool forceCenterPosition = false;
375 bool forceLinePosition = false;
376 bool textRequireFit = false;
377 IconData iconData;
378 for (auto decl : result.declarations()) {
379 applyGenericStyle(decl, item);
380 applyFontStyle(decl, item->font);
381 switch (decl->property()) {
383 item->color = decl->colorValue();
384 break;
386 textOpacity = decl->doubleValue();
387 break;
389 item->casingColor = decl->colorValue();
390 break;
392 item->casingWidth = decl->doubleValue();
393 break;
395 item->shieldColor = decl->colorValue();
396 break;
398 shieldOpacity = decl->doubleValue();
399 break;
401 item->frameColor = decl->colorValue();
402 break;
404 item->frameWidth = decl->doubleValue();
405 break;
407 switch (decl->textPosition()) {
408 case MapCSSDeclaration::Position::Line:
409 forceLinePosition = true;
410 if (d->m_labelPlacementPath.size() > 1) {
411 item->angle = SceneGeometry::polylineMidPointAngle(d->m_labelPlacementPath);
412 }
413 break;
414 case MapCSSDeclaration::Position::Center:
415 forceCenterPosition = true;
416 break;
417 case MapCSSDeclaration::Position::NoPostion:
418 break;
419 }
420 break;
422 item->textOffset = decl->doubleValue();
423 break;
425 // work around for QStaticText misbehaving when we have a max width but can't actually word-wrap
426 // far from perfect but covers the most common cases
427 if (canWordWrap(text)) {
428 item->text.setTextWidth(decl->intValue());
429 }
430 break;
432 if (!decl->keyValue().isEmpty()) {
433 iconData.name = QString::fromUtf8(state.element.tagValue(decl->keyValue().constData()));
434 } else {
435 iconData.name = decl->stringValue();
436 }
437 break;
439 item->iconSize.setHeight(PenWidthUtil::penWidth(state.element, decl, item->iconHeightUnit));
440 break;
442 item->iconSize.setWidth(PenWidthUtil::penWidth(state.element, decl, item->iconWidthUnit));
443 break;
445 {
446 const auto alpha = iconData.color.alphaF();
447 iconData.color = decl->colorValue().rgb();
448 iconData.color.setAlphaF(alpha);
449 break;
450 }
452 iconData.color.setAlphaF(decl->doubleValue());
453 break;
455 item->haloColor = decl->colorValue();
456 break;
458 item->haloRadius = decl->doubleValue();
459 break;
461 item->allowIconOverlap = decl->boolValue();
462 break;
464 item->allowTextOverlap = decl->boolValue();
465 break;
467 textRequireFit = true;
468 break;
469 default:
470 break;
471 }
472 }
473
474 if (item->pos.isNull()) {
475 if ((result.hasAreaProperties() || forceCenterPosition) && !forceLinePosition) {
476 // for simple enough shapes we can use the faster centroid rather than the expensive PIA
477 if (d->m_labelPlacementPath.size() > 6) {
478 item->pos = d->m_piaFinder.find(d->m_labelPlacementPath);
479 } else {
480 item->pos = SceneGeometry::polygonCentroid(d->m_labelPlacementPath);
481 }
482 } else if (result.hasLineProperties() || forceLinePosition) {
483 item->pos = SceneGeometry::polylineMidPoint(d->m_labelPlacementPath);
484 }
485 if (item->pos.isNull()) {
486 item->pos = d->m_view->mapGeoToScene(state.element.center()); // node or something failed above
487 }
488 }
489
490 if (item->color.isValid() && textOpacity < 1.0) {
491 auto c = item->color;
492 c.setAlphaF(c.alphaF() * textOpacity);
493 item->color = c;
494 }
495 if (item->shieldColor.isValid() && shieldOpacity < 1.0) {
496 auto c = item->shieldColor;
497 c.setAlphaF(c.alphaF() * shieldOpacity);
498 item->shieldColor = c;
499 }
500 if (!iconData.name.isEmpty() && iconData.color.alphaF() > 0.0) {
501 if (!iconData.color.isValid()) {
502 iconData.color = d->m_defaultTextColor;
503 }
504 item->icon = d->m_iconLoader.loadIcon(iconData);
505 item->iconOpacity = iconData.color.alphaF();
506 }
507 if (!item->icon.isNull()) {
508 const auto iconSourceSize = item->icon.availableSizes().at(0);
509 const auto aspectRatio = (double)iconSourceSize.width() / (double)iconSourceSize.height();
510 if (item->iconSize.width() <= 0.0 && item->iconSize.height() <= 0.0) {
511 item->iconSize = iconSourceSize;
512 } else if (item->iconSize.width() <= 0.0) {
513 item->iconSize.setWidth(item->iconSize.height() * aspectRatio);
514 } else if (item->iconSize.height() <= 0.0) {
515 item->iconSize.setHeight(item->iconSize.width() / aspectRatio);
516 }
517 }
518
519 if (!item->text.text().isEmpty()) {
520 QTextOption opt;
522 opt.setWrapMode(item->text.textWidth() > 0.0 ? QTextOption::WordWrap : QTextOption::NoWrap);
523 item->text.setTextOption(opt);
524
525 if (item->text.text().contains('\n'_L1) || item->text.textWidth() > 0) {
526 item->isComplexText = true;
527 }
528
529 // do not use QStaticText::prepare here:
530 // the vast majority of text items will likely not be shown at all for being overlapped or out of view
531 // and pre-computing them is too expensive. Instead this will happen as needed on first use, for only
532 // a smaller amounts at a time.
533 // item->text.prepare({}, item->font);
534
535 // discard labels that are longer than the line they are aligned with
536 if (result.hasLineProperties() && d->m_labelPlacementPath.size() > 1 && item->angle != 0.0) {
537 const auto sceneLen = SceneGeometry::polylineLength(d->m_labelPlacementPath);
538 const auto sceneP1 = d->m_view->viewport().topLeft();
539 const auto sceneP2 = QPointF(sceneP1.x() + sceneLen, sceneP1.y());
540 const auto screenP1 = d->m_view->mapSceneToScreen(sceneP1);
541 const auto screenP2 = d->m_view->mapSceneToScreen(sceneP2);
542 const auto screenLen = screenP2.x() - screenP1.x();
543 if (screenLen < item->text.size().width()) {
544 item->text = {};
545 }
546 } else if (result.hasAreaProperties() && textRequireFit && d->m_labelPlacementPath.size() >= 5 && item->angle == 0.0) {
547 const auto textSize = item->textOutputSize();
548 QRectF sceneTextRect;
549 sceneTextRect.setWidth(d->m_view->mapScreenDistanceToSceneDistance(textSize.width()));
550 sceneTextRect.setHeight(d->m_view->mapScreenDistanceToSceneDistance(textSize.height()));
551 sceneTextRect.moveCenter(item->pos); // TODO consider icon and offset
552 if (!SceneGeometry::polygonContainsRect(d->m_labelPlacementPath, sceneTextRect)) {
553 item->text = {};
554 }
555 }
556
557 // put texts below icons by default
558 if (!item->icon.isNull() && item->textOffset == 0.0) {
559 item->textOffset = item->iconSize.height(); // ### what about heights in meters?
560 }
561 }
562
563 if (!item->icon.isNull() || !item->text.text().isEmpty()) {
564 addItem(sg, state, level, result, std::move(baseItem));
565 }
566 }
567 }
568}
569
570QPolygonF SceneController::createPolygon(OSM::Element e) const
571{
572 const auto path = e.outerPath(d->m_data.dataSet());
573 if (path.empty()) {
574 return {};
575 }
576
577 QPolygonF poly;
578 // Element::outerPath takes care of re-assembling broken up line segments
579 // the below takes care of properly merging broken up polygons
580 for (auto it = path.begin(); it != path.end();) {
581 QPolygonF subPoly;
582 subPoly.reserve(path.size());
583 OSM::Id pathBegin = (*it)->id;
584
585 auto subIt = it;
586 for (; subIt != path.end(); ++subIt) {
587 subPoly.push_back(d->m_view->mapGeoToScene((*subIt)->coordinate));
588 if ((*subIt)->id == pathBegin && subIt != it && subIt != std::prev(path.end())) {
589 ++subIt;
590 break;
591 }
592 }
593 it = subIt;
594 poly = poly.isEmpty() ? std::move(subPoly) : poly.united(subPoly);
595 }
596 return poly;
597}
598
599// @see https://wiki.openstreetmap.org/wiki/Relation:multipolygon
600QPainterPath SceneController::createPath(const OSM::Element e, QPolygonF &outerPath) const
601{
602 assert(e.type() == OSM::Type::Relation);
603 outerPath = createPolygon(e); // TODO this is actually not correct for the multiple outer polygon case
604 QPainterPath path;
605 path.setFillRule(Qt::OddEvenFill);
606
607 for (const auto &mem : e.relation()->members) {
608 const bool isInner = std::strcmp(mem.role().name(), "inner") == 0;
609 const bool isOuter = std::strcmp(mem.role().name(), "outer") == 0;
610 if (mem.type() != OSM::Type::Way || (!isInner && !isOuter)) {
611 continue;
612 }
613 if (auto way = d->m_data.dataSet().way(mem.id)) {
614 const auto subPoly = createPolygon(OSM::Element(way));
615 if (subPoly.isEmpty()) {
616 continue;
617 }
618 path.addPolygon(subPoly);
619 path.closeSubpath();
620 }
621 }
622
623 return path;
624}
625
626void SceneController::applyGenericStyle(const MapCSSDeclaration *decl, SceneGraphItemPayload *item) const
627{
628 if (decl->property() == MapCSSProperty::ZIndex) {
629 item->z = decl->intValue();
630 }
631}
632
633void SceneController::applyPenStyle(OSM::Element e, const MapCSSDeclaration *decl, QPen &pen, double &opacity, Unit &unit) const
634{
635 switch (decl->property()) {
637 pen.setColor(decl->colorValue());
638 break;
640 pen.setWidthF(PenWidthUtil::penWidth(e, decl, unit));
641 break;
643 pen.setDashPattern(decl->dashesValue());
644 break;
646 pen.setCapStyle(decl->capStyle());
647 break;
649 pen.setJoinStyle(decl->joinStyle());
650 break;
652 opacity = decl->doubleValue();
653 break;
655 pen.setBrush(d->m_textureCache.image(decl->stringValue()));
656 unit = Unit::Pixel; // TODO scalable line textures aren't implemented yet
657 break;
658 default:
659 break;
660 }
661}
662
663void SceneController::applyCasingPenStyle(OSM::Element e, const MapCSSDeclaration *decl, QPen &pen, double &opacity, Unit &unit) const
664{
665 switch (decl->property()) {
667 pen.setColor(decl->colorValue());
668 break;
670 pen.setWidthF(PenWidthUtil::penWidth(e, decl, unit));
671 break;
673 pen.setDashPattern(decl->dashesValue());
674 break;
676 pen.setCapStyle(decl->capStyle());
677 break;
679 pen.setJoinStyle(decl->joinStyle());
680 break;
682 opacity = decl->doubleValue();
683 break;
684 default:
685 break;
686 }
687}
688
689void SceneController::applyFontStyle(const MapCSSDeclaration *decl, QFont &font) const
690{
691 switch (decl->property()) {
693 font.setFamily(decl->stringValue());
694 break;
696 if (decl->unit() == MapCSSDeclaration::Pixels) {
697 font.setPixelSize(decl->doubleValue());
698 } else {
699 font.setPointSizeF(decl->doubleValue());
700 }
701 break;
703 font.setBold(decl->isBoldStyle());
704 break;
706 font.setItalic(decl->isItalicStyle());
707 break;
709 font.setCapitalization(decl->capitalizationStyle());
710 break;
712 font.setUnderline(decl->isUnderlineStyle());
713 break;
715 font.setCapitalization(decl->capitalizationStyle());
716 break;
717 default:
718 break;
719 }
720}
721
722void SceneController::initializePen(QPen &pen) const
723{
725 pen.setWidthF(0.0);
726
727 // default according to spec
731}
732
733void SceneController::finalizePen(QPen &pen, double opacity) const
734{
735 if (pen.color().isValid() && opacity < 1.0) {
736 auto c = pen.color();
737 c.setAlphaF(c.alphaF() * opacity);
738 pen.setColor(c);
739 }
740
741 if (pen.brush().style() == Qt::TexturePattern && pen.widthF() == 0.0) {
743 }
744
745 if (pen.color().alphaF() == 0.0 || pen.widthF() == 0.0) {
746 pen.setStyle(Qt::NoPen); // so the renderer can skip this entirely
747 }
748
749 // normalize dash pattern, as QPainter scales that with the line width
750 if (pen.widthF() > 0.0 && !pen.dashPattern().isEmpty()) {
751 auto dashes = pen.dashPattern();
752 std::for_each(dashes.begin(), dashes.end(), [pen](double &d) { d /= pen.widthF(); });
753 pen.setDashPattern(std::move(dashes));
754 }
755}
756
757void SceneController::addItem(SceneGraph &sg, const MapCSSState &state, int level, const MapCSSResultLayer &result, std::unique_ptr<SceneGraphItemPayload> &&payload) const
758{
759 SceneGraphItem item;
760 item.element = state.element;
761 item.layerSelector = result.layerSelector();
762 item.level = level;
763 item.payload = std::move(payload);
764
765 // get the OSM layer, if set
766 if (!d->m_overlay) {
767 const auto layerStr = result.resolvedTagValue(d->m_layerTag, state);
768 if (layerStr && !(*layerStr).isEmpty()) {
769 bool success = false;
770 const auto layer = (*layerStr).toInt(&success);
771 if (success) {
772
773 // ### Ignore layer information when it matches the level
774 // This is very wrong according to the specification, however it looks that in many places
775 // layer and level tags aren't correctly filled, possibly a side-effect of layer pre-dating
776 // level and layers not having been properly updated when retrofitting level information
777 // Strictly following the MapCSS rendering order yields sub-optimal results in that case, with
778 // relevant elements being hidden.
779 //
780 // Ideally we find a way to detect the presence of that problem, and only then enabling this
781 // workaround, but until we have this, this seems to produce better results in all tests.
782 if (level != layer * 10) {
783 item.layer = layer;
784 }
785 } else {
786 qCWarning(Log) << "Invalid layer:" << state.element.url() << *layerStr;
787 }
788 }
789 } else {
790 item.layer = std::numeric_limits<int>::max();
791 }
792
793 sg.addItem(std::move(item));
794}
795
797{
798 return d->m_hoverElement;
799}
800
801void SceneController::setHoveredElement(OSM::Element element)
802{
803 if (d->m_hoverElement == element) {
804 return;
805 }
806 d->m_hoverElement = element;
807 d->m_dirty = true;
808}
virtual void endSwap()
Indicates the end of a scene graph update.
virtual void beginSwap()
Indicates the being of a scene graph update.
Result of MapCSS stylesheet evaluation for a single layer selector.
LayerSelectorKey layerSelector() const
The layer selector for this result.
bool hasLineProperties() const
Returns true if a way/line needs to be drawn.
std::optional< QByteArray > resolvedTagValue(OSM::TagKey key, const MapCSSState &state) const
Returns the tag value set by preceding declarations, via MapCSS expressions or in the source data.
bool hasLabelProperties() const
Returns true if a label needs to be drawn.
const MapCSSDeclaration * declaration(MapCSSProperty prop) const
Returns the declaration for property @prop, or nullptr is this property isn't set.
const std::vector< const MapCSSDeclaration * > & declarations() const
The active declarations for the queried element.
bool hasAreaProperties() const
Returns true if an area/polygon needs to be drawn.
A parsed MapCSS style sheet.
Definition mapcssstyle.h:33
Raw OSM map data, separated by levels.
Definition mapdata.h:60
A floor level.
Definition mapdata.h:28
OSM::Element hoveredElement() const
Set currently hovered element.
void overlaySourceUpdated()
Overlay dirty state tracking.
void updateScene(SceneGraph &sg) const
Creates or updates sg based on the currently set style and view settings.
Payload base class for scene graph items.
OSM::Element element
The OSM::Element this item refers to.
Scene graph of the currently displayed level.
Definition scenegraph.h:29
View transformations and transformation manipulation.
Definition view.h:40
TagKey tagKey(const char *keyName) const
Look up a tag key for the given tag name, if it exists.
Definition datatypes.cpp:38
A reference to any of OSM::Node/OSMWay/OSMRelation.
Definition element.h:24
std::vector< const Node * > outerPath(const DataSet &dataSet) const
Returns all nodes belonging to the outer path of this element.
Definition element.cpp:166
static KOSM_EXPORT Languages fromQLocale(const QLocale &locale)
Convert QLocale::uiLanguages() into an OSM::Languages set.
Definition languages.cpp:40
QString path(const QString &relativePath)
OSM-based multi-floor indoor maps for buildings.
Unit
Unit for geometry sizes.
@ LineJoin
line end cap style: none (default), round, square
@ CasingDashes
line casing opacity
@ IconWidth
URL to the icon image.
@ FontStyle
font weight: bold or normal (default)
@ ShieldText
shield casing width
@ CasingOpacity
line casing color
@ ShieldFrameWidth
shield frame color
@ TextOpacity
text color used for the label
@ CasingColor
line casing width
@ TextRequireFit
text halo radius
@ FillColor
line casing join style
@ LineCap
fill image for the line
@ CasingLineCap
line casing dash pattern
@ IconAllowIconOverlap
the equivalent to CartoCSS's allow-overlap, non-standard extension
@ FontVariant
italic or normal (default)
@ CasingLineJoin
line casing end cap
@ ShieldCasingWidth
shield casing color
@ Text
maximum width before wrapping
@ TextTransform
none (default) or underline
@ TextColor
none (default), uppercase, lowercase or capitalize
@ IconAllowTextOverlap
for colorized SVGs, non-standard extension
@ ShieldColor
text has to fit into its associated geometry (custom extension)
@ ShieldCasingColor
shield frame width (0 to disable)
@ CasingWidth
line join style: round (default), miter, bevel
@ TextDecoration
small-caps or normal (default)
@ IconImage
image to fill the area with
@ MaxWidth
vertical offset from the center of the way or point
@ FontFamily
the equivalent to CartoCSS's ignore-placement, non-standard extension
QStringView level(QStringView ifopt)
int64_t Id
OSM element identifier.
Definition datatypes.h:30
const QColor & color() const const
void setColor(Qt::GlobalColor color)
void setStyle(Qt::BrushStyle style)
void setTextureImage(const QImage &image)
Qt::BrushStyle style() const const
QImage textureImage() const const
float alphaF() const const
bool isValid() const const
void setAlphaF(float alpha)
void setBold(bool enable)
void setCapitalization(Capitalization caps)
void setFamily(const QString &family)
void setItalic(bool enable)
void setPixelSize(int pixelSize)
void setPointSizeF(qreal pointSize)
void setUnderline(bool enable)
QPalette palette()
qreal devicePixelRatio() const const
int height() const const
bool isEmpty() const const
void push_back(parameter_type value)
void reserve(qsizetype size)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
const QColor & color(ColorGroup group, ColorRole role) const const
QBrush brush() const const
QColor color() const const
QList< qreal > dashPattern() const const
void setBrush(const QBrush &brush)
void setCapStyle(Qt::PenCapStyle style)
void setColor(const QColor &color)
void setDashPattern(const QList< qreal > &pattern)
void setJoinStyle(Qt::PenJoinStyle style)
void setStyle(Qt::PenStyle style)
void setWidthF(qreal width)
qreal widthF() const const
void moveCenter(const QPointF &position)
void setHeight(qreal height)
void setWidth(qreal width)
iterator begin()
iterator end()
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
qsizetype size() const const
AlignHCenter
SolidPattern
OddEvenFill
transparent
RoundJoin
SolidLine
void setAlignment(Qt::Alignment alignment)
void setWrapMode(WrapMode mode)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 24 2025 11:54:42 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.