Skip to content

Commit

Permalink
[api] Allow a symbology reference scale to be set on a QgsRenderContext
Browse files Browse the repository at this point in the history
This should match the desired scale denominator for the rendered map,
eg 1000.0 for a 1:1000 map render. Set (by default) to -1 to disable
symbology scaling by reference scale.

The symbology reference scale is an optional property which specifies
the reference scale at which symbology in paper units (such as
millimeters or points) is fixed to. For instance, if \a scale is set
to 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.
  • Loading branch information
nyalldawson committed Jun 28, 2021
1 parent 23048dd commit 54610b2
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 8 deletions.
39 changes: 39 additions & 0 deletions python/core/auto_generated/qgsrendercontext.sip.in
Expand Up @@ -335,6 +335,26 @@ for the rendered map, eg 1000.0 for a 1:1000 map render.
%End


double symbologyReferenceScale() const;
%Docstring
Returns the symbology reference ``scale``.

This represents the desired scale denominator for the rendered map, eg 1000.0 for a 1:1000 map render.
A value of -1 indicates that symbology scaling by reference scale is disabled.

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:`rendererScale`

.. versionadded:: 3.22
%End


QColor selectionColor() const;
%Docstring
Returns the color to use when rendering selected features.
Expand Down Expand Up @@ -451,6 +471,25 @@ Sets the renderer map scale. This should match the desired scale denominator
for the rendered map, eg 1000.0 for a 1:1000 map render.

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

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

This should match the desired scale denominator for the rendered map, eg 1000.0 for a 1:1000 map render.
Set to -1 to disable symbology scaling by reference scale.

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 ``scale`` is set to 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:`rendererScale`

.. versionadded:: 3.22
%End

void setPainter( QPainter *p );
Expand Down
24 changes: 16 additions & 8 deletions src/core/qgsrendercontext.cpp
Expand Up @@ -53,6 +53,7 @@ QgsRenderContext::QgsRenderContext( const QgsRenderContext &rh )
, mScaleFactor( rh.mScaleFactor )
, mDpiTarget( rh.mDpiTarget )
, mRendererScale( rh.mRendererScale )
, mSymbologyReferenceScale( rh.mSymbologyReferenceScale )
, mLabelingEngine( rh.mLabelingEngine )
, mSelectionColor( rh.mSelectionColor )
, mVectorSimplifyMethod( rh.mVectorSimplifyMethod )
Expand Down Expand Up @@ -91,6 +92,7 @@ QgsRenderContext &QgsRenderContext::operator=( const QgsRenderContext &rh )
mScaleFactor = rh.mScaleFactor;
mDpiTarget = rh.mDpiTarget;
mRendererScale = rh.mRendererScale;
mSymbologyReferenceScale = rh.mSymbologyReferenceScale;
mLabelingEngine = rh.mLabelingEngine;
mSelectionColor = rh.mSelectionColor;
mVectorSimplifyMethod = rh.mVectorSimplifyMethod;
Expand Down Expand Up @@ -381,13 +383,18 @@ double QgsRenderContext::convertToPainterUnits( double size, QgsUnitTypes::Rende
convertedSize = std::min( convertedSize, scale.maxSizeMM * mScaleFactor );
}

const double symbologyReferenceScaleFactor = mSymbologyReferenceScale > 1 ? mSymbologyReferenceScale / mRendererScale : 1;
convertedSize *= symbologyReferenceScaleFactor;

return convertedSize;
}

