Skip to content
Permalink
Browse files
Use a spatial index to optimise annotation layer item retrieval
  • Loading branch information
nyalldawson committed Aug 19, 2021
1 parent e3b10ff commit aeb7d89a24f9e9482f2f7b157c4624933f07e9e3
@@ -97,6 +97,15 @@ with the layer.
%Docstring
Returns the item with the specified ``id``, or ``None`` if no matching item was found.

.. versionadded:: 3.22
%End

QStringList itemsInBounds( const QgsRectangle &bounds, QgsFeedback *feedback = 0 ) const;
%Docstring
Returns a list of the IDs of all annotation items within the specified ``bounds``.

The optional ``feedback`` argument can be used to cancel the search early.

.. versionadded:: 3.22
%End

@@ -22,11 +22,83 @@
#include "qgslogger.h"
#include "qgspainting.h"
#include "qgsmaplayerfactory.h"
#include "qgsfeedback.h"
#include <QUuid>
#include "RTree.h"


class QgsAnnotationLayerSpatialIndex : public RTree<QString, float, 2, float>
{
public:

void insert( const QString &uuid, const QgsRectangle &bounds )
{
std::array< float, 4 > scaledBounds = scaleBounds( bounds );
this->Insert(
{
scaledBounds[0], scaledBounds[ 1]
},
{
scaledBounds[2], scaledBounds[3]
},
uuid );
}

/**
* Removes existing \a data from the spatial index, with the specified \a bounds.
*
* \a data is not deleted, and it is the caller's responsibility to ensure that
* it is appropriately cleaned up.
*/
void remove( const QString &uuid, const QgsRectangle &bounds )
{
std::array< float, 4 > scaledBounds = scaleBounds( bounds );
this->Remove(
{
scaledBounds[0], scaledBounds[ 1]
},
{
scaledBounds[2], scaledBounds[3]
},
uuid );
}

/**
* Performs an intersection check against the index, for data intersecting the specified \a bounds.
*
* The \a callback function will be called once for each matching data object encountered.
*/
bool intersects( const QgsRectangle &bounds, const std::function< bool( const QString &uuid )> &callback ) const
{
std::array< float, 4 > scaledBounds = scaleBounds( bounds );
this->Search(
{
scaledBounds[0], scaledBounds[ 1]
},
{
scaledBounds[2], scaledBounds[3]
},
callback );
return true;
}

private:
std::array<float, 4> scaleBounds( const QgsRectangle &bounds ) const
{
return
{
static_cast< float >( bounds.xMinimum() ),
static_cast< float >( bounds.yMinimum() ),
static_cast< float >( bounds.xMaximum() ),
static_cast< float >( bounds.yMaximum() )
};
}
};

