Skip to content

Commit 205273e

Browse files
committedJul 3, 2020
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...
1 parent 4869217 commit 205273e

File tree

39 files changed

+225
-4
lines changed

39 files changed

+225
-4
lines changed
 

‎python/core/auto_generated/geometry/qgsabstractgeometry.sip.in

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,18 @@ Similarly, m-values can be scaled via ``mScale`` and translated via ``mTranslate
318318
Draws the geometry using the specified QPainter.
319319

320320
:param p: destination QPainter
321+
%End
322+
323+
virtual QPainterPath asQPainterPath() const = 0;
324+
%Docstring
325+
Returns the geometry represented as a QPainterPath.
326+
327+
.. warning::
328+
329+
not all geometry subclasses can be represented by a QPainterPath, e.g.
330+
points and multipoint geometries will return an empty path.
331+
332+
.. versionadded:: 3.16
321333
%End
322334

323335
virtual int vertexNumberFromVertexId( QgsVertexId id ) const = 0;

‎python/core/auto_generated/geometry/qgscurve.sip.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ segments in a full circle)
8383
%Docstring
8484
Adds a curve to a painter path.
8585
%End
86+
virtual QPainterPath asQPainterPath() const;
87+
8688

8789
virtual void drawAsPolygon( QPainter &p ) const = 0;
8890
%Docstring

‎python/core/auto_generated/geometry/qgscurvepolygon.sip.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ direction.
192192
.. versionadded:: 3.6
193193
%End
194194

195+
virtual QPainterPath asQPainterPath() const;
196+
195197
virtual void draw( QPainter &p ) const;
196198

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

‎python/core/auto_generated/geometry/qgsgeometrycollection.sip.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ An IndexError will be raised if no geometry with the specified index exists.
149149

150150
virtual void draw( QPainter &p ) const;
151151

152+
virtual QPainterPath asQPainterPath() const;
153+
152154

153155
virtual bool fromWkb( QgsConstWkbPtr &wkb );
154156

‎python/core/auto_generated/geometry/qgspoint.sip.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,8 @@ Example
382382

383383
virtual void draw( QPainter &p ) const;
384384

385+
virtual QPainterPath asQPainterPath() const;
386+
385387
virtual void transform( const QgsCoordinateTransform &ct, QgsCoordinateTransform::TransformDirection d = QgsCoordinateTransform::ForwardTransform, bool transformZ = false ) throw( QgsCsException );
386388

387389
virtual void transform( const QTransform &t, double zTranslate = 0.0, double zScale = 1.0, double mTranslate = 0.0, double mScale = 1.0 );

‎src/core/geometry/qgsabstractgeometry.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class QDomElement;
4242
class QgsGeometryPartIterator;
4343
class QgsGeometryConstPartIterator;
4444
class QgsConstWkbPtr;
45+
class QPainterPath;
4546

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

360+
/**
361+
* Returns the geometry represented as a QPainterPath.
362+
*
363+
* \warning not all geometry subclasses can be represented by a QPainterPath, e.g.
364+
* points and multipoint geometries will return an empty path.
365+
*
366+
* \since QGIS 3.16
367+
*/
368+
virtual QPainterPath asQPainterPath() const = 0;
369+
359370
/**
360371
* Returns the vertex number corresponding to a vertex \a id.
361372
*

‎src/core/geometry/qgscurve.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ bool QgsCurve::isRing() const
5858
return ( isClosed() && numPoints() >= 4 );
5959
}
6060

61+
QPainterPath QgsCurve::asQPainterPath() const
62+
{
63+
QPainterPath p;
64+
addToPainterPath( p );
65+
return p;
66+
}
67+
6168
QgsCoordinateSequence QgsCurve::coordinateSequence() const
6269
{
6370
QgsCoordinateSequence sequence;

‎src/core/geometry/qgscurve.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ class CORE_EXPORT QgsCurve: public QgsAbstractGeometry
8989
* Adds a curve to a painter path.
9090
*/
9191
virtual void addToPainterPath( QPainterPath &path ) const = 0;
92+
QPainterPath asQPainterPath() const override;
9293

