Marble

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

KDE's Doxygen guidelines are available online.