From 29b345e2c3d4de7b47003f25dcdfbae3d2bbbec7 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 30 Sep 2024 07:48:04 +1000 Subject: [PATCH] Add scale method option for layout scale bars Instead of always calculating the scale along the bottom of the map, expose a choice of methods to the user (along bottom, middle, top, or average of the three measurements) For new scalebars, default to the average method, which better handles the scenario where the scale at the top or bottom of the map cannot be calculated (eg when the top/bottom of the map falls just outside valid areas for the map's crs) This fixes one of the most common scenarios which cause scale bar widths to blow out to massive sizes Refs #55240 --- python/PyQt6/core/auto_additions/qgis.py | 17 ++ .../layout/qgslayoutitemscalebar.sip.in | 18 ++ python/PyQt6/core/auto_generated/qgis.sip.in | 7 + python/core/auto_additions/qgis.py | 17 ++ .../layout/qgslayoutitemscalebar.sip.in | 18 ++ python/core/auto_generated/qgis.sip.in | 7 + src/core/layout/qgslayoutitemscalebar.cpp | 80 ++++++++- src/core/layout/qgslayoutitemscalebar.h | 17 ++ src/core/qgis.h | 13 ++ src/gui/layout/qgslayoutscalebarwidget.cpp | 22 +++ src/ui/layout/qgslayoutscalebarwidgetbase.ui | 115 ++++++------ tests/src/core/testqgslayoutscalebar.cpp | 170 ++++++++++++++++++ .../expected_scalebar_method.png | Bin 0 -> 21842 bytes 13 files changed, 440 insertions(+), 61 deletions(-) create mode 100644 tests/testdata/control_images/layout_scalebar/expected_scalebar_method/expected_scalebar_method.png diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index f2d7da96884..9a81e904078 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -9697,6 +9697,23 @@ Qgis.PictureFormat.__doc__ = """Picture formats. """ # -- Qgis.PictureFormat.baseClass = Qgis +# monkey patching scoped based enum +Qgis.ScaleCalculationMethod.HorizontalTop.__doc__ = "Calculate horizontally, across top of map" +Qgis.ScaleCalculationMethod.HorizontalMiddle.__doc__ = "Calculate horizontally, across midle of map" +Qgis.ScaleCalculationMethod.HorizontalBottom.__doc__ = "Calculate horizontally, across bottom of map" +Qgis.ScaleCalculationMethod.HorizontalAverage.__doc__ = "Calculate horizontally, using the average of the top, middle and bottom scales" +Qgis.ScaleCalculationMethod.__doc__ = """Scale calculation logic. + +.. versionadded:: 3.40 + +* ``HorizontalTop``: Calculate horizontally, across top of map +* ``HorizontalMiddle``: Calculate horizontally, across midle of map +* ``HorizontalBottom``: Calculate horizontally, across bottom of map +* ``HorizontalAverage``: Calculate horizontally, using the average of the top, middle and bottom scales + +""" +# -- +Qgis.ScaleCalculationMethod.baseClass = Qgis QgsScaleBarSettings.Alignment = Qgis.ScaleBarAlignment # monkey patching scoped based enum QgsScaleBarSettings.AlignLeft = Qgis.ScaleBarAlignment.Left diff --git a/python/PyQt6/core/auto_generated/layout/qgslayoutitemscalebar.sip.in b/python/PyQt6/core/auto_generated/layout/qgslayoutitemscalebar.sip.in index 57ce4fcce5e..ce118586938 100644 --- a/python/PyQt6/core/auto_generated/layout/qgslayoutitemscalebar.sip.in +++ b/python/PyQt6/core/auto_generated/layout/qgslayoutitemscalebar.sip.in @@ -813,6 +813,24 @@ Ownership of ``format`` is transferred to the scalebar. .. seealso:: :py:func:`numericFormat` .. versionadded:: 3.12 +%End + + Qgis::ScaleCalculationMethod method() const; +%Docstring +Returns the scale calculation method, which determines how the bar's scale will be calculated. + +.. seealso:: :py:func:`setMethod` + +.. versionadded:: 3.40 +%End + + void setMethod( Qgis::ScaleCalculationMethod method ); +%Docstring +Sets the scale calculation ``method``, which determines how the bar's scale will be calculated. + +.. seealso:: :py:func:`method` + +.. versionadded:: 3.40 %End void update(); diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index 20ba99053cc..714c9e794b5 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -2786,6 +2786,13 @@ The development version Unknown, }; + enum class ScaleCalculationMethod /BaseType=IntEnum/ + { + HorizontalTop, + HorizontalMiddle, + HorizontalBottom, + HorizontalAverage, + }; enum class ScaleBarAlignment /BaseType=IntEnum/ { diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index 47d8da080ff..19add925729 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -9620,6 +9620,23 @@ Qgis.PictureFormat.__doc__ = """Picture formats. """ # -- Qgis.PictureFormat.baseClass = Qgis +# monkey patching scoped based enum +Qgis.ScaleCalculationMethod.HorizontalTop.__doc__ = "Calculate horizontally, across top of map" +Qgis.ScaleCalculationMethod.HorizontalMiddle.__doc__ = "Calculate horizontally, across midle of map" +Qgis.ScaleCalculationMethod.HorizontalBottom.__doc__ = "Calculate horizontally, across bottom of map" +Qgis.ScaleCalculationMethod.HorizontalAverage.__doc__ = "Calculate horizontally, using the average of the top, middle and bottom scales" +Qgis.ScaleCalculationMethod.__doc__ = """Scale calculation logic. + +.. versionadded:: 3.40 + +* ``HorizontalTop``: Calculate horizontally, across top of map +* ``HorizontalMiddle``: Calculate horizontally, across midle of map +* ``HorizontalBottom``: Calculate horizontally, across bottom of map +* ``HorizontalAverage``: Calculate horizontally, using the average of the top, middle and bottom scales + +""" +# -- +Qgis.ScaleCalculationMethod.baseClass = Qgis QgsScaleBarSettings.Alignment = Qgis.ScaleBarAlignment # monkey patching scoped based enum QgsScaleBarSettings.AlignLeft = Qgis.ScaleBarAlignment.Left diff --git a/python/core/auto_generated/layout/qgslayoutitemscalebar.sip.in b/python/core/auto_generated/layout/qgslayoutitemscalebar.sip.in index 57ce4fcce5e..ce118586938 100644 --- a/python/core/auto_generated/layout/qgslayoutitemscalebar.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitemscalebar.sip.in @@ -813,6 +813,24 @@ Ownership of ``format`` is transferred to the scalebar. .. seealso:: :py:func:`numericFormat` .. versionadded:: 3.12 +%End + + Qgis::ScaleCalculationMethod method() const; +%Docstring +Returns the scale calculation method, which determines how the bar's scale will be calculated. + +.. seealso:: :py:func:`setMethod` + +.. versionadded:: 3.40 +%End + + void setMethod( Qgis::ScaleCalculationMethod method ); +%Docstring +Sets the scale calculation ``method``, which determines how the bar's scale will be calculated. + +.. seealso:: :py:func:`method` + +.. versionadded:: 3.40 %End void update(); diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index f78e841e1d9..cfaf28f6283 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -2786,6 +2786,13 @@ The development version Unknown, }; + enum class ScaleCalculationMethod + { + HorizontalTop, + HorizontalMiddle, + HorizontalBottom, + HorizontalAverage, + }; enum class ScaleBarAlignment { diff --git a/src/core/layout/qgslayoutitemscalebar.cpp b/src/core/layout/qgslayoutitemscalebar.cpp index 257ad9d7a29..1b1203b8aa6 100644 --- a/src/core/layout/qgslayoutitemscalebar.cpp +++ b/src/core/layout/qgslayoutitemscalebar.cpp @@ -290,6 +290,21 @@ void QgsLayoutItemScaleBar::disconnectCurrentMap() mMap = nullptr; } +Qgis::ScaleCalculationMethod QgsLayoutItemScaleBar::method() const +{ + return mMethod; +} + +void QgsLayoutItemScaleBar::setMethod( Qgis::ScaleCalculationMethod method ) +{ + if ( mMethod == method ) + return; + + mMethod = method; + refreshSegmentMillimeters(); + resizeToMinimumWidth(); +} + void QgsLayoutItemScaleBar::refreshUnitsPerSegment( const QgsExpressionContext *context ) { if ( mDataDefinedProperties.isActive( QgsLayoutObject::DataDefinedProperty::ScalebarSegmentWidth ) ) @@ -570,19 +585,62 @@ double QgsLayoutItemScaleBar::mapWidth() const da.setEllipsoid( mLayout->project()->ellipsoid() ); const Qgis::DistanceUnit units = da.lengthUnits(); - double measure = 0; - try + + QList< double > yValues; + switch ( mMethod ) { - measure = da.measureLine( QgsPointXY( mapExtent.xMinimum(), mapExtent.yMinimum() ), - QgsPointXY( mapExtent.xMaximum(), mapExtent.yMinimum() ) ); - measure /= QgsUnitTypes::fromUnitToUnitFactor( mSettings.units(), units ); + case Qgis::ScaleCalculationMethod::HorizontalTop: + yValues << mapExtent.yMaximum(); + break; + + case Qgis::ScaleCalculationMethod::HorizontalMiddle: + yValues << 0.5 * ( mapExtent.yMaximum() + mapExtent.yMinimum() ); + break; + + + case Qgis::ScaleCalculationMethod::HorizontalBottom: + yValues << mapExtent.yMinimum(); + break; + + case Qgis::ScaleCalculationMethod::HorizontalAverage: + yValues << mapExtent.yMaximum(); + yValues << 0.5 * ( mapExtent.yMaximum() + mapExtent.yMinimum() ); + yValues << mapExtent.yMinimum(); + break; } - catch ( QgsCsException & ) + + double sumValidMeasures = 0; + int validMeasureCount = 0; + + for ( const double y : std::as_const( yValues ) ) { - // TODO report errors to user - QgsDebugError( QStringLiteral( "An error occurred while calculating length" ) ); + try + { + double measure = da.measureLine( QgsPointXY( mapExtent.xMinimum(), y ), + QgsPointXY( mapExtent.xMaximum(), y ) ); + if ( std::isnan( measure ) ) + { + // TODO report errors to user + QgsDebugError( QStringLiteral( "An error occurred while calculating length" ) ); + continue; + } + + measure /= QgsUnitTypes::fromUnitToUnitFactor( mSettings.units(), units ); + sumValidMeasures += measure; + validMeasureCount++; + } + catch ( QgsCsException & ) + { + // TODO report errors to user + QgsDebugError( QStringLiteral( "An error occurred while calculating length" ) ); + continue; + } } - return measure; + + if ( validMeasureCount == 0 ) + return std::numeric_limits< double >::quiet_NaN(); + + return sumValidMeasures / validMeasureCount; } } @@ -965,6 +1023,7 @@ bool QgsLayoutItemScaleBar::writePropertiesToElement( QDomElement &composerScale composerScaleBarElem.setAttribute( QStringLiteral( "maxBarWidth" ), mSettings.maximumBarWidth() ); composerScaleBarElem.setAttribute( QStringLiteral( "segmentMillimeters" ), QString::number( mSegmentMillimeters ) ); composerScaleBarElem.setAttribute( QStringLiteral( "numMapUnitsPerScaleBarUnit" ), QString::number( mSettings.mapUnitsPerScaleBarUnit() ) ); + composerScaleBarElem.setAttribute( QStringLiteral( "method" ), qgsEnumValueToKey( mMethod ) ); const QDomElement textElem = mSettings.textFormat().writeXml( doc, rwContext ); composerScaleBarElem.appendChild( textElem ); @@ -1092,6 +1151,9 @@ bool QgsLayoutItemScaleBar::readPropertiesFromElement( const QDomElement &itemEl mSegmentMillimeters = itemElem.attribute( QStringLiteral( "segmentMillimeters" ), QStringLiteral( "0.0" ) ).toDouble(); mSettings.setMapUnitsPerScaleBarUnit( itemElem.attribute( QStringLiteral( "numMapUnitsPerScaleBarUnit" ), QStringLiteral( "1.0" ) ).toDouble() ); + // default to horizontal bottom to keep same behavior for older projects + mMethod = qgsEnumKeyToValue( itemElem.attribute( QStringLiteral( "method" ) ), Qgis::ScaleCalculationMethod::HorizontalBottom ); + const QDomElement lineSymbolElem = itemElem.firstChildElement( QStringLiteral( "lineSymbol" ) ); bool foundLineSymbol = false; if ( !lineSymbolElem.isNull() ) diff --git a/src/core/layout/qgslayoutitemscalebar.h b/src/core/layout/qgslayoutitemscalebar.h index 4db571c5dff..23b963ba980 100644 --- a/src/core/layout/qgslayoutitemscalebar.h +++ b/src/core/layout/qgslayoutitemscalebar.h @@ -648,6 +648,22 @@ class CORE_EXPORT QgsLayoutItemScaleBar: public QgsLayoutItem */ void setNumericFormat( QgsNumericFormat *format SIP_TRANSFER ); + /** + * Returns the scale calculation method, which determines how the bar's scale will be calculated. + * + * \see setMethod() + * \since QGIS 3.40 + */ + Qgis::ScaleCalculationMethod method() const; + + /** + * Sets the scale calculation \a method, which determines how the bar's scale will be calculated. + * + * \see method() + * \since QGIS 3.40 + */ + void setMethod( Qgis::ScaleCalculationMethod method ); + /** * Adjusts the scale bar box size and updates the item. */ @@ -675,6 +691,7 @@ class CORE_EXPORT QgsLayoutItemScaleBar: public QgsLayoutItem QString mMapUuid; QgsScaleBarSettings mSettings; + Qgis::ScaleCalculationMethod mMethod = Qgis::ScaleCalculationMethod::HorizontalAverage; //! Scalebar style std::unique_ptr< QgsScaleBarRenderer > mStyle; diff --git a/src/core/qgis.h b/src/core/qgis.h index 8e5298146e6..40a0c1affc0 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -4897,6 +4897,19 @@ class CORE_EXPORT Qgis }; Q_ENUM( PictureFormat ) + /** + * Scale calculation logic. + * + * \since QGIS 3.40 + */ + enum class ScaleCalculationMethod : int + { + HorizontalTop = 0, //!< Calculate horizontally, across top of map + HorizontalMiddle, //!< Calculate horizontally, across midle of map + HorizontalBottom, //!< Calculate horizontally, across bottom of map + HorizontalAverage, //!< Calculate horizontally, using the average of the top, middle and bottom scales + }; + Q_ENUM( ScaleCalculationMethod ) /** * Scalebar alignment. diff --git a/src/gui/layout/qgslayoutscalebarwidget.cpp b/src/gui/layout/qgslayoutscalebarwidget.cpp index 79bfb9e224c..2d7ac1f81f4 100644 --- a/src/gui/layout/qgslayoutscalebarwidget.cpp +++ b/src/gui/layout/qgslayoutscalebarwidget.cpp @@ -55,6 +55,20 @@ QgsLayoutScaleBarWidget::QgsLayoutScaleBarWidget( QgsLayoutItemScaleBar *scaleBa connect( mMinWidthSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutScaleBarWidget::mMinWidthSpinBox_valueChanged ); connect( mMaxWidthSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutScaleBarWidget::mMaxWidthSpinBox_valueChanged ); connect( mNumberFormatPushButton, &QPushButton::clicked, this, &QgsLayoutScaleBarWidget::changeNumberFormat ); + connect( mMethodCombo, qOverload( &QComboBox::currentIndexChanged ), this, [ = ] + { + if ( !mScalebar ) + { + return; + } + + disconnectUpdateSignal(); + mScalebar->beginCommand( tr( "Set Scalebar Method" ) ); + mScalebar->setMethod( mMethodCombo->currentData().value< Qgis::ScaleCalculationMethod >() ); + mScalebar->update(); + connectUpdateSignal(); + mScalebar->endCommand(); + } ); registerDataDefinedButton( mSegmentsLeftDDBtn, QgsLayoutObject::DataDefinedProperty::ScalebarLeftSegments ); registerDataDefinedButton( mSegmentsRightDDBtn, QgsLayoutObject::DataDefinedProperty::ScalebarRightSegments ); @@ -115,6 +129,11 @@ QgsLayoutScaleBarWidget::QgsLayoutScaleBarWidget( QgsLayoutItemScaleBar *scaleBa mUnitsComboBox->addItem( tr( "Millimeters" ), static_cast< int >( Qgis::DistanceUnit::Millimeters ) ); mUnitsComboBox->addItem( tr( "Inches" ), static_cast< int >( Qgis::DistanceUnit::Inches ) ); + mMethodCombo->addItem( tr( "Average Top, Middle and Bottom Scales" ), QVariant::fromValue( Qgis::ScaleCalculationMethod::HorizontalAverage ) ); + mMethodCombo->addItem( tr( "Calculate along Top of Map" ), QVariant::fromValue( Qgis::ScaleCalculationMethod::HorizontalTop ) ); + mMethodCombo->addItem( tr( "Calculate along Middle of Map" ), QVariant::fromValue( Qgis::ScaleCalculationMethod::HorizontalMiddle ) ); + mMethodCombo->addItem( tr( "Calculate along Bottom of Map" ), QVariant::fromValue( Qgis::ScaleCalculationMethod::HorizontalBottom ) ); + mLineStyleButton->setSymbolType( Qgis::SymbolType::Line ); connect( mLineStyleButton, &QgsSymbolButton::changed, this, &QgsLayoutScaleBarWidget::lineSymbolChanged ); @@ -349,6 +368,8 @@ void QgsLayoutScaleBarWidget::setGuiElements() mMinWidthSpinBox->setValue( mScalebar->minimumBarWidth() ); mMaxWidthSpinBox->setValue( mScalebar->maximumBarWidth() ); + mMethodCombo->setCurrentIndex( mMethodCombo->findData( QVariant::fromValue( mScalebar->method() ) ) ); + populateDataDefinedButtons(); blockMemberSignals( false ); @@ -745,6 +766,7 @@ void QgsLayoutScaleBarWidget::blockMemberSignals( bool block ) mFontButton->blockSignals( block ); mMinWidthSpinBox->blockSignals( block ); mMaxWidthSpinBox->blockSignals( block ); + mMethodCombo->blockSignals( block ); } void QgsLayoutScaleBarWidget::connectUpdateSignal() diff --git a/src/ui/layout/qgslayoutscalebarwidgetbase.ui b/src/ui/layout/qgslayoutscalebarwidgetbase.ui index 368599c9b14..777224db63d 100644 --- a/src/ui/layout/qgslayoutscalebarwidgetbase.ui +++ b/src/ui/layout/qgslayoutscalebarwidgetbase.ui @@ -62,14 +62,14 @@ 0 0 566 - 1018 + 1031 - Qt::StrongFocus + Qt::FocusPolicy::StrongFocus Main Properties @@ -122,7 +122,7 @@ - Qt::StrongFocus + Qt::FocusPolicy::StrongFocus Units @@ -134,10 +134,54 @@ false - - + + + + Text used for labeling the scalebar units, e.g., "m" or "km". This should be matched to reflect the multiplier above. + + + + + - &Label for units + Customize + + + + + + + Specifies the underlying units used for scalebar calculations, e.g., "meters" or "feet" + + + + + + + Specifies how many scalebar units per labeled unit. For example, if your scalebar units are set to "meters", a multiplier of 1000 will result in the scalebar labels in kilometers. + + + 6 + + + 9999999999999.000000000000000 + + + false + + + + + + + Scalebar units + + + + + + + Number format true @@ -160,47 +204,10 @@ - - - - Specifies how many scalebar units per labeled unit. For example, if your scalebar units are set to "meters", a multiplier of 1000 will result in the scalebar labels in kilometers. - - - 6 - - - 9999999999999.000000000000000 - - - false - - - - - - - Specifies the underlying units used for scalebar calculations, e.g., "meters" or "feet" - - - - - + + - Scalebar units - - - - - - - Text used for labeling the scalebar units, e.g., "m" or "km". This should be matched to reflect the multiplier above. - - - - - - - Number format + &Label for units true @@ -210,10 +217,13 @@ - - + + + + + - Customize + Method @@ -223,7 +233,7 @@ - Qt::StrongFocus + Qt::FocusPolicy::StrongFocus Segments @@ -505,7 +515,7 @@ 9999999999999.000000000000000 - + false @@ -526,7 +536,7 @@ - Qt::StrongFocus + Qt::FocusPolicy::StrongFocus Appearance @@ -782,6 +792,7 @@ mMapUnitsPerBarUnitSpinBox mUnitLabelLineEdit mNumberFormatPushButton + mMethodCombo mGroupBoxSegments mSegmentsLeftSpinBox mSegmentsLeftDDBtn diff --git a/tests/src/core/testqgslayoutscalebar.cpp b/tests/src/core/testqgslayoutscalebar.cpp index d0ebf7600d2..ab7defc6d9d 100644 --- a/tests/src/core/testqgslayoutscalebar.cpp +++ b/tests/src/core/testqgslayoutscalebar.cpp @@ -65,6 +65,7 @@ class TestQgsLayoutScaleBar : public QgsTest void hollow(); void hollowDefaults(); void tickSubdivisions(); + void methodTop(); }; void TestQgsLayoutScaleBar::initTestCase() @@ -906,6 +907,175 @@ void TestQgsLayoutScaleBar::tickSubdivisions() QGSVERIFYLAYOUTCHECK( QStringLiteral( "layoutscalebar_tick_subdivisions" ), &l ); } +void TestQgsLayoutScaleBar::methodTop() +{ + QgsLayout l( QgsProject::instance() ); + l.initializeDefaults(); + QgsLayoutItemMap *map1 = new QgsLayoutItemMap( &l ); + map1->attemptSetSceneRect( QRectF( 20, 20, 150, 150 ) ); + map1->setFrameEnabled( false ); + map1->setVisibility( false ); + l.addLayoutItem( map1 ); + // only scale at center of map can be calculated + map1->setExtent( QgsRectangle( -100, -100, 100, 100 ) ); + map1->setCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ); + + QgsLayoutItemScaleBar *scalebar1 = new QgsLayoutItemScaleBar( &l ); + scalebar1->attemptSetSceneRect( QRectF( 20, 10, 50, 20 ) ); + l.addLayoutItem( scalebar1 ); + scalebar1->setLinkedMap( map1 ); + scalebar1->setTextFormat( QgsTextFormat::fromQFont( QgsFontUtils::getStandardTestFont() ) ); + scalebar1->setUnits( Qgis::DistanceUnit::Kilometers ); + scalebar1->setUnitsPerSegment( 10000 ); + scalebar1->setNumberOfSegmentsLeft( 0 ); + scalebar1->setNumberOfSegments( 2 ); + scalebar1->setHeight( 5 ); + scalebar1->setSubdivisionsHeight( 25 ); //ensure subdivisionsHeight is non used in non tick-style scalebars + scalebar1->setMethod( Qgis::ScaleCalculationMethod::HorizontalMiddle ); + Q_NOWARN_DEPRECATED_PUSH + scalebar1->setLineWidth( 1.0 ); + Q_NOWARN_DEPRECATED_POP + qgis::down_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar1->numericFormat() ) )->setShowThousandsSeparator( false ); + scalebar1->setStyle( QStringLiteral( "Single Box" ) ); + + QgsLayoutItemScaleBar *scalebar1A = new QgsLayoutItemScaleBar( &l ); + scalebar1A->attemptSetSceneRect( QRectF( 20, 30, 50, 20 ) ); + l.addLayoutItem( scalebar1A ); + scalebar1A->setLinkedMap( map1 ); + scalebar1A->setTextFormat( QgsTextFormat::fromQFont( QgsFontUtils::getStandardTestFont() ) ); + scalebar1A->setUnits( Qgis::DistanceUnit::Kilometers ); + scalebar1A->setUnitsPerSegment( 10000 ); + scalebar1A->setNumberOfSegmentsLeft( 0 ); + scalebar1A->setNumberOfSegments( 2 ); + scalebar1A->setHeight( 5 ); + scalebar1A->setSubdivisionsHeight( 25 ); //ensure subdivisionsHeight is non used in non tick-style scalebars + scalebar1A->setMethod( Qgis::ScaleCalculationMethod::HorizontalAverage ); + Q_NOWARN_DEPRECATED_PUSH + scalebar1A->setLineWidth( 1.0 ); + Q_NOWARN_DEPRECATED_POP + qgis::down_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar1A->numericFormat() ) )->setShowThousandsSeparator( false ); + scalebar1A->setStyle( QStringLiteral( "Single Box" ) ); + + QgsLayoutItemMap *map2 = new QgsLayoutItemMap( &l ); + map2->attemptSetSceneRect( QRectF( 20, 20, 150, 150 ) ); + map2->setFrameEnabled( false ); + map2->setVisibility( false ); + l.addLayoutItem( map2 ); + // only scale at top of map can be calculated + map2->setExtent( QgsRectangle( -100, -280, 100, -80 ) ); + map2->setCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ); + + QgsLayoutItemScaleBar *scalebar2 = new QgsLayoutItemScaleBar( &l ); + scalebar2->attemptSetSceneRect( QRectF( 20, 50, 50, 20 ) ); + l.addLayoutItem( scalebar2 ); + scalebar2->setLinkedMap( map2 ); + scalebar2->setTextFormat( QgsTextFormat::fromQFont( QgsFontUtils::getStandardTestFont() ) ); + scalebar2->setUnits( Qgis::DistanceUnit::Kilometers ); + scalebar2->setUnitsPerSegment( 1000 ); + scalebar2->setNumberOfSegmentsLeft( 0 ); + scalebar2->setNumberOfSegments( 2 ); + scalebar2->setHeight( 5 ); + scalebar2->setSubdivisionsHeight( 25 ); //ensure subdivisionsHeight is non used in non tick-style scalebars + scalebar2->setMethod( Qgis::ScaleCalculationMethod::HorizontalTop ); + Q_NOWARN_DEPRECATED_PUSH + scalebar2->setLineWidth( 1.0 ); + Q_NOWARN_DEPRECATED_POP + qgis::down_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar2->numericFormat() ) )->setShowThousandsSeparator( false ); + scalebar2->setStyle( QStringLiteral( "Single Box" ) ); + + QgsLayoutItemScaleBar *scalebar2A = new QgsLayoutItemScaleBar( &l ); + scalebar2A->attemptSetSceneRect( QRectF( 20, 70, 50, 20 ) ); + l.addLayoutItem( scalebar2A ); + scalebar2A->setLinkedMap( map2 ); + scalebar2A->setTextFormat( QgsTextFormat::fromQFont( QgsFontUtils::getStandardTestFont() ) ); + scalebar2A->setUnits( Qgis::DistanceUnit::Kilometers ); + scalebar2A->setUnitsPerSegment( 1000 ); + scalebar2A->setNumberOfSegmentsLeft( 0 ); + scalebar2A->setNumberOfSegments( 2 ); + scalebar2A->setHeight( 5 ); + scalebar2A->setSubdivisionsHeight( 25 ); //ensure subdivisionsHeight is non used in non tick-style scalebars + scalebar2A->setMethod( Qgis::ScaleCalculationMethod::HorizontalAverage ); + Q_NOWARN_DEPRECATED_PUSH + scalebar2A->setLineWidth( 1.0 ); + Q_NOWARN_DEPRECATED_POP + qgis::down_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar2A->numericFormat() ) )->setShowThousandsSeparator( false ); + scalebar2A->setStyle( QStringLiteral( "Single Box" ) ); + + QgsLayoutItemMap *map3 = new QgsLayoutItemMap( &l ); + map3->attemptSetSceneRect( QRectF( 20, 90, 150, 150 ) ); + map3->setFrameEnabled( false ); + map3->setVisibility( false ); + l.addLayoutItem( map3 ); + // only scale at bottom of map can be calculated + map3->setExtent( QgsRectangle( -100, 80, 100, 280 ) ); + map3->setCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ); + + QgsLayoutItemScaleBar *scalebar3 = new QgsLayoutItemScaleBar( &l ); + scalebar3->attemptSetSceneRect( QRectF( 20, 90, 50, 20 ) ); + l.addLayoutItem( scalebar3 ); + scalebar3->setLinkedMap( map3 ); + scalebar3->setTextFormat( QgsTextFormat::fromQFont( QgsFontUtils::getStandardTestFont() ) ); + scalebar3->setUnits( Qgis::DistanceUnit::Kilometers ); + scalebar3->setUnitsPerSegment( 1000 ); + scalebar3->setNumberOfSegmentsLeft( 0 ); + scalebar3->setNumberOfSegments( 2 ); + scalebar3->setHeight( 5 ); + scalebar3->setSubdivisionsHeight( 25 ); //ensure subdivisionsHeight is non used in non tick-style scalebars + scalebar3->setMethod( Qgis::ScaleCalculationMethod::HorizontalBottom ); + Q_NOWARN_DEPRECATED_PUSH + scalebar3->setLineWidth( 1.0 ); + Q_NOWARN_DEPRECATED_POP + qgis::down_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar3->numericFormat() ) )->setShowThousandsSeparator( false ); + scalebar3->setStyle( QStringLiteral( "Single Box" ) ); + + QgsLayoutItemScaleBar *scalebar3A = new QgsLayoutItemScaleBar( &l ); + scalebar3A->attemptSetSceneRect( QRectF( 20, 110, 50, 20 ) ); + l.addLayoutItem( scalebar3A ); + scalebar3A->setLinkedMap( map3 ); + scalebar3A->setTextFormat( QgsTextFormat::fromQFont( QgsFontUtils::getStandardTestFont() ) ); + scalebar3A->setUnits( Qgis::DistanceUnit::Kilometers ); + scalebar3A->setUnitsPerSegment( 1000 ); + scalebar3A->setNumberOfSegmentsLeft( 0 ); + scalebar3A->setNumberOfSegments( 2 ); + scalebar3A->setHeight( 5 ); + scalebar3A->setSubdivisionsHeight( 25 ); //ensure subdivisionsHeight is non used in non tick-style scalebars + scalebar3A->setMethod( Qgis::ScaleCalculationMethod::HorizontalAverage ); + Q_NOWARN_DEPRECATED_PUSH + scalebar3A->setLineWidth( 1.0 ); + Q_NOWARN_DEPRECATED_POP + qgis::down_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar3A->numericFormat() ) )->setShowThousandsSeparator( false ); + scalebar3A->setStyle( QStringLiteral( "Single Box" ) ); + + QgsLayoutItemMap *map4 = new QgsLayoutItemMap( &l ); + map4->attemptSetSceneRect( QRectF( 20, 90, 150, 150 ) ); + map4->setFrameEnabled( false ); + map4->setVisibility( false ); + l.addLayoutItem( map4 ); + // scale is valid everywhere + map4->setExtent( QgsRectangle( -80, -80, 80, 80 ) ); + map4->setCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ); + + QgsLayoutItemScaleBar *scalebar4 = new QgsLayoutItemScaleBar( &l ); + scalebar4->attemptSetSceneRect( QRectF( 20, 130, 50, 20 ) ); + l.addLayoutItem( scalebar4 ); + scalebar4->setLinkedMap( map4 ); + scalebar4->setTextFormat( QgsTextFormat::fromQFont( QgsFontUtils::getStandardTestFont() ) ); + scalebar4->setUnits( Qgis::DistanceUnit::Kilometers ); + scalebar4->setUnitsPerSegment( 5000 ); + scalebar4->setNumberOfSegmentsLeft( 0 ); + scalebar4->setNumberOfSegments( 2 ); + scalebar4->setHeight( 5 ); + scalebar4->setSubdivisionsHeight( 25 ); //ensure subdivisionsHeight is non used in non tick-style scalebars + scalebar4->setMethod( Qgis::ScaleCalculationMethod::HorizontalAverage ); + Q_NOWARN_DEPRECATED_PUSH + scalebar4->setLineWidth( 1.0 ); + Q_NOWARN_DEPRECATED_POP + qgis::down_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar4->numericFormat() ) )->setShowThousandsSeparator( false ); + scalebar4->setStyle( QStringLiteral( "Single Box" ) ); + + QGSVERIFYLAYOUTCHECK( QStringLiteral( "scalebar_method" ), &l ); +} + QGSTEST_MAIN( TestQgsLayoutScaleBar ) #include "testqgslayoutscalebar.moc" diff --git a/tests/testdata/control_images/layout_scalebar/expected_scalebar_method/expected_scalebar_method.png b/tests/testdata/control_images/layout_scalebar/expected_scalebar_method/expected_scalebar_method.png new file mode 100644 index 0000000000000000000000000000000000000000..7f20b9695d413dd16025f4b22e756b0b99189692 GIT binary patch literal 21842 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 aINbBX_P$NrQiHNVQj}Lcl_q!b&;JW6BWaBQ literal 0 HcmV?d00001