Skip to content
Permalink
Browse files

Add method to QgsAbstractGeometry to convert geometry to QPainterPath

Unlike QgsGeometry::asQPolygonF, this allows for correct handling
of multipolygons and rings, etc.

And potentially, the generated QPainterPaths could use arc segments
instead of segmentizing geometries. In fact, there's been disabled
code which seems to do this in place since the new geometry engine
was introduced back in 2.10! TODO: check if this code works correctly...
  • Loading branch information
nyalldawson committed Jul 3, 2020
1 parent 4869217 commit 205273e7cd2e2df15068c3dbefc7eb5386bd049d
Showing with 225 additions and 4 deletions.
  1. +12 −0 python/core/auto_generated/geometry/qgsabstractgeometry.sip.in
  2. +2 −0 python/core/auto_generated/geometry/qgscurve.sip.in
  3. +2 −0 python/core/auto_generated/geometry/qgscurvepolygon.sip.in
  4. +2 −0 python/core/auto_generated/geometry/qgsgeometrycollection.sip.in
  5. +2 −0 python/core/auto_generated/geometry/qgspoint.sip.in
  6. +11 −0 src/core/geometry/qgsabstractgeometry.h
  7. +7 −0 src/core/geometry/qgscurve.cpp
  8. +1 −0 src/core/geometry/qgscurve.h
  9. +14 −0 src/core/geometry/qgscurvepolygon.cpp
  10. +1 −0 src/core/geometry/qgscurvepolygon.h
  11. +12 −0 src/core/geometry/qgsgeometrycollection.cpp
  12. +1 −0 src/core/geometry/qgsgeometrycollection.h
  13. +5 −0 src/core/geometry/qgspoint.cpp
  14. +1 −0 src/core/geometry/qgspoint.h
  15. +152 −4 tests/src/python/test_qgsgeometry.py
  16. BIN tests/testdata/control_images/geometry_path/expected_circular_string/expected_circular_string.png
  17. BIN ..._images/geometry_path/expected_collection_circular_string/expected_collection_circular_string.png
  18. BIN ...ol_images/geometry_path/expected_collection_compound_curve/expected_collection_compound_curve.png
  19. BIN ...trol_images/geometry_path/expected_collection_curve_polygon/expected_collection_curve_polygon.png
  20. BIN ...ometry_path/expected_collection_curve_polygon_no_arc/expected_collection_curve_polygon_no_arc.png
  21. BIN .../control_images/geometry_path/expected_collection_fill_symbol/expected_collection_fill_symbol.png
  22. BIN .../control_images/geometry_path/expected_collection_line_symbol/expected_collection_line_symbol.png
  23. BIN ...ta/control_images/geometry_path/expected_collection_linestring/expected_collection_linestring.png
  24. BIN ...trol_images/geometry_path/expected_collection_marker_symbol/expected_collection_marker_symbol.png
  25. BIN tests/testdata/control_images/geometry_path/expected_collection_mixed/expected_collection_mixed.png
  26. BIN ..._images/geometry_path/expected_collection_multilinestring/expected_collection_multilinestring.png
  27. BIN ...ontrol_images/geometry_path/expected_collection_multipolygon/expected_collection_multipolygon.png
  28. BIN ...testdata/control_images/geometry_path/expected_collection_polygon/expected_collection_polygon.png
  29. BIN tests/testdata/control_images/geometry_path/expected_compound_curve/expected_compound_curve.png
  30. BIN ...ontrol_images/geometry_path/expected_compoundcurve_with_line/expected_compoundcurve_with_line.png
  31. BIN tests/testdata/control_images/geometry_path/expected_curve_polygon/expected_curve_polygon.png
  32. BIN ...data/control_images/geometry_path/expected_curve_polygon_no_arc/expected_curve_polygon_no_arc.png
  33. BIN ..._curvepolygon_circularstring_interiorrings/expected_curvepolygon_circularstring_interiorrings.png
  34. BIN tests/testdata/control_images/geometry_path/expected_empty/expected_empty.png
  35. BIN tests/testdata/control_images/geometry_path/expected_linestring/expected_linestring.png
  36. BIN tests/testdata/control_images/geometry_path/expected_multicurve/expected_multicurve.png
  37. BIN tests/testdata/control_images/geometry_path/expected_multilinestring/expected_multilinestring.png
  38. BIN tests/testdata/control_images/geometry_path/expected_multipolygon/expected_multipolygon.png
  39. BIN tests/testdata/control_images/geometry_path/expected_polygon/expected_polygon.png
