Port annotation feature handling to QgsAnnotation

Not an ideal implementation (too much logic resides in the
gui QgsMapCanvasAnnotationItem class), but any approach
using current api will be dependant on some hacks.

Ideally we need a QgsVectorDataProvider method for finding
the closest feature(s) to a point(/line/polygon) within a
certain tolerance, with provider specific implementations
which offload this to the data store's spatial indices.
Then this handling could be bumped up to reside in
QgsAnnotation.
This commit is contained in:
Nyall Dawson 2017-01-29 23:14:58 +10:00
parent 3d372e615c
commit c22f5de690
14 changed files with 199 additions and 138 deletions

View File

@ -48,6 +48,9 @@ class QgsAnnotation : QObject
QgsMapLayer* mapLayer() const;
void setMapLayer( QgsMapLayer* layer );
QgsFeature associatedFeature() const;
virtual void setAssociatedFeature( const QgsFeature& feature );
signals:
void appearanceChanged();

View File

@ -17,6 +17,8 @@ class QgsHtmlAnnotation : QgsAnnotation
virtual void writeXml( QDomElement& elem, QDomDocument & doc ) const;
virtual void readXml( const QDomElement& itemElem, const QDomDocument& doc );
virtual void setAssociatedFeature( const QgsFeature& feature );
protected:
void renderAnnotation( QgsRenderContext& context, QSizeF size ) const;

View File

@ -242,6 +242,19 @@ class CORE_EXPORT QgsAnnotation : public QObject
*/
void setMapLayer( QgsMapLayer* layer );
/**
* Returns the feature associated with the annotation, or an invalid
* feature if none has been set.
* @see setAssociatedFeature()
*/
QgsFeature associatedFeature() const { return mFeature; }
/**
* Sets the feature associated with the annotation.
* @see associatedFeature()
*/
virtual void setAssociatedFeature( const QgsFeature& feature ) { mFeature = feature; }
signals:
//! Emitted whenever the annotation's appearance changes
@ -345,6 +358,9 @@ class CORE_EXPORT QgsAnnotation : public QObject
//! Associated layer (or nullptr if not attached to a layer)
QPointer<QgsMapLayer> mMapLayer;
//! Associated feature, or invalid feature if no feature associated
QgsFeature mFeature;
};
#endif // QGSANNOTATION_H

View File

