New "Filled line" symbol layer type

This adds a new line symbol type which renders lines using a
fill symbol. The interior of the line is drawn using any standard
QGIS fill symbol, allowing for lines filled with gradients, line
hatches, etc.

Sponsored by North Road, thanks to SLYR
This commit is contained in:
Nyall Dawson 2023-11-04 14:35:56 +10:00
parent 6d58958a54
commit 16c8c88247
16 changed files with 1087 additions and 0 deletions

View File

@ -1499,6 +1499,107 @@ Sets the color for endpoint of gradient, only used if the gradient color type is
};
class QgsFilledLineSymbolLayer : QgsLineSymbolLayer
{
%Docstring(signature="appended")
A line symbol layer type which fills a stroked line with a :py:class:`QgsFillSymbol`.
.. versionadded:: 3.36
%End
%TypeHeaderCode
#include "qgslinesymbollayer.h"
%End
public:
QgsFilledLineSymbolLayer( double width = DEFAULT_SIMPLELINE_WIDTH, QgsFillSymbol *fillSymbol /Transfer/ = 0 );
%Docstring
Constructor for QgsFilledLineSymbolLayer.
If a ``fillSymbol`` is specified, it will be transferred to the symbol layer and used
to fill the inside of the stroked line. If no ``fillSymbol`` is specified then a default
symbol will be used.
%End
~QgsFilledLineSymbolLayer();
static QgsSymbolLayer *create( const QVariantMap &properties = QVariantMap() ) /Factory/;
%Docstring
Creates a new QgsFilledLineSymbolLayer, using the settings
serialized in the ``properties`` map (corresponding to the output from
:py:func:`QgsFilledLineSymbolLayer.properties()` ).
%End
virtual QString layerType() const;
virtual void startRender( QgsSymbolRenderContext &context );
virtual void stopRender( QgsSymbolRenderContext &context );
virtual void renderPolyline( const QPolygonF &points, QgsSymbolRenderContext &context );
virtual QVariantMap properties() const;
virtual QgsFilledLineSymbolLayer *clone() const /Factory/;
virtual QgsSymbol *subSymbol();
virtual bool setSubSymbol( QgsSymbol *symbol /Transfer/ );
virtual bool hasDataDefinedProperties() const;
virtual void setColor( const QColor &c );
virtual QColor color() const;
virtual void setOutputUnit( Qgis::RenderUnit unit );
virtual Qgis::RenderUnit outputUnit() const;
virtual bool usesMapUnits() const;
virtual void setMapUnitScale( const QgsMapUnitScale &scale );
virtual QgsMapUnitScale mapUnitScale() const;
virtual double estimateMaxBleed( const QgsRenderContext &context ) const;
virtual QSet<QString> usedAttributes( const QgsRenderContext &context ) const;
Qt::PenJoinStyle penJoinStyle() const;
%Docstring
Returns the pen join style used to render the line (e.g. miter, bevel, round, etc).
.. seealso:: :py:func:`setPenJoinStyle`
%End
void setPenJoinStyle( Qt::PenJoinStyle style );
%Docstring
Sets the pen join ``style`` used to render the line (e.g. miter, bevel, round, etc).
.. seealso:: :py:func:`penJoinStyle`
%End
Qt::PenCapStyle penCapStyle() const;
%Docstring
Returns the pen cap style used to render the line (e.g. flat, square, round, etc).
.. seealso:: :py:func:`setPenCapStyle`
%End
void setPenCapStyle( Qt::PenCapStyle style );
%Docstring
Sets the pen cap ``style`` used to render the line (e.g. flat, square, round, etc).
.. seealso:: :py:func:`penCapStyle`
%End
private:
QgsFilledLineSymbolLayer( const QgsFilledLineSymbolLayer & );
};
/************************************************************************
* This file has been generated automatically from *

View File

@ -703,6 +703,48 @@ Creates a new QgsLineburstSymbolLayerWidget.
class QgsFilledLineSymbolLayerWidget : QgsSymbolLayerWidget
{
%Docstring(signature="appended")
A widget for configuring :py:class:`QgsFilledLineSymbolLayer`.
.. versionadded:: 3.36
%End
%TypeHeaderCode
#include "qgssymbollayerwidget.h"
%End
public:
QgsFilledLineSymbolLayerWidget( QgsVectorLayer *vl, QWidget *parent /TransferThis/ = 0 );
%Docstring
Constructor for QgsFilledLineSymbolLayerWidget.
:param vl: associated vector layer
:param parent: parent widget
%End
~QgsFilledLineSymbolLayerWidget();
static QgsSymbolLayerWidget *create( QgsVectorLayer *vl ) /Factory/;
%Docstring
Creates a new QgsFilledLineSymbolLayerWidget.
:param vl: associated vector layer
%End
virtual void setSymbolLayer( QgsSymbolLayer *layer );
virtual QgsSymbolLayer *symbolLayer();
};
class QgsSVGFillSymbolLayerWidget : QgsSymbolLayerWidget
{

View File

@ -31,6 +31,7 @@
#include "qgsfeedback.h"
#include "qgsimageoperation.h"
#include "qgscolorrampimpl.h"
#include "qgsfillsymbol.h"
#include <algorithm>
#include <QPainter>
@ -3788,3 +3789,309 @@ void QgsLineburstSymbolLayer::setColorRamp( QgsColorRamp *ramp )
{
mGradientRamp.reset( ramp );
}
//
// QgsFilledLineSymbolLayer
//
QgsFilledLineSymbolLayer::QgsFilledLineSymbolLayer( double width, QgsFillSymbol *fillSymbol )
: QgsLineSymbolLayer()
{
mWidth = width;
mFill.reset( fillSymbol ? fillSymbol : static_cast<QgsFillSymbol *>( QgsFillSymbol::createSimple( QVariantMap() ) ) );
}
QgsFilledLineSymbolLayer::~QgsFilledLineSymbolLayer() = default;
QgsSymbolLayer *QgsFilledLineSymbolLayer::create( const QVariantMap &props )
{
double width = DEFAULT_SIMPLELINE_WIDTH;
if ( props.contains( QStringLiteral( "line_width" ) ) )
{
width = props[QStringLiteral( "line_width" )].toDouble();
}
else if ( props.contains( QStringLiteral( "outline_width" ) ) )
{
width = props[QStringLiteral( "outline_width" )].toDouble();
}
else if ( props.contains( QStringLiteral( "width" ) ) )
{
//pre 2.5 projects used "width"
width = props[QStringLiteral( "width" )].toDouble();
}
std::unique_ptr<QgsFilledLineSymbolLayer > l = std::make_unique< QgsFilledLineSymbolLayer >( width, QgsFillSymbol::createSimple( props ) );
if ( props.contains( QStringLiteral( "line_width_unit" ) ) )
{
l->setWidthUnit( QgsUnitTypes::decodeRenderUnit( props[QStringLiteral( "line_width_unit" )].toString() ) );
}
else if ( props.contains( QStringLiteral( "outline_width_unit" ) ) )
{
l->setWidthUnit( QgsUnitTypes::decodeRenderUnit( props[QStringLiteral( "outline_width_unit" )].toString() ) );
}
else if ( props.contains( QStringLiteral( "width_unit" ) ) )
{
//pre 2.5 projects used "width_unit"
l->setWidthUnit( QgsUnitTypes::decodeRenderUnit( props[QStringLiteral( "width_unit" )].toString() ) );
}
if ( props.contains( QStringLiteral( "width_map_unit_scale" ) ) )
l->setWidthMapUnitScale( QgsSymbolLayerUtils::decodeMapUnitScale( props[QStringLiteral( "width_map_unit_scale" )].toString() ) );
if ( props.contains( QStringLiteral( "offset" ) ) )
l->setOffset( props[QStringLiteral( "offset" )].toDouble() );
if ( props.contains( QStringLiteral( "offset_unit" ) ) )
l->setOffsetUnit( QgsUnitTypes::decodeRenderUnit( props[QStringLiteral( "offset_unit" )].toString() ) );
if ( props.contains( QStringLiteral( "offset_map_unit_scale" ) ) )
l->setOffsetMapUnitScale( QgsSymbolLayerUtils::decodeMapUnitScale( props[QStringLiteral( "offset_map_unit_scale" )].toString() ) );
if ( props.contains( QStringLiteral( "joinstyle" ) ) )
l->setPenJoinStyle( QgsSymbolLayerUtils::decodePenJoinStyle( props[QStringLiteral( "joinstyle" )].toString() ) );
if ( props.contains( QStringLiteral( "capstyle" ) ) )
l->setPenCapStyle( QgsSymbolLayerUtils::decodePenCapStyle( props[QStringLiteral( "capstyle" )].toString() ) );
l->restoreOldDataDefinedProperties( props );
return l.release();
}
QString QgsFilledLineSymbolLayer::layerType() const
{
return QStringLiteral( "FilledLine" );
}
void QgsFilledLineSymbolLayer::startRender( QgsSymbolRenderContext &context )
{
if ( mFill )
{
mFill->startRender( context.renderContext(), context.fields() );
}
}
void QgsFilledLineSymbolLayer::stopRender( QgsSymbolRenderContext &context )
{
if ( mFill )
{
mFill->stopRender( context.renderContext() );
}
}
void QgsFilledLineSymbolLayer::renderPolyline( const QPolygonF &points, QgsSymbolRenderContext &context )
{
QPainter *p = context.renderContext().painter();
if ( !p || !mFill )
return;
double width = mWidth;
if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyStrokeWidth ) )
{
context.setOriginalValueVariable( mWidth );
width = mDataDefinedProperties.valueAsDouble( QgsSymbolLayer::PropertyStrokeWidth, context.renderContext().expressionContext(), mWidth );
}
const double scaledWidth = context.renderContext().convertToPainterUnits( width, mWidthUnit, mWidthMapUnitScale );
Qt::PenJoinStyle join = mPenJoinStyle;
if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyJoinStyle ) )
{
context.setOriginalValueVariable( QgsSymbolLayerUtils::encodePenJoinStyle( join ) );
QVariant exprVal = mDataDefinedProperties.value( QgsSymbolLayer::PropertyJoinStyle, context.renderContext().expressionContext() );
if ( !QgsVariantUtils::isNull( exprVal ) )
join = QgsSymbolLayerUtils::decodePenJoinStyle( exprVal.toString() );
}
Qt::PenCapStyle cap = mPenCapStyle;
if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyCapStyle ) )
{
context.setOriginalValueVariable( QgsSymbolLayerUtils::encodePenCapStyle( cap ) );
QVariant exprVal = mDataDefinedProperties.value( QgsSymbolLayer::PropertyCapStyle, context.renderContext().expressionContext() );
if ( !QgsVariantUtils::isNull( exprVal ) )
cap = QgsSymbolLayerUtils::decodePenCapStyle( exprVal.toString() );
}
double offset = mOffset;
if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyOffset ) )
{
context.setOriginalValueVariable( mOffset );
offset = mDataDefinedProperties.valueAsDouble( QgsSymbolLayer::PropertyOffset, context.renderContext().expressionContext(), offset );
}
const double prevOpacity = mFill->opacity();
mFill->setOpacity( mFill->opacity() * context.opacity() );
const bool prevIsSubsymbol = context.renderContext().flags() & Qgis::RenderContextFlag::RenderingSubSymbol;
context.renderContext().setFlag( Qgis::RenderContextFlag::RenderingSubSymbol );
const bool useSelectedColor = shouldRenderUsingSelectionColor( context );
// stroke out the path using the correct line cap/join style. We'll then use this as the fill polygon
QPainterPathStroker stroker;
stroker.setWidth( scaledWidth );
stroker.setCapStyle( cap );
stroker.setJoinStyle( join );
QPolygonF polygon;
if ( qgsDoubleNear( offset, 0 ) )
{
QPainterPath path;
path.addPolygon( points );
const QPainterPath stroke = stroker.createStroke( path ).simplified();
const QPolygonF polygon = stroke.toFillPolygon();
if ( !polygon.isEmpty() )
{
mFill->renderPolygon( polygon, /* rings */ nullptr, context.feature(), context.renderContext(), -1, useSelectedColor );
}
}
else
{
double scaledOffset = context.renderContext().convertToPainterUnits( offset, mOffsetUnit, mOffsetMapUnitScale );
if ( mOffsetUnit == Qgis::RenderUnit::MetersInMapUnits && context.renderContext().flags() & Qgis::RenderContextFlag::RenderSymbolPreview )
{
// rendering for symbol previews -- a size in meters in map units can't be calculated, so treat the size as millimeters
// and clamp it to a reasonable range. It's the best we can do in this situation!
scaledOffset = std::min( std::max( context.renderContext().convertToPainterUnits( offset, Qgis::RenderUnit::Millimeters ), 3.0 ), 100.0 );
}
const QList<QPolygonF> mline = ::offsetLine( points, scaledOffset, context.originalGeometryType() != Qgis::GeometryType::Unknown ? context.originalGeometryType() : Qgis::GeometryType::Line );
for ( const QPolygonF &part : mline )
{
QPainterPath path;
path.addPolygon( part );
const QPainterPath stroke = stroker.createStroke( path ).simplified();
const QPolygonF polygon = stroke.toFillPolygon();
if ( !polygon.isEmpty() )
{
mFill->renderPolygon( polygon, /* rings */ nullptr, context.feature(), context.renderContext(), -1, useSelectedColor );
}
}
}
context.renderContext().setFlag( Qgis::RenderContextFlag::RenderingSubSymbol, prevIsSubsymbol );
mFill->setOpacity( prevOpacity );
}
QVariantMap QgsFilledLineSymbolLayer::properties() const
{
QVariantMap map;
map[QStringLiteral( "line_width" )] = QString::number( mWidth );
map[QStringLiteral( "line_width_unit" )] = QgsUnitTypes::encodeUnit( mWidthUnit );
map[QStringLiteral( "width_map_unit_scale" )] = QgsSymbolLayerUtils::encodeMapUnitScale( mWidthMapUnitScale );
map[QStringLiteral( "joinstyle" )] = QgsSymbolLayerUtils::encodePenJoinStyle( mPenJoinStyle );
map[QStringLiteral( "capstyle" )] = QgsSymbolLayerUtils::encodePenCapStyle( mPenCapStyle );
map[QStringLiteral( "offset" )] = QString::number( mOffset );
map[QStringLiteral( "offset_unit" )] = QgsUnitTypes::encodeUnit( mOffsetUnit );
map[QStringLiteral( "offset_map_unit_scale" )] = QgsSymbolLayerUtils::encodeMapUnitScale( mOffsetMapUnitScale );
if ( mFill )
{
map[QStringLiteral( "color" )] = QgsSymbolLayerUtils::encodeColor( mFill->color() );
}
return map;
}
QgsFilledLineSymbolLayer *QgsFilledLineSymbolLayer::clone() const
{
std::unique_ptr< QgsFilledLineSymbolLayer > res( qgis::down_cast< QgsFilledLineSymbolLayer * >( QgsFilledLineSymbolLayer::create( properties() ) ) );
copyPaintEffect( res.get() );
copyDataDefinedProperties( res.get() );
res->setSubSymbol( mFill->clone() );
return res.release();
}
QgsSymbol *QgsFilledLineSymbolLayer::subSymbol()
{
return mFill.get();
}
bool QgsFilledLineSymbolLayer::setSubSymbol( QgsSymbol *symbol )
{
if ( symbol && symbol->type() == Qgis::SymbolType::Fill )
{
mFill.reset( static_cast<QgsFillSymbol *>( symbol ) );
return true;
}
else
{
delete symbol;
return false;
}
}
double QgsFilledLineSymbolLayer::estimateMaxBleed( const QgsRenderContext &context ) const
{
if ( mFill )
{
return QgsSymbolLayerUtils::estimateMaxSymbolBleed( mFill.get(), context );
}
return 0;
}
QSet<QString> QgsFilledLineSymbolLayer::usedAttributes( const QgsRenderContext &context ) const
{
QSet<QString> attr = QgsLineSymbolLayer::usedAttributes( context );
if ( mFill )
attr.unite( mFill->usedAttributes( context ) );
return attr;
}
bool QgsFilledLineSymbolLayer::hasDataDefinedProperties() const
{
if ( QgsSymbolLayer::hasDataDefinedProperties() )
return true;
if ( mFill && mFill->hasDataDefinedProperties() )
return true;
return false;
}
void QgsFilledLineSymbolLayer::setColor( const QColor &c )
{
mColor = c;
if ( mFill )
mFill->setColor( c );
}
QColor QgsFilledLineSymbolLayer::color() const
{
return mFill ? mFill->color() : mColor;
}
bool QgsFilledLineSymbolLayer::usesMapUnits() const
{
return mWidthUnit == Qgis::RenderUnit::MapUnits || mWidthUnit == Qgis::RenderUnit::MetersInMapUnits
|| mOffsetUnit == Qgis::RenderUnit::MapUnits || mOffsetUnit == Qgis::RenderUnit::MetersInMapUnits
|| ( mFill && mFill->usesMapUnits() );
}
void QgsFilledLineSymbolLayer::setMapUnitScale( const QgsMapUnitScale &scale )
{
QgsLineSymbolLayer::setMapUnitScale( scale );
if ( mFill )
mFill->setMapUnitScale( scale );
}
QgsMapUnitScale QgsFilledLineSymbolLayer::mapUnitScale() const
{
if ( mFill )
{
return mFill->mapUnitScale();
}
return QgsMapUnitScale();
}
void QgsFilledLineSymbolLayer::setOutputUnit( Qgis::RenderUnit unit )
{
QgsLineSymbolLayer::setOutputUnit( unit );
if ( mFill )
mFill->setOutputUnit( unit );
}
Qgis::RenderUnit QgsFilledLineSymbolLayer::outputUnit() const
{
if ( mFill )
{
return mFill->outputUnit();
}
return Qgis::RenderUnit::Unknown;
}

