[api] Add text-along-line annotation item type

This annotation item renders curved text along a linestring
This commit is contained in:
Nyall Dawson 2023-03-29 08:22:26 +10:00
parent b9c0fc46b5
commit cd3a1bf237
19 changed files with 810 additions and 7 deletions

View File

@ -38,6 +38,10 @@ Abstract base class for annotation items which are drawn with :py:class:`QgsAnno
{
sipType = sipType_QgsAnnotationPointTextItem;
}
else if ( sipCpp->type() == QLatin1String( "linetext" ) )
{
sipType = sipType_QgsAnnotationLineTextItem;
}
else
{
sipType = 0;

View File

@ -25,7 +25,7 @@ An annotation item which renders a line symbol along a line geometry.
QgsAnnotationLineItem( QgsCurve *curve /Transfer/ );
%Docstring
Constructor for QgsAnnotationLineItem, with the specified ``linestring``.
Constructor for QgsAnnotationLineItem, with the specified ``curve``.
%End
~QgsAnnotationLineItem();

View File

@ -0,0 +1,118 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/annotations/qgsannotationlinetextitem.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
class QgsAnnotationLineTextItem : QgsAnnotationItem
{
%Docstring(signature="appended")
An annotation item which renders text along a line geometry.
.. versionadded:: 3.32
%End
%TypeHeaderCode
#include "qgsannotationlinetextitem.h"
%End
public:
QgsAnnotationLineTextItem( const QString &text, QgsCurve *curve /Transfer/ );
%Docstring
Constructor for QgsAnnotationLineTextItem, with the specified ``curve`` and ``text``.
%End
~QgsAnnotationLineTextItem();
virtual Qgis::AnnotationItemFlags flags() const;
virtual QString type() const;
virtual void render( QgsRenderContext &context, QgsFeedback *feedback );
virtual bool writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const;
virtual QList< QgsAnnotationItemNode > nodes() const;
virtual Qgis::AnnotationItemEditOperationResult applyEdit( QgsAbstractAnnotationItemEditOperation *operation );
virtual QgsAnnotationItemEditOperationTransientResults *transientEditResults( QgsAbstractAnnotationItemEditOperation *operation ) /Factory/;
static QgsAnnotationLineTextItem *create() /Factory/;
%Docstring
Creates a new linestring annotation item.
%End
virtual bool readXml( const QDomElement &element, const QgsReadWriteContext &context );
virtual QgsRectangle boundingBox() const;
virtual QgsRectangle boundingBox( QgsRenderContext &context ) const;
virtual QgsAnnotationLineTextItem *clone() /Factory/;
const QgsCurve *geometry() const;
%Docstring
Returns the geometry of the item.
The coordinate reference system for the line will be the parent layer's :py:func:`QgsAnnotationLayer.crs()`.
.. seealso:: :py:func:`setGeometry`
%End
void setGeometry( QgsCurve *geometry /Transfer/ );
%Docstring
Sets the ``geometry`` of the item. Ownership of ``geometry`` is transferred.
The coordinate reference system for the line will be the parent layer's :py:func:`QgsAnnotationLayer.crs()`.
.. seealso:: :py:func:`geometry`
%End
QString text() const;
%Docstring
Returns the text rendered by the item.
.. seealso:: :py:func:`setText`
%End
void setText( const QString &text );
%Docstring
Sets the ``text`` rendered by the item.
.. seealso:: :py:func:`text`
%End
QgsTextFormat format() const;
%Docstring
Returns the text format used to render the text.
.. seealso:: :py:func:`setFormat`
%End
void setFormat( const QgsTextFormat &format );
%Docstring
Sets the text ``format`` used to render the text.
.. seealso:: :py:func:`format`
%End
private:
QgsAnnotationLineTextItem( const QgsAnnotationLineTextItem &other );
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/annotations/qgsannotationlinetextitem.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

View File

@ -227,6 +227,7 @@
%Include auto_generated/annotations/qgsannotationitemregistry.sip
%Include auto_generated/annotations/qgsannotationlayer.sip
%Include auto_generated/annotations/qgsannotationlineitem.sip
%Include auto_generated/annotations/qgsannotationlinetextitem.sip
%Include auto_generated/annotations/qgsannotationmarkeritem.sip
%Include auto_generated/annotations/qgsannotationmanager.sip
%Include auto_generated/annotations/qgsannotationpointtextitem.sip

View File

@ -207,6 +207,7 @@ set(QGIS_CORE_SRCS
annotations/qgsannotationlayer.cpp
annotations/qgsannotationlayerrenderer.cpp
annotations/qgsannotationlineitem.cpp
annotations/qgsannotationlinetextitem.cpp
annotations/qgsannotationmarkeritem.cpp
annotations/qgsannotationmanager.cpp
annotations/qgsannotationpointtextitem.cpp
@ -1269,6 +1270,7 @@ set(QGIS_CORE_HDRS
annotations/qgsannotationlayer.h
annotations/qgsannotationlayerrenderer.h
annotations/qgsannotationlineitem.h
annotations/qgsannotationlinetextitem.h
annotations/qgsannotationmarkeritem.h
annotations/qgsannotationmanager.h
annotations/qgsannotationpointtextitem.h

View File

@ -20,8 +20,6 @@
#include "qgis_core.h"
#include "qgis_sip.h"
#include "qgscoordinatereferencesystem.h"
#include "qgslinestring.h"
#include "qgspolygon.h"
class QgsFeedback;
class QgsMarkerSymbol;
@ -60,6 +58,10 @@ class CORE_EXPORT QgsAnnotationItem
{
sipType = sipType_QgsAnnotationPointTextItem;
}
else if ( sipCpp->type() == QLatin1String( "linetext" ) )
{
sipType = sipType_QgsAnnotationLineTextItem;
}
else
{
sipType = 0;

View File

@ -20,6 +20,7 @@
#include "qgsannotationlineitem.h"
#include "qgsannotationpolygonitem.h"
#include "qgsannotationpointtextitem.h"
#include "qgsannotationlinetextitem.h"
#include <QDomElement>
QgsAnnotationItemRegistry::QgsAnnotationItemRegistry( QObject *parent )
@ -45,6 +46,8 @@ bool QgsAnnotationItemRegistry::populate()
QgsAnnotationPolygonItem::create ) );
mMetadata.insert( QStringLiteral( "pointtext" ), new QgsAnnotationItemMetadata( QStringLiteral( "pointtext" ), QObject::tr( "Text at point" ), QObject::tr( "Text at points" ),
QgsAnnotationPointTextItem::create ) );
mMetadata.insert( QStringLiteral( "linetext" ), new QgsAnnotationItemMetadata( QStringLiteral( "linetext" ), QObject::tr( "Text along line" ), QObject::tr( "Text along lines" ),
QgsAnnotationLineTextItem::create ) );
return true;
}

View File

@ -21,6 +21,8 @@
#include "qgslinesymbol.h"
#include "qgsannotationitemnode.h"
#include "qgsannotationitemeditoperation.h"
#include "qgscurve.h"
#include "qgslinestring.h"
QgsAnnotationLineItem::QgsAnnotationLineItem( QgsCurve *curve )
: QgsAnnotationItem()
@ -201,6 +203,8 @@ QgsAnnotationLineItem *QgsAnnotationLineItem::clone()
return item.release();
}
void QgsAnnotationLineItem::setGeometry( QgsCurve *geometry ) { mCurve.reset( geometry ); }
const QgsLineSymbol *QgsAnnotationLineItem::symbol() const
{
return mSymbol.get();

View File

@ -35,7 +35,7 @@ class CORE_EXPORT QgsAnnotationLineItem : public QgsAnnotationItem
public:
/**
* Constructor for QgsAnnotationLineItem, with the specified \a linestring.
* Constructor for QgsAnnotationLineItem, with the specified \a curve.
*/
QgsAnnotationLineItem( QgsCurve *curve SIP_TRANSFER );
~QgsAnnotationLineItem() override;
@ -73,7 +73,7 @@ class CORE_EXPORT QgsAnnotationLineItem : public QgsAnnotationItem
*
* \see geometry()
*/
void setGeometry( QgsCurve *geometry SIP_TRANSFER ) { mCurve.reset( geometry ); }
void setGeometry( QgsCurve *geometry SIP_TRANSFER );
/**
* Returns the symbol used to render the item.

View File

@ -0,0 +1,250 @@
/***************************************************************************
qgsannotationlinetextitem.cpp
----------------
begin : March 2023
copyright : (C) 2023 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 "qgsannotationlinetextitem.h"
#include "qgsannotationitemnode.h"
#include "qgsannotationitemeditoperation.h"
#include "qgsrendercontext.h"
#include "qgscurve.h"
#include "qgslinestring.h"
#include "qgstextrenderer.h"
QgsAnnotationLineTextItem::QgsAnnotationLineTextItem( const QString &text, QgsCurve *curve )
: QgsAnnotationItem()
, mText( text )
, mCurve( curve )
{
}
Qgis::AnnotationItemFlags QgsAnnotationLineTextItem::flags() const
{
// in truth this should depend on whether the text format is scale dependent or not!
return Qgis::AnnotationItemFlag::ScaleDependentBoundingBox;
}
QgsAnnotationLineTextItem::~QgsAnnotationLineTextItem() = default;
QString QgsAnnotationLineTextItem::type() const
{
return QStringLiteral( "linetext" );
}
void QgsAnnotationLineTextItem::render( QgsRenderContext &context, QgsFeedback * )
{
// TODO -- expose as an option!
QgsGeometry smoothed( mCurve->clone() );
smoothed = smoothed.smooth( );
QPolygonF pts = smoothed.asQPolygonF();
//transform the QPolygonF to screen coordinates
if ( context.coordinateTransform().isValid() )
{
try
{
context.coordinateTransform().transformPolygon( pts );
}
catch ( QgsCsException & )
{
// we don't abort the rendering here, instead we remove any invalid points and just plot those which ARE valid
}
}
// remove non-finite points, e.g. infinite or NaN points caused by reprojecting errors
pts.erase( std::remove_if( pts.begin(), pts.end(),
[]( const QPointF point )
{
return !std::isfinite( point.x() ) || !std::isfinite( point.y() );
} ), pts.end() );
QPointF *ptr = pts.data();
for ( int i = 0; i < pts.size(); ++i, ++ptr )
{
context.mapToPixel().transformInPlace( ptr->rx(), ptr->ry() );
}
const QString displayText = QgsExpression::replaceExpressionText( mText, &context.expressionContext(), &context.distanceArea() );
QgsTextRenderer::drawTextOnLine( pts, displayText, context, mTextFormat );
}
bool QgsAnnotationLineTextItem::writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const
{
element.setAttribute( QStringLiteral( "wkt" ), mCurve->asWkt() );
element.setAttribute( QStringLiteral( "text" ), mText );
QDomElement textFormatElem = document.createElement( QStringLiteral( "lineTextFormat" ) );
textFormatElem.appendChild( mTextFormat.writeXml( document, context ) );
element.appendChild( textFormatElem );
writeCommonProperties( element, document, context );
return true;
}
QList<QgsAnnotationItemNode> QgsAnnotationLineTextItem::nodes() const
{
QList< QgsAnnotationItemNode > res;
int i = 0;
for ( auto it = mCurve->vertices_begin(); it != mCurve->vertices_end(); ++it, ++i )
{
res.append( QgsAnnotationItemNode( it.vertexId(), QgsPointXY( ( *it ).x(), ( *it ).y() ), Qgis::AnnotationItemNodeType::VertexHandle ) );
}
return res;
}
Qgis::AnnotationItemEditOperationResult QgsAnnotationLineTextItem::applyEdit( QgsAbstractAnnotationItemEditOperation *operation )
{
switch ( operation->type() )
{
case QgsAbstractAnnotationItemEditOperation::Type::MoveNode:
{
QgsAnnotationItemEditOperationMoveNode *moveOperation = qgis::down_cast< QgsAnnotationItemEditOperationMoveNode * >( operation );
if ( mCurve->moveVertex( moveOperation->nodeId(), QgsPoint( moveOperation->after() ) ) )
return Qgis::AnnotationItemEditOperationResult::Success;
break;
}
case QgsAbstractAnnotationItemEditOperation::Type::DeleteNode:
{
QgsAnnotationItemEditOperationDeleteNode *deleteOperation = qgis::down_cast< QgsAnnotationItemEditOperationDeleteNode * >( operation );
if ( mCurve->deleteVertex( deleteOperation->nodeId() ) )
return mCurve->isEmpty() ? Qgis::AnnotationItemEditOperationResult::ItemCleared : Qgis::AnnotationItemEditOperationResult::Success;
break;
}
case QgsAbstractAnnotationItemEditOperation::Type::AddNode:
{
QgsAnnotationItemEditOperationAddNode *addOperation = qgis::down_cast< QgsAnnotationItemEditOperationAddNode * >( operation );
QgsPoint segmentPoint;
QgsVertexId endOfSegmentVertex;
mCurve->closestSegment( addOperation->point(), segmentPoint, endOfSegmentVertex );
if ( mCurve->insertVertex( endOfSegmentVertex, segmentPoint ) )
return Qgis::AnnotationItemEditOperationResult::Success;
break;
}
case QgsAbstractAnnotationItemEditOperation::Type::TranslateItem:
{
QgsAnnotationItemEditOperationTranslateItem *moveOperation = qgis::down_cast< QgsAnnotationItemEditOperationTranslateItem * >( operation );
const QTransform transform = QTransform::fromTranslate( moveOperation->translationX(), moveOperation->translationY() );
mCurve->transform( transform );
return Qgis::AnnotationItemEditOperationResult::Success;
}
}
return Qgis::AnnotationItemEditOperationResult::Invalid;
}
QgsAnnotationItemEditOperationTransientResults *QgsAnnotationLineTextItem::transientEditResults( QgsAbstractAnnotationItemEditOperation *operation )
{
switch ( operation->type() )
{
case QgsAbstractAnnotationItemEditOperation::Type::MoveNode:
{
QgsAnnotationItemEditOperationMoveNode *moveOperation = dynamic_cast< QgsAnnotationItemEditOperationMoveNode * >( operation );
std::unique_ptr< QgsCurve > modifiedCurve( mCurve->clone() );
if ( modifiedCurve->moveVertex( moveOperation->nodeId(), QgsPoint( moveOperation->after() ) ) )
{
return new QgsAnnotationItemEditOperationTransientResults( QgsGeometry( std::move( modifiedCurve ) ) );
}
break;
}
case QgsAbstractAnnotationItemEditOperation::Type::TranslateItem:
{
QgsAnnotationItemEditOperationTranslateItem *moveOperation = qgis::down_cast< QgsAnnotationItemEditOperationTranslateItem * >( operation );
const QTransform transform = QTransform::fromTranslate( moveOperation->translationX(), moveOperation->translationY() );
std::unique_ptr< QgsCurve > modifiedCurve( mCurve->clone() );
modifiedCurve->transform( transform );
return new QgsAnnotationItemEditOperationTransientResults( QgsGeometry( std::move( modifiedCurve ) ) );
}
case QgsAbstractAnnotationItemEditOperation::Type::DeleteNode:
case QgsAbstractAnnotationItemEditOperation::Type::AddNode:
break;
}
return nullptr;
}
QgsAnnotationLineTextItem *QgsAnnotationLineTextItem::create()
{
return new QgsAnnotationLineTextItem( QString(), new QgsLineString() );
}
bool QgsAnnotationLineTextItem::readXml( const QDomElement &element, const QgsReadWriteContext &context )
{
const QString wkt = element.attribute( QStringLiteral( "wkt" ) );
const QgsGeometry geometry = QgsGeometry::fromWkt( wkt );
if ( const QgsCurve *curve = qgsgeometry_cast< const QgsCurve * >( geometry.constGet() ) )
mCurve.reset( curve->clone() );
mText = element.attribute( QStringLiteral( "text" ) );
const QDomElement textFormatElem = element.firstChildElement( QStringLiteral( "lineTextFormat" ) );
if ( !textFormatElem.isNull() )
{
const QDomNodeList textFormatNodeList = textFormatElem.elementsByTagName( QStringLiteral( "text-style" ) );
const QDomElement textFormatElem = textFormatNodeList.at( 0 ).toElement();
mTextFormat.readXml( textFormatElem, context );
}
readCommonProperties( element, context );
return true;
}
QgsRectangle QgsAnnotationLineTextItem::boundingBox() const
{
return mCurve->boundingBox();
}
QgsRectangle QgsAnnotationLineTextItem::boundingBox( QgsRenderContext &context ) const
{
const QString displayText = QgsExpression::replaceExpressionText( mText, &context.expressionContext(), &context.distanceArea() );
const double heightInPixels = QgsTextRenderer::textHeight( context, mTextFormat, { displayText} );
// text size has already been calculated using any symbology reference scale factor above -- we need
// to temporarily remove the reference scale here or we'll be undoing the scaling
QgsScopedRenderContextReferenceScaleOverride resetScaleFactor( context, -1.0 );
const double heightInMapUnits = context.convertToMapUnits( heightInPixels, Qgis::RenderUnit::Pixels );
return mCurve->boundingBox().buffered( heightInMapUnits );
}
QgsAnnotationLineTextItem *QgsAnnotationLineTextItem::clone()
{
std::unique_ptr< QgsAnnotationLineTextItem > item = std::make_unique< QgsAnnotationLineTextItem >( mText, mCurve->clone() );
item->setFormat( mTextFormat );
item->copyCommonProperties( this );
return item.release();
}
void QgsAnnotationLineTextItem::setGeometry( QgsCurve *geometry )
{
mCurve.reset( geometry );
}
QgsTextFormat QgsAnnotationLineTextItem::format() const
{
return mTextFormat;
}
void QgsAnnotationLineTextItem::setFormat( const QgsTextFormat &format )
{
mTextFormat = format;
}

View File

@ -0,0 +1,121 @@
/***************************************************************************
qgsannotationlinetextitem.h
----------------
begin : March 2023
copyright : (C) 2023 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 QGSANNOTATIONLINETEXTITEM_H
#define QGSANNOTATIONLINETEXTITEM_H
#include "qgis_core.h"
#include "qgis_sip.h"
#include "qgsannotationitem.h"
#include "qgstextformat.h"
class QgsCurve;
/**
* \ingroup core
* \brief An annotation item which renders text along a line geometry.
*
* \since QGIS 3.32
*/
class CORE_EXPORT QgsAnnotationLineTextItem : public QgsAnnotationItem
{
public:
/**
* Constructor for QgsAnnotationLineTextItem, with the specified \a curve and \a text.
*/
QgsAnnotationLineTextItem( const QString &text, QgsCurve *curve SIP_TRANSFER );
~QgsAnnotationLineTextItem() override;
Qgis::AnnotationItemFlags flags() const override;
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;
Qgis::AnnotationItemEditOperationResult applyEdit( QgsAbstractAnnotationItemEditOperation *operation ) override;
QgsAnnotationItemEditOperationTransientResults *transientEditResults( QgsAbstractAnnotationItemEditOperation *operation ) override SIP_FACTORY;
/**
* Creates a new linestring annotation item.
*/
static QgsAnnotationLineTextItem *create() SIP_FACTORY;
bool readXml( const QDomElement &element, const QgsReadWriteContext &context ) override;
QgsRectangle boundingBox() const override;
QgsRectangle boundingBox( QgsRenderContext &context ) const override;
QgsAnnotationLineTextItem *clone() override SIP_FACTORY;
/**
* Returns the geometry of the item.
*
* The coordinate reference system for the line will be the parent layer's QgsAnnotationLayer::crs().
*
* \see setGeometry()
*/
const QgsCurve *geometry() const { return mCurve.get(); }
/**
* Sets the \a geometry of the item. Ownership of \a geometry is transferred.
*
* The coordinate reference system for the line will be the parent layer's QgsAnnotationLayer::crs().
*
* \see geometry()
*/
void setGeometry( QgsCurve *geometry SIP_TRANSFER );
/**
* Returns the text rendered by the item.
*
* \see setText()
*/
QString text() const { return mText; }
/**
* Sets the \a text rendered by the item.
*
* \see text()
*/
void setText( const QString &text ) { mText = text; }
/**
* Returns the text format used to render the text.
*
* \see setFormat()
*/
QgsTextFormat format() const;
/**
* Sets the text \a format used to render the text.
*
* \see format()
*/
void setFormat( const QgsTextFormat &format );
private:
QString mText;
std::unique_ptr< QgsCurve > mCurve;
QgsTextFormat mTextFormat;
#ifdef SIP_RUN
QgsAnnotationLineTextItem( const QgsAnnotationLineTextItem &other );
#endif
};
#endif // QGSANNOTATIONLINETEXTITEM_H

View File

@ -18,7 +18,9 @@
#include "qgsannotationpolygonitem.h"
#include "qgssymbol.h"
#include "qgssymbollayerutils.h"
#include "qgssurface.h"
#include "qgscurvepolygon.h"
#include "qgscurve.h"
#include "qgspolygon.h"
#include "qgsfillsymbol.h"
#include "qgsannotationitemnode.h"
#include "qgsannotationitemeditoperation.h"
@ -230,6 +232,11 @@ QgsRectangle QgsAnnotationPolygonItem::boundingBox() const
return mPolygon->boundingBox();
}
void QgsAnnotationPolygonItem::setGeometry( QgsCurvePolygon *geometry )
{
mPolygon.reset( geometry );
}
const QgsFillSymbol *QgsAnnotationPolygonItem::symbol() const
{
return mSymbol.get();

View File

@ -22,6 +22,7 @@
#include "qgis_sip.h"
#include "qgsannotationitem.h"
class QgsCurvePolygon;
/**
* \ingroup core
@ -71,7 +72,7 @@ class CORE_EXPORT QgsAnnotationPolygonItem : public QgsAnnotationItem
*
* \see geometry()
*/
void setGeometry( QgsCurvePolygon *geometry SIP_TRANSFER ) { mPolygon.reset( geometry ); }
void setGeometry( QgsCurvePolygon *geometry SIP_TRANSFER );
/**
* Returns the symbol used to render the item.

View File

@ -28,6 +28,7 @@
#include "qgsadvanceddigitizingdockwidget.h"
#include "qgsapplication.h"
#include "qgsrecentstylehandler.h"
#include "qgscurvepolygon.h"
///@cond PRIVATE

View File

@ -23,6 +23,7 @@ ADD_PYTHON_TEST(PyQgsAnnotationItemEditOperation test_qgsannotationitemeditopera
ADD_PYTHON_TEST(PyQgsAnnotationItemNode test_qgsannotationitemnode.py)
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(PyQgsAnnotationPointTextItem test_qgsannotationpointtextitem.py)
ADD_PYTHON_TEST(PyQgsAnnotationPolygonItem test_qgsannotationpolygonitem.py)

View File

@ -0,0 +1,288 @@
"""QGIS Unit tests for QgsAnnotationLineTextItem.
From build dir, run: ctest -R PyQgsAnnotationlINETextItem -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__ = '10/08/2020'
__copyright__ = 'Copyright 2020, The QGIS Project'
import qgis # NOQA
from qgis.PyQt.QtCore import QDir, QSize, Qt
from qgis.PyQt.QtGui import QColor, QImage, QPainter
from qgis.PyQt.QtXml import QDomDocument
from qgis.core import (
Qgis,
QgsAnnotationItemEditOperationAddNode,
QgsAnnotationItemEditOperationDeleteNode,
QgsAnnotationItemEditOperationMoveNode,
QgsAnnotationItemEditOperationTranslateItem,
QgsAnnotationItemNode,
QgsAnnotationLineTextItem,
QgsCoordinateReferenceSystem,
QgsCoordinateTransform,
QgsMapSettings,
QgsPoint,
QgsPointXY,
QgsProject,
QgsReadWriteContext,
QgsRectangle,
QgsRenderChecker,
QgsRenderContext,
QgsTextFormat,
QgsVertexId,
QgsLineString,
)
from qgis.testing import start_app, unittest
from utilities import getTestFont, unitTestDataPath
start_app()
TEST_DATA_DIR = unitTestDataPath()
class TestQgsAnnotationLineTextItem(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.report = "<h1>Python QgsAnnotationLineTextItem Tests</h1>\n"
@classmethod
def tearDownClass(cls):
report_file_path = f"{QDir.tempPath()}/qgistest.html"
with open(report_file_path, 'a') as report_file:
report_file.write(cls.report)
def testBasic(self):
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
self.assertEqual(item.text(), 'my text')
self.assertEqual(item.geometry().asWkt(1), 'LineString (12 13, 13 13.1, 14 13)')
item.setText('tttttt')
item.setGeometry(QgsLineString(((12, 13), (13, 13.1))))
item.setZIndex(11)
format = QgsTextFormat()
format.setSize(37)
item.setFormat(format)
self.assertEqual(item.text(), 'tttttt')
self.assertEqual(item.geometry().asWkt(1), 'LineString (12 13, 13 13.1)')
self.assertEqual(item.zIndex(), 11)
self.assertEqual(item.format().size(), 37)
def test_nodes(self):
"""
Test nodes for item
"""
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
self.assertEqual(item.nodes(), [
QgsAnnotationItemNode(QgsVertexId(0, 0, 0), QgsPointXY(12, 13),
Qgis.AnnotationItemNodeType.VertexHandle),
QgsAnnotationItemNode(QgsVertexId(0, 0, 1), QgsPointXY(13, 13.1),
Qgis.AnnotationItemNodeType.VertexHandle),
QgsAnnotationItemNode(QgsVertexId(0, 0, 2), QgsPointXY(14, 13),
Qgis.AnnotationItemNodeType.VertexHandle)
])
def test_transform(self):
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
self.assertEqual(item.geometry().asWkt(1), 'LineString (12 13, 13 13.1, 14 13)')
self.assertEqual(item.applyEdit(QgsAnnotationItemEditOperationTranslateItem('', 100, 200)), Qgis.AnnotationItemEditOperationResult.Success)
self.assertEqual(item.geometry().asWkt(1), 'LineString (112 213, 113 213.1, 114 213)')
def test_apply_move_node_edit(self):
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
self.assertEqual(item.geometry().asWkt(1), 'LineString (12 13, 13 13.1, 14 13)')
self.assertEqual(item.applyEdit(QgsAnnotationItemEditOperationMoveNode('', QgsVertexId(0, 0, 1), QgsPoint(13, 13.1), QgsPoint(17, 18))), Qgis.AnnotationItemEditOperationResult.Success)
self.assertEqual(item.geometry().asWkt(1), 'LineString (12 13, 17 18, 14 13)')
def test_transient_move_operation(self):
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
self.assertEqual(item.geometry().asWkt(1), 'LineString (12 13, 13 13.1, 14 13)')
res = item.transientEditResults(QgsAnnotationItemEditOperationMoveNode('', QgsVertexId(0, 0, 1), QgsPoint(13, 13.1), QgsPoint(17, 18)))
self.assertEqual(res.representativeGeometry().asWkt(1), 'LineString (12 13, 17 18, 14 13)')
def test_transient_translate_operation(self):
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
self.assertEqual(item.geometry().asWkt(1), 'LineString (12 13, 13 13.1, 14 13)')
res = item.transientEditResults(QgsAnnotationItemEditOperationTranslateItem('', 100, 200))
self.assertEqual(res.representativeGeometry().asWkt(1), 'LineString (112 213, 113 213.1, 114 213)')
def test_apply_delete_node_edit(self):
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
self.assertEqual(item.geometry().asWkt(1), 'LineString (12 13, 13 13.1, 14 13)')
self.assertEqual(item.applyEdit(QgsAnnotationItemEditOperationDeleteNode('', QgsVertexId(0, 0, 1), QgsPoint(13, 13.1))), Qgis.AnnotationItemEditOperationResult.Success)
self.assertEqual(item.geometry().asWkt(1),
'LineString (12 13, 14 13)')
self.assertEqual(item.applyEdit(QgsAnnotationItemEditOperationDeleteNode('', QgsVertexId(0, 0, 1), QgsPoint(14, 13))), Qgis.AnnotationItemEditOperationResult.ItemCleared)
def test_apply_add_node_edit(self):
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
self.assertEqual(item.applyEdit(QgsAnnotationItemEditOperationAddNode('', QgsPoint(12.5, 12.8))), Qgis.AnnotationItemEditOperationResult.Success)
self.assertEqual(item.geometry().asWkt(1),
'LineString (12 13, 12.5 13, 13 13.1, 14 13)')
def testReadWriteXml(self):
doc = QDomDocument("testdoc")
elem = doc.createElement('test')
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
item.setZIndex(11)
format = QgsTextFormat()
format.setSize(37)
item.setFormat(format)
item.setUseSymbologyReferenceScale(True)
item.setSymbologyReferenceScale(5000)
self.assertTrue(item.writeXml(elem, doc, QgsReadWriteContext()))
s2 = QgsAnnotationLineTextItem.create()
self.assertTrue(s2.readXml(elem, QgsReadWriteContext()))
self.assertEqual(s2.text(), 'my text')
self.assertEqual(s2.geometry().asWkt(1), 'LineString (12 13, 13 13.1, 14 13)')
self.assertEqual(s2.zIndex(), 11)
self.assertEqual(s2.format().size(), 37)
self.assertTrue(s2.useSymbologyReferenceScale())
self.assertEqual(s2.symbologyReferenceScale(), 5000)
def testClone(self):
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 13))))
item.setZIndex(11)
format = QgsTextFormat()
format.setSize(37)
item.setFormat(format)
item.setUseSymbologyReferenceScale(True)
item.setSymbologyReferenceScale(5000)
item2 = item.clone()
self.assertEqual(item2.text(), 'my text')
self.assertEqual(item2.geometry().asWkt(1), 'LineString (12 13, 13 13.1, 14 13)')
self.assertEqual(item2.zIndex(), 11)
self.assertEqual(item2.format().size(), 37)
self.assertTrue(item2.useSymbologyReferenceScale())
self.assertEqual(item2.symbologyReferenceScale(), 5000)
def testRenderLine(self):
item = QgsAnnotationLineTextItem('my text', QgsLineString(((12, 13), (13, 13.1), (14, 12))))
format = QgsTextFormat.fromQFont(getTestFont('Bold'))
format.setColor(QColor(255, 0, 0))
format.setOpacity(150 / 255)
format.setSize(55)
item.setFormat(format)
settings = QgsMapSettings()
settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:4326'))
settings.setExtent(QgsRectangle(11.9, 11.9, 14.5, 14))
settings.setOutputSize(QSize(600, 300))
settings.setFlag(QgsMapSettings.Antialiasing, False)
rc = QgsRenderContext.fromMapSettings(settings)
image = QImage(600, 300, QImage.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.imageCheck('linetext_item', 'linetext_item', image))
def testRenderLineTextExpression(self):
item = QgsAnnotationLineTextItem('[% 1 + 1.5 %]', QgsLineString(((12, 13), (13, 13.1), (14, 12))))
format = QgsTextFormat.fromQFont(getTestFont('Bold'))
format.setColor(QColor(255, 0, 0))
format.setOpacity(150 / 255)
format.setSize(55)
item.setFormat(format)
settings = QgsMapSettings()
settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:4326'))
settings.setExtent(QgsRectangle(11.9, 11.9, 14.5, 14))
settings.setOutputSize(QSize(600, 300))
settings.setFlag(QgsMapSettings.Antialiasing, False)
rc = QgsRenderContext.fromMapSettings(settings)
image = QImage(600, 300, QImage.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.imageCheck('linetext_item_expression', 'linetext_item_expression', image))
def testRenderWithTransform(self):
item = QgsAnnotationLineTextItem('my text', QgsLineString(
((12, 13), (13, 13.1), (14, 12))))
format = QgsTextFormat.fromQFont(getTestFont('Bold'))
format.setColor(QColor(255, 0, 0))
format.setOpacity(150 / 255)
format.setSize(55)
item.setFormat(format)
settings = QgsMapSettings()
settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:3857'))
settings.setExtent(QgsRectangle(1291958, 1386945, 1420709, 1532518))
settings.setOutputSize(QSize(600, 300))
settings.setFlag(QgsMapSettings.Antialiasing, False)
rc = QgsRenderContext.fromMapSettings(settings)
rc.setCoordinateTransform(QgsCoordinateTransform(QgsCoordinateReferenceSystem('EPSG:4326'), settings.destinationCrs(), QgsProject.instance()))
image = QImage(600, 300, QImage.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.imageCheck('linetext_item_transform', 'linetext_item_transform', image))
def imageCheck(self, name, reference_image, image):
TestQgsAnnotationLineTextItem.report += f"<h2>Render {name}</h2>\n"
temp_dir = QDir.tempPath() + '/'
file_name = temp_dir + 'patch_' + name + ".png"
image.save(file_name, "PNG")
checker = QgsRenderChecker()
checker.setControlPathPrefix("annotation_layer")
checker.setControlName("expected_" + reference_image)
checker.setRenderedImage(file_name)
checker.setColorTolerance(2)
result = checker.compareImages(name, 20)
TestQgsAnnotationLineTextItem.report += checker.report()
return result
if __name__ == '__main__':
unittest.main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB