diff --git a/src/lib/marble/GeoPainter.cpp b/src/lib/marble/GeoPainter.cpp index 006be7c3b..741316213 100644 --- a/src/lib/marble/GeoPainter.cpp +++ b/src/lib/marble/GeoPainter.cpp @@ -1,1005 +1,1020 @@ // // 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 2006-2009 Torsten Rahn #include "GeoPainter.h" #include "GeoPainter_p.h" #include #include #include #include #include "MarbleDebug.h" #include "GeoDataCoordinates.h" #include "GeoDataLatLonAltBox.h" #include "GeoDataLineString.h" #include "GeoDataLinearRing.h" #include "GeoDataPoint.h" #include "GeoDataPolygon.h" #include "MarbleGlobal.h" #include "ViewportParams.h" #include "AbstractProjection.h" // #define MARBLE_DEBUG using namespace Marble; GeoPainterPrivate::GeoPainterPrivate( GeoPainter* q, const ViewportParams *viewport, MapQuality mapQuality ) : m_viewport( viewport ), m_mapQuality( mapQuality ), m_x( new qreal[100] ), m_parent(q) { } GeoPainterPrivate::~GeoPainterPrivate() { delete[] m_x; } void GeoPainterPrivate::createAnnotationLayout ( qreal x, qreal y, const QSizeF& bubbleSize, qreal bubbleOffsetX, qreal bubbleOffsetY, qreal xRnd, qreal yRnd, QPainterPath& path, QRectF& rect ) { // TODO: MOVE this into an own Annotation class qreal arrowPosition = 0.3; qreal arrowWidth = 12.0; qreal width = bubbleSize.width(); qreal height = bubbleSize.height(); qreal dx = ( bubbleOffsetX > 0 ) ? 1.0 : -1.0; // x-Mirror qreal dy = ( bubbleOffsetY < 0 ) ? 1.0 : -1.0; // y-Mirror qreal x0 = ( x + bubbleOffsetX ) - dx * ( 1.0 - arrowPosition ) * ( width - 2.0 * xRnd ) - xRnd *dx; qreal x1 = ( x + bubbleOffsetX ) - dx * ( 1.0 - arrowPosition ) * ( width - 2.0 * xRnd ); qreal x2 = ( x + bubbleOffsetX ) - dx * ( 1.0 - arrowPosition ) * ( width - 2.0 * xRnd ) + xRnd * dx; qreal x3 = ( x + bubbleOffsetX ) - dx * arrowWidth / 2.0; qreal x4 = ( x + bubbleOffsetX ) + dx * arrowWidth / 2.0; qreal x5 = ( x + bubbleOffsetX ) + dx * arrowPosition * ( width - 2.0 * xRnd )- xRnd * dx; qreal x6 = ( x + bubbleOffsetX ) + dx * arrowPosition * ( width - 2.0 * xRnd ); qreal x7 = ( x + bubbleOffsetX ) + dx * arrowPosition * ( width - 2.0 * xRnd ) + xRnd * dx; qreal y0 = ( y + bubbleOffsetY ); qreal y1 = ( y + bubbleOffsetY ) - dy * yRnd; qreal y2 = ( y + bubbleOffsetY ) - dy * 2 * yRnd; qreal y5 = ( y + bubbleOffsetY ) - dy * ( height - 2 * yRnd ); qreal y6 = ( y + bubbleOffsetY ) - dy * ( height - yRnd ); qreal y7 = ( y + bubbleOffsetY ) - dy * height; QPointF p1 ( x, y ); // pointing point QPointF p2 ( x4, y0 ); QPointF p3 ( x6, y0 ); QPointF p4 ( x7, y1 ); QPointF p5 ( x7, y6 ); QPointF p6 ( x6, y7 ); QPointF p7 ( x1, y7 ); QPointF p8 ( x0, y6 ); QPointF p9 ( x0, y1 ); QPointF p10( x1, y0 ); QPointF p11( x3, y0 ); QRectF bubbleBoundingBox( QPointF( x0, y7 ), QPointF( x7, y0 ) ); path.moveTo( p1 ); path.lineTo( p2 ); path.lineTo( p3 ); QRectF bottomRight( QPointF( x5, y2 ), QPointF( x7, y0 ) ); path.arcTo( bottomRight, 270.0, 90.0 ); path.lineTo( p5 ); QRectF topRight( QPointF( x5, y7 ), QPointF( x7, y5 ) ); path.arcTo( topRight, 0.0, 90.0 ); path.lineTo( p7 ); QRectF topLeft( QPointF( x0, y7 ), QPointF( x2, y5 ) ); path.arcTo( topLeft, 90.0, 90.0 ); path.lineTo( p9 ); QRectF bottomLeft( QPointF( x0, y2 ), QPointF( x2, y0 ) ); path.arcTo( bottomLeft, 180.0, 90.0 ); path.lineTo( p10 ); path.lineTo( p11 ); path.lineTo( p1 ); qreal left = ( dx > 0 ) ? x1 : x6; qreal right = ( dx > 0 ) ? x6 : x1; qreal top = ( dy > 0 ) ? y6 : y1; qreal bottom = ( dy > 0 ) ? y1 : y6; rect.setTopLeft( QPointF( left, top ) ); rect.setBottomRight( QPointF( right, bottom ) ); } GeoDataLinearRing GeoPainterPrivate::createLinearRingFromGeoRect( const GeoDataCoordinates & centerCoordinates, qreal width, qreal height ) { qreal lon = 0.0; qreal lat = 0.0; qreal altitude = centerCoordinates.altitude(); centerCoordinates.geoCoordinates( lon, lat, GeoDataCoordinates::Degree ); lon = GeoDataCoordinates::normalizeLon( lon, GeoDataCoordinates::Degree ); lat = GeoDataCoordinates::normalizeLat( lat, GeoDataCoordinates::Degree ); qreal west = GeoDataCoordinates::normalizeLon( lon - width * 0.5, GeoDataCoordinates::Degree ); qreal east = GeoDataCoordinates::normalizeLon( lon + width * 0.5, GeoDataCoordinates::Degree ); qreal north = GeoDataCoordinates::normalizeLat( lat + height * 0.5, GeoDataCoordinates::Degree ); qreal south = GeoDataCoordinates::normalizeLat( lat - height * 0.5, GeoDataCoordinates::Degree ); GeoDataCoordinates southWest( west, south, altitude, GeoDataCoordinates::Degree ); GeoDataCoordinates southEast( east, south, altitude, GeoDataCoordinates::Degree ); GeoDataCoordinates northEast( east, north, altitude, GeoDataCoordinates::Degree ); GeoDataCoordinates northWest( west, north, altitude, GeoDataCoordinates::Degree ); GeoDataLinearRing rectangle( Tessellate | RespectLatitudeCircle ); // If the width of the rect is larger as 180 degree, we have to enforce the long way. if ( width >= 180 ) { qreal center = lon; GeoDataCoordinates southCenter( center, south, altitude, GeoDataCoordinates::Degree ); GeoDataCoordinates northCenter( center, north, altitude, GeoDataCoordinates::Degree ); rectangle << southWest << southCenter << southEast << northEast << northCenter << northWest; } else { rectangle << southWest << southEast << northEast << northWest; } return rectangle; } bool GeoPainterPrivate::doClip( const ViewportParams *viewport ) { if ( !viewport->currentProjection()->isClippedToSphere() ) return true; const qint64 radius = viewport->radius() * viewport->currentProjection()->clippingRadius(); return ( radius > viewport->width() / 2 || radius > viewport->height() / 2 ); } qreal GeoPainterPrivate::normalizeAngle(qreal angle) { angle = fmodf(angle, 360); return angle < 0 ? angle + 360 : angle; } void GeoPainterPrivate::drawTextRotated( const QPointF &startPoint, qreal angle, const QString &text ) { QRectF textRect(startPoint, m_parent->fontMetrics().size( 0, text)); QTransform const oldTransform = m_parent->transform(); m_parent->translate(startPoint); m_parent->rotate(angle); m_parent->translate( -startPoint - QPointF(0.0, m_parent->fontMetrics().height()/2.0) ); m_parent->drawText( textRect, text); m_parent->setTransform(oldTransform); } // ------------------------------------------------------------------------------------------------- GeoPainter::GeoPainter( QPaintDevice* pd, const ViewportParams *viewport, MapQuality mapQuality ) : ClipPainter( pd, GeoPainterPrivate::doClip( viewport ) ), d( new GeoPainterPrivate( this, viewport, mapQuality ) ) { const bool antialiased = mapQuality == HighQuality || mapQuality == PrintQuality; setRenderHint( QPainter::Antialiasing, antialiased ); ClipPainter::setScreenClip(false); } GeoPainter::~GeoPainter() { delete d; } MapQuality GeoPainter::mapQuality() const { return d->m_mapQuality; } void GeoPainter::drawAnnotation( const GeoDataCoordinates & position, const QString & text, QSizeF bubbleSize, qreal bubbleOffsetX, qreal bubbleOffsetY, qreal xRnd, qreal yRnd ) { int pointRepeatNum; qreal y; bool globeHidesPoint; if ( bubbleSize.height() <= 0 ) { QRectF rect = QRectF( QPointF( 0.0, 0.0 ), bubbleSize - QSizeF( 2 * xRnd, 0.0 ) ); qreal idealTextHeight = boundingRect( rect, Qt::TextWordWrap, text ).height(); bubbleSize.setHeight( 2 * yRnd + idealTextHeight ); } bool visible = d->m_viewport->screenCoordinates( position, d->m_x, y, pointRepeatNum, QSizeF(), globeHidesPoint ); if ( visible ) { // Draw all the x-repeat-instances of the point on the screen for( int it = 0; it < pointRepeatNum; ++it ) { QPainterPath path; QRectF rect; d->createAnnotationLayout( d->m_x[it], y, bubbleSize, bubbleOffsetX, bubbleOffsetY, xRnd, yRnd, path, rect ); QPainter::drawPath( path ); QPainter::drawText( rect, Qt::TextWordWrap, text, &rect ); } } } void GeoPainter::drawPoint ( const GeoDataCoordinates & position ) { int pointRepeatNum; qreal y; bool globeHidesPoint; bool visible = d->m_viewport->screenCoordinates( position, d->m_x, y, pointRepeatNum, QSizeF(), globeHidesPoint ); if ( visible ) { // Draw all the x-repeat-instances of the point on the screen for( int it = 0; it < pointRepeatNum; ++it ) { QPainter::drawPoint(QPointF(d->m_x[it], y)); } } } QRegion GeoPainter::regionFromPoint ( const GeoDataCoordinates & position, qreal width ) const { return regionFromRect( position, width, width, false, 3 ); } void GeoPainter::drawPoint( const GeoDataPoint & point ) { drawPoint( point.coordinates() ); } QRegion GeoPainter::regionFromPoint ( const GeoDataPoint & point, qreal width ) const { return regionFromRect( point.coordinates(), width, width, false, 3 ); } void GeoPainter::drawText ( const GeoDataCoordinates & position, const QString & text, qreal xOffset, qreal yOffset, qreal width, qreal height, const QTextOption & option ) { // Of course in theory we could have the "isGeoProjected" parameter used // for drawText as well. However this would require us to convert all // glyphs to PainterPaths / QPolygons. From QPolygons we could create // GeoDataPolygons which could get painted on screen. Any patches appreciated ;-) int pointRepeatNum; qreal y; bool globeHidesPoint; QSizeF textSize( fontMetrics().width( text ), fontMetrics().height() ); bool visible = d->m_viewport->screenCoordinates( position, d->m_x, y, pointRepeatNum, textSize, globeHidesPoint ); if ( visible ) { // Draw all the x-repeat-instances of the point on the screen const qreal posY = y - yOffset; for( int it = 0; it < pointRepeatNum; ++it ) { const qreal posX = d->m_x[it] + xOffset; if (width == 0.0 && height == 0.0) { QPainter::drawText(QPointF(posX, posY), text); } else { const QRectF boundingRect(posX, posY, width, height); QPainter::drawText( boundingRect, text, option ); } } } } void GeoPainter::drawEllipse ( const GeoDataCoordinates & centerPosition, qreal width, qreal height, bool isGeoProjected ) { if ( !isGeoProjected ) { int pointRepeatNum; qreal y; bool globeHidesPoint; bool visible = d->m_viewport->screenCoordinates( centerPosition, d->m_x, y, pointRepeatNum, QSizeF( width, height ), globeHidesPoint ); if ( visible ) { // Draw all the x-repeat-instances of the point on the screen const qreal rx = width / 2.0; const qreal ry = height / 2.0; for( int it = 0; it < pointRepeatNum; ++it ) { QPainter::drawEllipse(QPointF(d->m_x[it], y), rx, ry); } } } else { // Initialize variables const qreal centerLon = centerPosition.longitude( GeoDataCoordinates::Degree ); const qreal centerLat = centerPosition.latitude( GeoDataCoordinates::Degree ); const qreal altitude = centerPosition.altitude(); // Ensure a valid latitude range: if ( centerLat + 0.5 * height > 90.0 || centerLat - 0.5 * height < -90.0 ) { return; } // Don't show the ellipse if it's too small: GeoDataLatLonBox ellipseBox( centerLat + 0.5 * height, centerLat - 0.5 * height, centerLon + 0.5 * width, centerLon - 0.5 * width, GeoDataCoordinates::Degree ); if ( !d->m_viewport->viewLatLonAltBox().intersects( ellipseBox ) || !d->m_viewport->resolves( ellipseBox ) ) return; GeoDataLinearRing ellipse; // Optimizing the precision by determining the size which the // ellipse covers on the screen: const qreal degreeResolution = d->m_viewport->angularResolution() * RAD2DEG; // To create a circle shape even for very small precision we require uneven numbers: const int precision = qMin( width / degreeResolution / 8 + 1, 81 ); // Calculate the shape of the upper half of the ellipse: for ( int i = 0; i <= precision; ++i ) { const qreal t = 1.0 - 2.0 * (qreal)(i) / (qreal)(precision); const qreal lat = centerLat + 0.5 * height * sqrt( 1.0 - t * t ); const qreal lon = centerLon + 0.5 * width * t; ellipse << GeoDataCoordinates( lon, lat, altitude, GeoDataCoordinates::Degree ); } // Calculate the shape of the lower half of the ellipse: for ( int i = 0; i <= precision; ++i ) { const qreal t = 2.0 * (qreal)(i) / (qreal)(precision) - 1.0; const qreal lat = centerLat - 0.5 * height * sqrt( 1.0 - t * t ); const qreal lon = centerLon + 0.5 * width * t; ellipse << GeoDataCoordinates( lon, lat, altitude, GeoDataCoordinates::Degree ); } drawPolygon( ellipse ); } } QRegion GeoPainter::regionFromEllipse ( const GeoDataCoordinates & centerPosition, qreal width, qreal height, bool isGeoProjected, qreal strokeWidth ) const { if ( !isGeoProjected ) { int pointRepeatNum; qreal y; bool globeHidesPoint; bool visible = d->m_viewport->screenCoordinates( centerPosition, d->m_x, y, pointRepeatNum, QSizeF( width, height ), globeHidesPoint ); QRegion regions; if ( visible ) { // only a hint, a backend could still ignore it, but we cannot know more const bool antialiased = testRenderHint(QPainter::Antialiasing); const qreal halfStrokeWidth = strokeWidth/2.0; const int startY = antialiased ? (qFloor(y - halfStrokeWidth)) : (qFloor(y+0.5 - halfStrokeWidth)); const int endY = antialiased ? (qCeil(y + height + halfStrokeWidth)) : (qFloor(y+0.5 + height + halfStrokeWidth)); // Draw all the x-repeat-instances of the point on the screen for( int it = 0; it < pointRepeatNum; ++it ) { const qreal x = d->m_x[it]; const int startX = antialiased ? (qFloor(x - halfStrokeWidth)) : (qFloor(x+0.5 - halfStrokeWidth)); const int endX = antialiased ? (qCeil(x + width + halfStrokeWidth)) : (qFloor(x+0.5 + width + halfStrokeWidth)); regions += QRegion(startX, startY, endX - startX, endY - startY, QRegion::Ellipse); } } return regions; } else { // Initialize variables const qreal centerLon = centerPosition.longitude( GeoDataCoordinates::Degree ); const qreal centerLat = centerPosition.latitude( GeoDataCoordinates::Degree ); const qreal altitude = centerPosition.altitude(); // Ensure a valid latitude range: if ( centerLat + 0.5 * height > 90.0 || centerLat - 0.5 * height < -90.0 ) { return QRegion(); } // Don't show the ellipse if it's too small: GeoDataLatLonBox ellipseBox( centerLat + 0.5 * height, centerLat - 0.5 * height, centerLon + 0.5 * width, centerLon - 0.5 * width, GeoDataCoordinates::Degree ); if ( !d->m_viewport->viewLatLonAltBox().intersects( ellipseBox ) || !d->m_viewport->resolves( ellipseBox ) ) return QRegion(); GeoDataLinearRing ellipse; // Optimizing the precision by determining the size which the // ellipse covers on the screen: const qreal degreeResolution = d->m_viewport->angularResolution() * RAD2DEG; // To create a circle shape even for very small precision we require uneven numbers: const int precision = qMin( width / degreeResolution / 8 + 1, 81 ); // Calculate the shape of the upper half of the ellipse: for ( int i = 0; i <= precision; ++i ) { const qreal t = 1.0 - 2.0 * (qreal)(i) / (qreal)(precision); const qreal lat = centerLat + 0.5 * height * sqrt( 1.0 - t * t ); const qreal lon = centerLon + 0.5 * width * t; ellipse << GeoDataCoordinates( lon, lat, altitude, GeoDataCoordinates::Degree ); } // Calculate the shape of the lower half of the ellipse: for ( int i = 0; i <= precision; ++i ) { const qreal t = 2.0 * (qreal)(i) / (qreal)(precision) - 1.0; const qreal lat = centerLat - 0.5 * height * sqrt( 1.0 - t * t ); const qreal lon = centerLon + 0.5 * width * t; ellipse << GeoDataCoordinates( lon, lat, altitude, GeoDataCoordinates::Degree ); } return regionFromPolygon( ellipse, Qt::OddEvenFill, strokeWidth ); } } void GeoPainter::drawImage ( const GeoDataCoordinates & centerPosition, const QImage & image /*, bool isGeoProjected */ ) { // isGeoProjected = true would project the image/pixmap onto the globe. This // requires to deal with the TextureMapping classes -> should get // implemented later on int pointRepeatNum; qreal y; bool globeHidesPoint; // if ( !isGeoProjected ) { bool visible = d->m_viewport->screenCoordinates( centerPosition, d->m_x, y, pointRepeatNum, image.size(), globeHidesPoint ); if ( visible ) { // Draw all the x-repeat-instances of the point on the screen const qreal posY = y - (image.height() / 2.0); for( int it = 0; it < pointRepeatNum; ++it ) { const qreal posX = d->m_x[it] - (image.width() / 2.0); QPainter::drawImage(QPointF(posX, posY), image); } } // } } void GeoPainter::drawPixmap ( const GeoDataCoordinates & centerPosition, const QPixmap & pixmap /* , bool isGeoProjected */ ) { int pointRepeatNum; qreal y; bool globeHidesPoint; // if ( !isGeoProjected ) { // FIXME: Better visibility detection that takes the circle geometry into account bool visible = d->m_viewport->screenCoordinates( centerPosition, d->m_x, y, pointRepeatNum, pixmap.size(), globeHidesPoint ); if ( visible ) { // Draw all the x-repeat-instances of the point on the screen const qreal posY = y - (pixmap.height() / 2.0); for( int it = 0; it < pointRepeatNum; ++it ) { const qreal posX = d->m_x[it] - (pixmap.width() / 2.0); QPainter::drawPixmap(QPointF(posX, posY), pixmap); } } // } } QRegion GeoPainter::regionFromPixmapRect(const GeoDataCoordinates & centerCoordinates, int width, int height, int margin) const { const int fullWidth = width + 2 * margin; const int fullHeight = height + 2 * margin; int pointRepeatNum; qreal y; bool globeHidesPoint; const bool visible = d->m_viewport->screenCoordinates(centerCoordinates, d->m_x, y, pointRepeatNum, QSizeF(fullWidth, fullHeight), globeHidesPoint); QRegion regions; if (visible) { // cmp. GeoPainter::drawPixmap() position calculation // QPainter::drawPixmap seems to qRound the passed position const int posY = qRound(y - (height / 2.0)) - margin; for (int it = 0; it < pointRepeatNum; ++it) { const int posX = qRound(d->m_x[it] - (width / 2.0)) - margin; regions += QRegion(posX, posY, width, height); } } return regions; } void GeoPainter::polygonsFromLineString( const GeoDataLineString &lineString, QVector &polygons ) { // Immediately leave this method now if: // - the object is not visible in the viewport or if // - the size of the object is below the resolution of the viewport if ( ! d->m_viewport->viewLatLonAltBox().intersects( lineString.latLonAltBox() ) || ! d->m_viewport->resolves( lineString.latLonAltBox() ) ) { // mDebug() << "LineString doesn't get displayed on the viewport"; return; } d->m_viewport->screenCoordinates( lineString, polygons ); } void GeoPainter::drawPolyline ( const GeoDataLineString & lineString, const QString& labelText, LabelPositionFlags labelPositionFlags, const QColor& labelColor) { // no labels to draw? // TODO: !labelColor.isValid() || labelColor.alpha() == 0 does not work, // something injects invalid labelColor for city streets if (labelText.isEmpty() || labelPositionFlags.testFlag(NoLabel) || labelColor == Qt::transparent) { drawPolyline(lineString); return; } QVector polygons; polygonsFromLineString(lineString, polygons); if (polygons.empty()) return; foreach(const QPolygonF* itPolygon, polygons) { ClipPainter::drawPolyline(*itPolygon); } drawLabelsForPolygons(polygons, labelText, labelPositionFlags, labelColor); qDeleteAll( polygons ); } void GeoPainter::drawLabelsForPolygons( const QVector &polygons, const QString& labelText, LabelPositionFlags labelPositionFlags, const QColor& labelColor ) { if (labelText.isEmpty()) { return; } QPen const oldPen = pen(); if (labelPositionFlags.testFlag(FollowLine)) { const qreal maximumLabelFontSize = 20; qreal fontSize = pen().widthF() * 0.45; fontSize = qMin( fontSize, maximumLabelFontSize ); if (fontSize < 6.0 || labelColor == "transparent") { return; } QFont font = this->font(); font.setPointSizeF(fontSize); setFont(font); int labelWidth = fontMetrics().width( labelText ); if (labelText.size() < 20) { labelWidth *= (20.0 / labelText.size()); } setPen(labelColor); QVector labelNodes; QRectF viewportRect = QRectF(QPointF(0, 0), d->m_viewport->size()); foreach( QPolygonF* itPolygon, polygons ) { if (!itPolygon->boundingRect().intersects(viewportRect)) { continue; } labelNodes.clear(); QPainterPath path; path.addPolygon(*itPolygon); qreal pathLength = path.length(); if (pathLength == 0) continue; int maxNumLabels = static_cast(pathLength / labelWidth); if (maxNumLabels > 0) { qreal textRelativeLength = labelWidth / pathLength; int numLabels = 1; if (maxNumLabels > 1) { numLabels = maxNumLabels/2; } qreal offset = (1.0 - numLabels*textRelativeLength)/numLabels; qreal startPercent = offset/2.0; for (int k = 0; k < numLabels; ++k, startPercent += textRelativeLength + offset) { QPointF point = path.pointAtPercent(startPercent); QPointF endPoint = path.pointAtPercent(startPercent + textRelativeLength); if ( viewport().contains(point.toPoint()) || viewport().contains(endPoint.toPoint()) ) { qreal angle = -path.angleAtPercent(startPercent); qreal angle2 = -path.angleAtPercent(startPercent + textRelativeLength); angle = GeoPainterPrivate::normalizeAngle(angle); angle2 = GeoPainterPrivate::normalizeAngle(angle2); bool upsideDown = angle > 90.0 && angle < 270.0; if ( qAbs(angle - angle2) < 3.0 ) { if ( upsideDown ) { angle += 180.0; point = path.pointAtPercent(startPercent + textRelativeLength); } d->drawTextRotated(point, angle, labelText); } else { for (int i = 0; i < labelText.length(); ++i) { qreal currentGlyphTextLength = fontMetrics().width(labelText.left(i)) / pathLength; if ( !upsideDown ) { angle = -path.angleAtPercent(startPercent + currentGlyphTextLength); point = path.pointAtPercent(startPercent + currentGlyphTextLength); } else { angle = -path.angleAtPercent(startPercent + textRelativeLength - currentGlyphTextLength) + 180; point = path.pointAtPercent(startPercent + textRelativeLength - currentGlyphTextLength); } d->drawTextRotated(point, angle, labelText.at(i)); } } } } } } } else { setPen(labelColor); int labelWidth = fontMetrics().width( labelText ); int labelAscent = fontMetrics().ascent(); QVector labelNodes; foreach( QPolygonF* itPolygon, polygons ) { labelNodes.clear(); ClipPainter::labelPosition( *itPolygon, labelNodes, labelPositionFlags ); if (!labelNodes.isEmpty()) { foreach ( const QPointF& labelNode, labelNodes ) { QPointF labelPosition = labelNode + QPointF( 3.0, -2.0 ); // FIXME: This is a Q&D fix. qreal xmax = viewport().width() - 10.0 - labelWidth; if ( labelPosition.x() > xmax ) labelPosition.setX( xmax ); qreal ymin = 10.0 + labelAscent; if ( labelPosition.y() < ymin ) labelPosition.setY( ymin ); qreal ymax = viewport().height() - 10.0 - labelAscent; if ( labelPosition.y() > ymax ) labelPosition.setY( ymax ); drawText( QRectF( labelPosition, fontMetrics().size( 0, labelText) ), labelText ); } } } } setPen(oldPen); } void GeoPainter::drawPolyline(const GeoDataLineString& lineString) { QVector polygons; polygonsFromLineString(lineString, polygons); if (polygons.empty()) return; foreach(const QPolygonF* itPolygon, polygons) { ClipPainter::drawPolyline(*itPolygon); } qDeleteAll(polygons); } QRegion GeoPainter::regionFromPolyline ( const GeoDataLineString & lineString, qreal strokeWidth ) const { // Immediately leave this method now if: // - the object is not visible in the viewport or if // - the size of the object is below the resolution of the viewport if ( ! d->m_viewport->viewLatLonAltBox().intersects( lineString.latLonAltBox() ) || ! d->m_viewport->resolves( lineString.latLonAltBox() ) ) { // mDebug() << "LineString doesn't get displayed on the viewport"; return QRegion(); } QPainterPath painterPath; QVector polygons; d->m_viewport->screenCoordinates( lineString, polygons ); foreach( QPolygonF* itPolygon, polygons ) { painterPath.addPolygon( *itPolygon ); } qDeleteAll( polygons ); QPainterPathStroker stroker; stroker.setWidth( strokeWidth ); QPainterPath strokePath = stroker.createStroke( painterPath ); return QRegion( strokePath.toFillPolygon().toPolygon(), Qt::WindingFill ); } void GeoPainter::drawPolygon ( const GeoDataLinearRing & linearRing, Qt::FillRule fillRule ) { // Immediately leave this method now if: // - the object is not visible in the viewport or if // - the size of the object is below the resolution of the viewport if ( ! d->m_viewport->viewLatLonAltBox().intersects( linearRing.latLonAltBox() ) || ! d->m_viewport->resolves( linearRing.latLonAltBox() ) ) { // mDebug() << "Polygon doesn't get displayed on the viewport"; return; } QVector polygons; d->m_viewport->screenCoordinates( linearRing, polygons ); foreach( QPolygonF* itPolygon, polygons ) { ClipPainter::drawPolygon( *itPolygon, fillRule ); } qDeleteAll( polygons ); } QRegion GeoPainter::regionFromPolygon ( const GeoDataLinearRing & linearRing, Qt::FillRule fillRule, qreal strokeWidth ) const { // Immediately leave this method now if: // - the object is not visible in the viewport or if // - the size of the object is below the resolution of the viewport if ( ! d->m_viewport->viewLatLonAltBox().intersects( linearRing.latLonAltBox() ) || ! d->m_viewport->resolves( linearRing.latLonAltBox() ) ) { return QRegion(); } QRegion regions; QVector polygons; d->m_viewport->screenCoordinates( linearRing, polygons ); if ( strokeWidth == 0 ) { // This is the faster way foreach( QPolygonF* itPolygon, polygons ) { regions += QRegion ( (*itPolygon).toPolygon(), fillRule ); } } else { QPainterPath painterPath; foreach( QPolygonF* itPolygon, polygons ) { painterPath.addPolygon( *itPolygon ); } QPainterPathStroker stroker; stroker.setWidth( strokeWidth ); QPainterPath strokePath = stroker.createStroke( painterPath ); painterPath = painterPath.united( strokePath ); regions = QRegion( painterPath.toFillPolygon().toPolygon() ); } qDeleteAll( polygons ); return regions; } void GeoPainter::drawPolygon ( const GeoDataPolygon & polygon, Qt::FillRule fillRule ) { // If the object is not visible in the viewport return if ( ! d->m_viewport->viewLatLonAltBox().intersects( polygon.outerBoundary().latLonAltBox() ) || // If the size of the object is below the resolution of the viewport then return ! d->m_viewport->resolves( polygon.outerBoundary().latLonAltBox() ) ) { // mDebug() << "Polygon doesn't get displayed on the viewport"; return; } // mDebug() << "Drawing Polygon"; QVector outerPolygons; QVector innerPolygons; d->m_viewport->screenCoordinates( polygon.outerBoundary(), outerPolygons ); - QPen const oldPen = pen(); + QPen const currentPen = pen(); - // When inner boundaries exist, the outline of the polygon must be painted - // separately to avoid connections between the outer and inner boundaries - // To avoid performance penalties the separate painting is only done when - // it's really needed. See review 105019 for details. bool const hasInnerBoundaries = !polygon.innerBoundaries().isEmpty(); bool innerBoundariesOnScreen = false; if ( hasInnerBoundaries ) { QVector innerBoundaries = polygon.innerBoundaries(); const GeoDataLatLonAltBox & viewLatLonAltBox = d->m_viewport->viewLatLonAltBox(); foreach( const GeoDataLinearRing& itInnerBoundary, innerBoundaries ) { if ( viewLatLonAltBox.intersects(itInnerBoundary.latLonAltBox()) - && d->m_viewport->resolves(itInnerBoundary.latLonAltBox()) ) { + && d->m_viewport->resolves(itInnerBoundary.latLonAltBox()), 4 ) { innerBoundariesOnScreen = true; break; } } if (innerBoundariesOnScreen) { - // Cut the outer polygons to the viewport - QVector viewportPolygon = QPolygonF(QRectF(0, 0, d->m_viewport->width(), d->m_viewport->height())); - foreach(QPolygonF* outerPolygon, outerPolygons) { - *outerPolygon = outerPolygon->intersected(QPolygonF(viewportPolygon)); - } - - setPen( QPen( Qt::NoPen ) ); - // Create the inner screen polygons foreach( const GeoDataLinearRing& itInnerBoundary, innerBoundaries ) { QVector innerPolygonsPerBoundary; d->m_viewport->screenCoordinates( itInnerBoundary, innerPolygonsPerBoundary ); foreach( QPolygonF* innerPolygonPerBoundary, innerPolygonsPerBoundary ) { innerPolygons << innerPolygonPerBoundary; } } - } - } - foreach( QPolygonF* outerPolygon, outerPolygons ) { - if (hasInnerBoundaries && innerBoundariesOnScreen) { - QRegion clip(outerPolygon->toPolygon()); + setPen(Qt::NoPen); + QVector fillPolygons = createFillPolygons( outerPolygons, + innerPolygons ); - foreach(QPolygonF* innerPolygon, innerPolygons) { - clip-=QRegion(innerPolygon->toPolygon()); + foreach( const QPolygonF* fillPolygon, fillPolygons ) { + ClipPainter::drawPolygon(*fillPolygon, fillRule); } - ClipPainter::setClipRegion(clip); + + setPen(currentPen); + QBrush currentBrush = brush(); + setBrush(QBrush(Qt::transparent)); + + foreach( const QPolygonF* outerPolygon, outerPolygons ) { + ClipPainter::drawPolygon( *outerPolygon, fillRule ); + } + foreach( const QPolygonF* innerPolygon, innerPolygons ) { + ClipPainter::drawPolygon( *innerPolygon, fillRule ); + } + + setBrush(currentBrush); + + qDeleteAll(fillPolygons); } - ClipPainter::drawPolygon( *outerPolygon, fillRule ); } - if ( hasInnerBoundaries && innerBoundariesOnScreen ) { - setPen( oldPen ); - foreach( const QPolygonF* outerPolygon, outerPolygons ) { - ClipPainter::drawPolyline( *outerPolygon ); - } - foreach( const QPolygonF* innerPolygon, innerPolygons ) { - ClipPainter::drawPolyline( *innerPolygon ); - } + if ( !hasInnerBoundaries || !innerBoundariesOnScreen ) { + drawPolygon( polygon.outerBoundary(), fillRule ); } + qDeleteAll(outerPolygons); qDeleteAll(innerPolygons); } +QVector GeoPainter::createFillPolygons( const QVector & outerPolygons, + const QVector & innerPolygons ) const +{ + QVector fillPolygons; + + foreach( const QPolygonF* outerPolygon, outerPolygons ) { + QPolygonF* fillPolygon = new QPolygonF; + *fillPolygon << *outerPolygon; + *fillPolygon << outerPolygon->first(); + + foreach( const QPolygonF* innerPolygon, innerPolygons ) { + *fillPolygon << *innerPolygon; + *fillPolygon << innerPolygon->first(); + *fillPolygon << outerPolygon->first(); + } + fillPolygons << fillPolygon; + } + + return fillPolygons; +} + void GeoPainter::drawRect ( const GeoDataCoordinates & centerCoordinates, qreal width, qreal height, bool isGeoProjected ) { if ( !isGeoProjected ) { int pointRepeatNum; qreal y; bool globeHidesPoint; bool visible = d->m_viewport->screenCoordinates( centerCoordinates, d->m_x, y, pointRepeatNum, QSizeF( width, height ), globeHidesPoint ); if ( visible ) { // Draw all the x-repeat-instances of the point on the screen const qreal posY = y - height / 2.0; for( int it = 0; it < pointRepeatNum; ++it ) { const qreal posX = d->m_x[it] - width / 2.0; QPainter::drawRect(QRectF(posX, posY, width, height)); } } } else { drawPolygon( d->createLinearRingFromGeoRect( centerCoordinates, width, height ), Qt::OddEvenFill ); } } QRegion GeoPainter::regionFromRect ( const GeoDataCoordinates & centerCoordinates, qreal width, qreal height, bool isGeoProjected, qreal strokeWidth ) const { if ( !isGeoProjected ) { int pointRepeatNum; qreal y; bool globeHidesPoint; bool visible = d->m_viewport->screenCoordinates( centerCoordinates, d->m_x, y, pointRepeatNum, QSizeF( width, height ), globeHidesPoint ); QRegion regions; if ( visible ) { // only a hint, a backend could still ignore it, but we cannot know more const bool antialiased = testRenderHint(QPainter::Antialiasing); const qreal halfStrokeWidth = strokeWidth/2.0; const int startY = antialiased ? (qFloor(y - halfStrokeWidth)) : (qFloor(y+0.5 - halfStrokeWidth)); const int endY = antialiased ? (qCeil(y + height + halfStrokeWidth)) : (qFloor(y+0.5 + height + halfStrokeWidth)); // Draw all the x-repeat-instances of the point on the screen for( int it = 0; it < pointRepeatNum; ++it ) { const qreal x = d->m_x[it]; const int startX = antialiased ? (qFloor(x - halfStrokeWidth)) : (qFloor(x+0.5 - halfStrokeWidth)); const int endX = antialiased ? (qCeil(x + width + halfStrokeWidth)) : (qFloor(x+0.5 + width + halfStrokeWidth)); regions += QRegion(startX, startY, endX - startX, endY - startY); } } return regions; } else { return regionFromPolygon( d->createLinearRingFromGeoRect( centerCoordinates, width, height ), Qt::OddEvenFill, strokeWidth ); } } void GeoPainter::drawRoundedRect(const GeoDataCoordinates ¢erPosition, qreal width, qreal height, qreal xRnd, qreal yRnd) { int pointRepeatNum; qreal y; bool globeHidesPoint; // FIXME: Better visibility detection that takes the circle geometry into account bool visible = d->m_viewport->screenCoordinates( centerPosition, d->m_x, y, pointRepeatNum, QSizeF( width, height ), globeHidesPoint ); if ( visible ) { // Draw all the x-repeat-instances of the point on the screen const qreal posY = y - height / 2.0; for( int it = 0; it < pointRepeatNum; ++it ) { const qreal posX = d->m_x[it] - width / 2.0; QPainter::drawRoundedRect(QRectF(posX, posY, width, height), xRnd, yRnd); } } } diff --git a/src/lib/marble/GeoPainter.h b/src/lib/marble/GeoPainter.h index 60b057153..645b51220 100644 --- a/src/lib/marble/GeoPainter.h +++ b/src/lib/marble/GeoPainter.h @@ -1,517 +1,520 @@ // // 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 2008-2009 Torsten Rahn // #ifndef MARBLE_GEOPAINTER_H #define MARBLE_GEOPAINTER_H #include "marble_export.h" // Marble #include "MarbleGlobal.h" #include "ClipPainter.h" #include class QImage; class QPaintDevice; class QRegion; class QString; namespace Marble { class ViewportParams; class GeoPainterPrivate; class GeoDataCoordinates; class GeoDataLineString; class GeoDataLinearRing; class GeoDataPoint; class GeoDataPolygon; /*! \class GeoPainter \brief A painter that allows to draw geometric primitives on the map. This class allows application developers to draw simple geometric shapes and objects onto the map. The API is modeled after the QPainter API. The GeoPainter provides a wide range of methods that are using geographic ("geodesic") coordinates to position the item. For example a point or the nodes of a polygon can fully be described in geographic coordinates. In all these cases the position of the object is specified in geographic coordinates. There are however some cases where there are two viable cases: \li the shape of the object could still use screen coordinates (like a label or an icon). \li Alternatively the shape of the object can get projected according to the current projection (e.g. a texture projected onto the spherical surface) If screen coordinates are used then e.g. width and height are assumed to be expressed in pixels, otherwise degrees are used. Painter transformations (e.g. translate) always happen in screen coordinates. Like in QPainter drawing objects onto a widget should always considered to be a volatile operation. This means that e.g. placemarks don't get added to the globe permanently. So the drawing needs to be done on every repaint to prevent that drawings will disappear during the next paint event. So if you want to add placemarks to your map widget permanently (i.e. you don't want to take care of repainting) then you need to use other solutions such as the KML import of the Marble framework or Marble's GeoGraphicsItems. \note By default the GeoPainter automatically filters geographical content in order to provide fast painting: \li Geographically positioned objects which are outside the viewport are not drawn at all. Parts of objects which are specified through geographic coordinates (like polygons, ellipses etc.) get cut off if they are not placed within the viewport. \li Objects which have a shape that is specified through geographic coordinates get filtered according to the viewport resolution: If the object is much smaller than a pixel then it won't get drawn at all. */ class MARBLE_EXPORT GeoPainter : public ClipPainter { public: /*! \brief Creates a new geo painter. To create a geo painter it's necessary to provide \a paintDevice as a canvas and the viewportParams to specify the map projection inside the viewport. */ GeoPainter( QPaintDevice * paintDevice, const ViewportParams *viewportParams, MapQuality mapQuality = NormalQuality ); /*! \brief Destroys the geo painter. */ ~GeoPainter(); /*! \brief Returns the map quality. \return The map quality that got assigned to the painter. */ MapQuality mapQuality() const; /*! \brief Draws a text annotation that points to a geodesic position. The annotation consists of a bubble with the specified \a text inside. By choosing an appropriate pen for the painter it's possible to change the color and line style of the bubble outline and the text. The brush chosen for the painter is used to paint the background of the bubble The optional parameters which describe the layout of the bubble are similar to those used by QPainter::drawRoundedRect(). Unlike in QPainter the rounded corners are not specified in percentage but in pixels to provide for optimal aesthetics. By choosing a positive or negative bubbleOffset it's possible to place the annotation on top, bottom, left or right of the annotated position. \param position The geodesic position \param text The text contained by the bubble \param bubbleSize The size of the bubble that holds the annotation text. A height of 0 can be used to have the height calculated automatically to fit the needed text height. \param bubbleOffsetX The x-axis offset between the annotated position and the "root" of the speech bubble's "arrow". \param bubbleOffsetY The y-axis offset between the annotated position and the "root" of the speech bubble's "arrow". \param xRnd Specifies the geometry of the rounded corners in pixels along the x-axis. \param yRnd Specifies the geometry of the rounded corners in pixels along the y-axis. \see GeoDataCoordinates */ void drawAnnotation( const GeoDataCoordinates & position, const QString & text, QSizeF bubbleSize = QSizeF( 130, 100 ), qreal bubbleOffsetX = -10, qreal bubbleOffsetY = -30, qreal xRnd = 5, qreal yRnd = 5 ); /*! \brief Draws a single point at a given geographic position. The point is drawn using the painter's pen color. \see GeoDataCoordinates */ void drawPoint ( const GeoDataCoordinates & position ); /*! \brief Creates a region for a given geographic position. A QRegion object is created that represents the area covered by GeoPainter::drawPoint( GeoDataCoordinates ). It can be used e.g. for input event handling of objects that have been painted using GeoPainter::drawPoint( GeoDataCoordinates ). The width allows to set the "stroke width" for the region. For input event handling it's always advisable to use a width that is slightly bigger than the width of the painter's pen. \see GeoDataCoordinates */ QRegion regionFromPoint ( const GeoDataCoordinates & position, qreal strokeWidth = 3) const; /*! \brief Draws a single point at a given geographic position. The point is drawn using the painter's pen color. \see GeoDataPoint */ void drawPoint ( const GeoDataPoint & point ); /*! \brief Create a region for a given geographic position. A QRegion object is created that represents the area covered by GeoPainter::drawPoint( GeoDataPoint ). It can be used e.g. for input event handling of objects that have been painted using GeoPainter::drawPoint( GeoDataPoint ). The width allows to set the "stroke width" for the region. For input event handling it's always advisable to use a width that is slightly bigger than the width of the painter's pen. */ QRegion regionFromPoint ( const GeoDataPoint & point, qreal strokeWidth = 3) const; /*! \brief Draws the given text at a given geographic position. The \a text is drawn starting at the given \a position using the painter's font property. The text rendering is performed in screen coordinates and is not subject to the current projection. An offset given in screenPixels can be provided via xOffset and yOffset in order to tweak the text position. By optionally adding a width, height and text options the text flow can be further influenced. */ void drawText ( const GeoDataCoordinates & position, const QString & text, qreal xOffset = 0.0, qreal yOffset = 0.0, qreal width = 0.0, qreal height = 0.0, const QTextOption & option = QTextOption() ); /*! \brief Draws an ellipse at the given position. The ellipse is placed with its center located at the given \a centerPosition. For the outline it uses the painter's pen and for the background the painter's brush. If \a isGeoProjected is true then the outline of the ellipse is drawn in geographic coordinates. In this case the \a width and the \a height are interpreted to be degrees. If \a isGeoProjected is false then the outline of the ellipse is drawn in screen coordinates. In this case the \a width and the \a height are interpreted to be pixels. \see GeoDataCoordinates */ void drawEllipse ( const GeoDataCoordinates & centerPosition, qreal width, qreal height, bool isGeoProjected = false ); /*! \brief Creates a region for an ellipse at a given position A QRegion object is created that represents the area covered by GeoPainter::drawEllipse(). As such it can be used e.g. for input event handling for objects that have been painted using GeoPainter::drawEllipse(). The \a strokeWidth allows to extrude the QRegion by half the amount of "stroke width" pixels. For input event handling it's always advisable to use a width that is slightly bigger than the width of the painter's pen. \see GeoDataCoordinates */ QRegion regionFromEllipse ( const GeoDataCoordinates & centerPosition, qreal width, qreal height, bool isGeoProjected = false, qreal strokeWidth = 3 ) const; /*! \brief Draws an image at the given position. The image is placed with its center located at the given \a centerPosition. The image rendering is performed in screen coordinates and is not subject to the current projection. \see GeoDataCoordinates */ void drawImage ( const GeoDataCoordinates & centerPosition, const QImage & image /* , bool isGeoProjected = false */ ); /*! \brief Draws a pixmap at the given position. The pixmap is placed with its center located at the given \a centerPosition. The image rendering is performed in screen coordinates and is not subject to the current projection. \see GeoDataCoordinates */ void drawPixmap ( const GeoDataCoordinates & centerPosition, const QPixmap & pixmap /*, bool isGeoProjected = false */ ); /*! \brief Creates a region for a rectangle for a pixmap at a given position. A QRegion object is created that represents the area covered by GeoPainter::drawPixmap(). This can be used e.g. for input event handling for objects that have been painted using GeoPainter::drawPixmap(). The \a margin allows to extrude the QRegion by "margin" pixels on every side. \see GeoDataCoordinates */ QRegion regionFromPixmapRect(const GeoDataCoordinates ¢erCoordinates, int width, int height, int margin = 0) const; /*! \brief Helper method for safe and quick linestring conversion. In general drawPolyline() should be used instead. However in situations where the same linestring is supposed to be drawn multiple times it's a good idea to cache the screen polygons using this method. \see GeoDataLineString */ void polygonsFromLineString( const GeoDataLineString &lineString, QVector &polygons ); /*! \brief Draws a given line string (a "polyline") with a label. The \a lineString is drawn using the current pen. It's possible to provide a \a labelText for the \a lineString. The text is rendered using the painter's font property. The position of the \a labelText can be specified using the \a labelPositionFlags. \see GeoDataLineString */ void drawPolyline ( const GeoDataLineString & lineString, const QString& labelText, LabelPositionFlags labelPositionFlags = LineCenter, const QColor& labelcolor = Qt::black); /*! \brief Draws Labels for a given set of screen polygons. In common cases the drawPolyline overload can be used instead. However in certain more complex cases this particular method might be helpful for further optimization. */ void drawLabelsForPolygons( const QVector &polygons, const QString& labelText, LabelPositionFlags labelPositionFlags, const QColor& labelColor ); /*! \brief Draws a given line string (a "polyline"). The \a lineString is drawn using the current pen. \see GeoDataLineString */ void drawPolyline(const GeoDataLineString & lineString); /*! \brief Creates a region for a given line string (a "polyline"). A QRegion object is created that represents the area covered by GeoPainter::drawPolyline( GeoDataLineString ). As such it can be used e.g. for input event handling for objects that have been painted using GeoPainter::drawPolyline( GeoDataLineString ). The \a strokeWidth allows to extrude the QRegion by half the amount of "stroke width" pixels. For input event handling it's always advisable to use a width that is slightly bigger than the width of the painter's pen. \see GeoDataLineString */ QRegion regionFromPolyline ( const GeoDataLineString & lineString, qreal strokeWidth = 3 ) const; /*! \brief Draws a given linear ring (a "polygon without holes"). The outline of the \a linearRing is drawn using the current pen. The background is painted using the current brush of the painter. Like in QPainter::drawPolygon() the \a fillRule specifies the fill algorithm that is used to fill the polygon. \see GeoDataLinearRing */ void drawPolygon ( const GeoDataLinearRing & linearRing, Qt::FillRule fillRule = Qt::OddEvenFill ); /*! \brief Creates a region for a given linear ring (a "polygon without holes"). A QRegion object is created that represents the area covered by GeoPainter::drawPolygon( GeoDataLinearRing ). As such it can be used e.g. for input event handling for objects that have been painted using GeoPainter::drawPolygon( GeoDataLinearRing ). Like in drawPolygon() the \a fillRule specifies the fill algorithm that is used to fill the polygon. The \a strokeWidth allows to extrude the QRegion by half the amount of "stroke width" pixels. For input event handling it's always advisable to use a width that is slightly bigger than the width of the painter's pen. For the polygon case a "cosmetic" strokeWidth of zero should provide the best performance. \see GeoDataLinearRing */ QRegion regionFromPolygon ( const GeoDataLinearRing & linearRing, Qt::FillRule fillRule, qreal strokeWidth = 3 ) const; /*! \brief Draws a given polygon (which may contain holes). The outline of the \a polygon is drawn using the current pen. The background is painted using the current brush of the painter. Like in QPainter::drawPolygon() the \a fillRule specifies the fill algorithm that is used to fill the polygon. \see GeoDataPolygon */ void drawPolygon ( const GeoDataPolygon & polygon, Qt::FillRule fillRule = Qt::OddEvenFill ); + + QVector createFillPolygons( const QVector & outerPolygons, + const QVector & innerPolygons ) const; /*! \brief Draws a rectangle at the given position. The rectangle is placed with its center located at the given \a centerPosition. For the outline it uses the painter's pen and for the background the painter's brush. If \a isGeoProjected is true then the outline of the rectangle is drawn in geographic coordinates. In this case the \a width and the \a height are interpreted to be degrees. If \a isGeoProjected is false then the outline of the rectangle is drawn in screen coordinates. In this case the \a width and the \a height are interpreted to be pixels. \see GeoDataCoordinates */ void drawRect ( const GeoDataCoordinates & centerPosition, qreal width, qreal height, bool isGeoProjected = false ); /*! \brief Creates a region for a rectangle at a given position. A QRegion object is created that represents the area covered by GeoPainter::drawRect(). This can be used e.g. for input event handling for objects that have been painted using GeoPainter::drawRect(). The isGeoProjected parameter is used the same way as for GeoPainter::drawRect(). The \a strokeWidth allows to extrude the QRegion by half the amount of "stroke width" pixels. For input event handling it's always advisable to use a width that is slightly bigger than the width of the painter's pen. This is especially true for small objects. \see GeoDataCoordinates */ QRegion regionFromRect ( const GeoDataCoordinates & centerPosition, qreal width, qreal height, bool isGeoProjected = false, qreal strokeWidth = 3 ) const; /*! \brief Draws a rectangle with rounded corners at the given position. The rectangle is placed with its center located at the given \a centerPosition. For the outline it uses the painter's pen and for the background the painter's brush. Unlike in QPainter::drawRoundedRect() the rounded corners are not specified in percentage but in pixels to provide for optimal aesthetics. \param width Width of the rectangle in pixels \param height Height of the rectangle in pixels \param xRnd Specifies the geometry of the rounded corners in pixels along the x-axis. \param yRnd Specifies the geometry of the rounded corners in pixels along the y-axis. \see GeoDataCoordinates */ void drawRoundedRect(const GeoDataCoordinates ¢erPosition, qreal width, qreal height, qreal xRnd = 25.0, qreal yRnd = 25.0); // Reenabling QPainter+ClipPainter methods. using QPainter::drawText; using QPainter::drawEllipse; using QPainter::drawImage; using QPainter::drawPixmap; using QPainter::drawPoint; using ClipPainter::drawPolyline; using ClipPainter::drawPolygon; using QPainter::drawRect; using QPainter::drawRoundedRect; private: Q_DISABLE_COPY( GeoPainter ) GeoPainterPrivate * const d; }; } #endif diff --git a/src/lib/marble/geodata/data/GeoDataPolygon.cpp b/src/lib/marble/geodata/data/GeoDataPolygon.cpp index acfeb26f8..ee9e7056c 100644 --- a/src/lib/marble/geodata/data/GeoDataPolygon.cpp +++ b/src/lib/marble/geodata/data/GeoDataPolygon.cpp @@ -1,273 +1,247 @@ // // 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 2008-2009 Patrick Spendrin // Copyright 2008 Inge Wallin // #include "GeoDataCoordinates.h" #include "GeoDataPolygon.h" #include "GeoDataPolygon_p.h" #include "MarbleDebug.h" #include #include namespace Marble { GeoDataPolygon::GeoDataPolygon( TessellationFlags f ) : GeoDataGeometry( new GeoDataPolygonPrivate( f ) ) { // nothing to do } GeoDataPolygon::GeoDataPolygon( const GeoDataGeometry & other ) : GeoDataGeometry( other ) { // nothing to do } GeoDataPolygon::~GeoDataPolygon() { #ifdef DEBUG_GEODATA mDebug() << "delete polygon"; #endif } bool GeoDataPolygon::operator==( const GeoDataPolygon &other ) const { Q_D(const GeoDataPolygon); const GeoDataPolygonPrivate *other_d = other.d_func(); if ( !GeoDataGeometry::equals(other) || tessellate() != other.tessellate() || isClosed() != other.isClosed() || d->inner.size() != other_d->inner.size() || d->outer != other_d->outer ) { return false; } QVector::const_iterator itBound = d->inner.constBegin(); QVector::const_iterator itEnd = d->inner.constEnd(); QVector::const_iterator otherItBound = other_d->inner.constBegin(); QVector::const_iterator otherItEnd= other_d->inner.constEnd(); for ( ; itBound != itEnd && otherItBound != otherItEnd; ++itBound, ++otherItBound ) { if ( *itBound != *otherItBound) { return false; } } Q_ASSERT ( itBound == itEnd && otherItBound == otherItEnd ); return true; } bool GeoDataPolygon::operator!=( const GeoDataPolygon &other ) const { return !this->operator==(other); } bool GeoDataPolygon::isClosed() const { return true; } bool GeoDataPolygon::tessellate() const { Q_D(const GeoDataPolygon); return d->m_tessellationFlags.testFlag(Tessellate); } void GeoDataPolygon::setTessellate( bool tessellate ) { // According to the KML reference the tesselation is done along great circles // for polygons in Google Earth. Our "Tesselate" flag does this. // Only for pure line strings and linear rings the // latitude circles are followed for subsequent points that share the same latitude. detach(); Q_D(GeoDataPolygon); if ( tessellate ) { d->m_tessellationFlags |= Tessellate; } else { d->m_tessellationFlags ^= Tessellate; } } TessellationFlags GeoDataPolygon::tessellationFlags() const { Q_D(const GeoDataPolygon); return d->m_tessellationFlags; } void GeoDataPolygon::setTessellationFlags( TessellationFlags f ) { detach(); Q_D(GeoDataPolygon); d->m_tessellationFlags = f; } const GeoDataLatLonAltBox& GeoDataPolygon::latLonAltBox() const { Q_D(const GeoDataPolygon); return d->outer.latLonAltBox(); } GeoDataLinearRing &GeoDataPolygon::outerBoundary() { detach(); Q_D(GeoDataPolygon); return (d->outer); } const GeoDataLinearRing &GeoDataPolygon::outerBoundary() const { Q_D(const GeoDataPolygon); return d->outer; } void GeoDataPolygon::setOuterBoundary( const GeoDataLinearRing& boundary ) { detach(); Q_D(GeoDataPolygon); d->outer = boundary; } QVector& GeoDataPolygon::innerBoundaries() { detach(); Q_D(GeoDataPolygon); return d->inner; } const QVector& GeoDataPolygon::innerBoundaries() const { Q_D(const GeoDataPolygon); return d->inner; } void GeoDataPolygon::appendInnerBoundary( const GeoDataLinearRing& boundary ) { detach(); Q_D(GeoDataPolygon); d->inner.append(boundary); } -GeoDataLinearRing GeoDataPolygon::toLinearRing() const -{ - Q_D(const GeoDataPolygon); - if (d->inner.size() == 0) { - return d->outer; - } - - GeoDataLinearRing unrolledRing(d->outer); - - // If there is no detail level set we need to ensure - // that all nodes get preserved - if (unrolledRing.first().detail() == 0) { - unrolledRing.setTessellationFlags(PreventNodeFiltering); - } - unrolledRing << unrolledRing.first(); - - for( int i = 0; i < d->inner.size(); ++i ) - { - unrolledRing << d->inner[i]; - unrolledRing << d->inner[i].first(); - unrolledRing << unrolledRing.first(); - } - - return unrolledRing; -} - void GeoDataPolygon::setRenderOrder(int renderOrder) { detach(); Q_D(GeoDataPolygon); d->m_renderOrder = renderOrder; } int GeoDataPolygon::renderOrder() const { Q_D(const GeoDataPolygon); return d->m_renderOrder; } void GeoDataPolygon::pack( QDataStream& stream ) const { Q_D(const GeoDataPolygon); GeoDataObject::pack( stream ); d->outer.pack( stream ); stream << d->inner.size(); stream << (qint32)(d->m_tessellationFlags); for( QVector::const_iterator iterator = d->inner.constBegin(); iterator != d->inner.constEnd(); ++iterator ) { mDebug() << "innerRing: size" << d->inner.size(); GeoDataLinearRing linearRing = ( *iterator ); linearRing.pack( stream ); } } void GeoDataPolygon::unpack( QDataStream& stream ) { detach(); Q_D(GeoDataPolygon); GeoDataObject::unpack( stream ); d->outer.unpack( stream ); qint32 size; qint32 tessellationFlags; stream >> size; stream >> tessellationFlags; d->m_tessellationFlags = (TessellationFlags)(tessellationFlags); QVector &inner = d->inner; inner.reserve(inner.size() + size); for(qint32 i = 0; i < size; i++ ) { GeoDataLinearRing linearRing; linearRing.unpack( stream ); inner.append(linearRing); } } bool GeoDataPolygon::contains( const GeoDataCoordinates &coordinates ) const { if ( !outerBoundary().contains( coordinates ) ) { // Not inside the polygon at all return false; } foreach( const GeoDataLinearRing &ring, innerBoundaries() ) { if ( ring.contains( coordinates ) ) { // Inside the polygon, but in one of its holes return false; } } return true; } } diff --git a/src/lib/marble/geodata/data/GeoDataPolygon.h b/src/lib/marble/geodata/data/GeoDataPolygon.h index d1861f65c..e32dd89be 100644 --- a/src/lib/marble/geodata/data/GeoDataPolygon.h +++ b/src/lib/marble/geodata/data/GeoDataPolygon.h @@ -1,245 +1,237 @@ // // 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 2008-2009 Patrick Spendrin // Copyright 2008 Inge Wallin // #ifndef MARBLE_GEODATAPOLYGON_H #define MARBLE_GEODATAPOLYGON_H #include #include "MarbleGlobal.h" #include "geodata_export.h" #include "GeoDataGeometry.h" namespace Marble { class GeoDataPolygonPrivate; class GeoDataLinearRing; class GeoDataCoordinates; /*! \class GeoDataPolygon \brief A polygon that can have "holes". GeoDataPolygon is a tool class that implements the Polygon tag/class of the Open Geospatial Consortium standard KML 2.2. GeoDataPolygon extends GeoDataGeometry to store and edit Polygons. In the QPainter API "pure" Polygons would represent polygons with "holes" inside. However QPolygon doesn't provide this feature directly. Whenever a Polygon is painted GeoDataLineStyle should be used to assign a color and line width. The polygon consists of \li a single outer boundary and \li optionally a set of inner boundaries. All boundaries are LinearRings. The boundaries of a GeoDataPolygon consist of several (geodetic) nodes which are each connected through line segments. The nodes are stored as GeoDataCoordinates objects. The API which provides access to the nodes is similar to the API of QVector. GeoDataPolygon allows Polygons to be tessellated in order to make them follow the terrain and the curvature of the earth. The tessellation options allow for different ways of visualization: \li Not tessellated: A Polygon that connects each two nodes directly and straight in screen coordinate space. \li A tessellated line: Each line segment is bent so that the Polygon follows the curvature of the earth and its terrain. A tessellated line segment connects two nodes at the shortest possible distance ("along great circles"). \li A tessellated line that follows latitude circles whenever possible: In this case Latitude circles are followed as soon as two subsequent nodes have exactly the same amount of latitude. In all other places the line segments follow great circles. Some convenience methods have been added that allow to calculate the geodesic bounding box or the length of a Polygon. \see GeoDataLinearRing */ class GEODATA_EXPORT GeoDataPolygon : public GeoDataGeometry { public: /*! \brief Creates a new Polygon. */ explicit GeoDataPolygon( TessellationFlags f = NoTessellation ); /*! \brief Creates a Polygon from an existing geometry object. */ explicit GeoDataPolygon( const GeoDataGeometry &other ); /*! \brief Destroys a Polygon. */ virtual ~GeoDataPolygon(); /*! \brief Returns true/false depending on whether this and other are/are not equal. */ bool operator==( const GeoDataPolygon &other ) const; bool operator!=( const GeoDataPolygon &other ) const; /*! \brief Returns whether a Polygon is a closed polygon. \return true for a Polygon. */ virtual bool isClosed() const; /*! \brief Returns whether the Polygon follows the earth's surface. \return true if the Polygon's line segments follow the earth's surface and terrain along great circles. */ bool tessellate() const; /*! \brief Sets the tessellation property for the Polygon. If \a tessellate is true then the Polygon's line segments are bent and follow the earth's surface and terrain along great circles. If \a tessellate is false then the Polygon's line segments are rendered as straight lines in screen coordinate space. */ void setTessellate( bool tessellate ); /*! \brief Returns the tessellation flags for a Polygon. */ TessellationFlags tessellationFlags() const; /*! \brief Sets the given tessellation flags for a Polygon. */ void setTessellationFlags( TessellationFlags f ); /*! \brief Returns the smallest latLonAltBox that contains the Polygon. \see GeoDataLatLonAltBox */ virtual const GeoDataLatLonAltBox& latLonAltBox() const; /*! \brief Returns the outer boundary that is represented as a LinearRing. \see GeoDataLinearRing */ GeoDataLinearRing &outerBoundary(); /*! \brief Returns the outer boundary that is represented as a LinearRing. \see GeoDataLinearRing */ const GeoDataLinearRing &outerBoundary() const; /*! \brief Sets the given LinearRing as an outer boundary of the Polygon. \see GeoDataLinearRing */ void setOuterBoundary( const GeoDataLinearRing& boundary ); /*! \brief Returns a set of inner boundaries which are represented as LinearRings. \see GeoDataLinearRing */ QVector& innerBoundaries(); /*! \brief Returns a set of inner boundaries which are represented as LinearRings. \see GeoDataLinearRing */ const QVector& innerBoundaries() const; /*! \brief Appends a given LinearRing as an inner boundary of the Polygon. \see GeoDataLinearRing */ void appendInnerBoundary( const GeoDataLinearRing& boundary ); -/*! - \brief Returns a linear ring that unrolls outer and inner rings into a - single linearring. This method is slow. - - \see GeoDataLinearRing -*/ - GeoDataLinearRing toLinearRing() const; - /*! \brief Returns whether the given coordinates lie within the polygon. \return true if the coordinates lie within the polygon (and not in its holes), false otherwise. */ virtual bool contains( const GeoDataCoordinates &coordinates ) const; // Serialization /*! \brief Serialize the Polygon to a stream. \param stream the stream. */ virtual void pack( QDataStream& stream ) const; /*! \brief Unserialize the Polygon from a stream. \param stream the stream. */ virtual void unpack( QDataStream& stream ); int renderOrder() const; void setRenderOrder(int); private: Q_DECLARE_PRIVATE(GeoDataPolygon) }; class GEODATA_EXPORT GeoDataOuterBoundary : public GeoDataPolygon { }; class GEODATA_EXPORT GeoDataInnerBoundary : public GeoDataPolygon { }; } #endif // GEODATAPOLYGON_H diff --git a/src/lib/marble/geodata/graphicsitem/AbstractGeoPolygonGraphicsItem.cpp b/src/lib/marble/geodata/graphicsitem/AbstractGeoPolygonGraphicsItem.cpp index bc69bc905..a60439be1 100644 --- a/src/lib/marble/geodata/graphicsitem/AbstractGeoPolygonGraphicsItem.cpp +++ b/src/lib/marble/geodata/graphicsitem/AbstractGeoPolygonGraphicsItem.cpp @@ -1,194 +1,173 @@ // // 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 // #include "AbstractGeoPolygonGraphicsItem.h" #include "GeoDataLinearRing.h" #include "GeoDataPolygon.h" #include "GeoPainter.h" #include "GeoDataLatLonAltBox.h" #include "GeoDataStyle.h" #include "GeoDataIconStyle.h" #include "GeoDataLineStyle.h" #include "GeoDataPlacemark.h" #include "GeoDataPolyStyle.h" #include "GeoDataTypes.h" #include "OsmPlacemarkData.h" #include "MarbleDebug.h" #include "ViewportParams.h" #include namespace Marble { QPixmapCache AbstractGeoPolygonGraphicsItem::m_textureCache = QPixmapCache(); AbstractGeoPolygonGraphicsItem::AbstractGeoPolygonGraphicsItem(const GeoDataPlacemark *placemark, const GeoDataPolygon *polygon) : GeoGraphicsItem(placemark), m_polygon(polygon), - m_ring(0), - m_unrolledRing(0) + m_ring(0) { - m_unrolledRing = new GeoDataLinearRing(); - *m_unrolledRing = polygon->toLinearRing(); } AbstractGeoPolygonGraphicsItem::AbstractGeoPolygonGraphicsItem(const GeoDataPlacemark *placemark, const GeoDataLinearRing *ring) : GeoGraphicsItem(placemark), m_polygon(0), - m_ring(ring), - m_unrolledRing(0) + m_ring(ring) { } AbstractGeoPolygonGraphicsItem::~AbstractGeoPolygonGraphicsItem() { - delete m_unrolledRing; } 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 ) { bool innerResolved = false; for(auto ring : m_polygon->innerBoundaries()) { if (viewport->resolves(ring.latLonAltBox(), 4)) { innerResolved = true; break; } } if (innerResolved) { - QPen currentPen = painter->pen(); - QBrush currentBrush = painter->brush(); - - // Draw the filling - painter->setPen(Qt::transparent); - painter->drawPolygon( *m_unrolledRing ); - - // Calculate and draw the outline only if it's not transparent - if (currentPen.color() != "transparent" && currentPen.style() != Qt::NoPen) { - painter->setPen(currentPen); - painter->setBrush(QBrush("transparent")); - painter->drawPolygon( m_polygon->outerBoundary() ); - for(auto ring : m_polygon->innerBoundaries()) { - painter->drawPolygon(ring); - } - } - painter->setBrush(currentBrush); + painter->drawPolygon(*m_polygon); } else { painter->drawPolygon(m_polygon->outerBoundary()); } } else if ( m_ring ) { painter->drawPolygon( *m_ring ); } painter->restore(); } QPen AbstractGeoPolygonGraphicsItem::configurePainter(GeoPainter *painter, const ViewportParams *viewport) { QPen currentPen = painter->pen(); GeoDataStyle::ConstPtr style = this->style(); if (!style) { painter->setPen( QPen() ); // "style-less" polygons: a 1px black solid line } else { const GeoDataPolyStyle& polyStyle = style->polyStyle(); if (polyStyle.outline()) { // polygons without outline: Qt::NoPen (not drawn) const GeoDataLineStyle& lineStyle = style->lineStyle(); currentPen.setColor(lineStyle.paintedColor()); currentPen.setWidthF(lineStyle.width()); currentPen.setCapStyle(lineStyle.capStyle()); currentPen.setStyle(lineStyle.penStyle()); if (painter->pen().color() != currentPen.color()) { 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()) { if (!polyStyle.texturePath().isEmpty() || !polyStyle.textureImage().isNull()) { GeoDataCoordinates coords = latLonAltBox().center(); qreal x, y; viewport->screenCoordinates(coords, x, y); QBrush brush(texture(polyStyle.texturePath(), polyStyle.textureImage(), paintedColor)); painter->setBrush(brush); painter->setBrushOrigin(QPoint(x,y)); } else { painter->setBrush(QBrush(paintedColor, polyStyle.brushStyle())); } } } } return currentPen; } int AbstractGeoPolygonGraphicsItem::extractElevation(const GeoDataPlacemark &placemark) { int elevation = 0; const OsmPlacemarkData &osmData = placemark.osmData(); const auto tagIter = osmData.findTag(QStringLiteral("ele")); if (tagIter != osmData.tagsEnd()) { elevation = tagIter.value().toInt(); } return elevation; } QPixmap AbstractGeoPolygonGraphicsItem::texture(const QString &texturePath, const QImage &textureImage, const QColor &color) { QString const key = QString("%1/%2").arg(color.rgba()).arg(texturePath); QPixmap texture; if (!m_textureCache.find(key, texture)) { if (textureImage.hasAlphaChannel()) { QImage image = QImage (textureImage.size(), QImage::Format_ARGB32_Premultiplied); image.fill(color); QPainter imagePainter(&image); imagePainter.drawImage(0, 0, textureImage); imagePainter.end(); texture = QPixmap::fromImage(image); } else { texture = QPixmap::fromImage(textureImage); } m_textureCache.insert(key, texture); } return texture; } } diff --git a/src/lib/marble/geodata/graphicsitem/AbstractGeoPolygonGraphicsItem.h b/src/lib/marble/geodata/graphicsitem/AbstractGeoPolygonGraphicsItem.h index b2198b18d..e94d452b0 100644 --- a/src/lib/marble/geodata/graphicsitem/AbstractGeoPolygonGraphicsItem.h +++ b/src/lib/marble/geodata/graphicsitem/AbstractGeoPolygonGraphicsItem.h @@ -1,61 +1,58 @@ // // 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 // #ifndef MARBLE_ABSTRACTGEOPOLYGONGRAPHICSITEM_H #define MARBLE_ABSTRACTGEOPOLYGONGRAPHICSITEM_H #include "GeoGraphicsItem.h" #include "marble_export.h" #include #include #include namespace Marble { class GeoDataLinearRing; class GeoDataPlacemark; class GeoDataPolygon; class MARBLE_EXPORT AbstractGeoPolygonGraphicsItem : public GeoGraphicsItem { protected: explicit AbstractGeoPolygonGraphicsItem(const GeoDataPlacemark *placemark, const GeoDataPolygon *polygon); explicit AbstractGeoPolygonGraphicsItem(const GeoDataPlacemark *placemark, const GeoDataLinearRing *ring); ~AbstractGeoPolygonGraphicsItem(); 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; } - inline - const GeoDataLinearRing *unrolledRing() const { return m_unrolledRing; } static int extractElevation(const GeoDataPlacemark &placemark); private: QPixmap texture(const QString &path, const QImage &textureImage, const QColor &color); const GeoDataPolygon *const m_polygon; const GeoDataLinearRing *const m_ring; - GeoDataLinearRing * m_unrolledRing; static QPixmapCache m_textureCache; }; } #endif diff --git a/src/lib/marble/geodata/graphicsitem/BuildingGeoPolygonGraphicsItem.cpp b/src/lib/marble/geodata/graphicsitem/BuildingGeoPolygonGraphicsItem.cpp index b356fd410..e7f904fc0 100644 --- a/src/lib/marble/geodata/graphicsitem/BuildingGeoPolygonGraphicsItem.cpp +++ b/src/lib/marble/geodata/graphicsitem/BuildingGeoPolygonGraphicsItem.cpp @@ -1,481 +1,512 @@ // // 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 // #include "BuildingGeoPolygonGraphicsItem.h" #include "MarbleDebug.h" #include "ViewportParams.h" #include "GeoDataTypes.h" #include "GeoDataPlacemark.h" #include "GeoDataLinearRing.h" #include "GeoDataPolygon.h" #include "GeoDataPolyStyle.h" #include "OsmPlacemarkData.h" #include "GeoPainter.h" #include #include namespace Marble { BuildingGeoPolygonGraphicsItem::BuildingGeoPolygonGraphicsItem(const GeoDataPlacemark *placemark, const GeoDataPolygon *polygon) : AbstractGeoPolygonGraphicsItem(placemark, polygon) , m_buildingHeight(extractBuildingHeight(*placemark)) , m_buildingLabel(extractBuildingLabel(*placemark)) , m_entries(extractNamedEntries(*placemark)) , m_hasInnerBoundaries(false) { 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 GeoDataPlacemark *placemark, const GeoDataLinearRing* ring) : AbstractGeoPolygonGraphicsItem(placemark, ring) , m_buildingHeight(extractBuildingHeight(*placemark)) , m_buildingLabel(extractBuildingLabel(*placemark)) , m_entries(extractNamedEntries(*placemark)) { 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 ) const { drawAccurate3D = false; isCameraAboveBuilding = false; auto const screen = QApplication::screens().first(); double const physicalSize = 1.0; // mm int const pixelSize = qRound(physicalSize * screen->physicalDotsPerInch() / (IN2M * M2MM)); QPointF offsetAtCorner = buildingOffset(QPointF(0, 0), viewport, &isCameraAboveBuilding); qreal maxOffset = qMax( qAbs( offsetAtCorner.x() ), qAbs( offsetAtCorner.y() ) ); drawAccurate3D = painter->mapQuality() == HighQuality ? maxOffset > pixelSize : maxOffset > 1.5 * pixelSize; } void BuildingGeoPolygonGraphicsItem::updatePolygons( const ViewportParams *viewport, - QVector& outlinePolygons, + QVector& outerPolygons, QVector& innerPolygons, - QVector& fillPolygons, bool &hasInnerBoundaries ) { // 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, fillPolygons); + screenPolygons(viewport, polygon(), innerPolygons, outerPolygons); } else { - viewport->screenCoordinates(polygon()->outerBoundary(), outlinePolygons); + viewport->screenCoordinates(polygon()->outerBoundary(), outerPolygons); } } else if (ring()) { - viewport->screenCoordinates(*ring(), outlinePolygons); + viewport->screenCoordinates(*ring(), outerPolygons); } } 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 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 GeoDataPlacemark &placemark) { double height = 8.0; const OsmPlacemarkData &osmData = placemark.osmData(); QHash::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 GeoDataPlacemark &placemark) { 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::extractNamedEntries(const GeoDataPlacemark &placemark) { QVector entries; 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"))) { - Q_ASSERT(m_cachedOutlinePolygons.isEmpty()); + Q_ASSERT(m_cachedOuterPolygons.isEmpty()); Q_ASSERT(m_cachedInnerPolygons.isEmpty()); - Q_ASSERT(m_cachedFillPolygons.isEmpty()); - updatePolygons(viewport, m_cachedOutlinePolygons, + updatePolygons(viewport, m_cachedOuterPolygons, m_cachedInnerPolygons, - m_cachedFillPolygons, m_hasInnerBoundaries); - if (m_cachedOutlinePolygons.isEmpty()) { + if (m_cachedOuterPolygons.isEmpty()) { return; } paintFrame(painter, viewport); } else if (layer.endsWith(QLatin1String("/roof"))) { - if (m_cachedOutlinePolygons.isEmpty()) { + if (m_cachedOuterPolygons.isEmpty()) { return; } paintRoof(painter, viewport); - qDeleteAll(m_cachedOutlinePolygons); - qDeleteAll(m_cachedFillPolygons); - m_cachedOutlinePolygons.clear(); + qDeleteAll(m_cachedOuterPolygons); + m_cachedOuterPolygons.clear(); m_cachedInnerPolygons.clear(); - m_cachedFillPolygons.clear(); } 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; initializeBuildingPainting(painter, viewport, drawAccurate3D, isCameraAboveBuilding); if (!isCameraAboveBuilding) { return; // do not render roof if we look inside the building } painter->save(); QPen const currentPen = configurePainter(painter, viewport); qreal maxSize(0.0); QPointF roofCenter; // first paint the area (and the outline if there are no inner boundaries) double maxArea = 0.0; - int outlineCount = 0; + QVector outerRoofs; + QVector innerRoofs; + + // First calculate the perspective for the outer and inner polygons + // The result is stored inside outerRoof and innerRoof + if ( drawAccurate3D) { + foreach(QPolygonF* outerPolygon, m_cachedOuterPolygons) { + QPolygonF * outerRoof = new QPolygonF(); + outerRoof->reserve(outerPolygon->size()); + foreach(const QPointF &point, *outerPolygon) { + *outerRoof << point + buildingOffset(point, viewport); + } + outerRoofs << outerRoof; + } + foreach(QPolygonF* innerPolygon, m_cachedInnerPolygons) { + QPolygonF * innerRoof = new QPolygonF(); + innerRoof->reserve(innerPolygon->size()); + foreach(const QPointF &point, *innerPolygon) { + *innerRoof << point + buildingOffset(point, viewport); + } + innerRoofs << innerRoof; + } + } - foreach(QPolygonF* outlinePolygon, m_cachedOutlinePolygons) { - QRectF const boundingRect = outlinePolygon->boundingRect(); - QPolygonF buildingRoof; + if ( drawAccurate3D) { + if (m_hasInnerBoundaries) { - // Label position calculation - if (!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); + painter->setPen(Qt::NoPen); + QVector fillPolygons = painter->createFillPolygons( outerRoofs, + innerRoofs ); + + foreach( const QPolygonF* fillPolygon, fillPolygons ) { + painter->drawPolygon(*fillPolygon); + } + + painter->setPen(currentPen); + + QBrush currentBrush = painter->brush(); + painter->setBrush(QBrush(Qt::transparent)); + + foreach( const QPolygonF* outerRoof, outerRoofs ) { + painter->drawPolygon( *outerRoof ); + } + foreach( const QPolygonF* innerRoof, innerRoofs ) { + painter->drawPolygon( *innerRoof ); } - } - if ( drawAccurate3D) { - buildingRoof.reserve(outlinePolygon->size()); - foreach(const QPointF &point, *outlinePolygon) { - buildingRoof << point + buildingOffset(point, viewport); + painter->setBrush(currentBrush); + } + else { + foreach( const QPolygonF* outerRoof, outerRoofs ) { + painter->drawPolygon( *outerRoof ); } + } + } + else { + QPointF const offset = buildingOffset(m_cachedOuterPolygons[0]->boundingRect().center(), viewport); + painter->translate(offset); - if (m_hasInnerBoundaries) { + if (m_hasInnerBoundaries) { - // Paint all fillPolygons once together - if (outlineCount == 0) { - painter->setPen(Qt::NoPen); + painter->setPen(Qt::NoPen); + QVector fillPolygons = painter->createFillPolygons( m_cachedOuterPolygons, + m_cachedInnerPolygons ); - foreach(QPolygonF* fillPolygon, m_cachedFillPolygons) { - QPolygonF buildingRoofFill; - buildingRoofFill.reserve(fillPolygon->size()); + foreach( const QPolygonF* fillPolygon, fillPolygons ) { + painter->drawPolygon(*fillPolygon); + } - foreach(const QPointF &point, *fillPolygon) { - buildingRoofFill << point + buildingOffset(point, viewport); - } - painter->drawPolygon( buildingRoofFill ); - } + painter->setPen(currentPen); - painter->setPen(currentPen); - } + QBrush currentBrush = painter->brush(); + painter->setBrush(QBrush(Qt::transparent)); - QBrush currentBrush = painter->brush(); - painter->setBrush(QBrush("transparent")); - painter->drawPolygon(buildingRoof); - painter->setBrush(currentBrush); + foreach( const QPolygonF* outerPolygon, m_cachedOuterPolygons ) { + painter->drawPolygon( *outerPolygon ); } - else { - painter->drawPolygon(buildingRoof); + foreach( const QPolygonF* innerPolygon, m_cachedInnerPolygons ) { + painter->drawPolygon( *innerPolygon ); } - } else { - QPointF const offset = buildingOffset(boundingRect.center(), viewport); - painter->translate(offset); - if (m_hasInnerBoundaries) { + painter->setBrush(currentBrush); - // Paint all fillPolygons once together - if (outlineCount == 0) { - painter->setPen(Qt::NoPen); - foreach(const QPolygonF* itPolygon, m_cachedFillPolygons) { - painter->drawPolygon( *itPolygon ); - } + } + else { + foreach( const QPolygonF* outerPolygon, m_cachedOuterPolygons ) { + painter->drawPolygon( *outerPolygon ); + } + } + painter->translate(-offset); - painter->setPen(currentPen); - } + } - QBrush currentBrush = painter->brush(); - painter->setBrush(QBrush("transparent")); - painter->drawPolygon( *outlinePolygon ); - painter->setBrush(currentBrush); - } - else { - painter->drawPolygon( *outlinePolygon ); + + for (int i = 0; i < m_cachedOuterPolygons.size(); ++i) { + QPolygonF* outerPolygon = m_cachedOuterPolygons[i]; + + QRectF const boundingRect = outerPolygon->boundingRect(); + + // Label position calculation + if (!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(*outerPolygon, area); + maxArea = qMax(area, maxArea); + roofCenter += buildingOffset(roofCenter, viewport); } - painter->translate(-offset); } + QPolygonF * outerRoof = outerRoofs[i]; // Draw the housenumber labels 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) + if (outerRoof->containsPoint(textPosition + QPointF(-2, -ascent), Qt::OddEvenFill) + && outerRoof->containsPoint(textPosition + QPointF(-2, descent), Qt::OddEvenFill) + && outerRoof->containsPoint(textPosition + QPointF(2+2*w2, descent), Qt::OddEvenFill) + && outerRoof->containsPoint(textPosition + QPointF(2+2*w2, -ascent), Qt::OddEvenFill) ) { painter->drawText(textPosition, m_buildingLabel); } } - - ++outlineCount; + ++i; } // 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); } + qDeleteAll(outerRoofs); + qDeleteAll(innerRoofs); + + painter->restore(); } void BuildingGeoPolygonGraphicsItem::paintFrame(GeoPainter *painter, const ViewportParams *viewport) { // TODO: how does this match the Q_ASSERT in the constructor? if (m_buildingHeight == 0.0) { return; } if ((polygon() && !viewport->resolves(polygon()->outerBoundary().latLonAltBox(), 4)) || (ring() && !viewport->resolves(ring()->latLonAltBox(), 4))) { return; } painter->save(); bool drawAccurate3D; bool isCameraAboveBuilding; initializeBuildingPainting(painter, viewport, drawAccurate3D, isCameraAboveBuilding); configureFramePainter(painter); - foreach(QPolygonF* outlinePolygon, m_cachedOutlinePolygons) { - if (outlinePolygon->isEmpty()) { - continue; - } - if ( drawAccurate3D && isCameraAboveBuilding ) { + if ( drawAccurate3D && isCameraAboveBuilding ) { + QVector outlines = m_cachedOuterPolygons; + outlines << m_cachedInnerPolygons; + foreach(QPolygonF* outline, outlines) { + if (outline->isEmpty()) { + continue; + } // draw the building sides - int const size = outlinePolygon->size(); - QPointF & a = (*outlinePolygon)[0]; + int const size = outline->size(); + QPointF & a = (*outline)[0]; QPointF shiftA = a + buildingOffset(a, viewport); for (int i=1; idrawPolygon(buildingSide); a = b; shiftA = shiftB; } - } else { + } + } else { // don't draw the building sides - just draw the base frame instead - foreach(const QPolygonF* itPolygon, m_cachedFillPolygons) { - painter->drawPolygon( *itPolygon ); + QVector fillPolygons = painter->createFillPolygons( m_cachedOuterPolygons, + m_cachedInnerPolygons ); + + foreach( QPolygonF* fillPolygon, fillPolygons ) { + painter->drawPolygon(*fillPolygon); } - } } painter->restore(); } void BuildingGeoPolygonGraphicsItem::screenPolygons(const ViewportParams *viewport, const GeoDataPolygon *polygon, QVector &innerPolygons, - QVector &outlines, - QVector &fill + QVector &outerPolygons ) { Q_ASSERT(polygon); - QVector outerPolygons; viewport->screenCoordinates( polygon->outerBoundary(), outerPolygons ); - viewport->screenCoordinates( *unrolledRing(), fill ); - - outlines << outerPolygons; QVector innerBoundaries = polygon->innerBoundaries(); foreach (const GeoDataLinearRing &innerBoundary, innerBoundaries) { QVector innerPolygonsPerBoundary; viewport->screenCoordinates(innerBoundary, innerPolygonsPerBoundary); - outlines << innerPolygonsPerBoundary; innerPolygons.reserve(innerPolygons.size() + innerPolygonsPerBoundary.size()); foreach( QPolygonF* innerPolygonPerBoundary, innerPolygonsPerBoundary ) { innerPolygons << innerPolygonPerBoundary; } } } void BuildingGeoPolygonGraphicsItem::configureFramePainter(GeoPainter *painter) const { GeoDataStyle::ConstPtr style = this->style(); if (!style) { painter->setPen( QPen() ); } else { const GeoDataPolyStyle& polyStyle = style->polyStyle(); const QColor transparentColor(Qt::transparent); painter->setPen(Qt::NoPen); if (!polyStyle.fill()) { painter->setBrush(transparentColor); } else { const QColor paintedColor = polyStyle.paintedColor(); painter->setBrush(paintedColor.darker(150)); } } } } diff --git a/src/lib/marble/geodata/graphicsitem/BuildingGeoPolygonGraphicsItem.h b/src/lib/marble/geodata/graphicsitem/BuildingGeoPolygonGraphicsItem.h index cff769967..a2a878dd9 100644 --- a/src/lib/marble/geodata/graphicsitem/BuildingGeoPolygonGraphicsItem.h +++ b/src/lib/marble/geodata/graphicsitem/BuildingGeoPolygonGraphicsItem.h @@ -1,73 +1,70 @@ // // 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 // #ifndef MARBLE_BUILDINGGEOPOLYGONGRAPHICSITEM_H #define MARBLE_BUILDINGGEOPOLYGONGRAPHICSITEM_H #include "AbstractGeoPolygonGraphicsItem.h" #include "GeoDataCoordinates.h" class QPointF; namespace Marble { class MARBLE_EXPORT BuildingGeoPolygonGraphicsItem : public AbstractGeoPolygonGraphicsItem { public: explicit BuildingGeoPolygonGraphicsItem(const GeoDataPlacemark *placemark, const GeoDataPolygon *polygon); explicit BuildingGeoPolygonGraphicsItem(const GeoDataPlacemark *placemark, const GeoDataLinearRing *ring); public: virtual void paint(GeoPainter* painter, const ViewportParams *viewport, const QString &layer); private: struct NamedEntry { GeoDataCoordinates point; QString label; }; void paintFrame(GeoPainter* painter, const ViewportParams *viewport); void paintRoof(GeoPainter* painter, const ViewportParams *viewport); void configureFramePainter(GeoPainter *painter) const; void initializeBuildingPainting(const GeoPainter* painter, const ViewportParams *viewport, bool &drawAccurate3D, bool &isCameraAboveBuilding) const; void updatePolygons( const ViewportParams *viewport, QVector& outlinePolygons, QVector& innerPolygons, - QVector &fill, bool &hasInnerBoundaries); QPointF buildingOffset(const QPointF &point, const ViewportParams *viewport, bool* isCameraAboveBuilding = nullptr) const; static QPointF centroid(const QPolygonF &polygon, double &area); void screenPolygons(const ViewportParams *viewport, const GeoDataPolygon* polygon, QVector &polygons, - QVector &outlines, - QVector &fill + QVector &outlines ); static double extractBuildingHeight(const GeoDataPlacemark &placemark); static QString extractBuildingLabel(const GeoDataPlacemark &placemark); static QVector extractNamedEntries(const GeoDataPlacemark &placemark); private: const double m_buildingHeight; const QString m_buildingLabel; const QVector m_entries; - QVector m_cachedOutlinePolygons; + QVector m_cachedOuterPolygons; QVector m_cachedInnerPolygons; - QVector m_cachedFillPolygons; bool m_hasInnerBoundaries; }; } #endif