Skip to content

Commit

Permalink
[api] Annotation items can have their symbology reference scale set
Browse files Browse the repository at this point in the history
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 46f1957
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 0 deletions.
54 changes: 54 additions & 0 deletions python/core/auto_generated/annotations/qgsannotationitem.sip.in
Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions src/core/annotations/qgsannotationitem.cpp
Expand Up @@ -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;
}
53 changes: 53 additions & 0 deletions src/core/annotations/qgsannotationitem.h
Expand Up @@ -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:

/**
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/core/annotations/qgsannotationlayerrenderer.cpp
Expand Up @@ -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 )
Expand Down Expand Up @@ -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 );
Expand Down
65 changes: 65 additions & 0 deletions tests/src/python/test_qgsannotationlayer.py
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions tests/src/python/test_qgsannotationlineitem.py
Expand Up @@ -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()))

Expand All @@ -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)]))
Expand Down
8 changes: 8 additions & 0 deletions tests/src/python/test_qgsannotationmarkeritem.py
Expand Up @@ -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()))

Expand All @@ -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))
Expand Down
8 changes: 8 additions & 0 deletions tests/src/python/test_qgsannotationpointtextitem.py
Expand Up @@ -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()))

Expand All @@ -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))
Expand All @@ -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')
Expand All @@ -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))
Expand Down
8 changes: 8 additions & 0 deletions tests/src/python/test_qgsannotationpolygonitem.py
Expand Up @@ -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()))

Expand All @@ -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)])))
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 46f1957

Please sign in to comment.