mirror of
https://github.com/qgis/QGIS.git
synced 2025-10-17 00:09:36 -04:00
523 lines
17 KiB
C++
523 lines
17 KiB
C++
/***************************************************************************
|
|
qgslayoutitempolyline.cpp
|
|
begin : March 2016
|
|
copyright : (C) 2016 Paul Blottiere, Oslandia
|
|
email : paul dot blottiere at oslandia dot com
|
|
***************************************************************************/
|
|
|
|
/***************************************************************************
|
|
* *
|
|
* 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 "qgslayoutitempolyline.h"
|
|
#include "qgslayoutitemregistry.h"
|
|
#include "qgssymbollayerutils.h"
|
|
#include "qgscolorutils.h"
|
|
#include "qgssymbol.h"
|
|
#include "qgslayout.h"
|
|
#include "qgslayoutrendercontext.h"
|
|
#include "qgslayoututils.h"
|
|
#include "qgsreadwritecontext.h"
|
|
#include "qgssvgcache.h"
|
|
#include "qgsstyleentityvisitor.h"
|
|
#include "qgslinesymbol.h"
|
|
|
|
#include <QSvgRenderer>
|
|
#include <limits>
|
|
#include <QGraphicsPathItem>
|
|
#include <QVector2D>
|
|
|
|
QgsLayoutItemPolyline::QgsLayoutItemPolyline( QgsLayout *layout )
|
|
: QgsLayoutNodesItem( layout )
|
|
{
|
|
createDefaultPolylineStyleSymbol();
|
|
}
|
|
|
|
QgsLayoutItemPolyline::QgsLayoutItemPolyline( const QPolygonF &polyline, QgsLayout *layout )
|
|
: QgsLayoutNodesItem( polyline, layout )
|
|
{
|
|
createDefaultPolylineStyleSymbol();
|
|
}
|
|
|
|
QgsLayoutItemPolyline::~QgsLayoutItemPolyline() = default;
|
|
|
|
QgsLayoutItemPolyline *QgsLayoutItemPolyline::create( QgsLayout *layout )
|
|
{
|
|
return new QgsLayoutItemPolyline( layout );
|
|
}
|
|
|
|
int QgsLayoutItemPolyline::type() const
|
|
{
|
|
return QgsLayoutItemRegistry::LayoutPolyline;
|
|
}
|
|
|
|
QIcon QgsLayoutItemPolyline::icon() const
|
|
{
|
|
return QgsApplication::getThemeIcon( QStringLiteral( "/mLayoutItemPolyline.svg" ) );
|
|
}
|
|
|
|
bool QgsLayoutItemPolyline::_addNode( const int indexPoint,
|
|
QPointF newPoint,
|
|
const double radius )
|
|
{
|
|
const double distStart = computeDistance( newPoint, mPolygon[0] );
|
|
const double distEnd = computeDistance( newPoint, mPolygon[mPolygon.size() - 1] );
|
|
|
|
if ( indexPoint == ( mPolygon.size() - 1 ) )
|
|
{
|
|
if ( distEnd < radius )
|
|
mPolygon.append( newPoint );
|
|
else if ( distStart < radius )
|
|
mPolygon.insert( 0, newPoint );
|
|
}
|
|
else
|
|
mPolygon.insert( indexPoint + 1, newPoint );
|
|
|
|
return true;
|
|
}
|
|
|
|
bool QgsLayoutItemPolyline::_removeNode( const int index )
|
|
{
|
|
if ( index < 0 || index >= mPolygon.size() )
|
|
return false;
|
|
|
|
mPolygon.remove( index );
|
|
|
|
if ( mPolygon.size() < 2 )
|
|
mPolygon.clear();
|
|
else
|
|
{
|
|
int newSelectNode = index;
|
|
if ( index >= mPolygon.size() )
|
|
newSelectNode = mPolygon.size() - 1;
|
|
setSelectedNode( newSelectNode );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::createDefaultPolylineStyleSymbol()
|
|
{
|
|
QVariantMap properties;
|
|
properties.insert( QStringLiteral( "color" ), QStringLiteral( "0,0,0,255" ) );
|
|
properties.insert( QStringLiteral( "width" ), QStringLiteral( "0.3" ) );
|
|
properties.insert( QStringLiteral( "capstyle" ), QStringLiteral( "square" ) );
|
|
|
|
mPolylineStyleSymbol.reset( QgsLineSymbol::createSimple( properties ) );
|
|
refreshSymbol();
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::refreshSymbol()
|
|
{
|
|
if ( auto *lLayout = layout() )
|
|
{
|
|
const QgsRenderContext rc = QgsLayoutUtils::createRenderContextForLayout( lLayout, nullptr, lLayout->renderContext().dpi() );
|
|
mMaxSymbolBleed = ( 25.4 / lLayout->renderContext().dpi() ) * QgsSymbolLayerUtils::estimateMaxSymbolBleed( mPolylineStyleSymbol.get(), rc );
|
|
}
|
|
|
|
updateSceneRect();
|
|
|
|
emit frameChanged();
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::drawStartMarker( QPainter *painter )
|
|
{
|
|
if ( mPolygon.size() < 2 )
|
|
return;
|
|
|
|
switch ( mStartMarker )
|
|
{
|
|
case MarkerMode::NoMarker:
|
|
break;
|
|
|
|
case MarkerMode::ArrowHead:
|
|
{
|
|
// calculate angle at start of line
|
|
const QLineF startLine( mPolygon.at( 0 ), mPolygon.at( 1 ) );
|
|
const double angle = startLine.angle();
|
|
drawArrow( painter, mPolygon.at( 0 ), angle );
|
|
break;
|
|
}
|
|
|
|
case MarkerMode::SvgMarker:
|
|
{
|
|
// calculate angle at start of line
|
|
const QLineF startLine( mPolygon.at( 0 ), mPolygon.at( 1 ) );
|
|
const double angle = startLine.angle();
|
|
drawSvgMarker( painter, mPolygon.at( 0 ), angle, mStartMarkerFile, mStartArrowHeadHeight );
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::drawEndMarker( QPainter *painter )
|
|
{
|
|
if ( mPolygon.size() < 2 )
|
|
return;
|
|
|
|
switch ( mEndMarker )
|
|
{
|
|
case MarkerMode::NoMarker:
|
|
break;
|
|
|
|
case MarkerMode::ArrowHead:
|
|
{
|
|
// calculate angle at end of line
|
|
const QLineF endLine( mPolygon.at( mPolygon.count() - 2 ), mPolygon.at( mPolygon.count() - 1 ) );
|
|
const double angle = endLine.angle();
|
|
|
|
//move end point depending on arrow width
|
|
const QVector2D dir = QVector2D( endLine.dx(), endLine.dy() ).normalized();
|
|
QPointF endPoint = endLine.p2();
|
|
endPoint += ( dir * 0.5 * mArrowHeadWidth ).toPointF();
|
|
|
|
drawArrow( painter, endPoint, angle );
|
|
break;
|
|
}
|
|
case MarkerMode::SvgMarker:
|
|
{
|
|
// calculate angle at end of line
|
|
const QLineF endLine( mPolygon.at( mPolygon.count() - 2 ), mPolygon.at( mPolygon.count() - 1 ) );
|
|
const double angle = endLine.angle();
|
|
drawSvgMarker( painter, endLine.p2(), angle, mEndMarkerFile, mEndArrowHeadHeight );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::drawArrow( QPainter *painter, QPointF center, double angle )
|
|
{
|
|
// translate angle from ccw from axis to cw from north
|
|
angle = 90 - angle;
|
|
QPen p;
|
|
p.setColor( mArrowHeadStrokeColor );
|
|
p.setWidthF( mArrowHeadStrokeWidth );
|
|
painter->setPen( p );
|
|
QBrush b;
|
|
b.setColor( mArrowHeadFillColor );
|
|
painter->setBrush( b );
|
|
|
|
drawArrowHead( painter, center.x(), center.y(), angle, mArrowHeadWidth );
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::updateMarkerSvgSizes()
|
|
{
|
|
setStartSvgMarkerPath( mStartMarkerFile );
|
|
setEndSvgMarkerPath( mEndMarkerFile );
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::drawArrowHead( QPainter *p, const double x, const double y, const double angle, const double arrowHeadWidth )
|
|
{
|
|
if ( !p )
|
|
return;
|
|
|
|
const double angleRad = angle / 180.0 * M_PI;
|
|
const QPointF middlePoint( x, y );
|
|
|
|
//rotate both arrow points
|
|
const QPointF p1 = QPointF( -arrowHeadWidth / 2.0, arrowHeadWidth );
|
|
const QPointF p2 = QPointF( arrowHeadWidth / 2.0, arrowHeadWidth );
|
|
|
|
QPointF p1Rotated, p2Rotated;
|
|
p1Rotated.setX( p1.x() * std::cos( angleRad ) + p1.y() * -std::sin( angleRad ) );
|
|
p1Rotated.setY( p1.x() * std::sin( angleRad ) + p1.y() * std::cos( angleRad ) );
|
|
p2Rotated.setX( p2.x() * std::cos( angleRad ) + p2.y() * -std::sin( angleRad ) );
|
|
p2Rotated.setY( p2.x() * std::sin( angleRad ) + p2.y() * std::cos( angleRad ) );
|
|
|
|
QPolygonF arrowHeadPoly;
|
|
arrowHeadPoly << middlePoint;
|
|
arrowHeadPoly << QPointF( middlePoint.x() + p1Rotated.x(), middlePoint.y() + p1Rotated.y() );
|
|
arrowHeadPoly << QPointF( middlePoint.x() + p2Rotated.x(), middlePoint.y() + p2Rotated.y() );
|
|
QPen arrowPen = p->pen();
|
|
arrowPen.setJoinStyle( Qt::RoundJoin );
|
|
QBrush arrowBrush = p->brush();
|
|
arrowBrush.setStyle( Qt::SolidPattern );
|
|
p->setPen( arrowPen );
|
|
p->setBrush( arrowBrush );
|
|
arrowBrush.setStyle( Qt::SolidPattern );
|
|
p->drawPolygon( arrowHeadPoly );
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::drawSvgMarker( QPainter *p, QPointF point, double angle, const QString &markerPath, double height ) const
|
|
{
|
|
// translate angle from ccw from axis to cw from north
|
|
angle = 90 - angle;
|
|
|
|
if ( mArrowHeadWidth <= 0 || height <= 0 )
|
|
{
|
|
//bad image size
|
|
return;
|
|
}
|
|
|
|
if ( markerPath.isEmpty() )
|
|
return;
|
|
|
|
QSvgRenderer r;
|
|
const QByteArray &svgContent = QgsApplication::svgCache()->svgContent( markerPath, mArrowHeadWidth, mArrowHeadFillColor, mArrowHeadStrokeColor, mArrowHeadStrokeWidth,
|
|
1.0 );
|
|
r.load( svgContent );
|
|
|
|
const QgsScopedQPainterState painterState( p );
|
|
p->translate( point.x(), point.y() );
|
|
p->rotate( angle );
|
|
p->translate( -mArrowHeadWidth / 2.0, -height / 2.0 );
|
|
r.render( p, QRectF( 0, 0, mArrowHeadWidth, height ) );
|
|
}
|
|
|
|
QString QgsLayoutItemPolyline::displayName() const
|
|
{
|
|
if ( !id().isEmpty() )
|
|
return id();
|
|
|
|
return tr( "<Polyline>" );
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::_draw( QgsLayoutItemRenderContext &context, const QStyleOptionGraphicsItem * )
|
|
{
|
|
QgsRenderContext renderContext = context.renderContext();
|
|
// symbol clipping messes with geometry generators used in the symbol for this item, and has no
|
|
// valid use here. See https://github.com/qgis/QGIS/issues/58909
|
|
renderContext.setFlag( Qgis::RenderContextFlag::DisableSymbolClippingToExtent );
|
|
|
|
const QgsScopedQPainterState painterState( renderContext.painter() );
|
|
//setup painter scaling to dots so that raster symbology is drawn to scale
|
|
const double scale = renderContext.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters );
|
|
const QTransform t = QTransform::fromScale( scale, scale );
|
|
|
|
mPolylineStyleSymbol->startRender( renderContext );
|
|
mPolylineStyleSymbol->renderPolyline( t.map( mPolygon ), nullptr, renderContext );
|
|
mPolylineStyleSymbol->stopRender( renderContext );
|
|
|
|
// painter is scaled to dots, so scale back to layout units
|
|
renderContext.painter()->scale( renderContext.scaleFactor(), renderContext.scaleFactor() );
|
|
|
|
drawStartMarker( renderContext.painter() );
|
|
drawEndMarker( renderContext.painter() );
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::_readXmlStyle( const QDomElement &elmt, const QgsReadWriteContext &context )
|
|
{
|
|
mPolylineStyleSymbol.reset( QgsSymbolLayerUtils::loadSymbol<QgsLineSymbol>( elmt, context ) );
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::setSymbol( QgsLineSymbol *symbol )
|
|
{
|
|
mPolylineStyleSymbol.reset( static_cast<QgsLineSymbol *>( symbol->clone() ) );
|
|
refreshSymbol();
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::setStartMarker( QgsLayoutItemPolyline::MarkerMode mode )
|
|
{
|
|
mStartMarker = mode;
|
|
update();
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::setEndMarker( QgsLayoutItemPolyline::MarkerMode mode )
|
|
{
|
|
mEndMarker = mode;
|
|
update();
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::setArrowHeadWidth( double width )
|
|
{
|
|
mArrowHeadWidth = width;
|
|
updateMarkerSvgSizes();
|
|
update();
|
|
}
|
|
|
|
QPainterPath QgsLayoutItemPolyline::shape() const
|
|
{
|
|
QPainterPath path;
|
|
path.addPolygon( mPolygon );
|
|
|
|
QPainterPathStroker ps;
|
|
|
|
ps.setWidth( 2 * mMaxSymbolBleed );
|
|
const QPainterPath strokedOutline = ps.createStroke( path );
|
|
|
|
return strokedOutline;
|
|
}
|
|
|
|
bool QgsLayoutItemPolyline::isValid() const
|
|
{
|
|
QList<QPointF> uniquePoints;
|
|
int seen = 0;
|
|
for (QPointF point: mPolygon)
|
|
{
|
|
if( !uniquePoints.contains( point ) )
|
|
{
|
|
uniquePoints.append( point );
|
|
if (++seen > 1)
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
QgsLineSymbol *QgsLayoutItemPolyline::symbol()
|
|
{
|
|
return mPolylineStyleSymbol.get();
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::setStartSvgMarkerPath( const QString &path )
|
|
{
|
|
QSvgRenderer r;
|
|
mStartMarkerFile = path;
|
|
if ( path.isEmpty() || !r.load( path ) )
|
|
{
|
|
mStartArrowHeadHeight = 0;
|
|
}
|
|
else
|
|
{
|
|
//calculate mArrowHeadHeight from svg file and mArrowHeadWidth
|
|
const QRect viewBox = r.viewBox();
|
|
mStartArrowHeadHeight = mArrowHeadWidth / viewBox.width() * viewBox.height();
|
|
}
|
|
updateBoundingRect();
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::setEndSvgMarkerPath( const QString &path )
|
|
{
|
|
QSvgRenderer r;
|
|
mEndMarkerFile = path;
|
|
if ( path.isEmpty() || !r.load( path ) )
|
|
{
|
|
mEndArrowHeadHeight = 0;
|
|
}
|
|
else
|
|
{
|
|
//calculate mArrowHeadHeight from svg file and mArrowHeadWidth
|
|
const QRect viewBox = r.viewBox();
|
|
mEndArrowHeadHeight = mArrowHeadWidth / viewBox.width() * viewBox.height();
|
|
}
|
|
updateBoundingRect();
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::setArrowHeadStrokeColor( const QColor &color )
|
|
{
|
|
mArrowHeadStrokeColor = color;
|
|
update();
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::setArrowHeadFillColor( const QColor &color )
|
|
{
|
|
mArrowHeadFillColor = color;
|
|
update();
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::setArrowHeadStrokeWidth( double width )
|
|
{
|
|
mArrowHeadStrokeWidth = width;
|
|
updateBoundingRect();
|
|
update();
|
|
}
|
|
|
|
bool QgsLayoutItemPolyline::accept( QgsStyleEntityVisitorInterface *visitor ) const
|
|
{
|
|
if ( mPolylineStyleSymbol )
|
|
{
|
|
QgsStyleSymbolEntity entity( mPolylineStyleSymbol.get() );
|
|
if ( !visitor->visit( QgsStyleEntityVisitorInterface::StyleLeaf( &entity, uuid(), displayName() ) ) )
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::_writeXmlStyle( QDomDocument &doc, QDomElement &elmt, const QgsReadWriteContext &context ) const
|
|
{
|
|
const QDomElement pe = QgsSymbolLayerUtils::saveSymbol( QString(),
|
|
mPolylineStyleSymbol.get(),
|
|
doc,
|
|
context );
|
|
elmt.appendChild( pe );
|
|
}
|
|
|
|
bool QgsLayoutItemPolyline::writePropertiesToElement( QDomElement &elmt, QDomDocument &doc, const QgsReadWriteContext &context ) const
|
|
{
|
|
QgsLayoutNodesItem::writePropertiesToElement( elmt, doc, context );
|
|
|
|
// absolute paths to relative
|
|
const QString startMarkerPath = QgsSymbolLayerUtils::svgSymbolPathToName( mStartMarkerFile, context.pathResolver() );
|
|
const QString endMarkerPath = QgsSymbolLayerUtils::svgSymbolPathToName( mEndMarkerFile, context.pathResolver() );
|
|
|
|
elmt.setAttribute( QStringLiteral( "arrowHeadWidth" ), QString::number( mArrowHeadWidth ) );
|
|
elmt.setAttribute( QStringLiteral( "arrowHeadFillColor" ), QgsColorUtils::colorToString( mArrowHeadFillColor ) );
|
|
elmt.setAttribute( QStringLiteral( "arrowHeadOutlineColor" ), QgsColorUtils::colorToString( mArrowHeadStrokeColor ) );
|
|
elmt.setAttribute( QStringLiteral( "outlineWidth" ), QString::number( mArrowHeadStrokeWidth ) );
|
|
elmt.setAttribute( QStringLiteral( "markerMode" ), mEndMarker );
|
|
elmt.setAttribute( QStringLiteral( "startMarkerMode" ), mStartMarker );
|
|
elmt.setAttribute( QStringLiteral( "startMarkerFile" ), startMarkerPath );
|
|
elmt.setAttribute( QStringLiteral( "endMarkerFile" ), endMarkerPath );
|
|
|
|
return true;
|
|
}
|
|
|
|
bool QgsLayoutItemPolyline::readPropertiesFromElement( const QDomElement &elmt, const QDomDocument &doc, const QgsReadWriteContext &context )
|
|
{
|
|
mArrowHeadWidth = elmt.attribute( QStringLiteral( "arrowHeadWidth" ), QStringLiteral( "2.0" ) ).toDouble();
|
|
mArrowHeadFillColor = QgsColorUtils::colorFromString( elmt.attribute( QStringLiteral( "arrowHeadFillColor" ), QStringLiteral( "0,0,0,255" ) ) );
|
|
mArrowHeadStrokeColor = QgsColorUtils::colorFromString( elmt.attribute( QStringLiteral( "arrowHeadOutlineColor" ), QStringLiteral( "0,0,0,255" ) ) );
|
|
mArrowHeadStrokeWidth = elmt.attribute( QStringLiteral( "outlineWidth" ), QStringLiteral( "1.0" ) ).toDouble();
|
|
// relative paths to absolute
|
|
const QString startMarkerPath = elmt.attribute( QStringLiteral( "startMarkerFile" ), QString() );
|
|
const QString endMarkerPath = elmt.attribute( QStringLiteral( "endMarkerFile" ), QString() );
|
|
setStartSvgMarkerPath( QgsSymbolLayerUtils::svgSymbolNameToPath( startMarkerPath, context.pathResolver() ) );
|
|
setEndSvgMarkerPath( QgsSymbolLayerUtils::svgSymbolNameToPath( endMarkerPath, context.pathResolver() ) );
|
|
mEndMarker = static_cast< QgsLayoutItemPolyline::MarkerMode >( elmt.attribute( QStringLiteral( "markerMode" ), QStringLiteral( "0" ) ).toInt() );
|
|
mStartMarker = static_cast< QgsLayoutItemPolyline::MarkerMode >( elmt.attribute( QStringLiteral( "startMarkerMode" ), QStringLiteral( "0" ) ).toInt() );
|
|
|
|
QgsLayoutNodesItem::readPropertiesFromElement( elmt, doc, context );
|
|
|
|
updateBoundingRect();
|
|
return true;
|
|
}
|
|
|
|
void QgsLayoutItemPolyline::updateBoundingRect()
|
|
{
|
|
QRectF br = rect();
|
|
|
|
double margin = std::max( mMaxSymbolBleed, computeMarkerMargin() );
|
|
if ( mEndMarker == ArrowHead )
|
|
{
|
|
margin += 0.5 * mArrowHeadWidth;
|
|
}
|
|
br.adjust( -margin, -margin, margin, margin );
|
|
prepareGeometryChange();
|
|
mCurrentRectangle = br;
|
|
|
|
// update
|
|
update();
|
|
}
|
|
|
|
|
|
double QgsLayoutItemPolyline::computeMarkerMargin() const
|
|
{
|
|
double margin = 0;
|
|
|
|
if ( mStartMarker == ArrowHead || mEndMarker == ArrowHead )
|
|
{
|
|
margin = mArrowHeadStrokeWidth / 2.0 + mArrowHeadWidth * M_SQRT2;
|
|
}
|
|
|
|
if ( mStartMarker == SvgMarker )
|
|
{
|
|
const double startMarkerMargin = std::sqrt( 0.25 * ( mStartArrowHeadHeight * mStartArrowHeadHeight + mArrowHeadWidth * mArrowHeadWidth ) );
|
|
margin = std::max( startMarkerMargin, margin );
|
|
}
|
|
|
|
if ( mEndMarker == SvgMarker )
|
|
{
|
|
const double endMarkerMargin = std::sqrt( 0.25 * ( mEndArrowHeadHeight * mEndArrowHeadHeight + mArrowHeadWidth * mArrowHeadWidth ) );
|
|
margin = std::max( endMarkerMargin, margin );
|
|
}
|
|
|
|
return margin;
|
|
}
|