Skip to content
Permalink
Browse files
[api] Annotation items can have their symbology reference scale set
This is especially important for annotation items, where users will
want to create text items with text which scales up and down with
the map.
  • Loading branch information
nyalldawson committed Sep 7, 2021
1 parent 837dd24 commit 46f1957ceecb1f7e688cc98922e9bb0b464962be
@@ -135,6 +135,60 @@ are rendered in the layer.
Returns the nodes for the item, used for editing the item.

.. versionadded:: 3.22
%End

bool useSymbologyReferenceScale() const;
%Docstring
Returns ``True`` if the annotation item uses a symbology reference scale.

.. seealso:: :py:func:`setUseSymbologyReferenceScale`

.. seealso:: :py:func:`symbologyReferenceScale`
%End

void setUseSymbologyReferenceScale( bool enabled );
%Docstring
Sets whether the annotation item uses a symbology reference scale.

.. seealso:: :py:func:`useSymbologyReferenceScale`

.. seealso:: :py:func:`setSymbologyReferenceScale`
%End

double symbologyReferenceScale() const;
%Docstring
Returns the annotation's symbology reference scale.

The reference scale will only be used if :py:func:`~QgsAnnotationItem.useSymbologyReferenceScale` returns ``True``.

This represents the desired scale denominator for the rendered map, eg 1000.0 for a 1:1000 map render.

The symbology reference scale is an optional property which specifies the reference
scale at which symbology in paper units (such a millimeters or points) is fixed
to. For instance, if the scale is 1000 then a 2mm thick line will be rendered at
exactly 2mm thick when a map is rendered at 1:1000, or 1mm thick when rendered at 1:2000, or 4mm thick at 1:500.

.. seealso:: :py:func:`setSymbologyReferenceScale`

.. seealso:: :py:func:`useSymbologyReferenceScale`
%End

void setSymbologyReferenceScale( double scale );
%Docstring
Sets the annotation's symbology reference ``scale``.

The reference scale will only be used if :py:func:`~QgsAnnotationItem.useSymbologyReferenceScale` returns ``True``.

This represents the desired scale denominator for the rendered map, eg 1000.0 for a 1:1000 map render.

The symbology reference scale is an optional property which specifies the reference
scale at which symbology in paper units (such a millimeters or points) is fixed
to. For instance, if the scale is 1000 then a 2mm thick line will be rendered at
exactly 2mm thick when a map is rendered at 1:1000, or 1mm thick when rendered at 1:2000, or 4mm thick at 1:500.

.. seealso:: :py:func:`symbologyReferenceScale`

.. seealso:: :py:func:`setUseSymbologyReferenceScale`
%End

protected:
@@ -31,16 +31,22 @@ QList<QgsAnnotationItemNode> QgsAnnotationItem::nodes() const
void QgsAnnotationItem::copyCommonProperties( const QgsAnnotationItem *other )
{
setZIndex( other->zIndex() );
setUseSymbologyReferenceScale( other->useSymbologyReferenceScale() );
setSymbologyReferenceScale( other->symbologyReferenceScale() );
}

bool QgsAnnotationItem::writeCommonProperties( QDomElement &element, QDomDocument &, const QgsReadWriteContext & ) const
{
element.setAttribute( QStringLiteral( "zIndex" ), zIndex() );
element.setAttribute( QStringLiteral( "useReferenceScale" ), useSymbologyReferenceScale() ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
element.setAttribute( QStringLiteral( "referenceScale" ), qgsDoubleToString( symbologyReferenceScale() ) );
return true;
}

bool QgsAnnotationItem::readCommonProperties( const QDomElement &element, const QgsReadWriteContext & )
{
setZIndex( element.attribute( QStringLiteral( "zIndex" ) ).toInt() );
setUseSymbologyReferenceScale( element.attribute( QStringLiteral( "useReferenceScale" ), QStringLiteral( "0" ) ).toInt() );
setSymbologyReferenceScale( element.attribute( QStringLiteral( "referenceScale" ) ).toDouble() );
return true;
}
@@ -162,6 +162,56 @@ class CORE_EXPORT QgsAnnotationItem
*/
virtual QList< QgsAnnotationItemNode > nodes() const;

/**
* Returns TRUE if the annotation item uses a symbology reference scale.
*
* \see setUseSymbologyReferenceScale()
* \see symbologyReferenceScale()
*/
bool useSymbologyReferenceScale() const { return mUseReferenceScale; }

/**
* Sets whether the annotation item uses a symbology reference scale.
*
* \see useSymbologyReferenceScale()
* \see setSymbologyReferenceScale()
*/
void setUseSymbologyReferenceScale( bool enabled ) { mUseReferenceScale = enabled; }

/**
* Returns the annotation's symbology reference scale.
*
* The reference scale will only be used if useSymbologyReferenceScale() returns TRUE.
*
* This represents the desired scale denominator for the rendered map, eg 1000.0 for a 1:1000 map render.
*
* The symbology reference scale is an optional property which specifies the reference
* scale at which symbology in paper units (such a millimeters or points) is fixed
* to. For instance, if the scale is 1000 then a 2mm thick line will be rendered at
* exactly 2mm thick when a map is rendered at 1:1000, or 1mm thick when rendered at 1:2000, or 4mm thick at 1:500.
*
* \see setSymbologyReferenceScale()
* \see useSymbologyReferenceScale()
*/
double symbologyReferenceScale() const { return mReferenceScale; }

/**
* Sets the annotation's symbology reference \a scale.
*
* The reference scale will only be used if useSymbologyReferenceScale() returns TRUE.
*
* This represents the desired scale denominator for the rendered map, eg 1000.0 for a 1:1000 map render.
*
* The symbology reference scale is an optional property which specifies the reference
* scale at which symbology in paper units (such a millimeters or points) is fixed
* to. For instance, if the scale is 1000 then a 2mm thick line will be rendered at
* exactly 2mm thick when a map is rendered at 1:1000, or 1mm thick when rendered at 1:2000, or 4mm thick at 1:500.
*
* \see symbologyReferenceScale()
* \see setUseSymbologyReferenceScale()
*/
void setSymbologyReferenceScale( double scale ) { mReferenceScale = scale; }

protected:

/**
@@ -191,6 +241,9 @@ class CORE_EXPORT QgsAnnotationItem

int mZIndex = 0;

bool mUseReferenceScale = false;
double mReferenceScale = 0;

#ifdef SIP_RUN
QgsAnnotationItem( const QgsAnnotationItem &other );
#endif
@@ -18,6 +18,7 @@
#include "qgsannotationlayer.h"
#include "qgsfeedback.h"
#include "qgsrenderedannotationitemdetails.h"
#include <optional>

QgsAnnotationLayerRenderer::QgsAnnotationLayerRenderer( QgsAnnotationLayer *layer, QgsRenderContext &context )
: QgsMapLayerRenderer( layer->id(), &context )
@@ -74,6 +75,12 @@ bool QgsAnnotationLayerRenderer::render()
const QgsRectangle bounds = item.second->boundingBox( context );
if ( bounds.intersects( context.extent() ) )
{
std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
if ( item.second->useSymbologyReferenceScale() )
{
referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, item.second->symbologyReferenceScale() ) );
}

item.second->render( context, mFeedback.get() );
std::unique_ptr< QgsRenderedAnnotationItemDetails > details = std::make_unique< QgsRenderedAnnotationItemDetails >( mLayerID, item.first );
details->setBoundingBox( bounds );
@@ -375,6 +375,71 @@ def testRenderWithTransform(self):
self.assertEqual([i.boundingBox().toString(2) for i in item_details if i.itemId() == i3_id][0],
'11.94,12.94 : 12.06,13.06')

def testRenderLayerWithReferenceScale(self):
layer = QgsAnnotationLayer('test', QgsAnnotationLayer.LayerOptions(QgsProject.instance().transformContext()))
layer.setCrs(QgsCoordinateReferenceSystem('EPSG:4326'))
self.assertTrue(layer.isValid())

item = QgsAnnotationPolygonItem(
QgsPolygon(QgsLineString([QgsPoint(12, 13), QgsPoint(14, 13), QgsPoint(14, 15), QgsPoint(12, 13)])))
item.setSymbol(
QgsFillSymbol.createSimple({'color': '200,100,100', 'outline_color': 'black', 'outline_width': '2'}))
item.setZIndex(3)
i1_id = layer.addItem(item)

item = QgsAnnotationLineItem(QgsLineString([QgsPoint(11, 13), QgsPoint(12, 13), QgsPoint(12, 15)]))
item.setSymbol(QgsLineSymbol.createSimple({'color': '#ffff00', 'line_width': '3'}))
item.setZIndex(2)
i2_id = layer.addItem(item)

item = QgsAnnotationMarkerItem(QgsPoint(12, 13))
item.setSymbol(QgsMarkerSymbol.createSimple({'color': '100,200,200', 'size': '6', 'outline_color': 'black'}))
item.setZIndex(1)
i3_id = layer.addItem(item)

settings = QgsMapSettings()
settings.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:4326'))
settings.setExtent(QgsRectangle(10, 10, 18, 18))
settings.setOutputSize(QSize(300, 300))

settings.setFlag(QgsMapSettings.Antialiasing, False)

rc = QgsRenderContext.fromMapSettings(settings)

layer.item(i1_id).setUseSymbologyReferenceScale(True)
layer.item(i1_id).setSymbologyReferenceScale(rc.rendererScale() * 2)
# note item 3 has use symbology reference scale set to false, so should be ignored
layer.item(i2_id).setUseSymbologyReferenceScale(False)
layer.item(i2_id).setSymbologyReferenceScale(rc.rendererScale() * 2)
layer.item(i3_id).setUseSymbologyReferenceScale(True)
layer.item(i3_id).setSymbologyReferenceScale(rc.rendererScale() * 2)

image = QImage(200, 200, QImage.Format_ARGB32)
image.setDotsPerMeterX(96 / 25.4 * 1000)
image.setDotsPerMeterY(96 / 25.4 * 1000)
image.fill(QColor(255, 255, 255))
painter = QPainter(image)
rc.setPainter(painter)

try:
renderer = layer.createMapRenderer(rc)
renderer.render()
finally:
painter.end()

self.assertTrue(self.imageCheck('layer_render_reference_scale', 'layer_render_reference_scale', image))

# also check details of rendered items
item_details = renderer.takeRenderedItemDetails()
self.assertEqual([i.layerId() for i in item_details], [layer.id()] * 3)
self.assertCountEqual([i.itemId() for i in item_details], [i1_id, i2_id, i3_id])
self.assertEqual([i.boundingBox() for i in item_details if i.itemId() == i1_id][0],
QgsRectangle(12, 13, 14, 15))
self.assertEqual([i.boundingBox() for i in item_details if i.itemId() == i2_id][0],
QgsRectangle(11, 13, 12, 15))
self.assertEqual([i.boundingBox().toString(1) for i in item_details if i.itemId() == i3_id][0],
'11.7,12.7 : 12.3,13.3')

def test_render_via_job(self):
"""
Test rendering an annotation layer via a map render job
@@ -86,6 +86,8 @@ def testReadWriteXml(self):
item = QgsAnnotationLineItem(QgsLineString([QgsPoint(12, 13), QgsPoint(14, 13), QgsPoint(14, 15)]))
item.setSymbol(QgsLineSymbol.createSimple({'color': '#ffff00', 'line_width': '3'}))
item.setZIndex(11)
item.setUseSymbologyReferenceScale(True)
item.setSymbologyReferenceScale(5000)

self.assertTrue(item.writeXml(elem, doc, QgsReadWriteContext()))

@@ -95,16 +97,22 @@ def testReadWriteXml(self):
self.assertEqual(s2.geometry().asWkt(), 'LineString (12 13, 14 13, 14 15)')
self.assertEqual(s2.symbol()[0].color(), QColor(255, 255, 0))
self.assertEqual(s2.zIndex(), 11)
self.assertTrue(s2.useSymbologyReferenceScale())
self.assertEqual(s2.symbologyReferenceScale(), 5000)

def testClone(self):
item = QgsAnnotationLineItem(QgsLineString([QgsPoint(12, 13), QgsPoint(14, 13), QgsPoint(14, 15)]))
item.setSymbol(QgsLineSymbol.createSimple({'color': '#ffff00', 'line_width': '3'}))
item.setZIndex(11)
item.setUseSymbologyReferenceScale(True)
item.setSymbologyReferenceScale(5000)

item2 = item.clone()
self.assertEqual(item2.geometry().asWkt(), 'LineString (12 13, 14 13, 14 15)')
self.assertEqual(item2.symbol()[0].color(), QColor(255, 255, 0))
self.assertEqual(item2.zIndex(), 11)
self.assertTrue(item2.useSymbologyReferenceScale())
self.assertEqual(item2.symbologyReferenceScale(), 5000)

def testRenderLineString(self):
item = QgsAnnotationLineItem(QgsLineString([QgsPoint(12, 13), QgsPoint(14, 13), QgsPoint(14, 15)]))
@@ -84,6 +84,8 @@ def testReadWriteXml(self):
item = QgsAnnotationMarkerItem(QgsPoint(12, 13))
item.setSymbol(QgsMarkerSymbol.createSimple({'color': '100,200,200', 'size': '3', 'outline_color': 'black'}))
item.setZIndex(11)
item.setUseSymbologyReferenceScale(True)
item.setSymbologyReferenceScale(5000)

self.assertTrue(item.writeXml(elem, doc, QgsReadWriteContext()))

@@ -94,17 +96,23 @@ def testReadWriteXml(self):
self.assertEqual(s2.geometry().y(), 13.0)
self.assertEqual(s2.symbol()[0].color(), QColor(100, 200, 200))
self.assertEqual(s2.zIndex(), 11)
self.assertTrue(s2.useSymbologyReferenceScale())
self.assertEqual(s2.symbologyReferenceScale(), 5000)

def testClone(self):
item = QgsAnnotationMarkerItem(QgsPoint(12, 13))
item.setSymbol(QgsMarkerSymbol.createSimple({'color': '100,200,200', 'size': '3', 'outline_color': 'black'}))
item.setZIndex(11)
item.setUseSymbologyReferenceScale(True)
item.setSymbologyReferenceScale(5000)

item2 = item.clone()
self.assertEqual(item2.geometry().x(), 12.0)
self.assertEqual(item2.geometry().y(), 13.0)
self.assertEqual(item2.symbol()[0].color(), QColor(100, 200, 200))
self.assertEqual(item2.zIndex(), 11)
self.assertTrue(item2.useSymbologyReferenceScale())
self.assertEqual(item2.symbologyReferenceScale(), 5000)

def testRenderMarker(self):
item = QgsAnnotationMarkerItem(QgsPoint(12, 13))
@@ -99,6 +99,8 @@ def testReadWriteXml(self):
format = QgsTextFormat()
format.setSize(37)
item.setFormat(format)
item.setUseSymbologyReferenceScale(True)
item.setSymbologyReferenceScale(5000)

self.assertTrue(item.writeXml(elem, doc, QgsReadWriteContext()))

@@ -111,6 +113,8 @@ def testReadWriteXml(self):
self.assertEqual(s2.alignment(), Qt.AlignRight)
self.assertEqual(s2.zIndex(), 11)
self.assertEqual(s2.format().size(), 37)
self.assertTrue(s2.useSymbologyReferenceScale())
self.assertEqual(s2.symbologyReferenceScale(), 5000)

def testClone(self):
item = QgsAnnotationPointTextItem('my text', QgsPointXY(12, 13))
@@ -120,6 +124,8 @@ def testClone(self):
format = QgsTextFormat()
format.setSize(37)
item.setFormat(format)
item.setUseSymbologyReferenceScale(True)
item.setSymbologyReferenceScale(5000)

item2 = item.clone()
self.assertEqual(item2.text(), 'my text')
@@ -129,6 +135,8 @@ def testClone(self):
self.assertEqual(item2.alignment(), Qt.AlignRight)
self.assertEqual(item2.zIndex(), 11)
self.assertEqual(item2.format().size(), 37)
self.assertTrue(item2.useSymbologyReferenceScale())
self.assertEqual(item2.symbologyReferenceScale(), 5000)

def testRenderMarker(self):
item = QgsAnnotationPointTextItem('my text', QgsPointXY(12.3, 13.2))
@@ -89,6 +89,8 @@ def testReadWriteXml(self):
item = QgsAnnotationPolygonItem(QgsPolygon(QgsLineString([QgsPoint(12, 13), QgsPoint(14, 13), QgsPoint(14, 15), QgsPoint(12, 13)])))
item.setSymbol(QgsFillSymbol.createSimple({'color': '200,100,100', 'outline_color': 'black'}))
item.setZIndex(11)
item.setUseSymbologyReferenceScale(True)
item.setSymbologyReferenceScale(5000)

self.assertTrue(item.writeXml(elem, doc, QgsReadWriteContext()))

@@ -98,16 +100,22 @@ def testReadWriteXml(self):
self.assertEqual(s2.geometry().asWkt(), 'Polygon ((12 13, 14 13, 14 15, 12 13))')
self.assertEqual(s2.symbol()[0].color(), QColor(200, 100, 100))
self.assertEqual(s2.zIndex(), 11)
self.assertTrue(s2.useSymbologyReferenceScale())
self.assertEqual(s2.symbologyReferenceScale(), 5000)

def testClone(self):
item = QgsAnnotationPolygonItem(QgsPolygon(QgsLineString([QgsPoint(12, 13), QgsPoint(14, 13), QgsPoint(14, 15), QgsPoint(12, 13)])))
item.setSymbol(QgsFillSymbol.createSimple({'color': '200,100,100', 'outline_color': 'black'}))
item.setZIndex(11)
item.setUseSymbologyReferenceScale(True)
item.setSymbologyReferenceScale(5000)

item2 = item.clone()
self.assertEqual(item2.geometry().asWkt(), 'Polygon ((12 13, 14 13, 14 15, 12 13))')
self.assertEqual(item2.symbol()[0].color(), QColor(200, 100, 100))
self.assertEqual(item2.zIndex(), 11)
self.assertTrue(item2.useSymbologyReferenceScale())
self.assertEqual(item2.symbologyReferenceScale(), 5000)

def testRenderPolygon(self):
item = QgsAnnotationPolygonItem(QgsPolygon(QgsLineString([QgsPoint(12, 13), QgsPoint(14, 13), QgsPoint(14, 15), QgsPoint(12, 13)])))
Binary file not shown.

0 comments on commit 46f1957

Please sign in to comment.