From 0fd267ddd52c242c29714187bb0720ffe3086d45 Mon Sep 17 00:00:00 2001 From: McGuiness Date: Sat, 28 Feb 2026 23:09:02 +1000 Subject: [PATCH 1/3] Add contour line labeling for raster layers Introduces on-the-fly contour line labels for raster DEM layers using GDAL's contour generation API. Contour geometries are generated at render time and registered with the PAL labeling engine for placement along the lines. New classes: - QgsRasterContourLabelProvider: generates contour lines from raster data and registers them as label features - QgsRasterLayerContourLabeling: configuration class with text format, numeric format, thinning, and scale-based visibility settings The label provider reads band, contour interval, index interval, and downscale factor from QgsRasterContourRenderer (Symbology tab) to avoid settings duplication. Sketched-With: Claude Co-Authored-By: Claude Opus 4.6 --- .../qgsrastercontourlabeling.py | 7 + .../raster/qgsrastercontourlabeling.sip.in | 93 ++++ .../raster/qgsrasterlabeling.sip.in | 2 + python/PyQt6/core/core_auto.sip | 1 + src/core/CMakeLists.txt | 2 + src/core/raster/qgsrastercontourlabeling.cpp | 472 ++++++++++++++++++ src/core/raster/qgsrastercontourlabeling.h | 142 ++++++ src/core/raster/qgsrasterlabeling.cpp | 5 + src/core/raster/qgsrasterlabeling.h | 16 +- src/gui/CMakeLists.txt | 2 + .../qgsrastercontourlabelsettingswidget.cpp | 230 +++++++++ .../qgsrastercontourlabelsettingswidget.h | 68 +++ src/gui/raster/qgsrasterlabelingwidget.cpp | 50 ++ tests/src/python/test_qgsrasterlabeling.py | 161 ++++++ 14 files changed, 1244 insertions(+), 7 deletions(-) create mode 100644 python/PyQt6/core/auto_additions/qgsrastercontourlabeling.py create mode 100644 python/PyQt6/core/auto_generated/raster/qgsrastercontourlabeling.sip.in create mode 100644 src/core/raster/qgsrastercontourlabeling.cpp create mode 100644 src/core/raster/qgsrastercontourlabeling.h create mode 100644 src/gui/raster/qgsrastercontourlabelsettingswidget.cpp create mode 100644 src/gui/raster/qgsrastercontourlabelsettingswidget.h diff --git a/python/PyQt6/core/auto_additions/qgsrastercontourlabeling.py b/python/PyQt6/core/auto_additions/qgsrastercontourlabeling.py new file mode 100644 index 000000000000..d94c2f846a97 --- /dev/null +++ b/python/PyQt6/core/auto_additions/qgsrastercontourlabeling.py @@ -0,0 +1,7 @@ +# The following has been generated automatically from src/core/raster/qgsrastercontourlabeling.h +try: + QgsRasterLayerContourLabeling.create = staticmethod(QgsRasterLayerContourLabeling.create) + QgsRasterLayerContourLabeling.__overridden_methods__ = ['type', 'clone', 'save', 'accept', 'requiresAdvancedEffects', 'hasNonDefaultCompositionMode', 'multiplyOpacity', 'isInScaleRange'] + QgsRasterLayerContourLabeling.__group__ = ['raster'] +except (NameError, AttributeError): + pass diff --git a/python/PyQt6/core/auto_generated/raster/qgsrastercontourlabeling.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrastercontourlabeling.sip.in new file mode 100644 index 000000000000..f78efacffe6d --- /dev/null +++ b/python/PyQt6/core/auto_generated/raster/qgsrastercontourlabeling.sip.in @@ -0,0 +1,93 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrastercontourlabeling.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ + + + + + +class QgsRasterLayerContourLabeling : QgsAbstractRasterLayerLabeling +{ +%Docstring(signature="appended") +Labeling configuration for raster contour lines. + +Produces labels placed along contour lines generated on-the-fly from +raster data. + +.. versionadded:: 3.44 +%End + +%TypeHeaderCode +#include "qgsrastercontourlabeling.h" +%End + public: + + QgsRasterLayerContourLabeling(); + ~QgsRasterLayerContourLabeling(); + + virtual QString type() const; + + virtual QgsRasterLayerContourLabeling *clone() const /Factory/; + + virtual QDomElement save( QDomDocument &doc, const QgsReadWriteContext &context ) const; + + virtual bool accept( QgsStyleEntityVisitorInterface *visitor ) const; + + virtual bool requiresAdvancedEffects() const; + + virtual bool hasNonDefaultCompositionMode() const; + + virtual void multiplyOpacity( double opacityFactor ); + + virtual bool isInScaleRange( double scale ) const; + + + static QgsRasterLayerContourLabeling *create( const QDomElement &element, const QgsReadWriteContext &context ) /Factory/; +%Docstring +Creates a QgsRasterLayerContourLabeling from a DOM element with saved +configuration +%End + + QgsTextFormat textFormat() const; + void setTextFormat( const QgsTextFormat &format ); + + const QgsNumericFormat *numericFormat() const; + void setNumericFormat( QgsNumericFormat *format /Transfer/ ); + + bool labelIndexOnly() const; + void setLabelIndexOnly( bool indexOnly ); + + double priority() const; + void setPriority( double priority ); + + QgsLabelPlacementSettings &placementSettings(); + void setPlacementSettings( const QgsLabelPlacementSettings &settings ); + + QgsLabelThinningSettings &thinningSettings(); + void setThinningSettings( const QgsLabelThinningSettings &settings ); + + double zIndex() const; + void setZIndex( double index ); + + double maximumScale() const; + void setMaximumScale( double scale ); + + double minimumScale() const; + void setMinimumScale( double scale ); + + void setScaleBasedVisibility( bool enabled ); + bool hasScaleBasedVisibility() const; + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrastercontourlabeling.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterlabeling.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterlabeling.sip.in index bb74e296bedb..830d1542f4c6 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterlabeling.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterlabeling.sip.in @@ -28,6 +28,8 @@ Abstract base class for labeling settings for raster layers. %ConvertToSubClassCode if ( sipCpp->type() == "simple" ) sipType = sipType_QgsRasterLayerSimpleLabeling; + else if ( sipCpp->type() == "contour" ) + sipType = sipType_QgsRasterLayerContourLabeling; else sipType = 0; %End diff --git a/python/PyQt6/core/core_auto.sip b/python/PyQt6/core/core_auto.sip index 9ebe607256bb..d0a56acba19c 100644 --- a/python/PyQt6/core/core_auto.sip +++ b/python/PyQt6/core/core_auto.sip @@ -680,6 +680,7 @@ %Include auto_generated/raster/qgsrasteridentifyresult.sip %Include auto_generated/raster/qgsrasterinterface.sip %Include auto_generated/raster/qgsrasteriterator.sip +%Include auto_generated/raster/qgsrastercontourlabeling.sip %Include auto_generated/raster/qgsrasterlabeling.sip %Include auto_generated/raster/qgsrasterlayer.sip %Include auto_generated/raster/qgsrasterlayerelevationproperties.sip diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index c7fdd1ed13fd..7b75d845769a 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -802,6 +802,7 @@ set(QGIS_CORE_SRCS raster/qgsraster.cpp raster/qgsrasterblock.cpp raster/qgsrasterchecker.cpp + raster/qgsrastercontourlabeling.cpp raster/qgsrastercontourrenderer.cpp raster/qgsrasterdataprovider.cpp raster/qgsrasterdataproviderelevationproperties.cpp @@ -1964,6 +1965,7 @@ set(QGIS_CORE_HDRS raster/qgsrasterbandstats.h raster/qgsrasterblock.h raster/qgsrasterchecker.h + raster/qgsrastercontourlabeling.h raster/qgsrastercontourrenderer.h raster/qgsrasterdataprovider.h raster/qgsrasterdataproviderelevationproperties.h diff --git a/src/core/raster/qgsrastercontourlabeling.cpp b/src/core/raster/qgsrastercontourlabeling.cpp new file mode 100644 index 000000000000..de848c72bca9 --- /dev/null +++ b/src/core/raster/qgsrastercontourlabeling.cpp @@ -0,0 +1,472 @@ +/*************************************************************************** + qgsrastercontourlabeling.cpp + --------------- + begin : February 2026 + copyright : (C) 2026 by the QGIS project + email : info at qgis dot org + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsrastercontourlabeling.h" +#include "qgsrastercontourrenderer.h" + +#include + +#include "qgsapplication.h" +#include "qgsbasicnumericformat.h" +#include "qgscoordinatetransform.h" +#include "qgsgeos.h" +#include "qgslinestring.h" +#include "qgsmessagelog.h" +#include "qgsnumericformat.h" +#include "qgsnumericformatregistry.h" +#include "qgsrasterdataprovider.h" +#include "qgsrasteriterator.h" +#include "qgsrasterlayer.h" +#include "qgsrasterlayerrenderer.h" +#include "qgsrasterpipe.h" +#include "qgsrasterviewport.h" +#include "qgsscaleutils.h" +#include "qgsstyle.h" +#include "qgsstyleentityvisitor.h" +#include "qgstextlabelfeature.h" + +#include + +using namespace Qt::StringLiterals; + +// +// QgsRasterContourLabelProvider +// + +struct ContourLabelData +{ + QgsRasterContourLabelProvider *provider; + QgsRenderContext *context; + QgsNumericFormat *numericFormat; + QgsNumericFormatContext *numericContext; + QgsRectangle extent; + int inputWidth; + int inputHeight; + QgsCoordinateTransform transform; + double contourIndexInterval; + bool labelIndexOnly; +}; + +static CPLErr _contourLabelWriter( double dfLevel, int nPoints, double *padfX, double *padfY, void *ptr ) +{ + ContourLabelData *data = static_cast( ptr ); + + if ( data->labelIndexOnly && data->contourIndexInterval > 0 ) + { + if ( !qgsDoubleNear( fmod( dfLevel, data->contourIndexInterval ), 0 ) ) + return CE_None; + } + + if ( nPoints < 2 ) + return CE_None; + + QVector xCoords( nPoints ); + QVector yCoords( nPoints ); + for ( int i = 0; i < nPoints; ++i ) + { + // pixel coords → layer CRS + xCoords[i] = data->extent.xMinimum() + ( padfX[i] / data->inputWidth ) * data->extent.width(); + yCoords[i] = data->extent.yMaximum() - ( padfY[i] / data->inputHeight ) * data->extent.height(); + } + + QgsLineString lineString( xCoords, yCoords ); + + if ( !data->transform.isShortCircuited() ) + { + try + { + lineString.transform( data->transform ); + } + catch ( QgsCsException & ) + { + return CE_None; + } + } + + const QString labelText = data->numericFormat->formatDouble( dfLevel, *data->numericContext ); + data->provider->addContourLabel( lineString, labelText, *data->context ); + + return CE_None; +} + + +QgsRasterContourLabelProvider::QgsRasterContourLabelProvider( QgsRasterLayer *layer ) + : QgsRasterLayerLabelProvider( layer ) +{ + mPlacement = Qgis::LabelPlacement::Line; + mFlags |= MergeConnectedLines; +} + +QgsRasterContourLabelProvider::~QgsRasterContourLabelProvider() = default; + +void QgsRasterContourLabelProvider::addContourLabel( const QgsLineString &line, const QString &text, QgsRenderContext &context ) +{ + const QgsMapToPixel &m2p = context.mapToPixel(); + const double uPP = m2p.mapUnitsPerPixel(); + + QgsLineString geom( line ); + + if ( !qgsDoubleNear( m2p.mapRotation(), 0 ) ) + { + QgsPointXY center = context.mapExtent().center(); + QTransform t = QTransform::fromTranslate( center.x(), center.y() ); + t.rotate( -m2p.mapRotation() ); + t.translate( -center.x(), -center.y() ); + geom.transform( t ); + } + + const QgsTextDocument doc = QgsTextDocument::fromTextAndFormat( { text }, mFormat ); + QgsTextDocumentMetrics documentMetrics = QgsTextDocumentMetrics::calculateMetrics( doc, mFormat, context ); + const QSizeF size = documentMetrics.documentSize( Qgis::TextLayoutMode::Labeling, Qgis::TextOrientation::Horizontal ); + + auto feature = std::make_unique( mLabels.size(), + QgsGeos::asGeos( &geom ), + QSizeF( size.width() * uPP, + size.height() * uPP ) ); + + feature->setDocument( doc, documentMetrics ); + feature->setZIndex( mZIndex ); + feature->setOverlapHandling( mPlacementSettings.overlapHandling() ); + + mLabels.append( feature.release() ); +} + +void QgsRasterContourLabelProvider::generateLabels( QgsRenderContext &context, QgsRasterPipe *pipe, QgsRasterViewPort *rasterViewPort, QgsRasterLayerRendererFeedback *feedback ) +{ + if ( !pipe ) + return; + + QgsRasterDataProvider *provider = pipe->provider(); + if ( !provider ) + return; + + if ( provider->xSize() == 0 || provider->ySize() == 0 ) + return; + + if ( !rasterViewPort ) + return; + + QgsCoordinateTransform layerToMapTransform = context.coordinateTransform(); + layerToMapTransform.setBallparkTransformsAreAppropriate( true ); + QgsRectangle layerVisibleExtent; + try + { + layerVisibleExtent = layerToMapTransform.transformBoundingBox( rasterViewPort->mDrawnExtent, Qgis::TransformDirection::Reverse ); + } + catch ( QgsCsException &cs ) + { + QgsMessageLog::logMessage( QObject::tr( "Could not reproject view extent: %1" ).arg( cs.what() ), QObject::tr( "Raster" ) ); + return; + } + + int subRegionWidth = 0; + int subRegionHeight = 0; + int subRegionLeft = 0; + int subRegionTop = 0; + QgsRectangle rasterSubRegion = QgsRasterIterator::subRegion( + provider->extent(), + provider->xSize(), + provider->ySize(), + layerVisibleExtent, + subRegionWidth, + subRegionHeight, + subRegionLeft, + subRegionTop ); + + const int inputWidth = std::max( 1, static_cast( std::round( subRegionWidth / mDownscale ) ) ); + const int inputHeight = std::max( 1, static_cast( std::round( subRegionHeight / mDownscale ) ) ); + + std::unique_ptr inputBlock( provider->block( mInputBand, rasterSubRegion, inputWidth, inputHeight, feedback ) ); + if ( !inputBlock || inputBlock->isEmpty() ) + return; + + if ( !inputBlock->convert( Qgis::DataType::Float64 ) ) + return; + + double *scanline = reinterpret_cast( inputBlock->bits() ); + + QgsNumericFormatContext numericContext; + numericContext.setExpressionContext( context.expressionContext() ); + + ContourLabelData clData; + clData.provider = this; + clData.context = &context; + clData.numericFormat = numericFormat(); + clData.numericContext = &numericContext; + clData.extent = rasterSubRegion; + clData.inputWidth = inputWidth; + clData.inputHeight = inputHeight; + clData.transform = layerToMapTransform; + clData.contourIndexInterval = mContourIndexInterval; + clData.labelIndexOnly = mLabelIndexOnly; + + const double contourBase = 0.; + GDALContourGeneratorH cg = GDAL_CG_Create( inputWidth, inputHeight, + inputBlock->hasNoDataValue(), inputBlock->noDataValue(), + mContourInterval, contourBase, + _contourLabelWriter, static_cast( &clData ) ); + + for ( int i = 0; i < inputHeight; ++i ) + { + if ( feedback && feedback->isCanceled() ) + break; + GDAL_CG_FeedLine( cg, scanline ); + scanline += inputWidth; + } + + GDAL_CG_Destroy( cg ); +} + + +// +// QgsRasterLayerContourLabeling +// + +QgsRasterLayerContourLabeling::QgsRasterLayerContourLabeling() + : mNumericFormat( std::make_unique() ) +{ + mThinningSettings.setMaximumNumberLabels( 2000 ); + mThinningSettings.setLimitNumberLabelsEnabled( true ); +} + +QgsRasterLayerContourLabeling::~QgsRasterLayerContourLabeling() = default; + +QString QgsRasterLayerContourLabeling::type() const +{ + return u"contour"_s; +} + +QgsRasterLayerContourLabeling *QgsRasterLayerContourLabeling::clone() const +{ + auto res = std::make_unique(); + res->setTextFormat( mTextFormat ); + + if ( mNumericFormat ) + res->mNumericFormat.reset( mNumericFormat->clone() ); + + res->setLabelIndexOnly( mLabelIndexOnly ); + res->setPriority( mPriority ); + res->setPlacementSettings( mPlacementSettings ); + res->setThinningSettings( mThinningSettings ); + res->setZIndex( mZIndex ); + res->setScaleBasedVisibility( mScaleVisibility ); + res->setMaximumScale( mMaximumScale ); + res->setMinimumScale( mMinimumScale ); + + return res.release(); +} + +std::unique_ptr QgsRasterLayerContourLabeling::provider( QgsRasterLayer *layer ) const +{ + auto res = std::make_unique( layer ); + res->setTextFormat( mTextFormat ); + res->setLabelIndexOnly( mLabelIndexOnly ); + res->setPriority( mPriority ); + res->setPlacementSettings( mPlacementSettings ); + res->setZIndex( mZIndex ); + res->setThinningSettings( mThinningSettings ); + if ( mNumericFormat ) + { + res->setNumericFormat( std::unique_ptr( mNumericFormat->clone() ) ); + } + + // Read contour parameters from the renderer so they stay in sync with the Symbology tab + if ( const QgsRasterContourRenderer *renderer = dynamic_cast( layer->renderer() ) ) + { + res->setInputBand( renderer->inputBand() ); + res->setContourInterval( renderer->contourInterval() ); + res->setContourIndexInterval( renderer->contourIndexInterval() ); + res->setDownscale( renderer->downscale() ); + } + + return res; +} + +QDomElement QgsRasterLayerContourLabeling::save( QDomDocument &doc, const QgsReadWriteContext &context ) const +{ + QDomElement elem = doc.createElement( u"labeling"_s ); + elem.setAttribute( u"type"_s, u"contour"_s ); + elem.setAttribute( u"labelIndexOnly"_s, mLabelIndexOnly ? 1 : 0 ); + elem.setAttribute( u"priority"_s, mPriority ); + elem.setAttribute( u"zIndex"_s, mZIndex ); + + { + QDomElement textFormatElem = doc.createElement( u"textFormat"_s ); + textFormatElem.appendChild( mTextFormat.writeXml( doc, context ) ); + elem.appendChild( textFormatElem ); + } + + { + QDomElement numericFormatElem = doc.createElement( u"numericFormat"_s ); + mNumericFormat->writeXml( numericFormatElem, doc, context ); + elem.appendChild( numericFormatElem ); + } + + { + QDomElement placementElem = doc.createElement( u"placement"_s ); + placementElem.setAttribute( u"overlapHandling"_s, qgsEnumValueToKey( mPlacementSettings.overlapHandling() ) ); + elem.appendChild( placementElem ); + } + + { + QDomElement thinningElem = doc.createElement( u"thinning"_s ); + thinningElem.setAttribute( u"maxNumLabels"_s, mThinningSettings.maximumNumberLabels() ); + thinningElem.setAttribute( u"limitNumLabels"_s, mThinningSettings.limitNumberOfLabelsEnabled() ); + elem.appendChild( thinningElem ); + } + + { + QDomElement renderingElem = doc.createElement( u"rendering"_s ); + renderingElem.setAttribute( u"scaleVisibility"_s, mScaleVisibility ); + // element names flipped vs member — matches vector labeling convention + renderingElem.setAttribute( u"scaleMin"_s, mMaximumScale ); + renderingElem.setAttribute( u"scaleMax"_s, mMinimumScale ); + elem.appendChild( renderingElem ); + } + + return elem; +} + +bool QgsRasterLayerContourLabeling::accept( QgsStyleEntityVisitorInterface *visitor ) const +{ + QgsStyleTextFormatEntity entity( mTextFormat ); + if ( !visitor->visit( &entity ) ) + return false; + + return true; +} + +bool QgsRasterLayerContourLabeling::requiresAdvancedEffects() const +{ + return mTextFormat.containsAdvancedEffects(); +} + +bool QgsRasterLayerContourLabeling::hasNonDefaultCompositionMode() const +{ + return mTextFormat.hasNonDefaultCompositionMode(); +} + +void QgsRasterLayerContourLabeling::multiplyOpacity( double opacityFactor ) +{ + mTextFormat.multiplyOpacity( opacityFactor ); +} + +bool QgsRasterLayerContourLabeling::isInScaleRange( double scale ) const +{ + return !mScaleVisibility + || ( ( mMinimumScale == 0 || !QgsScaleUtils::lessThanMaximumScale( scale, mMinimumScale ) ) + && ( mMaximumScale == 0 || !QgsScaleUtils::equalToOrGreaterThanMinimumScale( scale, mMaximumScale ) ) ); +} + +QgsRasterLayerContourLabeling *QgsRasterLayerContourLabeling::create( const QDomElement &element, const QgsReadWriteContext &context ) +{ + auto res = std::make_unique(); + res->setLabelIndexOnly( element.attribute( u"labelIndexOnly"_s, u"0"_s ).toInt() ); + res->setPriority( element.attribute( u"priority"_s, u"0.5"_s ).toDouble() ); + res->setZIndex( element.attribute( u"zIndex"_s, u"0"_s ).toDouble() ); + + const QDomElement textFormatElem = element.firstChildElement( u"textFormat"_s ); + if ( !textFormatElem.isNull() ) + { + const QDomNodeList textFormatNodeList = textFormatElem.elementsByTagName( u"text-style"_s ); + const QDomElement textFormatElem = textFormatNodeList.at( 0 ).toElement(); + QgsTextFormat format; + format.readXml( textFormatElem, context ); + res->setTextFormat( format ); + } + + const QDomNodeList numericFormatNodeList = element.elementsByTagName( u"numericFormat"_s ); + if ( !numericFormatNodeList.isEmpty() ) + { + const QDomElement numericFormatElem = numericFormatNodeList.at( 0 ).toElement(); + res->mNumericFormat.reset( QgsApplication::numericFormatRegistry()->createFromXml( numericFormatElem, context ) ); + } + + QDomElement placementElem = element.firstChildElement( u"placement"_s ); + res->mPlacementSettings.setOverlapHandling( qgsEnumKeyToValue( placementElem.attribute( u"overlapHandling"_s ), Qgis::LabelOverlapHandling::PreventOverlap ) ); + + QDomElement thinningElem = element.firstChildElement( u"thinning"_s ); + res->mThinningSettings.setMaximumNumberLabels( thinningElem.attribute( u"maxNumLabels"_s, u"2000"_s ).toInt() ); + res->mThinningSettings.setLimitNumberLabelsEnabled( thinningElem.attribute( u"limitNumLabels"_s, u"1"_s ).toInt() ); + + QDomElement renderingElem = element.firstChildElement( u"rendering"_s ); + res->mMaximumScale = renderingElem.attribute( u"scaleMin"_s, u"0"_s ).toDouble(); + res->mMinimumScale = renderingElem.attribute( u"scaleMax"_s, u"0"_s ).toDouble(); + res->mScaleVisibility = renderingElem.attribute( u"scaleVisibility"_s ).toInt(); + + return res.release(); +} + +QgsTextFormat QgsRasterLayerContourLabeling::textFormat() const +{ + return mTextFormat; +} + +void QgsRasterLayerContourLabeling::setTextFormat( const QgsTextFormat &format ) +{ + mTextFormat = format; +} + +const QgsNumericFormat *QgsRasterLayerContourLabeling::numericFormat() const +{ + return mNumericFormat.get(); +} + +void QgsRasterLayerContourLabeling::setNumericFormat( QgsNumericFormat *format ) +{ + if ( format != mNumericFormat.get() ) + mNumericFormat.reset( format ); +} + +double QgsRasterLayerContourLabeling::zIndex() const +{ + return mZIndex; +} + +void QgsRasterLayerContourLabeling::setZIndex( double index ) +{ + mZIndex = index; +} + +double QgsRasterLayerContourLabeling::maximumScale() const +{ + return mMaximumScale; +} + +void QgsRasterLayerContourLabeling::setMaximumScale( double scale ) +{ + mMaximumScale = scale; +} + +double QgsRasterLayerContourLabeling::minimumScale() const +{ + return mMinimumScale; +} + +void QgsRasterLayerContourLabeling::setMinimumScale( double scale ) +{ + mMinimumScale = scale; +} + +bool QgsRasterLayerContourLabeling::hasScaleBasedVisibility() const +{ + return mScaleVisibility; +} + +void QgsRasterLayerContourLabeling::setScaleBasedVisibility( bool enabled ) +{ + mScaleVisibility = enabled; +} diff --git a/src/core/raster/qgsrastercontourlabeling.h b/src/core/raster/qgsrastercontourlabeling.h new file mode 100644 index 000000000000..8c1729b01651 --- /dev/null +++ b/src/core/raster/qgsrastercontourlabeling.h @@ -0,0 +1,142 @@ +/*************************************************************************** + qgsrastercontourlabeling.h + --------------- + begin : February 2026 + copyright : (C) 2026 by the QGIS project + email : info at qgis dot org + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSRASTERCONTOURLABELING_H +#define QGSRASTERCONTOURLABELING_H + +#include "qgis_core.h" +#include "qgis_sip.h" +#include "qgsrasterlabeling.h" + +class QgsLineString; +class QgsNumericFormat; + +#ifndef SIP_RUN + +/** + * \ingroup core + * \brief Implements labeling for raster contour lines. + * + * Generates contour line geometries from a raster band using GDAL and registers + * them with the labeling engine for placement along the contour lines. + * + * \note Not available in Python bindings + * + * \since QGIS 3.44 + */ +class CORE_EXPORT QgsRasterContourLabelProvider : public QgsRasterLayerLabelProvider +{ + public: + explicit QgsRasterContourLabelProvider( QgsRasterLayer *layer ); + ~QgsRasterContourLabelProvider() override; + + void generateLabels( QgsRenderContext &context, QgsRasterPipe *pipe, QgsRasterViewPort *rasterViewPort, QgsRasterLayerRendererFeedback *feedback ) override; + + void setContourInterval( double interval ) { mContourInterval = interval; } + void setContourIndexInterval( double interval ) { mContourIndexInterval = interval; } + void setInputBand( int band ) { mInputBand = band; } + void setDownscale( double downscale ) { mDownscale = downscale; } + void setLabelIndexOnly( bool indexOnly ) { mLabelIndexOnly = indexOnly; } + + void addContourLabel( const QgsLineString &line, const QString &text, QgsRenderContext &context ); + + private: + double mContourInterval = 100.0; + double mContourIndexInterval = 0.0; + int mInputBand = 1; + double mDownscale = 4.0; + bool mLabelIndexOnly = false; +}; + +#endif + +/** + * \ingroup core + * \brief Labeling configuration for raster contour lines. + * + * Produces labels placed along contour lines generated on-the-fly from raster data. + * + * \since QGIS 3.44 + */ +class CORE_EXPORT QgsRasterLayerContourLabeling : public QgsAbstractRasterLayerLabeling +{ + public: + + QgsRasterLayerContourLabeling(); + ~QgsRasterLayerContourLabeling() override; + + QString type() const override; + QgsRasterLayerContourLabeling *clone() const override SIP_FACTORY; + std::unique_ptr< QgsRasterLayerLabelProvider > provider( QgsRasterLayer *layer ) const override SIP_SKIP; + QDomElement save( QDomDocument &doc, const QgsReadWriteContext &context ) const override; + bool accept( QgsStyleEntityVisitorInterface *visitor ) const override; + bool requiresAdvancedEffects() const override; + bool hasNonDefaultCompositionMode() const override; + void multiplyOpacity( double opacityFactor ) override; + bool isInScaleRange( double scale ) const override; + + //! Creates a QgsRasterLayerContourLabeling from a DOM element with saved configuration + static QgsRasterLayerContourLabeling *create( const QDomElement &element, const QgsReadWriteContext &context ) SIP_FACTORY; + + QgsTextFormat textFormat() const; + void setTextFormat( const QgsTextFormat &format ); + + const QgsNumericFormat *numericFormat() const; + void setNumericFormat( QgsNumericFormat *format SIP_TRANSFER ); + + bool labelIndexOnly() const { return mLabelIndexOnly; } + void setLabelIndexOnly( bool indexOnly ) { mLabelIndexOnly = indexOnly; } + + double priority() const { return mPriority; } + void setPriority( double priority ) { mPriority = priority; } + + const QgsLabelPlacementSettings &placementSettings() const { return mPlacementSettings; } SIP_SKIP + QgsLabelPlacementSettings &placementSettings() { return mPlacementSettings; } + void setPlacementSettings( const QgsLabelPlacementSettings &settings ) { mPlacementSettings = settings; } + + const QgsLabelThinningSettings &thinningSettings() const { return mThinningSettings; } SIP_SKIP + QgsLabelThinningSettings &thinningSettings() { return mThinningSettings; } + void setThinningSettings( const QgsLabelThinningSettings &settings ) { mThinningSettings = settings; } + + double zIndex() const; + void setZIndex( double index ); + + double maximumScale() const; + void setMaximumScale( double scale ); + + double minimumScale() const; + void setMinimumScale( double scale ); + + void setScaleBasedVisibility( bool enabled ); + bool hasScaleBasedVisibility() const; + + private: + bool mLabelIndexOnly = false; + + QgsTextFormat mTextFormat; + std::unique_ptr< QgsNumericFormat > mNumericFormat; + + double mPriority = 0.5; + QgsLabelPlacementSettings mPlacementSettings; + QgsLabelThinningSettings mThinningSettings; + double mZIndex = 0; + + bool mScaleVisibility = false; + double mMaximumScale = 0; + double mMinimumScale = 0; +}; + +#endif // QGSRASTERCONTOURLABELING_H diff --git a/src/core/raster/qgsrasterlabeling.cpp b/src/core/raster/qgsrasterlabeling.cpp index 41243919d5b4..4d22529b7fe6 100644 --- a/src/core/raster/qgsrasterlabeling.cpp +++ b/src/core/raster/qgsrasterlabeling.cpp @@ -15,6 +15,7 @@ ***************************************************************************/ #include "qgsrasterlabeling.h" +#include "qgsrastercontourlabeling.h" #include "feature.h" #include "labelposition.h" @@ -315,6 +316,10 @@ QgsAbstractRasterLayerLabeling *QgsAbstractRasterLayerLabeling::createFromElemen { return QgsRasterLayerSimpleLabeling::create( element, context ); } + else if ( type == "contour"_L1 ) + { + return QgsRasterLayerContourLabeling::create( element, context ); + } else { return nullptr; diff --git a/src/core/raster/qgsrasterlabeling.h b/src/core/raster/qgsrasterlabeling.h index 48cb98206f4f..83dacbaeef87 100644 --- a/src/core/raster/qgsrasterlabeling.h +++ b/src/core/raster/qgsrasterlabeling.h @@ -42,7 +42,7 @@ class QgsRasterLayerRendererFeedback; * * \since QGIS 3.42 */ -class CORE_EXPORT QgsRasterLayerLabelProvider final : public QgsAbstractLabelProvider +class CORE_EXPORT QgsRasterLayerLabelProvider : public QgsAbstractLabelProvider { public: /** @@ -50,15 +50,15 @@ class CORE_EXPORT QgsRasterLayerLabelProvider final : public QgsAbstractLabelPro */ explicit QgsRasterLayerLabelProvider( QgsRasterLayer *layer ); - ~QgsRasterLayerLabelProvider() final; - QList labelFeatures( QgsRenderContext & ) final; - void drawLabel( QgsRenderContext &context, pal::LabelPosition *label ) const final; - void startRender( QgsRenderContext &context ) final; + ~QgsRasterLayerLabelProvider() override; + QList labelFeatures( QgsRenderContext & ) override; + void drawLabel( QgsRenderContext &context, pal::LabelPosition *label ) const override; + void startRender( QgsRenderContext &context ) override; /** * Generates the labels, given a render context and input pipe. */ - void generateLabels( QgsRenderContext &context, QgsRasterPipe *pipe, QgsRasterViewPort *rasterViewPort, QgsRasterLayerRendererFeedback *feedback ); + virtual void generateLabels( QgsRenderContext &context, QgsRasterPipe *pipe, QgsRasterViewPort *rasterViewPort, QgsRasterLayerRendererFeedback *feedback ); /** * Adds a label at the specified point in map coordinates. @@ -138,7 +138,7 @@ class CORE_EXPORT QgsRasterLayerLabelProvider final : public QgsAbstractLabelPro */ void setResampleOver( int pixels ); - private: + protected: QgsTextFormat mFormat; int mBandNumber = 1; std::unique_ptr< QgsNumericFormat > mNumericFormat; @@ -169,6 +169,8 @@ class CORE_EXPORT QgsAbstractRasterLayerLabeling SIP_ABSTRACT SIP_CONVERT_TO_SUBCLASS_CODE if ( sipCpp->type() == "simple" ) sipType = sipType_QgsRasterLayerSimpleLabeling; + else if ( sipCpp->type() == "contour" ) + sipType = sipType_QgsRasterLayerContourLabeling; else sipType = 0; SIP_END diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index b2631681c03e..7c6e0932f3e9 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -19,6 +19,7 @@ set(QGIS_GUI_SRCS raster/qgsrastercontourrendererwidget.cpp raster/qgsrasterhistogramwidget.cpp raster/qgsrasterlabelingwidget.cpp + raster/qgsrastercontourlabelsettingswidget.cpp raster/qgsrasterlabelsettingswidget.cpp raster/qgsrasterlayerproperties.cpp raster/qgsrasterlayertemporalpropertieswidget.cpp @@ -1564,6 +1565,7 @@ set(QGIS_GUI_HDRS raster/qgsrastercontourrendererwidget.h raster/qgsrasterhistogramwidget.h raster/qgsrasterlabelingwidget.h + raster/qgsrastercontourlabelsettingswidget.h raster/qgsrasterlabelsettingswidget.h raster/qgsrasterminmaxwidget.h raster/qgsrasterrendererwidget.h diff --git a/src/gui/raster/qgsrastercontourlabelsettingswidget.cpp b/src/gui/raster/qgsrastercontourlabelsettingswidget.cpp new file mode 100644 index 000000000000..23ad3466439f --- /dev/null +++ b/src/gui/raster/qgsrastercontourlabelsettingswidget.cpp @@ -0,0 +1,230 @@ +/*************************************************************************** + qgsrastercontourlabelsettingswidget.cpp + --------------------------- + begin : February 2026 + copyright : (C) 2026 by the QGIS project + email : info at qgis dot org + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsrastercontourlabelsettingswidget.h" + +#include "qgsbasicnumericformat.h" +#include "qgsnumericformatselectorwidget.h" +#include "qgsrastercontourlabeling.h" +#include "qgsrasterlayer.h" + +#include +#include +#include + +#include "moc_qgsrastercontourlabelsettingswidget.cpp" + +QgsRasterContourLabelSettingsWidget::QgsRasterContourLabelSettingsWidget( QgsRasterLayer *layer, QgsMapCanvas *mapCanvas, QWidget *parent ) + : QgsLabelingGui( mapCanvas, parent, layer ) + , mNumberFormat( std::make_unique() ) +{ + mGeomType = Qgis::GeometryType::Line; + mMode = Labels; + + init(); + + QWidget *labelWithWidget = new QWidget(); + QGridLayout *gLayout = new QGridLayout(); + gLayout->setContentsMargins( 0, 0, 0, 0 ); + + // Row 0: Number format + gLayout->addWidget( new QLabel( tr( "Number format" ) ), 0, 0 ); + QPushButton *numberFormatButton = new QPushButton( tr( "Customize" ) ); + connect( numberFormatButton, &QPushButton::clicked, this, &QgsRasterContourLabelSettingsWidget::changeNumberFormat ); + gLayout->addWidget( numberFormatButton, 0, 1 ); + + // Row 1: Label index contours only + mLabelIndexOnlyCheck = new QCheckBox( tr( "Label index contours only" ) ); + connect( mLabelIndexOnlyCheck, &QCheckBox::toggled, this, &QgsRasterContourLabelSettingsWidget::widgetChanged ); + gLayout->addWidget( mLabelIndexOnlyCheck, 1, 0, 1, 2 ); + + gLayout->setColumnStretch( 0, 1 ); + gLayout->setColumnStretch( 1, 2 ); + + labelWithWidget->setLayout( gLayout ); + + mStackedWidgetLabelWith->addWidget( labelWithWidget ); + mStackedWidgetLabelWith->setCurrentWidget( labelWithWidget ); + + setLayer( layer ); + + const int prevIndex = mOptionsTab->currentIndex(); + + setPropertyOverrideButtonsVisible( true ); + mTextFormatsListWidget->setEntityTypes( QList() << QgsStyle::TextFormatEntity ); + + delete mCalloutItem; + mCalloutItem = nullptr; + delete mMaskItem; + mMaskItem = nullptr; + + mOptionsTab->removeTab( mOptionsTab->indexOf( calloutsTab ) ); + mOptionsTab->removeTab( mOptionsTab->indexOf( maskTab ) ); + + mLabelStackedWidget->removeWidget( mLabelPage_Callouts ); + mLabelStackedWidget->removeWidget( mLabelPage_Mask ); + + switch ( prevIndex ) + { + case 0: + case 1: + case 2: + break; + + case 4: // background - account for removed mask tab + case 5: // shadow + mLabelStackedWidget->setCurrentIndex( prevIndex - 1 ); + mOptionsTab->setCurrentIndex( prevIndex - 1 ); + break; + + case 7: // background - account for removed mask & callouts tab + case 8: // shadow- account for removed mask & callouts tab + mLabelStackedWidget->setCurrentIndex( prevIndex - 2 ); + mOptionsTab->setCurrentIndex( prevIndex - 2 ); + break; + + case 3: // mask + case 6: // callouts + mLabelStackedWidget->setCurrentIndex( 0 ); + mOptionsTab->setCurrentIndex( 0 ); + break; + + default: + break; + } + + // hide settings which have no relevance to raster contour labeling + mDirectSymbolsFrame->hide(); + mFormatNumFrame->hide(); + mFormatNumChkBx->hide(); + mFormatNumDDBtn->hide(); + mCheckBoxSubstituteText->hide(); + mToolButtonConfigureSubstitutes->hide(); + mLabelWrapOnCharacter->hide(); + wrapCharacterEdit->hide(); + mWrapCharDDBtn->hide(); + mLabelWrapLinesTo->hide(); + mAutoWrapLengthSpinBox->hide(); + mAutoWrapLengthDDBtn->hide(); + mAutoWrapTypeComboBox->hide(); + mFontMultiLineLabel->hide(); + mFontMultiLineAlignComboBox->hide(); + mFontMultiLineAlignDDBtn->hide(); + mGeometryGeneratorGroupBox->hide(); + mObstaclesGroupBox->hide(); + mPlacementDDGroupBox->hide(); + mPlacementGroupBox->hide(); + mInferiorPlacementWidget->hide(); + mLabelRenderingDDFrame->hide(); + mUpsidedownFrame->hide(); + mMultipartBehaviorWidget->hide(); + mFramePixelSizeVisibility->hide(); + line->hide(); + + mMinSizeFrame->show(); + mMinSizeLabel->setText( tr( "Suppress labeling of contours shorter than" ) ); + + mLimitLabelChkBox->setText( tr( "Limit number of contours to be labeled to" ) ); + + // fix precision for priority slider + mPrioritySlider->setRange( 0, 100 ); + mPrioritySlider->setTickInterval( 10 ); + +} + +QgsRasterContourLabelSettingsWidget::~QgsRasterContourLabelSettingsWidget() = default; + +void QgsRasterContourLabelSettingsWidget::setLabeling( QgsAbstractRasterLayerLabeling *labeling ) +{ + if ( QgsRasterLayerContourLabeling *contourLabeling = dynamic_cast( labeling ) ) + { + setFormat( contourLabeling->textFormat() ); + mLabelIndexOnlyCheck->setChecked( contourLabeling->labelIndexOnly() ); + mPrioritySlider->setValue( static_cast( 100 - contourLabeling->priority() * 100 ) ); + + mComboOverlapHandling->setCurrentIndex( mComboOverlapHandling->findData( static_cast( contourLabeling->placementSettings().overlapHandling() ) ) ); + mZIndexSpinBox->setValue( contourLabeling->zIndex() ); + if ( const QgsNumericFormat *format = contourLabeling->numericFormat() ) + mNumberFormat.reset( format->clone() ); + + mLimitLabelChkBox->setChecked( contourLabeling->thinningSettings().limitNumberOfLabelsEnabled() ); + mLimitLabelSpinBox->setValue( contourLabeling->thinningSettings().maximumNumberLabels() ); + mMinSizeSpinBox->setValue( contourLabeling->thinningSettings().minimumFeatureSize() ); + + mScaleBasedVisibilityChkBx->setChecked( contourLabeling->hasScaleBasedVisibility() ); + mMinScaleWidget->setScale( contourLabeling->minimumScale() ); + mMaxScaleWidget->setScale( contourLabeling->maximumScale() ); + + updateUi(); + } +} + +void QgsRasterContourLabelSettingsWidget::updateLabeling( QgsAbstractRasterLayerLabeling *labeling ) +{ + if ( QgsRasterLayerContourLabeling *contourLabeling = dynamic_cast( labeling ) ) + { + contourLabeling->setTextFormat( format() ); + contourLabeling->setLabelIndexOnly( mLabelIndexOnlyCheck->isChecked() ); + contourLabeling->setPriority( 1.0 - mPrioritySlider->value() / 100.0 ); + contourLabeling->placementSettings().setOverlapHandling( static_cast( mComboOverlapHandling->currentData().toInt() ) ); + contourLabeling->setZIndex( mZIndexSpinBox->value() ); + contourLabeling->setNumericFormat( mNumberFormat->clone() ); + + contourLabeling->thinningSettings().setLimitNumberLabelsEnabled( mLimitLabelChkBox->isChecked() ); + contourLabeling->thinningSettings().setMaximumNumberLabels( mLimitLabelSpinBox->value() ); + contourLabeling->thinningSettings().setMinimumFeatureSize( mMinSizeSpinBox->value() ); + + contourLabeling->setScaleBasedVisibility( mScaleBasedVisibilityChkBx->isChecked() ); + contourLabeling->setMinimumScale( mMinScaleWidget->scale() ); + contourLabeling->setMaximumScale( mMaxScaleWidget->scale() ); + } +} + +void QgsRasterContourLabelSettingsWidget::setLayer( QgsMapLayer *layer ) +{ + QgsLabelingGui::setLayer( layer ); + mMinSizeFrame->show(); +} + +void QgsRasterContourLabelSettingsWidget::changeNumberFormat() +{ + QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( this ); + if ( panel && panel->dockMode() ) + { + QgsNumericFormatSelectorWidget *widget = new QgsNumericFormatSelectorWidget( this ); + widget->setPanelTitle( tr( "Number Format" ) ); + widget->setFormat( mNumberFormat.get() ); + widget->registerExpressionContextGenerator( this ); + connect( widget, &QgsNumericFormatSelectorWidget::changed, this, [this, widget] { + if ( !mBlockChangesSignal ) + { + mNumberFormat.reset( widget->format() ); + emit widgetChanged(); + } + } ); + panel->openPanel( widget ); + } + else + { + QgsNumericFormatSelectorDialog dialog( this ); + dialog.setFormat( mNumberFormat.get() ); + dialog.registerExpressionContextGenerator( this ); + if ( dialog.exec() ) + { + mNumberFormat.reset( dialog.format() ); + emit widgetChanged(); + } + } +} diff --git a/src/gui/raster/qgsrastercontourlabelsettingswidget.h b/src/gui/raster/qgsrastercontourlabelsettingswidget.h new file mode 100644 index 000000000000..1b390383f565 --- /dev/null +++ b/src/gui/raster/qgsrastercontourlabelsettingswidget.h @@ -0,0 +1,68 @@ +/*************************************************************************** + qgsrastercontourlabelsettingswidget.h + ------------------------- + begin : February 2026 + copyright : (C) 2026 by the QGIS project + email : info at qgis dot org + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QGSRASTERCONTOURLABELSETTINGSWIDGET_H +#define QGSRASTERCONTOURLABELSETTINGSWIDGET_H + +#include "qgis_gui.h" +#include "qgslabelinggui.h" + +#define SIP_NO_FILE + +class QgsRasterLayer; +class QgsAbstractRasterLayerLabeling; +class QgsNumericFormat; +class QCheckBox; + +// We don't want to expose this in the public API + +/** + * \class QgsRasterContourLabelSettingsWidget + * \ingroup gui + * \brief A widget for customizing settings for raster contour line labeling. + * + * \since QGIS 3.44 + */ +class GUI_EXPORT QgsRasterContourLabelSettingsWidget : public QgsLabelingGui +{ + Q_OBJECT + public: + /** + * Constructor for QgsRasterContourLabelSettingsWidget, for configuring a raster \a layer contour labeling. + */ + QgsRasterContourLabelSettingsWidget( QgsRasterLayer *layer, QgsMapCanvas *mapCanvas, QWidget *parent = nullptr ); + ~QgsRasterContourLabelSettingsWidget() override; + + /** + * Sets the \a labeling settings to show in the widget. + */ + void setLabeling( QgsAbstractRasterLayerLabeling *labeling ); + + /** + * Updates \a labeling by setting properties to match the current state of the widget. + */ + void updateLabeling( QgsAbstractRasterLayerLabeling *labeling ); + + void setLayer( QgsMapLayer *layer ) final; + + private slots: + void changeNumberFormat(); + + private: + QCheckBox *mLabelIndexOnlyCheck = nullptr; + std::unique_ptr mNumberFormat; + int mBlockChangesSignal = 0; +}; + +#endif // QGSRASTERCONTOURLABELSETTINGSWIDGET_H diff --git a/src/gui/raster/qgsrasterlabelingwidget.cpp b/src/gui/raster/qgsrasterlabelingwidget.cpp index f3276b14acb6..ab3f2156afd0 100644 --- a/src/gui/raster/qgsrasterlabelingwidget.cpp +++ b/src/gui/raster/qgsrasterlabelingwidget.cpp @@ -18,6 +18,8 @@ #include "qgsapplication.h" #include "qgslabelingwidget.h" #include "qgsproject.h" +#include "qgsrastercontourlabeling.h" +#include "qgsrastercontourlabelsettingswidget.h" #include "qgsrasterlabelsettingswidget.h" #include "qgsrasterlayer.h" @@ -37,6 +39,7 @@ QgsRasterLabelingWidget::QgsRasterLabelingWidget( QgsRasterLayer *layer, QgsMapC mLabelModeComboBox->addItem( QgsApplication::getThemeIcon( u"labelingNone.svg"_s ), tr( "No Labels" ), u"none"_s ); mLabelModeComboBox->addItem( QgsApplication::getThemeIcon( u"labelingSingle.svg"_s ), tr( "Label with Pixel Values" ), u"simple"_s ); + mLabelModeComboBox->addItem( QgsApplication::getThemeIcon( u"labelingSingle.svg"_s ), tr( "Label with Contour Lines" ), u"contour"_s ); connect( mLabelModeComboBox, qOverload( &QComboBox::currentIndexChanged ), this, &QgsRasterLabelingWidget::labelModeChanged ); setLayer( layer ); @@ -57,6 +60,10 @@ void QgsRasterLabelingWidget::setDockMode( bool dockMode ) { l->setDockMode( dockMode ); } + else if ( QgsRasterContourLabelSettingsWidget *l = qobject_cast( mWidget ) ) + { + l->setDockMode( dockMode ); + } } void QgsRasterLabelingWidget::setLayer( QgsMapLayer *mapLayer ) @@ -92,6 +99,11 @@ void QgsRasterLabelingWidget::adaptToLayer() settingsWidget->setLayer( mLayer ); settingsWidget->setLabeling( labeling ); } + else if ( QgsRasterContourLabelSettingsWidget *settingsWidget = qobject_cast( mWidget ) ) + { + settingsWidget->setLayer( mLayer ); + settingsWidget->setLabeling( labeling ); + } } else { @@ -117,6 +129,16 @@ void QgsRasterLabelingWidget::writeSettingsToLayer() mLayer->setLabeling( labeling.release() ); mLayer->setLabelsEnabled( true ); } + else if ( mode == "contour"_L1 ) + { + auto labeling = std::make_unique(); + if ( QgsRasterContourLabelSettingsWidget *settingsWidget = qobject_cast( mWidget ) ) + { + settingsWidget->updateLabeling( labeling.get() ); + } + mLayer->setLabeling( labeling.release() ); + mLayer->setLabelsEnabled( true ); + } else { mLayer->setLabelsEnabled( false ); @@ -171,6 +193,34 @@ void QgsRasterLabelingWidget::labelModeChanged( int index ) mStackedWidget->addWidget( mWidget ); mStackedWidget->setCurrentWidget( mWidget ); } + else if ( mode == "contour"_L1 ) + { + QgsSymbolWidgetContext context; + context.setMapCanvas( mMapCanvas ); + context.setMessageBar( mMessageBar ); + + QgsRasterContourLabelSettingsWidget *settingsWidget = new QgsRasterContourLabelSettingsWidget( mLayer, mCanvas, this ); + settingsWidget->layout()->setContentsMargins( 0, 0, 0, 0 ); + settingsWidget->setContext( context ); + + settingsWidget->setDockMode( dockMode() ); + connect( settingsWidget, &QgsLabelingGui::widgetChanged, this, &QgsRasterLabelingWidget::widgetChanged ); + + mWidget = settingsWidget; + if ( !dynamic_cast( mLayer->labeling() ) ) + { + auto labeling = std::make_unique(); + settingsWidget->setLabeling( labeling.get() ); + mLayer->setLabeling( labeling.release() ); + } + else + { + settingsWidget->setLabeling( mLayer->labeling() ); + } + + mStackedWidget->addWidget( mWidget ); + mStackedWidget->setCurrentWidget( mWidget ); + } emit widgetChanged(); } diff --git a/tests/src/python/test_qgsrasterlabeling.py b/tests/src/python/test_qgsrasterlabeling.py index ca3af06fba2a..53404268bdc8 100644 --- a/tests/src/python/test_qgsrasterlabeling.py +++ b/tests/src/python/test_qgsrasterlabeling.py @@ -20,6 +20,7 @@ QgsPercentageNumericFormat, QgsProperty, QgsRasterLayer, + QgsRasterLayerContourLabeling, QgsRasterLayerSimpleLabeling, QgsReadWriteContext, QgsRectangle, @@ -418,5 +419,165 @@ def test_render_resampled(self): ) + def test_contour_labeling(self): + labeling = QgsRasterLayerContourLabeling() + self.assertEqual(labeling.type(), "contour") + + labeling.setBand(2) + text_format = QgsTextFormat() + text_format.setSize(14) + labeling.setTextFormat(text_format) + labeling.setNumericFormat(QgsCurrencyNumericFormat()) + labeling.setContourInterval(25) + labeling.setContourIndexInterval(100) + labeling.setDownscale(8) + labeling.setLabelIndexOnly(True) + labeling.setPriority(0.3) + labeling.placementSettings().setOverlapHandling( + Qgis.LabelOverlapHandling.AllowOverlapIfRequired + ) + labeling.thinningSettings().setLimitNumberLabelsEnabled(True) + labeling.thinningSettings().setMaximumNumberLabels(500) + labeling.setZIndex(5) + labeling.setMaximumScale(5000) + labeling.setMinimumScale(50000) + labeling.setScaleBasedVisibility(True) + + self.assertEqual(labeling.band(), 2) + self.assertEqual(labeling.textFormat().size(), 14) + self.assertIsInstance(labeling.numericFormat(), QgsCurrencyNumericFormat) + self.assertEqual(labeling.contourInterval(), 25) + self.assertEqual(labeling.contourIndexInterval(), 100) + self.assertEqual(labeling.downscale(), 8) + self.assertTrue(labeling.labelIndexOnly()) + self.assertEqual(labeling.priority(), 0.3) + self.assertEqual( + labeling.placementSettings().overlapHandling(), + Qgis.LabelOverlapHandling.AllowOverlapIfRequired, + ) + self.assertTrue(labeling.thinningSettings().limitNumberOfLabelsEnabled()) + self.assertEqual(labeling.thinningSettings().maximumNumberLabels(), 500) + self.assertEqual(labeling.zIndex(), 5) + self.assertEqual(labeling.maximumScale(), 5000) + self.assertEqual(labeling.minimumScale(), 50000) + self.assertTrue(labeling.hasScaleBasedVisibility()) + + labeling_clone = labeling.clone() + self.assertIsInstance(labeling_clone, QgsRasterLayerContourLabeling) + self.assertEqual(labeling_clone.band(), 2) + self.assertEqual(labeling_clone.textFormat().size(), 14) + self.assertIsInstance(labeling_clone.numericFormat(), QgsCurrencyNumericFormat) + self.assertEqual(labeling_clone.contourInterval(), 25) + self.assertEqual(labeling_clone.contourIndexInterval(), 100) + self.assertEqual(labeling_clone.downscale(), 8) + self.assertTrue(labeling_clone.labelIndexOnly()) + self.assertEqual(labeling_clone.priority(), 0.3) + self.assertEqual( + labeling_clone.placementSettings().overlapHandling(), + Qgis.LabelOverlapHandling.AllowOverlapIfRequired, + ) + self.assertTrue(labeling_clone.thinningSettings().limitNumberOfLabelsEnabled()) + self.assertEqual(labeling_clone.thinningSettings().maximumNumberLabels(), 500) + self.assertEqual(labeling_clone.zIndex(), 5) + self.assertEqual(labeling_clone.maximumScale(), 5000) + self.assertEqual(labeling_clone.minimumScale(), 50000) + self.assertTrue(labeling_clone.hasScaleBasedVisibility()) + + doc = QDomDocument() + context = QgsReadWriteContext() + element = labeling.save(doc, context) + + labeling_from_xml = QgsAbstractRasterLayerLabeling.createFromElement( + element, context + ) + self.assertIsInstance(labeling_from_xml, QgsRasterLayerContourLabeling) + self.assertEqual(labeling_from_xml.band(), 2) + self.assertEqual(labeling_from_xml.textFormat().size(), 14) + self.assertIsInstance( + labeling_from_xml.numericFormat(), QgsCurrencyNumericFormat + ) + self.assertEqual(labeling_from_xml.contourInterval(), 25) + self.assertEqual(labeling_from_xml.contourIndexInterval(), 100) + self.assertEqual(labeling_from_xml.downscale(), 8) + self.assertTrue(labeling_from_xml.labelIndexOnly()) + self.assertEqual(labeling_from_xml.priority(), 0.3) + self.assertEqual( + labeling_from_xml.placementSettings().overlapHandling(), + Qgis.LabelOverlapHandling.AllowOverlapIfRequired, + ) + self.assertTrue( + labeling_from_xml.thinningSettings().limitNumberOfLabelsEnabled() + ) + self.assertEqual( + labeling_from_xml.thinningSettings().maximumNumberLabels(), 500 + ) + self.assertEqual(labeling_from_xml.zIndex(), 5) + self.assertEqual(labeling_from_xml.maximumScale(), 5000) + self.assertEqual(labeling_from_xml.minimumScale(), 50000) + self.assertTrue(labeling_from_xml.hasScaleBasedVisibility()) + + def test_contour_labeling_defaults(self): + labeling = QgsRasterLayerContourLabeling() + self.assertEqual(labeling.band(), 1) + self.assertEqual(labeling.contourInterval(), 100) + self.assertEqual(labeling.contourIndexInterval(), 0) + self.assertEqual(labeling.downscale(), 4) + self.assertFalse(labeling.labelIndexOnly()) + self.assertEqual(labeling.priority(), 0.5) + self.assertEqual(labeling.zIndex(), 0) + self.assertFalse(labeling.hasScaleBasedVisibility()) + self.assertIsNotNone(labeling.numericFormat()) + self.assertIsInstance(labeling.numericFormat(), QgsBasicNumericFormat) + self.assertTrue(labeling.thinningSettings().limitNumberOfLabelsEnabled()) + self.assertEqual(labeling.thinningSettings().maximumNumberLabels(), 2000) + + def test_contour_labeling_scale_range(self): + labeling = QgsRasterLayerContourLabeling() + self.assertTrue(labeling.isInScaleRange(1000)) + self.assertTrue(labeling.isInScaleRange(100000)) + + labeling.setScaleBasedVisibility(True) + labeling.setMaximumScale(10000) + labeling.setMinimumScale(50000) + self.assertFalse(labeling.isInScaleRange(5000)) + self.assertTrue(labeling.isInScaleRange(25000)) + self.assertFalse(labeling.isInScaleRange(100000)) + + def test_contour_labeling_layer_roundtrip(self): + raster_layer = QgsRasterLayer( + self.get_test_data_path("raster/dem.tif").as_posix() + ) + self.assertTrue(raster_layer.isValid()) + + labeling = QgsRasterLayerContourLabeling() + labeling.setContourInterval(50) + labeling.setBand(1) + raster_layer.setLabeling(labeling) + raster_layer.setLabelsEnabled(True) + + doc = QDomDocument() + context = QgsReadWriteContext() + element = doc.createElement("maplayer") + raster_layer.writeLayerXml(element, doc, context) + + raster_layer2 = QgsRasterLayer( + self.get_test_data_path("raster/dem.tif").as_posix() + ) + raster_layer2.readLayerXml(element, context) + + self.assertIsInstance(raster_layer2.labeling(), QgsRasterLayerContourLabeling) + self.assertTrue(raster_layer2.labelsEnabled()) + self.assertEqual(raster_layer2.labeling().contourInterval(), 50) + + def testHasNonDefaultCompositionModeContour(self): + labeling = QgsRasterLayerContourLabeling() + self.assertFalse(labeling.hasNonDefaultCompositionMode()) + + t = QgsTextFormat() + t.setBlendMode(QPainter.CompositionMode.CompositionMode_DestinationAtop) + labeling.setTextFormat(t) + self.assertTrue(labeling.hasNonDefaultCompositionMode()) + + if __name__ == "__main__": unittest.main() From fa8fdabab47775012ccd2de1d612d9a771f47de1 Mon Sep 17 00:00:00 2001 From: James McGuiness <955899+Geojim@users.noreply.github.com> Date: Sun, 1 Mar 2026 07:54:50 +1000 Subject: [PATCH 2/3] Fix CI failures for contour labeling - Fix SIP include ordering (alphabetical) - Add Doxygen comments to all public methods - Rename _contourLabelWriter to avoid reserved identifier (clang-tidy) - Remove extra blank lines (pre-commit) - Update tests to match removed band/interval/downscale accessors Co-Authored-By: Claude Opus 4.6 --- .../raster/qgsrastercontourlabeling.sip.in | 79 ++++++++++++++++++- python/PyQt6/core/core_auto.sip | 2 +- src/core/raster/qgsrastercontourlabeling.cpp | 12 +-- src/core/raster/qgsrastercontourlabeling.h | 43 +++++++++- src/core/raster/qgsrasterlabeling.cpp | 2 +- .../qgsrastercontourlabelsettingswidget.cpp | 1 - tests/src/python/test_qgsrasterlabeling.py | 26 +----- 7 files changed, 131 insertions(+), 34 deletions(-) diff --git a/python/PyQt6/core/auto_generated/raster/qgsrastercontourlabeling.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrastercontourlabeling.sip.in index f78efacffe6d..3e80b297c78f 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrastercontourlabeling.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrastercontourlabeling.sip.in @@ -49,38 +49,115 @@ raster data. static QgsRasterLayerContourLabeling *create( const QDomElement &element, const QgsReadWriteContext &context ) /Factory/; %Docstring Creates a QgsRasterLayerContourLabeling from a DOM element with saved -configuration +configuration. %End QgsTextFormat textFormat() const; +%Docstring +Returns the text format used for rendering contour labels. +%End + void setTextFormat( const QgsTextFormat &format ); +%Docstring +Sets the text ``format`` used for rendering contour labels. +%End const QgsNumericFormat *numericFormat() const; +%Docstring +Returns the numeric format used for formatting contour elevation values. +%End + void setNumericFormat( QgsNumericFormat *format /Transfer/ ); +%Docstring +Sets the numeric ``format`` used for formatting contour elevation +values. Ownership is transferred. +%End bool labelIndexOnly() const; +%Docstring +Returns whether only index contours are labeled. +%End + void setLabelIndexOnly( bool indexOnly ); +%Docstring +Sets whether only index contours should be labeled. +%End double priority() const; +%Docstring +Returns the label priority, where 0 is lowest and 1 is highest. +%End + void setPriority( double priority ); +%Docstring +Sets the label ``priority``, where 0 is lowest and 1 is highest. +%End + QgsLabelPlacementSettings &placementSettings(); +%Docstring +Returns the label placement settings. +%End + void setPlacementSettings( const QgsLabelPlacementSettings &settings ); +%Docstring +Sets the label placement ``settings``. +%End + QgsLabelThinningSettings &thinningSettings(); +%Docstring +Returns the label thinning settings. +%End + void setThinningSettings( const QgsLabelThinningSettings &settings ); +%Docstring +Sets the label thinning ``settings``. +%End double zIndex() const; +%Docstring +Returns the z-index for label rendering order. +%End + void setZIndex( double index ); +%Docstring +Sets the z-``index`` for label rendering order. +%End double maximumScale() const; +%Docstring +Returns the maximum map scale (most zoomed in) at which labels are +visible. +%End + void setMaximumScale( double scale ); +%Docstring +Sets the maximum map ``scale`` (most zoomed in) at which labels are +visible. +%End double minimumScale() const; +%Docstring +Returns the minimum map scale (most zoomed out) at which labels are +visible. +%End + void setMinimumScale( double scale ); +%Docstring +Sets the minimum map ``scale`` (most zoomed out) at which labels are +visible. +%End void setScaleBasedVisibility( bool enabled ); +%Docstring +Sets whether scale-based visibility is ``enabled`` for labels. +%End + bool hasScaleBasedVisibility() const; +%Docstring +Returns whether scale-based visibility is enabled for labels. +%End }; diff --git a/python/PyQt6/core/core_auto.sip b/python/PyQt6/core/core_auto.sip index d0a56acba19c..ab90f7853346 100644 --- a/python/PyQt6/core/core_auto.sip +++ b/python/PyQt6/core/core_auto.sip @@ -669,6 +669,7 @@ %Include auto_generated/raster/qgsrasterbandstats.sip %Include auto_generated/raster/qgsrasterblock.sip %Include auto_generated/raster/qgsrasterchecker.sip +%Include auto_generated/raster/qgsrastercontourlabeling.sip %Include auto_generated/raster/qgsrastercontourrenderer.sip %Include auto_generated/raster/qgsrasterdataprovider.sip %Include auto_generated/raster/qgsrasterdataproviderelevationproperties.sip @@ -680,7 +681,6 @@ %Include auto_generated/raster/qgsrasteridentifyresult.sip %Include auto_generated/raster/qgsrasterinterface.sip %Include auto_generated/raster/qgsrasteriterator.sip -%Include auto_generated/raster/qgsrastercontourlabeling.sip %Include auto_generated/raster/qgsrasterlabeling.sip %Include auto_generated/raster/qgsrasterlayer.sip %Include auto_generated/raster/qgsrasterlayerelevationproperties.sip diff --git a/src/core/raster/qgsrastercontourlabeling.cpp b/src/core/raster/qgsrastercontourlabeling.cpp index de848c72bca9..a083354703bf 100644 --- a/src/core/raster/qgsrastercontourlabeling.cpp +++ b/src/core/raster/qgsrastercontourlabeling.cpp @@ -15,7 +15,6 @@ ***************************************************************************/ #include "qgsrastercontourlabeling.h" -#include "qgsrastercontourrenderer.h" #include @@ -27,6 +26,7 @@ #include "qgsmessagelog.h" #include "qgsnumericformat.h" #include "qgsnumericformatregistry.h" +#include "qgsrastercontourrenderer.h" #include "qgsrasterdataprovider.h" #include "qgsrasteriterator.h" #include "qgsrasterlayer.h" @@ -60,7 +60,7 @@ struct ContourLabelData bool labelIndexOnly; }; -static CPLErr _contourLabelWriter( double dfLevel, int nPoints, double *padfX, double *padfY, void *ptr ) +static CPLErr contourLabelWriter( double dfLevel, int nPoints, double *padfX, double *padfY, void *ptr ) { ContourLabelData *data = static_cast( ptr ); @@ -217,7 +217,7 @@ void QgsRasterContourLabelProvider::generateLabels( QgsRenderContext &context, Q GDALContourGeneratorH cg = GDAL_CG_Create( inputWidth, inputHeight, inputBlock->hasNoDataValue(), inputBlock->noDataValue(), mContourInterval, contourBase, - _contourLabelWriter, static_cast( &clData ) ); + contourLabelWriter, static_cast( &clData ) ); for ( int i = 0; i < inputHeight; ++i ) { @@ -366,9 +366,11 @@ void QgsRasterLayerContourLabeling::multiplyOpacity( double opacityFactor ) bool QgsRasterLayerContourLabeling::isInScaleRange( double scale ) const { + // mMaximumScale (most zoomed in) is exclusive ( < --> In range ) + // mMinimumScale (most zoomed out) is inclusive ( >= --> In range ) return !mScaleVisibility - || ( ( mMinimumScale == 0 || !QgsScaleUtils::lessThanMaximumScale( scale, mMinimumScale ) ) - && ( mMaximumScale == 0 || !QgsScaleUtils::equalToOrGreaterThanMinimumScale( scale, mMaximumScale ) ) ); + || ( ( mMinimumScale == 0 || !QgsScaleUtils::equalToOrGreaterThanMinimumScale( scale, mMinimumScale ) ) + && ( mMaximumScale == 0 || !QgsScaleUtils::lessThanMaximumScale( scale, mMaximumScale ) ) ); } QgsRasterLayerContourLabeling *QgsRasterLayerContourLabeling::create( const QDomElement &element, const QgsReadWriteContext &context ) diff --git a/src/core/raster/qgsrastercontourlabeling.h b/src/core/raster/qgsrastercontourlabeling.h index 8c1729b01651..72513158437f 100644 --- a/src/core/raster/qgsrastercontourlabeling.h +++ b/src/core/raster/qgsrastercontourlabeling.h @@ -40,17 +40,24 @@ class QgsNumericFormat; class CORE_EXPORT QgsRasterContourLabelProvider : public QgsRasterLayerLabelProvider { public: + //! Constructs a contour label provider for the given raster  layer. explicit QgsRasterContourLabelProvider( QgsRasterLayer *layer ); ~QgsRasterContourLabelProvider() override; void generateLabels( QgsRenderContext &context, QgsRasterPipe *pipe, QgsRasterViewPort *rasterViewPort, QgsRasterLayerRendererFeedback *feedback ) override; + //! Sets the contour  interval for generating contour lines. void setContourInterval( double interval ) { mContourInterval = interval; } + //! Sets the contour index  interval for distinguishing index contours. void setContourIndexInterval( double interval ) { mContourIndexInterval = interval; } + //! Sets the input raster  band to use for contour generation. void setInputBand( int band ) { mInputBand = band; } + //! Sets the  downscale factor for reducing raster resolution before contouring. void setDownscale( double downscale ) { mDownscale = downscale; } + //! Sets whether to label only index contours. void setLabelIndexOnly( bool indexOnly ) { mLabelIndexOnly = indexOnly; } + //! Adds a contour label along  line with the given  text. void addContourLabel( const QgsLineString &line, const QString &text, QgsRenderContext &context ); private: @@ -88,39 +95,73 @@ class CORE_EXPORT QgsRasterLayerContourLabeling : public QgsAbstractRasterLayerL void multiplyOpacity( double opacityFactor ) override; bool isInScaleRange( double scale ) const override; - //! Creates a QgsRasterLayerContourLabeling from a DOM element with saved configuration + //! Creates a QgsRasterLayerContourLabeling from a DOM element with saved configuration. static QgsRasterLayerContourLabeling *create( const QDomElement &element, const QgsReadWriteContext &context ) SIP_FACTORY; + //! Returns the text format used for rendering contour labels. QgsTextFormat textFormat() const; + + //! Sets the text \a format used for rendering contour labels. void setTextFormat( const QgsTextFormat &format ); + //! Returns the numeric format used for formatting contour elevation values. const QgsNumericFormat *numericFormat() const; + + //! Sets the numeric \a format used for formatting contour elevation values. Ownership is transferred. void setNumericFormat( QgsNumericFormat *format SIP_TRANSFER ); + //! Returns whether only index contours are labeled. bool labelIndexOnly() const { return mLabelIndexOnly; } + + //! Sets whether only index contours should be labeled. void setLabelIndexOnly( bool indexOnly ) { mLabelIndexOnly = indexOnly; } + //! Returns the label priority, where 0 is lowest and 1 is highest. double priority() const { return mPriority; } + + //! Sets the label \a priority, where 0 is lowest and 1 is highest. void setPriority( double priority ) { mPriority = priority; } + //! Returns the label placement settings. \note Not available in Python bindings. const QgsLabelPlacementSettings &placementSettings() const { return mPlacementSettings; } SIP_SKIP + + //! Returns the label placement settings. QgsLabelPlacementSettings &placementSettings() { return mPlacementSettings; } + + //! Sets the label placement \a settings. void setPlacementSettings( const QgsLabelPlacementSettings &settings ) { mPlacementSettings = settings; } + //! Returns the label thinning settings. \note Not available in Python bindings. const QgsLabelThinningSettings &thinningSettings() const { return mThinningSettings; } SIP_SKIP + + //! Returns the label thinning settings. QgsLabelThinningSettings &thinningSettings() { return mThinningSettings; } + + //! Sets the label thinning \a settings. void setThinningSettings( const QgsLabelThinningSettings &settings ) { mThinningSettings = settings; } + //! Returns the z-index for label rendering order. double zIndex() const; + + //! Sets the z-\a index for label rendering order. void setZIndex( double index ); + //! Returns the maximum map scale (most zoomed in) at which labels are visible. double maximumScale() const; + + //! Sets the maximum map \a scale (most zoomed in) at which labels are visible. void setMaximumScale( double scale ); + //! Returns the minimum map scale (most zoomed out) at which labels are visible. double minimumScale() const; + + //! Sets the minimum map \a scale (most zoomed out) at which labels are visible. void setMinimumScale( double scale ); + //! Sets whether scale-based visibility is \a enabled for labels. void setScaleBasedVisibility( bool enabled ); + + //! Returns whether scale-based visibility is enabled for labels. bool hasScaleBasedVisibility() const; private: diff --git a/src/core/raster/qgsrasterlabeling.cpp b/src/core/raster/qgsrasterlabeling.cpp index 4d22529b7fe6..74bd77bcbfd2 100644 --- a/src/core/raster/qgsrasterlabeling.cpp +++ b/src/core/raster/qgsrasterlabeling.cpp @@ -15,7 +15,6 @@ ***************************************************************************/ #include "qgsrasterlabeling.h" -#include "qgsrastercontourlabeling.h" #include "feature.h" #include "labelposition.h" @@ -24,6 +23,7 @@ #include "qgsmessagelog.h" #include "qgsnumericformat.h" #include "qgsnumericformatregistry.h" +#include "qgsrastercontourlabeling.h" #include "qgsrasterlayer.h" #include "qgsrasterlayerrenderer.h" #include "qgsrasterpipe.h" diff --git a/src/gui/raster/qgsrastercontourlabelsettingswidget.cpp b/src/gui/raster/qgsrastercontourlabelsettingswidget.cpp index 23ad3466439f..bbf66540f20c 100644 --- a/src/gui/raster/qgsrastercontourlabelsettingswidget.cpp +++ b/src/gui/raster/qgsrastercontourlabelsettingswidget.cpp @@ -141,7 +141,6 @@ QgsRasterContourLabelSettingsWidget::QgsRasterContourLabelSettingsWidget( QgsRas // fix precision for priority slider mPrioritySlider->setRange( 0, 100 ); mPrioritySlider->setTickInterval( 10 ); - } QgsRasterContourLabelSettingsWidget::~QgsRasterContourLabelSettingsWidget() = default; diff --git a/tests/src/python/test_qgsrasterlabeling.py b/tests/src/python/test_qgsrasterlabeling.py index 53404268bdc8..d00a4e62c1a4 100644 --- a/tests/src/python/test_qgsrasterlabeling.py +++ b/tests/src/python/test_qgsrasterlabeling.py @@ -418,19 +418,14 @@ def test_render_resampled(self): self.render_map_settings_check("resampling", "resampling", mapsettings) ) - def test_contour_labeling(self): labeling = QgsRasterLayerContourLabeling() self.assertEqual(labeling.type(), "contour") - labeling.setBand(2) text_format = QgsTextFormat() text_format.setSize(14) labeling.setTextFormat(text_format) labeling.setNumericFormat(QgsCurrencyNumericFormat()) - labeling.setContourInterval(25) - labeling.setContourIndexInterval(100) - labeling.setDownscale(8) labeling.setLabelIndexOnly(True) labeling.setPriority(0.3) labeling.placementSettings().setOverlapHandling( @@ -443,12 +438,8 @@ def test_contour_labeling(self): labeling.setMinimumScale(50000) labeling.setScaleBasedVisibility(True) - self.assertEqual(labeling.band(), 2) self.assertEqual(labeling.textFormat().size(), 14) self.assertIsInstance(labeling.numericFormat(), QgsCurrencyNumericFormat) - self.assertEqual(labeling.contourInterval(), 25) - self.assertEqual(labeling.contourIndexInterval(), 100) - self.assertEqual(labeling.downscale(), 8) self.assertTrue(labeling.labelIndexOnly()) self.assertEqual(labeling.priority(), 0.3) self.assertEqual( @@ -464,12 +455,8 @@ def test_contour_labeling(self): labeling_clone = labeling.clone() self.assertIsInstance(labeling_clone, QgsRasterLayerContourLabeling) - self.assertEqual(labeling_clone.band(), 2) self.assertEqual(labeling_clone.textFormat().size(), 14) self.assertIsInstance(labeling_clone.numericFormat(), QgsCurrencyNumericFormat) - self.assertEqual(labeling_clone.contourInterval(), 25) - self.assertEqual(labeling_clone.contourIndexInterval(), 100) - self.assertEqual(labeling_clone.downscale(), 8) self.assertTrue(labeling_clone.labelIndexOnly()) self.assertEqual(labeling_clone.priority(), 0.3) self.assertEqual( @@ -491,14 +478,10 @@ def test_contour_labeling(self): element, context ) self.assertIsInstance(labeling_from_xml, QgsRasterLayerContourLabeling) - self.assertEqual(labeling_from_xml.band(), 2) self.assertEqual(labeling_from_xml.textFormat().size(), 14) self.assertIsInstance( labeling_from_xml.numericFormat(), QgsCurrencyNumericFormat ) - self.assertEqual(labeling_from_xml.contourInterval(), 25) - self.assertEqual(labeling_from_xml.contourIndexInterval(), 100) - self.assertEqual(labeling_from_xml.downscale(), 8) self.assertTrue(labeling_from_xml.labelIndexOnly()) self.assertEqual(labeling_from_xml.priority(), 0.3) self.assertEqual( @@ -518,10 +501,6 @@ def test_contour_labeling(self): def test_contour_labeling_defaults(self): labeling = QgsRasterLayerContourLabeling() - self.assertEqual(labeling.band(), 1) - self.assertEqual(labeling.contourInterval(), 100) - self.assertEqual(labeling.contourIndexInterval(), 0) - self.assertEqual(labeling.downscale(), 4) self.assertFalse(labeling.labelIndexOnly()) self.assertEqual(labeling.priority(), 0.5) self.assertEqual(labeling.zIndex(), 0) @@ -550,8 +529,7 @@ def test_contour_labeling_layer_roundtrip(self): self.assertTrue(raster_layer.isValid()) labeling = QgsRasterLayerContourLabeling() - labeling.setContourInterval(50) - labeling.setBand(1) + labeling.setLabelIndexOnly(True) raster_layer.setLabeling(labeling) raster_layer.setLabelsEnabled(True) @@ -567,7 +545,7 @@ def test_contour_labeling_layer_roundtrip(self): self.assertIsInstance(raster_layer2.labeling(), QgsRasterLayerContourLabeling) self.assertTrue(raster_layer2.labelsEnabled()) - self.assertEqual(raster_layer2.labeling().contourInterval(), 50) + self.assertTrue(raster_layer2.labeling().labelIndexOnly()) def testHasNonDefaultCompositionModeContour(self): labeling = QgsRasterLayerContourLabeling() From 1e1bcdf6cf1c8f3f88be7e26f07ef5f359babb66 Mon Sep 17 00:00:00 2001 From: James McGuiness <955899+Geojim@users.noreply.github.com> Date: Sat, 7 Mar 2026 07:28:32 +1000 Subject: [PATCH 3/3] Fix comment style in contour labeling save() Co-Authored-By: Claude Opus 4.6 --- src/core/raster/qgsrastercontourlabeling.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/raster/qgsrastercontourlabeling.cpp b/src/core/raster/qgsrastercontourlabeling.cpp index a083354703bf..a4fb0c27532c 100644 --- a/src/core/raster/qgsrastercontourlabeling.cpp +++ b/src/core/raster/qgsrastercontourlabeling.cpp @@ -331,7 +331,7 @@ QDomElement QgsRasterLayerContourLabeling::save( QDomDocument &doc, const QgsRea { QDomElement renderingElem = doc.createElement( u"rendering"_s ); renderingElem.setAttribute( u"scaleVisibility"_s, mScaleVisibility ); - // element names flipped vs member — matches vector labeling convention + // note the element names are "flipped" vs the member -- this is intentional, and done to match vector labeling renderingElem.setAttribute( u"scaleMin"_s, mMaximumScale ); renderingElem.setAttribute( u"scaleMax"_s, mMinimumScale ); elem.appendChild( renderingElem );