View File

@ -28,6 +28,7 @@ class QgsMarkerSymbol;
class QgsLineSymbol;
class QgsPathResolver;
class QgsColorRamp;
class QgsFillSymbol;
#define DEFAULT_SIMPLELINE_COLOR QColor(35,35,35)
#define DEFAULT_SIMPLELINE_WIDTH DEFAULT_LINE_WIDTH
@ -1363,6 +1364,97 @@ class CORE_EXPORT QgsLineburstSymbolLayer : public QgsAbstractBrushedLineSymbolL
};
/**
* \ingroup core
* \class QgsFilledLineSymbolLayer
*
* \brief A line symbol layer type which fills a stroked line with a QgsFillSymbol.
*
* \since QGIS 3.36
*/
class CORE_EXPORT QgsFilledLineSymbolLayer : public QgsLineSymbolLayer
{
public:
/**
* Constructor for QgsFilledLineSymbolLayer.
*
* If a \a fillSymbol is specified, it will be transferred to the symbol layer and used
* to fill the inside of the stroked line. If no \a fillSymbol is specified then a default
* symbol will be used.
*/
QgsFilledLineSymbolLayer( double width = DEFAULT_SIMPLELINE_WIDTH, QgsFillSymbol *fillSymbol SIP_TRANSFER = nullptr );
~QgsFilledLineSymbolLayer() override;
/**
* Creates a new QgsFilledLineSymbolLayer, using the settings
* serialized in the \a properties map (corresponding to the output from
* QgsFilledLineSymbolLayer::properties() ).
*/
static QgsSymbolLayer *create( const QVariantMap &properties = QVariantMap() ) SIP_FACTORY;
QString layerType() const override;
void startRender( QgsSymbolRenderContext &context ) override;
void stopRender( QgsSymbolRenderContext &context ) override;
void renderPolyline( const QPolygonF &points, QgsSymbolRenderContext &context ) override;
QVariantMap properties() const override;
QgsFilledLineSymbolLayer *clone() const override SIP_FACTORY;
QgsSymbol *subSymbol() override;
bool setSubSymbol( QgsSymbol *symbol SIP_TRANSFER ) override;
bool hasDataDefinedProperties() const override;
void setColor( const QColor &c ) override;
QColor color() const override;
void setOutputUnit( Qgis::RenderUnit unit ) override;
Qgis::RenderUnit outputUnit() const override;
bool usesMapUnits() const override;
void setMapUnitScale( const QgsMapUnitScale &scale ) override;
QgsMapUnitScale mapUnitScale() const override;
double estimateMaxBleed( const QgsRenderContext &context ) const override;
QSet<QString> usedAttributes( const QgsRenderContext &context ) const override;
/**
* Returns the pen join style used to render the line (e.g. miter, bevel, round, etc).
*
* \see setPenJoinStyle()
*/
Qt::PenJoinStyle penJoinStyle() const { return mPenJoinStyle; }
/**
* Sets the pen join \a style used to render the line (e.g. miter, bevel, round, etc).
*
* \see penJoinStyle()
*/
void setPenJoinStyle( Qt::PenJoinStyle style ) { mPenJoinStyle = style; }
/**
* Returns the pen cap style used to render the line (e.g. flat, square, round, etc).
*
* \see setPenCapStyle()
*/
Qt::PenCapStyle penCapStyle() const { return mPenCapStyle; }
/**
* Sets the pen cap \a style used to render the line (e.g. flat, square, round, etc).
*
* \see penCapStyle()
*/
void setPenCapStyle( Qt::PenCapStyle style ) { mPenCapStyle = style; }
private:
#ifdef SIP_RUN
QgsFilledLineSymbolLayer( const QgsFilledLineSymbolLayer & );
#endif
//! Fill subsymbol
std::unique_ptr< QgsFillSymbol > mFill;
Qt::PenJoinStyle mPenJoinStyle = DEFAULT_SIMPLELINE_JOINSTYLE;
Qt::PenCapStyle mPenCapStyle = DEFAULT_SIMPLELINE_CAPSTYLE;
};
#endif

