From d515c621bfc5b0f74d2a60011f602a56e3d72180 Mon Sep 17 00:00:00 2001 From: Jean Felder Date: Thu, 2 Nov 2023 12:30:29 +0100 Subject: [PATCH 01/68] qgswfsgetfeature: Do not invert axis if no SRSNAME is passed A WFS request such as `SERVICE=WFS&REQUEST=GetFeature&VERSION=1.1.0&SRSNAME=EPSG:4326` does not invert the axis and return the coordinates in the LON/LAT order. For example: 2.358 48.865 2.37 48.876 However, the same request without a SRSNAME parameter inverts the axis and returns the the coordinates in the LAT/LON order: 48.865 2.358 48.876 2.37 With this change, the axis is not inverted if the SRSNAME parameter is not passed. This way, this is the same behavior as `SRSNAME=EPSG:4326`. This is a follow-up of https://github.com/qgis/QGIS/pull/45270. --- src/server/services/wfs/qgswfsgetfeature.cpp | 14 ++++++++++++-- ..._getFeature_1_1_0_featureid_0_1_1_0_srsname.txt | 10 +++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/server/services/wfs/qgswfsgetfeature.cpp b/src/server/services/wfs/qgswfsgetfeature.cpp index a484dd1d3bab..dd63670b888e 100644 --- a/src/server/services/wfs/qgswfsgetfeature.cpp +++ b/src/server/services/wfs/qgswfsgetfeature.cpp @@ -461,7 +461,16 @@ namespace QgsWfs { // For WFS 1.1 we honor requested CRS and axis order - const QString srsName {request.serverParameters().value( QStringLiteral( "SRSNAME" ) )}; + // if the CRS is defined in the parameters, use it + // otherwise: + // - geojson uses 'EPSG:4326' by default + // - other formats use the default CRS (DefaultSRS, which is the layer's CRS) + const QString requestSrsName = request.serverParameters().value( QStringLiteral( "SRSNAME" ) ); + const QString srsName + { + !requestSrsName.isEmpty() ? requestSrsName : + ( aRequest.outputFormat == QgsWfsParameters::Format::GeoJSON ? QStringLiteral( "EPSG:4326" ) : outputCrs.authid() ) + }; const bool invertAxis { mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) && outputCrs.hasAxisInverted() && ! srsName.startsWith( QLatin1String( "EPSG:" ) ) }; @@ -1248,7 +1257,8 @@ namespace QgsWfs if ( format == QgsWfsParameters::Format::GML3 ) { // For WFS 1.1 we honor requested CRS and axis order - const QString srsName {request.serverParameters().value( QStringLiteral( "SRSNAME" ) )}; + const QString requestSrsName = request.serverParameters().value( QStringLiteral( "SRSNAME" ) ); + const QString srsName = !requestSrsName.isEmpty() ? requestSrsName : crs.authid(); const bool invertAxis { mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) && crs.hasAxisInverted() && ! srsName.startsWith( QLatin1String( "EPSG:" ) ) }; diff --git a/tests/testdata/qgis_server/wfs_getFeature_1_1_0_featureid_0_1_1_0_srsname.txt b/tests/testdata/qgis_server/wfs_getFeature_1_1_0_featureid_0_1_1_0_srsname.txt index ce39f88833d0..c5c1206856e6 100644 --- a/tests/testdata/qgis_server/wfs_getFeature_1_1_0_featureid_0_1_1_0_srsname.txt +++ b/tests/testdata/qgis_server/wfs_getFeature_1_1_0_featureid_0_1_1_0_srsname.txt @@ -3,21 +3,21 @@ Content-Type: text/xml; subtype=gml/3.1.1; charset=utf-8 - 44.90139484 8.20345931 - 44.90148253 8.20354699 + 8.20345931 44.90139484 + 8.20354699 44.90148253 - 44.90148253 8.20349634 - 44.90148253 8.20349634 + 8.20349634 44.90148253 + 8.20349634 44.90148253 - 44.90148253 8.20349634 + 8.20349634 44.90148253 1 From 8d1c785df81c024958f6ce77410f6d6eed04c2e0 Mon Sep 17 00:00:00 2001 From: Jean Felder Date: Mon, 13 Nov 2023 15:23:43 +0100 Subject: [PATCH 02/68] qgswfsgetfeature: Add a comment to explain axis inversion logic --- src/server/services/wfs/qgswfsgetfeature.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/server/services/wfs/qgswfsgetfeature.cpp b/src/server/services/wfs/qgswfsgetfeature.cpp index dd63670b888e..681d3b7dd288 100644 --- a/src/server/services/wfs/qgswfsgetfeature.cpp +++ b/src/server/services/wfs/qgswfsgetfeature.cpp @@ -461,7 +461,11 @@ namespace QgsWfs { // For WFS 1.1 we honor requested CRS and axis order - // if the CRS is defined in the parameters, use it + // Axis is not inverted if srsName starts with EPSG + // It needs to be an EPSG urn, e.g. urn:ogc:def:crs:EPSG::4326 + // This follows geoserver convention + // See: https://docs.geoserver.org/stable/en/user/services/wfs/axis_order.html + // if the crs is defined in the parameters, use it // otherwise: // - geojson uses 'EPSG:4326' by default // - other formats use the default CRS (DefaultSRS, which is the layer's CRS) @@ -1257,6 +1261,10 @@ namespace QgsWfs if ( format == QgsWfsParameters::Format::GML3 ) { // For WFS 1.1 we honor requested CRS and axis order + // Axis is not inverted if srsName starts with EPSG + // It needs to be an EPSG urn, e.g. urn:ogc:def:crs:EPSG::4326 + // This follows geoserver convention + // See: https://docs.geoserver.org/stable/en/user/services/wfs/axis_order.html const QString requestSrsName = request.serverParameters().value( QStringLiteral( "SRSNAME" ) ); const QString srsName = !requestSrsName.isEmpty() ? requestSrsName : crs.authid(); const bool invertAxis { mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) && From fa34fb78883b80745750bf70dbf6a20c0ca39d56 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Thu, 14 Mar 2024 18:22:52 +0100 Subject: [PATCH 03/68] Browser: fix slow behavior with network drives on windows This fixes UNC paths not identified as network paths. By ensuring that a trailing slash for directories is passed down to the identify function. --- src/core/browser/qgsdirectoryitem.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/browser/qgsdirectoryitem.cpp b/src/core/browser/qgsdirectoryitem.cpp index cb268ba45045..aba67f0fcf4a 100644 --- a/src/core/browser/qgsdirectoryitem.cpp +++ b/src/core/browser/qgsdirectoryitem.cpp @@ -454,7 +454,9 @@ bool QgsDirectoryItem::pathShouldByMonitoredByDefault( const QString &path ) // else if we know that the path is on a slow device, we don't monitor by default // as this can be very expensive and slow down QGIS - if ( QgsFileUtils::pathIsSlowDevice( path ) ) + // Add trailing slash or windows API functions like GetDriveTypeW won't identify + // UNC network drives correctly + if ( QgsFileUtils::pathIsSlowDevice( path.endsWith( '/' ) ? path : path + '/' ) ) return false; // paths are monitored by default if no explicit setting is in place, and the user hasn't From 9beccf3e3c9998a9e819f0080f2d8771dc8d2a61 Mon Sep 17 00:00:00 2001 From: Denis Rouzaud Date: Thu, 14 Mar 2024 18:53:16 +0100 Subject: [PATCH 04/68] correcty save cache in GH actions (#56856) --- .github/workflows/macos-build.yml | 2 +- .github/workflows/mingw-w64-msys2.yml | 2 +- .github/workflows/mingw64.yml | 2 +- .github/workflows/ogc.yml | 2 +- .github/workflows/run-tests.yml | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/macos-build.yml b/.github/workflows/macos-build.yml index ae4852353799..97776a563129 100644 --- a/.github/workflows/macos-build.yml +++ b/.github/workflows/macos-build.yml @@ -135,4 +135,4 @@ jobs: if: ${{ github.event_name == 'push' }} with: path: ${{ env.CCACHE_DIR }} - key: build-ccache-mac-${{ github.ref_name }} + key: build-ccache-mac-${{ github.ref_name }}-${{ github.run_id }} diff --git a/.github/workflows/mingw-w64-msys2.yml b/.github/workflows/mingw-w64-msys2.yml index 51d2f10798b5..9c6d2f1429e6 100644 --- a/.github/workflows/mingw-w64-msys2.yml +++ b/.github/workflows/mingw-w64-msys2.yml @@ -91,4 +91,4 @@ jobs: if: ${{ github.event_name == 'push' }} with: path: build - key: build-ccache-mingw64-msys2-${{ github.ref_name }} + key: build-ccache-mingw64-msys2-${{ github.ref_name }}-${{ github.run_id }} diff --git a/.github/workflows/mingw64.yml b/.github/workflows/mingw64.yml index f01aaea94d88..1815b1ee4769 100644 --- a/.github/workflows/mingw64.yml +++ b/.github/workflows/mingw64.yml @@ -79,7 +79,7 @@ jobs: if: ${{ github.event_name == 'push' }} with: path: /w/.ccache/QGIS - key: build-ccache-mingw64-${{ github.ref_name }} + key: build-ccache-mingw64-${{ github.ref_name }}-${{ github.run_id }} - name: Create Portable zip run: | diff --git a/.github/workflows/ogc.yml b/.github/workflows/ogc.yml index c18b46c8fba3..ecf941d38fa9 100644 --- a/.github/workflows/ogc.yml +++ b/.github/workflows/ogc.yml @@ -76,7 +76,7 @@ jobs: if: ${{ github.event_name == 'push' }} with: path: /home/runner/QGIS/.ccache - key: build-ccache-ogc-${{ github.ref_name }} + key: build-ccache-ogc-${{ github.ref_name }}-${{ github.run_id }} - name: Install pyogctest run: | diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index c189cf45029c..85278b3bad8e 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -141,7 +141,7 @@ jobs: uses: actions/cache/restore@v4 with: path: /home/runner/QGIS/.ccache - key: build-ccache-${{ matrix.distro-version }}-qt${{ matrix.qt-version }}-${{ github.event.pull_request.base.ref || github.ref_name }} + key: build-ccache-${{ matrix.distro-version }}-qt${{ matrix.qt-version }}-${{ github.event.pull_request.base.ref || github.ref_name }} restore-keys: | build-ccache-${{ matrix.distro-version }}-qt${{ matrix.qt-version }}-master @@ -173,7 +173,7 @@ jobs: if: ${{ github.event_name == 'push' }} with: path: /home/runner/QGIS/.ccache - key: build-ccache-${{ matrix.distro-version }}-qt${{ matrix.qt-version }}-${{ github.ref_name }} + key: build-ccache-${{ matrix.distro-version }}-qt${{ matrix.qt-version }}-${{ github.ref_name }}-${{ github.run_id }} - name: Push artifact id: push_artifact From e0d8666fe493bab93e9ef7dda3a208d3007c42e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:54:37 +0000 Subject: [PATCH 05/68] Bump follow-redirects in /resources/server/src/landingpage Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.4 to 1.15.6. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.4...v1.15.6) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] --- resources/server/src/landingpage/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/server/src/landingpage/yarn.lock b/resources/server/src/landingpage/yarn.lock index b5dc9843d076..dca25662e444 100644 --- a/resources/server/src/landingpage/yarn.lock +++ b/resources/server/src/landingpage/yarn.lock @@ -4544,9 +4544,9 @@ flow-parser@0.*: integrity sha512-3dipGWKnXmE4LEE5yCPHJrSlMYOPAYU7wMBecfKiWPQSZp1CvkpJ59dfuuUIeM2TSttKGSatep77vGG9cjkeqg== follow-redirects@^1.0.0: - version "1.15.4" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf" - integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw== + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== for-in@^1.0.2: version "1.0.2" From 95bae0e8105be7243a7a264a758a7fa0b2274de9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Tue, 12 Mar 2024 18:02:30 +0100 Subject: [PATCH 06/68] [processing] Avoid non-spatial vector layers in DXF export alg --- .../qgsprocessingparameterdxflayers.cpp | 38 +++++++++++++------ .../qgsprocessingdxflayerswidgetwrapper.cpp | 2 +- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/core/processing/qgsprocessingparameterdxflayers.cpp b/src/core/processing/qgsprocessingparameterdxflayers.cpp index 030b236cac9e..93164aec23b2 100644 --- a/src/core/processing/qgsprocessingparameterdxflayers.cpp +++ b/src/core/processing/qgsprocessingparameterdxflayers.cpp @@ -37,9 +37,11 @@ bool QgsProcessingParameterDxfLayers::checkValueIsAcceptable( const QVariant &in if ( !input.isValid() ) return mFlags & Qgis::ProcessingParameterFlag::Optional; - if ( qobject_cast< QgsVectorLayer * >( qvariant_cast( input ) ) ) + QgsMapLayer *mapLayer = nullptr; + QgsVectorLayer *vectorLayer = qobject_cast< QgsVectorLayer * >( qvariant_cast( input ) ); + if ( vectorLayer ) { - return true; + return vectorLayer->isSpatial(); } if ( input.type() == QVariant::String ) @@ -50,8 +52,8 @@ bool QgsProcessingParameterDxfLayers::checkValueIsAcceptable( const QVariant &in if ( !context ) return true; - QgsMapLayer *mapLayer = QgsProcessingUtils::mapLayerFromString( input.toString(), *context ); - return mapLayer && ( mapLayer->type() == Qgis::LayerType::Vector ); + mapLayer = QgsProcessingUtils::mapLayerFromString( input.toString(), *context ); + return mapLayer && ( mapLayer->type() == Qgis::LayerType::Vector && mapLayer->isSpatial() ); } else if ( input.type() == QVariant::List ) { @@ -61,16 +63,22 @@ bool QgsProcessingParameterDxfLayers::checkValueIsAcceptable( const QVariant &in const QVariantList layerList = input.toList(); for ( const QVariant &variantLayer : layerList ) { - if ( qobject_cast< QgsVectorLayer * >( qvariant_cast( variantLayer ) ) ) - continue; + vectorLayer = qobject_cast< QgsVectorLayer * >( qvariant_cast( variantLayer ) ); + if ( vectorLayer ) + { + if ( vectorLayer->isSpatial() ) + continue; + else + return false; + } if ( variantLayer.type() == QVariant::String ) { if ( !context ) return true; - QgsMapLayer *mapLayer = QgsProcessingUtils::mapLayerFromString( variantLayer.toString(), *context ); - if ( !mapLayer || mapLayer->type() != Qgis::LayerType::Vector ) + mapLayer = QgsProcessingUtils::mapLayerFromString( variantLayer.toString(), *context ); + if ( !mapLayer || mapLayer->type() != Qgis::LayerType::Vector || !mapLayer->isSpatial() ) return false; } else if ( variantLayer.type() == QVariant::Map ) @@ -83,11 +91,11 @@ bool QgsProcessingParameterDxfLayers::checkValueIsAcceptable( const QVariant &in if ( !context ) return true; - QgsMapLayer *mapLayer = QgsProcessingUtils::mapLayerFromString( layerMap.value( QStringLiteral( "layer" ) ).toString(), *context ); - if ( !mapLayer || mapLayer->type() != Qgis::LayerType::Vector ) + mapLayer = QgsProcessingUtils::mapLayerFromString( layerMap.value( QStringLiteral( "layer" ) ).toString(), *context ); + if ( !mapLayer || mapLayer->type() != Qgis::LayerType::Vector || !mapLayer->isSpatial() ) return false; - QgsVectorLayer *vectorLayer = static_cast( mapLayer ); + vectorLayer = static_cast( mapLayer ); if ( !vectorLayer ) return false; @@ -113,7 +121,13 @@ bool QgsProcessingParameterDxfLayers::checkValueIsAcceptable( const QVariant &in for ( const QString &v : constToStringList ) { - if ( !QgsProcessingUtils::mapLayerFromString( v, *context ) ) + mapLayer = QgsProcessingUtils::mapLayerFromString( v, *context ); + if ( !mapLayer ) + return false; + + if ( mapLayer->type() == Qgis::LayerType::Vector && mapLayer->isSpatial() ) + continue; + else return false; } return true; diff --git a/src/gui/processing/qgsprocessingdxflayerswidgetwrapper.cpp b/src/gui/processing/qgsprocessingdxflayerswidgetwrapper.cpp index b8c8c7c6c6dd..dcc0e73705ef 100644 --- a/src/gui/processing/qgsprocessingdxflayerswidgetwrapper.cpp +++ b/src/gui/processing/qgsprocessingdxflayerswidgetwrapper.cpp @@ -93,7 +93,7 @@ QgsProcessingDxfLayersPanelWidget::QgsProcessingDxfLayersPanelWidget( seenVectorLayers.insert( layer.layer() ); } - const QList options = QgsProcessingUtils::compatibleVectorLayers( project, QList< int >() ); + const QList options = QgsProcessingUtils::compatibleVectorLayers( project, QList< int >() << static_cast( Qgis::ProcessingSourceType::VectorAnyGeometry ) ); for ( const QgsVectorLayer *layer : options ) { if ( seenVectorLayers.contains( layer ) ) From 124f93b86e09599f3501202e15a408b587580428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Tue, 12 Mar 2024 18:02:39 +0100 Subject: [PATCH 07/68] [tests] Tests for avoiding non-spatial vector layers in DXF export alg --- tests/src/analysis/testqgsprocessing.cpp | 27 ++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/src/analysis/testqgsprocessing.cpp b/tests/src/analysis/testqgsprocessing.cpp index 090ef9344b50..4dfbd740774d 100644 --- a/tests/src/analysis/testqgsprocessing.cpp +++ b/tests/src/analysis/testqgsprocessing.cpp @@ -11022,6 +11022,7 @@ void TestQgsProcessing::parameterDxfLayers() QVERIFY( !def->checkValueIsAcceptable( "" ) ); QVERIFY( !def->checkValueIsAcceptable( QVariant() ) ); QVERIFY( def->checkValueIsAcceptable( QVariant::fromValue( vectorLayer ) ) ); + QVERIFY( def->checkValueIsAcceptable( QStringLiteral( "PointLayer" ), &context ) ); // should also be OK QVERIFY( def->checkValueIsAcceptable( "c:/Users/admin/Desktop/roads_clipped_transformed_v1_reprojected_final_clipped_aAAA.shp" ) ); @@ -11051,6 +11052,32 @@ void TestQgsProcessing::parameterDxfLayers() layerList[0] = layerMap; QVERIFY( def->checkValueIsAcceptable( layerList, &context ) ); + // checkValueIsAcceptable on non-spatial layers + QgsVectorLayer *nonSpatialLayer = new QgsVectorLayer( QStringLiteral( "None" ), + QStringLiteral( "NonSpatialLayer" ), + QStringLiteral( "memory" ) ); + project.addMapLayer( nonSpatialLayer ); + + QVERIFY( !def->checkValueIsAcceptable( QVariant::fromValue( nonSpatialLayer ) ) ); + QVariantList wrongLayerList; + wrongLayerList.append( QVariant::fromValue( nonSpatialLayer ) ); + QVERIFY( !def->checkValueIsAcceptable( wrongLayerList ) ); + + QVERIFY( !def->checkValueIsAcceptable( QStringLiteral( "NonSpatialLayer" ), &context ) ); + + QStringList stringList = { QStringLiteral( "PointLayer" ) }; + QVERIFY( def->checkValueIsAcceptable( stringList ) ); + stringList << QStringLiteral( "NonSpatialLayer" ); + QVERIFY( !def->checkValueIsAcceptable( stringList, &context ) ); + + QVariantMap wrongLayerMap; + wrongLayerMap["layer"] = "NonSpatialLayer"; + wrongLayerMap["attributeIndex"] = -1; + QVariantList wrongLayerMapList; + wrongLayerMapList.append( wrongLayerMap ); + QVERIFY( !def->checkValueIsAcceptable( wrongLayerMapList, &context ) ); + + // Check values const QString valueAsPythonString = def->valueAsPythonString( layerList, context ); QCOMPARE( valueAsPythonString, QStringLiteral( "[{'layer': '%1','attributeIndex': -1}]" ).arg( vectorLayer->source() ) ); QCOMPARE( QString::fromStdString( QgsJsonUtils::jsonFromVariant( def->valueAsJsonObject( layerList, context ) ).dump() ), From f1ed461a61cc91e38d9dee77b682b3d564e51226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Thu, 14 Mar 2024 15:27:07 +0100 Subject: [PATCH 08/68] Apply code review suggestion --- src/core/processing/qgsprocessingparameterdxflayers.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/processing/qgsprocessingparameterdxflayers.cpp b/src/core/processing/qgsprocessingparameterdxflayers.cpp index 93164aec23b2..816e989363fe 100644 --- a/src/core/processing/qgsprocessingparameterdxflayers.cpp +++ b/src/core/processing/qgsprocessingparameterdxflayers.cpp @@ -38,7 +38,7 @@ bool QgsProcessingParameterDxfLayers::checkValueIsAcceptable( const QVariant &in return mFlags & Qgis::ProcessingParameterFlag::Optional; QgsMapLayer *mapLayer = nullptr; - QgsVectorLayer *vectorLayer = qobject_cast< QgsVectorLayer * >( qvariant_cast( input ) ); + QgsVectorLayer *vectorLayer = input.value(); if ( vectorLayer ) { return vectorLayer->isSpatial(); @@ -63,7 +63,7 @@ bool QgsProcessingParameterDxfLayers::checkValueIsAcceptable( const QVariant &in const QVariantList layerList = input.toList(); for ( const QVariant &variantLayer : layerList ) { - vectorLayer = qobject_cast< QgsVectorLayer * >( qvariant_cast( variantLayer ) ); + vectorLayer = input.value(); if ( vectorLayer ) { if ( vectorLayer->isSpatial() ) From c4272d700bc6b308e9eba4a4a664eb72244579af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Thu, 14 Mar 2024 15:53:37 +0100 Subject: [PATCH 09/68] Get rid of leftover lines in DXF Export --- src/app/qgsdxfexportdialog.cpp | 2 -- src/core/dxf/qgsdxfexport.cpp | 1 - 2 files changed, 3 deletions(-) diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index 731308ed778c..80e14e6b656d 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -538,7 +538,6 @@ void QgsVectorLayerAndAttributeModel::loadLayersOutputAttribute( QgsLayerTreeNod emit dataChanged( idx, idx, QVector() << Qt::EditRole ); } } - continue; } else if ( QgsLayerTree::isGroup( child ) ) { @@ -569,7 +568,6 @@ void QgsVectorLayerAndAttributeModel::saveLayersOutputAttribute( QgsLayerTreeNod vl->removeCustomProperty( QStringLiteral( "lastDxfOutputAttribute" ) ); } } - continue; } else if ( QgsLayerTree::isGroup( child ) ) { diff --git a/src/core/dxf/qgsdxfexport.cpp b/src/core/dxf/qgsdxfexport.cpp index 3575f9a2640d..96b920c0d7d4 100644 --- a/src/core/dxf/qgsdxfexport.cpp +++ b/src/core/dxf/qgsdxfexport.cpp @@ -1876,7 +1876,6 @@ void QgsDxfExport::addFeature( QgsSymbolRenderContext &ctx, const QgsCoordinateT if ( brushStyle != Qt::NoBrush ) { const QgsAbstractGeometry *sourceGeom = geom.constGet(); - std::unique_ptr< QgsAbstractGeometry > tempGeom; switch ( QgsWkbTypes::flatType( geometryType ) ) { From b0f8eed17a540c9b7a5bfb84583ebab33d048e2b Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Fri, 15 Mar 2024 14:19:43 +0700 Subject: [PATCH 10/68] [plugins manager] Normalize _all_ version references --- python/pyplugin_installer/installer_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/pyplugin_installer/installer_data.py b/python/pyplugin_installer/installer_data.py index 040d195b0edf..089d10a872a5 100644 --- a/python/pyplugin_installer/installer_data.py +++ b/python/pyplugin_installer/installer_data.py @@ -387,8 +387,8 @@ def xmlDownloaded(self): "plugin_id": plugin_id, "name": pluginNodes.item(i).toElement().attribute("name"), "version_available": version, - "version_available_stable": version if not experimental else "", - "version_available_experimental": version if experimental else "", + "version_available_stable": normalizeVersion(version) if not experimental else "", + "version_available_experimental": normalizeVersion(version) if experimental else "", "description": pluginNodes.item(i).firstChildElement("description").text().strip(), "about": pluginNodes.item(i).firstChildElement("about").text().strip(), "author_name": pluginNodes.item(i).firstChildElement("author_name").text().strip(), From 844f0ccc74fcbe2698f78b7c2daa27ad4ec8f58b Mon Sep 17 00:00:00 2001 From: Matthias Kuhn Date: Fri, 15 Mar 2024 16:50:00 +0100 Subject: [PATCH 11/68] Switch to conditional protobuf-lite target The original version number check was completely random numbers --- CMakeLists.txt | 3 --- src/core/CMakeLists.txt | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 564a1fe30bb4..e6f1278f7bf4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -375,9 +375,6 @@ if(WITH_CORE) find_package(Protobuf CONFIG) find_package(Protobuf REQUIRED) - if(Protobuf_VERSION GREATER_EQUAL 4.23) - set(Protobuf_LITE_LIBRARY protobuf::libprotobuf-lite) - endif() message(STATUS "Found Protobuf: ${Protobuf_LIBRARIES}") if (NOT Protobuf_PROTOC_EXECUTABLE) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 26fca1e2bf99..692753b4e273 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -2460,7 +2460,7 @@ target_link_libraries(qgis_core EXPAT::EXPAT ${SQLITE3_LIBRARY} ${LIBZIP_LIBRARY} - ${Protobuf_LITE_LIBRARY} + $ ${ZLIB_LIBRARIES} ${EXIV2_LIBRARY} PROJ::proj From 5d29d498691f5b69bea6450f68f38559bea7034f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 14 Mar 2024 12:45:26 +1000 Subject: [PATCH 12/68] [api] Add virtual QgsRasterRenderer::setInputBand method Attempts to set the input band for the renderer. Returns TRUE if the band was successfully set, or FALSE if the band could not be set. This was implemented in various raster renderer subclasses, but it was necessary to down cast and then call the individual methods (which don't have consistent names!). Instead, add a top level virtual method so that it's easy to change the input band for the renderers. --- .../raster/qgshillshaderenderer.sip.in | 2 ++ .../raster/qgspalettedrasterrenderer.sip.in | 3 +++ .../raster/qgsrastercontourrenderer.sip.in | 6 ++---- .../raster/qgsrasterrenderer.sip.in | 13 +++++++++++++ .../qgssinglebandcolordatarenderer.sip.in | 2 ++ .../raster/qgssinglebandgrayrenderer.sip.in | 3 +++ .../qgssinglebandpseudocolorrenderer.sip.in | 2 ++ .../raster/qgshillshaderenderer.sip.in | 2 ++ .../raster/qgspalettedrasterrenderer.sip.in | 3 +++ .../raster/qgsrastercontourrenderer.sip.in | 6 ++---- .../raster/qgsrasterrenderer.sip.in | 13 +++++++++++++ .../qgssinglebandcolordatarenderer.sip.in | 2 ++ .../raster/qgssinglebandgrayrenderer.sip.in | 3 +++ .../qgssinglebandpseudocolorrenderer.sip.in | 2 ++ src/core/raster/qgshillshaderenderer.cpp | 12 +++++++++--- src/core/raster/qgshillshaderenderer.h | 1 + src/core/raster/qgspalettedrasterrenderer.cpp | 6 ++++++ src/core/raster/qgspalettedrasterrenderer.h | 2 ++ src/core/raster/qgsrastercontourrenderer.cpp | 6 ++++++ src/core/raster/qgsrastercontourrenderer.h | 3 +-- src/core/raster/qgsrasterrenderer.cpp | 5 +++++ src/core/raster/qgsrasterrenderer.h | 11 +++++++++++ .../raster/qgssinglebandcolordatarenderer.cpp | 11 +++++++++-- .../raster/qgssinglebandcolordatarenderer.h | 1 + src/core/raster/qgssinglebandgrayrenderer.cpp | 11 +++++++++++ src/core/raster/qgssinglebandgrayrenderer.h | 4 +++- .../raster/qgssinglebandpseudocolorrenderer.cpp | 17 ++++++++++++----- .../raster/qgssinglebandpseudocolorrenderer.h | 1 + 28 files changed, 132 insertions(+), 21 deletions(-) diff --git a/python/PyQt6/core/auto_generated/raster/qgshillshaderenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgshillshaderenderer.sip.in index cc5d267880c4..27cda9e7897e 100644 --- a/python/PyQt6/core/auto_generated/raster/qgshillshaderenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgshillshaderenderer.sip.in @@ -71,6 +71,8 @@ Sets the band used by the renderer. .. seealso:: :py:func:`band` %End + virtual bool setInputBand( int band ); + double azimuth() const; %Docstring diff --git a/python/PyQt6/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in index 40300d544def..31201922779f 100644 --- a/python/PyQt6/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in @@ -127,6 +127,9 @@ Set category label Returns the raster band used for rendering the raster. %End + virtual bool setInputBand( int band ); + + virtual void writeXml( QDomDocument &doc, QDomElement &parentElem ) const; virtual QList< QPair< QString, QColor > > legendSymbologyItems() const; diff --git a/python/PyQt6/core/auto_generated/raster/qgsrastercontourrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrastercontourrenderer.sip.in index 76ac4cd2e463..c7b2ec5d393b 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrastercontourrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrastercontourrenderer.sip.in @@ -56,10 +56,8 @@ Creates an instance of the renderer based on definition from XML (used by render %Docstring Returns the number of the input raster band %End - void setInputBand( int band ); -%Docstring -Sets the number of the input raster band -%End + virtual bool setInputBand( int band ); + double contourInterval() const; %Docstring diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterrenderer.sip.in index e0b862500f9d..91c1201b6967 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterrenderer.sip.in @@ -67,6 +67,19 @@ The default implementation returns ``False``. virtual bool setInput( QgsRasterInterface *input ); + virtual bool setInputBand( int band ); +%Docstring +Attempts to set the input ``band`` for the renderer. + +Returns ``True`` if the band was successfully set, or ``False`` if the band could not be set. + +.. note:: + + Not all renderers support setting the input band. + +.. versionadded:: 3.38 +%End + virtual QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, diff --git a/python/PyQt6/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in index f4403c460cac..2238df13e45a 100644 --- a/python/PyQt6/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in @@ -35,6 +35,8 @@ QgsSingleBandColorDataRenderer cannot be copied. Use :py:func:`~QgsSingleBandCol virtual bool setInput( QgsRasterInterface *input ); + virtual bool setInputBand( int band ); + virtual QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = 0 ) /Factory/; diff --git a/python/PyQt6/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in index 57fd3ac74289..5785b7c4186f 100644 --- a/python/PyQt6/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in @@ -45,6 +45,9 @@ QgsSingleBandGrayRenderer cannot be copied. Use :py:func:`~QgsSingleBandGrayRend int grayBand() const; void setGrayBand( int band ); + virtual bool setInputBand( int band ); + + const QgsContrastEnhancement *contrastEnhancement() const; void setContrastEnhancement( QgsContrastEnhancement *ce /Transfer/ ); %Docstring diff --git a/python/PyQt6/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in index 4e2e1d230461..ebd39d0da3f9 100644 --- a/python/PyQt6/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in @@ -101,6 +101,8 @@ Sets the band used by the renderer. .. seealso:: :py:func:`band` %End + virtual bool setInputBand( int band ); + double classificationMin() const; double classificationMax() const; diff --git a/python/core/auto_generated/raster/qgshillshaderenderer.sip.in b/python/core/auto_generated/raster/qgshillshaderenderer.sip.in index cc5d267880c4..27cda9e7897e 100644 --- a/python/core/auto_generated/raster/qgshillshaderenderer.sip.in +++ b/python/core/auto_generated/raster/qgshillshaderenderer.sip.in @@ -71,6 +71,8 @@ Sets the band used by the renderer. .. seealso:: :py:func:`band` %End + virtual bool setInputBand( int band ); + double azimuth() const; %Docstring diff --git a/python/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in b/python/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in index 40300d544def..31201922779f 100644 --- a/python/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in +++ b/python/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in @@ -127,6 +127,9 @@ Set category label Returns the raster band used for rendering the raster. %End + virtual bool setInputBand( int band ); + + virtual void writeXml( QDomDocument &doc, QDomElement &parentElem ) const; virtual QList< QPair< QString, QColor > > legendSymbologyItems() const; diff --git a/python/core/auto_generated/raster/qgsrastercontourrenderer.sip.in b/python/core/auto_generated/raster/qgsrastercontourrenderer.sip.in index 76ac4cd2e463..c7b2ec5d393b 100644 --- a/python/core/auto_generated/raster/qgsrastercontourrenderer.sip.in +++ b/python/core/auto_generated/raster/qgsrastercontourrenderer.sip.in @@ -56,10 +56,8 @@ Creates an instance of the renderer based on definition from XML (used by render %Docstring Returns the number of the input raster band %End - void setInputBand( int band ); -%Docstring -Sets the number of the input raster band -%End + virtual bool setInputBand( int band ); + double contourInterval() const; %Docstring diff --git a/python/core/auto_generated/raster/qgsrasterrenderer.sip.in b/python/core/auto_generated/raster/qgsrasterrenderer.sip.in index e0b862500f9d..91c1201b6967 100644 --- a/python/core/auto_generated/raster/qgsrasterrenderer.sip.in +++ b/python/core/auto_generated/raster/qgsrasterrenderer.sip.in @@ -67,6 +67,19 @@ The default implementation returns ``False``. virtual bool setInput( QgsRasterInterface *input ); + virtual bool setInputBand( int band ); +%Docstring +Attempts to set the input ``band`` for the renderer. + +Returns ``True`` if the band was successfully set, or ``False`` if the band could not be set. + +.. note:: + + Not all renderers support setting the input band. + +.. versionadded:: 3.38 +%End + virtual QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, diff --git a/python/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in b/python/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in index f4403c460cac..2238df13e45a 100644 --- a/python/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in +++ b/python/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in @@ -35,6 +35,8 @@ QgsSingleBandColorDataRenderer cannot be copied. Use :py:func:`~QgsSingleBandCol virtual bool setInput( QgsRasterInterface *input ); + virtual bool setInputBand( int band ); + virtual QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = 0 ) /Factory/; diff --git a/python/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in b/python/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in index adbb2fdd92a0..f58d8a2763f3 100644 --- a/python/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in +++ b/python/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in @@ -45,6 +45,9 @@ QgsSingleBandGrayRenderer cannot be copied. Use :py:func:`~QgsSingleBandGrayRend int grayBand() const; void setGrayBand( int band ); + virtual bool setInputBand( int band ); + + const QgsContrastEnhancement *contrastEnhancement() const; void setContrastEnhancement( QgsContrastEnhancement *ce /Transfer/ ); %Docstring diff --git a/python/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in b/python/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in index 4e2e1d230461..ebd39d0da3f9 100644 --- a/python/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in +++ b/python/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in @@ -101,6 +101,8 @@ Sets the band used by the renderer. .. seealso:: :py:func:`band` %End + virtual bool setInputBand( int band ); + double classificationMin() const; double classificationMax() const; diff --git a/src/core/raster/qgshillshaderenderer.cpp b/src/core/raster/qgshillshaderenderer.cpp index c4d5f8e8211a..45bbe41212be 100644 --- a/src/core/raster/qgshillshaderenderer.cpp +++ b/src/core/raster/qgshillshaderenderer.cpp @@ -571,11 +571,17 @@ QList QgsHillshadeRenderer::usesBands() const void QgsHillshadeRenderer::setBand( int bandNo ) { - if ( bandNo > mInput->bandCount() || bandNo <= 0 ) + setInputBand( bandNo ); +} + +bool QgsHillshadeRenderer::setInputBand( int band ) +{ + if ( band > mInput->bandCount() || band <= 0 ) { - return; + return false; } - mBand = bandNo; + mBand = band; + return true; } void QgsHillshadeRenderer::toSld( QDomDocument &doc, QDomElement &element, const QVariantMap &props ) const diff --git a/src/core/raster/qgshillshaderenderer.h b/src/core/raster/qgshillshaderenderer.h index b75a3034052c..bb11335e9364 100644 --- a/src/core/raster/qgshillshaderenderer.h +++ b/src/core/raster/qgshillshaderenderer.h @@ -74,6 +74,7 @@ class CORE_EXPORT QgsHillshadeRenderer : public QgsRasterRenderer * \see band */ void setBand( int bandNo ); + bool setInputBand( int band ) override; /** * Returns the direction of the light over the raster between 0-360. diff --git a/src/core/raster/qgspalettedrasterrenderer.cpp b/src/core/raster/qgspalettedrasterrenderer.cpp index 54023f1e762b..2dac40ef63bf 100644 --- a/src/core/raster/qgspalettedrasterrenderer.cpp +++ b/src/core/raster/qgspalettedrasterrenderer.cpp @@ -175,6 +175,12 @@ void QgsPalettedRasterRenderer::setLabel( double idx, const QString &label ) } } +bool QgsPalettedRasterRenderer::setInputBand( int band ) +{ + mBand = band; + return true; +} + QgsRasterBlock *QgsPalettedRasterRenderer::block( int, QgsRectangle const &extent, int width, int height, QgsRasterBlockFeedback *feedback ) { std::unique_ptr< QgsRasterBlock > outputBlock( new QgsRasterBlock() ); diff --git a/src/core/raster/qgspalettedrasterrenderer.h b/src/core/raster/qgspalettedrasterrenderer.h index 23028265a219..9b209e0d9a90 100644 --- a/src/core/raster/qgspalettedrasterrenderer.h +++ b/src/core/raster/qgspalettedrasterrenderer.h @@ -146,6 +146,8 @@ class CORE_EXPORT QgsPalettedRasterRenderer: public QgsRasterRenderer */ int band() const { return mBand; } + bool setInputBand( int band ) override; + void writeXml( QDomDocument &doc, QDomElement &parentElem ) const override; QList< QPair< QString, QColor > > legendSymbologyItems() const override; QList createLegendNodes( QgsLayerTreeLayer *nodeLayer ) SIP_FACTORY override; diff --git a/src/core/raster/qgsrastercontourrenderer.cpp b/src/core/raster/qgsrastercontourrenderer.cpp index 1905ffe54829..08de0ddf5cbe 100644 --- a/src/core/raster/qgsrastercontourrenderer.cpp +++ b/src/core/raster/qgsrastercontourrenderer.cpp @@ -232,6 +232,12 @@ QList QgsRasterContourRenderer::createLegendNodes return nodes; } +bool QgsRasterContourRenderer::setInputBand( int band ) +{ + mInputBand = band; + return true; +} + void QgsRasterContourRenderer::setContourSymbol( QgsLineSymbol *symbol ) { mContourSymbol.reset( symbol ); diff --git a/src/core/raster/qgsrastercontourrenderer.h b/src/core/raster/qgsrastercontourrenderer.h index 9953f336e360..950541328676 100644 --- a/src/core/raster/qgsrastercontourrenderer.h +++ b/src/core/raster/qgsrastercontourrenderer.h @@ -55,8 +55,7 @@ class CORE_EXPORT QgsRasterContourRenderer : public QgsRasterRenderer //! Returns the number of the input raster band int inputBand() const { return mInputBand; } - //! Sets the number of the input raster band - void setInputBand( int band ) { mInputBand = band; } + bool setInputBand( int band ) override; //! Returns the interval of contour lines generation double contourInterval() const { return mContourInterval; } diff --git a/src/core/raster/qgsrasterrenderer.cpp b/src/core/raster/qgsrasterrenderer.cpp index 87e47e30a9b2..f70297264016 100644 --- a/src/core/raster/qgsrasterrenderer.cpp +++ b/src/core/raster/qgsrasterrenderer.cpp @@ -97,6 +97,11 @@ bool QgsRasterRenderer::setInput( QgsRasterInterface *input ) return true; } +bool QgsRasterRenderer::setInputBand( int ) +{ + return false; +} + bool QgsRasterRenderer::usesTransparency() const { if ( !mInput ) diff --git a/src/core/raster/qgsrasterrenderer.h b/src/core/raster/qgsrasterrenderer.h index e696b0af05f2..3cc9a9e880bb 100644 --- a/src/core/raster/qgsrasterrenderer.h +++ b/src/core/raster/qgsrasterrenderer.h @@ -85,6 +85,17 @@ class CORE_EXPORT QgsRasterRenderer : public QgsRasterInterface bool setInput( QgsRasterInterface *input ) override; + /** + * Attempts to set the input \a band for the renderer. + * + * Returns TRUE if the band was successfully set, or FALSE if the band could not be set. + * + * \note Not all renderers support setting the input band. + * + * \since QGIS 3.38 + */ + virtual bool setInputBand( int band ); + QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, diff --git a/src/core/raster/qgssinglebandcolordatarenderer.cpp b/src/core/raster/qgssinglebandcolordatarenderer.cpp index 870d7d7235d8..7bd001729d8e 100644 --- a/src/core/raster/qgssinglebandcolordatarenderer.cpp +++ b/src/core/raster/qgssinglebandcolordatarenderer.cpp @@ -23,8 +23,9 @@ #include #include -QgsSingleBandColorDataRenderer::QgsSingleBandColorDataRenderer( QgsRasterInterface *input, int band ): - QgsRasterRenderer( input, QStringLiteral( "singlebandcolordata" ) ), mBand( band ) +QgsSingleBandColorDataRenderer::QgsSingleBandColorDataRenderer( QgsRasterInterface *input, int band ) + : QgsRasterRenderer( input, QStringLiteral( "singlebandcolordata" ) ) + , mBand( band ) { } @@ -139,3 +140,9 @@ bool QgsSingleBandColorDataRenderer::setInput( QgsRasterInterface *input ) } return false; } + +bool QgsSingleBandColorDataRenderer::setInputBand( int band ) +{ + mBand = band; + return true; +} diff --git a/src/core/raster/qgssinglebandcolordatarenderer.h b/src/core/raster/qgssinglebandcolordatarenderer.h index a1c94e8735b8..7f2cf88e9e96 100644 --- a/src/core/raster/qgssinglebandcolordatarenderer.h +++ b/src/core/raster/qgssinglebandcolordatarenderer.h @@ -44,6 +44,7 @@ class CORE_EXPORT QgsSingleBandColorDataRenderer: public QgsRasterRenderer static QgsRasterRenderer *create( const QDomElement &elem, QgsRasterInterface *input ) SIP_FACTORY; bool setInput( QgsRasterInterface *input ) override; + bool setInputBand( int band ) override; QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = nullptr ) override SIP_FACTORY; diff --git a/src/core/raster/qgssinglebandgrayrenderer.cpp b/src/core/raster/qgssinglebandgrayrenderer.cpp index 4e98e5aebac5..13431033893b 100644 --- a/src/core/raster/qgssinglebandgrayrenderer.cpp +++ b/src/core/raster/qgssinglebandgrayrenderer.cpp @@ -193,6 +193,17 @@ QgsRasterBlock *QgsSingleBandGrayRenderer::block( int bandNo, const QgsRectangle return outputBlock.release(); } +void QgsSingleBandGrayRenderer::setGrayBand( int band ) +{ + setInputBand( band ); +} + +bool QgsSingleBandGrayRenderer::setInputBand( int band ) +{ + mGrayBand = band; + return true; +} + void QgsSingleBandGrayRenderer::writeXml( QDomDocument &doc, QDomElement &parentElem ) const { if ( parentElem.isNull() ) diff --git a/src/core/raster/qgssinglebandgrayrenderer.h b/src/core/raster/qgssinglebandgrayrenderer.h index 99fd1ba1416d..1c4a600689c3 100644 --- a/src/core/raster/qgssinglebandgrayrenderer.h +++ b/src/core/raster/qgssinglebandgrayrenderer.h @@ -56,7 +56,9 @@ class CORE_EXPORT QgsSingleBandGrayRenderer: public QgsRasterRenderer QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = nullptr ) override SIP_FACTORY; int grayBand() const { return mGrayBand; } - void setGrayBand( int band ) { mGrayBand = band; } + void setGrayBand( int band ); + bool setInputBand( int band ) override; + const QgsContrastEnhancement *contrastEnhancement() const { return mContrastEnhancement.get(); } //! Takes ownership void setContrastEnhancement( QgsContrastEnhancement *ce SIP_TRANSFER ); diff --git a/src/core/raster/qgssinglebandpseudocolorrenderer.cpp b/src/core/raster/qgssinglebandpseudocolorrenderer.cpp index a9f779537e21..f85bd3e93e47 100644 --- a/src/core/raster/qgssinglebandpseudocolorrenderer.cpp +++ b/src/core/raster/qgssinglebandpseudocolorrenderer.cpp @@ -37,17 +37,23 @@ QgsSingleBandPseudoColorRenderer::QgsSingleBandPseudoColorRenderer( QgsRasterInt } void QgsSingleBandPseudoColorRenderer::setBand( int bandNo ) +{ + setInputBand( bandNo ); +} + +bool QgsSingleBandPseudoColorRenderer::setInputBand( int band ) { if ( !mInput ) { - mBand = bandNo; - return; + mBand = band; + return true; } - - if ( bandNo <= mInput->bandCount() || bandNo > 0 ) + else if ( band <= mInput->bandCount() || band > 0 ) { - mBand = bandNo; + mBand = band; + return true; } + return false; } void QgsSingleBandPseudoColorRenderer::setClassificationMin( double min ) @@ -481,3 +487,4 @@ bool QgsSingleBandPseudoColorRenderer::canCreateRasterAttributeTable() const { return true; } + diff --git a/src/core/raster/qgssinglebandpseudocolorrenderer.h b/src/core/raster/qgssinglebandpseudocolorrenderer.h index d6278b0f95bc..333ffffef90f 100644 --- a/src/core/raster/qgssinglebandpseudocolorrenderer.h +++ b/src/core/raster/qgssinglebandpseudocolorrenderer.h @@ -95,6 +95,7 @@ class CORE_EXPORT QgsSingleBandPseudoColorRenderer: public QgsRasterRenderer * \see band */ void setBand( int bandNo ); + bool setInputBand( int band ) override; double classificationMin() const { return mClassificationMin; } double classificationMax() const { return mClassificationMax; } From 17111a848f9dab5fd39dd6c16a05a716f2c27d94 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 14 Mar 2024 12:50:55 +1000 Subject: [PATCH 13/68] Don't crash when calling QgsHillshadeRenderer::setBand if input is not available --- src/core/raster/qgshillshaderenderer.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/core/raster/qgshillshaderenderer.cpp b/src/core/raster/qgshillshaderenderer.cpp index 45bbe41212be..8cad04adadcf 100644 --- a/src/core/raster/qgshillshaderenderer.cpp +++ b/src/core/raster/qgshillshaderenderer.cpp @@ -576,12 +576,17 @@ void QgsHillshadeRenderer::setBand( int bandNo ) bool QgsHillshadeRenderer::setInputBand( int band ) { - if ( band > mInput->bandCount() || band <= 0 ) + if ( !mInput ) + { + mBand = band; + return true; + } + else if ( band > 0 && band <= mInput->bandCount() ) { - return false; + mBand = band; + return true; } - mBand = band; - return true; + return false; } void QgsHillshadeRenderer::toSld( QDomDocument &doc, QDomElement &element, const QVariantMap &props ) const From e297f54871ade939466a53e169093d865744740f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 14 Mar 2024 12:52:59 +1000 Subject: [PATCH 14/68] Add some see links --- .../core/auto_generated/raster/qgsrasterrenderer.sip.in | 7 ++++++- .../core/auto_generated/raster/qgsrasterrenderer.sip.in | 7 ++++++- src/core/raster/qgsrasterrenderer.h | 8 +++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterrenderer.sip.in index 91c1201b6967..713231f521a1 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterrenderer.sip.in @@ -77,6 +77,9 @@ Returns ``True`` if the band was successfully set, or ``False`` if the band coul Not all renderers support setting the input band. +.. seealso:: :py:func:`usesBands` + + .. versionadded:: 3.38 %End @@ -169,7 +172,9 @@ Useful when cloning renderers. virtual QList usesBands() const; %Docstring -Returns a list of band numbers used by the renderer +Returns a list of band numbers used by the renderer. + +.. seealso:: :py:func:`setInputBand` %End const QgsRasterMinMaxOrigin &minMaxOrigin() const; diff --git a/python/core/auto_generated/raster/qgsrasterrenderer.sip.in b/python/core/auto_generated/raster/qgsrasterrenderer.sip.in index 91c1201b6967..713231f521a1 100644 --- a/python/core/auto_generated/raster/qgsrasterrenderer.sip.in +++ b/python/core/auto_generated/raster/qgsrasterrenderer.sip.in @@ -77,6 +77,9 @@ Returns ``True`` if the band was successfully set, or ``False`` if the band coul Not all renderers support setting the input band. +.. seealso:: :py:func:`usesBands` + + .. versionadded:: 3.38 %End @@ -169,7 +172,9 @@ Useful when cloning renderers. virtual QList usesBands() const; %Docstring -Returns a list of band numbers used by the renderer +Returns a list of band numbers used by the renderer. + +.. seealso:: :py:func:`setInputBand` %End const QgsRasterMinMaxOrigin &minMaxOrigin() const; diff --git a/src/core/raster/qgsrasterrenderer.h b/src/core/raster/qgsrasterrenderer.h index 3cc9a9e880bb..c04b0f680973 100644 --- a/src/core/raster/qgsrasterrenderer.h +++ b/src/core/raster/qgsrasterrenderer.h @@ -92,6 +92,8 @@ class CORE_EXPORT QgsRasterRenderer : public QgsRasterInterface * * \note Not all renderers support setting the input band. * + * \see usesBands() + * * \since QGIS 3.38 */ virtual bool setInputBand( int band ); @@ -175,7 +177,11 @@ class CORE_EXPORT QgsRasterRenderer : public QgsRasterInterface */ void copyCommonProperties( const QgsRasterRenderer *other, bool copyMinMaxOrigin = true ); - //! Returns a list of band numbers used by the renderer + /** + * Returns a list of band numbers used by the renderer. + * + * \see setInputBand() + */ virtual QList usesBands() const { return QList(); } //! Returns const reference to origin of min/max values From 5f4218ad64f435527cbef2a71ca7588a9d5b5149 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Mar 2024 09:37:31 +1000 Subject: [PATCH 15/68] Always check input band --- src/core/raster/qgspalettedrasterrenderer.cpp | 13 +++++++++++-- src/core/raster/qgsrastercontourrenderer.cpp | 13 +++++++++++-- src/core/raster/qgssinglebandcolordatarenderer.cpp | 13 +++++++++++-- src/core/raster/qgssinglebandgrayrenderer.cpp | 13 +++++++++++-- .../raster/qgssinglebandpseudocolorrenderer.cpp | 2 +- 5 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/core/raster/qgspalettedrasterrenderer.cpp b/src/core/raster/qgspalettedrasterrenderer.cpp index 2dac40ef63bf..636ae664a6bc 100644 --- a/src/core/raster/qgspalettedrasterrenderer.cpp +++ b/src/core/raster/qgspalettedrasterrenderer.cpp @@ -177,8 +177,17 @@ void QgsPalettedRasterRenderer::setLabel( double idx, const QString &label ) bool QgsPalettedRasterRenderer::setInputBand( int band ) { - mBand = band; - return true; + if ( !mInput ) + { + mBand = band; + return true; + } + else if ( band > 0 && band <= mInput->bandCount() ) + { + mBand = band; + return true; + } + return false; } QgsRasterBlock *QgsPalettedRasterRenderer::block( int, QgsRectangle const &extent, int width, int height, QgsRasterBlockFeedback *feedback ) diff --git a/src/core/raster/qgsrastercontourrenderer.cpp b/src/core/raster/qgsrastercontourrenderer.cpp index 08de0ddf5cbe..ba5bf701f217 100644 --- a/src/core/raster/qgsrastercontourrenderer.cpp +++ b/src/core/raster/qgsrastercontourrenderer.cpp @@ -234,8 +234,17 @@ QList QgsRasterContourRenderer::createLegendNodes bool QgsRasterContourRenderer::setInputBand( int band ) { - mInputBand = band; - return true; + if ( !mInput ) + { + mInputBand = band; + return true; + } + else if ( band > 0 && band <= mInput->bandCount() ) + { + mInputBand = band; + return true; + } + return false; } void QgsRasterContourRenderer::setContourSymbol( QgsLineSymbol *symbol ) diff --git a/src/core/raster/qgssinglebandcolordatarenderer.cpp b/src/core/raster/qgssinglebandcolordatarenderer.cpp index 7bd001729d8e..2e27bb195ea3 100644 --- a/src/core/raster/qgssinglebandcolordatarenderer.cpp +++ b/src/core/raster/qgssinglebandcolordatarenderer.cpp @@ -143,6 +143,15 @@ bool QgsSingleBandColorDataRenderer::setInput( QgsRasterInterface *input ) bool QgsSingleBandColorDataRenderer::setInputBand( int band ) { - mBand = band; - return true; + if ( !mInput ) + { + mBand = band; + return true; + } + else if ( band > 0 && band <= mInput->bandCount() ) + { + mBand = band; + return true; + } + return false; } diff --git a/src/core/raster/qgssinglebandgrayrenderer.cpp b/src/core/raster/qgssinglebandgrayrenderer.cpp index 13431033893b..a14dc22afcad 100644 --- a/src/core/raster/qgssinglebandgrayrenderer.cpp +++ b/src/core/raster/qgssinglebandgrayrenderer.cpp @@ -200,8 +200,17 @@ void QgsSingleBandGrayRenderer::setGrayBand( int band ) bool QgsSingleBandGrayRenderer::setInputBand( int band ) { - mGrayBand = band; - return true; + if ( !mInput ) + { + mGrayBand = band; + return true; + } + else if ( band > 0 && band <= mInput->bandCount() ) + { + mGrayBand = band; + return true; + } + return false; } void QgsSingleBandGrayRenderer::writeXml( QDomDocument &doc, QDomElement &parentElem ) const diff --git a/src/core/raster/qgssinglebandpseudocolorrenderer.cpp b/src/core/raster/qgssinglebandpseudocolorrenderer.cpp index f85bd3e93e47..7b5002842675 100644 --- a/src/core/raster/qgssinglebandpseudocolorrenderer.cpp +++ b/src/core/raster/qgssinglebandpseudocolorrenderer.cpp @@ -48,7 +48,7 @@ bool QgsSingleBandPseudoColorRenderer::setInputBand( int band ) mBand = band; return true; } - else if ( band <= mInput->bandCount() || band > 0 ) + else if ( band > 0 && band <= mInput->bandCount() ) { mBand = band; return true; From 2556a6674da8bea7a71651c5095d2434152ad2d9 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Mar 2024 09:43:16 +1000 Subject: [PATCH 16/68] Also add virtual inputBand getter --- .../raster/qgshillshaderenderer.sip.in | 2 ++ .../raster/qgspalettedrasterrenderer.sip.in | 2 ++ .../raster/qgsrastercontourrenderer.sip.in | 6 +----- .../raster/qgsrasterrenderer.sip.in | 17 +++++++++++++++++ .../qgssinglebandcolordatarenderer.sip.in | 2 ++ .../raster/qgssinglebandgrayrenderer.sip.in | 2 ++ .../qgssinglebandpseudocolorrenderer.sip.in | 2 ++ .../raster/qgshillshaderenderer.sip.in | 2 ++ .../raster/qgspalettedrasterrenderer.sip.in | 2 ++ .../raster/qgsrastercontourrenderer.sip.in | 6 +----- .../raster/qgsrasterrenderer.sip.in | 17 +++++++++++++++++ .../qgssinglebandcolordatarenderer.sip.in | 2 ++ .../raster/qgssinglebandgrayrenderer.sip.in | 2 ++ .../qgssinglebandpseudocolorrenderer.sip.in | 2 ++ src/core/raster/qgshillshaderenderer.cpp | 5 +++++ src/core/raster/qgshillshaderenderer.h | 1 + src/core/raster/qgspalettedrasterrenderer.cpp | 5 +++++ src/core/raster/qgspalettedrasterrenderer.h | 1 + src/core/raster/qgsrastercontourrenderer.cpp | 5 +++++ src/core/raster/qgsrastercontourrenderer.h | 6 +----- src/core/raster/qgsrasterrenderer.cpp | 5 +++++ src/core/raster/qgsrasterrenderer.h | 15 +++++++++++++++ .../raster/qgssinglebandcolordatarenderer.cpp | 5 +++++ .../raster/qgssinglebandcolordatarenderer.h | 1 + src/core/raster/qgssinglebandgrayrenderer.cpp | 5 +++++ src/core/raster/qgssinglebandgrayrenderer.h | 1 + .../raster/qgssinglebandpseudocolorrenderer.cpp | 5 +++++ .../raster/qgssinglebandpseudocolorrenderer.h | 1 + 28 files changed, 112 insertions(+), 15 deletions(-) diff --git a/python/PyQt6/core/auto_generated/raster/qgshillshaderenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgshillshaderenderer.sip.in index 27cda9e7897e..36a6c092ef0a 100644 --- a/python/PyQt6/core/auto_generated/raster/qgshillshaderenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgshillshaderenderer.sip.in @@ -56,6 +56,8 @@ Factory method to create a new renderer virtual QList usesBands() const; + virtual int inputBand() const; + virtual void toSld( QDomDocument &doc, QDomElement &element, const QVariantMap &props = QVariantMap() ) const; diff --git a/python/PyQt6/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in index 31201922779f..e7ea1c214aff 100644 --- a/python/PyQt6/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in @@ -127,6 +127,8 @@ Set category label Returns the raster band used for rendering the raster. %End + virtual int inputBand() const; + virtual bool setInputBand( int band ); diff --git a/python/PyQt6/core/auto_generated/raster/qgsrastercontourrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrastercontourrenderer.sip.in index c7b2ec5d393b..55f8394fed14 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrastercontourrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrastercontourrenderer.sip.in @@ -50,12 +50,8 @@ Creates an instance of the renderer based on definition from XML (used by render virtual QList createLegendNodes( QgsLayerTreeLayer *nodeLayer ) /Factory/; + virtual int inputBand() const; - - int inputBand() const; -%Docstring -Returns the number of the input raster band -%End virtual bool setInputBand( int band ); diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterrenderer.sip.in index 713231f521a1..200e1c2e86dd 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterrenderer.sip.in @@ -67,6 +67,21 @@ The default implementation returns ``False``. virtual bool setInput( QgsRasterInterface *input ); + virtual int inputBand() const; +%Docstring +Returns the input band for the renderer, or -1 if no input band is available. + +For renderers which utilize multiple input bands -1 will be returned. In these +cases :py:func:`~QgsRasterRenderer.usesBands` will return a list of all utilized bands (including alpha +bands). + +.. seealso:: :py:func:`setInputBand` + +.. seealso:: :py:func:`usesBands` + +.. versionadded:: 3.38 +%End + virtual bool setInputBand( int band ); %Docstring Attempts to set the input ``band`` for the renderer. @@ -77,6 +92,8 @@ Returns ``True`` if the band was successfully set, or ``False`` if the band coul Not all renderers support setting the input band. +.. seealso:: :py:func:`inputBand` + .. seealso:: :py:func:`usesBands` diff --git a/python/PyQt6/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in index 2238df13e45a..1ac7bc0e53ba 100644 --- a/python/PyQt6/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in @@ -35,6 +35,8 @@ QgsSingleBandColorDataRenderer cannot be copied. Use :py:func:`~QgsSingleBandCol virtual bool setInput( QgsRasterInterface *input ); + virtual int inputBand() const; + virtual bool setInputBand( int band ); diff --git a/python/PyQt6/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in index 5785b7c4186f..18b1df3a1ced 100644 --- a/python/PyQt6/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in @@ -45,6 +45,8 @@ QgsSingleBandGrayRenderer cannot be copied. Use :py:func:`~QgsSingleBandGrayRend int grayBand() const; void setGrayBand( int band ); + virtual int inputBand() const; + virtual bool setInputBand( int band ); diff --git a/python/PyQt6/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in index ebd39d0da3f9..4b058e27ec2a 100644 --- a/python/PyQt6/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in @@ -101,6 +101,8 @@ Sets the band used by the renderer. .. seealso:: :py:func:`band` %End + virtual int inputBand() const; + virtual bool setInputBand( int band ); diff --git a/python/core/auto_generated/raster/qgshillshaderenderer.sip.in b/python/core/auto_generated/raster/qgshillshaderenderer.sip.in index 27cda9e7897e..36a6c092ef0a 100644 --- a/python/core/auto_generated/raster/qgshillshaderenderer.sip.in +++ b/python/core/auto_generated/raster/qgshillshaderenderer.sip.in @@ -56,6 +56,8 @@ Factory method to create a new renderer virtual QList usesBands() const; + virtual int inputBand() const; + virtual void toSld( QDomDocument &doc, QDomElement &element, const QVariantMap &props = QVariantMap() ) const; diff --git a/python/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in b/python/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in index 31201922779f..e7ea1c214aff 100644 --- a/python/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in +++ b/python/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in @@ -127,6 +127,8 @@ Set category label Returns the raster band used for rendering the raster. %End + virtual int inputBand() const; + virtual bool setInputBand( int band ); diff --git a/python/core/auto_generated/raster/qgsrastercontourrenderer.sip.in b/python/core/auto_generated/raster/qgsrastercontourrenderer.sip.in index c7b2ec5d393b..55f8394fed14 100644 --- a/python/core/auto_generated/raster/qgsrastercontourrenderer.sip.in +++ b/python/core/auto_generated/raster/qgsrastercontourrenderer.sip.in @@ -50,12 +50,8 @@ Creates an instance of the renderer based on definition from XML (used by render virtual QList createLegendNodes( QgsLayerTreeLayer *nodeLayer ) /Factory/; + virtual int inputBand() const; - - int inputBand() const; -%Docstring -Returns the number of the input raster band -%End virtual bool setInputBand( int band ); diff --git a/python/core/auto_generated/raster/qgsrasterrenderer.sip.in b/python/core/auto_generated/raster/qgsrasterrenderer.sip.in index 713231f521a1..200e1c2e86dd 100644 --- a/python/core/auto_generated/raster/qgsrasterrenderer.sip.in +++ b/python/core/auto_generated/raster/qgsrasterrenderer.sip.in @@ -67,6 +67,21 @@ The default implementation returns ``False``. virtual bool setInput( QgsRasterInterface *input ); + virtual int inputBand() const; +%Docstring +Returns the input band for the renderer, or -1 if no input band is available. + +For renderers which utilize multiple input bands -1 will be returned. In these +cases :py:func:`~QgsRasterRenderer.usesBands` will return a list of all utilized bands (including alpha +bands). + +.. seealso:: :py:func:`setInputBand` + +.. seealso:: :py:func:`usesBands` + +.. versionadded:: 3.38 +%End + virtual bool setInputBand( int band ); %Docstring Attempts to set the input ``band`` for the renderer. @@ -77,6 +92,8 @@ Returns ``True`` if the band was successfully set, or ``False`` if the band coul Not all renderers support setting the input band. +.. seealso:: :py:func:`inputBand` + .. seealso:: :py:func:`usesBands` diff --git a/python/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in b/python/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in index 2238df13e45a..1ac7bc0e53ba 100644 --- a/python/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in +++ b/python/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in @@ -35,6 +35,8 @@ QgsSingleBandColorDataRenderer cannot be copied. Use :py:func:`~QgsSingleBandCol virtual bool setInput( QgsRasterInterface *input ); + virtual int inputBand() const; + virtual bool setInputBand( int band ); diff --git a/python/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in b/python/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in index f58d8a2763f3..3b85e4a82d1a 100644 --- a/python/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in +++ b/python/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in @@ -45,6 +45,8 @@ QgsSingleBandGrayRenderer cannot be copied. Use :py:func:`~QgsSingleBandGrayRend int grayBand() const; void setGrayBand( int band ); + virtual int inputBand() const; + virtual bool setInputBand( int band ); diff --git a/python/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in b/python/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in index ebd39d0da3f9..4b058e27ec2a 100644 --- a/python/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in +++ b/python/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in @@ -101,6 +101,8 @@ Sets the band used by the renderer. .. seealso:: :py:func:`band` %End + virtual int inputBand() const; + virtual bool setInputBand( int band ); diff --git a/src/core/raster/qgshillshaderenderer.cpp b/src/core/raster/qgshillshaderenderer.cpp index 8cad04adadcf..66ce0e9ec496 100644 --- a/src/core/raster/qgshillshaderenderer.cpp +++ b/src/core/raster/qgshillshaderenderer.cpp @@ -569,6 +569,11 @@ QList QgsHillshadeRenderer::usesBands() const } +int QgsHillshadeRenderer::inputBand() const +{ + return mBand; +} + void QgsHillshadeRenderer::setBand( int bandNo ) { setInputBand( bandNo ); diff --git a/src/core/raster/qgshillshaderenderer.h b/src/core/raster/qgshillshaderenderer.h index bb11335e9364..1de4a8d737d5 100644 --- a/src/core/raster/qgshillshaderenderer.h +++ b/src/core/raster/qgshillshaderenderer.h @@ -61,6 +61,7 @@ class CORE_EXPORT QgsHillshadeRenderer : public QgsRasterRenderer QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = nullptr ) override SIP_FACTORY; QList usesBands() const override; + int inputBand() const override; void toSld( QDomDocument &doc, QDomElement &element, const QVariantMap &props = QVariantMap() ) const override; diff --git a/src/core/raster/qgspalettedrasterrenderer.cpp b/src/core/raster/qgspalettedrasterrenderer.cpp index 636ae664a6bc..a7d0299b9fec 100644 --- a/src/core/raster/qgspalettedrasterrenderer.cpp +++ b/src/core/raster/qgspalettedrasterrenderer.cpp @@ -175,6 +175,11 @@ void QgsPalettedRasterRenderer::setLabel( double idx, const QString &label ) } } +int QgsPalettedRasterRenderer::inputBand() const +{ + return mBand; +} + bool QgsPalettedRasterRenderer::setInputBand( int band ) { if ( !mInput ) diff --git a/src/core/raster/qgspalettedrasterrenderer.h b/src/core/raster/qgspalettedrasterrenderer.h index 9b209e0d9a90..31920ebb6a4c 100644 --- a/src/core/raster/qgspalettedrasterrenderer.h +++ b/src/core/raster/qgspalettedrasterrenderer.h @@ -146,6 +146,7 @@ class CORE_EXPORT QgsPalettedRasterRenderer: public QgsRasterRenderer */ int band() const { return mBand; } + int inputBand() const override; bool setInputBand( int band ) override; void writeXml( QDomDocument &doc, QDomElement &parentElem ) const override; diff --git a/src/core/raster/qgsrastercontourrenderer.cpp b/src/core/raster/qgsrastercontourrenderer.cpp index ba5bf701f217..0d5e57febb1a 100644 --- a/src/core/raster/qgsrastercontourrenderer.cpp +++ b/src/core/raster/qgsrastercontourrenderer.cpp @@ -232,6 +232,11 @@ QList QgsRasterContourRenderer::createLegendNodes return nodes; } +int QgsRasterContourRenderer::inputBand() const +{ + return mInputBand; +} + bool QgsRasterContourRenderer::setInputBand( int band ) { if ( !mInput ) diff --git a/src/core/raster/qgsrastercontourrenderer.h b/src/core/raster/qgsrastercontourrenderer.h index 950541328676..1ef9955110c7 100644 --- a/src/core/raster/qgsrastercontourrenderer.h +++ b/src/core/raster/qgsrastercontourrenderer.h @@ -50,11 +50,7 @@ class CORE_EXPORT QgsRasterContourRenderer : public QgsRasterRenderer QList usesBands() const override; QList createLegendNodes( QgsLayerTreeLayer *nodeLayer ) SIP_FACTORY override; - - // - - //! Returns the number of the input raster band - int inputBand() const { return mInputBand; } + int inputBand() const override; bool setInputBand( int band ) override; //! Returns the interval of contour lines generation diff --git a/src/core/raster/qgsrasterrenderer.cpp b/src/core/raster/qgsrasterrenderer.cpp index f70297264016..799f0930f5d3 100644 --- a/src/core/raster/qgsrasterrenderer.cpp +++ b/src/core/raster/qgsrasterrenderer.cpp @@ -97,6 +97,11 @@ bool QgsRasterRenderer::setInput( QgsRasterInterface *input ) return true; } +int QgsRasterRenderer::inputBand() const +{ + return -1; +} + bool QgsRasterRenderer::setInputBand( int ) { return false; diff --git a/src/core/raster/qgsrasterrenderer.h b/src/core/raster/qgsrasterrenderer.h index c04b0f680973..28fdb77c31ca 100644 --- a/src/core/raster/qgsrasterrenderer.h +++ b/src/core/raster/qgsrasterrenderer.h @@ -85,6 +85,20 @@ class CORE_EXPORT QgsRasterRenderer : public QgsRasterInterface bool setInput( QgsRasterInterface *input ) override; + /** + * Returns the input band for the renderer, or -1 if no input band is available. + * + * For renderers which utilize multiple input bands -1 will be returned. In these + * cases usesBands() will return a list of all utilized bands (including alpha + * bands). + * + * \see setInputBand() + * \see usesBands() + * + * \since QGIS 3.38 + */ + virtual int inputBand() const; + /** * Attempts to set the input \a band for the renderer. * @@ -92,6 +106,7 @@ class CORE_EXPORT QgsRasterRenderer : public QgsRasterInterface * * \note Not all renderers support setting the input band. * + * \see inputBand() * \see usesBands() * * \since QGIS 3.38 diff --git a/src/core/raster/qgssinglebandcolordatarenderer.cpp b/src/core/raster/qgssinglebandcolordatarenderer.cpp index 2e27bb195ea3..e9ed71cfa46a 100644 --- a/src/core/raster/qgssinglebandcolordatarenderer.cpp +++ b/src/core/raster/qgssinglebandcolordatarenderer.cpp @@ -141,6 +141,11 @@ bool QgsSingleBandColorDataRenderer::setInput( QgsRasterInterface *input ) return false; } +int QgsSingleBandColorDataRenderer::inputBand() const +{ + return mBand; +} + bool QgsSingleBandColorDataRenderer::setInputBand( int band ) { if ( !mInput ) diff --git a/src/core/raster/qgssinglebandcolordatarenderer.h b/src/core/raster/qgssinglebandcolordatarenderer.h index 7f2cf88e9e96..aaf0694bda44 100644 --- a/src/core/raster/qgssinglebandcolordatarenderer.h +++ b/src/core/raster/qgssinglebandcolordatarenderer.h @@ -44,6 +44,7 @@ class CORE_EXPORT QgsSingleBandColorDataRenderer: public QgsRasterRenderer static QgsRasterRenderer *create( const QDomElement &elem, QgsRasterInterface *input ) SIP_FACTORY; bool setInput( QgsRasterInterface *input ) override; + int inputBand() const override; bool setInputBand( int band ) override; QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = nullptr ) override SIP_FACTORY; diff --git a/src/core/raster/qgssinglebandgrayrenderer.cpp b/src/core/raster/qgssinglebandgrayrenderer.cpp index a14dc22afcad..d8302958040a 100644 --- a/src/core/raster/qgssinglebandgrayrenderer.cpp +++ b/src/core/raster/qgssinglebandgrayrenderer.cpp @@ -198,6 +198,11 @@ void QgsSingleBandGrayRenderer::setGrayBand( int band ) setInputBand( band ); } +int QgsSingleBandGrayRenderer::inputBand() const +{ + return mGrayBand; +} + bool QgsSingleBandGrayRenderer::setInputBand( int band ) { if ( !mInput ) diff --git a/src/core/raster/qgssinglebandgrayrenderer.h b/src/core/raster/qgssinglebandgrayrenderer.h index 1c4a600689c3..771ab0d92b73 100644 --- a/src/core/raster/qgssinglebandgrayrenderer.h +++ b/src/core/raster/qgssinglebandgrayrenderer.h @@ -57,6 +57,7 @@ class CORE_EXPORT QgsSingleBandGrayRenderer: public QgsRasterRenderer int grayBand() const { return mGrayBand; } void setGrayBand( int band ); + int inputBand() const override; bool setInputBand( int band ) override; const QgsContrastEnhancement *contrastEnhancement() const { return mContrastEnhancement.get(); } diff --git a/src/core/raster/qgssinglebandpseudocolorrenderer.cpp b/src/core/raster/qgssinglebandpseudocolorrenderer.cpp index 7b5002842675..8ac52dbec147 100644 --- a/src/core/raster/qgssinglebandpseudocolorrenderer.cpp +++ b/src/core/raster/qgssinglebandpseudocolorrenderer.cpp @@ -41,6 +41,11 @@ void QgsSingleBandPseudoColorRenderer::setBand( int bandNo ) setInputBand( bandNo ); } +int QgsSingleBandPseudoColorRenderer::inputBand() const +{ + return mBand; +} + bool QgsSingleBandPseudoColorRenderer::setInputBand( int band ) { if ( !mInput ) diff --git a/src/core/raster/qgssinglebandpseudocolorrenderer.h b/src/core/raster/qgssinglebandpseudocolorrenderer.h index 333ffffef90f..c593508a0470 100644 --- a/src/core/raster/qgssinglebandpseudocolorrenderer.h +++ b/src/core/raster/qgssinglebandpseudocolorrenderer.h @@ -95,6 +95,7 @@ class CORE_EXPORT QgsSingleBandPseudoColorRenderer: public QgsRasterRenderer * \see band */ void setBand( int bandNo ); + int inputBand() const override; bool setInputBand( int band ) override; double classificationMin() const { return mClassificationMin; } From 4a0bc4ae99ca2ed38b7024577fa5ed487f77f1b0 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Mar 2024 09:54:41 +1000 Subject: [PATCH 17/68] Deprecated subclass methods --- .../raster/qgshillshaderenderer.sip.in | 10 ++++++++-- .../raster/qgspalettedrasterrenderer.sip.in | 5 ++++- .../raster/qgssinglebandgrayrenderer.sip.in | 16 ++++++++++++++-- .../qgssinglebandpseudocolorrenderer.sip.in | 11 +++++++++-- .../raster/qgshillshaderenderer.sip.in | 10 ++++++++-- .../raster/qgspalettedrasterrenderer.sip.in | 5 ++++- .../raster/qgssinglebandgrayrenderer.sip.in | 16 ++++++++++++++-- .../qgssinglebandpseudocolorrenderer.sip.in | 11 +++++++++-- src/core/raster/qgshillshaderenderer.cpp | 4 ++-- src/core/raster/qgshillshaderenderer.h | 8 ++++++-- src/core/raster/qgspalettedrasterrenderer.cpp | 3 +-- src/core/raster/qgspalettedrasterrenderer.h | 4 +++- src/core/raster/qgsrasterattributetable.cpp | 4 ++-- src/core/raster/qgsrasterlayer.cpp | 12 ++++++------ src/core/raster/qgssinglebandgrayrenderer.cpp | 5 ++--- src/core/raster/qgssinglebandgrayrenderer.h | 12 ++++++++++-- .../raster/qgssinglebandpseudocolorrenderer.cpp | 6 +++--- .../raster/qgssinglebandpseudocolorrenderer.h | 9 +++++++-- src/gui/raster/qgshillshaderendererwidget.cpp | 2 +- src/gui/raster/qgspalettedrendererwidget.cpp | 2 +- .../raster/qgssinglebandgrayrendererwidget.cpp | 4 ++-- .../qgssinglebandpseudocolorrendererwidget.cpp | 6 +++--- 22 files changed, 119 insertions(+), 46 deletions(-) diff --git a/python/PyQt6/core/auto_generated/raster/qgshillshaderenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgshillshaderenderer.sip.in index 36a6c092ef0a..503741aa9925 100644 --- a/python/PyQt6/core/auto_generated/raster/qgshillshaderenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgshillshaderenderer.sip.in @@ -62,16 +62,22 @@ Factory method to create a new renderer virtual void toSld( QDomDocument &doc, QDomElement &element, const QVariantMap &props = QVariantMap() ) const; - int band() const; + int band() const /Deprecated/; %Docstring Returns the band used by the renderer + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsHillshadeRenderer.inputBand` instead %End - void setBand( int bandNo ); + void setBand( int bandNo ) /Deprecated/; %Docstring Sets the band used by the renderer. .. seealso:: :py:func:`band` + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsHillshadeRenderer.setInputBand` instead %End virtual bool setInputBand( int band ); diff --git a/python/PyQt6/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in index e7ea1c214aff..fabf4a25c007 100644 --- a/python/PyQt6/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in @@ -122,9 +122,12 @@ Returns optional category label Set category label %End - int band() const; + int band() const /Deprecated/; %Docstring Returns the raster band used for rendering the raster. + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsPalettedRasterRenderer.inputBand` instead %End virtual int inputBand() const; diff --git a/python/PyQt6/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in index 18b1df3a1ced..4a332ab54071 100644 --- a/python/PyQt6/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in @@ -43,8 +43,20 @@ QgsSingleBandGrayRenderer cannot be copied. Use :py:func:`~QgsSingleBandGrayRend virtual QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = 0 ) /Factory/; - int grayBand() const; - void setGrayBand( int band ); + int grayBand() const /Deprecated/; +%Docstring + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsSingleBandGrayRenderer.inputBand` instead +%End + + void setGrayBand( int band ) /Deprecated/; +%Docstring + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsSingleBandGrayRenderer.setInputBand` instead +%End + virtual int inputBand() const; virtual bool setInputBand( int band ); diff --git a/python/PyQt6/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in index 4b058e27ec2a..1c756431eb43 100644 --- a/python/PyQt6/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in @@ -90,17 +90,24 @@ Creates a color ramp shader virtual bool accept( QgsStyleEntityVisitorInterface *visitor ) const; - int band() const; + int band() const /Deprecated/; %Docstring Returns the band used by the renderer + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsSingleBandPseudoColorRenderer.inputBand` instead %End - void setBand( int bandNo ); + void setBand( int bandNo ) /Deprecated/; %Docstring Sets the band used by the renderer. .. seealso:: :py:func:`band` + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsSingleBandPseudoColorRenderer.setInputBand` instead %End + virtual int inputBand() const; virtual bool setInputBand( int band ); diff --git a/python/core/auto_generated/raster/qgshillshaderenderer.sip.in b/python/core/auto_generated/raster/qgshillshaderenderer.sip.in index 36a6c092ef0a..503741aa9925 100644 --- a/python/core/auto_generated/raster/qgshillshaderenderer.sip.in +++ b/python/core/auto_generated/raster/qgshillshaderenderer.sip.in @@ -62,16 +62,22 @@ Factory method to create a new renderer virtual void toSld( QDomDocument &doc, QDomElement &element, const QVariantMap &props = QVariantMap() ) const; - int band() const; + int band() const /Deprecated/; %Docstring Returns the band used by the renderer + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsHillshadeRenderer.inputBand` instead %End - void setBand( int bandNo ); + void setBand( int bandNo ) /Deprecated/; %Docstring Sets the band used by the renderer. .. seealso:: :py:func:`band` + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsHillshadeRenderer.setInputBand` instead %End virtual bool setInputBand( int band ); diff --git a/python/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in b/python/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in index e7ea1c214aff..fabf4a25c007 100644 --- a/python/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in +++ b/python/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in @@ -122,9 +122,12 @@ Returns optional category label Set category label %End - int band() const; + int band() const /Deprecated/; %Docstring Returns the raster band used for rendering the raster. + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsPalettedRasterRenderer.inputBand` instead %End virtual int inputBand() const; diff --git a/python/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in b/python/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in index 3b85e4a82d1a..08dd967c5a6f 100644 --- a/python/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in +++ b/python/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in @@ -43,8 +43,20 @@ QgsSingleBandGrayRenderer cannot be copied. Use :py:func:`~QgsSingleBandGrayRend virtual QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = 0 ) /Factory/; - int grayBand() const; - void setGrayBand( int band ); + int grayBand() const /Deprecated/; +%Docstring + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsSingleBandGrayRenderer.inputBand` instead +%End + + void setGrayBand( int band ) /Deprecated/; +%Docstring + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsSingleBandGrayRenderer.setInputBand` instead +%End + virtual int inputBand() const; virtual bool setInputBand( int band ); diff --git a/python/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in b/python/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in index 4b058e27ec2a..1c756431eb43 100644 --- a/python/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in +++ b/python/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in @@ -90,17 +90,24 @@ Creates a color ramp shader virtual bool accept( QgsStyleEntityVisitorInterface *visitor ) const; - int band() const; + int band() const /Deprecated/; %Docstring Returns the band used by the renderer + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsSingleBandPseudoColorRenderer.inputBand` instead %End - void setBand( int bandNo ); + void setBand( int bandNo ) /Deprecated/; %Docstring Sets the band used by the renderer. .. seealso:: :py:func:`band` + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsSingleBandPseudoColorRenderer.setInputBand` instead %End + virtual int inputBand() const; virtual bool setInputBand( int band ); diff --git a/src/core/raster/qgshillshaderenderer.cpp b/src/core/raster/qgshillshaderenderer.cpp index 66ce0e9ec496..6c61a29e1054 100644 --- a/src/core/raster/qgshillshaderenderer.cpp +++ b/src/core/raster/qgshillshaderenderer.cpp @@ -610,7 +610,7 @@ void QgsHillshadeRenderer::toSld( QDomDocument &doc, QDomElement &element, const // add Channel Selection tags (if band is not default 1) // Need to insert channelSelection in the correct sequence as in SLD standard e.g. // after opacity or geometry or as first element after sld:RasterSymbolizer - if ( band() != 1 ) + if ( mBand != 1 ) { QDomElement channelSelectionElem = doc.createElement( QStringLiteral( "sld:ChannelSelection" ) ); elements = rasterSymbolizerElem.elementsByTagName( QStringLiteral( "sld:Opacity" ) ); @@ -637,7 +637,7 @@ void QgsHillshadeRenderer::toSld( QDomDocument &doc, QDomElement &element, const // set band QDomElement sourceChannelNameElem = doc.createElement( QStringLiteral( "sld:SourceChannelName" ) ); - sourceChannelNameElem.appendChild( doc.createTextNode( QString::number( band() ) ) ); + sourceChannelNameElem.appendChild( doc.createTextNode( QString::number( mBand ) ) ); channelElem.appendChild( sourceChannelNameElem ); } diff --git a/src/core/raster/qgshillshaderenderer.h b/src/core/raster/qgshillshaderenderer.h index 1de4a8d737d5..22a51e39789e 100644 --- a/src/core/raster/qgshillshaderenderer.h +++ b/src/core/raster/qgshillshaderenderer.h @@ -67,14 +67,18 @@ class CORE_EXPORT QgsHillshadeRenderer : public QgsRasterRenderer /** * Returns the band used by the renderer + * + * \deprecated since QGIS 3.38 use inputBand() instead */ - int band() const { return mBand; } + Q_DECL_DEPRECATED int band() const SIP_DEPRECATED { return mBand; } /** * Sets the band used by the renderer. * \see band + * + * \deprecated since QGIS 3.38 use setInputBand() instead */ - void setBand( int bandNo ); + Q_DECL_DEPRECATED void setBand( int bandNo ) SIP_DEPRECATED; bool setInputBand( int band ) override; /** diff --git a/src/core/raster/qgspalettedrasterrenderer.cpp b/src/core/raster/qgspalettedrasterrenderer.cpp index a7d0299b9fec..0b79f62570de 100644 --- a/src/core/raster/qgspalettedrasterrenderer.cpp +++ b/src/core/raster/qgspalettedrasterrenderer.cpp @@ -17,7 +17,6 @@ #include "qgspalettedrasterrenderer.h" #include "qgsrastertransparency.h" -#include "qgsrasterviewport.h" #include "qgssymbollayerutils.h" #include "qgsstyleentityvisitor.h" #include "qgsmessagelog.h" @@ -357,7 +356,7 @@ void QgsPalettedRasterRenderer::toSld( QDomDocument &doc, QDomElement &element, // set band QDomElement sourceChannelNameElem = doc.createElement( QStringLiteral( "sld:SourceChannelName" ) ); - sourceChannelNameElem.appendChild( doc.createTextNode( QString::number( band() ) ) ); + sourceChannelNameElem.appendChild( doc.createTextNode( QString::number( mBand ) ) ); channelElem.appendChild( sourceChannelNameElem ); // add ColorMap tag diff --git a/src/core/raster/qgspalettedrasterrenderer.h b/src/core/raster/qgspalettedrasterrenderer.h index 31920ebb6a4c..58f6e5c1c3a7 100644 --- a/src/core/raster/qgspalettedrasterrenderer.h +++ b/src/core/raster/qgspalettedrasterrenderer.h @@ -143,8 +143,10 @@ class CORE_EXPORT QgsPalettedRasterRenderer: public QgsRasterRenderer /** * Returns the raster band used for rendering the raster. + * + * \deprecated since QGIS 3.38 use inputBand() instead */ - int band() const { return mBand; } + Q_DECL_DEPRECATED int band() const SIP_DEPRECATED { return mBand; } int inputBand() const override; bool setInputBand( int band ) override; diff --git a/src/core/raster/qgsrasterattributetable.cpp b/src/core/raster/qgsrasterattributetable.cpp index ee45be536628..f007a45a0088 100644 --- a/src/core/raster/qgsrasterattributetable.cpp +++ b/src/core/raster/qgsrasterattributetable.cpp @@ -1078,7 +1078,7 @@ QgsRasterAttributeTable *QgsRasterAttributeTable::createFromRaster( QgsRasterLay if ( bandNumber ) { - *bandNumber = palettedRenderer->band(); + *bandNumber = palettedRenderer->inputBand(); } return rat; } @@ -1174,7 +1174,7 @@ QgsRasterAttributeTable *QgsRasterAttributeTable::createFromRaster( QgsRasterLay if ( bandNumber ) { - *bandNumber = pseudoColorRenderer->band(); + *bandNumber = pseudoColorRenderer->inputBand(); } return rat; diff --git a/src/core/raster/qgsrasterlayer.cpp b/src/core/raster/qgsrasterlayer.cpp index 526728bf4509..e1bd136dc8f8 100644 --- a/src/core/raster/qgsrasterlayer.cpp +++ b/src/core/raster/qgsrasterlayer.cpp @@ -1369,7 +1369,7 @@ void QgsRasterLayer::setContrastEnhancement( QgsContrastEnhancement::ContrastEnh { return; } - myBands << myGrayRenderer->grayBand(); + myBands << myGrayRenderer->inputBand(); myRasterRenderer = myGrayRenderer; myMinMaxOrigin = myGrayRenderer->minMaxOrigin(); } @@ -1391,7 +1391,7 @@ void QgsRasterLayer::setContrastEnhancement( QgsContrastEnhancement::ContrastEnh { return; } - myBands << myPseudoColorRenderer->band(); + myBands << myPseudoColorRenderer->inputBand(); myRasterRenderer = myPseudoColorRenderer; myMinMaxOrigin = myPseudoColorRenderer->minMaxOrigin(); } @@ -1442,7 +1442,7 @@ void QgsRasterLayer::setContrastEnhancement( QgsContrastEnhancement::ContrastEnh QgsColorRampShader *colorRampShader = dynamic_cast( myPseudoColorRenderer->shader()->rasterShaderFunction() ); if ( colorRampShader ) { - colorRampShader->classifyColorRamp( myPseudoColorRenderer->band(), extent, myPseudoColorRenderer->input() ); + colorRampShader->classifyColorRamp( myPseudoColorRenderer->inputBand(), extent, myPseudoColorRenderer->input() ); } } } @@ -1573,7 +1573,7 @@ void QgsRasterLayer::refreshRenderer( QgsRasterRenderer *rasterRenderer, const Q mLastRectangleUsedByRefreshContrastEnhancementIfNeeded = extent; double min; double max; - computeMinMax( sbpcr->band(), + computeMinMax( sbpcr->inputBand(), rasterRenderer->minMaxOrigin(), rasterRenderer->minMaxOrigin().limits(), extent, static_cast( SAMPLE_SIZE ), min, max ); @@ -1585,7 +1585,7 @@ void QgsRasterLayer::refreshRenderer( QgsRasterRenderer *rasterRenderer, const Q QgsColorRampShader *colorRampShader = dynamic_cast( sbpcr->shader()->rasterShaderFunction() ); if ( colorRampShader ) { - colorRampShader->classifyColorRamp( sbpcr->band(), extent, rasterRenderer->input() ); + colorRampShader->classifyColorRamp( sbpcr->inputBand(), extent, rasterRenderer->input() ); } } @@ -1598,7 +1598,7 @@ void QgsRasterLayer::refreshRenderer( QgsRasterRenderer *rasterRenderer, const Q QgsColorRampShader *colorRampShader = dynamic_cast( r->shader()->rasterShaderFunction() ); if ( colorRampShader ) { - colorRampShader->classifyColorRamp( sbpcr->band(), extent, rasterRenderer->input() ); + colorRampShader->classifyColorRamp( sbpcr->inputBand(), extent, rasterRenderer->input() ); } } diff --git a/src/core/raster/qgssinglebandgrayrenderer.cpp b/src/core/raster/qgssinglebandgrayrenderer.cpp index d8302958040a..35c85d9fb7fc 100644 --- a/src/core/raster/qgssinglebandgrayrenderer.cpp +++ b/src/core/raster/qgssinglebandgrayrenderer.cpp @@ -22,7 +22,6 @@ #include "qgscolorramplegendnodesettings.h" #include "qgsreadwritecontext.h" #include "qgscolorrampimpl.h" -#include "qgssymbol.h" #include #include @@ -339,7 +338,7 @@ void QgsSingleBandGrayRenderer::toSld( QDomDocument &doc, QDomElement &element, // set band QDomElement sourceChannelNameElem = doc.createElement( QStringLiteral( "sld:SourceChannelName" ) ); - sourceChannelNameElem.appendChild( doc.createTextNode( QString::number( grayBand() ) ) ); + sourceChannelNameElem.appendChild( doc.createTextNode( QString::number( mGrayBand ) ) ); channelElem.appendChild( sourceChannelNameElem ); // set ContrastEnhancement @@ -358,7 +357,7 @@ void QgsSingleBandGrayRenderer::toSld( QDomDocument &doc, QDomElement &element, case QgsContrastEnhancement::ClipToMinimumMaximum: { // with this renderer export have to be check against real min/max values of the raster - const QgsRasterBandStats myRasterBandStats = mInput->bandStatistics( grayBand(), Qgis::RasterBandStatistic::Min | Qgis::RasterBandStatistic::Max ); + const QgsRasterBandStats myRasterBandStats = mInput->bandStatistics( mGrayBand, Qgis::RasterBandStatistic::Min | Qgis::RasterBandStatistic::Max ); // if minimum range differ from the real minimum => set is in exported SLD vendor option if ( !qgsDoubleNear( lContrastEnhancement->minimumValue(), myRasterBandStats.minimumValue ) ) diff --git a/src/core/raster/qgssinglebandgrayrenderer.h b/src/core/raster/qgssinglebandgrayrenderer.h index 771ab0d92b73..60a92bf46a95 100644 --- a/src/core/raster/qgssinglebandgrayrenderer.h +++ b/src/core/raster/qgssinglebandgrayrenderer.h @@ -55,8 +55,16 @@ class CORE_EXPORT QgsSingleBandGrayRenderer: public QgsRasterRenderer QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = nullptr ) override SIP_FACTORY; - int grayBand() const { return mGrayBand; } - void setGrayBand( int band ); + /** + * \deprecated since QGIS 3.38 use inputBand() instead + */ + Q_DECL_DEPRECATED int grayBand() const SIP_DEPRECATED { return mGrayBand; } + + /** + * \deprecated since QGIS 3.38 use setInputBand() instead + */ + Q_DECL_DEPRECATED void setGrayBand( int band ) SIP_DEPRECATED; + int inputBand() const override; bool setInputBand( int band ) override; diff --git a/src/core/raster/qgssinglebandpseudocolorrenderer.cpp b/src/core/raster/qgssinglebandpseudocolorrenderer.cpp index 8ac52dbec147..bbccbd2ff325 100644 --- a/src/core/raster/qgssinglebandpseudocolorrenderer.cpp +++ b/src/core/raster/qgssinglebandpseudocolorrenderer.cpp @@ -122,13 +122,13 @@ void QgsSingleBandPseudoColorRenderer::setShader( QgsRasterShader *shader ) void QgsSingleBandPseudoColorRenderer::createShader( QgsColorRamp *colorRamp, QgsColorRampShader::Type colorRampType, QgsColorRampShader::ClassificationMode classificationMode, int classes, bool clip, const QgsRectangle &extent ) { - if ( band() == -1 || classificationMin() >= classificationMax() ) + if ( mBand == -1 || classificationMin() >= classificationMax() ) { return; } QgsColorRampShader *colorRampShader = new QgsColorRampShader( classificationMin(), classificationMax(), colorRamp, colorRampType, classificationMode ); - colorRampShader->classifyColorRamp( classes, band(), extent, input() ); + colorRampShader->classifyColorRamp( classes, mBand, extent, input() ); colorRampShader->setClip( clip ); QgsRasterShader *rasterShader = new QgsRasterShader(); @@ -376,7 +376,7 @@ void QgsSingleBandPseudoColorRenderer::toSld( QDomDocument &doc, QDomElement &el // set band QDomElement sourceChannelNameElem = doc.createElement( QStringLiteral( "sld:SourceChannelName" ) ); - sourceChannelNameElem.appendChild( doc.createTextNode( QString::number( band() ) ) ); + sourceChannelNameElem.appendChild( doc.createTextNode( QString::number( mBand ) ) ); channelElem.appendChild( sourceChannelNameElem ); // add ColorMap tag diff --git a/src/core/raster/qgssinglebandpseudocolorrenderer.h b/src/core/raster/qgssinglebandpseudocolorrenderer.h index c593508a0470..be3c2d60d193 100644 --- a/src/core/raster/qgssinglebandpseudocolorrenderer.h +++ b/src/core/raster/qgssinglebandpseudocolorrenderer.h @@ -87,14 +87,19 @@ class CORE_EXPORT QgsSingleBandPseudoColorRenderer: public QgsRasterRenderer /** * Returns the band used by the renderer + * + * \deprecated since QGIS 3.38 use inputBand() instead */ - int band() const { return mBand; } + Q_DECL_DEPRECATED int band() const SIP_DEPRECATED { return mBand; } /** * Sets the band used by the renderer. * \see band + * + * \deprecated since QGIS 3.38 use setInputBand() instead */ - void setBand( int bandNo ); + Q_DECL_DEPRECATED void setBand( int bandNo ) SIP_DEPRECATED; + int inputBand() const override; bool setInputBand( int band ) override; diff --git a/src/gui/raster/qgshillshaderendererwidget.cpp b/src/gui/raster/qgshillshaderendererwidget.cpp index 03d68fa10925..b7fa042ce86a 100644 --- a/src/gui/raster/qgshillshaderendererwidget.cpp +++ b/src/gui/raster/qgshillshaderendererwidget.cpp @@ -79,7 +79,7 @@ void QgsHillshadeRendererWidget::setFromRenderer( const QgsRasterRenderer *rende const QgsHillshadeRenderer *r = dynamic_cast( renderer ); if ( r ) { - mBandsCombo->setBand( r->band() ); + mBandsCombo->setBand( r->inputBand() ); mLightAngle->setValue( r->altitude() ); mLightAzimuth->setValue( r->azimuth() ); mZFactor->setValue( r->zFactor() ); diff --git a/src/gui/raster/qgspalettedrendererwidget.cpp b/src/gui/raster/qgspalettedrendererwidget.cpp index 219ca4d7148c..e813d957b44f 100644 --- a/src/gui/raster/qgspalettedrendererwidget.cpp +++ b/src/gui/raster/qgspalettedrendererwidget.cpp @@ -155,7 +155,7 @@ void QgsPalettedRendererWidget::setFromRenderer( const QgsRasterRenderer *r ) const QgsPalettedRasterRenderer *pr = dynamic_cast( r ); if ( pr ) { - mBand = pr->band(); + mBand = pr->inputBand(); whileBlocking( mBandComboBox )->setBand( mBand ); //read values and colors and fill into tree widget diff --git a/src/gui/raster/qgssinglebandgrayrendererwidget.cpp b/src/gui/raster/qgssinglebandgrayrendererwidget.cpp index 2ec1173b64e0..bc7dc13a23fc 100644 --- a/src/gui/raster/qgssinglebandgrayrendererwidget.cpp +++ b/src/gui/raster/qgssinglebandgrayrendererwidget.cpp @@ -185,8 +185,8 @@ void QgsSingleBandGrayRendererWidget::setFromRenderer( const QgsRasterRenderer * if ( gr ) { //band - mGrayBandComboBox->setBand( gr->grayBand() ); - mMinMaxWidget->setBands( QList< int >() << gr->grayBand() ); + mGrayBandComboBox->setBand( gr->inputBand() ); + mMinMaxWidget->setBands( QList< int >() << gr->inputBand() ); mGradientComboBox->setCurrentIndex( mGradientComboBox->findData( gr->gradient() ) ); const QgsContrastEnhancement *ce = gr->contrastEnhancement(); diff --git a/src/gui/raster/qgssinglebandpseudocolorrendererwidget.cpp b/src/gui/raster/qgssinglebandpseudocolorrendererwidget.cpp index b1d12c5acc9c..f3dd9bc057f7 100644 --- a/src/gui/raster/qgssinglebandpseudocolorrendererwidget.cpp +++ b/src/gui/raster/qgssinglebandpseudocolorrendererwidget.cpp @@ -145,9 +145,9 @@ void QgsSingleBandPseudoColorRendererWidget::setFromRenderer( const QgsRasterRen const QgsSingleBandPseudoColorRenderer *pr = dynamic_cast( r ); if ( pr ) { - mBandComboBox->setBand( pr->band() ); - mMinMaxWidget->setBands( QList< int >() << pr->band() ); - mColorRampShaderWidget->setRasterBand( pr->band() ); + mBandComboBox->setBand( pr->inputBand() ); + mMinMaxWidget->setBands( QList< int >() << pr->inputBand() ); + mColorRampShaderWidget->setRasterBand( pr->inputBand() ); // need to set min/max properties here because if we use the raster shader below, // we may set a new color ramp which needs to have min/max values defined. From 8426fa259d1adea5f0cd8117e23606df879722be Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Mar 2024 10:34:39 +1000 Subject: [PATCH 18/68] Move paletted raster renderer tests to own file --- tests/src/python/CMakeLists.txt | 1 + .../python/test_qgspalettedrasterrenderer.py | 518 ++++++++++++++++++ tests/src/python/test_qgsrasterlayer.py | 441 --------------- 3 files changed, 519 insertions(+), 441 deletions(-) create mode 100644 tests/src/python/test_qgspalettedrasterrenderer.py diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 2556b22a98ea..ccbeba90c79b 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -200,6 +200,7 @@ ADD_PYTHON_TEST(PyQgsOptional test_qgsoptional.py) ADD_PYTHON_TEST(PyQgsOrientedBox3D test_qgsorientedbox3d.py) ADD_PYTHON_TEST(PyQgsOwsConnection test_qgsowsconnection.py) ADD_PYTHON_TEST(PyQgsPainting test_qgspainting.py) +ADD_PYTHON_TEST(PyQgsPalettedRasterRenderer test_qgspalettedrasterrenderer.py) ADD_PYTHON_TEST(PyQgsPalLabelingBase test_qgspallabeling_base.py) ADD_PYTHON_TEST(PyQgsPalLabelingCanvas test_qgspallabeling_canvas.py) ADD_PYTHON_TEST(PyQgsPalLabelingLayout test_qgspallabeling_layout.py) diff --git a/tests/src/python/test_qgspalettedrasterrenderer.py b/tests/src/python/test_qgspalettedrasterrenderer.py new file mode 100644 index 000000000000..2c1fa64fd76f --- /dev/null +++ b/tests/src/python/test_qgspalettedrasterrenderer.py @@ -0,0 +1,518 @@ +"""QGIS Unit tests for QgsPalettedRasterRenderer + +From build dir, run: +ctest -R PyQgsPalettedRasterRenderer -V + +.. note:: 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. +""" + +import filecmp +import os +from shutil import copyfile + +import numpy as np +from osgeo import gdal +from qgis.PyQt.QtCore import QFileInfo, QSize, QTemporaryDir +from qgis.PyQt.QtGui import QColor, QResizeEvent +from qgis.PyQt.QtXml import QDomDocument +from qgis.core import ( + Qgis, + QgsBilinearRasterResampler, + QgsColorRampShader, + QgsContrastEnhancement, + QgsCoordinateReferenceSystem, + QgsCoordinateTransformContext, + QgsCubicRasterResampler, + QgsDataProvider, + QgsExpressionContext, + QgsExpressionContextScope, + QgsGradientColorRamp, + QgsHueSaturationFilter, + QgsLayerDefinition, + QgsLimitedRandomColorRamp, + QgsMapLayerServerProperties, + QgsMapSettings, + QgsPalettedRasterRenderer, + QgsPointXY, + QgsProject, + QgsProperty, + QgsRaster, + QgsRasterHistogram, + QgsRasterLayer, + QgsRasterMinMaxOrigin, + QgsRasterPipe, + QgsRasterShader, + QgsRasterTransparency, + QgsReadWriteContext, + QgsSingleBandGrayRenderer, + QgsSingleBandPseudoColorRenderer, +) +import unittest +from qgis.testing import start_app, QgisTestCase +from qgis.testing.mocked import get_iface + +from utilities import unitTestDataPath + +# Convenience instances in case you may need them +# not used in this test +start_app() + + +class TestQgsPalettedRasterRenderer(QgisTestCase): + + def setUp(self): + self.iface = get_iface() + QgsProject.instance().removeAllMapLayers() + + self.iface.mapCanvas().viewport().resize(400, 400) + # For some reason the resizeEvent is not delivered, fake it + self.iface.mapCanvas().resizeEvent(QResizeEvent(QSize(400, 400), self.iface.mapCanvas().size())) + + def testPaletted(self): + """ test paletted raster renderer with raster with color table""" + path = os.path.join(unitTestDataPath('raster'), + 'with_color_table.tif') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') + + renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 1, + [QgsPalettedRasterRenderer.Class(1, QColor(0, 255, 0), 'class 2'), + QgsPalettedRasterRenderer.Class(3, QColor(255, 0, 0), 'class 1')]) + + self.assertEqual(renderer.nColors(), 2) + self.assertEqual(renderer.usesBands(), [1]) + self.assertEqual(renderer.inputBand(), 1) + + # test labels + self.assertEqual(renderer.label(1), 'class 2') + self.assertEqual(renderer.label(3), 'class 1') + self.assertFalse(renderer.label(101)) + + # test legend symbology - should be sorted by value + legend = renderer.legendSymbologyItems() + self.assertEqual(legend[0][0], 'class 2') + self.assertEqual(legend[1][0], 'class 1') + self.assertEqual(legend[0][1].name(), '#00ff00') + self.assertEqual(legend[1][1].name(), '#ff0000') + + # test retrieving classes + classes = renderer.classes() + self.assertEqual(classes[0].value, 1) + self.assertEqual(classes[1].value, 3) + self.assertEqual(classes[0].label, 'class 2') + self.assertEqual(classes[1].label, 'class 1') + self.assertEqual(classes[0].color.name(), '#00ff00') + self.assertEqual(classes[1].color.name(), '#ff0000') + + # test set label + # bad index + renderer.setLabel(1212, 'bad') + renderer.setLabel(3, 'new class') + self.assertEqual(renderer.label(3), 'new class') + + # color ramp + r = QgsLimitedRandomColorRamp(5) + renderer.setSourceColorRamp(r) + self.assertEqual(renderer.sourceColorRamp().type(), 'random') + self.assertEqual(renderer.sourceColorRamp().count(), 5) + + # clone + new_renderer = renderer.clone() + classes = new_renderer.classes() + self.assertEqual(classes[0].value, 1) + self.assertEqual(classes[1].value, 3) + self.assertEqual(classes[0].label, 'class 2') + self.assertEqual(classes[1].label, 'new class') + self.assertEqual(classes[0].color.name(), '#00ff00') + self.assertEqual(classes[1].color.name(), '#ff0000') + self.assertEqual(new_renderer.sourceColorRamp().type(), 'random') + self.assertEqual(new_renderer.sourceColorRamp().count(), 5) + + # write to xml and read + doc = QDomDocument('testdoc') + elem = doc.createElement('qgis') + renderer.writeXml(doc, elem) + restored = QgsPalettedRasterRenderer.create(elem.firstChild().toElement(), layer.dataProvider()) + self.assertTrue(restored) + self.assertEqual(restored.usesBands(), [1]) + classes = restored.classes() + self.assertTrue(classes) + self.assertEqual(classes[0].value, 1) + self.assertEqual(classes[1].value, 3) + self.assertEqual(classes[0].label, 'class 2') + self.assertEqual(classes[1].label, 'new class') + self.assertEqual(classes[0].color.name(), '#00ff00') + self.assertEqual(classes[1].color.name(), '#ff0000') + self.assertEqual(restored.sourceColorRamp().type(), 'random') + self.assertEqual(restored.sourceColorRamp().count(), 5) + + # render test + layer.setRenderer(renderer) + ms = QgsMapSettings() + ms.setLayers([layer]) + ms.setExtent(layer.extent()) + + self.assertTrue( + self.render_map_settings_check( + 'paletted_renderer', + 'paletted_renderer', + ms) + ) + + def testPalettedBand(self): + """ test paletted raster render band""" + path = os.path.join(unitTestDataPath(), + 'landsat_4326.tif') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') + + renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 2, + [QgsPalettedRasterRenderer.Class(137, QColor(0, 255, 0), 'class 2'), + QgsPalettedRasterRenderer.Class(138, QColor(255, 0, 0), 'class 1'), + QgsPalettedRasterRenderer.Class(139, QColor(0, 0, 255), 'class 1')]) + + layer.setRenderer(renderer) + ms = QgsMapSettings() + ms.setLayers([layer]) + ms.setExtent(layer.extent()) + + self.assertTrue( + self.render_map_settings_check( + 'paletted_renderer_band2', + 'paletted_renderer_band2', + ms) + ) + + renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 3, + [QgsPalettedRasterRenderer.Class(120, QColor(0, 255, 0), 'class 2'), + QgsPalettedRasterRenderer.Class(123, QColor(255, 0, 0), 'class 1'), + QgsPalettedRasterRenderer.Class(124, QColor(0, 0, 255), 'class 1')]) + + layer.setRenderer(renderer) + ms = QgsMapSettings() + ms.setLayers([layer]) + ms.setExtent(layer.extent()) + + self.assertTrue( + self.render_map_settings_check( + 'paletted_renderer_band3', + 'paletted_renderer_band3', + ms) + ) + + def testPalettedColorTableToClassData(self): + entries = [QgsColorRampShader.ColorRampItem(5, QColor(255, 0, 0), 'item1'), + QgsColorRampShader.ColorRampItem(3, QColor(0, 255, 0), 'item2'), + QgsColorRampShader.ColorRampItem(6, QColor(0, 0, 255), 'item3'), + ] + classes = QgsPalettedRasterRenderer.colorTableToClassData(entries) + self.assertEqual(classes[0].value, 5) + self.assertEqual(classes[1].value, 3) + self.assertEqual(classes[2].value, 6) + self.assertEqual(classes[0].label, 'item1') + self.assertEqual(classes[1].label, 'item2') + self.assertEqual(classes[2].label, 'item3') + self.assertEqual(classes[0].color.name(), '#ff0000') + self.assertEqual(classes[1].color.name(), '#00ff00') + self.assertEqual(classes[2].color.name(), '#0000ff') + + # test #13263 + path = os.path.join(unitTestDataPath('raster'), + 'hub13263.vrt') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') + classes = QgsPalettedRasterRenderer.colorTableToClassData(layer.dataProvider().colorTable(1)) + self.assertEqual(len(classes), 4) + classes = QgsPalettedRasterRenderer.colorTableToClassData(layer.dataProvider().colorTable(15)) + self.assertEqual(len(classes), 256) + + def testLoadPalettedColorDataFromString(self): + """ + Test interpreting a bunch of color data format strings + """ + esri_clr_format = '1 255 255 0\n2 64 0 128\n3 255 32 32\n4 0 255 0\n5 0 0 255' + esri_clr_format_win = '1 255 255 0\r\n2 64 0 128\r\n3 255 32 32\r\n4 0 255 0\r\n5 0 0 255' + esri_clr_format_tab = '1\t255\t255\t0\n2\t64\t0\t128\n3\t255\t32\t32\n4\t0\t255\t0\n5\t0\t0\t255' + esri_clr_spaces = '1 255 255 0\n2 64 0 128\n3 255 32 32\n4 0 255 0\n5 0 0 255' + gdal_clr_comma = '1,255,255,0\n2,64,0,128\n3,255,32,32\n4,0,255,0\n5,0,0,255' + gdal_clr_colon = '1:255:255:0\n2:64:0:128\n3:255:32:32\n4:0:255:0\n5:0:0:255' + for f in [esri_clr_format, + esri_clr_format_win, + esri_clr_format_tab, + esri_clr_spaces, + gdal_clr_comma, + gdal_clr_colon]: + classes = QgsPalettedRasterRenderer.classDataFromString(f) + self.assertEqual(len(classes), 5) + self.assertEqual(classes[0].value, 1) + self.assertEqual(classes[0].color.name(), '#ffff00') + self.assertEqual(classes[1].value, 2) + self.assertEqual(classes[1].color.name(), '#400080') + self.assertEqual(classes[2].value, 3) + self.assertEqual(classes[2].color.name(), '#ff2020') + self.assertEqual(classes[3].value, 4) + self.assertEqual(classes[3].color.name(), '#00ff00') + self.assertEqual(classes[4].value, 5) + self.assertEqual(classes[4].color.name(), '#0000ff') + + grass_named_colors = '0 white\n1 yellow\n3 black\n6 blue\n9 magenta\n11 aqua\n13 grey\n14 gray\n15 orange\n19 brown\n21 purple\n22 violet\n24 indigo\n90 green\n180 cyan\n270 red\n' + classes = QgsPalettedRasterRenderer.classDataFromString(grass_named_colors) + self.assertEqual(len(classes), 16) + self.assertEqual(classes[0].value, 0) + self.assertEqual(classes[0].color.name(), '#ffffff') + self.assertEqual(classes[1].value, 1) + self.assertEqual(classes[1].color.name(), '#ffff00') + self.assertEqual(classes[2].value, 3) + self.assertEqual(classes[2].color.name(), '#000000') + self.assertEqual(classes[3].value, 6) + self.assertEqual(classes[3].color.name(), '#0000ff') + self.assertEqual(classes[4].value, 9) + self.assertEqual(classes[4].color.name(), '#ff00ff') + self.assertEqual(classes[5].value, 11) + self.assertEqual(classes[5].color.name(), '#00ffff') + self.assertEqual(classes[6].value, 13) + self.assertEqual(classes[6].color.name(), '#808080') + self.assertEqual(classes[7].value, 14) + self.assertEqual(classes[7].color.name(), '#808080') + self.assertEqual(classes[8].value, 15) + self.assertEqual(classes[8].color.name(), '#ffa500') + self.assertEqual(classes[9].value, 19) + self.assertEqual(classes[9].color.name(), '#a52a2a') + self.assertEqual(classes[10].value, 21) + self.assertEqual(classes[10].color.name(), '#800080') + self.assertEqual(classes[11].value, 22) + self.assertEqual(classes[11].color.name(), '#ee82ee') + self.assertEqual(classes[12].value, 24) + self.assertEqual(classes[12].color.name(), '#4b0082') + self.assertEqual(classes[13].value, 90) + self.assertEqual(classes[13].color.name(), '#008000') + self.assertEqual(classes[14].value, 180) + self.assertEqual(classes[14].color.name(), '#00ffff') + self.assertEqual(classes[15].value, 270) + self.assertEqual(classes[15].color.name(), '#ff0000') + + gdal_alpha = '1:255:255:0:0\n2:64:0:128:50\n3:255:32:32:122\n4:0:255:0:200\n5:0:0:255:255' + classes = QgsPalettedRasterRenderer.classDataFromString(gdal_alpha) + self.assertEqual(len(classes), 5) + self.assertEqual(classes[0].value, 1) + self.assertEqual(classes[0].color.name(), '#ffff00') + self.assertEqual(classes[0].color.alpha(), 0) + self.assertEqual(classes[1].value, 2) + self.assertEqual(classes[1].color.name(), '#400080') + self.assertEqual(classes[1].color.alpha(), 50) + self.assertEqual(classes[2].value, 3) + self.assertEqual(classes[2].color.name(), '#ff2020') + self.assertEqual(classes[2].color.alpha(), 122) + self.assertEqual(classes[3].value, 4) + self.assertEqual(classes[3].color.name(), '#00ff00') + self.assertEqual(classes[3].color.alpha(), 200) + self.assertEqual(classes[4].value, 5) + self.assertEqual(classes[4].color.name(), '#0000ff') + self.assertEqual(classes[4].color.alpha(), 255) + + # qgis style, with labels + qgis_style = '3 255 0 0 255 class 1\n4 0 255 0 200 class 2' + classes = QgsPalettedRasterRenderer.classDataFromString(qgis_style) + self.assertEqual(len(classes), 2) + self.assertEqual(classes[0].value, 3) + self.assertEqual(classes[0].color.name(), '#ff0000') + self.assertEqual(classes[0].color.alpha(), 255) + self.assertEqual(classes[0].label, 'class 1') + self.assertEqual(classes[1].value, 4) + self.assertEqual(classes[1].color.name(), '#00ff00') + self.assertEqual(classes[1].color.alpha(), 200) + self.assertEqual(classes[1].label, 'class 2') + + # some bad inputs + bad = '' + classes = QgsPalettedRasterRenderer.classDataFromString(bad) + self.assertEqual(len(classes), 0) + bad = '\n\n\n' + classes = QgsPalettedRasterRenderer.classDataFromString(bad) + self.assertEqual(len(classes), 0) + bad = 'x x x x' + classes = QgsPalettedRasterRenderer.classDataFromString(bad) + self.assertEqual(len(classes), 0) + bad = '1 255 0 0\n2 255 255\n3 255 0 255' + classes = QgsPalettedRasterRenderer.classDataFromString(bad) + self.assertEqual(len(classes), 2) + bad = '1 255 a 0' + classes = QgsPalettedRasterRenderer.classDataFromString(bad) + self.assertEqual(len(classes), 1) + + def testLoadPalettedClassDataFromFile(self): + # bad file + classes = QgsPalettedRasterRenderer.classDataFromFile('ajdhjashjkdh kjahjkdhk') + self.assertEqual(len(classes), 0) + + # good file! + path = os.path.join(unitTestDataPath('raster'), + 'test.clr') + classes = QgsPalettedRasterRenderer.classDataFromFile(path) + self.assertEqual(len(classes), 10) + self.assertEqual(classes[0].value, 1) + self.assertEqual(classes[0].color.name(), '#000000') + self.assertEqual(classes[0].color.alpha(), 255) + self.assertEqual(classes[1].value, 2) + self.assertEqual(classes[1].color.name(), '#c8c8c8') + self.assertEqual(classes[2].value, 3) + self.assertEqual(classes[2].color.name(), '#006e00') + self.assertEqual(classes[3].value, 4) + self.assertEqual(classes[3].color.name(), '#6e4100') + self.assertEqual(classes[4].value, 5) + self.assertEqual(classes[4].color.name(), '#0000ff') + self.assertEqual(classes[4].color.alpha(), 255) + self.assertEqual(classes[5].value, 6) + self.assertEqual(classes[5].color.name(), '#0059ff') + self.assertEqual(classes[6].value, 7) + self.assertEqual(classes[6].color.name(), '#00aeff') + self.assertEqual(classes[7].value, 8) + self.assertEqual(classes[7].color.name(), '#00fff6') + self.assertEqual(classes[8].value, 9) + self.assertEqual(classes[8].color.name(), '#eeff00') + self.assertEqual(classes[9].value, 10) + self.assertEqual(classes[9].color.name(), '#ffb600') + + def testPalettedClassDataToString(self): + classes = [QgsPalettedRasterRenderer.Class(1, QColor(0, 255, 0), 'class 2'), + QgsPalettedRasterRenderer.Class(3, QColor(255, 0, 0), 'class 1')] + self.assertEqual(QgsPalettedRasterRenderer.classDataToString(classes), + '1 0 255 0 255 class 2\n3 255 0 0 255 class 1') + # must be sorted by value to work OK in ArcMap + classes = [QgsPalettedRasterRenderer.Class(4, QColor(0, 255, 0), 'class 2'), + QgsPalettedRasterRenderer.Class(3, QColor(255, 0, 0), 'class 1')] + self.assertEqual(QgsPalettedRasterRenderer.classDataToString(classes), + '3 255 0 0 255 class 1\n4 0 255 0 255 class 2') + + def testPalettedClassDataFromLayer(self): + # no layer + classes = QgsPalettedRasterRenderer.classDataFromRaster(None, 1) + self.assertFalse(classes) + + # 10 class layer + path = os.path.join(unitTestDataPath('raster'), + 'with_color_table.tif') + info = QFileInfo(path) + base_name = info.baseName() + layer10 = QgsRasterLayer(path, base_name) + classes = QgsPalettedRasterRenderer.classDataFromRaster(layer10.dataProvider(), 1) + self.assertEqual(len(classes), 10) + self.assertEqual(classes[0].value, 1) + self.assertEqual(classes[0].label, '1') + self.assertEqual(classes[1].value, 2) + self.assertEqual(classes[1].label, '2') + self.assertEqual(classes[2].value, 3) + self.assertEqual(classes[2].label, '3') + self.assertEqual(classes[3].value, 4) + self.assertEqual(classes[3].label, '4') + self.assertEqual(classes[4].value, 5) + self.assertEqual(classes[4].label, '5') + self.assertEqual(classes[5].value, 6) + self.assertEqual(classes[5].label, '6') + self.assertEqual(classes[6].value, 7) + self.assertEqual(classes[6].label, '7') + self.assertEqual(classes[7].value, 8) + self.assertEqual(classes[7].label, '8') + self.assertEqual(classes[8].value, 9) + self.assertEqual(classes[8].label, '9') + self.assertEqual(classes[9].value, 10) + self.assertEqual(classes[9].label, '10') + + # bad band + self.assertFalse(QgsPalettedRasterRenderer.classDataFromRaster(layer10.dataProvider(), 10101010)) + + # with ramp + r = QgsGradientColorRamp(QColor(200, 0, 0, 100), QColor(0, 200, 0, 200)) + classes = QgsPalettedRasterRenderer.classDataFromRaster(layer10.dataProvider(), 1, r) + self.assertEqual(len(classes), 10) + self.assertEqual(classes[0].color.name(), '#c80000') + self.assertEqual(classes[1].color.name(), '#b21600') + self.assertEqual(classes[2].color.name(), '#9c2c00') + self.assertIn(classes[3].color.name(), ('#854200', '#854300')) + self.assertEqual(classes[4].color.name(), '#6f5900') + self.assertEqual(classes[5].color.name(), '#596f00') + self.assertIn(classes[6].color.name(), ('#428500', '#438500')) + self.assertEqual(classes[7].color.name(), '#2c9c00') + self.assertEqual(classes[8].color.name(), '#16b200') + self.assertEqual(classes[9].color.name(), '#00c800') + + # 30 class layer + path = os.path.join(unitTestDataPath('raster'), + 'unique_1.tif') + info = QFileInfo(path) + base_name = info.baseName() + layer10 = QgsRasterLayer(path, base_name) + classes = QgsPalettedRasterRenderer.classDataFromRaster(layer10.dataProvider(), 1) + self.assertEqual(len(classes), 30) + expected = [11, 21, 22, 24, 31, 82, 2002, 2004, 2014, 2019, 2027, 2029, 2030, 2080, 2081, 2082, 2088, 2092, + 2097, 2098, 2099, 2105, 2108, 2110, 2114, 2118, 2126, 2152, 2184, 2220] + self.assertEqual([c.value for c in classes], expected) + + # bad layer + path = os.path.join(unitTestDataPath('raster'), + 'hub13263.vrt') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + classes = QgsPalettedRasterRenderer.classDataFromRaster(layer.dataProvider(), 1) + self.assertFalse(classes) + + def testPalettedRendererWithNegativeColorValue(self): + """ test paletted raster renderer with negative values in color table""" + + path = os.path.join(unitTestDataPath('raster'), + 'hub13263.vrt') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') + + renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 1, + [QgsPalettedRasterRenderer.Class(-1, QColor(0, 255, 0), 'class 2'), + QgsPalettedRasterRenderer.Class(3, QColor(255, 0, 0), 'class 1')]) + + self.assertEqual(renderer.nColors(), 2) + self.assertEqual(renderer.usesBands(), [1]) + + def testPalettedRendererWithFloats(self): + """Tests for https://github.com/qgis/QGIS/issues/39058""" + + tempdir = QTemporaryDir() + temppath = os.path.join(tempdir.path(), 'paletted.tif') + + # Create a float raster with unique values up to 65536 + one extra row + driver = gdal.GetDriverByName('GTiff') + outRaster = driver.Create(temppath, 256, 256 + 1, 1, gdal.GDT_Float32) + outband = outRaster.GetRasterBand(1) + data = [] + for r in range(256 + 1): + data.append(list(range(r * 256, (r + 1) * 256))) + npdata = np.array(data, np.float32) + outband.WriteArray(npdata) + outband.FlushCache() + outRaster.FlushCache() + del outRaster + + layer = QgsRasterLayer(temppath, 'paletted') + self.assertTrue(layer.isValid()) + self.assertEqual(layer.dataProvider().dataType(1), Qgis.DataType.Float32) + classes = QgsPalettedRasterRenderer.classDataFromRaster(layer.dataProvider(), 1) + # Check max classes count, hardcoded in QGIS renderer + self.assertEqual(len(classes), 65536) + class_values = [] + for c in classes: + class_values.append(c.value) + self.assertEqual(sorted(class_values), list(range(65536))) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsrasterlayer.py b/tests/src/python/test_qgsrasterlayer.py index 6c52760cfc12..66fe966741ae 100644 --- a/tests/src/python/test_qgsrasterlayer.py +++ b/tests/src/python/test_qgsrasterlayer.py @@ -456,141 +456,6 @@ def testQgsRasterMinMaxOrigin(self): mmoUnserialized.readXml(parentElem) self.assertEqual(mmo, mmoUnserialized) - def testPaletted(self): - """ test paletted raster renderer with raster with color table""" - path = os.path.join(unitTestDataPath('raster'), - 'with_color_table.tif') - info = QFileInfo(path) - base_name = info.baseName() - layer = QgsRasterLayer(path, base_name) - self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') - - renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 1, - [QgsPalettedRasterRenderer.Class(1, QColor(0, 255, 0), 'class 2'), - QgsPalettedRasterRenderer.Class(3, QColor(255, 0, 0), 'class 1')]) - - self.assertEqual(renderer.nColors(), 2) - self.assertEqual(renderer.usesBands(), [1]) - - # test labels - self.assertEqual(renderer.label(1), 'class 2') - self.assertEqual(renderer.label(3), 'class 1') - self.assertFalse(renderer.label(101)) - - # test legend symbology - should be sorted by value - legend = renderer.legendSymbologyItems() - self.assertEqual(legend[0][0], 'class 2') - self.assertEqual(legend[1][0], 'class 1') - self.assertEqual(legend[0][1].name(), '#00ff00') - self.assertEqual(legend[1][1].name(), '#ff0000') - - # test retrieving classes - classes = renderer.classes() - self.assertEqual(classes[0].value, 1) - self.assertEqual(classes[1].value, 3) - self.assertEqual(classes[0].label, 'class 2') - self.assertEqual(classes[1].label, 'class 1') - self.assertEqual(classes[0].color.name(), '#00ff00') - self.assertEqual(classes[1].color.name(), '#ff0000') - - # test set label - # bad index - renderer.setLabel(1212, 'bad') - renderer.setLabel(3, 'new class') - self.assertEqual(renderer.label(3), 'new class') - - # color ramp - r = QgsLimitedRandomColorRamp(5) - renderer.setSourceColorRamp(r) - self.assertEqual(renderer.sourceColorRamp().type(), 'random') - self.assertEqual(renderer.sourceColorRamp().count(), 5) - - # clone - new_renderer = renderer.clone() - classes = new_renderer.classes() - self.assertEqual(classes[0].value, 1) - self.assertEqual(classes[1].value, 3) - self.assertEqual(classes[0].label, 'class 2') - self.assertEqual(classes[1].label, 'new class') - self.assertEqual(classes[0].color.name(), '#00ff00') - self.assertEqual(classes[1].color.name(), '#ff0000') - self.assertEqual(new_renderer.sourceColorRamp().type(), 'random') - self.assertEqual(new_renderer.sourceColorRamp().count(), 5) - - # write to xml and read - doc = QDomDocument('testdoc') - elem = doc.createElement('qgis') - renderer.writeXml(doc, elem) - restored = QgsPalettedRasterRenderer.create(elem.firstChild().toElement(), layer.dataProvider()) - self.assertTrue(restored) - self.assertEqual(restored.usesBands(), [1]) - classes = restored.classes() - self.assertTrue(classes) - self.assertEqual(classes[0].value, 1) - self.assertEqual(classes[1].value, 3) - self.assertEqual(classes[0].label, 'class 2') - self.assertEqual(classes[1].label, 'new class') - self.assertEqual(classes[0].color.name(), '#00ff00') - self.assertEqual(classes[1].color.name(), '#ff0000') - self.assertEqual(restored.sourceColorRamp().type(), 'random') - self.assertEqual(restored.sourceColorRamp().count(), 5) - - # render test - layer.setRenderer(renderer) - ms = QgsMapSettings() - ms.setLayers([layer]) - ms.setExtent(layer.extent()) - - self.assertTrue( - self.render_map_settings_check( - 'paletted_renderer', - 'paletted_renderer', - ms) - ) - - def testPalettedBand(self): - """ test paletted raster render band""" - path = os.path.join(unitTestDataPath(), - 'landsat_4326.tif') - info = QFileInfo(path) - base_name = info.baseName() - layer = QgsRasterLayer(path, base_name) - self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') - - renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 2, - [QgsPalettedRasterRenderer.Class(137, QColor(0, 255, 0), 'class 2'), - QgsPalettedRasterRenderer.Class(138, QColor(255, 0, 0), 'class 1'), - QgsPalettedRasterRenderer.Class(139, QColor(0, 0, 255), 'class 1')]) - - layer.setRenderer(renderer) - ms = QgsMapSettings() - ms.setLayers([layer]) - ms.setExtent(layer.extent()) - - self.assertTrue( - self.render_map_settings_check( - 'paletted_renderer_band2', - 'paletted_renderer_band2', - ms) - ) - - renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 3, - [QgsPalettedRasterRenderer.Class(120, QColor(0, 255, 0), 'class 2'), - QgsPalettedRasterRenderer.Class(123, QColor(255, 0, 0), 'class 1'), - QgsPalettedRasterRenderer.Class(124, QColor(0, 0, 255), 'class 1')]) - - layer.setRenderer(renderer) - ms = QgsMapSettings() - ms.setLayers([layer]) - ms.setExtent(layer.extent()) - - self.assertTrue( - self.render_map_settings_check( - 'paletted_renderer_band3', - 'paletted_renderer_band3', - ms) - ) - def testBrightnessContrastGamma(self): """ test raster brightness/contrast/gamma filter""" path = os.path.join(unitTestDataPath(), @@ -731,312 +596,6 @@ def testInvertSemiOpaqueColors(self): ms) ) - def testPalettedColorTableToClassData(self): - entries = [QgsColorRampShader.ColorRampItem(5, QColor(255, 0, 0), 'item1'), - QgsColorRampShader.ColorRampItem(3, QColor(0, 255, 0), 'item2'), - QgsColorRampShader.ColorRampItem(6, QColor(0, 0, 255), 'item3'), - ] - classes = QgsPalettedRasterRenderer.colorTableToClassData(entries) - self.assertEqual(classes[0].value, 5) - self.assertEqual(classes[1].value, 3) - self.assertEqual(classes[2].value, 6) - self.assertEqual(classes[0].label, 'item1') - self.assertEqual(classes[1].label, 'item2') - self.assertEqual(classes[2].label, 'item3') - self.assertEqual(classes[0].color.name(), '#ff0000') - self.assertEqual(classes[1].color.name(), '#00ff00') - self.assertEqual(classes[2].color.name(), '#0000ff') - - # test #13263 - path = os.path.join(unitTestDataPath('raster'), - 'hub13263.vrt') - info = QFileInfo(path) - base_name = info.baseName() - layer = QgsRasterLayer(path, base_name) - self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') - classes = QgsPalettedRasterRenderer.colorTableToClassData(layer.dataProvider().colorTable(1)) - self.assertEqual(len(classes), 4) - classes = QgsPalettedRasterRenderer.colorTableToClassData(layer.dataProvider().colorTable(15)) - self.assertEqual(len(classes), 256) - - def testLoadPalettedColorDataFromString(self): - """ - Test interpreting a bunch of color data format strings - """ - esri_clr_format = '1 255 255 0\n2 64 0 128\n3 255 32 32\n4 0 255 0\n5 0 0 255' - esri_clr_format_win = '1 255 255 0\r\n2 64 0 128\r\n3 255 32 32\r\n4 0 255 0\r\n5 0 0 255' - esri_clr_format_tab = '1\t255\t255\t0\n2\t64\t0\t128\n3\t255\t32\t32\n4\t0\t255\t0\n5\t0\t0\t255' - esri_clr_spaces = '1 255 255 0\n2 64 0 128\n3 255 32 32\n4 0 255 0\n5 0 0 255' - gdal_clr_comma = '1,255,255,0\n2,64,0,128\n3,255,32,32\n4,0,255,0\n5,0,0,255' - gdal_clr_colon = '1:255:255:0\n2:64:0:128\n3:255:32:32\n4:0:255:0\n5:0:0:255' - for f in [esri_clr_format, - esri_clr_format_win, - esri_clr_format_tab, - esri_clr_spaces, - gdal_clr_comma, - gdal_clr_colon]: - classes = QgsPalettedRasterRenderer.classDataFromString(f) - self.assertEqual(len(classes), 5) - self.assertEqual(classes[0].value, 1) - self.assertEqual(classes[0].color.name(), '#ffff00') - self.assertEqual(classes[1].value, 2) - self.assertEqual(classes[1].color.name(), '#400080') - self.assertEqual(classes[2].value, 3) - self.assertEqual(classes[2].color.name(), '#ff2020') - self.assertEqual(classes[3].value, 4) - self.assertEqual(classes[3].color.name(), '#00ff00') - self.assertEqual(classes[4].value, 5) - self.assertEqual(classes[4].color.name(), '#0000ff') - - grass_named_colors = '0 white\n1 yellow\n3 black\n6 blue\n9 magenta\n11 aqua\n13 grey\n14 gray\n15 orange\n19 brown\n21 purple\n22 violet\n24 indigo\n90 green\n180 cyan\n270 red\n' - classes = QgsPalettedRasterRenderer.classDataFromString(grass_named_colors) - self.assertEqual(len(classes), 16) - self.assertEqual(classes[0].value, 0) - self.assertEqual(classes[0].color.name(), '#ffffff') - self.assertEqual(classes[1].value, 1) - self.assertEqual(classes[1].color.name(), '#ffff00') - self.assertEqual(classes[2].value, 3) - self.assertEqual(classes[2].color.name(), '#000000') - self.assertEqual(classes[3].value, 6) - self.assertEqual(classes[3].color.name(), '#0000ff') - self.assertEqual(classes[4].value, 9) - self.assertEqual(classes[4].color.name(), '#ff00ff') - self.assertEqual(classes[5].value, 11) - self.assertEqual(classes[5].color.name(), '#00ffff') - self.assertEqual(classes[6].value, 13) - self.assertEqual(classes[6].color.name(), '#808080') - self.assertEqual(classes[7].value, 14) - self.assertEqual(classes[7].color.name(), '#808080') - self.assertEqual(classes[8].value, 15) - self.assertEqual(classes[8].color.name(), '#ffa500') - self.assertEqual(classes[9].value, 19) - self.assertEqual(classes[9].color.name(), '#a52a2a') - self.assertEqual(classes[10].value, 21) - self.assertEqual(classes[10].color.name(), '#800080') - self.assertEqual(classes[11].value, 22) - self.assertEqual(classes[11].color.name(), '#ee82ee') - self.assertEqual(classes[12].value, 24) - self.assertEqual(classes[12].color.name(), '#4b0082') - self.assertEqual(classes[13].value, 90) - self.assertEqual(classes[13].color.name(), '#008000') - self.assertEqual(classes[14].value, 180) - self.assertEqual(classes[14].color.name(), '#00ffff') - self.assertEqual(classes[15].value, 270) - self.assertEqual(classes[15].color.name(), '#ff0000') - - gdal_alpha = '1:255:255:0:0\n2:64:0:128:50\n3:255:32:32:122\n4:0:255:0:200\n5:0:0:255:255' - classes = QgsPalettedRasterRenderer.classDataFromString(gdal_alpha) - self.assertEqual(len(classes), 5) - self.assertEqual(classes[0].value, 1) - self.assertEqual(classes[0].color.name(), '#ffff00') - self.assertEqual(classes[0].color.alpha(), 0) - self.assertEqual(classes[1].value, 2) - self.assertEqual(classes[1].color.name(), '#400080') - self.assertEqual(classes[1].color.alpha(), 50) - self.assertEqual(classes[2].value, 3) - self.assertEqual(classes[2].color.name(), '#ff2020') - self.assertEqual(classes[2].color.alpha(), 122) - self.assertEqual(classes[3].value, 4) - self.assertEqual(classes[3].color.name(), '#00ff00') - self.assertEqual(classes[3].color.alpha(), 200) - self.assertEqual(classes[4].value, 5) - self.assertEqual(classes[4].color.name(), '#0000ff') - self.assertEqual(classes[4].color.alpha(), 255) - - # qgis style, with labels - qgis_style = '3 255 0 0 255 class 1\n4 0 255 0 200 class 2' - classes = QgsPalettedRasterRenderer.classDataFromString(qgis_style) - self.assertEqual(len(classes), 2) - self.assertEqual(classes[0].value, 3) - self.assertEqual(classes[0].color.name(), '#ff0000') - self.assertEqual(classes[0].color.alpha(), 255) - self.assertEqual(classes[0].label, 'class 1') - self.assertEqual(classes[1].value, 4) - self.assertEqual(classes[1].color.name(), '#00ff00') - self.assertEqual(classes[1].color.alpha(), 200) - self.assertEqual(classes[1].label, 'class 2') - - # some bad inputs - bad = '' - classes = QgsPalettedRasterRenderer.classDataFromString(bad) - self.assertEqual(len(classes), 0) - bad = '\n\n\n' - classes = QgsPalettedRasterRenderer.classDataFromString(bad) - self.assertEqual(len(classes), 0) - bad = 'x x x x' - classes = QgsPalettedRasterRenderer.classDataFromString(bad) - self.assertEqual(len(classes), 0) - bad = '1 255 0 0\n2 255 255\n3 255 0 255' - classes = QgsPalettedRasterRenderer.classDataFromString(bad) - self.assertEqual(len(classes), 2) - bad = '1 255 a 0' - classes = QgsPalettedRasterRenderer.classDataFromString(bad) - self.assertEqual(len(classes), 1) - - def testLoadPalettedClassDataFromFile(self): - # bad file - classes = QgsPalettedRasterRenderer.classDataFromFile('ajdhjashjkdh kjahjkdhk') - self.assertEqual(len(classes), 0) - - # good file! - path = os.path.join(unitTestDataPath('raster'), - 'test.clr') - classes = QgsPalettedRasterRenderer.classDataFromFile(path) - self.assertEqual(len(classes), 10) - self.assertEqual(classes[0].value, 1) - self.assertEqual(classes[0].color.name(), '#000000') - self.assertEqual(classes[0].color.alpha(), 255) - self.assertEqual(classes[1].value, 2) - self.assertEqual(classes[1].color.name(), '#c8c8c8') - self.assertEqual(classes[2].value, 3) - self.assertEqual(classes[2].color.name(), '#006e00') - self.assertEqual(classes[3].value, 4) - self.assertEqual(classes[3].color.name(), '#6e4100') - self.assertEqual(classes[4].value, 5) - self.assertEqual(classes[4].color.name(), '#0000ff') - self.assertEqual(classes[4].color.alpha(), 255) - self.assertEqual(classes[5].value, 6) - self.assertEqual(classes[5].color.name(), '#0059ff') - self.assertEqual(classes[6].value, 7) - self.assertEqual(classes[6].color.name(), '#00aeff') - self.assertEqual(classes[7].value, 8) - self.assertEqual(classes[7].color.name(), '#00fff6') - self.assertEqual(classes[8].value, 9) - self.assertEqual(classes[8].color.name(), '#eeff00') - self.assertEqual(classes[9].value, 10) - self.assertEqual(classes[9].color.name(), '#ffb600') - - def testPalettedClassDataToString(self): - classes = [QgsPalettedRasterRenderer.Class(1, QColor(0, 255, 0), 'class 2'), - QgsPalettedRasterRenderer.Class(3, QColor(255, 0, 0), 'class 1')] - self.assertEqual(QgsPalettedRasterRenderer.classDataToString(classes), - '1 0 255 0 255 class 2\n3 255 0 0 255 class 1') - # must be sorted by value to work OK in ArcMap - classes = [QgsPalettedRasterRenderer.Class(4, QColor(0, 255, 0), 'class 2'), - QgsPalettedRasterRenderer.Class(3, QColor(255, 0, 0), 'class 1')] - self.assertEqual(QgsPalettedRasterRenderer.classDataToString(classes), - '3 255 0 0 255 class 1\n4 0 255 0 255 class 2') - - def testPalettedClassDataFromLayer(self): - # no layer - classes = QgsPalettedRasterRenderer.classDataFromRaster(None, 1) - self.assertFalse(classes) - - # 10 class layer - path = os.path.join(unitTestDataPath('raster'), - 'with_color_table.tif') - info = QFileInfo(path) - base_name = info.baseName() - layer10 = QgsRasterLayer(path, base_name) - classes = QgsPalettedRasterRenderer.classDataFromRaster(layer10.dataProvider(), 1) - self.assertEqual(len(classes), 10) - self.assertEqual(classes[0].value, 1) - self.assertEqual(classes[0].label, '1') - self.assertEqual(classes[1].value, 2) - self.assertEqual(classes[1].label, '2') - self.assertEqual(classes[2].value, 3) - self.assertEqual(classes[2].label, '3') - self.assertEqual(classes[3].value, 4) - self.assertEqual(classes[3].label, '4') - self.assertEqual(classes[4].value, 5) - self.assertEqual(classes[4].label, '5') - self.assertEqual(classes[5].value, 6) - self.assertEqual(classes[5].label, '6') - self.assertEqual(classes[6].value, 7) - self.assertEqual(classes[6].label, '7') - self.assertEqual(classes[7].value, 8) - self.assertEqual(classes[7].label, '8') - self.assertEqual(classes[8].value, 9) - self.assertEqual(classes[8].label, '9') - self.assertEqual(classes[9].value, 10) - self.assertEqual(classes[9].label, '10') - - # bad band - self.assertFalse(QgsPalettedRasterRenderer.classDataFromRaster(layer10.dataProvider(), 10101010)) - - # with ramp - r = QgsGradientColorRamp(QColor(200, 0, 0, 100), QColor(0, 200, 0, 200)) - classes = QgsPalettedRasterRenderer.classDataFromRaster(layer10.dataProvider(), 1, r) - self.assertEqual(len(classes), 10) - self.assertEqual(classes[0].color.name(), '#c80000') - self.assertEqual(classes[1].color.name(), '#b21600') - self.assertEqual(classes[2].color.name(), '#9c2c00') - self.assertIn(classes[3].color.name(), ('#854200', '#854300')) - self.assertEqual(classes[4].color.name(), '#6f5900') - self.assertEqual(classes[5].color.name(), '#596f00') - self.assertIn(classes[6].color.name(), ('#428500', '#438500')) - self.assertEqual(classes[7].color.name(), '#2c9c00') - self.assertEqual(classes[8].color.name(), '#16b200') - self.assertEqual(classes[9].color.name(), '#00c800') - - # 30 class layer - path = os.path.join(unitTestDataPath('raster'), - 'unique_1.tif') - info = QFileInfo(path) - base_name = info.baseName() - layer10 = QgsRasterLayer(path, base_name) - classes = QgsPalettedRasterRenderer.classDataFromRaster(layer10.dataProvider(), 1) - self.assertEqual(len(classes), 30) - expected = [11, 21, 22, 24, 31, 82, 2002, 2004, 2014, 2019, 2027, 2029, 2030, 2080, 2081, 2082, 2088, 2092, - 2097, 2098, 2099, 2105, 2108, 2110, 2114, 2118, 2126, 2152, 2184, 2220] - self.assertEqual([c.value for c in classes], expected) - - # bad layer - path = os.path.join(unitTestDataPath('raster'), - 'hub13263.vrt') - info = QFileInfo(path) - base_name = info.baseName() - layer = QgsRasterLayer(path, base_name) - classes = QgsPalettedRasterRenderer.classDataFromRaster(layer.dataProvider(), 1) - self.assertFalse(classes) - - def testPalettedRendererWithNegativeColorValue(self): - """ test paletted raster renderer with negative values in color table""" - - path = os.path.join(unitTestDataPath('raster'), - 'hub13263.vrt') - info = QFileInfo(path) - base_name = info.baseName() - layer = QgsRasterLayer(path, base_name) - self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') - - renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 1, - [QgsPalettedRasterRenderer.Class(-1, QColor(0, 255, 0), 'class 2'), - QgsPalettedRasterRenderer.Class(3, QColor(255, 0, 0), 'class 1')]) - - self.assertEqual(renderer.nColors(), 2) - self.assertEqual(renderer.usesBands(), [1]) - - def testPalettedRendererWithFloats(self): - """Tests for https://github.com/qgis/QGIS/issues/39058""" - - tempdir = QTemporaryDir() - temppath = os.path.join(tempdir.path(), 'paletted.tif') - - # Create a float raster with unique values up to 65536 + one extra row - driver = gdal.GetDriverByName('GTiff') - outRaster = driver.Create(temppath, 256, 256 + 1, 1, gdal.GDT_Float32) - outband = outRaster.GetRasterBand(1) - data = [] - for r in range(256 + 1): - data.append(list(range(r * 256, (r + 1) * 256))) - npdata = np.array(data, np.float32) - outband.WriteArray(npdata) - outband.FlushCache() - outRaster.FlushCache() - del outRaster - - layer = QgsRasterLayer(temppath, 'paletted') - self.assertTrue(layer.isValid()) - self.assertEqual(layer.dataProvider().dataType(1), Qgis.DataType.Float32) - classes = QgsPalettedRasterRenderer.classDataFromRaster(layer.dataProvider(), 1) - # Check max classes count, hardcoded in QGIS renderer - self.assertEqual(len(classes), 65536) - class_values = [] - for c in classes: - class_values.append(c.value) - self.assertEqual(sorted(class_values), list(range(65536))) - def testClone(self): myPath = os.path.join(unitTestDataPath('raster'), 'band1_float32_noct_epsg4326.tif') From bbf286142551b28f8a86377741975de89a32e115 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Mar 2024 10:38:32 +1000 Subject: [PATCH 19/68] Add tests for paletted renderer input band --- .../python/test_qgspalettedrasterrenderer.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/src/python/test_qgspalettedrasterrenderer.py b/tests/src/python/test_qgspalettedrasterrenderer.py index 2c1fa64fd76f..f7f629f3e215 100644 --- a/tests/src/python/test_qgspalettedrasterrenderer.py +++ b/tests/src/python/test_qgspalettedrasterrenderer.py @@ -164,6 +164,19 @@ def testPaletted(self): ms) ) + def testPalettedBandInvalidLayer(self): + """ test paletted raster render band with a broken layer path""" + renderer = QgsPalettedRasterRenderer(None, 2, + [QgsPalettedRasterRenderer.Class(137, QColor(0, 255, 0), 'class 2'), + QgsPalettedRasterRenderer.Class(138, QColor(255, 0, 0), 'class 1'), + QgsPalettedRasterRenderer.Class(139, QColor(0, 0, 255), 'class 1')]) + + self.assertEqual(renderer.inputBand(), 2) + + # the renderer input is broken, we don't know what bands are valid, so all should be accepted + self.assertTrue(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 10) + def testPalettedBand(self): """ test paletted raster render band""" path = os.path.join(unitTestDataPath(), @@ -178,6 +191,15 @@ def testPalettedBand(self): QgsPalettedRasterRenderer.Class(138, QColor(255, 0, 0), 'class 1'), QgsPalettedRasterRenderer.Class(139, QColor(0, 0, 255), 'class 1')]) + self.assertEqual(renderer.inputBand(), 2) + self.assertFalse(renderer.setInputBand(0)) + self.assertEqual(renderer.inputBand(), 2) + self.assertFalse(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 2) + self.assertTrue(renderer.setInputBand(1)) + self.assertEqual(renderer.inputBand(), 1) + self.assertTrue(renderer.setInputBand(2)) + layer.setRenderer(renderer) ms = QgsMapSettings() ms.setLayers([layer]) From 3cb235ed58af189e0e119fc01297cb7bb35f1253 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Mar 2024 10:43:41 +1000 Subject: [PATCH 20/68] Add unit tests for hillshade renderer --- tests/src/python/CMakeLists.txt | 1 + tests/src/python/test_qgshillshaderenderer.py | 81 +++++++++++++++++++ .../python/test_qgspalettedrasterrenderer.py | 26 +----- 3 files changed, 83 insertions(+), 25 deletions(-) create mode 100644 tests/src/python/test_qgshillshaderenderer.py diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index ccbeba90c79b..b894e73ece9a 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -105,6 +105,7 @@ ADD_PYTHON_TEST(PyQgsGraduatedSymbolRenderer test_qgsgraduatedsymbolrenderer.py) ADD_PYTHON_TEST(PyQgsGraph test_qgsgraph.py) ADD_PYTHON_TEST(PyQgsGroupLayer test_qgsgrouplayer.py) ADD_PYTHON_TEST(PyQgsHashLineSymbolLayer test_qgshashlinesymbollayer.py) +ADD_PYTHON_TEST(PyQgsHillshadeRenderer test_qgshillshaderenderer.py) ADD_PYTHON_TEST(PyQgsImageCache test_qgsimagecache.py) ADD_PYTHON_TEST(PyQgsInterpolatedLineSymbolLayer test_qgsinterpolatedlinesymbollayers.py) ADD_PYTHON_TEST(PyQgsInterval test_qgsinterval.py) diff --git a/tests/src/python/test_qgshillshaderenderer.py b/tests/src/python/test_qgshillshaderenderer.py new file mode 100644 index 000000000000..83d39fad5344 --- /dev/null +++ b/tests/src/python/test_qgshillshaderenderer.py @@ -0,0 +1,81 @@ +"""QGIS Unit tests for QgsHillshadeRenderer + +From build dir, run: +ctest -R PyQgsHillshadeRenderer -V + +.. note:: 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. +""" + +import os + +from qgis.PyQt.QtCore import QFileInfo, QSize +from qgis.PyQt.QtGui import QResizeEvent +from qgis.core import ( + QgsHillshadeRenderer, + QgsProject, + QgsRasterLayer, +) +import unittest +from qgis.testing import start_app, QgisTestCase +from qgis.testing.mocked import get_iface + +from utilities import unitTestDataPath + +# Convenience instances in case you may need them +# not used in this test +start_app() + + +class TestQgsHillshadeRenderer(QgisTestCase): + + def setUp(self): + self.iface = get_iface() + QgsProject.instance().removeAllMapLayers() + + self.iface.mapCanvas().viewport().resize(400, 400) + # For some reason the resizeEvent is not delivered, fake it + self.iface.mapCanvas().resizeEvent(QResizeEvent(QSize(400, 400), self.iface.mapCanvas().size())) + + def test_renderer(self): + path = os.path.join(unitTestDataPath(), + 'landsat.tif') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') + + renderer = QgsHillshadeRenderer(layer.dataProvider(), + 1, 90, 45) + + self.assertEqual(renderer.azimuth(), 90) + self.assertEqual(renderer.altitude(), 45) + self.assertEqual(renderer.inputBand(), 1) + + self.assertFalse(renderer.setInputBand(0)) + self.assertEqual(renderer.inputBand(), 1) + self.assertFalse(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 1) + self.assertTrue(renderer.setInputBand(2)) + self.assertEqual(renderer.inputBand(), 2) + + def test_hillshade_invalid_layer(self): + """ + Test hillshade raster render band with a broken layer path + """ + renderer = QgsHillshadeRenderer(None, + 1, 90, 45) + + self.assertEqual(renderer.azimuth(), 90) + self.assertEqual(renderer.altitude(), 45) + self.assertEqual(renderer.inputBand(), 1) + + # the renderer input is broken, we don't know what bands are valid, so all should be accepted + self.assertTrue(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 10) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgspalettedrasterrenderer.py b/tests/src/python/test_qgspalettedrasterrenderer.py index f7f629f3e215..9d9403bf0c7a 100644 --- a/tests/src/python/test_qgspalettedrasterrenderer.py +++ b/tests/src/python/test_qgspalettedrasterrenderer.py @@ -9,9 +9,7 @@ (at your option) any later version. """ -import filecmp import os -from shutil import copyfile import numpy as np from osgeo import gdal @@ -20,35 +18,13 @@ from qgis.PyQt.QtXml import QDomDocument from qgis.core import ( Qgis, - QgsBilinearRasterResampler, QgsColorRampShader, - QgsContrastEnhancement, - QgsCoordinateReferenceSystem, - QgsCoordinateTransformContext, - QgsCubicRasterResampler, - QgsDataProvider, - QgsExpressionContext, - QgsExpressionContextScope, QgsGradientColorRamp, - QgsHueSaturationFilter, - QgsLayerDefinition, QgsLimitedRandomColorRamp, - QgsMapLayerServerProperties, QgsMapSettings, QgsPalettedRasterRenderer, - QgsPointXY, QgsProject, - QgsProperty, - QgsRaster, - QgsRasterHistogram, - QgsRasterLayer, - QgsRasterMinMaxOrigin, - QgsRasterPipe, - QgsRasterShader, - QgsRasterTransparency, - QgsReadWriteContext, - QgsSingleBandGrayRenderer, - QgsSingleBandPseudoColorRenderer, + QgsRasterLayer ) import unittest from qgis.testing import start_app, QgisTestCase From da79086fef502c8620cfb31cd2528abf9e34a2db Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Mar 2024 10:47:15 +1000 Subject: [PATCH 21/68] Test input band logic for contour renderer --- tests/src/python/CMakeLists.txt | 1 + tests/src/python/test_qgshillshaderenderer.py | 13 +--- .../python/test_qgspalettedrasterrenderer.py | 14 +--- .../python/test_qgsrastercontourrenderer.py | 64 +++++++++++++++++++ 4 files changed, 68 insertions(+), 24 deletions(-) create mode 100644 tests/src/python/test_qgsrastercontourrenderer.py diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index b894e73ece9a..b48fae06f341 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -253,6 +253,7 @@ ADD_PYTHON_TEST(PyQgsProviderSublayerModel test_qgsprovidersublayermodel.py) ADD_PYTHON_TEST(TestQgsRandomMarkerSymbolLayer test_qgsrandommarkersymbollayer.py) ADD_PYTHON_TEST(PyQgsRange test_qgsrange.py) ADD_PYTHON_TEST(PyQgsRasterAttributeTable test_qgsrasterattributetable.py) +ADD_PYTHON_TEST(PyQgsRasterContourRenderer test_qgsrastercontourrenderer.py) ADD_PYTHON_TEST(PyQgsRasterFileWriter test_qgsrasterfilewriter.py) ADD_PYTHON_TEST(PyQgsRasterFileWriterTask test_qgsrasterfilewritertask.py) ADD_PYTHON_TEST(PyQgsRasterLayer test_qgsrasterlayer.py) diff --git a/tests/src/python/test_qgshillshaderenderer.py b/tests/src/python/test_qgshillshaderenderer.py index 83d39fad5344..2998cc69876f 100644 --- a/tests/src/python/test_qgshillshaderenderer.py +++ b/tests/src/python/test_qgshillshaderenderer.py @@ -11,16 +11,13 @@ import os -from qgis.PyQt.QtCore import QFileInfo, QSize -from qgis.PyQt.QtGui import QResizeEvent +from qgis.PyQt.QtCore import QFileInfo from qgis.core import ( QgsHillshadeRenderer, - QgsProject, QgsRasterLayer, ) import unittest from qgis.testing import start_app, QgisTestCase -from qgis.testing.mocked import get_iface from utilities import unitTestDataPath @@ -31,14 +28,6 @@ class TestQgsHillshadeRenderer(QgisTestCase): - def setUp(self): - self.iface = get_iface() - QgsProject.instance().removeAllMapLayers() - - self.iface.mapCanvas().viewport().resize(400, 400) - # For some reason the resizeEvent is not delivered, fake it - self.iface.mapCanvas().resizeEvent(QResizeEvent(QSize(400, 400), self.iface.mapCanvas().size())) - def test_renderer(self): path = os.path.join(unitTestDataPath(), 'landsat.tif') diff --git a/tests/src/python/test_qgspalettedrasterrenderer.py b/tests/src/python/test_qgspalettedrasterrenderer.py index 9d9403bf0c7a..eec66491e060 100644 --- a/tests/src/python/test_qgspalettedrasterrenderer.py +++ b/tests/src/python/test_qgspalettedrasterrenderer.py @@ -13,8 +13,8 @@ import numpy as np from osgeo import gdal -from qgis.PyQt.QtCore import QFileInfo, QSize, QTemporaryDir -from qgis.PyQt.QtGui import QColor, QResizeEvent +from qgis.PyQt.QtCore import QFileInfo, QTemporaryDir +from qgis.PyQt.QtGui import QColor from qgis.PyQt.QtXml import QDomDocument from qgis.core import ( Qgis, @@ -23,12 +23,10 @@ QgsLimitedRandomColorRamp, QgsMapSettings, QgsPalettedRasterRenderer, - QgsProject, QgsRasterLayer ) import unittest from qgis.testing import start_app, QgisTestCase -from qgis.testing.mocked import get_iface from utilities import unitTestDataPath @@ -39,14 +37,6 @@ class TestQgsPalettedRasterRenderer(QgisTestCase): - def setUp(self): - self.iface = get_iface() - QgsProject.instance().removeAllMapLayers() - - self.iface.mapCanvas().viewport().resize(400, 400) - # For some reason the resizeEvent is not delivered, fake it - self.iface.mapCanvas().resizeEvent(QResizeEvent(QSize(400, 400), self.iface.mapCanvas().size())) - def testPaletted(self): """ test paletted raster renderer with raster with color table""" path = os.path.join(unitTestDataPath('raster'), diff --git a/tests/src/python/test_qgsrastercontourrenderer.py b/tests/src/python/test_qgsrastercontourrenderer.py new file mode 100644 index 000000000000..a5e7f0441fcf --- /dev/null +++ b/tests/src/python/test_qgsrastercontourrenderer.py @@ -0,0 +1,64 @@ +"""QGIS Unit tests for QgsRasterContourRenderer + +From build dir, run: +ctest -R PyQgsRasterContourRenderer -V + +.. note:: 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. +""" + +import os + +from qgis.PyQt.QtCore import QFileInfo +from qgis.core import ( + QgsRasterContourRenderer, + QgsRasterLayer, +) +import unittest +from qgis.testing import start_app, QgisTestCase + +from utilities import unitTestDataPath + +# Convenience instances in case you may need them +# not used in this test +start_app() + + +class TestQgsRasterContourRenderer(QgisTestCase): + + def test_renderer(self): + path = os.path.join(unitTestDataPath(), + 'landsat.tif') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') + + renderer = QgsRasterContourRenderer(layer.dataProvider()) + + self.assertEqual(renderer.inputBand(), 1) + + self.assertFalse(renderer.setInputBand(0)) + self.assertEqual(renderer.inputBand(), 1) + self.assertFalse(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 1) + self.assertTrue(renderer.setInputBand(2)) + self.assertEqual(renderer.inputBand(), 2) + + def test_contour_invalid_layer(self): + """ + Test contour raster render band with a broken layer path + """ + renderer = QgsRasterContourRenderer(None) + + self.assertEqual(renderer.inputBand(), 1) + + # the renderer input is broken, we don't know what bands are valid, so all should be accepted + self.assertTrue(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 10) + + +if __name__ == '__main__': + unittest.main() From ac0b0a55c52a40ac3742f58bde60ebf3ea07ceb3 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Mar 2024 10:49:56 +1000 Subject: [PATCH 22/68] Add some tests for QgsSingleBandColorDataRenderer --- tests/src/python/CMakeLists.txt | 1 + .../test_qgssinglebandcolordatarenderer.py | 64 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 tests/src/python/test_qgssinglebandcolordatarenderer.py diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index b48fae06f341..24c0229e415e 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -282,6 +282,7 @@ ADD_PYTHON_TEST(PyQgsRenderer test_qgsrenderer.py) ADD_PYTHON_TEST(PyQgsReport test_qgsreport.py) ADD_PYTHON_TEST(PyQgsScaleBarRendererRegistry test_qgsscalebarrendererregistry.py) ADD_PYTHON_TEST(PyQgsScaleCalculator test_qgsscalecalculator.py) +ADD_PYTHON_TEST(PyQgsSingleBandColorDataRenderer test_qgssinglebandcolordatarenderer.py) ADD_PYTHON_TEST(PyQgsSingleSymbolRenderer test_qgssinglesymbolrenderer.py) ADD_PYTHON_TEST(PyQgsShapefileProvider test_provider_shapefile.py) ADD_PYTHON_TEST(PyQgsSphere test_qgssphere.py) diff --git a/tests/src/python/test_qgssinglebandcolordatarenderer.py b/tests/src/python/test_qgssinglebandcolordatarenderer.py new file mode 100644 index 000000000000..2c31e7b1cf8e --- /dev/null +++ b/tests/src/python/test_qgssinglebandcolordatarenderer.py @@ -0,0 +1,64 @@ +"""QGIS Unit tests for QgsSingleBandColorDataRenderer + +From build dir, run: +ctest -R PyQgsSingleBandColorDataRenderer -V + +.. note:: 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. +""" + +import os + +from qgis.PyQt.QtCore import QFileInfo +from qgis.core import ( + QgsSingleBandColorDataRenderer, + QgsRasterLayer, +) +import unittest +from qgis.testing import start_app, QgisTestCase + +from utilities import unitTestDataPath + +# Convenience instances in case you may need them +# not used in this test +start_app() + + +class TestQgsSingleBandColorDataRenderer(QgisTestCase): + + def test_renderer(self): + path = os.path.join(unitTestDataPath(), + 'landsat.tif') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') + + renderer = QgsSingleBandColorDataRenderer(layer.dataProvider(), 1) + + self.assertEqual(renderer.inputBand(), 1) + + self.assertFalse(renderer.setInputBand(0)) + self.assertEqual(renderer.inputBand(), 1) + self.assertFalse(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 1) + self.assertTrue(renderer.setInputBand(2)) + self.assertEqual(renderer.inputBand(), 2) + + def test_contour_invalid_layer(self): + """ + Test contour raster render band with a broken layer path + """ + renderer = QgsSingleBandColorDataRenderer(None, 5) + + self.assertEqual(renderer.inputBand(), 5) + + # the renderer input is broken, we don't know what bands are valid, so all should be accepted + self.assertTrue(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 10) + + +if __name__ == '__main__': + unittest.main() From 3b8d0b70c999cfcf6facb7f70299da4dc046cf76 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Mar 2024 10:53:48 +1000 Subject: [PATCH 23/68] Add tests for singleband gray renderer --- tests/src/python/CMakeLists.txt | 1 + .../test_qgssinglebandcolordatarenderer.py | 4 +- .../python/test_qgssinglebandgrayrenderer.py | 66 +++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 tests/src/python/test_qgssinglebandgrayrenderer.py diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 24c0229e415e..09c38fbc3286 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -283,6 +283,7 @@ ADD_PYTHON_TEST(PyQgsReport test_qgsreport.py) ADD_PYTHON_TEST(PyQgsScaleBarRendererRegistry test_qgsscalebarrendererregistry.py) ADD_PYTHON_TEST(PyQgsScaleCalculator test_qgsscalecalculator.py) ADD_PYTHON_TEST(PyQgsSingleBandColorDataRenderer test_qgssinglebandcolordatarenderer.py) +ADD_PYTHON_TEST(PyQgsSingleBandGrayRenderer test_qgssinglebandgrayrenderer.py) ADD_PYTHON_TEST(PyQgsSingleSymbolRenderer test_qgssinglesymbolrenderer.py) ADD_PYTHON_TEST(PyQgsShapefileProvider test_provider_shapefile.py) ADD_PYTHON_TEST(PyQgsSphere test_qgssphere.py) diff --git a/tests/src/python/test_qgssinglebandcolordatarenderer.py b/tests/src/python/test_qgssinglebandcolordatarenderer.py index 2c31e7b1cf8e..42fd44063c00 100644 --- a/tests/src/python/test_qgssinglebandcolordatarenderer.py +++ b/tests/src/python/test_qgssinglebandcolordatarenderer.py @@ -47,9 +47,9 @@ def test_renderer(self): self.assertTrue(renderer.setInputBand(2)) self.assertEqual(renderer.inputBand(), 2) - def test_contour_invalid_layer(self): + def test_singleband_invalid_layer(self): """ - Test contour raster render band with a broken layer path + Test singleband raster render band with a broken layer path """ renderer = QgsSingleBandColorDataRenderer(None, 5) diff --git a/tests/src/python/test_qgssinglebandgrayrenderer.py b/tests/src/python/test_qgssinglebandgrayrenderer.py new file mode 100644 index 000000000000..e82e1335492e --- /dev/null +++ b/tests/src/python/test_qgssinglebandgrayrenderer.py @@ -0,0 +1,66 @@ +"""QGIS Unit tests for QgsSingleBandGrayRenderer. + +From build dir, run: +ctest -R PyQgsSingleBandGrayRenderer -V + +.. note:: 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. +""" + +import os + +from qgis.PyQt.QtCore import QFileInfo +from qgis.core import ( + QgsRasterLayer, + QgsSingleBandGrayRenderer, +) +import unittest +from qgis.testing import start_app, QgisTestCase + +from utilities import unitTestDataPath + +# Convenience instances in case you may need them +# not used in this test +start_app() + + +class TestQgsSingleBandGrayRenderer(QgisTestCase): + + def test_renderer(self): + path = os.path.join(unitTestDataPath(), + 'landsat.tif') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') + + renderer = QgsSingleBandGrayRenderer(layer.dataProvider(), + 1) + + self.assertEqual(renderer.inputBand(), 1) + + self.assertFalse(renderer.setInputBand(0)) + self.assertEqual(renderer.inputBand(), 1) + self.assertFalse(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 1) + self.assertTrue(renderer.setInputBand(2)) + self.assertEqual(renderer.inputBand(), 2) + + def test_invalid_layer(self): + """ + Test gray renderer band with a broken layer path + """ + renderer = QgsSingleBandGrayRenderer(None, + 11) + + self.assertEqual(renderer.inputBand(), 11) + + # the renderer input is broken, we don't know what bands are valid, so all should be accepted + self.assertTrue(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 10) + + +if __name__ == '__main__': + unittest.main() From 2b7e1c531050e63186ca91f111c0f0a25f620619 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Mar 2024 10:58:08 +1000 Subject: [PATCH 24/68] Add unit tests for psuedo color renderer --- tests/src/python/CMakeLists.txt | 1 + .../test_qgssinglebandpseudocolorrenderer.py | 66 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 tests/src/python/test_qgssinglebandpseudocolorrenderer.py diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 09c38fbc3286..2943264d74ee 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -284,6 +284,7 @@ ADD_PYTHON_TEST(PyQgsScaleBarRendererRegistry test_qgsscalebarrendererregistry.p ADD_PYTHON_TEST(PyQgsScaleCalculator test_qgsscalecalculator.py) ADD_PYTHON_TEST(PyQgsSingleBandColorDataRenderer test_qgssinglebandcolordatarenderer.py) ADD_PYTHON_TEST(PyQgsSingleBandGrayRenderer test_qgssinglebandgrayrenderer.py) +ADD_PYTHON_TEST(PyQgsSingleBandPseudoColorRenderer test_qgssinglebandpseudocolorrenderer.py) ADD_PYTHON_TEST(PyQgsSingleSymbolRenderer test_qgssinglesymbolrenderer.py) ADD_PYTHON_TEST(PyQgsShapefileProvider test_provider_shapefile.py) ADD_PYTHON_TEST(PyQgsSphere test_qgssphere.py) diff --git a/tests/src/python/test_qgssinglebandpseudocolorrenderer.py b/tests/src/python/test_qgssinglebandpseudocolorrenderer.py new file mode 100644 index 000000000000..98c7a95bbc6e --- /dev/null +++ b/tests/src/python/test_qgssinglebandpseudocolorrenderer.py @@ -0,0 +1,66 @@ +"""QGIS Unit tests for QgsSingleBandPseudoColorRenderer. + +From build dir, run: +ctest -R PyQgsSingleBandPseudoColorRenderer -V + +.. note:: 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. +""" + +import os + +from qgis.PyQt.QtCore import QFileInfo +from qgis.core import ( + QgsRasterLayer, + QgsSingleBandPseudoColorRenderer, +) +import unittest +from qgis.testing import start_app, QgisTestCase + +from utilities import unitTestDataPath + +# Convenience instances in case you may need them +# not used in this test +start_app() + + +class TestQgsSingleBandPseudoColorRenderer(QgisTestCase): + + def test_renderer(self): + path = os.path.join(unitTestDataPath(), + 'landsat.tif') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') + + renderer = QgsSingleBandPseudoColorRenderer(layer.dataProvider(), + 1) + + self.assertEqual(renderer.inputBand(), 1) + + self.assertFalse(renderer.setInputBand(0)) + self.assertEqual(renderer.inputBand(), 1) + self.assertFalse(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 1) + self.assertTrue(renderer.setInputBand(2)) + self.assertEqual(renderer.inputBand(), 2) + + def test_invalid_layer(self): + """ + Test renderer band with a broken layer path + """ + renderer = QgsSingleBandPseudoColorRenderer(None, + 11) + + self.assertEqual(renderer.inputBand(), 11) + + # the renderer input is broken, we don't know what bands are valid, so all should be accepted + self.assertTrue(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 10) + + +if __name__ == '__main__': + unittest.main() From 253623feb08f2048bdfe265acf5f9ad458fc1898 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 14 Mar 2024 10:27:53 +1000 Subject: [PATCH 25/68] Introduce fixed elevation range for raster layers This introduces a new option for specifying how raster layers have associated elevation. It permits a fixed elevation range to be set for the layer. It can be used when the layer has a single fixed elevation, or a range (slice) of elevation values. Users can set the lower and upper elevation range for the layer, and whether the lower or upper limits are inclusive or exclusive. When enabled, the layer will only be visible in elevation filtered 2d maps when the layer's range is included in the map's z range. --- python/PyQt6/core/auto_additions/qgis.py | 14 + python/PyQt6/core/auto_generated/qgis.sip.in | 14 + .../qgsrasterlayerelevationproperties.sip.in | 68 +++- python/core/auto_additions/qgis.py | 14 + python/core/auto_generated/qgis.sip.in | 14 + .../qgsrasterlayerelevationproperties.sip.in | 68 +++- .../processing/pdal/qgspdalalgorithmbase.cpp | 1 + src/app/layers/qgsapplayerhandling.cpp | 1 + .../qgsprojectelevationsettingswidget.cpp | 1 + .../qgsrasterelevationpropertieswidget.cpp | 122 ++++++- .../qgsrasterelevationpropertieswidget.h | 1 + src/core/qgis.h | 26 ++ src/core/qgselevationutils.cpp | 1 + .../qgsrasterlayerelevationproperties.cpp | 128 +++++++- .../qgsrasterlayerelevationproperties.h | 55 +++- src/core/raster/qgsrasterlayerrenderer.cpp | 54 ++-- src/gui/qgsmaptoolidentify.cpp | 4 +- .../qgsrasterelevationpropertieswidgetbase.ui | 304 +++++++++++++----- .../test_qgsrasterlayerelevationproperties.py | 103 +++++- .../src/python/test_qgsrasterlayerrenderer.py | 52 +++ ...xpected_fixed_elevation_range_excluded.png | Bin 0 -> 471523 bytes ...xpected_fixed_elevation_range_included.png | Bin 0 -> 471523 bytes 22 files changed, 895 insertions(+), 150 deletions(-) create mode 100644 tests/testdata/control_images/rasterlayerrenderer/expected_fixed_elevation_range_excluded/expected_fixed_elevation_range_excluded.png create mode 100644 tests/testdata/control_images/rasterlayerrenderer/expected_fixed_elevation_range_included/expected_fixed_elevation_range_included.png diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index 9a02f80754b0..1725fc3d4ac2 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -3220,6 +3220,20 @@ # -- Qgis.AltitudeBinding.baseClass = Qgis # monkey patching scoped based enum +Qgis.RangeLimits.IncludeBoth.__doc__ = "Both lower and upper values are included in the range" +Qgis.RangeLimits.IncludeLowerExcludeUpper.__doc__ = "Lower value is included in the range, upper value is excluded" +Qgis.RangeLimits.ExcludeLowerIncludeUpper.__doc__ = "Lower value is excluded from the range, upper value in inccluded" +Qgis.RangeLimits.ExcludeBoth.__doc__ = "Both lower and upper values are excluded from the range" +Qgis.RangeLimits.__doc__ = "Describes how the limits of a range are handled.\n\n.. versionadded:: 3.38\n\n" + '* ``IncludeBoth``: ' + Qgis.RangeLimits.IncludeBoth.__doc__ + '\n' + '* ``IncludeLowerExcludeUpper``: ' + Qgis.RangeLimits.IncludeLowerExcludeUpper.__doc__ + '\n' + '* ``ExcludeLowerIncludeUpper``: ' + Qgis.RangeLimits.ExcludeLowerIncludeUpper.__doc__ + '\n' + '* ``ExcludeBoth``: ' + Qgis.RangeLimits.ExcludeBoth.__doc__ +# -- +Qgis.RangeLimits.baseClass = Qgis +# monkey patching scoped based enum +Qgis.RasterElevationMode.FixedElevationRange.__doc__ = "Layer has a fixed elevation range" +Qgis.RasterElevationMode.RepresentsElevationSurface.__doc__ = "Pixel values represent an elevation surface" +Qgis.RasterElevationMode.__doc__ = "Raster layer elevation modes.\n\n.. versionadded:: 3.38\n\n" + '* ``FixedElevationRange``: ' + Qgis.RasterElevationMode.FixedElevationRange.__doc__ + '\n' + '* ``RepresentsElevationSurface``: ' + Qgis.RasterElevationMode.RepresentsElevationSurface.__doc__ +# -- +Qgis.RasterElevationMode.baseClass = Qgis +# monkey patching scoped based enum Qgis.NoConstraint = Qgis.BetweenLineConstraint.NoConstraint Qgis.NoConstraint.is_monkey_patched = True Qgis.BetweenLineConstraint.NoConstraint.__doc__ = "No additional constraint" diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index 45ca6d95cfa7..bac3daa96e7f 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -1835,6 +1835,20 @@ The development version Centroid, }; + enum class RangeLimits /BaseType=IntEnum/ + { + IncludeBoth, + IncludeLowerExcludeUpper, + ExcludeLowerIncludeUpper, + ExcludeBoth, + }; + + enum class RasterElevationMode /BaseType=IntEnum/ + { + FixedElevationRange, + RepresentsElevationSurface + }; + enum class BetweenLineConstraint /BaseType=IntEnum/ { NoConstraint, diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in index ef87a71a957f..17ae8f8bbc95 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in @@ -59,12 +59,34 @@ Returns ``True`` if the elevation properties are enabled, i.e. the raster layer Sets whether the elevation properties are enabled, i.e. the raster layer values represent an elevation surface. .. seealso:: :py:func:`isEnabled` +%End + + Qgis::RasterElevationMode mode() const; +%Docstring +Returns the elevation mode. + +.. seealso:: :py:func:`setMode` + +.. versionadded:: 3.38 +%End + + void setMode( Qgis::RasterElevationMode mode ); +%Docstring +Sets the elevation ``mode``. + +.. seealso:: :py:func:`mode` + +.. versionadded:: 3.38 %End int bandNumber() const; %Docstring Returns the band number from which the elevation should be taken. +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.RepresentsElevationSurface. + .. seealso:: :py:func:`setBandNumber` %End @@ -72,14 +94,54 @@ Returns the band number from which the elevation should be taken. %Docstring Sets the ``band`` number from which the elevation should be taken. +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.RepresentsElevationSurface. + .. seealso:: :py:func:`bandNumber` %End - double elevationForPixelValue( int band, double pixelValue ) const; + QgsDoubleRange fixedRange() const; +%Docstring +Returns the fixed elevation range for the raster. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.FixedElevationRange. + +.. note:: + + When a fixed range is set any :py:func:`~QgsRasterLayerElevationProperties.zOffset` and :py:func:`~QgsRasterLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`setFixedRange` + +.. versionadded:: 3.38 +%End + + void setFixedRange( const QgsDoubleRange &range ); +%Docstring +Sets the fixed elevation ``range`` for the raster. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.FixedElevationRange. + +.. note:: + + When a fixed range is set any :py:func:`~QgsRasterLayerElevationProperties.zOffset` and :py:func:`~QgsRasterLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`fixedRange` + +.. versionadded:: 3.38 +%End + + QgsDoubleRange elevationRangeForPixelValue( int band, double pixelValue ) const; %Docstring -Returns the elevation corresponding to a raw pixel value from the specified ``band``. +Returns the elevation range corresponding to a raw pixel value from the specified ``band``. -Returns NaN if the pixel value does not correspond to an elevation value. +Returns an infinite range if the pixel value does not correspond to an elevation value. .. versionadded:: 3.38 %End diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index 2f54e05edff5..d63cad47d8c3 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -3166,6 +3166,20 @@ # -- Qgis.AltitudeBinding.baseClass = Qgis # monkey patching scoped based enum +Qgis.RangeLimits.IncludeBoth.__doc__ = "Both lower and upper values are included in the range" +Qgis.RangeLimits.IncludeLowerExcludeUpper.__doc__ = "Lower value is included in the range, upper value is excluded" +Qgis.RangeLimits.ExcludeLowerIncludeUpper.__doc__ = "Lower value is excluded from the range, upper value in inccluded" +Qgis.RangeLimits.ExcludeBoth.__doc__ = "Both lower and upper values are excluded from the range" +Qgis.RangeLimits.__doc__ = "Describes how the limits of a range are handled.\n\n.. versionadded:: 3.38\n\n" + '* ``IncludeBoth``: ' + Qgis.RangeLimits.IncludeBoth.__doc__ + '\n' + '* ``IncludeLowerExcludeUpper``: ' + Qgis.RangeLimits.IncludeLowerExcludeUpper.__doc__ + '\n' + '* ``ExcludeLowerIncludeUpper``: ' + Qgis.RangeLimits.ExcludeLowerIncludeUpper.__doc__ + '\n' + '* ``ExcludeBoth``: ' + Qgis.RangeLimits.ExcludeBoth.__doc__ +# -- +Qgis.RangeLimits.baseClass = Qgis +# monkey patching scoped based enum +Qgis.RasterElevationMode.FixedElevationRange.__doc__ = "Layer has a fixed elevation range" +Qgis.RasterElevationMode.RepresentsElevationSurface.__doc__ = "Pixel values represent an elevation surface" +Qgis.RasterElevationMode.__doc__ = "Raster layer elevation modes.\n\n.. versionadded:: 3.38\n\n" + '* ``FixedElevationRange``: ' + Qgis.RasterElevationMode.FixedElevationRange.__doc__ + '\n' + '* ``RepresentsElevationSurface``: ' + Qgis.RasterElevationMode.RepresentsElevationSurface.__doc__ +# -- +Qgis.RasterElevationMode.baseClass = Qgis +# monkey patching scoped based enum Qgis.NoConstraint = Qgis.BetweenLineConstraint.NoConstraint Qgis.NoConstraint.is_monkey_patched = True Qgis.BetweenLineConstraint.NoConstraint.__doc__ = "No additional constraint" diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index 2f9db0588010..2a86b3d09d41 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -1835,6 +1835,20 @@ The development version Centroid, }; + enum class RangeLimits + { + IncludeBoth, + IncludeLowerExcludeUpper, + ExcludeLowerIncludeUpper, + ExcludeBoth, + }; + + enum class RasterElevationMode + { + FixedElevationRange, + RepresentsElevationSurface + }; + enum class BetweenLineConstraint { NoConstraint, diff --git a/python/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in b/python/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in index ef87a71a957f..17ae8f8bbc95 100644 --- a/python/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in +++ b/python/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in @@ -59,12 +59,34 @@ Returns ``True`` if the elevation properties are enabled, i.e. the raster layer Sets whether the elevation properties are enabled, i.e. the raster layer values represent an elevation surface. .. seealso:: :py:func:`isEnabled` +%End + + Qgis::RasterElevationMode mode() const; +%Docstring +Returns the elevation mode. + +.. seealso:: :py:func:`setMode` + +.. versionadded:: 3.38 +%End + + void setMode( Qgis::RasterElevationMode mode ); +%Docstring +Sets the elevation ``mode``. + +.. seealso:: :py:func:`mode` + +.. versionadded:: 3.38 %End int bandNumber() const; %Docstring Returns the band number from which the elevation should be taken. +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.RepresentsElevationSurface. + .. seealso:: :py:func:`setBandNumber` %End @@ -72,14 +94,54 @@ Returns the band number from which the elevation should be taken. %Docstring Sets the ``band`` number from which the elevation should be taken. +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.RepresentsElevationSurface. + .. seealso:: :py:func:`bandNumber` %End - double elevationForPixelValue( int band, double pixelValue ) const; + QgsDoubleRange fixedRange() const; +%Docstring +Returns the fixed elevation range for the raster. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.FixedElevationRange. + +.. note:: + + When a fixed range is set any :py:func:`~QgsRasterLayerElevationProperties.zOffset` and :py:func:`~QgsRasterLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`setFixedRange` + +.. versionadded:: 3.38 +%End + + void setFixedRange( const QgsDoubleRange &range ); +%Docstring +Sets the fixed elevation ``range`` for the raster. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.FixedElevationRange. + +.. note:: + + When a fixed range is set any :py:func:`~QgsRasterLayerElevationProperties.zOffset` and :py:func:`~QgsRasterLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`fixedRange` + +.. versionadded:: 3.38 +%End + + QgsDoubleRange elevationRangeForPixelValue( int band, double pixelValue ) const; %Docstring -Returns the elevation corresponding to a raw pixel value from the specified ``band``. +Returns the elevation range corresponding to a raw pixel value from the specified ``band``. -Returns NaN if the pixel value does not correspond to an elevation value. +Returns an infinite range if the pixel value does not correspond to an elevation value. .. versionadded:: 3.38 %End diff --git a/src/analysis/processing/pdal/qgspdalalgorithmbase.cpp b/src/analysis/processing/pdal/qgspdalalgorithmbase.cpp index e09bc46f5b58..2a79a7fd66e9 100644 --- a/src/analysis/processing/pdal/qgspdalalgorithmbase.cpp +++ b/src/analysis/processing/pdal/qgspdalalgorithmbase.cpp @@ -163,6 +163,7 @@ class EnableElevationPropertiesPostProcessor : public QgsProcessingLayerPostProc if ( QgsRasterLayer *rl = qobject_cast< QgsRasterLayer * >( layer ) ) { QgsRasterLayerElevationProperties *props = qgis::down_cast< QgsRasterLayerElevationProperties * >( rl->elevationProperties() ); + props->setMode( Qgis::RasterElevationMode::RepresentsElevationSurface ); props->setEnabled( true ); rl->trigger3DUpdate(); } diff --git a/src/app/layers/qgsapplayerhandling.cpp b/src/app/layers/qgsapplayerhandling.cpp index 4d8730c7d908..17ba4e979e18 100644 --- a/src/app/layers/qgsapplayerhandling.cpp +++ b/src/app/layers/qgsapplayerhandling.cpp @@ -109,6 +109,7 @@ void QgsAppLayerHandling::postProcessAddedLayer( QgsMapLayer *layer ) if ( QgsRasterLayerElevationProperties::layerLooksLikeDem( rasterLayer ) ) { qgis::down_cast< QgsRasterLayerElevationProperties * >( rasterLayer->elevationProperties() )->setEnabled( true ); + qgis::down_cast< QgsRasterLayerElevationProperties * >( rasterLayer->elevationProperties() )->setMode( Qgis::RasterElevationMode::RepresentsElevationSurface ); } break; diff --git a/src/app/project/qgsprojectelevationsettingswidget.cpp b/src/app/project/qgsprojectelevationsettingswidget.cpp index 85883669457f..8688089a4a89 100644 --- a/src/app/project/qgsprojectelevationsettingswidget.cpp +++ b/src/app/project/qgsprojectelevationsettingswidget.cpp @@ -124,6 +124,7 @@ void QgsProjectElevationSettingsWidget::apply() QgsRasterLayer *demLayer = qobject_cast< QgsRasterLayer * >( mComboDemLayer->currentLayer() ); // always mark the terrain layer as a "dem" layer -- it seems odd for a user to have to manually set this after picking a terrain raster! qobject_cast< QgsRasterLayerElevationProperties * >( demLayer->elevationProperties() )->setEnabled( true ); + qobject_cast< QgsRasterLayerElevationProperties * >( demLayer->elevationProperties() )->setMode( Qgis::RasterElevationMode::RepresentsElevationSurface ); qgis::down_cast< QgsRasterDemTerrainProvider * >( provider.get() )->setLayer( demLayer ); } else if ( terrainType == QLatin1String( "mesh" ) ) diff --git a/src/app/raster/qgsrasterelevationpropertieswidget.cpp b/src/app/raster/qgsrasterelevationpropertieswidget.cpp index dbc9d652da76..67508c015d55 100644 --- a/src/app/raster/qgsrasterelevationpropertieswidget.cpp +++ b/src/app/raster/qgsrasterelevationpropertieswidget.cpp @@ -27,11 +27,25 @@ QgsRasterElevationPropertiesWidget::QgsRasterElevationPropertiesWidget( QgsRaste setupUi( this ); setObjectName( QStringLiteral( "mOptsPage_Elevation" ) ); + mModeComboBox->addItem( tr( "Disabled" ) ); + mModeComboBox->addItem( tr( "Represents Elevation Surface" ), QVariant::fromValue( Qgis::RasterElevationMode::RepresentsElevationSurface ) ); + mModeComboBox->addItem( tr( "Fixed Elevation Range" ), QVariant::fromValue( Qgis::RasterElevationMode::FixedElevationRange ) ); + + mLimitsComboBox->addItem( tr( "Include Lower and Upper" ), QVariant::fromValue( Qgis::RangeLimits::IncludeBoth ) ); + mLimitsComboBox->addItem( tr( "Include Lower, Exclude Upper" ), QVariant::fromValue( Qgis::RangeLimits::IncludeLowerExcludeUpper ) ); + mLimitsComboBox->addItem( tr( "Exclude Lower, Include Upper" ), QVariant::fromValue( Qgis::RangeLimits::ExcludeLowerIncludeUpper ) ); + mLimitsComboBox->addItem( tr( "Exclude Lower and Upper" ), QVariant::fromValue( Qgis::RangeLimits::ExcludeBoth ) ); + + mStackedWidget->setSizeMode( QgsStackedWidget::SizeMode::CurrentPageOnly ); mSymbologyStackedWidget->setSizeMode( QgsStackedWidget::SizeMode::CurrentPageOnly ); mOffsetZSpinBox->setClearValue( 0 ); mScaleZSpinBox->setClearValue( 1 ); - mElevationGroupBox->setChecked( false ); + mFixedLowerSpinBox->setClearValueMode( QgsDoubleSpinBox::ClearValueMode::MinimumValue, tr( "Not set" ) ); + mFixedUpperSpinBox->setClearValueMode( QgsDoubleSpinBox::ClearValueMode::MinimumValue, tr( "Not set" ) ); + mFixedLowerSpinBox->clear(); + mFixedUpperSpinBox->clear(); + mLineStyleButton->setSymbolType( Qgis::SymbolType::Line ); mFillStyleButton->setSymbolType( Qgis::SymbolType::Fill ); mStyleComboBox->addItem( QgsApplication::getThemeIcon( QStringLiteral( "mIconSurfaceElevationLine.svg" ) ), tr( "Line" ), static_cast< int >( Qgis::ProfileSurfaceSymbology::Line ) ); @@ -44,7 +58,8 @@ QgsRasterElevationPropertiesWidget::QgsRasterElevationPropertiesWidget( QgsRaste connect( mOffsetZSpinBox, qOverload( &QDoubleSpinBox::valueChanged ), this, &QgsRasterElevationPropertiesWidget::onChanged ); connect( mScaleZSpinBox, qOverload( &QDoubleSpinBox::valueChanged ), this, &QgsRasterElevationPropertiesWidget::onChanged ); connect( mElevationLimitSpinBox, qOverload( &QDoubleSpinBox::valueChanged ), this, &QgsRasterElevationPropertiesWidget::onChanged ); - connect( mElevationGroupBox, &QGroupBox::toggled, this, &QgsRasterElevationPropertiesWidget::onChanged ); + connect( mModeComboBox, qOverload( &QComboBox::currentIndexChanged ), this, &QgsRasterElevationPropertiesWidget::modeChanged ); + connect( mLimitsComboBox, qOverload( &QComboBox::currentIndexChanged ), this, &QgsRasterElevationPropertiesWidget::onChanged ); connect( mLineStyleButton, &QgsSymbolButton::changed, this, &QgsRasterElevationPropertiesWidget::onChanged ); connect( mFillStyleButton, &QgsSymbolButton::changed, this, &QgsRasterElevationPropertiesWidget::onChanged ); connect( mBandComboBox, &QgsRasterBandComboBox::bandChanged, this, &QgsRasterElevationPropertiesWidget::onChanged ); @@ -75,7 +90,27 @@ void QgsRasterElevationPropertiesWidget::syncToLayer( QgsMapLayer *layer ) mBlockUpdates = true; const QgsRasterLayerElevationProperties *props = qgis::down_cast< const QgsRasterLayerElevationProperties * >( mLayer->elevationProperties() ); - mElevationGroupBox->setChecked( props->isEnabled() ); + if ( !props->isEnabled() ) + { + mModeComboBox->setCurrentIndex( 0 ); + mStackedWidget->setCurrentWidget( mPageDisabled ); + mProfileChartGroupBox->hide(); + } + else + { + mModeComboBox->setCurrentIndex( mModeComboBox->findData( QVariant::fromValue( props->mode() ) ) ); + switch ( props->mode() ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + mStackedWidget->setCurrentWidget( mPageFixedRange ); + break; + case Qgis::RasterElevationMode::RepresentsElevationSurface: + mStackedWidget->setCurrentWidget( mPageSurface ); + break; + } + mProfileChartGroupBox->show(); + } + mOffsetZSpinBox->setValue( props->zOffset() ); mScaleZSpinBox->setValue( props->zScale() ); if ( std::isnan( props->elevationLimit() ) ) @@ -86,6 +121,24 @@ void QgsRasterElevationPropertiesWidget::syncToLayer( QgsMapLayer *layer ) mFillStyleButton->setSymbol( props->profileFillSymbol()->clone() ); mBandComboBox->setLayer( mLayer ); mBandComboBox->setBand( props->bandNumber() ); + + if ( props->fixedRange().lower() != std::numeric_limits< double >::lowest() ) + mFixedLowerSpinBox->setValue( props->fixedRange().lower() ); + else + mFixedLowerSpinBox->clear(); + if ( props->fixedRange().upper() != std::numeric_limits< double >::max() ) + mFixedUpperSpinBox->setValue( props->fixedRange().upper() ); + else + mFixedUpperSpinBox->clear(); + if ( props->fixedRange().includeLower() && props->fixedRange().includeUpper() ) + mLimitsComboBox->setCurrentIndex( mLimitsComboBox->findData( QVariant::fromValue( Qgis::RangeLimits::IncludeBoth ) ) ); + else if ( props->fixedRange().includeLower() ) + mLimitsComboBox->setCurrentIndex( mLimitsComboBox->findData( QVariant::fromValue( Qgis::RangeLimits::IncludeLowerExcludeUpper ) ) ); + else if ( props->fixedRange().includeUpper() ) + mLimitsComboBox->setCurrentIndex( mLimitsComboBox->findData( QVariant::fromValue( Qgis::RangeLimits::ExcludeLowerIncludeUpper ) ) ); + else + mLimitsComboBox->setCurrentIndex( mLimitsComboBox->findData( QVariant::fromValue( Qgis::RangeLimits::ExcludeBoth ) ) ); + mStyleComboBox->setCurrentIndex( mStyleComboBox->findData( static_cast ( props->profileSymbology() ) ) ); switch ( props->profileSymbology() ) { @@ -107,7 +160,17 @@ void QgsRasterElevationPropertiesWidget::apply() return; QgsRasterLayerElevationProperties *props = qgis::down_cast< QgsRasterLayerElevationProperties * >( mLayer->elevationProperties() ); - props->setEnabled( mElevationGroupBox->isChecked() ); + + if ( !mModeComboBox->currentData().isValid() ) + { + props->setEnabled( false ); + } + else + { + props->setEnabled( true ); + props->setMode( mModeComboBox->currentData().value< Qgis::RasterElevationMode >() ); + } + props->setZOffset( mOffsetZSpinBox->value() ); props->setZScale( mScaleZSpinBox->value() ); if ( mElevationLimitSpinBox->value() != mElevationLimitSpinBox->clearValue() ) @@ -118,9 +181,60 @@ void QgsRasterElevationPropertiesWidget::apply() props->setProfileFillSymbol( mFillStyleButton->clonedSymbol< QgsFillSymbol >() ); props->setProfileSymbology( static_cast< Qgis::ProfileSurfaceSymbology >( mStyleComboBox->currentData().toInt() ) ); props->setBandNumber( mBandComboBox->currentBand() ); + + double fixedLower = std::numeric_limits< double >::lowest(); + double fixedUpper = std::numeric_limits< double >::max(); + if ( mFixedLowerSpinBox->value() != mFixedLowerSpinBox->clearValue() ) + fixedLower = mFixedLowerSpinBox->value(); + if ( mFixedUpperSpinBox->value() != mFixedUpperSpinBox->clearValue() ) + fixedUpper = mFixedUpperSpinBox->value(); + + bool includeLower = true; + bool includeUpper = true; + switch ( mLimitsComboBox->currentData().value< Qgis::RangeLimits >() ) + { + case Qgis::RangeLimits::IncludeBoth: + break; + case Qgis::RangeLimits::IncludeLowerExcludeUpper: + includeUpper = false; + break; + case Qgis::RangeLimits::ExcludeLowerIncludeUpper: + includeLower = false; + break; + case Qgis::RangeLimits::ExcludeBoth: + includeLower = false; + includeUpper = false; + break; + } + props->setFixedRange( QgsDoubleRange( fixedLower, fixedUpper, includeLower, includeUpper ) ); + mLayer->trigger3DUpdate(); } +void QgsRasterElevationPropertiesWidget::modeChanged() +{ + if ( mModeComboBox->currentData().isValid() ) + { + switch ( mModeComboBox->currentData().value< Qgis::RasterElevationMode >() ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + mStackedWidget->setCurrentWidget( mPageFixedRange ); + break; + case Qgis::RasterElevationMode::RepresentsElevationSurface: + mStackedWidget->setCurrentWidget( mPageSurface ); + break; + } + mProfileChartGroupBox->show(); + } + else + { + mStackedWidget->setCurrentWidget( mPageDisabled ); + mProfileChartGroupBox->hide(); + } + + onChanged(); +} + void QgsRasterElevationPropertiesWidget::onChanged() { if ( !mBlockUpdates ) diff --git a/src/app/raster/qgsrasterelevationpropertieswidget.h b/src/app/raster/qgsrasterelevationpropertieswidget.h index c3d2b4dfebc9..f634b4f489df 100644 --- a/src/app/raster/qgsrasterelevationpropertieswidget.h +++ b/src/app/raster/qgsrasterelevationpropertieswidget.h @@ -37,6 +37,7 @@ class QgsRasterElevationPropertiesWidget : public QgsMapLayerConfigWidget, priva private slots: + void modeChanged(); void onChanged(); private: diff --git a/src/core/qgis.h b/src/core/qgis.h index 31763f173ad7..815aa1382fe4 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -3234,6 +3234,32 @@ class CORE_EXPORT Qgis }; Q_ENUM( AltitudeBinding ) + /** + * Describes how the limits of a range are handled. + * + * \since QGIS 3.38 + */ + enum class RangeLimits : int + { + IncludeBoth = 0, //!< Both lower and upper values are included in the range + IncludeLowerExcludeUpper, //!< Lower value is included in the range, upper value is excluded + ExcludeLowerIncludeUpper, //!< Lower value is excluded from the range, upper value in inccluded + ExcludeBoth, //!< Both lower and upper values are excluded from the range + }; + Q_ENUM( RangeLimits ) + + /** + * Raster layer elevation modes. + * + * \since QGIS 3.38 + */ + enum class RasterElevationMode : int + { + FixedElevationRange = 0, //!< Layer has a fixed elevation range + RepresentsElevationSurface = 1 //!< Pixel values represent an elevation surface + }; + Q_ENUM( RasterElevationMode ) + /** * Between line constraints which can be enabled * diff --git a/src/core/qgselevationutils.cpp b/src/core/qgselevationutils.cpp index 6870075a5881..8370b3458a15 100644 --- a/src/core/qgselevationutils.cpp +++ b/src/core/qgselevationutils.cpp @@ -68,6 +68,7 @@ bool QgsElevationUtils::enableElevationForLayer( QgsMapLayer *layer ) if ( QgsRasterLayerElevationProperties *properties = qobject_cast( layer->elevationProperties() ) ) { properties->setEnabled( true ); + properties->setMode( Qgis::RasterElevationMode::RepresentsElevationSurface ); // This could potentially be made smarter, eg by checking the data type of bands. But that's likely overkill..! properties->setBandNumber( 1 ); return true; diff --git a/src/core/raster/qgsrasterlayerelevationproperties.cpp b/src/core/raster/qgsrasterlayerelevationproperties.cpp index ae04921f064b..a9a42cae727d 100644 --- a/src/core/raster/qgsrasterlayerelevationproperties.cpp +++ b/src/core/raster/qgsrasterlayerelevationproperties.cpp @@ -44,12 +44,26 @@ QDomElement QgsRasterLayerElevationProperties::writeXml( QDomElement &parentElem { QDomElement element = document.createElement( QStringLiteral( "elevation" ) ); element.setAttribute( QStringLiteral( "enabled" ), mEnabled ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + element.setAttribute( QStringLiteral( "mode" ), qgsEnumValueToKey( mMode ) ); element.setAttribute( QStringLiteral( "symbology" ), qgsEnumValueToKey( mSymbology ) ); if ( !std::isnan( mElevationLimit ) ) element.setAttribute( QStringLiteral( "elevationLimit" ), qgsDoubleToString( mElevationLimit ) ); writeCommonProperties( element, document, context ); - element.setAttribute( QStringLiteral( "band" ), mBandNumber ); + + switch ( mMode ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + element.setAttribute( QStringLiteral( "lower" ), qgsDoubleToString( mFixedRange.lower() ) ); + element.setAttribute( QStringLiteral( "upper" ), qgsDoubleToString( mFixedRange.upper() ) ); + element.setAttribute( QStringLiteral( "includeLower" ), mFixedRange.includeLower() ? "1" : "0" ); + element.setAttribute( QStringLiteral( "includeUpper" ), mFixedRange.includeUpper() ? "1" : "0" ); + break; + + case Qgis::RasterElevationMode::RepresentsElevationSurface: + element.setAttribute( QStringLiteral( "band" ), mBandNumber ); + break; + } QDomElement profileLineSymbolElement = document.createElement( QStringLiteral( "profileLineSymbol" ) ); profileLineSymbolElement.appendChild( QgsSymbolLayerUtils::saveSymbol( QString(), mProfileLineSymbol.get(), document, context ) ); @@ -67,6 +81,7 @@ bool QgsRasterLayerElevationProperties::readXml( const QDomElement &element, con { const QDomElement elevationElement = element.firstChildElement( QStringLiteral( "elevation" ) ).toElement(); mEnabled = elevationElement.attribute( QStringLiteral( "enabled" ), QStringLiteral( "0" ) ).toInt(); + mMode = qgsEnumKeyToValue( elevationElement.attribute( QStringLiteral( "mode" ) ), Qgis::RasterElevationMode::RepresentsElevationSurface ); mSymbology = qgsEnumKeyToValue( elevationElement.attribute( QStringLiteral( "symbology" ) ), Qgis::ProfileSurfaceSymbology::Line ); if ( elevationElement.hasAttribute( QStringLiteral( "elevationLimit" ) ) ) mElevationLimit = elevationElement.attribute( QStringLiteral( "elevationLimit" ) ).toDouble(); @@ -74,7 +89,22 @@ bool QgsRasterLayerElevationProperties::readXml( const QDomElement &element, con mElevationLimit = std::numeric_limits< double >::quiet_NaN(); readCommonProperties( elevationElement, context ); - mBandNumber = elevationElement.attribute( QStringLiteral( "band" ), QStringLiteral( "1" ) ).toInt(); + + switch ( mMode ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + { + const double lower = elevationElement.attribute( QStringLiteral( "lower" ) ).toDouble(); + const double upper = elevationElement.attribute( QStringLiteral( "upper" ) ).toDouble(); + const bool includeLower = elevationElement.attribute( QStringLiteral( "includeLower" ) ).toInt(); + const bool includeUpper = elevationElement.attribute( QStringLiteral( "includeUpper" ) ).toInt(); + mFixedRange = QgsDoubleRange( lower, upper, includeLower, includeUpper ); + break; + } + case Qgis::RasterElevationMode::RepresentsElevationSurface: + mBandNumber = elevationElement.attribute( QStringLiteral( "band" ), QStringLiteral( "1" ) ).toInt(); + break; + } const QColor defaultColor = QgsApplication::colorSchemeRegistry()->fetchRandomStyleColor(); @@ -95,11 +125,13 @@ QgsRasterLayerElevationProperties *QgsRasterLayerElevationProperties::clone() co { std::unique_ptr< QgsRasterLayerElevationProperties > res = std::make_unique< QgsRasterLayerElevationProperties >( nullptr ); res->setEnabled( mEnabled ); + res->setMode( mMode ); res->setProfileLineSymbol( mProfileLineSymbol->clone() ); res->setProfileFillSymbol( mProfileFillSymbol->clone() ); res->setProfileSymbology( mSymbology ); res->setElevationLimit( mElevationLimit ); res->setBandNumber( mBandNumber ); + res->setFixedRange( mFixedRange ); res->copyCommonProperties( this ); return res.release(); } @@ -107,22 +139,48 @@ QgsRasterLayerElevationProperties *QgsRasterLayerElevationProperties::clone() co QString QgsRasterLayerElevationProperties::htmlSummary() const { QStringList properties; - properties << tr( "Elevation band: %1" ).arg( mBandNumber ); - properties << tr( "Scale: %1" ).arg( mZScale ); - properties << tr( "Offset: %1" ).arg( mZOffset ); + switch ( mMode ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + properties << tr( "Elevation range: %1 to %2" ).arg( mFixedRange.lower() ).arg( mFixedRange.upper() ); + break; + + case Qgis::RasterElevationMode::RepresentsElevationSurface: + properties << tr( "Elevation band: %1" ).arg( mBandNumber ); + properties << tr( "Scale: %1" ).arg( mZScale ); + properties << tr( "Offset: %1" ).arg( mZOffset ); + break; + } + return QStringLiteral( "
  • %1
  • " ).arg( properties.join( QLatin1String( "
  • " ) ) ); } -bool QgsRasterLayerElevationProperties::isVisibleInZRange( const QgsDoubleRange & ) const +bool QgsRasterLayerElevationProperties::isVisibleInZRange( const QgsDoubleRange &range ) const { - // TODO -- test actual raster z range - return true; + switch ( mMode ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + return mFixedRange.overlaps( range ); + + case Qgis::RasterElevationMode::RepresentsElevationSurface: + // TODO -- test actual raster z range + return true; + } + BUILTIN_UNREACHABLE } QgsDoubleRange QgsRasterLayerElevationProperties::calculateZRange( QgsMapLayer * ) const { - // TODO -- determine actual z range from raster statistics - return QgsDoubleRange(); + switch ( mMode ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + return mFixedRange; + + case Qgis::RasterElevationMode::RepresentsElevationSurface: + // TODO -- determine actual z range from raster statistics + return QgsDoubleRange(); + } + BUILTIN_UNREACHABLE } bool QgsRasterLayerElevationProperties::showByDefaultInElevationProfilePlots() const @@ -140,6 +198,20 @@ void QgsRasterLayerElevationProperties::setEnabled( bool enabled ) emit profileGenerationPropertyChanged(); } +Qgis::RasterElevationMode QgsRasterLayerElevationProperties::mode() const +{ + return mMode; +} + +void QgsRasterLayerElevationProperties::setMode( Qgis::RasterElevationMode mode ) +{ + if ( mMode == mode ) + return; + + mMode = mode; + emit changed(); +} + void QgsRasterLayerElevationProperties::setBandNumber( int band ) { if ( mBandNumber == band ) @@ -150,15 +222,25 @@ void QgsRasterLayerElevationProperties::setBandNumber( int band ) emit profileGenerationPropertyChanged(); } -double QgsRasterLayerElevationProperties::elevationForPixelValue( int band, double pixelValue ) const +QgsDoubleRange QgsRasterLayerElevationProperties::elevationRangeForPixelValue( int band, double pixelValue ) const { - if ( !mEnabled ) - return std::numeric_limits< double >::quiet_NaN(); + if ( !mEnabled || std::isnan( pixelValue ) ) + return QgsDoubleRange(); - if ( band != mBandNumber ) - return std::numeric_limits< double >::quiet_NaN(); + switch ( mMode ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + return mFixedRange; - return pixelValue * mZScale + mZOffset; + case Qgis::RasterElevationMode::RepresentsElevationSurface: + { + if ( band != mBandNumber ) + return QgsDoubleRange(); + + return QgsDoubleRange( pixelValue * mZScale + mZOffset, pixelValue * mZScale + mZOffset ); + } + } + BUILTIN_UNREACHABLE } QgsLineSymbol *QgsRasterLayerElevationProperties::profileLineSymbol() const @@ -298,3 +380,17 @@ void QgsRasterLayerElevationProperties::setDefaultProfileFillSymbol( const QColo profileFillLayer->setStrokeStyle( Qt::NoPen ); mProfileFillSymbol = std::make_unique< QgsFillSymbol>( QgsSymbolLayerList( { profileFillLayer.release() } ) ); } + +QgsDoubleRange QgsRasterLayerElevationProperties::fixedRange() const +{ + return mFixedRange; +} + +void QgsRasterLayerElevationProperties::setFixedRange( const QgsDoubleRange &range ) +{ + if ( range == mFixedRange ) + return; + + mFixedRange = range; + emit changed(); +} diff --git a/src/core/raster/qgsrasterlayerelevationproperties.h b/src/core/raster/qgsrasterlayerelevationproperties.h index 737b4c2bea9e..bf125eb5c9f4 100644 --- a/src/core/raster/qgsrasterlayerelevationproperties.h +++ b/src/core/raster/qgsrasterlayerelevationproperties.h @@ -69,9 +69,27 @@ class CORE_EXPORT QgsRasterLayerElevationProperties : public QgsMapLayerElevatio */ void setEnabled( bool enabled ); + /** + * Returns the elevation mode. + * + * \see setMode() + * \since QGIS 3.38 + */ + Qgis::RasterElevationMode mode() const; + + /** + * Sets the elevation \a mode. + * + * \see mode() + * \since QGIS 3.38 + */ + void setMode( Qgis::RasterElevationMode mode ); + /** * Returns the band number from which the elevation should be taken. * + * \note This is only considered when mode() is Qgis::RasterElevationMode::RepresentsElevationSurface. + * * \see setBandNumber() */ int bandNumber() const { return mBandNumber; } @@ -79,18 +97,44 @@ class CORE_EXPORT QgsRasterLayerElevationProperties : public QgsMapLayerElevatio /** * Sets the \a band number from which the elevation should be taken. * + * \note This is only considered when mode() is Qgis::RasterElevationMode::RepresentsElevationSurface. + * * \see bandNumber() */ void setBandNumber( int band ); /** - * Returns the elevation corresponding to a raw pixel value from the specified \a band. + * Returns the fixed elevation range for the raster. + * + * \note This is only considered when mode() is Qgis::RasterElevationMode::FixedElevationRange. * - * Returns NaN if the pixel value does not correspond to an elevation value. + * \note When a fixed range is set any zOffset() and zScale() is ignored. * + * \see setFixedRange() * \since QGIS 3.38 */ - double elevationForPixelValue( int band, double pixelValue ) const; + QgsDoubleRange fixedRange() const; + + /** + * Sets the fixed elevation \a range for the raster. + * + * \note This is only considered when mode() is Qgis::RasterElevationMode::FixedElevationRange. + * + * \note When a fixed range is set any zOffset() and zScale() is ignored. + * + * \see fixedRange() + * \since QGIS 3.38 + */ + void setFixedRange( const QgsDoubleRange &range ); + + /** + * Returns the elevation range corresponding to a raw pixel value from the specified \a band. + * + * Returns an infinite range if the pixel value does not correspond to an elevation value. + * + * \since QGIS 3.38 + */ + QgsDoubleRange elevationRangeForPixelValue( int band, double pixelValue ) const; /** * Returns the line symbol used to render the raster profile in elevation profile plots. @@ -185,12 +229,17 @@ class CORE_EXPORT QgsRasterLayerElevationProperties : public QgsMapLayerElevatio void setDefaultProfileFillSymbol( const QColor &color ); bool mEnabled = false; + + Qgis::RasterElevationMode mMode = Qgis::RasterElevationMode::RepresentsElevationSurface; + std::unique_ptr< QgsLineSymbol > mProfileLineSymbol; std::unique_ptr< QgsFillSymbol > mProfileFillSymbol; Qgis::ProfileSurfaceSymbology mSymbology = Qgis::ProfileSurfaceSymbology::Line; double mElevationLimit = std::numeric_limits< double >::quiet_NaN(); int mBandNumber = 1; + QgsDoubleRange mFixedRange; + }; #endif // QGSRASTERLAYERELEVATIONPROPERTIES_H diff --git a/src/core/raster/qgsrasterlayerrenderer.cpp b/src/core/raster/qgsrasterlayerrenderer.cpp index 0c79c05d3af6..aae5aa26e9d9 100644 --- a/src/core/raster/qgsrasterlayerrenderer.cpp +++ b/src/core/raster/qgsrasterlayerrenderer.cpp @@ -306,28 +306,42 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender mElevationOffset = elevProp->zOffset(); mElevationBand = elevProp->bandNumber(); - if ( !rendererContext.zRange().isInfinite() - && mPipe->renderer()->usesBands().contains( mElevationBand ) ) + if ( !rendererContext.zRange().isInfinite() ) { - // if layer has elevation settings and we are only rendering a slice of z values => we need to filter pixels by elevation - - std::unique_ptr< QgsRasterTransparency > transparency; - if ( const QgsRasterTransparency *rendererTransparency = mPipe->renderer()->rasterTransparency() ) - transparency = std::make_unique< QgsRasterTransparency >( *rendererTransparency ); - else - transparency = std::make_unique< QgsRasterTransparency >(); - - QVector transparentPixels = transparency->transparentSingleValuePixelList(); - - // account for z offset/zscale by reversing these calculations, so that we get the z range in - // raw pixel values - const double adjustedLower = ( rendererContext.zRange().lower() - mElevationOffset ) / mElevationScale; - const double adjustedUpper = ( rendererContext.zRange().upper() - mElevationOffset ) / mElevationScale; - transparentPixels.append( QgsRasterTransparency::TransparentSingleValuePixel( std::numeric_limits::lowest(), adjustedLower, 0, true, !rendererContext.zRange().includeLower() ) ); - transparentPixels.append( QgsRasterTransparency::TransparentSingleValuePixel( adjustedUpper, std::numeric_limits::max(), 0, !rendererContext.zRange().includeUpper(), true ) ); + switch ( elevProp->mode() ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + // don't need to handle anything here -- the layer renderer will never be created if the + // render context range doesn't match the layer's fixed elevation range + break; - transparency->setTransparentSingleValuePixelList( transparentPixels ); - mPipe->renderer()->setRasterTransparency( transparency.release() ); + case Qgis::RasterElevationMode::RepresentsElevationSurface: + { + if ( mPipe->renderer()->usesBands().contains( mElevationBand ) ) + { + // if layer has elevation settings and we are only rendering a slice of z values => we need to filter pixels by elevation + + std::unique_ptr< QgsRasterTransparency > transparency; + if ( const QgsRasterTransparency *rendererTransparency = mPipe->renderer()->rasterTransparency() ) + transparency = std::make_unique< QgsRasterTransparency >( *rendererTransparency ); + else + transparency = std::make_unique< QgsRasterTransparency >(); + + QVector transparentPixels = transparency->transparentSingleValuePixelList(); + + // account for z offset/zscale by reversing these calculations, so that we get the z range in + // raw pixel values + const double adjustedLower = ( rendererContext.zRange().lower() - mElevationOffset ) / mElevationScale; + const double adjustedUpper = ( rendererContext.zRange().upper() - mElevationOffset ) / mElevationScale; + transparentPixels.append( QgsRasterTransparency::TransparentSingleValuePixel( std::numeric_limits::lowest(), adjustedLower, 0, true, !rendererContext.zRange().includeLower() ) ); + transparentPixels.append( QgsRasterTransparency::TransparentSingleValuePixel( adjustedUpper, std::numeric_limits::max(), 0, !rendererContext.zRange().includeUpper(), true ) ); + + transparency->setTransparentSingleValuePixelList( transparentPixels ); + mPipe->renderer()->setRasterTransparency( transparency.release() ); + } + break; + } + } } } diff --git a/src/gui/qgsmaptoolidentify.cpp b/src/gui/qgsmaptoolidentify.cpp index 42a2e9f78ab3..67d4ae5062ce 100644 --- a/src/gui/qgsmaptoolidentify.cpp +++ b/src/gui/qgsmaptoolidentify.cpp @@ -1101,8 +1101,8 @@ bool QgsMapToolIdentify::identifyRasterLayer( QList *results, Qg continue; } const double value = it.value().toDouble(); - const double elevation = elevationProperties->elevationForPixelValue( it.key(), value ); - if ( identifyContext.zRange().contains( elevation ) ) + const QgsDoubleRange elevationRange = elevationProperties->elevationRangeForPixelValue( it.key(), value ); + if ( !elevationRange.isInfinite() && identifyContext.zRange().overlaps( elevationRange ) ) { foundMatch = true; break; diff --git a/src/ui/raster/qgsrasterelevationpropertieswidgetbase.ui b/src/ui/raster/qgsrasterelevationpropertieswidgetbase.ui index f35045c35b3c..248fee84b668 100644 --- a/src/ui/raster/qgsrasterelevationpropertieswidgetbase.ui +++ b/src/ui/raster/qgsrasterelevationpropertieswidgetbase.ui @@ -6,14 +6,20 @@ 0 0 - 417 - 382 + 537 + 575 + + + 0 + 0 + + Raster Elevation Properties - + 0 @@ -26,89 +32,208 @@ 0 - - - - Qt::StrongFocus - - - Represents Elevation Surface + + + + + 0 + 0 + - - true + + QFrame::NoFrame - - vectorgeneral + + 2 - - - - - Scale - - - - - - - 6 - - - 0.000000000000000 - - - 99999999999.000000000000000 - - - 1.000000000000000 - - - - - - - 6 - - - -99999999999.000000000000000 - - - 99999999999.000000000000000 - - - - - - - <html><head/><body><p><span style=" font-weight:600;">Elevation scaling and offset can be used to manually correct elevation values from the layer.</span></p><p>The scale is applied to the raster values before adding the offset.</p></body></html> - - - true - - - - - - - Offset - - - - - - - Band - - - - - - - + + + + 0 + 0 + + + + + + + + 0 + 0 + + + + The layer does not contain any elevation related data. + + + false + + + + + + + + + 0 + 0 + + + + + + + Scale + + + + + + + Offset + + + + + + + 6 + + + 0.000000000000000 + + + 99999999999.000000000000000 + + + 1.000000000000000 + + + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">The pixel values in the layer represent an elevation surface, such as a Digital Elevation Model (DEM).</span></p><p>Elevation scaling and offset can be used to manually correct elevation values from the layer. The scale is applied to the raster values before adding the offset.</p></body></html> + + + true + + + + + + + 6 + + + -99999999999.000000000000000 + + + 99999999999.000000000000000 + + + + + + + + + + Band + + + + + + + + + 0 + 0 + + + + false + + + + + + Lower + + + + + + + + + + Upper + + + + + + + Limits + + + + + + + 4 + + + -9999999998.000000000000000 + + + 9999999999.000000000000000 + + + + + + + 4 + + + -9999999998.000000000000000 + + + 9999999999.000000000000000 + + + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">The raster layer (or selected raster band) is associated with a fixed elevation range.</span></p><p>This mode can be used when a layer has a single fixed elevation, or a range (slice) of elevation values. If a range is specified, pixels will be extruded over this range.</p></body></html> + + + true + + + + + - - + + Profile Chart Appearance @@ -231,7 +356,10 @@ - + + + + Qt::Vertical @@ -244,6 +372,13 @@ + + + + Configuration + + + @@ -269,11 +404,6 @@ 1 - - mElevationGroupBox - mScaleZSpinBox - mOffsetZSpinBox - diff --git a/tests/src/python/test_qgsrasterlayerelevationproperties.py b/tests/src/python/test_qgsrasterlayerelevationproperties.py index a65fbd014ca2..853a835eda1a 100644 --- a/tests/src/python/test_qgsrasterlayerelevationproperties.py +++ b/tests/src/python/test_qgsrasterlayerelevationproperties.py @@ -19,7 +19,8 @@ QgsLineSymbol, QgsRasterLayerElevationProperties, QgsReadWriteContext, - QgsRasterLayer + QgsRasterLayer, + QgsDoubleRange ) import unittest from qgis.testing import start_app, QgisTestCase @@ -31,13 +32,18 @@ class TestQgsRasterLayerElevationProperties(QgisTestCase): - def testBasic(self): + def test_basic_elevation_surface(self): + """ + Basic tests for the class using the RepresentsElevationSurface mode + """ props = QgsRasterLayerElevationProperties(None) + self.assertEqual(props.mode(), Qgis.RasterElevationMode.RepresentsElevationSurface) self.assertEqual(props.zScale(), 1) self.assertEqual(props.zOffset(), 0) self.assertFalse(props.isEnabled()) self.assertFalse(props.hasElevation()) self.assertEqual(props.bandNumber(), 1) + self.assertTrue(props.fixedRange().isInfinite()) self.assertIsInstance(props.profileLineSymbol(), QgsLineSymbol) self.assertIsInstance(props.profileFillSymbol(), QgsFillSymbol) self.assertEqual(props.profileSymbology(), Qgis.ProfileSurfaceSymbology.Line) @@ -70,6 +76,7 @@ def testBasic(self): props2 = QgsRasterLayerElevationProperties(None) props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.mode(), Qgis.RasterElevationMode.RepresentsElevationSurface) self.assertEqual(props2.zScale(), 2) self.assertEqual(props2.zOffset(), 0.5) self.assertTrue(props2.isEnabled()) @@ -80,6 +87,7 @@ def testBasic(self): self.assertEqual(props2.elevationLimit(), 909) props2 = props.clone() + self.assertEqual(props2.mode(), Qgis.RasterElevationMode.RepresentsElevationSurface) self.assertEqual(props2.zScale(), 2) self.assertEqual(props2.zOffset(), 0.5) self.assertTrue(props2.isEnabled()) @@ -89,6 +97,64 @@ def testBasic(self): self.assertEqual(props2.profileSymbology(), Qgis.ProfileSurfaceSymbology.FillBelow) self.assertEqual(props2.elevationLimit(), 909) + def test_basic_fixed_range(self): + """ + Basic tests for the class using the FixedElevationRange mode + """ + props = QgsRasterLayerElevationProperties(None) + self.assertTrue(props.fixedRange().isInfinite()) + + props.setMode(Qgis.RasterElevationMode.FixedElevationRange) + props.setFixedRange(QgsDoubleRange(103.1, 106.8)) + # fixed ranges should not be affected by scale/offset + props.setZOffset(0.5) + props.setZScale(2) + self.assertEqual(props.fixedRange(), QgsDoubleRange(103.1, 106.8)) + self.assertEqual(props.calculateZRange(None), QgsDoubleRange(103.1, 106.8)) + self.assertFalse(props.isVisibleInZRange(QgsDoubleRange(3.1, 6.8))) + self.assertTrue(props.isVisibleInZRange(QgsDoubleRange(3.1, 104.8))) + self.assertTrue(props.isVisibleInZRange(QgsDoubleRange(104.8, 114.8))) + self.assertFalse(props.isVisibleInZRange(QgsDoubleRange(114.8, 124.8))) + + doc = QDomDocument("testdoc") + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsRasterLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.mode(), Qgis.RasterElevationMode.FixedElevationRange) + self.assertEqual(props2.fixedRange(), QgsDoubleRange(103.1, 106.8)) + + props2 = props.clone() + self.assertEqual(props2.mode(), Qgis.RasterElevationMode.FixedElevationRange) + self.assertEqual(props2.fixedRange(), QgsDoubleRange(103.1, 106.8)) + + # include lower, exclude upper + props.setFixedRange(QgsDoubleRange(103.1, 106.8, + includeLower=True, + includeUpper=False)) + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsRasterLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.fixedRange(), QgsDoubleRange(103.1, 106.8, + includeLower=True, + includeUpper=False)) + + # exclude lower, include upper + props.setFixedRange(QgsDoubleRange(103.1, 106.8, + includeLower=False, + includeUpper=True)) + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsRasterLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.fixedRange(), QgsDoubleRange(103.1, 106.8, + includeLower=False, + includeUpper=True)) + def test_looks_like_dem(self): layer = QgsRasterLayer( os.path.join(unitTestDataPath(), 'landsat.tif'), 'i am not a dem') @@ -122,27 +188,40 @@ def test_looks_like_dem(self): self.assertTrue( QgsRasterLayerElevationProperties.layerLooksLikeDem(layer)) - def test_elevation_for_pixel_value(self): + def test_elevation_range_for_pixel_value(self): """ - Test transforming pixel values to elevations + Test transforming pixel values to elevation ranges """ props = QgsRasterLayerElevationProperties(None) - self.assertTrue(math.isnan(props.elevationForPixelValue(band=1, pixelValue=3))) + + self.assertEqual(props.elevationRangeForPixelValue(band=1, pixelValue=3), + QgsDoubleRange()) props.setEnabled(True) - self.assertEqual(props.elevationForPixelValue(band=1, pixelValue=3), - 3) + self.assertEqual(props.elevationRangeForPixelValue(band=1, pixelValue=3), + QgsDoubleRange(3,3)) + self.assertEqual(props.elevationRangeForPixelValue(band=1, pixelValue=math.nan), + QgsDoubleRange()) # check that band number is respected props.setBandNumber(2) - self.assertTrue(math.isnan(props.elevationForPixelValue(band=1, pixelValue=3))) - self.assertEqual(props.elevationForPixelValue(band=2, pixelValue=3), - 3) + self.assertEqual(props.elevationRangeForPixelValue(band=1, pixelValue=3), + QgsDoubleRange()) + self.assertEqual(props.elevationRangeForPixelValue(band=2, pixelValue=3), + QgsDoubleRange(3,3)) # check that offset/scale is respected props.setZOffset(0.5) props.setZScale(2) - self.assertEqual(props.elevationForPixelValue(band=2, pixelValue=3), - 6.5) + self.assertEqual(props.elevationRangeForPixelValue(band=2, pixelValue=3), + QgsDoubleRange(6.5, 6.5)) + + # with fixed range mode + props.setMode(Qgis.RasterElevationMode.FixedElevationRange) + props.setFixedRange(QgsDoubleRange(11, 15)) + self.assertEqual(props.elevationRangeForPixelValue(band=1, pixelValue=math.nan), + QgsDoubleRange()) + self.assertEqual(props.elevationRangeForPixelValue(band=1, pixelValue=3), + QgsDoubleRange(11, 15)) if __name__ == '__main__': diff --git a/tests/src/python/test_qgsrasterlayerrenderer.py b/tests/src/python/test_qgsrasterlayerrenderer.py index b0e0140113bd..355c52c33ae8 100644 --- a/tests/src/python/test_qgsrasterlayerrenderer.py +++ b/tests/src/python/test_qgsrasterlayerrenderer.py @@ -13,6 +13,7 @@ from qgis.PyQt.QtCore import QSize from qgis.core import ( + Qgis, QgsCoordinateReferenceSystem, QgsGeometry, QgsMapClippingRegion, @@ -115,6 +116,57 @@ def test_render_dem_with_z_range_filter(self): map_settings) ) + def test_render_fixed_elevation_range_with_z_range_filter(self): + """ + Test rendering a raster with a fixed elevation range when + map settings has a z range filtrer + """ + raster_layer = QgsRasterLayer(os.path.join(TEST_DATA_DIR, '3d', 'dtm.tif')) + self.assertTrue(raster_layer.isValid()) + + # set layer as elevation enabled + raster_layer.elevationProperties().setEnabled(True) + raster_layer.elevationProperties().setMode( + Qgis.RasterElevationMode.FixedElevationRange + ) + raster_layer.elevationProperties().setFixedRange( + QgsDoubleRange(33, 38) + ) + + map_settings = QgsMapSettings() + map_settings.setOutputSize(QSize(400, 400)) + map_settings.setOutputDpi(96) + map_settings.setDestinationCrs(raster_layer.crs()) + map_settings.setExtent(raster_layer.extent()) + map_settings.setLayers([raster_layer]) + + # no filter on map settings + map_settings.setZRange(QgsDoubleRange()) + self.assertTrue( + self.render_map_settings_check( + 'No Z range filter on map settings, fixed elevation range layer', + 'dem_no_filter', + map_settings) + ) + + # map settings range includes layer's range + map_settings.setZRange(QgsDoubleRange(30, 35)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings includes layers fixed range', + 'fixed_elevation_range_included', + map_settings) + ) + + # map settings range excludes layer's range + map_settings.setZRange(QgsDoubleRange(130, 135)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings outside of layers fixed range', + 'fixed_elevation_range_excluded', + map_settings) + ) + if __name__ == '__main__': unittest.main() diff --git a/tests/testdata/control_images/rasterlayerrenderer/expected_fixed_elevation_range_excluded/expected_fixed_elevation_range_excluded.png b/tests/testdata/control_images/rasterlayerrenderer/expected_fixed_elevation_range_excluded/expected_fixed_elevation_range_excluded.png new file mode 100644 index 0000000000000000000000000000000000000000..08b004aced683ae4d42bf0f38dfea1acbc45235e GIT binary patch literal 471523 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 literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/rasterlayerrenderer/expected_fixed_elevation_range_included/expected_fixed_elevation_range_included.png b/tests/testdata/control_images/rasterlayerrenderer/expected_fixed_elevation_range_included/expected_fixed_elevation_range_included.png new file mode 100644 index 0000000000000000000000000000000000000000..60cd8200a8e4612bbca1e6bb01a15a4c8dc6f48f GIT binary patch literal 471523 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 Y%%*m%aO@*IW9noO7?Lcb*@E`L$ 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%0DI^hPM95V znB6Slap3tLMjK*1T7j|W$)xHZ6V)5Tiv-dtzR<^Q5>^PWA;rQhy5 zjNbLZ(s%1Rd&RxNs_Q3;{S&tbk{L%zV1MQ<`z(*(=tN-ZP97 z?ka8_XC3@IU5#(aS$;izm)4@z^6GuA;oQ6Sx?FYS^RMPFX5Hq97oJq(?uA{9Y#rE3 ze}4yFFQo79$@h?}nHH+3s-MN8(@N zk6n}f-I1*4oEg5{b1%fUM_7LK)I8(zuCyLkvBj$0jA<_;THpB9vzDD>^W&+Ny@D5Z z+Aw^5tsjmjo^Ph*{Gx61HjnS#I@2(H!_2;Rae3M5E_QPASJVqT`>MD6)3$fjdCC0_ zyk1E6d-M6G>o9p3?7?yn>Ai83b&xy6iTrGbbFuI%S)o{#*OBf4$VpC{d?ee+3p^KeMl0 zEWCWVVRvF~+Me=PTYs4O?&|&`aQ#|OJy#cNeJ2jv{_?_3d-qAL-2-R6*qu}}vES;P z{H-Bpv6KJuU;oERw`bry$Q`5(bGVxTXZ78Uvp1WjcL!OVc8F=X8;O6$!<#L^-Ps?3 z|F6CGdC_jo(zKGs${cC6g5prH{e=$1fespll5$YCw1`>gu%H#86>LOB=)}TGEGG*7 z2R(2gl8qAw4J{(lI8bS9(3$j5EFE-@l+H}F{jP{}eR0Pf>v{IxZ^U;h&-wNPZ>(SU zy4QNv`|Q2rjfnG|%@_F0OPF=!CZ;cOAE`ILwWt2_Mv*=EKBaHc-FMfVuoI`fiHE0s z_D#(91gYN#0C~LJlbp!rH8(MGr%zqH`$XbPTc0^+WbgG%4s}*}#^}xcfYtA8w5f;g z61%d~pFh^?THxeD_Wb0TWcM>+Zr}Ytx^xEeF!o{1*y_TeSY9;RNb+;#0ZzZV7X$VV z+x4D zz1z!bzwXW6z}vSu_)=fl_~FTT=AnDdKYqU*8cx;wfV*Ut8oUv!*^H*ZKcw!&b=gj%*RAWYd z-QN3ww0QqHj2!&DC_M1U{FtBq#6!J1h~_O?`0SQL2Ys>9pSrl@&p18G;XchldIL(I zdh(>6@#MxoU9eNG97i(wc09fpE55Tlj#%JYnwbj zx%D?=#B#wYfBI9WPgd_UF}MCQf%}2u=?<4JW0gq^YD5&@#_=Q(z^IP z+&j!J*8QC2%pRIQ?e0(82VCwu^H#d~oww^J_jrWc#j~fxq1#vb!RI+3*74+2hl~A* zm!tQg4PIV-Y0G1e))AwPOkd*lr_VfVYG0A?l0UtlD_(Q#37kIqb9VGGaexsIM!oa0 zr)l5hP76L?S>HOI{p8GA6X)0LyB|oKcfSvg7el6S@L0q=c{3nGGCmwcxBeqH%V~zKpeg^xf5p9OYXrQ>|Yx>;WFNO;!}s) zn@;Bt6N{YsAji3g#fR>9=*IEG(KdeOVZQUyCeF(G#*m3mog8uK?^*C<{fWz3?h%ap zGFKiY{dyMk$wPkTrJZ^BS>}-=7k+K@Twr{U+TXT);CIG}x%KA>+z+J1N$?OV7qC}{ z*h8KNn>_vK9`y9#->p8t@^?<-w7p=#XRf*z3${66(Ye{;2P2NyYRS2Z9;0Q zjC<40So+eB{!xqi{r*wczKwS;#Kk!?bGkP<^qZ%RObl^|!JLJnOXVEi6Lb!A=zXWOt2_|)NAt&C-F;#c488z*0kdoiZWUi6)nnVWjo z!Y1DStdo09f9uX6>oq3r74O7zmb3ZEGuIoU(!G;|Za=&)_TXIfbq?I=lf#-AUbzxK|Sbc*1i)}JSEKadtD!9$ZbVh@xlX``FtAq9uem=}{c;(73~cdP4(O^(D6&sx}t#h#WN znd?o@*p!FwD89;@{K@4U&(_G^C+;(I=)CO57(DoS?Kp`a^&}76?$;b*x%A_+2Xr~| zL8f16pK_)&KP7n|xv1}Xf>)cG?i+vWSFm!Cqprlq%DLq{#VNrO_d$=%$~pQ6PtTG0 zFEOnF@4e;;+z+IAE}y+zIC)`w4t!GddvHByeelE~eJIHzR(q22PBAYEViT@?XXQY>SK?7W8nK?-PL}2-@DpyT*1plPUlTN@YY!MD`&o+_~6T&uGca0!SF!J zHSx-iG)}I%`POVs<7cHj_MA2~^V$HD@zfK$!udd^H|w40lR9$Bfy`?l=L7z|b2-!3 z*MhkpNbh*3IgDN$(&UBH7Z{io-u%3HNp%sJ8@?Q`OV zD<4kTQ}#OLu6)U{!st&dy=Z^L9+Ly!e)!S@d$guH(%fJC;uo*pTe0?^(wO_uM)GQZ z=3dd|$@d4JH+gF19{Remuime?FXS9mi5u~p|d8Q=g8~4?A`b+-%j9u zAibT*L!LaC>UqFD)LwXEx|p~l|8!xPGjVv)PBHURicjB8?|ydJkBW88=e_;c|HfbY z=Rf}~z(=V2|jC? zzw?=sSbOr}Lk&0*iw!O@jmzFrmnVIxXD{iemc6AP{S0UBq4RJ1c>?#t=^ZD51IBmm zePQImBbJ9%T|ec)@0J(H3X}2F6R&?)ON@D9-5-8+1NrEcHM;k- z>5pG}_i9YXvBl&b@wp%TeIq7ss+WA|uHJ_=iK83WpS_4_pMIo0qPyTSM?LpJEbG~W z7~?6yI2Y?6<(ChiKaR5pd@M1OA8!0p4RWbZKHUB*5B5#mJaPD}hkmcu6SyBpujldE z@*wu%;c)unBf((u&}I()JdEZbiLny@^%yKQj!skPa6edT-5^_%Cm?|*>x@C;Lu%iX&b{2N7`WI6|W7BmwO<8`bL91Ink{@`H0_ZGl3uf z^k+Z0KPGJkbSC6U7t4ueaUzL%GR0|w$+$LB4)kdy2XSpBz7+=;>gLM4^s67muj0f{ za#ySSp7QbSI5KC#^?S0zlh2$pXX3P{EB)uwDDTwPO6i?a{_H_q?!$gkvJdxzlmkgG zeEgE1ywtICnJWfcojSQ6@`#Dab1San>Cd{UXAj_6)=+x}{QLjXU+upI=(^zAm$h4;dTGo@*;y-lO#h>`w~ zFK&82!SMo<61_hv$wO?ljCnr9;pf#oz$vfTmgFS{4rgI3edOy$vdkBkaboFzO8E4} z{c9^TS05boWIg=y>eD91`-<%gpDXdH6St@Kn|sT*6SyBvZ)fu4;qY;gkSq_?lTlwh z+~yEA&zLFrxu96`CwFp~vyz!NVODW_BWHzK`KLPi9?3CxlG$g+uhzPP&$_F;^!uJE ziTNJWxBlM4WG63Yhnr#kqA25h*RIXa?^x5#8vklf;NjqKm3+x{MW+T_%IqU~ z+;8HjkIl;XWa~2@$@^nIaaQ(XEV<1Wi*ydeoK@n$xUa+}FPMA1oxuG-dOMScJRa;! z_KVa^91e((&i+1@bX{+ zKk|z~=HbNVaN^^Yzclt@F%H&ceQl-O+9`K2abV=m8wT++`SRGi_7yqhdI2Yo{n&5D z(2cjh?WvxaDb~GMM``X#(y!k|uQ2#0KEI=fBjfaxK4SWk%UMnPk`GB7nL0e?_YB&W zOFXu^{b1V%`H4v#%ql*`;Lqh=@KIB4q`t&FIlsZ8i_?#kr**l-X0AEz+nIHaxwm|t z!2LkloP=)<*nj>X{AYi;{}$jW%R`qG`24Qb1F9s(@=$t#;q$v;Fye@_Jk02Fq3@Qt ziRoPHBu0J8H>Ny;5&Nj5M(#g(dOsQ4>8rkzZ<@`nm#H2caMC}y$=~>046L=m$%|}W z&XEr?^4y2~+QeA;k#M>6Ya`87X6#6Z&%D%oU+`EPe8!34Gv63~7Ph(ASx?*C#_=Ad zFY)NCtc?xcYdz1XePyl8H7*D8UT-IGKb+pq>E_1W=_GWop$8S?=4frkQ&i@1jv zzcKQ2<>61iy7;V@x;TB-v9@ufT}fw!f0K`XGT-;P-w!aW zd~aYGPrdh)G3slh`%Mg=wCy4LgG)Sm=4<0q2TOlQ_ka!teq`dG5;tGqAWsftFZdE? z9KU(SiRn|?Coy{yr_Ua+UGk@mzUq;=@M7gG;K62@i_f0zv!(H;#QV!QftNeu7`~l3 zfjs;kN~Hcgv>w>Z_wXXsIixIXZZ^C!Za%o(OPo@E?Uor=S2lKKgZBkS{VGrL%Yo0? zWc;c=aZLS9K5DgJzI-pdp|5(I&cX`Ye(F;TsSZXRZq5un*i&+tr_b{xhkkJF^Nfqx z$*kjy(wE%&yWj4?eRpp5K~CfJWuAKW<}CR4n0PTmeM<7YuF+ZY%cT#g&%NNwWjuYi z{xX646Vl5pPHC%~3l}ek(8KGYO;TbWRPn^~fkvE{zRaI|#^u#cnX%-^I_Ql{JNZ6R zpK8z}exw)hYVPmtCF`dyuf1UB?2M#Nrhfg79*7Jf7zY?H2 zxyF)Py`}F7e)F6SoY~iO=8v$%<)MbUa;lrS)U8i03vTPDM9-NSBPaJLk6axi51(B2 ztR(MV>j~Tsr1fmxL_CanSUqSdNd$wB@4CcuP+49i=;G9o8Bf3U@vT;4R<=D6Pd)#f zcvVAxbK}qbh`TDYCv%-0wimmxD{|`jY<~9DJZ+!l+~K-jzEAl(BRKL?8n~QS*Wdxf zvgek>n6dS+dzq6M`^}hr^qGc_TywC&ij!9#aaZQx(>B-o#NhEo+=Wkk*UEf#t_yYX z*sbq;#+-@0-121tAN)Z23xDD7{iEaG0=!iGj%pr8+RsBcZHKscF9LiVL=UBT9!@U~ z{mQOozJ75nt*^gr{YYc>Ew{BMZ2DHZysP-+H^&~tOuZ0ue&UeMT;GH-rnD}0>c&&r z+X|O)^c5~S>9PCHIJ!K|@53nlV6Ck``H1^q;(xZpa!%Gs{8=52tWBPMsOMf2E6#qq zAN{Kw`!;Sp=1Q(FZQ?BP_~pRQL!V{*$@uhzx%nR@i-%iWzmK_JIc|A8fe(H-9T)tr zAUt%de!WnLdnm4J}kF3Ua3qohxA;4@cS9%S}Qylwj-M$N7#H!=C` z!I=BUmVZ|}`@8j*3EU5)msy_d|5T&h-^hKVz;N|n;z~=&IoP3s?VwA?U z&C^!mw^sIHe3H%G_LTF8Pu}iJKb#%g)ycdaZi+J+y-qE+Q+w*AxUOW!NgQwk+xEKztwz}Wdad5eT zAbGi{=gU}1W8l-T-aOiqbZ?o9p37MK6Z56?UCpz7?Kq~sov#v1a-=@x>+=jK^|$mr zu%5DUxnD5a;>_braRcYmQevjQjU<d80+2a`3zC16!;-`t+w%H=mwYODub0`Jlva z%$nNZ|A5k&^8yoRgTziL(&61Dt9%n_M7ezi9P|F7udj2TVTqKJ)HT&%1 zZ>)H8p9tcMIpPfUUi2pqxzn6X+R(|)=HV-Ib)TD25`FkRAxcO73^Tl^vY_P`FGe7m*Lt--@ zPHg9!bK*LpXJd^euj`35rX)8PepdEnoEUu=!*))|hfcj-=C%K}k0)?Hkd9|GA~=X1 zP9)2Ns5BAVixVHY*yiB_!|Q07lURN7G)H2IiO1i1_SrmV>__u6Z*`HMsqcQo_TJM@ zJ?oR3v%w~AU&hSun&5MRGdKN@=+-tjZTnGXU#S!ChY@`A2rjYK*-2;9y~xw?#KD_3 zI?LY~JsF?Ah*9EKH-~zwTI{XyaFCPv>cn^;YRfGL@vF;RIj`n_4fy+kw0M^}*sJ9s z_5ubs9X4<-<6_CRhBz>Z%UpEpBFPntT&*c)-YVx0+wWZLBWI-#ncq!=`Kaa0l)gXo zWu2U%xT*dsw%Sv3WKCmo+LtzQedyHaPeovPd49m<%KY@V4o6~-`kdm)nLn!IK`?P! zQ|TVS;$xv(OC0|6VYAFbGAr>)bHM6De{@mjUi%5$6WY(`FzOA;!-n*LXRGvYz0`T<&`1A#vrA+KoAii{Ngq~D65|?`8zocz1s~YU{Xo1Oj9C04;WjwjEm-HuI zJ>TEVBi^<-S0we}NiKWxJYn|>yg2LSIc%TyB$H?2B}Yugv#0!HjO@E})827{b0*+; z*dg(yUyhEE&(bgM*?aueS&D31|1F0J{P?Fo`^o(=X&B7pI(sq`mv<%noK7V*#OlXK zj#rNMgd;{CSaQVRfA5;OzQmQ4by7du|J0MS`w}y;o-^O|$$or)9@$%R=bq9puXUyv zb8yLHo_?htsmO7!#!|XBee#*pad3%IrysHAip4f29-r@P#*;@ZJaU?YkELI1a%D|% z&soo3fA{x(_rJLMw*c_u`;dIMzMjDSa9YpiDdGW6So*x+jrT#%3*9_@VEy5KUI^gw zg7aXTZ;h;pE>3^!wfM$(MNa31j|| zT;<}ojvP6|%xfHYW}WX%+j1(Wdh&>wWcHE2p%-gBdqnq_IOz0-^hIv=p8J+}lDP*s zvE`X=@Nn4QR6{c7IHfpjmNU?goh#>({?t1sW4FGa!2Lkl&*yX01N+(W zN$~NkUK6`GS$)Ee);ip{iJ(m@( zxt)>u&RVbOJ+j=Va^kRV&cTO~`!YWz zT<&$^5Sy5+LB7|Bb?`m9;NX?BLf3B$*=zDO=C)rZa6gbIJ>ee~p6L&>szi0B?E7;U`xy&Wk)tcn4YV{1W z&Ppd|^*|zarN4fkR_{^wpE%Eza_T$x={)6tJ9|!j;v(*UV6=+o9I_60>g!8M%s$hP zFXv7_RK=aDQfj_f6l*-QG5EJv& z@wT%UdqDbXyqNZ5?_v}Fs&CbkJnluQ{}Cz2d*qqA@K4KGD#7#(YRa2@$zgm|*Ldcj zr|lWbemrl+R_9ZGBt0Z=#=fcfpZtHn{X3uk7QjA|+kB<_5-Vrgi6Ne`PPzWS|bK z{Rwb?*-qg8gtQn>AM^$6$=%qtGj|p9(Dv^g@V9T`1Lwu)1w<`*$!WWJ$Tv4*=C<#O zZEvfbX)fJ+=T143uk(&<>*l=VC1xC+-OSuGxx4r9s2}6mkG_@69>L^|)c3eC&DHhV z7N0XfZ(Q4nha=<0(r2B?XH3c78s~eKGXU=#)W!KA!ZycQi7{qfInuuM>l3&?Azh!@ z=L`-$uUu_&f$dhtIpkgpnWJy2Cob#4DR2709@*x}dsSMy<%;KMPg8vIt@t1DCqH`5 z1UpyW)Y5N`cyPRrUuxO2^_0}}CW>yq_@-r^5{!F6Pk-9J-~5vdth`}oE%JGRpB~co zC%5<~(r>ZDCBw$~bZW3so7b#3iwUhNZm%OCvU{)DtR1y>JE8REp0l36W({=m^(oUwmUhQGA1uD~ zn_~=lcCGesHvenE-4CP(-f0fkw7l>#me`e^_#@qmBDu)XHm(ob8;w3BE3wa>`RlZ= z8Tr!3DxQ9OPTxwW*CTxHQ=X~!>BcAC8qOxa0|%oYoVvL?nf<4}%jsUg9@T~0mp9+6 zL9ADD_Pp$Y9DX$CJm5A)j=EghKA@Lwg zZ1T>m?zinT9A6veejqL0aW9lb_?5daVuGEYcIs1Zl5=5z=fXI`W?k}f<%OO8)V*NS zcUI3m?Q+iGz;yo;=QNMW*STVt>1x?SWAv}`((ikhK6LzkD(Ki5d)0$8L;7Cob06j^ zt=G2xoD~>Tn>@*Vq-W2K?LE4W z#@+VY3EU5)w=;S2aH!5M4`<@f__Bt0WpYhA-!b-u2QDuLxWFh^arwn%4^s{BiOGER zNlrP9%b9p%74sDPZ~oM8|5FeCEr9h`d&=2aGbQ=vicMc)(XEG09xpy&xN@(FRhL`a zJfyMorJmpUr@!?ho~-TOlf$???o*6=MQ4p`#a}N@_l&`(ZQL42>!eTJJnSh~TubtJ!7Ycr z37bCg=|jI#KXW+;xsr!k+Nq15e8l{5+AeqEkS}MhPrT<`Zv5&kjhTbJ+HYdOon75u z_I{QFT=QJX{aUd1r=`U^&EYw_ChX+nAm>U<>WM)&mib&cS#07`7n?qFT7t<7%{V^uxmoh^Vi!*x zZi-x7JczW>y-<@+T`9jda`NGKPw3NH<$<+d^c{})*#%GXW}dxz(-CWJ@|+d(Yk%IT zvj#d?;^t5T4rB0IH+xbiFFExiSAH?pT;Yt1Z)u$T>@of3q_6ix3=XC7RbO%{t=)YX zf3JkMd(pOMYv|X$<@+aaKae&v^uhv?dZ$b=F0@_~E`3wZ5l7~OZ5tmKjt?enBr7i} zE_QJ37n3)GC(t)vy5?>ljZGeO;?~n{O!~pfnR!!A`m#>yV%&p!wx+ejQIi)qqz@$X z#UY8CkIn+e%*7+K<`d}P3taLYK;O)_@jD)3_SQ3VU+~Ex#yyGA-?iIzrtSYo+g{Ib zuCSBeIi2CXF)@K3|MX`+xt~fgkW^P!j#pM<)xEQ5BPaaiGdE+Y8|bN}P3zXkBhA&#yMUtd8P%O1cIOJC~t0M3>1m2b)ud*$a_e(Zx!U7jOppS?fw zoJaHO=Od_hP<`gXoAY(A)&r-m#CNo^4p?f~U&oRQO!h;~9X@l>SNQBb`4X@049@fc z7cX;?n||6(PV|nG!@}ekX$nzxfzusfdbbL@UNZQk9y;8rj%nvPGo zlkX@$b5ge+hg`o89%6a;Qzzy{h0f}@HoWRe@YX|@EB665G2kX2{*PE$Tiw3i%MXux zNAgN;_u2llbBDo1|hbm*sx zL;Ox!dxtTW{jKs=emOo;?)E&*$l4u)2VeWeE7_ZvX`k@qjLgeVf{6h;?ICNZ%SFyF ze({T^hmyJ3m$)-B^HL{w+J~IEclzsGaQOT=r+F#im&ZL$Jm!eOPF)*c`q7z>es%ej zorB-Gia9HjiyZ8?KjA;i_Wj1)52O?CZ(f96EK}MP%iJ?LawyH$o;bvG?Cb@!<0i%h zw8M1Wwx?WrTlL-fS8aIZ&lz}QnD%3=YuUST?A;ADadNp&>tf3b#<+NW*?Y!XPaF@W z{L@M5*pqYVd~JKO&&)LjR=z3EUd%Nwb9fN%W+*RGp7g0FXX?c5LoBi}9V4E3JACHe z`o~S+ejqJo=}p25Dq)k)xOv*h9bQ~w)aBAv>Tho1y!`zJ@sGHutZT0+vzPSmbaL{- z2TKilRZjW#@yMPsM;>aVf2yg^9PsYhJxm<<%#ov|`R&)vIdu)V+h^X-fBy5Qhteu8 z4mtHW`HphrvL<_*dL&LvT=KSVzVd9IJk&T-uX`~M9`ln^9cgdr%lB=CN&Kz9OyGVX z{osH5jo+uyc0rE#RXu`|-1#KPeNE_LFM@XOnE@cX`rQNE{+ z9N%YoKb#itZeDZ~#0z(dO}Ueg-1f_>U#Z`l(^Qw6kN+{!6o0SJ9-7A<^=m7qJY(jp zDrCOmXkkv0H6PV32Mj%c2$$uwBX}4 ztyORIV^5uzy>twl_$qh8)wZ6C+>xZvFKH?g!HAd3?6cT(lg*)tX|H&-|(03P1Vt zyI?urBd31O@}14?eYAbH-bdqB{d+?}t}thMA>SJ+JlI$F4&FTEw9?NX71(q7r@F?$ z+h_iDpgh(TLtf^t9yaEQ%{*fI;Z-;1QR%#6(* zc+)+s2e9Q0WtZO~#P zBkjbkxbTbH?Z^GE_5ueFo#`e_4fEj19*N1_pM)|t^@cBb)yaEgIfq@Yy-&3==APt& zyZ6u-v6JjvF;gGLZuvZcYY(TdaB3dhGh1I_k1n3hYunnBAMT@reiVoQs7EkUef+NG zIKq<$X4l^t4(q3!c$;fiH@SBD>gHn(@o6shq7;8t!U=cpvu*i~`V@!U^~B7s%*}r2 z*H2#b0>_n}vcDspn*G4cJx#ScPQUiv(%B#N(Q$d&f7>q;n12xc68~y74i7I^>iCSO z6pQ50`bvN5=C%(zbEn#3I)?x1g-)$U)>K=bDb_x1rMXCalRq&>`cb~|tflXWJL6Yw z(3$sMz27%&`*~z9yPn{cdxx81-Amh3&X@Ml125+`)jQkgS=?0P_PtEt{)F^0i&xro zz$RZNz2{&jck<|OX!X?nPT`7 zJK5%4m1p;7eem7uQBCmE%{V!aI974`kw>}08ei?1c=Ev`*4T>s$nSaD;j)JMnY9oJvZo?mpZX;b^Z0f@h|_o-?6_1xF1f76LR%N=E0umb}{f> zOS}?{dSdbMNol&6$l0yr+2O9l?D0y@RV+2U>5zMN@8(^t_g?(fIqzm@EcXmQ4+8gx z%m)M5w0HaIUeD%bKlWi?yLuVRy{Y%_gc5(G+q=DKlc%pa@ULQG-QUXApZj+|aI3%l z_c6@;y5;>qTD<=roJIK1eI~ze0FaueOkd)Sf60Hb!5X>Q^kqMZ$)3}nx%%_M)z|V2 zrgP5N_MbPa&J)vq?V}qQ+!F`0@E87aA@in{{?whLwVe-seTg@ozSgH6GjGK`)f1cf z)SIyQ#iO5D@c5n~SG+qe^LLnCtoH`?Xdg%MUA?a*c0Z6#zSBpCiTG|!7rgk)UFn_A zKNpk#^0QnYC9{t+oJaVM=YFP(JLA-!FlYKoj6L)X%vx7(QsR|aOI>bk>l>qw^rbH5 zip+h%mHg=0r+wj5pK2KcC+CDqA2_l0(BC*2&%D%gFR3SH(!rj^U%^hb^sV;7>~iMd z!LnZeyA@Ba`^%>Z+z+J1G4SAIc=BaDbuZ-fsZTuZ!)^{SNG|Zc$oue95A4|s^h$r( zn|)bNp2^O<#I5{^LC?LUPAvPtc1^hUiEY_8;><&zX5Jjy;HOVT#;?lNd&?}lZ@8x! z<8yAgx9&wwz7YcF-py^iHt{~E#*p%+uXQ;1@tt*zXTSE7G4aeoy>Z~LyoccF2cD^S zW8hd|SXXO2C4Gfo?g!H1P4^&mn1i*7H5Qu#x?3IB-qiqe<XVnf z%QN|m-SY7X+z+J1>^N{Av35LDjGI(mgo)u{#M=EwNyIZBJ6H0hf2AirA1diz>2PKa zc4Dzv`siC7|7stJ0e7^(uei+Zo79vu`OLo}-RGk>)ZD*Z$lL=Me(ohU_pdK|Hm=N= z`k8)r`SM(m^g402kKXX{Qa@X#=LUy$jcF^BTTJ>=KZ+;zYu6wD!OmAYISwSRqqV~6 zPYw<&FTZ1+Fyi^yoflsAkT(ml8Gl4S!;_fQsmGqKtj=#6uF01*j^c?u!J5FMC_CafUhL^ZH-8mxytaATN_)=u zq>D38nOJ>SCEVT^r`)qO!0qOe@vMnI|6`e5FH?W-@txfp{o23Mx>LVBht4rK=Rl6Q ziIW^Ka)VKy`0Uv{B>Xw+jG?od^Vz%qYTuwz&;99F8pCE$*I4@4XWHuKAmO^#dII+Y zX)&8TObf_q_v#StYIx|upih6glozBIO*hpwGN-YVoxLZQx{}<)%9ZiSchnF0M;Lgm zvzx;aPv@Sk?Y^!^dV${?x>64JqrH;$DBeA$UthjIU2ny|^8b-P@}K_YU;i`yEx`KE z53QX%_J%L@#C)WlHJAZ5%l823Jj9AiOvcD3N1qZu%UO%T=Pbb9>v#h9!|8ZNrw|^- zBk<9|%(%J_CJ(prsMZvhbBdkyo#mKt?bG%qkUh9hFvjzDB#A}$JwST1X^gg<*r^-; z2x&jokkj7W6aK`CJ1gy9>Hf8qXZW1?5f;w=G(*nW{1t!le&kH7m$QIhTM16RC9!F? z_+9MZzVpG9arIlioxuG-dOMRRkB4dzzU1a$AFak{`!G7nC%)?08{ioZG0kN^#*yhu zeEP*tK4Z<5e)Xe#aZ37mWZ&k0TQcWn&8vGg4@vIyP)I*>nJ~#S>D?>8|C}C5oty7P z_5selp5bX;bMc!mzYhcUsV^ltK0MOzT-t}7ImwSd@rgrsnWKHnw-fl7-_pM2{VF^j zQZ|dedi5|SoET%+UQFm5W|kL>^2vGdg&V@^)v4mrPfsp&C4O>T>EHQ`?c~IjwRXA0 z8k>Aq=4YMGX?}52?V}j)ul&l)Iiu(P>@9uCvGOM-bGBXu9a|nH zzFy>HTw5PfAGz4Z+E0A)nP*&Ep5#l>jqlMLSGKgfII_1%@7%;#qkV0QnM8p{ zU+wd356-CfG4+&sm6=3=uN z^Xz^3N}T0f&^u?c!QE?~!2Lj)=W^)n!)rqNJ+Q`nq-${`T+w_x>%wtH8$@{`jnW?isdC z2c7dsSMu)8v6$qae8w||&V1?BdE@g(b><*D4`1h{54;PU`95fzk9!lJSaa;nTVRiYiC^hQ_>AYi(hqmrD_?RrkMxn3HxPK4L-S3( zC(r|5vL5Mjuc+MX5srMZ=-|0Qnr93f-8pEJlS_R2l209s^G*C!y=Ox{ePo^HT*dXf z4>%@%=b!}7bzpwhOALNE#VN&Oiy_Z=OE8VaN3Sa^b?qgw=q&qsw(hUf1nvjYG?f!R z53A5Ti28Oi@f>mvD@$pv7om1zx{t(7`U>B58<+iNEOqL&Z4a5RPdUZ(d9#u?`_w1T zN66&2_DArpIH-4KA1e%V%9|v0z~(-7wO6q-{xdb;v7f0|>xglm&Lt)J#2X8T*HGun zzmoEI9KUllhR<_pos8)N#}ex~>bSYLe3`)gKzf4?kE|&gaQ-e|a;3`+@XkB+rgKltMgE+8#7*q=&kF*c{erC2!_?=%*Zf z_Bh3-*wnAN*+b)+EA7U{l;)y}#6&j0go|1AF&08GZ=l0SR4#+Q&sb+X6QyBGJ{ z{wsFg@H&q^nT`G0i?Z=MdzCZI9^VR|H6~2Iw~0?)>*@p7vq)QADZcZW6@3}c9@R4r zE^RUJPIFIQbQV3ipJ&hgb(+BaaGIvF@LfGn9^wVvliLH$fz3s(2cMX@#H{qIeZn*4 zbpL6mzRJhnIDEa1Fuiy1J+HRA#?Eg3#wIs&5}$pfZvN4}#38LA&y`&I$-a_z(ygVO zFpv1M2m7~%``Ym)#(ZbpJjUT8rme0tPH%~USKFReHSm+qnsV%sSaZm2o^k!Rd_95t zf%JMFZ>%#1NsP&DN;AbvwbCu>REo)r+Q91Fn&%UYAv9`Ub=%;UVq&Jz~3%thAJKi>!_bz*a#~Tm2 zer3jp=K`}@Q_OwvS9p)E?#Dh?eAZmWkMen6sL6|!`0Pc$5^Tn?U5&|aSnQWva4dYx zEc5VXT-_S-=uf+I^WkERDNk;DLW-Mw)+BbXw-dM@NN;EI(R+I_-#u$1^S$kO+nKl0GiRk+mpChD<$I91^e8st zPs!hWNk4k$B%gY6>T~YeiOE^fbH=ilTaO9+_@_Vn$^C?iflO+ix{h<2-@81`@>oyX zldL^SfBw@~E7(>3%AfpsRjs&^XTme1p3~H)n2cpFXLb9LkGz~Iabhjc@~?36?c%GL zetY))v`^#8oOA0rKWE|3!_1%Ls{fUL;yCgpm%X`Xa@Bpm#4F_|ZoYYKn}D=;?3Wc=y}2t%YP+SaxQ zyX8%Y8<~05-sKUq)+EiHxZ2ls-IEuwT=tqX6k`mmKJ(ry?Qg|n+`g47{M6r3&Wfx3 z%wghh+|JHk+*{%^t{>mZFQ#SZm~Tz(%rllg^yCtg`S`@7j&HJ)Gx_Am`f}*QHotTA zr<^dxttY2g;`e$zf%_BE>v_Dg@&;+ZLxs&v16wI(B|A59Q*1i8Q%~~PhdpT{udM9F zyvf)7i9z=N@X5Zr7j5I#d#|(?c&za#C;ROF;D@(k`mL3H=xYhj8C>U0w(m*zD>nP> zUd7nU(cFmhb6Nl7?FbM5R8yZlYft^qfA*Jp#>lmw#85+9%o$rQ^RdZGnX^w$_aRpM zmTxC;Kak$ee(hDfYl0WsHNDAz z$zD=VOyjUWYSH)ZhVp0~dChkg+Giy5@JDrL@K-VRlyiqeKYGXTx%z$8kDT@omwsy` zKQUJF;#=7h#}mOfUouZDA^XsW-RI3d<xj)cn0L$P34H$_NWc8s z|Mc&_`dffs-M8KYONV`EGd9J&P;z(Xz=h5Y582$Ale+ooL!WBlPkidca-FT8IcM}U zdpm=nR^F7H-_*B!%FI2}b7HK6kDHnG;Qq)ZFP!eP`Ex(Sj%+aY|H$vG1$n#GULu9V>?|u9N@IkGY`Ku(tk7(eU&3V z`?F5^#3fJSGHxIEe2LX=eAc@4*Aw{ue_DFI-%A#UZFN&UBJwcraB_>)_5f#Y-jJqv zVml5mHFyz&%O4e4lYIL~F7@VI`Lj))#M&+R9ZA##4&zdWlKhn#>@3YQJ_% zxjJ9}Ba+^!vs#p(%zHD`bzLDF-U!4Qto1j`x^E86VexW*YhIZ(Su8@cKXccko(ft zaurWLFE+8en(&J|Q!6htZ*J*J9X>J5{cYLojit=GlP}|$*VvqQ=j?3e^Cf0tf6DBA z)lXtqnDm2Z-FQy+j`X7j}JiApP}!^zZ!dz9WAN@Ol^Dg2h2%0jxeHeW6>g@wvyY)mS-`YnN|7O8Lyuwyu8hNPOa~FJ3$IjP0a(V9?Fi zuOv>aay>X?l@z_Uoi0?2f&i-$a zCfs{`S9_w?YHnBipf+4eX6(GLa*02BpTK3!NA?fLs#pB(2VCmdSC_M%`jmUsi*aje zEAgu<)0evM3poFqDdIUxZ1-=BJaV+NjxjmJz`>g0Vv@7*Ps#U}HxsxYNN+~+euoF~ z{Y=LX5o&nYSM`qi*~ND;>Up7hQ}U*;>ZSemel@Ro+O}`|>bN$z#AwskQBPvCmOlA& zf9l4*l+1qIhdyiOp7CYh=CpsZ_3=%KpRnOhJ}@kKjVU{Zog97BC#DaqD|7sV3u>^8 zsnbiw@N1*H$YV`0{6}#xxx}aM)?X&@>c@94?pt#LtAl7<3C@?kv+`_>=Cj_B?7XyB zwdG9g75(ak#|^^Ql{#mzt376JV$|t}rLEMT`8)mS!M4jiitXk+^{WqFb!FDdJrYk$ z>Yd|5>4-l$)VrqK`o-a&eEPxh@~=we(XWI<9ocrrh$jYKF~+)9Vp^hUL z>#uCM$cJBjg}v&3PhZr^d~{aso%h}MZZ#kMPsyLY$f?Aiy12|Uu1{Mjhq=m}r@AsR zt9<<6v#vgC6LWceXn#rh`~T!$`5(S-e+zIwoHi$>5AFozP`b|G#Ue8|{YSCHPBrme z;Yhw+PvY_>)OE79x|p=(+R41ocevHV&>O}sH)9h%Z$$JzVdQXjjgv#%6`3=Tx97j( zoBGw~`=M-}sc*6FwR5oL&sh58Ozh^=YIpM-54!`_~O`kb(WDa%Y z%KXHji)%mjy1@7Nh3DBAU zo8~h4uJqTloA|EuXl;2ogLw{(Ptu%@ovFXdZLYL4#~%5y8GdVuQD#26{>({?@gqKQ z@}zA~#^6Hd<&PZkB~Gk3d(}3cT=-n#u)*+RCFaR-fB8It`xDaVQM^^AgQHKGS(7j0 zziRq>`sL#CyHNK9pEo6aDYM=VmpNy3FZS+fm%rP?(O%%~n&!AS_p42g81!6XmBh%y zwoc+#x;d+wSwHm^pS7JUH}B*oFM9?@jyV4C!Q89?2LGxD{Vh8$pZcozqdl8{RqlE@;_@aaF1d0x#NAi!L%cDid|yIZGr4;o8B?F~j4ACu z_h_u8d@WZv{Px*7S=SpsJ&4l>hrPjNo_-!S<{-hi{AZWQm)n^6Sx29I$wjQO`0lly z!2Lj4&*qCo@&efvIg8tIoQ?H`lzGOuxLm2@^MbTzFKqE=rTOB$v1orOY5&RJzO?Og z+GpbA6R-VkNNY|rBxdj3@02&4foTua^3%o?PamACbCfvr?$KQR%6c#C?R$74vN^_LX-S>T#gPiR?M!42`6XogKw#O9&l5~-potJjv$fut1l*F8E`g>N|i6z&Sz5d#-{o0e8_tkyMO#qIJ=|iSJbBNvR+yw3i z(z!XEMsW~7dU?Tkfyl34`y=F)3&{(dy6Q@N-7mgrIkU_ahi}!Bd0?kp{JUIpm}B1b z$aAgojU&z1C$Dz)iLc}0CJDCl6F0?(y}?Rsi#@3-UjXK!+wmw0`dBR+B2bMhue zUFmx&Hxh1Y_VTO}Z~n$&gYyAoz2=r@%D;W{1nvjYJeOBq58TlKJBq&|Jus$=X7U}- zTngL2k_)VtJ|%H=r8#}G(Z7<`H4dLXrLmU8yFM6tm~tmo z#3034qb0qu@bhAsqkM9H`eI!BmTxC;Kak$eZ!TS$Dca6*m9wFT>iXq>Qg4KK4kZiu`@cotTPN$zLbxAC4^``~e_p1?0`omF87go z*P~x=D8#@zqtr96=hSv$|1kC5hx@T#=aTQSv8nIry|u;>US4J5^|b_J&5miW;^c}^ z;&Y`B4(wjeXNrjx0|(28p>gCcKXG59em{^t;9XxGFfWcgoR-t4eO6M-L!UXT9uo72 zzN$BI*+ci|9HyK{ece~v&AW?tzviajy%FPq<>E(F;F>@2VpC4>$(K3SN#805fA(*V z_=yV){(iqQM;>Kk^|wrpeBgnz*NMwm=8Mai`^o-0PM$p3A364+fAV!Kzi}~UO>KFw z*>|2DF}Hk~z}sIKyu@zp!~~`TP8x?l<&*J_FVwM?ddhcDAEsJ&0`6jocOL2c!_vFw zwx_c@u{#?ap;SEydyIHT5ZG6*E^H@(R|vMG zD`TlQzGK?Wv-1<2{96DYSo}9y{&!mNcR$Io(jUp0xK{h?2VB=Pul-#EoLFMud8Kcq zCzlwdJYewYZ)tz+HxIeOto(3UAKjNex%H>C9@zA!4(49#3EU5-#caI6^u@b?e?G*k z_@jOf^GB|{IIePg5z|lSwr$TVzwslPHIJ}Ywqcd^|W+{NJS zKCES4*KHeouA_Kjrx~vD^(*0-@{>O?<|&D(H&6SuQ=*%P?h9Z0ttH0X#$}wGd%c~& z{Ra+jXY$6w#miy($R(dQ2DtIHeYU22XYp5X$<0OPr>0fExd-Z)-~O{bcwvLL_A0k~ zGS|G0x4q)UKVfqojj?8O!ofN-3$dA>I{fYrf8tZmoYv3u(K*RYzIJknpd!^My(5^GA*X!%O56(Dm?N7T%OflEx@Ya~J9E0GIqkX*+RwUfsfzP_k3w#v~jcgv1z zPx7e#s-MhTVW^iea?(cER?6#psI+(E;_>6-JL}{VeZ^R>i4m9nj(1EuaoNwUzf9nMAid1uVa-E25qJoXF3>5r zeQ@~dnD(w0F*|9$JIoYY<(2=x`q3Z%5A$yUCaO0#>Ed{k(|c|_cKe!Shw0q5n+x0T zh^JWl{BaNa=)E-d5!=1(`X+Xz=c7K{(-Dq%_ti7V{?R+09C9^AzcSJ-b zycl~hE?%DYCm#J?Zzpg+oZiml!#p0)Jed04Q)I`{Hw7<_yoesD-4EA^FZY=>^r!EP zew5ceOmk_U^T_(vX{@$&w8>Rhib>wqdxrU5{qmQ;e7ez1Jl24(Il9JVlb;-7nkzA> zi@z#!FB4z;65qAJ8qYj+r5N+|D>JSSsZQPwBNmJ=a`dM(?kC8Mk#n!N6SyBpZ)fr$ z9uD7XbKk8+d&yA@U4<%ka z9Qd^Ik)K$yRzG}*(JTJlp01vmtcmX2+K)Ze-QmfZ;uFu#mwQTF>Jx^znAEc;rzJTv_l$m&*YUQ`)R-{tgV+_$3F{oE zI{1iBdHOnEKe?%kOJC~N>O3&w)Zt1#{YtsSVBhQQ1nvjY+nF4CJbdpxTpsGff}a3+ zu+QLknD_L(>e-oe-4(uZ+Pi&DT&umTc$%a0jVtL(t_hns_h624!oVeO=J8PC6VuT{ ziCj6%&xe9OW!9Q-$%oET%5g@LGx4wZz)y9Yb;eTfy-&R*ex+v}F-UcICye_Mrv%%t z#>$8PUau!`KagI}W3;9NHUd96J(#QbdH)~$&;R=0{rYbK;C_^UcMuIM{7jw&(3EU5-msz~gID~l+-vG+- z(Fg4jKKoTxi?7Tx7!syFCZFUJFL+<#j^wJw#6!)gE;S~6`!YXc=&scDA@wV>4}D1W z%+0##=Mx7HC31fL^Pj)k+r+ctZ_fNAle+oYd&ce4-+v)ji&*!Vyw);btWv)+{iz#A zreEEf+DP@z5vyOE62CJLD__dYH`lzzfxTBv;Kx7x*-!4LQVm7Zlc)WwE!F>v*2U-oQF*||G=;sFoW89p)ovgsGU_{Hbkqo41s z`s&)&XuN)9$5%Fc&YaX&IqlD!)eKiTnJY$)jG=3%4puz*6DD~Rt6#p9#+qOMEng;Z zKagH#@v!FMn+SG?dcx~7PkSe)3uA}x*qQvJJ+Asa!=3%C>P-A%r`YP^H)mHfdzQPU z`T8e&;t_XN@=?Wy5)UFiGWF^nIzHKtTsXs+4V+j zil60n|0^lKGI5iS_-aNI9-k|F*01zGG!ct5*80RFCUfxZ77rzUj<;G@xK$-`^BQYhjFNm~a^3Rv1iteRq<``6 z`e*;(_1^-#-rz@C`Ga+MJzUC1YMG17r9XWuJuiqA=Bht0;9c%fpS>@* zxrs}j3Fn@OO})xxygB5@&%(}}XYc)WHG%K^r=``#zHR^2L)QoQOuytjl6QtXG4H9< zb7(v3O!{hn@c1Hc;?20+laJVHkBQG3*_VE0)|zyE$cZQ8XLN3q{3wsV7w;Z2f3?TN zyjQ0e7rms7Klh8h>NowZr!8Ol&{uw9VwK%j`d9ii%f_y3dvpJZ!*{Qj3A~)97vHUY z3ls1=M-O_+E^n;;U)p}OE*D&0blOV#PQCX*-QAK~+1QDD@{#8ryQbVqYiKLwFrHG3 z(pc6a)^TwwDLx-c`lkJ4y%p!9{(b=2#~HrvD|1taD{b@eo$+V>gwMFTI3<3T^L@5n z_k|C9xK~(md!Objr=B{dJ@t~Y=E|74lh2rvhf+Qi{6@$4w;`>s?3(N~_mcWF z2jfcl(w_3br5}9eB#(aZ=I9froOsLu)A6)Z2b=YSNKKpyZfSLIiO`O02D>VnK0z-kIpEquN@Fa5cvogWV3tnNuav6XHf@;!?OLEikE zEBDi}?5$(i#+72U`D2TI5Q&+jc~cCZb2QFh7m{OM>h8-JIa6G}QoIs>uJora2A?l| zlN2*?O!%+acR!F0-u=EX4&YyP*c-#q5T+YfUbN~fo+-Y2pTr}3tzu$#dlDmO+mk;r zse@hZVb_B-lsp)A%NkvKmt%|^-aB)Z#8Vf4B+2o0ge5LEYpFXUG4gd>9^)$iF{vj8J#BFIu3Y8nKk83hN^<;VLH=X^@E`vNcYh0Tf$F;h-G6KK-Py;t z-f156Z@u_;pdj<{M@gyMx)-qzA^pmZYa3VMUvYq$-Y5E6@yRQv{P3vDD-YO=sasnd zdChA*bF_~n_|>AGwVStXadLHB|B-ZGM>y)yUoLARUCq~dVte+DlW!-j-ny}66Q8>ABRQQ5c&>7-@bXV_WAr%1+n0EAz%;I7`TH;NQx19edYQoeKzfuFA+r3TsQy#couM^MC*Id+`>VnJHiZd}js8-mo-G4ujzV;LFE$x50 zptH0WZTjA;!+Erh`dZ~TciZHpFYBZ~^{j>8SNc<5rTCa-}?j5#QLXFJ_YVw#ye|z7HO4WMeXxdg4+~%%p>5 zDJP7+slOlm;7|XrKmNym@PohY|NO1r{EZ)rT+7w`ruvf)W;pVd)h2#;d?ohQUr*rv z1Bci1xUDZd0Wa44vC*oeHv?tk=9^Qao^3f+IrMd=C)nVEN9mrja@I?nHT0R2czrF! z;NR)pPhy)BJMm!ozuB@s{yLBU2p4@dH~Flw!-2`Tb)89afor_+mE7T1vG!xLvQK^1 z%^1Fn8*^{wfBJWR@Bar^$j`3; literal 0 HcmV?d00001 From fca4570810d9f4dc6d6e75d16a88d9d943256db0 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 14 Mar 2024 10:50:39 +1000 Subject: [PATCH 26/68] Indentation --- .../test_qgsrasterlayerelevationproperties.py | 84 ++++++++++++------- 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/tests/src/python/test_qgsrasterlayerelevationproperties.py b/tests/src/python/test_qgsrasterlayerelevationproperties.py index 853a835eda1a..b7a27ab23979 100644 --- a/tests/src/python/test_qgsrasterlayerelevationproperties.py +++ b/tests/src/python/test_qgsrasterlayerelevationproperties.py @@ -9,8 +9,9 @@ __date__ = '09/11/2020' __copyright__ = 'Copyright 2020, The QGIS Project' -import os import math +import os +import unittest from qgis.PyQt.QtXml import QDomDocument from qgis.core import ( @@ -22,7 +23,6 @@ QgsRasterLayer, QgsDoubleRange ) -import unittest from qgis.testing import start_app, QgisTestCase from utilities import unitTestDataPath @@ -37,7 +37,8 @@ def test_basic_elevation_surface(self): Basic tests for the class using the RepresentsElevationSurface mode """ props = QgsRasterLayerElevationProperties(None) - self.assertEqual(props.mode(), Qgis.RasterElevationMode.RepresentsElevationSurface) + self.assertEqual(props.mode(), + Qgis.RasterElevationMode.RepresentsElevationSurface) self.assertEqual(props.zScale(), 1) self.assertEqual(props.zOffset(), 0) self.assertFalse(props.isEnabled()) @@ -46,7 +47,8 @@ def test_basic_elevation_surface(self): self.assertTrue(props.fixedRange().isInfinite()) self.assertIsInstance(props.profileLineSymbol(), QgsLineSymbol) self.assertIsInstance(props.profileFillSymbol(), QgsFillSymbol) - self.assertEqual(props.profileSymbology(), Qgis.ProfileSurfaceSymbology.Line) + self.assertEqual(props.profileSymbology(), + Qgis.ProfileSurfaceSymbology.Line) props.setZOffset(0.5) props.setZScale(2) @@ -59,10 +61,12 @@ def test_basic_elevation_surface(self): self.assertTrue(props.isEnabled()) self.assertEqual(props.bandNumber(), 2) self.assertTrue(props.hasElevation()) - self.assertEqual(props.profileSymbology(), Qgis.ProfileSurfaceSymbology.FillBelow) + self.assertEqual(props.profileSymbology(), + Qgis.ProfileSurfaceSymbology.FillBelow) self.assertEqual(props.elevationLimit(), 909) - sym = QgsLineSymbol.createSimple({'outline_color': '#ff4433', 'outline_width': 0.5}) + sym = QgsLineSymbol.createSimple( + {'outline_color': '#ff4433', 'outline_width': 0.5}) props.setProfileLineSymbol(sym) self.assertEqual(props.profileLineSymbol().color().name(), '#ff4433') @@ -76,25 +80,29 @@ def test_basic_elevation_surface(self): props2 = QgsRasterLayerElevationProperties(None) props2.readXml(elem, QgsReadWriteContext()) - self.assertEqual(props2.mode(), Qgis.RasterElevationMode.RepresentsElevationSurface) + self.assertEqual(props2.mode(), + Qgis.RasterElevationMode.RepresentsElevationSurface) self.assertEqual(props2.zScale(), 2) self.assertEqual(props2.zOffset(), 0.5) self.assertTrue(props2.isEnabled()) self.assertEqual(props2.bandNumber(), 2) self.assertEqual(props2.profileLineSymbol().color().name(), '#ff4433') self.assertEqual(props2.profileFillSymbol().color().name(), '#ff44ff') - self.assertEqual(props2.profileSymbology(), Qgis.ProfileSurfaceSymbology.FillBelow) + self.assertEqual(props2.profileSymbology(), + Qgis.ProfileSurfaceSymbology.FillBelow) self.assertEqual(props2.elevationLimit(), 909) props2 = props.clone() - self.assertEqual(props2.mode(), Qgis.RasterElevationMode.RepresentsElevationSurface) + self.assertEqual(props2.mode(), + Qgis.RasterElevationMode.RepresentsElevationSurface) self.assertEqual(props2.zScale(), 2) self.assertEqual(props2.zOffset(), 0.5) self.assertTrue(props2.isEnabled()) self.assertEqual(props2.bandNumber(), 2) self.assertEqual(props2.profileLineSymbol().color().name(), '#ff4433') self.assertEqual(props2.profileFillSymbol().color().name(), '#ff44ff') - self.assertEqual(props2.profileSymbology(), Qgis.ProfileSurfaceSymbology.FillBelow) + self.assertEqual(props2.profileSymbology(), + Qgis.ProfileSurfaceSymbology.FillBelow) self.assertEqual(props2.elevationLimit(), 909) def test_basic_fixed_range(self): @@ -110,7 +118,8 @@ def test_basic_fixed_range(self): props.setZOffset(0.5) props.setZScale(2) self.assertEqual(props.fixedRange(), QgsDoubleRange(103.1, 106.8)) - self.assertEqual(props.calculateZRange(None), QgsDoubleRange(103.1, 106.8)) + self.assertEqual(props.calculateZRange(None), + QgsDoubleRange(103.1, 106.8)) self.assertFalse(props.isVisibleInZRange(QgsDoubleRange(3.1, 6.8))) self.assertTrue(props.isVisibleInZRange(QgsDoubleRange(3.1, 104.8))) self.assertTrue(props.isVisibleInZRange(QgsDoubleRange(104.8, 114.8))) @@ -122,11 +131,13 @@ def test_basic_fixed_range(self): props2 = QgsRasterLayerElevationProperties(None) props2.readXml(elem, QgsReadWriteContext()) - self.assertEqual(props2.mode(), Qgis.RasterElevationMode.FixedElevationRange) + self.assertEqual(props2.mode(), + Qgis.RasterElevationMode.FixedElevationRange) self.assertEqual(props2.fixedRange(), QgsDoubleRange(103.1, 106.8)) props2 = props.clone() - self.assertEqual(props2.mode(), Qgis.RasterElevationMode.FixedElevationRange) + self.assertEqual(props2.mode(), + Qgis.RasterElevationMode.FixedElevationRange) self.assertEqual(props2.fixedRange(), QgsDoubleRange(103.1, 106.8)) # include lower, exclude upper @@ -166,7 +177,9 @@ def test_looks_like_dem(self): # layer data type doesn't look like a dem layer = QgsRasterLayer( - os.path.join(unitTestDataPath(), 'raster/band1_byte_ct_epsg4326.tif'), 'i am not a dem') + os.path.join(unitTestDataPath(), + 'raster/band1_byte_ct_epsg4326.tif'), + 'i am not a dem') self.assertTrue(layer.isValid()) self.assertFalse( QgsRasterLayerElevationProperties.layerLooksLikeDem(layer)) @@ -176,7 +189,8 @@ def test_looks_like_dem(self): self.assertTrue(layer.isValid()) # not like a dem, the layer name doesn't hint this to - self.assertFalse(QgsRasterLayerElevationProperties.layerLooksLikeDem(layer)) + self.assertFalse( + QgsRasterLayerElevationProperties.layerLooksLikeDem(layer)) layer.setName('i am a DEM') self.assertTrue( QgsRasterLayerElevationProperties.layerLooksLikeDem(layer)) @@ -194,34 +208,42 @@ def test_elevation_range_for_pixel_value(self): """ props = QgsRasterLayerElevationProperties(None) - self.assertEqual(props.elevationRangeForPixelValue(band=1, pixelValue=3), - QgsDoubleRange()) + self.assertEqual( + props.elevationRangeForPixelValue(band=1, pixelValue=3), + QgsDoubleRange()) props.setEnabled(True) - self.assertEqual(props.elevationRangeForPixelValue(band=1, pixelValue=3), - QgsDoubleRange(3,3)) - self.assertEqual(props.elevationRangeForPixelValue(band=1, pixelValue=math.nan), - QgsDoubleRange()) + self.assertEqual( + props.elevationRangeForPixelValue(band=1, pixelValue=3), + QgsDoubleRange(3, 3)) + self.assertEqual( + props.elevationRangeForPixelValue(band=1, pixelValue=math.nan), + QgsDoubleRange()) # check that band number is respected props.setBandNumber(2) - self.assertEqual(props.elevationRangeForPixelValue(band=1, pixelValue=3), - QgsDoubleRange()) - self.assertEqual(props.elevationRangeForPixelValue(band=2, pixelValue=3), - QgsDoubleRange(3,3)) + self.assertEqual( + props.elevationRangeForPixelValue(band=1, pixelValue=3), + QgsDoubleRange()) + self.assertEqual( + props.elevationRangeForPixelValue(band=2, pixelValue=3), + QgsDoubleRange(3, 3)) # check that offset/scale is respected props.setZOffset(0.5) props.setZScale(2) - self.assertEqual(props.elevationRangeForPixelValue(band=2, pixelValue=3), - QgsDoubleRange(6.5, 6.5)) + self.assertEqual( + props.elevationRangeForPixelValue(band=2, pixelValue=3), + QgsDoubleRange(6.5, 6.5)) # with fixed range mode props.setMode(Qgis.RasterElevationMode.FixedElevationRange) props.setFixedRange(QgsDoubleRange(11, 15)) - self.assertEqual(props.elevationRangeForPixelValue(band=1, pixelValue=math.nan), - QgsDoubleRange()) - self.assertEqual(props.elevationRangeForPixelValue(band=1, pixelValue=3), - QgsDoubleRange(11, 15)) + self.assertEqual( + props.elevationRangeForPixelValue(band=1, pixelValue=math.nan), + QgsDoubleRange()) + self.assertEqual( + props.elevationRangeForPixelValue(band=1, pixelValue=3), + QgsDoubleRange(11, 15)) if __name__ == '__main__': From 7fc4ed39990f81057969d27a8174dc730cb261f3 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 16 Mar 2024 09:41:50 +1000 Subject: [PATCH 27/68] Add range limits api to QgsRange --- .../PyQt6/core/auto_generated/qgsrange.sip.in | 31 ++++++++++ python/core/auto_generated/qgsrange.sip.in | 31 ++++++++++ src/core/qgsrange.h | 50 +++++++++++++++ tests/src/python/test_qgsrange.py | 62 ++++++++++++++++++- 4 files changed, 173 insertions(+), 1 deletion(-) diff --git a/python/PyQt6/core/auto_generated/qgsrange.sip.in b/python/PyQt6/core/auto_generated/qgsrange.sip.in index cb27d74b71cd..12f109f64d0b 100644 --- a/python/PyQt6/core/auto_generated/qgsrange.sip.in +++ b/python/PyQt6/core/auto_generated/qgsrange.sip.in @@ -41,6 +41,14 @@ whether ranges overlap or during calculation of range intersections. %Docstring Constructor for QgsRange. The ``lower`` and ``upper`` bounds are specified, and optionally whether or not these bounds are included in the range. +%End + + QgsRange( T lower, T upper, Qgis::RangeLimits limits ); +%Docstring +Constructor for QgsRange. The ``lower`` and ``upper`` bounds are specified, +and whether or not these bounds are included in the range. + +.. versionadded:: 3.38 %End T lower() const; @@ -79,6 +87,13 @@ bound is exclusive. .. seealso:: :py:func:`upper` .. seealso:: :py:func:`includeLower` +%End + + Qgis::RangeLimits rangeLimits() const; +%Docstring +Returns the limit handling of the range. + +.. versionadded:: 3.38 %End bool isEmpty() const; @@ -146,6 +161,14 @@ typedef QgsRange QgsRangedoubleBase; %End public: + QgsDoubleRange( double lower, double upper, Qgis::RangeLimits limits ); +%Docstring +Constructor for QgsDoubleRange. The ``lower`` and ``upper`` bounds are specified, +and whether or not these bounds are included in the range. + +.. versionadded:: 3.38 +%End + QgsDoubleRange( double lower, double upper, @@ -207,6 +230,14 @@ typedef QgsRange QgsRangeintBase; %End public: + QgsIntRange( int lower, int upper, Qgis::RangeLimits limits ); +%Docstring +Constructor for QgsIntRange. The ``lower`` and ``upper`` bounds are specified, +and whether or not these bounds are included in the range. + +.. versionadded:: 3.38 +%End + QgsIntRange( int lower, int upper, diff --git a/python/core/auto_generated/qgsrange.sip.in b/python/core/auto_generated/qgsrange.sip.in index cb27d74b71cd..12f109f64d0b 100644 --- a/python/core/auto_generated/qgsrange.sip.in +++ b/python/core/auto_generated/qgsrange.sip.in @@ -41,6 +41,14 @@ whether ranges overlap or during calculation of range intersections. %Docstring Constructor for QgsRange. The ``lower`` and ``upper`` bounds are specified, and optionally whether or not these bounds are included in the range. +%End + + QgsRange( T lower, T upper, Qgis::RangeLimits limits ); +%Docstring +Constructor for QgsRange. The ``lower`` and ``upper`` bounds are specified, +and whether or not these bounds are included in the range. + +.. versionadded:: 3.38 %End T lower() const; @@ -79,6 +87,13 @@ bound is exclusive. .. seealso:: :py:func:`upper` .. seealso:: :py:func:`includeLower` +%End + + Qgis::RangeLimits rangeLimits() const; +%Docstring +Returns the limit handling of the range. + +.. versionadded:: 3.38 %End bool isEmpty() const; @@ -146,6 +161,14 @@ typedef QgsRange QgsRangedoubleBase; %End public: + QgsDoubleRange( double lower, double upper, Qgis::RangeLimits limits ); +%Docstring +Constructor for QgsDoubleRange. The ``lower`` and ``upper`` bounds are specified, +and whether or not these bounds are included in the range. + +.. versionadded:: 3.38 +%End + QgsDoubleRange( double lower, double upper, @@ -207,6 +230,14 @@ typedef QgsRange QgsRangeintBase; %End public: + QgsIntRange( int lower, int upper, Qgis::RangeLimits limits ); +%Docstring +Constructor for QgsIntRange. The ``lower`` and ``upper`` bounds are specified, +and whether or not these bounds are included in the range. + +.. versionadded:: 3.38 +%End + QgsIntRange( int lower, int upper, diff --git a/src/core/qgsrange.h b/src/core/qgsrange.h index aea46eef3748..163121301662 100644 --- a/src/core/qgsrange.h +++ b/src/core/qgsrange.h @@ -57,6 +57,19 @@ class QgsRange , mIncludeUpper( includeUpper ) {} + /** + * Constructor for QgsRange. The \a lower and \a upper bounds are specified, + * and whether or not these bounds are included in the range. + * + * \since QGIS 3.38 + */ + QgsRange( T lower, T upper, Qgis::RangeLimits limits ) + : mLower( lower ) + , mUpper( upper ) + , mIncludeLower( limits == Qgis::RangeLimits::IncludeLowerExcludeUpper || limits == Qgis::RangeLimits::IncludeBoth ) + , mIncludeUpper( limits == Qgis::RangeLimits::ExcludeLowerIncludeUpper || limits == Qgis::RangeLimits::IncludeBoth ) + {} + /** * Returns the lower bound of the range. * \see upper() @@ -87,6 +100,23 @@ class QgsRange */ bool includeUpper() const { return mIncludeUpper; } + /** + * Returns the limit handling of the range. + * + * \since QGIS 3.38 + */ + Qgis::RangeLimits rangeLimits() const + { + if ( mIncludeLower && mIncludeUpper ) + return Qgis::RangeLimits::IncludeBoth; + else if ( mIncludeLower && !mIncludeUpper ) + return Qgis::RangeLimits::IncludeLowerExcludeUpper; + else if ( !mIncludeLower && mIncludeUpper ) + return Qgis::RangeLimits::ExcludeLowerIncludeUpper; + else + return Qgis::RangeLimits::ExcludeBoth; + } + /** * Returns TRUE if the range is empty, ie the lower bound equals (or exceeds) the upper bound * and either the bounds are exclusive. @@ -201,6 +231,16 @@ class CORE_EXPORT QgsDoubleRange : public QgsRange< double > { public: + /** + * Constructor for QgsDoubleRange. The \a lower and \a upper bounds are specified, + * and whether or not these bounds are included in the range. + * + * \since QGIS 3.38 + */ + QgsDoubleRange( double lower, double upper, Qgis::RangeLimits limits ) + : QgsRange( lower, upper, limits ) + {} + #ifndef SIP_RUN /** @@ -287,6 +327,16 @@ class CORE_EXPORT QgsIntRange : public QgsRange< int > { public: + /** + * Constructor for QgsIntRange. The \a lower and \a upper bounds are specified, + * and whether or not these bounds are included in the range. + * + * \since QGIS 3.38 + */ + QgsIntRange( int lower, int upper, Qgis::RangeLimits limits ) + : QgsRange( lower, upper, limits ) + {} + #ifndef SIP_RUN /** diff --git a/tests/src/python/test_qgsrange.py b/tests/src/python/test_qgsrange.py index ed59039bce15..f1cef0877539 100644 --- a/tests/src/python/test_qgsrange.py +++ b/tests/src/python/test_qgsrange.py @@ -10,7 +10,7 @@ __copyright__ = 'Copyright 2017, The QGIS Project' from qgis.PyQt.QtCore import QDate -from qgis.core import QgsDateRange, QgsDoubleRange, QgsIntRange +from qgis.core import QgsDateRange, QgsDoubleRange, QgsIntRange, Qgis from qgis.testing import unittest @@ -22,12 +22,42 @@ def testGetters(self): self.assertEqual(range.upper(), 11) self.assertTrue(range.includeLower()) self.assertTrue(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.IncludeBoth) range = QgsIntRange(-1, 3, False, False) self.assertEqual(range.lower(), -1) self.assertEqual(range.upper(), 3) self.assertFalse(range.includeLower()) self.assertFalse(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.ExcludeBoth) + + range = QgsIntRange(-1, 3, Qgis.RangeLimits.IncludeBoth) + self.assertEqual(range.lower(), -1) + self.assertEqual(range.upper(), 3) + self.assertTrue(range.includeLower()) + self.assertTrue(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.IncludeBoth) + + range = QgsIntRange(-1, 3, Qgis.RangeLimits.IncludeLowerExcludeUpper) + self.assertEqual(range.lower(), -1) + self.assertEqual(range.upper(), 3) + self.assertTrue(range.includeLower()) + self.assertFalse(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.IncludeLowerExcludeUpper) + + range = QgsIntRange(-1, 3, Qgis.RangeLimits.ExcludeLowerIncludeUpper) + self.assertEqual(range.lower(), -1) + self.assertEqual(range.upper(), 3) + self.assertFalse(range.includeLower()) + self.assertTrue(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.ExcludeLowerIncludeUpper) + + range = QgsIntRange(-1, 3, Qgis.RangeLimits.ExcludeBoth) + self.assertEqual(range.lower(), -1) + self.assertEqual(range.upper(), 3) + self.assertFalse(range.includeLower()) + self.assertFalse(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.ExcludeBoth) def testIsInfinite(self): range = QgsIntRange() @@ -207,12 +237,42 @@ def testGetters(self): self.assertEqual(range.upper(), 11) self.assertTrue(range.includeLower()) self.assertTrue(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.IncludeBoth) range = QgsDoubleRange(-1.0, 3.0, False, False) self.assertEqual(range.lower(), -1) self.assertEqual(range.upper(), 3) self.assertFalse(range.includeLower()) self.assertFalse(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.ExcludeBoth) + + range = QgsIntRange(-1, 3, Qgis.RangeLimits.IncludeBoth) + self.assertEqual(range.lower(), -1) + self.assertEqual(range.upper(), 3) + self.assertTrue(range.includeLower()) + self.assertTrue(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.IncludeBoth) + + range = QgsIntRange(-1, 3, Qgis.RangeLimits.IncludeLowerExcludeUpper) + self.assertEqual(range.lower(), -1) + self.assertEqual(range.upper(), 3) + self.assertTrue(range.includeLower()) + self.assertFalse(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.IncludeLowerExcludeUpper) + + range = QgsIntRange(-1, 3, Qgis.RangeLimits.ExcludeLowerIncludeUpper) + self.assertEqual(range.lower(), -1) + self.assertEqual(range.upper(), 3) + self.assertFalse(range.includeLower()) + self.assertTrue(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.ExcludeLowerIncludeUpper) + + range = QgsIntRange(-1, 3, Qgis.RangeLimits.ExcludeBoth) + self.assertEqual(range.lower(), -1) + self.assertEqual(range.upper(), 3) + self.assertFalse(range.includeLower()) + self.assertFalse(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.ExcludeBoth) def testEquality(self): self.assertEqual(QgsDoubleRange(1, 10), QgsDoubleRange(1, 10)) From c46ae9b630cb8d4efae0033bb32d576bd1e59990 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 16 Mar 2024 09:41:56 +1000 Subject: [PATCH 28/68] Simplify code --- .../qgsrasterelevationpropertieswidget.cpp | 28 ++----------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/src/app/raster/qgsrasterelevationpropertieswidget.cpp b/src/app/raster/qgsrasterelevationpropertieswidget.cpp index 67508c015d55..8ff05b96d651 100644 --- a/src/app/raster/qgsrasterelevationpropertieswidget.cpp +++ b/src/app/raster/qgsrasterelevationpropertieswidget.cpp @@ -130,14 +130,7 @@ void QgsRasterElevationPropertiesWidget::syncToLayer( QgsMapLayer *layer ) mFixedUpperSpinBox->setValue( props->fixedRange().upper() ); else mFixedUpperSpinBox->clear(); - if ( props->fixedRange().includeLower() && props->fixedRange().includeUpper() ) - mLimitsComboBox->setCurrentIndex( mLimitsComboBox->findData( QVariant::fromValue( Qgis::RangeLimits::IncludeBoth ) ) ); - else if ( props->fixedRange().includeLower() ) - mLimitsComboBox->setCurrentIndex( mLimitsComboBox->findData( QVariant::fromValue( Qgis::RangeLimits::IncludeLowerExcludeUpper ) ) ); - else if ( props->fixedRange().includeUpper() ) - mLimitsComboBox->setCurrentIndex( mLimitsComboBox->findData( QVariant::fromValue( Qgis::RangeLimits::ExcludeLowerIncludeUpper ) ) ); - else - mLimitsComboBox->setCurrentIndex( mLimitsComboBox->findData( QVariant::fromValue( Qgis::RangeLimits::ExcludeBoth ) ) ); + mLimitsComboBox->setCurrentIndex( mLimitsComboBox->findData( QVariant::fromValue( props->fixedRange().rangeLimits() ) ) ); mStyleComboBox->setCurrentIndex( mStyleComboBox->findData( static_cast ( props->profileSymbology() ) ) ); switch ( props->profileSymbology() ) @@ -189,24 +182,7 @@ void QgsRasterElevationPropertiesWidget::apply() if ( mFixedUpperSpinBox->value() != mFixedUpperSpinBox->clearValue() ) fixedUpper = mFixedUpperSpinBox->value(); - bool includeLower = true; - bool includeUpper = true; - switch ( mLimitsComboBox->currentData().value< Qgis::RangeLimits >() ) - { - case Qgis::RangeLimits::IncludeBoth: - break; - case Qgis::RangeLimits::IncludeLowerExcludeUpper: - includeUpper = false; - break; - case Qgis::RangeLimits::ExcludeLowerIncludeUpper: - includeLower = false; - break; - case Qgis::RangeLimits::ExcludeBoth: - includeLower = false; - includeUpper = false; - break; - } - props->setFixedRange( QgsDoubleRange( fixedLower, fixedUpper, includeLower, includeUpper ) ); + props->setFixedRange( QgsDoubleRange( fixedLower, fixedUpper, mLimitsComboBox->currentData().value< Qgis::RangeLimits >() ) ); mLayer->trigger3DUpdate(); } From a5b4d9743e8cab61df3ad3878a46a44f60be2888 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 16 Mar 2024 09:42:52 +1000 Subject: [PATCH 29/68] Microoptimisation --- src/core/raster/qgsrasterlayerelevationproperties.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/raster/qgsrasterlayerelevationproperties.cpp b/src/core/raster/qgsrasterlayerelevationproperties.cpp index a9a42cae727d..e217f30d9960 100644 --- a/src/core/raster/qgsrasterlayerelevationproperties.cpp +++ b/src/core/raster/qgsrasterlayerelevationproperties.cpp @@ -237,7 +237,8 @@ QgsDoubleRange QgsRasterLayerElevationProperties::elevationRangeForPixelValue( i if ( band != mBandNumber ) return QgsDoubleRange(); - return QgsDoubleRange( pixelValue * mZScale + mZOffset, pixelValue * mZScale + mZOffset ); + const double z = pixelValue * mZScale + mZOffset; + return QgsDoubleRange( z, z ); } } BUILTIN_UNREACHABLE From a91f5bf7c2e95aeddc01b0e834be67c8f6bad55a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 14 Mar 2024 14:21:47 +1000 Subject: [PATCH 30/68] Add a fixed range width option to QgsRangeSlider Allows forcing the widget to have a specific fixed range width, so that interactions with the lower or upper slider automatically force the other slider to move to keep a constant width --- .../gui/auto_generated/qgsrangeslider.sip.in | 37 ++++++ .../gui/auto_generated/qgsrangeslider.sip.in | 37 ++++++ src/gui/qgsrangeslider.cpp | 116 +++++++++++++++++- src/gui/qgsrangeslider.h | 36 ++++++ tests/src/python/test_qgsrangeslider.py | 54 ++++++++ 5 files changed, 276 insertions(+), 4 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/qgsrangeslider.sip.in b/python/PyQt6/gui/auto_generated/qgsrangeslider.sip.in index 2f4fe6ef4fe4..8b2e0769305f 100644 --- a/python/PyQt6/gui/auto_generated/qgsrangeslider.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsrangeslider.sip.in @@ -168,6 +168,32 @@ This corresponds to the larger increment or decrement applied when the user pres .. seealso:: :py:func:`setPageStep` .. seealso:: :py:func:`singleStep` +%End + + int fixedRangeWidth() const; +%Docstring +Returns the slider's fixed range width, or -1 if not set. + +If a fixed range width is set then moving either the lower or upper slider will automatically +move the other slider accordingly, in order to keep the selected range at the specified +fixed width. + +.. seealso:: :py:func:`setFixedRangeWidth` + +.. versionadded:: 3.38 +%End + + void setFixedRangeWidth( int width ); +%Docstring +Sets the slider's fixed range ``width``. Set to -1 if no fixed width is desired. + +If a fixed range width is set then moving either the lower or upper slider will automatically +move the other slider accordingly, in order to keep the selected range at the specified +fixed width. + +.. seealso:: :py:func:`fixedRangeWidth` + +.. versionadded:: 3.38 %End public slots: @@ -265,6 +291,17 @@ Emitted when the range selected in the widget is changed. void rangeLimitsChanged( int minimum, int maximum ); %Docstring Emitted when the limits of values allowed in the widget is changed. +%End + + void fixedRangeWidthChanged( int width ); +%Docstring +Emitted when the widget's fixed range width is changed. + +.. seealso:: :py:func:`fixedRangeWidth` + +.. seealso:: :py:func:`setFixedRangeWidth` + +.. versionadded:: 3.38 %End }; diff --git a/python/gui/auto_generated/qgsrangeslider.sip.in b/python/gui/auto_generated/qgsrangeslider.sip.in index 2f4fe6ef4fe4..8b2e0769305f 100644 --- a/python/gui/auto_generated/qgsrangeslider.sip.in +++ b/python/gui/auto_generated/qgsrangeslider.sip.in @@ -168,6 +168,32 @@ This corresponds to the larger increment or decrement applied when the user pres .. seealso:: :py:func:`setPageStep` .. seealso:: :py:func:`singleStep` +%End + + int fixedRangeWidth() const; +%Docstring +Returns the slider's fixed range width, or -1 if not set. + +If a fixed range width is set then moving either the lower or upper slider will automatically +move the other slider accordingly, in order to keep the selected range at the specified +fixed width. + +.. seealso:: :py:func:`setFixedRangeWidth` + +.. versionadded:: 3.38 +%End + + void setFixedRangeWidth( int width ); +%Docstring +Sets the slider's fixed range ``width``. Set to -1 if no fixed width is desired. + +If a fixed range width is set then moving either the lower or upper slider will automatically +move the other slider accordingly, in order to keep the selected range at the specified +fixed width. + +.. seealso:: :py:func:`fixedRangeWidth` + +.. versionadded:: 3.38 %End public slots: @@ -265,6 +291,17 @@ Emitted when the range selected in the widget is changed. void rangeLimitsChanged( int minimum, int maximum ); %Docstring Emitted when the limits of values allowed in the widget is changed. +%End + + void fixedRangeWidthChanged( int width ); +%Docstring +Emitted when the widget's fixed range width is changed. + +.. seealso:: :py:func:`fixedRangeWidth` + +.. seealso:: :py:func:`setFixedRangeWidth` + +.. versionadded:: 3.38 %End }; diff --git a/src/gui/qgsrangeslider.cpp b/src/gui/qgsrangeslider.cpp index 5225d93e2051..9331cbba6128 100644 --- a/src/gui/qgsrangeslider.cpp +++ b/src/gui/qgsrangeslider.cpp @@ -120,7 +120,15 @@ void QgsRangeSlider::setLowerValue( int lowerValue ) return; mLowerValue = std::min( mStyleOption.maximum, std::max( mStyleOption.minimum, lowerValue ) ); - mUpperValue = std::max( mLowerValue, mUpperValue ); + if ( mFixedRangeWidth >= 0 ) + { + mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + } + else + { + mUpperValue = std::max( mLowerValue, mUpperValue ); + } emit rangeChanged( mLowerValue, mUpperValue ); update(); } @@ -137,7 +145,16 @@ void QgsRangeSlider::setUpperValue( int upperValue ) return; mUpperValue = std::max( mStyleOption.minimum, std::min( mStyleOption.maximum, upperValue ) ); - mLowerValue = std::min( mLowerValue, mUpperValue ); + if ( mFixedRangeWidth >= 0 ) + { + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); + } + else + { + mLowerValue = std::min( mLowerValue, mUpperValue ); + } + emit rangeChanged( mLowerValue, mUpperValue ); update(); } @@ -152,6 +169,15 @@ void QgsRangeSlider::setRange( int lower, int upper ) mLowerValue = std::min( mStyleOption.maximum, std::max( mStyleOption.minimum, lower ) ); mUpperValue = std::min( mStyleOption.maximum, std::max( mStyleOption.minimum, upper ) ); + if ( mFixedRangeWidth >= 0 ) + { + mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + } + else + { + mUpperValue = std::min( mStyleOption.maximum, std::max( mStyleOption.minimum, upper ) ); + } emit rangeChanged( mLowerValue, mUpperValue ); update(); } @@ -317,6 +343,24 @@ QRect QgsRangeSlider::selectedRangeRect() return selectionRect.adjusted( -1, 1, 1, -1 ); } +int QgsRangeSlider::fixedRangeWidth() const +{ + return mFixedRangeWidth; +} + +void QgsRangeSlider::setFixedRangeWidth( int width ) +{ + if ( width == mFixedRangeWidth ) + return; + + mFixedRangeWidth = width; + + if ( mFixedRangeWidth >= 0 ) + setUpperValue( mLowerValue + mFixedRangeWidth ); + + emit fixedRangeWidthChanged( mFixedRangeWidth ); +} + void QgsRangeSlider::applyStep( int step ) { switch ( mFocusControl ) @@ -327,6 +371,11 @@ void QgsRangeSlider::applyStep( int step ) if ( newLowerValue != mLowerValue ) { mLowerValue = newLowerValue; + if ( mFixedRangeWidth >= 0 ) + { + mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + } emit rangeChanged( mLowerValue, mUpperValue ); update(); } @@ -339,6 +388,11 @@ void QgsRangeSlider::applyStep( int step ) if ( newUpperValue != mUpperValue ) { mUpperValue = newUpperValue; + if ( mFixedRangeWidth >= 0 ) + { + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); + } emit rangeChanged( mLowerValue, mUpperValue ); update(); } @@ -354,7 +408,15 @@ void QgsRangeSlider::applyStep( int step ) if ( newLowerValue != mLowerValue ) { mLowerValue = newLowerValue; - mUpperValue = std::min( mStyleOption.maximum, mLowerValue + previousWidth ); + if ( mFixedRangeWidth >= 0 ) + { + mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + } + else + { + mUpperValue = std::min( mStyleOption.maximum, mLowerValue + previousWidth ); + } emit rangeChanged( mLowerValue, mUpperValue ); update(); } @@ -366,7 +428,15 @@ void QgsRangeSlider::applyStep( int step ) if ( newUpperValue != mUpperValue ) { mUpperValue = newUpperValue; - mLowerValue = std::max( mStyleOption.minimum, mUpperValue - previousWidth ); + if ( mFixedRangeWidth >= 0 ) + { + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); + } + else + { + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - previousWidth ); + } emit rangeChanged( mLowerValue, mUpperValue ); update(); } @@ -604,6 +674,12 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event ) { changed = true; mUpperValue = mPreDragUpperValue; + if ( mFixedRangeWidth >= 0 ) + { + // don't permit fixed width drags if it pushes the other value out of range + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); + } } } else if ( newPosition > mStartDragPos ) @@ -614,6 +690,12 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event ) { changed = true; mLowerValue = mPreDragLowerValue; + if ( mFixedRangeWidth >= 0 ) + { + // don't permit fixed width drags if it pushes the other value out of range + mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + } } } else @@ -623,11 +705,23 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event ) { changed = true; mUpperValue = mPreDragUpperValue; + if ( mFixedRangeWidth >= 0 ) + { + // don't permit fixed width drags if it pushes the other value out of range + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); + } } if ( mLowerValue != mPreDragLowerValue ) { changed = true; mLowerValue = mPreDragLowerValue; + if ( mFixedRangeWidth >= 0 ) + { + // don't permit fixed width drags if it pushes the other value out of range + mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + } } } } @@ -645,6 +739,13 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event ) if ( mLowerValue != newPosition ) { mLowerValue = newPosition; + if ( mFixedRangeWidth >= 0 ) + { + // don't permit fixed width drags if it pushes the other value out of range + mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + } + changed = true; } break; @@ -657,6 +758,13 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event ) if ( mUpperValue != newPosition ) { mUpperValue = newPosition; + if ( mFixedRangeWidth >= 0 ) + { + // don't permit fixed width drags if it pushes the other value out of range + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); + } + changed = true; } break; diff --git a/src/gui/qgsrangeslider.h b/src/gui/qgsrangeslider.h index b94ded7e1b92..2bd05d115a02 100644 --- a/src/gui/qgsrangeslider.h +++ b/src/gui/qgsrangeslider.h @@ -169,6 +169,30 @@ class GUI_EXPORT QgsRangeSlider : public QWidget */ int pageStep() const; + /** + * Returns the slider's fixed range width, or -1 if not set. + * + * If a fixed range width is set then moving either the lower or upper slider will automatically + * move the other slider accordingly, in order to keep the selected range at the specified + * fixed width. + * + * \see setFixedRangeWidth() + * \since QGIS 3.38 + */ + int fixedRangeWidth() const; + + /** + * Sets the slider's fixed range \a width. Set to -1 if no fixed width is desired. + * + * If a fixed range width is set then moving either the lower or upper slider will automatically + * move the other slider accordingly, in order to keep the selected range at the specified + * fixed width. + * + * \see fixedRangeWidth() + * \since QGIS 3.38 + */ + void setFixedRangeWidth( int width ); + public slots: /** @@ -255,6 +279,16 @@ class GUI_EXPORT QgsRangeSlider : public QWidget */ void rangeLimitsChanged( int minimum, int maximum ); + /** + * Emitted when the widget's fixed range width is changed. + * + * \see fixedRangeWidth() + * \see setFixedRangeWidth() + * + * \since QGIS 3.38 + */ + void fixedRangeWidthChanged( int width ); + private: int pick( const QPoint &pt ) const; @@ -270,6 +304,8 @@ class GUI_EXPORT QgsRangeSlider : public QWidget int mSingleStep = 1; int mPageStep = 10; + int mFixedRangeWidth = -1; + QStyleOptionSlider mStyleOption; enum Control { diff --git a/tests/src/python/test_qgsrangeslider.py b/tests/src/python/test_qgsrangeslider.py index 20cab71d3bef..a563ad22d5d1 100644 --- a/tests/src/python/test_qgsrangeslider.py +++ b/tests/src/python/test_qgsrangeslider.py @@ -41,6 +41,10 @@ def testSettersGetters(self): w.setPageStep(5) self.assertEqual(w.pageStep(), 5) + self.assertEqual(w.fixedRangeWidth(), -1) + w.setFixedRangeWidth(5) + self.assertEqual(w.fixedRangeWidth(), 5) + def testLimits(self): w = QgsRangeSlider() spy = QSignalSpy(w.rangeLimitsChanged) @@ -268,6 +272,56 @@ def testChangeLimitsOutsideValue(self): self.assertEqual(len(spy), 6) self.assertEqual(spy[-1], [3, 7]) + def test_fixed_range_width(self): + """ + Test interactions with fixed range widths + """ + w = QgsRangeSlider() + w.setRangeLimits(0, 100) + w.setFixedRangeWidth(10) + self.assertEqual(w.upperValue() - w.lowerValue(), 10) + + w.setUpperValue(70) + self.assertEqual(w.upperValue(), 70) + self.assertEqual(w.lowerValue(), 60) + + w.setLowerValue(5) + self.assertEqual(w.upperValue(), 15) + self.assertEqual(w.lowerValue(), 5) + + # try to force value outside range + w.setUpperValue(5) + self.assertEqual(w.upperValue(), 10) + self.assertEqual(w.lowerValue(), 0) + + w.setLowerValue(95) + self.assertEqual(w.upperValue(), 100) + self.assertEqual(w.lowerValue(), 90) + + w.setRange(0, 5) + self.assertEqual(w.upperValue(), 10) + self.assertEqual(w.lowerValue(), 0) + + w.setRange(95, 100) + self.assertEqual(w.upperValue(), 100) + self.assertEqual(w.lowerValue(), 90) + + # with zero width fixed range + w.setFixedRangeWidth(0) + self.assertEqual(w.upperValue() - w.lowerValue(), 0) + + w.setUpperValue(70) + self.assertEqual(w.upperValue(), 70) + self.assertEqual(w.lowerValue(), 70) + + w.setLowerValue(5) + self.assertEqual(w.upperValue(), 5) + self.assertEqual(w.lowerValue(), 5) + + w.setRange(0, 5) + self.assertEqual(w.upperValue(), 0) + self.assertEqual(w.lowerValue(), 0) + if __name__ == '__main__': unittest.main() From f306e558c60a84775a73e29b7c63a9e41ba88cce Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 14 Mar 2024 14:38:15 +1000 Subject: [PATCH 31/68] Fix overlapping text when narrow range is close to top --- .../qgselevationcontrollerwidget.cpp | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/gui/elevation/qgselevationcontrollerwidget.cpp b/src/gui/elevation/qgselevationcontrollerwidget.cpp index ba56a771c0db..dee437b3199a 100644 --- a/src/gui/elevation/qgselevationcontrollerwidget.cpp +++ b/src/gui/elevation/qgselevationcontrollerwidget.cpp @@ -224,6 +224,7 @@ void QgsElevationControllerLabels::paintEvent( QPaintEvent * ) const bool lowerIsCloseToLimit = lowerY + fm.height() > rect().bottom() - fm.descent(); const bool upperIsCloseToLimit = upperY - fm.height() < rect().top() + fm.ascent(); + const bool lowerIsCloseToUpperLimit = lowerY - fm.height() < rect().top() + fm.ascent(); QLocale locale; @@ -246,17 +247,28 @@ void QgsElevationControllerLabels::paintEvent( QPaintEvent * ) if ( mLimits.upper() < std::numeric_limits< double >::max() ) { - if ( upperIsCloseToLimit ) + if ( qgsDoubleNear( mRange.upper(), mRange.lower() ) ) { - f.setBold( true ); - path.addText( left, upperY, f, locale.toString( mRange.upper() ) ); + if ( !lowerIsCloseToUpperLimit ) + { + f.setBold( false ); + path.addText( left, rect().top() + fm.ascent(), f, locale.toString( mLimits.upper() ) ); + } } else { - f.setBold( true ); - path.addText( left, upperY, f, locale.toString( mRange.upper() ) ); - f.setBold( false ); - path.addText( left, rect().top() + fm.ascent(), f, locale.toString( mLimits.upper() ) ); + if ( upperIsCloseToLimit ) + { + f.setBold( true ); + path.addText( left, upperY, f, locale.toString( mRange.upper() ) ); + } + else + { + f.setBold( true ); + path.addText( left, upperY, f, locale.toString( mRange.upper() ) ); + f.setBold( false ); + path.addText( left, rect().top() + fm.ascent(), f, locale.toString( mLimits.upper() ) ); + } } } From 2da02f22c11be721bb1bfb23428c2e3957007b84 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 14 Mar 2024 14:49:43 +1000 Subject: [PATCH 32/68] Move current range labels to sit on the vertical outside of sliders This ensures that the labels don't overlap when the selected range is narrow --- src/gui/elevation/qgselevationcontrollerwidget.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/gui/elevation/qgselevationcontrollerwidget.cpp b/src/gui/elevation/qgselevationcontrollerwidget.cpp index dee437b3199a..09bc0480a718 100644 --- a/src/gui/elevation/qgselevationcontrollerwidget.cpp +++ b/src/gui/elevation/qgselevationcontrollerwidget.cpp @@ -219,8 +219,10 @@ void QgsElevationControllerLabels::paintEvent( QPaintEvent * ) const double limitRange = mLimits.upper() - mLimits.lower(); const double lowerFraction = ( mRange.lower() - mLimits.lower() ) / limitRange; const double upperFraction = ( mRange.upper() - mLimits.lower() ) / limitRange; - const int lowerY = static_cast< int >( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * lowerFraction - fm.descent() ) ); - const int upperY = static_cast< int >( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * upperFraction + fm.ascent() ) ); + const int lowerY = std::min( static_cast< int >( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * lowerFraction + fm.ascent() ) ), + rect().bottom() - fm.descent() ); + const int upperY = std::max( static_cast< int >( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * upperFraction - fm.descent() ) ), + rect().top() + fm.ascent() ); const bool lowerIsCloseToLimit = lowerY + fm.height() > rect().bottom() - fm.descent(); const bool upperIsCloseToLimit = upperY - fm.height() < rect().top() + fm.ascent(); From ecc71b4479244bae8f1b8d45a342f882ce078525 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 14 Mar 2024 14:54:43 +1000 Subject: [PATCH 33/68] Add fixed range width option to QgsElevationControllerWidget --- .../qgselevationcontrollerwidget.sip.in | 18 ++++++++++ .../qgselevationcontrollerwidget.sip.in | 18 ++++++++++ .../qgselevationcontrollerwidget.cpp | 36 ++++++++++++++++++- .../elevation/qgselevationcontrollerwidget.h | 19 ++++++++++ .../test_qgselevationcontrollerwidget.py | 14 ++++++++ 5 files changed, 104 insertions(+), 1 deletion(-) diff --git a/python/PyQt6/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in b/python/PyQt6/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in index 4e13daf0fb30..b56b575fa878 100644 --- a/python/PyQt6/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in @@ -59,6 +59,15 @@ Returns a reference to the slider component of the widget. %Docstring Returns a reference to the widget's configuration menu, which can be used to add actions to the menu. +%End + + double fixedRangeWidth() const; +%Docstring +Returns the fixed range width, or -1 if no fixed width is set. + +A fixed width forces the selected elevation range to have a matching width. + +.. seealso:: :py:func:`setFixedRangeWidth` %End public slots: @@ -77,6 +86,15 @@ Sets the current visible ``range`` for the widget. Sets the limits of the elevation range which can be selected by the widget. .. seealso:: :py:func:`rangeLimits` +%End + + void setFixedRangeWidth( double width ); +%Docstring +Sets the fixed range ``width``. Set to -1 if no fixed width is desired. + +A fixed width forces the selected elevation range to have a matching width. + +.. seealso:: :py:func:`fixedRangeWidth` %End signals: diff --git a/python/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in b/python/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in index 4e13daf0fb30..b56b575fa878 100644 --- a/python/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in +++ b/python/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in @@ -59,6 +59,15 @@ Returns a reference to the slider component of the widget. %Docstring Returns a reference to the widget's configuration menu, which can be used to add actions to the menu. +%End + + double fixedRangeWidth() const; +%Docstring +Returns the fixed range width, or -1 if no fixed width is set. + +A fixed width forces the selected elevation range to have a matching width. + +.. seealso:: :py:func:`setFixedRangeWidth` %End public slots: @@ -77,6 +86,15 @@ Sets the current visible ``range`` for the widget. Sets the limits of the elevation range which can be selected by the widget. .. seealso:: :py:func:`rangeLimits` +%End + + void setFixedRangeWidth( double width ); +%Docstring +Sets the fixed range ``width``. Set to -1 if no fixed width is desired. + +A fixed width forces the selected elevation range to have a matching width. + +.. seealso:: :py:func:`fixedRangeWidth` %End signals: diff --git a/src/gui/elevation/qgselevationcontrollerwidget.cpp b/src/gui/elevation/qgselevationcontrollerwidget.cpp index 09bc0480a718..b9abca42d17b 100644 --- a/src/gui/elevation/qgselevationcontrollerwidget.cpp +++ b/src/gui/elevation/qgselevationcontrollerwidget.cpp @@ -102,7 +102,20 @@ QgsDoubleRange QgsElevationControllerWidget::range() const if ( snappedLower == mSlider->lowerValue() && snappedUpper == mSlider->upperValue() ) return mCurrentRange; - return QgsDoubleRange( mSlider->lowerValue() / mSliderPrecision, mSlider->upperValue() / mSliderPrecision ); + const QgsDoubleRange sliderRange( mSlider->lowerValue() / mSliderPrecision, mSlider->upperValue() / mSliderPrecision ); + if ( mFixedRangeWidth >= 0 ) + { + // adjust range so that it has exactly the fixed width (given slider int precision the slider range + // will not have the exact fixed width) + if ( sliderRange.upper() + mFixedRangeWidth <= mRangeLimits.upper() ) + return QgsDoubleRange( sliderRange.lower(), sliderRange.lower() + mFixedRangeWidth ); + else + return QgsDoubleRange( sliderRange.upper() - mFixedRangeWidth, sliderRange.upper() ); + } + else + { + return sliderRange; + } } QgsDoubleRange QgsElevationControllerWidget::rangeLimits() const @@ -180,6 +193,27 @@ void QgsElevationControllerWidget::updateWidgetMask() setMask( reg ); } +double QgsElevationControllerWidget::fixedRangeWidth() const +{ + return mFixedRangeWidth; +} + +void QgsElevationControllerWidget::setFixedRangeWidth( double width ) +{ + if ( width == mFixedRangeWidth ) + return; + + mFixedRangeWidth = width; + if ( mFixedRangeWidth < 0 ) + { + mSlider->setFixedRangeWidth( -1 ); + } + else + { + mSlider->setFixedRangeWidth( static_cast< int >( std::round( mFixedRangeWidth * mSliderPrecision ) ) ); + } +} + // // QgsElevationControllerLabels // diff --git a/src/gui/elevation/qgselevationcontrollerwidget.h b/src/gui/elevation/qgselevationcontrollerwidget.h index d308c2a48276..425538a756fe 100644 --- a/src/gui/elevation/qgselevationcontrollerwidget.h +++ b/src/gui/elevation/qgselevationcontrollerwidget.h @@ -97,6 +97,15 @@ class GUI_EXPORT QgsElevationControllerWidget : public QWidget */ QMenu *menu(); + /** + * Returns the fixed range width, or -1 if no fixed width is set. + * + * A fixed width forces the selected elevation range to have a matching width. + * + * \see setFixedRangeWidth() + */ + double fixedRangeWidth() const; + public slots: /** @@ -114,6 +123,15 @@ class GUI_EXPORT QgsElevationControllerWidget : public QWidget */ void setRangeLimits( const QgsDoubleRange &limits ); + /** + * Sets the fixed range \a width. Set to -1 if no fixed width is desired. + * + * A fixed width forces the selected elevation range to have a matching width. + * + * \see fixedRangeWidth() + */ + void setFixedRangeWidth( double width ); + signals: /** @@ -134,6 +152,7 @@ class GUI_EXPORT QgsElevationControllerWidget : public QWidget QgsElevationControllerLabels *mSliderLabels = nullptr; QgsDoubleRange mRangeLimits; QgsDoubleRange mCurrentRange; + double mFixedRangeWidth = -1; int mBlockSliderChanges = 0; double mSliderPrecision = 100; diff --git a/tests/src/python/test_qgselevationcontrollerwidget.py b/tests/src/python/test_qgselevationcontrollerwidget.py index 7b4c999b1249..e155898de073 100644 --- a/tests/src/python/test_qgselevationcontrollerwidget.py +++ b/tests/src/python/test_qgselevationcontrollerwidget.py @@ -108,6 +108,20 @@ def test_slider_interaction(self): self.assertAlmostEqual(w.range().lower(), 459.644, 3) self.assertAlmostEqual(w.range().upper(), 729.495, 3) + def testFixedRangeWidth(self): + """ + Test that fixed range width is correctly handled + """ + w = QgsElevationControllerWidget() + w.setRangeLimits(QgsDoubleRange(100.5, 1000)) + w.setFixedRangeWidth(10.0001) + self.assertEqual(w.fixedRangeWidth(), 10.0001) + w.setRange(QgsDoubleRange(130.3, 920.6)) + self.assertAlmostEqual(w.range().upper() - w.range().lower(), 10.0001, 6) + + w.slider().setLowerValue(50) + self.assertAlmostEqual(w.range().upper() - w.range().lower(), 10.0001, 6) + def test_project_interaction(self): """ Test interaction of widget with project From e36e40243952608130d1f3d0249a50eca7a08c39 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 14 Mar 2024 15:06:59 +1000 Subject: [PATCH 34/68] [feature] Add option to set a fixed elevation slice width This adds a new option to the configure menu for the elevation controller, for setting a fixed elevation slice width. It can be used when a specific elevation range width is desired, or when the range should always be zero width. --- src/app/canvas/qgsappcanvasfiltering.cpp | 36 ++++++++++ src/app/canvas/qgsappcanvasfiltering.h | 13 ++++ ...elevationcontrollerfixedwidthdialogbase.ui | 68 +++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 src/ui/qgselevationcontrollerfixedwidthdialogbase.ui diff --git a/src/app/canvas/qgsappcanvasfiltering.cpp b/src/app/canvas/qgsappcanvasfiltering.cpp index f525385aa6af..cbe657bdc2ab 100644 --- a/src/app/canvas/qgsappcanvasfiltering.cpp +++ b/src/app/canvas/qgsappcanvasfiltering.cpp @@ -17,6 +17,7 @@ #include "qgselevationcontrollerwidget.h" #include "qgsmapcanvas.h" #include "qgisapp.h" +#include QgsAppCanvasFiltering::QgsAppCanvasFiltering( QObject *parent ) : QObject( parent ) @@ -40,6 +41,18 @@ void QgsAppCanvasFiltering::setupElevationControllerAction( QAction *action, Qgs { QgisApp::instance()->showProjectProperties( tr( "Elevation" ) ); } ); + QAction *setRangeWidthAction = new QAction( tr( "Set Fixed Range Width…" ), controller ); + controller->menu()->addAction( setRangeWidthAction ); + connect( setRangeWidthAction, &QAction::triggered, QgisApp::instance(), [controller ] + { + const double existingWidth = controller->fixedRangeWidth(); + QgsElevationControllerFixedWidthDialog dialog( controller ); + dialog.setFixedRangeWidth( existingWidth >= 0 ? existingWidth : -1 ); + if ( dialog.exec() ) + { + controller->setFixedRangeWidth( dialog.fixedRangeWidth() ); + } + } ); QAction *disableAction = new QAction( tr( "Disable Elevation Filter" ), controller ); controller->menu()->addAction( disableAction ); connect( disableAction, &QAction::triggered, action, [action] @@ -68,3 +81,26 @@ void QgsAppCanvasFiltering::setupElevationControllerAction( QAction *action, Qgs } } ); } + +// +// QgsElevationControllerFixedWidthDialog +// +QgsElevationControllerFixedWidthDialog::QgsElevationControllerFixedWidthDialog( QWidget *parent ) + : QDialog( parent ) +{ + setupUi( this ); + mWidthSpin->setClearValue( -1, tr( "Not set" ) ); + + connect( mButtonBox, &QDialogButtonBox::accepted, this, &QgsElevationControllerFixedWidthDialog::accept ); + connect( mButtonBox, &QDialogButtonBox::rejected, this, &QgsElevationControllerFixedWidthDialog::reject ); +} + +void QgsElevationControllerFixedWidthDialog::setFixedRangeWidth( double width ) +{ + mWidthSpin->setValue( width ); +} + +double QgsElevationControllerFixedWidthDialog::fixedRangeWidth() const +{ + return mWidthSpin->value(); +} diff --git a/src/app/canvas/qgsappcanvasfiltering.h b/src/app/canvas/qgsappcanvasfiltering.h index 822b50ece192..d62d7243fcbc 100644 --- a/src/app/canvas/qgsappcanvasfiltering.h +++ b/src/app/canvas/qgsappcanvasfiltering.h @@ -16,13 +16,26 @@ #define QGSAPPCANVASFILTERING_H #include "qgis.h" +#include "ui_qgselevationcontrollerfixedwidthdialogbase.h" #include #include +#include class QAction; class QgsMapCanvas; class QgsElevationControllerWidget; +class QgsElevationControllerFixedWidthDialog : public QDialog, private Ui::QgsElevationControllerFixedWidthDialogBase +{ + Q_OBJECT + public: + + QgsElevationControllerFixedWidthDialog( QWidget *parent ); + void setFixedRangeWidth( double width ); + double fixedRangeWidth() const; + +}; + class QgsAppCanvasFiltering : public QObject { Q_OBJECT diff --git a/src/ui/qgselevationcontrollerfixedwidthdialogbase.ui b/src/ui/qgselevationcontrollerfixedwidthdialogbase.ui new file mode 100644 index 000000000000..cf48643bd22a --- /dev/null +++ b/src/ui/qgselevationcontrollerfixedwidthdialogbase.ui @@ -0,0 +1,68 @@ + + + QgsElevationControllerFixedWidthDialogBase + + + + 0 + 0 + 447 + 98 + + + + Elevation Controller Range Width + + + + + + Elevation range width + + + + + + + 4 + + + -1.000000000000000 + + + 999999999.000000000000000 + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + QgsDoubleSpinBox + QDoubleSpinBox +
    qgsdoublespinbox.h
    +
    +
    + + +
    From 9ee09c176a16e1068962f1d99192dfe0e787e4cf Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 15 Mar 2024 17:39:25 +1000 Subject: [PATCH 35/68] Rename to fixed range size --- .../qgselevationcontrollerwidget.sip.in | 18 ++++++----- .../qgselevationcontrollerwidget.sip.in | 18 ++++++----- src/app/canvas/qgsappcanvasfiltering.cpp | 30 +++++++++---------- src/app/canvas/qgsappcanvasfiltering.h | 10 +++---- .../qgselevationcontrollerwidget.cpp | 22 +++++++------- .../elevation/qgselevationcontrollerwidget.h | 20 +++++++------ ...elevationcontrollerfixedsizedialogbase.ui} | 8 ++--- 7 files changed, 66 insertions(+), 60 deletions(-) rename src/ui/{qgselevationcontrollerfixedwidthdialogbase.ui => qgselevationcontrollerfixedsizedialogbase.ui} (87%) diff --git a/python/PyQt6/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in b/python/PyQt6/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in index b56b575fa878..77ae04d6b7ab 100644 --- a/python/PyQt6/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in @@ -61,13 +61,14 @@ Returns a reference to the widget's configuration menu, which can be used to add actions to the menu. %End - double fixedRangeWidth() const; + double fixedRangeSize() const; %Docstring -Returns the fixed range width, or -1 if no fixed width is set. +Returns the fixed range size, or -1 if no fixed size is set. -A fixed width forces the selected elevation range to have a matching width. +A fixed size forces the selected elevation range to have a matching difference between +the upper and lower elevation. -.. seealso:: :py:func:`setFixedRangeWidth` +.. seealso:: :py:func:`setFixedRangeSize` %End public slots: @@ -88,13 +89,14 @@ Sets the limits of the elevation range which can be selected by the widget. .. seealso:: :py:func:`rangeLimits` %End - void setFixedRangeWidth( double width ); + void setFixedRangeSize( double size ); %Docstring -Sets the fixed range ``width``. Set to -1 if no fixed width is desired. +Sets the fixed range ``size``. Set to -1 if no fixed size is desired. -A fixed width forces the selected elevation range to have a matching width. +A fixed size forces the selected elevation range to have a matching difference between +the upper and lower elevation. -.. seealso:: :py:func:`fixedRangeWidth` +.. seealso:: :py:func:`fixedRangeSize` %End signals: diff --git a/python/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in b/python/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in index b56b575fa878..77ae04d6b7ab 100644 --- a/python/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in +++ b/python/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in @@ -61,13 +61,14 @@ Returns a reference to the widget's configuration menu, which can be used to add actions to the menu. %End - double fixedRangeWidth() const; + double fixedRangeSize() const; %Docstring -Returns the fixed range width, or -1 if no fixed width is set. +Returns the fixed range size, or -1 if no fixed size is set. -A fixed width forces the selected elevation range to have a matching width. +A fixed size forces the selected elevation range to have a matching difference between +the upper and lower elevation. -.. seealso:: :py:func:`setFixedRangeWidth` +.. seealso:: :py:func:`setFixedRangeSize` %End public slots: @@ -88,13 +89,14 @@ Sets the limits of the elevation range which can be selected by the widget. .. seealso:: :py:func:`rangeLimits` %End - void setFixedRangeWidth( double width ); + void setFixedRangeSize( double size ); %Docstring -Sets the fixed range ``width``. Set to -1 if no fixed width is desired. +Sets the fixed range ``size``. Set to -1 if no fixed size is desired. -A fixed width forces the selected elevation range to have a matching width. +A fixed size forces the selected elevation range to have a matching difference between +the upper and lower elevation. -.. seealso:: :py:func:`fixedRangeWidth` +.. seealso:: :py:func:`fixedRangeSize` %End signals: diff --git a/src/app/canvas/qgsappcanvasfiltering.cpp b/src/app/canvas/qgsappcanvasfiltering.cpp index cbe657bdc2ab..d815e10b043a 100644 --- a/src/app/canvas/qgsappcanvasfiltering.cpp +++ b/src/app/canvas/qgsappcanvasfiltering.cpp @@ -41,16 +41,16 @@ void QgsAppCanvasFiltering::setupElevationControllerAction( QAction *action, Qgs { QgisApp::instance()->showProjectProperties( tr( "Elevation" ) ); } ); - QAction *setRangeWidthAction = new QAction( tr( "Set Fixed Range Width…" ), controller ); - controller->menu()->addAction( setRangeWidthAction ); - connect( setRangeWidthAction, &QAction::triggered, QgisApp::instance(), [controller ] + QAction *setRangeSizeAction = new QAction( tr( "Set Fixed Range Size…" ), controller ); + controller->menu()->addAction( setRangeSizeAction ); + connect( setRangeSizeAction, &QAction::triggered, QgisApp::instance(), [controller ] { - const double existingWidth = controller->fixedRangeWidth(); - QgsElevationControllerFixedWidthDialog dialog( controller ); - dialog.setFixedRangeWidth( existingWidth >= 0 ? existingWidth : -1 ); + const double existingSize = controller->fixedRangeSize(); + QgsElevationControllerFixedSizeDialog dialog( controller ); + dialog.setFixedRangeSize( existingSize >= 0 ? existingSize : -1 ); if ( dialog.exec() ) { - controller->setFixedRangeWidth( dialog.fixedRangeWidth() ); + controller->setFixedRangeSize( dialog.fixedRangeSize() ); } } ); QAction *disableAction = new QAction( tr( "Disable Elevation Filter" ), controller ); @@ -85,22 +85,22 @@ void QgsAppCanvasFiltering::setupElevationControllerAction( QAction *action, Qgs // // QgsElevationControllerFixedWidthDialog // -QgsElevationControllerFixedWidthDialog::QgsElevationControllerFixedWidthDialog( QWidget *parent ) +QgsElevationControllerFixedSizeDialog::QgsElevationControllerFixedSizeDialog( QWidget *parent ) : QDialog( parent ) { setupUi( this ); - mWidthSpin->setClearValue( -1, tr( "Not set" ) ); + mSizeSpin->setClearValue( -1, tr( "Not set" ) ); - connect( mButtonBox, &QDialogButtonBox::accepted, this, &QgsElevationControllerFixedWidthDialog::accept ); - connect( mButtonBox, &QDialogButtonBox::rejected, this, &QgsElevationControllerFixedWidthDialog::reject ); + connect( mButtonBox, &QDialogButtonBox::accepted, this, &QgsElevationControllerFixedSizeDialog::accept ); + connect( mButtonBox, &QDialogButtonBox::rejected, this, &QgsElevationControllerFixedSizeDialog::reject ); } -void QgsElevationControllerFixedWidthDialog::setFixedRangeWidth( double width ) +void QgsElevationControllerFixedSizeDialog::setFixedRangeSize( double size ) { - mWidthSpin->setValue( width ); + mSizeSpin->setValue( size ); } -double QgsElevationControllerFixedWidthDialog::fixedRangeWidth() const +double QgsElevationControllerFixedSizeDialog::fixedRangeSize() const { - return mWidthSpin->value(); + return mSizeSpin->value(); } diff --git a/src/app/canvas/qgsappcanvasfiltering.h b/src/app/canvas/qgsappcanvasfiltering.h index d62d7243fcbc..967f0103eba9 100644 --- a/src/app/canvas/qgsappcanvasfiltering.h +++ b/src/app/canvas/qgsappcanvasfiltering.h @@ -16,7 +16,7 @@ #define QGSAPPCANVASFILTERING_H #include "qgis.h" -#include "ui_qgselevationcontrollerfixedwidthdialogbase.h" +#include "ui_qgselevationcontrollerfixedsizedialogbase.h" #include #include #include @@ -25,14 +25,14 @@ class QAction; class QgsMapCanvas; class QgsElevationControllerWidget; -class QgsElevationControllerFixedWidthDialog : public QDialog, private Ui::QgsElevationControllerFixedWidthDialogBase +class QgsElevationControllerFixedSizeDialog : public QDialog, private Ui::QgsElevationControllerFixedSizeDialogBase { Q_OBJECT public: - QgsElevationControllerFixedWidthDialog( QWidget *parent ); - void setFixedRangeWidth( double width ); - double fixedRangeWidth() const; + QgsElevationControllerFixedSizeDialog( QWidget *parent ); + void setFixedRangeSize( double size ); + double fixedRangeSize() const; }; diff --git a/src/gui/elevation/qgselevationcontrollerwidget.cpp b/src/gui/elevation/qgselevationcontrollerwidget.cpp index b9abca42d17b..b5cddecc98ae 100644 --- a/src/gui/elevation/qgselevationcontrollerwidget.cpp +++ b/src/gui/elevation/qgselevationcontrollerwidget.cpp @@ -103,14 +103,14 @@ QgsDoubleRange QgsElevationControllerWidget::range() const return mCurrentRange; const QgsDoubleRange sliderRange( mSlider->lowerValue() / mSliderPrecision, mSlider->upperValue() / mSliderPrecision ); - if ( mFixedRangeWidth >= 0 ) + if ( mFixedRangeSize >= 0 ) { // adjust range so that it has exactly the fixed width (given slider int precision the slider range // will not have the exact fixed width) - if ( sliderRange.upper() + mFixedRangeWidth <= mRangeLimits.upper() ) - return QgsDoubleRange( sliderRange.lower(), sliderRange.lower() + mFixedRangeWidth ); + if ( sliderRange.upper() + mFixedRangeSize <= mRangeLimits.upper() ) + return QgsDoubleRange( sliderRange.lower(), sliderRange.lower() + mFixedRangeSize ); else - return QgsDoubleRange( sliderRange.upper() - mFixedRangeWidth, sliderRange.upper() ); + return QgsDoubleRange( sliderRange.upper() - mFixedRangeSize, sliderRange.upper() ); } else { @@ -193,24 +193,24 @@ void QgsElevationControllerWidget::updateWidgetMask() setMask( reg ); } -double QgsElevationControllerWidget::fixedRangeWidth() const +double QgsElevationControllerWidget::fixedRangeSize() const { - return mFixedRangeWidth; + return mFixedRangeSize; } -void QgsElevationControllerWidget::setFixedRangeWidth( double width ) +void QgsElevationControllerWidget::setFixedRangeSize( double size ) { - if ( width == mFixedRangeWidth ) + if ( size == mFixedRangeSize ) return; - mFixedRangeWidth = width; - if ( mFixedRangeWidth < 0 ) + mFixedRangeSize = size; + if ( mFixedRangeSize < 0 ) { mSlider->setFixedRangeWidth( -1 ); } else { - mSlider->setFixedRangeWidth( static_cast< int >( std::round( mFixedRangeWidth * mSliderPrecision ) ) ); + mSlider->setFixedRangeWidth( static_cast< int >( std::round( mFixedRangeSize * mSliderPrecision ) ) ); } } diff --git a/src/gui/elevation/qgselevationcontrollerwidget.h b/src/gui/elevation/qgselevationcontrollerwidget.h index 425538a756fe..88b5c8d0592d 100644 --- a/src/gui/elevation/qgselevationcontrollerwidget.h +++ b/src/gui/elevation/qgselevationcontrollerwidget.h @@ -98,13 +98,14 @@ class GUI_EXPORT QgsElevationControllerWidget : public QWidget QMenu *menu(); /** - * Returns the fixed range width, or -1 if no fixed width is set. + * Returns the fixed range size, or -1 if no fixed size is set. * - * A fixed width forces the selected elevation range to have a matching width. + * A fixed size forces the selected elevation range to have a matching difference between + * the upper and lower elevation. * - * \see setFixedRangeWidth() + * \see setFixedRangeSize() */ - double fixedRangeWidth() const; + double fixedRangeSize() const; public slots: @@ -124,13 +125,14 @@ class GUI_EXPORT QgsElevationControllerWidget : public QWidget void setRangeLimits( const QgsDoubleRange &limits ); /** - * Sets the fixed range \a width. Set to -1 if no fixed width is desired. + * Sets the fixed range \a size. Set to -1 if no fixed size is desired. * - * A fixed width forces the selected elevation range to have a matching width. + * A fixed size forces the selected elevation range to have a matching difference between + * the upper and lower elevation. * - * \see fixedRangeWidth() + * \see fixedRangeSize() */ - void setFixedRangeWidth( double width ); + void setFixedRangeSize( double size ); signals: @@ -152,7 +154,7 @@ class GUI_EXPORT QgsElevationControllerWidget : public QWidget QgsElevationControllerLabels *mSliderLabels = nullptr; QgsDoubleRange mRangeLimits; QgsDoubleRange mCurrentRange; - double mFixedRangeWidth = -1; + double mFixedRangeSize = -1; int mBlockSliderChanges = 0; double mSliderPrecision = 100; diff --git a/src/ui/qgselevationcontrollerfixedwidthdialogbase.ui b/src/ui/qgselevationcontrollerfixedsizedialogbase.ui similarity index 87% rename from src/ui/qgselevationcontrollerfixedwidthdialogbase.ui rename to src/ui/qgselevationcontrollerfixedsizedialogbase.ui index cf48643bd22a..b2f945444a4a 100644 --- a/src/ui/qgselevationcontrollerfixedwidthdialogbase.ui +++ b/src/ui/qgselevationcontrollerfixedsizedialogbase.ui @@ -1,7 +1,7 @@ - QgsElevationControllerFixedWidthDialogBase - + QgsElevationControllerFixedSizeDialogBase + 0 @@ -17,12 +17,12 @@ - Elevation range width + Elevation range size - + 4 From a9d2d43a0e906b0e8619adafcaea71713c1102dc Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 18 Mar 2024 06:56:19 +1000 Subject: [PATCH 36/68] Also rename QgsRangeSlider fixed range size methods --- .../gui/auto_generated/qgsrangeslider.sip.in | 28 +++--- .../gui/auto_generated/qgsrangeslider.sip.in | 28 +++--- .../qgselevationcontrollerwidget.cpp | 4 +- src/gui/qgsrangeslider.cpp | 94 +++++++++---------- src/gui/qgsrangeslider.h | 30 +++--- 5 files changed, 92 insertions(+), 92 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/qgsrangeslider.sip.in b/python/PyQt6/gui/auto_generated/qgsrangeslider.sip.in index 8b2e0769305f..126ed12e3acf 100644 --- a/python/PyQt6/gui/auto_generated/qgsrangeslider.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsrangeslider.sip.in @@ -170,28 +170,28 @@ This corresponds to the larger increment or decrement applied when the user pres .. seealso:: :py:func:`singleStep` %End - int fixedRangeWidth() const; + int fixedRangeSize() const; %Docstring -Returns the slider's fixed range width, or -1 if not set. +Returns the slider's fixed range size, or -1 if not set. -If a fixed range width is set then moving either the lower or upper slider will automatically +If a fixed range size is set then moving either the lower or upper slider will automatically move the other slider accordingly, in order to keep the selected range at the specified -fixed width. +fixed size. -.. seealso:: :py:func:`setFixedRangeWidth` +.. seealso:: :py:func:`setFixedRangeSize` .. versionadded:: 3.38 %End - void setFixedRangeWidth( int width ); + void setFixedRangeSize( int size ); %Docstring -Sets the slider's fixed range ``width``. Set to -1 if no fixed width is desired. +Sets the slider's fixed range ``size``. Set to -1 if no fixed size is desired. -If a fixed range width is set then moving either the lower or upper slider will automatically +If a fixed range size is set then moving either the lower or upper slider will automatically move the other slider accordingly, in order to keep the selected range at the specified -fixed width. +fixed size. -.. seealso:: :py:func:`fixedRangeWidth` +.. seealso:: :py:func:`fixedRangeSize` .. versionadded:: 3.38 %End @@ -293,13 +293,13 @@ Emitted when the range selected in the widget is changed. Emitted when the limits of values allowed in the widget is changed. %End - void fixedRangeWidthChanged( int width ); + void fixedRangeSizeChanged( int size ); %Docstring -Emitted when the widget's fixed range width is changed. +Emitted when the widget's fixed range size is changed. -.. seealso:: :py:func:`fixedRangeWidth` +.. seealso:: :py:func:`fixedRangeSize` -.. seealso:: :py:func:`setFixedRangeWidth` +.. seealso:: :py:func:`setFixedRangeSize` .. versionadded:: 3.38 %End diff --git a/python/gui/auto_generated/qgsrangeslider.sip.in b/python/gui/auto_generated/qgsrangeslider.sip.in index 8b2e0769305f..126ed12e3acf 100644 --- a/python/gui/auto_generated/qgsrangeslider.sip.in +++ b/python/gui/auto_generated/qgsrangeslider.sip.in @@ -170,28 +170,28 @@ This corresponds to the larger increment or decrement applied when the user pres .. seealso:: :py:func:`singleStep` %End - int fixedRangeWidth() const; + int fixedRangeSize() const; %Docstring -Returns the slider's fixed range width, or -1 if not set. +Returns the slider's fixed range size, or -1 if not set. -If a fixed range width is set then moving either the lower or upper slider will automatically +If a fixed range size is set then moving either the lower or upper slider will automatically move the other slider accordingly, in order to keep the selected range at the specified -fixed width. +fixed size. -.. seealso:: :py:func:`setFixedRangeWidth` +.. seealso:: :py:func:`setFixedRangeSize` .. versionadded:: 3.38 %End - void setFixedRangeWidth( int width ); + void setFixedRangeSize( int size ); %Docstring -Sets the slider's fixed range ``width``. Set to -1 if no fixed width is desired. +Sets the slider's fixed range ``size``. Set to -1 if no fixed size is desired. -If a fixed range width is set then moving either the lower or upper slider will automatically +If a fixed range size is set then moving either the lower or upper slider will automatically move the other slider accordingly, in order to keep the selected range at the specified -fixed width. +fixed size. -.. seealso:: :py:func:`fixedRangeWidth` +.. seealso:: :py:func:`fixedRangeSize` .. versionadded:: 3.38 %End @@ -293,13 +293,13 @@ Emitted when the range selected in the widget is changed. Emitted when the limits of values allowed in the widget is changed. %End - void fixedRangeWidthChanged( int width ); + void fixedRangeSizeChanged( int size ); %Docstring -Emitted when the widget's fixed range width is changed. +Emitted when the widget's fixed range size is changed. -.. seealso:: :py:func:`fixedRangeWidth` +.. seealso:: :py:func:`fixedRangeSize` -.. seealso:: :py:func:`setFixedRangeWidth` +.. seealso:: :py:func:`setFixedRangeSize` .. versionadded:: 3.38 %End diff --git a/src/gui/elevation/qgselevationcontrollerwidget.cpp b/src/gui/elevation/qgselevationcontrollerwidget.cpp index b5cddecc98ae..7cdfe759dc79 100644 --- a/src/gui/elevation/qgselevationcontrollerwidget.cpp +++ b/src/gui/elevation/qgselevationcontrollerwidget.cpp @@ -206,11 +206,11 @@ void QgsElevationControllerWidget::setFixedRangeSize( double size ) mFixedRangeSize = size; if ( mFixedRangeSize < 0 ) { - mSlider->setFixedRangeWidth( -1 ); + mSlider->setFixedRangeSize( -1 ); } else { - mSlider->setFixedRangeWidth( static_cast< int >( std::round( mFixedRangeSize * mSliderPrecision ) ) ); + mSlider->setFixedRangeSize( static_cast< int >( std::round( mFixedRangeSize * mSliderPrecision ) ) ); } } diff --git a/src/gui/qgsrangeslider.cpp b/src/gui/qgsrangeslider.cpp index 9331cbba6128..b0eaed600955 100644 --- a/src/gui/qgsrangeslider.cpp +++ b/src/gui/qgsrangeslider.cpp @@ -120,10 +120,10 @@ void QgsRangeSlider::setLowerValue( int lowerValue ) return; mLowerValue = std::min( mStyleOption.maximum, std::max( mStyleOption.minimum, lowerValue ) ); - if ( mFixedRangeWidth >= 0 ) + if ( mFixedRangeSize >= 0 ) { - mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); - mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); } else { @@ -145,10 +145,10 @@ void QgsRangeSlider::setUpperValue( int upperValue ) return; mUpperValue = std::max( mStyleOption.minimum, std::min( mStyleOption.maximum, upperValue ) ); - if ( mFixedRangeWidth >= 0 ) + if ( mFixedRangeSize >= 0 ) { - mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); - mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); } else { @@ -169,10 +169,10 @@ void QgsRangeSlider::setRange( int lower, int upper ) mLowerValue = std::min( mStyleOption.maximum, std::max( mStyleOption.minimum, lower ) ); mUpperValue = std::min( mStyleOption.maximum, std::max( mStyleOption.minimum, upper ) ); - if ( mFixedRangeWidth >= 0 ) + if ( mFixedRangeSize >= 0 ) { - mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); - mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); } else { @@ -343,22 +343,22 @@ QRect QgsRangeSlider::selectedRangeRect() return selectionRect.adjusted( -1, 1, 1, -1 ); } -int QgsRangeSlider::fixedRangeWidth() const +int QgsRangeSlider::fixedRangeSize() const { - return mFixedRangeWidth; + return mFixedRangeSize; } -void QgsRangeSlider::setFixedRangeWidth( int width ) +void QgsRangeSlider::setFixedRangeSize( int size ) { - if ( width == mFixedRangeWidth ) + if ( size == mFixedRangeSize ) return; - mFixedRangeWidth = width; + mFixedRangeSize = size; - if ( mFixedRangeWidth >= 0 ) - setUpperValue( mLowerValue + mFixedRangeWidth ); + if ( mFixedRangeSize >= 0 ) + setUpperValue( mLowerValue + mFixedRangeSize ); - emit fixedRangeWidthChanged( mFixedRangeWidth ); + emit fixedRangeSizeChanged( mFixedRangeSize ); } void QgsRangeSlider::applyStep( int step ) @@ -371,10 +371,10 @@ void QgsRangeSlider::applyStep( int step ) if ( newLowerValue != mLowerValue ) { mLowerValue = newLowerValue; - if ( mFixedRangeWidth >= 0 ) + if ( mFixedRangeSize >= 0 ) { - mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); - mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); } emit rangeChanged( mLowerValue, mUpperValue ); update(); @@ -388,10 +388,10 @@ void QgsRangeSlider::applyStep( int step ) if ( newUpperValue != mUpperValue ) { mUpperValue = newUpperValue; - if ( mFixedRangeWidth >= 0 ) + if ( mFixedRangeSize >= 0 ) { - mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); - mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); } emit rangeChanged( mLowerValue, mUpperValue ); update(); @@ -408,10 +408,10 @@ void QgsRangeSlider::applyStep( int step ) if ( newLowerValue != mLowerValue ) { mLowerValue = newLowerValue; - if ( mFixedRangeWidth >= 0 ) + if ( mFixedRangeSize >= 0 ) { - mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); - mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); } else { @@ -428,10 +428,10 @@ void QgsRangeSlider::applyStep( int step ) if ( newUpperValue != mUpperValue ) { mUpperValue = newUpperValue; - if ( mFixedRangeWidth >= 0 ) + if ( mFixedRangeSize >= 0 ) { - mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); - mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); } else { @@ -674,11 +674,11 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event ) { changed = true; mUpperValue = mPreDragUpperValue; - if ( mFixedRangeWidth >= 0 ) + if ( mFixedRangeSize >= 0 ) { // don't permit fixed width drags if it pushes the other value out of range - mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); - mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); } } } @@ -690,11 +690,11 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event ) { changed = true; mLowerValue = mPreDragLowerValue; - if ( mFixedRangeWidth >= 0 ) + if ( mFixedRangeSize >= 0 ) { // don't permit fixed width drags if it pushes the other value out of range - mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); - mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); } } } @@ -705,22 +705,22 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event ) { changed = true; mUpperValue = mPreDragUpperValue; - if ( mFixedRangeWidth >= 0 ) + if ( mFixedRangeSize >= 0 ) { // don't permit fixed width drags if it pushes the other value out of range - mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); - mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); } } if ( mLowerValue != mPreDragLowerValue ) { changed = true; mLowerValue = mPreDragLowerValue; - if ( mFixedRangeWidth >= 0 ) + if ( mFixedRangeSize >= 0 ) { // don't permit fixed width drags if it pushes the other value out of range - mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); - mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); } } } @@ -739,11 +739,11 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event ) if ( mLowerValue != newPosition ) { mLowerValue = newPosition; - if ( mFixedRangeWidth >= 0 ) + if ( mFixedRangeSize >= 0 ) { // don't permit fixed width drags if it pushes the other value out of range - mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); - mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); } changed = true; @@ -758,11 +758,11 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event ) if ( mUpperValue != newPosition ) { mUpperValue = newPosition; - if ( mFixedRangeWidth >= 0 ) + if ( mFixedRangeSize >= 0 ) { // don't permit fixed width drags if it pushes the other value out of range - mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth ); - mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); } changed = true; diff --git a/src/gui/qgsrangeslider.h b/src/gui/qgsrangeslider.h index 2bd05d115a02..bd92989cffcb 100644 --- a/src/gui/qgsrangeslider.h +++ b/src/gui/qgsrangeslider.h @@ -170,28 +170,28 @@ class GUI_EXPORT QgsRangeSlider : public QWidget int pageStep() const; /** - * Returns the slider's fixed range width, or -1 if not set. + * Returns the slider's fixed range size, or -1 if not set. * - * If a fixed range width is set then moving either the lower or upper slider will automatically + * If a fixed range size is set then moving either the lower or upper slider will automatically * move the other slider accordingly, in order to keep the selected range at the specified - * fixed width. + * fixed size. * - * \see setFixedRangeWidth() + * \see setFixedRangeSize() * \since QGIS 3.38 */ - int fixedRangeWidth() const; + int fixedRangeSize() const; /** - * Sets the slider's fixed range \a width. Set to -1 if no fixed width is desired. + * Sets the slider's fixed range \a size. Set to -1 if no fixed size is desired. * - * If a fixed range width is set then moving either the lower or upper slider will automatically + * If a fixed range size is set then moving either the lower or upper slider will automatically * move the other slider accordingly, in order to keep the selected range at the specified - * fixed width. + * fixed size. * - * \see fixedRangeWidth() + * \see fixedRangeSize() * \since QGIS 3.38 */ - void setFixedRangeWidth( int width ); + void setFixedRangeSize( int size ); public slots: @@ -280,14 +280,14 @@ class GUI_EXPORT QgsRangeSlider : public QWidget void rangeLimitsChanged( int minimum, int maximum ); /** - * Emitted when the widget's fixed range width is changed. + * Emitted when the widget's fixed range size is changed. * - * \see fixedRangeWidth() - * \see setFixedRangeWidth() + * \see fixedRangeSize() + * \see setFixedRangeSize() * * \since QGIS 3.38 */ - void fixedRangeWidthChanged( int width ); + void fixedRangeSizeChanged( int size ); private: @@ -304,7 +304,7 @@ class GUI_EXPORT QgsRangeSlider : public QWidget int mSingleStep = 1; int mPageStep = 10; - int mFixedRangeWidth = -1; + int mFixedRangeSize = -1; QStyleOptionSlider mStyleOption; enum Control From 465d8bd0bad07690df3ede32ad0ebc3427c7f534 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 18 Mar 2024 07:30:57 +1000 Subject: [PATCH 37/68] Use inline settings widget in menu instead of separate dialog --- .../qgselevationcontrollerwidget.sip.in | 15 ++++ .../qgselevationcontrollerwidget.sip.in | 15 ++++ src/app/canvas/qgsappcanvasfiltering.cpp | 35 ---------- src/app/canvas/qgsappcanvasfiltering.h | 12 ---- .../qgselevationcontrollerwidget.cpp | 47 +++++++++++++ .../elevation/qgselevationcontrollerwidget.h | 18 +++++ ...selevationcontrollerfixedsizedialogbase.ui | 68 ------------------- 7 files changed, 95 insertions(+), 115 deletions(-) delete mode 100644 src/ui/qgselevationcontrollerfixedsizedialogbase.ui diff --git a/python/PyQt6/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in b/python/PyQt6/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in index 77ae04d6b7ab..2fd2258a8d3f 100644 --- a/python/PyQt6/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in @@ -12,6 +12,21 @@ +class QgsElevationControllerSettingsAction: QWidgetAction +{ + +%TypeHeaderCode +#include "qgselevationcontrollerwidget.h" +%End + public: + + QgsElevationControllerSettingsAction( QWidget *parent = 0 ); + + QgsDoubleSpinBox *sizeSpin(); + +}; + + class QgsElevationControllerWidget : QWidget { diff --git a/python/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in b/python/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in index 77ae04d6b7ab..2fd2258a8d3f 100644 --- a/python/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in +++ b/python/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in @@ -12,6 +12,21 @@ +class QgsElevationControllerSettingsAction: QWidgetAction +{ + +%TypeHeaderCode +#include "qgselevationcontrollerwidget.h" +%End + public: + + QgsElevationControllerSettingsAction( QWidget *parent = 0 ); + + QgsDoubleSpinBox *sizeSpin(); + +}; + + class QgsElevationControllerWidget : QWidget { diff --git a/src/app/canvas/qgsappcanvasfiltering.cpp b/src/app/canvas/qgsappcanvasfiltering.cpp index d815e10b043a..433f9a347475 100644 --- a/src/app/canvas/qgsappcanvasfiltering.cpp +++ b/src/app/canvas/qgsappcanvasfiltering.cpp @@ -41,18 +41,6 @@ void QgsAppCanvasFiltering::setupElevationControllerAction( QAction *action, Qgs { QgisApp::instance()->showProjectProperties( tr( "Elevation" ) ); } ); - QAction *setRangeSizeAction = new QAction( tr( "Set Fixed Range Size…" ), controller ); - controller->menu()->addAction( setRangeSizeAction ); - connect( setRangeSizeAction, &QAction::triggered, QgisApp::instance(), [controller ] - { - const double existingSize = controller->fixedRangeSize(); - QgsElevationControllerFixedSizeDialog dialog( controller ); - dialog.setFixedRangeSize( existingSize >= 0 ? existingSize : -1 ); - if ( dialog.exec() ) - { - controller->setFixedRangeSize( dialog.fixedRangeSize() ); - } - } ); QAction *disableAction = new QAction( tr( "Disable Elevation Filter" ), controller ); controller->menu()->addAction( disableAction ); connect( disableAction, &QAction::triggered, action, [action] @@ -81,26 +69,3 @@ void QgsAppCanvasFiltering::setupElevationControllerAction( QAction *action, Qgs } } ); } - -// -// QgsElevationControllerFixedWidthDialog -// -QgsElevationControllerFixedSizeDialog::QgsElevationControllerFixedSizeDialog( QWidget *parent ) - : QDialog( parent ) -{ - setupUi( this ); - mSizeSpin->setClearValue( -1, tr( "Not set" ) ); - - connect( mButtonBox, &QDialogButtonBox::accepted, this, &QgsElevationControllerFixedSizeDialog::accept ); - connect( mButtonBox, &QDialogButtonBox::rejected, this, &QgsElevationControllerFixedSizeDialog::reject ); -} - -void QgsElevationControllerFixedSizeDialog::setFixedRangeSize( double size ) -{ - mSizeSpin->setValue( size ); -} - -double QgsElevationControllerFixedSizeDialog::fixedRangeSize() const -{ - return mSizeSpin->value(); -} diff --git a/src/app/canvas/qgsappcanvasfiltering.h b/src/app/canvas/qgsappcanvasfiltering.h index 967f0103eba9..d660d6d8732d 100644 --- a/src/app/canvas/qgsappcanvasfiltering.h +++ b/src/app/canvas/qgsappcanvasfiltering.h @@ -16,7 +16,6 @@ #define QGSAPPCANVASFILTERING_H #include "qgis.h" -#include "ui_qgselevationcontrollerfixedsizedialogbase.h" #include #include #include @@ -25,17 +24,6 @@ class QAction; class QgsMapCanvas; class QgsElevationControllerWidget; -class QgsElevationControllerFixedSizeDialog : public QDialog, private Ui::QgsElevationControllerFixedSizeDialogBase -{ - Q_OBJECT - public: - - QgsElevationControllerFixedSizeDialog( QWidget *parent ); - void setFixedRangeSize( double size ); - double fixedRangeSize() const; - -}; - class QgsAppCanvasFiltering : public QObject { Q_OBJECT diff --git a/src/gui/elevation/qgselevationcontrollerwidget.cpp b/src/gui/elevation/qgselevationcontrollerwidget.cpp index 7cdfe759dc79..136aef50dc95 100644 --- a/src/gui/elevation/qgselevationcontrollerwidget.cpp +++ b/src/gui/elevation/qgselevationcontrollerwidget.cpp @@ -21,6 +21,7 @@ #include "qgsproject.h" #include "qgsprojectelevationproperties.h" #include "qgsapplication.h" +#include "qgsdoublespinbox.h" #include #include @@ -29,6 +30,7 @@ #include #include #include +#include QgsElevationControllerWidget::QgsElevationControllerWidget( QWidget *parent ) : QWidget( parent ) @@ -47,6 +49,17 @@ QgsElevationControllerWidget::QgsElevationControllerWidget( QWidget *parent ) mMenu = new QMenu( this ); mConfigureButton->setMenu( mMenu ); + QgsElevationControllerSettingsAction *settingsAction = new QgsElevationControllerSettingsAction( mMenu ); + mMenu->addAction( settingsAction ); + + settingsAction->sizeSpin()->clear(); + connect( settingsAction->sizeSpin(), qOverload< double >( &QgsDoubleSpinBox::valueChanged ), this, [this]( double size ) + { + setFixedRangeSize( size < 0 ? -1 : size ); + } ); + + mMenu->addSeparator(); + mSlider = new QgsRangeSlider( Qt::Vertical ); mSlider->setFlippedDirection( true ); mSlider->setRangeLimits( 0, 100000 ); @@ -346,4 +359,38 @@ void QgsElevationControllerLabels::setRange( const QgsDoubleRange &range ) mRange = range; update(); } + +// +// QgsElevationControllerSettingsAction +// + +QgsElevationControllerSettingsAction::QgsElevationControllerSettingsAction( QWidget *parent ) + : QWidgetAction( parent ) +{ + QGridLayout *gLayout = new QGridLayout(); + gLayout->setContentsMargins( 3, 2, 3, 2 ); + + QLabel *label = new QLabel( tr( "Fixed Range Size" ) ); + gLayout->addWidget( label, 0, 0 ); + + mSizeSpin = new QgsDoubleSpinBox(); + mSizeSpin->setDecimals( 4 ); + mSizeSpin->setMinimum( -1.0 ); + mSizeSpin->setMaximum( 999999999.0 ); + mSizeSpin->setClearValue( -1, tr( "Not set" ) ); + mSizeSpin->setKeyboardTracking( false ); + mSizeSpin->setToolTip( tr( "Limit elevation range to a fixed size" ) ); + + gLayout->addWidget( mSizeSpin, 0, 1 ); + + QWidget *w = new QWidget(); + w->setLayout( gLayout ); + setDefaultWidget( w ); +} + +QgsDoubleSpinBox *QgsElevationControllerSettingsAction::sizeSpin() +{ + return mSizeSpin; +} + ///@endcond PRIVATE diff --git a/src/gui/elevation/qgselevationcontrollerwidget.h b/src/gui/elevation/qgselevationcontrollerwidget.h index 88b5c8d0592d..6ff61a5a0f76 100644 --- a/src/gui/elevation/qgselevationcontrollerwidget.h +++ b/src/gui/elevation/qgselevationcontrollerwidget.h @@ -22,8 +22,10 @@ #include "qgis_sip.h" #include "qgsrange.h" #include +#include class QgsRangeSlider; +class QgsDoubleSpinBox; class QToolButton; class QMenu; @@ -49,6 +51,22 @@ class GUI_EXPORT QgsElevationControllerLabels : public QWidget SIP_SKIP }; +class GUI_EXPORT QgsElevationControllerSettingsAction: public QWidgetAction +{ + Q_OBJECT + + public: + + QgsElevationControllerSettingsAction( QWidget *parent = nullptr ); + + QgsDoubleSpinBox *sizeSpin(); + + private: + + QgsDoubleSpinBox *mSizeSpin = nullptr; +}; + + ///@endcond PRIVATE /** diff --git a/src/ui/qgselevationcontrollerfixedsizedialogbase.ui b/src/ui/qgselevationcontrollerfixedsizedialogbase.ui deleted file mode 100644 index b2f945444a4a..000000000000 --- a/src/ui/qgselevationcontrollerfixedsizedialogbase.ui +++ /dev/null @@ -1,68 +0,0 @@ - - - QgsElevationControllerFixedSizeDialogBase - - - - 0 - 0 - 447 - 98 - - - - Elevation Controller Range Width - - - - - - Elevation range size - - - - - - - 4 - - - -1.000000000000000 - - - 999999999.000000000000000 - - - - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - QgsDoubleSpinBox - QDoubleSpinBox -
    qgsdoublespinbox.h
    -
    -
    - - -
    From 1673bc25f642f4bf14f50027f417665eeea3671c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 18 Mar 2024 09:18:31 +1000 Subject: [PATCH 38/68] Update test --- tests/src/python/test_qgsrangeslider.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/src/python/test_qgsrangeslider.py b/tests/src/python/test_qgsrangeslider.py index a563ad22d5d1..105147e74697 100644 --- a/tests/src/python/test_qgsrangeslider.py +++ b/tests/src/python/test_qgsrangeslider.py @@ -41,9 +41,9 @@ def testSettersGetters(self): w.setPageStep(5) self.assertEqual(w.pageStep(), 5) - self.assertEqual(w.fixedRangeWidth(), -1) - w.setFixedRangeWidth(5) - self.assertEqual(w.fixedRangeWidth(), 5) + self.assertEqual(w.fixedRangeSize(), -1) + w.setFixedRangeSize(5) + self.assertEqual(w.fixedRangeSize(), 5) def testLimits(self): w = QgsRangeSlider() @@ -278,7 +278,7 @@ def test_fixed_range_width(self): """ w = QgsRangeSlider() w.setRangeLimits(0, 100) - w.setFixedRangeWidth(10) + w.setFixedRangeSize(10) self.assertEqual(w.upperValue() - w.lowerValue(), 10) w.setUpperValue(70) @@ -307,7 +307,7 @@ def test_fixed_range_width(self): self.assertEqual(w.lowerValue(), 90) # with zero width fixed range - w.setFixedRangeWidth(0) + w.setFixedRangeSize(0) self.assertEqual(w.upperValue() - w.lowerValue(), 0) w.setUpperValue(70) From c486cf9a4342397d96c2a5062fc6dd5901eafb7b Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 18 Mar 2024 11:11:48 +1000 Subject: [PATCH 39/68] Fix truncated labels --- src/gui/elevation/qgselevationcontrollerwidget.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/gui/elevation/qgselevationcontrollerwidget.cpp b/src/gui/elevation/qgselevationcontrollerwidget.cpp index 136aef50dc95..9f5e52f96853 100644 --- a/src/gui/elevation/qgselevationcontrollerwidget.cpp +++ b/src/gui/elevation/qgselevationcontrollerwidget.cpp @@ -344,7 +344,8 @@ void QgsElevationControllerLabels::setLimits( const QgsDoubleRange &limits ) return; const QFontMetrics fm( font() ); - const int maxChars = QLocale().toString( std::floor( limits.upper() ) ).length() + 3; + const int maxChars = std::max( QLocale().toString( std::floor( limits.lower() ) ).length(), + QLocale().toString( std::floor( limits.upper() ) ).length() ) + 3; setMinimumWidth( fm.horizontalAdvance( '0' ) * maxChars ); mLimits = limits; From 9a2d8ad8bd28638c2b6b59d0eafc17ce7377f63e Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 18 Mar 2024 11:12:50 +1000 Subject: [PATCH 40/68] Update test --- tests/src/python/test_qgselevationcontrollerwidget.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/src/python/test_qgselevationcontrollerwidget.py b/tests/src/python/test_qgselevationcontrollerwidget.py index e155898de073..8c8fe3a2dc10 100644 --- a/tests/src/python/test_qgselevationcontrollerwidget.py +++ b/tests/src/python/test_qgselevationcontrollerwidget.py @@ -108,14 +108,14 @@ def test_slider_interaction(self): self.assertAlmostEqual(w.range().lower(), 459.644, 3) self.assertAlmostEqual(w.range().upper(), 729.495, 3) - def testFixedRangeWidth(self): + def testFixedRangeSize(self): """ - Test that fixed range width is correctly handled + Test that fixed range size is correctly handled """ w = QgsElevationControllerWidget() w.setRangeLimits(QgsDoubleRange(100.5, 1000)) - w.setFixedRangeWidth(10.0001) - self.assertEqual(w.fixedRangeWidth(), 10.0001) + w.setFixedRangeSize(10.0001) + self.assertEqual(w.fixedRangeSize(), 10.0001) w.setRange(QgsDoubleRange(130.3, 920.6)) self.assertAlmostEqual(w.range().upper() - w.range().lower(), 10.0001, 6) From 71583dbc0cd33d85d036bcccef5991bbd2e3b632 Mon Sep 17 00:00:00 2001 From: Denis Rouzaud Date: Mon, 18 Mar 2024 08:57:48 +0100 Subject: [PATCH 41/68] use new settings API in DXF export (#56852) --- src/app/qgsdxfexportdialog.cpp | 3 +-- src/app/qgsdxfexportdialog.h | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index 80e14e6b656d..ff9f1cc5e5a2 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -142,7 +142,6 @@ QgsVectorLayerAndAttributeModel::QgsVectorLayerAndAttributeModel( QgsLayerTree * : QgsLayerTreeModel( rootNode, parent ) { //init mCreateDDBlockInfo, mDDBlocksMaxNumberOfClasses - bool ddBlockDefaultEnabled = QgsSettings().value( QStringLiteral( "/qgis/dxfEnableDDBlocks" ), false ).toBool(); QSet layerIds; retrieveAllLayers( rootNode, layerIds ); for ( const auto &id : std::as_const( layerIds ) ) @@ -150,7 +149,7 @@ QgsVectorLayerAndAttributeModel::QgsVectorLayerAndAttributeModel( QgsLayerTree * const QgsVectorLayer *vLayer = qobject_cast< const QgsVectorLayer *>( QgsProject::instance()->mapLayer( id ) ); if ( vLayer ) { - mCreateDDBlockInfo[vLayer] = ddBlockDefaultEnabled; + mCreateDDBlockInfo[vLayer] = QgsDxfExportDialog::settingsDxfEnableDDBlocks->value(); mDDBlocksMaxNumberOfClasses[vLayer] = -1; } } diff --git a/src/app/qgsdxfexportdialog.h b/src/app/qgsdxfexportdialog.h index 93461a3278c4..973bea2722c3 100644 --- a/src/app/qgsdxfexportdialog.h +++ b/src/app/qgsdxfexportdialog.h @@ -22,6 +22,8 @@ #include "qgslayertreemodel.h" #include "qgslayertreeview.h" #include "qgsdxfexport.h" +#include "qgssettingstree.h" +#include "qgssettingsentryimpl.h" #include #include @@ -97,6 +99,9 @@ class QgsDxfExportDialog : public QDialog, private Ui::QgsDxfExportDialogBase { Q_OBJECT public: + static inline QgsSettingsTreeNode *sTreeAppDdxf = QgsSettingsTree::sTreeApp->createChildNode( QStringLiteral( "dxf" ) ); + static const inline QgsSettingsEntryBool *settingsDxfEnableDDBlocks = new QgsSettingsEntryBool( QStringLiteral( "enable-datadefined-blocks" ), sTreeAppDdxf, false ); + QgsDxfExportDialog( QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags() ); ~QgsDxfExportDialog() override; From 1796318afe6e46c81f9b5ddcd9f2217754948343 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 18 Mar 2024 11:22:08 +1000 Subject: [PATCH 42/68] Add a "fixed range per band" elevation mode for rasters In this mode, each band in the raster can have a fixed elevation range associated with it. This is designed for data sources which expose elevation related data in bands, eg netcdf files, such as a raster with temperate data at different ocean depths. --- python/PyQt6/core/auto_additions/qgis.py | 3 +- python/PyQt6/core/auto_generated/qgis.sip.in | 3 +- .../qgsrasterlayerelevationproperties.sip.in | 36 ++++++ python/core/auto_additions/qgis.py | 3 +- python/core/auto_generated/qgis.sip.in | 3 +- .../qgsrasterlayerelevationproperties.sip.in | 36 ++++++ src/core/qgis.h | 3 +- .../qgsrasterlayerelevationproperties.cpp | 108 ++++++++++++++++++ .../qgsrasterlayerelevationproperties.h | 25 ++++ .../test_qgsrasterlayerelevationproperties.py | 84 ++++++++++++++ 10 files changed, 299 insertions(+), 5 deletions(-) diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index 1725fc3d4ac2..9201ed375903 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -3230,7 +3230,8 @@ # monkey patching scoped based enum Qgis.RasterElevationMode.FixedElevationRange.__doc__ = "Layer has a fixed elevation range" Qgis.RasterElevationMode.RepresentsElevationSurface.__doc__ = "Pixel values represent an elevation surface" -Qgis.RasterElevationMode.__doc__ = "Raster layer elevation modes.\n\n.. versionadded:: 3.38\n\n" + '* ``FixedElevationRange``: ' + Qgis.RasterElevationMode.FixedElevationRange.__doc__ + '\n' + '* ``RepresentsElevationSurface``: ' + Qgis.RasterElevationMode.RepresentsElevationSurface.__doc__ +Qgis.RasterElevationMode.FixedRangePerBand.__doc__ = "Layer has a fixed elevation range per band" +Qgis.RasterElevationMode.__doc__ = "Raster layer elevation modes.\n\n.. versionadded:: 3.38\n\n" + '* ``FixedElevationRange``: ' + Qgis.RasterElevationMode.FixedElevationRange.__doc__ + '\n' + '* ``RepresentsElevationSurface``: ' + Qgis.RasterElevationMode.RepresentsElevationSurface.__doc__ + '\n' + '* ``FixedRangePerBand``: ' + Qgis.RasterElevationMode.FixedRangePerBand.__doc__ # -- Qgis.RasterElevationMode.baseClass = Qgis # monkey patching scoped based enum diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index bac3daa96e7f..2557d927de08 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -1846,7 +1846,8 @@ The development version enum class RasterElevationMode /BaseType=IntEnum/ { FixedElevationRange, - RepresentsElevationSurface + RepresentsElevationSurface, + FixedRangePerBand, }; enum class BetweenLineConstraint /BaseType=IntEnum/ diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in index 17ae8f8bbc95..63fcc4f888a6 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in @@ -134,6 +134,42 @@ Sets the fixed elevation ``range`` for the raster. .. seealso:: :py:func:`fixedRange` +.. versionadded:: 3.38 +%End + + QMap fixedRangePerBand() const; +%Docstring +Returns the fixed elevation range for each band. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.FixedRangePerBand. + +.. note:: + + When a fixed range is set any :py:func:`~QgsRasterLayerElevationProperties.zOffset` and :py:func:`~QgsRasterLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`setFixedRangePerBand` + +.. versionadded:: 3.38 +%End + + void setFixedRangePerBand( const QMap &ranges ); +%Docstring +Sets the fixed elevation range for each band. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.FixedRangePerBand. + +.. note:: + + When a fixed range is set any :py:func:`~QgsRasterLayerElevationProperties.zOffset` and :py:func:`~QgsRasterLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`fixedRangePerBand` + .. versionadded:: 3.38 %End diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index d63cad47d8c3..7481285da9d1 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -3176,7 +3176,8 @@ # monkey patching scoped based enum Qgis.RasterElevationMode.FixedElevationRange.__doc__ = "Layer has a fixed elevation range" Qgis.RasterElevationMode.RepresentsElevationSurface.__doc__ = "Pixel values represent an elevation surface" -Qgis.RasterElevationMode.__doc__ = "Raster layer elevation modes.\n\n.. versionadded:: 3.38\n\n" + '* ``FixedElevationRange``: ' + Qgis.RasterElevationMode.FixedElevationRange.__doc__ + '\n' + '* ``RepresentsElevationSurface``: ' + Qgis.RasterElevationMode.RepresentsElevationSurface.__doc__ +Qgis.RasterElevationMode.FixedRangePerBand.__doc__ = "Layer has a fixed elevation range per band" +Qgis.RasterElevationMode.__doc__ = "Raster layer elevation modes.\n\n.. versionadded:: 3.38\n\n" + '* ``FixedElevationRange``: ' + Qgis.RasterElevationMode.FixedElevationRange.__doc__ + '\n' + '* ``RepresentsElevationSurface``: ' + Qgis.RasterElevationMode.RepresentsElevationSurface.__doc__ + '\n' + '* ``FixedRangePerBand``: ' + Qgis.RasterElevationMode.FixedRangePerBand.__doc__ # -- Qgis.RasterElevationMode.baseClass = Qgis # monkey patching scoped based enum diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index 2a86b3d09d41..cb1e8df0340c 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -1846,7 +1846,8 @@ The development version enum class RasterElevationMode { FixedElevationRange, - RepresentsElevationSurface + RepresentsElevationSurface, + FixedRangePerBand, }; enum class BetweenLineConstraint diff --git a/python/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in b/python/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in index 17ae8f8bbc95..63fcc4f888a6 100644 --- a/python/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in +++ b/python/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in @@ -134,6 +134,42 @@ Sets the fixed elevation ``range`` for the raster. .. seealso:: :py:func:`fixedRange` +.. versionadded:: 3.38 +%End + + QMap fixedRangePerBand() const; +%Docstring +Returns the fixed elevation range for each band. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.FixedRangePerBand. + +.. note:: + + When a fixed range is set any :py:func:`~QgsRasterLayerElevationProperties.zOffset` and :py:func:`~QgsRasterLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`setFixedRangePerBand` + +.. versionadded:: 3.38 +%End + + void setFixedRangePerBand( const QMap &ranges ); +%Docstring +Sets the fixed elevation range for each band. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.FixedRangePerBand. + +.. note:: + + When a fixed range is set any :py:func:`~QgsRasterLayerElevationProperties.zOffset` and :py:func:`~QgsRasterLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`fixedRangePerBand` + .. versionadded:: 3.38 %End diff --git a/src/core/qgis.h b/src/core/qgis.h index 815aa1382fe4..7a690087ec91 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -3256,7 +3256,8 @@ class CORE_EXPORT Qgis enum class RasterElevationMode : int { FixedElevationRange = 0, //!< Layer has a fixed elevation range - RepresentsElevationSurface = 1 //!< Pixel values represent an elevation surface + RepresentsElevationSurface = 1, //!< Pixel values represent an elevation surface + FixedRangePerBand = 2, //!< Layer has a fixed elevation range per band }; Q_ENUM( RasterElevationMode ) diff --git a/src/core/raster/qgsrasterlayerelevationproperties.cpp b/src/core/raster/qgsrasterlayerelevationproperties.cpp index e217f30d9960..74e4ed8e0697 100644 --- a/src/core/raster/qgsrasterlayerelevationproperties.cpp +++ b/src/core/raster/qgsrasterlayerelevationproperties.cpp @@ -60,6 +60,23 @@ QDomElement QgsRasterLayerElevationProperties::writeXml( QDomElement &parentElem element.setAttribute( QStringLiteral( "includeUpper" ), mFixedRange.includeUpper() ? "1" : "0" ); break; + case Qgis::RasterElevationMode::FixedRangePerBand: + { + QDomElement ranges = document.createElement( QStringLiteral( "ranges" ) ); + for ( auto it = mRangePerBand.constBegin(); it != mRangePerBand.constEnd(); ++it ) + { + QDomElement range = document.createElement( QStringLiteral( "range" ) ); + range.setAttribute( QStringLiteral( "band" ), it.key() ); + range.setAttribute( QStringLiteral( "lower" ), qgsDoubleToString( it.value().lower() ) ); + range.setAttribute( QStringLiteral( "upper" ), qgsDoubleToString( it.value().upper() ) ); + range.setAttribute( QStringLiteral( "includeLower" ), it.value().includeLower() ? "1" : "0" ); + range.setAttribute( QStringLiteral( "includeUpper" ), it.value().includeUpper() ? "1" : "0" ); + ranges.appendChild( range ); + } + element.appendChild( ranges ); + break; + } + case Qgis::RasterElevationMode::RepresentsElevationSurface: element.setAttribute( QStringLiteral( "band" ), mBandNumber ); break; @@ -101,6 +118,25 @@ bool QgsRasterLayerElevationProperties::readXml( const QDomElement &element, con mFixedRange = QgsDoubleRange( lower, upper, includeLower, includeUpper ); break; } + + case Qgis::RasterElevationMode::FixedRangePerBand: + { + mRangePerBand.clear(); + + const QDomNodeList ranges = elevationElement.firstChildElement( QStringLiteral( "ranges" ) ).childNodes(); + for ( int i = 0; i < ranges.size(); ++i ) + { + const QDomElement rangeElement = ranges.at( i ).toElement(); + const int band = rangeElement.attribute( QStringLiteral( "band" ) ).toInt(); + const double lower = rangeElement.attribute( QStringLiteral( "lower" ) ).toDouble(); + const double upper = rangeElement.attribute( QStringLiteral( "upper" ) ).toDouble(); + const bool includeLower = rangeElement.attribute( QStringLiteral( "includeLower" ) ).toInt(); + const bool includeUpper = rangeElement.attribute( QStringLiteral( "includeUpper" ) ).toInt(); + mRangePerBand.insert( band, QgsDoubleRange( lower, upper, includeLower, includeUpper ) ); + } + break; + } + case Qgis::RasterElevationMode::RepresentsElevationSurface: mBandNumber = elevationElement.attribute( QStringLiteral( "band" ), QStringLiteral( "1" ) ).toInt(); break; @@ -132,6 +168,7 @@ QgsRasterLayerElevationProperties *QgsRasterLayerElevationProperties::clone() co res->setElevationLimit( mElevationLimit ); res->setBandNumber( mBandNumber ); res->setFixedRange( mFixedRange ); + res->setFixedRangePerBand( mRangePerBand ); res->copyCommonProperties( this ); return res.release(); } @@ -145,6 +182,15 @@ QString QgsRasterLayerElevationProperties::htmlSummary() const properties << tr( "Elevation range: %1 to %2" ).arg( mFixedRange.lower() ).arg( mFixedRange.upper() ); break; + case Qgis::RasterElevationMode::FixedRangePerBand: + { + for ( auto it = mRangePerBand.constBegin(); it != mRangePerBand.constEnd(); ++it ) + { + properties << tr( "Elevation for band %1: %2 to %3" ).arg( it.key() ).arg( it.value().lower() ).arg( it.value().upper() ); + } + break; + } + case Qgis::RasterElevationMode::RepresentsElevationSurface: properties << tr( "Elevation band: %1" ).arg( mBandNumber ); properties << tr( "Scale: %1" ).arg( mZScale ); @@ -162,6 +208,16 @@ bool QgsRasterLayerElevationProperties::isVisibleInZRange( const QgsDoubleRange case Qgis::RasterElevationMode::FixedElevationRange: return mFixedRange.overlaps( range ); + case Qgis::RasterElevationMode::FixedRangePerBand: + { + for ( auto it = mRangePerBand.constBegin(); it != mRangePerBand.constEnd(); ++it ) + { + if ( it.value().overlaps( range ) ) + return true; + } + return false; + } + case Qgis::RasterElevationMode::RepresentsElevationSurface: // TODO -- test actual raster z range return true; @@ -176,6 +232,36 @@ QgsDoubleRange QgsRasterLayerElevationProperties::calculateZRange( QgsMapLayer * case Qgis::RasterElevationMode::FixedElevationRange: return mFixedRange; + case Qgis::RasterElevationMode::FixedRangePerBand: + { + double lower = std::numeric_limits< double >::max(); + double upper = std::numeric_limits< double >::min(); + bool includeLower = true; + bool includeUpper = true; + for ( auto it = mRangePerBand.constBegin(); it != mRangePerBand.constEnd(); ++it ) + { + if ( it.value().lower() < lower ) + { + lower = it.value().lower(); + includeLower = it.value().includeLower(); + } + else if ( !includeLower && it.value().lower() == lower && it.value().includeLower() ) + { + includeLower = true; + } + if ( it.value().upper() > upper ) + { + upper = it.value().upper(); + includeUpper = it.value().includeUpper(); + } + else if ( !includeUpper && it.value().upper() == upper && it.value().includeUpper() ) + { + includeUpper = true; + } + } + return QgsDoubleRange( lower, upper, includeLower, includeUpper ); + } + case Qgis::RasterElevationMode::RepresentsElevationSurface: // TODO -- determine actual z range from raster statistics return QgsDoubleRange(); @@ -232,6 +318,14 @@ QgsDoubleRange QgsRasterLayerElevationProperties::elevationRangeForPixelValue( i case Qgis::RasterElevationMode::FixedElevationRange: return mFixedRange; + case Qgis::RasterElevationMode::FixedRangePerBand: + { + auto it = mRangePerBand.constFind( band ); + if ( it != mRangePerBand.constEnd() ) + return it.value(); + return QgsDoubleRange(); + } + case Qgis::RasterElevationMode::RepresentsElevationSurface: { if ( band != mBandNumber ) @@ -382,6 +476,20 @@ void QgsRasterLayerElevationProperties::setDefaultProfileFillSymbol( const QColo mProfileFillSymbol = std::make_unique< QgsFillSymbol>( QgsSymbolLayerList( { profileFillLayer.release() } ) ); } +QMap QgsRasterLayerElevationProperties::fixedRangePerBand() const +{ + return mRangePerBand; +} + +void QgsRasterLayerElevationProperties::setFixedRangePerBand( const QMap &ranges ) +{ + if ( ranges == mRangePerBand ) + return; + + mRangePerBand = ranges; + emit changed(); +} + QgsDoubleRange QgsRasterLayerElevationProperties::fixedRange() const { return mFixedRange; diff --git a/src/core/raster/qgsrasterlayerelevationproperties.h b/src/core/raster/qgsrasterlayerelevationproperties.h index bf125eb5c9f4..2b27e9fd7e2b 100644 --- a/src/core/raster/qgsrasterlayerelevationproperties.h +++ b/src/core/raster/qgsrasterlayerelevationproperties.h @@ -127,6 +127,30 @@ class CORE_EXPORT QgsRasterLayerElevationProperties : public QgsMapLayerElevatio */ void setFixedRange( const QgsDoubleRange &range ); + /** + * Returns the fixed elevation range for each band. + * + * \note This is only considered when mode() is Qgis::RasterElevationMode::FixedRangePerBand. + * + * \note When a fixed range is set any zOffset() and zScale() is ignored. + * + * \see setFixedRangePerBand() + * \since QGIS 3.38 + */ + QMap fixedRangePerBand() const; + + /** + * Sets the fixed elevation range for each band. + * + * \note This is only considered when mode() is Qgis::RasterElevationMode::FixedRangePerBand. + * + * \note When a fixed range is set any zOffset() and zScale() is ignored. + * + * \see fixedRangePerBand() + * \since QGIS 3.38 + */ + void setFixedRangePerBand( const QMap &ranges ); + /** * Returns the elevation range corresponding to a raw pixel value from the specified \a band. * @@ -239,6 +263,7 @@ class CORE_EXPORT QgsRasterLayerElevationProperties : public QgsMapLayerElevatio int mBandNumber = 1; QgsDoubleRange mFixedRange; + QMap< int, QgsDoubleRange > mRangePerBand; }; diff --git a/tests/src/python/test_qgsrasterlayerelevationproperties.py b/tests/src/python/test_qgsrasterlayerelevationproperties.py index b7a27ab23979..a520de6133ae 100644 --- a/tests/src/python/test_qgsrasterlayerelevationproperties.py +++ b/tests/src/python/test_qgsrasterlayerelevationproperties.py @@ -166,6 +166,76 @@ def test_basic_fixed_range(self): includeLower=False, includeUpper=True)) + def test_basic_fixed_range_per_band(self): + """ + Basic tests for the class using the FixedRangePerBand mode + """ + props = QgsRasterLayerElevationProperties(None) + self.assertFalse(props.fixedRangePerBand()) + + props.setMode(Qgis.RasterElevationMode.FixedRangePerBand) + props.setFixedRangePerBand({1: QgsDoubleRange(103.1, 106.8), + 2: QgsDoubleRange(106.8, 116.8), + 3: QgsDoubleRange(116.8, 126.8)}) + # fixed ranges should not be affected by scale/offset + props.setZOffset(0.5) + props.setZScale(2) + self.assertEqual(props.fixedRangePerBand(), {1: QgsDoubleRange(103.1, 106.8), + 2: QgsDoubleRange(106.8, 116.8), + 3: QgsDoubleRange(116.8, 126.8)}) + self.assertEqual(props.calculateZRange(None), + QgsDoubleRange(103.1, 126.8)) + self.assertFalse(props.isVisibleInZRange(QgsDoubleRange(3.1, 6.8))) + self.assertTrue(props.isVisibleInZRange(QgsDoubleRange(3.1, 104.8))) + self.assertTrue(props.isVisibleInZRange(QgsDoubleRange(104.8, 114.8))) + self.assertTrue(props.isVisibleInZRange(QgsDoubleRange(114.8, 124.8))) + self.assertFalse(props.isVisibleInZRange(QgsDoubleRange(128.8, 134.8))) + + doc = QDomDocument("testdoc") + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsRasterLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.mode(), + Qgis.RasterElevationMode.FixedRangePerBand) + self.assertEqual(props2.fixedRangePerBand(), {1: QgsDoubleRange(103.1, 106.8), + 2: QgsDoubleRange(106.8, 116.8), + 3: QgsDoubleRange(116.8, 126.8)}) + + props2 = props.clone() + self.assertEqual(props2.mode(), + Qgis.RasterElevationMode.FixedRangePerBand) + self.assertEqual(props2.fixedRangePerBand(), {1: QgsDoubleRange(103.1, 106.8), + 2: QgsDoubleRange(106.8, 116.8), + 3: QgsDoubleRange(116.8, 126.8)}) + + # include lower, exclude upper + props.setFixedRangePerBand({1: QgsDoubleRange(103.1, 106.8, + includeLower=True, + includeUpper=False)}) + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsRasterLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.fixedRangePerBand(), {1: QgsDoubleRange(103.1, 106.8, + includeLower=True, + includeUpper=False)}) + + # exclude lower, include upper + props.setFixedRangePerBand({1: QgsDoubleRange(103.1, 106.8, + includeLower=False, + includeUpper=True)}) + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsRasterLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.fixedRangePerBand(), {1: QgsDoubleRange(103.1, 106.8, + includeLower=False, + includeUpper=True)}) + def test_looks_like_dem(self): layer = QgsRasterLayer( os.path.join(unitTestDataPath(), 'landsat.tif'), 'i am not a dem') @@ -245,6 +315,20 @@ def test_elevation_range_for_pixel_value(self): props.elevationRangeForPixelValue(band=1, pixelValue=3), QgsDoubleRange(11, 15)) + # with fixed range per band mode + props.setMode(Qgis.RasterElevationMode.FixedRangePerBand) + props.setFixedRangePerBand({1: QgsDoubleRange(11, 15), + 2: QgsDoubleRange(16, 25)}) + self.assertEqual( + props.elevationRangeForPixelValue(band=1, pixelValue=math.nan), + QgsDoubleRange()) + self.assertEqual( + props.elevationRangeForPixelValue(band=1, pixelValue=3), + QgsDoubleRange(11, 15)) + self.assertEqual( + props.elevationRangeForPixelValue(band=2, pixelValue=3), + QgsDoubleRange(16, 25)) + if __name__ == '__main__': unittest.main() From a3f1f910cfc546b0f0a5b60813e00dbce858a2c1 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 18 Mar 2024 11:23:42 +1000 Subject: [PATCH 43/68] Add GUI to configure fixed range per band mode This is exposed as a user editable table of raster bands with lower and upper values. Users can either populate the lower and upper values manually, or use an expression to auto fill all band values based on a qgis expression. The expression based fill allows for users to design expressions which extract useful information from band names, eg extracting the depth value from a "Band 001: depth=-5500 (meters)" style band name. --- .../qgsrasterelevationpropertieswidget.cpp | 302 ++++++++++++++++++ .../qgsrasterelevationpropertieswidget.h | 48 ++- .../qgsrasterelevationpropertieswidgetbase.ui | 84 ++++- 3 files changed, 431 insertions(+), 3 deletions(-) diff --git a/src/app/raster/qgsrasterelevationpropertieswidget.cpp b/src/app/raster/qgsrasterelevationpropertieswidget.cpp index 8ff05b96d651..1344b2e556c7 100644 --- a/src/app/raster/qgsrasterelevationpropertieswidget.cpp +++ b/src/app/raster/qgsrasterelevationpropertieswidget.cpp @@ -20,6 +20,9 @@ #include "qgsrasterlayerelevationproperties.h" #include "qgslinesymbol.h" #include "qgsfillsymbol.h" +#include "qgsexpressionbuilderdialog.h" +#include +#include QgsRasterElevationPropertiesWidget::QgsRasterElevationPropertiesWidget( QgsRasterLayer *layer, QgsMapCanvas *canvas, QWidget *parent ) : QgsMapLayerConfigWidget( layer, canvas, parent ) @@ -30,6 +33,7 @@ QgsRasterElevationPropertiesWidget::QgsRasterElevationPropertiesWidget( QgsRaste mModeComboBox->addItem( tr( "Disabled" ) ); mModeComboBox->addItem( tr( "Represents Elevation Surface" ), QVariant::fromValue( Qgis::RasterElevationMode::RepresentsElevationSurface ) ); mModeComboBox->addItem( tr( "Fixed Elevation Range" ), QVariant::fromValue( Qgis::RasterElevationMode::FixedElevationRange ) ); + mModeComboBox->addItem( tr( "Fixed Elevation Range Per Band" ), QVariant::fromValue( Qgis::RasterElevationMode::FixedRangePerBand ) ); mLimitsComboBox->addItem( tr( "Include Lower and Upper" ), QVariant::fromValue( Qgis::RangeLimits::IncludeBoth ) ); mLimitsComboBox->addItem( tr( "Include Lower, Exclude Upper" ), QVariant::fromValue( Qgis::RangeLimits::IncludeLowerExcludeUpper ) ); @@ -53,6 +57,32 @@ QgsRasterElevationPropertiesWidget::QgsRasterElevationPropertiesWidget( QgsRaste mStyleComboBox->addItem( QgsApplication::getThemeIcon( QStringLiteral( "mIconSurfaceElevationFillAbove.svg" ) ), tr( "Fill Above" ), static_cast< int >( Qgis::ProfileSurfaceSymbology::FillAbove ) ); mElevationLimitSpinBox->setClearValue( mElevationLimitSpinBox->minimum(), tr( "Not set" ) ); + // NOTE -- this doesn't work, there's something broken in QgsStackedWidget which breaks the height calculations + mPageFixedRangePerBand->setFixedHeight( QFontMetrics( font() ).height() * 15 ); + + mFixedRangePerBandModel = new QgsRasterBandFixedElevationRangeModel( this ); + mBandElevationTable->verticalHeader()->setVisible( false ); + mBandElevationTable->setModel( mFixedRangePerBandModel ); + QgsFixedElevationRangeDelegate *tableDelegate = new QgsFixedElevationRangeDelegate( mBandElevationTable ); + mBandElevationTable->setItemDelegateForColumn( 1, tableDelegate ); + mBandElevationTable->setItemDelegateForColumn( 2, tableDelegate ); + + QMenu *calculateFixedRangePerBandMenu = new QMenu( mCalculateFixedRangePerBandButton ); + mCalculateFixedRangePerBandButton->setMenu( calculateFixedRangePerBandMenu ); + mCalculateFixedRangePerBandButton->setPopupMode( QToolButton::InstantPopup ); + QAction *calculateLowerAction = new QAction( "Calculate Lower by Expression…", calculateFixedRangePerBandMenu ); + calculateFixedRangePerBandMenu->addAction( calculateLowerAction ); + connect( calculateLowerAction, &QAction::triggered, this, [this] + { + calculateRangeByExpression( false ); + } ); + QAction *calculateUpperAction = new QAction( "Calculate Upper by Expression…", calculateFixedRangePerBandMenu ); + calculateFixedRangePerBandMenu->addAction( calculateUpperAction ); + connect( calculateUpperAction, &QAction::triggered, this, [this] + { + calculateRangeByExpression( true ); + } ); + syncToLayer( layer ); connect( mOffsetZSpinBox, qOverload( &QDoubleSpinBox::valueChanged ), this, &QgsRasterElevationPropertiesWidget::onChanged ); @@ -89,6 +119,7 @@ void QgsRasterElevationPropertiesWidget::syncToLayer( QgsMapLayer *layer ) return; mBlockUpdates = true; + const QgsRasterLayerElevationProperties *props = qgis::down_cast< const QgsRasterLayerElevationProperties * >( mLayer->elevationProperties() ); if ( !props->isEnabled() ) { @@ -107,6 +138,9 @@ void QgsRasterElevationPropertiesWidget::syncToLayer( QgsMapLayer *layer ) case Qgis::RasterElevationMode::RepresentsElevationSurface: mStackedWidget->setCurrentWidget( mPageSurface ); break; + case Qgis::RasterElevationMode::FixedRangePerBand: + mStackedWidget->setCurrentWidget( mPageFixedRangePerBand ); + break; } mProfileChartGroupBox->show(); } @@ -132,6 +166,11 @@ void QgsRasterElevationPropertiesWidget::syncToLayer( QgsMapLayer *layer ) mFixedUpperSpinBox->clear(); mLimitsComboBox->setCurrentIndex( mLimitsComboBox->findData( QVariant::fromValue( props->fixedRange().rangeLimits() ) ) ); + mFixedRangePerBandModel->setLayerData( mLayer, props->fixedRangePerBand() ); + mBandElevationTable->horizontalHeader()->setSectionResizeMode( 0, QHeaderView::Stretch ); + mBandElevationTable->horizontalHeader()->setSectionResizeMode( 1, QHeaderView::Stretch ); + mBandElevationTable->horizontalHeader()->setSectionResizeMode( 2, QHeaderView::Stretch ); + mStyleComboBox->setCurrentIndex( mStyleComboBox->findData( static_cast ( props->profileSymbology() ) ) ); switch ( props->profileSymbology() ) { @@ -184,6 +223,8 @@ void QgsRasterElevationPropertiesWidget::apply() props->setFixedRange( QgsDoubleRange( fixedLower, fixedUpper, mLimitsComboBox->currentData().value< Qgis::RangeLimits >() ) ); + props->setFixedRangePerBand( mFixedRangePerBandModel->rangeData() ); + mLayer->trigger3DUpdate(); } @@ -199,6 +240,9 @@ void QgsRasterElevationPropertiesWidget::modeChanged() case Qgis::RasterElevationMode::RepresentsElevationSurface: mStackedWidget->setCurrentWidget( mPageSurface ); break; + case Qgis::RasterElevationMode::FixedRangePerBand: + mStackedWidget->setCurrentWidget( mPageFixedRangePerBand ); + break; } mProfileChartGroupBox->show(); } @@ -217,6 +261,39 @@ void QgsRasterElevationPropertiesWidget::onChanged() emit widgetChanged(); } +void QgsRasterElevationPropertiesWidget::calculateRangeByExpression( bool isUpper ) +{ + QgsExpressionContext expressionContext; + QgsExpressionContextScope *bandScope = new QgsExpressionContextScope(); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band" ), 1, true, false, tr( "Band number" ) ) ); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band_name" ), mLayer->dataProvider()->displayBandName( 1 ), true, false, tr( "Band name" ) ) ); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band_description" ), mLayer->dataProvider()->bandDescription( 1 ), true, false, tr( "Band description" ) ) ); + + expressionContext.appendScope( bandScope ); + expressionContext.setHighlightedVariables( { QStringLiteral( "band" ), QStringLiteral( "band_name" ), QStringLiteral( "band_description" )} ); + + QgsExpressionBuilderDialog dlg = QgsExpressionBuilderDialog( nullptr, isUpper ? mFixedRangeUpperExpression : mFixedRangeLowerExpression, this, QStringLiteral( "generic" ), expressionContext ); + if ( dlg.exec() ) + { + if ( isUpper ) + mFixedRangeUpperExpression = dlg.expressionText(); + else + mFixedRangeLowerExpression = dlg.expressionText(); + + QgsExpression exp( dlg.expressionText() ); + exp.prepare( &expressionContext ); + for ( int band = 1; band <= mLayer->bandCount(); ++band ) + { + bandScope->setVariable( QStringLiteral( "band" ), band ); + bandScope->setVariable( QStringLiteral( "band_name" ), mLayer->dataProvider()->displayBandName( band ) ); + bandScope->setVariable( QStringLiteral( "band_description" ), mLayer->dataProvider()->bandDescription( band ) ); + + const QVariant res = exp.evaluate( &expressionContext ); + mFixedRangePerBandModel->setData( mFixedRangePerBandModel->index( band - 1, isUpper ? 2 : 1 ), res, Qt::EditRole ); + } + } +} + // // QgsRasterElevationPropertiesWidgetFactory @@ -254,3 +331,228 @@ QString QgsRasterElevationPropertiesWidgetFactory::layerPropertiesPagePositionHi return QStringLiteral( "mOptsPage_Metadata" ); } +// +// QgsRasterBandFixedElevationRangeModel +// + +QgsRasterBandFixedElevationRangeModel::QgsRasterBandFixedElevationRangeModel( QObject *parent ) + : QAbstractItemModel( parent ) +{ + +} + +int QgsRasterBandFixedElevationRangeModel::columnCount( const QModelIndex & ) const +{ + return 3; +} + +int QgsRasterBandFixedElevationRangeModel::rowCount( const QModelIndex &parent ) const +{ + if ( parent.isValid() ) + return 0; + return mBandCount; +} + +QModelIndex QgsRasterBandFixedElevationRangeModel::index( int row, int column, const QModelIndex &parent ) const +{ + if ( hasIndex( row, column, parent ) ) + { + return createIndex( row, column, row ); + } + + return QModelIndex(); +} + +QModelIndex QgsRasterBandFixedElevationRangeModel::parent( const QModelIndex &child ) const +{ + Q_UNUSED( child ) + return QModelIndex(); +} + +Qt::ItemFlags QgsRasterBandFixedElevationRangeModel::flags( const QModelIndex &index ) const +{ + if ( !index.isValid() ) + return Qt::ItemFlags(); + + if ( index.row() < 0 || index.row() >= mBandCount || index.column() < 0 || index.column() >= columnCount() ) + return Qt::ItemFlags(); + + switch ( index.column() ) + { + case 0: + return Qt::ItemFlag::ItemIsEnabled; + case 1: + case 2: + return Qt::ItemFlag::ItemIsEnabled | Qt::ItemFlag::ItemIsEditable | Qt::ItemFlag::ItemIsSelectable; + default: + break; + } + + return Qt::ItemFlags(); +} + +QVariant QgsRasterBandFixedElevationRangeModel::data( const QModelIndex &index, int role ) const +{ + if ( !index.isValid() ) + return QVariant(); + + if ( index.row() < 0 || index.row() >= mBandCount || index.column() < 0 || index.column() >= columnCount() ) + return QVariant(); + + const int band = index.row() + 1; + const QgsDoubleRange range = mRanges.value( band ); + + switch ( role ) + { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::ToolTipRole: + { + switch ( index.column() ) + { + case 0: + return mBandNames.value( band, QString::number( band ) ); + + case 1: + return range.lower() > std::numeric_limits< double >::lowest() ? range.lower() : QVariant(); + + case 2: + return range.upper() < std::numeric_limits< double >::max() ? range.upper() : QVariant(); + + default: + break; + } + break; + } + + case Qt::TextAlignmentRole: + { + switch ( index.column() ) + { + case 0: + return static_cast( Qt::AlignLeft | Qt::AlignVCenter ); + + case 1: + case 2: + return static_cast( Qt::AlignRight | Qt::AlignVCenter ); + default: + break; + } + break; + } + + default: + break; + } + return QVariant(); +} + +QVariant QgsRasterBandFixedElevationRangeModel::headerData( int section, Qt::Orientation orientation, int role ) const +{ + if ( role == Qt::DisplayRole && orientation == Qt::Horizontal ) + { + switch ( section ) + { + case 0: + return tr( "Band" ); + case 1: + return tr( "Lower" ); + case 2: + return tr( "Upper" ); + default: + break; + } + } + return QAbstractItemModel::headerData( section, orientation, role ); +} + +bool QgsRasterBandFixedElevationRangeModel::setData( const QModelIndex &index, const QVariant &value, int role ) +{ + if ( !index.isValid() ) + return false; + + if ( index.row() > mBandCount || index.row() < 0 ) + return false; + + const int band = index.row() + 1; + const QgsDoubleRange range = mRanges.value( band ); + + switch ( role ) + { + case Qt::EditRole: + { + bool ok = false; + double newValue = value.toDouble( &ok ); + if ( !ok ) + return false; + + switch ( index.column() ) + { + case 1: + { + mRanges[band] = QgsDoubleRange( newValue, range.upper(), range.includeLower(), range.includeUpper() ); + emit dataChanged( index, index, QVector() << role ); + break; + } + + case 2: + mRanges[band] = QgsDoubleRange( range.lower(), newValue, range.includeLower(), range.includeUpper() ); + emit dataChanged( index, index, QVector() << role ); + break; + + default: + break; + } + return true; + } + + default: + break; + } + + return false; +} + +void QgsRasterBandFixedElevationRangeModel::setLayerData( QgsRasterLayer *layer, const QMap &ranges ) +{ + beginResetModel(); + + mBandCount = layer->bandCount(); + mRanges = ranges; + + mBandNames.clear(); + for ( int band = 1; band <= mBandCount; ++band ) + { + mBandNames[band] = layer->dataProvider()->displayBandName( band ); + } + + endResetModel(); +} + +// +// QgsFixedElevationRangeDelegate +// + +QgsFixedElevationRangeDelegate::QgsFixedElevationRangeDelegate( QObject *parent ) + : QStyledItemDelegate( parent ) +{ + +} + +QWidget *QgsFixedElevationRangeDelegate::createEditor( QWidget *parent, const QStyleOptionViewItem &, const QModelIndex & ) const +{ + QgsDoubleSpinBox *spin = new QgsDoubleSpinBox( parent ); + spin->setDecimals( 4 ); + spin->setMinimum( -9999999998.0 ); + spin->setMaximum( 9999999999.0 ); + spin->setShowClearButton( false ); + return spin; +} + +void QgsFixedElevationRangeDelegate::setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const +{ + if ( QgsDoubleSpinBox *spin = qobject_cast< QgsDoubleSpinBox * >( editor ) ) + { + model->setData( index, spin->value() ); + } +} diff --git a/src/app/raster/qgsrasterelevationpropertieswidget.h b/src/app/raster/qgsrasterelevationpropertieswidget.h index f634b4f489df..ca392a9d8736 100644 --- a/src/app/raster/qgsrasterelevationpropertieswidget.h +++ b/src/app/raster/qgsrasterelevationpropertieswidget.h @@ -18,11 +18,53 @@ #include "qgsmaplayerconfigwidget.h" #include "qgsmaplayerconfigwidgetfactory.h" - #include "ui_qgsrasterelevationpropertieswidgetbase.h" +#include +#include + class QgsRasterLayer; +class QgsRasterBandFixedElevationRangeModel : public QAbstractItemModel +{ + Q_OBJECT + + public: + + QgsRasterBandFixedElevationRangeModel( QObject *parent ); + int columnCount( const QModelIndex &parent = QModelIndex() ) const override; + int rowCount( const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex index( int row, int column, const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex parent( const QModelIndex &child ) const override; + Qt::ItemFlags flags( const QModelIndex &index ) const override; + QVariant data( const QModelIndex &index, int role ) const override; + QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const override; + bool setData( const QModelIndex &index, const QVariant &value, int role ) override; + + void setLayerData( QgsRasterLayer *layer, const QMap &ranges ); + QMap rangeData() const { return mRanges; } + + private: + + int mBandCount = 0; + QMap mBandNames; + QMap mRanges; +}; + +class QgsFixedElevationRangeDelegate : public QStyledItemDelegate +{ + Q_OBJECT + + public: + + QgsFixedElevationRangeDelegate( QObject *parent ); + + protected: + QWidget *createEditor( QWidget *parent, const QStyleOptionViewItem & /*option*/, const QModelIndex &index ) const override; + void setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const override; + +}; + class QgsRasterElevationPropertiesWidget : public QgsMapLayerConfigWidget, private Ui::QgsRasterElevationPropertiesWidgetBase { Q_OBJECT @@ -39,11 +81,15 @@ class QgsRasterElevationPropertiesWidget : public QgsMapLayerConfigWidget, priva void modeChanged(); void onChanged(); + void calculateRangeByExpression( bool isUpper ); private: QgsRasterLayer *mLayer = nullptr; bool mBlockUpdates = false; + QgsRasterBandFixedElevationRangeModel *mFixedRangePerBandModel = nullptr; + QString mFixedRangeLowerExpression = QStringLiteral( "@band" ); + QString mFixedRangeUpperExpression = QStringLiteral( "@band" ); }; diff --git a/src/ui/raster/qgsrasterelevationpropertieswidgetbase.ui b/src/ui/raster/qgsrasterelevationpropertieswidgetbase.ui index 248fee84b668..0e264f880f22 100644 --- a/src/ui/raster/qgsrasterelevationpropertieswidgetbase.ui +++ b/src/ui/raster/qgsrasterelevationpropertieswidgetbase.ui @@ -44,7 +44,7 @@ QFrame::NoFrame
    - 2 + 3 @@ -230,6 +230,84 @@ + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">Each band in the raster layer is associated with a fixed elevation range.</span></p><p>This mode can be used when a layer has elevation data exposed through different raster bands.</p></body></html> + + + true + + + + + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + ... + + + + :/images/themes/default/mIconExpression.svg:/images/themes/default/mIconExpression.svg + + + QToolButton::MenuButtonPopup + + + false + + + + + + +
    @@ -404,6 +482,8 @@ 1 - + + +
    From b3d3a61d75d1383adcb554fc4404e4c52d57fc0f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 18 Mar 2024 11:43:31 +1000 Subject: [PATCH 44/68] Handle fixed range per band mode in raster renderer --- src/core/raster/qgsrasterlayerrenderer.cpp | 28 ++++++ .../src/python/test_qgsrasterlayerrenderer.py | 84 +++++++++++++++++- ...ected_elevation_range_per_band_match_3.png | Bin 0 -> 471523 bytes ...ected_elevation_range_per_band_match_4.png | Bin 0 -> 471523 bytes ...ected_elevation_range_per_band_match_5.png | Bin 0 -> 471523 bytes ...ted_elevation_range_per_band_no_filter.png | Bin 0 -> 471523 bytes 6 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_match_3/expected_elevation_range_per_band_match_3.png create mode 100644 tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_match_4/expected_elevation_range_per_band_match_4.png create mode 100644 tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_match_5/expected_elevation_range_per_band_match_5.png create mode 100644 tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_no_filter/expected_elevation_range_per_band_no_filter.png diff --git a/src/core/raster/qgsrasterlayerrenderer.cpp b/src/core/raster/qgsrasterlayerrenderer.cpp index aae5aa26e9d9..b449cbcb3560 100644 --- a/src/core/raster/qgsrasterlayerrenderer.cpp +++ b/src/core/raster/qgsrasterlayerrenderer.cpp @@ -315,6 +315,34 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender // render context range doesn't match the layer's fixed elevation range break; + case Qgis::RasterElevationMode::FixedRangePerBand: + { + // find the top-most band which matches the map range + const QMap< int, QgsDoubleRange > rangePerBand = elevProp->fixedRangePerBand(); + int currentMatchingBand = -1; + QgsDoubleRange currentMatchingRange; + for ( auto it = rangePerBand.constBegin(); it != rangePerBand.constEnd(); ++it ) + { + if ( it.value().overlaps( rendererContext.zRange() ) ) + { + if ( currentMatchingRange.isInfinite() + || ( it.value().includeUpper() && it.value().upper() >= currentMatchingRange.upper() ) + || ( !currentMatchingRange.includeUpper() && it.value().upper() >= currentMatchingRange.upper() ) ) + { + currentMatchingBand = it.key(); + currentMatchingRange = it.value(); + } + } + } + + // this is guaranteed, as we won't ever be creating a renderer if this condition is not met, but let's be ultra safe! + if ( currentMatchingBand > 0 ) + { + mPipe->renderer()->setInputBand( currentMatchingBand ); + } + break; + } + case Qgis::RasterElevationMode::RepresentsElevationSurface: { if ( mPipe->renderer()->usesBands().contains( mElevationBand ) ) diff --git a/tests/src/python/test_qgsrasterlayerrenderer.py b/tests/src/python/test_qgsrasterlayerrenderer.py index 355c52c33ae8..c4c55554ffe6 100644 --- a/tests/src/python/test_qgsrasterlayerrenderer.py +++ b/tests/src/python/test_qgsrasterlayerrenderer.py @@ -20,7 +20,9 @@ QgsMapSettings, QgsRasterLayer, QgsRectangle, - QgsDoubleRange + QgsDoubleRange, + QgsSingleBandGrayRenderer, + QgsContrastEnhancement ) import unittest from qgis.testing import start_app, QgisTestCase @@ -119,7 +121,7 @@ def test_render_dem_with_z_range_filter(self): def test_render_fixed_elevation_range_with_z_range_filter(self): """ Test rendering a raster with a fixed elevation range when - map settings has a z range filtrer + map settings has a z range filter """ raster_layer = QgsRasterLayer(os.path.join(TEST_DATA_DIR, '3d', 'dtm.tif')) self.assertTrue(raster_layer.isValid()) @@ -167,6 +169,84 @@ def test_render_fixed_elevation_range_with_z_range_filter(self): map_settings) ) + def test_render_fixed_range_per_band_with_z_range_filter(self): + """ + Test rendering a raster with a fixed range per band when + map settings has a z range filter + """ + raster_layer = QgsRasterLayer(os.path.join(TEST_DATA_DIR, 'landsat_4326.tif')) + self.assertTrue(raster_layer.isValid()) + + renderer = QgsSingleBandGrayRenderer(raster_layer.dataProvider(), 3) + contrast = QgsContrastEnhancement() + contrast.setMinimumValue(70) + contrast.setMaximumValue(125) + renderer.setContrastEnhancement(contrast) + raster_layer.setRenderer(renderer) + + # set layer as elevation enabled + raster_layer.elevationProperties().setEnabled(True) + raster_layer.elevationProperties().setMode( + Qgis.RasterElevationMode.FixedRangePerBand + ) + raster_layer.elevationProperties().setFixedRangePerBand( + {3: QgsDoubleRange(33, 38), + 4: QgsDoubleRange(35, 40), + 5: QgsDoubleRange(40, 48)} + ) + + map_settings = QgsMapSettings() + map_settings.setOutputSize(QSize(400, 400)) + map_settings.setOutputDpi(96) + map_settings.setDestinationCrs(raster_layer.crs()) + map_settings.setExtent(raster_layer.extent()) + map_settings.setLayers([raster_layer]) + + # no filter on map settings + map_settings.setZRange(QgsDoubleRange()) + self.assertTrue( + self.render_map_settings_check( + 'No Z range filter on map settings, elevation range per band', + 'elevation_range_per_band_no_filter', + map_settings) + ) + + # map settings range matches band 3 only + map_settings.setZRange(QgsDoubleRange(30, 34)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings matches band 3 only', + 'elevation_range_per_band_match_3', + map_settings) + ) + + # map settings range matches band 3 and 4, should pick the highest (4) + map_settings.setZRange(QgsDoubleRange(36, 38.5)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings matches band 3 and 4', + 'elevation_range_per_band_match_4', + map_settings) + ) + + # map settings range matches band 5 + map_settings.setZRange(QgsDoubleRange(46, 58.5)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings matches band 5', + 'elevation_range_per_band_match_5', + map_settings) + ) + + # map settings range excludes layer's range + map_settings.setZRange(QgsDoubleRange(130, 135)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings outside of layer band ranges', + 'fixed_elevation_range_excluded', + map_settings) + ) + if __name__ == '__main__': unittest.main() diff --git a/tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_match_3/expected_elevation_range_per_band_match_3.png b/tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_match_3/expected_elevation_range_per_band_match_3.png new file mode 100644 index 0000000000000000000000000000000000000000..00beb87ed28cd3c8181fe7dc7158223331331552 GIT binary patch literal 471523 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 literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_match_4/expected_elevation_range_per_band_match_4.png b/tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_match_4/expected_elevation_range_per_band_match_4.png new file mode 100644 index 0000000000000000000000000000000000000000..152c5ef8059f8bc2d8d51703ffa021ed2059e9f7 GIT binary patch literal 471523 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 literal 0 HcmV?d00001 From e9730b1bb38e47c6b3e37970df38de9fe21d6534 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 18 Mar 2024 12:08:21 +1000 Subject: [PATCH 45/68] Add capabilities for raster renderers And selectively expose some of QgsRasterRendererRegistry to python --- python/PyQt6/core/auto_additions/qgis.py | 8 ++ python/PyQt6/core/auto_generated/qgis.sip.in | 10 +++ .../raster/qgsrasterrendererregistry.sip.in | 72 +++++++++++++++++ python/PyQt6/core/core_auto.sip | 1 + python/core/auto_additions/qgis.py | 7 ++ python/core/auto_generated/qgis.sip.in | 10 +++ .../raster/qgsrasterrendererregistry.sip.in | 72 +++++++++++++++++ python/core/core_auto.sip | 1 + src/core/qgis.h | 20 +++++ src/core/raster/qgsrasterrendererregistry.cpp | 16 +++- src/core/raster/qgsrasterrendererregistry.h | 80 ++++++++++++++++--- tests/src/python/CMakeLists.txt | 1 + .../python/test_qgsrasterrendererregistry.py | 51 ++++++++++++ 13 files changed, 335 insertions(+), 14 deletions(-) create mode 100644 python/PyQt6/core/auto_generated/raster/qgsrasterrendererregistry.sip.in create mode 100644 python/core/auto_generated/raster/qgsrasterrendererregistry.sip.in create mode 100644 tests/src/python/test_qgsrasterrendererregistry.py diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index 9201ed375903..f0dfda08d7d2 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -1279,6 +1279,14 @@ Qgis.RasterRendererFlags.baseClass = Qgis RasterRendererFlags = Qgis # dirty hack since SIP seems to introduce the flags in module # monkey patching scoped based enum +Qgis.RasterRendererCapability.UsesMultipleBands.__doc__ = "The renderer utilizes multiple raster bands for color data (note that alpha bands are not considered for this capability)" +Qgis.RasterRendererCapability.__doc__ = "Raster renderer capabilities.\n\n.. versionadded:: 3.48\n\n" + '* ``UsesMultipleBands``: ' + Qgis.RasterRendererCapability.UsesMultipleBands.__doc__ +# -- +Qgis.RasterRendererCapability.baseClass = Qgis +Qgis.RasterRendererCapabilities = lambda flags=0: Qgis.RasterRendererCapability(flags) +Qgis.RasterRendererCapabilities.baseClass = Qgis +RasterRendererCapabilities = Qgis # dirty hack since SIP seems to introduce the flags in module +# monkey patching scoped based enum Qgis.RasterAttributeTableFieldUsage.Generic.__doc__ = "Field usage Generic" Qgis.RasterAttributeTableFieldUsage.PixelCount.__doc__ = "Field usage PixelCount" Qgis.RasterAttributeTableFieldUsage.Name.__doc__ = "Field usage Name" diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index 2557d927de08..eadc3ec4f531 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -691,6 +691,14 @@ The development version + enum class RasterRendererCapability /BaseType=IntFlag/ + { + UsesMultipleBands, + }; + + typedef QFlags RasterRendererCapabilities; + + enum class RasterAttributeTableFieldUsage /BaseType=IntEnum/ { Generic, @@ -2859,6 +2867,8 @@ QFlags operator|(Qgis::ProjectReadFlag f1, QFlags operator|(Qgis::RasterRendererFlag f1, QFlags f2); +QFlags operator|(Qgis::RasterRendererCapability f1, QFlags f2); + QFlags operator|(Qgis::RasterTemporalCapabilityFlag f1, QFlags f2); QFlags operator|(Qgis::RelationshipCapability f1, QFlags f2); diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterrendererregistry.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterrendererregistry.sip.in new file mode 100644 index 000000000000..eeb73ec78d79 --- /dev/null +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterrendererregistry.sip.in @@ -0,0 +1,72 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrasterrendererregistry.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsRasterRendererRegistry +{ +%Docstring(signature="appended") +Registry for raster renderers. + +:py:class:`QgsRasterRendererRegistry` is not usually directly created, but rather accessed through +:py:func:`QgsApplication.rasterRendererRegistry()`. + +.. note:: + + Exposed to Python bindings in QGIS 3.38 +%End + +%TypeHeaderCode +#include "qgsrasterrendererregistry.h" +%End + public: + + QgsRasterRendererRegistry(); +%Docstring +Constructor for QgsRasterRendererRegistry. + +QgsRasterRendererRegistry is not usually directly created, but rather accessed through +:py:func:`QgsApplication.rasterRendererRegistry()`. + +The registry is pre-populated with standard raster renderers. +%End + + + + + QStringList renderersList() const; +%Docstring +Returns a list of the names of registered renderers. +%End + + + Qgis::RasterRendererCapabilities rendererCapabilities( const QString &rendererName ) const; +%Docstring +Returns the capabilities for the renderer with the specified name. + +.. versionadded:: 3.38 +%End + + QgsRasterRenderer *defaultRendererForDrawingStyle( Qgis::RasterDrawingStyle drawingStyle, QgsRasterDataProvider *provider ) const /Factory/; +%Docstring +Creates a default renderer for a raster drawing style (considering user options such as default contrast enhancement). +Caller takes ownership. +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrasterrendererregistry.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/PyQt6/core/core_auto.sip b/python/PyQt6/core/core_auto.sip index 0f1a068e200a..71eb28b16c0e 100644 --- a/python/PyQt6/core/core_auto.sip +++ b/python/PyQt6/core/core_auto.sip @@ -642,6 +642,7 @@ %Include auto_generated/raster/qgsrasterpyramid.sip %Include auto_generated/raster/qgsrasterrange.sip %Include auto_generated/raster/qgsrasterrenderer.sip +%Include auto_generated/raster/qgsrasterrendererregistry.sip %Include auto_generated/raster/qgsrasterrendererutils.sip %Include auto_generated/raster/qgsrasterresamplefilter.sip %Include auto_generated/raster/qgsrasterresampler.sip diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index 7481285da9d1..8de9a1a5d29e 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -1253,6 +1253,13 @@ Qgis.RasterRendererFlags.baseClass = Qgis RasterRendererFlags = Qgis # dirty hack since SIP seems to introduce the flags in module # monkey patching scoped based enum +Qgis.RasterRendererCapability.UsesMultipleBands.__doc__ = "The renderer utilizes multiple raster bands for color data (note that alpha bands are not considered for this capability)" +Qgis.RasterRendererCapability.__doc__ = "Raster renderer capabilities.\n\n.. versionadded:: 3.48\n\n" + '* ``UsesMultipleBands``: ' + Qgis.RasterRendererCapability.UsesMultipleBands.__doc__ +# -- +Qgis.RasterRendererCapability.baseClass = Qgis +Qgis.RasterRendererCapabilities.baseClass = Qgis +RasterRendererCapabilities = Qgis # dirty hack since SIP seems to introduce the flags in module +# monkey patching scoped based enum Qgis.RasterAttributeTableFieldUsage.Generic.__doc__ = "Field usage Generic" Qgis.RasterAttributeTableFieldUsage.PixelCount.__doc__ = "Field usage PixelCount" Qgis.RasterAttributeTableFieldUsage.Name.__doc__ = "Field usage Name" diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index cb1e8df0340c..830419e82227 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -691,6 +691,14 @@ The development version + enum class RasterRendererCapability + { + UsesMultipleBands, + }; + + typedef QFlags RasterRendererCapabilities; + + enum class RasterAttributeTableFieldUsage { Generic, @@ -2859,6 +2867,8 @@ QFlags operator|(Qgis::ProjectReadFlag f1, QFlags operator|(Qgis::RasterRendererFlag f1, QFlags f2); +QFlags operator|(Qgis::RasterRendererCapability f1, QFlags f2); + QFlags operator|(Qgis::RasterTemporalCapabilityFlag f1, QFlags f2); QFlags operator|(Qgis::RelationshipCapability f1, QFlags f2); diff --git a/python/core/auto_generated/raster/qgsrasterrendererregistry.sip.in b/python/core/auto_generated/raster/qgsrasterrendererregistry.sip.in new file mode 100644 index 000000000000..eeb73ec78d79 --- /dev/null +++ b/python/core/auto_generated/raster/qgsrasterrendererregistry.sip.in @@ -0,0 +1,72 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrasterrendererregistry.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsRasterRendererRegistry +{ +%Docstring(signature="appended") +Registry for raster renderers. + +:py:class:`QgsRasterRendererRegistry` is not usually directly created, but rather accessed through +:py:func:`QgsApplication.rasterRendererRegistry()`. + +.. note:: + + Exposed to Python bindings in QGIS 3.38 +%End + +%TypeHeaderCode +#include "qgsrasterrendererregistry.h" +%End + public: + + QgsRasterRendererRegistry(); +%Docstring +Constructor for QgsRasterRendererRegistry. + +QgsRasterRendererRegistry is not usually directly created, but rather accessed through +:py:func:`QgsApplication.rasterRendererRegistry()`. + +The registry is pre-populated with standard raster renderers. +%End + + + + + QStringList renderersList() const; +%Docstring +Returns a list of the names of registered renderers. +%End + + + Qgis::RasterRendererCapabilities rendererCapabilities( const QString &rendererName ) const; +%Docstring +Returns the capabilities for the renderer with the specified name. + +.. versionadded:: 3.38 +%End + + QgsRasterRenderer *defaultRendererForDrawingStyle( Qgis::RasterDrawingStyle drawingStyle, QgsRasterDataProvider *provider ) const /Factory/; +%Docstring +Creates a default renderer for a raster drawing style (considering user options such as default contrast enhancement). +Caller takes ownership. +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrasterrendererregistry.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 0f1a068e200a..71eb28b16c0e 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -642,6 +642,7 @@ %Include auto_generated/raster/qgsrasterpyramid.sip %Include auto_generated/raster/qgsrasterrange.sip %Include auto_generated/raster/qgsrasterrenderer.sip +%Include auto_generated/raster/qgsrasterrendererregistry.sip %Include auto_generated/raster/qgsrasterrendererutils.sip %Include auto_generated/raster/qgsrasterresamplefilter.sip %Include auto_generated/raster/qgsrasterresampler.sip diff --git a/src/core/qgis.h b/src/core/qgis.h index 7a690087ec91..2f2f10b7aa33 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -1163,6 +1163,25 @@ class CORE_EXPORT Qgis Q_ENUM( RasterRendererFlag ) Q_FLAG( RasterRendererFlags ) + /** + * Raster renderer capabilities. + * + * \since QGIS 3.48 + */ + enum class RasterRendererCapability : int SIP_ENUM_BASETYPE( IntFlag ) + { + UsesMultipleBands = 1 << 0, //!< The renderer utilizes multiple raster bands for color data (note that alpha bands are not considered for this capability) + }; + Q_ENUM( RasterRendererCapability ) + + /** + * Raster renderer capabilities. + * + * \since QGIS 3.38 + */ + Q_DECLARE_FLAGS( RasterRendererCapabilities, RasterRendererCapability ) + Q_FLAG( RasterRendererCapabilities ) + /** * \brief The RasterAttributeTableFieldUsage enum represents the usage of a Raster Attribute Table field. * \note Directly mapped from GDALRATFieldUsage enum values. @@ -4977,6 +4996,7 @@ Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::ProfileGeneratorFlags ) Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::ProjectCapabilities ) Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::ProjectReadFlags ) Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::RasterRendererFlags ) +Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::RasterRendererCapabilities ) Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::RasterTemporalCapabilityFlags ) Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::RelationshipCapabilities ) Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::RenderContextFlags ) diff --git a/src/core/raster/qgsrasterrendererregistry.cpp b/src/core/raster/qgsrasterrendererregistry.cpp index 7fdcca9b2291..772b362d5e28 100644 --- a/src/core/raster/qgsrasterrendererregistry.cpp +++ b/src/core/raster/qgsrasterrendererregistry.cpp @@ -35,9 +35,10 @@ QgsRasterRendererRegistryEntry::QgsRasterRendererRegistryEntry( const QString &name, const QString &visibleName, QgsRasterRendererCreateFunc rendererFunction, - QgsRasterRendererWidgetCreateFunc widgetFunction ) + QgsRasterRendererWidgetCreateFunc widgetFunction, Qgis::RasterRendererCapabilities capabilities ) : name( name ) , visibleName( visibleName ) + , capabilities( capabilities ) , rendererCreateFunction( rendererFunction ) , widgetCreateFunction( widgetFunction ) { @@ -52,7 +53,8 @@ QgsRasterRendererRegistry::QgsRasterRendererRegistry() { // insert items in a particular order, which is returned in renderersList() insert( QgsRasterRendererRegistryEntry( QStringLiteral( "multibandcolor" ), QObject::tr( "Multiband color" ), - QgsMultiBandColorRenderer::create, nullptr ) ); + QgsMultiBandColorRenderer::create, nullptr, + Qgis::RasterRendererCapability::UsesMultipleBands ) ); insert( QgsRasterRendererRegistryEntry( QStringLiteral( "paletted" ), QObject::tr( "Paletted/Unique values" ), QgsPalettedRasterRenderer::create, nullptr ) ); insert( QgsRasterRendererRegistryEntry( QStringLiteral( "singlebandgray" ), QObject::tr( "Singleband gray" ), QgsSingleBandGrayRenderer::create, nullptr ) ); @@ -109,6 +111,16 @@ QList< QgsRasterRendererRegistryEntry > QgsRasterRendererRegistry::entries() con return result; } +Qgis::RasterRendererCapabilities QgsRasterRendererRegistry::rendererCapabilities( const QString &rendererName ) const +{ + const QHash< QString, QgsRasterRendererRegistryEntry >::const_iterator it = mEntries.constFind( rendererName ); + if ( it != mEntries.constEnd() ) + { + return it.value().capabilities; + } + return Qgis::RasterRendererCapabilities(); +} + QgsRasterRenderer *QgsRasterRendererRegistry::defaultRendererForDrawingStyle( Qgis::RasterDrawingStyle drawingStyle, QgsRasterDataProvider *provider ) const { if ( !provider || provider->bandCount() < 1 ) diff --git a/src/core/raster/qgsrasterrendererregistry.h b/src/core/raster/qgsrasterrendererregistry.h index 8230221d4402..424b2a945964 100644 --- a/src/core/raster/qgsrasterrendererregistry.h +++ b/src/core/raster/qgsrasterrendererregistry.h @@ -18,11 +18,6 @@ #ifndef QGSRASTERRENDERERREGISTRY_H #define QGSRASTERRENDERERREGISTRY_H - -#define SIP_NO_FILE - - - #include "qgis_core.h" #include "qgis.h" #include @@ -36,17 +31,26 @@ class QgsRasterRendererWidget; class QgsRasterDataProvider; class QgsRectangle; +#ifndef SIP_RUN typedef QgsRasterRenderer *( *QgsRasterRendererCreateFunc )( const QDomElement &, QgsRasterInterface *input ); typedef QgsRasterRendererWidget *( *QgsRasterRendererWidgetCreateFunc )( QgsRasterLayer *, const QgsRectangle &extent ); /** * \ingroup core * \brief Registry for raster renderer entries. + * + * \note Not available in Python bindings */ struct CORE_EXPORT QgsRasterRendererRegistryEntry { + + /** + * Constructor for QgsRasterRendererRegistryEntry. + * + * Since QGIS 3.38, the \a capabilities argument can be used to specify renderer capabilities. + */ QgsRasterRendererRegistryEntry( const QString &name, const QString &visibleName, QgsRasterRendererCreateFunc rendererFunction, - QgsRasterRendererWidgetCreateFunc widgetFunction ); + QgsRasterRendererWidgetCreateFunc widgetFunction, Qgis::RasterRendererCapabilities capabilities = Qgis::RasterRendererCapabilities() ); /** * Constructor for QgsRasterRendererRegistryEntry. @@ -54,11 +58,21 @@ struct CORE_EXPORT QgsRasterRendererRegistryEntry QgsRasterRendererRegistryEntry() = default; QString name; QString visibleName; //visible (and translatable) name + + /** + * Renderer capabilities. + * + * \since QGIS 3.38 + */ + Qgis::RasterRendererCapabilities capabilities; + QIcon icon(); QgsRasterRendererCreateFunc rendererCreateFunction = nullptr ; //pointer to create function QgsRasterRendererWidgetCreateFunc widgetCreateFunction = nullptr ; //pointer to create function for renderer widget }; +#endif + /** * \ingroup core * \brief Registry for raster renderers. @@ -66,25 +80,67 @@ struct CORE_EXPORT QgsRasterRendererRegistryEntry * QgsRasterRendererRegistry is not usually directly created, but rather accessed through * QgsApplication::rasterRendererRegistry(). * - * \note not available in Python bindings + * \note Exposed to Python bindings in QGIS 3.38 */ class CORE_EXPORT QgsRasterRendererRegistry { public: + /** + * Constructor for QgsRasterRendererRegistry. + * + * QgsRasterRendererRegistry is not usually directly created, but rather accessed through + * QgsApplication::rasterRendererRegistry(). + * + * The registry is pre-populated with standard raster renderers. + */ QgsRasterRendererRegistry(); - void insert( const QgsRasterRendererRegistryEntry &entry ); - void insertWidgetFunction( const QString &rendererName, QgsRasterRendererWidgetCreateFunc func ); - bool rendererData( const QString &rendererName, QgsRasterRendererRegistryEntry &data ) const; + /** + * Inserts a new \a entry into the registry. + * + * \note Not available in Python bindings + */ + void insert( const QgsRasterRendererRegistryEntry &entry ) SIP_SKIP; + + /** + * Sets the widget creation function for a renderer. + * + * \note Not available in Python bindings + */ + void insertWidgetFunction( const QString &rendererName, QgsRasterRendererWidgetCreateFunc func ) SIP_SKIP; + + /** + * Retrieves renderer data from the registry. + * + * \note Not available in Python bindings + */ + bool rendererData( const QString &rendererName, QgsRasterRendererRegistryEntry &data ) const SIP_SKIP; + + /** + * Returns a list of the names of registered renderers. + */ QStringList renderersList() const; - QList< QgsRasterRendererRegistryEntry > entries() const; + + /** + * Returns the list of registered renderers. + * + * \note Not available in Python bindings + */ + QList< QgsRasterRendererRegistryEntry > entries() const SIP_SKIP; + + /** + * Returns the capabilities for the renderer with the specified name. + * + * \since QGIS 3.38 + */ + Qgis::RasterRendererCapabilities rendererCapabilities( const QString &rendererName ) const; /** * Creates a default renderer for a raster drawing style (considering user options such as default contrast enhancement). * Caller takes ownership. */ - QgsRasterRenderer *defaultRendererForDrawingStyle( Qgis::RasterDrawingStyle drawingStyle, QgsRasterDataProvider *provider ) const; + QgsRasterRenderer *defaultRendererForDrawingStyle( Qgis::RasterDrawingStyle drawingStyle, QgsRasterDataProvider *provider ) const SIP_FACTORY; private: QHash< QString, QgsRasterRendererRegistryEntry > mEntries; diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 2943264d74ee..916a6c27b18a 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -264,6 +264,7 @@ ADD_PYTHON_TEST(PyQgsRasterColorRampShader test_qgsrastercolorrampshader.py) ADD_PYTHON_TEST(PyQgsRasterLineSymbolLayer test_qgsrasterlinesymbollayer.py) ADD_PYTHON_TEST(PyQgsRasterPipe test_qgsrasterpipe.py) ADD_PYTHON_TEST(PyQgsRasterRange test_qgsrasterrange.py) +ADD_PYTHON_TEST(PyQgsRasterRendererRegistry test_qgsrasterrendererregistry.py) ADD_PYTHON_TEST(PyQgsRasterRendererUtils test_qgsrasterrendererutils.py) ADD_PYTHON_TEST(PyQgsRasterResampler test_qgsrasterresampler.py) ADD_PYTHON_TEST(PyQgsRasterTransparency test_qgsrastertransparency.py) diff --git a/tests/src/python/test_qgsrasterrendererregistry.py b/tests/src/python/test_qgsrasterrendererregistry.py new file mode 100644 index 000000000000..b57ba82d8352 --- /dev/null +++ b/tests/src/python/test_qgsrasterrendererregistry.py @@ -0,0 +1,51 @@ +"""QGIS Unit tests for QgsRasterRendererRegistry + +.. note:: 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. +""" +from qgis.core import ( + Qgis, + QgsRasterRendererRegistry +) +import unittest +from qgis.testing import start_app, QgisTestCase + +from utilities import unitTestDataPath + +# Convenience instances in case you may need them +# not used in this test +start_app() +TEST_DATA_DIR = unitTestDataPath() + + +class TestQgsRasterRendererRegistry(QgisTestCase): + + def test_registered(self): + """ + Test that standard renderers are registered + """ + registry = QgsRasterRendererRegistry() + self.assertIn('multibandcolor', registry.renderersList()) + self.assertIn('singlebandgray', registry.renderersList()) + self.assertIn('singlebandpseudocolor', registry.renderersList()) + self.assertIn('singlebandcolordata', registry.renderersList()) + self.assertIn('hillshade', registry.renderersList()) + self.assertIn('paletted', registry.renderersList()) + self.assertIn('contour', registry.renderersList()) + + def test_capabilities(self): + """ + Test retrieving renderer capabilities + """ + registry = QgsRasterRendererRegistry() + self.assertFalse(registry.rendererCapabilities('not a renderer')) + self.assertEqual(registry.rendererCapabilities('multibandcolor'), + Qgis.RasterRendererCapability.UsesMultipleBands) + self.assertEqual(registry.rendererCapabilities('singlebandgray'), + Qgis.RasterRendererCapabilities()) + + +if __name__ == '__main__': + unittest.main() From 3eb14c28affcc0862686e77d136857ca40b3273d Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 18 Mar 2024 12:18:54 +1000 Subject: [PATCH 46/68] Don't allow using per-band elevation range mode when layer has a multi-band renderer set --- .../qgsrasterelevationpropertieswidget.cpp | 10 +- .../qgsrasterelevationpropertieswidgetbase.ui | 388 +++++++++--------- 2 files changed, 212 insertions(+), 186 deletions(-) diff --git a/src/app/raster/qgsrasterelevationpropertieswidget.cpp b/src/app/raster/qgsrasterelevationpropertieswidget.cpp index 1344b2e556c7..e2a4e203ab7a 100644 --- a/src/app/raster/qgsrasterelevationpropertieswidget.cpp +++ b/src/app/raster/qgsrasterelevationpropertieswidget.cpp @@ -21,6 +21,8 @@ #include "qgslinesymbol.h" #include "qgsfillsymbol.h" #include "qgsexpressionbuilderdialog.h" +#include "qgsrasterrendererregistry.h" +#include "qgsrasterrenderer.h" #include #include @@ -58,7 +60,7 @@ QgsRasterElevationPropertiesWidget::QgsRasterElevationPropertiesWidget( QgsRaste mElevationLimitSpinBox->setClearValue( mElevationLimitSpinBox->minimum(), tr( "Not set" ) ); // NOTE -- this doesn't work, there's something broken in QgsStackedWidget which breaks the height calculations - mPageFixedRangePerBand->setFixedHeight( QFontMetrics( font() ).height() * 15 ); + mWidgetFixedRangePerBand->setFixedHeight( QFontMetrics( font() ).height() * 15 ); mFixedRangePerBandModel = new QgsRasterBandFixedElevationRangeModel( this ); mBandElevationTable->verticalHeader()->setVisible( false ); @@ -171,6 +173,12 @@ void QgsRasterElevationPropertiesWidget::syncToLayer( QgsMapLayer *layer ) mBandElevationTable->horizontalHeader()->setSectionResizeMode( 1, QHeaderView::Stretch ); mBandElevationTable->horizontalHeader()->setSectionResizeMode( 2, QHeaderView::Stretch ); + if ( QgsApplication::rasterRendererRegistry()->rendererCapabilities( mLayer->renderer()->type() ) & Qgis::RasterRendererCapability::UsesMultipleBands ) + { + mWidgetFixedRangePerBand->hide(); + mFixedRangePerBandLabel->setText( tr( "This mode cannot be used with a multi-band renderer." ) ); + } + mStyleComboBox->setCurrentIndex( mStyleComboBox->findData( static_cast ( props->profileSymbology() ) ) ); switch ( props->profileSymbology() ) { diff --git a/src/ui/raster/qgsrasterelevationpropertieswidgetbase.ui b/src/ui/raster/qgsrasterelevationpropertieswidgetbase.ui index 0e264f880f22..41a4f041555b 100644 --- a/src/ui/raster/qgsrasterelevationpropertieswidgetbase.ui +++ b/src/ui/raster/qgsrasterelevationpropertieswidgetbase.ui @@ -32,6 +32,153 @@ 0 + + + + Profile Chart Appearance + + + + + + + 0 + 0 + + + + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Line style + + + + + + + + 0 + 0 + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + + + + + Limit + + + + + + + Fill style + + + + + + + 6 + + + -99999.000000000000000 + + + 99999.000000000000000 + + + + + + + + + + + Style + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Configuration + + + @@ -251,84 +398,8 @@ 0 - - - - - - - 0 - 0 - - - - <html><head/><body><p><span style=" font-weight:600;">Each band in the raster layer is associated with a fixed elevation range.</span></p><p>This mode can be used when a layer has elevation data exposed through different raster bands.</p></body></html> - - - true - - - - - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - ... - - - - :/images/themes/default/mIconExpression.svg:/images/themes/default/mIconExpression.svg - - - QToolButton::MenuButtonPopup - - - false - - - - - - - - - - - - - Profile Chart Appearance - - - - - - - 0 - 0 - - - - 0 - - - + + 0 @@ -341,120 +412,67 @@ 0 - - - - Line style - - - - - - - 0 - 0 - - - - - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - - - - - - - Limit - - - - - - - Fill style - - + - - - 6 - - - -99999.000000000000000 + + + 0 - - 99999.000000000000000 - - + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + ... + + + + :/images/themes/default/mIconExpression.svg:/images/themes/default/mIconExpression.svg + + + QToolButton::MenuButtonPopup + + + false + + + + - - - - - - Style - - - - - - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Configuration - + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">Each band in the raster layer is associated with a fixed elevation range.</span></p><p>This mode can be used when a layer has elevation data exposed through different raster bands.</p></body></html> + + + true + + + + + From a72c4bb0b3c4491e91e4325cde321368c59721bd Mon Sep 17 00:00:00 2001 From: Andrea Giudiceandrea Date: Mon, 18 Mar 2024 00:23:31 +0100 Subject: [PATCH 47/68] [layout] Fix hidden coverage layer --- src/core/layout/qgslayoutatlas.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index 10f44858a7b2..e22be880c634 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -119,7 +119,7 @@ bool QgsLayoutAtlas::readXml( const QDomElement &atlasElem, const QDomDocument & mFilterFeatures = atlasElem.attribute( QStringLiteral( "filterFeatures" ), QStringLiteral( "0" ) ).toInt(); mFilterExpression = atlasElem.attribute( QStringLiteral( "featureFilter" ) ); - mHideCoverage = atlasElem.attribute( QStringLiteral( "hideCoverage" ), QStringLiteral( "0" ) ).toInt(); + setHideCoverage( atlasElem.attribute( QStringLiteral( "hideCoverage" ), QStringLiteral( "0" ) ).toInt() ); emit toggled( mEnabled ); emit changed(); From 0bb3a26b20953539bd9c35048f9e3eeddc3a62cc Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 18 Mar 2024 13:53:10 +1000 Subject: [PATCH 48/68] Implement fixed elevation range for mesh layers Just like the equivalent mode for raster layers, this mode indicates that a fixed constant z range should be applied to the entire mesh layer. --- python/PyQt6/core/auto_additions/qgis.py | 6 + .../qgsmeshlayerelevationproperties.sip.in | 54 +++ python/PyQt6/core/auto_generated/qgis.sip.in | 6 + python/core/auto_additions/qgis.py | 6 + .../qgsmeshlayerelevationproperties.sip.in | 54 +++ python/core/auto_generated/qgis.sip.in | 6 + .../mesh/qgsmeshelevationpropertieswidget.cpp | 70 ++++ .../mesh/qgsmeshelevationpropertieswidget.h | 2 +- .../mesh/qgsmeshlayerelevationproperties.cpp | 99 +++++- .../mesh/qgsmeshlayerelevationproperties.h | 44 +++ src/core/mesh/qgsmeshlayerrenderer.cpp | 24 +- src/core/qgis.h | 12 + .../qgsmeshelevationpropertieswidgetbase.ui | 330 +++++++++++------- tests/src/python/CMakeLists.txt | 1 + .../test_qgsmeshlayerelevationproperties.py | 70 ++++ tests/src/python/test_qgsmeshlayerrenderer.py | 86 +++++ .../expected_elevation_no_filter.png | Bin 0 -> 471523 bytes ...xpected_fixed_elevation_range_excluded.png | Bin 0 -> 471523 bytes ...xpected_fixed_elevation_range_included.png | Bin 0 -> 471523 bytes 19 files changed, 734 insertions(+), 136 deletions(-) create mode 100644 tests/src/python/test_qgsmeshlayerrenderer.py create mode 100644 tests/testdata/control_images/mesh/expected_elevation_no_filter/expected_elevation_no_filter.png create mode 100644 tests/testdata/control_images/mesh/expected_fixed_elevation_range_excluded/expected_fixed_elevation_range_excluded.png create mode 100644 tests/testdata/control_images/mesh/expected_fixed_elevation_range_included/expected_fixed_elevation_range_included.png diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index f0dfda08d7d2..6d30d47c995c 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -3243,6 +3243,12 @@ # -- Qgis.RasterElevationMode.baseClass = Qgis # monkey patching scoped based enum +Qgis.MeshElevationMode.FixedElevationRange.__doc__ = "Layer has a fixed elevation range" +Qgis.MeshElevationMode.FromVertices.__doc__ = "Elevation should be taken from mesh vertices" +Qgis.MeshElevationMode.__doc__ = "Mesh layer elevation modes.\n\n.. versionadded:: 3.38\n\n" + '* ``FixedElevationRange``: ' + Qgis.MeshElevationMode.FixedElevationRange.__doc__ + '\n' + '* ``FromVertices``: ' + Qgis.MeshElevationMode.FromVertices.__doc__ +# -- +Qgis.MeshElevationMode.baseClass = Qgis +# monkey patching scoped based enum Qgis.NoConstraint = Qgis.BetweenLineConstraint.NoConstraint Qgis.NoConstraint.is_monkey_patched = True Qgis.BetweenLineConstraint.NoConstraint.__doc__ = "No additional constraint" diff --git a/python/PyQt6/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in b/python/PyQt6/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in index ddb9592ae4e2..7d6b65a46570 100644 --- a/python/PyQt6/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in +++ b/python/PyQt6/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in @@ -47,6 +47,60 @@ Constructor for QgsMeshLayerElevationProperties, with the specified ``parent`` o virtual bool showByDefaultInElevationProfilePlots() const; + Qgis::MeshElevationMode mode() const; +%Docstring +Returns the elevation mode. + +.. seealso:: :py:func:`setMode` + +.. versionadded:: 3.38 +%End + + void setMode( Qgis::MeshElevationMode mode ); +%Docstring +Sets the elevation ``mode``. + +.. seealso:: :py:func:`mode` + +.. versionadded:: 3.38 +%End + + QgsDoubleRange fixedRange() const; +%Docstring +Returns the fixed elevation range for the mesh. + +.. note:: + + This is only considered when :py:func:`~QgsMeshLayerElevationProperties.mode` is :py:class:`Qgis`.MeshElevationMode.FixedElevationRange. + +.. note:: + + When a fixed range is set any :py:func:`~QgsMeshLayerElevationProperties.zOffset` and :py:func:`~QgsMeshLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`setFixedRange` + +.. versionadded:: 3.38 +%End + + void setFixedRange( const QgsDoubleRange &range ); +%Docstring +Sets the fixed elevation ``range`` for the mesh. + +.. note:: + + This is only considered when :py:func:`~QgsMeshLayerElevationProperties.mode` is :py:class:`Qgis`.MeshElevationMode.FixedElevationRange. + +.. note:: + + When a fixed range is set any :py:func:`~QgsMeshLayerElevationProperties.zOffset` and :py:func:`~QgsMeshLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`fixedRange` + +.. versionadded:: 3.38 +%End + QgsLineSymbol *profileLineSymbol() const; %Docstring Returns the line symbol used to render the mesh profile in elevation profile plots. diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index eadc3ec4f531..22bb888448dd 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -1858,6 +1858,12 @@ The development version FixedRangePerBand, }; + enum class MeshElevationMode /BaseType=IntEnum/ + { + FixedElevationRange, + FromVertices + }; + enum class BetweenLineConstraint /BaseType=IntEnum/ { NoConstraint, diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index 8de9a1a5d29e..f383dff902b7 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -3188,6 +3188,12 @@ # -- Qgis.RasterElevationMode.baseClass = Qgis # monkey patching scoped based enum +Qgis.MeshElevationMode.FixedElevationRange.__doc__ = "Layer has a fixed elevation range" +Qgis.MeshElevationMode.FromVertices.__doc__ = "Elevation should be taken from mesh vertices" +Qgis.MeshElevationMode.__doc__ = "Mesh layer elevation modes.\n\n.. versionadded:: 3.38\n\n" + '* ``FixedElevationRange``: ' + Qgis.MeshElevationMode.FixedElevationRange.__doc__ + '\n' + '* ``FromVertices``: ' + Qgis.MeshElevationMode.FromVertices.__doc__ +# -- +Qgis.MeshElevationMode.baseClass = Qgis +# monkey patching scoped based enum Qgis.NoConstraint = Qgis.BetweenLineConstraint.NoConstraint Qgis.NoConstraint.is_monkey_patched = True Qgis.BetweenLineConstraint.NoConstraint.__doc__ = "No additional constraint" diff --git a/python/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in b/python/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in index ddb9592ae4e2..7d6b65a46570 100644 --- a/python/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in +++ b/python/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in @@ -47,6 +47,60 @@ Constructor for QgsMeshLayerElevationProperties, with the specified ``parent`` o virtual bool showByDefaultInElevationProfilePlots() const; + Qgis::MeshElevationMode mode() const; +%Docstring +Returns the elevation mode. + +.. seealso:: :py:func:`setMode` + +.. versionadded:: 3.38 +%End + + void setMode( Qgis::MeshElevationMode mode ); +%Docstring +Sets the elevation ``mode``. + +.. seealso:: :py:func:`mode` + +.. versionadded:: 3.38 +%End + + QgsDoubleRange fixedRange() const; +%Docstring +Returns the fixed elevation range for the mesh. + +.. note:: + + This is only considered when :py:func:`~QgsMeshLayerElevationProperties.mode` is :py:class:`Qgis`.MeshElevationMode.FixedElevationRange. + +.. note:: + + When a fixed range is set any :py:func:`~QgsMeshLayerElevationProperties.zOffset` and :py:func:`~QgsMeshLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`setFixedRange` + +.. versionadded:: 3.38 +%End + + void setFixedRange( const QgsDoubleRange &range ); +%Docstring +Sets the fixed elevation ``range`` for the mesh. + +.. note:: + + This is only considered when :py:func:`~QgsMeshLayerElevationProperties.mode` is :py:class:`Qgis`.MeshElevationMode.FixedElevationRange. + +.. note:: + + When a fixed range is set any :py:func:`~QgsMeshLayerElevationProperties.zOffset` and :py:func:`~QgsMeshLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`fixedRange` + +.. versionadded:: 3.38 +%End + QgsLineSymbol *profileLineSymbol() const; %Docstring Returns the line symbol used to render the mesh profile in elevation profile plots. diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index 830419e82227..f64199e3119c 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -1858,6 +1858,12 @@ The development version FixedRangePerBand, }; + enum class MeshElevationMode + { + FixedElevationRange, + FromVertices + }; + enum class BetweenLineConstraint { NoConstraint, diff --git a/src/app/mesh/qgsmeshelevationpropertieswidget.cpp b/src/app/mesh/qgsmeshelevationpropertieswidget.cpp index f794fd672e17..cd5ede8081b1 100644 --- a/src/app/mesh/qgsmeshelevationpropertieswidget.cpp +++ b/src/app/mesh/qgsmeshelevationpropertieswidget.cpp @@ -27,6 +27,17 @@ QgsMeshElevationPropertiesWidget::QgsMeshElevationPropertiesWidget( QgsMeshLayer setupUi( this ); setObjectName( QStringLiteral( "mOptsPage_Elevation" ) ); + mModeComboBox->addItem( tr( "From Vertices" ), QVariant::fromValue( Qgis::MeshElevationMode::FromVertices ) ); + mModeComboBox->addItem( tr( "Fixed Elevation Range" ), QVariant::fromValue( Qgis::MeshElevationMode::FixedElevationRange ) ); + + mLimitsComboBox->addItem( tr( "Include Lower and Upper" ), QVariant::fromValue( Qgis::RangeLimits::IncludeBoth ) ); + mLimitsComboBox->addItem( tr( "Include Lower, Exclude Upper" ), QVariant::fromValue( Qgis::RangeLimits::IncludeLowerExcludeUpper ) ); + mLimitsComboBox->addItem( tr( "Exclude Lower, Include Upper" ), QVariant::fromValue( Qgis::RangeLimits::ExcludeLowerIncludeUpper ) ); + mLimitsComboBox->addItem( tr( "Exclude Lower and Upper" ), QVariant::fromValue( Qgis::RangeLimits::ExcludeBoth ) ); + + mStackedWidget->setSizeMode( QgsStackedWidget::SizeMode::CurrentPageOnly ); + mSymbologyStackedWidget->setSizeMode( QgsStackedWidget::SizeMode::CurrentPageOnly ); + mOffsetZSpinBox->setClearValue( 0 ); mScaleZSpinBox->setClearValue( 1 ); mLineStyleButton->setSymbolType( Qgis::SymbolType::Line ); @@ -36,8 +47,14 @@ QgsMeshElevationPropertiesWidget::QgsMeshElevationPropertiesWidget( QgsMeshLayer mStyleComboBox->addItem( QgsApplication::getThemeIcon( QStringLiteral( "mIconSurfaceElevationFillAbove.svg" ) ), tr( "Fill Above" ), static_cast< int >( Qgis::ProfileSurfaceSymbology::FillAbove ) ); mElevationLimitSpinBox->setClearValue( mElevationLimitSpinBox->minimum(), tr( "Not set" ) ); + mFixedLowerSpinBox->setClearValueMode( QgsDoubleSpinBox::ClearValueMode::MinimumValue, tr( "Not set" ) ); + mFixedUpperSpinBox->setClearValueMode( QgsDoubleSpinBox::ClearValueMode::MinimumValue, tr( "Not set" ) ); + mFixedLowerSpinBox->clear(); + mFixedUpperSpinBox->clear(); + syncToLayer( layer ); + connect( mModeComboBox, qOverload( &QComboBox::currentIndexChanged ), this, &QgsMeshElevationPropertiesWidget::modeChanged ); connect( mOffsetZSpinBox, qOverload( &QDoubleSpinBox::valueChanged ), this, &QgsMeshElevationPropertiesWidget::onChanged ); connect( mScaleZSpinBox, qOverload( &QDoubleSpinBox::valueChanged ), this, &QgsMeshElevationPropertiesWidget::onChanged ); connect( mElevationLimitSpinBox, qOverload( &QDoubleSpinBox::valueChanged ), this, &QgsMeshElevationPropertiesWidget::onChanged ); @@ -70,12 +87,35 @@ void QgsMeshElevationPropertiesWidget::syncToLayer( QgsMapLayer *layer ) mBlockUpdates = true; const QgsMeshLayerElevationProperties *props = qgis::down_cast< const QgsMeshLayerElevationProperties * >( mLayer->elevationProperties() ); + + mModeComboBox->setCurrentIndex( mModeComboBox->findData( QVariant::fromValue( props->mode() ) ) ); + switch ( props->mode() ) + { + case Qgis::MeshElevationMode::FixedElevationRange: + mStackedWidget->setCurrentWidget( mPageFixedRange ); + break; + case Qgis::MeshElevationMode::FromVertices: + mStackedWidget->setCurrentWidget( mPageFromVertices ); + break; + } + mOffsetZSpinBox->setValue( props->zOffset() ); mScaleZSpinBox->setValue( props->zScale() ); if ( std::isnan( props->elevationLimit() ) ) mElevationLimitSpinBox->clear(); else mElevationLimitSpinBox->setValue( props->elevationLimit() ); + + if ( props->fixedRange().lower() != std::numeric_limits< double >::lowest() ) + mFixedLowerSpinBox->setValue( props->fixedRange().lower() ); + else + mFixedLowerSpinBox->clear(); + if ( props->fixedRange().upper() != std::numeric_limits< double >::max() ) + mFixedUpperSpinBox->setValue( props->fixedRange().upper() ); + else + mFixedUpperSpinBox->clear(); + mLimitsComboBox->setCurrentIndex( mLimitsComboBox->findData( QVariant::fromValue( props->fixedRange().rangeLimits() ) ) ); + mLineStyleButton->setSymbol( props->profileLineSymbol()->clone() ); mFillStyleButton->setSymbol( props->profileFillSymbol()->clone() ); @@ -100,18 +140,48 @@ void QgsMeshElevationPropertiesWidget::apply() return; QgsMeshLayerElevationProperties *props = qgis::down_cast< QgsMeshLayerElevationProperties * >( mLayer->elevationProperties() ); + props->setMode( mModeComboBox->currentData().value< Qgis::MeshElevationMode >() ); + props->setZOffset( mOffsetZSpinBox->value() ); props->setZScale( mScaleZSpinBox->value() ); if ( mElevationLimitSpinBox->value() != mElevationLimitSpinBox->clearValue() ) props->setElevationLimit( mElevationLimitSpinBox->value() ); else props->setElevationLimit( std::numeric_limits< double >::quiet_NaN() ); + + double fixedLower = std::numeric_limits< double >::lowest(); + double fixedUpper = std::numeric_limits< double >::max(); + if ( mFixedLowerSpinBox->value() != mFixedLowerSpinBox->clearValue() ) + fixedLower = mFixedLowerSpinBox->value(); + if ( mFixedUpperSpinBox->value() != mFixedUpperSpinBox->clearValue() ) + fixedUpper = mFixedUpperSpinBox->value(); + + props->setFixedRange( QgsDoubleRange( fixedLower, fixedUpper, mLimitsComboBox->currentData().value< Qgis::RangeLimits >() ) ); + props->setProfileLineSymbol( mLineStyleButton->clonedSymbol< QgsLineSymbol >() ); props->setProfileFillSymbol( mFillStyleButton->clonedSymbol< QgsFillSymbol >() ); props->setProfileSymbology( static_cast< Qgis::ProfileSurfaceSymbology >( mStyleComboBox->currentData().toInt() ) ); mLayer->trigger3DUpdate(); } +void QgsMeshElevationPropertiesWidget::modeChanged() +{ + if ( mModeComboBox->currentData().isValid() ) + { + switch ( mModeComboBox->currentData().value< Qgis::MeshElevationMode >() ) + { + case Qgis::MeshElevationMode::FixedElevationRange: + mStackedWidget->setCurrentWidget( mPageFixedRange ); + break; + case Qgis::MeshElevationMode::FromVertices: + mStackedWidget->setCurrentWidget( mPageFromVertices ); + break; + } + } + + onChanged(); +} + void QgsMeshElevationPropertiesWidget::onChanged() { if ( !mBlockUpdates ) diff --git a/src/app/mesh/qgsmeshelevationpropertieswidget.h b/src/app/mesh/qgsmeshelevationpropertieswidget.h index 7c99980111a1..c55e406e7d4e 100644 --- a/src/app/mesh/qgsmeshelevationpropertieswidget.h +++ b/src/app/mesh/qgsmeshelevationpropertieswidget.h @@ -36,7 +36,7 @@ class QgsMeshElevationPropertiesWidget : public QgsMapLayerConfigWidget, private void apply() override; private slots: - + void modeChanged(); void onChanged(); private: diff --git a/src/core/mesh/qgsmeshlayerelevationproperties.cpp b/src/core/mesh/qgsmeshlayerelevationproperties.cpp index 214a0c489386..7515e37ef4be 100644 --- a/src/core/mesh/qgsmeshlayerelevationproperties.cpp +++ b/src/core/mesh/qgsmeshlayerelevationproperties.cpp @@ -43,12 +43,26 @@ bool QgsMeshLayerElevationProperties::hasElevation() const QDomElement QgsMeshLayerElevationProperties::writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) { QDomElement element = document.createElement( QStringLiteral( "elevation" ) ); + element.setAttribute( QStringLiteral( "mode" ), qgsEnumValueToKey( mMode ) ); element.setAttribute( QStringLiteral( "symbology" ), qgsEnumValueToKey( mSymbology ) ); if ( !std::isnan( mElevationLimit ) ) element.setAttribute( QStringLiteral( "elevationLimit" ), qgsDoubleToString( mElevationLimit ) ); writeCommonProperties( element, document, context ); + switch ( mMode ) + { + case Qgis::MeshElevationMode::FixedElevationRange: + element.setAttribute( QStringLiteral( "lower" ), qgsDoubleToString( mFixedRange.lower() ) ); + element.setAttribute( QStringLiteral( "upper" ), qgsDoubleToString( mFixedRange.upper() ) ); + element.setAttribute( QStringLiteral( "includeLower" ), mFixedRange.includeLower() ? "1" : "0" ); + element.setAttribute( QStringLiteral( "includeUpper" ), mFixedRange.includeUpper() ? "1" : "0" ); + break; + + case Qgis::MeshElevationMode::FromVertices: + break; + } + QDomElement profileLineSymbolElement = document.createElement( QStringLiteral( "profileLineSymbol" ) ); profileLineSymbolElement.appendChild( QgsSymbolLayerUtils::saveSymbol( QString(), mProfileLineSymbol.get(), document, context ) ); element.appendChild( profileLineSymbolElement ); @@ -64,6 +78,7 @@ QDomElement QgsMeshLayerElevationProperties::writeXml( QDomElement &parentElemen bool QgsMeshLayerElevationProperties::readXml( const QDomElement &element, const QgsReadWriteContext &context ) { const QDomElement elevationElement = element.firstChildElement( QStringLiteral( "elevation" ) ).toElement(); + mMode = qgsEnumKeyToValue( elevationElement.attribute( QStringLiteral( "mode" ) ), Qgis::MeshElevationMode::FromVertices ); mSymbology = qgsEnumKeyToValue( elevationElement.attribute( QStringLiteral( "symbology" ) ), Qgis::ProfileSurfaceSymbology::Line ); if ( elevationElement.hasAttribute( QStringLiteral( "elevationLimit" ) ) ) mElevationLimit = elevationElement.attribute( QStringLiteral( "elevationLimit" ) ).toDouble(); @@ -72,6 +87,21 @@ bool QgsMeshLayerElevationProperties::readXml( const QDomElement &element, const readCommonProperties( elevationElement, context ); + switch ( mMode ) + { + case Qgis::MeshElevationMode::FixedElevationRange: + { + const double lower = elevationElement.attribute( QStringLiteral( "lower" ) ).toDouble(); + const double upper = elevationElement.attribute( QStringLiteral( "upper" ) ).toDouble(); + const bool includeLower = elevationElement.attribute( QStringLiteral( "includeLower" ) ).toInt(); + const bool includeUpper = elevationElement.attribute( QStringLiteral( "includeUpper" ) ).toInt(); + mFixedRange = QgsDoubleRange( lower, upper, includeLower, includeUpper ); + break; + } + case Qgis::MeshElevationMode::FromVertices: + break; + } + const QColor defaultColor = QgsApplication::colorSchemeRegistry()->fetchRandomStyleColor(); const QDomElement profileLineSymbolElement = elevationElement.firstChildElement( QStringLiteral( "profileLineSymbol" ) ).firstChildElement( QStringLiteral( "symbol" ) ); @@ -90,32 +120,59 @@ bool QgsMeshLayerElevationProperties::readXml( const QDomElement &element, const QString QgsMeshLayerElevationProperties::htmlSummary() const { QStringList properties; - properties << tr( "Scale: %1" ).arg( mZScale ); - properties << tr( "Offset: %1" ).arg( mZOffset ); + switch ( mMode ) + { + case Qgis::MeshElevationMode::FixedElevationRange: + properties << tr( "Elevation range: %1 to %2" ).arg( mFixedRange.lower() ).arg( mFixedRange.upper() ); + break; + + case Qgis::MeshElevationMode::FromVertices: + properties << tr( "Scale: %1" ).arg( mZScale ); + properties << tr( "Offset: %1" ).arg( mZOffset ); + break; + } return QStringLiteral( "
  • %1
  • " ).arg( properties.join( QLatin1String( "
  • " ) ) ); } QgsMeshLayerElevationProperties *QgsMeshLayerElevationProperties::clone() const { std::unique_ptr< QgsMeshLayerElevationProperties > res = std::make_unique< QgsMeshLayerElevationProperties >( nullptr ); + res->setMode( mMode ); res->setProfileLineSymbol( mProfileLineSymbol->clone() ); res->setProfileFillSymbol( mProfileFillSymbol->clone() ); res->setProfileSymbology( mSymbology ); res->setElevationLimit( mElevationLimit ); + res->setFixedRange( mFixedRange ); res->copyCommonProperties( this ); return res.release(); } -bool QgsMeshLayerElevationProperties::isVisibleInZRange( const QgsDoubleRange & ) const +bool QgsMeshLayerElevationProperties::isVisibleInZRange( const QgsDoubleRange &range ) const { - // TODO -- test actual raster z range - return true; + switch ( mMode ) + { + case Qgis::MeshElevationMode::FixedElevationRange: + return mFixedRange.overlaps( range ); + + case Qgis::MeshElevationMode::FromVertices: + // TODO -- test actual mesh z range + return true; + } + BUILTIN_UNREACHABLE } QgsDoubleRange QgsMeshLayerElevationProperties::calculateZRange( QgsMapLayer * ) const { - // TODO -- determine actual z range from raster statistics - return QgsDoubleRange(); + switch ( mMode ) + { + case Qgis::MeshElevationMode::FixedElevationRange: + return mFixedRange; + + case Qgis::MeshElevationMode::FromVertices: + // TODO -- determine actual z range from mesh statistics + return QgsDoubleRange(); + } + BUILTIN_UNREACHABLE } bool QgsMeshLayerElevationProperties::showByDefaultInElevationProfilePlots() const @@ -123,6 +180,34 @@ bool QgsMeshLayerElevationProperties::showByDefaultInElevationProfilePlots() con return true; } +Qgis::MeshElevationMode QgsMeshLayerElevationProperties::mode() const +{ + return mMode; +} + +void QgsMeshLayerElevationProperties::setMode( Qgis::MeshElevationMode mode ) +{ + if ( mMode == mode ) + return; + + mMode = mode; + emit changed(); +} + +QgsDoubleRange QgsMeshLayerElevationProperties::fixedRange() const +{ + return mFixedRange; +} + +void QgsMeshLayerElevationProperties::setFixedRange( const QgsDoubleRange &range ) +{ + if ( range == mFixedRange ) + return; + + mFixedRange = range; + emit changed(); +} + QgsLineSymbol *QgsMeshLayerElevationProperties::profileLineSymbol() const { return mProfileLineSymbol.get(); diff --git a/src/core/mesh/qgsmeshlayerelevationproperties.h b/src/core/mesh/qgsmeshlayerelevationproperties.h index d3a8f3e298d1..5618afb61743 100644 --- a/src/core/mesh/qgsmeshlayerelevationproperties.h +++ b/src/core/mesh/qgsmeshlayerelevationproperties.h @@ -56,6 +56,46 @@ class CORE_EXPORT QgsMeshLayerElevationProperties : public QgsMapLayerElevationP QgsDoubleRange calculateZRange( QgsMapLayer *layer ) const override; bool showByDefaultInElevationProfilePlots() const override; + /** + * Returns the elevation mode. + * + * \see setMode() + * \since QGIS 3.38 + */ + Qgis::MeshElevationMode mode() const; + + /** + * Sets the elevation \a mode. + * + * \see mode() + * \since QGIS 3.38 + */ + void setMode( Qgis::MeshElevationMode mode ); + + /** + * Returns the fixed elevation range for the mesh. + * + * \note This is only considered when mode() is Qgis::MeshElevationMode::FixedElevationRange. + * + * \note When a fixed range is set any zOffset() and zScale() is ignored. + * + * \see setFixedRange() + * \since QGIS 3.38 + */ + QgsDoubleRange fixedRange() const; + + /** + * Sets the fixed elevation \a range for the mesh. + * + * \note This is only considered when mode() is Qgis::MeshElevationMode::FixedElevationRange. + * + * \note When a fixed range is set any zOffset() and zScale() is ignored. + * + * \see fixedRange() + * \since QGIS 3.38 + */ + void setFixedRange( const QgsDoubleRange &range ); + /** * Returns the line symbol used to render the mesh profile in elevation profile plots. * @@ -131,10 +171,14 @@ class CORE_EXPORT QgsMeshLayerElevationProperties : public QgsMapLayerElevationP void setDefaultProfileLineSymbol( const QColor &color ); void setDefaultProfileFillSymbol( const QColor &color ); + Qgis::MeshElevationMode mMode = Qgis::MeshElevationMode::FromVertices; + std::unique_ptr< QgsLineSymbol > mProfileLineSymbol; std::unique_ptr< QgsFillSymbol > mProfileFillSymbol; Qgis::ProfileSurfaceSymbology mSymbology = Qgis::ProfileSurfaceSymbology::Line; double mElevationLimit = std::numeric_limits< double >::quiet_NaN(); + + QgsDoubleRange mFixedRange; }; #endif // QGSMESHLAYERELEVATIONPROPERTIES_H diff --git a/src/core/mesh/qgsmeshlayerrenderer.cpp b/src/core/mesh/qgsmeshlayerrenderer.cpp index aa20021e9df4..58d327272c77 100644 --- a/src/core/mesh/qgsmeshlayerrenderer.cpp +++ b/src/core/mesh/qgsmeshlayerrenderer.cpp @@ -42,6 +42,7 @@ #include "qgsapplication.h" #include "qgsruntimeprofiler.h" #include "qgsexpressioncontextutils.h" +#include "qgsmeshlayerelevationproperties.h" QgsMeshLayerRenderer::QgsMeshLayerRenderer( QgsMeshLayer *layer, @@ -89,9 +90,28 @@ QgsMeshLayerRenderer::QgsMeshLayerRenderer( if ( layer->elevationProperties() && layer->elevationProperties()->hasElevation() ) { + QgsMeshLayerElevationProperties *elevProp = qobject_cast( layer->elevationProperties() ); + mRenderElevationMap = true; - mElevationScale = layer->elevationProperties()->zScale(); - mElevationOffset = layer->elevationProperties()->zOffset(); + mElevationScale = elevProp->zScale(); + mElevationOffset = elevProp->zOffset(); + + if ( !context.zRange().isInfinite() ) + { + switch ( elevProp->mode() ) + { + case Qgis::MeshElevationMode::FixedElevationRange: + // don't need to handle anything here -- the layer renderer will never be created if the + // render context range doesn't match the layer's fixed elevation range + break; + + case Qgis::MeshElevationMode::FromVertices: + { + // TODO -- filtering by mesh z values is not currently implemented + break; + } + } + } } mPreparationTime = timer.elapsed(); diff --git a/src/core/qgis.h b/src/core/qgis.h index 2f2f10b7aa33..81d30b230c34 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -3280,6 +3280,18 @@ class CORE_EXPORT Qgis }; Q_ENUM( RasterElevationMode ) + /** + * Mesh layer elevation modes. + * + * \since QGIS 3.38 + */ + enum class MeshElevationMode : int + { + FixedElevationRange = 0, //!< Layer has a fixed elevation range + FromVertices = 1 //!< Elevation should be taken from mesh vertices + }; + Q_ENUM( MeshElevationMode ) + /** * Between line constraints which can be enabled * diff --git a/src/ui/mesh/qgsmeshelevationpropertieswidgetbase.ui b/src/ui/mesh/qgsmeshelevationpropertieswidgetbase.ui index 0d2f420a2a95..5dbb914e0da0 100644 --- a/src/ui/mesh/qgsmeshelevationpropertieswidgetbase.ui +++ b/src/ui/mesh/qgsmeshelevationpropertieswidgetbase.ui @@ -6,14 +6,14 @@ 0 0 - 435 - 407 + 515 + 424 Raster Elevation Properties - + 0 @@ -26,83 +26,189 @@ 0 - - - - Qt::StrongFocus + + + + Configuration - - Elevation Surface + + + + + + + + + + 0 + 0 + - - false + + QFrame::NoFrame - - vectorgeneral + + 1 - - - - - 6 - - - -99999999999.000000000000000 - - - 99999999999.000000000000000 - - - - - - - Offset - - - - - - - Scale - - - - - - - 6 - - - 0.000000000000000 - - - 99999999999.000000000000000 - - - 1.000000000000000 - - - - - - - <html><head/><body><p><span style=" font-weight:600;">Elevation scaling and offset can be used to manually correct elevation values from the layer.</span></p><p>The scale is applied to the mesh values before adding the offset.</p></body></html> - - - true - - - - + + + + 0 + 0 + + + + + + + Scale + + + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">The elevation will be taken from the mesh vertices.</span></p><p>Elevation scaling and offset can be used to manually correct elevation values from the layer. The scale is applied to the raster values before adding the offset.</p></body></html> + + + true + + + + + + + 6 + + + -99999999999.000000000000000 + + + 99999999999.000000000000000 + + + + + + + Offset + + + + + + + 6 + + + 0.000000000000000 + + + 99999999999.000000000000000 + + + 1.000000000000000 + + + + + + + + + 0 + 0 + + + + false + + + + + + Lower + + + + + + + + + + Upper + + + + + + + Limits + + + + + + + 4 + + + -9999999998.000000000000000 + + + 9999999999.000000000000000 + + + + + + + 4 + + + -9999999998.000000000000000 + + + 9999999999.000000000000000 + + + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">The mesh layer is associated with a fixed elevation range.</span></p><p>This mode can be used when a layer has a single fixed elevation, or a range (slice) of elevation values. If a range is specified, mesh values will be extruded over this range.</p></body></html> + + + true + + + + + - + Profile Chart Appearance + + + @@ -110,11 +216,8 @@ - - - - + 0 @@ -122,7 +225,7 @@ - 1 + 0 @@ -138,6 +241,13 @@ 0 + + + + Line style + + + @@ -151,26 +261,6 @@ - - - - Line style - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - @@ -187,13 +277,6 @@ 0 - - - - Fill style - - - @@ -201,16 +284,10 @@ - - - - - 0 - 0 - - + + - + Fill style @@ -227,18 +304,18 @@ - - - - Qt::Vertical + + + + + 0 + 0 + - - - 20 - 40 - + + - + @@ -247,7 +324,7 @@ - + Qt::Vertical @@ -273,12 +350,13 @@ QToolButton
    qgssymbolbutton.h
    + + QgsStackedWidget + QStackedWidget +
    qgsstackedwidget.h
    + 1 +
    - - mElevationGroupBox - mScaleZSpinBox - mOffsetZSpinBox - diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 916a6c27b18a..beba0d91bac8 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -176,6 +176,7 @@ ADD_PYTHON_TEST(PyQgsMarkerLineSymbolLayer test_qgsmarkerlinesymbollayer.py) ADD_PYTHON_TEST(PyQgsMatrix4x4 test_qgsmatrix4x4.py) ADD_PYTHON_TEST(PyQgsMergedFeatureRenderer test_qgsmergedfeaturerenderer.py) ADD_PYTHON_TEST(PyQgsMeshLayerElevationProperties test_qgsmeshlayerelevationproperties.py) +ADD_PYTHON_TEST(PyQgsMeshLayerRenderer test_qgsmeshlayerrenderer.py) ADD_PYTHON_TEST(PyQgsMessageLog test_qgsmessagelog.py) ADD_PYTHON_TEST(PyQgsMetadataBase test_qgsmetadatabase.py) ADD_PYTHON_TEST(PyQgsMetadataUtils test_qgsmetadatautils.py) diff --git a/tests/src/python/test_qgsmeshlayerelevationproperties.py b/tests/src/python/test_qgsmeshlayerelevationproperties.py index 214f06079aac..0243a429a00d 100644 --- a/tests/src/python/test_qgsmeshlayerelevationproperties.py +++ b/tests/src/python/test_qgsmeshlayerelevationproperties.py @@ -16,6 +16,7 @@ QgsLineSymbol, QgsMeshLayerElevationProperties, QgsReadWriteContext, + QgsDoubleRange ) import unittest from qgis.testing import start_app, QgisTestCase @@ -27,9 +28,13 @@ class TestQgsMeshLayerElevationProperties(QgisTestCase): def testBasic(self): props = QgsMeshLayerElevationProperties(None) + self.assertEqual(props.mode(), + Qgis.MeshElevationMode.FromVertices) + self.assertEqual(props.zScale(), 1) self.assertEqual(props.zOffset(), 0) self.assertTrue(props.hasElevation()) + self.assertTrue(props.fixedRange().isInfinite()) self.assertIsInstance(props.profileLineSymbol(), QgsLineSymbol) self.assertIsInstance(props.profileFillSymbol(), QgsFillSymbol) self.assertEqual(props.profileSymbology(), Qgis.ProfileSurfaceSymbology.Line) @@ -58,6 +63,8 @@ def testBasic(self): props2 = QgsMeshLayerElevationProperties(None) props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.mode(), + Qgis.MeshElevationMode.FromVertices) self.assertEqual(props2.zScale(), 2) self.assertEqual(props2.zOffset(), 0.5) self.assertEqual(props2.profileLineSymbol().color().name(), '#ff4433') @@ -66,6 +73,8 @@ def testBasic(self): self.assertEqual(props2.elevationLimit(), 909) props2 = props.clone() + self.assertEqual(props2.mode(), + Qgis.MeshElevationMode.FromVertices) self.assertEqual(props2.zScale(), 2) self.assertEqual(props2.zOffset(), 0.5) self.assertEqual(props2.profileLineSymbol().color().name(), '#ff4433') @@ -73,6 +82,67 @@ def testBasic(self): self.assertEqual(props2.profileSymbology(), Qgis.ProfileSurfaceSymbology.FillBelow) self.assertEqual(props2.elevationLimit(), 909) + def test_basic_fixed_range(self): + """ + Basic tests for the class using the FixedElevationRange mode + """ + props = QgsMeshLayerElevationProperties(None) + self.assertTrue(props.fixedRange().isInfinite()) + + props.setMode(Qgis.MeshElevationMode.FixedElevationRange) + props.setFixedRange(QgsDoubleRange(103.1, 106.8)) + # fixed ranges should not be affected by scale/offset + props.setZOffset(0.5) + props.setZScale(2) + self.assertEqual(props.fixedRange(), QgsDoubleRange(103.1, 106.8)) + self.assertEqual(props.calculateZRange(None), + QgsDoubleRange(103.1, 106.8)) + self.assertFalse(props.isVisibleInZRange(QgsDoubleRange(3.1, 6.8))) + self.assertTrue(props.isVisibleInZRange(QgsDoubleRange(3.1, 104.8))) + self.assertTrue(props.isVisibleInZRange(QgsDoubleRange(104.8, 114.8))) + self.assertFalse(props.isVisibleInZRange(QgsDoubleRange(114.8, 124.8))) + + doc = QDomDocument("testdoc") + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsMeshLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.mode(), + Qgis.MeshElevationMode.FixedElevationRange) + self.assertEqual(props2.fixedRange(), QgsDoubleRange(103.1, 106.8)) + + props2 = props.clone() + self.assertEqual(props2.mode(), + Qgis.MeshElevationMode.FixedElevationRange) + self.assertEqual(props2.fixedRange(), QgsDoubleRange(103.1, 106.8)) + + # include lower, exclude upper + props.setFixedRange(QgsDoubleRange(103.1, 106.8, + includeLower=True, + includeUpper=False)) + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsMeshLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.fixedRange(), QgsDoubleRange(103.1, 106.8, + includeLower=True, + includeUpper=False)) + + # exclude lower, include upper + props.setFixedRange(QgsDoubleRange(103.1, 106.8, + includeLower=False, + includeUpper=True)) + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsMeshLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.fixedRange(), QgsDoubleRange(103.1, 106.8, + includeLower=False, + includeUpper=True)) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgsmeshlayerrenderer.py b/tests/src/python/test_qgsmeshlayerrenderer.py new file mode 100644 index 000000000000..b8e44a14afe1 --- /dev/null +++ b/tests/src/python/test_qgsmeshlayerrenderer.py @@ -0,0 +1,86 @@ +"""QGIS Unit tests for QgsMeshLayerRenderer. + +.. note:: 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. +""" + +import os +import unittest + +from qgis.PyQt.QtCore import QSize +from qgis.core import ( + Qgis, + QgsDoubleRange, + QgsMapSettings, + QgsMeshLayer +) +from qgis.testing import start_app, QgisTestCase + +from utilities import unitTestDataPath + +start_app() + + +class TestQgsMeshLayerLabeling(QgisTestCase): + + @classmethod + def control_path_prefix(cls): + return "mesh" + + def test_render_fixed_elevation_range_with_z_range_filter(self): + """ + Test rendering a mesh with a fixed elevation range when + map settings has a z range filtrer + """ + mesh_layer = QgsMeshLayer( + os.path.join(unitTestDataPath(), 'mesh', 'quad_flower.2dm'), + 'mdal', 'mdal') + self.assertTrue(mesh_layer.isValid()) + + # set layer as elevation enabled + mesh_layer.elevationProperties().setMode( + Qgis.MeshElevationMode.FixedElevationRange + ) + mesh_layer.elevationProperties().setFixedRange( + QgsDoubleRange(33, 38) + ) + + map_settings = QgsMapSettings() + map_settings.setOutputSize(QSize(400, 400)) + map_settings.setOutputDpi(96) + map_settings.setDestinationCrs(mesh_layer.crs()) + map_settings.setExtent(mesh_layer.extent()) + map_settings.setLayers([mesh_layer]) + + # no filter on map settings + map_settings.setZRange(QgsDoubleRange()) + self.assertTrue( + self.render_map_settings_check( + 'No Z range filter on map settings, fixed elevation range layer', + 'elevation_no_filter', + map_settings) + ) + + # map settings range includes layer's range + map_settings.setZRange(QgsDoubleRange(30, 35)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings includes layers fixed range', + 'fixed_elevation_range_included', + map_settings) + ) + + # map settings range excludes layer's range + map_settings.setZRange(QgsDoubleRange(130, 135)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings outside of layers fixed range', + 'fixed_elevation_range_excluded', + map_settings) + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testdata/control_images/mesh/expected_elevation_no_filter/expected_elevation_no_filter.png b/tests/testdata/control_images/mesh/expected_elevation_no_filter/expected_elevation_no_filter.png new file mode 100644 index 0000000000000000000000000000000000000000..b5c0414fd914ba31b2624dc4987fbfe58456dd6f GIT binary patch literal 471523 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%0D&L`*r!2gi5ZI_iQyPHN<-zcz;l=Y0gDKnF>v7b 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 literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/mesh/expected_fixed_elevation_range_included/expected_fixed_elevation_range_included.png b/tests/testdata/control_images/mesh/expected_fixed_elevation_range_included/expected_fixed_elevation_range_included.png new file mode 100644 index 0000000000000000000000000000000000000000..b5c0414fd914ba31b2624dc4987fbfe58456dd6f GIT binary patch literal 471523 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%0D&L`*r!2gi5ZI_iQyPHN<-zcz;l=Y0gDKnF>v7b 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 Date: Mon, 18 Mar 2024 14:05:57 +1000 Subject: [PATCH 49/68] Don't identify mesh layers which aren't in visible elevation range --- src/gui/qgsmaptoolidentify.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/gui/qgsmaptoolidentify.cpp b/src/gui/qgsmaptoolidentify.cpp index 67d4ae5062ce..20345706613d 100644 --- a/src/gui/qgsmaptoolidentify.cpp +++ b/src/gui/qgsmaptoolidentify.cpp @@ -278,6 +278,12 @@ bool QgsMapToolIdentify::identifyMeshLayer( QListelevationProperties()->isVisibleInZRange( identifyContext.zRange() ) ) + return false; + } + double searchRadius = mOverrideCanvasSearchRadius < 0 ? searchRadiusMU( mCanvas ) : mOverrideCanvasSearchRadius; bool isTemporal = identifyContext.isTemporal() && layer->temporalProperties()->isActive(); From 40847d05960bb593ed3c19a78a73af4dbd7e3b1e Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 18 Mar 2024 14:20:48 +1000 Subject: [PATCH 50/68] Avoid some unnecessary redraws --- .../mesh/qgsmeshlayerelevationproperties.sip.in | 2 ++ .../qgsrasterlayerelevationproperties.sip.in | 2 ++ .../mesh/qgsmeshlayerelevationproperties.sip.in | 2 ++ .../qgsrasterlayerelevationproperties.sip.in | 2 ++ src/core/mesh/qgsmeshlayerelevationproperties.cpp | 13 +++++++++++++ src/core/mesh/qgsmeshlayerelevationproperties.h | 1 + .../raster/qgsrasterlayerelevationproperties.cpp | 15 +++++++++++++++ .../raster/qgsrasterlayerelevationproperties.h | 1 + 8 files changed, 38 insertions(+) diff --git a/python/PyQt6/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in b/python/PyQt6/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in index 7d6b65a46570..824657e07627 100644 --- a/python/PyQt6/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in +++ b/python/PyQt6/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in @@ -46,6 +46,8 @@ Constructor for QgsMeshLayerElevationProperties, with the specified ``parent`` o virtual bool showByDefaultInElevationProfilePlots() const; + virtual QgsMapLayerElevationProperties::Flags flags() const; + Qgis::MeshElevationMode mode() const; %Docstring diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in index 63fcc4f888a6..b44823b25354 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in @@ -46,6 +46,8 @@ Constructor for QgsRasterLayerElevationProperties, with the specified ``parent`` virtual bool showByDefaultInElevationProfilePlots() const; + virtual QgsMapLayerElevationProperties::Flags flags() const; + bool isEnabled() const; %Docstring diff --git a/python/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in b/python/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in index 7d6b65a46570..824657e07627 100644 --- a/python/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in +++ b/python/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in @@ -46,6 +46,8 @@ Constructor for QgsMeshLayerElevationProperties, with the specified ``parent`` o virtual bool showByDefaultInElevationProfilePlots() const; + virtual QgsMapLayerElevationProperties::Flags flags() const; + Qgis::MeshElevationMode mode() const; %Docstring diff --git a/python/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in b/python/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in index 63fcc4f888a6..b44823b25354 100644 --- a/python/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in +++ b/python/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in @@ -46,6 +46,8 @@ Constructor for QgsRasterLayerElevationProperties, with the specified ``parent`` virtual bool showByDefaultInElevationProfilePlots() const; + virtual QgsMapLayerElevationProperties::Flags flags() const; + bool isEnabled() const; %Docstring diff --git a/src/core/mesh/qgsmeshlayerelevationproperties.cpp b/src/core/mesh/qgsmeshlayerelevationproperties.cpp index 7515e37ef4be..3bc226397a60 100644 --- a/src/core/mesh/qgsmeshlayerelevationproperties.cpp +++ b/src/core/mesh/qgsmeshlayerelevationproperties.cpp @@ -180,6 +180,19 @@ bool QgsMeshLayerElevationProperties::showByDefaultInElevationProfilePlots() con return true; } +QgsMapLayerElevationProperties::Flags QgsMeshLayerElevationProperties::flags() const +{ + switch ( mMode ) + { + case Qgis::MeshElevationMode::FixedElevationRange: + return QgsMapLayerElevationProperties::Flag::FlagDontInvalidateCachedRendersWhenRangeChanges; + + case Qgis::MeshElevationMode::FromVertices: + break; + } + return QgsMapLayerElevationProperties::Flags(); +} + Qgis::MeshElevationMode QgsMeshLayerElevationProperties::mode() const { return mMode; diff --git a/src/core/mesh/qgsmeshlayerelevationproperties.h b/src/core/mesh/qgsmeshlayerelevationproperties.h index 5618afb61743..f862aea3a669 100644 --- a/src/core/mesh/qgsmeshlayerelevationproperties.h +++ b/src/core/mesh/qgsmeshlayerelevationproperties.h @@ -55,6 +55,7 @@ class CORE_EXPORT QgsMeshLayerElevationProperties : public QgsMapLayerElevationP bool isVisibleInZRange( const QgsDoubleRange &range ) const override; QgsDoubleRange calculateZRange( QgsMapLayer *layer ) const override; bool showByDefaultInElevationProfilePlots() const override; + QgsMapLayerElevationProperties::Flags flags() const override; /** * Returns the elevation mode. diff --git a/src/core/raster/qgsrasterlayerelevationproperties.cpp b/src/core/raster/qgsrasterlayerelevationproperties.cpp index 74e4ed8e0697..5bb42feca8f6 100644 --- a/src/core/raster/qgsrasterlayerelevationproperties.cpp +++ b/src/core/raster/qgsrasterlayerelevationproperties.cpp @@ -274,6 +274,21 @@ bool QgsRasterLayerElevationProperties::showByDefaultInElevationProfilePlots() c return mEnabled; } +QgsMapLayerElevationProperties::Flags QgsRasterLayerElevationProperties::flags() const +{ + if ( mEnabled ) + { + switch ( mMode ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + return QgsMapLayerElevationProperties::Flag::FlagDontInvalidateCachedRendersWhenRangeChanges; + case Qgis::RasterElevationMode::RepresentsElevationSurface: + break; + } + } + return QgsMapLayerElevationProperties::Flags(); +} + void QgsRasterLayerElevationProperties::setEnabled( bool enabled ) { if ( enabled == mEnabled ) diff --git a/src/core/raster/qgsrasterlayerelevationproperties.h b/src/core/raster/qgsrasterlayerelevationproperties.h index 2b27e9fd7e2b..d52f691e2be6 100644 --- a/src/core/raster/qgsrasterlayerelevationproperties.h +++ b/src/core/raster/qgsrasterlayerelevationproperties.h @@ -54,6 +54,7 @@ class CORE_EXPORT QgsRasterLayerElevationProperties : public QgsMapLayerElevatio bool isVisibleInZRange( const QgsDoubleRange &range ) const override; QgsDoubleRange calculateZRange( QgsMapLayer *layer ) const override; bool showByDefaultInElevationProfilePlots() const override; + QgsMapLayerElevationProperties::Flags flags() const override; /** * Returns TRUE if the elevation properties are enabled, i.e. the raster layer values represent an elevation surface. From 163c513496d825c4e741bc7706c20ba5eb318c03 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 19 Mar 2024 08:48:42 +1000 Subject: [PATCH 51/68] Fix warning --- src/core/raster/qgsrasterlayerelevationproperties.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/raster/qgsrasterlayerelevationproperties.cpp b/src/core/raster/qgsrasterlayerelevationproperties.cpp index 5bb42feca8f6..181be5e7292d 100644 --- a/src/core/raster/qgsrasterlayerelevationproperties.cpp +++ b/src/core/raster/qgsrasterlayerelevationproperties.cpp @@ -282,7 +282,9 @@ QgsMapLayerElevationProperties::Flags QgsRasterLayerElevationProperties::flags() { case Qgis::RasterElevationMode::FixedElevationRange: return QgsMapLayerElevationProperties::Flag::FlagDontInvalidateCachedRendersWhenRangeChanges; + case Qgis::RasterElevationMode::RepresentsElevationSurface: + case Qgis::RasterElevationMode::FixedRangePerBand: break; } } From d2e3534dbd852fa3006b1199369dbf5c8bb740f0 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 8 Mar 2024 10:39:16 +1000 Subject: [PATCH 52/68] [processing] Use correct ellipsoid for network analysis tools Use the processing context's ellipsoid instead of a hardcoded WGS84 ellipsoid for distance calculations during network analysis, so that the lengths used will exactly match other measurement tools used on the same features in the same project. --- .../tests/testdata/qgis_algorithm_tests2.yaml | 12 ++++++++++++ .../processing/qgsalgorithmnetworkanalysisbase.cpp | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests2.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests2.yaml index 7c45de6d2531..7005e7b50f2c 100644 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests2.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests2.yaml @@ -1823,6 +1823,7 @@ tests: - algorithm: native:shortestpathpointtopoint name: Shortest path (point to point, shortest route) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 5.0 @@ -1853,6 +1854,7 @@ tests: - algorithm: native:shortestpathpointtopoint name: Shortest path (point to point, fastest route) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 5.0 @@ -1884,6 +1886,7 @@ tests: - algorithm: native:shortestpathlayertopoint name: Shortest path layer to point + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 5.0 @@ -1914,6 +1917,7 @@ tests: - algorithm: native:shortestpathpointtolayer name: Shortest path point to layer + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 5.0 @@ -1944,6 +1948,7 @@ tests: - algorithm: native:serviceareafrompoint name: Service area from point (shortest, nodes) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 50.0 @@ -1975,6 +1980,7 @@ tests: - algorithm: native:serviceareafrompoint name: Service area from point (shortest, nodes, bounds) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 50.0 @@ -2001,6 +2007,7 @@ tests: - algorithm: native:serviceareafrompoint name: Service area from point (shortest, lines) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 50.0 @@ -2032,6 +2039,7 @@ tests: - algorithm: native:serviceareafrompoint name: Service area from point (fastest, old parameter) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 50.0 @@ -2066,6 +2074,7 @@ tests: - algorithm: native:serviceareafrompoint name: Service area from point (fastest, new parameter) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 50.0 @@ -2100,6 +2109,7 @@ tests: - algorithm: native:serviceareafromlayer name: Service area from layer (shortest, nodes) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 50.0 @@ -2135,6 +2145,7 @@ tests: - algorithm: native:serviceareafromlayer name: Service area from layer (shortest, nodes, boundary) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 50.0 @@ -2162,6 +2173,7 @@ tests: - algorithm: native:serviceareafromlayer name: Service area from layer (shortest, lines) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 50.0 diff --git a/src/analysis/processing/qgsalgorithmnetworkanalysisbase.cpp b/src/analysis/processing/qgsalgorithmnetworkanalysisbase.cpp index bc956f9d41eb..43dbbdc1b4d7 100644 --- a/src/analysis/processing/qgsalgorithmnetworkanalysisbase.cpp +++ b/src/analysis/processing/qgsalgorithmnetworkanalysisbase.cpp @@ -133,7 +133,7 @@ void QgsNetworkAnalysisAlgorithmBase::loadCommonParams( const QVariantMap ¶m mDirector->addStrategy( new QgsNetworkDistanceStrategy() ); } - mBuilder = std::make_unique< QgsGraphBuilder >( mNetwork->sourceCrs(), true, tolerance ); + mBuilder = std::make_unique< QgsGraphBuilder >( mNetwork->sourceCrs(), true, tolerance, context.ellipsoid() ); } void QgsNetworkAnalysisAlgorithmBase::loadPoints( QgsFeatureSource *source, QVector< QgsPointXY > &points, QHash< int, QgsAttributes > &attributes, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) From c997d5a8d9453d052c415ced24d10ff4cbea7de5 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 14 Mar 2024 09:10:10 +1000 Subject: [PATCH 53/68] Fix test --- python/plugins/processing/tests/AlgorithmsTestBase.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/python/plugins/processing/tests/AlgorithmsTestBase.py b/python/plugins/processing/tests/AlgorithmsTestBase.py index 5dbdbfd58134..270c4b864e57 100644 --- a/python/plugins/processing/tests/AlgorithmsTestBase.py +++ b/python/plugins/processing/tests/AlgorithmsTestBase.py @@ -167,6 +167,12 @@ def check_algorithm(self, name, defs): # ignore user setting for invalid geometry handling context = QgsProcessingContext() context.setProject(QgsProject.instance()) + if 'ellipsoid' in defs: + # depending on the project settings, we can't always rely + # on QgsProject.ellipsoid() returning the same ellipsoid as was + # specified in the test definition. So just force ensure that the + # context's ellipsoid is the desired one + context.setEllipsoid(defs['ellipsoid']) if 'skipInvalid' in defs and defs['skipInvalid']: context.setInvalidGeometryCheck(QgsFeatureRequest.InvalidGeometryCheck.GeometrySkipInvalid) From 1815858d93b9d34e135ed55f147445eaf311bc6b Mon Sep 17 00:00:00 2001 From: DelazJ Date: Tue, 19 Mar 2024 14:26:27 +0100 Subject: [PATCH 54/68] Relation reference: Simplify widget for limiting number of entries to display --- .../qgsrelationreferenceconfigdlg.cpp | 5 +- .../qgsrelationreferenceconfigdlgbase.ui | 51 ++++++++++--------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/gui/editorwidgets/qgsrelationreferenceconfigdlg.cpp b/src/gui/editorwidgets/qgsrelationreferenceconfigdlg.cpp index d1259f6f874e..40e5f2621032 100644 --- a/src/gui/editorwidgets/qgsrelationreferenceconfigdlg.cpp +++ b/src/gui/editorwidgets/qgsrelationreferenceconfigdlg.cpp @@ -70,6 +70,7 @@ QgsRelationReferenceConfigDlg::QgsRelationReferenceConfigDlg( QgsVectorLayer *vl connect( mExpressionWidget, static_cast( &QgsFieldExpressionWidget::fieldChanged ), this, &QgsEditorConfigWidget::changed ); connect( mEditExpression, &QAbstractButton::clicked, this, &QgsRelationReferenceConfigDlg::mEditExpression_clicked ); connect( mFilterExpression, &QTextEdit::textChanged, this, &QgsEditorConfigWidget::changed ); + connect( mFetchLimitCheckBox, &QCheckBox::toggled, mFetchLimit, &QSpinBox::setEnabled ); } void QgsRelationReferenceConfigDlg::mEditExpression_clicked() @@ -113,7 +114,7 @@ void QgsRelationReferenceConfigDlg::setConfig( const QVariantMap &config ) mCbxMapIdentification->setChecked( config.value( QStringLiteral( "MapIdentification" ), false ).toBool() ); mCbxAllowAddFeatures->setChecked( config.value( QStringLiteral( "AllowAddFeatures" ), false ).toBool() ); mCbxReadOnly->setChecked( config.value( QStringLiteral( "ReadOnly" ), false ).toBool() ); - mFetchLimitGroupBox->setChecked( config.value( QStringLiteral( "FetchLimitActive" ), QgsSettings().value( QStringLiteral( "maxEntriesRelationWidget" ), 100, QgsSettings::Gui ).toInt() > 0 ).toBool() ); + mFetchLimitCheckBox->setChecked( config.value( QStringLiteral( "FetchLimitActive" ), QgsSettings().value( QStringLiteral( "maxEntriesRelationWidget" ), 100, QgsSettings::Gui ).toInt() > 0 ).toBool() ); mFetchLimit->setValue( config.value( QStringLiteral( "FetchLimitNumber" ), QgsSettings().value( QStringLiteral( "maxEntriesRelationWidget" ), 100, QgsSettings::Gui ) ).toInt() ); mFilterExpression->setPlainText( config.value( QStringLiteral( "FilterExpression" ) ).toString() ); @@ -175,7 +176,7 @@ QVariantMap QgsRelationReferenceConfigDlg::config() myConfig.insert( QStringLiteral( "ReadOnly" ), mCbxReadOnly->isChecked() ); myConfig.insert( QStringLiteral( "Relation" ), mComboRelation->currentData() ); myConfig.insert( QStringLiteral( "AllowAddFeatures" ), mCbxAllowAddFeatures->isChecked() ); - myConfig.insert( QStringLiteral( "FetchLimitActive" ), mFetchLimitGroupBox->isChecked() ); + myConfig.insert( QStringLiteral( "FetchLimitActive" ), mFetchLimitCheckBox->isChecked() ); myConfig.insert( QStringLiteral( "FetchLimitNumber" ), mFetchLimit->value() ); if ( mFilterGroupBox->isChecked() ) diff --git a/src/ui/editorwidgets/qgsrelationreferenceconfigdlgbase.ui b/src/ui/editorwidgets/qgsrelationreferenceconfigdlgbase.ui index 70335b5bf60e..87c0a5980407 100644 --- a/src/ui/editorwidgets/qgsrelationreferenceconfigdlgbase.ui +++ b/src/ui/editorwidgets/qgsrelationreferenceconfigdlgbase.ui @@ -194,29 +194,31 @@
    - - - If no limit is set, all entries are loaded. - - - Limit number of entries - - - true - - - - - - Maximum number of entries - - - - - - - - + + + + + If unchecked, all the entries are loaded. + + + Limit number of entries to + + + true + + + + + + + + 0 + 0 + + + + + @@ -262,6 +264,8 @@ mCbxMapIdentification mCbxReadOnly mCbxAllowAddFeatures + mFetchLimitCheckBox + mFetchLimit mFilterGroupBox mAvailableFieldsList mAddFilterButton @@ -273,7 +277,6 @@ - From 18bba4b5933f8cd552cda9434ef93222271a1bc1 Mon Sep 17 00:00:00 2001 From: Harrissou Sant-anna Date: Tue, 19 Mar 2024 11:31:49 +0100 Subject: [PATCH 55/68] Use proper labels for buttons in the Organize columns dialog These buttons actually check or uncheck visibility of the fields, they do not (de)select them in the dialog --- src/ui/qgsorganizetablecolumnsdialog.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/qgsorganizetablecolumnsdialog.ui b/src/ui/qgsorganizetablecolumnsdialog.ui index 74ad8bca7d3f..ee67a1aa587e 100644 --- a/src/ui/qgsorganizetablecolumnsdialog.ui +++ b/src/ui/qgsorganizetablecolumnsdialog.ui @@ -43,7 +43,7 @@ - Deselect All + Hide All @@ -60,7 +60,7 @@ - Select All + Show All From 1ecd12284500e346b3e130ff98958b7175a4c6d6 Mon Sep 17 00:00:00 2001 From: Harrissou Sant-anna Date: Tue, 19 Mar 2024 11:47:26 +0100 Subject: [PATCH 56/68] Add tooltips --- src/ui/qgsorganizetablecolumnsdialog.ui | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/ui/qgsorganizetablecolumnsdialog.ui b/src/ui/qgsorganizetablecolumnsdialog.ui index ee67a1aa587e..03a7e0dd38c7 100644 --- a/src/ui/qgsorganizetablecolumnsdialog.ui +++ b/src/ui/qgsorganizetablecolumnsdialog.ui @@ -45,6 +45,9 @@ Hide All + + Hides all the fields and actions in the table + @@ -62,6 +65,9 @@ Show All + + Displays all the fields and actions in the table + @@ -69,6 +75,9 @@ Toggle Selection + + Toggles visibility of the selected fields and actions + From 988012c9997f23a82c761580a8dfa793557989f2 Mon Sep 17 00:00:00 2001 From: Andrea Giudiceandrea Date: Tue, 19 Mar 2024 07:09:06 +0100 Subject: [PATCH 57/68] [layouts] Avoid a potential speed regression Followup #56891 --- src/core/layout/qgslayoutatlas.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index e22be880c634..438f431af847 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -119,7 +119,8 @@ bool QgsLayoutAtlas::readXml( const QDomElement &atlasElem, const QDomDocument & mFilterFeatures = atlasElem.attribute( QStringLiteral( "filterFeatures" ), QStringLiteral( "0" ) ).toInt(); mFilterExpression = atlasElem.attribute( QStringLiteral( "featureFilter" ) ); - setHideCoverage( atlasElem.attribute( QStringLiteral( "hideCoverage" ), QStringLiteral( "0" ) ).toInt() ); + mHideCoverage = atlasElem.attribute( QStringLiteral( "hideCoverage" ), QStringLiteral( "0" ) ).toInt(); + mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagHideCoverageLayer, mHideCoverage ); emit toggled( mEnabled ); emit changed(); From c9c62c2da1c8359013c1b1847b9dfdbb97f9029c Mon Sep 17 00:00:00 2001 From: Andrea Giudiceandrea Date: Mon, 11 Mar 2024 07:04:55 +0100 Subject: [PATCH 58/68] Fix QgsSymbol::drawPreviewIcon Transform the "Opacity" value calculated by Data Defined Override expression from [0-100] range to [0-1] range. --- src/core/symbology/qgssymbol.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/symbology/qgssymbol.cpp b/src/core/symbology/qgssymbol.cpp index a667ce287645..7616af8fb6e0 100644 --- a/src/core/symbology/qgssymbol.cpp +++ b/src/core/symbology/qgssymbol.cpp @@ -943,7 +943,7 @@ void QgsSymbol::drawPreviewIcon( QPainter *painter, QSize size, QgsRenderContext const bool prevForceVector = context->forceVectorOutput(); context->setForceVectorOutput( true ); - const double opacity = expressionContext ? dataDefinedProperties().valueAsDouble( QgsSymbol::Property::Opacity, *expressionContext, mOpacity ) : mOpacity; + const double opacity = expressionContext ? dataDefinedProperties().valueAsDouble( QgsSymbol::Property::Opacity, *expressionContext, mOpacity * 100 ) * 0.01 : mOpacity; QgsSymbolRenderContext symbolContext( *context, Qgis::RenderUnit::Unknown, opacity, false, mRenderHints, nullptr ); symbolContext.setSelected( selected ); From 89731a78ef89fc14b7bc3f54e98cc4e0fc0d53c8 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 19 Mar 2024 12:36:44 +1000 Subject: [PATCH 59/68] Don't store numeric limits as z range for layout maps in project --- src/core/layout/qgslayoutitemmap.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/layout/qgslayoutitemmap.cpp b/src/core/layout/qgslayoutitemmap.cpp index 74b9c3a113f4..ac8e60108801 100644 --- a/src/core/layout/qgslayoutitemmap.cpp +++ b/src/core/layout/qgslayoutitemmap.cpp @@ -835,8 +835,10 @@ bool QgsLayoutItemMap::writePropertiesToElement( QDomElement &mapElem, QDomDocum } mapElem.setAttribute( QStringLiteral( "enableZRange" ), mZRangeEnabled ? 1 : 0 ); - mapElem.setAttribute( QStringLiteral( "zRangeLower" ), qgsDoubleToString( mZRange.lower() ) ); - mapElem.setAttribute( QStringLiteral( "zRangeUpper" ), qgsDoubleToString( mZRange.upper() ) ); + if ( mZRange.lower() != std::numeric_limits< double >::lowest() ) + mapElem.setAttribute( QStringLiteral( "zRangeLower" ), qgsDoubleToString( mZRange.lower() ) ); + if ( mZRange.upper() != std::numeric_limits< double >::max() ) + mapElem.setAttribute( QStringLiteral( "zRangeUpper" ), qgsDoubleToString( mZRange.upper() ) ); mAtlasClippingSettings->writeXml( mapElem, doc, context ); mItemClippingSettings->writeXml( mapElem, doc, context ); From 0f1649266bdd794407c7c922b78f78a086677121 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 20 Mar 2024 10:15:44 +1000 Subject: [PATCH 60/68] Use more descriptive default layer names when adding SensorThings layers from browser panel Fixes #56838 --- .../sensorthings/qgssensorthingsdataitems.cpp | 76 +++++++++++++++---- .../sensorthings/qgssensorthingsdataitems.h | 17 ++++- 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/src/core/providers/sensorthings/qgssensorthingsdataitems.cpp b/src/core/providers/sensorthings/qgssensorthingsdataitems.cpp index 807bc9f83d19..2c9cfe152399 100644 --- a/src/core/providers/sensorthings/qgssensorthingsdataitems.cpp +++ b/src/core/providers/sensorthings/qgssensorthingsdataitems.cpp @@ -96,18 +96,16 @@ QVector QgsSensorThingsConnectionItem::createChildren() children.append( new QgsSensorThingsEntityContainerItem( this, QgsSensorThingsUtils::displayString( entity, true ), mPath + '/' + qgsEnumValueToKey( entity ), - QgsProviderRegistry::instance()->encodeUri( - QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY, entityUriParts ) ) ); + entityUriParts, entity, mConnName ) ); } else { children.append( new QgsSensorThingsLayerEntityItem( this, QgsSensorThingsUtils::displayString( entity, true ), mPath + '/' + qgsEnumValueToKey( entity ), - QgsProviderRegistry::instance()->encodeUri( - QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY, entityUriParts ), + entityUriParts, QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY, - Qgis::BrowserLayerType::TableLayer ) ); + Qgis::BrowserLayerType::TableLayer, entity, mConnName ) ); } } @@ -119,9 +117,11 @@ QVector QgsSensorThingsConnectionItem::createChildren() // QgsSensorThingsEntityContainerItem // -QgsSensorThingsEntityContainerItem::QgsSensorThingsEntityContainerItem( QgsDataItem *parent, const QString &name, const QString &path, const QString &entityUri ) +QgsSensorThingsEntityContainerItem::QgsSensorThingsEntityContainerItem( QgsDataItem *parent, const QString &name, const QString &path, const QVariantMap &entityUriParts, Qgis::SensorThingsEntity entityType, const QString &connectionName ) : QgsDataCollectionItem( parent, name, path, QStringLiteral( "sensorthings" ) ) - , mEntityUri( entityUri ) + , mEntityUriParts( entityUriParts ) + , mEntityType( entityType ) + , mConnectionName( connectionName ) { mCapabilities |= Qgis::BrowserItemCapability::Collapse | Qgis::BrowserItemCapability::Fast; populate(); @@ -137,9 +137,6 @@ QVector QgsSensorThingsEntityContainerItem::createChildren() { QVector children; - const QVariantMap entityUriParts = QgsProviderRegistry::instance()->decodeUri( - QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY, mEntityUri ); - int sortKey = 1; for ( const Qgis::WkbType wkbType : { @@ -149,7 +146,7 @@ QVector QgsSensorThingsEntityContainerItem::createChildren() Qgis::WkbType::MultiPolygon } ) { - QVariantMap geometryUriParts = entityUriParts; + QVariantMap geometryUriParts = mEntityUriParts; QString name; Qgis::BrowserLayerType layerType = Qgis::BrowserLayerType::TableLayer; switch ( wkbType ) @@ -180,10 +177,9 @@ QVector QgsSensorThingsEntityContainerItem::createChildren() children.append( new QgsSensorThingsLayerEntityItem( this, name, mPath + '/' + name, - QgsProviderRegistry::instance()->encodeUri( - QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY, geometryUriParts ), + geometryUriParts, QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY, - layerType ) ); + layerType, mEntityType, mConnectionName ) ); children.last()->setSortKey( sortKey++ ); } @@ -194,12 +190,60 @@ QVector QgsSensorThingsEntityContainerItem::createChildren() // QgsSensorThingsLayerEntityItem // -QgsSensorThingsLayerEntityItem::QgsSensorThingsLayerEntityItem( QgsDataItem *parent, QString name, QString path, const QString &encodedUri, const QString &provider, Qgis::BrowserLayerType type ) - : QgsLayerItem( parent, name, path, encodedUri, type, provider ) +QgsSensorThingsLayerEntityItem::QgsSensorThingsLayerEntityItem( QgsDataItem *parent, const QString &name, const QString &path, + const QVariantMap &uriParts, const QString &provider, Qgis::BrowserLayerType type, Qgis::SensorThingsEntity entityType, const QString &connectionName ) + : QgsLayerItem( parent, name, path, + QgsProviderRegistry::instance()->encodeUri( QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY, uriParts ), + type, provider ) + , mUriParts( uriParts ) + , mEntityType( entityType ) + , mConnectionName( connectionName ) { setState( Qgis::BrowserItemState::Populated ); } +QString QgsSensorThingsLayerEntityItem::layerName() const +{ + QString baseName; + if ( QgsSensorThingsUtils::entityTypeHasGeometry( mEntityType ) ) + { + const QString geometryType = mUriParts.value( QStringLiteral( "geometryType" ) ).toString(); + QString geometryNamePart; + if ( geometryType.compare( QLatin1String( "point" ), Qt::CaseInsensitive ) == 0 || + geometryType.compare( QLatin1String( "multipoint" ), Qt::CaseInsensitive ) == 0 ) + { + geometryNamePart = tr( "Points" ); + } + else if ( geometryType.compare( QLatin1String( "line" ), Qt::CaseInsensitive ) == 0 ) + { + geometryNamePart = tr( "Lines" ); + } + else if ( geometryType.compare( QLatin1String( "polygon" ), Qt::CaseInsensitive ) == 0 ) + { + geometryNamePart = tr( "Polygons" ); + } + + if ( !geometryNamePart.isEmpty() ) + { + baseName = QStringLiteral( "%1 - %2 (%3)" ).arg( mConnectionName, + QgsSensorThingsUtils::displayString( mEntityType, true ), + geometryNamePart ); + } + else + { + baseName = QStringLiteral( "%1 - %2" ).arg( mConnectionName, + QgsSensorThingsUtils::displayString( mEntityType, true ) ); + } + } + else + { + baseName = QStringLiteral( "%1 - %2" ).arg( mConnectionName, + QgsSensorThingsUtils::displayString( mEntityType, true ) ); + } + + return baseName; +} + // // QgsSensorThingsDataItemProvider // diff --git a/src/core/providers/sensorthings/qgssensorthingsdataitems.h b/src/core/providers/sensorthings/qgssensorthingsdataitems.h index 62dfdb34f7a8..fd4db697d751 100644 --- a/src/core/providers/sensorthings/qgssensorthingsdataitems.h +++ b/src/core/providers/sensorthings/qgssensorthingsdataitems.h @@ -52,11 +52,14 @@ class CORE_EXPORT QgsSensorThingsEntityContainerItem : public QgsDataCollectionI { Q_OBJECT public: - QgsSensorThingsEntityContainerItem( QgsDataItem *parent, const QString &name, const QString &path, const QString &entityUri ); + QgsSensorThingsEntityContainerItem( QgsDataItem *parent, const QString &name, const QString &path, const QVariantMap &entityUriParts, + Qgis::SensorThingsEntity entityType, const QString &connectionName ); bool equal( const QgsDataItem *other ) override; QVector createChildren() override; private: - QString mEntityUri; + QVariantMap mEntityUriParts; + Qgis::SensorThingsEntity mEntityType = Qgis::SensorThingsEntity::Invalid; + QString mConnectionName; }; @@ -64,8 +67,14 @@ class CORE_EXPORT QgsSensorThingsLayerEntityItem : public QgsLayerItem { Q_OBJECT public: - QgsSensorThingsLayerEntityItem( QgsDataItem *parent, QString name, QString path, const QString &encodedUri, const QString &provider, Qgis::BrowserLayerType type ); - + QgsSensorThingsLayerEntityItem( QgsDataItem *parent, const QString &name, const QString &path, + const QVariantMap &uriParts, const QString &provider, Qgis::BrowserLayerType type, + Qgis::SensorThingsEntity entityType, const QString &connectionName ); + QString layerName() const final; + private: + QVariantMap mUriParts; + Qgis::SensorThingsEntity mEntityType = Qgis::SensorThingsEntity::Invalid; + QString mConnectionName; }; //! Provider for sensor things root data item From 84f37095bcfa2b65ede7eb34a8c88636f504fa9b Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 20 Mar 2024 14:17:00 +1000 Subject: [PATCH 61/68] Fix typo --- .../processing/tests/testdata/qgis_algorithm_tests3.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml index c0ef9a7f3bfc..3310b15a1f83 100644 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml @@ -284,7 +284,7 @@ tests: pk: id - algorithm: native:voronoipolygons - name: Voronoi without source atributes + name: Voronoi without source attributes params: BUFFER: 0.0 COPY_ATTRIBUTES: false From 4d2497be8a9f50a0e76ee865e192bef8b2b91169 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Mon, 18 Mar 2024 14:23:04 +0700 Subject: [PATCH 62/68] [layouts] Defer legend item feature count until layout is drawn --- src/core/layertree/qgslayertreemodel.cpp | 13 +++++++---- src/core/layout/qgslayoutitemlegend.cpp | 28 +++++++++++++++++++++--- src/core/layout/qgslayoutitemlegend.h | 8 ++++--- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/core/layertree/qgslayertreemodel.cpp b/src/core/layertree/qgslayertreemodel.cpp index 0aabc46ac2b7..8dddc7d2c024 100644 --- a/src/core/layertree/qgslayertreemodel.cpp +++ b/src/core/layertree/qgslayertreemodel.cpp @@ -41,7 +41,10 @@ QgsLayerTreeModel::QgsLayerTreeModel( QgsLayerTree *rootNode, QObject *parent ) , mRootNode( rootNode ) , mFlags( ShowLegend | AllowLegendChangeState | DeferredLegendInvalidation ) { - connectToRootNode(); + if ( rootNode ) + { + connectToRootNode(); + } mFontLayer.setBold( true ); @@ -1072,9 +1075,11 @@ void QgsLayerTreeModel::connectToRootNode() void QgsLayerTreeModel::disconnectFromRootNode() { - disconnect( mRootNode, nullptr, this, nullptr ); - - disconnectFromLayers( mRootNode ); + if ( mRootNode ) + { + disconnect( mRootNode, nullptr, this, nullptr ); + disconnectFromLayers( mRootNode ); + } } void QgsLayerTreeModel::recursivelyEmitDataChanged( const QModelIndex &idx ) diff --git a/src/core/layout/qgslayoutitemlegend.cpp b/src/core/layout/qgslayoutitemlegend.cpp index 7d208a0e8f0c..6d1a02d3cfda 100644 --- a/src/core/layout/qgslayoutitemlegend.cpp +++ b/src/core/layout/qgslayoutitemlegend.cpp @@ -46,7 +46,7 @@ QgsLayoutItemLegend::QgsLayoutItemLegend( QgsLayout *layout ) : QgsLayoutItem( layout ) - , mLegendModel( new QgsLegendModel( layout->project()->layerTreeRoot(), this ) ) + , mLegendModel( new QgsLegendModel( nullptr, this ) ) { #if 0 //no longer required? connect( &layout->atlasComposition(), &QgsAtlasComposition::renderEnded, this, &QgsLayoutItemLegend::onAtlasEnded ); @@ -105,6 +105,8 @@ void QgsLayoutItemLegend::paint( QPainter *painter, const QStyleOptionGraphicsIt if ( !painter ) return; + ensureModelIsInitialized(); + if ( mFilterAskedForUpdate ) { mFilterAskedForUpdate = false; @@ -309,11 +311,28 @@ bool QgsLayoutItemLegend::resizeToContents() const void QgsLayoutItemLegend::setCustomLayerTree( QgsLayerTree *rootGroup ) { - mLegendModel->setRootGroup( rootGroup ? rootGroup : ( mLayout ? mLayout->project()->layerTreeRoot() : nullptr ) ); + if ( !mDeferLegendModelInitialization ) + { + mLegendModel->setRootGroup( rootGroup ? rootGroup : ( mLayout ? mLayout->project()->layerTreeRoot() : nullptr ) ); + } mCustomLayerTree.reset( rootGroup ); } +void QgsLayoutItemLegend::ensureModelIsInitialized() +{ + if ( mDeferLegendModelInitialization ) + { + mDeferLegendModelInitialization = false; + setCustomLayerTree( mCustomLayerTree.release() ); + } +} + +QgsLegendModel *QgsLayoutItemLegend::model() +{ + ensureModelIsInitialized(); + return mLegendModel.get(); +} void QgsLayoutItemLegend::setAutoUpdateModel( bool autoUpdate ) { @@ -1020,7 +1039,10 @@ void QgsLayoutItemLegend::clearLegendCachedData() } }; - clearNodeCache( mLegendModel->rootGroup() ); + if ( QgsLayerTree *rootGroup = mLegendModel->rootGroup() ) + { + clearNodeCache( rootGroup ); + } } void QgsLayoutItemLegend::mapLayerStyleOverridesChanged() diff --git a/src/core/layout/qgslayoutitemlegend.h b/src/core/layout/qgslayoutitemlegend.h index b4995d1201ee..0302c18141a7 100644 --- a/src/core/layout/qgslayoutitemlegend.h +++ b/src/core/layout/qgslayoutitemlegend.h @@ -23,7 +23,7 @@ #include "qgslayoutitem.h" #include "qgslayertreemodel.h" #include "qgslegendsettings.h" -#include "qgslayertreegroup.h" +#include "qgslayertree.h" #include "qgsexpressioncontext.h" class QgsLayerTreeModel; @@ -156,7 +156,7 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem /** * Returns the legend model. */ - QgsLegendModel *model() { return mLegendModel.get(); } + QgsLegendModel *model(); /** * Sets whether the legend content should auto update to reflect changes in the project's @@ -634,8 +634,10 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem void setModelStyleOverrides( const QMap &overrides ); + void ensureModelIsInitialized(); std::unique_ptr< QgsLegendModel > mLegendModel; - std::unique_ptr< QgsLayerTreeGroup > mCustomLayerTree; + std::unique_ptr< QgsLayerTree > mCustomLayerTree; + bool mDeferLegendModelInitialization = true; QgsLegendSettings mSettings; From ec30cabf407bb1d1d35432355b0af6b10274efb5 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 20 Mar 2024 13:44:02 +1000 Subject: [PATCH 63/68] Add test mask to fix flaky test --- ...WMS_GetMap_Highlight_Empty_Labels_mask.png | Bin 15177 -> 15318 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/testdata/control_images/qgis_server/WMS_GetMap_Highlight_Empty_Labels/WMS_GetMap_Highlight_Empty_Labels_mask.png b/tests/testdata/control_images/qgis_server/WMS_GetMap_Highlight_Empty_Labels/WMS_GetMap_Highlight_Empty_Labels_mask.png index 61e2c19e14465e135263b6930bddd8c1cbba360c..d2528c6f0e3db87909b4fa15aa0bcdd8f17f9d5d 100644 GIT binary patch literal 15318 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 From 9f25c2d772d83f6d2fac890dba8a85b5bf400449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Fri, 15 Mar 2024 16:28:59 +0100 Subject: [PATCH 64/68] Export DXF Export dialog's settings to XML file --- src/app/qgsdxfexportdialog.cpp | 134 +++++++++++++++++++++++++++++++++ src/app/qgsdxfexportdialog.h | 6 ++ 2 files changed, 140 insertions(+) diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index ff9f1cc5e5a2..53c04da438cf 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -33,6 +33,7 @@ #include #include +#include const int LAYER_COL = 0; const int OUTPUT_LAYER_ATTRIBUTE_COL = 1; @@ -731,6 +732,13 @@ QgsDxfExportDialog::QgsDxfExportDialog( QWidget *parent, Qt::WindowFlags f ) mEncoding->addItems( QgsDxfExport::encodings() ); mEncoding->setCurrentIndex( mEncoding->findText( QgsProject::instance()->readEntry( QStringLiteral( "dxf" ), QStringLiteral( "/lastDxfEncoding" ), settings.value( QStringLiteral( "qgis/lastDxfEncoding" ), "CP1252" ).toString() ) ) ); + mBtnLoadSaveSettings = new QPushButton( tr( "Settings" ), this ); + QMenu *menuSettings = new QMenu( this ); + menuSettings->addAction( tr( "Load Settings from File…" ), this, &QgsDxfExportDialog::loadSettingsFromFile ); + menuSettings->addAction( tr( "Save Settings to File…" ), this, &QgsDxfExportDialog::saveSettingsToFile ); + mBtnLoadSaveSettings->setMenu( menuSettings ); + buttonBox->addButton( mBtnLoadSaveSettings, QDialogButtonBox::ResetRole ); + mModel->loadLayersOutputAttribute( mModel->rootGroup() ); } @@ -797,6 +805,132 @@ void QgsDxfExportDialog::deselectDataDefinedBlocks() } +void QgsDxfExportDialog::loadSettingsFromFile() +{ + +} + + +void QgsDxfExportDialog::saveSettingsToFile() +{ + QgsSettings settings; + const QString lastUsedDir = settings.value( QStringLiteral( "dxf/lastSettingsDir" ), QDir::homePath() ).toString(); + + QString outputFileName = QFileDialog::getSaveFileName( this, tr( "Save DXF Export settings as XML" ), + lastUsedDir, tr( "XML file" ) + " (*.xml)" ); + // return dialog focus on Mac + activateWindow(); + raise(); + if ( outputFileName.isEmpty() ) + { + return; + } + + //ensure the user never omitted the extension from the file name + if ( !outputFileName.endsWith( QStringLiteral( ".xml" ), Qt::CaseInsensitive ) ) + { + outputFileName += QStringLiteral( ".xml" ); + } + + QString myErrorMessage; + QDomDocument myDocument; + + saveSettingsToXML( myDocument ); + + const QFileInfo myFileInfo( outputFileName ); + const QFileInfo myDirInfo( myFileInfo.path() ); //excludes file name + if ( !myDirInfo.isWritable() ) + { + QMessageBox::information( this, tr( "Save DXF settings" ), tr( "The directory containing your dataset needs to be writable!" ) ); + return; + } + + QFile myFile( outputFileName ); + if ( myFile.open( QFile::WriteOnly | QFile::Truncate ) ) + { + QTextStream myFileStream( &myFile ); + // save as utf-8 with 2 spaces for indents + myDocument.save( myFileStream, 2 ); + myFile.close(); + QMessageBox::information( this, tr( "Save DXF settings" ), tr( "Created DXF settings file as %1" ).arg( outputFileName ) ); + settings.setValue( QStringLiteral( "dxf/lastSettingsDir" ), QFileInfo( outputFileName ).absolutePath() ); + return; + } + else + { + QMessageBox::information( this, tr( "Save DXF settings" ), tr( "ERROR: Failed to created DXF Export settings file as %1. Check file permissions and retry." ).arg( outputFileName ) ); + return; + } +} + + +void QgsDxfExportDialog::saveSettingsToXML( QDomDocument &doc ) const +{ + QDomImplementation DomImplementation; + const QDomDocumentType documentType = DomImplementation.createDocumentType( QStringLiteral( "qgis" ), QStringLiteral( "http://mrcc.com/qgis.dtd" ), QStringLiteral( "SYSTEM" ) ); + QDomDocument myDocument( documentType ); + + QDomElement myRootNode = myDocument.createElement( QStringLiteral( "qgis" ) ); + myRootNode.setAttribute( QStringLiteral( "version" ), Qgis::version() ); + myDocument.appendChild( myRootNode ); + + QDomElement symbologyModeElement = myDocument.createElement( QStringLiteral( "symbology_mode" ) ); + symbologyModeElement.appendChild( QgsXmlUtils::writeVariant( static_cast( symbologyMode() ), doc ) ); + myRootNode.appendChild( symbologyModeElement ); + + QDomElement symbologyScaleElement = myDocument.createElement( QStringLiteral( "symbology_scale" ) ); + symbologyScaleElement.appendChild( QgsXmlUtils::writeVariant( symbologyScale(), doc ) ); + myRootNode.appendChild( symbologyScaleElement ); + + QDomElement encodingElement = myDocument.createElement( QStringLiteral( "encoding" ) ); + encodingElement.appendChild( QgsXmlUtils::writeVariant( encoding(), doc ) ); + myRootNode.appendChild( encodingElement ); + + QDomElement crsElement = myDocument.createElement( QStringLiteral( "crs" ) ); + crsElement.appendChild( QgsXmlUtils::writeVariant( crs(), doc ) ); + myRootNode.appendChild( crsElement ); + + QDomElement mapThemeElement = myDocument.createElement( QStringLiteral( "map_theme" ) ); + mapThemeElement.appendChild( QgsXmlUtils::writeVariant( mapTheme(), doc ) ); + myRootNode.appendChild( mapThemeElement ); + + QDomElement layersElement = myDocument.createElement( QStringLiteral( "layers" ) ); + for ( const auto dxfLayer : layers() ) + { + QgsVectorLayer *vl = dxfLayer.layer(); + QDomElement layerElement = myDocument.createElement( QStringLiteral( "layer" ) ); + layerElement.setAttribute( QStringLiteral( "source" ), vl->source() ); + layerElement.setAttribute( QStringLiteral( "attribute-index" ), dxfLayer.layerOutputAttributeIndex() ) ; + layerElement.setAttribute( QStringLiteral( "use_symbol_blocks" ), dxfLayer.buildDataDefinedBlocks() ) ; + layerElement.setAttribute( QStringLiteral( "max_number_of_classes" ), dxfLayer.dataDefinedBlocksMaximumNumberOfClasses() ) ; + layersElement.appendChild( layerElement ); + } + myRootNode.appendChild( layersElement ); + + QDomElement titleAsNameElement = myDocument.createElement( QStringLiteral( "use_layer_title" ) ); + titleAsNameElement.appendChild( QgsXmlUtils::writeVariant( layerTitleAsName(), doc ) ); + myRootNode.appendChild( titleAsNameElement ); + + QDomElement useMapExtentElement = myDocument.createElement( QStringLiteral( "use_map_extent" ) ); + useMapExtentElement.appendChild( QgsXmlUtils::writeVariant( exportMapExtent(), doc ) ); + myRootNode.appendChild( useMapExtentElement ); + + QDomElement force2dElement = myDocument.createElement( QStringLiteral( "force_2d" ) ); + force2dElement.appendChild( QgsXmlUtils::writeVariant( force2d(), doc ) ); + myRootNode.appendChild( force2dElement ); + + QDomElement useMTextElement = myDocument.createElement( QStringLiteral( "mtext" ) ); + useMTextElement.appendChild( QgsXmlUtils::writeVariant( useMText(), doc ) ); + myRootNode.appendChild( useMTextElement ); + + QDomElement selectedFeatures = myDocument.createElement( QStringLiteral( "selected_features_only" ) ); + selectedFeatures.appendChild( QgsXmlUtils::writeVariant( selectedFeaturesOnly(), doc ) ); + myRootNode.appendChild( selectedFeatures ); + + doc = myDocument; +} + + QList< QgsDxfExport::DxfLayer > QgsDxfExportDialog::layers() const { return mModel->layers(); diff --git a/src/app/qgsdxfexportdialog.h b/src/app/qgsdxfexportdialog.h index 973bea2722c3..838fa6037723 100644 --- a/src/app/qgsdxfexportdialog.h +++ b/src/app/qgsdxfexportdialog.h @@ -24,6 +24,7 @@ #include "qgsdxfexport.h" #include "qgssettingstree.h" #include "qgssettingsentryimpl.h" +#include "qgsxmlutils.h" #include #include @@ -118,6 +119,8 @@ class QgsDxfExportDialog : public QDialog, private Ui::QgsDxfExportDialogBase QString mapTheme() const; QString encoding() const; QgsCoordinateReferenceSystem crs() const; + bool loadSettingsFromXML( QDomDocument &document ) const; + void saveSettingsToXML( QDomDocument &document ) const; public slots: //! Change the selection of layers in the list @@ -125,6 +128,8 @@ class QgsDxfExportDialog : public QDialog, private Ui::QgsDxfExportDialogBase void deSelectAll(); void selectDataDefinedBlocks(); void deselectDataDefinedBlocks(); + void loadSettingsFromFile(); + void saveSettingsToFile(); private slots: void setOkEnabled(); @@ -139,6 +144,7 @@ class QgsDxfExportDialog : public QDialog, private Ui::QgsDxfExportDialogBase FieldSelectorDelegate *mFieldSelectorDelegate = nullptr; QgsVectorLayerAndAttributeModel *mModel = nullptr; QgsDxfExportLayerTreeView *mTreeView = nullptr; + QPushButton *mBtnLoadSaveSettings = nullptr; QgsCoordinateReferenceSystem mCRS; }; From d5265317a490a023260a94d3fb4a7b23d221dfe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Fri, 15 Mar 2024 19:54:00 +0100 Subject: [PATCH 65/68] Load DXF Export dialog's settings from XML file --- src/app/qgsdxfexportdialog.cpp | 83 +++++++++++++++++++++++++++++++++- src/app/qgsdxfexportdialog.h | 2 +- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index 53c04da438cf..048863ef1ee5 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -807,7 +807,88 @@ void QgsDxfExportDialog::deselectDataDefinedBlocks() void QgsDxfExportDialog::loadSettingsFromFile() { + QgsSettings settings; + const QString lastUsedDir = settings.value( QStringLiteral( "dxf/lastSettingsDir" ), QDir::homePath() ).toString(); + + const QString fileName = QFileDialog::getOpenFileName( this, tr( "Load DXF Export settings" ), lastUsedDir, + tr( "XML file" ) + " (*.xml)" ); + if ( fileName.isNull() ) + { + return; + } + + bool resultFlag = false; + + QDomDocument myDocument( QStringLiteral( "qgis" ) ); + + // location of problem associated with errorMsg + int line, column; + QString myErrorMessage; + + QFile myFile( fileName ); + if ( myFile.open( QFile::ReadOnly ) ) + { + QgsDebugMsgLevel( QStringLiteral( "file found %1" ).arg( fileName ), 2 ); + // read file + resultFlag = myDocument.setContent( &myFile, &myErrorMessage, &line, &column ); + if ( !resultFlag ) + myErrorMessage = tr( "%1 at line %2 column %3" ).arg( myErrorMessage ).arg( line ).arg( column ); + myFile.close(); + } + + resultFlag = loadSettingsFromXML( myDocument, myErrorMessage ); + if ( !resultFlag ) + QMessageBox::information( this, tr( "Load DXF settings" ), tr( "ERROR: Failed to load DXF Export settings file as %1. %2" ).arg( fileName, myErrorMessage ) ); + else + { + settings.setValue( QStringLiteral( "dxf/lastSettingsDir" ), QFileInfo( fileName ).path() ); + QMessageBox::information( this, tr( "Load DXF settings" ), tr( "DXF Export settings loaded!" ) ); + } +} + + +bool QgsDxfExportDialog::loadSettingsFromXML( QDomDocument &doc, QString &errorMessage ) const +{ + const QDomElement myRoot = doc.firstChildElement( QStringLiteral( "qgis" ) ); + if ( myRoot.isNull() ) + { + errorMessage = tr( "Root element could not be found" ); + return false; + } + + QDomElement mne; + mne = myRoot.namedItem( QStringLiteral( "symbology_mode" ) ).toElement(); + mSymbologyModeComboBox->setCurrentIndex( QgsXmlUtils::readVariant( mne.firstChildElement() ).toInt() ); + + mne = myRoot.namedItem( QStringLiteral( "symbology_scale" ) ).toElement(); + mScaleWidget->setScale( QgsXmlUtils::readVariant( mne.firstChildElement() ).toDouble() ); + + mne = myRoot.namedItem( QStringLiteral( "encoding" ) ).toElement(); + mEncoding->setCurrentText( QgsXmlUtils::readVariant( mne.firstChildElement() ).toString() ); + + mne = myRoot.namedItem( QStringLiteral( "crs" ) ).toElement(); + mCrsSelector->setCrs( QgsXmlUtils::readVariant( mne.firstChildElement() ).value< QgsCoordinateReferenceSystem >() ); + + mne = myRoot.namedItem( QStringLiteral( "map_theme" ) ).toElement(); + mVisibilityPresets->setCurrentText( QgsXmlUtils::readVariant( mne.firstChildElement() ).toString() ); + + // layers + mne = myRoot.namedItem( QStringLiteral( "use_layer_title" ) ).toElement(); + mLayerTitleAsName->setChecked( QgsXmlUtils::readVariant( mne.firstChildElement() ) == true ); + + mne = myRoot.namedItem( QStringLiteral( "use_map_extent" ) ).toElement(); + mMapExtentCheckBox->setChecked( QgsXmlUtils::readVariant( mne.firstChildElement() ) == true ); + + mne = myRoot.namedItem( QStringLiteral( "force_2d" ) ).toElement(); + mForce2d->setChecked( QgsXmlUtils::readVariant( mne.firstChildElement() ) == true ); + + mne = myRoot.namedItem( QStringLiteral( "mtext" ) ).toElement(); + mMTextCheckBox->setChecked( QgsXmlUtils::readVariant( mne.firstChildElement() ) == true ); + + mne = myRoot.namedItem( QStringLiteral( "selected_features_only" ) ).toElement(); + mSelectedFeaturesOnly->setChecked( QgsXmlUtils::readVariant( mne.firstChildElement() ) == true ); + return true; } @@ -899,7 +980,7 @@ void QgsDxfExportDialog::saveSettingsToXML( QDomDocument &doc ) const { QgsVectorLayer *vl = dxfLayer.layer(); QDomElement layerElement = myDocument.createElement( QStringLiteral( "layer" ) ); - layerElement.setAttribute( QStringLiteral( "source" ), vl->source() ); + layerElement.setAttribute( QStringLiteral( "source" ), vl->publicSource() ); layerElement.setAttribute( QStringLiteral( "attribute-index" ), dxfLayer.layerOutputAttributeIndex() ) ; layerElement.setAttribute( QStringLiteral( "use_symbol_blocks" ), dxfLayer.buildDataDefinedBlocks() ) ; layerElement.setAttribute( QStringLiteral( "max_number_of_classes" ), dxfLayer.dataDefinedBlocksMaximumNumberOfClasses() ) ; diff --git a/src/app/qgsdxfexportdialog.h b/src/app/qgsdxfexportdialog.h index 838fa6037723..a66647de54be 100644 --- a/src/app/qgsdxfexportdialog.h +++ b/src/app/qgsdxfexportdialog.h @@ -119,7 +119,7 @@ class QgsDxfExportDialog : public QDialog, private Ui::QgsDxfExportDialogBase QString mapTheme() const; QString encoding() const; QgsCoordinateReferenceSystem crs() const; - bool loadSettingsFromXML( QDomDocument &document ) const; + bool loadSettingsFromXML( QDomDocument &document, QString &errorMessage ) const; void saveSettingsToXML( QDomDocument &document ) const; public slots: From 6716585947a6646797823a09e9d99956934d90c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Mon, 18 Mar 2024 15:04:25 +0100 Subject: [PATCH 66/68] Make DXF Export load settings from XML flexible by ignoring elements not present in the XML; ask for confirmation before overriding GUI values --- src/app/qgsdxfexportdialog.cpp | 60 ++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index 048863ef1ee5..b4c4f4974d59 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -836,13 +836,18 @@ void QgsDxfExportDialog::loadSettingsFromFile() myFile.close(); } - resultFlag = loadSettingsFromXML( myDocument, myErrorMessage ); - if ( !resultFlag ) - QMessageBox::information( this, tr( "Load DXF settings" ), tr( "ERROR: Failed to load DXF Export settings file as %1. %2" ).arg( fileName, myErrorMessage ) ); - else + if ( QMessageBox::question( this, + tr( "DXF Export load from XML file" ), + tr( "Are you sure you want to load settings from XML? This will change some values in the DXF Export dialog." ) ) == QMessageBox::Yes ) { - settings.setValue( QStringLiteral( "dxf/lastSettingsDir" ), QFileInfo( fileName ).path() ); - QMessageBox::information( this, tr( "Load DXF settings" ), tr( "DXF Export settings loaded!" ) ); + resultFlag = loadSettingsFromXML( myDocument, myErrorMessage ); + if ( !resultFlag ) + QMessageBox::information( this, tr( "Load DXF settings" ), tr( "ERROR: Failed to load DXF Export settings file as %1. %2" ).arg( fileName, myErrorMessage ) ); + else + { + settings.setValue( QStringLiteral( "dxf/lastSettingsDir" ), QFileInfo( fileName ).path() ); + QMessageBox::information( this, tr( "Load DXF settings" ), tr( "DXF Export settings loaded!" ) ); + } } } @@ -857,36 +862,57 @@ bool QgsDxfExportDialog::loadSettingsFromXML( QDomDocument &doc, QString &errorM } QDomElement mne; + QVariant value; + mne = myRoot.namedItem( QStringLiteral( "symbology_mode" ) ).toElement(); - mSymbologyModeComboBox->setCurrentIndex( QgsXmlUtils::readVariant( mne.firstChildElement() ).toInt() ); + value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + if ( !value.isNull() ) + mSymbologyModeComboBox->setCurrentIndex( value.toInt() ); mne = myRoot.namedItem( QStringLiteral( "symbology_scale" ) ).toElement(); - mScaleWidget->setScale( QgsXmlUtils::readVariant( mne.firstChildElement() ).toDouble() ); + value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + if ( !value.isNull() ) + mScaleWidget->setScale( value.toDouble() ); mne = myRoot.namedItem( QStringLiteral( "encoding" ) ).toElement(); - mEncoding->setCurrentText( QgsXmlUtils::readVariant( mne.firstChildElement() ).toString() ); + value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + if ( !value.isNull() ) + mEncoding->setCurrentText( value.toString() ); mne = myRoot.namedItem( QStringLiteral( "crs" ) ).toElement(); - mCrsSelector->setCrs( QgsXmlUtils::readVariant( mne.firstChildElement() ).value< QgsCoordinateReferenceSystem >() ); + value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + if ( !value.isNull() ) + mCrsSelector->setCrs( value.value< QgsCoordinateReferenceSystem >() ); mne = myRoot.namedItem( QStringLiteral( "map_theme" ) ).toElement(); - mVisibilityPresets->setCurrentText( QgsXmlUtils::readVariant( mne.firstChildElement() ).toString() ); + value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + if ( !value.isNull() ) + mVisibilityPresets->setCurrentText( value.toString() ); - // layers mne = myRoot.namedItem( QStringLiteral( "use_layer_title" ) ).toElement(); - mLayerTitleAsName->setChecked( QgsXmlUtils::readVariant( mne.firstChildElement() ) == true ); + value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + if ( !value.isNull() ) + mLayerTitleAsName->setChecked( value == true ); mne = myRoot.namedItem( QStringLiteral( "use_map_extent" ) ).toElement(); - mMapExtentCheckBox->setChecked( QgsXmlUtils::readVariant( mne.firstChildElement() ) == true ); + value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + if ( !value.isNull() ) + mMapExtentCheckBox->setChecked( value == true ); mne = myRoot.namedItem( QStringLiteral( "force_2d" ) ).toElement(); - mForce2d->setChecked( QgsXmlUtils::readVariant( mne.firstChildElement() ) == true ); + value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + if ( !value.isNull() ) + mForce2d->setChecked( value == true ); mne = myRoot.namedItem( QStringLiteral( "mtext" ) ).toElement(); - mMTextCheckBox->setChecked( QgsXmlUtils::readVariant( mne.firstChildElement() ) == true ); + value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + if ( !value.isNull() ) + mMTextCheckBox->setChecked( value == true ); mne = myRoot.namedItem( QStringLiteral( "selected_features_only" ) ).toElement(); - mSelectedFeaturesOnly->setChecked( QgsXmlUtils::readVariant( mne.firstChildElement() ) == true ); + value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + if ( !value.isNull() ) + mSelectedFeaturesOnly->setChecked( value == true ); return true; } From b8c6bd37a2121acc90ffdabd77feeb2163ff8001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Wed, 20 Mar 2024 14:59:41 +0100 Subject: [PATCH 67/68] [dxf] Remove 'my' prefixes according to review --- src/app/qgsdxfexportdialog.cpp | 120 ++++++++++++++++----------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index b4c4f4974d59..abb4653eef39 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -819,30 +819,30 @@ void QgsDxfExportDialog::loadSettingsFromFile() bool resultFlag = false; - QDomDocument myDocument( QStringLiteral( "qgis" ) ); + QDomDocument domDocument( QStringLiteral( "qgis" ) ); // location of problem associated with errorMsg int line, column; - QString myErrorMessage; + QString errorMessage; - QFile myFile( fileName ); - if ( myFile.open( QFile::ReadOnly ) ) + QFile file( fileName ); + if ( file.open( QFile::ReadOnly ) ) { QgsDebugMsgLevel( QStringLiteral( "file found %1" ).arg( fileName ), 2 ); // read file - resultFlag = myDocument.setContent( &myFile, &myErrorMessage, &line, &column ); + resultFlag = domDocument.setContent( &file, &errorMessage, &line, &column ); if ( !resultFlag ) - myErrorMessage = tr( "%1 at line %2 column %3" ).arg( myErrorMessage ).arg( line ).arg( column ); - myFile.close(); + errorMessage = tr( "%1 at line %2 column %3" ).arg( errorMessage ).arg( line ).arg( column ); + file.close(); } if ( QMessageBox::question( this, tr( "DXF Export load from XML file" ), tr( "Are you sure you want to load settings from XML? This will change some values in the DXF Export dialog." ) ) == QMessageBox::Yes ) { - resultFlag = loadSettingsFromXML( myDocument, myErrorMessage ); + resultFlag = loadSettingsFromXML( domDocument, errorMessage ); if ( !resultFlag ) - QMessageBox::information( this, tr( "Load DXF settings" ), tr( "ERROR: Failed to load DXF Export settings file as %1. %2" ).arg( fileName, myErrorMessage ) ); + QMessageBox::information( this, tr( "Load DXF settings" ), tr( "ERROR: Failed to load DXF Export settings file as %1. %2" ).arg( fileName, errorMessage ) ); else { settings.setValue( QStringLiteral( "dxf/lastSettingsDir" ), QFileInfo( fileName ).path() ); @@ -854,8 +854,8 @@ void QgsDxfExportDialog::loadSettingsFromFile() bool QgsDxfExportDialog::loadSettingsFromXML( QDomDocument &doc, QString &errorMessage ) const { - const QDomElement myRoot = doc.firstChildElement( QStringLiteral( "qgis" ) ); - if ( myRoot.isNull() ) + const QDomElement rootElement = doc.firstChildElement( QStringLiteral( "qgis" ) ); + if ( rootElement.isNull() ) { errorMessage = tr( "Root element could not be found" ); return false; @@ -864,52 +864,52 @@ bool QgsDxfExportDialog::loadSettingsFromXML( QDomDocument &doc, QString &errorM QDomElement mne; QVariant value; - mne = myRoot.namedItem( QStringLiteral( "symbology_mode" ) ).toElement(); + mne = rootElement.namedItem( QStringLiteral( "symbology_mode" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mSymbologyModeComboBox->setCurrentIndex( value.toInt() ); - mne = myRoot.namedItem( QStringLiteral( "symbology_scale" ) ).toElement(); + mne = rootElement.namedItem( QStringLiteral( "symbology_scale" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mScaleWidget->setScale( value.toDouble() ); - mne = myRoot.namedItem( QStringLiteral( "encoding" ) ).toElement(); + mne = rootElement.namedItem( QStringLiteral( "encoding" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mEncoding->setCurrentText( value.toString() ); - mne = myRoot.namedItem( QStringLiteral( "crs" ) ).toElement(); + mne = rootElement.namedItem( QStringLiteral( "crs" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mCrsSelector->setCrs( value.value< QgsCoordinateReferenceSystem >() ); - mne = myRoot.namedItem( QStringLiteral( "map_theme" ) ).toElement(); + mne = rootElement.namedItem( QStringLiteral( "map_theme" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mVisibilityPresets->setCurrentText( value.toString() ); - mne = myRoot.namedItem( QStringLiteral( "use_layer_title" ) ).toElement(); + mne = rootElement.namedItem( QStringLiteral( "use_layer_title" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mLayerTitleAsName->setChecked( value == true ); - mne = myRoot.namedItem( QStringLiteral( "use_map_extent" ) ).toElement(); + mne = rootElement.namedItem( QStringLiteral( "use_map_extent" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mMapExtentCheckBox->setChecked( value == true ); - mne = myRoot.namedItem( QStringLiteral( "force_2d" ) ).toElement(); + mne = rootElement.namedItem( QStringLiteral( "force_2d" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mForce2d->setChecked( value == true ); - mne = myRoot.namedItem( QStringLiteral( "mtext" ) ).toElement(); + mne = rootElement.namedItem( QStringLiteral( "mtext" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mMTextCheckBox->setChecked( value == true ); - mne = myRoot.namedItem( QStringLiteral( "selected_features_only" ) ).toElement(); + mne = rootElement.namedItem( QStringLiteral( "selected_features_only" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mSelectedFeaturesOnly->setChecked( value == true ); @@ -939,26 +939,26 @@ void QgsDxfExportDialog::saveSettingsToFile() outputFileName += QStringLiteral( ".xml" ); } - QString myErrorMessage; - QDomDocument myDocument; + QString errorMessage; + QDomDocument domDocument; - saveSettingsToXML( myDocument ); + saveSettingsToXML( domDocument ); - const QFileInfo myFileInfo( outputFileName ); - const QFileInfo myDirInfo( myFileInfo.path() ); //excludes file name - if ( !myDirInfo.isWritable() ) + const QFileInfo fileInfo( outputFileName ); + const QFileInfo dirInfo( fileInfo.path() ); //excludes file name + if ( !dirInfo.isWritable() ) { QMessageBox::information( this, tr( "Save DXF settings" ), tr( "The directory containing your dataset needs to be writable!" ) ); return; } - QFile myFile( outputFileName ); - if ( myFile.open( QFile::WriteOnly | QFile::Truncate ) ) + QFile file( outputFileName ); + if ( file.open( QFile::WriteOnly | QFile::Truncate ) ) { - QTextStream myFileStream( &myFile ); + QTextStream fileStream( &file ); // save as utf-8 with 2 spaces for indents - myDocument.save( myFileStream, 2 ); - myFile.close(); + domDocument.save( fileStream, 2 ); + file.close(); QMessageBox::information( this, tr( "Save DXF settings" ), tr( "Created DXF settings file as %1" ).arg( outputFileName ) ); settings.setValue( QStringLiteral( "dxf/lastSettingsDir" ), QFileInfo( outputFileName ).absolutePath() ); return; @@ -975,66 +975,66 @@ void QgsDxfExportDialog::saveSettingsToXML( QDomDocument &doc ) const { QDomImplementation DomImplementation; const QDomDocumentType documentType = DomImplementation.createDocumentType( QStringLiteral( "qgis" ), QStringLiteral( "http://mrcc.com/qgis.dtd" ), QStringLiteral( "SYSTEM" ) ); - QDomDocument myDocument( documentType ); + QDomDocument domDocument( documentType ); - QDomElement myRootNode = myDocument.createElement( QStringLiteral( "qgis" ) ); - myRootNode.setAttribute( QStringLiteral( "version" ), Qgis::version() ); - myDocument.appendChild( myRootNode ); + QDomElement rootElement = domDocument.createElement( QStringLiteral( "qgis" ) ); + rootElement.setAttribute( QStringLiteral( "version" ), Qgis::version() ); + domDocument.appendChild( rootElement ); - QDomElement symbologyModeElement = myDocument.createElement( QStringLiteral( "symbology_mode" ) ); + QDomElement symbologyModeElement = domDocument.createElement( QStringLiteral( "symbology_mode" ) ); symbologyModeElement.appendChild( QgsXmlUtils::writeVariant( static_cast( symbologyMode() ), doc ) ); - myRootNode.appendChild( symbologyModeElement ); + rootElement.appendChild( symbologyModeElement ); - QDomElement symbologyScaleElement = myDocument.createElement( QStringLiteral( "symbology_scale" ) ); + QDomElement symbologyScaleElement = domDocument.createElement( QStringLiteral( "symbology_scale" ) ); symbologyScaleElement.appendChild( QgsXmlUtils::writeVariant( symbologyScale(), doc ) ); - myRootNode.appendChild( symbologyScaleElement ); + rootElement.appendChild( symbologyScaleElement ); - QDomElement encodingElement = myDocument.createElement( QStringLiteral( "encoding" ) ); + QDomElement encodingElement = domDocument.createElement( QStringLiteral( "encoding" ) ); encodingElement.appendChild( QgsXmlUtils::writeVariant( encoding(), doc ) ); - myRootNode.appendChild( encodingElement ); + rootElement.appendChild( encodingElement ); - QDomElement crsElement = myDocument.createElement( QStringLiteral( "crs" ) ); + QDomElement crsElement = domDocument.createElement( QStringLiteral( "crs" ) ); crsElement.appendChild( QgsXmlUtils::writeVariant( crs(), doc ) ); - myRootNode.appendChild( crsElement ); + rootElement.appendChild( crsElement ); - QDomElement mapThemeElement = myDocument.createElement( QStringLiteral( "map_theme" ) ); + QDomElement mapThemeElement = domDocument.createElement( QStringLiteral( "map_theme" ) ); mapThemeElement.appendChild( QgsXmlUtils::writeVariant( mapTheme(), doc ) ); - myRootNode.appendChild( mapThemeElement ); + rootElement.appendChild( mapThemeElement ); - QDomElement layersElement = myDocument.createElement( QStringLiteral( "layers" ) ); + QDomElement layersElement = domDocument.createElement( QStringLiteral( "layers" ) ); for ( const auto dxfLayer : layers() ) { QgsVectorLayer *vl = dxfLayer.layer(); - QDomElement layerElement = myDocument.createElement( QStringLiteral( "layer" ) ); + QDomElement layerElement = domDocument.createElement( QStringLiteral( "layer" ) ); layerElement.setAttribute( QStringLiteral( "source" ), vl->publicSource() ); layerElement.setAttribute( QStringLiteral( "attribute-index" ), dxfLayer.layerOutputAttributeIndex() ) ; layerElement.setAttribute( QStringLiteral( "use_symbol_blocks" ), dxfLayer.buildDataDefinedBlocks() ) ; layerElement.setAttribute( QStringLiteral( "max_number_of_classes" ), dxfLayer.dataDefinedBlocksMaximumNumberOfClasses() ) ; layersElement.appendChild( layerElement ); } - myRootNode.appendChild( layersElement ); + rootElement.appendChild( layersElement ); - QDomElement titleAsNameElement = myDocument.createElement( QStringLiteral( "use_layer_title" ) ); + QDomElement titleAsNameElement = domDocument.createElement( QStringLiteral( "use_layer_title" ) ); titleAsNameElement.appendChild( QgsXmlUtils::writeVariant( layerTitleAsName(), doc ) ); - myRootNode.appendChild( titleAsNameElement ); + rootElement.appendChild( titleAsNameElement ); - QDomElement useMapExtentElement = myDocument.createElement( QStringLiteral( "use_map_extent" ) ); + QDomElement useMapExtentElement = domDocument.createElement( QStringLiteral( "use_map_extent" ) ); useMapExtentElement.appendChild( QgsXmlUtils::writeVariant( exportMapExtent(), doc ) ); - myRootNode.appendChild( useMapExtentElement ); + rootElement.appendChild( useMapExtentElement ); - QDomElement force2dElement = myDocument.createElement( QStringLiteral( "force_2d" ) ); + QDomElement force2dElement = domDocument.createElement( QStringLiteral( "force_2d" ) ); force2dElement.appendChild( QgsXmlUtils::writeVariant( force2d(), doc ) ); - myRootNode.appendChild( force2dElement ); + rootElement.appendChild( force2dElement ); - QDomElement useMTextElement = myDocument.createElement( QStringLiteral( "mtext" ) ); + QDomElement useMTextElement = domDocument.createElement( QStringLiteral( "mtext" ) ); useMTextElement.appendChild( QgsXmlUtils::writeVariant( useMText(), doc ) ); - myRootNode.appendChild( useMTextElement ); + rootElement.appendChild( useMTextElement ); - QDomElement selectedFeatures = myDocument.createElement( QStringLiteral( "selected_features_only" ) ); + QDomElement selectedFeatures = domDocument.createElement( QStringLiteral( "selected_features_only" ) ); selectedFeatures.appendChild( QgsXmlUtils::writeVariant( selectedFeaturesOnly(), doc ) ); - myRootNode.appendChild( selectedFeatures ); + rootElement.appendChild( selectedFeatures ); - doc = myDocument; + doc = domDocument; } From 72249d68328d5d46e66009249751324d916c904e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Wed, 20 Mar 2024 17:08:17 +0100 Subject: [PATCH 68/68] [dxf] Use the new Settings API --- src/app/qgsdxfexportdialog.cpp | 16 ++++++---------- src/app/qgsdxfexportdialog.h | 1 + 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index abb4653eef39..874d33982b04 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -807,10 +807,8 @@ void QgsDxfExportDialog::deselectDataDefinedBlocks() void QgsDxfExportDialog::loadSettingsFromFile() { - QgsSettings settings; - const QString lastUsedDir = settings.value( QStringLiteral( "dxf/lastSettingsDir" ), QDir::homePath() ).toString(); - - const QString fileName = QFileDialog::getOpenFileName( this, tr( "Load DXF Export settings" ), lastUsedDir, + const QString fileName = QFileDialog::getOpenFileName( this, tr( "Load DXF Export settings" ), + QgsDxfExportDialog::settingsDxfLastSettingsDir->value(), tr( "XML file" ) + " (*.xml)" ); if ( fileName.isNull() ) { @@ -845,7 +843,7 @@ void QgsDxfExportDialog::loadSettingsFromFile() QMessageBox::information( this, tr( "Load DXF settings" ), tr( "ERROR: Failed to load DXF Export settings file as %1. %2" ).arg( fileName, errorMessage ) ); else { - settings.setValue( QStringLiteral( "dxf/lastSettingsDir" ), QFileInfo( fileName ).path() ); + QgsDxfExportDialog::settingsDxfLastSettingsDir->setValue( QFileInfo( fileName ).path() ); QMessageBox::information( this, tr( "Load DXF settings" ), tr( "DXF Export settings loaded!" ) ); } } @@ -920,11 +918,9 @@ bool QgsDxfExportDialog::loadSettingsFromXML( QDomDocument &doc, QString &errorM void QgsDxfExportDialog::saveSettingsToFile() { - QgsSettings settings; - const QString lastUsedDir = settings.value( QStringLiteral( "dxf/lastSettingsDir" ), QDir::homePath() ).toString(); - QString outputFileName = QFileDialog::getSaveFileName( this, tr( "Save DXF Export settings as XML" ), - lastUsedDir, tr( "XML file" ) + " (*.xml)" ); + QgsDxfExportDialog::settingsDxfLastSettingsDir->value(), + tr( "XML file" ) + " (*.xml)" ); // return dialog focus on Mac activateWindow(); raise(); @@ -960,7 +956,7 @@ void QgsDxfExportDialog::saveSettingsToFile() domDocument.save( fileStream, 2 ); file.close(); QMessageBox::information( this, tr( "Save DXF settings" ), tr( "Created DXF settings file as %1" ).arg( outputFileName ) ); - settings.setValue( QStringLiteral( "dxf/lastSettingsDir" ), QFileInfo( outputFileName ).absolutePath() ); + QgsDxfExportDialog::settingsDxfLastSettingsDir->setValue( QFileInfo( outputFileName ).absolutePath() ); return; } else diff --git a/src/app/qgsdxfexportdialog.h b/src/app/qgsdxfexportdialog.h index a66647de54be..4afb9e33ac7d 100644 --- a/src/app/qgsdxfexportdialog.h +++ b/src/app/qgsdxfexportdialog.h @@ -102,6 +102,7 @@ class QgsDxfExportDialog : public QDialog, private Ui::QgsDxfExportDialogBase public: static inline QgsSettingsTreeNode *sTreeAppDdxf = QgsSettingsTree::sTreeApp->createChildNode( QStringLiteral( "dxf" ) ); static const inline QgsSettingsEntryBool *settingsDxfEnableDDBlocks = new QgsSettingsEntryBool( QStringLiteral( "enable-datadefined-blocks" ), sTreeAppDdxf, false ); + static const inline QgsSettingsEntryString *settingsDxfLastSettingsDir = new QgsSettingsEntryString( QStringLiteral( "last-settings-dir" ), sTreeAppDdxf, QDir::homePath() ); QgsDxfExportDialog( QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags() ); ~QgsDxfExportDialog() override;