[api] Add method to retrieve the nodes for an annotation item

For now all nodes just directly represent vertices in the item,
but the intention here is that they can represent any form of handle
which can be used to manipulate an item (e.g. a bezier curve point,
some "smart shape" handle for resizing or rotating an item, etc)
This commit is contained in:
Nyall Dawson 2021-08-31 14:27:01 +10:00
parent 0904dcf9a9
commit 4ff1e55a21
28 changed files with 404 additions and 4 deletions

View File

@ -656,3 +656,8 @@ Qgis.AnnotationItemFlag.ScaleDependentBoundingBox.__doc__ = "Item's bounding box
Qgis.AnnotationItemFlag.__doc__ = 'Flags for annotation items.\n\n.. versionadded:: 3.22\n\n' + '* ``ScaleDependentBoundingBox``: ' + Qgis.AnnotationItemFlag.ScaleDependentBoundingBox.__doc__
# --
Qgis.AnnotationItemFlag.baseClass = Qgis
# monkey patching scoped based enum
Qgis.AnnotationItemNodeType.VertexHandle.__doc__ = "Node is a handle for manipulating vertices"
Qgis.AnnotationItemNodeType.__doc__ = 'Annotation item node types.\n\n.. versionadded:: 3.22\n\n' + '* ``VertexHandle``: ' + Qgis.AnnotationItemNodeType.VertexHandle.__doc__
# --
Qgis.AnnotationItemNodeType.baseClass = Qgis

View File

@ -112,6 +112,13 @@ Sets the item's z ``index``, which controls the order in which annotation items
are rendered in the layer.
.. seealso:: :py:func:`zIndex`
%End
virtual QList< QgsAnnotationItemNode > nodes() const;
%Docstring
Returns the nodes for the item, used for editing the item.
.. versionadded:: 3.22
%End
private:

View File