View File

@ -42,6 +42,8 @@ QgsSymbolLayerRegistry::QgsSymbolLayerRegistry()
QgsRasterLineSymbolLayer::create, nullptr, QgsRasterLineSymbolLayer::resolvePaths ) );
addSymbolLayerType( new QgsSymbolLayerMetadata( QStringLiteral( "Lineburst" ), QObject::tr( "Lineburst" ), Qgis::SymbolType::Line,
QgsLineburstSymbolLayer::create ) );
addSymbolLayerType( new QgsSymbolLayerMetadata( QStringLiteral( "FilledLine" ), QObject::tr( "Filled Line" ), Qgis::SymbolType::Line,
QgsFilledLineSymbolLayer::create ) );
addSymbolLayerType( new QgsSymbolLayerMetadata( QStringLiteral( "SimpleMarker" ), QObject::tr( "Simple Marker" ), Qgis::SymbolType::Marker,
QgsSimpleMarkerSymbolLayer::create, QgsSimpleMarkerSymbolLayer::createFromSld ) );

View File

@ -82,6 +82,7 @@ static void _initWidgetFunctions()
_initWidgetFunction( QStringLiteral( "InterpolatedLine" ), QgsInterpolatedLineSymbolLayerWidget::create );
_initWidgetFunction( QStringLiteral( "RasterLine" ), QgsRasterLineSymbolLayerWidget::create );
_initWidgetFunction( QStringLiteral( "Lineburst" ), QgsLineburstSymbolLayerWidget::create );
_initWidgetFunction( QStringLiteral( "FilledLine" ), QgsFilledLineSymbolLayerWidget::create );
_initWidgetFunction( QStringLiteral( "SimpleMarker" ), QgsSimpleMarkerSymbolLayerWidget::create );
_initWidgetFunction( QStringLiteral( "FilledMarker" ), QgsFilledMarkerSymbolLayerWidget::create );

