Commit ba2a3c9c authored by Friedrich W. H. Kossebau's avatar Friedrich W. H. Kossebau
Browse files

Split GeoPolygonGraphicsItem class into subtypes Building & Normal

Summary:
GeoPolygonGraphicsItem has special code paths for polygons of buildings
which complicate the logic (and also bring a small price at
runtime, due to repeated checks or unneeded building properties).
Distinct subclasses of GeoPolygonGraphicsItem for each type make the methods
more simple and focussed.
With more work on Vector OSM I expect even more special code for
GeoPolygonGraphicsItem, so separate subclasses should help even more in the
future.

Input wanted especially on:
* naming pattern of new classes
* who should know about types, GeoPolygonGraphicsItem or GeometryLayer?
  GeometryLayer::whichBuildingAt(...) already puts concept of buildings
  into GeometryLayer (though GeoPolygonGraphicsItem's isBuilding() checks
  for even more visual categories, mismatch correct?)

Reviewers: shentey, rahn, nienhueser, #marble

Reviewed By: nienhueser, #marble

Differential Revision: https://phabricator.kde.org/D2828
parent 2729488e
......@@ -228,6 +228,8 @@ SET( geodata_graphicsitem_SRCS
geodata/graphicsitem/GeoPhotoGraphicsItem.cpp
geodata/graphicsitem/GeoPointGraphicsItem.cpp
geodata/graphicsitem/GeoPolygonGraphicsItem.cpp
geodata/graphicsitem/AbstractGeoPolygonGraphicsItem.cpp
geodata/graphicsitem/BuildingGeoPolygonGraphicsItem.cpp
geodata/graphicsitem/GeoTrackGraphicsItem.cpp
geodata/graphicsitem/ScreenOverlayGraphicsItem.cpp
)
......
//
// This file is part of the Marble Virtual Globe.
//
// This program is free software licensed under the GNU LGPL. You can
// find a copy of this license in LICENSE.txt in the top directory of
// the source code.
//
// Copyright 2011 Konstantin Oblaukhov <oblaukhov.konstantin@gmail.com>
//
#include "AbstractGeoPolygonGraphicsItem.h"
#include "GeoDataLinearRing.h"
#include "GeoDataPolygon.h"
#include "GeoPainter.h"
#include "GeoDataLatLonAltBox.h"
#include "ViewportParams.h"
#include "GeoDataStyle.h"
#include "GeoDataIconStyle.h"
#include "GeoDataLineStyle.h"
#include "GeoDataPolyStyle.h"
#include "GeoDataTypes.h"
#include "GeoDataPlacemark.h"
#include "OsmPlacemarkData.h"
#include "MarbleDebug.h"
namespace Marble
{
AbstractGeoPolygonGraphicsItem::AbstractGeoPolygonGraphicsItem(const GeoDataFeature *feature, const GeoDataPolygon *polygon) :
GeoGraphicsItem(feature),
m_polygon(polygon),
m_ring(0)
{
}
AbstractGeoPolygonGraphicsItem::AbstractGeoPolygonGraphicsItem(const GeoDataFeature *feature, const GeoDataLinearRing *ring) :
GeoGraphicsItem(feature),
m_polygon(0),
m_ring(ring)
{
}
const GeoDataLatLonAltBox& AbstractGeoPolygonGraphicsItem::latLonAltBox() const
{
if( m_polygon ) {
return m_polygon->latLonAltBox();
}
return m_ring->latLonAltBox();
}
void AbstractGeoPolygonGraphicsItem::paint( GeoPainter* painter, const ViewportParams* viewport, const QString &layer )
{
Q_UNUSED(layer);
painter->save();
configurePainter(painter, viewport);
if ( m_polygon ) {
painter->drawPolygon( *m_polygon );
} else if ( m_ring ) {
painter->drawPolygon( *m_ring );
}
const GeoDataIconStyle& iconStyle = style()->iconStyle();
bool const hasIcon = !iconStyle.iconPath().isEmpty();
if (hasIcon) {
QImage const icon = iconStyle.scaledIcon();
painter->drawImage(latLonAltBox().center(), icon);
}
painter->restore();
}
QPen AbstractGeoPolygonGraphicsItem::configurePainter(GeoPainter *painter, const ViewportParams *viewport)
{
QPen currentPen = painter->pen();
GeoDataStyle::ConstPtr style = this->style();
if (!style) {
painter->setPen( QPen() );
}
else {
const GeoDataPolyStyle& polyStyle = style->polyStyle();
if (!polyStyle.outline()) {
currentPen.setColor( Qt::transparent );
}
else {
const GeoDataLineStyle& lineStyle = style->lineStyle();
currentPen.setColor(lineStyle.paintedColor());
currentPen.setWidthF(lineStyle.width());
currentPen.setCapStyle(lineStyle.capStyle());
currentPen.setStyle(lineStyle.penStyle());
}
painter->setPen(currentPen);
if (!polyStyle.fill()) {
painter->setBrush(QColor(Qt::transparent));
}
else {
const QColor paintedColor = polyStyle.paintedColor();
if (painter->brush().color() != paintedColor ||
painter->brush().style() != polyStyle.brushStyle()) {
const QImage textureImage = polyStyle.textureImage();
if (!textureImage.isNull()) {
GeoDataCoordinates coords = latLonAltBox().center();
qreal x, y;
viewport->screenCoordinates(coords, x, y);
const QString texturePath = polyStyle.texturePath();
if (m_cachedTexturePath != texturePath ||
m_cachedTextureColor != paintedColor) {
if (textureImage.hasAlphaChannel()) {
m_cachedTexture = QImage ( textureImage.size(), QImage::Format_ARGB32_Premultiplied );
m_cachedTexture.fill(paintedColor);
QPainter imagePainter(&m_cachedTexture );
imagePainter.drawImage(0, 0, textureImage);
}
else {
m_cachedTexture = textureImage;
}
m_cachedTexturePath = texturePath;
m_cachedTextureColor = paintedColor;
}
QBrush brush(m_cachedTexture);
painter->setBrush(brush);
painter->setBrushOrigin(QPoint(x,y));
}
else {
painter->setBrush(QBrush(paintedColor, polyStyle.brushStyle()));
}
}
}
}
return currentPen;
}
int AbstractGeoPolygonGraphicsItem::extractElevation(const GeoDataFeature &feature)
{
int elevation = 0;
if (feature.nodeType() == GeoDataTypes::GeoDataPlacemarkType) {
const GeoDataPlacemark *placemark = static_cast<const GeoDataPlacemark *>(&feature);
const OsmPlacemarkData &osmData = placemark->osmData();
const auto tagIter = osmData.findTag(QStringLiteral("ele"));
if (tagIter != osmData.tagsEnd()) {
elevation = tagIter.value().toInt();
}
}
return elevation;
}
}
//
// This file is part of the Marble Virtual Globe.
//
// This program is free software licensed under the GNU LGPL. You can
// find a copy of this license in LICENSE.txt in the top directory of
// the source code.
//
// Copyright 2011 Konstantin Oblaukhov <oblaukhov.konstantin@gmail.com>
//
#ifndef MARBLE_ABSTRACTGEOPOLYGONGRAPHICSITEM_H
#define MARBLE_ABSTRACTGEOPOLYGONGRAPHICSITEM_H
#include "GeoGraphicsItem.h"
#include "marble_export.h"
#include <QImage>
#include <QColor>
namespace Marble
{
class GeoDataLinearRing;
class GeoDataPolygon;
class GeoDataFeature;
class MARBLE_EXPORT AbstractGeoPolygonGraphicsItem : public GeoGraphicsItem
{
protected:
explicit AbstractGeoPolygonGraphicsItem(const GeoDataFeature *feature, const GeoDataPolygon* polygon);
explicit AbstractGeoPolygonGraphicsItem(const GeoDataFeature *feature, const GeoDataLinearRing* ring);
public:
virtual const GeoDataLatLonAltBox& latLonAltBox() const;
virtual void paint(GeoPainter* painter, const ViewportParams *viewport, const QString &layer);
protected:
QPen configurePainter(GeoPainter* painter, const ViewportParams *viewport);
inline
const GeoDataPolygon *polygon() const { return m_polygon; }
inline
const GeoDataLinearRing *ring() const { return m_ring; }
static int extractElevation(const GeoDataFeature &feature);
private:
const GeoDataPolygon *const m_polygon;
const GeoDataLinearRing *const m_ring;
QString m_cachedTexturePath;
QColor m_cachedTextureColor;
QImage m_cachedTexture;
};
}
#endif
//
// This file is part of the Marble Virtual Globe.
//
// This program is free software licensed under the GNU LGPL. You can
// find a copy of this license in LICENSE.txt in the top directory of
// the source code.
//
// Copyright 2011 Konstantin Oblaukhov <oblaukhov.konstantin@gmail.com>
//
#include "BuildingGeoPolygonGraphicsItem.h"
#include "MarbleDebug.h"
#include "ViewportParams.h"
#include "GeoDataTypes.h"
#include "GeoDataPlacemark.h"
#include "GeoDataLinearRing.h"
#include "GeoDataPolygon.h"
#include "GeoDataIconStyle.h"
#include "GeoDataPolyStyle.h"
#include "OsmPlacemarkData.h"
#include "GeoPainter.h"
namespace Marble
{
BuildingGeoPolygonGraphicsItem::BuildingGeoPolygonGraphicsItem(const GeoDataFeature *feature,
const GeoDataPolygon *polygon)
: AbstractGeoPolygonGraphicsItem(feature, polygon)
, m_buildingHeight(extractBuildingHeight(feature))
, m_buildingLabel(extractBuildingLabel(feature))
, m_entries(extractNamedEntries(feature))
{
setZValue(this->zValue() + m_buildingHeight);
Q_ASSERT(m_buildingHeight > 0.0);
QStringList paintLayers;
paintLayers << QStringLiteral("Polygon/Building/frame")
<< QStringLiteral("Polygon/Building/roof");
setPaintLayers(paintLayers);
}
BuildingGeoPolygonGraphicsItem::BuildingGeoPolygonGraphicsItem(const GeoDataFeature *feature,
const GeoDataLinearRing* ring)
: AbstractGeoPolygonGraphicsItem(feature, ring)
, m_buildingHeight(extractBuildingHeight(feature))
, m_buildingLabel(extractBuildingLabel(feature))
, m_entries(extractNamedEntries(feature))
{
setZValue(this->zValue() + m_buildingHeight);
Q_ASSERT(m_buildingHeight > 0.0);
QStringList paintLayers;
paintLayers << QStringLiteral("Polygon/Building/frame")
<< QStringLiteral("Polygon/Building/roof");
setPaintLayers(paintLayers);
}
void BuildingGeoPolygonGraphicsItem::initializeBuildingPainting(const GeoPainter* painter, const ViewportParams *viewport,
bool &drawAccurate3D, bool &isCameraAboveBuilding, bool &hasInnerBoundaries,
QVector<QPolygonF*>& outlinePolygons,
QVector<QPolygonF*>& innerPolygons) const
{
drawAccurate3D = false;
isCameraAboveBuilding = false;
QPointF offsetAtCorner = buildingOffset(QPointF(0, 0), viewport, &isCameraAboveBuilding);
qreal maxOffset = qMax( qAbs( offsetAtCorner.x() ), qAbs( offsetAtCorner.y() ) );
drawAccurate3D = painter->mapQuality() == HighQuality ? maxOffset > 5.0 : maxOffset > 8.0;
// Since subtracting one fully contained polygon from another results in a single
// polygon with a "connecting line" between the inner and outer part we need
// to first paint the inner area with no pen and then the outlines with the correct pen.
hasInnerBoundaries = polygon() ? !polygon()->innerBoundaries().isEmpty() : false;
if (polygon()) {
if (hasInnerBoundaries) {
screenPolygons(viewport, polygon(), innerPolygons, outlinePolygons);
}
viewport->screenCoordinates(polygon()->outerBoundary(), outlinePolygons);
} else if (ring()) {
viewport->screenCoordinates(*ring(), outlinePolygons);
}
}
QPointF BuildingGeoPolygonGraphicsItem::centroid(const QPolygonF &polygon, double &area)
{
auto centroid = QPointF(0.0, 0.0);
area = 0.0;
for (auto i=0, n=polygon.size(); i<n; ++i) {
auto const x0 = polygon[i].x();
auto const y0 = polygon[i].y();
auto const j = i == n-1 ? 0 : i+1;
auto const x1 = polygon[j].x();
auto const y1 = polygon[j].y();
auto const a = x0*y1 - x1*y0;
area += a;
centroid.rx() += (x0 + x1)*a;
centroid.ry() += (y0 + y1)*a;
}
area *= 0.5;
return centroid / (6.0*area);
}
QPointF BuildingGeoPolygonGraphicsItem::buildingOffset(const QPointF &point, const ViewportParams *viewport, bool* isCameraAboveBuilding) const
{
qreal const cameraFactor = 0.5 * tan(0.5 * 110 * DEG2RAD);
Q_ASSERT(m_buildingHeight > 0.0);
qreal const buildingFactor = m_buildingHeight / EARTH_RADIUS;
qreal const cameraHeightPixel = viewport->width() * cameraFactor;
qreal buildingHeightPixel = viewport->radius() * buildingFactor;
qreal const cameraDistance = cameraHeightPixel-buildingHeightPixel;
if (isCameraAboveBuilding) {
*isCameraAboveBuilding = cameraDistance > 0;
}
qreal const cc = cameraDistance * cameraHeightPixel;
qreal const cb = cameraDistance * buildingHeightPixel;
// The following lines calculate the same result, but are potentially slower due
// to using more trigonometric method calls
// qreal const alpha1 = atan2(offsetX, cameraHeightPixel);
// qreal const alpha2 = atan2(offsetX, cameraHeightPixel-buildingHeightPixel);
// qreal const shiftX = 2 * (cameraHeightPixel-buildingHeightPixel) * sin(0.5*(alpha2-alpha1));
qreal const offsetX = point.x() - viewport->width() / 2.0;
qreal const offsetY = point.y() - viewport->height() / 2.0;
qreal const shiftX = offsetX * cb / (cc + offsetX);
qreal const shiftY = offsetY * cb / (cc + offsetY);
return QPointF(shiftX, shiftY);
}
double BuildingGeoPolygonGraphicsItem::extractBuildingHeight(const GeoDataFeature *feature)
{
double height = 8.0;
if (feature->nodeType() == GeoDataTypes::GeoDataPlacemarkType) {
const GeoDataPlacemark *placemark = static_cast<const GeoDataPlacemark *>(feature);
const OsmPlacemarkData &osmData = placemark->osmData();
QHash<QString, QString>::const_iterator tagIter;
if ((tagIter = osmData.findTag(QStringLiteral("height"))) != osmData.tagsEnd()) {
/** @todo Also parse non-SI units, see https://wiki.openstreetmap.org/wiki/Key:height#Height_of_buildings */
QString const heightValue = QString(tagIter.value()).remove(QStringLiteral(" meters")).remove(QStringLiteral(" m"));
bool extracted = false;
double extractedHeight = heightValue.toDouble(&extracted);
if (extracted) {
height = extractedHeight;
}
} else if ((tagIter = osmData.findTag(QStringLiteral("building:levels"))) != osmData.tagsEnd()) {
int const levels = tagIter.value().toInt();
int const skipLevels = osmData.tagValue(QStringLiteral("building:min_level")).toInt();
/** @todo Is 35 as an upper bound for the number of levels sane? */
height = 3.0 * qBound(1, 1+levels-skipLevels, 35);
}
}
return qBound(1.0, height, 1000.0);
}
QString BuildingGeoPolygonGraphicsItem::extractBuildingLabel(const GeoDataFeature *feature)
{
if (feature->nodeType() == GeoDataTypes::GeoDataPlacemarkType) {
const GeoDataPlacemark *placemark = static_cast<const GeoDataPlacemark *>(feature);
if (!placemark->name().isEmpty()) {
return placemark->name();
}
const OsmPlacemarkData &osmData = placemark->osmData();
auto tagIter = osmData.findTag(QStringLiteral("addr:housename"));
if (tagIter != osmData.tagsEnd()) {
return tagIter.value();
}
tagIter = osmData.findTag(QStringLiteral("addr:housenumber"));
if (tagIter != osmData.tagsEnd()) {
return tagIter.value();
}
}
return QString();
}
QVector<BuildingGeoPolygonGraphicsItem::NamedEntry> BuildingGeoPolygonGraphicsItem::extractNamedEntries(const GeoDataFeature *feature)
{
QVector<NamedEntry> entries;
if (feature->nodeType() == GeoDataTypes::GeoDataPlacemarkType) {
const GeoDataPlacemark *placemark = static_cast<const GeoDataPlacemark *>(feature);
const auto end = placemark->osmData().nodeReferencesEnd();
for (auto iter = placemark->osmData().nodeReferencesBegin(); iter != end; ++iter) {
const auto tagIter = iter.value().findTag(QStringLiteral("addr:housenumber"));
if (tagIter != iter.value().tagsEnd()) {
NamedEntry entry;
entry.point = iter.key();
entry.label = tagIter.value();
entries.push_back(entry);
}
}
}
return entries;
}
void BuildingGeoPolygonGraphicsItem::paint(GeoPainter* painter, const ViewportParams* viewport, const QString &layer)
{
if (layer.endsWith(QLatin1String("/frame"))) {
paintFrame(painter, viewport);
} else if (layer.endsWith(QLatin1String("/roof"))) {
paintRoof(painter, viewport);
} else {
mDebug() << "Didn't expect to have to paint layer " << layer << ", ignoring it.";
}
}
void BuildingGeoPolygonGraphicsItem::paintRoof(GeoPainter* painter, const ViewportParams* viewport)
{
bool drawAccurate3D;
bool isCameraAboveBuilding;
bool hasInnerBoundaries;
QVector<QPolygonF*> outlinePolygons;
QVector<QPolygonF*> innerPolygons;
initializeBuildingPainting(painter, viewport, drawAccurate3D, isCameraAboveBuilding, hasInnerBoundaries, outlinePolygons, innerPolygons);
if (!isCameraAboveBuilding) {
return; // do not render roof if we look inside the building
}
painter->save();
QPen const currentPen = configurePainter(painter, viewport);
bool const hasIcon = !style()->iconStyle().iconPath().isEmpty();
qreal maxSize(0.0);
QPointF roofCenter;
if (hasInnerBoundaries) {
painter->setPen(Qt::NoPen);
}
// first paint the area and icon (and the outline if there are no inner boundaries)
double maxArea = 0.0;
foreach(QPolygonF* outlinePolygon, outlinePolygons) {
QRectF const boundingRect = outlinePolygon->boundingRect();
QPolygonF buildingRoof;
if (hasIcon || !m_buildingLabel.isEmpty() || !m_entries.isEmpty()) {
QSizeF const polygonSize = boundingRect.size();
qreal size = polygonSize.width() * polygonSize.height();
if (size > maxSize) {
maxSize = size;
double area;
roofCenter = centroid(*outlinePolygon, area);
maxArea = qMax(area, maxArea);
roofCenter += buildingOffset(roofCenter, viewport);
}
}
if ( drawAccurate3D) {
buildingRoof.reserve(outlinePolygon->size());
foreach(const QPointF &point, *outlinePolygon) {
buildingRoof << point + buildingOffset(point, viewport);
}
if (hasInnerBoundaries) {
QRegion clip(buildingRoof.toPolygon());
foreach(QPolygonF* innerPolygon, innerPolygons) {
QPolygonF buildingInner;
buildingInner.reserve(innerPolygon->size());
foreach(const QPointF &point, *innerPolygon) {
buildingInner << point + buildingOffset(point, viewport);
}
clip-=QRegion(buildingInner.toPolygon());
}
painter->setClipRegion(clip);
}
painter->drawPolygon(buildingRoof);
} else {
QPointF const offset = buildingOffset(boundingRect.center(), viewport);
painter->translate(offset);
if (hasInnerBoundaries) {
QRegion clip(outlinePolygon->toPolygon());
foreach(QPolygonF* clipPolygon, innerPolygons) {
clip-=QRegion(clipPolygon->toPolygon());
}
painter->setClipRegion(clip);
}
painter->drawPolygon(*outlinePolygon);
painter->translate(-offset);
}
if (hasIcon && !roofCenter.isNull()) {
QImage const icon = style()->iconStyle().scaledIcon();
QPointF const iconCenter(icon.size().width()/2.0, icon.size().height()/2.0);
painter->drawImage(roofCenter-iconCenter, icon);
} else if (drawAccurate3D && !m_buildingLabel.isEmpty() && !roofCenter.isNull()) {
double const w2 = 0.5 * painter->fontMetrics().width(m_buildingLabel);
double const ascent = painter->fontMetrics().ascent();
double const descent = painter->fontMetrics().descent();
double const a2 = 0.5 * painter->fontMetrics().ascent();
QPointF const textPosition = roofCenter - QPointF(w2, -a2);
if (buildingRoof.containsPoint(textPosition + QPointF(-2, -ascent), Qt::OddEvenFill)
&& buildingRoof.containsPoint(textPosition + QPointF(-2, descent), Qt::OddEvenFill)
&& buildingRoof.containsPoint(textPosition + QPointF(2+2*w2, descent), Qt::OddEvenFill)
&& buildingRoof.containsPoint(textPosition + QPointF(2+2*w2, -ascent), Qt::OddEvenFill)
) {
painter->drawText(textPosition, m_buildingLabel);
}
}
}
// Render additional housenumbers at building entries
if (!m_entries.isEmpty() && maxArea > 1600 * m_entries.size()) {
QBrush brush = painter->brush();
QColor const brushColor = brush.color();
QColor lighterColor = brushColor.lighter(110);
lighterColor.setAlphaF(0.9);
brush.setColor(lighterColor);
painter->setBrush(brush);
foreach(const auto &entry, m_entries) {
qreal x, y;
viewport->screenCoordinates(entry.point, x, y);
QPointF point(x, y);
point += buildingOffset(point, viewport);
auto const width = painter->fontMetrics().width(entry.label);
auto const height = painter->fontMetrics().height();
QRectF rectangle(point, QSizeF(qMax(1.2*width, 1.1*height), 1.2*height));
rectangle.moveCenter(point);
painter->drawRoundedRect(rectangle, 3, 3);
painter->drawText(rectangle, Qt::AlignCenter, entry.label);
}
brush.setColor(brushColor);
painter->setBrush(brush);
}
// then paint the outlines if there are inner boundaries
if (hasInnerBoundaries) {
painter->setPen(currentPen);
foreach(QPolygonF * polygon, outlinePolygons) {
QRectF const boundingRect = polygon->boundingRect();
if ( drawAccurate3D) {
QPolygonF buildingRoof;
buildingRoof.reserve(polygon->size());
foreach(const QPointF &point, *polygon) {