27
27
#include " qgsproperty.h"
28
28
#include " qgsexpressioncontextutils.h"
29
29
30
+ #include < algorithm>
30
31
#include < QPainter>
31
32
#include < QDomDocument>
32
33
#include < QDomElement>
@@ -184,6 +185,12 @@ QgsSymbolLayer *QgsSimpleLineSymbolLayer::create( const QgsStringMap &props )
184
185
if ( props.contains ( QStringLiteral ( " dash_pattern_offset_map_unit_scale" ) ) )
185
186
l->setDashPatternOffsetMapUnitScale ( QgsSymbolLayerUtils::decodeMapUnitScale ( props[QStringLiteral ( " dash_pattern_offset_map_unit_scale" )] ) );
186
187
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
+
187
194
l->restoreOldDataDefinedProperties ( props );
188
195
189
196
return l;
@@ -327,36 +334,37 @@ void QgsSimpleLineSymbolLayer::renderPolyline( const QPolygonF &points, QgsSymbo
327
334
double offset = mOffset ;
328
335
applyDataDefinedSymbology ( context, mPen , mSelPen , offset );
329
336
330
- p-> setPen ( context.selected () ? mSelPen : mPen ) ;
337
+ const QPen pen = context.selected () ? mSelPen : mPen ;
331
338
p->setBrush ( Qt::NoBrush );
332
339
340
+ const bool antialiasingWasEnabled = p->testRenderHint ( QPainter::Antialiasing );
333
341
// Disable 'Antialiasing' if the geometry was generalized in the current RenderContext (We known that it must have least #2 points).
334
342
if ( points.size () <= 2 &&
335
343
( context.renderContext ().vectorSimplifyMethod ().simplifyHints () & QgsVectorSimplifyMethod::AntialiasingSimplification ) &&
336
344
QgsAbstractGeometrySimplifier::isGeneralizableByDeviceBoundingBox ( points, context.renderContext ().vectorSimplifyMethod ().threshold () ) &&
337
345
( p->renderHints () & QPainter::Antialiasing ) )
338
346
{
339
347
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 ;
349
348
}
350
349
350
+ const bool applyPatternTweaks = mAlignDashPattern
351
+ && pen.widthF () > 1.0
352
+ && ( pen.style () != Qt::SolidLine || !pen.dashPattern ().empty () )
353
+ && pen.dashOffset () == 0 ;
354
+
351
355
if ( qgsDoubleNear ( offset, 0 ) )
352
356
{
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
+ }
360
368
}
361
369
else
362
370
{
@@ -368,18 +376,24 @@ void QgsSimpleLineSymbolLayer::renderPolyline( const QPolygonF &points, QgsSymbo
368
376
scaledOffset = std::min ( std::max ( context.renderContext ().convertToPainterUnits ( offset, QgsUnitTypes::RenderMillimeters ), 3.0 ), 100.0 );
369
377
}
370
378
379
+ p->setPen ( pen );
371
380
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 )
373
382
{
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
+ }
381
393
}
382
394
}
395
+
396
+ p->setRenderHint ( QPainter::Antialiasing, antialiasingWasEnabled );
383
397
}
384
398
385
399
QgsStringMap QgsSimpleLineSymbolLayer::properties () const
@@ -404,6 +418,8 @@ QgsStringMap QgsSimpleLineSymbolLayer::properties() const
404
418
map[QStringLiteral ( " dash_pattern_offset_map_unit_scale" )] = QgsSymbolLayerUtils::encodeMapUnitScale ( mDashPatternOffsetMapUnitScale );
405
419
map[QStringLiteral ( " draw_inside_polygon" )] = ( mDrawInsidePolygon ? QStringLiteral ( " 1" ) : QStringLiteral ( " 0" ) );
406
420
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" );
407
423
return map;
408
424
}
409
425
@@ -426,6 +442,9 @@ QgsSimpleLineSymbolLayer *QgsSimpleLineSymbolLayer::clone() const
426
442
l->setDashPatternOffset ( mDashPatternOffset );
427
443
l->setDashPatternOffsetUnit ( mDashPatternOffsetUnit );
428
444
l->setDashPatternOffsetMapUnitScale ( mDashPatternOffsetMapUnitScale );
445
+ l->setAlignDashPattern ( mAlignDashPattern );
446
+ l->setTweakDashPatternOnCorners ( mPatternCartographicTweakOnSharpCorners );
447
+
429
448
copyDataDefinedProperties ( l );
430
449
copyPaintEffect ( l );
431
450
return l;
@@ -615,6 +634,293 @@ void QgsSimpleLineSymbolLayer::applyDataDefinedSymbology( QgsSymbolRenderContext
615
634
}
616
635
}
617
636
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, ¤tRemainingDashLength, ¤tRemainingGapLength, ¤tBufferLineLength, &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
+
618
924
double QgsSimpleLineSymbolLayer::estimateMaxBleed ( const QgsRenderContext &context ) const
619
925
{
620
926
if ( mDrawInsidePolygon )
@@ -667,6 +973,26 @@ QColor QgsSimpleLineSymbolLayer::dxfColor( QgsSymbolRenderContext &context ) con
667
973
return mColor ;
668
974
}
669
975
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
+
670
996
double QgsSimpleLineSymbolLayer::dxfOffset ( const QgsDxfExport &e, QgsSymbolRenderContext &context ) const
671
997
{
672
998
Q_UNUSED ( e )
0 commit comments