View File

@ -5333,3 +5333,117 @@ QgsSymbolLayer *QgsLineburstSymbolLayerWidget::symbolLayer()
{
return mLayer;
}
//
// QgsFilledLineSymbolLayerWidget
//
QgsFilledLineSymbolLayerWidget::QgsFilledLineSymbolLayerWidget( QgsVectorLayer *vl, QWidget *parent )
: QgsSymbolLayerWidget( parent, vl )
{
mLayer = nullptr;
setupUi( this );
mPenWidthUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << Qgis::RenderUnit::Millimeters << Qgis::RenderUnit::MetersInMapUnits << Qgis::RenderUnit::MapUnits << Qgis::RenderUnit::Pixels
<< Qgis::RenderUnit::Points << Qgis::RenderUnit::Inches );
mOffsetUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << Qgis::RenderUnit::Millimeters << Qgis::RenderUnit::MetersInMapUnits << Qgis::RenderUnit::MapUnits << Qgis::RenderUnit::Pixels
<< Qgis::RenderUnit::Points << Qgis::RenderUnit::Inches );
connect( mPenWidthUnitWidget, &QgsUnitSelectionWidget::changed, this, [ = ]
{
if ( mLayer )
{
mLayer->setWidthUnit( mPenWidthUnitWidget->unit() );
mLayer->setWidthMapUnitScale( mPenWidthUnitWidget->getMapUnitScale() );
emit changed();
}
} );
connect( spinWidth, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, [ = ]
{
if ( mLayer )
{
mLayer->setWidth( spinWidth->value() );
emit changed();
}
} );
connect( mOffsetUnitWidget, &QgsUnitSelectionWidget::changed, this, [ = ]
{
if ( mLayer )
{
mLayer->setOffsetUnit( mOffsetUnitWidget->unit() );
mLayer->setOffsetMapUnitScale( mOffsetUnitWidget->getMapUnitScale() );
emit changed();
}
} );
spinOffset->setClearValue( 0.0 );
connect( spinOffset, qOverload< double >( &QDoubleSpinBox::valueChanged ), this, [ = ]( double val )
{
if ( mLayer )
{
mLayer->setOffset( val );
emit changed();
}
} );
connect( cboCapStyle, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, [ = ]
{
if ( mLayer )
{
mLayer->setPenCapStyle( cboCapStyle->penCapStyle() );
emit changed();
}
} );
connect( cboJoinStyle, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, [ = ]
{
if ( mLayer )
{
mLayer->setPenJoinStyle( cboJoinStyle->penJoinStyle() );
emit changed();
}
} );
}
QgsFilledLineSymbolLayerWidget::~QgsFilledLineSymbolLayerWidget() = default;
void QgsFilledLineSymbolLayerWidget::setSymbolLayer( QgsSymbolLayer *layer )
{
if ( !layer )
{
return;
}
if ( layer->layerType() != QLatin1String( "FilledLine" ) )
{
return;
}
mLayer = dynamic_cast<QgsFilledLineSymbolLayer *>( layer );
if ( !mLayer )
{
return;
}
whileBlocking( spinWidth )->setValue( mLayer->width() );
whileBlocking( mPenWidthUnitWidget )->setUnit( mLayer->widthUnit() );
whileBlocking( mPenWidthUnitWidget )->setMapUnitScale( mLayer->widthMapUnitScale() );
whileBlocking( mOffsetUnitWidget )->setUnit( mLayer->offsetUnit() );
whileBlocking( mOffsetUnitWidget )->setMapUnitScale( mLayer->offsetMapUnitScale() );
whileBlocking( spinOffset )->setValue( mLayer->offset() );
whileBlocking( cboJoinStyle )->setPenJoinStyle( mLayer->penJoinStyle() );
whileBlocking( cboCapStyle )->setPenCapStyle( mLayer->penCapStyle() );
registerDataDefinedButton( mPenWidthDDBtn, QgsSymbolLayer::PropertyStrokeWidth );
registerDataDefinedButton( mOffsetDDBtn, QgsSymbolLayer::PropertyOffset );
registerDataDefinedButton( mJoinStyleDDBtn, QgsSymbolLayer::PropertyJoinStyle );
registerDataDefinedButton( mCapStyleDDBtn, QgsSymbolLayer::PropertyCapStyle );
}
QgsSymbolLayer *QgsFilledLineSymbolLayerWidget::symbolLayer()
{
return mLayer;
}

