1 /*
2     SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "painterrenderer.h"
8 #include "stackblur_p.h"
9 #include "view.h"
10 #include "render-logging.h"
11 
12 #include <KOSMIndoorMap/SceneGraph>
13 
14 #include <QDebug>
15 #include <QElapsedTimer>
16 #include <QFontMetricsF>
17 #include <QGuiApplication>
18 #include <QImage>
19 #include <QLinearGradient>
20 #include <QPainter>
21 
22 using namespace KOSMIndoorMap;
23 
24 PainterRenderer::PainterRenderer() = default;
25 PainterRenderer::~PainterRenderer() = default;
26 
setPainter(QPainter * painter)27 void PainterRenderer::setPainter(QPainter *painter)
28 {
29     m_painter = painter;
30 }
31 
render(const SceneGraph & sg,View * view)32 void PainterRenderer::render(const SceneGraph &sg, View *view)
33 {
34     QElapsedTimer frameTimer;
35     frameTimer.start();
36 
37     m_view = view;
38     beginRender();
39     renderBackground(sg.backgroundColor());
40 
41     for (const auto &layerOffsets : sg.layerOffsets()) {
42         const auto layerBegin = sg.itemsBegin(layerOffsets);
43         const auto layerEnd = sg.itemsEnd(layerOffsets);
44         //qDebug() << "rendering layer" << (*layerBegin)->layer;
45 
46         // select elements currently in view
47         m_renderBatch.clear();
48         m_renderBatch.reserve(layerOffsets.second - layerOffsets.first);
49         const QRectF screenRect(QPointF(0, 0), QSizeF(m_view->screenWidth(), m_view->screenHeight()));
50         for (auto it = layerBegin; it != layerEnd; ++it) {
51             if ((*it).payload->inSceneSpace() && m_view->viewport().intersects((*it).payload->boundingRect())) {
52                 m_renderBatch.push_back((*it).payload.get());
53             }
54             if ((*it).payload->inHUDSpace()) {
55                 auto bbox = (*it).payload->boundingRect();
56                 bbox.moveCenter(m_view->mapSceneToScreen(bbox.center()));
57                 if (screenRect.intersects(bbox)) {
58                     m_renderBatch.push_back((*it).payload.get());
59                 }
60             }
61         }
62 
63         for (auto phase : {SceneGraphItemPayload::FillPhase, SceneGraphItemPayload::CasingPhase, SceneGraphItemPayload::StrokePhase, SceneGraphItemPayload::LabelPhase}) {
64             beginPhase(phase);
65             for (const auto item : m_renderBatch) {
66                 if ((item->renderPhases() & phase) == 0) {
67                     continue;
68                 }
69 
70                 if (auto i = dynamic_cast<PolygonItem*>(item)) {
71                     renderPolygon(i, phase);
72                 } else if (auto i = dynamic_cast<MultiPolygonItem*>(item)) {
73                     renderMultiPolygon(i, phase);
74                 } else if (auto i = dynamic_cast<PolylineItem*>(item)) {
75                     renderPolyline(i, phase);
76                 } else if (auto i = dynamic_cast<LabelItem*>(item)) {
77                     renderLabel(i);
78                 } else {
79                     qCritical() << "Unsupported scene graph item!";
80                 }
81             }
82         }
83     }
84 
85     renderForeground(sg.backgroundColor());
86     endRender();
87     m_view = nullptr;
88 
89     qCDebug(RenderLog) << "rendering took:" << frameTimer.elapsed() << "ms for" << sg.items().size() << "items on" << sg.layerOffsets().size() << "layers";
90 }
91 
beginRender()92 void PainterRenderer::beginRender()
93 {
94     m_painter->save();
95 }
96 
renderBackground(const QColor & bgColor)97 void PainterRenderer::renderBackground(const QColor &bgColor)
98 {
99     m_painter->setTransform(m_view->deviceTransform());
100     m_painter->fillRect(0, 0, m_view->screenWidth(), m_view->screenHeight(), bgColor);
101 }
102 
beginPhase(SceneGraphItemPayload::RenderPhase phase)103 void PainterRenderer::beginPhase(SceneGraphItemPayload::RenderPhase phase)
104 {
105     switch (phase) {
106         case SceneGraphItemPayload::NoPhase:
107             Q_UNREACHABLE();
108         case SceneGraphItemPayload::FillPhase:
109             m_painter->setPen(Qt::NoPen);
110             m_painter->setTransform(m_view->sceneToScreenTransform() * m_view->deviceTransform());
111             m_painter->setClipRect(m_view->viewport().intersected(m_view->sceneBoundingBox()));
112             m_painter->setRenderHint(QPainter::Antialiasing, false);
113             break;
114         case SceneGraphItemPayload::CasingPhase:
115         case SceneGraphItemPayload::StrokePhase:
116             m_painter->setBrush(Qt::NoBrush);
117             m_painter->setTransform(m_view->sceneToScreenTransform() * m_view->deviceTransform());
118             m_painter->setClipRect(m_view->viewport().intersected(m_view->sceneBoundingBox()));
119             m_painter->setRenderHint(QPainter::Antialiasing, true);
120             break;
121         case SceneGraphItemPayload::LabelPhase:
122             m_painter->setTransform(m_view->deviceTransform());
123             m_painter->setRenderHint(QPainter::Antialiasing, true);
124             break;
125     }
126 }
127 
renderPolygon(PolygonItem * item,SceneGraphItemPayload::RenderPhase phase)128 void PainterRenderer::renderPolygon(PolygonItem *item, SceneGraphItemPayload::RenderPhase phase)
129 {
130     if (phase == SceneGraphItemPayload::FillPhase) {
131         m_painter->setBrush(item->brush);
132         m_painter->drawPolygon(item->polygon);
133     } else {
134         auto p = item->pen;
135         p.setWidthF(mapToSceneWidth(item->pen.widthF(), item->penWidthUnit));
136         m_painter->setPen(p);
137         m_painter->drawPolygon(item->polygon);
138     }
139 }
140 
renderMultiPolygon(MultiPolygonItem * item,SceneGraphItemPayload::RenderPhase phase)141 void PainterRenderer::renderMultiPolygon(MultiPolygonItem *item, SceneGraphItemPayload::RenderPhase phase)
142 {
143     if (phase == SceneGraphItemPayload::FillPhase) {
144         m_painter->setBrush(item->brush);
145         m_painter->drawPath(item->path);
146     } else {
147         auto p = item->pen;
148         p.setWidthF(mapToSceneWidth(item->pen.widthF(), item->penWidthUnit));
149         m_painter->setPen(p);
150         m_painter->drawPath(item->path);
151     }
152 }
153 
renderPolyline(PolylineItem * item,SceneGraphItemPayload::RenderPhase phase)154 void PainterRenderer::renderPolyline(PolylineItem *item, SceneGraphItemPayload::RenderPhase phase)
155 {
156     if (phase == SceneGraphItemPayload::StrokePhase) {
157         auto p = item->pen;
158         p.setWidthF(mapToSceneWidth(item->pen.widthF(), item->penWidthUnit));
159         m_painter->setPen(p);
160         m_painter->drawPolyline(item->path);
161     } else {
162         auto p = item->casingPen;
163         p.setWidthF(mapToSceneWidth(item->pen.widthF(), item->penWidthUnit) + mapToSceneWidth(item->casingPen.widthF(), item->casingPenWidthUnit));
164         m_painter->setPen(p);
165         m_painter->drawPolyline(item->path);
166     }
167 }
168 
renderLabel(LabelItem * item)169 void PainterRenderer::renderLabel(LabelItem *item)
170 {
171     m_painter->save();
172     m_painter->translate(m_view->mapSceneToScreen(item->pos));
173     m_painter->rotate(item->angle);
174 
175     auto box = item->boundingRect();
176     box.moveCenter({0.0, item->offset});
177 
178     // draw shield
179     // @see https://wiki.openstreetmap.org/wiki/MapCSS/0.2#Shield_properties
180     auto w = item->casingWidth + item->frameWidth + 2.0;
181     if (item->casingWidth > 0.0 && item->casingColor.alpha() > 0) {
182         m_painter->fillRect(box.adjusted(-w, -w, w, w), item->casingColor);
183     }
184     w -= item->casingWidth;
185     if (item->frameWidth > 0.0 && item->frameColor.alpha() > 0) {
186         m_painter->fillRect(box.adjusted(-w, -w, w, w), item->frameColor);
187     }
188     w -= item->frameWidth;
189     if (item->shieldColor.alpha() > 0) {
190         m_painter->fillRect(box.adjusted(-w, -w, w, w), item->shieldColor);
191     }
192 
193     // draw icon
194     if (!item->icon.isNull()) {
195         QRectF iconRect(QPointF(0.0, 0.0), item->iconSize);
196         iconRect.moveCenter(QPointF(0.0, -((box.height() - item->iconSize.height()) / 2.0) + item->offset));
197         item->icon.paint(m_painter, iconRect.toRect());
198     }
199     box.moveTop(box.top() + item->iconSize.height());
200 
201     // draw text halo
202     if (item->haloRadius > 0.0 && item->haloColor.alphaF() > 0.0) {
203         const auto haloBox = box.adjusted(-item->haloRadius, -item->haloRadius, item->haloRadius, item->haloRadius);
204         QImage haloBuffer(haloBox.size().toSize(), QImage::Format_ARGB32);
205         haloBuffer.fill(Qt::transparent);
206         QPainter haloPainter(&haloBuffer);
207         haloPainter.setPen(item->haloColor);
208         haloPainter.setFont(item->font);
209         auto haloTextRect = box;
210         haloTextRect.moveTopLeft({item->haloRadius, item->haloRadius});
211         haloPainter.drawStaticText(haloTextRect.topLeft(), item->text);
212         StackBlur::blur(haloBuffer, item->haloRadius);
213         haloPainter.setCompositionMode(QPainter::CompositionMode_SourceIn);
214         haloPainter.fillRect(haloBuffer.rect(), item->haloColor);
215         m_painter->drawImage(haloBox, haloBuffer);
216     }
217 
218     // draw text
219     if (!item->text.text().isEmpty()) {
220         m_painter->setPen(item->color);
221         m_painter->setFont(item->font);
222         m_painter->drawStaticText(box.topLeft(), item->text);
223     }
224 
225     m_painter->restore();
226 }
227 
renderForeground(const QColor & bgColor)228 void PainterRenderer::renderForeground(const QColor &bgColor)
229 {
230     // fade out the map at the end of the scene box, to indicate you can't scroll further
231     m_painter->setTransform(m_view->deviceTransform());
232     m_painter->setClipRect(m_view->mapSceneToScreen(m_view->viewport()));
233     const auto borderWidth = 10;
234 
235     QColor c(bgColor);
236     c.setAlphaF(0.75);
237     QLinearGradient gradient;
238     gradient.setColorAt(0, bgColor);
239     gradient.setColorAt(0.2, c);
240     gradient.setColorAt(1, Qt::transparent);
241 
242     auto r = m_view->mapSceneToScreen(m_view->sceneBoundingBox());
243     r.setBottom(r.top() + borderWidth);
244     gradient.setStart(r.topLeft());
245     gradient.setFinalStop(r.bottomLeft());
246     m_painter->fillRect(r, gradient);
247 
248     r = m_view->mapSceneToScreen(m_view->sceneBoundingBox());
249     r.setTop(r.bottom() - borderWidth);
250     gradient.setStart(r.bottomLeft());
251     gradient.setFinalStop(r.topLeft());
252     m_painter->fillRect(r, gradient);
253 
254     r = m_view->mapSceneToScreen(m_view->sceneBoundingBox());
255     r.setRight(r.left() + borderWidth);
256     gradient.setStart(r.topLeft());
257     gradient.setFinalStop(r.topRight());
258     m_painter->fillRect(r, gradient);
259 
260     r = m_view->mapSceneToScreen(m_view->sceneBoundingBox());
261     r.setLeft(r.right() - borderWidth);
262     gradient.setStart(r.topRight());
263     gradient.setFinalStop(r.topLeft());
264     m_painter->fillRect(r, gradient);
265 }
266 
endRender()267 void PainterRenderer::endRender()
268 {
269     m_painter->restore();
270 }
271 
mapToSceneWidth(double width,Unit unit) const272 double PainterRenderer::mapToSceneWidth(double width, Unit unit) const
273 {
274     switch (unit) {
275         case Unit::Pixel:
276             return m_view->mapScreenDistanceToSceneDistance(width);
277         case Unit::Meter:
278             return m_view->mapMetersToScene(width);
279     }
280 
281     return width;
282 }
283