9394
/**
9495
* Draws the curve as a polygon on the specified QPainter.

‎src/core/geometry/qgscurvepolygon.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,20 @@ void QgsCurvePolygon::forceRHR()
768768
mInteriorRings = validRings;
769769
}
770770

771+
QPainterPath QgsCurvePolygon::asQPainterPath() const
772+
{
773+
QPainterPath p;
774+
if ( mExteriorRing )
775+
mExteriorRing->addToPainterPath( p );
776+
777+
for ( const QgsCurve *ring : mInteriorRings )
778+
{
779+
p.addPath( ring->asQPainterPath() );
780+
}
781+
782+
return p;
783+
}
784+
771785
void QgsCurvePolygon::draw( QPainter &p ) const
772786
{
773787
if ( !mExteriorRing )

‎src/core/geometry/qgscurvepolygon.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ class CORE_EXPORT QgsCurvePolygon: public QgsSurface
212212
*/
213213
void forceRHR();
214214

215+
QPainterPath asQPainterPath() const override;
215216
void draw( QPainter &p ) const override;
216217
void transform( const QgsCoordinateTransform &ct, QgsCoordinateTransform::TransformDirection d = QgsCoordinateTransform::ForwardTransform, bool transformZ = false ) override SIP_THROW( QgsCsException );
217218
void transform( const QTransform &t, double zTranslate = 0.0, double zScale = 1.0, double mTranslate = 0.0, double mScale = 1.0 ) override;

‎src/core/geometry/qgsgeometrycollection.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,18 @@ void QgsGeometryCollection::draw( QPainter &p ) const
308308
}
309309
}
310310

311+
QPainterPath QgsGeometryCollection::asQPainterPath() const
312+
{
313+
QPainterPath p;
314+
for ( const QgsAbstractGeometry *geom : mGeometries )
315+
{
316+
QPainterPath partPath = geom->asQPainterPath();
317+
if ( !partPath.isEmpty() )
318+
p.addPath( partPath );
319+
}
320+
return p;
321+
}
322+
311323
bool QgsGeometryCollection::fromWkb( QgsConstWkbPtr &wkbPtr )
312324
{
313325
if ( !wkbPtr )

‎src/core/geometry/qgsgeometrycollection.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ class CORE_EXPORT QgsGeometryCollection: public QgsAbstractGeometry
176176
void transform( const QTransform &t, double zTranslate = 0.0, double zScale = 1.0, double mTranslate = 0.0, double mScale = 1.0 ) override;
177177

178178
void draw( QPainter &p ) const override;
179+
QPainterPath asQPainterPath() const override;
179180

180181
bool fromWkb( QgsConstWkbPtr &wkb ) override;
181182
bool fromWkt( const QString &wkt ) override;

‎src/core/geometry/qgspoint.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,11 @@ void QgsPoint::draw( QPainter &p ) const
321321
p.drawRect( QRectF( mX - 2, mY - 2, 4, 4 ) );
322322
}
323323

324+
QPainterPath QgsPoint::asQPainterPath() const
325+
{
326+
return QPainterPath();
327+
}
328+
324329
void QgsPoint::clear()
325330
{
326331
mX = mY = std::numeric_limits<double>::quiet_NaN();

‎src/core/geometry/qgspoint.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,7 @@ class CORE_EXPORT QgsPoint: public QgsAbstractGeometry
498498
json asJsonObject( int precision = 17 ) const override SIP_SKIP;
499499
QString asKml( int precision = 17 ) const override;
500500
void draw( QPainter &p ) const override;
501+
QPainterPath asQPainterPath() const override;
501502
void transform( const QgsCoordinateTransform &ct, QgsCoordinateTransform::TransformDirection d = QgsCoordinateTransform::ForwardTransform, bool transformZ = false ) override SIP_THROW( QgsCsException );
502503
void transform( const QTransform &t, double zTranslate = 0.0, double zScale = 1.0, double mTranslate = 0.0, double mScale = 1.0 ) override;
503504
QgsCoordinateSequence coordinateSequence() const override;

‎tests/src/python/test_qgsgeometry.py

Lines changed: 152 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
QgsCoordinateReferenceSystem,
4040
QgsProject
4141
)
42-
from qgis.PyQt.QtCore import QDir, QPointF
43-
from qgis.PyQt.QtGui import QImage, QPainter, QPen, QColor, QBrush, QPainterPath, QPolygonF
42+
from qgis.PyQt.QtCore import QDir, QPointF, QRectF
43+
from qgis.PyQt.QtGui import QImage, QPainter, QPen, QColor, QBrush, QPainterPath, QPolygonF, QTransform
4444

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

