Skip to content
Permalink
Browse files

[FEATURE] Add option to force right-hand-rule during polygon symbol r…

…endering

This new option, available under the "Advanced" button for fill symbols,
allows forcing rendered polygons to follow the standard "right hand
rule" for ring orientation (where exterior ring is clockwise, and
interior rings are all counter-clockwise).

The orientation fix is applied while rendering only, and the original
feature geometry is unchanged.

This allows for creation of fill symbols with consistent appearance,
regardless of the dataset being rendered and the ring orientation
of individual features.

Refs #12652
  • Loading branch information
nyalldawson committed Nov 6, 2018
1 parent ae22554 commit 73d0ced5df1f0418e6ab53343046cfcc240a52d4
@@ -370,6 +370,32 @@ side effects for certain symbol types.
.. seealso:: :py:func:`setClipFeaturesToExtent`

.. versionadded:: 2.9
%End

void setForceRHR( bool force );
%Docstring
Sets whether polygon features drawn by the symbol should be reoriented to follow the
standard right-hand-rule orientation, in which the area that is
bounded by the polygon is to the right of the boundary. In particular, the exterior
ring is oriented in a clockwise direction and the interior rings in a counter-clockwise
direction.

.. seealso:: :py:func:`forceRHR`

.. versionadded:: 3.6
%End

bool forceRHR() const;
%Docstring
Returns true if polygon features drawn by the symbol will be reoriented to follow the
standard right-hand-rule orientation, in which the area that is
bounded by the polygon is to the right of the boundary. In particular, the exterior
ring is oriented in a clockwise direction and the interior rings in a counter-clockwise
direction.

.. seealso:: :py:func:`setForceRHR`

.. versionadded:: 3.6
%End

QSet<QString> usedAttributes( const QgsRenderContext &context ) const;
@@ -428,14 +454,20 @@ Creates a point in screen coordinates from a QgsPoint in map coordinates
Creates a line string in screen coordinates from a QgsCurve in map coordinates
%End

static QPolygonF _getPolygonRing( QgsRenderContext &context, const QgsCurve &curve, bool clipToExtent );
static QPolygonF _getPolygonRing( QgsRenderContext &context, const QgsCurve &curve, bool clipToExtent, bool isExteriorRing = false, bool correctRingOrientation = false );
%Docstring
Creates a polygon ring in screen coordinates from a QgsCurve in map coordinates
Creates a polygon ring in screen coordinates from a QgsCurve in map coordinates.

If ``correctRingOrientation`` is true then the ring will be oriented to match standard ring orientation, e.g.
clockwise for exterior rings and counter-clockwise for interior rings.
%End

static void _getPolygon( QPolygonF &pts, QList<QPolygonF> &holes, QgsRenderContext &context, const QgsPolygon &polygon, bool clipToExtent = true );
static void _getPolygon( QPolygonF &pts, QList<QPolygonF> &holes, QgsRenderContext &context, const QgsPolygon &polygon, bool clipToExtent = true, bool correctRingOrientation = false );
%Docstring
Creates a polygon in screen coordinates from a QgsPolygonXYin map coordinates

If ``correctRingOrientation`` is true then the ring will be oriented to match standard ring orientation, e.g.
clockwise for exterior rings and counter-clockwise for interior rings.
%End

QgsSymbolLayerList cloneLayers() const /Factory/;
@@ -141,7 +141,7 @@ QPolygonF QgsSymbol::_getLineString( QgsRenderContext &context, const QgsCurve &
return pts;
}

QPolygonF QgsSymbol::_getPolygonRing( QgsRenderContext &context, const QgsCurve &curve, bool clipToExtent )
QPolygonF QgsSymbol::_getPolygonRing( QgsRenderContext &context, const QgsCurve &curve, const bool clipToExtent, const bool isExteriorRing, const bool correctRingOrientation )
{
const QgsCoordinateTransform ct = context.coordinateTransform();
const QgsMapToPixel &mtp = context.mapToPixel();
@@ -155,6 +155,15 @@ QPolygonF QgsSymbol::_getPolygonRing( QgsRenderContext &context, const QgsCurve
if ( curve.numPoints() < 1 )
return QPolygonF();

if ( correctRingOrientation )
{
// ensure consistent polygon ring orientation
if ( isExteriorRing && curve.orientation() != QgsCurve::Clockwise )
std::reverse( poly.begin(), poly.end() );
else if ( !isExteriorRing && curve.orientation() != QgsCurve::CounterClockwise )
std::reverse( poly.begin(), poly.end() );
}

//clip close to view extent, if needed
const QRectF ptsRect = poly.boundingRect();
if ( clipToExtent && !context.extent().contains( ptsRect ) )
@@ -184,14 +193,14 @@ QPolygonF QgsSymbol::_getPolygonRing( QgsRenderContext &context, const QgsCurve
return poly;
}

void QgsSymbol::_getPolygon( QPolygonF &pts, QList<QPolygonF> &holes, QgsRenderContext &context, const QgsPolygon &polygon, bool clipToExtent )
void QgsSymbol::_getPolygon( QPolygonF &pts, QList<QPolygonF> &holes, QgsRenderContext &context, const QgsPolygon &polygon, const bool clipToExtent, const bool correctRingOrientation )
{
holes.clear();

pts = _getPolygonRing( context, *polygon.exteriorRing(), clipToExtent );
pts = _getPolygonRing( context, *polygon.exteriorRing(), clipToExtent, true, correctRingOrientation );
for ( int idx = 0; idx < polygon.numInteriorRings(); idx++ )
{
const QPolygonF hole = _getPolygonRing( context, *( polygon.interiorRing( idx ) ), clipToExtent );
const QPolygonF hole = _getPolygonRing( context, *( polygon.interiorRing( idx ) ), clipToExtent, false, correctRingOrientation );
if ( !hole.isEmpty() ) holes.append( hole );
}
}
@@ -850,7 +859,7 @@ void QgsSymbol::renderFeature( const QgsFeature &feature, QgsRenderContext &cont
QgsDebugMsg( QStringLiteral( "cannot render polygon with no exterior ring" ) );
break;
}
_getPolygon( pts, holes, context, polygon, !tileMapRendering && clipFeaturesToExtent() );
_getPolygon( pts, holes, context, polygon, !tileMapRendering && clipFeaturesToExtent(), mForceRHR );
static_cast<QgsFillSymbol *>( this )->renderPolygon( pts, ( !holes.isEmpty() ? &holes : nullptr ), &feature, context, layer, selected );

if ( drawVertexMarker && !usingSegmentizedGeometry )
@@ -980,7 +989,7 @@ void QgsSymbol::renderFeature( const QgsFeature &feature, QgsRenderContext &cont
if ( !polygon.exteriorRing() )
break;

_getPolygon( pts, holes, context, polygon, !tileMapRendering && clipFeaturesToExtent() );
_getPolygon( pts, holes, context, polygon, !tileMapRendering && clipFeaturesToExtent(), mForceRHR );
static_cast<QgsFillSymbol *>( this )->renderPolygon( pts, ( !holes.isEmpty() ? &holes : nullptr ), &feature, context, layer, selected );

if ( drawVertexMarker && !usingSegmentizedGeometry )
@@ -1574,6 +1583,7 @@ QgsMarkerSymbol *QgsMarkerSymbol::clone() const
cloneSymbol->setLayer( mLayer );
Q_NOWARN_DEPRECATED_POP
cloneSymbol->setClipFeaturesToExtent( mClipFeaturesToExtent );
cloneSymbol->setForceRHR( mForceRHR );
return cloneSymbol;
}

@@ -1793,6 +1803,7 @@ QgsLineSymbol *QgsLineSymbol::clone() const
cloneSymbol->setLayer( mLayer );
Q_NOWARN_DEPRECATED_POP
cloneSymbol->setClipFeaturesToExtent( mClipFeaturesToExtent );
cloneSymbol->setForceRHR( mForceRHR );
return cloneSymbol;
}

@@ -1913,6 +1924,7 @@ QgsFillSymbol *QgsFillSymbol::clone() const
cloneSymbol->setLayer( mLayer );
Q_NOWARN_DEPRECATED_POP
cloneSymbol->setClipFeaturesToExtent( mClipFeaturesToExtent );
cloneSymbol->setForceRHR( mForceRHR );
return cloneSymbol;
}

@@ -379,6 +379,28 @@ class CORE_EXPORT QgsSymbol
*/
bool clipFeaturesToExtent() const { return mClipFeaturesToExtent; }