@ -0,0 +1,87 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/annotations/qgsannotationitemnode.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
class QgsAnnotationItemNode
{
%Docstring(signature="appended")
Contains information about a node used for editing an annotation item.
.. versionadded:: 3.22
%End
%TypeHeaderCode
#include "qgsannotationitemnode.h"
%End
public:
QgsAnnotationItemNode();
%Docstring
Default constructor
%End
QgsAnnotationItemNode( const QgsPointXY &point, Qgis::AnnotationItemNodeType type );
%Docstring
Constructor for QgsAnnotationItemNode, with the specified ``point`` and ``type``.
%End
SIP_PYOBJECT __repr__();
%MethodCode
QString str = QStringLiteral( "<QgsAnnotationItemNode: %1 (%2, %3)>" ).arg( qgsEnumValueToKey( sipCpp->type() ) )
.arg( sipCpp->point().x() )
.arg( sipCpp->point().y() );
sipRes = PyUnicode_FromString( str.toUtf8().constData() );
%End
QgsPointXY point() const;
%Docstring
Returns the node's position, in geographic coordinates.
The coordinates will match the annotation item's CRS.
.. seealso:: :py:func:`setPoint`
%End
void setPoint( QgsPointXY point );
%Docstring
Sets the node's position, in geographic coordinates.
The coordinates will match the annotation item's CRS.
.. seealso:: :py:func:`point`
%End
Qgis::AnnotationItemNodeType type() const;
%Docstring
Returns the node type.
.. seealso:: :py:func:`setType`
%End
void setType( Qgis::AnnotationItemNodeType type );
%Docstring
Sets the node type.
.. seealso:: :py:func:`type`
%End
bool operator==( const QgsAnnotationItemNode &other ) const;
bool operator!=( const QgsAnnotationItemNode &other ) const;
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/annotations/qgsannotationitemnode.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

View File

@ -35,6 +35,8 @@ Constructor for QgsAnnotationLineItem, with the specified ``linestring``.
virtual bool writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const;
virtual QList< QgsAnnotationItemNode > nodes() const;
static QgsAnnotationLineItem *create() /Factory/;
%Docstring

View File

@ -37,6 +37,8 @@ Constructor for QgsAnnotationMarkerItem, at the specified ``point``.
virtual Qgis::AnnotationItemFlags flags() const;
virtual QList< QgsAnnotationItemNode > nodes() const;
static QgsAnnotationMarkerItem *create() /Factory/;
%Docstring

View File

@ -51,6 +51,8 @@ Creates a new text at point annotation item.
virtual QgsRectangle boundingBox( QgsRenderContext &context ) const;
virtual QList< QgsAnnotationItemNode > nodes() const;
QgsPointXY point() const;
%Docstring

View File

@ -35,6 +35,8 @@ Constructor for QgsAnnotationPolygonItem, with the specified ``polygon`` geometr
virtual bool writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const;
virtual QList< QgsAnnotationItemNode > nodes() const;
static QgsAnnotationPolygonItem *create() /Factory/;
%Docstring

View File

@ -474,6 +474,11 @@ The development version
typedef QFlags<Qgis::AnnotationItemFlag> AnnotationItemFlags;
enum class AnnotationItemNodeType
{
VertexHandle,
};
static const double DEFAULT_SEARCH_RADIUS_MM;
static const float DEFAULT_MAPTOPIXEL_THRESHOLD;

View File

@ -205,6 +205,7 @@
%Include auto_generated/./3d/qgsabstract3drenderer.sip
%Include auto_generated/annotations/qgsannotation.sip
%Include auto_generated/annotations/qgsannotationitem.sip
%Include auto_generated/annotations/qgsannotationitemnode.sip
%Include auto_generated/annotations/qgsannotationitemregistry.sip
%Include auto_generated/annotations/qgsannotationlayer.sip
%Include auto_generated/annotations/qgsannotationlineitem.sip

View File

@ -1153,6 +1153,7 @@ set(QGIS_CORE_HDRS
annotations/qgsannotation.h
annotations/qgsannotationitem.h
annotations/qgsannotationitemnode.h
annotations/qgsannotationitemregistry.h
annotations/qgsannotationlayer.h
annotations/qgsannotationlayerrenderer.h

View File

@ -16,8 +16,14 @@
***************************************************************************/
#include "qgsannotationitem.h"
#include "qgsannotationitemnode.h"
Qgis::AnnotationItemFlags QgsAnnotationItem::flags() const
{
return Qgis::AnnotationItemFlags();
}
QList<QgsAnnotationItemNode> QgsAnnotationItem::nodes() const
{
return {};
}

View File

@ -28,6 +28,7 @@ class QgsFeedback;
class QgsMarkerSymbol;
class QgsLineSymbol;
class QgsFillSymbol;
class QgsAnnotationItemNode;
/**
* \ingroup core
@ -140,6 +141,13 @@ class CORE_EXPORT QgsAnnotationItem
*/
void setZIndex( int index ) { mZIndex = index; }
/**
* Returns the nodes for the item, used for editing the item.
*
* \since QGIS 3.22
*/
virtual QList< QgsAnnotationItemNode > nodes() const;
private:
int mZIndex = 0;

View File

@ -0,0 +1,107 @@
/***************************************************************************
qgsannotationitemnode.h
----------------
copyright : (C) 2021 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 QGSANNOTATIONITEMEDITNODE_H
#define QGSANNOTATIONITEMEDITNODE_H
#include "qgis_core.h"
#include "qgis_sip.h"
#include "qgspointxy.h"
#include "qgis.h"
/**
* \ingroup core
* \brief Contains information about a node used for editing an annotation item.
* \since QGIS 3.22
*/
class CORE_EXPORT QgsAnnotationItemNode
{
public:
/**
* Default constructor
*/
QgsAnnotationItemNode() = default;
/**
* Constructor for QgsAnnotationItemNode, with the specified \a point and \a type.
*/
QgsAnnotationItemNode( const QgsPointXY &point, Qgis::AnnotationItemNodeType type )
: mPoint( point )
, mType( type )
{}
#ifdef SIP_RUN
SIP_PYOBJECT __repr__();
% MethodCode
QString str = QStringLiteral( "<QgsAnnotationItemNode: %1 (%2, %3)>" ).arg( qgsEnumValueToKey( sipCpp->type() ) )
.arg( sipCpp->point().x() )
.arg( sipCpp->point().y() );
sipRes = PyUnicode_FromString( str.toUtf8().constData() );
% End
#endif
/**
* Returns the node's position, in geographic coordinates.
*
* The coordinates will match the annotation item's CRS.
*
* \see setPoint()
*/
QgsPointXY point() const { return mPoint; }
/**
* Sets the node's position, in geographic coordinates.
*
* The coordinates will match the annotation item's CRS.
*
* \see point()
*/
void setPoint( QgsPointXY point ) { mPoint = point; }
/**
* Returns the node type.
*
* \see setType()
*/
Qgis::AnnotationItemNodeType type() const { return mType; }
/**
* Sets the node type.
*
* \see type()
*/
void setType( Qgis::AnnotationItemNodeType type ) { mType = type; }
// TODO c++20 - replace with = default
bool operator==( const QgsAnnotationItemNode &other ) const
{
return mType == other.mType && mPoint == other.mPoint;
}
bool operator!=( const QgsAnnotationItemNode &other ) const
{
return !( *this == other );
}
private:
QgsPointXY mPoint;
Qgis::AnnotationItemNodeType mType = Qgis::AnnotationItemNodeType::VertexHandle;
};
#endif // QGSANNOTATIONITEMEDITNODE_H

View File

@ -19,6 +19,7 @@
#include "qgssymbol.h"
#include "qgssymbollayerutils.h"
#include "qgslinesymbol.h"
#include "qgsannotationitemnode.h"
QgsAnnotationLineItem::QgsAnnotationLineItem( QgsCurve *curve )
: QgsAnnotationItem()
@ -80,6 +81,16 @@ bool QgsAnnotationLineItem::writeXml( QDomElement &element, QDomDocument &docume
return true;
}
QList<QgsAnnotationItemNode> QgsAnnotationLineItem::nodes() const
{
QList< QgsAnnotationItemNode > res;
for ( auto it = mCurve->vertices_begin(); it != mCurve->vertices_end(); ++it )
{
res.append( QgsAnnotationItemNode( QgsPointXY( ( *it ).x(), ( *it ).y() ), Qgis::AnnotationItemNodeType::VertexHandle ) );
}
return res;
}
QgsAnnotationLineItem *QgsAnnotationLineItem::create()
{
return new QgsAnnotationLineItem( new QgsLineString() );

View File

@ -43,6 +43,7 @@ class CORE_EXPORT QgsAnnotationLineItem : public QgsAnnotationItem
QString type() const override;
void render( QgsRenderContext &context, QgsFeedback *feedback ) override;
bool writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const override;
QList< QgsAnnotationItemNode > nodes() const override;
/**
* Creates a new linestring annotation item.

View File

@ -19,6 +19,7 @@
#include "qgssymbol.h"
#include "qgssymbollayerutils.h"
#include "qgsmarkersymbol.h"
#include "qgsannotationitemnode.h"
QgsAnnotationMarkerItem::QgsAnnotationMarkerItem( const QgsPoint &point )
: QgsAnnotationItem()
@ -73,6 +74,11 @@ Qgis::AnnotationItemFlags QgsAnnotationMarkerItem::flags() const
return Qgis::AnnotationItemFlag::ScaleDependentBoundingBox;
}
QList<QgsAnnotationItemNode> QgsAnnotationMarkerItem::nodes() const
{
return { QgsAnnotationItemNode( mPoint, Qgis::AnnotationItemNodeType::VertexHandle )};
}
QgsAnnotationMarkerItem *QgsAnnotationMarkerItem::create()
{
return new QgsAnnotationMarkerItem( QgsPoint() );

View File

@ -43,6 +43,7 @@ class CORE_EXPORT QgsAnnotationMarkerItem : public QgsAnnotationItem
void render( QgsRenderContext &context, QgsFeedback *feedback ) override;
bool writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const override;
Qgis::AnnotationItemFlags flags() const override;
QList< QgsAnnotationItemNode > nodes() const override;
/**
* Creates a new marker annotation item.

View File

@ -17,6 +17,7 @@
#include "qgsannotationpointtextitem.h"
#include "qgstextrenderer.h"
#include "qgsannotationitemnode.h"
QgsAnnotationPointTextItem::QgsAnnotationPointTextItem( const QString &text, QgsPointXY point )
: QgsAnnotationItem()
@ -128,6 +129,11 @@ QgsRectangle QgsAnnotationPointTextItem::boundingBox( QgsRenderContext &context
return QgsRectangle( mPoint.x(), mPoint.y(), mPoint.x() + widthInMapUnits, mPoint.y() + heightInMapUnits );
}
QList<QgsAnnotationItemNode> QgsAnnotationPointTextItem::nodes() const
{
return { QgsAnnotationItemNode( mPoint, Qgis::AnnotationItemNodeType::VertexHandle )};
}
QgsTextFormat QgsAnnotationPointTextItem::format() const
{
return mTextFormat;

View File

@ -54,6 +54,7 @@ class CORE_EXPORT QgsAnnotationPointTextItem : public QgsAnnotationItem
QgsAnnotationPointTextItem *clone() override SIP_FACTORY;
QgsRectangle boundingBox() const override;
QgsRectangle boundingBox( QgsRenderContext &context ) const override;
QList< QgsAnnotationItemNode > nodes() const override;
/**
* Returns the point location of the text.

View File

@ -20,6 +20,7 @@
#include "qgssymbollayerutils.h"
#include "qgssurface.h"
#include "qgsfillsymbol.h"
#include "qgsannotationitemnode.h"
QgsAnnotationPolygonItem::QgsAnnotationPolygonItem( QgsCurvePolygon *polygon )
: QgsAnnotationItem()
@ -94,6 +95,33 @@ bool QgsAnnotationPolygonItem::writeXml( QDomElement &element, QDomDocument &doc
return true;
}
QList<QgsAnnotationItemNode> QgsAnnotationPolygonItem::nodes() const
{
QList< QgsAnnotationItemNode > res;
auto processRing = [&res]( const QgsCurve * ring )
{
// we don't want a duplicate node for the closed ring vertex
const int count = ring->isClosed() ? ring->numPoints() - 1 : ring->numPoints();
res.reserve( res.size() + count );
for ( int i = 0; i < count; ++i )
{
res << QgsAnnotationItemNode( QgsPointXY( ring->xAt( i ), ring->yAt( i ) ), Qgis::AnnotationItemNodeType::VertexHandle );
}
};
if ( const QgsCurve *ring = mPolygon->exteriorRing() )
{
processRing( ring );
}
for ( int i = 0; i < mPolygon->numInteriorRings(); ++i )
{
processRing( mPolygon->interiorRing( i ) );
}
return res;
}
QgsAnnotationPolygonItem *QgsAnnotationPolygonItem::create()
{
return new QgsAnnotationPolygonItem( new QgsPolygon() );

View File

@ -42,6 +42,7 @@ class CORE_EXPORT QgsAnnotationPolygonItem : public QgsAnnotationItem
QString type() const override;
void render( QgsRenderContext &context, QgsFeedback *feedback ) override;
bool writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const override;
QList< QgsAnnotationItemNode > nodes() const override;
/**
* Creates a new polygon annotation item.

View File

@ -743,6 +743,17 @@ class CORE_EXPORT Qgis
Q_DECLARE_FLAGS( AnnotationItemFlags, AnnotationItemFlag )
Q_ENUM( AnnotationItemFlag )
/**
* Annotation item node types.
*
* \since QGIS 3.22
*/
enum class AnnotationItemNodeType : int
{
VertexHandle, //!< Node is a handle for manipulating vertices
};
Q_ENUM( AnnotationItemNodeType )
/**
* Identify search radius in mm
* \since QGIS 2.3

View File

@ -19,6 +19,7 @@ ADD_PYTHON_TEST(PyQgsArcGisPortalUtils test_qgsarcgisportalutils.py)
ADD_PYTHON_TEST(PyQgsPythonProvider test_provider_python.py)
ADD_PYTHON_TEST(PyQgsAggregateCalculator test_qgsaggregatecalculator.py)
ADD_PYTHON_TEST(PyQgsAnnotation test_qgsannotation.py)
ADD_PYTHON_TEST(PyQgsAnnotationItemNode test_qgsannotationitemnode.py)
ADD_PYTHON_TEST(PyQgsAnnotationLayer test_qgsannotationlayer.py)
ADD_PYTHON_TEST(PyQgsAnnotationLineItem test_qgsannotationlineitem.py)
ADD_PYTHON_TEST(PyQgsAnnotationMarkerItem test_qgsannotationmarkeritem.py)

View File

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsAnnotationItemNode
From build dir, run: ctest -R PyQgsAnnotationLayer -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.
"""
__author__ = '(C) 2020 by Nyall Dawson'
__date__ = '29/07/2020'
__copyright__ = 'Copyright 2020, The QGIS Project'
import qgis # NOQA
from qgis.core import (
QgsAnnotationItemNode,
QgsPointXY,
Qgis
)
from qgis.testing import start_app, unittest
from utilities import unitTestDataPath
start_app()
TEST_DATA_DIR = unitTestDataPath()
class TestQgsAnnotationItemNode(unittest.TestCase):
def test_basic(self):
node = QgsAnnotationItemNode(QgsPointXY(1, 2), Qgis.AnnotationItemNodeType.VertexHandle)
self.assertEqual(node.point(), QgsPointXY(1, 2))
node.setPoint(QgsPointXY(3, 4))
self.assertEqual(node.point(), QgsPointXY(3, 4))
self.assertEqual(node.type(), Qgis.AnnotationItemNodeType.VertexHandle)
def test_repr(self):
node = QgsAnnotationItemNode(QgsPointXY(1, 2), Qgis.AnnotationItemNodeType.VertexHandle)
self.assertEqual(str(node), '<QgsAnnotationItemNode: VertexHandle (1, 2)>')
def test_equality(self):
node = QgsAnnotationItemNode(QgsPointXY(1, 2), Qgis.AnnotationItemNodeType.VertexHandle)
node2 = QgsAnnotationItemNode(QgsPointXY(1, 2), Qgis.AnnotationItemNodeType.VertexHandle)
self.assertEqual(node, node2)
node2.setPoint(QgsPointXY(3, 4))
self.assertNotEqual(node, node2)
if __name__ == '__main__':
unittest.main()

View File

@ -31,7 +31,10 @@ from qgis.core import (QgsMapSettings,
QgsAnnotationLineItem,
QgsRectangle,
QgsLineString,
QgsCircularString
QgsCircularString,
QgsAnnotationItemNode,
QgsPointXY,
Qgis
)
from qgis.PyQt.QtXml import QDomDocument
@ -67,6 +70,15 @@ class TestQgsAnnotationLineItem(unittest.TestCase):
item.setSymbol(QgsLineSymbol.createSimple({'color': '#ffff00', 'line_width': '3'}))
self.assertEqual(item.symbol()[0].color(), QColor(255, 255, 0))
def test_nodes(self):
"""
Test nodes for item
"""
item = QgsAnnotationLineItem(QgsLineString([QgsPoint(12, 13), QgsPoint(14, 13), QgsPoint(14, 15)]))
self.assertEqual(item.nodes(), [QgsAnnotationItemNode(QgsPointXY(12, 13), Qgis.AnnotationItemNodeType.VertexHandle),
QgsAnnotationItemNode(QgsPointXY(14, 13), Qgis.AnnotationItemNodeType.VertexHandle),
QgsAnnotationItemNode(QgsPointXY(14, 15), Qgis.AnnotationItemNodeType.VertexHandle)])
def testReadWriteXml(self):
doc = QDomDocument("testdoc")
elem = doc.createElement('test')

View File

@ -29,7 +29,10 @@ from qgis.core import (QgsMapSettings,
QgsReadWriteContext,
QgsRenderContext,
QgsAnnotationMarkerItem,
QgsRectangle
QgsRectangle,
QgsAnnotationItemNode,
Qgis,
QgsPointXY
)
from qgis.PyQt.QtXml import QDomDocument
@ -67,6 +70,13 @@ class TestQgsAnnotationMarkerItem(unittest.TestCase):
item.setSymbol(QgsMarkerSymbol.createSimple({'color': '100,200,200', 'size': '3', 'outline_color': 'black'}))
self.assertEqual(item.symbol()[0].color(), QColor(100, 200, 200))
def test_nodes(self):
"""
Test nodes for item
"""
item = QgsAnnotationMarkerItem(QgsPoint(12, 13))
self.assertEqual(item.nodes(), [QgsAnnotationItemNode(QgsPointXY(12, 13), Qgis.AnnotationItemNodeType.VertexHandle)])
def testReadWriteXml(self):
doc = QDomDocument("testdoc")
elem = doc.createElement('test')

View File

@ -31,7 +31,9 @@ from qgis.core import (QgsMapSettings,
QgsRenderContext,
QgsAnnotationPointTextItem,
QgsRectangle,
QgsTextFormat
QgsTextFormat,
QgsAnnotationItemNode,
Qgis
)
from qgis.PyQt.QtXml import QDomDocument
@ -79,6 +81,13 @@ class TestQgsAnnotationPointTextItem(unittest.TestCase):
self.assertEqual(item.zIndex(), 11)
self.assertEqual(item.format().size(), 37)
def test_nodes(self):
"""
Test nodes for item
"""
item = QgsAnnotationPointTextItem('my text', QgsPointXY(12, 13))
self.assertEqual(item.nodes(), [QgsAnnotationItemNode(QgsPointXY(12, 13), Qgis.AnnotationItemNodeType.VertexHandle)])
def testReadWriteXml(self):
doc = QDomDocument("testdoc")
elem = doc.createElement('test')

View File

@ -33,7 +33,10 @@ from qgis.core import (QgsMapSettings,
QgsLineString,
QgsPolygon,
QgsCurvePolygon,
QgsCircularString
QgsCircularString,
QgsAnnotationItemNode,
Qgis,
QgsPointXY
)
from qgis.PyQt.QtXml import QDomDocument
@ -69,6 +72,16 @@ class TestQgsAnnotationPolygonItem(unittest.TestCase):
item.setSymbol(QgsFillSymbol.createSimple({'color': '200,100,100', 'outline_color': 'black'}))
self.assertEqual(item.symbol()[0].color(), QColor(200, 100, 100))
def test_nodes(self):
"""
Test nodes for item
"""
item = QgsAnnotationPolygonItem(QgsPolygon(QgsLineString([QgsPoint(12, 13), QgsPoint(14, 13), QgsPoint(14, 15), QgsPoint(12, 13)])))
# nodes shouldn't form a closed ring
self.assertEqual(item.nodes(), [QgsAnnotationItemNode(QgsPointXY(12, 13), Qgis.AnnotationItemNodeType.VertexHandle),
QgsAnnotationItemNode(QgsPointXY(14, 13), Qgis.AnnotationItemNodeType.VertexHandle),
QgsAnnotationItemNode(QgsPointXY(14, 15), Qgis.AnnotationItemNodeType.VertexHandle)])
def testReadWriteXml(self):
doc = QDomDocument("testdoc")
elem = doc.createElement('test')