View File

@ -915,6 +915,50 @@ class GUI_EXPORT QgsLineburstSymbolLayerWidget : public QgsSymbolLayerWidget, pr
};
///////////
#include "ui_widget_filledline.h"
class QgsFilledLineSymbolLayer;
/**
* \ingroup gui
* \class QgsFilledLineSymbolLayerWidget
* A widget for configuring QgsFilledLineSymbolLayer.
* \since QGIS 3.36
*/
class GUI_EXPORT QgsFilledLineSymbolLayerWidget : public QgsSymbolLayerWidget, private Ui::WidgetFilledLine
{
Q_OBJECT
public:
/**
* Constructor for QgsFilledLineSymbolLayerWidget.
* \param vl associated vector layer
* \param parent parent widget
*/
QgsFilledLineSymbolLayerWidget( QgsVectorLayer *vl, QWidget *parent SIP_TRANSFERTHIS = nullptr );
~QgsFilledLineSymbolLayerWidget() override;
/**
* Creates a new QgsFilledLineSymbolLayerWidget.
* \param vl associated vector layer
*/
static QgsSymbolLayerWidget *create( QgsVectorLayer *vl ) SIP_FACTORY { return new QgsFilledLineSymbolLayerWidget( vl ); }
void setSymbolLayer( QgsSymbolLayer *layer ) override;
QgsSymbolLayer *symbolLayer() override;
private:
QgsFilledLineSymbolLayer *mLayer = nullptr;
};
///////////
#include "ui_widget_svgfill.h"

