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 "MarbleDebug.h"
9#include "ViewportParams.h"
10#include "GeoDataPlacemark.h"
11#include "GeoDataLinearRing.h"
12#include "GeoDataPolygon.h"
13#include "GeoDataBuilding.h"
14#include "GeoDataMultiGeometry.h"
15#include "GeoDataPolyStyle.h"
16#include "OsmPlacemarkData.h"
17#include "GeoPainter.h"
18
19#include <QScreen>
20#include <QApplication>
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")
39 << QStringLiteral("Polygon/Building/roof");
40 setPaintLayers(paintLayers);
41}
42
43BuildingGraphicsItem::~BuildingGraphicsItem()
44{
45 qDeleteAll(m_cachedOuterPolygons);
46 qDeleteAll(m_cachedInnerPolygons);
47 qDeleteAll(m_cachedOuterRoofPolygons);
48 qDeleteAll(m_cachedInnerRoofPolygons);
49}
50
51void BuildingGraphicsItem::initializeBuildingPainting(const GeoPainter* painter, const ViewportParams *viewport,
52 bool &drawAccurate3D, bool &isCameraAboveBuilding ) const
53{
54 drawAccurate3D = false;
55 isCameraAboveBuilding = false;
56
57 auto const screen = QApplication::screens().first();
58 double const physicalSize = 1.0; // mm
59 int const pixelSize = qRound(physicalSize * screen->physicalDotsPerInch() / (IN2M * M2MM));
60
61 QPointF offsetAtCorner = buildingOffset(QPointF(0, 0), viewport, &isCameraAboveBuilding);
62 qreal maxOffset = qMax( qAbs( offsetAtCorner.x() ), qAbs( offsetAtCorner.y() ) );
63 drawAccurate3D = painter->mapQuality() == HighQuality ? maxOffset > pixelSize : maxOffset > 1.5 * pixelSize;
64}
65
66// backface culling for the perspective drawing below assumes all polygons in clockwise order
67static void normalizeWindingOrder(QPolygonF *polygon)
68{
69 double c = 0;
70 for (int i = 0; i <polygon->size(); ++i) {
71 c += (polygon->at((i + 1) % polygon->size()).x() - polygon->at(i).x()) * (polygon->at(i).y() + polygon->at((i + 1) % polygon->size()).y());
72 }
73 if (c < 0) {
74 std::reverse(polygon->begin(), polygon->end());
75 }
76}
77
78void BuildingGraphicsItem::updatePolygons(const ViewportParams &viewport,
79 QVector<QPolygonF*>& outerPolygons,
80 QVector<QPolygonF*>& innerPolygons,
81 bool &hasInnerBoundaries) const
82{
83 // Since subtracting one fully contained polygon from another results in a single
84 // polygon with a "connecting line" between the inner and outer part we need
85 // to first paint the inner area with no pen and then the outlines with the correct pen.
86 hasInnerBoundaries = polygon() ? !polygon()->innerBoundaries().isEmpty() : false;
87 if (polygon()) {
88 if (hasInnerBoundaries) {
89 screenPolygons(viewport, polygon(), innerPolygons, outerPolygons);
90 }
91 else {
92 viewport.screenCoordinates(polygon()->outerBoundary(), outerPolygons);
93 }
94 } else if (ring()) {
95 viewport.screenCoordinates(*ring(), outerPolygons);
96 }
97 for (auto *polygon : outerPolygons) {
98 normalizeWindingOrder(polygon);
99 }
100 for (auto *polygon : 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 (auto 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 QPointF(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(QLatin1String("/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(QLatin1String("/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,
180 m_cachedInnerPolygons,
181 m_hasInnerBoundaries);
182 if (m_cachedOuterPolygons.isEmpty()) {
183 return;
184 }
185 paintFrame(painter, viewport);
186 } else if (layer.endsWith(QLatin1String("/roof"))) {
187 if (m_cachedOuterPolygons.isEmpty()) {
188 return;
189 }
190 paintRoof(painter, viewport);
191 } else {
192 mDebug() << "Didn't expect to have to paint layer " << layer << ", ignoring it.";
193 }
194}
195
196void BuildingGraphicsItem::paintRoof(GeoPainter* painter, const ViewportParams* viewport)
197{
198 bool drawAccurate3D;
199 bool isCameraAboveBuilding;
200 initializeBuildingPainting(painter, viewport, drawAccurate3D, isCameraAboveBuilding);
201 if (!isCameraAboveBuilding) {
202 return; // do not render roof if we look inside the building
203 }
204
205 bool isValid = true;
206 if (s_previousStyle != style().data()) {
207 isValid = configurePainter(painter, *viewport);
208
209 QFont font = painter->font(); // TODO: better font configuration
210 if (font.pointSize() != 10) {
211 font.setPointSize( 10 );
212 painter->setFont(font);
213 }
214 }
215 s_previousStyle = style().data();
216
217 if (!isValid) return;
218
219 // first paint the area (and the outline if there are no inner boundaries)
220
221 if ( drawAccurate3D) {
222 if (m_hasInnerBoundaries) {
223
224 QPen const currentPen = painter->pen();
225
226 painter->setPen(Qt::NoPen);
227 QVector<QPolygonF*> fillPolygons =
228 painter->createFillPolygons( m_cachedOuterRoofPolygons,
229 m_cachedInnerRoofPolygons );
230
231 for( const QPolygonF* fillPolygon: fillPolygons ) {
232 painter->drawPolygon(*fillPolygon);
233 }
234
235 painter->setPen(currentPen);
236
237 for( const QPolygonF* outerRoof: m_cachedOuterRoofPolygons ) {
238 painter->drawPolyline( *outerRoof );
239 }
240 for( const QPolygonF* innerRoof: m_cachedInnerRoofPolygons ) {
241 painter->drawPolyline( *innerRoof );
242 }
243 qDeleteAll(fillPolygons);
244 }
245 else {
246 for( const QPolygonF* outerRoof: m_cachedOuterRoofPolygons ) {
247 painter->drawPolygon( *outerRoof );
248 }
249 }
250 }
251 else {
252 QPointF const offset = buildingOffset(m_cachedOuterPolygons[0]->boundingRect().center(), viewport);
253 painter->translate(offset);
254
255 if (m_hasInnerBoundaries) {
256
257 QPen const currentPen = painter->pen();
258
259 painter->setPen(Qt::NoPen);
260 QVector<QPolygonF*> fillPolygons = painter->createFillPolygons( m_cachedOuterPolygons,
261 m_cachedInnerPolygons );
262
263 for( const QPolygonF* fillPolygon: fillPolygons ) {
264 painter->drawPolygon(*fillPolygon);
265 }
266
267 painter->setPen(currentPen);
268
269 for( const QPolygonF* outerPolygon: m_cachedOuterPolygons ) {
270 painter->drawPolyline( *outerPolygon );
271 }
272 for( const QPolygonF* innerPolygon: m_cachedInnerPolygons ) {
273 painter->drawPolyline( *innerPolygon );
274 }
275 qDeleteAll(fillPolygons);
276 }
277 else {
278 for( const QPolygonF* outerPolygon: m_cachedOuterPolygons ) {
279 painter->drawPolygon( *outerPolygon );
280 }
281 }
282 painter->translate(-offset);
283
284 }
285
286
287 qreal maxSize(0.0);
288 double maxArea = 0.0;
289
290 for (int i = 0; i < m_cachedOuterRoofPolygons.size(); ++i) {
291 const QPolygonF *outerRoof = m_cachedOuterRoofPolygons[i];
292
293 QPointF roofCenter;
294
295 // Label position calculation
296 if (!building()->name().isEmpty() || !building()->entries().isEmpty()) {
297 QSizeF const polygonSize = outerRoof->boundingRect().size();
298 qreal size = polygonSize.width() * polygonSize.height();
299 if (size > maxSize) {
300 maxSize = size;
301 double area;
302 roofCenter = centroid(*outerRoof, area);
303 maxArea = qMax(area, maxArea);
304 }
305 }
306
307 // Draw the housenumber labels
308 if (drawAccurate3D && !building()->name().isEmpty() && !roofCenter.isNull()) {
309 double const w2 = 0.5 * painter->fontMetrics().horizontalAdvance(building()->name());
310 double const ascent = painter->fontMetrics().ascent();
311 double const descent = painter->fontMetrics().descent();
312 double const a2 = 0.5 * painter->fontMetrics().ascent();
313 QPointF const textPosition = roofCenter - QPointF(w2, -a2);
314 if (outerRoof->containsPoint(textPosition + QPointF(-2, -ascent), Qt::OddEvenFill)
315 && outerRoof->containsPoint(textPosition + QPointF(-2, descent), Qt::OddEvenFill)
316 && outerRoof->containsPoint(textPosition + QPointF(2+2*w2, descent), Qt::OddEvenFill)
317 && outerRoof->containsPoint(textPosition + QPointF(2+2*w2, -ascent), Qt::OddEvenFill)
318 ) {
319 painter->drawTextFragment(roofCenter.toPoint(), building()->name(),
320 painter->font().pointSize(), painter->brush().color());
321 }
322 }
323 }
324
325 // Render additional housenumbers at building entries
326 if (!building()->entries().isEmpty() && maxArea > 1600 * building()->entries().size()) {
327 for(const auto &entry: building()->entries()) {
328 qreal x, y;
329 viewport->screenCoordinates(entry.point, x, y);
330 QPointF point(x, y);
331 point += buildingOffset(point, viewport);
332 painter->drawTextFragment(point.toPoint(),
333 building()->name(), painter->font().pointSize(), painter->brush().color(),
334 GeoPainter::RoundFrame);
335 }
336 }
337}
338
339void BuildingGraphicsItem::paintFrame(GeoPainter *painter, const ViewportParams *viewport)
340{
341 // TODO: how does this match the Q_ASSERT in the constructor?
342 if (building()->height() == 0.0) {
343 return;
344 }
345
346 if ((polygon() && !viewport->resolves(polygon()->outerBoundary().latLonAltBox(), 4))
347 || (ring() && !viewport->resolves(ring()->latLonAltBox(), 4))) {
348 return;
349 }
350
351 bool drawAccurate3D;
352 bool isCameraAboveBuilding;
353 initializeBuildingPainting(painter, viewport, drawAccurate3D, isCameraAboveBuilding);
354
355 bool isValid = true;
356 if (s_previousStyle != style().data()) {
357 isValid = configurePainterForFrame(painter);
358 }
359 s_previousStyle = style().data();
360
361 if (!isValid) return;
362
363 if ( drawAccurate3D && isCameraAboveBuilding ) {
364 for (const QPolygonF *outline: m_cachedOuterPolygons) {
365 if (outline->isEmpty()) {
366 continue;
367 }
368 // draw the building sides
369 int const size = outline->size();
370 QPolygonF * outerRoof = new QPolygonF;
371 outerRoof->reserve(outline->size());
372 QPointF a = (*outline)[0];
373 QPointF shiftA = a + buildingOffset(a, viewport);
374 outerRoof->append(shiftA);
375 for (int i=1; i<size; ++i) {
376 QPointF const & b = (*outline)[i];
377 QPointF const shiftB = b + buildingOffset(b, viewport);
378 // perform backface culling
379 bool backface = (b.x() - a.x()) * (shiftA.y() - a.y())
380 - (b.y() - a.y()) * (shiftA.x() - a.x()) >= 0;
381 if (!backface) {
382 QPolygonF buildingSide;
383 buildingSide.reserve(4);
384 buildingSide << a << shiftA << shiftB << b;
385 painter->drawPolygon(buildingSide);
386 }
387 a = b;
388 shiftA = shiftB;
389 outerRoof->append(shiftA);
390 }
391 m_cachedOuterRoofPolygons.append(outerRoof);
392 }
393 for (const QPolygonF *outline: m_cachedInnerPolygons) {
394 if (outline->isEmpty()) {
395 continue;
396 }
397 // draw the building sides
398 int const size = outline->size();
399 QPolygonF * innerRoof = new QPolygonF;
400 innerRoof->reserve(outline->size());
401 QPointF a = (*outline)[0];
402 QPointF shiftA = a + buildingOffset(a, viewport);
403 innerRoof->append(shiftA);
404 for (int i=1; i<size; ++i) {
405 QPointF const & b = (*outline)[i];
406 QPointF const shiftB = b + buildingOffset(b, viewport);
407 // perform backface culling
408 bool backface = (b.x() - a.x()) * (shiftA.y() - a.y())
409 - (b.y() - a.y()) * (shiftA.x() - a.x()) >= 0;
410 if (backface) {
411 QPolygonF buildingSide;
412 buildingSide.reserve(4);
413 buildingSide << a << shiftA << shiftB << b;
414 painter->drawPolygon(buildingSide);
415 }
416 a = b;
417 shiftA = shiftB;
418 innerRoof->append(shiftA);
419 }
420 m_cachedInnerRoofPolygons.append(innerRoof);
421 }
422 } else {
423 // don't draw the building sides - just draw the base frame instead
424 QVector<QPolygonF*> fillPolygons = painter->createFillPolygons( m_cachedOuterPolygons,
425 m_cachedInnerPolygons );
426
427 for( QPolygonF* fillPolygon: fillPolygons ) {
428 painter->drawPolygon(*fillPolygon);
429 }
430 qDeleteAll(fillPolygons);
431 }
432}
433
434void BuildingGraphicsItem::screenPolygons(const ViewportParams &viewport, const GeoDataPolygon *polygon,
435 QVector<QPolygonF *> &innerPolygons,
436 QVector<QPolygonF *> &outerPolygons
437 )
438{
439 Q_ASSERT(polygon);
440
441 viewport.screenCoordinates(polygon->outerBoundary(), outerPolygons);
442
443 QVector<GeoDataLinearRing> const & innerBoundaries = polygon->innerBoundaries();
444 for (const GeoDataLinearRing &innerBoundary: innerBoundaries) {
445 QVector<QPolygonF*> innerPolygonsPerBoundary;
446 viewport.screenCoordinates(innerBoundary, innerPolygonsPerBoundary);
447
448 innerPolygons.reserve(innerPolygons.size() + innerPolygonsPerBoundary.size());
449 for( QPolygonF* innerPolygonPerBoundary: innerPolygonsPerBoundary ) {
450 innerPolygons << innerPolygonPerBoundary;
451 }
452 }
453}
454
455bool BuildingGraphicsItem::contains(const QPoint &screenPosition, const ViewportParams *viewport) const
456{
457 if (m_cachedOuterPolygons.isEmpty()) {
458 // Level 17
459 return AbstractGeoPolygonGraphicsItem::contains(screenPosition, viewport);
460 }
461
462 QPointF const point = screenPosition;
463 for (auto polygon: m_cachedOuterRoofPolygons) {
464 if (polygon->containsPoint(point, Qt::OddEvenFill)) {
465 for (auto polygon: m_cachedInnerRoofPolygons) {
466 if (polygon->containsPoint(point, Qt::OddEvenFill)) {
467 return false;
468 }
469 }
470 return true;
471 }
472 }
473 for (auto polygon: m_cachedOuterPolygons) {
474 if (polygon->containsPoint(point, Qt::OddEvenFill)) {
475 for (auto polygon: m_cachedInnerPolygons) {
476 if (polygon->containsPoint(point, Qt::OddEvenFill)) {
477 return false;
478 }
479 }
480 return true;
481 }
482 }
483 return false;
484}
485
486bool BuildingGraphicsItem::configurePainterForFrame(GeoPainter *painter) const
487{
488 QPen currentPen = painter->pen();
489
490 GeoDataStyle::ConstPtr style = this->style();
491 if (!style) {
492 painter->setPen( QPen() );
493 }
494 else {
495 const GeoDataPolyStyle& polyStyle = style->polyStyle();
496
497 if (currentPen.style() != Qt::NoPen) {
498 painter->setPen(Qt::NoPen);
499 }
500
501 if (!polyStyle.fill()) {
502 return false;
503 }
504 else {
505 const QColor paintedColor = polyStyle.paintedColor().darker(150);
506 if (painter->brush().color() != paintedColor) {
507 painter->setBrush(paintedColor);
508 }
509 }
510 }
511
512 return true;
513}
514
515}
This file contains the headers for ViewportParams.
bool isValid(QStringView ifopt)
QString name(StandardShortcut 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 Tue Mar 26 2024 11:18:17 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.