QgsAnnotationLayer::QgsAnnotationLayer( const QString &name, const LayerOptions &options )
: QgsMapLayer( QgsMapLayerType::AnnotationLayer, name )
, mTransformContext( options.transformContext )
, mSpatialIndex( std::make_unique< QgsAnnotationLayerSpatialIndex >() )
{
mShouldValidateCrs = false;
mValid = true;
@@ -50,6 +122,7 @@ QString QgsAnnotationLayer::addItem( QgsAnnotationItem *item )
{
const QString uuid = QUuid::createUuid().toString();
mItems.insert( uuid, item );
mSpatialIndex->insert( uuid, item->boundingBox() );

triggerRepaint();

@@ -61,7 +134,9 @@ bool QgsAnnotationLayer::removeItem( const QString &id )
if ( !mItems.contains( id ) )
return false;

delete mItems.take( id );
std::unique_ptr< QgsAnnotationItem> item( mItems.take( id ) );
mSpatialIndex->remove( id, item->boundingBox() );
item.reset();

triggerRepaint();

@@ -72,6 +147,7 @@ void QgsAnnotationLayer::clear()
{
qDeleteAll( mItems );
mItems.clear();
mSpatialIndex = std::make_unique< QgsAnnotationLayerSpatialIndex >();

triggerRepaint();
}
@@ -86,6 +162,18 @@ QgsAnnotationItem *QgsAnnotationLayer::item( const QString &id )
return mItems.value( id );
}

QStringList QgsAnnotationLayer::itemsInBounds( const QgsRectangle &bounds, QgsFeedback *feedback ) const
{
QStringList res;

mSpatialIndex->intersects( bounds, [&res, feedback]( const QString & uuid )->bool
{
res << uuid;
return !feedback || !feedback->isCanceled();
} );
return res;
}

Qgis::MapLayerProperties QgsAnnotationLayer::properties() const
{
// annotation layers are always editable
@@ -101,6 +189,7 @@ QgsAnnotationLayer *QgsAnnotationLayer::clone() const
for ( auto it = mItems.constBegin(); it != mItems.constEnd(); ++it )
{
layer->mItems.insert( it.key(), ( *it )->clone() );
layer->mSpatialIndex->insert( it.key(), ( *it )->boundingBox() );
}

return layer.release();
@@ -143,6 +232,7 @@ bool QgsAnnotationLayer::readXml( const QDomNode &layerNode, QgsReadWriteContext

qDeleteAll( mItems );
mItems.clear();
mSpatialIndex = std::make_unique< QgsAnnotationLayerSpatialIndex >();

const QDomNodeList itemsElements = layerNode.toElement().elementsByTagName( QStringLiteral( "items" ) );
if ( itemsElements.size() == 0 )
@@ -158,6 +248,7 @@ bool QgsAnnotationLayer::readXml( const QDomNode &layerNode, QgsReadWriteContext
if ( item )
{
item->readXml( itemElement, context );
mSpatialIndex->insert( id, item->boundingBox() );
mItems.insert( id, item.release() );
}
}
@@ -183,10 +274,10 @@ bool QgsAnnotationLayer::writeXml( QDomNode &layer_node, QDomDocument &doc, cons

mapLayerNode.setAttribute( QStringLiteral( "type" ), QgsMapLayerFactory::typeToString( QgsMapLayerType::AnnotationLayer ) );

QDomElement itemsElement = doc.createElement( "items" );
QDomElement itemsElement = doc.createElement( QStringLiteral( "items" ) );
for ( auto it = mItems.constBegin(); it != mItems.constEnd(); ++it )
{
QDomElement itemElement = doc.createElement( "item" );
QDomElement itemElement = doc.createElement( QStringLiteral( "item" ) );
itemElement.setAttribute( QStringLiteral( "type" ), ( *it )->type() );
itemElement.setAttribute( QStringLiteral( "id" ), it.key() );
( *it )->writeXml( itemElement, doc, context );
@@ -22,8 +22,9 @@
#include "qgsmaplayer.h"
#include "qgsmaplayerrenderer.h"

class QgsAnnotationItem;

class QgsAnnotationItem;
class QgsAnnotationLayerSpatialIndex;

/**
* \ingroup core
@@ -124,6 +125,15 @@ class CORE_EXPORT QgsAnnotationLayer : public QgsMapLayer
*/
QgsAnnotationItem *item( const QString &id );

/**
* Returns a list of the IDs of all annotation items within the specified \a bounds.
*
* The optional \a feedback argument can be used to cancel the search early.
*
* \since QGIS 3.22
*/
QStringList itemsInBounds( const QgsRectangle &bounds, QgsFeedback *feedback = nullptr ) const;

Qgis::MapLayerProperties properties() const override;
QgsAnnotationLayer *clone() const override SIP_FACTORY;
QgsMapLayerRenderer *createMapRenderer( QgsRenderContext &rendererContext ) override SIP_FACTORY;
@@ -139,6 +149,9 @@ class CORE_EXPORT QgsAnnotationLayer : public QgsMapLayer
private:
QMap<QString, QgsAnnotationItem *> mItems;
QgsCoordinateTransformContext mTransformContext;

std::unique_ptr< QgsAnnotationLayerSpatialIndex > mSpatialIndex;

};

#endif // QGSANNOTATIONLAYER_H
@@ -23,14 +23,11 @@ QgsAnnotationLayerRenderer::QgsAnnotationLayerRenderer( QgsAnnotationLayer *laye
, mFeedback( std::make_unique< QgsFeedback >() )
, mLayerOpacity( layer->opacity() )
{
// clone items from layer
const QMap< QString, QgsAnnotationItem * > items = layer->items();
// clone items from layer which fall inside the rendered extent
const QStringList items = layer->itemsInBounds( context.extent() );
mItems.reserve( items.size() );
for ( auto it = items.constBegin(); it != items.constEnd(); ++it )
{
if ( it.value() )
mItems << ( *it )->clone();
}
std::transform( items.begin(), items.end(), std::back_inserter( mItems ),
[layer]( const QString & id ) -> QgsAnnotationItem* { return layer->item( id )->clone(); } );

std::sort( mItems.begin(), mItems.end(), []( QgsAnnotationItem * a, QgsAnnotationItem * b ) { return a->zIndex() < b->zIndex(); } ); //clazy:exclude=detaching-member
}
@@ -145,6 +145,19 @@ def testExtent(self):
self.assertEqual(extent.yMinimum(), 13.0)
self.assertEqual(extent.yMaximum(), 15.0)

def testItemsInBounds(self):
layer = QgsAnnotationLayer('test', QgsAnnotationLayer.LayerOptions(QgsProject.instance().transformContext()))
self.assertTrue(layer.isValid())

item1uuid = layer.addItem(QgsAnnotationPolygonItem(QgsPolygon(QgsLineString([QgsPoint(12, 13), QgsPoint(14, 13), QgsPoint(14, 15), QgsPoint(12, 13)]))))
item2uuid = layer.addItem(QgsAnnotationLineItem(QgsLineString([QgsPoint(11, 13), QgsPoint(12, 13), QgsPoint(12, 150)])))
item3uuid = layer.addItem(QgsAnnotationMarkerItem(QgsPoint(120, 13)))

self.assertFalse(layer.itemsInBounds(QgsRectangle(-10,-10, -9,9)))
self.assertCountEqual(layer.itemsInBounds(QgsRectangle(12,13, 14,15)), [item1uuid, item2uuid])
self.assertCountEqual(layer.itemsInBounds(QgsRectangle(12, 130, 14, 150)), [item2uuid])
self.assertCountEqual(layer.itemsInBounds(QgsRectangle(110, 0, 120, 20)), [item3uuid])

def testReadWriteXml(self):
doc = QDomDocument("testdoc")

@@ -286,6 +299,7 @@ def testRenderWithTransform(self):

rc = QgsRenderContext.fromMapSettings(settings)
rc.setCoordinateTransform(QgsCoordinateTransform(layer.crs(), settings.destinationCrs(), QgsProject.instance()))
rc.setExtent(rc.coordinateTransform().transformBoundingBox(settings.extent(), QgsCoordinateTransform.ReverseTransform))
image = QImage(200, 200, QImage.Format_ARGB32)
image.setDotsPerMeterX(96 / 25.4 * 1000)
image.setDotsPerMeterY(96 / 25.4 * 1000)

0 comments on commit aeb7d89

Please sign in to comment.