Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Interpolated line renderer: don't assume that a feature is available
Instead we should ALWAYS use the provided points for rendering symbol
layers, or the symbol layer will be broken in various circumstances
(e.g. when used outside of a vector layer)

Fixes #45028
  • Loading branch information
nyalldawson committed Sep 21, 2021
1 parent cc240e1 commit 9e68b45
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 90 deletions.
Expand Up @@ -260,14 +260,28 @@ Returns the stroke color used to render
%Docstring
Renders a line in the ``context`` between ``point1`` and ``point2``
with color and width that vary depending on ``value1`` and ``value2``

This method assumes that ``point1`` and ``point2`` are in map units. See :py:func:`~QgsInterpolatedLineRenderer.renderInDeviceCoordinates` for an equivalent
method which renders lines in painter coordinates.
%End

void render( double valueColor1, double valueColor2, double valueWidth1, double valueWidth2, const QgsPointXY &point1, const QgsPointXY &point2, QgsRenderContext &context ) const;
%Docstring
Renders a line in the ``context`` between ``point1`` and ``point2``
with color that varies depending on ``valueColor1`` and ``valueColor2`` and and width that varies between ``valueWidth1`` and ``valueWidth2``

This method assumes that ``point1`` and ``point2`` are in map units. See :py:func:`~QgsInterpolatedLineRenderer.renderInDeviceCoordinates` for an equivalent
method which renders lines in painter coordinates.

.. versionadded:: 3.20
%End

void renderInDeviceCoordinates( double valueColor1, double valueColor2, double valueWidth1, double valueWidth2, QPointF point1, QPointF point2, QgsRenderContext &context ) const;
%Docstring
Renders a line in the ``context`` between ``point1`` and ``point2`` in device (painter) coordinates
with color that varies depending on ``valueColor1`` and ``valueColor2`` and and width that varies between ``valueWidth1`` and ``valueWidth2``.

.. versionadded:: 3.22
%End

