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
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.
A text or icon label.
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.
Result of MapCSS stylesheet evaluation for all layer selectors.
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
Multi-polygon item, used for polygons with "holes" in them.
Base item for filled polygons.
A single filled polygon.
A path/way/line item in the scenegraph.
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.
Scene graph item description and handle for its content.
OSM::Element element
The OSM::Element this item refers to.
Scene graph of the currently displayed level.
Definition scenegraph.h:29
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/OSM::Way/OSM::Relation.
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
Languages in preference order to consider when looking up translated tag values.
Definition languages.h:25
static KOSM_EXPORT Languages fromQLocale(const QLocale &locale)
Convert QLocale::uiLanguages() into an OSM::Languages set.
Definition languages.cpp:40
A key of an OSM tag.
Definition datatypes.h:179
QString path(const QString &relativePath)
OSM-based multi-floor indoor maps for buildings.
@ Hovered
element is selected
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
@ FillImage
area fill opacity
@ FillColor
line casing join style
@ ShieldFrameColor
shield opacity
@ 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)
@ TextHaloRadius
text halo color
@ ShieldCasingColor
shield frame width (0 to disable)
@ CasingWidth
line join style: round (default), miter, bevel
@ Image
line dash pattern
@ 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
@ FillOpacity
area fill color
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-2024 The KDE developers.
Generated on Fri Dec 6 2024 12:11:15 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.