/**
* Sets whether polygon features drawn by the symbol should be reoriented to follow the
* standard right-hand-rule orientation, in which the area that is
* bounded by the polygon is to the right of the boundary. In particular, the exterior
* ring is oriented in a clockwise direction and the interior rings in a counter-clockwise
* direction.
* \see forceRHR()
* \since QGIS 3.6
*/
void setForceRHR( bool force ) { mForceRHR = force; }

/**
* Returns true if polygon features drawn by the symbol will be reoriented to follow the
* standard right-hand-rule orientation, in which the area that is
* bounded by the polygon is to the right of the boundary. In particular, the exterior
* ring is oriented in a clockwise direction and the interior rings in a counter-clockwise
* direction.
* \see setForceRHR()
* \since QGIS 3.6
*/
bool forceRHR() const { return mForceRHR; }

/**
* Returns a list of attributes required to render this feature.
* This should include any attributes required by the symbology including
@@ -447,14 +469,21 @@ class CORE_EXPORT QgsSymbol
static QPolygonF _getLineString( QgsRenderContext &context, const QgsCurve &curve, bool clipToExtent = true );

/**
* Creates a polygon ring in screen coordinates from a QgsCurve in map coordinates
* Creates a polygon ring in screen coordinates from a QgsCurve in map coordinates.
*
* If \a correctRingOrientation is true then the ring will be oriented to match standard ring orientation, e.g.
* clockwise for exterior rings and counter-clockwise for interior rings.
*/
static QPolygonF _getPolygonRing( QgsRenderContext &context, const QgsCurve &curve, bool clipToExtent );
static QPolygonF _getPolygonRing( QgsRenderContext &context, const QgsCurve &curve, bool clipToExtent, bool isExteriorRing = false, bool correctRingOrientation = false );

/**
* Creates a polygon in screen coordinates from a QgsPolygonXYin map coordinates
*
* If \a correctRingOrientation is true then the ring will be oriented to match standard ring orientation, e.g.
* clockwise for exterior rings and counter-clockwise for interior rings.
*
*/
static void _getPolygon( QPolygonF &pts, QList<QPolygonF> &holes, QgsRenderContext &context, const QgsPolygon &polygon, bool clipToExtent = true );
static void _getPolygon( QPolygonF &pts, QList<QPolygonF> &holes, QgsRenderContext &context, const QgsPolygon &polygon, bool clipToExtent = true, bool correctRingOrientation = false );

