[labeling] Highlight all characters in curved labels when hovering

using a map label tool, instead of just one character
This commit is contained in:
Nyall Dawson 2022-03-15 15:08:13 +10:00
parent 52ff5f3941
commit 955cc8bf8c
10 changed files with 196 additions and 9 deletions

View File

@ -40,6 +40,13 @@ Returns the details of any labels placed at the specified point (in map coordina
QList<QgsLabelPosition> labelsWithinRect( const QgsRectangle &r ) const;
%Docstring
Returns the details of any labels placed within the specified rectangle (in map coordinates).
%End
QList<QgsLabelPosition> groupedLabelPositions( long long groupId ) const;
%Docstring
Returns a list of all label positions sharing the same group ID (i.e. positions for individual characters in a curved label).
.. versionadded:: 3.26
%End
QList<QgsCalloutPosition> calloutsWithinRectangle( const QgsRectangle &rectangle ) const;

View File

@ -83,6 +83,8 @@ Constructor for QgsLabelPosition
QString providerID;
bool isUnplaced;
long long groupedLabelId;
};
/************************************************************************

View File

@ -62,6 +62,7 @@ QgsLabelSearchTree keeps ownership, don't delete the returned objects.
.. versionadded:: 3.20
%End
void setMapSettings( const QgsMapSettings &settings );
%Docstring
Sets the map ``settings`` associated with the labeling run.

View File

@ -1194,11 +1194,26 @@ void QgsMapToolLabel::updateHoveredLabel( QgsMapMouseEvent *e )
mHoverRubberBand->show();
mHoverRubberBand->reset( QgsWkbTypes::LineGeometry );
mHoverRubberBand->addPoint( labelPos.cornerPoints.at( 0 ) );
mHoverRubberBand->addPoint( labelPos.cornerPoints.at( 1 ) );
mHoverRubberBand->addPoint( labelPos.cornerPoints.at( 2 ) );
mHoverRubberBand->addPoint( labelPos.cornerPoints.at( 3 ) );
mHoverRubberBand->addPoint( labelPos.cornerPoints.at( 0 ) );
if ( const QgsLabelingResults *labelingResults = mCanvas->labelingResults( false ) )
{
if ( labelPos.groupedLabelId != 0 )
{
// if it's a curved label, we need to highlight ALL characters
const QList< QgsLabelPosition > allPositions = labelingResults->groupedLabelPositions( labelPos.groupedLabelId );
for ( const QgsLabelPosition &position : allPositions )
{
mHoverRubberBand->addGeometry( position.labelGeometry );
}
}
else
{
mHoverRubberBand->addGeometry( labelPos.labelGeometry );
}
}
else
{
mHoverRubberBand->addGeometry( labelPos.labelGeometry );
}
QgisApp::instance()->statusBarIface()->showMessage( tr( "Label “%1” in %2" ).arg( labelPos.labelText, mCurrentHoverLabel.layer->name() ), 2000 );
}

View File

@ -65,6 +65,19 @@ QList<QgsLabelPosition> QgsLabelingResults::labelsWithinRect( const QgsRectangle
return positions;
}
QList<QgsLabelPosition> QgsLabelingResults::groupedLabelPositions( long long groupId ) const
{
QList<QgsLabelPosition> positions;
if ( mLabelSearchTree )
{
const QList<QgsLabelPosition *> positionPointers = mLabelSearchTree->groupedLabelPositions( groupId );
positions.reserve( positionPointers.size() );
for ( const QgsLabelPosition *pos : positionPointers )
positions.push_back( QgsLabelPosition( *pos ) );
}
return positions;
}
QList<QgsCalloutPosition> QgsLabelingResults::calloutsWithinRectangle( const QgsRectangle &rectangle ) const
{
QList<QgsCalloutPosition> positions;

View File

@ -57,6 +57,13 @@ class CORE_EXPORT QgsLabelingResults
*/
QList<QgsLabelPosition> labelsWithinRect( const QgsRectangle &r ) const;
/**
* Returns a list of all label positions sharing the same group ID (i.e. positions for individual characters in a curved label).
*
* \since QGIS 3.26
*/
QList<QgsLabelPosition> groupedLabelPositions( long long groupId ) const;
/**
* Returns a list of callouts with origins or destinations inside the given \a rectangle.
*

View File

@ -153,6 +153,13 @@ class CORE_EXPORT QgsLabelPosition
* \since QGIS 3.10
*/
bool isUnplaced = false;
/**
* If non zero, indicates that the label position is part of a group of label positions (i.e. a character in a curved label).
*
* All other linked positions will share the same groupedLabelId.
*/
long long groupedLabelId = 0;
};
#endif // QGSLABELPOSITION_H