@@ -318,6 +318,18 @@ Similarly, m-values can be scaled via ``mScale`` and translated via ``mTranslate
Draws the geometry using the specified QPainter.

:param p: destination QPainter
%End

virtual QPainterPath asQPainterPath() const = 0;
%Docstring
Returns the geometry represented as a QPainterPath.

.. warning::

not all geometry subclasses can be represented by a QPainterPath, e.g.
points and multipoint geometries will return an empty path.

.. versionadded:: 3.16
%End

virtual int vertexNumberFromVertexId( QgsVertexId id ) const = 0;
@@ -83,6 +83,8 @@ segments in a full circle)
%Docstring
Adds a curve to a painter path.
%End
virtual QPainterPath asQPainterPath() const;


virtual void drawAsPolygon( QPainter &p ) const = 0;
%Docstring
@@ -192,6 +192,8 @@ direction.
.. versionadded:: 3.6
%End

virtual QPainterPath asQPainterPath() const;

virtual void draw( QPainter &p ) const;

virtual void transform( const QgsCoordinateTransform &ct, QgsCoordinateTransform::TransformDirection d = QgsCoordinateTransform::ForwardTransform, bool transformZ = false ) throw( QgsCsException );
@@ -149,6 +149,8 @@ An IndexError will be raised if no geometry with the specified index exists.

virtual void draw( QPainter &p ) const;

virtual QPainterPath asQPainterPath() const;


virtual bool fromWkb( QgsConstWkbPtr &wkb );

@@ -382,6 +382,8 @@ Example

virtual void draw( QPainter &p ) const;

virtual QPainterPath asQPainterPath() const;

virtual void transform( const QgsCoordinateTransform &ct, QgsCoordinateTransform::TransformDirection d = QgsCoordinateTransform::ForwardTransform, bool transformZ = false ) throw( QgsCsException );

virtual void transform( const QTransform &t, double zTranslate = 0.0, double zScale = 1.0, double mTranslate = 0.0, double mScale = 1.0 );
@@ -42,6 +42,7 @@ class QDomElement;
class QgsGeometryPartIterator;
class QgsGeometryConstPartIterator;
class QgsConstWkbPtr;
class QPainterPath;

typedef QVector< QgsPoint > QgsPointSequence;
#ifndef SIP_RUN
@@ -356,6 +357,16 @@ class CORE_EXPORT QgsAbstractGeometry
*/
virtual void draw( QPainter &p ) const = 0;

/**
* Returns the geometry represented as a QPainterPath.
*
* \warning not all geometry subclasses can be represented by a QPainterPath, e.g.
* points and multipoint geometries will return an empty path.
*
* \since QGIS 3.16
*/
virtual QPainterPath asQPainterPath() const = 0;

/**
* Returns the vertex number corresponding to a vertex \a id.
*
@@ -58,6 +58,13 @@ bool QgsCurve::isRing() const
return ( isClosed() && numPoints() >= 4 );
}

QPainterPath QgsCurve::asQPainterPath() const
{
QPainterPath p;
addToPainterPath( p );
return p;
}

QgsCoordinateSequence QgsCurve::coordinateSequence() const
{
QgsCoordinateSequence sequence;
@@ -89,6 +89,7 @@ class CORE_EXPORT QgsCurve: public QgsAbstractGeometry
* Adds a curve to a painter path.
*/
virtual void addToPainterPath( QPainterPath &path ) const = 0;
QPainterPath asQPainterPath() const override;

/**
* Draws the curve as a polygon on the specified QPainter.
@@ -768,6 +768,20 @@ void QgsCurvePolygon::forceRHR()
mInteriorRings = validRings;
}

QPainterPath QgsCurvePolygon::asQPainterPath() const
{
QPainterPath p;
if ( mExteriorRing )
mExteriorRing->addToPainterPath( p );

for ( const QgsCurve *ring : mInteriorRings )
{
p.addPath( ring->asQPainterPath() );
}

return p;
}

void QgsCurvePolygon::draw( QPainter &p ) const
{
if ( !mExteriorRing )
@@ -212,6 +212,7 @@ class CORE_EXPORT QgsCurvePolygon: public QgsSurface
*/
void forceRHR();

