Skip to content
Permalink
Browse files

[FEATURE] "Cartographic" placement mode for point labels

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 4, 2016
1 parent a3cee7d commit b5898567358478775d65432e6f1fa121502e2553
@@ -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 );
@@ -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 );
@@ -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() ) )
@@ -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() ) )
@@ -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;

@@ -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();
@@ -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();
@@ -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();

@@ -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;
@@ -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;

@@ -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:
@@ -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;
}
}
@@ -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
@@ -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
@@ -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 );
@@ -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.
@@ -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;

0 comments on commit b589856

Please sign in to comment.
You can’t perform that action at this time.