From a36720163a84c1f586119e3ce9814dd17c376b11 Mon Sep 17 00:00:00 2001 From: McGuiness Date: Sat, 28 Feb 2026 23:09:02 +1000 Subject: [PATCH] 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 0000000000000..d94c2f846a971 --- /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 0000000000000..f78efacffe6d5 --- /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 dcdbfdde775fe..469c6b0410d52 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterlabeling.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterlabeling.sip.in @@ -30,6 +30,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 9ebe607256bbe..d0a56acba19cf 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 c7fdd1ed13fdf..7b75d845769ad 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 0000000000000..de848c72bca97 --- /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 0000000000000..8c1729b016517 --- /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 23f416bad3594..06d69d4f84bca 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" @@ -333,6 +334,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 b7d969d9b8c62..e6949f9b88ff7 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: @@ -51,15 +51,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. @@ -139,7 +139,7 @@ class CORE_EXPORT QgsRasterLayerLabelProvider final : public QgsAbstractLabelPro */ void setResampleOver( int pixels ); - private: + protected: QgsTextFormat mFormat; int mBandNumber = 1; std::unique_ptr< QgsNumericFormat > mNumericFormat; @@ -173,6 +173,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 b2631681c03e4..7c6e0932f3e90 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 0000000000000..23ad3466439f1 --- /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 0000000000000..1b390383f5658 --- /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 f3276b14acb64..ab3f2156afd07 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 ca3af06fba2a6..53404268bdc8e 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()