From ea4d9ce64b2f516479763fef6650ccdb41b847cc Mon Sep 17 00:00:00 2001 From: signedav Date: Fri, 19 Apr 2024 13:54:42 +0200 Subject: [PATCH 001/102] DuplicatePolicy like default value / dupicate / remove value to define what has to be done on duplicating a feature. --- python/PyQt6/core/auto_additions/qgis.py | 7 +++ python/PyQt6/core/auto_generated/qgis.sip.in | 7 +++ .../PyQt6/core/auto_generated/qgsfield.sip.in | 20 +++++++ .../vector/qgsvectorlayer.sip.in | 21 +++++++ python/core/auto_additions/qgis.py | 7 +++ python/core/auto_generated/qgis.sip.in | 7 +++ python/core/auto_generated/qgsfield.sip.in | 20 +++++++ .../vector/qgsvectorlayer.sip.in | 21 +++++++ src/core/qgis.h | 16 ++++++ src/core/qgsfield.cpp | 19 ++++++- src/core/qgsfield.h | 25 ++++++-- src/core/qgsfield_p.h | 5 ++ src/core/vector/qgsvectorlayer.cpp | 57 +++++++++++++++++++ src/core/vector/qgsvectorlayer.h | 30 ++++++++++ src/core/vector/qgsvectorlayereditutils.cpp | 3 +- src/core/vector/qgsvectorlayerutils.cpp | 25 +++++++- .../qgsattributetypedialog.cpp | 37 ++++++++++++ .../qgsattributetypedialog.h | 20 +++++++ .../vector/qgsattributesformproperties.cpp | 4 ++ src/gui/vector/qgsattributesformproperties.h | 1 + .../qgsattributetypeedit.ui | 28 +++++++-- 21 files changed, 368 insertions(+), 12 deletions(-) diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index 02cb7d44a2285..805449190defe 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -3204,6 +3204,13 @@ # -- Qgis.FieldDomainMergePolicy.baseClass = Qgis # monkey patching scoped based enum +Qgis.FieldDomainDuplicatePolicy.DefaultValue.__doc__ = "Use default field value" +Qgis.FieldDomainDuplicatePolicy.Duplicate.__doc__ = "Duplicate original value" +Qgis.FieldDomainDuplicatePolicy.UnsetField.__doc__ = "Clears the field value so that the data provider backend will populate using any backend triggers or similar logic (since QGIS 3.30)" +Qgis.FieldDomainDuplicatePolicy.__doc__ = "Duplicate policy for field domains.\n\nWhen a feature is duplicated, defines how the value of attributes\nfollowing the domain are computed.\n\n.. versionadded:: 3.38\n\n" + '* ``DefaultValue``: ' + Qgis.FieldDomainDuplicatePolicy.DefaultValue.__doc__ + '\n' + '* ``Duplicate``: ' + Qgis.FieldDomainDuplicatePolicy.Duplicate.__doc__ + '\n' + '* ``UnsetField``: ' + Qgis.FieldDomainDuplicatePolicy.UnsetField.__doc__ +# -- +Qgis.FieldDomainDuplicatePolicy.baseClass = Qgis +# monkey patching scoped based enum Qgis.FieldDomainType.Coded.__doc__ = "Coded field domain" Qgis.FieldDomainType.Range.__doc__ = "Numeric range field domain (min/max)" Qgis.FieldDomainType.Glob.__doc__ = "Glob string pattern field domain" diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index 38b07fa1399c3..38ccaed157429 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -1817,6 +1817,13 @@ The development version GeometryWeighted, }; + enum class FieldDomainDuplicatePolicy /BaseType=IntEnum/ + { + DefaultValue, + Duplicate, + UnsetField, + }; + enum class FieldDomainType /BaseType=IntEnum/ { Coded, diff --git a/python/PyQt6/core/auto_generated/qgsfield.sip.in b/python/PyQt6/core/auto_generated/qgsfield.sip.in index 655275c4790e3..65130f8ccf9be 100644 --- a/python/PyQt6/core/auto_generated/qgsfield.sip.in +++ b/python/PyQt6/core/auto_generated/qgsfield.sip.in @@ -466,6 +466,26 @@ be handled during a split operation. .. seealso:: :py:func:`splitPolicy` .. versionadded:: 3.30 +%End + + Qgis::FieldDomainDuplicatePolicy duplicatePolicy() const; +%Docstring +Returns the field's duplicate policy, which indicates how field values should +be handled during a duplicate operation. + +.. seealso:: :py:func:`setDuplicatePolicy` + +.. versionadded:: 3.38 +%End + + void setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy policy ); +%Docstring +Sets the field's duplicate ``policy``, which indicates how field values should +be handled during a duplicate operation. + +.. seealso:: :py:func:`duplicatePolicy` + +.. versionadded:: 3.38 %End SIP_PYOBJECT __repr__(); diff --git a/python/PyQt6/core/auto_generated/vector/qgsvectorlayer.sip.in b/python/PyQt6/core/auto_generated/vector/qgsvectorlayer.sip.in index 9aa67e3bd4aff..3c8e2e5fd9afe 100644 --- a/python/PyQt6/core/auto_generated/vector/qgsvectorlayer.sip.in +++ b/python/PyQt6/core/auto_generated/vector/qgsvectorlayer.sip.in @@ -1935,6 +1935,27 @@ Sets a split ``policy`` for the field with the specified index. } %End + void setFieldDuplicatePolicy( int index, Qgis::FieldDomainDuplicatePolicy policy ); +%Docstring +Sets a duplicate ``policy`` for the field with the specified index. + +:raises KeyError: if no field with the specified index exists + +.. versionadded:: 3.38 +%End + +%MethodCode + if ( a0 < 0 || a0 >= sipCpp->fields().count() ) + { + PyErr_SetString( PyExc_KeyError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + else + { + sipCpp->setFieldDuplicatePolicy( a0, a1 ); + } +%End + QSet excludeAttributesWms() const /Deprecated/; %Docstring A set of attributes that are not advertised in WMS requests with QGIS server. diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index 4918dd1f3f26a..05794066b2363 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -3149,6 +3149,13 @@ # -- Qgis.FieldDomainMergePolicy.baseClass = Qgis # monkey patching scoped based enum +Qgis.FieldDomainDuplicatePolicy.DefaultValue.__doc__ = "Use default field value" +Qgis.FieldDomainDuplicatePolicy.Duplicate.__doc__ = "Duplicate original value" +Qgis.FieldDomainDuplicatePolicy.UnsetField.__doc__ = "Clears the field value so that the data provider backend will populate using any backend triggers or similar logic (since QGIS 3.30)" +Qgis.FieldDomainDuplicatePolicy.__doc__ = "Duplicate policy for field domains.\n\nWhen a feature is duplicated, defines how the value of attributes\nfollowing the domain are computed.\n\n.. versionadded:: 3.38\n\n" + '* ``DefaultValue``: ' + Qgis.FieldDomainDuplicatePolicy.DefaultValue.__doc__ + '\n' + '* ``Duplicate``: ' + Qgis.FieldDomainDuplicatePolicy.Duplicate.__doc__ + '\n' + '* ``UnsetField``: ' + Qgis.FieldDomainDuplicatePolicy.UnsetField.__doc__ +# -- +Qgis.FieldDomainDuplicatePolicy.baseClass = Qgis +# monkey patching scoped based enum Qgis.FieldDomainType.Coded.__doc__ = "Coded field domain" Qgis.FieldDomainType.Range.__doc__ = "Numeric range field domain (min/max)" Qgis.FieldDomainType.Glob.__doc__ = "Glob string pattern field domain" diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index 965636863fe2d..31c60298831bf 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -1817,6 +1817,13 @@ The development version GeometryWeighted, }; + enum class FieldDomainDuplicatePolicy + { + DefaultValue, + Duplicate, + UnsetField, + }; + enum class FieldDomainType { Coded, diff --git a/python/core/auto_generated/qgsfield.sip.in b/python/core/auto_generated/qgsfield.sip.in index 655275c4790e3..65130f8ccf9be 100644 --- a/python/core/auto_generated/qgsfield.sip.in +++ b/python/core/auto_generated/qgsfield.sip.in @@ -466,6 +466,26 @@ be handled during a split operation. .. seealso:: :py:func:`splitPolicy` .. versionadded:: 3.30 +%End + + Qgis::FieldDomainDuplicatePolicy duplicatePolicy() const; +%Docstring +Returns the field's duplicate policy, which indicates how field values should +be handled during a duplicate operation. + +.. seealso:: :py:func:`setDuplicatePolicy` + +.. versionadded:: 3.38 +%End + + void setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy policy ); +%Docstring +Sets the field's duplicate ``policy``, which indicates how field values should +be handled during a duplicate operation. + +.. seealso:: :py:func:`duplicatePolicy` + +.. versionadded:: 3.38 %End SIP_PYOBJECT __repr__(); diff --git a/python/core/auto_generated/vector/qgsvectorlayer.sip.in b/python/core/auto_generated/vector/qgsvectorlayer.sip.in index 9aa67e3bd4aff..3c8e2e5fd9afe 100644 --- a/python/core/auto_generated/vector/qgsvectorlayer.sip.in +++ b/python/core/auto_generated/vector/qgsvectorlayer.sip.in @@ -1935,6 +1935,27 @@ Sets a split ``policy`` for the field with the specified index. } %End + void setFieldDuplicatePolicy( int index, Qgis::FieldDomainDuplicatePolicy policy ); +%Docstring +Sets a duplicate ``policy`` for the field with the specified index. + +:raises KeyError: if no field with the specified index exists + +.. versionadded:: 3.38 +%End + +%MethodCode + if ( a0 < 0 || a0 >= sipCpp->fields().count() ) + { + PyErr_SetString( PyExc_KeyError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + else + { + sipCpp->setFieldDuplicatePolicy( a0, a1 ); + } +%End + QSet excludeAttributesWms() const /Deprecated/; %Docstring A set of attributes that are not advertised in WMS requests with QGIS server. diff --git a/src/core/qgis.h b/src/core/qgis.h index 56a599e59dcbc..acc15d84df6ca 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -3203,6 +3203,22 @@ class CORE_EXPORT Qgis }; Q_ENUM( FieldDomainMergePolicy ) + /** + * Duplicate policy for field domains. + * + * When a feature is duplicated, defines how the value of attributes + * following the domain are computed. + * + * \since QGIS 3.38 + */ + enum class FieldDomainDuplicatePolicy : int + { + DefaultValue, //!< Use default field value + Duplicate, //!< Duplicate original value + UnsetField, //!< Clears the field value so that the data provider backend will populate using any backend triggers or similar logic (since QGIS 3.30) + }; + Q_ENUM( FieldDomainDuplicatePolicy ) + /** * Types of field domain * diff --git a/src/core/qgsfield.cpp b/src/core/qgsfield.cpp index 0db53d8faa0ba..0fa86ff527096 100644 --- a/src/core/qgsfield.cpp +++ b/src/core/qgsfield.cpp @@ -706,6 +706,11 @@ bool QgsField::convertCompatible( QVariant &v, QString *errorMessage ) const return true; } +QgsField::operator QVariant() const +{ + return QVariant::fromValue( *this ); +} + void QgsField::setEditorWidgetSetup( const QgsEditorWidgetSetup &v ) { d->editorWidgetSetup = v; @@ -736,6 +741,16 @@ void QgsField::setSplitPolicy( Qgis::FieldDomainSplitPolicy policy ) d->splitPolicy = policy; } +Qgis::FieldDomainDuplicatePolicy QgsField::duplicatePolicy() const +{ + return d->duplicatePolicy; +} + +void QgsField::setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy policy ) +{ + d->duplicatePolicy = policy; +} + /*************************************************************************** * This class is considered CRITICAL and any change MUST be accompanied with * full unit tests in testqgsfield.cpp. @@ -782,6 +797,7 @@ QDataStream &operator>>( QDataStream &in, QgsField &field ) quint32 strengthUnique; quint32 strengthExpression; int splitPolicy; + int duplicatePolicy; bool applyOnUpdate; @@ -796,7 +812,7 @@ QDataStream &operator>>( QDataStream &in, QgsField &field ) in >> name >> type >> typeName >> length >> precision >> comment >> alias >> defaultValueExpression >> applyOnUpdate >> constraints >> originNotNull >> originUnique >> originExpression >> strengthNotNull >> strengthUnique >> strengthExpression >> - constraintExpression >> constraintDescription >> subType >> splitPolicy >> metadata; + constraintExpression >> constraintDescription >> subType >> splitPolicy >> duplicatePolicy >> metadata; field.setName( name ); field.setType( static_cast< QVariant::Type >( type ) ); field.setTypeName( typeName ); @@ -806,6 +822,7 @@ QDataStream &operator>>( QDataStream &in, QgsField &field ) field.setAlias( alias ); field.setDefaultValueDefinition( QgsDefaultValue( defaultValueExpression, applyOnUpdate ) ); field.setSplitPolicy( static_cast< Qgis::FieldDomainSplitPolicy >( splitPolicy ) ); + field.setDuplicatePolicy( static_cast< Qgis::FieldDomainDuplicatePolicy >( duplicatePolicy ) ); QgsFieldConstraints fieldConstraints; if ( constraints & QgsFieldConstraints::ConstraintNotNull ) { diff --git a/src/core/qgsfield.h b/src/core/qgsfield.h index 090b40a869136..03c44be9d57e6 100644 --- a/src/core/qgsfield.h +++ b/src/core/qgsfield.h @@ -441,10 +441,7 @@ class CORE_EXPORT QgsField #endif //! Allows direct construction of QVariants from fields. - operator QVariant() const - { - return QVariant::fromValue( *this ); - } + operator QVariant() const; /** * Set the editor widget setup for the field. @@ -497,6 +494,26 @@ class CORE_EXPORT QgsField */ void setSplitPolicy( Qgis::FieldDomainSplitPolicy policy ); + /** + * Returns the field's duplicate policy, which indicates how field values should + * be handled during a duplicate operation. + * + * \see setDuplicatePolicy() + * + * \since QGIS 3.38 + */ + Qgis::FieldDomainDuplicatePolicy duplicatePolicy() const; + + /** + * Sets the field's duplicate \a policy, which indicates how field values should + * be handled during a duplicate operation. + * + * \see duplicatePolicy() + * + * \since QGIS 3.38 + */ + void setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy policy ); + #ifdef SIP_RUN SIP_PYOBJECT __repr__(); % MethodCode diff --git a/src/core/qgsfield_p.h b/src/core/qgsfield_p.h index 982e6d513533c..beab53710a6f1 100644 --- a/src/core/qgsfield_p.h +++ b/src/core/qgsfield_p.h @@ -84,6 +84,7 @@ class QgsFieldPrivate : public QSharedData , constraints( other.constraints ) , editorWidgetSetup( other.editorWidgetSetup ) , splitPolicy( other.splitPolicy ) + , duplicatePolicy( other.duplicatePolicy ) , isReadOnly( other.isReadOnly ) { } @@ -99,6 +100,7 @@ class QgsFieldPrivate : public QSharedData && ( alias == other.alias ) && ( defaultValueDefinition == other.defaultValueDefinition ) && ( constraints == other.constraints ) && ( flags == other.flags ) && ( splitPolicy == other.splitPolicy ) + && ( duplicatePolicy == other.duplicatePolicy ) && ( isReadOnly == other.isReadOnly ) && ( editorWidgetSetup == other.editorWidgetSetup ) ); } @@ -144,6 +146,9 @@ class QgsFieldPrivate : public QSharedData //! Split policy Qgis::FieldDomainSplitPolicy splitPolicy = Qgis::FieldDomainSplitPolicy::Duplicate; + //! Duplicate policy + Qgis::FieldDomainDuplicatePolicy duplicatePolicy = Qgis::FieldDomainDuplicatePolicy::Duplicate; + //! Read-only bool isReadOnly = false; diff --git a/src/core/vector/qgsvectorlayer.cpp b/src/core/vector/qgsvectorlayer.cpp index eac50e31e0ef0..722b6aebf699c 100644 --- a/src/core/vector/qgsvectorlayer.cpp +++ b/src/core/vector/qgsvectorlayer.cpp @@ -2243,6 +2243,10 @@ bool QgsVectorLayer::setDataProvider( QString const &provider, const QgsDataProv { mAttributeSplitPolicy[ field.name() ] = field.splitPolicy(); } + if ( !mAttributeDuplicatePolicy.contains( field.name() ) ) + { + mAttributeDuplicatePolicy[ field.name() ] = field.duplicatePolicy(); + } } if ( profile ) @@ -2573,6 +2577,21 @@ bool QgsVectorLayer::readSymbology( const QDomNode &layerNode, QString &errorMes } } + // The duplicate policy is - unlike alias and split policy - never defined by the data provider, so we clear the map + mAttributeDuplicatePolicy.clear(); + const QDomNode duplicatePoliciesNode = layerNode.namedItem( QStringLiteral( "duplicatePolicies" ) ); + if ( !duplicatePoliciesNode.isNull() ) + { + const QDomNodeList duplicatePolicyNodeList = duplicatePoliciesNode.toElement().elementsByTagName( QStringLiteral( "policy" ) ); + for ( int i = 0; i < duplicatePolicyNodeList.size(); ++i ) + { + const QDomElement duplicatePolicyElem = duplicatePolicyNodeList.at( i ).toElement(); + const QString field = duplicatePolicyElem.attribute( QStringLiteral( "field" ) ); + const Qgis::FieldDomainDuplicatePolicy policy = qgsEnumKeyToValue( duplicatePolicyElem.attribute( QStringLiteral( "policy" ) ), Qgis::FieldDomainDuplicatePolicy::Duplicate ); + mAttributeDuplicatePolicy.insert( field, policy ); + } + } + // default expressions mDefaultExpressionMap.clear(); QDomNode defaultsNode = layerNode.namedItem( QStringLiteral( "defaults" ) ); @@ -3085,6 +3104,19 @@ bool QgsVectorLayer::writeSymbology( QDomNode &node, QDomDocument &doc, QString node.appendChild( splitPoliciesElement ); } + //duplicate policies + { + QDomElement duplicatePoliciesElement = doc.createElement( QStringLiteral( "duplicatePolicies" ) ); + for ( const QgsField &field : std::as_const( mFields ) ) + { + QDomElement duplicatePolicyElem = doc.createElement( QStringLiteral( "policy" ) ); + duplicatePolicyElem.setAttribute( QStringLiteral( "field" ), field.name() ); + duplicatePolicyElem.setAttribute( QStringLiteral( "policy" ), qgsEnumValueToKey( field.duplicatePolicy() ) ); + duplicatePoliciesElement.appendChild( duplicatePolicyElem ); + } + node.appendChild( duplicatePoliciesElement ); + } + //default expressions QDomElement defaultsElem = doc.createElement( QStringLiteral( "defaults" ) ); for ( const QgsField &field : std::as_const( mFields ) ) @@ -3588,6 +3620,22 @@ void QgsVectorLayer::setFieldSplitPolicy( int index, Qgis::FieldDomainSplitPolic emit layerModified(); // TODO[MD]: should have a different signal? } +void QgsVectorLayer::setFieldDuplicatePolicy( int index, Qgis::FieldDomainDuplicatePolicy policy ) +{ + QGIS_PROTECT_QOBJECT_THREAD_ACCESS + + if ( index < 0 || index >= fields().count() ) + return; + + const QString name = fields().at( index ).name(); + + mAttributeDuplicatePolicy.insert( name, policy ); + mFields[ index ].setDuplicatePolicy( policy ); + mEditFormConfig.setFields( mFields ); + emit layerModified(); // TODO[MD]: should have a different signal? +} + + QSet QgsVectorLayer::excludeAttributesWms() const { QGIS_PROTECT_QOBJECT_THREAD_ACCESS @@ -4463,6 +4511,15 @@ void QgsVectorLayer::updateFields() mFields[ index ].setSplitPolicy( splitPolicyIt.value() ); } + for ( auto duplicatePolicyIt = mAttributeDuplicatePolicy.constBegin(); duplicatePolicyIt != mAttributeDuplicatePolicy.constEnd(); ++duplicatePolicyIt ) + { + int index = mFields.lookupField( duplicatePolicyIt.key() ); + if ( index < 0 ) + continue; + + mFields[ index ].setDuplicatePolicy( duplicatePolicyIt.value() ); + } + // Update configuration flags QMap< QString, Qgis::FieldConfigurationFlags >::const_iterator flagsIt = mFieldConfigurationFlags.constBegin(); for ( ; flagsIt != mFieldConfigurationFlags.constEnd(); ++flagsIt ) diff --git a/src/core/vector/qgsvectorlayer.h b/src/core/vector/qgsvectorlayer.h index 00af40a4e8955..1053bef0c1898 100644 --- a/src/core/vector/qgsvectorlayer.h +++ b/src/core/vector/qgsvectorlayer.h @@ -1840,6 +1840,13 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte * \since QGIS 3.30 */ void setFieldSplitPolicy( int index, Qgis::FieldDomainSplitPolicy policy ); + + /** + * Sets a duplicate \a policy for the field with the specified index. + * + * \since QGIS 3.38 + */ + void setFieldDuplicatePolicy( int index, Qgis::FieldDomainDuplicatePolicy policy ); #else /** @@ -1861,6 +1868,26 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte sipCpp->setFieldSplitPolicy( a0, a1 ); } % End + + /** + * Sets a duplicate \a policy for the field with the specified index. + * + * \throws KeyError if no field with the specified index exists + * \since QGIS 3.38 + */ + void setFieldDuplicatePolicy( int index, Qgis::FieldDomainDuplicatePolicy policy ); + + % MethodCode + if ( a0 < 0 || a0 >= sipCpp->fields().count() ) + { + PyErr_SetString( PyExc_KeyError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + else + { + sipCpp->setFieldDuplicatePolicy( a0, a1 ); + } + % End #endif /** @@ -2867,6 +2894,9 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte //! Map that stores the split policy for attributes QMap< QString, Qgis::FieldDomainSplitPolicy > mAttributeSplitPolicy; + //! Map that stores the duplicate policy for attributes + QMap< QString, Qgis::FieldDomainDuplicatePolicy > mAttributeDuplicatePolicy; + //! An internal structure to keep track of fields that have a defaultValueOnUpdate QSet mDefaultValueOnUpdateFields; diff --git a/src/core/vector/qgsvectorlayereditutils.cpp b/src/core/vector/qgsvectorlayereditutils.cpp index bd1a45a1e26ba..5605476df7077 100644 --- a/src/core/vector/qgsvectorlayereditutils.cpp +++ b/src/core/vector/qgsvectorlayereditutils.cpp @@ -521,8 +521,7 @@ Qgis::GeometryOperationResult QgsVectorLayerEditUtils::splitFeatures( const QgsC switch ( field.splitPolicy() ) { case Qgis::FieldDomainSplitPolicy::DefaultValue: - // TODO!!! - + //do nothing - default values ​​are determined break; case Qgis::FieldDomainSplitPolicy::Duplicate: diff --git a/src/core/vector/qgsvectorlayerutils.cpp b/src/core/vector/qgsvectorlayerutils.cpp index 06c5471977c53..af84e28cbdef2 100644 --- a/src/core/vector/qgsvectorlayerutils.cpp +++ b/src/core/vector/qgsvectorlayerutils.cpp @@ -36,6 +36,7 @@ #include "qgsauxiliarystorage.h" #include "qgssymbollayerreference.h" #include "qgspainteffect.h" +#include "qgsunsetattributevalue.h" QgsFeatureIterator QgsVectorLayerUtils::getValuesIterator( const QgsVectorLayer *layer, const QString &fieldOrExpression, bool &ok, bool selectedOnly ) { @@ -643,7 +644,29 @@ QgsFeature QgsVectorLayerUtils::duplicateFeature( QgsVectorLayer *layer, const Q QgsExpressionContext context = layer->createExpressionContext(); context.setFeature( feature ); - QgsFeature newFeature = createFeature( layer, feature.geometry(), feature.attributes().toMap(), &context ); + //respect field duplicate policy + QgsAttributeMap attributeMap; + const int fieldCount = layer->fields().count(); + for ( int fieldIdx = 0; fieldIdx < fieldCount; ++fieldIdx ) + { + const QgsField field = layer->fields().at( fieldIdx ); + switch ( field.duplicatePolicy() ) + { + case Qgis::FieldDomainDuplicatePolicy::DefaultValue: + //do nothing - default values ​​are determined + break; + + case Qgis::FieldDomainDuplicatePolicy::Duplicate: + attributeMap.insert( fieldIdx, feature.attribute( fieldIdx ) ); + break; + + case Qgis::FieldDomainDuplicatePolicy::UnsetField: + attributeMap.insert( fieldIdx, QgsUnsetAttributeValue() ); + break; + } + } + + QgsFeature newFeature = createFeature( layer, feature.geometry(), attributeMap, &context ); layer->addFeature( newFeature ); const QList relations = project->relationManager()->referencedRelations( layer ); diff --git a/src/gui/attributeformconfig/qgsattributetypedialog.cpp b/src/gui/attributeformconfig/qgsattributetypedialog.cpp index 3b1e7605935a6..0e717e15a861a 100644 --- a/src/gui/attributeformconfig/qgsattributetypedialog.cpp +++ b/src/gui/attributeformconfig/qgsattributetypedialog.cpp @@ -124,6 +124,12 @@ QgsAttributeTypeDialog::QgsAttributeTypeDialog( QgsVectorLayer *vl, int fieldIdx connect( mSplitPolicyComboBox, qOverload( &QComboBox::currentIndexChanged ), this, &QgsAttributeTypeDialog::updateSplitPolicyLabel ); updateSplitPolicyLabel(); + + mDuplicatePolicyComboBox->addItem( tr( "Duplicate Value" ), QVariant::fromValue( Qgis::FieldDomainDuplicatePolicy::Duplicate ) ); + mDuplicatePolicyComboBox->addItem( tr( "Use Default Value" ), QVariant::fromValue( Qgis::FieldDomainDuplicatePolicy::DefaultValue ) ); + mDuplicatePolicyComboBox->addItem( tr( "Remove Value" ), QVariant::fromValue( Qgis::FieldDomainDuplicatePolicy::UnsetField ) ); + connect( mDuplicatePolicyComboBox, qOverload( &QComboBox::currentIndexChanged ), this, &QgsAttributeTypeDialog::updateDuplicatePolicyLabel ); + updateDuplicatePolicyLabel(); } QgsAttributeTypeDialog::~QgsAttributeTypeDialog() @@ -381,6 +387,17 @@ void QgsAttributeTypeDialog::setSplitPolicy( Qgis::FieldDomainSplitPolicy policy updateSplitPolicyLabel(); } +Qgis::FieldDomainDuplicatePolicy QgsAttributeTypeDialog::duplicatePolicy() const +{ + return mDuplicatePolicyComboBox->currentData().value< Qgis::FieldDomainDuplicatePolicy >(); +} + +void QgsAttributeTypeDialog::setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy policy ) +{ + mDuplicatePolicyComboBox->setCurrentIndex( mDuplicatePolicyComboBox->findData( QVariant::fromValue( policy ) ) ); + updateSplitPolicyLabel(); +} + QString QgsAttributeTypeDialog::constraintExpression() const { return constraintExpressionWidget->asExpression(); @@ -498,6 +515,26 @@ void QgsAttributeTypeDialog::updateSplitPolicyLabel() mSplitPolicyDescriptionLabel->setText( QStringLiteral( "%1" ).arg( helperText ) ); } +void QgsAttributeTypeDialog::updateDuplicatePolicyLabel() +{ + QString helperText; + switch ( mDuplicatePolicyComboBox->currentData().value< Qgis::FieldDomainDuplicatePolicy >() ) + { + case Qgis::FieldDomainDuplicatePolicy::DefaultValue: + helperText = tr( "Resets the field by recalculating its default value." ); + break; + + case Qgis::FieldDomainDuplicatePolicy::Duplicate: + helperText = tr( "Copies the current field value without change." ); + break; + + case Qgis::FieldDomainDuplicatePolicy::UnsetField: + helperText = tr( "Clears the field to an unset state." ); + break; + } + mDuplicatePolicyDescriptionLabel->setText( QStringLiteral( "%1" ).arg( helperText ) ); +} + QStandardItem *QgsAttributeTypeDialog::currentItem() const { QStandardItemModel *widgetTypeModel = qobject_cast( mWidgetTypeComboBox->model() ); diff --git a/src/gui/attributeformconfig/qgsattributetypedialog.h b/src/gui/attributeformconfig/qgsattributetypedialog.h index 40305a0d9e32e..8b9eb28c2eb34 100644 --- a/src/gui/attributeformconfig/qgsattributetypedialog.h +++ b/src/gui/attributeformconfig/qgsattributetypedialog.h @@ -245,6 +245,24 @@ class GUI_EXPORT QgsAttributeTypeDialog: public QWidget, private Ui::QgsAttribut */ void setSplitPolicy( Qgis::FieldDomainSplitPolicy policy ); + /** + * Returns the field's duplicate policy. + * + * \see setDuplicatePolicy() + * + * \since QGIS 3.38 + */ + Qgis::FieldDomainDuplicatePolicy duplicatePolicy() const; + + /** + * Sets the field's duplicate policy. + * + * \see duplicatePolicy() + * + * \since QGIS 3.38 + */ + void setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy policy ); + private slots: /** @@ -257,6 +275,8 @@ class GUI_EXPORT QgsAttributeTypeDialog: public QWidget, private Ui::QgsAttribut void updateSplitPolicyLabel(); + void updateDuplicatePolicyLabel(); + private: QgsVectorLayer *mLayer = nullptr; int mFieldIdx; diff --git a/src/gui/vector/qgsattributesformproperties.cpp b/src/gui/vector/qgsattributesformproperties.cpp index 905eaed0e0c15..c50c40eeeffa9 100644 --- a/src/gui/vector/qgsattributesformproperties.cpp +++ b/src/gui/vector/qgsattributesformproperties.cpp @@ -302,6 +302,7 @@ void QgsAttributesFormProperties::loadAttributeTypeDialog() mAttributeTypeDialog->setUnique( constraints.constraints() & QgsFieldConstraints::ConstraintUnique ); mAttributeTypeDialog->setUniqueEnforced( constraints.constraintStrength( QgsFieldConstraints::ConstraintUnique ) == QgsFieldConstraints::ConstraintStrengthHard ); mAttributeTypeDialog->setSplitPolicy( cfg.mSplitPolicy ); + mAttributeTypeDialog->setDuplicatePolicy( cfg.mDuplicatePolicy ); QgsFieldConstraints::Constraints providerConstraints = QgsFieldConstraints::Constraints(); if ( constraints.constraintOrigin( QgsFieldConstraints::ConstraintNotNull ) == QgsFieldConstraints::ConstraintOriginProvider ) @@ -386,6 +387,7 @@ void QgsAttributesFormProperties::storeAttributeTypeDialog() cfg.mEditorWidgetConfig = mAttributeTypeDialog->editorWidgetConfig(); cfg.mSplitPolicy = mAttributeTypeDialog->splitPolicy(); + cfg.mDuplicatePolicy = mAttributeTypeDialog->duplicatePolicy(); const QString fieldName = mLayer->fields().at( mAttributeTypeDialog->fieldIdx() ).name(); @@ -1013,6 +1015,7 @@ void QgsAttributesFormProperties::apply() mLayer->setFieldAlias( idx, cfg.mAlias ); mLayer->setFieldSplitPolicy( idx, cfg.mSplitPolicy ); + mLayer->setFieldDuplicatePolicy( idx, cfg.mDuplicatePolicy ); } // tabs and groups @@ -1085,6 +1088,7 @@ QgsAttributesFormProperties::FieldConfig::FieldConfig( QgsVectorLayer *layer, in mEditorWidgetType = setup.type(); mEditorWidgetConfig = setup.config(); mSplitPolicy = layer->fields().at( idx ).splitPolicy(); + mDuplicatePolicy = layer->fields().at( idx ).duplicatePolicy(); } QgsAttributesFormProperties::FieldConfig::operator QVariant() diff --git a/src/gui/vector/qgsattributesformproperties.h b/src/gui/vector/qgsattributesformproperties.h index bcf7bde2e1483..505aca6f120fa 100644 --- a/src/gui/vector/qgsattributesformproperties.h +++ b/src/gui/vector/qgsattributesformproperties.h @@ -338,6 +338,7 @@ class GUI_EXPORT QgsAttributesFormProperties : public QWidget, public QgsExpress QgsPropertyCollection mDataDefinedProperties; QString mComment; Qgis::FieldDomainSplitPolicy mSplitPolicy = Qgis::FieldDomainSplitPolicy::Duplicate; + Qgis::FieldDomainDuplicatePolicy mDuplicatePolicy = Qgis::FieldDomainDuplicatePolicy::Duplicate; operator QVariant(); }; diff --git a/src/ui/attributeformconfig/qgsattributetypeedit.ui b/src/ui/attributeformconfig/qgsattributetypeedit.ui index 31bdb54180b42..7c46ca9b4f3ab 100644 --- a/src/ui/attributeformconfig/qgsattributetypeedit.ui +++ b/src/ui/attributeformconfig/qgsattributetypeedit.ui @@ -149,7 +149,7 @@ - + Qt::StrongFocus @@ -283,10 +283,20 @@ Policies - - + + + + + + When duplicating features + + + + + + @@ -294,7 +304,17 @@ - + + + + TextLabel + + + true + + + + TextLabel From 17c6584976afc6219defa3b64e1fa3fabe74a92f Mon Sep 17 00:00:00 2001 From: signedav Date: Mon, 22 Apr 2024 11:34:50 +0200 Subject: [PATCH 002/102] DuplicatePolicy set/get etc. tests --- tests/src/core/testqgsfield.cpp | 11 +++++++ tests/src/python/test_qgsvectorlayer.py | 40 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/tests/src/core/testqgsfield.cpp b/tests/src/core/testqgsfield.cpp index 73c54163542e6..7183897eb1cf6 100644 --- a/tests/src/core/testqgsfield.cpp +++ b/tests/src/core/testqgsfield.cpp @@ -103,6 +103,7 @@ void TestQgsField::copy() original.setConstraints( constraints ); original.setReadOnly( true ); original.setSplitPolicy( Qgis::FieldDomainSplitPolicy::GeometryRatio ); + original.setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy::UnsetField ); original.setMetadata( {{ 1, QStringLiteral( "abc" )}, {2, 5 }} ); QVariantMap config; @@ -130,6 +131,7 @@ void TestQgsField::assignment() original.setConstraints( constraints ); original.setReadOnly( true ); original.setSplitPolicy( Qgis::FieldDomainSplitPolicy::GeometryRatio ); + original.setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy::UnsetField ); original.setMetadata( {{ 1, QStringLiteral( "abc" )}, {2, 5 }} ); QgsField copy; copy = original; @@ -204,6 +206,9 @@ void TestQgsField::gettersSetters() field.setSplitPolicy( Qgis::FieldDomainSplitPolicy::GeometryRatio ); QCOMPARE( field.splitPolicy(), Qgis::FieldDomainSplitPolicy::GeometryRatio ); + field.setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy::UnsetField ); + QCOMPARE( field.duplicatePolicy(), Qgis::FieldDomainDuplicatePolicy::UnsetField ); + field.setMetadata( {{ static_cast< int >( Qgis::FieldMetadataProperty::GeometryCrs ), QStringLiteral( "abc" )}, {2, 5 }} ); QMap< int, QVariant> expected {{ static_cast< int >( Qgis::FieldMetadataProperty::GeometryCrs ), QStringLiteral( "abc" )}, {2, 5 }}; QCOMPARE( field.metadata(), expected ); @@ -359,6 +364,12 @@ void TestQgsField::equality() field2.setSplitPolicy( Qgis::FieldDomainSplitPolicy::GeometryRatio ); QVERIFY( field1 == field2 ); + field1.setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy::UnsetField ); + QVERIFY( !( field1 == field2 ) ); + QVERIFY( field1 != field2 ); + field2.setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy::UnsetField ); + QVERIFY( field1 == field2 ); + field1.setMetadata( {{ static_cast< int >( Qgis::FieldMetadataProperty::GeometryCrs ), QStringLiteral( "abc" )}, {2, 5 }} ); QVERIFY( !( field1 == field2 ) ); QVERIFY( field1 != field2 ); diff --git a/tests/src/python/test_qgsvectorlayer.py b/tests/src/python/test_qgsvectorlayer.py index 44db1a908b847..2935554c8bfa3 100644 --- a/tests/src/python/test_qgsvectorlayer.py +++ b/tests/src/python/test_qgsvectorlayer.py @@ -4547,6 +4547,46 @@ def test_split_policies(self): self.assertEqual(vl2.fields()[3].splitPolicy(), Qgis.FieldDomainSplitPolicy.GeometryRatio) + def test_duplicate_policies(self): + vl = QgsVectorLayer('Point?crs=epsg:3111&field=field_default:integer&field=field_dupe:integer&field=field_unset:integer', 'test', 'memory') + self.assertTrue(vl.isValid()) + + with self.assertRaises(KeyError): + vl.setFieldDuplicatePolicy(-1, Qgis.FieldDomainDuplicatePolicy.DefaultValue) + with self.assertRaises(KeyError): + vl.setFieldDuplicatePolicy(4, Qgis.FieldDomainDuplicatePolicy.DefaultValue) + + vl.setFieldDuplicatePolicy(0, Qgis.FieldDomainDuplicatePolicy.DefaultValue) + vl.setFieldDuplicatePolicy(1, Qgis.FieldDomainDuplicatePolicy.Duplicate) + vl.setFieldDuplicatePolicy(2, Qgis.FieldDomainDuplicatePolicy.UnsetField) + + self.assertEqual(vl.fields()[0].duplicatePolicy(), + Qgis.FieldDomainDuplicatePolicy.DefaultValue) + self.assertEqual(vl.fields()[1].duplicatePolicy(), + Qgis.FieldDomainDuplicatePolicy.Duplicate) + self.assertEqual(vl.fields()[2].duplicatePolicy(), + Qgis.FieldDomainDuplicatePolicy.UnsetField) + + p = QgsProject() + p.addMapLayer(vl) + + # test saving and restoring split policies + with tempfile.TemporaryDirectory() as temp: + self.assertTrue(p.write(temp + '/test.qgs')) + + p2 = QgsProject() + self.assertTrue(p2.read(temp + '/test.qgs')) + + vl2 = list(p2.mapLayers().values())[0] + self.assertEqual(vl2.name(), vl.name()) + + self.assertEqual(vl2.fields()[0].duplicatePolicy(), + Qgis.FieldDomainDuplicatePolicy.DefaultValue) + self.assertEqual(vl2.fields()[1].duplicatePolicy(), + Qgis.FieldDomainDuplicatePolicy.Duplicate) + self.assertEqual(vl2.fields()[2].duplicatePolicy(), + Qgis.FieldDomainDuplicatePolicy.UnsetField) + def test_selection_properties(self): vl = QgsVectorLayer( 'Point?crs=epsg:3111&field=field_default:integer&field=field_dupe:integer&field=field_unset:integer&field=field_ratio:integer', From a9747ae21c0eec802cfa86256806fd3dcee16535 Mon Sep 17 00:00:00 2001 From: signedav Date: Mon, 22 Apr 2024 11:35:20 +0200 Subject: [PATCH 003/102] DuplicatePolicy functionality test --- tests/src/python/test_qgsvectorlayerutils.py | 69 +++++++++++++++----- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/tests/src/python/test_qgsvectorlayerutils.py b/tests/src/python/test_qgsvectorlayerutils.py index 5e5e9bece97ab..3b8b6732efff9 100644 --- a/tests/src/python/test_qgsvectorlayerutils.py +++ b/tests/src/python/test_qgsvectorlayerutils.py @@ -15,6 +15,7 @@ from qgis.PyQt.QtCore import QVariant from qgis.core import ( NULL, + Qgis, QgsDefaultValue, QgsFeature, QgsField, @@ -478,29 +479,53 @@ def testCreateFeature(self): self.assertEqual(f.attributes(), ['test_5', 132, NULL]) def testDuplicateFeature(self): - """ test duplicating a feature """ + """ test duplicating a feature with relations """ project = QgsProject().instance() # LAYERS # - add first layer (parent) - layer1 = QgsVectorLayer("Point?field=fldtxt:string&field=pkid:integer", + layer1 = QgsVectorLayer("Point?field=fldtxt:string&field=pkid:integer&field=policycheck1value:text&field=policycheck2value:text&field=policycheck3value:text", "parentlayer", "memory") # > check first layer (parent) self.assertTrue(layer1.isValid()) - # - set the value for the copy + # - set the default values for pk and policy check and the field policy layer1.setDefaultValueDefinition(1, QgsDefaultValue("rand(1000,2000)")) + layer1.setDefaultValueDefinition(2, QgsDefaultValue("'Def Blabla L1'")) + layer1.setDefaultValueDefinition(3, QgsDefaultValue("'Def Blabla L1'")) + layer1.setDefaultValueDefinition(4, QgsDefaultValue("'Def Blabla L1'")) + layer1.setFieldDuplicatePolicy(2, Qgis.FieldDomainDuplicatePolicy.Duplicate) + layer1.setFieldDuplicatePolicy(3, Qgis.FieldDomainDuplicatePolicy.DefaultValue) + layer1.setFieldDuplicatePolicy(4, Qgis.FieldDomainDuplicatePolicy.UnsetField) # > check first layer (parent) self.assertTrue(layer1.isValid()) # - add second layer (child) - layer2 = QgsVectorLayer("Point?field=fldtxt:string&field=id:integer&field=foreign_key:integer", + layer2 = QgsVectorLayer("Point?field=fldtxt:string&field=id:integer&field=foreign_key:integer&field=policycheck1value:text&field=policycheck2value:text&field=policycheck3value:text", "childlayer1", "memory") # > check second layer (child) self.assertTrue(layer2.isValid()) - # - add second layer (child) - layer3 = QgsVectorLayer("Point?field=fldtxt:string&field=id:integer&field=foreign_key:integer", - "childlayer2", "memory") + # - set the default values for pk and policy check and the field policy + layer2.setDefaultValueDefinition(3, QgsDefaultValue("'Def Blabla L2'")) + layer2.setDefaultValueDefinition(4, QgsDefaultValue("'Def Blabla L2'")) + layer2.setDefaultValueDefinition(5, QgsDefaultValue("'Def Blabla L2'")) + layer2.setFieldDuplicatePolicy(3, Qgis.FieldDomainDuplicatePolicy.Duplicate) + layer2.setFieldDuplicatePolicy(4, Qgis.FieldDomainDuplicatePolicy.DefaultValue) + layer2.setFieldDuplicatePolicy(5, Qgis.FieldDomainDuplicatePolicy.UnsetField) # > check second layer (child) + self.assertTrue(layer2.isValid()) + # - add third layer (child) + layer3 = QgsVectorLayer("Point?field=fldtxt:string&field=id:integer&field=foreign_key:integer&field=policycheck1value:text&field=policycheck2value:text&field=policycheck3value:text", + "childlayer2", "memory") + # > check third layer (child) + self.assertTrue(layer3.isValid()) + # - set the default values for pk and policy check and the field policy + layer3.setDefaultValueDefinition(3, QgsDefaultValue("'Def Blabla L3'")) + layer3.setDefaultValueDefinition(4, QgsDefaultValue("'Def Blabla L3'")) + layer3.setDefaultValueDefinition(5, QgsDefaultValue("'Def Blabla L3'")) + layer3.setFieldDuplicatePolicy(3, Qgis.FieldDomainDuplicatePolicy.Duplicate) + layer3.setFieldDuplicatePolicy(4, Qgis.FieldDomainDuplicatePolicy.DefaultValue) + layer3.setFieldDuplicatePolicy(5, Qgis.FieldDomainDuplicatePolicy.UnsetField) + # > check third layer (child) self.assertTrue(layer3.isValid()) # - add layers project.addMapLayers([layer1, layer2, layer3]) @@ -509,34 +534,34 @@ def testDuplicateFeature(self): # - add 2 features on layer1 (parent) l1f1orig = QgsFeature() l1f1orig.setFields(layer1.fields()) - l1f1orig.setAttributes(["F_l1f1", 100]) + l1f1orig.setAttributes(["F_l1f1", 100, 'Orig Blabla L1', 'Orig Blabla L1', 'Orig Blabla L1']) l1f2orig = QgsFeature() l1f2orig.setFields(layer1.fields()) - l1f2orig.setAttributes(["F_l1f2", 101]) + l1f2orig.setAttributes(["F_l1f2", 101, 'Orig Blabla L1', 'Orig Blabla L1', 'Orig Blabla L1']) # > check by adding features self.assertTrue(layer1.dataProvider().addFeatures([l1f1orig, l1f2orig])) # add 4 features on layer2 (child) l2f1orig = QgsFeature() l2f1orig.setFields(layer2.fields()) - l2f1orig.setAttributes(["F_l2f1", 201, 100]) + l2f1orig.setAttributes(["F_l2f1", 201, 100, 'Orig Blabla L2', 'Orig Blabla L2', 'Orig Blabla L2']) l2f2orig = QgsFeature() l2f2orig.setFields(layer2.fields()) - l2f2orig.setAttributes(["F_l2f2", 202, 100]) + l2f2orig.setAttributes(["F_l2f2", 202, 100, 'Orig Blabla L2', 'Orig Blabla L2', 'Orig Blabla L2']) l2f3orig = QgsFeature() l2f3orig.setFields(layer2.fields()) - l2f3orig.setAttributes(["F_l2f3", 203, 100]) + l2f3orig.setAttributes(["F_l2f3", 203, 100, 'Orig Blabla L2', 'Orig Blabla L2', 'Orig Blabla L2']) l2f4orig = QgsFeature() l2f4orig.setFields(layer2.fields()) - l2f4orig.setAttributes(["F_l2f4", 204, 101]) + l2f4orig.setAttributes(["F_l2f4", 204, 101, 'Orig Blabla L2', 'Orig Blabla L2', 'Orig Blabla L2']) # > check by adding features self.assertTrue(layer2.dataProvider().addFeatures([l2f1orig, l2f2orig, l2f3orig, l2f4orig])) # add 2 features on layer3 (child) l3f1orig = QgsFeature() l3f1orig.setFields(layer3.fields()) - l3f1orig.setAttributes(["F_l3f1", 201, 100]) + l3f1orig.setAttributes(["F_l3f1", 301, 100, 'Orig Blabla L3', 'Orig Blabla L3', 'Orig Blabla L3']) l3f2orig = QgsFeature() l3f2orig.setFields(layer2.fields()) - l3f2orig.setAttributes(["F_l3f2", 202, 100]) + l3f2orig.setAttributes(["F_l3f2", 302, 100, 'Orig Blabla L3', 'Orig Blabla L3', 'Orig Blabla L3']) # > check by adding features self.assertTrue(layer3.dataProvider().addFeatures([l3f1orig, l3f2orig])) @@ -618,16 +643,30 @@ def testDuplicateFeature(self): results = QgsVectorLayerUtils.duplicateFeature(layer1, l1f1orig, project, 0) # > check if name is name of duplicated (pk is different) + # > and duplicate policy is concerned result_feature = results[0] self.assertEqual(result_feature.attribute('fldtxt'), l1f1orig.attribute('fldtxt')) + self.assertEqual(result_feature.attribute('policycheck1value'), 'Orig Blabla L1') # duplicated + self.assertEqual(result_feature.attribute('policycheck2value'), 'Def Blabla L1') # default Value + self.assertEqual(result_feature.attribute('policycheck3value'), None) # unset # > check duplicated children occurred on both layers self.assertEqual(len(results[1].layers()), 2) idx = results[1].layers().index(layer2) self.assertEqual(results[1].layers()[idx], layer2) self.assertTrue(results[1].duplicatedFeatures(layer2)) + for child_fid in results[1].duplicatedFeatures(layer2): + child_feature = layer2.getFeature(child_fid) + self.assertEqual(child_feature.attribute('policycheck1value'), 'Orig Blabla L2') # duplicated + self.assertEqual(child_feature.attribute('policycheck2value'), 'Def Blabla L2') # default Value + self.assertEqual(child_feature.attribute('policycheck3value'), None) # unset idx = results[1].layers().index(layer3) self.assertEqual(results[1].layers()[idx], layer3) self.assertTrue(results[1].duplicatedFeatures(layer3)) + for child_fid in results[1].duplicatedFeatures(layer3): + child_feature = layer3.getFeature(child_fid) + self.assertEqual(child_feature.attribute('policycheck1value'), 'Orig Blabla L3') # duplicated + self.assertEqual(child_feature.attribute('policycheck2value'), 'Def Blabla L3') # default Value + self.assertEqual(child_feature.attribute('policycheck3value'), None) # unset ''' # testoutput 2 From 39e6dbd0c1c50db1ac462dd4fe41df439fc4c7f5 Mon Sep 17 00:00:00 2001 From: signedav Date: Mon, 22 Apr 2024 13:20:08 +0200 Subject: [PATCH 004/102] fix datastream and comments --- python/PyQt6/core/auto_additions/qgis.py | 2 +- python/core/auto_additions/qgis.py | 2 +- src/core/qgis.h | 2 +- src/core/qgsfield.cpp | 1 + tests/src/core/testqgsfield.cpp | 1 + tests/src/python/test_qgsvectorlayerutils.py | 18 +++++++++--------- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index e7670ddf8522c..ef8d8971abe01 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -3218,7 +3218,7 @@ Qgis.FieldDomainDuplicatePolicy.DefaultValue.__doc__ = "Use default field value" Qgis.FieldDomainDuplicatePolicy.Duplicate.__doc__ = "Duplicate original value" Qgis.FieldDomainDuplicatePolicy.UnsetField.__doc__ = "Clears the field value so that the data provider backend will populate using any backend triggers or similar logic (since QGIS 3.30)" -Qgis.FieldDomainDuplicatePolicy.__doc__ = "Duplicate policy for field domains.\n\nWhen a feature is duplicated, defines how the value of attributes\nfollowing the domain are computed.\n\n.. versionadded:: 3.38\n\n" + '* ``DefaultValue``: ' + Qgis.FieldDomainDuplicatePolicy.DefaultValue.__doc__ + '\n' + '* ``Duplicate``: ' + Qgis.FieldDomainDuplicatePolicy.Duplicate.__doc__ + '\n' + '* ``UnsetField``: ' + Qgis.FieldDomainDuplicatePolicy.UnsetField.__doc__ +Qgis.FieldDomainDuplicatePolicy.__doc__ = "Duplicate policy for fields.\n\nWhen a feature is duplicated, defines how the value of attributes\nfollowing the domain are computed.\n\n.. versionadded:: 3.38\n\n" + '* ``DefaultValue``: ' + Qgis.FieldDomainDuplicatePolicy.DefaultValue.__doc__ + '\n' + '* ``Duplicate``: ' + Qgis.FieldDomainDuplicatePolicy.Duplicate.__doc__ + '\n' + '* ``UnsetField``: ' + Qgis.FieldDomainDuplicatePolicy.UnsetField.__doc__ # -- Qgis.FieldDomainDuplicatePolicy.baseClass = Qgis # monkey patching scoped based enum diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index 4fed270ba8074..f885d9a5d8224 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -3163,7 +3163,7 @@ Qgis.FieldDomainDuplicatePolicy.DefaultValue.__doc__ = "Use default field value" Qgis.FieldDomainDuplicatePolicy.Duplicate.__doc__ = "Duplicate original value" Qgis.FieldDomainDuplicatePolicy.UnsetField.__doc__ = "Clears the field value so that the data provider backend will populate using any backend triggers or similar logic (since QGIS 3.30)" -Qgis.FieldDomainDuplicatePolicy.__doc__ = "Duplicate policy for field domains.\n\nWhen a feature is duplicated, defines how the value of attributes\nfollowing the domain are computed.\n\n.. versionadded:: 3.38\n\n" + '* ``DefaultValue``: ' + Qgis.FieldDomainDuplicatePolicy.DefaultValue.__doc__ + '\n' + '* ``Duplicate``: ' + Qgis.FieldDomainDuplicatePolicy.Duplicate.__doc__ + '\n' + '* ``UnsetField``: ' + Qgis.FieldDomainDuplicatePolicy.UnsetField.__doc__ +Qgis.FieldDomainDuplicatePolicy.__doc__ = "Duplicate policy for fields.\n\nWhen a feature is duplicated, defines how the value of attributes\nfollowing the domain are computed.\n\n.. versionadded:: 3.38\n\n" + '* ``DefaultValue``: ' + Qgis.FieldDomainDuplicatePolicy.DefaultValue.__doc__ + '\n' + '* ``Duplicate``: ' + Qgis.FieldDomainDuplicatePolicy.Duplicate.__doc__ + '\n' + '* ``UnsetField``: ' + Qgis.FieldDomainDuplicatePolicy.UnsetField.__doc__ # -- Qgis.FieldDomainDuplicatePolicy.baseClass = Qgis # monkey patching scoped based enum diff --git a/src/core/qgis.h b/src/core/qgis.h index a3862cf2be7f4..610718daf04aa 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -3219,7 +3219,7 @@ class CORE_EXPORT Qgis Q_ENUM( FieldDomainMergePolicy ) /** - * Duplicate policy for field domains. + * Duplicate policy for fields. * * When a feature is duplicated, defines how the value of attributes * following the domain are computed. diff --git a/src/core/qgsfield.cpp b/src/core/qgsfield.cpp index 0fa86ff527096..1c3cc54e76ebe 100644 --- a/src/core/qgsfield.cpp +++ b/src/core/qgsfield.cpp @@ -779,6 +779,7 @@ QDataStream &operator<<( QDataStream &out, const QgsField &field ) out << field.constraints().constraintDescription(); out << static_cast< quint32 >( field.subType() ); out << static_cast< int >( field.splitPolicy() ); + out << static_cast< int >( field.duplicatePolicy() ); out << field.metadata(); return out; } diff --git a/tests/src/core/testqgsfield.cpp b/tests/src/core/testqgsfield.cpp index 7183897eb1cf6..81ecc7f94574b 100644 --- a/tests/src/core/testqgsfield.cpp +++ b/tests/src/core/testqgsfield.cpp @@ -934,6 +934,7 @@ void TestQgsField::dataStream() original.setAlias( QStringLiteral( "alias" ) ); original.setDefaultValueDefinition( QgsDefaultValue( QStringLiteral( "default" ) ) ); original.setSplitPolicy( Qgis::FieldDomainSplitPolicy::GeometryRatio ); + original.setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy::DefaultValue ); QgsFieldConstraints constraints; constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider ); constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginLayer ); diff --git a/tests/src/python/test_qgsvectorlayerutils.py b/tests/src/python/test_qgsvectorlayerutils.py index 3b8b6732efff9..c2fa6f2d3716d 100644 --- a/tests/src/python/test_qgsvectorlayerutils.py +++ b/tests/src/python/test_qgsvectorlayerutils.py @@ -646,9 +646,9 @@ def testDuplicateFeature(self): # > and duplicate policy is concerned result_feature = results[0] self.assertEqual(result_feature.attribute('fldtxt'), l1f1orig.attribute('fldtxt')) - self.assertEqual(result_feature.attribute('policycheck1value'), 'Orig Blabla L1') # duplicated - self.assertEqual(result_feature.attribute('policycheck2value'), 'Def Blabla L1') # default Value - self.assertEqual(result_feature.attribute('policycheck3value'), None) # unset + self.assertEqual(result_feature.attribute('policycheck1value'), 'Orig Blabla L1') # duplicated + self.assertEqual(result_feature.attribute('policycheck2value'), 'Def Blabla L1') # default Value + self.assertEqual(result_feature.attribute('policycheck3value'), None) # unset # > check duplicated children occurred on both layers self.assertEqual(len(results[1].layers()), 2) idx = results[1].layers().index(layer2) @@ -656,17 +656,17 @@ def testDuplicateFeature(self): self.assertTrue(results[1].duplicatedFeatures(layer2)) for child_fid in results[1].duplicatedFeatures(layer2): child_feature = layer2.getFeature(child_fid) - self.assertEqual(child_feature.attribute('policycheck1value'), 'Orig Blabla L2') # duplicated - self.assertEqual(child_feature.attribute('policycheck2value'), 'Def Blabla L2') # default Value - self.assertEqual(child_feature.attribute('policycheck3value'), None) # unset + self.assertEqual(child_feature.attribute('policycheck1value'), 'Orig Blabla L2') # duplicated + self.assertEqual(child_feature.attribute('policycheck2value'), 'Def Blabla L2') # default Value + self.assertEqual(child_feature.attribute('policycheck3value'), None) # unset idx = results[1].layers().index(layer3) self.assertEqual(results[1].layers()[idx], layer3) self.assertTrue(results[1].duplicatedFeatures(layer3)) for child_fid in results[1].duplicatedFeatures(layer3): child_feature = layer3.getFeature(child_fid) - self.assertEqual(child_feature.attribute('policycheck1value'), 'Orig Blabla L3') # duplicated - self.assertEqual(child_feature.attribute('policycheck2value'), 'Def Blabla L3') # default Value - self.assertEqual(child_feature.attribute('policycheck3value'), None) # unset + self.assertEqual(child_feature.attribute('policycheck1value'), 'Orig Blabla L3') # duplicated + self.assertEqual(child_feature.attribute('policycheck2value'), 'Def Blabla L3') # default Value + self.assertEqual(child_feature.attribute('policycheck3value'), None) # unset ''' # testoutput 2 From 13403e705ba17aef6f28ffb951611969e2d7c5a8 Mon Sep 17 00:00:00 2001 From: Owen Parkins <16523126+oparkins@users.noreply.github.com> Date: Thu, 2 May 2024 17:46:03 -0400 Subject: [PATCH 005/102] [FEATURE] Use TIFFTAG_DOCUMENTNAME as layer name if available (#57146) Originally the GDAL data provider ignores any metadata associated with tiff documents. Many documents contain metadata that provides the name and description of the document. This is more important to provide to the user when dealing with sublayers where users are unable to provide a name externally by renaming the file. This commit allows for sublayers to be defined by this metadata and provided to the user for sublayer selection. Fixes qgis/QGIS#57077 --- src/core/providers/gdal/qgsgdalprovider.cpp | 23 ++++++++++++++++++ tests/src/core/testqgsgdalprovider.cpp | 22 +++++++++++++++++ .../testdata/raster/gtiff_subdataset_tags.tif | Bin 0 -> 1112 bytes 3 files changed, 45 insertions(+) create mode 100644 tests/testdata/raster/gtiff_subdataset_tags.tif diff --git a/src/core/providers/gdal/qgsgdalprovider.cpp b/src/core/providers/gdal/qgsgdalprovider.cpp index 1040abb66fc7b..c959f721ec5f4 100644 --- a/src/core/providers/gdal/qgsgdalprovider.cpp +++ b/src/core/providers/gdal/qgsgdalprovider.cpp @@ -1835,12 +1835,35 @@ QList QgsGdalProvider::sublayerDetails( GDALDatasetH } else { + + // Check if the layer has TIFFTAG_DOCUMENTNAME associated with it. If so, use that name. + GDALDatasetH datasetHandle = GDALOpen( name, GA_ReadOnly ); + + if ( datasetHandle ) + { + + QString tagTIFFDocumentName = GDALGetMetadataItem( datasetHandle, "TIFFTAG_DOCUMENTNAME", nullptr ); + if ( ! tagTIFFDocumentName.isEmpty() ) + { + layerName = tagTIFFDocumentName; + } + + QString tagTIFFImageDescription = GDALGetMetadataItem( datasetHandle, "TIFFTAG_IMAGEDESCRIPTION", nullptr ); + if ( ! tagTIFFImageDescription.isEmpty() ) + { + layerDesc = tagTIFFImageDescription; + } + + GDALClose( datasetHandle ); + } + // try to extract layer name from a path like 'NETCDF:"/baseUri":cell_node' sepIdx = layerName.indexOf( datasetPath + "\":" ); if ( sepIdx >= 0 ) { layerName = layerName.mid( layerName.indexOf( datasetPath + "\":" ) + datasetPath.length() + 2 ); } + } QgsProviderSublayerDetails details; diff --git a/tests/src/core/testqgsgdalprovider.cpp b/tests/src/core/testqgsgdalprovider.cpp index a064e36275c26..5a1a7e2b480b9 100644 --- a/tests/src/core/testqgsgdalprovider.cpp +++ b/tests/src/core/testqgsgdalprovider.cpp @@ -667,6 +667,28 @@ void TestQgsGdalProvider::testGdalProviderQuerySublayers() QCOMPARE( res.at( 0 ).driverName(), QStringLiteral( "SENTINEL2" ) ); rl.reset( qgis::down_cast< QgsRasterLayer * >( res.at( 0 ).toLayer( options ) ) ); QVERIFY( rl->isValid() ); + + // tiff with two raster layers and TIFF Tags describing sublayers + res = mGdalMetadata->querySublayers( QStringLiteral( TEST_DATA_DIR ) + "/raster/gtiff_subdataset_tags.tif" ); + QCOMPARE( res.count(), 2 ); + QCOMPARE( res.at( 0 ).layerNumber(), 1 ); + QCOMPARE( res.at( 0 ).name(), QStringLiteral( "Test Document Name 1" ) ); + QCOMPARE( res.at( 0 ).description(), QStringLiteral( "Test Image Description 1" ) ); + QCOMPARE( res.at( 0 ).uri(), QStringLiteral( "GTIFF_DIR:1:%1/raster/gtiff_subdataset_tags.tif" ).arg( QStringLiteral( TEST_DATA_DIR ) ) ); + QCOMPARE( res.at( 0 ).providerKey(), QStringLiteral( "gdal" ) ); + QCOMPARE( res.at( 0 ).type(), Qgis::LayerType::Raster ); + QCOMPARE( res.at( 0 ).driverName(), QStringLiteral( "GTiff" ) ); + rl.reset( qgis::down_cast< QgsRasterLayer * >( res.at( 0 ).toLayer( options ) ) ); + QVERIFY( rl->isValid() ); + QCOMPARE( res.at( 1 ).layerNumber(), 2 ); + QCOMPARE( res.at( 1 ).name(), QStringLiteral( "Test Document Name 2" ) ); + QCOMPARE( res.at( 1 ).description(), QStringLiteral( "Test Image Description 2" ) ); + QCOMPARE( res.at( 1 ).uri(), QStringLiteral( "GTIFF_DIR:2:%1/raster/gtiff_subdataset_tags.tif" ).arg( QStringLiteral( TEST_DATA_DIR ) ) ); + QCOMPARE( res.at( 1 ).providerKey(), QStringLiteral( "gdal" ) ); + QCOMPARE( res.at( 1 ).type(), Qgis::LayerType::Raster ); + QCOMPARE( res.at( 1 ).driverName(), QStringLiteral( "GTiff" ) ); + rl.reset( qgis::down_cast< QgsRasterLayer * >( res.at( 1 ).toLayer( options ) ) ); + QVERIFY( rl->isValid() ); } void TestQgsGdalProvider::testGdalProviderQuerySublayers_NetCDF() diff --git a/tests/testdata/raster/gtiff_subdataset_tags.tif b/tests/testdata/raster/gtiff_subdataset_tags.tif new file mode 100644 index 0000000000000000000000000000000000000000..dcbb23218562524766e2363ee902a2586cb45de6 GIT binary patch literal 1112 zcmebD)MnsdU|^5{Vq_o$<1jKaFhbdI8jBbcl9&TjJu{LROfMT2FEdo$EQ7q<(BdG)V4FqJcs51oG0h7 literal 0 HcmV?d00001 From 13fa2c6d070063c732790e99dc86148b28ffae00 Mon Sep 17 00:00:00 2001 From: Marco Hugentobler Date: Fri, 3 May 2024 15:23:56 +0200 Subject: [PATCH 006/102] Fix loading of raster styles if the layer/table name is not in the provider uri --- src/core/providers/gdal/qgsgdalprovider.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/providers/gdal/qgsgdalprovider.cpp b/src/core/providers/gdal/qgsgdalprovider.cpp index c959f721ec5f4..e688f911d6362 100644 --- a/src/core/providers/gdal/qgsgdalprovider.cpp +++ b/src/core/providers/gdal/qgsgdalprovider.cpp @@ -4696,6 +4696,10 @@ QString QgsGdalProviderMetadata::loadStoredStyle( const QString &uri, QString &s } QVariantMap uriParts = QgsGdalProviderBase::decodeGdalUri( uri ); QString layerName = uriParts["layerName"].toString(); + if ( layerName.isEmpty() ) + { + layerName = GDALGetMetadataItem( ds.get(), "IDENTIFIER", "" ); + } return QgsOgrUtils::loadStoredStyle( ds.get(), layerName, "", styleName, errCause ); } From 484be5766ec9c5a73378d1f3e33c9cea3291f6c1 Mon Sep 17 00:00:00 2001 From: Marco Hugentobler Date: Fri, 3 May 2024 16:33:06 +0200 Subject: [PATCH 007/102] Fix it in other places too --- src/core/providers/gdal/qgsgdalprovider.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/core/providers/gdal/qgsgdalprovider.cpp b/src/core/providers/gdal/qgsgdalprovider.cpp index e688f911d6362..64cdc46726fdc 100644 --- a/src/core/providers/gdal/qgsgdalprovider.cpp +++ b/src/core/providers/gdal/qgsgdalprovider.cpp @@ -4622,6 +4622,10 @@ int QgsGdalProviderMetadata::listStyles( const QString &uri, QStringList &ids, Q } QVariantMap uriParts = QgsGdalProviderBase::decodeGdalUri( uri ); QString layerName = uriParts["layerName"].toString(); + if ( layerName.isEmpty() ) + { + layerName = GDALGetMetadataItem( ds.get(), "IDENTIFIER", "" ); + } return QgsOgrUtils::listStyles( ds.get(), layerName, "", ids, names, descriptions, errCause ); } @@ -4636,6 +4640,10 @@ bool QgsGdalProviderMetadata::styleExists( const QString &uri, const QString &st } QVariantMap uriParts = QgsGdalProviderBase::decodeGdalUri( uri ); QString layerName = uriParts["layerName"] .toString(); + if ( layerName.isEmpty() ) + { + layerName = GDALGetMetadataItem( ds.get(), "IDENTIFIER", "" ); + } return QgsOgrUtils::styleExists( ds.get(), layerName, "", styleId, errCause ); } @@ -4676,6 +4684,10 @@ bool QgsGdalProviderMetadata::saveStyle( const QString &uri, const QString &qmlS } QVariantMap uriParts = QgsGdalProviderBase::decodeGdalUri( uri ); QString layerName = uriParts["layerName"].toString(); + if ( layerName.isEmpty() ) + { + layerName = GDALGetMetadataItem( ds.get(), "IDENTIFIER", "" ); + } return QgsOgrUtils::saveStyle( ds.get(), layerName, "", qmlStyle, sldStyle, styleName, styleDescription, uiFileContent, useAsDefault, errCause ); } From 834167018e0d2bb5d19949a2e97e33dc63bfdc56 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 22 Apr 2024 10:12:58 +1000 Subject: [PATCH 008/102] Add algorithmAboutToRun signal to QgsProcessingAlgorithmDialogBase This signal can be used to tweak the algorithm's context prior to the algorithm execution. --- .../processing/qgsprocessingalgorithmdialogbase.sip.in | 9 +++++++++ .../processing/qgsprocessingalgorithmdialogbase.sip.in | 9 +++++++++ python/plugins/processing/gui/AlgorithmDialog.py | 1 + src/gui/processing/qgsprocessingalgorithmdialogbase.h | 9 +++++++++ 4 files changed, 28 insertions(+) diff --git a/python/PyQt6/gui/auto_generated/processing/qgsprocessingalgorithmdialogbase.sip.in b/python/PyQt6/gui/auto_generated/processing/qgsprocessingalgorithmdialogbase.sip.in index 6dfc1ac576de0..9b46a74e97e31 100644 --- a/python/PyQt6/gui/auto_generated/processing/qgsprocessingalgorithmdialogbase.sip.in +++ b/python/PyQt6/gui/auto_generated/processing/qgsprocessingalgorithmdialogbase.sip.in @@ -359,6 +359,15 @@ This allows the dialog to override default Processing settings for an individual signals: + void algorithmAboutToRun( QgsProcessingContext *context ); +%Docstring +Emitted when the algorithm is about to run in the specified ``context``. + +This signal can be used to tweak the ``context`` prior to the algorithm execution. + +.. versionadded:: 3.38 +%End + void algorithmFinished( bool successful, const QVariantMap &result ); %Docstring Emitted whenever an algorithm has finished executing in the dialog. diff --git a/python/gui/auto_generated/processing/qgsprocessingalgorithmdialogbase.sip.in b/python/gui/auto_generated/processing/qgsprocessingalgorithmdialogbase.sip.in index f7e34e24e9687..fccedb6a75c82 100644 --- a/python/gui/auto_generated/processing/qgsprocessingalgorithmdialogbase.sip.in +++ b/python/gui/auto_generated/processing/qgsprocessingalgorithmdialogbase.sip.in @@ -359,6 +359,15 @@ This allows the dialog to override default Processing settings for an individual signals: + void algorithmAboutToRun( QgsProcessingContext *context ); +%Docstring +Emitted when the algorithm is about to run in the specified ``context``. + +This signal can be used to tweak the ``context`` prior to the algorithm execution. + +.. versionadded:: 3.38 +%End + void algorithmFinished( bool successful, const QVariantMap &result ); %Docstring Emitted whenever an algorithm has finished executing in the dialog. diff --git a/python/plugins/processing/gui/AlgorithmDialog.py b/python/plugins/processing/gui/AlgorithmDialog.py index 825499d29d872..bac2f1725e9ee 100644 --- a/python/plugins/processing/gui/AlgorithmDialog.py +++ b/python/plugins/processing/gui/AlgorithmDialog.py @@ -166,6 +166,7 @@ def runAlgorithm(self): self.feedback = self.createFeedback() self.context = dataobjects.createContext(self.feedback) self.applyContextOverrides(self.context) + self.algorithmAboutToRun.emit(self.context) checkCRS = ProcessingConfig.getSetting(ProcessingConfig.WARN_UNMATCHING_CRS) try: diff --git a/src/gui/processing/qgsprocessingalgorithmdialogbase.h b/src/gui/processing/qgsprocessingalgorithmdialogbase.h index cf2920750938e..e368c6436ed1a 100644 --- a/src/gui/processing/qgsprocessingalgorithmdialogbase.h +++ b/src/gui/processing/qgsprocessingalgorithmdialogbase.h @@ -413,6 +413,15 @@ class GUI_EXPORT QgsProcessingAlgorithmDialogBase : public QDialog, public QgsPr signals: + /** + * Emitted when the algorithm is about to run in the specified \a context. + * + * This signal can be used to tweak the \a context prior to the algorithm execution. + * + * \since QGIS 3.38 + */ + void algorithmAboutToRun( QgsProcessingContext *context ); + /** * Emitted whenever an algorithm has finished executing in the dialog. * From 86cf8f8815a7ecd939766f0e33276bcfef08821f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 22 Apr 2024 10:22:24 +1000 Subject: [PATCH 009/102] Store more model execution details for later retrieval --- .../models/qgsprocessingmodelresult.sip.in | 12 ++++++ .../models/qgsprocessingmodelresult.sip.in | 12 ++++++ .../models/qgsprocessingmodelalgorithm.cpp | 7 ++-- .../models/qgsprocessingmodelresult.cpp | 8 ++++ .../models/qgsprocessingmodelresult.h | 42 +++++++++++++++++++ .../testqgsprocessingmodelalgorithm.cpp | 8 ++++ 6 files changed, 86 insertions(+), 3 deletions(-) diff --git a/python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in b/python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in index bff0a33a107a5..c9caf69f73699 100644 --- a/python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in +++ b/python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in @@ -100,6 +100,10 @@ Encapsulates the results of running a Processing model QgsProcessingModelResult(); + void clear(); +%Docstring +Clears any existing results. +%End QMap< QString, QgsProcessingModelChildAlgorithmResult > childResults() const; %Docstring @@ -109,6 +113,14 @@ Map keys refer to the child algorithm IDs. %End + + + + QSet< QString > executedChildIds() const; +%Docstring +Returns the set of child algorithm IDs which were executed during the model execution. +%End + }; diff --git a/python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in b/python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in index bff0a33a107a5..c9caf69f73699 100644 --- a/python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in +++ b/python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in @@ -100,6 +100,10 @@ Encapsulates the results of running a Processing model QgsProcessingModelResult(); + void clear(); +%Docstring +Clears any existing results. +%End QMap< QString, QgsProcessingModelChildAlgorithmResult > childResults() const; %Docstring @@ -109,6 +113,14 @@ Map keys refer to the child algorithm IDs. %End + + + + QSet< QString > executedChildIds() const; +%Docstring +Returns the set of child algorithm IDs which were executed during the model execution. +%End + }; diff --git a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp index 9df13d6240d92..ec32cc5990340 100644 --- a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp +++ b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp @@ -320,11 +320,12 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa QgsProcessingMultiStepFeedback modelFeedback( toExecute.count(), feedback ); QgsExpressionContext baseContext = createExpressionContext( parameters, context ); - QVariantMap childResults; - QVariantMap childInputs; + QVariantMap &childInputs = context.modelResult().rawChildInputs(); + QVariantMap &childResults = context.modelResult().rawChildOutputs(); + QSet< QString > &executed = context.modelResult().executedChildIds(); QVariantMap finalResults; - QSet< QString > executed; + bool executedAlg = true; int previousHtmlLogLength = feedback->htmlLog().length(); while ( executedAlg && executed.count() < toExecute.count() ) diff --git a/src/core/processing/models/qgsprocessingmodelresult.cpp b/src/core/processing/models/qgsprocessingmodelresult.cpp index 932663cce706a..2353c8ee2d948 100644 --- a/src/core/processing/models/qgsprocessingmodelresult.cpp +++ b/src/core/processing/models/qgsprocessingmodelresult.cpp @@ -29,3 +29,11 @@ QgsProcessingModelChildAlgorithmResult::QgsProcessingModelChildAlgorithmResult() // QgsProcessingModelResult::QgsProcessingModelResult() = default; + +void QgsProcessingModelResult::clear() +{ + mChildResults.clear(); + mExecutedChildren.clear(); + mRawChildInputs.clear(); + mRawChildOutputs.clear(); +} diff --git a/src/core/processing/models/qgsprocessingmodelresult.h b/src/core/processing/models/qgsprocessingmodelresult.h index df59884fe9bc4..e133b6d604279 100644 --- a/src/core/processing/models/qgsprocessingmodelresult.h +++ b/src/core/processing/models/qgsprocessingmodelresult.h @@ -20,6 +20,7 @@ #include "qgis_core.h" #include "qgis.h" +#include /** * \ingroup core @@ -122,6 +123,10 @@ class CORE_EXPORT QgsProcessingModelResult QgsProcessingModelResult(); + /** + * Clears any existing results. + */ + void clear(); /** * Returns the map of child algorithm results. @@ -139,10 +144,47 @@ class CORE_EXPORT QgsProcessingModelResult */ QMap< QString, QgsProcessingModelChildAlgorithmResult > &childResults() SIP_SKIP { return mChildResults; } + /** + * Returns a reference to the map of raw child algorithm inputs. + * + * Map keys refer to the child algorithm IDs. Map values may take any form, including + * values which are not safe to access from Python. + * + * \note Not available in Python bindings + */ + QVariantMap &rawChildInputs() SIP_SKIP { return mRawChildInputs; } + + /** + * Returns a reference to the map of raw child algorithm outputs. + * + * Map keys refer to the child algorithm IDs. Map values may take any form, including + * values which are not safe to access from Python. + * + * \note Not available in Python bindings + */ + QVariantMap &rawChildOutputs() SIP_SKIP { return mRawChildOutputs; } + + /** + * Returns a reference to the set of child algorithm IDs which were executed + * during the model execution. + * + * \note Not available in Python bindings + */ + QSet< QString > &executedChildIds() SIP_SKIP { return mExecutedChildren; } + + /** + * Returns the set of child algorithm IDs which were executed during the model execution. + */ + QSet< QString > executedChildIds() const { return mExecutedChildren; } + private: QMap< QString, QgsProcessingModelChildAlgorithmResult > mChildResults; + QSet< QString > mExecutedChildren; + QVariantMap mRawChildInputs; + QVariantMap mRawChildOutputs; + }; diff --git a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp index 7df66218f3a02..4aeddd21adae2 100644 --- a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp +++ b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp @@ -1657,6 +1657,7 @@ void TestQgsProcessingModelAlgorithm::modelBranchPruning() // raster input params.insert( QStringLiteral( "LAYER" ), QStringLiteral( "R1" ) ); + context.modelResult().clear(); results = model1.run( params, context, &feedback ); // we should get the raster branch outputs only QVERIFY( !results.value( QStringLiteral( "fill2:RASTER_OUTPUT" ) ).toString().isEmpty() ); @@ -1719,6 +1720,7 @@ void TestQgsProcessingModelAlgorithm::modelBranchPruningConditional() context.expressionContext().scope( 0 )->setVariable( QStringLiteral( "var1" ), 0 ); context.expressionContext().scope( 0 )->setVariable( QStringLiteral( "var2" ), 1 ); + context.modelResult().clear(); results = model1.run( params, context, &feedback, &ok ); QVERIFY( ok ); // the branch with the exception should NOT be hit } @@ -2364,10 +2366,16 @@ void TestQgsProcessingModelAlgorithm::modelWithChildException() QCOMPARE( context.modelResult().childResults().value( "buffer" ).htmlLog().left( 50 ), QStringLiteral( "Prepare algorithm: buffer" ) ); QCOMPARE( context.modelResult().childResults().value( "buffer" ).htmlLog().right( 21 ), QStringLiteral( "s (1 output(s)).
" ) ); QVERIFY( context.temporaryLayerStore()->mapLayer( context.modelResult().childResults().value( "buffer" ).outputs().value( "OUTPUT" ).toString() ) ); + QCOMPARE( context.modelResult().rawChildInputs().value( "buffer" ).toMap().value( "INPUT" ).toString(), QStringLiteral( "v1" ) ); + QCOMPARE( context.modelResult().rawChildInputs().value( "buffer" ).toMap().value( "OUTPUT" ).toString(), QStringLiteral( "memory:Buffered" ) ); + QCOMPARE( context.modelResult().rawChildOutputs().value( "buffer" ).toMap().value( "OUTPUT" ).toString(), context.modelResult().childResults().value( "buffer" ).outputs().value( "OUTPUT" ).toString() ); QCOMPARE( context.modelResult().childResults().value( "raise" ).executionStatus(), Qgis::ProcessingModelChildAlgorithmExecutionStatus::Failed ); QCOMPARE( context.modelResult().childResults().value( "raise" ).htmlLog().left( 49 ), QStringLiteral( "Prepare algorithm: raise" ) ); QVERIFY( context.modelResult().childResults().value( "raise" ).htmlLog().contains( QStringLiteral( "Error encountered while running my second step: something bad happened" ) ) ); + + QSet expected{ QStringLiteral( "buffer" ) }; + QCOMPARE( context.modelResult().executedChildIds(), expected ); } void TestQgsProcessingModelAlgorithm::modelDependencies() From 8cd3c547eef493d76ee4b988b07ed4c8a185c61f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 22 Apr 2024 11:40:45 +1000 Subject: [PATCH 010/102] Add API framework for setting initial state of a model algorithm And allow execution of just a subset of child algorithms from a model, using the initial state as a starting point This will allow "resuming" a model, running part of a model using the state from a previous execution. --- .../processing/qgsprocessingcontext.sip.in | 3 + .../processing/qgsprocessingcontext.sip.in | 3 + src/core/CMakeLists.txt | 2 + .../models/qgsprocessingmodelalgorithm.cpp | 27 +++++- .../processing/qgsprocessingalgorithm.cpp | 7 ++ src/core/processing/qgsprocessingcontext.cpp | 15 ++++ src/core/processing/qgsprocessingcontext.h | 49 +++++++++++ .../testqgsprocessingmodelalgorithm.cpp | 83 +++++++++++++++++++ 8 files changed, 185 insertions(+), 4 deletions(-) diff --git a/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in b/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in index 6ca842b447369..aaa74b652a660 100644 --- a/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in +++ b/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in @@ -657,6 +657,9 @@ Returns list of the equivalent qgis_process arguments representing the settings .. versionadded:: 3.24 %End + + + QgsProcessingModelResult modelResult() const; %Docstring Returns the model results, populated when the context is used to run a model algorithm. diff --git a/python/core/auto_generated/processing/qgsprocessingcontext.sip.in b/python/core/auto_generated/processing/qgsprocessingcontext.sip.in index 38acae6b9dd9a..db83057df622f 100644 --- a/python/core/auto_generated/processing/qgsprocessingcontext.sip.in +++ b/python/core/auto_generated/processing/qgsprocessingcontext.sip.in @@ -657,6 +657,9 @@ Returns list of the equivalent qgis_process arguments representing the settings .. versionadded:: 3.24 %End + + + QgsProcessingModelResult modelResult() const; %Docstring Returns the model results, populated when the context is used to run a model algorithm. diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 20f1221a97776..070d417c746fd 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -279,6 +279,7 @@ set(QGIS_CORE_SRCS processing/models/qgsprocessingmodelcomment.cpp processing/models/qgsprocessingmodelcomponent.cpp processing/models/qgsprocessingmodelgroupbox.cpp + processing/models/qgsprocessingmodelconfig.cpp processing/models/qgsprocessingmodelparameter.cpp processing/models/qgsprocessingmodeloutput.cpp processing/models/qgsprocessingmodelresult.cpp @@ -1742,6 +1743,7 @@ set(QGIS_CORE_HDRS processing/models/qgsprocessingmodelchildparametersource.h processing/models/qgsprocessingmodelcomment.h processing/models/qgsprocessingmodelcomponent.h + processing/models/qgsprocessingmodelconfig.h processing/models/qgsprocessingmodelgroupbox.h processing/models/qgsprocessingmodeloutput.h processing/models/qgsprocessingmodelparameter.h diff --git a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp index ec32cc5990340..43ffce7c82556 100644 --- a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp +++ b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp @@ -300,9 +300,11 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa QSet< QString > toExecute; QMap< QString, QgsProcessingModelChildAlgorithm >::const_iterator childIt = mChildAlgorithms.constBegin(); QSet< QString > broken; + const QSet childSubset = context.modelInitialRunConfig() ? context.modelInitialRunConfig()->childAlgorithmSubset() : QSet(); + const bool useSubsetOfChildren = !childSubset.empty(); for ( ; childIt != mChildAlgorithms.constEnd(); ++childIt ) { - if ( childIt->isActive() ) + if ( childIt->isActive() && ( !useSubsetOfChildren || childSubset.contains( childIt->childId() ) ) ) { if ( childIt->algorithm() ) toExecute.insert( childIt->childId() ); @@ -324,11 +326,27 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa QVariantMap &childResults = context.modelResult().rawChildOutputs(); QSet< QString > &executed = context.modelResult().executedChildIds(); + // start with initial configuration from the context's model configuration (allowing us to + // resume execution using a previous state) + if ( QgsProcessingModelInitialRunConfig *config = context.modelInitialRunConfig() ) + { + childInputs = config->initialChildInputs(); + childResults = config->initialChildOutputs(); + executed = config->previouslyExecutedChildAlgorithms(); + // discard the model config, this should only be used when running the top level model + context.setModelInitialRunConfig( nullptr ); + } + if ( useSubsetOfChildren ) + { + executed.subtract( childSubset ); + } + QVariantMap finalResults; bool executedAlg = true; int previousHtmlLogLength = feedback->htmlLog().length(); - while ( executedAlg && executed.count() < toExecute.count() ) + int countExecuted = 0; + while ( executedAlg && countExecuted < toExecute.count() ) { executedAlg = false; for ( const QString &childId : std::as_const( toExecute ) ) @@ -615,7 +633,8 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa } childAlg.reset( nullptr ); - modelFeedback.setCurrentStep( executed.count() ); + countExecuted++; + modelFeedback.setCurrentStep( countExecuted ); if ( feedback && !skipGenericLogging ) { feedback->pushInfo( QObject::tr( "OK. Execution took %1 s (%n output(s)).", nullptr, results.count() ).arg( childTime.elapsed() / 1000.0 ) ); @@ -647,7 +666,7 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa break; } if ( feedback ) - feedback->pushDebugInfo( QObject::tr( "Model processed OK. Executed %n algorithm(s) total in %1 s.", nullptr, executed.count() ).arg( totalTime.elapsed() / 1000.0 ) ); + feedback->pushDebugInfo( QObject::tr( "Model processed OK. Executed %n algorithm(s) total in %1 s.", nullptr, countExecuted ).arg( totalTime.elapsed() / 1000.0 ) ); mResults = finalResults; mResults.insert( QStringLiteral( "CHILD_RESULTS" ), childResults ); diff --git a/src/core/processing/qgsprocessingalgorithm.cpp b/src/core/processing/qgsprocessingalgorithm.cpp index bd131754473ca..c8f77c7b3e105 100644 --- a/src/core/processing/qgsprocessingalgorithm.cpp +++ b/src/core/processing/qgsprocessingalgorithm.cpp @@ -608,10 +608,17 @@ QVariantMap QgsProcessingAlgorithm::runPrepared( const QVariantMap ¶meters, mLocalContext.reset( new QgsProcessingContext() ); // copy across everything we can safely do from the passed context mLocalContext->copyThreadSafeSettings( context ); + // and we'll run the actual algorithm processing using the local thread safe context runContext = mLocalContext.get(); } + std::unique_ptr< QgsProcessingModelInitialRunConfig > modelConfig = context.takeModelInitialRunConfig(); + if ( modelConfig ) + { + runContext->setModelInitialRunConfig( std::move( modelConfig ) ); + } + mHasExecuted = true; try { diff --git a/src/core/processing/qgsprocessingcontext.cpp b/src/core/processing/qgsprocessingcontext.cpp index 09a39ca4ae431..3feb9876fced2 100644 --- a/src/core/processing/qgsprocessingcontext.cpp +++ b/src/core/processing/qgsprocessingcontext.cpp @@ -310,3 +310,18 @@ void QgsProcessingContext::LayerDetails::setOutputLayerName( QgsMapLayer *layer } } + +QgsProcessingModelInitialRunConfig *QgsProcessingContext::modelInitialRunConfig() +{ + return mModelConfig.get(); +} + +void QgsProcessingContext::setModelInitialRunConfig( std::unique_ptr< QgsProcessingModelInitialRunConfig > config ) +{ + mModelConfig = std::move( config ); +} + +std::unique_ptr< QgsProcessingModelInitialRunConfig > QgsProcessingContext::takeModelInitialRunConfig() +{ + return std::move( mModelConfig ); +} diff --git a/src/core/processing/qgsprocessingcontext.h b/src/core/processing/qgsprocessingcontext.h index 2fe0ce74ece25..48bef7c993e4b 100644 --- a/src/core/processing/qgsprocessingcontext.h +++ b/src/core/processing/qgsprocessingcontext.h @@ -25,6 +25,7 @@ #include "qgsprocessingfeedback.h" #include "qgsprocessingutils.h" #include "qgsprocessingmodelresult.h" +#include "qgsprocessingmodelconfig.h" #include #include @@ -735,6 +736,53 @@ class CORE_EXPORT QgsProcessingContext */ QStringList asQgisProcessArguments( QgsProcessingContext::ProcessArgumentFlags flags = QgsProcessingContext::ProcessArgumentFlags() ) const; + /** + * Returns a reference to the model initial run configuration, used + * to run a model algorithm. + * + * This may be NULLPTR, e.g. when the context is not being used to run a model. + * + * \note This configuration will only be used when running a "top-level" model algorithm, and + * will not be passed on to child models used within that initial top-level model. + * + * \note Not available in Python bindings + * + * \see setModelInitialRunConfig() + * \see takeModelInitialRunConfig() + * + * \since QGIS 3.38 + */ + QgsProcessingModelInitialRunConfig *modelInitialRunConfig() SIP_SKIP; + + /** + * Takes the model initial run configuration from the context. + * + * May return NULLPTR, e.g. when the context is not being used to run a model. + * + * \note Not available in Python bindings + * + * \see modelInitialRunConfig() + * \see setModelInitialRunConfig() + * + * \since QGIS 3.38 + */ + std::unique_ptr< QgsProcessingModelInitialRunConfig > takeModelInitialRunConfig() SIP_SKIP; + + /** + * Sets the model initial run configuration, used to run a model algorithm. + * + * \note This configuration will only be used when running a "top-level" model algorithm, and + * will not be passed on to child models used within that initial top-level model. + * + * \note Not available in Python bindings + * + * \see modelInitialRunConfig() + * \see takeModelInitialRunConfig() + * + * \since QGIS 3.38 + */ + void setModelInitialRunConfig( std::unique_ptr< QgsProcessingModelInitialRunConfig > config ) SIP_SKIP; + /** * Returns the model results, populated when the context is used to run a model algorithm. * @@ -786,6 +834,7 @@ class CORE_EXPORT QgsProcessingContext QString mTemporaryFolderOverride; int mMaximumThreads = QThread::idealThreadCount(); + std::unique_ptr< QgsProcessingModelInitialRunConfig > mModelConfig; QgsProcessingModelResult mModelResult; #ifdef SIP_RUN diff --git a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp index 4aeddd21adae2..b5c9c95a06e6b 100644 --- a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp +++ b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp @@ -148,6 +148,7 @@ class TestQgsProcessingModelAlgorithm: public QgsTest void modelInputs(); void modelOutputs(); void modelWithChildException(); + void modelExecuteWithPreviousState(); void modelDependencies(); void modelSource(); void modelNameMatchesFileName(); @@ -2378,6 +2379,88 @@ void TestQgsProcessingModelAlgorithm::modelWithChildException() QCOMPARE( context.modelResult().executedChildIds(), expected ); } +void TestQgsProcessingModelAlgorithm::modelExecuteWithPreviousState() +{ + QgsProcessingModelAlgorithm m; + + const QgsProcessingModelParameter sourceParam( "test" ); + m.addModelParameter( new QgsProcessingParameterString( "test" ), sourceParam ); + + QgsProcessingModelChildAlgorithm childAlgorithm; + childAlgorithm.setChildId( QStringLiteral( "calculate" ) ); + childAlgorithm.setAlgorithmId( "native:calculateexpression" ); + childAlgorithm.addParameterSources( "INPUT", { QgsProcessingModelChildParameterSource::fromExpression( " @test || '_1'" ) } ); + m.addChildAlgorithm( childAlgorithm ); + + QgsProcessingModelChildAlgorithm childAlgorithm2; + childAlgorithm2.setChildId( QStringLiteral( "calculate2" ) ); + childAlgorithm2.setAlgorithmId( "native:calculateexpression" ); + childAlgorithm2.addParameterSources( "INPUT", { QgsProcessingModelChildParameterSource::fromExpression( " @calculate_OUTPUT || '_2'" ) } ); + childAlgorithm2.setDependencies( { QgsProcessingModelChildDependency( QStringLiteral( "calculate" ) ) } ); + m.addChildAlgorithm( childAlgorithm2 ); + + // run and check context details + QgsProcessingContext context; + context.setLogLevel( Qgis::ProcessingLogLevel::ModelDebug ); + QgsProcessingFeedback feedback; + QVariantMap params; + params.insert( QStringLiteral( "test" ), QStringLiteral( "my string" ) ); + + // start with no initial state + bool ok = false; + m.run( params, context, &feedback, &ok ); + QVERIFY( ok ); + QCOMPARE( context.modelResult().childResults().value( "calculate" ).executionStatus(), Qgis::ProcessingModelChildAlgorithmExecutionStatus::Success ); + QCOMPARE( context.modelResult().childResults().value( "calculate" ).inputs().value( "INPUT" ).toString(), QStringLiteral( "my string_1" ) ); + QCOMPARE( context.modelResult().childResults().value( "calculate" ).outputs().value( "OUTPUT" ).toString(), QStringLiteral( "my string_1" ) ); + QCOMPARE( context.modelResult().rawChildInputs().value( "calculate" ).toMap().value( "INPUT" ).toString(), QStringLiteral( "my string_1" ) ); + QCOMPARE( context.modelResult().rawChildOutputs().value( "calculate" ).toMap().value( "OUTPUT" ).toString(), QStringLiteral( "my string_1" ) ); + + QCOMPARE( context.modelResult().childResults().value( "calculate2" ).executionStatus(), Qgis::ProcessingModelChildAlgorithmExecutionStatus::Success ); + QCOMPARE( context.modelResult().childResults().value( "calculate2" ).inputs().value( "INPUT" ).toString(), QStringLiteral( "my string_1_2" ) ); + QCOMPARE( context.modelResult().childResults().value( "calculate2" ).outputs().value( "OUTPUT" ).toString(), QStringLiteral( "my string_1_2" ) ); + QCOMPARE( context.modelResult().rawChildInputs().value( "calculate2" ).toMap().value( "INPUT" ).toString(), QStringLiteral( "my string_1_2" ) ); + QCOMPARE( context.modelResult().rawChildOutputs().value( "calculate2" ).toMap().value( "OUTPUT" ).toString(), QStringLiteral( "my string_1_2" ) ); + + QSet expected{ QStringLiteral( "calculate" ), QStringLiteral( "calculate2" ) }; + QCOMPARE( context.modelResult().executedChildIds(), expected ); + + context.modelResult().clear(); + // start with an initial state + + std::unique_ptr< QgsProcessingModelInitialRunConfig > modelConfig = std::make_unique< QgsProcessingModelInitialRunConfig >(); + modelConfig->setPreviouslyExecutedChildAlgorithms( { QStringLiteral( "calculate" )} ); + modelConfig->setInitialChildInputs( QVariantMap{ { + QStringLiteral( "calculate" ), QVariantMap{ + { QStringLiteral( "INPUT" ), QStringLiteral( "a different string" ) } + } + }} ); + modelConfig->setInitialChildOutputs( QVariantMap{ { + QStringLiteral( "calculate" ), QVariantMap{ + { QStringLiteral( "OUTPUT" ), QStringLiteral( "a different string" ) } + } + }} ); + context.setModelInitialRunConfig( std::move( modelConfig ) ); + + m.run( params, context, &feedback, &ok ); + QVERIFY( ok ); + // "calculate" should not be re-executed + QCOMPARE( context.modelResult().childResults().value( "calculate" ).executionStatus(), Qgis::ProcessingModelChildAlgorithmExecutionStatus::NotExecuted ); + + // the second child algorithm should be re-run + QCOMPARE( context.modelResult().childResults().value( "calculate2" ).executionStatus(), Qgis::ProcessingModelChildAlgorithmExecutionStatus::Success ); + QCOMPARE( context.modelResult().childResults().value( "calculate2" ).inputs().value( "INPUT" ).toString(), QStringLiteral( "a different string_2" ) ); + QCOMPARE( context.modelResult().childResults().value( "calculate2" ).outputs().value( "OUTPUT" ).toString(), QStringLiteral( "a different string_2" ) ); + QCOMPARE( context.modelResult().rawChildInputs().value( "calculate2" ).toMap().value( "INPUT" ).toString(), QStringLiteral( "a different string_2" ) ); + QCOMPARE( context.modelResult().rawChildOutputs().value( "calculate2" ).toMap().value( "OUTPUT" ).toString(), QStringLiteral( "a different string_2" ) ); + + expected = QSet { QStringLiteral( "calculate" ), QStringLiteral( "calculate2" ) }; + QCOMPARE( context.modelResult().executedChildIds(), expected ); + + // config should be discarded, it should never be re-used or passed on to non top-level models + QVERIFY( !context.modelInitialRunConfig() ); +} + void TestQgsProcessingModelAlgorithm::modelDependencies() { const QgsProcessingModelChildDependency dep( QStringLiteral( "childId" ), QStringLiteral( "branch" ) ); From 20d9657f19c23e3bf21c4fa9be4ab205b92fb53a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 22 Apr 2024 11:52:25 +1000 Subject: [PATCH 011/102] Add mechanism to merge results across different model execution runs So that if a part model was run from a previous state, we can generate the complete state of model results across both runs --- .../models/qgsprocessingmodelresult.sip.in | 7 +++++++ .../models/qgsprocessingmodelresult.sip.in | 7 +++++++ .../models/qgsprocessingmodelresult.cpp | 17 +++++++++++++++++ .../models/qgsprocessingmodelresult.h | 7 +++++++ .../models/qgsmodeldesignerdialog.cpp | 2 +- .../testqgsprocessingmodelalgorithm.cpp | 16 ++++++++++++++++ 6 files changed, 55 insertions(+), 1 deletion(-) diff --git a/python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in b/python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in index c9caf69f73699..1fd7f190d237d 100644 --- a/python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in +++ b/python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in @@ -103,6 +103,13 @@ Encapsulates the results of running a Processing model void clear(); %Docstring Clears any existing results. +%End + + void mergeWith( const QgsProcessingModelResult &other ); +%Docstring +Merges this set of results with an ``other`` set of results. + +Conflicting results from ``other`` will replace results in this object. %End QMap< QString, QgsProcessingModelChildAlgorithmResult > childResults() const; diff --git a/python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in b/python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in index c9caf69f73699..1fd7f190d237d 100644 --- a/python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in +++ b/python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in @@ -103,6 +103,13 @@ Encapsulates the results of running a Processing model void clear(); %Docstring Clears any existing results. +%End + + void mergeWith( const QgsProcessingModelResult &other ); +%Docstring +Merges this set of results with an ``other`` set of results. + +Conflicting results from ``other`` will replace results in this object. %End QMap< QString, QgsProcessingModelChildAlgorithmResult > childResults() const; diff --git a/src/core/processing/models/qgsprocessingmodelresult.cpp b/src/core/processing/models/qgsprocessingmodelresult.cpp index 2353c8ee2d948..815efc86eca5d 100644 --- a/src/core/processing/models/qgsprocessingmodelresult.cpp +++ b/src/core/processing/models/qgsprocessingmodelresult.cpp @@ -37,3 +37,20 @@ void QgsProcessingModelResult::clear() mRawChildInputs.clear(); mRawChildOutputs.clear(); } + +void QgsProcessingModelResult::mergeWith( const QgsProcessingModelResult &other ) +{ + for ( auto it = other.mChildResults.constBegin(); it != other.mChildResults.constEnd(); ++it ) + { + mChildResults.insert( it.key(), it.value() ); + } + mExecutedChildren.unite( other.mExecutedChildren ); + for ( auto it = other.mRawChildInputs.constBegin(); it != other.mRawChildInputs.constEnd(); ++it ) + { + mRawChildInputs.insert( it.key(), it.value() ); + } + for ( auto it = other.mRawChildOutputs.constBegin(); it != other.mRawChildOutputs.constEnd(); ++it ) + { + mRawChildOutputs.insert( it.key(), it.value() ); + } +} diff --git a/src/core/processing/models/qgsprocessingmodelresult.h b/src/core/processing/models/qgsprocessingmodelresult.h index e133b6d604279..e49111ebacd43 100644 --- a/src/core/processing/models/qgsprocessingmodelresult.h +++ b/src/core/processing/models/qgsprocessingmodelresult.h @@ -128,6 +128,13 @@ class CORE_EXPORT QgsProcessingModelResult */ void clear(); + /** + * Merges this set of results with an \a other set of results. + * + * Conflicting results from \a other will replace results in this object. + */ + void mergeWith( const QgsProcessingModelResult &other ); + /** * Returns the map of child algorithm results. * diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.cpp b/src/gui/processing/models/qgsmodeldesignerdialog.cpp index 6b20dba7c7e43..fedaa3b0019e1 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.cpp +++ b/src/gui/processing/models/qgsmodeldesignerdialog.cpp @@ -598,7 +598,7 @@ bool QgsModelDesignerDialog::checkForUnsavedChanges() void QgsModelDesignerDialog::setLastRunResult( const QgsProcessingModelResult &result ) { - mLastResult = result; + mLastResult.mergeWith( result ); if ( mScene ) mScene->setLastRunResult( mLastResult ); } diff --git a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp index b5c9c95a06e6b..6117e03ff0930 100644 --- a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp +++ b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp @@ -2424,6 +2424,7 @@ void TestQgsProcessingModelAlgorithm::modelExecuteWithPreviousState() QSet expected{ QStringLiteral( "calculate" ), QStringLiteral( "calculate2" ) }; QCOMPARE( context.modelResult().executedChildIds(), expected ); + QgsProcessingModelResult firstResult = context.modelResult(); context.modelResult().clear(); // start with an initial state @@ -2446,6 +2447,8 @@ void TestQgsProcessingModelAlgorithm::modelExecuteWithPreviousState() QVERIFY( ok ); // "calculate" should not be re-executed QCOMPARE( context.modelResult().childResults().value( "calculate" ).executionStatus(), Qgis::ProcessingModelChildAlgorithmExecutionStatus::NotExecuted ); + QVERIFY( context.modelResult().childResults().value( "calculate" ).inputs().isEmpty() ); + QVERIFY( context.modelResult().childResults().value( "calculate" ).outputs().isEmpty() ); // the second child algorithm should be re-run QCOMPARE( context.modelResult().childResults().value( "calculate2" ).executionStatus(), Qgis::ProcessingModelChildAlgorithmExecutionStatus::Success ); @@ -2459,6 +2462,19 @@ void TestQgsProcessingModelAlgorithm::modelExecuteWithPreviousState() // config should be discarded, it should never be re-used or passed on to non top-level models QVERIFY( !context.modelInitialRunConfig() ); + + // merge with first result, to get complete set of results across both executions + firstResult.mergeWith( context.modelResult() ); + QCOMPARE( firstResult.childResults().value( "calculate" ).executionStatus(), Qgis::ProcessingModelChildAlgorithmExecutionStatus::Success ); + QCOMPARE( firstResult.childResults().value( "calculate" ).inputs().value( "INPUT" ).toString(), QStringLiteral( "my string_1" ) ); + QCOMPARE( firstResult.childResults().value( "calculate" ).outputs().value( "OUTPUT" ).toString(), QStringLiteral( "my string_1" ) ); + QCOMPARE( firstResult.rawChildInputs().value( "calculate" ).toMap().value( "INPUT" ).toString(), QStringLiteral( "a different string" ) ); + QCOMPARE( firstResult.rawChildOutputs().value( "calculate" ).toMap().value( "OUTPUT" ).toString(), QStringLiteral( "a different string" ) ); + QCOMPARE( firstResult.childResults().value( "calculate2" ).executionStatus(), Qgis::ProcessingModelChildAlgorithmExecutionStatus::Success ); + QCOMPARE( firstResult.childResults().value( "calculate2" ).inputs().value( "INPUT" ).toString(), QStringLiteral( "a different string_2" ) ); + QCOMPARE( firstResult.childResults().value( "calculate2" ).outputs().value( "OUTPUT" ).toString(), QStringLiteral( "a different string_2" ) ); + QCOMPARE( firstResult.rawChildInputs().value( "calculate2" ).toMap().value( "INPUT" ).toString(), QStringLiteral( "a different string_2" ) ); + QCOMPARE( firstResult.rawChildOutputs().value( "calculate2" ).toMap().value( "OUTPUT" ).toString(), QStringLiteral( "a different string_2" ) ); } void TestQgsProcessingModelAlgorithm::modelDependencies() From 83c7ec07d6a6bd839bdfd062665bce325166bd70 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 22 Apr 2024 12:05:24 +1000 Subject: [PATCH 012/102] Add API framework to handle temporary layers generated by previous runs We need special handling to transfer temporary layers generated by a model execution when running a partial model from a previous state --- .../models/qgsprocessingmodelconfig.cpp | 42 +++++ .../models/qgsprocessingmodelconfig.h | 160 ++++++++++++++++++ .../processing/qgsprocessingalgorithm.cpp | 8 + .../testqgsprocessingmodelalgorithm.cpp | 18 ++ 4 files changed, 228 insertions(+) create mode 100644 src/core/processing/models/qgsprocessingmodelconfig.cpp create mode 100644 src/core/processing/models/qgsprocessingmodelconfig.h diff --git a/src/core/processing/models/qgsprocessingmodelconfig.cpp b/src/core/processing/models/qgsprocessingmodelconfig.cpp new file mode 100644 index 0000000000000..be9e30115338f --- /dev/null +++ b/src/core/processing/models/qgsprocessingmodelconfig.cpp @@ -0,0 +1,42 @@ +/*************************************************************************** + qgsprocessingmodelconfig.cpp + ---------------------- + begin : April 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 "qgsprocessingmodelconfig.h" +#include "qgsmaplayerstore.h" + +QgsProcessingModelInitialRunConfig::QgsProcessingModelInitialRunConfig() = default; + +QgsProcessingModelInitialRunConfig::~QgsProcessingModelInitialRunConfig() = default; + +QgsMapLayerStore *QgsProcessingModelInitialRunConfig::previousLayerStore() +{ + return mModelInitialLayerStore.get(); +} + +std::unique_ptr QgsProcessingModelInitialRunConfig::takePreviousLayerStore() +{ + return std::move( mModelInitialLayerStore ); +} + +void QgsProcessingModelInitialRunConfig::setPreviousLayerStore( std::unique_ptr store ) +{ + if ( store ) + { + Q_ASSERT_X( !store->thread(), "QgsProcessingModelInitialRunConfig::setPreviousLayerStore", "store must have been pushed to a nullptr thread prior to calling this method" ); + } + mModelInitialLayerStore = std::move( store ); +} diff --git a/src/core/processing/models/qgsprocessingmodelconfig.h b/src/core/processing/models/qgsprocessingmodelconfig.h new file mode 100644 index 0000000000000..af22d3b4705db --- /dev/null +++ b/src/core/processing/models/qgsprocessingmodelconfig.h @@ -0,0 +1,160 @@ +/*************************************************************************** + qgsprocessingmodelconfig.h + ---------------------- + begin : April 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 QGSPROCESSINGMODELCONFIG_H +#define QGSPROCESSINGMODELCONFIG_H + +#include "qgis_core.h" +#include "qgis.h" +#include + +#define SIP_NO_FILE + +class QgsMapLayerStore; + +/** + * \ingroup core + * \brief Configuration settings which control how a Processing model is executed. + * + * \note Not available in Python bindings. + * + * \since QGIS 3.38 +*/ +class CORE_EXPORT QgsProcessingModelInitialRunConfig +{ + public: + + QgsProcessingModelInitialRunConfig(); + ~QgsProcessingModelInitialRunConfig(); + + /** + * Returns the subset of child algorithms to run (by child ID). + * + * An empty set indicates the entire model should be run. + * + * \see setChildAlgorithmSubset() + */ + QSet childAlgorithmSubset() const { return mChildAlgorithmSubset; } + + /** + * Sets the \a subset of child algorithms to run (by child ID). + * + * An empty set indicates the entire model should be run. + * + * \see childAlgorithmSubset() + */ + void setChildAlgorithmSubset( const QSet &subset ) { mChildAlgorithmSubset = subset; } + + /** + * Returns the map of child algorithm inputs to use as the initial state when running the model. + * + * Map keys refer to the child algorithm IDs. + * + * \see setInitialChildInputs() + */ + QVariantMap initialChildInputs() { return mInitialChildInputs; } + + /** + * Sets the map of child algorithm \a inputs to use as the initial state when running the model. + * + * Map keys refer to the child algorithm IDs. + * + * \see initialChildInputs() + */ + void setInitialChildInputs( const QVariantMap &inputs ) { mInitialChildInputs = inputs; } + + /** + * Returns the map of child algorithm outputs to use as the initial state when running the model. + * + * Map keys refer to the child algorithm IDs. + * + * \see setInitialChildOutputs() + */ + QVariantMap initialChildOutputs() { return mInitialChildOutputs; } + + /** + * Sets the map of child algorithm \a outputs to use as the initial state when running the model. + * + * Map keys refer to the child algorithm IDs. + * + * \see initialChildOutputs() + */ + void setInitialChildOutputs( const QVariantMap &outputs ) { mInitialChildOutputs = outputs; } + + /** + * Returns the set of previously executed child algorithm IDs to use as the initial state + * when running the model. + * + * \see setPreviouslyExecutedChildAlgorithms() + */ + QSet< QString > previouslyExecutedChildAlgorithms() const { return mPreviouslyExecutedChildren; } + + /** + * Sets the previously executed child algorithm IDs to use as the initial state + * when running the model. + * + * \see previouslyExecutedChildAlgorithms() + */ + void setPreviouslyExecutedChildAlgorithms( const QSet< QString > &children ) { mPreviouslyExecutedChildren = children; } + + /** + * Returns a reference to a map store containing copies of temporary layers generated + * during previous model executions. + * + * This may be NULLPTR. + * + * \see setPreviousLayerStore() + * \see takePreviousLayerStore() + */ + QgsMapLayerStore *previousLayerStore(); + + /** + * Takes the map store containing copies of temporary layers generated + * during previous model executions. + * + * May return NULLPTR if this is not available. + * + * \see previousLayerStore() + * \see setPreviousLayerStore() + */ + std::unique_ptr< QgsMapLayerStore > takePreviousLayerStore(); + + /** + * Sets the map store containing copies of temporary layers generated + * during previous model executions. + * + * \warning \a store must have previous been moved to a NULLPTR thread via a call + * to QObject::moveToThread. An assert will be triggered if this condition is not met. + * + * \see previousLayerStore() + * \see takePreviousLayerStore() + */ + void setPreviousLayerStore( std::unique_ptr< QgsMapLayerStore > store ); + + private: + + QSet mChildAlgorithmSubset; + QVariantMap mInitialChildInputs; + QVariantMap mInitialChildOutputs; + QSet< QString > mPreviouslyExecutedChildren; + + std::unique_ptr< QgsMapLayerStore > mModelInitialLayerStore; + + +}; + +#endif // QGSPROCESSINGMODELCONFIG_H diff --git a/src/core/processing/qgsprocessingalgorithm.cpp b/src/core/processing/qgsprocessingalgorithm.cpp index c8f77c7b3e105..a569cf50a3363 100644 --- a/src/core/processing/qgsprocessingalgorithm.cpp +++ b/src/core/processing/qgsprocessingalgorithm.cpp @@ -616,6 +616,14 @@ QVariantMap QgsProcessingAlgorithm::runPrepared( const QVariantMap ¶meters, std::unique_ptr< QgsProcessingModelInitialRunConfig > modelConfig = context.takeModelInitialRunConfig(); if ( modelConfig ) { + std::unique_ptr< QgsMapLayerStore > modelPreviousLayerStore = modelConfig->takePreviousLayerStore(); + if ( modelPreviousLayerStore ) + { + // move layers from previous layer store to context's temporary layer store, in a thread-safe way + Q_ASSERT_X( !modelPreviousLayerStore->thread(), "QgsProcessingAlgorithm::runPrepared", "QgsProcessingModelConfig::modelPreviousLayerStore must have been pushed to a nullptr thread" ); + modelPreviousLayerStore->moveToThread( QThread::currentThread() ); + runContext->temporaryLayerStore()->transferLayersFromStore( modelPreviousLayerStore.get() ); + } runContext->setModelInitialRunConfig( std::move( modelConfig ) ); } diff --git a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp index 6117e03ff0930..798f69be20a61 100644 --- a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp +++ b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp @@ -2475,6 +2475,24 @@ void TestQgsProcessingModelAlgorithm::modelExecuteWithPreviousState() QCOMPARE( firstResult.childResults().value( "calculate2" ).outputs().value( "OUTPUT" ).toString(), QStringLiteral( "a different string_2" ) ); QCOMPARE( firstResult.rawChildInputs().value( "calculate2" ).toMap().value( "INPUT" ).toString(), QStringLiteral( "a different string_2" ) ); QCOMPARE( firstResult.rawChildOutputs().value( "calculate2" ).toMap().value( "OUTPUT" ).toString(), QStringLiteral( "a different string_2" ) ); + + QCOMPARE( context.temporaryLayerStore()->count(), 0 ); + + // test handling of temporary layers generated during earlier runs + modelConfig = std::make_unique< QgsProcessingModelInitialRunConfig >(); + + std::unique_ptr < QgsMapLayerStore > previousStore = std::make_unique< QgsMapLayerStore >(); + QgsVectorLayer *layer = new QgsVectorLayer( "Point?crs=epsg:3111", "v1", "memory" ); + previousStore->addMapLayer( layer ); + previousStore->moveToThread( nullptr ); + modelConfig->setPreviousLayerStore( std::move( previousStore ) ); + + context.setModelInitialRunConfig( std::move( modelConfig ) ); + m.run( params, context, &feedback, &ok ); + QVERIFY( ok ); + // layer should have been transferred to context's temporary layer store as part of model execution + QCOMPARE( context.temporaryLayerStore()->count(), 1 ); + QCOMPARE( context.temporaryLayerStore()->mapLayersByName( QStringLiteral( "v1" ) ).at( 0 ), layer ); } void TestQgsProcessingModelAlgorithm::modelDependencies() From d41a26cce84e4ff0959c003918e0f1196257f3aa Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 22 Apr 2024 12:06:42 +1000 Subject: [PATCH 013/102] Add method to run partial model to model designer API --- .../models/qgsmodeldesignerdialog.cpp | 29 ++++++++++++++++++- .../models/qgsmodeldesignerdialog.h | 2 +- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.cpp b/src/gui/processing/models/qgsmodeldesignerdialog.cpp index fedaa3b0019e1..5c3d624c8125d 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.cpp +++ b/src/gui/processing/models/qgsmodeldesignerdialog.cpp @@ -993,7 +993,7 @@ void QgsModelDesignerDialog::editHelp() } } -void QgsModelDesignerDialog::run() +void QgsModelDesignerDialog::run( const QSet &childAlgorithmSubset ) { QStringList errors; const bool isValid = model()->validate( errors ); @@ -1027,6 +1027,33 @@ void QgsModelDesignerDialog::run() dialog->setLogLevel( Qgis::ProcessingLogLevel::ModelDebug ); dialog->setParameters( mModel->designerParameterValues() ); + connect( dialog.get(), &QgsProcessingAlgorithmDialogBase::algorithmAboutToRun, this, [this, &childAlgorithmSubset]( QgsProcessingContext * context ) + { + if ( ! childAlgorithmSubset.empty() ) + { + // start from previous state + std::unique_ptr< QgsProcessingModelInitialRunConfig > modelConfig = std::make_unique< QgsProcessingModelInitialRunConfig >(); + modelConfig->setChildAlgorithmSubset( childAlgorithmSubset ); + modelConfig->setPreviouslyExecutedChildAlgorithms( mLastResult.executedChildIds() ); + modelConfig->setInitialChildInputs( mLastResult.rawChildInputs() ); + modelConfig->setInitialChildOutputs( mLastResult.rawChildOutputs() ); + + // add copies of layers from previous runs to context's layer store, so that they can be used + // when running the subset + const QMap previousOutputLayers = mLayerStore.temporaryLayerStore()->mapLayers(); + std::unique_ptr previousResultStore = std::make_unique< QgsMapLayerStore >(); + for ( auto it = previousOutputLayers.constBegin(); it != previousOutputLayers.constEnd(); ++it ) + { + std::unique_ptr< QgsMapLayer > clone( it.value()->clone() ); + clone->setId( it.value()->id() ); + previousResultStore->addMapLayer( clone.release() ); + } + previousResultStore->moveToThread( nullptr ); + modelConfig->setPreviousLayerStore( std::move( previousResultStore ) ); + context->setModelInitialRunConfig( std::move( modelConfig ) ); + } + } ); + connect( dialog.get(), &QgsProcessingAlgorithmDialogBase::algorithmFinished, this, [this, &dialog]( bool, const QVariantMap & ) { QgsProcessingContext *context = dialog->processingContext(); diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.h b/src/gui/processing/models/qgsmodeldesignerdialog.h index cf5a9eee61850..71b2ae4bcbb4c 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.h +++ b/src/gui/processing/models/qgsmodeldesignerdialog.h @@ -184,7 +184,7 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode void reorderOutputs(); void setPanelVisibility( bool hidden ); void editHelp(); - void run(); + void run( const QSet &childAlgorithmSubset = QSet() ); void showChildAlgorithmOutputs( const QString &childId ); void showChildAlgorithmLog( const QString &childId ); From ef89c1f213903737bc29d942b7b44039bd1cbfb9 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 22 Apr 2024 12:16:47 +1000 Subject: [PATCH 014/102] [feature] Add "Run Selected Steps" option to model designer This action will run only the selected steps in a model, allowing the user to run a subset of the model. The initial state will be taken from any previous executions of the model through the designer, so results from previous steps in the model are available for the selected steps. This makes it possible for a user to fix parts of a large model, without having to constantly run the entire model to test. Especially useful when earlier steps in the model are time consuming! --- scripts/spell_check/spelling.dat | 2 +- .../models/qgsmodeldesignerdialog.cpp | 24 +++++++++++++++++- .../models/qgsmodeldesignerdialog.h | 1 + .../processing/qgsmodeldesignerdialogbase.ui | 25 ++++++++++++++++--- 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/scripts/spell_check/spelling.dat b/scripts/spell_check/spelling.dat index b30f747d1dc5b..10dad32b93f76 100644 --- a/scripts/spell_check/spelling.dat +++ b/scripts/spell_check/spelling.dat @@ -7491,7 +7491,7 @@ unrepetant:unrepentant unrepetent:unrepentant unresonable:unreasonable unsed:unused -unselect:deselect +unselect:deselect:* unsinged:unsigned unspported:unsupported unsual:unusual diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.cpp b/src/gui/processing/models/qgsmodeldesignerdialog.cpp index 5c3d624c8125d..a65c730ca296b 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.cpp +++ b/src/gui/processing/models/qgsmodeldesignerdialog.cpp @@ -160,7 +160,8 @@ QgsModelDesignerDialog::QgsModelDesignerDialog( QWidget *parent, Qt::WindowFlags connect( mActionReorderOutputs, &QAction::triggered, this, &QgsModelDesignerDialog::reorderOutputs ); connect( mActionEditHelp, &QAction::triggered, this, &QgsModelDesignerDialog::editHelp ); connect( mReorderInputsButton, &QPushButton::clicked, this, &QgsModelDesignerDialog::reorderInputs ); - connect( mActionRun, &QAction::triggered, this, &QgsModelDesignerDialog::run ); + connect( mActionRun, &QAction::triggered, this, [this] { run(); } ); + connect( mActionRunSelectedSteps, &QAction::triggered, this, &QgsModelDesignerDialog::runSelectedSteps ); mActionSnappingEnabled->setChecked( settings.value( QStringLiteral( "/Processing/Modeler/enableSnapToGrid" ), false ).toBool() ); connect( mActionSnappingEnabled, &QAction::toggled, this, [ = ]( bool enabled ) @@ -993,6 +994,27 @@ void QgsModelDesignerDialog::editHelp() } } +void QgsModelDesignerDialog::runSelectedSteps() +{ + QSet children; + const QList< QgsModelComponentGraphicItem * > items = mScene->selectedComponentItems(); + for ( QgsModelComponentGraphicItem *item : items ) + { + if ( QgsProcessingModelChildAlgorithm *childAlgorithm = dynamic_cast< QgsProcessingModelChildAlgorithm *>( item->component() ) ) + { + children.insert( childAlgorithm->childId() ); + } + } + + if ( children.isEmpty() ) + { + mMessageBar->pushWarning( QString(), tr( "No steps are selected" ) ); + return; + } + + run( children ); +} + void QgsModelDesignerDialog::run( const QSet &childAlgorithmSubset ) { QStringList errors; diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.h b/src/gui/processing/models/qgsmodeldesignerdialog.h index 71b2ae4bcbb4c..3ea1af0b847f5 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.h +++ b/src/gui/processing/models/qgsmodeldesignerdialog.h @@ -184,6 +184,7 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode void reorderOutputs(); void setPanelVisibility( bool hidden ); void editHelp(); + void runSelectedSteps(); void run( const QSet &childAlgorithmSubset = QSet() ); void showChildAlgorithmOutputs( const QString &childId ); void showChildAlgorithmLog( const QString &childId ); diff --git a/src/ui/processing/qgsmodeldesignerdialogbase.ui b/src/ui/processing/qgsmodeldesignerdialogbase.ui index b143baa2718cb..ce5fdc8537011 100644 --- a/src/ui/processing/qgsmodeldesignerdialogbase.ui +++ b/src/ui/processing/qgsmodeldesignerdialogbase.ui @@ -60,6 +60,8 @@
+ + @@ -440,7 +442,7 @@ Open Model… - Open model (Ctrl+O) + Open model Ctrl+O @@ -455,7 +457,7 @@ Save Model - Save model (Ctrl+S) + Save model Ctrl+S @@ -470,7 +472,7 @@ Save Model as… - Save model as (Ctrl+S) + Save model as Ctrl+Shift+S @@ -605,7 +607,7 @@ Run Model… - Run model (F5) + Run model F5 @@ -765,6 +767,21 @@ Sets the order for adding layers generated by the model to projects + + + + :/images/themes/default/mActionStart.svg:/images/themes/default/mActionStart.svg + + + Run Selected Steps… + + + Run only the selected steps in the model + + + Shift+F5 + +
From 7b600f3af54b9efc323b25946116ba1151e544f1 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 22 Apr 2024 12:24:02 +1000 Subject: [PATCH 015/102] Add const --- src/core/processing/models/qgsprocessingmodelconfig.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/processing/models/qgsprocessingmodelconfig.h b/src/core/processing/models/qgsprocessingmodelconfig.h index af22d3b4705db..9f455fd82b121 100644 --- a/src/core/processing/models/qgsprocessingmodelconfig.h +++ b/src/core/processing/models/qgsprocessingmodelconfig.h @@ -66,7 +66,7 @@ class CORE_EXPORT QgsProcessingModelInitialRunConfig * * \see setInitialChildInputs() */ - QVariantMap initialChildInputs() { return mInitialChildInputs; } + QVariantMap initialChildInputs() const { return mInitialChildInputs; } /** * Sets the map of child algorithm \a inputs to use as the initial state when running the model. @@ -84,7 +84,7 @@ class CORE_EXPORT QgsProcessingModelInitialRunConfig * * \see setInitialChildOutputs() */ - QVariantMap initialChildOutputs() { return mInitialChildOutputs; } + QVariantMap initialChildOutputs() const { return mInitialChildOutputs; } /** * Sets the map of child algorithm \a outputs to use as the initial state when running the model. From e6fb69c41e048d2e5b3e4c18a06c7078525559c5 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 22 Apr 2024 12:36:12 +1000 Subject: [PATCH 016/102] Also add "Run from Here" action to right click menu on algorithms Allows running the part of the model which starts at the right clicked algorithm only --- .../processing/models/qgsmodelcomponentgraphicitem.sip.in | 7 +++++++ .../processing/models/qgsmodelgraphicsscene.sip.in | 7 +++++++ .../processing/models/qgsmodelcomponentgraphicitem.sip.in | 7 +++++++ .../processing/models/qgsmodelgraphicsscene.sip.in | 7 +++++++ .../processing/models/qgsmodelcomponentgraphicitem.cpp | 5 +++++ src/gui/processing/models/qgsmodelcomponentgraphicitem.h | 7 +++++++ src/gui/processing/models/qgsmodeldesignerdialog.cpp | 8 ++++++++ src/gui/processing/models/qgsmodeldesignerdialog.h | 1 + src/gui/processing/models/qgsmodelgraphicsscene.cpp | 4 ++++ src/gui/processing/models/qgsmodelgraphicsscene.h | 7 +++++++ 10 files changed, 60 insertions(+) diff --git a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in index 86f58913e8527..f4d4fa7c04390 100644 --- a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in +++ b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in @@ -403,6 +403,13 @@ Sets the ``results`` obtained for this child algorithm for the last model execut signals: + void runFromHere(); +%Docstring +Emitted when the user opts to run the model from this child algorithm. + +.. versionadded:: 3.38 +%End + void showPreviousResults(); %Docstring Emitted when the user opts to view previous results from this child algorithm. diff --git a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in index 8ec16930a4eba..8bcad0a6d7374 100644 --- a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in +++ b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in @@ -172,6 +172,13 @@ Emitted whenever a component of the model is changed. %Docstring Emitted whenever the selected item changes. If ``None``, no item is selected. +%End + + void runFromChild( const QString &childId ); +%Docstring +Emitted when the user opts to run the part of the model starting from the specified child algorithm. + +.. versionadded:: 3.38 %End void showChildAlgorithmOutputs( const QString &childId ); diff --git a/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in b/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in index 105283cfb8a11..1f5f79a0ec985 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in @@ -403,6 +403,13 @@ Sets the ``results`` obtained for this child algorithm for the last model execut signals: + void runFromHere(); +%Docstring +Emitted when the user opts to run the model from this child algorithm. + +.. versionadded:: 3.38 +%End + void showPreviousResults(); %Docstring Emitted when the user opts to view previous results from this child algorithm. diff --git a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in index 27f5386024aaa..bf93297c016af 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in @@ -172,6 +172,13 @@ Emitted whenever a component of the model is changed. %Docstring Emitted whenever the selected item changes. If ``None``, no item is selected. +%End + + void runFromChild( const QString &childId ); +%Docstring +Emitted when the user opts to run the part of the model starting from the specified child algorithm. + +.. versionadded:: 3.38 %End void showChildAlgorithmOutputs( const QString &childId ); diff --git a/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp b/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp index a9b0f6da59d60..e52c81a63cfb5 100644 --- a/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp +++ b/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp @@ -870,6 +870,11 @@ QgsModelChildAlgorithmGraphicItem::QgsModelChildAlgorithmGraphicItem( QgsProcess void QgsModelChildAlgorithmGraphicItem::contextMenuEvent( QGraphicsSceneContextMenuEvent *event ) { QMenu *popupmenu = new QMenu( event->widget() ); + QAction *runFromHereAction = popupmenu->addAction( QObject::tr( "Run from Here…" ) ); + runFromHereAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionStart.svg" ) ) ); + connect( runFromHereAction, &QAction::triggered, this, &QgsModelChildAlgorithmGraphicItem::runFromHere ); + popupmenu->addSeparator(); + QAction *removeAction = popupmenu->addAction( QObject::tr( "Remove" ) ); connect( removeAction, &QAction::triggered, this, &QgsModelChildAlgorithmGraphicItem::deleteComponent ); QAction *editAction = popupmenu->addAction( QObject::tr( "Edit…" ) ); diff --git a/src/gui/processing/models/qgsmodelcomponentgraphicitem.h b/src/gui/processing/models/qgsmodelcomponentgraphicitem.h index 296619661c718..d96ee0ade2894 100644 --- a/src/gui/processing/models/qgsmodelcomponentgraphicitem.h +++ b/src/gui/processing/models/qgsmodelcomponentgraphicitem.h @@ -468,6 +468,13 @@ class GUI_EXPORT QgsModelChildAlgorithmGraphicItem : public QgsModelComponentGra signals: + /** + * Emitted when the user opts to run the model from this child algorithm. + * + * \since QGIS 3.38 + */ + void runFromHere(); + /** * Emitted when the user opts to view previous results from this child algorithm. * diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.cpp b/src/gui/processing/models/qgsmodeldesignerdialog.cpp index a65c730ca296b..ffa2d4304ed6b 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.cpp +++ b/src/gui/processing/models/qgsmodeldesignerdialog.cpp @@ -513,6 +513,7 @@ void QgsModelDesignerDialog::setModelScene( QgsModelGraphicsScene *scene ) } ); connect( mScene, &QgsModelGraphicsScene::componentAboutToChange, this, [ = ]( const QString & description, int id ) { beginUndoCommand( description, id ); } ); connect( mScene, &QgsModelGraphicsScene::componentChanged, this, [ = ] { endUndoCommand(); } ); + connect( mScene, &QgsModelGraphicsScene::runFromChild, this, &QgsModelDesignerDialog::runFromChild ); connect( mScene, &QgsModelGraphicsScene::showChildAlgorithmOutputs, this, &QgsModelDesignerDialog::showChildAlgorithmOutputs ); connect( mScene, &QgsModelGraphicsScene::showChildAlgorithmLog, this, &QgsModelDesignerDialog::showChildAlgorithmLog ); @@ -1015,6 +1016,13 @@ void QgsModelDesignerDialog::runSelectedSteps() run( children ); } +void QgsModelDesignerDialog::runFromChild( const QString &id ) +{ + QSet children = mModel->dependentChildAlgorithms( id ); + children.insert( id ); + run( children ); +} + void QgsModelDesignerDialog::run( const QSet &childAlgorithmSubset ) { QStringList errors; diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.h b/src/gui/processing/models/qgsmodeldesignerdialog.h index 3ea1af0b847f5..f882bcc42688a 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.h +++ b/src/gui/processing/models/qgsmodeldesignerdialog.h @@ -185,6 +185,7 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode void setPanelVisibility( bool hidden ); void editHelp(); void runSelectedSteps(); + void runFromChild( const QString &id ); void run( const QSet &childAlgorithmSubset = QSet() ); void showChildAlgorithmOutputs( const QString &childId ); void showChildAlgorithmLog( const QString &childId ); diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.cpp b/src/gui/processing/models/qgsmodelgraphicsscene.cpp index 0b52ea83bcb24..4a915859905d2 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.cpp +++ b/src/gui/processing/models/qgsmodelgraphicsscene.cpp @@ -145,6 +145,10 @@ void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, Qgs connect( item, &QgsModelComponentGraphicItem::requestModelRepaint, this, &QgsModelGraphicsScene::rebuildRequired ); connect( item, &QgsModelComponentGraphicItem::changed, this, &QgsModelGraphicsScene::componentChanged ); connect( item, &QgsModelComponentGraphicItem::aboutToChange, this, &QgsModelGraphicsScene::componentAboutToChange ); + connect( item, &QgsModelChildAlgorithmGraphicItem::runFromHere, this, [this, childId] + { + emit runFromChild( childId ); + } ); connect( item, &QgsModelChildAlgorithmGraphicItem::showPreviousResults, this, [this, childId] { emit showChildAlgorithmOutputs( childId ); diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.h b/src/gui/processing/models/qgsmodelgraphicsscene.h index b02340fb4f78c..8dab3ac9e7ef5 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.h +++ b/src/gui/processing/models/qgsmodelgraphicsscene.h @@ -187,6 +187,13 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene */ void selectedItemChanged( QgsModelComponentGraphicItem *selected ); + /** + * Emitted when the user opts to run the part of the model starting from the specified child algorithm. + * + * \since QGIS 3.38 + */ + void runFromChild( const QString &childId ); + /** * Emitted when the user opts to view previous results from the child algorithm with matching ID. * From 2f5ad6b51febbbbbabe8156054bdea5b2ec34cb5 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 22 Apr 2024 13:46:48 +1000 Subject: [PATCH 017/102] Add Run Selected option to right click menu for selected algorithms --- .../models/qgsmodelcomponentgraphicitem.sip.in | 7 +++++++ .../processing/models/qgsmodelgraphicsscene.sip.in | 7 +++++++ .../models/qgsmodelcomponentgraphicitem.sip.in | 7 +++++++ .../processing/models/qgsmodelgraphicsscene.sip.in | 7 +++++++ .../processing/models/qgsmodelcomponentgraphicitem.cpp | 9 +++++++++ src/gui/processing/models/qgsmodelcomponentgraphicitem.h | 7 +++++++ src/gui/processing/models/qgsmodeldesignerdialog.cpp | 1 + src/gui/processing/models/qgsmodelgraphicsscene.cpp | 1 + src/gui/processing/models/qgsmodelgraphicsscene.h | 7 +++++++ 9 files changed, 53 insertions(+) diff --git a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in index f4d4fa7c04390..a7e625dab85f3 100644 --- a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in +++ b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in @@ -407,6 +407,13 @@ Sets the ``results`` obtained for this child algorithm for the last model execut %Docstring Emitted when the user opts to run the model from this child algorithm. +.. versionadded:: 3.38 +%End + + void runSelected(); +%Docstring +Emitted when the user opts to run selected steps from the model. + .. versionadded:: 3.38 %End diff --git a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in index 8bcad0a6d7374..94e778a804bfb 100644 --- a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in +++ b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in @@ -172,6 +172,13 @@ Emitted whenever a component of the model is changed. %Docstring Emitted whenever the selected item changes. If ``None``, no item is selected. +%End + + void runSelected(); +%Docstring +Emitted when the user opts to run selected steps from the model. + +.. versionadded:: 3.38 %End void runFromChild( const QString &childId ); diff --git a/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in b/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in index 1f5f79a0ec985..1c74dcbe86034 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in @@ -407,6 +407,13 @@ Sets the ``results`` obtained for this child algorithm for the last model execut %Docstring Emitted when the user opts to run the model from this child algorithm. +.. versionadded:: 3.38 +%End + + void runSelected(); +%Docstring +Emitted when the user opts to run selected steps from the model. + .. versionadded:: 3.38 %End diff --git a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in index bf93297c016af..eeba83f625e50 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in @@ -172,6 +172,13 @@ Emitted whenever a component of the model is changed. %Docstring Emitted whenever the selected item changes. If ``None``, no item is selected. +%End + + void runSelected(); +%Docstring +Emitted when the user opts to run selected steps from the model. + +.. versionadded:: 3.38 %End void runFromChild( const QString &childId ); diff --git a/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp b/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp index e52c81a63cfb5..a5356caebe888 100644 --- a/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp +++ b/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp @@ -870,9 +870,18 @@ QgsModelChildAlgorithmGraphicItem::QgsModelChildAlgorithmGraphicItem( QgsProcess void QgsModelChildAlgorithmGraphicItem::contextMenuEvent( QGraphicsSceneContextMenuEvent *event ) { QMenu *popupmenu = new QMenu( event->widget() ); + + if ( isSelected() ) + { + QAction *runSelectedStepsAction = popupmenu->addAction( QObject::tr( "Run Selected Steps…" ) ); + runSelectedStepsAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionStart.svg" ) ) ); + connect( runSelectedStepsAction, &QAction::triggered, this, &QgsModelChildAlgorithmGraphicItem::runSelected ); + } + QAction *runFromHereAction = popupmenu->addAction( QObject::tr( "Run from Here…" ) ); runFromHereAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionStart.svg" ) ) ); connect( runFromHereAction, &QAction::triggered, this, &QgsModelChildAlgorithmGraphicItem::runFromHere ); + popupmenu->addSeparator(); QAction *removeAction = popupmenu->addAction( QObject::tr( "Remove" ) ); diff --git a/src/gui/processing/models/qgsmodelcomponentgraphicitem.h b/src/gui/processing/models/qgsmodelcomponentgraphicitem.h index d96ee0ade2894..9ba6aa585d302 100644 --- a/src/gui/processing/models/qgsmodelcomponentgraphicitem.h +++ b/src/gui/processing/models/qgsmodelcomponentgraphicitem.h @@ -475,6 +475,13 @@ class GUI_EXPORT QgsModelChildAlgorithmGraphicItem : public QgsModelComponentGra */ void runFromHere(); + /** + * Emitted when the user opts to run selected steps from the model. + * + * \since QGIS 3.38 + */ + void runSelected(); + /** * Emitted when the user opts to view previous results from this child algorithm. * diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.cpp b/src/gui/processing/models/qgsmodeldesignerdialog.cpp index ffa2d4304ed6b..ca4ba4ab42d79 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.cpp +++ b/src/gui/processing/models/qgsmodeldesignerdialog.cpp @@ -514,6 +514,7 @@ void QgsModelDesignerDialog::setModelScene( QgsModelGraphicsScene *scene ) connect( mScene, &QgsModelGraphicsScene::componentAboutToChange, this, [ = ]( const QString & description, int id ) { beginUndoCommand( description, id ); } ); connect( mScene, &QgsModelGraphicsScene::componentChanged, this, [ = ] { endUndoCommand(); } ); connect( mScene, &QgsModelGraphicsScene::runFromChild, this, &QgsModelDesignerDialog::runFromChild ); + connect( mScene, &QgsModelGraphicsScene::runSelected, this, &QgsModelDesignerDialog::runSelectedSteps ); connect( mScene, &QgsModelGraphicsScene::showChildAlgorithmOutputs, this, &QgsModelDesignerDialog::showChildAlgorithmOutputs ); connect( mScene, &QgsModelGraphicsScene::showChildAlgorithmLog, this, &QgsModelDesignerDialog::showChildAlgorithmLog ); diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.cpp b/src/gui/processing/models/qgsmodelgraphicsscene.cpp index 4a915859905d2..87deb39e79a98 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.cpp +++ b/src/gui/processing/models/qgsmodelgraphicsscene.cpp @@ -149,6 +149,7 @@ void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, Qgs { emit runFromChild( childId ); } ); + connect( item, &QgsModelChildAlgorithmGraphicItem::runSelected, this, &QgsModelGraphicsScene::runSelected ); connect( item, &QgsModelChildAlgorithmGraphicItem::showPreviousResults, this, [this, childId] { emit showChildAlgorithmOutputs( childId ); diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.h b/src/gui/processing/models/qgsmodelgraphicsscene.h index 8dab3ac9e7ef5..ed6ace858273b 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.h +++ b/src/gui/processing/models/qgsmodelgraphicsscene.h @@ -187,6 +187,13 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene */ void selectedItemChanged( QgsModelComponentGraphicItem *selected ); + /** + * Emitted when the user opts to run selected steps from the model. + * + * \since QGIS 3.38 + */ + void runSelected(); + /** * Emitted when the user opts to run the part of the model starting from the specified child algorithm. * From a485eb811ca63ef18c9990fb9915fbc812dfaa61 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 22 Apr 2024 13:52:48 +1000 Subject: [PATCH 018/102] Add icon for run selected --- images/images.qrc | 1 + images/themes/default/mActionRunSelected.svg | 1 + src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp | 2 +- src/ui/processing/qgsmodeldesignerdialogbase.ui | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 images/themes/default/mActionRunSelected.svg diff --git a/images/images.qrc b/images/images.qrc index ee0d8e3dbd33f..e6d471965b575 100644 --- a/images/images.qrc +++ b/images/images.qrc @@ -998,6 +998,7 @@ themes/default/mTemporalNavigationMovie.svg themes/default/mActionAddSensorThingsLayer.svg themes/default/mIconSensorThings.svg + themes/default/mActionRunSelected.svg qgis_tips/symbol_levels.png diff --git a/images/themes/default/mActionRunSelected.svg b/images/themes/default/mActionRunSelected.svg new file mode 100644 index 0000000000000..c3883ba26a67f --- /dev/null +++ b/images/themes/default/mActionRunSelected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp b/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp index a5356caebe888..881579569f596 100644 --- a/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp +++ b/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp @@ -874,7 +874,7 @@ void QgsModelChildAlgorithmGraphicItem::contextMenuEvent( QGraphicsSceneContextM if ( isSelected() ) { QAction *runSelectedStepsAction = popupmenu->addAction( QObject::tr( "Run Selected Steps…" ) ); - runSelectedStepsAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionStart.svg" ) ) ); + runSelectedStepsAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionRunSelected.svg" ) ) ); connect( runSelectedStepsAction, &QAction::triggered, this, &QgsModelChildAlgorithmGraphicItem::runSelected ); } diff --git a/src/ui/processing/qgsmodeldesignerdialogbase.ui b/src/ui/processing/qgsmodeldesignerdialogbase.ui index ce5fdc8537011..a4508b75bfcb9 100644 --- a/src/ui/processing/qgsmodeldesignerdialogbase.ui +++ b/src/ui/processing/qgsmodeldesignerdialogbase.ui @@ -770,7 +770,7 @@ - :/images/themes/default/mActionStart.svg:/images/themes/default/mActionStart.svg + :/images/themes/default/mActionRunSelected.svg:/images/themes/default/mActionRunSelected.svg Run Selected Steps… From 4fbada39a56a9e6fcf1aceba27748fd9716ca66d Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 22 Apr 2024 15:30:22 +1000 Subject: [PATCH 019/102] Silence false positive warnings --- .../processing/models/qgsprocessingmodelalgorithm.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp index 43ffce7c82556..a0dda949cc419 100644 --- a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp +++ b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp @@ -450,7 +450,10 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa else { context.pushToThread( qApp->thread() ); +// silence false positive leak warning +#ifndef __clang_analyzer__ QMetaObject::invokeMethod( qApp, prepareOnMainThread, Qt::BlockingQueuedConnection ); +#endif } Q_ASSERT_X( QThread::currentThread() == context.thread(), "QgsProcessingModelAlgorithm::processAlgorithm", "context was not transferred back to model thread" ); @@ -480,7 +483,10 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa feedback->pushWarning( QObject::tr( "Algorithm “%1” cannot be run in a background thread, switching to main thread for this step" ).arg( childAlg->displayName() ) ); context.pushToThread( qApp->thread() ); +// silence false positive leak warning +#ifndef __clang_analyzer__ QMetaObject::invokeMethod( qApp, runOnMainThread, Qt::BlockingQueuedConnection ); +#endif } else { @@ -512,7 +518,10 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa else { context.pushToThread( qApp->thread() ); +// silence false positive leak warning +#ifndef __clang_analyzer__ QMetaObject::invokeMethod( qApp, postProcessOnMainThread, Qt::BlockingQueuedConnection ); +#endif } Q_ASSERT_X( QThread::currentThread() == context.thread(), "QgsProcessingModelAlgorithm::processAlgorithm", "context was not transferred back to model thread" ); From afbedeae250d84edf03ef6bd562d9bb349dcd892 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 22 Apr 2024 15:30:52 +1000 Subject: [PATCH 020/102] Silence clang tidy warning --- src/core/processing/models/qgsprocessingmodelalgorithm.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp index a0dda949cc419..398954d9b443e 100644 --- a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp +++ b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp @@ -675,7 +675,7 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa break; } if ( feedback ) - feedback->pushDebugInfo( QObject::tr( "Model processed OK. Executed %n algorithm(s) total in %1 s.", nullptr, countExecuted ).arg( totalTime.elapsed() / 1000.0 ) ); + feedback->pushDebugInfo( QObject::tr( "Model processed OK. Executed %n algorithm(s) total in %1 s.", nullptr, countExecuted ).arg( static_cast< double >( totalTime.elapsed() ) / 1000.0 ) ); mResults = finalResults; mResults.insert( QStringLiteral( "CHILD_RESULTS" ), childResults ); From 13109d7577ae4c4859e795186fc06fd8e138c8cc Mon Sep 17 00:00:00 2001 From: Jean Felder Date: Fri, 3 May 2024 15:01:10 +0200 Subject: [PATCH 021/102] qgs3dmapscene: Fix python bindings availability note Qgs3DMapScene is now available in Python bindings. See: https://github.com/qgis/qGIS/commit/176807bc3540f6a2ea001f6dad4f3899c28fc8a1 --- python/3d/auto_generated/qgs3dmapscene.sip.in | 4 ---- python/PyQt6/3d/auto_generated/qgs3dmapscene.sip.in | 4 ---- src/3d/qgs3dmapscene.h | 1 - 3 files changed, 9 deletions(-) diff --git a/python/3d/auto_generated/qgs3dmapscene.sip.in b/python/3d/auto_generated/qgs3dmapscene.sip.in index facf62b02b0e5..e7b740ea8c191 100644 --- a/python/3d/auto_generated/qgs3dmapscene.sip.in +++ b/python/3d/auto_generated/qgs3dmapscene.sip.in @@ -18,10 +18,6 @@ class Qgs3DMapScene : QObject { %Docstring(signature="appended") Entity that encapsulates our 3D scene - contains all other entities (such as terrain) as children. - -.. note:: - - Not available in Python bindings %End %TypeHeaderCode diff --git a/python/PyQt6/3d/auto_generated/qgs3dmapscene.sip.in b/python/PyQt6/3d/auto_generated/qgs3dmapscene.sip.in index 435229d610ab0..53540699166bf 100644 --- a/python/PyQt6/3d/auto_generated/qgs3dmapscene.sip.in +++ b/python/PyQt6/3d/auto_generated/qgs3dmapscene.sip.in @@ -18,10 +18,6 @@ class Qgs3DMapScene : QObject { %Docstring(signature="appended") Entity that encapsulates our 3D scene - contains all other entities (such as terrain) as children. - -.. note:: - - Not available in Python bindings %End %TypeHeaderCode diff --git a/src/3d/qgs3dmapscene.h b/src/3d/qgs3dmapscene.h index 4b370f20d0696..92880fd42b43a 100644 --- a/src/3d/qgs3dmapscene.h +++ b/src/3d/qgs3dmapscene.h @@ -62,7 +62,6 @@ class QgsDoubleRange; /** * \ingroup 3d * \brief Entity that encapsulates our 3D scene - contains all other entities (such as terrain) as children. - * \note Not available in Python bindings */ #ifndef SIP_RUN class _3D_EXPORT Qgs3DMapScene : public Qt3DCore::QEntity From 641668c10823403d6b70425396942110c3ab1e53 Mon Sep 17 00:00:00 2001 From: Jean Felder Date: Sat, 4 May 2024 00:03:15 +0200 Subject: [PATCH 022/102] testqgs3dcameracontroller: Increase tolerance on wheel position tests The computed positions of the tests which contain a mouse wheel operation do not seem to be reliable between different runs. It seems like the first wheel operation may return different results. The position itself is not important for those tests. The important part is to ensure that `mCumulatedWheelY`, `mClickPoint`, `mCurrentOperation`, `pitch()` and `yaw()` are correct. Increase the tolerance to ensure that the test does not fail. --- tests/src/3d/testqgs3dcameracontroller.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/src/3d/testqgs3dcameracontroller.cpp b/tests/src/3d/testqgs3dcameracontroller.cpp index bf9ddf2f11868..b3dc625bff732 100644 --- a/tests/src/3d/testqgs3dcameracontroller.cpp +++ b/tests/src/3d/testqgs3dcameracontroller.cpp @@ -283,9 +283,9 @@ void TestQgs3DCameraController::testZoomWheel() QImage depthImage = Qgs3DUtils::captureSceneDepthBuffer( engine, scene ); scene->cameraController()->depthBufferCaptured( depthImage ); - QGSCOMPARENEARVECTOR3D( scene->cameraController()->mZoomPoint, QVector3D( -1382.3, -1.0, -1036.7 ), 2.0 ); - QGSCOMPARENEARVECTOR3D( scene->cameraController()->cameraPose().centerPoint(), QVector3D( -480.0, -353.7, -360.6 ), 1.0 ); - QGSCOMPARENEAR( scene->cameraController()->cameraPose().distanceFromCenterPoint(), 1985.3, 1.0 ); + QGSCOMPARENEARVECTOR3D( scene->cameraController()->mZoomPoint, QVector3D( -1381.3, 1.0, -1036.7 ), 5.0 ); + QGSCOMPARENEARVECTOR3D( scene->cameraController()->cameraPose().centerPoint(), QVector3D( -480.0, -351.7, -360.6 ), 5.0 ); + QGSCOMPARENEAR( scene->cameraController()->cameraPose().distanceFromCenterPoint(), 1982.3, 5.0 ); QCOMPARE( scene->cameraController()->mCumulatedWheelY, 0 ); QCOMPARE( scene->cameraController()->mClickPoint, QPoint() ); QCOMPARE( scene->cameraController()->mCurrentOperation, QgsCameraController::MouseOperation::None ); @@ -548,7 +548,7 @@ void TestQgs3DCameraController::testRotationCenterZoomWheelRotationCenter() QGSCOMPARENEARVECTOR3D( scene->cameraController()->mZoomPoint, QVector3D( 283.2, -27.0, 923.1 ), 1.5 ); QGSCOMPARENEARVECTOR3D( scene->cameraController()->cameraPose().centerPoint(), QVector3D( 120.3, -116.8, 308.9 ), 2.0 ); - QGSCOMPARENEAR( scene->cameraController()->cameraPose().distanceFromCenterPoint(), 1742.9, 1.0 ); + QGSCOMPARENEAR( scene->cameraController()->cameraPose().distanceFromCenterPoint(), 1742.9, 2.0 ); QCOMPARE( scene->cameraController()->pitch(), initialPitch ); QCOMPARE( scene->cameraController()->yaw(), initialYaw ); QCOMPARE( scene->cameraController()->mCumulatedWheelY, 0 ); @@ -858,7 +858,7 @@ void TestQgs3DCameraController::testTranslateZoomWheelTranslate() QGSCOMPARENEARVECTOR3D( scene->cameraController()->mZoomPoint, QVector3D( 4.8, 9.9, 4.4 ), 1.0 ); QGSCOMPARENEARVECTOR3D( scene->cameraController()->cameraPose().centerPoint(), QVector3D( -615.0, -108.0, -116.6 ), 1.0 ); - QGSCOMPARENEAR( scene->cameraController()->cameraPose().distanceFromCenterPoint(), 1743.4, 1.0 ); + QGSCOMPARENEAR( scene->cameraController()->cameraPose().distanceFromCenterPoint(), 1743.4, 2.0 ); QCOMPARE( scene->cameraController()->mCumulatedWheelY, 0 ); QCOMPARE( scene->cameraController()->mClickPoint, QPoint() ); QCOMPARE( scene->cameraController()->mCurrentOperation, QgsCameraController::MouseOperation::None ); From 7d17b0e06c7fcbf2e7870ce87522940232d022f4 Mon Sep 17 00:00:00 2001 From: Julien Cabieces Date: Mon, 6 May 2024 10:15:59 +0200 Subject: [PATCH 023/102] fix: Avoid crash when deleting QGIS application --- src/app/qgisapp.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 84f643c23b3ba..a3f66dc16ffdf 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -2216,6 +2216,8 @@ QgisApp::~QgisApp() mCoordsEdit = nullptr; delete mLayerTreeView; mLayerTreeView = nullptr; + delete mMessageButton; + mMessageButton = nullptr; QgsGui::nativePlatformInterface()->cleanup(); From 16b616ff504abafaa9e928f55522e882e3327178 Mon Sep 17 00:00:00 2001 From: Jean Felder Date: Fri, 3 May 2024 21:08:52 +0200 Subject: [PATCH 024/102] qgs3dmapscene: Remove unused function addQLayerComponentsToHierarchy This was introduced by commit 85e444e1d6c8c2463f03fe343801e35e11c87d74 to handle shadow effects for old QT versions. Its usage has been removed by commit f08387638383ebb292ac77c76cbaec1dbe7cdbac but the function was still present. Remove it. --- src/3d/qgs3dmapscene.cpp | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/3d/qgs3dmapscene.cpp b/src/3d/qgs3dmapscene.cpp index 8626d6baa262d..1c4e661bacca5 100644 --- a/src/3d/qgs3dmapscene.cpp +++ b/src/3d/qgs3dmapscene.cpp @@ -352,19 +352,6 @@ void removeQLayerComponentsFromHierarchy( Qt3DCore::QEntity *entity ) } } -void addQLayerComponentsToHierarchy( Qt3DCore::QEntity *entity, const QVector &layers ) -{ - for ( Qt3DRender::QLayer *layer : layers ) - entity->addComponent( layer ); - - const QList< Qt3DCore::QEntity *> childEntities = entity->findChildren(); - for ( Qt3DCore::QEntity *child : childEntities ) - { - if ( child != nullptr ) - addQLayerComponentsToHierarchy( child, layers ); - } -} - void Qgs3DMapScene::updateScene( bool forceUpdate ) { if ( forceUpdate ) From 5fd8f87d2b86d3862bba42063582a958fed04682 Mon Sep 17 00:00:00 2001 From: Jean Felder Date: Fri, 3 May 2024 21:15:32 +0200 Subject: [PATCH 025/102] qgs3dmapscene: Remove unused function removeQLayerComponentsFromHierarchy This was introduced by commit 85e444e1d6c8c2463f03fe343801e35e11c87d74 to handle shadow effects for old QT versions. Its usage has been removed by commit f08387638383ebb292ac77c76cbaec1dbe7cdbac but the function was still present. Remove it. --- src/3d/qgs3dmapscene.cpp | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/3d/qgs3dmapscene.cpp b/src/3d/qgs3dmapscene.cpp index 1c4e661bacca5..1cb5f4fa1b48c 100644 --- a/src/3d/qgs3dmapscene.cpp +++ b/src/3d/qgs3dmapscene.cpp @@ -332,26 +332,6 @@ void Qgs3DMapScene::onCameraChanged() emit viewed2DExtentFrom3DChanged( extent2D ); } -void removeQLayerComponentsFromHierarchy( Qt3DCore::QEntity *entity ) -{ - QVector toBeRemovedComponents; - const Qt3DCore::QComponentVector entityComponents = entity->components(); - for ( Qt3DCore::QComponent *component : entityComponents ) - { - Qt3DRender::QLayer *layer = qobject_cast( component ); - if ( layer != nullptr ) - toBeRemovedComponents.push_back( layer ); - } - for ( Qt3DCore::QComponent *component : toBeRemovedComponents ) - entity->removeComponent( component ); - const QList< Qt3DCore::QEntity *> childEntities = entity->findChildren(); - for ( Qt3DCore::QEntity *obj : childEntities ) - { - if ( obj != nullptr ) - removeQLayerComponentsFromHierarchy( obj ); - } -} - void Qgs3DMapScene::updateScene( bool forceUpdate ) { if ( forceUpdate ) From 818986092a779336c748e613e0cfb6fc2a33507c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 May 2024 05:17:10 +0000 Subject: [PATCH 026/102] Bump ejs Bumps the npm_and_yarn group with 1 update in the /resources/server/src/landingpage directory: [ejs](https://github.com/mde/ejs). Updates `ejs` from 3.1.8 to 3.1.10 - [Release notes](https://github.com/mde/ejs/releases) - [Commits](https://github.com/mde/ejs/compare/v3.1.8...v3.1.10) --- updated-dependencies: - dependency-name: ejs dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- resources/server/src/landingpage/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/server/src/landingpage/yarn.lock b/resources/server/src/landingpage/yarn.lock index 26b998a0287aa..9cfcc254c229f 100644 --- a/resources/server/src/landingpage/yarn.lock +++ b/resources/server/src/landingpage/yarn.lock @@ -3899,9 +3899,9 @@ ee-first@1.1.1: integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== ejs@^3.1.6: - version "3.1.8" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.8.tgz#758d32910c78047585c7ef1f92f9ee041c1c190b" - integrity sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ== + version "3.1.10" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== dependencies: jake "^10.8.5" From 3d4c19d3b383f53f67d403134ebb4dc3aa0f1dfe Mon Sep 17 00:00:00 2001 From: uclaros Date: Wed, 27 Mar 2024 17:16:58 +0200 Subject: [PATCH 027/102] Add support for Wind Barb rendering for mesh vector datasets --- .../auto_additions/qgsmeshrenderersettings.py | 10 + .../mesh/qgsmeshrenderersettings.sip.in | 96 ++- .../auto_additions/qgsmeshrenderersettings.py | 10 + .../mesh/qgsmeshrenderersettings.sip.in | 96 ++- src/core/mesh/qgsmeshrenderersettings.cpp | 75 +++ src/core/mesh/qgsmeshrenderersettings.h | 92 ++- src/core/mesh/qgsmeshvectorrenderer.cpp | 163 ++++- src/core/mesh/qgsmeshvectorrenderer.h | 36 +- .../qgsmeshrenderervectorsettingswidget.cpp | 66 +- .../qgsmeshrenderervectorsettingswidget.h | 1 + ...qgsmeshrenderervectorsettingswidgetbase.ui | 599 ++++++++++-------- tests/src/core/testqgsmeshlayerrenderer.cpp | 30 + ...ex_vector_user_grid_dataset_wind_barbs.png | Bin 0 -> 80307 bytes ...ser_grid_dataset_wind_barbs_rotated_45.png | Bin 0 -> 80307 bytes 14 files changed, 1008 insertions(+), 266 deletions(-) create mode 100644 python/core/auto_additions/qgsmeshrenderersettings.py create mode 100644 tests/testdata/control_images/mesh/expected_quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs/expected_quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs.png create mode 100644 tests/testdata/control_images/mesh/expected_quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs_rotated_45/expected_quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs_rotated_45.png diff --git a/python/PyQt6/core/auto_additions/qgsmeshrenderersettings.py b/python/PyQt6/core/auto_additions/qgsmeshrenderersettings.py index cb060364851cb..2ece71f4d5af7 100644 --- a/python/PyQt6/core/auto_additions/qgsmeshrenderersettings.py +++ b/python/PyQt6/core/auto_additions/qgsmeshrenderersettings.py @@ -6,6 +6,16 @@ QgsMeshRendererVectorArrowSettings.Fixed = QgsMeshRendererVectorArrowSettings.ArrowScalingMethod.Fixed QgsMeshRendererVectorStreamlineSettings.MeshGridded = QgsMeshRendererVectorStreamlineSettings.SeedingStartPointsMethod.MeshGridded QgsMeshRendererVectorStreamlineSettings.Random = QgsMeshRendererVectorStreamlineSettings.SeedingStartPointsMethod.Random +# monkey patching scoped based enum +QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.MetersPerSecond.__doc__ = "Meters per second" +QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.KilometersPerHour.__doc__ = "Kilometers per hour" +QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.Knots.__doc__ = "Knots (Nautical miles per hour)" +QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.MilesPerHour.__doc__ = "Miles per hour" +QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.FeetPerSecond.__doc__ = "Feet per second" +QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.OtherUnit.__doc__ = "Other unit" +QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.__doc__ = "Wind speed units. Wind barbs use knots so we use this enum for preset conversion values\n\n" + '* ``MetersPerSecond``: ' + QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.MetersPerSecond.__doc__ + '\n' + '* ``KilometersPerHour``: ' + QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.KilometersPerHour.__doc__ + '\n' + '* ``Knots``: ' + QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.Knots.__doc__ + '\n' + '* ``MilesPerHour``: ' + QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.MilesPerHour.__doc__ + '\n' + '* ``FeetPerSecond``: ' + QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.FeetPerSecond.__doc__ + '\n' + '* ``OtherUnit``: ' + QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.OtherUnit.__doc__ +# -- QgsMeshRendererVectorSettings.Arrows = QgsMeshRendererVectorSettings.Symbology.Arrows QgsMeshRendererVectorSettings.Streamlines = QgsMeshRendererVectorSettings.Symbology.Streamlines QgsMeshRendererVectorSettings.Traces = QgsMeshRendererVectorSettings.Symbology.Traces +QgsMeshRendererVectorSettings.WindBarbs = QgsMeshRendererVectorSettings.Symbology.WindBarbs diff --git a/python/PyQt6/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in b/python/PyQt6/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in index 4e515a672a987..e120c26b13217 100644 --- a/python/PyQt6/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in +++ b/python/PyQt6/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in @@ -419,6 +419,84 @@ Writes configuration to a new DOM element }; +class QgsMeshRendererVectorWindBarbSettings +{ +%Docstring(signature="appended") + +Represents a mesh renderer settings for vector datasets displayed with wind barbs + +.. note:: + + The API is considered EXPERIMENTAL and can be changed without a notice + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsmeshrenderersettings.h" +%End + public: + enum class WindSpeedUnit + { + MetersPerSecond, + KilometersPerHour, + Knots, + MilesPerHour, + FeetPerSecond, + OtherUnit + }; + + double magnitudeMultiplier() const; +%Docstring +Returns the multiplier for the magnitude to convert it to knots +%End + + void setMagnitudeMultiplier( double magnitudeMultiplier ); +%Docstring +Sets a multiplier for the magnitude to convert it to knots +%End + + double shaftLength() const; +%Docstring +Returns the shaft length (in millimeters) +%End + + void setShaftLength( double shaftLength ); +%Docstring +Sets the shaft length (in millimeters) +%End + + Qgis::RenderUnit shaftLengthUnits(); +%Docstring +Sets the units for the shaft length +%End + + void setShaftLengthUnits( Qgis::RenderUnit shaftLengthUnit ); +%Docstring +Returns the units for the shaft length +%End + + WindSpeedUnit magnitudeUnits() const; +%Docstring +Returns the units that the data are in +%End + + void setMagnitudeUnits( WindSpeedUnit units ); +%Docstring +Sets the units that the data are in +%End + + QDomElement writeXml( QDomDocument &doc ) const; +%Docstring +Writes configuration to a new DOM element +%End + void readXml( const QDomElement &elem ); +%Docstring +Reads configuration from the given DOM element +%End + +}; + class QgsMeshRendererVectorSettings { %Docstring(signature="appended") @@ -444,7 +522,9 @@ Represents a renderer settings for vector datasets //! Displaying vector dataset with streamlines Streamlines, //! Displaying vector dataset with particle traces - Traces + Traces, + //! Displaying vector dataset with wind barbs + WindBarbs }; double lineWidth() const; @@ -609,6 +689,20 @@ Returns settings for vector rendered with traces Sets settings for vector rendered with traces .. versionadded:: 3.12 +%End + + QgsMeshRendererVectorWindBarbSettings windBarbSettings() const; +%Docstring +Returns settings for vector rendered with wind barbs + +.. versionadded:: 3.38 +%End + + void setWindBarbSettings( const QgsMeshRendererVectorWindBarbSettings &windBarbSettings ); +%Docstring +Sets settings for vector rendered with wind barbs + +.. versionadded:: 3.38 %End QDomElement writeXml( QDomDocument &doc, const QgsReadWriteContext &context = QgsReadWriteContext() ) const; diff --git a/python/core/auto_additions/qgsmeshrenderersettings.py b/python/core/auto_additions/qgsmeshrenderersettings.py new file mode 100644 index 0000000000000..61188fe179720 --- /dev/null +++ b/python/core/auto_additions/qgsmeshrenderersettings.py @@ -0,0 +1,10 @@ +# The following has been generated automatically from src/core/mesh/qgsmeshrenderersettings.h +# monkey patching scoped based enum +QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.MetersPerSecond.__doc__ = "Meters per second" +QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.KilometersPerHour.__doc__ = "Kilometers per hour" +QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.Knots.__doc__ = "Knots (Nautical miles per hour)" +QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.MilesPerHour.__doc__ = "Miles per hour" +QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.FeetPerSecond.__doc__ = "Feet per second" +QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.OtherUnit.__doc__ = "Other unit" +QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.__doc__ = "Wind speed units. Wind barbs use knots so we use this enum for preset conversion values\n\n" + '* ``MetersPerSecond``: ' + QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.MetersPerSecond.__doc__ + '\n' + '* ``KilometersPerHour``: ' + QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.KilometersPerHour.__doc__ + '\n' + '* ``Knots``: ' + QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.Knots.__doc__ + '\n' + '* ``MilesPerHour``: ' + QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.MilesPerHour.__doc__ + '\n' + '* ``FeetPerSecond``: ' + QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.FeetPerSecond.__doc__ + '\n' + '* ``OtherUnit``: ' + QgsMeshRendererVectorWindBarbSettings.WindSpeedUnit.OtherUnit.__doc__ +# -- diff --git a/python/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in b/python/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in index b8b6545e020ef..90062d5f4dcec 100644 --- a/python/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in +++ b/python/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in @@ -419,6 +419,84 @@ Writes configuration to a new DOM element }; +class QgsMeshRendererVectorWindBarbSettings +{ +%Docstring(signature="appended") + +Represents a mesh renderer settings for vector datasets displayed with wind barbs + +.. note:: + + The API is considered EXPERIMENTAL and can be changed without a notice + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsmeshrenderersettings.h" +%End + public: + enum class WindSpeedUnit + { + MetersPerSecond, + KilometersPerHour, + Knots, + MilesPerHour, + FeetPerSecond, + OtherUnit + }; + + double magnitudeMultiplier() const; +%Docstring +Returns the multiplier for the magnitude to convert it to knots +%End + + void setMagnitudeMultiplier( double magnitudeMultiplier ); +%Docstring +Sets a multiplier for the magnitude to convert it to knots +%End + + double shaftLength() const; +%Docstring +Returns the shaft length (in millimeters) +%End + + void setShaftLength( double shaftLength ); +%Docstring +Sets the shaft length (in millimeters) +%End + + Qgis::RenderUnit shaftLengthUnits(); +%Docstring +Sets the units for the shaft length +%End + + void setShaftLengthUnits( Qgis::RenderUnit shaftLengthUnit ); +%Docstring +Returns the units for the shaft length +%End + + WindSpeedUnit magnitudeUnits() const; +%Docstring +Returns the units that the data are in +%End + + void setMagnitudeUnits( WindSpeedUnit units ); +%Docstring +Sets the units that the data are in +%End + + QDomElement writeXml( QDomDocument &doc ) const; +%Docstring +Writes configuration to a new DOM element +%End + void readXml( const QDomElement &elem ); +%Docstring +Reads configuration from the given DOM element +%End + +}; + class QgsMeshRendererVectorSettings { %Docstring(signature="appended") @@ -444,7 +522,9 @@ Represents a renderer settings for vector datasets //! Displaying vector dataset with streamlines Streamlines, //! Displaying vector dataset with particle traces - Traces + Traces, + //! Displaying vector dataset with wind barbs + WindBarbs }; double lineWidth() const; @@ -609,6 +689,20 @@ Returns settings for vector rendered with traces Sets settings for vector rendered with traces .. versionadded:: 3.12 +%End + + QgsMeshRendererVectorWindBarbSettings windBarbSettings() const; +%Docstring +Returns settings for vector rendered with wind barbs + +.. versionadded:: 3.38 +%End + + void setWindBarbSettings( const QgsMeshRendererVectorWindBarbSettings &windBarbSettings ); +%Docstring +Sets settings for vector rendered with wind barbs + +.. versionadded:: 3.38 %End QDomElement writeXml( QDomDocument &doc, const QgsReadWriteContext &context = QgsReadWriteContext() ) const; diff --git a/src/core/mesh/qgsmeshrenderersettings.cpp b/src/core/mesh/qgsmeshrenderersettings.cpp index 49c7037fe7b32..b5308a0d0909d 100644 --- a/src/core/mesh/qgsmeshrenderersettings.cpp +++ b/src/core/mesh/qgsmeshrenderersettings.cpp @@ -612,6 +612,7 @@ QDomElement QgsMeshRendererVectorSettings::writeXml( QDomDocument &doc, const Qg elem.appendChild( mArrowsSettings.writeXml( doc ) ); elem.appendChild( mStreamLinesSettings.writeXml( doc ) ); elem.appendChild( mTracesSettings.writeXml( doc ) ); + elem.appendChild( mWindBarbSettings.writeXml( doc ) ); return elem; } @@ -644,6 +645,10 @@ void QgsMeshRendererVectorSettings::readXml( const QDomElement &elem, const QgsR const QDomElement elemTraces = elem.firstChildElement( QStringLiteral( "vector-traces-settings" ) ); if ( ! elemTraces.isNull() ) mTracesSettings.readXml( elemTraces ); + + const QDomElement elemWindBarb = elem.firstChildElement( QStringLiteral( "vector-windbarb-settings" ) ); + if ( ! elemWindBarb.isNull() ) + mWindBarbSettings.readXml( elemWindBarb ); } QgsInterpolatedLineColor::ColoringMethod QgsMeshRendererVectorSettings::coloringMethod() const @@ -744,3 +749,73 @@ bool QgsMeshRendererSettings::hasSettings( int datasetGroupIndex ) const { return mRendererScalarSettings.contains( datasetGroupIndex ) || mRendererVectorSettings.contains( datasetGroupIndex ); } + +QgsMeshRendererVectorWindBarbSettings QgsMeshRendererVectorSettings::windBarbSettings() const +{ + return mWindBarbSettings; +} + +void QgsMeshRendererVectorSettings::setWindBarbSettings( const QgsMeshRendererVectorWindBarbSettings &windBarbSettings ) +{ + mWindBarbSettings = windBarbSettings; +} + +void QgsMeshRendererVectorWindBarbSettings::readXml( const QDomElement &elem ) +{ + mShaftLength = elem.attribute( QStringLiteral( "shaft-length" ), QStringLiteral( "10" ) ).toDouble(); + mShaftLengthUnits = static_cast( + elem.attribute( QStringLiteral( "shaft-length-units" ) ).toInt() ); + mMagnitudeMultiplier = elem.attribute( QStringLiteral( "magnitude-multiplier" ), QStringLiteral( "1" ) ).toDouble(); + mMagnitudeUnits = static_cast( + elem.attribute( QStringLiteral( "magnitude-units" ), QStringLiteral( "0" ) ).toInt() ); +} + +QDomElement QgsMeshRendererVectorWindBarbSettings::writeXml( QDomDocument &doc ) const +{ + QDomElement elem = doc.createElement( QStringLiteral( "vector-windbarb-settings" ) ); + elem.setAttribute( QStringLiteral( "shaft-length" ), mShaftLength ); + elem.setAttribute( QStringLiteral( "shaft-length-units" ), static_cast< int >( mShaftLengthUnits ) ); + elem.setAttribute( QStringLiteral( "magnitude-multiplier" ), mMagnitudeMultiplier ); + elem.setAttribute( QStringLiteral( "magnitude-units" ), static_cast< int >( mMagnitudeUnits ) ); + return elem; +} + +double QgsMeshRendererVectorWindBarbSettings::magnitudeMultiplier() const +{ + return mMagnitudeMultiplier; +} + +void QgsMeshRendererVectorWindBarbSettings::setMagnitudeMultiplier( double magnitudeMultiplier ) +{ + mMagnitudeMultiplier = magnitudeMultiplier; +} + +double QgsMeshRendererVectorWindBarbSettings::shaftLength() const +{ + return mShaftLength; +} + +void QgsMeshRendererVectorWindBarbSettings::setShaftLength( double shaftLength ) +{ + mShaftLength = shaftLength; +} + +Qgis::RenderUnit QgsMeshRendererVectorWindBarbSettings::shaftLengthUnits() +{ + return mShaftLengthUnits; +} + +void QgsMeshRendererVectorWindBarbSettings::setShaftLengthUnits( Qgis::RenderUnit shaftLengthUnit ) +{ + mShaftLengthUnits = shaftLengthUnit; +} + +QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit QgsMeshRendererVectorWindBarbSettings::magnitudeUnits() const +{ + return mMagnitudeUnits; +} + +void QgsMeshRendererVectorWindBarbSettings::setMagnitudeUnits( WindSpeedUnit units ) +{ + mMagnitudeUnits = units; +} diff --git a/src/core/mesh/qgsmeshrenderersettings.h b/src/core/mesh/qgsmeshrenderersettings.h index 4a62621f54b48..4668c3a84e3dd 100644 --- a/src/core/mesh/qgsmeshrenderersettings.h +++ b/src/core/mesh/qgsmeshrenderersettings.h @@ -395,6 +395,81 @@ class CORE_EXPORT QgsMeshRendererVectorTracesSettings }; +/** + * \ingroup core + * + * \brief Represents a mesh renderer settings for vector datasets displayed with wind barbs + * + * \note The API is considered EXPERIMENTAL and can be changed without a notice + * + * \since QGIS 3.38 + */ +class CORE_EXPORT QgsMeshRendererVectorWindBarbSettings +{ + public: + //! Wind speed units. Wind barbs use knots so we use this enum for preset conversion values + enum class WindSpeedUnit + { + MetersPerSecond = 0, //!< Meters per second + KilometersPerHour, //!< Kilometers per hour + Knots, //!< Knots (Nautical miles per hour) + MilesPerHour, //!< Miles per hour + FeetPerSecond, //!< Feet per second + OtherUnit //!< Other unit + }; + + /** + * Returns the multiplier for the magnitude to convert it to knots + */ + double magnitudeMultiplier() const; + + /** + * Sets a multiplier for the magnitude to convert it to knots + */ + void setMagnitudeMultiplier( double magnitudeMultiplier ); + + /** + * Returns the shaft length (in millimeters) + */ + double shaftLength() const; + + /** + * Sets the shaft length (in millimeters) + */ + void setShaftLength( double shaftLength ); + + /** + * Sets the units for the shaft length + */ + Qgis::RenderUnit shaftLengthUnits(); + + /** + * Returns the units for the shaft length + */ + void setShaftLengthUnits( Qgis::RenderUnit shaftLengthUnit ); + + /** + * Returns the units that the data are in + */ + WindSpeedUnit magnitudeUnits() const; + + /** + * Sets the units that the data are in + */ + void setMagnitudeUnits( WindSpeedUnit units ); + + //! Writes configuration to a new DOM element + QDomElement writeXml( QDomDocument &doc ) const; + //! Reads configuration from the given DOM element + void readXml( const QDomElement &elem ); + + private: + double mShaftLength = 10; + Qgis::RenderUnit mShaftLengthUnits = Qgis::RenderUnit::Millimeters; + WindSpeedUnit mMagnitudeUnits = WindSpeedUnit::MetersPerSecond; + double mMagnitudeMultiplier = 1; +}; + /** * \ingroup core * @@ -419,7 +494,9 @@ class CORE_EXPORT QgsMeshRendererVectorSettings //! Displaying vector dataset with streamlines Streamlines, //! Displaying vector dataset with particle traces - Traces + Traces, + //! Displaying vector dataset with wind barbs + WindBarbs }; //! Returns line width of the arrow (in millimeters) @@ -551,6 +628,18 @@ class CORE_EXPORT QgsMeshRendererVectorSettings */ void setTracesSettings( const QgsMeshRendererVectorTracesSettings &tracesSettings ); + /** + * Returns settings for vector rendered with wind barbs + * \since QGIS 3.38 + */ + QgsMeshRendererVectorWindBarbSettings windBarbSettings() const; + + /** + * Sets settings for vector rendered with wind barbs + * \since QGIS 3.38 + */ + void setWindBarbSettings( const QgsMeshRendererVectorWindBarbSettings &windBarbSettings ); + //! Writes configuration to a new DOM element QDomElement writeXml( QDomDocument &doc, const QgsReadWriteContext &context = QgsReadWriteContext() ) const; //! Reads configuration from the given DOM element @@ -573,6 +662,7 @@ class CORE_EXPORT QgsMeshRendererVectorSettings QgsMeshRendererVectorArrowSettings mArrowsSettings; QgsMeshRendererVectorStreamlineSettings mStreamLinesSettings; QgsMeshRendererVectorTracesSettings mTracesSettings; + QgsMeshRendererVectorWindBarbSettings mWindBarbSettings; }; /** diff --git a/src/core/mesh/qgsmeshvectorrenderer.cpp b/src/core/mesh/qgsmeshvectorrenderer.cpp index 34533115f29f5..cc477ff305d44 100644 --- a/src/core/mesh/qgsmeshvectorrenderer.cpp +++ b/src/core/mesh/qgsmeshvectorrenderer.cpp @@ -62,11 +62,11 @@ QgsMeshVectorArrowRenderer::QgsMeshVectorArrowRenderer( , mDatasetValuesMag( datasetValuesMag ) , mMinMag( datasetMagMinimumValue ) , mMaxMag( datasetMagMaximumValue ) + , mDataType( dataType ) + , mBufferedExtent( context.mapExtent() ) , mContext( context ) , mCfg( settings ) - , mDataType( dataType ) , mOutputSize( size ) - , mBufferedExtent( context.mapExtent() ) { // should be checked in caller Q_ASSERT( !mDatasetValuesMag.empty() ); @@ -292,7 +292,7 @@ void QgsMeshVectorArrowRenderer::drawVectorDataOnPoints( const QSet indexes const double V = mDatasetValuesMag[i]; // pre-calculated magnitude const QgsPointXY lineStart = mContext.mapToPixel().transform( center.x(), center.y() ); - drawVectorArrow( lineStart, xVal, yVal, V ); + drawVector( lineStart, xVal, yVal, V ); } } @@ -405,13 +405,13 @@ void QgsMeshVectorArrowRenderer::drawVectorDataOnGrid( ) continue; const QgsPointXY lineStart( x, y ); - drawVectorArrow( lineStart, val.x(), val.y(), val.scalar() ); + drawVector( lineStart, val.x(), val.y(), val.scalar() ); } } } } -void QgsMeshVectorArrowRenderer::drawVectorArrow( const QgsPointXY &lineStart, double xVal, double yVal, double magnitude ) +void QgsMeshVectorArrowRenderer::drawVector( const QgsPointXY &lineStart, double xVal, double yVal, double magnitude ) { QgsPointXY lineEnd; double vectorLength; @@ -518,10 +518,163 @@ QgsMeshVectorRenderer *QgsMeshVectorRenderer::makeVectorRenderer( layerExtent, datasetMagMaximumValue ); break; + case QgsMeshRendererVectorSettings::WindBarbs: + renderer = new QgsMeshVectorWindBarbRenderer( + m, + datasetVectorValues, + datasetValuesMag, + datasetMagMaximumValue, + datasetMagMinimumValue, + dataType, + settings, + context, + size ); + break; } return renderer; } +QgsMeshVectorWindBarbRenderer::QgsMeshVectorWindBarbRenderer( + const QgsTriangularMesh &m, + const QgsMeshDataBlock &datasetValues, + const QVector &datasetValuesMag, + double datasetMagMaximumValue, double datasetMagMinimumValue, + QgsMeshDatasetGroupMetadata::DataType dataType, + const QgsMeshRendererVectorSettings &settings, + QgsRenderContext &context, + QSize size ) : QgsMeshVectorArrowRenderer( m, + datasetValues, + datasetValuesMag, + datasetMagMinimumValue, + datasetMagMaximumValue, + dataType, + settings, + context, + size ) +{ +} + +QgsMeshVectorWindBarbRenderer::~QgsMeshVectorWindBarbRenderer() = default; + +void QgsMeshVectorWindBarbRenderer::drawVector( const QgsPointXY &lineStart, double xVal, double yVal, double magnitude ) +{ + // do not render if magnitude is outside of the filtered range (if filtering is enabled) + if ( mCfg.filterMin() >= 0 && magnitude < mCfg.filterMin() ) + return; + if ( mCfg.filterMax() >= 0 && magnitude > mCfg.filterMax() ) + return; + + QPen pen( mContext.painter()->pen() ); + pen.setColor( mVectorColoring.color( magnitude ) ); + mContext.painter()->setPen( pen ); + + // we need a brush to fill center circle and pennants + QBrush brush( pen.color() ); + mContext.painter()->setBrush( brush ); + + const double shaftLength = mContext.convertToPainterUnits( mCfg.windBarbSettings().shaftLength(), + mCfg.windBarbSettings().shaftLengthUnits() ); + const double d = shaftLength / 25; // this is a magic number ratio between shaft length and other barb dimensions + const double centerRadius = d; + const double zeroCircleRadius = 2 * d; + const double barbLength = 8 * d + pen.widthF(); + const double barbAngle = 135; + const double barbOffset = 2 * d + pen.widthF(); + + // Determine the angle of the vector, counter-clockwise, from east + // (and associated trigs) + const double vectorAngle = -1.0 * atan( ( -1.0 * yVal ) / xVal ) - mContext.mapToPixel().mapRotation() * M_DEG2RAD; + const double cosAlpha = cos( vectorAngle ) * mag( xVal ); + const double sinAlpha = sin( vectorAngle ) * mag( xVal ); + + // Now determine the X and Y distances of the end of the line from the start + // Flip the Y axis (pixel vs real-world axis) + const double xDist = cosAlpha * shaftLength; + const double yDist = - sinAlpha * shaftLength; + + if ( std::abs( xDist ) < 1 && std::abs( yDist ) < 1 ) + return; + + // Determine the line coords + const QgsPointXY lineEnd = QgsPointXY( lineStart.x() - xDist, + lineStart.y() - yDist ); + + // Check to see if both of the coords are outside the QImage area, if so, skip the whole vector + if ( ( lineStart.x() < 0 || lineStart.x() > mOutputSize.width() || + lineStart.y() < 0 || lineStart.y() > mOutputSize.height() ) && + ( lineEnd.x() < 0 || lineEnd.x() > mOutputSize.width() || + lineEnd.y() < 0 || lineEnd.y() > mOutputSize.height() ) ) + return; + + // scale the magnitude to convert it to knots + double knots = magnitude * mCfg.windBarbSettings().magnitudeMultiplier() ; + QgsPointXY nextLineOrigin = lineEnd; + + // special case for no wind, just an empty circle + if ( knots < 2.5 ) + { + mContext.painter()->setBrush( Qt::NoBrush ); + mContext.painter()->drawEllipse( lineStart.toQPointF(), zeroCircleRadius, zeroCircleRadius ); + mContext.painter()->setBrush( brush ); + return; + } + + const double azimuth = lineEnd.azimuth( lineStart ); + + // conditionally draw the shaft + if ( knots < 47.5 && knots > 7.5 ) + { + // When first barb is a '10', we want to draw the shaft and barb as a single polyline for a proper join + const QVector< QPointF > pts{ lineStart.toQPointF(), + lineEnd.toQPointF(), + nextLineOrigin.project( barbLength, azimuth + barbAngle ).toQPointF() }; + mContext.painter()->drawPolyline( pts ); + nextLineOrigin = nextLineOrigin.project( barbOffset, azimuth ); + knots -= 10; + } + else + { + // draw just the shaft + mContext.painter()->drawLine( lineStart.toQPointF(), lineEnd.toQPointF() ); + } + + // draw the center circle + mContext.painter()->drawEllipse( lineStart.toQPointF(), centerRadius, centerRadius ); + + // draw pennants (50) + while ( knots > 47.5 ) + { + const QVector< QPointF > pts{ nextLineOrigin.toQPointF(), + nextLineOrigin.project( barbLength / 1.414, azimuth + 90 ).toQPointF(), + nextLineOrigin.project( barbLength / 1.414, azimuth ).toQPointF() }; + mContext.painter()->drawPolygon( pts ); + knots -= 50; + + // don't use an offset for the next pennant + if ( knots > 47.5 ) + nextLineOrigin = nextLineOrigin.project( barbLength / 1.414, azimuth ); + else + nextLineOrigin = nextLineOrigin.project( barbLength / 1.414 + barbOffset, azimuth ); + } + + // draw large barbs (10) + while ( knots > 7.5 ) + { + mContext.painter()->drawLine( nextLineOrigin.toQPointF(), nextLineOrigin.project( barbLength, azimuth + barbAngle ).toQPointF() ); + nextLineOrigin = nextLineOrigin.project( barbOffset, azimuth ); + knots -= 10; + } + + // draw small barb (5) + if ( knots > 2.5 ) + { + // a single '5' barb should not start at the line end + if ( nextLineOrigin == lineEnd ) + nextLineOrigin = nextLineOrigin.project( barbLength / 2, azimuth ); + + mContext.painter()->drawLine( nextLineOrigin.toQPointF(), nextLineOrigin.project( barbLength / 2, azimuth + barbAngle ).toQPointF() ); + } +} ///@endcond diff --git a/src/core/mesh/qgsmeshvectorrenderer.h b/src/core/mesh/qgsmeshvectorrenderer.h index bf5e62cc464dc..6bcf6a593eba3 100644 --- a/src/core/mesh/qgsmeshvectorrenderer.h +++ b/src/core/mesh/qgsmeshvectorrenderer.h @@ -105,7 +105,7 @@ class QgsMeshVectorArrowRenderer : public QgsMeshVectorRenderer //! Draws data on user-defined grid void drawVectorDataOnGrid( ); //! Draws arrow from start point and vector data - void drawVectorArrow( const QgsPointXY &lineStart, double xVal, double yVal, double magnitude ); + virtual void drawVector( const QgsPointXY &lineStart, double xVal, double yVal, double magnitude ); //! Calculates the end point of the arrow based on start point and vector data bool calcVectorLineEnd( QgsPointXY &lineEnd, double &vectorLength, @@ -130,18 +130,46 @@ class QgsMeshVectorArrowRenderer : public QgsMeshVectorRenderer const QVector &mDatasetValuesMag; //magnitudes double mMinMag = 0.0; double mMaxMag = 0.0; - QgsRenderContext &mContext; - const QgsMeshRendererVectorSettings mCfg; QgsMeshDatasetGroupMetadata::DataType mDataType = QgsMeshDatasetGroupMetadata::DataType::DataOnVertices; - QSize mOutputSize; QgsRectangle mBufferedExtent; QPen mPen; + protected: + QgsRenderContext &mContext; + const QgsMeshRendererVectorSettings mCfg; + QSize mOutputSize; QgsInterpolatedLineColor mVectorColoring; }; +/** + * \ingroup core + * + * \brief Helper private class for rendering vector datasets using Wind Barbs + * + * \note not available in Python bindings + * \since QGIS 3.38 + */ +class QgsMeshVectorWindBarbRenderer : public QgsMeshVectorArrowRenderer +{ + public: + //! Ctor + QgsMeshVectorWindBarbRenderer( const QgsTriangularMesh &m, + const QgsMeshDataBlock &datasetValues, + const QVector &datasetValuesMag, + double datasetMagMaximumValue, + double datasetMagMinimumValue, + QgsMeshDatasetGroupMetadata::DataType dataType, + const QgsMeshRendererVectorSettings &settings, + QgsRenderContext &context, + QSize size ); + //! Dtor + ~QgsMeshVectorWindBarbRenderer() override; + + private: + void drawVector( const QgsPointXY &lineStart, double xVal, double yVal, double magnitude ) override; +}; ///@endcond diff --git a/src/gui/mesh/qgsmeshrenderervectorsettingswidget.cpp b/src/gui/mesh/qgsmeshrenderervectorsettingswidget.cpp index e152be9879c68..76b9e21a3b009 100644 --- a/src/gui/mesh/qgsmeshrenderervectorsettingswidget.cpp +++ b/src/gui/mesh/qgsmeshrenderervectorsettingswidget.cpp @@ -27,7 +27,8 @@ QgsMeshRendererVectorSettingsWidget::QgsMeshRendererVectorSettingsWidget( QWidge widgets << mMinMagSpinBox << mMaxMagSpinBox << mHeadWidthSpinBox << mHeadLengthSpinBox << mMinimumShaftSpinBox << mMaximumShaftSpinBox - << mScaleShaftByFactorOfSpinBox << mShaftLengthSpinBox; + << mScaleShaftByFactorOfSpinBox << mShaftLengthSpinBox + << mWindBarbLengthSpinBox << mWindBarbMagnitudeMultiplierSpinBox; // Setup defaults and clear values for spin boxes for ( const auto &widget : std::as_const( widgets ) ) @@ -48,6 +49,9 @@ QgsMeshRendererVectorSettingsWidget::QgsMeshRendererVectorSettingsWidget( QWidge mTracesParticlesCountSpinBox->setClearValue( 1000 ); mTracesMaxLengthSpinBox->setClearValue( 100.0 ); + mWindBarbLengthSpinBox->setClearValue( 10.0 ); + mWindBarbMagnitudeMultiplierSpinBox->setClearValue( 1.0 ); + connect( mColorWidget, &QgsColorButton::colorChanged, this, &QgsMeshRendererVectorSettingsWidget::widgetChanged ); connect( mColoringMethodComboBox, qOverload( &QComboBox::currentIndexChanged ), this, &QgsMeshRendererVectorSettingsWidget::onColoringMethodChanged ); @@ -114,6 +118,19 @@ QgsMeshRendererVectorSettingsWidget::QgsMeshRendererVectorSettingsWidget( QWidge connect( mTracesTailLengthMapUnitWidget, &QgsUnitSelectionWidget::changed, this, &QgsMeshRendererVectorSettingsWidget::widgetChanged ); + + mWindBarbLengthMapUnitWidget->setUnits( + { + Qgis::RenderUnit::Millimeters, + Qgis::RenderUnit::Pixels, + Qgis::RenderUnit::Points + } ); + + connect( mWindBarbLengthMapUnitWidget, &QgsUnitSelectionWidget::changed, + this, &QgsMeshRendererVectorSettingsWidget::widgetChanged ); + connect( mWindBarbUnitsComboBox, qOverload( &QComboBox::currentIndexChanged ), + this, &QgsMeshRendererVectorSettingsWidget::onWindBarbUnitsChanged ); + onWindBarbUnitsChanged( 0 ); } void QgsMeshRendererVectorSettingsWidget::setLayer( QgsMeshLayer *layer ) @@ -191,6 +208,15 @@ QgsMeshRendererVectorSettings QgsMeshRendererVectorSettingsWidget::settings() co tracesSettings.setParticlesCount( mTracesParticlesCountSpinBox->value() ); settings.setTracesSettings( tracesSettings ); + // Wind Barb settings + QgsMeshRendererVectorWindBarbSettings windBarbSettings; + windBarbSettings.setShaftLength( mWindBarbLengthSpinBox->value() ); + windBarbSettings.setShaftLengthUnits( mWindBarbLengthMapUnitWidget->unit() ); + windBarbSettings.setMagnitudeUnits( + static_cast( mWindBarbUnitsComboBox->currentIndex() ) ); + windBarbSettings.setMagnitudeMultiplier( mWindBarbMagnitudeMultiplierSpinBox->value() ); + settings.setWindBarbSettings( windBarbSettings ); + return settings; } @@ -264,6 +290,12 @@ void QgsMeshRendererVectorSettingsWidget::syncToLayer( ) mTracesTailLengthMapUnitWidget->setUnit( tracesSettings.maximumTailLengthUnit() ); mTracesParticlesCountSpinBox->setValue( tracesSettings.particlesCount() ); + // Wind Barb settings + const QgsMeshRendererVectorWindBarbSettings windBarbSettings = settings.windBarbSettings(); + mWindBarbLengthSpinBox->setValue( windBarbSettings.shaftLength() ); + mWindBarbMagnitudeMultiplierSpinBox->setValue( windBarbSettings.magnitudeMultiplier() ); + mWindBarbUnitsComboBox->setCurrentIndex( static_cast( windBarbSettings.magnitudeUnits() ) ); + onWindBarbUnitsChanged( static_cast( windBarbSettings.magnitudeUnits() ) ); } void QgsMeshRendererVectorSettingsWidget::onSymbologyChanged( int currentIndex ) @@ -272,6 +304,7 @@ void QgsMeshRendererVectorSettingsWidget::onSymbologyChanged( int currentIndex ) mArrowLengthGroupBox->setVisible( currentIndex == QgsMeshRendererVectorSettings::Arrows ); mHeadOptionsGroupBox->setVisible( currentIndex == QgsMeshRendererVectorSettings::Arrows ); mTracesGroupBox->setVisible( currentIndex == QgsMeshRendererVectorSettings::Traces ); + mWindBarbGroupBox->setVisible( currentIndex == QgsMeshRendererVectorSettings::WindBarbs ); mDisplayVectorsOnGridGroupBox->setVisible( currentIndex != QgsMeshRendererVectorSettings::Traces ); filterByMagnitudeLabel->setVisible( currentIndex != QgsMeshRendererVectorSettings::Traces ); @@ -282,6 +315,7 @@ void QgsMeshRendererVectorSettingsWidget::onSymbologyChanged( int currentIndex ) mDisplayVectorsOnGridGroupBox->setEnabled( currentIndex == QgsMeshRendererVectorSettings::Arrows || + currentIndex == QgsMeshRendererVectorSettings::WindBarbs || ( currentIndex == QgsMeshRendererVectorSettings::Streamlines && mStreamlinesSeedingMethodComboBox->currentIndex() == QgsMeshRendererVectorStreamlineSettings::MeshGridded ) ) ; } @@ -295,6 +329,36 @@ void QgsMeshRendererVectorSettingsWidget::onStreamLineSeedingMethodChanged( int mDisplayVectorsOnGridGroupBox->setEnabled( !enabled ); } +void QgsMeshRendererVectorSettingsWidget::onWindBarbUnitsChanged( int currentIndex ) +{ + const QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit units = + static_cast( currentIndex ); + double multiplier; + switch ( units ) + { + case QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::Knots: + case QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::OtherUnit: + multiplier = 1.0; + break; + case QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::MetersPerSecond: + multiplier = 3600.0 / 1852.0; + break; + case QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::KilometersPerHour: + multiplier = 1.852; + break; + case QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::MilesPerHour: + multiplier = 1.609344 / 1.852; + break; + case QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::FeetPerSecond: + multiplier = 3600.0 / 1.852 / 5280.0 * 1.609344 ; + break; + } + + mWindBarbMagnitudeMultiplierLabel->setVisible( units == QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::OtherUnit ); + mWindBarbMagnitudeMultiplierSpinBox->setVisible( units == QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::OtherUnit ); + mWindBarbMagnitudeMultiplierSpinBox->setValue( multiplier ); +} + void QgsMeshRendererVectorSettingsWidget::onColoringMethodChanged() { mColorRampShaderGroupBox->setVisible( mColoringMethodComboBox->currentData() == QgsInterpolatedLineColor::ColorRamp ); diff --git a/src/gui/mesh/qgsmeshrenderervectorsettingswidget.h b/src/gui/mesh/qgsmeshrenderervectorsettingswidget.h index a9d6bc358f807..d1b32335e0347 100644 --- a/src/gui/mesh/qgsmeshrenderervectorsettingswidget.h +++ b/src/gui/mesh/qgsmeshrenderervectorsettingswidget.h @@ -68,6 +68,7 @@ class QgsMeshRendererVectorSettingsWidget : public QWidget, private Ui::QgsMeshR private slots: void onSymbologyChanged( int currentIndex ); void onStreamLineSeedingMethodChanged( int currentIndex ); + void onWindBarbUnitsChanged( int currentIndex ); void onColoringMethodChanged(); void onColorRampMinMaxChanged(); void loadColorRampShader(); diff --git a/src/ui/mesh/qgsmeshrenderervectorsettingswidgetbase.ui b/src/ui/mesh/qgsmeshrenderervectorsettingswidgetbase.ui index f086cfcd5712c..1e217dc743d22 100644 --- a/src/ui/mesh/qgsmeshrenderervectorsettingswidgetbase.ui +++ b/src/ui/mesh/qgsmeshrenderervectorsettingswidgetbase.ui @@ -6,8 +6,8 @@ 0 0 - 399 - 861 + 426 + 1006 @@ -26,20 +26,7 @@ 0 - - - - - 120 - 0 - - - - - - - - + Qt::Vertical @@ -52,94 +39,65 @@ - - - - Traces + + + + + + Filter by magnitude + + + + + + + Max + + + + + + + + 0 + 0 + + + + -1000000000000000.000000000000000 + + + 1000000000000000.000000000000000 + + + + + + + Min + + + + + + + -1000000000000000.000000000000000 + + + 1000000000000000.000000000000000 + + + + + + + + + + 120 + 0 + - - - QLayout::SetDefaultConstraint - - - - - 0 - - - - - Particles count - - - - - - - 1000000 - - - 100 - - - 1000 - - - - - - - - - 0 - - - - - Max tail length - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - 1.000000000000000 - - - 99999999999999.000000000000000 - - - 10.000000000000000 - - - 10.000000000000000 - - - - - - - - - @@ -231,29 +189,17 @@ - - - - Coloring Method - - + + - - - - - 0 - - - 0 - - - 0 - - - 0 - - + + + + + 0 + 0 + + @@ -273,58 +219,94 @@ Traces
+ + + Wind Barbs + + - - - - - - Filter by magnitude - - - - - - - Max - - - - - - - - 0 - 0 - - - - -1000000000000000.000000000000000 - - - 1000000000000000.000000000000000 - - - - - - - Min - - - - - - - -1000000000000000.000000000000000 - - - 1000000000000000.000000000000000 - - - - + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Head Options + + + + + + % of shaft length + + + + + + + 100.000000000000000 + + + + + + + + 0 + 0 + + + + 100.000000000000000 + + + + + + + % of shaft length + + + + + + + Length + + + + + + + Width + + + + + + + + + + Coloring Method + + @@ -376,20 +358,6 @@ - - - - Symbology - - - - - - - Line width - - - @@ -457,77 +425,24 @@ - - - - Head Options + + + + Symbology - - - - - % of shaft length - - - - - - - 100.000000000000000 - - - - - - - - 0 - 0 - - - - 100.000000000000000 - - - - - - - % of shaft length - - - - - - - Length - - - - - - - Width - - - - - - + + - Color + Line width - - - - - 0 - 0 - + + + + Color @@ -670,6 +585,184 @@ + + + + Traces + + + + QLayout::SetDefaultConstraint + + + + + 0 + + + + + Particles count + + + + + + + 1000000 + + + 100 + + + 1000 + + + + + + + + + 0 + + + + + Max tail length + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + 1.000000000000000 + + + 99999999999999.000000000000000 + + + 10.000000000000000 + + + 10.000000000000000 + + + + + + + + + + + + + + + Wind Barbs + + + + + + Length + + + + + + + Data units + + + + + + + Select the units the data are in.<br>Values are converted to knots for rendering the wind barbs. + + + + m/s + + + + + km/h + + + + + knots + + + + + mi/h + + + + + ft/s + + + + + other units + + + + + + + + Multiplier + + + + + + + Data will be multiplied by this value to be converted to knots (nautical miles per hour) + + + + + + + + + This defines the shaft length.<br>The pennants and barbs are scaled proportionally. + + + + + + + + + + + diff --git a/tests/src/core/testqgsmeshlayerrenderer.cpp b/tests/src/core/testqgsmeshlayerrenderer.cpp index 2416dadf22f25..47e229cc770e2 100644 --- a/tests/src/core/testqgsmeshlayerrenderer.cpp +++ b/tests/src/core/testqgsmeshlayerrenderer.cpp @@ -85,6 +85,7 @@ class TestQgsMeshRenderer : public QgsTest void test_face_vector_on_user_grid(); void test_face_vector_on_user_grid_streamlines(); void test_vertex_vector_on_user_grid(); + void test_vertex_vector_on_user_grid_wind_barbs(); void test_vertex_vector_on_user_grid_streamlines(); void test_vertex_vector_on_user_grid_streamlines_colorRamp(); void test_vertex_vector_traces(); @@ -161,6 +162,7 @@ void TestQgsMeshRenderer::initTestCase() // Mdal layer mMdalLayer = new QgsMeshLayer( mDataDir + "/quad_and_triangle.2dm", "Triangle and Quad Mdal", "mdal" ); mMdalLayer->dataProvider()->addDataset( mDataDir + "/quad_and_triangle_vertex_scalar_with_inactive_face.dat" ); + mMdalLayer->dataProvider()->addDataset( mDataDir + "/quad_and_triangle_vertex_vector2.dat" ); QVERIFY( mMdalLayer->isValid() ); // Memory layer @@ -474,6 +476,34 @@ void TestQgsMeshRenderer::test_vertex_scalar_dataset_with_inactive_face_renderin QVERIFY( imageCheck( "quad_and_triangle_vertex_scalar_dataset_with_inactive_face", mMdalLayer ) ); } +void TestQgsMeshRenderer::test_vertex_vector_on_user_grid_wind_barbs() +{ + const QgsMeshDatasetIndex ds( 2, 0 ); + const QgsMeshDatasetGroupMetadata metadata = mMdalLayer->dataProvider()->datasetGroupMetadata( ds ); + QCOMPARE( metadata.name(), QStringLiteral( "VertexVectorDataset2" ) ); + + QgsMeshRendererSettings rendererSettings = mMdalLayer->rendererSettings(); + QgsMeshRendererVectorSettings settings = rendererSettings.vectorSettings( ds.group() ); + settings.setOnUserDefinedGrid( true ); + settings.setUserGridCellWidth( 30 ); + settings.setUserGridCellHeight( 30 ); + settings.setLineWidth( 0.5 ); + settings.setSymbology( QgsMeshRendererVectorSettings::WindBarbs ); + settings.setColoringMethod( QgsInterpolatedLineColor::SingleColor ); + QgsMeshRendererVectorWindBarbSettings windBarbSettings = settings.windBarbSettings(); + windBarbSettings.setShaftLength( 20 ); + windBarbSettings.setShaftLengthUnits( Qgis::RenderUnit::Pixels ); + windBarbSettings.setMagnitudeUnits( QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::OtherUnit ); + windBarbSettings.setMagnitudeMultiplier( 2 ); + settings.setWindBarbSettings( windBarbSettings ); + rendererSettings.setVectorSettings( ds.group(), settings ); + mMdalLayer->setRendererSettings( rendererSettings ); + mMdalLayer->setStaticVectorDatasetIndex( ds ); + + QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs", mMemoryLayer ) ); + QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs_rotated_45", mMemoryLayer, 45.0 ) ); +} + void TestQgsMeshRenderer::test_face_vector_on_user_grid() { const QgsMeshDatasetIndex ds( 3, 0 ); diff --git a/tests/testdata/control_images/mesh/expected_quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs/expected_quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs.png b/tests/testdata/control_images/mesh/expected_quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs/expected_quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs.png new file mode 100644 index 0000000000000000000000000000000000000000..4d52273c2648958fec591a954700bf3ad45de786 GIT binary patch literal 80307 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 literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/mesh/expected_quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs_rotated_45/expected_quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs_rotated_45.png b/tests/testdata/control_images/mesh/expected_quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs_rotated_45/expected_quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs_rotated_45.png new file mode 100644 index 0000000000000000000000000000000000000000..4470df0e83b0e090d4301ca3f7e4a01386093833 GIT binary patch literal 80307 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 Ti<~bx>pstP@4V`%SA72u Date: Wed, 1 May 2024 12:31:44 +0300 Subject: [PATCH 028/102] address review --- src/core/mesh/qgsmeshvectorrenderer.cpp | 43 ++++++++----------------- 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/src/core/mesh/qgsmeshvectorrenderer.cpp b/src/core/mesh/qgsmeshvectorrenderer.cpp index cc477ff305d44..d91f3dcbff9d6 100644 --- a/src/core/mesh/qgsmeshvectorrenderer.cpp +++ b/src/core/mesh/qgsmeshvectorrenderer.cpp @@ -34,15 +34,6 @@ #define M_DEG2RAD 0.0174532925 #endif -inline double mag( double input ) -{ - if ( input < 0.0 ) - { - return -1.0; - } - return 1.0; -} - inline bool nodataValue( double x, double y ) { return ( std::isnan( x ) || std::isnan( y ) ); @@ -148,10 +139,10 @@ bool QgsMeshVectorArrowRenderer::calcVectorLineEnd( // Determine the angle of the vector, counter-clockwise, from east // (and associated trigs) - const double vectorAngle = -1.0 * atan( ( -1.0 * yVal ) / xVal ) - mContext.mapToPixel().mapRotation() * M_DEG2RAD; + const double vectorAngle = std::atan2( yVal, xVal ) - mContext.mapToPixel().mapRotation() * M_DEG2RAD; - cosAlpha = cos( vectorAngle ) * mag( xVal ); - sinAlpha = sin( vectorAngle ) * mag( xVal ); + cosAlpha = cos( vectorAngle ); + sinAlpha = sin( vectorAngle ); // Now determine the X and Y distances of the end of the line from the start double xDist = 0.0; @@ -202,11 +193,8 @@ bool QgsMeshVectorArrowRenderer::calcVectorLineEnd( vectorLength = sqrt( xDist * xDist + yDist * yDist ); - // Check to see if both of the coords are outside the QImage area, if so, skip the whole vector - if ( ( lineStart.x() < 0 || lineStart.x() > mOutputSize.width() || - lineStart.y() < 0 || lineStart.y() > mOutputSize.height() ) && - ( lineEnd.x() < 0 || lineEnd.x() > mOutputSize.width() || - lineEnd.y() < 0 || lineEnd.y() > mOutputSize.height() ) ) + // skip rendering if line bbox does not intersect the QImage area + if ( !QgsRectangle( lineStart, lineEnd ).intersects( QgsRectangle( 0, 0, mOutputSize.width(), mOutputSize.height() ) ) ) return true; return false; //success @@ -576,6 +564,9 @@ void QgsMeshVectorWindBarbRenderer::drawVector( const QgsPointXY &lineStart, dou const double shaftLength = mContext.convertToPainterUnits( mCfg.windBarbSettings().shaftLength(), mCfg.windBarbSettings().shaftLengthUnits() ); + if ( shaftLength < 1 ) + return; + const double d = shaftLength / 25; // this is a magic number ratio between shaft length and other barb dimensions const double centerRadius = d; const double zeroCircleRadius = 2 * d; @@ -585,27 +576,19 @@ void QgsMeshVectorWindBarbRenderer::drawVector( const QgsPointXY &lineStart, dou // Determine the angle of the vector, counter-clockwise, from east // (and associated trigs) - const double vectorAngle = -1.0 * atan( ( -1.0 * yVal ) / xVal ) - mContext.mapToPixel().mapRotation() * M_DEG2RAD; - const double cosAlpha = cos( vectorAngle ) * mag( xVal ); - const double sinAlpha = sin( vectorAngle ) * mag( xVal ); + const double vectorAngle = std::atan2( yVal, xVal ) - mContext.mapToPixel().mapRotation() * M_DEG2RAD; // Now determine the X and Y distances of the end of the line from the start // Flip the Y axis (pixel vs real-world axis) - const double xDist = cosAlpha * shaftLength; - const double yDist = - sinAlpha * shaftLength; - - if ( std::abs( xDist ) < 1 && std::abs( yDist ) < 1 ) - return; + const double xDist = cos( vectorAngle ) * shaftLength; + const double yDist = - sin( vectorAngle ) * shaftLength; // Determine the line coords const QgsPointXY lineEnd = QgsPointXY( lineStart.x() - xDist, lineStart.y() - yDist ); - // Check to see if both of the coords are outside the QImage area, if so, skip the whole vector - if ( ( lineStart.x() < 0 || lineStart.x() > mOutputSize.width() || - lineStart.y() < 0 || lineStart.y() > mOutputSize.height() ) && - ( lineEnd.x() < 0 || lineEnd.x() > mOutputSize.width() || - lineEnd.y() < 0 || lineEnd.y() > mOutputSize.height() ) ) + // skip rendering if line bbox does not intersect the QImage area + if ( !QgsRectangle( lineStart, lineEnd ).intersects( QgsRectangle( 0, 0, mOutputSize.width(), mOutputSize.height() ) ) ) return; // scale the magnitude to convert it to knots From eee787f8255eb923403dd7d95d5755b729ac2a1d Mon Sep 17 00:00:00 2001 From: uclaros Date: Wed, 1 May 2024 13:11:26 +0300 Subject: [PATCH 029/102] return magnitude multiplier based on units set --- .../mesh/qgsmeshrenderersettings.sip.in | 3 +- .../mesh/qgsmeshrenderersettings.sip.in | 3 +- src/core/mesh/qgsmeshrenderersettings.cpp | 17 ++++++++++- src/core/mesh/qgsmeshrenderersettings.h | 3 +- .../qgsmeshrenderervectorsettingswidget.cpp | 28 ++++--------------- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/python/PyQt6/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in b/python/PyQt6/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in index e120c26b13217..e679521e1f162 100644 --- a/python/PyQt6/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in +++ b/python/PyQt6/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in @@ -448,7 +448,8 @@ Represents a mesh renderer settings for vector datasets displayed with wind barb double magnitudeMultiplier() const; %Docstring -Returns the multiplier for the magnitude to convert it to knots +Returns the multiplier for the magnitude to convert it to knots, according to the units set with :py:func:`~QgsMeshRendererVectorWindBarbSettings.setMagnitudeUnits` +A custom multiplier can be set with :py:func:`~QgsMeshRendererVectorWindBarbSettings.setMagnitudeMultiplier` for the case when units are set to OtherUnit %End void setMagnitudeMultiplier( double magnitudeMultiplier ); diff --git a/python/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in b/python/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in index 90062d5f4dcec..085a3e0d10b2d 100644 --- a/python/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in +++ b/python/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in @@ -448,7 +448,8 @@ Represents a mesh renderer settings for vector datasets displayed with wind barb double magnitudeMultiplier() const; %Docstring -Returns the multiplier for the magnitude to convert it to knots +Returns the multiplier for the magnitude to convert it to knots, according to the units set with :py:func:`~QgsMeshRendererVectorWindBarbSettings.setMagnitudeUnits` +A custom multiplier can be set with :py:func:`~QgsMeshRendererVectorWindBarbSettings.setMagnitudeMultiplier` for the case when units are set to OtherUnit %End void setMagnitudeMultiplier( double magnitudeMultiplier ); diff --git a/src/core/mesh/qgsmeshrenderersettings.cpp b/src/core/mesh/qgsmeshrenderersettings.cpp index b5308a0d0909d..94a8a5074429c 100644 --- a/src/core/mesh/qgsmeshrenderersettings.cpp +++ b/src/core/mesh/qgsmeshrenderersettings.cpp @@ -782,7 +782,22 @@ QDomElement QgsMeshRendererVectorWindBarbSettings::writeXml( QDomDocument &doc ) double QgsMeshRendererVectorWindBarbSettings::magnitudeMultiplier() const { - return mMagnitudeMultiplier; + switch ( mMagnitudeUnits ) + { + case QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::Knots: + return 1.0; + case QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::MetersPerSecond: + return 3600.0 / 1852.0; + case QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::KilometersPerHour: + return 1.0 / 1.852; + case QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::MilesPerHour: + return 1.609344 / 1.852; + case QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::FeetPerSecond: + return 3600.0 / 1.852 / 5280.0 * 1.609344 ; + case QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::OtherUnit: + return mMagnitudeMultiplier; + } + return 1.0; // should not reach } void QgsMeshRendererVectorWindBarbSettings::setMagnitudeMultiplier( double magnitudeMultiplier ) diff --git a/src/core/mesh/qgsmeshrenderersettings.h b/src/core/mesh/qgsmeshrenderersettings.h index 4668c3a84e3dd..2cca7ce0ef20a 100644 --- a/src/core/mesh/qgsmeshrenderersettings.h +++ b/src/core/mesh/qgsmeshrenderersettings.h @@ -419,7 +419,8 @@ class CORE_EXPORT QgsMeshRendererVectorWindBarbSettings }; /** - * Returns the multiplier for the magnitude to convert it to knots + * Returns the multiplier for the magnitude to convert it to knots, according to the units set with setMagnitudeUnits() + * A custom multiplier can be set with setMagnitudeMultiplier() for the case when units are set to OtherUnit */ double magnitudeMultiplier() const; diff --git a/src/gui/mesh/qgsmeshrenderervectorsettingswidget.cpp b/src/gui/mesh/qgsmeshrenderervectorsettingswidget.cpp index 76b9e21a3b009..c693ed72950e6 100644 --- a/src/gui/mesh/qgsmeshrenderervectorsettingswidget.cpp +++ b/src/gui/mesh/qgsmeshrenderervectorsettingswidget.cpp @@ -50,6 +50,7 @@ QgsMeshRendererVectorSettingsWidget::QgsMeshRendererVectorSettingsWidget( QWidge mTracesMaxLengthSpinBox->setClearValue( 100.0 ); mWindBarbLengthSpinBox->setClearValue( 10.0 ); + mWindBarbMagnitudeMultiplierSpinBox->setValue( 1.0 ); mWindBarbMagnitudeMultiplierSpinBox->setClearValue( 1.0 ); connect( mColorWidget, &QgsColorButton::colorChanged, this, &QgsMeshRendererVectorSettingsWidget::widgetChanged ); @@ -293,9 +294,9 @@ void QgsMeshRendererVectorSettingsWidget::syncToLayer( ) // Wind Barb settings const QgsMeshRendererVectorWindBarbSettings windBarbSettings = settings.windBarbSettings(); mWindBarbLengthSpinBox->setValue( windBarbSettings.shaftLength() ); - mWindBarbMagnitudeMultiplierSpinBox->setValue( windBarbSettings.magnitudeMultiplier() ); mWindBarbUnitsComboBox->setCurrentIndex( static_cast( windBarbSettings.magnitudeUnits() ) ); - onWindBarbUnitsChanged( static_cast( windBarbSettings.magnitudeUnits() ) ); + if ( windBarbSettings.magnitudeUnits() == QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::OtherUnit ) + mWindBarbMagnitudeMultiplierSpinBox->setValue( windBarbSettings.magnitudeMultiplier() ); } void QgsMeshRendererVectorSettingsWidget::onSymbologyChanged( int currentIndex ) @@ -333,30 +334,11 @@ void QgsMeshRendererVectorSettingsWidget::onWindBarbUnitsChanged( int currentInd { const QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit units = static_cast( currentIndex ); - double multiplier; - switch ( units ) - { - case QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::Knots: - case QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::OtherUnit: - multiplier = 1.0; - break; - case QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::MetersPerSecond: - multiplier = 3600.0 / 1852.0; - break; - case QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::KilometersPerHour: - multiplier = 1.852; - break; - case QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::MilesPerHour: - multiplier = 1.609344 / 1.852; - break; - case QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::FeetPerSecond: - multiplier = 3600.0 / 1.852 / 5280.0 * 1.609344 ; - break; - } mWindBarbMagnitudeMultiplierLabel->setVisible( units == QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::OtherUnit ); mWindBarbMagnitudeMultiplierSpinBox->setVisible( units == QgsMeshRendererVectorWindBarbSettings::WindSpeedUnit::OtherUnit ); - mWindBarbMagnitudeMultiplierSpinBox->setValue( multiplier ); + + emit widgetChanged(); } void QgsMeshRendererVectorSettingsWidget::onColoringMethodChanged() From cfe71973c2f7496d08d38f2545199f03841d1869 Mon Sep 17 00:00:00 2001 From: signedav Date: Mon, 6 May 2024 19:45:41 +0200 Subject: [PATCH 030/102] rename FieldDomainDuplicatePolicy to FieldDuplicatePolicy because - not like the Merge or Split Policy it does not affect the FieldDomains but only the Fields instead --- python/PyQt6/core/auto_additions/qgis.py | 10 ++++----- python/PyQt6/core/auto_generated/qgis.sip.in | 2 +- .../PyQt6/core/auto_generated/qgsfield.sip.in | 4 ++-- .../vector/qgsvectorlayer.sip.in | 2 +- python/core/auto_additions/qgis.py | 10 ++++----- python/core/auto_generated/qgis.sip.in | 2 +- python/core/auto_generated/qgsfield.sip.in | 4 ++-- .../vector/qgsvectorlayer.sip.in | 2 +- src/core/qgis.h | 7 +++--- src/core/qgsfield.cpp | 6 ++--- src/core/qgsfield.h | 4 ++-- src/core/qgsfield_p.h | 2 +- src/core/vector/qgsvectorlayer.cpp | 4 ++-- src/core/vector/qgsvectorlayer.h | 6 ++--- src/core/vector/qgsvectorlayerutils.cpp | 6 ++--- .../qgsattributetypedialog.cpp | 20 ++++++++--------- .../qgsattributetypedialog.h | 4 ++-- src/gui/vector/qgsattributesformproperties.h | 2 +- tests/src/core/testqgsfield.cpp | 14 ++++++------ tests/src/python/test_qgsvectorlayer.py | 22 +++++++++---------- tests/src/python/test_qgsvectorlayerutils.py | 18 +++++++-------- 21 files changed, 75 insertions(+), 76 deletions(-) diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index ef8d8971abe01..361389af6f2d1 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -3215,12 +3215,12 @@ # -- Qgis.FieldDomainMergePolicy.baseClass = Qgis # monkey patching scoped based enum -Qgis.FieldDomainDuplicatePolicy.DefaultValue.__doc__ = "Use default field value" -Qgis.FieldDomainDuplicatePolicy.Duplicate.__doc__ = "Duplicate original value" -Qgis.FieldDomainDuplicatePolicy.UnsetField.__doc__ = "Clears the field value so that the data provider backend will populate using any backend triggers or similar logic (since QGIS 3.30)" -Qgis.FieldDomainDuplicatePolicy.__doc__ = "Duplicate policy for fields.\n\nWhen a feature is duplicated, defines how the value of attributes\nfollowing the domain are computed.\n\n.. versionadded:: 3.38\n\n" + '* ``DefaultValue``: ' + Qgis.FieldDomainDuplicatePolicy.DefaultValue.__doc__ + '\n' + '* ``Duplicate``: ' + Qgis.FieldDomainDuplicatePolicy.Duplicate.__doc__ + '\n' + '* ``UnsetField``: ' + Qgis.FieldDomainDuplicatePolicy.UnsetField.__doc__ +Qgis.FieldDuplicatePolicy.DefaultValue.__doc__ = "Use default field value" +Qgis.FieldDuplicatePolicy.Duplicate.__doc__ = "Duplicate original value" +Qgis.FieldDuplicatePolicy.UnsetField.__doc__ = "Clears the field value so that the data provider backend will populate using any backend triggers or similar logic (since QGIS 3.30)" +Qgis.FieldDuplicatePolicy.__doc__ = "Duplicate policy for fields.\n\nWhen a feature is duplicated, defines how the value of attributes are computed.\n\n.. versionadded:: 3.38\n\n" + '* ``DefaultValue``: ' + Qgis.FieldDuplicatePolicy.DefaultValue.__doc__ + '\n' + '* ``Duplicate``: ' + Qgis.FieldDuplicatePolicy.Duplicate.__doc__ + '\n' + '* ``UnsetField``: ' + Qgis.FieldDuplicatePolicy.UnsetField.__doc__ # -- -Qgis.FieldDomainDuplicatePolicy.baseClass = Qgis +Qgis.FieldDuplicatePolicy.baseClass = Qgis # monkey patching scoped based enum Qgis.FieldDomainType.Coded.__doc__ = "Coded field domain" Qgis.FieldDomainType.Range.__doc__ = "Numeric range field domain (min/max)" diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index 1376e9118e233..82e79b2a67699 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -1826,7 +1826,7 @@ The development version GeometryWeighted, }; - enum class FieldDomainDuplicatePolicy /BaseType=IntEnum/ + enum class FieldDuplicatePolicy /BaseType=IntEnum/ { DefaultValue, Duplicate, diff --git a/python/PyQt6/core/auto_generated/qgsfield.sip.in b/python/PyQt6/core/auto_generated/qgsfield.sip.in index 65130f8ccf9be..0193040936b60 100644 --- a/python/PyQt6/core/auto_generated/qgsfield.sip.in +++ b/python/PyQt6/core/auto_generated/qgsfield.sip.in @@ -468,7 +468,7 @@ be handled during a split operation. .. versionadded:: 3.30 %End - Qgis::FieldDomainDuplicatePolicy duplicatePolicy() const; + Qgis::FieldDuplicatePolicy duplicatePolicy() const; %Docstring Returns the field's duplicate policy, which indicates how field values should be handled during a duplicate operation. @@ -478,7 +478,7 @@ be handled during a duplicate operation. .. versionadded:: 3.38 %End - void setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy policy ); + void setDuplicatePolicy( Qgis::FieldDuplicatePolicy policy ); %Docstring Sets the field's duplicate ``policy``, which indicates how field values should be handled during a duplicate operation. diff --git a/python/PyQt6/core/auto_generated/vector/qgsvectorlayer.sip.in b/python/PyQt6/core/auto_generated/vector/qgsvectorlayer.sip.in index bb1bd4e01ff5e..b54fb80e6f104 100644 --- a/python/PyQt6/core/auto_generated/vector/qgsvectorlayer.sip.in +++ b/python/PyQt6/core/auto_generated/vector/qgsvectorlayer.sip.in @@ -1935,7 +1935,7 @@ Sets a split ``policy`` for the field with the specified index. } %End - void setFieldDuplicatePolicy( int index, Qgis::FieldDomainDuplicatePolicy policy ); + void setFieldDuplicatePolicy( int index, Qgis::FieldDuplicatePolicy policy ); %Docstring Sets a duplicate ``policy`` for the field with the specified index. diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index f885d9a5d8224..15f42e5c2d9a1 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -3160,12 +3160,12 @@ # -- Qgis.FieldDomainMergePolicy.baseClass = Qgis # monkey patching scoped based enum -Qgis.FieldDomainDuplicatePolicy.DefaultValue.__doc__ = "Use default field value" -Qgis.FieldDomainDuplicatePolicy.Duplicate.__doc__ = "Duplicate original value" -Qgis.FieldDomainDuplicatePolicy.UnsetField.__doc__ = "Clears the field value so that the data provider backend will populate using any backend triggers or similar logic (since QGIS 3.30)" -Qgis.FieldDomainDuplicatePolicy.__doc__ = "Duplicate policy for fields.\n\nWhen a feature is duplicated, defines how the value of attributes\nfollowing the domain are computed.\n\n.. versionadded:: 3.38\n\n" + '* ``DefaultValue``: ' + Qgis.FieldDomainDuplicatePolicy.DefaultValue.__doc__ + '\n' + '* ``Duplicate``: ' + Qgis.FieldDomainDuplicatePolicy.Duplicate.__doc__ + '\n' + '* ``UnsetField``: ' + Qgis.FieldDomainDuplicatePolicy.UnsetField.__doc__ +Qgis.FieldDuplicatePolicy.DefaultValue.__doc__ = "Use default field value" +Qgis.FieldDuplicatePolicy.Duplicate.__doc__ = "Duplicate original value" +Qgis.FieldDuplicatePolicy.UnsetField.__doc__ = "Clears the field value so that the data provider backend will populate using any backend triggers or similar logic (since QGIS 3.30)" +Qgis.FieldDuplicatePolicy.__doc__ = "Duplicate policy for fields.\n\nWhen a feature is duplicated, defines how the value of attributes are computed.\n\n.. versionadded:: 3.38\n\n" + '* ``DefaultValue``: ' + Qgis.FieldDuplicatePolicy.DefaultValue.__doc__ + '\n' + '* ``Duplicate``: ' + Qgis.FieldDuplicatePolicy.Duplicate.__doc__ + '\n' + '* ``UnsetField``: ' + Qgis.FieldDuplicatePolicy.UnsetField.__doc__ # -- -Qgis.FieldDomainDuplicatePolicy.baseClass = Qgis +Qgis.FieldDuplicatePolicy.baseClass = Qgis # monkey patching scoped based enum Qgis.FieldDomainType.Coded.__doc__ = "Coded field domain" Qgis.FieldDomainType.Range.__doc__ = "Numeric range field domain (min/max)" diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index 0a65844c0e3ca..b1493a20cbdd8 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -1826,7 +1826,7 @@ The development version GeometryWeighted, }; - enum class FieldDomainDuplicatePolicy + enum class FieldDuplicatePolicy { DefaultValue, Duplicate, diff --git a/python/core/auto_generated/qgsfield.sip.in b/python/core/auto_generated/qgsfield.sip.in index 65130f8ccf9be..0193040936b60 100644 --- a/python/core/auto_generated/qgsfield.sip.in +++ b/python/core/auto_generated/qgsfield.sip.in @@ -468,7 +468,7 @@ be handled during a split operation. .. versionadded:: 3.30 %End - Qgis::FieldDomainDuplicatePolicy duplicatePolicy() const; + Qgis::FieldDuplicatePolicy duplicatePolicy() const; %Docstring Returns the field's duplicate policy, which indicates how field values should be handled during a duplicate operation. @@ -478,7 +478,7 @@ be handled during a duplicate operation. .. versionadded:: 3.38 %End - void setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy policy ); + void setDuplicatePolicy( Qgis::FieldDuplicatePolicy policy ); %Docstring Sets the field's duplicate ``policy``, which indicates how field values should be handled during a duplicate operation. diff --git a/python/core/auto_generated/vector/qgsvectorlayer.sip.in b/python/core/auto_generated/vector/qgsvectorlayer.sip.in index bb1bd4e01ff5e..b54fb80e6f104 100644 --- a/python/core/auto_generated/vector/qgsvectorlayer.sip.in +++ b/python/core/auto_generated/vector/qgsvectorlayer.sip.in @@ -1935,7 +1935,7 @@ Sets a split ``policy`` for the field with the specified index. } %End - void setFieldDuplicatePolicy( int index, Qgis::FieldDomainDuplicatePolicy policy ); + void setFieldDuplicatePolicy( int index, Qgis::FieldDuplicatePolicy policy ); %Docstring Sets a duplicate ``policy`` for the field with the specified index. diff --git a/src/core/qgis.h b/src/core/qgis.h index 610718daf04aa..475c0baad10c9 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -3221,18 +3221,17 @@ class CORE_EXPORT Qgis /** * Duplicate policy for fields. * - * When a feature is duplicated, defines how the value of attributes - * following the domain are computed. + * When a feature is duplicated, defines how the value of attributes are computed. * * \since QGIS 3.38 */ - enum class FieldDomainDuplicatePolicy : int + enum class FieldDuplicatePolicy : int { DefaultValue, //!< Use default field value Duplicate, //!< Duplicate original value UnsetField, //!< Clears the field value so that the data provider backend will populate using any backend triggers or similar logic (since QGIS 3.30) }; - Q_ENUM( FieldDomainDuplicatePolicy ) + Q_ENUM( FieldDuplicatePolicy ) /** * Types of field domain diff --git a/src/core/qgsfield.cpp b/src/core/qgsfield.cpp index 1c3cc54e76ebe..14107b82885f2 100644 --- a/src/core/qgsfield.cpp +++ b/src/core/qgsfield.cpp @@ -741,12 +741,12 @@ void QgsField::setSplitPolicy( Qgis::FieldDomainSplitPolicy policy ) d->splitPolicy = policy; } -Qgis::FieldDomainDuplicatePolicy QgsField::duplicatePolicy() const +Qgis::FieldDuplicatePolicy QgsField::duplicatePolicy() const { return d->duplicatePolicy; } -void QgsField::setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy policy ) +void QgsField::setDuplicatePolicy( Qgis::FieldDuplicatePolicy policy ) { d->duplicatePolicy = policy; } @@ -823,7 +823,7 @@ QDataStream &operator>>( QDataStream &in, QgsField &field ) field.setAlias( alias ); field.setDefaultValueDefinition( QgsDefaultValue( defaultValueExpression, applyOnUpdate ) ); field.setSplitPolicy( static_cast< Qgis::FieldDomainSplitPolicy >( splitPolicy ) ); - field.setDuplicatePolicy( static_cast< Qgis::FieldDomainDuplicatePolicy >( duplicatePolicy ) ); + field.setDuplicatePolicy( static_cast< Qgis::FieldDuplicatePolicy >( duplicatePolicy ) ); QgsFieldConstraints fieldConstraints; if ( constraints & QgsFieldConstraints::ConstraintNotNull ) { diff --git a/src/core/qgsfield.h b/src/core/qgsfield.h index 03c44be9d57e6..0c67ca03cfbbd 100644 --- a/src/core/qgsfield.h +++ b/src/core/qgsfield.h @@ -502,7 +502,7 @@ class CORE_EXPORT QgsField * * \since QGIS 3.38 */ - Qgis::FieldDomainDuplicatePolicy duplicatePolicy() const; + Qgis::FieldDuplicatePolicy duplicatePolicy() const; /** * Sets the field's duplicate \a policy, which indicates how field values should @@ -512,7 +512,7 @@ class CORE_EXPORT QgsField * * \since QGIS 3.38 */ - void setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy policy ); + void setDuplicatePolicy( Qgis::FieldDuplicatePolicy policy ); #ifdef SIP_RUN SIP_PYOBJECT __repr__(); diff --git a/src/core/qgsfield_p.h b/src/core/qgsfield_p.h index beab53710a6f1..3889bbef810ff 100644 --- a/src/core/qgsfield_p.h +++ b/src/core/qgsfield_p.h @@ -147,7 +147,7 @@ class QgsFieldPrivate : public QSharedData Qgis::FieldDomainSplitPolicy splitPolicy = Qgis::FieldDomainSplitPolicy::Duplicate; //! Duplicate policy - Qgis::FieldDomainDuplicatePolicy duplicatePolicy = Qgis::FieldDomainDuplicatePolicy::Duplicate; + Qgis::FieldDuplicatePolicy duplicatePolicy = Qgis::FieldDuplicatePolicy::Duplicate; //! Read-only bool isReadOnly = false; diff --git a/src/core/vector/qgsvectorlayer.cpp b/src/core/vector/qgsvectorlayer.cpp index 722b6aebf699c..308bea7a1d690 100644 --- a/src/core/vector/qgsvectorlayer.cpp +++ b/src/core/vector/qgsvectorlayer.cpp @@ -2587,7 +2587,7 @@ bool QgsVectorLayer::readSymbology( const QDomNode &layerNode, QString &errorMes { const QDomElement duplicatePolicyElem = duplicatePolicyNodeList.at( i ).toElement(); const QString field = duplicatePolicyElem.attribute( QStringLiteral( "field" ) ); - const Qgis::FieldDomainDuplicatePolicy policy = qgsEnumKeyToValue( duplicatePolicyElem.attribute( QStringLiteral( "policy" ) ), Qgis::FieldDomainDuplicatePolicy::Duplicate ); + const Qgis::FieldDuplicatePolicy policy = qgsEnumKeyToValue( duplicatePolicyElem.attribute( QStringLiteral( "policy" ) ), Qgis::FieldDuplicatePolicy::Duplicate ); mAttributeDuplicatePolicy.insert( field, policy ); } } @@ -3620,7 +3620,7 @@ void QgsVectorLayer::setFieldSplitPolicy( int index, Qgis::FieldDomainSplitPolic emit layerModified(); // TODO[MD]: should have a different signal? } -void QgsVectorLayer::setFieldDuplicatePolicy( int index, Qgis::FieldDomainDuplicatePolicy policy ) +void QgsVectorLayer::setFieldDuplicatePolicy( int index, Qgis::FieldDuplicatePolicy policy ) { QGIS_PROTECT_QOBJECT_THREAD_ACCESS diff --git a/src/core/vector/qgsvectorlayer.h b/src/core/vector/qgsvectorlayer.h index 7f9fe77342c2d..07a184243aae6 100644 --- a/src/core/vector/qgsvectorlayer.h +++ b/src/core/vector/qgsvectorlayer.h @@ -1847,7 +1847,7 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte * * \since QGIS 3.38 */ - void setFieldDuplicatePolicy( int index, Qgis::FieldDomainDuplicatePolicy policy ); + void setFieldDuplicatePolicy( int index, Qgis::FieldDuplicatePolicy policy ); #else /** @@ -1876,7 +1876,7 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte * \throws KeyError if no field with the specified index exists * \since QGIS 3.38 */ - void setFieldDuplicatePolicy( int index, Qgis::FieldDomainDuplicatePolicy policy ); + void setFieldDuplicatePolicy( int index, Qgis::FieldDuplicatePolicy policy ); % MethodCode if ( a0 < 0 || a0 >= sipCpp->fields().count() ) @@ -2896,7 +2896,7 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte QMap< QString, Qgis::FieldDomainSplitPolicy > mAttributeSplitPolicy; //! Map that stores the duplicate policy for attributes - QMap< QString, Qgis::FieldDomainDuplicatePolicy > mAttributeDuplicatePolicy; + QMap< QString, Qgis::FieldDuplicatePolicy > mAttributeDuplicatePolicy; //! An internal structure to keep track of fields that have a defaultValueOnUpdate QSet mDefaultValueOnUpdateFields; diff --git a/src/core/vector/qgsvectorlayerutils.cpp b/src/core/vector/qgsvectorlayerutils.cpp index af84e28cbdef2..6ea707ab27563 100644 --- a/src/core/vector/qgsvectorlayerutils.cpp +++ b/src/core/vector/qgsvectorlayerutils.cpp @@ -652,15 +652,15 @@ QgsFeature QgsVectorLayerUtils::duplicateFeature( QgsVectorLayer *layer, const Q const QgsField field = layer->fields().at( fieldIdx ); switch ( field.duplicatePolicy() ) { - case Qgis::FieldDomainDuplicatePolicy::DefaultValue: + case Qgis::FieldDuplicatePolicy::DefaultValue: //do nothing - default values ​​are determined break; - case Qgis::FieldDomainDuplicatePolicy::Duplicate: + case Qgis::FieldDuplicatePolicy::Duplicate: attributeMap.insert( fieldIdx, feature.attribute( fieldIdx ) ); break; - case Qgis::FieldDomainDuplicatePolicy::UnsetField: + case Qgis::FieldDuplicatePolicy::UnsetField: attributeMap.insert( fieldIdx, QgsUnsetAttributeValue() ); break; } diff --git a/src/gui/attributeformconfig/qgsattributetypedialog.cpp b/src/gui/attributeformconfig/qgsattributetypedialog.cpp index 0e717e15a861a..71135f4e904aa 100644 --- a/src/gui/attributeformconfig/qgsattributetypedialog.cpp +++ b/src/gui/attributeformconfig/qgsattributetypedialog.cpp @@ -125,9 +125,9 @@ QgsAttributeTypeDialog::QgsAttributeTypeDialog( QgsVectorLayer *vl, int fieldIdx connect( mSplitPolicyComboBox, qOverload( &QComboBox::currentIndexChanged ), this, &QgsAttributeTypeDialog::updateSplitPolicyLabel ); updateSplitPolicyLabel(); - mDuplicatePolicyComboBox->addItem( tr( "Duplicate Value" ), QVariant::fromValue( Qgis::FieldDomainDuplicatePolicy::Duplicate ) ); - mDuplicatePolicyComboBox->addItem( tr( "Use Default Value" ), QVariant::fromValue( Qgis::FieldDomainDuplicatePolicy::DefaultValue ) ); - mDuplicatePolicyComboBox->addItem( tr( "Remove Value" ), QVariant::fromValue( Qgis::FieldDomainDuplicatePolicy::UnsetField ) ); + mDuplicatePolicyComboBox->addItem( tr( "Duplicate Value" ), QVariant::fromValue( Qgis::FieldDuplicatePolicy::Duplicate ) ); + mDuplicatePolicyComboBox->addItem( tr( "Use Default Value" ), QVariant::fromValue( Qgis::FieldDuplicatePolicy::DefaultValue ) ); + mDuplicatePolicyComboBox->addItem( tr( "Remove Value" ), QVariant::fromValue( Qgis::FieldDuplicatePolicy::UnsetField ) ); connect( mDuplicatePolicyComboBox, qOverload( &QComboBox::currentIndexChanged ), this, &QgsAttributeTypeDialog::updateDuplicatePolicyLabel ); updateDuplicatePolicyLabel(); } @@ -387,12 +387,12 @@ void QgsAttributeTypeDialog::setSplitPolicy( Qgis::FieldDomainSplitPolicy policy updateSplitPolicyLabel(); } -Qgis::FieldDomainDuplicatePolicy QgsAttributeTypeDialog::duplicatePolicy() const +Qgis::FieldDuplicatePolicy QgsAttributeTypeDialog::duplicatePolicy() const { - return mDuplicatePolicyComboBox->currentData().value< Qgis::FieldDomainDuplicatePolicy >(); + return mDuplicatePolicyComboBox->currentData().value< Qgis::FieldDuplicatePolicy >(); } -void QgsAttributeTypeDialog::setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy policy ) +void QgsAttributeTypeDialog::setDuplicatePolicy( Qgis::FieldDuplicatePolicy policy ) { mDuplicatePolicyComboBox->setCurrentIndex( mDuplicatePolicyComboBox->findData( QVariant::fromValue( policy ) ) ); updateSplitPolicyLabel(); @@ -518,17 +518,17 @@ void QgsAttributeTypeDialog::updateSplitPolicyLabel() void QgsAttributeTypeDialog::updateDuplicatePolicyLabel() { QString helperText; - switch ( mDuplicatePolicyComboBox->currentData().value< Qgis::FieldDomainDuplicatePolicy >() ) + switch ( mDuplicatePolicyComboBox->currentData().value< Qgis::FieldDuplicatePolicy >() ) { - case Qgis::FieldDomainDuplicatePolicy::DefaultValue: + case Qgis::FieldDuplicatePolicy::DefaultValue: helperText = tr( "Resets the field by recalculating its default value." ); break; - case Qgis::FieldDomainDuplicatePolicy::Duplicate: + case Qgis::FieldDuplicatePolicy::Duplicate: helperText = tr( "Copies the current field value without change." ); break; - case Qgis::FieldDomainDuplicatePolicy::UnsetField: + case Qgis::FieldDuplicatePolicy::UnsetField: helperText = tr( "Clears the field to an unset state." ); break; } diff --git a/src/gui/attributeformconfig/qgsattributetypedialog.h b/src/gui/attributeformconfig/qgsattributetypedialog.h index 8b9eb28c2eb34..4f65c771679c9 100644 --- a/src/gui/attributeformconfig/qgsattributetypedialog.h +++ b/src/gui/attributeformconfig/qgsattributetypedialog.h @@ -252,7 +252,7 @@ class GUI_EXPORT QgsAttributeTypeDialog: public QWidget, private Ui::QgsAttribut * * \since QGIS 3.38 */ - Qgis::FieldDomainDuplicatePolicy duplicatePolicy() const; + Qgis::FieldDuplicatePolicy duplicatePolicy() const; /** * Sets the field's duplicate policy. @@ -261,7 +261,7 @@ class GUI_EXPORT QgsAttributeTypeDialog: public QWidget, private Ui::QgsAttribut * * \since QGIS 3.38 */ - void setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy policy ); + void setDuplicatePolicy( Qgis::FieldDuplicatePolicy policy ); private slots: diff --git a/src/gui/vector/qgsattributesformproperties.h b/src/gui/vector/qgsattributesformproperties.h index 505aca6f120fa..4eef017a99050 100644 --- a/src/gui/vector/qgsattributesformproperties.h +++ b/src/gui/vector/qgsattributesformproperties.h @@ -338,7 +338,7 @@ class GUI_EXPORT QgsAttributesFormProperties : public QWidget, public QgsExpress QgsPropertyCollection mDataDefinedProperties; QString mComment; Qgis::FieldDomainSplitPolicy mSplitPolicy = Qgis::FieldDomainSplitPolicy::Duplicate; - Qgis::FieldDomainDuplicatePolicy mDuplicatePolicy = Qgis::FieldDomainDuplicatePolicy::Duplicate; + Qgis::FieldDuplicatePolicy mDuplicatePolicy = Qgis::FieldDuplicatePolicy::Duplicate; operator QVariant(); }; diff --git a/tests/src/core/testqgsfield.cpp b/tests/src/core/testqgsfield.cpp index 81ecc7f94574b..d596e7f30576b 100644 --- a/tests/src/core/testqgsfield.cpp +++ b/tests/src/core/testqgsfield.cpp @@ -103,7 +103,7 @@ void TestQgsField::copy() original.setConstraints( constraints ); original.setReadOnly( true ); original.setSplitPolicy( Qgis::FieldDomainSplitPolicy::GeometryRatio ); - original.setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy::UnsetField ); + original.setDuplicatePolicy( Qgis::FieldDuplicatePolicy::UnsetField ); original.setMetadata( {{ 1, QStringLiteral( "abc" )}, {2, 5 }} ); QVariantMap config; @@ -131,7 +131,7 @@ void TestQgsField::assignment() original.setConstraints( constraints ); original.setReadOnly( true ); original.setSplitPolicy( Qgis::FieldDomainSplitPolicy::GeometryRatio ); - original.setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy::UnsetField ); + original.setDuplicatePolicy( Qgis::FieldDuplicatePolicy::UnsetField ); original.setMetadata( {{ 1, QStringLiteral( "abc" )}, {2, 5 }} ); QgsField copy; copy = original; @@ -206,8 +206,8 @@ void TestQgsField::gettersSetters() field.setSplitPolicy( Qgis::FieldDomainSplitPolicy::GeometryRatio ); QCOMPARE( field.splitPolicy(), Qgis::FieldDomainSplitPolicy::GeometryRatio ); - field.setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy::UnsetField ); - QCOMPARE( field.duplicatePolicy(), Qgis::FieldDomainDuplicatePolicy::UnsetField ); + field.setDuplicatePolicy( Qgis::FieldDuplicatePolicy::UnsetField ); + QCOMPARE( field.duplicatePolicy(), Qgis::FieldDuplicatePolicy::UnsetField ); field.setMetadata( {{ static_cast< int >( Qgis::FieldMetadataProperty::GeometryCrs ), QStringLiteral( "abc" )}, {2, 5 }} ); QMap< int, QVariant> expected {{ static_cast< int >( Qgis::FieldMetadataProperty::GeometryCrs ), QStringLiteral( "abc" )}, {2, 5 }}; @@ -364,10 +364,10 @@ void TestQgsField::equality() field2.setSplitPolicy( Qgis::FieldDomainSplitPolicy::GeometryRatio ); QVERIFY( field1 == field2 ); - field1.setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy::UnsetField ); + field1.setDuplicatePolicy( Qgis::FieldDuplicatePolicy::UnsetField ); QVERIFY( !( field1 == field2 ) ); QVERIFY( field1 != field2 ); - field2.setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy::UnsetField ); + field2.setDuplicatePolicy( Qgis::FieldDuplicatePolicy::UnsetField ); QVERIFY( field1 == field2 ); field1.setMetadata( {{ static_cast< int >( Qgis::FieldMetadataProperty::GeometryCrs ), QStringLiteral( "abc" )}, {2, 5 }} ); @@ -934,7 +934,7 @@ void TestQgsField::dataStream() original.setAlias( QStringLiteral( "alias" ) ); original.setDefaultValueDefinition( QgsDefaultValue( QStringLiteral( "default" ) ) ); original.setSplitPolicy( Qgis::FieldDomainSplitPolicy::GeometryRatio ); - original.setDuplicatePolicy( Qgis::FieldDomainDuplicatePolicy::DefaultValue ); + original.setDuplicatePolicy( Qgis::FieldDuplicatePolicy::DefaultValue ); QgsFieldConstraints constraints; constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider ); constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginLayer ); diff --git a/tests/src/python/test_qgsvectorlayer.py b/tests/src/python/test_qgsvectorlayer.py index 2935554c8bfa3..7513d1bd1c1a0 100644 --- a/tests/src/python/test_qgsvectorlayer.py +++ b/tests/src/python/test_qgsvectorlayer.py @@ -4552,20 +4552,20 @@ def test_duplicate_policies(self): self.assertTrue(vl.isValid()) with self.assertRaises(KeyError): - vl.setFieldDuplicatePolicy(-1, Qgis.FieldDomainDuplicatePolicy.DefaultValue) + vl.setFieldDuplicatePolicy(-1, Qgis.FieldDuplicatePolicy.DefaultValue) with self.assertRaises(KeyError): - vl.setFieldDuplicatePolicy(4, Qgis.FieldDomainDuplicatePolicy.DefaultValue) + vl.setFieldDuplicatePolicy(4, Qgis.FieldDuplicatePolicy.DefaultValue) - vl.setFieldDuplicatePolicy(0, Qgis.FieldDomainDuplicatePolicy.DefaultValue) - vl.setFieldDuplicatePolicy(1, Qgis.FieldDomainDuplicatePolicy.Duplicate) - vl.setFieldDuplicatePolicy(2, Qgis.FieldDomainDuplicatePolicy.UnsetField) + vl.setFieldDuplicatePolicy(0, Qgis.FieldDuplicatePolicy.DefaultValue) + vl.setFieldDuplicatePolicy(1, Qgis.FieldDuplicatePolicy.Duplicate) + vl.setFieldDuplicatePolicy(2, Qgis.FieldDuplicatePolicy.UnsetField) self.assertEqual(vl.fields()[0].duplicatePolicy(), - Qgis.FieldDomainDuplicatePolicy.DefaultValue) + Qgis.FieldDuplicatePolicy.DefaultValue) self.assertEqual(vl.fields()[1].duplicatePolicy(), - Qgis.FieldDomainDuplicatePolicy.Duplicate) + Qgis.FieldDuplicatePolicy.Duplicate) self.assertEqual(vl.fields()[2].duplicatePolicy(), - Qgis.FieldDomainDuplicatePolicy.UnsetField) + Qgis.FieldDuplicatePolicy.UnsetField) p = QgsProject() p.addMapLayer(vl) @@ -4581,11 +4581,11 @@ def test_duplicate_policies(self): self.assertEqual(vl2.name(), vl.name()) self.assertEqual(vl2.fields()[0].duplicatePolicy(), - Qgis.FieldDomainDuplicatePolicy.DefaultValue) + Qgis.FieldDuplicatePolicy.DefaultValue) self.assertEqual(vl2.fields()[1].duplicatePolicy(), - Qgis.FieldDomainDuplicatePolicy.Duplicate) + Qgis.FieldDuplicatePolicy.Duplicate) self.assertEqual(vl2.fields()[2].duplicatePolicy(), - Qgis.FieldDomainDuplicatePolicy.UnsetField) + Qgis.FieldDuplicatePolicy.UnsetField) def test_selection_properties(self): vl = QgsVectorLayer( diff --git a/tests/src/python/test_qgsvectorlayerutils.py b/tests/src/python/test_qgsvectorlayerutils.py index c2fa6f2d3716d..a995a0da48aa3 100644 --- a/tests/src/python/test_qgsvectorlayerutils.py +++ b/tests/src/python/test_qgsvectorlayerutils.py @@ -494,9 +494,9 @@ def testDuplicateFeature(self): layer1.setDefaultValueDefinition(2, QgsDefaultValue("'Def Blabla L1'")) layer1.setDefaultValueDefinition(3, QgsDefaultValue("'Def Blabla L1'")) layer1.setDefaultValueDefinition(4, QgsDefaultValue("'Def Blabla L1'")) - layer1.setFieldDuplicatePolicy(2, Qgis.FieldDomainDuplicatePolicy.Duplicate) - layer1.setFieldDuplicatePolicy(3, Qgis.FieldDomainDuplicatePolicy.DefaultValue) - layer1.setFieldDuplicatePolicy(4, Qgis.FieldDomainDuplicatePolicy.UnsetField) + layer1.setFieldDuplicatePolicy(2, Qgis.FieldDuplicatePolicy.Duplicate) + layer1.setFieldDuplicatePolicy(3, Qgis.FieldDuplicatePolicy.DefaultValue) + layer1.setFieldDuplicatePolicy(4, Qgis.FieldDuplicatePolicy.UnsetField) # > check first layer (parent) self.assertTrue(layer1.isValid()) # - add second layer (child) @@ -508,9 +508,9 @@ def testDuplicateFeature(self): layer2.setDefaultValueDefinition(3, QgsDefaultValue("'Def Blabla L2'")) layer2.setDefaultValueDefinition(4, QgsDefaultValue("'Def Blabla L2'")) layer2.setDefaultValueDefinition(5, QgsDefaultValue("'Def Blabla L2'")) - layer2.setFieldDuplicatePolicy(3, Qgis.FieldDomainDuplicatePolicy.Duplicate) - layer2.setFieldDuplicatePolicy(4, Qgis.FieldDomainDuplicatePolicy.DefaultValue) - layer2.setFieldDuplicatePolicy(5, Qgis.FieldDomainDuplicatePolicy.UnsetField) + layer2.setFieldDuplicatePolicy(3, Qgis.FieldDuplicatePolicy.Duplicate) + layer2.setFieldDuplicatePolicy(4, Qgis.FieldDuplicatePolicy.DefaultValue) + layer2.setFieldDuplicatePolicy(5, Qgis.FieldDuplicatePolicy.UnsetField) # > check second layer (child) self.assertTrue(layer2.isValid()) # - add third layer (child) @@ -522,9 +522,9 @@ def testDuplicateFeature(self): layer3.setDefaultValueDefinition(3, QgsDefaultValue("'Def Blabla L3'")) layer3.setDefaultValueDefinition(4, QgsDefaultValue("'Def Blabla L3'")) layer3.setDefaultValueDefinition(5, QgsDefaultValue("'Def Blabla L3'")) - layer3.setFieldDuplicatePolicy(3, Qgis.FieldDomainDuplicatePolicy.Duplicate) - layer3.setFieldDuplicatePolicy(4, Qgis.FieldDomainDuplicatePolicy.DefaultValue) - layer3.setFieldDuplicatePolicy(5, Qgis.FieldDomainDuplicatePolicy.UnsetField) + layer3.setFieldDuplicatePolicy(3, Qgis.FieldDuplicatePolicy.Duplicate) + layer3.setFieldDuplicatePolicy(4, Qgis.FieldDuplicatePolicy.DefaultValue) + layer3.setFieldDuplicatePolicy(5, Qgis.FieldDuplicatePolicy.UnsetField) # > check third layer (child) self.assertTrue(layer3.isValid()) # - add layers From 975a5a503710ee2a641b33261e8b3c721be10d68 Mon Sep 17 00:00:00 2001 From: Marco Hugentobler Date: Tue, 7 May 2024 16:16:41 +0200 Subject: [PATCH 031/102] Add helper function to get layer name and use identifier only for gpkg --- src/core/providers/gdal/qgsgdalprovider.cpp | 43 +++++++++++---------- src/core/providers/gdal/qgsgdalprovider.h | 4 ++ 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/core/providers/gdal/qgsgdalprovider.cpp b/src/core/providers/gdal/qgsgdalprovider.cpp index 64cdc46726fdc..b0c74b6778d33 100644 --- a/src/core/providers/gdal/qgsgdalprovider.cpp +++ b/src/core/providers/gdal/qgsgdalprovider.cpp @@ -37,7 +37,6 @@ #include "qgsrasterpyramid.h" #include "qgspointxy.h" #include "qgssettings.h" -#include "qgsogrutils.h" #include "qgsruntimeprofiler.h" #include "qgsprovidersublayerdetails.h" #include "qgsproviderutils.h" @@ -4620,12 +4619,8 @@ int QgsGdalProviderMetadata::listStyles( const QString &uri, QStringList &ids, Q errCause = QObject::tr( "Cannot open %1." ).arg( uri ); return -1; } - QVariantMap uriParts = QgsGdalProviderBase::decodeGdalUri( uri ); - QString layerName = uriParts["layerName"].toString(); - if ( layerName.isEmpty() ) - { - layerName = GDALGetMetadataItem( ds.get(), "IDENTIFIER", "" ); - } + + QString layerName = getLayerNameForStyle( uri, ds ); return QgsOgrUtils::listStyles( ds.get(), layerName, "", ids, names, descriptions, errCause ); } @@ -4638,12 +4633,8 @@ bool QgsGdalProviderMetadata::styleExists( const QString &uri, const QString &st errCause = QObject::tr( "Cannot open %1." ).arg( uri ); return false; } - QVariantMap uriParts = QgsGdalProviderBase::decodeGdalUri( uri ); - QString layerName = uriParts["layerName"] .toString(); - if ( layerName.isEmpty() ) - { - layerName = GDALGetMetadataItem( ds.get(), "IDENTIFIER", "" ); - } + + QString layerName = getLayerNameForStyle( uri, ds ); return QgsOgrUtils::styleExists( ds.get(), layerName, "", styleId, errCause ); } @@ -4682,12 +4673,8 @@ bool QgsGdalProviderMetadata::saveStyle( const QString &uri, const QString &qmlS errCause = QObject::tr( "Cannot open %1." ).arg( uri ); return false; } - QVariantMap uriParts = QgsGdalProviderBase::decodeGdalUri( uri ); - QString layerName = uriParts["layerName"].toString(); - if ( layerName.isEmpty() ) - { - layerName = GDALGetMetadataItem( ds.get(), "IDENTIFIER", "" ); - } + + QString layerName = getLayerNameForStyle( uri, ds ); return QgsOgrUtils::saveStyle( ds.get(), layerName, "", qmlStyle, sldStyle, styleName, styleDescription, uiFileContent, useAsDefault, errCause ); } @@ -4706,13 +4693,27 @@ QString QgsGdalProviderMetadata::loadStoredStyle( const QString &uri, QString &s errCause = QObject::tr( "Cannot open %1." ).arg( uri ); return QString(); } + + QString layerName = getLayerNameForStyle( uri, ds ); + return QgsOgrUtils::loadStoredStyle( ds.get(), layerName, "", styleName, errCause ); +} + +QString QgsGdalProviderMetadata::getLayerNameForStyle( const QString &uri, gdal::dataset_unique_ptr &ds ) +{ QVariantMap uriParts = QgsGdalProviderBase::decodeGdalUri( uri ); QString layerName = uriParts["layerName"].toString(); if ( layerName.isEmpty() ) { - layerName = GDALGetMetadataItem( ds.get(), "IDENTIFIER", "" ); + GDALDriverH driver = GDALGetDatasetDriver( ds.get() ); + if ( driver ) + { + if ( GDALGetDriverShortName( driver ) == QStringLiteral( "GPKG" ) ) + { + layerName = GDALGetMetadataItem( ds.get(), "IDENTIFIER", "" ); + } + } } - return QgsOgrUtils::loadStoredStyle( ds.get(), layerName, "", styleName, errCause ); + return layerName; } QgsGdalProviderMetadata::QgsGdalProviderMetadata(): diff --git a/src/core/providers/gdal/qgsgdalprovider.h b/src/core/providers/gdal/qgsgdalprovider.h index 52a743b42ea57..5f828ab7c07b8 100644 --- a/src/core/providers/gdal/qgsgdalprovider.h +++ b/src/core/providers/gdal/qgsgdalprovider.h @@ -24,6 +24,7 @@ #include "qgsgdalproviderbase.h" #include "qgsrectangle.h" #include "qgscolorrampshader.h" +#include "qgsogrutils.h" #include "qgsrasterbandstats.h" #include "qgsprovidermetadata.h" #include "qgsprovidersublayerdetails.h" @@ -416,6 +417,9 @@ class QgsGdalProviderMetadata final: public QgsProviderMetadata const QString &uiFileContent, bool useAsDefault, QString &errCause ) override; QString loadStyle( const QString &uri, QString &errCause ) override; QString loadStoredStyle( const QString &uri, QString &styleName, QString &errCause ) override; + private: + //! Get layer name from gdal url + static QString getLayerNameForStyle( const QString &uri, gdal::dataset_unique_ptr &ds ); }; ///@endcond From b3d184821205e57cae28120172e4f39f2a04f2dd Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Thu, 2 May 2024 19:26:22 +0200 Subject: [PATCH 032/102] Accept 2d box extent when estimating metadata Fixes GH-56541 --- src/providers/postgres/qgspostgresprovider.cpp | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/providers/postgres/qgspostgresprovider.cpp b/src/providers/postgres/qgspostgresprovider.cpp index e48859dcffa08..9d716742887db 100644 --- a/src/providers/postgres/qgspostgresprovider.cpp +++ b/src/providers/postgres/qgspostgresprovider.cpp @@ -3975,8 +3975,8 @@ QgsBox3D QgsPostgresProvider::extent3D() const { QgsDebugMsgLevel( QStringLiteral( "Got extents (%1) using: %2" ).arg( ext ).arg( sql ), 2 ); - const thread_local QRegularExpression rx( "\\((.+) (.+) (.+),(.+) (.+) (.+)\\)" ); - const QRegularExpressionMatch match = rx.match( ext ); + const thread_local QRegularExpression rx3d( "\\((.+) (.+) (.+),(.+) (.+) (.+)\\)" ); + const QRegularExpressionMatch match = rx3d.match( ext ); if ( match.hasMatch() ) { mLayerExtent.setXMinimum( match.captured( 1 ).toDouble() ); @@ -3994,7 +3994,19 @@ QgsBox3D QgsPostgresProvider::extent3D() const } else { - QgsMessageLog::logMessage( tr( "result of extents query invalid: %1" ).arg( ext ), tr( "PostGIS" ) ); + const thread_local QRegularExpression rx2d( "\\((.+) (.+),(.+) (.+)\\)" ); + const QRegularExpressionMatch match = rx2d.match( ext ); + if ( match.hasMatch() ) + { + mLayerExtent.setXMinimum( match.captured( 1 ).toDouble() ); + mLayerExtent.setYMinimum( match.captured( 2 ).toDouble() ); + mLayerExtent.setXMaximum( match.captured( 3 ).toDouble() ); + mLayerExtent.setYMaximum( match.captured( 4 ).toDouble() ); + } + else + { + QgsMessageLog::logMessage( tr( "result of extents query invalid: %1" ).arg( ext ), tr( "PostGIS" ) ); + } } } From 6301adfe9462a3e5f8dd2672a7abd7b05a599d1f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 8 May 2024 10:17:44 +1000 Subject: [PATCH 033/102] Don't write layer vertical crs if its not set Avoids some unwanted extra noise in the xml --- src/core/qgsmaplayer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/qgsmaplayer.cpp b/src/core/qgsmaplayer.cpp index dd969da983c95..799fc4dbe5592 100644 --- a/src/core/qgsmaplayer.cpp +++ b/src/core/qgsmaplayer.cpp @@ -761,6 +761,7 @@ bool QgsMapLayer::writeLayerXml( QDomElement &layerElement, QDomDocument &docume layerElement.appendChild( layerId ); + if ( mVerticalCrs.isValid() ) { QDomElement verticalSrsNode = document.createElement( QStringLiteral( "verticalCrs" ) ); mVerticalCrs.writeXml( verticalSrsNode, document ); From b7f0770633397882b88a5ede0059f525f52cec26 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 7 May 2024 12:16:42 +1000 Subject: [PATCH 034/102] Introduce QgsCodeEditorWidget This widget wraps an existing QgsCodeEditor object in a widget which provides additional standard functionality, currently a line-for-line port of the Python console script editor search tools. The caller must create an unparented QgsCodeEditor object (or a subclass of QgsCodeEditor) first, and then construct a QgsCodeEditorWidget passing this object to the constructor. Ideally, this functionality would be added to the base QgsCodeEditor class itself. But this is NOT possible without considerable API breakage, as QgsCodeEditor currently inherits the QsciScintilla widget. We cannot change QgsCodeEditor to inherit a generic QWidget container containing a QsciScintilla widget + other widgets in a layout without breaking API. I've added a cleanup note for QGIS 4.0 here. --- .../codeeditors/qgscodeeditor.sip.in | 67 +++++- .../codeeditors/qgscodeeditor.sip.in | 67 +++++- src/gui/codeeditors/qgscodeeditor.cpp | 207 ++++++++++++++++++ src/gui/codeeditors/qgscodeeditor.h | 91 +++++++- 4 files changed, 428 insertions(+), 4 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in index f8c7c072b5206..8980e4169b3b1 100644 --- a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in +++ b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in @@ -62,7 +62,6 @@ whenever the public :py:func:`~QgsCodeInterpreter.exec` method is called. - class QgsCodeEditor : QsciScintilla { %Docstring(signature="appended") @@ -604,6 +603,72 @@ QFlags operator|(QgsCodeEditor::Flag f1, QFlags operator|(QgsCodeEditor::Flag f1, QFlags #include @@ -35,6 +36,10 @@ #include #include #include +#include +#include +#include +#include QMap< QgsCodeEditorColorScheme::ColorRole, QString > QgsCodeEditor::sColorRoleToSettingsKey { @@ -1166,3 +1171,205 @@ int QgsCodeInterpreter::exec( const QString &command ) mState = execCommandImpl( command ); return mState; } + +// +// QgsCodeEditorWidget +// + +QgsCodeEditorWidget::QgsCodeEditorWidget( QgsCodeEditor *editor, QWidget *parent ) + : QgsPanelWidget( parent ) + , mEditor( editor ) +{ + Q_ASSERT( mEditor ); + + QVBoxLayout *vl = new QVBoxLayout(); + vl->setContentsMargins( 0, 0, 0, 0 ); + vl->setSpacing( 0 ); + vl->addWidget( editor, 1 ); + + mFindWidget = new QWidget(); + QHBoxLayout *layoutFind = new QHBoxLayout(); + layoutFind->setContentsMargins( 0, 2, 0, 0 ); + mLineEditFind = new QgsFilterLineEdit(); + mLineEditFind->setShowSearchIcon( true ); + mLineEditFind->setPlaceholderText( tr( "Enter text to find…" ) ); + layoutFind->addWidget( mLineEditFind, 1 ); + + mFindPrevButton = new QToolButton(); + mFindPrevButton->setEnabled( false ); + mFindPrevButton->setToolTip( tr( "Find Previous" ) ); + mFindPrevButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconSearchPrevEditorConsole.svg" ) ) ); + mFindPrevButton->setAutoRaise( true ); + layoutFind->addWidget( mFindPrevButton ); + + mFindNextButton = new QToolButton(); + mFindNextButton->setEnabled( false ); + mFindNextButton->setToolTip( tr( "Find Next" ) ); + mFindNextButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconSearchNextEditorConsole.svg" ) ) ); + mFindNextButton->setAutoRaise( true ); + layoutFind->addWidget( mFindNextButton ); + + mCaseSensitiveCheck = new QCheckBox( tr( "Case Sensitive" ) ); + layoutFind->addWidget( mCaseSensitiveCheck ); + + mWholeWordCheck = new QCheckBox( tr( "Whole Word" ) ); + layoutFind->addWidget( mWholeWordCheck ); + + mWrapAroundCheck = new QCheckBox( tr( "Wrap Around" ) ); + layoutFind->addWidget( mWrapAroundCheck ); + + connect( mLineEditFind, &QLineEdit::returnPressed, this, &QgsCodeEditorWidget::findNext ); + connect( mLineEditFind, &QLineEdit::textChanged, this, &QgsCodeEditorWidget::textFindChanged ); + connect( mFindNextButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findNext ); + connect( mFindPrevButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findPrevious ); + connect( mCaseSensitiveCheck, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateFind ); + connect( mWholeWordCheck, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateFind ); + connect( mWrapAroundCheck, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateFind ); + + QShortcut *findShortcut = new QShortcut( QKeySequence::StandardKey::Find, mEditor ); + findShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut ); + connect( findShortcut, &QShortcut::activated, this, [this] + { + showFind(); + mLineEditFind->setFocus(); + if ( mEditor->hasSelectedText() ) + { + mBlockSearching++; + mLineEditFind->setText( mEditor->selectedText().trimmed() ); + mBlockSearching--; + } + mLineEditFind->selectAll(); + } ); + + QShortcut *findNextShortcut = new QShortcut( QKeySequence::StandardKey::FindNext, this ); + findNextShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut ); + connect( findNextShortcut, &QShortcut::activated, this, &QgsCodeEditorWidget::findNext ); + + QShortcut *findPreviousShortcut = new QShortcut( QKeySequence::StandardKey::FindPrevious, this ); + findPreviousShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut ); + connect( findPreviousShortcut, &QShortcut::activated, this, &QgsCodeEditorWidget::findPrevious ); + + // escape on editor hides the find bar + QShortcut *closeFindShortcut = new QShortcut( Qt::Key::Key_Escape, this ); + closeFindShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut ); + connect( closeFindShortcut, &QShortcut::activated, this, [this] + { + hideFind(); + mEditor->setFocus(); + } ); + + mFindWidget->setLayout( layoutFind ); + vl->addWidget( mFindWidget ); + mFindWidget->hide(); + + setLayout( vl ); +} + +void QgsCodeEditorWidget::showFind() +{ + mFindWidget->show(); +} + +void QgsCodeEditorWidget::hideFind() +{ + mFindWidget->hide(); +} + +void QgsCodeEditorWidget::setFindVisible( bool visible ) +{ + if ( visible ) + showFind(); + else + hideFind(); +} + +void QgsCodeEditorWidget::findNext() +{ + findText( true, false ); +} + +void QgsCodeEditorWidget::findPrevious() +{ + findText( false, false ); +} + +void QgsCodeEditorWidget::textFindChanged( const QString &text ) +{ + if ( !text.isEmpty() ) + { + mFindNextButton->setEnabled( true ); + mFindPrevButton->setEnabled( true ); + updateFind(); + } + else + { + mLineEditFind->setStyleSheet( QString() ); + mFindNextButton->setEnabled( false ); + mFindPrevButton->setEnabled( false ); + } +} + +void QgsCodeEditorWidget::updateFind() +{ + if ( mBlockSearching ) + return; + + const QString searchString = mLineEditFind->text(); + if ( searchString.isEmpty() ) + return; + + findText( true, true, true ); +} + +void QgsCodeEditorWidget::findText( bool forward, bool findFirst, bool showNotFoundWarning ) +{ + const QString searchString = mLineEditFind->text(); + if ( searchString.isEmpty() ) + return; + + int lineFrom = 0; + int indexFrom = 0; + int lineTo = 0; + int indexTo = 0; + mEditor->getSelection( &lineFrom, &indexFrom, &lineTo, &indexTo ); + + int line = 0; + int index = 0; + if ( !findFirst ) + { + mEditor->getCursorPosition( &line, &index ); + } + if ( !forward ) + { + line = lineFrom; + index = indexFrom; + } + + const bool isRegEx = false; + const bool wrapAround = mWrapAroundCheck->isChecked(); + const bool isCaseSensitive = mCaseSensitiveCheck->isChecked(); + const bool isWholeWordOnly = mWholeWordCheck->isChecked(); + + const bool found = mEditor->findFirst( searchString, isRegEx, isCaseSensitive, isWholeWordOnly, wrapAround, forward, + line, index ); + + if ( !found ) + { + const QString styleError = QStringLiteral( "QLineEdit {background-color: #d65253; color: #ffffff;}" ); + mLineEditFind->setStyleSheet( styleError ); + + Q_UNUSED( showNotFoundWarning ) +#if 0 // TODO -- port this bit when messagebar is available + if ( showMessage ) + { + mMessageBar->pushMessage( QString(), tr( "\"%1\" was not found" ).arg( searchString ), + Qgis::MessageLevel::Info ); + } +#endif + } + else + { + mLineEditFind->setStyleSheet( QString() ); + } +} + diff --git a/src/gui/codeeditors/qgscodeeditor.h b/src/gui/codeeditors/qgscodeeditor.h index 548f3fdf4a297..4bbfd6631eb41 100644 --- a/src/gui/codeeditors/qgscodeeditor.h +++ b/src/gui/codeeditors/qgscodeeditor.h @@ -21,6 +21,7 @@ #include "qgscodeeditorcolorscheme.h" #include "qgis.h" #include "qgssettingstree.h" +#include "qgspanelwidget.h" // qscintilla includes #include @@ -29,6 +30,9 @@ #include +class QgsFilterLineEdit; +class QToolButton; +class QCheckBox; SIP_IF_MODULE( HAVE_QSCI_SIP ) @@ -81,8 +85,11 @@ class GUI_EXPORT QgsCodeInterpreter }; - -class QWidget; +// TODO QGIS 4.0 -- make QgsCodeEditor inherit QWidget only, +// with a separate getter for the QsciScintilla child widget. This +// would give us more flexibility to add functionality to the base +// QgsCodeEditor class, eg adding a message bar or other child widgets +// to the editor widget. /** * \ingroup gui @@ -654,6 +661,86 @@ class GUI_EXPORT QgsCodeEditor : public QsciScintilla Q_DECLARE_OPERATORS_FOR_FLAGS( QgsCodeEditor::Flags ) + +/** + * \ingroup gui + * \brief A widget which wraps a QgsCodeEditor in additional functionality. + * + * This widget wraps an existing QgsCodeEditor object in a widget which provides + * additional standard functionality, such as search/replace tools. The caller + * must create an unparented QgsCodeEditor object (or a subclass of QgsCodeEditor) + * first, and then construct a QgsCodeEditorWidget passing this object to the + * constructor. + * + * \note may not be available in Python bindings, depending on platform support + */ +class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsCodeEditorWidget, wrapping the specified \a editor widget. + * + * Ownership of \a editor will be transferred to this widget. + */ + QgsCodeEditorWidget( QgsCodeEditor *editor SIP_TRANSFER, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + + /** + * Returns the wrapped code editor. + */ + QgsCodeEditor *editor() { return mEditor; } + + public slots: + + /** + * Shows the find bar. + * + * \see hideFind() + * \see setFindVisible() + */ + void showFind(); + + /** + * Hides the find bar. + * + * \see showFind() + * \see setFindVisible() + */ + void hideFind(); + + /** + * Sets whether the find bar is \a visible. + * + * \see showFind() + * \see hideFind() + */ + void setFindVisible( bool visible ); + + private slots: + + void findNext(); + void findPrevious(); + void textFindChanged( const QString &text ); + void updateFind(); + + private: + + void findText( bool forward, bool findFirst, bool showNotFoundWarning = false ); + + QgsCodeEditor *mEditor = nullptr; + QWidget *mFindWidget = nullptr; + QgsFilterLineEdit *mLineEditFind = nullptr; + QToolButton *mFindPrevButton = nullptr; + QToolButton *mFindNextButton = nullptr; + QCheckBox *mCaseSensitiveCheck = nullptr; + QCheckBox *mWholeWordCheck = nullptr; + QCheckBox *mWrapAroundCheck = nullptr; + int mBlockSearching = 0; +}; + + // clazy:excludeall=qstring-allocations #endif From 4870b971ebaf306010a84b7286e5adf9c582f8a2 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 7 May 2024 12:23:22 +1000 Subject: [PATCH 035/102] Use QgsCodeEditorWidget in execute sql dialog Allows searching in the query via Ctrl+F --- src/gui/qgsqueryresultwidget.cpp | 7 +++++++ src/gui/qgsqueryresultwidget.h | 3 +++ src/ui/qgsqueryresultwidgetbase.ui | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/gui/qgsqueryresultwidget.cpp b/src/gui/qgsqueryresultwidget.cpp index 138e24c1dab4f..5f01fe3aafd8f 100644 --- a/src/gui/qgsqueryresultwidget.cpp +++ b/src/gui/qgsqueryresultwidget.cpp @@ -45,6 +45,13 @@ QgsQueryResultWidget::QgsQueryResultWidget( QWidget *parent, QgsAbstractDatabase mProgressBar->hide(); + mSqlEditor = new QgsCodeEditorSQL(); + mCodeEditorWidget = new QgsCodeEditorWidget( mSqlEditor ); + QVBoxLayout *vl = new QVBoxLayout(); + vl->setContentsMargins( 0, 0, 0, 0 ); + vl->addWidget( mCodeEditorWidget ); + mSqlEditorContainer->setLayout( vl ); + connect( mExecuteButton, &QPushButton::pressed, this, &QgsQueryResultWidget::executeQuery ); connect( mClearButton, &QPushButton::pressed, this, [ = ] { diff --git a/src/gui/qgsqueryresultwidget.h b/src/gui/qgsqueryresultwidget.h index 091121ed2acc1..f196cdc999093 100644 --- a/src/gui/qgsqueryresultwidget.h +++ b/src/gui/qgsqueryresultwidget.h @@ -216,6 +216,9 @@ class GUI_EXPORT QgsQueryResultWidget: public QWidget, private Ui::QgsQueryResul private: + QgsCodeEditorWidget *mCodeEditorWidget = nullptr; + QgsCodeEditorSQL *mSqlEditor = nullptr; + std::unique_ptr mConnection; std::unique_ptr mModel; std::unique_ptr mFeedback; diff --git a/src/ui/qgsqueryresultwidgetbase.ui b/src/ui/qgsqueryresultwidgetbase.ui index d7536c3140963..9a7c54c9a874d 100644 --- a/src/ui/qgsqueryresultwidgetbase.ui +++ b/src/ui/qgsqueryresultwidgetbase.ui @@ -18,7 +18,7 @@ - + From 347191e797a3c87a0cad4a613f17314711ea821c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 7 May 2024 12:38:55 +1000 Subject: [PATCH 036/102] Add since --- .../PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in | 2 ++ python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in | 2 ++ src/gui/codeeditors/qgscodeeditor.h | 2 ++ 3 files changed, 6 insertions(+) diff --git a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in index 8980e4169b3b1..2ffe8b4db221e 100644 --- a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in +++ b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in @@ -617,6 +617,8 @@ constructor. .. note:: may not be available in Python bindings, depending on platform support + +.. versionadded:: 3.38 %End %TypeHeaderCode diff --git a/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in b/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in index e2e03b0be16c5..87e6cfd45a3a5 100644 --- a/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in +++ b/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in @@ -617,6 +617,8 @@ constructor. .. note:: may not be available in Python bindings, depending on platform support + +.. versionadded:: 3.38 %End %TypeHeaderCode diff --git a/src/gui/codeeditors/qgscodeeditor.h b/src/gui/codeeditors/qgscodeeditor.h index 4bbfd6631eb41..49cbf2bb46bb3 100644 --- a/src/gui/codeeditors/qgscodeeditor.h +++ b/src/gui/codeeditors/qgscodeeditor.h @@ -673,6 +673,8 @@ Q_DECLARE_OPERATORS_FOR_FLAGS( QgsCodeEditor::Flags ) * constructor. * * \note may not be available in Python bindings, depending on platform support + * + * \since QGIS 3.38 */ class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget { From 5f4c9b3c1e0aee3e818e8fc6f74ab4e0dfae025e Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 8 May 2024 10:22:39 +1000 Subject: [PATCH 037/102] Make TODO less strong --- src/gui/codeeditors/qgscodeeditor.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/gui/codeeditors/qgscodeeditor.h b/src/gui/codeeditors/qgscodeeditor.h index 49cbf2bb46bb3..f5dde9269c135 100644 --- a/src/gui/codeeditors/qgscodeeditor.h +++ b/src/gui/codeeditors/qgscodeeditor.h @@ -85,11 +85,12 @@ class GUI_EXPORT QgsCodeInterpreter }; -// TODO QGIS 4.0 -- make QgsCodeEditor inherit QWidget only, +// TODO QGIS 4.0 -- Consider making QgsCodeEditor inherit QWidget only, // with a separate getter for the QsciScintilla child widget. This // would give us more flexibility to add functionality to the base // QgsCodeEditor class, eg adding a message bar or other child widgets -// to the editor widget. +// to the editor widget. For now this extra functionality lives in +// the QgsCodeEditorWidget wrapper widget. /** * \ingroup gui From 8654d57f168efc1b9d7161048f50c4c7de96ea68 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 8 May 2024 10:25:26 +1000 Subject: [PATCH 038/102] Rename methods for clarity --- .../codeeditors/qgscodeeditor.sip.in | 24 ++++++++-------- .../codeeditors/qgscodeeditor.sip.in | 24 ++++++++-------- src/gui/codeeditors/qgscodeeditor.cpp | 28 +++++++++---------- src/gui/codeeditors/qgscodeeditor.h | 28 +++++++++---------- 4 files changed, 52 insertions(+), 52 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in index 2ffe8b4db221e..ed6555a1d5714 100644 --- a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in +++ b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in @@ -640,31 +640,31 @@ Returns the wrapped code editor. public slots: - void showFind(); + void showSearchBar(); %Docstring -Shows the find bar. +Shows the search bar. -.. seealso:: :py:func:`hideFind` +.. seealso:: :py:func:`hideSearchBar` -.. seealso:: :py:func:`setFindVisible` +.. seealso:: :py:func:`setSearchBarVisible` %End - void hideFind(); + void hideSearchBar(); %Docstring -Hides the find bar. +Hides the search bar. -.. seealso:: :py:func:`showFind` +.. seealso:: :py:func:`showSearchBar` -.. seealso:: :py:func:`setFindVisible` +.. seealso:: :py:func:`setSearchBarVisible` %End - void setFindVisible( bool visible ); + void setSearchBarVisible( bool visible ); %Docstring -Sets whether the find bar is ``visible``. +Sets whether the search bar is ``visible``. -.. seealso:: :py:func:`showFind` +.. seealso:: :py:func:`showSearchBar` -.. seealso:: :py:func:`hideFind` +.. seealso:: :py:func:`hideSearchBar` %End }; diff --git a/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in b/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in index 87e6cfd45a3a5..f292cc8094923 100644 --- a/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in +++ b/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in @@ -640,31 +640,31 @@ Returns the wrapped code editor. public slots: - void showFind(); + void showSearchBar(); %Docstring -Shows the find bar. +Shows the search bar. -.. seealso:: :py:func:`hideFind` +.. seealso:: :py:func:`hideSearchBar` -.. seealso:: :py:func:`setFindVisible` +.. seealso:: :py:func:`setSearchBarVisible` %End - void hideFind(); + void hideSearchBar(); %Docstring -Hides the find bar. +Hides the search bar. -.. seealso:: :py:func:`showFind` +.. seealso:: :py:func:`showSearchBar` -.. seealso:: :py:func:`setFindVisible` +.. seealso:: :py:func:`setSearchBarVisible` %End - void setFindVisible( bool visible ); + void setSearchBarVisible( bool visible ); %Docstring -Sets whether the find bar is ``visible``. +Sets whether the search bar is ``visible``. -.. seealso:: :py:func:`showFind` +.. seealso:: :py:func:`showSearchBar` -.. seealso:: :py:func:`hideFind` +.. seealso:: :py:func:`hideSearchBar` %End }; diff --git a/src/gui/codeeditors/qgscodeeditor.cpp b/src/gui/codeeditors/qgscodeeditor.cpp index 3cfa951aeecb1..b797b09e969e1 100644 --- a/src/gui/codeeditors/qgscodeeditor.cpp +++ b/src/gui/codeeditors/qgscodeeditor.cpp @@ -1219,18 +1219,18 @@ QgsCodeEditorWidget::QgsCodeEditorWidget( QgsCodeEditor *editor, QWidget *parent layoutFind->addWidget( mWrapAroundCheck ); connect( mLineEditFind, &QLineEdit::returnPressed, this, &QgsCodeEditorWidget::findNext ); - connect( mLineEditFind, &QLineEdit::textChanged, this, &QgsCodeEditorWidget::textFindChanged ); + connect( mLineEditFind, &QLineEdit::textChanged, this, &QgsCodeEditorWidget::textSearchChanged ); connect( mFindNextButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findNext ); connect( mFindPrevButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findPrevious ); - connect( mCaseSensitiveCheck, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateFind ); - connect( mWholeWordCheck, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateFind ); - connect( mWrapAroundCheck, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateFind ); + connect( mCaseSensitiveCheck, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateSearch ); + connect( mWholeWordCheck, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateSearch ); + connect( mWrapAroundCheck, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateSearch ); QShortcut *findShortcut = new QShortcut( QKeySequence::StandardKey::Find, mEditor ); findShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut ); connect( findShortcut, &QShortcut::activated, this, [this] { - showFind(); + showSearchBar(); mLineEditFind->setFocus(); if ( mEditor->hasSelectedText() ) { @@ -1254,7 +1254,7 @@ QgsCodeEditorWidget::QgsCodeEditorWidget( QgsCodeEditor *editor, QWidget *parent closeFindShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut ); connect( closeFindShortcut, &QShortcut::activated, this, [this] { - hideFind(); + hideSearchBar(); mEditor->setFocus(); } ); @@ -1265,22 +1265,22 @@ QgsCodeEditorWidget::QgsCodeEditorWidget( QgsCodeEditor *editor, QWidget *parent setLayout( vl ); } -void QgsCodeEditorWidget::showFind() +void QgsCodeEditorWidget::showSearchBar() { mFindWidget->show(); } -void QgsCodeEditorWidget::hideFind() +void QgsCodeEditorWidget::hideSearchBar() { mFindWidget->hide(); } -void QgsCodeEditorWidget::setFindVisible( bool visible ) +void QgsCodeEditorWidget::setSearchBarVisible( bool visible ) { if ( visible ) - showFind(); + showSearchBar(); else - hideFind(); + hideSearchBar(); } void QgsCodeEditorWidget::findNext() @@ -1293,13 +1293,13 @@ void QgsCodeEditorWidget::findPrevious() findText( false, false ); } -void QgsCodeEditorWidget::textFindChanged( const QString &text ) +void QgsCodeEditorWidget::textSearchChanged( const QString &text ) { if ( !text.isEmpty() ) { mFindNextButton->setEnabled( true ); mFindPrevButton->setEnabled( true ); - updateFind(); + updateSearch(); } else { @@ -1309,7 +1309,7 @@ void QgsCodeEditorWidget::textFindChanged( const QString &text ) } } -void QgsCodeEditorWidget::updateFind() +void QgsCodeEditorWidget::updateSearch() { if ( mBlockSearching ) return; diff --git a/src/gui/codeeditors/qgscodeeditor.h b/src/gui/codeeditors/qgscodeeditor.h index f5dde9269c135..8979b2557a092 100644 --- a/src/gui/codeeditors/qgscodeeditor.h +++ b/src/gui/codeeditors/qgscodeeditor.h @@ -698,35 +698,35 @@ class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget public slots: /** - * Shows the find bar. + * Shows the search bar. * - * \see hideFind() - * \see setFindVisible() + * \see hideSearchBar() + * \see setSearchBarVisible() */ - void showFind(); + void showSearchBar(); /** - * Hides the find bar. + * Hides the search bar. * - * \see showFind() - * \see setFindVisible() + * \see showSearchBar() + * \see setSearchBarVisible() */ - void hideFind(); + void hideSearchBar(); /** - * Sets whether the find bar is \a visible. + * Sets whether the search bar is \a visible. * - * \see showFind() - * \see hideFind() + * \see showSearchBar() + * \see hideSearchBar() */ - void setFindVisible( bool visible ); + void setSearchBarVisible( bool visible ); private slots: void findNext(); void findPrevious(); - void textFindChanged( const QString &text ); - void updateFind(); + void textSearchChanged( const QString &text ); + void updateSearch(); private: From 1e8c00354f34833263b7e257871bbb83f7a15e54 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 8 May 2024 10:37:47 +1000 Subject: [PATCH 039/102] Move QgsCodeEditorWidget to a new file --- .../codeeditors/qgscodeeditor.sip.in | 68 ------ .../codeeditors/qgscodeeditorwidget.sip.in | 85 +++++++ .../qgsqueryresultwidget.sip.in | 1 + python/PyQt6/gui/gui_auto.sip | 3 + .../codeeditors/qgscodeeditor.sip.in | 68 ------ .../codeeditors/qgscodeeditorwidget.sip.in | 85 +++++++ .../qgsqueryresultwidget.sip.in | 1 + python/gui/gui_auto.sip | 3 + src/gui/CMakeLists.txt | 2 + src/gui/codeeditors/qgscodeeditor.cpp | 207 ---------------- src/gui/codeeditors/qgscodeeditor.h | 82 ------- src/gui/codeeditors/qgscodeeditorwidget.cpp | 222 ++++++++++++++++++ src/gui/codeeditors/qgscodeeditorwidget.h | 110 +++++++++ src/gui/qgsqueryresultwidget.cpp | 1 + src/gui/qgsqueryresultwidget.h | 2 + 15 files changed, 515 insertions(+), 425 deletions(-) create mode 100644 python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in create mode 100644 python/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in create mode 100644 src/gui/codeeditors/qgscodeeditorwidget.cpp create mode 100644 src/gui/codeeditors/qgscodeeditorwidget.h diff --git a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in index ed6555a1d5714..1f899fc3aef81 100644 --- a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in +++ b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in @@ -603,74 +603,6 @@ QFlags operator|(QgsCodeEditor::Flag f1, QFlags operator|(QgsCodeEditor::Flag f1, QFlags #include @@ -36,10 +35,6 @@ #include #include #include -#include -#include -#include -#include QMap< QgsCodeEditorColorScheme::ColorRole, QString > QgsCodeEditor::sColorRoleToSettingsKey { @@ -1171,205 +1166,3 @@ int QgsCodeInterpreter::exec( const QString &command ) mState = execCommandImpl( command ); return mState; } - -// -// QgsCodeEditorWidget -// - -QgsCodeEditorWidget::QgsCodeEditorWidget( QgsCodeEditor *editor, QWidget *parent ) - : QgsPanelWidget( parent ) - , mEditor( editor ) -{ - Q_ASSERT( mEditor ); - - QVBoxLayout *vl = new QVBoxLayout(); - vl->setContentsMargins( 0, 0, 0, 0 ); - vl->setSpacing( 0 ); - vl->addWidget( editor, 1 ); - - mFindWidget = new QWidget(); - QHBoxLayout *layoutFind = new QHBoxLayout(); - layoutFind->setContentsMargins( 0, 2, 0, 0 ); - mLineEditFind = new QgsFilterLineEdit(); - mLineEditFind->setShowSearchIcon( true ); - mLineEditFind->setPlaceholderText( tr( "Enter text to find…" ) ); - layoutFind->addWidget( mLineEditFind, 1 ); - - mFindPrevButton = new QToolButton(); - mFindPrevButton->setEnabled( false ); - mFindPrevButton->setToolTip( tr( "Find Previous" ) ); - mFindPrevButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconSearchPrevEditorConsole.svg" ) ) ); - mFindPrevButton->setAutoRaise( true ); - layoutFind->addWidget( mFindPrevButton ); - - mFindNextButton = new QToolButton(); - mFindNextButton->setEnabled( false ); - mFindNextButton->setToolTip( tr( "Find Next" ) ); - mFindNextButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconSearchNextEditorConsole.svg" ) ) ); - mFindNextButton->setAutoRaise( true ); - layoutFind->addWidget( mFindNextButton ); - - mCaseSensitiveCheck = new QCheckBox( tr( "Case Sensitive" ) ); - layoutFind->addWidget( mCaseSensitiveCheck ); - - mWholeWordCheck = new QCheckBox( tr( "Whole Word" ) ); - layoutFind->addWidget( mWholeWordCheck ); - - mWrapAroundCheck = new QCheckBox( tr( "Wrap Around" ) ); - layoutFind->addWidget( mWrapAroundCheck ); - - connect( mLineEditFind, &QLineEdit::returnPressed, this, &QgsCodeEditorWidget::findNext ); - connect( mLineEditFind, &QLineEdit::textChanged, this, &QgsCodeEditorWidget::textSearchChanged ); - connect( mFindNextButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findNext ); - connect( mFindPrevButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findPrevious ); - connect( mCaseSensitiveCheck, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateSearch ); - connect( mWholeWordCheck, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateSearch ); - connect( mWrapAroundCheck, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateSearch ); - - QShortcut *findShortcut = new QShortcut( QKeySequence::StandardKey::Find, mEditor ); - findShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut ); - connect( findShortcut, &QShortcut::activated, this, [this] - { - showSearchBar(); - mLineEditFind->setFocus(); - if ( mEditor->hasSelectedText() ) - { - mBlockSearching++; - mLineEditFind->setText( mEditor->selectedText().trimmed() ); - mBlockSearching--; - } - mLineEditFind->selectAll(); - } ); - - QShortcut *findNextShortcut = new QShortcut( QKeySequence::StandardKey::FindNext, this ); - findNextShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut ); - connect( findNextShortcut, &QShortcut::activated, this, &QgsCodeEditorWidget::findNext ); - - QShortcut *findPreviousShortcut = new QShortcut( QKeySequence::StandardKey::FindPrevious, this ); - findPreviousShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut ); - connect( findPreviousShortcut, &QShortcut::activated, this, &QgsCodeEditorWidget::findPrevious ); - - // escape on editor hides the find bar - QShortcut *closeFindShortcut = new QShortcut( Qt::Key::Key_Escape, this ); - closeFindShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut ); - connect( closeFindShortcut, &QShortcut::activated, this, [this] - { - hideSearchBar(); - mEditor->setFocus(); - } ); - - mFindWidget->setLayout( layoutFind ); - vl->addWidget( mFindWidget ); - mFindWidget->hide(); - - setLayout( vl ); -} - -void QgsCodeEditorWidget::showSearchBar() -{ - mFindWidget->show(); -} - -void QgsCodeEditorWidget::hideSearchBar() -{ - mFindWidget->hide(); -} - -void QgsCodeEditorWidget::setSearchBarVisible( bool visible ) -{ - if ( visible ) - showSearchBar(); - else - hideSearchBar(); -} - -void QgsCodeEditorWidget::findNext() -{ - findText( true, false ); -} - -void QgsCodeEditorWidget::findPrevious() -{ - findText( false, false ); -} - -void QgsCodeEditorWidget::textSearchChanged( const QString &text ) -{ - if ( !text.isEmpty() ) - { - mFindNextButton->setEnabled( true ); - mFindPrevButton->setEnabled( true ); - updateSearch(); - } - else - { - mLineEditFind->setStyleSheet( QString() ); - mFindNextButton->setEnabled( false ); - mFindPrevButton->setEnabled( false ); - } -} - -void QgsCodeEditorWidget::updateSearch() -{ - if ( mBlockSearching ) - return; - - const QString searchString = mLineEditFind->text(); - if ( searchString.isEmpty() ) - return; - - findText( true, true, true ); -} - -void QgsCodeEditorWidget::findText( bool forward, bool findFirst, bool showNotFoundWarning ) -{ - const QString searchString = mLineEditFind->text(); - if ( searchString.isEmpty() ) - return; - - int lineFrom = 0; - int indexFrom = 0; - int lineTo = 0; - int indexTo = 0; - mEditor->getSelection( &lineFrom, &indexFrom, &lineTo, &indexTo ); - - int line = 0; - int index = 0; - if ( !findFirst ) - { - mEditor->getCursorPosition( &line, &index ); - } - if ( !forward ) - { - line = lineFrom; - index = indexFrom; - } - - const bool isRegEx = false; - const bool wrapAround = mWrapAroundCheck->isChecked(); - const bool isCaseSensitive = mCaseSensitiveCheck->isChecked(); - const bool isWholeWordOnly = mWholeWordCheck->isChecked(); - - const bool found = mEditor->findFirst( searchString, isRegEx, isCaseSensitive, isWholeWordOnly, wrapAround, forward, - line, index ); - - if ( !found ) - { - const QString styleError = QStringLiteral( "QLineEdit {background-color: #d65253; color: #ffffff;}" ); - mLineEditFind->setStyleSheet( styleError ); - - Q_UNUSED( showNotFoundWarning ) -#if 0 // TODO -- port this bit when messagebar is available - if ( showMessage ) - { - mMessageBar->pushMessage( QString(), tr( "\"%1\" was not found" ).arg( searchString ), - Qgis::MessageLevel::Info ); - } -#endif - } - else - { - mLineEditFind->setStyleSheet( QString() ); - } -} - diff --git a/src/gui/codeeditors/qgscodeeditor.h b/src/gui/codeeditors/qgscodeeditor.h index 8979b2557a092..8ef5d11cf0c52 100644 --- a/src/gui/codeeditors/qgscodeeditor.h +++ b/src/gui/codeeditors/qgscodeeditor.h @@ -662,88 +662,6 @@ class GUI_EXPORT QgsCodeEditor : public QsciScintilla Q_DECLARE_OPERATORS_FOR_FLAGS( QgsCodeEditor::Flags ) - -/** - * \ingroup gui - * \brief A widget which wraps a QgsCodeEditor in additional functionality. - * - * This widget wraps an existing QgsCodeEditor object in a widget which provides - * additional standard functionality, such as search/replace tools. The caller - * must create an unparented QgsCodeEditor object (or a subclass of QgsCodeEditor) - * first, and then construct a QgsCodeEditorWidget passing this object to the - * constructor. - * - * \note may not be available in Python bindings, depending on platform support - * - * \since QGIS 3.38 - */ -class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget -{ - Q_OBJECT - - public: - - /** - * Constructor for QgsCodeEditorWidget, wrapping the specified \a editor widget. - * - * Ownership of \a editor will be transferred to this widget. - */ - QgsCodeEditorWidget( QgsCodeEditor *editor SIP_TRANSFER, QWidget *parent SIP_TRANSFERTHIS = nullptr ); - - /** - * Returns the wrapped code editor. - */ - QgsCodeEditor *editor() { return mEditor; } - - public slots: - - /** - * Shows the search bar. - * - * \see hideSearchBar() - * \see setSearchBarVisible() - */ - void showSearchBar(); - - /** - * Hides the search bar. - * - * \see showSearchBar() - * \see setSearchBarVisible() - */ - void hideSearchBar(); - - /** - * Sets whether the search bar is \a visible. - * - * \see showSearchBar() - * \see hideSearchBar() - */ - void setSearchBarVisible( bool visible ); - - private slots: - - void findNext(); - void findPrevious(); - void textSearchChanged( const QString &text ); - void updateSearch(); - - private: - - void findText( bool forward, bool findFirst, bool showNotFoundWarning = false ); - - QgsCodeEditor *mEditor = nullptr; - QWidget *mFindWidget = nullptr; - QgsFilterLineEdit *mLineEditFind = nullptr; - QToolButton *mFindPrevButton = nullptr; - QToolButton *mFindNextButton = nullptr; - QCheckBox *mCaseSensitiveCheck = nullptr; - QCheckBox *mWholeWordCheck = nullptr; - QCheckBox *mWrapAroundCheck = nullptr; - int mBlockSearching = 0; -}; - - // clazy:excludeall=qstring-allocations #endif diff --git a/src/gui/codeeditors/qgscodeeditorwidget.cpp b/src/gui/codeeditors/qgscodeeditorwidget.cpp new file mode 100644 index 0000000000000..fe047f7dc091c --- /dev/null +++ b/src/gui/codeeditors/qgscodeeditorwidget.cpp @@ -0,0 +1,222 @@ +/*************************************************************************** + qgscodeeditorwidget.cpp + -------------------------------------- + Date : May 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 "qgscodeeditorwidget.h" +#include "qgscodeeditor.h" +#include "qgsfilterlineedit.h" +#include "qgsapplication.h" + +#include +#include +#include +#include + +QgsCodeEditorWidget::QgsCodeEditorWidget( QgsCodeEditor *editor, QWidget *parent ) + : QgsPanelWidget( parent ) + , mEditor( editor ) +{ + Q_ASSERT( mEditor ); + + QVBoxLayout *vl = new QVBoxLayout(); + vl->setContentsMargins( 0, 0, 0, 0 ); + vl->setSpacing( 0 ); + vl->addWidget( editor, 1 ); + + mFindWidget = new QWidget(); + QHBoxLayout *layoutFind = new QHBoxLayout(); + layoutFind->setContentsMargins( 0, 2, 0, 0 ); + mLineEditFind = new QgsFilterLineEdit(); + mLineEditFind->setShowSearchIcon( true ); + mLineEditFind->setPlaceholderText( tr( "Enter text to find…" ) ); + layoutFind->addWidget( mLineEditFind, 1 ); + + mFindPrevButton = new QToolButton(); + mFindPrevButton->setEnabled( false ); + mFindPrevButton->setToolTip( tr( "Find Previous" ) ); + mFindPrevButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconSearchPrevEditorConsole.svg" ) ) ); + mFindPrevButton->setAutoRaise( true ); + layoutFind->addWidget( mFindPrevButton ); + + mFindNextButton = new QToolButton(); + mFindNextButton->setEnabled( false ); + mFindNextButton->setToolTip( tr( "Find Next" ) ); + mFindNextButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconSearchNextEditorConsole.svg" ) ) ); + mFindNextButton->setAutoRaise( true ); + layoutFind->addWidget( mFindNextButton ); + + mCaseSensitiveCheck = new QCheckBox( tr( "Case Sensitive" ) ); + layoutFind->addWidget( mCaseSensitiveCheck ); + + mWholeWordCheck = new QCheckBox( tr( "Whole Word" ) ); + layoutFind->addWidget( mWholeWordCheck ); + + mWrapAroundCheck = new QCheckBox( tr( "Wrap Around" ) ); + layoutFind->addWidget( mWrapAroundCheck ); + + connect( mLineEditFind, &QLineEdit::returnPressed, this, &QgsCodeEditorWidget::findNext ); + connect( mLineEditFind, &QLineEdit::textChanged, this, &QgsCodeEditorWidget::textSearchChanged ); + connect( mFindNextButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findNext ); + connect( mFindPrevButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findPrevious ); + connect( mCaseSensitiveCheck, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateSearch ); + connect( mWholeWordCheck, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateSearch ); + connect( mWrapAroundCheck, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateSearch ); + + QShortcut *findShortcut = new QShortcut( QKeySequence::StandardKey::Find, mEditor ); + findShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut ); + connect( findShortcut, &QShortcut::activated, this, [this] + { + showSearchBar(); + mLineEditFind->setFocus(); + if ( mEditor->hasSelectedText() ) + { + mBlockSearching++; + mLineEditFind->setText( mEditor->selectedText().trimmed() ); + mBlockSearching--; + } + mLineEditFind->selectAll(); + } ); + + QShortcut *findNextShortcut = new QShortcut( QKeySequence::StandardKey::FindNext, this ); + findNextShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut ); + connect( findNextShortcut, &QShortcut::activated, this, &QgsCodeEditorWidget::findNext ); + + QShortcut *findPreviousShortcut = new QShortcut( QKeySequence::StandardKey::FindPrevious, this ); + findPreviousShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut ); + connect( findPreviousShortcut, &QShortcut::activated, this, &QgsCodeEditorWidget::findPrevious ); + + // escape on editor hides the find bar + QShortcut *closeFindShortcut = new QShortcut( Qt::Key::Key_Escape, this ); + closeFindShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut ); + connect( closeFindShortcut, &QShortcut::activated, this, [this] + { + hideSearchBar(); + mEditor->setFocus(); + } ); + + mFindWidget->setLayout( layoutFind ); + vl->addWidget( mFindWidget ); + mFindWidget->hide(); + + setLayout( vl ); +} + +void QgsCodeEditorWidget::showSearchBar() +{ + mFindWidget->show(); +} + +void QgsCodeEditorWidget::hideSearchBar() +{ + mFindWidget->hide(); +} + +void QgsCodeEditorWidget::setSearchBarVisible( bool visible ) +{ + if ( visible ) + showSearchBar(); + else + hideSearchBar(); +} + +void QgsCodeEditorWidget::findNext() +{ + findText( true, false ); +} + +void QgsCodeEditorWidget::findPrevious() +{ + findText( false, false ); +} + +void QgsCodeEditorWidget::textSearchChanged( const QString &text ) +{ + if ( !text.isEmpty() ) + { + mFindNextButton->setEnabled( true ); + mFindPrevButton->setEnabled( true ); + updateSearch(); + } + else + { + mLineEditFind->setStyleSheet( QString() ); + mFindNextButton->setEnabled( false ); + mFindPrevButton->setEnabled( false ); + } +} + +void QgsCodeEditorWidget::updateSearch() +{ + if ( mBlockSearching ) + return; + + const QString searchString = mLineEditFind->text(); + if ( searchString.isEmpty() ) + return; + + findText( true, true, true ); +} + +void QgsCodeEditorWidget::findText( bool forward, bool findFirst, bool showNotFoundWarning ) +{ + const QString searchString = mLineEditFind->text(); + if ( searchString.isEmpty() ) + return; + + int lineFrom = 0; + int indexFrom = 0; + int lineTo = 0; + int indexTo = 0; + mEditor->getSelection( &lineFrom, &indexFrom, &lineTo, &indexTo ); + + int line = 0; + int index = 0; + if ( !findFirst ) + { + mEditor->getCursorPosition( &line, &index ); + } + if ( !forward ) + { + line = lineFrom; + index = indexFrom; + } + + const bool isRegEx = false; + const bool wrapAround = mWrapAroundCheck->isChecked(); + const bool isCaseSensitive = mCaseSensitiveCheck->isChecked(); + const bool isWholeWordOnly = mWholeWordCheck->isChecked(); + + const bool found = mEditor->findFirst( searchString, isRegEx, isCaseSensitive, isWholeWordOnly, wrapAround, forward, + line, index ); + + if ( !found ) + { + const QString styleError = QStringLiteral( "QLineEdit {background-color: #d65253; color: #ffffff;}" ); + mLineEditFind->setStyleSheet( styleError ); + + Q_UNUSED( showNotFoundWarning ) +#if 0 // TODO -- port this bit when messagebar is available + if ( showMessage ) + { + mMessageBar->pushMessage( QString(), tr( "\"%1\" was not found" ).arg( searchString ), + Qgis::MessageLevel::Info ); + } +#endif + } + else + { + mLineEditFind->setStyleSheet( QString() ); + } +} + diff --git a/src/gui/codeeditors/qgscodeeditorwidget.h b/src/gui/codeeditors/qgscodeeditorwidget.h new file mode 100644 index 0000000000000..bfa80c05a891f --- /dev/null +++ b/src/gui/codeeditors/qgscodeeditorwidget.h @@ -0,0 +1,110 @@ +/*************************************************************************** + qgscodeeditorwidget.h + -------------------------------------- + Date : May 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 QGSCODEEDITORWIDGET_H +#define QGSCODEEDITORWIDGET_H + +#include "qgis_gui.h" +#include "qgis_sip.h" +#include "qgspanelwidget.h" + +class QgsCodeEditor; +class QgsFilterLineEdit; +class QToolButton; +class QCheckBox; + +SIP_IF_MODULE( HAVE_QSCI_SIP ) + +/** + * \ingroup gui + * \brief A widget which wraps a QgsCodeEditor in additional functionality. + * + * This widget wraps an existing QgsCodeEditor object in a widget which provides + * additional standard functionality, such as search/replace tools. The caller + * must create an unparented QgsCodeEditor object (or a subclass of QgsCodeEditor) + * first, and then construct a QgsCodeEditorWidget passing this object to the + * constructor. + * + * \note may not be available in Python bindings, depending on platform support + * + * \since QGIS 3.38 + */ +class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsCodeEditorWidget, wrapping the specified \a editor widget. + * + * Ownership of \a editor will be transferred to this widget. + */ + QgsCodeEditorWidget( QgsCodeEditor *editor SIP_TRANSFER, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + + /** + * Returns the wrapped code editor. + */ + QgsCodeEditor *editor() { return mEditor; } + + public slots: + + /** + * Shows the search bar. + * + * \see hideSearchBar() + * \see setSearchBarVisible() + */ + void showSearchBar(); + + /** + * Hides the search bar. + * + * \see showSearchBar() + * \see setSearchBarVisible() + */ + void hideSearchBar(); + + /** + * Sets whether the search bar is \a visible. + * + * \see showSearchBar() + * \see hideSearchBar() + */ + void setSearchBarVisible( bool visible ); + + private slots: + + void findNext(); + void findPrevious(); + void textSearchChanged( const QString &text ); + void updateSearch(); + + private: + + void findText( bool forward, bool findFirst, bool showNotFoundWarning = false ); + + QgsCodeEditor *mEditor = nullptr; + QWidget *mFindWidget = nullptr; + QgsFilterLineEdit *mLineEditFind = nullptr; + QToolButton *mFindPrevButton = nullptr; + QToolButton *mFindNextButton = nullptr; + QCheckBox *mCaseSensitiveCheck = nullptr; + QCheckBox *mWholeWordCheck = nullptr; + QCheckBox *mWrapAroundCheck = nullptr; + int mBlockSearching = 0; +}; + +#endif // QGSCODEEDITORWIDGET_H diff --git a/src/gui/qgsqueryresultwidget.cpp b/src/gui/qgsqueryresultwidget.cpp index 5f01fe3aafd8f..46083f3ed0e42 100644 --- a/src/gui/qgsqueryresultwidget.cpp +++ b/src/gui/qgsqueryresultwidget.cpp @@ -26,6 +26,7 @@ #include "qgshistoryentry.h" #include "qgsproviderregistry.h" #include "qgsprovidermetadata.h" +#include "qgscodeeditorwidget.h" #include #include diff --git a/src/gui/qgsqueryresultwidget.h b/src/gui/qgsqueryresultwidget.h index f196cdc999093..8f1887ce94c26 100644 --- a/src/gui/qgsqueryresultwidget.h +++ b/src/gui/qgsqueryresultwidget.h @@ -28,6 +28,8 @@ #include #include +class QgsCodeEditorWidget; + ///@cond private #ifndef SIP_RUN From 57cc7ed3824dd68df12fdbfa81632706c40beeab Mon Sep 17 00:00:00 2001 From: Roni Lindholm Date: Mon, 11 Mar 2024 08:42:43 +0200 Subject: [PATCH 040/102] Fix merge features with hidden fields Instead of ignoring always hidden fields, use field default value or value from one of merged features. Fixes #28253 Co-authored-by: Joonalai --- src/app/qgsmergeattributesdialog.cpp | 19 +++--- .../src/app/testqgsmergeattributesdialog.cpp | 68 +++++++++++++++++++ 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/src/app/qgsmergeattributesdialog.cpp b/src/app/qgsmergeattributesdialog.cpp index 18355e717b79f..76e200f3a2d1f 100644 --- a/src/app/qgsmergeattributesdialog.cpp +++ b/src/app/qgsmergeattributesdialog.cpp @@ -171,6 +171,9 @@ void QgsMergeAttributesDialog::createTableWidgetContents() if ( setup.type() == QLatin1String( "Hidden" ) || setup.type() == QLatin1String( "Immutable" ) ) { mHiddenAttributes.insert( idx ); + } + if ( setup.type() == QLatin1String( "Immutable" ) ) + { continue; } @@ -182,13 +185,18 @@ void QgsMergeAttributesDialog::createTableWidgetContents() mTableWidget->setHorizontalHeaderItem( col, item ); QComboBox *cb = createMergeComboBox( mFields.at( idx ).type(), col ); - if ( ! mVectorLayer->dataProvider()->pkAttributeIndexes().contains( mFields.fieldOriginIndex( idx ) ) && - mFields.at( idx ).constraints().constraints() & QgsFieldConstraints::ConstraintUnique ) + if ( ( ! mVectorLayer->dataProvider()->pkAttributeIndexes().contains( mFields.fieldOriginIndex( idx ) ) && + mFields.at( idx ).constraints().constraints() & QgsFieldConstraints::ConstraintUnique ) || mHiddenAttributes.contains( idx ) ) { cb->setCurrentIndex( cb->findData( "skip" ) ); } mTableWidget->setCellWidget( 0, col, cb ); + if ( mHiddenAttributes.contains( idx ) ) + { + mTableWidget->setColumnHidden( idx, col ); + } + col++; } @@ -768,13 +776,6 @@ QgsAttributes QgsMergeAttributesDialog::mergedAttributes() const QgsAttributes results( mFields.count() ); for ( int fieldIdx = 0; fieldIdx < mFields.count(); ++fieldIdx ) { - if ( mHiddenAttributes.contains( fieldIdx ) ) - { - //hidden attribute, set to default value - results[fieldIdx] = QVariant(); - continue; - } - QComboBox *comboBox = qobject_cast( mTableWidget->cellWidget( 0, widgetIndex ) ); if ( !comboBox ) continue; diff --git a/tests/src/app/testqgsmergeattributesdialog.cpp b/tests/src/app/testqgsmergeattributesdialog.cpp index f6e9a99ca17db..1d89bf26c9590 100644 --- a/tests/src/app/testqgsmergeattributesdialog.cpp +++ b/tests/src/app/testqgsmergeattributesdialog.cpp @@ -150,6 +150,74 @@ class TestQgsMergeattributesDialog : public QgsTest QVERIFY( !dialog.mergedAttributes().at( 0 ).isValid() ); QCOMPARE( dialog.mergedAttributes().at( 1 ), 22 ); } + + void testWithHiddenField() + { + // Create test layer + QgsVectorFileWriter::SaveVectorOptions options; + QgsVectorLayer ml( "LineString", "test", "memory" ); + QVERIFY( ml.isValid() ); + + QgsField notHiddenField( QStringLiteral( "not_hidden" ), QVariant::Int ); + QgsField hiddenField( QStringLiteral( "hidden" ), QVariant::Int ); + // hide the field + ml.setEditorWidgetSetup( 1, QgsEditorWidgetSetup( QStringLiteral( "Hidden" ), QVariantMap() ) ); + QVERIFY( ml.dataProvider()->addAttributes( { notHiddenField, hiddenField } ) ); + ml.updateFields(); + + // Create features + QgsFeature f1( ml.fields(), 1 ); + f1.setAttributes( QVector() << 1 << 2 ); + f1.setGeometry( QgsGeometry::fromWkt( "LINESTRING(0 0, 10 0)" ) ); + QVERIFY( ml.dataProvider()->addFeature( f1 ) ); + QCOMPARE( ml.featureCount(), 1 ); + + QgsFeature f2( ml.fields(), 2 ); + f2.setAttributes( QVector() << 3 << 4 ); + f2.setGeometry( QgsGeometry::fromWkt( "LINESTRING(10 0, 15 0)" ) ); + QVERIFY( ml.dataProvider()->addFeature( f2 ) ); + QCOMPARE( ml.featureCount(), 2 ); + + // Merge the attributes together + QgsMergeAttributesDialog dialog( QgsFeatureList() << f1 << f2, &ml, mQgisApp->mapCanvas() ); + QVERIFY( QMetaObject::invokeMethod( &dialog, "mFromLargestPushButton_clicked" ) ); + QCOMPARE( dialog.mergedAttributes(), QgsAttributes() << 1 << 2 ); + } + + void testWithHiddenFieldDefaultsToEmpty() + { + // Create test layer + QgsVectorFileWriter::SaveVectorOptions options; + QgsVectorLayer ml( "LineString", "test", "memory" ); + QVERIFY( ml.isValid() ); + + QgsField notHiddenField( QStringLiteral( "not_hidden" ), QVariant::Int ); + QgsField hiddenField( QStringLiteral( "hidden" ), QVariant::Int ); + QVERIFY( ml.dataProvider()->addAttributes( { notHiddenField, hiddenField } ) ); + ml.updateFields(); + + // hide the field + ml.setEditorWidgetSetup( 1, QgsEditorWidgetSetup( QStringLiteral( "Hidden" ), QVariantMap() ) ); + + + // Create features + QgsFeature f1( ml.fields(), 1 ); + f1.setAttributes( QVector() << 1 << 2 ); + f1.setGeometry( QgsGeometry::fromWkt( "LINESTRING(0 0, 10 0)" ) ); + QVERIFY( ml.dataProvider()->addFeature( f1 ) ); + QCOMPARE( ml.featureCount(), 1 ); + + QgsFeature f2( ml.fields(), 2 ); + f2.setAttributes( QVector() << 3 << 4 ); + f2.setGeometry( QgsGeometry::fromWkt( "LINESTRING(10 0, 15 0)" ) ); + QVERIFY( ml.dataProvider()->addFeature( f2 ) ); + QCOMPARE( ml.featureCount(), 2 ); + + // Merge the attributes together + QgsMergeAttributesDialog dialog( QgsFeatureList() << f1 << f2, &ml, mQgisApp->mapCanvas() ); + // QVariant gets turned into default value while saving the layer + QCOMPARE( dialog.mergedAttributes(), QgsAttributes() << 1 << QVariant() ); + } }; QGSTEST_MAIN( TestQgsMergeattributesDialog ) From 19d553df1046b755a967ef3d6844f93948b34bfb Mon Sep 17 00:00:00 2001 From: Matthias Kuhn Date: Wed, 8 May 2024 18:14:42 +0200 Subject: [PATCH 041/102] Add safety check, avoid crash --- src/server/services/wms/qgswmsrestorer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/services/wms/qgswmsrestorer.cpp b/src/server/services/wms/qgswmsrestorer.cpp index 9ccd804e849b0..6094f46d440fb 100644 --- a/src/server/services/wms/qgswmsrestorer.cpp +++ b/src/server/services/wms/qgswmsrestorer.cpp @@ -58,7 +58,7 @@ QgsLayerRestorer::QgsLayerRestorer( const QList &layers ) { QgsRasterLayer *rLayer = qobject_cast( layer ); - if ( rLayer ) + if ( rLayer && rLayer->renderer() ) { settings.mOpacity = rLayer->renderer()->opacity(); } @@ -119,7 +119,7 @@ QgsLayerRestorer::~QgsLayerRestorer() { QgsRasterLayer *rLayer = qobject_cast( layer ); - if ( rLayer ) + if ( rLayer && rLayer->renderer() ) { rLayer->renderer()->setOpacity( settings.mOpacity ); } From 997648baa91d296077e2c133c6ed285a30bf1f9f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 2 May 2024 07:41:27 +1000 Subject: [PATCH 042/102] Add 'over point' placement option for cartographic label mode This adds a new option for placement when labels are set to the "cartographic" mode, for "O" = "over point". When a feature's data defined placement priorities include this new 'O' option, a label can be placed directly over the corresponding point. Sponsored by Rubicon Concierge Real Estate Services --- python/PyQt6/core/auto_additions/qgis.py | 5 ++++- python/PyQt6/core/auto_generated/qgis.sip.in | 1 + python/core/auto_additions/qgis.py | 5 ++++- python/core/auto_generated/qgis.sip.in | 1 + src/core/labeling/qgslabelingengine.cpp | 5 +++++ src/core/labeling/qgspallabeling.cpp | 4 +--- src/core/pal/feature.cpp | 10 ++++++++-- src/core/qgis.h | 1 + .../python/test_qgspallabeling_placement.py | 12 ++++++++++++ .../sp_point_ordered_placement_over_point.png | Bin 0 -> 471523 bytes 10 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 tests/testdata/control_images/expected_pal_placement/sp_point_ordered_placement_over_point/sp_point_ordered_placement_over_point.png diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index 361389af6f2d1..9fc097f1f844a 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -1040,7 +1040,10 @@ QgsPalLayerSettings.BottomRight = Qgis.LabelPredefinedPointPosition.BottomRight QgsPalLayerSettings.BottomRight.is_monkey_patched = True QgsPalLayerSettings.BottomRight.__doc__ = "Label on bottom right of point" -Qgis.LabelPredefinedPointPosition.__doc__ = "Positions for labels when using the Qgis.LabelPlacement.OrderedPositionsAroundPoint placement mode.\n\n.. note::\n\n Prior to QGIS 3.26 this was available as :py:class:`QgsPalLayerSettings`.PredefinedPointPosition\n\n.. versionadded:: 3.26\n\n" + '* ``TopLeft``: ' + Qgis.LabelPredefinedPointPosition.TopLeft.__doc__ + '\n' + '* ``TopSlightlyLeft``: ' + Qgis.LabelPredefinedPointPosition.TopSlightlyLeft.__doc__ + '\n' + '* ``TopMiddle``: ' + Qgis.LabelPredefinedPointPosition.TopMiddle.__doc__ + '\n' + '* ``TopSlightlyRight``: ' + Qgis.LabelPredefinedPointPosition.TopSlightlyRight.__doc__ + '\n' + '* ``TopRight``: ' + Qgis.LabelPredefinedPointPosition.TopRight.__doc__ + '\n' + '* ``MiddleLeft``: ' + Qgis.LabelPredefinedPointPosition.MiddleLeft.__doc__ + '\n' + '* ``MiddleRight``: ' + Qgis.LabelPredefinedPointPosition.MiddleRight.__doc__ + '\n' + '* ``BottomLeft``: ' + Qgis.LabelPredefinedPointPosition.BottomLeft.__doc__ + '\n' + '* ``BottomSlightlyLeft``: ' + Qgis.LabelPredefinedPointPosition.BottomSlightlyLeft.__doc__ + '\n' + '* ``BottomMiddle``: ' + Qgis.LabelPredefinedPointPosition.BottomMiddle.__doc__ + '\n' + '* ``BottomSlightlyRight``: ' + Qgis.LabelPredefinedPointPosition.BottomSlightlyRight.__doc__ + '\n' + '* ``BottomRight``: ' + Qgis.LabelPredefinedPointPosition.BottomRight.__doc__ +QgsPalLayerSettings.OverPoint = Qgis.LabelPredefinedPointPosition.OverPoint +QgsPalLayerSettings.OverPoint.is_monkey_patched = True +QgsPalLayerSettings.OverPoint.__doc__ = "Label directly centered over point (since QGIS 3.38)" +Qgis.LabelPredefinedPointPosition.__doc__ = "Positions for labels when using the Qgis.LabelPlacement.OrderedPositionsAroundPoint placement mode.\n\n.. note::\n\n Prior to QGIS 3.26 this was available as :py:class:`QgsPalLayerSettings`.PredefinedPointPosition\n\n.. versionadded:: 3.26\n\n" + '* ``TopLeft``: ' + Qgis.LabelPredefinedPointPosition.TopLeft.__doc__ + '\n' + '* ``TopSlightlyLeft``: ' + Qgis.LabelPredefinedPointPosition.TopSlightlyLeft.__doc__ + '\n' + '* ``TopMiddle``: ' + Qgis.LabelPredefinedPointPosition.TopMiddle.__doc__ + '\n' + '* ``TopSlightlyRight``: ' + Qgis.LabelPredefinedPointPosition.TopSlightlyRight.__doc__ + '\n' + '* ``TopRight``: ' + Qgis.LabelPredefinedPointPosition.TopRight.__doc__ + '\n' + '* ``MiddleLeft``: ' + Qgis.LabelPredefinedPointPosition.MiddleLeft.__doc__ + '\n' + '* ``MiddleRight``: ' + Qgis.LabelPredefinedPointPosition.MiddleRight.__doc__ + '\n' + '* ``BottomLeft``: ' + Qgis.LabelPredefinedPointPosition.BottomLeft.__doc__ + '\n' + '* ``BottomSlightlyLeft``: ' + Qgis.LabelPredefinedPointPosition.BottomSlightlyLeft.__doc__ + '\n' + '* ``BottomMiddle``: ' + Qgis.LabelPredefinedPointPosition.BottomMiddle.__doc__ + '\n' + '* ``BottomSlightlyRight``: ' + Qgis.LabelPredefinedPointPosition.BottomSlightlyRight.__doc__ + '\n' + '* ``BottomRight``: ' + Qgis.LabelPredefinedPointPosition.BottomRight.__doc__ + '\n' + '* ``OverPoint``: ' + Qgis.LabelPredefinedPointPosition.OverPoint.__doc__ # -- Qgis.LabelPredefinedPointPosition.baseClass = Qgis QgsPalLayerSettings.OffsetType = Qgis.LabelOffsetType diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index 82e79b2a67699..b87aecd8d8a13 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -577,6 +577,7 @@ The development version BottomMiddle, BottomSlightlyRight, BottomRight, + OverPoint, }; enum class LabelOffsetType /BaseType=IntEnum/ diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index 15f42e5c2d9a1..29d628388b3d4 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -1019,7 +1019,10 @@ QgsPalLayerSettings.BottomRight = Qgis.LabelPredefinedPointPosition.BottomRight QgsPalLayerSettings.BottomRight.is_monkey_patched = True QgsPalLayerSettings.BottomRight.__doc__ = "Label on bottom right of point" -Qgis.LabelPredefinedPointPosition.__doc__ = "Positions for labels when using the Qgis.LabelPlacement.OrderedPositionsAroundPoint placement mode.\n\n.. note::\n\n Prior to QGIS 3.26 this was available as :py:class:`QgsPalLayerSettings`.PredefinedPointPosition\n\n.. versionadded:: 3.26\n\n" + '* ``TopLeft``: ' + Qgis.LabelPredefinedPointPosition.TopLeft.__doc__ + '\n' + '* ``TopSlightlyLeft``: ' + Qgis.LabelPredefinedPointPosition.TopSlightlyLeft.__doc__ + '\n' + '* ``TopMiddle``: ' + Qgis.LabelPredefinedPointPosition.TopMiddle.__doc__ + '\n' + '* ``TopSlightlyRight``: ' + Qgis.LabelPredefinedPointPosition.TopSlightlyRight.__doc__ + '\n' + '* ``TopRight``: ' + Qgis.LabelPredefinedPointPosition.TopRight.__doc__ + '\n' + '* ``MiddleLeft``: ' + Qgis.LabelPredefinedPointPosition.MiddleLeft.__doc__ + '\n' + '* ``MiddleRight``: ' + Qgis.LabelPredefinedPointPosition.MiddleRight.__doc__ + '\n' + '* ``BottomLeft``: ' + Qgis.LabelPredefinedPointPosition.BottomLeft.__doc__ + '\n' + '* ``BottomSlightlyLeft``: ' + Qgis.LabelPredefinedPointPosition.BottomSlightlyLeft.__doc__ + '\n' + '* ``BottomMiddle``: ' + Qgis.LabelPredefinedPointPosition.BottomMiddle.__doc__ + '\n' + '* ``BottomSlightlyRight``: ' + Qgis.LabelPredefinedPointPosition.BottomSlightlyRight.__doc__ + '\n' + '* ``BottomRight``: ' + Qgis.LabelPredefinedPointPosition.BottomRight.__doc__ +QgsPalLayerSettings.OverPoint = Qgis.LabelPredefinedPointPosition.OverPoint +QgsPalLayerSettings.OverPoint.is_monkey_patched = True +QgsPalLayerSettings.OverPoint.__doc__ = "Label directly centered over point (since QGIS 3.38)" +Qgis.LabelPredefinedPointPosition.__doc__ = "Positions for labels when using the Qgis.LabelPlacement.OrderedPositionsAroundPoint placement mode.\n\n.. note::\n\n Prior to QGIS 3.26 this was available as :py:class:`QgsPalLayerSettings`.PredefinedPointPosition\n\n.. versionadded:: 3.26\n\n" + '* ``TopLeft``: ' + Qgis.LabelPredefinedPointPosition.TopLeft.__doc__ + '\n' + '* ``TopSlightlyLeft``: ' + Qgis.LabelPredefinedPointPosition.TopSlightlyLeft.__doc__ + '\n' + '* ``TopMiddle``: ' + Qgis.LabelPredefinedPointPosition.TopMiddle.__doc__ + '\n' + '* ``TopSlightlyRight``: ' + Qgis.LabelPredefinedPointPosition.TopSlightlyRight.__doc__ + '\n' + '* ``TopRight``: ' + Qgis.LabelPredefinedPointPosition.TopRight.__doc__ + '\n' + '* ``MiddleLeft``: ' + Qgis.LabelPredefinedPointPosition.MiddleLeft.__doc__ + '\n' + '* ``MiddleRight``: ' + Qgis.LabelPredefinedPointPosition.MiddleRight.__doc__ + '\n' + '* ``BottomLeft``: ' + Qgis.LabelPredefinedPointPosition.BottomLeft.__doc__ + '\n' + '* ``BottomSlightlyLeft``: ' + Qgis.LabelPredefinedPointPosition.BottomSlightlyLeft.__doc__ + '\n' + '* ``BottomMiddle``: ' + Qgis.LabelPredefinedPointPosition.BottomMiddle.__doc__ + '\n' + '* ``BottomSlightlyRight``: ' + Qgis.LabelPredefinedPointPosition.BottomSlightlyRight.__doc__ + '\n' + '* ``BottomRight``: ' + Qgis.LabelPredefinedPointPosition.BottomRight.__doc__ + '\n' + '* ``OverPoint``: ' + Qgis.LabelPredefinedPointPosition.OverPoint.__doc__ # -- Qgis.LabelPredefinedPointPosition.baseClass = Qgis QgsPalLayerSettings.OffsetType = Qgis.LabelOffsetType diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index b1493a20cbdd8..c9534de7ea6fa 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -577,6 +577,7 @@ The development version BottomMiddle, BottomSlightlyRight, BottomRight, + OverPoint, }; enum class LabelOffsetType diff --git a/src/core/labeling/qgslabelingengine.cpp b/src/core/labeling/qgslabelingengine.cpp index f19838c7b406e..4960376744ff5 100644 --- a/src/core/labeling/qgslabelingengine.cpp +++ b/src/core/labeling/qgslabelingengine.cpp @@ -740,6 +740,9 @@ QString QgsLabelingUtils::encodePredefinedPositionOrder( const QVector QgsLabelingUtils::decodePredefinedPo result << Qgis::LabelPredefinedPointPosition::BottomSlightlyRight; else if ( cleaned == QLatin1String( "BR" ) ) result << Qgis::LabelPredefinedPointPosition::BottomRight; + else if ( cleaned == QLatin1String( "O" ) ) + result << Qgis::LabelPredefinedPointPosition::OverPoint; } return result; } diff --git a/src/core/labeling/qgspallabeling.cpp b/src/core/labeling/qgspallabeling.cpp index d0a39b3f39ded..1c2b932e11541 100644 --- a/src/core/labeling/qgspallabeling.cpp +++ b/src/core/labeling/qgspallabeling.cpp @@ -24,8 +24,6 @@ #include "qgsstyle.h" #include "qgstextrenderer.h" -#include - #include "pal/labelposition.h" #include @@ -221,7 +219,7 @@ void QgsPalLayerSettings::initPropertyDefinitions() "TSR=Top, slightly right|TR=Top right|
" "L=Left|R=Right|
" "BL=Bottom left|BSL=Bottom, slightly left|B=Bottom middle|
" - "BSR=Bottom, slightly right|BR=Bottom right]" ), origin ) + "BSR=Bottom, slightly right|BR=Bottom right|O=Over point]" ), origin ) }, { static_cast< int >( QgsPalLayerSettings::Property::LinePlacementOptions ), QgsPropertyDefinition( "LinePlacementFlags", QgsPropertyDefinition::DataTypeString, QObject::tr( "Line placement options" ), QObject::tr( "Comma separated list of placement options
" ) diff --git a/src/core/pal/feature.cpp b/src/core/pal/feature.cpp index 3d9962de7c0d4..aa676b38afbd0 100644 --- a/src/core/pal/feature.cpp +++ b/src/core/pal/feature.cpp @@ -41,13 +41,11 @@ #include "qgsmessagelog.h" #include "qgsgeometryutils.h" #include "qgsgeometryutils_base.h" -#include "qgslabeling.h" #include "qgspolygon.h" #include "qgstextrendererutils.h" #include #include -#include using namespace pal; @@ -523,6 +521,14 @@ void createCandidateAtOrderedPositionOverPoint( double &labelX, double &labelY, deltaX = -visualMargin.left() + symbolWidthOffset; deltaY = -labelHeight + visualMargin.top() - symbolHeightOffset; break; + + case Qgis::LabelPredefinedPointPosition::OverPoint: + quadrant = LabelPosition::QuadrantOver; + alpha = 0; + distanceToLabel = 0; + deltaX = -labelWidth / 2.0; + deltaY = -labelHeight / 2.0;// TODO - should this be adjusted by visual margin?? + break; } // Take care of the label angle when creating candidates. See pr comments #44944 for details diff --git a/src/core/qgis.h b/src/core/qgis.h index 20b8f691f448d..fd45f0bf08a61 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -945,6 +945,7 @@ class CORE_EXPORT Qgis BottomMiddle, //!< Label directly below point BottomSlightlyRight, //!< Label below point, slightly right of center BottomRight, //!< Label on bottom right of point + OverPoint, //!< Label directly centered over point (since QGIS 3.38) }; Q_ENUM( LabelPredefinedPointPosition ) diff --git a/tests/src/python/test_qgspallabeling_placement.py b/tests/src/python/test_qgspallabeling_placement.py index f6ba6319faf34..29561cf07c30c 100644 --- a/tests/src/python/test_qgspallabeling_placement.py +++ b/tests/src/python/test_qgspallabeling_placement.py @@ -745,6 +745,18 @@ def test_point_dd_ordered_placement1(self): self.lyr.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.PredefinedPositionOrder, QgsProperty()) self.layer = None + def test_point_ordered_placement_over_point(self): + # Test ordered placements using over point placement + self.layer = TestQgsPalLabeling.loadFeatureLayer('point_ordered_placement') + self._TestMapSettings = self.cloneMapSettings(self._MapSettings) + self.lyr.placement = QgsPalLayerSettings.Placement.OrderedPositionsAroundPoint + self.lyr.dist = 2 + self.lyr.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.PredefinedPositionOrder, QgsProperty.fromExpression("'O'")) + self.checkTest() + self.removeMapLayer(self.layer) + self.lyr.dataDefinedProperties().setProperty(QgsPalLayerSettings.Property.PredefinedPositionOrder, QgsProperty()) + self.layer = None + def test_point_ordered_symbol_bound_offset(self): # Test ordered placements for point using symbol bounds offset self.layer = TestQgsPalLabeling.loadFeatureLayer('point_ordered_placement') diff --git a/tests/testdata/control_images/expected_pal_placement/sp_point_ordered_placement_over_point/sp_point_ordered_placement_over_point.png b/tests/testdata/control_images/expected_pal_placement/sp_point_ordered_placement_over_point/sp_point_ordered_placement_over_point.png new file mode 100644 index 0000000000000000000000000000000000000000..175d28a00bfa53cc20ad22ed94f72a723fcf0e41 GIT binary patch literal 471523 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 Date: Thu, 2 May 2024 08:42:51 +1000 Subject: [PATCH 043/102] Add GPS navigation status to QgsGpsInformation --- python/PyQt6/core/auto_additions/qgis.py | 8 ++++++++ .../gps/qgsgpsinformation.sip.in | 18 ++++++++++++++++++ python/PyQt6/core/auto_generated/qgis.sip.in | 8 ++++++++ python/core/auto_additions/qgis.py | 8 ++++++++ .../gps/qgsgpsinformation.sip.in | 18 ++++++++++++++++++ python/core/auto_generated/qgis.sip.in | 8 ++++++++ src/core/gps/qgsgpsinformation.h | 19 +++++++++++++++++++ src/core/gps/qgsnmeaconnection.cpp | 17 +++++++++++++++++ src/core/qgis.h | 14 ++++++++++++++ 9 files changed, 118 insertions(+) diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index 9fc097f1f844a..7c5c22663f2b1 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -1551,6 +1551,14 @@ # -- Qgis.GpsQualityIndicator.baseClass = Qgis # monkey patching scoped based enum +Qgis.GpsNavigationStatus.NotValid.__doc__ = "Navigation status not valid" +Qgis.GpsNavigationStatus.Safe.__doc__ = "Safe" +Qgis.GpsNavigationStatus.Caution.__doc__ = "Caution" +Qgis.GpsNavigationStatus.Unsafe.__doc__ = "Unsafe" +Qgis.GpsNavigationStatus.__doc__ = "GPS navigation status.\n\n.. versionadded:: 3.38\n\n" + '* ``NotValid``: ' + Qgis.GpsNavigationStatus.NotValid.__doc__ + '\n' + '* ``Safe``: ' + Qgis.GpsNavigationStatus.Safe.__doc__ + '\n' + '* ``Caution``: ' + Qgis.GpsNavigationStatus.Caution.__doc__ + '\n' + '* ``Unsafe``: ' + Qgis.GpsNavigationStatus.Unsafe.__doc__ +# -- +Qgis.GpsNavigationStatus.baseClass = Qgis +# monkey patching scoped based enum Qgis.GpsInformationComponent.Location.__doc__ = "2D location (latitude/longitude), as a QgsPointXY value" Qgis.GpsInformationComponent.Altitude.__doc__ = "Altitude/elevation above or below the mean sea level" Qgis.GpsInformationComponent.GroundSpeed.__doc__ = "Ground speed" diff --git a/python/PyQt6/core/auto_generated/gps/qgsgpsinformation.sip.in b/python/PyQt6/core/auto_generated/gps/qgsgpsinformation.sip.in index e5f1c55000c5d..65f98b7219662 100644 --- a/python/PyQt6/core/auto_generated/gps/qgsgpsinformation.sip.in +++ b/python/PyQt6/core/auto_generated/gps/qgsgpsinformation.sip.in @@ -88,6 +88,24 @@ Returns the best fix status and corresponding constellation. bool satInfoComplete; + Qgis::GpsNavigationStatus navigationStatus() const; +%Docstring +Returns the navigation status. + +.. seealso:: :py:func:`setNavigationStatus` + +.. versionadded:: 3.38 +%End + + void setNavigationStatus( Qgis::GpsNavigationStatus status ); +%Docstring +Sets the navigation ``status``. + +.. seealso:: :py:func:`navigationStatus` + +.. versionadded:: 3.38 +%End + bool isValid() const; %Docstring Returns whether the connection information is valid diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index b87aecd8d8a13..2c84aca227df5 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -896,6 +896,14 @@ The development version Simulation, }; + enum class GpsNavigationStatus /BaseType=IntEnum/ + { + NotValid, + Safe, + Caution, + Unsafe, + }; + enum class GpsInformationComponent /BaseType=IntFlag/ { Location, diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index 29d628388b3d4..d04309e176020 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -1522,6 +1522,14 @@ # -- Qgis.GpsQualityIndicator.baseClass = Qgis # monkey patching scoped based enum +Qgis.GpsNavigationStatus.NotValid.__doc__ = "Navigation status not valid" +Qgis.GpsNavigationStatus.Safe.__doc__ = "Safe" +Qgis.GpsNavigationStatus.Caution.__doc__ = "Caution" +Qgis.GpsNavigationStatus.Unsafe.__doc__ = "Unsafe" +Qgis.GpsNavigationStatus.__doc__ = "GPS navigation status.\n\n.. versionadded:: 3.38\n\n" + '* ``NotValid``: ' + Qgis.GpsNavigationStatus.NotValid.__doc__ + '\n' + '* ``Safe``: ' + Qgis.GpsNavigationStatus.Safe.__doc__ + '\n' + '* ``Caution``: ' + Qgis.GpsNavigationStatus.Caution.__doc__ + '\n' + '* ``Unsafe``: ' + Qgis.GpsNavigationStatus.Unsafe.__doc__ +# -- +Qgis.GpsNavigationStatus.baseClass = Qgis +# monkey patching scoped based enum Qgis.GpsInformationComponent.Location.__doc__ = "2D location (latitude/longitude), as a QgsPointXY value" Qgis.GpsInformationComponent.Altitude.__doc__ = "Altitude/elevation above or below the mean sea level" Qgis.GpsInformationComponent.GroundSpeed.__doc__ = "Ground speed" diff --git a/python/core/auto_generated/gps/qgsgpsinformation.sip.in b/python/core/auto_generated/gps/qgsgpsinformation.sip.in index e5f1c55000c5d..65f98b7219662 100644 --- a/python/core/auto_generated/gps/qgsgpsinformation.sip.in +++ b/python/core/auto_generated/gps/qgsgpsinformation.sip.in @@ -88,6 +88,24 @@ Returns the best fix status and corresponding constellation. bool satInfoComplete; + Qgis::GpsNavigationStatus navigationStatus() const; +%Docstring +Returns the navigation status. + +.. seealso:: :py:func:`setNavigationStatus` + +.. versionadded:: 3.38 +%End + + void setNavigationStatus( Qgis::GpsNavigationStatus status ); +%Docstring +Sets the navigation ``status``. + +.. seealso:: :py:func:`navigationStatus` + +.. versionadded:: 3.38 +%End + bool isValid() const; %Docstring Returns whether the connection information is valid diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index c9534de7ea6fa..93dec02db9a69 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -896,6 +896,14 @@ The development version Simulation, }; + enum class GpsNavigationStatus + { + NotValid, + Safe, + Caution, + Unsafe, + }; + enum class GpsInformationComponent { Location, diff --git a/src/core/gps/qgsgpsinformation.h b/src/core/gps/qgsgpsinformation.h index 9add166f7b4d5..7879b0a5053bf 100644 --- a/src/core/gps/qgsgpsinformation.h +++ b/src/core/gps/qgsgpsinformation.h @@ -201,6 +201,24 @@ class CORE_EXPORT QgsGpsInformation */ bool satInfoComplete = false; + /** + * Returns the navigation status. + * + * \see setNavigationStatus() + * + * \since QGIS 3.38 + */ + Qgis::GpsNavigationStatus navigationStatus() const { return mNavigationStatus; } + + /** + * Sets the navigation \a status. + * + * \see navigationStatus() + * + * \since QGIS 3.38 + */ + void setNavigationStatus( Qgis::GpsNavigationStatus status ) { mNavigationStatus = status; } + /** * Returns whether the connection information is valid * \since QGIS 3.10 @@ -230,6 +248,7 @@ class CORE_EXPORT QgsGpsInformation private: QMap< Qgis::GnssConstellation, Qgis::GpsFixStatus > mConstellationFixStatus; + Qgis::GpsNavigationStatus mNavigationStatus = Qgis::GpsNavigationStatus::NotValid; friend class QgsNmeaConnection; friend class QgsQtLocationConnection; diff --git a/src/core/gps/qgsnmeaconnection.cpp b/src/core/gps/qgsnmeaconnection.cpp index 72dc8fc7cb396..83613f47bf44a 100644 --- a/src/core/gps/qgsnmeaconnection.cpp +++ b/src/core/gps/qgsnmeaconnection.cpp @@ -377,6 +377,23 @@ void QgsNmeaConnection::processRmcSentence( const char *data, int len ) mLastGPSInformation.qualityIndicator = Qgis::GpsQualityIndicator::Invalid; } } + + if ( result.navstatus == 'S' ) + { + mLastGPSInformation.setNavigationStatus( Qgis::GpsNavigationStatus::Safe ); + } + else if ( result.navstatus == 'C' ) + { + mLastGPSInformation.setNavigationStatus( Qgis::GpsNavigationStatus::Caution ); + } + else if ( result.navstatus == 'U' ) + { + mLastGPSInformation.setNavigationStatus( Qgis::GpsNavigationStatus::Unsafe ); + } + else + { + mLastGPSInformation.setNavigationStatus( Qgis::GpsNavigationStatus::NotValid ); + } } void QgsNmeaConnection::processGsvSentence( const char *data, int len ) diff --git a/src/core/qgis.h b/src/core/qgis.h index fd45f0bf08a61..50e7c39eb6298 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -1522,6 +1522,20 @@ class CORE_EXPORT Qgis }; Q_ENUM( GpsQualityIndicator ) + /** + * GPS navigation status. + * + * \since QGIS 3.38 + */ + enum class GpsNavigationStatus : int + { + NotValid, //!< Navigation status not valid + Safe, //!< Safe + Caution, //!< Caution + Unsafe, //!< Unsafe + }; + Q_ENUM( GpsNavigationStatus ); + /** * GPS information component. * From 0360367b1ecb351a7080f38a738c35a7e4f46552 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 1 May 2024 11:26:42 +1000 Subject: [PATCH 044/102] Handle url link clicks correctly in history widget --- src/gui/history/qgshistorywidget.cpp | 16 +++++++++++++++- src/gui/history/qgshistorywidget.h | 1 + 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/gui/history/qgshistorywidget.cpp b/src/gui/history/qgshistorywidget.cpp index 83fd387e8bf8c..e252a11e681ca 100644 --- a/src/gui/history/qgshistorywidget.cpp +++ b/src/gui/history/qgshistorywidget.cpp @@ -18,10 +18,13 @@ #include "qgshistoryentrymodel.h" #include "qgshistoryentrynode.h" #include "qgssettings.h" +#include "qgsnative.h" #include #include #include +#include +#include QgsHistoryWidget::QgsHistoryWidget( const QString &providerId, Qgis::HistoryProviderBackends backends, QgsHistoryProviderRegistry *registry, const QgsHistoryWidgetContext &context, QWidget *parent ) : QgsPanelWidget( parent ) @@ -71,8 +74,10 @@ void QgsHistoryWidget::currentItemChanged( const QModelIndex &selected, const QM if ( !html.isEmpty() ) { QTextBrowser *htmlBrowser = new QTextBrowser(); - htmlBrowser->setOpenExternalLinks( true ); + htmlBrowser->setOpenLinks( false ); htmlBrowser->setHtml( html ); + connect( htmlBrowser, &QTextBrowser::anchorClicked, this, &QgsHistoryWidget::urlClicked ); + newWidget = htmlBrowser; } } @@ -124,6 +129,15 @@ void QgsHistoryWidget::showNodeContextMenu( const QPoint &pos ) } } +void QgsHistoryWidget::urlClicked( const QUrl &url ) +{ + const QFileInfo file( url.toLocalFile() ); + if ( file.exists() && !file.isDir() ) + QgsGui::nativePlatformInterface()->openFileExplorerAndSelectFile( url.toLocalFile() ); + else + QDesktopServices::openUrl( url ); +} + // // QgsHistoryEntryProxyModel // diff --git a/src/gui/history/qgshistorywidget.h b/src/gui/history/qgshistorywidget.h index 095de0ad925d5..13fa94f58548f 100644 --- a/src/gui/history/qgshistorywidget.h +++ b/src/gui/history/qgshistorywidget.h @@ -81,6 +81,7 @@ class GUI_EXPORT QgsHistoryWidget : public QgsPanelWidget, private Ui::QgsHistor void currentItemChanged( const QModelIndex &selected, const QModelIndex &previous ); void nodeDoubleClicked( const QModelIndex &index ); void showNodeContextMenu( const QPoint &pos ); + void urlClicked( const QUrl &url ); private: From e9976c2ee72f6437357e9c296a55eaa04e4a73dd Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 24 Apr 2023 16:05:35 +1000 Subject: [PATCH 045/102] [processing] Show more detail in history dialog Use a tree display for processing history entries, where the root item for each entry shows the full algorithm log when clicked, and the python/qgis_process commands are instead shown as child items This provides more useful information for users browsing the history, while still making the all the previous information available --- .../qgsprocessinghistoryprovider.sip.in | 2 + .../qgsprocessinghistoryprovider.sip.in | 2 + .../qgsprocessinghistoryprovider.cpp | 301 ++++++++++++++---- .../processing/qgsprocessinghistoryprovider.h | 3 +- 4 files changed, 244 insertions(+), 64 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/processing/qgsprocessinghistoryprovider.sip.in b/python/PyQt6/gui/auto_generated/processing/qgsprocessinghistoryprovider.sip.in index 0e2d50e421c81..6ebd566e919d1 100644 --- a/python/PyQt6/gui/auto_generated/processing/qgsprocessinghistoryprovider.sip.in +++ b/python/PyQt6/gui/auto_generated/processing/qgsprocessinghistoryprovider.sip.in @@ -35,6 +35,8 @@ This should only be called once -- calling multiple times will result in duplica virtual QgsHistoryEntryNode *createNodeForEntry( const QgsHistoryEntry &entry, const QgsHistoryWidgetContext &context ) /Factory/; + virtual void updateNodeForEntry( QgsHistoryEntryNode *node, const QgsHistoryEntry &entry, const QgsHistoryWidgetContext &context ); + signals: diff --git a/python/gui/auto_generated/processing/qgsprocessinghistoryprovider.sip.in b/python/gui/auto_generated/processing/qgsprocessinghistoryprovider.sip.in index 0e2d50e421c81..6ebd566e919d1 100644 --- a/python/gui/auto_generated/processing/qgsprocessinghistoryprovider.sip.in +++ b/python/gui/auto_generated/processing/qgsprocessinghistoryprovider.sip.in @@ -35,6 +35,8 @@ This should only be called once -- calling multiple times will result in duplica virtual QgsHistoryEntryNode *createNodeForEntry( const QgsHistoryEntry &entry, const QgsHistoryWidgetContext &context ) /Factory/; + virtual void updateNodeForEntry( QgsHistoryEntryNode *node, const QgsHistoryEntry &entry, const QgsHistoryWidgetContext &context ); + signals: diff --git a/src/gui/processing/qgsprocessinghistoryprovider.cpp b/src/gui/processing/qgsprocessinghistoryprovider.cpp index 2aa47a050bd5b..8b765780188c7 100644 --- a/src/gui/processing/qgsprocessinghistoryprovider.cpp +++ b/src/gui/processing/qgsprocessinghistoryprovider.cpp @@ -22,6 +22,8 @@ #include "qgshistoryentrynode.h" #include "qgsprocessingregistry.h" #include "qgscodeeditorpython.h" +#include "qgscodeeditorshell.h" +#include "qgscodeeditorjson.h" #include "qgsjsonutils.h" #include @@ -83,11 +85,12 @@ void QgsProcessingHistoryProvider::portOldLog() ///@cond PRIVATE -class ProcessingHistoryNode : public QgsHistoryEntryGroup + +class ProcessingHistoryBaseNode : public QgsHistoryEntryGroup { public: - ProcessingHistoryNode( const QgsHistoryEntry &entry, QgsProcessingHistoryProvider *provider ) + ProcessingHistoryBaseNode( const QgsHistoryEntry &entry, QgsProcessingHistoryProvider *provider ) : mEntry( entry ) , mAlgorithmId( mEntry.entry.value( "algorithm_id" ).toString() ) , mPythonCommand( mEntry.entry.value( "python_command" ).toString() ) @@ -100,65 +103,7 @@ class ProcessingHistoryNode : public QgsHistoryEntryGroup { const QVariantMap parametersMap = parameters.toMap(); mInputs = parametersMap.value( QStringLiteral( "inputs" ) ).toMap(); - mDescription = QgsProcessingUtils::variantToPythonLiteral( mInputs ); - } - else - { - // an older history entry which didn't record inputs - mDescription = mPythonCommand; - } - - if ( mDescription.length() > 300 ) - { - mDescription = QObject::tr( "%1…" ).arg( mDescription.left( 299 ) ); - } - } - - QVariant data( int role = Qt::DisplayRole ) const override - { - if ( mAlgorithmInformation.displayName.isEmpty() ) - { - mAlgorithmInformation = QgsApplication::processingRegistry()->algorithmInformation( mAlgorithmId ); - } - - switch ( role ) - { - case Qt::DisplayRole: - { - const QString algName = mAlgorithmInformation.displayName; - if ( !mDescription.isEmpty() ) - return QStringLiteral( "[%1] %2 - %3" ).arg( mEntry.timestamp.toString( QStringLiteral( "yyyy-MM-dd hh:mm" ) ), - algName, - mDescription ); - else - return QStringLiteral( "[%1] %2" ).arg( mEntry.timestamp.toString( QStringLiteral( "yyyy-MM-dd hh:mm" ) ), - algName ); - } - - case Qt::DecorationRole: - { - return mAlgorithmInformation.icon; - } } - return QVariant(); - } - - QWidget *createWidget( const QgsHistoryWidgetContext & ) override - { - QgsCodeEditorPython *codeEditor = new QgsCodeEditorPython( ); - codeEditor->setReadOnly( true ); - codeEditor->setCaretLineVisible( false ); - codeEditor->setLineNumbersVisible( false ); - codeEditor->setFoldingVisible( false ); - codeEditor->setEdgeMode( QsciScintilla::EdgeNone ); - codeEditor->setWrapMode( QsciScintilla::WrapMode::WrapWord ); - - - const QString introText = QStringLiteral( "\"\"\"\n%1\n\"\"\"\n\n " ).arg( - QObject::tr( "Double-click on the history item or paste the command below to re-run the algorithm" ) ); - codeEditor->setText( introText + mPythonCommand ); - - return codeEditor; } bool doubleClicked( const QgsHistoryWidgetContext & ) override @@ -248,18 +193,248 @@ class ProcessingHistoryNode : public QgsHistoryEntryGroup QString mPythonCommand; QString mProcessCommand; QVariantMap mInputs; - QString mDescription; - mutable QgsProcessingAlgorithmInformation mAlgorithmInformation; QgsProcessingHistoryProvider *mProvider = nullptr; }; +class ProcessingHistoryPythonCommandNode : public ProcessingHistoryBaseNode +{ + public: + + ProcessingHistoryPythonCommandNode( const QgsHistoryEntry &entry, QgsProcessingHistoryProvider *provider ) + : ProcessingHistoryBaseNode( entry, provider ) + {} + + QVariant data( int role = Qt::DisplayRole ) const override + { + switch ( role ) + { + case Qt::DisplayRole: + { + QString display = mPythonCommand; + if ( display.length() > 300 ) + { + display = QObject::tr( "%1…" ).arg( display.left( 299 ) ); + } + return display; + } + case Qt::DecorationRole: + return QgsApplication::getThemeIcon( QStringLiteral( "mIconPythonFile.svg" ) ); + + default: + break; + } + return QVariant(); + } + + QWidget *createWidget( const QgsHistoryWidgetContext & ) override + { + QgsCodeEditorPython *codeEditor = new QgsCodeEditorPython( ); + codeEditor->setReadOnly( true ); + codeEditor->setCaretLineVisible( false ); + codeEditor->setLineNumbersVisible( false ); + codeEditor->setFoldingVisible( false ); + codeEditor->setEdgeMode( QsciScintilla::EdgeNone ); + codeEditor->setWrapMode( QsciScintilla::WrapMode::WrapWord ); + + + const QString introText = QStringLiteral( "\"\"\"\n%1\n\"\"\"\n\n " ).arg( + QObject::tr( "Double-click on the history item or paste the command below to re-run the algorithm" ) ); + codeEditor->setText( introText + mPythonCommand ); + + return codeEditor; + } +}; + +class ProcessingHistoryProcessCommandNode : public ProcessingHistoryBaseNode +{ + public: + + ProcessingHistoryProcessCommandNode( const QgsHistoryEntry &entry, QgsProcessingHistoryProvider *provider ) + : ProcessingHistoryBaseNode( entry, provider ) + {} + + QVariant data( int role = Qt::DisplayRole ) const override + { + switch ( role ) + { + case Qt::DisplayRole: + { + QString display = mProcessCommand; + if ( display.length() > 300 ) + { + display = QObject::tr( "%1…" ).arg( display.left( 299 ) ); + } + return display; + } + case Qt::DecorationRole: + return QgsApplication::getThemeIcon( QStringLiteral( "mActionTerminal.svg" ) ); + + default: + break; + } + return QVariant(); + } + + QWidget *createWidget( const QgsHistoryWidgetContext & ) override + { + QgsCodeEditorShell *codeEditor = new QgsCodeEditorShell( ); + codeEditor->setReadOnly( true ); + codeEditor->setCaretLineVisible( false ); + codeEditor->setLineNumbersVisible( false ); + codeEditor->setFoldingVisible( false ); + codeEditor->setEdgeMode( QsciScintilla::EdgeNone ); + codeEditor->setWrapMode( QsciScintilla::WrapMode::WrapWord ); + + codeEditor->setText( mProcessCommand ); + + return codeEditor; + } +}; + + +class ProcessingHistoryJsonNode : public ProcessingHistoryBaseNode +{ + public: + + ProcessingHistoryJsonNode( const QgsHistoryEntry &entry, QgsProcessingHistoryProvider *provider ) + : ProcessingHistoryBaseNode( entry, provider ) + { + mJson = QString::fromStdString( QgsJsonUtils::jsonFromVariant( mInputs ).dump( 2 ) ); + mJsonSingleLine = QString::fromStdString( QgsJsonUtils::jsonFromVariant( mInputs ).dump() ); + } + + QVariant data( int role = Qt::DisplayRole ) const override + { + switch ( role ) + { + case Qt::DisplayRole: + { + QString display = mJsonSingleLine; + if ( display.length() > 300 ) + { + display = QObject::tr( "%1…" ).arg( display.left( 299 ) ); + } + return display; + } + case Qt::DecorationRole: + return QgsApplication::getThemeIcon( QStringLiteral( "mIconFieldJson.svg" ) ); + + default: + break; + } + return QVariant(); + } + + QWidget *createWidget( const QgsHistoryWidgetContext & ) override + { + QgsCodeEditorJson *codeEditor = new QgsCodeEditorJson( ); + codeEditor->setReadOnly( true ); + codeEditor->setCaretLineVisible( false ); + codeEditor->setLineNumbersVisible( false ); + codeEditor->setFoldingVisible( false ); + codeEditor->setEdgeMode( QsciScintilla::EdgeNone ); + codeEditor->setWrapMode( QsciScintilla::WrapMode::WrapWord ); + + codeEditor->setText( mJson ); + + return codeEditor; + } + + QString mJson; + QString mJsonSingleLine; +}; + + +class ProcessingHistoryRootNode : public ProcessingHistoryBaseNode +{ + public: + + ProcessingHistoryRootNode( const QgsHistoryEntry &entry, QgsProcessingHistoryProvider *provider ) + : ProcessingHistoryBaseNode( entry, provider ) + { + const QVariant parameters = mEntry.entry.value( QStringLiteral( "parameters" ) ); + if ( parameters.type() == QVariant::Map ) + { + mDescription = QgsProcessingUtils::variantToPythonLiteral( mInputs ); + } + else + { + // an older history entry which didn't record inputs + mDescription = mPythonCommand; + } + + if ( mDescription.length() > 300 ) + { + mDescription = QObject::tr( "%1…" ).arg( mDescription.left( 299 ) ); + } + + addChild( new ProcessingHistoryPythonCommandNode( mEntry, mProvider ) ); + addChild( new ProcessingHistoryProcessCommandNode( mEntry, mProvider ) ); + addChild( new ProcessingHistoryJsonNode( mEntry, mProvider ) ); + } + + void setEntry( const QgsHistoryEntry &entry ) + { + mEntry = entry; + } + + QVariant data( int role = Qt::DisplayRole ) const override + { + if ( mAlgorithmInformation.displayName.isEmpty() ) + { + mAlgorithmInformation = QgsApplication::processingRegistry()->algorithmInformation( mAlgorithmId ); + } + + switch ( role ) + { + case Qt::DisplayRole: + { + const QString algName = mAlgorithmInformation.displayName; + if ( !mDescription.isEmpty() ) + return QStringLiteral( "[%1] %2 - %3" ).arg( mEntry.timestamp.toString( QStringLiteral( "yyyy-MM-dd hh:mm" ) ), + algName, + mDescription ); + else + return QStringLiteral( "[%1] %2" ).arg( mEntry.timestamp.toString( QStringLiteral( "yyyy-MM-dd hh:mm" ) ), + algName ); + } + + case Qt::DecorationRole: + { + return mAlgorithmInformation.icon; + } + + default: + break; + } + return QVariant(); + } + + QString html( const QgsHistoryWidgetContext & ) const override + { + return mEntry.entry.value( QStringLiteral( "log" ) ).toString(); + } + + QString mDescription; + mutable QgsProcessingAlgorithmInformation mAlgorithmInformation; + +}; + ///@endcond QgsHistoryEntryNode *QgsProcessingHistoryProvider::createNodeForEntry( const QgsHistoryEntry &entry, const QgsHistoryWidgetContext & ) { - return new ProcessingHistoryNode( entry, this ); + return new ProcessingHistoryRootNode( entry, this ); +} + +void QgsProcessingHistoryProvider::updateNodeForEntry( QgsHistoryEntryNode *node, const QgsHistoryEntry &entry, const QgsHistoryWidgetContext & ) +{ + if ( ProcessingHistoryRootNode *rootNode = dynamic_cast< ProcessingHistoryRootNode * >( node ) ) + { + rootNode->setEntry( entry ); + } } QString QgsProcessingHistoryProvider::oldLogPath() const diff --git a/src/gui/processing/qgsprocessinghistoryprovider.h b/src/gui/processing/qgsprocessinghistoryprovider.h index 03f74ddc8097c..c24fe3db6a1fa 100644 --- a/src/gui/processing/qgsprocessinghistoryprovider.h +++ b/src/gui/processing/qgsprocessinghistoryprovider.h @@ -45,6 +45,7 @@ class GUI_EXPORT QgsProcessingHistoryProvider : public QgsAbstractHistoryProvide void portOldLog(); QgsHistoryEntryNode *createNodeForEntry( const QgsHistoryEntry &entry, const QgsHistoryWidgetContext &context ) override SIP_FACTORY; + void updateNodeForEntry( QgsHistoryEntryNode *node, const QgsHistoryEntry &entry, const QgsHistoryWidgetContext &context ) override; signals: @@ -72,7 +73,7 @@ class GUI_EXPORT QgsProcessingHistoryProvider : public QgsAbstractHistoryProvide //! Returns the path to the old log file QString oldLogPath() const; - friend class ProcessingHistoryNode; + friend class ProcessingHistoryBaseNode; }; From 6d49c9b03aa2c05f0f91b23109f3d71db2106608 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 2 May 2024 10:40:49 +1000 Subject: [PATCH 046/102] Update test masks --- ...gend_node_flipped_horizontal_icon_mask.png | Bin 1229 -> 1378 bytes ..._ramp_legend_node_horizontal_icon_mask.png | Bin 2056 -> 2005 bytes ...ected_color_ramp_legend_node_icon_mask.png | Bin 3776 -> 4799 bytes ...mp_legend_node_prefix_suffix_icon_mask.png | Bin 4334 -> 5478 bytes ...or_ramp_legend_node_settings_icon_mask.png | Bin 3434 -> 4003 bytes 5 files changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/testdata/control_images/color_ramp_legend_node/expected_color_ramp_legend_node_flipped_horizontal_icon/expected_color_ramp_legend_node_flipped_horizontal_icon_mask.png b/tests/testdata/control_images/color_ramp_legend_node/expected_color_ramp_legend_node_flipped_horizontal_icon/expected_color_ramp_legend_node_flipped_horizontal_icon_mask.png index d33c15e8a0e594d5b80db77d5ac4276962a827d4..677c94a0bf09b2623132337e5f3b4d96b4fcc62c 100644 GIT binary patch literal 1378 zcmchX`BTyf7{vT*E>U006R&3i~lWuXdiB>hUBM*{40? zS$`=eu7paNIooe|fZ}d{#zaJ4u0$n};|{ud|KVX%J*{*hXA(6}`G#f%^Y^D^kZ+#^ zVQ(29zs~DJl=qL-Xn$$jJeNz#d9M!QzZEUj4!_9a*@1wKP`J&I%W8W!$ohW(ZC-Na z$_3SNnNEkJZbvLEE_OPxt5#Q6Z&o@uIv&Z&vhl8l2!+DDyu6P7ev7>k5v;1J&vnW@ zp=Zvt4JYcc(T#B@cWvk>-HV2VQ7Bk?-``%A%%Y?_#P*h0eI5l_x6 zFB4I_IS{#A?&RzonwjZs34z$SxO{EQ;IP>j*4NKWi1s#|C*BT^hyWK96yO2_OCCQC zt{u$VT znO>!*#>d-VEiZTTohP7Z#w_5Nnt#ke!=5rPsq5 z3`V8wyxGGYs9}p#7pq*0bQ3rn9=U&i-}LlvRLN>ST6S$RU7XCSsIZO)4-O6<9ksQy zW7YyVKR;W|0j6hxlypjG?DH_rX-m#``7jQbYg)K92`Fx!?LF#eFos*N1@RsswJu4kbam8RToJ%E= zNZtn*KYTDd_!jHsbu+Gc;N?p<#9spg);h*nzn?{LBVaH%@z9|jmC9yZtF2cZA5g0= zb$9QHlHdj1Jv~g_4U@EhfB+IWLn@V$E@q|M+S*F+W<4KaFc@j&xkBM9PF7I2*ThDg z<}8o7*RMkZm~N30Z7w}0XJ*(iI9N%WC(RkN#C&|~7D~v#U@&PdiIQ>i?i=&-C+>;G zdMaCz?P<^9@vh&4_@tkD;!;;vm%5g&bQCo;d6bryqkVi}B9UlBt95)`741f*P*mgN zS4&GbMG3m#EBH)0-7Y01B``3sV`{2l3&!8yuB@yK=Uf)s9Pi=j*}1T=z()d{u}l~_ zIDaz8?Lb^y@qM{GW_fwJm&fB3jw29=n6~1=KHF>tLvGxGdUO==q^<4xy`-%UN@TlF za!Se`lDI_Fh{Yzp$50teIHMFBjCQSbWJ*eQs*iy!ZUzx_I2}FhC!~P6S GEBXgR@p;|= literal 1229 zcmeAS@N?(olHy`uVBq!ia0vp^2|(=1!3HF^{(QU_NO2Z;L>4nJ@ErkR#;MwT(hLkN z^`0({Ar*7p&a(9=ikCPbZyay3Rf%KKi_^1uRvb|AShU`8#X5yNM+?@e0<2p@y}f;F zd35kqrz<5e$Yp4H9AwJk%3b<}{G%Rp4OK(bH?v z==$*E2S>+@DPG>*n^$ewyjj@H++0zD=g{ld+AW6@ckJ5Lv~;QJidCzAZ95o{xG^H7 zc&eVLn3&pBcLoPX0R!Q@?b44wdU-Aldid*C)h#QgMuuH6>$ZG-5`AjwwQJXod^$Dt zM~&Ulwo9ccm6efU=f8e6y_I9O<@Q?%9yUS2i|@ZD$H&Y6JW{)~At%{+VZbDYO#L@+ z-W<4dM zt7BVykwaf!-*W3@ev7$&8Tt9=t>hRP_QtK>;`G>oZ&~o^ty{L7C~J;dJMGcmx_UqD z=kb0kAD?iEjEqd$Q>55`T-o~bX2A(7g41$4+FFrVMfMMO5H4iRc6qJ*fe|+nf z6fnTN3O7dhWbP?pvO9HpdrL<}eZ7A4%Z~m3dGa$dCM4C`$tx+n`ugj=xIp*OOUFb- zMgK}LzT{wQ-t^NEau=f_?k!H0zR+naj${ zm%jFY-*Dcp<#R22ikH{p?&q~9`6Ogy&PWK?bNi{3rasaB{rmTYt5=s+_^-cy`csLO zEDt|FGeg4{%|i(W4M&qs-iRnK-~Q$ImoFms?%iARx~Yi?=t%#r_b*>IPMRbnAusQL z%3tJBfrZhw-};YBtPZ{`IrQ_V<(s!}4?caW>asY{VD?!9Gcz`EadFR&&d$yS)z#Xu z$_xwc=1sS?nbNnwB`AJV(Do<`nQ2o$R_$F8wmMMcsC?m{l;(p86CP=6X$2ksQ|P*K zes8d#YhYP@Qj$`xBa(5E#-iJA!{#_|zNvF`*4Z>0OUpp92j74H zU-9wC>-hb34`02~nm2du!xJYsu3f#lFl_bFM~{+n^74+|ycxM?=gyyoljrX^z{k&j z@$tt5cX#$_)2AZpf`Qnlmk`SbnPKK|a{zb#-L)1^|K*+oBR2-zDxu}TZQ k!{p5TKb(b7?tW%3%A@GAZOf)9!19T~)78&qol`;+0Ac_ng#Z8m diff --git a/tests/testdata/control_images/color_ramp_legend_node/expected_color_ramp_legend_node_horizontal_icon/expected_color_ramp_legend_node_horizontal_icon_mask.png b/tests/testdata/control_images/color_ramp_legend_node/expected_color_ramp_legend_node_horizontal_icon/expected_color_ramp_legend_node_horizontal_icon_mask.png index b4c264c442abe419625c1215fea42a7619cfdd23..3f7a5dd5f7c966778a43c1ab757cc6ffcf064279 100644 GIT binary patch delta 1838 zcmV+}2hsS55Y-QmIe&*qL_t(|ob8)?NKx>Zm#k&h0;PdnmTVq<+?!5gaXZ= z9HJ5VM}LG8bW!>*41$OvFrpIw(2^-#EF(17ykxgOEX}5v8hN2Dw@mZ0f!oqO?;o!_ zyyu<1uS&g-zC9lp?EKDoeml?kaGvM)oWleF2pH7xqJ09tJCVU#k?IeBZ~_Vn3P1>% z*XPx%S9H(%ClePJhuqv;l$DjCv$GS^)6=~E78MoED~~`Rz~RG(QBzZcFJHdE;c#GR zXb3M~zQpO%ry&xFJle2306>0zKH}oy004w|{%3K;iWUEwBOo9E&!0a>dU`rsE*BRU z2m}Ct(9lq{w6q{PI-1viPT>BRWLQ`jR;^k!uM-Xo48+>CYf)8IHLv5BD&F4SP^;DO z_4S2NC`5O6x5qK5si}yFh`@;xC+5r}6beyWTZ_TLL3DO@qOPtEKYsk+_D4rYBP%Nl z+qP{(Utb@xv$HWYGz0*cQ*ByW8iIm?X5IJs^Jjef_HCj4U2-UY6bk5cI&9y*9U_qk z>FMc6OiaYJYu7M5JUs6_d3kv#EiFYyM+XJ_AId2A!ygGU5jb9WVvwR0?wa552;iNLI{LH;k?@l1OlvFxsrGB_w3mNu~-ZM z@bdD)@#Dt<0K>z7!^q0YLU3>}LPA1-f4T!yDiu;vQXr8?FfuZN_wV1Mp`if)FfuZN z_V)J0_jrjw2!YXPgw19{OG^t{TU(*kYN6F?@%8K1dF62%`Lr%%|rbt|4eeLA;*rluyOq@?g-QBhHUh>wp)W@aV^1_l<_>m@{I zXD2pn*uaZTPfsr>M{H~?-o1Ori*D5|5JI>K`O%|CP^nbFKivV1 zjg4q*Y~;4x6P83G!HpX?pwVbBIXStYll?y;lgW4j!otGv=FJ=4G57D^hfpZwRYzlE zW58?z5)%`Dv3vJ!$Ye6uY&JAEH{;>Mhj2Qb(ChWk>-9)TNI*_b4vLG5e{JuUH~|3x z9wlmIWF+e9>UgpC_IAi*G7Jt5axr%m#7)9GcI-fXeLa$slX;DOe0)$+Qo@TF3!!bTS&Wo*Hz1rhAi^bxx2>bs1JMhIDFDxg3?1m5|9~v;I(ATpaf9-OG!KM4~xY%D)Q(0|VHyWeYE6u~-lvAJ2;&IB>wD zyj{0$9q{)mNT<`GtE&sOwYBK(?uM_gFH|aj7544h2ZzJKOAr$i!%Za1o!Hn|-ePGs z)M_>N-DotTx3?FgqoeTg@j-HOGM+tq2Fz4E2M->EQmI5;T^(X#VvvxK0E5B6wcoU9 z6ZhO{1!iXCii!$EMn(bvHgDdH?CfmV?RGRbHzOt{1{oO{T!N605UAB^G&ME->}x@P zh3xEX?A*B%0MOOdlu9L9TU#+PF#(gwgw)hj zl$V#|(xpr2?(T+GtL5cWsZ^-0t_Ivg5FrG8eSJ7`L2`}gC?lPACW+Wb;~ghC+$yO zTj+E;)YsR;U@-6=r($M-cJ$~`SglqhB_$y@Hy3yB-u*?F=D$HAk-%!TLZi`t;KPRx z^J=3|D3F|-j8mshak1RoTwJ_(5e9<+CX)%3m6ho4@8|Xh1_t8Lp+iVZOG8Ub3yvK- z#v9U!zrQ~*nM~x)ojatwyqp*e1~NK2N~Wf!h}CK(Jv}|-_U+rGp`n3{j*gP7tSmC~ z9${I3BoYZBgpjJLDzXrYii$jca-BSRlGyEbQdwC^YHDhT*=#0Gr;`j150lrgUz00W zt`MD0M}mTa{@(V_FJiHnXfzsf^X5&`+}uothK7jK=_Gc$otVvLqSxz5et!OJ36|~0 zEnp!$e*AdOd~&&*T)ldgbaZr(@$qqDx7&%)Xe8Cu)g&`BlL!O?k2c%~8!-kQ*gIj7 c-y;_04-v)7eprEulmGw#07*qoM6N<$f|ezIdH?_b delta 1895 zcmV-t2blQP4~P(uIe)rIL_t(|ob8)^NK;)L$G>wrb&gV;_M(O_<-3b8TQ$ms(0fYx z!VJQ{jUY;i3ZkNaq7V$qq(D=qs7Y#QvDDDaKL(0T8&1k8&T^SL-@r|D=lR3DbXz`; zNpbY)eBi=8zn$~rp7Yr~_nbQ>06@T?h7;`z_}gIuD8mF$hLOlxk?;zC6Of&q4MNDW zj<;{$(!A#1PE1S;GBYz#SXc8?^PQY%rb8&${ z008jw^Fv!(8zLeicK3JSuxbLW=KBNPfzS67G6pFcyd*Q36^9s>gdn3z*tKgH?%lhGmX?-f=Q(@!ED8z=P*YO_o6QC%CnsFJdKC!?3Fz+b#+55q z@a)+$?Ax~wj~+cjc6K%{UAhEdg;S71p;$??Rm-hgw{YXe4M?R@5JDgn3YXnhAP~UC z#RU}=6|mWC0D#PY%uI;IVgP`XlM`;KEeU|u;sJ|0G+kr%6|sNkj~02mt^!}$0(fFGtHFE1}7CMIJ4{{09J4#xEK zG_+bRa&vQ`)9Iko>2Ux4eWa$QA|oRM`T6;Os$XjYFE1~LC2Ck$80zcmd9lvUP6P)B zL$B9!vBgyocL(g*vj^|rzsG?C2Y8KLU0w0=>sp`f5(hbr7Kw_A;vOVxo#^Oj?i;xjN~MxJHJJd{c$nwpw^%K*?OJv|+J z_wEG%ba!`SWMqW*xu2gOu3x{-mCMV^(c9Y#0C@WJDIy~yA(zXc)oRh)+>FPMAEUUq z7`JcVhQVOq?W{L!*Z{d)j;g9E0N2=Qi-9VAue|6)G4&KwsK{Y$plYNPp&ML zN)a9&&Xx1?^O2E}fos>UL7`A!c6JuWjvYf}WF+i%J32Z#(A?Y%U}4!>US5uWo}M1w zLzx5w1Q4^?Op1z%$eA-|h)gCUo}QjWC=?QjL_$JCLrF$P25D?;B-Pc`#A2}!l}bgh z@G5)F9ukR!5JE_Da`Lj+EEWryo0}t3Q&Xg; zr-wXw@`N-tHvVr0|9b(Im6haI_)!1=a{l~zVzpXHX=y2`t*s?PLqlYCc9x8cjF8sW zR&w|5U82!wh_|=*k8KxD#9}c?Nl77jd3i*u)so@iVKO^AORQEa85$ZuBClS(B3W5k zKTEJ?k3|8$LUncZlKDbHLP$w?lgUJ?s;Wp@S{e}u1P*PuU&I)6 hV801dk>Dd1{{V}8FYH2cG!p;-002ovPDHLkV1o7+d~W~% diff --git a/tests/testdata/control_images/color_ramp_legend_node/expected_color_ramp_legend_node_icon/set1/expected_color_ramp_legend_node_icon_mask.png b/tests/testdata/control_images/color_ramp_legend_node/expected_color_ramp_legend_node_icon/set1/expected_color_ramp_legend_node_icon_mask.png index b648b4f094ae084402e89d24938d75dd96bda889..099bacb0324470a541b4133d0517865c01b53fcb 100644 GIT binary patch literal 4799 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 literal 3776 zcmd5<_g7QR77Z;Fg-|471QJ1tNH_FQ6r_eO{i6#}AaoGvMGPHLAT$LP6h)**R9Zla zD4oz-5~T}D7Yx43dhf0E{)G3#+_~kfnKLtcpS^FgnTZ}d8;lJE0sSDNInZ2K zz`!;6rwSJ6SpD_wfq(<4*L-;!1mXxZ(7Adetmwz%2sG(CyjydA^L-mx{4Cwo zw`})udLE zJzXmq!O+G4m8Hn&n1iDyEd%=z3txyq-!2r7@A;timwrdi4+wynK`f`iWj{w|L11^G zJgsv#Iv5QvZ8mLlAf9zL5C~Jv9M6aJN4el|zkStgvsE!>ABdrdR|70fm)dxoZ_!O7 z#PyY&e(g%{VaCoQp5Rr^RUYeJ?Cfr7$9B8g;Wtrr=I z+S%D@%MegQNJ+7u1~V)NtNz^I$BmBOP!3r=<1NT{GKw8O_*zHY6j=#I66B!tG!4PEF2tQ;N>Z(WGwr6MJ+u&=F#zS zZ3BbPrM;(FSYlJp%Cx7SS90l0;e{4DeGFyqFV=)`adBC1dtfjflXVzZ7Z;GC-`wd_ zXQdgNogi6RS=HX-(n*<_xQq;rq>PLw7S37Vmlm&BVnRX`KD>W#XY62Z9w#Lw#U~(u zRnR~p&%E=Q6gp*7LAp0jBJu8qH%rb=HF(#&d6UKx@vHM}!0d+*Xe$oKyfInFOb03s zqp&{HsI*SSLEyR=%+jT4?I~*!&F+9@GC9t~#6(AVVB6NtF4@4qASGmd`W=Kuqg4{R zM@D!wX*-sqE5Y&(Ioa8W?f{*G^~pNc7&kYepmwoc#l(R+`Fsm z>$7EJf#)(K{4+fM?AYk&LL39LEE1XBQ=F3{QXvc+V?{+pO36@ZTpV+poRE-kaJ1PMjeHs_4OZoR|#I`g!1ybnwW5G%rzG#6>7$`_|z*Qkqd=>Ey>^nI>&$h zktEN}%^e&cZSt(l&d$n7N~ZN}0vo=&yQ?z+5G^VH&u6jXPY)kHJR;zRaX|qA!~xB} z{6RrMasc}jVCf>#l9H$ABR18AXb(k<&aBzi-ei5E5e2ifu#iq?+ z!r^dVdW#giq2#5zf^~IuzYcy;4}cA&EG!@j(4smz$b59|PS8?+PELMNQQGg{zpdF< zS65Svjg5P@n>v^zcB%j_4isx<0*edV-*7zB82FtnE405zCB(7t$Y2t zT}x_kq5&HHsSY#1Ee#YD@hggoxp`KYH(Kzh9rJdXS%*X%jz{3#9$lwN)i@RJ9GzSo zxy9;y<0c zCBu_O5~X*y7cZq4%uH@=1-gBspDAPy3=FI;DFK~gW4r00KYg+B!2`~n?d?a2-QC^O zeS(JYw>33c%Kkr5nE+`T1E{)^;^K&0%CAFVsS*2_2Kyh4SO-3dR5v#_1VE9luC62$fA{C! zp<75ZJ3CfjS+yrAP@asLe=Sskz|Nm1k5xM4R#Uq(I|$fmm&sZWP97cxCnu-c$gxA= zuK9UqD3qTceb8Vp)%fuv11Pq*7|w;y%DV7zfUp>S^y8huKCcVR)5zQ8cJTBn{=Pw# z(!)oOSYr$gQz+al{+lV6rKS5RcuRLbB#dgiq)ANO~2R&$UagqBn z@4$8*9_Uoff7~J)ZBr=jIqx`!3|0Px9({o5TKcvnOLf_ zJ{o;*sW?XPJ`N`)DSY7q9dJViYBEY$#WgQ438a#ec0`0)mM97}Q#%|M9?mU; z2CAn2>gXlgmPnQI8xkOpe_&udP@UQVFp&YKSG#qx6;3N)>t0N#go7RHJkx!;R@T-! zh{UTt-rm}9IGplYw;*Qhja}HmPuFLUb8>QS$?{ddcmW2qO^RG$Q{gPe*;r1(@vfpZ1P4M>hqU%&7^Xh~LBI3Wqb&feKqsO0cxXlN*G z>ze@u+3Gv1y1u&FJ~U*$zPZ^Q7e|M>v%&8>QB4=3`gna1=?*d2gWj^U%U{p#-A1kM zhTnK~V`6X+5kccznyfVPUNF6$uHp z>FMd3>T1q}_;|o#JZo*$1j>jzAgZ+H}@uy@Sou;ZP8N+%s1*;#=* zn{(+(esiBrHhX1-cm>mM`_>4sA2LEGlEbwyM`!qTdAZz26a@0|@)8<$Y>B)(Qz0Q@ zV{dPm9YStrvV$cMWA!oxJLl${_yq(y0TFsrRn^_rriJ7?Egzwe=_Zq1^ZTMABdZAn zXbf%F=0u}3j}I8u*4DbCo}c~4)|SuO#)e9&u&HTng{SKZpI`jl@v7C=ubdJ!W9B)3-e~5LM8M9n zxu_YJ4`kKFhh{mqaxmgt;2L4ZY-w+=`fC3+5j#!5ekerq`hWo=S$OrthxV4hM9sh? z**N*~`>fel7100dGyiQ>|9?WZ%~)?#TAMBxbMViK+8?4wp{N`59$!%>c>>=?eH2ZX zF@wzivB-vHLVo}g9rlDdgDUBY8ekasJAE^QnuN4yJyVwcK>;v8P_eh#GaPzCJ9<-K zIZETJ?9175`5Fe(?%vT3PjyQy(4ssb5CV|!^*+xld3~POls)SvAA-4vK8n+QwxW+% zl`|N0vPpCZ_>GL%rMpHnieeJipGVkC`%1!u@stdaU;#wr7+(KYm+M?+F^RGjjkk|W zNkm-d+zk5#j3rI~xSYLRtAy!kr6El*AMJvKb*C%A(&e9?VsNVdh%~WpQD>HJpl1So zxs?rbUF+ESh3wqyXtTEI~Q%}2?eM!=NjB$0@WA3K>=Un#yn&r8?_}P2%rBP<9`07!H znY#1_yAdY_ynUQRh?(l^mlpTAQ3I9%!^O*-$e}wErTcORgZ4?)N32NM;(-l06Xw9jtFB^kASX}rt9E}IOo+!UUs*kHx7m6Ao4JgrN* zA38ZXkZ!~&RhO>96`y=MvF%hrg0Fg40&6hX6>I9VHtZ#z|3*VU?HvvYOIQ30&Q^!Z z=V@U$DN^_S12l3vk`H$LeneZY_}>EJoJ^D9rg>U$`ck-k<8an7DkqSMF2$L`SW2I> z(VSSJxkB!pvYMc^GaAYnLg*xNdKcBp?JKRDsM#Cv&Vf7&{-`=RTB}8W9;fRbx8}zb z{P3$82^J${>>n%NCOf;ln#4k;3+S{Rh0lWipq~pe`+R8z?YUw9WzML{M0n+va|~VW qS#(KgyLFsKkJw25=@TR#Gy4_(E-r$@8i8L@kim5mowwIq;{OH2{234c diff --git a/tests/testdata/control_images/color_ramp_legend_node/expected_color_ramp_legend_node_prefix_suffix_icon/set1/expected_color_ramp_legend_node_prefix_suffix_icon_mask.png b/tests/testdata/control_images/color_ramp_legend_node/expected_color_ramp_legend_node_prefix_suffix_icon/set1/expected_color_ramp_legend_node_prefix_suffix_icon_mask.png index 4bf39b9e5db4efd3bb07937466f5cdbb51d178bb..27e5ee08585a3c4a8dcb5763c6bd80edd6954a17 100644 GIT binary patch literal 5478 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 jm!_tO#tWl_%`R9WoJ7+jNuQAVdELkSKoF literal 4334 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 Date: Thu, 2 May 2024 10:25:24 +1000 Subject: [PATCH 047/102] Move responsibility for creating vector legend nodes to renderer This will allow renderer subclasses to create legend nodes which aren't QgsSymbolLegendNodes. --- .../symbology/qgsrenderer.sip.in | 15 +++++++++++++ .../symbology/qgsrenderer.sip.in | 15 +++++++++++++ src/core/qgsmaplayerlegend.cpp | 18 ++++++--------- src/core/symbology/qgsrenderer.cpp | 22 +++++++++++++++++++ src/core/symbology/qgsrenderer.h | 17 +++++++++++++- 5 files changed, 75 insertions(+), 12 deletions(-) diff --git a/python/PyQt6/core/auto_generated/symbology/qgsrenderer.sip.in b/python/PyQt6/core/auto_generated/symbology/qgsrenderer.sip.in index e8bb6baafda7b..281ea31e2e522 100644 --- a/python/PyQt6/core/auto_generated/symbology/qgsrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/symbology/qgsrenderer.sip.in @@ -358,7 +358,22 @@ the features displayed using that key. %Docstring Returns a list of symbology items for the legend +.. seealso:: :py:func:`createLayerTreeModelLegendNodes` + .. seealso:: :py:func:`legendKeys` +%End + + virtual QList createLegendNodes( QgsLayerTreeLayer *nodeLayer ) const /Factory/; +%Docstring +Returns a list of legend nodes to be used for the legend for the renderer. + +Ownership is transferred to the caller. + +The default implementation creates a legend node for each symbol item returned by :py:func:`~QgsFeatureRenderer.legendSymbolItems` + +.. seealso:: :py:func:`legendSymbolItems` + +.. versionadded:: 3.38 %End virtual QString legendClassificationAttribute() const; diff --git a/python/core/auto_generated/symbology/qgsrenderer.sip.in b/python/core/auto_generated/symbology/qgsrenderer.sip.in index 0722a00a5fe6f..97c4ab95b50d4 100644 --- a/python/core/auto_generated/symbology/qgsrenderer.sip.in +++ b/python/core/auto_generated/symbology/qgsrenderer.sip.in @@ -358,7 +358,22 @@ the features displayed using that key. %Docstring Returns a list of symbology items for the legend +.. seealso:: :py:func:`createLayerTreeModelLegendNodes` + .. seealso:: :py:func:`legendKeys` +%End + + virtual QList createLegendNodes( QgsLayerTreeLayer *nodeLayer ) const /Factory/; +%Docstring +Returns a list of legend nodes to be used for the legend for the renderer. + +Ownership is transferred to the caller. + +The default implementation creates a legend node for each symbol item returned by :py:func:`~QgsFeatureRenderer.legendSymbolItems` + +.. seealso:: :py:func:`legendSymbolItems` + +.. versionadded:: 3.38 %End virtual QString legendClassificationAttribute() const; diff --git a/src/core/qgsmaplayerlegend.cpp b/src/core/qgsmaplayerlegend.cpp index 336220fb410a6..4ea0c91d76fb5 100644 --- a/src/core/qgsmaplayerlegend.cpp +++ b/src/core/qgsmaplayerlegend.cpp @@ -384,27 +384,24 @@ QList QgsDefaultVectorLayerLegend::createLayerTre nodes.append( new QgsSimpleLegendNode( nodeLayer, r->legendClassificationAttribute() ) ); } - const auto constLegendSymbolItems = r->legendSymbolItems(); - for ( const QgsLegendSymbolItem &i : constLegendSymbolItems ) + const QList rendererNodes = r->createLegendNodes( nodeLayer ); + for ( QgsLayerTreeModelLegendNode *node : rendererNodes ) { - if ( auto *lDataDefinedSizeLegendSettings = i.dataDefinedSizeLegendSettings() ) - nodes << new QgsDataDefinedSizeLegendNode( nodeLayer, *lDataDefinedSizeLegendSettings ); - else + if ( QgsSymbolLegendNode *legendNode = qobject_cast< QgsSymbolLegendNode *>( node ) ) { - QgsSymbolLegendNode *legendNode = new QgsSymbolLegendNode( nodeLayer, i ); - if ( mTextOnSymbolEnabled && mTextOnSymbolContent.contains( i.ruleKey() ) ) + const QString ruleKey = legendNode->data( static_cast< int >( QgsLayerTreeModelLegendNode::CustomRole::RuleKey ) ).toString(); + if ( mTextOnSymbolEnabled && mTextOnSymbolContent.contains( ruleKey ) ) { - legendNode->setTextOnSymbolLabel( mTextOnSymbolContent.value( i.ruleKey() ) ); + legendNode->setTextOnSymbolLabel( mTextOnSymbolContent.value( ruleKey ) ); legendNode->setTextOnSymbolTextFormat( mTextOnSymbolTextFormat ); } - nodes << legendNode; } + nodes << node; } if ( nodes.count() == 1 && nodes[0]->data( Qt::EditRole ).toString().isEmpty() ) nodes[0]->setEmbeddedInParent( true ); - if ( mLayer->diagramsEnabled() ) { const auto constLegendItems = mLayer->diagramRenderer()->legendItems( nodeLayer ); @@ -440,7 +437,6 @@ QList QgsDefaultVectorLayerLegend::createLayerTre } } - return nodes; } diff --git a/src/core/symbology/qgsrenderer.cpp b/src/core/symbology/qgsrenderer.cpp index 2ba71427ef356..6d10414b99a19 100644 --- a/src/core/symbology/qgsrenderer.cpp +++ b/src/core/symbology/qgsrenderer.cpp @@ -32,6 +32,7 @@ #include "qgsapplication.h" #include "qgsmarkersymbol.h" #include "qgslinesymbol.h" +#include "qgslayertreemodellegendnode.h" #include #include @@ -392,6 +393,27 @@ QgsLegendSymbolList QgsFeatureRenderer::legendSymbolItems() const return QgsLegendSymbolList(); } +QList QgsFeatureRenderer::createLegendNodes( QgsLayerTreeLayer *nodeLayer ) const +{ + QList nodes; + + const QgsLegendSymbolList symbolItems = legendSymbolItems(); + nodes.reserve( symbolItems.size() ); + + for ( const QgsLegendSymbolItem &item : symbolItems ) + { + if ( const QgsDataDefinedSizeLegend *dataDefinedSizeLegendSettings = item.dataDefinedSizeLegendSettings() ) + { + nodes << new QgsDataDefinedSizeLegendNode( nodeLayer, *dataDefinedSizeLegendSettings ); + } + else + { + nodes << new QgsSymbolLegendNode( nodeLayer, item ); + } + } + return nodes; +} + void QgsFeatureRenderer::setVertexMarkerAppearance( Qgis::VertexMarkerType type, double size ) { mCurrentVertexMarkerType = type; diff --git a/src/core/symbology/qgsrenderer.h b/src/core/symbology/qgsrenderer.h index 8a6493f0573d6..cc64048032d94 100644 --- a/src/core/symbology/qgsrenderer.h +++ b/src/core/symbology/qgsrenderer.h @@ -38,6 +38,8 @@ class QgsPaintEffect; class QgsReadWriteContext; class QgsStyleEntityVisitorInterface; class QgsRenderContext; +class QgsLayerTreeModelLegendNode; +class QgsLayerTreeLayer; typedef QMap QgsStringMap SIP_SKIP; @@ -396,11 +398,24 @@ class CORE_EXPORT QgsFeatureRenderer /** * Returns a list of symbology items for the legend * + * \see createLayerTreeModelLegendNodes() * \see legendKeys() - * */ virtual QgsLegendSymbolList legendSymbolItems() const; + /** + * Returns a list of legend nodes to be used for the legend for the renderer. + * + * Ownership is transferred to the caller. + * + * The default implementation creates a legend node for each symbol item returned by legendSymbolItems() + * + * \see legendSymbolItems() + * + * \since QGIS 3.38 + */ + virtual QList createLegendNodes( QgsLayerTreeLayer *nodeLayer ) const SIP_FACTORY; + /** * If supported by the renderer, return classification attribute for the use in legend */ From 495e35aef87594533f15aae13d2e971c235511fa Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 2 May 2024 10:55:00 +1000 Subject: [PATCH 048/102] Only QgsSymbolLegendNode nodes can be embedded in parent in legend --- src/core/qgsmaplayerlegend.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/qgsmaplayerlegend.cpp b/src/core/qgsmaplayerlegend.cpp index 4ea0c91d76fb5..58f2557948167 100644 --- a/src/core/qgsmaplayerlegend.cpp +++ b/src/core/qgsmaplayerlegend.cpp @@ -399,7 +399,7 @@ QList QgsDefaultVectorLayerLegend::createLayerTre nodes << node; } - if ( nodes.count() == 1 && nodes[0]->data( Qt::EditRole ).toString().isEmpty() ) + if ( nodes.count() == 1 && nodes[0]->data( Qt::EditRole ).toString().isEmpty() && qobject_cast< QgsSymbolLegendNode * >( nodes[0] ) ) nodes[0]->setEmbeddedInParent( true ); if ( mLayer->diagramsEnabled() ) From 59bc838172fb0badd4259c6ed4990b9f30feb82f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 2 May 2024 11:38:19 +1000 Subject: [PATCH 049/102] Show a color ramp legend for vector heatmap layers Instead of showing no legend for these layers, show the color ramp as a gradient bar Fixes #54772 --- .../symbology/qgsheatmaprenderer.sip.in | 20 +++ .../symbology/qgsheatmaprenderer.sip.in | 20 +++ src/core/symbology/qgsheatmaprenderer.cpp | 28 +++- src/core/symbology/qgsheatmaprenderer.h | 20 +++ .../symbology/qgsheatmaprendererwidget.cpp | 31 ++++ src/gui/symbology/qgsheatmaprendererwidget.h | 1 + .../qgsheatmaprendererwidgetbase.ui | 150 +++++++++++------- tests/src/core/testqgslegendrenderer.cpp | 35 ++++ tests/src/python/CMakeLists.txt | 1 + tests/src/python/test_qgsheatmaprenderer.py | 78 +++++++++ .../expected_heatmap/expected_heatmap.png | Bin 0 -> 4312 bytes 11 files changed, 326 insertions(+), 58 deletions(-) create mode 100644 tests/src/python/test_qgsheatmaprenderer.py create mode 100644 tests/testdata/control_images/legend/expected_heatmap/expected_heatmap.png diff --git a/python/PyQt6/core/auto_generated/symbology/qgsheatmaprenderer.sip.in b/python/PyQt6/core/auto_generated/symbology/qgsheatmaprenderer.sip.in index a107425c98ea6..73149a0ab87db 100644 --- a/python/PyQt6/core/auto_generated/symbology/qgsheatmaprenderer.sip.in +++ b/python/PyQt6/core/auto_generated/symbology/qgsheatmaprenderer.sip.in @@ -60,6 +60,8 @@ Creates a new heatmap renderer instance from XML static QgsHeatmapRenderer *convertFromRenderer( const QgsFeatureRenderer *renderer ) /Factory/; virtual bool accept( QgsStyleEntityVisitorInterface *visitor ) const; + virtual QList createLegendNodes( QgsLayerTreeLayer *nodeLayer ) const /Factory/; + virtual void modifyRequestExtent( QgsRectangle &extent, QgsRenderContext &context ); @@ -81,6 +83,24 @@ Sets the color ramp to use for shading the heatmap. :param ramp: color ramp for heatmap. Ownership of ramp is transferred to the renderer. .. seealso:: :py:func:`colorRamp` +%End + + const QgsColorRampLegendNodeSettings &legendSettings() const; +%Docstring +Returns the color ramp legend settings. + +.. seealso:: :py:func:`setLegendSettings` + +.. versionadded:: 3.38 +%End + + void setLegendSettings( const QgsColorRampLegendNodeSettings &settings ); +%Docstring +Sets the color ramp legend ``settings``. + +.. seealso:: :py:func:`legendSettings` + +.. versionadded:: 3.38 %End double radius() const; diff --git a/python/core/auto_generated/symbology/qgsheatmaprenderer.sip.in b/python/core/auto_generated/symbology/qgsheatmaprenderer.sip.in index a107425c98ea6..73149a0ab87db 100644 --- a/python/core/auto_generated/symbology/qgsheatmaprenderer.sip.in +++ b/python/core/auto_generated/symbology/qgsheatmaprenderer.sip.in @@ -60,6 +60,8 @@ Creates a new heatmap renderer instance from XML static QgsHeatmapRenderer *convertFromRenderer( const QgsFeatureRenderer *renderer ) /Factory/; virtual bool accept( QgsStyleEntityVisitorInterface *visitor ) const; + virtual QList createLegendNodes( QgsLayerTreeLayer *nodeLayer ) const /Factory/; + virtual void modifyRequestExtent( QgsRectangle &extent, QgsRenderContext &context ); @@ -81,6 +83,24 @@ Sets the color ramp to use for shading the heatmap. :param ramp: color ramp for heatmap. Ownership of ramp is transferred to the renderer. .. seealso:: :py:func:`colorRamp` +%End + + const QgsColorRampLegendNodeSettings &legendSettings() const; +%Docstring +Returns the color ramp legend settings. + +.. seealso:: :py:func:`setLegendSettings` + +.. versionadded:: 3.38 +%End + + void setLegendSettings( const QgsColorRampLegendNodeSettings &settings ); +%Docstring +Sets the color ramp legend ``settings``. + +.. seealso:: :py:func:`legendSettings` + +.. versionadded:: 3.38 %End double radius() const; diff --git a/src/core/symbology/qgsheatmaprenderer.cpp b/src/core/symbology/qgsheatmaprenderer.cpp index d261983e80299..71bc8788ad6f8 100644 --- a/src/core/symbology/qgsheatmaprenderer.cpp +++ b/src/core/symbology/qgsheatmaprenderer.cpp @@ -23,6 +23,7 @@ #include "qgscolorrampimpl.h" #include "qgsrendercontext.h" #include "qgsstyleentityvisitor.h" +#include "qgscolorramplegendnode.h" #include #include @@ -31,6 +32,8 @@ QgsHeatmapRenderer::QgsHeatmapRenderer() : QgsFeatureRenderer( QStringLiteral( "heatmapRenderer" ) ) { mGradientRamp = new QgsGradientColorRamp( QColor( 255, 255, 255 ), QColor( 0, 0, 0 ) ); + mLegendSettings.setMinimumLabel( QObject::tr( "Minimum" ) ); + mLegendSettings.setMaximumLabel( QObject::tr( "Maximum" ) ); } QgsHeatmapRenderer::~QgsHeatmapRenderer() @@ -284,6 +287,7 @@ QgsHeatmapRenderer *QgsHeatmapRenderer::clone() const newRenderer->setMaximumValue( mExplicitMax ); newRenderer->setRenderQuality( mRenderQuality ); newRenderer->setWeightExpression( mWeightExpressionString ); + newRenderer->setLegendSettings( mLegendSettings ); copyRendererData( newRenderer ); return newRenderer; @@ -316,12 +320,16 @@ QgsFeatureRenderer *QgsHeatmapRenderer::create( QDomElement &element, const QgsR { r->setColorRamp( QgsSymbolLayerUtils::loadColorRamp( sourceColorRampElem ) ); } + + QgsColorRampLegendNodeSettings legendSettings; + legendSettings.readXml( element, context ); + r->setLegendSettings( legendSettings ); + return r; } QDomElement QgsHeatmapRenderer::save( QDomDocument &doc, const QgsReadWriteContext &context ) { - Q_UNUSED( context ) QDomElement rendererElem = doc.createElement( RENDERER_TAG_NAME ); rendererElem.setAttribute( QStringLiteral( "type" ), QStringLiteral( "heatmapRenderer" ) ); rendererElem.setAttribute( QStringLiteral( "radius" ), QString::number( mRadius ) ); @@ -336,6 +344,7 @@ QDomElement QgsHeatmapRenderer::save( QDomDocument &doc, const QgsReadWriteConte const QDomElement colorRampElem = QgsSymbolLayerUtils::saveColorRamp( QStringLiteral( "[source]" ), mGradientRamp, doc ); rendererElem.appendChild( colorRampElem ); } + mLegendSettings.writeXml( doc, rendererElem, context ); saveRendererData( doc, rendererElem, context ); @@ -395,8 +404,25 @@ bool QgsHeatmapRenderer::accept( QgsStyleEntityVisitorInterface *visitor ) const return true; } +QList QgsHeatmapRenderer::createLegendNodes( QgsLayerTreeLayer *nodeLayer ) const +{ + return + { + new QgsColorRampLegendNode( nodeLayer, + mGradientRamp->clone(), + mLegendSettings, + 0, + 1 ) + }; +} + void QgsHeatmapRenderer::setColorRamp( QgsColorRamp *ramp ) { delete mGradientRamp; mGradientRamp = ramp; } + +void QgsHeatmapRenderer::setLegendSettings( const QgsColorRampLegendNodeSettings &settings ) +{ + mLegendSettings = settings; +} diff --git a/src/core/symbology/qgsheatmaprenderer.h b/src/core/symbology/qgsheatmaprenderer.h index b78f1cd4c72bb..9f275e46fe62a 100644 --- a/src/core/symbology/qgsheatmaprenderer.h +++ b/src/core/symbology/qgsheatmaprenderer.h @@ -22,6 +22,7 @@ #include "qgsgeometry.h" #include "qgsmapunitscale.h" #include "qgis.h" +#include "qgscolorramplegendnodesettings.h" class QgsColorRamp; @@ -58,6 +59,7 @@ class CORE_EXPORT QgsHeatmapRenderer : public QgsFeatureRenderer QDomElement save( QDomDocument &doc, const QgsReadWriteContext &context ) override; static QgsHeatmapRenderer *convertFromRenderer( const QgsFeatureRenderer *renderer ) SIP_FACTORY; bool accept( QgsStyleEntityVisitorInterface *visitor ) const override; + QList createLegendNodes( QgsLayerTreeLayer *nodeLayer ) const override SIP_FACTORY; //reimplemented to extent the request so that points up to heatmap's radius distance outside //visible area are included @@ -79,6 +81,22 @@ class CORE_EXPORT QgsHeatmapRenderer : public QgsFeatureRenderer */ void setColorRamp( QgsColorRamp *ramp SIP_TRANSFER ); + /** + * Returns the color ramp legend settings. + * + * \see setLegendSettings() + * \since QGIS 3.38 + */ + const QgsColorRampLegendNodeSettings &legendSettings() const { return mLegendSettings; } + + /** + * Sets the color ramp legend \a settings. + * + * \see legendSettings() + * \since QGIS 3.38 + */ + void setLegendSettings( const QgsColorRampLegendNodeSettings &settings ); + /** * Returns the radius for the heatmap * \returns heatmap radius @@ -202,6 +220,8 @@ class CORE_EXPORT QgsHeatmapRenderer : public QgsFeatureRenderer int mFeaturesRendered = 0; + QgsColorRampLegendNodeSettings mLegendSettings; + double uniformKernel( double distance, int bandwidth ) const; double quarticKernel( double distance, int bandwidth ) const; double triweightKernel( double distance, int bandwidth ) const; diff --git a/src/gui/symbology/qgsheatmaprendererwidget.cpp b/src/gui/symbology/qgsheatmaprendererwidget.cpp index 566ebd128d380..953ed9e2c04bd 100644 --- a/src/gui/symbology/qgsheatmaprendererwidget.cpp +++ b/src/gui/symbology/qgsheatmaprendererwidget.cpp @@ -22,6 +22,7 @@ #include "qgsstyle.h" #include "qgsproject.h" #include "qgsmapcanvas.h" +#include "qgscolorramplegendnodewidget.h" #include #include @@ -116,6 +117,7 @@ QgsHeatmapRendererWidget::QgsHeatmapRendererWidget( QgsVectorLayer *layer, QgsSt btnColorRamp->setShowGradientOnly( true ); connect( btnColorRamp, &QgsColorRampButton::colorRampChanged, this, &QgsHeatmapRendererWidget::applyColorRamp ); + connect( mLegendSettingsButton, &QPushButton::clicked, this, &QgsHeatmapRendererWidget::showLegendSettings ); if ( mRenderer->colorRamp() ) { @@ -171,6 +173,35 @@ void QgsHeatmapRendererWidget::applyColorRamp() emit widgetChanged(); } +void QgsHeatmapRendererWidget::showLegendSettings() +{ + QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( qobject_cast< QWidget * >( parent() ) ); + if ( panel && panel->dockMode() ) + { + QgsColorRampLegendNodeWidget *legendPanel = new QgsColorRampLegendNodeWidget(); + legendPanel->setUseContinuousRampCheckBoxVisibility( false ); + legendPanel->setPanelTitle( tr( "Legend Settings" ) ); + legendPanel->setSettings( mRenderer->legendSettings() ); + connect( legendPanel, &QgsColorRampLegendNodeWidget::widgetChanged, this, [ = ] + { + mRenderer->setLegendSettings( legendPanel->settings() ); + emit widgetChanged(); + } ); + panel->openPanel( legendPanel ); + } + else + { + QgsColorRampLegendNodeDialog dialog( mRenderer->legendSettings(), this ); + dialog.setUseContinuousRampCheckBoxVisibility( false ); + dialog.setWindowTitle( tr( "Legend Settings" ) ); + if ( dialog.exec() ) + { + mRenderer->setLegendSettings( dialog.settings() ); + emit widgetChanged(); + } + } +} + void QgsHeatmapRendererWidget::mRadiusUnitWidget_changed() { if ( !mRenderer ) diff --git a/src/gui/symbology/qgsheatmaprendererwidget.h b/src/gui/symbology/qgsheatmaprendererwidget.h index 1f0681012096b..d17645f1ac950 100644 --- a/src/gui/symbology/qgsheatmaprendererwidget.h +++ b/src/gui/symbology/qgsheatmaprendererwidget.h @@ -61,6 +61,7 @@ class GUI_EXPORT QgsHeatmapRendererWidget : public QgsRendererWidget, private Ui private slots: void applyColorRamp(); + void showLegendSettings(); void mRadiusUnitWidget_changed(); void mRadiusSpinBox_valueChanged( double d ); void mMaxSpinBox_valueChanged( double d ); diff --git a/src/ui/symbollayer/qgsheatmaprendererwidgetbase.ui b/src/ui/symbollayer/qgsheatmaprendererwidgetbase.ui index b92dc98f27fcd..42fdcbe34f772 100644 --- a/src/ui/symbollayer/qgsheatmaprendererwidgetbase.ui +++ b/src/ui/symbollayer/qgsheatmaprendererwidgetbase.ui @@ -1,7 +1,7 @@ QgsHeatmapRendererWidgetBase - + 0 @@ -14,38 +14,32 @@ Form - - - - Qt::Vertical - - - - 20 - 40 - + + + + Color ramp - +
- - + + - - 1 + + 0 0 - - Automatic - - - 6 - - - 99999999.000000000000000 + + + 120 + 0 + - - 0.200000000000000 + + + 16777215 + 16777215 + @@ -56,13 +50,6 @@
- - - - Rendering quality - - - @@ -112,39 +99,32 @@ - - - - Color ramp - - - - - + + - - 0 + + 1 0 - - - 120 - 0 - + + Automatic - - - 16777215 - 16777215 - + + 6 + + + 99999999.000000000000000 + + + 0.200000000000000 - - + + - Maximum value + Rendering quality @@ -174,6 +154,19 @@ + + + + Qt::Vertical + + + + 20 + 40 + + + + @@ -200,6 +193,43 @@ + + + + Maximum value + + + + + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Legend Settings… + + + + + @@ -226,6 +256,12 @@
qgscolorrampbutton.h
1 + + QgsPanelWidth + QWidget +
qgspanelwidget.h
+ 1 +
btnColorRamp diff --git a/tests/src/core/testqgslegendrenderer.cpp b/tests/src/core/testqgslegendrenderer.cpp index 6c3868c80c2a9..788de1810fb28 100644 --- a/tests/src/core/testqgslegendrenderer.cpp +++ b/tests/src/core/testqgslegendrenderer.cpp @@ -48,6 +48,7 @@ #include "qgslinesymbol.h" #include "qgsmarkersymbol.h" #include "qgsfillsymbol.h" +#include "qgsheatmaprenderer.h" static void _setStandardTestFont( QgsLegendSettings &settings, const QString &style = QStringLiteral( "Roman" ) ) { @@ -206,6 +207,7 @@ class TestQgsLegendRenderer : public QgsTest void testBigMarkerJson(); void testLabelLegend(); + void testHeatmap(); private: QgsLayerTree *mRoot = nullptr; @@ -1804,6 +1806,39 @@ void TestQgsLegendRenderer::testLabelLegend() mVL3->setLabelsEnabled( bkLabelsEnabled ); } +void TestQgsLegendRenderer::testHeatmap() +{ + std::unique_ptr< QgsLayerTree > root( new QgsLayerTree() ); + + QgsVectorLayer *vl = new QgsVectorLayer( QStringLiteral( "Points" ), QStringLiteral( "Points" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( vl ); + QgsHeatmapRenderer *renderer = new QgsHeatmapRenderer(); + renderer->setColorRamp( new QgsGradientColorRamp( QColor( 255, 0, 0 ), QColor( 255, 200, 100 ) ) ); + QgsColorRampLegendNodeSettings rampSettings; + + QFont font( QgsFontUtils::getStandardTestFont( QStringLiteral( "Bold" ) ) ); + QgsTextFormat f; + f.setSize( 16 ); + f.setFont( font ); + rampSettings.setTextFormat( f ); + rampSettings.setMinimumLabel( "min" ); + rampSettings.setMaximumLabel( "max" ); + renderer->setLegendSettings( rampSettings ); + + vl->setRenderer( renderer ); + vl->setLegend( new QgsDefaultVectorLayerLegend( vl ) ); + root->addLayer( vl ); + + QgsLayerTreeModel legendModel( root.get() ); + QgsLegendSettings settings; + settings.rstyle( QgsLegendStyle::Style::Symbol ).setMargin( QgsLegendStyle::Side::Top, 9 ); + _setStandardTestFont( settings, QStringLiteral( "Bold" ) ); + const QImage res = _renderLegend( &legendModel, settings ); + + QgsProject::instance()->removeMapLayer( vl ); + QVERIFY( _verifyImage( res, QStringLiteral( "heatmap" ) ) ); +} + QGSTEST_MAIN( TestQgsLegendRenderer ) #include "testqgslegendrenderer.moc" diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 7b1402a7b3266..51f359d931161 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -106,6 +106,7 @@ ADD_PYTHON_TEST(PyQgsGraduatedSymbolRenderer test_qgsgraduatedsymbolrenderer.py) ADD_PYTHON_TEST(PyQgsGraph test_qgsgraph.py) ADD_PYTHON_TEST(PyQgsGroupLayer test_qgsgrouplayer.py) ADD_PYTHON_TEST(PyQgsHashLineSymbolLayer test_qgshashlinesymbollayer.py) +ADD_PYTHON_TEST(PyQgsHeatmapRenderer test_qgsheatmaprenderer.py) ADD_PYTHON_TEST(PyQgsHillshadeRenderer test_qgshillshaderenderer.py) ADD_PYTHON_TEST(PyQgsImageCache test_qgsimagecache.py) ADD_PYTHON_TEST(PyQgsInterpolatedLineSymbolLayer test_qgsinterpolatedlinesymbollayers.py) diff --git a/tests/src/python/test_qgsheatmaprenderer.py b/tests/src/python/test_qgsheatmaprenderer.py new file mode 100644 index 0000000000000..4dda8a235d9b2 --- /dev/null +++ b/tests/src/python/test_qgsheatmaprenderer.py @@ -0,0 +1,78 @@ +"""QGIS Unit tests for QgsHeatmapRenderer + +From build dir, run: ctest -R PyQgsHeatmapRenderer -V + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" + +import os + +from qgis.PyQt.QtGui import QColor +from qgis.PyQt.QtXml import QDomDocument +from qgis.core import ( + QgsHeatmapRenderer, + QgsGradientColorRamp, + QgsReadWriteContext, + QgsColorRampLegendNodeSettings +) +import unittest +from qgis.testing import start_app, QgisTestCase + +from utilities import unitTestDataPath + +start_app() + +TEST_DATA_DIR = unitTestDataPath() + + +class TestQgsHeatmapRenderer(QgisTestCase): + + def test_clone(self): + """ + Test cloning renderer + """ + + renderer = QgsHeatmapRenderer() + renderer.setColorRamp( + QgsGradientColorRamp(QColor(255, 0, 0), QColor(255, 200, 100))) + + legend_settings = QgsColorRampLegendNodeSettings() + legend_settings.setMaximumLabel('my max') + legend_settings.setMinimumLabel('my min') + renderer.setLegendSettings(legend_settings) + + renderer2 = renderer.clone() + self.assertEqual(renderer2.colorRamp().color1(), QColor(255, 0, 0)) + self.assertEqual(renderer2.colorRamp().color2(), QColor(255, 200, 100)) + self.assertEqual(renderer2.legendSettings().minimumLabel(), 'my min') + self.assertEqual(renderer2.legendSettings().maximumLabel(), 'my max') + + def test_write_read_xml(self): + """ + Test writing renderer to xml and restoring + """ + + renderer = QgsHeatmapRenderer() + renderer.setColorRamp( + QgsGradientColorRamp(QColor(255, 0, 0), QColor(255, 200, 100))) + + legend_settings = QgsColorRampLegendNodeSettings() + legend_settings.setMaximumLabel('my max') + legend_settings.setMinimumLabel('my min') + renderer.setLegendSettings(legend_settings) + + doc = QDomDocument("testdoc") + elem = renderer.save(doc, QgsReadWriteContext()) + + renderer2 = QgsHeatmapRenderer.create(elem, QgsReadWriteContext()) + self.assertEqual(renderer2.colorRamp().color1(), QColor(255, 0, 0)) + self.assertEqual(renderer2.colorRamp().color2(), QColor(255, 200, 100)) + self.assertEqual(renderer2.legendSettings().minimumLabel(), 'my min') + self.assertEqual(renderer2.legendSettings().maximumLabel(), 'my max') + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/testdata/control_images/legend/expected_heatmap/expected_heatmap.png b/tests/testdata/control_images/legend/expected_heatmap/expected_heatmap.png new file mode 100644 index 0000000000000000000000000000000000000000..dcb45625bd2b66da7bff11403bd92b16b077f429 GIT binary patch literal 4312 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 Caa|7p literal 0 HcmV?d00001 From a095dc5e4ec34cfbc1cf7b452f6e5279959977d2 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 2 May 2024 11:53:36 +1000 Subject: [PATCH 050/102] Hide settings which don't apply for heatmap legends --- .../qgscolorramplegendnodewidget.py | 14 +++++++ .../qgscolorramplegendnodewidget.sip.in | 24 +++++++++-- .../qgscolorramplegendnodewidget.py | 13 ++++++ .../qgscolorramplegendnodewidget.sip.in | 24 +++++++++-- src/gui/layout/qgslayoutlegendwidget.cpp | 1 + src/gui/qgscolorramplegendnodewidget.cpp | 41 ++++++++++++++++--- src/gui/qgscolorramplegendnodewidget.h | 36 ++++++++++++++-- .../symbology/qgsheatmaprendererwidget.cpp | 4 +- src/ui/qgscolorramplegendnodewidgetbase.ui | 8 ++-- 9 files changed, 143 insertions(+), 22 deletions(-) create mode 100644 python/PyQt6/gui/auto_additions/qgscolorramplegendnodewidget.py create mode 100644 python/gui/auto_additions/qgscolorramplegendnodewidget.py diff --git a/python/PyQt6/gui/auto_additions/qgscolorramplegendnodewidget.py b/python/PyQt6/gui/auto_additions/qgscolorramplegendnodewidget.py new file mode 100644 index 0000000000000..67618898a0322 --- /dev/null +++ b/python/PyQt6/gui/auto_additions/qgscolorramplegendnodewidget.py @@ -0,0 +1,14 @@ +# The following has been generated automatically from src/gui/qgscolorramplegendnodewidget.h +# monkey patching scoped based enum +QgsColorRampLegendNodeWidget.Capability.Prefix.__doc__ = "Allow editing legend prefix" +QgsColorRampLegendNodeWidget.Capability.Suffix.__doc__ = "Allow editing legend suffix" +QgsColorRampLegendNodeWidget.Capability.NumberFormat.__doc__ = "Allow editing number format" +QgsColorRampLegendNodeWidget.Capability.DefaultMinimum.__doc__ = "Allow resetting minimum label to default" +QgsColorRampLegendNodeWidget.Capability.DefaultMaximum.__doc__ = "Allow resetting maximum label to default" +QgsColorRampLegendNodeWidget.Capability.AllCapabilities.__doc__ = "All capabilities" +QgsColorRampLegendNodeWidget.Capability.__doc__ = "Capabilities to expose in the widget.\n\n.. versionadded:: 3.38\n\n" + '* ``Prefix``: ' + QgsColorRampLegendNodeWidget.Capability.Prefix.__doc__ + '\n' + '* ``Suffix``: ' + QgsColorRampLegendNodeWidget.Capability.Suffix.__doc__ + '\n' + '* ``NumberFormat``: ' + QgsColorRampLegendNodeWidget.Capability.NumberFormat.__doc__ + '\n' + '* ``DefaultMinimum``: ' + QgsColorRampLegendNodeWidget.Capability.DefaultMinimum.__doc__ + '\n' + '* ``DefaultMaximum``: ' + QgsColorRampLegendNodeWidget.Capability.DefaultMaximum.__doc__ + '\n' + '* ``AllCapabilities``: ' + QgsColorRampLegendNodeWidget.Capability.AllCapabilities.__doc__ +# -- +QgsColorRampLegendNodeWidget.Capability.baseClass = QgsColorRampLegendNodeWidget +QgsColorRampLegendNodeWidget.Capabilities = lambda flags=0: QgsColorRampLegendNodeWidget.Capability(flags) +QgsColorRampLegendNodeWidget.Capabilities.baseClass = QgsColorRampLegendNodeWidget +Capabilities = QgsColorRampLegendNodeWidget # dirty hack since SIP seems to introduce the flags in module diff --git a/python/PyQt6/gui/auto_generated/qgscolorramplegendnodewidget.sip.in b/python/PyQt6/gui/auto_generated/qgscolorramplegendnodewidget.sip.in index bbe8d561270b5..02af80c7d3cb4 100644 --- a/python/PyQt6/gui/auto_generated/qgscolorramplegendnodewidget.sip.in +++ b/python/PyQt6/gui/auto_generated/qgscolorramplegendnodewidget.sip.in @@ -11,7 +11,6 @@ - class QgsColorRampLegendNodeWidget: QgsPanelWidget { %Docstring(signature="appended") @@ -30,9 +29,24 @@ When changes are made the to settings by a user the :py:func:`~widgetChanged` si %End public: - QgsColorRampLegendNodeWidget( QWidget *parent = 0 ); + enum class Capability /BaseType=IntFlag/ + { + Prefix, + Suffix, + NumberFormat, + DefaultMinimum, + DefaultMaximum, + AllCapabilities, + }; + + typedef QFlags Capabilities; + + + QgsColorRampLegendNodeWidget( QWidget *parent = 0, QgsColorRampLegendNodeWidget::Capabilities capabilities = QgsColorRampLegendNodeWidget::Capability::AllCapabilities ); %Docstring Constructor for QgsColorRampLegendNodeWidget, with the specified ``parent`` widget. + +Since QGIS 3.38, the ``capabilities`` argument can be used to fine-tune settings exposed in the widget. %End QgsColorRampLegendNodeSettings settings() const; @@ -59,6 +73,8 @@ when using single band gray renderer). %End }; +QFlags operator|(QgsColorRampLegendNodeWidget::Capability f1, QFlags f2); + class QgsColorRampLegendNodeDialog : QDialog { @@ -73,9 +89,11 @@ A dialog for configuring a :py:class:`QgsColorRampLegendNode` (:py:class:`QgsCol %End public: - QgsColorRampLegendNodeDialog( const QgsColorRampLegendNodeSettings &settings, QWidget *parent /TransferThis/ = 0 ); + QgsColorRampLegendNodeDialog( const QgsColorRampLegendNodeSettings &settings, QWidget *parent /TransferThis/ = 0, QgsColorRampLegendNodeWidget::Capabilities capabilities = QgsColorRampLegendNodeWidget::Capability::AllCapabilities ); %Docstring Constructor for QgsColorRampLegendNodeDialog, initially showing the specified ``settings``. + +Since QGIS 3.38, the ``capabilities`` argument can be used to fine-tune settings exposed in the dialog. %End QgsColorRampLegendNodeSettings settings() const; diff --git a/python/gui/auto_additions/qgscolorramplegendnodewidget.py b/python/gui/auto_additions/qgscolorramplegendnodewidget.py new file mode 100644 index 0000000000000..e4590dcb25e26 --- /dev/null +++ b/python/gui/auto_additions/qgscolorramplegendnodewidget.py @@ -0,0 +1,13 @@ +# The following has been generated automatically from src/gui/qgscolorramplegendnodewidget.h +# monkey patching scoped based enum +QgsColorRampLegendNodeWidget.Capability.Prefix.__doc__ = "Allow editing legend prefix" +QgsColorRampLegendNodeWidget.Capability.Suffix.__doc__ = "Allow editing legend suffix" +QgsColorRampLegendNodeWidget.Capability.NumberFormat.__doc__ = "Allow editing number format" +QgsColorRampLegendNodeWidget.Capability.DefaultMinimum.__doc__ = "Allow resetting minimum label to default" +QgsColorRampLegendNodeWidget.Capability.DefaultMaximum.__doc__ = "Allow resetting maximum label to default" +QgsColorRampLegendNodeWidget.Capability.AllCapabilities.__doc__ = "All capabilities" +QgsColorRampLegendNodeWidget.Capability.__doc__ = "Capabilities to expose in the widget.\n\n.. versionadded:: 3.38\n\n" + '* ``Prefix``: ' + QgsColorRampLegendNodeWidget.Capability.Prefix.__doc__ + '\n' + '* ``Suffix``: ' + QgsColorRampLegendNodeWidget.Capability.Suffix.__doc__ + '\n' + '* ``NumberFormat``: ' + QgsColorRampLegendNodeWidget.Capability.NumberFormat.__doc__ + '\n' + '* ``DefaultMinimum``: ' + QgsColorRampLegendNodeWidget.Capability.DefaultMinimum.__doc__ + '\n' + '* ``DefaultMaximum``: ' + QgsColorRampLegendNodeWidget.Capability.DefaultMaximum.__doc__ + '\n' + '* ``AllCapabilities``: ' + QgsColorRampLegendNodeWidget.Capability.AllCapabilities.__doc__ +# -- +QgsColorRampLegendNodeWidget.Capability.baseClass = QgsColorRampLegendNodeWidget +QgsColorRampLegendNodeWidget.Capabilities.baseClass = QgsColorRampLegendNodeWidget +Capabilities = QgsColorRampLegendNodeWidget # dirty hack since SIP seems to introduce the flags in module diff --git a/python/gui/auto_generated/qgscolorramplegendnodewidget.sip.in b/python/gui/auto_generated/qgscolorramplegendnodewidget.sip.in index bbe8d561270b5..d9700eed8c2a5 100644 --- a/python/gui/auto_generated/qgscolorramplegendnodewidget.sip.in +++ b/python/gui/auto_generated/qgscolorramplegendnodewidget.sip.in @@ -11,7 +11,6 @@ - class QgsColorRampLegendNodeWidget: QgsPanelWidget { %Docstring(signature="appended") @@ -30,9 +29,24 @@ When changes are made the to settings by a user the :py:func:`~widgetChanged` si %End public: - QgsColorRampLegendNodeWidget( QWidget *parent = 0 ); + enum class Capability + { + Prefix, + Suffix, + NumberFormat, + DefaultMinimum, + DefaultMaximum, + AllCapabilities, + }; + + typedef QFlags Capabilities; + + + QgsColorRampLegendNodeWidget( QWidget *parent = 0, QgsColorRampLegendNodeWidget::Capabilities capabilities = QgsColorRampLegendNodeWidget::Capability::AllCapabilities ); %Docstring Constructor for QgsColorRampLegendNodeWidget, with the specified ``parent`` widget. + +Since QGIS 3.38, the ``capabilities`` argument can be used to fine-tune settings exposed in the widget. %End QgsColorRampLegendNodeSettings settings() const; @@ -59,6 +73,8 @@ when using single band gray renderer). %End }; +QFlags operator|(QgsColorRampLegendNodeWidget::Capability f1, QFlags f2); + class QgsColorRampLegendNodeDialog : QDialog { @@ -73,9 +89,11 @@ A dialog for configuring a :py:class:`QgsColorRampLegendNode` (:py:class:`QgsCol %End public: - QgsColorRampLegendNodeDialog( const QgsColorRampLegendNodeSettings &settings, QWidget *parent /TransferThis/ = 0 ); + QgsColorRampLegendNodeDialog( const QgsColorRampLegendNodeSettings &settings, QWidget *parent /TransferThis/ = 0, QgsColorRampLegendNodeWidget::Capabilities capabilities = QgsColorRampLegendNodeWidget::Capability::AllCapabilities ); %Docstring Constructor for QgsColorRampLegendNodeDialog, initially showing the specified ``settings``. + +Since QGIS 3.38, the ``capabilities`` argument can be used to fine-tune settings exposed in the dialog. %End QgsColorRampLegendNodeSettings settings() const; diff --git a/src/gui/layout/qgslayoutlegendwidget.cpp b/src/gui/layout/qgslayoutlegendwidget.cpp index 4b91628ff53cd..04327ed2c0f20 100644 --- a/src/gui/layout/qgslayoutlegendwidget.cpp +++ b/src/gui/layout/qgslayoutlegendwidget.cpp @@ -42,6 +42,7 @@ #include "qgssymbol.h" #include "qgslayoutundostack.h" #include "qgsexpressionfinder.h" +#include "qgscolorramplegendnode.h" #include #include diff --git a/src/gui/qgscolorramplegendnodewidget.cpp b/src/gui/qgscolorramplegendnodewidget.cpp index 47da95afd0a4c..3bff355631f92 100644 --- a/src/gui/qgscolorramplegendnodewidget.cpp +++ b/src/gui/qgscolorramplegendnodewidget.cpp @@ -16,13 +16,12 @@ ***************************************************************************/ #include "qgscolorramplegendnodewidget.h" -#include "qgscolorramplegendnode.h" #include "qgshelp.h" #include "qgsnumericformatselectorwidget.h" #include "qgsnumericformat.h" #include -QgsColorRampLegendNodeWidget::QgsColorRampLegendNodeWidget( QWidget *parent ) +QgsColorRampLegendNodeWidget::QgsColorRampLegendNodeWidget( QWidget *parent, Capabilities capabilities ) : QgsPanelWidget( parent ) { setupUi( this ); @@ -33,8 +32,22 @@ QgsColorRampLegendNodeWidget::QgsColorRampLegendNodeWidget( QWidget *parent ) mOrientationComboBox->addItem( tr( "Vertical" ), Qt::Vertical ); mOrientationComboBox->addItem( tr( "Horizontal" ), Qt::Horizontal ); - mMinLabelLineEdit->setPlaceholderText( tr( "Default" ) ); - mMaxLabelLineEdit->setPlaceholderText( tr( "Default" ) ); + if ( capabilities.testFlag( Capability::DefaultMinimum ) ) + { + mMinLabelLineEdit->setPlaceholderText( tr( "Default" ) ); + } + else + { + mMinLabelLineEdit->setShowClearButton( false ); + } + if ( capabilities.testFlag( Capability::DefaultMinimum ) ) + { + mMaxLabelLineEdit->setPlaceholderText( tr( "Default" ) ); + } + else + { + mMaxLabelLineEdit->setShowClearButton( false ); + } mFontButton->setShowNullFormat( true ); mFontButton->setNoFormatString( tr( "Default" ) ); @@ -54,6 +67,22 @@ QgsColorRampLegendNodeWidget::QgsColorRampLegendNodeWidget( QWidget *parent ) connect( mOrientationComboBox, qOverload( &QComboBox::currentIndexChanged ), this, &QgsColorRampLegendNodeWidget::onOrientationChanged ); connect( mNumberFormatPushButton, &QPushButton::clicked, this, &QgsColorRampLegendNodeWidget::changeNumberFormat ); connect( mFontButton, &QgsFontButton::changed, this, &QgsColorRampLegendNodeWidget::onChanged ); + + if ( !capabilities.testFlag( Capability::Prefix ) ) + { + mPrefixLineEdit->hide(); + mPrefixLabel->hide(); + } + if ( !capabilities.testFlag( Capability::Suffix ) ) + { + mSuffixLineEdit->hide(); + mSuffixLabel->hide(); + } + if ( !capabilities.testFlag( Capability::NumberFormat ) ) + { + mNumberFormatPushButton->hide(); + mNumberFormatLabel->hide(); + } } QgsColorRampLegendNodeSettings QgsColorRampLegendNodeWidget::settings() const @@ -137,11 +166,11 @@ void QgsColorRampLegendNodeWidget::onChanged() // QgsColorRampLegendNodeDialog // -QgsColorRampLegendNodeDialog::QgsColorRampLegendNodeDialog( const QgsColorRampLegendNodeSettings &settings, QWidget *parent ) +QgsColorRampLegendNodeDialog::QgsColorRampLegendNodeDialog( const QgsColorRampLegendNodeSettings &settings, QWidget *parent, QgsColorRampLegendNodeWidget::Capabilities capabilities ) : QDialog( parent ) { QVBoxLayout *vLayout = new QVBoxLayout(); - mWidget = new QgsColorRampLegendNodeWidget( nullptr ); + mWidget = new QgsColorRampLegendNodeWidget( nullptr, capabilities ); vLayout->addWidget( mWidget ); mButtonBox = new QDialogButtonBox( QDialogButtonBox::Cancel | QDialogButtonBox::Help | QDialogButtonBox::Ok, Qt::Horizontal ); connect( mButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept ); diff --git a/src/gui/qgscolorramplegendnodewidget.h b/src/gui/qgscolorramplegendnodewidget.h index c3bdcdf52413d..b461b8c7f4898 100644 --- a/src/gui/qgscolorramplegendnodewidget.h +++ b/src/gui/qgscolorramplegendnodewidget.h @@ -21,8 +21,7 @@ #include "qgis_gui.h" #include "ui_qgscolorramplegendnodewidgetbase.h" - -#include "qgscolorramplegendnode.h" +#include "qgscolorramplegendnodesettings.h" #include class QDialogButtonBox; @@ -44,10 +43,36 @@ class GUI_EXPORT QgsColorRampLegendNodeWidget: public QgsPanelWidget, private Ui public: + /** + * Capabilities to expose in the widget. + * + * \since QGIS 3.38 + */ + enum class Capability : int SIP_ENUM_BASETYPE( IntFlag ) + { + Prefix = 1 << 0, //!< Allow editing legend prefix + Suffix = 1 << 1, //!< Allow editing legend suffix + NumberFormat = 1 << 2, //!< Allow editing number format + DefaultMinimum = 1 << 3, //!< Allow resetting minimum label to default + DefaultMaximum = 1 << 4, //!< Allow resetting maximum label to default + AllCapabilities = Prefix | Suffix | NumberFormat | DefaultMinimum | DefaultMaximum, //!< All capabilities + }; + Q_ENUM( Capability ) + + /** + * Capabilities to expose in the widget. + * + * \since QGIS 3.38 + */ + Q_DECLARE_FLAGS( Capabilities, Capability ) + Q_FLAG( Capabilities ) + /** * Constructor for QgsColorRampLegendNodeWidget, with the specified \a parent widget. + * + * Since QGIS 3.38, the \a capabilities argument can be used to fine-tune settings exposed in the widget. */ - QgsColorRampLegendNodeWidget( QWidget *parent = nullptr ); + QgsColorRampLegendNodeWidget( QWidget *parent = nullptr, QgsColorRampLegendNodeWidget::Capabilities capabilities = QgsColorRampLegendNodeWidget::Capability::AllCapabilities ); /** * Returns the legend node settings as defined by the widget. @@ -84,6 +109,7 @@ class GUI_EXPORT QgsColorRampLegendNodeWidget: public QgsPanelWidget, private Ui QgsColorRampLegendNodeSettings mSettings; }; +Q_DECLARE_OPERATORS_FOR_FLAGS( QgsColorRampLegendNodeWidget::Capabilities ) /** * \ingroup gui @@ -98,8 +124,10 @@ class GUI_EXPORT QgsColorRampLegendNodeDialog : public QDialog /** * Constructor for QgsColorRampLegendNodeDialog, initially showing the specified \a settings. + * + * Since QGIS 3.38, the \a capabilities argument can be used to fine-tune settings exposed in the dialog. */ - QgsColorRampLegendNodeDialog( const QgsColorRampLegendNodeSettings &settings, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsColorRampLegendNodeDialog( const QgsColorRampLegendNodeSettings &settings, QWidget *parent SIP_TRANSFERTHIS = nullptr, QgsColorRampLegendNodeWidget::Capabilities capabilities = QgsColorRampLegendNodeWidget::Capability::AllCapabilities ); /** * Returns the legend node settings as defined by the dialog. diff --git a/src/gui/symbology/qgsheatmaprendererwidget.cpp b/src/gui/symbology/qgsheatmaprendererwidget.cpp index 953ed9e2c04bd..e380a9d19fd62 100644 --- a/src/gui/symbology/qgsheatmaprendererwidget.cpp +++ b/src/gui/symbology/qgsheatmaprendererwidget.cpp @@ -178,7 +178,7 @@ void QgsHeatmapRendererWidget::showLegendSettings() QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( qobject_cast< QWidget * >( parent() ) ); if ( panel && panel->dockMode() ) { - QgsColorRampLegendNodeWidget *legendPanel = new QgsColorRampLegendNodeWidget(); + QgsColorRampLegendNodeWidget *legendPanel = new QgsColorRampLegendNodeWidget( nullptr, QgsColorRampLegendNodeWidget::Capabilities() ); legendPanel->setUseContinuousRampCheckBoxVisibility( false ); legendPanel->setPanelTitle( tr( "Legend Settings" ) ); legendPanel->setSettings( mRenderer->legendSettings() ); @@ -191,7 +191,7 @@ void QgsHeatmapRendererWidget::showLegendSettings() } else { - QgsColorRampLegendNodeDialog dialog( mRenderer->legendSettings(), this ); + QgsColorRampLegendNodeDialog dialog( mRenderer->legendSettings(), this, QgsColorRampLegendNodeWidget::Capabilities() ); dialog.setUseContinuousRampCheckBoxVisibility( false ); dialog.setWindowTitle( tr( "Legend Settings" ) ); if ( dialog.exec() ) diff --git a/src/ui/qgscolorramplegendnodewidgetbase.ui b/src/ui/qgscolorramplegendnodewidgetbase.ui index b3eff07544c46..72d45bbc89601 100644 --- a/src/ui/qgscolorramplegendnodewidgetbase.ui +++ b/src/ui/qgscolorramplegendnodewidgetbase.ui @@ -7,7 +7,7 @@ 0 0 359 - 368 + 399 @@ -78,7 +78,7 @@
- + Prefix @@ -92,7 +92,7 @@ - + Suffix @@ -106,7 +106,7 @@ - + Number format From 61b72b7d9676696250eacc74ebdfc021fce3c0ae Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 3 May 2024 10:26:09 +1000 Subject: [PATCH 051/102] Fix see also --- python/PyQt6/core/auto_generated/symbology/qgsrenderer.sip.in | 2 +- python/core/auto_generated/symbology/qgsrenderer.sip.in | 2 +- src/core/symbology/qgsrenderer.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/python/PyQt6/core/auto_generated/symbology/qgsrenderer.sip.in b/python/PyQt6/core/auto_generated/symbology/qgsrenderer.sip.in index 281ea31e2e522..e1a70d0359166 100644 --- a/python/PyQt6/core/auto_generated/symbology/qgsrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/symbology/qgsrenderer.sip.in @@ -358,7 +358,7 @@ the features displayed using that key. %Docstring Returns a list of symbology items for the legend -.. seealso:: :py:func:`createLayerTreeModelLegendNodes` +.. seealso:: :py:func:`createLegendNodes` .. seealso:: :py:func:`legendKeys` %End diff --git a/python/core/auto_generated/symbology/qgsrenderer.sip.in b/python/core/auto_generated/symbology/qgsrenderer.sip.in index 97c4ab95b50d4..5e51ddc6863b5 100644 --- a/python/core/auto_generated/symbology/qgsrenderer.sip.in +++ b/python/core/auto_generated/symbology/qgsrenderer.sip.in @@ -358,7 +358,7 @@ the features displayed using that key. %Docstring Returns a list of symbology items for the legend -.. seealso:: :py:func:`createLayerTreeModelLegendNodes` +.. seealso:: :py:func:`createLegendNodes` .. seealso:: :py:func:`legendKeys` %End diff --git a/src/core/symbology/qgsrenderer.h b/src/core/symbology/qgsrenderer.h index cc64048032d94..f4fec0b041acf 100644 --- a/src/core/symbology/qgsrenderer.h +++ b/src/core/symbology/qgsrenderer.h @@ -398,7 +398,7 @@ class CORE_EXPORT QgsFeatureRenderer /** * Returns a list of symbology items for the legend * - * \see createLayerTreeModelLegendNodes() + * \see createLegendNodes() * \see legendKeys() */ virtual QgsLegendSymbolList legendSymbolItems() const; From e8a33646a43045d2a3159b81b33bba8315859514 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 3 May 2024 13:14:24 +1000 Subject: [PATCH 052/102] Test mask for qt 6 --- .../expected_heatmap/expected_heatmap_mask.png | Bin 0 -> 2486 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/testdata/control_images/legend/expected_heatmap/expected_heatmap_mask.png diff --git a/tests/testdata/control_images/legend/expected_heatmap/expected_heatmap_mask.png b/tests/testdata/control_images/legend/expected_heatmap/expected_heatmap_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..01071994c90c31cf941e2a95a05f39a235764dbb GIT binary patch literal 2486 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 tZWZymUyKg+ph_S2{J!5r>fdB-M{ Date: Wed, 8 May 2024 12:07:16 +1000 Subject: [PATCH 053/102] Use QgsCodeEditorWidget in expression builder dialog Adds Ctrl+F search bar support to expression builder dialog --- .../auto_generated/qgsexpressionbuilderwidget.sip.in | 1 - .../auto_generated/qgsexpressionbuilderwidget.sip.in | 1 - src/gui/qgsexpressionbuilderwidget.cpp | 10 ++++++++-- src/gui/qgsexpressionbuilderwidget.h | 4 +++- src/gui/qgsexpressionlineedit.cpp | 2 +- src/ui/qgsexpressionbuilder.ui | 8 +------- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/qgsexpressionbuilderwidget.sip.in b/python/PyQt6/gui/auto_generated/qgsexpressionbuilderwidget.sip.in index 6614ea27cec90..fe3674ffdfb42 100644 --- a/python/PyQt6/gui/auto_generated/qgsexpressionbuilderwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsexpressionbuilderwidget.sip.in @@ -12,7 +12,6 @@ - class QgsExpressionBuilderWidget : QWidget { %Docstring(signature="appended") diff --git a/python/gui/auto_generated/qgsexpressionbuilderwidget.sip.in b/python/gui/auto_generated/qgsexpressionbuilderwidget.sip.in index b746daf926d1f..9542b1e398d3e 100644 --- a/python/gui/auto_generated/qgsexpressionbuilderwidget.sip.in +++ b/python/gui/auto_generated/qgsexpressionbuilderwidget.sip.in @@ -12,7 +12,6 @@ - class QgsExpressionBuilderWidget : QWidget { %Docstring(signature="appended") diff --git a/src/gui/qgsexpressionbuilderwidget.cpp b/src/gui/qgsexpressionbuilderwidget.cpp index 10fff0940176a..8872997532e30 100644 --- a/src/gui/qgsexpressionbuilderwidget.cpp +++ b/src/gui/qgsexpressionbuilderwidget.cpp @@ -39,7 +39,6 @@ #include "qgspythonrunner.h" #include "qgsgeometry.h" #include "qgsfeature.h" -#include "qgsfeatureiterator.h" #include "qgsvectorlayer.h" #include "qgssettings.h" #include "qgsproject.h" @@ -49,7 +48,7 @@ #include "qgsfieldformatter.h" #include "qgsexpressionstoredialog.h" #include "qgsexpressiontreeview.h" - +#include "qgscodeeditorwidget.h" bool formatterCanProvideAvailableValues( QgsVectorLayer *layer, const QString &fieldName ) @@ -76,6 +75,13 @@ QgsExpressionBuilderWidget::QgsExpressionBuilderWidget( QWidget *parent ) { setupUi( this ); + txtExpressionString = new QgsCodeEditorExpression(); + QgsCodeEditorWidget *codeEditorWidget = new QgsCodeEditorWidget( txtExpressionString ); + QVBoxLayout *vl = new QVBoxLayout(); + vl->setContentsMargins( 0, 0, 0, 0 ); + vl->addWidget( codeEditorWidget ); + mExpressionEditorContainer->setLayout( vl ); + connect( btnRun, &QToolButton::pressed, this, &QgsExpressionBuilderWidget::btnRun_pressed ); connect( btnNewFile, &QPushButton::clicked, this, &QgsExpressionBuilderWidget::btnNewFile_pressed ); connect( btnRemoveFile, &QPushButton::clicked, this, &QgsExpressionBuilderWidget::btnRemoveFile_pressed ); diff --git a/src/gui/qgsexpressionbuilderwidget.h b/src/gui/qgsexpressionbuilderwidget.h index c341e326229f3..73a64476cc27e 100644 --- a/src/gui/qgsexpressionbuilderwidget.h +++ b/src/gui/qgsexpressionbuilderwidget.h @@ -32,7 +32,7 @@ class QgsFields; class QgsExpressionHighlighter; class QgsRelation; - +class QgsCodeEditorExpression; /** * \ingroup gui @@ -493,6 +493,8 @@ class GUI_EXPORT QgsExpressionBuilderWidget : public QWidget, private Ui::QgsExp // Translated name of the user expressions group QString mUserExpressionsGroupName; + + QgsCodeEditorExpression *txtExpressionString = nullptr; }; // clazy:excludeall=qstring-allocations diff --git a/src/gui/qgsexpressionlineedit.cpp b/src/gui/qgsexpressionlineedit.cpp index 2eaeba2e79b2e..476e5d7e0c97b 100644 --- a/src/gui/qgsexpressionlineedit.cpp +++ b/src/gui/qgsexpressionlineedit.cpp @@ -19,10 +19,10 @@ #include "qgsapplication.h" #include "qgsexpressionbuilderdialog.h" #include "qgsexpressioncontextgenerator.h" -#include "qgscodeeditorsql.h" #include "qgsproject.h" #include "qgsvectorlayer.h" #include "qgsexpressioncontextutils.h" +#include "qgscodeeditorexpression.h" #include #include diff --git a/src/ui/qgsexpressionbuilder.ui b/src/ui/qgsexpressionbuilder.ui index 4b1a8b7180c30..6469220c56800 100644 --- a/src/ui/qgsexpressionbuilder.ui +++ b/src/ui/qgsexpressionbuilder.ui @@ -220,7 +220,7 @@ - + @@ -881,12 +881,6 @@ Saved scripts are auto loaded on QGIS startup. QLineEdit
qgsfilterlineedit.h
- - QgsCodeEditorExpression - QWidget -
qgscodeeditorexpression.h
- 1 -
QgsCodeEditorPython QWidget From 09364b1945960e8594cc94eff511f50500e51369 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 8 May 2024 12:11:38 +1000 Subject: [PATCH 054/102] Use QgsCodeEditorWidget in rich text editor HTML view --- src/gui/qgsrichtexteditor.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/gui/qgsrichtexteditor.cpp b/src/gui/qgsrichtexteditor.cpp index 944a5f06ff532..f6910617b88de 100644 --- a/src/gui/qgsrichtexteditor.cpp +++ b/src/gui/qgsrichtexteditor.cpp @@ -33,6 +33,7 @@ #include "qgscolorbutton.h" #include "qgscodeeditor.h" #include "qgscodeeditorhtml.h" +#include "qgscodeeditorwidget.h" #include #include @@ -59,7 +60,8 @@ QgsRichTextEditor::QgsRichTextEditor( QWidget *parent ) QVBoxLayout *sourceLayout = new QVBoxLayout(); sourceLayout->setContentsMargins( 0, 0, 0, 0 ); mSourceEdit = new QgsCodeEditorHTML(); - sourceLayout->addWidget( mSourceEdit ); + QgsCodeEditorWidget *codeEditorWidget = new QgsCodeEditorWidget( mSourceEdit ); + sourceLayout->addWidget( codeEditorWidget ); mPageSourceEdit->setLayout( sourceLayout ); mToolBar->setIconSize( QgsGuiUtils::iconSize( false ) ); From e59c0df8a8e51434580bc9221e359404d1652f4b Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 8 May 2024 11:26:43 +1000 Subject: [PATCH 055/102] Port decorated scrollbar widget class from QtCreator Allows decorating scrollbars with colored highlight bars --- .../auto_additions/qgsdecoratedscrollbar.py | 9 + .../qgsdecoratedscrollbar.sip.in | 153 +++++++ python/PyQt6/gui/gui_auto.sip | 1 + .../auto_additions/qgsdecoratedscrollbar.py | 9 + .../qgsdecoratedscrollbar.sip.in | 153 +++++++ python/gui/gui_auto.sip | 1 + src/gui/CMakeLists.txt | 2 + src/gui/qgsdecoratedscrollbar.cpp | 424 ++++++++++++++++++ src/gui/qgsdecoratedscrollbar.h | 215 +++++++++ 9 files changed, 967 insertions(+) create mode 100644 python/PyQt6/gui/auto_additions/qgsdecoratedscrollbar.py create mode 100644 python/PyQt6/gui/auto_generated/qgsdecoratedscrollbar.sip.in create mode 100644 python/gui/auto_additions/qgsdecoratedscrollbar.py create mode 100644 python/gui/auto_generated/qgsdecoratedscrollbar.sip.in create mode 100644 src/gui/qgsdecoratedscrollbar.cpp create mode 100644 src/gui/qgsdecoratedscrollbar.h diff --git a/python/PyQt6/gui/auto_additions/qgsdecoratedscrollbar.py b/python/PyQt6/gui/auto_additions/qgsdecoratedscrollbar.py new file mode 100644 index 0000000000000..a9d54d7d6f82e --- /dev/null +++ b/python/PyQt6/gui/auto_additions/qgsdecoratedscrollbar.py @@ -0,0 +1,9 @@ +# The following has been generated automatically from src/gui/qgsdecoratedscrollbar.h +# monkey patching scoped based enum +QgsScrollBarHighlight.Priority.Invalid.__doc__ = "Invalid" +QgsScrollBarHighlight.Priority.LowPriority.__doc__ = "Low priority, rendered below all other highlights" +QgsScrollBarHighlight.Priority.NormalPriority.__doc__ = "Normal priority" +QgsScrollBarHighlight.Priority.HighPriority.__doc__ = "High priority" +QgsScrollBarHighlight.Priority.HighestPriority.__doc__ = "Highest priority, rendered above all other highlights" +QgsScrollBarHighlight.Priority.__doc__ = "Priority, which dictates how overlapping highlights are rendered\n\n" + '* ``Invalid``: ' + QgsScrollBarHighlight.Priority.Invalid.__doc__ + '\n' + '* ``LowPriority``: ' + QgsScrollBarHighlight.Priority.LowPriority.__doc__ + '\n' + '* ``NormalPriority``: ' + QgsScrollBarHighlight.Priority.NormalPriority.__doc__ + '\n' + '* ``HighPriority``: ' + QgsScrollBarHighlight.Priority.HighPriority.__doc__ + '\n' + '* ``HighestPriority``: ' + QgsScrollBarHighlight.Priority.HighestPriority.__doc__ +# -- diff --git a/python/PyQt6/gui/auto_generated/qgsdecoratedscrollbar.sip.in b/python/PyQt6/gui/auto_generated/qgsdecoratedscrollbar.sip.in new file mode 100644 index 0000000000000..b5de42ab39bd8 --- /dev/null +++ b/python/PyQt6/gui/auto_generated/qgsdecoratedscrollbar.sip.in @@ -0,0 +1,153 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsdecoratedscrollbar.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsScrollBarHighlight +{ +%Docstring(signature="appended") +Encapsulates the details of a highlight in a scrollbar, used alongside :py:class:`QgsScrollBarHighlightController`. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsdecoratedscrollbar.h" +%End + public: + + enum class Priority /BaseType=IntEnum/ + { + Invalid, + LowPriority, + NormalPriority, + HighPriority, + HighestPriority + }; + + QgsScrollBarHighlight( int category, int position, const QColor &color, QgsScrollBarHighlight::Priority priority = QgsScrollBarHighlight::Priority::NormalPriority ); +%Docstring +Constructor for QgsScrollBarHighlight. +%End + QgsScrollBarHighlight(); + + int category; + + int position; + + QColor color; + + QgsScrollBarHighlight::Priority priority; +}; + +class QgsScrollBarHighlightController +{ +%Docstring(signature="appended") +Adds highlights (colored markers) to a scrollbar. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsdecoratedscrollbar.h" +%End + public: + + QgsScrollBarHighlightController(); + ~QgsScrollBarHighlightController(); + + QScrollBar *scrollBar() const; +%Docstring +Returns the associated scroll bar. +%End + + QAbstractScrollArea *scrollArea() const; +%Docstring +Returns the associated scroll area. + +.. seealso:: :py:func:`setScrollArea` +%End + + void setScrollArea( QAbstractScrollArea *scrollArea ); +%Docstring +Sets the associated scroll bar. + +.. seealso:: :py:func:`scrollArea` +%End + + double lineHeight() const; +%Docstring +Returns the line height for text associated with the scroll area. + +.. seealso:: :py:func:`setLineHeight` +%End + + void setLineHeight( double height ); +%Docstring +Sets the line ``height`` for text associated with the scroll area. + +.. seealso:: :py:func:`lineHeight` +%End + + double visibleRange() const; +%Docstring +Returns the visible range of the scroll area (i.e. the viewport's height). + +.. seealso:: :py:func:`setVisibleRange` +%End + + void setVisibleRange( double visibleRange ); +%Docstring +Sets the visible range of the scroll area (i.e. the viewport's height). + +.. seealso:: :py:func:`visibleRange` +%End + + double margin() const; +%Docstring +Returns the document margins for the associated viewport. + +.. seealso:: :py:func:`setMargin` +%End + + void setMargin( double margin ); +%Docstring +Sets the document ``margin`` for the associated viewport. + +.. seealso:: :py:func:`margin` +%End + + + void addHighlight( const QgsScrollBarHighlight &highlight ); +%Docstring +Adds a ``highlight`` to the scrollbar. +%End + + void removeHighlights( int category ); +%Docstring +Removes all highlights with matching ``category`` from the scrollbar. +%End + + void removeAllHighlights(); +%Docstring +Removes all highlights from the scroll bar. +%End + +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsdecoratedscrollbar.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 f845846e857d2..e6bf953cc4333 100644 --- a/python/PyQt6/gui/gui_auto.sip +++ b/python/PyQt6/gui/gui_auto.sip @@ -46,6 +46,7 @@ %Include auto_generated/qgsdataitemguiproviderregistry.sip %Include auto_generated/qgsdatasourceselectdialog.sip %Include auto_generated/qgsdbrelationshipwidget.sip +%Include auto_generated/qgsdecoratedscrollbar.sip %Include auto_generated/qgsnewdatabasetablenamewidget.sip %Include auto_generated/qgsdetaileditemdata.sip %Include auto_generated/qgsdetaileditemdelegate.sip diff --git a/python/gui/auto_additions/qgsdecoratedscrollbar.py b/python/gui/auto_additions/qgsdecoratedscrollbar.py new file mode 100644 index 0000000000000..a9d54d7d6f82e --- /dev/null +++ b/python/gui/auto_additions/qgsdecoratedscrollbar.py @@ -0,0 +1,9 @@ +# The following has been generated automatically from src/gui/qgsdecoratedscrollbar.h +# monkey patching scoped based enum +QgsScrollBarHighlight.Priority.Invalid.__doc__ = "Invalid" +QgsScrollBarHighlight.Priority.LowPriority.__doc__ = "Low priority, rendered below all other highlights" +QgsScrollBarHighlight.Priority.NormalPriority.__doc__ = "Normal priority" +QgsScrollBarHighlight.Priority.HighPriority.__doc__ = "High priority" +QgsScrollBarHighlight.Priority.HighestPriority.__doc__ = "Highest priority, rendered above all other highlights" +QgsScrollBarHighlight.Priority.__doc__ = "Priority, which dictates how overlapping highlights are rendered\n\n" + '* ``Invalid``: ' + QgsScrollBarHighlight.Priority.Invalid.__doc__ + '\n' + '* ``LowPriority``: ' + QgsScrollBarHighlight.Priority.LowPriority.__doc__ + '\n' + '* ``NormalPriority``: ' + QgsScrollBarHighlight.Priority.NormalPriority.__doc__ + '\n' + '* ``HighPriority``: ' + QgsScrollBarHighlight.Priority.HighPriority.__doc__ + '\n' + '* ``HighestPriority``: ' + QgsScrollBarHighlight.Priority.HighestPriority.__doc__ +# -- diff --git a/python/gui/auto_generated/qgsdecoratedscrollbar.sip.in b/python/gui/auto_generated/qgsdecoratedscrollbar.sip.in new file mode 100644 index 0000000000000..c9a8a0fb389c0 --- /dev/null +++ b/python/gui/auto_generated/qgsdecoratedscrollbar.sip.in @@ -0,0 +1,153 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsdecoratedscrollbar.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsScrollBarHighlight +{ +%Docstring(signature="appended") +Encapsulates the details of a highlight in a scrollbar, used alongside :py:class:`QgsScrollBarHighlightController`. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsdecoratedscrollbar.h" +%End + public: + + enum class Priority + { + Invalid, + LowPriority, + NormalPriority, + HighPriority, + HighestPriority + }; + + QgsScrollBarHighlight( int category, int position, const QColor &color, QgsScrollBarHighlight::Priority priority = QgsScrollBarHighlight::Priority::NormalPriority ); +%Docstring +Constructor for QgsScrollBarHighlight. +%End + QgsScrollBarHighlight(); + + int category; + + int position; + + QColor color; + + QgsScrollBarHighlight::Priority priority; +}; + +class QgsScrollBarHighlightController +{ +%Docstring(signature="appended") +Adds highlights (colored markers) to a scrollbar. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsdecoratedscrollbar.h" +%End + public: + + QgsScrollBarHighlightController(); + ~QgsScrollBarHighlightController(); + + QScrollBar *scrollBar() const; +%Docstring +Returns the associated scroll bar. +%End + + QAbstractScrollArea *scrollArea() const; +%Docstring +Returns the associated scroll area. + +.. seealso:: :py:func:`setScrollArea` +%End + + void setScrollArea( QAbstractScrollArea *scrollArea ); +%Docstring +Sets the associated scroll bar. + +.. seealso:: :py:func:`scrollArea` +%End + + double lineHeight() const; +%Docstring +Returns the line height for text associated with the scroll area. + +.. seealso:: :py:func:`setLineHeight` +%End + + void setLineHeight( double height ); +%Docstring +Sets the line ``height`` for text associated with the scroll area. + +.. seealso:: :py:func:`lineHeight` +%End + + double visibleRange() const; +%Docstring +Returns the visible range of the scroll area (i.e. the viewport's height). + +.. seealso:: :py:func:`setVisibleRange` +%End + + void setVisibleRange( double visibleRange ); +%Docstring +Sets the visible range of the scroll area (i.e. the viewport's height). + +.. seealso:: :py:func:`visibleRange` +%End + + double margin() const; +%Docstring +Returns the document margins for the associated viewport. + +.. seealso:: :py:func:`setMargin` +%End + + void setMargin( double margin ); +%Docstring +Sets the document ``margin`` for the associated viewport. + +.. seealso:: :py:func:`margin` +%End + + + void addHighlight( const QgsScrollBarHighlight &highlight ); +%Docstring +Adds a ``highlight`` to the scrollbar. +%End + + void removeHighlights( int category ); +%Docstring +Removes all highlights with matching ``category`` from the scrollbar. +%End + + void removeAllHighlights(); +%Docstring +Removes all highlights from the scroll bar. +%End + +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsdecoratedscrollbar.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 f845846e857d2..e6bf953cc4333 100644 --- a/python/gui/gui_auto.sip +++ b/python/gui/gui_auto.sip @@ -46,6 +46,7 @@ %Include auto_generated/qgsdataitemguiproviderregistry.sip %Include auto_generated/qgsdatasourceselectdialog.sip %Include auto_generated/qgsdbrelationshipwidget.sip +%Include auto_generated/qgsdecoratedscrollbar.sip %Include auto_generated/qgsnewdatabasetablenamewidget.sip %Include auto_generated/qgsdetaileditemdata.sip %Include auto_generated/qgsdetaileditemdelegate.sip diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 6d86f940452d7..a9a7a64db345f 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -554,6 +554,7 @@ set(QGIS_GUI_SRCS qgsdatasourceselectdialog.cpp qgsdbqueryhistoryprovider.cpp qgsdbrelationshipwidget.cpp + qgsdecoratedscrollbar.cpp qgsdetaileditemdata.cpp qgsdetaileditemdelegate.cpp qgsdetaileditemwidget.cpp @@ -825,6 +826,7 @@ set(QGIS_GUI_HDRS qgsdatasourceselectdialog.h qgsdbqueryhistoryprovider.h qgsdbrelationshipwidget.h + qgsdecoratedscrollbar.h qgsnewdatabasetablenamewidget.h qgsdetaileditemdata.h qgsdetaileditemdelegate.h diff --git a/src/gui/qgsdecoratedscrollbar.cpp b/src/gui/qgsdecoratedscrollbar.cpp new file mode 100644 index 0000000000000..6c27a47d0132c --- /dev/null +++ b/src/gui/qgsdecoratedscrollbar.cpp @@ -0,0 +1,424 @@ +/*************************************************************************** + qgsdecoratedscrollbar.cpp + -------------------------------------- + Date : May 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 "qgsdecoratedscrollbar.h" +#include +#include +#include +#include +#include +#include + +///@cond PRIVATE + +// +// QgsScrollBarHighlightOverlay +// + +QgsScrollBarHighlightOverlay::QgsScrollBarHighlightOverlay( QgsScrollBarHighlightController *scrollBarController ) + : QWidget( scrollBarController->scrollArea() ) + , mHighlightController( scrollBarController ) +{ + setAttribute( Qt::WA_TransparentForMouseEvents ); + scrollBar()->parentWidget()->installEventFilter( this ); + doResize(); + doMove(); + setVisible( scrollBar()->isVisible() ); +} + +void QgsScrollBarHighlightOverlay::doResize() +{ + resize( scrollBar()->size() ); +} + +void QgsScrollBarHighlightOverlay::doMove() +{ + move( parentWidget()->mapFromGlobal( scrollBar()->mapToGlobal( scrollBar()->pos() ) ) ); +} + +void QgsScrollBarHighlightOverlay::scheduleUpdate() +{ + if ( mIsCacheUpdateScheduled ) + return; + + mIsCacheUpdateScheduled = true; + QMetaObject::invokeMethod( this, QOverload<>::of( &QWidget::update ), Qt::QueuedConnection ); +} + +void QgsScrollBarHighlightOverlay::paintEvent( QPaintEvent *paintEvent ) +{ + QWidget::paintEvent( paintEvent ); + + updateCache(); + + if ( mHighlightCache.isEmpty() ) + return; + + QPainter painter( this ); + painter.setRenderHint( QPainter::Antialiasing, false ); + + const QRect &gRect = overlayRect(); + const QRect &hRect = handleRect(); + + constexpr int marginX = 3; + constexpr int marginH = -2 * marginX + 1; + const QRect aboveHandleRect = QRect( gRect.x() + marginX, + gRect.y(), + gRect.width() + marginH, + hRect.y() - gRect.y() ); + const QRect handleRect = QRect( gRect.x() + marginX, + hRect.y(), + gRect.width() + marginH, + hRect.height() ); + const QRect belowHandleRect = QRect( gRect.x() + marginX, + hRect.y() + hRect.height(), + gRect.width() + marginH, + gRect.height() - hRect.height() + gRect.y() - hRect.y() ); + + const int aboveValue = scrollBar()->value(); + const int belowValue = scrollBar()->maximum() - scrollBar()->value(); + const int sizeDocAbove = int( aboveValue * mHighlightController->lineHeight() ); + const int sizeDocBelow = int( belowValue * mHighlightController->lineHeight() ); + const int sizeDocVisible = int( mHighlightController->visibleRange() ); + + const int scrollBarBackgroundHeight = aboveHandleRect.height() + belowHandleRect.height(); + const int sizeDocInvisible = sizeDocAbove + sizeDocBelow; + const double backgroundRatio = sizeDocInvisible + ? ( ( double )scrollBarBackgroundHeight / sizeDocInvisible ) : 0; + + + if ( aboveValue ) + { + drawHighlights( &painter, + 0, + sizeDocAbove, + backgroundRatio, + 0, + aboveHandleRect ); + } + + if ( belowValue ) + { + // This is the hypothetical handle height if the handle would + // be stretched using the background ratio. + const double handleVirtualHeight = sizeDocVisible * backgroundRatio; + // Skip the doc above and visible part. + const int offset = qRound( aboveHandleRect.height() + handleVirtualHeight ); + + drawHighlights( &painter, + sizeDocAbove + sizeDocVisible, + sizeDocBelow, + backgroundRatio, + offset, + belowHandleRect ); + } + + const double handleRatio = sizeDocVisible + ? ( ( double )handleRect.height() / sizeDocVisible ) : 0; + + // This is the hypothetical handle position if the background would + // be stretched using the handle ratio. + const double aboveVirtualHeight = sizeDocAbove * handleRatio; + + // This is the accurate handle position (double) + const double accurateHandlePos = sizeDocAbove * backgroundRatio; + // The correction between handle position (int) and accurate position (double) + const double correction = aboveHandleRect.height() - accurateHandlePos; + // Skip the doc above and apply correction + const int offset = qRound( aboveVirtualHeight + correction ); + + drawHighlights( &painter, + sizeDocAbove, + sizeDocVisible, + handleRatio, + offset, + handleRect ); +} + +void QgsScrollBarHighlightOverlay::drawHighlights( QPainter *painter, + int docStart, + int docSize, + double docSizeToHandleSizeRatio, + int handleOffset, + const QRect &viewport ) +{ + if ( docSize <= 0 ) + return; + + painter->save(); + painter->setClipRect( viewport ); + + const double lineHeight = mHighlightController->lineHeight(); + + for ( const QMap> &colors : std::as_const( mHighlightCache ) ) + { + const auto itColorEnd = colors.constEnd(); + for ( auto itColor = colors.constBegin(); itColor != itColorEnd; ++itColor ) + { + const QColor color = itColor.key(); + const QMap &positions = itColor.value(); + const auto itPosEnd = positions.constEnd(); + const auto firstPos = int( docStart / lineHeight ); + auto itPos = positions.upperBound( firstPos ); + if ( itPos != positions.constBegin() ) + --itPos; + while ( itPos != itPosEnd ) + { + const double posStart = itPos.key() * lineHeight; + const double posEnd = ( itPos.value() + 1 ) * lineHeight; + if ( posEnd < docStart ) + { + ++itPos; + continue; + } + if ( posStart > docStart + docSize ) + break; + + const int height = qMax( qRound( ( posEnd - posStart ) * docSizeToHandleSizeRatio ), 1 ); + const int top = qRound( posStart * docSizeToHandleSizeRatio ) - handleOffset + viewport.y(); + + const QRect rect( viewport.left(), top, viewport.width(), height ); + painter->fillRect( rect, color ); + ++itPos; + } + } + } + painter->restore(); +} + +bool QgsScrollBarHighlightOverlay::eventFilter( QObject *object, QEvent *event ) +{ + switch ( event->type() ) + { + case QEvent::Move: + doMove(); + break; + case QEvent::Resize: + doResize(); + break; + case QEvent::ZOrderChange: + raise(); + break; + case QEvent::Show: + show(); + break; + case QEvent::Hide: + hide(); + break; + default: + break; + } + return QWidget::eventFilter( object, event ); +} + +static void insertPosition( QMap *map, int position ) +{ + auto itNext = map->upperBound( position ); + + bool gluedWithPrev = false; + if ( itNext != map->begin() ) + { + auto itPrev = std::prev( itNext ); + const int keyStart = itPrev.key(); + const int keyEnd = itPrev.value(); + if ( position >= keyStart && position <= keyEnd ) + return; // pos is already included + + if ( keyEnd + 1 == position ) + { + // glue with prev + ( *itPrev )++; + gluedWithPrev = true; + } + } + + if ( itNext != map->end() && itNext.key() == position + 1 ) + { + const int keyEnd = itNext.value(); + itNext = map->erase( itNext ); + if ( gluedWithPrev ) + { + // glue with prev and next + auto itPrev = std::prev( itNext ); + *itPrev = keyEnd; + } + else + { + // glue with next + itNext = map->insert( itNext, position, keyEnd ); + } + return; // glued + } + + if ( gluedWithPrev ) + return; // glued + + map->insert( position, position ); +} + +void QgsScrollBarHighlightOverlay::updateCache() +{ + if ( !mIsCacheUpdateScheduled ) + return; + + mHighlightCache.clear(); + + const QHash> highlightsForId = mHighlightController->highlights(); + for ( const QVector &highlights : highlightsForId ) + { + for ( const QgsScrollBarHighlight &highlight : highlights ) + { + QMap &highlightMap = mHighlightCache[highlight.priority][highlight.color.rgba()]; + insertPosition( &highlightMap, highlight.position ); + } + } + + mIsCacheUpdateScheduled = false; +} + +QRect QgsScrollBarHighlightOverlay::overlayRect() const +{ + QStyleOptionSlider opt = qt_qscrollbarStyleOption( scrollBar() ); + return scrollBar()->style()->subControlRect( QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarGroove, scrollBar() ); +} + +QRect QgsScrollBarHighlightOverlay::handleRect() const +{ + QStyleOptionSlider opt = qt_qscrollbarStyleOption( scrollBar() ); + return scrollBar()->style()->subControlRect( QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSlider, scrollBar() ); +} + +///@endcond PRIVATE + +// +// QgsScrollBarHighlight +// + +QgsScrollBarHighlight::QgsScrollBarHighlight( int category, int position, + const QColor &color, QgsScrollBarHighlight::Priority priority ) + : category( category ) + , position( position ) + , color( color ) + , priority( priority ) +{ +} + + +// +// QgsScrollBarHighlightController +// + +QgsScrollBarHighlightController::QgsScrollBarHighlightController() = default; + +QgsScrollBarHighlightController::~QgsScrollBarHighlightController() +{ + if ( mOverlay ) + delete mOverlay; +} + +QScrollBar *QgsScrollBarHighlightController::scrollBar() const +{ + if ( mScrollArea ) + return mScrollArea->verticalScrollBar(); + + return nullptr; +} + +QAbstractScrollArea *QgsScrollBarHighlightController::scrollArea() const +{ + return mScrollArea; +} + +void QgsScrollBarHighlightController::setScrollArea( QAbstractScrollArea *scrollArea ) +{ + if ( mScrollArea == scrollArea ) + return; + + if ( mOverlay ) + { + delete mOverlay; + mOverlay = nullptr; + } + + mScrollArea = scrollArea; + + if ( mScrollArea ) + { + mOverlay = new QgsScrollBarHighlightOverlay( this ); + mOverlay->scheduleUpdate(); + } +} + +double QgsScrollBarHighlightController::lineHeight() const +{ + return std::ceil( mLineHeight ); +} + +void QgsScrollBarHighlightController::setLineHeight( double lineHeight ) +{ + mLineHeight = lineHeight; +} + +double QgsScrollBarHighlightController::visibleRange() const +{ + return mVisibleRange; +} + +void QgsScrollBarHighlightController::setVisibleRange( double visibleRange ) +{ + mVisibleRange = visibleRange; +} + +double QgsScrollBarHighlightController::margin() const +{ + return mMargin; +} + +void QgsScrollBarHighlightController::setMargin( double margin ) +{ + mMargin = margin; +} + +QHash> QgsScrollBarHighlightController::highlights() const +{ + return mHighlights; +} + +void QgsScrollBarHighlightController::addHighlight( const QgsScrollBarHighlight &highlight ) +{ + if ( !mOverlay ) + return; + + mHighlights[highlight.category] << highlight; + mOverlay->scheduleUpdate(); +} + +void QgsScrollBarHighlightController::removeHighlights( int category ) +{ + if ( !mOverlay ) + return; + + mHighlights.remove( category ); + mOverlay->scheduleUpdate(); +} + +void QgsScrollBarHighlightController::removeAllHighlights() +{ + if ( !mOverlay ) + return; + + mHighlights.clear(); + mOverlay->scheduleUpdate(); +} diff --git a/src/gui/qgsdecoratedscrollbar.h b/src/gui/qgsdecoratedscrollbar.h new file mode 100644 index 0000000000000..a2084b0f1600b --- /dev/null +++ b/src/gui/qgsdecoratedscrollbar.h @@ -0,0 +1,215 @@ +/*************************************************************************** + qgsdecoratedscrollbar.h + -------------------------------------- + Date : May 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 QGSDECORATEDSCROLLBAR_H +#define QGSDECORATEDSCROLLBAR_H + +#include "qgis_gui.h" +#include "qgis_sip.h" + +#include +#include +#include +#include +#include + +class QScrollBar; +class QAbstractScrollArea; +class QgsScrollBarHighlightOverlay; + +// ported from QtCreator's HighlightScrollBarController implementation + +/** + * \ingroup gui + * \brief Encapsulates the details of a highlight in a scrollbar, used alongside QgsScrollBarHighlightController. + * \since QGIS 3.38 + */ +class GUI_EXPORT QgsScrollBarHighlight +{ + public: + + /** + * Priority, which dictates how overlapping highlights are rendered + */ + enum class Priority : int + { + Invalid = -1, //!< Invalid + LowPriority = 0, //!< Low priority, rendered below all other highlights + NormalPriority = 1, //!< Normal priority + HighPriority = 2, //!< High priority + HighestPriority = 3 //!< Highest priority, rendered above all other highlights + }; + + /** + * Constructor for QgsScrollBarHighlight. + */ + QgsScrollBarHighlight( int category, int position, const QColor &color, QgsScrollBarHighlight::Priority priority = QgsScrollBarHighlight::Priority::NormalPriority ); + QgsScrollBarHighlight() = default; + + //! Category ID + int category = -1; + + //! Position in scroll bar + int position = -1; + + //! Highlight color + QColor color; + + //! Priority, which dictates how overlapping highlights are rendered + QgsScrollBarHighlight::Priority priority = QgsScrollBarHighlight::Priority::Invalid; +}; + +/** + * \ingroup gui + * \brief Adds highlights (colored markers) to a scrollbar. + * \since QGIS 3.38 + */ +class GUI_EXPORT QgsScrollBarHighlightController +{ + public: + + QgsScrollBarHighlightController(); + ~QgsScrollBarHighlightController(); + + /** + * Returns the associated scroll bar. + */ + QScrollBar *scrollBar() const; + + /** + * Returns the associated scroll area. + * + * \see setScrollArea() + */ + QAbstractScrollArea *scrollArea() const; + + /** + * Sets the associated scroll bar. + * + * \see scrollArea() + */ + void setScrollArea( QAbstractScrollArea *scrollArea ); + + /** + * Returns the line height for text associated with the scroll area. + * + * \see setLineHeight() + */ + double lineHeight() const; + + /** + * Sets the line \a height for text associated with the scroll area. + * + * \see lineHeight() + */ + void setLineHeight( double height ); + + /** + * Returns the visible range of the scroll area (i.e. the viewport's height). + * + * \see setVisibleRange() + */ + double visibleRange() const; + + /** + * Sets the visible range of the scroll area (i.e. the viewport's height). + * + * \see visibleRange() + */ + void setVisibleRange( double visibleRange ); + + /** + * Returns the document margins for the associated viewport. + * + * \see setMargin() + */ + double margin() const; + + /** + * Sets the document \a margin for the associated viewport. + * + * \see margin() + */ + void setMargin( double margin ); + + /** + * Returns the hash of all highlights in the scrollbar, with highlight categories as hash keys. + * + * \note Not available in Python bindings + */ + QHash> highlights() const SIP_SKIP; + + /** + * Adds a \a highlight to the scrollbar. + */ + void addHighlight( const QgsScrollBarHighlight &highlight ); + + /** + * Removes all highlights with matching \a category from the scrollbar. + */ + void removeHighlights( int category ); + + /** + * Removes all highlights from the scroll bar. + */ + void removeAllHighlights(); + + private: + + QHash > mHighlights; + double mLineHeight = 0.0; + double mVisibleRange = 0.0; // in pixels + double mMargin = 0.0; // in pixels + QAbstractScrollArea *mScrollArea = nullptr; + QPointer mOverlay; +}; + +///@cond PRIVATE +#ifndef SIP_RUN +class QgsScrollBarHighlightOverlay : public QWidget +{ + public: + QgsScrollBarHighlightOverlay( QgsScrollBarHighlightController *scrollBarController ); + + void doResize(); + void doMove(); + void scheduleUpdate(); + + protected: + void paintEvent( QPaintEvent *paintEvent ) override; + bool eventFilter( QObject *object, QEvent *event ) override; + + private: + void drawHighlights( QPainter *painter, + int docStart, + int docSize, + double docSizeToHandleSizeRatio, + int handleOffset, + const QRect &viewport ); + void updateCache(); + QRect overlayRect() const; + QRect handleRect() const; + + // line start to line end + QMap>> mHighlightCache; + + inline QScrollBar *scrollBar() const { return mHighlightController->scrollBar(); } + QgsScrollBarHighlightController *mHighlightController = nullptr; + bool mIsCacheUpdateScheduled = true; +}; +#endif +///@endcond PRIVATE + +#endif // QGSDECORATEDSCROLLBAR_H From a484895363ac653a137855b5962bd2c1994bde3e Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 8 May 2024 12:16:57 +1000 Subject: [PATCH 056/102] Fix some checks --- .../PyQt6/gui/auto_generated/qgsdecoratedscrollbar.sip.in | 4 ++++ python/gui/auto_generated/qgsdecoratedscrollbar.sip.in | 4 ++++ src/gui/qgsdecoratedscrollbar.cpp | 8 ++++---- src/gui/qgsdecoratedscrollbar.h | 6 ++++++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/qgsdecoratedscrollbar.sip.in b/python/PyQt6/gui/auto_generated/qgsdecoratedscrollbar.sip.in index b5de42ab39bd8..66663e70b6690 100644 --- a/python/PyQt6/gui/auto_generated/qgsdecoratedscrollbar.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsdecoratedscrollbar.sip.in @@ -37,7 +37,11 @@ Encapsulates the details of a highlight in a scrollbar, used alongside :py:class %Docstring Constructor for QgsScrollBarHighlight. %End + QgsScrollBarHighlight(); +%Docstring +Default constructor for QgsScrollBarHighlight. +%End int category; diff --git a/python/gui/auto_generated/qgsdecoratedscrollbar.sip.in b/python/gui/auto_generated/qgsdecoratedscrollbar.sip.in index c9a8a0fb389c0..d26e35aaf6b7f 100644 --- a/python/gui/auto_generated/qgsdecoratedscrollbar.sip.in +++ b/python/gui/auto_generated/qgsdecoratedscrollbar.sip.in @@ -37,7 +37,11 @@ Encapsulates the details of a highlight in a scrollbar, used alongside :py:class %Docstring Constructor for QgsScrollBarHighlight. %End + QgsScrollBarHighlight(); +%Docstring +Default constructor for QgsScrollBarHighlight. +%End int category; diff --git a/src/gui/qgsdecoratedscrollbar.cpp b/src/gui/qgsdecoratedscrollbar.cpp index 6c27a47d0132c..e7d0804b87362 100644 --- a/src/gui/qgsdecoratedscrollbar.cpp +++ b/src/gui/qgsdecoratedscrollbar.cpp @@ -115,7 +115,7 @@ void QgsScrollBarHighlightOverlay::paintEvent( QPaintEvent *paintEvent ) // be stretched using the background ratio. const double handleVirtualHeight = sizeDocVisible * backgroundRatio; // Skip the doc above and visible part. - const int offset = qRound( aboveHandleRect.height() + handleVirtualHeight ); + const int offset = std::round( aboveHandleRect.height() + handleVirtualHeight ); drawHighlights( &painter, sizeDocAbove + sizeDocVisible, @@ -137,7 +137,7 @@ void QgsScrollBarHighlightOverlay::paintEvent( QPaintEvent *paintEvent ) // The correction between handle position (int) and accurate position (double) const double correction = aboveHandleRect.height() - accurateHandlePos; // Skip the doc above and apply correction - const int offset = qRound( aboveVirtualHeight + correction ); + const int offset = std::round( aboveVirtualHeight + correction ); drawHighlights( &painter, sizeDocAbove, @@ -186,8 +186,8 @@ void QgsScrollBarHighlightOverlay::drawHighlights( QPainter *painter, if ( posStart > docStart + docSize ) break; - const int height = qMax( qRound( ( posEnd - posStart ) * docSizeToHandleSizeRatio ), 1 ); - const int top = qRound( posStart * docSizeToHandleSizeRatio ) - handleOffset + viewport.y(); + const int height = std::max( static_cast< int >( std::round( ( posEnd - posStart ) * docSizeToHandleSizeRatio ) ), 1 ); + const int top = std::round( posStart * docSizeToHandleSizeRatio ) - handleOffset + viewport.y(); const QRect rect( viewport.left(), top, viewport.width(), height ); painter->fillRect( rect, color ); diff --git a/src/gui/qgsdecoratedscrollbar.h b/src/gui/qgsdecoratedscrollbar.h index a2084b0f1600b..fc85120faf9bd 100644 --- a/src/gui/qgsdecoratedscrollbar.h +++ b/src/gui/qgsdecoratedscrollbar.h @@ -56,6 +56,10 @@ class GUI_EXPORT QgsScrollBarHighlight * Constructor for QgsScrollBarHighlight. */ QgsScrollBarHighlight( int category, int position, const QColor &color, QgsScrollBarHighlight::Priority priority = QgsScrollBarHighlight::Priority::NormalPriority ); + + /** + * Default constructor for QgsScrollBarHighlight. + */ QgsScrollBarHighlight() = default; //! Category ID @@ -180,6 +184,8 @@ class GUI_EXPORT QgsScrollBarHighlightController #ifndef SIP_RUN class QgsScrollBarHighlightOverlay : public QWidget { + Q_OBJECT + public: QgsScrollBarHighlightOverlay( QgsScrollBarHighlightController *scrollBarController ); From 706dc09e4bc3fb273ab4d58cc93ae60cd6fa5bfd Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 8 May 2024 12:29:35 +1000 Subject: [PATCH 057/102] Use qOverload --- src/gui/qgsdecoratedscrollbar.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/qgsdecoratedscrollbar.cpp b/src/gui/qgsdecoratedscrollbar.cpp index e7d0804b87362..5506df773d3bf 100644 --- a/src/gui/qgsdecoratedscrollbar.cpp +++ b/src/gui/qgsdecoratedscrollbar.cpp @@ -54,7 +54,7 @@ void QgsScrollBarHighlightOverlay::scheduleUpdate() return; mIsCacheUpdateScheduled = true; - QMetaObject::invokeMethod( this, QOverload<>::of( &QWidget::update ), Qt::QueuedConnection ); + QMetaObject::invokeMethod( this, qOverload<>( &QWidget::update ), Qt::QueuedConnection ); } void QgsScrollBarHighlightOverlay::paintEvent( QPaintEvent *paintEvent ) From 9d6ff0f897204e8dd47b669e9036793abd6f36c2 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 8 May 2024 13:48:56 +1000 Subject: [PATCH 058/102] Fix clang tidy warnings --- src/gui/qgsdecoratedscrollbar.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/gui/qgsdecoratedscrollbar.cpp b/src/gui/qgsdecoratedscrollbar.cpp index 5506df773d3bf..791ccbcdd85e2 100644 --- a/src/gui/qgsdecoratedscrollbar.cpp +++ b/src/gui/qgsdecoratedscrollbar.cpp @@ -54,7 +54,10 @@ void QgsScrollBarHighlightOverlay::scheduleUpdate() return; mIsCacheUpdateScheduled = true; +// silence false positive leak warning +#ifndef __clang_analyzer__ QMetaObject::invokeMethod( this, qOverload<>( &QWidget::update ), Qt::QueuedConnection ); +#endif } void QgsScrollBarHighlightOverlay::paintEvent( QPaintEvent *paintEvent ) @@ -115,7 +118,7 @@ void QgsScrollBarHighlightOverlay::paintEvent( QPaintEvent *paintEvent ) // be stretched using the background ratio. const double handleVirtualHeight = sizeDocVisible * backgroundRatio; // Skip the doc above and visible part. - const int offset = std::round( aboveHandleRect.height() + handleVirtualHeight ); + const int offset = static_cast< int >( std::round( aboveHandleRect.height() + handleVirtualHeight ) ); drawHighlights( &painter, sizeDocAbove + sizeDocVisible, @@ -137,7 +140,7 @@ void QgsScrollBarHighlightOverlay::paintEvent( QPaintEvent *paintEvent ) // The correction between handle position (int) and accurate position (double) const double correction = aboveHandleRect.height() - accurateHandlePos; // Skip the doc above and apply correction - const int offset = std::round( aboveVirtualHeight + correction ); + const int offset = static_cast< int >( std::round( aboveVirtualHeight + correction ) ); drawHighlights( &painter, sizeDocAbove, @@ -187,7 +190,7 @@ void QgsScrollBarHighlightOverlay::drawHighlights( QPainter *painter, break; const int height = std::max( static_cast< int >( std::round( ( posEnd - posStart ) * docSizeToHandleSizeRatio ) ), 1 ); - const int top = std::round( posStart * docSizeToHandleSizeRatio ) - handleOffset + viewport.y(); + const int top = static_cast< int >( std::round( posStart * docSizeToHandleSizeRatio ) - handleOffset + viewport.y() ); const QRect rect( viewport.left(), top, viewport.width(), height ); painter->fillRect( rect, color ); From a34669e784788aac952f0cd7361361e38facb553 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 8 May 2024 15:13:17 +1000 Subject: [PATCH 059/102] Add code editor color scheme option for search match highlight color --- .../qgscodeeditorcolorscheme.py | 3 +- .../codeeditors/qgscodeeditor.sip.in | 2 ++ .../qgscodeeditorcolorscheme.sip.in | 1 + .../qgscodeeditorcolorscheme.py | 3 +- .../codeeditors/qgscodeeditor.sip.in | 2 ++ .../qgscodeeditorcolorscheme.sip.in | 1 + src/app/options/qgscodeeditoroptions.cpp | 29 +++++++++++++++++-- src/gui/codeeditors/qgscodeeditor.cpp | 8 +++++ src/gui/codeeditors/qgscodeeditor.h | 3 ++ .../codeeditors/qgscodeeditorcolorscheme.h | 1 + .../qgscodeeditorcolorschemeregistry.cpp | 3 ++ src/ui/qgscodeditorsettings.ui | 18 ++++++++++-- 12 files changed, 68 insertions(+), 6 deletions(-) diff --git a/python/PyQt6/gui/auto_additions/qgscodeeditorcolorscheme.py b/python/PyQt6/gui/auto_additions/qgscodeeditorcolorscheme.py index 3253296bc13a4..173a692ba01b2 100644 --- a/python/PyQt6/gui/auto_additions/qgscodeeditorcolorscheme.py +++ b/python/PyQt6/gui/auto_additions/qgscodeeditorcolorscheme.py @@ -35,5 +35,6 @@ QgsCodeEditorColorScheme.ColorRole.FoldIconForeground.__doc__ = "Fold icon foreground color" QgsCodeEditorColorScheme.ColorRole.FoldIconHalo.__doc__ = "Fold icon halo color" QgsCodeEditorColorScheme.ColorRole.IndentationGuide.__doc__ = "Indentation guide line" -QgsCodeEditorColorScheme.ColorRole.__doc__ = "Color roles.\n\n" + '* ``Default``: ' + QgsCodeEditorColorScheme.ColorRole.Default.__doc__ + '\n' + '* ``Keyword``: ' + QgsCodeEditorColorScheme.ColorRole.Keyword.__doc__ + '\n' + '* ``Class``: ' + QgsCodeEditorColorScheme.ColorRole.Class.__doc__ + '\n' + '* ``Method``: ' + QgsCodeEditorColorScheme.ColorRole.Method.__doc__ + '\n' + '* ``Decoration``: ' + QgsCodeEditorColorScheme.ColorRole.Decoration.__doc__ + '\n' + '* ``Number``: ' + QgsCodeEditorColorScheme.ColorRole.Number.__doc__ + '\n' + '* ``Comment``: ' + QgsCodeEditorColorScheme.ColorRole.Comment.__doc__ + '\n' + '* ``CommentLine``: ' + QgsCodeEditorColorScheme.ColorRole.CommentLine.__doc__ + '\n' + '* ``CommentBlock``: ' + QgsCodeEditorColorScheme.ColorRole.CommentBlock.__doc__ + '\n' + '* ``Background``: ' + QgsCodeEditorColorScheme.ColorRole.Background.__doc__ + '\n' + '* ``Cursor``: ' + QgsCodeEditorColorScheme.ColorRole.Cursor.__doc__ + '\n' + '* ``CaretLine``: ' + QgsCodeEditorColorScheme.ColorRole.CaretLine.__doc__ + '\n' + '* ``SingleQuote``: ' + QgsCodeEditorColorScheme.ColorRole.SingleQuote.__doc__ + '\n' + '* ``DoubleQuote``: ' + QgsCodeEditorColorScheme.ColorRole.DoubleQuote.__doc__ + '\n' + '* ``TripleSingleQuote``: ' + QgsCodeEditorColorScheme.ColorRole.TripleSingleQuote.__doc__ + '\n' + '* ``TripleDoubleQuote``: ' + QgsCodeEditorColorScheme.ColorRole.TripleDoubleQuote.__doc__ + '\n' + '* ``Operator``: ' + QgsCodeEditorColorScheme.ColorRole.Operator.__doc__ + '\n' + '* ``QuotedOperator``: ' + QgsCodeEditorColorScheme.ColorRole.QuotedOperator.__doc__ + '\n' + '* ``Identifier``: ' + QgsCodeEditorColorScheme.ColorRole.Identifier.__doc__ + '\n' + '* ``QuotedIdentifier``: ' + QgsCodeEditorColorScheme.ColorRole.QuotedIdentifier.__doc__ + '\n' + '* ``Tag``: ' + QgsCodeEditorColorScheme.ColorRole.Tag.__doc__ + '\n' + '* ``UnknownTag``: ' + QgsCodeEditorColorScheme.ColorRole.UnknownTag.__doc__ + '\n' + '* ``MarginBackground``: ' + QgsCodeEditorColorScheme.ColorRole.MarginBackground.__doc__ + '\n' + '* ``MarginForeground``: ' + QgsCodeEditorColorScheme.ColorRole.MarginForeground.__doc__ + '\n' + '* ``SelectionBackground``: ' + QgsCodeEditorColorScheme.ColorRole.SelectionBackground.__doc__ + '\n' + '* ``SelectionForeground``: ' + QgsCodeEditorColorScheme.ColorRole.SelectionForeground.__doc__ + '\n' + '* ``MatchedBraceBackground``: ' + QgsCodeEditorColorScheme.ColorRole.MatchedBraceBackground.__doc__ + '\n' + '* ``MatchedBraceForeground``: ' + QgsCodeEditorColorScheme.ColorRole.MatchedBraceForeground.__doc__ + '\n' + '* ``Edge``: ' + QgsCodeEditorColorScheme.ColorRole.Edge.__doc__ + '\n' + '* ``Fold``: ' + QgsCodeEditorColorScheme.ColorRole.Fold.__doc__ + '\n' + '* ``Error``: ' + QgsCodeEditorColorScheme.ColorRole.Error.__doc__ + '\n' + '* ``ErrorBackground``: ' + QgsCodeEditorColorScheme.ColorRole.ErrorBackground.__doc__ + '\n' + '* ``FoldIconForeground``: ' + QgsCodeEditorColorScheme.ColorRole.FoldIconForeground.__doc__ + '\n' + '* ``FoldIconHalo``: ' + QgsCodeEditorColorScheme.ColorRole.FoldIconHalo.__doc__ + '\n' + '* ``IndentationGuide``: ' + QgsCodeEditorColorScheme.ColorRole.IndentationGuide.__doc__ +QgsCodeEditorColorScheme.ColorRole.SearchMatchBackground.__doc__ = "Background color for search matches (since QGIS 3.38)" +QgsCodeEditorColorScheme.ColorRole.__doc__ = "Color roles.\n\n" + '* ``Default``: ' + QgsCodeEditorColorScheme.ColorRole.Default.__doc__ + '\n' + '* ``Keyword``: ' + QgsCodeEditorColorScheme.ColorRole.Keyword.__doc__ + '\n' + '* ``Class``: ' + QgsCodeEditorColorScheme.ColorRole.Class.__doc__ + '\n' + '* ``Method``: ' + QgsCodeEditorColorScheme.ColorRole.Method.__doc__ + '\n' + '* ``Decoration``: ' + QgsCodeEditorColorScheme.ColorRole.Decoration.__doc__ + '\n' + '* ``Number``: ' + QgsCodeEditorColorScheme.ColorRole.Number.__doc__ + '\n' + '* ``Comment``: ' + QgsCodeEditorColorScheme.ColorRole.Comment.__doc__ + '\n' + '* ``CommentLine``: ' + QgsCodeEditorColorScheme.ColorRole.CommentLine.__doc__ + '\n' + '* ``CommentBlock``: ' + QgsCodeEditorColorScheme.ColorRole.CommentBlock.__doc__ + '\n' + '* ``Background``: ' + QgsCodeEditorColorScheme.ColorRole.Background.__doc__ + '\n' + '* ``Cursor``: ' + QgsCodeEditorColorScheme.ColorRole.Cursor.__doc__ + '\n' + '* ``CaretLine``: ' + QgsCodeEditorColorScheme.ColorRole.CaretLine.__doc__ + '\n' + '* ``SingleQuote``: ' + QgsCodeEditorColorScheme.ColorRole.SingleQuote.__doc__ + '\n' + '* ``DoubleQuote``: ' + QgsCodeEditorColorScheme.ColorRole.DoubleQuote.__doc__ + '\n' + '* ``TripleSingleQuote``: ' + QgsCodeEditorColorScheme.ColorRole.TripleSingleQuote.__doc__ + '\n' + '* ``TripleDoubleQuote``: ' + QgsCodeEditorColorScheme.ColorRole.TripleDoubleQuote.__doc__ + '\n' + '* ``Operator``: ' + QgsCodeEditorColorScheme.ColorRole.Operator.__doc__ + '\n' + '* ``QuotedOperator``: ' + QgsCodeEditorColorScheme.ColorRole.QuotedOperator.__doc__ + '\n' + '* ``Identifier``: ' + QgsCodeEditorColorScheme.ColorRole.Identifier.__doc__ + '\n' + '* ``QuotedIdentifier``: ' + QgsCodeEditorColorScheme.ColorRole.QuotedIdentifier.__doc__ + '\n' + '* ``Tag``: ' + QgsCodeEditorColorScheme.ColorRole.Tag.__doc__ + '\n' + '* ``UnknownTag``: ' + QgsCodeEditorColorScheme.ColorRole.UnknownTag.__doc__ + '\n' + '* ``MarginBackground``: ' + QgsCodeEditorColorScheme.ColorRole.MarginBackground.__doc__ + '\n' + '* ``MarginForeground``: ' + QgsCodeEditorColorScheme.ColorRole.MarginForeground.__doc__ + '\n' + '* ``SelectionBackground``: ' + QgsCodeEditorColorScheme.ColorRole.SelectionBackground.__doc__ + '\n' + '* ``SelectionForeground``: ' + QgsCodeEditorColorScheme.ColorRole.SelectionForeground.__doc__ + '\n' + '* ``MatchedBraceBackground``: ' + QgsCodeEditorColorScheme.ColorRole.MatchedBraceBackground.__doc__ + '\n' + '* ``MatchedBraceForeground``: ' + QgsCodeEditorColorScheme.ColorRole.MatchedBraceForeground.__doc__ + '\n' + '* ``Edge``: ' + QgsCodeEditorColorScheme.ColorRole.Edge.__doc__ + '\n' + '* ``Fold``: ' + QgsCodeEditorColorScheme.ColorRole.Fold.__doc__ + '\n' + '* ``Error``: ' + QgsCodeEditorColorScheme.ColorRole.Error.__doc__ + '\n' + '* ``ErrorBackground``: ' + QgsCodeEditorColorScheme.ColorRole.ErrorBackground.__doc__ + '\n' + '* ``FoldIconForeground``: ' + QgsCodeEditorColorScheme.ColorRole.FoldIconForeground.__doc__ + '\n' + '* ``FoldIconHalo``: ' + QgsCodeEditorColorScheme.ColorRole.FoldIconHalo.__doc__ + '\n' + '* ``IndentationGuide``: ' + QgsCodeEditorColorScheme.ColorRole.IndentationGuide.__doc__ + '\n' + '* ``SearchMatchBackground``: ' + QgsCodeEditorColorScheme.ColorRole.SearchMatchBackground.__doc__ # -- diff --git a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in index 1f899fc3aef81..8b4bca093bbe5 100644 --- a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in +++ b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in @@ -102,6 +102,8 @@ A text editor based on QScintilla2. typedef QFlags Flags; + static const int SEARCH_RESULT_INDICATOR; + QgsCodeEditor( QWidget *parent /TransferThis/ = 0, const QString &title = QString(), bool folding = false, bool margin = false, QgsCodeEditor::Flags flags = QgsCodeEditor::Flags(), QgsCodeEditor::Mode mode = QgsCodeEditor::Mode::ScriptEditor ); %Docstring Construct a new code editor. diff --git a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorcolorscheme.sip.in b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorcolorscheme.sip.in index 49e317000368c..162ea2db34909 100644 --- a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorcolorscheme.sip.in +++ b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorcolorscheme.sip.in @@ -58,6 +58,7 @@ Defines a color scheme for use in :py:class:`QgsCodeEditor` widgets. FoldIconForeground, FoldIconHalo, IndentationGuide, + SearchMatchBackground, }; QgsCodeEditorColorScheme( const QString &id = QString(), const QString &name = QString() ); diff --git a/python/gui/auto_additions/qgscodeeditorcolorscheme.py b/python/gui/auto_additions/qgscodeeditorcolorscheme.py index 3253296bc13a4..173a692ba01b2 100644 --- a/python/gui/auto_additions/qgscodeeditorcolorscheme.py +++ b/python/gui/auto_additions/qgscodeeditorcolorscheme.py @@ -35,5 +35,6 @@ QgsCodeEditorColorScheme.ColorRole.FoldIconForeground.__doc__ = "Fold icon foreground color" QgsCodeEditorColorScheme.ColorRole.FoldIconHalo.__doc__ = "Fold icon halo color" QgsCodeEditorColorScheme.ColorRole.IndentationGuide.__doc__ = "Indentation guide line" -QgsCodeEditorColorScheme.ColorRole.__doc__ = "Color roles.\n\n" + '* ``Default``: ' + QgsCodeEditorColorScheme.ColorRole.Default.__doc__ + '\n' + '* ``Keyword``: ' + QgsCodeEditorColorScheme.ColorRole.Keyword.__doc__ + '\n' + '* ``Class``: ' + QgsCodeEditorColorScheme.ColorRole.Class.__doc__ + '\n' + '* ``Method``: ' + QgsCodeEditorColorScheme.ColorRole.Method.__doc__ + '\n' + '* ``Decoration``: ' + QgsCodeEditorColorScheme.ColorRole.Decoration.__doc__ + '\n' + '* ``Number``: ' + QgsCodeEditorColorScheme.ColorRole.Number.__doc__ + '\n' + '* ``Comment``: ' + QgsCodeEditorColorScheme.ColorRole.Comment.__doc__ + '\n' + '* ``CommentLine``: ' + QgsCodeEditorColorScheme.ColorRole.CommentLine.__doc__ + '\n' + '* ``CommentBlock``: ' + QgsCodeEditorColorScheme.ColorRole.CommentBlock.__doc__ + '\n' + '* ``Background``: ' + QgsCodeEditorColorScheme.ColorRole.Background.__doc__ + '\n' + '* ``Cursor``: ' + QgsCodeEditorColorScheme.ColorRole.Cursor.__doc__ + '\n' + '* ``CaretLine``: ' + QgsCodeEditorColorScheme.ColorRole.CaretLine.__doc__ + '\n' + '* ``SingleQuote``: ' + QgsCodeEditorColorScheme.ColorRole.SingleQuote.__doc__ + '\n' + '* ``DoubleQuote``: ' + QgsCodeEditorColorScheme.ColorRole.DoubleQuote.__doc__ + '\n' + '* ``TripleSingleQuote``: ' + QgsCodeEditorColorScheme.ColorRole.TripleSingleQuote.__doc__ + '\n' + '* ``TripleDoubleQuote``: ' + QgsCodeEditorColorScheme.ColorRole.TripleDoubleQuote.__doc__ + '\n' + '* ``Operator``: ' + QgsCodeEditorColorScheme.ColorRole.Operator.__doc__ + '\n' + '* ``QuotedOperator``: ' + QgsCodeEditorColorScheme.ColorRole.QuotedOperator.__doc__ + '\n' + '* ``Identifier``: ' + QgsCodeEditorColorScheme.ColorRole.Identifier.__doc__ + '\n' + '* ``QuotedIdentifier``: ' + QgsCodeEditorColorScheme.ColorRole.QuotedIdentifier.__doc__ + '\n' + '* ``Tag``: ' + QgsCodeEditorColorScheme.ColorRole.Tag.__doc__ + '\n' + '* ``UnknownTag``: ' + QgsCodeEditorColorScheme.ColorRole.UnknownTag.__doc__ + '\n' + '* ``MarginBackground``: ' + QgsCodeEditorColorScheme.ColorRole.MarginBackground.__doc__ + '\n' + '* ``MarginForeground``: ' + QgsCodeEditorColorScheme.ColorRole.MarginForeground.__doc__ + '\n' + '* ``SelectionBackground``: ' + QgsCodeEditorColorScheme.ColorRole.SelectionBackground.__doc__ + '\n' + '* ``SelectionForeground``: ' + QgsCodeEditorColorScheme.ColorRole.SelectionForeground.__doc__ + '\n' + '* ``MatchedBraceBackground``: ' + QgsCodeEditorColorScheme.ColorRole.MatchedBraceBackground.__doc__ + '\n' + '* ``MatchedBraceForeground``: ' + QgsCodeEditorColorScheme.ColorRole.MatchedBraceForeground.__doc__ + '\n' + '* ``Edge``: ' + QgsCodeEditorColorScheme.ColorRole.Edge.__doc__ + '\n' + '* ``Fold``: ' + QgsCodeEditorColorScheme.ColorRole.Fold.__doc__ + '\n' + '* ``Error``: ' + QgsCodeEditorColorScheme.ColorRole.Error.__doc__ + '\n' + '* ``ErrorBackground``: ' + QgsCodeEditorColorScheme.ColorRole.ErrorBackground.__doc__ + '\n' + '* ``FoldIconForeground``: ' + QgsCodeEditorColorScheme.ColorRole.FoldIconForeground.__doc__ + '\n' + '* ``FoldIconHalo``: ' + QgsCodeEditorColorScheme.ColorRole.FoldIconHalo.__doc__ + '\n' + '* ``IndentationGuide``: ' + QgsCodeEditorColorScheme.ColorRole.IndentationGuide.__doc__ +QgsCodeEditorColorScheme.ColorRole.SearchMatchBackground.__doc__ = "Background color for search matches (since QGIS 3.38)" +QgsCodeEditorColorScheme.ColorRole.__doc__ = "Color roles.\n\n" + '* ``Default``: ' + QgsCodeEditorColorScheme.ColorRole.Default.__doc__ + '\n' + '* ``Keyword``: ' + QgsCodeEditorColorScheme.ColorRole.Keyword.__doc__ + '\n' + '* ``Class``: ' + QgsCodeEditorColorScheme.ColorRole.Class.__doc__ + '\n' + '* ``Method``: ' + QgsCodeEditorColorScheme.ColorRole.Method.__doc__ + '\n' + '* ``Decoration``: ' + QgsCodeEditorColorScheme.ColorRole.Decoration.__doc__ + '\n' + '* ``Number``: ' + QgsCodeEditorColorScheme.ColorRole.Number.__doc__ + '\n' + '* ``Comment``: ' + QgsCodeEditorColorScheme.ColorRole.Comment.__doc__ + '\n' + '* ``CommentLine``: ' + QgsCodeEditorColorScheme.ColorRole.CommentLine.__doc__ + '\n' + '* ``CommentBlock``: ' + QgsCodeEditorColorScheme.ColorRole.CommentBlock.__doc__ + '\n' + '* ``Background``: ' + QgsCodeEditorColorScheme.ColorRole.Background.__doc__ + '\n' + '* ``Cursor``: ' + QgsCodeEditorColorScheme.ColorRole.Cursor.__doc__ + '\n' + '* ``CaretLine``: ' + QgsCodeEditorColorScheme.ColorRole.CaretLine.__doc__ + '\n' + '* ``SingleQuote``: ' + QgsCodeEditorColorScheme.ColorRole.SingleQuote.__doc__ + '\n' + '* ``DoubleQuote``: ' + QgsCodeEditorColorScheme.ColorRole.DoubleQuote.__doc__ + '\n' + '* ``TripleSingleQuote``: ' + QgsCodeEditorColorScheme.ColorRole.TripleSingleQuote.__doc__ + '\n' + '* ``TripleDoubleQuote``: ' + QgsCodeEditorColorScheme.ColorRole.TripleDoubleQuote.__doc__ + '\n' + '* ``Operator``: ' + QgsCodeEditorColorScheme.ColorRole.Operator.__doc__ + '\n' + '* ``QuotedOperator``: ' + QgsCodeEditorColorScheme.ColorRole.QuotedOperator.__doc__ + '\n' + '* ``Identifier``: ' + QgsCodeEditorColorScheme.ColorRole.Identifier.__doc__ + '\n' + '* ``QuotedIdentifier``: ' + QgsCodeEditorColorScheme.ColorRole.QuotedIdentifier.__doc__ + '\n' + '* ``Tag``: ' + QgsCodeEditorColorScheme.ColorRole.Tag.__doc__ + '\n' + '* ``UnknownTag``: ' + QgsCodeEditorColorScheme.ColorRole.UnknownTag.__doc__ + '\n' + '* ``MarginBackground``: ' + QgsCodeEditorColorScheme.ColorRole.MarginBackground.__doc__ + '\n' + '* ``MarginForeground``: ' + QgsCodeEditorColorScheme.ColorRole.MarginForeground.__doc__ + '\n' + '* ``SelectionBackground``: ' + QgsCodeEditorColorScheme.ColorRole.SelectionBackground.__doc__ + '\n' + '* ``SelectionForeground``: ' + QgsCodeEditorColorScheme.ColorRole.SelectionForeground.__doc__ + '\n' + '* ``MatchedBraceBackground``: ' + QgsCodeEditorColorScheme.ColorRole.MatchedBraceBackground.__doc__ + '\n' + '* ``MatchedBraceForeground``: ' + QgsCodeEditorColorScheme.ColorRole.MatchedBraceForeground.__doc__ + '\n' + '* ``Edge``: ' + QgsCodeEditorColorScheme.ColorRole.Edge.__doc__ + '\n' + '* ``Fold``: ' + QgsCodeEditorColorScheme.ColorRole.Fold.__doc__ + '\n' + '* ``Error``: ' + QgsCodeEditorColorScheme.ColorRole.Error.__doc__ + '\n' + '* ``ErrorBackground``: ' + QgsCodeEditorColorScheme.ColorRole.ErrorBackground.__doc__ + '\n' + '* ``FoldIconForeground``: ' + QgsCodeEditorColorScheme.ColorRole.FoldIconForeground.__doc__ + '\n' + '* ``FoldIconHalo``: ' + QgsCodeEditorColorScheme.ColorRole.FoldIconHalo.__doc__ + '\n' + '* ``IndentationGuide``: ' + QgsCodeEditorColorScheme.ColorRole.IndentationGuide.__doc__ + '\n' + '* ``SearchMatchBackground``: ' + QgsCodeEditorColorScheme.ColorRole.SearchMatchBackground.__doc__ # -- diff --git a/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in b/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in index 289796894565e..966fefb3861e3 100644 --- a/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in +++ b/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in @@ -102,6 +102,8 @@ A text editor based on QScintilla2. typedef QFlags Flags; + static const int SEARCH_RESULT_INDICATOR; + QgsCodeEditor( QWidget *parent /TransferThis/ = 0, const QString &title = QString(), bool folding = false, bool margin = false, QgsCodeEditor::Flags flags = QgsCodeEditor::Flags(), QgsCodeEditor::Mode mode = QgsCodeEditor::Mode::ScriptEditor ); %Docstring Construct a new code editor. diff --git a/python/gui/auto_generated/codeeditors/qgscodeeditorcolorscheme.sip.in b/python/gui/auto_generated/codeeditors/qgscodeeditorcolorscheme.sip.in index 49e317000368c..162ea2db34909 100644 --- a/python/gui/auto_generated/codeeditors/qgscodeeditorcolorscheme.sip.in +++ b/python/gui/auto_generated/codeeditors/qgscodeeditorcolorscheme.sip.in @@ -58,6 +58,7 @@ Defines a color scheme for use in :py:class:`QgsCodeEditor` widgets. FoldIconForeground, FoldIconHalo, IndentationGuide, + SearchMatchBackground, }; QgsCodeEditorColorScheme( const QString &id = QString(), const QString &name = QString() ); diff --git a/src/app/options/qgscodeeditoroptions.cpp b/src/app/options/qgscodeeditoroptions.cpp index 4a6de3f5fa655..f0b56f207dc53 100644 --- a/src/app/options/qgscodeeditoroptions.cpp +++ b/src/app/options/qgscodeeditoroptions.cpp @@ -71,6 +71,7 @@ QgsCodeEditorOptionsWidget::QgsCodeEditorOptionsWidget( QWidget *parent ) {QgsCodeEditorColorScheme::ColorRole::FoldIconForeground, mColorFoldIcon }, {QgsCodeEditorColorScheme::ColorRole::FoldIconHalo, mColorFoldIconHalo }, {QgsCodeEditorColorScheme::ColorRole::IndentationGuide, mColorIndentation }, + {QgsCodeEditorColorScheme::ColorRole::SearchMatchBackground, mColorSearchResult }, }; for ( auto it = mColorButtonMap.constBegin(); it != mColorButtonMap.constEnd(); ++it ) @@ -188,6 +189,12 @@ QgsCodeEditorOptionsWidget::QgsCodeEditorOptionsWidget( QWidget *parent ) mPreviewStackedWidget->setCurrentIndex( mListLanguage->currentRow() ); } ); + auto addSearchHighlight = []( QgsCodeEditor * editor, int start, int length ) + { + editor->SendScintilla( QsciScintilla::SCI_SETINDICATORCURRENT, QgsCodeEditor::SEARCH_RESULT_INDICATOR ); + editor->SendScintilla( QsciScintilla::SCI_INDICATORFILLRANGE, start, length ); + }; + mPythonPreview->setText( R"""(def simple_function(x,y,z): """ Function docstring @@ -204,16 +211,20 @@ def somefunc(param1: str='', param2=0): class SomeClass: """ My class docstring + + A search result """ pass )""" ); + addSearchHighlight( mPythonPreview, 385, 13 ); mExpressionPreview->setText( R"""(aggregate(layer:='rail_stations', aggregate:='collect', -- a comment expression:=centroid($geometry), /* a comment */ - filter:="region_name" = attribute(@parent,'name') + 55 + filter:="region_name" = attribute(@parent,'name') + 55 /* a search result */ ) )"""); + addSearchHighlight( mExpressionPreview, 190, 13 ); mSQLPreview->setText( R"""(CREATE TABLE "my_table" ( "pk" serial NOT NULL PRIMARY KEY, @@ -223,7 +234,9 @@ class SomeClass: -- Retrieve values SELECT count(*) FROM "my_table" WHERE "a_field" > 'a value'; +-- A search result )"""); + addSearchHighlight( mSQLPreview, 209, 13 ); mHtmlPreview->setText(R"""( @@ -234,9 +247,11 @@ SELECT count(*) FROM "my_table" WHERE "a_field" > 'a value';

Sample paragraph

+ )"""); + addSearchHighlight( mHtmlPreview, 196, 13 ); mCssPreview->setText( R"""(@import url(print.css); @@ -250,6 +265,7 @@ p.style_name:lang(en) { background: #600; } +/* A search result */ ul > li, a:hover { line-height: 11px; text-decoration: underline; @@ -261,6 +277,7 @@ ul > li, a:hover { } } )""" ); + addSearchHighlight( mCssPreview, 178, 13 ); mJsPreview->setText( R"""(// my sample JavaScript function @@ -273,10 +290,12 @@ window.onAction(function update() { element.name = 'a string'; element.title= "another string"; + /* A search result */ if (prevPos.x > 100) { element.x += max(100*2, 100); } });)""" ); + addSearchHighlight( mJsPreview, 255, 13 ); mRPreview->setText( R"""(# a comment x <- 1:12 @@ -288,6 +307,7 @@ resample(x[x > 8]) # length 2 a_variable <- "My string" +# a search result `%func_name%` <- function(arg_1,arg_2) { # function body } @@ -297,7 +317,7 @@ a_variable <- "My string" return(x^y) } )"""); - + addSearchHighlight( mRPreview, 181, 13 ); mBashPreview->setText(R"""(#!/bin/bash @@ -309,6 +329,7 @@ a_variable <- "My string" [ ! -d "$1" ] && { echo "Error: $1 does not exist or is not a directory."; exit 1; } +# A search result echo "Files with extension .$2 in $1:" for file in "$1"/*."$2"; do @@ -316,6 +337,7 @@ for file in "$1"/*."$2"; do echo "$(basename "$file"): $((size / 1024)) KB" done )""" ); + addSearchHighlight( mBashPreview, 361, 13 ); mBatchPreview->setText( R"""(@echo off @@ -333,6 +355,8 @@ if not exist %1 ( exit /b 1 ) +REM A search result + echo Files with extension %2 in %1: for %%f in (%1\*.%2) do ( @@ -343,6 +367,7 @@ for %%f in (%1\*.%2) do ( echo Done. )""" ); + addSearchHighlight( mBatchPreview, 367, 13 ); mListLanguage->setCurrentRow( 0 ); mPreviewStackedWidget->setCurrentIndex( 0 ); diff --git a/src/gui/codeeditors/qgscodeeditor.cpp b/src/gui/codeeditors/qgscodeeditor.cpp index efbb94ef36791..a17a78352d71c 100644 --- a/src/gui/codeeditors/qgscodeeditor.cpp +++ b/src/gui/codeeditors/qgscodeeditor.cpp @@ -73,6 +73,7 @@ QMap< QgsCodeEditorColorScheme::ColorRole, QString > QgsCodeEditor::sColorRoleTo {QgsCodeEditorColorScheme::ColorRole::FoldIconForeground, QStringLiteral( "foldIconForeground" ) }, {QgsCodeEditorColorScheme::ColorRole::FoldIconHalo, QStringLiteral( "foldIconHalo" ) }, {QgsCodeEditorColorScheme::ColorRole::IndentationGuide, QStringLiteral( "indentationGuide" ) }, + {QgsCodeEditorColorScheme::ColorRole::SearchMatchBackground, QStringLiteral( "searchMatchBackground" ) } }; @@ -402,6 +403,12 @@ void QgsCodeEditor::runPostLexerConfigurationTasks() SendScintilla( SCI_STYLESETFORE, STYLE_INDENTGUIDE, lexerColor( QgsCodeEditorColorScheme::ColorRole::IndentationGuide ) ); SendScintilla( SCI_STYLESETBACK, STYLE_INDENTGUIDE, lexerColor( QgsCodeEditorColorScheme::ColorRole::IndentationGuide ) ); + SendScintilla( QsciScintilla::SCI_INDICSETSTYLE, SEARCH_RESULT_INDICATOR, QsciScintilla::INDIC_STRAIGHTBOX ); + SendScintilla( QsciScintilla::SCI_INDICSETFORE, SEARCH_RESULT_INDICATOR, lexerColor( QgsCodeEditorColorScheme::ColorRole::SearchMatchBackground ) ); + SendScintilla( QsciScintilla::SCI_INDICSETALPHA, SEARCH_RESULT_INDICATOR, 100 ); + SendScintilla( QsciScintilla::SCI_INDICSETUNDER, SEARCH_RESULT_INDICATOR, true ); + SendScintilla( QsciScintilla::SCI_INDICGETOUTLINEALPHA, SEARCH_RESULT_INDICATOR, 255 ); + if ( mMode == QgsCodeEditor::Mode::CommandInput ) { setCaretLineVisible( false ); @@ -964,6 +971,7 @@ QColor QgsCodeEditor::defaultColor( QgsCodeEditorColorScheme::ColorRole role, co {QgsCodeEditorColorScheme::ColorRole::FoldIconForeground, QStringLiteral( "foldIconForeground" ) }, {QgsCodeEditorColorScheme::ColorRole::FoldIconHalo, QStringLiteral( "foldIconHalo" ) }, {QgsCodeEditorColorScheme::ColorRole::IndentationGuide, QStringLiteral( "indentationGuide" ) }, + {QgsCodeEditorColorScheme::ColorRole::SearchMatchBackground, QStringLiteral( "searchMatchBackground" ) }, }; const QgsCodeEditorColorScheme defaultScheme = QgsGui::codeEditorColorSchemeRegistry()->scheme( QStringLiteral( "default" ) ); diff --git a/src/gui/codeeditors/qgscodeeditor.h b/src/gui/codeeditors/qgscodeeditor.h index 8ef5d11cf0c52..b893c58abbe43 100644 --- a/src/gui/codeeditors/qgscodeeditor.h +++ b/src/gui/codeeditors/qgscodeeditor.h @@ -158,6 +158,9 @@ class GUI_EXPORT QgsCodeEditor : public QsciScintilla Q_DECLARE_FLAGS( Flags, Flag ) Q_FLAG( Flags ) + //! Indicator index for search results + static constexpr int SEARCH_RESULT_INDICATOR = QsciScintilla::INDIC_MAX - 1; + /** * Construct a new code editor. * diff --git a/src/gui/codeeditors/qgscodeeditorcolorscheme.h b/src/gui/codeeditors/qgscodeeditorcolorscheme.h index 13d8f222d4066..754bddc3df78d 100644 --- a/src/gui/codeeditors/qgscodeeditorcolorscheme.h +++ b/src/gui/codeeditors/qgscodeeditorcolorscheme.h @@ -71,6 +71,7 @@ class GUI_EXPORT QgsCodeEditorColorScheme FoldIconForeground, //!< Fold icon foreground color FoldIconHalo, //!< Fold icon halo color IndentationGuide, //!< Indentation guide line + SearchMatchBackground, //!< Background color for search matches (since QGIS 3.38) }; /** diff --git a/src/gui/codeeditors/qgscodeeditorcolorschemeregistry.cpp b/src/gui/codeeditors/qgscodeeditorcolorschemeregistry.cpp index f39c87dc406f1..a340cdc0d8b21 100644 --- a/src/gui/codeeditors/qgscodeeditorcolorschemeregistry.cpp +++ b/src/gui/codeeditors/qgscodeeditorcolorschemeregistry.cpp @@ -56,6 +56,7 @@ QgsCodeEditorColorSchemeRegistry::QgsCodeEditorColorSchemeRegistry() {QgsCodeEditorColorScheme::ColorRole::FoldIconForeground, QColor( "#ffffff" ) }, {QgsCodeEditorColorScheme::ColorRole::FoldIconHalo, QColor( "#000000" ) }, {QgsCodeEditorColorScheme::ColorRole::IndentationGuide, QColor( "#d5d5d5" ) }, + {QgsCodeEditorColorScheme::ColorRole::SearchMatchBackground, QColor( "#dadada" ) }, } ); addColorScheme( defaultScheme ); @@ -97,6 +98,7 @@ QgsCodeEditorColorSchemeRegistry::QgsCodeEditorColorSchemeRegistry() {QgsCodeEditorColorScheme::ColorRole::FoldIconForeground, QColor( "#ffffff" ) }, {QgsCodeEditorColorScheme::ColorRole::FoldIconHalo, QColor( "#93a1a1" ) }, {QgsCodeEditorColorScheme::ColorRole::IndentationGuide, QColor( "#c2beb3" ) }, + {QgsCodeEditorColorScheme::ColorRole::SearchMatchBackground, QColor( "#b58900" ) }, } ); addColorScheme( solarizedLight ); @@ -138,6 +140,7 @@ QgsCodeEditorColorSchemeRegistry::QgsCodeEditorColorSchemeRegistry() {QgsCodeEditorColorScheme::ColorRole::FoldIconForeground, QColor( "#586e75" ) }, {QgsCodeEditorColorScheme::ColorRole::FoldIconHalo, QColor( "#839496" ) }, {QgsCodeEditorColorScheme::ColorRole::IndentationGuide, QColor( "#586E75" ) }, + {QgsCodeEditorColorScheme::ColorRole::SearchMatchBackground, QColor( "#075e42" ) }, } ); addColorScheme( solarizedDark ); } diff --git a/src/ui/qgscodeditorsettings.ui b/src/ui/qgscodeditorsettings.ui index f515bce2d53c4..d362d86c95be7 100644 --- a/src/ui/qgscodeditorsettings.ui +++ b/src/ui/qgscodeditorsettings.ui @@ -244,8 +244,8 @@ 0 0 - 663 - 549 + 677 + 531 @@ -825,6 +825,20 @@
+ + + + + + + + + + + Search result + + +
From 8f77b1ab3e7f0b70eeb01ad24779b342a7ee22eb Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 8 May 2024 15:17:43 +1000 Subject: [PATCH 060/102] Add an explicit 'close' button in the search bar --- src/gui/codeeditors/qgscodeeditorwidget.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/gui/codeeditors/qgscodeeditorwidget.cpp b/src/gui/codeeditors/qgscodeeditorwidget.cpp index fe047f7dc091c..7307d4ba6481b 100644 --- a/src/gui/codeeditors/qgscodeeditorwidget.cpp +++ b/src/gui/codeeditors/qgscodeeditorwidget.cpp @@ -17,6 +17,7 @@ #include "qgscodeeditor.h" #include "qgsfilterlineedit.h" #include "qgsapplication.h" +#include "qgsguiutils.h" #include #include @@ -105,6 +106,25 @@ QgsCodeEditorWidget::QgsCodeEditorWidget( QgsCodeEditor *editor, QWidget *parent mEditor->setFocus(); } ); + QToolButton *closeFindButton = new QToolButton( this ); + closeFindButton->setToolTip( tr( "Close" ) ); + closeFindButton->setMinimumWidth( QgsGuiUtils::scaleIconSize( 44 ) ); + closeFindButton->setStyleSheet( + "QToolButton { border:none; background-color: rgba(0, 0, 0, 0); }" + "QToolButton::menu-button { border:none; background-color: rgba(0, 0, 0, 0); }" ); + closeFindButton->setCursor( Qt::PointingHandCursor ); + closeFindButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconClose.svg" ) ) ); + + const int iconSize = std::max( 18.0, Qgis::UI_SCALE_FACTOR * fontMetrics().height() * 0.9 ); + closeFindButton->setIconSize( QSize( iconSize, iconSize ) ); + closeFindButton->setFixedSize( QSize( iconSize, iconSize ) ); + connect( closeFindButton, &QAbstractButton::clicked, this, [this] + { + hideSearchBar(); + mEditor->setFocus(); + } ); + layoutFind->addWidget( closeFindButton ); + mFindWidget->setLayout( layoutFind ); vl->addWidget( mFindWidget ); mFindWidget->hide(); From c22c31cf62ab8d6f7fe66e0e251a95a88c8691e1 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 8 May 2024 15:18:51 +1000 Subject: [PATCH 061/102] Highlight matching results in code editors while searching --- src/gui/codeeditors/qgscodeeditorwidget.cpp | 42 ++++++++++++++++++++- src/gui/codeeditors/qgscodeeditorwidget.h | 2 + 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/gui/codeeditors/qgscodeeditorwidget.cpp b/src/gui/codeeditors/qgscodeeditorwidget.cpp index 7307d4ba6481b..ae5b9acad806a 100644 --- a/src/gui/codeeditors/qgscodeeditorwidget.cpp +++ b/src/gui/codeeditors/qgscodeeditorwidget.cpp @@ -78,7 +78,7 @@ QgsCodeEditorWidget::QgsCodeEditorWidget( QgsCodeEditor *editor, QWidget *parent findShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut ); connect( findShortcut, &QShortcut::activated, this, [this] { - showSearchBar(); + clearSearchHighlights(); mLineEditFind->setFocus(); if ( mEditor->hasSelectedText() ) { @@ -87,6 +87,7 @@ QgsCodeEditorWidget::QgsCodeEditorWidget( QgsCodeEditor *editor, QWidget *parent mBlockSearching--; } mLineEditFind->selectAll(); + showSearchBar(); } ); QShortcut *findNextShortcut = new QShortcut( QKeySequence::StandardKey::FindNext, this ); @@ -134,11 +135,13 @@ QgsCodeEditorWidget::QgsCodeEditorWidget( QgsCodeEditor *editor, QWidget *parent void QgsCodeEditorWidget::showSearchBar() { + addSearchHighlights(); mFindWidget->show(); } void QgsCodeEditorWidget::hideSearchBar() { + clearSearchHighlights(); mFindWidget->hide(); } @@ -170,6 +173,7 @@ void QgsCodeEditorWidget::textSearchChanged( const QString &text ) } else { + clearSearchHighlights(); mLineEditFind->setStyleSheet( QString() ); mFindNextButton->setEnabled( false ); mFindPrevButton->setEnabled( false ); @@ -181,11 +185,45 @@ void QgsCodeEditorWidget::updateSearch() if ( mBlockSearching ) return; + clearSearchHighlights(); + addSearchHighlights(); + + findText( true, true, true ); +} + +void QgsCodeEditorWidget::addSearchHighlights() +{ const QString searchString = mLineEditFind->text(); if ( searchString.isEmpty() ) return; - findText( true, true, true ); + const long originalStartPos = mEditor->SendScintilla( QsciScintilla::SCI_GETTARGETSTART ); + const long originalEndPos = mEditor->SendScintilla( QsciScintilla::SCI_GETTARGETEND ); + long startPos = 0; + long docEnd = mEditor->length(); + + while ( true ) + { + mEditor->SendScintilla( QsciScintilla::SCI_SETTARGETRANGE, startPos, docEnd ); + const int fstart = mEditor->SendScintilla( QsciScintilla::SCI_SEARCHINTARGET, searchString.length(), searchString.toLocal8Bit().constData() ); + if ( fstart < 0 ) + break; + + startPos = fstart + searchString.length(); + + mEditor->SendScintilla( QsciScintilla::SCI_SETINDICATORCURRENT, QgsCodeEditor::SEARCH_RESULT_INDICATOR ); + mEditor->SendScintilla( QsciScintilla::SCI_INDICATORFILLRANGE, fstart, searchString.length() ); + } + + mEditor->SendScintilla( QsciScintilla::SCI_SETTARGETRANGE, originalStartPos, originalEndPos ); +} + +void QgsCodeEditorWidget::clearSearchHighlights() +{ + long docStart = 0; + long docEnd = mEditor->length(); + mEditor->SendScintilla( QsciScintilla::SCI_SETINDICATORCURRENT, QgsCodeEditor::SEARCH_RESULT_INDICATOR ); + mEditor->SendScintilla( QsciScintilla::SCI_INDICATORCLEARRANGE, docStart, docEnd - docStart ); } void QgsCodeEditorWidget::findText( bool forward, bool findFirst, bool showNotFoundWarning ) diff --git a/src/gui/codeeditors/qgscodeeditorwidget.h b/src/gui/codeeditors/qgscodeeditorwidget.h index bfa80c05a891f..78c6809a188ad 100644 --- a/src/gui/codeeditors/qgscodeeditorwidget.h +++ b/src/gui/codeeditors/qgscodeeditorwidget.h @@ -94,6 +94,8 @@ class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget private: + void clearSearchHighlights(); + void addSearchHighlights(); void findText( bool forward, bool findFirst, bool showNotFoundWarning = false ); QgsCodeEditor *mEditor = nullptr; From 03f967848cdab10bbdf6676b3ca3381ff448edfc Mon Sep 17 00:00:00 2001 From: uclaros Date: Thu, 9 May 2024 12:45:49 +0300 Subject: [PATCH 062/102] add missing tr to header generated by map_to_html_table expression function --- resources/function_help/json/map_to_html_table | 4 ++-- src/core/expression/qgsexpressionfunction.cpp | 2 +- tests/src/core/testqgsexpression.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/function_help/json/map_to_html_table b/resources/function_help/json/map_to_html_table index af17eab3acb3e..4b1df88b5ff40 100644 --- a/resources/function_help/json/map_to_html_table +++ b/resources/function_help/json/map_to_html_table @@ -8,8 +8,8 @@ "description": "the input map" }], "examples": [{ - "expression": "map_to_html_table(map('qgis','rocks'))", - "returns": "
qgis
rocks
" + "expression": "map_to_html_table(map('1','one','2','two'))", + "returns": "
12
onetwo
" }], "tags": ["formatted", "map", "html"] } diff --git a/src/core/expression/qgsexpressionfunction.cpp b/src/core/expression/qgsexpressionfunction.cpp index 31ff412df2ffe..81cd40df84b02 100644 --- a/src/core/expression/qgsexpressionfunction.cpp +++ b/src/core/expression/qgsexpressionfunction.cpp @@ -1901,7 +1901,7 @@ static QVariant fcnMapToHtmlTable( const QVariantList &values, const QgsExpressi QString table { R"html( - + diff --git a/tests/src/core/testqgsexpression.cpp b/tests/src/core/testqgsexpression.cpp index 06cd19e17486b..50b3a637279e8 100644 --- a/tests/src/core/testqgsexpression.cpp +++ b/tests/src/core/testqgsexpression.cpp @@ -1750,7 +1750,7 @@ class TestQgsExpression: public QObject QTest::newRow( "array_replace (map)" ) << "array_replace(array('APP','SHOULD','ROCK'),map('APP','QGIS','SHOULD','DOES'))" << false << QVariant( QVariantList() << "QGIS" << "DOES" << "ROCK" ); // map HTML formatting - QTest::newRow( "map_to_html_table (map)" ) << "map_to_html_table(map('APP','QGIS','','DOES'))" << false << QVariant( "\n
%1
%1
%2
\n \n \n \n \n \n \n
<SHOULD>APP
DOESQGIS
" ); + QTest::newRow( "map_to_html_table (map)" ) << "map_to_html_table(map('APP','QGIS','','DOES'))" << false << QVariant( "\n \n \n \n \n \n \n \n
<SHOULD>APP
DOESQGIS
" ); QTest::newRow( "map_to_html_dl (map)" ) << "map_to_html_dl(map('APP','QGIS','','DOES'))" << false << QVariant( "\n
\n
<SHOULD>
DOES
APP
QGIS
\n
" ); //fuzzy matching From 10559048c05f7ffe48aba288b240e7203efc073e Mon Sep 17 00:00:00 2001 From: uclaros Date: Thu, 9 May 2024 12:47:09 +0300 Subject: [PATCH 063/102] QGIS does rock, but we need comprehensive examples --- resources/function_help/json/from_json | 4 ++-- resources/function_help/json/map_to_hstore | 4 ++-- resources/function_help/json/map_to_html_dl | 4 ++-- resources/function_help/json/to_json | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/resources/function_help/json/from_json b/resources/function_help/json/from_json index 5cd842c36a904..02c358ebb7bbe 100644 --- a/resources/function_help/json/from_json +++ b/resources/function_help/json/from_json @@ -8,8 +8,8 @@ "description": "JSON string" }], "examples": [{ - "expression": "from_json('{\"qgis\":\"rocks\"}')", - "returns": "{ 'qgis': 'rocks' }" + "expression": "from_json('{\"1\":\"one\",\"2\":\"two\"}')", + "returns": "{ '1': 'one', '2': 'two' }" }, { "expression": "from_json('[1,2,3]')", "returns": "[1,2,3]" diff --git a/resources/function_help/json/map_to_hstore b/resources/function_help/json/map_to_hstore index 8bfa9a86480eb..49df34059e881 100644 --- a/resources/function_help/json/map_to_hstore +++ b/resources/function_help/json/map_to_hstore @@ -8,8 +8,8 @@ "description": "the input map" }], "examples": [{ - "expression": "map_to_hstore(map('qgis','rocks'))", - "returns": "'\"qgis\"=>\"rocks\"'" + "expression": "map_to_hstore(map('1','one','2','two'))", + "returns": "'\"1\"=>\"one\"','\"2\"=>\"two\"'" }], "tags": ["formatted", "hstore", "elements", "map", "merge"] } diff --git a/resources/function_help/json/map_to_html_dl b/resources/function_help/json/map_to_html_dl index 0579841ec46be..0a7da8abae2ab 100644 --- a/resources/function_help/json/map_to_html_dl +++ b/resources/function_help/json/map_to_html_dl @@ -8,8 +8,8 @@ "description": "the input map" }], "examples": [{ - "expression": "map_to_html_dl(map('qgis','rocks'))", - "returns": "
qgis
rocks
" + "expression": "map_to_html_dl(map('1','one','2','two'))", + "returns": "
1
one
2
two
" }], "tags": ["formatted", "map", "html"] } diff --git a/resources/function_help/json/to_json b/resources/function_help/json/to_json index 56aa1bab10c10..b8a08df5d6409 100644 --- a/resources/function_help/json/to_json +++ b/resources/function_help/json/to_json @@ -8,8 +8,8 @@ "description": "The input value" }], "examples": [{ - "expression": "to_json(map('qgis','rocks'))", - "returns": "{\"qgis\":\"rocks\"}" + "expression": "to_json(map('1','one','2','two'))", + "returns": "{\"1\":\"one\",\"2\":\"two\"}" }, { "expression": "to_json(array(1,2,3))", "returns": "[1,2,3]" From 570370eb624f6b17100f5d9c410f939c47e9b8d2 Mon Sep 17 00:00:00 2001 From: uclaros Date: Thu, 9 May 2024 16:54:21 +0300 Subject: [PATCH 064/102] Invalidate GDAL network cache when reloading provider data --- src/core/providers/gdal/qgsgdalprovider.cpp | 18 ++++++++++++++++++ src/core/providers/gdal/qgsgdalprovider.h | 3 +++ 2 files changed, 21 insertions(+) diff --git a/src/core/providers/gdal/qgsgdalprovider.cpp b/src/core/providers/gdal/qgsgdalprovider.cpp index b0c74b6778d33..e90046bd58b80 100644 --- a/src/core/providers/gdal/qgsgdalprovider.cpp +++ b/src/core/providers/gdal/qgsgdalprovider.cpp @@ -167,6 +167,8 @@ QgsGdalProvider::QgsGdalProvider( const QString &uri, const ProviderOptions &opt return; } + invalidateNetworkCache(); + mGdalDataset = nullptr; if ( dataset ) { @@ -479,6 +481,7 @@ void QgsGdalProvider::closeDataset() void QgsGdalProvider::reloadProviderData() { QMutexLocker locker( mpMutex ); + invalidateNetworkCache(); closeDataset(); mHasInit = false; @@ -4271,6 +4274,21 @@ Qgis::ProviderStyleStorageCapabilities QgsGdalProvider::styleStorageCapabilities return storageCapabilities; } +void QgsGdalProvider::invalidateNetworkCache() +{ + const QString uri( dataSourceUri() ); + + if ( uri.startsWith( QLatin1String( "/vsicurl/" ) ) || + uri.startsWith( QLatin1String( "/vsis3/" ) ) || + uri.startsWith( QLatin1String( "/vsigs/" ) ) || + uri.startsWith( QLatin1String( "/vsiaz/" ) ) || + uri.startsWith( QLatin1String( "/vsiadls/" ) ) ) + { + QgsDebugMsgLevel( QString( "Invalidating cache for %1" ).arg( uri ), 3 ); + VSICurlPartialClearCache( uri.toUtf8().constData() ); + } +} + // pyramids resampling // see http://www.gdal.org/gdaladdo.html diff --git a/src/core/providers/gdal/qgsgdalprovider.h b/src/core/providers/gdal/qgsgdalprovider.h index 5f828ab7c07b8..0adbca9647825 100644 --- a/src/core/providers/gdal/qgsgdalprovider.h +++ b/src/core/providers/gdal/qgsgdalprovider.h @@ -372,6 +372,9 @@ class QgsGdalProvider final: public QgsRasterDataProvider, QgsGdalProviderBase const QgsRectangle &reqExtent, int bufferWidthPix, int bufferHeightPix ); + + //! Invalidate GDAL /vsicurl/ RAM cache for this uri + void invalidateNetworkCache(); }; /** From aa39eabde1e9bd3f9c94012fd98a4af1b88e3d76 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 2 May 2024 13:10:44 +1000 Subject: [PATCH 065/102] Add framework for feature renderer level data defined properties --- .../PyQt6/core/auto_additions/qgsrenderer.py | 5 ++ .../symbology/qgsrenderer.sip.in | 51 ++++++++++++++ .../symbology/qgsrenderer.sip.in | 51 ++++++++++++++ src/core/symbology/qgsrenderer.cpp | 40 ++++++++++- src/core/symbology/qgsrenderer.h | 67 +++++++++++++++++++ 5 files changed, 213 insertions(+), 1 deletion(-) diff --git a/python/PyQt6/core/auto_additions/qgsrenderer.py b/python/PyQt6/core/auto_additions/qgsrenderer.py index 43fd1e3a65e5b..43e3b60be4339 100644 --- a/python/PyQt6/core/auto_additions/qgsrenderer.py +++ b/python/PyQt6/core/auto_additions/qgsrenderer.py @@ -1,4 +1,9 @@ # The following has been generated automatically from src/core/symbology/qgsrenderer.h +# monkey patching scoped based enum +QgsFeatureRenderer.Property.HeatmapRadius.__doc__ = "Heatmap renderer radius" +QgsFeatureRenderer.Property.HeatmapMaximum.__doc__ = "Heatmap maximum value" +QgsFeatureRenderer.Property.__doc__ = "Data definable properties for renderers.\n\n.. versionadded:: 3.38\n\n" + '* ``HeatmapRadius``: ' + QgsFeatureRenderer.Property.HeatmapRadius.__doc__ + '\n' + '* ``HeatmapMaximum``: ' + QgsFeatureRenderer.Property.HeatmapMaximum.__doc__ +# -- QgsFeatureRenderer.SymbolLevels = QgsFeatureRenderer.Capability.SymbolLevels QgsFeatureRenderer.MoreSymbolsPerFeature = QgsFeatureRenderer.Capability.MoreSymbolsPerFeature QgsFeatureRenderer.Filter = QgsFeatureRenderer.Capability.Filter diff --git a/python/PyQt6/core/auto_generated/symbology/qgsrenderer.sip.in b/python/PyQt6/core/auto_generated/symbology/qgsrenderer.sip.in index e1a70d0359166..8aecfd9e772db 100644 --- a/python/PyQt6/core/auto_generated/symbology/qgsrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/symbology/qgsrenderer.sip.in @@ -82,6 +82,20 @@ class QgsFeatureRenderer %End public: + enum class Property /BaseType=IntEnum/ + { + HeatmapRadius, + HeatmapMaximum, + }; + + static const QgsPropertiesDefinition &propertyDefinitions(); +%Docstring +Returns the symbol property definitions. + +.. versionadded:: 3.18 +%End + + static QgsFeatureRenderer *defaultRenderer( Qgis::GeometryType geomType ) /Factory/; %Docstring Returns a new renderer - used by default in vector layers @@ -449,6 +463,42 @@ Sets whether the renderer should be rendered to a raster destination. would result in a large, complex vector output. .. seealso:: :py:func:`forceRasterRender` +%End + + void setDataDefinedProperty( Property key, const QgsProperty &property ); +%Docstring +Sets a data defined property for the renderer. Any existing property with the same key +will be overwritten. + +.. seealso:: :py:func:`dataDefinedProperties` + +.. seealso:: Property + +.. versionadded:: 3.38 +%End + + QgsPropertyCollection &dataDefinedProperties(); +%Docstring +Returns a reference to the renderer's property collection, used for data defined overrides. + +.. seealso:: :py:func:`setDataDefinedProperties` + +.. seealso:: Property + +.. versionadded:: 3.38 +%End + + + void setDataDefinedProperties( const QgsPropertyCollection &collection ); +%Docstring +Sets the renderer's property collection, used for data defined overrides. + +:param collection: property collection. Existing properties will be replaced. + +.. seealso:: :py:func:`dataDefinedProperties` + + +.. versionadded:: 3.38 %End double referenceScale() const; @@ -567,6 +617,7 @@ Currently clones - Reference scale - Symbol levels enabled/disabled - Force raster render enabled/disabled +- Data defined properties :param destRenderer: destination renderer for copied effect diff --git a/python/core/auto_generated/symbology/qgsrenderer.sip.in b/python/core/auto_generated/symbology/qgsrenderer.sip.in index 5e51ddc6863b5..a7db1eaa021a9 100644 --- a/python/core/auto_generated/symbology/qgsrenderer.sip.in +++ b/python/core/auto_generated/symbology/qgsrenderer.sip.in @@ -82,6 +82,20 @@ class QgsFeatureRenderer %End public: + enum class Property + { + HeatmapRadius, + HeatmapMaximum, + }; + + static const QgsPropertiesDefinition &propertyDefinitions(); +%Docstring +Returns the symbol property definitions. + +.. versionadded:: 3.18 +%End + + static QgsFeatureRenderer *defaultRenderer( Qgis::GeometryType geomType ) /Factory/; %Docstring Returns a new renderer - used by default in vector layers @@ -449,6 +463,42 @@ Sets whether the renderer should be rendered to a raster destination. would result in a large, complex vector output. .. seealso:: :py:func:`forceRasterRender` +%End + + void setDataDefinedProperty( Property key, const QgsProperty &property ); +%Docstring +Sets a data defined property for the renderer. Any existing property with the same key +will be overwritten. + +.. seealso:: :py:func:`dataDefinedProperties` + +.. seealso:: Property + +.. versionadded:: 3.38 +%End + + QgsPropertyCollection &dataDefinedProperties(); +%Docstring +Returns a reference to the renderer's property collection, used for data defined overrides. + +.. seealso:: :py:func:`setDataDefinedProperties` + +.. seealso:: Property + +.. versionadded:: 3.38 +%End + + + void setDataDefinedProperties( const QgsPropertyCollection &collection ); +%Docstring +Sets the renderer's property collection, used for data defined overrides. + +:param collection: property collection. Existing properties will be replaced. + +.. seealso:: :py:func:`dataDefinedProperties` + + +.. versionadded:: 3.38 %End double referenceScale() const; @@ -567,6 +617,7 @@ Currently clones - Reference scale - Symbol levels enabled/disabled - Force raster render enabled/disabled +- Data defined properties :param destRenderer: destination renderer for copied effect diff --git a/src/core/symbology/qgsrenderer.cpp b/src/core/symbology/qgsrenderer.cpp index 6d10414b99a19..9713510aec8c6 100644 --- a/src/core/symbology/qgsrenderer.cpp +++ b/src/core/symbology/qgsrenderer.cpp @@ -39,6 +39,8 @@ #include #include +QgsPropertiesDefinition QgsFeatureRenderer::sPropertyDefinitions; + QPointF QgsFeatureRenderer::_getPoint( QgsRenderContext &context, const QgsPoint &point ) { return QgsSymbol::_getPoint( context, point ); @@ -57,6 +59,7 @@ void QgsFeatureRenderer::copyRendererData( QgsFeatureRenderer *destRenderer ) co destRenderer->mOrderBy = mOrderBy; destRenderer->mOrderByEnabled = mOrderByEnabled; destRenderer->mReferenceScale = mReferenceScale; + destRenderer->mDataDefinedProperties = mDataDefinedProperties; } QgsFeatureRenderer::QgsFeatureRenderer( const QString &type ) @@ -71,6 +74,12 @@ QgsFeatureRenderer::~QgsFeatureRenderer() delete mPaintEffect; } +const QgsPropertiesDefinition &QgsFeatureRenderer::propertyDefinitions() +{ + QgsFeatureRenderer::initPropertyDefinitions(); + return sPropertyDefinitions; +} + QgsFeatureRenderer *QgsFeatureRenderer::defaultRenderer( Qgis::GeometryType geomType ) { return new QgsSingleSymbolRenderer( QgsSymbol::defaultSymbol( geomType ) ); @@ -88,7 +97,7 @@ QSet< QString > QgsFeatureRenderer::legendKeysForFeature( const QgsFeature &feat return QSet< QString >(); } -void QgsFeatureRenderer::startRender( QgsRenderContext &, const QgsFields & ) +void QgsFeatureRenderer::startRender( QgsRenderContext &context, const QgsFields & ) { #ifdef QGISDEBUG if ( !mThread ) @@ -100,6 +109,8 @@ void QgsFeatureRenderer::startRender( QgsRenderContext &, const QgsFields & ) Q_ASSERT_X( mThread == QThread::currentThread(), "QgsFeatureRenderer::startRender", "startRender called in a different thread - use a cloned renderer instead" ); } #endif + + mDataDefinedProperties.prepare( context.expressionContext() ); } bool QgsFeatureRenderer::canSkipRender() @@ -186,6 +197,10 @@ QgsFeatureRenderer *QgsFeatureRenderer::load( QDomElement &element, const QgsRea const QDomElement orderByElem = element.firstChildElement( QStringLiteral( "orderby" ) ); r->mOrderBy.load( orderByElem ); r->setOrderByEnabled( element.attribute( QStringLiteral( "enableorderby" ), QStringLiteral( "0" ) ).toInt() ); + + const QDomElement elemDataDefinedProperties = element.firstChildElement( QStringLiteral( "data-defined-properties" ) ); + if ( !elemDataDefinedProperties.isNull() ) + r->mDataDefinedProperties.readXml( elemDataDefinedProperties, propertyDefinitions() ); } return r; } @@ -207,6 +222,10 @@ void QgsFeatureRenderer::saveRendererData( QDomDocument &doc, QDomElement &rende rendererElem.setAttribute( QStringLiteral( "symbollevels" ), ( mUsingSymbolLevels ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ) ); rendererElem.setAttribute( QStringLiteral( "referencescale" ), mReferenceScale ); + QDomElement elemDataDefinedProperties = doc.createElement( QStringLiteral( "data-defined-properties" ) ); + mDataDefinedProperties.writeXml( elemDataDefinedProperties, propertyDefinitions() ); + rendererElem.appendChild( elemDataDefinedProperties ); + if ( mPaintEffect && !QgsPaintEffectRegistry::isDefaultStack( mPaintEffect ) ) mPaintEffect->saveProperties( doc, rendererElem ); @@ -491,6 +510,11 @@ void QgsFeatureRenderer::setPaintEffect( QgsPaintEffect *effect ) mPaintEffect = effect; } +void QgsFeatureRenderer::setDataDefinedProperty( Property key, const QgsProperty &property ) +{ + mDataDefinedProperties.setProperty( key, property ); +} + QgsFeatureRequest::OrderBy QgsFeatureRenderer::orderBy() const { return mOrderBy; @@ -560,6 +584,20 @@ void QgsFeatureRenderer::convertSymbolRotation( QgsSymbol *symbol, const QString } } +void QgsFeatureRenderer::initPropertyDefinitions() +{ + if ( !sPropertyDefinitions.isEmpty() ) + return; + + QString origin = QStringLiteral( "renderer" ); + + sPropertyDefinitions = QgsPropertiesDefinition + { + { static_cast< int >( QgsFeatureRenderer::Property::HeatmapRadius ), QgsPropertyDefinition( "heatmapRadius", QObject::tr( "Radius" ), QgsPropertyDefinition::DoublePositive, origin )}, + { static_cast< int >( QgsFeatureRenderer::Property::HeatmapMaximum ), QgsPropertyDefinition( "heatmapMaximum", QObject::tr( "Maximum" ), QgsPropertyDefinition::DoublePositive, origin )}, + }; +} + QgsSymbol *QgsSymbolLevelItem::symbol() const { return mSymbol; diff --git a/src/core/symbology/qgsrenderer.h b/src/core/symbology/qgsrenderer.h index f4fec0b041acf..ccad7f1138796 100644 --- a/src/core/symbology/qgsrenderer.h +++ b/src/core/symbology/qgsrenderer.h @@ -23,6 +23,7 @@ #include "qgsfields.h" #include "qgsfeaturerequest.h" #include "qgsconfig.h" +#include "qgspropertycollection.h" #include #include @@ -136,6 +137,24 @@ class CORE_EXPORT QgsFeatureRenderer #endif public: + + /** + * Data definable properties for renderers. + * + * \since QGIS 3.38 + */ + enum class Property : int + { + HeatmapRadius, //!< Heatmap renderer radius + HeatmapMaximum, //!< Heatmap maximum value + }; + + /** + * Returns the symbol property definitions. + * \since QGIS 3.18 + */ + static const QgsPropertiesDefinition &propertyDefinitions(); + // renderer takes ownership of its symbols! //! Returns a new renderer - used by default in vector layers @@ -481,6 +500,47 @@ class CORE_EXPORT QgsFeatureRenderer */ void setForceRasterRender( bool forceRaster ) { mForceRaster = forceRaster; } + /** + * Sets a data defined property for the renderer. Any existing property with the same key + * will be overwritten. + * + * \see dataDefinedProperties() + * \see Property + * + * \since QGIS 3.38 + */ + void setDataDefinedProperty( Property key, const QgsProperty &property ); + + /** + * Returns a reference to the renderer's property collection, used for data defined overrides. + * + * \see setDataDefinedProperties() + * \see Property + * + * \since QGIS 3.38 + */ + QgsPropertyCollection &dataDefinedProperties() { return mDataDefinedProperties; } + + /** + * Returns a reference to the renderer's property collection, used for data defined overrides. + * + * \see setDataDefinedProperties() + * + * \since QGIS 3.38 + */ + const QgsPropertyCollection &dataDefinedProperties() const { return mDataDefinedProperties; } SIP_SKIP + + /** + * Sets the renderer's property collection, used for data defined overrides. + * + * \param collection property collection. Existing properties will be replaced. + * + * \see dataDefinedProperties() + * + * \since QGIS 3.38 + */ + void setDataDefinedProperties( const QgsPropertyCollection &collection ) { mDataDefinedProperties = collection; } + /** * Returns the symbology reference scale. * @@ -578,6 +638,7 @@ class CORE_EXPORT QgsFeatureRenderer * - Reference scale * - Symbol levels enabled/disabled * - Force raster render enabled/disabled + * - Data defined properties * * \param destRenderer destination renderer for copied effect * \since QGIS 3.22 @@ -656,11 +717,17 @@ class CORE_EXPORT QgsFeatureRenderer QgsFeatureRenderer &operator=( const QgsFeatureRenderer & ); #endif + static void initPropertyDefinitions(); + //! Property definitions + static QgsPropertiesDefinition sPropertyDefinitions; + #ifdef QGISDEBUG //! Pointer to thread in which startRender was first called QThread *mThread = nullptr; #endif + QgsPropertyCollection mDataDefinedProperties; + Q_DISABLE_COPY( QgsFeatureRenderer ) }; From 19436fa9b115297a898b279d2f29b0a279a96d84 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 2 May 2024 13:32:46 +1000 Subject: [PATCH 066/102] Add framework for data defined property buttons at feature renderer level --- .../qgscategorizedsymbolrendererwidget.sip.in | 2 + .../qgsembeddedsymbolrendererwidget.sip.in | 2 +- .../qgsgraduatedsymbolrendererwidget.sip.in | 2 + .../symbology/qgsheatmaprendererwidget.sip.in | 2 + .../qgspointclusterrendererwidget.sip.in | 2 +- .../qgspointdisplacementrendererwidget.sip.in | 2 +- .../symbology/qgsrendererwidget.sip.in | 11 +++- .../qgscategorizedsymbolrendererwidget.sip.in | 2 + .../qgsembeddedsymbolrendererwidget.sip.in | 2 +- .../qgsgraduatedsymbolrendererwidget.sip.in | 2 + .../symbology/qgsheatmaprendererwidget.sip.in | 2 + .../qgspointclusterrendererwidget.sip.in | 2 +- .../qgspointdisplacementrendererwidget.sip.in | 2 +- .../symbology/qgsrendererwidget.sip.in | 11 +++- .../qgscategorizedsymbolrendererwidget.h | 5 +- .../qgsembeddedsymbolrendererwidget.h | 2 +- .../qgsgraduatedsymbolrendererwidget.h | 4 +- src/gui/symbology/qgsheatmaprendererwidget.h | 5 +- .../symbology/qgspointclusterrendererwidget.h | 2 +- .../qgspointdisplacementrendererwidget.h | 3 +- src/gui/symbology/qgsrendererwidget.cpp | 52 +++++++++++++++++++ src/gui/symbology/qgsrendererwidget.h | 15 +++++- 22 files changed, 112 insertions(+), 22 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/symbology/qgscategorizedsymbolrendererwidget.sip.in b/python/PyQt6/gui/auto_generated/symbology/qgscategorizedsymbolrendererwidget.sip.in index 806252ab2ab5b..38ae70ab6b14e 100644 --- a/python/PyQt6/gui/auto_generated/symbology/qgscategorizedsymbolrendererwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/symbology/qgscategorizedsymbolrendererwidget.sip.in @@ -35,6 +35,8 @@ class QgsCategorizedSymbolRendererWidget : QgsRendererWidget virtual void setContext( const QgsSymbolWidgetContext &context ); + virtual QgsExpressionContext createExpressionContext() const; + int matchToSymbols( QgsStyle *style ); %Docstring diff --git a/python/PyQt6/gui/auto_generated/symbology/qgsembeddedsymbolrendererwidget.sip.in b/python/PyQt6/gui/auto_generated/symbology/qgsembeddedsymbolrendererwidget.sip.in index 74e36c341a671..f50e3778474fc 100644 --- a/python/PyQt6/gui/auto_generated/symbology/qgsembeddedsymbolrendererwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/symbology/qgsembeddedsymbolrendererwidget.sip.in @@ -8,7 +8,7 @@ -class QgsEmbeddedSymbolRendererWidget : QgsRendererWidget, QgsExpressionContextGenerator +class QgsEmbeddedSymbolRendererWidget : QgsRendererWidget { %Docstring(signature="appended") A widget used represent options of a :py:class:`QgsEmbeddedSymbolRenderer` diff --git a/python/PyQt6/gui/auto_generated/symbology/qgsgraduatedsymbolrendererwidget.sip.in b/python/PyQt6/gui/auto_generated/symbology/qgsgraduatedsymbolrendererwidget.sip.in index 0d033cc2d56f9..699e388177e2b 100644 --- a/python/PyQt6/gui/auto_generated/symbology/qgsgraduatedsymbolrendererwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/symbology/qgsgraduatedsymbolrendererwidget.sip.in @@ -30,6 +30,8 @@ class QgsGraduatedSymbolRendererWidget : QgsRendererWidget virtual void setContext( const QgsSymbolWidgetContext &context ); + virtual QgsExpressionContext createExpressionContext() const; + public slots: void graduatedColumnChanged( const QString &field ); diff --git a/python/PyQt6/gui/auto_generated/symbology/qgsheatmaprendererwidget.sip.in b/python/PyQt6/gui/auto_generated/symbology/qgsheatmaprendererwidget.sip.in index f5a0cf7aa6173..1a0250db77d6b 100644 --- a/python/PyQt6/gui/auto_generated/symbology/qgsheatmaprendererwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/symbology/qgsheatmaprendererwidget.sip.in @@ -39,6 +39,8 @@ Constructor virtual void setContext( const QgsSymbolWidgetContext &context ); + virtual QgsExpressionContext createExpressionContext() const; + }; diff --git a/python/PyQt6/gui/auto_generated/symbology/qgspointclusterrendererwidget.sip.in b/python/PyQt6/gui/auto_generated/symbology/qgspointclusterrendererwidget.sip.in index fa4e7f9df7b57..a35daf3e2d2a8 100644 --- a/python/PyQt6/gui/auto_generated/symbology/qgspointclusterrendererwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/symbology/qgspointclusterrendererwidget.sip.in @@ -11,7 +11,7 @@ -class QgsPointClusterRendererWidget: QgsRendererWidget, QgsExpressionContextGenerator +class QgsPointClusterRendererWidget: QgsRendererWidget { %Docstring(signature="appended") A widget which allows configuration of the properties for a :py:class:`QgsPointClusterRenderer`. diff --git a/python/PyQt6/gui/auto_generated/symbology/qgspointdisplacementrendererwidget.sip.in b/python/PyQt6/gui/auto_generated/symbology/qgspointdisplacementrendererwidget.sip.in index fea5f06266b1f..85f064ef55bae 100644 --- a/python/PyQt6/gui/auto_generated/symbology/qgspointdisplacementrendererwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/symbology/qgspointdisplacementrendererwidget.sip.in @@ -10,7 +10,7 @@ -class QgsPointDisplacementRendererWidget: QgsRendererWidget, QgsExpressionContextGenerator +class QgsPointDisplacementRendererWidget: QgsRendererWidget { %TypeHeaderCode diff --git a/python/PyQt6/gui/auto_generated/symbology/qgsrendererwidget.sip.in b/python/PyQt6/gui/auto_generated/symbology/qgsrendererwidget.sip.in index 521fc8d006d65..fc6614f1d377f 100644 --- a/python/PyQt6/gui/auto_generated/symbology/qgsrendererwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/symbology/qgsrendererwidget.sip.in @@ -8,7 +8,7 @@ -class QgsRendererWidget : QgsPanelWidget +class QgsRendererWidget : QgsPanelWidget, QgsExpressionContextGenerator { %Docstring(signature="appended") Base class for renderer settings widgets. @@ -27,6 +27,8 @@ WORKFLOW: %End public: QgsRendererWidget( QgsVectorLayer *layer, QgsStyle *style ); + virtual QgsExpressionContext createExpressionContext() const; + virtual QgsFeatureRenderer *renderer() = 0; %Docstring @@ -112,6 +114,13 @@ The ``levels`` argument defines the updated list of symbols with rendering passe The ``enabled`` arguments specifies if symbol levels should be enabled for the renderer. .. versionadded:: 3.20 +%End + + void registerDataDefinedButton( QgsPropertyOverrideButton *button, QgsFeatureRenderer::Property key ); +%Docstring +Registers a data defined override button. Handles setting up connections +for the button and initializing the button to show the correct descriptions +and help text for the associated property. %End protected slots: diff --git a/python/gui/auto_generated/symbology/qgscategorizedsymbolrendererwidget.sip.in b/python/gui/auto_generated/symbology/qgscategorizedsymbolrendererwidget.sip.in index b1c10ce22d73d..2e4f5a7278adc 100644 --- a/python/gui/auto_generated/symbology/qgscategorizedsymbolrendererwidget.sip.in +++ b/python/gui/auto_generated/symbology/qgscategorizedsymbolrendererwidget.sip.in @@ -35,6 +35,8 @@ class QgsCategorizedSymbolRendererWidget : QgsRendererWidget virtual void setContext( const QgsSymbolWidgetContext &context ); + virtual QgsExpressionContext createExpressionContext() const; + int matchToSymbols( QgsStyle *style ); %Docstring diff --git a/python/gui/auto_generated/symbology/qgsembeddedsymbolrendererwidget.sip.in b/python/gui/auto_generated/symbology/qgsembeddedsymbolrendererwidget.sip.in index 74e36c341a671..f50e3778474fc 100644 --- a/python/gui/auto_generated/symbology/qgsembeddedsymbolrendererwidget.sip.in +++ b/python/gui/auto_generated/symbology/qgsembeddedsymbolrendererwidget.sip.in @@ -8,7 +8,7 @@ -class QgsEmbeddedSymbolRendererWidget : QgsRendererWidget, QgsExpressionContextGenerator +class QgsEmbeddedSymbolRendererWidget : QgsRendererWidget { %Docstring(signature="appended") A widget used represent options of a :py:class:`QgsEmbeddedSymbolRenderer` diff --git a/python/gui/auto_generated/symbology/qgsgraduatedsymbolrendererwidget.sip.in b/python/gui/auto_generated/symbology/qgsgraduatedsymbolrendererwidget.sip.in index 0d033cc2d56f9..699e388177e2b 100644 --- a/python/gui/auto_generated/symbology/qgsgraduatedsymbolrendererwidget.sip.in +++ b/python/gui/auto_generated/symbology/qgsgraduatedsymbolrendererwidget.sip.in @@ -30,6 +30,8 @@ class QgsGraduatedSymbolRendererWidget : QgsRendererWidget virtual void setContext( const QgsSymbolWidgetContext &context ); + virtual QgsExpressionContext createExpressionContext() const; + public slots: void graduatedColumnChanged( const QString &field ); diff --git a/python/gui/auto_generated/symbology/qgsheatmaprendererwidget.sip.in b/python/gui/auto_generated/symbology/qgsheatmaprendererwidget.sip.in index f5a0cf7aa6173..1a0250db77d6b 100644 --- a/python/gui/auto_generated/symbology/qgsheatmaprendererwidget.sip.in +++ b/python/gui/auto_generated/symbology/qgsheatmaprendererwidget.sip.in @@ -39,6 +39,8 @@ Constructor virtual void setContext( const QgsSymbolWidgetContext &context ); + virtual QgsExpressionContext createExpressionContext() const; + }; diff --git a/python/gui/auto_generated/symbology/qgspointclusterrendererwidget.sip.in b/python/gui/auto_generated/symbology/qgspointclusterrendererwidget.sip.in index fa4e7f9df7b57..a35daf3e2d2a8 100644 --- a/python/gui/auto_generated/symbology/qgspointclusterrendererwidget.sip.in +++ b/python/gui/auto_generated/symbology/qgspointclusterrendererwidget.sip.in @@ -11,7 +11,7 @@ -class QgsPointClusterRendererWidget: QgsRendererWidget, QgsExpressionContextGenerator +class QgsPointClusterRendererWidget: QgsRendererWidget { %Docstring(signature="appended") A widget which allows configuration of the properties for a :py:class:`QgsPointClusterRenderer`. diff --git a/python/gui/auto_generated/symbology/qgspointdisplacementrendererwidget.sip.in b/python/gui/auto_generated/symbology/qgspointdisplacementrendererwidget.sip.in index fea5f06266b1f..85f064ef55bae 100644 --- a/python/gui/auto_generated/symbology/qgspointdisplacementrendererwidget.sip.in +++ b/python/gui/auto_generated/symbology/qgspointdisplacementrendererwidget.sip.in @@ -10,7 +10,7 @@ -class QgsPointDisplacementRendererWidget: QgsRendererWidget, QgsExpressionContextGenerator +class QgsPointDisplacementRendererWidget: QgsRendererWidget { %TypeHeaderCode diff --git a/python/gui/auto_generated/symbology/qgsrendererwidget.sip.in b/python/gui/auto_generated/symbology/qgsrendererwidget.sip.in index 521fc8d006d65..fc6614f1d377f 100644 --- a/python/gui/auto_generated/symbology/qgsrendererwidget.sip.in +++ b/python/gui/auto_generated/symbology/qgsrendererwidget.sip.in @@ -8,7 +8,7 @@ -class QgsRendererWidget : QgsPanelWidget +class QgsRendererWidget : QgsPanelWidget, QgsExpressionContextGenerator { %Docstring(signature="appended") Base class for renderer settings widgets. @@ -27,6 +27,8 @@ WORKFLOW: %End public: QgsRendererWidget( QgsVectorLayer *layer, QgsStyle *style ); + virtual QgsExpressionContext createExpressionContext() const; + virtual QgsFeatureRenderer *renderer() = 0; %Docstring @@ -112,6 +114,13 @@ The ``levels`` argument defines the updated list of symbols with rendering passe The ``enabled`` arguments specifies if symbol levels should be enabled for the renderer. .. versionadded:: 3.20 +%End + + void registerDataDefinedButton( QgsPropertyOverrideButton *button, QgsFeatureRenderer::Property key ); +%Docstring +Registers a data defined override button. Handles setting up connections +for the button and initializing the button to show the correct descriptions +and help text for the associated property. %End protected slots: diff --git a/src/gui/symbology/qgscategorizedsymbolrendererwidget.h b/src/gui/symbology/qgscategorizedsymbolrendererwidget.h index f6121b4e37cb6..f4c9420788194 100644 --- a/src/gui/symbology/qgscategorizedsymbolrendererwidget.h +++ b/src/gui/symbology/qgscategorizedsymbolrendererwidget.h @@ -112,7 +112,7 @@ class QgsCategorizedRendererViewItemDelegate: public QStyledItemDelegate * \ingroup gui * \class QgsCategorizedSymbolRendererWidget */ -class GUI_EXPORT QgsCategorizedSymbolRendererWidget : public QgsRendererWidget, private Ui::QgsCategorizedSymbolRendererWidget, private QgsExpressionContextGenerator +class GUI_EXPORT QgsCategorizedSymbolRendererWidget : public QgsRendererWidget, private Ui::QgsCategorizedSymbolRendererWidget { Q_OBJECT public: @@ -140,6 +140,7 @@ class GUI_EXPORT QgsCategorizedSymbolRendererWidget : public QgsRendererWidget, QgsFeatureRenderer *renderer() override; void setContext( const QgsSymbolWidgetContext &context ) override; void disableSymbolLevels() override SIP_SKIP; + QgsExpressionContext createExpressionContext() const override; /** * Replaces category symbols with the symbols from a style that have a matching @@ -257,8 +258,6 @@ class GUI_EXPORT QgsCategorizedSymbolRendererWidget : public QgsRendererWidget, QAction *mUnmergeCategoriesAction = nullptr; QAction *mActionLevels = nullptr; - QgsExpressionContext createExpressionContext() const override; - friend class TestQgsCategorizedRendererWidget; }; diff --git a/src/gui/symbology/qgsembeddedsymbolrendererwidget.h b/src/gui/symbology/qgsembeddedsymbolrendererwidget.h index 72348f0ded071..89ba1dc0328a5 100644 --- a/src/gui/symbology/qgsembeddedsymbolrendererwidget.h +++ b/src/gui/symbology/qgsembeddedsymbolrendererwidget.h @@ -29,7 +29,7 @@ class QgsEmbeddedSymbolRenderer; * * \since QGIS 3.20 */ -class GUI_EXPORT QgsEmbeddedSymbolRendererWidget : public QgsRendererWidget, public QgsExpressionContextGenerator, private Ui::QgsEmbeddedSymbolRendererWidgetBase +class GUI_EXPORT QgsEmbeddedSymbolRendererWidget : public QgsRendererWidget, private Ui::QgsEmbeddedSymbolRendererWidgetBase { Q_OBJECT diff --git a/src/gui/symbology/qgsgraduatedsymbolrendererwidget.h b/src/gui/symbology/qgsgraduatedsymbolrendererwidget.h index 94acdeefd7f75..e284ae6192b0c 100644 --- a/src/gui/symbology/qgsgraduatedsymbolrendererwidget.h +++ b/src/gui/symbology/qgsgraduatedsymbolrendererwidget.h @@ -92,7 +92,7 @@ class QgsGraduatedSymbolRendererViewStyle: public QgsProxyStyle * \ingroup gui * \class QgsGraduatedSymbolRendererWidget */ -class GUI_EXPORT QgsGraduatedSymbolRendererWidget : public QgsRendererWidget, private Ui::QgsGraduatedSymbolRendererWidget, private QgsExpressionContextGenerator +class GUI_EXPORT QgsGraduatedSymbolRendererWidget : public QgsRendererWidget, private Ui::QgsGraduatedSymbolRendererWidget { Q_OBJECT @@ -105,6 +105,7 @@ class GUI_EXPORT QgsGraduatedSymbolRendererWidget : public QgsRendererWidget, pr QgsFeatureRenderer *renderer() override; void setContext( const QgsSymbolWidgetContext &context ) override; void disableSymbolLevels() override SIP_SKIP; + QgsExpressionContext createExpressionContext() const override; public slots: void graduatedColumnChanged( const QString &field ); @@ -186,7 +187,6 @@ class GUI_EXPORT QgsGraduatedSymbolRendererWidget : public QgsRendererWidget, pr SizeMode }; - QgsExpressionContext createExpressionContext() const override; void toggleMethodWidgets( MethodMode mode ); void clearParameterWidgets(); diff --git a/src/gui/symbology/qgsheatmaprendererwidget.h b/src/gui/symbology/qgsheatmaprendererwidget.h index d17645f1ac950..08c8b256b895b 100644 --- a/src/gui/symbology/qgsheatmaprendererwidget.h +++ b/src/gui/symbology/qgsheatmaprendererwidget.h @@ -27,7 +27,7 @@ class QgsHeatmapRenderer; * \ingroup gui * \class QgsHeatmapRendererWidget */ -class GUI_EXPORT QgsHeatmapRendererWidget : public QgsRendererWidget, private Ui::QgsHeatmapRendererWidgetBase, private QgsExpressionContextGenerator +class GUI_EXPORT QgsHeatmapRendererWidget : public QgsRendererWidget, private Ui::QgsHeatmapRendererWidgetBase { Q_OBJECT @@ -52,12 +52,11 @@ class GUI_EXPORT QgsHeatmapRendererWidget : public QgsRendererWidget, private Ui QgsFeatureRenderer *renderer() override; void setContext( const QgsSymbolWidgetContext &context ) override; + QgsExpressionContext createExpressionContext() const override; private: std::unique_ptr< QgsHeatmapRenderer > mRenderer; - QgsExpressionContext createExpressionContext() const override; - private slots: void applyColorRamp(); diff --git a/src/gui/symbology/qgspointclusterrendererwidget.h b/src/gui/symbology/qgspointclusterrendererwidget.h index d5ca52383daf3..d6997b2a6a5ec 100644 --- a/src/gui/symbology/qgspointclusterrendererwidget.h +++ b/src/gui/symbology/qgspointclusterrendererwidget.h @@ -32,7 +32,7 @@ class QgsPointClusterRenderer; * \brief A widget which allows configuration of the properties for a QgsPointClusterRenderer. */ -class GUI_EXPORT QgsPointClusterRendererWidget: public QgsRendererWidget, public QgsExpressionContextGenerator, private Ui::QgsPointClusterRendererWidgetBase +class GUI_EXPORT QgsPointClusterRendererWidget: public QgsRendererWidget, private Ui::QgsPointClusterRendererWidgetBase { Q_OBJECT diff --git a/src/gui/symbology/qgspointdisplacementrendererwidget.h b/src/gui/symbology/qgspointdisplacementrendererwidget.h index 60436aebfa5c5..cc086a65c8575 100644 --- a/src/gui/symbology/qgspointdisplacementrendererwidget.h +++ b/src/gui/symbology/qgspointdisplacementrendererwidget.h @@ -21,7 +21,6 @@ #include "ui_qgspointdisplacementrendererwidgetbase.h" #include "qgis_sip.h" #include "qgsrendererwidget.h" -#include "qgsexpressioncontextgenerator.h" #include "qgis_gui.h" class QgsPointDisplacementRenderer; @@ -30,7 +29,7 @@ class QgsPointDisplacementRenderer; * \ingroup gui * \class QgsPointDisplacementRendererWidget */ -class GUI_EXPORT QgsPointDisplacementRendererWidget: public QgsRendererWidget, public QgsExpressionContextGenerator, private Ui::QgsPointDisplacementRendererWidgetBase +class GUI_EXPORT QgsPointDisplacementRendererWidget: public QgsRendererWidget, private Ui::QgsPointDisplacementRendererWidgetBase { Q_OBJECT public: diff --git a/src/gui/symbology/qgsrendererwidget.cpp b/src/gui/symbology/qgsrendererwidget.cpp index 44cbcfe8bf0e1..d7bc2c63e602a 100644 --- a/src/gui/symbology/qgsrendererwidget.cpp +++ b/src/gui/symbology/qgsrendererwidget.cpp @@ -314,6 +314,14 @@ void QgsRendererWidget::copySymbol() QApplication::clipboard()->setMimeData( QgsSymbolLayerUtils::symbolToMimeData( symbolList.at( 0 ) ) ); } +void QgsRendererWidget::updateDataDefinedProperty() +{ + QgsPropertyOverrideButton *button = qobject_cast( sender() ); + const QgsFeatureRenderer::Property key = static_cast< QgsFeatureRenderer::Property >( button->propertyKey() ); + renderer()->setDataDefinedProperty( key, button->toProperty() ); + emit widgetChanged(); +} + void QgsRendererWidget::showSymbolLevelsDialog( QgsFeatureRenderer *r ) { QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( this ); @@ -388,6 +396,50 @@ void QgsRendererWidget::setSymbolLevels( const QList< QgsLegendSymbolItem > &, b } +void QgsRendererWidget::registerDataDefinedButton( QgsPropertyOverrideButton *button, QgsFeatureRenderer::Property key ) +{ + // note that we don't specify the layer here -- we don't want to expose a choice of fields for renderer level buttons, + // as the settings apply to the WHOLE layer and aren't evaluated on a feature-by-feature basis + button->init( static_cast< int >( key ), renderer()->dataDefinedProperties(), QgsFeatureRenderer::propertyDefinitions(), nullptr, true ); + connect( button, &QgsPropertyOverrideButton::changed, this, &QgsRendererWidget::updateDataDefinedProperty ); + + button->registerExpressionContextGenerator( this ); +} + +QgsExpressionContext QgsRendererWidget::createExpressionContext() const +{ + if ( auto *lExpressionContext = mContext.expressionContext() ) + return *lExpressionContext; + + QgsExpressionContext expContext( mContext.globalProjectAtlasMapLayerScopes( vectorLayer() ) ); + + // additional scopes + const auto constAdditionalExpressionContextScopes = mContext.additionalExpressionContextScopes(); + for ( const QgsExpressionContextScope &scope : constAdditionalExpressionContextScopes ) + { + expContext.appendScope( new QgsExpressionContextScope( scope ) ); + } + + //TODO - show actual value + expContext.setOriginalValueVariable( QVariant() ); + + QStringList highlights; + highlights << QgsExpressionContext::EXPR_ORIGINAL_VALUE; + + if ( expContext.hasVariable( QStringLiteral( "zoom_level" ) ) ) + { + highlights << QStringLiteral( "zoom_level" ); + } + if ( expContext.hasVariable( QStringLiteral( "vector_tile_zoom" ) ) ) + { + highlights << QStringLiteral( "vector_tile_zoom" ); + } + + expContext.setHighlightedVariables( highlights ); + + return expContext; +} + // // QgsDataDefinedValueDialog // diff --git a/src/gui/symbology/qgsrendererwidget.h b/src/gui/symbology/qgsrendererwidget.h index 0882691ccd00d..96789bd82b5d7 100644 --- a/src/gui/symbology/qgsrendererwidget.h +++ b/src/gui/symbology/qgsrendererwidget.h @@ -20,15 +20,17 @@ #include #include "qgspanelwidget.h" #include "qgssymbolwidgetcontext.h" +#include "qgsrenderer.h" +#include "qgsexpressioncontextgenerator.h" class QgsDataDefinedSizeLegend; class QgsDataDefinedSizeLegendWidget; class QgsVectorLayer; class QgsStyle; -class QgsFeatureRenderer; class QgsMapCanvas; class QgsMarkerSymbol; class QgsLegendSymbolItem; +class QgsPropertyOverrideButton; /** * \ingroup gui @@ -42,11 +44,12 @@ class QgsLegendSymbolItem; * - on any change of renderer type, create some default (dummy?) version and change the stacked widget * - when clicked OK/Apply, get the renderer from active widget and clone it for the layer */ -class GUI_EXPORT QgsRendererWidget : public QgsPanelWidget +class GUI_EXPORT QgsRendererWidget : public QgsPanelWidget, public QgsExpressionContextGenerator { Q_OBJECT public: QgsRendererWidget( QgsVectorLayer *layer, QgsStyle *style ); + QgsExpressionContext createExpressionContext() const override; //! Returns pointer to the renderer (no transfer of ownership) virtual QgsFeatureRenderer *renderer() = 0; @@ -151,6 +154,13 @@ class GUI_EXPORT QgsRendererWidget : public QgsPanelWidget */ virtual void setSymbolLevels( const QList< QgsLegendSymbolItem > &levels, bool enabled ); + /** + * Registers a data defined override button. Handles setting up connections + * for the button and initializing the button to show the correct descriptions + * and help text for the associated property. + */ + void registerDataDefinedButton( QgsPropertyOverrideButton *button, QgsFeatureRenderer::Property key ); + protected slots: void contextMenuViewCategories( QPoint p ); //! Change color of selected symbols @@ -180,6 +190,7 @@ class GUI_EXPORT QgsRendererWidget : public QgsPanelWidget private slots: void copySymbol(); + void updateDataDefinedProperty(); private: From 2f5b247bd7dbdf3d1ccf395ed703fa4b96932523 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 2 May 2024 13:19:41 +1000 Subject: [PATCH 067/102] Add data defined control over heatmap radius and maximum value This allows users to define the radius and maximum as a value which makes sense for the current situation, e.g the current map scale or current print atlas feature. Sponsored by Rubicon Concierge Real Estate Services --- src/core/symbology/qgsheatmaprenderer.cpp | 9 + .../symbology/qgsheatmaprendererwidget.cpp | 3 + .../qgsheatmaprendererwidgetbase.ui | 244 ++++++++++-------- tests/src/python/test_qgsheatmaprenderer.py | 152 ++++++++++- .../expected_data_defined_maximum.png | Bin 0 -> 471523 bytes .../expected_data_defined_radius.png | Bin 0 -> 471523 bytes .../expected_render_heatmap.png | Bin 0 -> 471523 bytes 7 files changed, 295 insertions(+), 113 deletions(-) create mode 100644 tests/testdata/control_images/heatmap_renderer/expected_data_defined_maximum/expected_data_defined_maximum.png create mode 100644 tests/testdata/control_images/heatmap_renderer/expected_data_defined_radius/expected_data_defined_radius.png create mode 100644 tests/testdata/control_images/heatmap_renderer/expected_render_heatmap/expected_render_heatmap.png diff --git a/src/core/symbology/qgsheatmaprenderer.cpp b/src/core/symbology/qgsheatmaprenderer.cpp index 71bc8788ad6f8..98dfa74453c60 100644 --- a/src/core/symbology/qgsheatmaprenderer.cpp +++ b/src/core/symbology/qgsheatmaprenderer.cpp @@ -68,6 +68,15 @@ void QgsHeatmapRenderer::startRender( QgsRenderContext &context, const QgsFields mWeightExpression->prepare( &context.expressionContext() ); } + bool ok = false; + const double dataDefinedExplicitMax = dataDefinedProperties().valueAsDouble( Property::HeatmapMaximum, context.expressionContext(), mExplicitMax, &ok ); + if ( ok ) + mExplicitMax = dataDefinedExplicitMax; + + const double dataDefinedRadius = dataDefinedProperties().valueAsDouble( Property::HeatmapRadius, context.expressionContext(), mRadius, &ok ); + if ( ok ) + mRadius = dataDefinedRadius; + initializeValues( context ); } diff --git a/src/gui/symbology/qgsheatmaprendererwidget.cpp b/src/gui/symbology/qgsheatmaprendererwidget.cpp index e380a9d19fd62..bdb1d01676aa9 100644 --- a/src/gui/symbology/qgsheatmaprendererwidget.cpp +++ b/src/gui/symbology/qgsheatmaprendererwidget.cpp @@ -142,6 +142,9 @@ QgsHeatmapRendererWidget::QgsHeatmapRendererWidget( QgsVectorLayer *layer, QgsSt mWeightExpressionWidget->setLayer( layer ); mWeightExpressionWidget->setField( mRenderer->weightExpression() ); connect( mWeightExpressionWidget, static_cast < void ( QgsFieldExpressionWidget::* )( const QString & ) >( &QgsFieldExpressionWidget::fieldChanged ), this, &QgsHeatmapRendererWidget::weightExpressionChanged ); + + registerDataDefinedButton( mRadiusDDBtn, QgsFeatureRenderer::Property::HeatmapRadius ); + registerDataDefinedButton( mMaximumValueDDBtn, QgsFeatureRenderer::Property::HeatmapMaximum ); } QgsHeatmapRendererWidget::~QgsHeatmapRendererWidget() = default; diff --git a/src/ui/symbollayer/qgsheatmaprendererwidgetbase.ui b/src/ui/symbollayer/qgsheatmaprendererwidgetbase.ui index 42fdcbe34f772..a94898d8d1043 100644 --- a/src/ui/symbollayer/qgsheatmaprendererwidgetbase.ui +++ b/src/ui/symbollayer/qgsheatmaprendererwidgetbase.ui @@ -6,14 +6,34 @@ 0 0 - 323 + 334 386 Form - + + + + + Maximum value + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + @@ -21,7 +41,90 @@ - + + + + Rendering quality + + + + + + + + 1 + 0 + + + + Automatic + + + 6 + + + 99999999.000000000000000 + + + 0.200000000000000 + + + + + + + Radius + + + + + + + Weight points by + + + + + + + + + + 1 + 0 + + + + 6 + + + 99999999.000000000000000 + + + 0.200000000000000 + + + + + + + + + + + + + + + + + + + + + + + @@ -43,14 +146,26 @@ - - - - Radius + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Qt::StrongFocus - + @@ -99,108 +214,7 @@ - - - - - 1 - 0 - - - - Automatic - - - 6 - - - 99999999.000000000000000 - - - 0.200000000000000 - - - - - - - Rendering quality - - - - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Qt::StrongFocus - - - - - - - Weight points by - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - 1 - 0 - - - - 6 - - - 99999999.000000000000000 - - - 0.200000000000000 - - - - - - - - - - - - Maximum value - - - - + 0 @@ -257,18 +271,26 @@ 1 - QgsPanelWidth + QgsPanelWidget QWidget
qgspanelwidget.h
1
+ + QgsPropertyOverrideButton + QToolButton +
qgspropertyoverridebutton.h
+
btnColorRamp mRadiusSpinBox + mRadiusDDBtn mMaxSpinBox + mMaximumValueDDBtn mWeightExpressionWidget mQualitySlider + mLegendSettingsButton diff --git a/tests/src/python/test_qgsheatmaprenderer.py b/tests/src/python/test_qgsheatmaprenderer.py index 4dda8a235d9b2..d34439addf82d 100644 --- a/tests/src/python/test_qgsheatmaprenderer.py +++ b/tests/src/python/test_qgsheatmaprenderer.py @@ -10,13 +10,21 @@ import os +from qgis.PyQt.QtCore import QSize from qgis.PyQt.QtGui import QColor from qgis.PyQt.QtXml import QDomDocument from qgis.core import ( + Qgis, QgsHeatmapRenderer, QgsGradientColorRamp, QgsReadWriteContext, - QgsColorRampLegendNodeSettings + QgsColorRampLegendNodeSettings, + QgsProperty, + QgsFeatureRenderer, + QgsVectorLayer, + QgsMapSettings, + QgsExpressionContext, + QgsExpressionContextScope ) import unittest from qgis.testing import start_app, QgisTestCase @@ -30,6 +38,10 @@ class TestQgsHeatmapRenderer(QgisTestCase): + @classmethod + def control_path_prefix(cls): + return 'heatmap_renderer' + def test_clone(self): """ Test cloning renderer @@ -44,11 +56,28 @@ def test_clone(self): legend_settings.setMinimumLabel('my min') renderer.setLegendSettings(legend_settings) + renderer.setDataDefinedProperty( + QgsFeatureRenderer.Property.HeatmapRadius, + QgsProperty.fromField('radius_field') + ) + renderer.setDataDefinedProperty( + QgsFeatureRenderer.Property.HeatmapMaximum, + QgsProperty.fromField('max_field') + ) + renderer2 = renderer.clone() self.assertEqual(renderer2.colorRamp().color1(), QColor(255, 0, 0)) self.assertEqual(renderer2.colorRamp().color2(), QColor(255, 200, 100)) self.assertEqual(renderer2.legendSettings().minimumLabel(), 'my min') self.assertEqual(renderer2.legendSettings().maximumLabel(), 'my max') + self.assertEqual( + renderer2.dataDefinedProperties().property(QgsFeatureRenderer.Property.HeatmapRadius).field(), + 'radius_field' + ) + self.assertEqual( + renderer2.dataDefinedProperties().property(QgsFeatureRenderer.Property.HeatmapMaximum).field(), + 'max_field' + ) def test_write_read_xml(self): """ @@ -64,15 +93,134 @@ def test_write_read_xml(self): legend_settings.setMinimumLabel('my min') renderer.setLegendSettings(legend_settings) + renderer.setDataDefinedProperty( + QgsFeatureRenderer.Property.HeatmapRadius, + QgsProperty.fromField('radius_field') + ) + renderer.setDataDefinedProperty( + QgsFeatureRenderer.Property.HeatmapMaximum, + QgsProperty.fromField('max_field') + ) + doc = QDomDocument("testdoc") elem = renderer.save(doc, QgsReadWriteContext()) - renderer2 = QgsHeatmapRenderer.create(elem, QgsReadWriteContext()) + renderer2 = QgsFeatureRenderer.load(elem, QgsReadWriteContext()) self.assertEqual(renderer2.colorRamp().color1(), QColor(255, 0, 0)) self.assertEqual(renderer2.colorRamp().color2(), QColor(255, 200, 100)) self.assertEqual(renderer2.legendSettings().minimumLabel(), 'my min') self.assertEqual(renderer2.legendSettings().maximumLabel(), 'my max') + self.assertEqual( + renderer2.dataDefinedProperties().property(QgsFeatureRenderer.Property.HeatmapRadius).field(), + 'radius_field' + ) + self.assertEqual( + renderer2.dataDefinedProperties().property(QgsFeatureRenderer.Property.HeatmapMaximum).field(), + 'max_field' + ) + + def test_render(self): + """ + Test heatmap rendering + """ + layer = QgsVectorLayer(os.path.join(TEST_DATA_DIR, 'points.shp'), 'Points', 'ogr') + self.assertTrue(layer.isValid()) + + renderer = QgsHeatmapRenderer() + renderer.setColorRamp( + QgsGradientColorRamp(QColor(255, 0, 0), QColor(255, 200, 100))) + renderer.setRadius(20) + renderer.setRadiusUnit(Qgis.RenderUnit.Millimeters) + layer.setRenderer(renderer) + + mapsettings = QgsMapSettings() + mapsettings.setOutputSize(QSize(400, 400)) + mapsettings.setOutputDpi(96) + mapsettings.setExtent(layer.extent()) + mapsettings.setLayers([layer]) + + self.assertTrue( + self.render_map_settings_check( + 'Render heatmap', + 'render_heatmap', + mapsettings) + ) + + def test_data_defined_radius(self): + """ + Test heatmap rendering with data defined radius + """ + layer = QgsVectorLayer(os.path.join(TEST_DATA_DIR, 'points.shp'), 'Points', 'ogr') + self.assertTrue(layer.isValid()) + + renderer = QgsHeatmapRenderer() + renderer.setColorRamp( + QgsGradientColorRamp(QColor(255, 0, 0), QColor(255, 200, 100))) + renderer.setRadius(20) + renderer.setRadiusUnit(Qgis.RenderUnit.Millimeters) + renderer.setDataDefinedProperty( + QgsFeatureRenderer.Property.HeatmapRadius, + QgsProperty.fromExpression('@my_var * 2') + ) + + layer.setRenderer(renderer) + + mapsettings = QgsMapSettings() + mapsettings.setOutputSize(QSize(400, 400)) + mapsettings.setOutputDpi(96) + mapsettings.setExtent(layer.extent()) + mapsettings.setLayers([layer]) + scope = QgsExpressionContextScope() + scope.setVariable('my_var', 20) + context = QgsExpressionContext() + context.appendScope(scope) + mapsettings.setExpressionContext(context) + + self.assertTrue( + self.render_map_settings_check( + 'Render heatmap with data defined radius', + 'data_defined_radius', + mapsettings) + ) + + def test_data_defined_maximum(self): + """ + Test heatmap rendering with data defined maximum value + """ + layer = QgsVectorLayer(os.path.join(TEST_DATA_DIR, 'points.shp'), 'Points', 'ogr') + self.assertTrue(layer.isValid()) + + renderer = QgsHeatmapRenderer() + renderer.setColorRamp( + QgsGradientColorRamp(QColor(255, 0, 0), QColor(255, 200, 100))) + renderer.setRadius(20) + renderer.setRadiusUnit(Qgis.RenderUnit.Millimeters) + renderer.setDataDefinedProperty( + QgsFeatureRenderer.Property.HeatmapMaximum, + QgsProperty.fromExpression('@my_var * 2') + ) + + layer.setRenderer(renderer) + + mapsettings = QgsMapSettings() + mapsettings.setOutputSize(QSize(400, 400)) + mapsettings.setOutputDpi(96) + mapsettings.setExtent(layer.extent()) + mapsettings.setLayers([layer]) + scope = QgsExpressionContextScope() + scope.setVariable('my_var', 0.5) + context = QgsExpressionContext() + context.appendScope(scope) + mapsettings.setExpressionContext(context) + + self.assertTrue( + self.render_map_settings_check( + 'Render heatmap with data defined maximum', + 'data_defined_maximum', + mapsettings) + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/testdata/control_images/heatmap_renderer/expected_data_defined_maximum/expected_data_defined_maximum.png b/tests/testdata/control_images/heatmap_renderer/expected_data_defined_maximum/expected_data_defined_maximum.png new file mode 100644 index 0000000000000000000000000000000000000000..afd19ebd49c96c41e8c745a504fb3af177917498 GIT binary patch literal 471523 zcmeI*Z>VKide`xNtIoakue+0B5aSqhg4h_MAetc%@lTH(XIe}gLn$#LNP;-vo53h? zVyQTVkr~A|D*94TCSVaoWgvl|F)&F+ks&Y%5i%%YNC-p_rl-2<)~zaU_jKocp1So^ z?Nj%jbM`*x*L2g*S!eCFp7mX8?R}r$dG_9){@C|?!11~r~kY5iI4up9~y=mpMLwlgYWpl|M>A?_~7uy>#yDX+XutV+jnjq zJRXMAf1mQt!NIcd682^6#ieB(?L*2BFE8s@zUaz1);6^}fAqQ7OBc#$A2W3Ahm=3U zK6$iksIwnf=i#!xb!ZMa_W2)`^^>Y&WE?~^FFrchZ7c= z9F|`0l*QS9r^c1Rgg^*{KnTo?KrTo#o3OYW0wE9ryGnq#`*et%+@ZW+=-tU~UD#gA zxfWV(`$giT{$*@s5N*ezL_h>Q7y>J^Xg^ef!&ZXpWP}I$iKI~cq`dR_fPRVW2a3yB zrnXwkc$p04U+a`|7?yI{@z~Dp*mkVCb<&peMK>?{=2+ZEWiKG*Szp^WyD@^Q8*WkV z-4|-y46(GM58C=U%7@bG@`K}&mHxPAPgpZ!&LR4FiDT`z^Ispdm);k&7RNqI0QzUY zOi2iYKnR4uvWXbC|j9J>BhCB?Of}dV|UGW`$!3^V`cmH z>%jEi^`q^$WL0cFIA8i;zvr>(YVcekXS39l*%$0DnV*MY84Iv5i+P~(qyM5j)g!7gus3gIF;;(fel>^X_W{Jla&;TO(LJ=M_qX% z@!6MBhJJPu!5F!~Fkicq>5jRs%WaIG;4kv2>(CHhyVOBvQHrt7I&9Yu7=~`_ z@;Q%P+Na}gJ#>DSawY@wG!+$cOaNxdcyui>Ei^I=xLjDt#5J$n?VhrZ$I8dO;CQ=n zjmfgJ9ox6xGT=3Qa9TW9EHhxg^VJ{u&0+ZZa{-1yR@S$?W7w@L$F4u^+JJ-mw5w0` zBK?)}-TdGd+07AVR3Q)oArJzyAdm~vET$<=hCm2}!0r=R42GrJhOvgXos8hKxqSXr zDMzNWTsgw_ONh;}`lip?$_w2*8a@}vj#zpr1q_3doj1eEr}M3Z`NsK;bGT5XA$=}( z)232V)78qwvUObYmxH#f%KY-TQIU%+A7K}NsIPC@W3u%cw1t~MNIe-tvXF4m~ zSpRZ!@5oje&Gw7k_)zNL&zO1lSejqE>705FasQeDJ7$f^P}?!eMB4#(Yr}O?_PYn~ zm(7mduYqT@ArJx~5CYR8kPFhZ<|E#Oz?KO7hr>_5f0+xgB`4Bt2>cd-#h`pq`^pqb6WU!^ZHWC_!+pZ&!fGWK!++&Fr2rp~zuB5?tw*g)(L=?o;B~!0r&J8Fz-LCF~{JM_E95 zP1#M!Zp>RLJ=QkutN6opv`7ua%q5hjyv!Xjv)M1oEUQX(_h41zJA4@9oBoWk_4zS< z6`S!E4$TLesW*7?#d+3@^E<}w)<9Qon_V5Ho3fTzV`aV?R!T0;m-C`+eWwMqjI~<$Sl+cmx!f8&8!eXMm~qK%jO}>kU)BjuU=|MD8gM<716KUd z^*c&W?D}e)Yl^#8x^h-kS0&{k5CVn3CqH-aFOv&UkmN%MTsVQ7x9{A_73#vVroj*h zfe;9R7a_11x|r3tUoE8AJjCuQaevY9tb=_G+xqHxr7zmoH#G@Uz>Kp^8SCO&br)?l zJX7to?~`h$23hn2uKoH};(n&knAlif9mscRSg!xJZ#%Y6x5bp3K9@D)ykN_r*vz}- zt3D9XwP8Em`mv73I!9$(%$E$cg(X|(qMIY@c&xVVtGl>{og;m7FF3}I!x7`v_Sj-m zUmdIXJb@40oQ^4X%{xYInRn&Rkw~0J_oH@Kzg-W_0ru5YvAEd99~vst2ijvDQcc304)7|54ytlX`_6S>s`eVp%7;Z?b zZ_07r5!?CvkPzFk+oo$nfAmw^b>g_3mn)ZlNIIKSPzEB{)*tOuk&+Zkhb zZR)@4%JIrURx*^W^W`f~PgpaScXMi+Wo?v>mmi#KeOBTwJlS9J>7-QoXfHjOp5=Tw zr>)K2Q3Cax+1WmUz|6ferPCnmgTouI zzjm{$lkyMxWDFt^7irt}p%6e?wivSfbAo zaw@Hik?-y@7BiQMd`HKI*xE}9r_nRc8TNKEojxn|XcsAu&N>M~hO*Ue*8ohE&f58< zzIARe@2-Ykoy{UW$Hzmy_<@s3M+ICL6I;9+*rJQ};N#F8!jX8I=X}RYI>*)U&+jeldok)G? z&PA@n&URlkS*DO}-N_dr5CS0(0wJ(=0=Xcq9dSAcfe;9R5Ew(CX55)!QbJ3vTZZk1 zoSj`{0i=LVx+yZt82JTBLD^x<=*QI2{{g8-3FR#^?)1T9*{;Mnj8%IT?5*V#W5C}c~KTC^G+SJ>AE%W68v9cZQQ^v<$YDlI&R}IytWb`Rh6gk4RL7UxN z%Rx!gdDk|xPN~f`Am^8_EN9zt>uW3VDN`(4*{tQf%R$@u^))DEuF+GJ?;MSNx%OO> z?qBC_3alK$%uVC!pMUlkyMv*SIyw)E7n+VFWr$nf0Zh7Boj`3#r@J`Vc(>?qq|{mN zYj2O??46gk>78)K-^TZ9jp_lNZK{#$M7yjL)|l%=yQ~wmtCDuNLt33{qqEgxWZTtG z`B?()&0_Nbvwd)2r`4UH)v1AbSF8Kc^fWavk98FK=ALzLx_@0W+NA^BCT3QY^b_j$(2!y~k2>j#!^~-;0gh zY+qTx(A&`7wV@QCL|~m_Bh7WPURgi*={qG#N+$B1>qNPDwu>T;ubqHvUc2j9Y0G_4 z)<5gq_3Xa(Soer`vHPGnk45n+ZYtl|zP`CfT&nVydbAx_z zPD_LY_n09N0wE9r+ar(*()QS==MV^i5C{PXEC%H`A4FF&qmWaSN3@k$%AJnNEM!LQ zn@k7vVk$iu(h|lF_sFtSn$z}O8J0UohSEgd!wK=fGKKPjw&8YXTVKh}a@Ux0hw`tI zU1t}d`LI6(e*Uwc`4cag3t*u1vF@7q+*_S?V zQoidXyJKRZ#!qTUFm?!pKnR3D2yB)>E=Zf@o!&wq1VSJL8Ul-<$R=cuDEnypKr-c& z?#4#tDKpK?xNDr-i(*4p87jePzeNV)TMw3qoR_zdTb*A+X{7+e`DKg4pl{km3gB;= zuH0jbl*f8g%JYGUW$iI`{fr#~V+a%}j04;Y6xWBzvCdt?SDKVK_tXjNyp!pa?DR*U z^-WvJ&OPY9(;sc+dfU|2AN|z!rvZP4;AYs8-QD;GWj%-;0wE9rArJzaC6Ei!W_hQ# z5ZDfZYd`tLfBJ&C0NZgmz3w}KC*(V1ALXE8BcT-ej`B{j^W6gbPX1}*m!~q_U1d5r zo27P@I!bm*VGocIlm#x4-Dg&wA@+h}eSd{gz!=*$l|js0(pJ{fej2}&g}}22xaO6l zT=QnxDT}#oU7xn&v+P{kN_NUz`lA$Po7U;Zrl0OXeOA`9zINBXK4?2Ge;V*Sqm;Mc zJahSE*&9A2T3TX>6#^j;0wJ(P0=Xb<5q`Q3fe;9R5Lgme3}!SH2?%M(XV#S)W4##` z+iW`RG1(nU-}c**d9jh^lv2w)BCjdmc@wS@fii{E38hQ+T?zmBo5uRu$_O5-^ryU_ z+@Y;+u|r^00?JL=N_GRA!FB8URI;P`a7gx@&#zP3a^H=4uATB-H(t-HozLz&>$p$5 z_GP92j;ly^NORs8JHiLfCY?7a4}tw6@ZtaV3x6TG0Q)uP8SV54ix3Eb z5D0D zhd>B~KnR4uW(nkivd zN@EUw`>v0BJOIxdW3`KXM~H2BZn%Ad+^6&*8$)uBwcPrI=XP6Uic$wH!*bh<9Rd9cj*cS>_gcG~V)$8P)bbPt-rsC;*s z!RU`|>a*>`5aQO39ReW`0wE9rnp zzF-1|+pq8}L1j8^B|F1y-SRb$5BM9!3_ZeX4;_>Uw3Pxp!0_KTwL8hhc4CJ>k$sSi zTw~Vhh$$;6H>gC6)Ak(Vv2xJ=vNz#eWICjN zQR(xDHoHS@W~26}KK79hBo|<#$kSN}gg^*Pj=nlmG ztSIGe#NBjK2>2YlSGZxe$aj>RA!8_Q$bX-|QLL}@=dn6eWC6dxE>Zvu*_XD@&bmrF zG53dnYlbB=D5F_tjBPvGQdQ2%aYWwvxEE{qX1VSJLLLdY-OCT4d&GJrfArJx~u#*Jd_dh@U$1XA# zpl07G*(v26HB#PABA)4>UP0R`@*P<*X45Gh7>1jPckHo*+lJxF5&o&8q##o3D+v<9 z555Uk+je5_9s$>two;huM*`M&T@{;yt#A8XxqWpu%2vsa>*_1(d2Cm19p$pJf7yge zcG^W|r%_B#4@Gu!U+aTDdoajLJI)JxAMaag`62{DAOu1n1lCXB=IuMTa#dPC=kyQ) zArJx~FoM9TZigm!!~^Zr>a3$ZqJvb~5CKzciiX!{Z)z3B-^S(Y0mtZe#@y;04%K!n zw_lGnz0SUh21yE`y0Y!Zo)-bv0QyaNSe)Wr1K1#m(t+u#S7ma&7Cdy+*Y&M!vI-{c3mPMcr&u=gI@{7=YuleWuzr zMQHWdArJx~5CS2vQ3AOjZ4`Gp3xN;_fe`2jRLM>`PWet*uWKpg(TmJfCA zl=74>w3SwrH7qw<#?V`7#X2Ie-1rdta%yT61It0E>;8ea-9^qQm zm51DX3B2Tuv`8gpyZK%SGvbs)I$2Liq}U)+eo2@}z*Ab;o2f(=k;?{^A0wE9rArJzaC6Ei!W_hQ#5D0+~2!V#c6Ovsc-*w??(`brk zK;XvV(#dAE1d~4oWLL%FZOF!4FCzOOMA{W*C}zIPU1w zx<~@bZrYtp=W9xo^}Na2b~vZf9va zUT<|JCBbPY0U$}{nl@|0O<(wph(vB)Xq zSPoX+QOdi{c;`HaQ}^9@>0~-H>y+#)FE;aQo1J{;+LM<*Njz@;E-!UBn59>xX4bsN zD&M(x7VIw$rKjCBinLT^Jcz2!ucggupZje| zY;_&$*p5DUC-I&wfwd3Uxlo5wtJ5xZ(2PqeDCM@_&2Jeb3shZ=rCQSHF7t(JSljk# z-WzV)yJ@X&JNssn9_#63-!NpnArJx~5CRh-kPFg;CL^wdKnR4uJ`-3BravgzS-#JN z_F?Q-N&hLSm;Ak=#GzzYQtcX8N*q3%Oqm6B#G7UZB^{+3ZKWP<+gGNuFYT9k5MVQu z6hDW6GO2U44!e`=TyMJTT7AUYwvKCC8QJ=-nW21Gwq#$n>D{!;+Oggx(9BBsgg^*{ zKnR4uW(nkibOF47{oB9vo#p~uK$B@A1VSJL_L{(AP?qVks`eVu9t_@j_ikw=+#*4c zDuh``scMi|S08%E7s`6dFZO%P-%`qSN=DlHpr5U@!U%3e%7ezUP3Nn~bUf9tl#?|# zrrg&KE7@t6JFpvfSw7`%Y}bbC&Gld8UltZ-)9dMtwRC{l*5BhVoT5F(kfDb_2!ucg zY@0wXNZX!)^dABt5CS3a4uM5`16Kg;CxI(u-}xXerQISypg~|3cuG1-bJ|L6$|E*( z35oB7zooik^;20=DMs5m+TKvCKe4|~z*iG@_84i-I<{lEGM)48T5`QPN6K+ZZQVLC zW7GCspY~;&_8WUN0e1E1W6o3Y54`^~$pttMz#JO_ArJ!lKw#4sr+r|WF@``0gg^+q z0D;qoo2j{Izd&m_Wc~!M@XUCgJvAuhDT8!NMk%969DHH9l8Ca2b(C4GGB>D)a|s@@U$PGb(tQe7NT}yTHGT97BxB3=+r{WYiXH`6WY-S zHK)#2LsD}(LMu8%=i7(7X`MTnbhZq&S7ly~ML*M@k_yKf&}2Cjeadzc)St+BfYAe}w$0`GsI&iif*Oh*{W|Vx(pWJ*npnca;*M4~q z2J>#e#h&BDZ{KHTX#38@gAfRT5D0<&C6Ei!{?2_y9s(f{0+S-J7>XQ$WMR2?Mo!B8 zII~s)X49EncAYy-l{l2hI{5;%V`bmXl2SskT!qgL>{nZ#wUvV$qqcINtn7C#woH`B zl+i42ZKN>ycs_Zz&Ch=B;Qy9ffKh$XzH{N+x%P^D=i;CpyE{- z&k{(qbe2g=)W;mkYh(Gsb|@Z{{`6G&rPzE>y7AaK+DdIJ z+b~g5bDgwuA9LNxcPuhxRpm07AK=epCi{TDzb{jV=F1v(emmQKY0~0$jg?CI@HPQv zC_q9W1VSJLLSTCYazWZ2`}7{Ph>s+7D@9W5SN;JwERr`2)f68Ur%4GLk7Qkl)zNo1C5d+ZqLuaosW);gBkZ&`aRf-*;xEBBRS(cMt{IC7lnvD&WfaV_m_ zEw|dL}S=Lw5EVeag{C9P9dDu>Ng-VaH{_ z^Y|b`l+nS7?B>^Ur7&%!U1c}rxX!k|w(V;>6|PmY?DSdN+3MOUZBnT#S2m=&UBy02 zfE{@D%anw`{0RKvum0fwNiM+r<}DtFKqZiiQ{|CQArJx~5CU5wuo&(>Saz)(p-fS1 zV$qk7ah9#b+I0fTYd6Tm>m(5~)BF?23^&)_qPdB($T;dvtV!D`@*RGfJ?OLhu2RS6 z`<2&oG*aGN=G-|_{xSp2I-qqL%W$o4o63XEow8nMclB-Ge)UsFFIh>O5frJ7hLn_x z%_3I5qgWT{Dduwsi10a8^K}S>KnR3D2&|F7&D(cw<*Kwsz-c-JLLdY}U_60bkjAsx zshU6Ym81VS3?Cfcc>T4TJ2kKjCImuYCkUMWsC2v>hytlXYgfKJav1UhlQj0nKIBK1 zml5P$GYazl=wq(6IoLG?1)%V$S ziuReqv&R1AFZ}H3?A zm%GO)wB=}0zBp-22Ay^EL7V2w&jgk`8LqeTdjpk}d9AN}*V?pWozkX_Qs47x=Nr}c z*-V7A&$FMghd>B~z=R0of;6GYh$|rw0wJ)^1gd0b$lQh1mY;d)$=(0=ksD9s0vz)c zO{EyKo|I6OaSV<3d6sr{>}KMB~KnQGwKrTpI!JbY-AOu1n1fC>N=6TR`crvu%e%s|l-8-*S$ixMm6Z1@&W|A&>~^43ZRtKnR4uz7hDsFZ}P14#Sk5 z3y_P`zOBKGHv~c;1SU$L%6EqFt*vB7Hk6rBN_KB4+3^a1t4K7RWM`I?@|`zeT0T+8 zaqAoeym9q1ubDVUlY4h)k;vG;MVh1hn0>7*zp8Ef?fImU{`6T1&h)h+-;w%exyNcN z$64OmtQzaA+78XcJWC)E(pe@c34ss@fe_dNfn1Qb06krXKnR3D2t0+r&D(cwoxZ1n z2i=u8+c;hu?Znr9s?}X1?pIec?aq`sA4t||b-p*?9{Qi}#PH6`eNMA_%4DZ#&j_e{ zshO$0xqsE&$JDXD?RczqcW&v*^DK~vj+x9Kje)kUJzwz2XyjOAo ztW#12(IX%7$A34joUfeOcWkJg`7Q+^5CS0(0wJ(50=Xb<40SpRfe;9R5O@}W`kGn8 z?anq-_km<@A?saj5wQhOIxHeVO{6i!7I? zKW#X!J+__2J;soshd>B~KnQG`KrTqzo`Liq0=r4zuYK+-r#Bx?>A3*A$$4i-wHUms z^_XjwVWy$?6+?I>JMMxtgRYb5l+>)_vC4PX?H60$y72!St}Z9gHkYNbLSX&`I_XTQ zt+SQHt<#+c?92Mv*6Hl-4&4*vHSbJymXz%%&^lw!^Vzp_pZ-6rio7aDtzP{ASd=@7i*~N)Jdo1;r6g`&N^sWK2g|j~vFzDSaxaoylT9~9vU|XJ#=g4pat`Et!j^+VAOu1n z1TLDuVlb>Op(x>`;i|9kTo4W$aw^juA`6&RqHTTOU*U7}^ixTJ&~Gq{#^+EO+LOpr zU`EO-IPzh_1eDS4vcEc+&T{K3-&sd_&An#7mRsL_+Ddsyb{=c{O77jB_2-61oQs?~ z&s~S~`+&d2mGV}V>^M!@?LbnVuhwW^KkFFH3ijMsI+*zf=bClGtjv6XaX$n?AOu2S z)&z1vn)UR>`49+!5ZE08i^0&lNOla;N5LB=UnT@zaBb^sWjZB3%a!c(%@9wS&d^@F zlj#hx9j{|FWClab2@v?>|NM7+>%Qj#bh4PTTao6F^~RJVp$tlD?nh-V%dM}Uo!!;9 zeY0oHzH`rdUQ#Y|$$DN=n)6&_cCm7w?U(b09bhZLc`kbQRTuBn^k;?EW-vZL!1JVj z;=<%i&(A(bu~`FLQ6@K(I1~aQ5CS2vM+9;~+M~(NP(vUDLSV`S7K34O7nT|(ck^V(9^e|_u5J8)BNq&!K+UN-^PurgY)SCr$%NOqmP zR%CPJddpk+j{1#6Q{+MSyHbx#%3kK2V;zbIWxDSCbD0~5%lU!HwqJ@%Hk~q^{-5BG zKRuj~CFuFhbF!R|sb3yMK>Pj_qMz%wi_e_5eTHGTPAF$2>-IS9hd>B~KnR4uGYH(g zedkuLO3wf^rhXWMs?`mAz%2MK5x1$0X`3oytZdwA zjB6aMZ^r97VVqqwMXtg2YkkL6(r2zLd$@75v}1+9ItZw@eGRS7^g8uC>vwk7j`h{> z)Z5h6icW(*r_O{PGpwlJ47TImcR#vk7k+_V_C9`g=O52e+THwm{_(t|&uVi1M&Uay zI-d8{>hRw?Xno-55$89bWT0m0dCxJ*)A6=rgtKGxfupV&=d{@9rj#=hy@?$HArJx~ z5CWSekPFggd8fA!2!Rj?frh}MK6t6AbwHR_s-SZz{ZTv^9(J;xp`T%|VWYC4m(dcQ z)3G6{w#QPv49npflI!P7NO`tj8p?+d7(qbkOnL3iA6wLLG{rZ(LclV2ywPad8hWK|?P#_KzO z{MkYY?ht89$<8)CR+&!Q@oIPD_1r6GZO5za+K_|m!8%BMo}LTy{rPbG6TkD*$pzS- z`OZi~AOz+>AQz`O%u;*|fe;9RJs`j>s?Qj$a#pFf8@GgtS za@#Rn)m9!fbXAr#L?z@LZX7PX&igKwEMoNplrogslHbTm?9p6D=xpJTKomqCu zYi3d@#gx5(BxN1PSmc_fzJ2+iJ8w-bdzKT8e_FfWJy%$6J4%1fnCAh{QJxc&86CUt z#87JUyriwvW_@k5=}I|+w;kJ;!>F+XLJr;7T@TtaaeghwW+XTeGXz2)1VSJLHcTKF zqzwa4cOei0ArJyjCooR3d+*ra^-W*=PZyO7U|qX#%b#v+R`Sw7-gEr<8Y>a;3RZk{uh& zwW?(2dRB_*Y~+`rm1x*6r9P=_wy#X2eCOVA{}xG#zDoIs)rBHhGLxM<&jUWV&MY|X zBB|lD=cppJ5i^w6dRva|vGye^`x?WfbZV) zA*vF@F-U#qhLW8(zIC=f#NG`8X1gi7y@`ybO!w9M%W+$-tXJCP=1R+pO`R^w&h?C( zGH4h1WsF4A)N$|IKGg@$3GPQ_{{s$iFCTCYVe2o!K4G;Do zng4Q8xd0ntl8e)ZK&P7!2!Rj?foBsyYkGFYjeTqQY{>!v z$}!8h^{s@lgs9z(fmiTT$?hsspl!CEazt?`A0|&gdB^OyF5}M3Iwd=$JnJaUnQ>>i znRVLAckU_oS|hUz)^VRI!F6@IHr-!JMrD%u!oIBI{?)b)&8QBx@|1l!F6URt&dj%! z&qX-u-Y>Eq15@JjoL20nO^RF7dEr!7Sb08lel2&M*d}w42&tWgd=Ua65CS2vNdmbb zZIX8S3V{#^fe>g3pcl1uHv0wPTL}pb_qy=eoBNdS?s45MH`r1>CcGs|E>H|Vi=Pap zg!#NR(sIXTsH=Qu_+OmJhe;ApIvXR?DaR?(Xh{i5{LtwYzO>s*PZl(YUV zcfZ)?m z4y8UofNwn3_AR%~uAC-CTW%cgcsi%Db-pa``nq}h&Mi*SR*M}1ArJx~5CWSekPFgg zd8fA!2!Rj?frh~JG&}>w#{`?kg=6ArHHnf6$2C};ue&4d5~+L~T4HG7mbjKLI-f0f zjK;f`U*nw^>I8HZbISx=Z>HL5tLLf7b+##Z*0D|Xxy~-S9h#F`lZId4#Is-2RZQr@bPTH#qfII_m(Jl510SA33sN4NmN zaqgs62!ucggg^+agFr4w>!6x8Lm&h~AOuz>fabIEvFks;P+D7gK`HEnn6SL!lIyPW z-I%c25W5?(zLk*QFu$?&pDHFE7U|N6SzF&C-$`RLw)3uZrli){N_pB!d6sM2<~<}h zA7W<)pi*|X_FR0fG3~C-W@taVm$DzJTC(prSGe!WxrG7yj)lt{P$WCrT=n5|zAu7i z#9ZV@Tqx%UhFgyv^@WwNb?EuU<>w4ez2%)4HVNiZovvbrKnPqEf&YH{mp`0bfQzD; zcJ_ooE>3$g(HUk4gg^*PoxsH9JMbxwXq#ncW{Wb#C1efjXe-l|Oc>)#{LwU&HvHE& z{n56L$9l7#R6QZ^wz!B-K4K%r!?pO^&B$BF6S8?YV2+-&ZA>2^=;EHEbnYi z9GEJORkEYfiepTK)XhQ4Lm&h~AOto_AQz;K;!bBF5CS0(0v&;AOLlr~D5|t|K=@h0 zXXFdR=TS199b@Z*VY2?1#iTUnYbPIYldR9q^4QWQW3*hUq4X!y?(SPFP~B`28@QJI^P|cb-EYB1v5W zoOOx~MkOrk&}7L>~Y+7LLdY} zV0#2|LE0Ys^c(^q5CS0pff>nncy0KpgoO%HGC&THI1DXII7$1OhPS0(zEE~kjypuA z(=IX#b&AY_w4wxOU)t8Oonpg5DenqCcoVT4VxJ|Tyrx~`yC$=)$amDS9qqDam`CT= zHQ>H*-Lh=Y4rotHWKvl1h3Ab&oH#ItPLA_f>s!7m`r;=CcjZcy46EFPOEX1Y)8{ph zF%eRkgnS5r5D0+~*cO3YkhVqs{Qds!AO8m*onj?vG2hk)&9cYRr>*yugSO!pPa z)$oe8M}1Ql8&rE&Y$}|3TUYL#uVZZ6>F$ViJ-8;dUAKETN#_`_T9@Ym&okaSDra>s z>y&bQ>&mI(jm;L4oMo$B-WLIL>nO*yav#U~dRW(hYtJ?5zS!N_*v`m2 zSEz-Ra$GC&7v)NM7LT%<&YI#;OO4;M7X-H(v|);GTL#pD8%wUZAa5CS1E zbppG8ahm!J#J>;-fe;9RcL<=v%#?`XBnfWiZZNa&9Hz%AcW4*MjsuiUv=>M&K3Go4 z&O1}BbHsEg%ySp>F0LpGk86=bqkNLvL(qpyFzVkt1KHRRzcUsT#zO+6>%g4LLdb8n*ch` zj)73VKtU;)cn;b6+Dal1xqdHb^u%&(GwX`YUAslTLoU0I-DSRYl94i@Hlq%x5k!yO!p^4;8Ts_i%z*-eIxU1T@bk2kt%yY{r53vK5mb_mRnfTxogx*wNA zAOu1n1pdj7{K|LlOfJB?)0vA?)^!MkKnR4uBnY6@Ov0b_IHAOWYErZBkkqVWcA2)) z)@2Sb(@Z(*64!oT)o2}WwmqhPmwi`cIwYgEoYcdsWaTMCnN>()S9vs-V_Cp$f#hiR zpf~5PXNK3?s`8!LadKN#nsY4`X^x?E{6Svk#riZ8GX&;BfV19QT#lb15CS0(0=q~c z7o=UB)r=_wLLdZYNMJuCJ10h2Ln+TXMYcj8QCd-^Q^HcZQJNcL7x@nP>j*hc`KYy# z=6uGTw$hX1m4lgbN^^(!_5i!Psna&+V!KXcHA<%A;4#u%$D~^`W^b08zUjAXGv)99 z_P_IIcP$rSCucR$(oRlm1{DG!5CStKaP#(^Te&LDaN^=}2!ucg>>`2v)9|`kRG;&e ziI36n)Knf?j&@?5V$-qOm~|{KDR}5)Mb9JMujqM9Yk9aD^~-qiO+C{0PIT|DP_yjx zD*JN0bUQh-)9Spd)zr&w>@1BkMwZXuysOV?o6f2B?E16=nOhz^1SU)%5z>SwC$5D+ z2!y~s5y%B;pJqE_4S}%)e)zwB@y{n0U@WcFTLS@fo;9>S*Cv%*id4cEN-mGkVlW4# zD$6~#D}TTj-2?Va4fDMjmMb?o7JX1AGQG|R?_EXuvrT29F}6&mygrI7>DXBVj#st= zb(qGo9!k0;eHCc(oK>3{6;O0 zgG*MG<=V7Eamn&BgR9&S%0u;(?8ey2hMle4gqxEyl{)p=a-CjPrYrC9;3jBgI_)DS z(rh|kcdUG;&&r1SPm>NMb_h&?Kq90m%tCw!fe;9R5Eybn$|4AX5D0+~m;`}HcH5YS zRyJwAKv65N!|i-muJIMG<((4ffZkL)My4B8hp{Z{|H}9O*bmGj7XbGQ=1!)Qx!H`` zKBrFkv@16ga>;NQydk>WK#kMBN5$tc>c3K68Du_$!1f6wTH5{`#DfqBfe;9RQv$gl zWf_D(2!ucgOoBku8^lGT8N{oHc3c=y#aqW@M$j1(h758FxqgU1(OM41LS6?R2)B^#^wbpS9RMuW!A8UBh5eOl+l#el;DcZ zow7=H%52;a@Lf|Sz^AtayfU-=d%b?5+W|tKy58Z0zth`3n>ts45JLRlS zs(Lo0Yx!HpmciKv7WA2)DydK-xX45GZTCS~c+O88{cifHFvBwU9H4{j*v}V9* zJ_JG_1VUgOfn1QraZ0Ta2!Rj?fi)9|WVdF(8*5(quCtY8l&4Va$^-Mvrt55_IWz7$ zyGV9qon1!yQ+m=~2TYkLW6G!Fsc*_?o$OZ1X|xV{DpKv0Deuv>5{DA&>~^ zJoIvG2!ucgte?Q&{QOIwcvddJ`aRe}54kvP0eZR&fe;9R5I9R9^4(b`Gb&M+!EH0F ztYSIU+e|#=EN`&wq&&-&?~c(2kC5q<^2}0G&N^a=Iu`vukL?(h^13#a%k3o8Bw44}#ku7N4sGYR)L9<7JJwfj zeDhBX!z;h>#_O-$+?`SF<4D|6`#AoLIRru=1VUgU1ad)|$V9}A5D0+~*mnY_!TjUL eul~tj`}r%s?aH^k4dadP`kvSS{fGa`r~W_CmCUdJ literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/heatmap_renderer/expected_data_defined_radius/expected_data_defined_radius.png b/tests/testdata/control_images/heatmap_renderer/expected_data_defined_radius/expected_data_defined_radius.png new file mode 100644 index 0000000000000000000000000000000000000000..d382b7f854abbb1472d232845cc5800e2c85d3a9 GIT binary patch literal 471523 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 literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/heatmap_renderer/expected_render_heatmap/expected_render_heatmap.png b/tests/testdata/control_images/heatmap_renderer/expected_render_heatmap/expected_render_heatmap.png new file mode 100644 index 0000000000000000000000000000000000000000..c00acc1659dcf69a5112168526cde2f3a0df8b5d GIT binary patch literal 471523 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 literal 0 HcmV?d00001 From 249cc6d59162c11bacfd2518717351d1e7d53f98 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 9 May 2024 10:29:08 +1000 Subject: [PATCH 068/102] [console] Clean up objects a little - Don't use ambiguous .parent members, use explicit names - Add some typehints --- python/console/console.py | 27 +++-- python/console/console_editor.py | 189 +++++++++++++++++-------------- python/console/console_output.py | 41 +++---- python/console/console_sci.py | 20 ++-- 4 files changed, 150 insertions(+), 127 deletions(-) diff --git a/python/console/console.py b/python/console/console.py index 1653e77200c21..429957d502de0 100644 --- a/python/console/console.py +++ b/python/console/console.py @@ -145,10 +145,13 @@ def __init__(self, parent=None): QWidget.__init__(self, parent) self.setWindowTitle(QCoreApplication.translate("PythonConsole", "Python Console")) - self.shell = ShellScintilla(self) + self.shell = ShellScintilla(console_widget=self) self.setFocusProxy(self.shell) - self.shellOut = ShellOutputScintilla(self) - self.tabEditorWidget = EditorTabWidget(self) + self.shell_output = ShellOutputScintilla( + console_widget=self, + shell_editor=self.shell + ) + self.tabEditorWidget = EditorTabWidget(console_widget=self) # ------------ UI ------------------------------- @@ -160,7 +163,7 @@ def __init__(self, parent=None): self.shellOutWidget = QWidget(self) self.shellOutWidget.setLayout(QVBoxLayout()) self.shellOutWidget.layout().setContentsMargins(0, 0, 0, 0) - self.shellOutWidget.layout().addWidget(self.shellOut) + self.shellOutWidget.layout().addWidget(self.shell_output) self.splitter = QSplitter(self.splitterEditor) self.splitter.setOrientation(Qt.Orientation.Vertical) @@ -455,10 +458,10 @@ def __init__(self, parent=None): sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.shellOut.sizePolicy().hasHeightForWidth()) - self.shellOut.setSizePolicy(sizePolicy) + sizePolicy.setHeightForWidth(self.shell_output.sizePolicy().hasHeightForWidth()) + self.shell_output.setSizePolicy(sizePolicy) - self.shellOut.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.shell_output.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) self.shell.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) # ------------ Layout ------------------------------- @@ -534,7 +537,7 @@ def __init__(self, parent=None): self.copyEditorButton.triggered.connect(self.copyEditor) self.pasteEditorButton.triggered.connect(self.pasteEditor) self.showEditorButton.toggled.connect(self.toggleEditor) - self.clearButton.triggered.connect(self.shellOut.clearConsole) + self.clearButton.triggered.connect(self.shell_output.clearConsole) self.optionsButton.triggered.connect(self.openSettings) self.runButton.triggered.connect(self.shell.entered) self.openFileButton.triggered.connect(self.openScriptFile) @@ -756,14 +759,14 @@ def openSettings(self): def updateSettings(self): self.shell.refreshSettingsShell() - self.shellOut.refreshSettingsOutput() + self.shell_output.refreshSettingsOutput() self.tabEditorWidget.refreshSettingsEditor() def callWidgetMessageBar(self, text): - self.shellOut.widgetMessageBar(iface, text) + self.shell_output.widgetMessageBar(text) - def callWidgetMessageBarEditor(self, text, level, timeout): - self.tabEditorWidget.showMessage(text, level, timeout) + def callWidgetMessageBarEditor(self, text, level): + self.tabEditorWidget.showMessage(text, level) def updateTabListScript(self, script, action=None): if action == 'remove': diff --git a/python/console/console_editor.py b/python/console/console_editor.py index 58fc0c3fc296e..eae190dc8a634 100644 --- a/python/console/console_editor.py +++ b/python/console/console_editor.py @@ -59,9 +59,15 @@ class Editor(QgsCodeEditorPython): - def __init__(self, parent=None): - super().__init__(parent) - self.parent = parent + def __init__(self, + editor_tab: 'EditorTab', + console_widget: 'PythonConsoleWidget', + tab_widget: 'EditorTabWidget'): + super().__init__(editor_tab) + self.editor_tab: 'EditorTab' = editor_tab + self.console_widget: 'PythonConsoleWidget' = console_widget + self.tab_widget: 'EditorTabWidget' = tab_widget + self.path: Optional[str] = None # recent modification time self.lastModified = 0 @@ -93,7 +99,7 @@ def __init__(self, parent=None): self.syntaxCheckScut = QShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_4), self) self.syntaxCheckScut.setContext(Qt.ShortcutContext.WidgetShortcut) self.syntaxCheckScut.activated.connect(self.syntaxCheck) - self.modificationChanged.connect(self.parent.modified) + self.modificationChanged.connect(self.editor_tab.modified) self.modificationAttempted.connect(self.fileReadOnly) def settingsEditor(self): @@ -210,7 +216,7 @@ def contextMenuEvent(self, e): menu.addSeparator() menu.addAction(QgsApplication.getThemeIcon("console/iconSettingsConsole.svg"), QCoreApplication.translate("PythonConsole", "Options…"), - self.pythonconsole.openSettings) + self.console_widget.openSettings) syntaxCheckAction.setEnabled(False) pasteAction.setEnabled(False) pyQGISHelpAction.setEnabled(False) @@ -249,11 +255,11 @@ def findText(self, forward, showMessage=True, findFirst=False): index = 0 else: line, index = self.getCursorPosition() - text = self.pythonconsole.lineEditFind.text() + text = self.console_widget.lineEditFind.text() re = False - wrap = self.pythonconsole.wrapAround.isChecked() - cs = self.pythonconsole.caseSensitive.isChecked() - wo = self.pythonconsole.wholeWord.isChecked() + wrap = self.console_widget.wrapAround.isChecked() + cs = self.console_widget.caseSensitive.isChecked() + wo = self.console_widget.wholeWord.isChecked() notFound = False if text: if not forward: @@ -269,10 +275,10 @@ def findText(self, forward, showMessage=True, findFirst=False): if showMessage: msgText = QCoreApplication.translate('PythonConsole', '"{0}" was not found.').format(text) - self.parent.showMessage(msgText) + self.showMessage(msgText) else: styleError = '' - self.pythonconsole.lineEditFind.setStyleSheet(styleError) + self.console_widget.lineEditFind.setStyleSheet(styleError) def findNext(self): self.findText(True) @@ -281,25 +287,26 @@ def findPrevious(self): self.findText(False) def objectListEditor(self): - listObj = self.pythonconsole.listClassMethod + listObj = self.console_widget.listClassMethod if listObj.isVisible(): listObj.hide() - self.pythonconsole.objectListButton.setChecked(False) + self.console_widget.objectListButton.setChecked(False) else: listObj.show() - self.pythonconsole.objectListButton.setChecked(True) + self.console_widget.objectListButton.setChecked(True) def shareOnGist(self, is_public): ACCESS_TOKEN = QgsSettings().value("pythonConsole/accessTokenGithub", '', type=QByteArray) if not ACCESS_TOKEN: msg_text = QCoreApplication.translate( 'PythonConsole', 'GitHub personal access token must be generated (see Console Options)') - self.parent.showMessage(msg_text, Qgis.MessageLevel.Warning, 5) + self.showMessage(msg_text, + level=Qgis.MessageLevel.Warning) return URL = "https://api.github.com/gists" - path = self.tabwidget.currentWidget().path + path = self.tab_widget.currentWidget().path filename = os.path.basename(path) if path else None filename = filename if filename else "pyqgis_snippet.py" @@ -321,30 +328,31 @@ def shareOnGist(self, is_public): link = _json.object()['html_url'].toString() QApplication.clipboard().setText(link) msg = QCoreApplication.translate('PythonConsole', 'URL copied to clipboard.') - self.parent.showMessage(msg) + self.showMessage(msg) else: msg = QCoreApplication.translate('PythonConsole', 'Connection error: ') - self.parent.showMessage(msg + request.erroMessage(), Qgis.MessageLevel.Warning, 5) + self.showMessage(msg + request.erroMessage(), + level=Qgis.MessageLevel.Warning) def hideEditor(self): - self.pythonconsole.splitterObj.hide() - self.pythonconsole.showEditorButton.setChecked(False) + self.console_widget.splitterObj.hide() + self.console_widget.showEditorButton.setChecked(False) def openFindWidget(self): - wF = self.pythonconsole.widgetFind + wF = self.console_widget.widgetFind wF.show() if self.hasSelectedText(): - self.pythonconsole.lineEditFind.setText(self.selectedText().strip()) - self.pythonconsole.lineEditFind.setFocus() - self.pythonconsole.findTextButton.setChecked(True) + self.console_widget.lineEditFind.setText(self.selectedText().strip()) + self.console_widget.lineEditFind.setFocus() + self.console_widget.findTextButton.setChecked(True) def closeFindWidget(self): - wF = self.pythonconsole.widgetFind + wF = self.console_widget.widgetFind wF.hide() - self.pythonconsole.findTextButton.setChecked(False) + self.console_widget.findTextButton.setChecked(False) def toggleFindWidget(self): - wF = self.pythonconsole.widgetFind + wF = self.console_widget.widgetFind if wF.isVisible(): self.closeFindWidget() else: @@ -359,14 +367,14 @@ def createTempFile(self): def runScriptCode(self): autoSave = QgsSettings().value("pythonConsole/autoSaveScript", False, type=bool) - tabWidget = self.tabwidget.currentWidget() + tabWidget = self.tab_widget.currentWidget() filename = tabWidget.path filename_override = None msgEditorBlank = QCoreApplication.translate('PythonConsole', 'Hey, type something to run!') if filename is None: if not self.isModified(): - self.parent.showMessage(msgEditorBlank) + self.showMessage(msgEditorBlank) return deleteTempFile = False @@ -376,20 +384,20 @@ def runScriptCode(self): elif not filename or self.isModified(): # Create a new temp file if the file isn't already saved. filename = self.createTempFile() - filename_override = self.tabwidget.tabText(self.tabwidget.currentIndex()) + filename_override = self.tab_widget.tabText(self.tab_widget.currentIndex()) if filename_override.startswith('*'): filename_override = filename_override[1:] deleteTempFile = True - self.pythonconsole.shell.runFile(filename, filename_override) + self.console_widget.shell.runFile(filename, filename_override) if deleteTempFile: Path(filename).unlink() def runSelectedCode(self): # spellok cmd = self.selectedText() - self.pythonconsole.shell.insertFromDropPaste(cmd) - self.pythonconsole.shell.entered() + self.console_widget.shell.insertFromDropPaste(cmd) + self.console_widget.shell.entered() self.setFocus() def getTextFromEditor(self): @@ -431,7 +439,8 @@ def focusInEvent(self, e): if not QFileInfo(self.path).exists(): msgText = QCoreApplication.translate('PythonConsole', 'The file "{0}" has been deleted or is not accessible').format(self.path) - self.parent.showMessage(msgText, Qgis.MessageLevel.Critical) + self.showMessage(msgText, + level=Qgis.MessageLevel.Critical) return if self.path and self.lastModified != QFileInfo(self.path).lastModified(): self.beginUndoAction() @@ -441,21 +450,21 @@ def focusInEvent(self, e): self.setModified(False) self.endUndoAction() - self.tabwidget.listObject(self.tabwidget.currentWidget()) + self.tab_widget.listObject(self.tab_widget.currentWidget()) self.lastModified = QFileInfo(self.path).lastModified() super().focusInEvent(e) def fileReadOnly(self): - tabWidget = self.tabwidget.currentWidget() + tabWidget = self.tab_widget.currentWidget() msgText = QCoreApplication.translate('PythonConsole', 'The file "{0}" is read only, please save to different file first.').format(tabWidget.path) - self.parent.showMessage(msgText, Qgis.MessageLevel.Warning) + self.showMessage(msgText) - def loadFile(self, filename, readOnly=False): + def loadFile(self, filename: str, read_only: bool = False): self.lastModified = QFileInfo(filename).lastModified() self.path = filename self.setText(Path(filename).read_text(encoding='utf-8')) - self.setReadOnly(readOnly) + self.setReadOnly(read_only) self.setModified(False) self.recolor() @@ -466,7 +475,7 @@ def save(self, filename: Optional[str] = None): if QgsSettings().value("pythonConsole/formatOnSave", False, type=bool): self.reformatCode() - index = self.tabwidget.indexOf(self.parent) + index = self.tab_widget.indexOf(self.editor_tab) if filename: self.path = filename if not self.path: @@ -475,7 +484,7 @@ def save(self, filename: Optional[str] = None): folder = QgsSettings().value("pythonConsole/lastDirPath", QDir.homePath()) self.path, filter = QFileDialog().getSaveFileName(self, saveTr, - os.path.join(folder, self.tabwidget.tabText(index).replace('*', '') + '.py'), + os.path.join(folder, self.tab_widget.tabText(index).replace('*', '') + '.py'), "Script file (*.py)") # If the user didn't select a file, abort the save operation if not self.path: @@ -484,19 +493,19 @@ def save(self, filename: Optional[str] = None): msgText = QCoreApplication.translate('PythonConsole', 'Script was correctly saved.') - self.parent.showMessage(msgText) + self.showMessage(msgText) # Save the new contents # Need to use newline='' to avoid adding extra \r characters on Windows with open(self.path, 'w', encoding='utf-8', newline='') as f: f.write(self.text()) - self.tabwidget.setTabTitle(index, Path(self.path).name) - self.tabwidget.setTabToolTip(index, self.path) + self.tab_widget.setTabTitle(index, Path(self.path).name) + self.tab_widget.setTabToolTip(index, self.path) self.setModified(False) - self.pythonconsole.saveFileButton.setEnabled(False) + self.console_widget.saveFileButton.setEnabled(False) self.lastModified = QFileInfo(self.path).lastModified() - self.pythonconsole.updateTabListScript(self.path, action='append') - self.tabwidget.listObject(self.parent) + self.console_widget.updateTabListScript(self.path, action='append') + self.tab_widget.listObject(self.editor_tab) QgsSettings().setValue("pythonConsole/lastDirPath", Path(self.path).parent.as_posix()) @@ -525,11 +534,11 @@ def keyPressEvent(self, e): # Ctrl+W: close current tab if ctrl and e.key() == Qt.Key.Key_W: - self.parent.close() + self.editor_tab.close() # Ctrl+Shift+W: close all tabs if ctrl_shift and e.key() == Qt.Key.Key_W: - self.tabwidget.closeAll() + self.tab_widget.closeAll() # Ctrl+S: save current tab if ctrl and e.key() == Qt.Key.Key_S: @@ -537,30 +546,37 @@ def keyPressEvent(self, e): # Ctrl+Shift+S: save current tab as if ctrl_shift and e.key() == Qt.Key.Key_S: - self.tabwidget.saveAs() + self.tab_widget.saveAs() # Ctrl+T: open new tab if ctrl and e.key() == Qt.Key.Key_T: - self.tabwidget.newTabEditor() + self.tab_widget.newTabEditor() super().keyPressEvent(e) - def showMessage(self, title, text, level): - self.parent.showMessage(text, level, title=title) + def showMessage(self, + text: str, + title: Optional[str] = None, + level=Qgis.MessageLevel.Info): + self.editor_tab.showMessage(text, level, title=title) class EditorTab(QWidget): - def __init__(self, parent, pythonconsole, filename, readOnly): - super().__init__(parent) - self.tabwidget = parent - - self._editor = Editor(self) - self._editor.pythonconsole = pythonconsole - self._editor.tabwidget = parent + def __init__(self, + tab_widget: 'EditorTabWidget', + console_widget: 'PythonConsoleWidget', + filename: Optional[str], + read_only: bool): + super().__init__(tab_widget) + self.tab_widget: 'EditorTabWidget' = tab_widget + + self._editor = Editor(editor_tab=self, + console_widget=console_widget, + tab_widget=tab_widget) if filename: if QFileInfo(filename).exists(): - self._editor.loadFile(filename, readOnly) + self._editor.loadFile(filename, read_only) # Creates layout for message bar self.layout = QGridLayout(self._editor) @@ -578,10 +594,10 @@ def __init__(self, parent, pythonconsole, filename, readOnly): self.tabLayout.addWidget(self._editor) def modified(self, modified): - self.tabwidget.tabModified(self, modified) + self.tab_widget.tabModified(self, modified) def close(self): - self.tabwidget._removeTab(self, tab2index=True) + self.tab_widget._removeTab(self, tab2index=True) def __getattr__(self, name): """ Forward all missing attribute requests to the editor.""" @@ -603,9 +619,9 @@ def showMessage(self, text, level=Qgis.MessageLevel.Info, timeout=-1, title=""): class EditorTabWidget(QTabWidget): - def __init__(self, parent): + def __init__(self, console_widget: 'PythonConsoleWidget'): super().__init__(parent=None) - self.parent = parent + self.console_widget: 'PythonConsoleWidget' = console_widget self.idx = -1 # Layout for top frame (restore tabs) @@ -753,22 +769,22 @@ def closeAll(self): self._removeTab(0) def saveAs(self): - self.parent.saveAsScriptFile(self.idx) + self.console_widget.saveAsScriptFile(self.idx) def enableSaveIfModified(self, tab): tabWidget = self.widget(tab) if tabWidget: - self.parent.saveFileButton.setEnabled(tabWidget.isModified()) + self.console_widget.saveFileButton.setEnabled(tabWidget.isModified()) def enableToolBarEditor(self, enable): if self.topFrame.isVisible(): enable = False - self.parent.toolBarEditor.setEnabled(enable) + self.console_widget.toolBarEditor.setEnabled(enable) - def newTabEditor(self, tabName=None, filename=None): - readOnly = False + def newTabEditor(self, tabName=None, filename: Optional[str] = None): + read_only = False if filename: - readOnly = not QFileInfo(filename).isWritable() + read_only = not QFileInfo(filename).isWritable() try: fn = codecs.open(filename, "rb", encoding='utf-8') fn.read() @@ -784,9 +800,12 @@ def newTabEditor(self, tabName=None, filename=None): nr = self.count() if not tabName: tabName = QCoreApplication.translate('PythonConsole', 'Untitled-{0}').format(nr) - tab = EditorTab(self, self.parent, filename, readOnly) + tab = EditorTab(tab_widget=self, + console_widget=self.console_widget, + filename=filename, + read_only=read_only) self.iconTab = QgsApplication.getThemeIcon('console/iconTabEditorConsole.svg') - self.addTab(tab, self.iconTab, tabName + ' (ro)' if readOnly else tabName) + self.addTab(tab, self.iconTab, tabName + ' (ro)' if read_only else tabName) self.setCurrentWidget(tab) if filename: self.setTabToolTip(self.currentIndex(), filename) @@ -797,7 +816,7 @@ def tabModified(self, tab, modified): index = self.indexOf(tab) s = self.tabText(index) self.setTabTitle(index, '*{}'.format(s) if modified else re.sub(r'^(\*)', '', s)) - self.parent.saveFileButton.setEnabled(modified) + self.console_widget.saveFileButton.setEnabled(modified) def setTabTitle(self, tab, title): self.setTabText(tab, title) @@ -819,13 +838,13 @@ def _removeTab(self, tab, tab2index=False): if res == QMessageBox.StandardButton.Save: editorTab.save() if editorTab.path: - self.parent.updateTabListScript(editorTab.path, action='remove') + self.console_widget.updateTabListScript(editorTab.path, action='remove') self.removeTab(tab) if self.count() < 1: self.newTabEditor() else: if editorTab.path: - self.parent.updateTabListScript(editorTab.path, action='remove') + self.console_widget.updateTabListScript(editorTab.path, action='remove') if self.count() <= 1: self.removeTab(tab) self.newTabEditor() @@ -860,7 +879,7 @@ def restoreTabs(self): print('## Error: ') s = errOnRestore sys.stderr.write(s) - self.parent.updateTabListScript(pathFile, action='remove') + self.console_widget.updateTabListScript(pathFile, action='remove') if self.count() < 1: self.newTabEditor(filename=None) self.topFrame.close() @@ -868,7 +887,7 @@ def restoreTabs(self): self.currentWidget()._editor.setFocus(Qt.FocusReason.TabFocusReason) def closeRestore(self): - self.parent.updateTabListScript(None) + self.console_widget.updateTabListScript(None) self.topFrame.close() self.newTabEditor(filename=None) self.enableToolBarEditor(True) @@ -885,7 +904,7 @@ def showFileTabMenuTriggered(self, action): self.setCurrentIndex(index) def listObject(self, tab): - self.parent.listClassMethod.clear() + self.console_widget.listClassMethod.clear() if isinstance(tab, EditorTab): tabWidget = self.widget(self.indexOf(tab)) else: @@ -938,7 +957,7 @@ def listObject(self, tab): methodItem.setSizeHint(0, QSize(18, 18)) classItem.addChild(methodItem) dictObject[meth] = lineno - self.parent.listClassMethod.addTopLevelItem(classItem) + self.console_widget.listClassMethod.addTopLevelItem(classItem) for func_name, data in sorted(list(readModuleFunction.items()), key=lambda x: x[1].lineno): if isinstance(data, pyclbr.Function) and \ os.path.normpath(data.file) == os.path.normpath(tabWidget.path): @@ -951,7 +970,7 @@ def listObject(self, tab): if sys.platform.startswith('win'): funcItem.setSizeHint(0, QSize(18, 18)) dictObject[func_name] = data.lineno - self.parent.listClassMethod.addTopLevelItem(funcItem) + self.console_widget.listClassMethod.addTopLevelItem(funcItem) if found: sys.path.remove(pathFile) except: @@ -960,18 +979,18 @@ def listObject(self, tab): msgItem.setText(1, 'syntaxError') iconWarning = QgsApplication.getThemeIcon("console/iconSyntaxErrorConsole.svg") msgItem.setIcon(0, iconWarning) - self.parent.listClassMethod.addTopLevelItem(msgItem) + self.console_widget.listClassMethod.addTopLevelItem(msgItem) def refreshSettingsEditor(self): objInspectorEnabled = QgsSettings().value("pythonConsole/enableObjectInsp", False, type=bool) - listObj = self.parent.objectListButton - if self.parent.listClassMethod.isVisible(): + listObj = self.console_widget.objectListButton + if self.console_widget.listClassMethod.isVisible(): listObj.setChecked(objInspectorEnabled) listObj.setEnabled(objInspectorEnabled) if objInspectorEnabled: cW = self.currentWidget() - if cW and not self.parent.listClassMethod.isVisible(): + if cW and not self.console_widget.listClassMethod.isVisible(): with OverrideCursor(Qt.CursorShape.WaitCursor): self.listObject(cW) diff --git a/python/console/console_output.py b/python/console/console_output.py index 1dcbe93676cfd..3772eed53611a 100644 --- a/python/console/console_output.py +++ b/python/console/console_output.py @@ -117,10 +117,12 @@ def isatty(self): class ShellOutputScintilla(QgsCodeEditorPython): - def __init__(self, parent=None): - super().__init__(parent) - self.parent = parent - self.shell = self.parent.shell + def __init__(self, + console_widget: 'PythonConsoleWidget', + shell_editor: 'ShellScintilla'): + super().__init__(console_widget) + self.console_widget: 'PythonConsoleWidget' = console_widget + self.shell_editor: 'ShellScintilla' = shell_editor # Creates layout for message bar self.layout = QGridLayout(self) @@ -202,7 +204,7 @@ def refreshSettingsOutput(self): def clearConsole(self): self.setText('') self.insertInitText() - self.shell.setFocus() + self.shell_editor.setFocus() def contextMenuEvent(self, e): menu = QMenu(self) @@ -254,7 +256,7 @@ def contextMenuEvent(self, e): settings_action = QAction(QgsApplication.getThemeIcon("console/iconSettingsConsole.svg"), QCoreApplication.translate("PythonConsole", "Options…"), menu) - settings_action.triggered.connect(self.parent.openSettings) + settings_action.triggered.connect(self.console_widget.openSettings) menu.addAction(settings_action) runAction.setEnabled(False) @@ -270,21 +272,21 @@ def contextMenuEvent(self, e): if not self.text(3) == '': selectAllAction.setEnabled(True) clearAction.setEnabled(True) - if self.parent.tabEditorWidget.isVisible(): + if self.console_widget.tabEditorWidget.isVisible(): showEditorAction.setEnabled(False) menu.exec(self.mapToGlobal(e.pos())) def hideToolBar(self): - tB = self.parent.toolBar + tB = self.console_widget.toolBar tB.hide() if tB.isVisible() else tB.show() - self.shell.setFocus() + self.shell_editor.setFocus() def showEditor(self): - Ed = self.parent.splitterObj + Ed = self.console_widget.splitterObj if not Ed.isVisible(): Ed.show() - self.parent.showEditorButton.setChecked(True) - self.shell.setFocus() + self.console_widget.showEditorButton.setChecked(True) + self.shell_editor.setFocus() def copy(self): """Copy text to clipboard... or keyboard interrupt""" @@ -297,21 +299,20 @@ def copy(self): def enteredSelected(self): cmd = self.selectedText() - self.shell.insertFromDropPaste(cmd) - self.shell.entered() + self.shell_editor.insertFromDropPaste(cmd) + self.shell_editor.entered() def keyPressEvent(self, e): # empty text indicates possible shortcut key sequence so stay in output txt = e.text() if len(txt) and txt >= " ": - self.shell.append(txt) - self.shell.moveCursorToEnd() - self.shell.setFocus() + self.shell_editor.append(txt) + self.shell_editor.moveCursorToEnd() + self.shell_editor.setFocus() e.ignore() else: # possible shortcut key sequence, accept it e.accept() - def widgetMessageBar(self, iface, text): - timeout = iface.messageTimeout() - self.infoBar.pushMessage(text, Qgis.MessageLevel.Info, timeout) + def widgetMessageBar(self, text: str): + self.infoBar.pushMessage(text, Qgis.MessageLevel.Info) diff --git a/python/console/console_sci.py b/python/console/console_sci.py index e80e75c06858e..bdd1b987b82ef 100644 --- a/python/console/console_sci.py +++ b/python/console/console_sci.py @@ -150,11 +150,11 @@ def _pyqgis(object=None): class PythonInterpreter(QgsCodeInterpreter, code.InteractiveInterpreter): - def __init__(self, shell): + def __init__(self, shell: 'ShellScintilla'): super(QgsCodeInterpreter, self).__init__() code.InteractiveInterpreter.__init__(self, locals=None) - self.shell = shell + self.shell: ShellScintilla = shell self.sub_process = None self.buffer = [] @@ -219,7 +219,7 @@ def execCommandImpl(self, cmd, show_input=True): re.findall(r'^\d.[0-9]*', Qgis.QGIS_VERSION)[0] if cmd == "?": - self.shell.parent.shellOut.insertHelp() + self.shell.console_widget.shell_output.insertHelp() elif cmd == '_pyqgis': webbrowser.open("https://qgis.org/pyqgis/{}".format(version)) elif cmd == '_api': @@ -280,15 +280,15 @@ def processFinished(self, errorcode): class ShellScintilla(QgsCodeEditorPython): - def __init__(self, parent=None): + def __init__(self, console_widget: 'PythonConsoleWidget'): # We set the ImmediatelyUpdateHistory flag here, as users can easily # crash QGIS by entering a Python command, and we don't want the - # history leading to the crash lost.. - super().__init__(parent, [], QgsCodeEditor.Mode.CommandInput, + # history leading to the crash lost... + super().__init__(console_widget, [], QgsCodeEditor.Mode.CommandInput, flags=QgsCodeEditor.Flags(QgsCodeEditor.Flag.CodeFolding | QgsCodeEditor.Flag.ImmediatelyUpdateHistory)) - self.parent = parent - self._interpreter = PythonInterpreter(self) + self.console_widget: 'PythonConsoleWidget' = console_widget + self._interpreter = PythonInterpreter(shell=self) self.setInterpreter(self._interpreter) self.opening = ['(', '{', '[', "'", '"'] @@ -335,12 +335,12 @@ def refreshSettingsShell(self): def on_session_history_cleared(self): msgText = QCoreApplication.translate('PythonConsole', 'Session history cleared successfully.') - self.parent.callWidgetMessageBar(msgText) + self.console_widget.callWidgetMessageBar(msgText) def on_persistent_history_cleared(self): msgText = QCoreApplication.translate('PythonConsole', 'History cleared successfully.') - self.parent.callWidgetMessageBar(msgText) + self.console_widget.callWidgetMessageBar(msgText) def keyPressEvent(self, e): From 4854779200bc78fb8dc03762809a37aafeb7c19b Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 9 May 2024 10:39:19 +1000 Subject: [PATCH 069/102] Use QgsCodeEditorWidget in python console script editor --- python/console/console_editor.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/python/console/console_editor.py b/python/console/console_editor.py index eae190dc8a634..e1a4557440aeb 100644 --- a/python/console/console_editor.py +++ b/python/console/console_editor.py @@ -32,7 +32,12 @@ from pathlib import Path from qgis.core import Qgis, QgsApplication, QgsBlockingNetworkRequest, QgsSettings -from qgis.gui import QgsCodeEditorPython, QgsMessageBar +from qgis.gui import ( + QgsCodeEditorPython, + QgsCodeEditorWidget, + QgsMessageBar +) + from qgis.PyQt.Qsci import QsciScintilla from qgis.PyQt.QtCore import QByteArray, QCoreApplication, QDir, QEvent, QFileInfo, QJsonDocument, QSize, Qt, QUrl from qgis.PyQt.QtGui import QKeySequence @@ -574,6 +579,11 @@ def __init__(self, self._editor = Editor(editor_tab=self, console_widget=console_widget, tab_widget=tab_widget) + + self._editor_code_widget = QgsCodeEditorWidget( + self._editor + ) + if filename: if QFileInfo(filename).exists(): self._editor.loadFile(filename, read_only) @@ -591,7 +601,7 @@ def __init__(self, self.tabLayout = QGridLayout(self) self.tabLayout.setContentsMargins(0, 0, 0, 0) - self.tabLayout.addWidget(self._editor) + self.tabLayout.addWidget(self._editor_code_widget) def modified(self, modified): self.tab_widget.tabModified(self, modified) From 64e0fff67f434dcab6877716a443fdf32cd14abd Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 9 May 2024 11:12:50 +1000 Subject: [PATCH 070/102] [console] Rely on QgsCodeEditorWidget search functionality Remove duplicate code searching functionality from console script editor and just use the standard QgsCodeEditorWidget implementation --- .../codeeditors/qgscodeeditorwidget.sip.in | 20 +++ python/console/console.py | 114 +++------------- python/console/console_editor.py | 125 +++++++++--------- .../codeeditors/qgscodeeditorwidget.sip.in | 20 +++ src/gui/codeeditors/qgscodeeditorwidget.cpp | 35 +++-- src/gui/codeeditors/qgscodeeditorwidget.h | 20 +++ 6 files changed, 164 insertions(+), 170 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in index 5d244b224817e..d25f83aac8fe5 100644 --- a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in @@ -43,6 +43,11 @@ Ownership of ``editor`` will be transferred to this widget. QgsCodeEditor *editor(); %Docstring Returns the wrapped code editor. +%End + + bool isSearchBarVisible() const; +%Docstring +Returns ``True`` if the search bar is visible. %End public slots: @@ -72,6 +77,21 @@ Sets whether the search bar is ``visible``. .. seealso:: :py:func:`showSearchBar` .. seealso:: :py:func:`hideSearchBar` +%End + + void triggerFind(); +%Docstring +Triggers a find operation, using the default behavior. + +This will automatically open the search bar and start a find operation using +the default behavior, e.g. searching for any selected text in the code editor. +%End + + signals: + + void searchBarToggled( bool visible ); +%Docstring +Emitted when the visibility of the search bar is changed. %End }; diff --git a/python/console/console.py b/python/console/console.py index 429957d502de0..4b3523fd4642a 100644 --- a/python/console/console.py +++ b/python/console/console.py @@ -177,7 +177,6 @@ def __init__(self, parent=None): self.splitterObj.setOrientation(Qt.Orientation.Horizontal) self.widgetEditor = QWidget(self.splitterObj) - self.widgetFind = QWidget(self) self.listClassMethod = QTreeWidget(self.splitterObj) self.listClassMethod.setColumnCount(2) @@ -189,8 +188,6 @@ def __init__(self, parent=None): # Hide side editor on start up self.splitterObj.hide() self.listClassMethod.hide() - # Hide search widget on start up - self.widgetFind.hide() icon_size = iface.iconSize(dockedToolbar=True) if iface else QSize(16, 16) @@ -318,16 +315,24 @@ def __init__(self, parent=None): self.objectListButton.setIconVisibleInMenu(True) self.objectListButton.setToolTip(objList) self.objectListButton.setText(objList) + # Action for Find text findText = QCoreApplication.translate("PythonConsole", "Find Text") - self.findTextButton = QAction(self) - self.findTextButton.setCheckable(True) - self.findTextButton.setEnabled(True) - self.findTextButton.setIcon(QgsApplication.getThemeIcon("console/iconSearchEditorConsole.svg")) - self.findTextButton.setMenuRole(QAction.MenuRole.PreferencesRole) - self.findTextButton.setIconVisibleInMenu(True) - self.findTextButton.setToolTip(findText) - self.findTextButton.setText(findText) + self.find_text_action = QAction(self) + self.find_text_action.setCheckable(True) + self.find_text_action.setEnabled(True) + self.find_text_action.setIcon(QgsApplication.getThemeIcon("console/iconSearchEditorConsole.svg")) + self.find_text_action.setMenuRole(QAction.MenuRole.PreferencesRole) + self.find_text_action.setIconVisibleInMenu(True) + self.find_text_action.setToolTip(findText) + self.find_text_action.setText(findText) + + self.tabEditorWidget.search_bar_toggled.connect( + self.find_text_action.setChecked + ) + self.find_text_action.toggled.connect( + self.tabEditorWidget.toggle_search_bar + ) # ----------------Toolbar Console------------------------------------- @@ -434,7 +439,7 @@ def __init__(self, parent=None): self.toolBarEditor.addAction(self.copyEditorButton) self.toolBarEditor.addAction(self.pasteEditorButton) self.toolBarEditor.addSeparator() - self.toolBarEditor.addAction(self.findTextButton) + self.toolBarEditor.addAction(self.find_text_action) self.toolBarEditor.addSeparator() self.toolBarEditor.addAction(self.toggleCommentEditorButton) self.toolBarEditor.addAction(self.reformatCodeEditorButton) @@ -480,47 +485,6 @@ def __init__(self, parent=None): self.layoutEditor.addWidget(self.toolBarEditor, 0, 1, 1, 1) self.layoutEditor.addWidget(self.widgetButtonEditor, 1, 0, 2, 1) self.layoutEditor.addWidget(self.tabEditorWidget, 1, 1, 1, 1) - self.layoutEditor.addWidget(self.widgetFind, 2, 1, 1, 1) - - # Layout for the find widget - self.layoutFind = QGridLayout(self.widgetFind) - self.layoutFind.setContentsMargins(0, 0, 0, 0) - self.lineEditFind = QgsFilterLineEdit() - self.lineEditFind.setShowSearchIcon(True) - placeHolderTxt = QCoreApplication.translate("PythonConsole", "Enter text to find…") - - self.lineEditFind.setPlaceholderText(placeHolderTxt) - self.toolBarFindText = QToolBar() - self.toolBarFindText.setIconSize(icon_size) - self.findNextButton = QAction(self) - self.findNextButton.setEnabled(False) - toolTipfindNext = QCoreApplication.translate("PythonConsole", "Find Next") - self.findNextButton.setToolTip(toolTipfindNext) - self.findNextButton.setIcon(QgsApplication.getThemeIcon("console/iconSearchNextEditorConsole.svg")) - self.findPrevButton = QAction(self) - self.findPrevButton.setEnabled(False) - toolTipfindPrev = QCoreApplication.translate("PythonConsole", "Find Previous") - self.findPrevButton.setToolTip(toolTipfindPrev) - self.findPrevButton.setIcon(QgsApplication.getThemeIcon("console/iconSearchPrevEditorConsole.svg")) - self.caseSensitive = QCheckBox() - caseSensTr = QCoreApplication.translate("PythonConsole", "Case Sensitive") - self.caseSensitive.setText(caseSensTr) - self.wholeWord = QCheckBox() - wholeWordTr = QCoreApplication.translate("PythonConsole", "Whole Word") - self.wholeWord.setText(wholeWordTr) - self.wrapAround = QCheckBox() - self.wrapAround.setChecked(True) - wrapAroundTr = QCoreApplication.translate("PythonConsole", "Wrap Around") - self.wrapAround.setText(wrapAroundTr) - - self.toolBarFindText.addWidget(self.lineEditFind) - self.toolBarFindText.addAction(self.findPrevButton) - self.toolBarFindText.addAction(self.findNextButton) - self.toolBarFindText.addWidget(self.caseSensitive) - self.toolBarFindText.addWidget(self.wholeWord) - self.toolBarFindText.addWidget(self.wrapAround) - - self.layoutFind.addWidget(self.toolBarFindText, 0, 1, 1, 1) # ------------ Add first Tab in Editor ------------------------------- @@ -528,7 +492,6 @@ def __init__(self, parent=None): # ------------ Signal ------------------------------- - self.findTextButton.triggered.connect(self._toggleFind) self.objectListButton.toggled.connect(self.toggleObjectListWidget) self.toggleCommentEditorButton.triggered.connect(self.toggleComment) self.reformatCodeEditorButton.triggered.connect(self.reformatCode) @@ -548,27 +511,6 @@ def __init__(self, parent=None): self.helpAPIAction.triggered.connect(self.openHelpAPI) self.helpCookbookAction.triggered.connect(self.openHelpCookbook) self.listClassMethod.itemClicked.connect(self.onClickGoToLine) - self.lineEditFind.returnPressed.connect(self._findNext) - self.findNextButton.triggered.connect(self._findNext) - self.findPrevButton.triggered.connect(self._findPrev) - self.lineEditFind.textChanged.connect(self._textFindChanged) - - self.findScut = QShortcut(QKeySequence.StandardKey.Find, self.widgetEditor) - self.findScut.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut) - self.findScut.activated.connect(self._openFind) - - self.findNextScut = QShortcut(QKeySequence.StandardKey.FindNext, self.widgetEditor) - self.findNextScut.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut) - self.findNextScut.activated.connect(self._findNext) - - self.findPreviousScut = QShortcut(QKeySequence.StandardKey.FindPrevious, self.widgetEditor) - self.findPreviousScut.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut) - self.findPreviousScut.activated.connect(self._findPrev) - - # Escape on editor hides the find bar - self.findScut = QShortcut(Qt.Key.Key_Escape, self.widgetEditor) - self.findScut.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut) - self.findScut.activated.connect(self._closeFind) if iface is not None: self.exit_blocker = ConsoleExitBlocker(self) @@ -601,28 +543,6 @@ def allowExit(self): def _toggleFind(self): self.tabEditorWidget.currentWidget().toggleFindWidget() - def _openFind(self): - self.tabEditorWidget.currentWidget().openFindWidget() - - def _closeFind(self): - self.tabEditorWidget.currentWidget().closeFindWidget() - - def _findNext(self): - self.tabEditorWidget.currentWidget().findText(True) - - def _findPrev(self): - self.tabEditorWidget.currentWidget().findText(False) - - def _textFindChanged(self): - if self.lineEditFind.text(): - self.findNextButton.setEnabled(True) - self.findPrevButton.setEnabled(True) - self.tabEditorWidget.currentWidget().findText(True, showMessage=False, findFirst=True) - else: - self.lineEditFind.setStyleSheet('') - self.findNextButton.setEnabled(False) - self.findPrevButton.setEnabled(False) - def onClickGoToLine(self, item, column): tabEditor = self.tabEditorWidget.currentWidget() if item.text(1) == 'syntaxError': diff --git a/python/console/console_editor.py b/python/console/console_editor.py index e1a4557440aeb..74604d1bcc896 100644 --- a/python/console/console_editor.py +++ b/python/console/console_editor.py @@ -39,7 +39,18 @@ ) from qgis.PyQt.Qsci import QsciScintilla -from qgis.PyQt.QtCore import QByteArray, QCoreApplication, QDir, QEvent, QFileInfo, QJsonDocument, QSize, Qt, QUrl +from qgis.PyQt.QtCore import ( + pyqtSignal, + QByteArray, + QCoreApplication, + QDir, + QEvent, + QFileInfo, + QJsonDocument, + QSize, + Qt, + QUrl +) from qgis.PyQt.QtGui import QKeySequence from qgis.PyQt.QtNetwork import QNetworkRequest from qgis.PyQt.QtWidgets import ( @@ -64,6 +75,8 @@ class Editor(QgsCodeEditorPython): + trigger_find = pyqtSignal() + def __init__(self, editor_tab: 'EditorTab', console_widget: 'PythonConsoleWidget', @@ -165,7 +178,7 @@ def contextMenuEvent(self, e): QgsApplication.getThemeIcon("console/iconSearchEditorConsole.svg"), QCoreApplication.translate("PythonConsole", "Find Text"), menu) - find_action.triggered.connect(self.openFindWidget) + find_action.triggered.connect(self.trigger_find) menu.addAction(find_action) cutAction = QAction( @@ -253,44 +266,6 @@ def contextMenuEvent(self, e): showCodeInspection.setEnabled(True) menu.exec(self.mapToGlobal(e.pos())) - def findText(self, forward, showMessage=True, findFirst=False): - lineFrom, indexFrom, lineTo, indexTo = self.getSelection() - if findFirst: - line = 0 - index = 0 - else: - line, index = self.getCursorPosition() - text = self.console_widget.lineEditFind.text() - re = False - wrap = self.console_widget.wrapAround.isChecked() - cs = self.console_widget.caseSensitive.isChecked() - wo = self.console_widget.wholeWord.isChecked() - notFound = False - if text: - if not forward: - line = lineFrom - index = indexFrom - # findFirst(QString(), re bool, cs bool, wo bool, wrap, bool, forward=True) - # re = Regular Expression, cs = Case Sensitive, wo = Whole Word, wrap = Wrap Around - if not self.findFirst(text, re, cs, wo, wrap, forward, line, index): - notFound = True - if notFound: - styleError = 'QLineEdit {background-color: #d65253; \ - color: #ffffff;}' - if showMessage: - msgText = QCoreApplication.translate('PythonConsole', - '"{0}" was not found.').format(text) - self.showMessage(msgText) - else: - styleError = '' - self.console_widget.lineEditFind.setStyleSheet(styleError) - - def findNext(self): - self.findText(True) - - def findPrevious(self): - self.findText(False) - def objectListEditor(self): listObj = self.console_widget.listClassMethod if listObj.isVisible(): @@ -343,26 +318,6 @@ def hideEditor(self): self.console_widget.splitterObj.hide() self.console_widget.showEditorButton.setChecked(False) - def openFindWidget(self): - wF = self.console_widget.widgetFind - wF.show() - if self.hasSelectedText(): - self.console_widget.lineEditFind.setText(self.selectedText().strip()) - self.console_widget.lineEditFind.setFocus() - self.console_widget.findTextButton.setChecked(True) - - def closeFindWidget(self): - wF = self.console_widget.widgetFind - wF.hide() - self.console_widget.findTextButton.setChecked(False) - - def toggleFindWidget(self): - wF = self.console_widget.widgetFind - if wF.isVisible(): - self.closeFindWidget() - else: - self.openFindWidget() - def createTempFile(self): name = tempfile.NamedTemporaryFile(delete=False).name # Need to use newline='' to avoid adding extra \r characters on Windows @@ -568,6 +523,8 @@ def showMessage(self, class EditorTab(QWidget): + search_bar_toggled = pyqtSignal(bool) + def __init__(self, tab_widget: 'EditorTabWidget', console_widget: 'PythonConsoleWidget', @@ -583,6 +540,13 @@ def __init__(self, self._editor_code_widget = QgsCodeEditorWidget( self._editor ) + self._editor_code_widget.searchBarToggled.connect( + self.search_bar_toggled + ) + + self._editor.trigger_find.connect( + self._editor_code_widget.triggerFind + ) if filename: if QFileInfo(filename).exists(): @@ -606,6 +570,24 @@ def __init__(self, def modified(self, modified): self.tab_widget.tabModified(self, modified) + def search_bar_visible(self) -> bool: + """ + Returns True if the tab's search bar is visible + """ + return self._editor_code_widget.isSearchBarVisible() + + def trigger_find(self): + """ + Triggers a find operation using the default behavior + """ + self._editor_code_widget.triggerFind() + + def hide_search_bar(self): + """ + Hides the search bar + """ + self._editor_code_widget.hideSearchBar() + def close(self): self.tab_widget._removeTab(self, tab2index=True) @@ -629,6 +611,8 @@ def showMessage(self, text, level=Qgis.MessageLevel.Info, timeout=-1, title=""): class EditorTabWidget(QTabWidget): + search_bar_toggled = pyqtSignal(bool) + def __init__(self, console_widget: 'PythonConsoleWidget'): super().__init__(parent=None) self.console_widget: 'PythonConsoleWidget' = console_widget @@ -725,6 +709,19 @@ def _currentWidgetChanged(self, tab): self.changeLastDirPath(tab) self.enableSaveIfModified(tab) + self.search_bar_toggled.emit( + self.currentWidget().search_bar_visible() + ) + + def toggle_search_bar(self, visible: bool): + """ + Toggles whether the search bar should be visible + """ + if visible and not self.currentWidget().search_bar_visible(): + self.currentWidget().trigger_find() + elif not visible and self.currentWidget().search_bar_visible(): + self.currentWidget().hide_search_bar() + def contextMenuEvent(self, e): tabBar = self.tabBar() self.idx = tabBar.tabAt(e.pos()) @@ -822,6 +819,14 @@ def newTabEditor(self, tabName=None, filename: Optional[str] = None): else: self.setTabToolTip(self.currentIndex(), tabName) + tab.search_bar_toggled.connect(self._tab_search_bar_toggled) + + def _tab_search_bar_toggled(self, visible: bool): + if self.sender() != self.currentWidget(): + return + + self.search_bar_toggled.emit(visible) + def tabModified(self, tab, modified): index = self.indexOf(tab) s = self.tabText(index) diff --git a/python/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in b/python/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in index 5d244b224817e..d25f83aac8fe5 100644 --- a/python/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in +++ b/python/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in @@ -43,6 +43,11 @@ Ownership of ``editor`` will be transferred to this widget. QgsCodeEditor *editor(); %Docstring Returns the wrapped code editor. +%End + + bool isSearchBarVisible() const; +%Docstring +Returns ``True`` if the search bar is visible. %End public slots: @@ -72,6 +77,21 @@ Sets whether the search bar is ``visible``. .. seealso:: :py:func:`showSearchBar` .. seealso:: :py:func:`hideSearchBar` +%End + + void triggerFind(); +%Docstring +Triggers a find operation, using the default behavior. + +This will automatically open the search bar and start a find operation using +the default behavior, e.g. searching for any selected text in the code editor. +%End + + signals: + + void searchBarToggled( bool visible ); +%Docstring +Emitted when the visibility of the search bar is changed. %End }; diff --git a/src/gui/codeeditors/qgscodeeditorwidget.cpp b/src/gui/codeeditors/qgscodeeditorwidget.cpp index ae5b9acad806a..56fcb0a3d9286 100644 --- a/src/gui/codeeditors/qgscodeeditorwidget.cpp +++ b/src/gui/codeeditors/qgscodeeditorwidget.cpp @@ -76,19 +76,7 @@ QgsCodeEditorWidget::QgsCodeEditorWidget( QgsCodeEditor *editor, QWidget *parent QShortcut *findShortcut = new QShortcut( QKeySequence::StandardKey::Find, mEditor ); findShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut ); - connect( findShortcut, &QShortcut::activated, this, [this] - { - clearSearchHighlights(); - mLineEditFind->setFocus(); - if ( mEditor->hasSelectedText() ) - { - mBlockSearching++; - mLineEditFind->setText( mEditor->selectedText().trimmed() ); - mBlockSearching--; - } - mLineEditFind->selectAll(); - showSearchBar(); - } ); + connect( findShortcut, &QShortcut::activated, this, &QgsCodeEditorWidget::triggerFind ); QShortcut *findNextShortcut = new QShortcut( QKeySequence::StandardKey::FindNext, this ); findNextShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut ); @@ -133,16 +121,23 @@ QgsCodeEditorWidget::QgsCodeEditorWidget( QgsCodeEditor *editor, QWidget *parent setLayout( vl ); } +bool QgsCodeEditorWidget::isSearchBarVisible() const +{ + return !mFindWidget->isHidden(); +} + void QgsCodeEditorWidget::showSearchBar() { addSearchHighlights(); mFindWidget->show(); + emit searchBarToggled( true ); } void QgsCodeEditorWidget::hideSearchBar() { clearSearchHighlights(); mFindWidget->hide(); + emit searchBarToggled( false ); } void QgsCodeEditorWidget::setSearchBarVisible( bool visible ) @@ -153,6 +148,20 @@ void QgsCodeEditorWidget::setSearchBarVisible( bool visible ) hideSearchBar(); } +void QgsCodeEditorWidget::triggerFind() +{ + clearSearchHighlights(); + mLineEditFind->setFocus(); + if ( mEditor->hasSelectedText() ) + { + mBlockSearching++; + mLineEditFind->setText( mEditor->selectedText().trimmed() ); + mBlockSearching--; + } + mLineEditFind->selectAll(); + showSearchBar(); +} + void QgsCodeEditorWidget::findNext() { findText( true, false ); diff --git a/src/gui/codeeditors/qgscodeeditorwidget.h b/src/gui/codeeditors/qgscodeeditorwidget.h index 78c6809a188ad..c489b0e17f689 100644 --- a/src/gui/codeeditors/qgscodeeditorwidget.h +++ b/src/gui/codeeditors/qgscodeeditorwidget.h @@ -59,6 +59,11 @@ class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget */ QgsCodeEditor *editor() { return mEditor; } + /** + * Returns TRUE if the search bar is visible. + */ + bool isSearchBarVisible() const; + public slots: /** @@ -85,6 +90,21 @@ class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget */ void setSearchBarVisible( bool visible ); + /** + * Triggers a find operation, using the default behavior. + * + * This will automatically open the search bar and start a find operation using + * the default behavior, e.g. searching for any selected text in the code editor. + */ + void triggerFind(); + + signals: + + /** + * Emitted when the visibility of the search bar is changed. + */ + void searchBarToggled( bool visible ); + private slots: void findNext(); From 58dafb48f8cfec4545b0b0767c93c235e6582416 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 9 May 2024 11:25:51 +1000 Subject: [PATCH 071/102] Move message bar logic to QgsCodeEditorWidget --- .../codeeditors/qgscodeeditorwidget.sip.in | 12 +++++++- python/console/console_editor.py | 13 +-------- .../codeeditors/qgscodeeditorwidget.sip.in | 12 +++++++- src/gui/codeeditors/qgscodeeditorwidget.cpp | 29 +++++++++++++++---- src/gui/codeeditors/qgscodeeditorwidget.h | 14 ++++++++- src/gui/qgsqueryresultwidget.cpp | 2 +- 6 files changed, 61 insertions(+), 21 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in index d25f83aac8fe5..a0c3ae61226a0 100644 --- a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in @@ -33,11 +33,16 @@ constructor. %End public: - QgsCodeEditorWidget( QgsCodeEditor *editor /Transfer/, QWidget *parent /TransferThis/ = 0 ); + QgsCodeEditorWidget( QgsCodeEditor *editor /Transfer/, + QgsMessageBar *messageBar = 0, + QWidget *parent /TransferThis/ = 0 ); %Docstring Constructor for QgsCodeEditorWidget, wrapping the specified ``editor`` widget. Ownership of ``editor`` will be transferred to this widget. + +If an explicit ``messageBar`` is specified then it will be used to provide +feedback, otherwise an integrated message bar will be used. %End QgsCodeEditor *editor(); @@ -48,6 +53,11 @@ Returns the wrapped code editor. bool isSearchBarVisible() const; %Docstring Returns ``True`` if the search bar is visible. +%End + + QgsMessageBar *messageBar(); +%Docstring +Returns the message bar associated with the widget, to use for user feedback. %End public slots: diff --git a/python/console/console_editor.py b/python/console/console_editor.py index 74604d1bcc896..5ece644cf2e79 100644 --- a/python/console/console_editor.py +++ b/python/console/console_editor.py @@ -552,17 +552,6 @@ def __init__(self, if QFileInfo(filename).exists(): self._editor.loadFile(filename, read_only) - # Creates layout for message bar - self.layout = QGridLayout(self._editor) - self.layout.setContentsMargins(0, 0, 0, 0) - spacerItem = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) - self.layout.addItem(spacerItem, 1, 0, 1, 1) - # messageBar instance - self.infoBar = QgsMessageBar() - sizePolicy = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed) - self.infoBar.setSizePolicy(sizePolicy) - self.layout.addWidget(self.infoBar, 0, 0, 1, 1) - self.tabLayout = QGridLayout(self) self.tabLayout.setContentsMargins(0, 0, 0, 0) self.tabLayout.addWidget(self._editor_code_widget) @@ -606,7 +595,7 @@ def __setattr__(self, name, value): return setattr(self._editor, name, value) def showMessage(self, text, level=Qgis.MessageLevel.Info, timeout=-1, title=""): - self.infoBar.pushMessage(title, text, level, timeout) + self._editor_code_widget.messageBar().pushMessage(title, text, level, timeout) class EditorTabWidget(QTabWidget): diff --git a/python/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in b/python/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in index d25f83aac8fe5..a0c3ae61226a0 100644 --- a/python/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in +++ b/python/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in @@ -33,11 +33,16 @@ constructor. %End public: - QgsCodeEditorWidget( QgsCodeEditor *editor /Transfer/, QWidget *parent /TransferThis/ = 0 ); + QgsCodeEditorWidget( QgsCodeEditor *editor /Transfer/, + QgsMessageBar *messageBar = 0, + QWidget *parent /TransferThis/ = 0 ); %Docstring Constructor for QgsCodeEditorWidget, wrapping the specified ``editor`` widget. Ownership of ``editor`` will be transferred to this widget. + +If an explicit ``messageBar`` is specified then it will be used to provide +feedback, otherwise an integrated message bar will be used. %End QgsCodeEditor *editor(); @@ -48,6 +53,11 @@ Returns the wrapped code editor. bool isSearchBarVisible() const; %Docstring Returns ``True`` if the search bar is visible. +%End + + QgsMessageBar *messageBar(); +%Docstring +Returns the message bar associated with the widget, to use for user feedback. %End public slots: diff --git a/src/gui/codeeditors/qgscodeeditorwidget.cpp b/src/gui/codeeditors/qgscodeeditorwidget.cpp index 56fcb0a3d9286..f58b47222458a 100644 --- a/src/gui/codeeditors/qgscodeeditorwidget.cpp +++ b/src/gui/codeeditors/qgscodeeditorwidget.cpp @@ -18,15 +18,20 @@ #include "qgsfilterlineedit.h" #include "qgsapplication.h" #include "qgsguiutils.h" +#include "qgsmessagebar.h" #include #include #include #include -QgsCodeEditorWidget::QgsCodeEditorWidget( QgsCodeEditor *editor, QWidget *parent ) +QgsCodeEditorWidget::QgsCodeEditorWidget( + QgsCodeEditor *editor, + QgsMessageBar *messageBar, + QWidget *parent ) : QgsPanelWidget( parent ) , mEditor( editor ) + , mMessageBar( messageBar ) { Q_ASSERT( mEditor ); @@ -35,6 +40,18 @@ QgsCodeEditorWidget::QgsCodeEditorWidget( QgsCodeEditor *editor, QWidget *parent vl->setSpacing( 0 ); vl->addWidget( editor, 1 ); + if ( !mMessageBar ) + { + QGridLayout *layout = new QGridLayout( mEditor ); + layout->setContentsMargins( 0, 0, 0, 0 ); + layout->addItem( new QSpacerItem( 20, 40, QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding ), 1, 0, 1, 1 ); + + mMessageBar = new QgsMessageBar(); + QSizePolicy sizePolicy = QSizePolicy( QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Fixed ); + mMessageBar->setSizePolicy( sizePolicy ); + layout->addWidget( mMessageBar, 0, 0, 1, 1 ); + } + mFindWidget = new QWidget(); QHBoxLayout *layoutFind = new QHBoxLayout(); layoutFind->setContentsMargins( 0, 2, 0, 0 ); @@ -126,6 +143,11 @@ bool QgsCodeEditorWidget::isSearchBarVisible() const return !mFindWidget->isHidden(); } +QgsMessageBar *QgsCodeEditorWidget::messageBar() +{ + return mMessageBar; +} + void QgsCodeEditorWidget::showSearchBar() { addSearchHighlights(); @@ -272,14 +294,11 @@ void QgsCodeEditorWidget::findText( bool forward, bool findFirst, bool showNotFo const QString styleError = QStringLiteral( "QLineEdit {background-color: #d65253; color: #ffffff;}" ); mLineEditFind->setStyleSheet( styleError ); - Q_UNUSED( showNotFoundWarning ) -#if 0 // TODO -- port this bit when messagebar is available - if ( showMessage ) + if ( showNotFoundWarning ) { mMessageBar->pushMessage( QString(), tr( "\"%1\" was not found" ).arg( searchString ), Qgis::MessageLevel::Info ); } -#endif } else { diff --git a/src/gui/codeeditors/qgscodeeditorwidget.h b/src/gui/codeeditors/qgscodeeditorwidget.h index c489b0e17f689..40f0512ed4463 100644 --- a/src/gui/codeeditors/qgscodeeditorwidget.h +++ b/src/gui/codeeditors/qgscodeeditorwidget.h @@ -24,6 +24,7 @@ class QgsCodeEditor; class QgsFilterLineEdit; class QToolButton; class QCheckBox; +class QgsMessageBar; SIP_IF_MODULE( HAVE_QSCI_SIP ) @@ -51,8 +52,13 @@ class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget * Constructor for QgsCodeEditorWidget, wrapping the specified \a editor widget. * * Ownership of \a editor will be transferred to this widget. + * + * If an explicit \a messageBar is specified then it will be used to provide + * feedback, otherwise an integrated message bar will be used. */ - QgsCodeEditorWidget( QgsCodeEditor *editor SIP_TRANSFER, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsCodeEditorWidget( QgsCodeEditor *editor SIP_TRANSFER, + QgsMessageBar *messageBar = nullptr, + QWidget *parent SIP_TRANSFERTHIS = nullptr ); /** * Returns the wrapped code editor. @@ -64,6 +70,11 @@ class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget */ bool isSearchBarVisible() const; + /** + * Returns the message bar associated with the widget, to use for user feedback. + */ + QgsMessageBar *messageBar(); + public slots: /** @@ -127,6 +138,7 @@ class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget QCheckBox *mWholeWordCheck = nullptr; QCheckBox *mWrapAroundCheck = nullptr; int mBlockSearching = 0; + QgsMessageBar *mMessageBar = nullptr; }; #endif // QGSCODEEDITORWIDGET_H diff --git a/src/gui/qgsqueryresultwidget.cpp b/src/gui/qgsqueryresultwidget.cpp index 46083f3ed0e42..7fe968bb9b4f8 100644 --- a/src/gui/qgsqueryresultwidget.cpp +++ b/src/gui/qgsqueryresultwidget.cpp @@ -47,7 +47,7 @@ QgsQueryResultWidget::QgsQueryResultWidget( QWidget *parent, QgsAbstractDatabase mProgressBar->hide(); mSqlEditor = new QgsCodeEditorSQL(); - mCodeEditorWidget = new QgsCodeEditorWidget( mSqlEditor ); + mCodeEditorWidget = new QgsCodeEditorWidget( mSqlEditor, mMessageBar ); QVBoxLayout *vl = new QVBoxLayout(); vl->setContentsMargins( 0, 0, 0, 0 ); vl->addWidget( mCodeEditorWidget ); From cd3b7f17bf7794eb23a632df36711a8829f54f9c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 10 May 2024 10:05:36 +1000 Subject: [PATCH 072/102] Fix lint error in type checking --- python/console/console_editor.py | 36 ++++++++++++++++++++------------ python/console/console_output.py | 21 +++++++++++++------ python/console/console_sci.py | 21 ++++++++++++------- 3 files changed, 52 insertions(+), 26 deletions(-) diff --git a/python/console/console_editor.py b/python/console/console_editor.py index 5ece644cf2e79..1e2cbcb6cc415 100644 --- a/python/console/console_editor.py +++ b/python/console/console_editor.py @@ -1,4 +1,3 @@ -# -*- coding:utf-8 -*- """ /*************************************************************************** Python Console for QGIS @@ -19,6 +18,11 @@ Some portions of code were taken from https://code.google.com/p/pydee/ """ +try: + from __future__ import annotations +except SyntaxError: + pass + import codecs import importlib import os @@ -26,7 +30,10 @@ import re import sys import tempfile -from typing import Optional +from typing import ( + Optional, + TYPE_CHECKING +) from functools import partial from operator import itemgetter from pathlib import Path @@ -72,19 +79,22 @@ ) from qgis.utils import OverrideCursor, iface +if TYPE_CHECKING: + from .console import PythonConsoleWidget + class Editor(QgsCodeEditorPython): trigger_find = pyqtSignal() def __init__(self, - editor_tab: 'EditorTab', - console_widget: 'PythonConsoleWidget', - tab_widget: 'EditorTabWidget'): + editor_tab: EditorTab, + console_widget: PythonConsoleWidget, + tab_widget: EditorTabWidget): super().__init__(editor_tab) - self.editor_tab: 'EditorTab' = editor_tab - self.console_widget: 'PythonConsoleWidget' = console_widget - self.tab_widget: 'EditorTabWidget' = tab_widget + self.editor_tab: EditorTab = editor_tab + self.console_widget: PythonConsoleWidget = console_widget + self.tab_widget: EditorTabWidget = tab_widget self.path: Optional[str] = None # recent modification time @@ -526,12 +536,12 @@ class EditorTab(QWidget): search_bar_toggled = pyqtSignal(bool) def __init__(self, - tab_widget: 'EditorTabWidget', - console_widget: 'PythonConsoleWidget', + tab_widget: EditorTabWidget, + console_widget: PythonConsoleWidget, filename: Optional[str], read_only: bool): super().__init__(tab_widget) - self.tab_widget: 'EditorTabWidget' = tab_widget + self.tab_widget: EditorTabWidget = tab_widget self._editor = Editor(editor_tab=self, console_widget=console_widget, @@ -602,9 +612,9 @@ class EditorTabWidget(QTabWidget): search_bar_toggled = pyqtSignal(bool) - def __init__(self, console_widget: 'PythonConsoleWidget'): + def __init__(self, console_widget: PythonConsoleWidget): super().__init__(parent=None) - self.console_widget: 'PythonConsoleWidget' = console_widget + self.console_widget: PythonConsoleWidget = console_widget self.idx = -1 # Layout for top frame (restore tabs) diff --git a/python/console/console_output.py b/python/console/console_output.py index 3772eed53611a..98f9638e68f65 100644 --- a/python/console/console_output.py +++ b/python/console/console_output.py @@ -1,4 +1,3 @@ -# -*- coding:utf-8 -*- """ /*************************************************************************** Python Console for QGIS @@ -18,6 +17,13 @@ ***************************************************************************/ Some portions of code were taken from https://code.google.com/p/pydee/ """ +try: + from __future__ import annotations +except SyntaxError: + pass + +import sys +from typing import TYPE_CHECKING from qgis.PyQt import sip from qgis.PyQt.QtCore import Qt, QCoreApplication, QThread, QMetaObject, Q_ARG, QObject, pyqtSlot @@ -26,7 +32,10 @@ from qgis.PyQt.Qsci import QsciScintilla from qgis.core import Qgis, QgsApplication, QgsSettings from qgis.gui import QgsMessageBar, QgsCodeEditorPython -import sys + +if TYPE_CHECKING: + from .console import PythonConsoleWidget + from .console_sci import ShellScintilla class writeOut(QObject): @@ -118,11 +127,11 @@ def isatty(self): class ShellOutputScintilla(QgsCodeEditorPython): def __init__(self, - console_widget: 'PythonConsoleWidget', - shell_editor: 'ShellScintilla'): + console_widget: PythonConsoleWidget, + shell_editor: ShellScintilla): super().__init__(console_widget) - self.console_widget: 'PythonConsoleWidget' = console_widget - self.shell_editor: 'ShellScintilla' = shell_editor + self.console_widget: PythonConsoleWidget = console_widget + self.shell_editor: ShellScintilla = shell_editor # Creates layout for message bar self.layout = QGridLayout(self) diff --git a/python/console/console_sci.py b/python/console/console_sci.py index bdd1b987b82ef..57834086789f8 100644 --- a/python/console/console_sci.py +++ b/python/console/console_sci.py @@ -1,4 +1,3 @@ -# -*- coding:utf-8 -*- """ /*************************************************************************** Python Console for QGIS @@ -19,12 +18,20 @@ Some portions of code were taken from https://code.google.com/p/pydee/ """ +try: + from __future__ import annotations +except SyntaxError: + pass + import code import os import re import sys import traceback -from typing import Optional +from typing import ( + Optional, + TYPE_CHECKING +) from pathlib import Path from tempfile import NamedTemporaryFile @@ -34,18 +41,18 @@ from qgis.PyQt.QtWidgets import QShortcut, QApplication from qgis.core import ( QgsApplication, - QgsSettings, Qgis, QgsProcessingUtils ) from qgis.gui import ( QgsCodeEditorPython, - QgsCodeEditorColorScheme, QgsCodeEditor, QgsCodeInterpreter ) from .process_wrapper import ProcessWrapper +if TYPE_CHECKING: + from .console import PythonConsoleWidget _init_statements = [ # Python @@ -150,7 +157,7 @@ def _pyqgis(object=None): class PythonInterpreter(QgsCodeInterpreter, code.InteractiveInterpreter): - def __init__(self, shell: 'ShellScintilla'): + def __init__(self, shell: ShellScintilla): super(QgsCodeInterpreter, self).__init__() code.InteractiveInterpreter.__init__(self, locals=None) @@ -280,14 +287,14 @@ def processFinished(self, errorcode): class ShellScintilla(QgsCodeEditorPython): - def __init__(self, console_widget: 'PythonConsoleWidget'): + def __init__(self, console_widget: PythonConsoleWidget): # We set the ImmediatelyUpdateHistory flag here, as users can easily # crash QGIS by entering a Python command, and we don't want the # history leading to the crash lost... super().__init__(console_widget, [], QgsCodeEditor.Mode.CommandInput, flags=QgsCodeEditor.Flags(QgsCodeEditor.Flag.CodeFolding | QgsCodeEditor.Flag.ImmediatelyUpdateHistory)) - self.console_widget: 'PythonConsoleWidget' = console_widget + self.console_widget: PythonConsoleWidget = console_widget self._interpreter = PythonInterpreter(shell=self) self.setInterpreter(self._interpreter) From eece497f3e6b5be4cc9369c083e096b583e84b68 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 10 May 2024 10:06:58 +1000 Subject: [PATCH 073/102] Remove redundant coding lines --- python/console/console.py | 1 - python/console/console_compile_apis.py | 1 - python/console/console_settings.py | 1 - python/console/process_wrapper.py | 2 -- 4 files changed, 5 deletions(-) diff --git a/python/console/console.py b/python/console/console.py index 4b3523fd4642a..adc5ff55873fa 100644 --- a/python/console/console.py +++ b/python/console/console.py @@ -1,4 +1,3 @@ -# -*- coding:utf-8 -*- """ /*************************************************************************** Python Console for QGIS diff --git a/python/console/console_compile_apis.py b/python/console/console_compile_apis.py index 73634f4d20ffc..32204bb4ebc2d 100644 --- a/python/console/console_compile_apis.py +++ b/python/console/console_compile_apis.py @@ -1,4 +1,3 @@ -# -*- coding:utf-8 -*- """ /*************************************************************************** Module to generate prepared APIs for calltips and auto-completion. diff --git a/python/console/console_settings.py b/python/console/console_settings.py index 36ba6e2df4091..5c9e2b08e7eca 100644 --- a/python/console/console_settings.py +++ b/python/console/console_settings.py @@ -1,4 +1,3 @@ -# -*- coding:utf-8 -*- """ /*************************************************************************** Python Console for QGIS diff --git a/python/console/process_wrapper.py b/python/console/process_wrapper.py index b18f43b4bc26d..b59d8a6ead92c 100644 --- a/python/console/process_wrapper.py +++ b/python/console/process_wrapper.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ *************************************************************************** process_wrapper.py From 8b60a8bfeeb22a493a774492107801d1d3f2c3cd Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 8 May 2024 15:19:22 +1000 Subject: [PATCH 074/102] Show indicators for search results in scrollbar --- src/gui/codeeditors/qgscodeeditorpython.cpp | 2 ++ src/gui/codeeditors/qgscodeeditorwidget.cpp | 16 ++++++++++++++++ src/gui/codeeditors/qgscodeeditorwidget.h | 8 ++++++++ 3 files changed, 26 insertions(+) diff --git a/src/gui/codeeditors/qgscodeeditorpython.cpp b/src/gui/codeeditors/qgscodeeditorpython.cpp index 7f2a698e15927..a0c96d5bf6d0c 100644 --- a/src/gui/codeeditors/qgscodeeditorpython.cpp +++ b/src/gui/codeeditors/qgscodeeditorpython.cpp @@ -93,6 +93,8 @@ void QgsCodeEditorPython::initializeLexer() setWhitespaceVisibility( QsciScintilla::WsVisibleAfterIndent ); + SendScintilla( QsciScintillaBase::SCI_SETPROPERTY, "highlight.current.word", "1" ); + QFont font = lexerFont(); const QColor defaultColor = lexerColor( QgsCodeEditorColorScheme::ColorRole::Default ); diff --git a/src/gui/codeeditors/qgscodeeditorwidget.cpp b/src/gui/codeeditors/qgscodeeditorwidget.cpp index f58b47222458a..cfae24713e239 100644 --- a/src/gui/codeeditors/qgscodeeditorwidget.cpp +++ b/src/gui/codeeditors/qgscodeeditorwidget.cpp @@ -19,6 +19,7 @@ #include "qgsapplication.h" #include "qgsguiutils.h" #include "qgsmessagebar.h" +#include "qgsdecoratedscrollbar.h" #include #include @@ -136,8 +137,13 @@ QgsCodeEditorWidget::QgsCodeEditorWidget( mFindWidget->hide(); setLayout( vl ); + + mHighlightController = std::make_unique< QgsScrollBarHighlightController >(); + mHighlightController->setScrollArea( mEditor ); } +QgsCodeEditorWidget::~QgsCodeEditorWidget() = default; + bool QgsCodeEditorWidget::isSearchBarVisible() const { return !mFindWidget->isHidden(); @@ -233,6 +239,9 @@ void QgsCodeEditorWidget::addSearchHighlights() long startPos = 0; long docEnd = mEditor->length(); + mHighlightController->setLineHeight( QFontMetrics( mEditor->font() ).lineSpacing() ); + mHighlightController->setVisibleRange( mEditor->viewport()->rect().height() ); + while ( true ) { mEditor->SendScintilla( QsciScintilla::SCI_SETTARGETRANGE, startPos, docEnd ); @@ -244,6 +253,11 @@ void QgsCodeEditorWidget::addSearchHighlights() mEditor->SendScintilla( QsciScintilla::SCI_SETINDICATORCURRENT, QgsCodeEditor::SEARCH_RESULT_INDICATOR ); mEditor->SendScintilla( QsciScintilla::SCI_INDICATORFILLRANGE, fstart, searchString.length() ); + + int thisLine = 0; + int thisIndex = 0; + mEditor->lineIndexFromPosition( fstart, &thisLine, &thisIndex ); + mHighlightController->addHighlight( QgsScrollBarHighlight( SearchMatch, thisLine, QColor( 0, 200, 0 ), QgsScrollBarHighlight::Priority::HighPriority ) ); } mEditor->SendScintilla( QsciScintilla::SCI_SETTARGETRANGE, originalStartPos, originalEndPos ); @@ -255,6 +269,8 @@ void QgsCodeEditorWidget::clearSearchHighlights() long docEnd = mEditor->length(); mEditor->SendScintilla( QsciScintilla::SCI_SETINDICATORCURRENT, QgsCodeEditor::SEARCH_RESULT_INDICATOR ); mEditor->SendScintilla( QsciScintilla::SCI_INDICATORCLEARRANGE, docStart, docEnd - docStart ); + + mHighlightController->removeHighlights( SearchMatch ); } void QgsCodeEditorWidget::findText( bool forward, bool findFirst, bool showNotFoundWarning ) diff --git a/src/gui/codeeditors/qgscodeeditorwidget.h b/src/gui/codeeditors/qgscodeeditorwidget.h index 40f0512ed4463..dffbcd55a1e73 100644 --- a/src/gui/codeeditors/qgscodeeditorwidget.h +++ b/src/gui/codeeditors/qgscodeeditorwidget.h @@ -25,6 +25,7 @@ class QgsFilterLineEdit; class QToolButton; class QCheckBox; class QgsMessageBar; +class QgsScrollBarHighlightController; SIP_IF_MODULE( HAVE_QSCI_SIP ) @@ -59,6 +60,7 @@ class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget QgsCodeEditorWidget( QgsCodeEditor *editor SIP_TRANSFER, QgsMessageBar *messageBar = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + ~QgsCodeEditorWidget() override; /** * Returns the wrapped code editor. @@ -129,6 +131,11 @@ class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget void addSearchHighlights(); void findText( bool forward, bool findFirst, bool showNotFoundWarning = false ); + enum HighlightCategory + { + SearchMatch = 0 + }; + QgsCodeEditor *mEditor = nullptr; QWidget *mFindWidget = nullptr; QgsFilterLineEdit *mLineEditFind = nullptr; @@ -139,6 +146,7 @@ class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget QCheckBox *mWrapAroundCheck = nullptr; int mBlockSearching = 0; QgsMessageBar *mMessageBar = nullptr; + std::unique_ptr< QgsScrollBarHighlightController > mHighlightController; }; #endif // QGSCODEEDITORWIDGET_H From 5959be560e88a54d973ce2074e39cb77d2df78b7 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 10 May 2024 10:54:04 +1000 Subject: [PATCH 075/102] Fix future imports --- python/console/console_editor.py | 6 +----- python/console/console_output.py | 5 +---- python/console/console_sci.py | 6 +----- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/python/console/console_editor.py b/python/console/console_editor.py index 1e2cbcb6cc415..9c3921bac3e1e 100644 --- a/python/console/console_editor.py +++ b/python/console/console_editor.py @@ -17,11 +17,7 @@ ***************************************************************************/ Some portions of code were taken from https://code.google.com/p/pydee/ """ - -try: - from __future__ import annotations -except SyntaxError: - pass +from __future__ import annotations import codecs import importlib diff --git a/python/console/console_output.py b/python/console/console_output.py index 98f9638e68f65..a07c79d3e021c 100644 --- a/python/console/console_output.py +++ b/python/console/console_output.py @@ -17,10 +17,7 @@ ***************************************************************************/ Some portions of code were taken from https://code.google.com/p/pydee/ """ -try: - from __future__ import annotations -except SyntaxError: - pass +from __future__ import annotations import sys from typing import TYPE_CHECKING diff --git a/python/console/console_sci.py b/python/console/console_sci.py index 57834086789f8..ba0688c8d9df2 100644 --- a/python/console/console_sci.py +++ b/python/console/console_sci.py @@ -17,11 +17,7 @@ ***************************************************************************/ Some portions of code were taken from https://code.google.com/p/pydee/ """ - -try: - from __future__ import annotations -except SyntaxError: - pass +from __future__ import annotations import code import os From 280b92cd44a2f445576886ce17f131fde88cebac Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 10 May 2024 10:57:35 +1000 Subject: [PATCH 076/102] Indentation --- .../gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in | 1 + .../gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in | 1 + src/gui/codeeditors/qgscodeeditorwidget.h | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in index a0c3ae61226a0..eaeb55ef70fd2 100644 --- a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in @@ -44,6 +44,7 @@ Ownership of ``editor`` will be transferred to this widget. If an explicit ``messageBar`` is specified then it will be used to provide feedback, otherwise an integrated message bar will be used. %End + ~QgsCodeEditorWidget(); QgsCodeEditor *editor(); %Docstring diff --git a/python/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in b/python/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in index a0c3ae61226a0..eaeb55ef70fd2 100644 --- a/python/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in +++ b/python/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in @@ -44,6 +44,7 @@ Ownership of ``editor`` will be transferred to this widget. If an explicit ``messageBar`` is specified then it will be used to provide feedback, otherwise an integrated message bar will be used. %End + ~QgsCodeEditorWidget(); QgsCodeEditor *editor(); %Docstring diff --git a/src/gui/codeeditors/qgscodeeditorwidget.h b/src/gui/codeeditors/qgscodeeditorwidget.h index dffbcd55a1e73..16056642433f6 100644 --- a/src/gui/codeeditors/qgscodeeditorwidget.h +++ b/src/gui/codeeditors/qgscodeeditorwidget.h @@ -60,7 +60,7 @@ class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget QgsCodeEditorWidget( QgsCodeEditor *editor SIP_TRANSFER, QgsMessageBar *messageBar = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); - ~QgsCodeEditorWidget() override; + ~QgsCodeEditorWidget() override; /** * Returns the wrapped code editor. From 99179d1caf32fd044a9681b9f0ea7ff733b5855a Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Fri, 10 May 2024 07:20:09 +0200 Subject: [PATCH 077/102] Reduce queries to detect select privileges for PostgreSQL relations (#57389) Reduce queries to detect select privileges for PostgreSQL relations Use a single query rather than 3 to determine recovery status, select privilege and update/insert/delete privileges. Also drops the need to evaluate view definitions to determine selectability. Co-authored-by: Juergen E. Fischer --- .../postgres/qgspostgresprovider.cpp | 139 +++++++++--------- .../python/test_provider_postgres_latency.py | 2 +- 2 files changed, 69 insertions(+), 72 deletions(-) diff --git a/src/providers/postgres/qgspostgresprovider.cpp b/src/providers/postgres/qgspostgresprovider.cpp index 9d716742887db..c964d57e0ea24 100644 --- a/src/providers/postgres/qgspostgresprovider.cpp +++ b/src/providers/postgres/qgspostgresprovider.cpp @@ -1492,113 +1492,110 @@ bool QgsPostgresProvider::hasSufficientPermsAndCapabilities() mEnabledCapabilities = QgsVectorDataProvider::Capability::ReloadData; + QString sql; QgsPostgresResult testAccess; + + bool forceReadOnly = ( mReadFlags & QgsDataProvider::ForceReadOnly ); + bool inRecovery = false; + sql = QStringLiteral( "SELECT " + "has_table_privilege(%1,'SELECT')," // 0 + "pg_is_in_recovery()," // 1 + "current_schema(), " // 2 + "has_table_privilege(%1,'INSERT')," // 3 + "has_table_privilege(%1,'DELETE')" ) // 4 + .arg( quotedValue( mQuery ) ); + if ( !mIsQuery ) { - // Check that we can read from the table (i.e., we have select permission). - QString sql = QStringLiteral( "SELECT * FROM %1 LIMIT 1" ).arg( mQuery ); - QgsPostgresResult testAccess( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) ); + + // postgres has fast access to features at id (thanks to primary key / unique index) + // the latter flag is here just for compatibility + if ( !mSelectAtIdDisabled ) + { + mEnabledCapabilities |= QgsVectorDataProvider::SelectAtId; + } + + if ( connectionRO()->pgVersion() >= 80400 ) + { + sql += QString( ",has_any_column_privilege(%1,'UPDATE')" // 5 + ",%2" ) // 6 + .arg( quotedValue( mQuery ), + mGeometryColumn.isNull() + ? QStringLiteral( "'f'" ) + : QStringLiteral( "has_column_privilege(%1,%2,'UPDATE')" ) + .arg( quotedValue( mQuery ), + quotedValue( mGeometryColumn ) ) + ); + } + else + { + sql += QString( ",has_table_privilege(%1,'UPDATE')" // 5 + ",has_table_privilege(%1,'UPDATE')" ) // 6 + .arg( quotedValue( mQuery ) ); + } + + testAccess = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ); if ( testAccess.PQresultStatus() != PGRES_TUPLES_OK ) { - QgsMessageLog::logMessage( tr( "Unable to access the %1 relation.\nThe error message from the database was:\n%2.\nSQL: %3" ) + QgsMessageLog::logMessage( tr( "Unable to determine table access privileges for the %1 relation.\nThe error message from the database was:\n%2.\nSQL: %3" ) .arg( mQuery, testAccess.PQresultErrorMessage(), - sql ), tr( "PostGIS" ) ); + sql ), + tr( "PostGIS" ) ); return false; } - bool forceReadOnly = ( mReadFlags & QgsDataProvider::ForceReadOnly ); - bool inRecovery = false; - // Check if the database is still in recovery after a database crash - // or if you are connected to a (read-only) standby server - // only if the provider has not been force to be in read-only mode - if ( !forceReadOnly && connectionRO()->pgVersion() >= 90000 ) + if ( testAccess.PQgetvalue( 0, 0 ) != QLatin1String( "t" ) ) { - testAccess = connectionRO()->LoggedPQexec( "QgsPostgresProvider", QStringLiteral( "SELECT pg_is_in_recovery()" ) ); - if ( testAccess.PQresultStatus() != PGRES_TUPLES_OK || testAccess.PQgetvalue( 0, 0 ) == QLatin1String( "t" ) ) - { - QgsMessageLog::logMessage( tr( "PostgreSQL is still in recovery after a database crash\n(or you are connected to a (read-only) standby server).\nWrite accesses will be denied." ), tr( "PostGIS" ) ); - inRecovery = true; - } + // SELECT + QgsMessageLog::logMessage( tr( "User has no SELECT privilege on %1 relation." ) + .arg( mQuery ), tr( "PostGIS" ) ); + return false; } - // postgres has fast access to features at id (thanks to primary key / unique index) - // the latter flag is here just for compatibility - if ( !mSelectAtIdDisabled ) + if ( testAccess.PQgetvalue( 0, 1 ) == QLatin1String( "t" ) ) { - mEnabledCapabilities |= QgsVectorDataProvider::SelectAtId; + // RECOVERY + QgsMessageLog::logMessage( + tr( "PostgreSQL is still in recovery after a database crash\n(or you are connected to a (read-only) standby server).\nWrite accesses will be denied." ), + tr( "PostGIS" ) + ); + inRecovery = true; } - // Do not check the editable capabilities if the provider has been forced to be + // CURRENT SCHEMA + if ( mSchemaName.isEmpty() ) + mSchemaName = testAccess.PQgetvalue( 0, 2 ); + + // Do not set editable capabilities if the provider has been forced to be // in read-only mode or if the database is still in recovery if ( !forceReadOnly && !inRecovery ) { - if ( connectionRO()->pgVersion() >= 80400 ) - { - sql = QString( "SELECT " - "has_table_privilege(%1,'DELETE')," - "has_any_column_privilege(%1,'UPDATE')," - "%2" - "has_table_privilege(%1,'INSERT')," - "current_schema()" ) - .arg( quotedValue( mQuery ), - mGeometryColumn.isNull() - ? QStringLiteral( "'f'," ) - : QStringLiteral( "has_column_privilege(%1,%2,'UPDATE')," ) - .arg( quotedValue( mQuery ), - quotedValue( mGeometryColumn ) ) - ); - } - else - { - sql = QString( "SELECT " - "has_table_privilege(%1,'DELETE')," - "has_table_privilege(%1,'UPDATE')," - "has_table_privilege(%1,'UPDATE')," - "has_table_privilege(%1,'INSERT')," - "current_schema()" ) - .arg( quotedValue( mQuery ) ); - } - - testAccess = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ); - if ( testAccess.PQresultStatus() != PGRES_TUPLES_OK ) + if ( testAccess.PQgetvalue( 0, 3 ) == QLatin1String( "t" ) ) { - QgsMessageLog::logMessage( tr( "Unable to determine table access privileges for the %1 relation.\nThe error message from the database was:\n%2.\nSQL: %3" ) - .arg( mQuery, - testAccess.PQresultErrorMessage(), - sql ), - tr( "PostGIS" ) ); - return false; + // INSERT + mEnabledCapabilities |= QgsVectorDataProvider::AddFeatures; } - - if ( testAccess.PQgetvalue( 0, 0 ) == QLatin1String( "t" ) ) + if ( testAccess.PQgetvalue( 0, 4 ) == QLatin1String( "t" ) ) { // DELETE mEnabledCapabilities |= QgsVectorDataProvider::DeleteFeatures | QgsVectorDataProvider::FastTruncate; } - if ( testAccess.PQgetvalue( 0, 1 ) == QLatin1String( "t" ) ) + if ( testAccess.PQgetvalue( 0, 5 ) == QLatin1String( "t" ) ) { // UPDATE mEnabledCapabilities |= QgsVectorDataProvider::ChangeAttributeValues; } - if ( testAccess.PQgetvalue( 0, 2 ) == QLatin1String( "t" ) ) + if ( testAccess.PQgetvalue( 0, 6 ) == QLatin1String( "t" ) ) { - // UPDATE + // UPDATE (geom column specific) mEnabledCapabilities |= QgsVectorDataProvider::ChangeGeometries; } - if ( testAccess.PQgetvalue( 0, 3 ) == QLatin1String( "t" ) ) - { - // INSERT - mEnabledCapabilities |= QgsVectorDataProvider::AddFeatures; - } - - if ( mSchemaName.isEmpty() ) - mSchemaName = testAccess.PQgetvalue( 0, 4 ); - + // TODO: merge this with the previous query sql = QString( "SELECT 1 FROM pg_class,pg_namespace WHERE " "pg_class.relnamespace=pg_namespace.oid AND " "%3 AND " diff --git a/tests/src/python/test_provider_postgres_latency.py b/tests/src/python/test_provider_postgres_latency.py index ce138c82601c5..707a24e62a090 100644 --- a/tests/src/python/test_provider_postgres_latency.py +++ b/tests/src/python/test_provider_postgres_latency.py @@ -156,7 +156,7 @@ def testProjectOpenTime(self): settings = QgsSettingsTree.node('core').childSetting('provider-parallel-loading') settings.setVariantValue(False) - davg = 8.67 + davg = 7.61 dmin = round(davg - 0.2, 2) dmax = round(davg + 0.3, 2) error_string = 'expected from {0}s to {1}s, got {2}s\nHINT: set davg={2} to pass the test :)' From 8882fd9aa18a14d79b685f0a4443b627868706f1 Mon Sep 17 00:00:00 2001 From: Alexander Bruy Date: Wed, 17 Jan 2024 14:46:16 +0200 Subject: [PATCH 078/102] duplicate composer map grid (fix #47511) --- .../layout/qgslayoutitemmapgrid.sip.in | 7 ++ .../layout/qgslayoutitemmapgrid.sip.in | 7 ++ src/core/layout/qgslayoutitemmapgrid.cpp | 78 ++++++++++++++++++- src/core/layout/qgslayoutitemmapgrid.h | 7 ++ src/gui/layout/qgslayoutmapwidget.cpp | 25 ++++++ src/gui/layout/qgslayoutmapwidget.h | 1 + src/ui/layout/qgslayoutmapwidgetbase.ui | 14 ++++ tests/src/python/test_qgslayoutmapgrid.py | 61 +++++++++++++++ 8 files changed, 197 insertions(+), 3 deletions(-) diff --git a/python/PyQt6/core/auto_generated/layout/qgslayoutitemmapgrid.sip.in b/python/PyQt6/core/auto_generated/layout/qgslayoutitemmapgrid.sip.in index 5fd12928fad70..815c4dbacb86c 100644 --- a/python/PyQt6/core/auto_generated/layout/qgslayoutitemmapgrid.sip.in +++ b/python/PyQt6/core/auto_generated/layout/qgslayoutitemmapgrid.sip.in @@ -1115,6 +1115,13 @@ Retrieves the second fill color for the grid frame. virtual void refresh(); + void copyProperties( const QgsLayoutItemMapGrid *other ); +%Docstring +Copies properties from specified map grid. + +.. versionadded:: 3.38 +%End + signals: void crsChanged(); diff --git a/python/core/auto_generated/layout/qgslayoutitemmapgrid.sip.in b/python/core/auto_generated/layout/qgslayoutitemmapgrid.sip.in index 02444b6b5eb1d..5e72cf12c8afe 100644 --- a/python/core/auto_generated/layout/qgslayoutitemmapgrid.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitemmapgrid.sip.in @@ -1115,6 +1115,13 @@ Retrieves the second fill color for the grid frame. virtual void refresh(); + void copyProperties( const QgsLayoutItemMapGrid *other ); +%Docstring +Copies properties from specified map grid. + +.. versionadded:: 3.38 +%End + signals: void crsChanged(); diff --git a/src/core/layout/qgslayoutitemmapgrid.cpp b/src/core/layout/qgslayoutitemmapgrid.cpp index 4e6b18d0d1f69..48b06834dec2e 100644 --- a/src/core/layout/qgslayoutitemmapgrid.cpp +++ b/src/core/layout/qgslayoutitemmapgrid.cpp @@ -1912,7 +1912,6 @@ bool QgsLayoutItemMapGrid::shouldShowForDisplayMode( QgsLayoutItemMapGrid::Annot || ( mode == QgsLayoutItemMapGrid::LongitudeOnly && coordinate == QgsLayoutItemMapGrid::Longitude ); } - QgsLayoutItemMapGrid::DisplayMode gridAnnotationDisplayModeFromDD( QString ddValue, QgsLayoutItemMapGrid::DisplayMode defValue ) { if ( ddValue.compare( QLatin1String( "x_only" ), Qt::CaseInsensitive ) == 0 ) @@ -1927,7 +1926,6 @@ QgsLayoutItemMapGrid::DisplayMode gridAnnotationDisplayModeFromDD( QString ddVal return defValue; } - void QgsLayoutItemMapGrid::refreshDataDefinedProperties() { const QgsExpressionContext context = createExpressionContext(); @@ -1986,7 +1984,6 @@ void QgsLayoutItemMapGrid::refreshDataDefinedProperties() mEvaluatedRightFrameDivisions = gridAnnotationDisplayModeFromDD( mDataDefinedProperties.valueAsString( QgsLayoutObject::DataDefinedProperty::MapGridFrameDivisionsRight, context ), mRightFrameDivisions ); mEvaluatedTopFrameDivisions = gridAnnotationDisplayModeFromDD( mDataDefinedProperties.valueAsString( QgsLayoutObject::DataDefinedProperty::MapGridFrameDivisionsTop, context ), mTopFrameDivisions ); mEvaluatedBottomFrameDivisions = gridAnnotationDisplayModeFromDD( mDataDefinedProperties.valueAsString( QgsLayoutObject::DataDefinedProperty::MapGridFrameDivisionsBottom, context ), mBottomFrameDivisions ); - } double QgsLayoutItemMapGrid::mapWidth() const @@ -2637,3 +2634,78 @@ QList QgsLayoutItemMapGrid::trimLinesToMap( const QPolygonF &line, co } return trimmedLines; } + +void QgsLayoutItemMapGrid::copyProperties( const QgsLayoutItemMapGrid *other ) +{ + // grid + setStyle( other->style() ); + setIntervalX( other->intervalX() ); + setIntervalY( other->intervalY() ); + setOffsetX( other->offsetX() ); + setOffsetY( other->offsetX() ); + setCrossLength( other->crossLength() ); + setFrameStyle( other->frameStyle() ); + setFrameSideFlags( other->frameSideFlags() ); + setFrameWidth( other->frameWidth() ); + setFrameMargin( other->frameMargin() ); + setFramePenSize( other->framePenSize() ); + setFramePenColor( other->framePenColor() ); + setFrameFillColor1( other->frameFillColor1() ); + setFrameFillColor2( other->frameFillColor2() ); + + setFrameDivisions( other->frameDivisions( QgsLayoutItemMapGrid::BorderSide::Left ), QgsLayoutItemMapGrid::BorderSide::Left ); + setFrameDivisions( other->frameDivisions( QgsLayoutItemMapGrid::BorderSide::Right ), QgsLayoutItemMapGrid::BorderSide::Right ); + setFrameDivisions( other->frameDivisions( QgsLayoutItemMapGrid::BorderSide::Bottom ), QgsLayoutItemMapGrid::BorderSide::Bottom ); + setFrameDivisions( other->frameDivisions( QgsLayoutItemMapGrid::BorderSide::Top ), QgsLayoutItemMapGrid::BorderSide::Top ); + + setRotatedTicksLengthMode( other->rotatedTicksLengthMode() ); + setRotatedTicksEnabled( other->rotatedTicksEnabled() ); + setRotatedTicksMinimumAngle( other->rotatedTicksMinimumAngle() ); + setRotatedTicksMarginToCorner( other->rotatedTicksMarginToCorner() ); + setRotatedAnnotationsLengthMode( other->rotatedAnnotationsLengthMode() ); + setRotatedAnnotationsEnabled( other->rotatedAnnotationsEnabled() ); + setRotatedAnnotationsMinimumAngle( other->rotatedAnnotationsMinimumAngle() ); + setRotatedAnnotationsMarginToCorner( other->rotatedAnnotationsMarginToCorner() ); + + if ( other->lineSymbol() ) + { + setLineSymbol( other->lineSymbol()->clone() ); + } + + if ( other->markerSymbol() ) + { + setMarkerSymbol( other->markerSymbol()->clone() ); + } + + setCrs( other->crs() ); + + setBlendMode( other->blendMode() ); + + //annotation + setAnnotationEnabled( other->annotationEnabled() ); + setAnnotationFormat( other->annotationFormat() ); + setAnnotationExpression( other->annotationExpression() ); + + setAnnotationPosition( other->annotationPosition( QgsLayoutItemMapGrid::BorderSide::Left ), QgsLayoutItemMapGrid::BorderSide::Left ); + setAnnotationPosition( other->annotationPosition( QgsLayoutItemMapGrid::BorderSide::Right ), QgsLayoutItemMapGrid::BorderSide::Right ); + setAnnotationPosition( other->annotationPosition( QgsLayoutItemMapGrid::BorderSide::Bottom ), QgsLayoutItemMapGrid::BorderSide::Bottom ); + setAnnotationPosition( other->annotationPosition( QgsLayoutItemMapGrid::BorderSide::Top ), QgsLayoutItemMapGrid::BorderSide::Top ); + setAnnotationDisplay( other->annotationDisplay( QgsLayoutItemMapGrid::BorderSide::Left ), QgsLayoutItemMapGrid::BorderSide::Left ); + setAnnotationDisplay( other->annotationDisplay( QgsLayoutItemMapGrid::BorderSide::Right ), QgsLayoutItemMapGrid::BorderSide::Right ); + setAnnotationDisplay( other->annotationDisplay( QgsLayoutItemMapGrid::BorderSide::Bottom ), QgsLayoutItemMapGrid::BorderSide::Bottom ); + setAnnotationDisplay( other->annotationDisplay( QgsLayoutItemMapGrid::BorderSide::Top ), QgsLayoutItemMapGrid::BorderSide::Top ); + setAnnotationDirection( other->annotationDirection( QgsLayoutItemMapGrid::BorderSide::Left ), QgsLayoutItemMapGrid::BorderSide::Left ); + setAnnotationDirection( other->annotationDirection( QgsLayoutItemMapGrid::BorderSide::Right ), QgsLayoutItemMapGrid::BorderSide::Right ); + setAnnotationDirection( other->annotationDirection( QgsLayoutItemMapGrid::BorderSide::Bottom ), QgsLayoutItemMapGrid::BorderSide::Bottom ); + setAnnotationDirection( other->annotationDirection( QgsLayoutItemMapGrid::BorderSide::Top ), QgsLayoutItemMapGrid::BorderSide::Top ); + setAnnotationFrameDistance( other->annotationFrameDistance() ); + setAnnotationTextFormat( other->annotationTextFormat() ); + + setAnnotationPrecision( other->annotationPrecision() ); + setUnits( other->units() ); + setMinimumIntervalWidth( other->minimumIntervalWidth() ); + setMaximumIntervalWidth( other->maximumIntervalWidth() ); + + setDataDefinedProperties( other->dataDefinedProperties() ); + refreshDataDefinedProperties(); +} diff --git a/src/core/layout/qgslayoutitemmapgrid.h b/src/core/layout/qgslayoutitemmapgrid.h index b2ee47cba0782..c3e9309e71e62 100644 --- a/src/core/layout/qgslayoutitemmapgrid.h +++ b/src/core/layout/qgslayoutitemmapgrid.h @@ -1005,6 +1005,13 @@ class CORE_EXPORT QgsLayoutItemMapGrid : public QgsLayoutItemMapItem bool accept( QgsStyleEntityVisitorInterface *visitor ) const override; void refresh() override; + /** + * Copies properties from specified map grid. + * + * \since QGIS 3.38 + */ + void copyProperties( const QgsLayoutItemMapGrid *other ); + signals: /** diff --git a/src/gui/layout/qgslayoutmapwidget.cpp b/src/gui/layout/qgslayoutmapwidget.cpp index 63e27642933e8..eb32d2ae23183 100644 --- a/src/gui/layout/qgslayoutmapwidget.cpp +++ b/src/gui/layout/qgslayoutmapwidget.cpp @@ -73,6 +73,7 @@ QgsLayoutMapWidget::QgsLayoutMapWidget( QgsLayoutItemMap *item, QgsMapCanvas *ma connect( mAtlasPredefinedScaleRadio, &QRadioButton::toggled, this, &QgsLayoutMapWidget::mAtlasPredefinedScaleRadio_toggled ); connect( mAddGridPushButton, &QPushButton::clicked, this, &QgsLayoutMapWidget::mAddGridPushButton_clicked ); connect( mRemoveGridPushButton, &QPushButton::clicked, this, &QgsLayoutMapWidget::mRemoveGridPushButton_clicked ); + connect( mCopyGridPushButton, &QPushButton::clicked, this, &QgsLayoutMapWidget::mCopyGridPushButton_clicked ); connect( mGridUpButton, &QPushButton::clicked, this, &QgsLayoutMapWidget::mGridUpButton_clicked ); connect( mGridDownButton, &QPushButton::clicked, this, &QgsLayoutMapWidget::mGridDownButton_clicked ); connect( mGridListWidget, &QListWidget::currentItemChanged, this, &QgsLayoutMapWidget::mGridListWidget_currentItemChanged ); @@ -1245,6 +1246,30 @@ void QgsLayoutMapWidget::mRemoveGridPushButton_clicked() mMapItem->update(); } +void QgsLayoutMapWidget::mCopyGridPushButton_clicked() +{ + QListWidgetItem *item = mGridListWidget->currentItem(); + if ( !item ) + { + return; + } + + QgsLayoutItemMapGrid *sourceGrid = mMapItem->grids()->grid( item->data( Qt::UserRole ).toString() ); + const QString itemName = tr( "Grid %1" ).arg( mMapItem->grids()->size() + 1 ); + QgsLayoutItemMapGrid *grid = new QgsLayoutItemMapGrid( itemName, mMapItem ); + grid->copyProperties( sourceGrid ); + + mMapItem->layout()->undoStack()->beginCommand( mMapItem, tr( "Duplicate Map Grid" ) ); + mMapItem->grids()->addGrid( grid ); + mMapItem->layout()->undoStack()->endCommand(); + mMapItem->updateBoundingRect(); + mMapItem->update(); + + addGridListItem( grid->id(), grid->name() ); + mGridListWidget->setCurrentRow( 0 ); + mGridListWidget_currentItemChanged( mGridListWidget->currentItem(), nullptr ); +} + void QgsLayoutMapWidget::mGridUpButton_clicked() { QListWidgetItem *item = mGridListWidget->currentItem(); diff --git a/src/gui/layout/qgslayoutmapwidget.h b/src/gui/layout/qgslayoutmapwidget.h index 83a56c86f9012..3ccab9b61aaa3 100644 --- a/src/gui/layout/qgslayoutmapwidget.h +++ b/src/gui/layout/qgslayoutmapwidget.h @@ -86,6 +86,7 @@ class GUI_EXPORT QgsLayoutMapWidget: public QgsLayoutItemBaseWidget, private Ui: void mAddGridPushButton_clicked(); void mRemoveGridPushButton_clicked(); + void mCopyGridPushButton_clicked(); void mGridUpButton_clicked(); void mGridDownButton_clicked(); diff --git a/src/ui/layout/qgslayoutmapwidgetbase.ui b/src/ui/layout/qgslayoutmapwidgetbase.ui index 5a667e478743d..31281d3f299c5 100644 --- a/src/ui/layout/qgslayoutmapwidgetbase.ui +++ b/src/ui/layout/qgslayoutmapwidgetbase.ui @@ -752,6 +752,20 @@
+ + + + Duplicate selected grid + + + + + + + :/images/themes/default/mActionEditCopy.svg:/images/themes/default/mActionEditCopy.svg + + + diff --git a/tests/src/python/test_qgslayoutmapgrid.py b/tests/src/python/test_qgslayoutmapgrid.py index 13acb4e1dbb94..9b7bea23d7342 100644 --- a/tests/src/python/test_qgslayoutmapgrid.py +++ b/tests/src/python/test_qgslayoutmapgrid.py @@ -1097,6 +1097,67 @@ def testCrsChanged(self): grid.setCrs(QgsCoordinateReferenceSystem("EPSG:3111")) self.assertEqual(len(spy), 9) + def testCopyGrid(self): + layout = QgsLayout(QgsProject.instance()) + layout.initializeDefaults() + map = QgsLayoutItemMap(layout) + map.attemptSetSceneRect(QRectF(20, 20, 200, 100)) + map.setFrameEnabled(True) + map.setBackgroundColor(QColor(150, 100, 100)) + layout.addLayoutItem(map) + myRectangle = QgsRectangle(781662.375, 3339523.125, 793062.375, 3345223.125) + map.setExtent(myRectangle) + map.grid().setEnabled(True) + map.grid().setIntervalX(2000) + map.grid().setIntervalY(2000) + map.grid().setAnnotationEnabled(True) + map.grid().setGridLineColor(QColor(0, 255, 0)) + map.grid().setGridLineWidth(0.5) + + format = QgsTextFormat.fromQFont(getTestFont("Bold", 20)) + format.setColor(QColor(255, 0, 0)) + format.setOpacity(150 / 255) + map.grid().setAnnotationTextFormat(format) + + map.grid().setAnnotationPrecision(0) + map.grid().setAnnotationDisplay( + QgsLayoutItemMapGrid.DisplayMode.HideAll, QgsLayoutItemMapGrid.BorderSide.Left + ) + map.grid().setAnnotationPosition( + QgsLayoutItemMapGrid.AnnotationPosition.OutsideMapFrame, QgsLayoutItemMapGrid.BorderSide.Right + ) + map.grid().setAnnotationDisplay( + QgsLayoutItemMapGrid.DisplayMode.HideAll, QgsLayoutItemMapGrid.BorderSide.Top + ) + map.grid().setAnnotationPosition( + QgsLayoutItemMapGrid.AnnotationPosition.OutsideMapFrame, QgsLayoutItemMapGrid.BorderSide.Bottom + ) + map.grid().setAnnotationDirection( + QgsLayoutItemMapGrid.AnnotationDirection.Horizontal, QgsLayoutItemMapGrid.BorderSide.Right + ) + map.grid().setAnnotationDirection( + QgsLayoutItemMapGrid.AnnotationDirection.Horizontal, QgsLayoutItemMapGrid.BorderSide.Bottom + ) + map.grid().setBlendMode(QPainter.CompositionMode.CompositionMode_Overlay) + map.updateBoundingRect() + + map.grid().dataDefinedProperties().setProperty( + QgsLayoutObject.DataDefinedProperty.MapGridLabelDistance, QgsProperty.fromValue(10) + ) + map.grid().refresh() + + source_grid = map.grid() + grid = QgsLayoutItemMapGrid("testGrid", map) + grid.copyProperties(source_grid) + map.grids().removeGrid(source_grid.id()) + self.assertEqual(map.grids().size(), 0) + map.grids().addGrid(grid) + map.grid().refresh() + self.assertTrue( + self.render_layout_check("composermap_datadefined_annotationdistance", + layout) + ) + if __name__ == "__main__": unittest.main() From 1a5e52866099f1b3c549cd1561735f0fcfac86b3 Mon Sep 17 00:00:00 2001 From: Alexander Bruy Date: Tue, 6 Feb 2024 17:26:29 +0200 Subject: [PATCH 079/102] better names for grid copies --- src/gui/layout/qgslayoutmapwidget.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/gui/layout/qgslayoutmapwidget.cpp b/src/gui/layout/qgslayoutmapwidget.cpp index eb32d2ae23183..c098f4f5bdfce 100644 --- a/src/gui/layout/qgslayoutmapwidget.cpp +++ b/src/gui/layout/qgslayoutmapwidget.cpp @@ -1255,7 +1255,23 @@ void QgsLayoutMapWidget::mCopyGridPushButton_clicked() } QgsLayoutItemMapGrid *sourceGrid = mMapItem->grids()->grid( item->data( Qt::UserRole ).toString() ); - const QString itemName = tr( "Grid %1" ).arg( mMapItem->grids()->size() + 1 ); + int i = 0; + bool found = true; + QString itemName = tr( "%1 - Copy" ).arg( sourceGrid->name() ); + QList< QgsLayoutItemMapGrid * > grids = mMapItem->grids()->asList(); + while ( found ) + { + for ( const QgsLayoutItemMapGrid *grd : std::as_const( grids ) ) + { + if ( grd->name() == itemName ) + { + i++; + itemName = tr( "%1 - Copy %2" ).arg( sourceGrid->name() ).arg( i ); + } + } + found = false; + } + //const QString itemName = tr( "Grid %1" ).arg( mMapItem->grids()->size() + 1 ); QgsLayoutItemMapGrid *grid = new QgsLayoutItemMapGrid( itemName, mMapItem ); grid->copyProperties( sourceGrid ); From 1106a17ac2eeaefc691045dbebeb3b008bbd7781 Mon Sep 17 00:00:00 2001 From: Alexander Bruy Date: Wed, 7 Feb 2024 11:10:49 +0200 Subject: [PATCH 080/102] better implementation for name change --- src/gui/layout/qgslayoutmapwidget.cpp | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/gui/layout/qgslayoutmapwidget.cpp b/src/gui/layout/qgslayoutmapwidget.cpp index c098f4f5bdfce..f9495872a46f2 100644 --- a/src/gui/layout/qgslayoutmapwidget.cpp +++ b/src/gui/layout/qgslayoutmapwidget.cpp @@ -1256,22 +1256,19 @@ void QgsLayoutMapWidget::mCopyGridPushButton_clicked() QgsLayoutItemMapGrid *sourceGrid = mMapItem->grids()->grid( item->data( Qt::UserRole ).toString() ); int i = 0; - bool found = true; QString itemName = tr( "%1 - Copy" ).arg( sourceGrid->name() ); QList< QgsLayoutItemMapGrid * > grids = mMapItem->grids()->asList(); - while ( found ) + while ( true ) { - for ( const QgsLayoutItemMapGrid *grd : std::as_const( grids ) ) + const auto it = std::find_if( grids.begin(), grids.end(), [&itemName]( const QgsLayoutItemMapGrid * grd ) { return grd->name() == itemName; } ); + if ( it != grids.end() ) { - if ( grd->name() == itemName ) - { - i++; - itemName = tr( "%1 - Copy %2" ).arg( sourceGrid->name() ).arg( i ); - } + i++; + itemName = tr( "%1 - Copy %2" ).arg( sourceGrid->name() ).arg( i ); + continue; } - found = false; + break; } - //const QString itemName = tr( "Grid %1" ).arg( mMapItem->grids()->size() + 1 ); QgsLayoutItemMapGrid *grid = new QgsLayoutItemMapGrid( itemName, mMapItem ); grid->copyProperties( sourceGrid ); From 2860f23fa3b000da7c775f01190e2fd586498d7f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 24 Apr 2024 08:58:00 +1000 Subject: [PATCH 081/102] Add QgsLineString method to interpolate nan m values along line Fills in any nan m values by interpolating from non-nan values in neighbouring vertices --- .../geometry/qgslinestring.sip.in | 19 +- .../geometry/qgslinestring.sip.in | 19 +- src/core/geometry/qgslinestring.cpp | 173 ++++++++++++++++++ src/core/geometry/qgslinestring.h | 23 ++- tests/src/python/test_qgslinestring.py | 108 ++++++++++- 5 files changed, 328 insertions(+), 14 deletions(-) diff --git a/python/PyQt6/core/auto_generated/geometry/qgslinestring.sip.in b/python/PyQt6/core/auto_generated/geometry/qgslinestring.sip.in index 8337f9e16d127..61ce1397087c7 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgslinestring.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgslinestring.sip.in @@ -369,7 +369,7 @@ corresponds to the last point in the line. %Docstring Returns the z-coordinate of the specified node in the line string. -If the LineString does not have a z-dimension then ``nan`` will be returned. +If the LineString does not have a z-dimension then ``NaN`` will be returned. Indexes can be less than 0, in which case they correspond to positions from the end of the line. E.g. an index of -1 corresponds to the last point in the line. @@ -398,7 +398,7 @@ corresponds to the last point in the line. %Docstring Returns the m-coordinate of the specified node in the line string. -If the LineString does not have a m-dimension then ``nan`` will be returned. +If the LineString does not have a m-dimension then ``NaN`` will be returned. Indexes can be less than 0, in which case they correspond to positions from the end of the line. E.g. an index of -1 corresponds to the last point in the line. @@ -863,13 +863,26 @@ Calculates the minimal 3D bounding box for the geometry. .. versionadded:: 3.34 %End - QgsLineString *measuredLine( double start, double end ) const /Factory/; %Docstring Re-write the measure ordinate (or add one, if it isn't already there) interpolating the measure between the supplied ``start`` and ``end`` values. .. versionadded:: 3.36 +%End + + QgsLineString *interpolateM( bool use3DDistance = true ) const /Factory/; +%Docstring +Returns a copy of this line with all missing (NaN) m values interpolated +from m values of surrounding vertices. + +If the line does not contain m values, ``None`` is returned. + +The ``use3DDistance`` controls whether 2D or 3D distances between vertices +should be used during interpolation. This option is only considered for lines +with z values. + +.. versionadded:: 3.38 %End protected: diff --git a/python/core/auto_generated/geometry/qgslinestring.sip.in b/python/core/auto_generated/geometry/qgslinestring.sip.in index 8337f9e16d127..61ce1397087c7 100644 --- a/python/core/auto_generated/geometry/qgslinestring.sip.in +++ b/python/core/auto_generated/geometry/qgslinestring.sip.in @@ -369,7 +369,7 @@ corresponds to the last point in the line. %Docstring Returns the z-coordinate of the specified node in the line string. -If the LineString does not have a z-dimension then ``nan`` will be returned. +If the LineString does not have a z-dimension then ``NaN`` will be returned. Indexes can be less than 0, in which case they correspond to positions from the end of the line. E.g. an index of -1 corresponds to the last point in the line. @@ -398,7 +398,7 @@ corresponds to the last point in the line. %Docstring Returns the m-coordinate of the specified node in the line string. -If the LineString does not have a m-dimension then ``nan`` will be returned. +If the LineString does not have a m-dimension then ``NaN`` will be returned. Indexes can be less than 0, in which case they correspond to positions from the end of the line. E.g. an index of -1 corresponds to the last point in the line. @@ -863,13 +863,26 @@ Calculates the minimal 3D bounding box for the geometry. .. versionadded:: 3.34 %End - QgsLineString *measuredLine( double start, double end ) const /Factory/; %Docstring Re-write the measure ordinate (or add one, if it isn't already there) interpolating the measure between the supplied ``start`` and ``end`` values. .. versionadded:: 3.36 +%End + + QgsLineString *interpolateM( bool use3DDistance = true ) const /Factory/; +%Docstring +Returns a copy of this line with all missing (NaN) m values interpolated +from m values of surrounding vertices. + +If the line does not contain m values, ``None`` is returned. + +The ``use3DDistance`` controls whether 2D or 3D distances between vertices +should be used during interpolation. This option is only considered for lines +with z values. + +.. versionadded:: 3.38 %End protected: diff --git a/src/core/geometry/qgslinestring.cpp b/src/core/geometry/qgslinestring.cpp index 2a27cc66adb27..64e8a3c69d5bc 100644 --- a/src/core/geometry/qgslinestring.cpp +++ b/src/core/geometry/qgslinestring.cpp @@ -2387,3 +2387,176 @@ QgsLineString *QgsLineString::measuredLine( double start, double end ) const return cloned.release(); } + +QgsLineString *QgsLineString::interpolateM( bool use3DDistance ) const +{ + if ( !isMeasure() ) + return nullptr; + + const int totalPoints = numPoints(); + if ( totalPoints < 2 ) + return clone(); + + const double *xData = mX.constData(); + const double *yData = mY.constData(); + const double *mData = mM.constData(); + const double *zData = is3D() ? mZ.constData() : nullptr; + use3DDistance &= is3D(); + + QVector< double > xOut( totalPoints ); + QVector< double > yOut( totalPoints ); + QVector< double > mOut( totalPoints ); + QVector< double > zOut( is3D() ? totalPoints : 0 ); + + double *xOutData = xOut.data(); + double *yOutData = yOut.data(); + double *mOutData = mOut.data(); + double *zOutData = is3D() ? zOut.data() : nullptr; + + int i = 0; + double currentSegmentLength = 0; + double lastValidM = std::numeric_limits< double >::quiet_NaN(); + double prevX = *xData; + double prevY = *yData; + double prevZ = zData ? *zData : 0; + while ( i < totalPoints ) + { + double thisX = *xData++; + double thisY = *yData++; + double thisZ = zData ? *zData++ : 0; + double thisM = *mData++; + + currentSegmentLength = use3DDistance + ? QgsGeometryUtilsBase::distance3D( prevX, prevY, prevZ, thisX, thisY, thisZ ) + : QgsGeometryUtilsBase::distance2D( prevX, prevY, thisX, thisY ); + + if ( !std::isnan( thisM ) ) + { + *xOutData++ = thisX; + *yOutData++ = thisY; + *mOutData++ = thisM; + if ( zOutData ) + *zOutData++ = thisZ; + lastValidM = thisM; + } + else if ( i == 0 ) + { + // nan m value at start of line, read ahead to find first non-nan value and backfill + int j = 0; + double scanAheadM = thisM; + while ( i + j + 1 < totalPoints && std::isnan( scanAheadM ) ) + { + scanAheadM = mData[ j ]; + ++j; + } + if ( std::isnan( scanAheadM ) ) + { + // no valid m values in line + return nullptr; + } + *xOutData++ = thisX; + *yOutData++ = thisY; + *mOutData++ = scanAheadM; + if ( zOutData ) + *zOutData++ = thisZ; + for ( ; i < j; ++i ) + { + thisX = *xData++; + thisY = *yData++; + *xOutData++ = thisX; + *yOutData++ = thisY; + *mOutData++ = scanAheadM; + mData++; + if ( zOutData ) + *zOutData++ = *zData++; + } + lastValidM = scanAheadM; + } + else + { + // nan m value in middle of line, read ahead till next non-nan value and interpolate + int j = 0; + double scanAheadX = thisX; + double scanAheadY = thisY; + double scanAheadZ = thisZ; + double distanceToNextValidM = currentSegmentLength; + std::vector< double > scanAheadSegmentLengths; + scanAheadSegmentLengths.emplace_back( currentSegmentLength ); + + double nextValidM = std::numeric_limits< double >::quiet_NaN(); + while ( i + j < totalPoints - 1 ) + { + double nextScanAheadX = xData[j]; + double nextScanAheadY = yData[j]; + double nextScanAheadZ = zData ? zData[j] : 0; + double nextScanAheadM = mData[ j ]; + const double scanAheadSegmentLength = use3DDistance + ? QgsGeometryUtilsBase::distance3D( scanAheadX, scanAheadY, scanAheadZ, nextScanAheadX, nextScanAheadY, nextScanAheadZ ) + : QgsGeometryUtilsBase::distance2D( scanAheadX, scanAheadY, nextScanAheadX, nextScanAheadY ); + scanAheadSegmentLengths.emplace_back( scanAheadSegmentLength ); + distanceToNextValidM += scanAheadSegmentLength; + + if ( !std::isnan( nextScanAheadM ) ) + { + nextValidM = nextScanAheadM; + break; + } + + scanAheadX = nextScanAheadX; + scanAheadY = nextScanAheadY; + scanAheadZ = nextScanAheadZ; + ++j; + } + + if ( std::isnan( nextValidM ) ) + { + // no more valid m values, so just fill remainder of vertices with previous valid m value + *xOutData++ = thisX; + *yOutData++ = thisY; + *mOutData++ = lastValidM; + if ( zOutData ) + *zOutData++ = thisZ; + ++i; + for ( ; i < totalPoints; ++i ) + { + *xOutData++ = *xData++; + *yOutData++ = *yData++; + *mOutData++ = lastValidM; + if ( zOutData ) + *zOutData++ = *zData++; + } + break; + } + else + { + // interpolate along segments + const double delta = ( nextValidM - lastValidM ) / distanceToNextValidM; + *xOutData++ = thisX; + *yOutData++ = thisY; + *mOutData++ = lastValidM + delta * scanAheadSegmentLengths[0]; + double totalScanAheadLength = scanAheadSegmentLengths[0]; + if ( zOutData ) + *zOutData++ = thisZ; + for ( int k = 1; k <= j; ++i, ++k ) + { + thisX = *xData++; + thisY = *yData++; + *xOutData++ = thisX; + *yOutData++ = thisY; + totalScanAheadLength += scanAheadSegmentLengths[k]; + *mOutData++ = lastValidM + delta * totalScanAheadLength; + mData++; + if ( zOutData ) + *zOutData++ = *zData++; + } + lastValidM = nextValidM; + } + } + + prevX = thisX; + prevY = thisY; + prevZ = thisZ; + ++i; + } + return new QgsLineString( xOut, yOut, zOut, mOut ); +} diff --git a/src/core/geometry/qgslinestring.h b/src/core/geometry/qgslinestring.h index 27ba2c973fd43..0079a33a3f6d7 100644 --- a/src/core/geometry/qgslinestring.h +++ b/src/core/geometry/qgslinestring.h @@ -626,7 +626,7 @@ class CORE_EXPORT QgsLineString: public QgsCurve /** * Returns the z-coordinate of the specified node in the line string. * \param index index of node, where the first node in the line is 0 - * \returns z-coordinate of node, or ``nan`` if index is out of bounds or the line + * \returns z-coordinate of node, or ``NaN`` if index is out of bounds or the line * does not have a z dimension * \see setZAt() */ @@ -642,7 +642,7 @@ class CORE_EXPORT QgsLineString: public QgsCurve /** * Returns the z-coordinate of the specified node in the line string. * - * If the LineString does not have a z-dimension then ``nan`` will be returned. + * If the LineString does not have a z-dimension then ``NaN`` will be returned. * * Indexes can be less than 0, in which case they correspond to positions from the end of the line. E.g. an index of -1 * corresponds to the last point in the line. @@ -672,7 +672,7 @@ class CORE_EXPORT QgsLineString: public QgsCurve /** * Returns the m value of the specified node in the line string. * \param index index of node, where the first node in the line is 0 - * \returns m value of node, or ``nan`` if index is out of bounds or the line + * \returns m value of node, or ``NaN`` if index is out of bounds or the line * does not have m values * \see setMAt() */ @@ -688,7 +688,7 @@ class CORE_EXPORT QgsLineString: public QgsCurve /** * Returns the m-coordinate of the specified node in the line string. * - * If the LineString does not have a m-dimension then ``nan`` will be returned. + * If the LineString does not have a m-dimension then ``NaN`` will be returned. * * Indexes can be less than 0, in which case they correspond to positions from the end of the line. E.g. an index of -1 * corresponds to the last point in the line. @@ -1187,7 +1187,6 @@ class CORE_EXPORT QgsLineString: public QgsCurve */ QgsBox3D calculateBoundingBox3D() const override; - /** * Re-write the measure ordinate (or add one, if it isn't already there) interpolating * the measure between the supplied \a start and \a end values. @@ -1196,6 +1195,20 @@ class CORE_EXPORT QgsLineString: public QgsCurve */ QgsLineString *measuredLine( double start, double end ) const SIP_FACTORY; + /** + * Returns a copy of this line with all missing (NaN) m values interpolated + * from m values of surrounding vertices. + * + * If the line does not contain m values, NULLPTR is returned. + * + * The \a use3DDistance controls whether 2D or 3D distances between vertices + * should be used during interpolation. This option is only considered for lines + * with z values. + * + * \since QGIS 3.38 + */ + QgsLineString *interpolateM( bool use3DDistance = true ) const SIP_FACTORY; + protected: int compareToSameClass( const QgsAbstractGeometry *other ) const final; diff --git a/tests/src/python/test_qgslinestring.py b/tests/src/python/test_qgslinestring.py index 0191088a6e709..e8373c5d74322 100644 --- a/tests/src/python/test_qgslinestring.py +++ b/tests/src/python/test_qgslinestring.py @@ -9,10 +9,10 @@ __date__ = '12/09/2023' __copyright__ = 'Copyright 2023, The QGIS Project' -import qgis # NOQA - -from qgis.core import QgsLineString, QgsPoint import unittest +import math + +from qgis.core import Qgis, QgsLineString, QgsPoint from qgis.testing import start_app, QgisTestCase start_app() @@ -117,6 +117,108 @@ def testFuzzyComparisons(self): self.assertTrue(geom1.fuzzyEqual(geom2, epsilon)) self.assertTrue(geom1.fuzzyDistanceEqual(geom2, epsilon)) + def testInterpolateM(self): + line = QgsLineString() + + # empty line + self.assertIsNone(line.interpolateM()) + + # not m + line.fromWkt('LineString (10 6, 20 6)') + self.assertIsNone(line.interpolateM()) + + # single point + line.fromWkt('LineStringM (10 6 0)') + self.assertEqual(line.interpolateM().asWkt(), 'LineStringM (10 6 0)') + + # valid cases + line.fromWkt('LineStringM (10 6 0, 20 6 10)') + self.assertEqual(line.interpolateM().asWkt(), 'LineStringM (10 6 0, 20 6 10)') + + line.fromWkt('LineStringM (10 6 1, 20 6 0, 10 10 1)') + self.assertEqual(line.interpolateM().asWkt(), 'LineStringM (10 6 1, 20 6 0, 10 10 1)') + + line.fromWkt('LineStringZM (10 6 1 5, 20 6 0 6, 10 10 1 7)') + self.assertEqual(line.interpolateM().asWkt(), 'LineStringZM (10 6 1 5, 20 6 0 6, 10 10 1 7)') + + # no valid m values + line = QgsLineString([[10, 6, 1, math.nan], [20, 6, 2, math.nan]]) + self.assertEqual(line.wkbType(), Qgis.WkbType.LineStringZM) + self.assertIsNone(line.interpolateM()) + + # missing m values at start of line + line = QgsLineString([[10, 6, 1, math.nan], [20, 6, 2, 13], [20, 10, 5, 17]]) + self.assertEqual(line.interpolateM().asWkt(), 'LineStringZM (10 6 1 13, 20 6 2 13, 20 10 5 17)') + + line = QgsLineString([[10, 6, 1, math.nan], [20, 6, 2, math.nan], [20, 10, 5, 17]]) + self.assertEqual(line.interpolateM().asWkt(), 'LineStringZM (10 6 1 17, 20 6 2 17, 20 10 5 17)') + + line = QgsLineString([[10, 6, 1, math.nan], [20, 6, 2, math.nan], [20, 10, 5, 17], [22, 10, 15, 19]]) + self.assertEqual(line.interpolateM().asWkt(), 'LineStringZM (10 6 1 17, 20 6 2 17, 20 10 5 17, 22 10 15 19)') + + # missing m values at end of line + line = QgsLineString([[20, 6, 2, 13], [20, 10, 5, 17], [10, 6, 1, math.nan]]) + self.assertEqual(line.interpolateM().asWkt(), 'LineStringZM (20 6 2 13, 20 10 5 17, 10 6 1 17)') + + line = QgsLineString([[20, 10, 5, 17], [10, 6, 1, math.nan], [20, 6, 2, math.nan]]) + self.assertEqual(line.interpolateM().asWkt(), 'LineStringZM (20 10 5 17, 10 6 1 17, 20 6 2 17)') + + line = QgsLineString([[20, 10, 5, 17], [22, 10, 15, 19], [10, 6, 1, math.nan], [20, 6, 2, math.nan]]) + self.assertEqual(line.interpolateM().asWkt(), 'LineStringZM (20 10 5 17, 22 10 15 19, 10 6 1 19, 20 6 2 19)') + + # missing m values in middle of line + line = QgsLineString([[20, 10, 5, 17], [30, 10, 12, math.nan], [30, 40, 17, 27]]) + # 2d distance + self.assertEqual(line.interpolateM(False).asWkt(), 'LineStringZM (20 10 5 17, 30 10 12 19.5, 30 40 17 27)') + # 3d distance + self.assertEqual(line.interpolateM(True).asWkt(2), 'LineStringZM (20 10 5 17, 30 10 12 19.86, 30 40 17 27)') + + line = QgsLineString([[20, 10, 5, 17], [30, 10, 12, math.nan], [30, 40, 17, math.nan], [20, 40, 19, 27]]) + # 2d distance + self.assertEqual(line.interpolateM(False).asWkt(), 'LineStringZM (20 10 5 17, 30 10 12 19, 30 40 17 25, 20 40 19 27)') + # 3d distance + self.assertEqual(line.interpolateM(True).asWkt(2), 'LineStringZM (20 10 5 17, 30 10 12 19.31, 30 40 17 25.07, 20 40 19 27)') + + line = QgsLineString([[20, 10, 5, 17], [30, 10, 12, math.nan], [30, 40, 17, math.nan], [20, 40, 19, math.nan], [20, 50, 21, 29]]) + # 2d distance + self.assertEqual(line.interpolateM(False).asWkt(), 'LineStringZM (20 10 5 17, 30 10 12 19, 30 40 17 25, 20 40 19 27, 20 50 21 29)') + # 3d distance + self.assertEqual(line.interpolateM(True).asWkt(2), 'LineStringZM (20 10 5 17, 30 10 12 19.32, 30 40 17 25.12, 20 40 19 27.06, 20 50 21 29)') + + # multiple missing chunks + line = QgsLineString([[20, 10, 5, 17], [30, 10, 12, math.nan], [30, 40, 17, math.nan], [20, 40, 19, 27], [20, 50, 21, math.nan], [25, 50, 22, 30]]) + # 2d distance + self.assertEqual(line.interpolateM(False).asWkt(), 'LineStringZM (20 10 5 17, 30 10 12 19, 30 40 17 25, 20 40 19 27, 20 50 21 29, 25 50 22 30)') + # 3d distance + self.assertEqual(line.interpolateM(True).asWkt(2), 'LineStringZM (20 10 5 17, 30 10 12 19.31, 30 40 17 25.07, 20 40 19 27, 20 50 21 29, 25 50 22 30)') + + line = QgsLineString([[20, 10, 5, 17], [30, 10, 12, math.nan], [30, 40, 17, math.nan], [20, 40, 19, 27], [20, 50, 21, math.nan], [25, 50, 22, math.nan], [25, 55, 22, math.nan], [30, 55, 22, 37]]) + # 2d distance + self.assertEqual(line.interpolateM(False).asWkt(), 'LineStringZM (20 10 5 17, 30 10 12 19, 30 40 17 25, 20 40 19 27, 20 50 21 31, 25 50 22 33, 25 55 22 35, 30 55 22 37)') + # 3d distance + self.assertEqual(line.interpolateM(True).asWkt(2), 'LineStringZM (20 10 5 17, 30 10 12 19.31, 30 40 17 25.07, 20 40 19 27, 20 50 21 31.03, 25 50 22 33.05, 25 55 22 35.02, 30 55 22 37)') + + # missing at start and middle + line = QgsLineString([[10, 10, 1, math.nan],[10, 12, 2, math.nan], [20, 10, 5, 17], [30, 10, 12, math.nan], [30, 40, 17, math.nan], [20, 40, 19, math.nan], [20, 50, 21, 29]]) + # 2d distance + self.assertEqual(line.interpolateM(False).asWkt(), 'LineStringZM (10 10 1 17, 10 12 2 17, 20 10 5 17, 30 10 12 19, 30 40 17 25, 20 40 19 27, 20 50 21 29)') + # 3d distance + self.assertEqual(line.interpolateM(True).asWkt(2), 'LineStringZM (10 10 1 17, 10 12 2 17, 20 10 5 17, 30 10 12 19.72, 30 40 17 25.27, 20 40 19 27.14, 20 50 21 29)') + + # missing at middle and end + line = QgsLineString([[20, 10, 5, 17], [30, 10, 12, math.nan], [30, 40, 17, math.nan], [20, 40, 19, 27], [20, 50, 21, math.nan], [25, 50, 22, math.nan], [25, 55, 22, math.nan]]) + # 2d distance + self.assertEqual(line.interpolateM(False).asWkt(), 'LineStringZM (20 10 5 17, 30 10 12 19, 30 40 17 25, 20 40 19 27, 20 50 21 27, 25 50 22 27, 25 55 22 27)') + # 3d distance + self.assertEqual(line.interpolateM(True).asWkt(2), 'LineStringZM (20 10 5 17, 30 10 12 19.31, 30 40 17 25.07, 20 40 19 27, 20 50 21 27, 25 50 22 27, 25 55 22 27)') + + # missing at start, middle, end + line = QgsLineString([[5, 10, 15, math.nan], [6, 11, 16, math.nan], [20, 10, 5, 17], [30, 10, 12, math.nan], [30, 40, 17, math.nan], [20, 40, 19, 27], [20, 50, 21, math.nan], [25, 50, 22, math.nan], [25, 55, 22, math.nan]]) + # 2d distance + self.assertEqual(line.interpolateM(False).asWkt(), 'LineStringZM (5 10 15 17, 6 11 16 17, 20 10 5 17, 30 10 12 19, 30 40 17 25, 20 40 19 27, 20 50 21 27, 25 50 22 27, 25 55 22 27)') + # 3d distance + self.assertEqual(line.interpolateM(True).asWkt(2), 'LineStringZM (5 10 15 17, 6 11 16 17, 20 10 5 17, 30 10 12 19.05, 30 40 17 25, 20 40 19 27, 20 50 21 27, 25 50 22 27, 25 55 22 27)') + if __name__ == '__main__': unittest.main() From cb8cf14333b4c6ddae83d7391e3043f058162eb3 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 10 May 2024 13:01:04 +1000 Subject: [PATCH 082/102] Indentation --- tests/src/python/test_qgslinestring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/python/test_qgslinestring.py b/tests/src/python/test_qgslinestring.py index e8373c5d74322..86bb1b2c8d7a5 100644 --- a/tests/src/python/test_qgslinestring.py +++ b/tests/src/python/test_qgslinestring.py @@ -199,7 +199,7 @@ def testInterpolateM(self): self.assertEqual(line.interpolateM(True).asWkt(2), 'LineStringZM (20 10 5 17, 30 10 12 19.31, 30 40 17 25.07, 20 40 19 27, 20 50 21 31.03, 25 50 22 33.05, 25 55 22 35.02, 30 55 22 37)') # missing at start and middle - line = QgsLineString([[10, 10, 1, math.nan],[10, 12, 2, math.nan], [20, 10, 5, 17], [30, 10, 12, math.nan], [30, 40, 17, math.nan], [20, 40, 19, math.nan], [20, 50, 21, 29]]) + line = QgsLineString([[10, 10, 1, math.nan], [10, 12, 2, math.nan], [20, 10, 5, 17], [30, 10, 12, math.nan], [30, 40, 17, math.nan], [20, 40, 19, math.nan], [20, 50, 21, 29]]) # 2d distance self.assertEqual(line.interpolateM(False).asWkt(), 'LineStringZM (10 10 1 17, 10 12 2 17, 20 10 5 17, 30 10 12 19, 30 40 17 25, 20 40 19 27, 20 50 21 29)') # 3d distance From 483d01be368a01073b6bdf27e8ea7079e7a5dbec Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Fri, 10 May 2024 17:18:07 +0700 Subject: [PATCH 083/102] [qml] Make QgsGeometry::asWkt and QgsGeometry::fromWkt invokable --- src/core/geometry/qgsgeometry.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/geometry/qgsgeometry.h b/src/core/geometry/qgsgeometry.h index ecd16457f87ce..8a38db03cc3fd 100644 --- a/src/core/geometry/qgsgeometry.h +++ b/src/core/geometry/qgsgeometry.h @@ -241,7 +241,7 @@ class CORE_EXPORT QgsGeometry bool isNull() const SIP_HOLDGIL; //! Creates a new geometry from a WKT string - static QgsGeometry fromWkt( const QString &wkt ); + Q_INVOKABLE static QgsGeometry fromWkt( const QString &wkt ); //! Creates a new geometry from a QgsPointXY object static QgsGeometry fromPointXY( const QgsPointXY &point ) SIP_HOLDGIL; @@ -2132,7 +2132,7 @@ class CORE_EXPORT QgsGeometry * \returns TRUE in case of success and FALSE else * \note precision parameter added in QGIS 2.4 */ - QString asWkt( int precision = 17 ) const; + Q_INVOKABLE QString asWkt( int precision = 17 ) const; #ifdef SIP_RUN SIP_PYOBJECT __repr__(); From 60308ac26b2d89cf2c460d01c5131fd7db8beb80 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 9 May 2024 08:59:35 +1000 Subject: [PATCH 084/102] Don't create dummy folders in browser model when changing data source of a layer with a source pointing to an existing file --- src/app/qgisapp.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 450dbaea75ce6..18295736e3e33 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -7753,7 +7753,9 @@ void QgisApp::changeDataSource( QgsMapLayer *layer ) { const QString path = sourceParts.value( QStringLiteral( "path" ) ).toString(); const QString closestPath = QFile::exists( path ) ? path : QgsFileUtils::findClosestExistingPath( path ); - dlg.expandPath( closestPath ); + + const QFileInfo pathInfo( closestPath ); + dlg.expandPath( pathInfo.isDir() ? closestPath : pathInfo.dir().path() ); if ( source.contains( path ) ) { source.replace( path, QStringLiteral( "%2" ).arg( QUrl::fromLocalFile( closestPath ).toString(), From 54244d50bec2a12aac8bdef749f0894f80f185ab Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 9 May 2024 09:01:05 +1000 Subject: [PATCH 085/102] Set initial selected item to matching path when changing data source too --- .../PyQt6/gui/auto_generated/qgsbrowsertreeview.sip.in | 4 +++- .../auto_generated/qgsdatasourceselectdialog.sip.in | 8 ++++++-- python/gui/auto_generated/qgsbrowsertreeview.sip.in | 4 +++- .../auto_generated/qgsdatasourceselectdialog.sip.in | 8 ++++++-- src/app/qgisapp.cpp | 2 +- src/gui/qgsbrowsertreeview.cpp | 7 ++++++- src/gui/qgsbrowsertreeview.h | 4 +++- src/gui/qgsdatasourceselectdialog.cpp | 10 ++++------ src/gui/qgsdatasourceselectdialog.h | 8 ++++++-- 9 files changed, 38 insertions(+), 17 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/qgsbrowsertreeview.sip.in b/python/PyQt6/gui/auto_generated/qgsbrowsertreeview.sip.in index b66a85c928562..b1066e07390d9 100644 --- a/python/PyQt6/gui/auto_generated/qgsbrowsertreeview.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsbrowsertreeview.sip.in @@ -66,12 +66,14 @@ Returns ``True`` if the item was found and could be selected. .. versionadded:: 3.28 %End - void expandPath( const QString &path ); + void expandPath( const QString &path, bool selectPath = false ); %Docstring Expands out a file ``path`` in the view. The ``path`` must correspond to a valid directory existing on the file system. +Since QGIS 3.38 the ``selectPath`` argument can be used to automatically select the path too. + .. versionadded:: 3.28 %End diff --git a/python/PyQt6/gui/auto_generated/qgsdatasourceselectdialog.sip.in b/python/PyQt6/gui/auto_generated/qgsdatasourceselectdialog.sip.in index 9784db0fb3be2..bb10bdfa3b08b 100644 --- a/python/PyQt6/gui/auto_generated/qgsdatasourceselectdialog.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsdatasourceselectdialog.sip.in @@ -65,12 +65,14 @@ Sets a description label .. versionadded:: 3.8 %End - void expandPath( const QString &path ); + void expandPath( const QString &path, bool selectPath = false ); %Docstring Expands out a file ``path`` in the view. The ``path`` must correspond to a valid directory existing on the file system. +Since QGIS 3.38 the ``selectPath`` argument can be used to automatically select the path too. + .. versionadded:: 3.28 %End @@ -176,12 +178,14 @@ Sets a description label .. versionadded:: 3.8 %End - void expandPath( const QString &path ); + void expandPath( const QString &path, bool selectPath = false ); %Docstring Expands out a file ``path`` in the view. The ``path`` must correspond to a valid directory existing on the file system. +Since QGIS 3.38 the ``selectPath`` argument can be used to automatically select the path too. + .. versionadded:: 3.28 %End diff --git a/python/gui/auto_generated/qgsbrowsertreeview.sip.in b/python/gui/auto_generated/qgsbrowsertreeview.sip.in index b66a85c928562..b1066e07390d9 100644 --- a/python/gui/auto_generated/qgsbrowsertreeview.sip.in +++ b/python/gui/auto_generated/qgsbrowsertreeview.sip.in @@ -66,12 +66,14 @@ Returns ``True`` if the item was found and could be selected. .. versionadded:: 3.28 %End - void expandPath( const QString &path ); + void expandPath( const QString &path, bool selectPath = false ); %Docstring Expands out a file ``path`` in the view. The ``path`` must correspond to a valid directory existing on the file system. +Since QGIS 3.38 the ``selectPath`` argument can be used to automatically select the path too. + .. versionadded:: 3.28 %End diff --git a/python/gui/auto_generated/qgsdatasourceselectdialog.sip.in b/python/gui/auto_generated/qgsdatasourceselectdialog.sip.in index 9784db0fb3be2..bb10bdfa3b08b 100644 --- a/python/gui/auto_generated/qgsdatasourceselectdialog.sip.in +++ b/python/gui/auto_generated/qgsdatasourceselectdialog.sip.in @@ -65,12 +65,14 @@ Sets a description label .. versionadded:: 3.8 %End - void expandPath( const QString &path ); + void expandPath( const QString &path, bool selectPath = false ); %Docstring Expands out a file ``path`` in the view. The ``path`` must correspond to a valid directory existing on the file system. +Since QGIS 3.38 the ``selectPath`` argument can be used to automatically select the path too. + .. versionadded:: 3.28 %End @@ -176,12 +178,14 @@ Sets a description label .. versionadded:: 3.8 %End - void expandPath( const QString &path ); + void expandPath( const QString &path, bool selectPath = false ); %Docstring Expands out a file ``path`` in the view. The ``path`` must correspond to a valid directory existing on the file system. +Since QGIS 3.38 the ``selectPath`` argument can be used to automatically select the path too. + .. versionadded:: 3.28 %End diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 18295736e3e33..c6856c998c8d9 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -7755,7 +7755,7 @@ void QgisApp::changeDataSource( QgsMapLayer *layer ) const QString closestPath = QFile::exists( path ) ? path : QgsFileUtils::findClosestExistingPath( path ); const QFileInfo pathInfo( closestPath ); - dlg.expandPath( pathInfo.isDir() ? closestPath : pathInfo.dir().path() ); + dlg.expandPath( pathInfo.isDir() ? closestPath : pathInfo.dir().path(), true ); if ( source.contains( path ) ) { source.replace( path, QStringLiteral( "%2" ).arg( QUrl::fromLocalFile( closestPath ).toString(), diff --git a/src/gui/qgsbrowsertreeview.cpp b/src/gui/qgsbrowsertreeview.cpp index 6ce4a05b989d1..4377084e8e0f5 100644 --- a/src/gui/qgsbrowsertreeview.cpp +++ b/src/gui/qgsbrowsertreeview.cpp @@ -177,7 +177,7 @@ bool QgsBrowserTreeView::hasExpandedDescendant( const QModelIndex &index ) const return false; } -void QgsBrowserTreeView::expandPath( const QString &str ) +void QgsBrowserTreeView::expandPath( const QString &str, bool selectPath ) { const QStringList pathParts = QgsFileUtils::splitPathToComponents( str ); if ( pathParts.isEmpty() ) @@ -289,6 +289,7 @@ void QgsBrowserTreeView::expandPath( const QString &str ) currentDir = QDir( thisPath ); } + QgsDirectoryItem *lastItem = nullptr; for ( QgsDirectoryItem *i : std::as_const( pathItems ) ) { QModelIndex index = mBrowserModel->findItem( i ); @@ -297,7 +298,11 @@ void QgsBrowserTreeView::expandPath( const QString &str ) index = proxyModel->mapFromSource( index ); } expand( index ); + lastItem = i; } + + if ( selectPath && lastItem ) + setSelectedItem( lastItem ); } bool QgsBrowserTreeView::setSelectedItem( QgsDataItem *item ) diff --git a/src/gui/qgsbrowsertreeview.h b/src/gui/qgsbrowsertreeview.h index 121b8f6003ab6..d91a8c08e3b9b 100644 --- a/src/gui/qgsbrowsertreeview.h +++ b/src/gui/qgsbrowsertreeview.h @@ -82,9 +82,11 @@ class GUI_EXPORT QgsBrowserTreeView : public QTreeView * * The \a path must correspond to a valid directory existing on the file system. * + * Since QGIS 3.38 the \a selectPath argument can be used to automatically select the path too. + * * \since QGIS 3.28 */ - void expandPath( const QString &path ); + void expandPath( const QString &path, bool selectPath = false ); protected: diff --git a/src/gui/qgsdatasourceselectdialog.cpp b/src/gui/qgsdatasourceselectdialog.cpp index fc76099de596f..af21480a89f8f 100644 --- a/src/gui/qgsdatasourceselectdialog.cpp +++ b/src/gui/qgsdatasourceselectdialog.cpp @@ -194,9 +194,9 @@ void QgsDataSourceSelectWidget::setDescription( const QString &description ) } } -void QgsDataSourceSelectWidget::expandPath( const QString &path ) +void QgsDataSourceSelectWidget::expandPath( const QString &path, bool selectPath ) { - mBrowserTreeView->expandPath( path ); + mBrowserTreeView->expandPath( path, selectPath ); } void QgsDataSourceSelectWidget::setFilter() @@ -205,7 +205,6 @@ void QgsDataSourceSelectWidget::setFilter() mBrowserProxyModel.setFilterString( filter ); } - void QgsDataSourceSelectWidget::refreshModel( const QModelIndex &index ) { @@ -255,7 +254,6 @@ void QgsDataSourceSelectWidget::setValid( bool valid ) } - void QgsDataSourceSelectWidget::setFilterSyntax( QAction *action ) { if ( !action ) @@ -354,9 +352,9 @@ void QgsDataSourceSelectDialog::setDescription( const QString &description ) mWidget->setDescription( description ); } -void QgsDataSourceSelectDialog::expandPath( const QString &path ) +void QgsDataSourceSelectDialog::expandPath( const QString &path, bool selectPath ) { - mWidget->expandPath( path ); + mWidget->expandPath( path, selectPath ); } QgsMimeDataUtils::Uri QgsDataSourceSelectDialog::uri() const diff --git a/src/gui/qgsdatasourceselectdialog.h b/src/gui/qgsdatasourceselectdialog.h index d9668835d1c33..95861cab06fca 100644 --- a/src/gui/qgsdatasourceselectdialog.h +++ b/src/gui/qgsdatasourceselectdialog.h @@ -82,9 +82,11 @@ class GUI_EXPORT QgsDataSourceSelectWidget: public QgsPanelWidget, private Ui::Q * * The \a path must correspond to a valid directory existing on the file system. * + * Since QGIS 3.38 the \a selectPath argument can be used to automatically select the path too. + * * \since QGIS 3.28 */ - void expandPath( const QString &path ); + void expandPath( const QString &path, bool selectPath = false ); /** * Returns the (possibly invalid) uri of the selected data source @@ -195,9 +197,11 @@ class GUI_EXPORT QgsDataSourceSelectDialog: public QDialog * * The \a path must correspond to a valid directory existing on the file system. * + * Since QGIS 3.38 the \a selectPath argument can be used to automatically select the path too. + * * \since QGIS 3.28 */ - void expandPath( const QString &path ); + void expandPath( const QString &path, bool selectPath = false ); /** * Returns the (possibly invalid) uri of the selected data source From 64f9a1d17dbbec2daf03ac27491f59196cc2e0dd Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 9 May 2024 09:01:53 +1000 Subject: [PATCH 086/102] Allow dropping paths onto data source select dialog to expand browser paths Makes it a bit easier to fix data sources for files in deep paths --- .../qgsdatasourceselectdialog.sip.in | 5 ++ .../qgsdatasourceselectdialog.sip.in | 5 ++ src/gui/qgsdatasourceselectdialog.cpp | 55 +++++++++++++++++++ src/gui/qgsdatasourceselectdialog.h | 6 ++ 4 files changed, 71 insertions(+) diff --git a/python/PyQt6/gui/auto_generated/qgsdatasourceselectdialog.sip.in b/python/PyQt6/gui/auto_generated/qgsdatasourceselectdialog.sip.in index bb10bdfa3b08b..5e68643d31882 100644 --- a/python/PyQt6/gui/auto_generated/qgsdatasourceselectdialog.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsdatasourceselectdialog.sip.in @@ -103,6 +103,11 @@ Apply filter to the model Scroll to last selected index and expand it's children %End + virtual void dragEnterEvent( QDragEnterEvent *event ); + + virtual void dropEvent( QDropEvent *event ); + + signals: void validationChanged( bool isValid ); diff --git a/python/gui/auto_generated/qgsdatasourceselectdialog.sip.in b/python/gui/auto_generated/qgsdatasourceselectdialog.sip.in index bb10bdfa3b08b..5e68643d31882 100644 --- a/python/gui/auto_generated/qgsdatasourceselectdialog.sip.in +++ b/python/gui/auto_generated/qgsdatasourceselectdialog.sip.in @@ -103,6 +103,11 @@ Apply filter to the model Scroll to last selected index and expand it's children %End + virtual void dragEnterEvent( QDragEnterEvent *event ); + + virtual void dropEvent( QDropEvent *event ); + + signals: void validationChanged( bool isValid ); diff --git a/src/gui/qgsdatasourceselectdialog.cpp b/src/gui/qgsdatasourceselectdialog.cpp index af21480a89f8f..b98b57639caa0 100644 --- a/src/gui/qgsdatasourceselectdialog.cpp +++ b/src/gui/qgsdatasourceselectdialog.cpp @@ -31,6 +31,7 @@ #include #include #include +#include QgsDataSourceSelectWidget::QgsDataSourceSelectWidget( QgsBrowserGuiModel *browserModel, @@ -116,6 +117,8 @@ QgsDataSourceSelectWidget::QgsDataSourceSelectWidget( { mActionShowFilter->trigger(); } + + setAcceptDrops( true ); } QgsDataSourceSelectWidget::~QgsDataSourceSelectWidget() = default; @@ -145,6 +148,58 @@ void QgsDataSourceSelectWidget::showEvent( QShowEvent *e ) } } +QString QgsDataSourceSelectWidget::acceptableFilePath( QDropEvent *event ) const +{ + if ( event->mimeData()->hasUrls() ) + { + const QList< QUrl > urls = event->mimeData()->urls(); + for ( const QUrl &url : urls ) + { + const QString local = url.toLocalFile(); + if ( local.isEmpty() ) + continue; + + if ( QFile::exists( local ) ) + { + return local; + } + } + } + return QString(); +} + +void QgsDataSourceSelectWidget::dragEnterEvent( QDragEnterEvent *event ) +{ + const QString filePath = acceptableFilePath( event ); + if ( !filePath.isEmpty() ) + { + event->acceptProposedAction(); + } + else + { + event->ignore(); + } +} + +void QgsDataSourceSelectWidget::dropEvent( QDropEvent *event ) +{ + const QString filePath = acceptableFilePath( event ); + if ( !filePath.isEmpty() ) + { + event->acceptProposedAction(); + + const QFileInfo fi( filePath ); + if ( fi.isDir() ) + { + expandPath( filePath, true ); + } + else + { + expandPath( fi.dir().path(), true ); + } + } +} + void QgsDataSourceSelectWidget::showFilterWidget( bool visible ) { QgsSettings().setValue( QStringLiteral( "datasourceSelectFilterVisible" ), visible, QgsSettings::Section::Gui ); diff --git a/src/gui/qgsdatasourceselectdialog.h b/src/gui/qgsdatasourceselectdialog.h index 95861cab06fca..8a2796b505914 100644 --- a/src/gui/qgsdatasourceselectdialog.h +++ b/src/gui/qgsdatasourceselectdialog.h @@ -104,6 +104,9 @@ class GUI_EXPORT QgsDataSourceSelectWidget: public QgsPanelWidget, private Ui::Q //! Scroll to last selected index and expand it's children void showEvent( QShowEvent *e ) override; + void dragEnterEvent( QDragEnterEvent *event ) override; + void dropEvent( QDropEvent *event ) override; + signals: /** @@ -137,6 +140,9 @@ class GUI_EXPORT QgsDataSourceSelectWidget: public QgsPanelWidget, private Ui::Q void setValid( bool valid ); + //! Returns file name if object meets drop criteria. + QString acceptableFilePath( QDropEvent *event ) const; + QgsBrowserProxyModel mBrowserProxyModel; QgsBrowserGuiModel *mBrowserModel = nullptr; QgsMimeDataUtils::Uri mUri; From c35efbed2375548fcbd64e14964cc54a2cbbaa1e Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sun, 12 May 2024 18:09:49 +1000 Subject: [PATCH 087/102] Use icon buttons instead of checkboxes in code editor search bar --- images/images.qrc | 3 ++ .../default/mIconSearchCaseSensitive.svg | 1 + .../themes/default/mIconSearchWholeWord.svg | 1 + .../themes/default/mIconSearchWrapAround.svg | 1 + src/gui/codeeditors/qgscodeeditorwidget.cpp | 42 ++++++++++++------- src/gui/codeeditors/qgscodeeditorwidget.h | 6 +-- 6 files changed, 36 insertions(+), 18 deletions(-) create mode 100644 images/themes/default/mIconSearchCaseSensitive.svg create mode 100644 images/themes/default/mIconSearchWholeWord.svg create mode 100644 images/themes/default/mIconSearchWrapAround.svg diff --git a/images/images.qrc b/images/images.qrc index e6d471965b575..8d135b5a94dc8 100644 --- a/images/images.qrc +++ b/images/images.qrc @@ -999,6 +999,9 @@ themes/default/mActionAddSensorThingsLayer.svg themes/default/mIconSensorThings.svg themes/default/mActionRunSelected.svg + themes/default/mIconSearchCaseSensitive.svg + themes/default/mIconSearchWholeWord.svg + themes/default/mIconSearchWrapAround.svg qgis_tips/symbol_levels.png diff --git a/images/themes/default/mIconSearchCaseSensitive.svg b/images/themes/default/mIconSearchCaseSensitive.svg new file mode 100644 index 0000000000000..ce25357e5ea89 --- /dev/null +++ b/images/themes/default/mIconSearchCaseSensitive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/themes/default/mIconSearchWholeWord.svg b/images/themes/default/mIconSearchWholeWord.svg new file mode 100644 index 0000000000000..81a8063f67847 --- /dev/null +++ b/images/themes/default/mIconSearchWholeWord.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/themes/default/mIconSearchWrapAround.svg b/images/themes/default/mIconSearchWrapAround.svg new file mode 100644 index 0000000000000..96cfb61b9c411 --- /dev/null +++ b/images/themes/default/mIconSearchWrapAround.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/codeeditors/qgscodeeditorwidget.cpp b/src/gui/codeeditors/qgscodeeditorwidget.cpp index cfae24713e239..de1090a750653 100644 --- a/src/gui/codeeditors/qgscodeeditorwidget.cpp +++ b/src/gui/codeeditors/qgscodeeditorwidget.cpp @@ -61,6 +61,27 @@ QgsCodeEditorWidget::QgsCodeEditorWidget( mLineEditFind->setPlaceholderText( tr( "Enter text to find…" ) ); layoutFind->addWidget( mLineEditFind, 1 ); + mCaseSensitiveButton = new QToolButton(); + mCaseSensitiveButton->setToolTip( tr( "Case Sensitive" ) ); + mCaseSensitiveButton->setCheckable( true ); + mCaseSensitiveButton->setAutoRaise( true ); + mCaseSensitiveButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchCaseSensitive.svg" ) ) ); + layoutFind->addWidget( mCaseSensitiveButton ); + + mWholeWordButton = new QToolButton( ); + mWholeWordButton->setToolTip( tr( "Whole Word" ) ); + mWholeWordButton->setCheckable( true ); + mWholeWordButton->setAutoRaise( true ); + mWholeWordButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchWholeWord.svg" ) ) ); + layoutFind->addWidget( mWholeWordButton ); + + mWrapAroundButton = new QToolButton(); + mWrapAroundButton->setToolTip( tr( "Wrap Around" ) ); + mWrapAroundButton->setCheckable( true ); + mWrapAroundButton->setAutoRaise( true ); + mWrapAroundButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchWrapAround.svg" ) ) ); + layoutFind->addWidget( mWrapAroundButton ); + mFindPrevButton = new QToolButton(); mFindPrevButton->setEnabled( false ); mFindPrevButton->setToolTip( tr( "Find Previous" ) ); @@ -75,22 +96,13 @@ QgsCodeEditorWidget::QgsCodeEditorWidget( mFindNextButton->setAutoRaise( true ); layoutFind->addWidget( mFindNextButton ); - mCaseSensitiveCheck = new QCheckBox( tr( "Case Sensitive" ) ); - layoutFind->addWidget( mCaseSensitiveCheck ); - - mWholeWordCheck = new QCheckBox( tr( "Whole Word" ) ); - layoutFind->addWidget( mWholeWordCheck ); - - mWrapAroundCheck = new QCheckBox( tr( "Wrap Around" ) ); - layoutFind->addWidget( mWrapAroundCheck ); - connect( mLineEditFind, &QLineEdit::returnPressed, this, &QgsCodeEditorWidget::findNext ); connect( mLineEditFind, &QLineEdit::textChanged, this, &QgsCodeEditorWidget::textSearchChanged ); connect( mFindNextButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findNext ); connect( mFindPrevButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findPrevious ); - connect( mCaseSensitiveCheck, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateSearch ); - connect( mWholeWordCheck, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateSearch ); - connect( mWrapAroundCheck, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateSearch ); + connect( mCaseSensitiveButton, &QToolButton::toggled, this, &QgsCodeEditorWidget::updateSearch ); + connect( mCaseSensitiveButton, &QToolButton::toggled, this, &QgsCodeEditorWidget::updateSearch ); + connect( mWrapAroundButton, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateSearch ); QShortcut *findShortcut = new QShortcut( QKeySequence::StandardKey::Find, mEditor ); findShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut ); @@ -298,9 +310,9 @@ void QgsCodeEditorWidget::findText( bool forward, bool findFirst, bool showNotFo } const bool isRegEx = false; - const bool wrapAround = mWrapAroundCheck->isChecked(); - const bool isCaseSensitive = mCaseSensitiveCheck->isChecked(); - const bool isWholeWordOnly = mWholeWordCheck->isChecked(); + const bool wrapAround = mWrapAroundButton->isChecked(); + const bool isCaseSensitive = mCaseSensitiveButton->isChecked(); + const bool isWholeWordOnly = mWholeWordButton->isChecked(); const bool found = mEditor->findFirst( searchString, isRegEx, isCaseSensitive, isWholeWordOnly, wrapAround, forward, line, index ); diff --git a/src/gui/codeeditors/qgscodeeditorwidget.h b/src/gui/codeeditors/qgscodeeditorwidget.h index 16056642433f6..068ccff4d5757 100644 --- a/src/gui/codeeditors/qgscodeeditorwidget.h +++ b/src/gui/codeeditors/qgscodeeditorwidget.h @@ -141,9 +141,9 @@ class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget QgsFilterLineEdit *mLineEditFind = nullptr; QToolButton *mFindPrevButton = nullptr; QToolButton *mFindNextButton = nullptr; - QCheckBox *mCaseSensitiveCheck = nullptr; - QCheckBox *mWholeWordCheck = nullptr; - QCheckBox *mWrapAroundCheck = nullptr; + QToolButton *mCaseSensitiveButton = nullptr; + QToolButton *mWholeWordButton = nullptr; + QToolButton *mWrapAroundButton = nullptr; int mBlockSearching = 0; QgsMessageBar *mMessageBar = nullptr; std::unique_ptr< QgsScrollBarHighlightController > mHighlightController; From 8d9c68b52756577e4bfbcda8303ba881ea3f185e Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sun, 12 May 2024 18:13:29 +1000 Subject: [PATCH 088/102] Tighten spacing --- src/gui/codeeditors/qgscodeeditorwidget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gui/codeeditors/qgscodeeditorwidget.cpp b/src/gui/codeeditors/qgscodeeditorwidget.cpp index de1090a750653..52d67d6cd3b51 100644 --- a/src/gui/codeeditors/qgscodeeditorwidget.cpp +++ b/src/gui/codeeditors/qgscodeeditorwidget.cpp @@ -56,6 +56,7 @@ QgsCodeEditorWidget::QgsCodeEditorWidget( mFindWidget = new QWidget(); QHBoxLayout *layoutFind = new QHBoxLayout(); layoutFind->setContentsMargins( 0, 2, 0, 0 ); + layoutFind->setSpacing( 1 ); mLineEditFind = new QgsFilterLineEdit(); mLineEditFind->setShowSearchIcon( true ); mLineEditFind->setPlaceholderText( tr( "Enter text to find…" ) ); From b8cfddd7fcd6007f9d26dad5313994eae6c5c2ce Mon Sep 17 00:00:00 2001 From: Andrea Giudiceandrea Date: Mon, 13 May 2024 01:22:54 +0200 Subject: [PATCH 089/102] Allow to set network cache size >= 2 GiB and < 2TiB --- src/app/options/qgsoptions.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/options/qgsoptions.cpp b/src/app/options/qgsoptions.cpp index 6bd2a9bb8afc2..2032e8410312d 100644 --- a/src/app/options/qgsoptions.cpp +++ b/src/app/options/qgsoptions.cpp @@ -1589,7 +1589,7 @@ void QgsOptions::saveOptions() else mSettings->remove( QStringLiteral( "cache/directory" ) ); - mSettings->setValue( QStringLiteral( "cache/size" ), QVariant::fromValue( mCacheSize->value() * 1024L ) ); + mSettings->setValue( QStringLiteral( "cache/size" ), QVariant::fromValue( mCacheSize->value() * 1024LL ) ); //url with no proxy at all QStringList noProxyUrls; From 406c67c6754a392e6534183c4257b7fb9d7f29cc Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 13 May 2024 08:47:15 +1000 Subject: [PATCH 090/102] Add search option for regular expression based searching Adds regex search mode for use in code editor searching --- images/images.qrc | 1 + images/themes/default/mIconSearchRegex.svg | 1 + src/gui/codeeditors/qgscodeeditorwidget.cpp | 31 +++++++++++++++++---- src/gui/codeeditors/qgscodeeditorwidget.h | 2 ++ 4 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 images/themes/default/mIconSearchRegex.svg diff --git a/images/images.qrc b/images/images.qrc index 8d135b5a94dc8..b609842a0d987 100644 --- a/images/images.qrc +++ b/images/images.qrc @@ -1002,6 +1002,7 @@ themes/default/mIconSearchCaseSensitive.svg themes/default/mIconSearchWholeWord.svg themes/default/mIconSearchWrapAround.svg + themes/default/mIconSearchRegex.svg qgis_tips/symbol_levels.png diff --git a/images/themes/default/mIconSearchRegex.svg b/images/themes/default/mIconSearchRegex.svg new file mode 100644 index 0000000000000..cec740439ab97 --- /dev/null +++ b/images/themes/default/mIconSearchRegex.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/codeeditors/qgscodeeditorwidget.cpp b/src/gui/codeeditors/qgscodeeditorwidget.cpp index 52d67d6cd3b51..b1a553c6d9abe 100644 --- a/src/gui/codeeditors/qgscodeeditorwidget.cpp +++ b/src/gui/codeeditors/qgscodeeditorwidget.cpp @@ -76,6 +76,13 @@ QgsCodeEditorWidget::QgsCodeEditorWidget( mWholeWordButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchWholeWord.svg" ) ) ); layoutFind->addWidget( mWholeWordButton ); + mRegexButton = new QToolButton( ); + mRegexButton->setToolTip( tr( "Use Regular Expressions" ) ); + mRegexButton->setCheckable( true ); + mRegexButton->setAutoRaise( true ); + mRegexButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchRegex.svg" ) ) ); + layoutFind->addWidget( mRegexButton ); + mWrapAroundButton = new QToolButton(); mWrapAroundButton->setToolTip( tr( "Wrap Around" ) ); mWrapAroundButton->setCheckable( true ); @@ -102,7 +109,8 @@ QgsCodeEditorWidget::QgsCodeEditorWidget( connect( mFindNextButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findNext ); connect( mFindPrevButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findPrevious ); connect( mCaseSensitiveButton, &QToolButton::toggled, this, &QgsCodeEditorWidget::updateSearch ); - connect( mCaseSensitiveButton, &QToolButton::toggled, this, &QgsCodeEditorWidget::updateSearch ); + connect( mWholeWordButton, &QToolButton::toggled, this, &QgsCodeEditorWidget::updateSearch ); + connect( mRegexButton, &QToolButton::toggled, this, &QgsCodeEditorWidget::updateSearch ); connect( mWrapAroundButton, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateSearch ); QShortcut *findShortcut = new QShortcut( QKeySequence::StandardKey::Find, mEditor ); @@ -255,6 +263,17 @@ void QgsCodeEditorWidget::addSearchHighlights() mHighlightController->setLineHeight( QFontMetrics( mEditor->font() ).lineSpacing() ); mHighlightController->setVisibleRange( mEditor->viewport()->rect().height() ); + int searchFlags = 0; + const bool isRegEx = mRegexButton->isChecked(); + const bool isCaseSensitive = mCaseSensitiveButton->isChecked(); + const bool isWholeWordOnly = mWholeWordButton->isChecked(); + if ( isRegEx ) + searchFlags |= QsciScintilla::SCFIND_REGEXP | QsciScintilla::SCFIND_CXX11REGEX; + if ( isCaseSensitive ) + searchFlags |= QsciScintilla::SCFIND_MATCHCASE; + if ( isWholeWordOnly ) + searchFlags |= QsciScintilla::SCFIND_WHOLEWORD; + mEditor->SendScintilla( QsciScintilla::SCI_SETSEARCHFLAGS, searchFlags ); while ( true ) { mEditor->SendScintilla( QsciScintilla::SCI_SETTARGETRANGE, startPos, docEnd ); @@ -262,10 +281,12 @@ void QgsCodeEditorWidget::addSearchHighlights() if ( fstart < 0 ) break; - startPos = fstart + searchString.length(); + const int matchLength = mEditor->SendScintilla( QsciScintilla::SCI_GETTARGETTEXT, 0, static_cast< void * >( nullptr ) ); + + startPos = fstart + matchLength; mEditor->SendScintilla( QsciScintilla::SCI_SETINDICATORCURRENT, QgsCodeEditor::SEARCH_RESULT_INDICATOR ); - mEditor->SendScintilla( QsciScintilla::SCI_INDICATORFILLRANGE, fstart, searchString.length() ); + mEditor->SendScintilla( QsciScintilla::SCI_INDICATORFILLRANGE, fstart, matchLength ); int thisLine = 0; int thisIndex = 0; @@ -310,13 +331,13 @@ void QgsCodeEditorWidget::findText( bool forward, bool findFirst, bool showNotFo index = indexFrom; } - const bool isRegEx = false; + const bool isRegEx = mRegexButton->isChecked(); const bool wrapAround = mWrapAroundButton->isChecked(); const bool isCaseSensitive = mCaseSensitiveButton->isChecked(); const bool isWholeWordOnly = mWholeWordButton->isChecked(); const bool found = mEditor->findFirst( searchString, isRegEx, isCaseSensitive, isWholeWordOnly, wrapAround, forward, - line, index ); + line, index, true, true, isRegEx ); if ( !found ) { diff --git a/src/gui/codeeditors/qgscodeeditorwidget.h b/src/gui/codeeditors/qgscodeeditorwidget.h index 068ccff4d5757..48fec65fd90ae 100644 --- a/src/gui/codeeditors/qgscodeeditorwidget.h +++ b/src/gui/codeeditors/qgscodeeditorwidget.h @@ -129,6 +129,7 @@ class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget void clearSearchHighlights(); void addSearchHighlights(); + int searchFlags() const; void findText( bool forward, bool findFirst, bool showNotFoundWarning = false ); enum HighlightCategory @@ -143,6 +144,7 @@ class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget QToolButton *mFindNextButton = nullptr; QToolButton *mCaseSensitiveButton = nullptr; QToolButton *mWholeWordButton = nullptr; + QToolButton *mRegexButton = nullptr; QToolButton *mWrapAroundButton = nullptr; int mBlockSearching = 0; QgsMessageBar *mMessageBar = nullptr; From d1369880981ed99cb6b3501bf7688d07bc35152f Mon Sep 17 00:00:00 2001 From: Alexander Bruy Date: Mon, 13 May 2024 10:49:08 +0100 Subject: [PATCH 091/102] address review --- src/gui/layout/qgslayoutmapwidget.cpp | 4 ++++ src/ui/layout/qgslayoutmapwidgetbase.ui | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/gui/layout/qgslayoutmapwidget.cpp b/src/gui/layout/qgslayoutmapwidget.cpp index f9495872a46f2..92d30794b504e 100644 --- a/src/gui/layout/qgslayoutmapwidget.cpp +++ b/src/gui/layout/qgslayoutmapwidget.cpp @@ -1255,6 +1255,10 @@ void QgsLayoutMapWidget::mCopyGridPushButton_clicked() } QgsLayoutItemMapGrid *sourceGrid = mMapItem->grids()->grid( item->data( Qt::UserRole ).toString() ); + if ( !sourceGrid ) + { + return; + } int i = 0; QString itemName = tr( "%1 - Copy" ).arg( sourceGrid->name() ); QList< QgsLayoutItemMapGrid * > grids = mMapItem->grids()->asList(); diff --git a/src/ui/layout/qgslayoutmapwidgetbase.ui b/src/ui/layout/qgslayoutmapwidgetbase.ui index 31281d3f299c5..1cb6df2b49cae 100644 --- a/src/ui/layout/qgslayoutmapwidgetbase.ui +++ b/src/ui/layout/qgslayoutmapwidgetbase.ui @@ -88,9 +88,9 @@ 0 - -123 - 548 - 1478 + -798 + 539 + 1640 @@ -762,7 +762,7 @@ - :/images/themes/default/mActionEditCopy.svg:/images/themes/default/mActionEditCopy.svg + :/images/themes/default/mActionDuplicateLayout.svg:/images/themes/default/mActionDuplicateLayout.svg From 4eb7661d85b7ca749b581b94a3f32285929144dd Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 26 Mar 2024 15:31:25 +1000 Subject: [PATCH 092/102] Add fixed elevation range per dataset group mode for mesh layers This mimics the "fixed range per band" mode we have for raster layers, but allows a per-dataset group elevation range to be set for mesh layers --- python/PyQt6/core/auto_additions/qgis.py | 3 +- .../auto_generated/mesh/qgsmeshlayer.sip.in | 30 +- .../qgsmeshlayerelevationproperties.sip.in | 36 ++ python/PyQt6/core/auto_generated/qgis.sip.in | 3 +- python/core/auto_additions/qgis.py | 3 +- .../auto_generated/mesh/qgsmeshlayer.sip.in | 30 +- .../qgsmeshlayerelevationproperties.sip.in | 36 ++ python/core/auto_generated/qgis.sip.in | 3 +- .../mesh/qgsmeshelevationpropertieswidget.cpp | 331 +++++++++++++++++ .../mesh/qgsmeshelevationpropertieswidget.h | 48 +++ src/core/mesh/qgsmeshlayer.cpp | 20 +- src/core/mesh/qgsmeshlayer.h | 26 +- .../mesh/qgsmeshlayerelevationproperties.cpp | 101 ++++++ .../mesh/qgsmeshlayerelevationproperties.h | 27 ++ src/core/mesh/qgsmeshlayerrenderer.cpp | 62 +++- src/core/qgis.h | 3 +- .../qgsmeshelevationpropertieswidgetbase.ui | 338 +++++++++++------- tests/src/python/test_qgsmeshlayerrenderer.py | 66 ++++ ...d_elevation_range_per_group_match1and2.png | Bin 0 -> 471523 bytes ...ected_elevation_range_per_group_match3.png | Bin 0 -> 471523 bytes ...ed_elevation_range_per_group_no_filter.png | Bin 0 -> 471523 bytes 21 files changed, 957 insertions(+), 209 deletions(-) create mode 100644 tests/testdata/control_images/mesh/expected_elevation_range_per_group_match1and2/expected_elevation_range_per_group_match1and2.png create mode 100644 tests/testdata/control_images/mesh/expected_elevation_range_per_group_match3/expected_elevation_range_per_group_match3.png create mode 100644 tests/testdata/control_images/mesh/expected_elevation_range_per_group_no_filter/expected_elevation_range_per_group_no_filter.png diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index 7c5c22663f2b1..5d10aa2e64fb1 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -3278,7 +3278,8 @@ # monkey patching scoped based enum Qgis.MeshElevationMode.FixedElevationRange.__doc__ = "Layer has a fixed elevation range" Qgis.MeshElevationMode.FromVertices.__doc__ = "Elevation should be taken from mesh vertices" -Qgis.MeshElevationMode.__doc__ = "Mesh layer elevation modes.\n\n.. versionadded:: 3.38\n\n" + '* ``FixedElevationRange``: ' + Qgis.MeshElevationMode.FixedElevationRange.__doc__ + '\n' + '* ``FromVertices``: ' + Qgis.MeshElevationMode.FromVertices.__doc__ +Qgis.MeshElevationMode.FixedRangePerGroup.__doc__ = "Layer has a fixed (manually specified) elevation range per group" +Qgis.MeshElevationMode.__doc__ = "Mesh layer elevation modes.\n\n.. versionadded:: 3.38\n\n" + '* ``FixedElevationRange``: ' + Qgis.MeshElevationMode.FixedElevationRange.__doc__ + '\n' + '* ``FromVertices``: ' + Qgis.MeshElevationMode.FromVertices.__doc__ + '\n' + '* ``FixedRangePerGroup``: ' + Qgis.MeshElevationMode.FixedRangePerGroup.__doc__ # -- Qgis.MeshElevationMode.baseClass = Qgis # monkey patching scoped based enum diff --git a/python/PyQt6/core/auto_generated/mesh/qgsmeshlayer.sip.in b/python/PyQt6/core/auto_generated/mesh/qgsmeshlayer.sip.in index ad3f120c534f0..89931877d2c71 100644 --- a/python/PyQt6/core/auto_generated/mesh/qgsmeshlayer.sip.in +++ b/python/PyQt6/core/auto_generated/mesh/qgsmeshlayer.sip.in @@ -592,52 +592,54 @@ Dataset index is valid even the temporal properties is inactive. This method is .. versionadded:: 3.22 %End - QgsMeshDatasetIndex activeScalarDatasetAtTime( const QgsDateTimeRange &timeRange ) const; + QgsMeshDatasetIndex activeScalarDatasetAtTime( const QgsDateTimeRange &timeRange, int group = -1 ) const; %Docstring Returns dataset index from active scalar group depending on the time range. If the temporal properties is not active, return the static dataset -:param timeRange: the time range - -:return: dataset index +Since QGIS 3.38, the ``group`` argument can be used to specify a fixed group +to use. If this is not specified, then the active group from the layer's renderer will be used. .. note:: the returned dataset index depends on the matching method, see :py:func:`~QgsMeshLayer.setTemporalMatchingMethod` - .. versionadded:: 3.14 %End - QgsMeshDatasetIndex activeVectorDatasetAtTime( const QgsDateTimeRange &timeRange ) const; + QgsMeshDatasetIndex activeVectorDatasetAtTime( const QgsDateTimeRange &timeRange, int group = -1 ) const; %Docstring Returns dataset index from active vector group depending on the time range If the temporal properties is not active, return the static dataset -:param timeRange: the time range - -:return: dataset index +Since QGIS 3.38, the ``group`` argument can be used to specify a fixed group +to use. If this is not specified, then the active group from the layer's renderer will be used. .. note:: the returned dataset index depends on the matching method, see :py:func:`~QgsMeshLayer.setTemporalMatchingMethod` - .. versionadded:: 3.14 %End - QgsMeshDatasetIndex staticScalarDatasetIndex() const; + QgsMeshDatasetIndex staticScalarDatasetIndex( int group = -1 ) const; %Docstring -Returns the static scalar dataset index that is rendered if the temporal properties is not active +Returns the static scalar dataset index that is rendered if the temporal properties is not active. + +Since QGIS 3.38, the ``group`` argument can be used to specify a fixed group +to use. If this is not specified, then the active group from the layer's renderer will be used. .. versionadded:: 3.14 %End - QgsMeshDatasetIndex staticVectorDatasetIndex() const; + QgsMeshDatasetIndex staticVectorDatasetIndex( int group = -1 ) const; %Docstring -Returns the static vector dataset index that is rendered if the temporal properties is not active +Returns the static vector dataset index that is rendered if the temporal properties is not active. + +Since QGIS 3.38, the ``group`` argument can be used to specify a fixed group +to use. If this is not specified, then the active group from the layer's renderer will be used. .. versionadded:: 3.14 %End diff --git a/python/PyQt6/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in b/python/PyQt6/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in index f15a152d4cf72..28266b6c237c6 100644 --- a/python/PyQt6/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in +++ b/python/PyQt6/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in @@ -100,6 +100,42 @@ Sets the fixed elevation ``range`` for the mesh. .. seealso:: :py:func:`fixedRange` +.. versionadded:: 3.38 +%End + + QMap fixedRangePerGroup() const; +%Docstring +Returns the fixed elevation range for each group. + +.. note:: + + This is only considered when :py:func:`~QgsMeshLayerElevationProperties.mode` is :py:class:`Qgis`.MeshElevationMode.FixedRangePerGroup. + +.. note:: + + When a fixed range is set any :py:func:`~QgsMeshLayerElevationProperties.zOffset` and :py:func:`~QgsMeshLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`setFixedRangePerGroup` + +.. versionadded:: 3.38 +%End + + void setFixedRangePerGroup( const QMap &ranges ); +%Docstring +Sets the fixed elevation range for each group. + +.. note:: + + This is only considered when :py:func:`~QgsMeshLayerElevationProperties.mode` is :py:class:`Qgis`.MeshElevationMode.FixedRangePerGroup. + +.. note:: + + When a fixed range is set any :py:func:`~QgsMeshLayerElevationProperties.zOffset` and :py:func:`~QgsMeshLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`fixedRangePerGroup` + .. versionadded:: 3.38 %End diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index 2c84aca227df5..879b61c981c46 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -1888,7 +1888,8 @@ The development version enum class MeshElevationMode /BaseType=IntEnum/ { FixedElevationRange, - FromVertices + FromVertices, + FixedRangePerGroup, }; enum class BetweenLineConstraint /BaseType=IntEnum/ diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index d04309e176020..e47789aef6aa6 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -3223,7 +3223,8 @@ # monkey patching scoped based enum Qgis.MeshElevationMode.FixedElevationRange.__doc__ = "Layer has a fixed elevation range" Qgis.MeshElevationMode.FromVertices.__doc__ = "Elevation should be taken from mesh vertices" -Qgis.MeshElevationMode.__doc__ = "Mesh layer elevation modes.\n\n.. versionadded:: 3.38\n\n" + '* ``FixedElevationRange``: ' + Qgis.MeshElevationMode.FixedElevationRange.__doc__ + '\n' + '* ``FromVertices``: ' + Qgis.MeshElevationMode.FromVertices.__doc__ +Qgis.MeshElevationMode.FixedRangePerGroup.__doc__ = "Layer has a fixed (manually specified) elevation range per group" +Qgis.MeshElevationMode.__doc__ = "Mesh layer elevation modes.\n\n.. versionadded:: 3.38\n\n" + '* ``FixedElevationRange``: ' + Qgis.MeshElevationMode.FixedElevationRange.__doc__ + '\n' + '* ``FromVertices``: ' + Qgis.MeshElevationMode.FromVertices.__doc__ + '\n' + '* ``FixedRangePerGroup``: ' + Qgis.MeshElevationMode.FixedRangePerGroup.__doc__ # -- Qgis.MeshElevationMode.baseClass = Qgis # monkey patching scoped based enum diff --git a/python/core/auto_generated/mesh/qgsmeshlayer.sip.in b/python/core/auto_generated/mesh/qgsmeshlayer.sip.in index ad3f120c534f0..89931877d2c71 100644 --- a/python/core/auto_generated/mesh/qgsmeshlayer.sip.in +++ b/python/core/auto_generated/mesh/qgsmeshlayer.sip.in @@ -592,52 +592,54 @@ Dataset index is valid even the temporal properties is inactive. This method is .. versionadded:: 3.22 %End - QgsMeshDatasetIndex activeScalarDatasetAtTime( const QgsDateTimeRange &timeRange ) const; + QgsMeshDatasetIndex activeScalarDatasetAtTime( const QgsDateTimeRange &timeRange, int group = -1 ) const; %Docstring Returns dataset index from active scalar group depending on the time range. If the temporal properties is not active, return the static dataset -:param timeRange: the time range - -:return: dataset index +Since QGIS 3.38, the ``group`` argument can be used to specify a fixed group +to use. If this is not specified, then the active group from the layer's renderer will be used. .. note:: the returned dataset index depends on the matching method, see :py:func:`~QgsMeshLayer.setTemporalMatchingMethod` - .. versionadded:: 3.14 %End - QgsMeshDatasetIndex activeVectorDatasetAtTime( const QgsDateTimeRange &timeRange ) const; + QgsMeshDatasetIndex activeVectorDatasetAtTime( const QgsDateTimeRange &timeRange, int group = -1 ) const; %Docstring Returns dataset index from active vector group depending on the time range If the temporal properties is not active, return the static dataset -:param timeRange: the time range - -:return: dataset index +Since QGIS 3.38, the ``group`` argument can be used to specify a fixed group +to use. If this is not specified, then the active group from the layer's renderer will be used. .. note:: the returned dataset index depends on the matching method, see :py:func:`~QgsMeshLayer.setTemporalMatchingMethod` - .. versionadded:: 3.14 %End - QgsMeshDatasetIndex staticScalarDatasetIndex() const; + QgsMeshDatasetIndex staticScalarDatasetIndex( int group = -1 ) const; %Docstring -Returns the static scalar dataset index that is rendered if the temporal properties is not active +Returns the static scalar dataset index that is rendered if the temporal properties is not active. + +Since QGIS 3.38, the ``group`` argument can be used to specify a fixed group +to use. If this is not specified, then the active group from the layer's renderer will be used. .. versionadded:: 3.14 %End - QgsMeshDatasetIndex staticVectorDatasetIndex() const; + QgsMeshDatasetIndex staticVectorDatasetIndex( int group = -1 ) const; %Docstring -Returns the static vector dataset index that is rendered if the temporal properties is not active +Returns the static vector dataset index that is rendered if the temporal properties is not active. + +Since QGIS 3.38, the ``group`` argument can be used to specify a fixed group +to use. If this is not specified, then the active group from the layer's renderer will be used. .. versionadded:: 3.14 %End diff --git a/python/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in b/python/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in index f15a152d4cf72..28266b6c237c6 100644 --- a/python/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in +++ b/python/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in @@ -100,6 +100,42 @@ Sets the fixed elevation ``range`` for the mesh. .. seealso:: :py:func:`fixedRange` +.. versionadded:: 3.38 +%End + + QMap fixedRangePerGroup() const; +%Docstring +Returns the fixed elevation range for each group. + +.. note:: + + This is only considered when :py:func:`~QgsMeshLayerElevationProperties.mode` is :py:class:`Qgis`.MeshElevationMode.FixedRangePerGroup. + +.. note:: + + When a fixed range is set any :py:func:`~QgsMeshLayerElevationProperties.zOffset` and :py:func:`~QgsMeshLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`setFixedRangePerGroup` + +.. versionadded:: 3.38 +%End + + void setFixedRangePerGroup( const QMap &ranges ); +%Docstring +Sets the fixed elevation range for each group. + +.. note:: + + This is only considered when :py:func:`~QgsMeshLayerElevationProperties.mode` is :py:class:`Qgis`.MeshElevationMode.FixedRangePerGroup. + +.. note:: + + When a fixed range is set any :py:func:`~QgsMeshLayerElevationProperties.zOffset` and :py:func:`~QgsMeshLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`fixedRangePerGroup` + .. versionadded:: 3.38 %End diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index 93dec02db9a69..7eb6600c38a1b 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -1888,7 +1888,8 @@ The development version enum class MeshElevationMode { FixedElevationRange, - FromVertices + FromVertices, + FixedRangePerGroup, }; enum class BetweenLineConstraint diff --git a/src/app/mesh/qgsmeshelevationpropertieswidget.cpp b/src/app/mesh/qgsmeshelevationpropertieswidget.cpp index cd5ede8081b10..25e3150317ea9 100644 --- a/src/app/mesh/qgsmeshelevationpropertieswidget.cpp +++ b/src/app/mesh/qgsmeshelevationpropertieswidget.cpp @@ -20,6 +20,10 @@ #include "qgsmeshlayerelevationproperties.h" #include "qgslinesymbol.h" #include "qgsfillsymbol.h" +#include "qgsexpressionbuilderdialog.h" +#include "qgsexpressioncontextutils.h" +#include +#include QgsMeshElevationPropertiesWidget::QgsMeshElevationPropertiesWidget( QgsMeshLayer *layer, QgsMapCanvas *canvas, QWidget *parent ) : QgsMapLayerConfigWidget( layer, canvas, parent ) @@ -29,6 +33,7 @@ QgsMeshElevationPropertiesWidget::QgsMeshElevationPropertiesWidget( QgsMeshLayer mModeComboBox->addItem( tr( "From Vertices" ), QVariant::fromValue( Qgis::MeshElevationMode::FromVertices ) ); mModeComboBox->addItem( tr( "Fixed Elevation Range" ), QVariant::fromValue( Qgis::MeshElevationMode::FixedElevationRange ) ); + mModeComboBox->addItem( tr( "Fixed Elevation Range Per Group" ), QVariant::fromValue( Qgis::MeshElevationMode::FixedRangePerGroup ) ); mLimitsComboBox->addItem( tr( "Include Lower and Upper" ), QVariant::fromValue( Qgis::RangeLimits::IncludeBoth ) ); mLimitsComboBox->addItem( tr( "Include Lower, Exclude Upper" ), QVariant::fromValue( Qgis::RangeLimits::IncludeLowerExcludeUpper ) ); @@ -52,6 +57,29 @@ QgsMeshElevationPropertiesWidget::QgsMeshElevationPropertiesWidget( QgsMeshLayer mFixedLowerSpinBox->clear(); mFixedUpperSpinBox->clear(); + mFixedRangePerGroupModel = new QgsMeshGroupFixedElevationRangeModel( this ); + mGroupElevationTable->verticalHeader()->setVisible( false ); + mGroupElevationTable->setModel( mFixedRangePerGroupModel ); + QgsMeshFixedElevationRangeDelegate *tableDelegate = new QgsMeshFixedElevationRangeDelegate( mGroupElevationTable ); + mGroupElevationTable->setItemDelegateForColumn( 1, tableDelegate ); + mGroupElevationTable->setItemDelegateForColumn( 2, tableDelegate ); + + QMenu *calculateFixedRangePerGroupMenu = new QMenu( mCalculateFixedRangePerGroupButton ); + mCalculateFixedRangePerGroupButton->setMenu( calculateFixedRangePerGroupMenu ); + mCalculateFixedRangePerGroupButton->setPopupMode( QToolButton::InstantPopup ); + QAction *calculateLowerAction = new QAction( "Calculate Lower by Expression…", calculateFixedRangePerGroupMenu ); + calculateFixedRangePerGroupMenu->addAction( calculateLowerAction ); + connect( calculateLowerAction, &QAction::triggered, this, [this] + { + calculateRangeByExpression( false ); + } ); + QAction *calculateUpperAction = new QAction( "Calculate Upper by Expression…", calculateFixedRangePerGroupMenu ); + calculateFixedRangePerGroupMenu->addAction( calculateUpperAction ); + connect( calculateUpperAction, &QAction::triggered, this, [this] + { + calculateRangeByExpression( true ); + } ); + syncToLayer( layer ); connect( mModeComboBox, qOverload( &QComboBox::currentIndexChanged ), this, &QgsMeshElevationPropertiesWidget::modeChanged ); @@ -97,6 +125,9 @@ void QgsMeshElevationPropertiesWidget::syncToLayer( QgsMapLayer *layer ) case Qgis::MeshElevationMode::FromVertices: mStackedWidget->setCurrentWidget( mPageFromVertices ); break; + case Qgis::MeshElevationMode::FixedRangePerGroup: + mStackedWidget->setCurrentWidget( mPageFixedRangePerGroup ); + break; } mOffsetZSpinBox->setValue( props->zOffset() ); @@ -116,6 +147,11 @@ void QgsMeshElevationPropertiesWidget::syncToLayer( QgsMapLayer *layer ) mFixedUpperSpinBox->clear(); mLimitsComboBox->setCurrentIndex( mLimitsComboBox->findData( QVariant::fromValue( props->fixedRange().rangeLimits() ) ) ); + mFixedRangePerGroupModel->setLayerData( mLayer, props->fixedRangePerGroup() ); + mGroupElevationTable->horizontalHeader()->setSectionResizeMode( 0, QHeaderView::Stretch ); + mGroupElevationTable->horizontalHeader()->setSectionResizeMode( 1, QHeaderView::Stretch ); + mGroupElevationTable->horizontalHeader()->setSectionResizeMode( 2, QHeaderView::Stretch ); + mLineStyleButton->setSymbol( props->profileLineSymbol()->clone() ); mFillStyleButton->setSymbol( props->profileFillSymbol()->clone() ); @@ -157,6 +193,7 @@ void QgsMeshElevationPropertiesWidget::apply() fixedUpper = mFixedUpperSpinBox->value(); props->setFixedRange( QgsDoubleRange( fixedLower, fixedUpper, mLimitsComboBox->currentData().value< Qgis::RangeLimits >() ) ); + props->setFixedRangePerGroup( mFixedRangePerGroupModel->rangeData() ); props->setProfileLineSymbol( mLineStyleButton->clonedSymbol< QgsLineSymbol >() ); props->setProfileFillSymbol( mFillStyleButton->clonedSymbol< QgsFillSymbol >() ); @@ -176,6 +213,9 @@ void QgsMeshElevationPropertiesWidget::modeChanged() case Qgis::MeshElevationMode::FromVertices: mStackedWidget->setCurrentWidget( mPageFromVertices ); break; + case Qgis::MeshElevationMode::FixedRangePerGroup: + mStackedWidget->setCurrentWidget( mPageFixedRangePerGroup ); + break; } } @@ -188,6 +228,68 @@ void QgsMeshElevationPropertiesWidget::onChanged() emit widgetChanged(); } +void QgsMeshElevationPropertiesWidget::calculateRangeByExpression( bool isUpper ) +{ + QgsExpressionContext expressionContext; + QgsExpressionContextScope *groupScope = new QgsExpressionContextScope(); + groupScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "group" ), 1, true, false, tr( "Group number" ) ) ); + const int groupIndex = mLayer->datasetGroupsIndexes().at( 0 ); + const QgsMeshDatasetGroupMetadata meta = mLayer->datasetGroupMetadata( groupIndex ); + groupScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "group_name" ), meta.name(), true, false, tr( "Group name" ) ) ); + + expressionContext.appendScope( groupScope ); + expressionContext.setHighlightedVariables( { QStringLiteral( "group" ), QStringLiteral( "group_name" )} ); + + QgsExpressionBuilderDialog dlg = QgsExpressionBuilderDialog( nullptr, isUpper ? mFixedRangeUpperExpression : mFixedRangeLowerExpression, this, QStringLiteral( "generic" ), expressionContext ); + + QList > groupChoices; + for ( int group = 0; group < mLayer->datasetGroupCount(); ++group ) + { + const int groupIndex = mLayer->datasetGroupsIndexes().at( group ); + const QgsMeshDatasetGroupMetadata meta = mLayer->datasetGroupMetadata( groupIndex ); + groupChoices << qMakePair( meta.name(), group ); + } + dlg.expressionBuilder()->setCustomPreviewGenerator( tr( "Group" ), groupChoices, [this]( const QVariant & value )-> QgsExpressionContext + { + return createExpressionContextForGroup( value.toInt() ); + } ); + + if ( dlg.exec() ) + { + if ( isUpper ) + mFixedRangeUpperExpression = dlg.expressionText(); + else + mFixedRangeLowerExpression = dlg.expressionText(); + + QgsExpression exp( dlg.expressionText() ); + exp.prepare( &expressionContext ); + for ( int group = 0; group < mLayer->datasetGroupCount(); ++group ) + { + groupScope->setVariable( QStringLiteral( "group" ), group + 1 ); + const int groupIndex = mLayer->datasetGroupsIndexes().at( group ); + const QgsMeshDatasetGroupMetadata meta = mLayer->datasetGroupMetadata( groupIndex ); + groupScope->setVariable( QStringLiteral( "group_name" ), meta.name() ); + + const QVariant res = exp.evaluate( &expressionContext ); + mFixedRangePerGroupModel->setData( mFixedRangePerGroupModel->index( group, isUpper ? 2 : 1 ), res, Qt::EditRole ); + } + } +} + +QgsExpressionContext QgsMeshElevationPropertiesWidget::createExpressionContextForGroup( int group ) const +{ + QgsExpressionContext context; + context.appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( mLayer ) ); + QgsExpressionContextScope *groupScope = new QgsExpressionContextScope(); + groupScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "group" ), group + 1, true, false, tr( "Group number" ) ) ); + const int groupIndex = mLayer->datasetGroupsIndexes().at( group ); + const QgsMeshDatasetGroupMetadata meta = mLayer->datasetGroupMetadata( groupIndex ); + groupScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "group_name" ), meta.name(), true, false, tr( "Group name" ) ) ); + context.appendScope( groupScope ); + context.setHighlightedVariables( { QStringLiteral( "group" ), QStringLiteral( "group_name" )} ); + return context; +} + // // QgsMeshElevationPropertiesWidgetFactory @@ -225,3 +327,232 @@ QString QgsMeshElevationPropertiesWidgetFactory::layerPropertiesPagePositionHint return QStringLiteral( "mOptsPage_Metadata" ); } + +// +// QgsMeshGroupFixedElevationRangeModel +// + +QgsMeshGroupFixedElevationRangeModel::QgsMeshGroupFixedElevationRangeModel( QObject *parent ) + : QAbstractItemModel( parent ) +{ + +} + +int QgsMeshGroupFixedElevationRangeModel::columnCount( const QModelIndex & ) const +{ + return 3; +} + +int QgsMeshGroupFixedElevationRangeModel::rowCount( const QModelIndex &parent ) const +{ + if ( parent.isValid() ) + return 0; + return mGroupCount; +} + +QModelIndex QgsMeshGroupFixedElevationRangeModel::index( int row, int column, const QModelIndex &parent ) const +{ + if ( hasIndex( row, column, parent ) ) + { + return createIndex( row, column, row ); + } + + return QModelIndex(); +} + +QModelIndex QgsMeshGroupFixedElevationRangeModel::parent( const QModelIndex &child ) const +{ + Q_UNUSED( child ) + return QModelIndex(); +} + +Qt::ItemFlags QgsMeshGroupFixedElevationRangeModel::flags( const QModelIndex &index ) const +{ + if ( !index.isValid() ) + return Qt::ItemFlags(); + + if ( index.row() < 0 || index.row() >= mGroupCount || index.column() < 0 || index.column() >= columnCount() ) + return Qt::ItemFlags(); + + switch ( index.column() ) + { + case 0: + return Qt::ItemFlag::ItemIsEnabled; + case 1: + case 2: + return Qt::ItemFlag::ItemIsEnabled | Qt::ItemFlag::ItemIsEditable | Qt::ItemFlag::ItemIsSelectable; + default: + break; + } + + return Qt::ItemFlags(); +} + +QVariant QgsMeshGroupFixedElevationRangeModel::data( const QModelIndex &index, int role ) const +{ + if ( !index.isValid() ) + return QVariant(); + + if ( index.row() < 0 || index.row() >= mGroupCount || index.column() < 0 || index.column() >= columnCount() ) + return QVariant(); + + const int group = index.row(); + const QgsDoubleRange range = mRanges.value( group ); + + switch ( role ) + { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::ToolTipRole: + { + switch ( index.column() ) + { + case 0: + return mGroupNames.value( group, QString::number( group ) ); + + case 1: + return range.lower() > std::numeric_limits< double >::lowest() ? range.lower() : QVariant(); + + case 2: + return range.upper() < std::numeric_limits< double >::max() ? range.upper() : QVariant(); + + default: + break; + } + break; + } + + case Qt::TextAlignmentRole: + { + switch ( index.column() ) + { + case 0: + return static_cast( Qt::AlignLeft | Qt::AlignVCenter ); + + case 1: + case 2: + return static_cast( Qt::AlignRight | Qt::AlignVCenter ); + default: + break; + } + break; + } + + default: + break; + } + return QVariant(); +} + +QVariant QgsMeshGroupFixedElevationRangeModel::headerData( int section, Qt::Orientation orientation, int role ) const +{ + if ( role == Qt::DisplayRole && orientation == Qt::Horizontal ) + { + switch ( section ) + { + case 0: + return tr( "Group" ); + case 1: + return tr( "Lower" ); + case 2: + return tr( "Upper" ); + default: + break; + } + } + return QAbstractItemModel::headerData( section, orientation, role ); +} + +bool QgsMeshGroupFixedElevationRangeModel::setData( const QModelIndex &index, const QVariant &value, int role ) +{ + if ( !index.isValid() ) + return false; + + if ( index.row() >= mGroupCount || index.row() < 0 ) + return false; + + const int group = index.row(); + const QgsDoubleRange range = mRanges.value( group ); + + switch ( role ) + { + case Qt::EditRole: + { + bool ok = false; + double newValue = value.toDouble( &ok ); + if ( !ok ) + return false; + + switch ( index.column() ) + { + case 1: + { + mRanges[group] = QgsDoubleRange( newValue, range.upper(), range.includeLower(), range.includeUpper() ); + emit dataChanged( index, index, QVector() << role ); + break; + } + + case 2: + mRanges[group] = QgsDoubleRange( range.lower(), newValue, range.includeLower(), range.includeUpper() ); + emit dataChanged( index, index, QVector() << role ); + break; + + default: + break; + } + return true; + } + + default: + break; + } + + return false; +} + +void QgsMeshGroupFixedElevationRangeModel::setLayerData( QgsMeshLayer *layer, const QMap &ranges ) +{ + beginResetModel(); + + mGroupCount = layer->datasetGroupCount(); + mRanges = ranges; + + mGroupNames.clear(); + for ( int group = 0; group < mGroupCount; ++group ) + { + const int groupIndex = layer->datasetGroupsIndexes().at( group ); + const QgsMeshDatasetGroupMetadata meta = layer->datasetGroupMetadata( groupIndex ); + mGroupNames[group] = meta.name(); + } + + endResetModel(); +} + + +// +// QgsFixedElevationRangeDelegate +// + +QgsMeshFixedElevationRangeDelegate::QgsMeshFixedElevationRangeDelegate( QObject *parent ) + : QStyledItemDelegate( parent ) +{ + +} + +QWidget *QgsMeshFixedElevationRangeDelegate::createEditor( QWidget *parent, const QStyleOptionViewItem &, const QModelIndex & ) const +{ + QgsDoubleSpinBox *spin = new QgsDoubleSpinBox( parent ); + spin->setDecimals( 4 ); + spin->setMinimum( -9999999998.0 ); + spin->setMaximum( 9999999999.0 ); + spin->setShowClearButton( false ); + return spin; +} + +void QgsMeshFixedElevationRangeDelegate::setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const +{ + if ( QgsDoubleSpinBox *spin = qobject_cast< QgsDoubleSpinBox * >( editor ) ) + { + model->setData( index, spin->value() ); + } +} diff --git a/src/app/mesh/qgsmeshelevationpropertieswidget.h b/src/app/mesh/qgsmeshelevationpropertieswidget.h index c55e406e7d4e8..a5af9b443ad1c 100644 --- a/src/app/mesh/qgsmeshelevationpropertieswidget.h +++ b/src/app/mesh/qgsmeshelevationpropertieswidget.h @@ -19,10 +19,53 @@ #include "qgsmaplayerconfigwidget.h" #include "qgsmaplayerconfigwidgetfactory.h" +#include + #include "ui_qgsmeshelevationpropertieswidgetbase.h" class QgsMeshLayer; +class QgsMeshGroupFixedElevationRangeModel : public QAbstractItemModel +{ + Q_OBJECT + + public: + + QgsMeshGroupFixedElevationRangeModel( QObject *parent ); + int columnCount( const QModelIndex &parent = QModelIndex() ) const override; + int rowCount( const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex index( int row, int column, const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex parent( const QModelIndex &child ) const override; + Qt::ItemFlags flags( const QModelIndex &index ) const override; + QVariant data( const QModelIndex &index, int role ) const override; + QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const override; + bool setData( const QModelIndex &index, const QVariant &value, int role ) override; + + void setLayerData( QgsMeshLayer *layer, const QMap &ranges ); + QMap rangeData() const { return mRanges; } + + private: + + int mGroupCount = 0; + QMap mGroupNames; + QMap mRanges; +}; + + +class QgsMeshFixedElevationRangeDelegate : public QStyledItemDelegate +{ + Q_OBJECT + + public: + + QgsMeshFixedElevationRangeDelegate( QObject *parent ); + + protected: + QWidget *createEditor( QWidget *parent, const QStyleOptionViewItem & /*option*/, const QModelIndex &index ) const override; + void setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const override; + +}; + class QgsMeshElevationPropertiesWidget : public QgsMapLayerConfigWidget, private Ui::QgsMeshElevationPropertiesWidgetBase { Q_OBJECT @@ -38,11 +81,16 @@ class QgsMeshElevationPropertiesWidget : public QgsMapLayerConfigWidget, private private slots: void modeChanged(); void onChanged(); + void calculateRangeByExpression( bool isUpper ); private: + QgsExpressionContext createExpressionContextForGroup( int group ) const; QgsMeshLayer *mLayer = nullptr; bool mBlockUpdates = false; + QgsMeshGroupFixedElevationRangeModel *mFixedRangePerGroupModel = nullptr; + QString mFixedRangeLowerExpression = QStringLiteral( "@group" ); + QString mFixedRangeUpperExpression = QStringLiteral( "@group" ); }; diff --git a/src/core/mesh/qgsmeshlayer.cpp b/src/core/mesh/qgsmeshlayer.cpp index 4982232dd2594..9538c31624397 100644 --- a/src/core/mesh/qgsmeshlayer.cpp +++ b/src/core/mesh/qgsmeshlayer.cpp @@ -797,24 +797,24 @@ void QgsMeshLayer::applyClassificationOnScalarSettings( const QgsMeshDatasetGrou } } -QgsMeshDatasetIndex QgsMeshLayer::activeScalarDatasetAtTime( const QgsDateTimeRange &timeRange ) const +QgsMeshDatasetIndex QgsMeshLayer::activeScalarDatasetAtTime( const QgsDateTimeRange &timeRange, int group ) const { QGIS_PROTECT_QOBJECT_THREAD_ACCESS if ( mTemporalProperties->isActive() ) - return datasetIndexAtTime( timeRange, mRendererSettings.activeScalarDatasetGroup() ); + return datasetIndexAtTime( timeRange, group >= 0 ? group : mRendererSettings.activeScalarDatasetGroup() ); else - return QgsMeshDatasetIndex( mRendererSettings.activeScalarDatasetGroup(), mStaticScalarDatasetIndex ); + return QgsMeshDatasetIndex( group >= 0 ? group : mRendererSettings.activeScalarDatasetGroup(), mStaticScalarDatasetIndex ); } -QgsMeshDatasetIndex QgsMeshLayer::activeVectorDatasetAtTime( const QgsDateTimeRange &timeRange ) const +QgsMeshDatasetIndex QgsMeshLayer::activeVectorDatasetAtTime( const QgsDateTimeRange &timeRange, int group ) const { QGIS_PROTECT_QOBJECT_THREAD_ACCESS if ( mTemporalProperties->isActive() ) - return datasetIndexAtTime( timeRange, mRendererSettings.activeVectorDatasetGroup() ); + return datasetIndexAtTime( timeRange, group >= 0 ? group : mRendererSettings.activeVectorDatasetGroup() ); else - return QgsMeshDatasetIndex( mRendererSettings.activeVectorDatasetGroup(), mStaticVectorDatasetIndex ); + return QgsMeshDatasetIndex( group >= 0 ? group : mRendererSettings.activeVectorDatasetGroup(), mStaticVectorDatasetIndex ); } void QgsMeshLayer::fillNativeMesh() @@ -903,11 +903,11 @@ int QgsMeshLayer::closestEdge( const QgsPointXY &point, double searchRadius, Qgs return selectedIndex; } -QgsMeshDatasetIndex QgsMeshLayer::staticVectorDatasetIndex() const +QgsMeshDatasetIndex QgsMeshLayer::staticVectorDatasetIndex( int group ) const { QGIS_PROTECT_QOBJECT_THREAD_ACCESS - return QgsMeshDatasetIndex( mRendererSettings.activeVectorDatasetGroup(), mStaticVectorDatasetIndex ); + return QgsMeshDatasetIndex( group >= 0 ? group : mRendererSettings.activeVectorDatasetGroup(), mStaticVectorDatasetIndex ); } void QgsMeshLayer::setReferenceTime( const QDateTime &referenceTime ) @@ -1545,11 +1545,11 @@ QList QgsMeshLayer::selectFacesByExpression( QgsExpression expression ) return ret; } -QgsMeshDatasetIndex QgsMeshLayer::staticScalarDatasetIndex() const +QgsMeshDatasetIndex QgsMeshLayer::staticScalarDatasetIndex( int group ) const { QGIS_PROTECT_QOBJECT_THREAD_ACCESS - return QgsMeshDatasetIndex( mRendererSettings.activeScalarDatasetGroup(), mStaticScalarDatasetIndex ); + return QgsMeshDatasetIndex( group >= 0 ? group : mRendererSettings.activeScalarDatasetGroup(), mStaticScalarDatasetIndex ); } void QgsMeshLayer::setStaticVectorDatasetIndex( const QgsMeshDatasetIndex &staticVectorDatasetIndex ) diff --git a/src/core/mesh/qgsmeshlayer.h b/src/core/mesh/qgsmeshlayer.h index 75b002b5f1dfa..2da422e3b431c 100644 --- a/src/core/mesh/qgsmeshlayer.h +++ b/src/core/mesh/qgsmeshlayer.h @@ -608,27 +608,27 @@ class CORE_EXPORT QgsMeshLayer : public QgsMapLayer, public QgsAbstractProfileSo * Returns dataset index from active scalar group depending on the time range. * If the temporal properties is not active, return the static dataset * - * \param timeRange the time range - * \returns dataset index + * Since QGIS 3.38, the \a group argument can be used to specify a fixed group + * to use. If this is not specified, then the active group from the layer's renderer will be used. * * \note the returned dataset index depends on the matching method, see setTemporalMatchingMethod() * * \since QGIS 3.14 */ - QgsMeshDatasetIndex activeScalarDatasetAtTime( const QgsDateTimeRange &timeRange ) const; + QgsMeshDatasetIndex activeScalarDatasetAtTime( const QgsDateTimeRange &timeRange, int group = -1 ) const; /** * Returns dataset index from active vector group depending on the time range * If the temporal properties is not active, return the static dataset * - * \param timeRange the time range - * \returns dataset index + * Since QGIS 3.38, the \a group argument can be used to specify a fixed group + * to use. If this is not specified, then the active group from the layer's renderer will be used. * * \note the returned dataset index depends on the matching method, see setTemporalMatchingMethod() * * \since QGIS 3.14 */ - QgsMeshDatasetIndex activeVectorDatasetAtTime( const QgsDateTimeRange &timeRange ) const; + QgsMeshDatasetIndex activeVectorDatasetAtTime( const QgsDateTimeRange &timeRange, int group = -1 ) const; /** * Sets the static scalar dataset index that is rendered if the temporal properties is not active @@ -649,18 +649,24 @@ class CORE_EXPORT QgsMeshLayer : public QgsMapLayer, public QgsAbstractProfileSo void setStaticVectorDatasetIndex( const QgsMeshDatasetIndex &staticVectorDatasetIndex ) SIP_SKIP; /** - * Returns the static scalar dataset index that is rendered if the temporal properties is not active + * Returns the static scalar dataset index that is rendered if the temporal properties is not active. + * + * Since QGIS 3.38, the \a group argument can be used to specify a fixed group + * to use. If this is not specified, then the active group from the layer's renderer will be used. * * \since QGIS 3.14 */ - QgsMeshDatasetIndex staticScalarDatasetIndex() const; + QgsMeshDatasetIndex staticScalarDatasetIndex( int group = -1 ) const; /** - * Returns the static vector dataset index that is rendered if the temporal properties is not active + * Returns the static vector dataset index that is rendered if the temporal properties is not active. + * + * Since QGIS 3.38, the \a group argument can be used to specify a fixed group + * to use. If this is not specified, then the active group from the layer's renderer will be used. * * \since QGIS 3.14 */ - QgsMeshDatasetIndex staticVectorDatasetIndex() const; + QgsMeshDatasetIndex staticVectorDatasetIndex( int group = -1 ) const; /** * Sets the reference time of the layer diff --git a/src/core/mesh/qgsmeshlayerelevationproperties.cpp b/src/core/mesh/qgsmeshlayerelevationproperties.cpp index 2ee3838f55eca..b988c74342ac4 100644 --- a/src/core/mesh/qgsmeshlayerelevationproperties.cpp +++ b/src/core/mesh/qgsmeshlayerelevationproperties.cpp @@ -59,6 +59,23 @@ QDomElement QgsMeshLayerElevationProperties::writeXml( QDomElement &parentElemen element.setAttribute( QStringLiteral( "includeUpper" ), mFixedRange.includeUpper() ? "1" : "0" ); break; + case Qgis::MeshElevationMode::FixedRangePerGroup: + { + QDomElement ranges = document.createElement( QStringLiteral( "ranges" ) ); + for ( auto it = mRangePerGroup.constBegin(); it != mRangePerGroup.constEnd(); ++it ) + { + QDomElement range = document.createElement( QStringLiteral( "range" ) ); + range.setAttribute( QStringLiteral( "group" ), it.key() ); + range.setAttribute( QStringLiteral( "lower" ), qgsDoubleToString( it.value().lower() ) ); + range.setAttribute( QStringLiteral( "upper" ), qgsDoubleToString( it.value().upper() ) ); + range.setAttribute( QStringLiteral( "includeLower" ), it.value().includeLower() ? "1" : "0" ); + range.setAttribute( QStringLiteral( "includeUpper" ), it.value().includeUpper() ? "1" : "0" ); + ranges.appendChild( range ); + } + element.appendChild( ranges ); + break; + } + case Qgis::MeshElevationMode::FromVertices: break; } @@ -98,6 +115,25 @@ bool QgsMeshLayerElevationProperties::readXml( const QDomElement &element, const mFixedRange = QgsDoubleRange( lower, upper, includeLower, includeUpper ); break; } + + case Qgis::MeshElevationMode::FixedRangePerGroup: + { + mRangePerGroup.clear(); + + const QDomNodeList ranges = elevationElement.firstChildElement( QStringLiteral( "ranges" ) ).childNodes(); + for ( int i = 0; i < ranges.size(); ++i ) + { + const QDomElement rangeElement = ranges.at( i ).toElement(); + const int group = rangeElement.attribute( QStringLiteral( "group" ) ).toInt(); + const double lower = rangeElement.attribute( QStringLiteral( "lower" ) ).toDouble(); + const double upper = rangeElement.attribute( QStringLiteral( "upper" ) ).toDouble(); + const bool includeLower = rangeElement.attribute( QStringLiteral( "includeLower" ) ).toInt(); + const bool includeUpper = rangeElement.attribute( QStringLiteral( "includeUpper" ) ).toInt(); + mRangePerGroup.insert( group, QgsDoubleRange( lower, upper, includeLower, includeUpper ) ); + } + break; + } + case Qgis::MeshElevationMode::FromVertices: break; } @@ -126,6 +162,15 @@ QString QgsMeshLayerElevationProperties::htmlSummary() const properties << tr( "Elevation range: %1 to %2" ).arg( mFixedRange.lower() ).arg( mFixedRange.upper() ); break; + case Qgis::MeshElevationMode::FixedRangePerGroup: + { + for ( auto it = mRangePerGroup.constBegin(); it != mRangePerGroup.constEnd(); ++it ) + { + properties << tr( "Elevation for group %1: %2 to %3" ).arg( it.key() ).arg( it.value().lower() ).arg( it.value().upper() ); + } + break; + } + case Qgis::MeshElevationMode::FromVertices: properties << tr( "Scale: %1" ).arg( mZScale ); properties << tr( "Offset: %1" ).arg( mZOffset ); @@ -143,6 +188,7 @@ QgsMeshLayerElevationProperties *QgsMeshLayerElevationProperties::clone() const res->setProfileSymbology( mSymbology ); res->setElevationLimit( mElevationLimit ); res->setFixedRange( mFixedRange ); + res->setFixedRangePerGroup( mRangePerGroup ); res->copyCommonProperties( this ); return res.release(); } @@ -154,6 +200,16 @@ bool QgsMeshLayerElevationProperties::isVisibleInZRange( const QgsDoubleRange &r case Qgis::MeshElevationMode::FixedElevationRange: return mFixedRange.overlaps( range ); + case Qgis::MeshElevationMode::FixedRangePerGroup: + { + for ( auto it = mRangePerGroup.constBegin(); it != mRangePerGroup.constEnd(); ++it ) + { + if ( it.value().overlaps( range ) ) + return true; + } + return false; + } + case Qgis::MeshElevationMode::FromVertices: // TODO -- test actual mesh z range return true; @@ -168,6 +224,36 @@ QgsDoubleRange QgsMeshLayerElevationProperties::calculateZRange( QgsMapLayer * ) case Qgis::MeshElevationMode::FixedElevationRange: return mFixedRange; + case Qgis::MeshElevationMode::FixedRangePerGroup: + { + double lower = std::numeric_limits< double >::max(); + double upper = std::numeric_limits< double >::min(); + bool includeLower = true; + bool includeUpper = true; + for ( auto it = mRangePerGroup.constBegin(); it != mRangePerGroup.constEnd(); ++it ) + { + if ( it.value().lower() < lower ) + { + lower = it.value().lower(); + includeLower = it.value().includeLower(); + } + else if ( !includeLower && it.value().lower() == lower && it.value().includeLower() ) + { + includeLower = true; + } + if ( it.value().upper() > upper ) + { + upper = it.value().upper(); + includeUpper = it.value().includeUpper(); + } + else if ( !includeUpper && it.value().upper() == upper && it.value().includeUpper() ) + { + includeUpper = true; + } + } + return QgsDoubleRange( lower, upper, includeLower, includeUpper ); + } + case Qgis::MeshElevationMode::FromVertices: // TODO -- determine actual z range from mesh statistics return QgsDoubleRange(); @@ -187,6 +273,7 @@ QgsMapLayerElevationProperties::Flags QgsMeshLayerElevationProperties::flags() c case Qgis::MeshElevationMode::FixedElevationRange: return QgsMapLayerElevationProperties::Flag::FlagDontInvalidateCachedRendersWhenRangeChanges; + case Qgis::MeshElevationMode::FixedRangePerGroup: case Qgis::MeshElevationMode::FromVertices: break; } @@ -221,6 +308,20 @@ void QgsMeshLayerElevationProperties::setFixedRange( const QgsDoubleRange &range emit changed(); } +QMap QgsMeshLayerElevationProperties::fixedRangePerGroup() const +{ + return mRangePerGroup; +} + +void QgsMeshLayerElevationProperties::setFixedRangePerGroup( const QMap &ranges ) +{ + if ( ranges == mRangePerGroup ) + return; + + mRangePerGroup = ranges; + emit changed(); +} + QgsLineSymbol *QgsMeshLayerElevationProperties::profileLineSymbol() const { return mProfileLineSymbol.get(); diff --git a/src/core/mesh/qgsmeshlayerelevationproperties.h b/src/core/mesh/qgsmeshlayerelevationproperties.h index 88be2fb7c6eb6..319f01ba3cf2f 100644 --- a/src/core/mesh/qgsmeshlayerelevationproperties.h +++ b/src/core/mesh/qgsmeshlayerelevationproperties.h @@ -22,6 +22,7 @@ #include "qgis_core.h" #include "qgis_sip.h" #include "qgsmaplayerelevationproperties.h" +#include "qgsmeshdataset.h" #include "qgis.h" class QgsLineSymbol; @@ -97,6 +98,30 @@ class CORE_EXPORT QgsMeshLayerElevationProperties : public QgsMapLayerElevationP */ void setFixedRange( const QgsDoubleRange &range ); + /** + * Returns the fixed elevation range for each group. + * + * \note This is only considered when mode() is Qgis::MeshElevationMode::FixedRangePerGroup. + * + * \note When a fixed range is set any zOffset() and zScale() is ignored. + * + * \see setFixedRangePerGroup() + * \since QGIS 3.38 + */ + QMap fixedRangePerGroup() const; + + /** + * Sets the fixed elevation range for each group. + * + * \note This is only considered when mode() is Qgis::MeshElevationMode::FixedRangePerGroup. + * + * \note When a fixed range is set any zOffset() and zScale() is ignored. + * + * \see fixedRangePerGroup() + * \since QGIS 3.38 + */ + void setFixedRangePerGroup( const QMap &ranges ); + /** * Returns the line symbol used to render the mesh profile in elevation profile plots. * @@ -180,6 +205,8 @@ class CORE_EXPORT QgsMeshLayerElevationProperties : public QgsMapLayerElevationP double mElevationLimit = std::numeric_limits< double >::quiet_NaN(); QgsDoubleRange mFixedRange; + + QMap< int, QgsDoubleRange > mRangePerGroup; }; #endif // QGSMESHLAYERELEVATIONPROPERTIES_H diff --git a/src/core/mesh/qgsmeshlayerrenderer.cpp b/src/core/mesh/qgsmeshlayerrenderer.cpp index 58d327272c774..655d10e12a97a 100644 --- a/src/core/mesh/qgsmeshlayerrenderer.cpp +++ b/src/core/mesh/qgsmeshlayerrenderer.cpp @@ -74,20 +74,6 @@ QgsMeshLayerRenderer::QgsMeshLayerRenderer( mNativeMesh = *( layer->nativeMesh() ); mLayerExtent = layer->extent(); - // copy triangular mesh - copyTriangularMeshes( layer, context ); - - // copy datasets - copyScalarDatasetValues( layer ); - copyVectorDatasetValues( layer ); - - calculateOutputSize(); - - QSet attrs; - prepareLabeling( layer, attrs ); - - mClippingRegions = QgsMapClippingUtils::collectClippingRegionsForLayer( *renderContext(), layer ); - if ( layer->elevationProperties() && layer->elevationProperties()->hasElevation() ) { QgsMeshLayerElevationProperties *elevProp = qobject_cast( layer->elevationProperties() ); @@ -110,10 +96,50 @@ QgsMeshLayerRenderer::QgsMeshLayerRenderer( // TODO -- filtering by mesh z values is not currently implemented break; } + + case Qgis::MeshElevationMode::FixedRangePerGroup: + { + // find the top-most group which matches the map range + int currentMatchingGroup = -1; + QgsDoubleRange currentMatchingRange; + const QMap rangePerGroup = elevProp->fixedRangePerGroup(); + for ( auto it = rangePerGroup.constBegin(); it != rangePerGroup.constEnd(); ++it ) + { + if ( it.value().overlaps( context.zRange() ) ) + { + if ( currentMatchingRange.isInfinite() + || ( it.value().includeUpper() && it.value().upper() >= currentMatchingRange.upper() ) + || ( !currentMatchingRange.includeUpper() && it.value().upper() >= currentMatchingRange.upper() ) ) + { + currentMatchingGroup = it.key(); + currentMatchingRange = it.value(); + } + } + } + if ( currentMatchingGroup >= 0 ) + { + mRendererSettings.setActiveScalarDatasetGroup( currentMatchingGroup ); + mRendererSettings.setActiveVectorDatasetGroup( currentMatchingGroup ); + } + } } } } + // copy triangular mesh + copyTriangularMeshes( layer, context ); + + // copy datasets + copyScalarDatasetValues( layer ); + copyVectorDatasetValues( layer ); + + calculateOutputSize(); + + QSet attrs; + prepareLabeling( layer, attrs ); + + mClippingRegions = QgsMapClippingUtils::collectClippingRegionsForLayer( *renderContext(), layer ); + mPreparationTime = timer.elapsed(); } @@ -154,9 +180,9 @@ void QgsMeshLayerRenderer::copyScalarDatasetValues( QgsMeshLayer *layer ) { QgsMeshDatasetIndex datasetIndex; if ( renderContext()->isTemporal() ) - datasetIndex = layer->activeScalarDatasetAtTime( renderContext()->temporalRange() ); + datasetIndex = layer->activeScalarDatasetAtTime( renderContext()->temporalRange(), mRendererSettings.activeScalarDatasetGroup() ); else - datasetIndex = layer->staticScalarDatasetIndex(); + datasetIndex = layer->staticScalarDatasetIndex( mRendererSettings.activeScalarDatasetGroup() ); // Find out if we can use cache up to date. If yes, use it and return const int datasetGroupCount = layer->datasetGroupCount(); @@ -255,9 +281,9 @@ void QgsMeshLayerRenderer::copyVectorDatasetValues( QgsMeshLayer *layer ) { QgsMeshDatasetIndex datasetIndex; if ( renderContext()->isTemporal() ) - datasetIndex = layer->activeVectorDatasetAtTime( renderContext()->temporalRange() ); + datasetIndex = layer->activeVectorDatasetAtTime( renderContext()->temporalRange(), mRendererSettings.activeVectorDatasetGroup() ); else - datasetIndex = layer->staticVectorDatasetIndex(); + datasetIndex = layer->staticVectorDatasetIndex( mRendererSettings.activeVectorDatasetGroup() ); // Find out if we can use cache up to date. If yes, use it and return const int datasetGroupCount = layer->datasetGroupCount(); diff --git a/src/core/qgis.h b/src/core/qgis.h index 50e7c39eb6298..e1442f858dc37 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -3335,7 +3335,8 @@ class CORE_EXPORT Qgis enum class MeshElevationMode : int { FixedElevationRange = 0, //!< Layer has a fixed elevation range - FromVertices = 1 //!< Elevation should be taken from mesh vertices + FromVertices = 1, //!< Elevation should be taken from mesh vertices + FixedRangePerGroup = 2, //!< Layer has a fixed (manually specified) elevation range per group }; Q_ENUM( MeshElevationMode ) diff --git a/src/ui/mesh/qgsmeshelevationpropertieswidgetbase.ui b/src/ui/mesh/qgsmeshelevationpropertieswidgetbase.ui index 5dbb914e0da02..cf4175997d394 100644 --- a/src/ui/mesh/qgsmeshelevationpropertieswidgetbase.ui +++ b/src/ui/mesh/qgsmeshelevationpropertieswidgetbase.ui @@ -36,6 +36,143 @@ + + + + Profile Chart Appearance + + + + + + + + + Style + + + + + + + + 0 + 0 + + + + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Line style + + + + + + + + 0 + 0 + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Limit + + + + + + + Fill style + + + + + + + 6 + + + -99999.000000000000000 + + + 99999.000000000000000 + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + @@ -48,7 +185,7 @@ QFrame::NoFrame - 1 + 2 @@ -198,145 +335,68 @@
- - - - - - Profile Chart Appearance - - - - - - - - - Style - - - - - - - - 0 - 0 - - - - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Line style - - - - - - - - 0 - 0 - - - - - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Limit - - - - - - - Fill style - - - - - - - 6 - - - -99999.000000000000000 - - - 99999.000000000000000 - - - - - - - - 0 - 0 - - - - - - - - + + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">Each group in the mesh layer is associated with a fixed elevation range.</span></p><p>This mode can be used when a layer has elevation data exposed through different dataset groups.</p></body></html> + + + true + - - - + + + + + + + + 0 + + + + + Qt::Horizontal + + + + 494 + 20 + + + + + + + + ... + + + + :/images/themes/default/mIconExpression.svg:/images/themes/default/mIconExpression.svg + + + QToolButton::MenuButtonPopup + + + false + + + + + +
+ - - - - Qt::Vertical - - - - 20 - 40 - - - - @@ -357,6 +417,8 @@ 1 - + + + diff --git a/tests/src/python/test_qgsmeshlayerrenderer.py b/tests/src/python/test_qgsmeshlayerrenderer.py index b8e44a14afe1e..2668064018f74 100644 --- a/tests/src/python/test_qgsmeshlayerrenderer.py +++ b/tests/src/python/test_qgsmeshlayerrenderer.py @@ -81,6 +81,72 @@ def test_render_fixed_elevation_range_with_z_range_filter(self): map_settings) ) + def test_render_fixed_range_per_group_with_z_range_filter(self): + """ + Test rendering a mesh with a fixed range per group when + map settings has a z range filter + """ + layer = QgsMeshLayer( + self.get_test_data_path( + 'mesh/netcdf_parent_quantity.nc').as_posix(), + 'mesh', + 'mdal' + ) + self.assertTrue(layer.isValid()) + + # set layer as elevation enabled + layer.elevationProperties().setMode( + Qgis.MeshElevationMode.FixedRangePerGroup + ) + layer.elevationProperties().setFixedRangePerGroup( + {1: QgsDoubleRange(33, 38), + 2: QgsDoubleRange(35, 40), + 3: QgsDoubleRange(40, 48)} + ) + + map_settings = QgsMapSettings() + map_settings.setOutputSize(QSize(400, 400)) + map_settings.setOutputDpi(96) + map_settings.setDestinationCrs(layer.crs()) + map_settings.setExtent(layer.extent()) + map_settings.setLayers([layer]) + + # no filter on map settings + map_settings.setZRange(QgsDoubleRange()) + self.assertTrue( + self.render_map_settings_check( + 'No Z range filter on map settings, elevation range per group', + 'elevation_range_per_group_no_filter', + map_settings) + ) + + # map settings range matches group 3 only + map_settings.setZRange(QgsDoubleRange(40.5, 49.5)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings matches group 3 only', + 'elevation_range_per_group_match3', + map_settings) + ) + + # map settings range matches group 1 and 2 + map_settings.setZRange(QgsDoubleRange(33, 39.5)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings matches group 1 and 2', + 'elevation_range_per_group_match1and2', + map_settings) + ) + + # map settings range excludes layer's range + map_settings.setZRange(QgsDoubleRange(130, 135)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings outside of layer group ranges', + 'fixed_elevation_range_excluded', + map_settings) + ) + if __name__ == '__main__': unittest.main() diff --git a/tests/testdata/control_images/mesh/expected_elevation_range_per_group_match1and2/expected_elevation_range_per_group_match1and2.png b/tests/testdata/control_images/mesh/expected_elevation_range_per_group_match1and2/expected_elevation_range_per_group_match1and2.png new file mode 100644 index 0000000000000000000000000000000000000000..a318096cbc56978336966ec8c52e1e45b369e96b GIT binary patch literal 471523 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 GKly(?tDV*W literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/mesh/expected_elevation_range_per_group_no_filter/expected_elevation_range_per_group_no_filter.png b/tests/testdata/control_images/mesh/expected_elevation_range_per_group_no_filter/expected_elevation_range_per_group_no_filter.png new file mode 100644 index 0000000000000000000000000000000000000000..3e98a42c40f8ecd79a9ae6a593948d7d7449cc91 GIT binary patch literal 471523 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 O?6?2SKllg#mH!_tMpLN( literal 0 HcmV?d00001 From 0ff90a56985f7723972e3de099002981554e1709 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 16 Apr 2024 13:05:30 +1000 Subject: [PATCH 093/102] When matching render elevation range to layer ranges, only consider datasets from the same parent group --- .../auto_generated/mesh/qgsmeshdataset.sip.in | 7 +++ .../auto_generated/mesh/qgsmeshdataset.sip.in | 7 +++ src/core/mesh/qgsmeshdataset.cpp | 17 ++++++- src/core/mesh/qgsmeshdataset.h | 8 ++++ src/core/mesh/qgsmeshlayerrenderer.cpp | 45 +++++++++++++------ src/core/mesh/qgsmeshlayerutils.cpp | 15 +++++++ src/core/mesh/qgsmeshlayerutils.h | 8 ++++ 7 files changed, 93 insertions(+), 14 deletions(-) diff --git a/python/PyQt6/core/auto_generated/mesh/qgsmeshdataset.sip.in b/python/PyQt6/core/auto_generated/mesh/qgsmeshdataset.sip.in index 5df433842ae5b..f25cc96e33025 100644 --- a/python/PyQt6/core/auto_generated/mesh/qgsmeshdataset.sip.in +++ b/python/PyQt6/core/auto_generated/mesh/qgsmeshdataset.sip.in @@ -431,6 +431,13 @@ Constructs a valid metadata object QString name() const; %Docstring Returns name of the dataset group +%End + + QString parentGroup() const; +%Docstring +Returns the name of the dataset's parent group. + +.. versionadded:: 3.38 %End QString uri() const; diff --git a/python/core/auto_generated/mesh/qgsmeshdataset.sip.in b/python/core/auto_generated/mesh/qgsmeshdataset.sip.in index 16f891990df01..7ad800e693e3c 100644 --- a/python/core/auto_generated/mesh/qgsmeshdataset.sip.in +++ b/python/core/auto_generated/mesh/qgsmeshdataset.sip.in @@ -431,6 +431,13 @@ Constructs a valid metadata object QString name() const; %Docstring Returns name of the dataset group +%End + + QString parentGroup() const; +%Docstring +Returns the name of the dataset's parent group. + +.. versionadded:: 3.38 %End QString uri() const; diff --git a/src/core/mesh/qgsmeshdataset.cpp b/src/core/mesh/qgsmeshdataset.cpp index 56d5dc6830f59..ed2667df8f772 100644 --- a/src/core/mesh/qgsmeshdataset.cpp +++ b/src/core/mesh/qgsmeshdataset.cpp @@ -19,6 +19,8 @@ #include "qgsmeshdataprovider.h" #include "qgsrectangle.h" #include "qgis.h" +#include +#include QgsMeshDatasetIndex::QgsMeshDatasetIndex( int group, int dataset ) : mGroupIndex( group ), mDatasetIndex( dataset ) @@ -142,6 +144,13 @@ QgsMeshDatasetGroupMetadata::QgsMeshDatasetGroupMetadata( const QString &name, , mReferenceTime( referenceTime ) , mIsTemporal( isTemporal ) { + const thread_local QRegularExpression parentGroupNameRegex( QStringLiteral( "^(.*):.*?$" ) ); + + const QRegularExpressionMatch parentGroupMatch = parentGroupNameRegex.match( mName ); + if ( parentGroupMatch.hasMatch() ) + { + mParentGroupName = parentGroupMatch.captured( 1 ); + } } QMap QgsMeshDatasetGroupMetadata::extraOptions() const @@ -169,7 +178,13 @@ QString QgsMeshDatasetGroupMetadata::name() const return mName; } -QgsMeshDatasetGroupMetadata::DataType QgsMeshDatasetGroupMetadata::dataType() const +QString QgsMeshDatasetGroupMetadata::parentGroup() const +{ + return mParentGroupName; +} + +QgsMeshDatasetGroupMetadata::DataType +QgsMeshDatasetGroupMetadata::dataType() const { return mDataType; } diff --git a/src/core/mesh/qgsmeshdataset.h b/src/core/mesh/qgsmeshdataset.h index ee0d34c9df934..6218fb4cb7c5f 100644 --- a/src/core/mesh/qgsmeshdataset.h +++ b/src/core/mesh/qgsmeshdataset.h @@ -397,6 +397,13 @@ class CORE_EXPORT QgsMeshDatasetGroupMetadata */ QString name() const; + /** + * Returns the name of the dataset's parent group. + * + * \since QGIS 3.38 + */ + QString parentGroup() const; + /** * Returns the uri of the source * @@ -457,6 +464,7 @@ class CORE_EXPORT QgsMeshDatasetGroupMetadata private: QString mName; + QString mParentGroupName; QString mUri; bool mIsScalar = false; DataType mDataType = DataType::DataOnFaces; diff --git a/src/core/mesh/qgsmeshlayerrenderer.cpp b/src/core/mesh/qgsmeshlayerrenderer.cpp index 655d10e12a97a..d494a133c5d52 100644 --- a/src/core/mesh/qgsmeshlayerrenderer.cpp +++ b/src/core/mesh/qgsmeshlayerrenderer.cpp @@ -99,28 +99,47 @@ QgsMeshLayerRenderer::QgsMeshLayerRenderer( case Qgis::MeshElevationMode::FixedRangePerGroup: { - // find the top-most group which matches the map range - int currentMatchingGroup = -1; - QgsDoubleRange currentMatchingRange; + // find the top-most group which matches the map range and parent group + int currentMatchingVectorGroup = -1; + int currentMatchingScalarGroup = -1; + QgsDoubleRange currentMatchingVectorRange; + QgsDoubleRange currentMatchingScalarRange; + const QMap rangePerGroup = elevProp->fixedRangePerGroup(); + + const int activeVectorDatasetGroup = mRendererSettings.activeVectorDatasetGroup(); + const int activeScalarDatasetGroup = mRendererSettings.activeScalarDatasetGroup(); + for ( auto it = rangePerGroup.constBegin(); it != rangePerGroup.constEnd(); ++it ) { if ( it.value().overlaps( context.zRange() ) ) { - if ( currentMatchingRange.isInfinite() - || ( it.value().includeUpper() && it.value().upper() >= currentMatchingRange.upper() ) - || ( !currentMatchingRange.includeUpper() && it.value().upper() >= currentMatchingRange.upper() ) ) + const bool matchesVectorParentGroup = QgsMeshLayerUtils::haveSameParentGroup( layer, QgsMeshDatasetIndex( activeVectorDatasetGroup ), QgsMeshDatasetIndex( it.key() ) ); + const bool matchesScalarParentGroup = QgsMeshLayerUtils::haveSameParentGroup( layer, QgsMeshDatasetIndex( activeScalarDatasetGroup ), QgsMeshDatasetIndex( it.key() ) ); + + if ( matchesVectorParentGroup && ( + currentMatchingVectorRange.isInfinite() + || ( it.value().includeUpper() && it.value().upper() >= currentMatchingVectorRange.upper() ) + || ( !currentMatchingVectorRange.includeUpper() && it.value().upper() >= currentMatchingVectorRange.upper() ) ) ) { - currentMatchingGroup = it.key(); - currentMatchingRange = it.value(); + currentMatchingVectorGroup = it.key(); + currentMatchingVectorRange = it.value(); + } + + if ( matchesScalarParentGroup && ( + currentMatchingScalarRange.isInfinite() + || ( it.value().includeUpper() && it.value().upper() >= currentMatchingScalarRange.upper() ) + || ( !currentMatchingScalarRange.includeUpper() && it.value().upper() >= currentMatchingScalarRange.upper() ) ) ) + { + currentMatchingScalarGroup = it.key(); + currentMatchingScalarRange = it.value(); } } } - if ( currentMatchingGroup >= 0 ) - { - mRendererSettings.setActiveScalarDatasetGroup( currentMatchingGroup ); - mRendererSettings.setActiveVectorDatasetGroup( currentMatchingGroup ); - } + if ( currentMatchingVectorGroup >= 0 ) + mRendererSettings.setActiveVectorDatasetGroup( currentMatchingVectorGroup ); + if ( currentMatchingScalarGroup >= 0 ) + mRendererSettings.setActiveScalarDatasetGroup( currentMatchingScalarGroup ); } } } diff --git a/src/core/mesh/qgsmeshlayerutils.cpp b/src/core/mesh/qgsmeshlayerutils.cpp index e6c00b840f351..a5398f0abbcec 100644 --- a/src/core/mesh/qgsmeshlayerutils.cpp +++ b/src/core/mesh/qgsmeshlayerutils.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include "qgsmeshlayerutils.h" #include "qgsmeshtimesettings.h" @@ -702,4 +704,17 @@ QVector QgsMeshLayerUtils::calculateNormals( const QgsTriangularMesh return normals; } +bool QgsMeshLayerUtils::haveSameParentGroup( const QgsMeshLayer *layer, const QgsMeshDatasetIndex &index1, const QgsMeshDatasetIndex &index2 ) +{ + const QgsMeshDatasetGroupMetadata metadata1 = layer->datasetGroupMetadata( index1 ); + if ( metadata1.parentGroup().isEmpty() ) + return false; + + const QgsMeshDatasetGroupMetadata metadata2 = layer->datasetGroupMetadata( index2 ); + if ( metadata2.parentGroup().isEmpty() ) + return false; + + return metadata1.parentGroup().compare( metadata2.parentGroup(), Qt::CaseInsensitive ) == 0; +} + ///@endcond diff --git a/src/core/mesh/qgsmeshlayerutils.h b/src/core/mesh/qgsmeshlayerutils.h index 2d25837692b98..e15f182dd10b9 100644 --- a/src/core/mesh/qgsmeshlayerutils.h +++ b/src/core/mesh/qgsmeshlayerutils.h @@ -369,6 +369,14 @@ class CORE_EXPORT QgsMeshLayerUtils const QgsTriangularMesh &triangularMesh, const QVector &verticalMagnitude, bool isRelative ); + + /** + * Returns TRUE if the datasets from \a layer at \a index1 and \a index2 share the same parent group. + * + * \since QGIS 3.38 + */ + static bool haveSameParentGroup( const QgsMeshLayer *layer, const QgsMeshDatasetIndex &index1, const QgsMeshDatasetIndex &index2 ); + }; ///@endcond From 701af9462b45b5ba9b79da68da1a63e34fe1b13a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 13 May 2024 11:02:27 +1000 Subject: [PATCH 094/102] Rename to parentQuantityName --- .../core/auto_generated/mesh/qgsmeshdataset.sip.in | 5 +++-- .../core/auto_generated/mesh/qgsmeshdataset.sip.in | 5 +++-- src/core/mesh/qgsmeshdataset.cpp | 2 +- src/core/mesh/qgsmeshdataset.h | 14 ++++++++++---- src/core/mesh/qgsmeshlayerrenderer.cpp | 4 ++-- src/core/mesh/qgsmeshlayerutils.cpp | 8 ++++---- src/core/mesh/qgsmeshlayerutils.h | 4 ++-- 7 files changed, 25 insertions(+), 17 deletions(-) diff --git a/python/PyQt6/core/auto_generated/mesh/qgsmeshdataset.sip.in b/python/PyQt6/core/auto_generated/mesh/qgsmeshdataset.sip.in index f25cc96e33025..4bb5e15422756 100644 --- a/python/PyQt6/core/auto_generated/mesh/qgsmeshdataset.sip.in +++ b/python/PyQt6/core/auto_generated/mesh/qgsmeshdataset.sip.in @@ -12,6 +12,7 @@ + class QgsMeshDatasetIndex { %Docstring(signature="appended") @@ -433,9 +434,9 @@ Constructs a valid metadata object Returns name of the dataset group %End - QString parentGroup() const; + QString parentQuantityName() const; %Docstring -Returns the name of the dataset's parent group. +Returns the name of the dataset's parent quantity, if available. .. versionadded:: 3.38 %End diff --git a/python/core/auto_generated/mesh/qgsmeshdataset.sip.in b/python/core/auto_generated/mesh/qgsmeshdataset.sip.in index 7ad800e693e3c..438f0fbf903d5 100644 --- a/python/core/auto_generated/mesh/qgsmeshdataset.sip.in +++ b/python/core/auto_generated/mesh/qgsmeshdataset.sip.in @@ -12,6 +12,7 @@ + class QgsMeshDatasetIndex { %Docstring(signature="appended") @@ -433,9 +434,9 @@ Constructs a valid metadata object Returns name of the dataset group %End - QString parentGroup() const; + QString parentQuantityName() const; %Docstring -Returns the name of the dataset's parent group. +Returns the name of the dataset's parent quantity, if available. .. versionadded:: 3.38 %End diff --git a/src/core/mesh/qgsmeshdataset.cpp b/src/core/mesh/qgsmeshdataset.cpp index ed2667df8f772..58ee1aff1d187 100644 --- a/src/core/mesh/qgsmeshdataset.cpp +++ b/src/core/mesh/qgsmeshdataset.cpp @@ -178,7 +178,7 @@ QString QgsMeshDatasetGroupMetadata::name() const return mName; } -QString QgsMeshDatasetGroupMetadata::parentGroup() const +QString QgsMeshDatasetGroupMetadata::parentQuantityName() const { return mParentGroupName; } diff --git a/src/core/mesh/qgsmeshdataset.h b/src/core/mesh/qgsmeshdataset.h index 6218fb4cb7c5f..750a88cb691ae 100644 --- a/src/core/mesh/qgsmeshdataset.h +++ b/src/core/mesh/qgsmeshdataset.h @@ -22,17 +22,21 @@ #include #include #include +#include +#include #include +#include #include "qgis_core.h" #include "qgis_sip.h" -#include "qgspoint.h" -#include "qgsdataprovider.h" class QgsMeshLayer; class QgsMeshDatasetGroup; class QgsRectangle; +class QDomDocument; +class QgsReadWriteContext; + struct QgsMesh; /** @@ -398,11 +402,13 @@ class CORE_EXPORT QgsMeshDatasetGroupMetadata QString name() const; /** - * Returns the name of the dataset's parent group. + * Returns the name of the dataset's parent quantity, if available. + * + * * * \since QGIS 3.38 */ - QString parentGroup() const; + QString parentQuantityName() const; /** * Returns the uri of the source diff --git a/src/core/mesh/qgsmeshlayerrenderer.cpp b/src/core/mesh/qgsmeshlayerrenderer.cpp index d494a133c5d52..d6003f34e5b66 100644 --- a/src/core/mesh/qgsmeshlayerrenderer.cpp +++ b/src/core/mesh/qgsmeshlayerrenderer.cpp @@ -114,8 +114,8 @@ QgsMeshLayerRenderer::QgsMeshLayerRenderer( { if ( it.value().overlaps( context.zRange() ) ) { - const bool matchesVectorParentGroup = QgsMeshLayerUtils::haveSameParentGroup( layer, QgsMeshDatasetIndex( activeVectorDatasetGroup ), QgsMeshDatasetIndex( it.key() ) ); - const bool matchesScalarParentGroup = QgsMeshLayerUtils::haveSameParentGroup( layer, QgsMeshDatasetIndex( activeScalarDatasetGroup ), QgsMeshDatasetIndex( it.key() ) ); + const bool matchesVectorParentGroup = QgsMeshLayerUtils::haveSameParentQuantity( layer, QgsMeshDatasetIndex( activeVectorDatasetGroup ), QgsMeshDatasetIndex( it.key() ) ); + const bool matchesScalarParentGroup = QgsMeshLayerUtils::haveSameParentQuantity( layer, QgsMeshDatasetIndex( activeScalarDatasetGroup ), QgsMeshDatasetIndex( it.key() ) ); if ( matchesVectorParentGroup && ( currentMatchingVectorRange.isInfinite() diff --git a/src/core/mesh/qgsmeshlayerutils.cpp b/src/core/mesh/qgsmeshlayerutils.cpp index a5398f0abbcec..b68f5bcfe9cdc 100644 --- a/src/core/mesh/qgsmeshlayerutils.cpp +++ b/src/core/mesh/qgsmeshlayerutils.cpp @@ -704,17 +704,17 @@ QVector QgsMeshLayerUtils::calculateNormals( const QgsTriangularMesh return normals; } -bool QgsMeshLayerUtils::haveSameParentGroup( const QgsMeshLayer *layer, const QgsMeshDatasetIndex &index1, const QgsMeshDatasetIndex &index2 ) +bool QgsMeshLayerUtils::haveSameParentQuantity( const QgsMeshLayer *layer, const QgsMeshDatasetIndex &index1, const QgsMeshDatasetIndex &index2 ) { const QgsMeshDatasetGroupMetadata metadata1 = layer->datasetGroupMetadata( index1 ); - if ( metadata1.parentGroup().isEmpty() ) + if ( metadata1.parentQuantityName().isEmpty() ) return false; const QgsMeshDatasetGroupMetadata metadata2 = layer->datasetGroupMetadata( index2 ); - if ( metadata2.parentGroup().isEmpty() ) + if ( metadata2.parentQuantityName().isEmpty() ) return false; - return metadata1.parentGroup().compare( metadata2.parentGroup(), Qt::CaseInsensitive ) == 0; + return metadata1.parentQuantityName().compare( metadata2.parentQuantityName(), Qt::CaseInsensitive ) == 0; } ///@endcond diff --git a/src/core/mesh/qgsmeshlayerutils.h b/src/core/mesh/qgsmeshlayerutils.h index e15f182dd10b9..da7c985d75fa0 100644 --- a/src/core/mesh/qgsmeshlayerutils.h +++ b/src/core/mesh/qgsmeshlayerutils.h @@ -371,11 +371,11 @@ class CORE_EXPORT QgsMeshLayerUtils bool isRelative ); /** - * Returns TRUE if the datasets from \a layer at \a index1 and \a index2 share the same parent group. + * Returns TRUE if the datasets from \a layer at \a index1 and \a index2 share the same parent quantity. * * \since QGIS 3.38 */ - static bool haveSameParentGroup( const QgsMeshLayer *layer, const QgsMeshDatasetIndex &index1, const QgsMeshDatasetIndex &index2 ); + static bool haveSameParentQuantity( const QgsMeshLayer *layer, const QgsMeshDatasetIndex &index1, const QgsMeshDatasetIndex &index2 ); }; From f62e9455788640b91105d661393082b4f441c86c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 13 May 2024 11:03:44 +1000 Subject: [PATCH 095/102] Improve documentation --- python/PyQt6/core/auto_generated/mesh/qgsmeshdataset.sip.in | 3 +++ python/core/auto_generated/mesh/qgsmeshdataset.sip.in | 3 +++ src/core/mesh/qgsmeshdataset.h | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/python/PyQt6/core/auto_generated/mesh/qgsmeshdataset.sip.in b/python/PyQt6/core/auto_generated/mesh/qgsmeshdataset.sip.in index 4bb5e15422756..a0725f4ddc7dc 100644 --- a/python/PyQt6/core/auto_generated/mesh/qgsmeshdataset.sip.in +++ b/python/PyQt6/core/auto_generated/mesh/qgsmeshdataset.sip.in @@ -438,6 +438,9 @@ Returns name of the dataset group %Docstring Returns the name of the dataset's parent quantity, if available. +The quantity can be used to collect dataset groups which represent a single quantity +but at different values (e.g. groups which represent different elevations). + .. versionadded:: 3.38 %End diff --git a/python/core/auto_generated/mesh/qgsmeshdataset.sip.in b/python/core/auto_generated/mesh/qgsmeshdataset.sip.in index 438f0fbf903d5..fae0e459b1c20 100644 --- a/python/core/auto_generated/mesh/qgsmeshdataset.sip.in +++ b/python/core/auto_generated/mesh/qgsmeshdataset.sip.in @@ -438,6 +438,9 @@ Returns name of the dataset group %Docstring Returns the name of the dataset's parent quantity, if available. +The quantity can be used to collect dataset groups which represent a single quantity +but at different values (e.g. groups which represent different elevations). + .. versionadded:: 3.38 %End diff --git a/src/core/mesh/qgsmeshdataset.h b/src/core/mesh/qgsmeshdataset.h index 750a88cb691ae..727c2580de669 100644 --- a/src/core/mesh/qgsmeshdataset.h +++ b/src/core/mesh/qgsmeshdataset.h @@ -404,7 +404,8 @@ class CORE_EXPORT QgsMeshDatasetGroupMetadata /** * Returns the name of the dataset's parent quantity, if available. * - * + * The quantity can be used to collect dataset groups which represent a single quantity + * but at different values (e.g. groups which represent different elevations). * * \since QGIS 3.38 */ From 9253b6c876da02ed559712670bf248f040aab2ef Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 13 May 2024 11:06:22 +1000 Subject: [PATCH 096/102] Add note, rename more members for consistency --- src/core/mesh/qgsmeshdataset.cpp | 13 +++++++------ src/core/mesh/qgsmeshdataset.h | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/core/mesh/qgsmeshdataset.cpp b/src/core/mesh/qgsmeshdataset.cpp index 58ee1aff1d187..9c6d977f6994f 100644 --- a/src/core/mesh/qgsmeshdataset.cpp +++ b/src/core/mesh/qgsmeshdataset.cpp @@ -144,12 +144,13 @@ QgsMeshDatasetGroupMetadata::QgsMeshDatasetGroupMetadata( const QString &name, , mReferenceTime( referenceTime ) , mIsTemporal( isTemporal ) { - const thread_local QRegularExpression parentGroupNameRegex( QStringLiteral( "^(.*):.*?$" ) ); - - const QRegularExpressionMatch parentGroupMatch = parentGroupNameRegex.match( mName ); - if ( parentGroupMatch.hasMatch() ) + // this relies on the naming convention used by MDAL's NetCDF driver: _: + // If future MDAL releases expose quantities via a standard API then we can safely remove this and port to the new API. + const thread_local QRegularExpression parentQuantityRegex( QStringLiteral( "^(.*):.*?$" ) ); + const QRegularExpressionMatch parentQuantityMatch = parentQuantityRegex.match( mName ); + if ( parentQuantityMatch.hasMatch() ) { - mParentGroupName = parentGroupMatch.captured( 1 ); + mParentQuantityName = parentQuantityMatch.captured( 1 ); } } @@ -180,7 +181,7 @@ QString QgsMeshDatasetGroupMetadata::name() const QString QgsMeshDatasetGroupMetadata::parentQuantityName() const { - return mParentGroupName; + return mParentQuantityName; } QgsMeshDatasetGroupMetadata::DataType diff --git a/src/core/mesh/qgsmeshdataset.h b/src/core/mesh/qgsmeshdataset.h index 727c2580de669..426ac0f40cb96 100644 --- a/src/core/mesh/qgsmeshdataset.h +++ b/src/core/mesh/qgsmeshdataset.h @@ -471,7 +471,7 @@ class CORE_EXPORT QgsMeshDatasetGroupMetadata private: QString mName; - QString mParentGroupName; + QString mParentQuantityName; QString mUri; bool mIsScalar = false; DataType mDataType = DataType::DataOnFaces; From f2b84709fa0d7d58f64d83646c23ad5ae2b98cb8 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 13 May 2024 11:43:24 +1000 Subject: [PATCH 097/102] Add test for parentQuantityName --- python/testing/__init__.py | 13 ++++ tests/src/python/CMakeLists.txt | 1 + tests/src/python/test_qgsmeshlayer.py | 68 ++++++++++++++++++ tests/testdata/mesh/netcdf_parent_quantity.nc | Bin 0 -> 207339 bytes 4 files changed, 82 insertions(+) create mode 100644 tests/src/python/test_qgsmeshlayer.py create mode 100644 tests/testdata/mesh/netcdf_parent_quantity.nc diff --git a/python/testing/__init__.py b/python/testing/__init__.py index 800a7fda454a9..f7608a27518f4 100644 --- a/python/testing/__init__.py +++ b/python/testing/__init__.py @@ -305,6 +305,19 @@ def render_layout_check( return result + @staticmethod + def get_test_data_path(file_path: str) -> Path: + """ + Returns the full path to a file contained within the test data + directory. + """ + from utilities import unitTestDataPath + + return ( + Path(unitTestDataPath()) / + (file_path[1:] if file_path.startswith('/') else file_path) + ) + def assertLayersEqual(self, layer_expected, layer_result, **kwargs): """ :param layer_expected: The first layer to compare diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 51f359d931161..ad77a8f5c02ff 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -177,6 +177,7 @@ ADD_PYTHON_TEST(PyQgsMargins test_qgsmargins.py) ADD_PYTHON_TEST(PyQgsMarkerLineSymbolLayer test_qgsmarkerlinesymbollayer.py) ADD_PYTHON_TEST(PyQgsMatrix4x4 test_qgsmatrix4x4.py) ADD_PYTHON_TEST(PyQgsMergedFeatureRenderer test_qgsmergedfeaturerenderer.py) +ADD_PYTHON_TEST(PyQgsMeshLayer test_qgsmeshlayer.py) ADD_PYTHON_TEST(PyQgsMeshLayerElevationProperties test_qgsmeshlayerelevationproperties.py) ADD_PYTHON_TEST(PyQgsMeshLayerRenderer test_qgsmeshlayerrenderer.py) ADD_PYTHON_TEST(PyQgsMessageLog test_qgsmessagelog.py) diff --git a/tests/src/python/test_qgsmeshlayer.py b/tests/src/python/test_qgsmeshlayer.py new file mode 100644 index 0000000000000..ac023360fddfb --- /dev/null +++ b/tests/src/python/test_qgsmeshlayer.py @@ -0,0 +1,68 @@ +"""QGIS Unit tests for QgsMeshLayer + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" + +from qgis.core import ( + QgsMeshLayer, + QgsMeshDatasetIndex +) +import unittest +from qgis.testing import start_app, QgisTestCase + +start_app() + + +class TestQgsMeshLayer(QgisTestCase): + + def test_dataset_group_metadata(self): + """ + Test datasetGroupMetadata + """ + layer = QgsMeshLayer( + self.get_test_data_path('mesh/netcdf_parent_quantity.nc').as_posix(), + 'mesh', + 'mdal' + ) + self.assertTrue(layer.isValid()) + + self.assertEqual( + layer.datasetGroupMetadata(QgsMeshDatasetIndex(0)).name(), + 'air_temperature_height:10') + self.assertEqual( + layer.datasetGroupMetadata( + QgsMeshDatasetIndex(0)).parentQuantityName(), + 'air_temperature_height') + self.assertEqual( + layer.datasetGroupMetadata(QgsMeshDatasetIndex(1)).name(), + 'air_temperature_height:20') + self.assertEqual( + layer.datasetGroupMetadata( + QgsMeshDatasetIndex(1)).parentQuantityName(), + 'air_temperature_height') + self.assertEqual( + layer.datasetGroupMetadata(QgsMeshDatasetIndex(2)).name(), + 'air_temperature_height:30') + self.assertEqual( + layer.datasetGroupMetadata( + QgsMeshDatasetIndex(2)).parentQuantityName(), + 'air_temperature_height') + self.assertEqual( + layer.datasetGroupMetadata(QgsMeshDatasetIndex(3)).name(), + 'air_temperature_height:5') + self.assertEqual( + layer.datasetGroupMetadata( + QgsMeshDatasetIndex(3)).parentQuantityName(), + 'air_temperature_height') + self.assertFalse( + layer.datasetGroupMetadata(QgsMeshDatasetIndex(4)).name()) + self.assertFalse( + layer.datasetGroupMetadata( + QgsMeshDatasetIndex(4)).parentQuantityName()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testdata/mesh/netcdf_parent_quantity.nc b/tests/testdata/mesh/netcdf_parent_quantity.nc new file mode 100644 index 0000000000000000000000000000000000000000..1d3a88195ea16673445a01524b8116d1dfd71ac5 GIT binary patch literal 207339 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 Date: Mon, 13 May 2024 11:49:05 +1000 Subject: [PATCH 098/102] Add test for haveSameParentQuantity --- tests/src/core/testqgsmeshlayer.cpp | 31 +++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/tests/src/core/testqgsmeshlayer.cpp b/tests/src/core/testqgsmeshlayer.cpp index 73c3b4786b25e..5c6b80909e54e 100644 --- a/tests/src/core/testqgsmeshlayer.cpp +++ b/tests/src/core/testqgsmeshlayer.cpp @@ -28,6 +28,7 @@ #include "qgstriangularmesh.h" #include "qgsexpression.h" #include "qgsmeshlayertemporalproperties.h" +#include "qgsmeshlayerutils.h" #include "qgsmeshdataprovidertemporalcapabilities.h" #include "qgsprovidermetadata.h" @@ -37,12 +38,14 @@ * \ingroup UnitTests * This is a unit test for a mesh layer */ -class TestQgsMeshLayer : public QObject +class TestQgsMeshLayer : public QgsTest { Q_OBJECT public: - TestQgsMeshLayer() = default; + TestQgsMeshLayer() + : QgsTest( QStringLiteral( "Mesh layer tests" ) ) + {} private: QString mDataDir; @@ -108,6 +111,8 @@ class TestQgsMeshLayer : public QObject void keepDatasetIndexConsistency(); void symbologyConsistencyWithName(); void updateTimePropertiesWhenReloading(); + + void testHaveSameParentQuantity(); }; QString TestQgsMeshLayer::readFile( const QString &fname ) const @@ -2339,5 +2344,27 @@ void TestQgsMeshLayer::updateTimePropertiesWhenReloading() QCOMPARE( timeExtent1, static_cast( layer->temporalProperties() )->timeExtent() ); } +void TestQgsMeshLayer::testHaveSameParentQuantity() +{ + QgsMeshLayer layer1( + testDataPath( "mesh/netcdf_parent_quantity.nc" ), + QStringLiteral( "mesh" ), + QStringLiteral( "mdal" ) ); + QVERIFY( layer1.isValid() ); + + QVERIFY( QgsMeshLayerUtils::haveSameParentQuantity( &layer1, QgsMeshDatasetIndex( 0 ), QgsMeshDatasetIndex( 1 ) ) ); + QVERIFY( QgsMeshLayerUtils::haveSameParentQuantity( &layer1, QgsMeshDatasetIndex( 0 ), QgsMeshDatasetIndex( 2 ) ) ); + QVERIFY( QgsMeshLayerUtils::haveSameParentQuantity( &layer1, QgsMeshDatasetIndex( 2 ), QgsMeshDatasetIndex( 3 ) ) ); + QVERIFY( !QgsMeshLayerUtils::haveSameParentQuantity( &layer1, QgsMeshDatasetIndex( 0 ), QgsMeshDatasetIndex( 4 ) ) ); + + QgsMeshLayer layer2( + testDataPath( "mesh/mesh_z_ws_d.nc" ), + QStringLiteral( "mesh" ), + QStringLiteral( "mdal" ) ); + QVERIFY( layer2.isValid() ); + + QVERIFY( !QgsMeshLayerUtils::haveSameParentQuantity( &layer2, QgsMeshDatasetIndex( 0 ), QgsMeshDatasetIndex( 1 ) ) ); +} + QGSTEST_MAIN( TestQgsMeshLayer ) #include "testqgsmeshlayer.moc" From dd01d113e3369cc3e659caa4a89bf45e27a6a82f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 14 May 2024 08:19:20 +1000 Subject: [PATCH 099/102] Port test to standard render checker methods --- tests/src/core/testqgsmeshlayerrenderer.cpp | 253 ++++++++++++++------ 1 file changed, 185 insertions(+), 68 deletions(-) diff --git a/tests/src/core/testqgsmeshlayerrenderer.cpp b/tests/src/core/testqgsmeshlayerrenderer.cpp index 47e229cc770e2..8c3cb3692d409 100644 --- a/tests/src/core/testqgsmeshlayerrenderer.cpp +++ b/tests/src/core/testqgsmeshlayerrenderer.cpp @@ -35,9 +35,6 @@ #include "qgsmesh3daveraging.h" #include "qgsmaplayertemporalproperties.h" -//qgis test includes -#include "qgsrenderchecker.h" - /** * \ingroup UnitTests * This is a unit test for the different renderers for mesh layers. @@ -47,7 +44,7 @@ class TestQgsMeshRenderer : public QgsTest Q_OBJECT public: - TestQgsMeshRenderer() : QgsTest( QStringLiteral( "Mesh Layer Rendering Tests" ) ) {} + TestQgsMeshRenderer() : QgsTest( QStringLiteral( "Mesh Layer Rendering Tests" ), QStringLiteral( "mesh" ) ) {} private: QString mDataDir; @@ -61,10 +58,8 @@ class TestQgsMeshRenderer : public QgsTest void initTestCase();// will be called before the first testfunction is executed. void cleanupTestCase();// will be called after the last testfunction was executed. void init(); // will be called before each testfunction is executed. - bool imageCheck( const QString &testType, QgsMeshLayer *layer, double rotation = 0.0 ); QString readFile( const QString &fname ) const; - void test_native_mesh_rendering(); void test_native_mesh_renderingWithClipping(); void test_triangular_mesh_rendering(); @@ -215,23 +210,6 @@ QString TestQgsMeshRenderer::readFile( const QString &fname ) const return uri; } -bool TestQgsMeshRenderer::imageCheck( const QString &testType, QgsMeshLayer *layer, double rotation ) -{ - mMapSettings->setDestinationCrs( layer->crs() ); - mMapSettings->setExtent( layer->extent() ); - mMapSettings->setRotation( rotation ); - mMapSettings->setOutputDpi( 96 ); - - QgsRenderChecker myChecker; - myChecker.setControlPathPrefix( QStringLiteral( "mesh" ) ); - myChecker.setControlName( "expected_" + testType ); - myChecker.setMapSettings( *mMapSettings ); - myChecker.setColorTolerance( 15 ); - const bool myResultFlag = myChecker.runTest( testType, 0 ); - mReport += myChecker.report(); - return myResultFlag; -} - void TestQgsMeshRenderer::test_native_mesh_rendering() { QgsMeshRendererSettings rendererSettings = mMemoryLayer->rendererSettings(); @@ -240,8 +218,16 @@ void TestQgsMeshRenderer::test_native_mesh_rendering() settings.setLineWidth( 1. ); rendererSettings.setNativeMeshSettings( settings ); mMemoryLayer->setRendererSettings( rendererSettings ); - QVERIFY( imageCheck( "quad_and_triangle_native_mesh", mMemoryLayer ) ); - QVERIFY( imageCheck( "quad_and_triangle_native_mesh_rotated_45", mMemoryLayer, 45.0 ) ); + + mMapSettings->setDestinationCrs( mMemoryLayer->crs() ); + mMapSettings->setExtent( mMemoryLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_native_mesh", "quad_and_triangle_native_mesh", *mMapSettings, 0, 15 ); + + mMapSettings->setRotation( 45 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_native_mesh_rotated_45", "quad_and_triangle_native_mesh_rotated_45", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 0 ); } void TestQgsMeshRenderer::test_native_mesh_renderingWithClipping() @@ -260,10 +246,12 @@ void TestQgsMeshRenderer::test_native_mesh_renderingWithClipping() mMapSettings->addClippingRegion( region ); mMapSettings->addClippingRegion( region2 ); - const bool res = imageCheck( "painterclip_region", mMemoryLayer ); - + mMapSettings->setDestinationCrs( mMemoryLayer->crs() ); + mMapSettings->setExtent( mMemoryLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "painterclip_region", "painterclip_region", *mMapSettings, 0, 15 ); mMapSettings->setClippingRegions( QList< QgsMapClippingRegion >() ); - QVERIFY( res ); } void TestQgsMeshRenderer::test_triangular_mesh_rendering() @@ -275,8 +263,16 @@ void TestQgsMeshRenderer::test_triangular_mesh_rendering() settings.setLineWidth( 0.26 ); rendererSettings.setTriangularMeshSettings( settings ); mMemoryLayer->setRendererSettings( rendererSettings ); - QVERIFY( imageCheck( "quad_and_triangle_triangular_mesh", mMemoryLayer ) ); - QVERIFY( imageCheck( "quad_and_triangle_triangular_mesh_rotated_45", mMemoryLayer, 45.0 ) ); + + mMapSettings->setDestinationCrs( mMemoryLayer->crs() ); + mMapSettings->setExtent( mMemoryLayer->extent() ); + mMapSettings->setRotation( 0 ); + mMapSettings->setOutputDpi( 96 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_triangular_mesh", "quad_and_triangle_triangular_mesh", *mMapSettings, 0, 15 ); + + mMapSettings->setRotation( 45 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_triangular_mesh_rotated_45", "quad_and_triangle_triangular_mesh_rotated_45", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 0 ); } void TestQgsMeshRenderer::test_edge_mesh_rendering() @@ -288,7 +284,12 @@ void TestQgsMeshRenderer::test_edge_mesh_rendering() settings.setLineWidth( 0.26 ); rendererSettings.setEdgeMeshSettings( settings ); mMemory1DLayer->setRendererSettings( rendererSettings ); - QVERIFY( imageCheck( "lines_edge_mesh", mMemory1DLayer ) ); + + mMapSettings->setDestinationCrs( mMemory1DLayer->crs() ); + mMapSettings->setExtent( mMemory1DLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "lines_edge_mesh", "lines_edge_mesh", *mMapSettings, 0, 15 ); } void TestQgsMeshRenderer::test_1d_vertex_scalar_dataset_rendering() @@ -308,8 +309,14 @@ void TestQgsMeshRenderer::test_1d_vertex_scalar_dataset_rendering() mMemory1DLayer->setRendererSettings( rendererSettings ); mMemory1DLayer->setStaticScalarDatasetIndex( ds ); - QVERIFY( imageCheck( "lines_vertex_scalar_dataset", mMemory1DLayer ) ); - QVERIFY( imageCheck( "lines_vertex_scalar_dataset_rotated_45", mMemory1DLayer, 45 ) ); + mMapSettings->setDestinationCrs( mMemory1DLayer->crs() ); + mMapSettings->setExtent( mMemory1DLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "lines_vertex_scalar_dataset", "lines_vertex_scalar_dataset", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 45 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "lines_vertex_scalar_dataset_rotated_45", "lines_vertex_scalar_dataset_rotated_45", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 0 ); } void TestQgsMeshRenderer::test_1d_vertex_vector_dataset_rendering() @@ -327,8 +334,14 @@ void TestQgsMeshRenderer::test_1d_vertex_vector_dataset_rendering() mMemory1DLayer->setRendererSettings( rendererSettings ); mMemory1DLayer->setStaticVectorDatasetIndex( ds ); - QVERIFY( imageCheck( "lines_vertex_vector_dataset", mMemory1DLayer ) ); - QVERIFY( imageCheck( "lines_vertex_vector_dataset_rotated_45", mMemory1DLayer, 45 ) ); + mMapSettings->setDestinationCrs( mMemory1DLayer->crs() ); + mMapSettings->setExtent( mMemory1DLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "lines_vertex_vector_dataset", "lines_vertex_vector_dataset", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 45 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "lines_vertex_vector_dataset_rotated_45", "lines_vertex_vector_dataset_rotated_45", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 0 ); } void TestQgsMeshRenderer::test_1d_edge_scalar_dataset_rendering() @@ -348,8 +361,14 @@ void TestQgsMeshRenderer::test_1d_edge_scalar_dataset_rendering() mMemory1DLayer->setRendererSettings( rendererSettings ); mMemory1DLayer->setStaticScalarDatasetIndex( ds ); - QVERIFY( imageCheck( "lines_edge_scalar_dataset", mMemory1DLayer ) ); - QVERIFY( imageCheck( "lines_edge_scalar_dataset_rotated_45", mMemory1DLayer, 45 ) ); + mMapSettings->setDestinationCrs( mMemory1DLayer->crs() ); + mMapSettings->setExtent( mMemory1DLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "lines_edge_scalar_dataset", "lines_edge_scalar_dataset", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 45 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "lines_edge_scalar_dataset_rotated_45", "lines_edge_scalar_dataset_rotated_45", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 0 ); } void TestQgsMeshRenderer::test_1d_edge_vector_dataset_rendering() @@ -362,8 +381,14 @@ void TestQgsMeshRenderer::test_1d_edge_vector_dataset_rendering() mMemory1DLayer->setRendererSettings( rendererSettings ); mMemory1DLayer->setStaticVectorDatasetIndex( ds ); - QVERIFY( imageCheck( "lines_edge_vector_dataset", mMemory1DLayer ) ); - QVERIFY( imageCheck( "lines_edge_vector_dataset_rotated_45", mMemory1DLayer, 45 ) ); + mMapSettings->setDestinationCrs( mMemory1DLayer->crs() ); + mMapSettings->setExtent( mMemory1DLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "lines_edge_vector_dataset", "lines_edge_vector_dataset", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 45 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "lines_edge_vector_dataset_rotated_45", "lines_edge_vector_dataset_rotated_45", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 0 ); } void TestQgsMeshRenderer::test_vertex_scalar_dataset_rendering() @@ -376,8 +401,14 @@ void TestQgsMeshRenderer::test_vertex_scalar_dataset_rendering() mMemoryLayer->setRendererSettings( rendererSettings ); mMemoryLayer->setStaticScalarDatasetIndex( ds ); - QVERIFY( imageCheck( "quad_and_triangle_vertex_scalar_dataset", mMemoryLayer ) ); - QVERIFY( imageCheck( "quad_and_triangle_vertex_scalar_dataset_rotated_45", mMemoryLayer, 45.0 ) ); + mMapSettings->setDestinationCrs( mMemoryLayer->crs() ); + mMapSettings->setExtent( mMemoryLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_vertex_scalar_dataset", "quad_and_triangle_vertex_scalar_dataset", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 45 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_vertex_scalar_dataset_rotated_45", "quad_and_triangle_vertex_scalar_dataset_rotated_45", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 0 ); } void TestQgsMeshRenderer::test_vertex_vector_dataset_rendering() @@ -395,8 +426,14 @@ void TestQgsMeshRenderer::test_vertex_vector_dataset_rendering() mMemoryLayer->setRendererSettings( rendererSettings ); mMemoryLayer->setStaticVectorDatasetIndex( ds ); - QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_dataset", mMemoryLayer ) ); - QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_dataset_rotated_45", mMemoryLayer, 45.0 ) ); + mMapSettings->setDestinationCrs( mMemoryLayer->crs() ); + mMapSettings->setExtent( mMemoryLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_vertex_vector_dataset", "quad_and_triangle_vertex_vector_dataset", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 45 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_vertex_vector_dataset_rotated_45", "quad_and_triangle_vertex_vector_dataset_rotated_45", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 0 ); } void TestQgsMeshRenderer::test_vertex_vector_dataset_colorRamp_rendering() @@ -415,7 +452,11 @@ void TestQgsMeshRenderer::test_vertex_vector_dataset_colorRamp_rendering() rendererSettings.setVectorSettings( ds.group(), settings ); mMemoryLayer->setRendererSettings( rendererSettings ); - QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_dataset_colorRamp", mMemoryLayer ) ); + mMapSettings->setDestinationCrs( mMemoryLayer->crs() ); + mMapSettings->setExtent( mMemoryLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_vertex_vector_dataset_colorRamp", "quad_and_triangle_vertex_vector_dataset_colorRamp", *mMapSettings, 0, 15 ); } void TestQgsMeshRenderer::test_face_scalar_dataset_rendering() @@ -428,8 +469,14 @@ void TestQgsMeshRenderer::test_face_scalar_dataset_rendering() mMemoryLayer->setRendererSettings( rendererSettings ); mMemoryLayer->setStaticScalarDatasetIndex( ds ); - QVERIFY( imageCheck( "quad_and_triangle_face_scalar_dataset", mMemoryLayer ) ); - QVERIFY( imageCheck( "quad_and_triangle_face_scalar_dataset_rotated_45", mMemoryLayer, 45.0 ) ); + mMapSettings->setDestinationCrs( mMemoryLayer->crs() ); + mMapSettings->setExtent( mMemoryLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_face_scalar_dataset", "quad_and_triangle_face_scalar_dataset", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 45 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_face_scalar_dataset_rotated_45", "quad_and_triangle_face_scalar_dataset_rotated_45", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 0 ); } void TestQgsMeshRenderer::test_face_scalar_dataset_interpolated_neighbour_average_rendering() @@ -445,10 +492,13 @@ void TestQgsMeshRenderer::test_face_scalar_dataset_interpolated_neighbour_averag mMemoryLayer->setRendererSettings( rendererSettings ); mMemoryLayer->setStaticScalarDatasetIndex( ds ); - QVERIFY( imageCheck( "quad_and_triangle_face_scalar_interpolated_neighbour_average_dataset", mMemoryLayer ) ); + mMapSettings->setDestinationCrs( mMemoryLayer->crs() ); + mMapSettings->setExtent( mMemoryLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_face_scalar_interpolated_neighbour_average_dataset", "quad_and_triangle_face_scalar_interpolated_neighbour_average_dataset", *mMapSettings, 0, 15 ); } - void TestQgsMeshRenderer::test_face_vector_dataset_rendering() { const QgsMeshDatasetIndex ds( 3, 0 ); @@ -459,8 +509,14 @@ void TestQgsMeshRenderer::test_face_vector_dataset_rendering() mMemoryLayer->setRendererSettings( rendererSettings ); mMemoryLayer->setStaticVectorDatasetIndex( ds ); - QVERIFY( imageCheck( "quad_and_triangle_face_vector_dataset", mMemoryLayer ) ); - QVERIFY( imageCheck( "quad_and_triangle_face_vector_dataset_rotated_45", mMemoryLayer, 45.0 ) ); + mMapSettings->setDestinationCrs( mMemoryLayer->crs() ); + mMapSettings->setExtent( mMemoryLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_face_vector_dataset", "quad_and_triangle_face_vector_dataset", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 45 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_face_vector_dataset_rotated_45", "quad_and_triangle_face_vector_dataset_rotated_45", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 0 ); } void TestQgsMeshRenderer::test_vertex_scalar_dataset_with_inactive_face_rendering() @@ -473,7 +529,11 @@ void TestQgsMeshRenderer::test_vertex_scalar_dataset_with_inactive_face_renderin mMdalLayer->setRendererSettings( rendererSettings ); mMdalLayer->setStaticScalarDatasetIndex( ds ); - QVERIFY( imageCheck( "quad_and_triangle_vertex_scalar_dataset_with_inactive_face", mMdalLayer ) ); + mMapSettings->setDestinationCrs( mMdalLayer->crs() ); + mMapSettings->setExtent( mMdalLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_vertex_scalar_dataset_with_inactive_face", "quad_and_triangle_vertex_scalar_dataset_with_inactive_face", *mMapSettings, 0, 15 ); } void TestQgsMeshRenderer::test_vertex_vector_on_user_grid_wind_barbs() @@ -500,8 +560,14 @@ void TestQgsMeshRenderer::test_vertex_vector_on_user_grid_wind_barbs() mMdalLayer->setRendererSettings( rendererSettings ); mMdalLayer->setStaticVectorDatasetIndex( ds ); - QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs", mMemoryLayer ) ); - QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs_rotated_45", mMemoryLayer, 45.0 ) ); + mMapSettings->setDestinationCrs( mMdalLayer->crs() ); + mMapSettings->setExtent( mMdalLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs", "quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 45 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs_rotated_45", "quad_and_triangle_vertex_vector_user_grid_dataset_wind_barbs_rotated_45", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 0 ); } void TestQgsMeshRenderer::test_face_vector_on_user_grid() @@ -521,8 +587,14 @@ void TestQgsMeshRenderer::test_face_vector_on_user_grid() mMemoryLayer->setRendererSettings( rendererSettings ); mMemoryLayer->setStaticVectorDatasetIndex( ds ); - QVERIFY( imageCheck( "quad_and_triangle_face_vector_user_grid_dataset", mMemoryLayer ) ); - QVERIFY( imageCheck( "quad_and_triangle_face_vector_user_grid_dataset_rotated_45", mMemoryLayer, 45.0 ) ); + mMapSettings->setDestinationCrs( mMemoryLayer->crs() ); + mMapSettings->setExtent( mMemoryLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_face_vector_user_grid_dataset", "quad_and_triangle_face_vector_user_grid_dataset", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 45 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_face_vector_user_grid_dataset_rotated_45", "quad_and_triangle_face_vector_user_grid_dataset_rotated_45", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 0 ); } void TestQgsMeshRenderer::test_face_vector_on_user_grid_streamlines() @@ -542,8 +614,14 @@ void TestQgsMeshRenderer::test_face_vector_on_user_grid_streamlines() mMemoryLayer->setRendererSettings( rendererSettings ); mMemoryLayer->setStaticVectorDatasetIndex( ds ); - QVERIFY( imageCheck( "quad_and_triangle_face_vector_user_grid_dataset_streamlines", mMemoryLayer ) ); - QVERIFY( imageCheck( "quad_and_triangle_face_vector_user_grid_dataset_streamlines_rotated_45", mMemoryLayer, 45.0 ) ); + mMapSettings->setDestinationCrs( mMemoryLayer->crs() ); + mMapSettings->setExtent( mMemoryLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_face_vector_user_grid_dataset_streamlines", "quad_and_triangle_face_vector_user_grid_dataset_streamlines", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 45 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_face_vector_user_grid_dataset_streamlines_rotated_45", "quad_and_triangle_face_vector_user_grid_dataset_streamlines_rotated_45", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 0 ); } void TestQgsMeshRenderer::test_vertex_vector_on_user_grid() @@ -564,8 +642,14 @@ void TestQgsMeshRenderer::test_vertex_vector_on_user_grid() mMemoryLayer->setRendererSettings( rendererSettings ); mMemoryLayer->setStaticVectorDatasetIndex( ds ); - QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_user_grid_dataset", mMemoryLayer ) ); - QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_user_grid_dataset_rotated_45", mMemoryLayer, 45.0 ) ); + mMapSettings->setDestinationCrs( mMemoryLayer->crs() ); + mMapSettings->setExtent( mMemoryLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_vertex_vector_user_grid_dataset", "quad_and_triangle_vertex_vector_user_grid_dataset", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 45 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_vertex_vector_user_grid_dataset_rotated_45", "quad_and_triangle_vertex_vector_user_grid_dataset_rotated_45", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 0 ); } void TestQgsMeshRenderer::test_vertex_vector_on_user_grid_streamlines() @@ -586,8 +670,14 @@ void TestQgsMeshRenderer::test_vertex_vector_on_user_grid_streamlines() mMemoryLayer->setRendererSettings( rendererSettings ); mMemoryLayer->setStaticVectorDatasetIndex( ds ); - QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_user_grid_dataset_streamlines", mMemoryLayer ) ); - QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_user_grid_dataset_streamlines_rotated_45", mMemoryLayer, 45.0 ) ); + mMapSettings->setDestinationCrs( mMemoryLayer->crs() ); + mMapSettings->setExtent( mMemoryLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_vertex_vector_user_grid_dataset_streamlines", "quad_and_triangle_vertex_vector_user_grid_dataset_streamlines", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 45 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_vertex_vector_user_grid_dataset_streamlines_rotated_45", "quad_and_triangle_vertex_vector_user_grid_dataset_streamlines_rotated_45", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 0 ); } void TestQgsMeshRenderer::test_vertex_vector_on_user_grid_streamlines_colorRamp() @@ -608,7 +698,11 @@ void TestQgsMeshRenderer::test_vertex_vector_on_user_grid_streamlines_colorRamp( mMemoryLayer->setRendererSettings( rendererSettings ); mMemoryLayer->setStaticVectorDatasetIndex( ds ); - QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_user_grid_dataset_streamlines_colorRamp", mMemoryLayer ) ); + mMapSettings->setDestinationCrs( mMemoryLayer->crs() ); + mMapSettings->setExtent( mMemoryLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_vertex_vector_user_grid_dataset_streamlines_colorRamp", "quad_and_triangle_vertex_vector_user_grid_dataset_streamlines_colorRamp", *mMapSettings, 0, 15 ); } void TestQgsMeshRenderer::test_vertex_vector_traces() @@ -635,8 +729,14 @@ void TestQgsMeshRenderer::test_vertex_vector_traces() mMemoryLayer->setRendererSettings( rendererSettings ); mMemoryLayer->setStaticVectorDatasetIndex( ds ); - QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_traces", mMemoryLayer ) ); - QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_traces_rotated_45", mMemoryLayer, 45.0 ) ); + mMapSettings->setDestinationCrs( mMemoryLayer->crs() ); + mMapSettings->setExtent( mMemoryLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "lines_edge_quad_and_triangle_vertex_vector_traces", "quad_and_triangle_vertex_vector_traces", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 45 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_vertex_vector_traces_rotated_45", "quad_and_triangle_vertex_vector_traces_rotated_45", *mMapSettings, 0, 15 ); + mMapSettings->setRotation( 0 ); } void TestQgsMeshRenderer::test_vertex_vector_traces_colorRamp() @@ -663,7 +763,11 @@ void TestQgsMeshRenderer::test_vertex_vector_traces_colorRamp() mMemoryLayer->setRendererSettings( rendererSettings ); mMemoryLayer->setStaticVectorDatasetIndex( ds ); - QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_traces_colorRamp", mMemoryLayer ) ); + mMapSettings->setDestinationCrs( mMemoryLayer->crs() ); + mMapSettings->setExtent( mMemoryLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "quad_and_triangle_vertex_vector_traces_colorRamp", "quad_and_triangle_vertex_vector_traces_colorRamp", *mMapSettings, 0, 15 ); } void TestQgsMeshRenderer::test_signals() @@ -716,7 +820,11 @@ void TestQgsMeshRenderer::test_stacked_3d_mesh_single_level_averaging() mMdal3DLayer->setRendererSettings( rendererSettings ); mMdal3DLayer->setStaticVectorDatasetIndex( ds ); - QVERIFY( imageCheck( "stacked_3d_mesh_single_level_averaging", mMdal3DLayer ) ); + mMapSettings->setDestinationCrs( mMdal3DLayer->crs() ); + mMapSettings->setExtent( mMdal3DLayer->extent() ); + mMapSettings->setRotation( 0 ); + mMapSettings->setOutputDpi( 96 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "stacked_3d_mesh_single_level_averaging", "stacked_3d_mesh_single_level_averaging", *mMapSettings, 0, 15 ); } void TestQgsMeshRenderer::test_simplified_triangular_mesh_rendering() @@ -733,7 +841,12 @@ void TestQgsMeshRenderer::test_simplified_triangular_mesh_rendering() mMdal3DLayer->setRendererSettings( rendererSettings ); mMdal3DLayer->setMeshSimplificationSettings( simplificatationSettings ); - QVERIFY( imageCheck( "simplified_triangular_mesh", mMdal3DLayer ) ); + + mMapSettings->setDestinationCrs( mMdal3DLayer->crs() ); + mMapSettings->setExtent( mMdal3DLayer->extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "simplified_triangular_mesh", "simplified_triangular_mesh", *mMapSettings, 0, 15 ); } void TestQgsMeshRenderer::test_classified_values() @@ -747,7 +860,11 @@ void TestQgsMeshRenderer::test_classified_values() classifiedMesh.temporalProperties()->setIsActive( false ); classifiedMesh.setStaticScalarDatasetIndex( QgsMeshDatasetIndex( 3, 4 ) ); - QVERIFY( imageCheck( "classified_values", &classifiedMesh ) ); + mMapSettings->setDestinationCrs( classifiedMesh.crs() ); + mMapSettings->setExtent( classifiedMesh.extent() ); + mMapSettings->setOutputDpi( 96 ); + mMapSettings->setRotation( 0 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "classified_values", "classified_values", *mMapSettings, 0, 15 ); } QGSTEST_MAIN( TestQgsMeshRenderer ) From 9076de0956082e35f193f17ca49285f76473915c Mon Sep 17 00:00:00 2001 From: "Juergen E. Fischer" Date: Tue, 30 Apr 2024 09:23:49 +0200 Subject: [PATCH 100/102] add osgeo4w workflow --- .github/workflows/osgeo4w.yml | 118 ++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 .github/workflows/osgeo4w.yml diff --git a/.github/workflows/osgeo4w.yml b/.github/workflows/osgeo4w.yml new file mode 100644 index 0000000000000..deccc879a0c65 --- /dev/null +++ b/.github/workflows/osgeo4w.yml @@ -0,0 +1,118 @@ +name: OSGeo4W Windows Build + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +on: + push: + branches: + - master + - release-** + - queued_ltr_backports + paths: + - 'src/**' + - 'external/**' + - 'python/**' + - 'tests/**' + - 'ms-windows/**' + - 'CMakeLists.txt' + - '.github/workflows/osgeo4w.yml' + pull_request: + workflow_dispatch: + +permissions: + contents: read + +env: + REPO: ${{ github.server_url }}/${{ github.repository }} + +jobs: + osgeo4w-packages: + runs-on: ubuntu-latest + + outputs: + matrix: ${{ steps.osgeo4w-packages.outputs.matrix }} + + steps: + - name: Determine package names + id: osgeo4w-packages + run: | + RELBRANCH=$(git ls-remote --heads $REPO "refs/heads/release-*_*" | sed -e '/\^{}$/d' -ne 's#^.*refs/heads/release-#release-#p' | sort -V | tail -1) + LTRBRANCH=$(git ls-remote --tags $REPO | sed -e '/\^{}$/d' -ne 's#^.*refs/tags/ltr-#release-#p' | sort -V | fgrep -vx $RELBRANCH | tail -1) + + if [ -n "$GITHUB_BASE_REF" ]; then + branch=$GITHUB_BASE_REF + else + branch=${GITHUB_REF##*/} + fi + + case "$branch" in + $LTRBRANCH|queued_ltr_backports) + p=qgis-ltr-dev + ;; + $RELBRANCH) + p=qgis-rel-dev + ;; + master) + p="qgis-dev qgis-qt6-dev" + ;; + *) + echo "Could not determine package name" + exit 1 + ;; + esac + + echo "matrix={\"pkg\":[\"${p// /\",\"}\"]}">>$GITHUB_OUTPUT + + osgeo4w-build: + name: OSGeo4W Windows Build + needs: osgeo4w-packages + runs-on: windows-latest + env: + O4W_REPO: jef-n/OSGeo4W + + strategy: + matrix: ${{ fromJson(needs.osgeo4w-packages.outputs.matrix) }} + + steps: + - name: Restore build cache + uses: actions/cache/restore@v4 + with: + path: ccache + key: build-ccache-osgeo4w-${{ matrix.pkg }}-${{ github.event.pull_request.base.ref || github.ref_name }} + restore-keys: | + build-ccache-osgeo4w-${{ matrix.pkg }} + + - name: Build QGIS + shell: cmd + env: + PKG: ${{ matrix.pkg }} + GITHUB_EVENT_NUMBER: ${{ github.event.number }} + REF: ${{ github.ref }} + PKGDESC: "QGIS build of ${{ github.ref }}" + OSGEO4W_BUILD_RDEPS: 0 + PATH: C:\WINDOWS\system32;C:\Windows + CCACHE_DIR: ${{ github.workspace }}/ccache + SITE: github.com + run: | + curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/%O4W_REPO%/master/bootstrap.cmd>bootstrap.cmd + curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/%O4W_REPO%/master/bootstrap.sh>bootstrap.sh + + set O4W_GIT_REPO=%GITHUB_SERVER_URL%/%O4W_REPO% + call bootstrap.cmd %PKG% + + %GITHUB_WORKSPACE%\scripts\ccache -sv + + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.pkg }} + path: x86_64/ + retention-days: 1 + + - name: Save build cache for push only + uses: actions/cache/save@v4 + if: ${{ github.event_name == 'push' }} + with: + path: ccache + key: build-ccache-osgeo4w-${{ matrix.pkg }}-${{ github.ref_name }}-${{ github.run_id }} From ed0d4e2b1794812d18b9fb2557daca31300f2309 Mon Sep 17 00:00:00 2001 From: "Juergen E. Fischer" Date: Tue, 14 May 2024 01:24:26 +0200 Subject: [PATCH 101/102] apply osgeo4w patches --- .ci/azure-pipelines/azure-pipelines.yml | 221 ------- .gitignore | 14 - CMakeLists.txt | 24 +- external/mdal/frmts/mdal_h2i.cpp | 2 +- external/mdal/mdal_utils.cpp | 14 +- external/qspatialite/CMakeLists.txt | 14 +- external/qspatialite/qsql_spatialite.cpp | 2 +- external/untwine/api/QgisUntwine_win.cpp | 2 +- .../Installer-Files/InstallHeaderImage.bmp | Bin 9354 -> 0 bytes ms-windows/Installer-Files/Install_QGIS.ico | Bin 9262 -> 0 bytes ms-windows/Installer-Files/LICENSE.txt | 28 - ms-windows/Installer-Files/QGIS-WebSite.URL | 2 - ms-windows/Installer-Files/QGIS.ico | Bin 17542 -> 0 bytes ms-windows/Installer-Files/QGIS_Web.ico | Bin 17542 -> 0 bytes .../Installer-Files/UnInstallHeaderImage.bmp | Bin 9354 -> 0 bytes ms-windows/Installer-Files/Uninstall_QGIS.ico | Bin 9662 -> 0 bytes .../Installer-Files/WelcomeFinishPage.bmp | Bin 154542 -> 0 bytes .../Installer-Files/WelcomeFinishPage_old.bmp | Bin 154542 -> 0 bytes ms-windows/Installer-Files/sidelogomaster.svg | 1 - .../Installer-Files/sidelogomaster.xcf.bz2 | Bin 199014 -> 0 bytes ms-windows/QGIS-Installer.nsi | 569 ------------------ ms-windows/osgeo4w/configonly.bat | 46 -- ms-windows/osgeo4w/creatensis.pl | 567 ----------------- ms-windows/osgeo4w/designer.bat.tmpl | 9 - ms-windows/osgeo4w/httpd.conf.tmpl | 28 - ms-windows/osgeo4w/msvc-env.bat | 91 --- ms-windows/osgeo4w/ninja/ninja.bat | 7 - ms-windows/osgeo4w/ninja/ninja.sln | 22 - ms-windows/osgeo4w/ninja/ninja.vcxproj | 108 ---- ms-windows/osgeo4w/package-nightly.cmd | 325 ---------- ms-windows/osgeo4w/package.cmd | 482 --------------- ms-windows/osgeo4w/postinstall-common.bat | 5 - ms-windows/osgeo4w/postinstall-desktop.bat | 25 - ms-windows/osgeo4w/postinstall-dev.bat | 55 -- .../postinstall-grass-plugin-common.bat | 2 - ms-windows/osgeo4w/postinstall-grass.bat | 13 - ms-windows/osgeo4w/postinstall-server.bat | 5 - ms-windows/osgeo4w/preremove-desktop.bat | 15 - ms-windows/osgeo4w/preremove-dev.bat | 30 - .../osgeo4w/preremove-grass-plugin-common.bat | 1 - ms-windows/osgeo4w/preremove-grass.bat | 10 - ms-windows/osgeo4w/preremove-server.bat | 1 - ms-windows/osgeo4w/process.bat.tmpl | 14 - ms-windows/osgeo4w/python.bat.tmpl | 14 - ms-windows/osgeo4w/qgis-grass.bat.tmpl | 15 - ms-windows/osgeo4w/qgis.bat.tmpl | 13 - ms-windows/osgeo4w/qgis.reg.tmpl | 48 -- ms-windows/osgeo4w/qgis.vars | 30 - ms-windows/osgeo4w/runasadmin.ps1 | 4 - ms-windows/plugins.nsh | 14 - ms-windows/python_plugins.nsh | 14 - ms-windows/quickpackage.sh | 40 -- ms-windows/x64.nsh | 54 -- python/PyQt6/gui/gui_auto.sip | 2 + python/gui/gui_auto.sip | 2 + src/app/CMakeLists.txt | 4 + src/app/main.cpp | 10 +- src/app/qgisapp.cpp | 4 + src/core/layout/qgslayoutitemlegend.cpp | 2 +- src/core/mesh/qgstopologicalmesh.cpp | 2 +- src/core/pal/labelposition.cpp | 2 +- src/core/pointcloud/qgslazdecoder.cpp | 2 + .../pointcloud/qgspointcloudlayerrenderer.cpp | 2 +- src/core/qgsattributetableconfig.h | 2 +- src/core/qgsexpressioncontext.cpp | 4 + src/core/qgsopenclutils.cpp | 26 +- src/gui/CMakeLists.txt | 9 +- src/native/CMakeLists.txt | 2 +- src/native/win/qgswinnative.cpp | 24 +- src/native/win/qgswinnative.h | 14 +- src/providers/grass/qgsgrass.cpp | 4 +- src/server/qgis_mapserver.cpp | 4 +- 72 files changed, 126 insertions(+), 2995 deletions(-) delete mode 100644 .ci/azure-pipelines/azure-pipelines.yml delete mode 100644 ms-windows/Installer-Files/InstallHeaderImage.bmp delete mode 100644 ms-windows/Installer-Files/Install_QGIS.ico delete mode 100644 ms-windows/Installer-Files/LICENSE.txt delete mode 100644 ms-windows/Installer-Files/QGIS-WebSite.URL delete mode 100644 ms-windows/Installer-Files/QGIS.ico delete mode 100644 ms-windows/Installer-Files/QGIS_Web.ico delete mode 100644 ms-windows/Installer-Files/UnInstallHeaderImage.bmp delete mode 100644 ms-windows/Installer-Files/Uninstall_QGIS.ico delete mode 100644 ms-windows/Installer-Files/WelcomeFinishPage.bmp delete mode 100644 ms-windows/Installer-Files/WelcomeFinishPage_old.bmp delete mode 100644 ms-windows/Installer-Files/sidelogomaster.svg delete mode 100644 ms-windows/Installer-Files/sidelogomaster.xcf.bz2 delete mode 100644 ms-windows/QGIS-Installer.nsi delete mode 100644 ms-windows/osgeo4w/configonly.bat delete mode 100755 ms-windows/osgeo4w/creatensis.pl delete mode 100644 ms-windows/osgeo4w/designer.bat.tmpl delete mode 100644 ms-windows/osgeo4w/httpd.conf.tmpl delete mode 100644 ms-windows/osgeo4w/msvc-env.bat delete mode 100644 ms-windows/osgeo4w/ninja/ninja.bat delete mode 100644 ms-windows/osgeo4w/ninja/ninja.sln delete mode 100644 ms-windows/osgeo4w/ninja/ninja.vcxproj delete mode 100644 ms-windows/osgeo4w/package-nightly.cmd delete mode 100644 ms-windows/osgeo4w/package.cmd delete mode 100644 ms-windows/osgeo4w/postinstall-common.bat delete mode 100644 ms-windows/osgeo4w/postinstall-desktop.bat delete mode 100644 ms-windows/osgeo4w/postinstall-dev.bat delete mode 100644 ms-windows/osgeo4w/postinstall-grass-plugin-common.bat delete mode 100644 ms-windows/osgeo4w/postinstall-grass.bat delete mode 100644 ms-windows/osgeo4w/postinstall-server.bat delete mode 100644 ms-windows/osgeo4w/preremove-desktop.bat delete mode 100644 ms-windows/osgeo4w/preremove-dev.bat delete mode 100644 ms-windows/osgeo4w/preremove-grass-plugin-common.bat delete mode 100644 ms-windows/osgeo4w/preremove-grass.bat delete mode 100644 ms-windows/osgeo4w/preremove-server.bat delete mode 100644 ms-windows/osgeo4w/process.bat.tmpl delete mode 100644 ms-windows/osgeo4w/python.bat.tmpl delete mode 100644 ms-windows/osgeo4w/qgis-grass.bat.tmpl delete mode 100644 ms-windows/osgeo4w/qgis.bat.tmpl delete mode 100644 ms-windows/osgeo4w/qgis.reg.tmpl delete mode 100644 ms-windows/osgeo4w/qgis.vars delete mode 100644 ms-windows/osgeo4w/runasadmin.ps1 delete mode 100644 ms-windows/plugins.nsh delete mode 100644 ms-windows/python_plugins.nsh delete mode 100755 ms-windows/quickpackage.sh delete mode 100644 ms-windows/x64.nsh diff --git a/.ci/azure-pipelines/azure-pipelines.yml b/.ci/azure-pipelines/azure-pipelines.yml deleted file mode 100644 index 73406cb0724cf..0000000000000 --- a/.ci/azure-pipelines/azure-pipelines.yml +++ /dev/null @@ -1,221 +0,0 @@ -variables: - LR: release-3_12 - LTR: release-3_10 - CTEST_CUSTOM_TESTS_IGNORE: "ProcessingGdalAlgorithmsRasterTest;ProcessingGdalAlgorithmsVectorTest;ProcessingGrass7AlgorithmsImageryTest;ProcessingGrass7AlgorithmsRasterTest;ProcessingGrass7AlgorithmsVectorTest;ProcessingGuiTest;ProcessingOtbAlgorithmsTest;ProcessingQgisAlgorithmsTestPt1;ProcessingQgisAlgorithmsTestPt2;ProcessingQgisAlgorithmsTestPt3;ProcessingQgisAlgorithmsTestPt4;ProcessingScriptUtilsTest;PyQgsAnnotation;PyQgsAppStartup;PyQgsAuthManagerOAuth2OWSTest;PyQgsAuthManagerPasswordOWSTest;PyQgsAuthManagerPKIOWSTest;PyQgsAuthManagerProxy;PyQgsAuthSettingsWidget;PyQgsAuxiliaryStorage;PyQgsBlockingNetworkRequest;PyQgsExifTools;PyQgsFileDownloader;PyQgsFileUtils;PyQgsGeometryTest;PyQgsImageCache;PyQgsImportIntoPostGIS;PyQgsLayoutAtlas;PyQgsLayoutLegend;PyQgsLayoutMap;PyQgsLayoutMapGrid;PyQgsMapLayer;PyQgsOfflineEditingWFS;PyQgsOGRProvider;PyQgsOGRProviderGpkg;PyQgsOGRProviderSqlite;PyQgsPalLabelingCanvas;PyQgsPalLabelingLayout;PyQgsPalLabelingPlacement;PyQgsPointDisplacementRenderer;PyQgsProject;PyQgsProviderConnectionGpkg;PyQgsProviderConnectionPostgres;PyQgsPythonProvider;PyQgsRasterFileWriter;PyQgsRasterLayer;PyQgsSelectiveMasking;PyQgsServerAccessControlWMSGetlegendgraphic;PyQgsServerApi;PyQgsServerCacheManager;PyQgsServerLocaleOverride;PyQgsServerSecurity;PyQgsServerSettings;PyQgsServerWMS;PyQgsServerWMSDimension;PyQgsServerWMSGetFeatureInfo;PyQgsServerWMSGetLegendGraphic;PyQgsServerWMSGetMap;PyQgsServerWMSGetPrint;PyQgsServerWMSGetPrintExtra;PyQgsServerWMSGetPrintOutputs;PyQgsServerWMSGetPrintAtlas;PyQgsServerWMTS;PyQgsSettings;PyQgsShapefileProvider;PyQgsSpatialiteProvider;PyQgsSvgCache;PyQgsSymbolLayer;PyQgsTaskManager;PyQgsTextRenderer;PyQgsVectorFileWriter;PyQgsVectorLayer;PyQgsVectorLayerUtils;PyQgsVirtualLayerProvider;PyQgsWFSProviderGUI;PyQgsZipUtils;qgis_3drenderingtest;qgis_alignrastertest;qgis_arcgisrestutilstest;qgis_banned_keywords;qgis_browsermodeltest;qgis_callouttest;qgis_compositionconvertertest;qgis_coordinatereferencesystemtest;qgis_datadefinedsizelegendtest;qgis_datumtransformdialog;qgis_diagramtest;qgis_doxygen_order;qgis_dxfexporttest;qgis_expressiontest;qgis_filedownloader;qgis_geometrycheckstest;qgis_geometrytest;qgis_grassprovidertest7;qgis_imagecachetest;qgis_invertedpolygonrenderertest;qgis_labelingenginetest;qgis_layerdefinitiontest;qgis_layout3dmaptest;qgis_layouthtmltest;qgis_layoutlabeltest;qgis_layoutmapgridtest;qgis_layoutmaptest;qgis_layoutpicturetest;qgis_layoutscalebartest;qgis_layouttabletest;qgis_legendrenderertest;qgis_licenses;qgis_defwindowtitle;qgis_maprendererjobtest;qgis_maprotationtest;qgis_mapsettingsutilstest;qgis_maptooladdfeatureline;qgis_mimedatautilstest;qgis_networkaccessmanagertest;qgis_openclutilstest;qgis_painteffecttest;qgis_pallabelingtest;qgis_processingtest;qgis_projecttest;qgis_qgisappclipboard;qgis_rasterlayersaveasdialog;qgis_shellcheck;qgis_sipify;qgis_sip_include;qgis_sip_uptodate;qgis_spelling;qgis_styletest;qgis_svgcachetest;qgis_taskmanagertest;qgis_transformdialog;qgis_vectorfilewritertest;qgis_wcsprovidertest;qgis_ziplayertest;qgis_meshcalculator;qgis_pointlocatortest;PyQgsExpressionBuilderWidget;PyQgsDatumTransform;qgis_vertextool;PyQgsCoordinateOperationWidget;PyQgsProviderConnectionSpatialite;qgis_maptoolsplitpartstest;qgis_vectortilelayertest;qgis_ogrproviderguitest" - Agent.Source.Git.ShallowFetchDepth: 120 - -trigger: - branches: - include: -# - master - - $(LR) - - $(LTR) - - azure-pipelines - -pr: -#- master -- $(LR) -- $(LTR) - -jobs: -- job: OSGeo4W - pool: - vmImage: vs2017-win2016 - timeoutInMinutes: 360 - strategy: - maxParallel: 4 - matrix: - x86: - OSGEO4W_ROOT: C:\OSGeo4W - OSGEO4W_ARCH: x86 - CLCACHE_DIR: c:\clcache-x86 - PLATFORM: x86 - CC: C:\OSGeo4W\bin\clcache.bat - CXX: C:\OSGeo4W\bin\clcache.bat - - x86_64: - OSGEO4W_ROOT: C:\OSGeo4W64 - OSGEO4W_ARCH: x86_64 - CLCACHE_DIR: c:\clcache-x86_64 - PLATFORM: x64 - CC: C:\OSGeo4W64\bin\clcache.bat - CXX: C:\OSGeo4W64\bin\clcache.bat - - steps: - - bash: | - if [ "$BUILD_REASON" = "PullRequest" ]; then - branch=$SYSTEM_PULLREQUEST_TARGETBRANCH - pr=$SYSTEM_PULLREQUEST_PULLREQUESTNUMBER - else - branch=$BUILD_SOURCEBRANCHNAME - fi - - echo "BRANCH: ${branch}" - echo "PR: ${pr}" - echo "LR: ${LR}" - echo "LTR: ${LTR}" - - case "${branch}" in - "${LTR}") - OSGEO4W_PKG=qgis-ltr-dev - OSGEO4W_DEPS=qgis-ltr-dev-deps - ;; - "${LR}") - OSGEO4W_PKG=qgis-rel-dev - OSGEO4W_DEPS=qgis-rel-dev-deps - ;; - *) - OSGEO4W_PKG=qgis-dev - OSGEO4W_DEPS=qgis-dev-deps - ;; - esac - - target=Experimental - major=$(sed -ne 's/^SET(CPACK_PACKAGE_VERSION_MAJOR "\([0-9]*\)")\s*$/\1/ip' CMakeLists.txt) - minor=$(sed -ne 's/^SET(CPACK_PACKAGE_VERSION_MINOR "\([0-9]*\)")\s*$/\1/ip' CMakeLists.txt) - patch=$(sed -ne 's/^SET(CPACK_PACKAGE_VERSION_PATCH "\([0-9]*\)")\s*$/\1/ip' CMakeLists.txt) - binary=$(curl --location-trusted http://ftp.osuosl.org/pub/osgeo/download/osgeo4w/$OSGEO4W_ARCH/release/qgis/$OSGEO4W_PKG/LATEST.sha | sed -e "s/:.*$//") - (( binary++ )) || true - - version=$major.$minor.$patch - sha="${BUILD_SOURCEVERSION:0:10}" - - if [ "$BUILD_REASON" = "PullRequest" ]; then - buildname="PR $pr / $branch ($BUILD_BUILDID) ($sha) ($OSGEO4W_PKG $target $OSGEO4W_ARCH)" # no colons allowed here - else - buildname="$OSGEO4W_PKG-$version-$sha-$target-VC14-$OSGEO4W_ARCH" - fi - - url=$buildname - url=${url//(/%28} - url=${url//)/%29} - url=${url// /+} - url="https://cdash.orfeo-toolbox.org/index.php?project=QGIS&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercount=4&showfilters=0&filtercombine=and&field1=buildname&compare1=61&value1=$url&field2=site&compare2=65&value2=azure-pipelines&field3=buildstarttime&compare3=83&value3=$(date +%Y-%m-%d --date=yesterday)&field4=buildstarttime&compare4=84&value4=$(date +%Y-%m-%d --date=tomorrow)" - - echo "##vso[task.setvariable variable=TARGET]$target" - echo "##vso[task.setvariable variable=OSGEO4W_PKG]$OSGEO4W_PKG" - echo "##vso[task.setvariable variable=OSGEO4W_DEPS]$OSGEO4W_DEPS" - echo "##vso[task.setvariable variable=MAJOR]$major" - echo "##vso[task.setvariable variable=MINOR]$minor" - echo "##vso[task.setvariable variable=PATCH]$patch" - echo "##vso[task.setvariable variable=BINARY]$binary" - echo "##vso[task.setvariable variable=VERSION]$version" - echo "##vso[task.setvariable variable=BUILDNAME]$buildname" - echo "##vso[task.setvariable variable=DASHURL]${url//&/^&}" - - displayName: 'Setup build variables' - - - script: curl --output c:\setup-x86.exe https://cygwin.com/setup-x86.exe - displayName: 'Download cygwin Installer' - - - script: curl --output c:\osgeo4w-setup.exe http://ftp.osuosl.org/pub/osgeo/download/osgeo4w/osgeo4w-setup-%OSGEO4W_ARCH%.exe - displayName: 'Download OSGeo4W Installer' - - - script: curl --location-trusted --output c:\ninja.zip https://github.com/ninja-build/ninja/releases/download/v1.9.0/ninja-win.zip - displayName: 'Download Ninja' - -# - script: curl --location-trusted --output c:\depends.zip http://www.dependencywalker.com/depends22_%PLATFORM%.zip -# displayName: 'Download Dependency walker' - -# Too large… -# - task: Cache@2 -# inputs: -# key: 'cygwin | $(Date:yyyyMMdd)' -# path: 'c:\cygwin' -# restoreKeys: | -# cygwin | $(Date:yyyyMM) -# cygwin | $(Date:yyyy) -# cygwin -# displayName: Cache cygwin - - - powershell: ms-windows/osgeo4w/runasadmin.ps1 c:\setup-x86.exe -qnNdO -R C:/cygwin -s http://cygwin.mirror.constant.com -l C:/temp/cygwin -P "bison,flex,git,poppler,doxygen,unzip" - displayName: 'Installing cygwin' - -# Too large… -# - task: Cache@2 -# inputs: -# key: 'osgeo4w | $(OSGEO4W_ARCH) | $(Date:yyyyMMdd)' -# path: '$(OSGEO4W_ROOT)' -# restoreKeys: | -# osgeo4w | $(OSGEO4W_ARCH) | $(Date:yyyyMMdd) -# osgeo4w | $(OSGEO4W_ARCH) | $(Date:yyyyMM) -# osgeo4w | $(OSGEO4W_ARCH) | $(Date:yyyy) -# osgeo4w | $(OSGEO4W_ARCH) -# displayName: Cache OSGeo4W - - - powershell: ms-windows/osgeo4w/runasadmin.ps1 c:\osgeo4w-setup.exe --autoaccept --advanced --arch $env:OSGEO4W_ARCH --quiet-mode --upgrade-also --root $env:OSGEO4W_ROOT --only-site -s http://ftp.osuosl.org/pub/osgeo/download/osgeo4w -l c:\temp\osgeo4w -P $env:OSGEO4W_DEPS -P python3-clcache - displayName: 'Installing OSGeo4W' - - - script: | - rmdir /s /q c:\temp\cygwin - rmdir /s /q c:\temp\osgeo4w - displayName: 'Clear package caches' - - - script: c:\cygwin\bin\unzip -o c:\ninja.zip -d %OSGEO4W_ROOT%\bin - displayName: 'Extracting Ninja' - -# - script: c:\cygwin\bin\unzip -o c:\depends.zip -d %OSGEO4W_ROOT%\bin -# displayName: 'Extracting Dependency Walker' - - - script: | - PATH %OSGEO4W_ROOT%\bin;%ProgramFiles%\CMake\bin;%PATH% - cmake --version - ctest --version - ninja --version - displayName: 'Display tool versions' - -# Too large… -# - task: Cache@2 -# inputs: -# key: 'clcache | $(OSGEO4W_ARCH) | $(OSGEO4W_PKG) | $(Date:yyyyMMdd) | $(Hours)' -# path: '$(CLCACHE_DIR)' -# restoreKeys: | -# clcache | $(OSGEO4W_ARCH) | $(OSGEO4W_PKG) | $(Date:yyyyMMdd) | $(Hours) -# clcache | $(OSGEO4W_ARCH) | $(OSGEO4W_PKG) | $(Date:yyyyMMdd) -# clcache | $(OSGEO4W_ARCH) | $(OSGEO4W_PKG) | $(Date:yyyyMM) -# clcache | $(OSGEO4W_ARCH) | $(OSGEO4W_PKG) | $(Date:yyyy) -# clcache | $(OSGEO4W_ARCH) | $(OSGEO4W_PKG) -# displayName: Cache clcache - - - script: | - echo on - PATH c:\cygwin\bin;%OSGEO4W_ROOT%\bin;%PATH% - cd ms-windows\osgeo4w - touch skippackage - set OSGEO4W_CXXFLAGS=/MD /MP /Od /D NDEBUG - @echo ##[section]%OSGEO4W_ARCH% results available at %DASHURL% - package-nightly.cmd %VERSION% %BINARY% %OSGEO4W_PKG% %OSGEO4W_ARCH% %BUILD_SOURCEVERSION:~0,10% azure-pipelines - displayName: 'Building QGIS' - -# - script: | -# echo on -# PATH %OSGEO4W_ROOT%\bin;%PATH% -# cd ms-windows\osgeo4w\build-%OSGEO4W_PKG%-%OSGEO4W_ARCH% -# set /P tag= #ifdef _MSC_VER +#ifndef UNICODE #define UNICODE +#endif #include #include #include @@ -1113,12 +1115,12 @@ std::vector MDAL::Library::libraryFilesInDir( const std::string &di { std::vector filesList; #ifdef _WIN32 - WIN32_FIND_DATA data; + WIN32_FIND_DATAA data; HANDLE hFind; std::string pattern = dirPath; pattern.push_back( '*' ); - hFind = FindFirstFile( pattern.c_str(), &data ); + hFind = FindFirstFileA( pattern.c_str(), &data ); if ( hFind == INVALID_HANDLE_VALUE ) return filesList; @@ -1129,7 +1131,7 @@ std::vector MDAL::Library::libraryFilesInDir( const std::string &di if ( !fileName.empty() && fileExtension( fileName ) == ".dll" ) filesList.push_back( fileName ); } - while ( FindNextFile( hFind, &data ) != 0 ); + while ( FindNextFileA( hFind, &data ) != 0 ); FindClose( hFind ); #else @@ -1140,8 +1142,8 @@ std::vector MDAL::Library::libraryFilesInDir( const std::string &di std::string fileName( de->d_name ); if ( !fileName.empty() ) { - std::string extentsion = fileExtension( fileName ); - if ( extentsion == ".so" || extentsion == ".dylib" ) + std::string extension = fileExtension( fileName ); + if ( extension == ".so" || extension == ".dylib" ) filesList.push_back( fileName ); } de = readdir( dir ); @@ -1160,7 +1162,7 @@ bool MDAL::Library::loadLibrary() #ifdef _WIN32 UINT uOldErrorMode = SetErrorMode( SEM_NOOPENFILEERRORBOX | SEM_FAILCRITICALERRORS ); - d->mLibrary = LoadLibrary( d->mLibraryFile.c_str() ); + d->mLibrary = LoadLibraryA( d->mLibraryFile.c_str() ); SetErrorMode( uOldErrorMode ); #else d->mLibrary = dlopen( d->mLibraryFile.c_str(), RTLD_LAZY ); diff --git a/external/qspatialite/CMakeLists.txt b/external/qspatialite/CMakeLists.txt index 33536dafe913c..527f383303c7c 100644 --- a/external/qspatialite/CMakeLists.txt +++ b/external/qspatialite/CMakeLists.txt @@ -7,18 +7,18 @@ add_definitions(-DQT_SHARED) include_directories(SYSTEM ${SQLITE3_INCLUDE_DIR} - ${Qt5Sql_PRIVATE_INCLUDE_DIRS} + ${${QT_VERSION_BASE}Sql_PRIVATE_INCLUDE_DIRS} ) -set(QSQLSPATIALITE_SRC qsql_spatialite.cpp smain.cpp) -QT5_WRAP_CPP(QSQLSPATIALITE_SRC qsql_spatialite.h smain.h) +set(QSQLSPATIALITE_SRC qsql_spatialite.cpp smain.cpp qsql_spatialite.h smain.h) add_library(qsqlspatialite SHARED ${QSQLSPATIALITE_SRC}) + target_link_libraries(qsqlspatialite - ${Qt5Core_LIBRARIES} - ${Qt5Sql_LIBRARIES} - spatialite::spatialite - qgis_core + ${QT_VERSION_BASE}::Core + ${QT_VERSION_BASE}::Sql + spatialite::spatialite + qgis_core ) install(TARGETS qsqlspatialite diff --git a/external/qspatialite/qsql_spatialite.cpp b/external/qspatialite/qsql_spatialite.cpp index 0e8f15a25f766..5a75a04748b31 100644 --- a/external/qspatialite/qsql_spatialite.cpp +++ b/external/qspatialite/qsql_spatialite.cpp @@ -632,7 +632,7 @@ bool QSpatiaLiteDriver::open( const QString &db, const QString &, const QString bool openReadOnlyOption = false; bool openUriOption = false; - const auto opts = conOpts.splitRef( QLatin1Char( ';' ) ); + const auto opts = conOpts.split( QLatin1Char( ';' ) ); for ( auto option : opts ) { option = option.trimmed(); diff --git a/external/untwine/api/QgisUntwine_win.cpp b/external/untwine/api/QgisUntwine_win.cpp index 7469655df8f81..47d355ce36759 100644 --- a/external/untwine/api/QgisUntwine_win.cpp +++ b/external/untwine/api/QgisUntwine_win.cpp @@ -28,7 +28,7 @@ bool QgisUntwine::start(Options& options) cmdline += "--" + op.first + " \"" + op.second + "\" "; PROCESS_INFORMATION processInfo; - STARTUPINFO startupInfo; + STARTUPINFOA startupInfo; ZeroMemory(&processInfo, sizeof(PROCESS_INFORMATION)); ZeroMemory(&startupInfo, sizeof(STARTUPINFO)); diff --git a/ms-windows/Installer-Files/InstallHeaderImage.bmp b/ms-windows/Installer-Files/InstallHeaderImage.bmp deleted file mode 100644 index 2332341daa04f184dbeb4f15a896e8ac8c4b3253..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9354 zcmd^^dr(x@9mk=X#K}Y_HHl7Z8r$-IEG(wADm9%kljd0-#xX65;sXJL79T+Y6(uU5 zqM)X{RuB-7HzrOy(`NEVC!Nf6tPM43O~$l38UiFmF@}c`PCs|?Y%Z6(d+&nuk9Nn8 z=iGbG{rx`Y%dWV)i+*vy+r$1iz~4K-dKXWR?|V#Au5dm3d&4M)!w=!%G5kk46pNl+ zyyfl%)5Et+ul)aA_~dV5Ji`i?PYWwrK7;YGu)<|ifAmI-r@M*FDU5k$PT{XUfh$_( z6|cAwQJwNjcWY|bhdy0J%o>uo~Z_h$6XfqJ=O5ay(`}=HADpJM-trq*pJOdA7A8n#^CWPg7&)Z!K9p!&CDqz`iGXTv4)nez5>Ed3FEy1_toG1#(mWbWYedY z&-}6k5nYdK!wwxXUXeAlV`%Bxg>W5kSF?uWaq$oCcepcuk!<=M8Of{|=a(npo*@?F zu0ugYZL03S$)UFRxo{K27`e@!ufYXqgUi(EWBfaOM`p$RCBR*s&+vRf`KuyY@owA& zJGC5+Xxy#q9%5Vf1ayFKlP^MT>k9Dvxz)CAYOpPSM19Ub01r9bZSgO0+z+ENT5vL1 zL@VA+nWJgkbuh9aeMD_p(_%}U9C9M@-H;Or!VSJ)Z3=sRP}VUTLu?5Vtnb&$XFkeS0ZU3aBtNLf;9$cZG}&60SQ{mwM&kgx<@E^MZ-cP=t8)73sn>ActH%)MmA4l52l)m= z2jH083y;Zb!6!Fv17U(r_BTV`XJ&jPt$Xq8L)~8~?%((iXSA?7RYbMygZ&!z4RJnA z*0+>z#C-$qBeLBpuLqy}^C+ z9X~xPQXUVg*!&6zBiN_%R@c-6=cUqJ>%a8!wL2@`f3I()4olyuPL>Vmhx?{ZAX}#_ z4XWId55fdLmbG1tT6eX7Oh=DM>iSZTp~S8)9qzcy2&&xN33$2WNKn<*vmhK)G9OJZ z=N@q7tz3IFt$P~21B$a6<@w{neGvB~b#45o(eG;=53SqTz^c@9AY2htmHJn8eP?t0 zr{g`Sc30-12W|{Tymj-A4g)dgZpQ1?3#xkUGLWlF&IO*@b_s+5Z|U{Wp40OW-hV@c z=lAGwXF-0C)At|b`6pMI-o1wV_N#!uOX{_*ddFISi);5)dNt%Z@_gGMQzi4O0o7^$ z1R)7zu7lh8hh#g=eURK^rKhto@KoBnfTK%338>CE5BR&}yt6*)^Gg0O80y^pPWX|Y zW$JsJ%>$~_{{?uthm&OrUukx zd<}TH1oxe9_}Aj>av$TI)h z-Cq8ucMB6-^*_DG@OPMW80(176tZkLk*AXR50pN5eE{siki@ZinY|B(1QS^*dAG@$ zesx*9K$xJ@ukLln`v!Ucy&CC1-S2c(I|vhG$i6tu@~hiB0PMk#Gl4Z(ZoU7KeT+st zY5@mB`dQ=n97(C`SHG_egh}#zS`OXQJj_w#MY$pB$YHft1e-htWb8t%-wcRF?IhHJ^1hNI)X zrM8!Eea?Npjk)guZl<}#`YbU0x@PTzumS%rFp-yI%^%sT{p(GwyFB)3JTMzNK)6XS z>l+{r0(}~CCo$SzBfgC}p3IfHOP~n;4KC{Ea%~i!vS;1rjdYizPvgP4K1~OEK#YR{ z?){uvoxt9xHFmDE-!SQCX7p(~6zFM*g? z@~E#vQ{F|g^V52{bL`!GG#9#>_J)U-I5;(m%LjF#4!1c z6T(F@dfr_>GwZ!t3hsvlua=`9fpAU%*WBhqa#;iSnQnvA5v^!0p%#xDM@b0sa7`5D%f=EyrE{ LE>idTU!DI4WR^=H diff --git a/ms-windows/Installer-Files/Install_QGIS.ico b/ms-windows/Installer-Files/Install_QGIS.ico deleted file mode 100644 index 752cb9e25f09de8e2b04261c51da9871ee83a496..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9262 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 diff --git a/ms-windows/Installer-Files/QGIS_Web.ico b/ms-windows/Installer-Files/QGIS_Web.ico deleted file mode 100644 index e07ad44c0b52781cc12867d99711cd60549d6526..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17542 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 diff --git a/ms-windows/Installer-Files/WelcomeFinishPage_old.bmp b/ms-windows/Installer-Files/WelcomeFinishPage_old.bmp deleted file mode 100644 index 72e3579bc255d790181519e957cfbcb66a32b5a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 154542 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 \ No newline at end of file diff --git a/ms-windows/Installer-Files/sidelogomaster.xcf.bz2 b/ms-windows/Installer-Files/sidelogomaster.xcf.bz2 deleted file mode 100644 index 16b6b71e66a8d8b82d5d2eae06feb520cbc405a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 199014 zcmagEbxa&g^zV%nXrVx9i&JQel%l0*aVfMEE5)7S?!GK;#VN&MDNx*9mR;Q4VHb9B zXBS^!k^B7e=DmO1w8ufTzMvaMFL#;FOaUu zD0O3RQKO&>6ufc;HTmB(oR>H_Nbr@GScNgE8#p4<0e2Sdl`ON>GHs&ORBIo9+e-O>{^T2D5DmzZ8F|UW6I)3eZ6l< z-zHxYL8?OgU*q5q90SG5{g~*RIFo*aakEWCF5aTsqfx&&Y;1^d4jS-+ z{Yi5HsA@0yO7rjD&52+fKQ4}V7zYj!o__G;)y+0$esyMJ(bedq2VP{&?m3Y4U==eK zkq{zs11Q3J&Q)b5O@jfEj!+Yaa6^yRz}2}N;`su55<#sW{?ial4dac@&#oHmu&7t1 z!G|Wc&YR=rVS;w()H+xaCIFnMT4~TIeg1w`;#l%Ta&4REH3OUZf^e=bQ5^;e}{SL$!H5ENUc(Uwf2?}Skz^8aCmv@L-bho?Nix5sUU=(l*8J zEFrGf53;5<1^j%Q;!ms%t^T6HK`dDFT%&>JjBG~CGVUfBleL?*CMIhdqQ#E%#UMW& z?=S(DUjoeF2!r-B>+hJFgQ>qFSpLX(Ai9N#bDQkM3R5-Kn*QUp^-``cl6mkI%N`ir zzLTLGIaw9gnv-pbTK;}c-QEm7ylDnbNJrnSY@i`yXOKgW8_5GgfBvm`HBY^AL16?C zvx59IUo9upn1L2+Mv8$}^7}_K`j(cUJ<~CBg*v%jUMQ5GV+_5`&PwXo#j)hPSXMI< zD277aTsHiJ4WqM`F98*p#)dzYzG!t<9ai%6@ZKI<%s}iD2@#z)BT|5rorThCt#EfPf+b14L(AH#^MH3oSM^ zdat0)P&=mhOA0AUprxgimBY>~3n5`dy_h6w9ZLv*tp+$cXy0*16|3p!2f=+Yh&>GA zB1c_mrUf+6Ev`YP35*7S(5nU#ea$ z3Bo|oP)V$IQ=ISI^42nJ>2eL>F;rJ2q+btexu355E4 zL(Y(rD+4xwR`|s0n8)Bx-XrLA>!po zFaH$&Eesg#oeN9>*&U#+&(P%{&s-q_q`tZ+iPiO-F{ z@T@Cl2`M>tflx%9u?_(EivjI7rvQJf{u!Eo0=(~k<1XUw*1Ui;1}ydLI$h7Ix%bZ; zVJ1I=@^-R)g{xImh3kDyoZFr2bA2zpp^*9=1X2TOVC{Ki;+??e?RT^`lLPkw0gf!Gql2W0djDlMt`^hYaE!0Y84IPLFVSb~_#<67TNbkIz{F$W*m4!Mqw0c{3OsC5=SCV zhuJsY#`O`dp`Mp6#)r$aJ5A_fPi3~7aS{^%U>A#6Ao{dSZ`7hU);$N06t$l5^VR?( z&;kgM`z|J}b#Di0pCg7AmdvV#B7n4K3^@pRLV+jBABx=?DDua=bXO)pE$_8oboSK6 zEx1qRSbq`eqDPTl><}%%a}}^#omnW7s#T-$r-*%t{Z1IRIo%t9MK_*H?syjf(SXTz z6xl?TQN+ywxK>Ou{%obdf!yjG0C6u^Iq-@DtsaeatpraddJq#d$=P*T5fDg_;_2fj z+UAEgwj91{w#xt4Y0uyd%#4i9l5yL|WZ;NdgG&MNm=>TegBp3`h3C%9fh+h;^$Ro* za>bEFxae}BmE z3%9HQ-1e6vrUJu=cJ)k8GYl%k!INhH`k%DGVO1pl_XUz)Ru-gAtc7P(Vq-5sSNg~dRXE=C2asCrTPm1tNap>@##?e`1 z6nyT?NS66CGwsMs@hh(=HbcYDW)P&Tkg2A^|IIZ0yCcQhXF<;d>`d{@Yy@m(&1{o7 z0+cC2WeO-tgNtG_NpX^KlNtpkCp+I4jXn2cBaEF_1Jkl_;=Hk%1__dHnb>F3yt>3)=q{WM@hk zDy^&{@uUF<6lP{&`*(7R@1s@eX7Q?u2FyaB`=f<`n&R%X-$%Q58IMjAG%urmmihVp z4QG*gHv9AA3+3m2nWgCWaUbF|F@KKB0aX1gyio$2RS937zYNAa3%V!F7I z7$j6E#bI&5#~%^j@wsfVir?Q@f;J6v$eXsGVqb1>YRA4;)s!0zS^-||m`Ohjy$>8- zzyYt=zo4~P)!hFojboe`8vRTwubB!k^rBaXSr?%==5Q?p&#p44zg$3=S=`;&fwKt= ziarO>RpP7v!CB5;=Dzf+Y?pqbMf8Y*4Y#CVJlytirsrlx@4vT1-EN2N_m34Mo_yL> zWLX$Ww8uGoEc{p`j8Qq{n4~q%n+Io0{aF}`F2wC(E#R7LSAN6-cY-5vUsF$~>3U)? zhE0qi_+Fl7$0CVWR-iEdC5gb(1M6{PCxwZ_A;aVVFFEJhVVC zR6FRz4~Ic$C_cAkT%yAJi((|oQ)Z!UTa_Vi2e?hN*5TN4*}sC3r04mq%JIgtIhAvs zoW<7F>f0*=LTo`dO4qxu{T8^Njc@wWfE~yB8EqYCjibw8(>i}LA1CS&>5s{Lu69)? z!c~|?WM%_rHulA%Q?XeWw~)_21Qes9PlRG1ixr7zW#Vw|`^JR1)f!)JaylXC8Pk)t z@R0Xq(n^mNA2}w*3huJg?o#5{$;HbuS7ym4O5@2y4&h}EZ}ODCV6~rz8yzg#iP>Vd9uRw}&ha_B%XX=K8k9o`ySsBA1mnw91^(f)=^3r%GOpum2{0>q)d9 zF?1Nj@)Y+v-_2e7RaM=`$OQW(NeLSEg?^gNp{3W2jyn!1X-+B&=FeD;Cm0x>gG$*~ z-@Z3C(z{K3%1n$;_t2><)n!7P+Wl_iI94pXO8iNqr^z;iy+3DVBRM!l8?P{X!OLU`wO)`y`N4?Gd z!ORhg!&#;%?rB_`gU!dR1H+?}b8WM?0yvty=SR(YV&n?En(v=&S1x$%A3if(qa#~{o2(x^wVWLizNCPVd`zc=;XOc zbu|N4@*$OTsI6CNqz9t1-h1b5x#R5K)=#xgwOv*YtirAM5?wG^!7uZ7_VcD6IeJ-w z+Zz8BwZ}KY3r5bYa#`QlILG={$UDJa&nnhk-rKgNEVJVlyTkU|P(vOQNL758pAX6? z_b-FCa{_?W5Wm;Z(BQ6WfrtqdQrn|IO6sN~{+~cSH`)+BM%NmC|I}ho!EHu*S@7w- z4CdI?34dhx?Xz&4UnSegZeTF)$I`_=A-f*nX(h z?OD^Op50(o{dMZshrqy9?DI*ccs0-5hUvfdspbxF#zHZxqkc2DvAkYgN}rTP!PRn0 zV{WvFL{f;r9K(U$`c#N7Afo&8Fx6|W5Dw9@K7OIsOY437@3!nTVNHv%EaT-u?L-{M z`x_I1ql#u`+ZD@|?JV*-FOUZ-iqygfXDU^1>vikI``2FgSy&0GQHntJ26h>@j1Ow7 zz1dv+O-8Z@na9GQvu#xA28JIhU{IlT4q1uUuit*&=&b9R4fh+0tCkw}uso`69~!uy z4RbzuVAZd@)W!!#tn@m(3|f= zwM_}y)-m7vAjPaR(b)o$oQtTBCy3l>LNEW+31l#rHVMsM3nQ@2}Y8 zgNmOlEz1K@l!{*!<+}It4|E4_P#i=P`1tj~yvHj4b~MZ5?WSUG&IAWOba35T0Qwxk z42wmaz9bba6^=h=DHZoeR`ojp3q4%K{-UU&x+x}+Y5uAoTt6F-1#B?qWw z!tLln0oJamKH;-XeghY`!hrt55wGLS`wq)F*-AHmmqZ{LS2Bey$ZfeG5+W?fUZw{Yf_n(qbeg4@_=Fv)-#5f2oC5d ztJIW-=vWxeHa>4Ou%@;$U6)58Ek+c3WLI|A8bFw?qW!J|{;S~zobnhdD_@%O;UpW> z?vnhYB&?9HRs6y;`f0nvr-Yxh#OZGK%#dY819}@tmtLM?sU}4&(Us}qa(Z7fAE@xw zehi6C6%UMkuPY>o>*d7J>Gmm>n&?@&^{x&Pp@lFuHn4I5 z!xVh#4Kz8qbMgzEI^JvpWDfp=Mji*Ax=lu(Zt2)I>pb+21{KymQ8eRnIaAZ+oJ z2Op8h8}VRuKt1Zsgm*I_kG(sPq@GE+OC{s@UJ|riQqpj%2~ehdCfIPm`H0N#nGm}{ z*z31A_Uw+MD$6y=X_x&`;;`b1g|;4T=*CX%B)_*HRbr>HL(nwbH?_cnld{2*x} z9)4X|NIj)UMvfQ`!P1UpoyH65Rv6)P&-P~ZY!7feHg@$|=0QRu`TF+Z(5YB^DvW_a znOlvJXfDc8KgGvqeS_}$4T}u}o7f&yH`{3+qn`>D8yYOTqely)H>r3Y_yJdUxz(XP zc>M!c9qY-#LH7k`r;QCGu8YsP9Qyw`Rp?Z{OWUcltAjev?B2b1SYmiNKzJW@Bk3>P zQ!oqEb{3rJWL|elQ{GCmUF^Q5X%h=ubz|s}W~@##Aw9km;Uqs&q=*q$O(Ac4NREpQ zy4-h}yyotOn#s&iK8}oQx|i!REK}KOGXJ^WhPgk=tab6fElyy*Lf@+jzC*Um-C(5z z+p^k)r!qm5F+I2bl=1##2m%9>)(9@`Eb08p1=?8e#&s9oco!`t@)6gT6+4&HiVg{s z1l5I8QDhU=4`wC=*Fda(KM^z0`BA?!=^-G>R9_raGkjU^_yc_jJ+NCwu23hUHGVj_ zXerDw00iNMAt@Q%pf0r2>w3=&2=n*@_izFOR|j3+I{5ZCH7T+_7+!*0p>E7C7 zeM*~VBC)gCa%Ui1>zWXrsKbGv!v2Qy{gnXZh zE??bkUS|D>4KJFIO`L2ByxhAtOW-Fl#5lGqy?dv4w9eWg=0(;~cKI>y!Ki^sfcRuv zO0Np9{d7TZ$zGUzv$0#wMf{V2;O`!{(mR1e=ex`fu+<3oWxTAK@=5o7dxMK!C25m! z)d!!l`LQUD!tSG%_? zcS3mG3pXIL2(f&n*kz-M^2;H?&H{SmyptwL3ZS z1GYLs9K`LN$kVF4CRQ(Z`(MD_*so5qftAQ#Nl9rj7xPM8H`R`nw4e*Ag~F06%;Mk# z>uD+==!0@fWmp`AY;{OtOvIzB8U=!td=7ZjbZ z3g|GRypo%4Phu-Jw(HaltlUXgxgSh?W2FjE#Z6>-;tMVSD5C zq~cuJF%a7# zB!o0B)+p|`SaSojdC$P@bIy}Z(eVk%f9ji?_~c!ZwlR4$4=sAAMi@KkUxuI{gXX82 zsAezuFBwF8PrIkjMCU6#Bo+38f+3pR&)w7alvsZQGs)FjH*=B4?f2n0QZ(7}p+oOn zgCI?b_e-I*Fi~$Y7B1_M4hUFM;3PUc#M|(q-{|113F7FNblQGm8v<e;6S>iqojz z?Lr^sR~gwu_3>sLo5rETXIda&^Vi0zx=lb^1zQ=!ean9aGwz^LEM*HjYZyAKCDQu& z!Gwr4A?7k_cAJ0FZDYOL!UKt5I-ikJmGLpZCj(|KnPH9XJz+;qOhc3f+X3~Wi#|+k zEimQ^V=FsH4iBuBaW8lOS`}>O7YNw>GD$fBtkN8l7L>kl~$z0-|R@qz-+m> zzGP8-gte5WFl)x7KA2jpn9q_J55J4|wPfVqpW`fKR|V90M-o2Moez=L20n0Q=@{z- z#Db|gELxEy#FpERe)YuYq(6H(yN3K-XVyi`xX-W8e4RQ^M))mU49Sw5___f}{Ns89 z_)pAHV44R>zj((m5UZHxZ@&Q*WYAhGvsT}8+gS7d3sC*LNNd)l9$Mz&q#2Ja4qW~Z z7s?Uo)?ZD-&@M!@PqJP6E!eCcNrB{E@yCmZh|F17nDukH?Zt7$Wzf>}X>(Y=LW#;+ zS+KCMSy`*T;g7P-5tUQTrj1mN;Qz>$ahEakba?N?#WhTLl+e`YD$ACPjPJ*KxT;y` zUC9i|wb1eine87j{daYHf%(@z6E^~ThSImzo#9&c`gMA-!E^bQo^A7mn3JUX@?6#c zGiPAkZx6u*f-wrO76XKod8s(lkIzc52L6>x4gzmJ2biHwxYls5;d`v;k&uO(fhfGP zN71@XdY;Rb-E?@;{4W|-<4UI9PWG!?GT?o$0PX0voV`DXv73uR+{VL$ViS)Ui7>*E(Bn30L+7~~LdZB!z`hGr*Qq)>}D~CDVQs(&7VtAcw zds}2Z2&qDTmK@4};tj^Uw0X37m$1Apt~c@A875%cz~J<0y)#bkQ~d$d@GvcFWY&JR zU^q0AII>mLQ!BPvI#-8s)=#*erQiyZZ~i7fVQ}zV-D2s3VtQDJQxAN4T@q(e=&6wC zx9U`$r?6)zUUaGrM>5cP6K>bHp0cyyEs+Jl#r(Hr)jUx$-pC8cTu7$gXx2%=$s7D_P z-+f1EP}eqiWnX`gB)hyjx_m%sA^;y^S7UZ-SJvPBHV~%USLnw2YTl^2fpowD{h~m) zCUx!j&>HtWkEhgKr(fhX{b_!s_Z6W*ry0O`nXZ8$#&1?Ap!D>ci#tylDi97H=Rbgb z?^^nhhGbFbPovK6-4v|7c-3%(;%x3K%&uUYI*>4{7@h?kKKDeELYw(cHBa1NuZ)Zy z+||2`_NTU$%r3Yt=fLY)W=AKw2gg~|QCPB5wvXTD@(#Bl-&>VcLXcusqqHvjbiAwH zFO0h`859e@H(k3;+x|Q-`!GeZv~1}%kevRDRxSCcw%fCeANDD5zsJA9_>fPXo6f~b#@e038P-%j1ok~hb z&#ecPBZ${leY-X-IWDHGI|1k)JX*cb&P8sn`PrTls#oGVANDS>HS>}E(>gmEn&b7S z7wtLy(QEKvmqfmVk547?k(EL@0U(_bL>x!B`&biQi&IS&jK9f zsY`7IqvbJ5l(WMnyzeRWXVL#oT?_&D)N+b5bK9$2=bhs}RbJt& zx(B=)!!A!-=)8B_S()xL(mCtv@U{Qg4#PC=_XOGy)S2;QM_Xx4^XB96%JW8rQaWhq zqs3SHh0Zt5yS3^Ai_uztOVgRrI#-_Dlw_C1wsxDX>4n3xb=zizw$9P-w%BoHBVf`E zbIak6*I0Rf`LzoV*1kn_J<0sA{rdM}H;sF>?Cg7sV4pnO28TaFsvY-%ztk{!t^Ak1 zNb5^)=eiJq8GAXp2(#L;gp12^SJBU7YAgAQyE!)6Q(HpNeCL6in!0~Q;ou1q&Kc z6BCOUeCHpI%bBNt@5TGZ9K=M|KGvMzf`&~n+1NO~| zsu^(Mu)h5ff!Umld{Bj!@rI9(gSBXL5Vle5U|xtP^RchF_`J?R+=%B(2Q6)jQiEy? zrGX*qaG!gRlfYt*SJj#A+Fr$c3%x^_0qDj2_y*7q`lXk+&$77m`>3k0^Y;}s$u4o3 z1$2VbnhlR9iRG93z?x~<9vCybNqR!tj1mi#s`fd|WTv1r4XeOR$sJQQ_?7#tbTTkB zDKMtP--kEg;}8uDV;hj%F)XoEDdbNAPy+A+;6h`b_L$(Pj#g*wK~Kowf@Imw*>>5b zT`DhSgiy6*d-QhXD}tcd<$0{r=JLpf_{VA=>nscJcf`Gozzx!vCB)l`T_lC zBvJbJ7-pW3r!8Gw1dO5U;r8=t`##N8rCYI^!yhdK&}jJYzXMlda%rWk-F}C}R^8(E zOT%vTN)PSPo@TkCOY1m;lOt;Vx^TLK$+HN=Y%JZB>(t>`!XJ?wQ|5j&{p8e%MZI&*_|K!&T!=zvi+g7aA0z-sR=5;q>&1vDDyQLjHn8!ikkDzfs^`!&uvo+oJD zLKLc(Ztg(2%_CD2cT*FB1O^!1KPguIY)!w@IU{X*oGjs7XYiyk9p07P5wSI&n1!$3 zcTnjh_=md3Q|rZChbt(GQ6vvWG>kBsIgdk9s%Pv0l5=y!Rhu)m*7QL$$;F9CbP1jx z)2()7!Jo?LJ2a#ONc5~^obLe!jc%ecY$yT-KKFV#EGa8EK{xPFjY*)W*uS+)Z$Di7 zD)7~(?^+#Cs~sBToMA73)lj;oX3+ zM43m4KJqk37&WEW;STE6%}Noy;$T?6xu?`&*m(hR?Hb1DuPGb}_$3y0!7YixKLq6L z2KSAEv<=9-r*s(@+Ef|XFFtaIU{8QmBa@Qc*$95>SLp>~Ig>0eMb3KF3x$QRlUsZ~ z{oY-S7hfEvSB&{6Pxc5FBcF^-WmX~}Su~JLY*RZ}uVYI2k{t{w+wh3?XKzeAOViCM zEU#DyP)i*BR&M!*W}-MNUr7F``j~u$UdokEa|=NrrUbS9XC$)4)o4QKQo@ zzf1+~bGNvoYaDj(?gDqzn|OD_{UnbYuKf}}=Y&U+6od8lBaPs$t>9hTf@MJ_x-(-a z?#q)pXLyqqoUsX`X56FuAT{B0>`PY3aBaUuptHW-`!G*xIedt5(4K>3NqPLeyNi`; zc2+bJ_;zb9Z;OX8fj%7xpngH^U)#B0)b1}X{Oojbk*)3u(_=%VKB3|ma zNkOc)Wmwzwx!Ux(nEs16_upP+@XUl4$;@uu^D%(J^^Y2Mi8>=&mRi0E0#BtfYoAr+wZ=D{4e8zw_WM&NoX^puD@*IIGB7jc;G_IjblPeS zoBXNk2x-rAq(c2+^n;(bbHA;>mo)E}FyBCI-4fy~#;molw@CfO%{O}{`)0)WkKIZm z(I{QSSjPJ-O|K_C(LpH<9YVnXh_-|;ibr>vsevizqy(U#5PZfShcpLbwGLUJ?1p@6Klhr4GQj5?Wvvd29oCS2F$m;_rHc*hlFD{_4r0%=+xNRyYK4Fp3vQzO7GgSO5;rrR{rkq zpOq(bEH#uQeVCh8ZVW}}aZ-|74=wqHtQQi=%MQC#t6AYieAmnP`g<9^MtX_IvO4z0 zG|DkAl@mwdrEH{+>uQO(`abo)dPboKR&z)vOXjmk6j+6Ig-enjQcbhb`>*8VCzM`j zgN#w2mf3P%QMczpYh0wRTZl(*1RomnFHkp%SN^yahiR_M9k#hq7E$KNGqS?PT}>LG zzkur>pd(3{W68e+j%5e1d<@d97OhNpVXzet)#tE!$|M#&R(J6oJSjsrMk2}};n)+^ zmIHmQ`6(gWkvrgS&c6)ey1)G#i(Ldar;)~-R2sp*>7HG%F_8v7p#;=#I2a6M$t$&Q z-!*gx=$NF+rV@{&wSR_}xjbnwppK&7CT1cJ4aZ2GJ3Isj9}Sx`j!e_HFt_{oJ-_lK zsFkbdmGA7PQuD}DkLtOCGpM z>mJia^OmNGyra1%@@vBsLjq=&5;m?N!ogKn7cep8yV3Li%>@A#$e7z0tVXR1T^<{q zBcW7gw|I-xq_t?pNyz+yYDKrwB_Pq*O;rB|mrtYL(a&b^@0CsWxe^7MJo60K249*r z1XI6ywS?En%R9*ZJHrq|GiIxNz0UP-tzUV43rX&s9=yTxjXxrNPG?GtL$MM%rKHYn zV-ki;!x?f&UZneCHuh=#a@vI&wckQ3Vu`G-0tByAZXyYR8(tZF;>|J(lmjQk%|rM3dCihIrg#aW$#%ds>+8lz8(Zx6>>m#=#2sAJ=XA-uMA=T1T0*%u(0@D(>iU>Uf#`nBmayDXS&lAI0^l+o>ZBdWbSkJ|94(@G<;@TH6(q6?%ayU0{Z`eyKPxU=T-mC)n^*dR(#42q%k zuB&JCJMTy(H+J0lz**Gd`a|&1UAnJ{5J_wQ&mw5}CRMpu=XsN5e7VMi`f|qd^H#~6 z1ho}^o^Ca@nYrT9d9D8|MiNH3Uy!b?UB3=gx2e}bo$bxnddQm(24x#|E9N&7b6Y&^ z)F?D?;Q-hF{^8e16OHdHXWf|v$iIUcdE<@jK(X9wHwkbyBd8#@ zc|X*1j6Te4sHpd(SiEOcwOAc~6_8zlWW~;?#Cwrm7iD#tc=*NG4_%74r>Q0h+^9^Q z%zKg;FDOP=?yJaMhRn`0a?mvq^6@@-!D>nvJ3={}@j+{(TaizPQfoWQpZy!mMxeho zX1l~55*mE@-u)!4l8^7Rt?fcOIpP5IDkaym`I6M78gr4sy`6ckTp}w>6>qC-osF;QpEk4m*s73xg9|inzFtH+rbOLkDf#m>t9la-I#~%2 zop2eDEDaIA<+fMjYQN-ibhJUDGG{buRp7%?`$g7!B6zSIfw({o`u<` zeb9`Zi(+7%a+@EK7nQshdt&#qpfIM+KdP)QyhV0eoPE~DuG`|f=zgtUhvzAETB>`!`m5}t1VnGo zmeC3wdd=&-AMJksJc3r<6RKhvJ(SaOUMg#3`jb~34Zp1 z4|RC+QV-d`qGFM?W50NN^wE{-=k;%JKA+%hX9-|o~Ob`w&5r(vY2v6vdQV`YCb zUeWLmx7ov5Qa{!{@=q;uFqjr}(j6A}6DN>g2krShk>=S^LFGVQZ z4whLZx+$x+DAj!k*tCLw=4S+# zkVphxdbmZ-jmy*WDaUIAennp1*78v47YWuYCXzSf_=j`^{d67WmKVI+;N)Ifx@?u( zgUoIWL?>(ISKcapz#bvp>6_nq-mt6=-`5QUtD(6QHx3KM!as5ysm!u)m%Vc09D|*g zeg1pX@Y_H8z;Dz)62}DwI#Z$X=k2j#B6r)Gzja5un%veZ@zZ&?%uJ|Qdx!dp4(o(j zT;_X6qQ;fhUP&^491Uqm6TOv6 zpMlGjOa1WN!!%9DOROJyC)yaZ)EXlbIX%7*ANu!?2bcAzZ>?Qt>!5kPlj{xnhO>Kd z#m3%C#D~2{e=Vuf&gQ9IuNBnst~W`owzX?{j_R@(n3H7rv_Np!Nm2q&kiP&;s(h(+ zu0kqqE5j279^@=UDkIk8PFb`c1W%65A~kn3e0Xf8@Tq?uT9%gF)miZndFKOa`j6 zBzm9(ulh)%nj+S-zMa%Ml!S<#UhMHTz4BO6a9#p~O~Pinw<{0EUi8mdjTkzN7k#)lOgRn!N$tQ505T6 zxrWHY#i?wg0>!By9>=OJ4o@+?c44Zo{H|duB+J>^;U`y}0nU^x&%erFydxj??5Fvp zB&yvv>ght}&ama8LTtDzi5ipZ|FI$~S%&8qhGX~V&3uS)$@+N$iQ10rUhK6EiL zZn>VOP%=Hs-DtPNf_&X_SSeO+?BVxr!8&Xzpfb-*S)r2U6TX3A-%J$0%TQP-_7XrE z=JDc9<@kl1sV$yG?WoQlgQ}6EyqT5$Tew7H=c3rykYe9#kEXLHN58(pSx~QmnKB#v z_D1sYSE+ugu}ISn-IOQVFcg|87u*wn7ih9|FQqj9Fj;*8NkE>!`%r>ja-q#!KANyb zjZ$6Ly~mNgwQE1PHKW^w-)z-{G0?n?{|>y0PI|W3&i=Sg#77*F%Wm=x-_U+OTdQnw z#>{{!!)(TUvtyCNg?AU@7ztQkz5ZUk&RD&5dw10#SsuBS&?Bw%Fx;Ws%UcuezBHt7 zEmVh$)E5<;tN;26R-b;PHN2tsZ(d7S>88AG(5KfsAvv$Kj*o3zZ43e8%-Op#wEmos z#cADas?{|;y#tZ8pDoi4Vf=PU2uic;@;>i%W499fg&+vCbzS#^w(k6%chhG`V*@7j zMZG+#I;t=?_gG4rSdw!p%4W4IGb+ zvcVKT;_Qv|R5E950`%wMt8iNJt%%J#=Y?&KPnA1+>8_z$g4cnt=9Bq#&Rrj1T(w45 zC)`E+z<=OdrTSrX5U-X1rFed7unVy~t7*7)+5X!Sz3!I|p|b1q{xaxqb1-J}gw30` zMW%6_2jgy1_f2lXfS8VH?k9%bE*vLbw8(ywH}!V~O8u$sLmBqXn}st@TX=h{l|@0# z)k~s-pI-{NbvW}MeW_DD;=Ow>>MsbDXI z{rP{qtU;+IzPpU$y@9!>shCt^(QTN4KKg zPBN2X=tdsAzFoKgv+sm12Ef~Mk4H_=-qPD-It$UP8>ZlOx+zpZ^rHUt$@rx zmOkglQDmWJFGtxNrABwp^6|x)=?VO;=P0&y_#gfJ^xGr6#VZuv{uMtl6L>DoXNot|YVfP950dTPc4xR| z{(9qW-oqi)>lc4zbIX`5E4{wjXpa9YUb%MDXW|fvA?|R@V|0v|ivAOghgWdU^6U{2 zj!(qjeM0Zb+|!o&_BJXTdNS(-i$uCt62zu69@_Y*Qf<^6zDd_^Svwv} zHm~(}vQn#3Ke&t?gOt3lCLyePEymbprK=H4`sf)>;g7%n1gX7d_tI8SX;rd3Z9gBdO#gs8#fFlF37H(Bkc$Urc_Yuo;+~sGdXu6qLWKy(x>m=RH$dSdv{L0fYSkm-mdleXmD81D;UAr^fnX{oCf zRg#`zO~7c+F?vjgx4b3ukGqU?&|<;#w?bYjWZAX#;7ghY_ir!wlq(3!$-_gnOFH{@ zjmw9S#_UyLyvpT)g~y^9B&n~fhos>1mu8{RNU%`ysJi}HVBkA=S-UoQB30hW~zh{X%tg^6A`}3pxa+zSE*t;WVzt zuGBWBrJWd{YpQi^5!opi4oN%vZDP^TEIv!KgFE-tr6M-FrroJDd9spWG-LgZcrrZr z{BJ;|{G*l{Gm`vso4b2eJGvpfJaZ_LzX}1X7?M92S(EGL-6}E&GpUxU(JOuX2(Q_z zCtaMA>TvAaF+be!P?@{hVBrdLg+?F=sEwM5%f>wDuY`-__&6wd%Nbeb{w7p@&D_46VM)Tc7smY*Of9=h+=;elnmi8ShyOmHsZ6-rt%nfG# z_=yA7(bdFLNK2-{L~N#okjk8b=3*_&0qJD^#-H;!J9lzI@5~joDBaj!x5A%lL4~qK zKZrAJ*c}?464nvGwlGXGkmbB^gIjsYeh>q$bpA-p1;TtH4upi zGn+>q^)FfSxwMijIFXT2gKkg4fk~x0AO7gd{Yh-~_PveZ1ou#pbwVnp)&ZF6cVpS^ z&eE?BaLQ7Xwu7sHmw8`|J@lqDOAZ=2t@nZ=Mj$6#R>KxuVL~r~GV#)?;rLG}^#4WH zqzf78oCc0NQ!rWkVi74FyUjhuh&+1v80n;d53CKe>YAtBgA*kuzsshc4bg^o?l-6z z75OQ3LLYOhIgafw-0#w4ZN428k!}guVIFVTgUBVR3bkC_cTc&p1Uw?B!(%7TouoS> zGBS{8%msHY>Ld>RoqbIN>*1il@lf9*q~HiZc11*la6kDMEh|Q)WhyW{G54~(?4SBD zjR*mSxu5obdR^vu#-o2_B@}kwa1Fn|Z3|L;i9Wp zZeniGb!cIdokNu$3)2akquG)WDFI&W@*(3=v@e~>ZzSvgzH>i0_^9J^6oV(rK!E>8 zS~#(S8)d8fC`M*NS^oofnKOGW#}^e%eOQ!ZlXywF z%WRu_N7T-KhMyL0*(KX<$-nqKRWW(a*3`5a<>uMcmxnj3X-Uds-pQg1uVckMoeb`j z@nxM~F?}VlPoziCU`x!azf{}P_=8iLnIlBfjePUWw@Et)k~L4J_?UikX$CPoefnKi zn?hRixeUvsNTJ5RPaZ;!(Ft8)BNZYtS)J@SSt(EP2=H+iX2As~LzO1J%1WsUbl ze^Q@$3TaB)RCi)T#3c&{4AY9u#U8;_qq*E?_(XZ-@`Mf9r>0C9?MS(+lHW^bX^EEs zsU`uJxUjr2_^dMbKXmFE?lUcveqRpPSFW#|baSMh7X}_gO>v+|I>8MK9oA2X^4?jx z!l+^OIKEnOBkrP4^UU}-oyXCIQPIIP(ZQ5&)*ba=rLPPwML+NkX&GB{S1hKKx&?&&*sBA0&L zc-L{4Jl~QiRPq(~&%KF;|0;g)_j90edHu%KG8weqP0TopQe!Al&B)}2@1@3ytPh$Z zD2OJE4B*kieB?bwKigJz&crGVjcy=({h9U+zpo0F#oH{qDUjdC$9(7j_o#8ay8{Z@c1_N}#%4Rya%DUlQsmHS^RJg~1xcTzth4vB zliCnId{PvV+6f+V8i>J<_;epiR(E;qrn@66Yd$XR|6cUVs?{-Y0nvGarYJvasp_)9O8_RA-5`dBdI$qm={0Wm}M>%-WK8RkFr zVJ%|kY`I@V$xBabR}&p}8mDP&oZ4w%KljtV;njt)87VAt{43-7^LJYwXVA3OgS&dc z$^5HGHf77_FJT}52nI_Oj7DUtHY~4y4=VdBJStIp5y7|4Jo0CfL%N$u`TYEFj)+r2 zs6l$4T!uQS(g!t8u|`2PR_#nYLIMiLeY`~5$5+a@Ly@7Lz!|!Lfh~dK--4~i8I9W- zp_LS&-KQT~sY#`Otdab2Em-gu1HGh z{O$YuXa3mxJ@41^^>{sx$K&~a9v{oPvZbSQkpRu!_2Pu2sJIk(IPVzEo$&D7@3AX= z7gSJ|y2k+*4M>{Zk%WV!6U4T%)%5mC6KS%|&Gkp$WE7Beyvof&Kw_RdY4Nw(*&$DV z`Yy`PnxauYH@*kgq%C)o{}MM4ly>*JEOdEW*DL*m(Gc%IiM*c!-bJ#l&*Mb3PkyHF z1GNklc;UkbgG^jy!9YNNuZwk;Yt3Mq7LV=(k9ja$=y7z=PO~6Jd?K7mhH|#XaSt%-0@{4vK)Y8Ngngp2d%_jo=f4a7t!jmpod;c|^ zP9>{89O!1jU(KB>h%+M=oINA5czVHpp~b=y`!=WNDaSv%X^*1)#0SUD;+x)qcXf7P zN532HO)`rJFsXZeKMa@0(l*x|$Y#VP{j{)jB4;Gsu5`%G-`f&669TI_Zewz=t52mq zXrT7qE1MT$x~Cs^=UEje71ZNJy%)am?+t-W0;P~sS^esWv{aDjm^vSH9>VXV{TN|@O zeUA>UhH~nj9%xE#yD9f}j;!wU;FWd2>la0^;B8g8c&DIXyR+X<;AULKoEd3;C&ZbR zvX%{m8!UpL2ALr1b@Tclp_O2^_NHaw0wt=|66|m<)=)((ZFVBOf4t8x^ye$fgA_Yx z1{lywz6lbrc4SDnjOz8EVP%WC`clQj%HW4j`-={B^l6mVLXC1Bu&6Rxw31yr(|gy@uTb;@U^3g24JconO@2{YQ!M}y%pJ4-s( zl}J*41HDDmmeRue2FeCz9%kH zda^6(=PC70+X54J>zbhj&+FbLxr36S8Ew(&Mvon$eMVdhFAV-Sf`_cy!CK#Um+EY; z8-G~Xm+Q!3#~fZ%xf0ezcWUknj*g9NxEfm~@@+kazX-awCKwHyHt9h*i3bp2Fo(Qe zsmo`TUQdKnakQDYMpBl~wFj1?kerRhfcZ1F+m8Eh6RE8$lc9Olv(1C^g}TSA9<7T% zc6?fJ?%4aKJVmdnTRo4@EalV?+CbEU2tfn3U-XzZ*!&DJGk=)WE1xDdj`8rN)3bgw z=g$oQi=sA+{9kqYS-7yBqzqD6q8DdSFS2?MYv}^w&?9H|emDWL8hssOBzu_hX6}>s z75_9d$}zZH-3cYf{Wj2Vd+&SxyeCh5WqcPu`?Y90#?ivvGII0q(b`)9)$s*N*^2{R zm)DnO1dm1KdyP>gTI!2p(S=S5QuwXe=t0=%tBi=waQ1y~DT2rkm5~!(ey9+e$YL8u z6`jaK8)`Yz&MwoCGkGgWDG$H>DEwK&Nx^@wnKxjl-l(EtrsvT~=nXqpg|f?gv-BUm z$vtF0Zl?uH8#s3;B%0kZ+>izV;pv2e@s5w@J8|05i&H8eQAfKPIGT$jG(O5w2%5D&1teT;G;18Nl>la{IJ=ibHT zg0Q-pg@Kn-Zjw4r1rW)jS1u)fFk-0(biJtJqsDU`Uv|yJD25g1pZffv5vpBm*syR= zdrtGq9iz3(@`+LDFpR=U)AN@FZUwLkJ6H+W4p~rLl*phoc=5ZU6 z`OC@Dv+0cTt`yG4J2vxCzbMP0#U=XnrKf*KHq|ryy0)}m{)FCjeQ|}D>%~lc7+t{# z8aJ$K9cdvwd%gG3x#*SE{@zu;;F%jeLE|C})_F6~tLcffHyIfjq&=!c&2m|hL^l_I zFE!1e1kq!$Y9}t3rF{@VlPL>XL*BcF5SH`yDBd*@cAW zq(c){k&%IcYazK$XTCh0fBncg%)#`M=T(uemCGMN!7Z?^po3<*F${|{WfwqlH`rt| z?E)sUZZ@?nQ{LlLQRK7Od+$cGEdmBaCxc(v%pNcr5dT#4HFl^{UgdWPsD}oTpMT`H zQ=AH$3ZyKe#9l!N2g=?VkPOQz6;4~20ru&>>Yw;n1NH}bhgQYlR3EIdfs>tEH628t zd2y0zXRDTR!J(Nu+lTjFu*f}7nDml#zKryDW;^GWZiKq6$m^RiEzk3@VAOO-^I#M! z+8h(Ii8+^Q_zRAK*OcuX>~_#``7GN}7*MHoXbyhg`1IcRfeR--Sp|GFyz!9bzx3@f z7y?x>+2TaydbqvmX6wK51t=XcKO5S|{x1|9e3BT-I(0v6=;!5cFLE_JUah<@T)cDe zoYf%>4ZZORM{TQSal6>(k!GC{>9PT%xbW|U{PFOGAhXDOIxo_{R7dr}zs&V6?2UgH zzq+k!!7oXw=;_>N`zThq|rgdJw{9KAzNBw0();253%0;9qz+CL~ z)?E`Eo86q%?-hGICdU7A1kmB7F*Bi{aGs%O0(=lQ-*PyFx@RAiyF>JTr+pAJI#;Gz zbDx=Vpi<+m1-B-c6Cd(NJow`cr#sOD&JACDEjf~M5FN8*-w^dz@beUt`;GsmL&{z_ zqaKt!_DduIKJT75=vDK8Uz7-a!!(-jD)eO8o$~Zef8`#vddIz1V$#rZ+n5PR8p650 zoLJwE8ow}E`go0tc~1Ju4T^EA3LS4vPrYTnLl~R7vx)|^g%8$8+fFuqWxO3swjfq3 zt5pp+i`$(_X*+NRe6aV}_MLl9kJ>wf6eM3eLYfIEeckvwl*qzGJS$}&&-me;tM}go z@u$vs?}#~C*Yt|^I6ZAr{pP-Jh$@RWRr9*G z;7~JbdNAJG?$P99e~(=1*kI$?8-!*8%PAen<6SCENp@I0?cHowajJWyq+P?P4*tr% zU-$05x{{bk`|Wl=#KVSH#t$nEZI9+FiNtu>c*0)}7g@)fhQg$_*6Nz~INq&J;wRj* z-);lcGS7a8`5M|ijk;SXbL`9oX*b+=GdD!&+4}SQK%2PsWvg++4dk#b`-@)@8 z4zvwAM|#9Kcn$XJ`x+X&4w+)*;k2zAH%(EijclFBp!V&Sp}T-`<^5LOCg2q)j55G@ z^n?U=q2DN4Kd5!1u?*`3kD4s&Kewa1}FT(=a{ywK&Vx|fpOjwtZcEQ>cBSPlNpBtMJ2UwhT^{buVcOLAoi}{``&6&fX`8id zT{7&2xQ(+$xpn%9l9KBthZLuCmArc0GTWy`nVh!#z7RtA-WkZ)T`{uR6W2M&d@(is z^K`B`jFWOn;aIDy+L>!+uw&i9gwCM#Uwqb`XotZ4xIX;jm|#;D%!L`{YZUFkW^>%d zd-v_CHvKh!Wl+(6S;<@%sMx*v=>mFt^kr&7F2`_1WuW7qZHml9^OW1)Q)cuxV+Q{= zlwP&mEG|sBf(=Jh>wSM7 zn9Ip<9{PN%G;49)_yE&*k@JG|N*i z*ZUZFd|7{A><@DbYID8U>8FyBZ}VDO*|VRXAxKNg9qE|VoTx~DH7R*pH^h+xeUcQ6 z()C#wX6R)<>|F}DaDTgLjOG9OD}Hy_+AG9nMRPKgGqu{-*TXLhMsrIdO@`a-9!izv zYb6HA9@|~MFKb0}F#kcJ5@1W;-oCrOVPHweLG~pnz{b2^qNbGXb;F-BKVTp}NBVLz z`HU&?Ql%%$lv275@0n#9|L3BW>h!_?T}&cg>*ls~G1l3;ZSZ8lLc7X1=?sc(?`k*S z{7~hqsAyeALI?dlE$>Cu$XZ%+^!*M-FY84ZB(h_^)x$VHCgnqZUYV=)9!-gx@~SU^ zA?-nt4nv-ePdXr44W}EbUwsh1a<+pH{(_1?eQcH=5A%PouoO&1ebevj?_UU1q9i{X zq8x3rtJpU*;CHFXH|R|2Eepy2P_N{SXH;Wg;q6Je*47i|h$XXvW`oo1(1?BU^z_ga z8^;>oC>NgFP#1o$mlO!^c2S=dq3ovJ^0XWl(GC-pN}-=f>71;%Z$eMsM@Tb@T6aiRE|PaDOh5cS zA;G#*0S@G7h_Hw8w{kwI)GFE(y2$@36!74C%+%B59y;0+RiKCNOf6cu*z#53YzBbr-@vEi=A(?(u-Q;Xq%&$N?(BWO7^xj<{u0Jg}R(J5h z;Pdckob&tk$*$CQ_j|L#fDY5D&XfFB(s-=w-j~+9D&YpK_=R;rCJIt^fN?^!M)0*I z+;^m%NuR6%@A@qeTR0qN2D2|vYu4<^r<&?PUT>WwYoc&_J^85l$=0@ev(2vt_Q_`I z7VgyDEIRFdQRd`1CKR}v<7I1|`eC8U4Jsk`y85y=8e_b@?dbs>N-ch4?%!;rZjiF? z3#K9%>5)+7(|HGHx>2r}a$hJxWW`w0lp>njPAaqmz!SPggi=ZYz5)+&Qf3<$P7H zg`%)qmbHSH5(RLs1*h##l7kMgDbpp{7qlxu#z9B19?Ayh$oHx;7voKU*`4L?W=7ua z=t~CyTTeV#k2hKE4anW7;rd>a6M)`q(+t>|vma&-hh6X@NJoNH>;k^0{7<9!N(mNmt`)Fy|b8(JAQNRy!ua58F=%G z1!znqaOL&!nr#`v>=&*=p*Qt~W5m#tC#!>7F2m+q7e5p-E-k$%b^XL}88ETTRU-9V z)K(!cc}ITqPFITzgH$vYW!Wd`C&|oJq#4U5Y34z(VEc;v8n^~Lv*82zwUrhk{KI%5 zvUEe(wbrTlH6>0iu=H$|5duv= zVw?QsPr+vk^~|YDz{v387@0HIIC1eE>sC@~39Fp)S?#5=Z@YUoi2$E+PfPu}H;VgE z!-oU``Xwaux$>6U`!AP_)_-rKj7^h^)#7vB_VkH=6nk)rl(n$u+SyZPyEbnhdHpG9 zXkbBE)!aDv+dnL?`(n5R1~t7MznC-g;_0>36fAsq^IXlXqF&zBpU;PE-?Vhzs(eCx zrU})a|)PR9{MM^?r%+t)TX_fW-y5pG~)7U3OL)ek@ZZ zcFyK!ZoYb}+v@!9fgS5d!d^94c* z2Kn*$zg+sJY`F7UVoKhT%xigXtC@lH5a_c%7d`xU_KU`uGVE5*b3-4-Z)A`P&6;Ts z)*D*gA~bR$xGS|x-t~LCx=FiAkLSPdnW)cdJyw3O=Sm#%&}D1g>IT2anT*f~3A(>V zm7(Ej!0~<+FA`RFZ`pm9hJ~VP`M9VVe6pFW>^6TrG zChR%4>+GR@nxAAJV=ZFc93;SN4M~?}|4giZSfSr~Xr(a}_x$Gco1LleUy{jFUjujg zYL~$;G8lrLQ3&awF8HwRO}{T6DwV%fzQF!~u6W%l^SORS*2}|A;y~Mlk+r>hZp!?q zsIX5H(+7tuOQ(IiUPEmErGfnW>8$-EucPtZG4~II@1dvn?d<)&cEGw5-Zs8^65D5eq$b=coySuRj})mH1)#03|sBv z`g?BX;I9q*yr?(jXi=MnQqXbmIkmvQ8^oP^R^4H-`}l~5Y>7|hD-YSoBM-sS$3`y# zV4~l}ArxxX&L?%p%dG)c#Xcv)x>1*RumQW?VF^7rUb{ zwMmGtj^>pDVlo&An1^Dx7@7qpX?FQ~)DTJ_2Cv!OZ8Qg3PL_jFAOzFQWA_5dF-UnF zS=pBG#>c2b04%nq%2Q&mg0`k6WBt{YU`(E2cG_q|6Q0nFgfZ37D1}fcz4aR!su=z4YnLZg(P&^fz=xX9%c->w+<6|GnXvr(khZT`1eNBGP!KD9 zyz3iFoZ{e`4db6sA#7fIu=InxsBUWNo_}SqwDMe?v-Y%5YcOoN)X;+u(z+Ns!kK^5 z6o@BNbaLe{tudT*rfSTd;4Y~~#k4q|v6w}O0*tx>lg!MiOmT`D!opF4Y@mwhU+&c? zUx___?U1{=X@)njbC$50nH9n_^f1i9gPJaufZlY`z8KZz>qTlu2O03$vywa)$-lN_BUX z^csS6*5YD5%~w1KV`ne_FuDBN^4#I$NBQ>t6?F0PWC~S6-Eb_lFk^pfszFtwwvrpt z`y_n&@FCHyC)+EljJiz&XJzD~2dKG>-qFSu8pQHY;{2M7m>9)} zg5^@D_KwkKlafyWLyKSY2JK$y&vBl_8+zDNsT%Dp7eQNQk!)Q{H{w2 zlqJdXkfdo?b2DLaPliAL4$XRJAd5o-L_`1`oq$mjue4scR?@`>CPI8kJvmkN^1`*s`wXMn`{xvP@i!l}QP$H8D#CL89Q1DQr8nd> zsyMjW6v1glb!_}u9?G6~7++IhhxEV9UOViWG1pLm7xSZtSqDz~!liAH_PV z9Fs(2B>=+B+RfvN;EW;Y4Fxn*m;^8Y4rdSQv?8I5GtHUk%8JI*k&F*edIv&~w7iH} zrukVD=fwO$YI(?B91Q~nOwNJ0R);Mf1AoR^Q?7^sh=s(dTVT>lI+4IzX*Sk&$&vbY z)vGG-&8*BjbBHX9Muho^II2|AlR-e*w|PqFPs9RfYoOF#lcC*V$>`q zb$Eyd&UNunYY=v`;TX?l&N@4?y(4wp(AQ_7bStzu#myH6X8NTWFdR^ZbYI^XxZ}8Q z*cc-rSzO#1XdxW&fl`=XpS5V22XF!998o^09m6LlIvTO@q?kh!D9+x9|G;5-RH0d^Z$wKCqg>pGs>sCEaUsNE-v?@Zj?9gT|n7(vvXD zg7p)s4Q#qQfOJ9ujRMVqW@qQ2X&tk3AdJw>qXB{;5fL;UEzK}*H6gG`jRI2R#66zT zm>aV$*xW{YRVRFW(t#M-(ZW+ejiC^C?78YqI4Z!emfFXf^ivbKKGSCvD=vr~C39`o4t@2%?vQZxx#6#jq zed(b0gi$uBM|gjab_`1=(3kY&uE_1wCCgYKnb)vFKB44GRzDJK^yX|&SRYs^tr66y z4D(m#ig0RWSxhXkBOC(G&#@?tf0jvo{FU9x9|Q61?AfGNB&lkl$>rPzUgd#*RizS&gRoH6Zqn$e* z=We&Dw>kxCqN-%r*+wl zh{Y<%9FxFw71#WCHHE_c6C(2t;srlEx+eVcgc>4=i>AVQD|2Yi3{ z^v;*V7q&GJ;}Ma*p`e^rgC4Syt1kkgo;s>x9mOZZ6QPK?E)qMfy+MbqOK5Vh*m_hF z_B4{=xKNKw*J?cy)ojGxn0824HjI0&!CSr;&O5v!#GCLy{GF!&|FD$E_9bth-n+a| zVCrV4!&@Wi3E0^jHDMNjce6Yre}%@ZhewlO$ZRC3iB8DXdWKwyHog26J^Z5KV!H(< zId6FfSduU*E3kYDG~gBJ^f(Z@4CoGC(+tIyAsSXT8_8lN@TjbWbkLUfmCF;R@j<;+ zJj1k{rp&XaBPql&A!zwHxJ+{2eD_5(dQK@_NpDJjp{95;m0KUfaBN7pz1pgZZU1o) zxMmKP_jQoClrTF5EYC`~P3cmlx%om9Hl;S^K2H@JQ2aW1@h7U*?L&N&d=i0zq%n9onM4b?CBKFSt7Dn-%4QcsiDoq6kHMw1;fnB zWY6_P4GDkj%yPF4z@rAOIoe|s=YY-Q0gwKA< z?+K*4o=~=R&9Bi4{4GoDZS+@SF~P%g3Q){ktL2 z56LW<^&?}kQ5HhyL_?_)-0#YcK>j=r~7 zJ+gnzV)$*Bye3(=j1De3(TY8mBE|#@AVg-I$ZGys#_gwSlSJl4#4JBYUPU(H4~;4T zicZ{81ES$*37i0cMqAS3_z43G*Vi|#ef)FFhnFR3*3^zQ3{U7Bxtzp)Y=g@}M656f zzR6+%#CK6yyoeRJW_)qmTN-9LB5)Up5o4!*)9br7A>ROjWC+0i=vlG<~fLCWt4CtI~kOg zLk890_4o_dz#NSkH-nyxNf@Tf%Y%#CpV+q#c6JXI%wo}^hRTiRz=@donh5dJvSel- zSR@f3Kr=B5W2pH=41&uKbv=no8j=_;?7MxHFmKltcZG)(>^qlCE$|8t) zD8mPc{>&pL;&vnWj*|;~UvsQnXER9+O*6mbja7q-4oCtzJA1*>y^wl=f;iKhCX5Gs zY|cCyv%qCA&lpWnj=bcwED>p9;Do7;PJd2jolOag=0=fZQpB*3Umqbe9dknhdO!8eddAf3%d=xD2#;Nhm zE$LSMKcl_+4QFz-K|F)esrc570srAsAr=_S=lGp5js;p=k_sbqbPPj!^OfsWYH(YunpokR4R1Y?=z!1|@M*uvM^b zqakdypa=>5^e2p48^Qw=$3BWy(hBTh@vzQ7)-ZNgzu=o0LBOpIH4kA5^TC&K?J&%fO#l7 zgqSy?&3{k!#QGCvmjS*;3m?$tyD0vikZ@#YfiG+qi-*i&P5#5uhA==20Ei#h)hoL? zL!H1NvM5I$VzP!ksVu%mKcsVvfk9&g0$LGnh}9=4jUk>__{x5hdolF0RtG!zc2u|; za0oMxzyX2m4yG{CE6Xkdg+4bPE&{Bf=@^vka!1sV5WB4V{hj+=#P~FN*qy8ai$yub z5ix6NKow7?pWo2@1XPGmKRaPi@2dLoqJ2R4HILr^4I*s%``Q;Of&wYfAWvfaDVGJ1 zsQDg=@E`2=(!foc3bhzwJtn?tt`_(iEQ~5DeNSD@)sFh1#T+-I!pyU#&KL6vU0KR@ zf_1n4DN@5)DImyU>UyX7ui2%Of@o)8GUO_}R+!mQ{D{f^;#gqT^>iQRL9x*CET-ATdGl+%*VjvKI0E&ZwcCX2lBZ5&4T_{-&FHk`*Oxs* zl}(!be3@@D4zTrZ1zN7;I)B)=U7bgiQckf>mF1x-!J)JvTZEkJ9|J65$N(>REO5wt2 z0lvuF!()jUj2QsYsoEYzW4@`enH^ROQiaVgHvN}`!R~QAs_LlFI$U=T#m5Fp>1(rq zCOLm|klj}vgs-S2snspx3$LMm>&9@=qOFvDW>5po5yP8W*|yN*&PH=k8yi&;`OpU~t<)Qt zOHet73SG^Kh9j}`H`k&oP0-FsEA)qG<$QYbH=we8u8x}P))PUH2`YTd{@-P|Qd`4Z zgJTmoy8VkDMan*D(USWt7@N6lQ6g3`J6qx5A_W>HMTi}b(pGV$Mf0R99S-DptUkyv z;(_`g$$FjVOwxKIeNaJff;wuDtg!@9@rS?OVc&*6POS4xW7B=f@&PWF$42KTE)Rka zX)H^e8Rb>^5_`-i?9K#%zh)#Ll0Fbw-+p|k;T_Q}^apURQ2)-8LqP3EN*IVqOp9N! z(eDgUw^er&rAR2@F=wSVJzkG z_g)u6dbx_n@x6;9E|OU(^rVbCjy3Gf0H1|S)N9YnYvBR0M?Uy;rt)e!21@UV76v2# zo}%qY0lWS4_>s<;>*^T`_NFZ})$B5K9j1{ko@DlehY? z{+=rc66uKsWS(ByUl=rOLZHU4a$3&X1=gTW@SP8s+S&ttTRYLqyK+t+zb#XM5U3l& z^u&OQKAYx%;g>(@W5QquVrBk;NDGHoQJbjj_A7UQymv<<^hBJC6P?jT<;|vR4-eoJ z5?SPgoEM@w)%jYdM)?g@U+M%Ei)k6K;*?Hjv%-4|$3Q;hnZ!NuN5>M0em#!5j^*Da zbzJoG6?I(~$k_8sE;e>|UU&xRe?m|v(>i+tc^Ey1phg6;S9`lGQ4F%lgxyba-=h2Y zVWgz(Z53Cu!HQ7%?2kDGFY77}*X5SCrA+I1WxpyQa4wA^T%FznDs*R7nBnX>zb5k3#vnehf3{KlkTT$js6; z(4%W5f@`xMut~0ReFEdDf?TLr&QC>0i=>$9|NKAACb~XA-$A%*$=j17(FK)z zwh6=6G981Zy21HXMF?W-pZ8X`&j^<`0nhYTm(`D(h`;X}H?D-|$OFrHG-$GIas`_);~uBTAMbj!7})1VJYZgdDq3+0!i zoQ^$+2NsK`saYU~)}j#Rr`>;nnLb%S|%0Dqi-hMA=~qv=-Umf7Xyk0EPoXb~t=-;s72O!Kg@|KzB3 zP%%xD*D)lM>aWp0mRN<9Oio`qXih}#?Sx7o7E?}*x}q`;Xt#rJQ(dNJypE`Sb5>H?r#QJyW?-a zV0h53y<-ym^E`trd4eya57-^j_2Wyz_qpW5-7nISMCSjVbyzDGOz8o1)i++0v>u}F?t)tERXx(3J#^+GgnqhRlakB8_!pKdibkn^W^v)X@ z5g@}(CO=(EU+^0yb;q&C?Z}gbCOqE#kB}5eHkIS#7CoH#e3<%CXR~`Lw`jBktn?8V zcBeM?_xH?8vw|CF8Pk?z@kCLy7LE%=DZqt%oPd84H`mTZDbVLygrB1PEWtbwE&P%p zBIIs_bRiVc)CoXC0ZknO!F8MEKQ6O<1Z^LFenrs zVm6Cn!(uUo%scox1>_5Jg<+{v-A4UrxUhDVz(S!SP$_|s`hp{%C>=BhnyAS^bfD3~ zVi%G0zl)Bv&dwjnM7vhl29T17)uMb9Z*zkq*KfawAWO$wz0 z`UOH=|9{IBT9k_tnxSgN{NQ@}SVMDBkeaIZK> z%*UG8Cj#b*gD6Bv8c56yOry+n8IT3xBhXkgfsxjK*`I?3d#ft!tF+JgbN3v=}-h9Nw_efL6U?W z4-6spbcQfjsud^EuR-eR*ZAQGD!3VqVM0WqaAEp>8nO|SlF!3(Ge=cqO z%D&W^;Pv|QaEIs7m6Uh+v6(w@a>^gcP7m+yjyOuNKDwXl^1W*?tJ*>Gex8l8;AZFd z>76?ntG7Alf1G6zUY51fiO1GDY@1~zj$ThazV_G!`KWPB z?a{{fW&GJ}QhJ;jz=CDnd95agTgk0yxb~V00U7S~OGX}?23`jhPChih7%>MM1IO3* zAb0I#-yLkW>f!Cz*U)u2CCj*VNR)~+S)dS8C1w8z`p zdym-r;5!GjwRFn%SlQoQc}*mSY#mX3zPR|*_GE`$xpB9cf5||iEOFt;?_Yp}Ym;}| zo}K8wZxR5KV7@%o*?DjyCih&NWq6i%AbmzBkkt7{A@czIt>+>I3Am6 zmphL5JQ?ya$IcXSFbJH`391x6h6TyiGL-Q+Nf$ zeBRB>_sr(RKe!Voe*EUdM#$9c1(m=Y_z+pEJ-a(1GO+H)t+-?|Z)2x7u~T;}KYL!t zy1L?(gwkMLbEOVhM0jiz_9$rK(3+r*ISxtz4TlQdCjWnGUzz~WG%sYUo#9jw7Y9MG zT`9I!cJ?*4#s}g@#((w2zO!C#qg(UdB*=ho=@KS3R5P~PF&ihs7Yifu}qdus`6tFcB^jJmPr zkB^QYWMCN>02i}yq4)E@+b@53aa&#peJ&q*R9biUY1%2*de~iJ*+%N2r(ga^xPSWd zZ}6-8Q?B}#qPKG6U2}XP@>(YU*B$L&Z@)8RuiV~PO&s*Uc|E3TJ+{@*wNwnQLTJ=>AvNK% z(}Z+CUq}$w^<*GZ4HVw3Fa7#2VP?ZWVeC3uL@3>e&_zDst1}E8IH2x4_aHhN^?IQM#?$=Bb-v=l<(#{U+NfS2<-vK7NRqN-Q339NOYjF2yH$u0 z&!CB=NGF5XZArHe=A^y!&3>Fy5oDCRI}xlo+C*Uc;_ZWUrdn~3k3S#GEc5Qa=G-JK zG~{Zr<=Gx;u+nMagw{fsQN5vuvg3?!ih?zv!l_6)KV6Biwc0rL*whs*6k=yFcdRr) z7sY*zxVx5NRg7S*9+i+*6JKAy%zc!>iu((^2jOE{4Nh>S6vv*Oi#d%m4NlTCuiMg< zU;|oJ;|}-S25*FX3zP;HLt-vjNS9B-{IM8}eWTJli#OS4pL}|ot9?St$$-DpW*WZw z789n^Ik`r5sv_{1x$4p^`73wJU-UiBbF7Eckc8v!jCeWmoNLc77AL3g;+J5dJ!1gD zyuJXD+h&(xm!H-|>KzC&)*IsZ-wo+5GSJJO3soP;rRBQ`1toea^5eZx6=B#*C{03+ zl~I$6FiXS&%R>jNLf#PAD=+n=?S!2P2EC?4;A*Sdf~ik9wu%af8rsF3iW&*HnU}e* z-o}HRa^sYv^mRTh$(oZnmI$2)$Erc;(9y3Vm@VQIYZohF(Of*Pe5&(Xn8>H8$%|o| zKh(kFST)@%o9av%T+y%?QN~N_axfv%ZCpspAn3vTHB1tA%Q~^EZc` z-%iC2AKlO>i*g2rZ!i8I7A4CCShhY78`E()ODC~YQ3l;C72=ojFRYnZV(;!`u{ex5 zaqOTSsmV3p*A1@Yo|@@U^FrhqBCVnc?|`pY3$iOw;!HICuS>v?Jh@yQsCGGUwj0;L z)^!`ourpS1t30jKou8e#m)xCuB{hLb?HpbrX7Q*zaIZ5^QCIAOq2oxPwKCEGl((C{ zv!E4(BnOPS)T_fr$Y2#Wq_7tShV)R3`d#&m66`4~}YaL5I)J`T= zsWXdbZOs!}w^W0;vh6VPB+`x9T}rd$`hRG|!Y$}PsJ=j$3nmGSVTSQ1<3ANODw}#J zRWV0wj6&_CX?%1mdgdOJ-^%W~H`;P(Feg-r)aL7umeY{Nc0;aN z7LUb@JP8!8E(abo=BNI?cCpE!W`xk=3_Li;_hA-m3{3|}%cjuAD-^Av z^eOufKe2l&#q2AR3e5`b=T6wziP5gfNu;Z&m{x;qxi1w}qPP!v;qeM7fwaqklf>Im zv$I1(`8@F%b4VVC}&J4WG=JDs$ewr#}Fc*IBP7Cu0XepjecAj zIxCEIN?*3rlz*_o3WITR8toTU8#4Rh-CkM zNe@hgtBHIECUooBUwS3mt+#9?mW*2mPe!53JA@rB6}?z#i&UzH$!^n>T*>PKq$P{TUL6gs6Sr@iU?EYe^dE9 zP!t!9{+h+pX~?MuwOZYEnQb<3$|JFVmtB3$NYb+>ajf(zJ7TP3Vy&al&_B5O|8*{E z5<8c?b{G4L9;wfoNR~sgB$FZJU9d~DArz3cyf|7|VQKj~$DV_icjmMHoe`N#rYI99 zeT0KF#F26DR>l48b3n5Kn&ih$x}b7;x7GoJRM~(;tQL4dxQ2pbT{}u1@))(3gX~m*-qX*NH{Kjm$|z$5dJ~fthAWG z5czzn@7}+WUh5ztleiuIwGHB7LSmnL_tBxeFB_kV#DFZXP{$r$j{}Y{xJT4V0ZBrZ z+D9b4htcqE#4T%(8`I{w`b5N5`!li7gd-fX!XpT~BY$0-bhCavgbcXZfGhA$J zvjiR7S}~O2RWvTB(J#Y* zm!BEd)Pp~jC|?2nKaS1I;O$>>HYK}w|s0crT(-~Vuqhl6wHaJ_f$`+c6z^St6FN)f#UI|*B!BBhHX(6f>f z!YG#*rW?x^Hot^AA4^x(&YD}ce1QNZit0!}Foz{1LAAC#(H9AYLNO7;>5Oy&Nq-8d z=!~iFVWi17bQAC0HNG)nQ=QiMP|7U2gbYtfTbm-)nInWN(>A^H@3N8*QF;+XZv9^% zPPGDttA1%I&|)i6!l?L!yD^)F*|-&ntcZ$v+ME4>s@VNy0+Q-1?qThwds`vpV4o z6s;kU(r76eE0d{cVh-3aqh;re1v0p&2p)?SqaPTy8hNmK_}#po5wg&Wdu&)JOGFYH zqgxcMkdi>+VHw?tO&OGptt*>#x4dS_6+|wDo9WM_VKw3T(a`U#oCx@>@3t_g#v64- z3v_(Ob~ZFQBCshmX0=W|9D{sMN7rS4S&WJ~)G)bfnXw-`xE0}?2E~G3v4-8rFuTqv z!HN^xRZDmSc=b-`-2{^Q6a(}P-8YroO2^6P%x%Z$H=bWqEQjT-$1SPlQ6h;fAoX{g zBWN^#JIqQ?P=tj#(Z0F`LW!94Fe3W0L@H#^pKA$7Sl{w}+jVcEfFjzy$E@ zWp6F8gDs+=Cw{q}I2C~*swjP~RT0)ba$%%uwHI%4NjPt9?+-Vpb~vpbX~o#O*;C!E z9&ZeHMKT)pxtVZ~6uEe=I)^1sVi<>B*_6)ozEMu?U`?p|b^W%M9mkKl3n-^Un1)`C zJ)fE=kw&fCxM=zw+iWiM+wdf6EdB@n!?`CR;RRWYvFWtnMSK3WH9bf#s~`hIg74bh z2kbg(kvd8dYB%XkK!lwSn1Un?R%dZAK-# zv?g;Df<^pmtoUGkoi-O76nH!1oIDKB4TEy=g2xzYGPRtQ9e?XG3YoT$fCC4J(pmLa zcPZ~zdJ0YuA*nnC1 z__B#w?QL|y3k9gDEMxW~r(?9i0@;~#j0UYUcU^^TcU^9L3US!qexJO3T~;oiTJ#Ud%t zJ#I+S5I>N9sOfw+2JQ-$ZDUMfAk_=(sgTBeI#ih+ViWfFlodhLxM}iw zYIQygfq8PXKWc#$=V??gBD}!Ej#3#$;GxdM=$26lBdFc@Sr7YyRN$N=$@()~2zej8mr#`C@?s z9n*S{9#XE-%LXqL7(%%VFEudTn($P%F^VW#NIFpiF|wTPh?*1i>qoex98O9$hDTEC z3wTT8+bvTBZvU!e zFi}L~rcCl?uN1xm_HqU+E+{bcS-n>S(xy-Wi_^_wDG<`@uL;%>VVn#FkIZ=f%$OSH zkGUJ1u@?V1P=5Bk+rzyu<*;-WUpe0by}45>V|<#-dmq@_BE zu+Tck5JT0aCcB4it@?+N@H*!;-F6I)Jnq;t@{rGb>**ZdTR(>tW&jbkQKz;~AS%ux z6;nuXlc4k(Ay0S@cHE_GETXKeOSFKa2*+WT*aRM}1*|q(+?O%^$yu}b6HA^Hvj-_n z4YiJ9zT~y{#U8ceUX4o`6{gu7J(FW)D3|S{8u!eoa#oMV#q!E|?}tt->0Kv)a%U?F zFt}~?ONgreULG1VHPX{Q?KHMe_u@lo#rDkd*W^TPR&f2+bm{%+ zWOaahu?FyA;O%UMp=p4oNCdt@($uYK3Bc|u4Zwk|i>>JrMXA;QdnvH{wN9tCa@8eM z5b@Mjc)Yq5o*Dvjbyc@+Q-9$K{L_RqYk(96c)m0|-16<+RvazZw%l$gy9K1HTjnF*2uh0QxVe0|{Fgw{lEKXH{3XOE(%rwYC8Quw>di za3OxChlO>7A^|-E(+_Bkf|v?mHfTjs7q*3Jpa8nPAJnabMgj(ZB&@#zK^cl_ZEfub z1jznw19y#IX?Q{xa~dRpsBWF*>S~>yW}Qw{rv_8o;qe-PU~cvQKLI8h;B?gOsMW97 z3IJCD4*~vH%z-QQN|YM7Jn6t+0~gR7xM1n&DP26O@U}KuJNwE7-)G-`EFWY!<*AJ9 z@A~M=)=}O+k;q|KwtbX&qjFyH({WWI@59a?$D`YVpS~*PbgB1W$yqlYafHIYCYsut zv1QI|qbfgfEAmheIkFu1!ERQX@-oat8&8n#KVA#EM7eRQYP>-4TDnGndi;uw_V?Ccf6ig+$v@FbFvJnjLS8@O5qtt!z4+%FS^nEy^>%{pusYf!)5KlzAmQ70F%!I#s-fc^c=|6a6S3<~~aYGHq}%AWhXLD)9>h4iawnm?XwWgAhCE>42@ zpM8JD=&9jAZzSknP8asVhZB?2e+y4l!`QE>>*{JD02bR7mPRlZdi5?dQ2Yt&DH03F ztm4ybr}&plrd-r}%PfE*{idQ;?|?e-+EQu+AKE%6jb1u3H@$b;0cF(0r8j*WGaO=Z z%S!5Baf>3cu7&%6iBvmbu9}ybZiaau?mA8J}X~!`p z-_sSX_@T<|N|${!6sF+vyg__5$;>n>?F*oL{2J7kaZcvcn(PFgERoi>8!R?<;-ogU_1*Hpfu+}gRl-**dU5Y%z!Z=90J_-gyAgxMu~rfat$o(>A)2z zJvo8wWfI?}xyD*+To=yU;NX~8^yqu0SI$yX?IlB6w6LJMEyi_;F}~zN`8iEq7&L6# zU3h)(w7TSPd7r=xr$jTz<(qJYO?3?R7oZMRVEa8;zV@6r@(PjP1P4<68Jhj_WO_4^iT~G|5FCA9zk4w|F$j ze>F3gsK%0o%c;esu*n<%BbNyHv{4l>5Wsp@0A3jI^oyOVK;lO&Zu<-h2^KDQp$hH@ zo6%xTODk)H$Afauefi@i3XTsb_9U82RU9v>vfG)iUsfX-<<${HZ(AIG6~bpqlojss zm15`orCb(bx-mXpX7^=FQjU-l|zUkA~1lhM9CZ1%dgm-EZ0rb zZV{`IVrC^*PBhF|PPU7S7Mz?K=`cY^=qd#cB>7or-!|7uOV$hP#ss4!5%HLfjlU^v zL`x2A^_Fgjr|^MOyL5%{0?xy$Vd?E|AQPRO&YbpR?17|^ZKguOhv}($*gRu819TF$ zv9aIOaPC#S=uP263!H~oR$-A6lNgaH{EOTp<0QQ9B+=;!|LONAJ1BDGXRhwyEU#1X zPLwuTUT1xFO0fH{!g9P2gpb1;SNTga!+=n$J2zETnO6OKO*&b!1{E@Wdsr?J#PD^gZ zr!r5uZwrzWJ!@1^x{0Q_h<7p@IFZsxg5F0=qel}ed;Bc!WK z-dp|T(tSIFkaO-{XyPc=858XE{p47zX;W=3p-0xPTHA8oaa|A{C@3|tHAf(%jL@r} z^yPW82;UaDL95}^fS1Un)WK@5CA|X>fFv?p1l#3V0A!>{%7Kzi!Zzh2{;5TtuCpeA zs=F0w>xoS(g8C^j*fMz3ai?ht_hZWPOAiIL&??|=RY94r~!(N%+9wc@sFm9<_!#Ij=m|+X_ zR5%_1Hn?4%fDBOFzB|>IEV<2(dFP z41P@N6g-Vw=T;T(%}^ksjt7n_CMqx%jq+=L2etDIS+oLI4tQ5K z$rvWZJ)uHqm<*wd@@$vt?>`#`7)*K^ z>5?!^PQ<&`?7TLnrKY330H+uk(|h!hdhhrW6Jz(Fhh%vE;Jhv1R7Gvala0DCq(l~$ z&B14g{-iIc_5?Cs+V~F6&3?7W2NZ_Rd!^2t&uc0^saZV|DP5hv#OEU1=}D5Y(JSkL zr%B(fTWdq`1gkD=UQI6-Tl=GLEUsRrh>1Q$ZeW7&ZwM906o+ zTg(<9AYlftgvRn~6493kU*Ne8BeJQw0e^qHSke+xnDwrJa2lr$uha@tZ2Q11K4}qQ zXl3z1u$OE9D%Y0g}xDUPxKFib(AALQ}&ZVdSB8b)5DhxF{bdRtpa z>+m|&4-kI|KPv9K%CbLl=+h8uLN)5*W2&v@9nq*Yto6jGdq>BIc=&*HHa zyp@IE3Wr??=@{rdcp{ri+6uMjadsT|kaRF;1L>_X+;1d2ZjY%AEGV`H?kjr;_^Z)j zJ|@ZkdC03&SDg%fF@-RVh&bE41U-AfYyGTtPfvYogzJA1)miC_?5 zzg8-+CGg*L^J`1ji*IUUp^vMGG2sGI=j7Hd=J}D_HQw5B%l`GB9h}SbVfFQ#e!DPP|Y%Xzp7FXHO#QlqN&`%0?`|kQNMQ$ISb$n^bsRJZPj4ZxAdrj?~kBBMpKZVddDbTS7>l8G!Mo?jubu)AuTJUk7W=1Bu zMr(Y|EMb${yX0AEZPUq)y`Iivvn1HlkqW$}X3iu|T<@TGeAaksQdFt>ubG5yMYuO0hoAON3Y zD*z0Vcwo{5B40!RRRYK)=qu}#EJ9mCI2$wI0RR(a0uk8T;w>huLlITI{fHV#0QW)C zyvni`v*CunRhHmooDQ2xk0d%0@eX!)RRF311Fp<}DM!;VF);{pI~r`9js<=I`T-Jf z@9nPu{CYt&6hL_T0duFKFH#E?2J1lHWVY%+uz`C2BYXN`VPU}MJuo{0l^?C`HBb-= zhGay75NuFM1ap5Y@S6ztcNcbJ7?Gj0H~^R-0+B($`A15EqSu=IC@T1$47U8(x2O{J zyD+(QUFHGLoh8qfpw!o`z8L&Hco0eF1d)0gXAt_cx$A;M<?W7CnU;PFGG9%chiZgAVI zVF_?vI$B@jUbq=em6dvsFBtg4Z0Tyk)8Cn4AJpi|UWNYXKBROG<}ac&R;T>%hJwmY zjmqpe&4aX`z^l>pX5RYD67;e6DZOhl6z@NITr%Gcd{4@w3A=UOOoia89)P^X6{q-o zabeNflk6ToI8nCu`3WMV{Ej2H42@``U01^`zmz+A6c6q`H*I`TNOy<6O9pOMTu^}T z?!L}>=l9Uw z$87cQgD7VDnd(Se>Y*N{jdE(W<+j~ZU8LR(Sv0-`)dyg!^% z!J;oV;fE3qNmXyZ_{&YVy{t^HjNE(67c!eo&vWY}gg=Vx6$3P6=4@R9uH1I!7cjJZ`Fo7ee2 zpoL(qVN7oegJ;I;v>|v75XknS~;t_A+WHMMb#^Pk10nG&|iEtQ_AvKpKlBA#N zeIE(QRA(MnRev*dt8}>(ob6n}ta@3mL}-8PlKdJ??lJ1RGQ4$mW#UaK<5%ym_YrUt zU-~bJ@W?RuUo6je`>GCFOa~L?)R%O|PbxTS6{U1ge0bA9cruU~?(XAw_0J{4Lt941 zMpXQF6WFVvCR{p0H!&)*rz6W{Hukl;jGyppot4dOdA9)d1}R5NA@(s|UW|uUeq&~x zE>^NkX;22cwktcW)mye^e#0B~uL}?M<*UgX-m7O?+dJa&;=?cvQAJD$3iN?r7sQg{ zxNl->Ez`+v)bwZ_yU%&WnV%|uSpV9TjZ}1T;gsmgUm+KCkIner|Imnun@lE?wG@BJ zVk^_$Yk#!O32OE!7o(F*BEw_66b;Q+&VaN#PZ~*1qIwOWYV=0C?w2VV&vbZ+=q3v1 zvKC|lG=i-SNuoSlG-qshb9uS?exq~&_`QDY+*r-@kM@~Yi5-7t`Q5i%dN@^r;>>mx zwf-dAR6LKG-C6rfQik43?BG=} z+In@Onc44ZhVY9eAR`$zH0?9thwO}F8n*YXckYbKiIWXb;8fNv%Yoj$iSW}}pR=9n~<= zQN(Uf916))h_}>o4kzBp*SZR3cEBmBG2MLK{UyCKQE=dkEU{PWCa+wbG&Ghy?9CvK z-TRyG7w1S-h2DHXz%KOc&IirbijbtS-N;4_W0JI5J_~<7T>MFUY!Vq6V_$NEttK?) zVwEvrcWV$T=od1q#0J~l;7glv-LpmX(L>RA8Q?o+*Hdp!H)Z~CsL%IulXVkt# z-mMq>4fbNM=L4FQ0)Z2H6oeIU@#5b|(#U2h9yFAXdO-|p2t zch*gq(mH)J_E5Qdet4=m%I#-aBK=g2e&N7mvY=KVGL|$3v%eFwLSWj9>6Zt=TL(uX z?A6jsFmz0BIv7K#gDS^hz7JInJOeuF2}RB)rV;Q+If#l;D7dFpJcTt7UZw;aOXrbl zIk1&WlQ{-$=`N$>Y3=D2ty_B{>o4&!y^+sG$DGkz8uY7R$R;{|4u!UjJn`~hWchPv zg&|ahfp}=py1g!)-e)f3Y4XDOEoZMlC&`%oi5)9#0@FGne8nmufCf9SYMn!WdJi}g zcRMYhj-LVsFL+w_s&XTa4&k955m~(W!Y~+V=N!vkr+x3<`(O<;K3^zuBcXFl`&VZx zmy9k+!+Gd2t%~R96qn!whk+;VbAz?fIRfm&KFNbpo03&amg|gnfe=f zdF1BBNBf!1Mb5>_XDhyCQKHTlvAlouHx_?7+)eE(*Gm+3x_y52qGb{-cCMM4U)((+ zxc_6O=A1=Z$umXzvv$`TKVKOK-H)wzP8o6(zKW2nV@X;v|15_!KBSUm0u`oE2PC5j z#&11;yGSV4`lh2UjelH6TC9n$>U^4&S?dTjTZdOyL|OzNFxY(dUh%(N&p5)6|Bh07 z^a1^2eT~mDb#XM5@m0=_u5#3_3Y+2Bkd{@GyvvFK@J zz;owk{h$KtXU?PMTk%JaV}G30C(h`*_3?5RI-bq}ZGl2tx&PunN2oI|3{GJC+XW#n zaeBXNBG#f-$dVCb#>MGEdevs4z_}00V|ry})y0|4Q;9&0&+Pq{zd?tt%PUPh{LAxLcnKSXcc4iH*syiu89|6p zSif>KcE!pT`ale#1+h#5!$Tn|!uDV^cD@GG%XS^X#)!aCL6OuzZWS;An3sSJTq{!0 zIu42;u>pQT0c?=k>y19bvp2!jR>q2ukWTj!|KIKtfFA!_&w=KMFwy^Zm2?0-jR(ww zlypFM1^g0Pz)=D4?J&mQhR41wcfMA9E0!Xq1-*ZGOIp0}xtqdP&Q|F5%VUv`7exja zMkS`so~dZIgENJGY1`YXlzV|rx2@u0=T0wDd)bx*K!xUGc>!cQ=hN3Q(n@fUCg7nA|TF(#FQ_Q zXi|iHuv87yx?49MfRFu*kNowWK7UsEJ)v|r%niym+v;GS@56J6*vwABoYy68nm4(! zp<;mF1PlE{|Ab#$(Lj@%%ihjY%?fkU$OtA1Rd42c&T8O5!*?DX|6{up{Rb#oAn4y(| zCC|ACkCi6=bt)GE_JMHZW$3^n2+2sa=Rxzhhu;~W-%@C%ih*@wfT5w$x{YX^{=j_& zjp+dD6sTgNFoVblOn?7i7B7PGE#R*K<{DVt$`vxVLP)1)4TW6Jzro)ft@+wgdxJ|) z3nYXT`Um#HtJ~@VcO)&C{}S_x`npo;WjUqr*5R$eZqJBE@Mfa@x$G?JOIZjfnMgEWaJ$E5J4KpcyFg~ZXTiN_AwB6* zx$=O?S)7LkFm7@{i)=oyGcSb}6GB?Mc&CgQKpPz~Q?Zg2PyU#DdRx^M?05P2zdp~>ks8Wf->W5|IHGn|#P8t8r>r`!Pnzk(|pSBe)E%xTW zaLRwy8cpa6aED?z|CFD>;07=v4lXHDV8_B+97TICralGY3_d=viFz-p$V6kcmbj}w zt!v_3petn#vnlKhD+zt}%~B?evMH7E})$4#o#>Ud+C| z%14ty){NTO|G}7(^wmpF%EC*`Pso$h@&p(C^mX^Ysh`<#dhd$NY<-P)&E*h&n-5{~ z-yi%~btF6{#=;O%~}R+(RTD&FaN=x!ZWznt32 zV>rkN93&cP9dul^B%}ZMf|LEGlDHyaEpBc`Po+7N=ZFcF$aqM6oTwlTzwxhO1`?@O zKxfx5nRY`okTN%QVek?-bs0%B#|H_yp*$398?Wk0uOcgQrY^jAno-7Ij^*_o@F z=gXQ$Fy9#y$#r=)GLvv8Xv1jGLL3)}$onn(JE*o)_~S(#-7)oK%M%LO@l$p;<(~}?9{grF(&-d#Vda9yp05zk7_KS^OzA!w(WXbRGdsjM zteK3*o~(|BLez6A#YY!X%ru^LwFvRkd2HR$JrPV~;9&W^mT3@i(Q5tvBz~5$MP!`s zW^2<^n~S;63QU~JU(c4tznv9a3k{UEzVr0Fex&8mS<#)q68tG{e^me2h&v+s;}N;o zU@~-b;seja`p?g(C}&oGkW-_6j5f5~|F=r^{B{eGqv3dGlOt=43IJTz-fay36#@ z(Ze=U`uA>u$o7y+=kZy~A?{51p6OH6#~I#wi&yDQS?zY)1Z73AN{BIQ^=|YN}rUjKb)yLPnjbkNF(RV zQZ|?Z1aqi{cqq4fh6_2vwKHlVs;pFc45Rx%%DFOAQeO)m3C9t4nmjDIHEqb7TAGgW zJ2J6fYh5ZOleqw2uozCNt<0~8u>?wginZX0#=>PJDXY=isTlJCAgRF^j^}|8?G5C; z@`?cl6V|VQ%<<*luR>CF&N4D8(ty#bfI@}Z_M!=1)hG<9hDy zSSnanR!+*d)ky|y1yDZ#6JT0_{t;03<<|c~bc|{-%Ro1l@=n5S$9`-k59DPUU06C6 zLPv;QMLKK-ERXYs4wA7m5i2+NV5*`~WGtXmqeT==lJIUR+S z7c%dgaD-$p@HW$sK!FguJ=eZW+u}}|GTQS{_5**icDHpm_h@l5fM;c_kaGZG`E2`wpAVWkq!T=dTzWW! zd*M26AT;yYbNYEON>zDJ_F{3XIp1ew^U{mZOGVxhQnF@(%n=ty#MjT;n|Z5rsAEQpPnw;3jBWiJ2-=1 zRc7(}X-O648;LQ1`K8QeT$olVP1Z^qcf6-N(pXp+RkqTWsJ(!ItEuY>{43H+5YPar z-lfo89v`sM)l`CY*4TWF3Op|SW8oF|r7KD-R?|e#3|q)up&MI3uAJ$2EYz1Uct0}p zh=;q3d&N_c|L-0Xnw9`20QHOBcbDV~ zUX@;Hjd-WV1??qPdN#1nZK6ez~SNMNa@(bo8k_Y z0bZK5_dY}v>}A$m!cs;&x{4GKZk-=`Pv}IK5EcWk06i%`V~Bvbx;pPZ2uI%pZ!QL4!FsShpp2r>&U{NxU3f0j?G?rVA6}0lKLOU8UJfG?ZT?Dmct^$~m=q zB}5V81u&rg4Cv!Pg)IREbs{+9Ql8g3oe?PyW(~Nt2H%Qz$k#lGsLm%xs7Z*22|^F! zb*!dWl&)tcec=78c%#73t?BSj`=n3eBeOUYsn_zjq&BJ0-YOe9Gux=j8Hq+Trd@=} zd68#V@ilL3CbwVn>OQxG-VdIy#oA67vp50{{6#-OvY6(-y*JQI7zPctCfdrl5-h`X z(?hbXzu={?6_$YGI}Y>&k~46|Mdn&?5gZ2&gB|0F8e{*f_$|M%EU5xepNsC|3tBgE zY3gD$@Iy4MS2~cB)Fnbv%%zM+emedVo@(H3;)?6!00Jtw>Jw2##!qoQfbe2Ol1~xH z{zj8_=z;`wG*KZ(ht{fA)0;R*`dZ<_DPk-(ozd*hTUM{{m_%}Y$@`sX`g;8t+jK(w z*Y3`++Dt;o_mIVos?E@WT-}}v*|nvSD{A_e)fnaMS#-cxqjeZB+rEPQ`h8^Nc!A1V zqQFz%g=dJRI&Au-YBR>Tdmq>C7RAnd7I`S?Supgorn4vR33wf-0d%~NP*iAc{D|Yz zed{-Jq;hxH2Nm^O;GKW%xclC6e5Bu7TrastPIAe1A7jZ4kB9#nQn43ZmCs5UGX{Kv zo$Czc0wc|kBQXdo0(An-{i-R{lo|RIUwBVFb$T0AdBs2n@uW}!#~9Wc{IZya6;YeF zgeq8%QTz3KV#Re~>Q{rdo%8X{$;uy(`K>wSbJ_m{Jc)SEs2-&lsa?4BQYb~?Zt7M^ z+id(Nc$O6F_FFsh<2miQ`U!2IokTzYkw2;WDGN>-3)B`4s2LFVF$_vTArD^(z=NpY z8#uSP#lOjjUOfFpdS5%MT&4IX04HvAW@qTaS@ohMd{xDa>2o{3t@DZ2*JQzwTBDSI zM3cu3c&QW3ywd|3Cd?2EvESR_SHnmCoYSND+>OcKmfMe+844VaKj*(x5?h19bnd_X z+H@Ro{87aiD=B`7NjGe-eDp*4sA)KNkkvsc&!>Zn?Tba8y5H;PWTtnhPyA&!`1WiK z0-i_YA5`TGnw+&KM+LZl%x&N%oCO7{U;o%bae69#wduf)dSBg{-kiLZ z`4F#bM30KULrpXK+a{*|pq@v|x)Hk1bXMovJsO}rC*|_s=Xv>)E@X+42-BgeF>dsf zb(3k8Rcen0y{ckp&Geo#;-Q(Nxh)9TEH>JMKgqtk0@vrZD+Zc$@eOa{`r+LA=%StbW>>c?JMHmjJj=?Bjt=c{PrM&0%(6BvPV#EHf+VHaCtJ z{F774&F_=VmlWgwR!lq|;0htM0ILXurq#jhY4!tybR|n+v5aY))@(o*V-}A_t2JG) zYXVs#ivZyAl5uYVA}XO3Z!4+_vnDgAV{QKsQ3}J-HsZCT*|dgp~#~YC+}W1+G$$P9~&D?!h;4AZS^^_T9;Kt z3QP76e%x@0rzG!qsys`ai2SMd^YF<9uW_EmMw^tD?<;o=}J;wkX{#XnyLoNj=s5fgNMdC*$CC`I8PX39D0v#wz4S53Mh}DhV{^I4KZZb zQUM83z;P>O9Bfm}rTv2XAdnq>{1)49#hldW&Y`#W42oiQ4o~&r51%pbNJW(u z66~p|!IYEUO(@XN;3ChIQM!i0Y@ofNcF3B1V}lpvCy=kv@*xnhXJv*B=F)A`Fwf?c zURiAIpv~H!-1*4&p6cD0DaIJ$&ZOGWBsl7 z{oqKDK^>bXUsFF#UI^2R!JJ0$CoQ}?>@Q>)*|y;GO<~MxloyD!074Vsm?%#A&bqdG z+E=^y-}-6gIGg4KRFOs{w{`=-k;WKhk^~zJ)PG`5_%D!eot`PQ{SB^}1h>3mL0>A- z1&YD`t#gu>!EN7jto5Gb%&*tgf@708r6S>pF~EWbWTiN?G@)dGpJju5!Po;Rtv$7I z8et0L1Jn>=0E@LB1_BmlMbMIh(mJYxA!^-Rk&#zlPPkO1wYmcn>X9T-jsXxL`N_zr zLuZ=`ck?x%oHx@``|y6tIm&rk)29qbimqGaZ0O>?%;`^fc)ynZ6WC#pxSaMq`BhY~ zfharS%Azeic7JCU<*|)xi39&axR_priX(P)fCM z5p$UsGmUAf*c2?Fe&7T|HL~0piMG;2)W%X?I^FWLY%)niMM0R0Wf zGyfY7$0%X?88>>Oy3#tmO>D|KxxA;qrOK7syI?}L;=|!C9Re5vA~3E)n|c;XdAGi4 z7~B!8@agSz#C`b_6BUNaqjhO5K=4?OQ83L{Daa@YyC=>A(bnh2i8$|t;x`K>Hnrs( zhkBhnQZe4X1;}Z47g-$SyVuRtjBnS1H;euJA=#7a=%OyNe`@qX+d!PVv6rhNTlDCz zFW)WIB}TtTiot&ijo4eckPKfX>}cpr-ttB7AR<*&U)bo2`tQQiW}A_BhCi+Fs&~L0 z)jM<_2mIkuWBC|q8qWIMAhCMqDr?aAxNS6__>N=Gn%_rRF|p^d+0Hjn^gJT&WnbYG zvnawl>QJ+W!FqPMxUGcIFd^>)O#G2&ID6h?4omIsHJ(fr8=BYDM`-&U?!+$ZS|*nu z)^femxsr+#Mc1i6x9Ti1F0OheS?;}0#_@|jVpt=ESho%5yb2*0mLa)+Z&$t53EpT! zt1czSkgcTEdS#e`=q~3hG1EUPvnZ$f7Ci*SewR9oEFAXE7O1 zf-cnh3J$R!?;f4-mjzfB`LLL<{!G^bTG8-kLr)j6Lh%kBS*A2}2F@Hn5H zX02v1Ja^E4-e>&LD<`)rsz>oz6(#mVX(xJo0hFq4E}y2Jhhkp-deTpAD%)3IcB?=5 zQ?@1Zh4x?(?kmTQ2%I0%- z4d4ES{w?()e{hPs%5IZsoLLNnH7AkwAp{Di{j+Ba?CoF z!sv)=vmiC|$>%GQ+=Rn{`l4ESKA}x!`S~k(7fj*pOdCPL1kI_PkgtksyDiMn z8k_JKGu;07gZPCl-Seei_oXFzOt56Qt(%Qn3^axa>)%)!Qcz$_n98yyn@f;ybGIBy z;-(}$bldB(V;GwXV>32R21&hH+gJ(x6gE6%mMw~hx8p6|T`}wQi!v<|8T)k*WnKUg zGVU3awW%d(MpaIYb#_J71EZ)as2kIb>5*C!g}PKMR}r=9d4ImmD23kK+_uaav^S*M=<#F!k4|Yk z0Zwn)-ImT9{ZB!j)KpDY?#$N`{_?(Nc;vLu?vwgY9(57n&rO3@A4qxO)BZfjkv4~_ z)aU*Nv@R<~-=f@$`pZMUvVMQuy5QxkRjC?tLX&wbR^rVq$ zn@U7{vtxH~E|pmA50cmpd$&2oc#(N=Qn~SL_vm*F)xSX6XBP65>EZn5>lh?GP~osQ zgaJk9xCw_O-qC6DJhwbd0SKq-l;83 zk53VfE}Z;oUtAHPRPP|fTaCxF;RhrN-wXSgOWo&lB1u`?jxjUM?X1AcE-y!h1x>_v zjb-#uT%o5>{rxgDan`I0ey7sXud%6uceR37<^iw;yL{6kcG=9l!!BwXfspHd-&d;m zgbQ1i$0^@z`1JQjU%tm|neCgNh0D_jg~C6$(oZg<^@-TVB&A&ch268X-KNuZr%WM% z-lWXDRld~FxMRY_+lF!OaG^Ln%cG;pNM0vN^13&-jJvtg3z?L}0qpxR01mCd-2qpd zP1N9%a8KYd-55|io6?*4#VW)lG;m8pnO+RpC5!Qa4-)Lm^X(5Rjj&E zoj}V$GNcW}EAPJI3dA6RmY^CCva{`1M;?(fAt8OXjj7%qGKPxWdB6kYRRH-!ov&D% z0hj`9%F4*s1v|l&KlA`dcil8CUyr0h7yDxuUUT6oFXg0t0OTJF*UT(J?n`v#R0M5Q zSU44S>P~lm$&SH|Jome?)Vt7L7F#Il$eQqQ(g;mK7pAKPJRf%dRl)-Es7&EhI=DLhCDm?t`nft5()x)hYvQ z323?On;JlMyGsRQs79brP&hPu2pzK&irh$NEJpJpgHi3wLr=3H;XKr)m>@YzK24j# zshEKL!cO3@jl>bF1ryzQ?9?MVPFZtVtInq!qLMFTqG>eaC*(*d^R-TIj&n^%o09D) zpjX1ipbjo76|eRnKT?5{PQ|xc^p9MJaeGYHalFq-MlqDBy2`y7ZQ8dE`_^1OS?oA$9i?@H&rOy9eP1rDAfUH z?M%83ekJ|GA1~SR-2L6$HLCkdZYgK;wIV(F?dcz_&s1Jni;^5;zikS3yOWhe5>mtQ zEmqD>tXsuDy!QHTaO=M;IUPL;Nd``$l)N%>)HnO3q_#%dokbS;9R6Oc-EuoKOzimX zk!%^^0`kFoT2a!H<_&y7z}`4%mo&_4T3qY5SndWlja@A#ogrb5wEeHERZ zx0qw0__5f-LAERWBqT<);CqU4`d$Z_CccNlsO;5@fAA5FpToHGyJMR0U4g5oCavM$|2(^v zs_#T49N(?WFTry7c7^G|I{i1_`O~5V>_ykq7C+odq2wgzt~co9m{dqfgAu2aI5tI0 zF)rqHN#ELZ1cOsQK!{OX5S)M}4*XuTq^+dF30TN#OtDq1tA~H0Hdd!VRsaiy_X37K zygLfjKPY#k6J$oTW|$S={t)B3TsGBX;!=Sv8{-OaKV-{y$B?Q3(vAv*K*RdO?;tM^ z!qWzEw3z;w*5m8LI*xP?^}xM~`&H}qZ*ED8*NVC(-DIXg!0k)OI$QCp3C z(euw+J`9~-6WBRReM7T$^tf$w-leYDIAp@VHljG{d03G{wL3Wn%aHzk{Z?&nYx}Kgf3+9G8fT zKkL2t=+Tn93ik`GI@fnYP)8-{JaEw#N^qQ|!(2ADf_-?c7krfhl-qfKW3K>!l%wAF zgstvB{X>*aqL$*|9}mdn(CUFFK9HB5mM9|^WJLjik&gdr%oN}9Py@a9DsBMC&I(~w z_flls&(%3q4*tF+PS31<$)N%_POrU9fanrIfI@O3QqX3V+eXt+uVh zz@|E-$BFN++WQq$-oMTNSAKHlCm$}n61$tFF!#h^w5-bp6BC0I5=!dfvmr^D!^5)t zjb7dP=Xhd-$Zo50xDuRK@$E``hcTM9NX^8RB!8-u-T3a0!}8duo2QIN;l@*bfuE_R zH(uZS_#UxL8WJiPbaQv}JWc&3r)Vg<*z~YcbQOgy7wi-)Go%Lo?Y$D{T zp@9${6&PuNQVWw!?&X=Szgz9&P1x_>a#t)y1NT=46d+i;ectkbfzvP2Y-McWMBo%* zL8}vOplD59-5e559b^{{EPZ0C-8fi9KMY9Hn0Z(z-|ngL05}S*$z)CmJlNcv5;%J0 z@Ev2g4h{v+MTIYC6IdMj*A3rbQ(|l$?g7ON($$vy%Z^chJI#P;hzvX`n7`u%gS<~XPMaVsmYEf;QhEB&G9EV7fa9OokRHvD{ z?%x5Ou~~T3Y#a!Q+C#=Aq%Xt55Il)G&rLIBZ48CZU+YYXV9Uzhb=p*TXThUJMPeno zGTyCJW(4`HDgCM3`<_D+Lj+O*+VS@bshEG(7bFPM7%fM_Mc9pPy35KrG_j)ADQW6< zfF9xi3A_8&7784Iv$WK(>FM6BgO(iL_Ye6s^=&+^y>`=u+z+(E2$TC`MYZzl-KL8B zziuqT%gyPRHpa-9?-GQH`TV1P3ztv54<}yKIq#ZP(TPY z^@T}pVl{pu=ib9Dxk*4M1}G@nO9MJ(R=$cBnx}|8udOB(j$8QQr3=k84h<&|iY5{U zPRTEA63qs5+K2#1oUP2!>aKVU-rHDRo84l3v*tyTmb7zT=}M^)^r-NOJMewBS$yN* zD)M2>KSjA(pfSf>)Ab#v4y}hHs!QOfdN;sxib6_Ep;=2tsx|L!No5>H$LjH=M=)b2 zCNE!oN&NUJ1cv;c#V; zLP4FBgolCJMI*!34e~?6x8E?{g_4=?S=o+_^jzRu&=9KQcy&BA8cHx`SV%gW&w*`Y z$g9QrlF!Gz$jL1?mMC`yt(QO-!9)L}=q#g}{NFeXA|Z|t1xGttKxK?hX+c7i!3Y)D z7z_~sDQTo(H29+t>1L#a(qnWZozgAc|L1>>hZnx$oU!Nme((G9xvn_NCx4XJ7%O#r zUa^pI#gj)$MVevCn?%2hu#`4@h-=GG3>v?7!N-UI3Vj@5p+rYS$(7 z`n3;t?|VTAYWkwdCANXtGSuabh_rRYb}@|&mHIX#eu9hh?^gU{j{hWZ6X4+DjD1e= z>#o@A?ZWXU?G<~^ww*`!2ox_J;$Fy|hm^U{mR(MV4p5b~mX9t_+BieaH z_{n}`5w7Um7*9eJ9$T$=S9>#+5RLMZj`(9S`esLaV!Lsl2%rov`OE%xXuIBLE|dU! zpN{@14lt(*Vl&V`jVoWg`=)Q6P4{EP?c2gLoR3_4coF%rW6s|x2Uy()oA)6dL&&Sb z?mj7UXTSo3y2Y3j3DDi#a-ck!)4%smwWey2z-mvY9c51+o-mOe`IlxE8pfr+9fKIs z)E96v*kZD&$`P}R4L}sqY4*Tw`-2FMW$ejkE+)!^+gijGBGTnOFc{3wXOTS>45RjA z(G{l069dU1Ub5%6z@73%i0QsRMsZEKy9Q+m`g^G;0=h0*d#kA~)-Fx9X- zu>DoT?^gI)x4m)b3cN>CRki+9+_m)TSakR_(W|b7#<|&W*Yjspg4ZAB|JdHC*9Ys^ zo5}|(II=u@`ivy4^XPK0uQO8dkwzS3z=!t7`yZf8u*Lpr0M^^OG7vC)gGiiYwwQyH!6)PEu^e!Z%$s4aKYYZ_!lw z)tZD>80h#db?cowLwx`i{%^QQ^A#mmyPgJa(KPn_S9-1|d7@0x%28j3it9{ql0o?F zzHC?~XH~?f2NC>rYDQY4p?GNc61#6+tZv1Qhs45aU5cvEQ>X+ri6n8LV!+Dn+Y&W) zANw9Rn3~%4pkWUm5dEA}^5o+VmWBB*GV>rIAz3~_Ka(zWK3`u2O5`!&rShT`NtG20 zE4@Mi&2CZt&WC30Xk8%%yQ|n>`@{rAkMa2>nk@-H=fysD0~^L{yn7SLcTO2QwH_i% z#zokJhT?@)sT8Et;d-G3N7k`zH_Oa28WULc8@ft= zfd2)7fnYEa5&=+uEbS1p{CRzlT`O#ng#~z-qYc7nJ9bl)fITu}qVEFZeMZ2D08G8f z7%ALLTpWkNIG%VokNhH1qu^3heFT@STs*#M4ZS%gwA*CvPr7onBg!NCdWs0K0IdgT zE?kZ|2BxsRA<5N*9hlftkpP0lNJy&ieW6z2iX2N8#stqyh~JBXf_w>-o5qYePgg7u z2X6zwaVj3IbSO-?AZv-%lCF78qyVQDl=lxNoQqK8jajKTvfamBU$BVm9e5~>{^Hd+ zFqAb8?hq3e49(<-LX)8$-v$HkOMf1qa8I8=n`Rt_rK6Q-0$Thijdx@rOHp*(Y^ zmW~h#T#{lHTB@f=LVHlf*(adEre;CeK|lKvz<|zZNkTTq5or8FdQ;HA<3C!am0I2$ z7}xgyoDb;}2KQH?P@(0LP=9Ysjz|Gc|HVO^n*isiV0dy}!qBfwX5i$s(a9_TFx0=P z@)xYo;o6FhpwG!NEu^PTOl(d|{Xd(^@krZuSz02% zfJe+!x^tY4SX04^LLlxx0f^KZgRd z)$}%7sg1b5`B*9vF$;sQ$&N2Z6z z-y;2Y0G(ly5+w7hc}A3BqOodkuqdwgYqiQma z^;k-I)O3g@y=)r2RwDf%Dz^}}6(>xKXUmEDrgWF;qiocttgl>$gPWr0zIPHikql3_ zBk4b3W;UbP9c5&>(1Vi<(PYjjW$-}PEg*(K^zdtWCMnCJYxfuankDu9%P<<(_t+y| zsjWY3jT&=T`-NT={m{!y|MM(n#(%1xI7HaF4g{j%R2lzt)UUl)yfY|0o_kfQ|8a9U z|C)W*;4RFL;I_JEGc_Q}Y7b}9vTP}eY_F#N9B(04SbWSrU2KjY+9-&$(Daa(I91yq zZ-ERiuorp<>*Tii%JBxuzs+h1GAJ^-(u}w)U4XJvWlq{8Q@-*TX&jyJ2 z?(-tL!FN-oG_jZ^_mAMOG_9C8CnX4{L#*xLPL{Y&r4&s?}Cf7ZE$dDiKw zc!QhPlFQ!IPfuwKjfTu%AX+)zcO`*^ku=Jmhs-TGO<>BlCl89b+DB!^US`@=I=tpACXpR0D|7rGrttE z;w37 zdOh$xRw1b9<3o$jv|Yg_2xkl8*Fc5=_=thqDQzIzuM~A|L(W-!adWLCtbP^&=x_i- zVt6$CHKM3F51-_OWW)9U*=upBw%vodb9`T#?$- zXM^Rxmwc3=f^r*^Z4~eqw&62}KQbB8zs3keNC2vpKEU)NBmtWhW@PU-n60t7j&l~4 z%SO5BtjS&FssVi~tUA0?SL8bMSG_YKDYV&;;YG*e4owINbt0a;4B$wT?r8=NzDAs> z%G?EFGe`>*+~$zUrj(PKWYzoMQD5Ki)fOom0O{-bQCS4AydPt0V*Sk+sf_R zZAet2VN@VHZKtUDj4V0cz4uqUyX9l4Ig9@0{ghPeg)ZE7LJ`1);HktJGBxM(S56rH zKW4h~aaFxVYq1?5^Zu&W^oB=oGRRGoYkgBwvB8bwRRHWPp(B#cn!k}a(mF3K6ew*K zNfmCqB(!2v0=Sv(ekxzgxxE=7#e6?s96W9lmHFi!iXoC)p|ND(; zb1wWN;N+l}9$cAq4$7a zZ`D<0nCBN1#3oSI*j)EnbOW|uAWHP=yBO>oG8gXIA}lI3r$3d<_M4-6Du22PNupUh zq@PN@n*W-V*VSA-q3MM$(MNKSf)6?3ou-v|U%u-jO~>A~Hvid#2e39@61(3-`u6D2 zKPmT<$xr$z`fPly+O{-(OrT;4KS^A92CH3LI}^Z$SIImbT<;K@vduqDK08lfr(|Fn`>excAIS`6A=8K6yRdJhd61E6rD5tvJa}@9X%6NE0}B7jR`m0^pxLqy z#UHZ!c)dtZC^qCf>jnH*7x4b<%XXR-a#cHWMUo$%Nu9lSC4_RnOOX=od&QTNmmN0s zhwZ*-ILv#%As!_6`@ff00`St)rpm6node)moxy>u{CMo>dwmDMC z(D%hoHMYaCd&VMvJMOYfq5yd!f2}}@SGjn&fHQPtEA1{PYf%%+{}Tn834TV7THk@E z?u*1EKop4b#kB|O#y#@qBUbduc&^t;K9zKOWH&O-Lj7O<{r5M&!47g8i`6fEm_)!O zUS9`!DIrmt1Ct+8dOQP;c3sX0M1M1!^*rDGCs$Hhn0>fd*pP4V=}|_b>0t>%@ax5Q z{U8Bi;`TNpZ-FaIrINTkd3*N4cH*`;U}JzIusoo6T`4M)(wbxJZ_6#_ww9t3g4_Ju zXfb7b0a_7_lGrUEJvvs-e{X$l+40YtxABy@F8#>0|e$^;rClQ7^ztdqkOm^DS)_%wu3w@T%@3oQ!`wYJ{$A?x;lCR z0c^VssxIEM{Cn#v=d$A^kmbrOjTq{1kE#C1W)LtvY#9=sWjBp^#{)gE0btgLpKF>o zYrP`FYCC}>o>N2Ql7#yoUtWoozccUyFYR90$m+4lh+SR@WO*(rnUkP>`}7ah z(v#Uo13We;RPj?jPykJ?XJ*FGeK}p<<|yA46LK;|bZ9;wPAxnH%B!lPui`Agghes} zI#zHyTwg#-Ko^k__iD%|lu;Ri6FaIjl;*w%*`>1hpi<5b@ujYR#6IL=e<9A|F5Czm z4hGhOK#{qp6$XSbVqx-?^Aa$*4-zfJ0EfR-Sf?7vQDjuGQ%Z+Vr4OS=!3{a{k?dQA z$-hFRr?T2T!~lx#8=`|E2|_Gz!v(RYN60L}LObq=5IWg-vLcg!5-V4!Y;qWT)AUQPe zVt38(iU9sd-!$>I*;o3cFJrpv2;d??W~vn3J(C;pPlA=Y?i;}M$TqL7Y>Us1Zmo?G z3yrmKDN>3hsd&U9nhW^Oe*)@)wFDLm9yg`)$W6ft4Uwef5JRf@E)~#^;812}{%XXe z7#v`JZWyFZ92Y~eoMWagUnF4ETdfdmP(q(DSE+fvNalEfHp`3VR=_?CR4;i;kz8`>uBEtU>qK=I*M$smi|_DJ;x|TAWTf@_4tXMz(Y%G@A5Xl zw+@m%YcTr8*;XH<`{AT`kbF=k3Rz5dLq8As|8cw8XcCDfA#=R_E2Y0Nmewt-tS+xGTo`JHF-L*6=+iw|leh>JY)u(SmxZg<&9>g0Nc%EJe) zjIXbDH%Q(bnO(nC+vAa|`#sQnWz_U(`ej_0anTmX_(I{h4e@f*%fcsFTaD^B9m}`K z?aXWsyfgoi-vmYed(RfMc>4e$H!*w(+JEtOLM9o~No%UZ*tB|ZK`hKTB zVOTQ|X-hWTbzx%#YPb@gvwVgc-xeHAxh9UKiSPPr$8hn9MAl`>(j4RfH~TGE zHdZA6fR+zRwuho5ICU9ZD^jFCS7ILtOzJsG+8?jo7=X|0AL`PMxvHg8hcW$~y1$y% zJX&@y<)dd~aECa#lPl!x4)j}hD=9%=_Zkk*(mZyekG=W*P)VS6||qoXa&S%WJF zs>j{DVmU|Zo2vM0nNjr^`KZhVahuYbwzQeUr>lIxWU^cZ@&Da; z2W=1wizVYuL<%c)VonUQDqPUJ4#%UyT5#}9Eu<3V2S|A!FuvS6xHN~+7 z3|tvN{y?%URcO9gWSCwbh^1+%rLwhev29lb@j9p9udh#{7e_Nv3ob!m?NRu|1S{a- z3kI7d!hI1X)VRfz8(dt|eXtKFoaD&K~tlN8zJ^x>8 z^?ldV0P+UBU2I$+57!v>Bhb!q%Pt=WjBaC_YMdEh{W_U$+$}@YH2!3c5?5h2 zcTv-!^fEEHb|DEXS(dfAtQzOpY2i5e@_}KabIn$SVy^ML^tF@Ul4IM#&7a?cQL{3t zw!1yGW<%vOKQ@=GIge}t=q-=*^gE{SR*74P%oTj_*?ZV`l-Xh%Ayfyv@yR&3MP_tX&cj zU%43gRANEs1!)A4X6qf?Sl)A;{=sKDuZN7u)|<@VXtB?_?sjb)jN|8?7$z+%K8P&o zT`qNG+@g|OskuIMPfrz5v`!_;-4WIDajHun%Dd5fLcG_5Vkv{=B6Yhysd3&7ur2rz zc1}lv8d#Kj>xSlWn5045z0;~CsXHf<*2b`zaTo838hBXEIn6t||HtiPkj&w=QF$-^ zc$V4~I^T4jc9L*_*5lns@r1P*lCm7jXGkriN$?pU9)j3i8sf{p1@7?BOT zlMTr?j4(39aFt0j>uv{e+)^S|IpZ;uD&4v0Ymqvf_ds9X=lcF^Wo*^9{bY%iri_wo zSVSONi3ONyTY?SmQ8H5HE2}`l>HhxU#XaD&QU~%svc})Xe1Y+a(iChnfqh#{OfLz6 z7nVzf8^PhB1uAF@MhZo=BCvM=;H{QAzwI1Bz$O9sM*w1OfD-LFpeSdtPuE`ZS+mE2 z+e2@(BDKoeZQ9~{jiRrY7y;B?82JDAinqYxm9X~{sasZXV}I%VVyK{?263^hp8Idk z2cNph8DN8(w{1%(RY$7x7iiq1V&Mh=V*@!aKz|L#x@nsF=yU+dcK1FSvtfzF`xKhJ zk(`3&0Tqm=_u5Pt~pxW7ZesHRCR{QW-2zgWgB z&@2H>{>W|R9hJjU9%};n3m{~s#oy)uD70i2XgH#smC3-J4v|lD_?}=hqJDuEGCI;I zh^;aLpqg0MDb*i^Ue+FjBa11q(#w#0ASJ)@D0Yo&sw)Pj^ zZIn6l5pfK+uZbGnR6cZVoy;DzLyhCSp52{J-#q_v_@;*X6>-Y*V4~Sq@vmlh3aBpM z)y!;C*1A?qH8QL(IUTd8P48~XDNy|ywX96zahd%3*HXi@nt5gHbef0QxcS%iujZ2S z6U*4FInRHV{%Wb$!kpKJNICsQ7N<6=K}sf#y`B)c&SOF4$&Na`^t?AQ)q0u^o?$Da z^ZMf2+M4Q!PVis6T>RkClVq-$*Qn5fB0GGNCr7(osn0^2U7jl}QbY>Om>85zp`&oe zzxOVjESZr;4m1q7naBW1o&@a=B4X7x@V$eJXESwwt~>Oe(Zo9!U7z?xI&C+CUQm)2U>EE|NG8Tsu3J8%x64cw--uuL@S?h zQTj~9sb|)JYrlMSIb+&*Di4%=t@r5(REWKLsIb$K=!+teLUzrccRQV4_g;LHo7Ib{ z=vcXBGCx?Q^oZbb0LEpzul5P6CRdh2vF>O9%TbL;vb$_v5#&}r-ZIaML0op+k<2Q< zbz+fp=7ss2tjwxi`Qj)&2^fd7WqvCyo)zXt(tXYTVP^ioG9Yx<9LR-hNi#^;;=4JS z8x;vVF70et<~k(0;h6!U1u^Ze;7)q@q4hA3I{4@H>85}F=Kk`fw(jB=wga1doPq?HKB!m|iCL`sc-Zx-7RV)ukB(3U&nz;=s_G0n3X|D1<5!OREkc|Z+<5%mNL?LMkQ zGg6=c$aG>Y&rCk#^EZnOp46Z>+ac8DYuT>(6lKD0@5$?Ad%WO zs+qzRt%#vN(;hz^n?;WP1X!SO6kYU)j&WZreI&YpPd|SnPZlkTDjH#y5E*y^_)lPG;y|Hd5h$1OI{(!EX|@B zPdnQu6f!K#gi>Fcmwd$L88zLP5XLzr4s<5Vc5FK7)e7{tXTq52(c@^!q8C>ODek&C zm6p*BOd^SPW8UTHCwxDMf@!H7V?3{``e+ntP7^J9uSF+_Q|`iPx5=#at`_pYb1g4y ziTBWi{pNQ{#zv@x3p282uBA!*Y<2z`TPErHlDdnC`GVS*ub|77&vI}Wcf8#|(}a}i zeI&iZyM8g|E$#JqP4UKe-Sut*r6Q90!t;{l@my;)4Xgd{l#uG++`M1M?r!4c(!=ZS zc1Ry0qU2Fp+Fh6I-QHMUw}%%y4bOH#yPsEEvdp$qhA<{Yr=PoTHBG%DyVHlg7P_(B zvYi%FV6W9jpWLece%p*#@TT#-y-$dJwWe+zGe)sYuSEm*O|DDdz1;-j zS(u8LnwSo8qkCCAe5QeG_D#Mt_$^o8J?~T(M;C3}b5xY8RorC%>q~}(C?cYJ7t*>< zopjjn<{fXo^RzaN3>X&d{dly{MM`c3>3Pmr^tgCvL}<>$(|9_}m3aE2<&xpvw#)3W z9kQ~jcq|Bc{v|T88J0d--lxZHufs@2bgQ;k7u@jACq}8{zAvJ4luQG%;H@EPJWd)*0)MMNtHpiO!6ydlco;?#u#$+15jAWuh`+IWb4t=*5>y+zvCR5Xmo;STLqr3w zwl^g~B7J=<0xaRyBgTpKA%DAl#@fhwK@70mtpNf~Ox9Uol70*dsZ)YHNGIN7Ns*M4 z`&AC8-q@cXbLpBFbes$F80E{05!&?<>jOBG!LbZbta@l613z$=DHn|MY+U#LZ{k>< zIm-d=h;sd*#a~%4lOsSs3l!G31)7SJ%(Z~K_H^p*t&Z*g*qg$5pka)x_axi(QUrmA zscL)vFov?su&&@nNGFqZ!xCyVP`T^$`3rDULeYHs-L(YG3k zDCrxol|w}B)n0ulvacU zaT4RBd<>@Co{G2XC@lzC;v|LdWv?ln7OejA=WngdiA1}j*J?`nW65%nG-^Vl_N{;D z*`JP}_9{k&cb!B->apYfM4#e-3XZ=p&yDwYM}JsGN}6rTM*%G;+z8PQM{JmNR(_<< zaS!4p30-GRZS9jF!EVP!^HKl*j10(Txd0qLaB!vr{@PGjU0om;Fw3tQs%RGTAVJa8 zaY<;2MsdAEW5+9unJb^aaK{*Uvz|9r0~0w!sb~df50Y6JxL;s_-cSU4Hh?^;j8w>HavcXwv<4-Z5cwZPhzo8noKfgP=hnNb%7}LL zy5ISoetTn}(23Vy;6SeXf%#vXAsnxm{5B>;Hq1G5xXGH6=|SHZv%@*G>m{QZ7lwpr&@HXd?^Bq` zm*I8)VSxmRI3?@4l4ONtR6i!l9-V3RC(eUf>@uAUUwxiq49?9uaz0ujFVA~bw{>+P z2$tx+7#KMImDbWmpnsbyy1DE>mEfgjKfvT-@jhrPw~lggO=d$e;co)*^7|K*9_zl< z&0eH6Z4HuBR32ghHO{}Pueyq^wsAHuF3OUsuFNHs6klNkKP6#Wsx|Kz1{>ZoB~4Rb z7t!~K2;1~{(vg)Gx#1};pFcTsZaKb~T&9z4Pc699v;_l9!ouvRrN}?OT63$MUEdi? zLu7C(2aGAM5?g?5PLUSiD)_M^0nqCaYXNk+=0x2NF4U`PB+G?gCY~ogn}M$&q$p+U zhUv2tu5%`GU|V|TX{NV4VUfM{zjirN?%%6~hLhD9CF_$~*5D~rM}_e0Q<*IMoog}W zqpJAT->MJoU+45rz9S@$%+7-)COk&2%OCtbla8hPOYEpHd}1BOwm}@!>+vB_;djS$ z_u>7MUY*!8#s^W-yfy1;4Z_}u0bYcQ+p|07pIxciUAge~$_~NR(l* zw%#i1-1@$fY6j~n+jZIW*lOAs%}Y$&{{2b*de)3;#M9_^F_PVw-Gng7ua!w3BeNWl zjYSMw0J~*Ghr4d11zWp$XzU?7-H?_C8AR+YzyrL)Rq&%wsEJ??!*=y^rO!4s58Vg- zDY@qTP*v}?(w2bl+C^#saR2&Qa;izB74SvW?quWjop+_@>4{0eooPwd3WfnU3V3KC z{TSc?6hy2|M0%E8>{?)bFZKEIn}e%ZfLxsg8n1f$Ry^!yl zS#S6g^*A_gpB*suGa27={8$Xac^OaCY|K8+4J4n;NNA@7H5_pLA9Do*SgL&Y)wqtDtufxgLs*j_ajhagHrf3)4GqV5mIY;C3BgU>FdpCT2e zJ6VfUnOrLIIQIm6s`R@LF+cY9OoJX=s)kTliACg38UFlsB28W?k~wmhaTy`bn$0Ww zU1Z3SL1X*Yl4>{amyMspHvdW|G}HM;B%xL-0<52S#X7zCj1);LvTFI;;&?6q#Q(WT z%!h}%Qw<Wm;3P3vwjhDlkF=_d!j1GkN^KTT(abQ@%kmZ9t3ublz4T6xc@g>5~TaTi~O41@N`ABIf+GTJdgzq0q% z-Swvx+7e=JtG~+;Lt5k6CvnAI`;OZ)1=&Ul<4dy=+9V=7z#)Mom#)yw~1cT!?xLbONr z>m{rMkQ&AyFxr4|*YgK!J%yu<><5~!mb}xyn}@$!ee9EA!henO{d)jpA1&M%?}HiX zh!fNoydzsnvBhI*?kov=l@k?K`67k%=7eDe0s4^*L@5E$9)fZ%1nG;}0S!Em2Whx~ zCMw2UQ~jNo+HT|M75lg2sak%GoiB*BJ?W^_?t5a+MW0xEMGDz7(+antmCh0l=Cy69 z460Ho;t^UZ=8fDJzZ(;lg2ZIS3k-~LukAKnktg2&zN|i)781i!#*21DsWGFQJ2NB8 z?Z!4DONxA#yYwz1Ieym#7C@90GJ`)wJz<@di{l97OSLtqJ@U~s`uGg`_zKk}{F6BS zxZO>k?F|59fk5Q_S7Ve4?e+N!E#UglZ0xX>C}%t-+sp!j4V}(}#9d_G)u`U*n^mR{ z$0i2Nmq`GPdzo<-9xwoTAw%&HPcM;#_N)&b0Yk6Aojw|#tH)Ft1T7Mw8_3LXjxz6L z6|d@GrTQgzba|1tLfNqHzap>D#-tfu-yG#_WYh+d-n`*1y>X*^D~k;4z*YryYs8W? zWRKmau5^?~u1wRbWrEbz~b$6;Ja;!`-(n^_IjmuCaoHGF2 zJISHCSHzuo5I#AD%NuUP_d-Jng}lJ0%|$4ljWF<{igdr%b_XRes3v5u-ND?tqM9@% zkrwOU=Q9~wIX&>%+calaz&A@!OMiOYXJR^l*T+6ykwr-)hTAXCpIm~YCZ{Z#I>;g< zU_v6j<$}|!OdL&h$nij~=ic(!cvIX(>#vw&OGf#m~ZAq zuWirCL~6k}EXMAmmWsy(nhU2A8Lw_EXf5FXO*NrdF5?Bz_Xl{Np&heAQm((3n}_QM z2OQ;1#!XEbkoW2Sv*stgb?a95U2tN-b-l-;8nWs8g!V;!aqdZ8QHh4C22rHayYz|= zi!*zVSZ?j^ObQxp7w|6J#79r@O~ZI}Zq!FgQXZN_rVgCSrpKeoZhxf`p9@7uP*+1u z?c`;~Hx72hWEzXxtM1%lL9_(Tz9S{qRv3Ujf(S=kkHm5r z+&tS`uPgW+A01lASv5^qV?z#bC_SQ;4SL9!UT^2PMZHN6R|$>D=Z@|Y;B2H4X%Ag% zOK7((7YN}@iVjx;AtA)8@*{^vsHfyf&?o$;M=ua}b-Mq;5;q#>dM~_z7reDd9Pppi z`}k8^d3?c_{m3dZzu;Fxw#)&PQIeBrb|$6#C&6?od%HEba4<0)~}GE%e~xN`fsEZWozVPSEdKC`}XMPO|%4qm=ZC)8Q zd3zot)N{$ZlgfcfjRJ(pXlspg$M~EieKj6200<7Ka3jFTIUJqVB+5%5R%b!X8rXVz zZxr*_HjE|z)Jv}aCQnt*4E>tbk%f;R*)JE@@8)d7!Bt#22t$DA`&UkkSGkzJDnGa9 z=Da9-Fs{7XU`{T(LPrz}e35UoL`nnmh4~wnodsQ)1volV)((!tM;lOX=TP0pD*3=5 z>ao#tb$>1%6jxfa>b9UEM}fLfkuDbH;S62+e*YF=kr|c%p!}eoo>4>Q>i3-Jeqn+9 z_jrVSb*4pxb;#MyT8B~LSRrA6fGZ~qg$uMCG1-;MM%KD%{9W5L=M{B8?CPbn&1_~C zhi3w-OHn5E3N0xC!cfO&ZFQNIuh^&_+8(rj_{uUPL$AZ(QEep+k`Hg z@48EQZ&*o}BYKHaX&>3&2s+UnCZCBPOOoWbeWIIwMiCrvtG<06jBMij;-X+m&DrQA zU!*qj>5<0!R?}e9>&R9nXXL{{x!*pbA2Y62yF136LmQ870#}TO#bEF zGeHIqE03h(45IA9r8dLNcxhp+_(C{P-U4R8<^?a=4Gv53 z9!i#!B@EC=n{p8xQi%@GA)D7$V=A2>G&XzosX=6xvAsT) z0`3VKuCHSSWeD~4K_YfYu=4baLHjAXZ9McorYIgp8r1xJ5gl7?bCacANP`6rg2OpEvTvkE)B7XK@C6Yv#+uKP&k6aO zO(RuPNEC8h^~2=5k7O*DouR zC)b@1B@oz~Dlwi8bjQOBI z-%OK=x+k3j$*l^-{+EKw4R;N~k@bF298}}V6yaA$*{Xj{6ri9um(8Z#si*I*ZHZs@ zvz>$*-j5P_QlZed@M(4^fS6!_9KKFx-#qJ#p3#%M%zD2v(s=g4N%Sz+FtXXZ&)Qb> zbzQbOb#9((2edWpQecjnVluKU0F)iA9IkDGuFWJGH0n5Z@~XsU`+nsL3hNlZY}98;@syf z{`a|WhXk7{3*`myL7u+?&#&zNYM@EqOw%|qOuXZpm~GjmpD-{~kcC$jDMHdAhRJ({ zf_CR!Sf>HiSc>H?*o*Ra7Gpf1CIuokmUU(zw6mRk)UhT+E?qk@>gY4Jr$>6+{>~au z3pMF@kuUo%I=Y^t@PQkdk`3m{r00$q2w)MmM^(Cku^^4XyiNL6MlKiVeFizE4yw_X z4jy105K6$$t$U5;bm1D{5mk2UfR*n$=u`sOPIU>WOY+z9<9Gg0O{v&Sm*48@{`%d* z6#SHuWNe5$&C6u;u4<`XR^-#LK^IAzY%-CHvG)In_Ocu|g=j5KS9%osMrQ;3gO7-E zZry%KoXZE#iS3=96IhqB@0h9=kGMQeo3m{B7;zL0=hpkiuTdI`4Dzi254AHP=`cB} zNufhHcutaf=uknsH6YGo!s^Gv!|08G*la9=THC=`T5(f^0q(&7 zt%zwDRj+ix)o;D8u^n-4C}HLr2MaBN=jTtAK*CH)lZ~Y9j-bQEM#)@vooRlZX+b9u z{6_3JG;*}jmT*Sa1E3?FRa2P^(oT%5#elU?OkPV_4+usAM9-Rr0H<45>#z_tdibmTz;pRb)gUWp4y9|ASU%l4XAu`eMu5LfgO&w;Gc+9;V=g@cxR8ey>l^|>>5j=8+!S)Q}L6dk^(ZjTJZiseOz;~Qb%*CrlWtz-jVQ7a*auNOq~VeQ z&FM-uBvUfcnn8vNPwF7MH&0xV)<0*^#BG+U| zyK3Jy>b3wszvg9(lKh>z8?z-?m#fk4H_YR3MaTvR9a1A%OMu>}NL{0}S})<)Mjy(X zZ6eZ+D2Tz!x(97A|FD_S7v?Vn3Ey8;YmG>ivzT520WmkUxnw)0f-}Ftj2VCw<>7Lv zHHaxFUS&KDrU2Stw@;pZBHr~yxzAaonC{I;(m2D~F<(DTUV2Hg?_RY?xziV(2Mb*y zNRK!whvu?fW3xhX6JPyK+o<12 z^})`M;^>E+^8-p{DG?(gh{k8&8`Q)oOJO$P&usgs{n!h>HQT!cAk<+%tQkgv&0I#X z0yX*wL-0gs@i%$m`z`#0%iIm7aIQQvUjVUd7N|&jj9t9nVXgW9Hv(Mk4Hq(28ylnc zx*f6Q-JPR4_i}NfMSF^Mumx9kFUsWT<=hJlR?#Z9 zYbU+Q0$Gk;@D8zV27xR)!tgn>=j!Tpi!^6a{j^l-2iDgjA3$h}JMTA@tab0w9h_g& zWQl>;_=eneiU->;)1Rn&K|1tmYo5cX<1s37x91(QftY69H&ZEyhiLJZ{^zOC{n2L* zU)VgCt{JpoDg47t+ zf`-2+<}aul4S5zFKEcSqU#`|L%y%ttm81HN3%5^gi|@_UM9vnHdX4U+M(6Zt5OCa~ z#rzt(n7n*JYn8}eB&RNdfLANg&uqc>s)Wcbzi0eF07VA4R}3 zCa;hKhlP8&;WpD9XDV%gT%9`^G@CwpWLQ9*KdYSo7mvbU*g#(^2%u8HI5dSH3kA{y zTWTjLqR|%A89|H*j=)xY2*(}5_4g2g--hM@16vGF6+O_R(Ff=^m&c;as$!UQ;wZe= zekuTI0CJ~Pi8LOt4aVs$m>*2&tmYt9igpLVUSydG{!)vZjOB6?iN)joI*F-gBbGU` zw%l^J-%q&6`L<}U(@*b&;=(xU)BLo@3u3#9Y;nBJU*2x3cEv8P8W6VGK4-GuZ>zt_ z^sP%$S=I{w@=lj_IewYEe3Deb*|Rs+bi!+M+c;LO=3kP;L9R4~mzE}2`*G2Jp8M+n zr>H=S@?F82aD}c1#5Db~ZHelo9ybfF;u34#r$NV?9jBk^XHm>ZFN*g87!!fB>hbEJ z-KiYbh8WI0{c6_Lg)b>%no`f8Dtjs3<5{wK?$nYBBc$IUxBWTvnE05i-$QfVhx#Br z^|-rQ;SkL}DW@zE5jQGLkJr`uxENu)iayGAL|;|F0;_8d2+uJ9CY!!S(`EvYiiQ?X z1EU??2)QS@>+zi?{NTR-YtLY9t`WY;O44%q9j!8)czbnd&yPE%_K9uM>1xNbmPr!w z%~5|)Bg+8-VX?g@iKE+Tc#FOh!7ZX2Osg4~MDdk;#)F@^8&;>22d2ENbNcURZ%VaJ z;wP>znDcVg$~W)#nfsNs`}qG7rtsAKanDV`@Ar(r{26-r_c;w*TIjZ2+mS8cC2j#6 zYjV#e=Fe+bl$0W*%<#m+kX&tQq4j@OX~jVU84zrb1PB6ZC82l$fI@|YVIUE8bBzMP zFuTJd(jE=ZRy!_aP^aff{{OSApfo1J*1iuKnT&{apX^GqlnJuVok z#H;RO;c(0-%N%OcHa0d;4L~84j-9z+;1w&JOin7O@RS5x{UWu9jR$csN(`G#voDgr zqZO<0{qSd9#mVo7nUDl}q{scoHp})rw~KmM-zTDvbC>>;t$_{I*wBY^|2Y55GZEX9 zqwfSki=F~{q=0qr^D8CV>8tO&w@ESIHK)I)KhL--_fP5mSc zQUjl9EGQLEld*# zhZCH|Spa!4rl3F*Yu+(Xpd}AQY}$@^>%JEo=BUBF{<0CPoozEb#(!T+RmMi2au^(| zsh)S?4wrWZ*_<9F=-|3w_;|Z|UQ9U@e$a!`i?fCzij!=M z(_=2ai4-!3shihlviTKnBsm(ge3MQs3X#F_#-H}8a^c#5LsE!jp+*T{ny3Im!OABa z;`-~7@+WRI=t;HBQQmtfCsimC7ul_sYJFo^PlrJqf6~$VA4lgMPUZjq@uZNB5gnal zA3IcMh(ksZA`~Boqlt4Idu1hiL^yVI?2PPntdt#&LkC$!M)ux&M8Es{yDop?x*YfY ze!s@^`FN@<;Wu3w{x-g#tRO%eK{8Bl#mceIDoM=BHe1 z&bL3`82`*=bDiBeM5c`-PNE)1MF0A=cJi8?ik>h6^<>;QhQ0_n3s+d#5U9?OsyH#W zd-%~gfUZr=NWhb}@=W4~Aic_3fWY6Xl(X)xs#|cm1jIY@$KUS%=jNReVQDkiKd}G{W@LEd1%|;@e=YJ9{RL!P zUd}r89v+lS+WtB^FT4KAf*aR%< z+f59U_O%uE_+D;@h~dSHs&zwK*a9ba-T<5e(>LD*z`u?J)F-CeFt4!c7^1a#lha6U z>(4jMyF_GWjxGh7HgYK`6&KeKWLeVGxaH?XTU>Xdk`3Gt;ma5t7}yW!NBy#;0zuYK z<-$w#!>KLVP4>lu_OuWKK_F87LNRr|fGfnL^d;#c`R?te^t!mox{G=`@>O;)Ufba@ zM1s!PbtmkJYs^LayIi-)Hr&CIJ&>##=&y!^<`nMf-*NQnB1n4l3zo_0W%|dR_$7#x z6(Fhl*VL5O*Z2QTQXRL}V~Q&vm{%NNWlc&81&}~d$yi`m+MU}=53bY!T1@kt!2u!d z!a@MNgDq?Kn$Pk3IR3S2vQ;@L#MN{uDXD|O654TAc1G*(xcz3x>4c)OF7H<^g&GSt}}V1t?-$1%LoIvqDK>rd%4SpI_8CK>3at^6h<}wz;`@Z>50k@*%%lj zqI!T0++QwAoQE`#Xr!dhAJ-BI$JiY>z;f9`%(kELGMK8e$An z>NllFw!{6}acv2!*tQgawNvWr#iRI>9!gR5%5Ht&p0on$bxu_Sz#YHdWcku>lH&Bj zSHerRfw*qt)CxqE(Ir{n;F8qA2{m$4VwH-lCS;avfHMm3I|Yyp6Y84#i`2UHK(tnh z!dKV5(~zCS!<#Op>m{YV?XOcMe@z`BX9RBee1qMaTHH#Ee&+E)AU%ryeV+>?m}|?( zy0@XLJM)L{GMo!DT|jk$TL@Klj7Kp5I-lKjrfR3q}Kzs+vS|DzWq`*n~-Y(dciiF9WLg$zb@ zU46wOwD_%yD{~2X7QjV-|5oJu2=-WfJ@zW&`K`v|dgYs_@qzy~3KU1>0`Ve-n$D$v zr1?=)quu=bME*&VT zFzV^wsuNGNZvV*t#lJJ;*ey~Hh@t)Vl@M=eim+b#caHwVhZTR|VMERZ=8aF!}pG@ge$7r2_ zZSHTAV^8K088v{-@2$6Ghm!~7eo{gb#|06+%v8%Ls!l!*#Wsy1ZXv*UVlV+A-g!*( zkhbe6ve5v7?I8Ydmp?-yzciE2m&^XO)kHMPo5i!W5_?RbBZf7?7iR+EjFEWTVrE1GvS z_m<0%lc+S1oda+9dVR0-yLTZx*|X5eSN`?>zSW=n#agb7+Paonv@6@Yt5G>%%t6sZ z))PtVb9(!Jtn_r3x$-C5H7J#7b`K*nVYpv{%OFiXO%ZNPokr1<_mKcijp*PP2l%n>X_?Gnmd*L3P0v@$A zL%BXDD&O%rowlG`@+)O`RM1h*e%_C9#HX<<=WuIdQ1RsC#YC;rOAQSU$b2JzNA;Lh z(6G78-kr@xwyQ&NU!2_;a4$acrMcdGRNE6>cF8Fz)@B3LH)MX2+BE-EabfH36deyo z@a1hf&-#Km|Af0&?mV>*n)=>&RPVSO&~j`9ZV3nxkZg@Yaz^Kw{Q#j9ED3%!$5)uV zLcx^B1cIjy8om@l(7}>}1Y8WfkE8g&eFo-a90z9e- z(1p+JmpLn zFa^}|02y|bHDI9o<+!z&)cT(azy-F|9X%ukekvsdG&{;mc{oB}t1t&9lB6M-vxWGi z^|Ca3?z88U=uV?Vb?jmF1M^6!N_1-vs`A_-kOE*y;5AHG9(yq-GTk zTtPO##0E|OlqngNA;;!Cl3$P_Y&{!3HeP*cFka;%@G2Y7_WUO-^w~%{f(z}A`1E=j zU{$nZ&vtlk^S$tU8-sD^MI~U%f_o)CJycYU5kejZDP8?U&p^>SgD;eLWQ)D~Zt+6t z=Zzu$r(fh)3k@Kz@;YxHvTd7pr*>RBK{(1UDTLRc7#Nys4PcH#?7T;gt*0u~c18(( zj$5}bzkl6c#n``{sAy*T{bOQTp*kb;lzF_8Y_@obyf$2Wv+kL&JId_|vdWaumzB(S z-&%NF!IrQWITTyOudA$-Ln8k#wHvo?o+_y;spFH)9r^o}SBIFvi{JWm%=Dn}`*;0e zqCqz92PaMjK?;L9C$HsRZDn{TCd70fSX3NCX%DBH8q`(XU&dy2 z92cINBD0r)ZfUmi8(RCZxtJEn8*t3iL|^ll&o9J>CjJ?ZetGI+oBiJ&f|xh*Ayu|r zWxS8cbv0XAKNU!a16cTYF|!Uvh`{}sya#XEeIk@G0VnKUpYd?lUei`0{0Ea7eHE+{ zh049(0jS&q&Um_6)j%jf{$vR;1Mx?~iUqx+zDLpg<6)p+6sV__O*|ge*>WbO0Bv(1 z8yIWcC&kAB$i=-;#D2*PH!(I$E_~{#MjxgMSZnfVq(x^sVLclk6{xXW$ufhbmG0!q zv2_k}u`4=cWxxI{%Z6Hg)Uhiv-Av-2`!o`wG{UD=!x~|kWv@nBuOrS}Ym~E zTA<9wjK~z-=7Hto(hU!hwGVG?ab20;0q; zGpxW1AwcK`&K>F~SgN4_(orZ7P2?I36vjXj5wKowUY`lXgh}6@gOH=Oh``JW+g}YS zEVf>(iN&<$BBd-1$j2-hXTPAU0EGtF2UPOE?!WWN zb{Z0eB_qPg9g{V9jGH_;&rii^0I-EzV^(|Ir=|i+i4D^qTyF>(itRCb{b+<*xDl?ut}^8%uet*b zqU*XKAw1*R;pS<5z-88^slrOHe(n$8qT5d&w^9MkWNto@S|q;(YUmsn=BC{CLxi2C z3U?FZ0bd*Un5krdBr^Z9Yky|aw=Rz1t*7?9*)60Wo$QG?j?DKnfFUs0yzx5GN4t?D z%SPIvumIDO#fLDK)s#?bc`W-)*nImAa*+#&Q0dNLT8$mo-> zu5Mpb$D~qtlRZ}Fj}jHauL)*aeO6fIF(0lnfgkh0 znz^5~2Ci=YVfC|{*>)J4FBe_SY|Wp$4Wig?RL*twTYdZz2W0`PNrWmQ+HIa#R(A5b zA*$(-#s<=gJ#`~$W#ia`o2|s2jGKU}d~&h}9ZbD|Zk|L%rNM#}7}U6=xP8?KGH)oW zKr$on02=t>2)E{D)_!Wqm83;+yBAp)Cj2~o2r!U3jT9*)a;_wqNQ@0EY=4uhp`57GCnfWP>+7R;i`Z~EG2RnovyEKd#-1`7oPJ@Rq>CmK*bO3%n-U>X%8tkBN91Gq}^fQA=hniY?S?Nf#l+-+-oxan_#SqkM08`>%24nuUw}h3czCCJgq>ii}Cg4TG_lB!yt4%l8s#tqO&M z(<3fcRU_5ne@Yq&KrGr=(rkLC&#gOATg%*8IT34mXAj;nT{*9&)aX9!SmM7*E7$_mvg1x zvNRn1u2k?-)u=jylk`DsM*U~9_s{CV^#^z1ud$B#E_vuZ#B}%P+&6B`0jKsp!v{Cs zL8tw-1Yzt4v6KL{$=0r+!$t+jK0bBo52!gvRR^aAyA;zg`jt@!r8G0j7U$d&5Cgm> z&~!73rNGU3Q0s+bcU7SJfkL6eV6kOtVO;Gij1H|;aSQ+_0=|NP1LCIHqPvIX-GeC# zUS^t(2)G7h5&OibFa>oj?8w(54gf!^Y^|@7dC*jHym2}HtqUgVXTtOD48mH}5caQo zkC5Ji_=C+aMZ9R=4}aEY$6HrlzFPJ8n*O1AS&@sbm#c$owewXV6Zypq)=w&Qp4rA@8!z_)Z}W~S%+F5tKU zOk}+yV5fK{i^C_FfQdK}BMn4Y#TMQM7?y34If}NcMhvVCJXFMaWQjz(V)y)xiO<68 zDauQ^8mr+Q{fHUsDx+*&q)cJhzJm3Pk*wD4Wo{slO;-pY2`pm=G1hoIwjJlb3WS(R z7QxvxPSt>Lx(-VQ0)yEs>ewMQy2%%%D~ETAE7Xg!b2UtnHcXT$c52fx)ScORDr;AG zAh71^&^^un=7C2!O=6b_>#eK=AlItUU_rj5w@zk&T@GsE+;Ps%Fr7^Fx=7@B^~NTy z2+|IesfB~-+vL0gZtJ{Ms`Y}D$Yx<7@5sQ!>)PN}V4_TY^v$dTZ$Pfpm5B%VI>kmJ zr$GiHBb6dJ-ejU-f_b5y9T8htnoDz~ed7ZuZqbx70)GV;QBRtZ>MX|u{E8I44i?gG zSpErec6p54{Pq?3%k9dQmvw_GG7(`;H2yLdaokuyOvI+Sh)%#+^su2#9&I`l4u|n} z+%79>o|^bKe{<8dN!jh3IdAk__Ua;aHM9Ay`;r>&x=pPIZR4AlXXf#S@slfaB1(Cz z_y5yqe9N#6zb8qM(k>f>;{m->k#^a8FCdsZP72DUjx9XwV39RUKi!pn<#RF4V{cX( z^0+#1HIwdTjn`gCp_FK2$bh<%wdu|WUQe$(e995k#dx~Ju9dGZXXNfUQ{Bj6Q-Cuo64b}MFQN~*=irROn z&G)_~>rCeB7reQBVc<#Ex1HDYIZp~K_IvToo8Lu_jU#|$0(Km)>H zwo*O`r^bI&%MqAAPYmJ#Ux-+n*Bhn^N%l*{I$mg{kFl{H7~L95Q+ zlX`dplUJ0D?5e0M^K1E8v|xzFa*AAX$Mcs*#}7sSTKucWq{aq64nN&3%S*_zOmM3F zY3`Q%^!by;*@AQJ5WMo+!EtrjixijqGf(f-HyzQRZ)D6n_T^=`kIm|aAP34nJ<;@E7`#GFc*QT``+4nwEtikL-w;ic)c;7)iE^@N}N8} zxTUmyX2f{VwXOKK)M6oxB>(hsUJ3*HGNS|*;tJ^_;PJH zZ{xp@9u`b=tAb3AVN7tj{Ku^zc!6-Y2cyMD;2^uS!3!8F015$=%M5IKgK=`!gbv{T~ga{@~YT=bgfB1lqo*r+aG{z`J zS_>0$)D#d+U*1bt3Q^NT5syq;28gS+iC#1M`GmmU=o9FBlX8#LzSGgP2VDZyKG!x} zc^ZNyGp@0veAnPpo>4M%6zNdl8?n@#x958PW7gjCt|?0`#a4!!qcGWWrkjph&&Q~> z*ZbrwZx{EC7TP}N#+8;i3l9VJq@mb+i@5D5Qlm0dAfej&iFsAKL-3&WCnJ>)b>O_w zVrxJ~a=)LtC1vSF8LwOi$7!r;g#iIzVzTL5RX7oc67^`|Wp1rHf`()r=H*t6Zy=>B zu46YLg zW>wi6%V*ka;22$FfP_$pYW)EwYQaBH#G=bbVA)h?r=CB<{g&5BIF;0RJau8dckODK7xi^I zi`pZOMapdx&*BYSn%DgDS(IGuLzPE*VT~_8FUwuoQfYV~)UxxQ>iOdOgCABlw;^xm z1olru?^TFLnf-rWV$$D^rEw+iu+u8VqGbW*B?=>5B1G%M+5$LqzEViPf+B4Wl)~m& zcxOI+OomX8FwmikyZrs}_=hJNs9l8Lza^qa}@uUqw&R2+;2J5xvV z*rNA|$7yP*THc*MFMkY{7-c4)paRC-X+O8-8>B4Ox-%D>G`;2@DcUoHh{ldgJyEN= zkBJqH5KwIg@~3TNju`s3bVrMAkZ9{q;q6ZSx`~P`Nj$PPa)a(NApl4TDb^YVTN>lV z({GKb@dGJ4aySFYBL3SzDmQscyFI)FsTe6!Acx}-0Tx(NyZ{eJ)NZ_h8+#(Fxx$Tq zbLCs(R!*{sK+f5T@VnC7tuQI=(nno(18$CBedX$>DpN+lL@g7reBfhG7UafMemP7( zi0?HH>lTxYjSvaXdF-x-2DO5a73?!YRz_y&-4siL8o*a4Vf29>YX5#!_sxgi&+cfO2!c{T9>;J^Bx`LCVtH0=4fb{yNS%WQMiF%u01_L#|* z3n9{Qu5(TAS_48If=oWX2>&*0G&VIfMc?o3dJc2ykIN4Ye$>2 zInU>=G!Yz-&GEN3b4TFRT#e;TvCL(A9DH4bW)`h@-3GGod0hN^(jSi}d3h0oS>}EX z*L?nMI@XW>`{M8rWS$q7l4JJxEhT~By`cNtIrJwsU*IL*kg(g;L(u~gGkEimRs`hi zGB5g!Ud=5`+L@;s3f3k*=)PtY`0<9Q)#Lp0LC(iMilZ#K9Z}OZXf1r@6ey4 z0rD8wNwom|SYdQO zMapuw(NEZFyU0Vr^f7{>P=};?{8~b)hC@XENJ0R@r*R{%kARm(@ z_svk-brJ9rrjcHui*x^Ozvb^s?@}qh!pY72Xb2?VJLEcMH@k~D<~jTGdyS{4z)kvH zGFDy%5&e)Lb!ri%EB%aU5n+S|Tm!{gD?EsDKzgF?rD4;_LypDnaC3*lkmWrA9ne%K zH3s0W(~5nuhg(WEjkl_hENthJ1$4M}|BJa?eL^%|eJX2RQTW*$CPujLF07s?$^pHS z#1zeaKeH9oDNta%mY!`j=vOfyti9r9-mSFtHj`kt(tkiP&(&d4v~A@SOpIVH26~=w65CZO?npWWR7~qVNP1BaEOYhZx$VJxwABh9%>#3*X3yrTvxVYE z^braxC+Jx9bk36C;-pwVGU~~+T_op|S5+8S>F_0f79~2CEZgwPlb00-s?e@KV*2~T zC1C%-(}ty=FEV~twrAg0`9yzp*Zn-{)b`3|!=pO6oT#M>c`opc@pbH^$wfxrKbM`p zHpm|liTgeQBmtI*^J@NN&-;Sfk3Zi06sR>)k_-rfF@@&s&Ku63vWJVf?x$-Yw%nFA zK0D&;G}7B+D?PriAN2GFzI)95is+m4~;jaK3r_-&a1~b zO&yfMR*Q$yABvLQ9h5F6pC8-W{BAwVON7Dn`ek$t%8D(@_}D*bkqD z2>h=9y}BuCW>bq}e)R)gJM&yhSmGTI)R}2=W*hx#xN^SfL0x{uFt~NJC`4IQAKmxg=3!V6T%xFap$#cv+y&@B@eHk&3a! z;k$<5#fp1lafNZRW=wcEnB*1W^uN+mkiZC(tBk4~;X znpQPv7cbP1t@~3---I6z;0zrGgh%o6tZ54Ju(noDWF|e zX&oU@Yr^3Qpm-=n$GW5n$t*uQ9GHX`5hIu-u|AkV&j@+v-oJo)HpeV?*+GXrKm39D ze|ht=LtMLi4+pnj6!}_JO&?;d1r1?y$7J(42b+!ep%2lR%9%>i37;~*$gkr4Ivvxc z_wx77pWgb^-Wcm&o0F9p&^qojXEPUbiSC~Y<=mm>PLOOW30j+W8=gOL>^u`VQdF>ta(t>=>y|*9{`=g1VNZYkuL)ZD)|)%ePFP(! z^An@P4tAXO)&-Qv6UsC4)lGh}YiASbj{;o(^)_b96`cOToo%gujTXDtbeHLskelc= zebx8pzy2QRk*6-9;7ckV-#q1V?;qEwddK?%8IuRmcA>*aV4cA>iAFEAAwnP*LV4}R zH7`R75E=*rqii>0vF_<&;~ig*LofNyM)F?@wa*fJVt0C{Ywf0v1@5{hB=0bCyGC}= z{)7Kulzq;pIUqCsN8r&Py@oq47W#wqE4&9a%b-X%SW0nvilso+nPZXNc$vx$&x7&B zEjOXQ@Cs=isv2yR*wefObRKBhG=E5>6hYBmrbPOQq;?^&j%Z1038hP)8)zxN16K*0 zhJa2}05=_~b|#7y%>+Egt33edqnK*lQD!!PB2EJ0Aq6Kqf@PI9li3W|Ys}I!P5v5N z9t?XRIVv{moa(Le`4q$GXoJ3S>-7oUPVf#4hy4EL%b?>UZrk#*)U&%(JufCU5| zchQhk1Qx|{m?{lG83Qm(?j{sNnc#hs0eTdS6}1U*g~OX^OnTH@BnQWJP%9kHQ0r+%p6aJ14J>YHy%`iQ{sQ`Ns_5pww zqBsg$gr3Us1teXHNn+!kY4`zL*Dtam1=4_iego2OoL`{Cl`T|DUAM-$%lxKDHMi+t zj}zkHZ$#6GL^e|S zc-YZ^-7D!3zY+niawz{0V|Z1Y1hHTHmcBSj3@QPy)}Gm|H|kc4|2nG74EQ~5+VopH$fHo)O(N;{Wr3P6Y{>SO;Fj^lGuW~P9^ys z4Zz;1aw^ZoA_7wVJ}&*ZQ;+rajMgw+ z)KgR4|0&vPJaX{rtxuh55g8SR|CzstSSqX5J$fZnBwp~~*j!>FqQxI>EOjMW!OCK>u(II;*>Qm}@`2IaM*XMn-J22u|9KRivO zMgdVAS*;bT-M{RAd==eb14uns^lGo{fpqNXS zE`^R(?%M4`FC8uLnvI75SG!qD49Uri1PuYlBuySRPtIh)Y+al95+{*qUn%{LvhE*n5CHb{6fLUkp)WvcDMEEb%n( z*t$X@Z}{S@^L3b5I7rL*dP99&_F(o?30r+j3ClIMYU!y4gjPgyRjdAl>BWym$ZN@v zO?Lyq&%qP>w%5i)a&8@H1+tRM-V5AeOuM#!v)8TQ2IyT~RzEyEW0L#1~MQp2h z>_(pX{KBt2?-O}%fx}Z^-|+Y5!vpc6ukSdn|BL@G{W1T)$^9>8T}BV1%}+_+SALr~ zIT=f#&Yd$=6=eEGG7BnR{7JmaQ2IDOzaWJ1L~SW_q%p1K2Mx8t>&v5n*G+1->$uXT ztb~m#TJ)7`&uK@8I$-#c&5fqgav+QL4 zRo$t-F*8zD-#|YOxZ5jocDQ82RbWqZiokUo7(cCi3gbACqyk?GR+>>>3!PAbPA*Lb zs~aC+mJ{;#x@Y(SMlrAo5f-Wz-oOl=PdB!MZC0Igw&C zC7zr|cd6C@8hM0dJX=xQs)O+?j0ca!%k$ zkDHRx4d}HJVz4(8Wc3{E(NK?Plp-{pT2y%%T&st(ajnR_4|pSi)i0rc^4(x_f-E}0 ze){a%M(^lD2H`jUroDA#Tkze8mL`_Dg?H;u1DpEF{KMr%x=qxSdM3gXWK3$fg#~9S z)9-z9N6Pt(t_c*_O^K7Wa+lh}%g@shE~>``e<%{w`@(QKzVWm4#Xq(nyKbd)pA#1; z#Dyn!BGO-NwECZUHx7E`=UKkh^vw4*PJ((_yYih2;2d)=nyaDqS-ih14>qW{Xc&M} zBB@}50Z=py1WXg=leTm-$ff$vNCom}pF8oBhYiM?$7#YL3JTJ0R~aeOJLv3tNt9SF zLiE3KHKE?vSdMHPkJd9qv+0LHETm5E#MR%<49RVyY}=+VkMg`cHuoaOb*N}NX+|hJ zKzd+yfVE%n)5P(Hg>(AC3%T{3I~f+5fs>vuzS~AA;2#hm0#Qd?lC`+aCpg`q=;FDW zC6@738%gnBh;V7Z%(I2y1Y=%ei?MNdG%*B(LMex(nkCGldz)S%a!5rYfPagK0gQU! z7Z|P198xg=3wse>H6T1L#ew5fa0USiND(WJCvU#voz^zpvpj>Pnryfml@vHbPYs_{#5`?7!C`{usW~4?UVr@8{IzUJ~ zM`PYYp_Fo+F9mG#(ru;*@}!{^zr%v&Ax>U3&Op0$q4~~_r03|#4d}&LBsY-%s0!RB zst`JoJ6^|7n-MA|RVs~R(9Aht$+p@7M-GW;h@6gYJ%?4)vBH9Yr|}y*dgk4Oc$M_& z>$#?CkBF(RXcTaI1G3~^F}jZW9$00NQ2Ik4ra+b#qH302Nd)RMIDq)32Z*@tg3gBk zj7X=sJ%QJJ(BYPUK0DXb2YHVMf6LZOihYl|;#!r-aq*_bQTWqGE9N;sdV0?%kxZR% z`t25z3d{|rsxeS|cIAF3QiSnCyguU3=5OPMVhJs~L!MK=GjaS`p6zOJO+$rx=-u4v zG^jf+o$j4#KBoO@0Hs;6MlFGj^?=~{*=%4Y^qiN+4+XE-*_WG!Z%^k;%bBlAC$Qw{vf!TA7Er*7s5x!qBPAxWU6q~Mp9DUx9S?Ma% zi@}?TlE0hJZrQVoYSZuc#(35+KzxTfn))FJpqXYxHlp*EV~H21uO7aw8Op@*UQo{h zb&XwRW7{je@$s{}p64%uf|)rgTJZc?T|JMhjBE#(<>DrruMH?=IvM$j|LJcztZ?3( z*Az7gQ`z6~Mxt+h+I)4$<}80LdYn-4r#<(LqZG*E@n>3BLL&AX*kE37VST~L>!p3P zFnLGsKsHK4NR^!y{Ede_NIEP0I{UjElZDIIA&~Dmq9I8n3%nSE<#Yvv25LpMjmKh! z2M7sA+VNcSlHx1~4j^Ah9b3d8XC0gOtH`}OTi*miHd2p0T8+}4koEBr5=*GG8Z=B) zH=b#_*!MT2Q@Ib*3RcI&9zPn8iC@Ki5#M|ckbTGZi?#9#v-=H66(S|sYgoW9bX!=* zJFqakFyHi0kCmnXkVmD_t@LbWp?pac%l9+ybiQ$(1J2H*RLkPlkLjcPN0BDOpE3E{oI&zOz_zprFpch0H)GFI4}0 z^W*%Qjt>%8iSsf3Z%-fd>Yc5)nJnG&lj<@P^()K$J$UdLJYTfm`0Vq}-&dfUnw7e> zbpR$V*C#Wy`Y&T|LK_F#CZ~tKW#bI8xLpvUk z@vZ2Ve_S$!BSwg*$3_YHi+!3hrv3V{fA(B_WY>5DK0#3G$qZgp+xKr!PL%c*Q{L9N z&PLKRgz41p^amazJZ*dQ9)={(6TaiGjwjapz3r>t&i&b8d)};H_NeHmQ*?yO+r5U~ zAaBjGCFR3^qOENpDo`7OXkjoQl~_7NWhD)jxvv-cgaI$#!Fw)-q^fE;RLgo>BvBe$ z#LpO(e;_$B(J?5~VkD;OQx^q-_cFZUgY7$ncDt_9Pz2fgM_e=vwnW;kD!xQ4Ap(Mm zLZMiDfYl-`n5q_a2@xUu?O#Fx|LIfdSPbX% z>UBzub>;t1ROV`%!)nl^bH@t>1GTF&sP&O^6h@s8I1q$zZYByoIrGjm_**5D=?r}g z{SggzpaVPI=TGGS5CM#Efi4)ubQc~B>S{f=`ATzRw{+g)&Z+0rk?+asp@&D4#D3$` zuFOe;>=zcdo5|<;`I*H&2tT`fOZTJhS9ji;x3{UbdcOP%ITslF*{}Thct(=RZV^%c(fKgN63Ww`$6+{FYJ~xuL+c-^u;(jpy^fVI6`? zY;jYZn`KtBRTHt#Xy$6fc~%hpzC*by?cgTcC+9ZHrBSpOMK+4(m%aEF2jPR3icZ<5 z&Zzi(6|L#&yOtBD?spn~y!rQMYPVs*o3FbL)Zp!-VEO8$`oHa#4@Tl%XfA8VL%n!h z{OHaM$qWK9?F8Hg8iqSOo8V8?%%5;S=CjYGNa5>rab2^(vPThsZCVA z(`f}Rq$HMcia@PZF>XR;(7M|QvG3sIfp%J(>QYF}V8MlS4|NhFrRM&14j<|{*B$?B z9PfS(-nscXjp9tOo_ySVNNUKe^1I6m4=q*Vi$1(wE>QGCV)Kcq>EnEL31wcDUi*r( zqk)uSj)5|b`B`QgBGt+ar#ny-bMnjFPoVBv_$P^h0sn|Ljvfx$ntL$9GoCpF}yX8S8XY&BV~zCkQ*;%p=Dm==2Ll894T3BCo)T=X207q8&FVn z1_2WhJR;_#B@GlNWZfoL)CtT#_+l=_ut^Wc1L04zSb#?L5PS%FEn+QRWP=#%b#FbQ zh0uD*9#JtYz|DSV zDZOgcG5~CbJW@yq@*R#I$^NZ?71XAjJ))G6^IOv;PFseYOB&FLtLo5{?W=ObabqHf z3*$s1rHznM^dJxl_<`CPDM0h5ZlIw!oZ}~|(c5b#@YyiVb9saY;P2Uw_wToan2McC zU+HYO*4$5s1u#K#ui}@tMgBd~ao9M`n*_Y=9I2Ok-cP>|6E@^3e=GIjb8zoVm-fqo zO~V|JO;Z@Z!m)c~MZT+1^4}VuZ~zg-wmj7s3*b9`BU!gxc&@P^+}MxgM6uUyNT%=MdJ)7R*AznqzZ)OXH-|s?sx30X|G#A~ZjE+s^|jAB zhUNN=W~OvR7q#@mriVke|56$=Ai=#H>9=PI1a#|YEeehNJU&Sf5IW&|v~%{5r`=H? zA<7|41mL1Oer>`vW$g3F`7AeTwI)9*G1ucMETmk|%*cS1B?YNUTZJ&`3-LkF3htyu zMFZ-;TMO?Osas=A8*C5imYf$y-E1~@S@3;CYHD%sH`4YhFlS<3vR8g4qhIT*sXn@` zB8(~9I7K@8XFG0s zI4m$Hu00%RyQ+ZH%9kQ#R+R25=EV^JtS>$YGT zsUMwX)KwT)VI#8N_cFJVT@*Isa#LK~@Uq!uzB3SFmBr(}f+cvR=Q(hi@ao3v;kpk3u%0B6IBG zvs13fE{q+-(d4x=8ft ze5*atN)6Kd5ZdwquYZdr5?oAmeEs((JqQ^S+zsEOh0vc0!{N7?wa)ztEKw*_cT0xb z*TsI!6svUPr!PldN5Pfu%zPPQlf6IL3g3l`A^p9%7(Xoz|De%mR2^DyGYzE(h^swA z)ChDT1V)-=XA~Ju=^OIm9)$?wSbwJ88BTF(4UGupmv25FPKBFFgsI+7FpAUu%r%PM zMdW#;OwW#Rej*wNly=v9-9X;}ILY!wbd1U4>l6P;d}P?a+C5vw z24@_t9i{KW;8aFgekW^&^&W^s^r?nyo=Dg-nK238V7k)z)=xmBut~F{Lo3+Qb6@UH z>w?w-k)YthkF(q`WgEIn2iKXpx%ZiQ#rvL=C<;iao6z!rq|Ee7=u|~jX8~u zy(JoO|1GlRm=-=Y&`F!tB^R|ha*Ao)yzERGVZAAwt>pN{*+|VU;CcH6Rl|2&S9Ikn zev?LjPW(P!fnPbSu5d4*uU5;i!BW_=Odr#g-~9xW6w<%y|2o#{9{Gdd)F1xEpqYI$ z9=4X(>?^PjyafKWL*h(D{DmLNL4~Gf=i}k%38?AC;LG+?2PWO%g{ZA^6=sD|Um<#V zwR>G==W~NRS>i5L`HNh>+{JxeW}i#cY2!|chIKf-R*wJMx0jjUUwowFHrOo`X=jR; zz^iy*C9cHcD@-3xA?b^6h2*E&K5XgudNjrF!g=X^%LcWmajLb5)1q4bySUlZkAruL z%!l@nhOSoYbJD&oZ&MVM`9b_V&ZCcFJlyV_xD^kNl-dVf@EWvy-`~*xeWkdi*7aAu z0M81dceIi#a>zsE@3U9B*$tDupFf0J3@B=GALs_Z;B4?%@Txm#$1;5ITN%F3?o7=I zZmEd!()t%>aYZS%;})m0tll(au95L7nu(pN#JeUh*PEnvfzx)(Y9r|0nL$8n^mM&O z*v&GbGe&yXoll^K@Bqmz9!f}eozw^8TfGpw*I<{rIjV)JNBOi!7_k+m=UH0 z>y|4)>V)9pkoOO*pR+GA#N|Z}IUOXJdR~XB7kAzF;9nOpij_ceRoqfaC8+gP@Wu`V z(p09+{tX4QAS*;#75|sus7hd;es$XOyOj9Hgm*#YQ^yc@NPziT zD*Y~LsKf?DfSSY{JWU1ff`n2eRMsrQI?90XmhtfAy|o;Bp(1ca6$J@Q2S0;fXyn~U z)1!2%6wmQgi(s_W4HHawD!4J>Wk2EO)UvTkE@_NkLadmy?;H_Zss^D}pl71a{ZEnb zP7KK@{YYnudUs9>KXM@>xr%&^Myp|tv9uN^#P#SYnBmj{jWT35w@9iy!#_K;K}(1bTI~3 z!z*NT?>w$~ z3YT{0{o3)B4KEwZViWp70%O@TA7SwrbNS7tA$z;RK0B{xyP`w&y_FvK)X*sIGVIQO z%0`p1ZgcD*V~?0^eq6{2_mc8W%?*J@$H>2rTX1Iolu3TyGGj%)&vron7smT7pd; z7X)rY`k)^YpYg?iRZbZFu(g*|J15^GGn5wg;O&IUP4yOOV@}XjT|5EW^7DKWW~xf# zbC}tl(=_dwt5LS~?2?=qC8bhh;{w2tn|4&Nck2sl(<++x{T{U^x0JWlLR(%c&foH_ zQ#h#~n^Z#0vZlExSMYjX%lHtRU#(?!_Czw{rRu@KiXTn(`GWmJiM`p7IVKkYRu83K zm6?OzvbADoY!hJ9%hqh5V<}ku!jh!lfA!g}qV7VbD)Xl5Dpl7j|@<=B{P*9aJtS+mU|`Bv+9!>(A|f zJMw<=F-&haFy}mn6u8vxoP>pj0HvW#$A8-Wg}Q+IvJgOubQ?o2#mvH| z`R?M{{^mv8U1u;$N~na4KO5)g%Kn8m0~=s~33N`G7STj_6MVR)A+oZ#qGco-3FO(y zN(f6$`4Zw)KfvyLBUI!Vdq{_FCeHrC^LrDdSh!?hyA}}V2$pRWI%QxoQx-mle zKZ?#hn(6!zer`qYg&*$}iJ)h6Vlh0L9z!NRTMS<*RS%H=2VMB?7<6wIp zMRDPm*D@#QTNz-Atk9=f*g`U#L;dZ$GL}AX;+WG)y{G;XMlbq`7G`*|hGIwKR9^UZ zv<6tFK!NwH)gcuZ`|>jiZxSLQC;7TFo&$%0+b9wTuC?BNiV582%`O{%9s-NSPC2ylSb9H6Jh4Rrd%E*95 ze1St@V03Ifu69*(Q(as3aZ%d#voF!F|13+s%)3B0<2^2H(0nZzKIkjfFqWyoBwFdQ!~ z&gFI&fZD%klG8yg#2O0crw4>d42}VW-AVy}F<`@x0{6_koYM@AIRJTfcdDIOV1ovcd(r z#usUe!J%iyq#69Z>#rlE_EM%B_6kO#Fn9L>q7LwABOpk|r}3UohDCLAKXq!5bKhj= zD*GIswmJqqa8fUWOCQY)R^{{d$@oznkFIH%{uJd6xZ_~`Vy9v`RAa63#(v@zTl+uc>N7je5Uy6T8^bYBg5@^1O2s&2@u4H~rmre0&1dpQD%u zYud#n#hIekJ>#?hvHAnh+KFDRv6ZBum0NwcWy|lC`^_O)_j{_mW#~lf#X|_{79W<& zmjRadV1K<;Bnbmv#2cmg)#|0Hu!^jykf!3%2De=ORk5SyONLEX-I&pIx8f9r1ic^# znv_KJ0HrC>Lqv~4flSwoD?DI#g%+7^^)`kAivR^!n-O8&R=mT<#-}<}4A|e6 zlXv{+*=YV0Il*16`2W)SF-)PK6a^2@xgf;V6G7%4$|8|~sm7Rl?Ij{UBaAGq_QzE) zqaqoY*?=2b>PRTmKIoil(mPg13^fu>W$kao05wf!%LwtwFey^gl@?%%g&7lV1$GM} zc4jm~&dFS#W#!miV#@yopX%5|Snc_}$$g$e@;GzkKwxt^?OZJ814Z@lV5!2$EHgW$ z0sG`53DI5Hnngh=PKI~<#a;3{rb{Cv={O3{tM-G7dsz4 z5;#<0(;|6=j5!FE+6M9JpV}K5xMn7?)L}Vw%W=lCEM-o zwUIrsoyVJ+nemj9KgCeNZNb6kD36B*s&>ESKW$3UtxT9I+qtM}dZlqP7kc5hUH1DC zX`0}X{og(x`i@m_N^TAOtXggO@Zb#5BEg%f(l+@D4&#W&=;)ZlmKbq&MOX!&v(P!y zVd7LC_gO9U>9USR&Gj}RxfcLL=Z97cDzl>ERev}!4VyZ&ZXG#t5_&W?HZJQ0{93@3 zAy3~rnuHzk!^8TtD{Tq`VOZj{kerQ&nX}P^N0jl^{GfrK}Mg_()zXKz~NZvkeFh1-X7vJgd z%LLbvZe5=y5D0P0r(XiqH6~yH8&Z|6op)-DJ6KM%aAE?5HsO;4SSz)oVp-)0?LtHhH8DxX z?ofqTHeOYfNPyZq(Ed)AwX{lQh*d$rXir5l4zf<`Mbroh(HzH+bL=oIf~$kAhgPa{ zZ82PJG9wWY8O}7KJ)f~-AcVEQL*ELcixpvdlVM~@k~i==_%yXTJiD`mHYk%LCF9xN zeQ)B_K{nCoR^ren;S)JNL*w_g*=z(7{qjO(LrmK9oFf_1NkbnogvDa)xbRmw5Nc4G%u zi7eE+tSdh=_TY&dae@+(Hvmype>kIS>X}J?PHo1}A@8$ds7I<~!B=kvL<5gKo>R1m zfQ?3fYJ6TSBHbZ*fcTvE^by|uv1%4Y^|7qUUxP!8D8Z*{p5Oqq8)hd&KbCgLGF5Z@ zTt^IeEJGZ#vYSL2p7oZ*`}u(qPP7OE98QR8>+ z+#%x8D5vcLkJa+q5+ASTNqj_IV|#vAh%JLox>{aH!F7YNWCpYyvmzU-Z;}v z=#`G-QfoDwxPxU2ZS9<#I5Fw5^j(;l8*B3CP}70L?a$t%`&BVi_Bh+hAwwGXumK=9 zlvw)f4^5L)TGGy@U74R0^}ziCUtT+vIx-QF$U$JV$0-?*U`#x4=U9aKMnP<0(Z@1BVeHfCj# zi0Y7?BFVGUgZ!k}(hAkz*M{$D$gR&xs@Pbj$lq}(2kBy^ET%lYh*qIqYTjyr+epxe zGl?BcFfnJ*O97skWd&K8qX?TbTAIbl3r^l77xIYq9HT$iFezrhwMWNS++Om2hPHx+OPP{8+iNUG2;r%`bpNd>V?O^b%x z;yAC;R;Mv7FM9prh_*#@_7Jf@yxQ-aX~VTEFoK)z43jS<_l$w*)SlmrN};ZQ!+OTy z-*5G|_>fxl?FYjft!%2|&#>B$v>TcYXi$6ATOCDXp=J?h@Vwt6?Tjsvpm=kMUVxos8vk02zhNE@GT

3+ z*7w%L@gqoMqNvH54#~mdwJ%%U0K`)<73WvBHM5MXZgol-NBvTz*Nm&hnf;-)-fOUm zQzu&>EbBnVrc0RH4FZ^#D#k zXIL^572E2#T-%Dzu5_XQ*0h)zXmPv=OMa^g#r;AtY|q#Syh>_*O^_|(=NDmOKOrgE zz^e+RJ5QEJ&aCiJ-)518ZUj{b6QO_61GkNDiBnc-jg85tfxx8|8zKT6NmFRpqL6U2 zPt%VYswxHvYSpPm%nk!R;d4Aw)$wzN=L84`4bSb9h5?_4Yevp>avMk=xf*nB zEKpeCxATGqJXBBC(jWSJ;wFpxeiU(>BEO@ zO@8O{tY^^X*9E$&D|@&8ae7!u*4gMHujg#Wxr0whf-c&{r_qv;S}wtp@c_o5%_tE> zI{>znqG?BK?BfP_n+GNgo zF;UczRf1hH-OV4bAtyks~e9p9k0Z-8`15AF-Suja3QDS>+5jtsK`#%3!}0m4Q$ zMzT_npMX?Zxj!moX#_YvjC~Gd1r$j-apbJc`vJ28RftB5w#G8Ov2a0}4@FAA`y+GC zFfN-N$wo`iNbpE2q7gKz=Uvc++-+tCbf*rbkgS^e0Jb=HTjITm+xy3d-bd~uxf@G? z2Nb)!XR6c+R~7Js0G1a?Ne(zM6|sZ=QiRYl>D|c6phoRTSu5d34;Hd9n9XjUxgkOH ziGJkD2^~#bz?WT5i~jF(m&e{ONa&Q>#)Rs`|0>@}F#Kk-y7Q&eacvD5Ax9J>8h=_o znG|}}bW8HNSsno{tZJMWUwq7z_Bh~B=c)$bM1pp@AJpJbbG4bjymX>=b5mNX^67G# zno;SGorV@a%zyJef-`H!bV%AoBW`8d6t#$ z9B8Z3cdgF~;031ByVatUpZ^wGj96tGVITb4HX=-JUyYiV-irv{r%;lkL5VMd5lWg*J;d9`Fg^IV=b;eG*7+}5#mlnl=TDPeiH z+99IXM!IR<>R<|#Kr&rwzjnZhX~bLDHG;xztx`SypZV8*Am-HMW-aFo&++bpnF{S< zEM^hUYkp(v{wXYej4}_>ko)WG2S3y`f%Y2^_Sc+UmG&ZzSo{08@kZoay{C}Z+1^dY zUlu>yxbl7O!`^t{g~{0{CT8uw{5Ix0^7XdxHrGB8mc2?T7{7Wn(dr90yx~yIWX{v; zBc#(hr$NIroJJ<6*J5M?()sv&5?kc00Nj}*0+8!Od20~eYDCsnbfJTnn6plF$TD;L1VNrl5Iik`jaH)_Qi%t{bua zi)gsYnU@kmK!j^aME&9176XisK#CBC3Sl>kAlZ#<)*X?*L-SStutoRDuEK{o(Y#|m zUrlhyDsl3lEH~z7d4l+B*kt_wyGnuC2wkHBV}Oyisyq?_n*F%?5{jyI)AG}BPE7e# zalEOTM;t1)&nlJ)u5~Hm9so2GXvoa;EXVnm6CHAfEF^$YCE1Kd-?WUWW_Gk~S7^w# z#%`!3{Zwx;VDJFOL^oKwQ@e%TvxRc>bk71);sF{NON}D-AcopV#^fTG0Mh_PDX@y4 zrWnIpbf1VYaUf;qLOQG{QpW3`5XzmHFsbH&!#A|0ZCXF<3(F114rDdw3-_H*d2Ib6 zY=c(v^|Jmd!};}h&mHwYM~#7s|7V#{=PaRI223;_REs#n`5F(nltR)H`>h?Y`i1*eoeceNUw!F7-=)9? zGw&r^erGL{@lLeQQ#1Y0Z$E$D|3FGG{eH8fbs)}UUHY1h2=mw5xr5tj`vuMyw4a13 zh`xQ;`mp$-d~RZ5q>fESl7#@ahbsJnsIH+9o80PgN!BZGl)f-Puucxy_tBT$b?L2+ zsg9|99+>^VqN+_P=UM7#=0&wt_bkT>wVLq|XETjWO3GNqlRZ2E^Q6w>C29CyaAK8` zR#F>4M0nOg(G}I7qSP8G$c$SYvw-tsPtI#t=F4we!vaI4)}MH9yq(msmC6>PxkgDk z?oS)6OpEO&Sb4--8>JKt{CD&EgbCQCvhS%_KXL$Py__&ynomz^&gWRNQs{PCcDQ%7 z^x|?{Fb|>-PQx0*^hPJcrqc!inp4PXx<%c{w{Z?>mEU|+RRU~7+~eUedNb(yz7|g( zf-3cc8-$ePGZ`pe6VNfu`)Yv)?^-|6&8BV0sF;(I%#{B=?$Y<6=^db){Dn4`T4#E! zS);W8dqxcxTbz2{u?3}QK1-0d18Y8HRJ{+=7zXn%0 zwXuK8EqBaI2uBhSsQG{(6;83^Joh<}k?0I|XYO*Fv1S+g3D@a( zHN1MiLne-31*-UofRhOl*s~r5xMcH+uioFA=V6$fA2RWd)%jC_YyNN>umIs7w5*Z`26#XP*Oi z-lSxXaDqx{tU~b#&(~ibMZvG8;=U-ahxg?cwYG};WgJ%Adel~r2z^|$a>^(uE!$z0 zd*6c{+m5YZu|J^bhCljbe5XxzWKQX#aZu$#WZ&WzV_yr#`jwjLx@i_*-rPc1y*hX5 zPUm$djfBC9PJT7T>weZ8Lz_5N?xSC9q0w=dke19?tcgfemimUMz}lnp19Fqtxg8-r z{@ATyzb`LGQ{H~txW~Lbm{Y-Wp|JQ@5fhiFc3qHHj!U#&qyK*Lt>w*YqXoi zUFUEGH&nnP+Cl{&NtuR2)wUtub;@V097w?cnP%EgcP zv|`S(ZkRiFCVHA?2p_C)ggX#J;A)*~WK}umiYIv&l`sD1?oy0&b|&W4>-x-A{U*U& z$X41c|8mMgp53+LfHjMEuN`Q=nI^HNtF{5j;vzpgfFqg$W*Rm_yH`<##rhtnUTq2* zjDRuR9^h>IYD@c}_D&TlWwIic5KF-WXL625S9hA{KI4jNRF1*xCF2NDphOk;Ln>!f z*Kf)_dXUymwJ1o6ZM#cW8rC8v>{30e_3EVgL^$z?1xbDp3f@(~welgW)#5L3k-v48 z!Ead0bD}QEG~?M(J2W2}xzbgL9%Th8a~hFIB%2+%%ah>T2O?x*RxzK@FmUZ$80r&Y z$BcudG>NIwux6^F_#(eV4+nfV4&alNGa|s%V#7)}+Er}lkc8PgmsTfrrqKdHR*4`t zXP}q~e70_-V4;8ub{|WvHZJdTKs1$aN6%Y4y_V~FJ;QSCQPhyCL&d_&VdHH%x9d)St4RACD;_z! zK#s{RyMEDo^%%1Mz?6)YB=t_Ve9YhX`>g-urj$Mrnpyk7*Qzu%uv0j6Z#&=DHoujB zBzNF_3JrjLNDFmf_F1%Z|bA8{3e zObTg_|67?vKq#!Iab82<3D{BEN(=Zqhz zynHcO>+Bb+>vqmU8*vZyrSFN*v!1iG4z>ccqsa}!xHwb!3WK}N9N+(2sPP>@H+jKo zh2=#ESg|o(T^L9qC^^qafkw#VRjtuyGh<5u3riI{XHF=Kh^Wd72TD;jGxF}d@>oF+c2pG zCE%JN>KabL7I;XSv(9%Y=PI8`4R%d^to9SsO*fB+m-Exl+^i|Vmfn)25EZt!EjsPARJGYL;+neszX{8k`tLD237)@+tlR1S-v zi+u^+%8`uUSyB*P??9{mIMr7dL1-2SwKJHMlS~EIyE5Rx$2kG4{~-~G3p`2BOp-s5 zb$4XBH~#zJgUdcjEjlR0Y^pzQM#Xm4Xex;BnRkUzbW2Xz*PO>ww?344=8rj`Rawuk zn)t)yC4-{jsx3Of-yKSUJ@U*Zn*ntZFSYP1b$A&Mi-ciz-rT&ByDfoSv2YiVKegcN z6lGc-c6#d4CDrdKM)i&lUWO129}L?gJFVg*&cyhR@yVfLh`mQmLCU^t^;DqdT_VT} zs@?Gstq7roDRT?tCTe2Fw@B#wO2gJq@6k41zgPX}wlcNlj#z{J9>nym%i3<==5vtX z<{?Oh)%q{s{N{Tus(-7Dn%k_q_(06z1Xm>gf^oaXL(0qZB|jSk(#$JWVI?XroT@dw zD*EAVB1>`G-g=lS#`Kk$IlUpl48eievST|t--_>ej<$fD&c&pG;_q_1UrhZN_lmHN3Fc)Yt`<`bM2M342#>gv@W;igQTUa%-hh zy-WLALkczoNB8dnk2;VZ(DF$3pBf}s6UthFM@S505-rk_42~OJ42n1OkG}(}Jn0f=2~+)hkHzuY01z;Jfaw!1 zKys1HA}XF;_nbtaqpfy_$FoE@7y(?2N?GH=4Rt!+EZnJ5V%SKdc<558qVmof+U%El zOD1l@@ckz2PfzE}WDPw!hfbND0l&zH68jLbbn{E3>oe08wX`RC6ry%m}Gsx2bO zrQ@=Z_}{BFj|YTK%Ol3E_UD2F;$y(B{MV3k_;}(96{Jf^Y6)*3$Y&{j_w)b3P|eMxw9Xs9@caI+TGS*N1OvqUPtWZA^Ck-DlK zKKM%Z(aJXZwM0S9=H&ZG$=^{ifDA_Q|1~>iE`%(`+T(L()V#_Gvh-_-8=i4#N(Z#9 zx`CLWH`LUWn9Z}}rH7~q&r!zXgQO_}kYf*fcN4|uAH3rw-`zOq5TtP_snD)&bKB6N z)4Z1OPgHtg`A0h2z0{-=;60itn-a$d1o_k_4rBg?kXr)lHC zgcwg#_=r2nHPyF~J;^nx-HpM3a3!n*{3W3CY#LxQ_od2paOF6n%x1=Wiq z+cpJqltYYj(wif!S*XsMO|J?)aIig=9O+_2Xw!-v0S|UlV5$ zqf$TOn&m=N07NyeiSbG5Bm@CorEGLnR8YqxB(hHp=ZC@(6QYWDq}0DB9E=-qYC3Qr zgLak{7>|h6g%6S(kAS}GM2K%Uq>P+X0; z69-y=V`f&Ml0Z}4Eh)JN+kP724%wDw4IZ(@If`Z`pv|sbPn;P4Sk~I``=5(3S`JFP zsxs3WFx7mZtzNxpd(xu1Uiv0q+RQ-gOM6vlEK`6$9cSmEBf!WPWeK()fX}aA#1At* zjF`U6&4eZ<{*;u2Q~3B?LN3e)>E-@{3BXMRAj)p%!ySA?{GLLBvW;>(`yXwpub0&< zHu?dKdDCdvi>Mwh*FS#v-0Ihz3|Q8)!~4|gE@!<&!3DbxX@|iAh~t)1L$!D2mS*L$ zI(my}0oVDH_%MR<{-TfbZ>5;~&GD=*`ss}#8X;>7Sy=b)k&%BLrCi=WivMa=$$Pzv zZ`kVnlX;i7Xx_V$!_LHGeZ6*eX9i=dj&kt|OPoLA@-q3_mjnF?j`R$TX3F!cR=;`$ z9ya_Il^5Z$592a>CYA8qKdHcR2n}&kX={5Ec)#vmEnwDPWU4T8hFZXKlk`&Vo_EVx z*e4e8B3{8H2BoTeF7c0y7+(tJ{7CU@4f|e~F_?b2YsS77AGVk7qt5o=eNhFa(_Z&N zo-%Ay7U!*ZI1+)~dhHL9hmMMIg+4O*WLnkvWeS z%NDom_s(t01TJ3V{d2`aT{DVn*ymUazIVeuC2a{+0tT49Rqq2x|^lZ8+rCY8L z9yKc!#pm12#pXfRfLIb}reYD05JWWLA(aK#L16L)dCF+Z$pR|Xi368ACugDZ5ulGk z;ZCKyeIM=>jcLHjq}Yz?>8Kd_4S)}x{E7k&b}!cYVZt=tu%x~sKlzSFR&X%`oMdJ8 zG!y9P2Oz*5a2S;ZbL5VK3y`G?Xxse%x(r*Zt?mSii31t)DxXQx57!nX%mgK*2t;{s z7hjE6{_P6R`qt;bqwvH}NHzv6rdXCe)jr?Jr`eI+$P}nEY>N+70Akp-HYPx^ic?{x ztEV*5rdsyTQPAP@rJVt_f=0os?# zLxY|E@9{5tOE?0j1c@a$_h=h=GXB8$=^?F+0jGrDO$V7qq-3i@6;7TE8IOBihjM;Y zUH^?a&W;uEjuccpvo-hi$j+kpxks*A@O{$uaYsbBXSZyHVl-=t?!P4m%lDjx8}t`H z`S1D)DfDA4wg2VUUhawuB>$kxB4cN_`Tpz>mT<_{2|rEQcPTcA&OePWWYv)f7jB5h!+oi*CaNx_Nowc*>brHK^wH6-<($J} ztG3C0mE)7ESGrd{x3dNRU&dD+`<&)G0n^Kkn9ha>dF0qC`Kq%4h8=>g*(YXrGaPtB z;K0NSHinKipi(ym5~Hm=KB>wBVr(E_ffQaC$WLoeC?MDsE+xxzO5)N6nZgd^lgJ*i z_fhYBATURBY`hSU+<-b~2KN1;ME+3Zy;AF^r~(s%vLfi94-o-ijje@WY`72I|=>rB&tr7=IH#6BPn)eHI!4Id?I zNT*b38(nXPp_RDPGMc6)#9b+{2N`0Z6d9fo)rXuEwp^)1?f|kt*`o(T;ucrI7Rmzm ze~*FrvVqOsg_ghXdGFk{184=DSyJAMyxZqa(V(~Jxc-l_6h2ze+f%2qjE)Y?IL?fm zQI3ud%b3=yJ;6F@27{dTK4G7_??{w@x1VNcewl*s;qUi8-1zfW(L*HQa5_sZ{n*#L zo5PCA4{kwT$$a<{cHyHvf7tvi7oRWh$_a(Ol5NVpR~HGr7UR}Wm|sn2VXShz7UlTq zLyyBZo!XL-zfZdD{<^*8+mqCw9)X*Bz46ua>95AI)qq>4_?j=bEWq|hq%+hnejokn zb1wJW50|}kv!x^le_}2c5_Lepxd?kQyXcl%L_Y-ykuxyJ@kkS60Ol%liQ!_V{T?JrACUdVjHbl>GV5o@#BeSa|C|F}rV7!;#dkgBK3?)kP!G$`xjGnBck1Bx{nzusT%ApFd zmr)hIx^T`ukq&M4vJE2IJRiyKDFw(pV`wTV^kkTi*y50|QEpTCpApyWp$v{LiQn8! z+@0i2l70}(6VWc!{ma~s%G1J_&t{Kfcz$O5ONt_wdviAdcnw?t`zUJBnPV6{;UkC? zwwi45FatG1s&-T&HKeyt9e}$8NF+D~?@qFNL;^oMa3FkoXwg}aJy*^UMvjl~VpM@N zk6fPPNf>CjI8SqQeblcG?<8fsI#Hg<(7B>?D(lDJ?T|b9%jzL(Lc?oKFn!jselA%7 zbmXJs`{j=+nI)HvKFt{Bls6$n5>d9Qsn>*>6u#_)u2V5T=B|IpS-f+fzUBDFQ2C(J zkGuKrea*IK>vjxfF6FFTOsF_i9scgP@)Z%(jklVdillGb_rHg$- z6!aYSm|V@P1%|5nhp&q-6)T6?{5<-%=6uNW*cQW}2eD7quWvQnZhVr;B{Oh3Z0@;o zh^ou+$N2z!68K=W1&>lxDUYQmU8~!;)%2bsHTjl8L}brJvbznIi+qSx_(KXpS-Is$ z)L{43jvf>}KU-!N|I5jT*7bd0`|52Ov-+vs(X0>84&E#h=PT4MNj+M7ocB3bRivoZ z(TfVbi`tx-Cl4kg>xvcvRrXOlu=^luSF8`%WzK{oXwCTtopg8`-PG%rUoALMtrq)x zdbvX7m4q3@v*Ja5WnrjwJ*7kbXI0SG{1mt;5g`CjJ>U#n!Jx%>z^TV7v#h!i`cCaK zmEfKRY=uKktkTxU&}$2|tyF?+Y3YYxqSr6jP;nn%6ve2@ew@0Y8FKRh$4})$Zqe^r zRLgBxaw#xq?ul6o_d5O^Q2Jc%bMzosV6}#fUVDe{m~(hqn*vQ}zaAPsF{WPm(#_^I zZK-yp;RISfKh!7qT*19Vybe|tUrUtf4k59P9%(?j5@vwbZJ=1WmKZ#hwMyDjb1U1p z&D9o1aQC26Nmv#c8-WqH?mJ%2;_u+}^^CqJ+<#8r&biH#>R)M46OL0VEYTkb4X%+{ z()WxrFEJ9=8(c|ym(TIdfZ8d&Bg+J_2*5%(nPJBFpj&&zO8>S%nS`1Lc#65z?w$F{ z8C3OKnx>1v;AujIzTM#mC@@!=$5&=@sB5xe6VY>LZ4OL$aOg&5k4GQ4coOQf9je8?uNPB)9Id07r<|>_9#U*1qxUu;1gRsYI=w9~J#{1o>Rl%jz zBIju0!s;pA2tL{JkY=XQZ>Ij2(S{1s4h}T7PpRii3^!K_yXqn)Xf8|Y-+J=SRhF<4 z!S}CkcI^R?4H@00Pcyqdk^YhbW=9i_eqJ8x(_szAce)>0IrQf=7nEFGbvl{PycjBp z)OyZGRI2yp=ab5gR0R4VJlU&SL*A3FsEPArv6!m=SLh2fzQAU(%_c(sm1=rj_Yb_=r_$ZvRFtYBO)Q)w>#^A6j#*MNL`Zg zdEbu7+$PYS#*~CFO4>0=ht=5(2*{FBV^DZhgsk-o&^D;vEJCMV&s%1tzL58m13^_o z`2P3iJrlZlB?=+H@8n>u+y)748A`gO6`~i<{!=xsmxC=b#|n5vz4o2pHk6R}KWE+pl*C?$9YW+j{@foubdxM)=VLfb*YKy*_gfb+y8&-)V;ErU0~; zZVJf-o1*%#1_FuvzqWw@KVTr387ChK5PkWG7IWG{Xl`U4M(6M|lUTd(;qaN1!2l=C{;cd8 zyZgmpUFK)$W!{~cL#9bzF1E#&v-{S4ZjoN7C8AC{iL9L?46Cj+jnji$Zrgj{9x9!n z#?sxG&N9Rb)cQ&FO0+6hq-Kif>C<6#$_M_#@7hL$68Gvxg!IS{1^njsGcKPRmX5vN zde*hb4{p2sV|(`1*6oQ_yWfUMeCN4{R{qY$k^6bUU`^CBxu(uo?fK%e4h{!eB6|+R z8NPsP6GIN6rw3s~d!&*&j~Yo8ru{P-U9BBrSnIULX%a~3M{cv9@IGue9$!1y(b02B zhCkvH^X^`k|L3DQwt)<}^uo1Qwj8y|{Yr0wiwPvn$>by*qQRK7&cm&NMphl%w==6D zql2~WeC7$vK5;)c3g=c@Hd@B}7*2Jf&*peKenK0?SG@MD^3K1Z=Ey;trY6~odrP{* zNM|Xo;lK?O=99<-oCpE<0Z`HB6Sa5^m|n}{scyvuPNR0HYDp4&sI@7v#SqI=$RFv` z^i-4py*$W~sPk}ZoB3?2<)$={uRh%x45p~?3_&z3itX)_2p&LUM$opvnyMZ;$ZAmV zLCeu@A}nK(y@ze_!szA(G_1?PI$da0PV|=aO@Ii+t2EKLIhg-LvRLL;}>~qt26gKJM5c| zDShc#zRUP?geZD;&oJ*=z>CSR|DZ~yON*lh->^up#WeDlE8_L{`^IXkWHz(6-w$Q{ z2>PJ@Rgcz3&~ZA8$|s@+kJuwbnCOr4pZo5pR#rniq~pSlcT(rJL;fXvTjBWpd@%NR zRcr4|2=$my=v-mb)rH}S%@vutv*`wlPwBaV@dD?c51T_%m5G*IlNg?1LyPknG#UJ4 z)f@|pF`Xlfsfywp(ylEM-SRo)6Ow8&_4vQqI4>5f6m-$ftn}lq50*n8F7Q4ato8`^ z448{ZGj8hq)38Oj(T*#5ADSYQ=pf&}v%~#tqSEPl3G9}s_Ut1s@bA{llcg!xaI9WF z-#DMlqwwOzK9#a1So={;!Y|;Ik|;m>RiytJKJrZduLu8nOX zZTrYDqP)Ugci+6i(~8DeNneI;psDZLoHk}=^fwmRT*~$yOK8X)@uVl@<2=8re^pP; zZ^eOSbva@4keoG&N_1<)3K_oVL}!wMzJN*Z!@}N|_I!G%(W0UFmw9+#1SPs3tnwjUG zgh%A0`y?i=Sxw5CW9y^CBMiRM7Y8P8$g}c+0YTMIr8OE$1#X%IZ5H9U2m>w81|DWm z(Ut`uK)x(Lwyq2DxNA62dgKv_AbMI_Cbu#_IPbJw#L>PX>quaH)B2dIxfaK_mGIwu zNrT^3GVMgG`pbN+!Z&N$pU67TDM>J<a>tn%oWwt# z?#wNzaWcu2ni+oXQo{XlU*vv10oTcH$Xd^!_7>U2w>vQ{gOk8Xliup@rvG|hG4$Zl zk7w^LUa5JRdekv(&i9N6S1$L#V||qF)3q<2U1Yg1Ft+()3tSmdf!e^$1BAeTAs^*9 zA*R1Qk;_822dZ?0Ga0p%g+jDSsqNg}_k=$D!WSD04XNI^L{zRa$0Yet@(V`tU{B68 z4TGr~TaKb$&Al_tUu{X3MN1F$rIp}}6Tg=#NqKK1xY8mnzFtTO;rXb!u!q#uC>W)( zCx-JEP8@xjf9b1R;9lqyVRELG|3-cpw7Ak#z5o6~iY10?L4OqS9E**nHH&~1k_eKu zAM7l6Fv&0qf(lrvELtPW3cR;$Xfa8EuLo<%jO}AW9N#-ANn&ASKOcw>r)o|+a>ox# zGWH#h0uvh#GKxlWmn8Xt-{49kkwp@-yn#Zu#msbiVfjPe_c3%w(@->53zyVFRK)Hh z!84CMaE;y%1eS8ZpNvxA#8Z5`0F*F3%En-_h-?Me2i@Ho*DH)Bn%bWBPmg9V%WIuB z0DMX|pg*xfYkS&!VwlRtik#!AS14nmTaVz9dgBXGNd-ygeaL{)gBT{u z)Pc3cAaNfMMZhT{Dt&f?{U2f?90C^ejN42;q~sM4ZpdN zQjI_~?IB7|l<&CE7mvdlWGLuZGlTiPb1OfR?<6xQ(>}bAxDcuM+t18bU*|YxMJRNJ z&uK5;UYe_bXGdmZuQ5b!SjJ^i@Z+xdwUv3Za{FMzS0P4b`Ck7B>)TObx6OVH?l89( zr9K?~$Dg<;+^tzute)dKxKnw_ivK&iOsa`H;a4wR(zSoH>{%Q0_s&SucKOz)-7A8s z??QGDx_cSoxR(rw81m{8QFPt&uDgozC@i{N4aHc6u%IwB0b zad|*|7lGz3w4=dkBpAsjNx!DRkK~$-(9>qg#M^${D!h_@&eaAi&ST@({~;R$HI>K`fbO zED_Is5z@I2CbTA!!Nqi;WZE4(OC#aA!U2HgS#skz_IppHk01QguXV(yxuq;J)~l(6 zWK4>HX=p?ccBO}h$0r&0>)7*!Bo`N=Uist@dFB6{?;d_h>DdWg-$)D(BvRNX{7<*U zt!l-CeXT(OR1bT%{5B#!O(Ed}0c4?Rd|+j^&lsLDl-LC500%NM&SWJ1%27Jv+bk*u zz`A`hIe;UFIdxDX=~mxwGv2E__s(nwyG|Ym@_~p~ZKAIBynS=V<*)m5;ii9Pc)9=n zc7M?+b#=vly`-xGvc8g*@%{9HpKFX)1Skua5nQDP^Rzqv5sD%-|i{3Aepk*pH zk=4uny2@SU$Jhac(8rzeTR}!!{@n@RgKQ)+b;b&$9Vce*eb~NztCmlIepNkXh2(!^3g$49( zn~979@KzMRyqlGyB3c<(L)4RBIL%8l3c)j<;tm#2qGZ=+2g2L155+JC4a9hsX;NUj z?DhheWFM$cVEwKz%>+@XzNS97K(qoDem>eA`7OAbEgg~19)-~NRE2?=5OgDBOSuJ~qNCpNi)<+0q0|=Lj-T7m8!opsJJv`p;z2cqlQe9y40 zS8!Lo;eH|W`$c>%1N+~FJXGkTQLtJCvI-XcIl8~navsad_VTr|S|gwN77O30`uz0w z#09$XvjvU14?WL4Uc;}BnW=WST$rqn8~KkeCMqBRhUT@(bh`|^06cMK&F}0C$o3n< z1%SmG(Et@V87xyJ2I&}Huo{g6?U znT642TiX`@(%GC}>uQFlT1AFGHJ>Q@bc*EXvpD^{<=fMz^E{Exk*6LD3eNaUJ!1|ejW2U_Lh>B%+k;&_RBfU@i&buERcPXY!*#>yjfzzn*>jc zf?o#JuV5?%w1My1QLo7ICEmuqj= zdAkHTGqH6Hqr!3R=KYL9_!7*Y3TYP4$j;b*Clry`l%xncY!rz)6!^5HZyz0J?}1kp z_qfRFvNkm}xw}ALOQ`1LE4oA~l7N8^t`tzMf1^!l`@gWS=0n%4%DvlW#FKV#*NubL zkA$>F<<>b{tT2EUK+!{sAw?MV@yM;1HN&S;8QCK~;aT24 zR^}vHo|cwc_u&!-Cr5Te|8041v?+CFlKAek6FxU%lu2i!ir>{bF)^3dD5#>FT{4^H zFY||-=P#5$`J&bv^?R$vc35*u=C$yq#mA50XbPq*bKr-cYR|8Wv*eKoflF#8jFOw( zXMdgL30iw}qEmD_tcN_-^^dim@1O`*Ybx&{er_gqa7owq*EsUVxx1eApOO>-jw9`| z3yB{_=7*8^=P4qXq+g4NtTcyN6H4*>LhqEPGS|7h7$(_Qt8& z2=$e-Dxm|!Ox+AP476b4x=g(L6B?Z6@EtMlSi2HD@mYMXKf94TB$1)0W}hsnbR)Nl z%p|bnuX3tnx=iJ{?^onc-2PzsP8;c8C$er8<}uW?F7(TF5bJRuJlBlR1Vw?T0T)gp zg-__T7!@-{meSTl#R+c?6nMIoReb(l$Y%X4_W-yeOotByzOCr(=PGnTHo$DoY1H1% z9#Flf2o%}^tL>ksMQrK3SD)oxs}qPy#mLkDJxVZWAS{4XJwlfoNWoE=WH9;=6-oRy zLP$_oU?YU#`nL?AzHA0hH-J;3sm+o9qv$NdntB^BE`Ov)NOzZn0z(*G3ewUbDlr&c zf^;`HT9EGU2BjG=Vsv+dv~*8?E^W5>fF$Xc!snVyN-*9LowwLk2OUgUc zd738E9ODB79SUsmrLdk~`&PJ9IofzF?qVRm^ap(>W+>E@*5m#&gMYtq{z|WZP5T4w zyPE!zz)RU$vJfk4xNgdE)cXPg)#Y>R#z!i9E?%W?9S$NJQ4e7)3}HoJ^nKaP^ll zj|(t7*0v9?(*epl;3&nmU)LZ_V)u-liek67oFNG0*=95idfU<(Bv@=XpLmwuFL*oy zt_+sJbc1~+x0Dn35;1OTTwJkM-_W=*$!l7CMbS~IJ0e^n6E~x3Xj{up+D)qwkhZK{ zWO431#qj!@%n8eiaJvHiHSNH_0Bd@h=gK(+zT2ok^?{18BJzi}(h!-VC~&k%3Fhc? zBDSc9^xLyi)5V5T5u2eK!@7EyQes18r8!(_VY>gH0MMmMMjJy3(EsXn0~f()%}v;o zC)&JViFGviT&3lalk~`qnMT`Q--`1i!7{o}sg{mo{k>64gxlaCFnZzJ6hgth9??d* zkc{`GB>*?0__R#DP*;&m(}n$QQT$*fCrU6_PWr@*+@vc2CpMUs-6YiFM<}F>2AD0^ zfNQ5vP9#a9Kk7i~RIJ}H+ybHL{-fr-fDG*9!T$GH{*X{GXxdsp zp0n3X1Gt4t?C1Al0M)x}#;|Ks&o4sEjwW-sWe!C@#iBpXry*ISTo$Sumt9}6!`87) z2r^HzKM$TVNK_c6q8jG+zhu)JLQDc2r36-H+;m{vsz3TM-s8jAijmRzhCEhu(|P9& z*#r=;B?l~-A&)cF^vvZ zpVWNF+k6PaoL%oaT85U$(sUAiChgMD_Ha8 zd!_AA7|9%MTbqLwK0@QYfnZv87nh_Z78lk`Uq2D8;G3brG8IZ$D<%Gr^IYzkcuuk_ zRiHCf75%;fK0lN?QXOQG{ za(w$s_dCDt{&|HsC@o-KA(t+h;E8LAQr%od6jgjEf2; zaX@zQ>ra%JmZ?m#DDvs@n>OSixz!O}^HHp)RHI&E*y=`DR4I8ka1Yb|8|!%;or4q> zR`2e%b87b)F}M8#SDUKP_fwgXH9AmDg=1ZL?n9vORl-#>H;J+p73?pg53EseM~$xV z0c!<(I3O@n5fwtCFfp=>RYS)#vX-u4YYdl>Nf!GdlAIz=n;!Zi0aR$f&N}U;uOe&q zPG}rU*}^ut%QPFm?(oibA-zbiNHGxBCd2f_NBK0nwh{NTn6pt??b*xb@-bySYR(a= zroKOGqJX%87#~8vb;RQQ`lZ9;pD?d~mb*zGnU-KV%*3Hw24dUv9ODNDnpK<2$&350 zl?O@t$-kGbha^vL>3FLq9cC}7j-RB?Uz^OGKJzX)mE*ZW9Qt#@sPA0mDE~7951Y_e#Qizc?pz6vuN z425yUbw?o%Ba7N%7#rL376w=`NI9SBXachl^&8$p_(Hk`Q#0@iW_th{#LY*5D*S(a z6jcfNzkUk{CBRjd5sEDb4uD8D#P(6jS~zY@r70}FVX=(=PUEe0arY=Sa%?{z7kR<` z+s|d>hRm6+-*uO%j*x=ZDeI1E@&(Y2(}n0aj2Gh0$Mc)^AL+Z(q3<%`XL0_M%ZD5h zz6@jPlQqVAv4wz-$wWT`b-#oMqdl9bKQ}=+9oB&@E%qo~%|0|CLLfyS&155XO1L;- z>tj6)Q5CEv3s%1>p=Lt5L|KAs|60&H4DNyLa33wy2u%5=OI zD)2k(*Bt)>Rf<0+h+pG5XEsj`+P-;nN=nQMSg37l)1)=Yn`b2M*V-pn!lJkP#QXYU zquRozUku-8T`BBEmKfA7jqDZ-(r4MJX$Z@i! zQ4H0Y$*aAe=OEobF@DKLF}*J!A_a~>2C3Z?!^B_D+n2mXq+%K@9Ee7JXH9xMoOK5N ze4t-LQ30=COrqlA=RyMbtvRgLN*FvZYY70R_Ip{9$(tvJ*u!HK6(QDK6NlO&{Ep(i zDfu4cFgO%5k?Ozt=K|W@s9)^{DLlcYms@k;zhxPu+DCt{-!_8r=Kk7b3QlI0iXV(m zQEU@2s|hvmbR!l};up0AzuesA`G5Y?m1QZ*g8FOrJ z|0y@S4F&@x76I0g!;w*nLsvOhSt??a<$#Djm?tgZlBA?yn}ouEE88TPk)937;6L(A z4miJo2e92-f&sG!FckV-V}?9-eRmI>Rz?$I7q&*U$zgV6$7QYwF@YnjWptGNcWmES z?_tcoo?XSIHNSxXf1hMVbkV4=Rcqw3Df_nhXMpIne09lVk)%DKDfAG=I;hyclNau8~Rs8?5BK!nHYQU$JP$P)>a z@Zy+&TAUd=uon*_$^xy4iJHWAg#ddDeV$h&1M(;y&SL)%Y^LiUm|`X|@B3`Hkd<1* zntPb7u3f1=4a2k%x~j(5&Au=Jq8V=fGAtqi!VF&)*3m+`~ zB4hQNGaeW#rjT&mYB)Qz?sV&^_A{t^N;WB1x0E> zm~X{YEj{Ft1+P`FKIsg--VIpMT{XUD(Bnrkdj|e47E;w;jC!K(N@*+}ipbEI7HHna z6U0Arh>>Ag_^|&?=%Pr$w#sSyYLO|8cS)7;J?DoUY2XXVBu$tAozSn!)+2`Q8A{3L z+HV6@l(@)HfJ7G?HUP@=2ja=xgq>xXgnlqj zfGJC_97D223viwCVfy5jf&jgiV9MxTV$ipWXCNx>#kQ6FQy z3*`F7dQLF8rNFKkh9|EHMviKOQ495<#j9gB3sm`w5nk>qoUhz-Y17*4YLeRTV%^O?IQC_T@Lp!_ndVlaxB*TB1XERg$=!w+`&? z8!%ZkTk4@gI983p`N!1+Cm`7*;}l7*wt1<>>VTH-!$okQi5(iP? z=bfsPi*-Fg2i3XeIxJ2J)$BcTIcD>oS?MUKg zAQ|_3#ihjnA7aZBc3%`o$YT(n5W!a$`jk1J_=_UHXXL{a47G%~c}|rZf+nj3=6n1O z<|gVImB@pHwH;@OT2Sz3EL-zwtO16U0C5W+q?-htkjWqLVNH;vPANZJ0A3YeaPS}s z^58}X#`dtlTL0}XeO@lcv>lAZn#Mve%At~%?X?QJvwjqa%ECFBd&C?7d}X=ok_`{8 zH5AF9vD_L2*s^p=m!krjOUY;}0_=R;577k7>h-4V9%fM51*nRdZffMDijD?OI_+ms z7f}RtZR}*Oqk9M2WFxStaF1{1t}EI5<$EZXGFdm^Mos|hb6t^J=lqGqrToh=^;Yac ziuzA+g&B6K*8G~LrMad`@im&JP(NZnZN>N&Lk}SnttFEPr!jS9Kt}kCPZe@@);1xFEwy~dh@ihx zf94km`H^m6luTi`^qBGCA=Ibbysw8_Q{@Q_?jKTq37&~IlyF%0u+rcE_4HQymDIW9 zGdR-VuyNZJxT|K^p%s`Vt~b%D6i#G8+ev(L@i~}XXra}eSPVZ>>CLOYEYofD4fhwP zM5{zZ7`A{PNW_jw1p@T(*ns1jOxHXcEVjJQWu2fUVf3G)Kb8feVy16EF>Dxa*;@-@RNAr!hFELah z0e@``c>KDFSZgKC;W~<@tvmQhtfybU=?m8E`z4M1N5X9RESZVj--Ay8zJu=%MjtX~ zQ0o5eckYO`+Yn=@RHGBv$E6B=!3}`^%Pl!n(K02CZ^#XsU?4LowqSmJH?Jl+(f>=j zXrTrxEtaYxV#{2r$&)PPl9y5PiR|+4;EFoY*eZbCu~bV5Do2b$Mp*BCf~1D6U?sv)*_of@cfNw=pXD57MKj`i{a%vHv|&G3*w8!yH-TDE544F z>{x?xZ7BhZuA5LBL;9y*D<{C~2$vLNo$&I``S<6Tw4XkIpW2Olk@a4Q;cKQH*OP|< z&zd-&y?vP(=|!<0QV3%=Qy1trevi7}VPHr6A_)W{QD&n5XkGdzr$nB}gVbV0PDv6( znAF57fgbNMA5?i1lpDZ9h5liu{IuTu#}xRv2s)m+uNHoKwq^B)l@3y#x(Lg9P5h`T zlD8eX(}FrZTWvZgm$VXW3dX(uvr*N%{077ij-!9KxAO{r%1g34sMOVTHdz~)d~#(k zq)}qBTfz`g z;G>7n$2UUgiUN-An-Cx}1%#(jgQ6yYcCk*`(q#0uZ9nn|(moGu7o18P|UFVl2E#~@%RRzwA z8n-9YzoK|_^q=;DuKT)P#AmYFj$>KDiD%P+eNO@>PAtxPv(FDQPEICSAcZy7>&IBTINUS6vhgFdhK zD_2Y;pU0}89W-c-rgYsni4;a5-8RG<(`mK%P}h#bpp8`GKX|o~#lkwzv!%GPT0-y% z^EjD8tNV`Zm;Nv9Cl zBBI$@AY!jJU*-#zqr~Tv=^h$^S&-EreS;ttW+c&yOzaRRjPR{FyxRDYm)z6d?cwAj zWq~|>TVwb8mXfr<)aWt0D$V8_aKkLP$FiIJ`VX=`X`$tk@=Z3l&a2 z4?8Yjr1QsX?*$E4+m50W7Fh%{AdH=W#C>cEv6;kqUDy1au^FTpcuUrBr=7Y!>Si3! zddcT$uqUzhnMP(K1S6V(hwz8b_h*nnRJ`)u21n|Cl&8Oq_+5d2ri-En|bs+b6mJhYY<*(3)B$MgTyspeS%P-sBRL!M;V$!BIsx#g#E_i4XU zW_yPw>z@rspus_nqln_UmKQG$C|}-|DE_K<2PeqxkA)+zcg~-AuLvtJX!-B*`40F5 zXVezv2;1D&^K;<%z1&TNf()=)Q`C2FUT>tjk4VVeD}IW!Nbg@QtbD_Xi;H!k%F8Q< zPQfG(f9tEP$P=oi0P2}@{uezSxC#G5FR`@6{s!+I4zJ)}o<9QAoG)LeJ-Lf}@PZ$q zaxdeyttteJVqSxjLkF{2d+6g@@EH~u+PzmeC+LQ_;l&;^a@g)zK?1F&yn6oeWN~B`N1!=G6 zJ)A+OUN#>AsD(EMZ2AucasgSD7MNt))u+H24EY?w(QND`FAX^!dF>Wx#;8)o zw*%Rr!j=JZo-%}vfU)yOfc;;4^sOj)!AX7tY5;X7SY^LT1MrECZ9^jTw*^Rb=$o8E zqeN)ZqfBd;jtVHv-uklSOpt)WGCS80=mdul)bT0g>&(#v01V=RD;{LsHd;M>t(E+* zSv%hE((h$asUOa7wf+(z{hdR-ZNU{`cuYB<0Gl>J$+=plBLSid95T4cy(UXH^M9>R zV)$XS{2ytxIKro|fe{ z83&?-zs-W32kO2c!u6)*)FynA77%12Aj^g0792q>abN!v#0&gWTrm__E-T<|U9(U2 z-J?W>|Ke?R%Jod|62&|9!kK1&=r)B(eoa$7zF$~6nyzUA4NclNkX&sg& zw$Lh#Jj!>%zR$dPsv?-3D81}~uHQ3MaM*ph979g2g#|$gM{GajDKw9l)rxWqA7ou^ zKU+W}M9!LT+XtMqP0L+t0!tMIOG_gF*R|Kkr1(K$On|1`fLwCVD19<%|1R>_Zl0Pq zS`}(h*%f)mW)=rcb5-cp+h6PxGBZv{j1b&GdNHf7mXj#)B!r@(v#I*}7+v_fNrL@d zZ5f-3lhkRU@%5AHGnGP$agh~?;n38t7IY$*<}j-TP`9GF6Rt(H|h^YNn;N=t%5g2N;s+n;dqr=zxpXwq6Eqs~Y zo2?L3Z*3%>p3J;#pHp+da2+HNR(n$610>%&d|6pVQt8`DBZHV~{7z$?moaQOu{CfwbVe_(Vk zzy2vb5z1O+c4$*1GEostyI9Ih$t?rxW;8Lgu&jy*`j%wWwlHF(U^4pI(_w3wL3nQa zm)NyJ(PXNJcSkRO#9CwJ_8voJ@89dQIh6q8+x*|&?Q|Ht^1extKQ#H~g!65VrMA~w zSrC`6nM8r!R$ax1eI%03}pU6{ZF4Ghc4pJ%2ihG}b7qZL2ji zs{-9TW&P!kafK*bmmLB=xuTD;#p=L(-w~2A3$i`m{N2t+CUEqES^UEw)n2Fv{}1rL zK512Ug3nY@e0$g?$&P++dc+WQ8{l@J7%KTPOf4+_4`5b+3IFh2$Gog}oR8${B_m-5 z$cY9llv3%4A~gcgXd)%5JF{uSfxf8s#0Rt8ZRLT5b#-xD{BM-s%HXuShhaKf<89d> zZcmgoD`K#NES*7K*LU5+^;qP^B_7)|b-e6dnQ1G{=R^pjbx{R$;-l15v$!?#jQ!-` z=lnGI;-x{#??&8f7^ouT{jU1$G3A(9o{LN;Q8dr+N5~FHH!3go^xW^tlI-PGJSE2X zwJDxFl9DwiT6Z+~wTJjn*bz8f-4#7bxP7?fxhJP6fx)ov(kKawyj&TMK$0~f;&teC zOmzgZGSOn0trAGQLKvH#QUZ;DB*%sUjqQ=O(oK4_>H({1uYJw$`dTZv%R!>8HwG04 zG$Aa~E`Tb>Sf}a~69+zKs%h z5-PZ~Rc!tBp||p2zo#T)$6;4*?32UGkknBzM4yYvqZYaM&%&P=sK?rrm*Q+!v@`=_ zGKr}r>+XL!8TJIUmHXk5H(ch{{}>`FrOs9zy`GF$W}E5iDU?v1kVl*S8&N_$=KoM1 z`(UcQ?*DJi@ePXAkkF<8tVV?wh51%GnmW41z?~HsI9x;I5v>8s=ISZtbWH!SnqAPF zDdk`=7~T1mcaV|M7MupFkS{6hEfwim@aV@u;<<^q7h^=q*;3N;<8|2AgYnk+J2i`B zMh;MN@t2(JL|s5cgJ=W5g@c0Ep`2m*-8m%$o=zy2olGo~P&#O#%T7JD%g8HB*nhCe zqK^b0R1U8`o6^zJ@fkw*QkHSDFk-us$#hJWN`&<*?EkhJ+HdfGCtG>Cn*2N6;;NLA>Ch?FYsYn&u!Q{(mRwQ)OVLTZ&8^)RuWg(C3)xPu(n& zV`eP26Lr?kd$KWOFl`S^F6E?8GTkZU#^EmBiutEEqXxAL_~TKhLsc>eOaEHKpNCF< z!J{kZn>Rf-9o#B@K?mDjXJ_vohA4?3qv98-79TURoCDS~3(hvo0WwZ%II#v*O0&6eX=S%wQO`0aX28V(j zzdIMx5j-! ztnPtG*tq$i51~=lzAvV|4=LWSMBqFpYqF((t92bEe#AO>Gi)AN)4Wbx9@!6|ZBt^y zD9zMBKtQp_G(WeOWtBh3L|4kS?+tS`)#qa-_(&L04?Df?iklkC^XYQ0{ktzT!&=9> zVmcff9V-13`Bxy*W~T=jsfSh{j6RFre1f%`1lyd!v(vGg+fAcw!nLhpan)X_>JRNCk(O_%P@*+NwAA28SV-9E z{3TorL~#1#wep)jwUz5m$KxnHHQG8NUR*`7Fm=j!FWm%?Zn3ebdI=o)`}Upao6>FX zrKDG1;5c7?WZn6aj)D7QM&f39@>pgB@HQ3hp7cEgtB_g8c*V2&R#cmG2Y-UENTwiugE&V<7 zJtfOx!-H?x+AfoeeRgZ`wtvcsS^UQV9(@{mT!yY2;r4%KE&)Jkr) zFGisiLe2_Rqfe#lY_oGG;H-sq^flqlzny%pA%sn5o#CL|)~nWP6Yf(bi|ys@uZY*l zA2)6LQSl)jZ!wNmCfEg7xj_J5U;JAk0G|cj4(xU`4%c%i7gf5weg;Ut!9dYp zz+{E$%9`L9#EG%HK-K^j2?`W^Y?%OP7`~^Myn<&nt+P25qFiNd7m)RjG2Kw+{!$+A zycgo)$#jQ)?p8z)u44i3}q#O?ucVb&VN%%w8Pkh=CtxoWj1>Wd}1UYJ7ka zL%G-WG~VUe!o$7R^W{Fr0hcebma3YlT%|2!NO7-Y0&@#xf+|1IXYb=Ce;3oQ*IT~` z>KvO8O9^VCvM>hMN|-z^hrv!?;8TtOZ6uRDM^{V$iZVkWK)WZK{k0V19SHZV+-Fm#mmq)JlnPN*Xa%0xV0$ps})~h1ej*u`_g~lgOB12sC z_t@Q}6p#`WRFWQ`#x-)q%=k#1Uwe{0MlC_C7PO{fyEjC?x}K-6MHRnw8P;4SP=}4Iur&;EZ~*4wUP z9xG|_VK=|ZrM-F>fqi-yRpRbEF$07?B1~o9yTwB_aRq@b2M}V}4>uFwfVIOps_)ci z#>x~d=4S4MZA?WV3AxCZ)c`?>Eee?$gbHt|;918Sl?D$AtsNdxTmXASWvORTyG!DE2@QXx0aRqy~RarpeiNfNP3=&!01OmlXJok zA4Q?8LLNEcDx>gYM3D-g6(LYpOm_i+ir+g=?g`$oyGR=4T>5le*nFQkzMNma>lobf zJ*Mty_AS)WjE+#A$n;xPspxK6X6Q6&zpms_9Ju99;Gxr_YZoa`Ct&4Gf3eN6wN@|6 zdo~$T??oFfup!bW`myr_HLh))M}`SnAqxLz89zEo#AkdrYd)lUY|A+O@o6@1Z}!}4 znd~6sW}}SC`?vjHs&sWRrUQ?M20f${ik}hjgA??Q3-ys-vo7tsltkEY-Hv?9=bU6> zBy9$t)oRyutT@c2n`EXgk`DT(27eI~Q*xneqNn2n%|6tH|M5heEIoJ zowr8JYD+`q3tL78=kk~Ps>^MM&vEGU|D%-#j$IJ~bUY};q7N3n3oEgmhn^>m`ZI(8 zF&qKL-DK(<2}|zWs1pN=e{$giIy!uv*N2Cwk(3@Lpld)$)dz|!V`%+$`sW|RwzqTo zVf^1a^V`th3;c+E=6X1g?#f%|+<3Xtlbl{tG+4%}l2?1!ud)u32_gVXM;zsqQU)i2 z#vA~<4L5|PmIs~_YbQHVYU$#!+{kb7Xa7fv4@D(UJyyc|T2@U0h{x~EFU}#hXI7cc zBUbr|(4}8*s1={QSRQpVbTra&lI<@}dLGtM^)OBpQvUTr!u4<@u6!+@PzfV;uEr^(_0guH2 zajcXTA=NNR2Nu)y+LYsx>c81q+NwivzuVvTcm`1H>@TPm+Bb#<`qDO2#lpL_b3~AZKRYN`1w(8aiLL;nv*t^D9Ld8#h>9IKT&F z>ZVu=J!E*~9Duzl2Jl4Ug4v)#ze_>zP*~FKcuH#cj}hN>6Ay)#NnH+(>19(&8&#Lf zdktrQ#bZ2!uC@mbJ+Pb~Ue|bC*XREk7sDJhbc>vF@3&%8Yc8=(hQ>WsYOh?7YgdtV z54;X+Q9Ox*cn4-`Jh7{H#iVgY+ooL+Ja!2-*#zj!J$nw!BmWm}K`Iz3Xr%2nHoBCe zq$cyW7Bn{giWHp>1>`YgNzkLYV<@nwV)22c7Qik9@ktJZnK7j#yYzpv6~&aLgH)mu zRy*ym{G34k%AA=@DME*7T%j}4?@flgxZuFS5U@aVf2`(<`Fib~XlAs~W--jMtjv=r z+$Zw@Lj6wes|>o5-XxDAzZa!vA`SHTinXeOQwhp5d34~$bZ0EJ^r;szGn|!u^)%yY zvQ~&Uw_euxjWcN=0c#*1v;^~vqW?Gd>DA7KM3itoJxF4n2 zjdwjhWzPzWg+(ohI1F&{lc+XTU~p?}e60|Yfm?wr2kECKWsBg&V>&#hH?Ys!d~yIT zC)Og6>3K(fMV=<00pstAE=jp>W3sO63Y#fLMik{x>z7ur_PUFC9Iv++4cbn~w)AGM zwJ~Qa=Dz3Q)Bv$nccaUkL`y2I+ElGO_)r>UELx~n*-F&XiG&xbY>`0Y?201_^Ox@! zPDYdBv1!bnOM?kCy|^hxx?Xc{U(kGsu?r=mBju?qs0HgsV}yUS})f}KV=e{Z=qb!AR*Vj}v75?~omBk{I2inh?t zt&P@T64xJp0>i91qb=60G2bKVVV3&|Z109#I{Da^jo3>5O7`vc<095#2$S3htTE3=ovZp;}(FmdXxiKyqMhk&;w1C2e&(A^F3U8fAnW==F z0QEP0u1btrzvfhufuwiqXu2FDD7c3^=@CHce!W}kHh1& zIL2*^FsLWmNYXA0F`yqN-27_0se>Zlv2yI5LZ9&2599e1TN5Ey@crx8_-b94{%c3S zQT=)ZLU=XPnwO28$S;2+Z7jCd40hE|QWxp>Lz5X@?R>aS^p%_;TNNHVJUr{dt=nX~ zyBt+46k{vB38&ElS@Ivqkbk||YZ%TvNzm7_srt|9eRYXD!|cA6iS54VVe#^vNZo6q z!Zu9&6(WddkdMRb8Gep+K@_@`^!mlx%?}g5c=0$whb<4OqGt!h(U8t$OViJ_vEG<5jA<{Ef(bNcQBi z^Wic<%IEiKYw~I$*lNe%XLmCn5Om zX<1g|#oJ_*Iz8*-UE_B$HVxxPO!*^GsXNFam{8?Dj81_4%45bO1+@3q)dZWAHGWHR zuDd`6*>FT()obZcr`O^toH^QWnhRSMatm1PIO_}FIzOIuTsZvAmlr8ZcNI>xMl3`^ zlCuQGNEVu9KmNK%GoVeQtHQe;{w4PvZS}S#i(|Pu^UP$@cF82{ndi z1Ptzb=Jr(i-@P~~%x)Y`g(J(eKzM8@wePumwj;b1L+LL|d!wPUS7<#+jP*HfjRZ}_ zaK|E(g&WB7&(E9k4v%*;wgT7tXL1EMG;Icp$F3>?$Nk2{q}9}2D&ee5DKgJor_dNl-4d~^Y`u8xK`{uEjQbwnKzPNK20i5P742pNu4~Y>6M-tjoh|J zh!$Gxr7Mi*Uh7~<1Jhc7lj17ndnm^8%hQ$8By7mEneX9WkSb4%S6s)Sh{DIxBVZ_N zt{?e(QX`jCVzpLt0{#2g)d zuz{tK>lcmgua}*vw^sdh&LsLQMear92JfN+maD)2N(l&Ngi4zUq`WxLLM+S^J`>)P zg7z2v8S%^aVV-$|Ii3=VRgGF(L!nTe5-_R^f>59~9gBRj*SPQscGD(~t02T^&! zL{H7+GU@(A9@ zx4yu8x?g1`*512lSJ7l0Ea^ z`^a=KN)X*__G&(RA97rzuS6~rg%9#ak$PhamQF^9l*gL_z)|RZ#)Bl&5&g2qz?t>& z-oRApE_DJ+LKyK1O$+6M5RX8?OL_0o;A$W!TERC(<%GSOrs_G-V|-TEE?Fl&WX3Y{ z6W_Zau1BPqe5cZ}FnI}gFfwKVcNkvp_jNR)t!n1-Xjv*PlcVa2#Xv@_th|cNw^pnN z={b3B?EY4kBx*r;@GeAgcvvYuHvyOo28q?u{j{Y^2}>Zrwih7r2N(?x7DQtx z;OtgornglSlUxH(O#!2*!~YSwUcbCrKv?B$(ffP<&Z5gVYLUi-z`&pOuOlNRj}B%3 z0A-S$tt9Pfl$ZTy&sMkI0Fr6Plca*1V0A80M`HmX}Qav1Y*65v^Rh6`n zhykO>ghDy~%dqO(y8bQu+Un2>fnF&oa3VP<(S|-<1i}(hDGg2LGvIi2+93a67S)mC z&YS6ok0EQ8UTLb|EM<563ANeG%}YK4DObr^rntuMG;ZQ-h}Hg3K_*5}xJ_VS8yfjv z=S%d3+3mu%Hetk{j(1jI?S}L+05g0c7btCR^vahIYh^55pgNnE=62J5TfuzmCpPc_>%^G*VLiQ+bnWS_PEU z{Ts~*5PbNQ*1(=ZKV1|2=F>-G{ZF6POlzk3pnfF7dBdX6k2j$StUueyn;lh4gSHL5 z@QuQ2#RIGYC=cJ>Tz}ISdE6!=RVK;x4A|2g%Mo~n_VrhZjB{di!e{opnR@2q#f-mG zsmPKwM$BC*<`z)_eNR*{sUH<(BYAY!7@GOP)Ap61e z!bbJiT^A}mWi3H|E+|#(@acPHx-9X%v0HgZlK=t-S~GT&IChiR<;*{#PQP|iQf3h0 z=f-;f`R9Bk)f|B(QINhN-DAuy>e^(Q5}x_=rKPIpV0q@W?lM)W{I_bmUHvkm$Qk|x*u|k3C=q%7N3sq@Y~CZI0%yN<)d9^XYHzP(#*i8b?!Ju z6wHh-c;cS*abl4ES8XRvM8w4Dz{TU5mIz441xTu3b}L-8K3W)S`Q|tM+Zj`3T{{xj zLwWwlYBM&&^n_NXJPjq%7*XRPNjDrbi(|2rOjr=}SV}Ti9T~ZL_nl$Ch=FCbVTRAJ z-M7Z(qddtg%!yX*44dI^HzL=KJ64aS=JT;WI(RU3#wSL!|2T0|;uv8tOfz&j zaUuvx!f-$~J%gr})@`y-ml9ZcdAsD41EMD&0B)rH03q?_n5njZtlWT%1Ij`!m`A3g zqk){YnX$IpFe;u!argBRD04P~^UdN;=3GAn8pjD)F!gCmXko|D{78)?hC|;6Gl6p6 z3jC(*Gf*ENY-mevfGn~vNAILh=|nD4wcZ@L9v?ey1uXePA>xg1H$kFuUXamUFq|vb zDsHBX8Nh35nrWn#evoG)BC^7fi#OFE8}WE@`XiFyfbdwA*!(qemRJ>&Fq}0xa;TeA zw5^V_g@fVz+x*E^cU6z=V@xI8S2PFC?&uDd?>y_uJbT+^H1t~OwTZG9FW7}##xk&e z2rv%v{3tFgPX0{Zn_K-&zU9=oj(=vv%^~gnPzSC0?5WG0lNYYqI9_dM-PGsvSq6^Cy<*O5Wc$4Z^Sek~}&p{_Ws%YGLj2CDSFDz)_IRAY|<8SAfmPCv{_muT|tE zQ8RIFJRW-})=CS5WwvgD!H-YJJ}klMuO+R^CTW*xkoac0=?fCv-}=H{+r zLW>5WK*J2weLTK+43s6p+CTJ5xk_Y00lF2k%|dRWb<}eCUDN)_U6@_-oP;xBS)x~~ z&OrW4IafIjO5cVIw9RUC_@yn1b4x0Q1F&{ zph{w2@In`21tn*#xi|O4Qz07wG*pvhSqoYxAfJ$KL9}5t86(sw28zfD+xvy01D|?S z>0B;jm5oXu-ucKILUPm^cBvnVInz8R6-8&(54wbgdpc-%8cHdvq08%26&!mXKBaQL zvUjq@U{T9fAW>1tUg9lPU=laKZ&Lns&olbGwfZc_Y^5U(sTk7Q!BJ!9$!k+y!i$Y) zVle>zfDeQ-xW0Y+F^GavA=3<6`8D+b<~C;tMj^?2xn3kz=+c|Zw9vNMhYU$Qih{83 zjOW;r$2>@*Q-mSeTKU+0joK5O0aJ|R;ojc(%NJv%!DqakgZ|jqc1!^HND|h0O!fCG zB`bnSAn4zcr}Y~SNu^rVRGqj_W~vMN$K^3!%!5Bt4iN{5{72Z;prJ)38*CXM#7vaU2}}+o8^h2hndMfz!yqnGZ&F88jAuqg zlMM`W3qzEtDe&Iqz!mes0+c3wX6!iFKr10*Nso?z0L-)^CQ507gQL$N7X$|rjM6*d z16(J`cZ=%Q3+*Ozv#^{`v*WRt!#(+R`slSoMO_8cv)JmI)#3tDK2dcAV)x70vtsII3(^Sx z`+rh%CpMaW!oNA#U=O2A&#$iF9ojfSD|r$5;A z4hP$hPyM_JYYZcSNOh_GCu@cu)i2O*WKE+rc%KvV7GMiusnjt6uVNH?1k4|_Kg7(?*8D0X{zazsrhkl1nWweO|VH!V4T23vL5B+ex?sH zORDi7Awxr>Bn4>AcJcg!J3JWpjS1uoOhHE5Kbg9ktec{Nf0Fjznvl|V1N%2IMaIZ?8S~Dgi=7}8CElOFUuJI+Q6<8`uxaN zLc!{MoAAzu9VcMqB^hRNSA>-S5`BxT-OKno1_gxTiC$>a6U zqerAd-LD0y#ljB1RTJLoUzQb(^As1sFLR?zCwlX`=X&y#g6bE@4H)HBk$m5Jvn*!h z_U69IQDuKp1i5V!9KL~U!biLn)IW6|_J1p<6bqvVVjZbt_WJUNrs@;TrKMHZ85K*4 z176^`dMyovMb-()Q(60$hdJZ+-{PkEG6T z)Hzxx@0_$|N!GHyN_9NSm`vA5cd8Wq=e)bO_WY>ULrJ%2<|t`~Ds+76Y%%-pi1W{J zS07Mkm!-=5i;#Sc*M2BgjRFy(VbeyI8xPR$3-Dji2#%Gpr1ti`)A-I@TCSZ842DTo z?YhiQEf++_m)(>ipOPkh`_6piXheC^e>jnKtGjM8))~HGQ3d6jcImPv>{9(3>YW4^ zx&chP6jg1q43&V|o=R0@!iO0uGm#0>7+)E70eqqemU)r($NaS7(s*P5l$_^VNlo3i0PycIyDF=QO3)()89bj`KO+ z3+ZAsPP=zR#F}FoJvPVV(0b%%~l{t%T2f$GkGVjpZ$ z$|tPPjsSbgOk9+ky}?2nI@>)Bi|1%eW@rw-19fjt)1{Q6eERL?i?m zA&P`FNW+`Z-QDoNf6wz~A71SR+r8_&&hz{pN1Owi&J;+J-n2DZf4cN}PJ`+Z;b;SD?V#bb?kcNn;46nNi~%y@THRuqAWR`gQ0w=={a)YLU! zd$&&9&xi4m&?Vi@JW(OvA2N{34diQa771Yam40;T_{GOfoY{Po1!dzIwXxv=t-1Xx z`tvU7Zq2SvPISa8*Vy-|cml6KG|^R=!!YMv6%>>3(e*h$eXKYM%3-PF6aWm7@7OO> z*8>YsX>|Ai%f_qJnfT&Wa~IJ%#<3P_q88hsmWDLV<5k7U!Qn5}i4l!Q<);>xM2lrL zcU3K>lVO1%L9XOE4#~fbh!5(!kEudYD_4I~>SA+k zBN)tlNVp6&AxH?)lo9hQBxQdI*abg=W-SnnB25%5Lu31@Cf=7&slTM=7c{EsKp6Ns zbhtv2fiI$ehdn|Pj&Rugj0g4>31}|(CEA`cy*N{`CmQVDT}XeUN%uCtp>s=;Duw8& ziu$)ikg_@e0hMDEU;OtwoQMfdse}rG$bA@9duPa?>Tji=!yekqTnBJVnyPe5BY?~g zGbP2{1|DPbYD6I5hln4C1&F<1GCoMN|1uZ0D#``JO6yoHOu{h0&Gs=2R5^V?LV8g){D=mm%_0xzBg3LC>k&glkM!ChcoAkJ*MZBSH#Fx z>3aSF(~{DlFrI_(rWhKhOg`12o9%ZP6z&B3tHLQIiH)2Ty8FvmGZ0nzh3ZZxNmya$ z%2V9sq?r-Ad+D+7c?1?}EAX@{ao2}(@&|%f`5%}=4kbyE2=I&D$I&rJ55NZSuJi*= zjG4YFzA1Cmb7K}Xr2(>PEFof$UiV|Xk8Q4h7sKQ3EIr|eptYE7lmltq-1@iGp=s|0 zdl7oKB`zM*y~CNv(;ME#QgwEPmAwY$scxhDX|%Se0dN`juOVxpwtBaMk6{DQ>*WC- zbN3@g^X@^fis#?A>h#NZjTIk7?V`~j5I;pWU)q1bUo$#ySRr0eu;F+O7J$W`si*1&$oovC-!{TvFLU0a#%d&jSjsWd+|%EHy?yzW;-eB`+@#Z5$1FK z@Dy?y!Oeiz4pRmq4#l|0@4oR% zh!P~rvhGJ@BIg+w0K_ej5o^ZfkU4%YCcX#`q4kRb73Y%u5?+Vdm^#w9sl6nOUE@^K z#vizJk-gtS>BRn5c3?%Cg@y_PnfQI9gNq*)TnBJmY<545^EM?Kt$NzrX$wSYun z<=J|Bc2qa46ZQE0R!(2BNwgELK$$SlNm^Bni*1>ZkF|GRtnJ!QlSR}SVd1sE zocX(rqL~(BhTZ?>Ch{8Ez#~`|(RSo^!8-m|ikn`Dcbg!?STSb-bN53{o>_4#Ld*5Q zdGBfNdW))dF!Hy`r>%(AdX&b&arAp5@4AAZpyws0o0-9VFwr3cdaJvh(ntO5Z}CB; zpisbO+3%do`*2i`y(3E9%J_DZ;hlpiuB5twp?;TPM=}kUhEWw;a&J^vH76CD%pS|{ z?`kxt|*CZxwCIAH2YgeEa2LHLrkEx9OCWp%1IO_J$Yv}aVVJE>Z#m6yz)XxV7)~dy2w6d~nn^G@c zhq&*@lb(S}0eD(BloNIi$Ewi38OnTOrz@0(7sdkn-34f-fm)k(;Tlh}9VZ6m>d+(a zg!z@u2ah1jc^HiZI=sYfzjV7BputXq>4lChAQ<}+Hk>@5XT)y>%w8N?!fYK+Uo5RW zqy-*;yew>LUI5buk2Rh|%^aI}Q$Sm{HQ{(p&;YDUt`$ruZ?~`sUxq~=o&uk~jq#qg zQLmbs#_gWKcyX^U&T%9T@@Y107u#*&r31lur+VEQ!!p$G z#JpU7One`~rDwA-0$!HVN-7{pA1|FdJ*wrFbZsf+#e(GJ`mJaxwT*`#H|c#@KCP#M zQ3lgKd8xdDjAxq9?EEg<*IFP0)E$edREQ(HuhL)p+2tgU50*QcvF7)cJ+^|NSmzQr zY`wGf6Y~mea{b@8jvPr{NU9_x18@wV&JL1WvrAw~V`ABhiHf<++h-RTF8go4tdHf( zC`yyOH4Dd*@9uFxI>eALUw?OHuzOZ+?p~;`^R0+`?X5wrrexK7TWK{4`9c-Zdl*e& znz#NC!o#4xSoc_Th zyTF8vvA_)ARY8rbIjT%by#!f$)u`0M!F+76e5QB5`B;9w$%(Kf1JP~avu_2b&T@@EJ2~~+ud1?et#HsR=Na6=; zXs6>mI5_qw6SLy)f0?Ca`*!>-RjvE>q4ECUTvYr|Qvb2oJDw5o7A&z;`SJtI6k>8P zul#GF!G0CkratFqcVc0`R|vqB(V`1_4~^vH3lkcrhVwjnd3gOs$r{W)rjkF9NJ7XR zl$o@*!56!-0(#rm0a2eYQnc1-;AuD*_ev8T{?J*U(vSQylkE9YpFL+|)A3X-oFm(0 zzg94ynbHYA|FOIFOm2=(S$EGnS-Z7X6C@CdMDqfNZ-GM$dP&%9+1=LhZh+rCvwtpp zAO#=6V2cF;p@(6~E2P3EZ0-WP-NL@QG4s2t>{}-aF*8WO%>fPeBHk@WBH%+_`{@b~ zH+koOZBi-rrrlvje4u@U(+G5K7LyU4JP6HA7K%R?Nr2#s33{%kJRbT{;SHy@N{hSh z4J}p;jQ1KB6Xk0f1mv?ho*6S4*C1irr!z*8iGV^hIj)%fbzB5H$r8K75Elzpm=U3$ zFE@SqcYXi1n))^oy*?KDgdliV(L(l^!NTRI6dL!x#qs?(y~JAfoH)-**4qks^s?vz zaAdAy?-z0NFP()M)tM5vP%TQo<(nrxg^4T2Y^2(obS|RWOta=S?N~n~s_7A7TR7(Y~#apm;U<{`AwaYnil2VlV}=>4UA$nZ@j*^iH8 zg?;O4{p^)-z1Sp9e{(nf(oSuH&krbw$9BbJ%sMWjOPEQbjp3D3`N*qRWUF^4B0K(xP7X`hqs`fw#N`|$2|w}^HF`wdnDrW#j49i2I`_)+sFPgJdtG$ zu_FFc6p4f@rzDvP(Dx>)gU*bauyS5XxseU zyW;}+BtXQ-F?C1>ZbrTi-ek0Gu4;7iiGF6cZOKgLasQK2=>d_Qru0}eGt@Ttj;7qfAxIhn{shXKU?+CB9FffEwHYcn{I(}t1w z4Ib0OM6sUYUuM*O?eyfpSb5?14u6)t1is54_qvKC)*1FdTjd{btZ*JOa}AmqPI8gk zYp{D8+hkCuN23vUI#$rJSM2-jG;7*KS$X*JF+{i=`*$)2e;8L5Sb&SNlZx*9fXX_( zJMj0@V6hRi$9z8EvWE?!zN_hbW|XXsT9@|7OZ@Tm+XwFPf0u7`DjG3%b;eYIH-GJ?RCNbWz0WDl@fpmZKnzKXYaCRGp;xn3$ zx}$~1{U%cx?If|^YtBTzw_2^Cs#53vZd3ieVY$0dZZw)Tl0Wxu8N&RMC6f?lFVh8i0dQqs*q)m@+WdtzvUyzwWoQ28qfnUd_ZRo6kjZ>5nYvm0I@=lOT#MURC_qWcwEEwHnrYFfkVN;fDP~Mi!}Q* z`(8S*k}ENrLjXCT7>9HHcrQHLo;I>2kpDO0*N48F6sZv=2Z9HY|04zY3q@%9%I20v9sC^#jx>f)Fx7hSwkZjpd$%^b|T*lv6|JMc=PimWW^z6k&#P5+&Tj)UGLIx%b_E2)xeC@{I zE_4o|X6>70=%a+B41(7vxJoVIh$IAon(M)IJ_22|+ zL?1eRSwB`&c*5FbO^?{gs>MNI_Obq39fMICYFc71Pll+-FWW61jU*ii(m+c=Tg2s2Y&Zal~$LM`le zqtz>EM<*J0?~Nd%T%j-|BYMJ=feTn;6((P(`WhP0ygcluL6k7pHF8rtMrILgDW^A=r3B>BIJ4;42BL4%E!6ac zL3+wzm6!`!M}dQRI=`NdmAj0Jy~mtw)OgVNUDz%2J#YvagW;s0j`_6=E6zXa{T7A=)B=cW{^Rx^uOe?ivx21|Nb@PdMg`_4J6@g=` zIrCQue&yXTQdi&Y^72(AO?3RN=i$f~{WFz9%v6#Xk&*J^!%0>Y8{>jZrh_|=JL z_FO2~Uz9}wI`*1KeEws`v$&`aGUHO^b(KwajpOCYFh)N6s{3=1ttp;G7a zw3O2K30z}2)I%LWk7gc&at)9&L^5W-j5Hx|fNj#T!tkvR^3)xIl!UM;vtfIbAADcv zvv&jFhdXrEY^lz2G9~JDxol%g?@^(Uha~da(%A&>IcQ(U*t1#~?x`PV-~El8JNmh|e@qavxBpiGM&h{`+&Ik7%;CM#!dK@w_NdYI zAg$yqGIH$%^fq{X9w-Sf8a|q|Mct z7JvZ>k&wUcU5kuq^jwK#2y=A&hV?>v!9ld(aExN-!KoJQO23R%Q&G$CXDvi9RS<;- zS*Denn@7(lwkUGi_?P|+pL?S)@_rfWGD^S|iQnrANih9?X(&&4+-vn*Gaeq0#SizWG26ghmE9v1?ZAJG=WdiG7Hjc+2xM`!8*yX#va9{C5w zI-aH<1$ZV*fysbFAWs;}mHjP5pkGy*OGF@L+FfTfez;k{nc%aBr}4_bm-Dw?qmhem zR7dtsG^9e;g;Xbpv_Ai~Ywu#em~7E6gbF(cxVh}a>k z%`q&c(E5^Li1wr&K!J8M9Je9_W+b2;~Ktl%qWtoY>m)yEIJ<3*)~5S5(% ze0r)#!FrkJiY=CASW0LCgmcxh>W80d-uWS(+i#WyEEW%l$# zw_QcTdn@fr@W{dCynzWpYqV4-6FC_}{aZ~ud8}#Q!4>)!=w^g4aLjkB*ZFe?sJ8)% z#;innPyW15;-1VC^smW`;c&PV#%%!|u8GjT&|wp?y8lc{#xv4R|7`=`ufxgiQCKr3 z0^Lx;nNE{i>Xa@9lWc6GN8&O(c5@druRl_*jVhY2RPO0ATi3`8SpC&4|4KqtPqAv# zS9PVgPfUF5RHIbYL1B3w~n++Xq#Q@9vr~7+?}NV{WCrny_mdj!ddff(OBsXm;S7I{6KYZ zJd~8!^$I+LW$>IB*WLQVj98^u%d_tghrndG?;|>_2o`b(zEn10eYSa3EKBm(!WGTy zc&`3$nnDLkj1j&3U&|Gr%v79F#u;emjs%^O+bZJR%5r$)Kfa+Q<#n-clV?aGxNAD* zvT{t{UfaBHYw~8bl2%Gpmp*~OKfFx+EXg~UPSH59Fywyl>Ou^U@9=VFk&!~Msjtta z@dprk8~0ghdJ{QBvHT}xgR%YE=Sr(?%i=&_hKCqmHQr1SO5zXE_8w4-rDVk} zU)#^8_TJW}`-NL-!Dwkt=U`R6k+E-Q*uCy1qxhwJEQ>#ti)&NqVk|w3&@H4&>_XXN z7zfj|%fF8&ta{)-aJG1GKi&4O+Q^=E!l^+pVj+0P{khs~=dyUa&V<*)>8)Mv(luY! z5&a`_a#G!RVZCn!^uw4DbeSqAE&OHeHGM7bDbSkjYQ;Owg zr`Z}=XK0K8b9NX*;F+xZy*4j?S>9I^wIT2^cx36 zcQ{`Vr(r|m0aXCtgOK3KL@*?r%- zb~Mb7@5)dPbeBdXY}K%u5>80HYKTZ6egaJABP35*=Euf6=VV zs9|?k9-a#+j}Z2lm@xb|QjUC>Jf@it>L~83U6hsH2o)>HrMjx^joLMKSsUKVW+MV2 zDtAHZOqPm&IRX`LDi zl@ei$bQ+c#6*sDdRYip5GQT0RWGN1lt%U3m+0qWu)JO97WN!r4(S*y46UdilR~Dqi zOfbEZ=LJNsZ9ptmqXQ6lv*y~?yQZ53nh~cp-RhS(5#_I+LvEieGz;S)=p+r30SNkL zH6LqOJo4p_Ue%*&*@@a-&75bK$@0noQ`qco-QGk2eJrZi4-wzGt#u>G?&o|OW*|HJ zTwCf%W;MOd@(&|7j?qs|_6F*+8NJ*|^={Pj;Ww?UnanOD#1 zAWrGhnP?mFR^JoxqeP(P6-9kTRo(G3Qsnrip4vL+?dyKlXs#T7nExol>CY_#x+Zl3 zR9|?QNs=_Ca;<)8Z6g`HuE*|&%j}R%A3+5wdZWGUpPj8$K6P{3g58&!1({VQg`Wz_ zLkcr~2oeYt?$SwKz@&VBkScZ0lLR<;Sfh2Uw3EyC1ovI5+v_+Q7lJxZE%R}y6!f6H zv2>#Oi1<3qFTPx680=+yt)-aUN3u};L}gwI( z3*|0UeT*-#p89Y*SUFD|&m-Mt#qs=P#Zfvue7M`=r_zUFZ3_o06nIyj85`5* z?U4f>0~^+9BtI|@Yfw(l#Tq`g677aE*tu?sOH^?_mn4RSOG#7nvE$|4xWxS;fhAFK zp&o{e(_hCFZSsZZbMxPYD9%u!pYMNvfO=N^3@-K6uL~-2t_Gi)$quMzN{md61Ic@#5n{h1Q{!o6sD3x{d+AIi~sfvs*HVK z{}{-3zqTlw3l4gI{{n&F96>LpyZ7@P zRVcVh>|oWT7X=MYkH>U!9wYvY^qI1O&-xZHr9^{8b@OO6_uVTqyb8n^MJEpC^N;AU zxbvx1JC9bFx>gkoo`{3`zGk23u_ltiWSXtHc@DW%?P$AYzVo(UE$ zlR2hZT9HmFnkMkGY1+ucj#toi>i5s)5ABuPN>)-z8$E$m$>(Pb)g*W&R^mKP)^jY2 z9*TQ|>-Kn5+@$1YA(E9M97NVTuROlB2*)QA|NINGJ{#=D=wh5R0|T>JKw6RPG1mF` z7d_X!j;->)YH&9+N(e2EbN31_C?y?9ps4`?!r@NO@y&qkhK*mP*Z17vi*`eHL&rAt zBD%Cf{hu%eLR^7&F_DXvi%BLu^&WQOg4J{!j9UL;rg(yAm74svRHe9|SF82d{5^rs zPZMb(hA$QjcO+1y1aV5=0Ys^jM12JeFx)`{qCX{Uq3*kU zs`%6~7{N||H0>z2JQ1!<7Sc3PstmH^JxPVHx11^lEhnKwZzyIY+kS!dBIH&MlhZBa z6A0~#f+l}(KH$=v`_2Y*oz=C=^=s=ejI*vo>f%3}&+R$SY9h!rZCkBNwpli_Rf)$C zsFeWlrntRUu1)EzQUuRh%g*V#OxKI%g?%~uAC8&&wRe`ylU|1oW^at66*e7qczH#e zds?Ijer~@CX#JLfFBYs;&3z(6K2Z=cW-LYgIw9&E3a*ERYi*?XxruErX5)-3-96Z_ zCiv^Vw_g{phkWJ3^`;9O%5~`Tem)`Pqs0B^oGDB}{#~BG%(MCJaA&u3^c%S{zxS(} zZ$Yg`DD4Za%AZ@ek1Ls~eeVz0x{kJ+n9|UnD|GP5omfK?_O?|3%ePxK>gNe^qU>h4v*%p>(p>%%`Mg%C zukz++SWGgpD9)R(UO9b(symrHqsXZ@b=sP_;?BdO@FGj%?Ux@DAj?vK0{i3V z`dSaGTJToQ-No1KB}$(1a)qAnRoZL!vuZ6zEw~4b&lXWxX*B=Y7^yreM$NzWWwEF- zwAIe;I?)^j1Lmid8~m#|IEeL!{pF z)G8&Z+vI5SBBI|es!^7P^aqkprS;MAnr!hpYPTZvr zr)`6^y`7sj!{7Sd$lGxwK7E&3Xy{Mkx*7uSOaW|-G0C{fNW+EB&;RZIfgv$5$&lD1 z*wQ`~H3s+C=;vd_9^BnlPA4HtVrDuI)wSOAyk2dd<1EuxOzXXcgKRi19KFE3u! zyk-CXkn}PmYkcc6fVc%u7BpKGmV6BBMN& zs`bWmH|{{MeZ$m0pPK1=&URgB2}nLhxt5)AV7C}=_aa_8n^EEk`#h*kcPXkTiHCV? z@Q|K%fY58B&fbjjasM{tUY&JY`KU)07$&yy(?=M|la3}bX#&pxsGC<*KhxXQ-huNA z$|bmT+MSF=0kZ?}c4w?U!G2Xu5x`VxqKV@BWMT}A+?g3yoX!UeL-Z^Wm-A#E=&U4u zftugkSa2Hh8=NsECu}h|6deITJxT}Pu%F&Y>>mT}Q%h4s42gSB-7B3ahCry~30)4EIa8_ay|XUueA4 zdSS^LJ@el9Qb%5Q>hagT{u`tF9r2&TwGVZDC;L7S&~Ee{Q}?~=LgwMpucuvDL?44f zO~}lU?Ec~y@OB~<=`8J+a3vI@+f5N+IjLFmt|-sj<`XAyfKZN-3yo^y)(;N7Q+qCU z^A~4R5q!$M+eeFs^|%IG(b?DEnI~F&unjWe9=gvn&NFmm^Lyqc>_nQ)*ibuOp+vs4 zCk{LX|C}|#QdgX0a*k!aXY;)$yvTT(1E!nLR4X_KB2D`vO9EJ__#)>%8 z;@1Mh+f==eG4kj;v&Cc5Kj*aA_&dDIJ85(Rd?Egl;I29oUp1xl1&YqRH2DVkiN_x+ zOdrh5JG|5#h;4^`4 zKB|sBPP;XoSZ{pL`T0jLx=y4@>&236uoTTck;O@J;NfG9_qXUbq(%XFPxjy0+x~+#~CMbInArZ!>wH1@q&`l6=-EM zlaT~9Kcl+U#+V+g=|JnF#x~Z9Kb=FJ>cMZVtG(K)ZgvMj2)Rp)gv9b8O<7pj5FmsZu3@Tg-EgXHrEMR`dnz=pwwfaXAZ^D+0lzIT{H>t#YZ@4&Y`aH>ag8$){*G&xA;k~V0$e3Rx{tZ0FgI{Mj$ zv&Oq%fFOu$x%d57-7qh0uY*3HAuaLm?)RPu5UV@EJ z8KT@w-nmB+!|?5caW&48@gTZTp(UYJtPuA*@^EkxjV`6up9o~~af(tOLYmZt(15Ow zo(#%6ii}CoqC}D+l^j-HSyuXa6cQ{DC^n>>^5ZQ#E`a`u0Viz>pRvl48tT`tX6bpxmL40~1A-G0=hS(lX! zmUnVVa*_wt6G@F4_UVw%nU4~GS~lV<)wAg$A@kpM73TV5|FG7p&p9@Vcsg`l^vJD{ zgk^c{jZ=Fh%djLi1Q8oyZp@ePVcVNn1D%pAmlDV3<}P8RfOKQ5LFDQfK9eS*^Ea%j zp>Cw*78om8CsD6{y1kBLJ%*^15Yi*<2US)kno~>(dHO1`fA%XS%O@4MXdXq`= zT-jgZ%||~&f7z5|0pmXB@61dQ6M15`5mT?p`X-|Drxb_x`xd$Ei{lmKEyW8;WB&jU z!$_7gE~B=W##EnF)A(XE0WLN#aKMqG#R7m%eBlGbPR-QtMun=Xgrr|KCLy`r(Nr?T z*NWaF`Q6lL<&l+HY+Bo~6wUz_Pjtl9aRW#mt*B=!Aw`o~qCy`*WZPpILZo4o>jyUP z^{Y;$?8^3p2boq^gY+L*IA75_tDnoH!{nJz)VK&;S;DpyhYp_ZGvu|O+@Z*8&n*1B^2y?IP#8_LUJ64A` zbBIOE@9yKz^wR+Tn+{hJflCMJ+nsmVd*;Ovb9iF0QorU5@l?14m*7 zDYx?WOdNZ6NS>fCS@1cSVekIrU;1iIPruXC4s>oFiyXnXb?{Ok8hS$K5R+`cnAL9p zNb`X7wy*>JUlOyo5=x{`7g2xhw7=CQny=<28bneMJM#U(!&=C;UK(ozJujGE2hXdB z7W;j%vRQJgo&2N^WtPuZ_6nw#34hB@34WKx0O>-{?Wq}Tj6T8NY>hZ`e%bqDC0&Hd ze{&4>SzBIeULUx*zV4lO^o6I4+a}c)CRi2U%-f}MS%iO=^$=zvHBx0YBZLQ;pU*aa zjSWO(B}t9{YAuC8U~)0TdhT=)RHbHoPd+aEDp1&2FL-?ryl%tT=;_f+Dlw&6=W^e! zT{E6Wo8)*|s1Dt18}zPxmVq2}qxkBu`4tP#sz3xm0v7LW)3sqFUMT%ieO}zdm$WB zv|_P7`Tv-MMsjS1N@wuJSisodMx5fBQbzAQ6+-jVCNrjO!bx|l7WruaCMbb_N{S2w zX^1pTP-w~s62a4Q$9XIH9uTETzIXn)!T+A$6ZdTE59|eNT07l-n9Lu$zxVAO7l#rp zH$qS9!NO&PP20+Q|CT*=Vl*2b{iA&HOJnYY-u2_m)l0A-;UMibou9-LB(l3+fmTMp zKL`7#yJQx8&AB6|(MZJf;*v9j;(@8X`S-r>hlnCdecheLa=setL^3J(fFsAL1lc18 zLiNMefB2U^-B|Q~ocKyqSfun?F4+C=EkcrBXfVL+1soYBqN|7RBTHyGzJpOPNUyve zImATF^_wA43UfUB$`+;L#!rFG(Zm3L?}|5YaOT26;*M=jB#&bOcjSZkhZm8H)0b}a zc)`yvHJcG_$T8@&_FI^_}toe!|HsVa15(bmMWcMa_xgAgIcAn{( zz`0!!Yi(UN+*-X3k+#Lu@A zC<(rl6kIk;Z+(HEWTZWs!H53_c8{BNiKFWoPc&F(xqO*i%HsYk+SzT_Q_F5Kq~5Kn z*P4#uWN>5~$;Y*G(PPorm1$y?^fZQDPFJN#V(G&7T7G!FTbp9McZ-5^sixs&?>IanQFQ2_uFY(8fQ`b>f~)jt)g<_i=R zOjt^_1x@BYo$`41GA1jLJ`Tn@fteIHVE!6tVWuuLHz5@gm zG{@;OKCi$EEvAxD_6}VJJ$yaJw+9P}=_wsrZ!Qk+836)j1$fq?@cmrylLdie_rB`( zZ)$Yn(b6ANmWmCpefpvux8vYaXUvG2b^mqi zSekxo`_0{7k9T*kJx$2Q|El6EO;-|)2NBYspFoI|gp`ceu3P?;P+;*qkfih0FYYzI zSN<0qc%|#xbyvSLzxw$^cJv6=X?rLwDK#Fw(gH2L8ew9Fy)%+dRQpE|BH?RY?EmA7 zl@sOLz{a?b5N9jRS6KS$0up(Y7)1Azi?1GDNMJBYgqC4EgB0tB+J@4%6l>86!0$qnZGaEs*@Hn;ZD{V442&*Q@D<6DC=tIgNFa#I{Z8!ygszvfgIS zAJIq=@0DfAktoTtlfy@`BU!M_Gr5u;|G5Sf-1Q1MnTCGaemg2bOkvsFc!W{o9x4Y&grr1;@W`>4TB z*PkhQ?!V~<4}q+me)W1sd7~0bvajk(lk>zb=v20HVr^8I9qceEg&lfX6$1a)8Cg7Mq1@LOc5}*x0=TC#8gCjlIN3qozkQ^rN?QsLE%%;3I+PKU3rb zPX$`F;ROF+u(227^vw9W_7hj3%viwfA|zgU?I_ zzx(0}$4lj10_=06OF;gGaYfJqNbASysTno#MPI3fl`sV&|Fg?L@Fh)6SQw{{N?-kA ze;oEGkbqlv6=<%(=X35Wtn`p1iP(m(~HG5{rK>W3hrOEmo8;<&0j7) zXAa<4kS1V!eXa=JC&n12h5X7i?4}D%6@tGpiFRLpENkl(++Yg zWo83Ac-Dt${(I%iZX-R>=};*N%EpbH%&)9RZtY_Wj7mL2m+~t{AKps3-BoN%3L(e* zJpxT-9~Gmhp=w4GekC%0H*G+^)<-J`t7)gts);T-Y5UXf%9%I5d*AwluNxmDNi|i( z(rUctXl3_Q;tP22P}dUP-X;tj3?gTHEx3P}*AN)L1%33yt*u9G&n!1yeOO;yvoN-5 zc(PG&U4MP`+t3FeS%0zeEl0OeNz7H<)D$5Jj5sAF&78TTiDj1ztIt1lu1MIdExg#8 zoN>RQ8!Y=Q9)mHFl#8MCEryi;nL{vU570L7-?N8H?B1(f6K7y`o)+o9O>8wbPoKxz ztn{cP*@ab0EwZD8`r|Km0UhF-pMWb2&14KhcZhT`zRh4R5VoAmp4iqg1|*uxJ)PTgHS z4>=hO0{YsiLo7%UT-U8=3EmOHc*!nn6!0b ze$a*O%F09jgY_F)HTu|4_@D|*;`#2p^%I)YrO%qdo#7NF2E%7?&GsklCvy$YZ{Gcn zqqF{NvSFh*h*FLa1V%FiBxNwV1wlzc=^hM_lc&2=Q%ins zyfTg;xr@~p`7SCa|9wogzgeU*?vD&8Hn!mubj|TlmJN!ssO(gFO}aJx678j_q`H?s z#23Xf4GUH%mup*%I_DbRK2rjD!DVu*P4)ml8-K48C{czq2I+sI0yumCQy2?ZQzKD- zJ>@PomU|Iry;*yw!B500&z21&%ZmCJFd^PZfkVQ@s|L9l+F-EVnYMRx->I}6p4287 z7mz1PIL+gyTMAQL&i012;{h&u3>auA)|S?BTURQ@)(%^emUK1vtD$QZh5EZBcnl;7 z!QX~ulL)|eK)0MJf>-Qo>V0P6&^yah@wG0A393*o8Oz5-wbY6C`?-6gzFm`&Etl6h zKI%{p*N%RCxuHLlXo!^fHobO@!QQCbZo~9fiHVHX^WkIvF!@%4zpSMB=1K=>*@6u| zrd3siBM$3+W84cH8Z=%{plx!T?K4hgsjbXSSLj``duQ&a^dtKZUKuG+eF?!u_tvPf z_#)Db*sp2P6{tVI_DaX^{JOZVJA6!rUeqR}o|I7cis{+r_9xx@NQQm6q3#QgZ#Yo{ z{=BmfB-V0sqg8G!YJb}XBU`Y{C(Gx))d|HoWq9#b1uZ${Xc_+CYe?ivs@uFWQjvm) z8%td47NZ85M+Teg2V1nBT0OK{1it7{NB>0f{TVj>J3vkNZx$!qPNYd4MWTE} zb!L7=%9AuH7&BPqn(wfCGVSNcGo)6AR*AjM$78LGdf4gPIlp9_nxR&AI6kW5rojqd zTDo+LOir>Dp-TSHNd8P?o~w{Y=KW0=IaY}7+)6wUl}&mPaP}-4bsSie3kS8mzysa- zOOYczb^Jrc3ftQ^BdPJD{R_=7!5Yq3gvl{Xgh>c_n^qw<)@SdYG622dKj3u+EJObSNHcE|2OnR~EFodw)~;Q|hZJ{|bN3<-|yr z9$%S2{OnHPxjv#$W2QT~1ltay+~Z5#uS|@$zBFtfRyH#$yw1zKCAZg{QqtG*AKQVJ zV28`A!^*3N*FUy2@oU8@>_&-a9$uG>^;0bT!VL0DM%^6>MiHT<1SA zns#f#GRf&n&+5JPQnTzY$LF^o(WLqH%waa>#ly`^-7XcStki_3ik5`aK*RXqP3ZGL zm?d`BI}&sK_^1hj;@)Fht8=}47xTYv&G*WDA@&uob z6)+I|79`;82s`IYPSQfQog(-psmQyS2xnGSnh*j!#`M2cvT{?5fr^$MX=B>pn}fIY zYEG@yH~dFO;^~p>Tq07ywTyA?Jy1U`Y3hCnTuIQ6+3}%6dsXyME{;Xw1%8txWKljx z1>$QV5!H&~( zVA-kupfgl>vN>JtQ140F67~~@rfHtjmQRg)ualcu{yF#YpHhwW)jVW&JfsqwTNE3W zWFivG9u|t#JgVD*rO;HB+xHV9SuXQ-BQJ&9w?4cLsXlx)~4@A)WYRll^!7f@&7<+{ALR6uEhP^3}6k-p@n#U6KKI4;3#*?x)hNb}|5 zM~&lJ)%4az^4?@*@Jk|MXuwdki7nXFy@1NDhl*AJ>oZM|?Gbktw{UcOFZEu)Aal1- zPkam_f{i4`sFm#%1wB1^$|pzN7?aJI4}I8N;j$4gmLgU0B7z0_q##oQWw@x$S?CH& zny$O9t4wV|jSxAKiUlPJkK~^-ZYvw4mjaSSVB6$JC}CkB&5fJ<8@LVNYcUE|1cXct z$$ibT+U#(AJzDV7F`oE#|KK<{-8-~vm`4~Hm*JXGhfmnkWpmM<)^A&qmQuE{6r=9! z^j4@!Dom~{quoTOj;(%2T$)9_-k(ae*pF^s|6tRe{`94AyaWsEovX`NX@ZvGgbP{z zuBOylHQP_7S!f1>cotHcgca5fHoYdC{J5rHDCG) zX{tK2qgQ6!WGNpWP;AkV=)hA!(`?cu6fOleH5;q~2aQ)X45Cmg;KOxicaiG%t^Eu> zUVk>kt1+fAsiCW4`x#QOYX3-qfIef0KLiFfreH@9jR4zkdMQ`ubG`)?HKEf|(NiEL(cRgmw}xxpp7RG65O;4Dc@(K*Lz5TEGT z+F}twV&hLRQ(8y~9@ew6x~O-Q6+x=kcBiF5vz?3aR_@n-N624Y^<!dmh)oJB zKkxozIov8*N~w+U?`YN@^)^{{&5CiS4h%7)B%NUb0_ztk@r+$a#lt4N2Dg1?Yu{%w zUM~)uLwTP&`;XIwV-w2IA<`mvKYl;@IqJYw!^dlGh-q3s@u!8kLsvh7024F3Rw}bY zi?2#1pax~~m)vwt)shN-@G$x1(ksMII`y6Drp&>Cpd~)5dA$?Sey`pQofb8|v`F?K zR-RQoUzaUeEh#IjoYdjw;~RcQ;t;c+%1m!+1+_n^ukP-Zuyn{b8&B9+&F9fM*S^Qu zfgH>r;pcB4!Eq2kKzxc4Erx(Z60>n@KS%4(?fv}&XfAOkn%UhX;j*E3^^I=g#at%V zuH}PGreiAF0i`O0-{lhmz&tTM*=A?+=E`HxdwBtDySZY9KLr>WGp%?QaszJWSGTM& zm|Wr05W=ojKeQ>z&EDAK9+#|Y7!>;P_o@(<6olRu!Eb6N(f2_DDIfrYaWO8(qp2(V z&ejZa(!OaT4+$GnpE?(BCxb?&$t$p4M-OXHXD8t;*khc{{xqm%5&WDs?`gwS1g#iG zuFLMXFUm7%^^mQlb5M~|>-Wpd5fyrB=vB7ojDGK+(j!r`T1nxkls>^qh5hsEp0%pZxNdg@cZ&(E-W-T5nPYNc>=xzQhMOZaiJVg69g zVtLPOMk6^Ww+g#I-c|}nCSaN*gTUeKl!bj^pXM0hl>?=@ZnA6XHmcLj_NTj|Nkxa2 z^ly~CzmM)i6rVjx+-X$anG}PIOs$0Ot3Hp};J2%)_T9#;3;4hRs#+nnG|`8-x9Q}U zai+{LhjsZWNGhHU&0}^SqposE1mx7h+Ci`HB9+ol^Lh^L@V$So-2UgJ`J7 zvp#R*g4qiE_+>ik)>QUh?V7V)x{0?@6P6XbRsS*zzNT20$mXg2cUP%2&Tg9Sb{3}b zcnG?QU|5c%9kVL&y8IkM$}X$-m4EQh&slbj8rAfVt4#?!%U$00+G^@pv&O+6K1czG z4&a0UVO)3(jjaAl?^^2npwMq&enVEK65GK@#R=OCpDL_>|Gc`^r7Rp^+(Rg z=KH@{8>VNRGi;wS#xB!5h9r7uwQAnZ1QxubSZ zh+ZQQ#3JQ5Udju4!Wpp&1YV0RCT8gR%&8l&3copA?yb3Ak8}aAZ!T`~*Zc5No+H1d zr)Zdte94|4-q;s=L#2PQL^VA3#nQt}or3)M=VtxMLcNRjO{YK;=i4$Fo3n@N9Tk3P z8}P+KA8Gl;l&ykYLR>>#?e-PF`&*8?fW(yFmb?6 zFcrmwuUO-ok9T5o8%ZsxIWsQidL2)+q=+g5J(!+CkR~Rl?|am3Qnv=%-pHk z*QDybvAF_3*(3^ZtgMOjO8o%4Dn|GM1QlC8F-agCQ0=t>nAA;GlBF^$7f-#5y$Fv? z4V3}LAL%}OneKXPkB(TPrjIx6w~e53wqnt)~FxqLiRt^PbIL(BFr zJvWD`;D(G(?>3eDTvXqC!k*eFfrHl4!r)8TIZ(=fKdSD*%6}IXPUoc=RHb~yU-yUD zaCsUM9I+UK?sd$#xRza?MMk@ai2q%F#Dk8+I+1~Kn|Vn`i!CE#E}Q#?>Vwx0Q+%CX zHImMjkHPpm3imcyXF-E4H3q+qn~ue-@*U5gbJjYK+nS}l>bg-Fko;%>a@a;k#$Cv- z1{zb1PleRPTD1LOR>20V)4T|(-g~WNRfZCS_;U5qj0Z5wKYb@f@xlKc3gt2S0n$jJ|9G+F!)-t)l4O<+K zLFoIn@A|l~8&s$v840;Dya!qw>YY;tUNJt);(DI&zK7@Ur<0P#ju9T-h_KEuFdd;~ zYh+8$7(YhZ;%6zhat~ctDQ(8>03ls_un7iz^N$ALyo1rF{MRIz!?*OaEc^Q}?EP5o zl!X(s324JkMaC{$6*I$p{S1-}%%cTL8aTtY2D*ly_x3=fehm^Y=X4bMU;WBJ`1Dg? z4Xzu{{HmE}keMgrytv6W7a|bYB6bY!^7aTy&wC?PD@EkIU&0+RA*DQFy{hRJwOLrf zC)6oR|D7jN%P0c*_Y+T%#b-2vuR;4~V%tQ9N#3s@;V^734}X7g?T6=ggG`$*e6!^nL zJOK)1>4dbNLU9S-v}cW3cw@S&2s+Qy4mTMEWfVa}=Y~~=gIcLiF0CFMYCT#zm?tWD z{pp>JTgq(b^dmyQXV=&Sa)uer8fbFBOxif|Y5qc6wnqiaF5AJKW#c!L=?gv~w_6v7 zVy!stX?ho@1110XL;b7+!%2Q@?Lj|&m1?~jbm!dIU$OQ(cZlmx-_hNuX(&Pui)P?R zUwQJhrm`fKL*raW*I`7s+>P+6{9zc)1a)gGlt7;_C?K{l3|vmoc16Q7Q#04zIdC)i zo~T2OSs)%EV7hKmTG*9{d!H5wuea4o`7|fRe5>FdCQRJ!bhMw&JwdAucV~vG*s;^)@FzR zh8OrdP6Zr3u>2v3{j-%eM^y~PTX@ej_t}Nd!p!}&z)xHC&|mBm*+WC#P)x#jFi9I1 zE+ty9r0*Bs807}TVX7z6&%tz9gb0WfNMEA+Y6hSC+cI`^r?3HW{@B~f7VijxtsE^) zwIGvbE)ae{(~rZegCVph zU=Uy68vJlj+?Sj-KcDdm+S$fUfwn`7xb9U#1))K6SHoc8sG+*8&ihRNi@Dd6N|&Tw z2@wB*_9QAwx|4`p46r2FRMO;KZ+uHXG_>#Naq$Q*{h;RJO-;zayx*3i0IG;vhKo_N zRGnQVpUbFQ0o73r;m*QEA%pFr{UmUSlz%YezI;jmyd-wy8HH}e1zml0&Nzt5PCE+k z6=lSZE^SS!xxnBA#ry;7$9En5;ba|o^X2}SzH-nXFKQjxs6szF-{k))5=o@`I4FOA z*_8SZNQ8ebwfhs(=*qR206w8dhazNFU=5JmZR0_fsdfK+BAg$F-p7T6d`JWqto2{( zNeZbA!HJg{33XpOutH1r7O0L{N=r^A#>d+ZIWIJ-g%; zg62`eo7%CEeu>Nq=SJcr8CNI$ScO1f7_rX>%_@J$^}X2VQTuh* zn|PuF23bx(*hk*c4XXA&L~!$|#xrW>kP6uqFw&6jso<>w-Wena1KQRGBM=op{~aMu zAAm)K`^5L5i3F2F4nK`w$)blpa z)xWl4{M(2Zn_8aHY~uAna5Lc&X`7KhKifzpo0S+futnkaF*$M0)7$PIF|1Edvv9dD>hQ(vav=IDjW# z7l_lZ7JRDEZ0j@o#=^mz4T*SVU)?^20m`XHI|ho2Adw3P!+Qg{&RFGJ^KO?ZTGll$ zt*r^OA)G?yEgspiQj|coeH);(0u3d;ZXTTb=hmjC3-wAcHW&sp2n`hgjABc``~@Mn z()E5WM4g6dVuz#xOJM>GpjI?sOu~Lg2T)^Ae5=l8HZE-%iW(g{DjiQzyaIIQrlT&> zX;%iLZV#*zE~?WLR~AxQF=ri3hx#}8sL$@W(21?vMDw&4hSc=mZLI`#|8j>HKS`ei z;ZzPib2nmYRQ5j5#V&D0mEzq0|V;}qdb=$E+`O1|LQI&RthioR`eB3Z1sbJ3V4SI z+Fx90W=$9u&XRoc#5ar4l2Qjqcg^08 z{>oFoUc|Typ_x5e|1?I_Vb|vo&^e>G4X!t>5u1>|2zO}Qm(I{b4UH>IWi0IAsPBTX z1PjWSxG}ona*``}_G6LL=Eb+B$t2NdDhy|khBs*0ixYci>h870SCe6Xy>(?H0 zuXqmy{W6L3S2;htz*}w%`onKDJx1Eu;Lhn);%*%%4M(IilmZ@wTsMZtcS|5(VKjr52Y^Q0_M~rsedwptN z{B_&8kvZUxpTkQMQ?;oqagXFNs*QG?cc9f4IG}3|un@FMoYbr=jH!slcK!v1!eDIM zL0E**>0-fS>B-|JAu(|Uj!$R`!g6^!s31M+8iYMO2q@j4<`Q9UqZOy_ZfjRv${&Ru zQQ7DYBEofUbZ){<(6>!21}aBMAuIXd?2PNevLEDhFewTM_InbXNQ-pd zj};V=inB8;qamD={ukkazZOz>F?KKZ(0K5j=u~!n3w$}h5Mp!AnGA zHq5wm*rYn>`Cr2g3FL#_;L%%Xxy;clOlIhUX@XRvrv>7*7cwv2q8pFOB`VbT(a1MH zQ(u{>F5gyqGssnf6Y|(td%;$le=bf2sdu9v75X&9Z}rDhIkJTG{aoY)F;%&mcFrpf z&Y$T;=0-(}!b^~I9Bjh=0ZWpv+B#~uEyg_v1s{Or&B7B%TaW0fmJK#3Hhn zp|Xai(BJZ_Q{7}W=5+A4&ln&W!F4izCpQHZIdBh=BG&@h0nNNGlMJc0k7`zhp&b>d z5E%6IgzL({a^EeJ;7a9ULHw@uFrBW`TlT|wAWz8RtVe5p)gRBbpM4lv@&>GJ?yrxG zviw`PsJ1s&)~=7GPl??=%^WkyB4YqO@~9*%l63}LGjVtNhAz-U2VK2NJ?^bm189v&z; zH%+Bqng&?X(2>$jH=MiskRN}Snp`)TLN?E+sF#T~w}&fO15MPyKP!5y(^+JJLQ$X9 zOX(6!N^^|*f+dB93Za*S;eXYqQ5vExdcgWf#_ymkwmudf5O4iG2b_k6+L)JJ4T7Z@$l?Y z-=~Q2FQVIUsYqneA!U|*G^@SzOy2yrV1pZf<+u7mb&r*{QV`nA<$8(LlTNl&d; zr9_((sg* zz@+_LdUNk_xA!V_)V;`!dd>FCl{TV-Pn7FhvO`FAQRlX2c#+K4{uOTygs9qGzM8&j zg+%e@r(q_c7sQ*(69e#gK8tE9|HP=TX4dR{sn*qS_Q~!*UiN1nI5ZH&{D{ff_1jaA zN#h!V%FCCetHF4KwSfHk+En^)WiRYly zP0eyYVp9m@oYu(gt|&1rhmegRTQ^Q=N@&-+Gx94nEJIwbAs}=~Bq7dpHYHIYE*2TU zeL-#(9H#BU;+L4AYVf2wXA7o$l%>mPGyZSHuTMUcnke2XTL8NNw~GS)scUD^%gkCR+5Mu zI4zSRy!zx#oGg>p?}XMre_r~t>2RgyvN`pB=0xt0A{O?+n0#}%D_a4EpGIZ3IKP2u zjC5U0UiY305YWUIfQcvtDc}duK+*xLEkh>=+Gh+vzU|@V83LPWn>PcL8?wx1E;_!ZZa-xvu(dw}VX8 zB^{TL^qSd4MUvfN^o2BX?H_Q_lzju9wFMr0-nOu$8|$GU)8INR)?Sc znd$#a<(8mh;i9+*7JH+!yG7!-?z1C|iLou(fnERn`m2Jt?@g;IJ_8ov>7tP&gv1k z+T6lv);HnenkMiOJU7P8L=8IbA#VNg#=0>3pfu&5(aN#D!}0jg4^~UCiRs?eg9EWO zr54#=w0mBOPldp)Q71E90##t(O99YWVVhNqN!0X=1f&|c zi2j$3Z75I(z5Qyt4%(y=ag9jnm_hmN_rTl=@Nicv9GfddNV)eZpBbTFGLe~-?+ODH zRR&#UC2F-ovTb`!KB1r`UX#lXwvBt6QuxC-Q-d^J@pWJ8is2U-M&_Wo+EMsLOw#W} zjNtKmn4)nx(R*H?xf);kSf-p`M~l1kTb!LtVGn7Y_=E#FFCp7UlAG$i%_z9nz_R~l zdtR;kT+eh(o@6KFhq^?MaoAg_6EX<$b+u+UE{o{qUyZ8%wB}f^lfH?g8#h)yspzfd zjn_R}wc7j%M;hzi+gT6PZeZo^wXYMtH*SHT1ESJI*&R9B=_5BooLQyxW4GP zFN|_4b=f1`jqjSd3R-UW3v<)rb#mKqX1bVYq%iUv17I!fT@bRXO!lC#PFLgYmCYkP zz5y)At*Z1 z6NiUP!+bmUrkxQ?)UZg%u^#?^c}7rfBH}05%E0K0ArV0ML;t0gs)$}Z-!YG?5J@l2 z+>z|K@2IlTr0t4CHx-p7qly2yboD?IYwQOxqO!2MLK&0y_+bPZAU+7p;w6?Yzl0B{Z}RN-&mhu9aAfw*Y8uVaBK(vpDR)HVH4MUQD|jvR#xc3kDD zV2TZ5j^$#H3*i^NS=V?Af~^6&s76tPs1f@C&?z1KNRK9kU8Q&BVRv&syHBx-??nw> z(7=BCy)J%F0p}+RDZH?TR>QDkq@Sd!#7}<&>rK+1lAn+XDU$fYnisCt&$>YuS6{if z{E!C%gR|`tA%*Tv;(?vx$@!XII{Fxs4zjuqH?*cNR=K>cxST~1U2NAYCttakW3)?T zm{&V5L?fIubmY3;)sL|Aj!v4|ix>o*Ib2a~mtR<|P|v}>fkCLpPQo_NFo=MN?Y7bE z?D9WIb)5+4A+_3GF=K3FQ0KJ`!L*8Bh;onpuByoEfPWgYDMIW^2{C>z z?(kk@IfEt1lhBi-T!(mhE_Le0x+C)(J28N>3Gx!4Hi(AJM+yIKcX1^Fm1}-lfGu3= z`21qt=F#{>A1eUvmx0fYrO@|r@-idUS!}wN$zE?y%Q4F3qyKf5yX6e>d7rLs1M=Z9 z%trj*^uM82Gsf~EOoe+^);C;W`FphV;P3J=zvbJ+AyVvlXx#?Vy8Rj?CmNL;MJl5% zjNa^LOLuA+4^M>9H#J$rad&8GxWww;Hhq;%zB=vLPW|S@9b;$Gu@cS4U)OuG@LTK; zS@6ongaCNP&W6oX+^FU2`l%b|=+{Tf!ktL9ps~kTHJ4&%w9$d7XF1!#pNX3%4S8q{ z;+SW_ohl=k&kT0{*-sbJZPkp%7?1k}_L8nTSPgDl*6fx~=Ng>EN#M24L`sC% zuXK95k1_aMuMq(+l`u2Ox`!WV2Kf%~MOufFt+eq46~hr>asKaR5IK(Qn%)JV7UxVd z$h`BFd-6e?g)Fj@g;|(7`QD)|woCqzl!MVL zbaE35-@dX3(4Q*8K>%?Hro;q)LVN&D2=Fw!G}@|RqDBNbzFy+=G)zWlI>Qc+zeOI=4 z(E`g5$+0c*Fa6J&>ii&e^Ns3*BLwQg(4Jq@HhnBUu>&028d^Z`UA6~M>`!rn%Csc#(1TF zjFJ@#%R&2EVAZBWTDgBr|2FrjN**Lk(~PX%3s=o->78e+;i8G{E__FilvdghLp`5& zS?{Qy(D2rr!@vp6JDApXH@ywIx#`{V7Q=E3fWX549@v4r_yjH}%@3+dweA(x;GK5hkQExBAc#rpHPGIyIl9}=YEa)lCNo|ED;|AOBfHXKdrP}Io|Z*AX3H; zm;^rKK^P$^9Pb|H_9;tvc18W;&|8Ad9uwDFGU;X7j+ndw#;(#5x{>!Y z{j78&%^BKN1C6mF{Ku;>?78zDHUR|dV~TQ{OC=|kW%{oV9^78P$Z%yhzh&HJSJpCQE4w(e8Sg|47YYpd{>Ovd1bOEh;?({|;mxMHG)OU$EZB={-+!#M=vJ)i)=G7Hs;!P6%u;lO`0(n7Iwi+Nv|Nvjv7!i z>K#26TddCf3h8qIei;bad~RksOdy}#5(~@6@^|ZM6K#iCXhmNzdRPBmgKhn^@M%3Y zIB!As!9jrT zBHHhDh7{f2HEnK=gPqifxyv%v4(p9oR~4tSSC^kuq;`tMK7?6;$b&KIZRpfceOQq-NwSt$NV^{#jfy0=z>#+->ePu0>B1IS!hL0URK%!FxB>5F` zvsPxt76QRfohDwyBBH;uJ8Ba=u_7am&;CW<_pDI9jKMWdLPEK#K`2xRze~mbK;;~d z@!^ew`Xc7=_JgqiU}A=$fOj*3F*yxP1&{Vi#z_Wp%CN|R6&2IehKkm4FgaR+d{l61 zbq{tmM4O4Tub65G6P(bZ^gz&}sl>Nldinr;vp6puH zC#{V~Rnr$i94T5IHIl!14Gu$jLmSq$2>6RGhtHigR>}@7)Yp$Z=@ulNl{o1>BHHPV_1#nWgwtn&ht=e)qIm;De`dp{?)af z+YzQ5$vj5kmUa4m3)#t6ryo6|Q>7(s{kpZ$7h${k=kkd0Hm8eOwq`4P=f^aU#qwJW z{kc8f#un{Mnzvig$$m)0>)DFKT~V5yah=Wv7~}#}mgKqN4D{^+10fV8Fw_xrcZfGH z97XMX3Lg`Om9QVn=Q!E*Nm*6k=z=l9V1Sq)4;nD@HJx^yGi|P;TH}L9uVAo+1BFqM z^Oq!JK!5ArDs&0Z5x9wg2gV?Dm0k+kCxHSc@lK(G3zs`}Vcb&vkciZtw@zHWJs0tj zzxKrhhIY<_$5cJZD2}7&*&lw|^;xJ_-_7^XejElH#9Z&*I&%b*Ma4+7gPtLDS?9|L zZYP_&x62xDb}NlM2tPLXqqY0`w)xhbFI4_k>lIPLIXv(&h++~vIoeDvfFoVnvrlF) zxjR}4HJaw`+~&k;jZXQYBNUXC>JlS*Al^#R*oBW(bCb)WV=m7LaDFl8lxNN&}k!^1a#RlC5iHdo+?}>g0EM z52cgfh2W*x)jlhM3Y%5K{!qKW{+8hcMwZ5FVR-w(z)C0#m0sGzZbZY$6oq!t^K|O# z6)6q7=E>`kF9vFTg)Yr51v*7Gvz7lmeOwwZXg5LQrk!(M$1Oiz1Y*>$6YCcF5iYb_ zk)<?Nj|E4}s1VnuQopV?DV!x(E`{FK$z!B5&d6UAFMMNRr ze&ZhMNme~8y85BdB&kl0ZAOLG17D?4`O&Ko0F<7rHf#@<`HgWC=Ay1$yj9h(vN(cl z+b$ygnHZERt^Kad)Is+9xImehjC1m-H9q7p4CHWKL=wYNae z?`b3}u`npd-&6qFCrL`^oBe-dHBz<%`9}xwbNP*1X=igfhSIJYI}8;#wY*+xe7oqe zu_27!5`$bhnd92Pf1znFHf1|DAgX-KTCO-pBFd;NOhGz7Ql$1sk?A44wcMe46=m4= z5}kGhB`y24&9USPpTBRbMg`R9{$>_Kj#|+Yly|6r$D7nRAf&M6sNau8g|eSxXiZ$( z^1*Od`pa{$YC+xCT5RQra;7};tzw{}a``FU^}cR%Su>FDWbnDiN4zX6cf6k!4Oni6 zV<1~s=~sbuie>%8S*b4PMinKWihM|z5f9VS-$-$ZB!D&fB@7B#tw2TZh)kk>YnaUA zU;{V@g`p`NuZM`dhxXtXt=X#mM(q~g%o3&{LTM%jxE8^E-QSS1&y|h|3tH_sBO=WO zRFjJ4W7JuzA(3;_Wyt}+6Qhd@+Xo%sNvL7mk=(?WNyZq$$2{9H*tjq0ToVtrx$AX} zCqsSSy-d`^W6(ba8yAGU_x0yv<&3)Z z7?PP|u0I&mr^tEzsn9eYc0JSvhUEPs|A{hk_Fa8o8r>zX$%pZ z%z&h&m7mj}qs>wFm}z%vDISP*x0#yZlH~p|NxYq?V$tV$7W z!$G`mk8_CJDft|}My7$%^W&UOQ>Eb3XCrUx(fwS#;Lw@S(<)$5mTCR(xJI}~l3WXj zOwlweqN)kX*=bl&}**FdL4xVj)FjThv3Z-NW|kkykcEROIHF+rMPmm1Qcd zBkHt3eju`zh$w31IL(K$WQx}KuoO7KLO>{jCk;!ekdD=G7<4er47~@zgk~2+t`#32 zkdDt-=}c=jbnlaL==I7GsN8$bYBfK}mHzHcG+gM9FX1D%uVQq>T5 zNJB;XU^TlChmiwb)dSto+J5xTp|cl!6uz5OX*Tzlo1h-G04(0M_qeX>yQ1?h^uKH4 z8uw9j?5Wy#QqDs>%Po-Ssv4B*a!|*;2b<#)t_`oC9MtiB&zSCt*_NmrNgtdm{2`@L z?LK1SQZ4v%Uw5&YN9}8UE`>t@yF4U$IXq#Ph6sx$MbOkK=`Nt8O4LqEI%n`LDVbqV zD^E5C!S%n-PjLV!l3Cdg1~~+(kK#7H`0jlO+SblajKw{IBq_*~JYv;kHCHrya_C>< zmNJZ~goYw{BylYCqlAb$$g{)uybT5rRw)75GA8m^yd-Kw;T4|LnbH%^;i- z#Zk)SD9Su1{ZJALz%uPgm}X!pAg2@zY;(y$x{H4%fy$l$s1)>IP(1JK*Zys5YKmqN zuVX@Qur7xfk3)0#((W&o=lG`!cO3Xs2nEV;2VoA89~E8yQYzD(=B2Nn)Rx}SsuM=# zn78HLlIMLSXi4i)*P{QtH}&?+;D_8_s&_9`Dw}vm^XUKFDfxm_vB)UiQ+-ZCfVf0I z(+%^oqlOA6!FHK}KKb%uV|oi@W6EwG4&h^7DCnx1?92ZCpTSfqv-eO+t;X@&7wm)QY=6j)YI1%scvz1rs{^ZAFv*IxQ(nZ3nHSzS{fskjr z_gzSThp@98uH;LO;wz0*6OAehl@41&_DkV?>E+88{ifjl{!|Te9dSFAQ+@BSIDyOd zWRz!Z@Soh2RNbZxUR0M|9r(A`&$;J6pRro1mgV-7*y4twDFg8?(xGvs;ou7bw~04- zo^M_V0O3VNs2sLx5OAq_jOg_Vi}=Ok7Jf~TwnX!^WUM=FV|rLf5)+<8j`KeHqippT z@4t%4`)~cnmzIFoz;2M;T^$y+y)89F$QIrX!os4$G%;0Wf+O9$79d$TNh~SZY1UQb z{yuTRVp-weO~l+cH-SjBxj7R3{UaJGNUTfK4=nQAO~P9@FE+Tt+@^YCzwT@NWC zf{TfQCXEW$S7a}!hF;$!#9B#O{;DxFz&h-ZCGmf^YIDS{pPKFa9y z-Xy%YmH)`4`}-rlu}7g&QDwE-`^Q7dT>l4Nu|E}E3jRGG&()BZVx#9r(gp-?=1byY z^Tzg67u-gECt7A*GYLLAQ}X^yPk&jRvz3~DEArNuW2k7Dn+yxHFqO^dtqmdJOW#f* zzg#X&1VnGq+wKnJ@@^idj2v7>PND@KljUYD30;l}f2>>GHE;?lJ`otc7iSQ{#5zuo zQ$}_|pirRfRz&Y+tN`VoP;X!`x^xgwA``K)W4NX7{3@_@I#jDdT<&SqE?i;HVZ_Xtp0-e^QW~=M_4Dw2IZfH z62H?pSgJ_;!}ij0O!xJ5Gs5}3^6|-ar=JP#4j46`f2LNi%TC*!vR?RdIhwAJ_?kAe zMn`zq)FIU;@0Z}!KAv2XmU!&g^KiR}pH3%4qbmrjQR8|T7wJ#w(6v@3)}wvPq%Uib zadc&7<(B{_n}kJM?LrWLnMQ-b1~}|NAV_<1axYd`B&dUeJi4zpqg?x}+Bl)SxkvB2SKiWLgalrV;i?bK(4V_9iDt7TV=n zI{?!2k!HyU5Bv`_;^Qt}n`L_5&H6_5=LoEO|HOobt&D4zmGi9T47D-qmGEla3h*VG zZj2^YVj7zi?S-1Ku%eH{nRjN$$cV$g!L^Nnwmzv8-hdR6CnvOrxSHJj0pg+Ke@PX3 zyZQy64%O|e@2=NzFXWHMFew3cHUmW!d4Ja<3sLKuMNBHPgk>W6RrjIaJ3PE0b^GBm zQf_|kL6?hz{QurYJkc~pE*`YaKIiKoHS~kx4*x_@d0}RIYW^>|* zSH|)}z?3v1=1)v$Y>Z|P8+}o?N4j~C5=_W6NC7m6$AyRe)eu1kN18ZmE%koebxCg0 zZSTPZQe#{q(}l=`G=Z1`0f6E}bpq#ZbboAPv2v<++^BRMl+8dADUn(QpZgQ9Y<6;MEA11#!p`i?xK zW}lqmuH5kERcte82x^C~lvMQdyJm?xZV3?m9QtcW&*U`u7N3mzFgg7rc4sApmHB%B z)Sz0OWOdMK6&&)~TspJVUHEuB4-DQ4TX3YQ28mw}rWtv35EU)ypf3REUy1$x=-27r zm@GpJ7r4D)>Riap?X=;Z)+lpg8@1m4M(}L)tN(BLF}%e2FDWZ_Ke;rX=MHNw!N=|5 z%}?1_$e8i(#?EhB_>(MJ#GSWuP0l8ZrJsF!7MbFkvdTO}hHEqP?gw@i(zYU>plLEn zBZlT zA*CtQSWx=AUKhGk_c*r{nDIvLAGOT_+sWZmUgq9khqS1z`NuyOuGIir$4x}3wAWrA zfBmL4?18!=(vC6mOMGbAt2xp9fgB*ax)B&A7naV-(y=s_YZt8h$30{9_E_mJZt z&BBW2q^grvVfeb*4pn)b9Y}C`mTX9R__}=k=gV?~geuZ{*;{|^R;7Ou3q$@G*=M+? zgMsyhs~$myBR`n~M7{+|C8F9Tx$>?x8Z2KcFHGCKn8J2@s2;C;zxJfoNaE!)WFjqB zINsPaPi0ZTEy^{4jOHwBLrlxEUlCVS&PvGxI-JzXub0k=^WCVwq_0)Cj2snE5oEYU z5tv%^PPk}wA)s?_1^-h$(mX(!x+?Pf9>?_r1vXReC)-Lwp! zwNGcp@~gm}Y*ZvEGe;DHpy_>M=;L)6+fgz8Ox;bDhqTQ zR>FlVlW@T08)7n7WEo^3~-M2kCPFNM~tdw-qZmm z?+Ji?3eEl6)$JKMYc9tY+r#?tur6FJB9);l-dG-7#idzPsfH7dAnCom89_$_{H~G# zAQ^6hmzZp?Q!_c?N_zf*5dk5K*XBWU{*?o6_UyR(8B_S3c@mgT3@e>EdvLI z7+tK%IQ((jzr%pYhb02Y;ST9AMO6a@i852O%)ahZ^mcgGo# zG%^^&GmJBi12{eFv&-CkxnCmB10%RE9mfJ_LlB3oz~dnd49_s7;b@5HIvK}k&!pkxYw;`HfHuv*gNs81s#aJ;p22PJD0@(g3Wawh6&J4<5rG`SKmuL5^@4Hxm1p7a< z)?K3g&7@6M?+jl`6`KH9I4ezcqlzr{%Vz9@4q9P^XckH^r7*)KkSzzDSh9Df^|G#{ z>E0q;d`orE-)#Q|-R_my=wd==9Tfl}>zu?Lwf;xYbuFvNWJH(imen(ZiamM78HJpL zXW+@#@~~$+h{7vVHqXO9j_Kree1zXl`n%%KIjBOXNy-!d99k-MD;=nfR1%+sh>$3X z6etWb69cm|w&?Bko}+%yhVkpL2N~gERgotqMk>WsixpU^#w$LSk!YgIIKh^Cym!Z4 z?hBywIij?Y3}}sxp$!ZJAu}lN4KZ?JH0NQr-Tt=c$M58C9hhO6m>Hfkjv?)!arv%0 zGR2WqsFJ>cV@lM2O_DLE#I!L$DecznB7mLE8;%0ck7{F-?ndfHE0TWC}c~Wt6bARhZH^G-Z<`9i-Rb_aCF= z0r+eAx6R(S+pyXE% zJm#l1LuWAzVTKuFyHpImVcT8Ly2K9jPcHR;wT?0Yhr!Gyu`;&*h24kf%F)r$x@J92 zae4rVh|mY>aj;wioDWXp0*rDM7Hy>)78$~+UB(@npdb)joEMfD<6Jve{H~W%^N{np zfDyAdDtzJEee!v~H)j@H-mNjVx}3Bjv<7Qh?A7(6<3oc?ME3o+3DbNHH9>mc?_GYt zWx5ys$A>=0x99!$p+6(()zpAM$V{);u*?koNMEb2GrKbkhY^Pdc5w-q9A;*IlYrtA z2H>wFo{N{c4b#9gG8i{+5k0~X1FkPy3M1KUJNup){|3&2VVRZF2xBuS;|?&)gv0qP zI%FmR(h$$Xz;IRu6MuQ=dyeB?tL$%k_|-ew4Kl_Z=FfA%X{vCC3znUK$kSHsDYd{x z1i5!c4|*6-52!Fp%BEEflYaoVZ=9U7<3AoB?1t%A;&c3a!E#^~j8Uu_{Xn!}nJQ~~ z{Id#fE>>1^arV<7)1&N^V6gUq5^G~=8OCS!wfBPdT(!rHwlU}MK=(11`n=9Fu>o3k zrUu#TCA0m@eZ5Wn2j6&-b9xU(wcE#Op-cY{z}(-@%Q%mnXxk0b8p^pR_Py`D^s#;? z5BNGCHs^KjeLL))d(54P`L|ffe zvjejsvoOOmFx6M&KV;L7{yw@)Xj+_>Dx}B0QQ}sa4Kp(X2wcYoedizIyuPn@g98lo zE)NR>49q%a8O9%icVMzMmkL(Jp{wCOFP!E0X*C z=GQOqY)pAVhPFUM&_j&Q+w9*3_T_D0;Z6BW%iLIM(0`@Kw|n+>I&}5EbzA8B>{Y(U zxxKPQN>u7~Z`LRnGsGwiAPb8sFGUKv#3-_)t~vFshdd1@wXnXeY7|`Pi*se z-Va<(FLrs@tk3)d_`_B;)8f5-)ahHqFq`Uuw8JxzXyLgnkH^TpPhht>RK`sw*={k{+7iqGlf z@E%9CEeQ*LgT&_TtMOv${rvv;h?9lM@2=PyycwE|*CYSZkc+R#jQnZ392fbGo455o z&a>6Jb-W0gnj7ZNa}^!h=H}z9#WDM*3<8-Z>mUKq1quLA0}N>(88Z@17Q_T(CPHVS zkiTMjNi(Rv!!iXL0%rdekX~Hv^m5&aM+HAp@ zm}X`}bTPyxM3``9W*i(4ne|K#Gcvkmagc#R0s@5rLWK$*tknmV^gyn{C{VH{rzXOM zhZQ%N&o-HgzZ^A^IAlGWSbPg}bV#JSr@yh{w!-}_gH1LAJTh{6{C6IvlKX}|QfmXvxO(Fho;hT?SCIqlP9QTZ)^NqmaLVvZqG%Ae(`al5D~pKo11h-1Gu z-*zzhm(#e1-kR~T6gBeHC@`f(MG%0(%;-Ae*uS!`$r|%-Tzs6i{hWTqBO)ITQ?J>3 z(z>KigCxFYM-|}7Zr9we_~f79SixBxucN(!k}w7vDMI4~G||8uEt3Q9FTKqEaqLXB z@=i^$Hl!GtslMe++}z?!EmdFvw3Gm5QSPugFVnvGK4yh>AGBe?o(wp^$e89KgCb99 zw}W;a%!Ulg^-PLzW@ksg)({$IeM=9bVc;?zGmIYHTrE+hVzF3~$gxslqL?xhFwD#k z2DNv7cJ$x5@;xrSCf<*}^uD8||6f6`sJNUC{>pjNcJK1#pS=P0Iz3dprJ<9AObW1U zhZ|p=VB@YgBaRs8YOmqw^7S_R-qY>3{;c_X(54i?n+N1F+0IpM=hx0K*7F10f7EJ?nrtp~B8E!Vv%c^;^H|@OXY7 z2dnsgwCTJLSd;>Odh7KIHV@!bFBX}J+;Fj-P11J`bAzm_;v4VJj8-OTod-3ggL$Iv zUAj7*eCU|y0bq4ofwE60ZgFA079sBD*kU;qOIujI9zEk7wP*#9g)4HV zs0#&xhYD0yLnsPiS_`xHN} zs~{^6Ndpmu7~M`kNjhby7RH%OA0i5nDD4zL$RPwHGa==(KKpx4!viuKK4y^860LVOgLL5Wq7q$a7~|yQiMUXqm_yLVx@`1T;QN4$RJ&ahaGxVVGf* zn8q>X(N7K4S*p+zlqX$8;ZPG@@3mFVEhXR&jVIfMww? zm{DsAIfk4t+@SjX7Ys9l;<@yHzHTQQ^SoL+_E{sgfi;?D`Df_+L$Q9UcfN@=mTs8E zOGcEBFNJ2}S^Fd~xR2#njvU2uwWMTYF^>wx)A8lf`)M$JlWOEhO%X6=Ya2 zM9g~>iMF~LNi;JAAksgaq;KfRVD1=Z8QHY+!O8;&m&%N?7l8kry zya&S<>g{x&qa~~k|BP2pckWM}zw>Fac;+s8om~G@;(e$8V0XIh&4Qa3tNklVuiEQ} z3{+HTaLKR#PWeqkZ+djJ7%EuD_gl}BuSt zY?8?pQ9nVEkbq?nsGulN6=cz@e={hQ;Yc}@`^v>N>YNLw&x$P}Weyjgh5E;CBexpd zw)`;}41;-UCIsCRoHKqLu2*A2;NA59YtEM=@K@uvKSp*KzySt;;uP6PfRCtA4JD)q z6nlVs4iqsxn-~FyTvi^tCPnh1ofg*AY0}#$rsbN8Wdp|jTCe`l*P?*gr_Ettd_2otuqVz*iO$)UTR zK{kODfMKT}CyygbJv}?TpGIM#8DT9IK-m$5*dqV}!9Y-<@`K6$SNXO?-I2>0W<&N^ZyHI2SjI@jaIBG5JvYsEKd0Zsb3)h@ly{V>PH?wLHYXBo;MTBX zS-_d!-p-aQ-G@7GuLRidn{75p*|=hxLSa;f`l6$F#Wbnr7`B$(pHIC{%=&2Y&p6(d zpKExZ`}_V*ml77)`a#FNmP|3l=vQ_J7~_z7_`#q+6gxJTmdC>=^WUgjQXbE>blXhs zFZ;j8lPz3%s;~Z$(v+ivHpH5)+bHE_qd&{)m+iDS)p#kUQZ_zMDHdTWmy?tyZJs7J)NVI z?2P_@t$m*}fHNWM#&zrsa{A4{?_rqIoI%qW%% zX%QpTl|wWVvoj)i?l=yi4daW)7wNCz!LuO@%sc~*4@}5oG8%Xs2SYGDTMp3Z7(!%8 zke6@I?f74BySu<~g#bBrGuciY;9 zp#4aFp^WBTJcH&d7c@heCc( ztbUN?y%sXef3DF+W=1UWnNfyXTvzjXj9_Ew)3z&Mwk_pv&z79KHAL7cMKsuGESB-V zTaI5Vyt}_x=w<+(G3NSH;iZ0mdpPUv(grM`Hdu1f7H57~>o%XgI{(v^f$DOxQ*YCb zg=2Fwnx2+^VsQG-|I>Qt>;JdP|6lpfRk#qCW>=_SdMOPu5UDiD85S#}7^@mt72sIs zG&je2{AhL^GMLN*#ew|y91#0i<*$FkU+C;`bd&IKF`0&DLyI`W{Rd78XmsO_o(CNa z$bQo+g9uD8Ffl+-6yGwWomB+&*@aB9+H_AMr_oFLSHMQo>30+!*GqC<>z!!)35Vx= zwsRai>N#B28FLcqeZtN(!s>M)!uDD+BfBHJtav9}(()B%vdlaefKZOdls65spKYb3A$`p#UT?TF%z;o5ju0{{tN0Pg$2U`D78M1M%Xiou-L_{30{{H}jrv`J z{S)GSe_;Iu@Wj4w%C8q+z`6Ok0=ez6^6u{Exlt^GTzNnGT9f4QW$IZGo$F)r^6JE5 zbUbN&#y+FsIx}s)FYWbv(1sXfH@y>zz6m(%fV}d4c1wItpnBSQy43Xf~+2ic?f2qvsmeg92YhtI65XS`~>g zCJa_9Wn#3kRf@=U?hk;@oz5YQhRowL%*bLA4^+$IP^xxUfoiEvV()@!DemQ&(#+h* zjXU3YdcOw$!@Cygx2rkL9l=>2_r0tix5b;R*Krs`7b&%)yaw+Ki!y-DPJ*N1#&9_| zs;u_v>5%Nx|9xmPWZb>HATaj*qyA0mqFO{hf871V^ zOOnZzIR*g;Aw>x+^sl}9v%)}q_oLuLg_LGthk=#EBqkgghC_P@b5HNPJ5RFD4Xg)y zz=Vbg5U-;`5Sa=5#}MlT+azG{UuMCKGO)lu72@NenFt(kafAdaEsjNi3j~xPAyGo2 z0~aXT8d_;P1swwuEp4(dv~WFb(6$^grO+OvQ!u3u;l{_Wg*vQvWk&HMX1B*O8&`2S z$z)}Phgc4Kmtt~npJe}NDp6QPC0aQ)uMdKn&QadV0NSgo^ zL;(pvfuv;i{c|A?3y=Circ*2dnWsF+*T3(UIIua6XU$=x_O`4S{Kt+agH^x#fb0V+ zL3jB6Cv!j8u3iWAc&F%k6lB=WL1i-_p325Z{4%d_{WuBW+Ws|k{{sRe~p~fO7nXJ7>mIdJ}Q$1CHCb|p2^+xgAFc0HRPPOM8z__Rw@&}}jFf1h%=jZePN%Vw| ztbvfv$-#`m&V4!b=Rk_)rvs~BS<~^X_ozn-&aacygLHv}x#N+<&sVc-!&^Jq_nsBR z;xReP)~H?at6tLa+Ok05z5SDA=f2D?D-;xpL5RUgZC9IMMow_?B~mz9m_w~(afU)dLyX|A!wfo{X%5SdARJ~8^4JDp`lZ>ZKf%7uvg0t! zIN;(QLq22pZq9*$fPGs(0n!tS;~_sSz;T)7@;T={H;MV1wc317PtB7uhyi~^1HA5?3w(>#wrV)$#1n^&6iGH zgDc3dIQMPZFJV{3aD#HL+f$WSeKe!++q@&$!}XiV$^a@3R4eE`TU}CTaCk=Mn=tP6 ztjlHcYEmRMk10;;7@0uBfj%n7B!mDj5#j#t&A*lR9#e;eV0W?;41^fMkUVBr2TUOh z$U_XX-nbuv_?@^q?}y3Ia6J}9R7@gutbYApt&1Bq%rX#!I&sEkW>1wPe4uuy?8-IyAaY@obl3`nPD7V>h8 z1QghTks8z<-IJTEuN<+k71x9V(WqG;e=EZ$cFM3hBBHbk*Dt^QWCj znXGkDJqlO0p^dI7SXWCu3He=h*bd-v_}#gE?*1Ex@KREK+iccaDOYVqUb^7Z9qJx3ZE?GtB7CCgvc7K0Py_`6 zhYcbSZjdm-CiX&Mh8!Gm5)2u@agdSe&2v#p)M-jSDk+ToMv@XD0cj>CAyB~#=y(N@ zl)>or!QVN52Z|do!y(9Vh8zY!G2+nZLSdPh8Q+fFh6>6Q0D!E%Rxsph%CcmbU$@VT zG}$KU$vR5u%5Tn+$tG#ffN{)q-+{ow{~dft#iMjUag^=0tQ8-Rn|4?kSLd8FFLNJ7 z`_sbNS6o%9`bT&7Yjkj^V&9$Y(vO}*Ld+kUK;GI=w3<4hrAF?h1USv4e{0Ir-}PCC z$?S3yCBN`;R3Be~v6z2zGw*0L=WI~K=zo}ea}M!oEb3|UbA4!=-ll5LVNwcCZ#GhA z5SZ3geVJ5xfWFMkCNQ{M!maZ7`xLq1-tTRlr5!`vGS5ej4~&Zk%n*j=X0}-Rg=s}q zRap{gyG=idvlZIMQ(h0%Ir;kdJzV5(EMl=Eg=(b5im|B}mtOg1lP?C{wSW{vJ3JtK1{W3E@4DuDf)D>SCySrk+#RG`^fX>!+U=W&?k?7n$y zCcB}V=wN=2Z-=eD>Fd7^4mw_Y)5Dk%W;CI+f-L1{Jb*Az{{;ye{_V-QV6;?+M#Lgi zBo*t+Cc?SS%|W=N)6#rm@R+FTHzm8Q}VyR z|Jka?*=plsVLG1UFblD3>FzrX*H@=P%ddm{$y2}NdRaZ{I)AT*5@{O*QiVE^N{cP2 zgbw#5lBYn;$ur;){7dvcm)m_p6EnOI=QIA>v+*QN*$t4)erxF*70}Eu%mc7941~k? zEN!gkdP}Mh)+6(c^@o1wn3dy75a3s!DPs*QuoW4@k%mwR_zVQr<6Y8wU#cdG$={u3Bs8TMCY$=2mk_e@r0`wZxJ3^5+_gtw|z7Oam~9u;U#Eb2r_@BuSYF8I)m!CK-^G*`Gb@ zxHRr$2!xt7tqkSt&r?!;^h=9nOyY(u`T19xbUb9vnj^!dr@ZJ7hwZ^{gc*vr{=a(x zfs#U^flA0CY9vIaHSTYN(fs+iE#D2sodG2FqSqXO2=rpTmNgUca8e-P>~#yL}-t?58vaD9DgA zD=<3@Ap<{@py6JMsPxu|Dlv2&x8!c)^Nf z6tHxrOyo}b{=^mc@ceSSu7~#dkr^yv@!TmCN%EFOF>q^ehrgr3^)Y@`a&X-`oTmmi z!T59a@8$H46cKbyjTiv3SOG++()CMLHViIAwvS=RLkz8n{^l)LziFz5YT2_jF&hg^ ze}1vGb!&M_4S{)6#I74(FP3piI0wvDAYacLRJ=d!pLaX(#s=1W_Od>ox$SxVpEc+n zr{~sr;K0s_>431cSJ6|UQ>Z~eRNyBUa6ev)zfk2RmO2a7VVTK$ThGrRt zalx7F-45L3?1l9hfNy8(nAitpU>SvSeEM)2^xdv*_9GC1%omy-`#WE z`=4dC`Hx%nKo((87+_Ga!i|)U34kgGu@yks$#Ptk!tYnd`81gs?ua|Gq6r?K*R*g_(fCT9 zK93@QJdL@>1LC4H-!lbQEbVTqX_zww3+dxkzH{t9>B3;<@e{`L?z#Q4z8wS4oVo1p z{$)vh{$8o<YIt0OmE1RR&z^KiMP?HRv}(i~ps-{jmr8ri>ECm@=33;hG8 zKZ>#AE#;o}Ns6{xr|vC^1yg7~&D&b{gZc59;3HIH%3+iq(s>&i3L>$~K&7e1Eru`Q z`X3N`;BaPUM3@|8CI@G^xGd1@KMC%f zeRr0*M>elTzpArAwCr)`>mbs@$Ucrq94xNWIdrQXc`-iiWE}01ab5J)Q=P&59=LR-~ zzCF=6u!jA6v-o^=-*-8D`Wyk_q0UG2e_i7h93LXJe1R_o3>XX=*{-PAfmu;|&z?~E z=oou^kEmeaJ9fOp_Uc+V9|t@wMI?O&3f%Q0*F4ojMGn|17I^Y;c;C~^ictGpOfl?2MKFF+Mx zRi4hV`0vVC_!5_p)6&<+*C z#%1+CHy7xvo;3Wy^qIfh`TOUFue9!!m4VJ!@&_2o-%bK)fSHN3%c&4BF74rO=arMo z-_>qm?S+y8kA3+X6>wkCYN%ya1$G{FqS6()W6LHSj7ps*=QY6PkA?f+kXXG zMQ+FoAU%~KdMICv+bot2NVIfy=9x8Um%%`2&v5ZhLc2E&K=DbY`eTJ}5_T{&)lJ6e z<|7W`uN6IZ%NE9z1$(FTE#ZGIE}{1sram&?<*0N$P(LM?(Sq#$CnR!MzAon~QxNg! zn_~sIKKEGTc$fzFkB(w(m)-oX6fI1$!o9@cnd^z2bdz^ufLg@d8M}}WpesTg7}fAB z85xl_al|GNm|>bfxX9AJ>GMyYaQVhOKTpXV)=1hdMCw?X36Kwf@^OQ&&x^xh4cUk8 z7;&Gwal|2{AQ=gnnUOjk9@lSJYT@;G8P!A=+39AmWnsMVo{Dk8S|Vbag!5MBp<~Qv z`}uQ&W&xhO)o*(2814;4%j&^O_j8h5Z|l|LqJJ=epfz*zny@%BhJ+t<0)>-UD+Pks z|42t%unxswx+MXJC`pJaG9Qd6U0BzKS}uy=w&=-ZsIPOzh&jwgs!TS zX=Tx!(>Al~)nrDm_8>;L?N^xhwHYD(&7aS?l7Rf_u?D%*ZpJ^gl%!r#it=)i^fu(vlwQDVVGnmf-54_O4InyefTfdaU*_Ff@B>QDo9uD zUT9=9Fw8O&Gc%?gog!l)Gcf#(wshxL@j=rA&+a>K#L?UMzYh4ny~=!*=O&{Zb5Wa} zw{p86Zx|S#m4j&aofVKbD0(200o+V`{$aswPvMEp#8e~e&yQdk!zMtLGN7s?QSa~y%FFY!LQv#_fvk1#37@4j+^Vi5#B8)i&Cq)P)cD@1lsG=_n-^=^ z-(bHWVoxh8YV3Sk_y&ExwncBfe0Nl`13C)KJMh5&fH-SOCan$Yer>^_l)9Sn$Nkcen# zV;CN)^0L6=Ha*^3z2EuqQ_wZ#ytatAWljGnJvs}n*GFv;8eTg(C7+-FVvJ|#AbcAA zQ$9*3mFR2=F`gTHBBUQuHp4#ZIU|!9#gDlRhGa%Tng62Fq-$LIBqoXjHc^9eLk!@u zW-X2lnrx)ogLnDnPV~S*#pW;mFW-&Um@?;k{I`vZn`iFa9A2Lth@de!4NcJc|HjMv z1K;zuk2<~)>HOP+^mZJ)JVqHKOJuq`wRy7{?=C#=v7TT0Tl~SY7BUjbdz`dHDdK4_ z?v}FZY15{YAs$~@Vd+Uz$JG1#JIm64vg+PLGc*0oW2R&YoCX{k#gU5@O;$TuAJtdg zKP~rjhsp0Bvs54XI(U@{hqqx5Eg`v|3h<$dIvM=;OjxNbY^a}H`vylhOo=$|zh3+A zvA>tw;pN}(o#JO%n}|V=W$nWeikNM>VbneKRu0_gjAbr@(eo}idt;2Sexy4@GX($x z`Vkn(EYG#M-0tzUIRTB4l=Vx)W@Fr8pbZH^2IDSjMzZpSK;#+$u|p0}9}N+XEE`Ga zOgBCrY3!E~BBG|}0U9n~r>ON#m3<0ZEC#;P5YG~nBR z11^3m^*9oj&+dCJ@1aHZZ*!cJVbOn^Bd{{#j#~VU>LudhaHsec?t#j*+s3?ZQm;PL zS<1gjRZLS^I-W(fGKE^1VIBjL;+E9#Ds)1?mXfdlLN7+@Qy96V0I~;pB4EQz!#(3T zJ+mQ<&)2$tU$5RYsW~bt$LKW@aFU9yNh-F+<{ujP=O2l@ahPNzBqT|OXLwY>Pe{Vc zpES3%PT#rWy7*gE8xQbB4#M?T?2)rEQm*GPr!uP56fs|(LJabTon^< zcbeGT=kZ+LKR;a|SpD3c!?hWcX$EodL)gBBzE>@NZpZ_uwt^Sg^xi=x~7oxK?aD~C@mc}^E7s7=O^t}T5P_;J6 zqww)?es4qX^)=K-^E12V_h`@Zr~`bV-VjFIp(R1@uPcxdaH*`q1;hnVvJaakc76x! zQ^AL3OuZMQD15a(+!p%;!Ps~*r(fzkLs|x|Z5Dr@Mp)*X)ERg@lKsfaoArR?PhzBU z#t_AGtS;?{KC@U};{<}w?m;u^k}$o0`1Z}g)TP6V=>J|H|EMLLoz+8I^g+AF7HE*9 z!BrYegvzOuUl~f_8+lbHXYXp}X{eh`QwF@iWJBt*D#+wf1BYeO0>ibqd&@A|zu3;f zn(uY~9c0(Iba!33$l6Aj$v=qFO_WK( zy4#q4XJ#a=F^MQB3KTe{3Y07;W~r<{0SF(o-v*z#`!ZRnd@ZMk;8N?YdnnF2Z`hYMyYT((i~~iFBKK4z^;LDAvrhiMMW0a@eRwYiYBY_Ll{=am8RRU$&kE9q zMr3z#(WJrRio#N^4F&%!ao1Aw5tcU#ZZdJH7(U`%ixUS=0f&r~IRJ8o2G7)%HkgZJ zqw0rzukMV$xd*+I#=JP|;k$cs*4)?k9^DPb{P12oG1p{1d$PB7Y3tQ!0<5qwtV*3s;;F;ij#XB zI@>gpe_OlRyvFgRh`2$aj7o?^W;C>NIvz7s>kZ{|)6Vp3nz^TotV;+9o@xq#w;750 zlg)Ff%dhTmEsv=I{z81I#zRB(d!Lq~DA|R(SA-j12bIxguff#YdURR`wJ3hN0Copr z0h&cBg-jIo&jKY0C<|lr6@&aR3XKvU#YZoO*u!ucGcL~lm=+BorUs4YMqH_9h-pA| z6nwC?EFZSxsC+CHJ{?CB)=|3|2`5Rrkpli5Z zS$M+!aN2zM+~R3ckCTk@_>6Q|N_6ut1Xokj+_%FVS^CbrPOa;vDfh|gXRNhV!P_Bu z%elymh)~LJOxeq2SqoZxbr$0>QuR!zwX*n)HOiGYysc}N=X*QbW9xFe3B*3PtLhQs z8P9=eC}Hkg&11Y^bPTM>L*?1~#vO$c%Wgg>em=W#1~QP?#zUT$xijhk2z{4Xt^E z%ZIaIsvNeSrC9Sb%^LeI&4OUiACUmsPVhzDE}M_S@7g4PezxL;zSJ z!UNJZH5TOo+Y}%v$z=Rl0~-m|q$9+4w>XS;WE;uK8RRN7A7&iYHid)t8?L~{)o2fZ z?qi{T|I1cfZA~e;uB~JK-fMN*a|e_-#hg;SMV`A&CL$(%;&nyVwZy#tmVI# z&k`PeHw|Nel5lb?Hhcnnpalv8y1AOMa7llZ#>UcU?u8j7ez(Dg@H3my-t(0-;MY1R zk9)K-4;=7&vy{^`t1==0E3kfj#V>IXRRe;oU@he`Y&qx<+g+$5{OjcMST`7FFGc(w zj{ClPeK*o^?;_-}#Hy+D?WtBS@n2a=$Ifd;niaq2D>htLP5+D!_uM_NsJHYT$-h#V zDVTf@=O}1sKth1$D2scEx{sj3Zn7607kAn|{)41%P4)*O|A=C-xRyA*tJ_L3Z9z5-ui=1tbfDL)_n6aX zL8e+E^~OXdgqlB$++}%rZQlro!aB~;@{MiTB<6IaV-!xgZ1pdJrZHI}Raz*q;`aKi>_qvf=UH?}pS0=TZi7+JqFvSvM;*x*p;mK9{#YwVC(#HdZeO@vg z3rgf0A?SkH>Z3T7RD<+y4yxV|KGgK^o7XyT&($KJk#PIGi*_khiUD z!E26w!{9z#w#r=Zi`=F&$o1{u7|9>jWdB=}^h0s|CvnZH<{~oR`u70$WSJ9DRXk0Z zsWs5_oPSdr)UzCDoyE@Hd<`GAg_GcIku4bM*344yl&~1?KRmAKM5Y_12y5P7(ujeI zL`7s0_<>`-zHmLo{ep+ojOz*jXsXztR9wcPXq2|Zl{yWP^B9E(;u=q@J(+MXmH@CH zUU+3qC|YNps!~vYo2Kt11ePIbLqtEFo9=^P!bXhI1hOJNOgRyoOnR*K>Efd4L*a8(+_kWnV9>J zcwT>VtIFN2@+@4KJD&6!hPI%vV0{PoxE-rp7aQ&`Wf}k*a2@v|!kfjOVRI(2Pp^A?MFVtLm!wDsk zQjyWuuCJ=>>*H+WmuvY-?N=pz%ie3|AFkXquBZ85i1lqYRG*jL?Q?yre1m0TCv;S)R7vAh)LCbzS0{!`pdtl834z^2*AD%d0))svkDv zJ}U>o>EaLRgC-E;vV8`aF3;KWYk%IxH{giww)JFv|E~Lgef=MO;z`!`vGGYNPjIb< z&nq8`rd5yrZcQIc(a{!*a9KD^xnQp9Zvg$-!rb#f$#dt|nn9L0sLZd!S?sgPJ(%P5 z97h2CCk1wV{wi}n7k^*!pW4uV9+|`b#w`5@YGk1~<}%cLP*T2~v8Y!b{03U)aM^^< zYZE_>3}wZ9@+hdQ;CqefY^5dJVeu5D5eT9kvrTd8Ju~VpHLv2Qi$|d-NuGj2p`@Y= zXQeksmps9NP9KP|%1EgiejHsoHc99-#?Ql7`l`_g3=kB6{x7EP~Om@Q=}SH=8X0Im$D&}Jsrf6AL)-<_+G@@7t` z&>2UAn?HHuod@QBi)b}IKKSM!x5tosM!t1B^5!1F^O|Rg-^lyM)B5`Sdj>ZT zPHLFU-jmsWlzhVBP88e@LYheUsEUppsHpfUhvHs0*&%e@YBnW+z(|!T*isbuky$k{ zmt8(lrPAR>S1s$EeW$4CP{w}Q*m-gsme_YN;KMWOozBE~caiq%AEc=7huC)|qCMG=u)ww@rXc;}-Gg2= zpJf4|tWg}V2+~j%s#lr@I7ja&57KE$9qEvuDL`lepgg}IJz6u^Fw}>vaL5l5jMOqm z{4l5G+gSjNKy$xN;2t4@N}-qT@G5RjBfS$R_}FK4|5n7`R^(sIIICX`_~Ffrv}7iB z8r8!#*%jkvZGhd55tb06W|etS{_D*WVnIrF5 z{u5EBHa1}x>4V0;U6??a4-Jz9Y7mU2O<_|DTDP(Sq`$37349YurI9mz5b*m)hr(10 z=Sy}!+~@q!vLH2*hyH|PvMhoUo>+1)-kn!ZfB#k?zZ+bke>S65AXQ5m8Qx5E#zIzi zq8^Ceqe1As=YFFjeYE)J8R*V*Nt#c0dItO%Iy2Kf%rzi5^7*BFjOc?pb+f?G8UR3` zGG~ep2nM{btG`iY~CJpd4t^;n_KI-KhLG;yJcN*;Q30I@*uKeQR(ZxnOtC z-ZQt&$7h_73}KL&34vnC&PF>J#Ghud>ec_6#?B&7?71}?5>ON^_57KS zB}MyZWY2Qr+V)+4X0zmWv&TZXx84N>CqIdh>blaRN39g5rRoKnh0+$2aFWB-$*En=92U zW8Sf~YQ{xZx?>g1Qe;$|nCQkT#aSHNu!}5YLpk%0C`!@qh9O{Q`ShFTv7ejaFu1Q~ z{D@exDoL{u<>Fn2o5PpJ10l;<#(`Ud;cht$;=LjxT~%cMGaT+77_xB3716XGgUe#R zUAmfo(MWI(rebDG7f%^z>T4z5wC@4X zdzd4=1q>0i??GYB{N5&xtbA>ZSfon4+)>|1u_rJkDOHIuW zczX*2Bjqd6=I)tnzpwyA6FsQdBb{Z`D=txJT}Gf{Zjn}b0*RX;f_vCT%0gK~d~}CM zVBa2WhDpvwB*l?cUhhv&W^-TFy|=q+TMik#u>#SdP}E%*!#ODOaT?Oatr#L4@fd8xlf75qLxA=`b8!ol5( z_EE$weojVCx$b{h8jrVKdo%`65IT!t?665f_l-Gr2{@933~~eSnUaRzcskS2Jkt>^ zK3^$8iTyBUZJm+rnuc5MHeH$Q8}WvN_zzp)Eyk3QR4LGWpzHrDuIK)9?B8w9bpE9d zy~m{eM>c9KWf9@8=UiE!GzN6pWMRY33R9{3a47hm{B#mv3%I;QL{22)knnYw-`8*y zfjhU{0{qk$g0%s-yOlUV!h#AUN$|EF*UlXo7fjZT9ysZIkB~S?$l9JXwny|Lzbat# zF(M^?L~-j}+cd^3PN!BINmer#t?;;Q6;E2rV>L4WFyb0*gu@hU8Dmyh3SxLlAV%TJ zA}Vosr#_6A)iyrMSbQ*MvHR3Y%|f+qO?^?phdq|%MGDWRca>t(t7V3+JB8%k2Fsg4 z317Vc{%W&#l>v+PJ(l02jA?U?E@(hIf&+RGQiK;ydEm|#ef`O!G-fR4C{6s@)Orqm z$`hg8P`8$@42>bTg;kE6<*B~r3)y&#bn#oM2EB&M%KlqriN^P8PWdl|N96@TY!45I%7bSxH`WvW0$BPbO| zMx)W~*n)y1Oec5|ffeE%>{u8fq250hb?keqU8Dk*0^}+Rp#mx0A}L8h4u}!F`sV0E z47gBsi9(7%*(v9$ee9?3B62TqTPs_4Uk~6T`Hk19_o)BM4Zc)M)vG^~*Jf?%)iY69 zj~Mo*CJR%BaMgj^JDooBx&z#Jc8D#AaOQX&RPh5Qf50bjZ-0U}m9Sn<;?~^F98|%m zne&hQW&zgF8mHtA;O)!+%C@b}ZVeg#AKvP}F~|LS_l~TtjQv(ikjOX8knM^tI{uX z9n1C0$v!~TcKx#rG#x*@>lxPVge(_Gjwu%`#IUr!#QHn_=-gt5#yQx*IF&RYb(m}2 zqs(})mkY*$dY){AU>iudh*63N%lnXPMp*E@#^q!Qa8yy*#+A00FlP{7fIgp<=ZtFO zTw6^af0PXqSktTr)rv639#GtyW6!ALIbGjsGUAsqZ<{k;oHKGp^2j8N=8#bDHhn((y}Nc*QV+a(R!D zfj>DbKJ-u_tN}90r;Df+2QAM?OzL}q)AL;p0V!z6>9F5 zs2(7zQk2JpK;pCOI(nbA{AjWiR9BAKo3eZuqef#ezduiQBv?!uPmWNr0|Wf-$hFI>MSk$sqF>sxRzjje?4RUzhSxk9B<PwYJ-MqCZ(@9OpOU|xqmj+o-)TKcqd?aU}X3}yYEvPDwGpH z5yNPh|BO>YYXiJ1vzRWKPa$HamLcQryV6`Tt)l8Qm*aHUo6+(czOeiTmgdGBr6uF$ znBZw#QJCRGQ4(PSP|;wEV9K2ALqdsdsHV$u&~VVAT2UaLF5sJmJhen0FVWT>b6Z5j zs*ibn6iH-3bJ*=n{tEQ+;v)wdpeSW3C{X}Ffr6zmRuv3hIbQ3Y zF!E}eyt1sM3|!Be$wy4KI44g#nW|kT+O%-Oqo+rWHol1v@j6kp`3Uj^T2wkzMGuTZ zbi;z6fT*Yq>@L{uXOD^geD4h*P%qDt=1lYev$H{b1+WvxhpV160JqYm33)NatwwXY zXnQ5bc#-`rtV+HlHfYMOf0i*ZMW z-iYFN3wky3%5fZTx>|LX`IGkk=AWzP{i>vjnch-kHaMCcHXVFD5BF07!j)O7(%{43 zfu6(1mKoT@oF=H!no`=79l2KBM&Rk>fXe9#pN%17-re7@0Od?@F|KtbDT|zm#H@ja z^p?JY_&LkSpHi>|-mJoIPE7FG$ef7*#^CkU?-xfCW*#KEWjqsGB(4e8f`y#8kj-S?!+ev#a`$a&mq z9X}djlU_|qV5|+*I)&tYLwR``rt8zcP}i!fQ93HH)g1EE>a~q)m_=h-61>PFs|LvQ zRC9v_Ud(y#%sX^t6NX61WR_t5@eGOZ`%&Mbgep8A1qTi5JBNIp1S_q9$ml(-EmT!h zTl9juc@VJszpu|JbIRrNd9Tl58aKZakPS@SEA>j;{&zo%#8@chmkU>-;TW{bp+`Fj?-yFM@B+r>Jmug5=0#&jQ6x zbWb`Y$&{xcCrc<*a@pj;v@ui2nqsp|X@EQscnlxgB7dXDEJE=P;eAs~YKwGwrATQ^ z$!CgF*Wl@!v2mO!RO8mBm~&)NfK?F#t!xbc$HP;nuK78(=Z7yY?(Y)nX%@;${ib)S zzO3z~y>ZxMRb+2EjCLoj9>#xb|b_HTD%DD2aKhucE6 zh6LbaLc+o%>mf>1>Zg`B!Z<4BC3L{IG-7w3;>Eyub|@54jZ3b3=j6+_3vDEd(q>)Bf~fKBkP{nK2jTe zqr)@6@WGPdfjnv3@QQX0An=U&>zVLgzQAY*4kIW5y#l@=KsaWzfl<$v2MD7lRyW!g z!@@L0ML^G-S#dsP%|G&3j-V z)={-=se+}H=o|@|GY4vBLH*{7d5}=#ia#UB86(grSGFu;iq9T&sMn4x^HwJcX3$mU zfmG3nEz8Dm*TKqd@2^Jf;CIj{aOu@3GM#dvDgG4?2?P32*pxSJI)-1ypf~B~uPDWP z+uHaC2%Qt!u&#cIu5#gL9F(#v~n2Zjk_H33iYfaV7f8_mU2V}>m6WnOdP3Uf|o_-z$?mt z=OzlqNF1TS@K{-3s7?Z%Q|OL~WV7~Sj{vVt)+!4JEO5EC9KrGykj`Y!%vD!8gL`1M zX2azx%mbM@!UBpFx+w;8%QGi3Kx7OG%f1=MmRZdl!Cq(;9ohIxOuoMG&kz-#P%8$= zuv(%yuy|nPZhq0on|^E^0Ye7}4-mW#AwXmyY%r%jS&pp(=gxkB7F1BweIFst@eX_M zKT?Z$f|;NbMdvR5Kx01BR&BNU^cGe*NxlIA`ic*lyH?P#`APe^+=Vd8D%~~jhXc{<4Dn14QRA$V^1}OpP-Gln_Ak7Ltk3`bJlm8>RVpd3q~(5+r#uv zeMeI%N$eDzO8rV|Ce_Go8*iz9)c9^8aee7SU*ysyX-Ed&7Ww8~4(Lg%Zz1uQm>}=8V~IXFw`>qfS?neJmBB zrz-1|hRrB^g(TK)fIO6-4%X0eP=KU5ApYCBAQD zR5Rs1W5v*)v+g<%-|Q%EzdvQyzJo*i&R|%8`+k-8x!L~t>Zf!y6v9-Qc>z&(Qu_A8wEo0YX234omM$`0X_ilZ9+Fp#dy%R%1 z|DjWW$}MQ5D1rHfK1I3ejQc4R2R}imO!AZ741uA6Q6vvLAD>HlFZ?=NfT^EwK}ngr z9R=@B`TOZMlsLTEjzWY1nnB710JsFm(HGLRJit^0np_G1v!XdA!StF3-Y8J8G+?up zU7ie=^N4P2@u;kE$ZPh8->h55><7b*aM)k9>Wz?O|8;Sg zu+(#mw#y9PE5(eGWB13{e`=nsf`Yy2UmPFZmYT<{_I>}a0cu3A*{#_XR3eRvTc(>j zSg4-7;a9Kq>o4~8^?Q8@?W@J7@pPNIhVV85Nm=+p`JkS z{kCV2vMc^d4~~QYAj%L(sA`AV-`4h7ZD>NEZeald@KuLIJHKLcyx*fJS~+~!9K+XJ zpwS1*VDl{g0R3x$Fn_Co#Otxc)L;&s9_Va+$uo~-?wr%^oYVK??uVOXJ5k-*jqG3U z>Gw`M9x&1vVI&{jebcAi1RWP6q2{i0jn4b0Skjal`NQ}=z@G?g^91>id;2LyaR=dRnMI}M?eZ?fzN}&(iSuY2gIDxgF->6P71>A-0WiwaNuq(ArZRnFqSY}<+N z!Ye_mXG21)yQMLsP-G(-jXN4P9R|gRO3)1+VVuKG{>IB86WTwnyog|{U?9`1Gs$X& zYikyLvp{DL$M&bnxBV>k!pfqkR^xGJR6Q?BrPaf$AMWuzl`lv8ac$SK=J!pz*7u4C z57A)Tf6;$eTgg|%NEVHy2{)nh@A(s${WI`}Iwfa|z$_ptKp86c4xhtz&CcclpT7rB zi9NfC_zZqO;h~?0Ji<5j_-XrSe#GZV;awi6N_E6U_9I>SDJM6b0*ryAI!!K==|~6;l<7*#WrIMc=0VYvC|8I|xGOPQ z62)IeLNnl>w0klXmhmI6Egk6gh{PFZ?F{uE@{#zA4T~j1lyh2Rn?t8MX0*q`SSwld z6>UyblC3$^9#|^Vp~{v{_>Qy{`V-1@!SuL z48I%TIj7?eI@#{sOnhcOGY`cQL*j!!5%$A@I%gk<(9g;EsDGLKHgaYZAMW4M_|FEL;xX{w*mDQjIQCA? zABN~~52p8Sb8LM1J{unlecOTXpN7xjFgoQ}$HO0l;qae@f$E>*a}fA$AJ8%I+rIms zE%4nlx8t|@pNRfLtp{t2-w}Qq8TIkl!s0)ihqyPakoM&ED0>N|?N50(V_fO&JZbd( zv4Y3$?EjOlhCebR-u}4g>c5G@-?RKv@6IO=`QMk0el!oQjszS8Art=;8+88wL9r-3 z+Yd0TJbL~m4FA5QP$ z4#zil0f+G%PmVBS@x)O^+a zlyyQ+^FQ&`B9(FnD2}MXMiIO7Ts~?Nf3RoAa2fI6{2V?z;4tUMe=#uG!Ow(qV<*fy zp^}e!B|01eK@(+OQ zepr}>d4|X2S3UTJO*%Mzwj`> z(#eLK5k7S0H_o3S^`Doc{yk=KN1DIW^RvHW0{>0!-3OUBABj=&phP+^xn6rhhhNyBJp<^gZT$?fmi<3XyFIj0 zYZ!dW=s?6NO&|%D*;beU6z&$ku|a#ViD1dyHNflKv&P zj~H3*q3$spTK@yv*RJ`gbkCY9Dd*?9uzP>XuL)4U83cZPmT>h z09cRHX?2}X&GW|Qr628Wo(Vlyg~0et@GU%4}Qaer{#C<=Wl7P>^gh~dFSp`!%tM; zY46a_;Si4s&wRo6UIVk>JC)oY*SLHKN7VQ41E(K`_yeO%z51~C?|kR+J_Z_R_?!2? zG0y;Q+VBbYv)_)Kd-0T?-)FuZe^-D#`=r^&4*+}j6Tlz69Dj}3p0DQa9l&^28k%27 zYv~@?iXTj5J?a|sKTPV1w!9PZwo2-W__xcyapjJN*X8Wtjalg<9;qKhmESx?G1i}i_#8{rx#t-eJTY3 zuis+#ttR^R`sH2tY60#UTqn4zQLc^0qrLsE(GC%}LE zVqXEJ^wR~s`cU;f#7*{1e=27M^xyM8eVh)~AEL!~g02E^EWXdMvEwK_p$82zTm6H> z6QV3zZf5WeV~>{`Drsm*{`4lRDwcxF^_qAurqPC zwa#l`H6h?|SfY3uHC2}AGIg0(f2~_=98Jp1E-2p{MHZtuI1?TPm8j!y;tF;XbQ;R_ z-3I8i43|6C(C8L;NblC3?7ILGNEMdGFT)SuZS#*7^?=^(0l|s@5ET3)qfQk8w#2U} zxwWluA!<+#OTZxM4xc)BJ`>2m56kbAd->2mp$ESe;K#rPB4qFc36+uN4LnbPiL)s9 z4hU~uogpD2X%7Jjd{&2Md;!_;9qcnXv?jyfvmwZI2Y?JZ8J__R4L$l8bDKT@=?(FM_<|Bu5q32J^0Drj?V+4cF#~7 zoO|_g!%y(DGc({bJDrac-?+U0euh2s-~*U3)!qQd@QwT|JW4_Nh(Z0?e}s9%#Qs=2 zCyeR2I{b(7+%#`~A{u`!-2VGGJ3pAiJBLVpt2z0O$U~oy3}7|YV2L*!{!3rxp6Bu@T_i?j4 z@9?-EAqxZK4^j{2uE)*U$E*1d$(hssU+JqyCue&&WeQK;NFm`23EM*?*!4WeqRrM`n~uZj)Mg-pDJtc1ng7_y4ga zJulNC5Ap)I$CiiGd45DJmOxl49#9k4%_=TweQku zG`j3|{|@@M(^6kblR9BRHNwKMN#+mfPScoyUh27l594H4^o*d#ShT@+ZxSf}Dat4Z zETR99eh3_)71t^sDjy1S58SVr2Y7Q-JfNT(Pq9JaIvrg8WANz64fi95EM+tDRN5@LJ!(if_*dw zaEIh%L1oeZM~TzG?;LwaV%oN+Ahy=O7xl3Gt{ubrT+RI#U(D|g?C|w`HvX@6@=yD} z#kwGKU>Su3CO{y2d{4>#&pY-1Cy(zv$MwAjOF%z-e@6l0%FF?Mk1uF-dkv#Z7Iwqe zyvg9%M2ZbYGyXKd6J>xFLttLakSNO2E6DL5PJK{adExYL9$19JvY3D6{@aOlnA?9C z5MmXml3NSoBnGMlL-@cWC_$k|?DG*Yo8+)O;lxrM<^ID%$(e@`!svP*!5&WNZ=@wV zvHgdX;5$Dgau^?yAqPY9M*~kwgYsHJ6ZjX&2jsJfjcm`!VblGGXY!rhFzjL*v+`IP zlO)rzkL#;%C+|M-xaR6&CkD+4GD$ytKSIuf(C&uFY56YWkMzOWf$92_<8@=N z+W%GkX3UYiWd6FQpVw7k)jS@-@sI1UEQ%rSbB1r77?%=!Q{t4ruxgKrM6Asv{bN*? zmX-#Ruc|rU&&otrkLNQfdW3vbO6w5~9bz&WNfvl9C_)fk0@2nKg3!n?_=xKgC%W+9 zM6C?#1iKO=g&JS}-T#692>$~MheS60VFocPI>X?US6FeTMKdFNS~|o-tSm|;Ry0dG z!6=#WB44%px&JC&8I3#(;$}`?o!sV&VqIQkFUs6DYf?w8;*Lw^#{Zo(3BevV|Lr3e+ z(!ZZU^9NrH>f-9oJ2I$!Sp7f3h-q=LY_IS5RZH+C7p)!AUl07tQ~nfQf6fsH;5Pyy zU0~9QKCt-FUst@Py2n{d;u!|Y2hxYCgWB{VP+u%U3IYFZ|Ldpykk=N& zrkwNjD18aS`hvQtxxIxS)jHnlm`_1PeP`a`1~}=}JQXm)k+-~oY>uf@Ep?m@D-5?+ zy1+e7Prg4w+%fRO@_!=zZ%{u=Qv?5m@?Z-4P>5zkh6E-+J14fquz7Rs!<<`7_J=E(wH9tefK_(fI%r?LfA8t@exL2{ z`1$|8-m7;1Zg*R!@AE<2?A~w9^Lhxyu7 zLPYUCO0YYj0X#I|2Ie2&077yU|3W0f6Ckqw!~y*Cz(2q^sU*c1vHjOQBW8vwOi_sJ zuAT%Y2iv+sY4_j4hXbWPnhoOv{4zhn>6--YKZA1&Go?R=domD)FaNH?DMY@Fn{(xs z3)5j4B+5%{?CZvilJEtF$F~SO!Tu&3H0cun{vE2z-GjB%;qt~YlNmJ3eoG)_KQ2e*4v@g;!|(^?y9Yq|dGuefJB;YuXZ>!O3_p;6 zka0AJ@(wzEOAZesWKEj-XlE)>^vX)m(fXwj`esDk;U7%y|47Wpv5UxW*grtWLmiDc ze55{7{)Uf~pDJS#*X%=HW&wgxa8OpOVq`vFo*?!qNvp>%O7I|z=z{3>=YuuXP7iz1~DD0Vbj zz$TZ6cxS-u6#cH>duK1$7G!oA5c9OQYq0E88|*}%XrKF@Rvj(dDK5nNiJgj+y9@an z`E%d8KbQT6d0*xK+e&jL>k6TiqS%mukLNl{nsxOeU5xBI1$oky_@<7;ij!TBVLKHR z?0X(3=TrkUKl{1s9%1V|Bn^VU)gSzdQ+546O1Z&bLV^!F9)#*A(w|Q$N9Isa<)`?h zQ06aeSGM$eRyjkLXEj+?Wko)m8$oJ84a*FAIbj(i-5$nQ z*igJLwv>ONV0@}R>n7XWw$!*I|5IPJD_3I@FR=dgT3k=+yf62EMwD6XB7YBUxvZYE z!e`IIiGTXyD7QYYHLqFQG_1FCUXuE`Qi-;PWhQaaJ_sDX`G!fBnkEVu3h1pTC;Mz=e|>Emek0uJW!o^cnWF-N(v=_t1r#AtNFfRUK@VeI+6GC)D3UwHC>H=P zVZh^Xc=ZEN{RfhAM{1oW(meJ(O5?MMMgQlNaV`T1@A9Q|{d_(+@*08$jF zABnlzZa8|V0B{HW3Opkqw8#fv^XwjcNrgfomS7*vM41VJnGhvUobxa~N+igEzGE{? zhvdWFa6_TffD9+*$v>+bt95B%6mG56WQ#j$II?cR?u^0`8IFm8@#kmrW)Q?bySw?Q znp6JMuzcWW0r}YDx%tfLm>WQT$9;xES-@SsXrn?#`wRV&KipzaETlv&$MSsN_clWS zHyChD&+j3-B4|&Fg$R&4Y5C6f9AOQY_g(Bb8g%?e7-mC4eCKm0;vc%~gbpWK&lmbt z-ox{lc0&e3_fXR~ZiA+GGxMBr4JVx5^pA{o6QDPt`>f8nEcW~W`4$_H!?1E-3`6G* zC+%^~>gX`pTu+<~I{ne|j)3xGXD(spd= zFWqr8z~KDn5XbjNK68Xe`?68$l=?t)JkpjBJvBZT}OJO(*u> z^9(nSpH>sxeIxkNXfsG*-wPm@&OCtxzDUry>O@Q2eBvU{oHU?(>mn^KfeeXQCMrpb z$9a)2_f!@0zH((HPWVy_k-3u>?tFu{`RyXZ!f8wAriM~Cdb1Cnot?x#yy#$%$E-hk z!P$H+4$oG`0me2H7RmkBLTRo_;$lPx?`9@8Oc0bD1ipI|SncSvTZv0aLjNsy`dIn~ z@BIi`TsP1{`Uz6{8IeDjS>cOI=X}B${P8H2rjFgNwfys~A}nWGhFF=_^{pZ=MAqmT zV%k9eUz51&SnR`lgvfQQp`B|N5Yh4a)S^5R>slh)JI@Jhwz}nxxOk=QRWESXH!$C_ z3$JkPb~^JUtt^u(Lc!dB35>&#!2|}N%7E5pVyC#cVXG)=QfhVk^r;-j(e?m^Tt*;q< zu2}1`%bV+~M_i(hi?S-C=uWwH$NxTNX@05qdeg4^y0Rt8{@>=+)*XIJVe574wyll) z*Vf5>E7jj2vzpoRpLNnSKja^(Hu=;)U)C9)*Ur#k0rMyNgX2Kk&E^VU>G*=g40-zN zidmn5uCgUQ%09-uHTDvUE+?sLg@#9O3-`<$ujF z6nNK{GJE&+rXr0!>&(>=Qf~6^^zI!Ka=7Oy)8NJ&WKP5tM?lI9iOnA<>~v}Ixgsb` ztVjgH5{E+^V~gJb3}FQfkj53J4@2esAALYWW)KO6Vd!z202r9%0m?Z)NUGx?6DSV` z0bCj+sM_%Fgks4C^Og-P5?H7*9FXsh%%hCs4l)?e76o(%_vG)0ZpfxHA(D2HN@tM< z&scGhAk==Q<2m|>X`BtGaowKy;Nzg(=x16reF#2!G*;u|5sTpmBChp~Joi{E8on08$~_mB)Lpqbu=;ByrTId=$#T z`i$Tweeb{bIEFd|+<$ToCxDQ>ec22Y*?=d>p@H=5I{-`KVfo{@a1Y?@(D~ys8V;Mj z@j`HR0q?j5hrm}m>>c+V#s@>+c6W52?8e~8ci1>S3yJ!Ju*2Z%{!6|+_XqkH0hjV) z^xell^!}UQbbS@Uhh#rdod0?dX$U@7```99fa^w2J!uF$ApG_mN4|%AK2xRKhwDD? zPU+uw6TbZn7@tS$I~*SS4ZG=hiQjYze!t>p1@ng$+DcL=)$^QR z5#Y=eGYmL7M3Mg=>0z_qBvNqmkn@}rIxW2B0h1H)VBCD>98Wpj6BBPb5X9*>98+O| zJO(yJAbLFS(PuOj=ym;U2GVWk4hE5f{(H_JIl%c*>>lrZ%^}fn+HmOHJm?wG@wg9t zng>TKa6ISEjXrd054yGjPFC*dMjUS1dDDrr9>=3R%Y!d`AoH1Wd*R>2>F?*96Rq%a zf5G>de9)7{NISZ7Dh5!LKK5u5XZ*3I3HImB4Rl7{bLS?vBP8B@;O^o2bM?JAyCi^j zHjz{REcvGs4G!B07$zxa3a&V1)OH^Y7yZbzLSa}Uk=p@+=y z@m(?gnH_?B@H?2NoFqMDI7nFVh zf6P`a<7I!(3|0DP5}u>c?ApUs_afOD2WhU#9g?O0NB(MlwC;{m3u8hPVSuZ?bSv zqSAet0w#TzxOCEOHX`TYk*xtfL>L&Kf0XZ`E*gNC=!I#7jwU{OQTc7hKTSl44%w!R$wX1HFL~U_xb3 zW&xQJVF_USD;V3W$Ho0Op&L4w^~&{ev~@a`H5CJ^SNa8IW*LTH83-6=VF{U#m>r!A z?BnOc7{J3bG(aq&DjFEbkMm`sj_|sUqm=42nF(ewrAdnH?e-qO!Qq@F^{MPjG)YR~$nG~~ubRGX_Jt8P)vCzaMZ-nH2095!cCN&jV& z{>2WUPuyeSO0WA{F(>&OzqH~uX_-fk^uUHsg*?9?hrs z-`aRy82_>VbB9{&s_J;6ybX~ySgO=m#f_FRVoI@ERy4*us}gLfD|SZ8s;x3Aj~b-X z#>&PvYBpHKNy4j!H5^78G}AH=;noe&kz_uR!W^-Y_Kikm7&t-{hL55|#Z;A1D#cOQ z#t-f&&GY{D`7-S)tMq!x=TyYFwXJ%pK`2YimaAZ0e4cW&-gUy6I7sGIN7-oGV4l^?_GsMA) z#?k+4BTDeStF_Tv$2RRZcei>^@-g8(PchKrA&fkj9+n4A%;Pc>M#~ymBTDs+F;+)= z86J|q#Nm=QamHtxrx4RKFyn|!4lv9L#bT;Tu5B=EukrbtSk!MAt7FO;5*Z1Yali)w zm}PO1nB>9bM3aV`NX9ZnRaos~|Feb;&g{c849+qW4l)?XOwmu_uL$ zN!qd^RduB3<|?>Y#gSG;QB~4T7EZTgQPEOVlvN~-al~%1UCd&L@CUJrCSa3aK9g2yi1}u@+BdOHySskrAT>Wbv zhF}dahM0O8MR}pf9xTl4^grp@k~FI$qoYq7X)#iI#gW{ju~0@~4ws~Q+~)Ee9uJDb z8%O|f9=op1&It~Im}X{ZeM<;0S%<`7-Of&K)}))nol6oNmJOqcT%KyIaH3CcvP7Js zwpR`#qP?$F~nFwZQ(=sOS7py%9LLCDk3_a7kv!H!+_0OF{)aP0B zj~A{w5i$8}J`)IRgbvIOGtt!bKR$FEQ0t-Rvx+kV&%6&@@jJ$5LQ#;A?3oRo9%11I zR$&k5V0zca8{UV+?;bJ}3=9kdArI9sk15{vJbp)7@>zytBs_})iYwkQZCsLluBTLQ z7DZVr;#Nj0%HhAUj%yWCc@{@L9&?Ud+@syABBbHoe4Y;z(>*USnV4onVcX9hZ2aRP z(1)dW?!J4?oI{f$ewQ8_h;%#;C$jqCnGS3ZIPjYVL^NG5V>ld6&yi<@VGX5dsG^E! zf$caQd3eu5#pHyBGEL4OGUlz{reu#?sXyHA_}7Wn?R7EINcC}DvGGo}(YnZ!oA+P- zA2_3}vinV(B(gl;aP0Pvqxl#AcVqtd&q(tf&pU|W+Zd{oZ(8uaHR9X=~IDpSKyv6f*K7l9rSd+-HT�Ms}<+GZj+mJME?57`fZ#! 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 diff --git a/ms-windows/QGIS-Installer.nsi b/ms-windows/QGIS-Installer.nsi deleted file mode 100644 index 892a7083aa932..0000000000000 --- a/ms-windows/QGIS-Installer.nsi +++ /dev/null @@ -1,569 +0,0 @@ -;-------------------------------------------------------------------------- -; QGIS-Installer.nsi - QGIS Installer for Windows -; --------------------- -; Date : September 2008 -; Copyright : (C) 2008 by Marco Pasetti -; Email : marco dot pasetti at alice dot it -;-------------------------------------------------------------------------- -; # -; 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. # -; # -;-------------------------------------------------------------------------- - -;Extended for creatensis.pl by Jürgen E. Fischer - -;---------------------------------------------------------------------------------------------------------------------------- - -; Added by Tim to get optimal compression -SetCompressor /SOLID lzma - -; Added by Tim to allow privilege elevation in vista -RequestExecutionLevel admin - -;---------------------------------------------------------------------------------------------------------------------------- - -;NSIS Includes - -!include "x64.nsh" -!include "MUI.nsh" -!include "LogicLib.nsh" - -;---------------------------------------------------------------------------------------------------------------------------- - -;Set the installer variables, depending on the selected version to build - -!addplugindir osgeo4w/untgz -!addplugindir osgeo4w/nsis -!addplugindir osgeo4w/inetc - -;---------------------------------------------------------------------------------------------------------------------------- - -;Publisher variables - -!define PUBLISHER "QGIS Development Team" -!define WEB_SITE "https://qgis.org" -!define WIKI_PAGE "https://qgis.org/en/docs/" - -;---------------------------------------------------------------------------------------------------------------------------- - -;General Definitions - -;Name of the application shown during install -Name "${DISPLAYED_NAME}" - -;Name of the output file (installer executable) -OutFile "${INSTALLER_NAME}" - -;Tell the installer to show Install and Uninstall details as default -ShowInstDetails hide -ShowUnInstDetails hide - -;---------------------------------------------------------------------------------------------------------------------------- - -; .onInit Function (called when the installer is nearly finished initializing) - -; Check if QGIS is already installed on the system and, if yes, what version and binary release; -; depending on that, select the install procedure: - -; 1. first installation = if QGIS is not already installed -; install QGIS asking for the install PATH - -; 2. upgrade installation = if an older release of QGIS is already installed -; call the uninstaller of the currently installed QGIS release -; if the uninstall procedure succeeded, call the current installer without asking for the install PATH -; QGIS will be installed in the same PATH of the previous installation - -; 3. downgrade installation = if a newer release of QGIS is already installed -; call the uninstaller of the currently installed QGIS release -; if the uninstall procedure succeeded, call the current installer without asking for the install PATH -; QGIS will be installed in the same PATH of the previous installation - -; 4. repair installation = if the same release of QGIS is already installed -; call the uninstaller of the currently installed QGIS release -; if the uninstall procedure succeeded, call the current installer asking for the install PATH - -Function .onInit -!ifdef INNER - WriteUninstaller "${UNINSTALLERDEST}\uninstall.exe" - Quit -!endif - ${If} ${ARCH} == "x86_64" - ${If} ${RunningX64} - DetailPrint "Installer running on 64-bit host" - ; disable registry redirection (enable access to 64-bit portion of registry) - SetRegView 64 - ; change install dir - ${If} $INSTDIR == "" - StrCpy $INSTDIR "$PROGRAMFILES64\${QGIS_BASE}" - ${EndIf} - ${EndIf} - ${EndIf} - - ${If} $INSTDIR == "" - StrCpy $INSTDIR "$PROGRAMFILES\${QGIS_BASE}" - ${EndIf} - - Var /GLOBAL ASK_FOR_PATH - StrCpy $ASK_FOR_PATH "YES" - - Var /GLOBAL UNINSTALL_STRING - Var /GLOBAL INSTALL_PATH - - Var /GLOBAL INSTALLED_VERSION_NUMBER - Var /GLOBAL INSTALLED_SVN_REVISION - Var /GLOBAL INSTALLED_BINARY_REVISION - - Var /GLOBAL INSTALLED_VERSION_INT - - Var /GLOBAL DISPLAYED_INSTALLED_VERSION - - Var /GLOBAL MESSAGE_0_ - Var /GLOBAL MESSAGE_1_ - Var /GLOBAL MESSAGE_2_ - Var /GLOBAL MESSAGE_3_ - - ReadRegStr $UNINSTALL_STRING HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${QGIS_BASE}" "UninstallString" - ReadRegStr $INSTALL_PATH HKLM "Software\${QGIS_BASE}" "InstallPath" - ReadRegStr $INSTALLED_VERSION_NUMBER HKLM "Software\${QGIS_BASE}" "VersionNumber" - ReadRegStr $INSTALLED_BINARY_REVISION HKLM "Software\${QGIS_BASE}" "BinaryRevision" - - ReadRegStr $INSTALLED_VERSION_INT HKLM "Software\${QGIS_BASE}" "VersionInt" - ${If} $INSTALLED_VERSION_INT == "" - # First using new scheme: 1080001 - # Previous: SvnRevision 14615 + BinaryRevision 0 - ReadRegStr $INSTALLED_SVN_REVISION HKLM "Software\${QGIS_BASE}" "SvnRevision" - IntOp $INSTALLED_VERSION_INT $INSTALLED_SVN_REVISION + $INSTALLED_BINARY_REVISION - ${EndIf} - - StrCpy $MESSAGE_0_ "${QGIS_BASE} is already installed on your system.$\r$\n" - StrCpy $MESSAGE_0_ "$MESSAGE_0_$\r$\n" - - ${If} $INSTALLED_BINARY_REVISION == "" - StrCpy $DISPLAYED_INSTALLED_VERSION "$INSTALLED_VERSION_NUMBER" - ${Else} - StrCpy $DISPLAYED_INSTALLED_VERSION "$INSTALLED_VERSION_NUMBER-$INSTALLED_BINARY_REVISION" - ${EndIf} - - StrCpy $MESSAGE_0_ "$MESSAGE_0_The installed release is $DISPLAYED_INSTALLED_VERSION$\r$\n" - - StrCpy $MESSAGE_1_ "$MESSAGE_0_$\r$\n" - StrCpy $MESSAGE_1_ "$MESSAGE_1_You are going to install a newer release of ${QGIS_BASE}$\r$\n" - StrCpy $MESSAGE_1_ "$MESSAGE_1_$\r$\n" - StrCpy $MESSAGE_1_ "$MESSAGE_1_Press OK to uninstall QGIS $DISPLAYED_INSTALLED_VERSION" - StrCpy $MESSAGE_1_ "$MESSAGE_1_ and install ${DISPLAYED_NAME} or Cancel to quit." - - StrCpy $MESSAGE_2_ "$MESSAGE_0_$\r$\n" - StrCpy $MESSAGE_2_ "$MESSAGE_2_You are going to install an older release of ${QGIS_BASE}$\r$\n" - StrCpy $MESSAGE_2_ "$MESSAGE_2_$\r$\n" - StrCpy $MESSAGE_2_ "$MESSAGE_2_Press OK to uninstall QGIS $DISPLAYED_INSTALLED_VERSION" - StrCpy $MESSAGE_2_ "$MESSAGE_2_ and install ${DISPLAYED_NAME} or Cancel to quit." - - StrCpy $MESSAGE_3_ "$MESSAGE_0_$\r$\n" - StrCpy $MESSAGE_3_ "$MESSAGE_3_This is the latest release available.$\r$\n" - StrCpy $MESSAGE_3_ "$MESSAGE_3_$\r$\n" - StrCpy $MESSAGE_3_ "$MESSAGE_3_Press OK to reinstall ${DISPLAYED_NAME} or Cancel to quit." - - ${If} $INSTALLED_VERSION_INT = 0 - ${Else} - ${If} $INSTALLED_VERSION_INT < ${VERSION_INT} - MessageBox MB_OKCANCEL "$MESSAGE_1_" IDOK upgrade IDCANCEL quit_upgrade - upgrade: - StrCpy $ASK_FOR_PATH "NO" - ExecWait '"$UNINSTALL_STRING" _?=$INSTALL_PATH' $0 - Goto continue_upgrade - quit_upgrade: - Abort - continue_upgrade: - ${ElseIf} $INSTALLED_VERSION_INT > ${VERSION_INT} - MessageBox MB_OKCANCEL "$MESSAGE_2_" IDOK downgrade IDCANCEL quit_downgrade - downgrade: - StrCpy $ASK_FOR_PATH "NO" - ExecWait '"$UNINSTALL_STRING" _?=$INSTALL_PATH' $0 - Goto continue_downgrade - quit_downgrade: - Abort - continue_downgrade: - ${ElseIf} $INSTALLED_VERSION_INT = ${VERSION_INT} - MessageBox MB_OKCANCEL "$MESSAGE_3_" IDOK reinstall IDCANCEL quit_reinstall - reinstall: - ExecWait '"$UNINSTALL_STRING" _?=$INSTALL_PATH' $0 - Goto continue_reinstall - quit_reinstall: - Abort - continue_reinstall: - ${EndIf} - - ${If} $0 = 0 - ${Else} - Abort - ${EndIf} - ${EndIf} -FunctionEnd - -;---------------------------------------------------------------------------------------------------------------------------- - -;CheckUpdate Function -;Check if to show the MUI_PAGE_DIRECTORY during the installation (to ask for the install PATH) - -Function CheckUpdate - - ${If} $ASK_FOR_PATH == "NO" - Abort - ${EndIf} - -FunctionEnd - -;---------------------------------------------------------------------------------------------------------------------------- - -;Interface Settings - -!define MUI_ABORTWARNING -!define MUI_ICON ".\Installer-Files\Install_QGIS.ico" -!define MUI_UNICON ".\Installer-Files\Uninstall_QGIS.ico" -!define MUI_HEADERIMAGE_BITMAP_NOSTETCH ".\Installer-Files\InstallHeaderImage.bmp" -!define MUI_HEADERIMAGE_UNBITMAP_NOSTRETCH ".\Installer-Files\UnInstallHeaderImage.bmp" -!define MUI_WELCOMEFINISHPAGE_BITMAP ".\Installer-Files\WelcomeFinishPage.bmp" -!define MUI_UNWELCOMEFINISHPAGE_BITMAP ".\Installer-Files\WelcomeFinishPage.bmp" - -;---------------------------------------------------------------------------------------------------------------------------- - -;Installer Pages - -!define MUI_WELCOMEPAGE_TITLE_3LINES -!insertmacro MUI_PAGE_WELCOME -!insertmacro MUI_PAGE_LICENSE ${LICENSE_FILE} - -!define MUI_PAGE_CUSTOMFUNCTION_PRE CheckUpdate -!insertmacro MUI_PAGE_DIRECTORY - -!insertmacro MUI_PAGE_COMPONENTS -!insertmacro MUI_PAGE_INSTFILES -!define MUI_FINISHPAGE_TITLE_3LINES -!insertmacro MUI_PAGE_FINISH - -!insertmacro MUI_UNPAGE_WELCOME -!insertmacro MUI_UNPAGE_CONFIRM -!insertmacro MUI_UNPAGE_INSTFILES -!insertmacro MUI_UNPAGE_FINISH - -;---------------------------------------------------------------------------------------------------------------------------- - -; Language files -!insertmacro MUI_LANGUAGE "English" -!insertmacro MUI_LANGUAGE "German" -!insertmacro MUI_LANGUAGE "French" -!insertmacro MUI_LANGUAGE "Russian" -!insertmacro MUI_LANGUAGE "Japanese" -!insertmacro MUI_LANGUAGE "Italian" -!insertmacro MUI_LANGUAGE "Polish" -!insertmacro MUI_LANGUAGE "Spanish" -!insertmacro MUI_LANGUAGE "PortugueseBR" -!insertmacro MUI_LANGUAGE "Portuguese" -!insertmacro MUI_LANGUAGE "Czech" -!insertmacro MUI_LANGUAGE "Croatian" -!insertmacro MUI_LANGUAGE "Thai" -!insertmacro MUI_LANGUAGE "Dutch" - -;---------------------------------------------------------------------------------------------------------------------------- - -;Installer Sections - -;Declares the variables for optional Sample Data Sections -Var /GLOBAL HTTP_PATH -Var /GLOBAL ARCHIVE_NAME -Var /GLOBAL EXTENDED_ARCHIVE_NAME -Var /GLOBAL ORIGINAL_UNTAR_FOLDER -Var /GLOBAL CUSTOM_UNTAR_FOLDER -Var /GLOBAL ARCHIVE_SIZE_KB -Var /GLOBAL ARCHIVE_SIZE_MB -Var /GLOBAL DOWNLOAD_MESSAGE_ - -!ifndef INNER -Section "QGIS" SecQGIS - SectionIn RO - - ;Added by Tim to set the reg key so we get default plugin loading - !include plugins.nsh - ;Added by Tim to set the reg key so we get default python & py plugins - !include python_plugins.nsh - - ;Set the INSTALL_DIR variable - Var /GLOBAL INSTALL_DIR - - ${If} $ASK_FOR_PATH == "NO" - StrCpy $INSTALL_DIR "$INSTALL_PATH" - ${Else} - StrCpy $INSTALL_DIR "$INSTDIR" - ${EndIf} - - ;Set to try to overwrite existing files - SetOverwrite try - - ;Set the GIS_DATABASE directory - SetShellVarContext current - Var /GLOBAL GIS_DATABASE - StrCpy $GIS_DATABASE "$DOCUMENTS\GIS DataBase" - - ;Create the GIS_DATABASE directory - CreateDirectory "$GIS_DATABASE" - - ;add Installer files - SetOutPath "$INSTALL_DIR\icons" - File .\Installer-Files\QGIS.ico - File .\Installer-Files\QGIS_Web.ico - SetOutPath "$INSTALL_DIR" - File .\Installer-Files\postinstall.bat - File .\Installer-Files\preremove.bat - - ;add QGIS files - SetOutPath "$INSTALL_DIR" - File /r ${PACKAGE_FOLDER}\*.* - -!ifndef INNER - SetOutPath $INSTDIR - File uninstall.exe -!endif - - ;Registry Key Entries - - ;HKEY_LOCAL_MACHINE Install entries - ;Set the Name, Version and Revision of QGIS+ PublisherInfo + InstallPath - WriteRegStr HKLM "Software\${QGIS_BASE}" "Name" "${QGIS_BASE}" - WriteRegStr HKLM "Software\${QGIS_BASE}" "VersionNumber" "${VERSION_NUMBER}" - WriteRegStr HKLM "Software\${QGIS_BASE}" "VersionName" "${VERSION_NAME}" - WriteRegStr HKLM "Software\${QGIS_BASE}" "VersionInt" "${VERSION_INT}" - WriteRegStr HKLM "Software\${QGIS_BASE}" "BinaryRevision" "${BINARY_REVISION}" - WriteRegStr HKLM "Software\${QGIS_BASE}" "Publisher" "${PUBLISHER}" - WriteRegStr HKLM "Software\${QGIS_BASE}" "WebSite" "${WEB_SITE}" - WriteRegStr HKLM "Software\${QGIS_BASE}" "InstallPath" "$INSTALL_DIR" - - ;HKEY_LOCAL_MACHINE Uninstall entries - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${QGIS_BASE}" "DisplayName" "${DISPLAYED_NAME}" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${QGIS_BASE}" "DisplayVersion" "${VERSION_NUMBER}" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${QGIS_BASE}" "UninstallString" "$INSTALL_DIR\uninstall.exe" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${QGIS_BASE}" "DisplayIcon" "$INSTALL_DIR\icons\QGIS.ico" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${QGIS_BASE}" "EstimatedSize" 1 - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${QGIS_BASE}" "HelpLink" "${WIKI_PAGE}" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${QGIS_BASE}" "URLInfoAbout" "${WEB_SITE}" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${QGIS_BASE}" "Publisher" "${PUBLISHER}" - - ;Create the Desktop Shortcut - SetShellVarContext current - - ;Create the Windows Start Menu Shortcuts - SetShellVarContext all - - CreateDirectory "$SMPROGRAMS\${QGIS_BASE}" - - GetFullPathName /SHORT $0 $INSTALL_DIR - System::Call 'Kernel32::SetEnvironmentVariableA(t, t) i("OSGEO4W_ROOT", "$0").r0' - System::Call 'Kernel32::SetEnvironmentVariableA(t, t) i("OSGEO4W_STARTMENU", "$SMPROGRAMS\${QGIS_BASE}").r0' - System::Call 'Kernel32::SetEnvironmentVariableA(t, t) i("OSGEO4W_DESKTOP", "$DESKTOP\${QGIS_BASE}").r0' - System::Call 'Kernel32::SetEnvironmentVariableA(t, t) i("OSGEO4W_MENU_LINKS", "1").r0' - System::Call 'Kernel32::SetEnvironmentVariableA(t, t) i("OSGEO4W_DESKTOP_LINKS", "1").r0' - - ReadEnvStr $0 COMSPEC - nsExec::ExecToLog '"$0" /c "$INSTALL_DIR\postinstall.bat"' - - IfFileExists "$INSTALL_DIR\etc\reboot" RebootNecessary NoRebootNecessary - -RebootNecessary: - IfSilent FlagRebootNecessary - SetRebootFlag true - Return - -FlagRebootNecessary: - SetErrorLevel 3010 ; ERROR_SUCCESS_REBOOT_REQUIRED - Return - -NoRebootNecessary: - Return - -SectionEnd -!endif - -Function DownloadDataSet - - IntOp $ARCHIVE_SIZE_MB $ARCHIVE_SIZE_KB / 1024 - - StrCpy $DOWNLOAD_MESSAGE_ "The installer will download the $EXTENDED_ARCHIVE_NAME sample data set.$\r$\n" - StrCpy $DOWNLOAD_MESSAGE_ "$DOWNLOAD_MESSAGE_$\r$\n" - StrCpy $DOWNLOAD_MESSAGE_ "$DOWNLOAD_MESSAGE_The archive is about $ARCHIVE_SIZE_MB MB and may take" - StrCpy $DOWNLOAD_MESSAGE_ "$DOWNLOAD_MESSAGE_ several minutes to be downloaded.$\r$\n" - StrCpy $DOWNLOAD_MESSAGE_ "$DOWNLOAD_MESSAGE_$\r$\n" - StrCpy $DOWNLOAD_MESSAGE_ "$DOWNLOAD_MESSAGE_The $EXTENDED_ARCHIVE_NAME will be copied to:$\r$\n" - StrCpy $DOWNLOAD_MESSAGE_ "$DOWNLOAD_MESSAGE_$GIS_DATABASE\$CUSTOM_UNTAR_FOLDER.$\r$\n" - StrCpy $DOWNLOAD_MESSAGE_ "$DOWNLOAD_MESSAGE_$\r$\n" - StrCpy $DOWNLOAD_MESSAGE_ "$DOWNLOAD_MESSAGE_Press OK to continue or Cancel to skip the download and complete the ${QGIS_BASE}" - StrCpy $DOWNLOAD_MESSAGE_ "$DOWNLOAD_MESSAGE_ installation without the $EXTENDED_ARCHIVE_NAME data set.$\r$\n" - - MessageBox MB_OKCANCEL "$DOWNLOAD_MESSAGE_" IDOK download IDCANCEL cancel_download - - download: - SetShellVarContext current - InitPluginsDir - inetc::get /caption "$ARCHIVE_NAME" /canceltext "Cancel" "$HTTP_PATH/$ARCHIVE_NAME" "$TEMP\$ARCHIVE_NAME" /end - Pop $0 # return value = exit code, "OK" means OK - StrCmp $0 "OK" download_ok download_failed - - download_ok: - InitPluginsDir - untgz::extract "-d" "$GIS_DATABASE" "$TEMP\$ARCHIVE_NAME" - Pop $0 - StrCmp $0 "success" untar_ok untar_failed - - untar_ok: - Rename "$GIS_DATABASE\$ORIGINAL_UNTAR_FOLDER" "$GIS_DATABASE\$CUSTOM_UNTAR_FOLDER" - Delete "$TEMP\$ARCHIVE_NAME" - Goto end - - download_failed: - DetailPrint "$0" ;print error message to log - MessageBox MB_OK "Download Failed.$\r$\n${QGIS_BASE} will be installed without the $EXTENDED_ARCHIVE_NAME sample data set." - Goto end - - cancel_download: - MessageBox MB_OK "Download Canceled.$\r$\n${QGIS_BASE} will be installed without the $EXTENDED_ARCHIVE_NAME sample data set." - Goto end - - untar_failed: - DetailPrint "$0" ;print error message to log - - end: - -FunctionEnd - -Section /O "North Carolina Data Set" SecNorthCarolinaSDB - - ;Set the size (in KB) of the archive file - StrCpy $ARCHIVE_SIZE_KB 138629 - - ;Set the size (in KB) of the unpacked archive file - AddSize 293314 - - StrCpy $HTTP_PATH "https://grass.osgeo.org/sampledata" - StrCpy $ARCHIVE_NAME "nc_spm_latest.tar.gz" - StrCpy $EXTENDED_ARCHIVE_NAME "North Carolina" - StrCpy $ORIGINAL_UNTAR_FOLDER "nc_spm_08" - StrCpy $CUSTOM_UNTAR_FOLDER "North-Carolina" - - Call DownloadDataSet - -SectionEnd - -Section /O "South Dakota (Spearfish) Data Set" SecSpearfishSDB - - ;Set the size (in KB) of the archive file - StrCpy $ARCHIVE_SIZE_KB 20803 - - ;Set the size (in KB) of the unpacked archive file - AddSize 42171 - - StrCpy $HTTP_PATH "https://grass.osgeo.org/sampledata" - StrCpy $ARCHIVE_NAME "spearfish_grass60data-0.3.tar.gz" - StrCpy $EXTENDED_ARCHIVE_NAME "South Dakota (Spearfish)" - StrCpy $ORIGINAL_UNTAR_FOLDER "spearfish60" - StrCpy $CUSTOM_UNTAR_FOLDER "Spearfish60" - - Call DownloadDataSet - -SectionEnd - -Section /O "Alaska Data Set" SecAlaskaSDB - - ;Set the size (in KB) of the archive file - StrCpy $ARCHIVE_SIZE_KB 10253 - - ;Set the size (in KB) of the unpacked archive file - AddSize 33914 - - StrCpy $HTTP_PATH "https://qgis.org/downloads/data" - StrCpy $ARCHIVE_NAME "qgis_sample_data.tar.gz" - StrCpy $EXTENDED_ARCHIVE_NAME "Alaska" - StrCpy $ORIGINAL_UNTAR_FOLDER "qgis_sample_data" - StrCpy $CUSTOM_UNTAR_FOLDER "Alaska" - - Call DownloadDataSet - -SectionEnd - -;---------------------------------------------------------------------------------------------------------------------------- - -;Uninstaller Section - -!ifdef INNER -Section "Uninstall" - ${If} ${ARCH} == "x86_64" - ${If} ${RunningX64} - DetailPrint "Installer running on 64-bit host" - ; disable registry redirection (enable access to 64-bit portion of registry) - SetRegView 64 - ${EndIf} - ${EndIf} - - GetFullPathName /SHORT $0 $INSTDIR - System::Call 'Kernel32::SetEnvironmentVariableA(t, t) i("OSGEO4W_ROOT", "$0").r0' - System::Call 'Kernel32::SetEnvironmentVariableA(t, t) i("OSGEO4W_STARTMENU", "$SMPROGRAMS\${QGIS_BASE}").r0' - System::Call 'Kernel32::SetEnvironmentVariableA(t, t) i("OSGEO4W_DESKTOP", "$DESKTOP\${QGIS_BASE}").r0' - System::Call 'Kernel32::SetEnvironmentVariableA(t, t) i("OSGEO4W_MENU_LINKS", "1").r0' - System::Call 'Kernel32::SetEnvironmentVariableA(t, t) i("OSGEO4W_DESKTOP_LINKS", "1").r0' - - ReadEnvStr $0 COMSPEC - nsExec::ExecToLog '"$0" /c "$INSTDIR\preremove.bat"' - - Delete "$INSTDIR\uninstall.exe" - Delete "$INSTDIR\*.bat.done" - Delete "$INSTDIR\*.log" - Delete "$INSTDIR\*.txt" - Delete "$INSTDIR\*.ico" - Delete "$INSTDIR\*.bat" - Delete "$INSTDIR\*.dll" - - RMDir /r "$INSTDIR\bin" - RMDir /r "$INSTDIR\apps" - RMDir /r "$INSTDIR\etc" - RMDir /r "$INSTDIR\include" - RMDir /r "$INSTDIR\lib" - RMDir /r "$INSTDIR\share" - RMDir /r "$INSTDIR\icons" - RMDir /r "$INSTDIR\src" - RMDir /r "$INSTDIR\contrib" - RMDir /r "$INSTDIR\manifest" - RMDir /r "$INSTDIR\man" - - ;if empty, remove the install folder - RMDir "$INSTDIR" - - ;remove the Desktop ShortCut - SetShellVarContext all - Delete "$DESKTOP\${QGIS_BASE}\QGIS Desktop (${VERSION_NUMBER}).lnk" - Delete "$DESKTOP\${QGIS_BASE}\QGIS Browser (${VERSION_NUMBER}).lnk" - Delete "$DESKTOP\${QGIS_BASE}\OSGeo4W.lnk" - RmDir "$DESKTOP\${QGIS_BASE}" - - ;remove the Programs Start ShortCut - SetShellVarContext all - RMDir /r "$SMPROGRAMS\${QGIS_BASE}" - - ;remove the Registry Entries - DeleteRegKey HKLM "Software\${QGIS_BASE}" - DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${QGIS_BASE}" -SectionEnd -!endif - -;---------------------------------------------------------------------------------------------------------------------------- - -!ifndef INNER -;Installer Section Descriptions -!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN - !insertmacro MUI_DESCRIPTION_TEXT ${SecQGIS} "Install ${QGIS_BASE}" - !insertmacro MUI_DESCRIPTION_TEXT ${SecNorthCarolinaSDB} "Download and install the North Carolina sample data set" - !insertmacro MUI_DESCRIPTION_TEXT ${SecSpearfishSDB} "Download and install the South Dakota (Spearfish) sample data set" - !insertmacro MUI_DESCRIPTION_TEXT ${SecAlaskaSDB} "Download and install the Alaska sample database (shapefiles and TIFF data)" -!insertmacro MUI_FUNCTION_DESCRIPTION_END -!endif - -;---------------------------------------------------------------------------------------------------------------------------- diff --git a/ms-windows/osgeo4w/configonly.bat b/ms-windows/osgeo4w/configonly.bat deleted file mode 100644 index ee2aa62541879..0000000000000 --- a/ms-windows/osgeo4w/configonly.bat +++ /dev/null @@ -1,46 +0,0 @@ -@echo off -REM *************************************************************************** -REM configonly.cmd -REM --------------------- -REM begin : June 2018 -REM copyright : (C) 2018 by Juergen E. Fischer -REM email : jef at norbit dot de -REM *************************************************************************** -REM * * -REM * This program is free software; you can redistribute it and/or modify * -REM * it under the terms of the GNU General Public License as published by * -REM * the Free Software Foundation; either version 2 of the License, or * -REM * (at your option) any later version. * -REM * * -REM *************************************************************************** - -set ARCH=%1 -if "%ARCH%"=="x86" ( - set CMAKEGEN=Visual Studio 14 2015 -) else ( - set CMAKEGEN=Visual Studio 14 2015 Win64 - set ARCH=x86_64 -) - -set CONFIGONLY=1 - -setlocal enabledelayedexpansion - -for /f "tokens=*" %%L in (..\..\CMakeLists.txt) do ( - set L=%%L - set V=!L:SET(CPACK_PACKAGE_VERSION_=! - if not !V!==!L! ( - set V=!V:"=! - set V=!V:^)=! - set _major=!V:MAJOR =! - set _minor=!V:MINOR =! - set _patch=!V:PATCH =! - if not !_major!==!V! set MAJOR=!_major! - if not !_minor!==!V! set MINOR=!_minor! - if not !_patch!==!V! set PATCH=!_patch! - ) -) - -package-nightly.cmd %MAJOR%.%MINOR%.%PATCH% 99 qgis-test %ARCH% - -endlocal diff --git a/ms-windows/osgeo4w/creatensis.pl b/ms-windows/osgeo4w/creatensis.pl deleted file mode 100755 index 81d349d7989f3..0000000000000 --- a/ms-windows/osgeo4w/creatensis.pl +++ /dev/null @@ -1,567 +0,0 @@ -#!/usr/bin/env perl -# creates a NSIS installer from OSGeo4W packages -# note: works also on Unix - -# Copyright (C) 2010 Jürgen E. Fischer - -# 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. - -# -# Download OSGeo4W packages -# - -BEGIN { - # ignore requireAdministrator execution level while producing the - # uninstaller - $ENV{"__COMPAT_LAYER"} = 'RUNASINVOKER'; -} - -use strict; -use warnings; -use Getopt::Long; -use Pod::Usage; - -my $keep = 0; -my $verbose = 0; - -my $packagename = "QGIS"; -my $releasename; -my $shortname = "qgis"; -my $version; -my $binary; -my $root = "http://download.osgeo.org/osgeo4w"; -my $ininame = "setup.ini"; -my $arch = "x86_64"; -my $signwith; -my $signpass; -my $help; - -my $result = GetOptions( - "verbose+" => \$verbose, - "keep" => \$keep, - "signwith=s" => \$signwith, - "signpass=s" => \$signpass, - "releasename=s" => \$releasename, - "version=s" => \$version, - "binary=i" => \$binary, - "packagename=s" => \$packagename, - "shortname=s" => \$shortname, - "ininame=s" => \$ininame, - "mirror=s" => \$root, - "arch=s" => \$arch, - "help" => \$help - ); - -die "certificate not found" if defined $signwith && ! -f $signwith; - -pod2usage(1) if $help; - -my $wgetopt = $verbose ? "" : "-nv"; - -unless(-f "nsis/System.dll") { - mkdir "nsis", 0755 unless -d "nsis"; - system "wget $wgetopt -Onsis/System.dll http://qgis.org/downloads/System.dll"; - die "download of System.dll failed" if $?; -} - -my $archpath = $arch eq "" ? "" : "/$arch"; -my $archpostfix = $arch eq "" ? "" : "-$arch"; -my $unpacked = "unpacked" . ($arch eq "" ? "" : "-$arch"); -my $packages = "packages" . ($arch eq "" ? "" : "-$arch"); - -mkdir $packages, 0755 unless -d $packages; -chdir $packages; - -system "wget $wgetopt -c http://qgis.org/downloads/Untgz.zip" unless -f "Untgz.zip"; -die "download of Untgz.zip failed" if $?; - -system "wget $wgetopt -c https://qgis.org/downloads/Inetc.zip" unless -f "Inetc.zip"; -die "download of Inetc.zip failed" if $?; - -my %dep; -my %file; -my %lic; -my %sdesc; -my %md5; -my $package; - -system "wget $wgetopt -O setup.ini $root$archpath/$ininame"; -die "download of setup.ini failed" if $?; -open F, "setup.ini" || die "setup.ini not found"; -while() { - my $file; - my $md5; - - chop; - if(/^@ (\S+)/) { - $package = $1; - } elsif( /^requires: (.*)$/ ) { - @{$dep{$package}} = split / /, $1; - } elsif( ($file,$md5) = /^install:\s+(\S+)\s+.*\s+(\S+)$/) { - $file{$package} = $file unless exists $file{$package}; - $file =~ s/^.*\///; - $md5{$file} = $md5 unless exists $md5{$file}; - } elsif( ($file,$md5) = /^license:\s+(\S+)\s+.*\s+(\S+)$/) { - $lic{$package} = $file unless exists $lic{$package}; - $file =~ s/^.*\///; - $md5{$file} = $md5 unless exists $md5{$file}; - } elsif( /^sdesc:\s*"(.*)"\s*$/) { - $sdesc{$package} = $1 unless exists $sdesc{$package}; - } -} -close F; - -my %pkgs; - -sub getDeps { - my $pkg = shift; - - my $deponly = $pkg =~ /-$/; - $pkg =~ s/-$//; - - unless($deponly) { - return if exists $pkgs{$pkg}; - print " Including package $pkg\n" if $verbose; - $pkgs{$pkg} = 1; - } elsif( exists $pkgs{$pkg} ) { - print " Excluding package $pkg\n" if $verbose; - delete $pkgs{$pkg}; - return; - } else { - print " Including dependencies of package $pkg\n" if $verbose; - } - - foreach my $p ( @{ $dep{$pkg} } ) { - getDeps($p); - } -} - -unless(@ARGV) { - print "Defaulting to qgis-full package...\n" if $verbose; - push @ARGV, "qgis-full"; -} - -getDeps($_) for @ARGV; - -if(-f "../addons/bin/NCSEcw4_RO.dll") { - print "Enabling ECW support...\n" if $verbose; - getDeps("gdal-ecw") -} - -my @lic; -my @desc; -foreach my $p ( keys %pkgs ) { - my @f; - unless( exists $file{$p} ) { - print "No file for package $p found.\n" if $verbose; - next; - } - push @f, "$root/$file{$p}"; - - if( exists $lic{$p} ) { - push @f, "$root/$lic{$p}"; - my($l) = $lic{$p} =~ /([^\/]+)$/; - push @lic, $l; - push @desc, $sdesc{$p}; - } - - for my $f (@f) { - $f =~ s/\/\.\//\//g; - - my($file) = $f =~ /([^\/]+)$/; - - next if -f $file; - - print "Downloading $file [$f]...\n" if $verbose; - system "wget $wgetopt -c $f"; - die "download of $f failed" if $? or ! -f $file; - - if( exists $md5{$file} ) { - my $md5; - open F, "md5sum '$file'|"; - while() { - if( /^(\S+)\s+\*?(.*)$/ && $2 eq $file ) { - $md5 = $1; - } - } - close F; - - die "No md5sum of $p determined [$file]" unless defined $md5; - if( $md5 eq $md5{$file} ) { - print "md5sum of $file verified.\n" if $verbose; - } else { - die "md5sum mismatch for $file [$md5 vs $md5{$file{$p}}]" - } - } - else - { - die "md5sum for $file not found.\n"; - } - } -} - -chdir ".."; - -# -# Unpack them -# Add nircmd -# Add addons -# - -if( -d $unpacked ) { - unless( $keep ) { - print "Removing $unpacked directory\n" if $verbose; - system "rm -rf $unpacked"; - } else { - print "Keeping $unpacked directory\n" if $verbose; - } -} - -my $taropt = "v" x $verbose; - -unless(-d $unpacked ) { - mkdir "$unpacked", 0755; - mkdir "$unpacked/bin", 0755; - mkdir "$unpacked/etc", 0755; - mkdir "$unpacked/etc/setup", 0755; - - # Create package database - open O, ">$unpacked/etc/setup/installed.db"; - print O "INSTALLED.DB 2\n"; - - foreach my $pn ( keys %pkgs ) { - my $p = $file{$pn}; - unless( defined $p ) { - print "No package found for $pn\n" if $verbose; - next; - } - - $p =~ s#^.*/#$packages/#; - - unless( -r $p ) { - print "Package $p not found.\n" if $verbose; - next; - } - - print O "$pn $p 0\n"; - - print "Unpacking $p...\n" if $verbose; - system "bash -c 'tar $taropt -C $unpacked -xjvf $p | gzip -c >$unpacked/etc/setup/$pn.lst.gz && [ \${PIPESTATUS[0]} == 0 -a \${PIPESTATUS[1]} == 0 ]'"; - die "unpacking of $p failed" if $?; - } - - close O; - - chdir $unpacked; - - mkdir "bin", 0755; - - unless( -f "bin/nircmd.exe" ) { - unless( -f "../$packages/nircmd.zip" ) { - system "cd ../$packages; wget $wgetopt -c http://www.nirsoft.net/utils/nircmd.zip"; - die "download of nircmd.zip failed" if $?; - } - - mkdir "apps", 0755; - mkdir "apps/nircmd", 0755; - system "cd apps/nircmd; unzip ../../../$packages/nircmd.zip && mv nircmd.exe nircmdc.exe ../../bin"; - die "unpacking of nircmd failed" if $?; - } - - if( -d "../addons" ) { - print " Including addons...\n" if $verbose; - system "tar -C ../addons -cf - . | tar $taropt -xf -"; - die "copying of addons failed" if $?; - } - - chdir ".."; -} - -my($major, $minor, $patch); - -open F, "../../CMakeLists.txt"; -while() { - if(/SET\(CPACK_PACKAGE_VERSION_MAJOR "(\d+)"\)/i) { - $major = $1; - } elsif(/SET\(CPACK_PACKAGE_VERSION_MINOR "(\d+)"\)/i) { - $minor = $1; - } elsif(/SET\(CPACK_PACKAGE_VERSION_PATCH "(\d+)"\)/i) { - $patch = $1; - } elsif(/SET\(RELEASE_NAME "(.+)"\)/i) { - $releasename = $1 unless defined $releasename; - } -} -close F; - -$version = "$major.$minor.$patch" unless defined $version; - -my($pmajor,$pminor,$ppatch) = $version =~ /^(\d+)\.(\d+)\.(\d+)$/; -die "Invalid version $version" unless defined $ppatch; - -unless( defined $binary ) { - if( -f "binary$archpostfix-$version" ) { - open P, "binary$archpostfix-$version"; - $binary =

; - close P; - $binary++; - } else { - $binary = 1; - } -} - -# -# Create postinstall.bat -# - -open F, ">../Installer-Files/postinstall.bat"; - -my $r = ">>postinstall.log 2>&1\r\n"; - -print F "\@echo off\r\n"; -print F "if exist postinstall.log del postinstall.log\r\n"; -print F "set OSGEO4W_ROOT_MSYS=%OSGEO4W_ROOT:\\=/%$r"; -print F "if \"%OSGEO4W_ROOT_MSYS:~1,1%\"==\":\" set OSGEO4W_ROOT_MSYS=/%OSGEO4W_ROOT_MSYS:~0,1%/%OSGEO4W_ROOT_MSYS:~3%$r"; - -print F "del preremove-conf.bat$r"; -my $c = ">>preremove-conf.bat\r\n"; -print F "echo set OSGEO4W_ROOT=%OSGEO4W_ROOT%$c"; -print F "echo set OSGEO4W_ROOT_MSYS=%OSGEO4W_ROOT_MSYS%$c"; -print F "echo set OSGEO4W_STARTMENU=%OSGEO4W_STARTMENU%$c"; -print F "echo set OSGEO4W_DESKTOP=%OSGEO4W_DESKTOP%$c"; - -print F "echo OSGEO4W_ROOT=%OSGEO4W_ROOT%$r"; -print F "echo OSGEO4W_ROOT_MSYS=%OSGEO4W_ROOT_MSYS%$r"; -print F "echo OSGEO4W_STARTMENU=%OSGEO4W_STARTMENU%$r"; -print F "echo OSGEO4W_DESKTOP=%OSGEO4W_DESKTOP%$r"; -print F "PATH %OSGEO4W_ROOT%\\bin;%PATH%$r"; -print F "cd /d %OSGEO4W_ROOT%$r"; - -chdir $unpacked; -for my $p () { - $p =~ s/\//\\/g; - my($dir,$file) = $p =~ /^(.+)\\([^\\]+)$/; - - print F "echo Running postinstall $file...$r"; - print F "%COMSPEC% /c $p$r"; - print F "ren $p $file.done$r"; -} -chdir ".."; - -print F "ren postinstall.bat postinstall.bat.done$r"; - -close F; - -open F, ">../Installer-Files/preremove.bat"; - -$r = ">>%TEMP%\\$packagename-OSGeo4W-$version-$binary-preremove.log 2>&1\r\n"; - -print F "\@echo off\r\n"; -print F "call \"%~dp0\\preremove-conf.bat\"$r"; -print F "echo OSGEO4W_ROOT=%OSGEO4W_ROOT%$r"; -print F "echo OSGEO4W_STARTMENU=%OSGEO4W_STARTMENU%$r"; -print F "echo OSGEO4W_DESKTOP=%OSGEO4W_DESKTOP%$r"; -print F "set OSGEO4W_ROOT_MSYS=%OSGEO4W_ROOT:\\=/%$r"; -print F "if \"%OSGEO4W_ROOT_MSYS:~1,1%\"==\":\" set OSGEO4W_ROOT_MSYS=/%OSGEO4W_ROOT_MSYS:~0,1%/%OSGEO4W_ROOT_MSYS:~3%$r"; -print F "echo OSGEO4W_ROOT_MSYS=%OSGEO4W_ROOT_MSYS%$r"; -print F "PATH %OSGEO4W_ROOT%\\bin;%PATH%$r"; -print F "cd /d \"%OSGEO4W_ROOT%\"$r"; - -chdir $unpacked; -for my $p () { - $p =~ s/\//\\/g; - my($dir,$file) = $p =~ /^(.+)\\([^\\]+)$/; - - print F "echo Running preremove $file...$r"; - print F "%COMSPEC% /c $p$r"; - print F "ren $p $file.done$r"; -} -chdir ".."; - -print F "ren preremove.bat preremove.bat.done$r"; - -close F; - -unless(-d "untgz") { - system "unzip $packages/Untgz.zip"; - die "unpacking Untgz.zip failed" if $?; -} - -unless(-d "inetc") { - mkdir "inetc", 0755; - system "unzip -p $packages/Inetc.zip Plugins/x86-ansi/INetC.dll >inetc/INetC.dll"; - die "unpacking Inetc.zip failed" if $?; -} - -chdir ".."; - - -print "Creating license file\n" if $verbose; -open O, ">license.tmp"; -my $lic; -for my $l ( ( "osgeo4w/$unpacked/apps/$shortname/doc/LICENSE", "../COPYING", "./Installer-Files/LICENSE.txt" ) ) { - next unless -f $l; - $lic = $l; - last; -} - -die "no license found" unless defined $lic; - -my $i = 0; -if( @lic ) { - print O "License overview:\n"; - print O "1. QGIS\n"; - $i = 1; - for my $l ( @desc ) { - print O ++$i . ". $l\n"; - } - $i = 0; - print O "\n\n----------\n\n" . ++$i . ". License of 'QGIS'\n\n"; -} - -print " Including QGIS license $lic\n" if $verbose; -open I, $lic; -while() { - s/\s*$/\n/; - print O; -} -close I; - -for my $l (@lic) { - print " Including license $l\n" if $verbose; - - open I, "osgeo4w/$packages/$l" or die "License $l not found."; - print O "\n\n----------\n\n" . ++$i . ". License of '" . shift(@desc) . "'\n\n"; - while() { - s/\s*$/\n/; - print O; - } - close I; -} - -close O; - -my $license = "license.tmp"; -if( -f "osgeo4w/$unpacked/apps/$shortname/doc/LICENSE" ) { - open O, ">osgeo4w/$unpacked/apps/$shortname/doc/LICENSE"; - open I, $license; - while() { - print O; - } - close O; - close I; - - $license = "osgeo4w/$unpacked/apps/$shortname/doc/LICENSE"; -} - - -print "Running NSIS\n" if $verbose; - -my $installerbase = "$packagename-OSGeo4W-$version-$binary-Setup$archpostfix"; - -my $run; -my $instdest; - -if($^O eq "cygwin") { - $run = "cygstart "; - $instdest = `cygpath -w \$PWD`; -} else { - $run = "wine "; - $instdest = `winepath -w \$PWD`; -} - -$instdest =~ s/\s+$//; -$instdest =~ s/\\/\\\\/g; - - -my $args = ""; -$args .= " -V$verbose"; -$args .= " -DVERSION_NAME='$releasename'"; -$args .= " -DVERSION_NUMBER='$version'"; -$args .= " -DBINARY_REVISION=$binary"; -$args .= sprintf( " -DVERSION_INT='%d%02d%02d%02d'", $pmajor, $pminor, $ppatch, $binary ); -$args .= sprintf( " -DQGIS_BASE='$packagename %d.%d'", $pmajor, $pminor ); -$args .= " -DDISPLAYED_NAME=\"$packagename $version '$releasename'\""; -$args .= " -DPACKAGE_FOLDER=osgeo4w/$unpacked"; -$args .= " -DLICENSE_FILE='$license'"; -$args .= " -DARCH='$arch'"; -$args .= " QGIS-Installer.nsi"; - -sub sign { - my $base = shift; - - my $cmd = "osslsigncode sign"; - $cmd .= " -pkcs12 \"$signwith\""; - $cmd .= " -pass \"$signpass\"" if defined $signpass; - $cmd .= " -n \"$packagename $version '$releasename'\""; - $cmd .= " -h sha256"; - $cmd .= " -i \"https://qgis.org\""; - $cmd .= " -t \"http://timestamp.digicert.com\""; - $cmd .= " -in \"$base.exe\""; - $cmd .= " $base-signed.exe"; - system $cmd; - die "signing failed [$cmd]" if $?; - - rename("$base-signed.exe", "$base.exe") or die "rename failed: $!"; -} - -my $cmd; -unlink "makeuinst.exe"; -$cmd = "makensis -DINNER=1 -DUNINSTALLERDEST='$instdest' -DINSTALLER_NAME='makeuinst.exe' $args"; -system $cmd; -die "running makensis failed [$cmd]" if $?; -die "makeuinst.exe not created" unless -f "makeuinst.exe"; - -unlink "uninstall.exe"; -chmod 0755, "makeuinst.exe"; -system "${run}makeuinst.exe"; -sleep 5; -die "uninstall.exe not created" unless -f "uninstall.exe"; -unlink "makeuinst.exe"; - -sign "uninstall" if $signwith; - -$cmd = "makensis -DINSTALLER_NAME='$installerbase.exe' $args"; -system $cmd; -die "running makensis failed [$cmd]" if $?; - -sign "$installerbase" if $signwith; - -open P, ">osgeo4w/binary$archpostfix-$version"; -print P $binary; -close P; - -system "md5sum $installerbase.exe >$installerbase.exe.md5sum"; - -__END__ - -=head1 NAME - -creatensis.pl - create NSIS package from OSGeo4W packages - -=head1 SYNOPSIS - -creatensis.pl [options] [packages...] - - Options: - -verbose increase verbosity - -releasename=name name of release (defaults to CMakeLists.txt setting) - -keep don't start with a fresh unpacked directory - -signwith=cert.p12 optionally sign package with certificate (requires osslsigncode) - -signpass=password password of certificate - -version=m.m.p package version (defaults to CMakeLists.txt setting) - -binary=b binary version of package - -ininame=filename name of the setup.ini (defaults to setup.ini) - -packagename=s name of package (defaults to 'QGIS') - -shortname=s shortname used for batch file (defaults to 'qgis') - -mirror=s default mirror (defaults to 'http://download.osgeo.org/osgeo4w') - -arch=s architecture (x86 or x86_64; defaults to 'x86_64') - -help this help - - If no packages are given 'qgis-full' and it's dependencies will be retrieved - and packaged. - - Packages with a appended '-' are excluded, but their dependencies are included. -=cut diff --git a/ms-windows/osgeo4w/designer.bat.tmpl b/ms-windows/osgeo4w/designer.bat.tmpl deleted file mode 100644 index 6778fb445cf05..0000000000000 --- a/ms-windows/osgeo4w/designer.bat.tmpl +++ /dev/null @@ -1,9 +0,0 @@ -@echo off -call "%~dp0\o4w_env.bat" -call qt5_env.bat -call py3_env.bat -path %OSGEO4W_ROOT%\apps\@package@\bin;%PATH% -set QGIS_PREFIX_PATH=%OSGEO4W_ROOT:\=/%/apps/@package@ -set QT_PLUGIN_PATH=%OSGEO4W_ROOT%\apps\@package@\qtplugins;%OSGEO4W_ROOT%\apps\qt5\plugins -cd %USERPROFILE% -start "Qt Designer with QGIS custom widgets" /B "%OSGEO4W_ROOT%\apps\qt5\bin\designer.exe" %* diff --git a/ms-windows/osgeo4w/httpd.conf.tmpl b/ms-windows/osgeo4w/httpd.conf.tmpl deleted file mode 100644 index 382be280173da..0000000000000 --- a/ms-windows/osgeo4w/httpd.conf.tmpl +++ /dev/null @@ -1,28 +0,0 @@ -LoadModule fcgid_module modules/mod_fcgid.so - -DefaultInitEnv O4W_QT_PREFIX "@osgeo4w@/apps/Qt5" -DefaultInitEnv O4W_QT_BINARIES "@osgeo4w@/apps/Qt5/bin" -DefaultInitEnv O4W_QT_PLUGINS "@osgeo4w@/apps/Qt5/plugins" -DefaultInitEnv O4W_QT_LIBRARIES "@osgeo4w@/apps/Qt5/lib" -DefaultInitEnv O4W_QT_TRANSLATIONS "@osgeo4w@/apps/Qt5/translations" -DefaultInitEnv O4W_QT_HEADERS "@osgeo4w@/apps/Qt5/include" -DefaultInitEnv O4W_QT_DOC "@osgeo4w@/apps/Qt5/doc" - -DefaultInitEnv PATH "@osgeo4w@\apps\qt5\bin;@osgeo4w@\bin;@osgeo4w@\apps\@package@\bin;@osgeo4w@\apps\grass\@grasspath@\bin;@osgeo4w@\apps\grass\@grasspath@\lib;@windir@\system32;@windir@;@windir@\System32\Wbem" -DefaultInitEnv QGIS_PREFIX_PATH "@osgeo4w@\apps\@package@" -DefaultInitEnv QT_PLUGIN_PATH "@osgeo4w@\apps\@package@\qtplugins;@osgeo4w@\apps\qt5\plugins" -DefaultInitEnv TEMP "@temp@" -DefaultInitEnv PYTHONHOME "@osgeo4w@\apps\Python37" -DefaultInitEnv PYTHONPATH "@osgeo4w@\apps\Python37;@osgeo4w@\apps\Python37\Scripts" - -Alias /@package@/ @osgeo4w@/apps/@package@/bin/ - - - SetHandler fcgid-script - Options ExecCGI - # Order/Allow is for Apache 2.2 - #Order allow,deny - #Allow from all - # Require is for Apache 2.4 - Require all granted - diff --git a/ms-windows/osgeo4w/msvc-env.bat b/ms-windows/osgeo4w/msvc-env.bat deleted file mode 100644 index 0dae07f8be412..0000000000000 --- a/ms-windows/osgeo4w/msvc-env.bat +++ /dev/null @@ -1,91 +0,0 @@ -@echo off -REM *************************************************************************** -REM msvc-env.cmd -REM --------------------- -REM begin : June 2018 -REM copyright : (C) 2018 by Juergen E. Fischer -REM email : jef at norbit dot de -REM *************************************************************************** -REM * * -REM * This program is free software; you can redistribute it and/or modify * -REM * it under the terms of the GNU General Public License as published by * -REM * the Free Software Foundation; either version 2 of the License, or * -REM * (at your option) any later version. * -REM * * -REM *************************************************************************** - -if not "%PROGRAMFILES(X86)%"=="" set PF86=%PROGRAMFILES(X86)% -if "%PF86%"=="" set PF86=%PROGRAMFILES% -if "%PF86%"=="" (echo PROGRAMFILES not set & goto error) - -if "%VCSDK%"=="" set VCSDK=10.0.14393.0 - -set ARCH=%1 -if "%ARCH%"=="x86" goto x86 -if "%ARCH%"=="x86_64" goto x86_64 -goto usage - -:x86 -set VCARCH=x86 -set CMAKE_COMPILER_PATH=%PF86%\Microsoft Visual Studio 14.0\VC\bin -set DBGHLP_PATH=%PF86%\Microsoft Visual Studio 14.0\Common7\IDE\Remote Debugger\x86 -set SETUPAPI_LIBRARY=%PF86%\Windows Kits\10\Lib\%VCSDK%\um\x86\SetupAPI.Lib -goto archset - -:x86_64 -set VCARCH=amd64 -set CMAKE_COMPILER_PATH=%PF86%\Microsoft Visual Studio 14.0\VC\bin\amd64 -set DBGHLP_PATH=%PF86%\Microsoft Visual Studio 14.0\Common7\IDE\Remote Debugger\x64 -set SETUPAPI_LIBRARY=%PF86%\Windows Kits\10\Lib\%VCSDK%\um\x64\SetupAPI.Lib - -:archset -if not exist "%SETUPAPI_LIBRARY%" (echo SETUPAPI_LIBRARY not found & goto error) - -if "%CC%"=="" set CC=%CMAKE_COMPILER_PATH:\=/%/cl.exe -if "%CXX%"=="" set CXX=%CMAKE_COMPILER_PATH:\=/%/cl.exe -set CLCACHE_CL=%CMAKE_COMPILER_PATH:\=/%/cl.exe - -if "%OSGEO4W_ROOT%"=="" if "%ARCH%"=="x86" ( - set OSGEO4W_ROOT=C:\OSGeo4W -) else ( - set OSGEO4W_ROOT=C:\OSGeo4W64 -) - -if not exist "%OSGEO4W_ROOT%\bin\o4w_env.bat" (echo o4w_env.bat not found & goto error) -call "%OSGEO4W_ROOT%\bin\o4w_env.bat" -call "%OSGEO4W_ROOT%\bin\py3_env.bat" -call "%OSGEO4W_ROOT%\bin\qt5_env.bat" - -set VS140COMNTOOLS=%PF86%\Microsoft Visual Studio 14.0\Common7\Tools\ -call "%PF86%\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" %VCARCH% - -path %path%;%PF86%\Microsoft Visual Studio 14.0\VC\bin - -set GRASS7= -if exist %OSGEO4W_ROOT%\bin\grass74.bat set GRASS7=%OSGEO4W_ROOT%\bin\grass74.bat -if exist %OSGEO4W_ROOT%\bin\grass76.bat set GRASS7=%OSGEO4W_ROOT%\bin\grass76.bat -if exist %OSGEO4W_ROOT%\bin\grass78.bat set GRASS7=%OSGEO4W_ROOT%\bin\grass78.bat -if "%GRASS7%"=="" (echo GRASS7 not found & goto error) -for /f "usebackq tokens=1" %%a in (`%GRASS7% --config path`) do set GRASS_PREFIX=%%a - -set PYTHONPATH= -if exist "%PROGRAMFILES%\CMake\bin" path %PATH%;%PROGRAMFILES%\CMake\bin -if exist "%PF86%\CMake\bin" path %PATH%;%PF86%\CMake\bin -if exist c:\cygwin64\bin path %PATH%;c:\cygwin64\bin -if exist c:\cygwin\bin path %PATH%;c:\cygwin\bin - -set LIB=%LIB%;%OSGEO4W_ROOT%\apps\Qt5\lib;%OSGEO4W_ROOT%\lib -set INCLUDE=%INCLUDE%;%OSGEO4W_ROOT%\apps\Qt5\include;%OSGEO4W_ROOT%\include - -goto end - -:usage -echo usage: %0 [x86^|x86_64] -echo sample: %0 x86_64 -exit /b 1 - -:error -echo ENV ERROR %ERRORLEVEL%: %DATE% %TIME% -exit /b 1 - -:end diff --git a/ms-windows/osgeo4w/ninja/ninja.bat b/ms-windows/osgeo4w/ninja/ninja.bat deleted file mode 100644 index e8b08649b1446..0000000000000 --- a/ms-windows/osgeo4w/ninja/ninja.bat +++ /dev/null @@ -1,7 +0,0 @@ -@echo off -call %OSGEO4W_ROOT%\bin\o4w_env.bat -call py3_env.bat -call qt5_env.bat -call "c:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" amd64 -path %PATH%;c:\cygwin\bin;c:\program files\cmake\bin -%OSGEO4W_ROOT%\bin\ninja -j4 -C ..\build-qgis-dev-x86_64 diff --git a/ms-windows/osgeo4w/ninja/ninja.sln b/ms-windows/osgeo4w/ninja/ninja.sln deleted file mode 100644 index 7a7812b16bd65..0000000000000 --- a/ms-windows/osgeo4w/ninja/ninja.sln +++ /dev/null @@ -1,22 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ninja", "ninja.vcxproj", "{02B448C7-945C-46D6-954C-AEAE0653BA59}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - RelWithDebInfo|Win32 = RelWithDebInfo|Win32 - RelWithDebInfo|x64 = RelWithDebInfo|x64 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {02B448C7-945C-46D6-954C-AEAE0653BA59}.RelWithDebInfo|Win32.ActiveCfg = Release|Win32 - {02B448C7-945C-46D6-954C-AEAE0653BA59}.RelWithDebInfo|Win32.Build.0 = Release|Win32 - {02B448C7-945C-46D6-954C-AEAE0653BA59}.RelWithDebInfo|x64.ActiveCfg = Release|x64 - {02B448C7-945C-46D6-954C-AEAE0653BA59}.RelWithDebInfo|x64.Build.0 = Release|x64 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/ms-windows/osgeo4w/ninja/ninja.vcxproj b/ms-windows/osgeo4w/ninja/ninja.vcxproj deleted file mode 100644 index 347ae90b71769..0000000000000 --- a/ms-windows/osgeo4w/ninja/ninja.vcxproj +++ /dev/null @@ -1,108 +0,0 @@ - - - - - Debug - Win32 - - - Release - Win32 - - - Debug - x64 - - - Release - x64 - - - - {02B448C7-945C-46D6-954C-AEAE0653BA59} - MakeFileProj - 8.1 - - - - Makefile - true - v140 - - - Makefile - false - v140 - - - Makefile - true - v140 - - - Makefile - false - v140 - - - - - - - - - - - - - - - - - - - - - ninja -C ../build-qgis-dev-x86 all - ..\build-qgis-dev-x86\output\bin\qgis.exe - WIN32;_DEBUG;$(NMakePreprocessorDefinitions) - ..\build-qgis-dev-x86 - - ninja -C ../build-qgis-dev-x86 clean all - ninja -C ../build-qgis-dev-x86 clean - - - ninja -C ../build-qgis-dev-x86 all - ..\build-qgis-dev-x86\output\bin\qgis.exe - WIN32;NDEBUG;$(NMakePreprocessorDefinitions) - ..\build-qgis-dev-x86 - - ninja -C ../build-qgis-dev-x86 clean all - ninja -C ../build-qgis-dev-x86 clean - - - ninja -C ../build-qgis-dev-x86_64 -j4 -k1000 all - ninja -C ../build-qgis-dev-x86_64 -j4 -k1000 clean all - ninja -C ../build-qgis-dev-x86_64 clean - ..\build-qgis-dev-x86_64\output\bin\qgis.exe - - - ninja -C ../build-qgis-dev-x86_64 -j4 -k1000 all - ninja -C ../build-qgis-dev-x86_64 -j4 -k1000 clean all - ninja -C ../build-qgis-dev-x86_64 clean - ..\build-qgis-dev-x86_64\output\bin\qgis.exe - - - - x86.log - - - - - x86.log - - - - - - \ No newline at end of file diff --git a/ms-windows/osgeo4w/package-nightly.cmd b/ms-windows/osgeo4w/package-nightly.cmd deleted file mode 100644 index f1e89aaf83327..0000000000000 --- a/ms-windows/osgeo4w/package-nightly.cmd +++ /dev/null @@ -1,325 +0,0 @@ -@echo off -REM *************************************************************************** -REM package-nightly.cmd -REM --------------------- -REM begin : January 2011 -REM copyright : (C) 2011 by Juergen E. Fischer -REM email : jef at norbit dot de -REM *************************************************************************** -REM * * -REM * This program is free software; you can redistribute it and/or modify * -REM * it under the terms of the GNU General Public License as published by * -REM * the Free Software Foundation; either version 2 of the License, or * -REM * (at your option) any later version. * -REM * * -REM *************************************************************************** - -setlocal enabledelayedexpansion - -set VERSION=%1 -set PACKAGE=%2 -set PACKAGENAME=%3 -set ARCH=%4 -set SHA=%5 -set SITE=%6 -if "%VERSION%"=="" goto usage -if "%PACKAGE%"=="" goto usage -if "%PACKAGENAME%"=="" goto usage -if "%ARCH%"=="" goto usage -if not "%SHA%"=="" set SHA=-%SHA% -if "%SITE%"=="" set SITE=qgis.org -if "%TARGET%"=="" set TARGET=Nightly -if "%BUILDNAME%"=="" set BUILDNAME=%PACKAGENAME%-%VERSION%%SHA%-%TARGET%-VC14-%ARCH% - -if "%BUILDDIR%"=="" set BUILDDIR=%CD%\build-%PACKAGENAME%-%ARCH% -if not exist "%BUILDDIR%" mkdir %BUILDDIR% -if not exist "%BUILDDIR%" (echo could not create build directory %BUILDDIR% & goto error) - -call msvc-env.bat %ARCH% -call gdal-dev-env.bat - -set O4W_ROOT=%OSGEO4W_ROOT:\=/% -set LIB_DIR=%O4W_ROOT% - -if "%ARCH%"=="x86" ( - set CMAKE_OPT=^ - -D SPATIALINDEX_LIBRARY=%O4W_ROOT%/lib/spatialindex-32.lib -) else ( - set CMAKE_OPT=^ - -D SPATIALINDEX_LIBRARY=%O4W_ROOT%/lib/spatialindex-64.lib ^ - -D CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_NO_WARNINGS=TRUE -) - -for %%i in ("%GRASS_PREFIX%") do set GRASS7_VERSION=%%~nxi -set GRASS_VERSIONS=%GRASS7_VERSION% - -set TAR=tar.exe -if exist "c:\cygwin\bin\tar.exe" set TAR=c:\cygwin\bin\tar.exe -if exist "c:\cygwin64\bin\tar.exe" set TAR=c:\cygwin64\bin\tar.exe - -set BUILDCONF=RelWithDebInfo - -cd ..\.. -set SRCDIR=%CD% - -if "%BUILDDIR:~1,1%"==":" %BUILDDIR:~0,2% -cd %BUILDDIR% - -set PKGDIR=%OSGEO4W_ROOT%\apps\%PACKAGENAME% - -if exist repackage goto package - -if not exist build.log goto build - -REM -REM try renaming the logfile to see if it's locked -REM - -if exist build.tmp del build.tmp -if exist build.tmp (echo could not remove build.tmp & goto error) - -ren build.log build.tmp -if exist build.log goto locked -if not exist build.tmp goto locked - -ren build.tmp build.log -if exist build.tmp goto locked -if not exist build.log goto locked - -goto build - -:locked -echo Logfile locked -if exist build.tmp del build.tmp -goto error - -:build -echo BEGIN: %DATE% %TIME% - -set >buildenv.log - -if exist qgsversion.h del qgsversion.h - -if exist CMakeCache.txt if exist skipcmake goto skipcmake - -touch %SRCDIR%\CMakeLists.txt - -echo CMAKE: %DATE% %TIME% - -if "%CMAKEGEN%"=="" set CMAKEGEN=Ninja -if "%OSGEO4W_CXXFLAGS%"=="" set OSGEO4W_CXXFLAGS=/MD /Z7 /MP /Od /D NDEBUG - -for %%i in (%PYTHONHOME%) do set PYVER=%%~ni - -cmake -G "%CMAKEGEN%" ^ - -D CMAKE_CXX_COMPILER="%CXX:\=/%" ^ - -D CMAKE_C_COMPILER="%CC:\=/%" ^ - -D CMAKE_LINKER="%CMAKE_COMPILER_PATH:\=/%/link.exe" ^ - -D CMAKE_CXX_FLAGS_RELWITHDEBINFO="%OSGEO4W_CXXFLAGS%" ^ - -D CMAKE_PDB_OUTPUT_DIRECTORY_RELWITHDEBINFO=%BUILDDIR%\apps\%PACKAGENAME%\pdb ^ - -D SUBMIT_URL="https://cdash.orfeo-toolbox.org/submit.php?project=QGIS" ^ - -D BUILDNAME="%BUILDNAME%" ^ - -D SITE="%SITE%" ^ - -D PEDANTIC=TRUE ^ - -D WITH_QSPATIALITE=TRUE ^ - -D WITH_SERVER=TRUE ^ - -D SERVER_SKIP_ECW=TRUE ^ - -D WITH_GRASS=TRUE ^ - -D WITH_3D=TRUE ^ - -D WITH_GRASS7=TRUE ^ - -D WITH_HANA=TRUE ^ - -D GRASS_PREFIX7=%GRASS_PREFIX:\=/% ^ - -D WITH_ORACLE=TRUE ^ - -D WITH_CUSTOM_WIDGETS=TRUE ^ - -D CMAKE_BUILD_TYPE=%BUILDCONF% ^ - -D CMAKE_CONFIGURATION_TYPES=%BUILDCONF% ^ - -D SETUPAPI_LIBRARY="%SETUPAPI_LIBRARY%" ^ - -D PROJ_LIBRARY=%O4W_ROOT%/apps/proj-dev/lib/proj.lib ^ - -D PROJ_INCLUDE_DIR=%O4W_ROOT%/apps/proj-dev/include ^ - -D GDAL_LIBRARY=%O4W_ROOT%/apps/gdal-dev/lib/gdal_i.lib ^ - -D GDAL_INCLUDE_DIR=%O4W_ROOT%/apps/gdal-dev/include ^ - -D GEOS_LIBRARY=%O4W_ROOT%/lib/geos_c.lib ^ - -D SQLITE3_LIBRARY=%O4W_ROOT%/lib/sqlite3_i.lib ^ - -D SPATIALITE_LIBRARY=%O4W_ROOT%/lib/spatialite_i.lib ^ - -D PYTHON_EXECUTABLE=%O4W_ROOT%/bin/python3.exe ^ - -D SIP_BINARY_PATH=%PYTHONHOME:\=/%/sip.exe ^ - -D PYTHON_INCLUDE_DIR=%PYTHONHOME:\=/%/include ^ - -D PYTHON_LIBRARY=%PYTHONHOME:\=/%/libs/%PYVER%.lib ^ - -D QT_LIBRARY_DIR=%O4W_ROOT%/lib ^ - -D QT_HEADERS_DIR=%O4W_ROOT%/apps/qt5/include ^ - -D CMAKE_INSTALL_PREFIX=%O4W_ROOT%/apps/%PACKAGENAME% ^ - -D FCGI_INCLUDE_DIR=%O4W_ROOT%/include ^ - -D FCGI_LIBRARY=%O4W_ROOT%/lib/libfcgi.lib ^ - -D QCA_INCLUDE_DIR=%OSGEO4W_ROOT%\apps\Qt5\include\QtCrypto ^ - -D QCA_LIBRARY=%OSGEO4W_ROOT%\apps\Qt5\lib\qca-qt5.lib ^ - -D QSCINTILLA_LIBRARY=%OSGEO4W_ROOT%\apps\Qt5\lib\qscintilla2.lib ^ - -D DART_TESTING_TIMEOUT=60 ^ - -D PUSH_TO_CDASH=TRUE ^ - %CMAKE_OPT% ^ - %SRCDIR:\=/% -if errorlevel 1 (echo cmake failed & goto error) - -if "%CONFIGONLY%"=="1" (echo Exiting after configuring build directory: %CD% & goto end) - -:skipcmake -if exist ..\noclean (echo skip clean & goto skipclean) -echo CLEAN: %DATE% %TIME% -cmake --build %BUILDDIR% --target clean --config %BUILDCONF% -if errorlevel 1 (echo clean failed & goto error) - -:skipclean -if exist ..\skipbuild (echo skip build & goto skipbuild) -echo ALL_BUILD: %DATE% %TIME% -cmake --build %BUILDDIR% --target %TARGET%Build --config %BUILDCONF% -set /P tag=<%BUILDDIR%\Testing\TAG -findstr "" %BUILDDIR%\Testing\%tag%\Build.xml >nul -if not errorlevel 1 ( - cmake --build %BUILDDIR% --target %TARGET%Submit --config %BUILDCONF% - if errorlevel 1 echo SUBMITTING BUILD ERRORS WAS NOT SUCCESSFUL. - echo build failed - goto error -) - -:skipbuild -if exist ..\skiptests goto skiptests - -echo RUN_TESTS: %DATE% %TIME% - -reg add "HKCU\Software\Microsoft\Windows\Windows Error Reporting" /v DontShow /t REG_DWORD /d 1 /f - -set oldtemp=%TEMP% -set oldtmp=%TMP% -set oldpath=%PATH% - -set TEMP=%TEMP%\%PACKAGENAME%-%ARCH% -set TMP=%TEMP% -if exist "%TEMP%" rmdir /s /q "%TEMP%" -mkdir "%TEMP%" - -for %%g IN (%GRASS_VERSIONS%) do ( - set path=!path!;%OSGEO4W_ROOT%\apps\grass\%%g\lib - set GISBASE=%OSGEO4W_ROOT%\apps\grass\%%g -) -PATH %path%;%BUILDDIR%\output\plugins -set QT_PLUGIN_PATH=%BUILDDIR%\output\plugins;%OSGEO4W_ROOT%\apps\qt5\plugins - -cmake --build %BUILDDIR% --target %TARGET%Test --config %BUILDCONF% -if errorlevel 1 echo TESTS WERE NOT SUCCESSFUL. - -set TEMP=%oldtemp% -set TMP=%oldtmp% -PATH %oldpath% - -cmake --build %BUILDDIR% --target %TARGET%Submit --config %BUILDCONF% -if errorlevel 1 echo TEST SUBMISSION WAS NOT SUCCESSFUL. - -:skiptests -if exist ..\skippackage goto end - -if exist "%PKGDIR%" ( - echo REMOVE: %DATE% %TIME% - rmdir /s /q "%PKGDIR%" -) - -echo INSTALL: %DATE% %TIME% -cmake --build %BUILDDIR% --target install --config %BUILDCONF% -if errorlevel 1 (echo INSTALL failed & goto error) - -:package -echo PACKAGE: %DATE% %TIME% - -cd .. - -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' -e 's/@grassversions@/%GRASS_VERSIONS%/g' postinstall-dev.bat >%OSGEO4W_ROOT%\etc\postinstall\%PACKAGENAME%.bat -if errorlevel 1 (echo creation of desktop postinstall failed & goto error) - -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' -e 's/@grassversions@/%GRASS_VERSIONS%/g' preremove-dev.bat >%OSGEO4W_ROOT%\etc\preremove\%PACKAGENAME%.bat -if errorlevel 1 (echo creation of desktop preremove failed & goto error) - -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' -e 's/^call py3_env.bat/call gdal-dev-py3-env.bat/' designer.bat.tmpl >%OSGEO4W_ROOT%\bin\%PACKAGENAME%-designer.bat.tmpl -if errorlevel 1 (echo creation of designer template failed & goto error) -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' qgis.reg.tmpl >%PKGDIR%\bin\qgis.reg.tmpl -if errorlevel 1 (echo creation of registry template & goto error) - -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' -e 's/^call py3_env.bat/call gdal-dev-py3-env.bat/' qgis.bat.tmpl >%OSGEO4W_ROOT%\bin\%PACKAGENAME%.bat.tmpl -if errorlevel 1 (echo creation of desktop template failed & goto error) - -set batches=bin/%PACKAGENAME%.bat.tmpl -for %%g IN (%GRASS_VERSIONS%) do ( - for /f "usebackq tokens=1" %%a in (`%%g --config version`) do set gv=%%a - for /F "delims=." %%i in ("!gv!") do set v=%%i - - sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' -e 's/@grasspath@/%%g/g' -e 's/@grassversion@/!gv!/g' -e 's/^call py3_env.bat/call gdal-dev-py3-env.bat/' qgis-grass.bat.tmpl >%OSGEO4W_ROOT%\bin\%PACKAGENAME%-g!v!.bat.tmpl - if errorlevel 1 (echo creation of desktop template failed & goto error) - set batches=!batches! bin/%PACKAGENAME%-g!v!.bat.tmpl -) - -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' -e 's/^call py3_env.bat/call gdal-dev-py3-env.bat/' python.bat.tmpl >%OSGEO4W_ROOT%\bin\python-%PACKAGENAME%.bat.tmpl -if errorlevel 1 (echo creation of python wrapper template failed & goto error) - -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' -e 's/^call py3_env.bat/call gdal-dev-py3-env.bat/' process.bat.tmpl >%OSGEO4W_ROOT%\bin\qgis_process-%PACKAGENAME%.bat.tmpl -if errorlevel 1 (echo creation of qgis process wrapper template failed & goto error) - -touch exclude -if exist ..\skipbuild (echo skip build & goto skipbuild) - -move %PKGDIR%\bin\qgis.exe %OSGEO4W_ROOT%\bin\%PACKAGENAME%-bin.exe -if errorlevel 1 (echo move of desktop executable failed & goto error) -copy qgis.vars %OSGEO4W_ROOT%\bin\%PACKAGENAME%-bin.vars -if errorlevel 1 (echo copy of desktop executable vars failed & goto error) - -if not exist %PKGDIR%\qtplugins\sqldrivers mkdir %PKGDIR%\qtplugins\sqldrivers -move %OSGEO4W_ROOT%\apps\qt5\plugins\sqldrivers\qsqlocispatial.dll %PKGDIR%\qtplugins\sqldrivers -if errorlevel 1 (echo move of oci sqldriver failed & goto error) -move %OSGEO4W_ROOT%\apps\qt5\plugins\sqldrivers\qsqlspatialite.dll %PKGDIR%\qtplugins\sqldrivers -if errorlevel 1 (echo move of spatialite sqldriver failed & goto error) - -if not exist %PKGDIR%\qtplugins\designer mkdir %PKGDIR%\qtplugins\designer -move %OSGEO4W_ROOT%\apps\qt5\plugins\designer\qgis_customwidgets.dll %PKGDIR%\qtplugins\designer -if errorlevel 1 (echo move of customwidgets failed & goto error) - -if not exist %PKGDIR%\python\PyQt5\uic\widget-plugins mkdir %PKGDIR%\python\PyQt5\uic\widget-plugins -move %PYTHONHOME%\Lib\site-packages\PyQt5\uic\widget-plugins\qgis_customwidgets.py %PKGDIR%\python\PyQt5\uic\widget-plugins -if errorlevel 1 (echo move of customwidgets binding failed & goto error) - -for %%i in (dbghelp.dll symsrv.dll) do ( - copy "%DBGHLP_PATH%\%%i" %OSGEO4W_ROOT%\apps\%PACKAGENAME% - if errorlevel 1 (echo %%i not found & goto error) -) - -if not exist %ARCH%\release\qgis\%PACKAGENAME% mkdir %ARCH%\release\qgis\%PACKAGENAME% -%TAR% -C %OSGEO4W_ROOT% -cjf %ARCH%/release/qgis/%PACKAGENAME%/%PACKAGENAME%-%VERSION%-%PACKAGE%.tar.bz2 ^ - --exclude-from exclude ^ - --exclude "*.pyc" ^ - apps/%PACKAGENAME% ^ - bin/%PACKAGENAME%-bin.exe ^ - bin/%PACKAGENAME%-bin.vars ^ - %batches% ^ - bin/%PACKAGENAME%-designer.bat.tmpl ^ - bin/python-%PACKAGENAME%.bat.tmpl ^ - bin/qgis_process-%PACKAGENAME%.bat.tmpl ^ - etc/postinstall/%PACKAGENAME%.bat ^ - etc/preremove/%PACKAGENAME%.bat -if errorlevel 1 (echo tar failed & goto error) - -if not exist %ARCH%\release\qgis\%PACKAGENAME%-pdb mkdir %ARCH%\release\qgis\%PACKAGENAME%-pdb -%TAR% -C %BUILDDIR% -cjf %ARCH%/release/qgis/%PACKAGENAME%-pdb/%PACKAGENAME%-pdb-%VERSION%-%PACKAGE%.tar.bz2 ^ - apps/%PACKAGENAME%/pdb -if errorlevel 1 (echo tar failed & goto error) - -goto end - -:usage -echo usage: %0 version package packagename arch [sha [site]] -echo sample: %0 2.11.0 38 qgis-dev x86_64 339dbf1 qgis.org -exit /b 1 - -:error -echo BUILD ERROR %ERRORLEVEL%: %DATE% %TIME% -if exist %PACKAGENAME%-%VERSION%-%PACKAGE%.tar.bz2 del %PACKAGENAME%-%VERSION%-%PACKAGE%.tar.bz2 -exit /b 1 - -:end -echo FINISHED: %DATE% %TIME% - -endlocal diff --git a/ms-windows/osgeo4w/package.cmd b/ms-windows/osgeo4w/package.cmd deleted file mode 100644 index ee7c45dfd616c..0000000000000 --- a/ms-windows/osgeo4w/package.cmd +++ /dev/null @@ -1,482 +0,0 @@ -@echo off -REM *************************************************************************** -REM package.cmd -REM --------------------- -REM begin : July 2009 -REM copyright : (C) 2009 by Juergen E. Fischer -REM email : jef at norbit dot de -REM *************************************************************************** -REM * * -REM * This program is free software; you can redistribute it and/or modify * -REM * it under the terms of the GNU General Public License as published by * -REM * the Free Software Foundation; either version 2 of the License, or * -REM * (at your option) any later version. * -REM * * -REM *************************************************************************** - -setlocal enabledelayedexpansion - -set VERSION=%1 -set PACKAGE=%2 -set PACKAGENAME=%3 -set ARCH=%4 -set SHA=%5 -set SITE=%6 -if "%VERSION%"=="" goto usage -if "%PACKAGE%"=="" goto usage -if "%PACKAGENAME%"=="" goto usage -if "%ARCH%"=="" goto usage -if not "%SHA%"=="" set SHA=-%SHA% -if "%SITE%"=="" set SITE=qgis.org -if "%BUILDNAME%"=="" set BUILDNAME=%PACKAGENAME%-%VERSION%%SHA%-Release-VC14-%ARCH% - -set BUILDDIR=%CD%\build-%PACKAGENAME%-%ARCH% -if not exist "%BUILDDIR%" mkdir %BUILDDIR% -if not exist "%BUILDDIR%" (echo could not create build directory %BUILDDIR% & goto error) - -call msvc-env.bat %ARCH% - -set O4W_ROOT=%OSGEO4W_ROOT:\=/% -set LIB_DIR=%O4W_ROOT% - -if "%ARCH%"=="x86" ( - set CMAKE_OPT=^ - -D SPATIALINDEX_LIBRARY=%O4W_ROOT%/lib/spatialindex-32.lib -) else ( - set CMAKE_OPT=^ - -D SPATIALINDEX_LIBRARY=%O4W_ROOT%/lib/spatialindex-64.lib ^ - -D CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_NO_WARNINGS=TRUE -) - -for %%i in ("%GRASS_PREFIX%") do set GRASS7_VERSION=%%~nxi -set GRASS_VERSIONS=%GRASS7_VERSION% - -set TAR=tar.exe -if exist "c:\cygwin\bin\tar.exe" set TAR=c:\cygwin\bin\tar.exe -if exist "c:\cygwin64\bin\tar.exe" set TAR=c:\cygwin64\bin\tar.exe - -set BUILDCONF=Release - -cd ..\.. -set SRCDIR=%CD% - -if "%BUILDDIR:~1,1%"==":" %BUILDDIR:~0,2% -cd %BUILDDIR% - -set PKGDIR=%OSGEO4W_ROOT%\apps\%PACKAGENAME% - -if exist repackage goto package - -if not exist build.log goto build - -REM -REM try renaming the logfile to see if it's locked -REM - -if exist build.tmp del build.tmp -if exist build.tmp (echo could not remove build.tmp & goto error) - -ren build.log build.tmp -if exist build.log goto locked -if not exist build.tmp goto locked - -ren build.tmp build.log -if exist build.tmp goto locked -if not exist build.log goto locked - -goto build - -:locked -echo Logfile locked -if exist build.tmp del build.tmp -goto error - -:build -echo BEGIN: %DATE% %TIME% - -set >buildenv.log - -if exist qgsversion.h del qgsversion.h - -if exist CMakeCache.txt if exist skipcmake goto skipcmake - -touch %SRCDIR%\CMakeLists.txt - -echo CMAKE: %DATE% %TIME% -if errorlevel 1 goto error - -if "%CMAKEGEN%"=="" set CMAKEGEN=Ninja -if "%CC%"=="" set CC="%CMAKE_COMPILER_PATH:\=/%/cl.exe" -if "%CXX%"=="" set CXX="%CMAKE_COMPILER_PATH:\=/%/cl.exe" -if "%OSGEO4W_CXXFLAGS%"=="" set OSGEO4W_CXXFLAGS=/MD /Z7 /MP /O2 /Ob2 /D NDEBUG - -for %%i in (%PYTHONHOME%) do set PYVER=%%~ni - -cmake -G "%CMAKEGEN%" ^ - -D CMAKE_CXX_COMPILER="%CXX:\=/%" ^ - -D CMAKE_C_COMPILER="%CC:\=/%" ^ - -D CMAKE_LINKER="%CMAKE_COMPILER_PATH:\=/%/link.exe" ^ - -D CMAKE_CXX_FLAGS_RELEASE="%OSGEO4W_CXXFLAGS%" ^ - -D CMAKE_PDB_OUTPUT_DIRECTORY_RELEASE=%BUILDDIR%\apps\%PACKAGENAME%\pdb ^ - -D CMAKE_SHARED_LINKER_FLAGS_RELEASE="/INCREMENTAL:NO /DEBUG /OPT:REF /OPT:ICF" ^ - -D CMAKE_MODULE_LINKER_FLAGS_RELEASE="/INCREMENTAL:NO /DEBUG /OPT:REF /OPT:ICF" ^ - -D SUBMIT_URL="https://cdash.orfeo-toolbox.org/submit.php?project=QGIS" ^ - -D BUILDNAME="%BUILDNAME%" ^ - -D SITE="%SITE%" ^ - -D PEDANTIC=TRUE ^ - -D WITH_QSPATIALITE=TRUE ^ - -D WITH_SERVER=TRUE ^ - -D WITH_HANA=TRUE ^ - -D SERVER_SKIP_ECW=TRUE ^ - -D WITH_GRASS=TRUE ^ - -D WITH_3D=TRUE ^ - -D WITH_GRASS7=TRUE ^ - -D GRASS_PREFIX7=%GRASS_PREFIX:\=/% ^ - -D WITH_ORACLE=TRUE ^ - -D WITH_CUSTOM_WIDGETS=TRUE ^ - -D CMAKE_BUILD_TYPE=%BUILDCONF% ^ - -D CMAKE_CONFIGURATION_TYPES=%BUILDCONF% ^ - -D SETUPAPI_LIBRARY="%SETUPAPI_LIBRARY%" ^ - -D GEOS_LIBRARY=%O4W_ROOT%/lib/geos_c.lib ^ - -D SQLITE3_LIBRARY=%O4W_ROOT%/lib/sqlite3_i.lib ^ - -D SPATIALITE_LIBRARY=%O4W_ROOT%/lib/spatialite_i.lib ^ - -D PYTHON_EXECUTABLE=%O4W_ROOT%/bin/python3.exe ^ - -D SIP_BINARY_PATH=%PYTHONHOME:\=/%/sip.exe ^ - -D PYTHON_INCLUDE_DIR=%PYTHONHOME:\=/%/include ^ - -D PYTHON_LIBRARY=%PYTHONHOME:\=/%/libs/%PYVER%.lib ^ - -D QT_LIBRARY_DIR=%O4W_ROOT%/lib ^ - -D QT_HEADERS_DIR=%O4W_ROOT%/apps/qt5/include ^ - -D CMAKE_INSTALL_PREFIX=%O4W_ROOT%/apps/%PACKAGENAME% ^ - -D FCGI_INCLUDE_DIR=%O4W_ROOT%/include ^ - -D FCGI_LIBRARY=%O4W_ROOT%/lib/libfcgi.lib ^ - -D QCA_INCLUDE_DIR=%OSGEO4W_ROOT%\apps\Qt5\include\QtCrypto ^ - -D QCA_LIBRARY=%OSGEO4W_ROOT%\apps\Qt5\lib\qca-qt5.lib ^ - -D QSCINTILLA_LIBRARY=%OSGEO4W_ROOT%\apps\Qt5\lib\qscintilla2.lib ^ - -D DART_TESTING_TIMEOUT=60 ^ - %CMAKE_OPT% ^ - %SRCDIR:\=/% -if errorlevel 1 (echo cmake failed & goto error) - -if "%CONFIGONLY%"=="1" (echo Exiting after configuring build directory: %CD% & goto end) - -:skipcmake -if exist ..\noclean (echo skip clean & goto skipclean) -echo CLEAN: %DATE% %TIME% -cmake --build %BUILDDIR% --target clean --config %BUILDCONF% -if errorlevel 1 (echo clean failed & goto error) - -:skipclean -if exist ..\skipbuild (echo skip build & goto skipbuild) -echo ALL_BUILD: %DATE% %TIME% -cmake --build %BUILDDIR% --config %BUILDCONF% -if errorlevel 1 (echo build failed & goto error) - -:skipbuild -if exist ..\skiptests goto skiptests - -echo RUN_TESTS: %DATE% %TIME% - -reg add "HKCU\Software\Microsoft\Windows\Windows Error Reporting" /v DontShow /t REG_DWORD /d 1 /f - -set oldtemp=%TEMP% -set oldtmp=%TMP% -set oldpath=%PATH% - -set TEMP=%TEMP%\%PACKAGENAME%-%ARCH% -set TMP=%TEMP% -if exist "%TEMP%" rmdir /s /q "%TEMP%" -mkdir "%TEMP%" - -for %%g IN (%GRASS_VERSIONS%) do ( - set path=!path!;%OSGEO4W_ROOT%\apps\grass\%%g\lib - set GISBASE=%OSGEO4W_ROOT%\apps\grass\%%g -) -PATH %path%;%BUILDDIR%\output\plugins -set QT_PLUGIN_PATH=%BUILDDIR%\output\plugins;%OSGEO4W_ROOT%\apps\qt5\plugins - -cmake --build %BUILDDIR% --target Experimental --config %BUILDCONF% -if errorlevel 1 echo TESTS WERE NOT SUCCESSFUL. - -set TEMP=%oldtemp% -set TMP=%oldtmp% -PATH %oldpath% - -:skiptests -if exist ..\skippackage goto end - -if exist "%PKGDIR%" ( - echo REMOVE: %DATE% %TIME% - rmdir /s /q "%PKGDIR%" -) - -echo INSTALL: %DATE% %TIME% -cmake --build %BUILDDIR% --target install --config %BUILDCONF% -if errorlevel 1 (echo INSTALL failed & goto error) - -:package -echo PACKAGE: %DATE% %TIME% - -cd .. -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' postinstall-common.bat >%OSGEO4W_ROOT%\etc\postinstall\%PACKAGENAME%-common.bat -if errorlevel 1 (echo creation of common postinstall failed & goto error) - -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' postinstall-desktop.bat >%OSGEO4W_ROOT%\etc\postinstall\%PACKAGENAME%.bat -if errorlevel 1 (echo creation of desktop postinstall failed & goto error) - -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' preremove-desktop.bat >%OSGEO4W_ROOT%\etc\preremove\%PACKAGENAME%.bat -if errorlevel 1 (echo creation of desktop preremove failed & goto error) - -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' qgis.bat.tmpl >%OSGEO4W_ROOT%\bin\%PACKAGENAME%.bat.tmpl -if errorlevel 1 (echo creation of desktop template failed & goto error) - -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' designer.bat.tmpl >%OSGEO4W_ROOT%\bin\%PACKAGENAME%-designer.bat.tmpl -if errorlevel 1 (echo creation of designer template failed & goto error) -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' qgis.reg.tmpl >%PKGDIR%\bin\qgis.reg.tmpl -if errorlevel 1 (echo creation of registry template & goto error) - -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' postinstall-server.bat >%OSGEO4W_ROOT%\etc\postinstall\%PACKAGENAME%-server.bat -if errorlevel 1 (echo creation of server postinstall failed & goto error) - -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' preremove-server.bat >%OSGEO4W_ROOT%\etc\preremove\%PACKAGENAME%-server.bat -if errorlevel 1 (echo creation of server preremove failed & goto error) - -if not exist %OSGEO4W_ROOT%\httpd.d mkdir %OSGEO4W_ROOT%\httpd.d -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' httpd.conf.tmpl >%OSGEO4W_ROOT%\httpd.d\httpd_%PACKAGENAME%.conf.tmpl -if errorlevel 1 (echo creation of httpd.conf template failed & goto error) - -set packages="" "-common" "-server" "-devel" "-oracle-provider" "-grass-plugin-common" - -for %%g IN (%GRASS_VERSIONS%) do ( - for /f "usebackq tokens=1" %%a in (`%%g --config version`) do set gv=%%a - for /F "delims=." %%i in ("!gv!") do set v=%%i - set w=!v! - if !v!==6 set w= - - sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' -e 's/@grasspath@/%%g/g' -e 's/@grassversion@/!gv!/g' -e 's/@grassmajor@/!v!/g' postinstall-grass.bat >%OSGEO4W_ROOT%\etc\postinstall\%PACKAGENAME%-grass-plugin!w!.bat - if errorlevel 1 (echo creation of grass desktop postinstall failed & goto error) - sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' -e 's/@grasspath@/%%g/g' -e 's/@grassversion@/!gv!/g' -e 's/@grassmajor@/!v!/g' preremove-grass.bat >%OSGEO4W_ROOT%\etc\preremove\%PACKAGENAME%-grass-plugin!w!.bat - if errorlevel 1 (echo creation of grass desktop preremove failed & goto error) - sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' -e 's/@grasspath@/%%g/g' -e 's/@grassversion@/!gv!/g' -e 's/@grassmajor@/!v!/g' qgis-grass.bat.tmpl >%OSGEO4W_ROOT%\bin\%PACKAGENAME%-grass!v!.bat.tmpl - if errorlevel 1 (echo creation of grass desktop template failed & goto error) - - set packages=!packages! "-grass-plugin!w!" -) - -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' python.bat.tmpl >%OSGEO4W_ROOT%\bin\python-%PACKAGENAME%.bat.tmpl -if errorlevel 1 (echo creation of python wrapper template failed & goto error) - -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' process.bat.tmpl >%OSGEO4W_ROOT%\bin\qgis_process-%PACKAGENAME%.bat.tmpl -if errorlevel 1 (echo creation of qgis process wrapper template failed & goto error) - -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' preremove-grass-plugin-common.bat >%OSGEO4W_ROOT%\etc\preremove\%PACKAGENAME%-grass-plugin-common.bat -if errorlevel 1 (echo creation of grass common preremove failed & goto error) -sed -e 's/@package@/%PACKAGENAME%/g' -e 's/@version@/%VERSION%/g' postinstall-grass-plugin-common.bat >%OSGEO4W_ROOT%\etc\postinstall\%PACKAGENAME%-grass-plugin-common.bat -if errorlevel 1 (echo creation of grass common postinstall failed & goto error) - -touch exclude -if exist ..\skipbuild (echo skip build & goto skipbuild) - -for %%i in (%packages%) do ( - if not exist %ARCH%\release\qgis\%PACKAGENAME%%%i mkdir %ARCH%\release\qgis\%PACKAGENAME%%%i -) - -%TAR% -C %OSGEO4W_ROOT% -cjf %ARCH%/release/qgis/%PACKAGENAME%-common/%PACKAGENAME%-common-%VERSION%-%PACKAGE%.tar.bz2 ^ - --exclude-from exclude ^ - --exclude "*.pyc" ^ - "apps/%PACKAGENAME%/bin/qgispython.dll" ^ - "apps/%PACKAGENAME%/bin/qgis_analysis.dll" ^ - "apps/%PACKAGENAME%/bin/qgis_3d.dll" ^ - "apps/%PACKAGENAME%/bin/qgis_core.dll" ^ - "apps/%PACKAGENAME%/bin/qgis_gui.dll" ^ - "apps/%PACKAGENAME%/bin/qgis_native.dll" ^ - "apps/%PACKAGENAME%/bin/qgis_process.exe" ^ - "apps/%PACKAGENAME%/doc/" ^ - "apps/%PACKAGENAME%/plugins/authmethod_basic.dll" ^ - "apps/%PACKAGENAME%/plugins/provider_delimitedtext.dll" ^ - "apps/%PACKAGENAME%/plugins/authmethod_esritoken.dll" ^ - "apps/%PACKAGENAME%/plugins/provider_gpx.dll" ^ - "apps/%PACKAGENAME%/plugins/authmethod_identcert.dll" ^ - "apps/%PACKAGENAME%/plugins/provider_mssql.dll" ^ - "apps/%PACKAGENAME%/plugins/authmethod_pkcs12.dll" ^ - "apps/%PACKAGENAME%/plugins/authmethod_pkipaths.dll" ^ - "apps/%PACKAGENAME%/plugins/provider_postgres.dll" ^ - "apps/%PACKAGENAME%/plugins/provider_postgresraster.dll" ^ - "apps/%PACKAGENAME%/plugins/provider_spatialite.dll" ^ - "apps/%PACKAGENAME%/plugins/provider_virtuallayer.dll" ^ - "apps/%PACKAGENAME%/plugins/provider_wcs.dll" ^ - "apps/%PACKAGENAME%/plugins/provider_wfs.dll" ^ - "apps/%PACKAGENAME%/plugins/provider_wms.dll" ^ - "apps/%PACKAGENAME%/plugins/provider_arcgismapserver.dll" ^ - "apps/%PACKAGENAME%/plugins/provider_arcgisfeatureserver.dll" ^ - "apps/%PACKAGENAME%/plugins/provider_mdal.dll" ^ - "apps/%PACKAGENAME%/plugins/provider_hana.dll" ^ - "apps/%PACKAGENAME%/plugins/authmethod_oauth2.dll" ^ - "apps/%PACKAGENAME%/plugins/authmethod_maptilerhmacsha256.dll" ^ - "apps/%PACKAGENAME%/resources/qgis.db" ^ - "apps/%PACKAGENAME%/resources/spatialite.db" ^ - "apps/%PACKAGENAME%/resources/srs.db" ^ - "apps/%PACKAGENAME%/resources/symbology-style.xml" ^ - "apps/%PACKAGENAME%/resources/cpt-city-qgis-min/" ^ - "apps/%PACKAGENAME%/svg/" ^ - "apps/%PACKAGENAME%/crssync.exe" ^ - "etc/postinstall/%PACKAGENAME%-common.bat" -if errorlevel 1 (echo tar common failed & goto error) - -%TAR% -C %OSGEO4W_ROOT% -cjf %ARCH%/release/qgis/%PACKAGENAME%-server/%PACKAGENAME%-server-%VERSION%-%PACKAGE%.tar.bz2 ^ - --exclude-from exclude ^ - --exclude "*.pyc" ^ - "apps/%PACKAGENAME%/bin/qgis_mapserv.fcgi.exe" ^ - "apps/%PACKAGENAME%/bin/qgis_server.dll" ^ - "apps/%PACKAGENAME%/bin/admin.sld" ^ - "apps/%PACKAGENAME%/bin/wms_metadata.xml" ^ - "apps/%PACKAGENAME%/resources/server/" ^ - "apps/%PACKAGENAME%/server/" ^ - "apps/%PACKAGENAME%/python/qgis/_server.pyd" ^ - "apps/%PACKAGENAME%/python/qgis/_server.pyi" ^ - "apps/%PACKAGENAME%/python/qgis/server/" ^ - "httpd.d/httpd_%PACKAGENAME%.conf.tmpl" ^ - "etc/postinstall/%PACKAGENAME%-server.bat" ^ - "etc/preremove/%PACKAGENAME%-server.bat" -if errorlevel 1 (echo tar server failed & goto error) - -move %PKGDIR%\bin\qgis.exe %OSGEO4W_ROOT%\bin\%PACKAGENAME%-bin.exe -if errorlevel 1 (echo move of desktop executable failed & goto error) -copy qgis.vars %OSGEO4W_ROOT%\bin\%PACKAGENAME%-bin.vars -if errorlevel 1 (echo copy of desktop executable vars failed & goto error) - -if not exist %PKGDIR%\qtplugins\sqldrivers mkdir %PKGDIR%\qtplugins\sqldrivers -move %OSGEO4W_ROOT%\apps\qt5\plugins\sqldrivers\qsqlocispatial.dll %PKGDIR%\qtplugins\sqldrivers -if errorlevel 1 (echo move of oci sqldriver failed & goto error) -move %OSGEO4W_ROOT%\apps\qt5\plugins\sqldrivers\qsqlspatialite.dll %PKGDIR%\qtplugins\sqldrivers -if errorlevel 1 (echo move of spatialite sqldriver failed & goto error) - -if not exist %PKGDIR%\qtplugins\designer mkdir %PKGDIR%\qtplugins\designer -move %OSGEO4W_ROOT%\apps\qt5\plugins\designer\qgis_customwidgets.dll %PKGDIR%\qtplugins\designer -if errorlevel 1 (echo move of customwidgets failed & goto error) - -if not exist %PKGDIR%\python\PyQt5\uic\widget-plugins mkdir %PKGDIR%\python\PyQt5\uic\widget-plugins -move %PYTHONHOME%\Lib\site-packages\PyQt5\uic\widget-plugins\qgis_customwidgets.py %PKGDIR%\python\PyQt5\uic\widget-plugins -if errorlevel 1 (echo move of customwidgets binding failed & goto error) - -for %%i in (dbghelp.dll symsrv.dll) do ( - copy "%DBGHLP_PATH%\%%i" %OSGEO4W_ROOT%\apps\%PACKAGENAME% - if errorlevel 1 (echo %%i not found & goto error) -) - -if not exist %ARCH%\release\qgis\%PACKAGENAME% mkdir %ARCH%\release\qgis\%PACKAGENAME% -%TAR% -C %OSGEO4W_ROOT% -cjf %ARCH%/release/qgis/%PACKAGENAME%/%PACKAGENAME%-%VERSION%-%PACKAGE%.tar.bz2 ^ - --exclude-from exclude ^ - --exclude "*.pyc" ^ - --exclude "apps/%PACKAGENAME%/python/qgis/_server.pyd" ^ - --exclude "apps/%PACKAGENAME%/python/qgis/_server.pyi" ^ - --exclude "apps/%PACKAGENAME%/python/qgis/_server.lib" ^ - --exclude "apps/%PACKAGENAME%/python/qgis/server" ^ - --exclude "apps/%PACKAGENAME%/server/" ^ - "bin/%PACKAGENAME%-bin.exe" ^ - "bin/%PACKAGENAME%-bin.vars" ^ - "bin/python-%PACKAGENAME%.bat.tmpl" ^ - "bin/qgis_process-%PACKAGENAME%.bat.tmpl" ^ - "apps/%PACKAGENAME%/bin/qgis_app.dll" ^ - "apps/%PACKAGENAME%/bin/qgis.reg.tmpl" ^ - "apps/%PACKAGENAME%/i18n/" ^ - "apps/%PACKAGENAME%/icons/" ^ - "apps/%PACKAGENAME%/images/" ^ - "apps/%PACKAGENAME%/plugins/plugin_offlineediting.dll" ^ - "apps/%PACKAGENAME%/plugins/plugin_topology.dll" ^ - "apps/%PACKAGENAME%/plugins/plugin_geometrychecker.dll" ^ - "apps/%PACKAGENAME%/qtplugins/sqldrivers/qsqlspatialite.dll" ^ - "apps/%PACKAGENAME%/qtplugins/designer/" ^ - "apps/%PACKAGENAME%/python/" ^ - "apps/%PACKAGENAME%/resources/customization.xml" ^ - "apps/%PACKAGENAME%/resources/themes/" ^ - "apps/%PACKAGENAME%/resources/data/" ^ - "apps/%PACKAGENAME%/resources/metadata-ISO/" ^ - "apps/%PACKAGENAME%/resources/opencl_programs/" ^ - "apps/%PACKAGENAME%/resources/palettes/" ^ - "apps/%PACKAGENAME%/resources/2to3migration.txt" ^ - "apps/%PACKAGENAME%/resources/qgis_global_settings.ini" ^ - "apps/%PACKAGENAME%/qgiscrashhandler.exe" ^ - "apps/%PACKAGENAME%/dbghelp.dll" ^ - "apps/%PACKAGENAME%/symsrv.dll" ^ - "bin/%PACKAGENAME%.bat.tmpl" ^ - "bin/%PACKAGENAME%-designer.bat.tmpl" ^ - "etc/postinstall/%PACKAGENAME%.bat" ^ - "etc/preremove/%PACKAGENAME%.bat" -if errorlevel 1 (echo tar desktop failed & goto error) - -if not exist %ARCH%\release\qgis\%PACKAGENAME%-pdb mkdir %ARCH%\release\qgis\%PACKAGENAME%-pdb -%TAR% -C %BUILDDIR% -cjf %ARCH%/release/qgis/%PACKAGENAME%-pdb/%PACKAGENAME%-pdb-%VERSION%-%PACKAGE%.tar.bz2 ^ - apps/%PACKAGENAME%/pdb -if errorlevel 1 (echo tar failed & goto error) - -%TAR% -C %OSGEO4W_ROOT% -cjf %ARCH%/release/qgis/%PACKAGENAME%-grass-plugin-common/%PACKAGENAME%-grass-plugin-common-%VERSION%-%PACKAGE%.tar.bz2 ^ - --exclude-from exclude ^ - --exclude "*.pyc" ^ - --exclude "apps/%PACKAGENAME%/grass/modules/qgis.d.rast6.exe" ^ - --exclude "apps/%PACKAGENAME%/grass/modules/qgis.d.rast7.exe" ^ - --exclude "apps/%PACKAGENAME%/grass/modules/qgis.g.info6.exe" ^ - --exclude "apps/%PACKAGENAME%/grass/modules/qgis.g.info7.exe" ^ - --exclude "apps/%PACKAGENAME%/grass/modules/qgis.r.in6.exe" ^ - --exclude "apps/%PACKAGENAME%/grass/modules/qgis.r.in7.exe" ^ - --exclude "apps/%PACKAGENAME%/grass/modules/qgis.v.in6.exe" ^ - --exclude "apps/%PACKAGENAME%/grass/modules/qgis.v.in7.exe" ^ - --exclude "apps/%PACKAGENAME%/grass/bin/qgis.g.browser6.exe" ^ - --exclude "apps/%PACKAGENAME%/grass/bin/qgis.g.browser7.exe" ^ - "apps/%PACKAGENAME%/grass" ^ - "etc/postinstall/%PACKAGENAME%-grass-plugin-common.bat" ^ - "etc/preremove/%PACKAGENAME%-grass-plugin-common.bat" -if errorlevel 1 (echo tar grass-plugin failed & goto error) - -for %%g IN (%GRASS_VERSIONS%) do ( - for /f "usebackq tokens=1" %%a in (`%%g --config version`) do set gv=%%a - for /F "delims=." %%i in ("!gv!") do set v=%%i - set w=!v! - if !v!==6 set w= - - %TAR% -C %OSGEO4W_ROOT% -cjf %ARCH%/release/qgis/%PACKAGENAME%-grass-plugin!w!/%PACKAGENAME%-grass-plugin!w!-%VERSION%-%PACKAGE%.tar.bz2 ^ - "apps/%PACKAGENAME%/bin/qgisgrass!v!.dll" ^ - "apps/%PACKAGENAME%/grass/bin/qgis.g.browser!v!.exe" ^ - "apps/%PACKAGENAME%/grass/modules/qgis.d.rast!v!.exe" ^ - "apps/%PACKAGENAME%/grass/modules/qgis.g.info!v!.exe" ^ - "apps/%PACKAGENAME%/grass/modules/qgis.r.in!v!.exe" ^ - "apps/%PACKAGENAME%/grass/modules/qgis.v.in!v!.exe" ^ - "apps/%PACKAGENAME%/plugins/plugin_grass!v!.dll" ^ - "apps/%PACKAGENAME%/plugins/provider_grass!v!.dll" ^ - "apps/%PACKAGENAME%/plugins/provider_grassraster!v!.dll" ^ - "bin/%PACKAGENAME%-grass!v!.bat.tmpl" ^ - "etc/postinstall/%PACKAGENAME%-grass-plugin!w!.bat" ^ - "etc/preremove/%PACKAGENAME%-grass-plugin!w!.bat" - if errorlevel 1 (echo tar grass-plugin!w! failed & goto error) -) - -%TAR% -C %OSGEO4W_ROOT% -cjf %ARCH%/release/qgis/%PACKAGENAME%-oracle-provider/%PACKAGENAME%-oracle-provider-%VERSION%-%PACKAGE%.tar.bz2 ^ - "apps/%PACKAGENAME%/plugins/oracleprovider.dll" ^ - "apps/%PACKAGENAME%/qtplugins/sqldrivers/qsqlocispatial.dll" -if errorlevel 1 (echo tar oracle-provider failed & goto error) - -%TAR% -C %OSGEO4W_ROOT% -cjf %ARCH%/release/qgis/%PACKAGENAME%-devel/%PACKAGENAME%-devel-%VERSION%-%PACKAGE%.tar.bz2 ^ - --exclude-from exclude ^ - --exclude "*.pyc" ^ - "apps/%PACKAGENAME%/FindQGIS.cmake" ^ - "apps/%PACKAGENAME%/include/" ^ - "apps/%PACKAGENAME%/lib/" -if errorlevel 1 (echo tar devel failed & goto error) - -goto end - -:usage -echo usage: %0 version package packagename arch [sha [site]] -echo sample: %0 2.0.1 3 qgis x86 f802808 -exit /b 1 - -:error -echo BUILD ERROR %ERRORLEVEL%: %DATE% %TIME% -for %%i in (%packages%) do ( - if exist %ARCH%\release\qgis\%PACKAGENAME%%%i\%PACKAGENAME%%%i-%VERSION%-%PACKAGE%.tar.bz2 del %ARCH%\release\qgis\%PACKAGENAME%%%i\%PACKAGENAME%%%i-%VERSION%-%PACKAGE%.tar.bz2 -) -exit /b 1 - -:end -echo FINISHED: %DATE% %TIME% - -endlocal diff --git a/ms-windows/osgeo4w/postinstall-common.bat b/ms-windows/osgeo4w/postinstall-common.bat deleted file mode 100644 index 4cbee4a1bf3e7..0000000000000 --- a/ms-windows/osgeo4w/postinstall-common.bat +++ /dev/null @@ -1,5 +0,0 @@ -call "%OSGEO4W_ROOT%\bin\o4w_env.bat" -call qt5_env.bat -path %PATH%;%OSGEO4W_ROOT%\apps\@package@\bin -set QGIS_PREFIX_PATH=%OSGEO4W_ROOT:\=/%/apps/@package@ -"%OSGEO4W_ROOT%\apps\@package@\crssync" diff --git a/ms-windows/osgeo4w/postinstall-desktop.bat b/ms-windows/osgeo4w/postinstall-desktop.bat deleted file mode 100644 index 392aeccfde0a5..0000000000000 --- a/ms-windows/osgeo4w/postinstall-desktop.bat +++ /dev/null @@ -1,25 +0,0 @@ -textreplace -std -t bin\@package@.bat -textreplace -std -t bin\@package@-designer.bat -textreplace -std -t bin\python-@package@.bat -textreplace -std -t bin\qgis_process-@package@.bat - -REM get short path without blanks -for %%i in ("%OSGEO4W_ROOT%") do set O4W_ROOT=%%~fsi -if "%OSGEO4W_DESKTOP%"=="" set OSGEO4W_DESKTOP=~$folder.common_desktop$ - -call "%OSGEO4W_ROOT%\bin\@package@.bat" --postinstall - -if not %OSGEO4W_MENU_LINKS%==0 mkdir "%OSGEO4W_STARTMENU%" -if not %OSGEO4W_DESKTOP_LINKS%==0 mkdir "%OSGEO4W_DESKTOP%" - -if not %OSGEO4W_MENU_LINKS%==0 nircmd shortcut "%O4W_ROOT%\bin\@package@-bin.exe" "%OSGEO4W_STARTMENU%" "QGIS Desktop @version@" "" "" "" "" "~$folder.mydocuments$" -if not %OSGEO4W_DESKTOP_LINKS%==0 nircmd shortcut "%O4W_ROOT%\bin\@package@-bin.exe" "%OSGEO4W_DESKTOP%" "QGIS Desktop @version@" "" "" "" "" "~$folder.mydocuments$" - -if not %OSGEO4W_MENU_LINKS%==0 nircmd shortcut "%O4W_ROOT%\bin\nircmd.exe" "%OSGEO4W_STARTMENU%" "Qt Designer with QGIS @version@ custom widgets" "exec hide """%OSGEO4W_ROOT%\bin\@package@-designer.bat"" "%O4W_ROOT%\apps\@package@\icons\QGIS.ico" "" "" "~$folder.mydocuments$" -if not %OSGEO4W_DESKTOP_LINKS%==0 nircmd shortcut "%O4W_ROOT%\bin\nircmd.exe" "%OSGEO4W_DESKTOP%" "Qt Designer with QGIS @version@ custom widgets" "exec hide %O4W_ROOT%\bin\@package@-designer.bat" "%O4W_ROOT%\apps\@package@\icons\QGIS.ico" "" "" "~$folder.mydocuments$" - -set OSGEO4W_ROOT=%OSGEO4W_ROOT:\=\\% -textreplace -std -t "%O4W_ROOT%\apps\@package@\bin\qgis.reg" -nircmd elevate "%WINDIR%\regedit" /s "%O4W_ROOT%\apps\@package@\bin\qgis.reg" -del /s /q "%OSGEO4W_ROOT%\apps\@package@\python\*.pyc" -exit /b 0 diff --git a/ms-windows/osgeo4w/postinstall-dev.bat b/ms-windows/osgeo4w/postinstall-dev.bat deleted file mode 100644 index 870873951314e..0000000000000 --- a/ms-windows/osgeo4w/postinstall-dev.bat +++ /dev/null @@ -1,55 +0,0 @@ -setlocal enabledelayedexpansion - -textreplace -std -t bin\@package@.bat -textreplace -std -t bin\@package@-designer.bat -textreplace -std -t bin\python-@package@.bat -textreplace -std -t bin\qgis_process-@package@.bat - -if "%OSGEO4W_DESKTOP%"=="" set OSGEO4W_DESKTOP=~$folder.common_desktop$ - -if not %OSGEO4W_MENU_LINKS%==0 mkdir "%OSGEO4W_STARTMENU%" -if not %OSGEO4W_DESKTOP_LINKS%==0 mkdir "%OSGEO4W_DESKTOP%" - -call "%OSGEO4W_ROOT%\bin\@package@.bat" --postinstall - -if not %OSGEO4W_MENU_LINKS%==0 nircmd shortcut "%OSGEO4W_ROOT%\bin\@package@-bin.exe" "%OSGEO4W_STARTMENU%" "QGIS Desktop @version@" "" "" "" "" "~$folder.mydocuments$" -if not %OSGEO4W_DESKTOP_LINKS%==0 nircmd shortcut "%OSGEO4W_ROOT%\bin\@package@-bin.exe" "%OSGEO4W_DESKTOP%" "QGIS Desktop @version@" "" "" "" "" "~$folder.mydocuments$" - -for %%g in (@grassversions@) do ( - set gv= - for /f "usebackq tokens=1" %%a in (`%%g --config version`) do set gv=%%a - if not "!gv!"=="" ( - for /F "delims=." %%i in ("!gv!") do set v=%%i - - copy "%OSGEO4W_ROOT%\bin\@package@-bin.exe" "%OSGEO4W_ROOT%\bin\@package@-bin-g!v!.exe" - copy "%OSGEO4W_ROOT%\bin\@package@-bin.vars" "%OSGEO4W_ROOT%\bin\@package@-bin-g!v!.vars" - textreplace -std -map @grassmajor@ !v! -t bin\@package@-g!v!.bat - call "%OSGEO4W_ROOT%\bin\@package@-g!v!.bat" --postinstall - - if not %OSGEO4W_MENU_LINKS%==0 nircmd shortcut "%OSGEO4W_ROOT%\bin\@package@-bin-g!v!.exe" "%OSGEO4W_STARTMENU%" "QGIS Desktop @version@ with GRASS !gv! (Nightly)" "" "" "" "" "~$folder.mydocuments$" - if not %OSGEO4W_DESKTOP_LINKS%==0 nircmd shortcut "%OSGEO4W_ROOT%\bin\@package@-bin-g!v!.exe" "%OSGEO4W_DESKTOP%" "QGIS Desktop @version@ with GRASS !gv! (Nightly)" "" "" "" "" "~$folder.mydocuments$" - ) -) - -if not %OSGEO4W_MENU_LINKS%==0 nircmd shortcut "%OSGEO4W_ROOT%\bin\nircmd.exe" "%OSGEO4W_STARTMENU%" "Qt Designer with QGIS @version@ custom widgets (Nightly)" "exec hide """%OSGEO4W_ROOT%\bin\@package@-designer.bat"" "%OSGEO4W_ROOT%\apps\@package@\icons\QGIS.ico" "" "" "~$folder.mydocuments$" -if not %OSGEO4W_DESKTOP_LINKS%==0 nircmd shortcut "%OSGEO4W_ROOT%\bin\nircmd.exe" "%OSGEO4W_DESKTOP%" "Qt Designer with QGIS @version@ custom widgets (Nightly)" "exec hide """%OSGEO4W_ROOT%\bin\@package@-designer.bat"" "%OSGEO4W_ROOT%\apps\@package@\icons\QGIS.ico" "" "" "~$folder.mydocuments$" - -set O4W_ROOT=%OSGEO4W_ROOT% -set OSGEO4W_ROOT=%OSGEO4W_ROOT:\=\\% -textreplace -std -t "%O4W_ROOT%\apps\@package@\bin\qgis.reg" -set OSGEO4W_ROOT=%O4W_ROOT% - -REM Do not register extensions if release is installed -if not exist "%O4W_ROOT%\apps\qgis\bin\qgis.reg" nircmd elevate "%WINDIR%\regedit" /s "%O4W_ROOT%\apps\@package@\bin\qgis.reg" - -call "%OSGEO4W_ROOT%\bin\o4w_env.bat" -call qt5_env.bat -call gdal-dev-env.bat -path %PATH%;%OSGEO4W_ROOT%\apps\@package@\bin -set QGIS_PREFIX_PATH=%OSGEO4W_ROOT:\=/%/apps/@package@ -"%OSGEO4W_ROOT%\apps\@package@\crssync" - -del /s /q "%OSGEO4W_ROOT%\apps\@package@\*.pyc" -exit /b 0 - -endlocal diff --git a/ms-windows/osgeo4w/postinstall-grass-plugin-common.bat b/ms-windows/osgeo4w/postinstall-grass-plugin-common.bat deleted file mode 100644 index 892c9d81cc324..0000000000000 --- a/ms-windows/osgeo4w/postinstall-grass-plugin-common.bat +++ /dev/null @@ -1,2 +0,0 @@ -del /s /q "%OSGEO4W_ROOT%\apps\@package@\grass\*.pyc" -exit /b 0 diff --git a/ms-windows/osgeo4w/postinstall-grass.bat b/ms-windows/osgeo4w/postinstall-grass.bat deleted file mode 100644 index b851c2cde834e..0000000000000 --- a/ms-windows/osgeo4w/postinstall-grass.bat +++ /dev/null @@ -1,13 +0,0 @@ -textreplace -std -t bin\@package@-grass@grassmajor@.bat - -if "%OSGEO4W_DESKTOP%"=="" set OSGEO4W_DESKTOP=~$folder.common_desktop$ - -copy bin\@package@-bin.exe bin\@package@-bin-g@grassmajor@.exe -copy bin\@package@-bin.vars bin\@package@-bin-g@grassmajor@.vars -call "%OSGEO4W_ROOT%\bin\@package@-grass@grassmajor@.bat" --postinstall - -if not %OSGEO4W_MENU_LINKS%==0 mkdir "%OSGEO4W_STARTMENU%" -if not %OSGEO4W_DESKTOP_LINKS%==0 mkdir "%OSGEO4W_DESKTOP%" - -if not %OSGEO4W_MENU_LINKS%==0 nircmd shortcut "%OSGEO4W_ROOT%\bin\@package@-bin-g@grassmajor@.exe" "%OSGEO4W_STARTMENU%" "QGIS Desktop @version@ with GRASS @grassversion@" "" "" "" "" "~$folder.mydocuments$" -if not %OSGEO4W_DESKTOP_LINKS%==0 nircmd shortcut "%OSGEO4W_ROOT%\bin\@package@-bin-g@grassmajor@.exe" "%OSGEO4W_DESKTOP%" "QGIS Desktop @version@ with GRASS @grassversion@" "" "" "" "" "~$folder.mydocuments$" diff --git a/ms-windows/osgeo4w/postinstall-server.bat b/ms-windows/osgeo4w/postinstall-server.bat deleted file mode 100644 index db70852841684..0000000000000 --- a/ms-windows/osgeo4w/postinstall-server.bat +++ /dev/null @@ -1,5 +0,0 @@ -textreplace -std ^ - -map @windir@ "%WINDIR%" ^ - -map @temp@ "%TEMP%" ^ - -t httpd.d\httpd_@package@.conf -del httpd.d\httpd_@package@.conf.tmpl diff --git a/ms-windows/osgeo4w/preremove-desktop.bat b/ms-windows/osgeo4w/preremove-desktop.bat deleted file mode 100644 index 242a205480ca8..0000000000000 --- a/ms-windows/osgeo4w/preremove-desktop.bat +++ /dev/null @@ -1,15 +0,0 @@ -del "%OSGEO4W_STARTMENU%\QGIS Desktop @version@.lnk" -del "%OSGEO4W_STARTMENU%\QGIS Browser @version@.lnk" -del "%OSGEO4W_STARTMENU%\Qt Designer with QGIS @version@ custom widgets.lnk" -rmdir "%OSGEO4W_STARTMENU%" -del "%OSGEO4W_DESKTOP%\QGIS Desktop @version@.lnk" -del "%OSGEO4W_DESKTOP%\QGIS Browser @version@.lnk" -del "%OSGEO4W_DESKTOP%\Qt Designer with QGIS @version@ custom widgets.lnk" -rmdir "%OSGEO4W_DESKTOP%" -del "%OSGEO4W_ROOT%\bin\@package@.bat" -del "%OSGEO4W_ROOT%\bin\@package@-bin.vars" -del "%OSGEO4W_ROOT%\bin\@package@-bin.env" -del "%OSGEO4W_ROOT%\bin\@package@-designer.bat" -del "%OSGEO4W_ROOT%\apps\@package@\python\qgis\qgisconfig.py" -del "%OSGEO4W_ROOT%\apps\@package@\bin\qgis.reg" -del /s /q "%OSGEO4W_ROOT%\apps\@package@\python\*.pyc" diff --git a/ms-windows/osgeo4w/preremove-dev.bat b/ms-windows/osgeo4w/preremove-dev.bat deleted file mode 100644 index ae7532026c20c..0000000000000 --- a/ms-windows/osgeo4w/preremove-dev.bat +++ /dev/null @@ -1,30 +0,0 @@ -setlocal enabledelayedexpansion - -for %%g in (@grassversions@) do ( - for /f "usebackq tokens=1" %%a in (`%%g --config version`) do set gv=%%a - for /F "delims=." %%i in ("!gv!") do set v=%%i - - del "%OSGEO4W_STARTMENU%\QGIS Desktop @version@ with GRASS !gv! (Nightly).lnk" - del "%OSGEO4W_STARTMENU%\QGIS Browser @version@ with GRASS !gv! (Nightly).lnk" - del "%OSGEO4W_DESKTOP%\QGIS Desktop @version@ with GRASS !gv! (Nightly).lnk" - del "%OSGEO4W_DESKTOP%\QGIS Browser @version@ with GRASS !gv! (Nightly).lnk" - del "%OSGEO4W_ROOT%\bin\@package@-g!v!.bat" - del "%OSGEO4W_ROOT%\bin\@package@-bin-g!v!.exe" - del "%OSGEO4W_ROOT%\bin\@package@-bin-g!v!.env" - del "%OSGEO4W_ROOT%\bin\@package@-bin-g!v!.vars" -) - -del "%OSGEO4W_STARTMENU%\Qt Designer with QGIS @version@ custom widgets (Nightly).lnk" -rmdir "%OSGEO4W_STARTMENU%" -del "%OSGEO4W_DESKTOP%\Qt Designer with QGIS @version@ custom widgets (Nightly).lnk" -rmdir "%OSGEO4W_DESKTOP%" - -del "%OSGEO4W_ROOT%\bin\@package@-bin.env" -del "%OSGEO4W_ROOT%\bin\@package@-designer.bat" -del "%OSGEO4W_ROOT%\bin\python-@package@.bat" -del "%OSGEO4W_ROOT%\bin\qgis_process-@package@.bat" -del "%OSGEO4W_ROOT%\apps\@package@\python\qgis\qgisconfig.py" -del "%OSGEO4W_ROOT%\apps\@package@\bin\qgis.reg" -del /s /q "%OSGEO4W_ROOT%\apps\@package@\*.pyc" - -endlocal diff --git a/ms-windows/osgeo4w/preremove-grass-plugin-common.bat b/ms-windows/osgeo4w/preremove-grass-plugin-common.bat deleted file mode 100644 index 59ae472ac9642..0000000000000 --- a/ms-windows/osgeo4w/preremove-grass-plugin-common.bat +++ /dev/null @@ -1 +0,0 @@ -del /s /q "%OSGEO4W_ROOT%\apps\@package@\grass\*.pyc" diff --git a/ms-windows/osgeo4w/preremove-grass.bat b/ms-windows/osgeo4w/preremove-grass.bat deleted file mode 100644 index 1b2c2abc33c40..0000000000000 --- a/ms-windows/osgeo4w/preremove-grass.bat +++ /dev/null @@ -1,10 +0,0 @@ -del "%OSGEO4W_STARTMENU%\QGIS Desktop @version@ with GRASS @grassversion@.lnk" -del "%OSGEO4W_STARTMENU%\QGIS Browser @version@ with GRASS @grassversion@.lnk" -rmdir "%OSGEO4W_STARTMENU%" -del "%OSGEO4W_DESKTOP%\QGIS Desktop @version@ with GRASS @grassversion@.lnk" -del "%OSGEO4W_DESKTOP%\QGIS Browser @version@ with GRASS @grassversion@.lnk" -rmdir "%OSGEO4W_DESKTOP%" -del "%OSGEO4W_ROOT%\bin\@package@-grass@grassmajor@.bat" -del "%OSGEO4W_ROOT%\bin\@package@-bin-g@grassmajor@.exe" -del "%OSGEO4W_ROOT%\bin\@package@-bin-g@grassmajor@.vars" -del "%OSGEO4W_ROOT%\bin\@package@-bin-g@grassmajor@.env" diff --git a/ms-windows/osgeo4w/preremove-server.bat b/ms-windows/osgeo4w/preremove-server.bat deleted file mode 100644 index 91f3f99f79fa8..0000000000000 --- a/ms-windows/osgeo4w/preremove-server.bat +++ /dev/null @@ -1 +0,0 @@ -del "%OSGEO4W_ROOT%\httpd.d\httpd_@package@.conf" diff --git a/ms-windows/osgeo4w/process.bat.tmpl b/ms-windows/osgeo4w/process.bat.tmpl deleted file mode 100644 index 87746d50bdc43..0000000000000 --- a/ms-windows/osgeo4w/process.bat.tmpl +++ /dev/null @@ -1,14 +0,0 @@ -@echo off -call "%~dp0\o4w_env.bat" -call qt5_env.bat -call py3_env.bat -@echo off -path %OSGEO4W_ROOT%\apps\@package@\bin;%PATH% -set QGIS_PREFIX_PATH=%OSGEO4W_ROOT:\=/%/apps/@package@ -set GDAL_FILENAME_IS_UTF8=YES -rem Set VSI cache to be used as buffer, see #6448 -set VSI_CACHE=TRUE -set VSI_CACHE_SIZE=1000000 -set QT_PLUGIN_PATH=%OSGEO4W_ROOT%\apps\@package@\qtplugins;%OSGEO4W_ROOT%\apps\qt5\plugins -set PYTHONPATH=%OSGEO4W_ROOT%\apps\@package@\python;%PYTHONPATH% -"%OSGEO4W_ROOT%\apps\@package@\bin\qgis_process.exe" %* diff --git a/ms-windows/osgeo4w/python.bat.tmpl b/ms-windows/osgeo4w/python.bat.tmpl deleted file mode 100644 index 3282dcd50e5df..0000000000000 --- a/ms-windows/osgeo4w/python.bat.tmpl +++ /dev/null @@ -1,14 +0,0 @@ -@echo off -call "%~dp0\o4w_env.bat" -call qt5_env.bat -call py3_env.bat -@echo off -path %OSGEO4W_ROOT%\apps\@package@\bin;%PATH% -set QGIS_PREFIX_PATH=%OSGEO4W_ROOT:\=/%/apps/@package@ -set GDAL_FILENAME_IS_UTF8=YES -rem Set VSI cache to be used as buffer, see #6448 -set VSI_CACHE=TRUE -set VSI_CACHE_SIZE=1000000 -set QT_PLUGIN_PATH=%OSGEO4W_ROOT%\apps\@package@\qtplugins;%OSGEO4W_ROOT%\apps\qt5\plugins -set PYTHONPATH=%OSGEO4W_ROOT%\apps\@package@\python;%PYTHONPATH% -"%PYTHONHOME%\python" %* diff --git a/ms-windows/osgeo4w/qgis-grass.bat.tmpl b/ms-windows/osgeo4w/qgis-grass.bat.tmpl deleted file mode 100644 index 4d45dca5a881f..0000000000000 --- a/ms-windows/osgeo4w/qgis-grass.bat.tmpl +++ /dev/null @@ -1,15 +0,0 @@ -@echo off -call "%~dp0\o4w_env.bat" -call qt5_env.bat -call py3_env.bat -set savedpath=%PATH% -call "%OSGEO4W_ROOT%\apps\grass\@grasspath@\etc\env.bat" -@echo off -path %OSGEO4W_ROOT%\apps\@package@\bin;%OSGEO4W_ROOT%\apps\grass\@grasspath@\lib;%OSGEO4W_ROOT%\apps\grass\@grasspath@\bin;%savedpath% -set QGIS_PREFIX_PATH=%OSGEO4W_ROOT:\=/%/apps/@package@ -set GDAL_FILENAME_IS_UTF8=YES -rem Set VSI cache to be used as buffer, see #6448 -set VSI_CACHE=TRUE -set VSI_CACHE_SIZE=1000000 -set QT_PLUGIN_PATH=%OSGEO4W_ROOT%\apps\@package@\qtplugins;%OSGEO4W_ROOT%\apps\qt5\plugins -start "QGIS" /B "%OSGEO4W_ROOT%\bin\@package@-bin-g@grassmajor@.exe" %* diff --git a/ms-windows/osgeo4w/qgis.bat.tmpl b/ms-windows/osgeo4w/qgis.bat.tmpl deleted file mode 100644 index e3c09c80e4b3b..0000000000000 --- a/ms-windows/osgeo4w/qgis.bat.tmpl +++ /dev/null @@ -1,13 +0,0 @@ -@echo off -call "%~dp0\o4w_env.bat" -call qt5_env.bat -call py3_env.bat -@echo off -path %OSGEO4W_ROOT%\apps\@package@\bin;%PATH% -set QGIS_PREFIX_PATH=%OSGEO4W_ROOT:\=/%/apps/@package@ -set GDAL_FILENAME_IS_UTF8=YES -rem Set VSI cache to be used as buffer, see #6448 -set VSI_CACHE=TRUE -set VSI_CACHE_SIZE=1000000 -set QT_PLUGIN_PATH=%OSGEO4W_ROOT%\apps\@package@\qtplugins;%OSGEO4W_ROOT%\apps\qt5\plugins -start "QGIS" /B "%OSGEO4W_ROOT%\bin\@package@-bin.exe" %* diff --git a/ms-windows/osgeo4w/qgis.reg.tmpl b/ms-windows/osgeo4w/qgis.reg.tmpl deleted file mode 100644 index 9bbd6b5afc2d6..0000000000000 --- a/ms-windows/osgeo4w/qgis.reg.tmpl +++ /dev/null @@ -1,48 +0,0 @@ -Windows Registry Editor Version 5.00 - -[HKEY_CLASSES_ROOT\QGIS Project] -@="QGIS Project" - -[HKEY_CLASSES_ROOT\QGIS Project\DefaultIcon] -@="@osgeo4w@\\apps\\@package@\\icons\\qgis-qgs.ico" - -[HKEY_CLASSES_ROOT\QGIS Project\Shell] - -[HKEY_CLASSES_ROOT\QGIS Project\Shell\open] -@="" - -[HKEY_CLASSES_ROOT\QGIS Project\Shell\open\command] -@="\"@osgeo4w@\\bin\\@package@.bat\" \"%1\"" - -[HKEY_CLASSES_ROOT\.qgs] -@="QGIS Project" - -[HKEY_CLASSES_ROOT\.qgz] -@="QGIS Project" - -[HKEY_CLASSES_ROOT\QGIS Composer Template] -@="QGIS Composer Template" - -[HKEY_CLASSES_ROOT\QGIS Composer Template\DefaultIcon] -@="@osgeo4w@\\apps\\@package@\\icons\\qgis-qpt.ico" - -[HKEY_CLASSES_ROOT\.qpt] -@="QGIS Composer Template" - -[HKEY_CLASSES_ROOT\QGIS Layer Settings] -@="QGIS Layer Settings" - -[HKEY_CLASSES_ROOT\QGIS Layer Settings\DefaultIcon] -@="@osgeo4w@\\apps\\@package@\\icons\\qgis-qml.ico" - -[HKEY_CLASSES_ROOT\.qml] -@="QGIS Layer Settings" - -[HKEY_CLASSES_ROOT\QGIS Layer Definition] -@="QGIS Layer Definition" - -[HKEY_CLASSES_ROOT\QGIS Layer Definition\DefaultIcon] -@="@osgeo4w@\\apps\\@package@\\icons\\qgis-qlr.ico" - -[HKEY_CLASSES_ROOT\.qlr] -@="QGIS Layer Definition" diff --git a/ms-windows/osgeo4w/qgis.vars b/ms-windows/osgeo4w/qgis.vars deleted file mode 100644 index 2fa37cfef9cb1..0000000000000 --- a/ms-windows/osgeo4w/qgis.vars +++ /dev/null @@ -1,30 +0,0 @@ -PATH -GDAL_DATA -GDAL_DRIVER_PATH -GDAL_FILENAME_IS_UTF8 -GEOTIFF_CSV -GISBASE -GRASS_HTML_BROWSER -GRASS_PROJSHARE -GRASS_PYTHON -GRASS_SH -GRASS_WISH -JPEGMEM -NLS_LANG -OSGEO4W_ROOT -PROJ_LIB -PYTHONHOME -PYTHONPATH -QGIS_PREFIX_PATH -QT_PLUGIN_PATH -QT_RASTER_CLIP_LIMIT -VSI_CACHE -VSI_CACHE_SIZE -O4W_QT_PREFIX -O4W_QT_BINARIES -O4W_QT_PLUGINS -O4W_QT_LIBRARIES -O4W_QT_TRANSLATIONS -O4W_QT_HEADERS -PGEO_DRIVER_TEMPLATE -OGR_SKIP diff --git a/ms-windows/osgeo4w/runasadmin.ps1 b/ms-windows/osgeo4w/runasadmin.ps1 deleted file mode 100644 index ab2b4fca35a08..0000000000000 --- a/ms-windows/osgeo4w/runasadmin.ps1 +++ /dev/null @@ -1,4 +0,0 @@ -#-RunAsAdministrator -Write-Output ($args -join ' ') -$cmd, $args = $args -Start-Process $cmd -Wait -ArgumentList $args -NoNewWindow diff --git a/ms-windows/plugins.nsh b/ms-windows/plugins.nsh deleted file mode 100644 index 335ed71ba0e20..0000000000000 --- a/ms-windows/plugins.nsh +++ /dev/null @@ -1,14 +0,0 @@ -############################### reg2nsis begin ################################# -# This NSIS-script was generated by the Reg2Nsis utility # -# Author : Artem Zankovich # -# URL : http://aarrtteemm.nm.ru # -# Usage : You can freely inserts this into your setup script as inline text # -# or include file with the help of !include directive. # -# Please don't remove this header. # -################################################################################ - -WriteRegStr HKEY_CURRENT_USER "Software\QGIS\QGIS3\Plugins" "grassplugin" "true" -WriteRegStr HKEY_CURRENT_USER "Software\QGIS\QGIS3\Plugins" "offlineeditingplugin" "true" -WriteRegStr HKEY_CURRENT_USER "Software\QGIS\QGIS3\Plugins" "topolplugin" "true" - -############################### reg2nsis end ################################# diff --git a/ms-windows/python_plugins.nsh b/ms-windows/python_plugins.nsh deleted file mode 100644 index 7a86b37ec826d..0000000000000 --- a/ms-windows/python_plugins.nsh +++ /dev/null @@ -1,14 +0,0 @@ -############################### reg2nsis begin ################################# -# This NSIS-script was generated by the Reg2Nsis utility # -# Author : Artem Zankovich # -# URL : http://aarrtteemm.nm.ru # -# Usage : You can freely inserts this into your setup script as inline text # -# or include file with the help of !include directive. # -# Please don't remove this header. # -################################################################################ - -WriteRegStr HKEY_CURRENT_USER "Software\QGIS\QGIS3\PythonPlugins" "GdalTools" "true" -WriteRegStr HKEY_CURRENT_USER "Software\QGIS\QGIS3\PythonPlugins" "db_manager" "true" -WriteRegStr HKEY_CURRENT_USER "Software\QGIS\QGIS3\PythonPlugins" "processing" "true" - -############################### reg2nsis end ################################# diff --git a/ms-windows/quickpackage.sh b/ms-windows/quickpackage.sh deleted file mode 100755 index a146c1fa12ab1..0000000000000 --- a/ms-windows/quickpackage.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash -########################################################################### -# quickpackage.sh -# --------------------- -# Date : November 2010 -# Copyright : (C) 2010 by Tim Sutton -# Email : tim at kartoza 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. # -# # -########################################################################### - - -# This script is just for if you want to run the nsis (under linux) part -# of the package building process. Typically you should use -# -# osgeo4w/creatensis.pl -# -# rather to do the complete package build process. However running this -# script can be useful if you have manually tweaked the package contents -# under osgeo4w/unpacked and want to create a new package based on that. -# -# Tim Sutton November 2010 - -makensis \ --DVERSION_NUMBER='1.7.0' \ --DVERSION_NAME='Wroclaw' \ --DSVN_REVISION='0' \ --DQGIS_BASE='QGIS' \ --DINSTALLER_NAME='QGIS-1-7-0-Setup.exe' \ --DDISPLAYED_NAME='QGIS 1.7.0' \ --DBINARY_REVISION=1 \ --DINSTALLER_TYPE=OSGeo4W \ --DPACKAGE_FOLDER=osgeo4w/unpacked \ --DSHORTNAME=qgis \ -QGIS-Installer.nsi diff --git a/ms-windows/x64.nsh b/ms-windows/x64.nsh deleted file mode 100644 index e694c1e613693..0000000000000 --- a/ms-windows/x64.nsh +++ /dev/null @@ -1,54 +0,0 @@ -; --------------------- -; x64.nsh -; --------------------- -; -; A few simple macros to handle installations on x64 machines. -; -; RunningX64 checks if the installer is running on x64. -; -; ${If} ${RunningX64} -; MessageBox MB_OK "running on x64" -; ${EndIf} -; -; DisableX64FSRedirection disables file system redirection. -; EnableX64FSRedirection enables file system redirection. -; -; SetOutPath $SYSDIR -; ${DisableX64FSRedirection} -; File some.dll # extracts to C:\Windows\System32 -; ${EnableX64FSRedirection} -; File some.dll # extracts to C:\Windows\SysWOW64 -; - -!ifndef ___X64__NSH___ -!define ___X64__NSH___ - -!include LogicLib.nsh - -!macro _RunningX64 _a _b _t _f - !insertmacro _LOGICLIB_TEMP - System::Call kernel32::GetCurrentProcess()i.s - System::Call kernel32::IsWow64Process(is,*i.s) - Pop $_LOGICLIB_TEMP - !insertmacro _!= $_LOGICLIB_TEMP 0 `${_t}` `${_f}` -!macroend - -!define RunningX64 `"" RunningX64 ""` - -!macro DisableX64FSRedirection - - System::Call kernel32::Wow64EnableWow64FsRedirection(i0) - -!macroend - -!define DisableX64FSRedirection "!insertmacro DisableX64FSRedirection" - -!macro EnableX64FSRedirection - - System::Call kernel32::Wow64EnableWow64FsRedirection(i1) - -!macroend - -!define EnableX64FSRedirection "!insertmacro EnableX64FSRedirection" - -!endif # !___X64__NSH___ diff --git a/python/PyQt6/gui/gui_auto.sip b/python/PyQt6/gui/gui_auto.sip index e6bf953cc4333..6b52a03ca1d77 100644 --- a/python/PyQt6/gui/gui_auto.sip +++ b/python/PyQt6/gui/gui_auto.sip @@ -329,7 +329,9 @@ %Include auto_generated/editorwidgets/qgsdefaultsearchwidgetwrapper.sip %Include auto_generated/editorwidgets/qgsdoublespinbox.sip %Include auto_generated/editorwidgets/qgshtmlwidgetwrapper.sip +%If ( HAVE_QSCI_SIP ) %Include auto_generated/editorwidgets/qgsjsoneditwidget.sip +%End %Include auto_generated/editorwidgets/qgsmultiedittoolbutton.sip %Include auto_generated/editorwidgets/qgsrelationaggregatesearchwidgetwrapper.sip %Include auto_generated/editorwidgets/qgsrelationreferencesearchwidgetwrapper.sip diff --git a/python/gui/gui_auto.sip b/python/gui/gui_auto.sip index e6bf953cc4333..6b52a03ca1d77 100644 --- a/python/gui/gui_auto.sip +++ b/python/gui/gui_auto.sip @@ -329,7 +329,9 @@ %Include auto_generated/editorwidgets/qgsdefaultsearchwidgetwrapper.sip %Include auto_generated/editorwidgets/qgsdoublespinbox.sip %Include auto_generated/editorwidgets/qgshtmlwidgetwrapper.sip +%If ( HAVE_QSCI_SIP ) %Include auto_generated/editorwidgets/qgsjsoneditwidget.sip +%End %Include auto_generated/editorwidgets/qgsmultiedittoolbutton.sip %Include auto_generated/editorwidgets/qgsrelationaggregatesearchwidgetwrapper.sip %Include auto_generated/editorwidgets/qgsrelationreferencesearchwidgetwrapper.sip diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index e9847f5041b6e..3bd86feef79d9 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -515,6 +515,10 @@ if (ANDROID) else() add_executable(${QGIS_APP_NAME} MACOSX_BUNDLE WIN32 ${QGIS_APPMAIN_SRCS}) + if(MSVC AND BUILD_WITH_QT6) + qt_disable_unicode_defines(${QGIS_APP_NAME}) + endif(MSVC AND BUILD_WITH_QT6) + # require c++17 target_compile_features(${QGIS_APP_NAME} PRIVATE cxx_std_17) target_compile_definitions(${QGIS_APP_NAME} PRIVATE "QT_PLUGINS_DIR=\"${QT_PLUGINS_DIR}\"") diff --git a/src/app/main.cpp b/src/app/main.cpp index 5cb8ee81e5865..bce6f9f1ae430 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -177,10 +177,10 @@ void usage( const QString &appName ) << QStringLiteral( " the PostGIS extension\n" ) ; // OK #ifdef Q_OS_WIN - MessageBox( nullptr, - msg.join( QString() ).toLocal8Bit().constData(), - "QGIS command line options", - MB_OK ); + MessageBoxA( nullptr, + msg.join( QString() ).toLocal8Bit().constData(), + "QGIS command line options", + MB_OK ); #else std::cout << msg.join( QString() ).toLocal8Bit().constData(); #endif @@ -219,7 +219,7 @@ void myPrint( const char *fmt, ... ) #if defined(Q_OS_WIN) char buffer[1024]; vsnprintf( buffer, sizeof buffer, fmt, ap ); - OutputDebugString( buffer ); + OutputDebugStringA( buffer ); #else vfprintf( stderr, fmt, ap ); #endif diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index c6856c998c8d9..af244470b57c4 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -13054,7 +13054,11 @@ void QgisApp::openURL( QString url, bool useQgisDocDirectory ) CFRelease( urlRef ); #elif defined(Q_OS_WIN) if ( url.startsWith( "file://", Qt::CaseInsensitive ) ) +#ifdef UNICODE + ShellExecute( 0, 0, url.mid( 7 ).toStdWString().c_str(), 0, 0, SW_SHOWNORMAL ); +#else ShellExecute( 0, 0, url.mid( 7 ).toLocal8Bit().constData(), 0, 0, SW_SHOWNORMAL ); +#endif else QDesktopServices::openUrl( url ); #else diff --git a/src/core/layout/qgslayoutitemlegend.cpp b/src/core/layout/qgslayoutitemlegend.cpp index 6d1a02d3cfdaa..1ccef7dc7c81f 100644 --- a/src/core/layout/qgslayoutitemlegend.cpp +++ b/src/core/layout/qgslayoutitemlegend.cpp @@ -1198,7 +1198,7 @@ void QgsLayoutItemLegend::doUpdateFilterByMap() { mapExtent.transform( mapTransform ); } - catch ( QgsCsException &cse ) + catch ( QgsCsException & ) { continue; } diff --git a/src/core/mesh/qgstopologicalmesh.cpp b/src/core/mesh/qgstopologicalmesh.cpp index b789f224f99d9..e6b040f7e0675 100644 --- a/src/core/mesh/qgstopologicalmesh.cpp +++ b/src/core/mesh/qgstopologicalmesh.cpp @@ -703,7 +703,7 @@ QgsMeshEditingError QgsTopologicalMesh::counterClockwiseFaces( QgsMeshFace &face if ( error != QgsMeshEditingError() ) return error; - if ( clockwise > 0 )// clockwise --> reverse the order of the index; + if ( clockwise )// clockwise --> reverse the order of the index; { for ( int i = 0; i < faceSize / 2; ++i ) { diff --git a/src/core/pal/labelposition.cpp b/src/core/pal/labelposition.cpp index ba1a576c58d8a..caf010ed7af13 100644 --- a/src/core/pal/labelposition.cpp +++ b/src/core/pal/labelposition.cpp @@ -574,7 +574,7 @@ double LabelPosition::getDistanceToPoint( double xp, double yp, bool useOuterBou geos::unique_ptr point( GEOSGeom_createPointFromXY_r( geosctxt, xp, yp ) ); contains = ( GEOSPreparedContainsProperly_r( geosctxt, mPreparedOuterBoundsGeos, point.get() ) == 1 ); } - catch ( GEOSException &e ) + catch ( GEOSException & ) { contains = false; } diff --git a/src/core/pointcloud/qgslazdecoder.cpp b/src/core/pointcloud/qgslazdecoder.cpp index 67a4e3b7d462b..efde5717dba55 100644 --- a/src/core/pointcloud/qgslazdecoder.cpp +++ b/src/core/pointcloud/qgslazdecoder.cpp @@ -38,7 +38,9 @@ #include "lazperf/readers.hpp" #if defined(_MSC_VER) +#ifndef UNICODE #define UNICODE +#endif #include #include #endif diff --git a/src/core/pointcloud/qgspointcloudlayerrenderer.cpp b/src/core/pointcloud/qgspointcloudlayerrenderer.cpp index b4868da229b2e..276167147f58b 100644 --- a/src/core/pointcloud/qgspointcloudlayerrenderer.cpp +++ b/src/core/pointcloud/qgspointcloudlayerrenderer.cpp @@ -688,7 +688,7 @@ void QgsPointCloudLayerRenderer::renderTriangulatedSurface( QgsPointCloudRenderC { delaunator.reset( new delaunator::Delaunator( points ) ); } - catch ( std::exception &e ) + catch ( std::exception & ) { // something went wrong, better to retrieve initial state QgsDebugMsgLevel( QStringLiteral( "Error with triangulation" ), 4 ); diff --git a/src/core/qgsattributetableconfig.h b/src/core/qgsattributetableconfig.h index ba50862a33122..9daf64a74a2f3 100644 --- a/src/core/qgsattributetableconfig.h +++ b/src/core/qgsattributetableconfig.h @@ -48,7 +48,7 @@ class CORE_EXPORT QgsAttributeTableConfig /** * Defines the configuration of a column in the attribute table. */ - struct ColumnConfig + struct CORE_EXPORT ColumnConfig { //! Constructor for ColumnConfig ColumnConfig() = default; diff --git a/src/core/qgsexpressioncontext.cpp b/src/core/qgsexpressioncontext.cpp index b05f99490a5e7..8e0b9c2bcedd7 100644 --- a/src/core/qgsexpressioncontext.cpp +++ b/src/core/qgsexpressioncontext.cpp @@ -106,7 +106,11 @@ void QgsExpressionContextScope::addVariable( const QgsExpressionContextScope::St bool QgsExpressionContextScope::removeVariable( const QString &name ) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + return mVariables.remove( name ); +#else return mVariables.remove( name ) > 0; +#endif } bool QgsExpressionContextScope::hasVariable( const QString &name ) const diff --git a/src/core/qgsopenclutils.cpp b/src/core/qgsopenclutils.cpp index 39fab282a0d0c..beb1cf53e1570 100644 --- a/src/core/qgsopenclutils.cpp +++ b/src/core/qgsopenclutils.cpp @@ -25,6 +25,9 @@ #include #ifdef Q_OS_WIN +#if defined(UNICODE) && !defined(_UNICODE) +#define _UNICODE +#endif #include #include #endif @@ -100,7 +103,12 @@ void QgsOpenClUtils::init() } #ifdef Q_OS_WIN - HMODULE hModule = GetModuleHandle( "OpenCL.dll" ); +#ifdef _UNICODE +#define _T(x) L##x +#else +#define _T(x) x +#endif + HMODULE hModule = GetModuleHandle( _T( "OpenCL.dll" ) ); if ( hModule ) { TCHAR pszFileName[1024]; @@ -114,13 +122,13 @@ void QgsOpenClUtils::init() DWORD dwLen = GetFileVersionInfoSize( pszFileName, &dwUseless ); if ( dwLen ) { - LPTSTR lpVI = ( LPSTR ) malloc( dwLen ); + LPTSTR lpVI = ( LPTSTR ) malloc( dwLen * sizeof( TCHAR ) ); if ( lpVI ) { if ( GetFileVersionInfo( pszFileName, 0, dwLen, lpVI ) ) { VS_FIXEDFILEINFO *lpFFI; - if ( VerQueryValue( lpVI, "\\", ( LPVOID * ) &lpFFI, ( UINT * ) &dwUseless ) ) + if ( VerQueryValue( lpVI, _T( "\\" ), ( LPVOID * ) &lpFFI, ( UINT * ) &dwUseless ) ) { QgsMessageLog::logMessage( QObject::tr( "OpenCL Product version: %1.%2.%3.%4" ) .arg( lpFFI->dwProductVersionMS >> 16 ) @@ -163,13 +171,23 @@ void QgsOpenClUtils::init() QgsDebugMsgLevel( QString( "d:%1 subBlock:%2" ).arg( d ).arg( subBlock ), 2 ); - BOOL r = VerQueryValue( lpVI, subBlock.toUtf8(), ( LPVOID * )&lpBuffer, ( UINT * )&dwUseless ); + BOOL r = VerQueryValue( lpVI, +#ifdef UNICODE + subBlock.toStdWString().c_str(), +#else + subBlock.toUtf8(), +#endif + ( LPVOID * )&lpBuffer, ( UINT * )&dwUseless ); if ( r && lpBuffer && lpBuffer != INVALID_HANDLE_VALUE && dwUseless < 1023 ) { QgsMessageLog::logMessage( QObject::tr( "Found OpenCL version info %1: %2" ) .arg( d ) +#ifdef UNICODE + .arg( QString::fromUtf16( ( const ushort * ) lpBuffer ) ), +#else .arg( QString::fromLocal8Bit( lpBuffer ) ), +#endif LOGMESSAGE_TAG, Qgis::MessageLevel::Info ); } } diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index a9a7a64db345f..ae6d3f0850d25 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -1618,6 +1618,13 @@ else() PROPERTIES COMPILE_FLAGS "-w -Wno-deprecated-declarations") endif() +if(MSVC) + set_source_files_properties( + ${CMAKE_BINARY_DIR}/src/gui/qgis_gui_autogen/mocs_compilation.cpp + PROPERTIES COMPILE_FLAGS "/bigobj" + ) +endif() + ############################################################# # qgis_gui library @@ -1713,7 +1720,7 @@ GENERATE_EXPORT_HEADER( set(QGIS_GUI_HDRS ${QGIS_GUI_HDRS} ${CMAKE_CURRENT_BINARY_DIR}/qgis_gui.h) if(NOT APPLE OR NOT QGIS_MACAPP_FRAMEWORK) - if (WIN32 ) + if (WIN32) include_directories(${CMAKE_SOURCE_DIR}/src/native/win) elseif (APPLE) include_directories(${CMAKE_SOURCE_DIR}/src/native/mac) diff --git a/src/native/CMakeLists.txt b/src/native/CMakeLists.txt index cedc517814a18..73edb952b1ad8 100644 --- a/src/native/CMakeLists.txt +++ b/src/native/CMakeLists.txt @@ -144,7 +144,7 @@ if (UNIX AND NOT APPLE AND NOT ANDROID) target_link_libraries(qgis_native ${QT_VERSION_BASE}::DBus) endif() -if (MSVC) +if (MSVC AND NOT BUILD_WITH_QT6) find_package(${QT_VERSION_BASE}WinExtras) target_link_libraries(qgis_native shell32) diff --git a/src/native/win/qgswinnative.cpp b/src/native/win/qgswinnative.cpp index 008c207a35aaf..dbc83f6819b29 100644 --- a/src/native/win/qgswinnative.cpp +++ b/src/native/win/qgswinnative.cpp @@ -23,16 +23,24 @@ #include #include #include +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) #include #include #include #include #include +#endif #include "wintoastlib.h" #include #include #include +#ifdef UNICODE +#define _T(x) L##x +#else +#define _T(x) x +#endif + struct LPITEMIDLISTDeleter { @@ -57,6 +65,7 @@ void QgsWinNative::initializeMainWindow( QWindow *window, const QString &version ) { mWindow = window; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) if ( mTaskButton ) return; // already initialized! @@ -64,6 +73,7 @@ void QgsWinNative::initializeMainWindow( QWindow *window, mTaskButton->setWindow( window ); mTaskProgress = mTaskButton->progress(); mTaskProgress->setVisible( false ); +#endif QString appName = qgetenv( "QGIS_WIN_APP_NAME" ); if ( appName.isEmpty() ) @@ -126,7 +136,7 @@ void QgsWinNative::showFileProperties( const QString &path ) info.nShow = SW_SHOWNORMAL; info.fMask = SEE_MASK_INVOKEIDLIST; info.lpIDList = pidl.get(); - info.lpVerb = "properties"; + info.lpVerb = _T( "properties" ); ShellExecuteEx( &info ); } @@ -134,24 +144,31 @@ void QgsWinNative::showFileProperties( const QString &path ) void QgsWinNative::showUndefinedApplicationProgress() { +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) mTaskProgress->setMaximum( 0 ); mTaskProgress->show(); +#endif } void QgsWinNative::setApplicationProgress( double progress ) { +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) mTaskProgress->setMaximum( 100 ); mTaskProgress->show(); mTaskProgress->setValue( static_cast< int >( std::round( progress ) ) ); +#endif } void QgsWinNative::hideApplicationProgress() { +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) mTaskProgress->hide(); +#endif } void QgsWinNative::onRecentProjectsChanged( const std::vector &recentProjects ) { +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) QWinJumpList jumplist; jumplist.recent()->clear(); for ( const RecentProjectProperties &recentProject : recentProjects ) @@ -163,6 +180,7 @@ void QgsWinNative::onRecentProjectsChanged( const std::vectorsetArguments( QStringList( recentProject.path ) ); jumplist.recent()->addItem( newProject ); } +#endif } class NotificationHandler : public WinToastLib::IWinToastHandler @@ -228,7 +246,11 @@ bool QgsWinNative::openTerminalAtPath( const QString &path ) return process.startDetached( &pid ); } +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) bool QgsWinNativeEventFilter::nativeEventFilter( const QByteArray &eventType, void *message, long * ) +#else +bool QgsWinNativeEventFilter::nativeEventFilter( const QByteArray &eventType, void *message, qintptr * ) +#endif { static const QByteArray sWindowsGenericMSG{ "windows_generic_MSG" }; if ( !message || eventType != sWindowsGenericMSG ) diff --git a/src/native/win/qgswinnative.h b/src/native/win/qgswinnative.h index 9f0e7479d144e..c679029981289 100644 --- a/src/native/win/qgswinnative.h +++ b/src/native/win/qgswinnative.h @@ -15,8 +15,8 @@ * * ***************************************************************************/ -#ifndef QGSMACNATIVE_H -#define QGSMACNATIVE_H +#ifndef QGSWINNATIVE_H +#define QGSWINNATIVE_H #include "qgsnative.h" #include @@ -25,8 +25,10 @@ #include #pragma comment(lib,"Shell32.lib") +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) class QWinTaskbarButton; class QWinTaskbarProgress; +#endif class QWindow; @@ -35,7 +37,11 @@ class QgsWinNativeEventFilter : public QObject, public QAbstractNativeEventFilte Q_OBJECT public: +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) bool nativeEventFilter( const QByteArray &eventType, void *message, long * ) override; +#else + bool nativeEventFilter( const QByteArray &eventType, void *message, qintptr *result ) override; +#endif signals: @@ -70,10 +76,12 @@ class NATIVE_EXPORT QgsWinNative : public QgsNative QWindow *mWindow = nullptr; Capabilities mCapabilities = NativeFilePropertiesDialog | NativeOpenTerminalAtPath; bool mWinToastInitialized = false; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) QWinTaskbarButton *mTaskButton = nullptr; QWinTaskbarProgress *mTaskProgress = nullptr; +#endif QgsWinNativeEventFilter *mNativeEventFilter = nullptr; }; -#endif // QGSMACNATIVE_H +#endif // QGSWINNATIVE_H diff --git a/src/providers/grass/qgsgrass.cpp b/src/providers/grass/qgsgrass.cpp index eb8585c112e3e..946af31a1ded9 100644 --- a/src/providers/grass/qgsgrass.cpp +++ b/src/providers/grass/qgsgrass.cpp @@ -270,8 +270,8 @@ QString QgsGrass::pathSeparator() #include QString QgsGrass::shortPath( const QString &path ) { - TCHAR buf[MAX_PATH]; - int len = GetShortPathName( path.toUtf8().constData(), buf, MAX_PATH ); + char buf[MAX_PATH]; + int len = GetShortPathNameA( path.toUtf8().constData(), buf, MAX_PATH ); if ( len == 0 || len > MAX_PATH ) { diff --git a/src/server/qgis_mapserver.cpp b/src/server/qgis_mapserver.cpp index 38d9fe5dcb7ce..1ed9042085e09 100644 --- a/src/server/qgis_mapserver.cpp +++ b/src/server/qgis_mapserver.cpp @@ -177,7 +177,7 @@ class TcpServerWorker: public QObject }; // This will delete the connection - QTcpSocket::connect( clientConnection, &QAbstractSocket::disconnected, clientConnection, connectionDeleter, Qt::QueuedConnection ); + QObject::connect( clientConnection, &QAbstractSocket::disconnected, clientConnection, connectionDeleter, Qt::QueuedConnection ); #if 0 // Debugging output clientConnection->connect( clientConnection, &QAbstractSocket::errorOccurred, clientConnection, [ = ]( QAbstractSocket::SocketError socketError ) @@ -187,7 +187,7 @@ class TcpServerWorker: public QObject #endif // Incoming connection parser - QTcpSocket::connect( clientConnection, &QIODevice::readyRead, context, [ = ] { + QObject::connect( clientConnection, &QIODevice::readyRead, context, [ = ] { // Read all incoming data while ( clientConnection->bytesAvailable() > 0 ) From f4e43a233fbcdc5a58103195934d6de74e4d3970 Mon Sep 17 00:00:00 2001 From: "Juergen E. Fischer" Date: Tue, 14 May 2024 16:45:43 +0200 Subject: [PATCH 102/102] a bit of alignment --- CMakeLists.txt | 15 +++++++-------- python/PyQt6/gui/gui_auto.sip | 2 -- python/gui/gui_auto.sip | 2 -- src/app/main.cpp | 6 +++--- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 80d0070e49d09..96bb4d6df6e62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,13 @@ endif() # don't relink it only the shared object changes set(CMAKE_LINK_DEPENDS_NO_SHARED ON) +set (WITH_BINDINGS TRUE CACHE BOOL "Determines whether Python bindings should be built") +set (WITH_3D TRUE CACHE BOOL "Determines whether QGIS 3D library should be built") +set (WITH_QGIS_PROCESS TRUE CACHE BOOL "Determines whether the standalone \"qgis_process\" tool should be built") +set (WITH_DESKTOP TRUE CACHE BOOL "Determines whether QGIS desktop should be built") +set (WITH_GUI TRUE CACHE BOOL "Determines whether QGIS GUI library should be built") + + ############################################################# # Project and version set(CPACK_PACKAGE_VERSION_MAJOR "3") @@ -138,8 +145,6 @@ if(WITH_CORE) endif() endforeach (GRASS_SEARCH_VERSION) - set (WITH_GUI TRUE CACHE BOOL "Determines whether QGIS GUI library (and everything built on top of it) should be built") - set (WITH_OAUTH2_PLUGIN TRUE CACHE BOOL "Determines whether OAuth2 authentication method plugin should be built") if(WITH_OAUTH2_PLUGIN) set(HAVE_OAUTH2_PLUGIN TRUE) @@ -149,8 +154,6 @@ if(WITH_CORE) set (WITH_ANALYSIS TRUE CACHE BOOL "Determines whether QGIS analysis library should be built") - set (WITH_DESKTOP TRUE CACHE BOOL "Determines whether QGIS desktop should be built") - if(WITH_DESKTOP) if((WIN32 AND NOT MINGW) OR (UNIX AND NOT APPLE AND NOT ANDROID AND NOT IOS)) set (CRASH_HANDLER_AVAILABLE TRUE) @@ -169,17 +172,13 @@ if(WITH_CORE) endif() endif() - set (WITH_3D TRUE CACHE BOOL "Determines whether QGIS 3D library should be built") set (WITH_QUICK FALSE CACHE BOOL "Determines whether QGIS Quick library should be built") - set (WITH_QGIS_PROCESS TRUE CACHE BOOL "Determines whether the standalone \"qgis_process\" tool should be built") - set (NATIVE_CRSSYNC_BIN "" CACHE PATH "Path to a natively compiled synccrsdb binary. If set, crssync will not build but use provided bin instead.") mark_as_advanced (NATIVE_CRSSYNC_BIN) # try to configure and build python bindings by default - set (WITH_BINDINGS TRUE CACHE BOOL "Determines whether Python bindings should be built") if (WITH_BINDINGS) # By default bindings will be installed only to QGIS directory # Someone might want to install it to python site-packages directory diff --git a/python/PyQt6/gui/gui_auto.sip b/python/PyQt6/gui/gui_auto.sip index 6b52a03ca1d77..e6bf953cc4333 100644 --- a/python/PyQt6/gui/gui_auto.sip +++ b/python/PyQt6/gui/gui_auto.sip @@ -329,9 +329,7 @@ %Include auto_generated/editorwidgets/qgsdefaultsearchwidgetwrapper.sip %Include auto_generated/editorwidgets/qgsdoublespinbox.sip %Include auto_generated/editorwidgets/qgshtmlwidgetwrapper.sip -%If ( HAVE_QSCI_SIP ) %Include auto_generated/editorwidgets/qgsjsoneditwidget.sip -%End %Include auto_generated/editorwidgets/qgsmultiedittoolbutton.sip %Include auto_generated/editorwidgets/qgsrelationaggregatesearchwidgetwrapper.sip %Include auto_generated/editorwidgets/qgsrelationreferencesearchwidgetwrapper.sip diff --git a/python/gui/gui_auto.sip b/python/gui/gui_auto.sip index 6b52a03ca1d77..e6bf953cc4333 100644 --- a/python/gui/gui_auto.sip +++ b/python/gui/gui_auto.sip @@ -329,9 +329,7 @@ %Include auto_generated/editorwidgets/qgsdefaultsearchwidgetwrapper.sip %Include auto_generated/editorwidgets/qgsdoublespinbox.sip %Include auto_generated/editorwidgets/qgshtmlwidgetwrapper.sip -%If ( HAVE_QSCI_SIP ) %Include auto_generated/editorwidgets/qgsjsoneditwidget.sip -%End %Include auto_generated/editorwidgets/qgsmultiedittoolbutton.sip %Include auto_generated/editorwidgets/qgsrelationaggregatesearchwidgetwrapper.sip %Include auto_generated/editorwidgets/qgsrelationreferencesearchwidgetwrapper.sip diff --git a/src/app/main.cpp b/src/app/main.cpp index bce6f9f1ae430..2bfbaefbd8979 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -177,9 +177,9 @@ void usage( const QString &appName ) << QStringLiteral( " the PostGIS extension\n" ) ; // OK #ifdef Q_OS_WIN - MessageBoxA( nullptr, - msg.join( QString() ).toLocal8Bit().constData(), - "QGIS command line options", + MessageBoxW( nullptr, + msg.join( QString() ).toStdWString().c_str(), + L"QGIS command line options", MB_OK ); #else std::cout << msg.join( QString() ).toLocal8Bit().constData();