View File

@ -0,0 +1,223 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>WidgetFilledLine</class>
<widget class="QWidget" name="WidgetFilledLine">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>432</width>
<height>232</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="2">
<widget class="QgsPropertyOverrideButton" name="mCapStyleDDBtn">
<property name="text">
<string>…</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QgsPenCapStyleComboBox" name="cboCapStyle"/>
</item>
<item row="1" column="2">
<widget class="QgsPropertyOverrideButton" name="mOffsetDDBtn">
<property name="text">
<string>…</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QgsDoubleSpinBox" name="spinWidth">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="specialValueText">
<string>Hairline</string>
</property>
<property name="decimals">
<number>6</number>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
<property name="singleStep">
<double>0.200000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
<property name="showClearButton" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QgsUnitSelectionWidget" name="mPenWidthUnitWidget" native="true">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Stroke width</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Cap style</string>
</property>
</widget>
</item>
<item row="6" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="1">
<widget class="QgsPenJoinStyleComboBox" name="cboJoinStyle"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Offset</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QgsPropertyOverrideButton" name="mPenWidthDDBtn">
<property name="text">
<string>…</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QgsDoubleSpinBox" name="spinOffset">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="decimals">
<number>6</number>
</property>
<property name="minimum">
<double>-100000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
<property name="singleStep">
<double>0.200000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QgsUnitSelectionWidget" name="mOffsetUnitWidget" native="true">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Join style</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QgsPropertyOverrideButton" name="mJoinStyleDDBtn">
<property name="text">
<string>…</string>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>QgsDoubleSpinBox</class>
<extends>QDoubleSpinBox</extends>
<header>qgsdoublespinbox.h</header>
</customwidget>
<customwidget>
<class>QgsPropertyOverrideButton</class>
<extends>QToolButton</extends>
<header>qgspropertyoverridebutton.h</header>
</customwidget>
<customwidget>
<class>QgsUnitSelectionWidget</class>
<extends>QWidget</extends>
<header>qgsunitselectionwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>QgsPenJoinStyleComboBox</class>
<extends>QComboBox</extends>
<header>qgspenstylecombobox.h</header>
</customwidget>
<customwidget>
<class>QgsPenCapStyleComboBox</class>
<extends>QComboBox</extends>
<header>qgspenstylecombobox.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>spinWidth</tabstop>
<tabstop>mPenWidthUnitWidget</tabstop>
<tabstop>mPenWidthDDBtn</tabstop>
<tabstop>spinOffset</tabstop>
<tabstop>mOffsetUnitWidget</tabstop>
<tabstop>mOffsetDDBtn</tabstop>
<tabstop>cboJoinStyle</tabstop>
<tabstop>mJoinStyleDDBtn</tabstop>
<tabstop>cboCapStyle</tabstop>
<tabstop>mCapStyleDDBtn</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -101,6 +101,7 @@ ADD_PYTHON_TEST(PyQgsFeatureSink test_qgsfeaturesink.py)
ADD_PYTHON_TEST(PyQgsFeatureSource test_qgsfeaturesource.py)
ADD_PYTHON_TEST(PyQgsFieldComboBoxTest test_qgsfieldcombobox.py)
ADD_PYTHON_TEST(PyQgsFieldFormattersTest test_qgsfieldformatters.py)
ADD_PYTHON_TEST(PyQgsFilledLineSymbolLayer test_qgsfilledlinesymbollayer.py)
ADD_PYTHON_TEST(PyQgsFillSymbolLayers test_qgsfillsymbollayers.py)
ADD_PYTHON_TEST(PyQgsFontManager test_qgsfontmanager.py)
ADD_PYTHON_TEST(PyQgsProject test_qgsproject.py)