View File

@ -73,7 +73,7 @@ void QgsLabelSearchTree::labelsInRect( const QgsRectangle &r, QList<QgsLabelPosi
}
}
bool QgsLabelSearchTree::insertLabel( pal::LabelPosition *labelPos, QgsFeatureId featureId, const QString &layerName, const QString &labeltext, const QFont &labelfont, bool diagram, bool pinned, const QString &providerId, bool isUnplaced )
bool QgsLabelSearchTree::insertLabel( pal::LabelPosition *labelPos, QgsFeatureId featureId, const QString &layerName, const QString &labeltext, const QFont &labelfont, bool diagram, bool pinned, const QString &providerId, bool isUnplaced, long long linkedId )
{
if ( !labelPos )
{
@ -97,16 +97,30 @@ bool QgsLabelSearchTree::insertLabel( pal::LabelPosition *labelPos, QgsFeatureId
yMax = std::max( yMax, res.y() );
}
pal::LabelPosition *next = labelPos->nextPart();
long long uniqueLinkedId = 0;
if ( linkedId != 0 )
uniqueLinkedId = linkedId;
else if ( next )
uniqueLinkedId = mNextFeatureId++;
const QgsRectangle bounds( xMin, yMin, xMax, yMax );
const QgsGeometry labelGeometry( QgsGeometry::fromPolygonXY( QVector<QgsPolylineXY>() << cornerPoints ) );
std::unique_ptr< QgsLabelPosition > newEntry = std::make_unique< QgsLabelPosition >( featureId, labelPos->getAlpha() + mMapSettings.rotation(), cornerPoints, bounds,
labelPos->getWidth(), labelPos->getHeight(), layerName, labeltext, labelfont, labelPos->getUpsideDown(), diagram, pinned, providerId, labelGeometry, isUnplaced );
newEntry->groupedLabelId = uniqueLinkedId;
mSpatialIndex.insert( newEntry.get(), bounds );
if ( uniqueLinkedId != 0 )
{
mLinkedLabelHash[ uniqueLinkedId ].append( newEntry.get() );
}
mOwnedPositions.emplace_back( std::move( newEntry ) );
if ( pal::LabelPosition *next = labelPos->nextPart() )
if ( next )
{
return insertLabel( next, featureId, layerName, labeltext, labelfont, diagram, pinned, providerId, isUnplaced );
return insertLabel( next, featureId, layerName, labeltext, labelfont, diagram, pinned, providerId, isUnplaced, uniqueLinkedId );
}
return true;
}
@ -141,6 +155,11 @@ QList<const QgsCalloutPosition *> QgsLabelSearchTree::calloutsInRectangle( const
return searchResults;
}
QList<QgsLabelPosition *> QgsLabelSearchTree::groupedLabelPositions( long long groupId ) const
{
return mLinkedLabelHash.value( groupId );
}
void QgsLabelSearchTree::setMapSettings( const QgsMapSettings &settings )
{
mMapSettings = settings;

View File

@ -92,7 +92,7 @@ class CORE_EXPORT QgsLabelSearchTree
* \returns TRUE in case of success
* \note not available in Python bindings
*/
bool insertLabel( pal::LabelPosition *labelPos, QgsFeatureId featureId, const QString &layerName, const QString &labeltext, const QFont &labelfont, bool diagram = false, bool pinned = false, const QString &providerId = QString(), bool isUnplaced = false ) SIP_SKIP;
bool insertLabel( pal::LabelPosition *labelPos, QgsFeatureId featureId, const QString &layerName, const QString &labeltext, const QFont &labelfont, bool diagram = false, bool pinned = false, const QString &providerId = QString(), bool isUnplaced = false, long long linkedId = 0 ) SIP_SKIP;
/**
* Inserts a rendered callout position.
@ -114,6 +114,16 @@ class CORE_EXPORT QgsLabelSearchTree
*/
QList<const QgsCalloutPosition *> calloutsInRectangle( const QgsRectangle &rectangle ) const;
/**
* Returns a list of all label positions sharing the same group ID (i.e. positions for individual characters in a curved label).
*
* QgsLabelSearchTree keeps ownership, don't delete the LabelPositions
*
* \note not available in Python bindings
* \since QGIS 3.26
*/
QList<QgsLabelPosition *> groupedLabelPositions( long long groupId ) const SIP_SKIP;
/**
* Sets the map \a settings associated with the labeling run.
* \since QGIS 3.4.8
@ -122,6 +132,8 @@ class CORE_EXPORT QgsLabelSearchTree
private:
QgsGenericSpatialIndex< QgsLabelPosition > mSpatialIndex;
long long mNextFeatureId = 1;
QHash< long long, QList< QgsLabelPosition * > > mLinkedLabelHash;
std::vector< std::unique_ptr< QgsLabelPosition > > mOwnedPositions;
QgsGenericSpatialIndex< QgsCalloutPosition > mCalloutIndex;
std::vector< std::unique_ptr< QgsCalloutPosition > > mOwnedCalloutPositions;

View File

@ -84,6 +84,7 @@ class TestQgsLabelingEngine : public QObject
void testLabelRotationUnit();
void drawUnplaced();
void labelingResults();
void labelingResultsCurved();
void labelingResultsWithCallouts();
void pointsetExtend();
void curvedOverrun();
@ -2458,6 +2459,109 @@ void TestQgsLabelingEngine::labelingResults()
QCOMPARE( labels.count(), 0 );
}
void TestQgsLabelingEngine::labelingResultsCurved()
{
// test retrieval of labeling results for curved placement
QgsPalLayerSettings settings;
setDefaultLabelParams( settings );
QgsTextFormat format = settings.format();
format.setSize( 20 );
format.setColor( QColor( 0, 0, 0 ) );
settings.setFormat( format );
settings.fieldName = QStringLiteral( "\"id\"" );
settings.isExpression = true;
settings.placement = QgsPalLayerSettings::Curved;
settings.priority = 10;
std::unique_ptr< QgsVectorLayer> vl2( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:4326&field=id:integer" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
vl2->setRenderer( new QgsNullSymbolRenderer() );
QgsFeature f;
f.setAttributes( QgsAttributes() << 1 );
f.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "LineString (-23.48732038587919746 58.94708170839115979, -11.017713405345674 60.99534928128858979)" ) ) );
QVERIFY( vl2->dataProvider()->addFeature( f ) );
f.setAttributes( QgsAttributes() << 8888 );
f.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "LineString (-16.22889244198655234 57.45670302419525655, -5.39708458725444817 57.53670918909697463)" ) ) );
QVERIFY( vl2->dataProvider()->addFeature( f ) );
f.setAttributes( QgsAttributes() << 33333 );
f.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "LineString (-23.52454309328377136 55.7985199122237816, -12.24606274969673869 53.62741130396216249)" ) ) );
QVERIFY( vl2->dataProvider()->addFeature( f ) );
vl2->updateExtents();
vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) ); // TODO: this should not be necessary!
vl2->setLabelsEnabled( true );
// make a fake render context
const QSize size( 640, 480 );
QgsMapSettings mapSettings;
mapSettings.setLabelingEngineSettings( createLabelEngineSettings() );
const QgsCoordinateReferenceSystem tgtCrs( QStringLiteral( "EPSG:3857" ) );
mapSettings.setDestinationCrs( tgtCrs );
mapSettings.setOutputSize( size );
mapSettings.setExtent( QgsRectangle( -4137976.6, 6557092.6, 1585557.4, 9656515.0 ) );
// mapSettings.setRotation( 60 );
mapSettings.setLayers( QList<QgsMapLayer *>() << vl2.get() );
mapSettings.setOutputDpi( 96 );
QgsLabelingEngineSettings engineSettings = mapSettings.labelingEngineSettings();
engineSettings.setFlag( QgsLabelingEngineSettings::UsePartialCandidates, false );
engineSettings.setFlag( QgsLabelingEngineSettings::DrawLabelRectOnly, true );
//engineSettings.setFlag( QgsLabelingEngineSettings::DrawCandidates, true );
mapSettings.setLabelingEngineSettings( engineSettings );
QgsMapRendererSequentialJob job( mapSettings );
job.start();
job.waitForFinished();
std::unique_ptr< QgsLabelingResults > results( job.takeLabelingResults() );
QVERIFY( results );
// retrieve some labels
QList<QgsLabelPosition> labels = results->allLabels();
QCOMPARE( labels.count(), 10 );
std::sort( labels.begin(), labels.end(), []( const QgsLabelPosition & a, const QgsLabelPosition & b )
{
return a.labelText.compare( b.labelText ) < 0;
} );
QCOMPARE( labels.at( 0 ).labelText, QStringLiteral( "1" ) );
QCOMPARE( labels.at( 1 ).labelText, QStringLiteral( "33333" ) );
long long group2 = labels.at( 1 ).groupedLabelId;
QCOMPARE( labels.at( 2 ).labelText, QStringLiteral( "33333" ) );
QCOMPARE( labels.at( 2 ).groupedLabelId, group2 );
QCOMPARE( labels.at( 3 ).labelText, QStringLiteral( "33333" ) );
QCOMPARE( labels.at( 3 ).groupedLabelId, group2 );
QCOMPARE( labels.at( 4 ).labelText, QStringLiteral( "33333" ) );
QCOMPARE( labels.at( 4 ).groupedLabelId, group2 );
QCOMPARE( labels.at( 5 ).labelText, QStringLiteral( "33333" ) );
QCOMPARE( labels.at( 5 ).groupedLabelId, group2 );
long long group3 = labels.at( 6 ).groupedLabelId;
QCOMPARE( labels.at( 6 ).labelText, QStringLiteral( "8888" ) );
QCOMPARE( labels.at( 7 ).labelText, QStringLiteral( "8888" ) );
QCOMPARE( labels.at( 7 ).groupedLabelId, group3 );
QCOMPARE( labels.at( 8 ).labelText, QStringLiteral( "8888" ) );
QCOMPARE( labels.at( 8 ).groupedLabelId, group3 );
QCOMPARE( labels.at( 9 ).labelText, QStringLiteral( "8888" ) );
QCOMPARE( labels.at( 9 ).groupedLabelId, group3 );
labels = results->groupedLabelPositions( group2 );
QCOMPARE( labels.size(), 5 );
QCOMPARE( labels.at( 0 ).labelText, QStringLiteral( "33333" ) );
QCOMPARE( labels.at( 1 ).labelText, QStringLiteral( "33333" ) );
QCOMPARE( labels.at( 2 ).labelText, QStringLiteral( "33333" ) );
QCOMPARE( labels.at( 3 ).labelText, QStringLiteral( "33333" ) );
QCOMPARE( labels.at( 4 ).labelText, QStringLiteral( "33333" ) );
labels = results->groupedLabelPositions( group3 );
QCOMPARE( labels.size(), 4 );
QCOMPARE( labels.at( 0 ).labelText, QStringLiteral( "8888" ) );
QCOMPARE( labels.at( 1 ).labelText, QStringLiteral( "8888" ) );
QCOMPARE( labels.at( 2 ).labelText, QStringLiteral( "8888" ) );
QCOMPARE( labels.at( 3 ).labelText, QStringLiteral( "8888" ) );
}
void TestQgsLabelingEngine::labelingResultsWithCallouts()
{
// test retrieval of rendered callout properties from labeling results