Skip to content

Commit

Permalink
[FEATURE] "Cartographic" placement mode for point labels
Browse files Browse the repository at this point in the history
In this placement mode, point label candidates are generated
following ideal cartographic placement rules, eg labels
placements are priortised in the order:
- top right
- top left
- bottom right
- bottom left
- middle right
- middle left
- top, slightly right
- bottom, slightly left
(respecting the guidelines from Krygier and Wood (2011) and other
cartographic textbooks)

Placement priority can also be set for an individual feature using
a data defined list of prioritised positions. This also allows for
only certain placements to be used, so eg for coastal features you
could prevent labels being placed over the land.

TODO:
- while the ordering can be customised by editing a project file,
there's no GUI to customise this ordering if you want to deviate
from this standard priority (and it's out of scope for this
current work)
- tests

Sponsored by Andreas Neumann
  • Loading branch information
nyalldawson committed Jan 11, 2016
1 parent a3cee7d commit b589856
Show file tree
Hide file tree
Showing 12 changed files with 475 additions and 77 deletions.
14 changes: 13 additions & 1 deletion src/app/qgslabelinggui.cpp
Expand Up @@ -244,8 +244,9 @@ QgsLabelingGui::QgsLabelingGui( QgsVectorLayer* layer, QgsMapCanvas* mapCanvas,
connect( chkLineAbove, SIGNAL( toggled( bool ) ), this, SLOT( updatePlacementWidgets() ) );
connect( chkLineBelow, SIGNAL( toggled( bool ) ), this, SLOT( updatePlacementWidgets() ) );

// setup point placement button group (assigned enum id currently unused)
// setup point placement button group
mPlacePointBtnGrp = new QButtonGroup( this );
mPlacePointBtnGrp->addButton( radPredefinedOrder, ( int )QgsPalLayerSettings::OrderedPositionsAroundPoint );
mPlacePointBtnGrp->addButton( radAroundPoint, ( int )QgsPalLayerSettings::AroundPoint );
mPlacePointBtnGrp->addButton( radOverPoint, ( int )QgsPalLayerSettings::OverPoint );
mPlacePointBtnGrp->setExclusive( true );
Expand Down Expand Up @@ -358,6 +359,9 @@ void QgsLabelingGui::init()
radOverPoint->setChecked( true );
radOverCentroid->setChecked( true );
break;
case QgsPalLayerSettings::OrderedPositionsAroundPoint:
radPredefinedOrder->setChecked( true );
break;
case QgsPalLayerSettings::Line:
radLineParallel->setChecked( true );
radPolygonPerimeter->setChecked( true );
Expand Down Expand Up @@ -659,6 +663,10 @@ QgsPalLayerSettings QgsLabelingGui::layerSettings()
{
lyr.placement = QgsPalLayerSettings::OverPoint;
}
else if ( curPlacementWdgt == pagePoint && radPredefinedOrder->isChecked() )
{
lyr.placement = QgsPalLayerSettings::OrderedPositionsAroundPoint;
}
else if (( curPlacementWdgt == pageLine && radLineParallel->isChecked() )
|| ( curPlacementWdgt == pagePolygon && radPolygonPerimeter->isChecked() )
|| ( curPlacementWdgt == pageLine && radLineCurved->isChecked() ) )
Expand Down Expand Up @@ -1377,6 +1385,10 @@ void QgsLabelingGui::updatePlacementWidgets()
showOffsetFrame = true;
showRotationFrame = true;
}
else if ( curWdgt == pagePoint && radPredefinedOrder->isChecked() )
{
showDistanceFrame = true;
}
else if (( curWdgt == pageLine && radLineParallel->isChecked() )
|| ( curWdgt == pagePolygon && radPolygonPerimeter->isChecked() )
|| ( curWdgt == pageLine && radLineCurved->isChecked() ) )
Expand Down
164 changes: 144 additions & 20 deletions src/core/pal/feature.cpp
Expand Up @@ -227,7 +227,7 @@ LabelPosition::Quadrant FeaturePart::quadrantFromOffset() const
}
}

