Marble

BuildingGraphicsItem.cpp
1// SPDX-License-Identifier: LGPL-2.1-or-later
2//
3// SPDX-FileCopyrightText: 2011 Konstantin Oblaukhov <oblaukhov.konstantin@gmail.com>
4//
5
6#include "BuildingGraphicsItem.h"
7
8#include "GeoDataBuilding.h"
9#include "GeoDataLinearRing.h"
10#include "GeoDataMultiGeometry.h"
11#include "GeoDataPlacemark.h"
12#include "GeoDataPolyStyle.h"
13#include "GeoDataPolygon.h"
14#include "GeoPainter.h"
15#include "MarbleDebug.h"
16#include "OsmPlacemarkData.h"
17#include "ViewportParams.h"
18
19#include <QApplication>
20#include <QScreen>
21
22namespace Marble
23{
24
25BuildingGraphicsItem::BuildingGraphicsItem(const GeoDataPlacemark *placemark, const GeoDataBuilding *building)
26 : AbstractGeoPolygonGraphicsItem(placemark, building)
27{
28 if (const auto ring = geodata_cast<GeoDataLinearRing>(&building->multiGeometry()->at(0))) {
29 setLinearRing(ring);
30 } else if (const auto poly = geodata_cast<GeoDataPolygon>(&building->multiGeometry()->at(0))) {
31 setPolygon(poly);
32 }
33
34 setZValue(building->height());
35 Q_ASSERT(building->height() > 0.0);
36
37 QStringList paintLayers;
38 paintLayers << QStringLiteral("Polygon/Building/frame") << QStringLiteral("Polygon/Building/roof");
39 setPaintLayers(paintLayers);
40}
41
42BuildingGraphicsItem::~BuildingGraphicsItem()
43{
44 qDeleteAll(m_cachedOuterPolygons);
45 qDeleteAll(m_cachedInnerPolygons);
46 qDeleteAll(m_cachedOuterRoofPolygons);
47 qDeleteAll(m_cachedInnerRoofPolygons);
48}
49
50void BuildingGraphicsItem::initializeBuildingPainting(const GeoPainter *painter,
51 const ViewportParams *viewport,
52 bool &drawAccurate3D,
53 bool &isCameraAboveBuilding) const
54{
55 drawAccurate3D = false;
56 isCameraAboveBuilding = false;
57
58 auto const screen = QApplication::screens().first();
59 double const physicalSize = 1.0; // mm
60 int const pixelSize = qRound(physicalSize * screen->physicalDotsPerInch() / (IN2M * M2MM));
61
62 QPointF offsetAtCorner = buildingOffset(QPointF(0, 0), viewport, &isCameraAboveBuilding);
63 qreal maxOffset = qMax(qAbs(offsetAtCorner.x()), qAbs(offsetAtCorner.y()));
64 drawAccurate3D = painter->mapQuality() == HighQuality ? maxOffset > pixelSize : maxOffset > 1.5 * pixelSize;
65}
66
67// backface culling for the perspective drawing below assumes all polygons in clockwise order
68static void normalizeWindingOrder(QPolygonF *polygon)
69{
70 double c = 0;
71 for (int i = 0; i < polygon->size(); ++i) {
72 c += (polygon->at((i + 1) % polygon->size()).x() - polygon->at(i).x()) * (polygon->at(i).y() + polygon->at((i + 1) % polygon->size()).y());
73 }
74 if (c < 0) {
75 std::reverse(polygon->begin(), polygon->end());
76 }
77}
78
79void BuildingGraphicsItem::updatePolygons(const ViewportParams &viewport,
80 QList<QPolygonF *> &outerPolygons,
81 QList<QPolygonF *> &innerPolygons,
82 bool &hasInnerBoundaries) const
83{
84 // Since subtracting one fully contained polygon from another results in a single
85 // polygon with a "connecting line" between the inner and outer part we need
86 // to first paint the inner area with no pen and then the outlines with the correct pen.
87 hasInnerBoundaries = polygon() ? !polygon()->innerBoundaries().isEmpty() : false;
88 if (polygon()) {
89 if (hasInnerBoundaries) {
90 screenPolygons(viewport, polygon(), innerPolygons, outerPolygons);
91 } else {
92 viewport.screenCoordinates(polygon()->outerBoundary(), outerPolygons);
93 }
94 } else if (ring()) {
95 viewport.screenCoordinates(*ring(), outerPolygons);
96 }
97 for (auto polygon : std::as_const(outerPolygons)) {
98 normalizeWindingOrder(polygon);
99 }
100 for (auto polygon : std::as_const(innerPolygons)) {
101 normalizeWindingOrder(polygon);
102 }
103}
104
105QPointF BuildingGraphicsItem::centroid(const QPolygonF &polygon, double &area)
106{
107 auto centroid = QPointF(0.0, 0.0);
108 area = 0.0;
109 for (qsizetype i = 0, n = polygon.size(); i < n; ++i) {
110 auto const x0 = polygon[i].x();
111 auto const y0 = polygon[i].y();
112 auto const j = i == n - 1 ? 0 : i + 1;
113 auto const x1 = polygon[j].x();
114 auto const y1 = polygon[j].y();
115 auto const a = x0 * y1 - x1 * y0;
116 area += a;
117 centroid.rx() += (x0 + x1) * a;
118 centroid.ry() += (y0 + y1) * a;
119 }
120
121 area *= 0.5;
122 return area != 0 ? centroid / (6.0 * area) : polygon.boundingRect().center();
123}
124
125QPointF BuildingGraphicsItem::buildingOffset(const QPointF &point, const ViewportParams *viewport, bool *isCameraAboveBuilding) const
126{
127 qreal const cameraFactor = 0.5 * tan(0.5 * 110 * DEG2RAD);
128 Q_ASSERT(building()->height() > 0.0);
129 qreal const buildingFactor = building()->height() / EARTH_RADIUS;
130
131 qreal const cameraHeightPixel = viewport->width() * cameraFactor;
132 qreal buildingHeightPixel = viewport->radius() * buildingFactor;
133 qreal const cameraDistance = cameraHeightPixel - buildingHeightPixel;
134
135 if (isCameraAboveBuilding) {
136 *isCameraAboveBuilding = cameraDistance > 0;
137 }
138
139 qreal const cc = cameraDistance * cameraHeightPixel;
140 qreal const cb = cameraDistance * buildingHeightPixel;
141
142 // The following lines calculate the same result, but are potentially slower due
143 // to using more trigonometric method calls
144 // qreal const alpha1 = atan2(offsetX, cameraHeightPixel);
145 // qreal const alpha2 = atan2(offsetX, cameraHeightPixel-buildingHeightPixel);
146 // qreal const shiftX = 2 * (cameraHeightPixel-buildingHeightPixel) * sin(0.5*(alpha2-alpha1));
147
148 qreal const offsetX = point.x() - viewport->width() / 2.0;
149 qreal const offsetY = point.y() - viewport->height() / 2.0;
150
151 qreal const shiftX = offsetX * cb / (cc + offsetX);
152 qreal const shiftY = offsetY * cb / (cc + offsetY);
153
154 return {shiftX, shiftY};
155}
156
157void BuildingGraphicsItem::paint(GeoPainter *painter, const ViewportParams *viewport, const QString &layer, int tileZoomLevel)
158{
159 // Just display flat buildings for tile level 17
160 if (tileZoomLevel == 17) {
161 setZValue(0.0);
162 if (layer.endsWith(QLatin1StringView("/roof"))) {
163 AbstractGeoPolygonGraphicsItem::paint(painter, viewport, layer, tileZoomLevel);
164 }
165 return;
166 }
167 setZValue(building()->height());
168
169 // For level 18, 19 .. render 3D buildings in perspective
170 if (layer.endsWith(QLatin1StringView("/frame"))) {
171 qDeleteAll(m_cachedOuterPolygons);
172 qDeleteAll(m_cachedInnerPolygons);
173 qDeleteAll(m_cachedOuterRoofPolygons);
174 qDeleteAll(m_cachedInnerRoofPolygons);
175 m_cachedOuterPolygons.clear();
176 m_cachedInnerPolygons.clear();
177 m_cachedOuterRoofPolygons.clear();
178 m_cachedInnerRoofPolygons.clear();
179 updatePolygons(*viewport, m_cachedOuterPolygons, m_cachedInnerPolygons, m_hasInnerBoundaries);
180 if (m_cachedOuterPolygons.isEmpty()) {
181 return;
182 }
183 paintFrame(painter, viewport);
184 } else if (layer.endsWith(QLatin1StringView("/roof"))) {
185 if (m_cachedOuterPolygons.isEmpty()) {
186 return;
187 }
188 paintRoof(painter, viewport);
189 } else {
190 mDebug() << "Didn't expect to have to paint layer " << layer << ", ignoring it.";
191 }
192}
193
194void BuildingGraphicsItem::paintRoof(GeoPainter *painter, const ViewportParams *viewport)
195{
196 bool drawAccurate3D;
197 bool isCameraAboveBuilding;
198 initializeBuildingPainting(painter, viewport, drawAccurate3D, isCameraAboveBuilding);
199 if (!isCameraAboveBuilding) {
200 return; // do not render roof if we look inside the building
201 }
202
203 bool isValid = true;
204 if (s_previousStyle != style().data()) {
205 isValid = configurePainter(painter, *viewport);
206
207 QFont font = painter->font(); // TODO: better font configuration
208 if (font.pointSize() != 10) {
209 font.setPointSize(10);
210 painter->setFont(font);
211 }
212 }
213 s_previousStyle = style().data();
214
215 if (!isValid)
216 return;
217
218 // first paint the area (and the outline if there are no inner boundaries)
219
220 if (drawAccurate3D) {
221 if (m_hasInnerBoundaries) {
222 QPen const currentPen = painter->pen();
223
224 painter->setPen(Qt::NoPen);
225 QList<QPolygonF *> fillPolygons = painter->createFillPolygons(m_cachedOuterRoofPolygons, m_cachedInnerRoofPolygons);
226
227 for (const QPolygonF *fillPolygon : std::as_const(fillPolygons)) {
228 painter->drawPolygon(*fillPolygon);
229 }
230
231 painter->setPen(currentPen);
232
233 for (const QPolygonF *outerRoof : std::as_const(m_cachedOuterRoofPolygons)) {
234 painter->drawPolyline(*outerRoof);
235 }
236 for (const QPolygonF *innerRoof : std::as_const(m_cachedInnerRoofPolygons)) {
237 painter->drawPolyline(*innerRoof);
238 }
239 qDeleteAll(fillPolygons);
240 } else {
241 for (const QPolygonF *outerRoof : std::as_const(m_cachedOuterRoofPolygons)) {
242 painter->drawPolygon(*outerRoof);
243 }
244 }
245 } else {
246 QPointF const offset = buildingOffset(m_cachedOuterPolygons[0]->boundingRect().center(), viewport);
247 painter->translate(offset);
248
249 if (m_hasInnerBoundaries) {
250 QPen const currentPen = painter->pen();
251
252 painter->setPen(Qt::NoPen);
253 QList<QPolygonF *> fillPolygons = painter->createFillPolygons(m_cachedOuterPolygons, m_cachedInnerPolygons);
254
255 for (const QPolygonF *fillPolygon : std::as_const(fillPolygons)) {
256 painter->drawPolygon(*fillPolygon);
257 }
258
259 painter->setPen(currentPen);
260
261 for (const QPolygonF *outerPolygon : std::as_const(m_cachedOuterPolygons)) {
262 painter->drawPolyline(*outerPolygon);
263 }
264 for (const QPolygonF *innerPolygon : std::as_const(m_cachedInnerPolygons)) {
265 painter->drawPolyline(*innerPolygon);
266 }
267 qDeleteAll(fillPolygons);
268 } else {
269 for (const QPolygonF *outerPolygon : std::as_const(m_cachedOuterPolygons)) {
270 painter->drawPolygon(*outerPolygon);
271 }
272 }
273 painter->translate(-offset);
274 }
275
276 qreal maxSize(0.0);
277 double maxArea = 0.0;
278
279 for (int i = 0; i < m_cachedOuterRoofPolygons.size(); ++i) {
280 const QPolygonF *outerRoof = m_cachedOuterRoofPolygons[i];
281
282 QPointF roofCenter;
283
284 // Label position calculation
285 if (!building()->name().isEmpty() || !building()->entries().isEmpty()) {
286 QSizeF const polygonSize = outerRoof->boundingRect().size();
287 qreal size = polygonSize.width() * polygonSize.height();
288 if (size > maxSize) {
289 maxSize = size;
290 double area;
291 roofCenter = centroid(*outerRoof, area);
292 maxArea = qMax(area, maxArea);
293 }
294 }
295
296 // Draw the housenumber labels
297 if (drawAccurate3D && !building()->name().isEmpty() && !roofCenter.isNull()) {
298 double const w2 = 0.5 * painter->fontMetrics().horizontalAdvance(building()->name());
299 double const ascent = painter->fontMetrics().ascent();
300 double const descent = painter->fontMetrics().descent();
301 double const a2 = 0.5 * painter->fontMetrics().ascent();
302 QPointF const textPosition = roofCenter - QPointF(w2, -a2);
303 if (outerRoof->containsPoint(textPosition + QPointF(-2, -ascent), Qt::OddEvenFill)
304 && outerRoof->containsPoint(textPosition + QPointF(-2, descent), Qt::OddEvenFill)
305 && outerRoof->containsPoint(textPosition + QPointF(2 + 2 * w2, descent), Qt::OddEvenFill)
306 && outerRoof->containsPoint(textPosition + QPointF(2 + 2 * w2, -ascent), Qt::OddEvenFill)) {
307 painter->drawTextFragment(roofCenter.toPoint(), building()->name(), painter->font().pointSize(), painter->brush().color());
308 }
309 }
310 }
311
312 // Render additional housenumbers at building entries
313 if (!building()->entries().isEmpty() && maxArea > 1600 * building()->entries().size()) {
314 for (const auto &entry : building()->entries()) {
315 qreal x, y;
316 viewport->screenCoordinates(entry.point, x, y);
317 QPointF point(x, y);
318 point += buildingOffset(point, viewport);
319 painter->drawTextFragment(point.toPoint(), building()->name(), painter->font().pointSize(), painter->brush().color(), GeoPainter::RoundFrame);
320 }
321 }
322}
323
324void BuildingGraphicsItem::paintFrame(GeoPainter *painter, const ViewportParams *viewport)
325{
326 // TODO: how does this match the Q_ASSERT in the constructor?
327 if (building()->height() == 0.0) {
328 return;
329 }
330
331 if ((polygon() && !viewport->resolves(polygon()->outerBoundary().latLonAltBox(), 4)) || (ring() && !viewport->resolves(ring()->latLonAltBox(), 4))) {
332 return;
333 }
334
335 bool drawAccurate3D;
336 bool isCameraAboveBuilding;
337 initializeBuildingPainting(painter, viewport, drawAccurate3D, isCameraAboveBuilding);
338
339 bool isValid = true;
340 if (s_previousStyle != style().data()) {
341 isValid = configurePainterForFrame(painter);
342 }
343 s_previousStyle = style().data();
344
345 if (!isValid)
346 return;
347
348 if (drawAccurate3D && isCameraAboveBuilding) {
349 for (const QPolygonF *outline : std::as_const(m_cachedOuterPolygons)) {
350 if (outline->isEmpty()) {
351 continue;
352 }
353 // draw the building sides
354 int const size = outline->size();
355 auto outerRoof = new QPolygonF;
356 outerRoof->reserve(outline->size());
357 QPointF a = (*outline)[0];
358 QPointF shiftA = a + buildingOffset(a, viewport);
359 outerRoof->append(shiftA);
360 for (int i = 1; i < size; ++i) {
361 QPointF const &b = (*outline)[i];
362 QPointF const shiftB = b + buildingOffset(b, viewport);
363 // perform backface culling
364 bool backface = (b.x() - a.x()) * (shiftA.y() - a.y()) - (b.y() - a.y()) * (shiftA.x() - a.x()) >= 0;
365 if (!backface) {
366 QPolygonF buildingSide;
367 buildingSide.reserve(4);
368 buildingSide << a << shiftA << shiftB << b;
369 painter->drawPolygon(buildingSide);
370 }
371 a = b;
372 shiftA = shiftB;
373 outerRoof->append(shiftA);
374 }
375 m_cachedOuterRoofPolygons.append(outerRoof);
376 }
377 for (const QPolygonF *outline : std::as_const(m_cachedInnerPolygons)) {
378 if (outline->isEmpty()) {
379 continue;
380 }
381 // draw the building sides
382 int const size = outline->size();
383 auto innerRoof = new QPolygonF;
384 innerRoof->reserve(outline->size());
385 QPointF a = (*outline)[0];
386 QPointF shiftA = a + buildingOffset(a, viewport);
387 innerRoof->append(shiftA);
388 for (int i = 1; i < size; ++i) {
389 QPointF const &b = (*outline)[i];
390 QPointF const shiftB = b + buildingOffset(b, viewport);
391 // perform backface culling
392 bool backface = (b.x() - a.x()) * (shiftA.y() - a.y()) - (b.y() - a.y()) * (shiftA.x() - a.x()) >= 0;
393 if (backface) {
394 QPolygonF buildingSide;
395 buildingSide.reserve(4);
396 buildingSide << a << shiftA << shiftB << b;
397 painter->drawPolygon(buildingSide);
398 }
399 a = b;
400 shiftA = shiftB;
401 innerRoof->append(shiftA);
402 }
403 m_cachedInnerRoofPolygons.append(innerRoof);
404 }
405 } else {
406 // don't draw the building sides - just draw the base frame instead
407 QList<QPolygonF *> fillPolygons = painter->createFillPolygons(m_cachedOuterPolygons, m_cachedInnerPolygons);
408
409 for (QPolygonF *fillPolygon : std::as_const(fillPolygons)) {
410 painter->drawPolygon(*fillPolygon);
411 }
412 qDeleteAll(fillPolygons);
413 }
414}
415
416void BuildingGraphicsItem::screenPolygons(const ViewportParams &viewport,
417 const GeoDataPolygon *polygon,
418 QList<QPolygonF *> &innerPolygons,
419 QList<QPolygonF *> &outerPolygons)
420{
421 Q_ASSERT(polygon);
422
423 viewport.screenCoordinates(polygon->outerBoundary(), outerPolygons);
424
425 QList<GeoDataLinearRing> const &innerBoundaries = polygon->innerBoundaries();
426 for (const GeoDataLinearRing &innerBoundary : innerBoundaries) {
427 QList<QPolygonF *> innerPolygonsPerBoundary;
428 viewport.screenCoordinates(innerBoundary, innerPolygonsPerBoundary);
429
430 innerPolygons.reserve(innerPolygons.size() + innerPolygonsPerBoundary.size());
431 for (QPolygonF *innerPolygonPerBoundary : std::as_const(innerPolygonsPerBoundary)) {
432 innerPolygons << innerPolygonPerBoundary;
433 }
434 }
435}
436
437bool BuildingGraphicsItem::contains(const QPoint &screenPosition, const ViewportParams *viewport) const
438{
439 if (m_cachedOuterPolygons.isEmpty()) {
440 // Level 17
441 return AbstractGeoPolygonGraphicsItem::contains(screenPosition, viewport);
442 }
443
444 QPointF const point = screenPosition;
445 for (auto polygon : m_cachedOuterRoofPolygons) {
446 if (polygon->containsPoint(point, Qt::OddEvenFill)) {
447 for (auto polygon : m_cachedInnerRoofPolygons) {
448 if (polygon->containsPoint(point, Qt::OddEvenFill)) {
449 return false;
450 }
451 }
452 return true;
453 }
454 }
455 for (auto polygon : m_cachedOuterPolygons) {
456 if (polygon->containsPoint(point, Qt::OddEvenFill)) {
457 for (auto polygon : m_cachedInnerPolygons) {
458 if (polygon->containsPoint(point, Qt::OddEvenFill)) {
459 return false;
460 }
461 }
462 return true;
463 }
464 }
465 return false;
466}
467
468bool BuildingGraphicsItem::configurePainterForFrame(GeoPainter *painter) const
469{
470 QPen currentPen = painter->pen();
471
472 GeoDataStyle::ConstPtr style = this->style();
473 if (!style) {
474 painter->setPen(QPen());
475 } else {
476 const GeoDataPolyStyle &polyStyle = style->polyStyle();
477
478 if (currentPen.style() != Qt::NoPen) {
479 painter->setPen(Qt::NoPen);
480 }
481
482 if (!polyStyle.fill()) {
483 return false;
484 } else {
485 const QColor paintedColor = polyStyle.paintedColor().darker(150);
486 if (painter->brush().color() != paintedColor) {
487 painter->setBrush(paintedColor);
488 }
489 }
490 }
491
492 return true;
493}
494
495}
This file contains the headers for ViewportParams.
bool isValid(QStringView ifopt)
QString name(StandardAction id)
Binds a QML item to a specific geodetic location in screen coordinates.
@ HighQuality
High quality (e.g. antialiasing for lines)
QColor darker(int factor) const const
int pointSize() const const
void setPointSize(int pointSize)
QList< QScreen * > screens()
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
iterator begin()
iterator end()
bool isEmpty() const const
void reserve(qsizetype size)
qsizetype size() const const
Qt::PenStyle style() const const
bool isNull() const const
QPoint toPoint() const const
qreal x() const const
qreal y() const const
QRectF boundingRect() const const
bool containsPoint(const QPointF &point, Qt::FillRule fillRule) const const
QSizeF size() const const
qreal height() const const
qreal width() const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
OddEvenFill
QTextStream & center(QTextStream &stream)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:15:45 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.