/*************************************************************************** qgscomposerarrow.cpp ---------------------- begin : November 2009 copyright : (C) 2009 by Marco Hugentobler email : marco@hugis.net ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "qgscomposerarrow.h" #include "qgscomposition.h" #include "qgscomposerutils.h" #include "qgssymbollayerutils.h" #include "qgssvgcache.h" #include "qgsmapsettings.h" #include #include #include #include QgsComposerArrow::QgsComposerArrow( QgsComposition* c ) : QgsComposerItem( c ) , mStartPoint( 0, 0 ) , mStopPoint( 0, 0 ) , mStartXIdx( 0 ) , mStartYIdx( 0 ) , mMarkerMode( DefaultMarker ) , mArrowHeadOutlineWidth( 1.0 ) , mArrowHeadOutlineColor( Qt::black ) , mArrowHeadFillColor( Qt::black ) , mBoundsBehavior( 24 ) , mLineSymbol( nullptr ) { init(); } QgsComposerArrow::QgsComposerArrow( QPointF startPoint, QPointF stopPoint, QgsComposition* c ) : QgsComposerItem( c ) , mStartPoint( startPoint ) , mStopPoint( stopPoint ) , mMarkerMode( DefaultMarker ) , mArrowHeadOutlineWidth( 1.0 ) , mArrowHeadOutlineColor( Qt::black ) , mArrowHeadFillColor( Qt::black ) , mBoundsBehavior( 24 ) , mLineSymbol( nullptr ) { mStartXIdx = mStopPoint.x() < mStartPoint.x(); mStartYIdx = mStopPoint.y() < mStartPoint.y(); init(); adaptItemSceneRect(); } QgsComposerArrow::~QgsComposerArrow() { delete mLineSymbol; } void QgsComposerArrow::init() { setArrowHeadWidth( 4 ); mPen.setColor( mArrowHeadOutlineColor ); mPen.setWidthF( 1 ); mBrush.setColor( mArrowHeadFillColor ); createDefaultLineSymbol(); //default to no background setBackgroundEnabled( false ); } void QgsComposerArrow::createDefaultLineSymbol() { delete mLineSymbol; QgsStringMap properties; properties.insert( QStringLiteral( "color" ), QStringLiteral( "0,0,0,255" ) ); properties.insert( QStringLiteral( "width" ), QStringLiteral( "1" ) ); properties.insert( QStringLiteral( "capstyle" ), QStringLiteral( "square" ) ); mLineSymbol = QgsLineSymbol::createSimple( properties ); } void QgsComposerArrow::paint( QPainter* painter, const QStyleOptionGraphicsItem *itemStyle, QWidget *pWidget ) { Q_UNUSED( itemStyle ); Q_UNUSED( pWidget ); if ( !painter || !painter->device() ) { return; } if ( !shouldDrawItem() ) { return; } drawBackground( painter ); painter->save(); //antialiasing on painter->setRenderHint( QPainter::Antialiasing, true ); //draw line section drawLine( painter ); //draw arrowhead if required if ( mMarkerMode != NoMarker ) { painter->setBrush( mBrush ); painter->setPen( mPen ); if ( mMarkerMode == DefaultMarker ) { drawHardcodedMarker( painter, EndMarker ); } else if ( mMarkerMode == SVGMarker ) { drawSVGMarker( painter, StartMarker, mStartMarkerFile ); drawSVGMarker( painter, EndMarker, mEndMarkerFile ); } } painter->restore(); drawFrame( painter ); if ( isSelected() ) { drawSelectionBoxes( painter ); } } void QgsComposerArrow::setSceneRect( const QRectF& rectangle ) { //update rect for data defined size and position QRectF evaluatedRect = evalItemRect( rectangle ); if ( evaluatedRect.width() < 0 ) { mStartXIdx = 1 - mStartXIdx; } if ( evaluatedRect.height() < 0 ) { mStartYIdx = 1 - mStartYIdx; } double margin = computeMarkerMargin(); // Ensure the rectangle is at least as large as needed to include the markers QRectF rect = rectangle.united( QRectF( evaluatedRect.x(), evaluatedRect.y(), 2. * margin, 2. * margin ) ); // Compute new start and stop positions double x[2] = {rect.x(), rect.x() + rect.width()}; double y[2] = {rect.y(), rect.y() + rect.height()}; double xsign = x[mStartXIdx] < x[1 - mStartXIdx] ? 1.0 : -1.0; double ysign = y[mStartYIdx] < y[1 - mStartYIdx] ? 1.0 : -1.0; mStartPoint = QPointF( x[mStartXIdx] + xsign * margin, y[mStartYIdx] + ysign * margin ); mStopPoint = QPointF( x[1 - mStartXIdx] - xsign * margin, y[1 - mStartYIdx] - ysign * margin ); QgsComposerItem::setSceneRect( rect ); } void QgsComposerArrow::drawLine( QPainter *painter ) { if ( ! mLineSymbol || ! mComposition ) { return; } QPaintDevice* paintDevice = painter->device(); painter->save(); //setup painter scaling to dots so that raster symbology is drawn to scale double dotsPerMM = paintDevice->logicalDpiX() / 25.4; painter->scale( 1 / dotsPerMM, 1 / dotsPerMM ); //scale painter from mm to dots //setup render context QgsRenderContext context = QgsComposerUtils::createRenderContextForComposition( mComposition, painter ); context.setForceVectorOutput( true ); QgsExpressionContext expressionContext = createExpressionContext(); context.setExpressionContext( expressionContext ); //line scaled to dots QPolygonF line; line << QPointF( mStartPoint.x() - pos().x(), mStartPoint.y() - pos().y() ) * dotsPerMM << QPointF( mStopPoint.x() - pos().x(), mStopPoint.y() - pos().y() ) * dotsPerMM; mLineSymbol->startRender( context ); mLineSymbol->renderPolyline( line, nullptr, context ); mLineSymbol->stopRender( context ); painter->restore(); } void QgsComposerArrow::drawHardcodedMarker( QPainter *p, MarkerType type ) { Q_UNUSED( type ); if ( mBoundsBehavior == 22 ) { //if arrow was created in versions prior to 2.4, use the old rendering style QgsComposerUtils::drawArrowHead( p, mStopPoint.x() - pos().x(), mStopPoint.y() - pos().y(), QgsComposerUtils::angle( mStartPoint, mStopPoint ), mArrowHeadWidth ); } else { QVector2D dir = QVector2D( mStopPoint - mStartPoint ).normalized(); QPointF stop = mStopPoint + ( dir * 0.5 * mArrowHeadWidth ).toPointF(); QgsComposerUtils::drawArrowHead( p, stop.x() - pos().x(), stop.y() - pos().y(), QgsComposerUtils::angle( mStartPoint, stop ), mArrowHeadWidth ); } } void QgsComposerArrow::drawSVGMarker( QPainter* p, MarkerType type, const QString &markerPath ) { Q_UNUSED( markerPath ); double angle = QgsComposerUtils::angle( mStartPoint, mStopPoint ); double arrowHeadHeight; if ( type == StartMarker ) { arrowHeadHeight = mStartArrowHeadHeight; } else { arrowHeadHeight = mStopArrowHeadHeight; } if ( mArrowHeadWidth <= 0 || arrowHeadHeight <= 0 ) { //bad image size return; } QPointF imageFixPoint; imageFixPoint.setX( mArrowHeadWidth / 2.0 ); QPointF canvasPoint; if ( type == StartMarker ) { canvasPoint = QPointF( mStartPoint.x() - pos().x(), mStartPoint.y() - pos().y() ); imageFixPoint.setY( mStartArrowHeadHeight ); } else //end marker { canvasPoint = QPointF( mStopPoint.x() - pos().x(), mStopPoint.y() - pos().y() ); imageFixPoint.setY( 0 ); } QString svgFileName = ( type == StartMarker ? mStartMarkerFile : mEndMarkerFile ); if ( svgFileName.isEmpty() ) return; QSvgRenderer r; const QByteArray &svgContent = QgsApplication::svgCache()->svgContent( svgFileName, mArrowHeadWidth, mArrowHeadFillColor, mArrowHeadOutlineColor, mArrowHeadOutlineWidth, 1.0 ); r.load( svgContent ); p->save(); p->setRenderHint( QPainter::Antialiasing ); if ( mBoundsBehavior == 22 ) { //if arrow was created in versions prior to 2.4, use the old rendering style //rotate image fix point for backtransform QPointF fixPoint; if ( type == StartMarker ) { fixPoint.setX( 0 ); fixPoint.setY( arrowHeadHeight / 2.0 ); } else { fixPoint.setX( 0 ); fixPoint.setY( -arrowHeadHeight / 2.0 ); } QPointF rotatedFixPoint; double angleRad = angle / 180 * M_PI; rotatedFixPoint.setX( fixPoint.x() * cos( angleRad ) + fixPoint.y() * -sin( angleRad ) ); rotatedFixPoint.setY( fixPoint.x() * sin( angleRad ) + fixPoint.y() * cos( angleRad ) ); p->translate( canvasPoint.x() - rotatedFixPoint.x(), canvasPoint.y() - rotatedFixPoint.y() ); } else { p->translate( canvasPoint.x(), canvasPoint.y() ); } p->rotate( angle ); p->translate( -mArrowHeadWidth / 2.0, -arrowHeadHeight / 2.0 ); r.render( p, QRectF( 0, 0, mArrowHeadWidth, arrowHeadHeight ) ); p->restore(); } void QgsComposerArrow::setStartMarker( const QString& svgPath ) { QSvgRenderer r; mStartMarkerFile = svgPath; if ( svgPath.isEmpty() || !r.load( svgPath ) ) { mStartArrowHeadHeight = 0; } else { //calculate mArrowHeadHeight from svg file and mArrowHeadWidth QRect viewBox = r.viewBox(); mStartArrowHeadHeight = mArrowHeadWidth / viewBox.width() * viewBox.height(); } adaptItemSceneRect(); } void QgsComposerArrow::setEndMarker( const QString& svgPath ) { QSvgRenderer r; mEndMarkerFile = svgPath; if ( svgPath.isEmpty() || !r.load( svgPath ) ) { mStopArrowHeadHeight = 0; } else { //calculate mArrowHeadHeight from svg file and mArrowHeadWidth QRect viewBox = r.viewBox(); mStopArrowHeadHeight = mArrowHeadWidth / viewBox.width() * viewBox.height(); } adaptItemSceneRect(); } void QgsComposerArrow::setArrowHeadOutlineColor( const QColor &color ) { mArrowHeadOutlineColor = color; mPen.setColor( color ); } void QgsComposerArrow::setArrowHeadFillColor( const QColor &color ) { mArrowHeadFillColor = color; mBrush.setColor( color ); } void QgsComposerArrow::setArrowHeadOutlineWidth( const double width ) { mArrowHeadOutlineWidth = width; mPen.setWidthF( mArrowHeadOutlineWidth ); adaptItemSceneRect(); } void QgsComposerArrow::setLineSymbol( QgsLineSymbol *symbol ) { delete mLineSymbol; mLineSymbol = symbol; } void QgsComposerArrow::setArrowHeadWidth( double width ) { mArrowHeadWidth = width; setStartMarker( mStartMarkerFile ); setEndMarker( mEndMarkerFile ); adaptItemSceneRect(); } double QgsComposerArrow::computeMarkerMargin() const { double margin = 0; if ( mBoundsBehavior == 22 ) { //if arrow was created in versions prior to 2.4, use the old rendering style if ( mMarkerMode == DefaultMarker ) { margin = mPen.widthF() / 2.0 + mArrowHeadWidth / 2.0; } else if ( mMarkerMode == NoMarker ) { margin = mPen.widthF() / 2.0; } else if ( mMarkerMode == SVGMarker ) { double maxArrowHeight = qMax( mStartArrowHeadHeight, mStopArrowHeadHeight ); margin = mPen.widthF() / 2 + qMax( mArrowHeadWidth / 2.0, maxArrowHeight / 2.0 ); } } else { if ( mMarkerMode == DefaultMarker ) { margin = mPen.widthF() / std::sqrt( 2.0 ) + mArrowHeadWidth / 2.0; } else if ( mMarkerMode == NoMarker ) { margin = mPen.widthF() / std::sqrt( 2.0 ); } else if ( mMarkerMode == SVGMarker ) { double startMarkerMargin = std::sqrt( 0.25 * ( mStartArrowHeadHeight * mStartArrowHeadHeight + mArrowHeadWidth * mArrowHeadWidth ) ); double stopMarkerMargin = std::sqrt( 0.25 * ( mStopArrowHeadHeight * mStopArrowHeadHeight + mArrowHeadWidth * mArrowHeadWidth ) ); double markerMargin = qMax( startMarkerMargin, stopMarkerMargin ); margin = qMax( mPen.widthF() / std::sqrt( 2.0 ), markerMargin ); } } return margin; } void QgsComposerArrow::adaptItemSceneRect() { //rectangle containing start and end point QRectF rect = QRectF( qMin( mStartPoint.x(), mStopPoint.x() ), qMin( mStartPoint.y(), mStopPoint.y() ), qAbs( mStopPoint.x() - mStartPoint.x() ), qAbs( mStopPoint.y() - mStartPoint.y() ) ); double enlarge = computeMarkerMargin(); rect.adjust( -enlarge, -enlarge, enlarge, enlarge ); QgsComposerItem::setSceneRect( rect ); } void QgsComposerArrow::setMarkerMode( MarkerMode mode ) { mMarkerMode = mode; adaptItemSceneRect(); } bool QgsComposerArrow::writeXml( QDomElement& elem, QDomDocument & doc ) const { QDomElement composerArrowElem = doc.createElement( QStringLiteral( "ComposerArrow" ) ); composerArrowElem.setAttribute( QStringLiteral( "arrowHeadWidth" ), QString::number( mArrowHeadWidth ) ); composerArrowElem.setAttribute( QStringLiteral( "arrowHeadFillColor" ), QgsSymbolLayerUtils::encodeColor( mArrowHeadFillColor ) ); composerArrowElem.setAttribute( QStringLiteral( "arrowHeadOutlineColor" ), QgsSymbolLayerUtils::encodeColor( mArrowHeadOutlineColor ) ); composerArrowElem.setAttribute( QStringLiteral( "outlineWidth" ), QString::number( mArrowHeadOutlineWidth ) ); composerArrowElem.setAttribute( QStringLiteral( "markerMode" ), mMarkerMode ); composerArrowElem.setAttribute( QStringLiteral( "startMarkerFile" ), mStartMarkerFile ); composerArrowElem.setAttribute( QStringLiteral( "endMarkerFile" ), mEndMarkerFile ); composerArrowElem.setAttribute( QStringLiteral( "boundsBehaviorVersion" ), QString::number( mBoundsBehavior ) ); QDomElement styleElem = doc.createElement( QStringLiteral( "lineStyle" ) ); QDomElement lineStyleElem = QgsSymbolLayerUtils::saveSymbol( QString(), mLineSymbol, doc ); styleElem.appendChild( lineStyleElem ); composerArrowElem.appendChild( styleElem ); //start point QDomElement startPointElem = doc.createElement( QStringLiteral( "StartPoint" ) ); startPointElem.setAttribute( QStringLiteral( "x" ), QString::number( mStartPoint.x() ) ); startPointElem.setAttribute( QStringLiteral( "y" ), QString::number( mStartPoint.y() ) ); composerArrowElem.appendChild( startPointElem ); //stop point QDomElement stopPointElem = doc.createElement( QStringLiteral( "StopPoint" ) ); stopPointElem.setAttribute( QStringLiteral( "x" ), QString::number( mStopPoint.x() ) ); stopPointElem.setAttribute( QStringLiteral( "y" ), QString::number( mStopPoint.y() ) ); composerArrowElem.appendChild( stopPointElem ); elem.appendChild( composerArrowElem ); return _writeXml( composerArrowElem, doc ); } bool QgsComposerArrow::readXml( const QDomElement& itemElem, const QDomDocument& doc ) { mArrowHeadWidth = itemElem.attribute( QStringLiteral( "arrowHeadWidth" ), QStringLiteral( "2.0" ) ).toDouble(); mArrowHeadFillColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "arrowHeadFillColor" ), QStringLiteral( "0,0,0,255" ) ) ); mArrowHeadOutlineColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "arrowHeadOutlineColor" ), QStringLiteral( "0,0,0,255" ) ) ); mArrowHeadOutlineWidth = itemElem.attribute( QStringLiteral( "outlineWidth" ), QStringLiteral( "1.0" ) ).toDouble(); setStartMarker( itemElem.attribute( QStringLiteral( "startMarkerFile" ), QLatin1String( "" ) ) ); setEndMarker( itemElem.attribute( QStringLiteral( "endMarkerFile" ), QLatin1String( "" ) ) ); mMarkerMode = QgsComposerArrow::MarkerMode( itemElem.attribute( QStringLiteral( "markerMode" ), QStringLiteral( "0" ) ).toInt() ); //if bounds behavior version is not set, default to 2.2 behavior mBoundsBehavior = itemElem.attribute( QStringLiteral( "boundsBehaviorVersion" ), QStringLiteral( "22" ) ).toInt(); //arrow style QDomElement styleElem = itemElem.firstChildElement( QStringLiteral( "lineStyle" ) ); if ( !styleElem.isNull() ) { QDomElement lineStyleElem = styleElem.firstChildElement( QStringLiteral( "symbol" ) ); if ( !lineStyleElem.isNull() ) { delete mLineSymbol; mLineSymbol = QgsSymbolLayerUtils::loadSymbol( lineStyleElem ); } } else { //old project file, read arrow width and color delete mLineSymbol; QgsStringMap properties; properties.insert( QStringLiteral( "width" ), itemElem.attribute( QStringLiteral( "outlineWidth" ), QStringLiteral( "1.0" ) ) ); if ( mBoundsBehavior == 22 ) { //if arrow was created in versions prior to 2.4, use the old rendering style properties.insert( QStringLiteral( "capstyle" ), QStringLiteral( "flat" ) ); } else { properties.insert( QStringLiteral( "capstyle" ), QStringLiteral( "square" ) ); } int red = 0; int blue = 0; int green = 0; int alpha = 255; QDomNodeList arrowColorList = itemElem.elementsByTagName( QStringLiteral( "ArrowColor" ) ); if ( !arrowColorList.isEmpty() ) { QDomElement arrowColorElem = arrowColorList.at( 0 ).toElement(); red = arrowColorElem.attribute( QStringLiteral( "red" ), QStringLiteral( "0" ) ).toInt(); green = arrowColorElem.attribute( QStringLiteral( "green" ), QStringLiteral( "0" ) ).toInt(); blue = arrowColorElem.attribute( QStringLiteral( "blue" ), QStringLiteral( "0" ) ).toInt(); alpha = arrowColorElem.attribute( QStringLiteral( "alpha" ), QStringLiteral( "255" ) ).toInt(); mArrowHeadFillColor = QColor( red, green, blue, alpha ); mArrowHeadOutlineColor = QColor( red, green, blue, alpha ); } properties.insert( QStringLiteral( "color" ), QStringLiteral( "%1,%2,%3,%4" ).arg( red ).arg( green ).arg( blue ).arg( alpha ) ); mLineSymbol = QgsLineSymbol::createSimple( properties ); } mPen.setColor( mArrowHeadOutlineColor ); mPen.setWidthF( mArrowHeadOutlineWidth ); mBrush.setColor( mArrowHeadFillColor ); //restore general composer item properties //needs to be before start point / stop point because setSceneRect() QDomNodeList composerItemList = itemElem.elementsByTagName( QStringLiteral( "ComposerItem" ) ); if ( !composerItemList.isEmpty() ) { QDomElement composerItemElem = composerItemList.at( 0 ).toElement(); _readXml( composerItemElem, doc ); } //start point QDomNodeList startPointList = itemElem.elementsByTagName( QStringLiteral( "StartPoint" ) ); if ( !startPointList.isEmpty() ) { QDomElement startPointElem = startPointList.at( 0 ).toElement(); mStartPoint.setX( startPointElem.attribute( QStringLiteral( "x" ), QStringLiteral( "0.0" ) ).toDouble() ); mStartPoint.setY( startPointElem.attribute( QStringLiteral( "y" ), QStringLiteral( "0.0" ) ).toDouble() ); } //stop point QDomNodeList stopPointList = itemElem.elementsByTagName( QStringLiteral( "StopPoint" ) ); if ( !stopPointList.isEmpty() ) { QDomElement stopPointElem = stopPointList.at( 0 ).toElement(); mStopPoint.setX( stopPointElem.attribute( QStringLiteral( "x" ), QStringLiteral( "0.0" ) ).toDouble() ); mStopPoint.setY( stopPointElem.attribute( QStringLiteral( "y" ), QStringLiteral( "0.0" ) ).toDouble() ); } mStartXIdx = mStopPoint.x() < mStartPoint.x(); mStartYIdx = mStopPoint.y() < mStartPoint.y(); adaptItemSceneRect(); emit itemChanged(); return true; }