From 426382c919a0082e86d22b63200bb9c39b065573 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 25 Mar 2019 17:58:47 +1000 Subject: [PATCH] [FEATURE] Average line angles for marker and hashed line symbology Previously, when marker or hash lines were rendered using interval or center point place placement, the symbol angles were determined by taking the exact line orientation at the position of the symbol. This often leads to undesirable rendering effects, where little jaggies or corners in lines which occur at the position of the symbol cause the marker or hash line to be oriented at a very different angle to what the eye expects to see. With this new option, the angle is instead calculated by averaging the line over a specified distance either side of the symbol. E.g. averaging the line angle over 4mm means we take the points along the line 2mm from either side of the symbol placement, and use these instead to calculate the line angle for that symbol. This has the effect of smoothing (or removing) any tiny local deviations from the overall line direction, resulting in much nicer visual orientation of marker or hash lines. Like all symbol settings, the average angle smoothing distance can be set using mm/pixels/map units/etc, and supports data-defined values. Closed rings also correctly consider wrapping around these average angles from the start/end vertex. (Sponsored by an anonymous corporate backer) --- .../symbology/qgslinesymbollayer.sip.in | 76 ++++ .../symbology/qgssymbollayer.sip.in | 1 + src/core/symbology/qgslinesymbollayer.cpp | 334 +++++++++++++--- src/core/symbology/qgslinesymbollayer.h | 79 +++- src/core/symbology/qgssymbollayer.cpp | 1 + src/core/symbology/qgssymbollayer.h | 1 + src/gui/symbology/qgssymbollayerwidget.cpp | 72 +++- src/gui/symbology/qgssymbollayerwidget.h | 4 + src/ui/symbollayer/widget_hashline.ui | 371 +++++++++++------- src/ui/symbollayer/widget_markerline.ui | 162 ++++++-- tests/src/core/testqgsmarkerlinesymbol.cpp | 241 +++++++++++- .../src/python/test_qgshashlinesymbollayer.py | 73 ++++ .../python/test_qgsmarkerlinesymbollayer.py | 94 +++++ ...pected_composerpaper_markerborder_mask.png | Bin 0 -> 15064 bytes ..._importComposerTemplatePolyline_0_mask.png | Bin 0 -> 6371 bytes ...expected_importComposerTemplate_1_mask.png | Bin 23368 -> 24283 bytes .../expected_style_linecanvasclip_mask.png | Bin 2266 -> 2249 bytes ...expected_style_linecanvasclip_off_mask.png | Bin 2037 -> 2022 bytes .../expected_line_hash_average_angle.png | Bin 0 -> 1241 bytes ...xpected_line_hash_center_average_angle.png | Bin 0 -> 344 bytes .../expected_line_hash_ring_average_angle.png | Bin 0 -> 1509 bytes .../expected_markerline_average_angle.png | Bin 0 -> 1470 bytes ...pected_markerline_center_average_angle.png | Bin 0 -> 311 bytes ...expected_markerline_ring_average_angle.png | Bin 0 -> 1666 bytes .../expected_markerline_ring_no_dupes.png | Bin 0 -> 435 bytes 25 files changed, 1257 insertions(+), 252 deletions(-) create mode 100644 tests/testdata/control_images/composer_paper/expected_composerpaper_markerborder/layout/expected_composerpaper_markerborder_mask.png create mode 100644 tests/testdata/control_images/compositionconverter/expected_importComposerTemplatePolyline_0/expected_importComposerTemplatePolyline_0_mask.png create mode 100644 tests/testdata/control_images/symbol_hashline/expected_line_hash_average_angle/expected_line_hash_average_angle.png create mode 100644 tests/testdata/control_images/symbol_hashline/expected_line_hash_center_average_angle/expected_line_hash_center_average_angle.png create mode 100644 tests/testdata/control_images/symbol_hashline/expected_line_hash_ring_average_angle/expected_line_hash_ring_average_angle.png create mode 100644 tests/testdata/control_images/symbol_markerline/expected_markerline_average_angle/expected_markerline_average_angle.png create mode 100644 tests/testdata/control_images/symbol_markerline/expected_markerline_center_average_angle/expected_markerline_center_average_angle.png create mode 100644 tests/testdata/control_images/symbol_markerline/expected_markerline_ring_average_angle/expected_markerline_ring_average_angle.png create mode 100644 tests/testdata/control_images/symbol_markerline/expected_markerline_ring_no_dupes/expected_markerline_ring_no_dupes.png diff --git a/python/core/auto_generated/symbology/qgslinesymbollayer.sip.in b/python/core/auto_generated/symbology/qgslinesymbollayer.sip.in index 15889530463..b944a346859 100644 --- a/python/core/auto_generated/symbology/qgslinesymbollayer.sip.in +++ b/python/core/auto_generated/symbology/qgslinesymbollayer.sip.in @@ -425,6 +425,82 @@ Returns the map unit scale used for calculating the offset in map units along li Sets the map unit ``scale`` used for calculating the offset in map units along line for symbols. .. seealso:: :py:func:`offsetAlongLineMapUnitScale` +%End + + double averageAngleLength() const; +%Docstring +Returns the length of line over which the line's direction is averaged when +calculating individual symbol angles. Longer lengths smooth out angles from jagged lines to a greater extent. + +Units are retrieved through averageAngleUnit() + +.. seealso:: :py:func:`setAverageAngleLength` + +.. seealso:: :py:func:`averageAngleUnit` + +.. seealso:: :py:func:`averageAngleMapUnitScale` +%End + + void setAverageAngleLength( double length ); +%Docstring +Sets the ``length`` of line over which the line's direction is averaged when +calculating individual symbol angles. Longer lengths smooth out angles from jagged lines to a greater extent. + +Units are set through setAverageAngleUnit() + +.. seealso:: :py:func:`averageAngleLength` + +.. seealso:: :py:func:`setAverageAngleUnit` + +.. seealso:: :py:func:`setAverageAngleMapUnitScale` +%End + + void setAverageAngleUnit( QgsUnitTypes::RenderUnit unit ); +%Docstring +Sets the ``unit`` for the length over which the line's direction is averaged when +calculating individual symbol angles. + +.. seealso:: :py:func:`averageAngleUnit` + +.. seealso:: :py:func:`setAverageAngleLength` + +.. seealso:: :py:func:`setAverageAngleMapUnitScale` +%End + + QgsUnitTypes::RenderUnit averageAngleUnit() const; +%Docstring +Returns the unit for the length over which the line's direction is averaged when +calculating individual symbol angles. + +.. seealso:: :py:func:`setAverageAngleUnit` + +.. seealso:: :py:func:`averageAngleLength` + +.. seealso:: :py:func:`averageAngleMapUnitScale` +%End + + void setAverageAngleMapUnitScale( const QgsMapUnitScale &scale ); +%Docstring +Sets the map unit ``scale`` for the length over which the line's direction is averaged when +calculating individual symbol angles. + +.. seealso:: :py:func:`averageAngleMapUnitScale` + +.. seealso:: :py:func:`setAverageAngleLength` + +.. seealso:: :py:func:`setAverageAngleUnit` +%End + + const QgsMapUnitScale &averageAngleMapUnitScale() const; +%Docstring +Returns the map unit scale for the length over which the line's direction is averaged when +calculating individual symbol angles. + +.. seealso:: :py:func:`setAverageAngleMapUnitScale` + +.. seealso:: :py:func:`averageAngleLength` + +.. seealso:: :py:func:`averageAngleUnit` %End virtual void renderPolyline( const QPolygonF &points, QgsSymbolRenderContext &context ) ${SIP_FINAL}; diff --git a/python/core/auto_generated/symbology/qgssymbollayer.sip.in b/python/core/auto_generated/symbology/qgssymbollayer.sip.in index 5ddf4036891..e0fa145a08c 100644 --- a/python/core/auto_generated/symbology/qgssymbollayer.sip.in +++ b/python/core/auto_generated/symbology/qgssymbollayer.sip.in @@ -123,6 +123,7 @@ class QgsSymbolLayer PropertyPlacement, PropertyInterval, PropertyOffsetAlongLine, + PropertyAverageAngleLength, PropertyHorizontalAnchor, PropertyVerticalAnchor, PropertyLayerEnabled, diff --git a/src/core/symbology/qgslinesymbollayer.cpp b/src/core/symbology/qgslinesymbollayer.cpp index c1af7119932..b8bb3e176ab 100644 --- a/src/core/symbology/qgslinesymbollayer.cpp +++ b/src/core/symbology/qgslinesymbollayer.cpp @@ -799,16 +799,24 @@ void QgsTemplatedLineSymbolLayerBase::renderPolyline( const QPolygonF &points, Q context.renderContext().painter()->save(); + double averageOver = mAverageAngleLength; + if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyAverageAngleLength ) ) + { + context.setOriginalValueVariable( mAverageAngleLength ); + averageOver = mDataDefinedProperties.valueAsDouble( QgsSymbolLayer::PropertyAverageAngleLength, context.renderContext().expressionContext(), mAverageAngleLength ); + } + averageOver = context.renderContext().convertToPainterUnits( averageOver, mAverageAngleLengthUnit, mAverageAngleLengthMapUnitScale ) / 2.0; + if ( qgsDoubleNear( offset, 0.0 ) ) { switch ( placement ) { case Interval: - renderPolylineInterval( points, context ); + renderPolylineInterval( points, context, averageOver ); break; case CentralPoint: - renderPolylineCentral( points, context ); + renderPolylineCentral( points, context, averageOver ); break; case Vertex: @@ -831,11 +839,11 @@ void QgsTemplatedLineSymbolLayerBase::renderPolyline( const QPolygonF &points, Q switch ( placement ) { case Interval: - renderPolylineInterval( points2, context ); + renderPolylineInterval( points2, context, averageOver ); break; case CentralPoint: - renderPolylineCentral( points2, context ); + renderPolylineCentral( points2, context, averageOver ); break; case Vertex: @@ -937,6 +945,10 @@ QgsStringMap QgsTemplatedLineSymbolLayerBase::properties() const map[QStringLiteral( "offset_map_unit_scale" )] = QgsSymbolLayerUtils::encodeMapUnitScale( mOffsetMapUnitScale ); map[QStringLiteral( "interval_unit" )] = QgsUnitTypes::encodeUnit( intervalUnit() ); map[QStringLiteral( "interval_map_unit_scale" )] = QgsSymbolLayerUtils::encodeMapUnitScale( intervalMapUnitScale() ); + map[QStringLiteral( "average_angle_length" )] = QString::number( mAverageAngleLength ); + map[QStringLiteral( "average_angle_unit" )] = QgsUnitTypes::encodeUnit( mAverageAngleLengthUnit ); + map[QStringLiteral( "average_angle_map_unit_scale" )] = QgsSymbolLayerUtils::encodeMapUnitScale( mAverageAngleLengthMapUnitScale ); + switch ( mPlacement ) { case Vertex: @@ -975,6 +987,9 @@ void QgsTemplatedLineSymbolLayerBase::copyTemplateSymbolProperties( QgsTemplated destLayer->setOffsetAlongLine( offsetAlongLine() ); destLayer->setOffsetAlongLineMapUnitScale( offsetAlongLineMapUnitScale() ); destLayer->setOffsetAlongLineUnit( offsetAlongLineUnit() ); + destLayer->setAverageAngleLength( mAverageAngleLength ); + destLayer->setAverageAngleUnit( mAverageAngleLengthUnit ); + destLayer->setAverageAngleMapUnitScale( mAverageAngleLengthMapUnitScale ); destLayer->setRingFilter( mRingFilter ); copyDataDefinedProperties( destLayer ); copyPaintEffect( destLayer ); @@ -1016,6 +1031,19 @@ void QgsTemplatedLineSymbolLayerBase::setCommonProperties( QgsTemplatedLineSymbo destLayer->setIntervalMapUnitScale( QgsSymbolLayerUtils::decodeMapUnitScale( properties[QStringLiteral( "interval_map_unit_scale" )] ) ); } + if ( properties.contains( QStringLiteral( "average_angle_length" ) ) ) + { + destLayer->setAverageAngleLength( properties[QStringLiteral( "average_angle_length" )].toDouble() ); + } + if ( properties.contains( QStringLiteral( "average_angle_unit" ) ) ) + { + destLayer->setAverageAngleUnit( QgsUnitTypes::decodeRenderUnit( properties[QStringLiteral( "average_angle_unit" )] ) ); + } + if ( properties.contains( ( QStringLiteral( "average_angle_map_unit_scale" ) ) ) ) + { + destLayer->setAverageAngleMapUnitScale( QgsSymbolLayerUtils::decodeMapUnitScale( properties[QStringLiteral( "average_angle_map_unit_scale" )] ) ); + } + if ( properties.contains( QStringLiteral( "placement" ) ) ) { if ( properties[QStringLiteral( "placement" )] == QLatin1String( "vertex" ) ) @@ -1040,12 +1068,11 @@ void QgsTemplatedLineSymbolLayerBase::setCommonProperties( QgsTemplatedLineSymbo destLayer->restoreOldDataDefinedProperties( properties ); } -void QgsTemplatedLineSymbolLayerBase::renderPolylineInterval( const QPolygonF &points, QgsSymbolRenderContext &context ) +void QgsTemplatedLineSymbolLayerBase::renderPolylineInterval( const QPolygonF &points, QgsSymbolRenderContext &context, double averageOver ) { if ( points.isEmpty() ) return; - QPointF lastPt = points[0]; double lengthLeft = 0; // how much is left until next marker QgsRenderContext &rc = context.renderContext(); @@ -1070,45 +1097,106 @@ void QgsTemplatedLineSymbolLayerBase::renderPolylineInterval( const QPolygonF &p offsetAlongLine = mDataDefinedProperties.valueAsDouble( QgsSymbolLayer::PropertyOffsetAlongLine, context.renderContext().expressionContext(), mOffsetAlongLine ); } - double painterUnitInterval = rc.convertToPainterUnits( interval, intervalUnit(), intervalMapUnitScale() ); - lengthLeft = painterUnitInterval - rc.convertToPainterUnits( offsetAlongLine, offsetAlongLineUnit(), offsetAlongLineMapUnitScale() ); + const double painterUnitInterval = rc.convertToPainterUnits( interval, intervalUnit(), intervalMapUnitScale() ); - int pointNum = 0; - for ( int i = 1; i < points.count(); ++i ) + if ( painterUnitInterval < 0 ) + return; + + const double painterUnitOffsetAlongLine = rc.convertToPainterUnits( offsetAlongLine, offsetAlongLineUnit(), offsetAlongLineMapUnitScale() ); + lengthLeft = painterUnitInterval - painterUnitOffsetAlongLine; + + if ( averageOver > 0 && !qgsDoubleNear( averageOver, 0.0 ) ) { - const QPointF &pt = points[i]; + QVector< QPointF > angleStartPoints; + QVector< QPointF > symbolPoints; + QVector< QPointF > angleEndPoints; - if ( lastPt == pt ) // must not be equal! - continue; + // we collect 3 arrays of points. These correspond to + // 1. the actual point at which to render the symbol + // 2. the start point of a line averaging the angle over the desired distance (i.e. -averageOver distance from the points in array 1) + // 3. the end point of a line averaging the angle over the desired distance (i.e. +averageOver distance from the points in array 2) + // it gets quite tricky, because for closed rings we need to trace backwards from the initial point to calculate this + // (or trace past the final point) + collectOffsetPoints( points, symbolPoints, painterUnitInterval, lengthLeft ); - // for each line, find out dx and dy, and length - MyLine l( lastPt, pt ); - QPointF diff = l.diffForInterval( painterUnitInterval ); - - // if there's some length left from previous line - // use only the rest for the first point in new line segment - double c = 1 - lengthLeft / painterUnitInterval; - - lengthLeft += l.length(); - - // rotate marker (if desired) - if ( rotateSymbols() ) + if ( symbolPoints.constFirst() == symbolPoints.constLast() ) { - setSymbolLineAngle( l.angle() * 180 / M_PI ); + // avoid duplicate points at start and end of closed rings + symbolPoints.pop_back(); } - // while we're not at the end of line segment, draw! - while ( lengthLeft > painterUnitInterval ) + angleEndPoints.reserve( symbolPoints.size() ); + angleStartPoints.reserve( symbolPoints.size() ); + if ( averageOver <= painterUnitOffsetAlongLine ) { - // "c" is 1 for regular point or in interval (0,1] for begin of line segment - lastPt += c * diff; - lengthLeft -= painterUnitInterval; + collectOffsetPoints( points, angleStartPoints, painterUnitInterval, lengthLeft + averageOver, 0, symbolPoints.size() ); + } + else + { + collectOffsetPoints( points, angleStartPoints, painterUnitInterval, 0, averageOver - painterUnitOffsetAlongLine, symbolPoints.size() ); + } + collectOffsetPoints( points, angleEndPoints, painterUnitInterval, lengthLeft - averageOver, 0, symbolPoints.size() ); + + int pointNum = 0; + for ( int i = 0; i < symbolPoints.size(); ++ i ) + { + const QPointF pt = symbolPoints[i]; + const QPointF startPt = angleStartPoints[i]; + const QPointF endPt = angleEndPoints[i]; + + MyLine l( startPt, endPt ); + // rotate marker (if desired) + if ( rotateSymbols() ) + { + setSymbolLineAngle( l.angle() * 180 / M_PI ); + } + scope->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_GEOMETRY_POINT_NUM, ++pointNum, true ) ); - renderSymbol( lastPt, context.feature(), rc, -1, context.selected() ); - c = 1; // reset c (if wasn't 1 already) + renderSymbol( pt, context.feature(), rc, -1, context.selected() ); + } + } + else + { + // not averaging line angle -- always use exact section angle + int pointNum = 0; + QPointF lastPt = points[0]; + for ( int i = 1; i < points.count(); ++i ) + { + const QPointF &pt = points[i]; + + if ( lastPt == pt ) // must not be equal! + continue; + + // for each line, find out dx and dy, and length + MyLine l( lastPt, pt ); + QPointF diff = l.diffForInterval( painterUnitInterval ); + + // if there's some length left from previous line + // use only the rest for the first point in new line segment + double c = 1 - lengthLeft / painterUnitInterval; + + lengthLeft += l.length(); + + // rotate marker (if desired) + if ( rotateSymbols() ) + { + setSymbolLineAngle( l.angle() * 180 / M_PI ); + } + + // while we're not at the end of line segment, draw! + while ( lengthLeft > painterUnitInterval ) + { + // "c" is 1 for regular point or in interval (0,1] for begin of line segment + lastPt += c * diff; + lengthLeft -= painterUnitInterval; + scope->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_GEOMETRY_POINT_NUM, ++pointNum, true ) ); + renderSymbol( lastPt, context.feature(), rc, -1, context.selected() ); + c = 1; // reset c (if wasn't 1 already) + } + + lastPt = pt; } - lastPt = pt; } delete context.renderContext().expressionContext().popScope(); @@ -1397,7 +1485,116 @@ void QgsTemplatedLineSymbolLayerBase::renderOffsetVertexAlongLine( const QPolygo //didn't find point } -void QgsTemplatedLineSymbolLayerBase::renderPolylineCentral( const QPolygonF &points, QgsSymbolRenderContext &context ) +void QgsTemplatedLineSymbolLayerBase::collectOffsetPoints( const QVector &p, QVector &dest, double intervalPainterUnits, double initialOffset, double initialLag, int numberPointsRequired ) +{ + if ( p.empty() ) + return; + + QVector< QPointF > points = p; + const bool closedRing = points.first() == points.last(); + + double lengthLeft = initialOffset; + + double initialLagLeft = initialLag > 0 ? -initialLag : 1; // an initialLagLeft of > 0 signifies end of lagging start points + if ( initialLagLeft < 0 && closedRing ) + { + // tracking back around the ring from the first point, insert pseudo vertices before the first vertex + QPointF lastPt = points.constLast(); + QVector< QPointF > pseudoPoints; + for ( int i = points.count() - 2; i > 0; --i ) + { + if ( initialLagLeft >= 0 ) + { + break; + } + + const QPointF &pt = points[i]; + + if ( lastPt == pt ) // must not be equal! + continue; + + MyLine l( lastPt, pt ); + initialLagLeft += l.length(); + lastPt = pt; + + pseudoPoints << pt; + } + std::reverse( pseudoPoints.begin(), pseudoPoints.end() ); + + points = pseudoPoints; + points.append( p ); + } + else + { + while ( initialLagLeft < 0 ) + { + dest << points.constFirst(); + initialLagLeft += intervalPainterUnits; + } + } + if ( initialLag > 0 ) + { + lengthLeft += intervalPainterUnits - initialLagLeft; + } + + QPointF lastPt = points[0]; + for ( int i = 1; i < points.count(); ++i ) + { + const QPointF &pt = points[i]; + + if ( lastPt == pt ) // must not be equal! + { + if ( closedRing && i == points.count() - 1 && numberPointsRequired > 0 && dest.size() < numberPointsRequired ) + { + lastPt = points[0]; + i = 0; + } + continue; + } + + // for each line, find out dx and dy, and length + MyLine l( lastPt, pt ); + QPointF diff = l.diffForInterval( intervalPainterUnits ); + + // if there's some length left from previous line + // use only the rest for the first point in new line segment + double c = 1 - lengthLeft / intervalPainterUnits; + + lengthLeft += l.length(); + + + while ( lengthLeft > intervalPainterUnits || qgsDoubleNear( lengthLeft, intervalPainterUnits, 0.000000001 ) ) + { + // "c" is 1 for regular point or in interval (0,1] for begin of line segment + lastPt += c * diff; + lengthLeft -= intervalPainterUnits; + dest << lastPt; + c = 1; // reset c (if wasn't 1 already) + if ( numberPointsRequired > 0 && dest.size() >= numberPointsRequired ) + break; + } + lastPt = pt; + + if ( numberPointsRequired > 0 && dest.size() >= numberPointsRequired ) + break; + + // if a closed ring, we keep looping around the ring until we hit the required number of points + if ( closedRing && i == points.count() - 1 && numberPointsRequired > 0 && dest.size() < numberPointsRequired ) + { + lastPt = points[0]; + i = 0; + } + } + + if ( !closedRing && numberPointsRequired > 0 && dest.size() < numberPointsRequired ) + { + // pad with repeating last point to match desired size + while ( dest.size() < numberPointsRequired ) + dest << points.constLast(); + } +} + +void QgsTemplatedLineSymbolLayerBase::renderPolylineCentral( const QPolygonF &points, QgsSymbolRenderContext &context, double averageAngleOver ) { if ( !points.isEmpty() ) { @@ -1412,33 +1609,56 @@ void QgsTemplatedLineSymbolLayerBase::renderPolylineCentral( const QPolygonF &po last = *it; } - // find the segment where the central point lies - it = points.constBegin(); - last = *it; - qreal last_at = 0, next_at = 0; - QPointF next; - int segment = 0; - for ( ++it; it != points.constEnd(); ++it ) - { - next = *it; - next_at += std::sqrt( ( last.x() - it->x() ) * ( last.x() - it->x() ) + - ( last.y() - it->y() ) * ( last.y() - it->y() ) ); - if ( next_at >= length / 2 ) - break; // we have reached the center - last = *it; - last_at = next_at; - segment++; - } + const double midPoint = length / 2; - // find out the central point on segment - MyLine l( last, next ); // for line angle - qreal k = ( length * 0.5 - last_at ) / ( next_at - last_at ); - QPointF pt = last + ( next - last ) * k; + QPointF pt; + double thisSymbolAngle = 0; + + if ( averageAngleOver > 0 && !qgsDoubleNear( averageAngleOver, 0.0 ) ) + { + QVector< QPointF > angleStartPoints; + QVector< QPointF > symbolPoints; + QVector< QPointF > angleEndPoints; + // collectOffsetPoints will have the first point in the line as the first result -- we don't want this, we need the second + collectOffsetPoints( points, symbolPoints, midPoint, midPoint, 0.0, 2 ); + collectOffsetPoints( points, angleStartPoints, midPoint, 0, averageAngleOver, 2 ); + collectOffsetPoints( points, angleEndPoints, midPoint, midPoint - averageAngleOver, 0, 2 ); + + pt = symbolPoints.at( 1 ); + MyLine l( angleStartPoints.at( 1 ), angleEndPoints.at( 1 ) ); + thisSymbolAngle = l.angle(); + } + else + { + // find the segment where the central point lies + it = points.constBegin(); + last = *it; + qreal last_at = 0, next_at = 0; + QPointF next; + int segment = 0; + for ( ++it; it != points.constEnd(); ++it ) + { + next = *it; + next_at += std::sqrt( ( last.x() - it->x() ) * ( last.x() - it->x() ) + + ( last.y() - it->y() ) * ( last.y() - it->y() ) ); + if ( next_at >= midPoint ) + break; // we have reached the center + last = *it; + last_at = next_at; + segment++; + } + + // find out the central point on segment + MyLine l( last, next ); // for line angle + qreal k = ( length * 0.5 - last_at ) / ( next_at - last_at ); + pt = last + ( next - last ) * k; + thisSymbolAngle = l.angle(); + } // draw the marker double origAngle = symbolAngle(); if ( rotateSymbols() ) - setSymbolAngle( origAngle + l.angle() * 180 / M_PI ); + setSymbolAngle( origAngle + thisSymbolAngle * 180 / M_PI ); renderSymbol( pt, context.feature(), context.renderContext(), -1, context.selected() ); if ( rotateSymbols() ) setSymbolAngle( origAngle ); diff --git a/src/core/symbology/qgslinesymbollayer.h b/src/core/symbology/qgslinesymbollayer.h index 274b8c7632c..9ddf2ec7151 100644 --- a/src/core/symbology/qgslinesymbollayer.h +++ b/src/core/symbology/qgslinesymbollayer.h @@ -401,6 +401,70 @@ class CORE_EXPORT QgsTemplatedLineSymbolLayerBase : public QgsLineSymbolLayer */ void setOffsetAlongLineMapUnitScale( const QgsMapUnitScale &scale ) { mOffsetAlongLineMapUnitScale = scale; } + /** + * Returns the length of line over which the line's direction is averaged when + * calculating individual symbol angles. Longer lengths smooth out angles from jagged lines to a greater extent. + * + * Units are retrieved through averageAngleUnit() + * + * \see setAverageAngleLength() + * \see averageAngleUnit() + * \see averageAngleMapUnitScale() + */ + double averageAngleLength() const { return mAverageAngleLength; } + + /** + * Sets the \a length of line over which the line's direction is averaged when + * calculating individual symbol angles. Longer lengths smooth out angles from jagged lines to a greater extent. + * + * Units are set through setAverageAngleUnit() + * + * \see averageAngleLength() + * \see setAverageAngleUnit() + * \see setAverageAngleMapUnitScale() + */ + void setAverageAngleLength( double length ) { mAverageAngleLength = length; } + + /** + * Sets the \a unit for the length over which the line's direction is averaged when + * calculating individual symbol angles. + * + * \see averageAngleUnit() + * \see setAverageAngleLength() + * \see setAverageAngleMapUnitScale() + */ + void setAverageAngleUnit( QgsUnitTypes::RenderUnit unit ) { mAverageAngleLengthUnit = unit; } + + /** + * Returns the unit for the length over which the line's direction is averaged when + * calculating individual symbol angles. + * + * \see setAverageAngleUnit() + * \see averageAngleLength() + * \see averageAngleMapUnitScale() + */ + QgsUnitTypes::RenderUnit averageAngleUnit() const { return mAverageAngleLengthUnit; } + + /** + * Sets the map unit \a scale for the length over which the line's direction is averaged when + * calculating individual symbol angles. + * + * \see averageAngleMapUnitScale() + * \see setAverageAngleLength() + * \see setAverageAngleUnit() + */ + void setAverageAngleMapUnitScale( const QgsMapUnitScale &scale ) { mAverageAngleLengthMapUnitScale = scale; } + + /** + * Returns the map unit scale for the length over which the line's direction is averaged when + * calculating individual symbol angles. + * + * \see setAverageAngleMapUnitScale() + * \see averageAngleLength() + * \see averageAngleUnit() + */ + const QgsMapUnitScale &averageAngleMapUnitScale() const { return mAverageAngleLengthMapUnitScale; } + void renderPolyline( const QPolygonF &points, QgsSymbolRenderContext &context ) FINAL; void renderPolygonStroke( const QPolygonF &points, QList *rings, QgsSymbolRenderContext &context ) FINAL; QgsUnitTypes::RenderUnit outputUnit() const FINAL; @@ -455,9 +519,9 @@ class CORE_EXPORT QgsTemplatedLineSymbolLayerBase : public QgsLineSymbolLayer private: - void renderPolylineInterval( const QPolygonF &points, QgsSymbolRenderContext &context ); + void renderPolylineInterval( const QPolygonF &points, QgsSymbolRenderContext &context, double averageAngleOver ); void renderPolylineVertex( const QPolygonF &points, QgsSymbolRenderContext &context, QgsTemplatedLineSymbolLayerBase::Placement placement = QgsTemplatedLineSymbolLayerBase::Vertex ); - void renderPolylineCentral( const QPolygonF &points, QgsSymbolRenderContext &context ); + void renderPolylineCentral( const QPolygonF &points, QgsSymbolRenderContext &context, double averageAngleOver ); double markerAngle( const QPolygonF &points, bool isRing, int vertex ); /** @@ -473,6 +537,11 @@ class CORE_EXPORT QgsTemplatedLineSymbolLayerBase : public QgsLineSymbolLayer */ void renderOffsetVertexAlongLine( const QPolygonF &points, int vertex, double distance, QgsSymbolRenderContext &context ); + + static void collectOffsetPoints( const QVector< QPointF> &points, + QVector< QPointF> &dest, double intervalPainterUnits, double initialOffset, double initialLag = 0, + int numberPointsRequired = -1 ); + bool mRotateSymbols = true; double mInterval = 3; QgsUnitTypes::RenderUnit mIntervalUnit = QgsUnitTypes::RenderMillimeters; @@ -481,6 +550,12 @@ class CORE_EXPORT QgsTemplatedLineSymbolLayerBase : public QgsLineSymbolLayer double mOffsetAlongLine = 0; //distance to offset along line before marker is drawn QgsUnitTypes::RenderUnit mOffsetAlongLineUnit = QgsUnitTypes::RenderMillimeters; //unit for offset along line QgsMapUnitScale mOffsetAlongLineMapUnitScale; + double mAverageAngleLength = 4; + QgsUnitTypes::RenderUnit mAverageAngleLengthUnit = QgsUnitTypes::RenderMillimeters; + QgsMapUnitScale mAverageAngleLengthMapUnitScale; + + friend class TestQgsMarkerLineSymbol; + }; /** diff --git a/src/core/symbology/qgssymbollayer.cpp b/src/core/symbology/qgssymbollayer.cpp index 61137f8f05c..bdd765030b3 100644 --- a/src/core/symbology/qgssymbollayer.cpp +++ b/src/core/symbology/qgssymbollayer.cpp @@ -84,6 +84,7 @@ void QgsSymbolLayer::initPropertyDefinitions() { QgsSymbolLayer::PropertyPlacement, QgsPropertyDefinition( "placement", QgsPropertyDefinition::DataTypeString, QObject::tr( "Marker placement" ), QObject::tr( "string " ) + "[interval|vertex|lastvertex|firstvertex|centerpoint|curvepoint]", origin )}, { QgsSymbolLayer::PropertyInterval, QgsPropertyDefinition( "interval", QObject::tr( "Marker interval" ), QgsPropertyDefinition::DoublePositive, origin )}, { QgsSymbolLayer::PropertyOffsetAlongLine, QgsPropertyDefinition( "offsetAlongLine", QObject::tr( "Offset along line" ), QgsPropertyDefinition::DoublePositive, origin )}, + { QgsSymbolLayer::PropertyAverageAngleLength, QgsPropertyDefinition( "averageAngleLength", QObject::tr( "Average line angles over" ), QgsPropertyDefinition::DoublePositive, origin )}, { QgsSymbolLayer::PropertyHorizontalAnchor, QgsPropertyDefinition( "hAnchor", QObject::tr( "Horizontal anchor point" ), QgsPropertyDefinition::HorizontalAnchor, origin )}, { QgsSymbolLayer::PropertyVerticalAnchor, QgsPropertyDefinition( "vAnchor", QObject::tr( "Vertical anchor point" ), QgsPropertyDefinition::VerticalAnchor, origin )}, { QgsSymbolLayer::PropertyLayerEnabled, QgsPropertyDefinition( "enabled", QObject::tr( "Layer enabled" ), QgsPropertyDefinition::Boolean, origin )}, diff --git a/src/core/symbology/qgssymbollayer.h b/src/core/symbology/qgssymbollayer.h index 8662cb2ebf5..ad095df0e62 100644 --- a/src/core/symbology/qgssymbollayer.h +++ b/src/core/symbology/qgssymbollayer.h @@ -165,6 +165,7 @@ class CORE_EXPORT QgsSymbolLayer PropertyPlacement, //!< Line marker placement PropertyInterval, //!< Line marker interval PropertyOffsetAlongLine, //!< Offset along line + PropertyAverageAngleLength, //!< Length to average symbol angles over PropertyHorizontalAnchor, //!< Horizontal anchor point PropertyVerticalAnchor, //!< Vertical anchor point PropertyLayerEnabled, //!< Whether symbol layer is enabled diff --git a/src/gui/symbology/qgssymbollayerwidget.cpp b/src/gui/symbology/qgssymbollayerwidget.cpp index a59846d129e..13cb874ac99 100644 --- a/src/gui/symbology/qgssymbollayerwidget.cpp +++ b/src/gui/symbology/qgssymbollayerwidget.cpp @@ -1670,12 +1670,15 @@ QgsMarkerLineSymbolLayerWidget::QgsMarkerLineSymbolLayerWidget( QgsVectorLayer * connect( mIntervalUnitWidget, &QgsUnitSelectionWidget::changed, this, &QgsMarkerLineSymbolLayerWidget::mIntervalUnitWidget_changed ); connect( mOffsetUnitWidget, &QgsUnitSelectionWidget::changed, this, &QgsMarkerLineSymbolLayerWidget::mOffsetUnitWidget_changed ); connect( mOffsetAlongLineUnitWidget, &QgsUnitSelectionWidget::changed, this, &QgsMarkerLineSymbolLayerWidget::mOffsetAlongLineUnitWidget_changed ); + connect( mAverageAngleUnit, &QgsUnitSelectionWidget::changed, this, &QgsMarkerLineSymbolLayerWidget::averageAngleUnitChanged ); mIntervalUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << QgsUnitTypes::RenderMillimeters << QgsUnitTypes::RenderMetersInMapUnits << QgsUnitTypes::RenderMapUnits << QgsUnitTypes::RenderPixels << QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches ); mOffsetUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << QgsUnitTypes::RenderMillimeters << QgsUnitTypes::RenderMetersInMapUnits << QgsUnitTypes::RenderMapUnits << QgsUnitTypes::RenderPixels << QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches ); mOffsetAlongLineUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << QgsUnitTypes::RenderMillimeters << QgsUnitTypes::RenderMetersInMapUnits << QgsUnitTypes::RenderMapUnits << QgsUnitTypes::RenderPixels << QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches ); + mAverageAngleUnit->setUnits( QgsUnitTypes::RenderUnitList() << QgsUnitTypes::RenderMillimeters << QgsUnitTypes::RenderMetersInMapUnits << QgsUnitTypes::RenderMapUnits << QgsUnitTypes::RenderPixels + << QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches ); mRingFilterComboBox->addItem( QgsApplication::getThemeIcon( QStringLiteral( "mIconAllRings.svg" ) ), tr( "All Rings" ), QgsLineSymbolLayer::AllRings ); mRingFilterComboBox->addItem( QgsApplication::getThemeIcon( QStringLiteral( "mIconExteriorRing.svg" ) ), tr( "Exterior Ring Only" ), QgsLineSymbolLayer::ExteriorRingOnly ); @@ -1698,10 +1701,13 @@ QgsMarkerLineSymbolLayerWidget::QgsMarkerLineSymbolLayerWidget( QgsVectorLayer * mRingsLabel->hide(); } + mSpinAverageAngleLength->setClearValue( 4.0 ); + connect( spinInterval, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsMarkerLineSymbolLayerWidget::setInterval ); connect( mSpinOffsetAlongLine, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsMarkerLineSymbolLayerWidget::setOffsetAlongLine ); connect( chkRotateMarker, &QAbstractButton::clicked, this, &QgsMarkerLineSymbolLayerWidget::setRotate ); connect( spinOffset, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsMarkerLineSymbolLayerWidget::setOffset ); + connect( mSpinAverageAngleLength, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsMarkerLineSymbolLayerWidget::setAverageAngle ); connect( radInterval, &QAbstractButton::clicked, this, &QgsMarkerLineSymbolLayerWidget::setPlacement ); connect( radVertex, &QAbstractButton::clicked, this, &QgsMarkerLineSymbolLayerWidget::setPlacement ); connect( radVertexLast, &QAbstractButton::clicked, this, &QgsMarkerLineSymbolLayerWidget::setPlacement ); @@ -1758,6 +1764,10 @@ void QgsMarkerLineSymbolLayerWidget::setSymbolLayer( QgsSymbolLayer *layer ) mOffsetAlongLineUnitWidget->setMapUnitScale( mLayer->offsetAlongLineMapUnitScale() ); mOffsetAlongLineUnitWidget->blockSignals( false ); + whileBlocking( mAverageAngleUnit )->setUnit( mLayer->averageAngleUnit() ); + whileBlocking( mAverageAngleUnit )->setMapUnitScale( mLayer->averageAngleMapUnitScale() ); + whileBlocking( mSpinAverageAngleLength )->setValue( mLayer->averageAngleLength() ); + whileBlocking( mRingFilterComboBox )->setCurrentIndex( mRingFilterComboBox->findData( mLayer->ringFilter() ) ); setPlacement(); // update gui @@ -1766,6 +1776,7 @@ void QgsMarkerLineSymbolLayerWidget::setSymbolLayer( QgsSymbolLayer *layer ) registerDataDefinedButton( mLineOffsetDDBtn, QgsSymbolLayer::PropertyOffset ); registerDataDefinedButton( mPlacementDDBtn, QgsSymbolLayer::PropertyPlacement ); registerDataDefinedButton( mOffsetAlongLineDDBtn, QgsSymbolLayer::PropertyOffsetAlongLine ); + registerDataDefinedButton( mAverageAngleDDBtn, QgsSymbolLayer::PropertyAverageAngleLength ); } QgsSymbolLayer *QgsMarkerLineSymbolLayerWidget::symbolLayer() @@ -1787,6 +1798,9 @@ void QgsMarkerLineSymbolLayerWidget::setOffsetAlongLine( double val ) void QgsMarkerLineSymbolLayerWidget::setRotate() { + mSpinAverageAngleLength->setEnabled( chkRotateMarker->isChecked() && ( radInterval->isChecked() || radCentralPoint->isChecked() ) ); + mAverageAngleUnit->setEnabled( mSpinAverageAngleLength->isEnabled() ); + mLayer->setRotateSymbols( chkRotateMarker->isChecked() ); emit changed(); } @@ -1802,6 +1816,9 @@ void QgsMarkerLineSymbolLayerWidget::setPlacement() bool interval = radInterval->isChecked(); spinInterval->setEnabled( interval ); mSpinOffsetAlongLine->setEnabled( radInterval->isChecked() || radVertexLast->isChecked() || radVertexFirst->isChecked() ); + mOffsetAlongLineUnitWidget->setEnabled( mSpinOffsetAlongLine->isEnabled() ); + mSpinAverageAngleLength->setEnabled( chkRotateMarker->isChecked() && ( radInterval->isChecked() || radCentralPoint->isChecked() ) ); + mAverageAngleUnit->setEnabled( mSpinAverageAngleLength->isEnabled() ); //mLayer->setPlacement( interval ? QgsMarkerLineSymbolLayer::Interval : QgsMarkerLineSymbolLayer::Vertex ); if ( radInterval->isChecked() ) mLayer->setPlacement( QgsTemplatedLineSymbolLayerBase::Interval ); @@ -1849,6 +1866,25 @@ void QgsMarkerLineSymbolLayerWidget::mOffsetAlongLineUnitWidget_changed() emit changed(); } +void QgsMarkerLineSymbolLayerWidget::averageAngleUnitChanged() +{ + if ( mLayer ) + { + mLayer->setAverageAngleUnit( mAverageAngleUnit->unit() ); + mLayer->setAverageAngleMapUnitScale( mAverageAngleUnit->getMapUnitScale() ); + } + emit changed(); +} + +void QgsMarkerLineSymbolLayerWidget::setAverageAngle( double val ) +{ + if ( mLayer ) + { + mLayer->setAverageAngleLength( val ); + emit changed(); + } +} + /////////// @@ -1861,6 +1897,7 @@ QgsHashedLineSymbolLayerWidget::QgsHashedLineSymbolLayerWidget( QgsVectorLayer * connect( mIntervalUnitWidget, &QgsUnitSelectionWidget::changed, this, &QgsHashedLineSymbolLayerWidget::mIntervalUnitWidget_changed ); connect( mOffsetUnitWidget, &QgsUnitSelectionWidget::changed, this, &QgsHashedLineSymbolLayerWidget::mOffsetUnitWidget_changed ); connect( mOffsetAlongLineUnitWidget, &QgsUnitSelectionWidget::changed, this, &QgsHashedLineSymbolLayerWidget::mOffsetAlongLineUnitWidget_changed ); + connect( mAverageAngleUnit, &QgsUnitSelectionWidget::changed, this, &QgsHashedLineSymbolLayerWidget::averageAngleUnitChanged ); connect( mHashLengthUnitWidget, &QgsUnitSelectionWidget::changed, this, &QgsHashedLineSymbolLayerWidget::hashLengthUnitWidgetChanged ); mIntervalUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << QgsUnitTypes::RenderMillimeters << QgsUnitTypes::RenderMetersInMapUnits << QgsUnitTypes::RenderMapUnits << QgsUnitTypes::RenderPixels << QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches ); @@ -1868,7 +1905,8 @@ QgsHashedLineSymbolLayerWidget::QgsHashedLineSymbolLayerWidget( QgsVectorLayer * << QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches ); mOffsetAlongLineUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << QgsUnitTypes::RenderMillimeters << QgsUnitTypes::RenderMetersInMapUnits << QgsUnitTypes::RenderMapUnits << QgsUnitTypes::RenderPixels << QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches ); - + mAverageAngleUnit->setUnits( QgsUnitTypes::RenderUnitList() << QgsUnitTypes::RenderMillimeters << QgsUnitTypes::RenderMetersInMapUnits << QgsUnitTypes::RenderMapUnits << QgsUnitTypes::RenderPixels + << QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches ); mHashLengthUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << QgsUnitTypes::RenderMillimeters << QgsUnitTypes::RenderMetersInMapUnits << QgsUnitTypes::RenderMapUnits << QgsUnitTypes::RenderPixels << QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches ); @@ -1894,6 +1932,7 @@ QgsHashedLineSymbolLayerWidget::QgsHashedLineSymbolLayerWidget( QgsVectorLayer * } mHashRotationSpinBox->setClearValue( 0 ); + mSpinAverageAngleLength->setClearValue( 4.0 ); connect( spinInterval, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsHashedLineSymbolLayerWidget::setInterval ); connect( mSpinOffsetAlongLine, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsHashedLineSymbolLayerWidget::setOffsetAlongLine ); @@ -1901,6 +1940,7 @@ QgsHashedLineSymbolLayerWidget::QgsHashedLineSymbolLayerWidget( QgsVectorLayer * connect( mHashRotationSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsHashedLineSymbolLayerWidget::setHashAngle ); connect( chkRotateMarker, &QAbstractButton::clicked, this, &QgsHashedLineSymbolLayerWidget::setRotate ); connect( spinOffset, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsHashedLineSymbolLayerWidget::setOffset ); + connect( mSpinAverageAngleLength, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsHashedLineSymbolLayerWidget::setAverageAngle ); connect( radInterval, &QAbstractButton::clicked, this, &QgsHashedLineSymbolLayerWidget::setPlacement ); connect( radVertex, &QAbstractButton::clicked, this, &QgsHashedLineSymbolLayerWidget::setPlacement ); connect( radVertexLast, &QAbstractButton::clicked, this, &QgsHashedLineSymbolLayerWidget::setPlacement ); @@ -1958,7 +1998,9 @@ void QgsHashedLineSymbolLayerWidget::setSymbolLayer( QgsSymbolLayer *layer ) mOffsetAlongLineUnitWidget->setUnit( mLayer->offsetAlongLineUnit() ); mOffsetAlongLineUnitWidget->setMapUnitScale( mLayer->offsetAlongLineMapUnitScale() ); mOffsetAlongLineUnitWidget->blockSignals( false ); - + whileBlocking( mAverageAngleUnit )->setUnit( mLayer->averageAngleUnit() ); + whileBlocking( mAverageAngleUnit )->setMapUnitScale( mLayer->averageAngleMapUnitScale() ); + whileBlocking( mSpinAverageAngleLength )->setValue( mLayer->averageAngleLength() ); whileBlocking( mHashLengthUnitWidget )->setUnit( mLayer->hashLengthUnit() ); whileBlocking( mHashLengthUnitWidget )->setMapUnitScale( mLayer->hashLengthMapUnitScale() ); @@ -1972,6 +2014,7 @@ void QgsHashedLineSymbolLayerWidget::setSymbolLayer( QgsSymbolLayer *layer ) registerDataDefinedButton( mOffsetAlongLineDDBtn, QgsSymbolLayer::PropertyOffsetAlongLine ); registerDataDefinedButton( mHashLengthDDBtn, QgsSymbolLayer::PropertyLineDistance ); registerDataDefinedButton( mHashRotationDDBtn, QgsSymbolLayer::PropertyLineAngle ); + registerDataDefinedButton( mAverageAngleDDBtn, QgsSymbolLayer::PropertyAverageAngleLength ); } QgsSymbolLayer *QgsHashedLineSymbolLayerWidget::symbolLayer() @@ -2005,6 +2048,9 @@ void QgsHashedLineSymbolLayerWidget::setHashAngle( double val ) void QgsHashedLineSymbolLayerWidget::setRotate() { + mSpinAverageAngleLength->setEnabled( chkRotateMarker->isChecked() && ( radInterval->isChecked() || radCentralPoint->isChecked() ) ); + mAverageAngleUnit->setEnabled( mSpinAverageAngleLength->isEnabled() ); + mLayer->setRotateSymbols( chkRotateMarker->isChecked() ); emit changed(); } @@ -2020,6 +2066,9 @@ void QgsHashedLineSymbolLayerWidget::setPlacement() bool interval = radInterval->isChecked(); spinInterval->setEnabled( interval ); mSpinOffsetAlongLine->setEnabled( radInterval->isChecked() || radVertexLast->isChecked() || radVertexFirst->isChecked() ); + mOffsetAlongLineUnitWidget->setEnabled( mSpinOffsetAlongLine->isEnabled() ); + mSpinAverageAngleLength->setEnabled( chkRotateMarker->isChecked() && ( radInterval->isChecked() || radCentralPoint->isChecked() ) ); + mAverageAngleUnit->setEnabled( mSpinAverageAngleLength->isEnabled() ); //mLayer->setPlacement( interval ? QgsMarkerLineSymbolLayer::Interval : QgsMarkerLineSymbolLayer::Vertex ); if ( radInterval->isChecked() ) mLayer->setPlacement( QgsTemplatedLineSymbolLayerBase::Interval ); @@ -2077,6 +2126,25 @@ void QgsHashedLineSymbolLayerWidget::hashLengthUnitWidgetChanged() emit changed(); } +void QgsHashedLineSymbolLayerWidget::averageAngleUnitChanged() +{ + if ( mLayer ) + { + mLayer->setAverageAngleUnit( mAverageAngleUnit->unit() ); + mLayer->setAverageAngleMapUnitScale( mAverageAngleUnit->getMapUnitScale() ); + } + emit changed(); +} + +void QgsHashedLineSymbolLayerWidget::setAverageAngle( double val ) +{ + if ( mLayer ) + { + mLayer->setAverageAngleLength( val ); + emit changed(); + } +} + /////////// diff --git a/src/gui/symbology/qgssymbollayerwidget.h b/src/gui/symbology/qgssymbollayerwidget.h index ed066381236..508639ea598 100644 --- a/src/gui/symbology/qgssymbollayerwidget.h +++ b/src/gui/symbology/qgssymbollayerwidget.h @@ -498,6 +498,8 @@ class GUI_EXPORT QgsMarkerLineSymbolLayerWidget : public QgsSymbolLayerWidget, p void mIntervalUnitWidget_changed(); void mOffsetUnitWidget_changed(); void mOffsetAlongLineUnitWidget_changed(); + void averageAngleUnitChanged(); + void setAverageAngle( double val ); }; @@ -549,6 +551,8 @@ class GUI_EXPORT QgsHashedLineSymbolLayerWidget : public QgsSymbolLayerWidget, p void mOffsetUnitWidget_changed(); void mOffsetAlongLineUnitWidget_changed(); void hashLengthUnitWidgetChanged(); + void averageAngleUnitChanged(); + void setAverageAngle( double val ); private: QgsHashedLineSymbolLayer *mLayer = nullptr; diff --git a/src/ui/symbollayer/widget_hashline.ui b/src/ui/symbollayer/widget_hashline.ui index 396af538801..243a97a2a7c 100644 --- a/src/ui/symbollayer/widget_hashline.ui +++ b/src/ui/symbollayer/widget_hashline.ui @@ -191,166 +191,29 @@ 0 - + - - - - - 1 - 0 - - - - 6 - - - 10000000.000000000000000 - - - 0.200000000000000 - - - 1.000000000000000 - - - - - + + - - - - Hash rotation - - - - - - - - 20 - 0 - - - - Qt::TabFocus - - - - - - - Offset along line - - - - + - - - - - 0 - 0 - - - - Qt::TabFocus - - - - - - - Rings - - - - - - - Line offset - - - - - - - - - - - 1 - 0 - - - - 6 - - - -999999999.000000000000000 - - - 999999999.000000000000000 - - - 0.200000000000000 - - - - - - - Rotate hash to follow line direction - - - - - - - Hash length - - - - - - - - 1 - 0 - - - - 6 - - - 10000000.000000000000000 - - - 0.200000000000000 - - - 1.000000000000000 - - - - - + + 20 @@ -362,14 +225,21 @@ - + + + + Line offset + + + + - + @@ -397,6 +267,201 @@ + + + + Hash rotation + + + + + + + + 20 + 0 + + + + Qt::TabFocus + + + + + + + Offset along line + + + + + + + Hash length + + + + + + + + 1 + 0 + + + + 6 + + + 10000000.000000000000000 + + + 0.200000000000000 + + + 1.000000000000000 + + + + + + + + + + + 0 + 0 + + + + Qt::TabFocus + + + + + + + Rings + + + + + + + + 1 + 0 + + + + 6 + + + -999999999.000000000000000 + + + 999999999.000000000000000 + + + 0.200000000000000 + + + + + + + Average angle over + + + + + + + + 20 + 0 + + + + Qt::TabFocus + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 1 + 0 + + + + 6 + + + 10000000.000000000000000 + + + 0.200000000000000 + + + 1.000000000000000 + + + + + + + + + + + + + + + 1 + 0 + + + + 6 + + + 10000000.000000000000000 + + + 0.200000000000000 + + + 1.000000000000000 + + + + + + + Rotate hash to follow line direction + + + @@ -430,6 +495,22 @@ radVertexFirst radCentralPoint radCurvePoint + mSpinOffsetAlongLine + mOffsetAlongLineUnitWidget + mOffsetAlongLineDDBtn + mSpinHashLength + mHashLengthUnitWidget + mHashLengthDDBtn + mHashRotationSpinBox + mHashRotationDDBtn + chkRotateMarker + mSpinAverageAngleLength + mAverageAngleUnit + mAverageAngleDDBtn + spinOffset + mOffsetUnitWidget + mLineOffsetDDBtn + mRingFilterComboBox diff --git a/src/ui/symbollayer/widget_markerline.ui b/src/ui/symbollayer/widget_markerline.ui index 83c5c3d01e6..0b8e050a1aa 100644 --- a/src/ui/symbollayer/widget_markerline.ui +++ b/src/ui/symbollayer/widget_markerline.ui @@ -191,7 +191,7 @@ 0 - + @@ -213,32 +213,28 @@ - - - - - - + + - - - - Offset along line - - - - + - + + + + Average angle over + + + + - 0 + 20 0 @@ -247,27 +243,7 @@ - - - - Line offset - - - - - - - - 0 - 0 - - - - Qt::TabFocus - - - - + @@ -289,23 +265,112 @@ - + + + + + 0 + 0 + + + + Qt::TabFocus + + + + + + + + 20 + 0 + + + + Qt::TabFocus + + + + + + + + + + + + + + + + + + + + + + 1 + 0 + + + + 6 + + + 10000000.000000000000000 + + + 0.200000000000000 + + + 1.000000000000000 + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + Offset along line + + + + Rotate marker - + + + + Line offset + + + + Rings - - - @@ -339,6 +404,17 @@ radVertexFirst radCentralPoint radCurvePoint + mSpinOffsetAlongLine + mOffsetAlongLineUnitWidget + mOffsetAlongLineDDBtn + chkRotateMarker + mSpinAverageAngleLength + mAverageAngleUnit + mAverageAngleDDBtn + spinOffset + mOffsetUnitWidget + mLineOffsetDDBtn + mRingFilterComboBox diff --git a/tests/src/core/testqgsmarkerlinesymbol.cpp b/tests/src/core/testqgsmarkerlinesymbol.cpp index e172ab8b9be..a16f6137c80 100644 --- a/tests/src/core/testqgsmarkerlinesymbol.cpp +++ b/tests/src/core/testqgsmarkerlinesymbol.cpp @@ -60,6 +60,8 @@ class TestQgsMarkerLineSymbol : public QObject void lineOffset(); void pointNumInterval(); void pointNumVertex(); + void collectPoints_data(); + void collectPoints(); private: bool render( const QString &fileName ); @@ -132,7 +134,7 @@ void TestQgsMarkerLineSymbol::lineOffset() QVERIFY( success ); mMapSettings->setExtent( QgsRectangle( -140, -140, 140, 140 ) ); - QVERIFY( render( "line_offset" ) ); + QVERIFY( render( QStringLiteral( "line_offset" ) ) ); // TODO: -0.0 offset, see // https://issues.qgis.org/issues/13811#note-1 @@ -165,7 +167,7 @@ void TestQgsMarkerLineSymbol::pointNumInterval() mLinesLayer->setRenderer( r ); mMapSettings->setExtent( QgsRectangle( -140, -140, 140, 140 ) ); - QVERIFY( render( "point_num_interval" ) ); + QVERIFY( render( QStringLiteral( "point_num_interval" ) ) ); } void TestQgsMarkerLineSymbol::pointNumVertex() @@ -194,7 +196,240 @@ void TestQgsMarkerLineSymbol::pointNumVertex() mLinesLayer->setRenderer( r ); mMapSettings->setExtent( QgsRectangle( -140, -140, 140, 140 ) ); - QVERIFY( render( "point_num_vertex" ) ); + QVERIFY( render( QStringLiteral( "point_num_vertex" ) ) ); +} + +void TestQgsMarkerLineSymbol::collectPoints_data() +{ + QTest::addColumn>( "input" ); + QTest::addColumn( "interval" ); + QTest::addColumn( "initialOffset" ); + QTest::addColumn( "initialLag" ); + QTest::addColumn( "numberPointsRequired" ); + QTest::addColumn>( "expected" ); + + QTest::newRow( "empty" ) + << QVector< QPointF >() + << 1.0 << 0.0 << 0.0 << 0 + << QVector< QPointF >(); + + QTest::newRow( "a" ) + << ( QVector< QPointF >() << QPointF( 1, 2 ) ) + << 1.0 << 0.0 << 0.0 << 0 + << ( QVector< QPointF >() ); + + QTest::newRow( "b" ) + << ( QVector< QPointF >() << QPointF( 1, 2 ) << QPointF( 11, 2 ) ) + << 1.0 << 0.0 << 0.0 << 0 + << ( QVector< QPointF >() << QPointF( 2, 2 ) << QPointF( 3, 2 ) + << QPointF( 4, 2 ) << QPointF( 5, 2 ) << QPointF( 6, 2 ) << QPointF( 7, 2 ) << QPointF( 8, 2 ) + << QPointF( 9, 2 ) << QPointF( 10, 2 ) << QPointF( 11, 2 ) ); + + QTest::newRow( "b maxpoints" ) + << ( QVector< QPointF >() << QPointF( 1, 2 ) << QPointF( 11, 2 ) ) + << 1.0 << 0.0 << 0.0 << 3 + << ( QVector< QPointF >() << QPointF( 2, 2 ) << QPointF( 3, 2 ) + << QPointF( 4, 2 ) ); + + QTest::newRow( "b pad points" ) + << ( QVector< QPointF >() << QPointF( 1, 2 ) << QPointF( 11, 2 ) ) + << 1.0 << 0.0 << 0.0 << 13 + << ( QVector< QPointF >() << QPointF( 2, 2 ) << QPointF( 3, 2 ) + << QPointF( 4, 2 ) << QPointF( 5, 2 ) << QPointF( 6, 2 ) << QPointF( 7, 2 ) << QPointF( 8, 2 ) + << QPointF( 9, 2 ) << QPointF( 10, 2 ) << QPointF( 11, 2 ) << QPointF( 11, 2 ) << QPointF( 11, 2 ) << QPointF( 11, 2 ) ); + + QTest::newRow( "c" ) + << ( QVector< QPointF >() << QPointF( 1, 2 ) << QPointF( 11, 2 ) ) + << 1.0 << 1.0 << 0.0 << 0 + << ( QVector< QPointF >() << QPointF( 1, 2 ) << QPointF( 2, 2 ) << QPointF( 3, 2 ) + << QPointF( 4, 2 ) << QPointF( 5, 2 ) << QPointF( 6, 2 ) << QPointF( 7, 2 ) << QPointF( 8, 2 ) + << QPointF( 9, 2 ) << QPointF( 10, 2 ) << QPointF( 11, 2 ) ); + + QTest::newRow( "c3" ) + << ( QVector< QPointF >() << QPointF( 1, 2 ) << QPointF( 11, 2 ) ) + << 2.0 << 0.0 << 0.0 << 0 + << ( QVector< QPointF >() << QPointF( 3, 2 ) << QPointF( 5, 2 ) + << QPointF( 7, 2 ) << QPointF( 9, 2 ) << QPointF( 11, 2 ) ); + + QTest::newRow( "d" ) + << ( QVector< QPointF >() << QPointF( 1, 2 ) << QPointF( 11, 2 ) ) + << 2.0 << 1.0 << 0.0 << 0 + << ( QVector< QPointF >() << QPointF( 2, 2 ) << QPointF( 4, 2 ) + << QPointF( 6, 2 ) << QPointF( 8, 2 ) << QPointF( 10, 2 ) ); + + QTest::newRow( "e" ) + << ( QVector< QPointF >() << QPointF( 1, 2 ) << QPointF( 11, 2 ) ) + << 2.0 << 2.0 << 0.0 << 0 + << ( QVector< QPointF >() << QPointF( 1, 2 ) << QPointF( 3, 2 ) << QPointF( 5, 2 ) + << QPointF( 7, 2 ) << QPointF( 9, 2 ) << QPointF( 11, 2 ) ); + + QTest::newRow( "f" ) + << ( QVector< QPointF >() << QPointF( 1, 2 ) << QPointF( 11, 2 ) ) + << 2.0 << 0.0 << 1.0 << 0 + << ( QVector< QPointF >() << QPointF( 1, 2 ) << QPointF( 2, 2 ) << QPointF( 4, 2 ) << QPointF( 6, 2 ) << QPointF( 8, 2 ) + << QPointF( 10, 2 ) ); + + QTest::newRow( "g" ) + << ( QVector< QPointF >() << QPointF( 1, 2 ) << QPointF( 11, 2 ) ) + << 2.0 << 0.0 << 2.0 << 0 + << ( QVector< QPointF >() << QPointF( 1, 2 ) << QPointF( 1, 2 ) << QPointF( 3, 2 ) << QPointF( 5, 2 ) << QPointF( 7, 2 ) + << QPointF( 9, 2 ) << QPointF( 11, 2 ) ); + + QTest::newRow( "h" ) + << ( QVector< QPointF >() << QPointF( 1, 2 ) << QPointF( 11, 2 ) ) + << 2.0 << 0.0 << 2.1 << 0 + << ( QVector< QPointF >() << QPointF( 1, 2 ) << QPointF( 1, 2 ) << QPointF( 2.9, 2 ) << QPointF( 4.9, 2 ) << QPointF( 6.9, 2 ) + << QPointF( 8.9, 2 ) << QPointF( 10.9, 2 ) ); + + QTest::newRow( "i" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 8, 2 ) ) + << 2.0 << 2.0 << 0.0 << 0 + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 4, 2 ) << QPointF( 6, 2 ) << QPointF( 8, 2 ) ); + + QTest::newRow( "j" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 8, 2 ) ) + << 2.0 << 0.0 << 2.0 << 0 + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 4, 2 ) << QPointF( 6, 2 ) << QPointF( 8, 2 ) ); + + QTest::newRow( "k" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 8, 2 ) ) + << 2.0 << 0.0 << 0.0 << 0 + << ( QVector< QPointF >() << QPointF( 2, 2 ) << QPointF( 4, 2 ) << QPointF( 6, 2 ) << QPointF( 8, 2 ) ); + + QTest::newRow( "closed ring" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ) + << 2.0 << 2.0 << 0.0 << 0 + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ); + + QTest::newRow( "closed ring required points" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ) + << 2.0 << 2.0 << 0.0 << 7 + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) ); + QTest::newRow( "closed ring 1.0" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ) + << 1.0 << 1.0 << 0.0 << 0 + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 1, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 1 ) << QPointF( 2, 0 ) + << QPointF( 1, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 1 ) << QPointF( 0, 2 ) ); + QTest::newRow( "closed ring 1.0" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ) + << 1.0 << 1.0 << 0.0 << 11 + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 1, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 1 ) << QPointF( 2, 0 ) + << QPointF( 1, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 1 ) << QPointF( 0, 2 ) << QPointF( 1, 2 ) << QPointF( 2, 2 ) ); + QTest::newRow( "closed ring initial offset 1.0" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ) + << 1.0 << 0.0 << 0.0 << 0 + << ( QVector< QPointF >() << QPointF( 1, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 1 ) << QPointF( 2, 0 ) + << QPointF( 1, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 1 ) << QPointF( 0, 2 ) ); + QTest::newRow( "closed ring initial offset 1.0 num points" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ) + << 1.0 << 0.0 << 0.0 << 10 + << ( QVector< QPointF >() << QPointF( 1, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 1 ) << QPointF( 2, 0 ) + << QPointF( 1, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 1 ) << QPointF( 0, 2 ) << QPointF( 1, 2 ) << QPointF( 2, 2 ) ); + + QTest::newRow( "closed ring 1.0 initial lag 1.0" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ) + << 1.0 << 0.0 << 1.0 << 0 + << ( QVector< QPointF >() << QPointF( 0, 1 ) << QPointF( 0, 2 ) << QPointF( 1, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 1 ) << QPointF( 2, 0 ) + << QPointF( 1, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 1 ) << QPointF( 0, 2 ) ); + QTest::newRow( "closed ring 2.0 initial lag" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ) + << 2.0 << 0.0 << 1.0 << 0 + << ( QVector< QPointF >() << QPointF( 0, 1 ) << QPointF( 1, 2 ) << QPointF( 2, 1 ) << QPointF( 1, 0 ) << QPointF( 0, 1 ) ); + QTest::newRow( "closed ring 1.0 initial lag 0.5" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ) + << 1.0 << 0.0 << 0.5 << 0 + << ( QVector< QPointF >() << QPointF( 0, 1.5 ) << QPointF( 0.5, 2 ) << QPointF( 1.5, 2 ) << QPointF( 2, 1.5 ) << QPointF( 2, 0.5 ) << QPointF( 1.5, 0 ) + << QPointF( 0.5, 0 ) << QPointF( 0, 0.5 ) << QPointF( 0, 1.5 ) ); + QTest::newRow( "closed ring 1.0 initial offset 0.5" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ) + << 1.0 << 0.5 << 0.0 << 10 + << ( QVector< QPointF >() << QPointF( 0.5, 2 ) << QPointF( 1.5, 2 ) << QPointF( 2, 1.5 ) << QPointF( 2, 0.5 ) << QPointF( 1.5, 0 ) + << QPointF( 0.5, 0 ) << QPointF( 0, 0.5 ) << QPointF( 0, 1.5 ) << QPointF( 0.5, 2.0 ) << QPointF( 1.5, 2.0 ) ); + QTest::newRow( "closed ring 1.0 initial lag 1.5" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ) + << 1.0 << 0.0 << 1.5 << 0 + << ( QVector< QPointF >() << QPointF( 0, 0.5 ) << QPointF( 0, 1.5 ) << QPointF( 0.5, 2 ) << QPointF( 1.5, 2 ) << QPointF( 2, 1.5 ) << QPointF( 2, 0.5 ) << QPointF( 1.5, 0 ) + << QPointF( 0.5, 0 ) << QPointF( 0, 0.5 ) << QPointF( 0, 1.5 ) ); + QTest::newRow( "closed ring 1.0 initial lag 2.0" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ) + << 1.0 << 0.0 << 2.0 << 0 + << ( QVector< QPointF >() << QPointF( 0, 0 ) << QPointF( 0, 1 ) << QPointF( 0, 2 ) << QPointF( 1, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 1 ) << QPointF( 2, 0 ) + << QPointF( 1, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 1 ) << QPointF( 0, 2 ) ); + QTest::newRow( "closed ring 1.0 initial lag 3.0" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ) + << 1.0 << 0.0 << 3.0 << 0 + << ( QVector< QPointF >() << QPointF( 1, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 1 ) << QPointF( 0, 2 ) << QPointF( 1, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 1 ) << QPointF( 2, 0 ) + << QPointF( 1, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 1 ) << QPointF( 0, 2 ) ); + QTest::newRow( "closed ring 1.0 initial lag 3.5" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ) + << 1.0 << 0.0 << 3.5 << 0 + << ( QVector< QPointF >() << QPointF( 1.5, 0 ) << QPointF( 0.5, 0 ) << QPointF( 0, 0.5 ) << QPointF( 0, 1.5 ) << QPointF( 0.5, 2 ) << QPointF( 1.5, 2 ) << QPointF( 2, 1.5 ) << QPointF( 2, 0.5 ) + << QPointF( 1.5, 0 ) << QPointF( 0.5, 0 ) << QPointF( 0, 0.5 ) << QPointF( 0, 1.5 ) ); + QTest::newRow( "closed ring 1.0 initial lag 4.0" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ) + << 1.0 << 0.0 << 4.0 << 0 + << ( QVector< QPointF >() << QPointF( 2, 0 ) << QPointF( 1, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 1 ) << QPointF( 0, 2 ) << QPointF( 1, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 1 ) << QPointF( 2, 0 ) + << QPointF( 1, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 1 ) << QPointF( 0, 2 ) ); + QTest::newRow( "closed ring 1.0 initial lag 5.0" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ) + << 1.0 << 0.0 << 5.0 << 0 + << ( QVector< QPointF >() << QPointF( 2, 1 ) << QPointF( 2, 0 ) << QPointF( 1, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 1 ) << QPointF( 0, 2 ) << QPointF( 1, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 1 ) << QPointF( 2, 0 ) + << QPointF( 1, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 1 ) << QPointF( 0, 2 ) ); + + QTest::newRow( "simulate initial offset 0.5" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) ) + << 2.0 << 1.5 << 0.0 << 0 + << ( QVector< QPointF >() << QPointF( 0.5, 2 ) << QPointF( 2, 1.5 ) ); + QTest::newRow( "simulate initial offset 0.5 lag 0.5" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) ) + << 2.0 << 2.0 - ( 0.5 - 0.5 ) << 0.5 - 0.5 << 0 + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) ); + + QTest::newRow( "simulate initial offset 0.5 lag 0.1" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) ) + << 2.0 << 2.0 - ( 0.5 - 0.1 ) << 0.0 << 0 + << ( QVector< QPointF >() << QPointF( 0.4, 2 ) << QPointF( 2, 1.6 ) ); + QTest::newRow( "simulate initial offset 0.1 lag 0.5" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) ) + << 2.0 << 0.0 << 0.5 - 0.1 << 0 + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 1.6, 2.0 ) << QPointF( 2.0, 0.4 ) ); + + QTest::newRow( "simulate initial offset 0.5 lag -0.1" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) ) + << 2.0 << 2.0 - 0.5 - 0.1 << 0.0 << 0 + << ( QVector< QPointF >() << QPointF( 0.6, 2 ) << QPointF( 2, 1.4 ) ); + QTest::newRow( "simulate initial offset 0.1 lag -0.5" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) ) + << 2.0 << 2.0 - 0.1 - 0.5 << 0.0 << 0 + << ( QVector< QPointF >() << QPointF( 0.6, 2 ) << QPointF( 2.0, 1.4 ) ); + + QTest::newRow( "simulate initial offset 0.5 closed" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ) + << 2.0 << 1.5 << 0.0 << 0 + << ( QVector< QPointF >() << QPointF( 0.5, 2 ) << QPointF( 2, 1.5 ) << QPointF( 1.5, 0 ) << QPointF( 0.0, 0.5 ) ); + QTest::newRow( "simulate initial offset 0.5 lag 0.1 closed" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ) + << 2.0 << 2.0 - ( 0.5 - 0.1 ) << 0.0 << 0 + << ( QVector< QPointF >() << QPointF( 0.4, 2 ) << QPointF( 2, 1.6 ) << QPointF( 1.6, 0 ) << QPointF( 0, 0.4 ) ); + QTest::newRow( "simulate initial offset 0.1 lag 0.5 closed" ) + << ( QVector< QPointF >() << QPointF( 0, 2 ) << QPointF( 2, 2 ) << QPointF( 2, 0 ) << QPointF( 0, 0 ) << QPointF( 0, 2 ) ) + << 2.0 << 0.0 << 0.5 - 0.1 << 0 + << ( QVector< QPointF >() << QPointF( 0, 1.6 ) << QPointF( 1.6, 2 ) << QPointF( 2.0, 0.4 ) << QPointF( 0.4, 0.0 ) << QPointF( 0.0, 1.6 ) ); + +} + +void TestQgsMarkerLineSymbol::collectPoints() +{ + QFETCH( QVector< QPointF >, input ); + QFETCH( double, interval ); + QFETCH( double, initialOffset ); + QFETCH( double, initialLag ); + QFETCH( int, numberPointsRequired ); + QFETCH( QVector< QPointF >, expected ); + + QVector dest; + QgsTemplatedLineSymbolLayerBase::collectOffsetPoints( input, dest, interval, initialOffset, initialLag, numberPointsRequired ); + QCOMPARE( dest, expected ); } bool TestQgsMarkerLineSymbol::render( const QString &testType ) diff --git a/tests/src/python/test_qgshashlinesymbollayer.py b/tests/src/python/test_qgshashlinesymbollayer.py index 8a1ffe1d590..4be492100c8 100644 --- a/tests/src/python/test_qgshashlinesymbollayer.py +++ b/tests/src/python/test_qgshashlinesymbollayer.py @@ -119,6 +119,7 @@ class TestQgsHashedLineSymbolLayer(unittest.TestCase): hash_line.setSubSymbol(line_symbol) hash_line.setHashLength(7) hash_line.setHashAngle(45) + hash_line.setAverageAngleLength(0) s.appendSymbolLayer(hash_line.clone()) @@ -132,6 +133,74 @@ class TestQgsHashedLineSymbolLayer(unittest.TestCase): rendered_image = self.renderGeometry(s, g) assert self.imageCheck('line_hash_no_rotate', 'line_hash_no_rotate', rendered_image) + def testHashAverageAngle(self): + s = QgsLineSymbol() + s.deleteSymbolLayer(0) + + hash_line = QgsHashedLineSymbolLayer(True) + hash_line.setPlacement(QgsTemplatedLineSymbolLayerBase.Interval) + hash_line.setInterval(6) + simple_line = QgsSimpleLineSymbolLayer() + simple_line.setColor(QColor(0, 255, 0)) + simple_line.setWidth(1) + line_symbol = QgsLineSymbol() + line_symbol.changeSymbolLayer(0, simple_line) + hash_line.setSubSymbol(line_symbol) + hash_line.setHashLength(7) + hash_line.setHashAngle(45) + hash_line.setAverageAngleLength(30) + + s.appendSymbolLayer(hash_line.clone()) + + g = QgsGeometry.fromWkt('LineString(0 0, 10 10, 10 0)') + rendered_image = self.renderGeometry(s, g) + assert self.imageCheck('line_hash_average_angle', 'line_hash_average_angle', rendered_image) + + def testHashAverageAngle(self): + s = QgsLineSymbol() + s.deleteSymbolLayer(0) + + hash_line = QgsHashedLineSymbolLayer(True) + hash_line.setPlacement(QgsTemplatedLineSymbolLayerBase.CentralPoint) + simple_line = QgsSimpleLineSymbolLayer() + simple_line.setColor(QColor(0, 255, 0)) + simple_line.setWidth(1) + line_symbol = QgsLineSymbol() + line_symbol.changeSymbolLayer(0, simple_line) + hash_line.setSubSymbol(line_symbol) + hash_line.setHashLength(7) + hash_line.setHashAngle(45) + hash_line.setAverageAngleLength(30) + + s.appendSymbolLayer(hash_line.clone()) + + g = QgsGeometry.fromWkt('LineString(0 0, 10 10, 10 0)') + rendered_image = self.renderGeometry(s, g) + assert self.imageCheck('line_hash_center_average_angle', 'line_hash_center_average_angle', rendered_image) + + def testHashAverageAngleClosedRing(self): + s = QgsLineSymbol() + s.deleteSymbolLayer(0) + + hash_line = QgsHashedLineSymbolLayer(True) + hash_line.setPlacement(QgsTemplatedLineSymbolLayerBase.Interval) + hash_line.setInterval(6) + simple_line = QgsSimpleLineSymbolLayer() + simple_line.setColor(QColor(0, 255, 0)) + simple_line.setWidth(1) + line_symbol = QgsLineSymbol() + line_symbol.changeSymbolLayer(0, simple_line) + hash_line.setSubSymbol(line_symbol) + hash_line.setHashLength(7) + hash_line.setHashAngle(0) + hash_line.setAverageAngleLength(30) + + s.appendSymbolLayer(hash_line.clone()) + + g = QgsGeometry.fromWkt('LineString(0 0, 0 10, 10 10, 10 0, 0 0)') + rendered_image = self.renderGeometry(s, g) + assert self.imageCheck('line_hash_ring_average_angle', 'line_hash_ring_average_angle', rendered_image) + def testHashPlacement(self): s = QgsLineSymbol() s.deleteSymbolLayer(0) @@ -146,6 +215,7 @@ class TestQgsHashedLineSymbolLayer(unittest.TestCase): line_symbol.changeSymbolLayer(0, simple_line) hash_line.setSubSymbol(line_symbol) hash_line.setHashLength(7) + hash_line.setAverageAngleLength(0) s.appendSymbolLayer(hash_line.clone()) @@ -180,6 +250,7 @@ class TestQgsHashedLineSymbolLayer(unittest.TestCase): line_symbol.changeSymbolLayer(0, simple_line) hash_line.setSubSymbol(line_symbol) hash_line.setHashLength(10) + hash_line.setAverageAngleLength(0) s.appendSymbolLayer(hash_line.clone()) self.assertEqual(s.symbolLayer(0).ringFilter(), QgsLineSymbolLayer.AllRings) @@ -226,6 +297,7 @@ class TestQgsHashedLineSymbolLayer(unittest.TestCase): line_symbol.changeSymbolLayer(0, simple_line) hash_line.setSubSymbol(line_symbol) hash_line.setHashLength(10) + hash_line.setAverageAngleLength(0) s.appendSymbolLayer(hash_line.clone()) @@ -253,6 +325,7 @@ class TestQgsHashedLineSymbolLayer(unittest.TestCase): line_symbol.changeSymbolLayer(0, simple_line) hash_line.setSubSymbol(line_symbol) hash_line.setHashLength(10) + hash_line.setAverageAngleLength(0) s.appendSymbolLayer(hash_line.clone()) diff --git a/tests/src/python/test_qgsmarkerlinesymbollayer.py b/tests/src/python/test_qgsmarkerlinesymbollayer.py index 664c1619ed9..ade5b5e0460 100644 --- a/tests/src/python/test_qgsmarkerlinesymbollayer.py +++ b/tests/src/python/test_qgsmarkerlinesymbollayer.py @@ -41,6 +41,7 @@ from qgis.core import (QgsGeometry, QgsSymbolLayerUtils, QgsSimpleMarkerSymbolLayer, QgsLineSymbolLayer, + QgsTemplatedLineSymbolLayerBase, QgsMarkerLineSymbolLayer, QgsMarkerSymbol, QgsGeometryGeneratorSymbolLayer, @@ -137,6 +138,7 @@ class TestQgsMarkerLineSymbolLayer(unittest.TestCase): s3.appendSymbolLayer( QgsMarkerLineSymbolLayer()) s3.symbolLayer(0).setRingFilter(QgsLineSymbolLayer.ExteriorRingOnly) + s3.symbolLayer(0).setAverageAngleLength(0) g = QgsGeometry.fromWkt('Polygon((0 0, 10 0, 10 10, 0 10, 0 0),(1 1, 1 2, 2 2, 2 1, 1 1),(8 8, 9 8, 9 9, 8 9, 8 8))') rendered_image = self.renderGeometry(s3, g) @@ -164,6 +166,7 @@ class TestQgsMarkerLineSymbolLayer(unittest.TestCase): marker_symbol = QgsMarkerSymbol() marker_symbol.changeSymbolLayer(0, marker) marker_line.setSubSymbol(marker_symbol) + marker_line.setAverageAngleLength(0) line_symbol = QgsLineSymbol() line_symbol.changeSymbolLayer(0, marker_line) sym_layer.setSubSymbol(line_symbol) @@ -181,6 +184,97 @@ class TestQgsMarkerLineSymbolLayer(unittest.TestCase): rendered_image = self.renderGeometry(s, g, buffer=4) assert self.imageCheck('part_count_variable', 'part_count_variable', rendered_image) + def testMarkerAverageAngle(self): + s = QgsLineSymbol() + s.deleteSymbolLayer(0) + + marker_line = QgsMarkerLineSymbolLayer(True) + marker_line.setPlacement(QgsTemplatedLineSymbolLayerBase.Interval) + marker_line.setInterval(6) + marker = QgsSimpleMarkerSymbolLayer(QgsSimpleMarkerSymbolLayer.Triangle, 4) + marker.setColor(QColor(255, 0, 0)) + marker.setStrokeStyle(Qt.NoPen) + marker_symbol = QgsMarkerSymbol() + marker_symbol.changeSymbolLayer(0, marker) + marker_line.setSubSymbol(marker_symbol) + marker_line.setAverageAngleLength(60) + line_symbol = QgsLineSymbol() + line_symbol.changeSymbolLayer(0, marker_line) + + s.appendSymbolLayer(marker_line.clone()) + + g = QgsGeometry.fromWkt('LineString(0 0, 10 10, 10 0)') + rendered_image = self.renderGeometry(s, g) + assert self.imageCheck('markerline_average_angle', 'markerline_average_angle', rendered_image) + + def testMarkerAverageAngleRing(self): + s = QgsLineSymbol() + s.deleteSymbolLayer(0) + + marker_line = QgsMarkerLineSymbolLayer(True) + marker_line.setPlacement(QgsTemplatedLineSymbolLayerBase.Interval) + marker_line.setInterval(6) + marker = QgsSimpleMarkerSymbolLayer(QgsSimpleMarkerSymbolLayer.Triangle, 4) + marker.setColor(QColor(255, 0, 0)) + marker.setStrokeStyle(Qt.NoPen) + marker_symbol = QgsMarkerSymbol() + marker_symbol.changeSymbolLayer(0, marker) + marker_line.setSubSymbol(marker_symbol) + marker_line.setAverageAngleLength(60) + line_symbol = QgsLineSymbol() + line_symbol.changeSymbolLayer(0, marker_line) + + s.appendSymbolLayer(marker_line.clone()) + + g = QgsGeometry.fromWkt('LineString(0 0, 0 10, 10 10, 10 0, 0 0)') + rendered_image = self.renderGeometry(s, g) + assert self.imageCheck('markerline_ring_average_angle', 'markerline_ring_average_angle', rendered_image) + + def testMarkerAverageAngleCenter(self): + s = QgsLineSymbol() + s.deleteSymbolLayer(0) + + marker_line = QgsMarkerLineSymbolLayer(True) + marker_line.setPlacement(QgsTemplatedLineSymbolLayerBase.CentralPoint) + marker = QgsSimpleMarkerSymbolLayer(QgsSimpleMarkerSymbolLayer.Triangle, 4) + marker.setColor(QColor(255, 0, 0)) + marker.setStrokeStyle(Qt.NoPen) + marker_symbol = QgsMarkerSymbol() + marker_symbol.changeSymbolLayer(0, marker) + marker_line.setSubSymbol(marker_symbol) + marker_line.setAverageAngleLength(60) + line_symbol = QgsLineSymbol() + line_symbol.changeSymbolLayer(0, marker_line) + + s.appendSymbolLayer(marker_line.clone()) + + g = QgsGeometry.fromWkt('LineString(0 0, 10 10, 10 0)') + rendered_image = self.renderGeometry(s, g) + assert self.imageCheck('markerline_center_average_angle', 'markerline_center_average_angle', rendered_image) + + def testRingNoDupe(self): + s = QgsLineSymbol() + s.deleteSymbolLayer(0) + + marker_line = QgsMarkerLineSymbolLayer(True) + marker_line.setPlacement(QgsTemplatedLineSymbolLayerBase.Interval) + marker_line.setInterval(10) + marker_line.setIntervalUnit(QgsUnitTypes.RenderMapUnits) + marker = QgsSimpleMarkerSymbolLayer(QgsSimpleMarkerSymbolLayer.Circle, 4) + marker.setColor(QColor(255, 0, 0, 100)) + marker.setStrokeStyle(Qt.NoPen) + marker_symbol = QgsMarkerSymbol() + marker_symbol.changeSymbolLayer(0, marker) + marker_line.setSubSymbol(marker_symbol) + line_symbol = QgsLineSymbol() + line_symbol.changeSymbolLayer(0, marker_line) + + s.appendSymbolLayer(marker_line.clone()) + + g = QgsGeometry.fromWkt('LineString(0 0, 0 10, 10 10, 10 0, 0 0)') + rendered_image = self.renderGeometry(s, g) + assert self.imageCheck('markerline_ring_no_dupes', 'markerline_ring_no_dupes', rendered_image) + def renderGeometry(self, symbol, geom, buffer=20): f = QgsFeature() f.setGeometry(geom) diff --git a/tests/testdata/control_images/composer_paper/expected_composerpaper_markerborder/layout/expected_composerpaper_markerborder_mask.png b/tests/testdata/control_images/composer_paper/expected_composerpaper_markerborder/layout/expected_composerpaper_markerborder_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..76cd8d13f4a9683553193fdd90574caa3a17f672 GIT binary patch literal 15064 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 PN1;l~NyVJhy7PYl@M4xq literal 23368 zcmeIac{r8(+cv&XsZ`RW686>-rKp6m3hk6ov{gi*2^k`@ScZ0!5^7~AGL%r6LS_~X zl4RcIDO1QikBj&GXn&vQdEeuCp5N~{-amfF@jIS<^!*yvy6<)0pU-uj*Lj}Tb-Q;$ zO=+%B~&(a3AX}r9w;j=wY^Rk%Vrt=ND7gBM>eE(v;c`2GU5p3m8P@70Q$o6yX+srqg2{zLcZqi0Wt&%Tw6 zplS$h6$siV8N$7@V4TK2*N-%>gaKJem;+omPB z0Sd*6N+<95@@QZozWk>2KQI12EkeZ$F1KDks`6#+%E|Gm{;Qu0T_RbmG23Y7j81mS zlogh7gu9t5yllmuTDH8O4_l{7F4<1R>kU{s@3m(2H}nj}Xqq;pSEl!QE|9Ofdj#+I z(O<=LScR@**{C0P* z`w9+IbHkXeDgqL?#1zBYKLs;fcJ81z8&(FqxXel36MygFP7iWhiBOk`;P-uUaJzw#$|?$_g!>rnC)=VJpNgM( zu?wH$OnMQ+;Z&O?B@(seo%@T(FV5Hgsl5I{nZ+$nd~DoGrp>OK>SunZV->z0n<%?| zOZ?jAVlJ$1^eqWpsxsp!Aa}kVnJ8OW`&?B~F{)x)>c&nNcBFskv0D-g zCEahuNUmK%p@`n3D!rx?UqodCFk)0LT9eo^TqTzu%&w^k)L0@Jj{E-PMM(?5b?<1>HAGS zIX_o!Ii9rurPGO#d_1)o0dt<#N!LDUU zcz#VdMj?kk8OWt9t!8IKGQIuNnV*TBXkA#nd;Vu0XFldjV-qps@5xCiFK~Zn)b+jA z+hkJelsWPKYm&|zn4J!6$tS0!4wI8bhsS;3J;~vgx%fPmbCk;SeEEX&e8S2vKCYlp z+}(W#59+`Bi>lmGe&xH@pd&e;a=2NQ<}kQ*&$Y;RIy=aRmXzXGiq$oXQ(Jr7aLw;V zZ6a^3IWz;AQ&DY~0L$Ux1e!#p&@m;w4v)$+J`3W-A{zdn~lO zVNP|!uFw8$-R$AT8_JiYAGlyAKEl;;3*DIe$}I_nl-;bwNRX4owHi$FoM* zkyu^#c-+((H+Gjk=cG_r;Umo+T}KpZp19zG$WN-bzAqFNnZJm;8h0hgez|Fw*k_ab z4juQErL&1jHt)hE-lYmMApQQicyl;d{!7Md8c!=_<=cmR{J6_a&Fz zmRR>{furZn4Y-xFT_5}xbF2+bsh-5ukso<)t`60@?#w}PYdp0h-L0@}lzl`f?kesj zL#1N!B25#g3^h|(%cP7X5SYx`&VfALT9`rCl~9l$ki}hOMV@NrQiwC{Cs{k2j1#0x(k+s8ij zsOkXjhhJII;Vw6hdWif@&mKP9eV`;RbC!*wVA1(=K=}B{x?|X5N_b^O{*kDP`}2`V zxxG>jmxedkkLmQ?a1kLJG}Qgme$;N7GNqkZv*r)H%>A|wA5T$GOLt5Oqbk!|xJC}d z-NdT7=<+cYpV%#UNWEu^G9B7>jE1D4d{M4ar?Ge1BX`3!wx%OEzDwI&onX(ouNigP z1_+aLwe#!1;VQ12rKbA0zDZky35LN_5>k}TCAu4!idPcJ?K>XQx+CGMl3V$Pe7gFp zb-Fn^Hd*+dlJ!EYPBmeB;umVpT>PSSC{1Va)`(4Pbj9_{DU|j~->r9VEXq-`VdB6v zP>A$rRf<5il-OnZlnM7D=%4dz`*k_8O~@1OF@LnsGHFWftx|F{7J~Bjj|tp;du39y2fsH(Go50~j^ItR zz51clOxi=7E%e%79-!Xm0^GauX=2=py^760TvhfpU9i zJBigpI0etThJ{o9vG{15Ly&n{+i;4f#2*<(Dd9_(s{lBTbCuX5ju6eIqs^**28({j z$151yI!~;6J}Q9kMdJ{k%8X7Iwy7iG8a%QjCbk})Kvb5fFCZs6tHQ%nT>pT=dN0(9 zADvWIGdt;QQ#Yz)@u>&Gm+0%n_#IqQGHf%6?e5=te4=cN8=IqVl?+}!4yIDQEMzf9 zR#7*q^+>RW9dp8FeWw)3V+56wuQ+|zUN*-^CrC1CAsO9$DW4Tk%TTE_?nqFwsK?8% zBLigGUKxGh-7%w( zvU(-f!}aIW0pW|nZp!7s>BZ)y1h!xB(+kBO6e+L$gQE zO|1GI35Tkwq<%-@YNadN{lE1(4*!NLXT?O8FTtUQS7qXL9_N+J#pm6G$a=UX2dfJ& zb8(}^@Mk)(aJ10WZ}fX-Y8p?Eb6pdu{)zoc)oY@ziqR|xUHs+rwA7J2+|*m0rdtv` zVjMzj%O3J}lXIGOMGXjlSaDRrMJGmczDVi5k~q28G>1}`-;EqaIiX#|vi|w{)O~S| zPBvNc*@0bkI7lcnr+07)A8n?nj|z5s*YZ%e!h}@ue`=Ou^n#ss2ic_$k=2_oa#zWZ zD)T?yd3Rwg|IQr8Ole%hSz)?%?i%X(9eNNWZgs!YwQo`uT`|pG_rwIKXy!X$z|X!u z^f)=N-pizPp-o~1+k^MKBfQ1VsJiTJ;|r5vUAfrG0}-Xf~Y1hq3iX5!(7gFS!=o-)w^q zO}{bFzGBznTQm_6YU@I4lxNX^h4%dq$0#L#Wj>6v8+cKNnCRRhtKYZtOREl zG5jHEfe)5I`?QC+n|JhD=K9NJ#kIvZ$I``vQ9cwlfZFE9_PgvFc&XoMh0AliEv2vW zNr!iYOBW!oAQAsV^<+~o{b>+QqMiUBN;n|W%X6IA3w=)T18a`VDk@;ee0u4 z+i5oZ1n+RvWl-&C+h{auX>Vn%bi*YGfKHJQ{sIY_D=+1(zhCCk6XUYXaT6Ai_2pfd zKxDTYt?(4!*v-4T_Z2sUz(cBX$AY@&tTQ@UT>j$t5I65mde^1i_f}31&E9@9?7W7{ zO_j>v=i!lA@uJSuCdVY=Eq-MdJaC&YI+$@;^}JBe3tY-I+AWFtQvne&%TjGWkHmJ< z&i43_Ur*nXFuQLol4X!}*q{F-&foi(s(i{r{I8)@mlKTwnfmhXG7kM}y=oENH2+ON zJ~w(7{leK8snf3uf14}P$Vz}nu2CRw)jwM`aDI`-*%@E-Gn9bCEwF{s$(rcCp>Ixbzws-`=E zwo8+IS<=||k=5m1qQ2dcRnfeEM2{v`H2hvydLeO2wld&n;-i)Ah6}diBv}V1{!mX3 z`rfK}L&eYFQH1e3otVHo;weAB^2Flh5l%uNC<HkRU|M3@)EdR3z z|DP^GsM2z7ZY2gaXgK=n#fw5FNji#($BrLgd#>=lPRu%*ecuCj_hOf^#77a0$s9h~ zt9Z0a4`b6zX;y7VGbeup5}QJNq1z7dx{moA4<=Oaz|TiY0$sFssW`}S?n_RIC+r?s%go4omHcISpihlYlvwU#T$ zluWWKZ2j9L>4mD>RqM`@=X-5BPbfy4ZKk(C&a3 zg}SCmIz5e98LK}>8;$oh#5;}mNx9-PYL@s)U!v3ZA31VF*7Vu36)Qf+>Sjo@?32$X z>3CYuTls=cywZ66DTGxMl8aYbqV!oN^mZ}*w073oc&lvsWP!dHYkE8{_<++3jo0Uf zldAT)H44+&Lf82D`>RUs)Hb#J)~mBKQ7_J-plNIEkwtH^&S$mG)|$rUn#N@%_w`qp zB=LET>ReC?*SNRmc$iLuj-+*#p7*0ikJfM8c;6&xV~nO4)+8-0&D85p2DMutQ76Xk zW3gKB{tY7P!I{r1Rl*a@caZJBef#$M4IAzq2`lY#)IN7Ev$;Q-$ppWiHvL$|97~!Q z^mg48GWpHuMUC0#vNDa#kuud?^74Y*`v(*@?ROZsGkxB{Veg&0cjG1-H+%12C6PU2 zb!wZh18cOZH8F(MBc)$lT-@PEqll`xW)8E3id@OlPvbV1cH2Q@rMYZ=&{$frv$))N zT5-bwD?dMf;**FR4+jUwuAMtqDtT?Xhb{cn(_=Q1=Q^Xb>44)NAt6H}qt@ql1%BS5 zVdH}xf4)9mwM$BBZK0v$j~_o|i{p-j@e~vklwR4>5}VPNZkQKzz-jE<@N}elh?imF z^^e74y>-0PW4#{k?paRbc7Fv?l`k3^8W!ptQBt~OVzLbpaIr>7LCPiAbjiv+^8k)P# zsa1bvoov(0xPHz)Ai~s+v1y+b_4DV?7mba#1{4~S2qn9thy3f8bxN_x6!>DRe@Bu#K0K16V?Gh6DtAl|Wham_Zvjm2R zhpQIs?490myqw(e_JWI-FH_ZmWY@N~w#uB-@-vWeHhXEjQBqR!k%9KJeb!r6@XM~r zufX8!4Y8AtNKh3xx12EiJW`NaPuHJg| zwD;O?TefU@fz#dK;B1LWX}o{q#tnz@2K&~T$zeWbaPngj_0RU%(+%qd1ibbfCvhg= zGSTcX(R|k;wP$F^iat9W;{E#d>sOjlgF*dtqw*?Fg?=6;B}ZCJ_0^x3L*Ms0jaF#I zb*X5ZZY+;MBzbsvbjJ$YRO#B=vfmx6N-@pq$TJl1HV#1MyJJUenD#B7?l}w>%Fy&> z0t@$A|2TG8dmZ-qUTTl2gvUiY=Pw>xwM*|$s#wpxz$)Kte`Qkn<0W~*ZP zIXZpdJtxUU?`SW%vIm%_2(&^t1nP}_<*Qn^(J$K(E!&?3wzAD1VG$W%B$E?PumDY$9Q ztN=p&LxialF1B@``D0MrI1mAA-QP0QUO*z!p+9R}_T+1zLQ9QNh^Kwy^^Sl5N~Z;^ z8kOgoQHR4yQ;pqD_?i2T@vac9n|8@`#Wv zLInwDbad{o7Ex3Dr1IU8b!mL0s~jSlz%e56nXIizK?lA?*m$c+B% zO=I4rDOw&bXnpuE6cZb7fEuC6nPSPx!_&(+n{e)DDiTVWw7QY08rT-K4d zxlTp8-uKsc*#|LyF3n0<4`qE~c060qB5PEW06~nVN56Bw(^;k-#ZmP~8~N|l*hvOe z8<3po)w}ofHeXwdf}9)$w2Ydi8vEkkoZ2S9A+vA!^5q7H4joF#tVb#Li!j!Vs;Hcq z1T-J|1>maF=+mcOpQ@ah{kgF+V>Tuj!4MgIR@d(Q`R@_q?~kZ8 z&5orcqGD_pO0aMl4m8!L@*+cx%uE1heSQ5M@w#B~WZ$i7l#wmvUWCT{7cEBN z_=G8O5GX@b&2{V6VSCrFU*CCeVc_u2J$u&td)>M(X;idER|9N?L?EJb~p9jOliMd%R^D{ zepA)`h^nj@ZKLrv`ThI%uTE{_;UV$zSClqF=^`#gNQeY*>>{^1unj87SXf!@js!@<%sB(l- zK`S4we$Le(j#NVR0nols?K1H^vU#KD5k>k)W>La7>rHOU;pLs!n6=wEQp=ok`C_QXk04+DWv5(-E|b<-@()NIJTesp_j&AqK*v794?|k6My^m?bF|X`*{6M zktR&{;pQNG|Nnr!QZQc694z z*yVz_3U(9)iM`!M`KpK$?dz;s`A5kKx#{tKZvZ99-||THyaxLwT^nWHj_>bp5mibJS7FMLh=2C zJB9LV7QiGISg>7O9It~JU}M2^Tbu6+dNs}bkeSyQTts7hA~Bt1Vp}cbpxuNj*Fl$A zAi=|fvb#EYuN|e`Xg-`b^6`1Vk`uak<;tt?Ch1kUZ-kb+dv%J1{`-pPS5@7FsN-i8 zr}q_JOU@0Bpn-P$|3a63tfvRw;u z?vVSidU+{I26dklEHSNE@+n$+*M(PX+IzxAo8of|*V6aUW&N%3k5}ur^Pg>(LGjwW zd2>*lJ^1l>ldJ19ntg1}8;a;^Tu}Y*teuPd@77hDj`cSQT3cJkzkR!s(D00YW=1C? zz3BZ-&ahup0K#RW*TR+WBSGGB9xMW!G0%sVLs41!$~atmP>g1?Q{UUz}9*Y+w1lImG3o)zIj%c{5@vcH@iT?MZvsr5S&_p+UZBn>+#nA%4@9s`e0^(XTYAyDQ z=R(#7H@y=6{vtN>mQp3b-e;~Y_e7RPzsz7YG_iC$W@NG_3tN_Ix5wuRbT2rFEL^C? z!Ro+okBgjg=#EKQ8XMu1HPPZ>+5F))(6=kxpP=tkNu!pg+1@50Ap(1Hbc*P{f0xj0 zZ?L3_;8zlRkT8pVniyh*ok{z~TFiqDU*kNj58a)eqSVfMPm;xTR$I z5wXk1*G=DzhSx=itdwOJNx8rs>kS({e*F0CQlBj%BA3pe54CPUjP8z*utHqe5UeLceFiC->Emrp*N|jl9gqWRQuGbELpR<)J5%_ zYmFzWFt}b>S0_3N5JsR~wjXZoId0+j4ewn3{$u`Qk=C~S$5r$=jo0<>0uA~bv*O>q zTLUc&EepPPgh7>g=b^RD*EBXxGpc>IcHje-#HWb~hXmV;lwTXOn7e+Ul&2GC_G1r{G zem$*o)zXyA%zEGQUDB6g*le>W>pSFlL#FFH%qM0KjMuMr8tWmR1BlNh?*r?2cu*%T zM>o6PP0^5RsWx32ii(8zLL(3{M)KKpDwLhd*5@IV92`2A$RZyV(nuy)ijOGHkA z{d>`B<_U~9VF%age!gm)q!TzBq~~-pqj74R;B<9f3uG^mLPSb~=6?$FmfJ{ldQDBu zlb|3^clVSg7aB1o_Fnt0(6MWf1g2`zvgONjp>?WeYRKZ{nZnzHW#L#D*a1;U`fPDk zmt0(C11J&B-vIOA{a+Hc!>($9n3g#vOhgo%pTNqe^SlK+Vfo|R(|Ltmn0F;@m9D5)w6Ux<6YGP!;sP)UYzinCQ&bIac-@FAyf)b#9hiH6-u^ ztKU_Sa26!FI5+`uj)=ILXy$fZPkaq9+)w4|^8Cg7hmUQd*+ol+cZ17V4z=Y^l*_Zq z)jEfMY4y-EI5Fsj?1gSxm+(Z{9MQ8+O1Xn} zqn=wr(W)(;_Z2v}_pb~K3zIo#=i(yQmo?sqi1Z+#^XbzkzdvHmHpBrB2|hxKrCE2h zeDxKpS|HM!R|-P{`4vNBy>I=aKa;jFa zns6sUb_L5@5m zEWlNDr?cJ5$7g2ffMK%<3s5v%7Ph~1vhm((k(NT4rXG5OnRF|h6(HCCk&;8;Y{aiZ z-Y~<{JAV9V0Uh<6V>QirDhXVt>N$3WOGSDen41|Me9tKeT6_wlMzW>S87_r_qF-SS z6TByJ#3Oc6TH;*l+t*bIF4q7@#CL$xg_nq56_-Qp*{JXC1{W!B? zbkB$K6%eN?kv9!s^(1WXj!v|4t~yE+{oic`IDX=KnGVN8cgxB;7FJyah3f4odzaof z<}3x5Cwrz{A`A+Aur)7CCx-Z<^|n>IDbE_;`4L+eC|N$~2hDiu$knqyhki=yc{?3)a*lQPm3l+c&&(|P`4}PSOnO zR@NF5O5djD0)9wlpj7=kvC!=nhB9R}*AmrlB|Flz^!s(nh5LN=d!N)6;OO4xGUGt} zdlJ&T5_%7S3J$HFGGGj4;i`jhX15DzMu-xv1v*5)gh=SUNSKH?3rovgz>-?+)#0qv zz$Z`KiNPF0vO@Ov52L0@&*YqCv4TK#;x(U)RJ%Hv9J2rV?w_NMu;!Bz-OUqg&ZU(9 z_{V}EHic2)?T>f4L9JO169dfz=>R^d4udUMm%9gF|M7HU;3GF}e3~1c=)B{nl|6Mj zb7rUjQARRxkQBP{>^T;pL>J-F!c(I6J2lk}jar52b=7wJ)|D2=&do(+?c8U|2gmHL zfTJk*MoWd3{*Lrb6qPO3KZ8#OL}&*@+^&k}kAyO`b(P+^^P#Y^hJ$B{Q}j^ka%IM4 znCb&{Df}s6=-7`svi!zdAzH}W5HL~_!=$Pe$xpUDmSH(TIH@w)?sWVau94QolHI%a z2@byMN{_Y=jFn%$Y}sYwfJV*bu5;tAP5b6zESyaX3m{Ruvl*RY4K%XURu?j0|$bfyHxeR3JFbr zmSO%mHGW(T|FP&Q4C+~@)5=TBxvq9zl?hx21^vta$b-PH3m7XyW2LSn`6p&~&kAxA z6GXkuqGBVnQBC=5g$v#6op6@3U>j@WTi_79H9n@iJoY! z7dSpOiw0J_^{oEfs6JP8+vaRmuLaFxNnFyzTlE^4B%BfaY|sVfG`@z{k%GGEHm|K` zVL2_-9O9t~V}z=RH*|q!e;x+GnmGtBTY9v>w;wKX98F>6s#POkJJyx#p?ZDJRxO0q2p@Cr_Sq z+Pt6-Jq{T$-UiQt?E`P<2-fuycnj(QJc-=4wwD72%Oll-h__B$6MP|dx=66Yi_Qd$ zRdhUc+31;yRVT#>jfADKu};yAhGQ+c9uN<)!ya|uqIoC0kCwl) z-i~>WY8TAY1wWjhhH!T$KF{_YJ94Y5&%8fv8+YjLirnnE>Fm78;SLY++p8px_AigP z+Ncu{G1wXA{TaZH9@4}IUgn8^0)KhD+t!AjsXQY;1A#;t((t&lXCv{lvJvg~#W81U z9LP7`m(l;Zy}-Y<=1}&=!gGm$Abi1!o8D^t0MTd3JrY6;s2}c1ejP@O?g`gjeY=h z0Yo;UoMMKhqFgh=%pgTNreW*Bw8G-zJow#G<^BK#XU^!Pv-?O0v?lZtZGI>VW9*E#ze6_gXw2y{2Osw;bh{&=+r6X2M& z{1<$5aFvV_+r_>pd+;;gLd~*<*=E7n=hm~iy!zYUGQ4MgYGwc2?pALmZ84Gd3q%k? zu_=z@LbMGraR;t`pBg@Fg?TyvkxvdHsbdB{IZ1g!*Cx?hjLcDy+bt(2V4OarU#Jr# zp}@p8)OGDl6f2%`83_|429ly;)%KSe@LEtc^b{3|CG*{cjI;5EcJuFdcxyfTIArGX zJdjKVW_LZC3zMG;BhJXrOH~5Z6#F938d`G!0+$Q+1A0T-3Ekx3iDOQgz+Xe%zDPh@ zNYg7!DuE<0Myz0W!h%Qx&*$NR%GDqa0z_!_()AF^_?5wd6Ch01Dh=Js6osTcpvFx8jOp80s@*AvQ%@MFQnnB~Bopc9Q(JG!aLX8CD%NSQ-_ymhon-y5AgnC)oD!q4`> zWy%<=cH6g3RWFA1bg#S_HJk^A*;hP_mzUS@SbIEex`+H(FD`FO29Xyk$ucHhq$dl3C z#j#SzOJeX7V}r;rA}2scaKKW=zqyk}XC2JcZO zWS_g24Q>vf54DAtD&w z`;H1;*yr=_YP6I^9ZjtHhRUDt_U%Jx%Y?A2d%s$p2?P?8X0KW8yRCD>@^dHc zHc=`cI@YjNM$rz$$7)7(*=5-8AAD7M!Z^14g0VL*wy}B;m0N}%VUH<5`@&( z*Y{gd(T}7mJu%v(KKx7Eq!cGL7{#*3PY<$(6moiR zRZ4BD0ff}>oXOkfd;jTyXxh!2Hz#YWn0{)JkI@1an`D~^->Oe>lX%c#&|8DLf@;gC z-)@BDEO!bF6maT^&xA3ht*zaawG*Q@PD|#E$Qo>3}6Xmp$3!U-2YMO2!==epS;Wtl6PgwFfCh;+*39;PN~Z> z5EV?>lxu8`vn}xBXTw`XhA>l+`H9Nv6$Iouwz{&aTzBls-w~HNq+&5+8TfmhAk1FR zoNw@yDQ@f)jG5y#zfgP?z3mZ5Q+%6ZK4ulj=9s4h zVq%H5YEvprQ&ZEiK&qpo!%1ZHS_Uj{c^KPWHnRBVJUE?L*J}t}iTmBa$5#LrW|90` z&c@TPPkqDudJ_t!V=JiaTXLQ1b0g|btz`N`cuQLQ$MK-DWqnLlfALZA>S+UsS1vYS zYS<)dn)7SZrzmsKi~ZEJ>+w3m2ATZgYDY$8-u<00MlN`O8vDr;Z4MS1S<@T!8#+O|i zm)hvr=y2^CxxTA)$yc)&h+W@jKO$gIip9y(@!8T_^{WTYtxPCINnEyUnGbn}PI_58 z7hkY(rm23C4p~q{JMfP%N4uBI5wIP@0>2%vD@>HlgWdN?OKVzzu$`Ink;nb0}( ztxfvdMCxRsd@c%(B-9q9A z#?`Hb{3vkZa0ApC=)heb{!wV?!d8RU4>AH(0nqEjS8RnVIJ+Css1nY=a1(B>9WK8( zZ5_qbedu;^VU*lkJAPRkQ8L7UUbv>+novaqK4J~9U;Oi@NwRsv8~WsWc%4L9L!l?U zd*_4Z^8+t0!}5sZ21*>~Jx?$P(yr%js*GsJu#YV7x{Mr`$Q<~i#?oz7Z*}};^u6~1 z)fq4GlB1(6eiRnzmIzW4!N2^N4*3T2Dr_ozm|q6>F%Yx z){N5D)jiIDZ)pIUOY~s=>nU}N=A%+dU#<1=*K^#8`c*^{IG%E5vyX)SPTt)L*#S?U zAi|HL=2}4jHRi5+Yl%6SBltir<&g8Xwo>3{D%d;NYm#zu=5f2oRdDeIh&<`eYs_-8 zW=)ME65Br9=Bi0FQm=V5RfG?OzOp?#I0WtL|7@vf-CZt!(0YhmY=Y0TH*+=_4Yf8Z}pcYx4cJ*_C zb@2hMi!VHlHm_mwH5;8%hZ*w($`?H`L_1;x{$hQqWmwx~7KW8Nlbx?YjtqrMIBzAr zu3c*>ruT->sbYAZm@vN;VtL?cNJue=6eco4NGM-QZ9tPX1c6|f_!L)7Div0mSNxnP zZ6n20F{=mZ<^(G0 z)O<*;pihHK2oN_~g*8ohHhn>Q&iRd z4uPK+jQop^j&6hR;%r%O7g?U4WLjm_`WZdy2W=?WekSbEf8sH!Z@i&%$fFn;Ir!4o z4^GveR6kI`Cn#(43fD@XU|MS|u6M=^SQeLE?x;v$U|<0}OvcDz%OEPOf;5mw#)zI9 z<$N!&{=_S8wLMk|JoV17QO2vIG!OJnQ{kW6(<-`I@xm9wHM$G{Ai|Ms-?iG~c2aK3 zV(^sF<~=vzt*XY(M-tTHhlhb{NOXDEhXYoz*Bi`b(DG7^(Nqm96{V>)$zmP>LlmT3;N*~$`^Ty;8RX&pwsG>w z)|?Y-krMCfdSSMWdM#}aL$X!zVHofq#G>Trp8vrW5}2iNzE>8LZowG!JC|V>(<6!? z>f5S6aZE1u%HaKd(Z}IHBTMp;f@CZwp%r(LZd6ms8?Es=x6rS3U;QPXaBT?Hhv|121A7?O!)sSGwX_Pn@I(jSUj|BMSH@zr<;VIO0=ui? zj4oXG9U1WjMe8d9A3ZT^3|iIQ_XnZPY&(UknteL?;f`TM*%av`HKwBUA&)=pVRa#F zXTMCN?~=BPfvfv`@ke2V@f`|fV+;Aa0P|zSH+<%YeMzCme`}5+gYZDpZ+w01&i&Ba zS%Y;J*}4}Qvt#HYWXs3_b5Z(9(4hV@sOUZ*7AS63rx<(t7P@^n09Ot+qS03`m>Kq` z!!_c2@VpwHjQPqN+J9gmha+nc)c7npUq<$HM%Ex=_4Gx?Sqvun+~nlmq6~P~u_8~_ z+#Ooj>4a85)?o70I+bH(O~h0ehQilQ_{@K}`u2a_nwf*I zN_fv=`oUk7i#(g&DF)a>%{pthfJvd*^&Cauglpu(eMNyL$O3duK=*`X83gw?OxXOh zFGCB@48z$dH%}n}T2WD9){uUBa%dOEhlbEQGC~wS4GvaI`l*g~t;2z$n0?_*-T z47T4E4B;GOpp;*R?I~kkFS?JM7ddF2RdlNM3ILVRRE#!7>cotFU2iTUD;t1{O8P_? zuhf994J4t4l7R%MC|E%;*nQHNMaF)h01*ZWsi{vUCnpm&j|QQ=1(T?0j0tfT`*IHs zumtSdhLv|w|9pdxVC5!k#`U?e|2uQSkD@1bC;X;||J52D6mu1&gr;Z)OI)di93of9x}H};wI zSV^?vCMiZ<-h9sO?=MaAfU=rrL$xDN-!Yu0T4IBQnTHS{a=_5^P zS5*dsdQ*|)I$Wpl*kz}Tj6YN!io<2lkFzP};LS&C;-`v!6b6=?BqhMIf=&%D?`q4F zpS1)7p`33xJFPWmdJ4^(AWDLtZAL@FW(90rS(lBYh1RyUx!?09h_=v-@+G~a<4HQl zqbj~aq5L}dB-7{9LT-9IvIzCP%uJe)MbcvcS%}n&!yIa3K8gxnhRPfojdRWV6)an^ z_ZL8GQ0-oy+Gf0GBLQ*HmNIYvq8YK>q%TC+9Y6mLkU&bb=~sW-PrJFK)1h*46C~3i zh}DKC*8^^gFmS=3R>kQT)A}>RUIEho+Nxj4GadK#PpQB{?=?F>!tril;h*YYQeUCy ztYE#&qe=j$fWvR-{%j+`A8ql~|F3%EbzdbURdXM#o3N=%u~5%wP{VCQ2*PpPO{1N) zi)Ip_J*KAi1Ea&(JM>}CAlG-x$Y|LiGR{=}G~hfF*SCi&Tqh<1fjjsq3?<7hgLU}G zP1_DsXjICqJfGdok69y;@`bo(tT@gbSf%F_({uD!IZ+wXR;OuX<`@mXzhrV?Q?uSX z>?}!=oI4PI|B9%AeS~oZN?-j7zNBxr27^w|K&<5Ml5&VUo<#3Ekd#ssp^`j6`he<2 z@MW>MOomf`mJCp{0F!g)?5H^K$#VM}OnY>98z)FxW|uM=7cjCLCr~ZKkpU#b*(@J% ziGWGKpvpNWeIL5%gWt_GQM@dWjVbIQ?IQYOMrXR|tw~$x zl2TH>#{;De0M5$hec1^Z6nKUnnf%;ju{92!K_BE>|4dQ;2y=eSn>eo+ea?@HV+N28 z5ALIB;8 zF1hhyPD;*H1VpD^mWwGv4{(45=i9VtrrPo;;Im(D`gC80T}BXrY6AmdF)^_;yIvjW z8Y`GaR2kJ^C{bkc%evklm>8J6DL-+OnquyO5ZVFa1Iha|D5wwv2-fgu_YZy9oE0}% z9x}U!5C+&aAYQ&C!zSNzyOc~nqwWKtfz|vF^9^knfBu3?9KwWt2S%b;_40xgN_QZs zyQGuhnLSkz=LgNt8r7e(wV283ca8(0B{euHy}pe(8*fvCsz=h!REq)8Da!3o#85SfYwZ|(F(5DEel z2rQVEPe2i}3m|0str?dwg)<-JWEvU~Pt+e6wm;gLkvgMBYw^tJ&~_ez)Kz3c9j*Iz z^gW9pWv$Wa#FH@8d@ys3s0t0d0lppy0@4-;0fZyE@oy%3ZhS5Ql^WlZ)+#!7S;B|% z3!TrA8?QI!)n?g3c(Hoz;Aoo*sa~`EfZTJpDq192lrS;4V5g_D5}!X_j|CaveKc19OzT z1j7f`BD~NsIkHiO-ErB$kP9TEKo-!!%e?xz=+R~+JT1i_gH!vU1IQ&yxK0*K!sreV zocYc<`BjT)#cK{ksoV&9BZ;;=>0KG?v2?-2defnT>u`&a_Dd2x3ECLcupFSz}Ta#np`5cg+D)qL)+()A; zcI!GG!P&iQ*CYI)h|XR%bQN1)8J};MIDe^8E%Kz*D&`d^5$!W)zF><8T|)K#@#WEG z0=Ve2Ffm-ejZ{mNm=%I@&)_#8$1^dFw_(GEd@LZj-jqg1F*LyR@uY_&AeMB|Cp$2V zK=?mUX|l+k`IxkUiw|)h@WG0bo&zxgt`A+FA3-Ia z)8DZPgG6#obF<@;*l2RU@SKUR>mMc-mSA8n?A=27*;e^=V~HG;w_{uNRWx3o!oP&{ zbRD}jXNIvhC*!ITA&j87iAi80)m|WhE|s>PUJ*9WV#^mDQ9ZemUc1Njnc4D1!0KNH z%g8Dd#=$3egO5yNsLL z2qs*wUB<6!S92|edY@PHGw4pNeztj&zjtY`9SJA+u|V3__Bz}kT-znMIl4ciT4Hd3 zPz`9Bgw~NDc%I{*v0T^j=uP>RJwpf#KVI~Tr74p7m5=b}G3*wIk{T;*(fACN5CGuI zAK9N&*NdS2r7%P3+DdZO4^~MB;C1*#5jAEIN?$KEuf}6phUd@!ri|FLsngp>rd@p` z7N(VN_t7`>yT4(_Ko_xWF#3tJj;gJ!gWC$jr&tCLGT=7h^tq0ztE+dAa=LFV>Xhpk zee0JXz3UhcocD-L(o3)G^<2>R9VKo0%XH7=(4S=V1Y*J|+ez)&-akoUu3qUQAv_r! z$-h<4O`;58!nl0l!t*w~^49W<9h4zt*}n-J{4{UQ;>(&>jJ9&`b~aJ`U6*e}+P_3b zmUsC)dnJDri<6ta z1m)hp61?yK$(fTvxo4o1vk{L({LRM=ml5Tl0g5GS5V}U?m{kErCt#8@0u#@JV{V=| zi_s9x8Uz{<%{KcpZuX6g3zDC_8P9b-`6cqqYBc^O@^Ay8pyT-Re~N;K&|6vn&{iPn@X-MFgZw#VubI-`*|?0(w@d$8mCTmln+^)_bF&>>Ndsz5w!g%@xg zt$D{*urh)NNHk<8aiy?BqX`^lry#dba1HX@Y{{H9IWn>w2XDiXq|dKMAI!xtr$0Yh zOO*#e!q-R)7Z05ovD{m}~qN6YnTK)^QI z*MPUMXUOAs078U=K^_F0t@>nwS9R-Oo+gMpBr&|XG}!9UV)NDO*fAge7zrp8Dtg0& zYSjFR1wM3w8NJzldaaoOH;YW)05p{uc#edGFN*KHK+2y|2a5&!K-q-aEb!tWmcmf@ zZ}!`^xIAt7BcClK7T(O~B%K(oo%9J-y$v3~B?y^@$KmEb<>T9C^Mn6^)w);xi`7nl zs22On1%p%8b#rMAqzqB7PqzER`N3!09SWz0jJ^wV*@3?(LUAkp{GTd4Y-*>IVB?c1 zwZKyrmjySa1kuNw!|zx2+FdPL4$iBk+@igUa@z^bH4MiV&)eF4Z&boB{!&+ud?XZ_ z>e6jZE%xu-dj?`mT28K!&lMvz+rV705kz1xF2e}^WlIpx&mIqYG)~|->8%n+L#92l zCp;P_&NfYHLK%`u2@~)!6;Cia{r65nPCDjCfSv`!*|^5nj!nMW#LTj1sKGpdr)}0Z zwTqMiG$~)|WKBk8qb7Fi&pGD5#>TNApk8Xc9uaq|OqfTiFGz1NCpZGIec%ZEO>2B| z7V-&gIZ2dBD)C9l)8LGgD3()3E-uP=5$fTq*Ps|u*g(k*Ev2!~2wpz{=llOT+QzV6 z^P2wcD5@7Kh)2&mrbfFFWCluIH1I|oNO)t!@jxrA4wQ80FTr;G`ua06fF&}!1P2br zus=YQbaZ5bQKO(Co*@`8^_h}+0=UHXgha9HZBMgn*LIQ$jX5o}p}u;RkE7p)_Cw9C ze}(Os{)MoO>(_rlLm9uvoA$4hdk?sVQ^%m95U2F|B2w2VO=>)(75*`4VEFn;DSL=H z#%T$a#oQtm>M<3{c*nlebCVdJdSsqbM>D<(1D~|Re=T!jhw8z;k*`k3fRwskT~Qat zk+9}Ht5R=*n)h$+PwXgAI)G3|8SY{A?Zc!A@eW8@`Lrbl+r4yPV;+Ukpv-`~z<5jy zvkU(&!4TB{GReLKBMJjULi#=9(4rt_)@MG8k2;g;H`LG=NIPw$`B!_WM&FF&W#RejtFn*vcSXd=PhP1=H~mP%?~{cBdmfl z*xkQ@CQ|5Jrbs@~g5=YcTt}4t!Cb2X)N#^WqJoM3D+h4t^Gzyzf%gh91LOk#DA&fp zOE79fCE`8b7)xkb}TIB~2eJVB0l3E(2wqe9G>Jf!-W;!jq+_mLRb zwPv}X!H>#X8F*m8QmS%{rYfocYAG05em)k(`6P2q8?Qv8?zgxd_wOVbL5ZZfupD6p z5K7QflNd>QCRHxIRj^Z_FraLQ&?9WawB0Y;x%f`T!pU3@d*0y_(WKY%85sz1ARN)9 zPFOObdZ3i}hs3Jt--5q&!vzV?_k~mSf!yBtLK$qJL^knX0r)SM)`@gyp*phSJ#FM` zxLqecHcmh#l0GJW3%R0vF$8vpbgarMC0)viR5E=}h%QOXdCLs#5uA37@+A967HJE~ zG-BTVp7K;(tG7IGb-J#JI0Bs90^jhQ_hTv~{nmZ>Lu@{W$N}j6G&hIf*68b~$#BGBw-6neW^F|5 zB8=h(fNn!)3oDbRFb2VWpMm>T$`c&^C-m^dD@GSc=%HV<0Z@a9ai9y?4U$UyB=oY- zy{EXG#`+_efI1+);-qB&A#1T8wqjwROk`cJ-xK1xEt1sldohpJZq)qehwcu|3>SAm zpUqo%sNYt+*Kcp=*7k+K)z%FnV4~@zoJaz+%88+ko ze{7LoSoF5%)oG>XrF1qX*$}Q^! zC`-rxJIIsSxwC{%W~1k-$0I2aBaIFY4yiC0K-U|c384Bs15SSZ2Z0ZwCx;Lceew@{ v{a1_;@AYFGg9Kbji%001r~ zv*8800e_lt4}eLxw6s*Sv$NG|wQ6f?Yus~SVz`08Up#*Nxc2w=tJmvQ(=@fVwpQJ4 zcigjJA_xea;l+y=)#-HV=;)~4zI|ICKYpxUuUE^<%j2E~6QNaA!*P#*N%QE@qZ$r} zwXv~r>TG3YrJAOx4<9~^VHr#gHxM|((b3VFXMaC`{;XE3b&hLb@_0mt5&Hdpot&Kf z^>2nlmv{c%+(2N2PN#F`4ddkGq~_=6&v_9{9sz+7-o1NQ3kwUizP|p)=c}u$_4Vu5 z>i7F&S_YHDBRZU+X_|WY@L}!k?bY1eT(#Tn`u_cUbvm8e-rgSfJQzCxfq$^Pyj)XL zQxnxRO>J*)*WuyexF^9x5D)+&-jgu_6o2rKZrr$03kwT>eCB7o0G#RS)vL9%v{c*M z+jV?=T(@rB`tLtia{~cjq;9uc*REZwe!pMscDoiA7pv3h)VFWn>iP5MZXf`R_Ta&T zx_+9?F<;$0vot>?2w_8oq z)WN|)J$?Fg+|%GfYgN^7+#}%sWo>P(4h|0L)2C0T&U(FG^?JS9+}s?)GPwBMK)BR? z|NdR=c6;R6!ootGoSdBVBDg#RgiG!4fAFx5kB>*5HBD2)p$j;qJfd@{&CJZ48IbAe z>6)9HJLg4kdANaasWnYgKYsied3JDcP)A2c=e!6m50B_vde5FctDT*l>U26)Rn=;> zYIk?HR#sN(&6_u4UI!PKfbg$y@7}$do}RA3U{LLLyZZfpZEbChdlp=10s=t9v%m!g Z0~B+F;oYT=H}n7i002ovPDHLkV1jq(>74)o delta 922 zcmV;L17-Zl5!w-uBn<>}LP=Bz2nYy#2xN$nAsAPDNkl(GC=x@|< zh}6O$gF=EzGlC$fHU<9SE!&0#E)pTo60(RSM`m=+?m(!$oEwfa`d-&^9vAoS!!yr- zt*ROp0Q`?8Mm+$4%Sp4b2Dt%$k!I8bVB9S&E!Fh&bhTQo+S=M0^&A)*ZXoa%PoF-m zgM)(_3^ac^3K1T8wi}C)9IXh!#FuPsoB}t3tj}{M?l~V@87?# zxw*MoUtj;@^VQYW+S}W!UavQzWiUQGqQg0wrm4q|AJ_i=e$C9xRJ+}-@87>yr_-tJ z?d?&|gOL*u_y^0&%QZ0-F;Pv^)b{pv9UUEwdJ>EU0RbT54U-W96o3Ed)~#DLH#hgk zXMV;Dz`3qpzg|mAOSQedUB}1Ab?45V|Ne6|HxK~M)a`cb#*G`*>-DPLZrA+$e04gV z`u6Qxy?pu74FrI*t*orn;^Jbxef##0&%51j4TnPk0pPUz_wU!i!9jif`t{87M~@!) z3JZYKhQnb^O--G9-eqdF1O$N7c6N5^*RNl7_wL;@&yS9d1O$N7dc9uVyLYeV=jTtI zy?OJdZr{G`4FrJmy?*_=c6WEH-|yFOIIL^eu2sL^ug%R(0RiCeR##W6-ELRE->->@ ziTd#2gI{3*kn;wUQ2||lG@~8>7vb5nXSKh-U&qJCH9I?7Q&Us5v9U4gS#YVjfpF!m zudmmaFJEeUdb+yZZZ%C)hlhvt{Q2`yPlHRXRaL`LkAVM|wY9Z6JUpyVpFW*BTUc1A z#l^+i+}s?&GPwNQK)BL=|NdR=cKgh;g@uJWIXSuDMR0Wp2v^#F(a}*IA0MB2)-+8G zhc4id@`%orHZ?VMZa^j{Cu?SA=7JZ&)!_!hmDV&({rK_Y%(KJ8!x{_*7rY3r4v*+u zc`shPsNLP&>U26)Rn=;>YG-GsR#sN(-Me=qUI&+#fbg&I@ZrOnoSdwFzhCWkyL!D| wZEbChdKO%20s=q+#2d3g266(D2ay)}5BXW=<2v_<`~Uy|07*qoM6N<$f<=bpQ2yVoEBDEl!>> zJN@X_r}%Grb2xXd`)0B~ZHMLgM6+WOrx_XkFPB~|$8g}PiUNZQ6O)6|C^eLVU~(vb zLumEKM@N7B`1p9n*|cPOlC%nz=F`Twqj&XIt_jh+UZr^V1=kH%{xx4=VKMM;BiTrzeA`cqv zkhsKEiA5Y#G(Q`A$?Ay0*$6jAwA8yNetxKBY&#}qs{>LPe=gghkd;0Y0 z!n!&;Ioql&k3u)ZfpSe~e?w_eNr_2Wd3pWLLYvxOAjf@tb#=9-{0Tge>CZ%xcI?eLIOVyzI^#|$BrEm=jYr1fBNR(;r7RG-sIH$ z_>jnYI^k~xbK`s;waJAQ6&BAv|BL`eNp*Ggoqe^{bLPzHX>MkIFaIGwPvOJl?(Xi7 z|Nj2oxPQMr&~6KRd-?nOYNao~46%8@4wUCtn|u;zN5og22yb9P@JLvUp|gQu0td$^ jH5vq?seqV_@Q0fzddXk2*|+O~6*q&YtDnm{r-UW|V4`QO literal 2037 zcmeAS@N?(olHy`uVBq!ia0y~yV4MKL9Be?5hW%z|fD}uylV=DA5Y%v_bO8CB1s;*b z3=G_zAk1if^7L8;2KMQmE{-7;bKc&z&z=&=-2PD5*!j>51&IwCjTJVEEfaS9b1Ujr zW@4hSw4(UI?eZ+fyS(vZJyr;6!Sh`R#v9R%l)CA>Cn{D!a~DkmtQVTmzI`( z{O8Xfas8(I-33hhw?X+mj3_uw-D$DvF@WcZr+qUKi@w7t4f6S zK8FWF{BkxD>FMc@U%beeGk<>n#A7ROm1#&28DIExcEK9hvs_cI^oE z*GEOg{v;U9jFIX$Jhr3m@2?p?HnN8#$IBKtg5kkKxdyjT-e)KXZZgHiM_DpUXO@geCy)tyj(f diff --git a/tests/testdata/control_images/symbol_hashline/expected_line_hash_average_angle/expected_line_hash_average_angle.png b/tests/testdata/control_images/symbol_hashline/expected_line_hash_average_angle/expected_line_hash_average_angle.png new file mode 100644 index 0000000000000000000000000000000000000000..b8d4c53fd8f07c058aea38532ab1679f7d0f9d36 GIT binary patch literal 1241 zcmaKsc~Fx_7{;SxTZqFM4HX53ASr!NhnEEHrvfAsER+Q|0SlmcTl>UMu?QHbL0GH7mI^LREhu| z4poXVaLt-a2<{f#89JS)glo1l+@wN!8hPmvjHxeXE-41zq#MUaWwljagadKpqoMa7 z&I#KTzQpbrt)YWWY~ptD{$qFM*RK-CP;{-z|C)-uL>uqxQNeib#o zw#(`-2Km5Qk#m=gMhSE0C6fBCdPlPF5<+}B4+R%Ck?6>*p zrC@-AC)Jv>`@7t?94>J};N{QQo2sebx=SQ2H~3(Wj=p^MTz84@bd1v&i!+omX~cf| z($E~L7ssPZ!JJ`pXq3RTq0|l2mQTD2!1|4GGppX-k9}>Wv+#pG_ld2YfWYpUuk%=p zXUc2^1gK`En`sH<+mX?wzQQsGvl2;V4-mmB;G%u#oO}Lb>4_qYNXfH+0&#z+E;Be5 z(>PrEgl5k3_fr;>_=F*$rzI5rR3F`ZWsT3K=e0C$sgCB%dbrzi^Yk4$E-lfrG1uGg z!B{H&8WDQZ?9H0vm-tHJVnq$~25Sm8^)5j2Eo;i~4I#iUzXGst0yK64^p-{O?CzRF zWv`yG+=i@IA{=MZN?lfR`h!6_jdI~AA7;&~r{qqf(F=ij<5+ZCh8i=##3gMSP#Wa= zXq}&Fnv5^90HQqsO3V2rlA8ZdpjAikiCWu45M$cQthZNt?btKW$|yLPkXR4}^E;EM z!?$U4O-7*>5HtM_Ags{pa6fH%@`dVKvWEZ4VvPM#ZQFC??d%PMIgrZ51#%*HivjVA zswD4@@bpvs@^pZW@rl!k{8xP;fE; zDHOmz2jIsKV$;XZ#0Bc1)xIW=4?{EG%tgK?6s=how(Os=K6zoS){bk_Hk+|=ExEu- z)iPn853IiuuAF6Y@-%2cM3;*6AYwN%Er+nz^7XP z_H_WKTxmub0mU-p!!f?LNZb%1R|FdG9gVJ&O(?$Ne$HE^9fADDw{QrOc-BU;Nq~qPxo`;AvIq4-{2w?>3Mgn`zUi{0{GWYt? zTTB1x)JpZ9`@h}BBjWakNvHywg4rr zv02q|aA&W$c5-ivYX?{*-!I=1`3En3Xm z9^iRmr4@@b^h-UN@fixg$X3godq*8CX7N38=rzxZ6>KjHG8$98H6Xvf6m@hIEPI9Y z2n3^Qvl3l%RKu$-87&~6exQKCB!tYwm>DunRQ z^ynozp!M(nuOEeHd{6atF4d9xGOr*l9TqJIU0vsTXZ*%59_y1TT{FS)5aG*bC$zNl zeGU=sawahEn{$2*00000z|4+o5thRjT7M@4?Mb{bzMOxVgyGuVRlCGRXx>9dtmLil zM8&$iFXmFUNci{}>x{9ng(30ZEbp9_sJLY0f-*J(^<1drz9eisZgcC}6cDljTzBF- zYu>kKW%boyM76-SnIe#|QK-ijbFen-N9_@F6#xJL000000000$`?$Idg=oK^ZrKca z1|~{aVGxMaB+T*3ivy$Hv}(KlE)J_Nd+cri#?ITt$lcwpMQhzB}t4gd8u*b5;A0lt4n8P`Q=9HS};ERZLR*X zaY@(q%Q$pM7r@l{JnItAFOrv5f!RWUC{&8iIfH_KDae;`?j6UgyW!v zaXzQZmM&QF!SVh@k3`uL1L-3j6&oSVgI_oCxOrepEkW#l4?y z?J`(JxoPUAyGaS1A-_zrByNWgZYJ-ip>dsB5!1dbyN^dvu$Jh?p2r}_4*?ha=0>X1 zo;bp@ozSN`VP)hTD>Hjs|ZN=}|FBZ8IRmTYnI zp4Lcn)u$}76k^ER05wIEU{$gBo(#b%WM#HE$I40DVs&f(k5nvN>&PM%E2r~SZ^5in zu_~I~+Hg>@QZnXd>fBSY>Y7@&=x(TJ!8*lKn9bWdw0@eYSUFKJ0Bp738iB>lw!4s$ z8OCESXUv)WHX@fB;+avpn!;-NVS*KslV$M)OJNubt|JJye^IewTU@$mO)f;3EjCkB zwCof9_)dz0ij~RKWwAB21S=Iuwj`C0dxd$-JFTHLIkOp$ptbzEz89xr#pbp!xP4=I zCHy`T`)}<2Ed(o!sW+ifm;d{N0Ftsli9aaT>ph-Rm2wVJ9#jCo%T=D_^LfQV^^t;#waxfL`qyLoS4e46)%wE_^Vwo0*>X+A+Rs(0-eNNa1SFT> zsaOJuo2%$~-)a3ehuJ#5Jm*D%L2w~if7QtToV(D6@5tiar2f@BYCVT{l=muiuoLb2 zvUs=nRH#uTE_MWKsXF=Np&*OYiPT-S) zg-g}5!GwiAc8`3vSQ!a-fKjogh?uHNO@A{fM_{M%+qZkEZXtvogjvR$M&eTK&t@t7 zDD>IVRF(lDSl39{gM9=TtM}4BX6O@DV;L2zN3|N(ahTlF8IpAm#WOCDij{2&l{|Qt zrLbShR6PtH9ZO*+QZ@S(GNZq$8x^Y=sj?tgZAg_719c(&cauy)#j+w*O$e3=sRFP{ zg&Qv_se^IA{{wn99&H}9p&P5%3`m3A^ zJnR%L&obZAAypl2qN_P+gGH)FjK?eX_|sn{_j^d`Zumy3wEC)7wqTK}Nm}C-ej>QN zqow++ou^`vs%0$ID@m|O)nb}9g(uS&+zG|^v`iHpk(Mx-i`Fu@{KvG|%V`m53i zP_al=MBal%s)BMiar9T^;J0Ftsx4Fv0ZxNOsy34B0AmL6AJvyMO{rM)SEbQiy{1$w zPQc?*wV6y0*4*C&HC1c!tm&q|Dk$T^w{0^8tqMV@cBSrY^t&Ge@rr2Y_AiQ7M5=SF zofV5z#nLoi+k!=^0_m8nx${eZRXD0E?;9!>sR|+3_AgT9UySWv+}^2aUE2()DrHfy zNELu3R8N#gDW)}w6L7R@4p!U$L{GKcv`y6z3w}}i$ zF@&J7Z2$lO0000003;o~7vNd`^j?7KMk@%_@{G1#AM}NFh}KyW=Q#a4Uz|8qvcA=t zv4rI%F~aM6sCB7VQZIFPg~C~gB2DvAXY78;sKNwHH_E&@I1W*@2ClCJ`sN3>}{yU-$Sjq@!h@_+t+ztU*<%V4S1c z3#v|%0ZQ4{N9%w>9us3yF-P?0$!IJ7-fd`sR%h>x3}v9bf(U-Xjc zVZe#=GOT3Ml7un_QSEEPB&-7jQp4CP-CY#w1EOXRJGS8#-7M zjf+w{lbxeQ@)s|!{eBjRAB}ww7uJ6I;rRWRV8F)Dtt4J#!SYHx-BMds4tq*Rt@oY_ zad+8~yj>`Iq-;nG?k>?qDK0qkUiH8nAe)_j5dUcS%2~q$600000)S*2w$ys## zqrKkFQ&{mEFkwL2cC1V#(EGI%VFy^|Bb|=7puw;-eT9_EI%Ida)V60o+v=|Esl{%Ry>rl8a&=1#8jT_MG^f^J#nZcP9H00000 zND-bt(wqZ3gt5@iM^!}<`XgKpOeQj4I17uCTE{!r6&oP=_rtohcv|nMeH16qMK8 zg87Y>hwpqpV8TH?000000CSMER7Agnlf}`SQOC8iuP32!eaM_7h@Ti3X77$nWm}LSXI0+oPhB5 zl&N~)E|{xP_1ACdE}eG>c7-UNHB9Ya=b#kXQrvr?;ud?RvFS+;X#_#&rvLx|0000$ z9@o61mPmg6g+*0OY>4(ROw65MXJJuK6FZW>8acak;sfJBx^?MbdUWX^B$48x>=zW4 zYDhd+mG&=8PnhNC>4rBdZxQqrqKrMumPh6bOFJIskj*tW!o;me6NBdau>|1&00000 z0000OM&uif{nxznY0o0MUGt79(Xa}=c)v`vnS$EFCD|3En1BYgLi6c1+;&mfIP*f?uVjL>8F@OI_cRgNAFii87=5l z64BK2c{2b200000fZ_R0WO^=5z8fZT9+h*jwfVNNJ1?1U^h=DlfBoh1??o{;@6+Gw zKjlYE!L@T2N95IByr}2FcEjHtVcSQjSC_%{(ry;tHt{{ZY{vR-(YYKfGH;y+iUjdR zu)IdoF;)BYc$~gRCeB^qc?s@nYg3e$q(HMvvYR($Vr5k%(Yhb+Mhk1p-3q1q)OC&{ z3FX#(sMn zB0;jy`vCcK$It$6-xDRq1dVw2uX>Y>4;!_ zY55l59ek`7`-JZMuc8Q-4~|DD5{wYv;OPj)$}`PaiCA~9uz2(*hiNrbAME4`&c!8>dyh9n~ z3oBdW!mZ!Es)XL@j2|;eYHJ(gB=aMUa|(OdH~;_u000000N|1S0$6Tu&j!Nli2wiq M07*qoM6N<$f_I7_V*mgE literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/symbol_markerline/expected_markerline_ring_no_dupes/expected_markerline_ring_no_dupes.png b/tests/testdata/control_images/symbol_markerline/expected_markerline_ring_no_dupes/expected_markerline_ring_no_dupes.png new file mode 100644 index 0000000000000000000000000000000000000000..290811ae11184b4d9a746d19373a0a2d71473f03 GIT binary patch literal 435 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)K$^oCO|{#S9F5M?jcysy3fA0|R5K zr;B4q#hkY{4fzf^@US>MSX(L9zm$!2zM|PFSz}p&lhewk9*WTeYG+`me>JPVr21dQ z`q-A>Ushj2Hg5ZFzw-JjzBW6(Farhqww*HkwE=4$Haw9~3ljRZHeuoytvT_bQvx?$ zX?2L{pJgt#CqO4kLrhnsTP$#+j3CFKM|T!9onFy(>FHeAx6{8rlvo=T`n$gMAdsGY zfe~ya^@$Z_zINN+)>i7rw0yP{s#+DXo7qn4a0#_N@m_7^6?*|IkoylTm(ws)(SM^a Q&;#Ony85}Sb4q9e06{v4P5=M^ literal 0 HcmV?d00001