double QgsRenderContext::convertToMapUnits( double size, QgsUnitTypes::RenderUnit unit, const QgsMapUnitScale &scale ) const
{
double mup = mMapToPixel.mapUnitsPerPixel();

const double symbologyReferenceScaleFactor = mSymbologyReferenceScale > 1 ? mSymbologyReferenceScale / mRendererScale : 1;

switch ( unit )
{
case QgsUnitTypes::RenderMetersInMapUnits:
Expand Down Expand Up @@ -425,19 +432,19 @@ double QgsRenderContext::convertToMapUnits( double size, QgsUnitTypes::RenderUni
}
case QgsUnitTypes::RenderMillimeters:
{
return size * mScaleFactor * mup;
return size * mScaleFactor * mup / symbologyReferenceScaleFactor;
}
case QgsUnitTypes::RenderPoints:
{
return size * mScaleFactor * mup / POINTS_TO_MM;
return size * mScaleFactor * mup / POINTS_TO_MM / symbologyReferenceScaleFactor;
}
case QgsUnitTypes::RenderInches:
{
return size * mScaleFactor * mup * INCH_TO_MM;
return size * mScaleFactor * mup * INCH_TO_MM / symbologyReferenceScaleFactor;
}
case QgsUnitTypes::RenderPixels:
{
return size * mup;
return size * mup / symbologyReferenceScaleFactor;
}

case QgsUnitTypes::RenderUnknownUnit:
Expand All @@ -451,6 +458,7 @@ double QgsRenderContext::convertToMapUnits( double size, QgsUnitTypes::RenderUni
double QgsRenderContext::convertFromMapUnits( double sizeInMapUnits, QgsUnitTypes::RenderUnit outputUnit ) const
{
double mup = mMapToPixel.mapUnitsPerPixel();
const double symbologyReferenceScaleFactor = mSymbologyReferenceScale > 1 ? mSymbologyReferenceScale / mRendererScale : 1;

switch ( outputUnit )
{
Expand All @@ -464,19 +472,19 @@ double QgsRenderContext::convertFromMapUnits( double sizeInMapUnits, QgsUnitType
}
case QgsUnitTypes::RenderMillimeters:
{
return sizeInMapUnits / ( mScaleFactor * mup );
return sizeInMapUnits / ( mScaleFactor * mup ) * symbologyReferenceScaleFactor;
}
case QgsUnitTypes::RenderPoints:
{
return sizeInMapUnits / ( mScaleFactor * mup / POINTS_TO_MM );
return sizeInMapUnits / ( mScaleFactor * mup / POINTS_TO_MM ) * symbologyReferenceScaleFactor;
}
case QgsUnitTypes::RenderInches:
{
return sizeInMapUnits / ( mScaleFactor * mup * INCH_TO_MM );
return sizeInMapUnits / ( mScaleFactor * mup * INCH_TO_MM ) * symbologyReferenceScaleFactor;
}
case QgsUnitTypes::RenderPixels:
{
return sizeInMapUnits / mup;
return sizeInMapUnits / mup * symbologyReferenceScaleFactor;
}

case QgsUnitTypes::RenderUnknownUnit:
Expand Down
37 changes: 37 additions & 0 deletions src/core/qgsrendercontext.h
Expand Up @@ -384,6 +384,24 @@ class CORE_EXPORT QgsRenderContext : public QgsTemporalRangeObject
*/
double rendererScale() const {return mRendererScale;}


/**
* Returns the symbology reference \a scale.
*
* This represents the desired scale denominator for the rendered map, eg 1000.0 for a 1:1000 map render.
* A value of -1 indicates that symbology scaling by reference scale is disabled.
*
* 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 rendererScale()
* \since QGIS 3.22
*/
double symbologyReferenceScale() const { return mSymbologyReferenceScale; }

/**
* Gets access to new labeling engine (may be NULLPTR)
* \note not available in Python bindings
Expand Down Expand Up @@ -498,6 +516,23 @@ class CORE_EXPORT QgsRenderContext : public QgsTemporalRangeObject
*/
void setRendererScale( double scale ) {mRendererScale = scale;}

/**
* Sets the symbology reference \a scale.
*
* This should match the desired scale denominator for the rendered map, eg 1000.0 for a 1:1000 map render.
* Set to -1 to disable symbology scaling by reference scale.
*
* 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 \a scale is set to 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 rendererScale()
* \since QGIS 3.22
*/
void setSymbologyReferenceScale( double scale ) { mSymbologyReferenceScale = scale; }

/**
* Sets the destination QPainter for the render operation. Ownership of the painter
* is not transferred and the QPainter destination must stay alive for the duration
Expand Down Expand Up @@ -949,6 +984,8 @@ class CORE_EXPORT QgsRenderContext : public QgsTemporalRangeObject
//! Map scale
double mRendererScale = 1.0;

double mSymbologyReferenceScale = -1;

//! Newer labeling engine implementation (can be NULLPTR)
QgsLabelingEngine *mLabelingEngine = nullptr;

Expand Down
106 changes: 106 additions & 0 deletions tests/src/python/test_qgsrendercontext.py
Expand Up @@ -63,6 +63,10 @@ def testGettersSetters(self):
c.setZRange(QgsDoubleRange(1, 10))
self.assertEqual(c.zRange(), QgsDoubleRange(1, 10))

self.assertEqual(c.symbologyReferenceScale(), -1)
c.setSymbologyReferenceScale(1000)
self.assertEqual(c.symbologyReferenceScale(), 1000)

def testCopyConstructor(self):
"""
Test the copy constructor
Expand All @@ -72,11 +76,13 @@ def testCopyConstructor(self):
c1.setTextRenderFormat(QgsRenderContext.TextFormatAlwaysText)
c1.setMapExtent(QgsRectangle(1, 2, 3, 4))
c1.setZRange(QgsDoubleRange(1, 10))
c1.setSymbologyReferenceScale(1000)

c2 = QgsRenderContext(c1)
self.assertEqual(c2.textRenderFormat(), QgsRenderContext.TextFormatAlwaysText)
self.assertEqual(c2.mapExtent(), QgsRectangle(1, 2, 3, 4))
self.assertEqual(c2.zRange(), QgsDoubleRange(1, 10))
self.assertEqual(c2.symbologyReferenceScale(), 1000)

c1.setTextRenderFormat(QgsRenderContext.TextFormatAlwaysOutlines)
c2 = QgsRenderContext(c1)
Expand Down Expand Up @@ -145,6 +151,7 @@ def testFromMapSettings(self):
self.assertTrue(rc.testFlag(QgsRenderContext.LosslessImageRendering))
self.assertTrue(rc.testFlag(QgsRenderContext.Render3DMap))
self.assertEqual(ms.zRange(), QgsDoubleRange(1, 10))
self.assertEqual(rc.symbologyReferenceScale(), -1)

ms.setTextRenderFormat(QgsRenderContext.TextFormatAlwaysOutlines)
ms.setZRange(QgsDoubleRange())
Expand Down Expand Up @@ -304,6 +311,45 @@ def testConvertSingleUnit(self):
sf = r.convertToPainterUnits(1, QgsUnitTypes.RenderPixels, c)
self.assertAlmostEqual(sf, 1.0, places=5)

# with symbologyReferenceScale set
c = QgsMapUnitScale()
r.setSymbologyReferenceScale(1000)
r.setRendererScale(1000)
sf = r.convertToPainterUnits(1, QgsUnitTypes.RenderMapUnits, c)
self.assertAlmostEqual(sf, 0.5, places=5)
sf = r.convertToPainterUnits(1, QgsUnitTypes.RenderMillimeters, c)
self.assertAlmostEqual(sf, 11.8110236, places=5)
sf = r.convertToPainterUnits(1, QgsUnitTypes.RenderPoints, c)
self.assertAlmostEqual(sf, 4.166666665625, places=5)
sf = r.convertToPainterUnits(1, QgsUnitTypes.RenderInches, c)
self.assertAlmostEqual(sf, 300.0, places=5)
sf = r.convertToPainterUnits(1, QgsUnitTypes.RenderPixels, c)
self.assertAlmostEqual(sf, 1.0, places=5)

r.setRendererScale(2000)
sf = r.convertToPainterUnits(1, QgsUnitTypes.RenderMapUnits, c)
self.assertAlmostEqual(sf, 0.5 / 2, places=5)
sf = r.convertToPainterUnits(1, QgsUnitTypes.RenderMillimeters, c)
self.assertAlmostEqual(sf, 11.8110236 / 2, places=5)
sf = r.convertToPainterUnits(1, QgsUnitTypes.RenderPoints, c)
self.assertAlmostEqual(sf, 4.166666665625 / 2, places=5)
sf = r.convertToPainterUnits(1, QgsUnitTypes.RenderInches, c)
self.assertAlmostEqual(sf, 300.0 / 2, places=5)
sf = r.convertToPainterUnits(1, QgsUnitTypes.RenderPixels, c)
self.assertAlmostEqual(sf, 1.0 / 2, places=5)

r.setRendererScale(500)
sf = r.convertToPainterUnits(1, QgsUnitTypes.RenderMapUnits, c)
self.assertAlmostEqual(sf, 0.5 * 2, places=5)
sf = r.convertToPainterUnits(1, QgsUnitTypes.RenderMillimeters, c)
self.assertAlmostEqual(sf, 11.8110236 * 2, places=5)
sf = r.convertToPainterUnits(1, QgsUnitTypes.RenderPoints, c)
self.assertAlmostEqual(sf, 4.166666665625 * 2, places=5)
sf = r.convertToPainterUnits(1, QgsUnitTypes.RenderInches, c)
self.assertAlmostEqual(sf, 300.0 * 2, places=5)
sf = r.convertToPainterUnits(1, QgsUnitTypes.RenderPixels, c)
self.assertAlmostEqual(sf, 1.0 * 2, places=5)

def testConvertToPainterUnits(self):
ms = QgsMapSettings()
ms.setExtent(QgsRectangle(0, 0, 100, 100))
Expand Down Expand Up @@ -372,14 +418,19 @@ def testConvertToMapUnits(self):

size = r.convertToMapUnits(2, QgsUnitTypes.RenderMapUnits, c)
self.assertEqual(size, 2.0)
self.assertEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderMapUnits), 2)
size = r.convertToMapUnits(2, QgsUnitTypes.RenderMillimeters, c)
self.assertAlmostEqual(size, 47.244094, places=5)
self.assertEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderMillimeters), 2)
size = r.convertToMapUnits(5.66929, QgsUnitTypes.RenderPoints, c)
self.assertAlmostEqual(size, 47.2440833, places=5)
self.assertAlmostEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderPoints), 5.66929, 4)
size = r.convertToMapUnits(5.66929, QgsUnitTypes.RenderInches, c)
self.assertAlmostEqual(size, 3401.574, places=5)
self.assertAlmostEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderInches), 5.66929, 4)
size = r.convertToMapUnits(2, QgsUnitTypes.RenderPixels, c)
self.assertAlmostEqual(size, 4.0, places=5)
self.assertAlmostEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderPixels), 2, 4)

# minimum size greater than the calculated size, so size should be limited to minSizeMM
c.minSizeMM = 5
Expand Down Expand Up @@ -443,6 +494,61 @@ def testConvertToMapUnits(self):
self.assertAlmostEqual(size, 4.0, places=5)
c.maxScale = 0

# with symbology reference scale
c = QgsMapUnitScale()
r.setSymbologyReferenceScale(1000)
r.setRendererScale(1000)

size = r.convertToMapUnits(2, QgsUnitTypes.RenderMapUnits, c)
self.assertEqual(size, 2.0)
self.assertEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderMapUnits), 2)
size = r.convertToMapUnits(2, QgsUnitTypes.RenderMillimeters, c)
self.assertAlmostEqual(size, 47.244094, places=5)
self.assertEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderMillimeters), 2)
size = r.convertToMapUnits(5.66929, QgsUnitTypes.RenderPoints, c)
self.assertAlmostEqual(size, 47.2440833, places=5)
self.assertAlmostEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderPoints), 5.66929, 4)
size = r.convertToMapUnits(5.66929, QgsUnitTypes.RenderInches, c)
self.assertAlmostEqual(size, 3401.574, places=5)
self.assertAlmostEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderInches), 5.66929, 4)
size = r.convertToMapUnits(2, QgsUnitTypes.RenderPixels, c)
self.assertAlmostEqual(size, 4.0, places=5)
self.assertAlmostEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderPixels), 2, 4)

r.setRendererScale(2000)
size = r.convertToMapUnits(2, QgsUnitTypes.RenderMapUnits, c)
self.assertEqual(size, 2.0)
self.assertEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderMapUnits), 2)
size = r.convertToMapUnits(2, QgsUnitTypes.RenderMillimeters, c)
self.assertAlmostEqual(size, 47.244094 * 2, places=5)
self.assertEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderMillimeters), 2)
size = r.convertToMapUnits(5.66929, QgsUnitTypes.RenderPoints, c)
self.assertAlmostEqual(size, 47.2440833 * 2, places=5)
self.assertAlmostEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderPoints), 5.66929, 4)
size = r.convertToMapUnits(5.66929, QgsUnitTypes.RenderInches, c)
self.assertAlmostEqual(size, 3401.574 * 2, places=5)
self.assertAlmostEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderInches), 5.66929, 4)
size = r.convertToMapUnits(2, QgsUnitTypes.RenderPixels, c)
self.assertAlmostEqual(size, 4.0 * 2, places=5)
self.assertAlmostEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderPixels), 2, 4)

r.setRendererScale(500)
size = r.convertToMapUnits(2, QgsUnitTypes.RenderMapUnits, c)
self.assertEqual(size, 2.0)
self.assertEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderMapUnits), 2)
size = r.convertToMapUnits(2, QgsUnitTypes.RenderMillimeters, c)
self.assertAlmostEqual(size, 47.244094 / 2, places=5)
self.assertEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderMillimeters), 2)
size = r.convertToMapUnits(5.66929, QgsUnitTypes.RenderPoints, c)
self.assertAlmostEqual(size, 47.2440833 / 2, places=5)
self.assertAlmostEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderPoints), 5.66929, 4)
size = r.convertToMapUnits(5.66929, QgsUnitTypes.RenderInches, c)
self.assertAlmostEqual(size, 3401.574 / 2, places=5)
self.assertAlmostEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderInches), 5.66929, 4)
size = r.convertToMapUnits(2, QgsUnitTypes.RenderPixels, c)
self.assertAlmostEqual(size, 4.0 / 2, places=5)
self.assertAlmostEqual(r.convertFromMapUnits(size, QgsUnitTypes.RenderPixels), 2, 4)

def testPixelSizeScaleFactor(self):
ms = QgsMapSettings()
ms.setExtent(QgsRectangle(0, 0, 100, 100))
Expand Down

0 comments on commit 54610b2

Please sign in to comment.