Skip to content
Permalink
Browse files
Tweak labeling geometry logic for GEOS 3.9+ to give a massive speed
boost to labels in certain circumstances

Specifically when only a portion of a large polygon is visible in
the map extent and which participates in the labeling for the map
then a HUGE amount of time was previously spent calculating the
intersection of the polygon and the map extent.

Now we shortcut this where we can, by:
1. Using the geos rectangle clipping method, which is very fast
but can result in invalid geometries. We do this as a first-pass
(and sometimes only pass) in order to quickly reduce the number
of vertices we need to work with.

2. Rearranging the repair of geometries so we do it after the
fast clipping, as the clipping algorithm isn't suspectible to
invalid geometries and can itself create them

3. Only if needed (i.e. when the map extent isn't an axis-aligned
rectangle) do we do the exact intersection of the polygon to
the map extent
  • Loading branch information
nyalldawson committed Apr 29, 2021
1 parent 3c06b6d commit d4cf534e4c8ca10354c72eb87c4b9a33c5a9856b
Showing with 69 additions and 0 deletions.
  1. +69 −0 src/core/labeling/qgspallabeling.cpp
@@ -3776,6 +3776,74 @@ QgsGeometry QgsPalLabeling::prepareGeometry( const QgsGeometry &geometry, QgsRen
}
}

#if GEOS_VERSION_MAJOR>3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR>=9 )
// much faster code path for GEOS 3.9+
const bool mustClip = ( !clipGeometry.isNull() &&
( ( qgsDoubleNear( m2p.mapRotation(), 0 ) && !clipGeometry.boundingBox().contains( geom.boundingBox() ) )
|| ( !qgsDoubleNear( m2p.mapRotation(), 0 ) && !clipGeometry.contains( geom ) ) ) );

bool mustClipExact = false;
if ( mustClip )
{
// nice and fast, but can result in invalid geometries. At least it will potentially strip out a bunch of unwanted vertices upfront!
QgsGeometry clipGeom = geom.clipped( clipGeometry.boundingBox() );
if ( clipGeom.isEmpty() )
return QgsGeometry();

geom = clipGeom;

// we've now clipped against the BOUNDING BOX of clipGeometry. But if clipGeometry is an axis parallel rectangle, then there's no
// need to do an exact (potentially costly) intersection clip as well!
mustClipExact = !clipGeometry.isAxisParallelRectangle( 0.001 );
}

// fix invalid polygons
if ( geom.type() == QgsWkbTypes::PolygonGeometry )
{
if ( geom.isMultipart() )
{
// important -- we need to treat ever part in isolation here. We can't test the validity of the whole geometry
// at once, because touching parts would result in an invalid geometry, and buffering this "dissolves" the parts.
// because the actual label engine treats parts as separate entities, we aren't bound by the usual "touching parts are invalid" rule
// see https://github.com/qgis/QGIS/issues/26763
QVector< QgsGeometry> parts;
parts.reserve( qgsgeometry_cast< const QgsGeometryCollection * >( geom.constGet() )->numGeometries() );
for ( auto it = geom.const_parts_begin(); it != geom.const_parts_end(); ++it )
{
QgsGeometry partGeom( ( *it )->clone() );
if ( !partGeom.isGeosValid() )
{

partGeom = partGeom.makeValid();
}
parts.append( partGeom );
}
geom = QgsGeometry::collectGeometry( parts );
}
else if ( !geom.isGeosValid() )
{

QgsGeometry bufferGeom = geom.makeValid();
if ( bufferGeom.isNull() )
{
QgsDebugMsg( QStringLiteral( "Could not repair geometry: %1" ).arg( bufferGeom.lastError() ) );
return QgsGeometry();
}
geom = bufferGeom;
}
}

if ( mustClipExact )
{
// now do the real intersection against the actual clip geometry
QgsGeometry clipGeom = geom.intersection( clipGeometry );
if ( clipGeom.isEmpty() )
{
return QgsGeometry();
}
geom = clipGeom;
}
#else
// fix invalid polygons
if ( geom.type() == QgsWkbTypes::PolygonGeometry )
{
@@ -3821,6 +3889,7 @@ QgsGeometry QgsPalLabeling::prepareGeometry( const QgsGeometry &geometry, QgsRen
}
geom = clipGeom;
}
#endif

return geom;
}

0 comments on commit d4cf534

Please sign in to comment.