Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[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 9, 2018
1 parent ae22554 commit 73d0ced
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 19 deletions.
38 changes: 35 additions & 3 deletions python/core/auto_generated/symbology/qgssymbol.sip.in
Expand Up @@ -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;
Expand Down Expand Up @@ -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/;
Expand Down
24 changes: 18 additions & 6 deletions src/core/symbology/qgssymbol.cpp
Expand Up @@ -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();
Expand All @@ -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 ) )
Expand Down Expand Up @@ -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 );
}
}
Expand Down Expand Up @@ -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 )
Expand Down Expand Up @@ -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 )
Expand Down Expand Up @@ -1574,6 +1583,7 @@ QgsMarkerSymbol *QgsMarkerSymbol::clone() const
cloneSymbol->setLayer( mLayer );
Q_NOWARN_DEPRECATED_POP
cloneSymbol->setClipFeaturesToExtent( mClipFeaturesToExtent );
cloneSymbol->setForceRHR( mForceRHR );
return cloneSymbol;
}

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

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

Expand Down
36 changes: 33 additions & 3 deletions src/core/symbology/qgssymbol.h
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion src/core/symbology/qgssymbollayerutils.cpp
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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++ )
Expand Down
28 changes: 24 additions & 4 deletions src/gui/symbology/qgssymbolslistwidget.cpp
Expand Up @@ -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
Expand Down Expand Up @@ -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 )
Expand Down Expand Up @@ -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!
Expand Down Expand Up @@ -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 )
Expand Down
2 changes: 2 additions & 0 deletions src/gui/symbology/qgssymbolslistwidget.h
Expand Up @@ -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;
Expand Down

0 comments on commit 73d0ced

Please sign in to comment.