5905-
def imageCheck(self, name, reference_image, image):
5905+
5906+
def testGeometryAsQPainterPath(self):
5907+
'''Tests conversion of different geometries to QPainterPath, including bad/odd geometries.'''
5908+
5909+
empty_multipolygon = QgsMultiPolygon()
5910+
empty_multipolygon.addGeometry(QgsPolygon())
5911+
empty_polygon = QgsPolygon()
5912+
empty_linestring = QgsLineString()
5913+
5914+
tests = [{'name': 'LineString',
5915+
'wkt': 'LineString (0 0,3 4,4 3)',
5916+
'reference_image': 'linestring'},
5917+
{'name': 'Empty LineString',
5918+
'geom': QgsGeometry(empty_linestring),
5919+
'reference_image': 'empty'},
5920+
{'name': 'MultiLineString',
5921+
'wkt': 'MultiLineString ((0 0, 1 0, 1 1, 2 1, 2 0), (3 1, 5 1, 5 0, 6 0))',
5922+
'reference_image': 'multilinestring'},
5923+
{'name': 'Polygon',
5924+
'wkt': 'Polygon ((0 0, 10 0, 10 10, 0 10, 0 0),(5 5, 7 5, 7 7 , 5 7, 5 5))',
5925+
'reference_image': 'polygon'},
5926+
{'name': 'Empty Polygon',
5927+
'geom': QgsGeometry(empty_polygon),
5928+
'reference_image': 'empty'},
5929+
{'name': 'MultiPolygon',
5930+
'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)))',
5931+
'reference_image': 'multipolygon'},
5932+
{'name': 'Empty MultiPolygon',
5933+
'geom': QgsGeometry(empty_multipolygon),
5934+
'reference_image': 'empty'},
5935+
{'name': 'CircularString',
5936+
'wkt': 'CIRCULARSTRING(268 415,227 505,227 406)',
5937+
'reference_image': 'circular_string'},
5938+
{'name': 'CompoundCurve',
5939+
'wkt': 'COMPOUNDCURVE((5 3, 5 13), CIRCULARSTRING(5 13, 7 15, 9 13), (9 13, 9 3), CIRCULARSTRING(9 3, 7 1, 5 3))',
5940+
'reference_image': 'compound_curve'},
5941+
{'name': 'CurvePolygon',
5942+
'wkt': 'CURVEPOLYGON(CIRCULARSTRING(1 3, 3 5, 4 7, 7 3, 1 3))',
5943+
'reference_image': 'curve_polygon'},
5944+
{'name': 'MultiCurve',
5945+
'wkt': 'MultiCurve((5 5,3 5,3 3,0 3),CIRCULARSTRING(0 0, 2 1,2 2))',
5946+
'reference_image': 'multicurve'},
5947+
{'name': 'CurvePolygon_no_arc', # refs #14028
5948+
'wkt': 'CURVEPOLYGON(LINESTRING(1 3, 3 5, 4 7, 7 3, 1 3))',
5949+
'reference_image': 'curve_polygon_no_arc'},
5950+
{'name': 'CurvePolygonInteriorRings',
5951+
'wkt': 'CurvePolygon(CircularString (20 30, 50 30, 50 90, 10 50, 20 30),LineString(30 45, 55 45, 30 75, 30 45))',
5952+
'reference_image': 'curvepolygon_circularstring_interiorrings'},
5953+
{'name': 'CompoundCurve With Line',
5954+
'wkt': 'CompoundCurve(CircularString (20 30, 50 30, 50 90),LineString(50 90, 10 90))',
5955+
'reference_image': 'compoundcurve_with_line'},
5956+
{'name': 'Collection LineString',
5957+
'wkt': 'GeometryCollection( LineString (0 0,3 4,4 3) )',
5958+
'reference_image': 'collection_linestring'},
5959+
{'name': 'Collection MultiLineString',
5960+
'wkt': 'GeometryCollection (LineString(0 0, 1 0, 1 1, 2 1, 2 0), LineString(3 1, 5 1, 5 0, 6 0))',
5961+
'reference_image': 'collection_multilinestring'},
5962+
{'name': 'Collection Polygon',
5963+
'wkt': 'GeometryCollection(Polygon ((0 0, 10 0, 10 10, 0 10, 0 0),(5 5, 7 5, 7 7 , 5 7, 5 5)))',
5964+
'reference_image': 'collection_polygon'},
5965+
{'name': 'Collection MultiPolygon',
5966+
'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)))',
5967+
'reference_image': 'collection_multipolygon'},
5968+
{'name': 'Collection CircularString',
5969+
'wkt': 'GeometryCollection(CIRCULARSTRING(268 415,227 505,227 406))',
5970+
'reference_image': 'collection_circular_string'},
5971+
{'name': 'Collection CompoundCurve',
5972+
'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)))',
5973+
'reference_image': 'collection_compound_curve'},
5974+
{'name': 'Collection CurvePolygon',
5975+
'wkt': 'GeometryCollection(CURVEPOLYGON(CIRCULARSTRING(1 3, 3 5, 4 7, 7 3, 1 3)))',
5976+
'reference_image': 'collection_curve_polygon'},
5977+
{'name': 'Collection CurvePolygon_no_arc', # refs #14028
5978+
'wkt': 'GeometryCollection(CURVEPOLYGON(LINESTRING(1 3, 3 5, 4 7, 7 3, 1 3)))',
5979+
'reference_image': 'collection_curve_polygon_no_arc'},
5980+
{'name': 'Collection Mixed',
5981+
'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))))',
5982+
'reference_image': 'collection_mixed'},
5983+
]
5984+
5985+
for test in tests:
5986+
5987+
def get_geom():
5988+
if 'geom' not in test:
5989+
geom = QgsGeometry.fromWkt(test['wkt'])
5990+
assert geom and not geom.isNull(), 'Could not create geometry {}'.format(test['wkt'])
5991+
else:
5992+
geom = test['geom']
5993+
return geom
5994+
5995+
geom = get_geom()
5996+
rendered_image = self.renderGeometryUsingPath(geom)
5997+
self.assertTrue(self.imageCheck(test['name'], test['reference_image'], rendered_image, control_path="geometry_path"), test['name'])
5998+
5999+
# Note - each test is repeated with the same geometry and reference image, but with added
6000+
# z and m dimensions. This tests that presence of the dimensions does not affect rendering
6001+
6002+
# test with Z
6003+
geom_z = get_geom()
6004+
geom_z.get().addZValue(5)
6005+
rendered_image = self.renderGeometryUsingPath(geom_z)
6006+
assert self.imageCheck(test['name'] + 'Z', test['reference_image'], rendered_image, control_path="geometry_path")
6007+
6008+
# test with ZM
6009+
geom_z.get().addMValue(15)
6010+
rendered_image = self.renderGeometryUsingPath(geom_z)
6011+
assert self.imageCheck(test['name'] + 'ZM', test['reference_image'], rendered_image, control_path="geometry_path")
6012+
6013+
# test with M
6014+
geom_m = get_geom()
6015+
geom_m.get().addMValue(15)
6016+
rendered_image = self.renderGeometryUsingPath(geom_m)
6017+
assert self.imageCheck(test['name'] + 'M', test['reference_image'], rendered_image, control_path="geometry_path")
6018+
6019+
def renderGeometryUsingPath(self, geom):
6020+
image = QImage(200, 200, QImage.Format_RGB32)
6021+
dest_bounds = image.rect()
6022+
6023+
geom = QgsGeometry(geom)
6024+
6025+
src_bounds = geom.buffer(geom.boundingBox().width() / 10, 5).boundingBox()
6026+
if src_bounds.width() and src_bounds.height():
6027+
scale = min(dest_bounds.width() / src_bounds.width(), dest_bounds.height() / src_bounds.height())
6028+
t = QTransform.fromScale(scale, -scale)
6029+
geom.transform(t)
6030+
6031+
src_bounds = geom.buffer(geom.boundingBox().width() / 10, 5).boundingBox()
6032+
t = QTransform.fromTranslate(-src_bounds.xMinimum(), -src_bounds.yMinimum())
6033+
geom.transform(t)
6034+
6035+
path = geom.constGet().asQPainterPath()
6036+
6037+
painter = QPainter()
6038+
painter.begin(image)
6039+
pen = QPen(QColor(0, 255, 255))
6040+
pen.setWidth(6)
6041+
painter.setPen(pen)
6042+
painter.setBrush(QBrush(QColor(255, 255, 0)))
6043+
try:
6044+
image.fill(QColor(0, 0, 0))
6045+
6046+
painter.drawPath(path)
6047+
6048+
finally:
6049+
painter.end()
6050+
6051+
return image
6052+
6053+
def imageCheck(self, name, reference_image, image, control_path="geometry"):
59066054
self.report += "<h2>Render {}</h2>\n".format(name)
59076055
temp_dir = QDir.tempPath() + '/'
59086056
file_name = temp_dir + 'geometry_' + name + ".png"
59096057
image.save(file_name, "PNG")
59106058
checker = QgsRenderChecker()
5911-
checker.setControlPathPrefix("geometry")
6059+
checker.setControlPathPrefix(control_path)
59126060
checker.setControlName("expected_" + reference_image)
59136061
checker.setRenderedImage(file_name)
59146062
checker.setColorTolerance(2)

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

0 commit comments

Comments
 (0)
Please sign in to comment.