Skip to content

Commit

Permalink
[labeling] When label map tools are used to select a label and the
Browse files Browse the repository at this point in the history
user clicks on overlapping labels, prioritise either the labels in
the current layer OR fallback to picking the smallest candidate
label (since it will be the most difficult to select)
  • Loading branch information
nyalldawson committed Jul 16, 2019
1 parent 8420780 commit bf05575
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 7 deletions.
45 changes: 38 additions & 7 deletions src/app/qgsmaptoollabel.cpp
Expand Up @@ -48,18 +48,49 @@ bool QgsMapToolLabel::labelAtPosition( QMouseEvent *e, QgsLabelPosition &p )
{
QgsPointXY pt = toMapCoordinates( e->pos() );
const QgsLabelingResults *labelingResults = mCanvas->labelingResults();
if ( labelingResults )
if ( !labelingResults )
return false;

QList<QgsLabelPosition> labelPosList = labelingResults->labelsAtPosition( pt );
if ( labelPosList.empty() )
return false;

// prioritise labels in the current selected layer, in case of overlaps
QList<QgsLabelPosition> activeLayerLabels;
if ( const QgsVectorLayer *currentLayer = qobject_cast< QgsVectorLayer * >( mCanvas->currentLayer() ) )
{
QList<QgsLabelPosition> labelPosList = labelingResults->labelsAtPosition( pt );
QList<QgsLabelPosition>::const_iterator posIt = labelPosList.constBegin();
if ( posIt != labelPosList.constEnd() )
for ( const QgsLabelPosition &pos : qgis::as_const( labelPosList ) )
{
p = *posIt;
return true;
if ( pos.layerID == currentLayer->id() )
{
activeLayerLabels.append( pos );
}
}
}
if ( !activeLayerLabels.empty() )
labelPosList = activeLayerLabels;

return false;
if ( labelPosList.count() > 1 )
{
// multiple candidates found, so choose the smallest (i.e. most difficult to select otherwise)
double minSize = std::numeric_limits< double >::max();
for ( const QgsLabelPosition &pos : qgis::as_const( labelPosList ) )
{
const double labelSize = pos.width * pos.height;
if ( labelSize < minSize )
{
minSize = labelSize;
p = pos;
}
}
}
else
{
// only one candidate
p = labelPosList.at( 0 );
}

return true;
}

void QgsMapToolLabel::createRubberBands()
Expand Down
2 changes: 2 additions & 0 deletions src/app/qgsmaptoollabel.h
Expand Up @@ -188,6 +188,8 @@ class APP_EXPORT QgsMapToolLabel: public QgsMapTool

QList<QgsPalLayerSettings::Property> mPalProperties;
QList<QgsDiagramLayerSettings::Property> mDiagramProperties;

friend class TestQgsMapToolLabel;
};

