diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index 4d1de315bd17..3b937545e003 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -2187,13 +2187,20 @@ Qgis.Axis.baseClass = Qgis # monkey patching scoped based enum Qgis.AnnotationItemFlag.ScaleDependentBoundingBox.__doc__ = "Item's bounding box will vary depending on map scale" -Qgis.AnnotationItemFlag.__doc__ = "Flags for annotation items.\n\n.. versionadded:: 3.22\n\n" + '* ``ScaleDependentBoundingBox``: ' + Qgis.AnnotationItemFlag.ScaleDependentBoundingBox.__doc__ +Qgis.AnnotationItemFlag.SupportsReferenceScale.__doc__ = "Item supports reference scale based rendering (since QGIS 3.40)" +Qgis.AnnotationItemFlag.__doc__ = "Flags for annotation items.\n\n.. versionadded:: 3.22\n\n" + '* ``ScaleDependentBoundingBox``: ' + Qgis.AnnotationItemFlag.ScaleDependentBoundingBox.__doc__ + '\n' + '* ``SupportsReferenceScale``: ' + Qgis.AnnotationItemFlag.SupportsReferenceScale.__doc__ # -- Qgis.AnnotationItemFlags = lambda flags=0: Qgis.AnnotationItemFlag(flags) Qgis.AnnotationItemFlag.baseClass = Qgis Qgis.AnnotationItemFlags.baseClass = Qgis AnnotationItemFlags = Qgis # dirty hack since SIP seems to introduce the flags in module # monkey patching scoped based enum +Qgis.AnnotationPictureSizeMode.SpatialBounds.__doc__ = "Picture is rendered inside spatial bounds, and size will depend on map scale" +Qgis.AnnotationPictureSizeMode.FixedSize.__doc__ = "Picture is rendered at a fixed size, regardless of map scale" +Qgis.AnnotationPictureSizeMode.__doc__ = "Picture annotation item size modes.\n\n.. versionadded:: 3.40\n\n" + '* ``SpatialBounds``: ' + Qgis.AnnotationPictureSizeMode.SpatialBounds.__doc__ + '\n' + '* ``FixedSize``: ' + Qgis.AnnotationPictureSizeMode.FixedSize.__doc__ +# -- +Qgis.AnnotationPictureSizeMode.baseClass = Qgis +# monkey patching scoped based enum Qgis.AnnotationItemGuiFlag.FlagNoCreationTools.__doc__ = "Do not show item creation tools for the item type" Qgis.AnnotationItemGuiFlag.__doc__ = "Flags for controlling how an annotation item behaves in the GUI.\n\n.. versionadded:: 3.22\n\n" + '* ``FlagNoCreationTools``: ' + Qgis.AnnotationItemGuiFlag.FlagNoCreationTools.__doc__ # -- diff --git a/python/PyQt6/core/auto_generated/annotations/qgsannotationitem.sip.in b/python/PyQt6/core/auto_generated/annotations/qgsannotationitem.sip.in index 7cf91ec83418..cc6ec1cd1393 100644 --- a/python/PyQt6/core/auto_generated/annotations/qgsannotationitem.sip.in +++ b/python/PyQt6/core/auto_generated/annotations/qgsannotationitem.sip.in @@ -42,6 +42,10 @@ Abstract base class for annotation items which are drawn with :py:class:`QgsAnno { sipType = sipType_QgsAnnotationLineTextItem; } + else if ( sipCpp->type() == QLatin1String( "picture" ) ) + { + sipType = sipType_QgsAnnotationPictureItem; + } else { sipType = 0; diff --git a/python/PyQt6/core/auto_generated/annotations/qgsannotationlineitem.sip.in b/python/PyQt6/core/auto_generated/annotations/qgsannotationlineitem.sip.in index f925aa65757e..0dbd2eedeb48 100644 --- a/python/PyQt6/core/auto_generated/annotations/qgsannotationlineitem.sip.in +++ b/python/PyQt6/core/auto_generated/annotations/qgsannotationlineitem.sip.in @@ -41,6 +41,8 @@ Constructor for QgsAnnotationLineItem, with the specified ``curve``. virtual QgsAnnotationItemEditOperationTransientResults *transientEditResultsV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ) /Factory/; + virtual Qgis::AnnotationItemFlags flags() const; + static QgsAnnotationLineItem *create() /Factory/; %Docstring diff --git a/python/PyQt6/core/auto_generated/annotations/qgsannotationpictureitem.sip.in b/python/PyQt6/core/auto_generated/annotations/qgsannotationpictureitem.sip.in new file mode 100644 index 000000000000..7c3dcf430700 --- /dev/null +++ b/python/PyQt6/core/auto_generated/annotations/qgsannotationpictureitem.sip.in @@ -0,0 +1,259 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/annotations/qgsannotationpictureitem.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsAnnotationPictureItem : QgsAnnotationItem +{ +%Docstring(signature="appended") +An annotation item which renders a picture. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgsannotationpictureitem.h" +%End + public: + + QgsAnnotationPictureItem( Qgis::PictureFormat format, const QString &path, const QgsRectangle &bounds ); +%Docstring +Constructor for QgsAnnotationPictureItem, rendering the specified image ``path`` +within the specified ``bounds`` geometry. +%End + ~QgsAnnotationPictureItem(); + + virtual QString type() const; + + virtual Qgis::AnnotationItemFlags flags() const; + + virtual void render( QgsRenderContext &context, QgsFeedback *feedback ); + + virtual bool writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const; + + virtual QList< QgsAnnotationItemNode > nodesV2( const QgsAnnotationItemEditContext &context ) const; + + virtual Qgis::AnnotationItemEditOperationResult applyEditV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ); + + virtual QgsAnnotationItemEditOperationTransientResults *transientEditResultsV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ) /Factory/; + + + static QgsAnnotationPictureItem *create() /Factory/; +%Docstring +Creates a new polygon annotation item. +%End + + virtual bool readXml( const QDomElement &element, const QgsReadWriteContext &context ); + + virtual QgsAnnotationPictureItem *clone() const /Factory/; + + virtual QgsRectangle boundingBox() const; + + virtual QgsRectangle boundingBox( QgsRenderContext &context ) const; + + + QgsRectangle bounds() const; +%Docstring +Returns the bounds of the picture. + +The coordinate reference system for the bounds will be the parent layer's :py:func:`QgsAnnotationLayer.crs()`. + +When the :py:func:`~QgsAnnotationPictureItem.sizeMode` is :py:class:`Qgis`.AnnotationPictureSizeMode.FixedSize then the picture will be placed +at the center of the bounds. + +.. seealso:: :py:func:`setBounds` +%End + + void setBounds( const QgsRectangle &bounds ); +%Docstring +Sets the ``bounds`` of the picture. + +The coordinate reference system for the bounds will be the parent layer's :py:func:`QgsAnnotationLayer.crs()`. + +When the :py:func:`~QgsAnnotationPictureItem.sizeMode` is :py:class:`Qgis`.AnnotationPictureSizeMode.FixedSize then the picture will be placed +at the center of the bounds. + +.. seealso:: :py:func:`bounds` +%End + + QString path() const; +%Docstring +Returns the path of the image used to render the item. + +.. seealso:: :py:func:`setPath` +%End + + Qgis::PictureFormat format() const; +%Docstring +Returns the picture format. +%End + + void setPath( Qgis::PictureFormat format, const QString &path ); +%Docstring +Sets the ``format`` and ``path`` of the image used to render the item. + +.. seealso:: :py:func:`path` + +.. seealso:: :py:func:`format` +%End + + Qgis::AnnotationPictureSizeMode sizeMode() const; +%Docstring +Returns the size mode for the picture. + +.. seealso:: :py:func:`setSizeMode` +%End + + void setSizeMode( Qgis::AnnotationPictureSizeMode mode ); +%Docstring +Sets the size ``mode`` for the picture. + +.. seealso:: :py:func:`sizeMode` +%End + + QSizeF fixedSize() const; +%Docstring +Returns the fixed size to use for the picture, when the :py:func:`~QgsAnnotationPictureItem.sizeMode` is :py:class:`Qgis`.AnnotationPictureSizeMode.FixedSize. + +Units are retrieved via :py:func:`~QgsAnnotationPictureItem.fixedSizeUnit` + +.. seealso:: :py:func:`setFixedSize` + +.. seealso:: :py:func:`fixedSizeUnit` +%End + + void setFixedSize( const QSizeF &size ); +%Docstring +Sets the fixed ``size`` to use for the picture, when the :py:func:`~QgsAnnotationPictureItem.sizeMode` is :py:class:`Qgis`.AnnotationPictureSizeMode.FixedSize. + +Units are set via :py:func:`~QgsAnnotationPictureItem.setFixedSizeUnit` + +.. seealso:: :py:func:`fixedSize` + +.. seealso:: :py:func:`setFixedSizeUnit` +%End + + Qgis::RenderUnit fixedSizeUnit() const; +%Docstring +Returns the units to use for fixed picture sizes, when the :py:func:`~QgsAnnotationPictureItem.sizeMode` is :py:class:`Qgis`.AnnotationPictureSizeMode.FixedSize. + +.. seealso:: :py:func:`setFixedSizeUnit` + +.. seealso:: :py:func:`fixedSize` +%End + + void setFixedSizeUnit( Qgis::RenderUnit unit ); +%Docstring +Sets the ``unit`` to use for fixed picture sizes, when the :py:func:`~QgsAnnotationPictureItem.sizeMode` is :py:class:`Qgis`.AnnotationPictureSizeMode.FixedSize. + +.. seealso:: :py:func:`fixedSizeUnit` + +.. seealso:: :py:func:`setFixedSize` +%End + + bool lockAspectRatio() const; +%Docstring +Returns ``True`` if the aspect ratio of the picture will be retained. + +.. seealso:: :py:func:`setLockAspectRatio` +%End + + void setLockAspectRatio( bool locked ); +%Docstring +Sets whether the aspect ratio of the picture will be retained. + +.. seealso:: :py:func:`lockAspectRatio` +%End + + bool backgroundEnabled() const; +%Docstring +Returns ``True`` if the item's background should be rendered. + +.. seealso:: :py:func:`setBackgroundEnabled` + +.. seealso:: :py:func:`backgroundSymbol` +%End + + void setBackgroundEnabled( bool enabled ); +%Docstring +Sets whether the item's background should be rendered. + +.. seealso:: :py:func:`backgroundEnabled` + +.. seealso:: :py:func:`setBackgroundSymbol` +%End + + const QgsFillSymbol *backgroundSymbol() const; +%Docstring +Returns the symbol used to render the item's background. + +.. seealso:: :py:func:`backgroundEnabled` + +.. seealso:: :py:func:`setBackgroundSymbol` +%End + + void setBackgroundSymbol( QgsFillSymbol *symbol /Transfer/ ); +%Docstring +Sets the ``symbol`` used to render the item's background. + +The item takes ownership of the symbol. + +.. seealso:: :py:func:`backgroundSymbol` + +.. seealso:: :py:func:`setBackgroundEnabled` +%End + + bool frameEnabled() const; +%Docstring +Returns ``True`` if the item's frame should be rendered. + +.. seealso:: :py:func:`setFrameEnabled` + +.. seealso:: :py:func:`frameSymbol` +%End + + void setFrameEnabled( bool enabled ); +%Docstring +Sets whether the item's frame should be rendered. + +.. seealso:: :py:func:`frameEnabled` + +.. seealso:: :py:func:`setFrameSymbol` +%End + + const QgsFillSymbol *frameSymbol() const; +%Docstring +Returns the symbol used to render the item's frame. + +.. seealso:: :py:func:`frameEnabled` + +.. seealso:: :py:func:`setFrameSymbol` +%End + + void setFrameSymbol( QgsFillSymbol *symbol /Transfer/ ); +%Docstring +Sets the ``symbol`` used to render the item's frame. + +The item takes ownership of the symbol. + +.. seealso:: :py:func:`frameSymbol` + +.. seealso:: :py:func:`setBackgroundEnabled` +%End + + private: + QgsAnnotationPictureItem( const QgsAnnotationPictureItem &other ); +}; +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/annotations/qgsannotationpictureitem.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/PyQt6/core/auto_generated/annotations/qgsannotationpolygonitem.sip.in b/python/PyQt6/core/auto_generated/annotations/qgsannotationpolygonitem.sip.in index 4fb4b9b46f24..557e7f1c7cde 100644 --- a/python/PyQt6/core/auto_generated/annotations/qgsannotationpolygonitem.sip.in +++ b/python/PyQt6/core/auto_generated/annotations/qgsannotationpolygonitem.sip.in @@ -41,6 +41,8 @@ Constructor for QgsAnnotationPolygonItem, with the specified ``polygon`` geometr virtual QgsAnnotationItemEditOperationTransientResults *transientEditResultsV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ) /Factory/; + virtual Qgis::AnnotationItemFlags flags() const; + static QgsAnnotationPolygonItem *create() /Factory/; %Docstring diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index d6d9b35308f9..7b83d6e28eb0 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -1279,10 +1279,17 @@ The development version enum class AnnotationItemFlag /BaseType=IntFlag/ { ScaleDependentBoundingBox, + SupportsReferenceScale, }; typedef QFlags AnnotationItemFlags; + enum class AnnotationPictureSizeMode /BaseType=IntEnum/ + { + SpatialBounds, + FixedSize, + }; + enum class AnnotationItemGuiFlag /BaseType=IntFlag/ { FlagNoCreationTools, @@ -2607,6 +2614,7 @@ The development version Unknown, }; + enum class ScaleBarAlignment /BaseType=IntEnum/ { Left, diff --git a/python/PyQt6/core/core_auto.sip b/python/PyQt6/core/core_auto.sip index 3566f9a034d2..d5ccc57a6a7d 100644 --- a/python/PyQt6/core/core_auto.sip +++ b/python/PyQt6/core/core_auto.sip @@ -232,6 +232,7 @@ %Include auto_generated/annotations/qgsannotationlinetextitem.sip %Include auto_generated/annotations/qgsannotationmarkeritem.sip %Include auto_generated/annotations/qgsannotationmanager.sip +%Include auto_generated/annotations/qgsannotationpictureitem.sip %Include auto_generated/annotations/qgsannotationpointtextitem.sip %Include auto_generated/annotations/qgsannotationpolygonitem.sip %Include auto_generated/annotations/qgshtmlannotation.sip diff --git a/python/PyQt6/gui/auto_generated/annotations/qgscreateannotationitemmaptool.sip.in b/python/PyQt6/gui/auto_generated/annotations/qgscreateannotationitemmaptool.sip.in index 2669dc614af9..c7f47538ae72 100644 --- a/python/PyQt6/gui/auto_generated/annotations/qgscreateannotationitemmaptool.sip.in +++ b/python/PyQt6/gui/auto_generated/annotations/qgscreateannotationitemmaptool.sip.in @@ -85,6 +85,7 @@ whenever this signal is emitted. %End public: + virtual ~QgsCreateAnnotationItemMapToolInterface(); virtual QgsCreateAnnotationItemMapToolHandler *handler() = 0; diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index 0c77e0c4ca8c..5646321c12d3 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -2147,12 +2147,19 @@ Qgis.Axis.baseClass = Qgis # monkey patching scoped based enum Qgis.AnnotationItemFlag.ScaleDependentBoundingBox.__doc__ = "Item's bounding box will vary depending on map scale" -Qgis.AnnotationItemFlag.__doc__ = "Flags for annotation items.\n\n.. versionadded:: 3.22\n\n" + '* ``ScaleDependentBoundingBox``: ' + Qgis.AnnotationItemFlag.ScaleDependentBoundingBox.__doc__ +Qgis.AnnotationItemFlag.SupportsReferenceScale.__doc__ = "Item supports reference scale based rendering (since QGIS 3.40)" +Qgis.AnnotationItemFlag.__doc__ = "Flags for annotation items.\n\n.. versionadded:: 3.22\n\n" + '* ``ScaleDependentBoundingBox``: ' + Qgis.AnnotationItemFlag.ScaleDependentBoundingBox.__doc__ + '\n' + '* ``SupportsReferenceScale``: ' + Qgis.AnnotationItemFlag.SupportsReferenceScale.__doc__ # -- Qgis.AnnotationItemFlag.baseClass = Qgis Qgis.AnnotationItemFlags.baseClass = Qgis AnnotationItemFlags = Qgis # dirty hack since SIP seems to introduce the flags in module # monkey patching scoped based enum +Qgis.AnnotationPictureSizeMode.SpatialBounds.__doc__ = "Picture is rendered inside spatial bounds, and size will depend on map scale" +Qgis.AnnotationPictureSizeMode.FixedSize.__doc__ = "Picture is rendered at a fixed size, regardless of map scale" +Qgis.AnnotationPictureSizeMode.__doc__ = "Picture annotation item size modes.\n\n.. versionadded:: 3.40\n\n" + '* ``SpatialBounds``: ' + Qgis.AnnotationPictureSizeMode.SpatialBounds.__doc__ + '\n' + '* ``FixedSize``: ' + Qgis.AnnotationPictureSizeMode.FixedSize.__doc__ +# -- +Qgis.AnnotationPictureSizeMode.baseClass = Qgis +# monkey patching scoped based enum Qgis.AnnotationItemGuiFlag.FlagNoCreationTools.__doc__ = "Do not show item creation tools for the item type" Qgis.AnnotationItemGuiFlag.__doc__ = "Flags for controlling how an annotation item behaves in the GUI.\n\n.. versionadded:: 3.22\n\n" + '* ``FlagNoCreationTools``: ' + Qgis.AnnotationItemGuiFlag.FlagNoCreationTools.__doc__ # -- diff --git a/python/core/auto_generated/annotations/qgsannotationitem.sip.in b/python/core/auto_generated/annotations/qgsannotationitem.sip.in index 7cf91ec83418..cc6ec1cd1393 100644 --- a/python/core/auto_generated/annotations/qgsannotationitem.sip.in +++ b/python/core/auto_generated/annotations/qgsannotationitem.sip.in @@ -42,6 +42,10 @@ Abstract base class for annotation items which are drawn with :py:class:`QgsAnno { sipType = sipType_QgsAnnotationLineTextItem; } + else if ( sipCpp->type() == QLatin1String( "picture" ) ) + { + sipType = sipType_QgsAnnotationPictureItem; + } else { sipType = 0; diff --git a/python/core/auto_generated/annotations/qgsannotationlineitem.sip.in b/python/core/auto_generated/annotations/qgsannotationlineitem.sip.in index f925aa65757e..0dbd2eedeb48 100644 --- a/python/core/auto_generated/annotations/qgsannotationlineitem.sip.in +++ b/python/core/auto_generated/annotations/qgsannotationlineitem.sip.in @@ -41,6 +41,8 @@ Constructor for QgsAnnotationLineItem, with the specified ``curve``. virtual QgsAnnotationItemEditOperationTransientResults *transientEditResultsV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ) /Factory/; + virtual Qgis::AnnotationItemFlags flags() const; + static QgsAnnotationLineItem *create() /Factory/; %Docstring diff --git a/python/core/auto_generated/annotations/qgsannotationpictureitem.sip.in b/python/core/auto_generated/annotations/qgsannotationpictureitem.sip.in new file mode 100644 index 000000000000..7c3dcf430700 --- /dev/null +++ b/python/core/auto_generated/annotations/qgsannotationpictureitem.sip.in @@ -0,0 +1,259 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/annotations/qgsannotationpictureitem.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsAnnotationPictureItem : QgsAnnotationItem +{ +%Docstring(signature="appended") +An annotation item which renders a picture. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgsannotationpictureitem.h" +%End + public: + + QgsAnnotationPictureItem( Qgis::PictureFormat format, const QString &path, const QgsRectangle &bounds ); +%Docstring +Constructor for QgsAnnotationPictureItem, rendering the specified image ``path`` +within the specified ``bounds`` geometry. +%End + ~QgsAnnotationPictureItem(); + + virtual QString type() const; + + virtual Qgis::AnnotationItemFlags flags() const; + + virtual void render( QgsRenderContext &context, QgsFeedback *feedback ); + + virtual bool writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const; + + virtual QList< QgsAnnotationItemNode > nodesV2( const QgsAnnotationItemEditContext &context ) const; + + virtual Qgis::AnnotationItemEditOperationResult applyEditV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ); + + virtual QgsAnnotationItemEditOperationTransientResults *transientEditResultsV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ) /Factory/; + + + static QgsAnnotationPictureItem *create() /Factory/; +%Docstring +Creates a new polygon annotation item. +%End + + virtual bool readXml( const QDomElement &element, const QgsReadWriteContext &context ); + + virtual QgsAnnotationPictureItem *clone() const /Factory/; + + virtual QgsRectangle boundingBox() const; + + virtual QgsRectangle boundingBox( QgsRenderContext &context ) const; + + + QgsRectangle bounds() const; +%Docstring +Returns the bounds of the picture. + +The coordinate reference system for the bounds will be the parent layer's :py:func:`QgsAnnotationLayer.crs()`. + +When the :py:func:`~QgsAnnotationPictureItem.sizeMode` is :py:class:`Qgis`.AnnotationPictureSizeMode.FixedSize then the picture will be placed +at the center of the bounds. + +.. seealso:: :py:func:`setBounds` +%End + + void setBounds( const QgsRectangle &bounds ); +%Docstring +Sets the ``bounds`` of the picture. + +The coordinate reference system for the bounds will be the parent layer's :py:func:`QgsAnnotationLayer.crs()`. + +When the :py:func:`~QgsAnnotationPictureItem.sizeMode` is :py:class:`Qgis`.AnnotationPictureSizeMode.FixedSize then the picture will be placed +at the center of the bounds. + +.. seealso:: :py:func:`bounds` +%End + + QString path() const; +%Docstring +Returns the path of the image used to render the item. + +.. seealso:: :py:func:`setPath` +%End + + Qgis::PictureFormat format() const; +%Docstring +Returns the picture format. +%End + + void setPath( Qgis::PictureFormat format, const QString &path ); +%Docstring +Sets the ``format`` and ``path`` of the image used to render the item. + +.. seealso:: :py:func:`path` + +.. seealso:: :py:func:`format` +%End + + Qgis::AnnotationPictureSizeMode sizeMode() const; +%Docstring +Returns the size mode for the picture. + +.. seealso:: :py:func:`setSizeMode` +%End + + void setSizeMode( Qgis::AnnotationPictureSizeMode mode ); +%Docstring +Sets the size ``mode`` for the picture. + +.. seealso:: :py:func:`sizeMode` +%End + + QSizeF fixedSize() const; +%Docstring +Returns the fixed size to use for the picture, when the :py:func:`~QgsAnnotationPictureItem.sizeMode` is :py:class:`Qgis`.AnnotationPictureSizeMode.FixedSize. + +Units are retrieved via :py:func:`~QgsAnnotationPictureItem.fixedSizeUnit` + +.. seealso:: :py:func:`setFixedSize` + +.. seealso:: :py:func:`fixedSizeUnit` +%End + + void setFixedSize( const QSizeF &size ); +%Docstring +Sets the fixed ``size`` to use for the picture, when the :py:func:`~QgsAnnotationPictureItem.sizeMode` is :py:class:`Qgis`.AnnotationPictureSizeMode.FixedSize. + +Units are set via :py:func:`~QgsAnnotationPictureItem.setFixedSizeUnit` + +.. seealso:: :py:func:`fixedSize` + +.. seealso:: :py:func:`setFixedSizeUnit` +%End + + Qgis::RenderUnit fixedSizeUnit() const; +%Docstring +Returns the units to use for fixed picture sizes, when the :py:func:`~QgsAnnotationPictureItem.sizeMode` is :py:class:`Qgis`.AnnotationPictureSizeMode.FixedSize. + +.. seealso:: :py:func:`setFixedSizeUnit` + +.. seealso:: :py:func:`fixedSize` +%End + + void setFixedSizeUnit( Qgis::RenderUnit unit ); +%Docstring +Sets the ``unit`` to use for fixed picture sizes, when the :py:func:`~QgsAnnotationPictureItem.sizeMode` is :py:class:`Qgis`.AnnotationPictureSizeMode.FixedSize. + +.. seealso:: :py:func:`fixedSizeUnit` + +.. seealso:: :py:func:`setFixedSize` +%End + + bool lockAspectRatio() const; +%Docstring +Returns ``True`` if the aspect ratio of the picture will be retained. + +.. seealso:: :py:func:`setLockAspectRatio` +%End + + void setLockAspectRatio( bool locked ); +%Docstring +Sets whether the aspect ratio of the picture will be retained. + +.. seealso:: :py:func:`lockAspectRatio` +%End + + bool backgroundEnabled() const; +%Docstring +Returns ``True`` if the item's background should be rendered. + +.. seealso:: :py:func:`setBackgroundEnabled` + +.. seealso:: :py:func:`backgroundSymbol` +%End + + void setBackgroundEnabled( bool enabled ); +%Docstring +Sets whether the item's background should be rendered. + +.. seealso:: :py:func:`backgroundEnabled` + +.. seealso:: :py:func:`setBackgroundSymbol` +%End + + const QgsFillSymbol *backgroundSymbol() const; +%Docstring +Returns the symbol used to render the item's background. + +.. seealso:: :py:func:`backgroundEnabled` + +.. seealso:: :py:func:`setBackgroundSymbol` +%End + + void setBackgroundSymbol( QgsFillSymbol *symbol /Transfer/ ); +%Docstring +Sets the ``symbol`` used to render the item's background. + +The item takes ownership of the symbol. + +.. seealso:: :py:func:`backgroundSymbol` + +.. seealso:: :py:func:`setBackgroundEnabled` +%End + + bool frameEnabled() const; +%Docstring +Returns ``True`` if the item's frame should be rendered. + +.. seealso:: :py:func:`setFrameEnabled` + +.. seealso:: :py:func:`frameSymbol` +%End + + void setFrameEnabled( bool enabled ); +%Docstring +Sets whether the item's frame should be rendered. + +.. seealso:: :py:func:`frameEnabled` + +.. seealso:: :py:func:`setFrameSymbol` +%End + + const QgsFillSymbol *frameSymbol() const; +%Docstring +Returns the symbol used to render the item's frame. + +.. seealso:: :py:func:`frameEnabled` + +.. seealso:: :py:func:`setFrameSymbol` +%End + + void setFrameSymbol( QgsFillSymbol *symbol /Transfer/ ); +%Docstring +Sets the ``symbol`` used to render the item's frame. + +The item takes ownership of the symbol. + +.. seealso:: :py:func:`frameSymbol` + +.. seealso:: :py:func:`setBackgroundEnabled` +%End + + private: + QgsAnnotationPictureItem( const QgsAnnotationPictureItem &other ); +}; +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/annotations/qgsannotationpictureitem.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/annotations/qgsannotationpolygonitem.sip.in b/python/core/auto_generated/annotations/qgsannotationpolygonitem.sip.in index 4fb4b9b46f24..557e7f1c7cde 100644 --- a/python/core/auto_generated/annotations/qgsannotationpolygonitem.sip.in +++ b/python/core/auto_generated/annotations/qgsannotationpolygonitem.sip.in @@ -41,6 +41,8 @@ Constructor for QgsAnnotationPolygonItem, with the specified ``polygon`` geometr virtual QgsAnnotationItemEditOperationTransientResults *transientEditResultsV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ) /Factory/; + virtual Qgis::AnnotationItemFlags flags() const; + static QgsAnnotationPolygonItem *create() /Factory/; %Docstring diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index 2ee5117e2d4f..11a7f91d0f04 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -1279,10 +1279,17 @@ The development version enum class AnnotationItemFlag { ScaleDependentBoundingBox, + SupportsReferenceScale, }; typedef QFlags AnnotationItemFlags; + enum class AnnotationPictureSizeMode + { + SpatialBounds, + FixedSize, + }; + enum class AnnotationItemGuiFlag { FlagNoCreationTools, @@ -2607,6 +2614,7 @@ The development version Unknown, }; + enum class ScaleBarAlignment { Left, diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 3566f9a034d2..d5ccc57a6a7d 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -232,6 +232,7 @@ %Include auto_generated/annotations/qgsannotationlinetextitem.sip %Include auto_generated/annotations/qgsannotationmarkeritem.sip %Include auto_generated/annotations/qgsannotationmanager.sip +%Include auto_generated/annotations/qgsannotationpictureitem.sip %Include auto_generated/annotations/qgsannotationpointtextitem.sip %Include auto_generated/annotations/qgsannotationpolygonitem.sip %Include auto_generated/annotations/qgshtmlannotation.sip diff --git a/python/gui/auto_generated/annotations/qgscreateannotationitemmaptool.sip.in b/python/gui/auto_generated/annotations/qgscreateannotationitemmaptool.sip.in index 2669dc614af9..c7f47538ae72 100644 --- a/python/gui/auto_generated/annotations/qgscreateannotationitemmaptool.sip.in +++ b/python/gui/auto_generated/annotations/qgscreateannotationitemmaptool.sip.in @@ -85,6 +85,7 @@ whenever this signal is emitted. %End public: + virtual ~QgsCreateAnnotationItemMapToolInterface(); virtual QgsCreateAnnotationItemMapToolHandler *handler() = 0; diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 95f4796c8838..8043e196a281 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -834,6 +834,12 @@ void QgisApp::annotationItemTypeAdded( int id ) return; QgsCreateAnnotationItemMapToolInterface *tool = QgsGui::annotationItemGuiRegistry()->itemMetadata( id )->createMapTool( mMapCanvas, mAdvancedDigitizingDockWidget ); + if ( !tool ) + { + action->setChecked( false ); + return; + } + tool->mapTool()->setAction( action ); mMapCanvas->setMapTool( tool->mapTool() ); if ( qobject_cast< QgsMapToolCapture * >( tool->mapTool() ) ) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index eda085e9570c..56bf18d3e667 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -220,6 +220,7 @@ set(QGIS_CORE_SRCS annotations/qgsannotationlinetextitem.cpp annotations/qgsannotationmarkeritem.cpp annotations/qgsannotationmanager.cpp + annotations/qgsannotationpictureitem.cpp annotations/qgsannotationpointtextitem.cpp annotations/qgsannotationpolygonitem.cpp annotations/qgshtmlannotation.cpp @@ -1336,6 +1337,7 @@ set(QGIS_CORE_HDRS annotations/qgsannotationlinetextitem.h annotations/qgsannotationmarkeritem.h annotations/qgsannotationmanager.h + annotations/qgsannotationpictureitem.h annotations/qgsannotationpointtextitem.h annotations/qgsannotationpolygonitem.h annotations/qgsannotationregistry.h diff --git a/src/core/annotations/qgsannotationitem.h b/src/core/annotations/qgsannotationitem.h index 595e8e8e00be..58f3241a3a29 100644 --- a/src/core/annotations/qgsannotationitem.h +++ b/src/core/annotations/qgsannotationitem.h @@ -63,6 +63,10 @@ class CORE_EXPORT QgsAnnotationItem { sipType = sipType_QgsAnnotationLineTextItem; } + else if ( sipCpp->type() == QLatin1String( "picture" ) ) + { + sipType = sipType_QgsAnnotationPictureItem; + } else { sipType = 0; diff --git a/src/core/annotations/qgsannotationitemregistry.cpp b/src/core/annotations/qgsannotationitemregistry.cpp index 668d3bf17ca3..c77436fdb2d0 100644 --- a/src/core/annotations/qgsannotationitemregistry.cpp +++ b/src/core/annotations/qgsannotationitemregistry.cpp @@ -21,6 +21,7 @@ #include "qgsannotationpolygonitem.h" #include "qgsannotationpointtextitem.h" #include "qgsannotationlinetextitem.h" +#include "qgsannotationpictureitem.h" #include QgsAnnotationItemRegistry::QgsAnnotationItemRegistry( QObject *parent ) @@ -48,6 +49,8 @@ bool QgsAnnotationItemRegistry::populate() QgsAnnotationPointTextItem::create ) ); mMetadata.insert( QStringLiteral( "linetext" ), new QgsAnnotationItemMetadata( QStringLiteral( "linetext" ), QObject::tr( "Text along line" ), QObject::tr( "Text along lines" ), QgsAnnotationLineTextItem::create ) ); + mMetadata.insert( QStringLiteral( "picture" ), new QgsAnnotationItemMetadata( QStringLiteral( "picture" ), QObject::tr( "Picture" ), QObject::tr( "Pictures" ), + QgsAnnotationPictureItem::create ) ); return true; } diff --git a/src/core/annotations/qgsannotationlayerrenderer.cpp b/src/core/annotations/qgsannotationlayerrenderer.cpp index ec7ef3af42f4..39742289c4b8 100644 --- a/src/core/annotations/qgsannotationlayerrenderer.cpp +++ b/src/core/annotations/qgsannotationlayerrenderer.cpp @@ -89,7 +89,7 @@ bool QgsAnnotationLayerRenderer::render() continue; std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride; - if ( item.second->useSymbologyReferenceScale() ) + if ( item.second->useSymbologyReferenceScale() && item.second->flags() & Qgis::AnnotationItemFlag::SupportsReferenceScale ) { referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, item.second->symbologyReferenceScale() ) ); } diff --git a/src/core/annotations/qgsannotationlineitem.cpp b/src/core/annotations/qgsannotationlineitem.cpp index 9e95eb94b2f5..c0bf248217c8 100644 --- a/src/core/annotations/qgsannotationlineitem.cpp +++ b/src/core/annotations/qgsannotationlineitem.cpp @@ -169,6 +169,11 @@ QgsAnnotationItemEditOperationTransientResults *QgsAnnotationLineItem::transient return nullptr; } +Qgis::AnnotationItemFlags QgsAnnotationLineItem::flags() const +{ + return Qgis::AnnotationItemFlag::SupportsReferenceScale; +} + QgsAnnotationLineItem *QgsAnnotationLineItem::create() { return new QgsAnnotationLineItem( new QgsLineString() ); diff --git a/src/core/annotations/qgsannotationlineitem.h b/src/core/annotations/qgsannotationlineitem.h index 3bb2a0f5dfa8..9dc214c2438c 100644 --- a/src/core/annotations/qgsannotationlineitem.h +++ b/src/core/annotations/qgsannotationlineitem.h @@ -46,6 +46,7 @@ class CORE_EXPORT QgsAnnotationLineItem : public QgsAnnotationItem QList< QgsAnnotationItemNode > nodesV2( const QgsAnnotationItemEditContext &context ) const override; Qgis::AnnotationItemEditOperationResult applyEditV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ) override; QgsAnnotationItemEditOperationTransientResults *transientEditResultsV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ) override SIP_FACTORY; + Qgis::AnnotationItemFlags flags() const override; /** * Creates a new linestring annotation item. diff --git a/src/core/annotations/qgsannotationlinetextitem.cpp b/src/core/annotations/qgsannotationlinetextitem.cpp index cde0f6f60217..7e19981aa795 100644 --- a/src/core/annotations/qgsannotationlinetextitem.cpp +++ b/src/core/annotations/qgsannotationlinetextitem.cpp @@ -36,7 +36,8 @@ QgsAnnotationLineTextItem::QgsAnnotationLineTextItem( const QString &text, QgsCu Qgis::AnnotationItemFlags QgsAnnotationLineTextItem::flags() const { // in truth this should depend on whether the text format is scale dependent or not! - return Qgis::AnnotationItemFlag::ScaleDependentBoundingBox; + return Qgis::AnnotationItemFlag::ScaleDependentBoundingBox + | Qgis::AnnotationItemFlag::SupportsReferenceScale; } QgsAnnotationLineTextItem::~QgsAnnotationLineTextItem() = default; diff --git a/src/core/annotations/qgsannotationmarkeritem.cpp b/src/core/annotations/qgsannotationmarkeritem.cpp index 12cb559087d1..e39ff003ac9a 100644 --- a/src/core/annotations/qgsannotationmarkeritem.cpp +++ b/src/core/annotations/qgsannotationmarkeritem.cpp @@ -72,7 +72,8 @@ bool QgsAnnotationMarkerItem::writeXml( QDomElement &element, QDomDocument &docu Qgis::AnnotationItemFlags QgsAnnotationMarkerItem::flags() const { // in truth this should depend on whether the marker symbol is scale dependent or not! - return Qgis::AnnotationItemFlag::ScaleDependentBoundingBox; + return Qgis::AnnotationItemFlag::ScaleDependentBoundingBox + | Qgis::AnnotationItemFlag::SupportsReferenceScale; } QList QgsAnnotationMarkerItem::nodesV2( const QgsAnnotationItemEditContext & ) const diff --git a/src/core/annotations/qgsannotationpictureitem.cpp b/src/core/annotations/qgsannotationpictureitem.cpp new file mode 100644 index 000000000000..ede3ade24976 --- /dev/null +++ b/src/core/annotations/qgsannotationpictureitem.cpp @@ -0,0 +1,585 @@ +/*************************************************************************** + qgsannotationpictureitem.cpp + ---------------- + begin : July 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 "qgsannotationpictureitem.h" +#include "qgsapplication.h" +#include "qgsimagecache.h" +#include "qgssvgcache.h" +#include "qgsgeometry.h" +#include "qgsrendercontext.h" +#include "qgsannotationitemnode.h" +#include "qgsannotationitemeditoperation.h" +#include "qgspainting.h" +#include "qgsfillsymbol.h" +#include "qgssymbollayerutils.h" +#include "qgsfillsymbollayer.h" +#include "qgslinesymbollayer.h" +#include "qgsunittypes.h" + + +#include + +QgsAnnotationPictureItem::QgsAnnotationPictureItem( Qgis::PictureFormat format, const QString &path, const QgsRectangle &bounds ) + : QgsAnnotationItem() + , mBounds( bounds ) +{ + setPath( format, path ); + + mBackgroundSymbol = std::make_unique< QgsFillSymbol >( + QgsSymbolLayerList + { + new QgsSimpleFillSymbolLayer( QColor( 255, 255, 255 ), Qt::BrushStyle::SolidPattern, QColor( 0, 0, 0 ), Qt::PenStyle::NoPen ) + } + ); + QgsSimpleLineSymbolLayer *borderSymbol = new QgsSimpleLineSymbolLayer( QColor( 0, 0, 0 ) ); + borderSymbol->setPenJoinStyle( Qt::MiterJoin ); + mFrameSymbol = std::make_unique< QgsFillSymbol >( QgsSymbolLayerList{ borderSymbol } ); +} + +QgsAnnotationPictureItem::~QgsAnnotationPictureItem() = default; + +QString QgsAnnotationPictureItem::type() const +{ + return QStringLiteral( "picture" ); +} + +Qgis::AnnotationItemFlags QgsAnnotationPictureItem::flags() const +{ + switch ( mSizeMode ) + { + case Qgis::AnnotationPictureSizeMode::SpatialBounds: + return Qgis::AnnotationItemFlags(); + case Qgis::AnnotationPictureSizeMode::FixedSize: + return Qgis::AnnotationItemFlag::ScaleDependentBoundingBox;; + } + BUILTIN_UNREACHABLE +} + +void QgsAnnotationPictureItem::render( QgsRenderContext &context, QgsFeedback * ) +{ + QgsRectangle bounds = mBounds; + if ( context.coordinateTransform().isValid() ) + { + try + { + bounds = context.coordinateTransform().transformBoundingBox( mBounds ); + } + catch ( QgsCsException & ) + { + return; + } + } + + bool lockAspectRatio = mLockAspectRatio; + QRectF painterBounds; + + switch ( mSizeMode ) + { + case Qgis::AnnotationPictureSizeMode::SpatialBounds: + painterBounds = context.mapToPixel().transformBounds( bounds.toRectF() ); + break; + + case Qgis::AnnotationPictureSizeMode::FixedSize: + { + QPointF center = bounds.center().toQPointF(); + + context.mapToPixel().transformInPlace( center.rx(), center.ry() ); + + const double widthPixels = context.convertToPainterUnits( mFixedSize.width(), mFixedSizeUnit ); + const double heightPixels = context.convertToPainterUnits( mFixedSize.height(), mFixedSizeUnit ); + + painterBounds = QRectF( center.x() - widthPixels * 0.5, + center.y() - heightPixels * 0.5, + widthPixels, heightPixels ); + break; + } + } + + if ( painterBounds.width() < 1 || painterBounds.height() < 1 ) + return; + + if ( mDrawBackground && mBackgroundSymbol ) + { + mBackgroundSymbol->startRender( context ); + mBackgroundSymbol->renderPolygon( painterBounds, nullptr, nullptr, context ); + mBackgroundSymbol->stopRender( context ); + } + + bool fitsInCache = false; + switch ( mFormat ) + { + case Qgis::PictureFormat::SVG: + { + const QSizeF svgViewbox = QgsApplication::svgCache()->svgViewboxSize( mPath, painterBounds.width(), QColor(), QColor(), 1, context.scaleFactor() ); + double aspectRatio = painterBounds.height() / painterBounds.width(); + double svgWidth = painterBounds.width(); + if ( lockAspectRatio ) + { + aspectRatio = svgViewbox.height() / svgViewbox.width(); + if ( ( painterBounds.height() / painterBounds.width() ) < aspectRatio ) + { + svgWidth = painterBounds.height() / aspectRatio; + } + } + + const QPicture picture = QgsApplication::svgCache()->svgAsPicture( mPath, svgWidth, QColor(), QColor(), 1, context.scaleFactor(), context.forceVectorOutput(), aspectRatio ); + const double pictureWidth = picture.boundingRect().width(); + const double pictureHeight = picture.boundingRect().height(); + + double xOffset = 0; + if ( lockAspectRatio && static_cast< int >( painterBounds.width() ) > pictureWidth ) + { + xOffset = ( painterBounds.width() - pictureWidth ) * 0.5; + } + double yOffset = 0; + if ( lockAspectRatio && static_cast< int >( painterBounds.height() ) > pictureHeight ) + { + yOffset = ( painterBounds.height() - pictureHeight ) * 0.5; + } + + QgsPainting::drawPicture( context.painter(), QPointF( painterBounds.left() + pictureWidth / 2 + xOffset, + painterBounds.top() + pictureHeight / 2 + yOffset ), picture ); + + break; + } + + case Qgis::PictureFormat::Raster: + { + const QImage im = QgsApplication::imageCache()->pathAsImage( mPath, + QSize( static_cast< int >( std::round( painterBounds.width() ) ), static_cast< int >( std::round( painterBounds.height() ) ) ), + lockAspectRatio, 1, fitsInCache ); + double xOffset = 0; + if ( lockAspectRatio && static_cast< int >( painterBounds.width() ) > im.width() ) + { + xOffset = ( painterBounds.width() - im.width() ) * 0.5; + } + double yOffset = 0; + if ( lockAspectRatio && static_cast< int >( painterBounds.height() ) > im.height() ) + { + yOffset = ( painterBounds.height() - im.height() ) * 0.5; + } + context.painter()->drawImage( QPointF( painterBounds.left() + xOffset, painterBounds.top() + yOffset ), im ); + break; + } + + case Qgis::PictureFormat::Unknown: + break; + } + + if ( mDrawFrame && mFrameSymbol ) + { + mFrameSymbol->startRender( context ); + mFrameSymbol->renderPolygon( painterBounds, nullptr, nullptr, context ); + mFrameSymbol->stopRender( context ); + } + +} + +bool QgsAnnotationPictureItem::writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const +{ + element.setAttribute( QStringLiteral( "lockAspect" ), mLockAspectRatio ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + element.setAttribute( QStringLiteral( "xMin" ), qgsDoubleToString( mBounds.xMinimum() ) ); + element.setAttribute( QStringLiteral( "xMax" ), qgsDoubleToString( mBounds.xMaximum() ) ); + element.setAttribute( QStringLiteral( "yMin" ), qgsDoubleToString( mBounds.yMinimum() ) ); + element.setAttribute( QStringLiteral( "yMax" ), qgsDoubleToString( mBounds.yMaximum() ) ); + element.setAttribute( QStringLiteral( "path" ), mPath ); + element.setAttribute( QStringLiteral( "format" ), qgsEnumValueToKey( mFormat ) ); + element.setAttribute( QStringLiteral( "sizeMode" ), qgsEnumValueToKey( mSizeMode ) ); + element.setAttribute( QStringLiteral( "fixedWidth" ), qgsDoubleToString( mFixedSize.width() ) ); + element.setAttribute( QStringLiteral( "fixedHeight" ), qgsDoubleToString( mFixedSize.height() ) ); + element.setAttribute( QStringLiteral( "fixedSizeUnit" ), QgsUnitTypes::encodeUnit( mFixedSizeUnit ) ); + + element.setAttribute( QStringLiteral( "backgroundEnabled" ), mDrawBackground ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + if ( mBackgroundSymbol ) + { + QDomElement backgroundElement = document.createElement( QStringLiteral( "backgroundSymbol" ) ); + backgroundElement.appendChild( QgsSymbolLayerUtils::saveSymbol( QStringLiteral( "backgroundSymbol" ), mBackgroundSymbol.get(), document, context ) ); + element.appendChild( backgroundElement ); + } + + element.setAttribute( QStringLiteral( "frameEnabled" ), mDrawFrame ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + if ( mFrameSymbol ) + { + QDomElement frameElement = document.createElement( QStringLiteral( "frameSymbol" ) ); + frameElement.appendChild( QgsSymbolLayerUtils::saveSymbol( QStringLiteral( "frameSymbol" ), mFrameSymbol.get(), document, context ) ); + element.appendChild( frameElement ); + } + + writeCommonProperties( element, document, context ); + return true; +} + +QList QgsAnnotationPictureItem::nodesV2( const QgsAnnotationItemEditContext & ) const +{ + switch ( mSizeMode ) + { + case Qgis::AnnotationPictureSizeMode::SpatialBounds: + return + { + QgsAnnotationItemNode( QgsVertexId( 0, 0, 0 ), QgsPointXY( mBounds.xMinimum(), mBounds.yMinimum() ), Qgis::AnnotationItemNodeType::VertexHandle ), + QgsAnnotationItemNode( QgsVertexId( 0, 0, 1 ), QgsPointXY( mBounds.xMaximum(), mBounds.yMinimum() ), Qgis::AnnotationItemNodeType::VertexHandle ), + QgsAnnotationItemNode( QgsVertexId( 0, 0, 2 ), QgsPointXY( mBounds.xMaximum(), mBounds.yMaximum() ), Qgis::AnnotationItemNodeType::VertexHandle ), + QgsAnnotationItemNode( QgsVertexId( 0, 0, 3 ), QgsPointXY( mBounds.xMinimum(), mBounds.yMaximum() ), Qgis::AnnotationItemNodeType::VertexHandle ), + }; + + case Qgis::AnnotationPictureSizeMode::FixedSize: + return + { + QgsAnnotationItemNode( QgsVertexId( 0, 0, 0 ), mBounds.center(), Qgis::AnnotationItemNodeType::VertexHandle ) + }; + } + BUILTIN_UNREACHABLE +} + +Qgis::AnnotationItemEditOperationResult QgsAnnotationPictureItem::applyEditV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext & ) +{ + switch ( operation->type() ) + { + case QgsAbstractAnnotationItemEditOperation::Type::MoveNode: + { + QgsAnnotationItemEditOperationMoveNode *moveOperation = dynamic_cast< QgsAnnotationItemEditOperationMoveNode * >( operation ); + switch ( mSizeMode ) + { + case Qgis::AnnotationPictureSizeMode::SpatialBounds: + { + switch ( moveOperation->nodeId().vertex ) + { + case 0: + mBounds = QgsRectangle( moveOperation->after().x(), + moveOperation->after().y(), + mBounds.xMaximum(), + mBounds.yMaximum() ); + break; + case 1: + mBounds = QgsRectangle( mBounds.xMinimum(), + moveOperation->after().y(), + moveOperation->after().x(), + mBounds.yMaximum() ); + break; + case 2: + mBounds = QgsRectangle( mBounds.xMinimum(), + mBounds.yMinimum(), + moveOperation->after().x(), + moveOperation->after().y() ); + break; + case 3: + mBounds = QgsRectangle( moveOperation->after().x(), + mBounds.yMinimum(), + mBounds.xMaximum(), + moveOperation->after().y() ); + break; + default: + break; + } + return Qgis::AnnotationItemEditOperationResult::Success; + } + + case Qgis::AnnotationPictureSizeMode::FixedSize: + { + mBounds = QgsRectangle::fromCenterAndSize( moveOperation->after(), + mBounds.width(), + mBounds.height() ); + return Qgis::AnnotationItemEditOperationResult::Success; + } + } + break; + } + + case QgsAbstractAnnotationItemEditOperation::Type::TranslateItem: + { + QgsAnnotationItemEditOperationTranslateItem *moveOperation = qgis::down_cast< QgsAnnotationItemEditOperationTranslateItem * >( operation ); + mBounds = QgsRectangle( mBounds.xMinimum() + moveOperation->translationX(), + mBounds.yMinimum() + moveOperation->translationY(), + mBounds.xMaximum() + moveOperation->translationX(), + mBounds.yMaximum() + moveOperation->translationY() ); + return Qgis::AnnotationItemEditOperationResult::Success; + } + + case QgsAbstractAnnotationItemEditOperation::Type::DeleteNode: + case QgsAbstractAnnotationItemEditOperation::Type::AddNode: + break; + } + return Qgis::AnnotationItemEditOperationResult::Invalid; +} + +QgsAnnotationItemEditOperationTransientResults *QgsAnnotationPictureItem::transientEditResultsV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ) +{ + switch ( operation->type() ) + { + case QgsAbstractAnnotationItemEditOperation::Type::MoveNode: + { + QgsAnnotationItemEditOperationMoveNode *moveOperation = dynamic_cast< QgsAnnotationItemEditOperationMoveNode * >( operation ); + switch ( mSizeMode ) + { + case Qgis::AnnotationPictureSizeMode::SpatialBounds: + { + QgsRectangle modifiedBounds = mBounds; + switch ( moveOperation->nodeId().vertex ) + { + case 0: + modifiedBounds.setXMinimum( moveOperation->after().x() ); + modifiedBounds.setYMinimum( moveOperation->after().y() ); + break; + case 1: + modifiedBounds.setXMaximum( moveOperation->after().x() ); + modifiedBounds.setYMinimum( moveOperation->after().y() ); + break; + case 2: + modifiedBounds.setXMaximum( moveOperation->after().x() ); + modifiedBounds.setYMaximum( moveOperation->after().y() ); + break; + case 3: + modifiedBounds.setXMinimum( moveOperation->after().x() ); + modifiedBounds.setYMaximum( moveOperation->after().y() ); + break; + default: + break; + } + return new QgsAnnotationItemEditOperationTransientResults( QgsGeometry::fromRect( modifiedBounds ) );; + } + case Qgis::AnnotationPictureSizeMode::FixedSize: + { + const QgsRectangle currentBounds = context.currentItemBounds(); + const QgsRectangle newBounds = QgsRectangle::fromCenterAndSize( moveOperation->after(), currentBounds.width(), currentBounds.height() ); + return new QgsAnnotationItemEditOperationTransientResults( QgsGeometry::fromRect( newBounds ) ); + } + } + break; + } + + case QgsAbstractAnnotationItemEditOperation::Type::TranslateItem: + { + QgsAnnotationItemEditOperationTranslateItem *moveOperation = qgis::down_cast< QgsAnnotationItemEditOperationTranslateItem * >( operation ); + switch ( mSizeMode ) + { + case Qgis::AnnotationPictureSizeMode::SpatialBounds: + { + const QgsRectangle modifiedBounds( mBounds.xMinimum() + moveOperation->translationX(), + mBounds.yMinimum() + moveOperation->translationY(), + mBounds.xMaximum() + moveOperation->translationX(), + mBounds.yMaximum() + moveOperation->translationY() ); + return new QgsAnnotationItemEditOperationTransientResults( QgsGeometry::fromRect( modifiedBounds ) ); + } + + case Qgis::AnnotationPictureSizeMode::FixedSize: + { + const QgsRectangle currentBounds = context.currentItemBounds(); + const QgsRectangle newBounds = QgsRectangle::fromCenterAndSize( mBounds.center() + QgsVector( moveOperation->translationX(), moveOperation->translationY() ), + currentBounds.width(), currentBounds.height() ); + return new QgsAnnotationItemEditOperationTransientResults( QgsGeometry::fromRect( newBounds ) ); + } + } + break; + } + + case QgsAbstractAnnotationItemEditOperation::Type::DeleteNode: + case QgsAbstractAnnotationItemEditOperation::Type::AddNode: + break; + } + return nullptr; +} + +QgsAnnotationPictureItem *QgsAnnotationPictureItem::create() +{ + return new QgsAnnotationPictureItem( Qgis::PictureFormat::Unknown, QString(), QgsRectangle() ); +} + +bool QgsAnnotationPictureItem::readXml( const QDomElement &element, const QgsReadWriteContext &context ) +{ + mLockAspectRatio = element.attribute( QStringLiteral( "lockAspect" ), QStringLiteral( "1" ) ).toInt(); + mBounds.setXMinimum( element.attribute( QStringLiteral( "xMin" ) ).toDouble() ); + mBounds.setXMaximum( element.attribute( QStringLiteral( "xMax" ) ).toDouble() ); + mBounds.setYMinimum( element.attribute( QStringLiteral( "yMin" ) ).toDouble() ); + mBounds.setYMaximum( element.attribute( QStringLiteral( "yMax" ) ).toDouble() ); + + const Qgis::PictureFormat format = qgsEnumKeyToValue( element.attribute( QStringLiteral( "format" ) ), Qgis::PictureFormat::Unknown ); + setPath( format, element.attribute( QStringLiteral( "path" ) ) ); + + mSizeMode = qgsEnumKeyToValue( element.attribute( QStringLiteral( "sizeMode" ) ), Qgis::AnnotationPictureSizeMode::SpatialBounds ); + + mFixedSize = QSizeF( + element.attribute( QStringLiteral( "fixedWidth" ) ).toDouble(), + element.attribute( QStringLiteral( "fixedHeight" ) ).toDouble() + ); + mFixedSizeUnit = QgsUnitTypes::decodeRenderUnit( element.attribute( QStringLiteral( "fixedSizeUnit" ) ) ); + + mDrawBackground = element.attribute( QStringLiteral( "backgroundEnabled" ), QStringLiteral( "0" ) ).toInt(); + const QDomElement backgroundSymbolElem = element.firstChildElement( QStringLiteral( "backgroundSymbol" ) ).firstChildElement(); + if ( !backgroundSymbolElem.isNull() ) + { + setBackgroundSymbol( QgsSymbolLayerUtils::loadSymbol< QgsFillSymbol >( backgroundSymbolElem, context ) ); + } + + mDrawFrame = element.attribute( QStringLiteral( "frameEnabled" ), QStringLiteral( "0" ) ).toInt(); + const QDomElement frameSymbolElem = element.firstChildElement( QStringLiteral( "frameSymbol" ) ).firstChildElement(); + if ( !frameSymbolElem.isNull() ) + { + setFrameSymbol( QgsSymbolLayerUtils::loadSymbol< QgsFillSymbol >( frameSymbolElem, context ) ); + } + + readCommonProperties( element, context ); + return true; +} + +QgsAnnotationPictureItem *QgsAnnotationPictureItem::clone() const +{ + std::unique_ptr< QgsAnnotationPictureItem > item = std::make_unique< QgsAnnotationPictureItem >( mFormat, mPath, mBounds ); + item->setLockAspectRatio( mLockAspectRatio ); + item->setSizeMode( mSizeMode ); + item->setFixedSize( mFixedSize ); + item->setFixedSizeUnit( mFixedSizeUnit ); + + item->setBackgroundEnabled( mDrawBackground ); + if ( mBackgroundSymbol ) + item->setBackgroundSymbol( mBackgroundSymbol->clone() ); + + item->setFrameEnabled( mDrawFrame ); + if ( mFrameSymbol ) + item->setFrameSymbol( mFrameSymbol->clone() ); + + item->copyCommonProperties( this ); + return item.release(); +} + +QgsRectangle QgsAnnotationPictureItem::boundingBox() const +{ + switch ( mSizeMode ) + { + case Qgis::AnnotationPictureSizeMode::SpatialBounds: + return mBounds; + + case Qgis::AnnotationPictureSizeMode::FixedSize: + return QgsRectangle( mBounds.center(), mBounds.center() ); + } + BUILTIN_UNREACHABLE +} + +QgsRectangle QgsAnnotationPictureItem::boundingBox( QgsRenderContext &context ) const +{ + switch ( mSizeMode ) + { + case Qgis::AnnotationPictureSizeMode::SpatialBounds: + return QgsAnnotationPictureItem::boundingBox(); + + case Qgis::AnnotationPictureSizeMode::FixedSize: + { + QPointF center = mBounds.center().toQPointF(); + if ( context.coordinateTransform().isValid() ) + { + double x = center.x(); + double y = center.y(); + double z = 0.0; + context.coordinateTransform().transformInPlace( x, y, z ); + center = QPointF( x, y ); + } + + context.mapToPixel().transformInPlace( center.rx(), center.ry() ); + + const double widthPixels = context.convertToPainterUnits( mFixedSize.width(), mFixedSizeUnit ); + const double heightPixels = context.convertToPainterUnits( mFixedSize.height(), mFixedSizeUnit ); + + const QRectF boundsInPixels( center.x() - widthPixels * 0.5, + center.y() - heightPixels * 0.5, + widthPixels, heightPixels ); + const QgsPointXY topLeft = context.mapToPixel().toMapCoordinates( boundsInPixels.left(), boundsInPixels.top() ); + const QgsPointXY topRight = context.mapToPixel().toMapCoordinates( boundsInPixels.right(), boundsInPixels.top() ); + const QgsPointXY bottomLeft = context.mapToPixel().toMapCoordinates( boundsInPixels.left(), boundsInPixels.bottom() ); + const QgsPointXY bottomRight = context.mapToPixel().toMapCoordinates( boundsInPixels.right(), boundsInPixels.bottom() ); + + const QgsRectangle boundsMapUnits = QgsRectangle( topLeft.x(), bottomLeft.y(), bottomRight.x(), topRight.y() ); + return context.coordinateTransform().transformBoundingBox( boundsMapUnits, Qgis::TransformDirection::Reverse ); + } + } + BUILTIN_UNREACHABLE +} + +void QgsAnnotationPictureItem::setBounds( const QgsRectangle &bounds ) +{ + mBounds = bounds; +} + +void QgsAnnotationPictureItem::setPath( Qgis::PictureFormat format, const QString &path ) +{ + mPath = path; + mFormat = format; + + if ( path.isEmpty() ) + { + mFormat = Qgis::PictureFormat::Unknown; + return; + } +} + +bool QgsAnnotationPictureItem::lockAspectRatio() const +{ + return mLockAspectRatio; +} + +void QgsAnnotationPictureItem::setLockAspectRatio( bool locked ) +{ + mLockAspectRatio = locked; +} + +const QgsFillSymbol *QgsAnnotationPictureItem::backgroundSymbol() const +{ + return mBackgroundSymbol.get(); +} + +void QgsAnnotationPictureItem::setBackgroundSymbol( QgsFillSymbol *symbol ) +{ + mBackgroundSymbol.reset( symbol ); +} + +const QgsFillSymbol *QgsAnnotationPictureItem::frameSymbol() const +{ + return mFrameSymbol.get(); +} + +void QgsAnnotationPictureItem::setFrameSymbol( QgsFillSymbol *symbol ) +{ + mFrameSymbol.reset( symbol ); +} + +QSizeF QgsAnnotationPictureItem::fixedSize() const +{ + return mFixedSize; +} + +void QgsAnnotationPictureItem::setFixedSize( const QSizeF &size ) +{ + mFixedSize = size; +} + +Qgis::RenderUnit QgsAnnotationPictureItem::fixedSizeUnit() const +{ + return mFixedSizeUnit; +} + +void QgsAnnotationPictureItem::setFixedSizeUnit( Qgis::RenderUnit unit ) +{ + mFixedSizeUnit = unit; +} + +Qgis::AnnotationPictureSizeMode QgsAnnotationPictureItem::sizeMode() const +{ + return mSizeMode; +} + +void QgsAnnotationPictureItem::setSizeMode( Qgis::AnnotationPictureSizeMode mode ) +{ + mSizeMode = mode; +} diff --git a/src/core/annotations/qgsannotationpictureitem.h b/src/core/annotations/qgsannotationpictureitem.h new file mode 100644 index 000000000000..280833ab00c5 --- /dev/null +++ b/src/core/annotations/qgsannotationpictureitem.h @@ -0,0 +1,257 @@ +/*************************************************************************** + qgsannotationpictureitem.h + ---------------- + begin : July 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 QGSANNOTATIONPICTUREITEM_H +#define QGSANNOTATIONPICTUREITEM_H + +#include "qgis_core.h" +#include "qgis_sip.h" +#include "qgsannotationitem.h" + +/** + * \ingroup core + * \brief An annotation item which renders a picture. + * + * \since QGIS 3.40 + */ +class CORE_EXPORT QgsAnnotationPictureItem : public QgsAnnotationItem +{ + public: + + /** + * Constructor for QgsAnnotationPictureItem, rendering the specified image \a path + * within the specified \a bounds geometry. + */ + QgsAnnotationPictureItem( Qgis::PictureFormat format, const QString &path, const QgsRectangle &bounds ); + ~QgsAnnotationPictureItem() override; + + QString type() const override; + Qgis::AnnotationItemFlags flags() const override; + void render( QgsRenderContext &context, QgsFeedback *feedback ) override; + bool writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const override; + QList< QgsAnnotationItemNode > nodesV2( const QgsAnnotationItemEditContext &context ) const override; + Qgis::AnnotationItemEditOperationResult applyEditV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ) override; + QgsAnnotationItemEditOperationTransientResults *transientEditResultsV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ) override SIP_FACTORY; + + /** + * Creates a new polygon annotation item. + */ + static QgsAnnotationPictureItem *create() SIP_FACTORY; + + bool readXml( const QDomElement &element, const QgsReadWriteContext &context ) override; + QgsAnnotationPictureItem *clone() const override SIP_FACTORY; + QgsRectangle boundingBox() const override; + QgsRectangle boundingBox( QgsRenderContext &context ) const override; + + /** + * Returns the bounds of the picture. + * + * The coordinate reference system for the bounds will be the parent layer's QgsAnnotationLayer::crs(). + * + * When the sizeMode() is Qgis::AnnotationPictureSizeMode::FixedSize then the picture will be placed + * at the center of the bounds. + * + * \see setBounds() + */ + QgsRectangle bounds() const { return mBounds; } + + /** + * Sets the \a bounds of the picture. + * + * The coordinate reference system for the bounds will be the parent layer's QgsAnnotationLayer::crs(). + * + * When the sizeMode() is Qgis::AnnotationPictureSizeMode::FixedSize then the picture will be placed + * at the center of the bounds. + * + * \see bounds() + */ + void setBounds( const QgsRectangle &bounds ); + + /** + * Returns the path of the image used to render the item. + * + * \see setPath() + */ + QString path() const { return mPath; } + + /** + * Returns the picture format. + */ + Qgis::PictureFormat format() const { return mFormat; } + + /** + * Sets the \a format and \a path of the image used to render the item. + * + * \see path() + * \see format() + */ + void setPath( Qgis::PictureFormat format, const QString &path ); + + /** + * Returns the size mode for the picture. + * + * \see setSizeMode() + */ + Qgis::AnnotationPictureSizeMode sizeMode() const; + + /** + * Sets the size \a mode for the picture. + * + * \see sizeMode() + */ + void setSizeMode( Qgis::AnnotationPictureSizeMode mode ); + + /** + * Returns the fixed size to use for the picture, when the sizeMode() is Qgis::AnnotationPictureSizeMode::FixedSize. + * + * Units are retrieved via fixedSizeUnit() + * + * \see setFixedSize() + * \see fixedSizeUnit() + */ + QSizeF fixedSize() const; + + /** + * Sets the fixed \a size to use for the picture, when the sizeMode() is Qgis::AnnotationPictureSizeMode::FixedSize. + * + * Units are set via setFixedSizeUnit() + * + * \see fixedSize() + * \see setFixedSizeUnit() + */ + void setFixedSize( const QSizeF &size ); + + /** + * Returns the units to use for fixed picture sizes, when the sizeMode() is Qgis::AnnotationPictureSizeMode::FixedSize. + * + * \see setFixedSizeUnit() + * \see fixedSize() + */ + Qgis::RenderUnit fixedSizeUnit() const; + + /** + * Sets the \a unit to use for fixed picture sizes, when the sizeMode() is Qgis::AnnotationPictureSizeMode::FixedSize. + * + * \see fixedSizeUnit() + * \see setFixedSize() + */ + void setFixedSizeUnit( Qgis::RenderUnit unit ); + + /** + * Returns TRUE if the aspect ratio of the picture will be retained. + * + * \see setLockAspectRatio() + */ + bool lockAspectRatio() const; + + /** + * Sets whether the aspect ratio of the picture will be retained. + * + * \see lockAspectRatio() + */ + void setLockAspectRatio( bool locked ); + + /** + * Returns TRUE if the item's background should be rendered. + * + * \see setBackgroundEnabled() + * \see backgroundSymbol() + */ + bool backgroundEnabled() const { return mDrawBackground; } + + /** + * Sets whether the item's background should be rendered. + * + * \see backgroundEnabled() + * \see setBackgroundSymbol() + */ + void setBackgroundEnabled( bool enabled ) { mDrawBackground = enabled; } + + /** + * Returns the symbol used to render the item's background. + * + * \see backgroundEnabled() + * \see setBackgroundSymbol() + */ + const QgsFillSymbol *backgroundSymbol() const; + + /** + * Sets the \a symbol used to render the item's background. + * + * The item takes ownership of the symbol. + * + * \see backgroundSymbol() + * \see setBackgroundEnabled() + */ + void setBackgroundSymbol( QgsFillSymbol *symbol SIP_TRANSFER ); + + /** + * Returns TRUE if the item's frame should be rendered. + * + * \see setFrameEnabled() + * \see frameSymbol() + */ + bool frameEnabled() const { return mDrawFrame; } + + /** + * Sets whether the item's frame should be rendered. + * + * \see frameEnabled() + * \see setFrameSymbol() + */ + void setFrameEnabled( bool enabled ) { mDrawFrame = enabled; } + + /** + * Returns the symbol used to render the item's frame. + * + * \see frameEnabled() + * \see setFrameSymbol() + */ + const QgsFillSymbol *frameSymbol() const; + + /** + * Sets the \a symbol used to render the item's frame. + * + * The item takes ownership of the symbol. + * + * \see frameSymbol() + * \see setBackgroundEnabled() + */ + void setFrameSymbol( QgsFillSymbol *symbol SIP_TRANSFER ); + + private: + + QString mPath; + Qgis::PictureFormat mFormat = Qgis::PictureFormat::Unknown; + Qgis::AnnotationPictureSizeMode mSizeMode = Qgis::AnnotationPictureSizeMode::SpatialBounds; + QgsRectangle mBounds; + + QSizeF mFixedSize; + Qgis::RenderUnit mFixedSizeUnit = Qgis::RenderUnit::Millimeters; + + bool mLockAspectRatio = true; + bool mDrawBackground = false; + std::unique_ptr< QgsFillSymbol > mBackgroundSymbol; + bool mDrawFrame = false; + std::unique_ptr< QgsFillSymbol > mFrameSymbol; + +#ifdef SIP_RUN + QgsAnnotationPictureItem( const QgsAnnotationPictureItem &other ); +#endif + +}; +#endif // QGSANNOTATIONPICTUREITEM_H diff --git a/src/core/annotations/qgsannotationpointtextitem.cpp b/src/core/annotations/qgsannotationpointtextitem.cpp index 8a30ac8772af..ae1e50e5ce1b 100644 --- a/src/core/annotations/qgsannotationpointtextitem.cpp +++ b/src/core/annotations/qgsannotationpointtextitem.cpp @@ -32,7 +32,8 @@ QgsAnnotationPointTextItem::QgsAnnotationPointTextItem( const QString &text, Qgs Qgis::AnnotationItemFlags QgsAnnotationPointTextItem::flags() const { // in truth this should depend on whether the text format is scale dependent or not! - return Qgis::AnnotationItemFlag::ScaleDependentBoundingBox; + return Qgis::AnnotationItemFlag::ScaleDependentBoundingBox + | Qgis::AnnotationItemFlag::SupportsReferenceScale; } QgsAnnotationPointTextItem::~QgsAnnotationPointTextItem() = default; diff --git a/src/core/annotations/qgsannotationpolygonitem.cpp b/src/core/annotations/qgsannotationpolygonitem.cpp index bc5085a1b341..b401dbb5c286 100644 --- a/src/core/annotations/qgsannotationpolygonitem.cpp +++ b/src/core/annotations/qgsannotationpolygonitem.cpp @@ -199,6 +199,11 @@ QgsAnnotationItemEditOperationTransientResults *QgsAnnotationPolygonItem::transi return nullptr; } +Qgis::AnnotationItemFlags QgsAnnotationPolygonItem::flags() const +{ + return Qgis::AnnotationItemFlag::SupportsReferenceScale; +} + QgsAnnotationPolygonItem *QgsAnnotationPolygonItem::create() { return new QgsAnnotationPolygonItem( new QgsPolygon() ); diff --git a/src/core/annotations/qgsannotationpolygonitem.h b/src/core/annotations/qgsannotationpolygonitem.h index f7194f4e2ef4..61088ced95bb 100644 --- a/src/core/annotations/qgsannotationpolygonitem.h +++ b/src/core/annotations/qgsannotationpolygonitem.h @@ -46,6 +46,7 @@ class CORE_EXPORT QgsAnnotationPolygonItem : public QgsAnnotationItem QList< QgsAnnotationItemNode > nodesV2( const QgsAnnotationItemEditContext &context ) const override; Qgis::AnnotationItemEditOperationResult applyEditV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ) override; QgsAnnotationItemEditOperationTransientResults *transientEditResultsV2( QgsAbstractAnnotationItemEditOperation *operation, const QgsAnnotationItemEditContext &context ) override SIP_FACTORY; + Qgis::AnnotationItemFlags flags() const override; /** * Creates a new polygon annotation item. diff --git a/src/core/qgis.h b/src/core/qgis.h index bbaa69378eff..92ca29bc6354 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -2178,12 +2178,25 @@ class CORE_EXPORT Qgis enum class AnnotationItemFlag : int SIP_ENUM_BASETYPE( IntFlag ) { ScaleDependentBoundingBox = 1 << 0, //!< Item's bounding box will vary depending on map scale + SupportsReferenceScale = 1 << 1, //!< Item supports reference scale based rendering (since QGIS 3.40) }; //! Annotation item flags Q_DECLARE_FLAGS( AnnotationItemFlags, AnnotationItemFlag ) Q_ENUM( AnnotationItemFlag ) Q_FLAG( AnnotationItemFlags ) + /** + * Picture annotation item size modes. + * + * \since QGIS 3.40 + */ + enum class AnnotationPictureSizeMode : int + { + SpatialBounds = 0, //!< Picture is rendered inside spatial bounds, and size will depend on map scale + FixedSize, //!< Picture is rendered at a fixed size, regardless of map scale + }; + Q_ENUM( AnnotationPictureSizeMode ) + /** * Flags for controlling how an annotation item behaves in the GUI. * @@ -4652,6 +4665,7 @@ class CORE_EXPORT Qgis }; Q_ENUM( PictureFormat ) + /** * Scalebar alignment. * diff --git a/src/gui/annotations/qgsannotationitemcommonpropertieswidget.cpp b/src/gui/annotations/qgsannotationitemcommonpropertieswidget.cpp index 01c7e7429bcb..68fdc3befab2 100644 --- a/src/gui/annotations/qgsannotationitemcommonpropertieswidget.cpp +++ b/src/gui/annotations/qgsannotationitemcommonpropertieswidget.cpp @@ -45,6 +45,7 @@ void QgsAnnotationItemCommonPropertiesWidget::setItem( QgsAnnotationItem *item ) mSpinZIndex->setValue( item->zIndex() ); mReferenceScaleGroup->setChecked( item->useSymbologyReferenceScale() ); mReferenceScaleWidget->setScale( item->symbologyReferenceScale() ); + mReferenceScaleGroup->setVisible( item->flags() & Qgis::AnnotationItemFlag::SupportsReferenceScale ); } void QgsAnnotationItemCommonPropertiesWidget::updateItem( QgsAnnotationItem *item ) diff --git a/src/gui/annotations/qgsannotationitemguiregistry.cpp b/src/gui/annotations/qgsannotationitemguiregistry.cpp index d3a5cb2c9db7..1516e610bea7 100644 --- a/src/gui/annotations/qgsannotationitemguiregistry.cpp +++ b/src/gui/annotations/qgsannotationitemguiregistry.cpp @@ -20,6 +20,9 @@ #include "qgsannotationitemwidget_impl.h" #include "qgscreateannotationitemmaptool_impl.h" + +#include + // // QgsAnnotationItemAbstractGuiMetadata // @@ -247,4 +250,19 @@ void QgsAnnotationItemGuiRegistry::addDefaultItems() { return new QgsCreateLineTextItemMapTool( canvas, cadDockWidget ); } ) ); + + + addAnnotationItemGuiMetadata( new QgsAnnotationItemGuiMetadata( QStringLiteral( "picture" ), + QObject::tr( "Picture" ), + QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddImage.svg" ) ), + [ = ]( QgsAnnotationItem * item )->QgsAnnotationItemBaseWidget * + { + QgsAnnotationPictureItemWidget *widget = new QgsAnnotationPictureItemWidget( nullptr ); + widget->setItem( item ); + return widget; + }, QString(), Qgis::AnnotationItemGuiFlags(), nullptr, + [ = ]( QgsMapCanvas * canvas, QgsAdvancedDigitizingDockWidget * cadDockWidget )->QgsCreateAnnotationItemMapToolInterface * + { + return new QgsCreatePictureItemMapTool( canvas, cadDockWidget ); + } ) ); } diff --git a/src/gui/annotations/qgsannotationitemwidget_impl.cpp b/src/gui/annotations/qgsannotationitemwidget_impl.cpp index 24a215df90ae..54e07f5c5098 100644 --- a/src/gui/annotations/qgsannotationitemwidget_impl.cpp +++ b/src/gui/annotations/qgsannotationitemwidget_impl.cpp @@ -24,11 +24,14 @@ #include "qgsannotationmarkeritem.h" #include "qgsannotationpointtextitem.h" #include "qgsannotationlinetextitem.h" +#include "qgsannotationpictureitem.h" #include "qgsexpressionbuilderdialog.h" #include "qgstextformatwidget.h" #include "qgsapplication.h" #include "qgsrecentstylehandler.h" #include "qgsexpressionfinder.h" +#include "qgsimagecache.h" +#include "qgssvgcache.h" ///@cond PRIVATE @@ -606,5 +609,270 @@ void QgsAnnotationLineTextItemWidget::mInsertExpressionButton_clicked() } } + +// +// QgsAnnotationPictureItemWidget +// + +QgsAnnotationPictureItemWidget::QgsAnnotationPictureItemWidget( QWidget *parent ) + : QgsAnnotationItemBaseWidget( parent ) +{ + setupUi( this ); + + mSizeStackedWidget->setSizeMode( QgsStackedWidget::SizeMode::CurrentPageOnly ); + + mSizeModeCombo->addItem( tr( "Scale Dependent Size" ), QVariant::fromValue( Qgis::AnnotationPictureSizeMode::SpatialBounds ) ); + mSizeModeCombo->addItem( tr( "Fixed Size" ), QVariant::fromValue( Qgis::AnnotationPictureSizeMode::FixedSize ) ); + + mSizeUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << Qgis::RenderUnit::Pixels << Qgis::RenderUnit::Millimeters + << Qgis::RenderUnit::Points << Qgis::RenderUnit::Inches << Qgis::RenderUnit::Percentage ); + + mBackgroundSymbolButton->setSymbolType( Qgis::SymbolType::Fill ); + mBackgroundSymbolButton->setDialogTitle( tr( "Background" ) ); + mBackgroundSymbolButton->registerExpressionContextGenerator( this ); + mFrameSymbolButton->setSymbolType( Qgis::SymbolType::Fill ); + mFrameSymbolButton->setDialogTitle( tr( "Frame" ) ); + mFrameSymbolButton->registerExpressionContextGenerator( this ); + + connect( mPropertiesWidget, &QgsAnnotationItemCommonPropertiesWidget::itemChanged, this, [ = ] + { + if ( !mBlockChangedSignal ) + emit itemChanged(); + } ); + + connect( mSizeModeCombo, qOverload( &QComboBox::currentIndexChanged ), this, &QgsAnnotationPictureItemWidget::sizeModeChanged ); + + connect( mRadioSVG, &QRadioButton::toggled, this, &QgsAnnotationPictureItemWidget::modeChanged ); + connect( mRadioRaster, &QRadioButton::toggled, this, &QgsAnnotationPictureItemWidget::modeChanged ); + connect( mSourceLineEdit, &QgsPictureSourceLineEditBase::sourceChanged, this, [ = ]( const QString & source ) + { + if ( !mRadioSVG->isChecked() && QFileInfo( source ).suffix().compare( QLatin1String( "svg" ), Qt::CaseInsensitive ) == 0 ) + { + mRadioSVG->setChecked( true ); + } + + onWidgetChanged(); + } ); + + connect( mLockAspectRatioCheck, &QCheckBox::toggled, this, &QgsAnnotationPictureItemWidget::onWidgetChanged ); + connect( mFrameCheckbox, &QGroupBox::toggled, this, &QgsAnnotationPictureItemWidget::onWidgetChanged ); + connect( mBackgroundCheckbox, &QGroupBox::toggled, this, &QgsAnnotationPictureItemWidget::onWidgetChanged ); + connect( mBackgroundSymbolButton, &QgsSymbolButton::changed, this, &QgsAnnotationPictureItemWidget::onWidgetChanged ); + connect( mFrameSymbolButton, &QgsSymbolButton::changed, this, &QgsAnnotationPictureItemWidget::onWidgetChanged ); + connect( mSizeUnitWidget, &QgsUnitSelectionWidget::changed, this, &QgsAnnotationPictureItemWidget::onWidgetChanged ); + + connect( mWidthSpinBox, qOverload< double >( &QDoubleSpinBox::valueChanged ), this, &QgsAnnotationPictureItemWidget::setWidth ); + connect( mHeightSpinBox, qOverload< double >( &QDoubleSpinBox::valueChanged ), this, &QgsAnnotationPictureItemWidget::setHeight ); + connect( mLockAspectRatio, &QgsRatioLockButton::lockChanged, this, &QgsAnnotationPictureItemWidget::setLockAspectRatio ); +} + +QgsAnnotationPictureItemWidget::~QgsAnnotationPictureItemWidget() = default; + +QgsAnnotationItem *QgsAnnotationPictureItemWidget::createItem() +{ + QgsAnnotationPictureItem *newItem = mItem->clone(); + updateItem( newItem ); + return newItem; +} + +void QgsAnnotationPictureItemWidget::updateItem( QgsAnnotationItem *item ) +{ + if ( QgsAnnotationPictureItem *pictureItem = dynamic_cast< QgsAnnotationPictureItem * >( item ) ) + { + const bool svg = mRadioSVG->isChecked(); + const Qgis::PictureFormat newFormat = svg ? Qgis::PictureFormat::SVG : Qgis::PictureFormat::Raster; + const QString path = mSourceLineEdit->source(); + pictureItem->setPath( newFormat, path ); + + pictureItem->setSizeMode( mSizeModeCombo->currentData().value< Qgis::AnnotationPictureSizeMode >() ); + switch ( pictureItem->sizeMode() ) + { + case Qgis::AnnotationPictureSizeMode::SpatialBounds: + pictureItem->setLockAspectRatio( mLockAspectRatioCheck->isChecked() ); + break; + case Qgis::AnnotationPictureSizeMode::FixedSize: + pictureItem->setLockAspectRatio( mLockAspectRatio->isChecked() ); + break; + } + + pictureItem->setFixedSize( QSizeF( mWidthSpinBox->value(), mHeightSpinBox->value() ) ); + pictureItem->setFixedSizeUnit( mSizeUnitWidget->unit() ); + + pictureItem->setBackgroundEnabled( mBackgroundCheckbox->isChecked() ); + pictureItem->setFrameEnabled( mFrameCheckbox->isChecked() ); + pictureItem->setBackgroundSymbol( mBackgroundSymbolButton->clonedSymbol< QgsFillSymbol >() ); + pictureItem->setFrameSymbol( mFrameSymbolButton->clonedSymbol< QgsFillSymbol >() ); + + mPropertiesWidget->updateItem( pictureItem ); + } +} + +void QgsAnnotationPictureItemWidget::setDockMode( bool dockMode ) +{ + QgsAnnotationItemBaseWidget::setDockMode( dockMode ); +} + +void QgsAnnotationPictureItemWidget::setContext( const QgsSymbolWidgetContext &context ) +{ + QgsAnnotationItemBaseWidget::setContext( context ); + mPropertiesWidget->setContext( context ); + mBackgroundSymbolButton->setMapCanvas( context.mapCanvas() ); + mBackgroundSymbolButton->setMessageBar( context.messageBar() ); + mFrameSymbolButton->setMapCanvas( context.mapCanvas() ); + mFrameSymbolButton->setMessageBar( context.messageBar() ); +} + +QgsExpressionContext QgsAnnotationPictureItemWidget::createExpressionContext() const +{ + QgsExpressionContext expressionContext; + if ( context().expressionContext() ) + expressionContext = *( context().expressionContext() ); + else + expressionContext = QgsProject::instance()->createExpressionContext(); + return expressionContext; +} + +void QgsAnnotationPictureItemWidget::focusDefaultWidget() +{ + mSourceLineEdit->setFocus(); +} + +bool QgsAnnotationPictureItemWidget::setNewItem( QgsAnnotationItem *item ) +{ + QgsAnnotationPictureItem *pictureItem = dynamic_cast< QgsAnnotationPictureItem * >( item ); + if ( !pictureItem ) + return false; + + mItem.reset( pictureItem->clone() ); + + mBlockChangedSignal = true; + mPropertiesWidget->setItem( mItem.get() ); + + mLockAspectRatioCheck->setChecked( mItem->lockAspectRatio() ); + mLockAspectRatio->setLocked( mItem->lockAspectRatio() ); + switch ( pictureItem->format() ) + { + case Qgis::PictureFormat::SVG: + mRadioSVG->setChecked( true ); + break; + case Qgis::PictureFormat::Raster: + mRadioRaster->setChecked( true ); + break; + case Qgis::PictureFormat::Unknown: + break; + } + + mSourceLineEdit->setSource( pictureItem->path() ); + + mBackgroundCheckbox->setChecked( pictureItem->backgroundEnabled() ); + if ( const QgsSymbol *symbol = pictureItem->backgroundSymbol() ) + mBackgroundSymbolButton->setSymbol( symbol->clone() ); + + mFrameCheckbox->setChecked( pictureItem->frameEnabled() ); + if ( const QgsSymbol *symbol = pictureItem->frameSymbol() ) + mFrameSymbolButton->setSymbol( symbol->clone() ); + + mWidthSpinBox->setValue( pictureItem->fixedSize().width() ); + mHeightSpinBox->setValue( pictureItem->fixedSize().height() ); + mSizeModeCombo->setCurrentIndex( mSizeModeCombo->findData( QVariant::fromValue( pictureItem->sizeMode() ) ) ); + sizeModeChanged(); + + mBlockChangedSignal = false; + + return true; +} + +void QgsAnnotationPictureItemWidget::onWidgetChanged() +{ + if ( !mBlockChangedSignal ) + emit itemChanged(); +} + +void QgsAnnotationPictureItemWidget::modeChanged( bool checked ) +{ + if ( !checked ) + return; + + const bool svg = mRadioSVG->isChecked(); + + if ( svg ) + mSourceLineEdit->setMode( QgsPictureSourceLineEditBase::Svg ); + else + mSourceLineEdit->setMode( QgsPictureSourceLineEditBase::Image ); + + onWidgetChanged(); +} + +void QgsAnnotationPictureItemWidget::sizeModeChanged() +{ + const Qgis::AnnotationPictureSizeMode mode = mSizeModeCombo->currentData().value< Qgis::AnnotationPictureSizeMode >(); + switch ( mode ) + { + case Qgis::AnnotationPictureSizeMode::SpatialBounds: + mSizeStackedWidget->setCurrentWidget( mPageSpatialBounds ); + break; + case Qgis::AnnotationPictureSizeMode::FixedSize: + mSizeStackedWidget->setCurrentWidget( mPageFixedSize ); + break; + } + + onWidgetChanged(); +} + +void QgsAnnotationPictureItemWidget::setWidth() +{ + if ( mLockAspectRatio->locked() ) + { + const double ratio = pictureAspectRatio(); + if ( ratio > 0 ) + whileBlocking( mHeightSpinBox )->setValue( mWidthSpinBox->value() * ratio ); + } + + onWidgetChanged(); +} + +void QgsAnnotationPictureItemWidget::setHeight() +{ + if ( mLockAspectRatio->locked() ) + { + const double ratio = pictureAspectRatio(); + if ( ratio > 0 ) + whileBlocking( mWidthSpinBox )->setValue( mHeightSpinBox->value() / ratio ); + } + + onWidgetChanged(); +} + +void QgsAnnotationPictureItemWidget::setLockAspectRatio( bool locked ) +{ + if ( locked && !mBlockChangedSignal ) + { + const double ratio = pictureAspectRatio(); + if ( ratio > 0 ) + whileBlocking( mHeightSpinBox )->setValue( mWidthSpinBox->value() * ratio ); + } + + onWidgetChanged(); +} + +double QgsAnnotationPictureItemWidget::pictureAspectRatio() const +{ + const bool svg = mRadioSVG->isChecked(); + const QString path = mSourceLineEdit->source(); + QSizeF size; + if ( svg ) + { + size = QgsApplication::svgCache()->svgViewboxSize( path, 100, QColor(), QColor(), 1, 1 ); + } + else + { + size = QgsApplication::imageCache()->originalSize( path ); + } + if ( size.isValid() && size.width() > 0 ) + return size.height() / size.width(); + + return 0; +} + ///@endcond PRIVATE diff --git a/src/gui/annotations/qgsannotationitemwidget_impl.h b/src/gui/annotations/qgsannotationitemwidget_impl.h index 0de36dd507f8..44a9bcbee9c8 100644 --- a/src/gui/annotations/qgsannotationitemwidget_impl.h +++ b/src/gui/annotations/qgsannotationitemwidget_impl.h @@ -19,11 +19,13 @@ #include "qgis_sip.h" #include "qgis_gui.h" #include "qgstextformat.h" +#include "qgsexpressioncontextgenerator.h" #include #include "ui_qgsannotationpointtextwidgetbase.h" #include "ui_qgsannotationsymbolwidgetbase.h" #include "ui_qgsannotationlinetextwidgetbase.h" +#include "ui_qgsannotationpicturewidgetbase.h" class QgsSymbolSelectorWidget; class QgsFillSymbol; @@ -34,6 +36,7 @@ class QgsAnnotationLineItem; class QgsAnnotationMarkerItem; class QgsAnnotationPointTextItem; class QgsAnnotationLineTextItem; +class QgsAnnotationPictureItem; class QgsTextFormatWidget; #define SIP_NO_FILE @@ -166,6 +169,41 @@ class QgsAnnotationLineTextItemWidget : public QgsAnnotationItemBaseWidget, priv }; +class QgsAnnotationPictureItemWidget : public QgsAnnotationItemBaseWidget, private Ui_QgsAnnotationPictureWidgetBase, private QgsExpressionContextGenerator +{ + Q_OBJECT + + public: + QgsAnnotationPictureItemWidget( QWidget *parent ); + ~QgsAnnotationPictureItemWidget() override; + QgsAnnotationItem *createItem() override; + void updateItem( QgsAnnotationItem *item ) override; + void setDockMode( bool dockMode ) override; + void setContext( const QgsSymbolWidgetContext &context ) override; + QgsExpressionContext createExpressionContext() const override; + public slots: + + void focusDefaultWidget() override; + + protected: + bool setNewItem( QgsAnnotationItem *item ) override; + + private slots: + + void onWidgetChanged(); + + void modeChanged( bool checked ); + void sizeModeChanged(); + void setWidth(); + void setHeight(); + void setLockAspectRatio( bool locked ); + private: + double pictureAspectRatio() const; + + bool mBlockChangedSignal = false; + std::unique_ptr< QgsAnnotationPictureItem> mItem; +}; + ///@endcond #endif // QGSANNOTATIONITEMWIDGETIMPL_H diff --git a/src/gui/annotations/qgscreateannotationitemmaptool.h b/src/gui/annotations/qgscreateannotationitemmaptool.h index 79e62a6b8ba0..9fe5c618edad 100644 --- a/src/gui/annotations/qgscreateannotationitemmaptool.h +++ b/src/gui/annotations/qgscreateannotationitemmaptool.h @@ -18,6 +18,7 @@ #include "qgis_gui.h" #include "qgis_sip.h" #include "qgsmaptooladvanceddigitizing.h" +#include "qgssettingstree.h" class QgsAnnotationItem; class QgsAnnotationLayer; @@ -103,6 +104,10 @@ class GUI_EXPORT QgsCreateAnnotationItemMapToolInterface { public: +#ifndef SIP_RUN + static inline QgsSettingsTreeNode *sTreeAnnotationTools = QgsSettingsTree::sTreeGui->createChildNode( QStringLiteral( "annotation-items" ) ); +#endif + virtual ~QgsCreateAnnotationItemMapToolInterface() = default; /** diff --git a/src/gui/annotations/qgscreateannotationitemmaptool_impl.cpp b/src/gui/annotations/qgscreateannotationitemmaptool_impl.cpp index 59b571b05bf0..e257be841cda 100644 --- a/src/gui/annotations/qgscreateannotationitemmaptool_impl.cpp +++ b/src/gui/annotations/qgscreateannotationitemmaptool_impl.cpp @@ -20,6 +20,7 @@ #include "qgsannotationlineitem.h" #include "qgsannotationpolygonitem.h" #include "qgsannotationlinetextitem.h" +#include "qgsannotationpictureitem.h" #include "qgsannotationlayer.h" #include "qgsstyle.h" #include "qgsmapcanvas.h" @@ -30,6 +31,13 @@ #include "qgsapplication.h" #include "qgsrecentstylehandler.h" #include "qgscurvepolygon.h" +#include "qgsrubberband.h" +#include "qgssettingsregistrycore.h" +#include "qgssvgcache.h" +#include "qgsimagecache.h" + +#include +#include ///@cond PRIVATE @@ -220,6 +228,154 @@ void QgsCreatePolygonItemMapTool::polygonCaptured( const QgsCurvePolygon *polygo } } + +// +// QgsCreatePictureItemMapTool +// + +const QgsSettingsEntryString *QgsCreatePictureItemMapTool::settingLastSourceFolder = new QgsSettingsEntryString( QStringLiteral( "last-source-folder" ), sTreePicture, QString(), QStringLiteral( "Last used folder for picture annotation source files" ) ); + +QgsCreatePictureItemMapTool::QgsCreatePictureItemMapTool( QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWidget *cadDockWidget ) + : QgsMapToolAdvancedDigitizing( canvas, cadDockWidget ) + , mHandler( new QgsCreateAnnotationItemMapToolHandler( canvas, cadDockWidget, this ) ) +{ + setUseSnappingIndicator( true ); +} + +void QgsCreatePictureItemMapTool::cadCanvasPressEvent( QgsMapMouseEvent *event ) +{ + if ( event->button() == Qt::RightButton && mRubberBand ) + { + mRubberBand.reset(); + cadDockWidget()->clearPoints(); + return; + } + + if ( event->button() != Qt::LeftButton ) + return; + + if ( !mRubberBand ) + { + mFirstPoint = event->snapPoint(); + mRect.setRect( mFirstPoint.x(), mFirstPoint.y(), mFirstPoint.x(), mFirstPoint.y() ); + + mRubberBand.reset( new QgsRubberBand( mCanvas, Qgis::GeometryType::Polygon ) ); + mRubberBand->setWidth( digitizingStrokeWidth() ); + QColor color = digitizingStrokeColor(); + + const double alphaScale = QgsSettingsRegistryCore::settingsDigitizingLineColorAlphaScale->value(); + color.setAlphaF( color.alphaF() * alphaScale ); + mRubberBand->setLineStyle( Qt::DotLine ); + mRubberBand->setStrokeColor( color ); + + const QColor fillColor = digitizingFillColor(); + mRubberBand->setFillColor( fillColor ); + } + else + { + mRubberBand.reset(); + + QStringList formatsFilter; + formatsFilter.append( QStringLiteral( "*.svg" ) ); + const QByteArrayList supportedFormats = QImageReader::supportedImageFormats(); + for ( const auto &format : supportedFormats ) + { + formatsFilter.append( QString( QStringLiteral( "*.%1" ) ).arg( QString( format ) ) ); + } + const QString dialogFilter = QStringLiteral( "%1 (%2);;%3 (*.*)" ).arg( tr( "Images" ), formatsFilter.join( QLatin1Char( ' ' ) ), tr( "All files" ) ); + const QString initialDir = settingLastSourceFolder->value(); + const QString imagePath = QFileDialog::getOpenFileName( nullptr, tr( "Add Picture Annotation" ), initialDir.isEmpty() ? QDir::homePath() : initialDir, dialogFilter ); + + if ( imagePath.isEmpty() ) + { + return; //canceled by the user + } + + settingLastSourceFolder->setValue( QFileInfo( imagePath ).path() ); + + const QgsPointXY point1 = toLayerCoordinates( mHandler->targetLayer(), mFirstPoint ); + const QgsPointXY point2 = toLayerCoordinates( mHandler->targetLayer(), event->snapPoint() ); + + const QgsPointXY devicePoint1 = toCanvasCoordinates( mFirstPoint ); + const QgsPointXY devicePoint2 = toCanvasCoordinates( event->snapPoint() ); + const double initialWidthPixels = std::abs( devicePoint1.x() - devicePoint2.x() ); + const double initialHeightPixels = std::abs( devicePoint1.y() - devicePoint2.y() ); + + const QFileInfo pathInfo( imagePath ); + Qgis::PictureFormat format = Qgis::PictureFormat::Unknown; + + QSizeF size; + if ( pathInfo.suffix().compare( QLatin1String( "svg" ), Qt::CaseInsensitive ) == 0 ) + { + format = Qgis::PictureFormat::SVG; + size = QgsApplication::svgCache()->svgViewboxSize( imagePath, 100, QColor(), QColor(), 1, 1 ); + } + else + { + format = Qgis::PictureFormat::Raster; + size = QgsApplication::imageCache()->originalSize( imagePath ); + } + + cadDockWidget()->clearPoints(); + + std::unique_ptr< QgsAnnotationPictureItem > createdItem = std::make_unique< QgsAnnotationPictureItem >( format, imagePath, QgsRectangle( point1, point2 ) ); + if ( size.isValid() ) + { + const double pixelsToMm = mCanvas->mapSettings().outputDpi() / 25.4; + if ( size.width() / size.height() > initialWidthPixels / initialHeightPixels ) + { + createdItem->setFixedSize( QSizeF( initialWidthPixels / pixelsToMm, size.height() / size.width() * initialWidthPixels / pixelsToMm ) ); + } + else + { + createdItem->setFixedSize( QSizeF( size.width() / size.height() * initialHeightPixels / pixelsToMm, initialHeightPixels / pixelsToMm ) ); + } + createdItem->setFixedSizeUnit( Qgis::RenderUnit::Millimeters ); + } + + mHandler->pushCreatedItem( createdItem.release() ); + } +} + +void QgsCreatePictureItemMapTool::cadCanvasMoveEvent( QgsMapMouseEvent *event ) +{ + if ( !mRubberBand ) + return; + + const QgsPointXY mapPoint = event->snapPoint(); + mRect.setBottomRight( mapPoint.toQPointF() ); + + mRubberBand->reset( Qgis::GeometryType::Polygon ); + mRubberBand->addPoint( mRect.bottomLeft(), false ); + mRubberBand->addPoint( mRect.bottomRight(), false ); + mRubberBand->addPoint( mRect.topRight(), false ); + mRubberBand->addPoint( mRect.topLeft(), true ); +} + +void QgsCreatePictureItemMapTool::keyPressEvent( QKeyEvent *event ) +{ + if ( event->key() == Qt::Key_Escape ) + { + if ( mRubberBand ) + { + mRubberBand.reset(); + cadDockWidget()->clearPoints(); + event->ignore(); + } + } +} + +QgsCreateAnnotationItemMapToolHandler *QgsCreatePictureItemMapTool::handler() +{ + return mHandler; +} + +QgsMapTool *QgsCreatePictureItemMapTool::mapTool() +{ + return this; +} + + // // QgsCreateLineTextItemMapTool // diff --git a/src/gui/annotations/qgscreateannotationitemmaptool_impl.h b/src/gui/annotations/qgscreateannotationitemmaptool_impl.h index 2a788c710f75..2283736cd616 100644 --- a/src/gui/annotations/qgscreateannotationitemmaptool_impl.h +++ b/src/gui/annotations/qgscreateannotationitemmaptool_impl.h @@ -19,9 +19,13 @@ #include "qgis_sip.h" #include "qgscreateannotationitemmaptool.h" #include "qgsmaptoolcapture.h" +#include "qgssettingstree.h" #define SIP_NO_FILE +class QgsSettingsEntryString; +class QgsSettingsTreeNode; + ///@cond PRIVATE class QgsMapToolCaptureAnnotationItem: public QgsMapToolCapture, public QgsCreateAnnotationItemMapToolInterface @@ -98,6 +102,34 @@ class QgsCreatePolygonItemMapTool: public QgsMapToolCaptureAnnotationItem }; +class QgsCreatePictureItemMapTool: public QgsMapToolAdvancedDigitizing, public QgsCreateAnnotationItemMapToolInterface +{ + Q_OBJECT + + public: + static inline QgsSettingsTreeNode *sTreePicture = QgsCreateAnnotationItemMapToolInterface::sTreeAnnotationTools->createChildNode( QStringLiteral( "picture-item" ) ); + static const QgsSettingsEntryString *settingLastSourceFolder; + + QgsCreatePictureItemMapTool( QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWidget *cadDockWidget ); + + void cadCanvasPressEvent( QgsMapMouseEvent *event ) override; + void cadCanvasMoveEvent( QgsMapMouseEvent *event ) override; + void keyPressEvent( QKeyEvent *event ) override; + + QgsCreateAnnotationItemMapToolHandler *handler() override; + QgsMapTool *mapTool() override; + + private: + + QgsCreateAnnotationItemMapToolHandler *mHandler = nullptr; + + QRectF mRect; + QgsPointXY mFirstPoint; + QObjectUniquePtr< QgsRubberBand > mRubberBand; +}; + + + class QgsCreateLineTextItemMapTool: public QgsMapToolCaptureAnnotationItem { Q_OBJECT diff --git a/src/ui/annotations/qgsannotationpicturewidgetbase.ui b/src/ui/annotations/qgsannotationpicturewidgetbase.ui new file mode 100644 index 000000000000..0f552e9c798c --- /dev/null +++ b/src/ui/annotations/qgsannotationpicturewidgetbase.ui @@ -0,0 +1,366 @@ + + + QgsAnnotationPictureWidgetBase + + + + 0 + 0 + 321 + 575 + + + + Point Text Annotation + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + Raster image + + + + + + + SVG image + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Background + + + true + + + + + + Symbol + + + + + + + + 0 + 0 + + + + Change… + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 0 + + + + 1 + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Lock aspect ratio + + + + + + + + + 0 + 0 + + + + + + + Width + + + + + + + Unit + + + + + + + + 0 + 0 + + + + 6 + + + 100000.000000000000000 + + + 0.200000000000000 + + + 1.000000000000000 + + + false + + + + + + + + 0 + 0 + + + + 6 + + + 100000.000000000000000 + + + 0.200000000000000 + + + 1.000000000000000 + + + false + + + + + + + Height + + + + + + + + 0 + 0 + + + + Lock aspect ratio + + + 13 + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + + + + + + + + Qt::StrongFocus + + + + + + + Frame + + + true + + + + + + Symbol + + + + + + + + 0 + 0 + + + + Change… + + + + + + + + + + + QgsAnnotationItemCommonPropertiesWidget + QWidget +
qgsannotationitemcommonpropertieswidget.h
+ 1 +
+ + QgsPictureSourceLineEditBase + QWidget +
qgsfilecontentsourcelineedit.h
+ 1 +
+ + QgsSymbolButton + QToolButton +
qgssymbolbutton.h
+
+ + QgsStackedWidget + QStackedWidget +
qgsstackedwidget.h
+ 1 +
+ + QgsRatioLockButton + QToolButton +
qgsratiolockbutton.h
+ 1 +
+ + QgsDoubleSpinBox + QDoubleSpinBox +
qgsdoublespinbox.h
+
+ + QgsUnitSelectionWidget + QWidget +
qgsunitselectionwidget.h
+ 1 +
+
+ + mRadioRaster + mRadioSVG + mSourceLineEdit + mSizeModeCombo + mLockAspectRatioCheck + mWidthSpinBox + mLockAspectRatio + mHeightSpinBox + mSizeUnitWidget + mFrameCheckbox + mFrameSymbolButton + mBackgroundCheckbox + mBackgroundSymbolButton + + + +
diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 7390cb9c9598..fe39fb474905 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -21,6 +21,7 @@ ADD_PYTHON_TEST(PyQgsAnnotationLayer test_qgsannotationlayer.py) ADD_PYTHON_TEST(PyQgsAnnotationLineItem test_qgsannotationlineitem.py) ADD_PYTHON_TEST(PyQgsAnnotationLineTextItem test_qgsannotationlinetextitem.py) ADD_PYTHON_TEST(PyQgsAnnotationMarkerItem test_qgsannotationmarkeritem.py) +ADD_PYTHON_TEST(PyQgsAnnotationPictureItem test_qgsannotationpictureitem.py) ADD_PYTHON_TEST(PyQgsAnnotationPointTextItem test_qgsannotationpointtextitem.py) ADD_PYTHON_TEST(PyQgsAnnotationPolygonItem test_qgsannotationpolygonitem.py) ADD_PYTHON_TEST(PyQgsApplication test_qgsapplication.py) diff --git a/tests/src/python/test_qgsannotationpictureitem.py b/tests/src/python/test_qgsannotationpictureitem.py new file mode 100644 index 000000000000..29c31986857a --- /dev/null +++ b/tests/src/python/test_qgsannotationpictureitem.py @@ -0,0 +1,596 @@ +"""QGIS Unit tests for QgsAnnotationPictureItem. + +From build dir, run: ctest -R QgsAnnotationPictureItem -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. +""" + +from qgis.PyQt.QtCore import QSize, QSizeF +from qgis.PyQt.QtGui import QColor, QImage, QPainter +from qgis.PyQt.QtXml import QDomDocument +from qgis.core import ( + Qgis, + QgsAnnotationItemEditOperationAddNode, + QgsAnnotationItemEditOperationDeleteNode, + QgsAnnotationItemEditOperationMoveNode, + QgsAnnotationItemEditOperationTranslateItem, + QgsAnnotationItemEditContext, + QgsAnnotationItemNode, + QgsAnnotationPictureItem, + QgsCircularString, + QgsCoordinateReferenceSystem, + QgsCoordinateTransform, + QgsCurvePolygon, + QgsFillSymbol, + QgsLineString, + QgsMapSettings, + QgsPoint, + QgsPointXY, + QgsPolygon, + QgsProject, + QgsReadWriteContext, + QgsRectangle, + QgsRenderContext, + QgsVertexId, +) +import unittest +from qgis.testing import start_app, QgisTestCase + +from utilities import unitTestDataPath + +start_app() +TEST_DATA_DIR = unitTestDataPath() + + +class TestQgsAnnotationPictureItem(QgisTestCase): + + @classmethod + def control_path_prefix(cls): + return "annotation_layer" + + def testBasic(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, self.get_test_data_path('rgb256x256.png').as_posix(), + QgsRectangle(10, 20, 30, 40)) + + self.assertEqual(item.path(), self.get_test_data_path('rgb256x256.png').as_posix()) + self.assertEqual(item.format(), Qgis.PictureFormat.Raster) + self.assertEqual(item.boundingBox().toString(3), '10.000,20.000 : 30.000,40.000') + self.assertEqual(item.sizeMode(), Qgis.AnnotationPictureSizeMode.SpatialBounds) + + item.setBounds(QgsRectangle(100, 200, 300, 400)) + item.setZIndex(11) + item.setPath(Qgis.PictureFormat.SVG, self.get_test_data_path('sample_svg.svg').as_posix()) + item.setLockAspectRatio(False) + item.setBackgroundEnabled(True) + item.setFrameEnabled(True) + item.setSizeMode(Qgis.AnnotationPictureSizeMode.FixedSize) + item.setFixedSize(QSizeF(56, + 57)) + item.setFixedSizeUnit(Qgis.RenderUnit.Inches) + self.assertEqual(item.bounds().toString(3), '100.000,200.000 : 300.000,400.000') + self.assertEqual(item.path(), self.get_test_data_path('sample_svg.svg').as_posix()) + self.assertEqual(item.format(), Qgis.PictureFormat.SVG) + self.assertEqual(item.zIndex(), 11) + self.assertFalse(item.lockAspectRatio()) + self.assertTrue(item.backgroundEnabled()) + self.assertTrue(item.frameEnabled()) + self.assertEqual(item.sizeMode(), + Qgis.AnnotationPictureSizeMode.FixedSize) + self.assertEqual(item.fixedSize(), QSizeF(56, + 57)) + self.assertEqual(item.fixedSizeUnit(), Qgis.RenderUnit.Inches) + + item.setBackgroundSymbol(QgsFillSymbol.createSimple({'color': '200,100,100', 'outline_color': 'black'})) + item.setFrameSymbol(QgsFillSymbol.createSimple( + {'color': '100,200,250', 'outline_color': 'black'})) + self.assertEqual(item.backgroundSymbol()[0].color(), QColor(200, 100, 100)) + self.assertEqual(item.frameSymbol()[0].color(), + QColor(100, 200, 250)) + + def test_nodes_spatial_bounds(self): + """ + Test nodes for item, spatial bounds mode + """ + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, self.get_test_data_path('rgb256x256.png').as_posix(), + QgsRectangle(10, 20, 30, 40)) + # nodes shouldn't form a closed ring + self.assertEqual(item.nodesV2(QgsAnnotationItemEditContext()), [ + QgsAnnotationItemNode(QgsVertexId(0, 0, 0), QgsPointXY(10, 20), Qgis.AnnotationItemNodeType.VertexHandle), + QgsAnnotationItemNode(QgsVertexId(0, 0, 1), QgsPointXY(30, 20), Qgis.AnnotationItemNodeType.VertexHandle), + QgsAnnotationItemNode(QgsVertexId(0, 0, 2), QgsPointXY(30, 40), Qgis.AnnotationItemNodeType.VertexHandle), + QgsAnnotationItemNode(QgsVertexId(0, 0, 3), QgsPointXY(10, 40), Qgis.AnnotationItemNodeType.VertexHandle)]) + + def test_nodes_fixed_size(self): + """ + Test nodes for item, fixed size mode + """ + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, self.get_test_data_path('rgb256x256.png').as_posix(), + QgsRectangle(10, 20, 30, 40)) + item.setSizeMode(Qgis.AnnotationPictureSizeMode.FixedSize) + self.assertEqual(item.nodesV2(QgsAnnotationItemEditContext()), [ + QgsAnnotationItemNode(QgsVertexId(0, 0, 0), QgsPointXY(20, 30), Qgis.AnnotationItemNodeType.VertexHandle)]) + + def test_translate_spatial_bounds(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, + self.get_test_data_path( + 'rgb256x256.png').as_posix(), + QgsRectangle(10, 20, 30, 40)) + self.assertEqual(item.bounds().toString(3), '10.000,20.000 : 30.000,40.000') + + self.assertEqual(item.applyEditV2(QgsAnnotationItemEditOperationTranslateItem('', 100, 200), + QgsAnnotationItemEditContext()), + Qgis.AnnotationItemEditOperationResult.Success) + self.assertEqual(item.bounds().toString(3), '110.000,220.000 : 130.000,240.000') + + def test_translate_fixed_size(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, + self.get_test_data_path( + 'rgb256x256.png').as_posix(), + QgsRectangle(10, 20, 30, 40)) + item.setSizeMode(Qgis.AnnotationPictureSizeMode.FixedSize) + self.assertEqual(item.bounds().toString(3), '10.000,20.000 : 30.000,40.000') + + self.assertEqual(item.applyEditV2(QgsAnnotationItemEditOperationTranslateItem('', 100, 200), + QgsAnnotationItemEditContext()), + Qgis.AnnotationItemEditOperationResult.Success) + self.assertEqual(item.bounds().toString(3), '110.000,220.000 : 130.000,240.000') + + def test_apply_move_node_edit_spatial_bounds(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, + self.get_test_data_path( + 'rgb256x256.png').as_posix(), + QgsRectangle(10, 20, 30, 40)) + self.assertEqual(item.bounds().toString(3), '10.000,20.000 : 30.000,40.000') + + self.assertEqual(item.applyEditV2( + QgsAnnotationItemEditOperationMoveNode('', QgsVertexId(0, 0, 1), QgsPoint(30, 20), QgsPoint(17, 18)), + QgsAnnotationItemEditContext()), + Qgis.AnnotationItemEditOperationResult.Success) + self.assertEqual(item.bounds().toString(3), '10.000,18.000 : 17.000,40.000') + self.assertEqual(item.applyEditV2( + QgsAnnotationItemEditOperationMoveNode('', QgsVertexId(0, 0, 0), QgsPoint(10, 18), QgsPoint(5, 13)), + QgsAnnotationItemEditContext()), + Qgis.AnnotationItemEditOperationResult.Success) + self.assertEqual(item.bounds().toString(3), '5.000,13.000 : 17.000,40.000') + self.assertEqual(item.applyEditV2( + QgsAnnotationItemEditOperationMoveNode('', QgsVertexId(0, 0, 2), QgsPoint(17, 14), QgsPoint(18, 38)), + QgsAnnotationItemEditContext()), + Qgis.AnnotationItemEditOperationResult.Success) + self.assertEqual(item.bounds().toString(3), '5.000,13.000 : 18.000,38.000') + self.assertEqual(item.applyEditV2( + QgsAnnotationItemEditOperationMoveNode('', QgsVertexId(0, 0, 3), QgsPoint(5, 38), QgsPoint(2, 39)), + QgsAnnotationItemEditContext()), + Qgis.AnnotationItemEditOperationResult.Success) + self.assertEqual(item.bounds().toString(3), '2.000,13.000 : 18.000,39.000') + + def test_apply_move_node_edit_fixed_size(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, + self.get_test_data_path( + 'rgb256x256.png').as_posix(), + QgsRectangle(10, 20, 30, 40)) + item.setSizeMode(Qgis.AnnotationPictureSizeMode.FixedSize) + self.assertEqual(item.bounds().toString(3), '10.000,20.000 : 30.000,40.000') + + self.assertEqual(item.applyEditV2( + QgsAnnotationItemEditOperationMoveNode('', QgsVertexId(0, 0, 0), QgsPoint(30, 20), QgsPoint(17, 18)), + QgsAnnotationItemEditContext()), + Qgis.AnnotationItemEditOperationResult.Success) + self.assertEqual(item.bounds().toString(3), '7.000,8.000 : 27.000,28.000') + + def test_apply_delete_node_edit(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, + self.get_test_data_path( + 'rgb256x256.png').as_posix(), + QgsRectangle(10, 20, 30, 40)) + + self.assertEqual( + item.applyEdit(QgsAnnotationItemEditOperationDeleteNode('', QgsVertexId(0, 0, 1), QgsPoint(14, 13))), + Qgis.AnnotationItemEditOperationResult.Invalid) + + def test_apply_add_node_edit(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, + self.get_test_data_path( + 'rgb256x256.png').as_posix(), + QgsRectangle(10, 20, 30, 40)) + + self.assertEqual(item.applyEdit(QgsAnnotationItemEditOperationAddNode('', QgsPoint(15, 16))), + Qgis.AnnotationItemEditOperationResult.Invalid) + + def test_transient_move_operation_spatial_bounds(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, + self.get_test_data_path( + 'rgb256x256.png').as_posix(), + QgsRectangle(10, 20, 30, 40)) + self.assertEqual(item.bounds().toString(3), '10.000,20.000 : 30.000,40.000') + + res = item.transientEditResultsV2( + QgsAnnotationItemEditOperationMoveNode('', QgsVertexId(0, 0, 1), QgsPoint(30, 20), QgsPoint(17, 18)), + QgsAnnotationItemEditContext()) + self.assertEqual(res.representativeGeometry().asWkt(), 'Polygon ((10 18, 17 18, 17 40, 10 40, 10 18))') + + def test_transient_move_operation_fixed_size(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, + self.get_test_data_path( + 'rgb256x256.png').as_posix(), + QgsRectangle(10, 20, 30, 40)) + item.setSizeMode(Qgis.AnnotationPictureSizeMode.FixedSize) + item.setFixedSize(QSizeF(56, + 57)) + item.setFixedSizeUnit(Qgis.RenderUnit.Inches) + self.assertEqual(item.bounds().toString(3), '10.000,20.000 : 30.000,40.000') + + op = QgsAnnotationItemEditOperationMoveNode('', QgsVertexId(0, 0, 1), QgsPoint(30, 20), QgsPoint(17, 18)) + context = QgsAnnotationItemEditContext() + context.setCurrentItemBounds(QgsRectangle(1, 2, 3, 4)) + res = item.transientEditResultsV2(op, context) + self.assertEqual(res.representativeGeometry().asWkt(), 'Polygon ((16 17, 18 17, 18 19, 16 19, 16 17))') + + def test_transient_translate_operation_spatial_bounds(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, + self.get_test_data_path( + 'rgb256x256.png').as_posix(), + QgsRectangle(10, 20, 30, 40)) + self.assertEqual(item.bounds().toString(3), '10.000,20.000 : 30.000,40.000') + + res = item.transientEditResultsV2(QgsAnnotationItemEditOperationTranslateItem('', 100, 200), + QgsAnnotationItemEditContext()) + self.assertEqual(res.representativeGeometry().asWkt(), + 'Polygon ((110 220, 130 220, 130 240, 110 240, 110 220))') + + def test_transient_translate_operation_fixed_size(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, + self.get_test_data_path( + 'rgb256x256.png').as_posix(), + QgsRectangle(10, 20, 30, 40)) + item.setSizeMode(Qgis.AnnotationPictureSizeMode.FixedSize) + item.setFixedSize(QSizeF(56, + 57)) + item.setFixedSizeUnit(Qgis.RenderUnit.Inches) + self.assertEqual(item.bounds().toString(3), '10.000,20.000 : 30.000,40.000') + + op = QgsAnnotationItemEditOperationTranslateItem('', 100, 200) + context = QgsAnnotationItemEditContext() + context.setCurrentItemBounds(QgsRectangle(1, 2, 3, 4)) + res = item.transientEditResultsV2(op, context) + self.assertEqual(res.representativeGeometry().asWkt(), + 'Polygon ((119 229, 121 229, 121 231, 119 231, 119 229))') + + def testReadWriteXml(self): + doc = QDomDocument("testdoc") + elem = doc.createElement('test') + + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, self.get_test_data_path('rgb256x256.png').as_posix(), + QgsRectangle(10, 20, 30, 40)) + item.setBackgroundEnabled(True) + item.setBackgroundSymbol(QgsFillSymbol.createSimple({'color': '200,100,100', 'outline_color': 'black'})) + item.setFrameEnabled(True) + item.setFrameSymbol(QgsFillSymbol.createSimple({'color': '100,200,150', 'outline_color': 'black'})) + item.setZIndex(11) + item.setLockAspectRatio(False) + item.setSizeMode(Qgis.AnnotationPictureSizeMode.FixedSize) + item.setFixedSize(QSizeF(56, + 57)) + item.setFixedSizeUnit(Qgis.RenderUnit.Inches) + + self.assertTrue(item.writeXml(elem, doc, QgsReadWriteContext())) + + s2 = QgsAnnotationPictureItem.create() + self.assertTrue(s2.readXml(elem, QgsReadWriteContext())) + + self.assertEqual(s2.bounds().toString(3), '10.000,20.000 : 30.000,40.000') + self.assertEqual(s2.path(), self.get_test_data_path('rgb256x256.png').as_posix()) + self.assertEqual(s2.format(), Qgis.PictureFormat.Raster) + self.assertEqual(s2.backgroundSymbol()[0].color(), QColor(200, 100, 100)) + self.assertEqual(s2.frameSymbol()[0].color(), + QColor(100, 200, 150)) + self.assertEqual(s2.zIndex(), 11) + self.assertTrue(s2.frameEnabled()) + self.assertTrue(s2.backgroundEnabled()) + self.assertFalse(s2.lockAspectRatio()) + self.assertEqual(s2.sizeMode(), + Qgis.AnnotationPictureSizeMode.FixedSize) + self.assertEqual(s2.fixedSize(), QSizeF(56, + 57)) + self.assertEqual(s2.fixedSizeUnit(), Qgis.RenderUnit.Inches) + + def testClone(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, self.get_test_data_path('rgb256x256.png').as_posix(), + QgsRectangle(10, 20, 30, 40)) + item.setBackgroundEnabled(True) + item.setBackgroundSymbol(QgsFillSymbol.createSimple({'color': '200,100,100', 'outline_color': 'black'})) + item.setFrameEnabled(True) + item.setFrameSymbol(QgsFillSymbol.createSimple({'color': '100,200,150', 'outline_color': 'black'})) + item.setZIndex(11) + item.setLockAspectRatio(False) + item.setSizeMode(Qgis.AnnotationPictureSizeMode.FixedSize) + item.setFixedSize(QSizeF(56, + 57)) + item.setFixedSizeUnit(Qgis.RenderUnit.Inches) + + s2 = item.clone() + self.assertEqual(s2.bounds().toString(3), '10.000,20.000 : 30.000,40.000') + self.assertEqual(s2.path(), self.get_test_data_path('rgb256x256.png').as_posix()) + self.assertEqual(s2.format(), Qgis.PictureFormat.Raster) + self.assertEqual(s2.backgroundSymbol()[0].color(), QColor(200, 100, 100)) + self.assertEqual(s2.frameSymbol()[0].color(), + QColor(100, 200, 150)) + self.assertEqual(s2.zIndex(), 11) + self.assertTrue(s2.frameEnabled()) + self.assertTrue(s2.backgroundEnabled()) + self.assertFalse(s2.lockAspectRatio()) + self.assertEqual(s2.sizeMode(), + Qgis.AnnotationPictureSizeMode.FixedSize) + self.assertEqual(s2.fixedSize(), QSizeF(56, + 57)) + self.assertEqual(s2.fixedSizeUnit(), Qgis.RenderUnit.Inches) + + def testRenderRasterLockedAspect(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, self.get_test_data_path('rgb256x256.png').as_posix(), + QgsRectangle(12, 13, 16, 15)) + item.setLockAspectRatio(True) + + settings = QgsMapSettings() + settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:4326')) + settings.setExtent(QgsRectangle(10, 10, 18, 18)) + settings.setOutputSize(QSize(300, 300)) + + settings.setFlag(QgsMapSettings.Flag.Antialiasing, False) + + rc = QgsRenderContext.fromMapSettings(settings) + image = QImage(200, 200, QImage.Format.Format_ARGB32) + image.setDotsPerMeterX(int(96 / 25.4 * 1000)) + image.setDotsPerMeterY(int(96 / 25.4 * 1000)) + image.fill(QColor(255, 255, 255)) + painter = QPainter(image) + rc.setPainter(painter) + + try: + item.render(rc, None) + finally: + painter.end() + + self.assertTrue(self.image_check('picture_raster_locked_aspect', 'picture_raster_locked_aspect', image)) + + def testRenderRasterUnlockedAspect(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, self.get_test_data_path('rgb256x256.png').as_posix(), + QgsRectangle(12, 13, 16, 15)) + item.setLockAspectRatio(False) + + settings = QgsMapSettings() + settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:4326')) + settings.setExtent(QgsRectangle(10, 10, 18, 18)) + settings.setOutputSize(QSize(300, 300)) + + settings.setFlag(QgsMapSettings.Flag.Antialiasing, False) + + rc = QgsRenderContext.fromMapSettings(settings) + image = QImage(200, 200, QImage.Format.Format_ARGB32) + image.setDotsPerMeterX(int(96 / 25.4 * 1000)) + image.setDotsPerMeterY(int(96 / 25.4 * 1000)) + image.fill(QColor(255, 255, 255)) + painter = QPainter(image) + rc.setPainter(painter) + + try: + item.render(rc, None) + finally: + painter.end() + + self.assertTrue(self.image_check('picture_raster_unlocked_aspect', 'picture_raster_unlocked_aspect', image)) + + def testRenderSvgLockedAspect(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.SVG, self.get_test_data_path('sample_svg.svg').as_posix(), + QgsRectangle(12, 13, 16, 15)) + item.setLockAspectRatio(True) + + settings = QgsMapSettings() + settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:4326')) + settings.setExtent(QgsRectangle(10, 10, 18, 18)) + settings.setOutputSize(QSize(300, 300)) + + settings.setFlag(QgsMapSettings.Flag.Antialiasing, False) + + rc = QgsRenderContext.fromMapSettings(settings) + image = QImage(200, 200, QImage.Format.Format_ARGB32) + image.setDotsPerMeterX(int(96 / 25.4 * 1000)) + image.setDotsPerMeterY(int(96 / 25.4 * 1000)) + image.fill(QColor(255, 255, 255)) + painter = QPainter(image) + rc.setPainter(painter) + + try: + item.render(rc, None) + finally: + painter.end() + + self.assertTrue(self.image_check('picture_svg_locked_aspect', 'picture_svg_locked_aspect', image)) + + def testRenderSvgUnlockedAspect(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.SVG, self.get_test_data_path('sample_svg.svg').as_posix(), + QgsRectangle(12, 13, 16, 15)) + item.setLockAspectRatio(False) + + settings = QgsMapSettings() + settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:4326')) + settings.setExtent(QgsRectangle(10, 10, 18, 18)) + settings.setOutputSize(QSize(300, 300)) + + settings.setFlag(QgsMapSettings.Flag.Antialiasing, False) + + rc = QgsRenderContext.fromMapSettings(settings) + image = QImage(200, 200, QImage.Format.Format_ARGB32) + image.setDotsPerMeterX(int(96 / 25.4 * 1000)) + image.setDotsPerMeterY(int(96 / 25.4 * 1000)) + image.fill(QColor(255, 255, 255)) + painter = QPainter(image) + rc.setPainter(painter) + + try: + item.render(rc, None) + finally: + painter.end() + + self.assertTrue(self.image_check('picture_svg_unlocked_aspect', 'picture_svg_unlocked_aspect', image)) + + def testRenderWithTransform(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, self.get_test_data_path('rgb256x256.png').as_posix(), + QgsRectangle(11.5, 13, 12, 13.5)) + + settings = QgsMapSettings() + settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:3857')) + settings.setExtent(QgsRectangle(1250958, 1386945, 1420709, 1532518)) + settings.setOutputSize(QSize(300, 300)) + + settings.setFlag(QgsMapSettings.Flag.Antialiasing, False) + + rc = QgsRenderContext.fromMapSettings(settings) + rc.setCoordinateTransform( + QgsCoordinateTransform(QgsCoordinateReferenceSystem('EPSG:4326'), settings.destinationCrs(), + QgsProject.instance())) + image = QImage(200, 200, QImage.Format.Format_ARGB32) + image.setDotsPerMeterX(int(96 / 25.4 * 1000)) + image.setDotsPerMeterY(int(96 / 25.4 * 1000)) + image.fill(QColor(255, 255, 255)) + painter = QPainter(image) + rc.setPainter(painter) + + try: + item.render(rc, None) + finally: + painter.end() + + self.assertTrue(self.image_check('picture_transform', 'picture_transform', image)) + + def testRenderFixedSizeRaster(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, self.get_test_data_path('rgb256x256.png').as_posix(), + QgsRectangle(12, 13, 16, 15)) + item.setLockAspectRatio(True) + item.setSizeMode(Qgis.AnnotationPictureSizeMode.FixedSize) + item.setFixedSize(QSizeF(10, + 20)) + item.setFixedSizeUnit(Qgis.RenderUnit.Millimeters) + + settings = QgsMapSettings() + settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:4326')) + settings.setExtent(QgsRectangle(10, 10, 18, 18)) + settings.setOutputSize(QSize(300, 300)) + + settings.setFlag(QgsMapSettings.Flag.Antialiasing, False) + + rc = QgsRenderContext.fromMapSettings(settings) + image = QImage(200, 200, QImage.Format.Format_ARGB32) + image.setDotsPerMeterX(int(96 / 25.4 * 1000)) + image.setDotsPerMeterY(int(96 / 25.4 * 1000)) + image.fill(QColor(255, 255, 255)) + painter = QPainter(image) + rc.setPainter(painter) + + try: + item.render(rc, None) + finally: + painter.end() + + self.assertTrue(self.image_check('picture_fixed_size_raster', 'picture_fixed_size_raster', image)) + + def testRenderSvgFixedSize(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.SVG, self.get_test_data_path('sample_svg.svg').as_posix(), + QgsRectangle(12, 13, 16, 15)) + item.setLockAspectRatio(True) + item.setSizeMode(Qgis.AnnotationPictureSizeMode.FixedSize) + item.setFixedSize(QSizeF(30, + 50)) + item.setFixedSizeUnit(Qgis.RenderUnit.Millimeters) + + settings = QgsMapSettings() + settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:4326')) + settings.setExtent(QgsRectangle(10, 10, 18, 18)) + settings.setOutputSize(QSize(300, 300)) + + settings.setFlag(QgsMapSettings.Flag.Antialiasing, False) + + rc = QgsRenderContext.fromMapSettings(settings) + image = QImage(200, 200, QImage.Format.Format_ARGB32) + image.setDotsPerMeterX(int(96 / 25.4 * 1000)) + image.setDotsPerMeterY(int(96 / 25.4 * 1000)) + image.fill(QColor(255, 255, 255)) + painter = QPainter(image) + rc.setPainter(painter) + + try: + item.render(rc, None) + finally: + painter.end() + + self.assertTrue(self.image_check('picture_svg_fixed_size', 'picture_svg_fixed_size', image)) + + def testRenderWithTransformFixedSize(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, self.get_test_data_path('rgb256x256.png').as_posix(), + QgsRectangle(11.5, 13, 12, 13.5)) + item.setSizeMode(Qgis.AnnotationPictureSizeMode.FixedSize) + item.setFixedSize(QSizeF(10, + 20)) + item.setFixedSizeUnit(Qgis.RenderUnit.Millimeters) + + settings = QgsMapSettings() + settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:3857')) + settings.setExtent(QgsRectangle(1250958, 1386945, 1420709, 1532518)) + settings.setOutputSize(QSize(300, 300)) + + settings.setFlag(QgsMapSettings.Flag.Antialiasing, False) + + rc = QgsRenderContext.fromMapSettings(settings) + rc.setCoordinateTransform( + QgsCoordinateTransform(QgsCoordinateReferenceSystem('EPSG:4326'), settings.destinationCrs(), + QgsProject.instance())) + image = QImage(200, 200, QImage.Format.Format_ARGB32) + image.setDotsPerMeterX(int(96 / 25.4 * 1000)) + image.setDotsPerMeterY(int(96 / 25.4 * 1000)) + image.fill(QColor(255, 255, 255)) + painter = QPainter(image) + rc.setPainter(painter) + + try: + item.render(rc, None) + finally: + painter.end() + + self.assertTrue(self.image_check('picture_transform_fixed_size', 'picture_transform_fixed_size', image)) + + def testRenderBackgroundFrame(self): + item = QgsAnnotationPictureItem(Qgis.PictureFormat.Raster, self.get_test_data_path('rgb256x256.png').as_posix(), + QgsRectangle(12, 13, 16, 15)) + item.setLockAspectRatio(True) + item.setFrameEnabled(True) + item.setBackgroundEnabled(True) + item.setBackgroundSymbol(QgsFillSymbol.createSimple({'color': '200,100,100', 'outline_color': 'black'})) + item.setFrameSymbol(QgsFillSymbol.createSimple( + {'color': '100,200,250,120', 'outline_color': 'black', 'outline_width': 2})) + + settings = QgsMapSettings() + settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:4326')) + settings.setExtent(QgsRectangle(10, 10, 18, 18)) + settings.setOutputSize(QSize(300, 300)) + + settings.setFlag(QgsMapSettings.Flag.Antialiasing, False) + + rc = QgsRenderContext.fromMapSettings(settings) + image = QImage(200, 200, QImage.Format.Format_ARGB32) + image.setDotsPerMeterX(int(96 / 25.4 * 1000)) + image.setDotsPerMeterY(int(96 / 25.4 * 1000)) + image.fill(QColor(255, 255, 255)) + painter = QPainter(image) + rc.setPainter(painter) + + try: + item.render(rc, None) + finally: + painter.end() + + self.assertTrue(self.image_check('picture_frame_background', 'picture_frame_background', image)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testdata/control_images/annotation_layer/expected_picture_fixed_size_raster/expected_picture_fixed_size_raster.png b/tests/testdata/control_images/annotation_layer/expected_picture_fixed_size_raster/expected_picture_fixed_size_raster.png new file mode 100644 index 000000000000..d24cfe2f3b97 Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_picture_fixed_size_raster/expected_picture_fixed_size_raster.png differ diff --git a/tests/testdata/control_images/annotation_layer/expected_picture_fixed_size_raster/expected_picture_fixed_size_raster_mask.png b/tests/testdata/control_images/annotation_layer/expected_picture_fixed_size_raster/expected_picture_fixed_size_raster_mask.png new file mode 100644 index 000000000000..385f794d4dd4 Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_picture_fixed_size_raster/expected_picture_fixed_size_raster_mask.png differ diff --git a/tests/testdata/control_images/annotation_layer/expected_picture_frame_background/expected_picture_frame_background.png b/tests/testdata/control_images/annotation_layer/expected_picture_frame_background/expected_picture_frame_background.png new file mode 100644 index 000000000000..b4552b2c0078 Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_picture_frame_background/expected_picture_frame_background.png differ diff --git a/tests/testdata/control_images/annotation_layer/expected_picture_raster_locked_aspect/expected_picture_raster_locked_aspect.png b/tests/testdata/control_images/annotation_layer/expected_picture_raster_locked_aspect/expected_picture_raster_locked_aspect.png new file mode 100644 index 000000000000..81ea71846e30 Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_picture_raster_locked_aspect/expected_picture_raster_locked_aspect.png differ diff --git a/tests/testdata/control_images/annotation_layer/expected_picture_raster_unlocked_aspect/expected_picture_raster_unlocked_aspect.png b/tests/testdata/control_images/annotation_layer/expected_picture_raster_unlocked_aspect/expected_picture_raster_unlocked_aspect.png new file mode 100644 index 000000000000..c5b8063dc7d2 Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_picture_raster_unlocked_aspect/expected_picture_raster_unlocked_aspect.png differ diff --git a/tests/testdata/control_images/annotation_layer/expected_picture_svg_fixed_size/expected_picture_svg_fixed_size.png b/tests/testdata/control_images/annotation_layer/expected_picture_svg_fixed_size/expected_picture_svg_fixed_size.png new file mode 100644 index 000000000000..7cb019811f31 Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_picture_svg_fixed_size/expected_picture_svg_fixed_size.png differ diff --git a/tests/testdata/control_images/annotation_layer/expected_picture_svg_fixed_size/expected_picture_svg_fixed_size_mask.png b/tests/testdata/control_images/annotation_layer/expected_picture_svg_fixed_size/expected_picture_svg_fixed_size_mask.png new file mode 100644 index 000000000000..594d2cb0dbc8 Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_picture_svg_fixed_size/expected_picture_svg_fixed_size_mask.png differ diff --git a/tests/testdata/control_images/annotation_layer/expected_picture_svg_locked_aspect/expected_picture_svg_locked_aspect.png b/tests/testdata/control_images/annotation_layer/expected_picture_svg_locked_aspect/expected_picture_svg_locked_aspect.png new file mode 100644 index 000000000000..9e7f5bbba11e Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_picture_svg_locked_aspect/expected_picture_svg_locked_aspect.png differ diff --git a/tests/testdata/control_images/annotation_layer/expected_picture_svg_unlocked_aspect/expected_picture_svg_unlocked_aspect.png b/tests/testdata/control_images/annotation_layer/expected_picture_svg_unlocked_aspect/expected_picture_svg_unlocked_aspect.png new file mode 100644 index 000000000000..4c274b41f7e2 Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_picture_svg_unlocked_aspect/expected_picture_svg_unlocked_aspect.png differ diff --git a/tests/testdata/control_images/annotation_layer/expected_picture_transform/expected_picture_transform.png b/tests/testdata/control_images/annotation_layer/expected_picture_transform/expected_picture_transform.png new file mode 100644 index 000000000000..ecfd5a0ea0c9 Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_picture_transform/expected_picture_transform.png differ diff --git a/tests/testdata/control_images/annotation_layer/expected_picture_transform_fixed_size/expected_picture_transform_fixed_size.png b/tests/testdata/control_images/annotation_layer/expected_picture_transform_fixed_size/expected_picture_transform_fixed_size.png new file mode 100644 index 000000000000..983034d96058 Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_picture_transform_fixed_size/expected_picture_transform_fixed_size.png differ diff --git a/tests/testdata/control_images/annotation_layer/expected_picture_transform_fixed_size/expected_picture_transform_fixed_size_mask.png b/tests/testdata/control_images/annotation_layer/expected_picture_transform_fixed_size/expected_picture_transform_fixed_size_mask.png new file mode 100644 index 000000000000..d343e08ad1a7 Binary files /dev/null and b/tests/testdata/control_images/annotation_layer/expected_picture_transform_fixed_size/expected_picture_transform_fixed_size_mask.png differ