diff --git a/python/core/auto_generated/annotations/qgsannotationitem.sip.in b/python/core/auto_generated/annotations/qgsannotationitem.sip.in index b9eb93a8ab7e..e7fb64784143 100644 --- a/python/core/auto_generated/annotations/qgsannotationitem.sip.in +++ b/python/core/auto_generated/annotations/qgsannotationitem.sip.in @@ -73,6 +73,11 @@ Returns a unique (untranslated) string identifying the type of item. virtual QgsRectangle boundingBox() const = 0; %Docstring Returns the bounding box of the item's geographic location, in the parent layer's coordinate reference system. +%End + + virtual QgsRectangle boundingBox( const QgsRenderContext &context ) const; +%Docstring +Returns the bounding box of the item's geographic location, in the parent layer's coordinate reference system. %End virtual void render( QgsRenderContext &context, QgsFeedback *feedback ) = 0; diff --git a/python/core/auto_generated/annotations/qgsannotationlayer.sip.in b/python/core/auto_generated/annotations/qgsannotationlayer.sip.in index fc76b09368c7..9f63b10756b1 100644 --- a/python/core/auto_generated/annotations/qgsannotationlayer.sip.in +++ b/python/core/auto_generated/annotations/qgsannotationlayer.sip.in @@ -100,9 +100,10 @@ Returns the item with the specified ``id``, or ``None`` if no matching item was .. versionadded:: 3.22 %End - QStringList itemsInBounds( const QgsRectangle &bounds, QgsFeedback *feedback = 0 ) const; + QStringList itemsInBounds( const QgsRectangle &bounds, const QgsRenderContext &context, QgsFeedback *feedback = 0 ) const; %Docstring -Returns a list of the IDs of all annotation items within the specified ``bounds``. +Returns a list of the IDs of all annotation items within the specified ``bounds`` (in layer CRS), when +rendered using the given render ``context``. The optional ``feedback`` argument can be used to cancel the search early. diff --git a/python/core/auto_generated/annotations/qgsannotationpointtextitem.sip.in b/python/core/auto_generated/annotations/qgsannotationpointtextitem.sip.in index ea70a78c1080..14bdf8a1e1a2 100644 --- a/python/core/auto_generated/annotations/qgsannotationpointtextitem.sip.in +++ b/python/core/auto_generated/annotations/qgsannotationpointtextitem.sip.in @@ -29,6 +29,8 @@ Constructor for QgsAnnotationPointTextItem, containing the specified ``text`` at %End ~QgsAnnotationPointTextItem(); + virtual Qgis::AnnotationItemFlags flags() const; + virtual QString type() const; virtual void render( QgsRenderContext &context, QgsFeedback *feedback ); @@ -47,6 +49,8 @@ Creates a new text at point annotation item. virtual QgsRectangle boundingBox() const; + virtual QgsRectangle boundingBox( const QgsRenderContext &context ) const; + QgsPointXY point() const; %Docstring diff --git a/src/core/annotations/qgsannotationitem.h b/src/core/annotations/qgsannotationitem.h index 423fd6ead471..24c65f6f9054 100644 --- a/src/core/annotations/qgsannotationitem.h +++ b/src/core/annotations/qgsannotationitem.h @@ -101,6 +101,11 @@ class CORE_EXPORT QgsAnnotationItem */ virtual QgsRectangle boundingBox() const = 0; + /** + * Returns the bounding box of the item's geographic location, in the parent layer's coordinate reference system. + */ + virtual QgsRectangle boundingBox( const QgsRenderContext &context ) const { Q_UNUSED( context ) return boundingBox();} + /** * Renders the item to the specified render \a context. * diff --git a/src/core/annotations/qgsannotationlayer.cpp b/src/core/annotations/qgsannotationlayer.cpp index 7f867a895822..55f277cb5f32 100644 --- a/src/core/annotations/qgsannotationlayer.cpp +++ b/src/core/annotations/qgsannotationlayer.cpp @@ -123,7 +123,10 @@ QString QgsAnnotationLayer::addItem( QgsAnnotationItem *item ) { const QString uuid = QUuid::createUuid().toString(); mItems.insert( uuid, item ); - mSpatialIndex->insert( uuid, item->boundingBox() ); + if ( item->flags() & Qgis::AnnotationItemFlag::ScaleDependentBoundingBox ) + mNonIndexedItems.insert( uuid ); + else + mSpatialIndex->insert( uuid, item->boundingBox() ); triggerRepaint(); @@ -136,7 +139,17 @@ bool QgsAnnotationLayer::removeItem( const QString &id ) return false; std::unique_ptr< QgsAnnotationItem> item( mItems.take( id ) ); - mSpatialIndex->remove( id, item->boundingBox() ); + + auto it = mNonIndexedItems.find( id ); + if ( it == mNonIndexedItems.end() ) + { + mSpatialIndex->remove( id, item->boundingBox() ); + } + else + { + mNonIndexedItems.erase( it ); + } + item.reset(); triggerRepaint(); @@ -149,6 +162,7 @@ void QgsAnnotationLayer::clear() qDeleteAll( mItems ); mItems.clear(); mSpatialIndex = std::make_unique< QgsAnnotationLayerSpatialIndex >(); + mNonIndexedItems.clear(); triggerRepaint(); } @@ -163,7 +177,8 @@ QgsAnnotationItem *QgsAnnotationLayer::item( const QString &id ) return mItems.value( id ); } -QStringList QgsAnnotationLayer::itemsInBounds( const QgsRectangle &bounds, QgsFeedback *feedback ) const + +QStringList QgsAnnotationLayer::queryIndex( const QgsRectangle &bounds, QgsFeedback *feedback ) const { QStringList res; @@ -175,6 +190,19 @@ QStringList QgsAnnotationLayer::itemsInBounds( const QgsRectangle &bounds, QgsFe return res; } +QStringList QgsAnnotationLayer::itemsInBounds( const QgsRectangle &bounds, const QgsRenderContext &context, QgsFeedback *feedback ) const +{ + QStringList res = queryIndex( bounds, feedback ); + // we also have to search through any non-indexed items + for ( const QString &uuid : mNonIndexedItems ) + { + if ( mItems.value( uuid )->boundingBox( context ).intersects( bounds ) ) + res << uuid; + } + + return res; +} + Qgis::MapLayerProperties QgsAnnotationLayer::properties() const { // annotation layers are always editable @@ -190,7 +218,10 @@ 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() ); + if ( ( *it )->flags() & Qgis::AnnotationItemFlag::ScaleDependentBoundingBox ) + layer->mNonIndexedItems.insert( it.key() ); + else + layer->mSpatialIndex->insert( it.key(), ( *it )->boundingBox() ); } return layer.release(); @@ -234,6 +265,7 @@ bool QgsAnnotationLayer::readXml( const QDomNode &layerNode, QgsReadWriteContext qDeleteAll( mItems ); mItems.clear(); mSpatialIndex = std::make_unique< QgsAnnotationLayerSpatialIndex >(); + mNonIndexedItems.clear(); const QDomNodeList itemsElements = layerNode.toElement().elementsByTagName( QStringLiteral( "items" ) ); if ( itemsElements.size() == 0 ) @@ -249,7 +281,10 @@ bool QgsAnnotationLayer::readXml( const QDomNode &layerNode, QgsReadWriteContext if ( item ) { item->readXml( itemElement, context ); - mSpatialIndex->insert( id, item->boundingBox() ); + if ( item->flags() & Qgis::AnnotationItemFlag::ScaleDependentBoundingBox ) + mNonIndexedItems.insert( id ); + else + mSpatialIndex->insert( id, item->boundingBox() ); mItems.insert( id, item.release() ); } } diff --git a/src/core/annotations/qgsannotationlayer.h b/src/core/annotations/qgsannotationlayer.h index 59948b7cbf6f..09975d36f9de 100644 --- a/src/core/annotations/qgsannotationlayer.h +++ b/src/core/annotations/qgsannotationlayer.h @@ -128,13 +128,14 @@ 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. + * Returns a list of the IDs of all annotation items within the specified \a bounds (in layer CRS), when + * rendered using the given render \a context. * * 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; + QStringList itemsInBounds( const QgsRectangle &bounds, const QgsRenderContext &context, QgsFeedback *feedback = nullptr ) const; Qgis::MapLayerProperties properties() const override; QgsAnnotationLayer *clone() const override SIP_FACTORY; @@ -153,6 +154,11 @@ class CORE_EXPORT QgsAnnotationLayer : public QgsMapLayer QgsCoordinateTransformContext mTransformContext; std::unique_ptr< QgsAnnotationLayerSpatialIndex > mSpatialIndex; + QSet< QString > mNonIndexedItems; + + QStringList queryIndex( const QgsRectangle &bounds, QgsFeedback *feedback = nullptr ) const; + + friend class QgsAnnotationLayerRenderer; }; diff --git a/src/core/annotations/qgsannotationlayerrenderer.cpp b/src/core/annotations/qgsannotationlayerrenderer.cpp index 3249eb14d8ba..e9afda7c1008 100644 --- a/src/core/annotations/qgsannotationlayerrenderer.cpp +++ b/src/core/annotations/qgsannotationlayerrenderer.cpp @@ -23,8 +23,20 @@ QgsAnnotationLayerRenderer::QgsAnnotationLayerRenderer( QgsAnnotationLayer *laye , mFeedback( std::make_unique< QgsFeedback >() ) , mLayerOpacity( layer->opacity() ) { - // clone items from layer which fall inside the rendered extent - const QStringList items = layer->itemsInBounds( context.extent() ); + // Clone items from layer which fall inside the rendered extent + // Because some items have scale dependent bounds, we have to accept some limitations here. + // first, we can use the layer's spatial index to very quickly retrieve items we know will fall within the visible + // extent. This will ONLY apply to items which have a non-scale-dependent bounding box though. + + QSet< QString > items = qgis::listToSet( layer->queryIndex( context.extent() ) ); + + // we also have NO choice but to clone ALL non-indexed items (i.e. those with a scale-dependent bounding box) + // since these won't be in the layer's spatial index, and it's too expensive to determine their actual bounding box + // upfront (we are blocking the main thread right now!) + + // TODO -- come up with some brilliant way to avoid this and also index scale-dependent items ;) + items.unite( layer->mNonIndexedItems ); + mItems.reserve( items.size() ); std::transform( items.begin(), items.end(), std::back_inserter( mItems ), [layer]( const QString & id ) -> QgsAnnotationItem* { return layer->item( id )->clone(); } ); diff --git a/src/core/annotations/qgsannotationpointtextitem.cpp b/src/core/annotations/qgsannotationpointtextitem.cpp index 2b990e31060d..99f5de7e1af7 100644 --- a/src/core/annotations/qgsannotationpointtextitem.cpp +++ b/src/core/annotations/qgsannotationpointtextitem.cpp @@ -28,6 +28,7 @@ QgsAnnotationPointTextItem::QgsAnnotationPointTextItem( const QString &text, Qgs Qgis::AnnotationItemFlags QgsAnnotationPointTextItem::flags() const { + // in truth this should depend on whether the text format is scale dependent or not! return Qgis::AnnotationItemFlag::ScaleDependentBoundingBox; } @@ -116,6 +117,17 @@ QgsRectangle QgsAnnotationPointTextItem::boundingBox() const return QgsRectangle( mPoint.x(), mPoint.y(), mPoint.x(), mPoint.y() ); } +QgsRectangle QgsAnnotationPointTextItem::boundingBox( const QgsRenderContext &context ) const +{ + const double widthInPixels = QgsTextRenderer::textWidth( context, mTextFormat, mText.split( '\n' ) ); + const double heightInPixels = QgsTextRenderer::textHeight( context, mTextFormat, mText.split( '\n' ) ); + + const double widthInMapUnits = context.convertToMapUnits( widthInPixels, QgsUnitTypes::RenderPixels ); + const double heightInMapUnits = context.convertToMapUnits( heightInPixels, QgsUnitTypes::RenderPixels ); + + return QgsRectangle( mPoint.x(), mPoint.y(), mPoint.x() + widthInMapUnits, mPoint.y() + heightInMapUnits ); +} + QgsTextFormat QgsAnnotationPointTextItem::format() const { return mTextFormat; diff --git a/src/core/annotations/qgsannotationpointtextitem.h b/src/core/annotations/qgsannotationpointtextitem.h index d8007bd596e6..09df44a8f0d3 100644 --- a/src/core/annotations/qgsannotationpointtextitem.h +++ b/src/core/annotations/qgsannotationpointtextitem.h @@ -40,6 +40,7 @@ class CORE_EXPORT QgsAnnotationPointTextItem : public QgsAnnotationItem QgsAnnotationPointTextItem( const QString &text, QgsPointXY point ); ~QgsAnnotationPointTextItem() override; + Qgis::AnnotationItemFlags flags() const override; QString type() const override; void render( QgsRenderContext &context, QgsFeedback *feedback ) override; bool writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const override; @@ -52,6 +53,7 @@ class CORE_EXPORT QgsAnnotationPointTextItem : public QgsAnnotationItem bool readXml( const QDomElement &element, const QgsReadWriteContext &context ) override; QgsAnnotationPointTextItem *clone() override SIP_FACTORY; QgsRectangle boundingBox() const override; + QgsRectangle boundingBox( const QgsRenderContext &context ) const override; /** * Returns the point location of the text. diff --git a/tests/src/python/test_qgsannotationlayer.py b/tests/src/python/test_qgsannotationlayer.py index 570f94376987..431e39e5872b 100644 --- a/tests/src/python/test_qgsannotationlayer.py +++ b/tests/src/python/test_qgsannotationlayer.py @@ -159,10 +159,11 @@ def testItemsInBounds(self): 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]) + rc = QgsRenderContext() + self.assertFalse(layer.itemsInBounds(QgsRectangle(-10, -10, -9, 9), rc)) + self.assertCountEqual(layer.itemsInBounds(QgsRectangle(12, 13, 14, 15), rc), [item1uuid, item2uuid]) + self.assertCountEqual(layer.itemsInBounds(QgsRectangle(12, 130, 14, 150), rc), [item2uuid]) + self.assertCountEqual(layer.itemsInBounds(QgsRectangle(110, 0, 120, 20), rc), [item3uuid]) def testReadWriteXml(self): doc = QDomDocument("testdoc")