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..3e80b297c78f --- /dev/null +++ b/python/PyQt6/core/auto_generated/raster/qgsrastercontourlabeling.sip.in @@ -0,0 +1,170 @@ +/************************************************************************ + * 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; +%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 + +}; + +/************************************************************************ + * 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..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 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..a4fb0c27532c --- /dev/null +++ b/src/core/raster/qgsrastercontourlabeling.cpp @@ -0,0 +1,474 @@ +/*************************************************************************** + 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 + +#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 "qgsrastercontourrenderer.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 ); + // 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 ); + } + + 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 +{ + // mMaximumScale (most zoomed in) is exclusive ( < --> In range ) + // mMinimumScale (most zoomed out) is inclusive ( >= --> In range ) + return !mScaleVisibility + || ( ( mMinimumScale == 0 || !QgsScaleUtils::equalToOrGreaterThanMinimumScale( scale, mMinimumScale ) ) + && ( mMaximumScale == 0 || !QgsScaleUtils::lessThanMaximumScale( 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..72513158437f --- /dev/null +++ b/src/core/raster/qgsrastercontourlabeling.h @@ -0,0 +1,183 @@ +/*************************************************************************** + 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: + //! 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: + 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; + + //! 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: + 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..74bd77bcbfd2 100644 --- a/src/core/raster/qgsrasterlabeling.cpp +++ b/src/core/raster/qgsrasterlabeling.cpp @@ -23,6 +23,7 @@ #include "qgsmessagelog.h" #include "qgsnumericformat.h" #include "qgsnumericformatregistry.h" +#include "qgsrastercontourlabeling.h" #include "qgsrasterlayer.h" #include "qgsrasterlayerrenderer.h" #include "qgsrasterpipe.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..bbf66540f20c --- /dev/null +++ b/src/gui/raster/qgsrastercontourlabelsettingswidget.cpp @@ -0,0 +1,229 @@ +/*************************************************************************** + 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..d00a4e62c1a4 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, @@ -417,6 +418,144 @@ 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") + + text_format = QgsTextFormat() + text_format.setSize(14) + labeling.setTextFormat(text_format) + labeling.setNumericFormat(QgsCurrencyNumericFormat()) + 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.textFormat().size(), 14) + self.assertIsInstance(labeling.numericFormat(), QgsCurrencyNumericFormat) + 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.textFormat().size(), 14) + self.assertIsInstance(labeling_clone.numericFormat(), QgsCurrencyNumericFormat) + 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.textFormat().size(), 14) + self.assertIsInstance( + labeling_from_xml.numericFormat(), QgsCurrencyNumericFormat + ) + 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.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.setLabelIndexOnly(True) + 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.assertTrue(raster_layer2.labeling().labelIndexOnly()) + + 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()