void setSelected( bool selected );
Expand Down
149 changes: 65 additions & 84 deletions src/core/symbology/qgsinterpolatedlinerenderer.cpp
Expand Up @@ -804,10 +804,10 @@ QString QgsInterpolatedLineSymbolLayer::layerType() const {return QStringLiteral
void QgsInterpolatedLineSymbolLayer::startRender( QgsSymbolRenderContext &context )
{
// find out attribute index from name
mStartWidthAttributeIndex = mFields.lookupField( mStartWidthExpressionString );
mEndWidthAttributeIndex = mFields.lookupField( mEndWidthExpressionString );
mStartColorAttributeIndex = mFields.lookupField( mStartColorExpressionString );
mEndColorAttributeIndex = mFields.lookupField( mEndColorExpressionString );
mStartWidthAttributeIndex = -1; //mFields.lookupField( mStartWidthExpressionString );
mEndWidthAttributeIndex = -1; //mFields.lookupField( mEndWidthExpressionString );
mStartColorAttributeIndex = -1; //mFields.lookupField( mStartColorExpressionString );
mEndColorAttributeIndex = -1; //mFields.lookupField( mEndColorExpressionString );

if ( mStartWidthAttributeIndex == -1 )
{
Expand Down Expand Up @@ -932,8 +932,6 @@ void QgsInterpolatedLineSymbolLayer::drawPreviewIcon( QgsSymbolRenderContext &co
{
QgsGeometry geometry = context.patchShape() ? context.patchShape()->geometry()
: QgsStyle::defaultStyle()->defaultPatch( Qgis::SymbolType::Line, size ).geometry();
QgsFeature feature;
feature.setGeometry( geometry );

startRender( context );
mStartWidthAttributeIndex = -1;
Expand Down Expand Up @@ -1118,74 +1116,38 @@ QgsColorRampShader QgsInterpolatedLineSymbolLayer::createColorRampShaderFromProp
QgsInterpolatedLineSymbolLayer::QgsInterpolatedLineSymbolLayer(): QgsLineSymbolLayer( true ) {}


void QgsInterpolatedLineSymbolLayer::startFeatureRender( const QgsFeature &feature, QgsRenderContext & )
void QgsInterpolatedLineSymbolLayer::startFeatureRender( const QgsFeature &, QgsRenderContext & )
{
mFeature = feature;
mRenderingFeature = true;
mLineParts.clear();
}

void QgsInterpolatedLineSymbolLayer::stopFeatureRender( const QgsFeature &, QgsRenderContext & )
void QgsInterpolatedLineSymbolLayer::stopFeatureRender( const QgsFeature &, QgsRenderContext &context )
{
mFeature = QgsFeature();
mRenderingFeature = false;

if ( mLineParts.empty() )
return;

render( mLineParts, context );
mLineParts.clear();
}

void QgsInterpolatedLineSymbolLayer::renderPolyline( const QPolygonF &points, QgsSymbolRenderContext &context )
void QgsInterpolatedLineSymbolLayer::render( const QVector< QPolygonF > &parts, QgsRenderContext &context )
{
Q_UNUSED( points ); //this symbol layer need to used all the feature geometry, not clipped/simplified geometry
const double totalLength = std::accumulate( parts.begin(), parts.end(), 0.0, []( double total, const QPolygonF & part )
{
return total + QgsSymbolLayerUtils::polylineLength( part );
} );

QVector<QgsPolylineXY> lineStrings;
if ( qgsDoubleNear( totalLength, 0 ) )
return;

double startValWidth = 0;
double endValWidth = 0;
double variationPerMapUnitWidth = 0;
double startValColor = 0;
double endValColor = 0;
double variationPerMapUnitColor = 0;

QgsRenderContext renderContext = context.renderContext();

QgsGeometry geom = mFeature.geometry();

mLineRender.setSelected( context.selected() );

if ( geom.isEmpty() )
return;

switch ( QgsWkbTypes::flatType( geom.wkbType() ) )
{
case QgsWkbTypes::Unknown:
case QgsWkbTypes::Point:
case QgsWkbTypes::Polygon:
case QgsWkbTypes::Triangle:
case QgsWkbTypes::MultiPoint:
case QgsWkbTypes::MultiPolygon:
case QgsWkbTypes::GeometryCollection:
case QgsWkbTypes::CurvePolygon:
case QgsWkbTypes::MultiSurface:
case QgsWkbTypes::NoGeometry:
return;
break;
case QgsWkbTypes::LineString:
case QgsWkbTypes::CircularString:
case QgsWkbTypes::CompoundCurve:
lineStrings.append( geom.asPolyline() );
break;
case QgsWkbTypes::MultiCurve:
case QgsWkbTypes::MultiLineString:
lineStrings = geom.asMultiPolyline();
break;
default:
return;
break;
}

QgsExpressionContext expressionContext = renderContext.expressionContext();
expressionContext.setFeature( mFeature );

double totalLength = geom.length();

if ( totalLength == 0 )
return;

QVariant val1WidthVariant;
QVariant val2WidthVariant;
QVariant val1ColorVariant;
Expand All @@ -1196,19 +1158,19 @@ void QgsInterpolatedLineSymbolLayer::renderPolyline( const QPolygonF &points, Qg
{
if ( mStartWidthExpression )
{
val1WidthVariant = mStartWidthExpression->evaluate( &expressionContext );
val1WidthVariant = mStartWidthExpression->evaluate( &context.expressionContext() );
ok |= mStartWidthExpression->hasEvalError();
}
else
val1WidthVariant = mFeature.attribute( mStartWidthAttributeIndex );
//else
// val1WidthVariant = mFeature.attribute( mStartWidthAttributeIndex );

if ( mEndWithExpression )
{
val2WidthVariant = mEndWithExpression->evaluate( &expressionContext );
val2WidthVariant = mEndWithExpression->evaluate( &context.expressionContext() );
ok |= mEndWithExpression->hasEvalError();
}
else
val2WidthVariant = mFeature.attribute( mEndWidthAttributeIndex );
//else
// val2WidthVariant = mFeature.attribute( mEndWidthAttributeIndex );

if ( !ok )
return;
Expand All @@ -1217,7 +1179,7 @@ void QgsInterpolatedLineSymbolLayer::renderPolyline( const QPolygonF &points, Qg
if ( !ok )
return;

endValWidth = val2WidthVariant.toDouble( &ok );
const double endValWidth = val2WidthVariant.toDouble( &ok );
if ( !ok )
return;

Expand All @@ -1228,48 +1190,67 @@ void QgsInterpolatedLineSymbolLayer::renderPolyline( const QPolygonF &points, Qg
{
if ( mStartColorExpression )
{
val1ColorVariant = mStartColorExpression->evaluate( &expressionContext );
val1ColorVariant = mStartColorExpression->evaluate( &context.expressionContext() );
ok |= mStartColorExpression->hasEvalError();
}
else
val1ColorVariant = mFeature.attribute( mStartColorAttributeIndex );
// else
// val1ColorVariant = mFeature.attribute( mStartColorAttributeIndex );

if ( mEndColorExpression )
{
val2ColorVariant = mEndColorExpression->evaluate( &expressionContext );
val2ColorVariant = mEndColorExpression->evaluate( &context.expressionContext() );
ok |= mEndColorExpression->hasEvalError();
}
else
val2ColorVariant = mFeature.attribute( mEndColorAttributeIndex );
// else
// val2ColorVariant = mFeature.attribute( mEndColorAttributeIndex );

startValColor = val1ColorVariant.toDouble( &ok );
if ( !ok )
return;

endValColor = val2ColorVariant.toDouble( &ok );
const double endValColor = val2ColorVariant.toDouble( &ok );
if ( !ok )
return;

variationPerMapUnitColor = ( endValColor - startValColor ) / totalLength;
}

for ( const QgsPolylineXY &poly : std::as_const( lineStrings ) )
for ( const QPolygonF &poly : parts )
{
double lengthFromStart = 0;
for ( int i = 1; i < poly.count(); ++i )
{
QgsPointXY p1 = poly.at( i - 1 );
QgsPointXY p2 = poly.at( i );

double v1c = startValColor + variationPerMapUnitColor * lengthFromStart;
double v1w = startValWidth + variationPerMapUnitWidth * lengthFromStart;
lengthFromStart += p1.distance( p2 );
double v2c = startValColor + variationPerMapUnitColor * lengthFromStart;
double v2w = startValWidth + variationPerMapUnitWidth * lengthFromStart;
mLineRender.render( v1c, v2c, v1w, v2w, p1, p2, renderContext );
const QPointF p1 = poly.at( i - 1 );
const QPointF p2 = poly.at( i );

const double v1c = startValColor + variationPerMapUnitColor * lengthFromStart;
const double v1w = startValWidth + variationPerMapUnitWidth * lengthFromStart;
lengthFromStart += std::sqrt( ( p1.x() - p2.x() ) * ( p1.x() - p2.x() ) + ( p1.y() - p2.y() ) * ( p1.y() - p2.y() ) );
const double v2c = startValColor + variationPerMapUnitColor * lengthFromStart;
const double v2w = startValWidth + variationPerMapUnitWidth * lengthFromStart;
mLineRender.renderInDeviceCoordinates( v1c, v2c, v1w, v2w, p1, p2, context );
}
}
}

void QgsInterpolatedLineSymbolLayer::renderPolyline( const QPolygonF &points, QgsSymbolRenderContext &context )
{
mLineRender.setSelected( context.selected() );

if ( points.empty() )
return;

if ( mRenderingFeature )
{
// in the middle of rendering a possibly multi-part feature, so we collect all the parts and defer the actual rendering
// until after we've received the final part
mLineParts.append( points );
}
else
{
// not rendering a feature, so we can just render the polyline immediately
render( { points }, context.renderContext() );
}
}

bool QgsInterpolatedLineSymbolLayer::isCompatibleWithSymbol( QgsSymbol *symbol ) const
Expand Down
26 changes: 20 additions & 6 deletions src/core/symbology/qgsinterpolatedlinerenderer.h
Expand Up @@ -231,17 +231,31 @@ class CORE_EXPORT QgsInterpolatedLineRenderer
/**
* Renders a line in the \a context between \a point1 and \a point2
* with color and width that vary depending on \a value1 and \a value2
*
* This method assumes that \a point1 and \a point2 are in map units. See renderInDeviceCoordinates() for an equivalent
* method which renders lines in painter coordinates.
*/
void render( double value1, double value2, const QgsPointXY &point1, const QgsPointXY &point2, QgsRenderContext &context ) const;

/**
* Renders a line in the \a context between \a point1 and \a point2
* with color that varies depending on \a valueColor1 and \a valueColor2 and and width that varies between \a valueWidth1 and \a valueWidth2
*
* This method assumes that \a point1 and \a point2 are in map units. See renderInDeviceCoordinates() for an equivalent
* method which renders lines in painter coordinates.
*
* \since QGIS 3.20
*/
void render( double valueColor1, double valueColor2, double valueWidth1, double valueWidth2, const QgsPointXY &point1, const QgsPointXY &point2, QgsRenderContext &context ) const;

/**
* Renders a line in the \a context between \a point1 and \a point2 in device (painter) coordinates
* with color that varies depending on \a valueColor1 and \a valueColor2 and and width that varies between \a valueWidth1 and \a valueWidth2.
*
* \since QGIS 3.22
*/
void renderInDeviceCoordinates( double valueColor1, double valueColor2, double valueWidth1, double valueWidth2, QPointF point1, QPointF point2, QgsRenderContext &context ) const;

/**
* Sets if the rendering must be done as the element is selected
*
Expand All @@ -257,11 +271,6 @@ class CORE_EXPORT QgsInterpolatedLineRenderer
void adjustLine( double value, double value1, double value2, double &width, double &adjusting ) const;
bool mSelected = false;

/**
* Renders a line in the \a context between \a point1 and \a point2 in device coordinates
* with color and width that vary depending on \a value1 and \a value2
*/
void renderInDeviceCoordinate( double valueColor1, double valueColor2, double valueWidth1, double valueWidth2, const QPointF &p1, const QPointF &p2, QgsRenderContext &context ) const;

friend class QgsInterpolatedLineSymbolLayer;
};
Expand Down Expand Up @@ -340,6 +349,7 @@ class CORE_EXPORT QgsInterpolatedLineSymbolLayer : public QgsLineSymbolLayer
#endif

QgsInterpolatedLineRenderer mLineRender;

QString mStartWidthExpressionString;
QString mEndWidthExpressionString;
QString mStartColorExpressionString;
Expand All @@ -354,7 +364,11 @@ class CORE_EXPORT QgsInterpolatedLineSymbolLayer : public QgsLineSymbolLayer
std::unique_ptr<QgsExpression> mEndWithExpression;
std::unique_ptr<QgsExpression> mStartColorExpression;
std::unique_ptr<QgsExpression> mEndColorExpression;
QgsFeature mFeature;

QVector< QPolygonF > mLineParts;
bool mRenderingFeature = false;

void render( const QVector< QPolygonF > &parts, QgsRenderContext &context );

QVariant colorRampShaderProperties() const;
static QgsColorRampShader createColorRampShaderFromProperties( const QVariant &properties );
Expand Down

0 comments on commit 9e68b45

Please sign in to comment.