int FeaturePart::setPositionOverPoint( double x, double y, QList< LabelPosition*>& lPos, double angle, PointSet *mapShape )
int FeaturePart::createCandidatesOverPoint( double x, double y, QList< LabelPosition*>& lPos, double angle, PointSet *mapShape )
{
int nbp = 1;

Expand Down Expand Up @@ -306,7 +306,129 @@ int FeaturePart::setPositionOverPoint( double x, double y, QList< LabelPosition*
return nbp;
}

int FeaturePart::setPositionForPoint( double x, double y, QList< LabelPosition* >& lPos, double angle, PointSet *mapShape )
int FeaturePart::createCandidatesAtOrderedPositionsOverPoint( double x, double y, QList<LabelPosition*>& lPos, double angle )
{
QVector< QgsPalLayerSettings::PredefinedPointPosition > positions = mLF->predefinedPositionOrder();
double labelWidth = getLabelWidth();
double labelHeight = getLabelHeight();
double distanceToLabel = getLabelDistance();
const QgsLabelFeature::VisualMargin& visualMargin = mLF->visualMargin();

double cost = 0.0001;
int i = 0;
Q_FOREACH ( QgsPalLayerSettings::PredefinedPointPosition position, positions )
{
double alpha = 0.0;
double deltaX = 0;
double deltaY = 0;
LabelPosition::Quadrant quadrant;
switch ( position )
{
case QgsPalLayerSettings::TopLeft:
quadrant = LabelPosition::QuadrantAboveLeft;
alpha = 2.3561944902; //315 degrees
deltaX = -labelWidth + visualMargin.right;
deltaY = -visualMargin.bottom;
break;

case QgsPalLayerSettings::TopSlightlyLeft:
quadrant = LabelPosition::QuadrantAboveRight; //right quadrant, so labels are left-aligned
alpha = 1.5707963268; //0 degrees;
deltaX = -labelWidth / 4.0 - visualMargin.left;
deltaY = -visualMargin.bottom;
break;

case QgsPalLayerSettings::TopMiddle:
quadrant = LabelPosition::QuadrantAbove;
alpha = 1.5707963268; //0 degrees
deltaX = -labelWidth / 2.0;
deltaY = -visualMargin.bottom;
break;

case QgsPalLayerSettings::TopSlightlyRight:
quadrant = LabelPosition::QuadrantAboveLeft; //left quadrant, so labels are right-aligned
alpha = 1.5707963268; //0 degrees
deltaX = -labelWidth * 3.0 / 4.0 + visualMargin.right;
deltaY = -visualMargin.bottom;
break;

case QgsPalLayerSettings::TopRight:
quadrant = LabelPosition::QuadrantAboveRight;
alpha = 0.7853981634; // 45.0 degrees
deltaX = - visualMargin.left;
deltaY = -visualMargin.bottom;
break;

case QgsPalLayerSettings::MiddleLeft:
quadrant = LabelPosition::QuadrantLeft;
alpha = 3.1415926536; // 270.0 degrees
deltaX = -labelWidth + visualMargin.right;
deltaY = -labelHeight / 2.0;// TODO - should this be adjusted by visual margin??
break;

case QgsPalLayerSettings::MiddleRight:
quadrant = LabelPosition::QuadrantRight;
alpha = 0.0; // 90.0 degrees
deltaX = -visualMargin.left;
deltaY = -labelHeight / 2.0;// TODO - should this be adjusted by visual margin??
break;

case QgsPalLayerSettings::BottomLeft:
quadrant = LabelPosition::QuadrantBelowLeft;
alpha = 3.926990817; // 225.0 degrees
deltaX = -labelWidth + visualMargin.right;
deltaY = -labelHeight + visualMargin.top;
break;

case QgsPalLayerSettings::BottomSlightlyLeft:
quadrant = LabelPosition::QuadrantBelowRight; //right quadrant, so labels are left-aligned
alpha = 4.7123889804; // 180.0 degrees
deltaX = -labelWidth / 4.0 - visualMargin.left;
deltaY = -labelHeight + visualMargin.top;
break;

case QgsPalLayerSettings::BottomMiddle:
quadrant = LabelPosition::QuadrantBelow;
alpha = 4.7123889804; // 180.0 degrees
deltaX = -labelWidth / 2.0;
deltaY = -labelHeight + visualMargin.top;
break;

case QgsPalLayerSettings::BottomSlightlyRight:
quadrant = LabelPosition::QuadrantBelowLeft; //left quadrant, so labels are right-aligned
alpha = 4.7123889804; // 180.0 degrees
deltaX = -labelWidth * 3.0 / 4.0 + visualMargin.right;
deltaY = -labelHeight + visualMargin.top;
break;

case QgsPalLayerSettings::BottomRight:
quadrant = LabelPosition::QuadrantBelowRight;
alpha = 5.4977871438; // 135.0 degrees
deltaX = -visualMargin.left;
deltaY = -labelHeight + visualMargin.top;
break;
}

//have bearing, distance - calculate reference point
double referenceX = cos( alpha ) * distanceToLabel + x;
double referenceY = sin( alpha ) * distanceToLabel + y;

double labelX = referenceX + deltaX;
double labelY = referenceY + deltaY;

lPos << new LabelPosition( i, labelX, labelY, labelWidth, labelHeight, angle, cost, this, false, quadrant );


//TODO - tweak
cost += 0.001;

++i;
}

return lPos.count();
}

int FeaturePart::createCandidatesAroundPoint( double x, double y, QList< LabelPosition* >& lPos, double angle, PointSet *mapShape )
{
double labelWidth = getLabelWidth();
double labelHeight = getLabelHeight();
Expand Down Expand Up @@ -460,7 +582,7 @@ int FeaturePart::setPositionForPoint( double x, double y, QList< LabelPosition*
}

// TODO work with squared distance by removing call to sqrt or dist_euc2d
int FeaturePart::setPositionForLine( QList< LabelPosition* >& lPos, PointSet *mapShape )
int FeaturePart::createCandidatesAlongLine( QList< LabelPosition* >& lPos, PointSet *mapShape )
{
int i;
double distlabel = getLabelDistance();
Expand Down Expand Up @@ -821,7 +943,7 @@ static LabelPosition* _createCurvedCandidate( LabelPosition* lp, double angle, d
return newLp;
}

int FeaturePart::setPositionForLineCurved( QList< LabelPosition* >& lPos, PointSet* mapShape )
int FeaturePart::createCurvedCandidatesAlongLine( QList< LabelPosition* >& lPos, PointSet* mapShape )
{
LabelInfo* li = mLF->curvedLabelInfo();

Expand Down Expand Up @@ -956,7 +1078,7 @@ int FeaturePart::setPositionForLineCurved( QList< LabelPosition* >& lPos, PointS
*
*/

int FeaturePart::setPositionForPolygon( QList< LabelPosition*>& lPos, PointSet *mapShape )
int FeaturePart::createCandidatesForPolygon( QList< LabelPosition*>& lPos, PointSet *mapShape )
{
int i;
int j;
Expand Down Expand Up @@ -1162,16 +1284,16 @@ int FeaturePart::setPositionForPolygon( QList< LabelPosition*>& lPos, PointSet *
return nbp;
}

int FeaturePart::setPosition( QList< LabelPosition*>& lPos,
double bbox_min[2], double bbox_max[2],
PointSet *mapShape, RTree<LabelPosition*, double, 2, double> *candidates )
int FeaturePart::createCandidates( QList< LabelPosition*>& lPos,
double bboxMin[2], double bboxMax[2],
PointSet *mapShape, RTree<LabelPosition*, double, 2, double>* candidates )
{
double bbox[4];

bbox[0] = bbox_min[0];
bbox[1] = bbox_min[1];
bbox[2] = bbox_max[0];
bbox[3] = bbox_max[1];
bbox[0] = bboxMin[0];
bbox[1] = bboxMin[1];
bbox[2] = bboxMax[0];
bbox[3] = bboxMax[1];

double angle = mLF->hasFixedAngle() ? mLF->fixedAngle() : 0.0;

Expand All @@ -1185,15 +1307,17 @@ int FeaturePart::setPosition( QList< LabelPosition*>& lPos,
{
case GEOS_POINT:
if ( mLF->layer()->arrangement() == QgsPalLayerSettings::OverPoint || mLF->hasFixedQuadrant() )
setPositionOverPoint( x[0], y[0], lPos, angle );
createCandidatesOverPoint( x[0], y[0], lPos, angle );
else if ( mLF->layer()->arrangement() == QgsPalLayerSettings::OrderedPositionsAroundPoint )
createCandidatesAtOrderedPositionsOverPoint( x[0], y[0], lPos, angle );
else
setPositionForPoint( x[0], y[0], lPos, angle );
createCandidatesAroundPoint( x[0], y[0], lPos, angle );
break;
case GEOS_LINESTRING:
if ( mLF->layer()->arrangement() == QgsPalLayerSettings::Curved )
setPositionForLineCurved( lPos, mapShape );
createCurvedCandidatesAlongLine( lPos, mapShape );
else
setPositionForLine( lPos, mapShape );
createCandidatesAlongLine( lPos, mapShape );
break;

case GEOS_POLYGON:
Expand All @@ -1204,15 +1328,15 @@ int FeaturePart::setPosition( QList< LabelPosition*>& lPos,
double cx, cy;
mapShape->getCentroid( cx, cy, mLF->layer()->centroidInside() );
if ( mLF->layer()->arrangement() == QgsPalLayerSettings::OverPoint )
setPositionOverPoint( cx, cy, lPos, angle, mapShape );
createCandidatesOverPoint( cx, cy, lPos, angle, mapShape );
else
setPositionForPoint( cx, cy, lPos, angle, mapShape );
createCandidatesAroundPoint( cx, cy, lPos, angle, mapShape );
break;
case QgsPalLayerSettings::Line:
setPositionForLine( lPos, mapShape );
createCandidatesAlongLine( lPos, mapShape );
break;
default:
setPositionForPolygon( lPos, mapShape );
createCandidatesForPolygon( lPos, mapShape );
break;
}
}
Expand Down
65 changes: 37 additions & 28 deletions src/core/pal/feature.h
Expand Up @@ -100,6 +100,28 @@ namespace pal
*/
virtual ~FeaturePart();

/** Returns the parent feature.
*/
QgsLabelFeature* feature() { return mLF; }

/** Returns the layer that feature belongs to.
*/
Layer* layer();

/** Returns the unique ID of the feature.
*/
QgsFeatureId featureId() const;

/** Generic method to generate label candidates for the feature.
* \param lPos pointer to an array of candidates, will be filled by generated candidates
* \param bboxMin min values of the map extent
* \param bboxMax max values of the map extent
* \param mapShape generate candidates for this spatial entity
* \param candidates index for candidates
* \return the number of candidates generated in lPos
*/
int createCandidates( QList<LabelPosition *> &lPos, double bboxMin[2], double bboxMax[2], PointSet *mapShape, RTree<LabelPosition*, double, 2, double>* candidates );

/** Generate candidates for point feature, located around a specified point.
* @param x x coordinate of the point
* @param y y coordinate of the point
Expand All @@ -108,7 +130,7 @@ namespace pal
* @param mapShape optional geometry of source polygon
* @returns the number of generated candidates
*/
int setPositionForPoint( double x, double y, QList<LabelPosition *> &lPos, double angle, PointSet *mapShape = nullptr );
int createCandidatesAroundPoint( double x, double y, QList<LabelPosition *> &lPos, double angle, PointSet *mapShape = nullptr );

/** Generate one candidate over or offset the specified point.
* @param x x coordinate of the point
Expand All @@ -118,14 +140,24 @@ namespace pal
* @param mapShape optional geometry of source polygon
* @returns the number of generated candidates (always 1)
*/
int setPositionOverPoint( double x, double y, QList<LabelPosition *> &lPos, double angle, PointSet *mapShape = nullptr );
int createCandidatesOverPoint( double x, double y, QList<LabelPosition *> &lPos, double angle, PointSet *mapShape = nullptr );

/** Generates candidates following a prioritised list of predefined positions around a point.
* @param x x coordinate of the point
* @param y y coordinate of the point
* @param lPos pointer to an array of candidates, will be filled by generated candidate
* @param angle orientation of the label
* @param mapShape optional geometry of source polygon
* @returns the number of generated candidates
*/
int createCandidatesAtOrderedPositionsOverPoint( double x, double y, QList<LabelPosition *> &lPos, double angle );

/** Generate candidates for line feature.
* @param lPos pointer to an array of candidates, will be filled by generated candidates
* @param mapShape a pointer to the line
* @returns the number of generated candidates
*/
int setPositionForLine( QList<LabelPosition *> &lPos, PointSet *mapShape );
int createCandidatesAlongLine( QList<LabelPosition *> &lPos, PointSet *mapShape );

LabelPosition* curvedPlacementAtOffset( PointSet* path_positions, double* path_distances,
int orientation, int index, double distance );
Expand All @@ -135,37 +167,14 @@ namespace pal
* @param mapShape a pointer to the line
* @returns the number of generated candidates
*/
int setPositionForLineCurved( QList<LabelPosition *> &lPos, PointSet* mapShape );
int createCurvedCandidatesAlongLine( QList<LabelPosition *> &lPos, PointSet* mapShape );

/** Generate candidates for polygon features.
* \param lPos pointer to an array of candidates, will be filled by generated candidates
* \param mapShape a pointer to the polygon
* \return the number of generated candidates
*/
int setPositionForPolygon( QList<LabelPosition *> &lPos, PointSet *mapShape );

/** Returns the parent feature.
*/
QgsLabelFeature* feature() { return mLF; }

/** Returns the layer that feature belongs to.
*/
Layer* layer();

/** Generic method to generate candidates. This method will call either setPositionFromPoint(),
* setPositionFromLine or setPositionFromPolygon
* \param lPos pointer to an array of candidates, will be filled by generated candidates
* \param bbox_min min values of the map extent
* \param bbox_max max values of the map extent
* \param mapShape generate candidates for this spatial entity
* \param candidates index for candidates
* \return the number of candidates in *lPos
*/
int setPosition( QList<LabelPosition *> &lPos, double bbox_min[2], double bbox_max[2], PointSet *mapShape, RTree<LabelPosition*, double, 2, double>*candidates );

/** Returns the unique ID of the feature.
*/
QgsFeatureId featureId() const;
int createCandidatesForPolygon( QList<LabelPosition *> &lPos, PointSet *mapShape );

/** Tests whether this feature part belongs to the same QgsLabelFeature as another
* feature part.
Expand Down
4 changes: 1 addition & 3 deletions src/core/pal/geomfunction.cpp
Expand Up @@ -265,15 +265,13 @@ int GeomFunction::reorderPolygon( int nbPoints, double *x, double *y )
{
int inc = 0;
int *cHull;
int cHullSize;
int i;

int *pts = new int[nbPoints];
for ( i = 0; i < nbPoints; i++ )
pts[i] = i;


cHullSize = convexHullId( pts, x, y, nbPoints, cHull );
( void )convexHullId( pts, x, y, nbPoints, cHull );

if ( pts[cHull[0]] < pts[cHull[1]] && pts[cHull[1]] < pts[cHull[2]] )
inc = 1;
Expand Down

0 comments on commit b589856

Please sign in to comment.