@ -38,9 +38,6 @@
QgsHtmlAnnotation::QgsHtmlAnnotation( QObject* parent )
: QgsAnnotation( parent )
, mWebPage( nullptr )
, mHasAssociatedFeature( false )
, mFeatureId( -1 )
{
mWebPage = new QgsWebPage();
mWebPage->mainFrame()->setScrollBarPolicy( Qt::Horizontal, Qt::ScrollBarAlwaysOff );
@ -48,8 +45,6 @@ QgsHtmlAnnotation::QgsHtmlAnnotation( QObject* parent )
mWebPage->setNetworkAccessManager( QgsNetworkAccessManager::instance() );
connect( mWebPage->mainFrame(), &QWebFrame::javaScriptWindowObjectCleared, this, &QgsHtmlAnnotation::javascript );
setFeatureForMapPosition();
}
QgsHtmlAnnotation::~QgsHtmlAnnotation()
@ -71,16 +66,10 @@ void QgsHtmlAnnotation::setSourceFile( const QString& htmlFile )
}
file.close();
setFeatureForMapPosition();
setAssociatedFeature( associatedFeature() );
emit appearanceChanged();
}
#if 0
void QgsHtmlAnnotation::setMapPosition( const QgsPoint& pos )
{
QgsAnnotationItem::setMapPosition( pos );
setFeatureForMapPosition();
}
#endif
void QgsHtmlAnnotation::renderAnnotation( QgsRenderContext& context, QSizeF size ) const
{
if ( !context.painter() )
@ -108,8 +97,6 @@ QSizeF QgsHtmlAnnotation::minimumFrameSize() const
void QgsHtmlAnnotation::writeXml( QDomElement& elem, QDomDocument & doc ) const
{
QDomElement formAnnotationElem = doc.createElement( QStringLiteral( "HtmlAnnotationItem" ) );
formAnnotationElem.setAttribute( QStringLiteral( "hasFeature" ), mHasAssociatedFeature );
formAnnotationElem.setAttribute( QStringLiteral( "feature" ), mFeatureId );
formAnnotationElem.setAttribute( QStringLiteral( "htmlfile" ), sourceFile() );
_writeXml( formAnnotationElem, doc );
@ -118,8 +105,6 @@ void QgsHtmlAnnotation::writeXml( QDomElement& elem, QDomDocument & doc ) const
void QgsHtmlAnnotation::readXml( const QDomElement& itemElem, const QDomDocument& doc )
{
mHasAssociatedFeature = itemElem.attribute( QStringLiteral( "hasFeature" ), QStringLiteral( "0" ) ).toInt();
mFeatureId = itemElem.attribute( QStringLiteral( "feature" ), QStringLiteral( "0" ) ).toInt();
mHtmlFile = itemElem.attribute( QStringLiteral( "htmlfile" ), QLatin1String( "" ) );
QDomElement annotationElem = itemElem.firstChildElement( QStringLiteral( "AnnotationItem" ) );
if ( !annotationElem.isNull() )
@ -139,34 +124,15 @@ void QgsHtmlAnnotation::readXml( const QDomElement& itemElem, const QDomDocument
}
}
void QgsHtmlAnnotation::setFeatureForMapPosition()
void QgsHtmlAnnotation::setAssociatedFeature( const QgsFeature& feature )
{
QgsAnnotation::setAssociatedFeature( feature );
QString newText;
if ( QgsVectorLayer* vectorLayer = qobject_cast< QgsVectorLayer* >( mapLayer() ) )
QgsVectorLayer* vectorLayer = qobject_cast< QgsVectorLayer* >( mapLayer() );
if ( feature.isValid() && vectorLayer )
{
double halfIdentifyWidth = 0; // QgsMapTool::searchRadiusMU( mMapCanvas );
QgsRectangle searchRect( mapPosition().x() - halfIdentifyWidth, mapPosition().y() - halfIdentifyWidth,
mapPosition().x() + halfIdentifyWidth, mapPosition().y() + halfIdentifyWidth );
QgsFeatureIterator fit = vectorLayer->getFeatures( QgsFeatureRequest().setFilterRect( searchRect ).setFlags( QgsFeatureRequest::NoGeometry | QgsFeatureRequest::ExactIntersect ) );
QgsFeature currentFeature;
QgsFeatureId currentFeatureId = 0;
bool featureFound = false;
while ( fit.nextFeature( currentFeature ) )
{
currentFeatureId = currentFeature.id();
featureFound = true;
break;
}
mHasAssociatedFeature = featureFound;
mFeatureId = currentFeatureId;
mFeature = currentFeature;
QgsExpressionContext context( QgsExpressionContextUtils::globalProjectLayerScopes( vectorLayer ) );
context.setFeature( mFeature );
context.setFeature( feature );
newText = QgsExpression::replaceExpressionText( mHtmlSource, &context );
}
else

View File

@ -45,9 +45,6 @@ class CORE_EXPORT QgsHtmlAnnotation: public QgsAnnotation
~QgsHtmlAnnotation();
QSizeF minimumFrameSize() const override;
#if 0
void setMapPosition( const QgsPoint& pos ) override;
#endif
/**
* Sets the file path for the source HTML file.
@ -64,27 +61,21 @@ class CORE_EXPORT QgsHtmlAnnotation: public QgsAnnotation
virtual void writeXml( QDomElement& elem, QDomDocument & doc ) const override;
virtual void readXml( const QDomElement& itemElem, const QDomDocument& doc ) override;
void setAssociatedFeature( const QgsFeature& feature ) override;
protected:
void renderAnnotation( QgsRenderContext& context, QSizeF size ) const override;
private slots:
//! Sets a feature for the current map position and updates the dialog
void setFeatureForMapPosition();
void javascript();
private:
QgsWebPage* mWebPage;
//! True if the item is related to a vector feature
bool mHasAssociatedFeature;
//! Associated feature
QgsFeatureId mFeatureId;
QgsFeature mFeature;
QgsWebPage* mWebPage = nullptr;
QString mHtmlFile;
QString mHtmlSource;
QString replaceText( QString displayText, QgsVectorLayer *layer, QgsFeature &feat );
};
#endif // QGSHTMLANNOTATION_H

View File

@ -38,23 +38,12 @@
QgsFormAnnotation::QgsFormAnnotation( QObject* parent )
: QgsAnnotation( parent )
, mDesignerWidget( nullptr )
, mHasAssociatedFeature( false )
, mFeature( -1 )
{
setFeatureForMapPosition();
}
QgsFormAnnotation::~QgsFormAnnotation()
{
delete mDesignerWidget;
}
{}
void QgsFormAnnotation::setDesignerForm( const QString& uiFile )
{
mDesignerForm = uiFile;
delete mDesignerWidget;
mDesignerWidget = createDesignerWidget( uiFile );
mDesignerWidget.reset( createDesignerWidget( uiFile ) );
if ( mDesignerWidget )
{
mMinimumSize = mDesignerWidget->minimumSize();
@ -81,25 +70,21 @@ QWidget* QgsFormAnnotation::createDesignerWidget( const QString& filePath )
//get feature and set attribute information
QgsAttributeEditorContext context;
QgsVectorLayer* vectorLayer = qobject_cast< QgsVectorLayer* >( mapLayer() );
if ( vectorLayer && mHasAssociatedFeature )
if ( vectorLayer && associatedFeature().isValid() )
{
QgsFeature f;
if ( vectorLayer->getFeatures( QgsFeatureRequest().setFilterFid( mFeature ).setFlags( QgsFeatureRequest::NoGeometry ) ).nextFeature( f ) )
QgsFields fields = vectorLayer->fields();
QgsAttributes attrs = associatedFeature().attributes();
for ( int i = 0; i < attrs.count(); ++i )
{
const QgsFields& fields = vectorLayer->fields();
QgsAttributes attrs = f.attributes();
for ( int i = 0; i < attrs.count(); ++i )
if ( i < fields.count() )
{
if ( i < fields.count() )
QWidget* attWidget = widget->findChild<QWidget*>( fields.at( i ).name() );
if ( attWidget )
{
QWidget* attWidget = widget->findChild<QWidget*>( fields.at( i ).name() );
if ( attWidget )
QgsEditorWidgetWrapper* eww = QgsEditorWidgetRegistry::instance()->create( vectorLayer, i, attWidget, widget, context );
if ( eww )
{
QgsEditorWidgetWrapper* eww = QgsEditorWidgetRegistry::instance()->create( vectorLayer, i, attWidget, widget, context );
if ( eww )
{
eww->setValue( attrs.at( i ) );
}
eww->setValue( attrs.at( i ) );
}
}
}
@ -108,14 +93,6 @@ QWidget* QgsFormAnnotation::createDesignerWidget( const QString& filePath )
return widget;
}
#if 0
void QgsFormAnnotation::setMapPosition( const QgsPoint& pos )
{
QgsAnnotationItem::setMapPosition( pos );
setFeatureForMapPosition();
}
#endif
void QgsFormAnnotation::renderAnnotation( QgsRenderContext& context, QSizeF size ) const
{
if ( !mDesignerWidget )
@ -155,8 +132,6 @@ QSizeF QgsFormAnnotation::preferredFrameSize() const
void QgsFormAnnotation::writeXml( QDomElement& elem, QDomDocument & doc ) const
{
QDomElement formAnnotationElem = doc.createElement( QStringLiteral( "FormAnnotationItem" ) );
formAnnotationElem.setAttribute( QStringLiteral( "hasFeature" ), mHasAssociatedFeature );
formAnnotationElem.setAttribute( QStringLiteral( "feature" ), mFeature );
formAnnotationElem.setAttribute( QStringLiteral( "designerForm" ), mDesignerForm );
_writeXml( formAnnotationElem, doc );
elem.appendChild( formAnnotationElem );
@ -164,8 +139,6 @@ void QgsFormAnnotation::writeXml( QDomElement& elem, QDomDocument & doc ) const
void QgsFormAnnotation::readXml( const QDomElement& itemElem, const QDomDocument& doc )
{
mHasAssociatedFeature = itemElem.attribute( QStringLiteral( "hasFeature" ), QStringLiteral( "0" ) ).toInt();
mFeature = itemElem.attribute( QStringLiteral( "feature" ), QStringLiteral( "0" ) ).toInt();
mDesignerForm = itemElem.attribute( QStringLiteral( "designerForm" ), QLatin1String( "" ) );
QDomElement annotationElem = itemElem.firstChildElement( QStringLiteral( "AnnotationItem" ) );
if ( !annotationElem.isNull() )
@ -178,44 +151,19 @@ void QgsFormAnnotation::readXml( const QDomElement& itemElem, const QDomDocument
setMapLayer( QgsProject::instance()->mapLayer( itemElem.attribute( QStringLiteral( "vectorLayer" ) ) ) );
}
mDesignerWidget = createDesignerWidget( mDesignerForm );
mDesignerWidget.reset( createDesignerWidget( mDesignerForm ) );
if ( mDesignerWidget )
{
setFrameBackgroundColor( mDesignerWidget->palette().color( QPalette::Window ) );
}
}
void QgsFormAnnotation::setFeatureForMapPosition()
void QgsFormAnnotation::setAssociatedFeature( const QgsFeature& feature )
{
QgsVectorLayer* vectorLayer = qobject_cast< QgsVectorLayer* >( mapLayer() );
if ( !vectorLayer )
{
return;
}
double halfIdentifyWidth = 0; //QgsMapTool::searchRadiusMU( mMapCanvas );
QgsRectangle searchRect( mapPosition().x() - halfIdentifyWidth, mapPosition().y() - halfIdentifyWidth,
mapPosition().x() + halfIdentifyWidth, mapPosition().y() + halfIdentifyWidth );
QgsFeatureIterator fit = vectorLayer->getFeatures( QgsFeatureRequest().setFilterRect( searchRect ).setFlags( QgsFeatureRequest::NoGeometry | QgsFeatureRequest::ExactIntersect ).setSubsetOfAttributes( QgsAttributeList() ) );
QgsFeature currentFeature;
QgsFeatureId currentFeatureId = 0;
bool featureFound = false;
while ( fit.nextFeature( currentFeature ) )
{
currentFeatureId = currentFeature.id();
featureFound = true;
break;
}
mHasAssociatedFeature = featureFound;
mFeature = currentFeatureId;
QgsAnnotation::setAssociatedFeature( feature );
//create new embedded widget
delete mDesignerWidget;
mDesignerWidget = createDesignerWidget( mDesignerForm );
mDesignerWidget.reset( createDesignerWidget( mDesignerForm ) );
if ( mDesignerWidget )
{
setFrameBackgroundColor( mDesignerWidget->palette().color( QPalette::Window ) );

View File

@ -20,10 +20,9 @@
#include "qgsannotation.h"
#include "qgsfeature.h"
#include <QWidget>
#include "qgis_gui.h"
class QGraphicsProxyWidget;
/** \ingroup gui
* An annotation item that embedds a designer form showing the feature attribute*/
class GUI_EXPORT QgsFormAnnotation: public QgsAnnotation
@ -31,36 +30,27 @@ class GUI_EXPORT QgsFormAnnotation: public QgsAnnotation
Q_OBJECT
public:
QgsFormAnnotation( QObject* parent = nullptr );
~QgsFormAnnotation();
QSizeF minimumFrameSize() const override;
//! Returns the optimal frame size
QSizeF preferredFrameSize() const;
#if 0
void setMapPosition( const QgsPoint& pos ) override;
#endif
void setDesignerForm( const QString& uiFile );
QString designerForm() const { return mDesignerForm; }
virtual void writeXml( QDomElement& elem, QDomDocument & doc ) const override;
virtual void readXml( const QDomElement& itemElem, const QDomDocument& doc ) override;
void setAssociatedFeature( const QgsFeature& feature ) override;
protected:
void renderAnnotation( QgsRenderContext& context, QSizeF size ) const override;
private slots:
//! Sets a feature for the current map position and updates the dialog
void setFeatureForMapPosition();
private:
QWidget* mDesignerWidget;
QScopedPointer<QWidget> mDesignerWidget;
QSize mMinimumSize;
//! True if the item is related to a vector feature
bool mHasAssociatedFeature;
//! Associated feature
QgsFeatureId mFeature;
//! Path to (and including) the .ui file
QString mDesignerForm;

View File

@ -18,6 +18,10 @@
#include "qgsmapcanvasannotationitem.h"
#include "qgsannotation.h"
#include "qgsmapcanvas.h"
#include "qgsmaptool.h"
#include "qgsvectorlayer.h"
#include "qgsfeatureiterator.h"
#include "qgscsexception.h"
#include <QPainter>
@ -28,11 +32,14 @@ QgsMapCanvasAnnotationItem::QgsMapCanvasAnnotationItem( QgsAnnotation* annotatio
setFlag( QGraphicsItem::ItemIsSelectable, true );
connect( mAnnotation, &QgsAnnotation::appearanceChanged, this, [this] { update(); } );
connect( mAnnotation, &QgsAnnotation::moved, this, [this] { updatePosition(); } );
connect( mAnnotation, &QgsAnnotation::moved, this, &QgsMapCanvasAnnotationItem::setFeatureForMapPosition );
connect( mAnnotation, &QgsAnnotation::appearanceChanged, this, &QgsMapCanvasAnnotationItem::updateBoundingRect );
connect( mMapCanvas, &QgsMapCanvas::layersChanged, this, &QgsMapCanvasAnnotationItem::onCanvasLayersChanged );
connect( mAnnotation, &QgsAnnotation::mapLayerChanged, this, &QgsMapCanvasAnnotationItem::onCanvasLayersChanged );
updatePosition();
setFeatureForMapPosition();
}
QgsMapCanvasAnnotationItem::~QgsMapCanvasAnnotationItem()
@ -106,6 +113,40 @@ void QgsMapCanvasAnnotationItem::onCanvasLayersChanged()
}
}
void QgsMapCanvasAnnotationItem::setFeatureForMapPosition()
{
if ( !mAnnotation || !mAnnotation->hasFixedMapPosition() )
return;
QgsVectorLayer* vectorLayer = qobject_cast< QgsVectorLayer* >( mAnnotation->mapLayer() );
if ( !vectorLayer )
return;
double halfIdentifyWidth = QgsMapTool::searchRadiusMU( mMapCanvas );
QgsPoint mapPosition = mAnnotation->mapPosition();
try
{
QgsCoordinateTransform ct( mAnnotation->mapPositionCrs(), mMapCanvas->mapSettings().destinationCrs() );
if ( ct.isValid() )
mapPosition = ct.transform( mapPosition );
}
catch ( QgsCsException & )
{
}
QgsRectangle searchRect( mapPosition.x() - halfIdentifyWidth, mapPosition.y() - halfIdentifyWidth,
mapPosition.x() + halfIdentifyWidth, mapPosition.y() + halfIdentifyWidth );
searchRect = mMapCanvas->mapSettings().mapToLayerCoordinates( vectorLayer, searchRect );
QgsFeatureIterator fit = vectorLayer->getFeatures( QgsFeatureRequest().setFilterRect( searchRect ).setFlags( QgsFeatureRequest::ExactIntersect ).setLimit( 1 ) );
QgsFeature currentFeature;
( void )fit.nextFeature( currentFeature );
mAnnotation->setAssociatedFeature( currentFeature );
}
void QgsMapCanvasAnnotationItem::drawSelectionBoxes( QPainter* p ) const
{
if ( !p )

View File

@ -90,6 +90,9 @@ class GUI_EXPORT QgsMapCanvasAnnotationItem: public QObject, public QgsMapCanvas
void onCanvasLayersChanged();
//! Sets a feature for the current map position
void setFeatureForMapPosition();
private:
//! Draws selection handles around the item

View File

@ -22,7 +22,9 @@ from qgis.core import (QgsTextAnnotation,
QgsCoordinateReferenceSystem,
QgsRectangle,
QgsRenderChecker,
QgsPoint)
QgsPoint,
QgsVectorLayer,
QgsFeature)
from qgis.PyQt.QtCore import (QDir,
QPointF,
QSizeF)
@ -79,6 +81,26 @@ class TestQgsAnnotation(unittest.TestCase):
im = self.renderAnnotation(a, QPointF(20, 30))
self.assertTrue(self.imageCheck('html_annotation', 'html_annotation', im))
def testHtmlAnnotationWithFeature(self):
""" test rendering a html annotation with a feature"""
layer = QgsVectorLayer("Point?crs=EPSG:3111&field=station:string&field=suburb:string",
'test', "memory")
a = QgsHtmlAnnotation()
a.setFrameSize(QSizeF(400, 250))
a.setFrameOffsetFromReferencePoint(QPointF(70, 90))
a.setMapLayer(layer)
html = TEST_DATA_DIR + "/test_html_feature.html"
a.setSourceFile(html)
im = self.renderAnnotation(a, QPointF(20, 30))
self.assertTrue(self.imageCheck('html_nofeature', 'html_nofeature', im))
f = QgsFeature(layer.fields())
f.setValid(True)
f.setAttributes(['hurstbridge', 'somewhere'])
a.setAssociatedFeature(f)
im = self.renderAnnotation(a, QPointF(20, 30))
self.assertTrue(self.imageCheck('html_feature', 'html_feature', im))
def testRelativePosition(self):
""" test rendering an annotation without map point"""
a = QgsHtmlAnnotation()

View File

@ -19,7 +19,9 @@ from qgis.core import (QgsTextAnnotation,
QgsCoordinateReferenceSystem,
QgsRectangle,
QgsPoint,
QgsVectorLayer)
QgsVectorLayer,
QgsFeature,
QgsGeometry)
from qgis.gui import (QgsMapCanvas,
QgsMapCanvasAnnotationItem)
@ -121,6 +123,42 @@ class TestQgsMapCanvasAnnotationItem(unittest.TestCase):
canvas.setLayers([layer])
self.assertTrue(i.isVisible())
def testSettingFeature(self):
""" test that feature is set when item moves """
a = QgsTextAnnotation()
a.setFrameSize(QSizeF(300, 200))
a.setFrameOffsetFromReferencePoint(QPointF(40, 50))
a.setHasFixedMapPosition(True)
a.setMapPosition(QgsPoint(12, 34))
a.setMapPositionCrs(QgsCoordinateReferenceSystem(4326))
canvas = QgsMapCanvas()
canvas.setDestinationCrs(QgsCoordinateReferenceSystem(4326))
canvas.setFrameStyle(0)
canvas.resize(600, 400)
canvas.setExtent(QgsRectangle(10, 30, 20, 35))
i = QgsMapCanvasAnnotationItem(a, canvas)
layer = QgsVectorLayer("Point?crs=EPSG:4326&field=station:string&field=suburb:string",
'test', "memory")
canvas.setLayers([layer])
f = QgsFeature(layer.fields())
f.setGeometry(QgsGeometry.fromPoint(QgsPoint(14, 31)))
f.setValid(True)
f.setAttributes(['hurstbridge', 'somewhere'])
self.assertTrue(layer.dataProvider().addFeatures([f]))
a.setMapLayer(layer)
self.assertFalse(a.associatedFeature().isValid())
a.setMapPosition(QgsPoint(14, 31))
self.assertTrue(a.associatedFeature().isValid())
self.assertEqual(a.associatedFeature().attributes()[0], 'hurstbridge')
a.setMapPosition(QgsPoint(17, 31))
self.assertFalse(a.associatedFeature().isValid())
if __name__ == '__main__':
unittest.main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

41
tests/testdata/test_html_feature.html vendored Normal file
View File

@ -0,0 +1,41 @@
<html>
<head>
<style>
body
{
background-color: transparent;
margin: 6px;
padding: 0px;
}
table
{
width: 300px;
border-spacing: 6px;
margin: 0px;
padding: 0px;
}
table tr
{
height: 40px;
}
table td
{
border: 2px solid black;
font-family: arial;
font-size: 18px;
font-weight: bold;
}
tr.row1
{
background-color: yellow;
}
</style>
</head>
<body>
<table>
<tbody>
<tr class="row1"><td>[% "station" %]</td><td>[% "suburb" %]</td></tr>
</tbody>
</table>
</body>
</html>