QPainterPath asQPainterPath() const override;
void draw( QPainter &p ) const override;
void transform( const QgsCoordinateTransform &ct, QgsCoordinateTransform::TransformDirection d = QgsCoordinateTransform::ForwardTransform, bool transformZ = false ) override SIP_THROW( QgsCsException );
void transform( const QTransform &t, double zTranslate = 0.0, double zScale = 1.0, double mTranslate = 0.0, double mScale = 1.0 ) override;
@@ -308,6 +308,18 @@ void QgsGeometryCollection::draw( QPainter &p ) const
}
}

QPainterPath QgsGeometryCollection::asQPainterPath() const
{
QPainterPath p;
for ( const QgsAbstractGeometry *geom : mGeometries )
{
QPainterPath partPath = geom->asQPainterPath();
if ( !partPath.isEmpty() )
p.addPath( partPath );
}
return p;
}

bool QgsGeometryCollection::fromWkb( QgsConstWkbPtr &wkbPtr )
{
if ( !wkbPtr )
@@ -176,6 +176,7 @@ class CORE_EXPORT QgsGeometryCollection: public QgsAbstractGeometry
void transform( const QTransform &t, double zTranslate = 0.0, double zScale = 1.0, double mTranslate = 0.0, double mScale = 1.0 ) override;

void draw( QPainter &p ) const override;
QPainterPath asQPainterPath() const override;

bool fromWkb( QgsConstWkbPtr &wkb ) override;
bool fromWkt( const QString &wkt ) override;
@@ -321,6 +321,11 @@ void QgsPoint::draw( QPainter &p ) const
p.drawRect( QRectF( mX - 2, mY - 2, 4, 4 ) );
}

QPainterPath QgsPoint::asQPainterPath() const
{
return QPainterPath();
}

void QgsPoint::clear()
{
mX = mY = std::numeric_limits<double>::quiet_NaN();
@@ -498,6 +498,7 @@ class CORE_EXPORT QgsPoint: public QgsAbstractGeometry
json asJsonObject( int precision = 17 ) const override SIP_SKIP;
QString asKml( int precision = 17 ) const override;
void draw( QPainter &p ) const override;
QPainterPath asQPainterPath() const override;
void transform( const QgsCoordinateTransform &ct, QgsCoordinateTransform::TransformDirection d = QgsCoordinateTransform::ForwardTransform, bool transformZ = false ) override SIP_THROW( QgsCsException );
void transform( const QTransform &t, double zTranslate = 0.0, double zScale = 1.0, double mTranslate = 0.0, double mScale = 1.0 ) override;
QgsCoordinateSequence coordinateSequence() const override;
@@ -39,8 +39,8 @@
QgsCoordinateReferenceSystem,
QgsProject
)
from qgis.PyQt.QtCore import QDir, QPointF
from qgis.PyQt.QtGui import QImage, QPainter, QPen, QColor, QBrush, QPainterPath, QPolygonF
from qgis.PyQt.QtCore import QDir, QPointF, QRectF
from qgis.PyQt.QtGui import QImage, QPainter, QPen, QColor, QBrush, QPainterPath, QPolygonF, QTransform

