diff --git a/src/lib/marble/BatchedPlacemarkRenderer.cpp b/src/lib/marble/BatchedPlacemarkRenderer.cpp index 398e51c83..7fd92d773 100644 --- a/src/lib/marble/BatchedPlacemarkRenderer.cpp +++ b/src/lib/marble/BatchedPlacemarkRenderer.cpp @@ -1,95 +1,102 @@ // // 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 2016 Torsten Rahn // #include "BatchedPlacemarkRenderer.h" #include "GeoPainter.h" #include namespace Marble { BatchedPlacemarkRenderer:: BatchedPlacemarkRenderer(GeoPainter * painter) : m_painter(painter) { m_pixmapCache.setCacheLimit(4096); } BatchedPlacemarkRenderer::~BatchedPlacemarkRenderer() { } void BatchedPlacemarkRenderer::addTextFragment( const QPoint& position, const QString& text, - const QColor& color, QFlags flags ) + const qreal fontSize, const QColor& color, + const QFlags & flags ) { TextFragment fragment; fragment.text = text; fragment.position = position; + fragment.fontSize = fontSize; fragment.color = color; fragment.flags = flags; m_textFragments.append(fragment); } void BatchedPlacemarkRenderer::clearTextFragments() { m_textFragments.clear(); } void BatchedPlacemarkRenderer::drawTextFragments() { QPixmap pixmap(10,10); QPainter textPainter; textPainter.begin(&pixmap); QFontMetrics metrics = textPainter.fontMetrics(); textPainter.end(); for (int i = 0; i < m_textFragments.size(); ++i) { QString key = m_textFragments[i].text + ":" + QString::number( static_cast(m_textFragments[i].flags) ); if (!m_pixmapCache.find(key, &pixmap)) { bool hasRoundFrame = m_textFragments[i].flags.testFlag(RoundFrame); int width = metrics.width(m_textFragments[i].text); int height = metrics.height(); QSize size = hasRoundFrame ? QSize(qMax(1.2*width, 1.1*height), 1.2*height) : QSize(width, height); pixmap = QPixmap(size); pixmap.fill(Qt::transparent); QRect labelRect(QPoint(), size); textPainter.begin(&pixmap); + QFont textFont = textPainter.font(); + textFont.setPointSize(m_textFragments[i].fontSize); + textPainter.setFont(textFont); textPainter.setRenderHint(QPainter::Antialiasing, true); QColor const brushColor = m_textFragments[i].color; if (hasRoundFrame) { QColor lighterColor = brushColor.lighter(110); lighterColor.setAlphaF(0.9); textPainter.setBrush(lighterColor); textPainter.drawRoundedRect(labelRect, 3, 3); } textPainter.setBrush(brushColor); textPainter.drawText(labelRect, Qt::AlignHCenter , m_textFragments[i].text); if (hasRoundFrame) { textPainter.setBrush(brushColor); } textPainter.end(); m_pixmapCache.insert(key, pixmap); } - m_painter->drawPixmap(m_textFragments[i].position, pixmap); + m_painter->drawPixmap(m_textFragments[i].position.x() - pixmap.width()/2, + m_textFragments[i].position.y() - pixmap.height()/2, + pixmap); } } } diff --git a/src/lib/marble/BatchedPlacemarkRenderer.h b/src/lib/marble/BatchedPlacemarkRenderer.h index 5739d5bc0..c3d56eca0 100644 --- a/src/lib/marble/BatchedPlacemarkRenderer.h +++ b/src/lib/marble/BatchedPlacemarkRenderer.h @@ -1,68 +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 2016 Torsten Rahn // #ifndef MARBLE_BATCHEDPLACEMARKRENDERER_H #define MARBLE_BATCHEDPLACEMARKRENDERER_H #include "marble_export.h" #include namespace Marble { class GeoPainter; struct TextFragment; /** * @short This class renders placemarks where the positioning * criteria differ from those inside the default Placemark * layer / layout classes (e.g. house numbers). * */ class MARBLE_EXPORT BatchedPlacemarkRenderer { public: enum Frame { NoOptions = 0x0, RoundFrame = 0x1 }; Q_DECLARE_FLAGS(Frames, Frame) BatchedPlacemarkRenderer(GeoPainter * painter); ~BatchedPlacemarkRenderer(); void addTextFragment( const QPoint& targetPosition, const QString& text, - const QColor& color, QFlags flags ); + const qreal fontSize, const QColor& color, + const QFlags & flags ); void clearTextFragments(); void drawTextFragments(); private: QVector m_textFragments; QPixmapCache m_pixmapCache; GeoPainter * m_painter; }; struct TextFragment { QString text; QPoint position; + qreal fontSize; QColor color; QFlags flags; }; } #endif diff --git a/src/lib/marble/GeoPainter.cpp b/src/lib/marble/GeoPainter.cpp index 0a9b81aaf..9987aa625 100644 --- a/src/lib/marble/GeoPainter.cpp +++ b/src/lib/marble/GeoPainter.cpp @@ -1,1034 +1,1035 @@ // // 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_batchedPlacemarkRenderer(q), 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 currentPen = pen(); bool const hasInnerBoundaries = !polygon.innerBoundaries().isEmpty(); bool innerBoundariesOnScreen = false; if ( hasInnerBoundaries ) { QVector const & 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()), 4 ) { innerBoundariesOnScreen = true; break; } } if (innerBoundariesOnScreen) { // Create the inner screen polygons foreach( const GeoDataLinearRing& itInnerBoundary, innerBoundaries ) { QVector innerPolygonsPerBoundary; d->m_viewport->screenCoordinates( itInnerBoundary, innerPolygonsPerBoundary ); foreach( QPolygonF* innerPolygonPerBoundary, innerPolygonsPerBoundary ) { innerPolygons << innerPolygonPerBoundary; } } setPen(Qt::NoPen); QVector fillPolygons = createFillPolygons( outerPolygons, innerPolygons ); foreach( const QPolygonF* fillPolygon, fillPolygons ) { ClipPainter::drawPolygon(*fillPolygon, fillRule); } setPen(currentPen); foreach( const QPolygonF* outerPolygon, outerPolygons ) { ClipPainter::drawPolyline( *outerPolygon ); } foreach( const QPolygonF* innerPolygon, innerPolygons ) { ClipPainter::drawPolyline( *innerPolygon ); } qDeleteAll(fillPolygons); } } 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); } } } void GeoPainter::addTextFragment( const QPoint& position, const QString& text, - const QColor& color, QFlags flags ) + const qreal fontSize , const QColor& color, + const QFlags & flags ) { - d->m_batchedPlacemarkRenderer.addTextFragment( position, text, color, flags); + d->m_batchedPlacemarkRenderer.addTextFragment( position, text, fontSize, color, flags); } void GeoPainter::clearTextFragments() { d->m_batchedPlacemarkRenderer.clearTextFragments(); } void GeoPainter::drawTextFragments() { d->m_batchedPlacemarkRenderer.drawTextFragments(); } diff --git a/src/lib/marble/GeoPainter.h b/src/lib/marble/GeoPainter.h index 8e3a453b5..510fe72e6 100644 --- a/src/lib/marble/GeoPainter.h +++ b/src/lib/marble/GeoPainter.h @@ -1,526 +1,527 @@ // // 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 "BatchedPlacemarkRenderer.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); void addTextFragment( const QPoint& position, const QString& text, - const QColor& color = Qt::black, QFlags flags = 0 ); + const qreal fontSize, const QColor& color = Qt::black, + const QFlags & flags = 0 ); void clearTextFragments(); void drawTextFragments(); // 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/graphicsitem/BuildingGeoPolygonGraphicsItem.cpp b/src/lib/marble/geodata/graphicsitem/BuildingGeoPolygonGraphicsItem.cpp index 55d21058e..fcfffed83 100644 --- a/src/lib/marble/geodata/graphicsitem/BuildingGeoPolygonGraphicsItem.cpp +++ b/src/lib/marble/geodata/graphicsitem/BuildingGeoPolygonGraphicsItem.cpp @@ -1,536 +1,542 @@ // // 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_buildingText(extractBuildingLabel(*placemark)) , m_entries(extractNamedEntries(*placemark)) , m_hasInnerBoundaries(false) { setZValue(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_buildingText(extractBuildingLabel(*placemark)) , m_entries(extractNamedEntries(*placemark)) { setZValue(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& outerPolygons, QVector& innerPolygons, 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, outerPolygons); } else { viewport->screenCoordinates(polygon()->outerBoundary(), outerPolygons); } } else if (ring()) { 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, int tileZoomLevel) { // Just display flat buildings for tile level 17 if (tileZoomLevel == 17) { setZValue(0.0); if (layer.endsWith(QLatin1String("/frame"))) { AbstractGeoPolygonGraphicsItem::paint(painter, viewport, layer, tileZoomLevel ); } return; } setZValue(m_buildingHeight); // For level 18, 19 .. render 3D buildings in perspective if (layer.endsWith(QLatin1String("/frame"))) { Q_ASSERT(m_cachedOuterPolygons.isEmpty()); Q_ASSERT(m_cachedInnerPolygons.isEmpty()); Q_ASSERT(m_cachedOuterRoofPolygons.isEmpty()); Q_ASSERT(m_cachedInnerRoofPolygons.isEmpty()); updatePolygons(viewport, m_cachedOuterPolygons, m_cachedInnerPolygons, m_hasInnerBoundaries); if (m_cachedOuterPolygons.isEmpty()) { return; } paintFrame(painter, viewport); } else if (layer.endsWith(QLatin1String("/roof"))) { if (m_cachedOuterPolygons.isEmpty()) { return; } paintRoof(painter, viewport); qDeleteAll(m_cachedOuterPolygons); qDeleteAll(m_cachedOuterRoofPolygons); qDeleteAll(m_cachedInnerRoofPolygons); m_cachedOuterPolygons.clear(); m_cachedInnerPolygons.clear(); m_cachedOuterRoofPolygons.clear(); m_cachedInnerRoofPolygons.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 } bool isValid = true; if (s_previousStyle != reinterpret_cast(style().data())) { isValid = configurePainter(painter, viewport); + + QFont font = painter->font(); // TODO: better font configuration + if (font.pointSize() != 10) { + font.setPointSize( 10 ); + painter->setFont(font); + } } s_previousStyle = reinterpret_cast(style().data()); if (!isValid) return; qreal maxSize(0.0); QPointF roofCenter; // first paint the area (and the outline if there are no inner boundaries) double maxArea = 0.0; if ( drawAccurate3D) { if (m_hasInnerBoundaries) { QPen const currentPen = painter->pen(); painter->setPen(Qt::NoPen); QVector fillPolygons = painter->createFillPolygons( m_cachedOuterRoofPolygons, m_cachedInnerRoofPolygons ); foreach( const QPolygonF* fillPolygon, fillPolygons ) { painter->drawPolygon(*fillPolygon); } painter->setPen(currentPen); foreach( const QPolygonF* outerRoof, m_cachedOuterRoofPolygons ) { painter->drawPolyline( *outerRoof ); } foreach( const QPolygonF* innerRoof, m_cachedInnerRoofPolygons ) { painter->drawPolyline( *innerRoof ); } } else { foreach( const QPolygonF* outerRoof, m_cachedOuterRoofPolygons ) { painter->drawPolygon( *outerRoof ); } } } else { QPointF const offset = buildingOffset(m_cachedOuterPolygons[0]->boundingRect().center(), viewport); painter->translate(offset); if (m_hasInnerBoundaries) { QPen const currentPen = painter->pen(); painter->setPen(Qt::NoPen); QVector fillPolygons = painter->createFillPolygons( m_cachedOuterPolygons, m_cachedInnerPolygons ); foreach( const QPolygonF* fillPolygon, fillPolygons ) { painter->drawPolygon(*fillPolygon); } painter->setPen(currentPen); foreach( const QPolygonF* outerPolygon, m_cachedOuterPolygons ) { painter->drawPolyline( *outerPolygon ); } foreach( const QPolygonF* innerPolygon, m_cachedInnerPolygons ) { painter->drawPolyline( *innerPolygon ); } } else { foreach( const QPolygonF* outerPolygon, m_cachedOuterPolygons ) { painter->drawPolygon( *outerPolygon ); } } painter->translate(-offset); } for (int i = 0; i < m_cachedOuterPolygons.size(); ++i) { QPolygonF* outerPolygon = m_cachedOuterPolygons[i]; QRectF const boundingRect = outerPolygon->boundingRect(); // Label position calculation if (!m_buildingText.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); } } // Draw the housenumber labels if (drawAccurate3D && !m_buildingText.isEmpty() && !roofCenter.isNull() && !m_cachedOuterRoofPolygons.isEmpty()) { QPolygonF * outerRoof = m_cachedOuterRoofPolygons[i]; double const w2 = 0.5 * painter->fontMetrics().width(m_buildingText); 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 (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->addTextFragment((textPosition + QPointF(0, -2-ascent)).toPoint(), - m_buildingText, painter->brush().color()); + painter->addTextFragment(roofCenter.toPoint(), m_buildingText, + painter->font().pointSize(), painter->brush().color()); } } ++i; } // Render additional housenumbers at building entries if (!m_entries.isEmpty() && maxArea > 1600 * m_entries.size()) { foreach(const auto &entry, m_entries) { qreal x, y; viewport->screenCoordinates(entry.point, x, y); QPointF point(x, y); point += buildingOffset(point, viewport); painter->addTextFragment(point.toPoint(), - m_buildingText, painter->brush().color(), + m_buildingText, painter->font().pointSize(), painter->brush().color(), QFlags() |= BatchedPlacemarkRenderer::RoundFrame); } } } 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; } bool drawAccurate3D; bool isCameraAboveBuilding; initializeBuildingPainting(painter, viewport, drawAccurate3D, isCameraAboveBuilding); bool isValid = true; if (s_previousStyle != reinterpret_cast(style().data())) { isValid = configurePainterForFrame(painter); } s_previousStyle = reinterpret_cast(style().data()); if (!isValid) return; if ( drawAccurate3D && isCameraAboveBuilding ) { foreach(QPolygonF* outline, m_cachedOuterPolygons) { if (outline->isEmpty()) { continue; } // draw the building sides int const size = outline->size(); QPolygonF * outerRoof = new QPolygonF; outerRoof->reserve(outline->size()); QPointF & a = (*outline)[0]; QPointF shiftA = a + buildingOffset(a, viewport); outerRoof->append(shiftA); for (int i=1; i= 0; if (!backface) { QPolygonF buildingSide; buildingSide.reserve(4); buildingSide << a << shiftA << shiftB << b; painter->drawPolygon(buildingSide); } a = b; shiftA = shiftB; outerRoof->append(shiftA); } m_cachedOuterRoofPolygons.append(outerRoof); } foreach(QPolygonF* outline, m_cachedInnerPolygons) { if (outline->isEmpty()) { continue; } // draw the building sides int const size = outline->size(); QPolygonF * innerRoof = new QPolygonF; innerRoof->reserve(outline->size()); QPointF & a = (*outline)[0]; QPointF shiftA = a + buildingOffset(a, viewport); innerRoof->append(shiftA); for (int i=1; i= 0; if (backface) { QPolygonF buildingSide; buildingSide.reserve(4); buildingSide << a << shiftA << shiftB << b; painter->drawPolygon(buildingSide); } a = b; shiftA = shiftB; innerRoof->append(shiftA); } m_cachedInnerRoofPolygons.append(innerRoof); } } else { // don't draw the building sides - just draw the base frame instead QVector fillPolygons = painter->createFillPolygons( m_cachedOuterPolygons, m_cachedInnerPolygons ); foreach( QPolygonF* fillPolygon, fillPolygons ) { painter->drawPolygon(*fillPolygon); } } } void BuildingGeoPolygonGraphicsItem::screenPolygons(const ViewportParams *viewport, const GeoDataPolygon *polygon, QVector &innerPolygons, QVector &outerPolygons ) { Q_ASSERT(polygon); viewport->screenCoordinates( polygon->outerBoundary(), outerPolygons ); QVector const & innerBoundaries = polygon->innerBoundaries(); foreach (const GeoDataLinearRing &innerBoundary, innerBoundaries) { QVector innerPolygonsPerBoundary; viewport->screenCoordinates(innerBoundary, innerPolygonsPerBoundary); innerPolygons.reserve(innerPolygons.size() + innerPolygonsPerBoundary.size()); foreach( QPolygonF* innerPolygonPerBoundary, innerPolygonsPerBoundary ) { innerPolygons << innerPolygonPerBoundary; } } } bool BuildingGeoPolygonGraphicsItem::configurePainterForFrame(GeoPainter *painter) const { QPen currentPen = painter->pen(); GeoDataStyle::ConstPtr style = this->style(); if (!style) { painter->setPen( QPen() ); } else { const GeoDataPolyStyle& polyStyle = style->polyStyle(); if (currentPen.style() != Qt::NoPen) { painter->setPen(Qt::NoPen); } if (!polyStyle.fill()) { return false; } else { const QColor paintedColor = polyStyle.paintedColor().darker(150); if (painter->brush().color() != paintedColor) { painter->setBrush(paintedColor); } } } return true; } }