#endif // QGSMAPTOOLLABEL_H
2 changes: 2 additions & 0 deletions tests/src/app/CMakeLists.txt
Expand Up @@ -11,6 +11,7 @@ INCLUDE_DIRECTORIES(
${CMAKE_SOURCE_DIR}/src/core/locator
${CMAKE_SOURCE_DIR}/src/core/metadata
${CMAKE_SOURCE_DIR}/src/core/mesh
${CMAKE_SOURCE_DIR}/src/core/pal
${CMAKE_SOURCE_DIR}/src/core/raster
${CMAKE_SOURCE_DIR}/src/core/symbology
${CMAKE_SOURCE_DIR}/src/core/validity
Expand Down Expand Up @@ -108,6 +109,7 @@ ADD_QGIS_TEST(fieldcalculatortest testqgsfieldcalculator.cpp)
ADD_QGIS_TEST(maptooladdfeatureline testqgsmaptooladdfeatureline.cpp)
ADD_QGIS_TEST(maptooladdfeaturepoint testqgsmaptooladdfeaturepoint.cpp)
ADD_QGIS_TEST(maptoolidentifyaction testqgsmaptoolidentifyaction.cpp)
ADD_QGIS_TEST(maptoollabel testqgsmaptoollabel.cpp)
ADD_QGIS_TEST(maptoolselect testqgsmaptoolselect.cpp)
ADD_QGIS_TEST(maptoolreshape testqgsmaptoolreshape.cpp)
ADD_QGIS_TEST(maptoolcircularstringtest testqgsmaptoolcircularstring.cpp)
Expand Down
230 changes: 230 additions & 0 deletions tests/src/app/testqgsmaptoollabel.cpp
@@ -0,0 +1,230 @@
/***************************************************************************
testqgsmaptoollabel.cpp
-----------------------
Date : July 2019
Copyright : (C) 2019 by Nyall Dawson
Email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/

#include "qgstest.h"
#include <QObject>
#include <QString>
#include <QMouseEvent>

#include "qgsapplication.h"
#include "qgsvectorlayer.h"
#include "qgsvectordataprovider.h"
#include "qgsgeometry.h"
#include "qgsmapcanvas.h"
#include "qgsmaptoollabel.h"
#include "qgsfontutils.h"
#include "qgsvectorlayerlabelprovider.h"
#include "qgsvectorlayerlabeling.h"

class TestQgsMapToolLabel : public QObject
{
Q_OBJECT

public:
TestQgsMapToolLabel() = default;

private:

private slots:

void initTestCase()
{
QgsApplication::init();
QgsApplication::initQgis();

}

void cleanupTestCase()
{
QgsApplication::exitQgis();
}

void testSelectLabel()
{
std::unique_ptr< QgsVectorLayer > vl1 = qgis::make_unique< QgsVectorLayer >( QStringLiteral( "Point?crs=epsg:3946&field=text:string" ), QStringLiteral( "vl1" ), QStringLiteral( "memory" ) );
QVERIFY( vl1->isValid() );
QgsFeature f1;
f1.setAttributes( QgsAttributes() << QStringLiteral( "label" ) );
f1.setGeometry( QgsGeometry::fromPointXY( QgsPointXY( 1, 1 ) ) );
QVERIFY( vl1->dataProvider()->addFeature( f1 ) );
f1.setGeometry( QgsGeometry::fromPointXY( QgsPointXY( 3, 3 ) ) );
f1.setAttributes( QgsAttributes() << QStringLiteral( "l" ) );
QVERIFY( vl1->dataProvider()->addFeature( f1 ) );

std::unique_ptr< QgsVectorLayer > vl2 = qgis::make_unique< QgsVectorLayer >( QStringLiteral( "Point?crs=epsg:3946&field=text:string" ), QStringLiteral( "vl1" ), QStringLiteral( "memory" ) );
QVERIFY( vl2->isValid() );
f1.setGeometry( QgsGeometry::fromPointXY( QgsPointXY( 1, 1 ) ) );
f1.setAttributes( QgsAttributes() << QStringLiteral( "label" ) );
QVERIFY( vl2->dataProvider()->addFeature( f1 ) );
f1.setGeometry( QgsGeometry::fromPointXY( QgsPointXY( 3, 3 ) ) );
f1.setAttributes( QgsAttributes() << QStringLiteral( "label2" ) );
QVERIFY( vl2->dataProvider()->addFeature( f1 ) );
f1.setGeometry( QgsGeometry::fromPointXY( QgsPointXY( 3, 1 ) ) );
f1.setAttributes( QgsAttributes() << QStringLiteral( "label3" ) );
QVERIFY( vl2->dataProvider()->addFeature( f1 ) );

std::unique_ptr< QgsMapCanvas > canvas = qgis::make_unique< QgsMapCanvas >();
canvas->setDestinationCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3946" ) ) );
canvas->setLayers( QList<QgsMapLayer *>() << vl1.get() << vl2.get() );

QgsMapSettings mapSettings;
mapSettings.setOutputSize( QSize( 500, 500 ) );
mapSettings.setExtent( QgsRectangle( -1, -1, 4, 4 ) );
QVERIFY( mapSettings.hasValidSettings() );

mapSettings.setLayers( QList<QgsMapLayer *>() << vl1.get() << vl2.get() );

canvas->setFrameStyle( QFrame::NoFrame );
canvas->resize( 500, 500 );
canvas->setExtent( QgsRectangle( -1, -1, 4, 4 ) );
canvas->show(); // to make the canvas resize
canvas->hide();
QCOMPARE( canvas->mapSettings().outputSize(), QSize( 500, 500 ) );
QCOMPARE( canvas->mapSettings().visibleExtent(), QgsRectangle( -1, -1, 4, 4 ) );

std::unique_ptr< QgsMapToolLabel > tool( new QgsMapToolLabel( canvas.get() ) );

// no labels yet
QgsPointXY pt;
pt = tool->canvas()->mapSettings().mapToPixel().transform( 1, 1 );
std::unique_ptr< QMouseEvent > event( new QMouseEvent(
QEvent::MouseButtonPress,
QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
) );
QgsLabelPosition pos;
QVERIFY( !tool->labelAtPosition( event.get(), pos ) );

// add some labels
QgsPalLayerSettings pls1;
pls1.fieldName = QStringLiteral( "text" );
pls1.placement = QgsPalLayerSettings::OverPoint;
pls1.quadOffset = QgsPalLayerSettings::QuadrantOver;
pls1.displayAll = true;
QgsTextFormat format = pls1.format();
format.setFont( QgsFontUtils::getStandardTestFont( QStringLiteral( "Bold" ) ) );
format.setSize( 12 );
pls1.setFormat( format );

vl1->setLabeling( new QgsVectorLayerSimpleLabeling( pls1 ) );
vl1->setLabelsEnabled( true );

QEventLoop loop;
connect( canvas.get(), &QgsMapCanvas::mapCanvasRefreshed, &loop, &QEventLoop::quit );
canvas->refreshAllLayers();
canvas->show();
loop.exec();

QVERIFY( canvas->labelingResults() );
QVERIFY( tool->labelAtPosition( event.get(), pos ) );
QCOMPARE( pos.layerID, vl1->id() );
QCOMPARE( pos.labelText, QStringLiteral( "label" ) );

pt = tool->canvas()->mapSettings().mapToPixel().transform( 3, 3 );
event = qgis::make_unique< QMouseEvent >(
QEvent::MouseButtonPress,
QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
);
QVERIFY( tool->labelAtPosition( event.get(), pos ) );
QCOMPARE( pos.layerID, vl1->id() );
QCOMPARE( pos.labelText, QStringLiteral( "l" ) );

pt = tool->canvas()->mapSettings().mapToPixel().transform( 3, 1 );
event = qgis::make_unique< QMouseEvent >(
QEvent::MouseButtonPress,
QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
);
QVERIFY( !tool->labelAtPosition( event.get(), pos ) );

// label second layer
vl2->setLabeling( new QgsVectorLayerSimpleLabeling( pls1 ) );
vl2->setLabelsEnabled( true );

canvas->refreshAllLayers();
canvas->show();
loop.exec();

// should prioritise current layer
canvas->setCurrentLayer( vl1.get() );
pt = tool->canvas()->mapSettings().mapToPixel().transform( 1, 1 );
event = qgis::make_unique< QMouseEvent >(
QEvent::MouseButtonPress,
QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
);
QVERIFY( tool->labelAtPosition( event.get(), pos ) );
QCOMPARE( pos.layerID, vl1->id() );
QCOMPARE( pos.labelText, QStringLiteral( "label" ) );

pt = tool->canvas()->mapSettings().mapToPixel().transform( 3, 3 );
event = qgis::make_unique< QMouseEvent >(
QEvent::MouseButtonPress,
QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
);
QVERIFY( tool->labelAtPosition( event.get(), pos ) );
QCOMPARE( pos.layerID, vl1->id() );
QCOMPARE( pos.labelText, QStringLiteral( "l" ) );

//... but fallback to any labels if nothing in current layer
pt = tool->canvas()->mapSettings().mapToPixel().transform( 3, 1 );
event = qgis::make_unique< QMouseEvent >(
QEvent::MouseButtonPress,
QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
);
QVERIFY( tool->labelAtPosition( event.get(), pos ) );
QCOMPARE( pos.layerID, vl2->id() );
QCOMPARE( pos.labelText, QStringLiteral( "label3" ) );

canvas->setCurrentLayer( vl2.get() );
pt = tool->canvas()->mapSettings().mapToPixel().transform( 1, 1 );
event = qgis::make_unique< QMouseEvent >(
QEvent::MouseButtonPress,
QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
);
QVERIFY( tool->labelAtPosition( event.get(), pos ) );
QCOMPARE( pos.layerID, vl2->id() );
QCOMPARE( pos.labelText, QStringLiteral( "label" ) );

pt = tool->canvas()->mapSettings().mapToPixel().transform( 3, 3 );
event = qgis::make_unique< QMouseEvent >(
QEvent::MouseButtonPress,
QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
);
QVERIFY( tool->labelAtPosition( event.get(), pos ) );
QCOMPARE( pos.layerID, vl2->id() );
QCOMPARE( pos.labelText, QStringLiteral( "label2" ) );
pt = tool->canvas()->mapSettings().mapToPixel().transform( 3, 1 );
event = qgis::make_unique< QMouseEvent >(
QEvent::MouseButtonPress,
QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
);
QVERIFY( tool->labelAtPosition( event.get(), pos ) );
QCOMPARE( pos.layerID, vl2->id() );
QCOMPARE( pos.labelText, QStringLiteral( "label3" ) );

canvas->setCurrentLayer( nullptr );

// when multiple candidates exist, pick the smallest
pt = tool->canvas()->mapSettings().mapToPixel().transform( 3, 3 );
event = qgis::make_unique< QMouseEvent >(
QEvent::MouseButtonPress,
QPoint( std::round( pt.x() ), std::round( pt.y() ) ), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier
);
QVERIFY( tool->labelAtPosition( event.get(), pos ) );
QCOMPARE( pos.layerID, vl1->id() );
QCOMPARE( pos.labelText, QStringLiteral( "l" ) );
}
};

QGSTEST_MAIN( TestQgsMapToolLabel )
#include "testqgsmaptoollabel.moc"

0 comments on commit bf05575

Please sign in to comment.