from qgis.testing import (
start_app,
@@ -5902,13 +5902,161 @@ def testGeometryDraw(self):
rendered_image = self.renderGeometry(geom, False, True)
assert self.imageCheck(test['name'] + '_aspolygon', test['as_polygon_reference_image'], rendered_image)

def imageCheck(self, name, reference_image, image):

def testGeometryAsQPainterPath(self):
'''Tests conversion of different geometries to QPainterPath, including bad/odd geometries.'''

empty_multipolygon = QgsMultiPolygon()
empty_multipolygon.addGeometry(QgsPolygon())
empty_polygon = QgsPolygon()
empty_linestring = QgsLineString()

tests = [{'name': 'LineString',
'wkt': 'LineString (0 0,3 4,4 3)',
'reference_image': 'linestring'},
{'name': 'Empty LineString',
'geom': QgsGeometry(empty_linestring),
'reference_image': 'empty'},
{'name': 'MultiLineString',
'wkt': 'MultiLineString ((0 0, 1 0, 1 1, 2 1, 2 0), (3 1, 5 1, 5 0, 6 0))',
'reference_image': 'multilinestring'},
{'name': 'Polygon',
'wkt': 'Polygon ((0 0, 10 0, 10 10, 0 10, 0 0),(5 5, 7 5, 7 7 , 5 7, 5 5))',
'reference_image': 'polygon'},
{'name': 'Empty Polygon',
'geom': QgsGeometry(empty_polygon),
'reference_image': 'empty'},
{'name': 'MultiPolygon',
'wkt': 'MultiPolygon (((0 0, 1 0, 1 1, 2 1, 2 2, 0 2, 0 0)),((4 0, 5 0, 5 2, 3 2, 3 1, 4 1, 4 0)))',
'reference_image': 'multipolygon'},
{'name': 'Empty MultiPolygon',
'geom': QgsGeometry(empty_multipolygon),
'reference_image': 'empty'},
{'name': 'CircularString',
'wkt': 'CIRCULARSTRING(268 415,227 505,227 406)',
'reference_image': 'circular_string'},
{'name': 'CompoundCurve',
'wkt': 'COMPOUNDCURVE((5 3, 5 13), CIRCULARSTRING(5 13, 7 15, 9 13), (9 13, 9 3), CIRCULARSTRING(9 3, 7 1, 5 3))',
'reference_image': 'compound_curve'},
{'name': 'CurvePolygon',
'wkt': 'CURVEPOLYGON(CIRCULARSTRING(1 3, 3 5, 4 7, 7 3, 1 3))',
'reference_image': 'curve_polygon'},
{'name': 'MultiCurve',
'wkt': 'MultiCurve((5 5,3 5,3 3,0 3),CIRCULARSTRING(0 0, 2 1,2 2))',
'reference_image': 'multicurve'},
{'name': 'CurvePolygon_no_arc', # refs #14028
'wkt': 'CURVEPOLYGON(LINESTRING(1 3, 3 5, 4 7, 7 3, 1 3))',
'reference_image': 'curve_polygon_no_arc'},
{'name': 'CurvePolygonInteriorRings',
'wkt': 'CurvePolygon(CircularString (20 30, 50 30, 50 90, 10 50, 20 30),LineString(30 45, 55 45, 30 75, 30 45))',
'reference_image': 'curvepolygon_circularstring_interiorrings'},
{'name': 'CompoundCurve With Line',
'wkt': 'CompoundCurve(CircularString (20 30, 50 30, 50 90),LineString(50 90, 10 90))',
'reference_image': 'compoundcurve_with_line'},
{'name': 'Collection LineString',
'wkt': 'GeometryCollection( LineString (0 0,3 4,4 3) )',
'reference_image': 'collection_linestring'},
{'name': 'Collection MultiLineString',
'wkt': 'GeometryCollection (LineString(0 0, 1 0, 1 1, 2 1, 2 0), LineString(3 1, 5 1, 5 0, 6 0))',
'reference_image': 'collection_multilinestring'},
{'name': 'Collection Polygon',
'wkt': 'GeometryCollection(Polygon ((0 0, 10 0, 10 10, 0 10, 0 0),(5 5, 7 5, 7 7 , 5 7, 5 5)))',
'reference_image': 'collection_polygon'},
{'name': 'Collection MultiPolygon',
'wkt': 'GeometryCollection( Polygon((0 0, 1 0, 1 1, 2 1, 2 2, 0 2, 0 0)),Polygon((4 0, 5 0, 5 2, 3 2, 3 1, 4 1, 4 0)))',
'reference_image': 'collection_multipolygon'},
{'name': 'Collection CircularString',
'wkt': 'GeometryCollection(CIRCULARSTRING(268 415,227 505,227 406))',
'reference_image': 'collection_circular_string'},
{'name': 'Collection CompoundCurve',
'wkt': 'GeometryCollection(COMPOUNDCURVE((5 3, 5 13), CIRCULARSTRING(5 13, 7 15, 9 13), (9 13, 9 3), CIRCULARSTRING(9 3, 7 1, 5 3)))',
'reference_image': 'collection_compound_curve'},
{'name': 'Collection CurvePolygon',
'wkt': 'GeometryCollection(CURVEPOLYGON(CIRCULARSTRING(1 3, 3 5, 4 7, 7 3, 1 3)))',
'reference_image': 'collection_curve_polygon'},
{'name': 'Collection CurvePolygon_no_arc', # refs #14028
'wkt': 'GeometryCollection(CURVEPOLYGON(LINESTRING(1 3, 3 5, 4 7, 7 3, 1 3)))',
'reference_image': 'collection_curve_polygon_no_arc'},
{'name': 'Collection Mixed',
'wkt': 'GeometryCollection(Point(1 2), MultiPoint(3 3, 2 3), LineString (0 0,3 4,4 3), MultiLineString((3 1, 3 2, 4 2)), Polygon((0 0, 1 0, 1 1, 2 1, 2 2, 0 2, 0 0)), MultiPolygon(((4 0, 5 0, 5 1, 6 1, 6 2, 4 2, 4 0)),(( 1 4, 2 4, 1 5, 1 4))))',
'reference_image': 'collection_mixed'},
]

for test in tests:

def get_geom():
if 'geom' not in test:
geom = QgsGeometry.fromWkt(test['wkt'])
assert geom and not geom.isNull(), 'Could not create geometry {}'.format(test['wkt'])
else:
geom = test['geom']
return geom

geom = get_geom()
rendered_image = self.renderGeometryUsingPath(geom)
self.assertTrue(self.imageCheck(test['name'], test['reference_image'], rendered_image, control_path="geometry_path"), test['name'])

# Note - each test is repeated with the same geometry and reference image, but with added
# z and m dimensions. This tests that presence of the dimensions does not affect rendering

# test with Z
geom_z = get_geom()
geom_z.get().addZValue(5)
rendered_image = self.renderGeometryUsingPath(geom_z)
assert self.imageCheck(test['name'] + 'Z', test['reference_image'], rendered_image, control_path="geometry_path")

# test with ZM
geom_z.get().addMValue(15)
rendered_image = self.renderGeometryUsingPath(geom_z)
assert self.imageCheck(test['name'] + 'ZM', test['reference_image'], rendered_image, control_path="geometry_path")

# test with M
geom_m = get_geom()
geom_m.get().addMValue(15)
rendered_image = self.renderGeometryUsingPath(geom_m)
assert self.imageCheck(test['name'] + 'M', test['reference_image'], rendered_image, control_path="geometry_path")

def renderGeometryUsingPath(self, geom):
image = QImage(200, 200, QImage.Format_RGB32)
dest_bounds = image.rect()

geom = QgsGeometry(geom)

src_bounds = geom.buffer(geom.boundingBox().width() / 10, 5).boundingBox()
if src_bounds.width() and src_bounds.height():
scale = min(dest_bounds.width() / src_bounds.width(), dest_bounds.height() / src_bounds.height())
t = QTransform.fromScale(scale, -scale)
geom.transform(t)

src_bounds = geom.buffer(geom.boundingBox().width() / 10, 5).boundingBox()
t = QTransform.fromTranslate(-src_bounds.xMinimum(), -src_bounds.yMinimum())
geom.transform(t)

path = geom.constGet().asQPainterPath()

painter = QPainter()
painter.begin(image)
pen = QPen(QColor(0, 255, 255))
pen.setWidth(6)
painter.setPen(pen)
painter.setBrush(QBrush(QColor(255, 255, 0)))
try:
image.fill(QColor(0, 0, 0))

painter.drawPath(path)

finally:
painter.end()

return image

def imageCheck(self, name, reference_image, image, control_path="geometry"):
self.report += "<h2>Render {}</h2>\n".format(name)
temp_dir = QDir.tempPath() + '/'
file_name = temp_dir + 'geometry_' + name + ".png"
image.save(file_name, "PNG")
checker = QgsRenderChecker()
checker.setControlPathPrefix("geometry")
checker.setControlPathPrefix(control_path)
checker.setControlName("expected_" + reference_image)
checker.setRenderedImage(file_name)
checker.setColorTolerance(2)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 comments on commit 205273e

Please sign in to comment.
You can’t perform that action at this time.