Skip to content

Commit

Permalink
Add method to QgsAbstractGeometry to convert geometry to QPainterPath
Browse files Browse the repository at this point in the history
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 205273e
Show file tree
Hide file tree
Showing 39 changed files with 225 additions and 4 deletions.
12 changes: 12 additions & 0 deletions python/core/auto_generated/geometry/qgsabstractgeometry.sip.in
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions python/core/auto_generated/geometry/qgscurve.sip.in
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions python/core/auto_generated/geometry/qgscurvepolygon.sip.in
Expand Up @@ -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 );
Expand Down
Expand Up @@ -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 );

Expand Down
2 changes: 2 additions & 0 deletions python/core/auto_generated/geometry/qgspoint.sip.in
Expand Up @@ -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 );
Expand Down
11 changes: 11 additions & 0 deletions src/core/geometry/qgsabstractgeometry.h
Expand Up @@ -42,6 +42,7 @@ class QDomElement;
class QgsGeometryPartIterator;
class QgsGeometryConstPartIterator;
class QgsConstWkbPtr;
class QPainterPath;

typedef QVector< QgsPoint > QgsPointSequence;
#ifndef SIP_RUN
Expand Down Expand Up @@ -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.
*
Expand Down
7 changes: 7 additions & 0 deletions src/core/geometry/qgscurve.cpp
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/core/geometry/qgscurve.h
Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions src/core/geometry/qgscurvepolygon.cpp
Expand Up @@ -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 )
Expand Down
1 change: 1 addition & 0 deletions src/core/geometry/qgscurvepolygon.h
Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions src/core/geometry/qgsgeometrycollection.cpp
Expand Up @@ -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 )
Expand Down
1 change: 1 addition & 0 deletions src/core/geometry/qgsgeometrycollection.h
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions src/core/geometry/qgspoint.cpp
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/core/geometry/qgspoint.h
Expand Up @@ -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;
Expand Down
156 changes: 152 additions & 4 deletions tests/src/python/test_qgsgeometry.py
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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 205273e

Please sign in to comment.