View File

@ -0,0 +1,160 @@
"""
***************************************************************************
test_qgsfilledlinesymbollayer.py
---------------------
Date : November 2023
Copyright : (C) 2023 by Nyall Dawson
Email : nyall dot dawson at gmail 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. *
* *
***************************************************************************
"""
__author__ = 'Nyall Dawson'
__date__ = 'October 2021'
__copyright__ = '(C) 2021, Nyall Dawson'
from typing import Optional
import qgis # NOQA
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtGui import QColor, QImage, QPainter
from qgis.core import (
QgsFeature,
QgsGeometry,
QgsLineSymbol,
QgsMapSettings,
QgsRenderContext,
QgsFilledLineSymbolLayer,
)
import unittest
from qgis.testing import start_app, QgisTestCase
from utilities import unitTestDataPath
start_app()
TEST_DATA_DIR = unitTestDataPath()
class TestQgsFilledLineSymbolLayer(QgisTestCase):
@classmethod
def control_path_prefix(cls):
return "symbol_filledline"
def testRender(self):
s = QgsLineSymbol()
s.deleteSymbolLayer(0)
line = QgsFilledLineSymbolLayer()
# color should be passed to subsymbol
line.setColor(QColor(255, 0, 0))
line.setWidth(8)
s.appendSymbolLayer(line.clone())
g = QgsGeometry.fromWkt('LineString(0 0, 10 10, 10 0)')
rendered_image = self.renderGeometry(s, g)
self.assertTrue(self.image_check('render', 'render', rendered_image))
def testRenderFlatCap(self):
s = QgsLineSymbol()
s.deleteSymbolLayer(0)
line = QgsFilledLineSymbolLayer()
line.setColor(QColor(255, 0, 0))
line.setWidth(8)
line.setPenCapStyle(Qt.FlatCap)
s.appendSymbolLayer(line.clone())
g = QgsGeometry.fromWkt('LineString(0 0, 10 10, 10 0)')
rendered_image = self.renderGeometry(s, g)
self.assertTrue(self.image_check('renderflatcap', 'renderflatcap', rendered_image))
def testRenderMiterJoin(self):
s = QgsLineSymbol()
s.deleteSymbolLayer(0)
line = QgsFilledLineSymbolLayer()
line.setColor(QColor(255, 0, 0))
line.setWidth(8)
line.setPenJoinStyle(Qt.MiterJoin)
s.appendSymbolLayer(line.clone())
g = QgsGeometry.fromWkt('LineString(0 15, 0 0, 10 10, 10 0)')
rendered_image = self.renderGeometry(s, g)
self.assertTrue(self.image_check('render_miter', 'render_miter', rendered_image))
def testRenderBevelJoin(self):
s = QgsLineSymbol()
s.deleteSymbolLayer(0)
line = QgsFilledLineSymbolLayer()
line.setColor(QColor(255, 0, 0))
line.setWidth(8)
line.setPenJoinStyle(Qt.BevelJoin)
s.appendSymbolLayer(line.clone())
g = QgsGeometry.fromWkt('LineString(2 2, 10 10, 10 0)')
rendered_image = self.renderGeometry(s, g)
self.assertTrue(self.image_check('render_bevel', 'render_bevel', rendered_image))
def testLineOffset(self):
s = QgsLineSymbol()
s.deleteSymbolLayer(0)
line = QgsFilledLineSymbolLayer()
line.setColor(QColor(255, 0, 0))
line.setWidth(5)
line.setOffset(5)
s.appendSymbolLayer(line.clone())
g = QgsGeometry.fromWkt('LineString(2 2, 10 10, 10 0)')
rendered_image = self.renderGeometry(s, g)
self.assertTrue(self.image_check('render_offset', 'render_offset', rendered_image))
def renderGeometry(self, symbol, geom, buffer=20):
f = QgsFeature()
f.setGeometry(geom)
image = QImage(200, 200, QImage.Format_RGB32)
painter = QPainter()
ms = QgsMapSettings()
extent = geom.get().boundingBox()
# buffer extent by 10%
if extent.width() > 0:
extent = extent.buffered((extent.height() + extent.width()) / buffer)
else:
extent = extent.buffered(buffer / 2)
ms.setExtent(extent)
ms.setOutputSize(image.size())
context = QgsRenderContext.fromMapSettings(ms)
context.setPainter(painter)
context.setScaleFactor(96 / 25.4) # 96 DPI
context.expressionContext().setFeature(f)
painter.begin(image)
try:
image.fill(QColor(0, 0, 0))
symbol.startRender(context)
symbol.renderFeature(f, context)
symbol.stopRender(context)
finally:
painter.end()
return image
if __name__ == '__main__':
unittest.main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB