Skip to content

Commit 75635db

Browse files
committedJul 24, 2020
[FEATURE][symbology] Add options to dynamically tweak dash pattern
in simple line symbol layers New options are: - Align dash pattern to line length: If checked, the dash pattern lengths will be subtely adjusted in order to ensure that when a line is rendered it will end with a complete dash element, instead of a gap element or partial dash element - Tweak dash pattern at sharp corners: If checked, this option dynamically adjusts the dash pattern placement so that sharp corners are represented by a full dash element coming into and out of the sharp corner. It's designed to better represent the underlying geometry while rendering dashed lines, especially for jagged lines
1 parent 0641f3f commit 75635db

File tree

5 files changed

+666
-185
lines changed

5 files changed

+666
-185
lines changed
 

‎python/core/auto_generated/symbology/qgslinesymbollayer.sip.in

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,64 @@ This setting only has an effect when the line symbol is being
311311
used to render polygon rings.
312312

313313
.. seealso:: :py:func:`drawInsidePolygon`
314+
%End
315+
316+
bool alignDashPattern() const;
317+
%Docstring
318+
Returns ``True`` if dash patterns should be aligned to the start and end of lines, by
319+
applying subtle tweaks to the pattern sizing in order to ensure that the end of
320+
a line is represented by a complete dash element.
321+
322+
.. seealso:: :py:func:`setAlignDashPattern`
323+
324+
.. seealso:: :py:func:`tweakDashPatternOnCorners`
325+
326+
.. versionadded:: 3.16
327+
%End
328+
329+
void setAlignDashPattern( bool enabled );
330+
%Docstring
331+
Sets whether dash patterns should be aligned to the start and end of lines, by
332+
applying subtle tweaks to the pattern sizing in order to ensure that the end of
333+
a line is represented by a complete dash element.
334+
335+
.. seealso:: :py:func:`alignDashPattern`
336+
337+
.. seealso:: :py:func:`setTweakDashPatternOnCorners`
338+
339+
.. versionadded:: 3.16
340+
%End
341+
342+
bool tweakDashPatternOnCorners() const;
343+
%Docstring
344+
Returns ``True`` if dash patterns tweaks should be applied on sharp corners, to ensure
345+
that a double-length dash is drawn running into and out of the corner.
346+
347+
.. note::
348+
349+
This setting is only applied if :py:func:`~QgsSimpleLineSymbolLayer.alignDashPattern` is ``True``.
350+
351+
.. seealso:: :py:func:`setTweakDashPatternOnCorners`
352+
353+
.. seealso:: :py:func:`alignDashPattern`
354+
355+
.. versionadded:: 3.16
356+
%End
357+
358+
void setTweakDashPatternOnCorners( bool enabled );
359+
%Docstring
360+
Sets whether dash patterns tweaks should be applied on sharp corners, to ensure
361+
that a double-length dash is drawn running into and out of the corner.
362+
363+
.. note::
364+
365+
This setting is only applied if :py:func:`~QgsSimpleLineSymbolLayer.alignDashPattern` is ``True``.
366+
367+
.. seealso:: :py:func:`tweakDashPatternOnCorners`
368+
369+
.. seealso:: :py:func:`setAlignDashPattern`
370+
371+
.. versionadded:: 3.16
314372
%End
315373

316374
};

‎src/core/symbology/qgslinesymbollayer.cpp

Lines changed: 351 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include "qgsproperty.h"
2828
#include "qgsexpressioncontextutils.h"
2929

30+
#include <algorithm>
3031
#include <QPainter>
3132
#include <QDomDocument>
3233
#include <QDomElement>
@@ -184,6 +185,12 @@ QgsSymbolLayer *QgsSimpleLineSymbolLayer::create( const QgsStringMap &props )
184185
if ( props.contains( QStringLiteral( "dash_pattern_offset_map_unit_scale" ) ) )
185186
l->setDashPatternOffsetMapUnitScale( QgsSymbolLayerUtils::decodeMapUnitScale( props[QStringLiteral( "dash_pattern_offset_map_unit_scale" )] ) );
186187

188+
if ( props.contains( QStringLiteral( "align_dash_pattern" ) ) )
189+
l->setAlignDashPattern( props[ QStringLiteral( "align_dash_pattern" )].toInt() );
190+
191+
if ( props.contains( QStringLiteral( "tweak_dash_pattern_on_corners" ) ) )
192+
l->setTweakDashPatternOnCorners( props[ QStringLiteral( "tweak_dash_pattern_on_corners" )].toInt() );
193+
187194
l->restoreOldDataDefinedProperties( props );
188195

189196
return l;
@@ -327,36 +334,37 @@ void QgsSimpleLineSymbolLayer::renderPolyline( const QPolygonF &points, QgsSymbo
327334
double offset = mOffset;
328335
applyDataDefinedSymbology( context, mPen, mSelPen, offset );
329336

330-
p->setPen( context.selected() ? mSelPen : mPen );
337+
const QPen pen = context.selected() ? mSelPen : mPen;
331338
p->setBrush( Qt::NoBrush );
332339

340+
const bool antialiasingWasEnabled = p->testRenderHint( QPainter::Antialiasing );
333341
// Disable 'Antialiasing' if the geometry was generalized in the current RenderContext (We known that it must have least #2 points).
334342
if ( points.size() <= 2 &&
335343
( context.renderContext().vectorSimplifyMethod().simplifyHints() & QgsVectorSimplifyMethod::AntialiasingSimplification ) &&
336344
QgsAbstractGeometrySimplifier::isGeneralizableByDeviceBoundingBox( points, context.renderContext().vectorSimplifyMethod().threshold() ) &&
337345
( p->renderHints() & QPainter::Antialiasing ) )
338346
{
339347
p->setRenderHint( QPainter::Antialiasing, false );
340-
#if 0
341-
p->drawPolyline( points );
342-
#else
343-
QPainterPath path;
344-
path.addPolygon( points );
345-
p->drawPath( path );
346-
#endif
347-
p->setRenderHint( QPainter::Antialiasing, true );
348-
return;
349348
}
350349

350+
const bool applyPatternTweaks = mAlignDashPattern
351+
&& pen.widthF() > 1.0
352+
&& ( pen.style() != Qt::SolidLine || !pen.dashPattern().empty() )
353+
&& pen.dashOffset() == 0;
354+
351355
if ( qgsDoubleNear( offset, 0 ) )
352356
{
353-
#if 0
354-
p->drawPolyline( points );
355-
#else
356-
QPainterPath path;
357-
path.addPolygon( points );
358-
p->drawPath( path );
359-
#endif
357+
if ( applyPatternTweaks )
358+
{
359+
drawPathWithDashPatternTweaks( p, points, pen );
360+
}
361+
else
362+
{
363+
p->setPen( pen );
364+
QPainterPath path;
365+
path.addPolygon( points );
366+
p->drawPath( path );
367+
}
360368
}
361369
else
362370
{
@@ -368,18 +376,24 @@ void QgsSimpleLineSymbolLayer::renderPolyline( const QPolygonF &points, QgsSymbo
368376
scaledOffset = std::min( std::max( context.renderContext().convertToPainterUnits( offset, QgsUnitTypes::RenderMillimeters ), 3.0 ), 100.0 );
369377
}
370378

379+
p->setPen( pen );
371380
QList<QPolygonF> mline = ::offsetLine( points, scaledOffset, context.originalGeometryType() != QgsWkbTypes::UnknownGeometry ? context.originalGeometryType() : QgsWkbTypes::LineGeometry );
372-
for ( int part = 0; part < mline.count(); ++part )
381+
for ( const QPolygonF &part : mline )
373382
{
374-
#if 0
375-
p->drawPolyline( mline );
376-
#else
377-
QPainterPath path;
378-
path.addPolygon( mline[ part ] );
379-
p->drawPath( path );
380-
#endif
383+
if ( applyPatternTweaks )
384+
{
385+
drawPathWithDashPatternTweaks( p, part, pen );
386+
}
387+
else
388+
{
389+
QPainterPath path;
390+
path.addPolygon( part );
391+
p->drawPath( path );
392+
}
381393
}
382394
}
395+
396+
p->setRenderHint( QPainter::Antialiasing, antialiasingWasEnabled );
383397
}
384398

385399
QgsStringMap QgsSimpleLineSymbolLayer::properties() const
@@ -404,6 +418,8 @@ QgsStringMap QgsSimpleLineSymbolLayer::properties() const
404418
map[QStringLiteral( "dash_pattern_offset_map_unit_scale" )] = QgsSymbolLayerUtils::encodeMapUnitScale( mDashPatternOffsetMapUnitScale );
405419
map[QStringLiteral( "draw_inside_polygon" )] = ( mDrawInsidePolygon ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
406420
map[QStringLiteral( "ring_filter" )] = QString::number( static_cast< int >( mRingFilter ) );
421+
map[QStringLiteral( "align_dash_pattern" )] = mAlignDashPattern ? QStringLiteral( "1" ) : QStringLiteral( "0" );
422+
map[QStringLiteral( "tweak_dash_pattern_on_corners" )] = mPatternCartographicTweakOnSharpCorners ? QStringLiteral( "1" ) : QStringLiteral( "0" );
407423
return map;
408424
}
409425

@@ -426,6 +442,9 @@ QgsSimpleLineSymbolLayer *QgsSimpleLineSymbolLayer::clone() const
426442
l->setDashPatternOffset( mDashPatternOffset );
427443
l->setDashPatternOffsetUnit( mDashPatternOffsetUnit );
428444
l->setDashPatternOffsetMapUnitScale( mDashPatternOffsetMapUnitScale );
445+
l->setAlignDashPattern( mAlignDashPattern );
446+
l->setTweakDashPatternOnCorners( mPatternCartographicTweakOnSharpCorners );
447+
429448
copyDataDefinedProperties( l );
430449
copyPaintEffect( l );
431450
return l;
@@ -615,6 +634,293 @@ void QgsSimpleLineSymbolLayer::applyDataDefinedSymbology( QgsSymbolRenderContext
615634
}
616635
}
617636

637+
void QgsSimpleLineSymbolLayer::drawPathWithDashPatternTweaks( QPainter *painter, const QPolygonF &points, QPen pen ) const
638+
{
639+
if ( pen.dashPattern().empty() || points.size() < 2 )
640+
return;
641+
642+
QVector< qreal > sourcePattern = pen.dashPattern();
643+
const double dashWidthDiv = std::max( 1.0, pen.widthF() );
644+
// back to painter units
645+
for ( int i = 0; i < sourcePattern.size(); ++ i )
646+
sourcePattern[i ] *= dashWidthDiv;
647+
648+
QVector< qreal > buffer;
649+
QPolygonF bufferedPoints;
650+
QPolygonF previousSegmentBuffer;
651+
// we iterate through the line points, building a custom dash pattern and adding it to the buffer
652+
// as soon as we hit a sharp bend, we scale the buffered pattern in order to nicely place a dash component over the bend
653+
// and then append the buffer to the output pattern.
654+
655+
auto ptIt = points.constBegin();
656+
double totalBufferLength = 0;
657+
int patternIndex = 0;
658+
double currentRemainingDashLength = 0;
659+
double currentRemainingGapLength = 0;
660+
661+
auto compressPattern = []( const QVector< qreal > &buffer ) -> QVector< qreal >
662+
{
663+
QVector< qreal > result;
664+
result.reserve( buffer.size() );
665+
for ( auto it = buffer.begin(); it != buffer.end(); )
666+
{
667+
qreal dash = *it++;
668+
qreal gap = *it++;
669+
while ( dash == 0 && !result.empty() )
670+
{
671+
result.last() += gap;
672+
673+
if ( it == buffer.end() )
674+
return result;
675+
dash = *it++;
676+
gap = *it++;
677+
}
678+
while ( gap == 0 && it != buffer.end() )
679+
{
680+
dash += *it++;
681+
gap = *it++;
682+
}
683+
result << dash << gap;
684+
}
685+
return result;
686+
};
687+
688+
double currentBufferLineLength = 0;
689+
auto flushBuffer = [pen, painter, &buffer, &bufferedPoints, &previousSegmentBuffer, &currentRemainingDashLength, &currentRemainingGapLength, &currentBufferLineLength, &totalBufferLength,
690+
dashWidthDiv, &compressPattern]( QPointF * nextPoint )
691+
{
692+
if ( buffer.empty() || bufferedPoints.size() < 2 )
693+
{
694+
return;
695+
}
696+
697+
if ( currentRemainingDashLength )
698+
{
699+
// ended midway through a dash -- we want to finish this off
700+
buffer << currentRemainingDashLength << 0.0;
701+
totalBufferLength += currentRemainingDashLength;
702+
}
703+
QVector< qreal > compressed = compressPattern( buffer );
704+
if ( !currentRemainingDashLength )
705+
{
706+
// ended midway through a gap -- we don't want this, we want to end at previous dash
707+
totalBufferLength -= compressed.last();
708+
compressed.last() = 0;
709+
}
710+
711+
// rescale buffer for final bit of line -- we want to end at the end of a dash, not a gap
712+
const double scaleFactor = currentBufferLineLength / totalBufferLength;
713+
714+
bool shouldFlushPreviousSegmentBuffer = false;
715+
716+
if ( !previousSegmentBuffer.empty() )
717+
{
718+
// add first dash from current buffer
719+
QPolygonF firstDashSubstring = QgsSymbolLayerUtils::polylineSubstring( bufferedPoints, 0, compressed.first() * scaleFactor );
720+
if ( !firstDashSubstring.empty() )
721+
previousSegmentBuffer << firstDashSubstring;
722+
723+
// then we skip over the first dash and gap for this segment
724+
bufferedPoints = QgsSymbolLayerUtils::polylineSubstring( bufferedPoints, ( compressed.first() + compressed.at( 1 ) ) * scaleFactor, 0 );
725+
726+
compressed = compressed.mid( 2 );
727+
shouldFlushPreviousSegmentBuffer = !compressed.empty();
728+
}
729+
730+
if ( !previousSegmentBuffer.empty() && ( shouldFlushPreviousSegmentBuffer || !nextPoint ) )
731+
{
732+
QPen adjustedPen = pen;
733+
adjustedPen.setStyle( Qt::SolidLine );
734+
painter->setPen( adjustedPen );
735+
QPainterPath path;
736+
path.addPolygon( previousSegmentBuffer );
737+
painter->drawPath( path );
738+
previousSegmentBuffer.clear();
739+
}
740+
741+
double finalDash = 0;
742+
if ( nextPoint )
743+
{
744+
// sharp bend:
745+
// 1. rewind buffered points line by final dash and gap length
746+
// (later) 2. draw the bend with a solid line of length 2 * final dash size
747+
748+
if ( !compressed.empty() )
749+
{
750+
finalDash = compressed.at( compressed.size() - 2 );
751+
const double finalGap = compressed.size() > 2 ? compressed.at( compressed.size() - 3 ) : 0;
752+
753+
const QPolygonF thisPoints = bufferedPoints;
754+
bufferedPoints = QgsSymbolLayerUtils::polylineSubstring( thisPoints, 0, -( finalDash + finalGap ) * scaleFactor );
755+
previousSegmentBuffer = QgsSymbolLayerUtils::polylineSubstring( thisPoints, - finalDash * scaleFactor, 0 );
756+
}
757+
else
758+
{
759+
previousSegmentBuffer << bufferedPoints;
760+
}
761+
}
762+
763+
currentBufferLineLength = 0;
764+
currentRemainingDashLength = 0;
765+
currentRemainingGapLength = 0;
766+
totalBufferLength = 0;
767+
buffer.clear();
768+
769+
if ( !bufferedPoints.empty() && ( !compressed.empty() || !nextPoint ) )
770+
{
771+
QPen adjustedPen = pen;
772+
if ( !compressed.empty() )
773+
{
774+
// maximum size of dash pattern is 32 elements
775+
compressed = compressed.mid( 0, 32 );
776+
std::for_each( compressed.begin(), compressed.end(), [scaleFactor, dashWidthDiv]( qreal & element ) { element *= scaleFactor / dashWidthDiv; } );
777+
adjustedPen.setDashPattern( compressed );
778+
}
779+
else
780+
{
781+
adjustedPen.setStyle( Qt::SolidLine );
782+
}
783+
784+
painter->setPen( adjustedPen );
785+
QPainterPath path;
786+
path.addPolygon( bufferedPoints );
787+
painter->drawPath( path );
788+
}
789+
790+
bufferedPoints.clear();
791+
};
792+
793+
QPointF p1;
794+
QPointF p2 = *ptIt;
795+
ptIt++;
796+
bufferedPoints << p2;
797+
for ( ; ptIt != points.constEnd(); ++ptIt )
798+
{
799+
p1 = *ptIt;
800+
if ( qgsDoubleNear( p1.y(), p2.y() ) && qgsDoubleNear( p1.x(), p2.x() ) )
801+
{
802+
continue;
803+
}
804+
805+
double remainingSegmentDistance = std::sqrt( std::pow( p2.x() - p1.x(), 2.0 ) + std::pow( p2.y() - p1.y(), 2.0 ) );
806+
currentBufferLineLength += remainingSegmentDistance;
807+
while ( true )
808+
{
809+
// handle currentRemainingDashLength/currentRemainingGapLength
810+
if ( currentRemainingDashLength > 0 )
811+
{
812+
// bit more of dash to insert
813+
if ( remainingSegmentDistance >= currentRemainingDashLength )
814+
{
815+
// all of dash fits in
816+
buffer << currentRemainingDashLength << 0.0;
817+
totalBufferLength += currentRemainingDashLength;
818+
remainingSegmentDistance -= currentRemainingDashLength;
819+
patternIndex++;
820+
currentRemainingDashLength = 0.0;
821+
currentRemainingGapLength = sourcePattern.at( patternIndex );
822+
}
823+
else
824+
{
825+
// only part of remaining dash fits in
826+
buffer << remainingSegmentDistance << 0.0;
827+
totalBufferLength += remainingSegmentDistance;
828+
currentRemainingDashLength -= remainingSegmentDistance;
829+
break;
830+
}
831+
}
832+
if ( currentRemainingGapLength > 0 )
833+
{
834+
// bit more of gap to insert
835+
if ( remainingSegmentDistance >= currentRemainingGapLength )
836+
{
837+
// all of gap fits in
838+
buffer << 0.0 << currentRemainingGapLength;
839+
totalBufferLength += currentRemainingGapLength;
840+
remainingSegmentDistance -= currentRemainingGapLength;
841+
currentRemainingGapLength = 0.0;
842+
patternIndex++;
843+
}
844+
else
845+
{
846+
// only part of remaining gap fits in
847+
buffer << 0.0 << remainingSegmentDistance;
848+
totalBufferLength += remainingSegmentDistance;
849+
currentRemainingGapLength -= remainingSegmentDistance;
850+
break;
851+
}
852+
}
853+
854+
if ( patternIndex >= sourcePattern.size() )
855+
patternIndex = 0;
856+
857+
const double nextPatternDashLength = sourcePattern.at( patternIndex );
858+
const double nextPatternGapLength = sourcePattern.at( patternIndex + 1 );
859+
if ( nextPatternDashLength + nextPatternGapLength <= remainingSegmentDistance )
860+
{
861+
buffer << nextPatternDashLength << nextPatternGapLength;
862+
remainingSegmentDistance -= nextPatternDashLength + nextPatternGapLength;
863+
totalBufferLength += nextPatternDashLength + nextPatternGapLength;
864+
patternIndex += 2;
865+
}
866+
else if ( nextPatternDashLength <= remainingSegmentDistance )
867+
{
868+
// can fit in "dash", but not "gap"
869+
buffer << nextPatternDashLength << remainingSegmentDistance - nextPatternDashLength;
870+
totalBufferLength += remainingSegmentDistance;
871+
currentRemainingGapLength = nextPatternGapLength - ( remainingSegmentDistance - nextPatternDashLength );
872+
currentRemainingDashLength = 0;
873+
patternIndex++;
874+
break;
875+
}
876+
else
877+
{
878+
// can't fit in "dash"
879+
buffer << remainingSegmentDistance << 0.0;
880+
totalBufferLength += remainingSegmentDistance;
881+
currentRemainingGapLength = 0;
882+
currentRemainingDashLength = nextPatternDashLength - remainingSegmentDistance;
883+
break;
884+
}
885+
}
886+
887+
bufferedPoints << p1;
888+
if ( mPatternCartographicTweakOnSharpCorners && ptIt + 1 != points.constEnd() )
889+
{
890+
QPointF nextPoint = *( ptIt + 1 );
891+
892+
// extreme angles form more than 45 degree angle at a node
893+
if ( QgsSymbolLayerUtils::isSharpCorner( p2, p1, nextPoint ) )
894+
{
895+
// extreme angle. Rescale buffer and flush
896+
flushBuffer( &nextPoint );
897+
bufferedPoints << p1;
898+
// restart the line with the full length of the most recent dash element -- see
899+
// "Cartographic Generalization" (Swiss Society of Cartography) p33, example #8
900+
if ( patternIndex % 2 == 1 )
901+
{
902+
patternIndex--;
903+
}
904+
currentRemainingDashLength = sourcePattern.at( patternIndex );
905+
}
906+
}
907+
908+
p2 = p1;
909+
}
910+
911+
flushBuffer( nullptr );
912+
if ( !previousSegmentBuffer.empty() )
913+
{
914+
QPen adjustedPen = pen;
915+
adjustedPen.setStyle( Qt::SolidLine );
916+
painter->setPen( adjustedPen );
917+
QPainterPath path;
918+
path.addPolygon( previousSegmentBuffer );
919+
painter->drawPath( path );
920+
previousSegmentBuffer.clear();
921+
}
922+
}
923+
618924
double QgsSimpleLineSymbolLayer::estimateMaxBleed( const QgsRenderContext &context ) const
619925
{
620926
if ( mDrawInsidePolygon )
@@ -667,6 +973,26 @@ QColor QgsSimpleLineSymbolLayer::dxfColor( QgsSymbolRenderContext &context ) con
667973
return mColor;
668974
}
669975

976+
bool QgsSimpleLineSymbolLayer::alignDashPattern() const
977+
{
978+
return mAlignDashPattern;
979+
}
980+
981+
void QgsSimpleLineSymbolLayer::setAlignDashPattern( bool enabled )
982+
{
983+
mAlignDashPattern = enabled;
984+
}
985+
986+
bool QgsSimpleLineSymbolLayer::tweakDashPatternOnCorners() const
987+
{
988+
return mPatternCartographicTweakOnSharpCorners;
989+
}
990+
991+
void QgsSimpleLineSymbolLayer::setTweakDashPatternOnCorners( bool enabled )
992+
{
993+
mPatternCartographicTweakOnSharpCorners = enabled;
994+
}
995+
670996
double QgsSimpleLineSymbolLayer::dxfOffset( const QgsDxfExport &e, QgsSymbolRenderContext &context ) const
671997
{
672998
Q_UNUSED( e )

‎src/core/symbology/qgslinesymbollayer.h

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,52 @@ class CORE_EXPORT QgsSimpleLineSymbolLayer : public QgsLineSymbolLayer
288288
*/
289289
void setDrawInsidePolygon( bool drawInsidePolygon ) { mDrawInsidePolygon = drawInsidePolygon; }
290290

291+
/**
292+
* Returns TRUE if dash patterns should be aligned to the start and end of lines, by
293+
* applying subtle tweaks to the pattern sizing in order to ensure that the end of
294+
* a line is represented by a complete dash element.
295+
*
296+
* \see setAlignDashPattern()
297+
* \see tweakDashPatternOnCorners()
298+
* \since QGIS 3.16
299+
*/
300+
bool alignDashPattern() const;
301+
302+
/**
303+
* Sets whether dash patterns should be aligned to the start and end of lines, by
304+
* applying subtle tweaks to the pattern sizing in order to ensure that the end of
305+
* a line is represented by a complete dash element.
306+
*
307+
* \see alignDashPattern()
308+
* \see setTweakDashPatternOnCorners()
309+
* \since QGIS 3.16
310+
*/
311+
void setAlignDashPattern( bool enabled );
312+
313+
/**
314+
* Returns TRUE if dash patterns tweaks should be applied on sharp corners, to ensure
315+
* that a double-length dash is drawn running into and out of the corner.
316+
*
317+
* \note This setting is only applied if alignDashPattern() is TRUE.
318+
*
319+
* \see setTweakDashPatternOnCorners()
320+
* \see alignDashPattern()
321+
* \since QGIS 3.16
322+
*/
323+
bool tweakDashPatternOnCorners() const;
324+
325+
/**
326+
* Sets whether dash patterns tweaks should be applied on sharp corners, to ensure
327+
* that a double-length dash is drawn running into and out of the corner.
328+
*
329+
* \note This setting is only applied if alignDashPattern() is TRUE.
330+
*
331+
* \see tweakDashPatternOnCorners()
332+
* \see setAlignDashPattern()
333+
* \since QGIS 3.16
334+
*/
335+
void setTweakDashPatternOnCorners( bool enabled );
336+
291337
private:
292338

293339
Qt::PenStyle mPenStyle = Qt::SolidLine;
@@ -307,10 +353,14 @@ class CORE_EXPORT QgsSimpleLineSymbolLayer : public QgsLineSymbolLayer
307353
//! Vector with an even number of entries for the
308354
QVector<qreal> mCustomDashVector;
309355

356+
bool mAlignDashPattern = false;
357+
bool mPatternCartographicTweakOnSharpCorners = false;
358+
310359
bool mDrawInsidePolygon = false;
311360

312361
//helper functions for data defined symbology
313362
void applyDataDefinedSymbology( QgsSymbolRenderContext &context, QPen &pen, QPen &selPen, double &offset );
363+
void drawPathWithDashPatternTweaks( QPainter *painter, const QPolygonF &points, QPen pen ) const;
314364
};
315365

316366
/////////

‎src/gui/symbology/qgssymbollayerwidget.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,27 @@ QgsSimpleLineSymbolLayerWidget::QgsSimpleLineSymbolLayerWidget( QgsVectorLayer *
187187
connect( mDashPatternUnitWidget, &QgsUnitSelectionWidget::changed, this, &QgsSimpleLineSymbolLayerWidget::mDashPatternUnitWidget_changed );
188188
connect( mDrawInsideCheckBox, &QCheckBox::stateChanged, this, &QgsSimpleLineSymbolLayerWidget::mDrawInsideCheckBox_stateChanged );
189189
connect( mPatternOffsetUnitWidget, &QgsUnitSelectionWidget::changed, this, &QgsSimpleLineSymbolLayerWidget::patternOffsetUnitChanged );
190+
connect( mCheckAlignDash, &QCheckBox::toggled, this, [ = ]
191+
{
192+
mCheckDashCorners->setEnabled( mCheckAlignDash->isChecked() );
193+
if ( !mCheckAlignDash->isChecked() )
194+
mCheckDashCorners->setChecked( false );
195+
196+
if ( mLayer )
197+
{
198+
mLayer->setAlignDashPattern( mCheckAlignDash->isChecked() );
199+
emit changed();
200+
}
201+
} );
202+
connect( mCheckDashCorners, &QCheckBox::toggled, this, [ = ]
203+
{
204+
if ( mLayer )
205+
{
206+
mLayer->setTweakDashPatternOnCorners( mCheckDashCorners->isChecked() );
207+
emit changed();
208+
}
209+
} );
210+
190211
mPenWidthUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << QgsUnitTypes::RenderMillimeters << QgsUnitTypes::RenderMetersInMapUnits << QgsUnitTypes::RenderMapUnits << QgsUnitTypes::RenderPixels
191212
<< QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches );
192213
mOffsetUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << QgsUnitTypes::RenderMillimeters << QgsUnitTypes::RenderMetersInMapUnits << QgsUnitTypes::RenderMapUnits << QgsUnitTypes::RenderPixels
@@ -323,6 +344,12 @@ void QgsSimpleLineSymbolLayerWidget::setSymbolLayer( QgsSymbolLayer *layer )
323344

324345
whileBlocking( mRingFilterComboBox )->setCurrentIndex( mRingFilterComboBox->findData( mLayer->ringFilter() ) );
325346

347+
whileBlocking( mCheckAlignDash )->setChecked( mLayer->alignDashPattern() );
348+
mCheckDashCorners->setEnabled( mLayer->alignDashPattern() );
349+
whileBlocking( mCheckDashCorners )->setChecked( mLayer->tweakDashPatternOnCorners() );
350+
if ( !mLayer->alignDashPattern() )
351+
mCheckDashCorners->setChecked( false );
352+
326353
updatePatternIcon();
327354

328355
registerDataDefinedButton( mColorDDBtn, QgsSymbolLayer::PropertyStrokeColor );

‎src/ui/symbollayer/widget_simpleline.ui

Lines changed: 180 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -6,143 +6,26 @@
66
<rect>
77
<x>0</x>
88
<y>0</y>
9-
<width>300</width>
10-
<height>384</height>
9+
<width>325</width>
10+
<height>449</height>
1111
</rect>
1212
</property>
1313
<property name="windowTitle">
1414
<string notr="true">Form</string>
1515
</property>
1616
<layout class="QGridLayout" name="gridLayout">
17-
<item row="12" column="0">
18-
<widget class="QLabel" name="mRingsLabel">
19-
<property name="text">
20-
<string>Rings</string>
21-
</property>
22-
</widget>
23-
</item>
24-
<item row="5" column="3">
25-
<widget class="QgsPropertyOverrideButton" name="mCapStyleDDBtn">
26-
<property name="text">
27-
<string>…</string>
28-
</property>
29-
</widget>
30-
</item>
31-
<item row="4" column="3">
32-
<widget class="QgsPropertyOverrideButton" name="mJoinStyleDDBtn">
33-
<property name="text">
34-
<string>…</string>
35-
</property>
36-
</widget>
37-
</item>
38-
<item row="0" column="0">
39-
<widget class="QLabel" name="label">
40-
<property name="text">
41-
<string>Color</string>
42-
</property>
43-
</widget>
44-
</item>
45-
<item row="3" column="3">
46-
<widget class="QgsPropertyOverrideButton" name="mPenStyleDDBtn">
17+
<item row="10" column="0">
18+
<widget class="QLabel" name="label_7">
4719
<property name="text">
48-
<string></string>
20+
<string>Pattern offset</string>
4921
</property>
5022
</widget>
5123
</item>
52-
<item row="3" column="2">
53-
<widget class="QgsPenStyleComboBox" name="cboPenStyle"/>
54-
</item>
55-
<item row="1" column="2">
56-
<layout class="QHBoxLayout" name="horizontalLayout_2">
57-
<item>
58-
<widget class="QgsDoubleSpinBox" name="spinWidth">
59-
<property name="sizePolicy">
60-
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
61-
<horstretch>1</horstretch>
62-
<verstretch>0</verstretch>
63-
</sizepolicy>
64-
</property>
65-
<property name="specialValueText">
66-
<string>Hairline</string>
67-
</property>
68-
<property name="decimals">
69-
<number>6</number>
70-
</property>
71-
<property name="maximum">
72-
<double>100000.000000000000000</double>
73-
</property>
74-
<property name="singleStep">
75-
<double>0.200000000000000</double>
76-
</property>
77-
<property name="value">
78-
<double>1.000000000000000</double>
79-
</property>
80-
<property name="showClearButton" stdset="0">
81-
<bool>true</bool>
82-
</property>
83-
</widget>
84-
</item>
85-
<item>
86-
<widget class="QgsUnitSelectionWidget" name="mPenWidthUnitWidget" native="true">
87-
<property name="minimumSize">
88-
<size>
89-
<width>0</width>
90-
<height>0</height>
91-
</size>
92-
</property>
93-
<property name="focusPolicy">
94-
<enum>Qt::StrongFocus</enum>
95-
</property>
96-
</widget>
97-
</item>
98-
</layout>
99-
</item>
100-
<item row="4" column="2">
101-
<widget class="QgsPenJoinStyleComboBox" name="cboJoinStyle"/>
102-
</item>
10324
<item row="5" column="2">
10425
<widget class="QgsPenCapStyleComboBox" name="cboCapStyle"/>
10526
</item>
106-
<item row="5" column="0">
107-
<widget class="QLabel" name="label_6">
108-
<property name="text">
109-
<string>Cap style</string>
110-
</property>
111-
</widget>
112-
</item>
113-
<item row="2" column="3">
114-
<widget class="QgsPropertyOverrideButton" name="mOffsetDDBtn">
115-
<property name="text">
116-
<string>…</string>
117-
</property>
118-
</widget>
119-
</item>
120-
<item row="6" column="0" colspan="3">
121-
<widget class="QCheckBox" name="mCustomCheckBox">
122-
<property name="text">
123-
<string>Use custom dash pattern</string>
124-
</property>
125-
</widget>
126-
</item>
127-
<item row="1" column="0">
128-
<widget class="QLabel" name="label_2">
129-
<property name="text">
130-
<string>Stroke width</string>
131-
</property>
132-
</widget>
133-
</item>
134-
<item row="2" column="0">
135-
<widget class="QLabel" name="label_4">
136-
<property name="text">
137-
<string>Offset</string>
138-
</property>
139-
</widget>
140-
</item>
141-
<item row="12" column="2" colspan="2">
142-
<widget class="QComboBox" name="mRingFilterComboBox"/>
143-
</item>
144-
<item row="1" column="3">
145-
<widget class="QgsPropertyOverrideButton" name="mPenWidthDDBtn">
27+
<item row="3" column="3">
28+
<widget class="QgsPropertyOverrideButton" name="mPenStyleDDBtn">
14629
<property name="text">
14730
<string>…</string>
14831
</property>
@@ -188,7 +71,7 @@
18871
</item>
18972
</layout>
19073
</item>
191-
<item row="13" column="0">
74+
<item row="15" column="0">
19275
<spacer name="verticalSpacer">
19376
<property name="orientation">
19477
<enum>Qt::Vertical</enum>
@@ -201,8 +84,8 @@
20184
</property>
20285
</spacer>
20386
</item>
204-
<item row="7" column="3">
205-
<widget class="QgsPropertyOverrideButton" name="mDashPatternDDBtn">
87+
<item row="5" column="3">
88+
<widget class="QgsPropertyOverrideButton" name="mCapStyleDDBtn">
20689
<property name="text">
20790
<string>…</string>
20891
</property>
@@ -233,38 +116,38 @@
233116
</property>
234117
</widget>
235118
</item>
236-
<item row="4" column="0">
237-
<widget class="QLabel" name="label_5">
119+
<item row="0" column="3">
120+
<widget class="QgsPropertyOverrideButton" name="mColorDDBtn">
238121
<property name="text">
239-
<string>Join style</string>
122+
<string></string>
240123
</property>
241124
</widget>
242125
</item>
243-
<item row="10" column="0">
244-
<widget class="QLabel" name="label_7">
126+
<item row="2" column="3">
127+
<widget class="QgsPropertyOverrideButton" name="mOffsetDDBtn">
245128
<property name="text">
246-
<string>Pattern offset</string>
129+
<string></string>
247130
</property>
248131
</widget>
249132
</item>
250-
<item row="0" column="3">
251-
<widget class="QgsPropertyOverrideButton" name="mColorDDBtn">
133+
<item row="14" column="0">
134+
<widget class="QLabel" name="mRingsLabel">
252135
<property name="text">
253-
<string></string>
136+
<string>Rings</string>
254137
</property>
255138
</widget>
256139
</item>
257-
<item row="3" column="0">
258-
<widget class="QLabel" name="label_3">
140+
<item row="1" column="0">
141+
<widget class="QLabel" name="label_2">
259142
<property name="text">
260-
<string>Stroke style</string>
143+
<string>Stroke width</string>
261144
</property>
262145
</widget>
263146
</item>
264-
<item row="2" column="2">
265-
<layout class="QHBoxLayout" name="horizontalLayout_3">
147+
<item row="10" column="2">
148+
<layout class="QHBoxLayout" name="horizontalLayout_4">
266149
<item>
267-
<widget class="QgsDoubleSpinBox" name="spinOffset">
150+
<widget class="QgsDoubleSpinBox" name="spinPatternOffset">
268151
<property name="sizePolicy">
269152
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
270153
<horstretch>1</horstretch>
@@ -289,7 +172,7 @@
289172
</widget>
290173
</item>
291174
<item>
292-
<widget class="QgsUnitSelectionWidget" name="mOffsetUnitWidget" native="true">
175+
<widget class="QgsUnitSelectionWidget" name="mPatternOffsetUnitWidget" native="true">
293176
<property name="minimumSize">
294177
<size>
295178
<width>0</width>
@@ -303,17 +186,27 @@
303186
</item>
304187
</layout>
305188
</item>
306-
<item row="11" column="0" colspan="4">
307-
<widget class="QCheckBox" name="mDrawInsideCheckBox">
189+
<item row="1" column="3">
190+
<widget class="QgsPropertyOverrideButton" name="mPenWidthDDBtn">
308191
<property name="text">
309-
<string>Draw line only inside polygon</string>
192+
<string></string>
310193
</property>
311194
</widget>
312195
</item>
313-
<item row="10" column="2">
314-
<layout class="QHBoxLayout" name="horizontalLayout_4">
196+
<item row="4" column="3">
197+
<widget class="QgsPropertyOverrideButton" name="mJoinStyleDDBtn">
198+
<property name="text">
199+
<string>…</string>
200+
</property>
201+
</widget>
202+
</item>
203+
<item row="4" column="2">
204+
<widget class="QgsPenJoinStyleComboBox" name="cboJoinStyle"/>
205+
</item>
206+
<item row="2" column="2">
207+
<layout class="QHBoxLayout" name="horizontalLayout_3">
315208
<item>
316-
<widget class="QgsDoubleSpinBox" name="spinPatternOffset">
209+
<widget class="QgsDoubleSpinBox" name="spinOffset">
317210
<property name="sizePolicy">
318211
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
319212
<horstretch>1</horstretch>
@@ -338,7 +231,58 @@
338231
</widget>
339232
</item>
340233
<item>
341-
<widget class="QgsUnitSelectionWidget" name="mPatternOffsetUnitWidget" native="true">
234+
<widget class="QgsUnitSelectionWidget" name="mOffsetUnitWidget" native="true">
235+
<property name="minimumSize">
236+
<size>
237+
<width>0</width>
238+
<height>0</height>
239+
</size>
240+
</property>
241+
<property name="focusPolicy">
242+
<enum>Qt::StrongFocus</enum>
243+
</property>
244+
</widget>
245+
</item>
246+
</layout>
247+
</item>
248+
<item row="3" column="2">
249+
<widget class="QgsPenStyleComboBox" name="cboPenStyle"/>
250+
</item>
251+
<item row="14" column="2" colspan="2">
252+
<widget class="QComboBox" name="mRingFilterComboBox"/>
253+
</item>
254+
<item row="1" column="2">
255+
<layout class="QHBoxLayout" name="horizontalLayout_2">
256+
<item>
257+
<widget class="QgsDoubleSpinBox" name="spinWidth">
258+
<property name="sizePolicy">
259+
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
260+
<horstretch>1</horstretch>
261+
<verstretch>0</verstretch>
262+
</sizepolicy>
263+
</property>
264+
<property name="specialValueText">
265+
<string>Hairline</string>
266+
</property>
267+
<property name="decimals">
268+
<number>6</number>
269+
</property>
270+
<property name="maximum">
271+
<double>100000.000000000000000</double>
272+
</property>
273+
<property name="singleStep">
274+
<double>0.200000000000000</double>
275+
</property>
276+
<property name="value">
277+
<double>1.000000000000000</double>
278+
</property>
279+
<property name="showClearButton" stdset="0">
280+
<bool>true</bool>
281+
</property>
282+
</widget>
283+
</item>
284+
<item>
285+
<widget class="QgsUnitSelectionWidget" name="mPenWidthUnitWidget" native="true">
342286
<property name="minimumSize">
343287
<size>
344288
<width>0</width>
@@ -359,29 +303,105 @@
359303
</property>
360304
</widget>
361305
</item>
306+
<item row="11" column="0" colspan="3">
307+
<widget class="QCheckBox" name="mCheckAlignDash">
308+
<property name="toolTip">
309+
<string>If enabled, the dash pattern sizes will be dynamically tweaked to ensure that the end of the line is represented by a complete dash element</string>
310+
</property>
311+
<property name="text">
312+
<string>Align dash pattern to line length</string>
313+
</property>
314+
</widget>
315+
</item>
316+
<item row="4" column="0">
317+
<widget class="QLabel" name="label_5">
318+
<property name="text">
319+
<string>Join style</string>
320+
</property>
321+
</widget>
322+
</item>
323+
<item row="5" column="0">
324+
<widget class="QLabel" name="label_6">
325+
<property name="text">
326+
<string>Cap style</string>
327+
</property>
328+
</widget>
329+
</item>
330+
<item row="0" column="0">
331+
<widget class="QLabel" name="label">
332+
<property name="text">
333+
<string>Color</string>
334+
</property>
335+
</widget>
336+
</item>
337+
<item row="6" column="0" colspan="3">
338+
<widget class="QCheckBox" name="mCustomCheckBox">
339+
<property name="text">
340+
<string>Use custom dash pattern</string>
341+
</property>
342+
</widget>
343+
</item>
344+
<item row="2" column="0">
345+
<widget class="QLabel" name="label_4">
346+
<property name="text">
347+
<string>Offset</string>
348+
</property>
349+
</widget>
350+
</item>
351+
<item row="13" column="0" colspan="4">
352+
<widget class="QCheckBox" name="mDrawInsideCheckBox">
353+
<property name="text">
354+
<string>Draw line only inside polygon</string>
355+
</property>
356+
</widget>
357+
</item>
358+
<item row="3" column="0">
359+
<widget class="QLabel" name="label_3">
360+
<property name="text">
361+
<string>Stroke style</string>
362+
</property>
363+
</widget>
364+
</item>
365+
<item row="7" column="3">
366+
<widget class="QgsPropertyOverrideButton" name="mDashPatternDDBtn">
367+
<property name="text">
368+
<string>…</string>
369+
</property>
370+
</widget>
371+
</item>
372+
<item row="12" column="0" colspan="3">
373+
<widget class="QCheckBox" name="mCheckDashCorners">
374+
<property name="toolTip">
375+
<string>If enabled, the dash pattern for the line will be dynamically refined over sharp corners</string>
376+
</property>
377+
<property name="text">
378+
<string>Tweak dash pattern at sharp corners</string>
379+
</property>
380+
</widget>
381+
</item>
362382
</layout>
363383
</widget>
364384
<customwidgets>
365-
<customwidget>
366-
<class>QgsPropertyOverrideButton</class>
367-
<extends>QToolButton</extends>
368-
<header>qgspropertyoverridebutton.h</header>
369-
</customwidget>
370385
<customwidget>
371386
<class>QgsDoubleSpinBox</class>
372387
<extends>QDoubleSpinBox</extends>
373388
<header>qgsdoublespinbox.h</header>
374389
</customwidget>
375390
<customwidget>
376-
<class>QgsUnitSelectionWidget</class>
377-
<extends>QWidget</extends>
378-
<header>qgsunitselectionwidget.h</header>
391+
<class>QgsColorButton</class>
392+
<extends>QToolButton</extends>
393+
<header>qgscolorbutton.h</header>
379394
<container>1</container>
380395
</customwidget>
381396
<customwidget>
382-
<class>QgsColorButton</class>
397+
<class>QgsPropertyOverrideButton</class>
383398
<extends>QToolButton</extends>
384-
<header>qgscolorbutton.h</header>
399+
<header>qgspropertyoverridebutton.h</header>
400+
</customwidget>
401+
<customwidget>
402+
<class>QgsUnitSelectionWidget</class>
403+
<extends>QWidget</extends>
404+
<header>qgsunitselectionwidget.h</header>
385405
<container>1</container>
386406
</customwidget>
387407
<customwidget>

0 commit comments

Comments
 (0)
Please sign in to comment.