/**
* Retrieve a cloned list of all layers that make up this symbol.
@@ -487,6 +516,7 @@ class CORE_EXPORT QgsSymbol

RenderHints mRenderHints = nullptr;
bool mClipFeaturesToExtent = true;
bool mForceRHR = false;

Q_DECL_DEPRECATED const QgsVectorLayer *mLayer = nullptr; //current vectorlayer

@@ -960,7 +960,7 @@ QgsSymbol *QgsSymbolLayerUtils::loadSymbol( const QDomElement &element, const Qg
}
symbol->setOpacity( element.attribute( QStringLiteral( "alpha" ), QStringLiteral( "1.0" ) ).toDouble() );
symbol->setClipFeaturesToExtent( element.attribute( QStringLiteral( "clip_to_extent" ), QStringLiteral( "1" ) ).toInt() );

symbol->setForceRHR( element.attribute( QStringLiteral( "force_rhr" ), QStringLiteral( "0" ) ).toInt() );
return symbol;
}

@@ -1031,6 +1031,7 @@ QDomElement QgsSymbolLayerUtils::saveSymbol( const QString &name, QgsSymbol *sym
symEl.setAttribute( QStringLiteral( "name" ), name );
symEl.setAttribute( QStringLiteral( "alpha" ), QString::number( symbol->opacity() ) );
symEl.setAttribute( QStringLiteral( "clip_to_extent" ), symbol->clipFeaturesToExtent() ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
symEl.setAttribute( QStringLiteral( "force_rhr" ), symbol->forceRHR() ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
//QgsDebugMsg( "num layers " + QString::number( symbol->symbolLayerCount() ) );

for ( int i = 0; i < symbol->symbolLayerCount(); i++ )
@@ -117,6 +117,9 @@ QgsSymbolsListWidget::QgsSymbolsListWidget( QgsSymbol *symbol, QgsStyle *style,
mClipFeaturesAction = new QAction( tr( "Clip Features to Canvas Extent" ), this );
mClipFeaturesAction->setCheckable( true );
connect( mClipFeaturesAction, &QAction::toggled, this, &QgsSymbolsListWidget::clipFeaturesToggled );
mStandardizeRingsAction = new QAction( tr( "Force Right-Hand-Rule Orientation" ), this );
mStandardizeRingsAction->setCheckable( true );
connect( mStandardizeRingsAction, &QAction::toggled, this, &QgsSymbolsListWidget::forceRHRToggled );

double iconSize = Qgis::UI_SCALE_FACTOR * fontMetrics().width( 'X' ) * 10;
viewSymbols->setIconSize( QSize( static_cast< int >( iconSize ), static_cast< int >( iconSize * 0.9 ) ) ); // ~100, 90 on low dpi
@@ -218,6 +221,7 @@ QgsSymbolsListWidget::~QgsSymbolsListWidget()
// This action was added to the menu by this widget, clean it up
// The menu can be passed in the constructor, so may live longer than this widget
btnAdvanced->menu()->removeAction( mClipFeaturesAction );
btnAdvanced->menu()->removeAction( mStandardizeRingsAction );
}

void QgsSymbolsListWidget::registerDataDefinedButton( QgsPropertyOverrideButton *button, QgsSymbolLayer::Property key )
@@ -394,6 +398,15 @@ void QgsSymbolsListWidget::updateModelFilters()
}
}

void QgsSymbolsListWidget::forceRHRToggled( bool checked )
{
if ( !mSymbol )
return;

mSymbol->setForceRHR( checked );
emit changed();
}

void QgsSymbolsListWidget::openStyleManager()
{
// prefer to use global window manager to open the style manager, if possible!
@@ -687,27 +700,34 @@ void QgsSymbolsListWidget::updateSymbolInfo()

mOpacityWidget->setOpacity( mSymbol->opacity() );

// Remove all previous clip actions
// Clean up previous advanced symbol actions
const QList<QAction *> actionList( btnAdvanced->menu()->actions() );
for ( const auto &action : actionList )
{
if ( mClipFeaturesAction->text() == action->text() )
{
btnAdvanced->menu()->removeAction( action );
}
else if ( mStandardizeRingsAction->text() == action->text() )
{
btnAdvanced->menu()->removeAction( action );
}
}

if ( mSymbol->type() == QgsSymbol::Line || mSymbol->type() == QgsSymbol::Fill )
{
//add clip features option for line or fill symbols
btnAdvanced->menu()->addAction( mClipFeaturesAction );
}
if ( mSymbol->type() == QgsSymbol::Fill )
{
btnAdvanced->menu()->addAction( mStandardizeRingsAction );
}

btnAdvanced->setVisible( mAdvancedMenu || !btnAdvanced->menu()->isEmpty() );

mClipFeaturesAction->blockSignals( true );
mClipFeaturesAction->setChecked( mSymbol->clipFeaturesToExtent() );
mClipFeaturesAction->blockSignals( false );
whileBlocking( mClipFeaturesAction )->setChecked( mSymbol->clipFeaturesToExtent() );
whileBlocking( mStandardizeRingsAction )->setChecked( mSymbol->forceRHR() );
}

void QgsSymbolsListWidget::setSymbolFromStyle( const QModelIndex &index )
@@ -118,13 +118,15 @@ class GUI_EXPORT QgsSymbolsListWidget : public QWidget, private Ui::SymbolsListW
void opacityChanged( double value );
void createAuxiliaryField();
void updateModelFilters();
void forceRHRToggled( bool checked );

private:
QgsSymbol *mSymbol = nullptr;
std::shared_ptr< QgsSymbol > mAssistantSymbol;
QgsStyle *mStyle = nullptr;
QMenu *mAdvancedMenu = nullptr;
QAction *mClipFeaturesAction = nullptr;
QAction *mStandardizeRingsAction = nullptr;
QgsVectorLayer *mLayer = nullptr;
QgsMapCanvas *mMapCanvas = nullptr;
QgsStyleProxyModel *mModel = nullptr;

0 comments on commit 73d0ced

Please sign in to comment.
You can’t perform that action at this time.