From 97631189c95a27693104d64e4a3be6044c6c40d5 Mon Sep 17 00:00:00 2001 From: Marco Hugentobler Date: Wed, 6 Mar 2024 14:02:25 +0100 Subject: [PATCH 01/87] Send filter parameter in wms client if contained in url. Usefull for WMS requests with external wms layers --- src/providers/wms/qgswmscapabilities.cpp | 5 +++++ src/providers/wms/qgswmscapabilities.h | 2 ++ src/providers/wms/qgswmsprovider.cpp | 5 +++++ 3 files changed, 12 insertions(+) diff --git a/src/providers/wms/qgswmscapabilities.cpp b/src/providers/wms/qgswmscapabilities.cpp index 5ac23a33e53f..298cd5dc9d0a 100644 --- a/src/providers/wms/qgswmscapabilities.cpp +++ b/src/providers/wms/qgswmscapabilities.cpp @@ -213,6 +213,11 @@ bool QgsWmsSettings::parseUri( const QString &uriString ) } } + if ( uri.hasParam( QStringLiteral( "filter" ) ) ) + { + mFilter = uri.param( QStringLiteral( "filter" ) ); + } + mImageMimeType = uri.param( QStringLiteral( "format" ) ); QgsDebugMsgLevel( "Setting image encoding to " + mImageMimeType + '.', 2 ); diff --git a/src/providers/wms/qgswmscapabilities.h b/src/providers/wms/qgswmscapabilities.h index f56a5126d103..e954a140d708 100644 --- a/src/providers/wms/qgswmscapabilities.h +++ b/src/providers/wms/qgswmscapabilities.h @@ -874,6 +874,8 @@ class QgsWmsSettings bool mSmoothPixmapTransform; enum QgsWmsDpiMode mDpiMode; + QString mFilter; + /** * Active sublayers managed by this provider in a draw function, in order from bottom to top * (some may not be visible in a draw function, cf. activeSubLayerVisibility) diff --git a/src/providers/wms/qgswmsprovider.cpp b/src/providers/wms/qgswmsprovider.cpp index 46aa775d326c..48d202a7e43e 100644 --- a/src/providers/wms/qgswmsprovider.cpp +++ b/src/providers/wms/qgswmsprovider.cpp @@ -1298,6 +1298,11 @@ QUrl QgsWmsProvider::createRequestUrlWMS( const QgsRectangle &viewExtent, int pi setQueryItem( query, QStringLiteral( "OPACITIES" ), mSettings.mOpacities.join( ',' ) ); } + if ( !mSettings.mFilter.isEmpty() ) + { + setQueryItem( query, QStringLiteral( "FILTER" ), mSettings.mFilter ); + } + // For WMS-T layers if ( temporalCapabilities() && temporalCapabilities()->hasTemporalCapabilities() ) From 101a5cccf714ccdd3be3eba88ffb1cbf82ad9c1b Mon Sep 17 00:00:00 2001 From: Julien Cabieces Date: Tue, 23 Jan 2024 18:48:27 +0100 Subject: [PATCH 02/87] [Feat] Add crs information in WMS GetFeatureInfo output --- .../proj/qgscoordinatereferencesystem.sip.in | 8 +++++++ .../core/auto_generated/qgsjsonutils.sip.in | 1 + .../proj/qgscoordinatereferencesystem.sip.in | 8 +++++++ .../core/auto_generated/qgsjsonutils.sip.in | 1 + .../proj/qgscoordinatereferencesystem.cpp | 24 +++++++++++++++++++ src/core/proj/qgscoordinatereferencesystem.h | 8 +++++++ src/core/qgsjsonutils.cpp | 14 +++++++++++ src/core/qgsjsonutils.h | 9 +++++++ src/server/services/wms/qgswmsrenderer.cpp | 6 +++-- src/server/services/wms/qgswmsrenderer.h | 2 +- .../test_qgsserver_wms_getfeatureinfo.py | 24 +++++++++++++++++++ .../wms_getfeatureinfo_alias_json.txt | 7 ++++++ ..._getfeatureinfo_exclude_attribute_json.txt | 7 ++++++ .../wms_getfeatureinfo_geojson.txt | 7 ++++++ ...wms_getfeatureinfo_geometry_CRS84_json.txt | 24 +++++++++++++++++++ .../wms_getfeatureinfo_geometry_json.txt | 7 ++++++ .../qgis_server/wms_getfeatureinfo_json.txt | 7 ++++++ .../wms_getfeatureinfo_multiple_json.txt | 7 ++++++ 18 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 tests/testdata/qgis_server/wms_getfeatureinfo_geometry_CRS84_json.txt diff --git a/python/PyQt6/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in b/python/PyQt6/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in index d3f26e89b41e..6635d83da796 100644 --- a/python/PyQt6/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in +++ b/python/PyQt6/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in @@ -972,6 +972,14 @@ projection in the WGS 84 CRS. Returns the crs as OGC URI (format: http://www.opengis.net/def/crs/OGC/1.3/CRS84) Returns an empty string on failure. +.. versionadded:: 3.30 +%End + + QString toOgcUrn() const; +%Docstring +Returns the crs as OGC URN (format: urn:ogc:def:crs:OGC:1.3:CRS84) +Returns an empty string on failure. + .. versionadded:: 3.30 %End diff --git a/python/PyQt6/core/auto_generated/qgsjsonutils.sip.in b/python/PyQt6/core/auto_generated/qgsjsonutils.sip.in index 2b9322784515..799f629dfdd9 100644 --- a/python/PyQt6/core/auto_generated/qgsjsonutils.sip.in +++ b/python/PyQt6/core/auto_generated/qgsjsonutils.sip.in @@ -363,6 +363,7 @@ Returns a null geometry if the geometry could not be parsed. + }; /************************************************************************ diff --git a/python/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in b/python/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in index a35a7f51df42..8cd4b270aa54 100644 --- a/python/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in +++ b/python/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in @@ -972,6 +972,14 @@ projection in the WGS 84 CRS. Returns the crs as OGC URI (format: http://www.opengis.net/def/crs/OGC/1.3/CRS84) Returns an empty string on failure. +.. versionadded:: 3.30 +%End + + QString toOgcUrn() const; +%Docstring +Returns the crs as OGC URN (format: urn:ogc:def:crs:OGC:1.3:CRS84) +Returns an empty string on failure. + .. versionadded:: 3.30 %End diff --git a/python/core/auto_generated/qgsjsonutils.sip.in b/python/core/auto_generated/qgsjsonutils.sip.in index c45c146701cf..81b58724bc53 100644 --- a/python/core/auto_generated/qgsjsonutils.sip.in +++ b/python/core/auto_generated/qgsjsonutils.sip.in @@ -363,6 +363,7 @@ Returns a null geometry if the geometry could not be parsed. + }; /************************************************************************ diff --git a/src/core/proj/qgscoordinatereferencesystem.cpp b/src/core/proj/qgscoordinatereferencesystem.cpp index 67d4853ac9c8..5bf8a33b6824 100644 --- a/src/core/proj/qgscoordinatereferencesystem.cpp +++ b/src/core/proj/qgscoordinatereferencesystem.cpp @@ -1600,6 +1600,30 @@ QString QgsCoordinateReferenceSystem::toOgcUri() const return QString(); } +QString QgsCoordinateReferenceSystem::toOgcUrn() const +{ + const auto parts { authid().split( ':' ) }; + if ( parts.length() == 2 ) + { + if ( parts[0] == QLatin1String( "EPSG" ) ) + return QStringLiteral( "urn:ogc:def:crs:EPSG:0:%1" ).arg( parts[1] ); + else if ( parts[0] == QLatin1String( "OGC" ) ) + { + return QStringLiteral( "urn:ogc:def:crs:OGC:1.3:%1" ).arg( parts[1] ); + } + else + { + QgsMessageLog::logMessage( QStringLiteral( "Error converting published CRS to URN %1: (not OGC or EPSG)" ).arg( authid() ), QStringLiteral( "CRS" ), Qgis::MessageLevel::Critical ); + } + } + else + { + QgsMessageLog::logMessage( QStringLiteral( "Error converting published CRS to URN: %1" ).arg( authid() ), QStringLiteral( "CRS" ), Qgis::MessageLevel::Critical ); + } + return QString(); +} + + void QgsCoordinateReferenceSystem::updateDefinition() { if ( !d->mIsValid ) diff --git a/src/core/proj/qgscoordinatereferencesystem.h b/src/core/proj/qgscoordinatereferencesystem.h index 194474841f04..0f6bb77e4fb5 100644 --- a/src/core/proj/qgscoordinatereferencesystem.h +++ b/src/core/proj/qgscoordinatereferencesystem.h @@ -895,6 +895,14 @@ class CORE_EXPORT QgsCoordinateReferenceSystem */ QString toOgcUri() const; + /** + * Returns the crs as OGC URN (format: urn:ogc:def:crs:OGC:1.3:CRS84) + * Returns an empty string on failure. + * + * \since QGIS 3.30 + */ + QString toOgcUrn() const; + // Mutators ----------------------------------- /** diff --git a/src/core/qgsjsonutils.cpp b/src/core/qgsjsonutils.cpp index 644ba640e4b9..0585b2666d38 100644 --- a/src/core/qgsjsonutils.cpp +++ b/src/core/qgsjsonutils.cpp @@ -245,6 +245,9 @@ json QgsJsonExporter::exportFeaturesToJsonObject( const QgsFeatureList &features { "type", "FeatureCollection" }, { "features", json::array() } }; + + QgsJsonUtils::addCrsInfo( data, mDestinationCrs ); + for ( const QgsFeature &feature : std::as_const( features ) ) { data["features"].push_back( exportFeatureToJsonObject( feature ) ); @@ -912,3 +915,14 @@ json QgsJsonUtils::exportAttributesToJsonObject( const QgsFeature &feature, QgsV } return attrs; } + +void QgsJsonUtils::addCrsInfo( json &value, const QgsCoordinateReferenceSystem &crs ) +{ + // When user request EPSG:4326 we return a compliant CRS84 lon/lat GeoJSON + // so no need to add CRS information + if ( crs.authid() == "OGC:CRS84" || crs.authid() == "EPSG:4326" ) + return; + + value["crs"]["type"] = "name"; + value["crs"]["properties"]["name"] = crs.toOgcUrn().toStdString(); +} diff --git a/src/core/qgsjsonutils.h b/src/core/qgsjsonutils.h index 3e6aa0a7dbc6..5decc9fe1fc6 100644 --- a/src/core/qgsjsonutils.h +++ b/src/core/qgsjsonutils.h @@ -423,6 +423,15 @@ class CORE_EXPORT QgsJsonUtils */ static QVariant jsonToVariant( const json &value ) SIP_SKIP; + /** + * Add \a crs information entry in \a json object regarding old GeoJSON specification format + * if it differs from OGC:CRS84 or EPSG:4326. + * According to new specification RFC 7946, coordinate reference system for all GeoJSON coordinates + * is assumed to be OGC:CRS84 but when user specifically request a different CRS, this method + * allow to add this information in the JSON output + */ + static void addCrsInfo( json &value, const QgsCoordinateReferenceSystem &crs ) SIP_SKIP; + }; #endif // QGSJSONUTILS_H diff --git a/src/server/services/wms/qgswmsrenderer.cpp b/src/server/services/wms/qgswmsrenderer.cpp index 197c22f19200..8c832e4ec393 100644 --- a/src/server/services/wms/qgswmsrenderer.cpp +++ b/src/server/services/wms/qgswmsrenderer.cpp @@ -1316,7 +1316,7 @@ namespace QgsWms else if ( infoFormat == QgsWmsParameters::Format::HTML ) ba = convertFeatureInfoToHtml( result ); else if ( infoFormat == QgsWmsParameters::Format::JSON ) - ba = convertFeatureInfoToJson( layers, result ); + ba = convertFeatureInfoToJson( layers, result, mapSettings.destinationCrs() ); else ba = result.toByteArray(); @@ -2813,7 +2813,7 @@ namespace QgsWms return featureInfoString.toUtf8(); } - QByteArray QgsRenderer::convertFeatureInfoToJson( const QList &layers, const QDomDocument &doc ) const + QByteArray QgsRenderer::convertFeatureInfoToJson( const QList &layers, const QDomDocument &doc, const QgsCoordinateReferenceSystem &destCRS ) const { json json { @@ -2915,6 +2915,8 @@ namespace QgsWms exporter.setIncludeGeometry( withGeometry ); exporter.setTransformGeometries( false ); + QgsJsonUtils::addCrsInfo( json, destCRS ); + for ( const auto &feature : std::as_const( features ) ) { const QString id = QStringLiteral( "%1.%2" ).arg( layerName ).arg( fidMap.value( feature.id() ) ); diff --git a/src/server/services/wms/qgswmsrenderer.h b/src/server/services/wms/qgswmsrenderer.h index 9efbd00e7cfb..1ad582d8069a 100644 --- a/src/server/services/wms/qgswmsrenderer.h +++ b/src/server/services/wms/qgswmsrenderer.h @@ -323,7 +323,7 @@ namespace QgsWms QByteArray convertFeatureInfoToText( const QDomDocument &doc ) const; //! Converts a feature info xml document to json - QByteArray convertFeatureInfoToJson( const QList &layers, const QDomDocument &doc ) const; + QByteArray convertFeatureInfoToJson( const QList &layers, const QDomDocument &doc, const QgsCoordinateReferenceSystem &destCRS ) const; QDomElement createFeatureGML( const QgsFeature *feat, diff --git a/tests/src/python/test_qgsserver_wms_getfeatureinfo.py b/tests/src/python/test_qgsserver_wms_getfeatureinfo.py index 2416bec2f3be..0d634f74b2b8 100644 --- a/tests/src/python/test_qgsserver_wms_getfeatureinfo.py +++ b/tests/src/python/test_qgsserver_wms_getfeatureinfo.py @@ -615,6 +615,30 @@ def testGetFeatureInfoJSON(self): 'wms_getfeatureinfo_raster_json', normalizeJson=True) + # simple test with geometry with underlying layer in 4326 and CRS is EPSG:4326 + self.wms_request_compare('GetFeatureInfo', + '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + + 'info_format=application%2Fjson&transparent=true&' + + 'width=600&height=400&srs=EPSG:4326&' + + 'bbox=44.9014173,8.2034387,44.9015094,8.2036094&' + + 'query_layers=testlayer2&X=203&Y=116&' + + 'with_geometry=true', + 'wms_getfeatureinfo_geometry_CRS84_json', + 'test_project.qgs', + normalizeJson=True) + + # simple test with geometry with underlying layer in 4326 and CRS is CRS84 + self.wms_request_compare('GetFeatureInfo', + '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + + 'info_format=application%2Fjson&transparent=true&' + + 'width=600&height=400&srs=OGC:CRS84&' + + 'bbox=8.2034387,44.9014173,8.2036094,44.9015094&' + + 'query_layers=testlayer2&X=203&Y=116&' + + 'with_geometry=true', + 'wms_getfeatureinfo_geometry_CRS84_json', + 'test_project.qgs', + normalizeJson=True) + def testGetFeatureInfoGroupedLayers(self): """Test that we can get feature info from the top and group layers""" diff --git a/tests/testdata/qgis_server/wms_getfeatureinfo_alias_json.txt b/tests/testdata/qgis_server/wms_getfeatureinfo_alias_json.txt index c6e43c20d560..445b318673b2 100644 --- a/tests/testdata/qgis_server/wms_getfeatureinfo_alias_json.txt +++ b/tests/testdata/qgis_server/wms_getfeatureinfo_alias_json.txt @@ -2,6 +2,13 @@ Content-Type: application/json; charset=utf-8 { + "crs": + { + "properties": { + "name": "urn:ogc:def:crs:EPSG:0:3857" + }, + "type": "name" + }, "features": [ { "geometry": null, diff --git a/tests/testdata/qgis_server/wms_getfeatureinfo_exclude_attribute_json.txt b/tests/testdata/qgis_server/wms_getfeatureinfo_exclude_attribute_json.txt index 1535a9702c4f..d8984c9030cc 100644 --- a/tests/testdata/qgis_server/wms_getfeatureinfo_exclude_attribute_json.txt +++ b/tests/testdata/qgis_server/wms_getfeatureinfo_exclude_attribute_json.txt @@ -2,6 +2,13 @@ Content-Type: application/json; charset=utf-8 { + "crs": + { + "properties": { + "name": "urn:ogc:def:crs:EPSG:0:3857" + }, + "type": "name" + }, "features": [ { "geometry": null, diff --git a/tests/testdata/qgis_server/wms_getfeatureinfo_geojson.txt b/tests/testdata/qgis_server/wms_getfeatureinfo_geojson.txt index b910446e47c8..3b9f8db9af7c 100644 --- a/tests/testdata/qgis_server/wms_getfeatureinfo_geojson.txt +++ b/tests/testdata/qgis_server/wms_getfeatureinfo_geojson.txt @@ -2,6 +2,13 @@ Content-Type: application/geo+json; charset=utf-8 { + "crs": + { + "properties": { + "name": "urn:ogc:def:crs:EPSG:0:3857" + }, + "type": "name" + }, "features": [ { "geometry": null, diff --git a/tests/testdata/qgis_server/wms_getfeatureinfo_geometry_CRS84_json.txt b/tests/testdata/qgis_server/wms_getfeatureinfo_geometry_CRS84_json.txt new file mode 100644 index 000000000000..1125b3ee9e3d --- /dev/null +++ b/tests/testdata/qgis_server/wms_getfeatureinfo_geometry_CRS84_json.txt @@ -0,0 +1,24 @@ +***** +Content-Type: application/json; charset=utf-8 + +{ + "features": [ + { + "geometry": { + "coordinates": [ + 8.2035, + 44.9015 + ], + "type": "Point" + }, + "id": "testlayer2.0", + "properties": { + "id": 1, + "name": "one", + "utf8nameè": "one èé" + }, + "type": "Feature" + } + ], + "type": "FeatureCollection" +} diff --git a/tests/testdata/qgis_server/wms_getfeatureinfo_geometry_json.txt b/tests/testdata/qgis_server/wms_getfeatureinfo_geometry_json.txt index f248b083459c..5204b6fe23f0 100644 --- a/tests/testdata/qgis_server/wms_getfeatureinfo_geometry_json.txt +++ b/tests/testdata/qgis_server/wms_getfeatureinfo_geometry_json.txt @@ -2,6 +2,13 @@ Content-Type: application/json; charset=utf-8 { + "crs": + { + "properties": { + "name": "urn:ogc:def:crs:EPSG:0:3857" + }, + "type": "name" + }, "features": [ { "geometry": { diff --git a/tests/testdata/qgis_server/wms_getfeatureinfo_json.txt b/tests/testdata/qgis_server/wms_getfeatureinfo_json.txt index 4ba7c4ca5db5..41cc8910893c 100644 --- a/tests/testdata/qgis_server/wms_getfeatureinfo_json.txt +++ b/tests/testdata/qgis_server/wms_getfeatureinfo_json.txt @@ -2,6 +2,13 @@ Content-Type: application/json; charset=utf-8 { + "crs": + { + "properties": { + "name": "urn:ogc:def:crs:EPSG:0:3857" + }, + "type": "name" + }, "features": [ { "geometry": null, diff --git a/tests/testdata/qgis_server/wms_getfeatureinfo_multiple_json.txt b/tests/testdata/qgis_server/wms_getfeatureinfo_multiple_json.txt index b761b71e1d83..a8709da01519 100644 --- a/tests/testdata/qgis_server/wms_getfeatureinfo_multiple_json.txt +++ b/tests/testdata/qgis_server/wms_getfeatureinfo_multiple_json.txt @@ -2,6 +2,13 @@ Content-Type: application/json; charset=utf-8 { + "crs": + { + "properties": { + "name": "urn:ogc:def:crs:EPSG:0:3857" + }, + "type": "name" + }, "features": [ { "geometry": null, From 533a7170ecd652f207c3984ce74d9f67164bbd51 Mon Sep 17 00:00:00 2001 From: Julien Cabieces Date: Wed, 6 Mar 2024 17:19:48 +0100 Subject: [PATCH 03/87] update version --- .../auto_generated/proj/qgscoordinatereferencesystem.sip.in | 2 +- .../auto_generated/proj/qgscoordinatereferencesystem.sip.in | 2 +- src/core/proj/qgscoordinatereferencesystem.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/python/PyQt6/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in b/python/PyQt6/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in index 6635d83da796..e15df9182a22 100644 --- a/python/PyQt6/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in +++ b/python/PyQt6/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in @@ -980,7 +980,7 @@ Returns an empty string on failure. Returns the crs as OGC URN (format: urn:ogc:def:crs:OGC:1.3:CRS84) Returns an empty string on failure. -.. versionadded:: 3.30 +.. versionadded:: 3.38 %End diff --git a/python/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in b/python/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in index 8cd4b270aa54..11edc46b55bd 100644 --- a/python/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in +++ b/python/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in @@ -980,7 +980,7 @@ Returns an empty string on failure. Returns the crs as OGC URN (format: urn:ogc:def:crs:OGC:1.3:CRS84) Returns an empty string on failure. -.. versionadded:: 3.30 +.. versionadded:: 3.38 %End diff --git a/src/core/proj/qgscoordinatereferencesystem.h b/src/core/proj/qgscoordinatereferencesystem.h index 0f6bb77e4fb5..aa0dc4694511 100644 --- a/src/core/proj/qgscoordinatereferencesystem.h +++ b/src/core/proj/qgscoordinatereferencesystem.h @@ -899,7 +899,7 @@ class CORE_EXPORT QgsCoordinateReferenceSystem * Returns the crs as OGC URN (format: urn:ogc:def:crs:OGC:1.3:CRS84) * Returns an empty string on failure. * - * \since QGIS 3.30 + * \since QGIS 3.38 */ QString toOgcUrn() const; From f7d5febf33faa6e63fffcc985487152b99004cd2 Mon Sep 17 00:00:00 2001 From: Julien Cabieces Date: Wed, 6 Mar 2024 19:05:13 +0100 Subject: [PATCH 04/87] [Server][WFS] Add CRS info to GeoJSON output when requested CRS is not WGS84 --- src/server/services/wfs/qgswfsgetfeature.cpp | 15 +++++++++++++++ tests/src/python/test_qgsserver_wfs.py | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/src/server/services/wfs/qgswfsgetfeature.cpp b/src/server/services/wfs/qgswfsgetfeature.cpp index a484dd1d3bab..7d7149779fa4 100644 --- a/src/server/services/wfs/qgswfsgetfeature.cpp +++ b/src/server/services/wfs/qgswfsgetfeature.cpp @@ -1174,6 +1174,21 @@ namespace QgsWfs fcString = QStringLiteral( "{\"type\": \"FeatureCollection\",\n" ); fcString += " \"bbox\": [ " + qgsDoubleToString( rect->xMinimum(), prec ) + ", " + qgsDoubleToString( rect->yMinimum(), prec ) + ", " + qgsDoubleToString( rect->xMaximum(), prec ) + ", " + qgsDoubleToString( rect->yMaximum(), prec ) + "],\n"; + + const QString srsName {request.serverParameters().value( QStringLiteral( "SRSNAME" ) )}; + const QgsCoordinateReferenceSystem destinationCrs { srsName.isEmpty( ) ? QStringLiteral( "EPSG:4326" ) : srsName }; + if ( ! destinationCrs.isValid() ) + { + throw QgsRequestNotWellFormedException( QStringLiteral( "srsName error: '%1' is not valid." ).arg( srsName ) ); + } + + json value; + QgsJsonUtils::addCrsInfo( value, destinationCrs ); + for ( auto it : value.items() ) + { + fcString += " \"" + QString::fromStdString( it.key() ) + "\": " + QString::fromStdString( it.value().dump() ) + ",\n"; + } + fcString += QLatin1String( " \"features\": [\n" ); response.write( fcString.toUtf8() ); } diff --git a/tests/src/python/test_qgsserver_wfs.py b/tests/src/python/test_qgsserver_wfs.py index f97be0916b7d..ec5ffcfcb948 100644 --- a/tests/src/python/test_qgsserver_wfs.py +++ b/tests/src/python/test_qgsserver_wfs.py @@ -787,6 +787,7 @@ def test_getFeatureFeatureJsonCrs(self): jdata['features'][0]['geometry'] jdata['features'][0]['geometry']['coordinates'] self.assertEqual(jdata['features'][0]['geometry']['coordinates'], [807305, 5592878]) + self.assertEqual(jdata['crs']['properties']['name'], "urn:ogc:def:crs:EPSG:0:3857") query_string = "?" + "&".join(["%s=%s" % i for i in list({ "SERVICE": "WFS", @@ -803,6 +804,7 @@ def test_getFeatureFeatureJsonCrs(self): jdata['features'][0]['geometry'] jdata['features'][0]['geometry']['coordinates'] self.assertEqual([int(i) for i in jdata['features'][0]['geometry']['coordinates']], [7, 44]) + self.assertFalse('crs' in jdata) query_string = "?" + "&".join(["%s=%s" % i for i in list({ "SERVICE": "WFS", @@ -834,6 +836,7 @@ def test_getFeatureFeatureJsonCrs(self): jdata['features'][0]['geometry'] jdata['features'][0]['geometry']['coordinates'] self.assertEqual([int(i) for i in jdata['features'][0]['geometry']['coordinates']], [361806, 4964192]) + self.assertEqual(jdata['crs']['properties']['name'], "urn:ogc:def:crs:EPSG:0:32632") query_string = "?" + "&".join(["%s=%s" % i for i in list({ "SERVICE": "WFS", @@ -851,6 +854,7 @@ def test_getFeatureFeatureJsonCrs(self): jdata['features'][0]['geometry'] jdata['features'][0]['geometry']['coordinates'] self.assertEqual([int(i) for i in jdata['features'][0]['geometry']['coordinates']], [812191, 5589555]) + self.assertEqual(jdata['crs']['properties']['name'], "urn:ogc:def:crs:EPSG:0:3857") def test_insert_srsName(self): """Test srsName is respected when insering""" From d45c8fe443f61cfd9ac70330cfe70912bdda4f9e Mon Sep 17 00:00:00 2001 From: Julien Cabieces Date: Wed, 6 Mar 2024 19:10:29 +0100 Subject: [PATCH 05/87] fix spellcheck --- src/core/qgsjsonutils.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/qgsjsonutils.h b/src/core/qgsjsonutils.h index 5decc9fe1fc6..6e1b793e8df2 100644 --- a/src/core/qgsjsonutils.h +++ b/src/core/qgsjsonutils.h @@ -428,7 +428,7 @@ class CORE_EXPORT QgsJsonUtils * if it differs from OGC:CRS84 or EPSG:4326. * According to new specification RFC 7946, coordinate reference system for all GeoJSON coordinates * is assumed to be OGC:CRS84 but when user specifically request a different CRS, this method - * allow to add this information in the JSON output + * adds this information in the JSON output */ static void addCrsInfo( json &value, const QgsCoordinateReferenceSystem &crs ) SIP_SKIP; From e2ea46f0e5fa718bd83869d5a6848059ae04c1df Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Sat, 9 Mar 2024 19:07:54 +0100 Subject: [PATCH 06/87] Raster - paletted: fix slow rendering with huge number of classes Fix #56652 --- src/core/raster/qgspalettedrasterrenderer.cpp | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/core/raster/qgspalettedrasterrenderer.cpp b/src/core/raster/qgspalettedrasterrenderer.cpp index 54023f1e762b..e34e425c627e 100644 --- a/src/core/raster/qgspalettedrasterrenderer.cpp +++ b/src/core/raster/qgspalettedrasterrenderer.cpp @@ -42,21 +42,33 @@ QgsPalettedRasterRenderer::QgsPalettedRasterRenderer( QgsRasterInterface *input, : QgsRasterRenderer( input, QStringLiteral( "paletted" ) ) , mBand( bandNumber ) { + + QHash>> classData; + // Prepare for the worst case, where we have to store all the values for each class + classData.reserve( classes.size() ); + // This is to keep the ordering of the labels, because hash is fast but unordered + QVector labels; + labels.reserve( classes.size() ); + for ( const Class &klass : std::as_const( classes ) ) { - MultiValueClassData::iterator it = std::find_if( mMultiValueClassData.begin(), mMultiValueClassData.end(), [&klass]( const MultiValueClass & val ) -> bool - { - return val.label == klass.label && val.color == klass.color ; - } ); - if ( it != mMultiValueClassData.end() ) + if ( !classData.contains( klass.label ) ) { - it->values.push_back( klass.value ); + labels.push_back( klass.label ); } - else + classData[klass.label][klass.color].push_back( klass.value ); + } + + mMultiValueClassData.reserve( classData.size() ); + + for ( auto labelIt = labels.constBegin(); labelIt != labels.constEnd(); ++labelIt ) + { + for ( auto colorIt = classData[*labelIt].constBegin(); colorIt != classData[*labelIt].constEnd(); ++colorIt ) { - mMultiValueClassData.push_back( MultiValueClass{ { klass.value }, klass.color, klass.label } ); + mMultiValueClassData.push_back( MultiValueClass{ colorIt.value(), colorIt.key(), *labelIt } ); } } + updateArrays(); } From 57517b72920ebe0a60be12703199756c62f8c540 Mon Sep 17 00:00:00 2001 From: t0b3 Date: Sun, 24 Mar 2024 11:12:54 +0100 Subject: [PATCH 07/87] fix: build without qtserialport closes: https://github.com/qgis/QGIS/issues/56944 Signed-off-by: t0b3 --- python/PyQt6/core/core.sip.in | 19 +++++++++++-------- python/core/core.sip.in | 17 ++++++++++------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/python/PyQt6/core/core.sip.in b/python/PyQt6/core/core.sip.in index a9a7b7a95325..165aaa4ecbb6 100644 --- a/python/PyQt6/core/core.sip.in +++ b/python/PyQt6/core/core.sip.in @@ -91,6 +91,14 @@ done: %End +%Feature HAVE_GUI +%Feature HAVE_QTSERIALPORT +%Feature HAVE_QTPRINTER +%Feature ANDROID +%Feature VECTOR_MAPPED_TYPE +%Feature HAVE_WEBENGINE_SIP +%Feature PYQT6 + %Import QtXml/QtXmlmod.sip %Import QtNetwork/QtNetworkmod.sip %Import QtSql/QtSqlmod.sip @@ -98,15 +106,10 @@ done: %Import QtPrintSupport/QtPrintSupportmod.sip %Import QtWidgets/QtWidgetsmod.sip %Import QtPositioning/QtPositioningmod.sip -%Import QtSerialPort/QtSerialPortmod.sip -%Feature HAVE_GUI -%Feature HAVE_QTSERIALPORT -%Feature HAVE_QTPRINTER -%Feature ANDROID -%Feature VECTOR_MAPPED_TYPE -%Feature HAVE_WEBENGINE_SIP -%Feature PYQT6 +%If (HAVE_QTSERIALPORT) +%Import QtSerialPort/QtSerialPortmod.sip +%End %Include conversions.sip %Include qgsexception.sip diff --git a/python/core/core.sip.in b/python/core/core.sip.in index 10c6ff3f4c5c..8378dd455203 100644 --- a/python/core/core.sip.in +++ b/python/core/core.sip.in @@ -91,6 +91,13 @@ done: %End +%Feature HAVE_GUI +%Feature HAVE_QTSERIALPORT +%Feature HAVE_QTPRINTER +%Feature ANDROID +%Feature VECTOR_MAPPED_TYPE +%Feature HAVE_WEBENGINE_SIP + %Import QtXml/QtXmlmod.sip %Import QtNetwork/QtNetworkmod.sip %Import QtSql/QtSqlmod.sip @@ -98,14 +105,10 @@ done: %Import QtPrintSupport/QtPrintSupportmod.sip %Import QtWidgets/QtWidgetsmod.sip %Import QtPositioning/QtPositioningmod.sip -%Import QtSerialPort/QtSerialPortmod.sip -%Feature HAVE_GUI -%Feature HAVE_QTSERIALPORT -%Feature HAVE_QTPRINTER -%Feature ANDROID -%Feature VECTOR_MAPPED_TYPE -%Feature HAVE_WEBENGINE_SIP +%If (HAVE_QTSERIALPORT) +%Import QtSerialPort/QtSerialPortmod.sip +%End %Include conversions.sip %Include qgsexception.sip From 27e8f96d16662d5aa4a7518b83de97806c665108 Mon Sep 17 00:00:00 2001 From: t0b3 Date: Sun, 24 Mar 2024 11:08:08 +0100 Subject: [PATCH 08/87] fix: push docker image only if GH actor == qgis Signed-off-by: t0b3 --- .github/workflows/run-tests.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 85278b3bad8e..ea22106c714e 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -116,7 +116,7 @@ jobs: echo QT_VERSION: ${QT_VERSION} - name: Login to Docker Hub - if: ${{ github.event_name == 'push' }} + if: ${{ github.event_name == 'push' && github.actor == 'qgis' }} uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} @@ -129,7 +129,7 @@ jobs: context: . file: .docker/qgis3-qt${{ matrix.qt-version }}-build-deps.dockerfile tags: qgis/qgis3-build-deps-${{ matrix.distro-version }}-qt${{ matrix.qt-version }}:${{ github.event.pull_request.base.ref || github.ref_name }} - push: ${{ github.event_name == 'push' }} + push: ${{ github.event_name == 'push' && github.actor == 'qgis' }} pull: true build-args: DISTRO_VERSION=${{ matrix.distro-version }} @@ -347,7 +347,7 @@ jobs: echo CTEST_BUILD_NAME: ${CTEST_BUILD_NAME} - name: Login to Docker Hub - if: ${{ github.event_name == 'push' }} + if: ${{ github.event_name == 'push' && github.actor == 'qgis' }} uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} @@ -360,7 +360,7 @@ jobs: context: . file: .docker/qgis3-qt${{ matrix.qt-version }}-build-deps.dockerfile tags: qgis/qgis3-qt${{ matrix.qt-version }}-build-deps-bin-only:${{ github.event.pull_request.base.ref || github.ref_name }} - push: ${{ github.event_name == 'push' }} + push: ${{ github.event_name == 'push' && github.actor == 'qgis' }} pull: true target: ${{ matrix.docker-target }} build-args: @@ -458,7 +458,7 @@ jobs: fetch-depth: 2 - name: Login to Docker Hub - if: ${{ github.event_name == 'push' }} + if: ${{ github.event_name == 'push' && github.actor == 'qgis' }} uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} @@ -471,7 +471,7 @@ jobs: context: . file: .docker/qgis3-qt${{ matrix.qt-version }}-build-deps.dockerfile tags: qgis/qgis3-qt${{ matrix.qt-version }}-build-deps-bin-only:${{ github.event.pull_request.base.ref || github.ref_name }} - push: ${{ github.event_name == 'push' }} + push: ${{ github.event_name == 'push' && github.actor == 'qgis' }} pull: true target: ${{ matrix.docker-target }} build-args: From 4d442441f8e88f7c51d3e16da7831de35c036795 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 25 Mar 2024 10:21:59 +1000 Subject: [PATCH 09/87] [sensorthings] Add support for Multidatastreams This adds support for the Multidatastream entity type, as implemented in the SensorThings version 1.1 "MultiDatastream extension" While the specification mandates that MultiDatastreams have an optional polygon geometry, I've encountered numerous servers which expose different geometry types for this entity or which return errors when attempting to read the geometries from MultiDatastreams. Accordingly we always expose an option to load MultiDatastreams as geometryless layers alongside the default option to load them as polygon layers, to handle a wider range of connections. --- python/PyQt6/core/auto_additions/qgis.py | 3 +- .../sensorthings/qgssensorthingsutils.sip.in | 9 + python/PyQt6/core/auto_generated/qgis.sip.in | 1 + python/core/auto_additions/qgis.py | 3 +- .../sensorthings/qgssensorthingsutils.sip.in | 9 + python/core/auto_generated/qgis.sip.in | 1 + .../sensorthings/qgssensorthingsdataitems.cpp | 35 +- .../sensorthings/qgssensorthingsprovider.cpp | 28 +- .../qgssensorthingsshareddata.cpp | 41 +- .../sensorthings/qgssensorthingsutils.cpp | 47 ++ .../sensorthings/qgssensorthingsutils.h | 9 + src/core/qgis.h | 1 + .../qgssensorthingssourcewidget.cpp | 42 +- .../src/python/test_provider_sensorthings.py | 607 +++++++++++++++++- 14 files changed, 817 insertions(+), 19 deletions(-) diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index 5b55e1209b68..02cb7d44a228 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -4904,6 +4904,7 @@ Qgis.SensorThingsEntity.ObservedProperty.__doc__ = "An ObservedProperty specifies the phenomenon of an Observation" Qgis.SensorThingsEntity.Observation.__doc__ = "An Observation is the act of measuring or otherwise determining the value of a property" Qgis.SensorThingsEntity.FeatureOfInterest.__doc__ = "In the context of the Internet of Things, many Observations’ FeatureOfInterest can be the Location of the Thing. For example, the FeatureOfInterest of a wifi-connect thermostat can be the Location of the thermostat (i.e., the living room where the thermostat is located in). In the case of remote sensing, the FeatureOfInterest can be the geographical area or volume that is being sensed" -Qgis.SensorThingsEntity.__doc__ = "OGC SensorThings API entity types.\n\n.. versionadded:: 3.36\n\n" + '* ``Invalid``: ' + Qgis.SensorThingsEntity.Invalid.__doc__ + '\n' + '* ``Thing``: ' + Qgis.SensorThingsEntity.Thing.__doc__ + '\n' + '* ``Location``: ' + Qgis.SensorThingsEntity.Location.__doc__ + '\n' + '* ``HistoricalLocation``: ' + Qgis.SensorThingsEntity.HistoricalLocation.__doc__ + '\n' + '* ``Datastream``: ' + Qgis.SensorThingsEntity.Datastream.__doc__ + '\n' + '* ``Sensor``: ' + Qgis.SensorThingsEntity.Sensor.__doc__ + '\n' + '* ``ObservedProperty``: ' + Qgis.SensorThingsEntity.ObservedProperty.__doc__ + '\n' + '* ``Observation``: ' + Qgis.SensorThingsEntity.Observation.__doc__ + '\n' + '* ``FeatureOfInterest``: ' + Qgis.SensorThingsEntity.FeatureOfInterest.__doc__ +Qgis.SensorThingsEntity.MultiDatastream.__doc__ = "A MultiDatastream groups a collection of Observations and the Observations in a MultiDatastream have a complex result type. Implemented in the SensorThings version 1.1 \"MultiDatastream extension\". (Since QGIS 3.38)" +Qgis.SensorThingsEntity.__doc__ = "OGC SensorThings API entity types.\n\n.. versionadded:: 3.36\n\n" + '* ``Invalid``: ' + Qgis.SensorThingsEntity.Invalid.__doc__ + '\n' + '* ``Thing``: ' + Qgis.SensorThingsEntity.Thing.__doc__ + '\n' + '* ``Location``: ' + Qgis.SensorThingsEntity.Location.__doc__ + '\n' + '* ``HistoricalLocation``: ' + Qgis.SensorThingsEntity.HistoricalLocation.__doc__ + '\n' + '* ``Datastream``: ' + Qgis.SensorThingsEntity.Datastream.__doc__ + '\n' + '* ``Sensor``: ' + Qgis.SensorThingsEntity.Sensor.__doc__ + '\n' + '* ``ObservedProperty``: ' + Qgis.SensorThingsEntity.ObservedProperty.__doc__ + '\n' + '* ``Observation``: ' + Qgis.SensorThingsEntity.Observation.__doc__ + '\n' + '* ``FeatureOfInterest``: ' + Qgis.SensorThingsEntity.FeatureOfInterest.__doc__ + '\n' + '* ``MultiDatastream``: ' + Qgis.SensorThingsEntity.MultiDatastream.__doc__ # -- Qgis.SensorThingsEntity.baseClass = Qgis diff --git a/python/PyQt6/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in b/python/PyQt6/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in index abfde62514bf..3df39d7b2121 100644 --- a/python/PyQt6/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in +++ b/python/PyQt6/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in @@ -55,6 +55,15 @@ Returns the geometry field for a specified entity ``type``. static bool entityTypeHasGeometry( Qgis::SensorThingsEntity type ); %Docstring Returns ``True`` if the specified entity ``type`` can have geometry attached. +%End + + static Qgis::GeometryType geometryTypeForEntity( Qgis::SensorThingsEntity type ); +%Docstring +Returns the geometry type for if the specified entity ``type``. + +If there are no restrictions on the geometry type an ntity can have :py:class:`Qgis`.GeometryType.Unknown will be returned. + +.. versionadded:: 3.38 %End static QString filterForWkbType( Qgis::SensorThingsEntity entityType, Qgis::WkbType wkbType ); diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index b065f97f1ecb..38b07fa1399c 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -2751,6 +2751,7 @@ The development version ObservedProperty, Observation, FeatureOfInterest, + MultiDatastream, }; static const double DEFAULT_SEARCH_RADIUS_MM; diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index 4c38737929fe..4918dd1f3f26 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -4826,7 +4826,8 @@ Qgis.SensorThingsEntity.ObservedProperty.__doc__ = "An ObservedProperty specifies the phenomenon of an Observation" Qgis.SensorThingsEntity.Observation.__doc__ = "An Observation is the act of measuring or otherwise determining the value of a property" Qgis.SensorThingsEntity.FeatureOfInterest.__doc__ = "In the context of the Internet of Things, many Observations’ FeatureOfInterest can be the Location of the Thing. For example, the FeatureOfInterest of a wifi-connect thermostat can be the Location of the thermostat (i.e., the living room where the thermostat is located in). In the case of remote sensing, the FeatureOfInterest can be the geographical area or volume that is being sensed" -Qgis.SensorThingsEntity.__doc__ = "OGC SensorThings API entity types.\n\n.. versionadded:: 3.36\n\n" + '* ``Invalid``: ' + Qgis.SensorThingsEntity.Invalid.__doc__ + '\n' + '* ``Thing``: ' + Qgis.SensorThingsEntity.Thing.__doc__ + '\n' + '* ``Location``: ' + Qgis.SensorThingsEntity.Location.__doc__ + '\n' + '* ``HistoricalLocation``: ' + Qgis.SensorThingsEntity.HistoricalLocation.__doc__ + '\n' + '* ``Datastream``: ' + Qgis.SensorThingsEntity.Datastream.__doc__ + '\n' + '* ``Sensor``: ' + Qgis.SensorThingsEntity.Sensor.__doc__ + '\n' + '* ``ObservedProperty``: ' + Qgis.SensorThingsEntity.ObservedProperty.__doc__ + '\n' + '* ``Observation``: ' + Qgis.SensorThingsEntity.Observation.__doc__ + '\n' + '* ``FeatureOfInterest``: ' + Qgis.SensorThingsEntity.FeatureOfInterest.__doc__ +Qgis.SensorThingsEntity.MultiDatastream.__doc__ = "A MultiDatastream groups a collection of Observations and the Observations in a MultiDatastream have a complex result type. Implemented in the SensorThings version 1.1 \"MultiDatastream extension\". (Since QGIS 3.38)" +Qgis.SensorThingsEntity.__doc__ = "OGC SensorThings API entity types.\n\n.. versionadded:: 3.36\n\n" + '* ``Invalid``: ' + Qgis.SensorThingsEntity.Invalid.__doc__ + '\n' + '* ``Thing``: ' + Qgis.SensorThingsEntity.Thing.__doc__ + '\n' + '* ``Location``: ' + Qgis.SensorThingsEntity.Location.__doc__ + '\n' + '* ``HistoricalLocation``: ' + Qgis.SensorThingsEntity.HistoricalLocation.__doc__ + '\n' + '* ``Datastream``: ' + Qgis.SensorThingsEntity.Datastream.__doc__ + '\n' + '* ``Sensor``: ' + Qgis.SensorThingsEntity.Sensor.__doc__ + '\n' + '* ``ObservedProperty``: ' + Qgis.SensorThingsEntity.ObservedProperty.__doc__ + '\n' + '* ``Observation``: ' + Qgis.SensorThingsEntity.Observation.__doc__ + '\n' + '* ``FeatureOfInterest``: ' + Qgis.SensorThingsEntity.FeatureOfInterest.__doc__ + '\n' + '* ``MultiDatastream``: ' + Qgis.SensorThingsEntity.MultiDatastream.__doc__ # -- Qgis.SensorThingsEntity.baseClass = Qgis from enum import Enum diff --git a/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in b/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in index abfde62514bf..3df39d7b2121 100644 --- a/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in +++ b/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in @@ -55,6 +55,15 @@ Returns the geometry field for a specified entity ``type``. static bool entityTypeHasGeometry( Qgis::SensorThingsEntity type ); %Docstring Returns ``True`` if the specified entity ``type`` can have geometry attached. +%End + + static Qgis::GeometryType geometryTypeForEntity( Qgis::SensorThingsEntity type ); +%Docstring +Returns the geometry type for if the specified entity ``type``. + +If there are no restrictions on the geometry type an ntity can have :py:class:`Qgis`.GeometryType.Unknown will be returned. + +.. versionadded:: 3.38 %End static QString filterForWkbType( Qgis::SensorThingsEntity entityType, Qgis::WkbType wkbType ); diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index 8005b5ec3f02..965636863fe2 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -2751,6 +2751,7 @@ The development version ObservedProperty, Observation, FeatureOfInterest, + MultiDatastream, }; static const double DEFAULT_SEARCH_RADIUS_MM; diff --git a/src/core/providers/sensorthings/qgssensorthingsdataitems.cpp b/src/core/providers/sensorthings/qgssensorthingsdataitems.cpp index 2c9cfe152399..387f7e8925af 100644 --- a/src/core/providers/sensorthings/qgssensorthingsdataitems.cpp +++ b/src/core/providers/sensorthings/qgssensorthingsdataitems.cpp @@ -86,6 +86,7 @@ QVector QgsSensorThingsConnectionItem::createChildren() Qgis::SensorThingsEntity::ObservedProperty, Qgis::SensorThingsEntity::Observation, Qgis::SensorThingsEntity::FeatureOfInterest, + Qgis::SensorThingsEntity::MultiDatastream, } ) { QVariantMap entityUriParts = connectionUriParts; @@ -138,13 +139,28 @@ QVector QgsSensorThingsEntityContainerItem::createChildren() QVector children; int sortKey = 1; - for ( const Qgis::WkbType wkbType : - { - Qgis::WkbType::Point, - Qgis::WkbType::MultiPoint, - Qgis::WkbType::MultiLineString, - Qgis::WkbType::MultiPolygon - } ) + QList< Qgis::WkbType > compatibleTypes; + // we always expose "no geometry" types for these, even though they have a restricted fixed type + // according to the spec. This is because not all services respect the mandated geometry types! + switch ( QgsSensorThingsUtils::geometryTypeForEntity( mEntityType ) ) + { + case Qgis::GeometryType::Point: + compatibleTypes << Qgis::WkbType::Point << Qgis::WkbType::MultiPoint << Qgis::WkbType::NoGeometry; + break; + case Qgis::GeometryType::Line: + compatibleTypes << Qgis::WkbType::MultiLineString << Qgis::WkbType::NoGeometry; + break; + case Qgis::GeometryType::Polygon: + compatibleTypes << Qgis::WkbType::MultiPolygon << Qgis::WkbType::NoGeometry; + break; + case Qgis::GeometryType::Unknown: + compatibleTypes << Qgis::WkbType::Point << Qgis::WkbType::MultiPoint << Qgis::WkbType::MultiLineString << Qgis::WkbType::MultiPolygon; + break; + case Qgis::GeometryType::Null: + compatibleTypes << Qgis::WkbType::NoGeometry;; + } + + for ( const Qgis::WkbType wkbType : std::as_const( compatibleTypes ) ) { QVariantMap geometryUriParts = mEntityUriParts; QString name; @@ -171,6 +187,11 @@ QVector QgsSensorThingsEntityContainerItem::createChildren() name = tr( "Polygons" ); layerType = Qgis::BrowserLayerType::Polygon; break; + case Qgis::WkbType::NoGeometry: + geometryUriParts.remove( QStringLiteral( "geometryType" ) ); + name = tr( "No Geometry" ); + layerType = Qgis::BrowserLayerType::TableLayer; + break; default: break; } diff --git a/src/core/providers/sensorthings/qgssensorthingsprovider.cpp b/src/core/providers/sensorthings/qgssensorthingsprovider.cpp index c290bb9b00d8..7442a50a57a4 100644 --- a/src/core/providers/sensorthings/qgssensorthingsprovider.cpp +++ b/src/core/providers/sensorthings/qgssensorthingsprovider.cpp @@ -18,7 +18,6 @@ #include "qgssensorthingsprovider.h" #include "qgssensorthingsutils.h" #include "qgsapplication.h" -#include "qgsnetworkaccessmanager.h" #include "qgssetrequestinitiator_p.h" #include "qgsblockingnetworkrequest.h" #include "qgsthreadingutils.h" @@ -26,6 +25,7 @@ #include "qgssensorthingsfeatureiterator.h" #include "qgssensorthingsdataitems.h" #include "qgssensorthingsconnection.h" +#include "qgsmessagelog.h" #include #include @@ -99,7 +99,28 @@ QgsSensorThingsProvider::QgsSensorThingsProvider( const QString &uri, const Prov if ( !foundMatchingEntity ) { - appendError( QgsErrorMessage( tr( "Could not find url for %1" ).arg( qgsEnumValueToKey( mSharedData->mEntityType ) ), QStringLiteral( "SensorThings" ) ) ); + switch ( mSharedData->mEntityType ) + { + + case Qgis::SensorThingsEntity::Invalid: + case Qgis::SensorThingsEntity::Thing: + case Qgis::SensorThingsEntity::Location: + case Qgis::SensorThingsEntity::HistoricalLocation: + case Qgis::SensorThingsEntity::Datastream: + case Qgis::SensorThingsEntity::Sensor: + case Qgis::SensorThingsEntity::ObservedProperty: + case Qgis::SensorThingsEntity::Observation: + case Qgis::SensorThingsEntity::FeatureOfInterest: + appendError( QgsErrorMessage( tr( "Could not find url for %1" ).arg( qgsEnumValueToKey( mSharedData->mEntityType ) ), QStringLiteral( "SensorThings" ) ) ); + QgsMessageLog::logMessage( tr( "Could not find url for %1" ).arg( qgsEnumValueToKey( mSharedData->mEntityType ) ), tr( "SensorThings" ) ); + break; + + case Qgis::SensorThingsEntity::MultiDatastream: + appendError( QgsErrorMessage( tr( "MultiDatastreams are not supported by this connection" ), QStringLiteral( "SensorThings" ) ) ); + QgsMessageLog::logMessage( tr( "MultiDatastreams are not supported by this connection" ), tr( "SensorThings" ) ); + break; + } + return; } } @@ -306,8 +327,7 @@ QgsSensorThingsProviderMetadata::QgsSensorThingsProviderMetadata(): QIcon QgsSensorThingsProviderMetadata::icon() const { - // TODO - return QgsApplication::getThemeIcon( QStringLiteral( "mIconAfs.svg" ) ); + return QgsApplication::getThemeIcon( QStringLiteral( "mIconSensorThings.svg" ) ); } QList QgsSensorThingsProviderMetadata::dataItemProviders() const diff --git a/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp b/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp index 198ee4aef852..9caacd3e6ef1 100644 --- a/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp +++ b/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp @@ -62,8 +62,15 @@ QgsSensorThingsSharedData::QgsSensorThingsSharedData( const QString &uri ) { mGeometryType = Qgis::WkbType::MultiPolygonZ; } - // geometry is always GeoJSON spec (for now, at least), so CRS will always be WGS84 - mSourceCRS = QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ); + else if ( !uriParts.contains( QStringLiteral( "geometryType" ) ) ) + { + mGeometryType = Qgis::WkbType::NoGeometry; + } + if ( mGeometryType != Qgis::WkbType::NoGeometry ) + { + // geometry is always GeoJSON spec (for now, at least), so CRS will always be WGS84 + mSourceCRS = QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ); + } } else { @@ -484,6 +491,14 @@ bool QgsSensorThingsSharedData::processFeatureRequest( QString &nextPage, QgsFee return QgsJsonUtils::jsonToVariant( json[tag] ); }; + auto getVariantList = []( const basic_json<> &json, const char *tag ) -> QVariant + { + if ( !json.contains( tag ) ) + return QVariant(); + + return QgsJsonUtils::jsonToVariant( json[tag] ); + }; + auto getStringList = []( const basic_json<> &json, const char *tag ) -> QVariant { if ( !json.contains( tag ) ) @@ -659,6 +674,28 @@ bool QgsSensorThingsSharedData::processFeatureRequest( QString &nextPage, QgsFee << properties ); break; + + case Qgis::SensorThingsEntity::MultiDatastream: + { + std::pair< QVariant, QVariant > phenomenonTime = getDateTimeRange( featureData, "phenomenonTime" ); + std::pair< QVariant, QVariant > resultTime = getDateTimeRange( featureData, "resultTime" ); + feature.setAttributes( + QgsAttributes() + << iotId + << selfLink + << getString( featureData, "name" ) + << getString( featureData, "description" ) + << getVariantList( featureData, "unitOfMeasurements" ) + << getString( featureData, "observationType" ) + << getStringList( featureData, "multiObservationDataTypes" ) + << properties + << phenomenonTime.first + << phenomenonTime.second + << resultTime.first + << resultTime.second + ); + break; + } } // NOLINTEND(bugprone-branch-clone) diff --git a/src/core/providers/sensorthings/qgssensorthingsutils.cpp b/src/core/providers/sensorthings/qgssensorthingsutils.cpp index b4fd025fbcbd..87f9ba757a44 100644 --- a/src/core/providers/sensorthings/qgssensorthingsutils.cpp +++ b/src/core/providers/sensorthings/qgssensorthingsutils.cpp @@ -45,6 +45,8 @@ Qgis::SensorThingsEntity QgsSensorThingsUtils::stringToEntity( const QString &ty return Qgis::SensorThingsEntity::Observation; if ( trimmed.compare( QLatin1String( "FeatureOfInterest" ), Qt::CaseInsensitive ) == 0 ) return Qgis::SensorThingsEntity::FeatureOfInterest; + if ( trimmed.compare( QLatin1String( "MultiDatastream" ), Qt::CaseInsensitive ) == 0 ) + return Qgis::SensorThingsEntity::MultiDatastream; return Qgis::SensorThingsEntity::Invalid; } @@ -71,6 +73,8 @@ QString QgsSensorThingsUtils::displayString( Qgis::SensorThingsEntity type, bool return plural ? QObject::tr( "Observations" ) : QObject::tr( "Observation" ); case Qgis::SensorThingsEntity::FeatureOfInterest: return plural ? QObject::tr( "Features of Interest" ) : QObject::tr( "Feature of Interest" ); + case Qgis::SensorThingsEntity::MultiDatastream: + return plural ? QObject::tr( "MultiDatastreams" ) : QObject::tr( "MultiDatastream" ); } BUILTIN_UNREACHABLE } @@ -94,6 +98,8 @@ Qgis::SensorThingsEntity QgsSensorThingsUtils::entitySetStringToEntity( const QS return Qgis::SensorThingsEntity::Observation; if ( trimmed.compare( QLatin1String( "FeaturesOfInterest" ), Qt::CaseInsensitive ) == 0 ) return Qgis::SensorThingsEntity::FeatureOfInterest; + if ( trimmed.compare( QLatin1String( "MultiDatastreams" ), Qt::CaseInsensitive ) == 0 ) + return Qgis::SensorThingsEntity::MultiDatastream; return Qgis::SensorThingsEntity::Invalid; } @@ -180,6 +186,20 @@ QgsFields QgsSensorThingsUtils::fieldsForEntityType( Qgis::SensorThingsEntity ty fields.append( QgsField( QStringLiteral( "description" ), QVariant::String ) ); fields.append( QgsField( QStringLiteral( "properties" ), QVariant::Map, QStringLiteral( "json" ), 0, 0, QString(), QVariant::String ) ); break; + + case Qgis::SensorThingsEntity::MultiDatastream: + // https://docs.ogc.org/is/18-088/18-088.html#multidatastream-extension + fields.append( QgsField( QStringLiteral( "name" ), QVariant::String ) ); + fields.append( QgsField( QStringLiteral( "description" ), QVariant::String ) ); + fields.append( QgsField( QStringLiteral( "unitOfMeasurements" ), QVariant::Map, QStringLiteral( "json" ), 0, 0, QString(), QVariant::String ) ); + fields.append( QgsField( QStringLiteral( "observationType" ), QVariant::String ) ); + fields.append( QgsField( QStringLiteral( "multiObservationDataTypes" ), QVariant::StringList, QString(), 0, 0, QString(), QVariant::String ) ); + fields.append( QgsField( QStringLiteral( "properties" ), QVariant::Map, QStringLiteral( "json" ), 0, 0, QString(), QVariant::String ) ); + fields.append( QgsField( QStringLiteral( "phenomenonTimeStart" ), QVariant::DateTime ) ); + fields.append( QgsField( QStringLiteral( "phenomenonTimeEnd" ), QVariant::DateTime ) ); + fields.append( QgsField( QStringLiteral( "resultTimeStart" ), QVariant::DateTime ) ); + fields.append( QgsField( QStringLiteral( "resultTimeEnd" ), QVariant::DateTime ) ); + break; } return fields; @@ -203,6 +223,9 @@ QString QgsSensorThingsUtils::geometryFieldForEntityType( Qgis::SensorThingsEnti case Qgis::SensorThingsEntity::FeatureOfInterest: return QStringLiteral( "feature" ); + + case Qgis::SensorThingsEntity::MultiDatastream: + return QStringLiteral( "observedArea" ); } BUILTIN_UNREACHABLE } @@ -222,11 +245,35 @@ bool QgsSensorThingsUtils::entityTypeHasGeometry( Qgis::SensorThingsEntity type case Qgis::SensorThingsEntity::Location: case Qgis::SensorThingsEntity::FeatureOfInterest: + case Qgis::SensorThingsEntity::MultiDatastream: return true; } BUILTIN_UNREACHABLE } +Qgis::GeometryType QgsSensorThingsUtils::geometryTypeForEntity( Qgis::SensorThingsEntity type ) +{ + switch ( type ) + { + case Qgis::SensorThingsEntity::Invalid: + case Qgis::SensorThingsEntity::Thing: + case Qgis::SensorThingsEntity::HistoricalLocation: + case Qgis::SensorThingsEntity::Datastream: + case Qgis::SensorThingsEntity::Sensor: + case Qgis::SensorThingsEntity::Observation: + case Qgis::SensorThingsEntity::ObservedProperty: + return Qgis::GeometryType::Null; + + case Qgis::SensorThingsEntity::Location: + case Qgis::SensorThingsEntity::FeatureOfInterest: + return Qgis::GeometryType::Unknown; + + case Qgis::SensorThingsEntity::MultiDatastream: + return Qgis::GeometryType::Polygon; + } + BUILTIN_UNREACHABLE +} + QString QgsSensorThingsUtils::filterForWkbType( Qgis::SensorThingsEntity entityType, Qgis::WkbType wkbType ) { QString geometryTypeString; diff --git a/src/core/providers/sensorthings/qgssensorthingsutils.h b/src/core/providers/sensorthings/qgssensorthingsutils.h index c1cd617f2f8e..873abe1afa91 100644 --- a/src/core/providers/sensorthings/qgssensorthingsutils.h +++ b/src/core/providers/sensorthings/qgssensorthingsutils.h @@ -76,6 +76,15 @@ class CORE_EXPORT QgsSensorThingsUtils */ static bool entityTypeHasGeometry( Qgis::SensorThingsEntity type ); + /** + * Returns the geometry type for if the specified entity \a type. + * + * If there are no restrictions on the geometry type an ntity can have Qgis::GeometryType::Unknown will be returned. + * + * \since QGIS 3.38 + */ + static Qgis::GeometryType geometryTypeForEntity( Qgis::SensorThingsEntity type ); + /** * Returns a filter string which restricts results to those matching the specified * \a entityType and \a wkbType. diff --git a/src/core/qgis.h b/src/core/qgis.h index 3993ca3e9a32..56a599e59dcb 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -4872,6 +4872,7 @@ class CORE_EXPORT Qgis ObservedProperty, //!< An ObservedProperty specifies the phenomenon of an Observation Observation, //!< An Observation is the act of measuring or otherwise determining the value of a property FeatureOfInterest, //!< In the context of the Internet of Things, many Observations’ FeatureOfInterest can be the Location of the Thing. For example, the FeatureOfInterest of a wifi-connect thermostat can be the Location of the thermostat (i.e., the living room where the thermostat is located in). In the case of remote sensing, the FeatureOfInterest can be the geographical area or volume that is being sensed + MultiDatastream, //!< A MultiDatastream groups a collection of Observations and the Observations in a MultiDatastream have a complex result type. Implemented in the SensorThings version 1.1 "MultiDatastream extension". (Since QGIS 3.38) }; Q_ENUM( SensorThingsEntity ) diff --git a/src/gui/providers/sensorthings/qgssensorthingssourcewidget.cpp b/src/gui/providers/sensorthings/qgssensorthingssourcewidget.cpp index 8f4af395ed13..fd73b8fac82c 100644 --- a/src/gui/providers/sensorthings/qgssensorthingssourcewidget.cpp +++ b/src/gui/providers/sensorthings/qgssensorthingssourcewidget.cpp @@ -57,6 +57,7 @@ QgsSensorThingsSourceWidget::QgsSensorThingsSourceWidget( QWidget *parent ) Qgis::SensorThingsEntity::ObservedProperty, Qgis::SensorThingsEntity::Observation, Qgis::SensorThingsEntity::FeatureOfInterest, + Qgis::SensorThingsEntity::MultiDatastream, } ) { mComboEntityType->addItem( QgsSensorThingsUtils::displayString( type, true ), QVariant::fromValue( type ) ); @@ -190,6 +191,9 @@ QString QgsSensorThingsSourceWidget::updateUriFromGui( const QString &connection case Qgis::WkbType::MultiPolygon: parts.insert( QStringLiteral( "geometryType" ), QStringLiteral( "polygon" ) ); break; + case Qgis::WkbType::NoGeometry: + parts.remove( QStringLiteral( "geometryType" ) ); + break; default: break; } @@ -297,8 +301,10 @@ void QgsSensorThingsSourceWidget::rebuildGeometryTypes( Qgis::SensorThingsEntity mPropertiesTask = nullptr; } - mRetrieveTypesButton->setEnabled( QgsSensorThingsUtils::entityTypeHasGeometry( type ) && !mSourceParts.value( QStringLiteral( "url" ) ).toString().isEmpty() ); - if ( QgsSensorThingsUtils::entityTypeHasGeometry( type ) && mComboGeometryType->findData( QVariant::fromValue( Qgis::WkbType::Point ) ) < 0 ) + mRetrieveTypesButton->setEnabled( QgsSensorThingsUtils::geometryTypeForEntity( type ) == Qgis::GeometryType::Unknown + && !mSourceParts.value( QStringLiteral( "url" ) ).toString().isEmpty() ); + const Qgis::GeometryType geometryTypeForEntity = QgsSensorThingsUtils::geometryTypeForEntity( type ); + if ( geometryTypeForEntity == Qgis::GeometryType::Unknown && mComboGeometryType->findData( QVariant::fromValue( Qgis::WkbType::Point ) ) < 0 ) { mComboGeometryType->clear(); mComboGeometryType->addItem( QgsIconUtils::iconForWkbType( Qgis::WkbType::Point ), tr( "Point" ), QVariant::fromValue( Qgis::WkbType::Point ) ); @@ -307,11 +313,37 @@ void QgsSensorThingsSourceWidget::rebuildGeometryTypes( Qgis::SensorThingsEntity mComboGeometryType->addItem( QgsIconUtils::iconForWkbType( Qgis::WkbType::MultiPolygon ), tr( "Polygon" ), QVariant::fromValue( Qgis::WkbType::MultiPolygon ) ); setCurrentGeometryTypeFromString( mSourceParts.value( QStringLiteral( "geometryType" ) ).toString() ); } - else if ( !QgsSensorThingsUtils::entityTypeHasGeometry( type ) && mComboGeometryType->findData( QVariant::fromValue( Qgis::WkbType::NoGeometry ) ) < 0 ) + else if ( geometryTypeForEntity == Qgis::GeometryType::Null && mComboGeometryType->findData( QVariant::fromValue( Qgis::WkbType::NoGeometry ) ) < 0 ) { mComboGeometryType->clear(); mComboGeometryType->addItem( QgsIconUtils::iconForWkbType( Qgis::WkbType::NoGeometry ), tr( "No Geometry" ), QVariant::fromValue( Qgis::WkbType::NoGeometry ) ); } + else if ( ( geometryTypeForEntity != Qgis::GeometryType::Null && geometryTypeForEntity != Qgis::GeometryType::Unknown ) + && mComboGeometryType->findData( QVariant::fromValue( geometryTypeForEntity ) ) < 0 ) + { + mComboGeometryType->clear(); + switch ( geometryTypeForEntity ) + { + case Qgis::GeometryType::Point: + mComboGeometryType->addItem( QgsIconUtils::iconForWkbType( Qgis::WkbType::Point ), tr( "Point" ), QVariant::fromValue( Qgis::WkbType::Point ) ); + mComboGeometryType->addItem( QgsIconUtils::iconForWkbType( Qgis::WkbType::MultiPoint ), tr( "Multipoint" ), QVariant::fromValue( Qgis::WkbType::MultiPoint ) ); + break; + case Qgis::GeometryType::Line: + mComboGeometryType->addItem( QgsIconUtils::iconForWkbType( Qgis::WkbType::MultiLineString ), tr( "Line" ), QVariant::fromValue( Qgis::WkbType::MultiLineString ) ); + break; + + case Qgis::GeometryType::Polygon: + mComboGeometryType->addItem( QgsIconUtils::iconForWkbType( Qgis::WkbType::MultiPolygon ), tr( "Polygon" ), QVariant::fromValue( Qgis::WkbType::MultiPolygon ) ); + break; + + case Qgis::GeometryType::Unknown: + case Qgis::GeometryType::Null: + break; + } + // we always add a "no geometry" option here, as some services don't correctly respect the mandated geometry types for eg MultiDatastreams + mComboGeometryType->addItem( QgsIconUtils::iconForWkbType( Qgis::WkbType::NoGeometry ), tr( "No Geometry" ), QVariant::fromValue( Qgis::WkbType::NoGeometry ) ); + setCurrentGeometryTypeFromString( mSourceParts.value( QStringLiteral( "geometryType" ) ).toString() ); mComboGeometryType->setCurrentIndex( 0 ); + } } void QgsSensorThingsSourceWidget::setCurrentGeometryTypeFromString( const QString &geometryType ) @@ -332,6 +364,10 @@ void QgsSensorThingsSourceWidget::setCurrentGeometryTypeFromString( const QStrin { mComboGeometryType->setCurrentIndex( mComboGeometryType->findData( QVariant::fromValue( Qgis::WkbType::MultiPolygon ) ) ); } + else if ( geometryType.isEmpty() && mComboGeometryType->findData( QVariant::fromValue( Qgis::WkbType::NoGeometry ) ) >= 0 ) + { + mComboGeometryType->setCurrentIndex( mComboGeometryType->findData( QVariant::fromValue( Qgis::WkbType::NoGeometry ) ) ); + } else if ( geometryType.isEmpty() && mComboGeometryType->currentIndex() < 0 ) { mComboGeometryType->setCurrentIndex( 0 ); diff --git a/tests/src/python/test_provider_sensorthings.py b/tests/src/python/test_provider_sensorthings.py index a3fe8e306dd0..b5c03fe038cd 100644 --- a/tests/src/python/test_provider_sensorthings.py +++ b/tests/src/python/test_provider_sensorthings.py @@ -31,7 +31,8 @@ def sanitize(endpoint, x): for prefix in ('/Locations', '/HistoricalLocations', '/Things', - '/FeaturesOfInterest'): + '/FeaturesOfInterest', + '/MultiDatastreams'): if x.startswith(prefix): x = x[len(prefix):] endpoint = endpoint + "_" + prefix[1:] @@ -99,6 +100,10 @@ def test_filter_for_wkb_type(self): QgsSensorThingsUtils.filterForWkbType(Qgis.SensorThingsEntity.Location, Qgis.WkbType.LineString), "location/type eq 'LineString' or location/geometry/type eq 'LineString'" ) + self.assertEqual( + QgsSensorThingsUtils.filterForWkbType(Qgis.SensorThingsEntity.MultiDatastream, Qgis.WkbType.Polygon), + "observedArea/type eq 'Polygon' or observedArea/geometry/type eq 'Polygon'" + ) def test_utils_string_to_entity(self): self.assertEqual( @@ -136,6 +141,10 @@ def test_utils_string_to_entity(self): QgsSensorThingsUtils.stringToEntity(" FeatureOfInterest "), Qgis.SensorThingsEntity.FeatureOfInterest, ) + self.assertEqual( + QgsSensorThingsUtils.stringToEntity(" MultiDataStream "), + Qgis.SensorThingsEntity.MultiDatastream, + ) def test_utils_string_to_entityset(self): self.assertEqual( @@ -174,6 +183,10 @@ def test_utils_string_to_entityset(self): QgsSensorThingsUtils.entitySetStringToEntity(" FeaturesOfInterest "), Qgis.SensorThingsEntity.FeatureOfInterest, ) + self.assertEqual( + QgsSensorThingsUtils.entitySetStringToEntity(" MultidataStreams "), + Qgis.SensorThingsEntity.MultiDatastream, + ) def test_filter_for_extent(self): """ @@ -2973,6 +2986,598 @@ def test_feature_of_interest(self): ['Point (16.4 48.2)', 'Point (16.5 48.2)', 'Point (16.5 48.2)'], ) + def test_multidatastream_no_geometry(self): + """ + Test a layer retrieving 'MultiDatastream' entities from a service without geometry + """ + with tempfile.TemporaryDirectory() as temp_dir: + base_path = temp_dir.replace("\\", "/") + endpoint = base_path + "/fake_qgis_http_endpoint" + with open(sanitize(endpoint, ""), "wt", encoding="utf8") as f: + f.write( + """ +{ + "value": [ + { + "name": "MultiDatastreams", + "url": "endpoint/MultiDatastreams" + } + ], + "serverSettings": { + } +}""".replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, "/MultiDatastreams?$top=0&$count=true"), + "wt", + encoding="utf8", + ) as f: + f.write("""{"@iot.count":3,"value":[]}""") + + with open( + sanitize(endpoint, "/MultiDatastreams?$top=2&$count=false"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ +{ + "value": [ + { + "@iot.selfLink": "endpoint/MultiDatastreams(1)", + "@iot.id": 1, + "name": "MultiDatastream 1", + "description": "Desc 1", + "unitOfMeasurements": [ + { + "name": "ug.m-3", + "symbol": "ug.m-3", + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3" + } + ], + "observationType": "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "multiObservationDataTypes": ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + "phenomenonTime": "2017-12-31T23:00:00Z/2018-01-12T04:00:00Z", + "resultTime": "2017-12-31T23:30:00Z/2017-12-31T23:31:00Z", + "properties": { + "owner": "owner 1" + }, + "Things@iot.navigationLink": "endpoint/MultiDatastreams(1)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/MultiDatastreams(1)/HistoricalLocations" + }, + { + "@iot.selfLink": "endpoint/MultiDatastreams(2)", + "@iot.id": 2, + "name": "MultiDatastream 2", + "description": "Desc 2", + "unitOfMeasurements": [ + { + "name": "ug.m-3", + "symbol": "ug.m-3", + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3" + }], + "observationType": "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "multiObservationDataTypes": ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + "phenomenonTime": "2018-12-31T23:00:00Z/2019-01-12T04:00:00Z", + "resultTime": "2018-12-31T23:30:00Z/2018-12-31T23:31:00Z", + "properties": { + "owner": "owner 2" + }, + "Things@iot.navigationLink": "endpoint/MultiDatastreams(2)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/MultiDatastreams(2)/HistoricalLocations" + + } + ], + "@iot.nextLink": "endpoint/MultiDatastreams?$top=2&$skip=2" +} + """.replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, "/MultiDatastreams?$top=2&$skip=2"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ + { + "value": [ + { + "@iot.selfLink": "endpoint/MultiDatastreams(3)", + "@iot.id": 3, + "name": "MultiDatastream 3", + "description": "Desc 3", + "unitOfMeasurements": [{ + "name": "ug.m-3", + "symbol": "ug.m-3", + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3" + }], + "observationType": "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "multiObservationDataTypes": ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + "phenomenonTime": "2020-12-31T23:00:00Z/2021-01-12T04:00:00Z", + "resultTime": "2020-12-31T23:30:00Z/2020-12-31T23:31:00Z", + "properties": { + "owner": "owner 3" + }, + "Things@iot.navigationLink": "endpoint/MultiDatastreams(3)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/MultiDatastreams(3)/HistoricalLocations" + } + ] + } + """.replace( + "endpoint", "http://" + endpoint + ) + ) + + vl = QgsVectorLayer( + f"url='http://{endpoint}' pageSize=2 entity='MultiDatastream'", + "test", + "sensorthings", + ) + self.assertTrue(vl.isValid()) + # basic layer properties tests + self.assertEqual(vl.storageType(), "OGC SensorThings API") + self.assertEqual(vl.wkbType(), Qgis.WkbType.NoGeometry) + self.assertEqual(vl.featureCount(), 3) + self.assertFalse(vl.crs().isValid()) + self.assertIn("Entity TypeMultiDatastream", + vl.htmlMetadata()) + self.assertIn(f'href="http://{endpoint}/MultiDatastreams"', + vl.htmlMetadata()) + + self.assertEqual( + [f.name() for f in vl.fields()], + [ + "id", + "selfLink", + "name", + "description", + "unitOfMeasurements", + "observationType", + "multiObservationDataTypes", + "properties", + "phenomenonTimeStart", + "phenomenonTimeEnd", + "resultTimeStart", + "resultTimeEnd", + ], + ) + self.assertEqual( + [f.type() for f in vl.fields()], + [ + QVariant.String, + QVariant.String, + QVariant.String, + QVariant.String, + QVariant.Map, + QVariant.String, + QVariant.StringList, + QVariant.Map, + QVariant.DateTime, + QVariant.DateTime, + QVariant.DateTime, + QVariant.DateTime, + ], + ) + + # test retrieving all features from layer + features = list(vl.getFeatures()) + self.assertEqual([f.id() for f in features], [0, 1, 2]) + self.assertEqual([f["id"] for f in features], ["1", "2", "3"]) + self.assertEqual( + [f["selfLink"][-20:] for f in features], + ["/MultiDatastreams(1)", "/MultiDatastreams(2)", "/MultiDatastreams(3)"], + ) + self.assertEqual( + [f["name"] for f in features], + ["MultiDatastream 1", "MultiDatastream 2", "MultiDatastream 3"], + ) + self.assertEqual( + [f["description"] for f in features], + ["Desc 1", "Desc 2", "Desc 3"] + ) + self.assertEqual( + [f["unitOfMeasurements"] for f in features], + [ + [{ + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3", + "name": "ug.m-3", + "symbol": "ug.m-3", + }], + [{ + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3", + "name": "ug.m-3", + "symbol": "ug.m-3", + }], + [{ + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3", + "name": "ug.m-3", + "symbol": "ug.m-3", + }], + ], + ) + self.assertEqual( + [f["observationType"] for f in features], + [ + "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + ], + ) + self.assertEqual( + [f["multiObservationDataTypes"] for f in features], + [ + ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + ], + ) + self.assertEqual( + [f["phenomenonTimeStart"] for f in features], + [ + QDateTime(QDate(2017, 12, 31), QTime(23, 0, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2018, 12, 31), QTime(23, 0, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2020, 12, 31), QTime(23, 0, 0, 0), + Qt.TimeSpec(1)), + ], + ) + self.assertEqual( + [f["phenomenonTimeEnd"] for f in features], + [ + QDateTime(QDate(2018, 1, 12), QTime(4, 0, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2019, 1, 12), QTime(4, 0, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2021, 1, 12), QTime(4, 0, 0, 0), + Qt.TimeSpec(1)), + ], + ) + self.assertEqual( + [f["resultTimeStart"] for f in features], + [ + QDateTime(QDate(2017, 12, 31), QTime(23, 30, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2018, 12, 31), QTime(23, 30, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2020, 12, 31), QTime(23, 30, 0, 0), + Qt.TimeSpec(1)), + ], + ) + self.assertEqual( + [f["resultTimeEnd"] for f in features], + [ + QDateTime(QDate(2017, 12, 31), QTime(23, 31, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2018, 12, 31), QTime(23, 31, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2020, 12, 31), QTime(23, 31, 0, 0), + Qt.TimeSpec(1)), + ], + ) + self.assertEqual( + [f["properties"] for f in features], + [{"owner": "owner 1"}, {"owner": "owner 2"}, + {"owner": "owner 3"}], + ) + + def test_multidatastream_polygons(self): + """ + Test a layer retrieving 'MultiDatastream' entities from a service using polygons + """ + with tempfile.TemporaryDirectory() as temp_dir: + base_path = temp_dir.replace("\\", "/") + endpoint = base_path + "/fake_qgis_http_endpoint" + with open(sanitize(endpoint, ""), "wt", encoding="utf8") as f: + f.write( + """ +{ + "value": [ + { + "name": "MultiDatastreams", + "url": "endpoint/MultiDatastreams" + } + ], + "serverSettings": { + } +}""".replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, "/MultiDatastreams?$top=0&$count=true&$filter=observedArea/type eq 'Polygon' or observedArea/geometry/type eq 'Polygon'"), + "wt", + encoding="utf8", + ) as f: + f.write("""{"@iot.count":3,"value":[]}""") + + with open( + sanitize(endpoint, "/MultiDatastreams?$top=2&$count=false&$filter=observedArea/type eq 'Polygon' or observedArea/geometry/type eq 'Polygon'"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ +{ + "value": [ + { + "@iot.selfLink": "endpoint/MultiDatastreams(1)", + "@iot.id": 1, + "name": "MultiDatastream 1", + "description": "Desc 1", + "unitOfMeasurements": [ + { + "name": "ug.m-3", + "symbol": "ug.m-3", + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3" + } + ], + "observationType": "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "multiObservationDataTypes": ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + "phenomenonTime": "2017-12-31T23:00:00Z/2018-01-12T04:00:00Z", + "resultTime": "2017-12-31T23:30:00Z/2017-12-31T23:31:00Z", + "properties": { + "owner": "owner 1" + }, + "observedArea": { + "type": "Polygon", + "coordinates": [ + [ + [100, 0], [101, 0], [101, 1], [100, 1], [100, 0] + ] + ] + }, + "Things@iot.navigationLink": "endpoint/MultiDatastreams(1)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/MultiDatastreams(1)/HistoricalLocations" + }, + { + "@iot.selfLink": "endpoint/MultiDatastreams(2)", + "@iot.id": 2, + "name": "MultiDatastream 2", + "description": "Desc 2", + "unitOfMeasurements": [ + { + "name": "ug.m-3", + "symbol": "ug.m-3", + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3" + }], + "observationType": "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "multiObservationDataTypes": ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + "phenomenonTime": "2018-12-31T23:00:00Z/2019-01-12T04:00:00Z", + "resultTime": "2018-12-31T23:30:00Z/2018-12-31T23:31:00Z", + "properties": { + "owner": "owner 2" + }, + "observedArea": { + "type": "Polygon", + "coordinates": [ + [ + [102, 0], [103, 0], [103, 1], [102, 1], [102, 0] + ] + ] + }, + "Things@iot.navigationLink": "endpoint/MultiDatastreams(2)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/MultiDatastreams(2)/HistoricalLocations" + + } + ], + "@iot.nextLink": "endpoint/MultiDatastreams?$top=2&$skip=2&$filter=observedArea/type eq 'Polygon' or observedArea/geometry/type eq 'Polygon'" +} + """.replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, "/MultiDatastreams?$top=2&$skip=2&$filter=observedArea/type eq 'Polygon' or observedArea/geometry/type eq 'Polygon'"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ + { + "value": [ + { + "@iot.selfLink": "endpoint/MultiDatastreams(3)", + "@iot.id": 3, + "name": "MultiDatastream 3", + "description": "Desc 3", + "unitOfMeasurements": [{ + "name": "ug.m-3", + "symbol": "ug.m-3", + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3" + }], + "observationType": "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "multiObservationDataTypes": ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + "phenomenonTime": "2020-12-31T23:00:00Z/2021-01-12T04:00:00Z", + "resultTime": "2020-12-31T23:30:00Z/2020-12-31T23:31:00Z", + "properties": { + "owner": "owner 3" + }, + "observedArea": { + "type": "Polygon", + "coordinates": [ + [ + [103, 0], [104, 0], [104, 1], [103, 1], [103, 0] + ] + ] + }, + "Things@iot.navigationLink": "endpoint/MultiDatastreams(3)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/MultiDatastreams(3)/HistoricalLocations" + } + ] + } + """.replace( + "endpoint", "http://" + endpoint + ) + ) + + vl = QgsVectorLayer( + f"url='http://{endpoint}' pageSize=2 type=MultiPolygonZ entity='MultiDatastream'", + "test", + "sensorthings", + ) + self.assertTrue(vl.isValid()) + # basic layer properties tests + self.assertEqual(vl.storageType(), "OGC SensorThings API") + self.assertEqual(vl.wkbType(), Qgis.WkbType.MultiPolygonZ) + self.assertEqual(vl.featureCount(), 3) + self.assertEqual(vl.crs().authid(), 'EPSG:4326') + self.assertIn("Entity TypeMultiDatastream", + vl.htmlMetadata()) + self.assertIn(f'href="http://{endpoint}/MultiDatastreams"', + vl.htmlMetadata()) + + self.assertEqual( + [f.name() for f in vl.fields()], + [ + "id", + "selfLink", + "name", + "description", + "unitOfMeasurements", + "observationType", + "multiObservationDataTypes", + "properties", + "phenomenonTimeStart", + "phenomenonTimeEnd", + "resultTimeStart", + "resultTimeEnd", + ], + ) + self.assertEqual( + [f.type() for f in vl.fields()], + [ + QVariant.String, + QVariant.String, + QVariant.String, + QVariant.String, + QVariant.Map, + QVariant.String, + QVariant.StringList, + QVariant.Map, + QVariant.DateTime, + QVariant.DateTime, + QVariant.DateTime, + QVariant.DateTime, + ], + ) + + # test retrieving all features from layer + features = list(vl.getFeatures()) + self.assertEqual([f.id() for f in features], [0, 1, 2]) + self.assertEqual([f["id"] for f in features], ["1", "2", "3"]) + self.assertEqual( + [f["selfLink"][-20:] for f in features], + ["/MultiDatastreams(1)", "/MultiDatastreams(2)", "/MultiDatastreams(3)"], + ) + self.assertEqual( + [f["name"] for f in features], + ["MultiDatastream 1", "MultiDatastream 2", "MultiDatastream 3"], + ) + self.assertEqual( + [f["description"] for f in features], + ["Desc 1", "Desc 2", "Desc 3"] + ) + self.assertEqual( + [f["unitOfMeasurements"] for f in features], + [ + [{ + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3", + "name": "ug.m-3", + "symbol": "ug.m-3", + }], + [{ + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3", + "name": "ug.m-3", + "symbol": "ug.m-3", + }], + [{ + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3", + "name": "ug.m-3", + "symbol": "ug.m-3", + }], + ], + ) + self.assertEqual( + [f["observationType"] for f in features], + [ + "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + ], + ) + self.assertEqual( + [f["multiObservationDataTypes"] for f in features], + [ + ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + ], + ) + self.assertEqual( + [f["phenomenonTimeStart"] for f in features], + [ + QDateTime(QDate(2017, 12, 31), QTime(23, 0, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2018, 12, 31), QTime(23, 0, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2020, 12, 31), QTime(23, 0, 0, 0), + Qt.TimeSpec(1)), + ], + ) + self.assertEqual( + [f["phenomenonTimeEnd"] for f in features], + [ + QDateTime(QDate(2018, 1, 12), QTime(4, 0, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2019, 1, 12), QTime(4, 0, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2021, 1, 12), QTime(4, 0, 0, 0), + Qt.TimeSpec(1)), + ], + ) + self.assertEqual( + [f["resultTimeStart"] for f in features], + [ + QDateTime(QDate(2017, 12, 31), QTime(23, 30, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2018, 12, 31), QTime(23, 30, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2020, 12, 31), QTime(23, 30, 0, 0), + Qt.TimeSpec(1)), + ], + ) + self.assertEqual( + [f["resultTimeEnd"] for f in features], + [ + QDateTime(QDate(2017, 12, 31), QTime(23, 31, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2018, 12, 31), QTime(23, 31, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2020, 12, 31), QTime(23, 31, 0, 0), + Qt.TimeSpec(1)), + ], + ) + self.assertEqual( + [f["properties"] for f in features], + [{"owner": "owner 1"}, {"owner": "owner 2"}, + {"owner": "owner 3"}], + ) + self.assertEqual( + [f.geometry().asWkt() for f in features], + ['Polygon ((100 0, 101 0, 101 1, 100 1, 100 0))', + 'Polygon ((102 0, 103 0, 103 1, 102 1, 102 0))', + 'Polygon ((103 0, 104 0, 104 1, 103 1, 103 0))'], + ) + def testDecodeUri(self): """ Test decoding a SensorThings uri From 5bc06fe30ab6187226cb9e05508d2856379e5c9c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 25 Mar 2024 14:53:44 +1000 Subject: [PATCH 10/87] Readability --- .../qgssensorthingsshareddata.cpp | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp b/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp index 9caacd3e6ef1..3ff383dd6d52 100644 --- a/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp +++ b/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp @@ -45,32 +45,36 @@ QgsSensorThingsSharedData::QgsSensorThingsSharedData( const QString &uri ) if ( QgsSensorThingsUtils::entityTypeHasGeometry( mEntityType ) ) { - const QString geometryType = uriParts.value( QStringLiteral( "geometryType" ) ).toString(); - if ( geometryType.compare( QLatin1String( "point" ), Qt::CaseInsensitive ) == 0 ) + if ( uriParts.contains( QStringLiteral( "geometryType" ) ) ) { - mGeometryType = Qgis::WkbType::PointZ; - } - else if ( geometryType.compare( QLatin1String( "multipoint" ), Qt::CaseInsensitive ) == 0 ) - { - mGeometryType = Qgis::WkbType::MultiPointZ; - } - else if ( geometryType.compare( QLatin1String( "line" ), Qt::CaseInsensitive ) == 0 ) - { - mGeometryType = Qgis::WkbType::MultiLineStringZ; - } - else if ( geometryType.compare( QLatin1String( "polygon" ), Qt::CaseInsensitive ) == 0 ) - { - mGeometryType = Qgis::WkbType::MultiPolygonZ; + const QString geometryType = uriParts.value( QStringLiteral( "geometryType" ) ).toString(); + if ( geometryType.compare( QLatin1String( "point" ), Qt::CaseInsensitive ) == 0 ) + { + mGeometryType = Qgis::WkbType::PointZ; + } + else if ( geometryType.compare( QLatin1String( "multipoint" ), Qt::CaseInsensitive ) == 0 ) + { + mGeometryType = Qgis::WkbType::MultiPointZ; + } + else if ( geometryType.compare( QLatin1String( "line" ), Qt::CaseInsensitive ) == 0 ) + { + mGeometryType = Qgis::WkbType::MultiLineStringZ; + } + else if ( geometryType.compare( QLatin1String( "polygon" ), Qt::CaseInsensitive ) == 0 ) + { + mGeometryType = Qgis::WkbType::MultiPolygonZ; + } + + if ( mGeometryType != Qgis::WkbType::NoGeometry ) + { + // geometry is always GeoJSON spec (for now, at least), so CRS will always be WGS84 + mSourceCRS = QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ); + } } - else if ( !uriParts.contains( QStringLiteral( "geometryType" ) ) ) + else { mGeometryType = Qgis::WkbType::NoGeometry; } - if ( mGeometryType != Qgis::WkbType::NoGeometry ) - { - // geometry is always GeoJSON spec (for now, at least), so CRS will always be WGS84 - mSourceCRS = QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ); - } } else { From 94bef546b227e5aab64379734b75d8a4b8741bcf Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 22 Mar 2024 12:53:46 +1000 Subject: [PATCH 11/87] Ensure temporal and elevation filters work correctly in conjunction Fixes #56938 --- .../qgsrasterlayertemporalproperties.sip.in | 7 + .../raster/qgsrasterlayerutils.sip.in | 53 +++ python/PyQt6/core/core_auto.sip | 1 + .../qgsrasterlayertemporalproperties.sip.in | 7 + .../raster/qgsrasterlayerutils.sip.in | 53 +++ python/core/core_auto.sip | 1 + src/core/CMakeLists.txt | 2 + src/core/raster/qgsrasterlayerrenderer.cpp | 53 ++- .../qgsrasterlayertemporalproperties.cpp | 35 ++ .../raster/qgsrasterlayertemporalproperties.h | 7 + src/core/raster/qgsrasterlayerutils.cpp | 151 +++++++ src/core/raster/qgsrasterlayerutils.h | 59 +++ tests/src/python/CMakeLists.txt | 1 + tests/src/python/test_qgsrasterlayerutils.py | 397 ++++++++++++++++++ 14 files changed, 800 insertions(+), 27 deletions(-) create mode 100644 python/PyQt6/core/auto_generated/raster/qgsrasterlayerutils.sip.in create mode 100644 python/core/auto_generated/raster/qgsrasterlayerutils.sip.in create mode 100644 src/core/raster/qgsrasterlayerutils.cpp create mode 100644 src/core/raster/qgsrasterlayerutils.h create mode 100644 tests/src/python/test_qgsrasterlayerutils.py diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in index 2c166a62b112..62d35f120e7b 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in @@ -132,6 +132,13 @@ Returns the band corresponding to the specified ``range``. This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.FixedRangePerBand. For other modes it will always return -1. +.. versionadded:: 3.38 +%End + + QList< int > filteredBandsForTemporalRange( QgsRasterLayer *layer, const QgsDateTimeRange &range ) const; +%Docstring +Returns a filtered list of bands which match the specified ``range``. + .. versionadded:: 3.38 %End diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterlayerutils.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterlayerutils.sip.in new file mode 100644 index 000000000000..9d74477d06db --- /dev/null +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterlayerutils.sip.in @@ -0,0 +1,53 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrasterlayerutils.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsRasterLayerUtils +{ +%Docstring(signature="appended") +Contains utility functions for working with raster layers. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsrasterlayerutils.h" +%End + public: + + static int renderedBandForElevationAndTemporalRange( + QgsRasterLayer *layer, + const QgsDateTimeRange &temporalRange, + const QgsDoubleRange &elevationRange, + bool &matched /Out/ ); +%Docstring +Given a raster ``layer``, returns the band which should be used for +rendering the layer for a specified temporal and elevation range, +respecting any elevation and temporal settings which affect the rendered band. + +:param layer: Target raster layer +:param temporalRange: temporal range for rendering +:param elevationRange: elevation range for rendering + +:return: - Matched band, or -1 if the layer does not have any elevation + - matched: will be set to ``True`` if a band matching the temporal and elevation range was found + or temporal settings which affect the rendered band. +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrasterlayerutils.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 71eb28b16c0e..5ae6df11f734 100644 --- a/python/PyQt6/core/core_auto.sip +++ b/python/PyQt6/core/core_auto.sip @@ -635,6 +635,7 @@ %Include auto_generated/raster/qgsrasterlayer.sip %Include auto_generated/raster/qgsrasterlayerelevationproperties.sip %Include auto_generated/raster/qgsrasterlayertemporalproperties.sip +%Include auto_generated/raster/qgsrasterlayerutils.sip %Include auto_generated/raster/qgsrasterminmaxorigin.sip %Include auto_generated/raster/qgsrasternuller.sip %Include auto_generated/raster/qgsrasterpipe.sip diff --git a/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in b/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in index 2c166a62b112..62d35f120e7b 100644 --- a/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in +++ b/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in @@ -132,6 +132,13 @@ Returns the band corresponding to the specified ``range``. This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.FixedRangePerBand. For other modes it will always return -1. +.. versionadded:: 3.38 +%End + + QList< int > filteredBandsForTemporalRange( QgsRasterLayer *layer, const QgsDateTimeRange &range ) const; +%Docstring +Returns a filtered list of bands which match the specified ``range``. + .. versionadded:: 3.38 %End diff --git a/python/core/auto_generated/raster/qgsrasterlayerutils.sip.in b/python/core/auto_generated/raster/qgsrasterlayerutils.sip.in new file mode 100644 index 000000000000..9d74477d06db --- /dev/null +++ b/python/core/auto_generated/raster/qgsrasterlayerutils.sip.in @@ -0,0 +1,53 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrasterlayerutils.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsRasterLayerUtils +{ +%Docstring(signature="appended") +Contains utility functions for working with raster layers. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsrasterlayerutils.h" +%End + public: + + static int renderedBandForElevationAndTemporalRange( + QgsRasterLayer *layer, + const QgsDateTimeRange &temporalRange, + const QgsDoubleRange &elevationRange, + bool &matched /Out/ ); +%Docstring +Given a raster ``layer``, returns the band which should be used for +rendering the layer for a specified temporal and elevation range, +respecting any elevation and temporal settings which affect the rendered band. + +:param layer: Target raster layer +:param temporalRange: temporal range for rendering +:param elevationRange: elevation range for rendering + +:return: - Matched band, or -1 if the layer does not have any elevation + - matched: will be set to ``True`` if a band matching the temporal and elevation range was found + or temporal settings which affect the rendered band. +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrasterlayerutils.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 71eb28b16c0e..5ae6df11f734 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -635,6 +635,7 @@ %Include auto_generated/raster/qgsrasterlayer.sip %Include auto_generated/raster/qgsrasterlayerelevationproperties.sip %Include auto_generated/raster/qgsrasterlayertemporalproperties.sip +%Include auto_generated/raster/qgsrasterlayerutils.sip %Include auto_generated/raster/qgsrasterminmaxorigin.sip %Include auto_generated/raster/qgsrasternuller.sip %Include auto_generated/raster/qgsrasterpipe.sip diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 692753b4e273..c2a907fd1067 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -759,6 +759,7 @@ set(QGIS_CORE_SRCS raster/qgsrasterlayerprofilegenerator.cpp raster/qgsrasterlayerrenderer.cpp raster/qgsrasterlayertemporalproperties.cpp + raster/qgsrasterlayerutils.cpp raster/qgsrasterminmaxorigin.cpp raster/qgsrasternuller.cpp raster/qgsrasterpipe.cpp @@ -1861,6 +1862,7 @@ set(QGIS_CORE_HDRS raster/qgsrasterlayerprofilegenerator.h raster/qgsrasterlayerrenderer.h raster/qgsrasterlayertemporalproperties.h + raster/qgsrasterlayerutils.h raster/qgsrasterminmaxorigin.h raster/qgsrasternuller.h raster/qgsrasterpipe.h diff --git a/src/core/raster/qgsrasterlayerrenderer.cpp b/src/core/raster/qgsrasterlayerrenderer.cpp index cb489e3616f6..472a457aaf8e 100644 --- a/src/core/raster/qgsrasterlayerrenderer.cpp +++ b/src/core/raster/qgsrasterlayerrenderer.cpp @@ -34,6 +34,7 @@ #include "qgsruntimeprofiler.h" #include "qgsapplication.h" #include "qgsrastertransparency.h" +#include "qgsrasterlayerutils.h" #include #include @@ -272,26 +273,33 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender mPipe->evaluateDataDefinedProperties( rendererContext.expressionContext() ); const QgsRasterLayerTemporalProperties *temporalProperties = qobject_cast< const QgsRasterLayerTemporalProperties * >( layer->temporalProperties() ); + const QgsRasterLayerElevationProperties *elevationProperties = qobject_cast( layer->elevationProperties() ); + + if ( ( temporalProperties->isActive() && renderContext()->isTemporal() ) + || ( elevationProperties->hasElevation() && !renderContext()->zRange().isInfinite() ) ) + { + // temporal and/or elevation band filtering may be applicable + bool matched = false; + const int matchedBand = QgsRasterLayerUtils::renderedBandForElevationAndTemporalRange( + layer, + rendererContext.temporalRange(), + rendererContext.zRange(), + matched + ); + if ( matched && matchedBand > 0 ) + { + mPipe->renderer()->setInputBand( matchedBand ); + } + } + if ( temporalProperties->isActive() && renderContext()->isTemporal() ) { switch ( temporalProperties->mode() ) { case Qgis::RasterTemporalMode::FixedTemporalRange: case Qgis::RasterTemporalMode::RedrawLayerOnly: - break; - case Qgis::RasterTemporalMode::FixedRangePerBand: - { - const int matchingBand = temporalProperties->bandForTemporalRange( layer, rendererContext.temporalRange() ); - - // 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 ( matchingBand > 0 ) - { - mPipe->renderer()->setInputBand( matchingBand ); - } - break; - } case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: // in this mode we need to pass on the desired render temporal range to the data provider @@ -311,17 +319,16 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender mClippingRegions = QgsMapClippingUtils::collectClippingRegionsForLayer( *renderContext(), layer ); - if ( layer->elevationProperties() && layer->elevationProperties()->hasElevation() ) + if ( elevationProperties && elevationProperties->hasElevation() ) { - QgsRasterLayerElevationProperties *elevProp = qobject_cast( layer->elevationProperties() ); mDrawElevationMap = true; - mElevationScale = elevProp->zScale(); - mElevationOffset = elevProp->zOffset(); - mElevationBand = elevProp->bandNumber(); + mElevationScale = elevationProperties->zScale(); + mElevationOffset = elevationProperties->zOffset(); + mElevationBand = elevationProperties->bandNumber(); if ( !rendererContext.zRange().isInfinite() ) { - switch ( elevProp->mode() ) + switch ( elevationProperties->mode() ) { case Qgis::RasterElevationMode::FixedElevationRange: // don't need to handle anything here -- the layer renderer will never be created if the @@ -330,16 +337,8 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender case Qgis::RasterElevationMode::FixedRangePerBand: case Qgis::RasterElevationMode::DynamicRangePerBand: - { - const int matchingBand = elevProp->bandForElevationRange( layer, rendererContext.zRange() ); - - // 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 ( matchingBand > 0 ) - { - mPipe->renderer()->setInputBand( matchingBand ); - } + // temporal/elevation band based filtering was already handled earlier in this method break; - } case Qgis::RasterElevationMode::RepresentsElevationSurface: { diff --git a/src/core/raster/qgsrasterlayertemporalproperties.cpp b/src/core/raster/qgsrasterlayertemporalproperties.cpp index a2320f1bbe27..4cde844e5272 100644 --- a/src/core/raster/qgsrasterlayertemporalproperties.cpp +++ b/src/core/raster/qgsrasterlayertemporalproperties.cpp @@ -234,6 +234,41 @@ int QgsRasterLayerTemporalProperties::bandForTemporalRange( QgsRasterLayer *, co BUILTIN_UNREACHABLE } +QList QgsRasterLayerTemporalProperties::filteredBandsForTemporalRange( QgsRasterLayer *layer, const QgsDateTimeRange &range ) const +{ + switch ( mMode ) + { + case Qgis::RasterTemporalMode::FixedTemporalRange: + case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: + case Qgis::RasterTemporalMode::RedrawLayerOnly: + { + const int bandCount = layer->bandCount(); + QList< int > res; + res.reserve( bandCount ); + for ( int i = 1; i <= bandCount; ++i ) + res.append( i ); + return res; + } + + case Qgis::RasterTemporalMode::FixedRangePerBand: + { + QList res; + res.reserve( mRangePerBand.size() ); + // find the latest-most band which matches the map range + QgsDateTimeRange currentMatchingRange; + for ( auto it = mRangePerBand.constBegin(); it != mRangePerBand.constEnd(); ++it ) + { + if ( it.value().overlaps( range ) ) + { + res.append( it.key() ); + } + } + return res; + } + } + BUILTIN_UNREACHABLE +} + bool QgsRasterLayerTemporalProperties::readXml( const QDomElement &element, const QgsReadWriteContext &context ) { Q_UNUSED( context ) diff --git a/src/core/raster/qgsrasterlayertemporalproperties.h b/src/core/raster/qgsrasterlayertemporalproperties.h index 77fb62e7034b..aa2fabf15b92 100644 --- a/src/core/raster/qgsrasterlayertemporalproperties.h +++ b/src/core/raster/qgsrasterlayertemporalproperties.h @@ -137,6 +137,13 @@ class CORE_EXPORT QgsRasterLayerTemporalProperties : public QgsMapLayerTemporalP */ int bandForTemporalRange( QgsRasterLayer *layer, const QgsDateTimeRange &range ) const; + /** + * Returns a filtered list of bands which match the specified \a range. + * + * \since QGIS 3.38 + */ + QList< int > filteredBandsForTemporalRange( QgsRasterLayer *layer, const QgsDateTimeRange &range ) const; + QDomElement writeXml( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ) override; bool readXml( const QDomElement &element, const QgsReadWriteContext &context ) override; diff --git a/src/core/raster/qgsrasterlayerutils.cpp b/src/core/raster/qgsrasterlayerutils.cpp new file mode 100644 index 000000000000..3dc8b536cc98 --- /dev/null +++ b/src/core/raster/qgsrasterlayerutils.cpp @@ -0,0 +1,151 @@ +/*************************************************************************** + qgsrasterlayerutils.cpp + ------------------------- + begin : March 2024 + copyright : (C) 2024 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsrasterlayerutils.h" +#include "qgsrasterlayer.h" +#include "qgsrasterlayerelevationproperties.h" +#include "qgsrasterlayertemporalproperties.h" +#include "qgsexpressioncontext.h" +#include "qgsexpressioncontextutils.h" + +int QgsRasterLayerUtils::renderedBandForElevationAndTemporalRange( + QgsRasterLayer *layer, + const QgsDateTimeRange &temporalRange, + const QgsDoubleRange &elevationRange, + bool &matched ) +{ + matched = true; + const QgsRasterLayerElevationProperties *elevationProperties = qobject_cast< QgsRasterLayerElevationProperties * >( layer->elevationProperties() ); + const QgsRasterLayerTemporalProperties *temporalProperties = qobject_cast< QgsRasterLayerTemporalProperties *>( layer->temporalProperties() ); + + // neither active + if ( ( !temporalProperties->isActive() || temporalRange.isInfinite() ) + && ( !elevationProperties->hasElevation() || elevationRange.isInfinite() ) ) + { + return -1; + } + + // only elevation properties enabled + if ( !temporalProperties->isActive() || temporalRange.isInfinite() ) + { + const int band = elevationProperties->bandForElevationRange( layer, elevationRange ); + matched = band > 0; + return band; + } + + // only temporal properties enabled + if ( !elevationProperties->hasElevation() || elevationRange.isInfinite() ) + { + const int band = temporalProperties->bandForTemporalRange( layer, temporalRange ); + matched = band > 0; + return band; + } + + // both elevation and temporal properties enabled + + // first find bands matching the temporal range + const QList< int > temporalBands = temporalProperties->filteredBandsForTemporalRange( + layer, temporalRange ); + if ( temporalBands.empty() ) + { + matched = false; + return -1; + } + + switch ( elevationProperties->mode() ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + case Qgis::RasterElevationMode::RepresentsElevationSurface: + return temporalBands.at( 0 ); + + case Qgis::RasterElevationMode::FixedRangePerBand: + { + // find the top-most band which matches the map range + int currentMatchingBand = -1; + matched = false; + QgsDoubleRange currentMatchingRange; + const QMap rangePerBand = elevationProperties->fixedRangePerBand(); + for ( int band : temporalBands ) + { + const QgsDoubleRange rangeForBand = rangePerBand.value( band ); + if ( rangeForBand.overlaps( elevationRange ) ) + { + if ( currentMatchingRange.isInfinite() + || ( rangeForBand.includeUpper() && rangeForBand.upper() >= currentMatchingRange.upper() ) + || ( !currentMatchingRange.includeUpper() && rangeForBand.upper() >= currentMatchingRange.upper() ) ) + { + matched = true; + currentMatchingBand = band; + currentMatchingRange = rangeForBand; + } + } + } + return currentMatchingBand; + } + + case Qgis::RasterElevationMode::DynamicRangePerBand: + { + if ( layer ) + { + QgsExpressionContext context; + context.appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( layer ) ); + QgsExpressionContextScope *bandScope = new QgsExpressionContextScope(); + context.appendScope( bandScope ); + + QgsProperty lowerProperty = elevationProperties->dataDefinedProperties().property( QgsMapLayerElevationProperties::Property::RasterPerBandLowerElevation ); + QgsProperty upperProperty = elevationProperties->dataDefinedProperties().property( QgsMapLayerElevationProperties::Property::RasterPerBandUpperElevation ); + lowerProperty.prepare( context ); + upperProperty.prepare( context ); + + int currentMatchingBand = -1; + matched = false; + QgsDoubleRange currentMatchingRange; + + for ( int band : temporalBands ) + { + bandScope->setVariable( QStringLiteral( "band" ), band ); + bandScope->setVariable( QStringLiteral( "band_name" ), layer->dataProvider()->displayBandName( band ) ); + bandScope->setVariable( QStringLiteral( "band_description" ), layer->dataProvider()->bandDescription( band ) ); + + bool ok = false; + const double lower = lowerProperty.valueAsDouble( context, 0, &ok ); + if ( !ok ) + continue; + const double upper = upperProperty.valueAsDouble( context, 0, &ok ); + if ( !ok ) + continue; + + const QgsDoubleRange bandRange = QgsDoubleRange( lower, upper ); + if ( bandRange.overlaps( elevationRange ) ) + { + if ( currentMatchingRange.isInfinite() + || ( bandRange.includeUpper() && bandRange.upper() >= currentMatchingRange.upper() ) + || ( !currentMatchingRange.includeUpper() && bandRange.upper() >= currentMatchingRange.upper() ) ) + { + currentMatchingBand = band; + currentMatchingRange = bandRange; + matched = true; + } + } + } + return currentMatchingBand; + } + return -1; + } + } + BUILTIN_UNREACHABLE; +} diff --git a/src/core/raster/qgsrasterlayerutils.h b/src/core/raster/qgsrasterlayerutils.h new file mode 100644 index 000000000000..a19295c15c36 --- /dev/null +++ b/src/core/raster/qgsrasterlayerutils.h @@ -0,0 +1,59 @@ +/*************************************************************************** + qgsrasterlayerutils.h + ------------------------- + begin : March 2024 + copyright : (C) 2024 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSRASTERLAYERUTILS_H +#define QGSRASTERLAYERUTILS_H + +#include "qgis_core.h" +#include "qgis_sip.h" +#include "qgsrange.h" + +class QgsRasterLayer; + +/** + * \class QgsRasterLayerUtils + * \ingroup core + * \brief Contains utility functions for working with raster layers. + * + * \since QGIS 3.38 + */ +class CORE_EXPORT QgsRasterLayerUtils +{ + public: + + /** + * Given a raster \a layer, returns the band which should be used for + * rendering the layer for a specified temporal and elevation range, + * respecting any elevation and temporal settings which affect the rendered band. + * + * \param layer Target raster layer + * \param temporalRange temporal range for rendering + * \param elevationRange elevation range for rendering + * \param matched will be set to TRUE if a band matching the temporal and elevation range was found + * + * \returns Matched band, or -1 if the layer does not have any elevation + * or temporal settings which affect the rendered band. + */ + static int renderedBandForElevationAndTemporalRange( + QgsRasterLayer *layer, + const QgsDateTimeRange &temporalRange, + const QgsDoubleRange &elevationRange, + bool &matched SIP_OUT ); + +}; + +#endif //QGSRASTERLAYERUTILS_H diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index b0e4fcbcd97a..72140d293c02 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -263,6 +263,7 @@ ADD_PYTHON_TEST(PyQgsRasterLayerElevationProperties test_qgsrasterlayerelevation ADD_PYTHON_TEST(PyQgsRasterLayerProfileGenerator test_qgsrasterlayerprofilegenerator.py) ADD_PYTHON_TEST(PyQgsRasterLayerRenderer test_qgsrasterlayerrenderer.py) ADD_PYTHON_TEST(PyQgsRasterLayerTemporalProperties test_qgsrasterlayertemporalproperties.py) +ADD_PYTHON_TEST(PyQgsRasterLayerUtils test_qgsrasterlayerutils.py) ADD_PYTHON_TEST(PyQgsRasterColorRampShader test_qgsrastercolorrampshader.py) ADD_PYTHON_TEST(PyQgsRasterLineSymbolLayer test_qgsrasterlinesymbollayer.py) ADD_PYTHON_TEST(PyQgsRasterPipe test_qgsrasterpipe.py) diff --git a/tests/src/python/test_qgsrasterlayerutils.py b/tests/src/python/test_qgsrasterlayerutils.py new file mode 100644 index 000000000000..4b4b47fc061c --- /dev/null +++ b/tests/src/python/test_qgsrasterlayerutils.py @@ -0,0 +1,397 @@ +"""QGIS Unit tests for QgsRasterLayerUtils + +.. 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 ( + QDate, + QTime, + QDateTime +) +from qgis.core import ( + Qgis, + QgsRasterLayerUtils, + QgsRasterLayer, + QgsDoubleRange, + QgsDateTimeRange +) +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 TestQgsRasterLayerUtils(QgisTestCase): + + def test_rendered_band_for_elevation_and_temporal_ranges(self): + raster_layer = QgsRasterLayer(os.path.join(TEST_DATA_DIR, 'landsat_4326.tif')) + self.assertTrue(raster_layer.isValid()) + + # no temporal or elevation properties + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2023, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2023, 12, 31), + QTime(23, 59, 59)) + ), + QgsDoubleRange(30, 50) + ) + self.assertEqual(band, -1) + self.assertTrue(matched) + + # only elevation properties enabled + raster_layer.elevationProperties().setEnabled(True) + raster_layer.elevationProperties().setMode( + Qgis.RasterElevationMode.FixedRangePerBand + ) + raster_layer.elevationProperties().setFixedRangePerBand( + {1: QgsDoubleRange(1, 5), + 2: QgsDoubleRange(4, 10), + 3: QgsDoubleRange(11, 15), + 4: QgsDoubleRange(1, 5), + 5: QgsDoubleRange(4, 10), + 6: QgsDoubleRange(11, 16), + 7: QgsDoubleRange(1, 5), + 8: QgsDoubleRange(4, 10), + 9: QgsDoubleRange(11, 15), + } + ) + # no matching elevation + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2023, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2023, 12, 31), + QTime(23, 59, 59)) + ), + QgsDoubleRange(30, 50) + ) + self.assertEqual(band, -1) + self.assertFalse(matched) + # matching elevation + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2023, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2023, 12, 31), + QTime(23, 59, 59)) + ), + QgsDoubleRange(1, 3) + ) + self.assertEqual(band, 7) + self.assertTrue(matched) + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2023, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2023, 12, 31), + QTime(23, 59, 59)) + ), + QgsDoubleRange(5, 8) + ) + self.assertEqual(band, 8) + self.assertTrue(matched) + + # specify infinite elevation range + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2023, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2023, 12, 31), + QTime(23, 59, 59)) + ), + QgsDoubleRange() + ) + self.assertEqual(band, -1) + self.assertTrue(matched) + + # only temporal properties enabled + raster_layer.elevationProperties().setEnabled(False) + raster_layer.temporalProperties().setIsActive(True) + raster_layer.temporalProperties().setMode( + Qgis.RasterTemporalMode.FixedRangePerBand + ) + raster_layer.temporalProperties().setFixedRangePerBand( + { + 1: QgsDateTimeRange( + QDateTime(QDate(2020, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2020, 12, 31), + QTime(23, 59, 59)) + ), + 2: QgsDateTimeRange( + QDateTime(QDate(2020, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2020, 12, 31), + QTime(23, 59, 59)) + ), + 3: QgsDateTimeRange( + QDateTime(QDate(2020, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2020, 12, 31), + QTime(23, 59, 59)) + ), + 4: QgsDateTimeRange( + QDateTime(QDate(2021, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2021, 12, 31), + QTime(23, 59, 59)) + ), + 5: QgsDateTimeRange( + QDateTime(QDate(2021, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2021, 12, 31), + QTime(23, 59, 59)) + ), + 6: QgsDateTimeRange( + QDateTime(QDate(2021, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2021, 12, 31), + QTime(23, 59, 59)) + ), + 7: QgsDateTimeRange( + QDateTime(QDate(2022, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2022, 12, 31), + QTime(23, 59, 59)) + ), + 8: QgsDateTimeRange( + QDateTime(QDate(2022, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2022, 12, 31), + QTime(23, 59, 59)) + ), + 9: QgsDateTimeRange( + QDateTime(QDate(2022, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2022, 12, 31), + QTime(23, 59, 59)) + ) + } + ) + + # no matching time range + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2023, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2023, 12, 31), + QTime(23, 59, 59)) + ), + QgsDoubleRange(30, 50) + ) + self.assertEqual(band, -1) + self.assertFalse(matched) + # matching time range + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2020, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2020, 6, 30), + QTime(23, 59, 59)) + ), + QgsDoubleRange(1, 3) + ) + self.assertEqual(band, 3) + self.assertTrue(matched) + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2020, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2021, 6, 30), + QTime(23, 59, 59)) + ), + QgsDoubleRange(5, 8) + ) + self.assertEqual(band, 6) + self.assertTrue(matched) + + # specify infinite temporal range + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange(QDateTime(), QDateTime()), + QgsDoubleRange(5, 8) + ) + self.assertEqual(band, -1) + self.assertTrue(matched) + + # with both elevation and temporal handling enabled + raster_layer.elevationProperties().setEnabled(True) + + # specify infinite temporal range + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange(QDateTime(), QDateTime()), + QgsDoubleRange(5, 8) + ) + self.assertEqual(band, 8) + self.assertTrue(matched) + + # specify infinite elevation range + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2020, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2021, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange() + ) + self.assertEqual(band, 6) + self.assertTrue(matched) + + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2020, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2020, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(2, 3) + ) + self.assertEqual(band, 1) + self.assertTrue(matched) + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2020, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2020, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(3, 7) + ) + self.assertEqual(band, 2) + self.assertTrue(matched) + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2020, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2020, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(11, + 13) + ) + self.assertEqual(band, 3) + self.assertTrue(matched) + + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2021, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2021, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(2, 3) + ) + self.assertEqual(band, 4) + self.assertTrue(matched) + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2021, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2021, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(3, 7) + ) + self.assertEqual(band, 5) + self.assertTrue(matched) + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2021, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2021, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(11, + 13) + ) + self.assertEqual(band, 6) + self.assertTrue(matched) + + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2022, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2022, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(2, 3) + ) + self.assertEqual(band, 7) + self.assertTrue(matched) + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2022, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2022, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(3, 7) + ) + self.assertEqual(band, 8) + self.assertTrue(matched) + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2022, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2022, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(11, + 13) + ) + self.assertEqual(band, 9) + self.assertTrue(matched) + + # outside temporal range + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2023, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2023, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(11, + 13) + ) + self.assertEqual(band, -1) + self.assertFalse(matched) + + # outside elevation range + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2022, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2022, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(111, + 113) + ) + self.assertEqual(band, -1) + self.assertFalse(matched) + + +if __name__ == '__main__': + unittest.main() From b5cbcbd9d411daa3a4a94acaefd1788e7fe208c3 Mon Sep 17 00:00:00 2001 From: Harrissou Sant-anna Date: Mon, 25 Mar 2024 15:09:16 +0100 Subject: [PATCH 12/87] Transfer security matters of the authentication system from the user manual (refs https://github.com/qgis/QGIS-Documentation/pull/8985#issuecomment-2017822488) --- src/auth/README.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/auth/README.md diff --git a/src/auth/README.md b/src/auth/README.md new file mode 100644 index 000000000000..196b05f882da --- /dev/null +++ b/src/auth/README.md @@ -0,0 +1,62 @@ +_Originally available in the [user manual](https://docs.qgis.org/3.28/en/docs/user_manual/auth_system/auth_considerations.html)_ + +# Security Considerations + +Once the master password is entered, the API is open to access authentication +configs in the authentication database, similar to how Firefox works. +However, in the initial implementation, no wall against PyQGIS access has been defined. +This may lead to issues where a user downloads/installs a malicious PyQGIS plugin +or standalone app that gains access to authentication credentials. + +The quick solution for initial release of feature is to just not include most +PyQGIS bindings for the authentication system. + +Another simple, though not robust, fix is to add a combobox +in Settings --> Options --> Authentication (defaults to "never"): + +``` + "Allow Python access to authentication system" + Choices: [ confirm once per session | always confirm | always allow | never] +``` + +Such an option's setting would need to be saved in a location non-accessible to Python, +e.g. the authentication database, and encrypted with the master password. + +* Another option may be to track which plugins the user has specifically + allowed to access the authentication system, though it may be tricky to deduce + which plugin is actually making the call. + +* Sandboxing plugins, possibly in their own virtual environments, + would reduce 'cross-plugin' hacking of authentication configs from another plugin + that is authorized. + This might mean limiting cross-plugin communication as well, + but maybe only between third-party plugins. + +* Another good solution is to issue code-signing certificates to vetted plugin authors. + Then validate the plugin's certificate upon loading. + If need be the user can also directly set an untrusted policy for the certificate associated + with the plugin using existing certificate management dialogs. + +* Alternatively, access to sensitive authentication system data from Python could never be allowed, + and only the use of QGIS core widgets, or duplicating authentication system integrations, + would allow the plugin to work with resources that have an authentication configuration, + while keeping master password and authentication config loading in the realm of the main app. + +The same security concerns apply to C++ plugins, though it will be harder to restrict access, +since there is no function binding to simply be removed as with Python. + +## Restrictions + +The confusing [licensing and exporting](https://www.openssl.org/docs/faq.html) +issues associated with OpenSSL apply. +In order for Qt to work with SSL certificates, it needs access to the OpenSSL libraries. +Depending upon how Qt was compiled, the default is to dynamically link +to the OpenSSL libs at run-time (to avoid the export limitations). + +QCA follows a similar tactic, whereby linking to QCA incurs no restrictions, +because the qca-ossl (OpenSSL) plugin is loaded at run-time. +The qca-ossl plugin is directly linked to the OpenSSL libs. Packagers would be the ones +needing to ensure any OpenSSL-linking restrictions are met, if they ship the plugin. +Maybe. I don't really know. I'm not a lawyer. + +The authentication system safely disables itself when ``qca-ossl`` is not found at run-time. From d0fcca46a8b8e6e37c106621436997a851beddd5 Mon Sep 17 00:00:00 2001 From: Denis Rouzaud Date: Tue, 26 Mar 2024 15:45:58 +0100 Subject: [PATCH 13/87] Remove Docker build from action following discussion here https://github.com/qgis/QGIS-Enhancement-Proposals/issues/221 opengisch repo has been transferred here: https://github.com/qgis/qgis-docker --- .github/workflows/build-docker.yml | 184 ----------------------------- 1 file changed, 184 deletions(-) delete mode 100644 .github/workflows/build-docker.yml diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml deleted file mode 100644 index a49d28af767d..000000000000 --- a/.github/workflows/build-docker.yml +++ /dev/null @@ -1,184 +0,0 @@ -name: 🐳 Build Docker images for current branches - -# on commits to this file, schedule and dispatch runs, the workflow will build the 3 different Docker images (master, PR, LTR) -# on tags, it will build only the image of the given tag -# this is made by using a matrix defined in a dedicated job -on: - push: - tags: - - final-* - branches: - - master - paths: - - .github/workflows/build-docker.yml - schedule: - # runs every day - - cron: '0 0 * * *' - workflow_dispatch: - # POST https://api.github.com/repos/qgis/QGIS/actions/workflows/2264135/dispatches: - -permissions: - contents: read - -jobs: - define-strategy: - permissions: - contents: none - if: github.repository_owner == 'qgis' - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.matrix.outputs.matrix }} - steps: - - id: matrix - run: | - if [[ "${GITHUB_REF}" =~ ^refs/tags ]]; then - echo "matrix={\"branch\":[\"${GITHUB_REF##*/}\"]}" >> $GITHUB_OUTPUT - else - echo "matrix={\"branch\":[\"master\", \"release-3_34\", \"release-3_36\"]}" >> $GITHUB_OUTPUT - fi - - build-docker: - if: github.repository_owner == 'qgis' - runs-on: ubuntu-latest - needs: define-strategy - - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - GH_TOKEN: ${{ secrets.GH_TOKEN }} - CC: /usr/lib/ccache/gcc - CXX: /usr/lib/ccache/g++ # Building SIP binding freezes with Clang in Docker, maybe a SIP issue, maybe not - DOCKER_BUILD_DEPS_FILE: qgis3-qt5-build-deps.dockerfile - - strategy: - fail-fast: false - matrix: ${{ fromJSON( needs.define-strategy.outputs.matrix ) }} - - steps: - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@main - with: - tool-cache: true - large-packages: true - docker-images: false - swap-storage: true - - - name: Free additional space - run: | - df -h - rm -rf /tmp/workspace - rm -rf /usr/share/dotnet/sdk - sudo apt remove llvm-* ghc-* google-chrome-* dotnet-sdk-* - dpkg-query -Wf '${Installed-Size}\t${Package}\n' | sort -n | tail -n 100 - du -a /usr/share | sort -n -r | head -n 10 - du -a /usr/local/share | sort -n -r | head -n 10 - df -h - sudo apt clean - df -h - - - name: Cache - id: cache - uses: actions/cache@v4 - with: - path: ~/.ccache - key: docker-build-${{ matrix.branch }}-${{ github.sha }} - restore-keys: | - docker-build-${{ matrix.branch }}- - docker-build-master- - - - name: checkout ${{ matrix.branch }} - uses: actions/checkout@v4 - with: - ref: ${{ matrix.branch }} - - - name: Define vars - env: - branch: ${{ matrix.branch }} - run: | - export DOCKER_TAG=${branch//master/latest} - export DOCKER_DEPS_TAG=${DOCKER_TAG} - - # add vars for next steps - echo "DOCKER_TAG=${DOCKER_TAG}" >> $GITHUB_ENV - echo "DOCKER_DEPS_TAG=${DOCKER_DEPS_TAG}" >> $GITHUB_ENV - - echo "branch: ${branch}" - echo "docker tag: ${DOCKER_TAG}" - echo "docker deps tag: ${DOCKER_DEPS_TAG}" - - - name: Copy cache - run: | - [[ -d ~/.ccache ]] && echo "cache directory (~/.ccache) exists" || mkdir -p ~/.ccache - # copy ccache dir within QGIS source so it can be accessed from docker - cp -r ~/.ccache/. ./.ccache_image_build - - - name: QGIS deps Docker pull/rebuild - run: | - cd .docker - docker --version - docker pull "qgis/qgis3-build-deps:${DOCKER_DEPS_TAG}" || true - docker build --cache-from "qgis/qgis3-build-deps:${DOCKER_DEPS_TAG}" -t "qgis/qgis3-build-deps:${DOCKER_DEPS_TAG}" -f ${DOCKER_BUILD_DEPS_FILE} . - echo "push to qgis/qgis3-build-deps:${DOCKER_DEPS_TAG}" - docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" - docker push "qgis/qgis3-build-deps:${DOCKER_DEPS_TAG}" - - - name: Docker QGIS build - run: | - cd .docker - DOCKER_BUILD_ARGS="--build-arg DOCKER_DEPS_TAG --build-arg CC --build-arg CXX" - docker build ${DOCKER_BUILD_ARGS} \ - --cache-from "qgis/qgis:${DOCKER_TAG}" \ - -t "qgis/qgis:BUILDER" \ - -f qgis.dockerfile .. - - - name: Tag container and copy cache - run: | - docker run --name qgis_container qgis/qgis:BUILDER /bin/true - docker cp qgis_container:/QGIS/build_exit_value ./build_exit_value - - if [[ $(cat ./build_exit_value) != "OK" ]]; then - echo "Build failed, not pushing image" - exit 1 - fi - - echo "Copy build cache from Docker container to Travis cache directory" - rm -rf ~/.ccache/* - mkdir -p ~/.ccache - docker cp qgis_container:/QGIS/.ccache_image_build/. ~/.ccache - echo "Cache size: "$(du -sh ~/.ccache) - - - name: Finalize image - run: | - cd .docker - # enable experimental features in Docker to squash - echo '{ "experimental": true}' | sudo tee /etc/docker/daemon.json - sudo service docker restart - docker build ${DOCKER_BUILD_ARGS} \ - --cache-from "qgis/qgis:BUILDER" \ - --squash \ - -t "qgis/qgis:${DOCKER_TAG}" \ - -f qgis.dockerfile .. - - - name: Pushing image to docker hub - run: | - docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" - docker push "qgis/qgis:${DOCKER_TAG}" - - - name: Trigger PyQGIS API docs build for ${{ matrix.branch }} - if: success() && !startsWith(github.ref, 'refs/tags/') - env: - branch: ${{ matrix.branch }} - run: | - body='{ - "ref": "master", - "inputs": {"qgis_branch": "__QGIS_VERSION_BRANCH__"} - }' - body=$(sed "s/__QGIS_VERSION_BRANCH__/${branch}/;" <<< $body) - curl -X POST \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: token ${GH_TOKEN}" \ - https://api.github.com/repos/qgis/pyqgis/actions/workflows/2246440/dispatches \ - -d "${body}" - - - From 62f8b88a8a65e31d078082eb1e6d01182ea4edac Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 27 Mar 2024 10:29:52 +1000 Subject: [PATCH 14/87] Clear welcome page even when a non-spatial layer is added Previously we only cleared the welcome page when a spatial layer was added, which resulted in a weird UX where eg dragging a csv onto QGIS left the welcome page in place, even though a project is now open! Instead, clear the welcome page whenever ANY type of layer is added to the project --- src/app/qgisapp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 525ef313895c..3b36ea2c3a43 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -1137,7 +1137,7 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipBadLayers centralLayout->addWidget( mCentralContainer, 0, 0, 2, 1 ); mInfoBar->raise(); - connect( mMapCanvas, &QgsMapCanvas::layersChanged, this, &QgisApp::showMapCanvas ); + connect( QgsProject::instance(), &QgsProject::layersAdded, this, &QgisApp::showMapCanvas ); mCentralContainer->setCurrentIndex( mProjOpen ? 0 : 1 ); From 0dac88d94cb43474f89a16c4c9b2981f478ac182 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 25 Mar 2024 11:19:14 +1000 Subject: [PATCH 15/87] Fix capitalisation --- src/gui/editorwidgets/qgsjsoneditwidget.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/editorwidgets/qgsjsoneditwidget.cpp b/src/gui/editorwidgets/qgsjsoneditwidget.cpp index 5981300aa944..00c886ac2b73 100644 --- a/src/gui/editorwidgets/qgsjsoneditwidget.cpp +++ b/src/gui/editorwidgets/qgsjsoneditwidget.cpp @@ -26,8 +26,8 @@ QgsJsonEditWidget::QgsJsonEditWidget( QWidget *parent ) : QWidget( parent ) - , mCopyValueAction( new QAction( tr( "Copy value" ), this ) ) - , mCopyKeyAction( new QAction( tr( "Copy key" ), this ) ) + , mCopyValueAction( new QAction( tr( "Copy Value" ), this ) ) + , mCopyKeyAction( new QAction( tr( "Copy Key" ), this ) ) { setupUi( this ); From 3b9fe3bc196431cfd54303d00d0888d932b84666 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 25 Mar 2024 11:33:01 +1000 Subject: [PATCH 16/87] Use more robust way of setting monospace for json tree view Setting a stylesheet like this breaks things, eg the context menu incorrectly shows in the monospace font. Instead, directly set the font only on the tree view items. --- src/gui/editorwidgets/qgsjsoneditwidget.cpp | 17 +++++++++++++++-- src/gui/editorwidgets/qgsjsoneditwidget.h | 2 ++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/gui/editorwidgets/qgsjsoneditwidget.cpp b/src/gui/editorwidgets/qgsjsoneditwidget.cpp index 00c886ac2b73..24424c8181be 100644 --- a/src/gui/editorwidgets/qgsjsoneditwidget.cpp +++ b/src/gui/editorwidgets/qgsjsoneditwidget.cpp @@ -40,8 +40,6 @@ QgsJsonEditWidget::QgsJsonEditWidget( QWidget *parent ) mCodeEditorJson->SendScintilla( QsciScintillaBase::SCI_SETINDICATORCURRENT, SCINTILLA_UNDERLINE_INDICATOR_INDEX ); mCodeEditorJson->SendScintilla( QsciScintillaBase::SCI_SETMOUSEDWELLTIME, 400 ); - mTreeWidget->setStyleSheet( QStringLiteral( "font-family: %1;" ).arg( QgsCodeEditor::getMonospaceFont().family() ) ); - mTreeWidget->setContextMenuPolicy( Qt::ActionsContextMenu ); mTreeWidget->addAction( mCopyValueAction ); mTreeWidget->addAction( mCopyKeyAction ); @@ -261,6 +259,7 @@ void QgsJsonEditWidget::refreshTreeView( const QJsonDocument &jsonDocument ) { const QJsonValue jsonValue = jsonDocument.object().value( key ); QTreeWidgetItem *treeWidgetItem = new QTreeWidgetItem( mTreeWidget, QStringList() << key ); + treeWidgetItem->setFont( 0, monospaceFont() ); refreshTreeViewItem( treeWidgetItem, jsonValue ); mTreeWidget->addTopLevelItem( treeWidgetItem ); mTreeWidget->expandItem( treeWidgetItem ); @@ -281,6 +280,7 @@ void QgsJsonEditWidget::refreshTreeView( const QJsonDocument &jsonDocument ) for ( auto index = decltype( arraySize ) {0}; index < arraySize; index++ ) { QTreeWidgetItem *treeWidgetItem = new QTreeWidgetItem( mTreeWidget, QStringList() << QString::number( index ) ); + treeWidgetItem->setFont( 0, monospaceFont() ); if ( arraySize <= MAX_ELTS || ( index < MAX_ELTS / 2 || index + MAX_ELTS / 2 > arraySize ) ) { refreshTreeViewItem( treeWidgetItem, array.at( index ) ); @@ -334,6 +334,7 @@ void QgsJsonEditWidget::refreshTreeViewItem( QTreeWidgetItem *treeWidgetItem, co { QLabel *label = new QLabel( QString( "%1" ).arg( jsonValueString ) ); label->setOpenExternalLinks( true ); + label->setFont( monospaceFont() ); mTreeWidget->setItemWidget( treeWidgetItem, static_cast( TreeWidgetColumn::Value ), label ); mClickableLinkList.append( jsonValueString ); @@ -359,6 +360,7 @@ void QgsJsonEditWidget::refreshTreeViewItem( QTreeWidgetItem *treeWidgetItem, co for ( auto index = decltype( arraySize ) {0}; index < arraySize; index++ ) { QTreeWidgetItem *treeWidgetItemChild = new QTreeWidgetItem( treeWidgetItem, QStringList() << QString::number( index ) ); + treeWidgetItemChild->setFont( 0, monospaceFont() ); if ( arraySize <= MAX_ELTS || ( index < MAX_ELTS / 2 || index + MAX_ELTS / 2 > arraySize ) ) { refreshTreeViewItem( treeWidgetItemChild, jsonArray.at( index ) ); @@ -382,6 +384,7 @@ void QgsJsonEditWidget::refreshTreeViewItem( QTreeWidgetItem *treeWidgetItem, co for ( const QString &key : keys ) { QTreeWidgetItem *treeWidgetItemChild = new QTreeWidgetItem( treeWidgetItem, QStringList() << key ); + treeWidgetItemChild->setFont( 0, monospaceFont() ); refreshTreeViewItem( treeWidgetItemChild, jsonObject.value( key ) ); treeWidgetItem->addChild( treeWidgetItemChild ); treeWidgetItem->setExpanded( true ); @@ -399,7 +402,17 @@ void QgsJsonEditWidget::refreshTreeViewItem( QTreeWidgetItem *treeWidgetItem, co void QgsJsonEditWidget::refreshTreeViewItemValue( QTreeWidgetItem *treeWidgetItem, const QString &jsonValueString, const QColor &textColor ) { QLabel *label = new QLabel( jsonValueString ); + label->setFont( monospaceFont() ); + if ( textColor.isValid() ) label->setStyleSheet( QStringLiteral( "color: %1;" ).arg( textColor.name() ) ); mTreeWidget->setItemWidget( treeWidgetItem, static_cast( TreeWidgetColumn::Value ), label ); } + +QFont QgsJsonEditWidget::monospaceFont() const +{ + QFont f = QgsCodeEditor::getMonospaceFont(); + // use standard widget font size, not code editor font size + f.setPointSize( font().pointSize() ); + return f; +} diff --git a/src/gui/editorwidgets/qgsjsoneditwidget.h b/src/gui/editorwidgets/qgsjsoneditwidget.h index 89bf1cb421a9..34fd071fcc22 100644 --- a/src/gui/editorwidgets/qgsjsoneditwidget.h +++ b/src/gui/editorwidgets/qgsjsoneditwidget.h @@ -116,6 +116,8 @@ class GUI_EXPORT QgsJsonEditWidget : public QWidget, private Ui::QgsJsonEditWidg void refreshTreeViewItem( QTreeWidgetItem *treeWidgetItemParent, const QJsonValue &jsonValue ); void refreshTreeViewItemValue( QTreeWidgetItem *treeWidgetItem, const QString &jsonValueString, const QColor &textColor ); + QFont monospaceFont() const; + QString mJsonText; FormatJson mFormatJsonMode = FormatJson::Indented; From 5823c6d5e9ce825d38800f7d2861b63ab841d64a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 25 Mar 2024 11:37:42 +1000 Subject: [PATCH 17/87] Move mode toggle button in json editor widget to bottom This matches better the approach used in similar widgets elsewhere in QGIS --- src/ui/editorwidgets/qgsjsoneditwidget.ui | 150 ++++++++++++---------- 1 file changed, 84 insertions(+), 66 deletions(-) diff --git a/src/ui/editorwidgets/qgsjsoneditwidget.ui b/src/ui/editorwidgets/qgsjsoneditwidget.ui index d591fec8239b..ac384ea88bac 100644 --- a/src/ui/editorwidgets/qgsjsoneditwidget.ui +++ b/src/ui/editorwidgets/qgsjsoneditwidget.ui @@ -26,72 +26,7 @@ 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Show text - - - ... - - - - :/images/themes/default/mIconFieldText.svg:/images/themes/default/mIconFieldText.svg - - - true - - - - - - - Show tree - - - ... - - - - :/images/themes/default/mIconTreeView.svg:/images/themes/default/mIconTreeView.svg - - - true - - - - - - - + 1 @@ -150,6 +85,84 @@ + + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Show text + + + ... + + + + :/images/themes/default/mIconFieldText.svg:/images/themes/default/mIconFieldText.svg + + + true + + + true + + + + + + + Show tree + + + ... + + + + :/images/themes/default/mIconTreeView.svg:/images/themes/default/mIconTreeView.svg + + + true + + + true + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + @@ -159,6 +172,11 @@
qgscodeeditorjson.h
+ + mTreeWidget + mTextToolButton + mTreeToolButton + From 776394dca9c8b8f705e833ad46eca25fc5811258 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 25 Mar 2024 11:41:53 +1000 Subject: [PATCH 18/87] Move action buttons in table widget to right side Placing them on the left clashes with every other QGIS dialog, and breaks nice alignment of attribute forms --- src/ui/qgstablewidgetuibase.ui | 50 +++++++++++++++++----------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/ui/qgstablewidgetuibase.ui b/src/ui/qgstablewidgetuibase.ui index 254a6e1b192b..8f54d1e858ae 100644 --- a/src/ui/qgstablewidgetuibase.ui +++ b/src/ui/qgstablewidgetuibase.ui @@ -32,6 +32,31 @@ 0 + + + + QFrame::NoFrame + + + QAbstractItemView::AnyKeyPressed|QAbstractItemView::CurrentChanged|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + true + + + false + + + @@ -80,31 +105,6 @@ - - - - QFrame::NoFrame - - - QAbstractItemView::AnyKeyPressed|QAbstractItemView::CurrentChanged|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - false - - - true - - - false - - - From bb7090f1241a0594565abfd00068e120fd669be9 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 25 Mar 2024 11:44:02 +1000 Subject: [PATCH 19/87] Use correct add/remove icons in table widget --- src/ui/qgstablewidgetuibase.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/qgstablewidgetuibase.ui b/src/ui/qgstablewidgetuibase.ui index 8f54d1e858ae..1099d5891b92 100644 --- a/src/ui/qgstablewidgetuibase.ui +++ b/src/ui/qgstablewidgetuibase.ui @@ -69,7 +69,7 @@ - :/images/themes/default/mActionAdd.svg:/images/themes/default/mActionAdd.svg + :/images/themes/default/symbologyAdd.svg:/images/themes/default/symbologyAdd.svg
@@ -86,7 +86,7 @@ - :/images/themes/default/mActionRemove.svg:/images/themes/default/mActionRemove.svg + :/images/themes/default/symbologyRemove.svg:/images/themes/default/symbologyRemove.svg From 63c1d5cfc0063601ea339cfe6260eb302c51acac Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 25 Mar 2024 11:56:55 +1000 Subject: [PATCH 20/87] Implement a proper read-only state for list/key value widget wrappers Instead of just setting the whole widget as disabled, implement proper read-only state for these widgets. This fixes an annoying UI issue where the list / key value widgets can't be scrolled when opening read-only attribute forms, preventing users from viewing all the entries in the widget. --- .../auto_generated/qgskeyvaluewidget.sip.in | 4 +++ .../gui/auto_generated/qgslistwidget.sip.in | 5 ++++ .../auto_generated/qgstablewidgetbase.sip.in | 20 +++++++++++++++ .../auto_generated/qgskeyvaluewidget.sip.in | 4 +++ .../gui/auto_generated/qgslistwidget.sip.in | 5 ++++ .../auto_generated/qgstablewidgetbase.sip.in | 20 +++++++++++++++ .../qgskeyvaluewidgetwrapper.cpp | 8 ++++++ .../editorwidgets/qgskeyvaluewidgetwrapper.h | 2 ++ .../editorwidgets/qgslistwidgetwrapper.cpp | 8 ++++++ src/gui/editorwidgets/qgslistwidgetwrapper.h | 3 +++ src/gui/qgskeyvaluewidget.cpp | 25 ++++++++++++++++++- src/gui/qgskeyvaluewidget.h | 6 ++++- src/gui/qgslistwidget.cpp | 25 ++++++++++++++++++- src/gui/qgslistwidget.h | 7 +++++- src/gui/qgstablewidgetbase.cpp | 14 +++++++++++ src/gui/qgstablewidgetbase.h | 22 ++++++++++++++++ src/ui/qgstablewidgetuibase.ui | 2 +- 17 files changed, 175 insertions(+), 5 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/qgskeyvaluewidget.sip.in b/python/PyQt6/gui/auto_generated/qgskeyvaluewidget.sip.in index 5e09222255ad..1ef9ca3754ce 100644 --- a/python/PyQt6/gui/auto_generated/qgskeyvaluewidget.sip.in +++ b/python/PyQt6/gui/auto_generated/qgskeyvaluewidget.sip.in @@ -38,6 +38,10 @@ Gets the edit value. :return: the QVariantMap %End + public slots: + + virtual void setReadOnly( bool readOnly ); + }; diff --git a/python/PyQt6/gui/auto_generated/qgslistwidget.sip.in b/python/PyQt6/gui/auto_generated/qgslistwidget.sip.in index d94f0d2c51cf..f6129b0b6af5 100644 --- a/python/PyQt6/gui/auto_generated/qgslistwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/qgslistwidget.sip.in @@ -46,6 +46,11 @@ Check the content is valid :return: ``True`` if valid %End + public slots: + + virtual void setReadOnly( bool readOnly ); + + }; diff --git a/python/PyQt6/gui/auto_generated/qgstablewidgetbase.sip.in b/python/PyQt6/gui/auto_generated/qgstablewidgetbase.sip.in index 3f09390e1215..b851922e5dfc 100644 --- a/python/PyQt6/gui/auto_generated/qgstablewidgetbase.sip.in +++ b/python/PyQt6/gui/auto_generated/qgstablewidgetbase.sip.in @@ -25,6 +25,26 @@ Child classes must call init(QAbstractTableModel*) from their constructor. explicit QgsTableWidgetBase( QWidget *parent ); %Docstring Constructor. +%End + + bool isReadOnly() const; +%Docstring +Returns ``True`` if the widget is shown in a read-only state. + +.. seealso:: :py:func:`setReadOnly` + +.. versionadded:: 3.38 +%End + + public slots: + + virtual void setReadOnly( bool readOnly ); +%Docstring +Sets whether the widget should be shown in a read-only state. + +.. seealso:: :py:func:`isReadOnly` + +.. versionadded:: 3.38 %End protected: diff --git a/python/gui/auto_generated/qgskeyvaluewidget.sip.in b/python/gui/auto_generated/qgskeyvaluewidget.sip.in index 5e09222255ad..1ef9ca3754ce 100644 --- a/python/gui/auto_generated/qgskeyvaluewidget.sip.in +++ b/python/gui/auto_generated/qgskeyvaluewidget.sip.in @@ -38,6 +38,10 @@ Gets the edit value. :return: the QVariantMap %End + public slots: + + virtual void setReadOnly( bool readOnly ); + }; diff --git a/python/gui/auto_generated/qgslistwidget.sip.in b/python/gui/auto_generated/qgslistwidget.sip.in index d94f0d2c51cf..f6129b0b6af5 100644 --- a/python/gui/auto_generated/qgslistwidget.sip.in +++ b/python/gui/auto_generated/qgslistwidget.sip.in @@ -46,6 +46,11 @@ Check the content is valid :return: ``True`` if valid %End + public slots: + + virtual void setReadOnly( bool readOnly ); + + }; diff --git a/python/gui/auto_generated/qgstablewidgetbase.sip.in b/python/gui/auto_generated/qgstablewidgetbase.sip.in index 3f09390e1215..b851922e5dfc 100644 --- a/python/gui/auto_generated/qgstablewidgetbase.sip.in +++ b/python/gui/auto_generated/qgstablewidgetbase.sip.in @@ -25,6 +25,26 @@ Child classes must call init(QAbstractTableModel*) from their constructor. explicit QgsTableWidgetBase( QWidget *parent ); %Docstring Constructor. +%End + + bool isReadOnly() const; +%Docstring +Returns ``True`` if the widget is shown in a read-only state. + +.. seealso:: :py:func:`setReadOnly` + +.. versionadded:: 3.38 +%End + + public slots: + + virtual void setReadOnly( bool readOnly ); +%Docstring +Sets whether the widget should be shown in a read-only state. + +.. seealso:: :py:func:`isReadOnly` + +.. versionadded:: 3.38 %End protected: diff --git a/src/gui/editorwidgets/qgskeyvaluewidgetwrapper.cpp b/src/gui/editorwidgets/qgskeyvaluewidgetwrapper.cpp index da9dd095852a..f44dc213e7ea 100644 --- a/src/gui/editorwidgets/qgskeyvaluewidgetwrapper.cpp +++ b/src/gui/editorwidgets/qgskeyvaluewidgetwrapper.cpp @@ -33,6 +33,14 @@ void QgsKeyValueWidgetWrapper::showIndeterminateState() mWidget->setMap( QVariantMap() ); } +void QgsKeyValueWidgetWrapper::setEnabled( bool enabled ) +{ + if ( mWidget ) + { + mWidget->setReadOnly( !enabled ); + } +} + QWidget *QgsKeyValueWidgetWrapper::createWidget( QWidget *parent ) { if ( isInTable( parent ) ) diff --git a/src/gui/editorwidgets/qgskeyvaluewidgetwrapper.h b/src/gui/editorwidgets/qgskeyvaluewidgetwrapper.h index 1863f50fd346..e7146488b51c 100644 --- a/src/gui/editorwidgets/qgskeyvaluewidgetwrapper.h +++ b/src/gui/editorwidgets/qgskeyvaluewidgetwrapper.h @@ -49,6 +49,8 @@ class GUI_EXPORT QgsKeyValueWidgetWrapper : public QgsEditorWidgetWrapper public: QVariant value() const override; void showIndeterminateState() override; + public slots: + void setEnabled( bool enabled ) override; protected: QWidget *createWidget( QWidget *parent ) override; diff --git a/src/gui/editorwidgets/qgslistwidgetwrapper.cpp b/src/gui/editorwidgets/qgslistwidgetwrapper.cpp index d9ad07c1e380..72899efdbc2f 100644 --- a/src/gui/editorwidgets/qgslistwidgetwrapper.cpp +++ b/src/gui/editorwidgets/qgslistwidgetwrapper.cpp @@ -27,6 +27,14 @@ void QgsListWidgetWrapper::showIndeterminateState() mWidget->setList( QVariantList() ); } +void QgsListWidgetWrapper::setEnabled( bool enabled ) +{ + if ( mWidget ) + { + mWidget->setReadOnly( !enabled ); + } +} + QWidget *QgsListWidgetWrapper::createWidget( QWidget *parent ) { if ( isInTable( parent ) ) diff --git a/src/gui/editorwidgets/qgslistwidgetwrapper.h b/src/gui/editorwidgets/qgslistwidgetwrapper.h index 820677e22c0f..571d1439974b 100644 --- a/src/gui/editorwidgets/qgslistwidgetwrapper.h +++ b/src/gui/editorwidgets/qgslistwidgetwrapper.h @@ -50,6 +50,9 @@ class GUI_EXPORT QgsListWidgetWrapper : public QgsEditorWidgetWrapper QVariant value() const override; void showIndeterminateState() override; + public slots: + void setEnabled( bool enabled ) override; + protected: QWidget *createWidget( QWidget *parent ) override; void initWidget( QWidget *editor ) override; diff --git a/src/gui/qgskeyvaluewidget.cpp b/src/gui/qgskeyvaluewidget.cpp index d0972cc2b33a..07ec875eb918 100644 --- a/src/gui/qgskeyvaluewidget.cpp +++ b/src/gui/qgskeyvaluewidget.cpp @@ -28,6 +28,12 @@ void QgsKeyValueWidget::setMap( const QVariantMap &map ) mModel.setMap( map ); } +void QgsKeyValueWidget::setReadOnly( bool readOnly ) +{ + mModel.setReadOnly( readOnly ); + QgsTableWidgetBase::setReadOnly( readOnly ); +} + ///@cond PRIVATE void QgsKeyValueModel::setMap( const QVariantMap &map ) { @@ -96,6 +102,9 @@ QVariant QgsKeyValueModel::data( const QModelIndex &index, int role ) const bool QgsKeyValueModel::setData( const QModelIndex &index, const QVariant &value, int role ) { + if ( mReadOnly ) + return false; + if ( index.row() < 0 || index.row() >= mLines.count() || role != Qt::EditRole ) { return false; @@ -114,11 +123,17 @@ bool QgsKeyValueModel::setData( const QModelIndex &index, const QVariant &value, Qt::ItemFlags QgsKeyValueModel::flags( const QModelIndex &index ) const { - return QAbstractTableModel::flags( index ) | Qt::ItemIsEditable; + if ( !mReadOnly ) + return QAbstractTableModel::flags( index ) | Qt::ItemIsEditable; + else + return QAbstractTableModel::flags( index ); } bool QgsKeyValueModel::insertRows( int position, int rows, const QModelIndex &parent ) { + if ( mReadOnly ) + return false; + Q_UNUSED( parent ) beginInsertRows( QModelIndex(), position, position + rows - 1 ); for ( int i = 0; i < rows; ++i ) @@ -131,10 +146,18 @@ bool QgsKeyValueModel::insertRows( int position, int rows, const QModelIndex &pa bool QgsKeyValueModel::removeRows( int position, int rows, const QModelIndex &parent ) { + if ( mReadOnly ) + return false; + Q_UNUSED( parent ) beginRemoveRows( QModelIndex(), position, position + rows - 1 ); mLines.remove( position, rows ); endRemoveRows(); return true; } + +void QgsKeyValueModel::setReadOnly( bool readOnly ) +{ + mReadOnly = readOnly; +} ///@endcond diff --git a/src/gui/qgskeyvaluewidget.h b/src/gui/qgskeyvaluewidget.h index e50531760c51..1945a07c3126 100644 --- a/src/gui/qgskeyvaluewidget.h +++ b/src/gui/qgskeyvaluewidget.h @@ -48,10 +48,11 @@ class GUI_EXPORT QgsKeyValueModel : public QAbstractTableModel Qt::ItemFlags flags( const QModelIndex &index ) const override; bool insertRows( int position, int rows, const QModelIndex &parent = QModelIndex() ) override; bool removeRows( int position, int rows, const QModelIndex &parent = QModelIndex() ) override; - + void setReadOnly( bool readOnly ); typedef QPair Line; private: + bool mReadOnly = false; QVector mLines; }; ///@endcond @@ -83,6 +84,9 @@ class GUI_EXPORT QgsKeyValueWidget: public QgsTableWidgetBase */ QVariantMap map() const { return mModel.map(); } + public slots: + + void setReadOnly( bool readOnly ) override; private: QgsKeyValueModel mModel; }; diff --git a/src/gui/qgslistwidget.cpp b/src/gui/qgslistwidget.cpp index b44d73ef723e..c60caebc9f8d 100644 --- a/src/gui/qgslistwidget.cpp +++ b/src/gui/qgslistwidget.cpp @@ -29,6 +29,12 @@ void QgsListWidget::setList( const QVariantList &list ) mModel.setList( list ); } +void QgsListWidget::setReadOnly( bool readOnly ) +{ + mModel.setReadOnly( readOnly ); + QgsTableWidgetBase::setReadOnly( readOnly ); +} + ///@cond PRIVATE QgsListModel::QgsListModel( QVariant::Type subType, QObject *parent ) : @@ -101,6 +107,9 @@ QVariant QgsListModel::data( const QModelIndex &index, int role ) const bool QgsListModel::setData( const QModelIndex &index, const QVariant &value, int role ) { + if ( mReadOnly ) + return false; + if ( index.row() < 0 || index.row() >= mLines.count() || index.column() != 0 || role != Qt::EditRole ) { @@ -113,11 +122,17 @@ bool QgsListModel::setData( const QModelIndex &index, const QVariant &value, int Qt::ItemFlags QgsListModel::flags( const QModelIndex &index ) const { - return QAbstractTableModel::flags( index ) | Qt::ItemIsEditable; + if ( !mReadOnly ) + return QAbstractTableModel::flags( index ) | Qt::ItemIsEditable; + else + return QAbstractTableModel::flags( index ); } bool QgsListModel::insertRows( int position, int rows, const QModelIndex &parent ) { + if ( mReadOnly ) + return false; + Q_UNUSED( parent ) beginInsertRows( QModelIndex(), position, position + rows - 1 ); for ( int i = 0; i < rows; ++i ) @@ -130,6 +145,9 @@ bool QgsListModel::insertRows( int position, int rows, const QModelIndex &parent bool QgsListModel::removeRows( int position, int rows, const QModelIndex &parent ) { + if ( mReadOnly ) + return false; + Q_UNUSED( parent ) beginRemoveRows( QModelIndex(), position, position + rows - 1 ); for ( int i = 0; i < rows; ++i ) @@ -137,4 +155,9 @@ bool QgsListModel::removeRows( int position, int rows, const QModelIndex &parent endRemoveRows(); return true; } + +void QgsListModel::setReadOnly( bool readOnly ) +{ + mReadOnly = readOnly; +} ///@endcond diff --git a/src/gui/qgslistwidget.h b/src/gui/qgslistwidget.h index 10161451eba8..2a96d652e58d 100644 --- a/src/gui/qgslistwidget.h +++ b/src/gui/qgslistwidget.h @@ -48,8 +48,9 @@ class GUI_EXPORT QgsListModel : public QAbstractTableModel Qt::ItemFlags flags( const QModelIndex &index ) const override; bool insertRows( int position, int rows, const QModelIndex &parent = QModelIndex() ) override; bool removeRows( int position, int rows, const QModelIndex &parent = QModelIndex() ) override; - + void setReadOnly( bool readOnly ); private: + bool mReadOnly = false; QVariantList mLines; QVariant::Type mSubType; }; @@ -89,6 +90,10 @@ class GUI_EXPORT QgsListWidget: public QgsTableWidgetBase */ bool valid() const { return mModel.valid(); } + public slots: + + void setReadOnly( bool readOnly ) override; + private: QgsListModel mModel; QVariant::Type mSubType; diff --git a/src/gui/qgstablewidgetbase.cpp b/src/gui/qgstablewidgetbase.cpp index d517a1682465..9c9c342136c5 100644 --- a/src/gui/qgstablewidgetbase.cpp +++ b/src/gui/qgstablewidgetbase.cpp @@ -34,6 +34,9 @@ void QgsTableWidgetBase::init( QAbstractTableModel *model ) void QgsTableWidgetBase::addButton_clicked() { + if ( mReadOnly ) + return; + const QItemSelectionModel *select = tableView->selectionModel(); const int pos = select->hasSelection() ? select->selectedRows()[0].row() : 0; QAbstractItemModel *model = tableView->model(); @@ -46,6 +49,9 @@ void QgsTableWidgetBase::addButton_clicked() void QgsTableWidgetBase::removeButton_clicked() { + if ( mReadOnly ) + return; + const QItemSelectionModel *select = tableView->selectionModel(); // The UI is configured to have single row selection. if ( select->hasSelection() ) @@ -58,3 +64,11 @@ void QgsTableWidgetBase::onSelectionChanged() { removeButton->setEnabled( tableView->selectionModel()->hasSelection() ); } + +void QgsTableWidgetBase::setReadOnly( bool readOnly ) +{ + mReadOnly = readOnly; + + addButton->setEnabled( !mReadOnly ); + removeButton->setEnabled( !mReadOnly && tableView->selectionModel()->hasSelection() ); +} diff --git a/src/gui/qgstablewidgetbase.h b/src/gui/qgstablewidgetbase.h index c9d9c6ca4463..7c4a384bd504 100644 --- a/src/gui/qgstablewidgetbase.h +++ b/src/gui/qgstablewidgetbase.h @@ -39,6 +39,24 @@ class GUI_EXPORT QgsTableWidgetBase: public QWidget, protected Ui::QgsTableWidge */ explicit QgsTableWidgetBase( QWidget *parent ); + /** + * Returns TRUE if the widget is shown in a read-only state. + * + * \see setReadOnly() + * \since QGIS 3.38 + */ + bool isReadOnly() const { return mReadOnly; } + + public slots: + + /** + * Sets whether the widget should be shown in a read-only state. + * + * \see isReadOnly() + * \since QGIS 3.38 + */ + virtual void setReadOnly( bool readOnly ); + protected: /** @@ -71,6 +89,10 @@ class GUI_EXPORT QgsTableWidgetBase: public QWidget, protected Ui::QgsTableWidge */ void onSelectionChanged(); + private: + + bool mReadOnly = false; + friend class TestQgsKeyValueWidget; friend class TestQgsListWidget; diff --git a/src/ui/qgstablewidgetuibase.ui b/src/ui/qgstablewidgetuibase.ui index 1099d5891b92..0fe7f77de485 100644 --- a/src/ui/qgstablewidgetuibase.ui +++ b/src/ui/qgstablewidgetuibase.ui @@ -58,7 +58,7 @@ - + From 317aae6f0ed52b375ed7191c993381431d2e1b7d Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 25 Mar 2024 12:03:36 +1000 Subject: [PATCH 21/87] Hide useless action buttons when list widget is read only --- src/gui/qgstablewidgetbase.cpp | 11 ++++ src/ui/qgstablewidgetuibase.ui | 111 +++++++++++++++++++-------------- 2 files changed, 76 insertions(+), 46 deletions(-) diff --git a/src/gui/qgstablewidgetbase.cpp b/src/gui/qgstablewidgetbase.cpp index 9c9c342136c5..f870acd5ef49 100644 --- a/src/gui/qgstablewidgetbase.cpp +++ b/src/gui/qgstablewidgetbase.cpp @@ -71,4 +71,15 @@ void QgsTableWidgetBase::setReadOnly( bool readOnly ) addButton->setEnabled( !mReadOnly ); removeButton->setEnabled( !mReadOnly && tableView->selectionModel()->hasSelection() ); + + if ( mReadOnly ) + { + mWidgetActions->hide(); + layout()->setSpacing( 0 ); + } + else + { + mWidgetActions->show(); + layout()->setSpacing( 6 ); + } } diff --git a/src/ui/qgstablewidgetuibase.ui b/src/ui/qgstablewidgetuibase.ui index 0fe7f77de485..53cff3e9bdd6 100644 --- a/src/ui/qgstablewidgetuibase.ui +++ b/src/ui/qgstablewidgetuibase.ui @@ -58,55 +58,74 @@ - - - - - Add entry - - - - - - - :/images/themes/default/symbologyAdd.svg:/images/themes/default/symbologyAdd.svg - - - - - - - false - - - Remove entry - - - - - - - :/images/themes/default/symbologyRemove.svg:/images/themes/default/symbologyRemove.svg - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Add entry + + + + + + + :/images/themes/default/symbologyAdd.svg:/images/themes/default/symbologyAdd.svg + + + + + + + false + + + Remove entry + + + + + + + :/images/themes/default/symbologyRemove.svg:/images/themes/default/symbologyRemove.svg + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + tableView + addButton + removeButton + From 51c632cc09dc965c5f1a1fa21f677cdb9ccb1c21 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 25 Mar 2024 12:08:22 +1000 Subject: [PATCH 22/87] Always show a frame around list widget It just looks much better, and gives context to otherwise empty space when the list contains a small number of items --- .../editorwidgets/qgslistwidgetwrapper.cpp | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/gui/editorwidgets/qgslistwidgetwrapper.cpp b/src/gui/editorwidgets/qgslistwidgetwrapper.cpp index 72899efdbc2f..a5e8fc69bbde 100644 --- a/src/gui/editorwidgets/qgslistwidgetwrapper.cpp +++ b/src/gui/editorwidgets/qgslistwidgetwrapper.cpp @@ -37,20 +37,19 @@ void QgsListWidgetWrapper::setEnabled( bool enabled ) QWidget *QgsListWidgetWrapper::createWidget( QWidget *parent ) { + QFrame *ret = new QFrame( parent ); + ret->setFrameShape( QFrame::StyledPanel ); + QHBoxLayout *layout = new QHBoxLayout( ret ); + layout->setContentsMargins( 0, 0, 0, 0 ); + QgsListWidget *widget = new QgsListWidget( field().subType(), ret ); + layout->addWidget( widget ); + if ( isInTable( parent ) ) { - // if to be put in a table, draw a border and set a decent size - QFrame *ret = new QFrame( parent ); - ret->setFrameShape( QFrame::StyledPanel ); - QHBoxLayout *layout = new QHBoxLayout( ret ); - layout->addWidget( new QgsListWidget( field().subType(), ret ) ); + // if to be put in a table, set a decent size ret->setMinimumSize( QSize( 320, 110 ) ); - return ret; - } - else - { - return new QgsListWidget( field().subType(), parent ); } + return ret; } void QgsListWidgetWrapper::initWidget( QWidget *editor ) From fc3fb33beb35ecf5e9361d142295df363234445d Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 25 Mar 2024 13:11:03 +1000 Subject: [PATCH 23/87] Update test --- tests/src/gui/testqgslistwidget.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/src/gui/testqgslistwidget.cpp b/tests/src/gui/testqgslistwidget.cpp index b10192e54012..dcdd32e33ce4 100644 --- a/tests/src/gui/testqgslistwidget.cpp +++ b/tests/src/gui/testqgslistwidget.cpp @@ -73,7 +73,7 @@ class TestQgsListWidget : public QObject QVERIFY( wrapper ); const QSignalSpy spy( wrapper, SIGNAL( valueChanged( const QVariant & ) ) ); - QgsListWidget *widget = qobject_cast< QgsListWidget * >( wrapper->widget() ); + QgsListWidget *widget = wrapper->widget()->findChild(); QVERIFY( widget ); QStringList initial; @@ -108,7 +108,7 @@ class TestQgsListWidget : public QObject QVERIFY( wrapper ); QSignalSpy spy( wrapper, SIGNAL( valueChanged( const QVariant & ) ) ); - QgsListWidget *widget = qobject_cast< QgsListWidget * >( wrapper->widget() ); + QgsListWidget *widget = wrapper->widget()->findChild(); QVERIFY( widget ); QVariantList initial; @@ -160,7 +160,7 @@ class TestQgsListWidget : public QObject QVERIFY( vl_array_int->isValid( ) ); QgsListWidgetWrapper w_array_int( vl_array_int, vl_array_int->fields().indexOf( QLatin1String( "location" ) ), nullptr, nullptr ); - QgsListWidget *widget = qobject_cast< QgsListWidget * >( w_array_int.widget( ) ); + QgsListWidget *widget = w_array_int.widget( )->findChild(); vl_array_int->startEditing( ); QVariantList newList; @@ -204,7 +204,7 @@ class TestQgsListWidget : public QObject QVERIFY( vl_array_str->isValid() ); QgsListWidgetWrapper w_array_str( vl_array_str, vl_array_str->fields().indexOf( QLatin1String( "value" ) ), nullptr, nullptr ); - widget = qobject_cast< QgsListWidget * >( w_array_str.widget( ) ); + widget = w_array_str.widget( )->findChild(); vl_array_str->startEditing( ); QVariantList newListStr; From b20885823004545e94da123813befd414354a69a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 00:44:15 +0000 Subject: [PATCH 24/87] build(deps): bump the npm_and_yarn group across 1 directory with 1 update Bumps the npm_and_yarn group with 1 update in the /resources/server/src/landingpage directory: [express](https://github.com/expressjs/express). Updates `express` from 4.18.2 to 4.19.2 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2) --- updated-dependencies: - dependency-name: express dependency-type: indirect dependency-group: npm_and_yarn-security-group ... Signed-off-by: dependabot[bot] --- resources/server/src/landingpage/yarn.lock | 53 ++++++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/resources/server/src/landingpage/yarn.lock b/resources/server/src/landingpage/yarn.lock index 15e8d09e1958..26b998a0287a 100644 --- a/resources/server/src/landingpage/yarn.lock +++ b/resources/server/src/landingpage/yarn.lock @@ -2764,7 +2764,25 @@ bluebird@^3.1.1: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -body-parser@1.20.1, body-parser@^1.19.0: +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +body-parser@^1.19.0: version "1.20.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== @@ -3337,6 +3355,11 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" @@ -3347,10 +3370,10 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== copy-descriptor@^0.1.0: version "0.1.1" @@ -4230,16 +4253,16 @@ express-history-api-fallback@^2.2.1: integrity sha512-swxwm3aP8vrOOvlzOdZvHlSZtJGwHKaY94J6AkrAgCTmcbko3IRwbkhLv2wKV1WeZhjxX58aLMpP3atDBnKuZg== express@^4.17.1, express@^4.17.3: - version "4.18.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" - integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.1" + body-parser "1.20.2" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.5.0" + cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" @@ -7156,6 +7179,16 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" From 392d7ceadb6091992e408128fa7be052d5a798e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Tue, 26 Mar 2024 10:56:45 +0100 Subject: [PATCH 25/87] [dxf] Make sure selected attribute index is respected when resetting fieldsComboBox index --- src/gui/processing/qgsprocessingdxflayerswidgetwrapper.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/gui/processing/qgsprocessingdxflayerswidgetwrapper.cpp b/src/gui/processing/qgsprocessingdxflayerswidgetwrapper.cpp index dcc0e73705ef..a3b87d359741 100644 --- a/src/gui/processing/qgsprocessingdxflayerswidgetwrapper.cpp +++ b/src/gui/processing/qgsprocessingdxflayerswidgetwrapper.cpp @@ -48,7 +48,9 @@ QgsProcessingDxfLayerDetailsWidget::QgsProcessingDxfLayerDetailsWidget( const QV return; mFieldsComboBox->setLayer( mLayer ); - mFieldsComboBox->setCurrentIndex( layer.layerOutputAttributeIndex() ); + + if ( mLayer->fields().exists( layer.layerOutputAttributeIndex() ) ) + mFieldsComboBox->setField( mLayer->fields().at( layer.layerOutputAttributeIndex() ).name() ); connect( mFieldsComboBox, &QgsFieldComboBox::fieldChanged, this, &QgsPanelWidget::widgetChanged ); } From 66c716d8a3db16f6b6dae373c1c5f43292af9105 Mon Sep 17 00:00:00 2001 From: "Juergen E. Fischer" Date: Sat, 23 Mar 2024 23:30:16 +0100 Subject: [PATCH 26/87] debian packaging: make packaging of api documentation optional --- debian/control | 1 - debian/control.in | 28 ++++++++++++++-------------- debian/rules | 21 +++++++++++++++++---- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/debian/control b/debian/control index 851cf2852570..590317c5ab55 100644 --- a/debian/control +++ b/debian/control @@ -67,7 +67,6 @@ Build-Depends: qttools5-dev-tools, qttools5-dev, git, - doxygen, graphviz, xvfb, xauth, diff --git a/debian/control.in b/debian/control.in index 25e0a500788d..1e37feceb4e1 100644 --- a/debian/control.in +++ b/debian/control.in @@ -81,7 +81,7 @@ Build-Depends: protobuf-compiler, libzstd-dev, git, - doxygen, +#apidoc# doxygen, graphviz, xvfb, xauth, @@ -563,19 +563,19 @@ Description: QGIS server providing various OGC services display databases of geographic information. . This package contains the landing page service. - -Package: qgis-api-doc -Architecture: all -Section: doc -Depends: - ${misc:Depends} -Recommends: - qt5-doc-html -Description: QGIS API documentation - QGIS is a Geographic Information System (GIS) which manages, analyzes and - display databases of geographic information. - . - This package contains the QGIS API documentation. +#apidoc# +#apidoc#Package: qgis-api-doc +#apidoc#Architecture: all +#apidoc#Section: doc +#apidoc#Depends: +#apidoc# ${misc:Depends} +#apidoc#Recommends: +#apidoc# qt5-doc-html +#apidoc#Description: QGIS API documentation +#apidoc# QGIS is a Geographic Information System (GIS) which manages, analyzes and +#apidoc# display databases of geographic information. +#apidoc# . +#apidoc# This package contains the QGIS API documentation. #oracle# #oracle#Package: qgis-oracle-provider #oracle#Architecture: any diff --git a/debian/rules b/debian/rules index 4abb692fb603..71d1645447ec 100755 --- a/debian/rules +++ b/debian/rules @@ -88,8 +88,6 @@ CMAKE_OPTS := \ -DPEDANTIC=TRUE \ -DSERVER_SKIP_ECW=TRUE \ -DQGIS_CGIBIN_SUBDIR=/usr/lib/cgi-bin \ - -DWITH_APIDOC=TRUE \ - -DGENERATE_QHP=TRUE \ -DWITH_CUSTOM_WIDGETS=TRUE \ -DWITH_SERVER=TRUE \ -DWITH_SERVER_PLUGINS=TRUE \ @@ -134,6 +132,13 @@ ifneq (0,$(.SHELLSTATUS)) -DQT5_3DEXTRA_LIBRARY=/usr/lib/$(DEB_BUILD_MULTIARCH)/libQt53DExtras.so endif +ifneq (,$(WITH_APIDOC)) + CMAKE_OPTS += -DWITH_APIDOC=TRUE -DGENERATE_QHP=TRUE +else + CMAKE_OPTS += -DWITH_APIDOC=OFF -DGENERATE_QHP=OFF +endif + + ifneq (,$(WITH_ORACLE)) ifeq ($(DEB_BUILD_ARCH),amd64) ORACLE_INCLUDEDIR=/usr/include/oracle/21/client64/ @@ -195,12 +200,16 @@ endif CONTROL_EXPRESSIONS = $(DISTRIBUTION) grass$(GRASSVER) +ifneq (,$(WITH_APIDOC)) +CONTROL_EXPRESSIONS += apidoc +endif + ifneq (,$(WITH_ORACLE)) - CONTROL_EXPRESSIONS += oracle +CONTROL_EXPRESSIONS += oracle endif ifeq ($(shell pkg-config --exists pdal && dpkg --compare-versions $$(pkg-config --modversion pdal) ge 2.5 && echo 1 || echo 0),1) - CONTROL_EXPRESSIONS += pdal_wrench +CONTROL_EXPRESSIONS += pdal_wrench endif define gentemplate @@ -253,7 +262,9 @@ else ninja $(NINJA_OPTS) -C $(QGIS_BUILDDIR) endif +ifneq (,$(WITH_APIDOC)) ninja $(NINJA_OPTS) -C $(QGIS_BUILDDIR) apidoc +endif override_dh_auto_test: test-stamp @@ -280,8 +291,10 @@ endif override_dh_auto_install: DESTDIR=$(CURDIR)/debian/tmp ninja $(NINJA_OPTS) -C $(QGIS_BUILDDIR) install +ifneq (,$(WITH_APIDOC)) # remove unwanted files $(RM) $(CURDIR)/debian/tmp/usr/share/qgis/doc/api/installdox +endif # replace leaflet and jquery urls perl -i -p \ From f3151e61b8d40c324055e9f7543d18dce4ac16a9 Mon Sep 17 00:00:00 2001 From: Andrea Giudiceandrea Date: Wed, 27 Mar 2024 07:16:44 +0100 Subject: [PATCH 27/87] [OGR] Fix GeoPackage layer metadata reading --- src/core/providers/ogr/qgsogrprovider.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/providers/ogr/qgsogrprovider.cpp b/src/core/providers/ogr/qgsogrprovider.cpp index 7987ffcbc52d..e7bfd5380e44 100644 --- a/src/core/providers/ogr/qgsogrprovider.cpp +++ b/src/core/providers/ogr/qgsogrprovider.cpp @@ -1031,7 +1031,7 @@ void QgsOgrProvider::loadMetadata() QgsSqliteUtils::quotedString( QStringLiteral( "http://mrcc.com/qgis.dtd" ) ), QgsSqliteUtils::quotedString( QStringLiteral( "table" ) ) ); - if ( QgsOgrLayerUniquePtr l = mOgrOrigLayer->ExecuteSQL( sql.toLocal8Bit().constData() ) ) + if ( QgsOgrLayerUniquePtr l = mOgrOrigLayer->ExecuteSQL( sql.toUtf8().constData() ) ) { gdal::ogr_feature_unique_ptr f( l->GetNextFeature() ); if ( f ) From 713b5be010e9dab287f310f9728193b25bd8a394 Mon Sep 17 00:00:00 2001 From: Julien Cabieces Date: Wed, 27 Mar 2024 15:32:27 +0100 Subject: [PATCH 28/87] constify Co-authored-by: Paul Blottiere --- src/server/services/wfs/qgswfsgetfeature.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/services/wfs/qgswfsgetfeature.cpp b/src/server/services/wfs/qgswfsgetfeature.cpp index 7d7149779fa4..9f8763794019 100644 --- a/src/server/services/wfs/qgswfsgetfeature.cpp +++ b/src/server/services/wfs/qgswfsgetfeature.cpp @@ -1184,7 +1184,7 @@ namespace QgsWfs json value; QgsJsonUtils::addCrsInfo( value, destinationCrs ); - for ( auto it : value.items() ) + for ( const auto &it : value.items() ) { fcString += " \"" + QString::fromStdString( it.key() ) + "\": " + QString::fromStdString( it.value().dump() ) + ",\n"; } From bd6489303a77182a8aa46d5634166bd00d4d2173 Mon Sep 17 00:00:00 2001 From: Blottiere Paul Date: Thu, 28 Mar 2024 15:09:49 +0100 Subject: [PATCH 29/87] Remove the corresponding capabilities doc in cache when a project is removed from cache --- python/PyQt6/server/auto_generated/qgsconfigcache.sip.in | 3 +++ python/server/auto_generated/qgsconfigcache.sip.in | 3 +++ src/server/qgsconfigcache.cpp | 2 ++ src/server/qgsconfigcache.h | 3 +++ src/server/qgsserver.cpp | 2 ++ 5 files changed, 13 insertions(+) diff --git a/python/PyQt6/server/auto_generated/qgsconfigcache.sip.in b/python/PyQt6/server/auto_generated/qgsconfigcache.sip.in index a1ed747f785f..ee420039fb03 100644 --- a/python/PyQt6/server/auto_generated/qgsconfigcache.sip.in +++ b/python/PyQt6/server/auto_generated/qgsconfigcache.sip.in @@ -78,6 +78,9 @@ Initialize from settings %End + signals: + void projectRemovedFromCache( const QString &path ); + private: QgsConfigCache(); public slots: diff --git a/python/server/auto_generated/qgsconfigcache.sip.in b/python/server/auto_generated/qgsconfigcache.sip.in index a1ed747f785f..ee420039fb03 100644 --- a/python/server/auto_generated/qgsconfigcache.sip.in +++ b/python/server/auto_generated/qgsconfigcache.sip.in @@ -78,6 +78,9 @@ Initialize from settings %End + signals: + void projectRemovedFromCache( const QString &path ); + private: QgsConfigCache(); public slots: diff --git a/src/server/qgsconfigcache.cpp b/src/server/qgsconfigcache.cpp index 9c0728c96602..6731eb6fab81 100644 --- a/src/server/qgsconfigcache.cpp +++ b/src/server/qgsconfigcache.cpp @@ -249,6 +249,8 @@ void QgsConfigCache::removeEntry( const QString &path ) mXmlDocumentCache.remove( path ); mStrategy->entryRemoved( path ); + + emit projectRemovedFromCache( path ); } // slots diff --git a/src/server/qgsconfigcache.h b/src/server/qgsconfigcache.h index 00fe0adcdf88..b4b956ca1527 100644 --- a/src/server/qgsconfigcache.h +++ b/src/server/qgsconfigcache.h @@ -128,6 +128,9 @@ class SERVER_EXPORT QgsConfigCache : public QObject //! Initialize with a strategy implementation. QgsConfigCache( QgsAbstractCacheStrategy *strategy ) SIP_SKIP; + signals: + void projectRemovedFromCache( const QString &path ); + private: // SIP require this QgsConfigCache() SIP_FORCE; diff --git a/src/server/qgsserver.cpp b/src/server/qgsserver.cpp index 37cb497b60ef..52475fb5d5c3 100644 --- a/src/server/qgsserver.cpp +++ b/src/server/qgsserver.cpp @@ -368,6 +368,8 @@ bool QgsServer::init() // Initialize config cache QgsConfigCache::initialize( sSettings ); + QObject::connect( QgsConfigCache::instance(), &QgsConfigCache::projectRemovedFromCache, sCapabilitiesCache, &QgsCapabilitiesCache::removeCapabilitiesDocument ); + sInitialized = true; QgsMessageLog::logMessage( QStringLiteral( "Server initialized" ), QStringLiteral( "Server" ), Qgis::MessageLevel::Info ); return true; From f8b6a0b3f30d0863c6825468f6f25697dd09e31a Mon Sep 17 00:00:00 2001 From: Blottiere Paul Date: Thu, 28 Mar 2024 15:26:23 +0100 Subject: [PATCH 30/87] Add doc --- python/PyQt6/server/auto_generated/qgsconfigcache.sip.in | 5 +++++ python/server/auto_generated/qgsconfigcache.sip.in | 5 +++++ src/server/qgsconfigcache.h | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/python/PyQt6/server/auto_generated/qgsconfigcache.sip.in b/python/PyQt6/server/auto_generated/qgsconfigcache.sip.in index ee420039fb03..f2a150575ea3 100644 --- a/python/PyQt6/server/auto_generated/qgsconfigcache.sip.in +++ b/python/PyQt6/server/auto_generated/qgsconfigcache.sip.in @@ -80,6 +80,11 @@ Initialize from settings signals: void projectRemovedFromCache( const QString &path ); +%Docstring +Emitted whenever a project is removed from the cache. + +.. versionadded:: 3.38 +%End private: QgsConfigCache(); diff --git a/python/server/auto_generated/qgsconfigcache.sip.in b/python/server/auto_generated/qgsconfigcache.sip.in index ee420039fb03..f2a150575ea3 100644 --- a/python/server/auto_generated/qgsconfigcache.sip.in +++ b/python/server/auto_generated/qgsconfigcache.sip.in @@ -80,6 +80,11 @@ Initialize from settings signals: void projectRemovedFromCache( const QString &path ); +%Docstring +Emitted whenever a project is removed from the cache. + +.. versionadded:: 3.38 +%End private: QgsConfigCache(); diff --git a/src/server/qgsconfigcache.h b/src/server/qgsconfigcache.h index b4b956ca1527..a64756feec5c 100644 --- a/src/server/qgsconfigcache.h +++ b/src/server/qgsconfigcache.h @@ -129,6 +129,10 @@ class SERVER_EXPORT QgsConfigCache : public QObject QgsConfigCache( QgsAbstractCacheStrategy *strategy ) SIP_SKIP; signals: + /** + * Emitted whenever a project is removed from the cache. + * \since QGIS 3.38 + */ void projectRemovedFromCache( const QString &path ); private: From 46069292c8767af67c41cfc9779838e32539caed Mon Sep 17 00:00:00 2001 From: Blottiere Paul Date: Thu, 28 Mar 2024 15:32:43 +0100 Subject: [PATCH 31/87] Fix doc --- python/PyQt6/server/auto_generated/qgsconfigcache.sip.in | 1 + python/server/auto_generated/qgsconfigcache.sip.in | 1 + src/server/qgsconfigcache.h | 1 + 3 files changed, 3 insertions(+) diff --git a/python/PyQt6/server/auto_generated/qgsconfigcache.sip.in b/python/PyQt6/server/auto_generated/qgsconfigcache.sip.in index f2a150575ea3..145e0cc54a21 100644 --- a/python/PyQt6/server/auto_generated/qgsconfigcache.sip.in +++ b/python/PyQt6/server/auto_generated/qgsconfigcache.sip.in @@ -79,6 +79,7 @@ Initialize from settings signals: + void projectRemovedFromCache( const QString &path ); %Docstring Emitted whenever a project is removed from the cache. diff --git a/python/server/auto_generated/qgsconfigcache.sip.in b/python/server/auto_generated/qgsconfigcache.sip.in index f2a150575ea3..145e0cc54a21 100644 --- a/python/server/auto_generated/qgsconfigcache.sip.in +++ b/python/server/auto_generated/qgsconfigcache.sip.in @@ -79,6 +79,7 @@ Initialize from settings signals: + void projectRemovedFromCache( const QString &path ); %Docstring Emitted whenever a project is removed from the cache. diff --git a/src/server/qgsconfigcache.h b/src/server/qgsconfigcache.h index a64756feec5c..b0910a34320a 100644 --- a/src/server/qgsconfigcache.h +++ b/src/server/qgsconfigcache.h @@ -129,6 +129,7 @@ class SERVER_EXPORT QgsConfigCache : public QObject QgsConfigCache( QgsAbstractCacheStrategy *strategy ) SIP_SKIP; signals: + /** * Emitted whenever a project is removed from the cache. * \since QGIS 3.38 From 2b00dc1cde13fa2dfaa989921b564cdba76207c4 Mon Sep 17 00:00:00 2001 From: Blottiere Paul Date: Fri, 29 Mar 2024 09:43:03 +0100 Subject: [PATCH 32/87] Explicit slot --- .../PyQt6/server/auto_generated/qgscapabilitiescache.sip.in | 4 +++- python/server/auto_generated/qgscapabilitiescache.sip.in | 4 +++- src/server/qgscapabilitiescache.h | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/python/PyQt6/server/auto_generated/qgscapabilitiescache.sip.in b/python/PyQt6/server/auto_generated/qgscapabilitiescache.sip.in index 55f3f1db35a6..249c4c584275 100644 --- a/python/PyQt6/server/auto_generated/qgscapabilitiescache.sip.in +++ b/python/PyQt6/server/auto_generated/qgscapabilitiescache.sip.in @@ -39,9 +39,11 @@ Inserts new capabilities document (creates a copy of the document, does not take :param doc: the DOM document %End + public slots: + void removeCapabilitiesDocument( const QString &path ); %Docstring -Remove capabilities document +Removes capabilities document :param path: the project file path %End diff --git a/python/server/auto_generated/qgscapabilitiescache.sip.in b/python/server/auto_generated/qgscapabilitiescache.sip.in index 55f3f1db35a6..249c4c584275 100644 --- a/python/server/auto_generated/qgscapabilitiescache.sip.in +++ b/python/server/auto_generated/qgscapabilitiescache.sip.in @@ -39,9 +39,11 @@ Inserts new capabilities document (creates a copy of the document, does not take :param doc: the DOM document %End + public slots: + void removeCapabilitiesDocument( const QString &path ); %Docstring -Remove capabilities document +Removes capabilities document :param path: the project file path %End diff --git a/src/server/qgscapabilitiescache.h b/src/server/qgscapabilitiescache.h index cea69332b5ef..7ea9a0560444 100644 --- a/src/server/qgscapabilitiescache.h +++ b/src/server/qgscapabilitiescache.h @@ -53,8 +53,10 @@ class SERVER_EXPORT QgsCapabilitiesCache : public QObject */ void insertCapabilitiesDocument( const QString &configFilePath, const QString &key, const QDomDocument *doc ); + public slots: + /** - * Remove capabilities document + * Removes capabilities document * \param path the project file path */ void removeCapabilitiesDocument( const QString &path ); From c93258e2b92aa87d0b20f35a96c2236790ffab7a 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 33/87] 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 eb3b13e67b3a69ccabf33467ce8078f3264b1c55 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 34/87] 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 7c0713fac82a7c99901d06a404404ce23c96a674 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 35/87] 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 430dbf9e22c4d39371d3551a43cb274cad1c6836 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 36/87] [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 67bd1a26ee3f7c29f34d3c02599c4054c1088c1f 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 37/87] [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; From 1e9cf8f68b3eabf33889dfac6469185848eb1104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Wed, 20 Mar 2024 17:48:38 +0100 Subject: [PATCH 38/87] [dxf] Special tag for DXF settings XML document and correspondent validation --- src/app/qgsdxfexportdialog.cpp | 54 ++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index 874d33982b04..429f70b2db01 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -840,7 +840,7 @@ void QgsDxfExportDialog::loadSettingsFromFile() { 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, errorMessage ) ); + QMessageBox::information( this, tr( "Load DXF settings" ), tr( "ERROR: Failed to load DXF Export settings file as %1.\n\n%2" ).arg( fileName, errorMessage ) ); else { QgsDxfExportDialog::settingsDxfLastSettingsDir->setValue( QFileInfo( fileName ).path() ); @@ -859,55 +859,62 @@ bool QgsDxfExportDialog::loadSettingsFromXML( QDomDocument &doc, QString &errorM return false; } + const QDomElement dxfElement = rootElement.firstChildElement( QStringLiteral( "dxf_settings" ) ); + if ( dxfElement.isNull() ) + { + errorMessage = tr( "The XML file does not correspond to DXF settings. It must have a element." ); + return false; + } + QDomElement mne; QVariant value; - mne = rootElement.namedItem( QStringLiteral( "symbology_mode" ) ).toElement(); + mne = dxfElement.namedItem( QStringLiteral( "symbology_mode" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mSymbologyModeComboBox->setCurrentIndex( value.toInt() ); - mne = rootElement.namedItem( QStringLiteral( "symbology_scale" ) ).toElement(); + mne = dxfElement.namedItem( QStringLiteral( "symbology_scale" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mScaleWidget->setScale( value.toDouble() ); - mne = rootElement.namedItem( QStringLiteral( "encoding" ) ).toElement(); + mne = dxfElement.namedItem( QStringLiteral( "encoding" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mEncoding->setCurrentText( value.toString() ); - mne = rootElement.namedItem( QStringLiteral( "crs" ) ).toElement(); + mne = dxfElement.namedItem( QStringLiteral( "crs" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mCrsSelector->setCrs( value.value< QgsCoordinateReferenceSystem >() ); - mne = rootElement.namedItem( QStringLiteral( "map_theme" ) ).toElement(); + mne = dxfElement.namedItem( QStringLiteral( "map_theme" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mVisibilityPresets->setCurrentText( value.toString() ); - mne = rootElement.namedItem( QStringLiteral( "use_layer_title" ) ).toElement(); + mne = dxfElement.namedItem( QStringLiteral( "use_layer_title" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mLayerTitleAsName->setChecked( value == true ); - mne = rootElement.namedItem( QStringLiteral( "use_map_extent" ) ).toElement(); + mne = dxfElement.namedItem( QStringLiteral( "use_map_extent" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mMapExtentCheckBox->setChecked( value == true ); - mne = rootElement.namedItem( QStringLiteral( "force_2d" ) ).toElement(); + mne = dxfElement.namedItem( QStringLiteral( "force_2d" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mForce2d->setChecked( value == true ); - mne = rootElement.namedItem( QStringLiteral( "mtext" ) ).toElement(); + mne = dxfElement.namedItem( QStringLiteral( "mtext" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mMTextCheckBox->setChecked( value == true ); - mne = rootElement.namedItem( QStringLiteral( "selected_features_only" ) ).toElement(); + mne = dxfElement.namedItem( QStringLiteral( "selected_features_only" ) ).toElement(); value = QgsXmlUtils::readVariant( mne.firstChildElement() ); if ( !value.isNull() ) mSelectedFeaturesOnly->setChecked( value == true ); @@ -977,25 +984,28 @@ void QgsDxfExportDialog::saveSettingsToXML( QDomDocument &doc ) const rootElement.setAttribute( QStringLiteral( "version" ), Qgis::version() ); domDocument.appendChild( rootElement ); + QDomElement dxfElement = domDocument.createElement( QStringLiteral( "dxf_settings" ) ); + rootElement.appendChild( dxfElement ); + QDomElement symbologyModeElement = domDocument.createElement( QStringLiteral( "symbology_mode" ) ); symbologyModeElement.appendChild( QgsXmlUtils::writeVariant( static_cast( symbologyMode() ), doc ) ); - rootElement.appendChild( symbologyModeElement ); + dxfElement.appendChild( symbologyModeElement ); QDomElement symbologyScaleElement = domDocument.createElement( QStringLiteral( "symbology_scale" ) ); symbologyScaleElement.appendChild( QgsXmlUtils::writeVariant( symbologyScale(), doc ) ); - rootElement.appendChild( symbologyScaleElement ); + dxfElement.appendChild( symbologyScaleElement ); QDomElement encodingElement = domDocument.createElement( QStringLiteral( "encoding" ) ); encodingElement.appendChild( QgsXmlUtils::writeVariant( encoding(), doc ) ); - rootElement.appendChild( encodingElement ); + dxfElement.appendChild( encodingElement ); QDomElement crsElement = domDocument.createElement( QStringLiteral( "crs" ) ); crsElement.appendChild( QgsXmlUtils::writeVariant( crs(), doc ) ); - rootElement.appendChild( crsElement ); + dxfElement.appendChild( crsElement ); QDomElement mapThemeElement = domDocument.createElement( QStringLiteral( "map_theme" ) ); mapThemeElement.appendChild( QgsXmlUtils::writeVariant( mapTheme(), doc ) ); - rootElement.appendChild( mapThemeElement ); + dxfElement.appendChild( mapThemeElement ); QDomElement layersElement = domDocument.createElement( QStringLiteral( "layers" ) ); for ( const auto dxfLayer : layers() ) @@ -1008,27 +1018,27 @@ void QgsDxfExportDialog::saveSettingsToXML( QDomDocument &doc ) const layerElement.setAttribute( QStringLiteral( "max_number_of_classes" ), dxfLayer.dataDefinedBlocksMaximumNumberOfClasses() ) ; layersElement.appendChild( layerElement ); } - rootElement.appendChild( layersElement ); + dxfElement.appendChild( layersElement ); QDomElement titleAsNameElement = domDocument.createElement( QStringLiteral( "use_layer_title" ) ); titleAsNameElement.appendChild( QgsXmlUtils::writeVariant( layerTitleAsName(), doc ) ); - rootElement.appendChild( titleAsNameElement ); + dxfElement.appendChild( titleAsNameElement ); QDomElement useMapExtentElement = domDocument.createElement( QStringLiteral( "use_map_extent" ) ); useMapExtentElement.appendChild( QgsXmlUtils::writeVariant( exportMapExtent(), doc ) ); - rootElement.appendChild( useMapExtentElement ); + dxfElement.appendChild( useMapExtentElement ); QDomElement force2dElement = domDocument.createElement( QStringLiteral( "force_2d" ) ); force2dElement.appendChild( QgsXmlUtils::writeVariant( force2d(), doc ) ); - rootElement.appendChild( force2dElement ); + dxfElement.appendChild( force2dElement ); QDomElement useMTextElement = domDocument.createElement( QStringLiteral( "mtext" ) ); useMTextElement.appendChild( QgsXmlUtils::writeVariant( useMText(), doc ) ); - rootElement.appendChild( useMTextElement ); + dxfElement.appendChild( useMTextElement ); QDomElement selectedFeatures = domDocument.createElement( QStringLiteral( "selected_features_only" ) ); selectedFeatures.appendChild( QgsXmlUtils::writeVariant( selectedFeaturesOnly(), doc ) ); - rootElement.appendChild( selectedFeatures ); + dxfElement.appendChild( selectedFeatures ); doc = domDocument; } From 51596fa1262fdf72f755608fde06e0941845ea5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Thu, 21 Mar 2024 16:14:50 +0100 Subject: [PATCH 39/87] [dxf] Save and restore layer settings (XML) in DXF dialog --- src/app/qgsdxfexportdialog.cpp | 84 ++++++++++++++++++++++++---------- src/app/qgsdxfexportdialog.h | 1 + 2 files changed, 62 insertions(+), 23 deletions(-) diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index 429f70b2db01..14f51659587c 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -866,56 +866,91 @@ bool QgsDxfExportDialog::loadSettingsFromXML( QDomDocument &doc, QString &errorM return false; } - QDomElement mne; + QDomElement element; QVariant value; - mne = dxfElement.namedItem( QStringLiteral( "symbology_mode" ) ).toElement(); - value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + element = dxfElement.namedItem( QStringLiteral( "symbology_mode" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); if ( !value.isNull() ) mSymbologyModeComboBox->setCurrentIndex( value.toInt() ); - mne = dxfElement.namedItem( QStringLiteral( "symbology_scale" ) ).toElement(); - value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + element = dxfElement.namedItem( QStringLiteral( "symbology_scale" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); if ( !value.isNull() ) mScaleWidget->setScale( value.toDouble() ); - mne = dxfElement.namedItem( QStringLiteral( "encoding" ) ).toElement(); - value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + element = dxfElement.namedItem( QStringLiteral( "encoding" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); if ( !value.isNull() ) mEncoding->setCurrentText( value.toString() ); - mne = dxfElement.namedItem( QStringLiteral( "crs" ) ).toElement(); - value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + element = dxfElement.namedItem( QStringLiteral( "crs" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); if ( !value.isNull() ) mCrsSelector->setCrs( value.value< QgsCoordinateReferenceSystem >() ); - mne = dxfElement.namedItem( QStringLiteral( "map_theme" ) ).toElement(); - value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + element = dxfElement.namedItem( QStringLiteral( "map_theme" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); if ( !value.isNull() ) mVisibilityPresets->setCurrentText( value.toString() ); - mne = dxfElement.namedItem( QStringLiteral( "use_layer_title" ) ).toElement(); - value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + // layer settings + element = dxfElement.namedItem( QStringLiteral( "layers" ) ).toElement(); + QDomNodeList layerNodeList = element.elementsByTagName( QStringLiteral( "layer" ) ); + const QgsReadWriteContext rwContext = QgsReadWriteContext(); + + QgsVectorLayer *vl; + QgsVectorLayerRef vlRef; + + for ( int i = 0; i < layerNodeList.length(); i++ ) + { + element = layerNodeList.at( i ).toElement(); + if ( vlRef.readXml( element, rwContext ) ) + { + vl = vlRef.resolveWeakly( QgsProject::instance() ); + if ( vl ) + { + QgsLayerTreeLayer *treeNode = mLayerTreeGroup->findLayer( vl ); + QModelIndex idx = mModel->node2index( treeNode ); + + idx = mModel->index( idx.row(), OUTPUT_LAYER_ATTRIBUTE_COL, idx.parent() ); + mModel->setData( idx, element.attribute( QStringLiteral( "attribute-index" ), QStringLiteral( "-1" ) ) ); + + idx = mModel->index( idx.row(), ALLOW_DD_SYMBOL_BLOCKS_COL, idx.parent() ); + mModel->setData( idx, element.attribute( QStringLiteral( "use_symbol_blocks" ), QStringLiteral( "0" ) ), Qt::CheckStateRole ); + + idx = mModel->index( idx.row(), MAXIMUM_DD_SYMBOL_BLOCKS_COL, idx.parent() ); + mModel->setData( idx, element.attribute( QStringLiteral( "max_number_of_classes" ), QStringLiteral( "-1" ) ) ); + } + else + { + QgsDebugMsgLevel( QStringLiteral( " Layer '%1' found in the DXF settings XML file, but not present in the project." ).arg( element.attribute( QStringLiteral( "name" ) ) ), 1 ); + } + } + } + + element = dxfElement.namedItem( QStringLiteral( "use_layer_title" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); if ( !value.isNull() ) mLayerTitleAsName->setChecked( value == true ); - mne = dxfElement.namedItem( QStringLiteral( "use_map_extent" ) ).toElement(); - value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + element = dxfElement.namedItem( QStringLiteral( "use_map_extent" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); if ( !value.isNull() ) mMapExtentCheckBox->setChecked( value == true ); - mne = dxfElement.namedItem( QStringLiteral( "force_2d" ) ).toElement(); - value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + element = dxfElement.namedItem( QStringLiteral( "force_2d" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); if ( !value.isNull() ) mForce2d->setChecked( value == true ); - mne = dxfElement.namedItem( QStringLiteral( "mtext" ) ).toElement(); - value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + element = dxfElement.namedItem( QStringLiteral( "mtext" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); if ( !value.isNull() ) mMTextCheckBox->setChecked( value == true ); - mne = dxfElement.namedItem( QStringLiteral( "selected_features_only" ) ).toElement(); - value = QgsXmlUtils::readVariant( mne.firstChildElement() ); + element = dxfElement.namedItem( QStringLiteral( "selected_features_only" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); if ( !value.isNull() ) mSelectedFeaturesOnly->setChecked( value == true ); @@ -1008,11 +1043,14 @@ void QgsDxfExportDialog::saveSettingsToXML( QDomDocument &doc ) const dxfElement.appendChild( mapThemeElement ); QDomElement layersElement = domDocument.createElement( QStringLiteral( "layers" ) ); + QgsVectorLayerRef vlRef; + const QgsReadWriteContext rwContext = QgsReadWriteContext(); + for ( const auto dxfLayer : layers() ) { - QgsVectorLayer *vl = dxfLayer.layer(); QDomElement layerElement = domDocument.createElement( QStringLiteral( "layer" ) ); - layerElement.setAttribute( QStringLiteral( "source" ), vl->publicSource() ); + vlRef.setLayer( dxfLayer.layer() ); + vlRef.writeXml( layerElement, rwContext ); 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 4afb9e33ac7d..59beaafb57f2 100644 --- a/src/app/qgsdxfexportdialog.h +++ b/src/app/qgsdxfexportdialog.h @@ -25,6 +25,7 @@ #include "qgssettingstree.h" #include "qgssettingsentryimpl.h" #include "qgsxmlutils.h" +#include "qgsvectorlayerref.h" #include #include From ddc75a5e5e1231bd07e8b58b02320f0ffcef16f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Thu, 21 Mar 2024 18:19:26 +0100 Subject: [PATCH 40/87] [dxf] Non-modal success message for DXF Export dialog --- src/app/qgsdxfexportdialog.cpp | 6 +- src/app/qgsdxfexportdialog.h | 2 + src/ui/qgsdxfexportdialogbase.ui | 220 ++++++++++++++++--------------- 3 files changed, 119 insertions(+), 109 deletions(-) diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index 14f51659587c..850f90d37ee3 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -739,6 +739,10 @@ QgsDxfExportDialog::QgsDxfExportDialog( QWidget *parent, Qt::WindowFlags f ) mBtnLoadSaveSettings->setMenu( menuSettings ); buttonBox->addButton( mBtnLoadSaveSettings, QDialogButtonBox::ResetRole ); + mMessageBar = new QgsMessageBar(); + mMessageBar->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Fixed ); + mainLayout->insertWidget( 0, mMessageBar ); + mModel->loadLayersOutputAttribute( mModel->rootGroup() ); } @@ -844,7 +848,7 @@ void QgsDxfExportDialog::loadSettingsFromFile() else { QgsDxfExportDialog::settingsDxfLastSettingsDir->setValue( QFileInfo( fileName ).path() ); - QMessageBox::information( this, tr( "Load DXF settings" ), tr( "DXF Export settings loaded!" ) ); + mMessageBar->pushMessage( QString(), tr( "DXF Export settings loaded!" ), Qgis::MessageLevel::Success, 0 ); } } } diff --git a/src/app/qgsdxfexportdialog.h b/src/app/qgsdxfexportdialog.h index 59beaafb57f2..d811e43bf08b 100644 --- a/src/app/qgsdxfexportdialog.h +++ b/src/app/qgsdxfexportdialog.h @@ -26,6 +26,7 @@ #include "qgssettingsentryimpl.h" #include "qgsxmlutils.h" #include "qgsvectorlayerref.h" +#include "qgsmessagebar.h" #include #include @@ -147,6 +148,7 @@ class QgsDxfExportDialog : public QDialog, private Ui::QgsDxfExportDialogBase QgsVectorLayerAndAttributeModel *mModel = nullptr; QgsDxfExportLayerTreeView *mTreeView = nullptr; QPushButton *mBtnLoadSaveSettings = nullptr; + QgsMessageBar *mMessageBar = nullptr; QgsCoordinateReferenceSystem mCRS; }; diff --git a/src/ui/qgsdxfexportdialogbase.ui b/src/ui/qgsdxfexportdialogbase.ui index 2a3acbc211d8..46baa4792078 100644 --- a/src/ui/qgsdxfexportdialogbase.ui +++ b/src/ui/qgsdxfexportdialogbase.ui @@ -13,93 +13,121 @@ DXF Export - - - - - - 0 - 0 - - - - Symbology mode - - - - - - - - 0 - 0 - - - - Save as - - - - - - - - - - - 0 - 0 - - - - Symbology scale - - - - - - + + + + + + + + 0 + 0 + + + + Save as + + + + + + + + + + + 0 + 0 + + + + Symbology mode + + + + + + + + + + + 0 + 0 + + + + Symbology scale + + + + + + + Qt::StrongFocus + + + true + + + + + + + + 0 + 0 + + + + Encoding + + + + + + + + + + + 0 + 0 + + + + CRS + + + + + + + Qt::StrongFocus + + + + + + + + 0 + 0 + + + + Map themes + + + + + + - - - - Qt::StrongFocus - - - true - - - - - - - Qt::StrongFocus - - - - - - - - - - - - - - 0 - 0 - - - - CRS - - - @@ -132,32 +160,6 @@ - - - - - 0 - 0 - - - - Encoding - - - - - - - - 0 - 0 - - - - Map themes - - - @@ -224,6 +226,8 @@ + + From f26786c11f5af13803bf223452953a3e77c0a30b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Fri, 22 Mar 2024 10:14:24 +0100 Subject: [PATCH 41/87] [dxf] Adjust tab order in DXF Export dialog --- src/ui/qgsdxfexportdialogbase.ui | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ui/qgsdxfexportdialogbase.ui b/src/ui/qgsdxfexportdialogbase.ui index 46baa4792078..16b1a6890140 100644 --- a/src/ui/qgsdxfexportdialogbase.ui +++ b/src/ui/qgsdxfexportdialogbase.ui @@ -257,9 +257,12 @@ mVisibilityPresets mSelectAllButton mDeselectAllButton + mSelectDataDefinedBlocks + mDeselectDataDefinedBlocks mLayerTitleAsName mMTextCheckBox mMapExtentCheckBox + mSelectedFeaturesOnly mForce2d buttonBox From 3eded68a244afa28975c4f9eb123477d2855eff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Wed, 27 Mar 2024 14:19:44 +0100 Subject: [PATCH 42/87] [dxf] Adress review on title case consistency --- src/app/qgsdxfexportdialog.cpp | 19 +++++++++---------- src/ui/qgsdxfexportdialogbase.ui | 4 ++-- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index 850f90d37ee3..cecb8dc24c6a 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -811,7 +811,7 @@ void QgsDxfExportDialog::deselectDataDefinedBlocks() void QgsDxfExportDialog::loadSettingsFromFile() { - const QString fileName = QFileDialog::getOpenFileName( this, tr( "Load DXF Export settings" ), + const QString fileName = QFileDialog::getOpenFileName( this, tr( "Load DXF Export Settings" ), QgsDxfExportDialog::settingsDxfLastSettingsDir->value(), tr( "XML file" ) + " (*.xml)" ); if ( fileName.isNull() ) @@ -839,12 +839,12 @@ void QgsDxfExportDialog::loadSettingsFromFile() } if ( QMessageBox::question( this, - tr( "DXF Export load from XML file" ), + 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( domDocument, errorMessage ); if ( !resultFlag ) - QMessageBox::information( this, tr( "Load DXF settings" ), tr( "ERROR: Failed to load DXF Export settings file as %1.\n\n%2" ).arg( fileName, errorMessage ) ); + QMessageBox::information( this, tr( "Load DXF Export Settings" ), tr( "ERROR: Failed to load DXF Export settings file as %1.\n\n%2" ).arg( fileName, errorMessage ) ); else { QgsDxfExportDialog::settingsDxfLastSettingsDir->setValue( QFileInfo( fileName ).path() ); @@ -866,7 +866,7 @@ bool QgsDxfExportDialog::loadSettingsFromXML( QDomDocument &doc, QString &errorM const QDomElement dxfElement = rootElement.firstChildElement( QStringLiteral( "dxf_settings" ) ); if ( dxfElement.isNull() ) { - errorMessage = tr( "The XML file does not correspond to DXF settings. It must have a element." ); + errorMessage = tr( "The XML file does not correspond to DXF Export settings. It must have a element." ); return false; } @@ -928,7 +928,7 @@ bool QgsDxfExportDialog::loadSettingsFromXML( QDomDocument &doc, QString &errorM } else { - QgsDebugMsgLevel( QStringLiteral( " Layer '%1' found in the DXF settings XML file, but not present in the project." ).arg( element.attribute( QStringLiteral( "name" ) ) ), 1 ); + QgsDebugMsgLevel( QStringLiteral( " Layer '%1' found in the DXF Export settings XML file, but not present in the project." ).arg( element.attribute( QStringLiteral( "name" ) ) ), 1 ); } } } @@ -964,7 +964,7 @@ bool QgsDxfExportDialog::loadSettingsFromXML( QDomDocument &doc, QString &errorM void QgsDxfExportDialog::saveSettingsToFile() { - QString outputFileName = QFileDialog::getSaveFileName( this, tr( "Save DXF Export settings as XML" ), + QString outputFileName = QFileDialog::getSaveFileName( this, tr( "Save DXF Export Settings as XML" ), QgsDxfExportDialog::settingsDxfLastSettingsDir->value(), tr( "XML file" ) + " (*.xml)" ); // return dialog focus on Mac @@ -981,7 +981,6 @@ void QgsDxfExportDialog::saveSettingsToFile() outputFileName += QStringLiteral( ".xml" ); } - QString errorMessage; QDomDocument domDocument; saveSettingsToXML( domDocument ); @@ -990,7 +989,7 @@ void QgsDxfExportDialog::saveSettingsToFile() 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!" ) ); + QMessageBox::information( this, tr( "Save DXF Export Settings" ), tr( "The directory containing your dataset needs to be writable!" ) ); return; } @@ -1001,13 +1000,13 @@ void QgsDxfExportDialog::saveSettingsToFile() // save as utf-8 with 2 spaces for indents domDocument.save( fileStream, 2 ); file.close(); - QMessageBox::information( this, tr( "Save DXF settings" ), tr( "Created DXF settings file as %1" ).arg( outputFileName ) ); + QMessageBox::information( this, tr( "Save DXF Export Settings" ), tr( "Created DXF Export settings file as %1" ).arg( outputFileName ) ); QgsDxfExportDialog::settingsDxfLastSettingsDir->setValue( 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 ) ); + QMessageBox::information( this, tr( "Save DXF Export Settings" ), tr( "ERROR: Failed to created DXF Export settings file as %1. Check file permissions and retry." ).arg( outputFileName ) ); return; } } diff --git a/src/ui/qgsdxfexportdialogbase.ui b/src/ui/qgsdxfexportdialogbase.ui index 16b1a6890140..679c46bf1b2c 100644 --- a/src/ui/qgsdxfexportdialogbase.ui +++ b/src/ui/qgsdxfexportdialogbase.ui @@ -147,14 +147,14 @@ - Select Data DefinedBlocks + Select Data Defined Blocks - Deselect Data DefinedBlocks + Deselect Data Defined Blocks From 10b2a80330a415fd579891eadf855b8b5ed455e3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 19:01:54 +0000 Subject: [PATCH 43/87] build(deps): bump actions/cache from 3 to 4 Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ogc.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ogc.yml b/.github/workflows/ogc.yml index ecf941d38fa9..9a262bfd29d9 100644 --- a/.github/workflows/ogc.yml +++ b/.github/workflows/ogc.yml @@ -44,7 +44,7 @@ jobs: uses: actions/checkout@v4 - name: Restore build cache - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: /home/runner/QGIS/.ccache key: build-ccache-ogc-${{ github.event.pull_request.base.ref || github.ref_name }} @@ -72,7 +72,7 @@ jobs: DOCKER_IMAGE: ${{ steps.docker-build.outputs.imageid }} - name: Save build cache for push only - uses: actions/cache/save@v3 + uses: actions/cache/save@v4 if: ${{ github.event_name == 'push' }} with: path: /home/runner/QGIS/.ccache From 985cdefc4ad777873eaea0ec38e1335439c39fc2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 19:02:33 +0000 Subject: [PATCH 44/87] build(deps): bump tj-actions/changed-files from 43 to 44 Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 43 to 44. - [Release notes](https://github.com/tj-actions/changed-files/releases) - [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md) - [Commits](https://github.com/tj-actions/changed-files/compare/v43...v44) --- updated-dependencies: - dependency-name: tj-actions/changed-files dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/code_layout.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code_layout.yml b/.github/workflows/code_layout.yml index 36308e379482..6389bb3fc28a 100644 --- a/.github/workflows/code_layout.yml +++ b/.github/workflows/code_layout.yml @@ -162,7 +162,7 @@ jobs: silversearcher-ag - name: Retrieve changed files - uses: tj-actions/changed-files@v43 + uses: tj-actions/changed-files@v44 id: changed_files with: separator: " " From 78305b5fbb47a4414e0ff25553862e95262203b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=85=D9=87=D8=AF=D9=8A=20=D8=B4=D9=8A=D9=86=D9=88=D9=86?= =?UTF-8?q?=20=28Mehdi=20Chinoune=29?= Date: Tue, 2 Apr 2024 15:12:08 +0100 Subject: [PATCH 45/87] Fix check for Qwt>=6.2.0 for QWT_POLAR Also set QWT_POLAR_VERSION=0x060200 --- .github/workflows/mingw-w64-msys2.yml | 2 -- src/app/CMakeLists.txt | 7 ++++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/mingw-w64-msys2.yml b/.github/workflows/mingw-w64-msys2.yml index 9c6d2f1429e6..c667f99f521b 100644 --- a/.github/workflows/mingw-w64-msys2.yml +++ b/.github/workflows/mingw-w64-msys2.yml @@ -65,7 +65,6 @@ jobs: - name: Configure QGIS run: | - CXXFLAGS="-DQWT_POLAR_VERSION=0x060200" \ cmake \ -G"Ninja" \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ @@ -74,7 +73,6 @@ jobs: -DWITH_DRACO=ON \ -DWITH_PDAL=OFF \ -DWITH_CUSTOM_WIDGETS=ON \ - -DWITH_QWTPOLAR=OFF \ -DWITH_BINDINGS=OFF \ -DWITH_GRASS=OFF \ -DUSE_CCACHE=ON \ diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 928d98718706..e9847f5041b6 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -386,15 +386,16 @@ find_package(${QT_VERSION_BASE} COMPONENTS UiTools REQUIRED) set (WITH_QWTPOLAR FALSE CACHE BOOL "Determines whether QwtPolar is available or whether functionality requiring QwtPolar should be disabled.") # Once we bump the minimum QWT VERSION to 6.2 or newer, we should get rid of WITH_QWTPOLAR -if(Qwt_VERSION_STRING VERSION_GREATER_EQUAL 6.2 OR WITH_QWTPOLAR) +if(QWT_VERSION_STR VERSION_GREATER_EQUAL 6.2 OR WITH_QWTPOLAR) add_definitions(-DWITH_QWTPOLAR) - if(Qwt_VERSION_STRING VERSION_LESS 6.2) + if(QWT_VERSION_STR VERSION_LESS 6.2) find_package(QwtPolar REQUIRED) else() set(FOUND_QwtPolar TRUE) set(QWTPOLAR_LIBRARY ${QWT_LIBRARY}) set(QWTPOLAR_INCLUDE_DIR ${QWT_INCLUDE_DIR}) + add_definitions(-DQWT_POLAR_VERSION=0x060200) endif() # If not found on the system, offer the possibility to build QwtPolar # internally @@ -403,7 +404,7 @@ if(Qwt_VERSION_STRING VERSION_GREATER_EQUAL 6.2 OR WITH_QWTPOLAR) else() set(DEFAULT_WITH_INTERNAL_QWTPOLAR FALSE) endif() - set (WITH_INTERNAL_QWTPOLAR DEFAULT_WITH_INTERNAL_QWTPOLAR CACHE BOOL "Use internal build of QwtPolar") + set (WITH_INTERNAL_QWTPOLAR ${DEFAULT_WITH_INTERNAL_QWTPOLAR} CACHE BOOL "Use internal build of QwtPolar") if(WITH_INTERNAL_QWTPOLAR) set(QGIS_APP_SRCS From 97b19f453f3fa71bac91eefd95e334e4385827f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=85=D9=87=D8=AF=D9=8A=20=D8=B4=D9=8A=D9=86=D9=88=D9=86?= =?UTF-8?q?=20=28Mehdi=20Chinoune=29?= Date: Tue, 2 Apr 2024 21:36:51 +0000 Subject: [PATCH 46/87] Replace `docker-compose` by `docker compose` --- .ci/run_tests.sh | 10 +++------- .github/workflows/ogc.yml | 4 ++-- .github/workflows/run-tests.yml | 2 +- tests/README.md | 2 +- tests/src/python/test_authmanager_password_postgres.py | 2 +- tests/src/python/test_authmanager_pki_postgres.py | 2 +- 6 files changed, 9 insertions(+), 13 deletions(-) diff --git a/.ci/run_tests.sh b/.ci/run_tests.sh index 75662ccb2a16..5aed8e0c0ef1 100755 --- a/.ci/run_tests.sh +++ b/.ci/run_tests.sh @@ -2,15 +2,11 @@ set -e -# check for docker-compose and docker availability +# check for docker availability command -v docker > /dev/null || { echo "Please install docker" >&2 exit 1 } -command -v docker-compose > /dev/null || { - echo "Please install docker-compose" >&2 - exit 1 -} IMAGE_BUILD_DEPS=qgis/qgis3-build-deps:latest UPDATE_IMAGES=yes @@ -115,7 +111,7 @@ if test "$(docker images -q qgis3-build-deps-binary-image)" = ""; then fi if test "${INTERACTIVE}" = "no"; then - echo "--=[ Running tests via docker-compose" + echo "--=[ Running tests via docker compose" COMMAND=${QGIS_WORKSPACE_MOUNTPOINT}/.docker/docker-qgis-test.sh COMMAND_ARGS="${TESTS_TO_RUN}" else @@ -129,7 +125,7 @@ mkdir -p /tmp/minio_tests/test-bucket && chmod -R 777 /tmp/minio_tests # Create an empty webdav folder with appropriate permissions so www user can write inside it mkdir -p /tmp/webdav_tests && chmod 777 /tmp/webdav_tests -docker-compose \ +docker compose \ -f .docker/docker-compose-testing.yml \ run \ -w "${QGIS_WORKSPACE_MOUNTPOINT}" \ diff --git a/.github/workflows/ogc.yml b/.github/workflows/ogc.yml index ecf941d38fa9..3c8217a5436f 100644 --- a/.github/workflows/ogc.yml +++ b/.github/workflows/ogc.yml @@ -88,7 +88,7 @@ jobs: - name: Run WMS 1.3.0 OGC tests run: | source venv/bin/activate && ./pyogctest/pyogctest.py -s wms130 -e - docker-compose -f .ci/ogc/docker-compose.yml up -d + docker compose -f .ci/ogc/docker-compose.yml up -d source venv/bin/activate && ./pyogctest/pyogctest.py -n ogc_qgis -s wms130 -v -u http://$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' qgis_server_nginx)/qgisserver_wms130 env: DOCKER_IMAGE: ${{ steps.docker-build.outputs.imageid }} @@ -96,7 +96,7 @@ jobs: - name: Run OGC API Features 1.0 tests run: | cd data && git clone https://github.com/qgis/QGIS-Training-Data && cd - - docker-compose -f .ci/ogc/docker-compose.yml up -d + docker compose -f .ci/ogc/docker-compose.yml up -d source venv/bin/activate && ./pyogctest/pyogctest.py -n ogc_qgis -s ogcapif -v -u http://$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' qgis_server_nginx)/qgisserver_ogcapif env: DOCKER_IMAGE: ${{ steps.docker-build.outputs.imageid }} diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index ea22106c714e..80bfcdd21dd1 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -407,7 +407,7 @@ jobs: echo "DOCKERFILE=$DOCKERFILE" mkdir -p /tmp/webdav_tests && chmod 777 /tmp/webdav_tests mkdir -p /tmp/minio_tests/test-bucket && chmod -R 777 /tmp/minio_tests - docker-compose -f .docker/$DOCKERFILE run -e GITHUB_SHA=$GITHUB_SHA qgis-deps /root/QGIS/.docker/docker-qgis-test.sh $TEST_BATCH + docker compose -f .docker/$DOCKERFILE run -e GITHUB_SHA=$GITHUB_SHA qgis-deps /root/QGIS/.docker/docker-qgis-test.sh $TEST_BATCH - name: Fix permissions on test report if: ${{ failure() }} diff --git a/tests/README.md b/tests/README.md index cab7b5bcbe64..cbd64786c041 100644 --- a/tests/README.md +++ b/tests/README.md @@ -61,7 +61,7 @@ Some tests require a specific PostgreSQL server configuration to bring up such server would be to (tweak $srcdir appropriately): QGIS_WORKSPACE=${srcdir} \ - docker-compose -f .docker/docker-compose-testing-postgres.yml up -d postgres + docker compose -f .docker/docker-compose-testing-postgres.yml up -d postgres export PGHOST=`docker inspect docker_postgres_1 | jq -r .[0].NetworkSettings.Networks.docker_default.IPAddress` export PGUSER=docker export PGPASSWORD=docker diff --git a/tests/src/python/test_authmanager_password_postgres.py b/tests/src/python/test_authmanager_password_postgres.py index dcfc0f590ec5..ab27daae3104 100644 --- a/tests/src/python/test_authmanager_password_postgres.py +++ b/tests/src/python/test_authmanager_password_postgres.py @@ -9,7 +9,7 @@ It uses a docker container as postgres/postgis server with certificates from tests/testdata/auth_system/certs_keys_2048 -Use docker-compose -f .docker/docker-compose-testing-postgres.yml up postgres to start the server +Use docker compose -f .docker/docker-compose-testing-postgres.yml up postgres to start the server TODO: - Document how to restore the server data diff --git a/tests/src/python/test_authmanager_pki_postgres.py b/tests/src/python/test_authmanager_pki_postgres.py index 457958b4c4a8..3ab2b6a47b01 100644 --- a/tests/src/python/test_authmanager_pki_postgres.py +++ b/tests/src/python/test_authmanager_pki_postgres.py @@ -9,7 +9,7 @@ It uses a docker container as postgres/postgis server with certificates from tests/testdata/auth_system/certs_keys_2048 -Use docker-compose -f .docker/docker-compose-testing-postgres.yml up postgres to start the server. +Use docker compose -f .docker/docker-compose-testing-postgres.yml up postgres to start the server. TODO: - Document how to restore the server data From ed39ee0d4099f594164abc1a9ef22da5f02806fd Mon Sep 17 00:00:00 2001 From: Jean Felder Date: Wed, 3 Apr 2024 15:53:00 +0200 Subject: [PATCH 47/87] testqgsgeometry: Remove some spurious whitespaces --- tests/src/core/geometry/testqgsgeometry.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/src/core/geometry/testqgsgeometry.cpp b/tests/src/core/geometry/testqgsgeometry.cpp index 7f16e73f0c6c..5b4200cd18c6 100644 --- a/tests/src/core/geometry/testqgsgeometry.cpp +++ b/tests/src/core/geometry/testqgsgeometry.cpp @@ -1347,7 +1347,7 @@ void TestQgsGeometry::simplifyCheck1() initPainterTest(); QVERIFY( mpPolylineGeometryD->simplify( 0.5 ) ); // should be a single polygon as A intersect B - QgsGeometry *mypSimplifyGeometry = mpPolylineGeometryD->simplify( 0.5 ); + QgsGeometry *mypSimplifyGeometry = mpPolylineGeometryD->simplify( 0.5 ); qDebug( "Geometry Type: %s", Qgis::WkbType::displayString( mypSimplifyGeometry->wkbType() ) ); QVERIFY( mypSimplifyGeometry->wkbType() == Qgis::WkbType::LineString ); QgsPolyline myLine = mypSimplifyGeometry->asPolyline(); @@ -1369,7 +1369,7 @@ void TestQgsGeometry::intersectionCheck1() QVERIFY( engine->intersects( mpPolygonGeometryB.constGet() ) ); // should be a single polygon as A intersect B - QgsGeometry mypIntersectionGeometry = mpPolygonGeometryA.intersection( mpPolygonGeometryB ); + QgsGeometry mypIntersectionGeometry = mpPolygonGeometryA.intersection( mpPolygonGeometryB ); QVERIFY( mypIntersectionGeometry.wkbType() == Qgis::WkbType::Polygon ); QgsPolygonXY myPolygon = mypIntersectionGeometry.asPolygon(); QVERIFY( myPolygon.size() > 0 ); //check that the union created a feature @@ -1460,7 +1460,7 @@ void TestQgsGeometry::unionCheck1() { initPainterTest(); // should be a multipolygon with 2 parts as A does not intersect C - QgsGeometry mypUnionGeometry = mpPolygonGeometryA.combine( mpPolygonGeometryC ); + QgsGeometry mypUnionGeometry = mpPolygonGeometryA.combine( mpPolygonGeometryC ); QVERIFY( mypUnionGeometry.wkbType() == Qgis::WkbType::MultiPolygon ); QgsMultiPolygonXY myMultiPolygon = mypUnionGeometry.asMultiPolygon(); QVERIFY( myMultiPolygon.size() > 0 ); //check that the union did not fail @@ -1472,7 +1472,7 @@ void TestQgsGeometry::unionCheck2() { initPainterTest(); // should be a single polygon as A intersect B - QgsGeometry mypUnionGeometry = mpPolygonGeometryA.combine( mpPolygonGeometryB ); + QgsGeometry mypUnionGeometry = mpPolygonGeometryA.combine( mpPolygonGeometryB ); QVERIFY( mypUnionGeometry.wkbType() == Qgis::WkbType::Polygon ); QgsPolygonXY myPolygon = mypUnionGeometry.asPolygon(); QVERIFY( myPolygon.size() > 0 ); //check that the union created a feature From ef2f07bcb0fc96079c12d8a6a3edaeae27789457 Mon Sep 17 00:00:00 2001 From: Jean Felder Date: Wed, 3 Apr 2024 15:36:52 +0200 Subject: [PATCH 48/87] tests: Add tests for QgsGeometry::contains() --- tests/src/core/geometry/testqgsgeometry.cpp | 15 +++++++++++++++ tests/src/python/test_qgsgeometry.py | 9 +++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/src/core/geometry/testqgsgeometry.cpp b/tests/src/core/geometry/testqgsgeometry.cpp index 5b4200cd18c6..0ef9418f1e12 100644 --- a/tests/src/core/geometry/testqgsgeometry.cpp +++ b/tests/src/core/geometry/testqgsgeometry.cpp @@ -152,6 +152,8 @@ class TestQgsGeometry : public QgsTest void isSimple_data(); void isSimple(); + void contains(); + void reshapeGeometryLineMerge(); void createCollectionOfType(); @@ -2076,6 +2078,19 @@ void TestQgsGeometry::isSimple() QCOMPARE( res, simple ); } +void TestQgsGeometry::contains() +{ + QgsGeometry geomTest = QgsGeometry::fromWkt( QStringLiteral( "Polygon((0 0, 5 5, -2.1 12.1, -7.1 7.1))" ) ); + + QgsPointXY pointInside( 1, 2 ); + QVERIFY( geomTest.contains( &pointInside ) ); + QVERIFY( geomTest.contains( QgsGeometry::fromWkt( QStringLiteral( "Point(1 2)" ) ) ) ); + + QgsPointXY pointOutside( 3, 1 ); + QVERIFY( !geomTest.contains( &pointOutside ) ); + QVERIFY( !geomTest.contains( QgsGeometry::fromWkt( QStringLiteral( "Point(3 1)" ) ) ) ); +} + void TestQgsGeometry::reshapeGeometryLineMerge() { int res; diff --git a/tests/src/python/test_qgsgeometry.py b/tests/src/python/test_qgsgeometry.py index 975dc6810df6..9b8d0ba28564 100644 --- a/tests/src/python/test_qgsgeometry.py +++ b/tests/src/python/test_qgsgeometry.py @@ -1113,8 +1113,13 @@ def testContains(self): QgsPointXY(2, 2), QgsPointXY(0, 2), QgsPointXY(0, 0)]]) - myPoint = QgsGeometry.fromPointXY(QgsPointXY(1, 1)) - self.assertTrue(QgsGeometry.contains(myPoly, myPoint)) + pointInside = QgsPointXY(1, 1) + self.assertTrue(myPoly.contains(pointInside)) + self.assertTrue(myPoly.contains(QgsGeometry.fromPointXY(pointInside))) + + pointOutside = QgsPointXY(3, 3) + self.assertFalse(myPoly.contains(pointOutside)) + self.assertFalse(myPoly.contains(QgsGeometry.fromPointXY(pointOutside))) def testTouches(self): myLine = QgsGeometry.fromPolylineXY([ From 1a4e1a77e778730c9b451dc9115f0e97e35b89c9 Mon Sep 17 00:00:00 2001 From: Jean Felder Date: Wed, 3 Apr 2024 13:49:23 +0200 Subject: [PATCH 49/87] qgsgeometry: Add contains method from x,y coordinates --- .../core/auto_generated/geometry/qgsgeometry.sip.in | 7 +++++++ .../core/auto_generated/geometry/qgsgeometry.sip.in | 7 +++++++ src/core/geometry/qgsgeometry.cpp | 13 +++++++++++++ src/core/geometry/qgsgeometry.h | 7 +++++++ tests/src/core/geometry/testqgsgeometry.cpp | 2 ++ tests/src/python/test_qgsgeometry.py | 2 ++ 6 files changed, 38 insertions(+) diff --git a/python/PyQt6/core/auto_generated/geometry/qgsgeometry.sip.in b/python/PyQt6/core/auto_generated/geometry/qgsgeometry.sip.in index 56bc1221b191..ba77e7c03ba2 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgsgeometry.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgsgeometry.sip.in @@ -1322,6 +1322,13 @@ geometries. bool contains( const QgsPointXY *p ) const; %Docstring Returns ``True`` if the geometry contains the point ``p``. +%End + + bool contains( double x, double y ) const; +%Docstring +Returns ``True`` if the geometry contains the point at (``x``, ``y``). + +.. versionadded:: 3.38 %End bool contains( const QgsGeometry &geometry ) const; diff --git a/python/core/auto_generated/geometry/qgsgeometry.sip.in b/python/core/auto_generated/geometry/qgsgeometry.sip.in index 56bc1221b191..ba77e7c03ba2 100644 --- a/python/core/auto_generated/geometry/qgsgeometry.sip.in +++ b/python/core/auto_generated/geometry/qgsgeometry.sip.in @@ -1322,6 +1322,13 @@ geometries. bool contains( const QgsPointXY *p ) const; %Docstring Returns ``True`` if the geometry contains the point ``p``. +%End + + bool contains( double x, double y ) const; +%Docstring +Returns ``True`` if the geometry contains the point at (``x``, ``y``). + +.. versionadded:: 3.38 %End bool contains( const QgsGeometry &geometry ) const; diff --git a/src/core/geometry/qgsgeometry.cpp b/src/core/geometry/qgsgeometry.cpp index b61d36c158de..9cf1f2445f1c 100644 --- a/src/core/geometry/qgsgeometry.cpp +++ b/src/core/geometry/qgsgeometry.cpp @@ -1492,6 +1492,19 @@ bool QgsGeometry::contains( const QgsPointXY *p ) const return geos.contains( &pt, &mLastError ); } +bool QgsGeometry::contains( double x, double y ) const +{ + if ( !d->geometry ) + { + return false; + } + + QgsPoint pt( x, y ); + QgsGeos geos( d->geometry.get() ); + mLastError.clear(); + return geos.contains( &pt, &mLastError ); +} + bool QgsGeometry::contains( const QgsGeometry &geometry ) const { if ( !d->geometry || geometry.isNull() ) diff --git a/src/core/geometry/qgsgeometry.h b/src/core/geometry/qgsgeometry.h index fff7878c646c..ecd16457f87c 100644 --- a/src/core/geometry/qgsgeometry.h +++ b/src/core/geometry/qgsgeometry.h @@ -1417,6 +1417,13 @@ class CORE_EXPORT QgsGeometry */ bool contains( const QgsPointXY *p ) const; + /** + * Returns TRUE if the geometry contains the point at (\a x, \a y). + * + * \since QGIS 3.38 + */ + bool contains( double x, double y ) const; + /** * Returns TRUE if the geometry completely contains another \a geometry. * diff --git a/tests/src/core/geometry/testqgsgeometry.cpp b/tests/src/core/geometry/testqgsgeometry.cpp index 0ef9418f1e12..6f8a0c1c24c6 100644 --- a/tests/src/core/geometry/testqgsgeometry.cpp +++ b/tests/src/core/geometry/testqgsgeometry.cpp @@ -2085,10 +2085,12 @@ void TestQgsGeometry::contains() QgsPointXY pointInside( 1, 2 ); QVERIFY( geomTest.contains( &pointInside ) ); QVERIFY( geomTest.contains( QgsGeometry::fromWkt( QStringLiteral( "Point(1 2)" ) ) ) ); + QVERIFY( geomTest.contains( pointInside.x(), pointInside.y() ) ); QgsPointXY pointOutside( 3, 1 ); QVERIFY( !geomTest.contains( &pointOutside ) ); QVERIFY( !geomTest.contains( QgsGeometry::fromWkt( QStringLiteral( "Point(3 1)" ) ) ) ); + QVERIFY( !geomTest.contains( pointOutside.x(), pointOutside.y() ) ); } void TestQgsGeometry::reshapeGeometryLineMerge() diff --git a/tests/src/python/test_qgsgeometry.py b/tests/src/python/test_qgsgeometry.py index 9b8d0ba28564..c8b7ec3df4d5 100644 --- a/tests/src/python/test_qgsgeometry.py +++ b/tests/src/python/test_qgsgeometry.py @@ -1116,10 +1116,12 @@ def testContains(self): pointInside = QgsPointXY(1, 1) self.assertTrue(myPoly.contains(pointInside)) self.assertTrue(myPoly.contains(QgsGeometry.fromPointXY(pointInside))) + self.assertTrue(myPoly.contains(pointInside.x(), pointInside.y())) pointOutside = QgsPointXY(3, 3) self.assertFalse(myPoly.contains(pointOutside)) self.assertFalse(myPoly.contains(QgsGeometry.fromPointXY(pointOutside))) + self.assertFalse(myPoly.contains(pointOutside.x(), pointOutside.y())) def testTouches(self): myLine = QgsGeometry.fromPolylineXY([ From 6116658c1c3178f529c05a8313aacf60bc0f9789 Mon Sep 17 00:00:00 2001 From: Maxim Rylov Date: Thu, 4 Apr 2024 17:58:25 +0200 Subject: [PATCH 50/87] HANA: Upgrade EULA agreement to version 3.2 --- .docker/qgis3-qt5-build-deps.dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.docker/qgis3-qt5-build-deps.dockerfile b/.docker/qgis3-qt5-build-deps.dockerfile index eb73721c4877..4eb0263eba34 100644 --- a/.docker/qgis3-qt5-build-deps.dockerfile +++ b/.docker/qgis3-qt5-build-deps.dockerfile @@ -152,7 +152,7 @@ RUN apt-get update \ # HANA: client side # Install hdbsql tool -RUN curl -j -k -L -H "Cookie: eula_3_1_agreed=tools.hana.ondemand.com/developer-license-3_1.txt" https://tools.hana.ondemand.com/additional/hanaclient-latest-linux-x64.tar.gz --output hanaclient-latest-linux-x64.tar.gz \ +RUN curl -j -k -L -H "Cookie: eula_3_2_agreed=tools.hana.ondemand.com/developer-license-3_2.txt" https://tools.hana.ondemand.com/additional/hanaclient-latest-linux-x64.tar.gz --output hanaclient-latest-linux-x64.tar.gz \ && tar -xvf hanaclient-latest-linux-x64.tar.gz \ && mkdir /usr/sap \ && ./client/hdbinst -a client --sapmnt=/usr/sap \ From 2837fff5f4726ed729a582c62b79add949238103 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Thu, 4 Apr 2024 09:09:07 +0700 Subject: [PATCH 51/87] [temporal] Fix first canvas rendering on project not respecting cumulative state --- src/core/qgstemporalnavigationobject.cpp | 8 ++++++++ tests/src/core/testqgstemporalnavigationobject.cpp | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/core/qgstemporalnavigationobject.cpp b/src/core/qgstemporalnavigationobject.cpp index 7a8485e626c7..6edef7a8712b 100644 --- a/src/core/qgstemporalnavigationobject.cpp +++ b/src/core/qgstemporalnavigationobject.cpp @@ -270,7 +270,15 @@ double QgsTemporalNavigationObject::framesPerSecond() const void QgsTemporalNavigationObject::setTemporalRangeCumulative( bool state ) { + if ( mCumulativeTemporalRange == state ) + return; + mCumulativeTemporalRange = state; + + if ( !mBlockUpdateTemporalRangeSignal && mNavigationMode == Qgis::TemporalNavigationMode::Animated ) + { + emit updateTemporalRange( dateTimeRangeForFrameNumber( mCurrentFrameNumber ) ); + } } bool QgsTemporalNavigationObject::temporalRangeCumulative() const diff --git a/tests/src/core/testqgstemporalnavigationobject.cpp b/tests/src/core/testqgstemporalnavigationobject.cpp index 3588666fdd9e..b390e54a1601 100644 --- a/tests/src/core/testqgstemporalnavigationobject.cpp +++ b/tests/src/core/testqgstemporalnavigationobject.cpp @@ -272,13 +272,14 @@ void TestQgsTemporalNavigationObject::frameSettings() // Test if, when changing to Cumulative mode, the dateTimeRange for frame 2 (with 2 hours frames) is indeed the full range navigationObject->setTemporalRangeCumulative( true ); + QCOMPARE( temporalRangeSignal.count(), 8 ); QCOMPARE( navigationObject->dateTimeRangeForFrameNumber( 1 ), QgsDateTimeRange( QDateTime( QDate( 2020, 1, 1 ), QTime( 8, 0, 0 ) ), QDateTime( QDate( 2020, 1, 1 ), QTime( 12, 0, 0 ) ), true, false ) ); - QCOMPARE( temporalRangeSignal.count(), 7 ); + QCOMPARE( temporalRangeSignal.count(), 8 ); navigationObject->setTemporalRangeCumulative( false ); // interval which doesn't fit exactly into overall range From d542f50f4deafd5b16411a63ce43f92c5d93aa5a Mon Sep 17 00:00:00 2001 From: Andrea Giudiceandrea Date: Wed, 3 Apr 2024 02:43:24 +0200 Subject: [PATCH 52/87] [OGR] Fix deleting styles --- src/core/providers/ogr/qgsogrprovidermetadata.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/providers/ogr/qgsogrprovidermetadata.cpp b/src/core/providers/ogr/qgsogrprovidermetadata.cpp index 1f19bcd38824..01e3d930aac9 100644 --- a/src/core/providers/ogr/qgsogrprovidermetadata.cpp +++ b/src/core/providers/ogr/qgsogrprovidermetadata.cpp @@ -418,7 +418,7 @@ bool QgsOgrProviderMetadata::saveStyle( bool QgsOgrProviderMetadata::deleteStyleById( const QString &uri, const QString &styleId, QString &errCause ) { QString filePath; - QgsOgrLayerUniquePtr userLayer = LoadDataSourceAndLayer( uri, false, filePath, errCause ); + QgsOgrLayerUniquePtr userLayer = LoadDataSourceAndLayer( uri, true, filePath, errCause ); if ( !userLayer ) return false; QRecursiveMutex *mutex = nullptr; From c2720ab6538217079e21a93ea2913553b510d290 Mon Sep 17 00:00:00 2001 From: Andrea Giudiceandrea Date: Wed, 3 Apr 2024 12:31:52 +0200 Subject: [PATCH 53/87] [GDAL] Fix deleting styles --- src/core/providers/gdal/qgsgdalprovider.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/providers/gdal/qgsgdalprovider.cpp b/src/core/providers/gdal/qgsgdalprovider.cpp index 7f9d7a0b9a45..1040abb66fc7 100644 --- a/src/core/providers/gdal/qgsgdalprovider.cpp +++ b/src/core/providers/gdal/qgsgdalprovider.cpp @@ -4631,7 +4631,7 @@ QString QgsGdalProviderMetadata::getStyleById( const QString &uri, const QString bool QgsGdalProviderMetadata::deleteStyleById( const QString &uri, const QString &styleId, QString &errCause ) { gdal::dataset_unique_ptr ds; - ds.reset( QgsGdalProviderBase::gdalOpen( uri, GDAL_OF_READONLY ) ); + ds.reset( QgsGdalProviderBase::gdalOpen( uri, GDAL_OF_UPDATE ) ); if ( !ds ) { errCause = QObject::tr( "Cannot open %1." ).arg( uri ); From 94f56380f92487e891c163a2dc89b263980dab2f Mon Sep 17 00:00:00 2001 From: Andrea Giudiceandrea Date: Mon, 1 Apr 2024 07:18:45 +0200 Subject: [PATCH 54/87] [db manager] Fix field property editing --- python/plugins/db_manager/db_plugins/data_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/plugins/db_manager/db_plugins/data_model.py b/python/plugins/db_manager/db_plugins/data_model.py index 4842a79c37f8..1087035fe8ba 100644 --- a/python/plugins/db_manager/db_plugins/data_model.py +++ b/python/plugins/db_manager/db_plugins/data_model.py @@ -290,7 +290,7 @@ def getObject(self, row): match = regex.match(typestr) if match.hasMatch(): fld.dataType = match.captured(1).strip() - fld.modifier = regex.captured(2).strip() + fld.modifier = match.captured(2).strip() else: fld.modifier = None fld.dataType = typestr From 6e716e90ec7c579e177983379945071359a3f45e Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Sun, 31 Mar 2024 19:17:48 +0200 Subject: [PATCH 55/87] [OGR provider] Recognize "false"/"true" string as valid value for a boolean field in addFeature()/changeAttributeValues()" Fixes #55517 --- src/core/providers/ogr/qgsogrprovider.cpp | 66 +++++++++++++++- tests/src/python/test_provider_ogr_gpkg.py | 89 ++++++++++++++++++++++ 2 files changed, 151 insertions(+), 4 deletions(-) diff --git a/src/core/providers/ogr/qgsogrprovider.cpp b/src/core/providers/ogr/qgsogrprovider.cpp index e7bfd5380e44..f607029335c0 100644 --- a/src/core/providers/ogr/qgsogrprovider.cpp +++ b/src/core/providers/ogr/qgsogrprovider.cpp @@ -1593,6 +1593,24 @@ static int strictToInt( const QVariant &v, bool *ok ) return 0; } +// Converts a string with values "0", "false", "1", "true" to 0 / 1 +static int stringToBool( const QString &strVal, bool *ok ) +{ + if ( strVal.compare( QLatin1String( "0" ) ) == 0 || strVal.compare( QLatin1String( "false" ), Qt::CaseInsensitive ) == 0 ) + { + *ok = true; + return 0; + } + else if ( strVal.compare( QLatin1String( "1" ) ) == 0 || strVal.compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0 ) + { + *ok = true; + return 1; + } + *ok = false; + return 0; +} + + bool QgsOgrProvider::addFeaturePrivate( QgsFeature &f, Flags flags, QgsFeatureId incrementalFeatureId ) { bool returnValue = true; @@ -1688,12 +1706,29 @@ bool QgsOgrProvider::addFeaturePrivate( QgsFeature &f, Flags flags, QgsFeatureId } else { + bool errorEmitted = false; bool ok = false; switch ( type ) { case OFTInteger: - OGR_F_SetFieldInteger( feature.get(), ogrAttributeId, strictToInt( attrVal, &ok ) ); + { + if ( OGR_Fld_GetSubType( fldDef ) == OFSTBoolean && qType == QVariant::String ) + { + // compatibility with use case of https://github.com/qgis/QGIS/issues/55517 + const QString strVal = attrVal.toString(); + OGR_F_SetFieldInteger( feature.get(), ogrAttributeId, stringToBool( strVal, &ok ) ); + if ( !ok ) + { + pushError( tr( "wrong value for attribute %1 of feature %2: %3" ).arg( qgisAttributeId ) .arg( f.id() ).arg( strVal ) ); + errorEmitted = true; + } + } + else + { + OGR_F_SetFieldInteger( feature.get(), ogrAttributeId, strictToInt( attrVal, &ok ) ); + } break; + } case OFTInteger64: OGR_F_SetFieldInteger64( feature.get(), ogrAttributeId, attrVal.toLongLong( &ok ) ); @@ -1895,7 +1930,10 @@ bool QgsOgrProvider::addFeaturePrivate( QgsFeature &f, Flags flags, QgsFeatureId if ( !ok ) { - pushError( tr( "wrong data type for attribute %1 of feature %2: %3" ).arg( qgisAttributeId ) .arg( f.id() ).arg( qType ) ); + if ( !errorEmitted ) + { + pushError( tr( "wrong data type for attribute %1 of feature %2: %3" ).arg( qgisAttributeId ) .arg( f.id() ).arg( attrVal.typeName() ) ); + } returnValue = false; } } @@ -2656,12 +2694,29 @@ bool QgsOgrProvider::changeAttributeValues( const QgsChangedAttributesMap &attr_ } else { + bool errorEmitted = false; bool ok = false; switch ( type ) { case OFTInteger: - OGR_F_SetFieldInteger( of.get(), f, strictToInt( *it2, &ok ) ); + { + if ( OGR_Fld_GetSubType( fd ) == OFSTBoolean && qType == QVariant::String ) + { + // compatibility with use case of https://github.com/qgis/QGIS/issues/55517 + const QString strVal = it2->toString(); + OGR_F_SetFieldInteger( of.get(), f, stringToBool( strVal, &ok ) ); + if ( !ok ) + { + pushError( tr( "wrong value for attribute %1 of feature %2: %3" ).arg( it2.key() ) . arg( fid ) .arg( strVal ) ); + errorEmitted = true; + } + } + else + { + OGR_F_SetFieldInteger( of.get(), f, strictToInt( *it2, &ok ) ); + } break; + } case OFTInteger64: OGR_F_SetFieldInteger64( of.get(), f, it2->toLongLong( &ok ) ); break; @@ -2857,7 +2912,10 @@ bool QgsOgrProvider::changeAttributeValues( const QgsChangedAttributesMap &attr_ if ( !ok ) { - pushError( tr( "wrong data type for attribute %1 of feature %2: %3" ).arg( it2.key() ) . arg( fid ) .arg( qType ) ); + if ( !errorEmitted ) + { + pushError( tr( "wrong data type for attribute %1 of feature %2: %3" ).arg( it2.key() ) . arg( fid ) .arg( it2->typeName() ) ); + } returnValue = false; } } diff --git a/tests/src/python/test_provider_ogr_gpkg.py b/tests/src/python/test_provider_ogr_gpkg.py index 94b21bb54acb..570bc45efee4 100644 --- a/tests/src/python/test_provider_ogr_gpkg.py +++ b/tests/src/python/test_provider_ogr_gpkg.py @@ -2860,6 +2860,9 @@ def testChangeAttributeValuesErrors(self): lyr.CreateField(ogr.FieldDefn('datetime_field', ogr.OFTDateTime)) lyr.CreateField(ogr.FieldDefn('date_field', ogr.OFTDateTime)) lyr.CreateField(ogr.FieldDefn('string_field', ogr.OFTString)) + fld_defn = ogr.FieldDefn('bool_field', ogr.OFTInteger) + fld_defn.SetSubType(ogr.OFSTBoolean) + lyr.CreateField(fld_defn) f = ogr.Feature(lyr.GetLayerDefn()) lyr.CreateFeature(f) ds = None @@ -2886,6 +2889,9 @@ def testChangeAttributeValuesErrors(self): self.assertFalse(vl.dataProvider().changeAttributeValues({1: {4: "not a datetime"}})) self.assertFalse(vl.dataProvider().changeAttributeValues({1: {5: "not a date"}})) + # wrong value for attribute 7 of feature 1: wrong + self.assertFalse(vl.dataProvider().changeAttributeValues({1: {7: "wrong"}})) + # OK # int_field self.assertTrue(vl.dataProvider().changeAttributeValues({1: {1: 1}})) @@ -2903,6 +2909,89 @@ def testChangeAttributeValuesErrors(self): # string_field self.assertTrue(vl.dataProvider().changeAttributeValues({1: {6: "foo"}})) self.assertTrue(vl.dataProvider().changeAttributeValues({1: {6: 12345}})) + # bool field + self.assertTrue(vl.dataProvider().changeAttributeValues({1: {7: True}})) + self.assertEqual([feat["bool_field"] for feat in vl.getFeatures()][0], True) + self.assertTrue(vl.dataProvider().changeAttributeValues({1: {7: False}})) + self.assertEqual([feat["bool_field"] for feat in vl.getFeatures()][0], False) + self.assertTrue(vl.dataProvider().changeAttributeValues({1: {7: 1}})) + self.assertEqual([feat["bool_field"] for feat in vl.getFeatures()][0], True) + self.assertTrue(vl.dataProvider().changeAttributeValues({1: {7: 0}})) + self.assertEqual([feat["bool_field"] for feat in vl.getFeatures()][0], False) + self.assertTrue(vl.dataProvider().changeAttributeValues({1: {7: "true"}})) + self.assertEqual([feat["bool_field"] for feat in vl.getFeatures()][0], True) + self.assertTrue(vl.dataProvider().changeAttributeValues({1: {7: "false"}})) + self.assertEqual([feat["bool_field"] for feat in vl.getFeatures()][0], False) + self.assertTrue(vl.dataProvider().changeAttributeValues({1: {7: "1"}})) + self.assertEqual([feat["bool_field"] for feat in vl.getFeatures()][0], True) + self.assertTrue(vl.dataProvider().changeAttributeValues({1: {7: "0"}})) + self.assertEqual([feat["bool_field"] for feat in vl.getFeatures()][0], False) + + def testAttributeBoolean(self): + + tmpfile = os.path.join(self.basetestpath, 'testAttributeBoolean.gpkg') + ds = ogr.GetDriverByName('GPKG').CreateDataSource(tmpfile) + lyr = ds.CreateLayer('test', geom_type=ogr.wkbPoint) + fld_defn = ogr.FieldDefn('bool_field', ogr.OFTInteger) + fld_defn.SetSubType(ogr.OFSTBoolean) + lyr.CreateField(fld_defn) + ds = None + + vl = QgsVectorLayer(f'{tmpfile}' + "|layername=" + "test", 'test', 'ogr') + + f = QgsFeature(vl.fields()) + f.setAttribute(1, False) + ret, _ = vl.dataProvider().addFeatures([f]) + self.assertTrue(ret) + + f = QgsFeature(vl.fields()) + f.setAttribute(1, True) + vl.dataProvider().addFeatures([f]) + self.assertTrue(ret) + + f = QgsFeature(vl.fields()) + f.setAttribute(1, 0) + ret, _ = vl.dataProvider().addFeatures([f]) + self.assertTrue(ret) + + f = QgsFeature(vl.fields()) + f.setAttribute(1, 1) + vl.dataProvider().addFeatures([f]) + self.assertTrue(ret) + + # Test compatibility with use case of https://github.com/qgis/QGIS/issues/55517 + f = QgsFeature(vl.fields()) + f.setAttribute(1, "false") + ret, _ = vl.dataProvider().addFeatures([f]) + self.assertTrue(ret) + + f = QgsFeature(vl.fields()) + f.setAttribute(1, "true") + vl.dataProvider().addFeatures([f]) + self.assertTrue(ret) + + f = QgsFeature(vl.fields()) + f.setAttribute(1, "0") + ret, _ = vl.dataProvider().addFeatures([f]) + self.assertTrue(ret) + + f = QgsFeature(vl.fields()) + f.setAttribute(1, "1") + vl.dataProvider().addFeatures([f]) + self.assertTrue(ret) + + f = QgsFeature(vl.fields()) + f.setAttribute(1, "invalid") + ret, _ = vl.dataProvider().addFeatures([f]) + self.assertFalse(ret) + + f = QgsFeature(vl.fields()) + f.setAttribute(1, [1]) + ret, _ = vl.dataProvider().addFeatures([f]) + self.assertFalse(ret) + + self.assertEqual([feat["bool_field"] for feat in vl.getFeatures()], + [False, True, False, True, False, True, False, True]) def testExtent(self): # 2D points From 6ca63b06acbd81b38e383f6221e83b6ed8096ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Fri, 5 Apr 2024 15:49:33 +0200 Subject: [PATCH 56/87] [dxf] Apply review suggestions (use message bar instead of blocking messages, avoid member variable) --- src/app/qgsdxfexportdialog.cpp | 20 +++++++++++--------- src/app/qgsdxfexportdialog.h | 1 - 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index cecb8dc24c6a..6aaa2177c31a 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -732,12 +732,12 @@ 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 ); + QPushButton *btnLoadSaveSettings = 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 ); + btnLoadSaveSettings->setMenu( menuSettings ); + buttonBox->addButton( btnLoadSaveSettings, QDialogButtonBox::ResetRole ); mMessageBar = new QgsMessageBar(); mMessageBar->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Fixed ); @@ -844,7 +844,9 @@ void QgsDxfExportDialog::loadSettingsFromFile() { resultFlag = loadSettingsFromXML( domDocument, errorMessage ); if ( !resultFlag ) - QMessageBox::information( this, tr( "Load DXF Export Settings" ), tr( "ERROR: Failed to load DXF Export settings file as %1.\n\n%2" ).arg( fileName, errorMessage ) ); + { + mMessageBar->pushWarning( tr( "Load DXF Export Settings" ), tr( "Failed to load DXF Export settings file as %1. Details: %2" ).arg( fileName, errorMessage ) ); + } else { QgsDxfExportDialog::settingsDxfLastSettingsDir->setValue( QFileInfo( fileName ).path() ); @@ -859,14 +861,14 @@ bool QgsDxfExportDialog::loadSettingsFromXML( QDomDocument &doc, QString &errorM const QDomElement rootElement = doc.firstChildElement( QStringLiteral( "qgis" ) ); if ( rootElement.isNull() ) { - errorMessage = tr( "Root element could not be found" ); + errorMessage = tr( "Root <qgis> element could not be found." ); return false; } const QDomElement dxfElement = rootElement.firstChildElement( QStringLiteral( "dxf_settings" ) ); if ( dxfElement.isNull() ) { - errorMessage = tr( "The XML file does not correspond to DXF Export settings. It must have a element." ); + errorMessage = tr( "The XML file does not correspond to DXF Export settings. It must have a <dxf-settings> element." ); return false; } @@ -989,7 +991,7 @@ void QgsDxfExportDialog::saveSettingsToFile() const QFileInfo dirInfo( fileInfo.path() ); //excludes file name if ( !dirInfo.isWritable() ) { - QMessageBox::information( this, tr( "Save DXF Export Settings" ), tr( "The directory containing your dataset needs to be writable!" ) ); + mMessageBar->pushInfo( tr( "Save DXF Export Settings" ), tr( "The directory containing your dataset needs to be writable!" ) ); return; } @@ -1000,13 +1002,13 @@ void QgsDxfExportDialog::saveSettingsToFile() // save as utf-8 with 2 spaces for indents domDocument.save( fileStream, 2 ); file.close(); - QMessageBox::information( this, tr( "Save DXF Export Settings" ), tr( "Created DXF Export settings file as %1" ).arg( outputFileName ) ); + mMessageBar->pushSuccess( tr( "Save DXF Export Settings" ), tr( "Created DXF Export settings file as %1" ).arg( outputFileName ) ); QgsDxfExportDialog::settingsDxfLastSettingsDir->setValue( QFileInfo( outputFileName ).absolutePath() ); return; } else { - QMessageBox::information( this, tr( "Save DXF Export Settings" ), tr( "ERROR: Failed to created DXF Export settings file as %1. Check file permissions and retry." ).arg( outputFileName ) ); + mMessageBar->pushWarning( tr( "Save DXF Export Settings" ), tr( "Failed to created DXF Export settings file as %1. Check file permissions and retry." ).arg( outputFileName ) ); return; } } diff --git a/src/app/qgsdxfexportdialog.h b/src/app/qgsdxfexportdialog.h index d811e43bf08b..a6fd724ddda0 100644 --- a/src/app/qgsdxfexportdialog.h +++ b/src/app/qgsdxfexportdialog.h @@ -147,7 +147,6 @@ class QgsDxfExportDialog : public QDialog, private Ui::QgsDxfExportDialogBase FieldSelectorDelegate *mFieldSelectorDelegate = nullptr; QgsVectorLayerAndAttributeModel *mModel = nullptr; QgsDxfExportLayerTreeView *mTreeView = nullptr; - QPushButton *mBtnLoadSaveSettings = nullptr; QgsMessageBar *mMessageBar = nullptr; QgsCoordinateReferenceSystem mCRS; From e340945f7a16919195cd3bf72481b4e84589dcca Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Fri, 5 Apr 2024 16:38:55 +0700 Subject: [PATCH 57/87] Fix temporal unit conversion factor for milliseconds to weeks/months/years/decades/centuries --- src/core/qgsunittypes.cpp | 10 +++++----- tests/src/python/test_qgsunittypes.py | 23 ++++++++++++----------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/core/qgsunittypes.cpp b/src/core/qgsunittypes.cpp index 2c60ce5db534..6d6d124934cf 100644 --- a/src/core/qgsunittypes.cpp +++ b/src/core/qgsunittypes.cpp @@ -1611,15 +1611,15 @@ double QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit fromUnit, Qgis::Te case Qgis::TemporalUnit::Days: return 1 / 86400000.0; case Qgis::TemporalUnit::Weeks: - return 1 / 60480000.0; + return 1 / 604800000.0; case Qgis::TemporalUnit::Months: - return 1 / 259200000.0; + return 1 / 2592000000.0; case Qgis::TemporalUnit::Years: - return 1 / 3155760000.0; - case Qgis::TemporalUnit::Decades: return 1 / 31557600000.0; - case Qgis::TemporalUnit::Centuries: + case Qgis::TemporalUnit::Decades: return 1 / 315576000000.0; + case Qgis::TemporalUnit::Centuries: + return 1 / 3155760000000.0; case Qgis::TemporalUnit::Unknown: case Qgis::TemporalUnit::IrregularStep: return 1.0; diff --git a/tests/src/python/test_qgsunittypes.py b/tests/src/python/test_qgsunittypes.py index efc06d2f7777..37659ca8e98b 100644 --- a/tests/src/python/test_qgsunittypes.py +++ b/tests/src/python/test_qgsunittypes.py @@ -958,11 +958,11 @@ def testTemporalFromUnitToUnitFactor(self): QgsUnitTypes.TemporalUnit.TemporalSeconds: { QgsUnitTypes.TemporalUnit.TemporalMilliseconds: 1000.0, QgsUnitTypes.TemporalUnit.TemporalSeconds: 1, - QgsUnitTypes.TemporalUnit.TemporalMinutes: 0.016666675200000136831, - QgsUnitTypes.TemporalUnit.TemporalHours: 0.00027777792000000228051, + QgsUnitTypes.TemporalUnit.TemporalMinutes: 0.016666666666666666, + QgsUnitTypes.TemporalUnit.TemporalHours: 0.0002777777777777778, QgsUnitTypes.TemporalUnit.TemporalDays: 1.157408000000009502e-5, QgsUnitTypes.TemporalUnit.TemporalWeeks: 1.653440000000013514e-6, - QgsUnitTypes.TemporalUnit.TemporalMonths: 3.805172816248999917e-7, + QgsUnitTypes.TemporalUnit.TemporalMonths: 3.8580246913580245e-7, QgsUnitTypes.TemporalUnit.TemporalYears: 3.170980821917834046e-8, QgsUnitTypes.TemporalUnit.TemporalDecades: 3.170980821917834046e-9, QgsUnitTypes.TemporalUnit.TemporalCenturies: 3.170980821917834149e-10, @@ -975,10 +975,10 @@ def testTemporalFromUnitToUnitFactor(self): QgsUnitTypes.TemporalUnit.TemporalMinutes: 1, QgsUnitTypes.TemporalUnit.TemporalHours: 0.016666666666666666, QgsUnitTypes.TemporalUnit.TemporalDays: 0.0006944444444444445, - QgsUnitTypes.TemporalUnit.TemporalWeeks: 9.921893245713293505e-5, + QgsUnitTypes.TemporalUnit.TemporalWeeks: 9.92063492063492e-5, QgsUnitTypes.TemporalUnit.TemporalMonths: 2.3148148148148147e-05, - QgsUnitTypes.TemporalUnit.TemporalYears: 1.902828841643645226e-6, - QgsUnitTypes.TemporalUnit.TemporalDecades: 1.902828841643645332e-7, + QgsUnitTypes.TemporalUnit.TemporalYears: 1.901285268841737e-6, + QgsUnitTypes.TemporalUnit.TemporalDecades: 1.901285268841737e-7, QgsUnitTypes.TemporalUnit.TemporalCenturies: 1.9028288416436452e-8, QgsUnitTypes.TemporalUnit.TemporalUnknownUnit: 1.0, QgsUnitTypes.TemporalUnit.TemporalIrregularStep: 1.0, @@ -988,12 +988,12 @@ def testTemporalFromUnitToUnitFactor(self): QgsUnitTypes.TemporalUnit.TemporalSeconds: 3600, QgsUnitTypes.TemporalUnit.TemporalMinutes: 60, QgsUnitTypes.TemporalUnit.TemporalHours: 1, - QgsUnitTypes.TemporalUnit.TemporalDays: 0.041666700000240003421, - QgsUnitTypes.TemporalUnit.TemporalWeeks: 0.0059523857143200008604, + QgsUnitTypes.TemporalUnit.TemporalDays: 0.041666666666666664, + QgsUnitTypes.TemporalUnit.TemporalWeeks: 0.005952380952380952, QgsUnitTypes.TemporalUnit.TemporalMonths: 0.001388888888888889, QgsUnitTypes.TemporalUnit.TemporalYears: 0.00011407711613050422, - QgsUnitTypes.TemporalUnit.TemporalDecades: 1.141553424664109737e-5, - QgsUnitTypes.TemporalUnit.TemporalCenturies: 1.141553424664109737e-6, + QgsUnitTypes.TemporalUnit.TemporalDecades: 1.1407711613050422e-5, + QgsUnitTypes.TemporalUnit.TemporalCenturies: 1.1407711613050422e-6, QgsUnitTypes.TemporalUnit.TemporalUnknownUnit: 1.0, QgsUnitTypes.TemporalUnit.TemporalIrregularStep: 1.0, }, @@ -1007,7 +1007,7 @@ def testTemporalFromUnitToUnitFactor(self): QgsUnitTypes.TemporalUnit.TemporalMonths: 0.03333333333333333, QgsUnitTypes.TemporalUnit.TemporalYears: 0.0027378507871321013, QgsUnitTypes.TemporalUnit.TemporalDecades: 0.0002737850787132101, - QgsUnitTypes.TemporalUnit.TemporalCenturies: 2.739723287683189167e-5, + QgsUnitTypes.TemporalUnit.TemporalCenturies: 2.7378507871321012e-5, QgsUnitTypes.TemporalUnit.TemporalUnknownUnit: 1.0, QgsUnitTypes.TemporalUnit.TemporalIrregularStep: 1.0, }, @@ -1089,6 +1089,7 @@ def testTemporalFromUnitToUnitFactor(self): res = QgsUnitTypes.fromUnitToUnitFactor(from_unit, to_unit) self.assertAlmostEqual(res, expected_factor, + places=10, msg='got {:.15f}, expected {:.15f} when converting from {} to {}'.format(res, expected_factor, QgsUnitTypes.toString(from_unit), QgsUnitTypes.toString(to_unit))) From 7d51641ccfba78d4f481ca6bcea866031aae0d9a Mon Sep 17 00:00:00 2001 From: Marco Hugentobler Date: Thu, 4 Apr 2024 11:16:16 +0200 Subject: [PATCH 58/87] DXF: fix bug with some rotated symbols in data defined blocks --- src/core/dxf/qgsdxfexport.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/dxf/qgsdxfexport.cpp b/src/core/dxf/qgsdxfexport.cpp index 96b920c0d7d4..04f34a425973 100644 --- a/src/core/dxf/qgsdxfexport.cpp +++ b/src/core/dxf/qgsdxfexport.cpp @@ -2644,6 +2644,7 @@ void QgsDxfExport::createDDBlockInfo() } sctx.setFeature( &fet ); + sctx.renderContext().expressionContext().setFeature( fet ); DataDefinedBlockInfo blockInfo; blockInfo.blockName = QStringLiteral( "symbolLayer%1class%2" ).arg( symbolLayerNr ).arg( symbolHash ); From 5e81e2c69295fae8912994a331d3a1c5873d9d52 Mon Sep 17 00:00:00 2001 From: Marco Hugentobler Date: Thu, 4 Apr 2024 14:03:14 +0200 Subject: [PATCH 59/87] Add test for bugfix --- tests/src/core/testqgsdxfexport.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/src/core/testqgsdxfexport.cpp b/tests/src/core/testqgsdxfexport.cpp index e1d438b2253b..adfe17feb894 100644 --- a/tests/src/core/testqgsdxfexport.cpp +++ b/tests/src/core/testqgsdxfexport.cpp @@ -169,6 +169,7 @@ void TestQgsDxfExport::init() const QString blueMarkerSvgPath = QgsSymbolLayerUtils::svgSymbolNameToPath( QStringLiteral( "/symbol/blue-marker.svg" ), QgsPathResolver() ); QString expressionString = QString( "CASE WHEN \"CLASS\" = 'B52' THEN '%1' WHEN \"CLASS\" = 'Biplane' THEN '%2' WHEN \"CLASS\" = 'Jet' THEN '%3' END" ).arg( planeSvgPath ).arg( planeOrangeSvgPath ).arg( blueMarkerSvgPath ); ddProperties.setProperty( QgsSymbolLayer::Property::Name, QgsProperty::fromExpression( expressionString ) ); + ddProperties.setProperty( QgsSymbolLayer::Property::Angle, QgsProperty::fromExpression( "Heading" ) ); svgSymbolLayer->setDataDefinedProperties( ddProperties ); QgsSymbolLayerList ddSymbolLayerList; ddSymbolLayerList << svgSymbolLayer; @@ -276,7 +277,10 @@ void TestQgsDxfExport::testPointsDataDefinedSizeSymbol() dxfBuffer.close(); QString dxfString = QString::fromLatin1( dxfByteArray ); + //test if data defined blocks have been created QVERIFY( dxfString.contains( QStringLiteral( "symbolLayer0class" ) ) ); + //test a rotation for a referenced block + QVERIFY( dxfString.contains( QStringLiteral( "50\n5.0" ) ) ); } void TestQgsDxfExport::testLines() From bfbb1117480ef40e8bb0815df78dc989c806e18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ko=C5=88a=C5=99=C3=ADk?= Date: Sat, 6 Apr 2024 14:19:31 +0200 Subject: [PATCH 60/87] Fix build with QtWebEngine --- src/core/web/qgswebenginepage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/web/qgswebenginepage.cpp b/src/core/web/qgswebenginepage.cpp index f56e0c1b916b..309483ca4b58 100644 --- a/src/core/web/qgswebenginepage.cpp +++ b/src/core/web/qgswebenginepage.cpp @@ -18,11 +18,11 @@ #include "qgswebenginepage.h" #include "qgsconfig.h" #include +#include #include #ifdef HAVE_PDF4QT #include "qgspdfrenderer.h" -#include #include #else #include "qgsexception.h" From 891a8efc84232c2dcd7ebf6b3985307115513b65 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Wed, 3 Apr 2024 20:26:44 +0700 Subject: [PATCH 61/87] [raster][temporal] Add a brand new temporal mode: pixel value as temporal datetime --- python/PyQt6/core/auto_additions/qgis.py | 5 +- python/PyQt6/core/auto_generated/qgis.sip.in | 1 + .../qgsrasterlayertemporalproperties.sip.in | 124 ++++++++++++++++++ python/core/auto_additions/qgis.py | 5 +- python/core/auto_generated/qgis.sip.in | 1 + .../qgsrasterlayertemporalproperties.sip.in | 124 ++++++++++++++++++ src/core/qgis.h | 1 + src/core/raster/qgsrasterlayerrenderer.cpp | 25 ++++ .../qgsrasterlayertemporalproperties.cpp | 80 +++++++++++ .../raster/qgsrasterlayertemporalproperties.h | 89 +++++++++++++ src/core/raster/qgsrasterlayerutils.cpp | 21 ++- ...qgsrasterlayertemporalpropertieswidget.cpp | 43 ++++++ ...rasterlayertemporalpropertieswidgetbase.ui | 108 +++++++++++++++ .../test_qgsrasterlayertemporalproperties.py | 39 ++++++ 14 files changed, 662 insertions(+), 4 deletions(-) diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index 02cb7d44a228..b5c63e785fd6 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -2116,7 +2116,10 @@ QgsRasterLayerTemporalProperties.FixedRangePerBand = Qgis.RasterTemporalMode.FixedRangePerBand QgsRasterLayerTemporalProperties.FixedRangePerBand.is_monkey_patched = True QgsRasterLayerTemporalProperties.FixedRangePerBand.__doc__ = "Layer has a fixed temporal range per band (since QGIS 3.38)" -Qgis.RasterTemporalMode.__doc__ = "Raster layer temporal modes\n\n.. versionadded:: 3.22\n\n" + '* ``ModeFixedTemporalRange``: ' + Qgis.RasterTemporalMode.FixedTemporalRange.__doc__ + '\n' + '* ``ModeTemporalRangeFromDataProvider``: ' + Qgis.RasterTemporalMode.TemporalRangeFromDataProvider.__doc__ + '\n' + '* ``ModeRedrawLayerOnly``: ' + Qgis.RasterTemporalMode.RedrawLayerOnly.__doc__ + '\n' + '* ``FixedRangePerBand``: ' + Qgis.RasterTemporalMode.FixedRangePerBand.__doc__ +QgsRasterLayerTemporalProperties.RepresentsTemporalValues = Qgis.RasterTemporalMode.RepresentsTemporalValues +QgsRasterLayerTemporalProperties.RepresentsTemporalValues.is_monkey_patched = True +QgsRasterLayerTemporalProperties.RepresentsTemporalValues.__doc__ = "Pixel values represent an datetime" +Qgis.RasterTemporalMode.__doc__ = "Raster layer temporal modes\n\n.. versionadded:: 3.22\n\n" + '* ``ModeFixedTemporalRange``: ' + Qgis.RasterTemporalMode.FixedTemporalRange.__doc__ + '\n' + '* ``ModeTemporalRangeFromDataProvider``: ' + Qgis.RasterTemporalMode.TemporalRangeFromDataProvider.__doc__ + '\n' + '* ``ModeRedrawLayerOnly``: ' + Qgis.RasterTemporalMode.RedrawLayerOnly.__doc__ + '\n' + '* ``FixedRangePerBand``: ' + Qgis.RasterTemporalMode.FixedRangePerBand.__doc__ + '\n' + '* ``RepresentsTemporalValues``: ' + Qgis.RasterTemporalMode.RepresentsTemporalValues.__doc__ # -- Qgis.RasterTemporalMode.baseClass = Qgis QgsRasterDataProviderTemporalCapabilities.IntervalHandlingMethod = Qgis.TemporalIntervalMatchMethod diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index 38b07fa1399c..5b9871da60e9 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -1258,6 +1258,7 @@ The development version TemporalRangeFromDataProvider, RedrawLayerOnly, FixedRangePerBand, + RepresentsTemporalValues, }; enum class TemporalIntervalMatchMethod /BaseType=IntEnum/ diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in index 62d35f120e7b..65e50c481bb0 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in @@ -139,6 +139,130 @@ Returns the band corresponding to the specified ``range``. %Docstring Returns a filtered list of bands which match the specified ``range``. +.. versionadded:: 3.38 +%End + + int temporalRepresentationBandNumber() const; +%Docstring +Returns the band number from which the temporal values should be taken. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`setTemporalRepresentationBandNumber` + +.. versionadded:: 3.38 +%End + + void setTemporalRepresentationBandNumber( int number ); +%Docstring +Sets the band number from which the temporal values should be taken. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`temporalRepresentationBandNumber` + +.. versionadded:: 3.38 +%End + + QDateTime temporalRepresentationOffset() const; +%Docstring +Returns the temporal offset, which is a fixed datetime which should be added to individual pixel values +from the layer. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`setTemporalRepresentationOffset` + +.. versionadded:: 3.38 +%End + + void setTemporalRepresentationOffset( const QDateTime &offset ); +%Docstring +Sets the temporal offset, which is a fixed datetime which should be added to individual pixel values +from the layer. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`temporalRepresentationOffset` + +.. versionadded:: 3.38 +%End + + double temporalRepresentationScale() const; +%Docstring +Returns the scale, which is a duration factor which should be applied to individual pixel +values from the layer. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`setTemporalRepresentationScale` + +.. seealso:: :py:func:`temporalRepresentationScaleUnit` + +.. seealso:: :py:func:`setTemporalRepresentationScaleUnit` + +.. versionadded:: 3.38 +%End + + void setTemporalRepresentationScale( double scale ); +%Docstring +Sets the scale, which is a duration factor which should be applied to individual pixel +values from the layer. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`temporalRepresentationScale` + +.. seealso:: :py:func:`temporalRepresentationScaleUnit` + +.. seealso:: :py:func:`setTemporalRepresentationScaleUnit` + +.. versionadded:: 3.38 +%End + + Qgis::TemporalUnit temporalRepresentationScaleUnit() const; +%Docstring +Returns the scale's temporal unit type. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`setTemporalRepresentationScaleUnit` + +.. seealso:: :py:func:`temporalRepresentationScale` + +.. seealso:: :py:func:`setTemporalRepresentationScale` + +.. versionadded:: 3.38 +%End + + void setTemporalRepresentationScaleUnit( Qgis::TemporalUnit unit ); +%Docstring +Sets the scale's temporal unit type. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`temporalRepresentationScaleUnit` + +.. seealso:: :py:func:`temporalRepresentationScale` + +.. seealso:: :py:func:`setTemporalRepresentationScale` + .. versionadded:: 3.38 %End diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index 4918dd1f3f26..5a2b2539999a 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -2077,7 +2077,10 @@ QgsRasterLayerTemporalProperties.FixedRangePerBand = Qgis.RasterTemporalMode.FixedRangePerBand QgsRasterLayerTemporalProperties.FixedRangePerBand.is_monkey_patched = True QgsRasterLayerTemporalProperties.FixedRangePerBand.__doc__ = "Layer has a fixed temporal range per band (since QGIS 3.38)" -Qgis.RasterTemporalMode.__doc__ = "Raster layer temporal modes\n\n.. versionadded:: 3.22\n\n" + '* ``ModeFixedTemporalRange``: ' + Qgis.RasterTemporalMode.FixedTemporalRange.__doc__ + '\n' + '* ``ModeTemporalRangeFromDataProvider``: ' + Qgis.RasterTemporalMode.TemporalRangeFromDataProvider.__doc__ + '\n' + '* ``ModeRedrawLayerOnly``: ' + Qgis.RasterTemporalMode.RedrawLayerOnly.__doc__ + '\n' + '* ``FixedRangePerBand``: ' + Qgis.RasterTemporalMode.FixedRangePerBand.__doc__ +QgsRasterLayerTemporalProperties.RepresentsTemporalValues = Qgis.RasterTemporalMode.RepresentsTemporalValues +QgsRasterLayerTemporalProperties.RepresentsTemporalValues.is_monkey_patched = True +QgsRasterLayerTemporalProperties.RepresentsTemporalValues.__doc__ = "Pixel values represent an datetime" +Qgis.RasterTemporalMode.__doc__ = "Raster layer temporal modes\n\n.. versionadded:: 3.22\n\n" + '* ``ModeFixedTemporalRange``: ' + Qgis.RasterTemporalMode.FixedTemporalRange.__doc__ + '\n' + '* ``ModeTemporalRangeFromDataProvider``: ' + Qgis.RasterTemporalMode.TemporalRangeFromDataProvider.__doc__ + '\n' + '* ``ModeRedrawLayerOnly``: ' + Qgis.RasterTemporalMode.RedrawLayerOnly.__doc__ + '\n' + '* ``FixedRangePerBand``: ' + Qgis.RasterTemporalMode.FixedRangePerBand.__doc__ + '\n' + '* ``RepresentsTemporalValues``: ' + Qgis.RasterTemporalMode.RepresentsTemporalValues.__doc__ # -- Qgis.RasterTemporalMode.baseClass = Qgis QgsRasterDataProviderTemporalCapabilities.IntervalHandlingMethod = Qgis.TemporalIntervalMatchMethod diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index 965636863fe2..6df2b9c00282 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -1258,6 +1258,7 @@ The development version TemporalRangeFromDataProvider, RedrawLayerOnly, FixedRangePerBand, + RepresentsTemporalValues, }; enum class TemporalIntervalMatchMethod diff --git a/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in b/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in index 62d35f120e7b..65e50c481bb0 100644 --- a/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in +++ b/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in @@ -139,6 +139,130 @@ Returns the band corresponding to the specified ``range``. %Docstring Returns a filtered list of bands which match the specified ``range``. +.. versionadded:: 3.38 +%End + + int temporalRepresentationBandNumber() const; +%Docstring +Returns the band number from which the temporal values should be taken. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`setTemporalRepresentationBandNumber` + +.. versionadded:: 3.38 +%End + + void setTemporalRepresentationBandNumber( int number ); +%Docstring +Sets the band number from which the temporal values should be taken. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`temporalRepresentationBandNumber` + +.. versionadded:: 3.38 +%End + + QDateTime temporalRepresentationOffset() const; +%Docstring +Returns the temporal offset, which is a fixed datetime which should be added to individual pixel values +from the layer. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`setTemporalRepresentationOffset` + +.. versionadded:: 3.38 +%End + + void setTemporalRepresentationOffset( const QDateTime &offset ); +%Docstring +Sets the temporal offset, which is a fixed datetime which should be added to individual pixel values +from the layer. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`temporalRepresentationOffset` + +.. versionadded:: 3.38 +%End + + double temporalRepresentationScale() const; +%Docstring +Returns the scale, which is a duration factor which should be applied to individual pixel +values from the layer. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`setTemporalRepresentationScale` + +.. seealso:: :py:func:`temporalRepresentationScaleUnit` + +.. seealso:: :py:func:`setTemporalRepresentationScaleUnit` + +.. versionadded:: 3.38 +%End + + void setTemporalRepresentationScale( double scale ); +%Docstring +Sets the scale, which is a duration factor which should be applied to individual pixel +values from the layer. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`temporalRepresentationScale` + +.. seealso:: :py:func:`temporalRepresentationScaleUnit` + +.. seealso:: :py:func:`setTemporalRepresentationScaleUnit` + +.. versionadded:: 3.38 +%End + + Qgis::TemporalUnit temporalRepresentationScaleUnit() const; +%Docstring +Returns the scale's temporal unit type. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`setTemporalRepresentationScaleUnit` + +.. seealso:: :py:func:`temporalRepresentationScale` + +.. seealso:: :py:func:`setTemporalRepresentationScale` + +.. versionadded:: 3.38 +%End + + void setTemporalRepresentationScaleUnit( Qgis::TemporalUnit unit ); +%Docstring +Sets the scale's temporal unit type. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`temporalRepresentationScaleUnit` + +.. seealso:: :py:func:`temporalRepresentationScale` + +.. seealso:: :py:func:`setTemporalRepresentationScale` + .. versionadded:: 3.38 %End diff --git a/src/core/qgis.h b/src/core/qgis.h index 56a599e59dcb..c5b0d0eb0eee 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -2145,6 +2145,7 @@ class CORE_EXPORT Qgis TemporalRangeFromDataProvider SIP_MONKEYPATCH_COMPAT_NAME( ModeTemporalRangeFromDataProvider ) = 1, //!< Mode when raster layer delegates temporal range handling to the dataprovider. RedrawLayerOnly SIP_MONKEYPATCH_COMPAT_NAME( ModeRedrawLayerOnly ) = 2, //!< Redraw the layer when temporal range changes, but don't apply any filtering. Useful when raster symbology expressions depend on the time range. (since QGIS 3.22) FixedRangePerBand = 3, //!< Layer has a fixed temporal range per band (since QGIS 3.38) + RepresentsTemporalValues = 4, //!< Pixel values represent an datetime }; Q_ENUM( RasterTemporalMode ) diff --git a/src/core/raster/qgsrasterlayerrenderer.cpp b/src/core/raster/qgsrasterlayerrenderer.cpp index 472a457aaf8e..6ad73dac0663 100644 --- a/src/core/raster/qgsrasterlayerrenderer.cpp +++ b/src/core/raster/qgsrasterlayerrenderer.cpp @@ -35,6 +35,7 @@ #include "qgsapplication.h" #include "qgsrastertransparency.h" #include "qgsrasterlayerutils.h" +#include "qgsunittypes.h" #include #include @@ -301,6 +302,30 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender case Qgis::RasterTemporalMode::FixedRangePerBand: break; + case Qgis::RasterTemporalMode::RepresentsTemporalValues: + if ( mPipe->renderer()->usesBands().contains( temporalProperties->temporalRepresentationBandNumber() ) ) + { + // if layer has elevation settings and we are only rendering a temporal range => we need to filter pixels by temporal values + 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(); + + const qint64 msecsLower = temporalProperties->temporalRepresentationOffset().msecsTo( rendererContext.temporalRange().begin() ); + const qint64 msecsUpper = temporalProperties->temporalRepresentationOffset().msecsTo( rendererContext.temporalRange().end() ); + const double adjustedLower = msecsLower * QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit::Milliseconds, temporalProperties->temporalRepresentationScaleUnit() ) / temporalProperties->temporalRepresentationScale(); + const double adjustedUpper = msecsUpper * QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit::Milliseconds, temporalProperties->temporalRepresentationScaleUnit() ) / temporalProperties->temporalRepresentationScale(); + 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; + case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: // in this mode we need to pass on the desired render temporal range to the data provider if ( QgsRasterDataProviderTemporalCapabilities *temporalCapabilities = mPipe->provider()->temporalCapabilities() ) diff --git a/src/core/raster/qgsrasterlayertemporalproperties.cpp b/src/core/raster/qgsrasterlayertemporalproperties.cpp index 4cde844e5272..564c37cc4484 100644 --- a/src/core/raster/qgsrasterlayertemporalproperties.cpp +++ b/src/core/raster/qgsrasterlayertemporalproperties.cpp @@ -44,6 +44,7 @@ bool QgsRasterLayerTemporalProperties::isVisibleInTemporalRange( const QgsDateTi return false; } + case Qgis::RasterTemporalMode::RepresentsTemporalValues: case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: case Qgis::RasterTemporalMode::RedrawLayerOnly: return true; @@ -95,6 +96,7 @@ QgsDateTimeRange QgsRasterLayerTemporalProperties::calculateTemporalExtent( QgsM return QgsDateTimeRange( begin, end, includeBeginning, includeEnd ); } + case Qgis::RasterTemporalMode::RepresentsTemporalValues: case Qgis::RasterTemporalMode::RedrawLayerOnly: break; } @@ -131,6 +133,7 @@ QList QgsRasterLayerTemporalProperties::allTemporalRanges( Qgs return ranges.empty() ? QList< QgsDateTimeRange > { rasterLayer->dataProvider()->temporalCapabilities()->availableTemporalRange() } : ranges; } + case Qgis::RasterTemporalMode::RepresentsTemporalValues: case Qgis::RasterTemporalMode::RedrawLayerOnly: break; } @@ -160,6 +163,7 @@ QgsTemporalProperty::Flags QgsRasterLayerTemporalProperties::flags() const case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: case Qgis::RasterTemporalMode::RedrawLayerOnly: case Qgis::RasterTemporalMode::FixedRangePerBand: + case Qgis::RasterTemporalMode::RepresentsTemporalValues: return QgsTemporalProperty::Flags(); } BUILTIN_UNREACHABLE @@ -230,6 +234,9 @@ int QgsRasterLayerTemporalProperties::bandForTemporalRange( QgsRasterLayer *, co } return currentMatchingBand; } + + case Qgis::RasterTemporalMode::RepresentsTemporalValues: + return mTemporalRepresentationBandNumber; } BUILTIN_UNREACHABLE } @@ -265,10 +272,65 @@ QList QgsRasterLayerTemporalProperties::filteredBandsForTemporalRange( QgsR } return res; } + + case Qgis::RasterTemporalMode::RepresentsTemporalValues: + return QList() << mTemporalRepresentationBandNumber; } BUILTIN_UNREACHABLE } +int QgsRasterLayerTemporalProperties::temporalRepresentationBandNumber() const +{ + return mTemporalRepresentationBandNumber; +} + +void QgsRasterLayerTemporalProperties::setTemporalRepresentationBandNumber( int number ) +{ + if ( mTemporalRepresentationBandNumber == number ) + return; + + mTemporalRepresentationBandNumber = number; +} + +QDateTime QgsRasterLayerTemporalProperties::temporalRepresentationOffset() const +{ + return mTemporalRepresentationOffset; +} + +void QgsRasterLayerTemporalProperties::setTemporalRepresentationOffset( const QDateTime &offset ) +{ + if ( mTemporalRepresentationOffset == offset ) + return; + + mTemporalRepresentationOffset = offset; +} + +double QgsRasterLayerTemporalProperties::temporalRepresentationScale() const +{ + return mTemporalRepresentationScale; +} + +void QgsRasterLayerTemporalProperties::setTemporalRepresentationScale( double scale ) +{ + if ( mTemporalRepresentationScale == scale ) + return; + + mTemporalRepresentationScale = scale; +} + +Qgis::TemporalUnit QgsRasterLayerTemporalProperties::temporalRepresentationScaleUnit() const +{ + return mTemporalRepresentationScaleUnit; +} + +void QgsRasterLayerTemporalProperties::setTemporalRepresentationScaleUnit( Qgis::TemporalUnit unit ) +{ + if ( mTemporalRepresentationScaleUnit == unit ) + return; + + mTemporalRepresentationScaleUnit = unit; +} + bool QgsRasterLayerTemporalProperties::readXml( const QDomElement &element, const QgsReadWriteContext &context ) { Q_UNUSED( context ) @@ -316,6 +378,15 @@ bool QgsRasterLayerTemporalProperties::readXml( const QDomElement &element, cons break; } + case Qgis::RasterTemporalMode::RepresentsTemporalValues: + { + mTemporalRepresentationBandNumber = temporalNode.attribute( QStringLiteral( "temporalRepresentationBandNumber" ), QStringLiteral( "1" ) ).toInt(); + mTemporalRepresentationOffset = QDateTime::fromString( temporalNode.attribute( QStringLiteral( "temporalRepresentationOffset" ) ), Qt::ISODate ); + mTemporalRepresentationScale = temporalNode.attribute( QStringLiteral( "temporalRepresentationScale" ), QStringLiteral( "1" ) ).toDouble(); + mTemporalRepresentationScaleUnit = static_cast< Qgis::TemporalUnit >( temporalNode.attribute( QStringLiteral( "temporalRepresentationScaleUnit" ), QStringLiteral( "4" ) ).toInt() ); + break; + } + case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: case Qgis::RasterTemporalMode::RedrawLayerOnly: break; @@ -373,6 +444,15 @@ QDomElement QgsRasterLayerTemporalProperties::writeXml( QDomElement &element, QD break; } + case Qgis::RasterTemporalMode::RepresentsTemporalValues: + { + temporalElement.setAttribute( QStringLiteral( "temporalRepresentationBandNumber" ), QString::number( mTemporalRepresentationBandNumber ) ); + temporalElement.setAttribute( QStringLiteral( "temporalRepresentationOffset" ), mTemporalRepresentationOffset.toString( Qt::ISODate ) ); + temporalElement.setAttribute( QStringLiteral( "temporalRepresentationScale" ), QString::number( mTemporalRepresentationScale ) ); + temporalElement.setAttribute( QStringLiteral( "temporalRepresentationScaleUnit" ), QString::number( static_cast< int >( mTemporalRepresentationScaleUnit ) ) ); + break; + } + case Qgis::RasterTemporalMode::RedrawLayerOnly: case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: break; diff --git a/src/core/raster/qgsrasterlayertemporalproperties.h b/src/core/raster/qgsrasterlayertemporalproperties.h index aa2fabf15b92..b6aa967e37f2 100644 --- a/src/core/raster/qgsrasterlayertemporalproperties.h +++ b/src/core/raster/qgsrasterlayertemporalproperties.h @@ -144,6 +144,90 @@ class CORE_EXPORT QgsRasterLayerTemporalProperties : public QgsMapLayerTemporalP */ QList< int > filteredBandsForTemporalRange( QgsRasterLayer *layer, const QgsDateTimeRange &range ) const; + /** + * Returns the band number from which the temporal values should be taken. + * + * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. + * \see setTemporalRepresentationBandNumber() + * \since QGIS 3.38 + */ + int temporalRepresentationBandNumber() const; + + /** + * Sets the band number from which the temporal values should be taken. + * + * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. + * \see temporalRepresentationBandNumber() + * \since QGIS 3.38 + */ + void setTemporalRepresentationBandNumber( int number ); + + /** + * Returns the temporal offset, which is a fixed datetime which should be added to individual pixel values + * from the layer. + * + * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. + * \see setTemporalRepresentationOffset() + * \since QGIS 3.38 + */ + QDateTime temporalRepresentationOffset() const; + + /** + * Sets the temporal offset, which is a fixed datetime which should be added to individual pixel values + * from the layer. + * + * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. + * \see temporalRepresentationOffset() + * \since QGIS 3.38 + */ + void setTemporalRepresentationOffset( const QDateTime &offset ); + + /** + * Returns the scale, which is a duration factor which should be applied to individual pixel + * values from the layer. + * + * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. + * \see setTemporalRepresentationScale() + * \see temporalRepresentationScaleUnit() + * \see setTemporalRepresentationScaleUnit() + * \since QGIS 3.38 + */ + double temporalRepresentationScale() const; + + /** + * Sets the scale, which is a duration factor which should be applied to individual pixel + * values from the layer. + * + * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. + * \see temporalRepresentationScale() + * \see temporalRepresentationScaleUnit() + * \see setTemporalRepresentationScaleUnit() + * \since QGIS 3.38 + */ + void setTemporalRepresentationScale( double scale ); + + /** + * Returns the scale's temporal unit type. + * + * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. + * \see setTemporalRepresentationScaleUnit() + * \see temporalRepresentationScale() + * \see setTemporalRepresentationScale() + * \since QGIS 3.38 + */ + Qgis::TemporalUnit temporalRepresentationScaleUnit() const; + + /** + * Sets the scale's temporal unit type. + * + * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. + * \see temporalRepresentationScaleUnit() + * \see temporalRepresentationScale() + * \see setTemporalRepresentationScale() + * \since QGIS 3.38 + */ + void setTemporalRepresentationScaleUnit( Qgis::TemporalUnit unit ); + QDomElement writeXml( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ) override; bool readXml( const QDomElement &element, const QgsReadWriteContext &context ) override; @@ -162,6 +246,11 @@ class CORE_EXPORT QgsRasterLayerTemporalProperties : public QgsMapLayerTemporalP QgsDateTimeRange mFixedRange; QMap< int, QgsDateTimeRange > mRangePerBand; + + int mTemporalRepresentationBandNumber = 1; + QDateTime mTemporalRepresentationOffset; + double mTemporalRepresentationScale = 1.0; + Qgis::TemporalUnit mTemporalRepresentationScaleUnit = Qgis::TemporalUnit::Days; }; #endif // QGSRASTERLAYERTEMPORALPROPERTIES_H diff --git a/src/core/raster/qgsrasterlayerutils.cpp b/src/core/raster/qgsrasterlayerutils.cpp index 3dc8b536cc98..f5437c9fbc80 100644 --- a/src/core/raster/qgsrasterlayerutils.cpp +++ b/src/core/raster/qgsrasterlayerutils.cpp @@ -58,8 +58,25 @@ int QgsRasterLayerUtils::renderedBandForElevationAndTemporalRange( // both elevation and temporal properties enabled // first find bands matching the temporal range - const QList< int > temporalBands = temporalProperties->filteredBandsForTemporalRange( - layer, temporalRange ); + QList< int > temporalBands; + switch ( temporalProperties->mode() ) + { + case Qgis::RasterTemporalMode::RedrawLayerOnly: + case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: + case Qgis::RasterTemporalMode::FixedTemporalRange: + case Qgis::RasterTemporalMode::FixedRangePerBand: + { + temporalBands << temporalProperties->filteredBandsForTemporalRange( layer, temporalRange ); + break; + } + + case Qgis::RasterTemporalMode::RepresentsTemporalValues: + { + temporalBands << temporalProperties->temporalRepresentationBandNumber(); + break; + } + } + if ( temporalBands.empty() ) { matched = false; diff --git a/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp b/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp index 05f6ff1b006f..f8c3feb1d9eb 100644 --- a/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp +++ b/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp @@ -23,6 +23,8 @@ #include "qgsdatetimeedit.h" #include "qgsexpressionbuilderdialog.h" #include "qgsexpressioncontextutils.h" +#include "qgsunittypes.h" + #include #include @@ -48,8 +50,27 @@ QgsRasterLayerTemporalPropertiesWidget::QgsRasterLayerTemporalPropertiesWidget( } mModeComboBox->addItem( tr( "Fixed Time Range" ), QVariant::fromValue( Qgis::RasterTemporalMode::FixedTemporalRange ) ); mModeComboBox->addItem( tr( "Fixed Time Range Per Band" ), QVariant::fromValue( Qgis::RasterTemporalMode::FixedRangePerBand ) ); + mModeComboBox->addItem( tr( "Represents Temporal Values" ), QVariant::fromValue( Qgis::RasterTemporalMode::RepresentsTemporalValues ) ); mModeComboBox->addItem( tr( "Redraw Layer Only" ), QVariant::fromValue( Qgis::RasterTemporalMode::RedrawLayerOnly ) ); + for ( const Qgis::TemporalUnit unit : + { + Qgis::TemporalUnit::Milliseconds, + Qgis::TemporalUnit::Seconds, + Qgis::TemporalUnit::Minutes, + Qgis::TemporalUnit::Hours, + Qgis::TemporalUnit::Days, + Qgis::TemporalUnit::Weeks, + Qgis::TemporalUnit::Months, + Qgis::TemporalUnit::Years, + Qgis::TemporalUnit::Decades, + Qgis::TemporalUnit::Centuries, + } ) + { + mScaleUnitComboBox->addItem( QgsUnitTypes::toString( unit ), static_cast< int >( unit ) ); + } + mScaleUnitComboBox->setCurrentIndex( mScaleUnitComboBox->findData( static_cast< int >( Qgis::TemporalUnit::Days ) ) ); + mStackedWidget->setSizeMode( QgsStackedWidget::SizeMode::CurrentPageOnly ); mFixedRangePerBandModel = new QgsRasterBandFixedTemporalRangeModel( this ); @@ -65,6 +86,7 @@ QgsRasterLayerTemporalPropertiesWidget::QgsRasterLayerTemporalPropertiesWidget( mStartTemporalDateTimeEdit->setDisplayFormat( QStringLiteral( "yyyy-MM-dd HH:mm:ss" ) ); mEndTemporalDateTimeEdit->setDisplayFormat( QStringLiteral( "yyyy-MM-dd HH:mm:ss" ) ); + mOffsetDateTimeEdit->setDisplayFormat( QStringLiteral( "yyyy-MM-dd HH:mm:ss" ) ); QMenu *calculateFixedRangePerBandMenu = new QMenu( mCalculateFixedRangePerBandButton ); mCalculateFixedRangePerBandButton->setMenu( calculateFixedRangePerBandMenu ); @@ -100,6 +122,13 @@ void QgsRasterLayerTemporalPropertiesWidget::saveTemporalProperties() temporalProperties->setFixedRangePerBand( mFixedRangePerBandModel->rangeData() ); + temporalProperties->setTemporalRepresentationOffset( mOffsetDateTimeEdit->dateTime() ); + + temporalProperties->setTemporalRepresentationScale( mScaleSpinBox->value() ); + temporalProperties->setTemporalRepresentationScaleUnit( static_cast< Qgis::TemporalUnit >( mScaleUnitComboBox->currentData().toInt() ) ); + + temporalProperties->setTemporalRepresentationBandNumber( mBandComboBox->currentBand() ); + for ( QgsMapLayerConfigWidget *widget : std::as_const( mExtraWidgets ) ) { widget->apply(); @@ -124,6 +153,9 @@ void QgsRasterLayerTemporalPropertiesWidget::syncToLayer() case Qgis::RasterTemporalMode::FixedRangePerBand: mStackedWidget->setCurrentWidget( mPageFixedRangePerBand ); break; + case Qgis::RasterTemporalMode::RepresentsTemporalValues: + mStackedWidget->setCurrentWidget( mPageRepresentsTemporalValues ); + break; } mStartTemporalDateTimeEdit->setDateTime( temporalProperties->fixedTemporalRange().begin() ); @@ -134,6 +166,14 @@ void QgsRasterLayerTemporalPropertiesWidget::syncToLayer() mBandRangesTable->horizontalHeader()->setSectionResizeMode( 1, QHeaderView::Stretch ); mBandRangesTable->horizontalHeader()->setSectionResizeMode( 2, QHeaderView::Stretch ); + mOffsetDateTimeEdit->setDateTime( temporalProperties->temporalRepresentationOffset() ); + + mScaleSpinBox->setValue( temporalProperties->temporalRepresentationScale() ); + mScaleUnitComboBox->setCurrentIndex( mScaleUnitComboBox->findData( static_cast< int >( temporalProperties->temporalRepresentationScaleUnit() ) ) ); + + mBandComboBox->setLayer( mLayer ); + mBandComboBox->setBand( temporalProperties->temporalRepresentationBandNumber() ); + mTemporalGroupBox->setChecked( temporalProperties->isActive() ); for ( QgsMapLayerConfigWidget *widget : std::as_const( mExtraWidgets ) ) @@ -174,6 +214,9 @@ void QgsRasterLayerTemporalPropertiesWidget::modeChanged() case Qgis::RasterTemporalMode::FixedRangePerBand: mStackedWidget->setCurrentWidget( mPageFixedRangePerBand ); break; + case Qgis::RasterTemporalMode::RepresentsTemporalValues: + mStackedWidget->setCurrentWidget( mPageRepresentsTemporalValues ); + break; } } } diff --git a/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui b/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui index fed537432da6..75aa4926c1ff 100644 --- a/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui +++ b/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui @@ -272,6 +272,114 @@ background: white;QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::ti + + + + 0 + 0 + + + + + + + Scale + + + + + + + Offset + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + 6 + + + 0.000000000000000 + + + 99999999999.000000000000000 + + + 1.000000000000000 + + + + + + + false + + + + + + + + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">The pixel values in the layer represent a temporal value. </span></p><p> The offset is used to define the starting datetime while scaling identifies the scale – e.g. 1 day or 1 week - of each pixel value.</p></body></html> + + + true + + + + + + + M/d/yyyy h:mm AP + + + Qt::UTC + + + + + + + + + + Band + + + + + diff --git a/tests/src/python/test_qgsrasterlayertemporalproperties.py b/tests/src/python/test_qgsrasterlayertemporalproperties.py index 90cc2dfaa96c..e16af427eed1 100644 --- a/tests/src/python/test_qgsrasterlayertemporalproperties.py +++ b/tests/src/python/test_qgsrasterlayertemporalproperties.py @@ -273,6 +273,45 @@ def test_basic_fixed_range_per_band(self): includeBeginning=False, includeEnd=True)}) + def test_basic_represents_temporal_value(self): + """ + Basic tests for the class using the RepresentsTemporalValue mode + """ + props = QgsRasterLayerTemporalProperties(None) + props.setMode(Qgis.RasterTemporalMode.RepresentsTemporalValues) + self.assertEqual(props.mode(), + Qgis.RasterTemporalMode.RepresentsTemporalValues) + self.assertEqual(props.temporalRepresentationScale(), 1) + self.assertEqual(props.temporalRepresentationScaleUnit(), Qgis.TemporalUnit.Days) + self.assertEqual(props.temporalRepresentationOffset(), QDateTime()) + self.assertEqual(props.temporalRepresentationBandNumber(), 1) + self.assertFalse(props.isActive()) + + props.setTemporalRepresentationScale(2.5) + props.setTemporalRepresentationScaleUnit(Qgis.TemporalUnit.Weeks) + props.setTemporalRepresentationOffset(QDateTime(QDate(2024, 1, 1), QTime(0, 0, 0))) + props.setTemporalRepresentationBandNumber(2) + props.setIsActive(True) + self.assertEqual(props.temporalRepresentationScale(), 2.5) + self.assertEqual(props.temporalRepresentationScaleUnit(), Qgis.TemporalUnit.Weeks) + self.assertEqual(props.temporalRepresentationOffset(), QDateTime(QDate(2024, 1, 1), QTime(0, 0, 0))) + self.assertEqual(props.temporalRepresentationBandNumber(), 2) + self.assertTrue(props.isActive()) + + doc = QDomDocument("testdoc") + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsRasterLayerTemporalProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.mode(), + Qgis.RasterTemporalMode.RepresentsTemporalValues) + self.assertEqual(props2.temporalRepresentationScale(), 2.5) + self.assertEqual(props2.temporalRepresentationScaleUnit(), Qgis.TemporalUnit.Weeks) + self.assertEqual(props2.temporalRepresentationOffset(), QDateTime(QDate(2024, 1, 1), QTime(0, 0, 0))) + self.assertEqual(props2.temporalRepresentationBandNumber(), 2) + self.assertTrue(props2.isActive()) + if __name__ == '__main__': unittest.main() From 9d416f85c41176ede11bd345b87f4487027a4153 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Thu, 4 Apr 2024 09:38:48 +0700 Subject: [PATCH 62/87] [ui] Fix vertical spacing issues with raster layer properties dialog's temporal panel --- ...rasterlayertemporalpropertieswidgetbase.ui | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui b/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui index 75aa4926c1ff..7e2855573185 100644 --- a/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui +++ b/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui @@ -157,6 +157,19 @@ background: white;QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::ti + + + + Qt::Vertical + + + + 40 + 20 + + + + @@ -207,7 +220,7 @@ background: white;QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::ti - + @@ -217,7 +230,7 @@ background: white;QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::ti - + @@ -233,7 +246,7 @@ background: white;QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::ti - + 0 @@ -270,6 +283,19 @@ background: white;QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::ti + + + + Qt::Vertical + + + + 40 + 20 + + + + @@ -378,6 +404,19 @@ background: white;QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::ti + + + + Qt::Vertical + + + + 40 + 20 + + + + From 69b8a2c1abfcc69d5076f905a84362f216c212e8 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Thu, 4 Apr 2024 14:00:23 +0700 Subject: [PATCH 63/87] Make clang-tidy happy --- src/core/raster/qgsrasterlayerrenderer.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/core/raster/qgsrasterlayerrenderer.cpp b/src/core/raster/qgsrasterlayerrenderer.cpp index 6ad73dac0663..0e5c45024298 100644 --- a/src/core/raster/qgsrasterlayerrenderer.cpp +++ b/src/core/raster/qgsrasterlayerrenderer.cpp @@ -314,10 +314,8 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender QVector transparentPixels = transparency->transparentSingleValuePixelList(); - const qint64 msecsLower = temporalProperties->temporalRepresentationOffset().msecsTo( rendererContext.temporalRange().begin() ); - const qint64 msecsUpper = temporalProperties->temporalRepresentationOffset().msecsTo( rendererContext.temporalRange().end() ); - const double adjustedLower = msecsLower * QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit::Milliseconds, temporalProperties->temporalRepresentationScaleUnit() ) / temporalProperties->temporalRepresentationScale(); - const double adjustedUpper = msecsUpper * QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit::Milliseconds, temporalProperties->temporalRepresentationScaleUnit() ) / temporalProperties->temporalRepresentationScale(); + const double adjustedLower = static_cast< double >( temporalProperties->temporalRepresentationOffset().msecsTo( rendererContext.temporalRange().begin() ) ) * QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit::Milliseconds, temporalProperties->temporalRepresentationScaleUnit() ) / temporalProperties->temporalRepresentationScale(); + const double adjustedUpper = static_cast< double >( temporalProperties->temporalRepresentationOffset().msecsTo( rendererContext.temporalRange().end() ) ) * QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit::Milliseconds, temporalProperties->temporalRepresentationScaleUnit() ) / temporalProperties->temporalRepresentationScale(); 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 ) ); From feabbe722ed9084b16ee9ba107d5d27bf615f1be Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 6 Apr 2024 10:41:37 +0700 Subject: [PATCH 64/87] Address review --- .../qgsrasterlayertemporalproperties.sip.in | 62 +++--------------- .../qgsrasterlayertemporalproperties.sip.in | 62 +++--------------- src/core/raster/qgsrasterlayerrenderer.cpp | 8 ++- .../qgsrasterlayertemporalproperties.cpp | 44 +++++-------- .../raster/qgsrasterlayertemporalproperties.h | 53 ++++----------- src/core/raster/qgsrasterlayerutils.cpp | 2 +- ...qgsrasterlayertemporalpropertieswidget.cpp | 17 +++-- .../src/python/test_qgsrasterlayerrenderer.py | 52 +++++++++++++++ .../test_qgsrasterlayertemporalproperties.py | 34 ++++++---- ...cted_represents_temporal_values_filter.png | Bin 0 -> 471523 bytes ...d_represents_temporal_values_no_filter.png | Bin 0 -> 471523 bytes 11 files changed, 138 insertions(+), 196 deletions(-) create mode 100644 tests/testdata/control_images/rasterlayerrenderer/expected_represents_temporal_values_filter/expected_represents_temporal_values_filter.png create mode 100644 tests/testdata/control_images/rasterlayerrenderer/expected_represents_temporal_values_no_filter/expected_represents_temporal_values_no_filter.png diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in index 65e50c481bb0..606ace7eefd5 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in @@ -142,28 +142,28 @@ Returns a filtered list of bands which match the specified ``range``. .. versionadded:: 3.38 %End - int temporalRepresentationBandNumber() const; + int bandNumber() const; %Docstring -Returns the band number from which the temporal values should be taken. +Returns the band number from which temporal values should be taken. .. note:: This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. -.. seealso:: :py:func:`setTemporalRepresentationBandNumber` +.. seealso:: :py:func:`setBandNumber` .. versionadded:: 3.38 %End - void setTemporalRepresentationBandNumber( int number ); + void setBandNumber( int number ); %Docstring -Sets the band number from which the temporal values should be taken. +Sets the band number from which temporal values should be taken. .. note:: This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. -.. seealso:: :py:func:`temporalRepresentationBandNumber` +.. seealso:: :py:func:`bandNumber` .. versionadded:: 3.38 %End @@ -196,9 +196,9 @@ from the layer. .. versionadded:: 3.38 %End - double temporalRepresentationScale() const; + QgsInterval temporalRepresentationScale() const; %Docstring -Returns the scale, which is a duration factor which should be applied to individual pixel +Returns the scale, which is an interval factor which should be applied to individual pixel values from the layer. .. note:: @@ -207,16 +207,12 @@ values from the layer. .. seealso:: :py:func:`setTemporalRepresentationScale` -.. seealso:: :py:func:`temporalRepresentationScaleUnit` - -.. seealso:: :py:func:`setTemporalRepresentationScaleUnit` - .. versionadded:: 3.38 %End - void setTemporalRepresentationScale( double scale ); + void setTemporalRepresentationScale( QgsInterval scale ); %Docstring -Sets the scale, which is a duration factor which should be applied to individual pixel +Sets the scale, which is an interval factor which should be applied to individual pixel values from the layer. .. note:: @@ -225,44 +221,6 @@ values from the layer. .. seealso:: :py:func:`temporalRepresentationScale` -.. seealso:: :py:func:`temporalRepresentationScaleUnit` - -.. seealso:: :py:func:`setTemporalRepresentationScaleUnit` - -.. versionadded:: 3.38 -%End - - Qgis::TemporalUnit temporalRepresentationScaleUnit() const; -%Docstring -Returns the scale's temporal unit type. - -.. note:: - - This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. - -.. seealso:: :py:func:`setTemporalRepresentationScaleUnit` - -.. seealso:: :py:func:`temporalRepresentationScale` - -.. seealso:: :py:func:`setTemporalRepresentationScale` - -.. versionadded:: 3.38 -%End - - void setTemporalRepresentationScaleUnit( Qgis::TemporalUnit unit ); -%Docstring -Sets the scale's temporal unit type. - -.. note:: - - This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. - -.. seealso:: :py:func:`temporalRepresentationScaleUnit` - -.. seealso:: :py:func:`temporalRepresentationScale` - -.. seealso:: :py:func:`setTemporalRepresentationScale` - .. versionadded:: 3.38 %End diff --git a/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in b/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in index 65e50c481bb0..606ace7eefd5 100644 --- a/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in +++ b/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in @@ -142,28 +142,28 @@ Returns a filtered list of bands which match the specified ``range``. .. versionadded:: 3.38 %End - int temporalRepresentationBandNumber() const; + int bandNumber() const; %Docstring -Returns the band number from which the temporal values should be taken. +Returns the band number from which temporal values should be taken. .. note:: This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. -.. seealso:: :py:func:`setTemporalRepresentationBandNumber` +.. seealso:: :py:func:`setBandNumber` .. versionadded:: 3.38 %End - void setTemporalRepresentationBandNumber( int number ); + void setBandNumber( int number ); %Docstring -Sets the band number from which the temporal values should be taken. +Sets the band number from which temporal values should be taken. .. note:: This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. -.. seealso:: :py:func:`temporalRepresentationBandNumber` +.. seealso:: :py:func:`bandNumber` .. versionadded:: 3.38 %End @@ -196,9 +196,9 @@ from the layer. .. versionadded:: 3.38 %End - double temporalRepresentationScale() const; + QgsInterval temporalRepresentationScale() const; %Docstring -Returns the scale, which is a duration factor which should be applied to individual pixel +Returns the scale, which is an interval factor which should be applied to individual pixel values from the layer. .. note:: @@ -207,16 +207,12 @@ values from the layer. .. seealso:: :py:func:`setTemporalRepresentationScale` -.. seealso:: :py:func:`temporalRepresentationScaleUnit` - -.. seealso:: :py:func:`setTemporalRepresentationScaleUnit` - .. versionadded:: 3.38 %End - void setTemporalRepresentationScale( double scale ); + void setTemporalRepresentationScale( QgsInterval scale ); %Docstring -Sets the scale, which is a duration factor which should be applied to individual pixel +Sets the scale, which is an interval factor which should be applied to individual pixel values from the layer. .. note:: @@ -225,44 +221,6 @@ values from the layer. .. seealso:: :py:func:`temporalRepresentationScale` -.. seealso:: :py:func:`temporalRepresentationScaleUnit` - -.. seealso:: :py:func:`setTemporalRepresentationScaleUnit` - -.. versionadded:: 3.38 -%End - - Qgis::TemporalUnit temporalRepresentationScaleUnit() const; -%Docstring -Returns the scale's temporal unit type. - -.. note:: - - This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. - -.. seealso:: :py:func:`setTemporalRepresentationScaleUnit` - -.. seealso:: :py:func:`temporalRepresentationScale` - -.. seealso:: :py:func:`setTemporalRepresentationScale` - -.. versionadded:: 3.38 -%End - - void setTemporalRepresentationScaleUnit( Qgis::TemporalUnit unit ); -%Docstring -Sets the scale's temporal unit type. - -.. note:: - - This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. - -.. seealso:: :py:func:`temporalRepresentationScaleUnit` - -.. seealso:: :py:func:`temporalRepresentationScale` - -.. seealso:: :py:func:`setTemporalRepresentationScale` - .. versionadded:: 3.38 %End diff --git a/src/core/raster/qgsrasterlayerrenderer.cpp b/src/core/raster/qgsrasterlayerrenderer.cpp index 0e5c45024298..7d3a16b27602 100644 --- a/src/core/raster/qgsrasterlayerrenderer.cpp +++ b/src/core/raster/qgsrasterlayerrenderer.cpp @@ -35,6 +35,7 @@ #include "qgsapplication.h" #include "qgsrastertransparency.h" #include "qgsrasterlayerutils.h" +#include "qgsinterval.h" #include "qgsunittypes.h" #include @@ -303,7 +304,7 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender break; case Qgis::RasterTemporalMode::RepresentsTemporalValues: - if ( mPipe->renderer()->usesBands().contains( temporalProperties->temporalRepresentationBandNumber() ) ) + if ( mPipe->renderer()->usesBands().contains( temporalProperties->bandNumber() ) ) { // if layer has elevation settings and we are only rendering a temporal range => we need to filter pixels by temporal values std::unique_ptr< QgsRasterTransparency > transparency; @@ -314,8 +315,9 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender QVector transparentPixels = transparency->transparentSingleValuePixelList(); - const double adjustedLower = static_cast< double >( temporalProperties->temporalRepresentationOffset().msecsTo( rendererContext.temporalRange().begin() ) ) * QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit::Milliseconds, temporalProperties->temporalRepresentationScaleUnit() ) / temporalProperties->temporalRepresentationScale(); - const double adjustedUpper = static_cast< double >( temporalProperties->temporalRepresentationOffset().msecsTo( rendererContext.temporalRange().end() ) ) * QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit::Milliseconds, temporalProperties->temporalRepresentationScaleUnit() ) / temporalProperties->temporalRepresentationScale(); + const QgsInterval scale = temporalProperties->temporalRepresentationScale(); + const double adjustedLower = static_cast< double >( temporalProperties->temporalRepresentationOffset().msecsTo( rendererContext.temporalRange().begin() ) ) * QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit::Milliseconds, scale.originalUnit() ) / scale.originalDuration(); + const double adjustedUpper = static_cast< double >( temporalProperties->temporalRepresentationOffset().msecsTo( rendererContext.temporalRange().end() ) ) * QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit::Milliseconds, scale.originalUnit() ) / scale.originalDuration(); 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 ) ); diff --git a/src/core/raster/qgsrasterlayertemporalproperties.cpp b/src/core/raster/qgsrasterlayertemporalproperties.cpp index 564c37cc4484..0aef20cfe852 100644 --- a/src/core/raster/qgsrasterlayertemporalproperties.cpp +++ b/src/core/raster/qgsrasterlayertemporalproperties.cpp @@ -22,6 +22,7 @@ QgsRasterLayerTemporalProperties::QgsRasterLayerTemporalProperties( QObject *parent, bool enabled ) : QgsMapLayerTemporalProperties( parent, enabled ) { + mTemporalRepresentationScale.setDays( 1.0 ); } bool QgsRasterLayerTemporalProperties::isVisibleInTemporalRange( const QgsDateTimeRange &range ) const @@ -236,7 +237,7 @@ int QgsRasterLayerTemporalProperties::bandForTemporalRange( QgsRasterLayer *, co } case Qgis::RasterTemporalMode::RepresentsTemporalValues: - return mTemporalRepresentationBandNumber; + return mBandNumber; } BUILTIN_UNREACHABLE } @@ -274,22 +275,22 @@ QList QgsRasterLayerTemporalProperties::filteredBandsForTemporalRange( QgsR } case Qgis::RasterTemporalMode::RepresentsTemporalValues: - return QList() << mTemporalRepresentationBandNumber; + return QList() << mBandNumber; } BUILTIN_UNREACHABLE } -int QgsRasterLayerTemporalProperties::temporalRepresentationBandNumber() const +int QgsRasterLayerTemporalProperties::bandNumber() const { - return mTemporalRepresentationBandNumber; + return mBandNumber; } -void QgsRasterLayerTemporalProperties::setTemporalRepresentationBandNumber( int number ) +void QgsRasterLayerTemporalProperties::setBandNumber( int number ) { - if ( mTemporalRepresentationBandNumber == number ) + if ( mBandNumber == number ) return; - mTemporalRepresentationBandNumber = number; + mBandNumber = number; } QDateTime QgsRasterLayerTemporalProperties::temporalRepresentationOffset() const @@ -305,12 +306,12 @@ void QgsRasterLayerTemporalProperties::setTemporalRepresentationOffset( const QD mTemporalRepresentationOffset = offset; } -double QgsRasterLayerTemporalProperties::temporalRepresentationScale() const +QgsInterval QgsRasterLayerTemporalProperties::temporalRepresentationScale() const { return mTemporalRepresentationScale; } -void QgsRasterLayerTemporalProperties::setTemporalRepresentationScale( double scale ) +void QgsRasterLayerTemporalProperties::setTemporalRepresentationScale( QgsInterval scale ) { if ( mTemporalRepresentationScale == scale ) return; @@ -318,19 +319,6 @@ void QgsRasterLayerTemporalProperties::setTemporalRepresentationScale( double sc mTemporalRepresentationScale = scale; } -Qgis::TemporalUnit QgsRasterLayerTemporalProperties::temporalRepresentationScaleUnit() const -{ - return mTemporalRepresentationScaleUnit; -} - -void QgsRasterLayerTemporalProperties::setTemporalRepresentationScaleUnit( Qgis::TemporalUnit unit ) -{ - if ( mTemporalRepresentationScaleUnit == unit ) - return; - - mTemporalRepresentationScaleUnit = unit; -} - bool QgsRasterLayerTemporalProperties::readXml( const QDomElement &element, const QgsReadWriteContext &context ) { Q_UNUSED( context ) @@ -341,6 +329,7 @@ bool QgsRasterLayerTemporalProperties::readXml( const QDomElement &element, cons setIsActive( temporalNode.attribute( QStringLiteral( "enabled" ), QStringLiteral( "0" ) ).toInt() ); mMode = static_cast< Qgis::RasterTemporalMode >( temporalNode.attribute( QStringLiteral( "mode" ), QStringLiteral( "0" ) ). toInt() ); + mBandNumber = temporalNode.attribute( QStringLiteral( "bandNumber" ), QStringLiteral( "1" ) ).toInt(); mIntervalHandlingMethod = static_cast< Qgis::TemporalIntervalMatchMethod >( temporalNode.attribute( QStringLiteral( "fetchMode" ), QStringLiteral( "0" ) ). toInt() ); switch ( mMode ) @@ -380,10 +369,9 @@ bool QgsRasterLayerTemporalProperties::readXml( const QDomElement &element, cons case Qgis::RasterTemporalMode::RepresentsTemporalValues: { - mTemporalRepresentationBandNumber = temporalNode.attribute( QStringLiteral( "temporalRepresentationBandNumber" ), QStringLiteral( "1" ) ).toInt(); mTemporalRepresentationOffset = QDateTime::fromString( temporalNode.attribute( QStringLiteral( "temporalRepresentationOffset" ) ), Qt::ISODate ); - mTemporalRepresentationScale = temporalNode.attribute( QStringLiteral( "temporalRepresentationScale" ), QStringLiteral( "1" ) ).toDouble(); - mTemporalRepresentationScaleUnit = static_cast< Qgis::TemporalUnit >( temporalNode.attribute( QStringLiteral( "temporalRepresentationScaleUnit" ), QStringLiteral( "4" ) ).toInt() ); + mTemporalRepresentationScale = QgsInterval( temporalNode.attribute( QStringLiteral( "temporalRepresentationScale" ), QStringLiteral( "1" ) ).toDouble(), + static_cast< Qgis::TemporalUnit >( temporalNode.attribute( QStringLiteral( "temporalRepresentationScaleUnit" ), QStringLiteral( "4" ) ).toInt() ) ); break; } @@ -404,6 +392,7 @@ QDomElement QgsRasterLayerTemporalProperties::writeXml( QDomElement &element, QD QDomElement temporalElement = document.createElement( QStringLiteral( "temporal" ) ); temporalElement.setAttribute( QStringLiteral( "enabled" ), isActive() ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); temporalElement.setAttribute( QStringLiteral( "mode" ), QString::number( static_cast< int >( mMode ) ) ); + temporalElement.setAttribute( QStringLiteral( "bandNumber" ), QString::number( mBandNumber ) ); temporalElement.setAttribute( QStringLiteral( "fetchMode" ), QString::number( static_cast< int >( mIntervalHandlingMethod ) ) ); switch ( mMode ) @@ -446,10 +435,9 @@ QDomElement QgsRasterLayerTemporalProperties::writeXml( QDomElement &element, QD case Qgis::RasterTemporalMode::RepresentsTemporalValues: { - temporalElement.setAttribute( QStringLiteral( "temporalRepresentationBandNumber" ), QString::number( mTemporalRepresentationBandNumber ) ); temporalElement.setAttribute( QStringLiteral( "temporalRepresentationOffset" ), mTemporalRepresentationOffset.toString( Qt::ISODate ) ); - temporalElement.setAttribute( QStringLiteral( "temporalRepresentationScale" ), QString::number( mTemporalRepresentationScale ) ); - temporalElement.setAttribute( QStringLiteral( "temporalRepresentationScaleUnit" ), QString::number( static_cast< int >( mTemporalRepresentationScaleUnit ) ) ); + temporalElement.setAttribute( QStringLiteral( "temporalRepresentationScale" ), QString::number( mTemporalRepresentationScale.originalDuration() ) ); + temporalElement.setAttribute( QStringLiteral( "temporalRepresentationScaleUnit" ), QString::number( static_cast< int >( mTemporalRepresentationScale.originalUnit() ) ) ); break; } diff --git a/src/core/raster/qgsrasterlayertemporalproperties.h b/src/core/raster/qgsrasterlayertemporalproperties.h index b6aa967e37f2..166e1081a86b 100644 --- a/src/core/raster/qgsrasterlayertemporalproperties.h +++ b/src/core/raster/qgsrasterlayertemporalproperties.h @@ -22,6 +22,7 @@ #include "qgis_core.h" #include "qgis_sip.h" #include "qgis.h" +#include "qgsinterval.h" #include "qgsrange.h" #include "qgsmaplayertemporalproperties.h" @@ -145,22 +146,22 @@ class CORE_EXPORT QgsRasterLayerTemporalProperties : public QgsMapLayerTemporalP QList< int > filteredBandsForTemporalRange( QgsRasterLayer *layer, const QgsDateTimeRange &range ) const; /** - * Returns the band number from which the temporal values should be taken. + * Returns the band number from which temporal values should be taken. * * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. - * \see setTemporalRepresentationBandNumber() + * \see setBandNumber() * \since QGIS 3.38 */ - int temporalRepresentationBandNumber() const; + int bandNumber() const; /** - * Sets the band number from which the temporal values should be taken. + * Sets the band number from which temporal values should be taken. * * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. - * \see temporalRepresentationBandNumber() + * \see bandNumber() * \since QGIS 3.38 */ - void setTemporalRepresentationBandNumber( int number ); + void setBandNumber( int number ); /** * Returns the temporal offset, which is a fixed datetime which should be added to individual pixel values @@ -183,50 +184,24 @@ class CORE_EXPORT QgsRasterLayerTemporalProperties : public QgsMapLayerTemporalP void setTemporalRepresentationOffset( const QDateTime &offset ); /** - * Returns the scale, which is a duration factor which should be applied to individual pixel + * Returns the scale, which is an interval factor which should be applied to individual pixel * values from the layer. * * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. * \see setTemporalRepresentationScale() - * \see temporalRepresentationScaleUnit() - * \see setTemporalRepresentationScaleUnit() * \since QGIS 3.38 */ - double temporalRepresentationScale() const; + QgsInterval temporalRepresentationScale() const; /** - * Sets the scale, which is a duration factor which should be applied to individual pixel + * Sets the scale, which is an interval factor which should be applied to individual pixel * values from the layer. * * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. * \see temporalRepresentationScale() - * \see temporalRepresentationScaleUnit() - * \see setTemporalRepresentationScaleUnit() * \since QGIS 3.38 */ - void setTemporalRepresentationScale( double scale ); - - /** - * Returns the scale's temporal unit type. - * - * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. - * \see setTemporalRepresentationScaleUnit() - * \see temporalRepresentationScale() - * \see setTemporalRepresentationScale() - * \since QGIS 3.38 - */ - Qgis::TemporalUnit temporalRepresentationScaleUnit() const; - - /** - * Sets the scale's temporal unit type. - * - * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. - * \see temporalRepresentationScaleUnit() - * \see temporalRepresentationScale() - * \see setTemporalRepresentationScale() - * \since QGIS 3.38 - */ - void setTemporalRepresentationScaleUnit( Qgis::TemporalUnit unit ); + void setTemporalRepresentationScale( QgsInterval scale ); QDomElement writeXml( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ) override; @@ -247,10 +222,10 @@ class CORE_EXPORT QgsRasterLayerTemporalProperties : public QgsMapLayerTemporalP QMap< int, QgsDateTimeRange > mRangePerBand; - int mTemporalRepresentationBandNumber = 1; + int mBandNumber = 1; + QDateTime mTemporalRepresentationOffset; - double mTemporalRepresentationScale = 1.0; - Qgis::TemporalUnit mTemporalRepresentationScaleUnit = Qgis::TemporalUnit::Days; + QgsInterval mTemporalRepresentationScale; }; #endif // QGSRASTERLAYERTEMPORALPROPERTIES_H diff --git a/src/core/raster/qgsrasterlayerutils.cpp b/src/core/raster/qgsrasterlayerutils.cpp index f5437c9fbc80..2ffd4d62845b 100644 --- a/src/core/raster/qgsrasterlayerutils.cpp +++ b/src/core/raster/qgsrasterlayerutils.cpp @@ -72,7 +72,7 @@ int QgsRasterLayerUtils::renderedBandForElevationAndTemporalRange( case Qgis::RasterTemporalMode::RepresentsTemporalValues: { - temporalBands << temporalProperties->temporalRepresentationBandNumber(); + temporalBands << temporalProperties->bandNumber(); break; } } diff --git a/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp b/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp index f8c3feb1d9eb..0e63cad44d50 100644 --- a/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp +++ b/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp @@ -115,6 +115,7 @@ void QgsRasterLayerTemporalPropertiesWidget::saveTemporalProperties() QgsRasterLayerTemporalProperties *temporalProperties = qobject_cast< QgsRasterLayerTemporalProperties * >( mLayer->temporalProperties() ); temporalProperties->setMode( mModeComboBox->currentData().value< Qgis::RasterTemporalMode >() ); + temporalProperties->setBandNumber( mBandComboBox->currentBand() ); const QgsDateTimeRange normalRange = QgsDateTimeRange( mStartTemporalDateTimeEdit->dateTime(), mEndTemporalDateTimeEdit->dateTime() ); @@ -124,10 +125,8 @@ void QgsRasterLayerTemporalPropertiesWidget::saveTemporalProperties() temporalProperties->setTemporalRepresentationOffset( mOffsetDateTimeEdit->dateTime() ); - temporalProperties->setTemporalRepresentationScale( mScaleSpinBox->value() ); - temporalProperties->setTemporalRepresentationScaleUnit( static_cast< Qgis::TemporalUnit >( mScaleUnitComboBox->currentData().toInt() ) ); - - temporalProperties->setTemporalRepresentationBandNumber( mBandComboBox->currentBand() ); + const QgsInterval scale( mScaleSpinBox->value(), static_cast< Qgis::TemporalUnit >( mScaleUnitComboBox->currentData().toInt() ) ); + temporalProperties->setTemporalRepresentationScale( scale ); for ( QgsMapLayerConfigWidget *widget : std::as_const( mExtraWidgets ) ) { @@ -158,6 +157,9 @@ void QgsRasterLayerTemporalPropertiesWidget::syncToLayer() break; } + mBandComboBox->setLayer( mLayer ); + mBandComboBox->setBand( temporalProperties->bandNumber() ); + mStartTemporalDateTimeEdit->setDateTime( temporalProperties->fixedTemporalRange().begin() ); mEndTemporalDateTimeEdit->setDateTime( temporalProperties->fixedTemporalRange().end() ); @@ -168,11 +170,8 @@ void QgsRasterLayerTemporalPropertiesWidget::syncToLayer() mOffsetDateTimeEdit->setDateTime( temporalProperties->temporalRepresentationOffset() ); - mScaleSpinBox->setValue( temporalProperties->temporalRepresentationScale() ); - mScaleUnitComboBox->setCurrentIndex( mScaleUnitComboBox->findData( static_cast< int >( temporalProperties->temporalRepresentationScaleUnit() ) ) ); - - mBandComboBox->setLayer( mLayer ); - mBandComboBox->setBand( temporalProperties->temporalRepresentationBandNumber() ); + mScaleSpinBox->setValue( temporalProperties->temporalRepresentationScale().originalDuration() ); + mScaleUnitComboBox->setCurrentIndex( mScaleUnitComboBox->findData( static_cast< int >( temporalProperties->temporalRepresentationScale().originalUnit() ) ) ); mTemporalGroupBox->setChecked( temporalProperties->isActive() ); diff --git a/tests/src/python/test_qgsrasterlayerrenderer.py b/tests/src/python/test_qgsrasterlayerrenderer.py index 727a55affca5..222e7c6303a2 100644 --- a/tests/src/python/test_qgsrasterlayerrenderer.py +++ b/tests/src/python/test_qgsrasterlayerrenderer.py @@ -21,6 +21,7 @@ Qgis, QgsCoordinateReferenceSystem, QgsGeometry, + QgsInterval, QgsMapClippingRegion, QgsMapSettings, QgsRasterLayer, @@ -517,6 +518,57 @@ def test_render_fixed_range_per_band_with_temporal_range_filter(self): map_settings) ) + def test_render_represents_temporal_values(self): + """ + Test rendering a raster with its temporal properties' mode set + to represents temporal values + """ + raster_layer = QgsRasterLayer(os.path.join(TEST_DATA_DIR, 'scaleoffset.tif')) + self.assertTrue(raster_layer.isValid()) + + renderer = QgsSingleBandGrayRenderer(raster_layer.dataProvider(), 1) + raster_layer.setRenderer(renderer) + + # set layer as temporal enabled + raster_layer.temporalProperties().setIsActive(True) + raster_layer.temporalProperties().setMode( + Qgis.RasterTemporalMode.RepresentsTemporalValues + ) + raster_layer.temporalProperties().setBandNumber(1) + raster_layer.temporalProperties().setTemporalRepresentationOffset(QDateTime(QDate(2024, 1, 1), QTime(0, 0, 0))) + raster_layer.temporalProperties().setTemporalRepresentationScale(QgsInterval(1, Qgis.TemporalUnit.Days)) + + 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.setIsTemporal(False) + self.assertTrue( + self.render_map_settings_check( + 'No temporal range filter on map settings on represents temporal values mode', + 'represents_temporal_values_no_filter', + map_settings) + ) + + # map settings matches part of the overall range + map_settings.setIsTemporal(True) + map_settings.setTemporalRange(QgsDateTimeRange( + QDateTime(QDate(2024, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2024, 1, 5), + QTime(23, 59, 59)) + )) + self.assertTrue( + self.render_map_settings_check( + 'Temporal range filter on map settings on represents temporal values mode', + 'represents_temporal_values_filter', + map_settings) + ) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgsrasterlayertemporalproperties.py b/tests/src/python/test_qgsrasterlayertemporalproperties.py index e16af427eed1..ac5588cd903c 100644 --- a/tests/src/python/test_qgsrasterlayertemporalproperties.py +++ b/tests/src/python/test_qgsrasterlayertemporalproperties.py @@ -16,6 +16,7 @@ from qgis.PyQt.QtXml import QDomDocument from qgis.core import ( Qgis, + QgsInterval, QgsRasterLayerTemporalProperties, QgsReadWriteContext, QgsDateTimeRange @@ -281,23 +282,33 @@ def test_basic_represents_temporal_value(self): props.setMode(Qgis.RasterTemporalMode.RepresentsTemporalValues) self.assertEqual(props.mode(), Qgis.RasterTemporalMode.RepresentsTemporalValues) - self.assertEqual(props.temporalRepresentationScale(), 1) - self.assertEqual(props.temporalRepresentationScaleUnit(), Qgis.TemporalUnit.Days) + self.assertEqual(props.bandNumber(), 1) + self.assertEqual(props.temporalRepresentationScale(), QgsInterval(1, Qgis.TemporalUnit.Days)) self.assertEqual(props.temporalRepresentationOffset(), QDateTime()) - self.assertEqual(props.temporalRepresentationBandNumber(), 1) self.assertFalse(props.isActive()) - props.setTemporalRepresentationScale(2.5) - props.setTemporalRepresentationScaleUnit(Qgis.TemporalUnit.Weeks) + props.setBandNumber(2) + props.setTemporalRepresentationScale(QgsInterval(2.5, Qgis.TemporalUnit.Weeks)) props.setTemporalRepresentationOffset(QDateTime(QDate(2024, 1, 1), QTime(0, 0, 0))) - props.setTemporalRepresentationBandNumber(2) props.setIsActive(True) - self.assertEqual(props.temporalRepresentationScale(), 2.5) - self.assertEqual(props.temporalRepresentationScaleUnit(), Qgis.TemporalUnit.Weeks) + self.assertEqual(props.bandNumber(), 2) + self.assertEqual(props.temporalRepresentationScale(), QgsInterval(2.5, Qgis.TemporalUnit.Weeks)) self.assertEqual(props.temporalRepresentationOffset(), QDateTime(QDate(2024, 1, 1), QTime(0, 0, 0))) - self.assertEqual(props.temporalRepresentationBandNumber(), 2) self.assertTrue(props.isActive()) + self.assertEqual(props.bandForTemporalRange(None, QgsDateTimeRange( + QDateTime(QDate(2023, 5, 3), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 4), + QTime(12, 13, 14)) + )), 2) + self.assertEqual(props.filteredBandsForTemporalRange(None, QgsDateTimeRange( + QDateTime(QDate(2023, 5, 3), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 4), + QTime(12, 13, 14)) + )), [2]) + doc = QDomDocument("testdoc") elem = doc.createElement('test') props.writeXml(elem, doc, QgsReadWriteContext()) @@ -306,10 +317,9 @@ def test_basic_represents_temporal_value(self): props2.readXml(elem, QgsReadWriteContext()) self.assertEqual(props2.mode(), Qgis.RasterTemporalMode.RepresentsTemporalValues) - self.assertEqual(props2.temporalRepresentationScale(), 2.5) - self.assertEqual(props2.temporalRepresentationScaleUnit(), Qgis.TemporalUnit.Weeks) + self.assertEqual(props.bandNumber(), 2) + self.assertEqual(props2.temporalRepresentationScale(), QgsInterval(2.5, Qgis.TemporalUnit.Weeks)) self.assertEqual(props2.temporalRepresentationOffset(), QDateTime(QDate(2024, 1, 1), QTime(0, 0, 0))) - self.assertEqual(props2.temporalRepresentationBandNumber(), 2) self.assertTrue(props2.isActive()) diff --git a/tests/testdata/control_images/rasterlayerrenderer/expected_represents_temporal_values_filter/expected_represents_temporal_values_filter.png b/tests/testdata/control_images/rasterlayerrenderer/expected_represents_temporal_values_filter/expected_represents_temporal_values_filter.png new file mode 100644 index 0000000000000000000000000000000000000000..ebd0fd00bcdfdc84baf630bf9f033c1083269f7b GIT binary patch literal 471523 zcmeI&Z^)fx9S8993|rHj(O)bIl*~ZWCAk**pw1p^>xGydszF9lkiMza9LP9J=8?dl zVx=*f1yK-jYAeEgAq0gWu^99sl>YcGWHcBEi3?#(YB-j9Xa8KfY2(>X-m>!Gjc5P5aQBW~ zdj^9yJp9U^rEPya_RGQGvcb-k?Zf+*2E*}Wv~=iir_Y>S=)YA0_q_M@t3D~~*9{vs zblT9;(o(1Ab$!^Q&%W68k!M2(%IqNUiuh zOn?9Z0-*#1QYcCL5FkLH1A(9K{nSU#-3932xZaKsPQB4Pm;eC+1kM)_Nat(#nE(L- z1bPz?NWIZJm;eC+1kM)_Nat(#nE(L-1bPz?NWIZJm;iyu0{0!T-lr)#KMZ0RjZl5fDh}%t9pu2oR`RKp<5;e$5jgKp-6fft1cH zR6>9NfvN=rQq|+v{6YdReCKsTcL5fn^ALf#1%~6vNJ7ocWg!6q1PH_w5J)j~?M;9H z0RnRi2&B2GEF?gH0D+hS0x71hy$KK?KwxeGfiyRjg$)IsJaYObcL5rrSx$h!{{)1S zH-P{F0tAu}5J*XkK@kK95Fj9syaNOX5Fn6*fIv!O42mE?fB*r3l*Bvm{FR%&JSh}bQ10tA8! z2&CYoHY7lR0D(mW1kxgEo*_Vh0D<5F0x3AD4G9n+KwuF8fwYL4X9y5TK;Scv4mY|B zkia0KlpvgiHYJdlz~NJSe(NqkVnb2RECJy(OUHKv2oNC9nSem*jM_m22oNAJOF$sa((xSu z0t5(jCLoYHqjnGh0t5)m5)eqUbbME(z`+X+9(5O>%F$~&Apzl(&@hxjfB=E21%~6v zNGetR3TU1H0Rrg=2&8mop%MZF2vjW~kg6WP<_QoWkdA;rN@o_CRkG#X_dMb*KxRWz zE`e+Wgi|)7QYZle1j-f=NM+AgcLWF!$VNaQWiu*;5+FdJYyp8(_I!0mfB=DP1O!qS zqk7X#FD<(Z(1o-!2oNYlKsXgLNNo@xKp=Mkft33M)lYx`fkFfXQXzxX1_1&Dau*Ot zxld621PBl)L?F39I=XT5yW9mR9v5+FdJTmgYp?re2NAa8-K zU%mI&?gHdJ!xq&Grxth~B|v}xfp7u>DV(L92oNAZpoM@yYJulb0t5&U2qz$r!dcph z009C7S_lZF7I+>dkcz;yhrY1IU4T@kp^gv&!YPEJJqQpWK%ljNKx$2B0|EpH5C|b4 zkU}Whg8%^n1X>FSq}GHsAV7csfe-=$DTJauauT?I^!Q!w0^~F?wN@!0oT?nVrU?)r zke+}*N^d4AB0zvZodU!0WF(d9oV~6I5Fn78fIv!aBnl!xfIyuB0;$fquiy2NXFvC} zy8!DuSwVn66anEBh1I462oNC9R6rm#6|0pa9TAV7csfg}V3QW9fO1OWmB2nZza009C7 z2qYmOkdhdKA_x#5K%nbD`r>29ce@MVy(2(?0D+kTU5C@mVth$}009E^3J9cnXRmVt z1PCN0Adr$8hhhj2AW*M>K&p54IwwGYz`O!C{CH)#)4Kq|Y2MLTM}PnU0#OA7QdC}> z6Cgl*fyx8~Qe^|zE&&1rQWX$LsZK^+1PBnQO<*{ljHFU+Q`av60t6Bj5J-s* zCU#j{554(fcL8E6+n)e|MFoV@qH>-iK!5;&paKFZD5uQ`5FkKcQ2~LpsGR2r5FkJx zsDMBU%4stK1PBmVR6roDFSq6RC7*T|V0|Ym2oN9;Oh7mVqqGqL0t5)O5fDgi^gK&| z009EQ1O!qrN*fU%K!89S0fE#;&$9#w5FijtAaEdk`KnX@a2FuhbZkU`0D(#c0*6zj zirOYXfB=Ek0s^Tup$!NSAV465fIteNXb%De2oPv3Adp%U+JFE70=Wz9`q=|NOzSRy zaLWCiQa=F#1PT!lNQDei8w3at$X!4nrfB=E)1%~6vNGfGN zMhy@k(3QZ}XWsL^wC)0Q)p_^l2&eANok)NH0RmG51kw}~9}yrxfI#;G0;zj*ClVk) zfWQ<1fiwlhM+68EAke*lKKUK#BxwTLJ_K5NIYKkeYc}O@IIa0+9p+QY2X05}04$&v!p?ue$*Ads$h7 zfN-i|%6cR~fItQU0x5$bDUkpH0yPK-q#CBIM*;*0WFR1rG8mE)2@oJqgMdJ)Vag@- zc1WRQ}+1PBlyFjYVxO=a;R0RjXFbS)r| zx)ygH0RjXFOcxlACnKpe-6r=>PanAYYwiN%K1KBts8~QaRXlvH6Cgk!B>{ny(lpdU zfB=Dt1q4#X!`C_i0t8YL5J)LaLoEad5U5x{APpW_IkC%KfQtVOXq^Co1OFg2oOk6Kp-VJ6eSTLK%hnefmGwv^-O>OfdmDb1=5QT-Te`F0TLXe zk_ZqWP?kWma4M^J-4Gx^fIvb50x6+kD1`t40yPT=q?)I%cLD?mBqSh^5*mh52oN9; zMc~fmOMn0Y0v!nmq>i8+Lx2DQ0<#4K(rh2!5+E>DVBhs0xZYiWsWd(eA|RZC zFxrFw0RjZt3J9dOf}STpfB=CY0s<)rqfH19AV8q4fIw<1=y?JJ2oMM&AdrGE+JwLi zfy>@{;0AXAW&rt$z*+&}wAREY1PBly(1UH*!61PBlyuvS1Itu^rp0RjXF^dKOR zdO&w10RjXFtQ8PQYfXI8qre^izHQlEfF1|nXab1{2&Y5_p$q~92-GehkZOMe^iO~Q zfkXrZQX+#;1_1&DY8MblwZ8%SCqRHeA_4*_kwGj_#{TDCc-~!r1>ihFfItQU!|`M! zp)weh5(y9>P=kO#s$t4{BtU>b1_A;pgCQxA009Cu2neJarmRN-1PEjxP<|j?@b!Q0 za2FthS5b)s2oQKxp!{%pwO2n8AV7dX!U6&*;o&Ha009Cu2?(T`rma^31PCN7AdnIs zj?xGaAW)OQYXa$+ubh0Fy8tzfU#|oR5J*$tHQ|(IiK-$%fB=CS1O!qIQ`RE^0t7M; z5J(veNr?mq5U4>wAk{EsJrW>Lw7~LXFFjP+T>#-!^ed}50t5);BOs9SnUzWj5Fk*r zfIupGyqY6GfIvP10x6$asgwW#0!0f5q@u^GIRXS$2^_s*|A$Ju3$UuiGJX|KWz10* z1PBnwUtl<%jHFWjbJPI=0tAW>5J*LgQ4<6R5XfIZAmu+p9S|Tupa=nhRKyrHK_IZe zzQ0^|g}VTOQEk^mKsYtgvX%e=0tBK72&8DVHYPxT0D&d~0;!3XwFC$dAP`MJAVs6K zF#!Su2s9B8NKLe?B``(c511PBnATVObzjHJ@sTow`_K!89@0f7`#*WLsO5FjwOfIym?%0dDJ z2oQ)VkbNL+d+O9FcL8F)BK9UgfI#;G*@sj2WKJYNfB=CQ0s<+9syzu1AV8q8fIw04z@Lx3a{NaBoFH(^d8eLmrLMnj+qNy~ zKx=DjN#B?HWjw=**Y5ei@BbMVtdeSJ|6i#RG-D7TK!8Aa0f7|WR7C;=2oP98Kp-uF z<`Dt}2oMM_AdteFsz`tUfoKAU{=836(Wni3F5O|9Fo1PBnwS3n@;yBnPmAV8og0fE%ix(!Q! z0D*i3$_u0qfB%}j&I087Md^$H0RjO8$_u9eLn}dm009E|2?(V8cA_H!1PC-KAds3| zyP*jXAdsJcK+10?IwC-TKz#zY{o|aMJDdd&PW62PCL%z9K$!x=@nj^G%G{l<2@oJq zoq#~9ZlwkyK!8A*0s^Vbz3G|&0Rq(t2&C#(Y9Imx3KqEfs$c!O!&!iWC!N!}aGC?; zX95HW5J)2+kkUAN3jqQI2+R=>NOOSvOn?9Z0%-&UQW|G(AwYltfjI&KX%3K|2@uFt z;M3n7Zg&KKnlXB z2mt~F2=o;YNPPu8PoM;WgGcxO!dZY4Hl@ec1cZ}MfdByl1hNniNLj2w69fnlARv%@ z0|W>VAdrQCK+0kbnjk=c00DvI8z69kz=Nke_^7i0C!qK%fy4sBDKWU$5g1ON~qKp+nRft1G{bU}bX69P}){>W7!&jQqo zCY1PBly5KcfKg|k$N009C7dI$)l9(W!lK!5;&Z~_7; zoTW+x2oNC9LqH()!1E}9Tm&vU@cG@&0_3s{eS{DYP9YT4AV7csf!+cFsW+hl1PBly z5JEs8g-}$3009C7dJ71o-h>JeAV7dX2myf@1fmECqyVfRx$xloodpO0sRRK61YQ#mPCf+!1PBnwLO>v8u?9^LAV7eCK=KU` zAV7dX76Jk(i#2G1009C7QU}r(9)5bCvjDz30t5&Um?@AtoMtxTM*;*05NKCGAho-D zlM^67AS(fZl+`*kLx2E*b_E1dySq0z0RjXT6}bF|8|z8W0tlx?SK~ec1PBm_Dj<-e z@+wY%009Dv3J9b{rQAn=009DV1%~6vNGioOR-FI=0t6Nm5J(F`xs3n;0&@jEdBw*+ znDi{b+>VO9K{yq=I*k(`K%g!GfmGK%O+$bHfno&&Qn9PkH~|6#>Jku0b?wtM1PBl) zRzM&XyE=^%C|h9t!k->?7NG1M#_V1=#ne@s009C778Vdl3sbp~009C7VhRYPn7V2c zAV7e?!U6(mVJbHgAV7dXOaXxuQ&(*QVFaFh{;4C*0)(+tr4Rw(RLG*VNq_)>+64qs z?He{90RjXH5fDg)EJ~XM2oR`UKp@q=Ve=6nK%fu-fmFz%w7ICjO&7gz$XS3zwcJM_ zR{`Oa>t^&tfB=EU1O!rJ3pXwS0t9ju5JoCTQgH091PD|jAdo6qq%jB(AW*u1Kq`HM`X@kuKqUeKsggw+g8%^n#S09_laW*^ zevJkoKp>UCrO&+geR-V)NHux-_Xwx-=H5tv009D11O(C)6qg7PAV46!fIv!b?u`Tp z5Fju`Kp;&)aftu{0tC_v2&DAp-bkPYf$=%-IL%pr7B(GeM&T3*R#^fB2oUHdAdtFw zxtjn10t6xn2&71`$`T+zfIv3^fz-{*-2?~_AP`AFAVq>zmcZfyzq|SFTb%`1+{>LU z2neSZwroZM1PBx$Adm`Jk`@UNAkczKmh^*semPEkpKY#EeHst7Pef| zjA#AjiFIcIYTBq-2uu|aPE%Q2BtU=wfz$#5DYdxw5gZ0RjXFOcxlACnKpe-KO-b$L`tnRc8T8-=h8rG%O&T8eYE92@oKVlYl_VX&ZVW zK!8BQ0s^Vwfl>wD`SbJNRM=U7QmJS6JseL)5^A=QUkMN(Kp>HTKuQGdH3SF{ zATV1%AkFsiD**xo2qY2^NQt1mh5!Kq1ZE2eq}e`xB|u=Rz@3+T;1Xv6rqZ|=L_jzN zVN`?w0Rja23J9dWf}STpfB=CY0s<)rqap+d5FpT3Kp^!M^gICq1PBBX5J*876(KM~ z;Ow{EbGfqsGl2X=V5@*|+G^qo0RjXFtUy2@tpMFC2@oJaV5@*Y+G^qo0RjXFtUy2@ ztpMFC2@oJaV5@*Y+G^s;3I(qJ_qFTJ0<3TWUQHkq0pXO%BD6t(0D;y81XAl?fcXg! zAdrcGK+0qh+8{uHKb3jzYEg)N(r009C82-F`)r+n@2 zdz=L*;8WBh0RjXz1?mr{%~}14009C7G8Pa>881g`1PBmlNkAaAv~9BzAV45v0fCh9 za?{L+23odpn1Re!REBS3&aIRXNyoL%XZ009D3 z3kamD*K0Te1PGKPAdt%0l}-r|AW*e{K&pDZh9f}W1c67-z3W4@odq~y#5(>eoa)%4 zDF_fCP`dSfe2b5Fk*#fIupLhbACEfIt-j0;!5M8iGJz zfjj?j@wv_d1V&Y^i-2(IqUBx!1PBm_CLoZa(JD-U009DB1O!qSE%y>2K!89r0f7{a zR$&4J2oUHZAdtFfxtG8cffsg+_d5$P1;!-;Jp_bP4?K?&AV7dXI01na&Qc`;1PBo5 zAs~=?;CYk)0RjZV2?(TcmMRe-K!89G0fE#5&!e3M?wWk_ptAs-**rj?Jptj=-p)-- zfB=Ck1q4!-Yta+|0tDI<5J>Iq+{6S35Xe$MAZ580O%WhKpgjSBl&WgO%{mLP@&;oxAe>^T zs!4zV0Ro)`1X5=*4-gUW>qap+d5FpT3Kp^!M^gICq1PBBXSQ`vpdF7F( Yb{stQyubbS)rxzsy>8>sHJ`ipe}~ZQK>z>% literal 0 HcmV?d00001 From e9b3408a6c230fe14eaec753f791b661660cb264 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 6 Apr 2024 12:35:03 +0700 Subject: [PATCH 65/87] Optimize a bit --- .../raster/qgsrasterlayertemporalproperties.sip.in | 4 ++-- .../raster/qgsrasterlayertemporalproperties.sip.in | 4 ++-- src/core/raster/qgsrasterlayerrenderer.cpp | 7 ++++--- src/core/raster/qgsrasterlayertemporalproperties.cpp | 4 ++-- src/core/raster/qgsrasterlayertemporalproperties.h | 4 ++-- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in index 606ace7eefd5..623c5230738f 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in @@ -168,7 +168,7 @@ Sets the band number from which temporal values should be taken. .. versionadded:: 3.38 %End - QDateTime temporalRepresentationOffset() const; + const QDateTime temporalRepresentationOffset() const; %Docstring Returns the temporal offset, which is a fixed datetime which should be added to individual pixel values from the layer. @@ -196,7 +196,7 @@ from the layer. .. versionadded:: 3.38 %End - QgsInterval temporalRepresentationScale() const; + const QgsInterval temporalRepresentationScale() const; %Docstring Returns the scale, which is an interval factor which should be applied to individual pixel values from the layer. diff --git a/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in b/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in index 606ace7eefd5..623c5230738f 100644 --- a/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in +++ b/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in @@ -168,7 +168,7 @@ Sets the band number from which temporal values should be taken. .. versionadded:: 3.38 %End - QDateTime temporalRepresentationOffset() const; + const QDateTime temporalRepresentationOffset() const; %Docstring Returns the temporal offset, which is a fixed datetime which should be added to individual pixel values from the layer. @@ -196,7 +196,7 @@ from the layer. .. versionadded:: 3.38 %End - QgsInterval temporalRepresentationScale() const; + const QgsInterval temporalRepresentationScale() const; %Docstring Returns the scale, which is an interval factor which should be applied to individual pixel values from the layer. diff --git a/src/core/raster/qgsrasterlayerrenderer.cpp b/src/core/raster/qgsrasterlayerrenderer.cpp index 7d3a16b27602..3a9e8f824678 100644 --- a/src/core/raster/qgsrasterlayerrenderer.cpp +++ b/src/core/raster/qgsrasterlayerrenderer.cpp @@ -315,9 +315,10 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender QVector transparentPixels = transparency->transparentSingleValuePixelList(); - const QgsInterval scale = temporalProperties->temporalRepresentationScale(); - const double adjustedLower = static_cast< double >( temporalProperties->temporalRepresentationOffset().msecsTo( rendererContext.temporalRange().begin() ) ) * QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit::Milliseconds, scale.originalUnit() ) / scale.originalDuration(); - const double adjustedUpper = static_cast< double >( temporalProperties->temporalRepresentationOffset().msecsTo( rendererContext.temporalRange().end() ) ) * QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit::Milliseconds, scale.originalUnit() ) / scale.originalDuration(); + const QDateTime &offset = temporalProperties->temporalRepresentationOffset(); + const QgsInterval &scale = temporalProperties->temporalRepresentationScale(); + const double adjustedLower = static_cast< double >( offset.msecsTo( rendererContext.temporalRange().begin() ) ) * QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit::Milliseconds, scale.originalUnit() ) / scale.originalDuration(); + const double adjustedUpper = static_cast< double >( offset.msecsTo( rendererContext.temporalRange().end() ) ) * QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit::Milliseconds, scale.originalUnit() ) / scale.originalDuration(); 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 ) ); diff --git a/src/core/raster/qgsrasterlayertemporalproperties.cpp b/src/core/raster/qgsrasterlayertemporalproperties.cpp index 0aef20cfe852..2e96313df230 100644 --- a/src/core/raster/qgsrasterlayertemporalproperties.cpp +++ b/src/core/raster/qgsrasterlayertemporalproperties.cpp @@ -293,7 +293,7 @@ void QgsRasterLayerTemporalProperties::setBandNumber( int number ) mBandNumber = number; } -QDateTime QgsRasterLayerTemporalProperties::temporalRepresentationOffset() const +const QDateTime QgsRasterLayerTemporalProperties::temporalRepresentationOffset() const { return mTemporalRepresentationOffset; } @@ -306,7 +306,7 @@ void QgsRasterLayerTemporalProperties::setTemporalRepresentationOffset( const QD mTemporalRepresentationOffset = offset; } -QgsInterval QgsRasterLayerTemporalProperties::temporalRepresentationScale() const +const QgsInterval QgsRasterLayerTemporalProperties::temporalRepresentationScale() const { return mTemporalRepresentationScale; } diff --git a/src/core/raster/qgsrasterlayertemporalproperties.h b/src/core/raster/qgsrasterlayertemporalproperties.h index 166e1081a86b..9a3013fdf4d2 100644 --- a/src/core/raster/qgsrasterlayertemporalproperties.h +++ b/src/core/raster/qgsrasterlayertemporalproperties.h @@ -171,7 +171,7 @@ class CORE_EXPORT QgsRasterLayerTemporalProperties : public QgsMapLayerTemporalP * \see setTemporalRepresentationOffset() * \since QGIS 3.38 */ - QDateTime temporalRepresentationOffset() const; + const QDateTime temporalRepresentationOffset() const; /** * Sets the temporal offset, which is a fixed datetime which should be added to individual pixel values @@ -191,7 +191,7 @@ class CORE_EXPORT QgsRasterLayerTemporalProperties : public QgsMapLayerTemporalP * \see setTemporalRepresentationScale() * \since QGIS 3.38 */ - QgsInterval temporalRepresentationScale() const; + const QgsInterval temporalRepresentationScale() const; /** * Sets the scale, which is an interval factor which should be applied to individual pixel From 6dafb5d495d170f048555b924e116f126be9b923 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 7 Apr 2024 11:33:18 +0700 Subject: [PATCH 66/87] More review addressed --- .../raster/qgsrasterlayertemporalproperties.sip.in | 6 +++--- .../raster/qgsrasterlayertemporalproperties.sip.in | 6 +++--- src/core/raster/qgsrasterlayertemporalproperties.cpp | 6 +++--- src/core/raster/qgsrasterlayertemporalproperties.h | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in index 623c5230738f..4d9e5a0728e3 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in @@ -168,7 +168,7 @@ Sets the band number from which temporal values should be taken. .. versionadded:: 3.38 %End - const QDateTime temporalRepresentationOffset() const; + QDateTime temporalRepresentationOffset() const; %Docstring Returns the temporal offset, which is a fixed datetime which should be added to individual pixel values from the layer. @@ -196,7 +196,7 @@ from the layer. .. versionadded:: 3.38 %End - const QgsInterval temporalRepresentationScale() const; + const QgsInterval &temporalRepresentationScale() const; %Docstring Returns the scale, which is an interval factor which should be applied to individual pixel values from the layer. @@ -210,7 +210,7 @@ values from the layer. .. versionadded:: 3.38 %End - void setTemporalRepresentationScale( QgsInterval scale ); + void setTemporalRepresentationScale( const QgsInterval &scale ); %Docstring Sets the scale, which is an interval factor which should be applied to individual pixel values from the layer. diff --git a/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in b/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in index 623c5230738f..4d9e5a0728e3 100644 --- a/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in +++ b/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in @@ -168,7 +168,7 @@ Sets the band number from which temporal values should be taken. .. versionadded:: 3.38 %End - const QDateTime temporalRepresentationOffset() const; + QDateTime temporalRepresentationOffset() const; %Docstring Returns the temporal offset, which is a fixed datetime which should be added to individual pixel values from the layer. @@ -196,7 +196,7 @@ from the layer. .. versionadded:: 3.38 %End - const QgsInterval temporalRepresentationScale() const; + const QgsInterval &temporalRepresentationScale() const; %Docstring Returns the scale, which is an interval factor which should be applied to individual pixel values from the layer. @@ -210,7 +210,7 @@ values from the layer. .. versionadded:: 3.38 %End - void setTemporalRepresentationScale( QgsInterval scale ); + void setTemporalRepresentationScale( const QgsInterval &scale ); %Docstring Sets the scale, which is an interval factor which should be applied to individual pixel values from the layer. diff --git a/src/core/raster/qgsrasterlayertemporalproperties.cpp b/src/core/raster/qgsrasterlayertemporalproperties.cpp index 2e96313df230..659ab8aee834 100644 --- a/src/core/raster/qgsrasterlayertemporalproperties.cpp +++ b/src/core/raster/qgsrasterlayertemporalproperties.cpp @@ -293,7 +293,7 @@ void QgsRasterLayerTemporalProperties::setBandNumber( int number ) mBandNumber = number; } -const QDateTime QgsRasterLayerTemporalProperties::temporalRepresentationOffset() const +QDateTime QgsRasterLayerTemporalProperties::temporalRepresentationOffset() const { return mTemporalRepresentationOffset; } @@ -306,12 +306,12 @@ void QgsRasterLayerTemporalProperties::setTemporalRepresentationOffset( const QD mTemporalRepresentationOffset = offset; } -const QgsInterval QgsRasterLayerTemporalProperties::temporalRepresentationScale() const +const QgsInterval &QgsRasterLayerTemporalProperties::temporalRepresentationScale() const { return mTemporalRepresentationScale; } -void QgsRasterLayerTemporalProperties::setTemporalRepresentationScale( QgsInterval scale ) +void QgsRasterLayerTemporalProperties::setTemporalRepresentationScale( const QgsInterval &scale ) { if ( mTemporalRepresentationScale == scale ) return; diff --git a/src/core/raster/qgsrasterlayertemporalproperties.h b/src/core/raster/qgsrasterlayertemporalproperties.h index 9a3013fdf4d2..f4cff37b4b82 100644 --- a/src/core/raster/qgsrasterlayertemporalproperties.h +++ b/src/core/raster/qgsrasterlayertemporalproperties.h @@ -171,7 +171,7 @@ class CORE_EXPORT QgsRasterLayerTemporalProperties : public QgsMapLayerTemporalP * \see setTemporalRepresentationOffset() * \since QGIS 3.38 */ - const QDateTime temporalRepresentationOffset() const; + QDateTime temporalRepresentationOffset() const; /** * Sets the temporal offset, which is a fixed datetime which should be added to individual pixel values @@ -191,7 +191,7 @@ class CORE_EXPORT QgsRasterLayerTemporalProperties : public QgsMapLayerTemporalP * \see setTemporalRepresentationScale() * \since QGIS 3.38 */ - const QgsInterval temporalRepresentationScale() const; + const QgsInterval &temporalRepresentationScale() const; /** * Sets the scale, which is an interval factor which should be applied to individual pixel @@ -201,7 +201,7 @@ class CORE_EXPORT QgsRasterLayerTemporalProperties : public QgsMapLayerTemporalP * \see temporalRepresentationScale() * \since QGIS 3.38 */ - void setTemporalRepresentationScale( QgsInterval scale ); + void setTemporalRepresentationScale( const QgsInterval &scale ); QDomElement writeXml( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ) override; From 02562d7b915683cb7c196bf333fc299697039512 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 7 Apr 2024 12:27:22 +0700 Subject: [PATCH 67/87] [ui] Fix floating dock panels hidden when toggling reduced view --- src/app/qgisapp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 3b36ea2c3a43..6a34efc4a6b4 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -7336,7 +7336,7 @@ void QgisApp::toggleReducedView( bool viewMapOnly ) for ( QDockWidget *dock : docks ) { - if ( dock->isVisible() && dockWidgetArea( dock ) != Qt::NoDockWidgetArea ) + if ( dock->isVisible() && !dock->isFloating() && dockWidgetArea( dock ) != Qt::NoDockWidgetArea ) { // remember the active docs docksTitle << dock->windowTitle(); From 2ad9065bea62d1892a08e14f0bcd03eec0c88b8d Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 26 Mar 2024 14:34:13 +1000 Subject: [PATCH 68/87] [sensorthings] Observation phenomenonTime can be a time instant According to the specifications, an Observation phenomenonTime can either be a period OR a time instant. Correctly handle instants instead of returning null for the phenomenonTime --- .../providers/sensorthings/qgssensorthingsshareddata.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp b/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp index 3ff383dd6d52..99409a57172d 100644 --- a/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp +++ b/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp @@ -527,7 +527,7 @@ bool QgsSensorThingsSharedData::processFeatureRequest( QString &nextPage, QgsFee return QVariant(); }; - auto getDateTimeRange = []( const basic_json<> &json, const char *tag ) -> std::pair< QVariant, QVariant > + auto getDateTimeRange = []( const basic_json<> &json, const char *tag, bool allowInstant = false ) -> std::pair< QVariant, QVariant > { if ( !json.contains( tag ) ) return { QVariant(), QVariant() }; @@ -545,6 +545,11 @@ bool QgsSensorThingsSharedData::processFeatureRequest( QString &nextPage, QgsFee QDateTime::fromString( rangeParts.at( 1 ), Qt::ISODateWithMs ) }; } + else if ( allowInstant ) + { + const QDateTime instant = QDateTime::fromString( rangeString, Qt::ISODateWithMs ); + return { instant, instant }; + } } return { QVariant(), QVariant() }; @@ -650,7 +655,7 @@ bool QgsSensorThingsSharedData::processFeatureRequest( QString &nextPage, QgsFee case Qgis::SensorThingsEntity::Observation: { - std::pair< QVariant, QVariant > phenomenonTime = getDateTimeRange( featureData, "phenomenonTime" ); + std::pair< QVariant, QVariant > phenomenonTime = getDateTimeRange( featureData, "phenomenonTime", true ); std::pair< QVariant, QVariant > validTime = getDateTimeRange( featureData, "validTime" ); feature.setAttributes( QgsAttributes() From d7325cba401803b62d41854efe3d6191d72e6cc4 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 6 Apr 2024 17:49:44 +0700 Subject: [PATCH 69/87] [raster] New single color renderer --- .../raster/qgsrasterinterface.sip.in | 3 + .../raster/qgssinglecolorrenderer.sip.in | 76 ++++++++ python/PyQt6/core/core_auto.sip | 1 + .../qgssinglecolorrendererwidget.sip.in | 51 ++++++ python/PyQt6/gui/gui_auto.sip | 1 + .../raster/qgsrasterinterface.sip.in | 3 + .../raster/qgssinglecolorrenderer.sip.in | 76 ++++++++ python/core/core_auto.sip | 1 + .../qgssinglecolorrendererwidget.sip.in | 51 ++++++ python/gui/gui_auto.sip | 1 + src/core/CMakeLists.txt | 2 + src/core/raster/qgsrasterinterface.h | 3 + src/core/raster/qgsrasterrendererregistry.cpp | 3 + src/core/raster/qgssinglecolorrenderer.cpp | 164 ++++++++++++++++++ src/core/raster/qgssinglecolorrenderer.h | 77 ++++++++ src/gui/CMakeLists.txt | 2 + src/gui/raster/qgsrasterlayerproperties.cpp | 2 + .../qgsrendererrasterpropertieswidget.cpp | 2 + .../raster/qgssinglecolorrendererwidget.cpp | 75 ++++++++ src/gui/raster/qgssinglecolorrendererwidget.h | 54 ++++++ src/ui/qgssinglecolorrendererwidgetbase.ui | 86 +++++++++ tests/src/python/CMakeLists.txt | 1 + .../src/python/test_qgssinglecolorrenderer.py | 59 +++++++ .../expected_single_color_renderer.png | Bin 0 -> 360955 bytes 24 files changed, 794 insertions(+) create mode 100644 python/PyQt6/core/auto_generated/raster/qgssinglecolorrenderer.sip.in create mode 100644 python/PyQt6/gui/auto_generated/raster/qgssinglecolorrendererwidget.sip.in create mode 100644 python/core/auto_generated/raster/qgssinglecolorrenderer.sip.in create mode 100644 python/gui/auto_generated/raster/qgssinglecolorrendererwidget.sip.in create mode 100644 src/core/raster/qgssinglecolorrenderer.cpp create mode 100644 src/core/raster/qgssinglecolorrenderer.h create mode 100644 src/gui/raster/qgssinglecolorrendererwidget.cpp create mode 100644 src/gui/raster/qgssinglecolorrendererwidget.h create mode 100644 src/ui/qgssinglecolorrendererwidgetbase.ui create mode 100644 tests/src/python/test_qgssinglecolorrenderer.py create mode 100644 tests/testdata/control_images/expected_single_color_renderer/expected_single_color_renderer.png diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterinterface.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterinterface.sip.in index a5a1f1049395..d92205e66396 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterinterface.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterinterface.sip.in @@ -125,6 +125,7 @@ Base class for processing filters like renderers, reprojector, resampler etc. #include #include #include +#include #include %End %ConvertToSubClassCode @@ -157,6 +158,8 @@ Base class for processing filters like renderers, reprojector, resampler etc. sipType = sipType_QgsSingleBandGrayRenderer; else if ( dynamic_cast( sipCpp ) ) sipType = sipType_QgsSingleBandPseudoColorRenderer; + else if ( dynamic_cast( sipCpp ) ) + sipType = sipType_QgsSingleColorRenderer; else if ( dynamic_cast( sipCpp ) ) sipType = sipType_QgsRasterContourRenderer; else diff --git a/python/PyQt6/core/auto_generated/raster/qgssinglecolorrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgssinglecolorrenderer.sip.in new file mode 100644 index 000000000000..55f654b4c6ff --- /dev/null +++ b/python/PyQt6/core/auto_generated/raster/qgssinglecolorrenderer.sip.in @@ -0,0 +1,76 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgssinglecolorrenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsSingleColorRenderer: QgsRasterRenderer +{ +%Docstring(signature="appended") +Raster single color renderer pipe. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgssinglecolorrenderer.h" +%End + public: + + QgsSingleColorRenderer( QgsRasterInterface *input, QColor color ); +%Docstring +Creates a single ``color`` renderer +%End + + + virtual QgsSingleColorRenderer *clone() const /Factory/; + +%Docstring +QgsSingleColorRenderer cannot be copied. Use :py:func:`~QgsSingleColorRenderer.clone` instead. +%End + virtual Qgis::RasterRendererFlags flags() const; + + + static QgsRasterRenderer *create( const QDomElement &elem, QgsRasterInterface *input ) /Factory/; +%Docstring +Creates an instance of the renderer based on definition from XML (used by the renderer registry) +%End + + virtual QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = 0 ) /Factory/; + + + QColor color() const; +%Docstring +Returns the single color used by the renderer. +%End + + void setColor( QColor &color ); +%Docstring +Sets the single color used by the renderer. +%End + + virtual void writeXml( QDomDocument &doc, QDomElement &parentElem ) const; + + + virtual QList usesBands() const; + + + private: + QgsSingleColorRenderer( const QgsSingleColorRenderer & ); + const QgsSingleColorRenderer &operator=( const QgsSingleColorRenderer & ); +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgssinglecolorrenderer.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 5ae6df11f734..19db988c55c8 100644 --- a/python/PyQt6/core/core_auto.sip +++ b/python/PyQt6/core/core_auto.sip @@ -654,6 +654,7 @@ %Include auto_generated/raster/qgssinglebandcolordatarenderer.sip %Include auto_generated/raster/qgssinglebandgrayrenderer.sip %Include auto_generated/raster/qgssinglebandpseudocolorrenderer.sip +%Include auto_generated/raster/qgssinglecolorrenderer.sip %Include auto_generated/scalebar/qgsdoubleboxscalebarrenderer.sip %Include auto_generated/scalebar/qgshollowscalebarrenderer.sip %Include auto_generated/scalebar/qgsnumericscalebarrenderer.sip diff --git a/python/PyQt6/gui/auto_generated/raster/qgssinglecolorrendererwidget.sip.in b/python/PyQt6/gui/auto_generated/raster/qgssinglecolorrendererwidget.sip.in new file mode 100644 index 000000000000..82150e985267 --- /dev/null +++ b/python/PyQt6/gui/auto_generated/raster/qgssinglecolorrendererwidget.sip.in @@ -0,0 +1,51 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/raster/qgssinglecolorrendererwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsSingleColorRendererWidget: QgsRasterRendererWidget +{ +%Docstring(signature="appended") +Renderer widget for the single color renderer. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgssinglecolorrendererwidget.h" +%End + public: + QgsSingleColorRendererWidget( QgsRasterLayer *layer, const QgsRectangle &extent = QgsRectangle() ); +%Docstring +Constructs the widget +%End + + static QgsRasterRendererWidget *create( QgsRasterLayer *layer, const QgsRectangle &extent ) /Factory/; +%Docstring +Widget creation function (use by the renderer registry) +%End + + virtual QgsRasterRenderer *renderer() /Factory/; + + + void setFromRenderer( const QgsRasterRenderer *r ); +%Docstring +Sets the widget state from the specified renderer. +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/raster/qgssinglecolorrendererwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/PyQt6/gui/gui_auto.sip b/python/PyQt6/gui/gui_auto.sip index c668cc6c9a90..4efdc2a58934 100644 --- a/python/PyQt6/gui/gui_auto.sip +++ b/python/PyQt6/gui/gui_auto.sip @@ -461,6 +461,7 @@ %Include auto_generated/raster/qgsrendererrasterpropertieswidget.sip %Include auto_generated/raster/qgssinglebandgrayrendererwidget.sip %Include auto_generated/raster/qgssinglebandpseudocolorrendererwidget.sip +%Include auto_generated/raster/qgssinglecolorrendererwidget.sip %Include auto_generated/raster/qgsrasterlayerproperties.sip %Include auto_generated/raster/qgsrasterlayertemporalpropertieswidget.sip %Include auto_generated/vector/qgsfieldcalculator.sip diff --git a/python/core/auto_generated/raster/qgsrasterinterface.sip.in b/python/core/auto_generated/raster/qgsrasterinterface.sip.in index 4d152aa92a6e..f8fdbec251aa 100644 --- a/python/core/auto_generated/raster/qgsrasterinterface.sip.in +++ b/python/core/auto_generated/raster/qgsrasterinterface.sip.in @@ -125,6 +125,7 @@ Base class for processing filters like renderers, reprojector, resampler etc. #include #include #include +#include #include %End %ConvertToSubClassCode @@ -157,6 +158,8 @@ Base class for processing filters like renderers, reprojector, resampler etc. sipType = sipType_QgsSingleBandGrayRenderer; else if ( dynamic_cast( sipCpp ) ) sipType = sipType_QgsSingleBandPseudoColorRenderer; + else if ( dynamic_cast( sipCpp ) ) + sipType = sipType_QgsSingleColorRenderer; else if ( dynamic_cast( sipCpp ) ) sipType = sipType_QgsRasterContourRenderer; else diff --git a/python/core/auto_generated/raster/qgssinglecolorrenderer.sip.in b/python/core/auto_generated/raster/qgssinglecolorrenderer.sip.in new file mode 100644 index 000000000000..55f654b4c6ff --- /dev/null +++ b/python/core/auto_generated/raster/qgssinglecolorrenderer.sip.in @@ -0,0 +1,76 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgssinglecolorrenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsSingleColorRenderer: QgsRasterRenderer +{ +%Docstring(signature="appended") +Raster single color renderer pipe. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgssinglecolorrenderer.h" +%End + public: + + QgsSingleColorRenderer( QgsRasterInterface *input, QColor color ); +%Docstring +Creates a single ``color`` renderer +%End + + + virtual QgsSingleColorRenderer *clone() const /Factory/; + +%Docstring +QgsSingleColorRenderer cannot be copied. Use :py:func:`~QgsSingleColorRenderer.clone` instead. +%End + virtual Qgis::RasterRendererFlags flags() const; + + + static QgsRasterRenderer *create( const QDomElement &elem, QgsRasterInterface *input ) /Factory/; +%Docstring +Creates an instance of the renderer based on definition from XML (used by the renderer registry) +%End + + virtual QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = 0 ) /Factory/; + + + QColor color() const; +%Docstring +Returns the single color used by the renderer. +%End + + void setColor( QColor &color ); +%Docstring +Sets the single color used by the renderer. +%End + + virtual void writeXml( QDomDocument &doc, QDomElement &parentElem ) const; + + + virtual QList usesBands() const; + + + private: + QgsSingleColorRenderer( const QgsSingleColorRenderer & ); + const QgsSingleColorRenderer &operator=( const QgsSingleColorRenderer & ); +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgssinglecolorrenderer.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 5ae6df11f734..19db988c55c8 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -654,6 +654,7 @@ %Include auto_generated/raster/qgssinglebandcolordatarenderer.sip %Include auto_generated/raster/qgssinglebandgrayrenderer.sip %Include auto_generated/raster/qgssinglebandpseudocolorrenderer.sip +%Include auto_generated/raster/qgssinglecolorrenderer.sip %Include auto_generated/scalebar/qgsdoubleboxscalebarrenderer.sip %Include auto_generated/scalebar/qgshollowscalebarrenderer.sip %Include auto_generated/scalebar/qgsnumericscalebarrenderer.sip diff --git a/python/gui/auto_generated/raster/qgssinglecolorrendererwidget.sip.in b/python/gui/auto_generated/raster/qgssinglecolorrendererwidget.sip.in new file mode 100644 index 000000000000..82150e985267 --- /dev/null +++ b/python/gui/auto_generated/raster/qgssinglecolorrendererwidget.sip.in @@ -0,0 +1,51 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/raster/qgssinglecolorrendererwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsSingleColorRendererWidget: QgsRasterRendererWidget +{ +%Docstring(signature="appended") +Renderer widget for the single color renderer. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgssinglecolorrendererwidget.h" +%End + public: + QgsSingleColorRendererWidget( QgsRasterLayer *layer, const QgsRectangle &extent = QgsRectangle() ); +%Docstring +Constructs the widget +%End + + static QgsRasterRendererWidget *create( QgsRasterLayer *layer, const QgsRectangle &extent ) /Factory/; +%Docstring +Widget creation function (use by the renderer registry) +%End + + virtual QgsRasterRenderer *renderer() /Factory/; + + + void setFromRenderer( const QgsRasterRenderer *r ); +%Docstring +Sets the widget state from the specified renderer. +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/raster/qgssinglecolorrendererwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/gui/gui_auto.sip b/python/gui/gui_auto.sip index c668cc6c9a90..4efdc2a58934 100644 --- a/python/gui/gui_auto.sip +++ b/python/gui/gui_auto.sip @@ -461,6 +461,7 @@ %Include auto_generated/raster/qgsrendererrasterpropertieswidget.sip %Include auto_generated/raster/qgssinglebandgrayrendererwidget.sip %Include auto_generated/raster/qgssinglebandpseudocolorrendererwidget.sip +%Include auto_generated/raster/qgssinglecolorrendererwidget.sip %Include auto_generated/raster/qgsrasterlayerproperties.sip %Include auto_generated/raster/qgsrasterlayertemporalpropertieswidget.sip %Include auto_generated/vector/qgsfieldcalculator.sip diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index c2a907fd1067..66a03c0a6ffc 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -784,6 +784,7 @@ set(QGIS_CORE_SRCS raster/qgssinglebandcolordatarenderer.cpp raster/qgssinglebandgrayrenderer.cpp raster/qgssinglebandpseudocolorrenderer.cpp + raster/qgssinglecolorrenderer.cpp raster/qgshillshaderenderer.cpp mesh/qgsmesh3daveraging.cpp @@ -1881,6 +1882,7 @@ set(QGIS_CORE_HDRS raster/qgssinglebandcolordatarenderer.h raster/qgssinglebandgrayrenderer.h raster/qgssinglebandpseudocolorrenderer.h + raster/qgssinglecolorrenderer.h scalebar/qgsdoubleboxscalebarrenderer.h scalebar/qgshollowscalebarrenderer.h diff --git a/src/core/raster/qgsrasterinterface.h b/src/core/raster/qgsrasterinterface.h index 22c4e2e200d9..479d3bf7f271 100644 --- a/src/core/raster/qgsrasterinterface.h +++ b/src/core/raster/qgsrasterinterface.h @@ -151,6 +151,7 @@ class CORE_EXPORT QgsRasterInterface #include #include #include +#include #include #endif @@ -186,6 +187,8 @@ class CORE_EXPORT QgsRasterInterface sipType = sipType_QgsSingleBandGrayRenderer; else if ( dynamic_cast( sipCpp ) ) sipType = sipType_QgsSingleBandPseudoColorRenderer; + else if ( dynamic_cast( sipCpp ) ) + sipType = sipType_QgsSingleColorRenderer; else if ( dynamic_cast( sipCpp ) ) sipType = sipType_QgsRasterContourRenderer; else diff --git a/src/core/raster/qgsrasterrendererregistry.cpp b/src/core/raster/qgsrasterrendererregistry.cpp index 772b362d5e28..d840f6db65a0 100644 --- a/src/core/raster/qgsrasterrendererregistry.cpp +++ b/src/core/raster/qgsrasterrendererregistry.cpp @@ -26,6 +26,7 @@ #include "qgssinglebandcolordatarenderer.h" #include "qgssinglebandgrayrenderer.h" #include "qgssinglebandpseudocolorrenderer.h" +#include "qgssinglecolorrenderer.h" #include "qgshillshaderenderer.h" #include "qgsapplication.h" #include "qgssettings.h" @@ -62,6 +63,8 @@ QgsRasterRendererRegistry::QgsRasterRendererRegistry() QgsSingleBandPseudoColorRenderer::create, nullptr ) ); insert( QgsRasterRendererRegistryEntry( QStringLiteral( "singlebandcolordata" ), QObject::tr( "Singleband color data" ), QgsSingleBandColorDataRenderer::create, nullptr ) ); + insert( QgsRasterRendererRegistryEntry( QStringLiteral( "singlecolor" ), QObject::tr( "Single color" ), + QgsSingleColorRenderer::create, nullptr ) ); insert( QgsRasterRendererRegistryEntry( QStringLiteral( "hillshade" ), QObject::tr( "Hillshade" ), QgsHillshadeRenderer::create, nullptr ) ); insert( QgsRasterRendererRegistryEntry( QStringLiteral( "contour" ), QObject::tr( "Contours" ), diff --git a/src/core/raster/qgssinglecolorrenderer.cpp b/src/core/raster/qgssinglecolorrenderer.cpp new file mode 100644 index 000000000000..3103875e7815 --- /dev/null +++ b/src/core/raster/qgssinglecolorrenderer.cpp @@ -0,0 +1,164 @@ +/*************************************************************************** + qgssinglecolorrenderer.cpp + ----------------------------- + begin : April 2024 + copyright : (C) 2024 by Mathieu Pellerin + email : mathieu at opengis dot ch + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgssinglecolorrenderer.h" +#include "qgsrastertransparency.h" +#include "qgscolorutils.h" + +#include +#include + +QgsSingleColorRenderer::QgsSingleColorRenderer( QgsRasterInterface *input, QColor color ) + : QgsRasterRenderer( input, QStringLiteral( "singlecolor" ) ) + , mColor( color ) +{ +} + +QgsSingleColorRenderer *QgsSingleColorRenderer::clone() const +{ + QgsSingleColorRenderer *renderer = new QgsSingleColorRenderer( nullptr, mColor ); + renderer->copyCommonProperties( this ); + return renderer; +} + +Qgis::RasterRendererFlags QgsSingleColorRenderer::flags() const +{ + return Qgis::RasterRendererFlag::InternalLayerOpacityHandling; +} + +QgsRasterRenderer *QgsSingleColorRenderer::create( const QDomElement &elem, QgsRasterInterface *input ) +{ + if ( elem.isNull() ) + { + return nullptr; + } + + const QColor color = QgsColorUtils::colorFromString( elem.attribute( QStringLiteral( "color" ), QStringLiteral( "0,0,0" ) ) ); + QgsSingleColorRenderer *r = new QgsSingleColorRenderer( input, color ); + r->readXml( elem ); + + return r; +} + +QgsRasterBlock *QgsSingleColorRenderer::block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback ) +{ + QgsDebugMsgLevel( QStringLiteral( "width = %1 height = %2" ).arg( width ).arg( height ), 4 ); + + std::unique_ptr< QgsRasterBlock > outputBlock( new QgsRasterBlock() ); + if ( !mInput ) + { + return outputBlock.release(); + } + + const std::shared_ptr< QgsRasterBlock > inputBlock( mInput->block( bandNo, extent, width, height, feedback ) ); + if ( !inputBlock || inputBlock->isEmpty() ) + { + QgsDebugError( QStringLiteral( "No raster data!" ) ); + return outputBlock.release(); + } + + std::shared_ptr< QgsRasterBlock > alphaBlock; + if ( mAlphaBand > 0 ) + { + alphaBlock = inputBlock; + } + + if ( !outputBlock->reset( Qgis::DataType::ARGB32_Premultiplied, width, height ) ) + { + return outputBlock.release(); + } + + const QRgb myDefaultColor = renderColorForNodataPixel(); + bool isNoData = false; + for ( qgssize i = 0; i < ( qgssize )width * height; i++ ) + { + double value = inputBlock->valueAndNoData( i, isNoData ); + if ( isNoData ) + { + outputBlock->setColor( i, myDefaultColor ); + continue; + } + + double currentAlpha = mOpacity; + if ( mRasterTransparency ) + { + currentAlpha *= mRasterTransparency->opacityForValue( value ); + } + if ( mAlphaBand > 0 ) + { + const double alpha = alphaBlock->value( i ); + if ( alpha == 0 ) + { + outputBlock->setColor( i, myDefaultColor ); + continue; + } + else + { + currentAlpha *= alpha / 255.0; + } + } + + if ( qgsDoubleNear( currentAlpha, 1.0 ) ) + { + outputBlock->setColor( i, qRgba( mColor.red(), mColor.green(), mColor.blue(), mColor.alpha() ) ); + } + else + { + outputBlock->setColor( i, qRgba( static_cast( currentAlpha * mColor.red() ), + static_cast( currentAlpha * mColor.green() ), + static_cast( currentAlpha * mColor.blue() ), + static_cast( currentAlpha * mColor.alpha() ) ) ); + } + } + + return outputBlock.release(); +} + +void QgsSingleColorRenderer::setColor( QColor &color ) +{ + mColor = color;; +} + +QColor QgsSingleColorRenderer::color() const +{ + return mColor; +} + +void QgsSingleColorRenderer::writeXml( QDomDocument &doc, QDomElement &parentElem ) const +{ + if ( parentElem.isNull() ) + { + return; + } + + QDomElement rasterRendererElem = doc.createElement( QStringLiteral( "rasterrenderer" ) ); + _writeXml( doc, rasterRendererElem ); + + rasterRendererElem.setAttribute( QStringLiteral( "color" ), QgsColorUtils::colorToString( mColor ) ); + + parentElem.appendChild( rasterRendererElem ); +} + +QList QgsSingleColorRenderer::usesBands() const +{ + QList bandList; + for ( int i = 0; i <= bandCount(); i++ ) + { + bandList << i; + } + return bandList; +} diff --git a/src/core/raster/qgssinglecolorrenderer.h b/src/core/raster/qgssinglecolorrenderer.h new file mode 100644 index 000000000000..f0e7d1ede31e --- /dev/null +++ b/src/core/raster/qgssinglecolorrenderer.h @@ -0,0 +1,77 @@ +/*************************************************************************** + qgssinglecolorrenderer.h + --------------------------- + begin : April 2024 + copyright : (C) 2024 by Mathieu Pellerin + email : mathieu at opengis dot ch + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSSINGLECOLORRENDERER_H +#define QGSSINGLECOLORRENDERER_H + +#include "qgis_core.h" +#include "qgis_sip.h" +#include "qgsrasterrenderer.h" + +#include + +class QDomElement; + +/** + * \ingroup core + * \brief Raster single color renderer pipe. + * \since QGIS 3.38 + */ +class CORE_EXPORT QgsSingleColorRenderer: public QgsRasterRenderer +{ + public: + + //! Creates a single \a color renderer + QgsSingleColorRenderer( QgsRasterInterface *input, QColor color ); + + //! QgsSingleColorRenderer cannot be copied. Use clone() instead. + QgsSingleColorRenderer( const QgsSingleColorRenderer & ) = delete; + //! QgsSingleColorRenderer cannot be copied. Use clone() instead. + const QgsSingleColorRenderer &operator=( const QgsSingleColorRenderer & ) = delete; + + QgsSingleColorRenderer *clone() const override SIP_FACTORY; + Qgis::RasterRendererFlags flags() const override; + + //! Creates an instance of the renderer based on definition from XML (used by the renderer registry) + static QgsRasterRenderer *create( const QDomElement &elem, QgsRasterInterface *input ) SIP_FACTORY; + + QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = nullptr ) override SIP_FACTORY; + + /** + * Returns the single color used by the renderer. + */ + QColor color() const; + + /** + * Sets the single color used by the renderer. + */ + void setColor( QColor &color ); + + void writeXml( QDomDocument &doc, QDomElement &parentElem ) const override; + + QList usesBands() const override; + + private: +#ifdef SIP_RUN + QgsSingleColorRenderer( const QgsSingleColorRenderer & ); + const QgsSingleColorRenderer &operator=( const QgsSingleColorRenderer & ); +#endif + + QColor mColor; +}; + +#endif // QGSSINGLECOLORRENDERER_H diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 09b327700271..1fe88959352c 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -24,6 +24,7 @@ set(QGIS_GUI_SRCS raster/qgsrasterrendererwidget.cpp raster/qgssinglebandgrayrendererwidget.cpp raster/qgssinglebandpseudocolorrendererwidget.cpp + raster/qgssinglecolorrendererwidget.cpp raster/qgsrendererrasterpropertieswidget.cpp raster/qgsrastertransparencywidget.cpp raster/qgshillshaderendererwidget.cpp @@ -1456,6 +1457,7 @@ set(QGIS_GUI_HDRS raster/qgsrendererrasterpropertieswidget.h raster/qgssinglebandgrayrendererwidget.h raster/qgssinglebandpseudocolorrendererwidget.h + raster/qgssinglecolorrendererwidget.h raster/qgsrasterlayerproperties.h raster/qgsrasterlayertemporalpropertieswidget.h raster/qgsresamplingutils.h diff --git a/src/gui/raster/qgsrasterlayerproperties.cpp b/src/gui/raster/qgsrasterlayerproperties.cpp index be2d2436b3bb..edea0efb40d9 100644 --- a/src/gui/raster/qgsrasterlayerproperties.cpp +++ b/src/gui/raster/qgsrasterlayerproperties.cpp @@ -49,6 +49,7 @@ #include "qgsrastertransparency.h" #include "qgssinglebandgrayrendererwidget.h" #include "qgssinglebandpseudocolorrendererwidget.h" +#include "qgssinglecolorrendererwidget.h" #include "qgshuesaturationfilter.h" #include "qgshillshaderendererwidget.h" #include "qgssettings.h" @@ -462,6 +463,7 @@ QgsRasterLayerProperties::QgsRasterLayerProperties( QgsMapLayer *lyr, QgsMapCanv QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlebandgray" ), QgsSingleBandGrayRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "hillshade" ), QgsHillshadeRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "contour" ), QgsRasterContourRendererWidget::create ); + QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlecolor" ), QgsSingleColorRendererWidget::create ); //fill available renderers into combo box QgsRasterRendererRegistryEntry entry; diff --git a/src/gui/raster/qgsrendererrasterpropertieswidget.cpp b/src/gui/raster/qgsrendererrasterpropertieswidget.cpp index 95cf5e7a4f77..cc120ed05008 100644 --- a/src/gui/raster/qgsrendererrasterpropertieswidget.cpp +++ b/src/gui/raster/qgsrendererrasterpropertieswidget.cpp @@ -24,6 +24,7 @@ #include "qgsrasterrendererregistry.h" #include "qgssinglebandgrayrendererwidget.h" #include "qgssinglebandpseudocolorrendererwidget.h" +#include "qgssinglecolorrendererwidget.h" #include "qgsmultibandcolorrendererwidget.h" #include "qgspalettedrendererwidget.h" #include "qgshillshaderendererwidget.h" @@ -43,6 +44,7 @@ void QgsRendererRasterPropertiesWidget::initRendererWidgetFunctions() QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "multibandcolor" ), QgsMultiBandColorRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlebandpseudocolor" ), QgsSingleBandPseudoColorRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlebandgray" ), QgsSingleBandGrayRendererWidget::create ); + QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlecolor" ), QgsSingleColorRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "hillshade" ), QgsHillshadeRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "contour" ), QgsRasterContourRendererWidget::create ); diff --git a/src/gui/raster/qgssinglecolorrendererwidget.cpp b/src/gui/raster/qgssinglecolorrendererwidget.cpp new file mode 100644 index 000000000000..f6534e6d1207 --- /dev/null +++ b/src/gui/raster/qgssinglecolorrendererwidget.cpp @@ -0,0 +1,75 @@ +/*************************************************************************** + qgssinglecolorrendererwidget.cpp + --------------------------------- + begin : April 2024 + copyright : (C) 2024 by Mathieu Pellerin + email : mathieu at opengis dot ch + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgssinglecolorrendererwidget.h" +#include "qgssinglecolorrenderer.h" +#include "qgsrasterlayer.h" +#include "qgsrasterdataprovider.h" + +QgsSingleColorRendererWidget::QgsSingleColorRendererWidget( QgsRasterLayer *layer, const QgsRectangle &extent ) + : QgsRasterRendererWidget( layer, extent ) +{ + setupUi( this ); + + if ( mRasterLayer ) + { + QgsRasterDataProvider *provider = mRasterLayer->dataProvider(); + if ( !provider ) + { + return; + } + + connect( mColor, &QgsColorButton::colorChanged, this, &QgsSingleColorRendererWidget::colorChanged ); + + setFromRenderer( layer->renderer() ); + } +} + +QgsRasterRenderer *QgsSingleColorRendererWidget::renderer() +{ + if ( !mRasterLayer ) + { + return nullptr; + } + + QgsRasterDataProvider *provider = mRasterLayer->dataProvider(); + if ( !provider ) + { + return nullptr; + } + + QgsSingleColorRenderer *renderer = new QgsSingleColorRenderer( provider, mColor->color() ); + return renderer; +} + +void QgsSingleColorRendererWidget::colorChanged( const QColor & ) +{ + emit widgetChanged(); +} + +void QgsSingleColorRendererWidget::setFromRenderer( const QgsRasterRenderer *r ) +{ + const QgsSingleColorRenderer *scr = dynamic_cast( r ); + if ( scr ) + { + mColor->setColor( scr->color() ); + } + else + { + mColor->setColor( QColor( 0, 0, 0 ) ); + } +} diff --git a/src/gui/raster/qgssinglecolorrendererwidget.h b/src/gui/raster/qgssinglecolorrendererwidget.h new file mode 100644 index 000000000000..87825357405e --- /dev/null +++ b/src/gui/raster/qgssinglecolorrendererwidget.h @@ -0,0 +1,54 @@ +/*************************************************************************** + qgssinglecolorrendererwidget.h + --------------------------------- + begin : April 2024 + copyright : (C) 2024 by Mathieu Pellerin + email : mathieu at opengis dot ch + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSSINGLECOLORRENDERERWIDGET_H +#define QGSSINGLECOLORRENDERERWIDGET_H + +#include "ui_qgssinglecolorrendererwidgetbase.h" + +#include "qgsrasterrendererwidget.h" +#include "qgis_sip.h" +#include "qgis_gui.h" + +/** + * \brief Renderer widget for the single color renderer. + * \ingroup gui + * \since QGIS 3.38 + */ +class GUI_EXPORT QgsSingleColorRendererWidget: public QgsRasterRendererWidget, private Ui::QgsSingleColorRendererWidgetBase +{ + Q_OBJECT + public: + //! Constructs the widget + QgsSingleColorRendererWidget( QgsRasterLayer *layer, const QgsRectangle &extent = QgsRectangle() ); + + //! Widget creation function (use by the renderer registry) + static QgsRasterRendererWidget *create( QgsRasterLayer *layer, const QgsRectangle &extent ) SIP_FACTORY { return new QgsSingleColorRendererWidget( layer, extent ); } + + QgsRasterRenderer *renderer() SIP_FACTORY override; + + /** + * Sets the widget state from the specified renderer. + */ + void setFromRenderer( const QgsRasterRenderer *r ); + + private slots: + void colorChanged( const QColor &color ); + +}; + +#endif // QGSSINGLECOLORRENDERERWIDGET_H diff --git a/src/ui/qgssinglecolorrendererwidgetbase.ui b/src/ui/qgssinglecolorrendererwidgetbase.ui new file mode 100644 index 000000000000..309fe45fe955 --- /dev/null +++ b/src/ui/qgssinglecolorrendererwidgetbase.ui @@ -0,0 +1,86 @@ + + + QgsSingleColorRendererWidgetBase + + + + 0 + 0 + 395 + 409 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Color + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 16777215 + 16777215 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + QgsColorButton + QToolButton +
qgscolorbutton.h
+
+
+ + +
diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 72140d293c02..ec4624c1149c 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -291,6 +291,7 @@ ADD_PYTHON_TEST(PyQgsSingleBandColorDataRenderer test_qgssinglebandcolordatarend ADD_PYTHON_TEST(PyQgsSingleBandGrayRenderer test_qgssinglebandgrayrenderer.py) ADD_PYTHON_TEST(PyQgsSingleBandPseudoColorRenderer test_qgssinglebandpseudocolorrenderer.py) ADD_PYTHON_TEST(PyQgsSingleSymbolRenderer test_qgssinglesymbolrenderer.py) +ADD_PYTHON_TEST(PyQgsSingleColorRenderer test_qgssinglecolorrenderer.py) ADD_PYTHON_TEST(PyQgsShapefileProvider test_provider_shapefile.py) ADD_PYTHON_TEST(PyQgsSphere test_qgssphere.py) ADD_PYTHON_TEST(PyQgsSvgCache test_qgssvgcache.py) diff --git a/tests/src/python/test_qgssinglecolorrenderer.py b/tests/src/python/test_qgssinglecolorrenderer.py new file mode 100644 index 000000000000..a5f8b88b34fd --- /dev/null +++ b/tests/src/python/test_qgssinglecolorrenderer.py @@ -0,0 +1,59 @@ +"""QGIS Unit tests for QgsSingleColorRenderer. + +.. 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.PyQt.QtGui import QColor +from qgis.core import ( + QgsRasterLayer, + QgsSingleColorRenderer, + QgsMapSettings, +) +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 testRenderer(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 = QgsSingleColorRenderer(layer.dataProvider(), + QColor(255, 0, 0)) + + self.assertEqual(renderer.color(), QColor(255, 0, 0)) + renderer.setColor(QColor(0, 255, 0)) + self.assertEqual(renderer.color(), QColor(0, 255, 0)) + + layer.setRenderer(renderer) + ms = QgsMapSettings() + ms.setLayers([layer]) + ms.setExtent(layer.extent()) + + self.assertTrue( + self.render_map_settings_check( + 'single_color_renderer', + 'single_color_renderer', + ms) + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testdata/control_images/expected_single_color_renderer/expected_single_color_renderer.png b/tests/testdata/control_images/expected_single_color_renderer/expected_single_color_renderer.png new file mode 100644 index 0000000000000000000000000000000000000000..ac13c4815f887c4784a50ee880731f3e45894ca9 GIT binary patch literal 360955 zcmeI&U#MkO9S86;7L@}dN{}=?7$X%lM#Q8=Q8F>9S7RudgyIxJMS=8(Vr;C_gI$UK zKpHp?6AYB+DT5j^>Yw*0T_s?dt0~@y;yK^?%cf-a1UiOj;pWOGQZ=SsHzbij;+Xp^3 zn_czGi~sFAefX+Z&1SEjZ5%s#;?%y`d~><~~`J4k#$fIz7N3aiv) zoQnW~9Rdn#2Z;|F5qRlOPkpkDy8uB0RbD}ZH5P%00tzc)s%9b(R6t<`4c1r$A_^$1 zh^d;1Ku`gN6*O355r`&^T&+rgzz6~gYXnqF5+E>+fWjKb)v5#tj3A(}MnJVB0RrO)D6DZ@txBLZ zfv^1g`g3ahom%}bzS0fkj@c#ck>H35ayTH@v< zP_cl*syIAHC(xRJ!fGvX^JW+L$+gE%O?np~`^4iMKzWVhYE=RRMi5X~BcNK60D*A? z6xKMdRwY1S1ObIL0;(km5Ew^5VU6QzRRRP?5KvenpjwgufpG*B);O+K9YWyoFD(Dy zF2E3yA=gk|A!9WZfuI8O&E-OC1r66&1R@G3tcaRg%vbdV-bibps*sQY9<0f z1r%1$V2wo}qJYAxJ=HgV^yhDP7oheOoj*Z9c_pBEg#dv(0tzdSyIlzoNDxq1320s+ zKp>BR!ph@rR{{hQ1Qb>RnpX%A$RnVz^0?cT0D%O7h6?M*nIE5V7a-v`;uQi#3N%z+ zMfy7w0RlY)6jl!`UnM{wuYkhJ>u+}g1bPT4tR7gtN`OFK0fm*<-|hqm^bk;3J+OS0 z0D-&$s|xFeGjBWKE`s8d3ka+#uNQd5V*&&Stce@fGkRYJ2641OtfIuDrg_XzMt^}qRIQQFgrvu#uP+rsjgzQLwK%#)cN`&(o z0RkBW=9|lf*2(~HO9BMa1Qb@9o3{uM$RMDwGN9X%0D&|Cg_Y*!Edm5G2q>%!=(a3a z;HjJ6@@$~H00m30eKh5@HlYOw5SU6pVNE4%9|8o{7EoAg6Iy@(fvE%()>P8=AwXbl z0fn_Tp#=yKm`XrlO(ks~0tD6;P*`gdwz|NR?|J1@?gF%$ehG6cuM$#l1_A{3B%rYN z#NSPfWf}6jfe{GkA>vfkp%rRwF?hlRyZ8 z`Q~z=wL(N`2m);gD6BSeHYI@&0tzccjD{f4hJeCqBWF_*$RqH_e>{DDq`LshD^H|$ zB|soSKw%}Id4&LhJOTX5(E@h0-9F{5Xd8- zu=2RumB3{J55N41XGXdUaM?~#{;Is9Bxw!;4G1W#20}I>fhYnBD@u~)Akct-!fGI7 zBNB)rps=DOX$}Gn2q>%uLN+3SC;|#AN|No&aqZQg`kA``?d07#sq*Ti@qhq<(gYM% zX^A)w0Ro)@3agXG0|Ep}6Hr*CCE`2;2y_Z4tWFva2oNYuKw*`Zi1QF2&?%s>I%(>E zaL<`Ho^ThS{v1uPT0nWNcJV0z0tE^vtO7%EBmx9h3n;ABEcgB5(N}iiK#df0Rk%pnlG&V*Zlp6y8tVH8$KjZv_SLaRdhNIM}WXK z0fn`V!(RvxC|N*Zm7I>V5g@QlKw)j;@D~CEN)}L9C8y(T1PE*sP*~eI{DlC4k_Gl& zSYLf&^ImrWO3u>R2s92oP8)ps-f5_>cgB5(N}iiK#df z0Rk%p6xK=>9}*x?qJY9GF%@SbFrC1kA36W)D(?a)ujzgyb|OGvO#y|qCZN{|5SUIt zVNGXkCjtc46i`@e0(zYQf$0Pk)^yf(B0ykG0fn_Dpw|fym`*@pO=oSV$_0LO-TS^% zRY50;LHktkM#39s&e91r$~%jRz?LuleD|VRr#iz5)uXuh>xEzvJL@ z54sC5l%{nW6i{9bhHhj6wF)S#T9b2b0u2f%tOi3jGJ#qJ6jrUtIX8g@1r%0;p&OY% ztpW6jnoF8Ap}JMLWPE`ahXDNSb~Kwwt^ zg|#b>hXe?eB%rWLO2b(Q5ZF~fVeQJ}Aprs<2`H?R(r^|61a=pgZ!Q;FYj-102@ohq zKw%XWhNBR;T;RTkZhq4`cL6S6FVf$YSEMw}LLj(+!U`U+@d!i`P*{=DGz)>?0tzd5 zz{VpGNkCymO4BR^f(t0D-~k(tKqLW$6)DY@XSwmn-OsuU(9|zLc{LTcVF}bNps?!B z&*=#?C7`gHircUR>K0H~b?4{w1ey|1SWU%kSORqmD6G2kb9w?z2`H?l;=VZS1AqG9 zx7`J3F7|)}E3bf|8i_zu0fiMcS#uExD4?(chH4}NQ3VuM)MU*?AfSN43K*)92t*Z7 zSW%NT7lD8R3adNR??3d38{Gxy_VI*3RsrRemEYzB2=oz9Sbem7mjHpR0tzcDzs(5{ z=p&%8`e^ws0RmYC6joM#n-d_=OJKgaTxhLcWWG#*Ku&>m3+u7R9{-oS067!1H-Sb4 z)-A6_*^EtqKn?+gl|$X01PCMvD6AwlFA*S+LqK8WP`4)m0!ac2D~ZiZ1PJ61P*^$C z?Ma|?fpcdMoi6+?fbuH+N92422y6)`tSt(EAV8pW0fkk1Le58kz?Oi*+M@6W0t8AI zP*|lWlqR6CN=w9f2oUHL zP*|Nb9uOcI9Tmb&)wRfp!HH zR=e4ooIrH~3ah%v9GF160t&0$>`hLfIst`MU1SbSpj`ol)o%7CCs3V0wT1PHt3UoN zcLA#Vy*V&}!33%;ufYT?M1Vjw0t%~|s2r340fpr^K!89s0t%~|s2r340fpr^K!89s z0t%~|s2r3)3j(h_`-X@2b{9Z-weWi|BY_|S3M)vE#vsswfWm4aWit{8BA~E>1ZfNc zEeI&A7E(4Nfgl13D@c&WAkczs503Mj0I zshWvEPyvM%G+1L1h$x`2BBp940zm~7R?uLLMIfSp!it!xnFs_GP*_2OHP-V2Uw{3R z_cVPM;CYHrf7};?*uvH)KwvNdg*6zZg$NKBTR>rrEo^-P1O^jO vSc6eohya1H1r*lU!qz81U@(D4KmFpn{`tFa+;nlljdveE_Jeob{pJ4wbK0|# literal 0 HcmV?d00001 From 6da9741a3f30f0f2652249bbd627cd13a6921096 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 7 Apr 2024 16:29:28 +0700 Subject: [PATCH 70/87] Try a different raster, weird inconsistent behavior on CI vs. local --- tests/src/python/test_qgssinglecolorrenderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/python/test_qgssinglecolorrenderer.py b/tests/src/python/test_qgssinglecolorrenderer.py index a5f8b88b34fd..55c15fee7b42 100644 --- a/tests/src/python/test_qgssinglecolorrenderer.py +++ b/tests/src/python/test_qgssinglecolorrenderer.py @@ -29,7 +29,7 @@ class TestQgsSingleBandGrayRenderer(QgisTestCase): def testRenderer(self): path = os.path.join(unitTestDataPath(), - 'landsat.tif') + 'landsat-int16-b1.tif') info = QFileInfo(path) base_name = info.baseName() layer = QgsRasterLayer(path, base_name) From 4bd2e66b3f81f0ef48e078421c9397b0d8ab605f Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Mon, 8 Apr 2024 09:02:21 +0700 Subject: [PATCH 71/87] Rename classes --- .../raster/qgsrasterinterface.sip.in | 6 ++-- ...in => qgsrastersinglecolorrenderer.sip.in} | 18 ++++++------ python/PyQt6/core/core_auto.sip | 2 +- ...qgsrastersinglecolorrendererwidget.sip.in} | 10 +++---- python/PyQt6/gui/gui_auto.sip | 2 +- .../raster/qgsrasterinterface.sip.in | 6 ++-- ...in => qgsrastersinglecolorrenderer.sip.in} | 18 ++++++------ python/core/core_auto.sip | 2 +- ...qgsrastersinglecolorrendererwidget.sip.in} | 10 +++---- python/gui/gui_auto.sip | 2 +- src/core/CMakeLists.txt | 4 +-- src/core/raster/qgsrasterinterface.h | 6 ++-- src/core/raster/qgsrasterrendererregistry.cpp | 4 +-- ...r.cpp => qgsrastersinglecolorrenderer.cpp} | 26 +++++++++--------- ...derer.h => qgsrastersinglecolorrenderer.h} | 26 +++++++++--------- src/gui/CMakeLists.txt | 4 +-- src/gui/raster/qgsrasterlayerproperties.cpp | 4 +-- ...=> qgsrastersinglecolorrendererwidget.cpp} | 20 +++++++------- ...h => qgsrastersinglecolorrendererwidget.h} | 16 +++++------ .../qgsrendererrasterpropertieswidget.cpp | 4 +-- ...qgsrastersinglecolorrendererwidgetbase.ui} | 4 +-- tests/src/python/CMakeLists.txt | 2 +- ...y => test_qgsrastersinglecolorrenderer.py} | 12 ++++---- ...expected_raster_single_color_renderer.png} | Bin 24 files changed, 104 insertions(+), 104 deletions(-) rename python/PyQt6/core/auto_generated/raster/{qgssinglecolorrenderer.sip.in => qgsrastersinglecolorrenderer.sip.in} (74%) rename python/PyQt6/gui/auto_generated/raster/{qgssinglecolorrendererwidget.sip.in => qgsrastersinglecolorrendererwidget.sip.in} (80%) rename python/core/auto_generated/raster/{qgssinglecolorrenderer.sip.in => qgsrastersinglecolorrenderer.sip.in} (74%) rename python/gui/auto_generated/raster/{qgssinglecolorrendererwidget.sip.in => qgsrastersinglecolorrendererwidget.sip.in} (80%) rename src/core/raster/{qgssinglecolorrenderer.cpp => qgsrastersinglecolorrenderer.cpp} (79%) rename src/core/raster/{qgssinglecolorrenderer.h => qgsrastersinglecolorrenderer.h} (69%) rename src/gui/raster/{qgssinglecolorrendererwidget.cpp => qgsrastersinglecolorrendererwidget.cpp} (66%) rename src/gui/raster/{qgssinglecolorrendererwidget.h => qgsrastersinglecolorrendererwidget.h} (72%) rename src/ui/{qgssinglecolorrendererwidgetbase.ui => qgsrastersinglecolorrendererwidgetbase.ui} (94%) rename tests/src/python/{test_qgssinglecolorrenderer.py => test_qgsrastersinglecolorrenderer.py} (82%) rename tests/testdata/control_images/{expected_single_color_renderer/expected_single_color_renderer.png => expected_raster_single_color_renderer/expected_raster_single_color_renderer.png} (100%) diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterinterface.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterinterface.sip.in index d92205e66396..92a28eb00b2e 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterinterface.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterinterface.sip.in @@ -125,7 +125,7 @@ Base class for processing filters like renderers, reprojector, resampler etc. #include #include #include -#include +#include #include %End %ConvertToSubClassCode @@ -158,8 +158,8 @@ Base class for processing filters like renderers, reprojector, resampler etc. sipType = sipType_QgsSingleBandGrayRenderer; else if ( dynamic_cast( sipCpp ) ) sipType = sipType_QgsSingleBandPseudoColorRenderer; - else if ( dynamic_cast( sipCpp ) ) - sipType = sipType_QgsSingleColorRenderer; + else if ( dynamic_cast( sipCpp ) ) + sipType = sipType_QgsRasterSingleColorRenderer; else if ( dynamic_cast( sipCpp ) ) sipType = sipType_QgsRasterContourRenderer; else diff --git a/python/PyQt6/core/auto_generated/raster/qgssinglecolorrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in similarity index 74% rename from python/PyQt6/core/auto_generated/raster/qgssinglecolorrenderer.sip.in rename to python/PyQt6/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in index 55f654b4c6ff..fa3084087dce 100644 --- a/python/PyQt6/core/auto_generated/raster/qgssinglecolorrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in @@ -1,7 +1,7 @@ /************************************************************************ * This file has been generated automatically from * * * - * src/core/raster/qgssinglecolorrenderer.h * + * src/core/raster/qgsrastersinglecolorrenderer.h * * * * Do not edit manually ! Edit header and run scripts/sipify.pl again * ************************************************************************/ @@ -11,7 +11,7 @@ -class QgsSingleColorRenderer: QgsRasterRenderer +class QgsRasterSingleColorRenderer: QgsRasterRenderer { %Docstring(signature="appended") Raster single color renderer pipe. @@ -20,20 +20,20 @@ Raster single color renderer pipe. %End %TypeHeaderCode -#include "qgssinglecolorrenderer.h" +#include "qgsrastersinglecolorrenderer.h" %End public: - QgsSingleColorRenderer( QgsRasterInterface *input, QColor color ); + QgsRasterSingleColorRenderer( QgsRasterInterface *input, QColor color ); %Docstring Creates a single ``color`` renderer %End - virtual QgsSingleColorRenderer *clone() const /Factory/; + virtual QgsRasterSingleColorRenderer *clone() const /Factory/; %Docstring -QgsSingleColorRenderer cannot be copied. Use :py:func:`~QgsSingleColorRenderer.clone` instead. +QgsRasterSingleColorRenderer cannot be copied. Use :py:func:`~QgsRasterSingleColorRenderer.clone` instead. %End virtual Qgis::RasterRendererFlags flags() const; @@ -63,14 +63,14 @@ Sets the single color used by the renderer. private: - QgsSingleColorRenderer( const QgsSingleColorRenderer & ); - const QgsSingleColorRenderer &operator=( const QgsSingleColorRenderer & ); + QgsRasterSingleColorRenderer( const QgsRasterSingleColorRenderer & ); + const QgsRasterSingleColorRenderer &operator=( const QgsRasterSingleColorRenderer & ); }; /************************************************************************ * This file has been generated automatically from * * * - * src/core/raster/qgssinglecolorrenderer.h * + * src/core/raster/qgsrastersinglecolorrenderer.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 19db988c55c8..1127fa0d7f2a 100644 --- a/python/PyQt6/core/core_auto.sip +++ b/python/PyQt6/core/core_auto.sip @@ -654,7 +654,7 @@ %Include auto_generated/raster/qgssinglebandcolordatarenderer.sip %Include auto_generated/raster/qgssinglebandgrayrenderer.sip %Include auto_generated/raster/qgssinglebandpseudocolorrenderer.sip -%Include auto_generated/raster/qgssinglecolorrenderer.sip +%Include auto_generated/raster/qgsrastersinglecolorrenderer.sip %Include auto_generated/scalebar/qgsdoubleboxscalebarrenderer.sip %Include auto_generated/scalebar/qgshollowscalebarrenderer.sip %Include auto_generated/scalebar/qgsnumericscalebarrenderer.sip diff --git a/python/PyQt6/gui/auto_generated/raster/qgssinglecolorrendererwidget.sip.in b/python/PyQt6/gui/auto_generated/raster/qgsrastersinglecolorrendererwidget.sip.in similarity index 80% rename from python/PyQt6/gui/auto_generated/raster/qgssinglecolorrendererwidget.sip.in rename to python/PyQt6/gui/auto_generated/raster/qgsrastersinglecolorrendererwidget.sip.in index 82150e985267..f9077b14c6e2 100644 --- a/python/PyQt6/gui/auto_generated/raster/qgssinglecolorrendererwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/raster/qgsrastersinglecolorrendererwidget.sip.in @@ -1,7 +1,7 @@ /************************************************************************ * This file has been generated automatically from * * * - * src/gui/raster/qgssinglecolorrendererwidget.h * + * src/gui/raster/qgsrastersinglecolorrendererwidget.h * * * * Do not edit manually ! Edit header and run scripts/sipify.pl again * ************************************************************************/ @@ -10,7 +10,7 @@ -class QgsSingleColorRendererWidget: QgsRasterRendererWidget +class QgsRasterSingleColorRendererWidget: QgsRasterRendererWidget { %Docstring(signature="appended") Renderer widget for the single color renderer. @@ -19,10 +19,10 @@ Renderer widget for the single color renderer. %End %TypeHeaderCode -#include "qgssinglecolorrendererwidget.h" +#include "qgsrastersinglecolorrendererwidget.h" %End public: - QgsSingleColorRendererWidget( QgsRasterLayer *layer, const QgsRectangle &extent = QgsRectangle() ); + QgsRasterSingleColorRendererWidget( QgsRasterLayer *layer, const QgsRectangle &extent = QgsRectangle() ); %Docstring Constructs the widget %End @@ -45,7 +45,7 @@ Sets the widget state from the specified renderer. /************************************************************************ * This file has been generated automatically from * * * - * src/gui/raster/qgssinglecolorrendererwidget.h * + * src/gui/raster/qgsrastersinglecolorrendererwidget.h * * * * Do not edit manually ! Edit header and run scripts/sipify.pl again * ************************************************************************/ diff --git a/python/PyQt6/gui/gui_auto.sip b/python/PyQt6/gui/gui_auto.sip index 4efdc2a58934..e798e33d1fa4 100644 --- a/python/PyQt6/gui/gui_auto.sip +++ b/python/PyQt6/gui/gui_auto.sip @@ -461,7 +461,7 @@ %Include auto_generated/raster/qgsrendererrasterpropertieswidget.sip %Include auto_generated/raster/qgssinglebandgrayrendererwidget.sip %Include auto_generated/raster/qgssinglebandpseudocolorrendererwidget.sip -%Include auto_generated/raster/qgssinglecolorrendererwidget.sip +%Include auto_generated/raster/qgsrastersinglecolorrendererwidget.sip %Include auto_generated/raster/qgsrasterlayerproperties.sip %Include auto_generated/raster/qgsrasterlayertemporalpropertieswidget.sip %Include auto_generated/vector/qgsfieldcalculator.sip diff --git a/python/core/auto_generated/raster/qgsrasterinterface.sip.in b/python/core/auto_generated/raster/qgsrasterinterface.sip.in index f8fdbec251aa..f1d62b2aea28 100644 --- a/python/core/auto_generated/raster/qgsrasterinterface.sip.in +++ b/python/core/auto_generated/raster/qgsrasterinterface.sip.in @@ -125,7 +125,7 @@ Base class for processing filters like renderers, reprojector, resampler etc. #include #include #include -#include +#include #include %End %ConvertToSubClassCode @@ -158,8 +158,8 @@ Base class for processing filters like renderers, reprojector, resampler etc. sipType = sipType_QgsSingleBandGrayRenderer; else if ( dynamic_cast( sipCpp ) ) sipType = sipType_QgsSingleBandPseudoColorRenderer; - else if ( dynamic_cast( sipCpp ) ) - sipType = sipType_QgsSingleColorRenderer; + else if ( dynamic_cast( sipCpp ) ) + sipType = sipType_QgsRasterSingleColorRenderer; else if ( dynamic_cast( sipCpp ) ) sipType = sipType_QgsRasterContourRenderer; else diff --git a/python/core/auto_generated/raster/qgssinglecolorrenderer.sip.in b/python/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in similarity index 74% rename from python/core/auto_generated/raster/qgssinglecolorrenderer.sip.in rename to python/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in index 55f654b4c6ff..fa3084087dce 100644 --- a/python/core/auto_generated/raster/qgssinglecolorrenderer.sip.in +++ b/python/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in @@ -1,7 +1,7 @@ /************************************************************************ * This file has been generated automatically from * * * - * src/core/raster/qgssinglecolorrenderer.h * + * src/core/raster/qgsrastersinglecolorrenderer.h * * * * Do not edit manually ! Edit header and run scripts/sipify.pl again * ************************************************************************/ @@ -11,7 +11,7 @@ -class QgsSingleColorRenderer: QgsRasterRenderer +class QgsRasterSingleColorRenderer: QgsRasterRenderer { %Docstring(signature="appended") Raster single color renderer pipe. @@ -20,20 +20,20 @@ Raster single color renderer pipe. %End %TypeHeaderCode -#include "qgssinglecolorrenderer.h" +#include "qgsrastersinglecolorrenderer.h" %End public: - QgsSingleColorRenderer( QgsRasterInterface *input, QColor color ); + QgsRasterSingleColorRenderer( QgsRasterInterface *input, QColor color ); %Docstring Creates a single ``color`` renderer %End - virtual QgsSingleColorRenderer *clone() const /Factory/; + virtual QgsRasterSingleColorRenderer *clone() const /Factory/; %Docstring -QgsSingleColorRenderer cannot be copied. Use :py:func:`~QgsSingleColorRenderer.clone` instead. +QgsRasterSingleColorRenderer cannot be copied. Use :py:func:`~QgsRasterSingleColorRenderer.clone` instead. %End virtual Qgis::RasterRendererFlags flags() const; @@ -63,14 +63,14 @@ Sets the single color used by the renderer. private: - QgsSingleColorRenderer( const QgsSingleColorRenderer & ); - const QgsSingleColorRenderer &operator=( const QgsSingleColorRenderer & ); + QgsRasterSingleColorRenderer( const QgsRasterSingleColorRenderer & ); + const QgsRasterSingleColorRenderer &operator=( const QgsRasterSingleColorRenderer & ); }; /************************************************************************ * This file has been generated automatically from * * * - * src/core/raster/qgssinglecolorrenderer.h * + * src/core/raster/qgsrastersinglecolorrenderer.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 19db988c55c8..1127fa0d7f2a 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -654,7 +654,7 @@ %Include auto_generated/raster/qgssinglebandcolordatarenderer.sip %Include auto_generated/raster/qgssinglebandgrayrenderer.sip %Include auto_generated/raster/qgssinglebandpseudocolorrenderer.sip -%Include auto_generated/raster/qgssinglecolorrenderer.sip +%Include auto_generated/raster/qgsrastersinglecolorrenderer.sip %Include auto_generated/scalebar/qgsdoubleboxscalebarrenderer.sip %Include auto_generated/scalebar/qgshollowscalebarrenderer.sip %Include auto_generated/scalebar/qgsnumericscalebarrenderer.sip diff --git a/python/gui/auto_generated/raster/qgssinglecolorrendererwidget.sip.in b/python/gui/auto_generated/raster/qgsrastersinglecolorrendererwidget.sip.in similarity index 80% rename from python/gui/auto_generated/raster/qgssinglecolorrendererwidget.sip.in rename to python/gui/auto_generated/raster/qgsrastersinglecolorrendererwidget.sip.in index 82150e985267..f9077b14c6e2 100644 --- a/python/gui/auto_generated/raster/qgssinglecolorrendererwidget.sip.in +++ b/python/gui/auto_generated/raster/qgsrastersinglecolorrendererwidget.sip.in @@ -1,7 +1,7 @@ /************************************************************************ * This file has been generated automatically from * * * - * src/gui/raster/qgssinglecolorrendererwidget.h * + * src/gui/raster/qgsrastersinglecolorrendererwidget.h * * * * Do not edit manually ! Edit header and run scripts/sipify.pl again * ************************************************************************/ @@ -10,7 +10,7 @@ -class QgsSingleColorRendererWidget: QgsRasterRendererWidget +class QgsRasterSingleColorRendererWidget: QgsRasterRendererWidget { %Docstring(signature="appended") Renderer widget for the single color renderer. @@ -19,10 +19,10 @@ Renderer widget for the single color renderer. %End %TypeHeaderCode -#include "qgssinglecolorrendererwidget.h" +#include "qgsrastersinglecolorrendererwidget.h" %End public: - QgsSingleColorRendererWidget( QgsRasterLayer *layer, const QgsRectangle &extent = QgsRectangle() ); + QgsRasterSingleColorRendererWidget( QgsRasterLayer *layer, const QgsRectangle &extent = QgsRectangle() ); %Docstring Constructs the widget %End @@ -45,7 +45,7 @@ Sets the widget state from the specified renderer. /************************************************************************ * This file has been generated automatically from * * * - * src/gui/raster/qgssinglecolorrendererwidget.h * + * src/gui/raster/qgsrastersinglecolorrendererwidget.h * * * * Do not edit manually ! Edit header and run scripts/sipify.pl again * ************************************************************************/ diff --git a/python/gui/gui_auto.sip b/python/gui/gui_auto.sip index 4efdc2a58934..e798e33d1fa4 100644 --- a/python/gui/gui_auto.sip +++ b/python/gui/gui_auto.sip @@ -461,7 +461,7 @@ %Include auto_generated/raster/qgsrendererrasterpropertieswidget.sip %Include auto_generated/raster/qgssinglebandgrayrendererwidget.sip %Include auto_generated/raster/qgssinglebandpseudocolorrendererwidget.sip -%Include auto_generated/raster/qgssinglecolorrendererwidget.sip +%Include auto_generated/raster/qgsrastersinglecolorrendererwidget.sip %Include auto_generated/raster/qgsrasterlayerproperties.sip %Include auto_generated/raster/qgsrasterlayertemporalpropertieswidget.sip %Include auto_generated/vector/qgsfieldcalculator.sip diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 66a03c0a6ffc..85cbb80ae300 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -784,7 +784,7 @@ set(QGIS_CORE_SRCS raster/qgssinglebandcolordatarenderer.cpp raster/qgssinglebandgrayrenderer.cpp raster/qgssinglebandpseudocolorrenderer.cpp - raster/qgssinglecolorrenderer.cpp + raster/qgsrastersinglecolorrenderer.cpp raster/qgshillshaderenderer.cpp mesh/qgsmesh3daveraging.cpp @@ -1882,7 +1882,7 @@ set(QGIS_CORE_HDRS raster/qgssinglebandcolordatarenderer.h raster/qgssinglebandgrayrenderer.h raster/qgssinglebandpseudocolorrenderer.h - raster/qgssinglecolorrenderer.h + raster/qgsrastersinglecolorrenderer.h scalebar/qgsdoubleboxscalebarrenderer.h scalebar/qgshollowscalebarrenderer.h diff --git a/src/core/raster/qgsrasterinterface.h b/src/core/raster/qgsrasterinterface.h index 479d3bf7f271..ff4b2f2ddeba 100644 --- a/src/core/raster/qgsrasterinterface.h +++ b/src/core/raster/qgsrasterinterface.h @@ -151,7 +151,7 @@ class CORE_EXPORT QgsRasterInterface #include #include #include -#include +#include #include #endif @@ -187,8 +187,8 @@ class CORE_EXPORT QgsRasterInterface sipType = sipType_QgsSingleBandGrayRenderer; else if ( dynamic_cast( sipCpp ) ) sipType = sipType_QgsSingleBandPseudoColorRenderer; - else if ( dynamic_cast( sipCpp ) ) - sipType = sipType_QgsSingleColorRenderer; + else if ( dynamic_cast( sipCpp ) ) + sipType = sipType_QgsRasterSingleColorRenderer; else if ( dynamic_cast( sipCpp ) ) sipType = sipType_QgsRasterContourRenderer; else diff --git a/src/core/raster/qgsrasterrendererregistry.cpp b/src/core/raster/qgsrasterrendererregistry.cpp index d840f6db65a0..b905caccf6ea 100644 --- a/src/core/raster/qgsrasterrendererregistry.cpp +++ b/src/core/raster/qgsrasterrendererregistry.cpp @@ -26,7 +26,7 @@ #include "qgssinglebandcolordatarenderer.h" #include "qgssinglebandgrayrenderer.h" #include "qgssinglebandpseudocolorrenderer.h" -#include "qgssinglecolorrenderer.h" +#include "qgsrastersinglecolorrenderer.h" #include "qgshillshaderenderer.h" #include "qgsapplication.h" #include "qgssettings.h" @@ -64,7 +64,7 @@ QgsRasterRendererRegistry::QgsRasterRendererRegistry() insert( QgsRasterRendererRegistryEntry( QStringLiteral( "singlebandcolordata" ), QObject::tr( "Singleband color data" ), QgsSingleBandColorDataRenderer::create, nullptr ) ); insert( QgsRasterRendererRegistryEntry( QStringLiteral( "singlecolor" ), QObject::tr( "Single color" ), - QgsSingleColorRenderer::create, nullptr ) ); + QgsRasterSingleColorRenderer::create, nullptr ) ); insert( QgsRasterRendererRegistryEntry( QStringLiteral( "hillshade" ), QObject::tr( "Hillshade" ), QgsHillshadeRenderer::create, nullptr ) ); insert( QgsRasterRendererRegistryEntry( QStringLiteral( "contour" ), QObject::tr( "Contours" ), diff --git a/src/core/raster/qgssinglecolorrenderer.cpp b/src/core/raster/qgsrastersinglecolorrenderer.cpp similarity index 79% rename from src/core/raster/qgssinglecolorrenderer.cpp rename to src/core/raster/qgsrastersinglecolorrenderer.cpp index 3103875e7815..7237dc9b35de 100644 --- a/src/core/raster/qgssinglecolorrenderer.cpp +++ b/src/core/raster/qgsrastersinglecolorrenderer.cpp @@ -1,5 +1,5 @@ /*************************************************************************** - qgssinglecolorrenderer.cpp + qgsrastersinglecolorrenderer.cpp ----------------------------- begin : April 2024 copyright : (C) 2024 by Mathieu Pellerin @@ -15,32 +15,32 @@ * * ***************************************************************************/ -#include "qgssinglecolorrenderer.h" +#include "qgsrastersinglecolorrenderer.h" #include "qgsrastertransparency.h" #include "qgscolorutils.h" #include #include -QgsSingleColorRenderer::QgsSingleColorRenderer( QgsRasterInterface *input, QColor color ) +QgsRasterSingleColorRenderer::QgsRasterSingleColorRenderer( QgsRasterInterface *input, QColor color ) : QgsRasterRenderer( input, QStringLiteral( "singlecolor" ) ) , mColor( color ) { } -QgsSingleColorRenderer *QgsSingleColorRenderer::clone() const +QgsRasterSingleColorRenderer *QgsRasterSingleColorRenderer::clone() const { - QgsSingleColorRenderer *renderer = new QgsSingleColorRenderer( nullptr, mColor ); + QgsRasterSingleColorRenderer *renderer = new QgsRasterSingleColorRenderer( nullptr, mColor ); renderer->copyCommonProperties( this ); return renderer; } -Qgis::RasterRendererFlags QgsSingleColorRenderer::flags() const +Qgis::RasterRendererFlags QgsRasterSingleColorRenderer::flags() const { return Qgis::RasterRendererFlag::InternalLayerOpacityHandling; } -QgsRasterRenderer *QgsSingleColorRenderer::create( const QDomElement &elem, QgsRasterInterface *input ) +QgsRasterRenderer *QgsRasterSingleColorRenderer::create( const QDomElement &elem, QgsRasterInterface *input ) { if ( elem.isNull() ) { @@ -48,13 +48,13 @@ QgsRasterRenderer *QgsSingleColorRenderer::create( const QDomElement &elem, QgsR } const QColor color = QgsColorUtils::colorFromString( elem.attribute( QStringLiteral( "color" ), QStringLiteral( "0,0,0" ) ) ); - QgsSingleColorRenderer *r = new QgsSingleColorRenderer( input, color ); + QgsRasterSingleColorRenderer *r = new QgsRasterSingleColorRenderer( input, color ); r->readXml( elem ); return r; } -QgsRasterBlock *QgsSingleColorRenderer::block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback ) +QgsRasterBlock *QgsRasterSingleColorRenderer::block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback ) { QgsDebugMsgLevel( QStringLiteral( "width = %1 height = %2" ).arg( width ).arg( height ), 4 ); @@ -128,17 +128,17 @@ QgsRasterBlock *QgsSingleColorRenderer::block( int bandNo, const QgsRectangle &e return outputBlock.release(); } -void QgsSingleColorRenderer::setColor( QColor &color ) +void QgsRasterSingleColorRenderer::setColor( QColor &color ) { mColor = color;; } -QColor QgsSingleColorRenderer::color() const +QColor QgsRasterSingleColorRenderer::color() const { return mColor; } -void QgsSingleColorRenderer::writeXml( QDomDocument &doc, QDomElement &parentElem ) const +void QgsRasterSingleColorRenderer::writeXml( QDomDocument &doc, QDomElement &parentElem ) const { if ( parentElem.isNull() ) { @@ -153,7 +153,7 @@ void QgsSingleColorRenderer::writeXml( QDomDocument &doc, QDomElement &parentEle parentElem.appendChild( rasterRendererElem ); } -QList QgsSingleColorRenderer::usesBands() const +QList QgsRasterSingleColorRenderer::usesBands() const { QList bandList; for ( int i = 0; i <= bandCount(); i++ ) diff --git a/src/core/raster/qgssinglecolorrenderer.h b/src/core/raster/qgsrastersinglecolorrenderer.h similarity index 69% rename from src/core/raster/qgssinglecolorrenderer.h rename to src/core/raster/qgsrastersinglecolorrenderer.h index f0e7d1ede31e..9dfc653e40a9 100644 --- a/src/core/raster/qgssinglecolorrenderer.h +++ b/src/core/raster/qgsrastersinglecolorrenderer.h @@ -1,5 +1,5 @@ /*************************************************************************** - qgssinglecolorrenderer.h + qgsrastersinglecolorrenderer.h --------------------------- begin : April 2024 copyright : (C) 2024 by Mathieu Pellerin @@ -15,8 +15,8 @@ * * ***************************************************************************/ -#ifndef QGSSINGLECOLORRENDERER_H -#define QGSSINGLECOLORRENDERER_H +#ifndef QGSRASTERSINGLECOLORRENDERER_H +#define QGSRASTERSINGLECOLORRENDERER_H #include "qgis_core.h" #include "qgis_sip.h" @@ -31,19 +31,19 @@ class QDomElement; * \brief Raster single color renderer pipe. * \since QGIS 3.38 */ -class CORE_EXPORT QgsSingleColorRenderer: public QgsRasterRenderer +class CORE_EXPORT QgsRasterSingleColorRenderer: public QgsRasterRenderer { public: //! Creates a single \a color renderer - QgsSingleColorRenderer( QgsRasterInterface *input, QColor color ); + QgsRasterSingleColorRenderer( QgsRasterInterface *input, QColor color ); - //! QgsSingleColorRenderer cannot be copied. Use clone() instead. - QgsSingleColorRenderer( const QgsSingleColorRenderer & ) = delete; - //! QgsSingleColorRenderer cannot be copied. Use clone() instead. - const QgsSingleColorRenderer &operator=( const QgsSingleColorRenderer & ) = delete; + //! QgsRasterSingleColorRenderer cannot be copied. Use clone() instead. + QgsRasterSingleColorRenderer( const QgsRasterSingleColorRenderer & ) = delete; + //! QgsRasterSingleColorRenderer cannot be copied. Use clone() instead. + const QgsRasterSingleColorRenderer &operator=( const QgsRasterSingleColorRenderer & ) = delete; - QgsSingleColorRenderer *clone() const override SIP_FACTORY; + QgsRasterSingleColorRenderer *clone() const override SIP_FACTORY; Qgis::RasterRendererFlags flags() const override; //! Creates an instance of the renderer based on definition from XML (used by the renderer registry) @@ -67,11 +67,11 @@ class CORE_EXPORT QgsSingleColorRenderer: public QgsRasterRenderer private: #ifdef SIP_RUN - QgsSingleColorRenderer( const QgsSingleColorRenderer & ); - const QgsSingleColorRenderer &operator=( const QgsSingleColorRenderer & ); + QgsRasterSingleColorRenderer( const QgsRasterSingleColorRenderer & ); + const QgsRasterSingleColorRenderer &operator=( const QgsRasterSingleColorRenderer & ); #endif QColor mColor; }; -#endif // QGSSINGLECOLORRENDERER_H +#endif // QGSRASTERSINGLECOLORRENDERER_H diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 1fe88959352c..89df2a440e22 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -24,7 +24,7 @@ set(QGIS_GUI_SRCS raster/qgsrasterrendererwidget.cpp raster/qgssinglebandgrayrendererwidget.cpp raster/qgssinglebandpseudocolorrendererwidget.cpp - raster/qgssinglecolorrendererwidget.cpp + raster/qgsrastersinglecolorrendererwidget.cpp raster/qgsrendererrasterpropertieswidget.cpp raster/qgsrastertransparencywidget.cpp raster/qgshillshaderendererwidget.cpp @@ -1457,7 +1457,7 @@ set(QGIS_GUI_HDRS raster/qgsrendererrasterpropertieswidget.h raster/qgssinglebandgrayrendererwidget.h raster/qgssinglebandpseudocolorrendererwidget.h - raster/qgssinglecolorrendererwidget.h + raster/qgsrastersinglecolorrendererwidget.h raster/qgsrasterlayerproperties.h raster/qgsrasterlayertemporalpropertieswidget.h raster/qgsresamplingutils.h diff --git a/src/gui/raster/qgsrasterlayerproperties.cpp b/src/gui/raster/qgsrasterlayerproperties.cpp index edea0efb40d9..c27bf6e03d3c 100644 --- a/src/gui/raster/qgsrasterlayerproperties.cpp +++ b/src/gui/raster/qgsrasterlayerproperties.cpp @@ -49,7 +49,7 @@ #include "qgsrastertransparency.h" #include "qgssinglebandgrayrendererwidget.h" #include "qgssinglebandpseudocolorrendererwidget.h" -#include "qgssinglecolorrendererwidget.h" +#include "qgsrastersinglecolorrendererwidget.h" #include "qgshuesaturationfilter.h" #include "qgshillshaderendererwidget.h" #include "qgssettings.h" @@ -463,7 +463,7 @@ QgsRasterLayerProperties::QgsRasterLayerProperties( QgsMapLayer *lyr, QgsMapCanv QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlebandgray" ), QgsSingleBandGrayRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "hillshade" ), QgsHillshadeRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "contour" ), QgsRasterContourRendererWidget::create ); - QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlecolor" ), QgsSingleColorRendererWidget::create ); + QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlecolor" ), QgsRasterSingleColorRendererWidget::create ); //fill available renderers into combo box QgsRasterRendererRegistryEntry entry; diff --git a/src/gui/raster/qgssinglecolorrendererwidget.cpp b/src/gui/raster/qgsrastersinglecolorrendererwidget.cpp similarity index 66% rename from src/gui/raster/qgssinglecolorrendererwidget.cpp rename to src/gui/raster/qgsrastersinglecolorrendererwidget.cpp index f6534e6d1207..333f3f384804 100644 --- a/src/gui/raster/qgssinglecolorrendererwidget.cpp +++ b/src/gui/raster/qgsrastersinglecolorrendererwidget.cpp @@ -1,5 +1,5 @@ /*************************************************************************** - qgssinglecolorrendererwidget.cpp + qgsrastersinglecolorrendererwidget.cpp --------------------------------- begin : April 2024 copyright : (C) 2024 by Mathieu Pellerin @@ -15,12 +15,12 @@ * * ***************************************************************************/ -#include "qgssinglecolorrendererwidget.h" -#include "qgssinglecolorrenderer.h" +#include "qgsrastersinglecolorrendererwidget.h" +#include "qgsrastersinglecolorrenderer.h" #include "qgsrasterlayer.h" #include "qgsrasterdataprovider.h" -QgsSingleColorRendererWidget::QgsSingleColorRendererWidget( QgsRasterLayer *layer, const QgsRectangle &extent ) +QgsRasterSingleColorRendererWidget::QgsRasterSingleColorRendererWidget( QgsRasterLayer *layer, const QgsRectangle &extent ) : QgsRasterRendererWidget( layer, extent ) { setupUi( this ); @@ -33,13 +33,13 @@ QgsSingleColorRendererWidget::QgsSingleColorRendererWidget( QgsRasterLayer *laye return; } - connect( mColor, &QgsColorButton::colorChanged, this, &QgsSingleColorRendererWidget::colorChanged ); + connect( mColor, &QgsColorButton::colorChanged, this, &QgsRasterSingleColorRendererWidget::colorChanged ); setFromRenderer( layer->renderer() ); } } -QgsRasterRenderer *QgsSingleColorRendererWidget::renderer() +QgsRasterRenderer *QgsRasterSingleColorRendererWidget::renderer() { if ( !mRasterLayer ) { @@ -52,18 +52,18 @@ QgsRasterRenderer *QgsSingleColorRendererWidget::renderer() return nullptr; } - QgsSingleColorRenderer *renderer = new QgsSingleColorRenderer( provider, mColor->color() ); + QgsRasterSingleColorRenderer *renderer = new QgsRasterSingleColorRenderer( provider, mColor->color() ); return renderer; } -void QgsSingleColorRendererWidget::colorChanged( const QColor & ) +void QgsRasterSingleColorRendererWidget::colorChanged( const QColor & ) { emit widgetChanged(); } -void QgsSingleColorRendererWidget::setFromRenderer( const QgsRasterRenderer *r ) +void QgsRasterSingleColorRendererWidget::setFromRenderer( const QgsRasterRenderer *r ) { - const QgsSingleColorRenderer *scr = dynamic_cast( r ); + const QgsRasterSingleColorRenderer *scr = dynamic_cast( r ); if ( scr ) { mColor->setColor( scr->color() ); diff --git a/src/gui/raster/qgssinglecolorrendererwidget.h b/src/gui/raster/qgsrastersinglecolorrendererwidget.h similarity index 72% rename from src/gui/raster/qgssinglecolorrendererwidget.h rename to src/gui/raster/qgsrastersinglecolorrendererwidget.h index 87825357405e..3ebd16f85151 100644 --- a/src/gui/raster/qgssinglecolorrendererwidget.h +++ b/src/gui/raster/qgsrastersinglecolorrendererwidget.h @@ -1,5 +1,5 @@ /*************************************************************************** - qgssinglecolorrendererwidget.h + qgsrastersinglecolorrendererwidget.h --------------------------------- begin : April 2024 copyright : (C) 2024 by Mathieu Pellerin @@ -15,10 +15,10 @@ * * ***************************************************************************/ -#ifndef QGSSINGLECOLORRENDERERWIDGET_H -#define QGSSINGLECOLORRENDERERWIDGET_H +#ifndef QGSRASTERSINGLECOLORRENDERERWIDGET_H +#define QGSRASTERSINGLECOLORRENDERERWIDGET_H -#include "ui_qgssinglecolorrendererwidgetbase.h" +#include "ui_qgsrastersinglecolorrendererwidgetbase.h" #include "qgsrasterrendererwidget.h" #include "qgis_sip.h" @@ -29,15 +29,15 @@ * \ingroup gui * \since QGIS 3.38 */ -class GUI_EXPORT QgsSingleColorRendererWidget: public QgsRasterRendererWidget, private Ui::QgsSingleColorRendererWidgetBase +class GUI_EXPORT QgsRasterSingleColorRendererWidget: public QgsRasterRendererWidget, private Ui::QgsRasterSingleColorRendererWidgetBase { Q_OBJECT public: //! Constructs the widget - QgsSingleColorRendererWidget( QgsRasterLayer *layer, const QgsRectangle &extent = QgsRectangle() ); + QgsRasterSingleColorRendererWidget( QgsRasterLayer *layer, const QgsRectangle &extent = QgsRectangle() ); //! Widget creation function (use by the renderer registry) - static QgsRasterRendererWidget *create( QgsRasterLayer *layer, const QgsRectangle &extent ) SIP_FACTORY { return new QgsSingleColorRendererWidget( layer, extent ); } + static QgsRasterRendererWidget *create( QgsRasterLayer *layer, const QgsRectangle &extent ) SIP_FACTORY { return new QgsRasterSingleColorRendererWidget( layer, extent ); } QgsRasterRenderer *renderer() SIP_FACTORY override; @@ -51,4 +51,4 @@ class GUI_EXPORT QgsSingleColorRendererWidget: public QgsRasterRendererWidget, p }; -#endif // QGSSINGLECOLORRENDERERWIDGET_H +#endif // QGSRASTERSINGLECOLORRENDERERWIDGET_H diff --git a/src/gui/raster/qgsrendererrasterpropertieswidget.cpp b/src/gui/raster/qgsrendererrasterpropertieswidget.cpp index cc120ed05008..9daaa656b03a 100644 --- a/src/gui/raster/qgsrendererrasterpropertieswidget.cpp +++ b/src/gui/raster/qgsrendererrasterpropertieswidget.cpp @@ -24,7 +24,7 @@ #include "qgsrasterrendererregistry.h" #include "qgssinglebandgrayrendererwidget.h" #include "qgssinglebandpseudocolorrendererwidget.h" -#include "qgssinglecolorrendererwidget.h" +#include "qgsrastersinglecolorrendererwidget.h" #include "qgsmultibandcolorrendererwidget.h" #include "qgspalettedrendererwidget.h" #include "qgshillshaderendererwidget.h" @@ -44,7 +44,7 @@ void QgsRendererRasterPropertiesWidget::initRendererWidgetFunctions() QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "multibandcolor" ), QgsMultiBandColorRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlebandpseudocolor" ), QgsSingleBandPseudoColorRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlebandgray" ), QgsSingleBandGrayRendererWidget::create ); - QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlecolor" ), QgsSingleColorRendererWidget::create ); + QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlecolor" ), QgsRasterSingleColorRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "hillshade" ), QgsHillshadeRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "contour" ), QgsRasterContourRendererWidget::create ); diff --git a/src/ui/qgssinglecolorrendererwidgetbase.ui b/src/ui/qgsrastersinglecolorrendererwidgetbase.ui similarity index 94% rename from src/ui/qgssinglecolorrendererwidgetbase.ui rename to src/ui/qgsrastersinglecolorrendererwidgetbase.ui index 309fe45fe955..7b974f8c79c6 100644 --- a/src/ui/qgssinglecolorrendererwidgetbase.ui +++ b/src/ui/qgsrastersinglecolorrendererwidgetbase.ui @@ -1,7 +1,7 @@ - QgsSingleColorRendererWidgetBase - + QgsRasterSingleColorRendererWidgetBase + 0 diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index ec4624c1149c..0a818a94dac2 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -291,7 +291,7 @@ ADD_PYTHON_TEST(PyQgsSingleBandColorDataRenderer test_qgssinglebandcolordatarend ADD_PYTHON_TEST(PyQgsSingleBandGrayRenderer test_qgssinglebandgrayrenderer.py) ADD_PYTHON_TEST(PyQgsSingleBandPseudoColorRenderer test_qgssinglebandpseudocolorrenderer.py) ADD_PYTHON_TEST(PyQgsSingleSymbolRenderer test_qgssinglesymbolrenderer.py) -ADD_PYTHON_TEST(PyQgsSingleColorRenderer test_qgssinglecolorrenderer.py) +ADD_PYTHON_TEST(PyQgsRasterSingleColorRenderer test_rasterqgssinglecolorrenderer.py) ADD_PYTHON_TEST(PyQgsShapefileProvider test_provider_shapefile.py) ADD_PYTHON_TEST(PyQgsSphere test_qgssphere.py) ADD_PYTHON_TEST(PyQgsSvgCache test_qgssvgcache.py) diff --git a/tests/src/python/test_qgssinglecolorrenderer.py b/tests/src/python/test_qgsrastersinglecolorrenderer.py similarity index 82% rename from tests/src/python/test_qgssinglecolorrenderer.py rename to tests/src/python/test_qgsrastersinglecolorrenderer.py index 55c15fee7b42..9c2d8d56f089 100644 --- a/tests/src/python/test_qgssinglecolorrenderer.py +++ b/tests/src/python/test_qgsrastersinglecolorrenderer.py @@ -1,4 +1,4 @@ -"""QGIS Unit tests for QgsSingleColorRenderer. +"""QGIS Unit tests for QgsRasterSingleColorRenderer. .. 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 @@ -12,7 +12,7 @@ from qgis.PyQt.QtGui import QColor from qgis.core import ( QgsRasterLayer, - QgsSingleColorRenderer, + QgsRasterSingleColorRenderer, QgsMapSettings, ) import unittest @@ -25,7 +25,7 @@ start_app() -class TestQgsSingleBandGrayRenderer(QgisTestCase): +class TestQgsRasterSingleBandGrayRenderer(QgisTestCase): def testRenderer(self): path = os.path.join(unitTestDataPath(), @@ -35,7 +35,7 @@ def testRenderer(self): layer = QgsRasterLayer(path, base_name) self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') - renderer = QgsSingleColorRenderer(layer.dataProvider(), + renderer = QgsRasterSingleColorRenderer(layer.dataProvider(), QColor(255, 0, 0)) self.assertEqual(renderer.color(), QColor(255, 0, 0)) @@ -49,8 +49,8 @@ def testRenderer(self): self.assertTrue( self.render_map_settings_check( - 'single_color_renderer', - 'single_color_renderer', + 'raster_single_color_renderer', + 'raster_single_color_renderer', ms) ) diff --git a/tests/testdata/control_images/expected_single_color_renderer/expected_single_color_renderer.png b/tests/testdata/control_images/expected_raster_single_color_renderer/expected_raster_single_color_renderer.png similarity index 100% rename from tests/testdata/control_images/expected_single_color_renderer/expected_single_color_renderer.png rename to tests/testdata/control_images/expected_raster_single_color_renderer/expected_raster_single_color_renderer.png From 2a3ff27936b20cf2d8ef284be9cdcd8594930cbb Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 8 Apr 2024 14:33:40 +1000 Subject: [PATCH 72/87] Concurrency should not apply to test failure comment workflow --- .github/workflows/write_failure_comment.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/write_failure_comment.yml b/.github/workflows/write_failure_comment.yml index 9e85208e6407..38d676a1235d 100644 --- a/.github/workflows/write_failure_comment.yml +++ b/.github/workflows/write_failure_comment.yml @@ -1,9 +1,5 @@ name: Write test failure comment -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - on: workflow_run: workflows: [🧪 QGIS tests] From d68a428f6e85481c5810bc7d8c816a6a1def5ddf Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Mon, 8 Apr 2024 09:34:33 +0700 Subject: [PATCH 73/87] Address review --- .../qgsrastersinglecolorrenderer.sip.in | 16 +++-- .../qgsrastersinglecolorrenderer.sip.in | 16 +++-- .../raster/qgsrastersinglecolorrenderer.cpp | 58 ++++++++++++++----- .../raster/qgsrastersinglecolorrenderer.h | 13 +++-- .../qgsrastersinglecolorrendererwidget.cpp | 18 +++--- .../qgsrastersinglecolorrendererwidget.h | 3 - .../qgsrastersinglecolorrendererwidgetbase.ui | 19 +++++- tests/src/python/CMakeLists.txt | 2 +- .../test_qgsrastersinglecolorrenderer.py | 5 +- 9 files changed, 106 insertions(+), 44 deletions(-) diff --git a/python/PyQt6/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in index fa3084087dce..c31639035068 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in @@ -14,7 +14,7 @@ class QgsRasterSingleColorRenderer: QgsRasterRenderer { %Docstring(signature="appended") -Raster single color renderer pipe. +Raster renderer which renders all data pixels using a single color. .. versionadded:: 3.38 %End @@ -24,7 +24,7 @@ Raster single color renderer pipe. %End public: - QgsRasterSingleColorRenderer( QgsRasterInterface *input, QColor color ); + QgsRasterSingleColorRenderer( QgsRasterInterface *input, int band, const QColor &color ); %Docstring Creates a single ``color`` renderer %End @@ -49,16 +49,24 @@ Creates an instance of the renderer based on definition from XML (used by the re QColor color() const; %Docstring Returns the single color used by the renderer. + +.. seealso:: :py:func:`setColor` %End - void setColor( QColor &color ); + void setColor( const QColor &color ); %Docstring -Sets the single color used by the renderer. +Sets the single ``color`` used by the renderer. + +.. seealso:: :py:func:`color` %End virtual void writeXml( QDomDocument &doc, QDomElement &parentElem ) const; + virtual int inputBand() const; + + virtual bool setInputBand( int band ); + virtual QList usesBands() const; diff --git a/python/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in b/python/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in index fa3084087dce..c31639035068 100644 --- a/python/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in +++ b/python/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in @@ -14,7 +14,7 @@ class QgsRasterSingleColorRenderer: QgsRasterRenderer { %Docstring(signature="appended") -Raster single color renderer pipe. +Raster renderer which renders all data pixels using a single color. .. versionadded:: 3.38 %End @@ -24,7 +24,7 @@ Raster single color renderer pipe. %End public: - QgsRasterSingleColorRenderer( QgsRasterInterface *input, QColor color ); + QgsRasterSingleColorRenderer( QgsRasterInterface *input, int band, const QColor &color ); %Docstring Creates a single ``color`` renderer %End @@ -49,16 +49,24 @@ Creates an instance of the renderer based on definition from XML (used by the re QColor color() const; %Docstring Returns the single color used by the renderer. + +.. seealso:: :py:func:`setColor` %End - void setColor( QColor &color ); + void setColor( const QColor &color ); %Docstring -Sets the single color used by the renderer. +Sets the single ``color`` used by the renderer. + +.. seealso:: :py:func:`color` %End virtual void writeXml( QDomDocument &doc, QDomElement &parentElem ) const; + virtual int inputBand() const; + + virtual bool setInputBand( int band ); + virtual QList usesBands() const; diff --git a/src/core/raster/qgsrastersinglecolorrenderer.cpp b/src/core/raster/qgsrastersinglecolorrenderer.cpp index 7237dc9b35de..1c6f0f2bdc30 100644 --- a/src/core/raster/qgsrastersinglecolorrenderer.cpp +++ b/src/core/raster/qgsrastersinglecolorrenderer.cpp @@ -22,15 +22,16 @@ #include #include -QgsRasterSingleColorRenderer::QgsRasterSingleColorRenderer( QgsRasterInterface *input, QColor color ) +QgsRasterSingleColorRenderer::QgsRasterSingleColorRenderer( QgsRasterInterface *input, int band, const QColor &color ) : QgsRasterRenderer( input, QStringLiteral( "singlecolor" ) ) + , mInputBand( band ) , mColor( color ) { } QgsRasterSingleColorRenderer *QgsRasterSingleColorRenderer::clone() const { - QgsRasterSingleColorRenderer *renderer = new QgsRasterSingleColorRenderer( nullptr, mColor ); + QgsRasterSingleColorRenderer *renderer = new QgsRasterSingleColorRenderer( nullptr, mInputBand, mColor ); renderer->copyCommonProperties( this ); return renderer; } @@ -48,23 +49,24 @@ QgsRasterRenderer *QgsRasterSingleColorRenderer::create( const QDomElement &elem } const QColor color = QgsColorUtils::colorFromString( elem.attribute( QStringLiteral( "color" ), QStringLiteral( "0,0,0" ) ) ); - QgsRasterSingleColorRenderer *r = new QgsRasterSingleColorRenderer( input, color ); + const int band = elem.attribute( QStringLiteral( "band" ), QStringLiteral( "1" ) ).toInt(); + QgsRasterSingleColorRenderer *r = new QgsRasterSingleColorRenderer( input, band, color ); r->readXml( elem ); return r; } -QgsRasterBlock *QgsRasterSingleColorRenderer::block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback ) +QgsRasterBlock *QgsRasterSingleColorRenderer::block( int, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback ) { QgsDebugMsgLevel( QStringLiteral( "width = %1 height = %2" ).arg( width ).arg( height ), 4 ); std::unique_ptr< QgsRasterBlock > outputBlock( new QgsRasterBlock() ); - if ( !mInput ) + if ( !mInput || mInputBand == -1 ) { return outputBlock.release(); } - const std::shared_ptr< QgsRasterBlock > inputBlock( mInput->block( bandNo, extent, width, height, feedback ) ); + const std::shared_ptr< QgsRasterBlock > inputBlock( mInput->block( mInputBand, extent, width, height, feedback ) ); if ( !inputBlock || inputBlock->isEmpty() ) { QgsDebugError( QStringLiteral( "No raster data!" ) ); @@ -82,14 +84,17 @@ QgsRasterBlock *QgsRasterSingleColorRenderer::block( int bandNo, const QgsRectan return outputBlock.release(); } - const QRgb myDefaultColor = renderColorForNodataPixel(); + const QRgb defaultColor = renderColorForNodataPixel(); + const QRgb rendererColor = qRgba( mColor.red(), mColor.green(), mColor.blue(), mColor.alpha() ); + bool isNoData = false; - for ( qgssize i = 0; i < ( qgssize )width * height; i++ ) + const qgssize blockSize = static_cast< qgssize >( width ) * height; + for ( qgssize i = 0; i < blockSize; i++ ) { double value = inputBlock->valueAndNoData( i, isNoData ); if ( isNoData ) { - outputBlock->setColor( i, myDefaultColor ); + outputBlock->setColor( i, defaultColor ); continue; } @@ -103,7 +108,7 @@ QgsRasterBlock *QgsRasterSingleColorRenderer::block( int bandNo, const QgsRectan const double alpha = alphaBlock->value( i ); if ( alpha == 0 ) { - outputBlock->setColor( i, myDefaultColor ); + outputBlock->setColor( i, defaultColor ); continue; } else @@ -114,7 +119,7 @@ QgsRasterBlock *QgsRasterSingleColorRenderer::block( int bandNo, const QgsRectan if ( qgsDoubleNear( currentAlpha, 1.0 ) ) { - outputBlock->setColor( i, qRgba( mColor.red(), mColor.green(), mColor.blue(), mColor.alpha() ) ); + outputBlock->setColor( i, rendererColor ); } else { @@ -128,7 +133,7 @@ QgsRasterBlock *QgsRasterSingleColorRenderer::block( int bandNo, const QgsRectan return outputBlock.release(); } -void QgsRasterSingleColorRenderer::setColor( QColor &color ) +void QgsRasterSingleColorRenderer::setColor( const QColor &color ) { mColor = color;; } @@ -149,16 +154,37 @@ void QgsRasterSingleColorRenderer::writeXml( QDomDocument &doc, QDomElement &par _writeXml( doc, rasterRendererElem ); rasterRendererElem.setAttribute( QStringLiteral( "color" ), QgsColorUtils::colorToString( mColor ) ); + rasterRendererElem.setAttribute( QStringLiteral( "band" ), QgsColorUtils::colorToString( mInputBand ) ); parentElem.appendChild( rasterRendererElem ); } +int QgsRasterSingleColorRenderer::inputBand() const +{ + return mInputBand; +} + +bool QgsRasterSingleColorRenderer::setInputBand( int band ) +{ + if ( !mInput ) + { + mInputBand = band; + return true; + } + else if ( band > 0 && band <= mInput->bandCount() ) + { + mInputBand = band; + return true; + } + return false; +} + QList QgsRasterSingleColorRenderer::usesBands() const { - QList bandList; - for ( int i = 0; i <= bandCount(); i++ ) + QList bands; + if ( mInputBand != -1 ) { - bandList << i; + bands << mInputBand; } - return bandList; + return bands; } diff --git a/src/core/raster/qgsrastersinglecolorrenderer.h b/src/core/raster/qgsrastersinglecolorrenderer.h index 9dfc653e40a9..50f7fa94df2c 100644 --- a/src/core/raster/qgsrastersinglecolorrenderer.h +++ b/src/core/raster/qgsrastersinglecolorrenderer.h @@ -28,7 +28,7 @@ class QDomElement; /** * \ingroup core - * \brief Raster single color renderer pipe. + * \brief Raster renderer which renders all data pixels using a single color. * \since QGIS 3.38 */ class CORE_EXPORT QgsRasterSingleColorRenderer: public QgsRasterRenderer @@ -36,7 +36,7 @@ class CORE_EXPORT QgsRasterSingleColorRenderer: public QgsRasterRenderer public: //! Creates a single \a color renderer - QgsRasterSingleColorRenderer( QgsRasterInterface *input, QColor color ); + QgsRasterSingleColorRenderer( QgsRasterInterface *input, int band, const QColor &color ); //! QgsRasterSingleColorRenderer cannot be copied. Use clone() instead. QgsRasterSingleColorRenderer( const QgsRasterSingleColorRenderer & ) = delete; @@ -53,16 +53,20 @@ class CORE_EXPORT QgsRasterSingleColorRenderer: public QgsRasterRenderer /** * Returns the single color used by the renderer. + * \see setColor() */ QColor color() const; /** - * Sets the single color used by the renderer. + * Sets the single \a color used by the renderer. + * \see color() */ - void setColor( QColor &color ); + void setColor( const QColor &color ); void writeXml( QDomDocument &doc, QDomElement &parentElem ) const override; + int inputBand() const override; + bool setInputBand( int band ) override; QList usesBands() const override; private: @@ -71,6 +75,7 @@ class CORE_EXPORT QgsRasterSingleColorRenderer: public QgsRasterRenderer const QgsRasterSingleColorRenderer &operator=( const QgsRasterSingleColorRenderer & ); #endif + int mInputBand = -1; QColor mColor; }; diff --git a/src/gui/raster/qgsrastersinglecolorrendererwidget.cpp b/src/gui/raster/qgsrastersinglecolorrendererwidget.cpp index 333f3f384804..eef41fb13967 100644 --- a/src/gui/raster/qgsrastersinglecolorrendererwidget.cpp +++ b/src/gui/raster/qgsrastersinglecolorrendererwidget.cpp @@ -33,7 +33,10 @@ QgsRasterSingleColorRendererWidget::QgsRasterSingleColorRendererWidget( QgsRaste return; } - connect( mColor, &QgsColorButton::colorChanged, this, &QgsRasterSingleColorRendererWidget::colorChanged ); + mBandComboBox->setLayer( layer ); + + connect( mBandComboBox, &QgsRasterBandComboBox::bandChanged, this, [ = ]( int ) { emit widgetChanged(); } ); + connect( mColorButton, &QgsColorButton::colorChanged, this, [ = ]( const QColor & ) { emit widgetChanged(); } ); setFromRenderer( layer->renderer() ); } @@ -52,24 +55,21 @@ QgsRasterRenderer *QgsRasterSingleColorRendererWidget::renderer() return nullptr; } - QgsRasterSingleColorRenderer *renderer = new QgsRasterSingleColorRenderer( provider, mColor->color() ); + QgsRasterSingleColorRenderer *renderer = new QgsRasterSingleColorRenderer( provider, mBandComboBox->currentBand(), mColorButton->color() ); return renderer; } -void QgsRasterSingleColorRendererWidget::colorChanged( const QColor & ) -{ - emit widgetChanged(); -} - void QgsRasterSingleColorRendererWidget::setFromRenderer( const QgsRasterRenderer *r ) { const QgsRasterSingleColorRenderer *scr = dynamic_cast( r ); if ( scr ) { - mColor->setColor( scr->color() ); + mBandComboBox->setBand( scr->inputBand() ); + mColorButton->setColor( scr->color() ); } else { - mColor->setColor( QColor( 0, 0, 0 ) ); + mBandComboBox->setBand( 1 ); + mColorButton->setColor( QColor( 0, 0, 0 ) ); } } diff --git a/src/gui/raster/qgsrastersinglecolorrendererwidget.h b/src/gui/raster/qgsrastersinglecolorrendererwidget.h index 3ebd16f85151..55dc343d2f62 100644 --- a/src/gui/raster/qgsrastersinglecolorrendererwidget.h +++ b/src/gui/raster/qgsrastersinglecolorrendererwidget.h @@ -46,9 +46,6 @@ class GUI_EXPORT QgsRasterSingleColorRendererWidget: public QgsRasterRendererWid */ void setFromRenderer( const QgsRasterRenderer *r ); - private slots: - void colorChanged( const QColor &color ); - }; #endif // QGSRASTERSINGLECOLORRENDERERWIDGET_H diff --git a/src/ui/qgsrastersinglecolorrendererwidgetbase.ui b/src/ui/qgsrastersinglecolorrendererwidgetbase.ui index 7b974f8c79c6..b8b1276227d9 100644 --- a/src/ui/qgsrastersinglecolorrendererwidgetbase.ui +++ b/src/ui/qgsrastersinglecolorrendererwidgetbase.ui @@ -29,14 +29,24 @@ + + + Band + + + + + + + Color - - + + 0 @@ -80,6 +90,11 @@ QToolButton
qgscolorbutton.h
+ + QgsRasterBandComboBox + QComboBox +
qgsrasterbandcombobox.h
+
diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 0a818a94dac2..7b1402a7b326 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -291,7 +291,7 @@ ADD_PYTHON_TEST(PyQgsSingleBandColorDataRenderer test_qgssinglebandcolordatarend ADD_PYTHON_TEST(PyQgsSingleBandGrayRenderer test_qgssinglebandgrayrenderer.py) ADD_PYTHON_TEST(PyQgsSingleBandPseudoColorRenderer test_qgssinglebandpseudocolorrenderer.py) ADD_PYTHON_TEST(PyQgsSingleSymbolRenderer test_qgssinglesymbolrenderer.py) -ADD_PYTHON_TEST(PyQgsRasterSingleColorRenderer test_rasterqgssinglecolorrenderer.py) +ADD_PYTHON_TEST(PyQgsRasterSingleColorRenderer test_qgsrastersinglecolorrenderer.py) ADD_PYTHON_TEST(PyQgsShapefileProvider test_provider_shapefile.py) ADD_PYTHON_TEST(PyQgsSphere test_qgssphere.py) ADD_PYTHON_TEST(PyQgsSvgCache test_qgssvgcache.py) diff --git a/tests/src/python/test_qgsrastersinglecolorrenderer.py b/tests/src/python/test_qgsrastersinglecolorrenderer.py index 9c2d8d56f089..6ef5a3a420d1 100644 --- a/tests/src/python/test_qgsrastersinglecolorrenderer.py +++ b/tests/src/python/test_qgsrastersinglecolorrenderer.py @@ -36,8 +36,11 @@ def testRenderer(self): self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') renderer = QgsRasterSingleColorRenderer(layer.dataProvider(), - QColor(255, 0, 0)) + 1, + QColor(255, 0, 0)) + self.assertEqual(renderer.inputBand(), 1) + self.assertEqual(renderer.usesBands(), [1]) self.assertEqual(renderer.color(), QColor(255, 0, 0)) renderer.setColor(QColor(0, 255, 0)) self.assertEqual(renderer.color(), QColor(0, 255, 0)) From a62cc55e92a0fb828ec2fd3b9d8fae13b693acb9 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Mon, 8 Apr 2024 09:36:51 +0700 Subject: [PATCH 74/87] Remove renderer widget from python bindings --- .../qgsrastersinglecolorrendererwidget.sip.in | 51 ------------------- python/PyQt6/gui/gui_auto.sip | 1 - .../qgsrastersinglecolorrendererwidget.sip.in | 51 ------------------- python/gui/gui_auto.sip | 1 - .../qgsrastersinglecolorrendererwidget.h | 3 +- 5 files changed, 2 insertions(+), 105 deletions(-) delete mode 100644 python/PyQt6/gui/auto_generated/raster/qgsrastersinglecolorrendererwidget.sip.in delete mode 100644 python/gui/auto_generated/raster/qgsrastersinglecolorrendererwidget.sip.in diff --git a/python/PyQt6/gui/auto_generated/raster/qgsrastersinglecolorrendererwidget.sip.in b/python/PyQt6/gui/auto_generated/raster/qgsrastersinglecolorrendererwidget.sip.in deleted file mode 100644 index f9077b14c6e2..000000000000 --- a/python/PyQt6/gui/auto_generated/raster/qgsrastersinglecolorrendererwidget.sip.in +++ /dev/null @@ -1,51 +0,0 @@ -/************************************************************************ - * This file has been generated automatically from * - * * - * src/gui/raster/qgsrastersinglecolorrendererwidget.h * - * * - * Do not edit manually ! Edit header and run scripts/sipify.pl again * - ************************************************************************/ - - - - - -class QgsRasterSingleColorRendererWidget: QgsRasterRendererWidget -{ -%Docstring(signature="appended") -Renderer widget for the single color renderer. - -.. versionadded:: 3.38 -%End - -%TypeHeaderCode -#include "qgsrastersinglecolorrendererwidget.h" -%End - public: - QgsRasterSingleColorRendererWidget( QgsRasterLayer *layer, const QgsRectangle &extent = QgsRectangle() ); -%Docstring -Constructs the widget -%End - - static QgsRasterRendererWidget *create( QgsRasterLayer *layer, const QgsRectangle &extent ) /Factory/; -%Docstring -Widget creation function (use by the renderer registry) -%End - - virtual QgsRasterRenderer *renderer() /Factory/; - - - void setFromRenderer( const QgsRasterRenderer *r ); -%Docstring -Sets the widget state from the specified renderer. -%End - -}; - -/************************************************************************ - * This file has been generated automatically from * - * * - * src/gui/raster/qgsrastersinglecolorrendererwidget.h * - * * - * Do not edit manually ! Edit header and run scripts/sipify.pl again * - ************************************************************************/ diff --git a/python/PyQt6/gui/gui_auto.sip b/python/PyQt6/gui/gui_auto.sip index e798e33d1fa4..c668cc6c9a90 100644 --- a/python/PyQt6/gui/gui_auto.sip +++ b/python/PyQt6/gui/gui_auto.sip @@ -461,7 +461,6 @@ %Include auto_generated/raster/qgsrendererrasterpropertieswidget.sip %Include auto_generated/raster/qgssinglebandgrayrendererwidget.sip %Include auto_generated/raster/qgssinglebandpseudocolorrendererwidget.sip -%Include auto_generated/raster/qgsrastersinglecolorrendererwidget.sip %Include auto_generated/raster/qgsrasterlayerproperties.sip %Include auto_generated/raster/qgsrasterlayertemporalpropertieswidget.sip %Include auto_generated/vector/qgsfieldcalculator.sip diff --git a/python/gui/auto_generated/raster/qgsrastersinglecolorrendererwidget.sip.in b/python/gui/auto_generated/raster/qgsrastersinglecolorrendererwidget.sip.in deleted file mode 100644 index f9077b14c6e2..000000000000 --- a/python/gui/auto_generated/raster/qgsrastersinglecolorrendererwidget.sip.in +++ /dev/null @@ -1,51 +0,0 @@ -/************************************************************************ - * This file has been generated automatically from * - * * - * src/gui/raster/qgsrastersinglecolorrendererwidget.h * - * * - * Do not edit manually ! Edit header and run scripts/sipify.pl again * - ************************************************************************/ - - - - - -class QgsRasterSingleColorRendererWidget: QgsRasterRendererWidget -{ -%Docstring(signature="appended") -Renderer widget for the single color renderer. - -.. versionadded:: 3.38 -%End - -%TypeHeaderCode -#include "qgsrastersinglecolorrendererwidget.h" -%End - public: - QgsRasterSingleColorRendererWidget( QgsRasterLayer *layer, const QgsRectangle &extent = QgsRectangle() ); -%Docstring -Constructs the widget -%End - - static QgsRasterRendererWidget *create( QgsRasterLayer *layer, const QgsRectangle &extent ) /Factory/; -%Docstring -Widget creation function (use by the renderer registry) -%End - - virtual QgsRasterRenderer *renderer() /Factory/; - - - void setFromRenderer( const QgsRasterRenderer *r ); -%Docstring -Sets the widget state from the specified renderer. -%End - -}; - -/************************************************************************ - * This file has been generated automatically from * - * * - * src/gui/raster/qgsrastersinglecolorrendererwidget.h * - * * - * Do not edit manually ! Edit header and run scripts/sipify.pl again * - ************************************************************************/ diff --git a/python/gui/gui_auto.sip b/python/gui/gui_auto.sip index e798e33d1fa4..c668cc6c9a90 100644 --- a/python/gui/gui_auto.sip +++ b/python/gui/gui_auto.sip @@ -461,7 +461,6 @@ %Include auto_generated/raster/qgsrendererrasterpropertieswidget.sip %Include auto_generated/raster/qgssinglebandgrayrendererwidget.sip %Include auto_generated/raster/qgssinglebandpseudocolorrendererwidget.sip -%Include auto_generated/raster/qgsrastersinglecolorrendererwidget.sip %Include auto_generated/raster/qgsrasterlayerproperties.sip %Include auto_generated/raster/qgsrasterlayertemporalpropertieswidget.sip %Include auto_generated/vector/qgsfieldcalculator.sip diff --git a/src/gui/raster/qgsrastersinglecolorrendererwidget.h b/src/gui/raster/qgsrastersinglecolorrendererwidget.h index 55dc343d2f62..23197e736a39 100644 --- a/src/gui/raster/qgsrastersinglecolorrendererwidget.h +++ b/src/gui/raster/qgsrastersinglecolorrendererwidget.h @@ -18,10 +18,11 @@ #ifndef QGSRASTERSINGLECOLORRENDERERWIDGET_H #define QGSRASTERSINGLECOLORRENDERERWIDGET_H +#define SIP_NO_FILE + #include "ui_qgsrastersinglecolorrendererwidgetbase.h" #include "qgsrasterrendererwidget.h" -#include "qgis_sip.h" #include "qgis_gui.h" /** From 0c104008b2d6e3aba37f69a93790e53c58db711c Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Mon, 8 Apr 2024 11:56:53 +0700 Subject: [PATCH 75/87] Fix hasty copy/paste --- src/core/raster/qgsrastersinglecolorrenderer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/raster/qgsrastersinglecolorrenderer.cpp b/src/core/raster/qgsrastersinglecolorrenderer.cpp index 1c6f0f2bdc30..4a67442b4ca0 100644 --- a/src/core/raster/qgsrastersinglecolorrenderer.cpp +++ b/src/core/raster/qgsrastersinglecolorrenderer.cpp @@ -154,7 +154,7 @@ void QgsRasterSingleColorRenderer::writeXml( QDomDocument &doc, QDomElement &par _writeXml( doc, rasterRendererElem ); rasterRendererElem.setAttribute( QStringLiteral( "color" ), QgsColorUtils::colorToString( mColor ) ); - rasterRendererElem.setAttribute( QStringLiteral( "band" ), QgsColorUtils::colorToString( mInputBand ) ); + rasterRendererElem.setAttribute( QStringLiteral( "band" ), mInputBand ); parentElem.appendChild( rasterRendererElem ); } From b30d9897bdf221543d44de3d2bc4c79f2e1969c1 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Mon, 8 Apr 2024 15:38:21 +0700 Subject: [PATCH 76/87] Make clang-tidy happy --- src/core/raster/qgsrastersinglecolorrenderer.cpp | 7 +------ src/gui/qgsmapcanvas.cpp | 2 ++ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/core/raster/qgsrastersinglecolorrenderer.cpp b/src/core/raster/qgsrastersinglecolorrenderer.cpp index 4a67442b4ca0..bf2f6414c3cb 100644 --- a/src/core/raster/qgsrastersinglecolorrenderer.cpp +++ b/src/core/raster/qgsrastersinglecolorrenderer.cpp @@ -166,12 +166,7 @@ int QgsRasterSingleColorRenderer::inputBand() const bool QgsRasterSingleColorRenderer::setInputBand( int band ) { - if ( !mInput ) - { - mInputBand = band; - return true; - } - else if ( band > 0 && band <= mInput->bandCount() ) + if ( !mInput || ( band > 0 && band <= mInput->bandCount() ) ) { mInputBand = band; return true; diff --git a/src/gui/qgsmapcanvas.cpp b/src/gui/qgsmapcanvas.cpp index 176629800ef2..264bbe77ea17 100644 --- a/src/gui/qgsmapcanvas.cpp +++ b/src/gui/qgsmapcanvas.cpp @@ -1059,7 +1059,9 @@ void QgsMapCanvas::clearTemporalCache() continue; if ( !alreadyInvalidatedThisLayer ) + { mCache->invalidateCacheForLayer( layer ); + } } else if ( QgsGroupLayer *gl = qobject_cast( layer ) ) { From adfc0e06f39a0538de7459aaa9089b840b3d661c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 8 Apr 2024 11:39:59 +1000 Subject: [PATCH 77/87] Don't share GEOS contexts across threads GEOS contexts have always been documented as for use by a single thread, but we've gotten away with a single shared context across threads just because of how GEOS previously wasn't doing any non-thread safe internally with them. But recent GEOS versions DO, and it's no longer safe to share a single context across threads. Use the same approach as we use for thread local proj context handling for geos contexts --- src/analysis/vector/qgsgeometrysnapper.cpp | 12 +- src/core/geometry/qgsgeos.cpp | 702 ++++++++++-------- src/core/geometry/qgsgeos.h | 33 +- src/core/labeling/qgslabelfeature.cpp | 2 +- src/core/pal/feature.cpp | 12 +- src/core/pal/geomfunction.cpp | 2 +- src/core/pal/labelposition.cpp | 28 +- src/core/pal/layer.cpp | 4 +- src/core/pal/pal.cpp | 2 +- src/core/pal/pointset.cpp | 28 +- src/core/pal/util.cpp | 2 +- .../qgspointcloudlayerprofilegenerator.cpp | 8 +- src/core/qgstracer.cpp | 2 +- src/plugins/topology/topolTest.cpp | 2 +- tests/src/core/geometry/testqgsgeometry.cpp | 2 +- 15 files changed, 455 insertions(+), 386 deletions(-) diff --git a/src/analysis/vector/qgsgeometrysnapper.cpp b/src/analysis/vector/qgsgeometrysnapper.cpp index 3d6b9c34548c..1352679c733b 100644 --- a/src/analysis/vector/qgsgeometrysnapper.cpp +++ b/src/analysis/vector/qgsgeometrysnapper.cpp @@ -109,7 +109,7 @@ bool QgsSnapIndex::SegmentSnapItem::withinSquaredDistance( const QgsPoint &p, co QgsSnapIndex::QgsSnapIndex() { - mSTRTree = GEOSSTRtree_create_r( QgsGeos::getGEOSHandler(), ( size_t )10 ); + mSTRTree = GEOSSTRtree_create_r( QgsGeosContext::get(), ( size_t )10 ); } QgsSnapIndex::~QgsSnapIndex() @@ -117,14 +117,14 @@ QgsSnapIndex::~QgsSnapIndex() qDeleteAll( mCoordIdxs ); qDeleteAll( mSnapItems ); - GEOSSTRtree_destroy_r( QgsGeos::getGEOSHandler(), mSTRTree ); + GEOSSTRtree_destroy_r( QgsGeosContext::get(), mSTRTree ); } void QgsSnapIndex::addPoint( const CoordIdx *idx, bool isEndPoint ) { const QgsPoint p = idx->point(); - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); geos::unique_ptr point( GEOSGeom_createPointFromXY_r( geosctxt, p.x(), p.y() ) ); PointSnapItem *item = new PointSnapItem( idx, isEndPoint ); @@ -137,7 +137,7 @@ void QgsSnapIndex::addSegment( const CoordIdx *idxFrom, const CoordIdx *idxTo ) const QgsPoint pointFrom = idxFrom->point(); const QgsPoint pointTo = idxTo->point(); - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); GEOSCoordSequence *coord = GEOSCoordSeq_create_r( geosctxt, 2, 2 ); GEOSCoordSeq_setXY_r( geosctxt, coord, 0, pointFrom.x(), pointFrom.y() ); @@ -191,7 +191,7 @@ void _GEOSQueryCallback( void *item, void *userdata ) QgsPoint QgsSnapIndex::getClosestSnapToPoint( const QgsPoint &startPoint, const QgsPoint &midPoint ) { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); // Look for intersections on segment from the target point to the point opposite to the point reference point // p2 = p1 + 2 * (q - p1) @@ -231,7 +231,7 @@ QgsPoint QgsSnapIndex::getClosestSnapToPoint( const QgsPoint &startPoint, const QgsSnapIndex::SnapItem *QgsSnapIndex::getSnapItem( const QgsPoint &pos, const double tolerance, QgsSnapIndex::PointSnapItem **pSnapPoint, QgsSnapIndex::SegmentSnapItem **pSnapSegment, bool endPointOnly ) const { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); GEOSCoordSequence *coord = GEOSCoordSeq_create_r( geosctxt, 2, 2 ); GEOSCoordSeq_setXY_r( geosctxt, coord, 0, pos.x() - tolerance, pos.y() - tolerance ); diff --git a/src/core/geometry/qgsgeos.cpp b/src/core/geometry/qgsgeos.cpp index 6285cf0d4718..0a079b75ed11 100644 --- a/src/core/geometry/qgsgeos.cpp +++ b/src/core/geometry/qgsgeos.cpp @@ -94,45 +94,49 @@ static void printGEOSNotice( const char *fmt, ... ) #endif } -class GEOSInit -{ - public: - GEOSContextHandle_t ctxt; +// +// QgsGeosContext +// - GEOSInit() - { - ctxt = initGEOS_r( printGEOSNotice, throwGEOSException ); - } +thread_local QgsGeosContext QgsGeosContext::sGeosContext; - ~GEOSInit() - { - finishGEOS_r( ctxt ); - } +QgsGeosContext::QgsGeosContext() +{ + mContext = initGEOS_r( printGEOSNotice, throwGEOSException ); +} - GEOSInit( const GEOSInit &rh ) = delete; - GEOSInit &operator=( const GEOSInit &rh ) = delete; -}; +QgsGeosContext::~QgsGeosContext() +{ + finishGEOS_r( mContext ); +} -Q_GLOBAL_STATIC( GEOSInit, geosinit ) +GEOSContextHandle_t QgsGeosContext::get() +{ + return sGeosContext.mContext; +} + +// +// geos +// void geos::GeosDeleter::operator()( GEOSGeometry *geom ) const { - GEOSGeom_destroy_r( geosinit()->ctxt, geom ); + GEOSGeom_destroy_r( QgsGeosContext::get(), geom ); } void geos::GeosDeleter::operator()( const GEOSPreparedGeometry *geom ) const { - GEOSPreparedGeom_destroy_r( geosinit()->ctxt, geom ); + GEOSPreparedGeom_destroy_r( QgsGeosContext::get(), geom ); } void geos::GeosDeleter::operator()( GEOSBufferParams *params ) const { - GEOSBufferParams_destroy_r( geosinit()->ctxt, params ); + GEOSBufferParams_destroy_r( QgsGeosContext::get(), params ); } void geos::GeosDeleter::operator()( GEOSCoordSequence *sequence ) const { - GEOSCoordSeq_destroy_r( geosinit()->ctxt, sequence ); + GEOSCoordSeq_destroy_r( QgsGeosContext::get(), sequence ); } @@ -150,7 +154,7 @@ QgsGeos::QgsGeos( const QgsAbstractGeometry *geometry, double precision, bool al QgsGeometry QgsGeos::geometryFromGeos( GEOSGeometry *geos ) { QgsGeometry g( QgsGeos::fromGeos( geos ) ); - GEOSGeom_destroy_r( QgsGeos::getGEOSHandler(), geos ); + GEOSGeom_destroy_r( QgsGeosContext::get(), geos ); return g; } @@ -167,6 +171,8 @@ std::unique_ptr QgsGeos::makeValid( Qgis::MakeValidMethod m return nullptr; } + GEOSContextHandle_t context = QgsGeosContext::get(); + #if GEOS_VERSION_MAJOR==3 && GEOS_VERSION_MINOR<10 if ( method != Qgis::MakeValidMethod::Linework ) throw QgsNotSupportedException( QObject::tr( "The structured method to make geometries valid requires a QGIS build based on GEOS 3.10 or later" ) ); @@ -176,32 +182,32 @@ std::unique_ptr QgsGeos::makeValid( Qgis::MakeValidMethod m geos::unique_ptr geos; try { - geos.reset( GEOSMakeValid_r( geosinit()->ctxt, mGeos.get() ) ); + geos.reset( GEOSMakeValid_r( context, mGeos.get() ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) #else - GEOSMakeValidParams *params = GEOSMakeValidParams_create_r( geosinit()->ctxt ); + GEOSMakeValidParams *params = GEOSMakeValidParams_create_r( context ); switch ( method ) { case Qgis::MakeValidMethod::Linework: - GEOSMakeValidParams_setMethod_r( geosinit()->ctxt, params, GEOS_MAKE_VALID_LINEWORK ); + GEOSMakeValidParams_setMethod_r( context, params, GEOS_MAKE_VALID_LINEWORK ); break; case Qgis::MakeValidMethod::Structure: - GEOSMakeValidParams_setMethod_r( geosinit()->ctxt, params, GEOS_MAKE_VALID_STRUCTURE ); + GEOSMakeValidParams_setMethod_r( context, params, GEOS_MAKE_VALID_STRUCTURE ); break; } - GEOSMakeValidParams_setKeepCollapsed_r( geosinit()->ctxt, + GEOSMakeValidParams_setKeepCollapsed_r( context, params, keepCollapsed ? 1 : 0 ); geos::unique_ptr geos; try { - geos.reset( GEOSMakeValidWithParams_r( geosinit()->ctxt, mGeos.get(), params ) ); - GEOSMakeValidParams_destroy_r( geosinit()->ctxt, params ); + geos.reset( GEOSMakeValidWithParams_r( context, mGeos.get(), params ) ); + GEOSMakeValidParams_destroy_r( context, params ); } catch ( GEOSException &e ) { @@ -209,7 +215,7 @@ std::unique_ptr QgsGeos::makeValid( Qgis::MakeValidMethod m { *errorMsg = e.what(); } - GEOSMakeValidParams_destroy_r( geosinit()->ctxt, params ); + GEOSMakeValidParams_destroy_r( context, params ); return nullptr; } #endif @@ -258,7 +264,7 @@ void QgsGeos::prepareGeometry() } if ( mGeos ) { - mGeosPrepared.reset( GEOSPrepare_r( geosinit()->ctxt, mGeos.get() ) ); + mGeosPrepared.reset( GEOSPrepare_r( QgsGeosContext::get(), mGeos.get() ) ); } } @@ -296,7 +302,7 @@ std::unique_ptr QgsGeos::clip( const QgsRectangle &rect, QS try { - geos::unique_ptr opGeom( GEOSClipByRect_r( geosinit()->ctxt, mGeos.get(), rect.xMinimum(), rect.yMinimum(), rect.xMaximum(), rect.yMaximum() ) ); + geos::unique_ptr opGeom( GEOSClipByRect_r( QgsGeosContext::get(), mGeos.get(), rect.xMinimum(), rect.yMinimum(), rect.xMaximum(), rect.yMaximum() ) ); return fromGeos( opGeom.get() ); } catch ( GEOSException &e ) @@ -310,12 +316,10 @@ std::unique_ptr QgsGeos::clip( const QgsRectangle &rect, QS } } - - - void QgsGeos::subdivideRecursive( const GEOSGeometry *currentPart, int maxNodes, int depth, QgsGeometryCollection *parts, const QgsRectangle &clipRect, double gridSize ) const { - int partType = GEOSGeomTypeId_r( geosinit()->ctxt, currentPart ); + GEOSContextHandle_t context = QgsGeosContext::get(); + int partType = GEOSGeomTypeId_r( context, currentPart ); if ( qgsDoubleNear( clipRect.width(), 0.0 ) && qgsDoubleNear( clipRect.height(), 0.0 ) ) { if ( partType == GEOS_POINT ) @@ -331,10 +335,10 @@ void QgsGeos::subdivideRecursive( const GEOSGeometry *currentPart, int maxNodes, if ( partType == GEOS_MULTILINESTRING || partType == GEOS_MULTIPOLYGON || partType == GEOS_GEOMETRYCOLLECTION ) { - int partCount = GEOSGetNumGeometries_r( geosinit()->ctxt, currentPart ); + int partCount = GEOSGetNumGeometries_r( context, currentPart ); for ( int i = 0; i < partCount; ++i ) { - subdivideRecursive( GEOSGetGeometryN_r( geosinit()->ctxt, currentPart, i ), maxNodes, depth, parts, clipRect, gridSize ); + subdivideRecursive( GEOSGetGeometryN_r( context, currentPart, i ), maxNodes, depth, parts, clipRect, gridSize ); } return; } @@ -345,7 +349,7 @@ void QgsGeos::subdivideRecursive( const GEOSGeometry *currentPart, int maxNodes, return; } - int vertexCount = GEOSGetNumCoordinates_r( geosinit()->ctxt, currentPart ); + int vertexCount = GEOSGetNumCoordinates_r( context, currentPart ); if ( vertexCount == 0 ) { return; @@ -387,8 +391,8 @@ void QgsGeos::subdivideRecursive( const GEOSGeometry *currentPart, int maxNodes, halfClipRect2.setXMaximum( halfClipRect2.xMaximum() + std::numeric_limits::epsilon() ); } - geos::unique_ptr clipPart1( GEOSClipByRect_r( geosinit()->ctxt, currentPart, halfClipRect1.xMinimum(), halfClipRect1.yMinimum(), halfClipRect1.xMaximum(), halfClipRect1.yMaximum() ) ); - geos::unique_ptr clipPart2( GEOSClipByRect_r( geosinit()->ctxt, currentPart, halfClipRect2.xMinimum(), halfClipRect2.yMinimum(), halfClipRect2.xMaximum(), halfClipRect2.yMaximum() ) ); + geos::unique_ptr clipPart1( GEOSClipByRect_r( context, currentPart, halfClipRect1.xMinimum(), halfClipRect1.yMinimum(), halfClipRect1.xMaximum(), halfClipRect1.yMaximum() ) ); + geos::unique_ptr clipPart2( GEOSClipByRect_r( context, currentPart, halfClipRect2.xMinimum(), halfClipRect2.yMinimum(), halfClipRect2.xMaximum(), halfClipRect2.yMaximum() ) ); ++depth; @@ -396,7 +400,7 @@ void QgsGeos::subdivideRecursive( const GEOSGeometry *currentPart, int maxNodes, { if ( gridSize > 0 ) { - clipPart1.reset( GEOSIntersectionPrec_r( geosinit()->ctxt, mGeos.get(), clipPart1.get(), gridSize ) ); + clipPart1.reset( GEOSIntersectionPrec_r( context, mGeos.get(), clipPart1.get(), gridSize ) ); } subdivideRecursive( clipPart1.get(), maxNodes, depth, parts, halfClipRect1, gridSize ); } @@ -404,7 +408,7 @@ void QgsGeos::subdivideRecursive( const GEOSGeometry *currentPart, int maxNodes, { if ( gridSize > 0 ) { - clipPart2.reset( GEOSIntersectionPrec_r( geosinit()->ctxt, mGeos.get(), clipPart2.get(), gridSize ) ); + clipPart2.reset( GEOSIntersectionPrec_r( context, mGeos.get(), clipPart2.get(), gridSize ) ); } subdivideRecursive( clipPart2.get(), maxNodes, depth, parts, halfClipRect2, gridSize ); } @@ -447,17 +451,18 @@ QgsAbstractGeometry *QgsGeos::combine( const QVector &geo geosGeometries.emplace_back( asGeos( g, mPrecision ) ); } + GEOSContextHandle_t context = QgsGeosContext::get(); geos::unique_ptr geomUnion; try { geos::unique_ptr geomCollection = createGeosCollection( GEOS_GEOMETRYCOLLECTION, geosGeometries ); if ( parameters.gridSize() > 0 ) { - geomUnion.reset( GEOSUnaryUnionPrec_r( geosinit()->ctxt, geomCollection.get(), parameters.gridSize() ) ); + geomUnion.reset( GEOSUnaryUnionPrec_r( context, geomCollection.get(), parameters.gridSize() ) ); } else { - geomUnion.reset( GEOSUnaryUnion_r( geosinit()->ctxt, geomCollection.get() ) ); + geomUnion.reset( GEOSUnaryUnion_r( context, geomCollection.get() ) ); } } CATCH_GEOS_WITH_ERRMSG( nullptr ) @@ -478,6 +483,7 @@ QgsAbstractGeometry *QgsGeos::combine( const QVector &geomList, QSt geosGeometries.emplace_back( asGeos( g.constGet(), mPrecision ) ); } + GEOSContextHandle_t context = QgsGeosContext::get(); geos::unique_ptr geomUnion; try { @@ -485,11 +491,11 @@ QgsAbstractGeometry *QgsGeos::combine( const QVector &geomList, QSt if ( parameters.gridSize() > 0 ) { - geomUnion.reset( GEOSUnaryUnionPrec_r( geosinit()->ctxt, geomCollection.get(), parameters.gridSize() ) ); + geomUnion.reset( GEOSUnaryUnionPrec_r( context, geomCollection.get(), parameters.gridSize() ) ); } else { - geomUnion.reset( GEOSUnaryUnion_r( geosinit()->ctxt, geomCollection.get() ) ); + geomUnion.reset( GEOSUnaryUnion_r( context, geomCollection.get() ) ); } } @@ -518,15 +524,16 @@ double QgsGeos::distance( const QgsAbstractGeometry *geom, QString *errorMsg ) c return distance; } + GEOSContextHandle_t context = QgsGeosContext::get(); try { if ( mGeosPrepared ) { - GEOSPreparedDistance_r( geosinit()->ctxt, mGeosPrepared.get(), otherGeosGeom.get(), &distance ); + GEOSPreparedDistance_r( context, mGeosPrepared.get(), otherGeosGeom.get(), &distance ); } else { - GEOSDistance_r( geosinit()->ctxt, mGeos.get(), otherGeosGeom.get(), &distance ); + GEOSDistance_r( context, mGeos.get(), otherGeosGeom.get(), &distance ); } } CATCH_GEOS_WITH_ERRMSG( -1.0 ) @@ -546,15 +553,16 @@ double QgsGeos::distance( double x, double y, QString *errorMsg ) const if ( !point ) return distance; + GEOSContextHandle_t context = QgsGeosContext::get(); try { if ( mGeosPrepared ) { - GEOSPreparedDistance_r( geosinit()->ctxt, mGeosPrepared.get(), point.get(), &distance ); + GEOSPreparedDistance_r( context, mGeosPrepared.get(), point.get(), &distance ); } else { - GEOSDistance_r( geosinit()->ctxt, mGeos.get(), point.get(), &distance ); + GEOSDistance_r( context, mGeos.get(), point.get(), &distance ); } } CATCH_GEOS_WITH_ERRMSG( -1.0 ) @@ -580,23 +588,23 @@ bool QgsGeos::distanceWithin( const QgsAbstractGeometry *geom, double maxdist, Q // distance double distance; - + GEOSContextHandle_t context = QgsGeosContext::get(); try { if ( mGeosPrepared ) { #if GEOS_VERSION_MAJOR>3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR>=10 ) - return GEOSPreparedDistanceWithin_r( geosinit()->ctxt, mGeosPrepared.get(), otherGeosGeom.get(), maxdist ); + return GEOSPreparedDistanceWithin_r( context, mGeosPrepared.get(), otherGeosGeom.get(), maxdist ); #else - GEOSPreparedDistance_r( geosinit()->ctxt, mGeosPrepared.get(), otherGeosGeom.get(), &distance ); + GEOSPreparedDistance_r( context, mGeosPrepared.get(), otherGeosGeom.get(), &distance ); #endif } else { #if GEOS_VERSION_MAJOR>3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR>=10 ) - return GEOSDistanceWithin_r( geosinit()->ctxt, mGeos.get(), otherGeosGeom.get(), maxdist ); + return GEOSDistanceWithin_r( context, mGeos.get(), otherGeosGeom.get(), maxdist ); #else - GEOSDistance_r( geosinit()->ctxt, mGeos.get(), otherGeosGeom.get(), &distance ); + GEOSDistance_r( context, mGeos.get(), otherGeosGeom.get(), &distance ); #endif } } @@ -612,14 +620,15 @@ bool QgsGeos::contains( double x, double y, QString *errorMsg ) const return false; bool result = false; + GEOSContextHandle_t context = QgsGeosContext::get(); try { if ( mGeosPrepared ) //use faster version with prepared geometry { - return GEOSPreparedContains_r( geosinit()->ctxt, mGeosPrepared.get(), point.get() ) == 1; + return GEOSPreparedContains_r( context, mGeosPrepared.get(), point.get() ) == 1; } - result = ( GEOSContains_r( geosinit()->ctxt, mGeos.get(), point.get() ) == 1 ); + result = ( GEOSContains_r( context, mGeos.get(), point.get() ) == 1 ); } catch ( GEOSException &e ) { @@ -650,7 +659,7 @@ double QgsGeos::hausdorffDistance( const QgsAbstractGeometry *geom, QString *err try { - GEOSHausdorffDistance_r( geosinit()->ctxt, mGeos.get(), otherGeosGeom.get(), &distance ); + GEOSHausdorffDistance_r( QgsGeosContext::get(), mGeos.get(), otherGeosGeom.get(), &distance ); } CATCH_GEOS_WITH_ERRMSG( -1.0 ) @@ -673,7 +682,7 @@ double QgsGeos::hausdorffDistanceDensify( const QgsAbstractGeometry *geom, doubl try { - GEOSHausdorffDistanceDensify_r( geosinit()->ctxt, mGeos.get(), otherGeosGeom.get(), densifyFraction, &distance ); + GEOSHausdorffDistanceDensify_r( QgsGeosContext::get(), mGeos.get(), otherGeosGeom.get(), densifyFraction, &distance ); } CATCH_GEOS_WITH_ERRMSG( -1.0 ) @@ -696,7 +705,7 @@ double QgsGeos::frechetDistance( const QgsAbstractGeometry *geom, QString *error try { - GEOSFrechetDistance_r( geosinit()->ctxt, mGeos.get(), otherGeosGeom.get(), &distance ); + GEOSFrechetDistance_r( QgsGeosContext::get(), mGeos.get(), otherGeosGeom.get(), &distance ); } CATCH_GEOS_WITH_ERRMSG( -1.0 ) @@ -719,7 +728,7 @@ double QgsGeos::frechetDistanceDensify( const QgsAbstractGeometry *geom, double try { - GEOSFrechetDistanceDensify_r( geosinit()->ctxt, mGeos.get(), otherGeosGeom.get(), densifyFraction, &distance ); + GEOSFrechetDistanceDensify_r( QgsGeosContext::get(), mGeos.get(), otherGeosGeom.get(), densifyFraction, &distance ); } CATCH_GEOS_WITH_ERRMSG( -1.0 ) @@ -775,13 +784,14 @@ QString QgsGeos::relate( const QgsAbstractGeometry *geom, QString *errorMsg ) co } QString result; + GEOSContextHandle_t context = QgsGeosContext::get(); try { - char *r = GEOSRelate_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ); + char *r = GEOSRelate_r( context, mGeos.get(), geosGeom.get() ); if ( r ) { result = QString( r ); - GEOSFree_r( geosinit()->ctxt, r ); + GEOSFree_r( context, r ); } } catch ( GEOSException &e ) @@ -810,9 +820,10 @@ bool QgsGeos::relatePattern( const QgsAbstractGeometry *geom, const QString &pat } bool result = false; + GEOSContextHandle_t context = QgsGeosContext::get(); try { - result = ( GEOSRelatePattern_r( geosinit()->ctxt, mGeos.get(), geosGeom.get(), pattern.toLocal8Bit().constData() ) == 1 ); + result = ( GEOSRelatePattern_r( context, mGeos.get(), geosGeom.get(), pattern.toLocal8Bit().constData() ) == 1 ); } catch ( GEOSException &e ) { @@ -836,7 +847,7 @@ double QgsGeos::area( QString *errorMsg ) const try { - if ( GEOSArea_r( geosinit()->ctxt, mGeos.get(), &area ) != 1 ) + if ( GEOSArea_r( QgsGeosContext::get(), mGeos.get(), &area ) != 1 ) return -1.0; } CATCH_GEOS_WITH_ERRMSG( -1.0 ) @@ -852,7 +863,7 @@ double QgsGeos::length( QString *errorMsg ) const } try { - if ( GEOSLength_r( geosinit()->ctxt, mGeos.get(), &length ) != 1 ) + if ( GEOSLength_r( QgsGeosContext::get(), mGeos.get(), &length ) != 1 ) return -1.0; } CATCH_GEOS_WITH_ERRMSG( -1.0 ) @@ -878,7 +889,8 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitGeometry( const QgsLineSt return SplitCannotSplitPoint; //cannot split points } - if ( !GEOSisValid_r( geosinit()->ctxt, mGeos.get() ) ) + GEOSContextHandle_t context = QgsGeosContext::get(); + if ( !GEOSisValid_r( context, mGeos.get() ) ) return InvalidBaseGeometry; //make sure splitLine is valid @@ -904,7 +916,7 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitGeometry( const QgsLineSt return InvalidInput; } - if ( !GEOSisValid_r( geosinit()->ctxt, splitLineGeos.get() ) || !GEOSisSimple_r( geosinit()->ctxt, splitLineGeos.get() ) ) + if ( !GEOSisValid_r( context, splitLineGeos.get() ) || !GEOSisSimple_r( context, splitLineGeos.get() ) ) { return InvalidInput; } @@ -950,21 +962,22 @@ bool QgsGeos::topologicalTestPointsSplit( const GEOSGeometry *splitLine, QgsPoin return false; } + GEOSContextHandle_t context = QgsGeosContext::get(); try { testPoints.clear(); - geos::unique_ptr intersectionGeom( GEOSIntersection_r( geosinit()->ctxt, mGeos.get(), splitLine ) ); + geos::unique_ptr intersectionGeom( GEOSIntersection_r( context, mGeos.get(), splitLine ) ); if ( !intersectionGeom ) return false; bool simple = false; int nIntersectGeoms = 1; - if ( GEOSGeomTypeId_r( geosinit()->ctxt, intersectionGeom.get() ) == GEOS_LINESTRING - || GEOSGeomTypeId_r( geosinit()->ctxt, intersectionGeom.get() ) == GEOS_POINT ) + if ( GEOSGeomTypeId_r( context, intersectionGeom.get() ) == GEOS_LINESTRING + || GEOSGeomTypeId_r( context, intersectionGeom.get() ) == GEOS_POINT ) simple = true; if ( !simple ) - nIntersectGeoms = GEOSGetNumGeometries_r( geosinit()->ctxt, intersectionGeom.get() ); + nIntersectGeoms = GEOSGetNumGeometries_r( context, intersectionGeom.get() ); for ( int i = 0; i < nIntersectGeoms; ++i ) { @@ -972,16 +985,16 @@ bool QgsGeos::topologicalTestPointsSplit( const GEOSGeometry *splitLine, QgsPoin if ( simple ) currentIntersectGeom = intersectionGeom.get(); else - currentIntersectGeom = GEOSGetGeometryN_r( geosinit()->ctxt, intersectionGeom.get(), i ); + currentIntersectGeom = GEOSGetGeometryN_r( context, intersectionGeom.get(), i ); - const GEOSCoordSequence *lineSequence = GEOSGeom_getCoordSeq_r( geosinit()->ctxt, currentIntersectGeom ); + const GEOSCoordSequence *lineSequence = GEOSGeom_getCoordSeq_r( context, currentIntersectGeom ); unsigned int sequenceSize = 0; double x, y, z; - if ( GEOSCoordSeq_getSize_r( geosinit()->ctxt, lineSequence, &sequenceSize ) != 0 ) + if ( GEOSCoordSeq_getSize_r( context, lineSequence, &sequenceSize ) != 0 ) { for ( unsigned int i = 0; i < sequenceSize; ++i ) { - if ( GEOSCoordSeq_getXYZ_r( geosinit()->ctxt, lineSequence, i, &x, &y, &z ) ) + if ( GEOSCoordSeq_getXYZ_r( context, lineSequence, i, &x, &y, &z ) ) { testPoints.push_back( QgsPoint( x, y, z ) ); } @@ -996,7 +1009,8 @@ bool QgsGeos::topologicalTestPointsSplit( const GEOSGeometry *splitLine, QgsPoin geos::unique_ptr QgsGeos::linePointDifference( GEOSGeometry *GEOSsplitPoint ) const { - int type = GEOSGeomTypeId_r( geosinit()->ctxt, mGeos.get() ); + GEOSContextHandle_t context = QgsGeosContext::get(); + int type = GEOSGeomTypeId_r( context, mGeos.get() ); std::unique_ptr< QgsMultiCurve > multiCurve; if ( type == GEOS_MULTILINESTRING ) @@ -1145,14 +1159,14 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitLinearGeometry( const GEO if ( !mGeos ) return InvalidBaseGeometry; - GEOSContextHandle_t geosctxt = geosinit()->ctxt; + GEOSContextHandle_t context = QgsGeosContext::get(); - geos::unique_ptr intersectGeom( GEOSIntersection_r( geosctxt, splitLine, mGeos.get() ) ); - if ( !intersectGeom || GEOSisEmpty_r( geosctxt, intersectGeom.get() ) ) + geos::unique_ptr intersectGeom( GEOSIntersection_r( context, splitLine, mGeos.get() ) ); + if ( !intersectGeom || GEOSisEmpty_r( context, intersectGeom.get() ) ) return NothingHappened; //check that split line has no linear intersection - const int linearIntersect = GEOSRelatePattern_r( geosctxt, mGeos.get(), splitLine, "1********" ); + const int linearIntersect = GEOSRelatePattern_r( context, mGeos.get(), splitLine, "1********" ); if ( linearIntersect > 0 ) return InvalidInput; @@ -1163,18 +1177,18 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitLinearGeometry( const GEO std::vector lineGeoms; - const int splitType = GEOSGeomTypeId_r( geosctxt, splitGeom.get() ); + const int splitType = GEOSGeomTypeId_r( context, splitGeom.get() ); if ( splitType == GEOS_MULTILINESTRING ) { - const int nGeoms = GEOSGetNumGeometries_r( geosctxt, splitGeom.get() ); + const int nGeoms = GEOSGetNumGeometries_r( context, splitGeom.get() ); lineGeoms.reserve( nGeoms ); for ( int i = 0; i < nGeoms; ++i ) - lineGeoms.emplace_back( GEOSGeom_clone_r( geosctxt, GEOSGetGeometryN_r( geosctxt, splitGeom.get(), i ) ) ); + lineGeoms.emplace_back( GEOSGeom_clone_r( context, GEOSGetGeometryN_r( context, splitGeom.get(), i ) ) ); } else { - lineGeoms.emplace_back( GEOSGeom_clone_r( geosctxt, splitGeom.get() ) ); + lineGeoms.emplace_back( GEOSGeom_clone_r( context, splitGeom.get() ) ); } mergeGeometriesMultiTypeSplit( lineGeoms ); @@ -1200,10 +1214,10 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitPolygonGeometry( const GE if ( !mGeosPrepared ) return EngineError; - GEOSContextHandle_t geosctxt = geosinit()->ctxt; + GEOSContextHandle_t context = QgsGeosContext::get(); //first test if linestring intersects geometry. If not, return straight away - if ( !skipIntersectionCheck && !GEOSPreparedIntersects_r( geosctxt, mGeosPrepared.get(), splitLine ) ) + if ( !skipIntersectionCheck && !GEOSPreparedIntersects_r( context, mGeosPrepared.get(), splitLine ) ) return NothingHappened; //first union all the polygon rings together (to get them noded, see JTS developer guide) @@ -1212,7 +1226,7 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitPolygonGeometry( const GE return NodedGeometryError; //an error occurred during noding const GEOSGeometry *noded = nodedGeometry.get(); - geos::unique_ptr polygons( GEOSPolygonize_r( geosctxt, &noded, 1 ) ); + geos::unique_ptr polygons( GEOSPolygonize_r( context, &noded, 1 ) ); if ( !polygons ) { return InvalidBaseGeometry; @@ -1232,11 +1246,11 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitPolygonGeometry( const GE // those would be also returned by polygonize and we need to skip them) for ( int i = 0; i < numberOfGeometriesPolygon; i++ ) { - const GEOSGeometry *polygon = GEOSGetGeometryN_r( geosctxt, polygons.get(), i ); + const GEOSGeometry *polygon = GEOSGetGeometryN_r( context, polygons.get(), i ); - geos::unique_ptr pointOnSurface( GEOSPointOnSurface_r( geosctxt, polygon ) ); - if ( pointOnSurface && GEOSPreparedIntersects_r( geosctxt, mGeosPrepared.get(), pointOnSurface.get() ) ) - testedGeometries.emplace_back( GEOSGeom_clone_r( geosctxt, polygon ) ); + geos::unique_ptr pointOnSurface( GEOSPointOnSurface_r( context, polygon ) ); + if ( pointOnSurface && GEOSPreparedIntersects_r( context, mGeosPrepared.get(), pointOnSurface.get() ) ) + testedGeometries.emplace_back( GEOSGeom_clone_r( context, polygon ) ); } const size_t nGeometriesThis = numberOfGeometries( mGeos.get() ); //original number of geometries @@ -1253,7 +1267,7 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitPolygonGeometry( const GE mergeGeometriesMultiTypeSplit( testedGeometries ); size_t i; - for ( i = 0; i < testedGeometries.size() && GEOSisValid_r( geosctxt, testedGeometries[i].get() ); ++i ) + for ( i = 0; i < testedGeometries.size() && GEOSisValid_r( context, testedGeometries[i].get() ); ++i ) ; if ( i < testedGeometries.size() ) @@ -1275,13 +1289,14 @@ geos::unique_ptr QgsGeos::nodeGeometries( const GEOSGeometry *splitLine, const G return nullptr; geos::unique_ptr geometryBoundary; - if ( GEOSGeomTypeId_r( geosinit()->ctxt, geom ) == GEOS_POLYGON || GEOSGeomTypeId_r( geosinit()->ctxt, geom ) == GEOS_MULTIPOLYGON ) - geometryBoundary.reset( GEOSBoundary_r( geosinit()->ctxt, geom ) ); + GEOSContextHandle_t context = QgsGeosContext::get(); + if ( GEOSGeomTypeId_r( context, geom ) == GEOS_POLYGON || GEOSGeomTypeId_r( context, geom ) == GEOS_MULTIPOLYGON ) + geometryBoundary.reset( GEOSBoundary_r( context, geom ) ); else - geometryBoundary.reset( GEOSGeom_clone_r( geosinit()->ctxt, geom ) ); + geometryBoundary.reset( GEOSGeom_clone_r( context, geom ) ); - geos::unique_ptr splitLineClone( GEOSGeom_clone_r( geosinit()->ctxt, splitLine ) ); - geos::unique_ptr unionGeometry( GEOSUnion_r( geosinit()->ctxt, splitLineClone.get(), geometryBoundary.get() ) ); + geos::unique_ptr splitLineClone( GEOSGeom_clone_r( context, splitLine ) ); + geos::unique_ptr unionGeometry( GEOSUnion_r( context, splitLineClone.get(), geometryBoundary.get() ) ); return unionGeometry; } @@ -1292,7 +1307,8 @@ int QgsGeos::mergeGeometriesMultiTypeSplit( std::vector &split return 1; //convert mGeos to geometry collection - int type = GEOSGeomTypeId_r( geosinit()->ctxt, mGeos.get() ); + GEOSContextHandle_t context = QgsGeosContext::get(); + int type = GEOSGeomTypeId_r( context, mGeos.get() ); if ( type != GEOS_GEOMETRYCOLLECTION && type != GEOS_MULTILINESTRING && type != GEOS_MULTIPOLYGON && @@ -1308,9 +1324,9 @@ int QgsGeos::mergeGeometriesMultiTypeSplit( std::vector &split { //is this geometry a part of the original multitype? bool isPart = false; - for ( int j = 0; j < GEOSGetNumGeometries_r( geosinit()->ctxt, mGeos.get() ); j++ ) + for ( int j = 0; j < GEOSGetNumGeometries_r( context, mGeos.get() ); j++ ) { - if ( GEOSEquals_r( geosinit()->ctxt, splitResult[i].get(), GEOSGetGeometryN_r( geosinit()->ctxt, mGeos.get(), j ) ) ) + if ( GEOSEquals_r( context, splitResult[i].get(), GEOSGetGeometryN_r( context, mGeos.get(), j ) ) ) { isPart = true; break; @@ -1352,11 +1368,12 @@ geos::unique_ptr QgsGeos::createGeosCollection( int typeId, std::vector geomarr; geomarr.reserve( geoms.size() ); + GEOSContextHandle_t context = QgsGeosContext::get(); for ( geos::unique_ptr &geomUniquePtr : geoms ) { if ( geomUniquePtr ) { - if ( !GEOSisEmpty_r( geosinit()->ctxt, geomUniquePtr.get() ) ) + if ( !GEOSisEmpty_r( context, geomUniquePtr.get() ) ) { // don't add empty parts to a geos collection, it can cause crashes in GEOS // transfer ownership of the geometries to GEOSGeom_createCollection_r() @@ -1368,13 +1385,13 @@ geos::unique_ptr QgsGeos::createGeosCollection( int typeId, std::vectorctxt, typeId, geomarr.data(), geomarr.size() ) ); + geomRes.reset( GEOSGeom_createCollection_r( context, typeId, geomarr.data(), geomarr.size() ) ); } catch ( GEOSException & ) { for ( GEOSGeometry *geom : geomarr ) { - GEOSGeom_destroy_r( geosinit()->ctxt, geom ); + GEOSGeom_destroy_r( context, geom ); } } @@ -1388,21 +1405,22 @@ std::unique_ptr QgsGeos::fromGeos( const GEOSGeometry *geos return nullptr; } - int nCoordDims = GEOSGeom_getCoordinateDimension_r( geosinit()->ctxt, geos ); - int nDims = GEOSGeom_getDimensions_r( geosinit()->ctxt, geos ); + GEOSContextHandle_t context = QgsGeosContext::get(); + int nCoordDims = GEOSGeom_getCoordinateDimension_r( context, geos ); + int nDims = GEOSGeom_getDimensions_r( context, geos ); bool hasZ = ( nCoordDims == 3 ); bool hasM = ( ( nDims - nCoordDims ) == 1 ); - switch ( GEOSGeomTypeId_r( geosinit()->ctxt, geos ) ) + switch ( GEOSGeomTypeId_r( context, geos ) ) { case GEOS_POINT: // a point { - if ( GEOSisEmpty_r( geosinit()->ctxt, geos ) ) + if ( GEOSisEmpty_r( context, geos ) ) return nullptr; - const GEOSCoordSequence *cs = GEOSGeom_getCoordSeq_r( geosinit()->ctxt, geos ); + const GEOSCoordSequence *cs = GEOSGeom_getCoordSeq_r( context, geos ); unsigned int nPoints = 0; - GEOSCoordSeq_getSize_r( geosinit()->ctxt, cs, &nPoints ); + GEOSCoordSeq_getSize_r( context, cs, &nPoints ); return nPoints > 0 ? std::unique_ptr( coordSeqPoint( cs, 0, hasZ, hasM ).clone() ) : nullptr; } case GEOS_LINESTRING: @@ -1416,15 +1434,15 @@ std::unique_ptr QgsGeos::fromGeos( const GEOSGeometry *geos case GEOS_MULTIPOINT: { std::unique_ptr< QgsMultiPoint > multiPoint( new QgsMultiPoint() ); - int nParts = GEOSGetNumGeometries_r( geosinit()->ctxt, geos ); + int nParts = GEOSGetNumGeometries_r( context, geos ); multiPoint->reserve( nParts ); for ( int i = 0; i < nParts; ++i ) { - const GEOSCoordSequence *cs = GEOSGeom_getCoordSeq_r( geosinit()->ctxt, GEOSGetGeometryN_r( geosinit()->ctxt, geos, i ) ); + const GEOSCoordSequence *cs = GEOSGeom_getCoordSeq_r( context, GEOSGetGeometryN_r( context, geos, i ) ); if ( cs ) { unsigned int nPoints = 0; - GEOSCoordSeq_getSize_r( geosinit()->ctxt, cs, &nPoints ); + GEOSCoordSeq_getSize_r( context, cs, &nPoints ); if ( nPoints > 0 ) multiPoint->addGeometry( coordSeqPoint( cs, 0, hasZ, hasM ).clone() ); } @@ -1434,11 +1452,11 @@ std::unique_ptr QgsGeos::fromGeos( const GEOSGeometry *geos case GEOS_MULTILINESTRING: { std::unique_ptr< QgsMultiLineString > multiLineString( new QgsMultiLineString() ); - int nParts = GEOSGetNumGeometries_r( geosinit()->ctxt, geos ); + int nParts = GEOSGetNumGeometries_r( context, geos ); multiLineString->reserve( nParts ); for ( int i = 0; i < nParts; ++i ) { - std::unique_ptr< QgsLineString >line( sequenceToLinestring( GEOSGetGeometryN_r( geosinit()->ctxt, geos, i ), hasZ, hasM ) ); + std::unique_ptr< QgsLineString >line( sequenceToLinestring( GEOSGetGeometryN_r( context, geos, i ), hasZ, hasM ) ); if ( line ) { multiLineString->addGeometry( line.release() ); @@ -1450,11 +1468,11 @@ std::unique_ptr QgsGeos::fromGeos( const GEOSGeometry *geos { std::unique_ptr< QgsMultiPolygon > multiPolygon( new QgsMultiPolygon() ); - int nParts = GEOSGetNumGeometries_r( geosinit()->ctxt, geos ); + int nParts = GEOSGetNumGeometries_r( context, geos ); multiPolygon->reserve( nParts ); for ( int i = 0; i < nParts; ++i ) { - std::unique_ptr< QgsPolygon > poly = fromGeosPolygon( GEOSGetGeometryN_r( geosinit()->ctxt, geos, i ) ); + std::unique_ptr< QgsPolygon > poly = fromGeosPolygon( GEOSGetGeometryN_r( context, geos, i ) ); if ( poly ) { multiPolygon->addGeometry( poly.release() ); @@ -1465,11 +1483,11 @@ std::unique_ptr QgsGeos::fromGeos( const GEOSGeometry *geos case GEOS_GEOMETRYCOLLECTION: { std::unique_ptr< QgsGeometryCollection > geomCollection( new QgsGeometryCollection() ); - int nParts = GEOSGetNumGeometries_r( geosinit()->ctxt, geos ); + int nParts = GEOSGetNumGeometries_r( context, geos ); geomCollection->reserve( nParts ); for ( int i = 0; i < nParts; ++i ) { - std::unique_ptr< QgsAbstractGeometry > geom( fromGeos( GEOSGetGeometryN_r( geosinit()->ctxt, geos, i ) ) ); + std::unique_ptr< QgsAbstractGeometry > geom( fromGeos( GEOSGetGeometryN_r( context, geos, i ) ) ); if ( geom ) { geomCollection->addGeometry( geom.release() ); @@ -1483,30 +1501,31 @@ std::unique_ptr QgsGeos::fromGeos( const GEOSGeometry *geos std::unique_ptr QgsGeos::fromGeosPolygon( const GEOSGeometry *geos ) { - if ( GEOSGeomTypeId_r( geosinit()->ctxt, geos ) != GEOS_POLYGON ) + GEOSContextHandle_t context = QgsGeosContext::get(); + if ( GEOSGeomTypeId_r( context, geos ) != GEOS_POLYGON ) { return nullptr; } - int nCoordDims = GEOSGeom_getCoordinateDimension_r( geosinit()->ctxt, geos ); - int nDims = GEOSGeom_getDimensions_r( geosinit()->ctxt, geos ); + int nCoordDims = GEOSGeom_getCoordinateDimension_r( context, geos ); + int nDims = GEOSGeom_getDimensions_r( context, geos ); bool hasZ = ( nCoordDims == 3 ); bool hasM = ( ( nDims - nCoordDims ) == 1 ); std::unique_ptr< QgsPolygon > polygon( new QgsPolygon() ); - const GEOSGeometry *ring = GEOSGetExteriorRing_r( geosinit()->ctxt, geos ); + const GEOSGeometry *ring = GEOSGetExteriorRing_r( context, geos ); if ( ring ) { polygon->setExteriorRing( sequenceToLinestring( ring, hasZ, hasM ).release() ); } QVector interiorRings; - const int ringCount = GEOSGetNumInteriorRings_r( geosinit()->ctxt, geos ); + const int ringCount = GEOSGetNumInteriorRings_r( context, geos ); interiorRings.reserve( ringCount ); for ( int i = 0; i < ringCount; ++i ) { - ring = GEOSGetInteriorRingN_r( geosinit()->ctxt, geos, i ); + ring = GEOSGetInteriorRingN_r( context, geos, i ); if ( ring ) { interiorRings.push_back( sequenceToLinestring( ring, hasZ, hasM ).release() ); @@ -1519,10 +1538,11 @@ std::unique_ptr QgsGeos::fromGeosPolygon( const GEOSGeometry *geos ) std::unique_ptr QgsGeos::sequenceToLinestring( const GEOSGeometry *geos, bool hasZ, bool hasM ) { - const GEOSCoordSequence *cs = GEOSGeom_getCoordSeq_r( geosinit()->ctxt, geos ); + GEOSContextHandle_t context = QgsGeosContext::get(); + const GEOSCoordSequence *cs = GEOSGeom_getCoordSeq_r( context, geos ); unsigned int nPoints; - GEOSCoordSeq_getSize_r( geosinit()->ctxt, cs, &nPoints ); + GEOSCoordSeq_getSize_r( context, cs, &nPoints ); QVector< double > xOut( nPoints ); QVector< double > yOut( nPoints ); @@ -1539,17 +1559,17 @@ std::unique_ptr QgsGeos::sequenceToLinestring( const GEOSGeometry double *m = mOut.data(); #if GEOS_VERSION_MAJOR>3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR>=10 ) - GEOSCoordSeq_copyToArrays_r( geosinit()->ctxt, cs, x, y, hasZ ? z : nullptr, hasM ? m : nullptr ); + GEOSCoordSeq_copyToArrays_r( context, cs, x, y, hasZ ? z : nullptr, hasM ? m : nullptr ); #else for ( unsigned int i = 0; i < nPoints; ++i ) { if ( hasZ ) - GEOSCoordSeq_getXYZ_r( geosinit()->ctxt, cs, i, x++, y++, z++ ); + GEOSCoordSeq_getXYZ_r( context, cs, i, x++, y++, z++ ); else - GEOSCoordSeq_getXY_r( geosinit()->ctxt, cs, i, x++, y++ ); + GEOSCoordSeq_getXY_r( context, cs, i, x++, y++ ); if ( hasM ) { - GEOSCoordSeq_getOrdinate_r( geosinit()->ctxt, cs, i, 3, m++ ); + GEOSCoordSeq_getOrdinate_r( context, cs, i, 3, m++ ); } } #endif @@ -1562,13 +1582,14 @@ int QgsGeos::numberOfGeometries( GEOSGeometry *g ) if ( !g ) return 0; - int geometryType = GEOSGeomTypeId_r( geosinit()->ctxt, g ); + GEOSContextHandle_t context = QgsGeosContext::get(); + int geometryType = GEOSGeomTypeId_r( context, g ); if ( geometryType == GEOS_POINT || geometryType == GEOS_LINESTRING || geometryType == GEOS_LINEARRING || geometryType == GEOS_POLYGON ) return 1; //calling GEOSGetNumGeometries is save for multi types and collections also in geos2 - return GEOSGetNumGeometries_r( geosinit()->ctxt, g ); + return GEOSGetNumGeometries_r( context, g ); } QgsPoint QgsGeos::coordSeqPoint( const GEOSCoordSequence *cs, int i, bool hasZ, bool hasM ) @@ -1578,16 +1599,18 @@ QgsPoint QgsGeos::coordSeqPoint( const GEOSCoordSequence *cs, int i, bool hasZ, return QgsPoint(); } + GEOSContextHandle_t context = QgsGeosContext::get(); + double x, y; double z = 0; double m = 0; if ( hasZ ) - GEOSCoordSeq_getXYZ_r( geosinit()->ctxt, cs, i, &x, &y, &z ); + GEOSCoordSeq_getXYZ_r( context, cs, i, &x, &y, &z ); else - GEOSCoordSeq_getXY_r( geosinit()->ctxt, cs, i, &x, &y ); + GEOSCoordSeq_getXY_r( context, cs, i, &x, &y ); if ( hasM ) { - GEOSCoordSeq_getOrdinate_r( geosinit()->ctxt, cs, i, 3, &m ); + GEOSCoordSeq_getOrdinate_r( context, cs, i, 3, &m ); } Qgis::WkbType t = Qgis::WkbType::Point; @@ -1702,6 +1725,7 @@ std::unique_ptr QgsGeos::overlay( const QgsAbstractGeometry const double gridSize = parameters.gridSize(); + GEOSContextHandle_t context = QgsGeosContext::get(); try { geos::unique_ptr opGeom; @@ -1710,22 +1734,22 @@ std::unique_ptr QgsGeos::overlay( const QgsAbstractGeometry case OverlayIntersection: if ( gridSize > 0 ) { - opGeom.reset( GEOSIntersectionPrec_r( geosinit()->ctxt, mGeos.get(), geosGeom.get(), gridSize ) ); + opGeom.reset( GEOSIntersectionPrec_r( context, mGeos.get(), geosGeom.get(), gridSize ) ); } else { - opGeom.reset( GEOSIntersection_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) ); + opGeom.reset( GEOSIntersection_r( context, mGeos.get(), geosGeom.get() ) ); } break; case OverlayDifference: if ( gridSize > 0 ) { - opGeom.reset( GEOSDifferencePrec_r( geosinit()->ctxt, mGeos.get(), geosGeom.get(), gridSize ) ); + opGeom.reset( GEOSDifferencePrec_r( context, mGeos.get(), geosGeom.get(), gridSize ) ); } else { - opGeom.reset( GEOSDifference_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) ); + opGeom.reset( GEOSDifference_r( context, mGeos.get(), geosGeom.get() ) ); } break; @@ -1734,16 +1758,16 @@ std::unique_ptr QgsGeos::overlay( const QgsAbstractGeometry geos::unique_ptr unionGeometry; if ( gridSize > 0 ) { - unionGeometry.reset( GEOSUnionPrec_r( geosinit()->ctxt, mGeos.get(), geosGeom.get(), gridSize ) ); + unionGeometry.reset( GEOSUnionPrec_r( context, mGeos.get(), geosGeom.get(), gridSize ) ); } else { - unionGeometry.reset( GEOSUnion_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) ); + unionGeometry.reset( GEOSUnion_r( context, mGeos.get(), geosGeom.get() ) ); } - if ( unionGeometry && GEOSGeomTypeId_r( geosinit()->ctxt, unionGeometry.get() ) == GEOS_MULTILINESTRING ) + if ( unionGeometry && GEOSGeomTypeId_r( context, unionGeometry.get() ) == GEOS_MULTILINESTRING ) { - geos::unique_ptr mergedLines( GEOSLineMerge_r( geosinit()->ctxt, unionGeometry.get() ) ); + geos::unique_ptr mergedLines( GEOSLineMerge_r( context, unionGeometry.get() ) ); if ( mergedLines ) { unionGeometry = std::move( mergedLines ); @@ -1757,11 +1781,11 @@ std::unique_ptr QgsGeos::overlay( const QgsAbstractGeometry case OverlaySymDifference: if ( gridSize > 0 ) { - opGeom.reset( GEOSSymDifferencePrec_r( geosinit()->ctxt, mGeos.get(), geosGeom.get(), gridSize ) ); + opGeom.reset( GEOSSymDifferencePrec_r( context, mGeos.get(), geosGeom.get(), gridSize ) ); } else { - opGeom.reset( GEOSSymDifference_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) ); + opGeom.reset( GEOSSymDifference_r( context, mGeos.get(), geosGeom.get() ) ); } break; } @@ -1791,6 +1815,7 @@ bool QgsGeos::relation( const QgsAbstractGeometry *geom, Relation r, QString *er return false; } + GEOSContextHandle_t context = QgsGeosContext::get(); bool result = false; try { @@ -1799,25 +1824,25 @@ bool QgsGeos::relation( const QgsAbstractGeometry *geom, Relation r, QString *er switch ( r ) { case RelationIntersects: - result = ( GEOSPreparedIntersects_r( geosinit()->ctxt, mGeosPrepared.get(), geosGeom.get() ) == 1 ); + result = ( GEOSPreparedIntersects_r( context, mGeosPrepared.get(), geosGeom.get() ) == 1 ); break; case RelationTouches: - result = ( GEOSPreparedTouches_r( geosinit()->ctxt, mGeosPrepared.get(), geosGeom.get() ) == 1 ); + result = ( GEOSPreparedTouches_r( context, mGeosPrepared.get(), geosGeom.get() ) == 1 ); break; case RelationCrosses: - result = ( GEOSPreparedCrosses_r( geosinit()->ctxt, mGeosPrepared.get(), geosGeom.get() ) == 1 ); + result = ( GEOSPreparedCrosses_r( context, mGeosPrepared.get(), geosGeom.get() ) == 1 ); break; case RelationWithin: - result = ( GEOSPreparedWithin_r( geosinit()->ctxt, mGeosPrepared.get(), geosGeom.get() ) == 1 ); + result = ( GEOSPreparedWithin_r( context, mGeosPrepared.get(), geosGeom.get() ) == 1 ); break; case RelationContains: - result = ( GEOSPreparedContains_r( geosinit()->ctxt, mGeosPrepared.get(), geosGeom.get() ) == 1 ); + result = ( GEOSPreparedContains_r( context, mGeosPrepared.get(), geosGeom.get() ) == 1 ); break; case RelationDisjoint: - result = ( GEOSPreparedDisjoint_r( geosinit()->ctxt, mGeosPrepared.get(), geosGeom.get() ) == 1 ); + result = ( GEOSPreparedDisjoint_r( context, mGeosPrepared.get(), geosGeom.get() ) == 1 ); break; case RelationOverlaps: - result = ( GEOSPreparedOverlaps_r( geosinit()->ctxt, mGeosPrepared.get(), geosGeom.get() ) == 1 ); + result = ( GEOSPreparedOverlaps_r( context, mGeosPrepared.get(), geosGeom.get() ) == 1 ); break; } return result; @@ -1826,25 +1851,25 @@ bool QgsGeos::relation( const QgsAbstractGeometry *geom, Relation r, QString *er switch ( r ) { case RelationIntersects: - result = ( GEOSIntersects_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) == 1 ); + result = ( GEOSIntersects_r( context, mGeos.get(), geosGeom.get() ) == 1 ); break; case RelationTouches: - result = ( GEOSTouches_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) == 1 ); + result = ( GEOSTouches_r( context, mGeos.get(), geosGeom.get() ) == 1 ); break; case RelationCrosses: - result = ( GEOSCrosses_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) == 1 ); + result = ( GEOSCrosses_r( context, mGeos.get(), geosGeom.get() ) == 1 ); break; case RelationWithin: - result = ( GEOSWithin_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) == 1 ); + result = ( GEOSWithin_r( context, mGeos.get(), geosGeom.get() ) == 1 ); break; case RelationContains: - result = ( GEOSContains_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) == 1 ); + result = ( GEOSContains_r( context, mGeos.get(), geosGeom.get() ) == 1 ); break; case RelationDisjoint: - result = ( GEOSDisjoint_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) == 1 ); + result = ( GEOSDisjoint_r( context, mGeos.get(), geosGeom.get() ) == 1 ); break; case RelationOverlaps: - result = ( GEOSOverlaps_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) == 1 ); + result = ( GEOSOverlaps_r( context, mGeos.get(), geosGeom.get() ) == 1 ); break; } } @@ -1871,7 +1896,7 @@ QgsAbstractGeometry *QgsGeos::buffer( double distance, int segments, QString *er geos::unique_ptr geos; try { - geos.reset( GEOSBuffer_r( geosinit()->ctxt, mGeos.get(), distance, segments ) ); + geos.reset( GEOSBuffer_r( QgsGeosContext::get(), mGeos.get(), distance, segments ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ).release(); @@ -1887,7 +1912,7 @@ QgsAbstractGeometry *QgsGeos::buffer( double distance, int segments, Qgis::EndCa geos::unique_ptr geos; try { - geos.reset( GEOSBufferWithStyle_r( geosinit()->ctxt, mGeos.get(), distance, segments, static_cast< int >( endCapStyle ), static_cast< int >( joinStyle ), miterLimit ) ); + geos.reset( GEOSBufferWithStyle_r( QgsGeosContext::get(), mGeos.get(), distance, segments, static_cast< int >( endCapStyle ), static_cast< int >( joinStyle ), miterLimit ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ).release(); @@ -1902,7 +1927,7 @@ QgsAbstractGeometry *QgsGeos::simplify( double tolerance, QString *errorMsg ) co geos::unique_ptr geos; try { - geos.reset( GEOSTopologyPreserveSimplify_r( geosinit()->ctxt, mGeos.get(), tolerance ) ); + geos.reset( GEOSTopologyPreserveSimplify_r( QgsGeosContext::get(), mGeos.get(), tolerance ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ).release(); @@ -1917,7 +1942,7 @@ QgsAbstractGeometry *QgsGeos::interpolate( double distance, QString *errorMsg ) geos::unique_ptr geos; try { - geos.reset( GEOSInterpolate_r( geosinit()->ctxt, mGeos.get(), distance ) ); + geos.reset( GEOSInterpolate_r( QgsGeosContext::get(), mGeos.get(), distance ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ).release(); @@ -1934,15 +1959,16 @@ QgsPoint *QgsGeos::centroid( QString *errorMsg ) const double x; double y; + GEOSContextHandle_t context = QgsGeosContext::get(); try { - geos.reset( GEOSGetCentroid_r( geosinit()->ctxt, mGeos.get() ) ); + geos.reset( GEOSGetCentroid_r( context, mGeos.get() ) ); if ( !geos ) return nullptr; - GEOSGeomGetX_r( geosinit()->ctxt, geos.get(), &x ); - GEOSGeomGetY_r( geosinit()->ctxt, geos.get(), &y ); + GEOSGeomGetX_r( context, geos.get(), &x ); + GEOSGeomGetY_r( context, geos.get(), &y ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) @@ -1958,7 +1984,7 @@ QgsAbstractGeometry *QgsGeos::envelope( QString *errorMsg ) const geos::unique_ptr geos; try { - geos.reset( GEOSEnvelope_r( geosinit()->ctxt, mGeos.get() ) ); + geos.reset( GEOSEnvelope_r( QgsGeosContext::get(), mGeos.get() ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ).release(); @@ -1974,18 +2000,19 @@ QgsPoint *QgsGeos::pointOnSurface( QString *errorMsg ) const double x; double y; + GEOSContextHandle_t context = QgsGeosContext::get(); geos::unique_ptr geos; try { - geos.reset( GEOSPointOnSurface_r( geosinit()->ctxt, mGeos.get() ) ); + geos.reset( GEOSPointOnSurface_r( context, mGeos.get() ) ); - if ( !geos || GEOSisEmpty_r( geosinit()->ctxt, geos.get() ) != 0 ) + if ( !geos || GEOSisEmpty_r( context, geos.get() ) != 0 ) { return nullptr; } - GEOSGeomGetX_r( geosinit()->ctxt, geos.get(), &x ); - GEOSGeomGetY_r( geosinit()->ctxt, geos.get(), &y ); + GEOSGeomGetX_r( context, geos.get(), &x ); + GEOSGeomGetY_r( context, geos.get(), &y ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) @@ -2001,7 +2028,7 @@ QgsAbstractGeometry *QgsGeos::convexHull( QString *errorMsg ) const try { - geos::unique_ptr cHull( GEOSConvexHull_r( geosinit()->ctxt, mGeos.get() ) ); + geos::unique_ptr cHull( GEOSConvexHull_r( QgsGeosContext::get(), mGeos.get() ) ); std::unique_ptr< QgsAbstractGeometry > cHullGeom = fromGeos( cHull.get() ); return cHullGeom.release(); } @@ -2023,7 +2050,7 @@ QgsAbstractGeometry *QgsGeos::concaveHull( double targetPercent, bool allowHoles try { - geos::unique_ptr concaveHull( GEOSConcaveHull_r( geosinit()->ctxt, mGeos.get(), targetPercent, allowHoles ) ); + geos::unique_ptr concaveHull( GEOSConcaveHull_r( QgsGeosContext::get(), mGeos.get(), targetPercent, allowHoles ) ); std::unique_ptr< QgsAbstractGeometry > concaveHullGeom = fromGeos( concaveHull.get() ); return concaveHullGeom.release(); } @@ -2046,17 +2073,18 @@ Qgis::CoverageValidityResult QgsGeos::validateCoverage( double gapWidth, std::un return Qgis::CoverageValidityResult::Error; } + GEOSContextHandle_t context = QgsGeosContext::get(); try { GEOSGeometry *invalidEdgesGeos = nullptr; - const int result = GEOSCoverageIsValid_r( geosinit()->ctxt, mGeos.get(), gapWidth, invalidEdges ? &invalidEdgesGeos : nullptr ); + const int result = GEOSCoverageIsValid_r( context, mGeos.get(), gapWidth, invalidEdges ? &invalidEdgesGeos : nullptr ); if ( invalidEdges && invalidEdgesGeos ) { *invalidEdges = fromGeos( invalidEdgesGeos ); } if ( invalidEdgesGeos ) { - GEOSGeom_destroy_r( geosinit()->ctxt, invalidEdgesGeos ); + GEOSGeom_destroy_r( context, invalidEdgesGeos ); invalidEdgesGeos = nullptr; } @@ -2092,7 +2120,7 @@ std::unique_ptr QgsGeos::simplifyCoverageVW( double toleran try { - geos::unique_ptr simplified( GEOSCoverageSimplifyVW_r( geosinit()->ctxt, mGeos.get(), tolerance, preserveBoundary ? 1 : 0 ) ); + geos::unique_ptr simplified( GEOSCoverageSimplifyVW_r( QgsGeosContext::get(), mGeos.get(), tolerance, preserveBoundary ? 1 : 0 ) ); std::unique_ptr< QgsAbstractGeometry > simplifiedGeom = fromGeos( simplified.get() ); return simplifiedGeom; } @@ -2111,7 +2139,7 @@ std::unique_ptr QgsGeos::unionCoverage( QString *errorMsg ) try { - geos::unique_ptr unioned( GEOSCoverageUnion_r( geosinit()->ctxt, mGeos.get() ) ); + geos::unique_ptr unioned( GEOSCoverageUnion_r( QgsGeosContext::get(), mGeos.get() ) ); std::unique_ptr< QgsAbstractGeometry > result = fromGeos( unioned.get() ); return result; } @@ -2127,18 +2155,19 @@ bool QgsGeos::isValid( QString *errorMsg, const bool allowSelfTouchingHoles, Qgs return false; } + GEOSContextHandle_t context = QgsGeosContext::get(); try { GEOSGeometry *g1 = nullptr; char *r = nullptr; - char res = GEOSisValidDetail_r( geosinit()->ctxt, mGeos.get(), allowSelfTouchingHoles ? GEOSVALID_ALLOW_SELFTOUCHING_RING_FORMING_HOLE : 0, &r, &g1 ); + char res = GEOSisValidDetail_r( context, mGeos.get(), allowSelfTouchingHoles ? GEOSVALID_ALLOW_SELFTOUCHING_RING_FORMING_HOLE : 0, &r, &g1 ); const bool invalid = res != 1; QString error; if ( r ) { error = QString( r ); - GEOSFree_r( geosinit()->ctxt, r ); + GEOSFree_r( context, r ); } if ( invalid && errorMsg ) @@ -2170,7 +2199,7 @@ bool QgsGeos::isValid( QString *errorMsg, const bool allowSelfTouchingHoles, Qgs } else if ( g1 ) { - GEOSGeom_destroy_r( geosinit()->ctxt, g1 ); + GEOSGeom_destroy_r( context, g1 ); } } return !invalid; @@ -2192,7 +2221,7 @@ bool QgsGeos::isEqual( const QgsAbstractGeometry *geom, QString *errorMsg ) cons { return false; } - bool equal = GEOSEquals_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ); + bool equal = GEOSEquals_r( QgsGeosContext::get(), mGeos.get(), geosGeom.get() ); return equal; } CATCH_GEOS_WITH_ERRMSG( false ) @@ -2207,7 +2236,7 @@ bool QgsGeos::isEmpty( QString *errorMsg ) const try { - return GEOSisEmpty_r( geosinit()->ctxt, mGeos.get() ); + return GEOSisEmpty_r( QgsGeosContext::get(), mGeos.get() ); } CATCH_GEOS_WITH_ERRMSG( false ) } @@ -2221,14 +2250,14 @@ bool QgsGeos::isSimple( QString *errorMsg ) const try { - return GEOSisSimple_r( geosinit()->ctxt, mGeos.get() ); + return GEOSisSimple_r( QgsGeosContext::get(), mGeos.get() ); } CATCH_GEOS_WITH_ERRMSG( false ) } GEOSCoordSequence *QgsGeos::createCoordinateSequence( const QgsCurve *curve, double precision, bool forceClose ) { - GEOSContextHandle_t ctxt = geosinit()->ctxt; + GEOSContextHandle_t context = QgsGeosContext::get(); std::unique_ptr< QgsLineString > segmentized; const QgsLineString *line = qgsgeometry_cast( curve ); @@ -2257,7 +2286,7 @@ GEOSCoordSequence *QgsGeos::createCoordinateSequence( const QgsCurve *curve, dou // use optimised method if we don't have to force close an open ring try { - coordSeq = GEOSCoordSeq_copyFromArrays_r( ctxt, line->xData(), line->yData(), line->zData(), nullptr, numPoints ); + coordSeq = GEOSCoordSeq_copyFromArrays_r( context, line->xData(), line->yData(), line->zData(), nullptr, numPoints ); if ( !coordSeq ) { QgsDebugError( QStringLiteral( "GEOS Exception: Could not create coordinate sequence for %1 points" ).arg( numPoints ) ); @@ -2279,7 +2308,7 @@ GEOSCoordSequence *QgsGeos::createCoordinateSequence( const QgsCurve *curve, dou z.append( z.at( 0 ) ); try { - coordSeq = GEOSCoordSeq_copyFromArrays_r( ctxt, x.constData(), y.constData(), !hasZ ? nullptr : z.constData(), nullptr, numPoints + 1 ); + coordSeq = GEOSCoordSeq_copyFromArrays_r( context, x.constData(), y.constData(), !hasZ ? nullptr : z.constData(), nullptr, numPoints + 1 ); if ( !coordSeq ) { QgsDebugError( QStringLiteral( "GEOS Exception: Could not create closed coordinate sequence for %1 points" ).arg( numPoints + 1 ) ); @@ -2312,7 +2341,7 @@ GEOSCoordSequence *QgsGeos::createCoordinateSequence( const QgsCurve *curve, dou try { - coordSeq = GEOSCoordSeq_create_r( ctxt, numOutPoints, coordDims ); + coordSeq = GEOSCoordSeq_create_r( context, numOutPoints, coordDims ); if ( !coordSeq ) { QgsDebugError( QStringLiteral( "GEOS Exception: Could not create coordinate sequence for %1 points in %2 dimensions" ).arg( numPoints ).arg( coordDims ) ); @@ -2338,15 +2367,15 @@ GEOSCoordSequence *QgsGeos::createCoordinateSequence( const QgsCurve *curve, dou } if ( hasZ ) { - GEOSCoordSeq_setXYZ_r( ctxt, coordSeq, i, std::round( *xData++ / precision ) * precision, std::round( *yData++ / precision ) * precision, std::round( *zData++ / precision ) * precision ); + GEOSCoordSeq_setXYZ_r( context, coordSeq, i, std::round( *xData++ / precision ) * precision, std::round( *yData++ / precision ) * precision, std::round( *zData++ / precision ) * precision ); } else { - GEOSCoordSeq_setXY_r( ctxt, coordSeq, i, std::round( *xData++ / precision ) * precision, std::round( *yData++ / precision ) * precision ); + GEOSCoordSeq_setXY_r( context, coordSeq, i, std::round( *xData++ / precision ) * precision, std::round( *yData++ / precision ) * precision ); } if ( hasM ) { - GEOSCoordSeq_setOrdinate_r( ctxt, coordSeq, i, 3, *mData++ ); + GEOSCoordSeq_setOrdinate_r( context, coordSeq, i, 3, *mData++ ); } } } @@ -2364,15 +2393,15 @@ GEOSCoordSequence *QgsGeos::createCoordinateSequence( const QgsCurve *curve, dou } if ( hasZ ) { - GEOSCoordSeq_setXYZ_r( ctxt, coordSeq, i, *xData++, *yData++, *zData++ ); + GEOSCoordSeq_setXYZ_r( context, coordSeq, i, *xData++, *yData++, *zData++ ); } else { - GEOSCoordSeq_setXY_r( ctxt, coordSeq, i, *xData++, *yData++ ); + GEOSCoordSeq_setXY_r( context, coordSeq, i, *xData++, *yData++ ); } if ( hasM ) { - GEOSCoordSeq_setOrdinate_r( ctxt, coordSeq, i, 3, *mData++ ); + GEOSCoordSeq_setOrdinate_r( context, coordSeq, i, 3, *mData++ ); } } } @@ -2397,19 +2426,20 @@ geos::unique_ptr QgsGeos::createGeosPointXY( double x, double y, bool hasZ, doub Q_UNUSED( m ) geos::unique_ptr geosPoint; + GEOSContextHandle_t context = QgsGeosContext::get(); try { if ( coordDims == 2 ) { // optimised constructor if ( precision > 0. ) - geosPoint.reset( GEOSGeom_createPointFromXY_r( geosinit()->ctxt, std::round( x / precision ) * precision, std::round( y / precision ) * precision ) ); + geosPoint.reset( GEOSGeom_createPointFromXY_r( context, std::round( x / precision ) * precision, std::round( y / precision ) * precision ) ); else - geosPoint.reset( GEOSGeom_createPointFromXY_r( geosinit()->ctxt, x, y ) ); + geosPoint.reset( GEOSGeom_createPointFromXY_r( context, x, y ) ); return geosPoint; } - GEOSCoordSequence *coordSeq = GEOSCoordSeq_create_r( geosinit()->ctxt, 1, coordDims ); + GEOSCoordSequence *coordSeq = GEOSCoordSeq_create_r( context, 1, coordDims ); if ( !coordSeq ) { QgsDebugError( QStringLiteral( "GEOS Exception: Could not create coordinate sequence for point with %1 dimensions" ).arg( coordDims ) ); @@ -2417,29 +2447,29 @@ geos::unique_ptr QgsGeos::createGeosPointXY( double x, double y, bool hasZ, doub } if ( precision > 0. ) { - GEOSCoordSeq_setX_r( geosinit()->ctxt, coordSeq, 0, std::round( x / precision ) * precision ); - GEOSCoordSeq_setY_r( geosinit()->ctxt, coordSeq, 0, std::round( y / precision ) * precision ); + GEOSCoordSeq_setX_r( context, coordSeq, 0, std::round( x / precision ) * precision ); + GEOSCoordSeq_setY_r( context, coordSeq, 0, std::round( y / precision ) * precision ); if ( hasZ ) { - GEOSCoordSeq_setOrdinate_r( geosinit()->ctxt, coordSeq, 0, 2, std::round( z / precision ) * precision ); + GEOSCoordSeq_setOrdinate_r( context, coordSeq, 0, 2, std::round( z / precision ) * precision ); } } else { - GEOSCoordSeq_setX_r( geosinit()->ctxt, coordSeq, 0, x ); - GEOSCoordSeq_setY_r( geosinit()->ctxt, coordSeq, 0, y ); + GEOSCoordSeq_setX_r( context, coordSeq, 0, x ); + GEOSCoordSeq_setY_r( context, coordSeq, 0, y ); if ( hasZ ) { - GEOSCoordSeq_setOrdinate_r( geosinit()->ctxt, coordSeq, 0, 2, z ); + GEOSCoordSeq_setOrdinate_r( context, coordSeq, 0, 2, z ); } } #if 0 //disabled until geos supports m-coordinates if ( hasM ) { - GEOSCoordSeq_setOrdinate_r( geosinit()->ctxt, coordSeq, 0, 3, m ); + GEOSCoordSeq_setOrdinate_r( context, coordSeq, 0, 3, m ); } #endif - geosPoint.reset( GEOSGeom_createPoint_r( geosinit()->ctxt, coordSeq ) ); + geosPoint.reset( GEOSGeom_createPoint_r( context, coordSeq ) ); } CATCH_GEOS( nullptr ) return geosPoint; @@ -2458,7 +2488,7 @@ geos::unique_ptr QgsGeos::createGeosLinestring( const QgsAbstractGeometry *curve geos::unique_ptr geosGeom; try { - geosGeom.reset( GEOSGeom_createLineString_r( geosinit()->ctxt, coordSeq ) ); + geosGeom.reset( GEOSGeom_createLineString_r( QgsGeosContext::get(), coordSeq ) ); } CATCH_GEOS( nullptr ) return geosGeom; @@ -2476,10 +2506,11 @@ geos::unique_ptr QgsGeos::createGeosPolygon( const QgsAbstractGeometry *poly, do return nullptr; } + GEOSContextHandle_t context = QgsGeosContext::get(); geos::unique_ptr geosPolygon; try { - geos::unique_ptr exteriorRingGeos( GEOSGeom_createLinearRing_r( geosinit()->ctxt, createCoordinateSequence( exteriorRing, precision, true ) ) ); + geos::unique_ptr exteriorRingGeos( GEOSGeom_createLinearRing_r( context, createCoordinateSequence( exteriorRing, precision, true ) ) ); int nHoles = polygon->numInteriorRings(); GEOSGeometry **holes = nullptr; @@ -2491,9 +2522,9 @@ geos::unique_ptr QgsGeos::createGeosPolygon( const QgsAbstractGeometry *poly, do for ( int i = 0; i < nHoles; ++i ) { const QgsCurve *interiorRing = polygon->interiorRing( i ); - holes[i] = GEOSGeom_createLinearRing_r( geosinit()->ctxt, createCoordinateSequence( interiorRing, precision, true ) ); + holes[i] = GEOSGeom_createLinearRing_r( context, createCoordinateSequence( interiorRing, precision, true ) ); } - geosPolygon.reset( GEOSGeom_createPolygon_r( geosinit()->ctxt, exteriorRingGeos.release(), holes, nHoles ) ); + geosPolygon.reset( GEOSGeom_createPolygon_r( context, exteriorRingGeos.release(), holes, nHoles ) ); delete[] holes; } CATCH_GEOS( nullptr ) @@ -2513,7 +2544,7 @@ QgsAbstractGeometry *QgsGeos::offsetCurve( double distance, int segments, Qgis:: // https://github.com/qgis/QGIS/issues/53165#issuecomment-1563470832 if ( segments < 8 ) segments = 8; - offset.reset( GEOSOffsetCurve_r( geosinit()->ctxt, mGeos.get(), distance, segments, static_cast< int >( joinStyle ), miterLimit ) ); + offset.reset( GEOSOffsetCurve_r( QgsGeosContext::get(), mGeos.get(), distance, segments, static_cast< int >( joinStyle ), miterLimit ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) std::unique_ptr< QgsAbstractGeometry > offsetGeom = fromGeos( offset.get() ); @@ -2528,19 +2559,20 @@ std::unique_ptr QgsGeos::singleSidedBuffer( double distance } geos::unique_ptr geos; + GEOSContextHandle_t context = QgsGeosContext::get(); try { - geos::buffer_params_unique_ptr bp( GEOSBufferParams_create_r( geosinit()->ctxt ) ); - GEOSBufferParams_setSingleSided_r( geosinit()->ctxt, bp.get(), 1 ); - GEOSBufferParams_setQuadrantSegments_r( geosinit()->ctxt, bp.get(), segments ); - GEOSBufferParams_setJoinStyle_r( geosinit()->ctxt, bp.get(), static_cast< int >( joinStyle ) ); - GEOSBufferParams_setMitreLimit_r( geosinit()->ctxt, bp.get(), miterLimit ); //#spellok + geos::buffer_params_unique_ptr bp( GEOSBufferParams_create_r( context ) ); + GEOSBufferParams_setSingleSided_r( context, bp.get(), 1 ); + GEOSBufferParams_setQuadrantSegments_r( context, bp.get(), segments ); + GEOSBufferParams_setJoinStyle_r( context, bp.get(), static_cast< int >( joinStyle ) ); + GEOSBufferParams_setMitreLimit_r( context, bp.get(), miterLimit ); //#spellok if ( side == Qgis::BufferSide::Right ) { distance = -distance; } - geos.reset( GEOSBufferWithParams_r( geosinit()->ctxt, mGeos.get(), bp.get(), distance ) ); + geos.reset( GEOSBufferWithParams_r( context, mGeos.get(), bp.get(), distance ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ); @@ -2556,7 +2588,7 @@ std::unique_ptr QgsGeos::maximumInscribedCircle( double tol geos::unique_ptr geos; try { - geos.reset( GEOSMaximumInscribedCircle_r( geosinit()->ctxt, mGeos.get(), tolerance ) ); + geos.reset( GEOSMaximumInscribedCircle_r( QgsGeosContext::get(), mGeos.get(), tolerance ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ); @@ -2576,7 +2608,7 @@ std::unique_ptr QgsGeos::largestEmptyCircle( double toleran if ( boundary ) boundaryGeos = asGeos( boundary ); - geos.reset( GEOSLargestEmptyCircle_r( geosinit()->ctxt, mGeos.get(), boundaryGeos.get(), tolerance ) ); + geos.reset( GEOSLargestEmptyCircle_r( QgsGeosContext::get(), mGeos.get(), boundaryGeos.get(), tolerance ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ); @@ -2592,7 +2624,7 @@ std::unique_ptr QgsGeos::minimumWidth( QString *errorMsg ) geos::unique_ptr geos; try { - geos.reset( GEOSMinimumWidth_r( geosinit()->ctxt, mGeos.get() ) ); + geos.reset( GEOSMinimumWidth_r( QgsGeosContext::get(), mGeos.get() ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ); @@ -2609,7 +2641,7 @@ double QgsGeos::minimumClearance( QString *errorMsg ) const double res = 0; try { - if ( GEOSMinimumClearance_r( geosinit()->ctxt, mGeos.get(), &res ) != 0 ) + if ( GEOSMinimumClearance_r( QgsGeosContext::get(), mGeos.get(), &res ) != 0 ) return std::numeric_limits< double >::quiet_NaN(); } CATCH_GEOS_WITH_ERRMSG( std::numeric_limits< double >::quiet_NaN() ) @@ -2626,7 +2658,7 @@ std::unique_ptr QgsGeos::minimumClearanceLine( QString *err geos::unique_ptr geos; try { - geos.reset( GEOSMinimumClearanceLine_r( geosinit()->ctxt, mGeos.get() ) ); + geos.reset( GEOSMinimumClearanceLine_r( QgsGeosContext::get(), mGeos.get() ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ); @@ -2642,7 +2674,7 @@ std::unique_ptr QgsGeos::node( QString *errorMsg ) const geos::unique_ptr geos; try { - geos.reset( GEOSNode_r( geosinit()->ctxt, mGeos.get() ) ); + geos.reset( GEOSNode_r( QgsGeosContext::get(), mGeos.get() ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ); @@ -2662,7 +2694,7 @@ std::unique_ptr QgsGeos::sharedPaths( const QgsAbstractGeom if ( !otherGeos ) return nullptr; - geos.reset( GEOSSharedPaths_r( geosinit()->ctxt, mGeos.get(), otherGeos.get() ) ); + geos.reset( GEOSSharedPaths_r( QgsGeosContext::get(), mGeos.get(), otherGeos.get() ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ); @@ -2684,8 +2716,9 @@ std::unique_ptr QgsGeos::reshapeGeometry( const QgsLineStri geos::unique_ptr reshapeLineGeos = createGeosLinestring( &reshapeWithLine, mPrecision ); + GEOSContextHandle_t context = QgsGeosContext::get(); //single or multi? - int numGeoms = GEOSGetNumGeometries_r( geosinit()->ctxt, mGeos.get() ); + int numGeoms = GEOSGetNumGeometries_r( context, mGeos.get() ); if ( numGeoms == -1 ) { if ( errorCode ) @@ -2696,7 +2729,7 @@ std::unique_ptr QgsGeos::reshapeGeometry( const QgsLineStri } bool isMultiGeom = false; - int geosTypeId = GEOSGeomTypeId_r( geosinit()->ctxt, mGeos.get() ); + int geosTypeId = GEOSGeomTypeId_r( context, mGeos.get() ); if ( geosTypeId == GEOS_MULTILINESTRING || geosTypeId == GEOS_MULTIPOLYGON ) isMultiGeom = true; @@ -2732,9 +2765,9 @@ std::unique_ptr QgsGeos::reshapeGeometry( const QgsLineStri for ( int i = 0; i < numGeoms; ++i ) { if ( isLine ) - currentReshapeGeometry = reshapeLine( GEOSGetGeometryN_r( geosinit()->ctxt, mGeos.get(), i ), reshapeLineGeos.get(), mPrecision ); + currentReshapeGeometry = reshapeLine( GEOSGetGeometryN_r( context, mGeos.get(), i ), reshapeLineGeos.get(), mPrecision ); else - currentReshapeGeometry = reshapePolygon( GEOSGetGeometryN_r( geosinit()->ctxt, mGeos.get(), i ), reshapeLineGeos.get(), mPrecision ); + currentReshapeGeometry = reshapePolygon( GEOSGetGeometryN_r( context, mGeos.get(), i ), reshapeLineGeos.get(), mPrecision ); if ( currentReshapeGeometry ) { @@ -2743,18 +2776,18 @@ std::unique_ptr QgsGeos::reshapeGeometry( const QgsLineStri } else { - newGeoms[i] = GEOSGeom_clone_r( geosinit()->ctxt, GEOSGetGeometryN_r( geosinit()->ctxt, mGeos.get(), i ) ); + newGeoms[i] = GEOSGeom_clone_r( context, GEOSGetGeometryN_r( context, mGeos.get(), i ) ); } } geos::unique_ptr newMultiGeom; if ( isLine ) { - newMultiGeom.reset( GEOSGeom_createCollection_r( geosinit()->ctxt, GEOS_MULTILINESTRING, newGeoms, numGeoms ) ); + newMultiGeom.reset( GEOSGeom_createCollection_r( context, GEOS_MULTILINESTRING, newGeoms, numGeoms ) ); } else //multipolygon { - newMultiGeom.reset( GEOSGeom_createCollection_r( geosinit()->ctxt, GEOS_MULTIPOLYGON, newGeoms, numGeoms ) ); + newMultiGeom.reset( GEOSGeom_createCollection_r( context, GEOS_MULTIPOLYGON, newGeoms, numGeoms ) ); } delete[] newGeoms; @@ -2791,13 +2824,14 @@ QgsGeometry QgsGeos::mergeLines( QString *errorMsg ) const return QgsGeometry(); } - if ( GEOSGeomTypeId_r( geosinit()->ctxt, mGeos.get() ) != GEOS_MULTILINESTRING ) + GEOSContextHandle_t context = QgsGeosContext::get(); + if ( GEOSGeomTypeId_r( context, mGeos.get() ) != GEOS_MULTILINESTRING ) return QgsGeometry(); geos::unique_ptr geos; try { - geos.reset( GEOSLineMerge_r( geosinit()->ctxt, mGeos.get() ) ); + geos.reset( GEOSLineMerge_r( context, mGeos.get() ) ); } CATCH_GEOS_WITH_ERRMSG( QgsGeometry() ) return QgsGeometry( fromGeos( geos.get() ) ); @@ -2816,6 +2850,7 @@ QgsGeometry QgsGeos::closestPoint( const QgsGeometry &other, QString *errorMsg ) return QgsGeometry(); } + GEOSContextHandle_t context = QgsGeosContext::get(); double nx = 0.0; double ny = 0.0; try @@ -2823,15 +2858,15 @@ QgsGeometry QgsGeos::closestPoint( const QgsGeometry &other, QString *errorMsg ) geos::coord_sequence_unique_ptr nearestCoord; if ( mGeosPrepared ) // use faster version with prepared geometry { - nearestCoord.reset( GEOSPreparedNearestPoints_r( geosinit()->ctxt, mGeosPrepared.get(), otherGeom.get() ) ); + nearestCoord.reset( GEOSPreparedNearestPoints_r( context, mGeosPrepared.get(), otherGeom.get() ) ); } else { - nearestCoord.reset( GEOSNearestPoints_r( geosinit()->ctxt, mGeos.get(), otherGeom.get() ) ); + nearestCoord.reset( GEOSNearestPoints_r( context, mGeos.get(), otherGeom.get() ) ); } - ( void )GEOSCoordSeq_getX_r( geosinit()->ctxt, nearestCoord.get(), 0, &nx ); - ( void )GEOSCoordSeq_getY_r( geosinit()->ctxt, nearestCoord.get(), 0, &ny ); + ( void )GEOSCoordSeq_getX_r( context, nearestCoord.get(), 0, &nx ); + ( void )GEOSCoordSeq_getY_r( context, nearestCoord.get(), 0, &ny ); } catch ( GEOSException &e ) { @@ -2867,13 +2902,14 @@ QgsGeometry QgsGeos::shortestLine( const QgsAbstractGeometry *other, QString *er return QgsGeometry(); } + GEOSContextHandle_t context = QgsGeosContext::get(); double nx1 = 0.0; double ny1 = 0.0; double nx2 = 0.0; double ny2 = 0.0; try { - geos::coord_sequence_unique_ptr nearestCoord( GEOSNearestPoints_r( geosinit()->ctxt, mGeos.get(), otherGeom.get() ) ); + geos::coord_sequence_unique_ptr nearestCoord( GEOSNearestPoints_r( context, mGeos.get(), otherGeom.get() ) ); if ( !nearestCoord ) { @@ -2882,10 +2918,10 @@ QgsGeometry QgsGeos::shortestLine( const QgsAbstractGeometry *other, QString *er return QgsGeometry(); } - ( void )GEOSCoordSeq_getX_r( geosinit()->ctxt, nearestCoord.get(), 0, &nx1 ); - ( void )GEOSCoordSeq_getY_r( geosinit()->ctxt, nearestCoord.get(), 0, &ny1 ); - ( void )GEOSCoordSeq_getX_r( geosinit()->ctxt, nearestCoord.get(), 1, &nx2 ); - ( void )GEOSCoordSeq_getY_r( geosinit()->ctxt, nearestCoord.get(), 1, &ny2 ); + ( void )GEOSCoordSeq_getX_r( context, nearestCoord.get(), 0, &nx1 ); + ( void )GEOSCoordSeq_getY_r( context, nearestCoord.get(), 0, &ny1 ); + ( void )GEOSCoordSeq_getX_r( context, nearestCoord.get(), 1, &nx2 ); + ( void )GEOSCoordSeq_getY_r( context, nearestCoord.get(), 1, &ny2 ); } catch ( GEOSException &e ) { @@ -2919,7 +2955,7 @@ double QgsGeos::lineLocatePoint( const QgsPoint &point, QString *errorMsg ) cons double distance = -1; try { - distance = GEOSProject_r( geosinit()->ctxt, mGeos.get(), otherGeom.get() ); + distance = GEOSProject_r( QgsGeosContext::get(), mGeos.get(), otherGeom.get() ); } catch ( GEOSException &e ) { @@ -2948,7 +2984,7 @@ double QgsGeos::lineLocatePoint( double x, double y, QString *errorMsg ) const double distance = -1; try { - distance = GEOSProject_r( geosinit()->ctxt, mGeos.get(), point.get() ); + distance = GEOSProject_r( QgsGeosContext::get(), mGeos.get(), point.get() ); } catch ( GEOSException &e ) { @@ -2977,12 +3013,13 @@ QgsGeometry QgsGeos::polygonize( const QVector &geo } } + GEOSContextHandle_t context = QgsGeosContext::get(); try { - geos::unique_ptr result( GEOSPolygonize_r( geosinit()->ctxt, lineGeosGeometries, validLines ) ); + geos::unique_ptr result( GEOSPolygonize_r( context, lineGeosGeometries, validLines ) ); for ( int i = 0; i < validLines; ++i ) { - GEOSGeom_destroy_r( geosinit()->ctxt, lineGeosGeometries[i] ); + GEOSGeom_destroy_r( context, lineGeosGeometries[i] ); } delete[] lineGeosGeometries; return QgsGeometry( fromGeos( result.get() ) ); @@ -2995,7 +3032,7 @@ QgsGeometry QgsGeos::polygonize( const QVector &geo } for ( int i = 0; i < validLines; ++i ) { - GEOSGeom_destroy_r( geosinit()->ctxt, lineGeosGeometries[i] ); + GEOSGeom_destroy_r( context, lineGeosGeometries[i] ); } delete[] lineGeosGeometries; return QgsGeometry(); @@ -3020,11 +3057,12 @@ QgsGeometry QgsGeos::voronoiDiagram( const QgsAbstractGeometry *extent, double t } geos::unique_ptr geos; + GEOSContextHandle_t context = QgsGeosContext::get(); try { - geos.reset( GEOSVoronoiDiagram_r( geosinit()->ctxt, mGeos.get(), extentGeosGeom.get(), tolerance, edgesOnly ) ); + geos.reset( GEOSVoronoiDiagram_r( context, mGeos.get(), extentGeosGeom.get(), tolerance, edgesOnly ) ); - if ( !geos || GEOSisEmpty_r( geosinit()->ctxt, geos.get() ) != 0 ) + if ( !geos || GEOSisEmpty_r( context, geos.get() ) != 0 ) { return QgsGeometry(); } @@ -3041,12 +3079,13 @@ QgsGeometry QgsGeos::delaunayTriangulation( double tolerance, bool edgesOnly, QS return QgsGeometry(); } + GEOSContextHandle_t context = QgsGeosContext::get(); geos::unique_ptr geos; try { - geos.reset( GEOSDelaunayTriangulation_r( geosinit()->ctxt, mGeos.get(), tolerance, edgesOnly ) ); + geos.reset( GEOSDelaunayTriangulation_r( context, mGeos.get(), tolerance, edgesOnly ) ); - if ( !geos || GEOSisEmpty_r( geosinit()->ctxt, geos.get() ) != 0 ) + if ( !geos || GEOSisEmpty_r( context, geos.get() ) != 0 ) { return QgsGeometry(); } @@ -3068,11 +3107,12 @@ std::unique_ptr QgsGeos::constrainedDelaunayTriangulation( } geos::unique_ptr geos; + GEOSContextHandle_t context = QgsGeosContext::get(); try { - geos.reset( GEOSConstrainedDelaunayTriangulation_r( geosinit()->ctxt, mGeos.get() ) ); + geos.reset( GEOSConstrainedDelaunayTriangulation_r( context, mGeos.get() ) ); - if ( !geos || GEOSisEmpty_r( geosinit()->ctxt, geos.get() ) != 0 ) + if ( !geos || GEOSisEmpty_r( context, geos.get() ) != 0 ) { return nullptr; } @@ -3094,21 +3134,22 @@ std::unique_ptr QgsGeos::constrainedDelaunayTriangulation( //! Extract coordinates of linestring's endpoints. Returns false on error. static bool _linestringEndpoints( const GEOSGeometry *linestring, double &x1, double &y1, double &x2, double &y2 ) { - const GEOSCoordSequence *coordSeq = GEOSGeom_getCoordSeq_r( geosinit()->ctxt, linestring ); + GEOSContextHandle_t context = QgsGeosContext::get(); + const GEOSCoordSequence *coordSeq = GEOSGeom_getCoordSeq_r( context, linestring ); if ( !coordSeq ) return false; unsigned int coordSeqSize; - if ( GEOSCoordSeq_getSize_r( geosinit()->ctxt, coordSeq, &coordSeqSize ) == 0 ) + if ( GEOSCoordSeq_getSize_r( context, coordSeq, &coordSeqSize ) == 0 ) return false; if ( coordSeqSize < 2 ) return false; - GEOSCoordSeq_getX_r( geosinit()->ctxt, coordSeq, 0, &x1 ); - GEOSCoordSeq_getY_r( geosinit()->ctxt, coordSeq, 0, &y1 ); - GEOSCoordSeq_getX_r( geosinit()->ctxt, coordSeq, coordSeqSize - 1, &x2 ); - GEOSCoordSeq_getY_r( geosinit()->ctxt, coordSeq, coordSeqSize - 1, &y2 ); + GEOSCoordSeq_getX_r( context, coordSeq, 0, &x1 ); + GEOSCoordSeq_getY_r( context, coordSeq, 0, &y1 ); + GEOSCoordSeq_getX_r( context, coordSeq, coordSeqSize - 1, &x2 ); + GEOSCoordSeq_getY_r( context, coordSeq, coordSeqSize - 1, &y2 ); return true; } @@ -3131,14 +3172,15 @@ static geos::unique_ptr _mergeLinestrings( const GEOSGeometry *line1, const GEOS ( intersectionPoint.x() == rx1 && intersectionPoint.y() == ry1 ) || ( intersectionPoint.x() == rx2 && intersectionPoint.y() == ry2 ); + GEOSContextHandle_t context = QgsGeosContext::get(); // the intersection must be at the begin/end of both lines if ( intersectionAtOrigLineEndpoint && intersectionAtReshapeLineEndpoint ) { - geos::unique_ptr g1( GEOSGeom_clone_r( geosinit()->ctxt, line1 ) ); - geos::unique_ptr g2( GEOSGeom_clone_r( geosinit()->ctxt, line2 ) ); + geos::unique_ptr g1( GEOSGeom_clone_r( context, line1 ) ); + geos::unique_ptr g2( GEOSGeom_clone_r( context, line2 ) ); GEOSGeometry *geoms[2] = { g1.release(), g2.release() }; - geos::unique_ptr multiGeom( GEOSGeom_createCollection_r( geosinit()->ctxt, GEOS_MULTILINESTRING, geoms, 2 ) ); - geos::unique_ptr res( GEOSLineMerge_r( geosinit()->ctxt, multiGeom.get() ) ); + geos::unique_ptr multiGeom( GEOSGeom_createCollection_r( context, GEOS_MULTILINESTRING, geoms, 2 ) ); + geos::unique_ptr res( GEOSLineMerge_r( context, multiGeom.get() ) ); return res; } else @@ -3155,23 +3197,24 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome bool oneIntersection = false; QgsPointXY oneIntersectionPoint; + GEOSContextHandle_t context = QgsGeosContext::get(); try { //make sure there are at least two intersection between line and reshape geometry - geos::unique_ptr intersectGeom( GEOSIntersection_r( geosinit()->ctxt, line, reshapeLineGeos ) ); + geos::unique_ptr intersectGeom( GEOSIntersection_r( context, line, reshapeLineGeos ) ); if ( intersectGeom ) { - const int geomType = GEOSGeomTypeId_r( geosinit()->ctxt, intersectGeom.get() ); - atLeastTwoIntersections = ( geomType == GEOS_MULTIPOINT && GEOSGetNumGeometries_r( geosinit()->ctxt, intersectGeom.get() ) > 1 ) - || ( geomType == GEOS_GEOMETRYCOLLECTION && GEOSGetNumGeometries_r( geosinit()->ctxt, intersectGeom.get() ) > 0 ) // a collection implies at least two points! - || ( geomType == GEOS_MULTILINESTRING && GEOSGetNumGeometries_r( geosinit()->ctxt, intersectGeom.get() ) > 0 ); + const int geomType = GEOSGeomTypeId_r( context, intersectGeom.get() ); + atLeastTwoIntersections = ( geomType == GEOS_MULTIPOINT && GEOSGetNumGeometries_r( context, intersectGeom.get() ) > 1 ) + || ( geomType == GEOS_GEOMETRYCOLLECTION && GEOSGetNumGeometries_r( context, intersectGeom.get() ) > 0 ) // a collection implies at least two points! + || ( geomType == GEOS_MULTILINESTRING && GEOSGetNumGeometries_r( context, intersectGeom.get() ) > 0 ); // one point is enough when extending line at its endpoint - if ( GEOSGeomTypeId_r( geosinit()->ctxt, intersectGeom.get() ) == GEOS_POINT ) + if ( GEOSGeomTypeId_r( context, intersectGeom.get() ) == GEOS_POINT ) { - const GEOSCoordSequence *intersectionCoordSeq = GEOSGeom_getCoordSeq_r( geosinit()->ctxt, intersectGeom.get() ); + const GEOSCoordSequence *intersectionCoordSeq = GEOSGeom_getCoordSeq_r( context, intersectGeom.get() ); double xi, yi; - GEOSCoordSeq_getX_r( geosinit()->ctxt, intersectionCoordSeq, 0, &xi ); - GEOSCoordSeq_getY_r( geosinit()->ctxt, intersectionCoordSeq, 0, &yi ); + GEOSCoordSeq_getX_r( context, intersectionCoordSeq, 0, &xi ); + GEOSCoordSeq_getY_r( context, intersectionCoordSeq, 0, &yi ); oneIntersection = true; oneIntersectionPoint = QgsPointXY( xi, yi ); } @@ -3198,8 +3241,8 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome geos::unique_ptr endLineVertex = createGeosPointXY( x2, y2, false, 0, false, 0, 2, precision ); bool isRing = false; - if ( GEOSGeomTypeId_r( geosinit()->ctxt, line ) == GEOS_LINEARRING - || GEOSEquals_r( geosinit()->ctxt, beginLineVertex.get(), endLineVertex.get() ) == 1 ) + if ( GEOSGeomTypeId_r( context, line ) == GEOS_LINEARRING + || GEOSEquals_r( context, beginLineVertex.get(), endLineVertex.get() ) == 1 ) isRing = true; //node line and reshape line @@ -3210,18 +3253,18 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome } //and merge them together - geos::unique_ptr mergedLines( GEOSLineMerge_r( geosinit()->ctxt, nodedGeometry.get() ) ); + geos::unique_ptr mergedLines( GEOSLineMerge_r( context, nodedGeometry.get() ) ); if ( !mergedLines ) { return nullptr; } - int numMergedLines = GEOSGetNumGeometries_r( geosinit()->ctxt, mergedLines.get() ); + int numMergedLines = GEOSGetNumGeometries_r( context, mergedLines.get() ); if ( numMergedLines < 2 ) //some special cases. Normally it is >2 { if ( numMergedLines == 1 ) //reshape line is from begin to endpoint. So we keep the reshapeline { - geos::unique_ptr result( GEOSGeom_clone_r( geosinit()->ctxt, reshapeLineGeos ) ); + geos::unique_ptr result( GEOSGeom_clone_r( context, reshapeLineGeos ) ); return result; } else @@ -3233,7 +3276,7 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome for ( int i = 0; i < numMergedLines; ++i ) { - const GEOSGeometry *currentGeom = GEOSGetGeometryN_r( geosinit()->ctxt, mergedLines.get(), i ); + const GEOSGeometry *currentGeom = GEOSGetGeometryN_r( context, mergedLines.get(), i ); // have we already added this part? bool alreadyAdded = false; @@ -3241,7 +3284,7 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome double bufferDistance = std::pow( 10.0L, geomDigits( currentGeom ) - 11 ); for ( const GEOSGeometry *other : std::as_const( resultLineParts ) ) { - GEOSHausdorffDistance_r( geosinit()->ctxt, currentGeom, other, &distance ); + GEOSHausdorffDistance_r( context, currentGeom, other, &distance ); if ( distance < bufferDistance ) { alreadyAdded = true; @@ -3251,18 +3294,18 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome if ( alreadyAdded ) continue; - const GEOSCoordSequence *currentCoordSeq = GEOSGeom_getCoordSeq_r( geosinit()->ctxt, currentGeom ); + const GEOSCoordSequence *currentCoordSeq = GEOSGeom_getCoordSeq_r( context, currentGeom ); unsigned int currentCoordSeqSize; - GEOSCoordSeq_getSize_r( geosinit()->ctxt, currentCoordSeq, ¤tCoordSeqSize ); + GEOSCoordSeq_getSize_r( context, currentCoordSeq, ¤tCoordSeqSize ); if ( currentCoordSeqSize < 2 ) continue; //get the two endpoints of the current line merge result double xBegin, xEnd, yBegin, yEnd; - GEOSCoordSeq_getX_r( geosinit()->ctxt, currentCoordSeq, 0, &xBegin ); - GEOSCoordSeq_getY_r( geosinit()->ctxt, currentCoordSeq, 0, &yBegin ); - GEOSCoordSeq_getX_r( geosinit()->ctxt, currentCoordSeq, currentCoordSeqSize - 1, &xEnd ); - GEOSCoordSeq_getY_r( geosinit()->ctxt, currentCoordSeq, currentCoordSeqSize - 1, &yEnd ); + GEOSCoordSeq_getX_r( context, currentCoordSeq, 0, &xBegin ); + GEOSCoordSeq_getY_r( context, currentCoordSeq, 0, &yBegin ); + GEOSCoordSeq_getX_r( context, currentCoordSeq, currentCoordSeqSize - 1, &xEnd ); + GEOSCoordSeq_getY_r( context, currentCoordSeq, currentCoordSeqSize - 1, &yEnd ); geos::unique_ptr beginCurrentGeomVertex = createGeosPointXY( xBegin, yBegin, false, 0, false, 0, 2, precision ); geos::unique_ptr endCurrentGeomVertex = createGeosPointXY( xEnd, yEnd, false, 0, false, 0, 2, precision ); @@ -3276,12 +3319,12 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome //check how many endpoints equal the endpoints of the original line int nEndpointsSameAsOriginalLine = 0; - if ( GEOSEquals_r( geosinit()->ctxt, beginCurrentGeomVertex.get(), beginLineVertex.get() ) == 1 - || GEOSEquals_r( geosinit()->ctxt, beginCurrentGeomVertex.get(), endLineVertex.get() ) == 1 ) + if ( GEOSEquals_r( context, beginCurrentGeomVertex.get(), beginLineVertex.get() ) == 1 + || GEOSEquals_r( context, beginCurrentGeomVertex.get(), endLineVertex.get() ) == 1 ) nEndpointsSameAsOriginalLine += 1; - if ( GEOSEquals_r( geosinit()->ctxt, endCurrentGeomVertex.get(), beginLineVertex.get() ) == 1 - || GEOSEquals_r( geosinit()->ctxt, endCurrentGeomVertex.get(), endLineVertex.get() ) == 1 ) + if ( GEOSEquals_r( context, endCurrentGeomVertex.get(), beginLineVertex.get() ) == 1 + || GEOSEquals_r( context, endCurrentGeomVertex.get(), endLineVertex.get() ) == 1 ) nEndpointsSameAsOriginalLine += 1; //check if the current geometry overlaps the original geometry (GEOSOverlap does not seem to work with linestrings) @@ -3296,24 +3339,24 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome //logic to decide if this part belongs to the result if ( !isRing && nEndpointsSameAsOriginalLine == 1 && nEndpointsOnOriginalLine == 2 && currentGeomOverlapsOriginalGeom ) { - resultLineParts.push_back( GEOSGeom_clone_r( geosinit()->ctxt, currentGeom ) ); + resultLineParts.push_back( GEOSGeom_clone_r( context, currentGeom ) ); } //for closed rings, we take one segment from the candidate list else if ( isRing && nEndpointsOnOriginalLine == 2 && currentGeomOverlapsOriginalGeom ) { - probableParts.push_back( GEOSGeom_clone_r( geosinit()->ctxt, currentGeom ) ); + probableParts.push_back( GEOSGeom_clone_r( context, currentGeom ) ); } else if ( nEndpointsOnOriginalLine == 2 && !currentGeomOverlapsOriginalGeom ) { - resultLineParts.push_back( GEOSGeom_clone_r( geosinit()->ctxt, currentGeom ) ); + resultLineParts.push_back( GEOSGeom_clone_r( context, currentGeom ) ); } else if ( nEndpointsSameAsOriginalLine == 2 && !currentGeomOverlapsOriginalGeom ) { - resultLineParts.push_back( GEOSGeom_clone_r( geosinit()->ctxt, currentGeom ) ); + resultLineParts.push_back( GEOSGeom_clone_r( context, currentGeom ) ); } else if ( currentGeomOverlapsOriginalGeom && currentGeomOverlapsReshapeLine ) { - resultLineParts.push_back( GEOSGeom_clone_r( geosinit()->ctxt, currentGeom ) ); + resultLineParts.push_back( GEOSGeom_clone_r( context, currentGeom ) ); } } @@ -3327,7 +3370,7 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome for ( int i = 0; i < probableParts.size(); ++i ) { currentGeom = probableParts.at( i ); - GEOSLength_r( geosinit()->ctxt, currentGeom, ¤tLength ); + GEOSLength_r( context, currentGeom, ¤tLength ); if ( currentLength > maxLength ) { maxLength = currentLength; @@ -3335,7 +3378,7 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome } else { - GEOSGeom_destroy_r( geosinit()->ctxt, currentGeom ); + GEOSGeom_destroy_r( context, currentGeom ); } } resultLineParts.push_back( maxGeom.release() ); @@ -3358,15 +3401,15 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome } //create multiline from resultLineParts - geos::unique_ptr multiLineGeom( GEOSGeom_createCollection_r( geosinit()->ctxt, GEOS_MULTILINESTRING, lineArray, resultLineParts.size() ) ); + geos::unique_ptr multiLineGeom( GEOSGeom_createCollection_r( context, GEOS_MULTILINESTRING, lineArray, resultLineParts.size() ) ); delete [] lineArray; //then do a linemerge with the newly combined partstrings - result.reset( GEOSLineMerge_r( geosinit()->ctxt, multiLineGeom.get() ) ); + result.reset( GEOSLineMerge_r( context, multiLineGeom.get() ) ); } //now test if the result is a linestring. Otherwise something went wrong - if ( GEOSGeomTypeId_r( geosinit()->ctxt, result.get() ) != GEOS_LINESTRING ) + if ( GEOSGeomTypeId_r( context, result.get() ) != GEOS_LINESTRING ) { return nullptr; } @@ -3381,13 +3424,14 @@ geos::unique_ptr QgsGeos::reshapePolygon( const GEOSGeometry *polygon, const GEO int lastIntersectingRing = -2; const GEOSGeometry *lastIntersectingGeom = nullptr; - int nRings = GEOSGetNumInteriorRings_r( geosinit()->ctxt, polygon ); + GEOSContextHandle_t context = QgsGeosContext::get(); + int nRings = GEOSGetNumInteriorRings_r( context, polygon ); if ( nRings < 0 ) return nullptr; //does outer ring intersect? - const GEOSGeometry *outerRing = GEOSGetExteriorRing_r( geosinit()->ctxt, polygon ); - if ( GEOSIntersects_r( geosinit()->ctxt, outerRing, reshapeLineGeos ) == 1 ) + const GEOSGeometry *outerRing = GEOSGetExteriorRing_r( context, polygon ); + if ( GEOSIntersects_r( context, outerRing, reshapeLineGeos ) == 1 ) { ++nIntersections; lastIntersectingRing = -1; @@ -3401,8 +3445,8 @@ geos::unique_ptr QgsGeos::reshapePolygon( const GEOSGeometry *polygon, const GEO { for ( int i = 0; i < nRings; ++i ) { - innerRings[i] = GEOSGetInteriorRingN_r( geosinit()->ctxt, polygon, i ); - if ( GEOSIntersects_r( geosinit()->ctxt, innerRings[i], reshapeLineGeos ) == 1 ) + innerRings[i] = GEOSGetInteriorRingN_r( context, polygon, i ); + if ( GEOSIntersects_r( context, innerRings[i], reshapeLineGeos ) == 1 ) { ++nIntersections; lastIntersectingRing = i; @@ -3431,12 +3475,12 @@ geos::unique_ptr QgsGeos::reshapePolygon( const GEOSGeometry *polygon, const GEO //if reshaping took place, we need to reassemble the polygon and its rings GEOSGeometry *newRing = nullptr; - const GEOSCoordSequence *reshapeSequence = GEOSGeom_getCoordSeq_r( geosinit()->ctxt, reshapeResult.get() ); - GEOSCoordSequence *newCoordSequence = GEOSCoordSeq_clone_r( geosinit()->ctxt, reshapeSequence ); + const GEOSCoordSequence *reshapeSequence = GEOSGeom_getCoordSeq_r( context, reshapeResult.get() ); + GEOSCoordSequence *newCoordSequence = GEOSCoordSeq_clone_r( context, reshapeSequence ); reshapeResult.reset(); - newRing = GEOSGeom_createLinearRing_r( geosinit()->ctxt, newCoordSequence ); + newRing = GEOSGeom_createLinearRing_r( context, newCoordSequence ); if ( !newRing ) { delete [] innerRings; @@ -3447,13 +3491,13 @@ geos::unique_ptr QgsGeos::reshapePolygon( const GEOSGeometry *polygon, const GEO if ( lastIntersectingRing == -1 ) newOuterRing = newRing; else - newOuterRing = GEOSGeom_clone_r( geosinit()->ctxt, outerRing ); + newOuterRing = GEOSGeom_clone_r( context, outerRing ); //check if all the rings are still inside the outer boundary QVector ringList; if ( nRings > 0 ) { - GEOSGeometry *outerRingPoly = GEOSGeom_createPolygon_r( geosinit()->ctxt, GEOSGeom_clone_r( geosinit()->ctxt, newOuterRing ), nullptr, 0 ); + GEOSGeometry *outerRingPoly = GEOSGeom_createPolygon_r( context, GEOSGeom_clone_r( context, newOuterRing ), nullptr, 0 ); if ( outerRingPoly ) { ringList.reserve( nRings ); @@ -3463,16 +3507,16 @@ geos::unique_ptr QgsGeos::reshapePolygon( const GEOSGeometry *polygon, const GEO if ( lastIntersectingRing == i ) currentRing = newRing; else - currentRing = GEOSGeom_clone_r( geosinit()->ctxt, innerRings[i] ); + currentRing = GEOSGeom_clone_r( context, innerRings[i] ); //possibly a ring is no longer contained in the result polygon after reshape - if ( GEOSContains_r( geosinit()->ctxt, outerRingPoly, currentRing ) == 1 ) + if ( GEOSContains_r( context, outerRingPoly, currentRing ) == 1 ) ringList.push_back( currentRing ); else - GEOSGeom_destroy_r( geosinit()->ctxt, currentRing ); + GEOSGeom_destroy_r( context, currentRing ); } } - GEOSGeom_destroy_r( geosinit()->ctxt, outerRingPoly ); + GEOSGeom_destroy_r( context, outerRingPoly ); } GEOSGeometry **newInnerRings = new GEOSGeometry*[ringList.size()]; @@ -3481,7 +3525,7 @@ geos::unique_ptr QgsGeos::reshapePolygon( const GEOSGeometry *polygon, const GEO delete [] innerRings; - geos::unique_ptr reshapedPolygon( GEOSGeom_createPolygon_r( geosinit()->ctxt, newOuterRing, newInnerRings, ringList.size() ) ); + geos::unique_ptr reshapedPolygon( GEOSGeom_createPolygon_r( context, newOuterRing, newInnerRings, ringList.size() ) ); delete[] newInnerRings; return reshapedPolygon; @@ -3496,18 +3540,19 @@ int QgsGeos::lineContainedInLine( const GEOSGeometry *line1, const GEOSGeometry double bufferDistance = std::pow( 10.0L, geomDigits( line2 ) - 11 ); - geos::unique_ptr bufferGeom( GEOSBuffer_r( geosinit()->ctxt, line2, bufferDistance, DEFAULT_QUADRANT_SEGMENTS ) ); + GEOSContextHandle_t context = QgsGeosContext::get(); + geos::unique_ptr bufferGeom( GEOSBuffer_r( context, line2, bufferDistance, DEFAULT_QUADRANT_SEGMENTS ) ); if ( !bufferGeom ) return -2; - geos::unique_ptr intersectionGeom( GEOSIntersection_r( geosinit()->ctxt, bufferGeom.get(), line1 ) ); + geos::unique_ptr intersectionGeom( GEOSIntersection_r( context, bufferGeom.get(), line1 ) ); //compare ratio between line1Length and intersectGeomLength (usually close to 1 if line1 is contained in line2) double intersectGeomLength; double line1Length; - GEOSLength_r( geosinit()->ctxt, intersectionGeom.get(), &intersectGeomLength ); - GEOSLength_r( geosinit()->ctxt, line1, &line1Length ); + GEOSLength_r( context, intersectionGeom.get(), &intersectGeomLength ); + GEOSLength_r( context, line1, &line1Length ); double intersectRatio = line1Length / intersectGeomLength; if ( intersectRatio > 0.9 && intersectRatio < 1.1 ) @@ -3523,12 +3568,13 @@ int QgsGeos::pointContainedInLine( const GEOSGeometry *point, const GEOSGeometry double bufferDistance = std::pow( 10.0L, geomDigits( line ) - 11 ); - geos::unique_ptr lineBuffer( GEOSBuffer_r( geosinit()->ctxt, line, bufferDistance, 8 ) ); + GEOSContextHandle_t context = QgsGeosContext::get(); + geos::unique_ptr lineBuffer( GEOSBuffer_r( context, line, bufferDistance, 8 ) ); if ( !lineBuffer ) return -2; bool contained = false; - if ( GEOSContains_r( geosinit()->ctxt, lineBuffer.get(), point ) == 1 ) + if ( GEOSContains_r( context, lineBuffer.get(), point ) == 1 ) contained = true; return contained; @@ -3536,35 +3582,36 @@ int QgsGeos::pointContainedInLine( const GEOSGeometry *point, const GEOSGeometry int QgsGeos::geomDigits( const GEOSGeometry *geom ) { - geos::unique_ptr bbox( GEOSEnvelope_r( geosinit()->ctxt, geom ) ); + GEOSContextHandle_t context = QgsGeosContext::get(); + geos::unique_ptr bbox( GEOSEnvelope_r( context, geom ) ); if ( !bbox.get() ) return -1; - const GEOSGeometry *bBoxRing = GEOSGetExteriorRing_r( geosinit()->ctxt, bbox.get() ); + const GEOSGeometry *bBoxRing = GEOSGetExteriorRing_r( context, bbox.get() ); if ( !bBoxRing ) return -1; - const GEOSCoordSequence *bBoxCoordSeq = GEOSGeom_getCoordSeq_r( geosinit()->ctxt, bBoxRing ); + const GEOSCoordSequence *bBoxCoordSeq = GEOSGeom_getCoordSeq_r( context, bBoxRing ); if ( !bBoxCoordSeq ) return -1; unsigned int nCoords = 0; - if ( !GEOSCoordSeq_getSize_r( geosinit()->ctxt, bBoxCoordSeq, &nCoords ) ) + if ( !GEOSCoordSeq_getSize_r( context, bBoxCoordSeq, &nCoords ) ) return -1; int maxDigits = -1; for ( unsigned int i = 0; i < nCoords - 1; ++i ) { double t; - GEOSCoordSeq_getX_r( geosinit()->ctxt, bBoxCoordSeq, i, &t ); + GEOSCoordSeq_getX_r( context, bBoxCoordSeq, i, &t ); int digits; digits = std::ceil( std::log10( std::fabs( t ) ) ); if ( digits > maxDigits ) maxDigits = digits; - GEOSCoordSeq_getY_r( geosinit()->ctxt, bBoxCoordSeq, i, &t ); + GEOSCoordSeq_getY_r( context, bBoxCoordSeq, i, &t ); digits = std::ceil( std::log10( std::fabs( t ) ) ); if ( digits > maxDigits ) maxDigits = digits; @@ -3572,8 +3619,3 @@ int QgsGeos::geomDigits( const GEOSGeometry *geom ) return maxDigits; } - -GEOSContextHandle_t QgsGeos::getGEOSHandler() -{ - return geosinit()->ctxt; -} diff --git a/src/core/geometry/qgsgeos.h b/src/core/geometry/qgsgeos.h index 1e59b29c1180..92ad193c3159 100644 --- a/src/core/geometry/qgsgeos.h +++ b/src/core/geometry/qgsgeos.h @@ -28,6 +28,36 @@ class QgsPolygon; class QgsGeometry; class QgsGeometryCollection; + +/** + * \class QgsGeosContext + * \ingroup core + * \brief Used to create and store a proj context object, correctly freeing the context upon destruction. + * \note Not available in Python bindings + * \since QGIS 3.38 + */ +class CORE_EXPORT QgsGeosContext +{ + public: + + QgsGeosContext(); + ~QgsGeosContext(); + + /** + * Returns a thread local instance of a GEOS context, safe for use in the current thread. + */ + static GEOSContextHandle_t get(); + + private: + GEOSContextHandle_t mContext = nullptr; + + /** + * Thread local GEOS context storage. A new GEOS context will be created + * for every thread. + */ + static thread_local QgsGeosContext sGeosContext; +}; + /** * Contains geos related utilities and functions. * \note not available in Python bindings. @@ -660,9 +690,6 @@ class CORE_EXPORT QgsGeos: public QgsGeometryEngine static geos::unique_ptr asGeos( const QgsAbstractGeometry *geometry, double precision = 0, bool allowInvalidSubGeom = true ); static QgsPoint coordSeqPoint( const GEOSCoordSequence *cs, int i, bool hasZ, bool hasM ); - static GEOSContextHandle_t getGEOSHandler(); - - private: mutable geos::unique_ptr mGeos; geos::prepared_unique_ptr mGeosPrepared; diff --git a/src/core/labeling/qgslabelfeature.cpp b/src/core/labeling/qgslabelfeature.cpp index 4d0b699348b1..00a4b4bdd3ab 100644 --- a/src/core/labeling/qgslabelfeature.cpp +++ b/src/core/labeling/qgslabelfeature.cpp @@ -51,7 +51,7 @@ void QgsLabelFeature::setPermissibleZone( const QgsGeometry &geometry ) if ( !mPermissibleZoneGeos ) return; - mPermissibleZoneGeosPrepared.reset( GEOSPrepare_r( QgsGeos::getGEOSHandler(), mPermissibleZoneGeos.get() ) ); + mPermissibleZoneGeosPrepared.reset( GEOSPrepare_r( QgsGeosContext::get(), mPermissibleZoneGeos.get() ) ); } QgsFeature QgsLabelFeature::feature() const diff --git a/src/core/pal/feature.cpp b/src/core/pal/feature.cpp index 6fa3f04ab220..dc0fabe3fa0c 100644 --- a/src/core/pal/feature.cpp +++ b/src/core/pal/feature.cpp @@ -93,7 +93,7 @@ FeaturePart::~FeaturePart() void FeaturePart::extractCoords( const GEOSGeometry *geom ) { const GEOSCoordSequence *coordSeq = nullptr; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); type = GEOSGeomTypeId_r( geosctxt, geom ); @@ -410,7 +410,7 @@ std::unique_ptr FeaturePart::createCandidatePointOnSurface( Point double px, py; try { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); geos::unique_ptr pointGeom( GEOSPointOnSurface_r( geosctxt, mapShape->geos() ) ); if ( pointGeom ) { @@ -1995,7 +1995,7 @@ std::size_t FeaturePart::createCandidatesOutsidePolygon( std::vector if ( !mGeos ) createGeosGeom(); - GEOSContextHandle_t ctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t ctxt = QgsGeosContext::get(); int geomType = GEOSGeomTypeId_r( ctxt, mGeos ); double sizeCost = 0; @@ -2320,7 +2320,7 @@ bool FeaturePart::isConnected( FeaturePart *p2 ) const double p2otherX = p2startTouches ? x2last : x2first; const double p2otherY = p2startTouches ? y2last : y2first; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); GEOSCoordSequence *coord = GEOSCoordSeq_create_r( geosctxt, 1, 2 ); GEOSCoordSeq_setXY_r( geosctxt, coord, 0, p2otherX, p2otherY ); @@ -2345,7 +2345,7 @@ bool FeaturePart::mergeWithFeaturePart( FeaturePart *other ) if ( !other->mGeos ) other->createGeosGeom(); - GEOSContextHandle_t ctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t ctxt = QgsGeosContext::get(); try { GEOSGeometry *g1 = GEOSGeom_clone_r( ctxt, mGeos ); diff --git a/src/core/pal/geomfunction.cpp b/src/core/pal/geomfunction.cpp index 550151f24ec8..c23be07d8c9c 100644 --- a/src/core/pal/geomfunction.cpp +++ b/src/core/pal/geomfunction.cpp @@ -308,7 +308,7 @@ bool GeomFunction::containsCandidate( const GEOSPreparedGeometry *geom, double x try { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); GEOSCoordSequence *coord = GEOSCoordSeq_create_r( geosctxt, 5, 2 ); GEOSCoordSeq_setXY_r( geosctxt, coord, 0, x, y ); diff --git a/src/core/pal/labelposition.cpp b/src/core/pal/labelposition.cpp index 724eaaafaba7..ba1a576c58d8 100644 --- a/src/core/pal/labelposition.cpp +++ b/src/core/pal/labelposition.cpp @@ -169,7 +169,7 @@ LabelPosition::~LabelPosition() { if ( mPreparedOuterBoundsGeos ) { - GEOSPreparedGeom_destroy_r( QgsGeos::getGEOSHandler(), mPreparedOuterBoundsGeos ); + GEOSPreparedGeom_destroy_r( QgsGeosContext::get(), mPreparedOuterBoundsGeos ); mPreparedOuterBoundsGeos = nullptr; } } @@ -182,7 +182,7 @@ bool LabelPosition::intersects( const GEOSPreparedGeometry *geometry ) try { - if ( GEOSPreparedIntersects_r( QgsGeos::getGEOSHandler(), geometry, mOuterBoundsGeos ? mOuterBoundsGeos.get() : mGeos ) == 1 ) + if ( GEOSPreparedIntersects_r( QgsGeosContext::get(), geometry, mOuterBoundsGeos ? mOuterBoundsGeos.get() : mGeos ) == 1 ) { return true; } @@ -209,7 +209,7 @@ bool LabelPosition::within( const GEOSPreparedGeometry *geometry ) try { - if ( GEOSPreparedContains_r( QgsGeos::getGEOSHandler(), geometry, mOuterBoundsGeos ? mOuterBoundsGeos.get() : mGeos ) != 1 ) + if ( GEOSPreparedContains_r( QgsGeosContext::get(), geometry, mOuterBoundsGeos ? mOuterBoundsGeos.get() : mGeos ) != 1 ) { return false; } @@ -260,7 +260,7 @@ bool LabelPosition::isInConflict( const LabelPosition *lp ) const if ( !mOuterBoundsGeos && !mGeos ) createGeosGeom(); - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { const bool result = ( GEOSPreparedIntersects_r( geosctxt, lp->preparedOuterBoundsGeom() ? lp->preparedOuterBoundsGeom() : lp->preparedGeom(), @@ -289,7 +289,7 @@ bool LabelPosition::isInConflictMultiPart( const LabelPosition *lp ) const if ( !lp->mMultipartGeos ) lp->createMultiPartGeosGeom(); - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { const bool result = ( GEOSPreparedIntersects_r( geosctxt, preparedMultiPartGeom(), lp->mMultipartGeos ) == 1 ); @@ -311,7 +311,7 @@ void LabelPosition::createOuterBoundsGeom() if ( outerBounds.isNull() ) return; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); const double beta = this->alpha + M_PI_2; @@ -356,7 +356,7 @@ void LabelPosition::createOuterBoundsGeom() mOuterBoundsGeos.reset( GEOSGeom_createPolygon_r( geosctxt, GEOSGeom_createLinearRing_r( geosctxt, coord ), nullptr, 0 ) ); - mPreparedOuterBoundsGeos = GEOSPrepare_r( QgsGeos::getGEOSHandler(), mOuterBoundsGeos.get() ); + mPreparedOuterBoundsGeos = GEOSPrepare_r( QgsGeosContext::get(), mOuterBoundsGeos.get() ); auto xminmax = std::minmax_element( mOuterBoundsX.begin(), mOuterBoundsX.end() ); mOuterBoundsXMin = *xminmax.first; @@ -505,7 +505,7 @@ void LabelPosition::insertIntoIndex( PalRtree &index ) void LabelPosition::createMultiPartGeosGeom() const { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); std::vector< const GEOSGeometry * > geometries; const LabelPosition *tmp1 = this; @@ -535,7 +535,7 @@ const GEOSPreparedGeometry *LabelPosition::preparedMultiPartGeom() const if ( !mMultipartPreparedGeos ) { - mMultipartPreparedGeos = GEOSPrepare_r( QgsGeos::getGEOSHandler(), mMultipartGeos ); + mMultipartPreparedGeos = GEOSPrepare_r( QgsGeosContext::get(), mMultipartGeos ); } return mMultipartPreparedGeos; } @@ -549,7 +549,7 @@ double LabelPosition::getDistanceToPoint( double xp, double yp, bool useOuterBou { // this method may consider the label's outer bounds! - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); //first check if inside, if so then distance is -1 bool contains = false; @@ -655,7 +655,7 @@ bool LabelPosition::crossesLine( PointSet *line ) const if ( !line->mGeos ) line->createGeosGeom(); - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { if ( GEOSPreparedIntersects_r( geosctxt, line->preparedGeom(), mOuterBoundsGeos ? mOuterBoundsGeos.get() : mGeos ) == 1 ) @@ -686,7 +686,7 @@ bool LabelPosition::crossesBoundary( PointSet *polygon ) const if ( !polygon->mGeos ) polygon->createGeosGeom(); - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { if ( GEOSPreparedIntersects_r( geosctxt, polygon->preparedGeom(), mOuterBoundsGeos ? mOuterBoundsGeos.get() : mGeos ) == 1 @@ -726,7 +726,7 @@ bool LabelPosition::intersectsWithPolygon( PointSet *polygon ) const if ( !polygon->mGeos ) polygon->createGeosGeom(); - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { if ( GEOSPreparedIntersects_r( geosctxt, polygon->preparedGeom(), mOuterBoundsGeos ? mOuterBoundsGeos.get() : mGeos ) == 1 ) @@ -759,7 +759,7 @@ double LabelPosition::polygonIntersectionCostForParts( PointSet *polygon ) const if ( !polygon->mGeos ) polygon->createGeosGeom(); - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); double cost = 0; try { diff --git a/src/core/pal/layer.cpp b/src/core/pal/layer.cpp index 1b11ce8aba31..7a275564396d 100644 --- a/src/core/pal/layer.cpp +++ b/src/core/pal/layer.cpp @@ -107,7 +107,7 @@ bool Layer::registerFeature( QgsLabelFeature *lf ) throw InternalException::UnknownGeometry(); } - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); const bool featureGeomIsObstacleGeom = lf->obstacleSettings().obstacleGeometry().isNull(); @@ -364,7 +364,7 @@ int Layer::connectedFeatureId( QgsFeatureId featureId ) const void Layer::chopFeaturesAtRepeatDistance() { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); std::deque< std::unique_ptr< FeaturePart > > newFeatureParts; while ( !mFeatureParts.empty() ) { diff --git a/src/core/pal/pal.cpp b/src/core/pal/pal.cpp index 029de6d62ca2..051de8e4d358 100644 --- a/src/core/pal/pal.cpp +++ b/src/core/pal/pal.cpp @@ -134,7 +134,7 @@ std::unique_ptr Pal::extractProblem( const QgsRectangle &extent, const // prepare map boundary geos::unique_ptr mapBoundaryGeos( QgsGeos::asGeos( mapBoundary ) ); - geos::prepared_unique_ptr mapBoundaryPrepared( GEOSPrepare_r( QgsGeos::getGEOSHandler(), mapBoundaryGeos.get() ) ); + geos::prepared_unique_ptr mapBoundaryPrepared( GEOSPrepare_r( QgsGeosContext::get(), mapBoundaryGeos.get() ) ); int obstacleCount = 0; diff --git a/src/core/pal/pointset.cpp b/src/core/pal/pointset.cpp index 1cc41f07f14b..ae6b20a32ea7 100644 --- a/src/core/pal/pointset.cpp +++ b/src/core/pal/pointset.cpp @@ -92,14 +92,14 @@ PointSet::PointSet( const PointSet &ps ) if ( ps.mGeos ) { - mGeos = GEOSGeom_clone_r( QgsGeos::getGEOSHandler(), ps.mGeos ); + mGeos = GEOSGeom_clone_r( QgsGeosContext::get(), ps.mGeos ); mOwnsGeom = true; } } void PointSet::createGeosGeom() const { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); bool needClose = false; if ( type == GEOS_POLYGON && ( !qgsDoubleNear( x[0], x[ nbPoints - 1] ) || !qgsDoubleNear( y[0], y[ nbPoints - 1 ] ) ) ) @@ -159,14 +159,14 @@ const GEOSPreparedGeometry *PointSet::preparedGeom() const if ( !mPreparedGeom ) { - mPreparedGeom = GEOSPrepare_r( QgsGeos::getGEOSHandler(), mGeos ); + mPreparedGeom = GEOSPrepare_r( QgsGeosContext::get(), mGeos ); } return mPreparedGeom; } void PointSet::invalidateGeos() const { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); if ( mOwnsGeom ) // delete old geometry if we own it GEOSGeom_destroy_r( geosctxt, mGeos ); mOwnsGeom = false; @@ -201,7 +201,7 @@ void PointSet::invalidateGeos() const PointSet::~PointSet() { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); if ( mGeos && mOwnsGeom ) { @@ -270,7 +270,7 @@ std::unique_ptr PointSet::clone() const bool PointSet::containsPoint( double x, double y ) const { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { geos::unique_ptr point( GEOSGeom_createPointFromXY_r( geosctxt, x, y ) ); @@ -551,7 +551,7 @@ void PointSet::offsetCurveByDistance( double distance ) if ( !mGeos || type != GEOS_LINESTRING ) return; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); geos::unique_ptr newGeos = nullptr; try { @@ -866,7 +866,7 @@ double PointSet::minDistanceToPoint( double px, double py, double *rx, double *r if ( !mGeos ) return 0; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { geos::unique_ptr geosPt( GEOSGeom_createPointFromXY_r( geosctxt, px, py ) ); @@ -925,7 +925,7 @@ void PointSet::getCentroid( double &px, double &py, bool forceInside ) const try { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); geos::unique_ptr centroidGeom( GEOSGetCentroid_r( geosctxt, mGeos ) ); if ( centroidGeom ) { @@ -1020,7 +1020,7 @@ geos::unique_ptr PointSet::interpolatePoint( double distance ) const try { - geos::unique_ptr res( GEOSInterpolate_r( QgsGeos::getGEOSHandler(), thisGeos, distance ) ); + geos::unique_ptr res( GEOSInterpolate_r( QgsGeosContext::get(), thisGeos, distance ) ); return res; } catch ( GEOSException &e ) @@ -1039,7 +1039,7 @@ double PointSet::lineLocatePoint( const GEOSGeometry *point ) const double distance = -1; try { - distance = GEOSProject_r( QgsGeos::getGEOSHandler(), thisGeos, point ); + distance = GEOSProject_r( QgsGeosContext::get(), thisGeos, point ); } catch ( GEOSException &e ) { @@ -1069,7 +1069,7 @@ double PointSet::length() const if ( !mGeos ) return -1; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { @@ -1095,7 +1095,7 @@ double PointSet::area() const if ( !mGeos ) return -1; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { @@ -1121,7 +1121,7 @@ QString PointSet::toWkt() const if ( !mGeos ) createGeosGeom(); - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { diff --git a/src/core/pal/util.cpp b/src/core/pal/util.cpp index 085b35066022..7bd781f4145d 100644 --- a/src/core/pal/util.cpp +++ b/src/core/pal/util.cpp @@ -44,7 +44,7 @@ QLinkedList *pal::Util::unmulti( const GEOSGeometry *the_g int nGeom; int i; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); while ( !queue->isEmpty() ) { diff --git a/src/core/pointcloud/qgspointcloudlayerprofilegenerator.cpp b/src/core/pointcloud/qgspointcloudlayerprofilegenerator.cpp index c38090e5c64a..12c71799f1cc 100644 --- a/src/core/pointcloud/qgspointcloudlayerprofilegenerator.cpp +++ b/src/core/pointcloud/qgspointcloudlayerprofilegenerator.cpp @@ -37,18 +37,18 @@ QgsPointCloudLayerProfileResults::QgsPointCloudLayerProfileResults() { - mPointIndex = GEOSSTRtree_create_r( QgsGeos::getGEOSHandler(), ( size_t )10 ); + mPointIndex = GEOSSTRtree_create_r( QgsGeosContext::get(), ( size_t )10 ); } QgsPointCloudLayerProfileResults::~QgsPointCloudLayerProfileResults() { - GEOSSTRtree_destroy_r( QgsGeos::getGEOSHandler(), mPointIndex ); + GEOSSTRtree_destroy_r( QgsGeosContext::get(), mPointIndex ); mPointIndex = nullptr; } void QgsPointCloudLayerProfileResults::finalize( QgsFeedback *feedback ) { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); const std::size_t size = results.size(); PointResult *pointData = results.data(); @@ -240,7 +240,7 @@ QgsProfileSnapResult QgsPointCloudLayerProfileResults::snapPoint( const QgsProfi { QgsProfileSnapResult result; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); const double minDistance = point.distance() - context.maximumPointDistanceDelta; const double maxDistance = point.distance() + context.maximumPointDistanceDelta; diff --git a/src/core/qgstracer.cpp b/src/core/qgstracer.cpp index 778498289b69..94cb662f730a 100644 --- a/src/core/qgstracer.cpp +++ b/src/core/qgstracer.cpp @@ -558,7 +558,7 @@ bool QgsTracer::initGraph() t2a.start(); // GEOSNode_r may throw an exception geos::unique_ptr allGeomGeos( QgsGeos::asGeos( allGeom ) ); - geos::unique_ptr allNoded( GEOSNode_r( QgsGeos::getGEOSHandler(), allGeomGeos.get() ) ); + geos::unique_ptr allNoded( GEOSNode_r( QgsGeosContext::get(), allGeomGeos.get() ) ); timeNodingCall = t2a.elapsed(); QgsGeometry noded = QgsGeos::geometryFromGeos( allNoded.release() ); diff --git a/src/plugins/topology/topolTest.cpp b/src/plugins/topology/topolTest.cpp index 32dda6580d4c..b59480b7bdfc 100644 --- a/src/plugins/topology/topolTest.cpp +++ b/src/plugins/topology/topolTest.cpp @@ -470,7 +470,7 @@ ErrorList topolTest::checkGaps( QgsVectorLayer *layer1, QgsVectorLayer *layer2, int i = 0; ErrorList errorList; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); // could be enabled for lines and points too // so duplicate rule may be removed? diff --git a/tests/src/core/geometry/testqgsgeometry.cpp b/tests/src/core/geometry/testqgsgeometry.cpp index 6f8a0c1c24c6..5cbb5b6b8c01 100644 --- a/tests/src/core/geometry/testqgsgeometry.cpp +++ b/tests/src/core/geometry/testqgsgeometry.cpp @@ -592,7 +592,7 @@ void TestQgsGeometry::geos() polyWithEmptyParts.addGeometry( new QgsPolygon( new QgsLineString() ) ); polyWithEmptyParts.addGeometry( new QgsPolygon( new QgsLineString( QVector< QgsPoint >() << QgsPoint( 10, 0 ) << QgsPoint( 10, 1 ) << QgsPoint( 11, 1 ) << QgsPoint( 10, 0 ) ) ) ); asGeos = QgsGeos::asGeos( &polyWithEmptyParts ); - QCOMPARE( GEOSGetNumGeometries_r( QgsGeos::getGEOSHandler(), asGeos.get() ), 2 ); + QCOMPARE( GEOSGetNumGeometries_r( QgsGeosContext::get(), asGeos.get() ), 2 ); res = QgsGeometry( QgsGeos::fromGeos( asGeos.get() ) ); QCOMPARE( res.asWkt(), QStringLiteral( "MultiPolygon (((0 0, 0 1, 1 1, 0 0)),((10 0, 10 1, 11 1, 10 0)))" ) ); From 276b2cb565934d7f7182bd5ea38902562a9a3ed7 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 8 Apr 2024 11:44:05 +1000 Subject: [PATCH 78/87] Don't use deprecated GEOS context methods --- src/core/geometry/qgsgeos.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/core/geometry/qgsgeos.cpp b/src/core/geometry/qgsgeos.cpp index 0a079b75ed11..00c404bc63d9 100644 --- a/src/core/geometry/qgsgeos.cpp +++ b/src/core/geometry/qgsgeos.cpp @@ -102,12 +102,22 @@ thread_local QgsGeosContext QgsGeosContext::sGeosContext; QgsGeosContext::QgsGeosContext() { +#if GEOS_VERSION_MAJOR>3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR>=5 ) + mContext = GEOS_init_r(); + GEOSContext_setNoticeHandler_r( mContext, printGEOSNotice ); + GEOSContext_setErrorHandler_r( mContext, throwGEOSException ); +#else mContext = initGEOS_r( printGEOSNotice, throwGEOSException ); +#endif } QgsGeosContext::~QgsGeosContext() { +#if GEOS_VERSION_MAJOR>3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR>=5 ) + GEOS_finish_r( mContext ); +#else finishGEOS_r( mContext ); +#endif } GEOSContextHandle_t QgsGeosContext::get() From 96d5cc99b37acb59ed0704da34f7fd2bc49846ce Mon Sep 17 00:00:00 2001 From: Marco Hugentobler Date: Sun, 24 Mar 2024 16:13:45 +0100 Subject: [PATCH 79/87] Consider setting layerTreeInsertionMethod when adding content from qlr to canvas --- .../auto_generated/qgslayerdefinition.sip.in | 24 +++++++++++++-- .../auto_generated/qgslayerdefinition.sip.in | 24 +++++++++++++-- src/app/layers/qgsapplayerhandling.cpp | 8 ++--- src/app/layers/qgsapplayerhandling.h | 15 ++++++++-- src/app/qgisapp.cpp | 11 +++++-- src/app/qgisapp.h | 5 ++++ src/core/qgslayerdefinition.cpp | 27 ++++++++++++++--- src/core/qgslayerdefinition.h | 29 ++++++++++++++++--- 8 files changed, 122 insertions(+), 21 deletions(-) diff --git a/python/PyQt6/core/auto_generated/qgslayerdefinition.sip.in b/python/PyQt6/core/auto_generated/qgslayerdefinition.sip.in index a96b4c3d629a..ceaa70653802 100644 --- a/python/PyQt6/core/auto_generated/qgslayerdefinition.sip.in +++ b/python/PyQt6/core/auto_generated/qgslayerdefinition.sip.in @@ -25,13 +25,33 @@ files also store the layer tree info for the exported layers, including group in #include "qgslayerdefinition.h" %End public: - static bool loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/ ); + + static bool loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = 0 ); %Docstring Loads the QLR at path into QGIS. New layers are added to given project into layer tree specified by rootGroup + +:param path: file path to the qlr +:param project: the current project +:param rootGroup: the layer tree group to insert the qlr content +:param insertPoint: describes where in rootGroup the qlr layers/groups shall be inserted + +:return: - true in case of success + - errorMessage: the returned error message %End - static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, QgsReadWriteContext &context ); + + static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, QgsReadWriteContext &context, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = 0 ); %Docstring Loads the QLR from the XML document. New layers are added to given project into layer tree specified by rootGroup + +:param doc: the xml document +:param project: the current project +:param rootGroup: the layer tree group to insert the qlr content +:param context: the read write context +:param insertPoint: describes where in rootGroup the qlr layers/groups shall be inserted +:param true: in case of success + +:return: - true in case of success + - errorMessage: the returned error message %End static bool exportLayerDefinition( const QString &path, const QList &selectedTreeNodes, QString &errorMessage /Out/ ); diff --git a/python/core/auto_generated/qgslayerdefinition.sip.in b/python/core/auto_generated/qgslayerdefinition.sip.in index a96b4c3d629a..ceaa70653802 100644 --- a/python/core/auto_generated/qgslayerdefinition.sip.in +++ b/python/core/auto_generated/qgslayerdefinition.sip.in @@ -25,13 +25,33 @@ files also store the layer tree info for the exported layers, including group in #include "qgslayerdefinition.h" %End public: - static bool loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/ ); + + static bool loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = 0 ); %Docstring Loads the QLR at path into QGIS. New layers are added to given project into layer tree specified by rootGroup + +:param path: file path to the qlr +:param project: the current project +:param rootGroup: the layer tree group to insert the qlr content +:param insertPoint: describes where in rootGroup the qlr layers/groups shall be inserted + +:return: - true in case of success + - errorMessage: the returned error message %End - static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, QgsReadWriteContext &context ); + + static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, QgsReadWriteContext &context, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = 0 ); %Docstring Loads the QLR from the XML document. New layers are added to given project into layer tree specified by rootGroup + +:param doc: the xml document +:param project: the current project +:param rootGroup: the layer tree group to insert the qlr content +:param context: the read write context +:param insertPoint: describes where in rootGroup the qlr layers/groups shall be inserted +:param true: in case of success + +:return: - true in case of success + - errorMessage: the returned error message %End static bool exportLayerDefinition( const QString &path, const QList &selectedTreeNodes, QString &errorMessage /Out/ ); diff --git a/src/app/layers/qgsapplayerhandling.cpp b/src/app/layers/qgsapplayerhandling.cpp index 17ba4e979e18..145fa619775d 100644 --- a/src/app/layers/qgsapplayerhandling.cpp +++ b/src/app/layers/qgsapplayerhandling.cpp @@ -1209,7 +1209,7 @@ void QgsAppLayerHandling::addMapLayer( QgsMapLayer *mapLayer, bool addToLegend ) } } -void QgsAppLayerHandling::openLayerDefinition( const QString &filename ) +void QgsAppLayerHandling::openLayerDefinition( const QString &filename, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint ) { QString errorMessage; QgsReadWriteContext context; @@ -1236,7 +1236,7 @@ void QgsAppLayerHandling::openLayerDefinition( const QString &filename ) context.setPathResolver( QgsPathResolver( filename ) ); context.setProjectTranslator( QgsProject::instance() ); - loaded = QgsLayerDefinition::loadLayerDefinition( doc, QgsProject::instance(), QgsProject::instance()->layerTreeRoot(), errorMessage, context ); + loaded = QgsLayerDefinition::loadLayerDefinition( doc, QgsProject::instance(), QgsProject::instance()->layerTreeRoot(), errorMessage, context, insertPoint ); } } @@ -1260,7 +1260,7 @@ void QgsAppLayerHandling::openLayerDefinition( const QString &filename ) } } -void QgsAppLayerHandling::addLayerDefinition() +void QgsAppLayerHandling::addLayerDefinition( const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint ) { QgsSettings settings; QString lastUsedDir = settings.value( QStringLiteral( "UI/lastQLRDir" ), QDir::homePath() ).toString(); @@ -1272,7 +1272,7 @@ void QgsAppLayerHandling::addLayerDefinition() QFileInfo fi( path ); settings.setValue( QStringLiteral( "UI/lastQLRDir" ), fi.path() ); - openLayerDefinition( path ); + openLayerDefinition( path, insertPoint ); } QList< QgsMapLayer * > QgsAppLayerHandling::addDatabaseLayers( const QStringList &layerPathList, const QString &providerKey, bool &ok ) diff --git a/src/app/layers/qgsapplayerhandling.h b/src/app/layers/qgsapplayerhandling.h index 7e16fbbcf729..6df5b3f78ce8 100644 --- a/src/app/layers/qgsapplayerhandling.h +++ b/src/app/layers/qgsapplayerhandling.h @@ -20,6 +20,7 @@ #include "qgsconfig.h" #include "qgsmaplayer.h" #include "qgsvectorlayerref.h" +#include "qgslayertreeregistrybridge.h" #include @@ -149,10 +150,18 @@ class APP_EXPORT QgsAppLayerHandling //! Add a 'pre-made' map layer to the project static void addMapLayer( QgsMapLayer *mapLayer, bool addToLegend = true ); - static void openLayerDefinition( const QString &filename ); + /** + * Opens qlr + * \param filename file path to the qlr + * \param insertPoint describes where the qlr layers/groups shall be inserted + */ + static void openLayerDefinition( const QString &filename, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = nullptr ); - //! Add a Layer Definition file - static void addLayerDefinition(); + /** + * Add a Layer Definition file + * \param insertPoint describes where the qlr layers/groups shall be inserted + */ + static void addLayerDefinition( const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint ); //! Add a list of database layers to the map static QList< QgsMapLayer * > addDatabaseLayers( const QStringList &layerPathList, const QString &providerKey, bool &ok ); diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 6a34efc4a6b4..b40413bb7bf5 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -863,6 +863,12 @@ void QgisApp::annotationItemTypeAdded( int id ) } ); } +void QgisApp::addLayerDefinition() +{ + QgsLayerTreeRegistryBridge::InsertionPoint pt = layerTreeInsertionPoint(); + QgsAppLayerHandling::addLayerDefinition( &pt ); +} + /* * This function contains forced validation of CRS used in QGIS. * There are 4 options depending on the settings: @@ -3030,7 +3036,7 @@ void QgisApp::createActions() connect( mActionShowRasterCalculator, &QAction::triggered, this, &QgisApp::showRasterCalculator ); connect( mActionShowMeshCalculator, &QAction::triggered, this, &QgisApp::showMeshCalculator ); connect( mActionEmbedLayers, &QAction::triggered, this, &QgisApp::embedLayers ); - connect( mActionAddLayerDefinition, &QAction::triggered, this, [] { QgsAppLayerHandling::addLayerDefinition(); } ); + connect( mActionAddLayerDefinition, &QAction::triggered, this, &QgisApp::addLayerDefinition ); connect( mActionAddOgrLayer, &QAction::triggered, this, [ = ] { dataSourceManager( QStringLiteral( "ogr" ) ); } ); connect( mActionAddRasterLayer, &QAction::triggered, this, [ = ] { dataSourceManager( QStringLiteral( "gdal" ) ); } ); connect( mActionAddMeshLayer, &QAction::triggered, this, [ = ] { dataSourceManager( QStringLiteral( "mdal" ) ); } ); @@ -7062,7 +7068,8 @@ QList< QgsMapLayer * > QgisApp::openFile( const QString &fileName, const QString } else if ( fi.suffix().compare( QLatin1String( "qlr" ), Qt::CaseInsensitive ) == 0 ) { - QgsAppLayerHandling::openLayerDefinition( fileName ); + QgsLayerTreeRegistryBridge::InsertionPoint p = layerTreeInsertionPoint(); + QgsAppLayerHandling::openLayerDefinition( fileName, &p ); } else if ( fi.suffix().compare( QLatin1String( "qpt" ), Qt::CaseInsensitive ) == 0 ) { diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index 745c52536a4f..a0a986622b9c 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -2117,6 +2117,11 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow void annotationItemTypeAdded( int id ); + /** + * Open a qlr file + */ + void addLayerDefinition(); + signals: /** diff --git a/src/core/qgslayerdefinition.cpp b/src/core/qgslayerdefinition.cpp index 307218d1f831..518b87a30452 100644 --- a/src/core/qgslayerdefinition.cpp +++ b/src/core/qgslayerdefinition.cpp @@ -38,7 +38,7 @@ #include "qgslayertreegroup.h" #include "qgslayertreelayer.h" -bool QgsLayerDefinition::loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage ) +bool QgsLayerDefinition::loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint ) { QFile file( path ); if ( !file.open( QIODevice::ReadOnly ) ) @@ -62,10 +62,10 @@ bool QgsLayerDefinition::loadLayerDefinition( const QString &path, QgsProject *p context.setPathResolver( QgsPathResolver( path ) ); context.setProjectTranslator( project ); - return loadLayerDefinition( doc, project, rootGroup, errorMessage, context ); + return loadLayerDefinition( doc, project, rootGroup, errorMessage, context, insertPoint ); } -bool QgsLayerDefinition::loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage, QgsReadWriteContext &context ) +bool QgsLayerDefinition::loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage, QgsReadWriteContext &context, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint ) { errorMessage.clear(); @@ -195,7 +195,26 @@ bool QgsLayerDefinition::loadLayerDefinition( QDomDocument doc, QgsProject *proj root->abandonChildren(); delete root; - rootGroup->insertChildNodes( -1, nodes ); + QgsSettings settings; + if ( !insertPoint ) + { + rootGroup->insertChildNodes( -1, nodes ); + } + else + { + Qgis::LayerTreeInsertionMethod insertionMethod = settings.enumValue( QStringLiteral( "qgis/layerTreeInsertionMethod" ), Qgis::LayerTreeInsertionMethod::AboveInsertionPoint ); + switch ( insertionMethod ) + { + case Qgis::LayerTreeInsertionMethod::AboveInsertionPoint: + insertPoint->group->insertChildNodes( insertPoint->position, nodes ); + break; + case Qgis::LayerTreeInsertionMethod::TopOfTree: + rootGroup->insertChildNodes( 0, nodes ); + break; + default: + rootGroup->insertChildNodes( -1, nodes ); //Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup does not really make sense for qlr + } + } return true; } diff --git a/src/core/qgslayerdefinition.h b/src/core/qgslayerdefinition.h index 97d1c472a470..12ed746d609d 100644 --- a/src/core/qgslayerdefinition.h +++ b/src/core/qgslayerdefinition.h @@ -19,6 +19,7 @@ #include "qgis_core.h" #include "qgis_sip.h" #include "qgis.h" +#include "qgslayertreeregistrybridge.h" #include #include @@ -44,10 +45,30 @@ class QgsProject; class CORE_EXPORT QgsLayerDefinition { public: - //! Loads the QLR at path into QGIS. New layers are added to given project into layer tree specified by rootGroup - static bool loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage SIP_OUT ); - //! Loads the QLR from the XML document. New layers are added to given project into layer tree specified by rootGroup - static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage SIP_OUT, QgsReadWriteContext &context ); + + /** + * Loads the QLR at path into QGIS. New layers are added to given project into layer tree specified by rootGroup + * \param path file path to the qlr + * \param project the current project + * \param rootGroup the layer tree group to insert the qlr content + * \param errorMessage the returned error message + * \param insertPoint describes where in rootGroup the qlr layers/groups shall be inserted + * \return true in case of success + */ + static bool loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage SIP_OUT, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = nullptr ); + + /** + * Loads the QLR from the XML document. New layers are added to given project into layer tree specified by rootGroup + * \param doc the xml document + * \param project the current project + * \param rootGroup the layer tree group to insert the qlr content + * \param errorMessage the returned error message + * \param context the read write context + * \param insertPoint describes where in rootGroup the qlr layers/groups shall be inserted + * \param true in case of success + * \return true in case of success + */ + static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage SIP_OUT, QgsReadWriteContext &context, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = nullptr ); /** * Exports the selected layer tree nodes to a QLR file. From 62110c890f17752847df5d073c364c473705c2fa Mon Sep 17 00:00:00 2001 From: Marco Hugentobler Date: Mon, 25 Mar 2024 11:17:13 +0100 Subject: [PATCH 80/87] Add unit test --- src/core/qgslayerdefinition.cpp | 33 ++++++++++++----------- tests/src/core/testqgslayerdefinition.cpp | 18 +++++++++++++ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/core/qgslayerdefinition.cpp b/src/core/qgslayerdefinition.cpp index 518b87a30452..e62aaf4b3054 100644 --- a/src/core/qgslayerdefinition.cpp +++ b/src/core/qgslayerdefinition.cpp @@ -196,24 +196,25 @@ bool QgsLayerDefinition::loadLayerDefinition( QDomDocument doc, QgsProject *proj delete root; QgsSettings settings; - if ( !insertPoint ) - { - rootGroup->insertChildNodes( -1, nodes ); - } - else + + Qgis::LayerTreeInsertionMethod insertionMethod = settings.enumValue( QStringLiteral( "/qgis/layerTreeInsertionMethod" ), Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup ); + switch ( insertionMethod ) { - Qgis::LayerTreeInsertionMethod insertionMethod = settings.enumValue( QStringLiteral( "qgis/layerTreeInsertionMethod" ), Qgis::LayerTreeInsertionMethod::AboveInsertionPoint ); - switch ( insertionMethod ) - { - case Qgis::LayerTreeInsertionMethod::AboveInsertionPoint: + case Qgis::LayerTreeInsertionMethod::AboveInsertionPoint: + if ( insertPoint ) + { insertPoint->group->insertChildNodes( insertPoint->position, nodes ); - break; - case Qgis::LayerTreeInsertionMethod::TopOfTree: - rootGroup->insertChildNodes( 0, nodes ); - break; - default: - rootGroup->insertChildNodes( -1, nodes ); //Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup does not really make sense for qlr - } + } + else + { + rootGroup->insertChildNodes( -1, nodes ); + } + break; + case Qgis::LayerTreeInsertionMethod::TopOfTree: + rootGroup->insertChildNodes( 0, nodes ); + break; + default: //Keep current behavior for Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup + rootGroup->insertChildNodes( -1, nodes ); } return true; diff --git a/tests/src/core/testqgslayerdefinition.cpp b/tests/src/core/testqgslayerdefinition.cpp index 6bf8f69032fe..7cb936cf794c 100644 --- a/tests/src/core/testqgslayerdefinition.cpp +++ b/tests/src/core/testqgslayerdefinition.cpp @@ -41,6 +41,11 @@ class TestQgsLayerDefinition: public QObject */ void testFindLayers(); + /** + * Tests loading a qlr placing the content at the top of the layer tree + */ + void testLoadTopOfTree(); + /** * test that export does not crash: regression #18981 * https://github.com/qgis/QGIS/issues/26812 - Save QLR crashes QGIS 3 @@ -90,6 +95,19 @@ void TestQgsLayerDefinition::testFindLayers() QCOMPARE( QgsProject::instance()->layerTreeRoot()->findLayers().at( 1 )->name(), QStringLiteral( "NewMemory" ) ); } +void TestQgsLayerDefinition::testLoadTopOfTree() +{ + QgsSettings settings; + settings.setEnumValue( QStringLiteral( "/qgis/layerTreeInsertionMethod" ), Qgis::LayerTreeInsertionMethod::TopOfTree ); + QString errorMsg; + QgsLayerDefinition::loadLayerDefinition( TEST_DATA_DIR + QStringLiteral( "/vector_and_raster.qlr" ), QgsProject::instance(), QgsProject::instance()->layerTreeRoot(), errorMsg ); + //todo: test if new layers are on top + QList orderedLayers = QgsProject::instance()->layerTreeRoot()->layerOrder(); + QCOMPARE( orderedLayers.length(), 3 ); + QVERIFY( orderedLayers.at( 1 )->name() == QStringLiteral( "rgb256x256" ) ); + QVERIFY( orderedLayers.at( 0 )->name() == QStringLiteral( "memoryLayer" ) ); +} + void TestQgsLayerDefinition::testExportDoesNotCrash() { QString errorMessage; From ffeb91c2eb37549de27e9d3a67417c415302ebc1 Mon Sep 17 00:00:00 2001 From: Marco Hugentobler Date: Mon, 25 Mar 2024 12:53:56 +0100 Subject: [PATCH 81/87] Fix documentation --- python/PyQt6/core/auto_generated/qgslayerdefinition.sip.in | 1 - python/core/auto_generated/qgslayerdefinition.sip.in | 1 - src/core/qgslayerdefinition.h | 1 - 3 files changed, 3 deletions(-) diff --git a/python/PyQt6/core/auto_generated/qgslayerdefinition.sip.in b/python/PyQt6/core/auto_generated/qgslayerdefinition.sip.in index ceaa70653802..cb87faf3aad1 100644 --- a/python/PyQt6/core/auto_generated/qgslayerdefinition.sip.in +++ b/python/PyQt6/core/auto_generated/qgslayerdefinition.sip.in @@ -48,7 +48,6 @@ Loads the QLR from the XML document. New layers are added to given project into :param rootGroup: the layer tree group to insert the qlr content :param context: the read write context :param insertPoint: describes where in rootGroup the qlr layers/groups shall be inserted -:param true: in case of success :return: - true in case of success - errorMessage: the returned error message diff --git a/python/core/auto_generated/qgslayerdefinition.sip.in b/python/core/auto_generated/qgslayerdefinition.sip.in index ceaa70653802..cb87faf3aad1 100644 --- a/python/core/auto_generated/qgslayerdefinition.sip.in +++ b/python/core/auto_generated/qgslayerdefinition.sip.in @@ -48,7 +48,6 @@ Loads the QLR from the XML document. New layers are added to given project into :param rootGroup: the layer tree group to insert the qlr content :param context: the read write context :param insertPoint: describes where in rootGroup the qlr layers/groups shall be inserted -:param true: in case of success :return: - true in case of success - errorMessage: the returned error message diff --git a/src/core/qgslayerdefinition.h b/src/core/qgslayerdefinition.h index 12ed746d609d..5bafeb2b9027 100644 --- a/src/core/qgslayerdefinition.h +++ b/src/core/qgslayerdefinition.h @@ -65,7 +65,6 @@ class CORE_EXPORT QgsLayerDefinition * \param errorMessage the returned error message * \param context the read write context * \param insertPoint describes where in rootGroup the qlr layers/groups shall be inserted - * \param true in case of success * \return true in case of success */ static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage SIP_OUT, QgsReadWriteContext &context, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = nullptr ); From 76c641666d8cc7df7c99954452f974d41cb47790 Mon Sep 17 00:00:00 2001 From: mhugent Date: Wed, 27 Mar 2024 07:55:28 +0100 Subject: [PATCH 82/87] Update src/core/qgslayerdefinition.h Co-authored-by: Nyall Dawson --- src/core/qgslayerdefinition.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/qgslayerdefinition.h b/src/core/qgslayerdefinition.h index 5bafeb2b9027..a7d732fc8884 100644 --- a/src/core/qgslayerdefinition.h +++ b/src/core/qgslayerdefinition.h @@ -52,7 +52,7 @@ class CORE_EXPORT QgsLayerDefinition * \param project the current project * \param rootGroup the layer tree group to insert the qlr content * \param errorMessage the returned error message - * \param insertPoint describes where in rootGroup the qlr layers/groups shall be inserted + * \param insertPoint describes where in rootGroup the qlr layers/groups shall be inserted (since QGIS 3.38) * \return true in case of success */ static bool loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage SIP_OUT, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = nullptr ); From cbec502ebf490280b99696555e30c4a85e683cc4 Mon Sep 17 00:00:00 2001 From: mhugent Date: Wed, 27 Mar 2024 07:55:41 +0100 Subject: [PATCH 83/87] Update src/core/qgslayerdefinition.h Co-authored-by: Nyall Dawson --- src/core/qgslayerdefinition.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/qgslayerdefinition.h b/src/core/qgslayerdefinition.h index a7d732fc8884..eef01167e97e 100644 --- a/src/core/qgslayerdefinition.h +++ b/src/core/qgslayerdefinition.h @@ -64,7 +64,7 @@ class CORE_EXPORT QgsLayerDefinition * \param rootGroup the layer tree group to insert the qlr content * \param errorMessage the returned error message * \param context the read write context - * \param insertPoint describes where in rootGroup the qlr layers/groups shall be inserted + * \param insertPoint describes where in rootGroup the qlr layers/groups shall be inserted (since QGIS 3.38) * \return true in case of success */ static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage SIP_OUT, QgsReadWriteContext &context, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = nullptr ); From 077a161ef1a103cd35c598a17cb7b144fc696396 Mon Sep 17 00:00:00 2001 From: Marco Hugentobler Date: Wed, 27 Mar 2024 10:53:26 +0100 Subject: [PATCH 84/87] Move query of layerTreeInsertionMethod setting out of core --- .../core/auto_generated/qgslayerdefinition.sip.in | 10 ++++++---- python/core/auto_generated/qgslayerdefinition.sip.in | 10 ++++++---- src/app/layers/qgsapplayerhandling.cpp | 4 +++- src/app/qgisapp.cpp | 5 ++++- src/core/qgslayerdefinition.cpp | 11 ++++------- src/core/qgslayerdefinition.h | 6 ++++-- tests/src/core/testqgslayerdefinition.cpp | 4 +--- 7 files changed, 28 insertions(+), 22 deletions(-) diff --git a/python/PyQt6/core/auto_generated/qgslayerdefinition.sip.in b/python/PyQt6/core/auto_generated/qgslayerdefinition.sip.in index cb87faf3aad1..75c525ddff91 100644 --- a/python/PyQt6/core/auto_generated/qgslayerdefinition.sip.in +++ b/python/PyQt6/core/auto_generated/qgslayerdefinition.sip.in @@ -26,20 +26,21 @@ files also store the layer tree info for the exported layers, including group in %End public: - static bool loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = 0 ); + static bool loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, Qgis::LayerTreeInsertionMethod insertMethod = Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = 0 ); %Docstring Loads the QLR at path into QGIS. New layers are added to given project into layer tree specified by rootGroup :param path: file path to the qlr :param project: the current project :param rootGroup: the layer tree group to insert the qlr content -:param insertPoint: describes where in rootGroup the qlr layers/groups shall be inserted +:param insertMethod: method for layer tree (since QGIS 3.38) +:param insertPoint: describes where in rootGroup the qlr layers/groups shall be inserted (since QGIS 3.38) :return: - true in case of success - errorMessage: the returned error message %End - static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, QgsReadWriteContext &context, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = 0 ); + static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, QgsReadWriteContext &context, Qgis::LayerTreeInsertionMethod insertMethod = Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = 0 ); %Docstring Loads the QLR from the XML document. New layers are added to given project into layer tree specified by rootGroup @@ -47,7 +48,8 @@ Loads the QLR from the XML document. New layers are added to given project into :param project: the current project :param rootGroup: the layer tree group to insert the qlr content :param context: the read write context -:param insertPoint: describes where in rootGroup the qlr layers/groups shall be inserted +:param insertMethod: method for layer tree (since QGIS 3.38) +:param insertPoint: describes where in rootGroup the qlr layers/groups shall be inserted (since QGIS 3.38) :return: - true in case of success - errorMessage: the returned error message diff --git a/python/core/auto_generated/qgslayerdefinition.sip.in b/python/core/auto_generated/qgslayerdefinition.sip.in index cb87faf3aad1..75c525ddff91 100644 --- a/python/core/auto_generated/qgslayerdefinition.sip.in +++ b/python/core/auto_generated/qgslayerdefinition.sip.in @@ -26,20 +26,21 @@ files also store the layer tree info for the exported layers, including group in %End public: - static bool loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = 0 ); + static bool loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, Qgis::LayerTreeInsertionMethod insertMethod = Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = 0 ); %Docstring Loads the QLR at path into QGIS. New layers are added to given project into layer tree specified by rootGroup :param path: file path to the qlr :param project: the current project :param rootGroup: the layer tree group to insert the qlr content -:param insertPoint: describes where in rootGroup the qlr layers/groups shall be inserted +:param insertMethod: method for layer tree (since QGIS 3.38) +:param insertPoint: describes where in rootGroup the qlr layers/groups shall be inserted (since QGIS 3.38) :return: - true in case of success - errorMessage: the returned error message %End - static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, QgsReadWriteContext &context, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = 0 ); + static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, QgsReadWriteContext &context, Qgis::LayerTreeInsertionMethod insertMethod = Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = 0 ); %Docstring Loads the QLR from the XML document. New layers are added to given project into layer tree specified by rootGroup @@ -47,7 +48,8 @@ Loads the QLR from the XML document. New layers are added to given project into :param project: the current project :param rootGroup: the layer tree group to insert the qlr content :param context: the read write context -:param insertPoint: describes where in rootGroup the qlr layers/groups shall be inserted +:param insertMethod: method for layer tree (since QGIS 3.38) +:param insertPoint: describes where in rootGroup the qlr layers/groups shall be inserted (since QGIS 3.38) :return: - true in case of success - errorMessage: the returned error message diff --git a/src/app/layers/qgsapplayerhandling.cpp b/src/app/layers/qgsapplayerhandling.cpp index 145fa619775d..43ed92f8fdf7 100644 --- a/src/app/layers/qgsapplayerhandling.cpp +++ b/src/app/layers/qgsapplayerhandling.cpp @@ -1236,7 +1236,9 @@ void QgsAppLayerHandling::openLayerDefinition( const QString &filename, const Qg context.setPathResolver( QgsPathResolver( filename ) ); context.setProjectTranslator( QgsProject::instance() ); - loaded = QgsLayerDefinition::loadLayerDefinition( doc, QgsProject::instance(), QgsProject::instance()->layerTreeRoot(), errorMessage, context, insertPoint ); + QgsSettings settings; + Qgis::LayerTreeInsertionMethod insertionMethod = settings.enumValue( QStringLiteral( "/qgis/layerTreeInsertionMethod" ), Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup ); + loaded = QgsLayerDefinition::loadLayerDefinition( doc, QgsProject::instance(), QgsProject::instance()->layerTreeRoot(), errorMessage, context, insertionMethod, insertPoint ); } } diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index b40413bb7bf5..84f643c23b3b 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -10791,8 +10791,11 @@ void QgisApp::pasteLayer() root = QgsProject::instance()->layerTreeRoot(); } + QgsSettings settings; + Qgis::LayerTreeInsertionMethod insertionMethod = settings.enumValue( QStringLiteral( "/qgis/layerTreeInsertionMethod" ), Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup ); + QgsLayerTreeRegistryBridge::InsertionPoint insertionPoint = layerTreeInsertionPoint(); bool loaded = QgsLayerDefinition::loadLayerDefinition( doc, QgsProject::instance(), root, - errorMessage, readWriteContext ); + errorMessage, readWriteContext, insertionMethod, &insertionPoint ); if ( !loaded || !errorMessage.isEmpty() ) { diff --git a/src/core/qgslayerdefinition.cpp b/src/core/qgslayerdefinition.cpp index e62aaf4b3054..40e74703b328 100644 --- a/src/core/qgslayerdefinition.cpp +++ b/src/core/qgslayerdefinition.cpp @@ -38,7 +38,7 @@ #include "qgslayertreegroup.h" #include "qgslayertreelayer.h" -bool QgsLayerDefinition::loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint ) +bool QgsLayerDefinition::loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage, Qgis::LayerTreeInsertionMethod insertMethod, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint ) { QFile file( path ); if ( !file.open( QIODevice::ReadOnly ) ) @@ -62,10 +62,10 @@ bool QgsLayerDefinition::loadLayerDefinition( const QString &path, QgsProject *p context.setPathResolver( QgsPathResolver( path ) ); context.setProjectTranslator( project ); - return loadLayerDefinition( doc, project, rootGroup, errorMessage, context, insertPoint ); + return loadLayerDefinition( doc, project, rootGroup, errorMessage, context, insertMethod, insertPoint ); } -bool QgsLayerDefinition::loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage, QgsReadWriteContext &context, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint ) +bool QgsLayerDefinition::loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage, QgsReadWriteContext &context, Qgis::LayerTreeInsertionMethod insertMethod, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint ) { errorMessage.clear(); @@ -195,10 +195,7 @@ bool QgsLayerDefinition::loadLayerDefinition( QDomDocument doc, QgsProject *proj root->abandonChildren(); delete root; - QgsSettings settings; - - Qgis::LayerTreeInsertionMethod insertionMethod = settings.enumValue( QStringLiteral( "/qgis/layerTreeInsertionMethod" ), Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup ); - switch ( insertionMethod ) + switch ( insertMethod ) { case Qgis::LayerTreeInsertionMethod::AboveInsertionPoint: if ( insertPoint ) diff --git a/src/core/qgslayerdefinition.h b/src/core/qgslayerdefinition.h index eef01167e97e..5d9f812f91b1 100644 --- a/src/core/qgslayerdefinition.h +++ b/src/core/qgslayerdefinition.h @@ -52,10 +52,11 @@ class CORE_EXPORT QgsLayerDefinition * \param project the current project * \param rootGroup the layer tree group to insert the qlr content * \param errorMessage the returned error message + * \param insertMethod method for layer tree (since QGIS 3.38) * \param insertPoint describes where in rootGroup the qlr layers/groups shall be inserted (since QGIS 3.38) * \return true in case of success */ - static bool loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage SIP_OUT, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = nullptr ); + static bool loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage SIP_OUT, Qgis::LayerTreeInsertionMethod insertMethod = Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = nullptr ); /** * Loads the QLR from the XML document. New layers are added to given project into layer tree specified by rootGroup @@ -64,10 +65,11 @@ class CORE_EXPORT QgsLayerDefinition * \param rootGroup the layer tree group to insert the qlr content * \param errorMessage the returned error message * \param context the read write context + * \param insertMethod method for layer tree (since QGIS 3.38) * \param insertPoint describes where in rootGroup the qlr layers/groups shall be inserted (since QGIS 3.38) * \return true in case of success */ - static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage SIP_OUT, QgsReadWriteContext &context, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = nullptr ); + static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage SIP_OUT, QgsReadWriteContext &context, Qgis::LayerTreeInsertionMethod insertMethod = Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = nullptr ); /** * Exports the selected layer tree nodes to a QLR file. diff --git a/tests/src/core/testqgslayerdefinition.cpp b/tests/src/core/testqgslayerdefinition.cpp index 7cb936cf794c..6c3fe860b950 100644 --- a/tests/src/core/testqgslayerdefinition.cpp +++ b/tests/src/core/testqgslayerdefinition.cpp @@ -97,10 +97,8 @@ void TestQgsLayerDefinition::testFindLayers() void TestQgsLayerDefinition::testLoadTopOfTree() { - QgsSettings settings; - settings.setEnumValue( QStringLiteral( "/qgis/layerTreeInsertionMethod" ), Qgis::LayerTreeInsertionMethod::TopOfTree ); QString errorMsg; - QgsLayerDefinition::loadLayerDefinition( TEST_DATA_DIR + QStringLiteral( "/vector_and_raster.qlr" ), QgsProject::instance(), QgsProject::instance()->layerTreeRoot(), errorMsg ); + QgsLayerDefinition::loadLayerDefinition( TEST_DATA_DIR + QStringLiteral( "/vector_and_raster.qlr" ), QgsProject::instance(), QgsProject::instance()->layerTreeRoot(), errorMsg, Qgis::LayerTreeInsertionMethod::TopOfTree ); //todo: test if new layers are on top QList orderedLayers = QgsProject::instance()->layerTreeRoot()->layerOrder(); QCOMPARE( orderedLayers.length(), 3 ); From cb59f7a2f98010642ea80670b182036e32942b6a Mon Sep 17 00:00:00 2001 From: Marco Hugentobler Date: Sun, 7 Apr 2024 10:23:59 +0200 Subject: [PATCH 85/87] Remove todo --- tests/src/core/testqgslayerdefinition.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/core/testqgslayerdefinition.cpp b/tests/src/core/testqgslayerdefinition.cpp index 6c3fe860b950..d1b35e0bb013 100644 --- a/tests/src/core/testqgslayerdefinition.cpp +++ b/tests/src/core/testqgslayerdefinition.cpp @@ -99,7 +99,7 @@ void TestQgsLayerDefinition::testLoadTopOfTree() { QString errorMsg; QgsLayerDefinition::loadLayerDefinition( TEST_DATA_DIR + QStringLiteral( "/vector_and_raster.qlr" ), QgsProject::instance(), QgsProject::instance()->layerTreeRoot(), errorMsg, Qgis::LayerTreeInsertionMethod::TopOfTree ); - //todo: test if new layers are on top + //test if new layers are on top QList orderedLayers = QgsProject::instance()->layerTreeRoot()->layerOrder(); QCOMPARE( orderedLayers.length(), 3 ); QVERIFY( orderedLayers.at( 1 )->name() == QStringLiteral( "rgb256x256" ) ); From 534926b46ab9eabeae3667ed482b583f9b33e4b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ko=C5=88a=C5=99=C3=ADk?= Date: Sat, 6 Apr 2024 16:35:46 +0200 Subject: [PATCH 86/87] Add QtWebEngine re-exports to qgis.PyQt --- python/PyQt/CMakeLists.txt | 3 +++ python/PyQt/PyQt/QtWebEngineCore.py.in | 20 ++++++++++++++++++++ python/PyQt/PyQt/QtWebEngineQuick.py.in | 23 +++++++++++++++++++++++ python/PyQt/PyQt/QtWebEngineWidgets.py.in | 20 ++++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 python/PyQt/PyQt/QtWebEngineCore.py.in create mode 100644 python/PyQt/PyQt/QtWebEngineQuick.py.in create mode 100644 python/PyQt/PyQt/QtWebEngineWidgets.py.in diff --git a/python/PyQt/CMakeLists.txt b/python/PyQt/CMakeLists.txt index a54b918070c2..8b062819d1a5 100644 --- a/python/PyQt/CMakeLists.txt +++ b/python/PyQt/CMakeLists.txt @@ -10,6 +10,9 @@ set(PYQT_COMPAT_FILES QtPrintSupport.py QtWebKit.py QtWebKitWidgets.py + QtWebEngineCore.py + QtWebEngineQuick.py + QtWebEngineWidgets.py QtNetwork.py QtXml.py QtSql.py diff --git a/python/PyQt/PyQt/QtWebEngineCore.py.in b/python/PyQt/PyQt/QtWebEngineCore.py.in new file mode 100644 index 000000000000..791bb3f6e381 --- /dev/null +++ b/python/PyQt/PyQt/QtWebEngineCore.py.in @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + QtWebEngineCore.py + --------------------- + Date : April 2024 + Copyright : (C) 2024 David Koňařík + Email : dvdkon at konarici dot cz +*************************************************************************** +* * +* 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 PyQt@QT_VERSION_MAJOR@.QtWebEngineCore import * diff --git a/python/PyQt/PyQt/QtWebEngineQuick.py.in b/python/PyQt/PyQt/QtWebEngineQuick.py.in new file mode 100644 index 000000000000..5b940beb35bf --- /dev/null +++ b/python/PyQt/PyQt/QtWebEngineQuick.py.in @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + QtWebEngineQuick.py + --------------------- + Date : April 2024 + Copyright : (C) 2024 David Koňařík + Email : dvdkon at konarici dot cz +*************************************************************************** +* * +* 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. * +* * +*************************************************************************** +""" + +if @QT_VERSION_MAJOR@ == 5: + from PyQt5.QtWebEngine import * +else: + from PyQt@QT_VERSION_MAJOR@.QtWebEngineQuick import * diff --git a/python/PyQt/PyQt/QtWebEngineWidgets.py.in b/python/PyQt/PyQt/QtWebEngineWidgets.py.in new file mode 100644 index 000000000000..49364d6c6816 --- /dev/null +++ b/python/PyQt/PyQt/QtWebEngineWidgets.py.in @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + QtWebEngineWidgets.py + --------------------- + Date : April 2024 + Copyright : (C) 2024 David Koňařík + Email : dvdkon at konarici dot cz +*************************************************************************** +* * +* 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 PyQt@QT_VERSION_MAJOR@.QtWebEngineWidgets import * From 3e19378708eae6edcd997042a03ca048d0706047 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 9 Apr 2024 12:52:53 +1000 Subject: [PATCH 87/87] Ensure Refresh action is shown for Geopackage connection items Fixes #51523 --- .../ogr/qgsgeopackageitemguiprovider.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/gui/providers/ogr/qgsgeopackageitemguiprovider.cpp b/src/gui/providers/ogr/qgsgeopackageitemguiprovider.cpp index b04c6c668d99..be0159091253 100644 --- a/src/gui/providers/ogr/qgsgeopackageitemguiprovider.cpp +++ b/src/gui/providers/ogr/qgsgeopackageitemguiprovider.cpp @@ -85,6 +85,24 @@ void QgsGeoPackageItemGuiProvider::populateContextMenu( QgsDataItem *item, QMenu if ( QgsGeoPackageCollectionItem *collectionItem = qobject_cast< QgsGeoPackageCollectionItem * >( item ) ) { + if ( !( item->capabilities2() & Qgis::BrowserItemCapability::ItemRepresentsFile ) ) + { + // add a refresh action, but ONLY if the collection item isn't representing a file + // (if so, then QgsAppFileItemGuiProvider will add the Refresh item) + QAction *actionRefresh = new QAction( QObject::tr( "Refresh" ), menu ); + connect( actionRefresh, &QAction::triggered, collectionItem, [collectionItem] { collectionItem->refresh(); } ); + if ( !menu->actions().empty() ) + { + QAction *firstAction = menu->actions().at( 0 ); + menu->insertAction( firstAction, actionRefresh ); + menu->insertSeparator( firstAction ); + } + else + { + menu->addAction( actionRefresh ); + } + } + menu->addSeparator(); if ( QgsOgrDbConnection::connectionList( QStringLiteral( "GPKG" ) ).contains( collectionItem->name() ) )