Skip to content
Permalink
Browse files

[FEATURE][labeling] Add option to control how polygon layers

act as obstacles

Options are either avoid placing labels over polygon interior
or avoid placing over polygon boundaries. (Previous behaviour
was always avoid placing over interior).

Avoiding placing over boundaries is useful for regional boundary
layers, where the features cover an entire area. In this case
it's impossible to avoid placing labels within these features,
and it looks much better to avoid placing them over the boundaries
between features. End result is better cartographic placement of
labels in this situation.
  • Loading branch information
nyalldawson committed Jul 17, 2015
1 parent cc1a34f commit 3a44e294de08ee03c0386179693c61a3f76e5efe
@@ -145,6 +145,17 @@ class QgsPalLayerSettings
will be drawn with right alignment*/
};

/** Valid obstacle types, which affect how features within the layer will act as obstacles
* for labels.
*/
enum ObstacleType
{
PolygonInterior, /*!< avoid placing labels over interior of polygon (prefer placing labels totally
outside or just slightly inside polygon) */
PolygonBoundary /*!< avoid placing labels over boundary of polygon (prefer placing outside or
completely inside polygon) */
};

enum ShapeType
{
ShapeRectangle,
@@ -453,6 +464,10 @@ class QgsPalLayerSettings
double minFeatureSize; // minimum feature size to be labelled (in mm)
bool obstacle; // whether features for layer are obstacles to labels of other layers

/** Controls how features act as obstacles for labels
*/
ObstacleType obstacleType;

//-- scale factors
double vectorScaleFactor; //scale factor painter units->pixels
double rasterCompressFactor; //pixel resolution scale factor
@@ -80,6 +80,9 @@ QgsLabelingGui::QgsLabelingGui( QgsVectorLayer* layer, QgsMapCanvas* mapCanvas,
mFontLetterSpacingSpinBox->setClearValue( 0.0 );
mFontWordSpacingSpinBox->setClearValue( 0.0 );

mObstacleTypeComboBox->addItem( tr( "Over the feature's interior" ), QgsPalLayerSettings::PolygonInterior );
mObstacleTypeComboBox->addItem( tr( "Over the feature's boundary" ), QgsPalLayerSettings::PolygonBoundary );

mCharDlg = new QgsCharacterSelectorDialog( this );

mRefFont = lblFontPreview->font();
@@ -152,6 +155,7 @@ QgsLabelingGui::QgsLabelingGui( QgsVectorLayer* layer, QgsMapCanvas* mapCanvas,
chkMergeLines->setVisible( layer->geometryType() == QGis::Line );
mDirectSymbolsFrame->setVisible( layer->geometryType() == QGis::Line );
mMinSizeFrame->setVisible( layer->geometryType() != QGis::Point );
mPolygonObstacleTypeFrame->setVisible( layer->geometryType() == QGis::Polygon );

// field combo and expression button
mFieldExpressionWidget->setLayer( mLayer );
@@ -354,7 +358,8 @@ void QgsLabelingGui::init()
mRepeatDistanceUnitWidget->setMapUnitScale( lyr.repeatDistanceMapUnitScale );

mPrioritySlider->setValue( lyr.priority );
chkNoObstacle->setChecked( lyr.obstacle );
mChkNoObstacle->setChecked( lyr.obstacle );
mObstacleTypeComboBox->setCurrentIndex( mObstacleTypeComboBox->findData( lyr.obstacleType ) );
chkLabelPerFeaturePart->setChecked( lyr.labelPerPart );
mPalShowAllLabelsForLayerChkBx->setChecked( lyr.displayAll );
chkMergeLines->setChecked( lyr.mergeLines );
@@ -648,7 +653,8 @@ QgsPalLayerSettings QgsLabelingGui::layerSettings()
lyr.previewBkgrdColor = mPreviewBackgroundBtn->color();

lyr.priority = mPrioritySlider->value();
lyr.obstacle = chkNoObstacle->isChecked();
lyr.obstacle = mChkNoObstacle->isChecked();
lyr.obstacleType = ( QgsPalLayerSettings::ObstacleType )mObstacleTypeComboBox->itemData( mObstacleTypeComboBox->currentIndex() ).toInt();
lyr.labelPerPart = chkLabelPerFeaturePart->isChecked();
lyr.displayAll = mPalShowAllLabelsForLayerChkBx->isChecked();
lyr.mergeLines = chkMergeLines->isChecked();
@@ -1698,6 +1704,11 @@ void QgsLabelingGui::on_mDirectSymbRightToolBtn_clicked()
mDirectSymbRightLineEdit->setText( QString( dirSymb ) );
}

void QgsLabelingGui::on_mChkNoObstacle_toggled( bool active )
{
mPolygonObstacleTypeFrame->setEnabled( active );
}

void QgsLabelingGui::showBackgroundRadius( bool show )
{
mShapeRadiusLabel->setVisible( show );
@@ -80,6 +80,7 @@ class APP_EXPORT QgsLabelingGui : public QWidget, private Ui::QgsLabelingGuiBase
void on_mPreviewBackgroundBtn_colorChanged( const QColor &color );
void on_mDirectSymbLeftToolBtn_clicked();
void on_mDirectSymbRightToolBtn_clicked();
void on_mChkNoObstacle_toggled( bool active );

protected:
void blockInitSignals( bool block );
@@ -30,17 +30,17 @@
namespace pal
{

void CostCalculator::addObstacleCostPenalty( LabelPosition* lp, PointSet* feat )
void CostCalculator::addObstacleCostPenalty( LabelPosition* lp, FeaturePart* obstacle )
{
int n = 0;
double dist;
double distlabel = lp->feature->getLabelDistance();

switch ( feat->getGeosType() )
switch ( obstacle->getGeosType() )
{
case GEOS_POINT:

dist = lp->getDistanceToPoint( feat->x[0], feat->y[0] );
dist = lp->getDistanceToPoint( obstacle->x[0], obstacle->y[0] );
if ( dist < 0 )
n = 2;
else if ( dist < distlabel )
@@ -52,11 +52,23 @@ namespace pal
case GEOS_LINESTRING:

// Is one of label's borders crossing the line ?
n = ( lp->isBorderCrossingLine( feat ) ? 1 : 0 );
n = ( lp->isBorderCrossingLine( obstacle ) ? 1 : 0 );
break;

case GEOS_POLYGON:
n = lp->getNumPointsInPolygon( feat->getNumPoints(), feat->x, feat->y );
// behaviour depends on obstacle avoid type
switch ( obstacle->layer()->obstacleType() )
{
case PolygonInterior:
// n ranges from 0 -> 12
n = lp->getNumPointsInPolygon( obstacle->getNumPoints(), obstacle->x, obstacle->y );
break;
case PolygonBoundary:
// penalty may need tweaking, given that interior mode ranges up to 12
n = ( lp->isBorderCrossingLine( obstacle ) ? 6 : 0 );
break;
}

break;
}

@@ -67,7 +79,7 @@ namespace pal

////////

void CostCalculator::setPolygonCandidatesCost( int nblp, LabelPosition **lPos, int max_p, RTree<PointSet*, double, 2, double> *obstacles, double bbx[4], double bby[4] )
void CostCalculator::setPolygonCandidatesCost( int nblp, LabelPosition **lPos, int max_p, RTree<FeaturePart*, double, 2, double> *obstacles, double bbx[4], double bby[4] )
{
int i;

@@ -125,7 +137,7 @@ namespace pal
}


void CostCalculator::setCandidateCostFromPolygon( LabelPosition* lp, RTree <PointSet*, double, 2, double> *obstacles, double bbx[4], double bby[4] )
void CostCalculator::setCandidateCostFromPolygon( LabelPosition* lp, RTree <FeaturePart*, double, 2, double> *obstacles, double bbx[4], double bby[4] )
{

double amin[2];
@@ -153,7 +165,7 @@ namespace pal
delete pCost;
}

int CostCalculator::finalizeCandidatesCosts( Feats* feat, int max_p, RTree <PointSet*, double, 2, double> *obstacles, double bbx[4], double bby[4] )
int CostCalculator::finalizeCandidatesCosts( Feats* feat, int max_p, RTree <FeaturePart*, double, 2, double> *obstacles, double bbx[4], double bby[4] )
{
// If candidates list is smaller than expected
if ( max_p > feat->nblp )
@@ -191,7 +203,7 @@ namespace pal

if ( feat->feature->getGeosType() == GEOS_POLYGON )
{
int arrangement = feat->feature->getLayer()->arrangement();
int arrangement = feat->feature->layer()->arrangement();
if ( arrangement == P_FREE || arrangement == P_HORIZ )
setPolygonCandidatesCost( stop, ( LabelPosition** ) feat->lPos, max_p, obstacles, bbx, bby );
}
@@ -25,18 +25,18 @@ namespace pal
{
public:
/** Increase candidate's cost according to its collision with passed feature */
static void addObstacleCostPenalty( LabelPosition* lp, PointSet* feat );
static void addObstacleCostPenalty( LabelPosition* lp, pal::FeaturePart *obstacle );

static void setPolygonCandidatesCost( int nblp, LabelPosition **lPos, int max_p, RTree<PointSet*, double, 2, double> *obstacles, double bbx[4], double bby[4] );
static void setPolygonCandidatesCost( int nblp, LabelPosition **lPos, int max_p, RTree<pal::FeaturePart*, double, 2, double> *obstacles, double bbx[4], double bby[4] );

/** Set cost to the smallest distance between lPos's centroid and a polygon stored in geoetry field */
static void setCandidateCostFromPolygon( LabelPosition* lp, RTree <PointSet*, double, 2, double> *obstacles, double bbx[4], double bby[4] );
static void setCandidateCostFromPolygon( LabelPosition* lp, RTree<pal::FeaturePart *, double, 2, double> *obstacles, double bbx[4], double bby[4] );

/** Sort candidates by costs, skip the worse ones, evaluate polygon candidates */
static int finalizeCandidatesCosts( Feats* feat, int max_p, RTree <PointSet*, double, 2, double> *obstacles, double bbx[4], double bby[4] );
static int finalizeCandidatesCosts( Feats* feat, int max_p, RTree<pal::FeaturePart *, double, 2, double> *obstacles, double bbx[4], double bby[4] );
};

/**
/**
* \brief Data structure to compute polygon's candidates costs
*
* eight segment from center of candidat to (rpx,rpy) points (0°, 45°, 90°, ..., 315°)
@@ -49,7 +49,7 @@ namespace pal
public:
PolygonCostCalculator( LabelPosition *lp );

void update( PointSet *pset );
void update( pal::PointSet *pset );

double getCost();

@@ -132,8 +132,6 @@ namespace pal

void FeaturePart::extractCoords( const GEOSGeometry* geom )
{
int i, j;

const GEOSCoordSequence *coordSeq;
GEOSContextHandle_t geosctxt = geosContext();

@@ -145,35 +143,15 @@ namespace pal
{
// set nbHoles, holes member variables
nbHoles = GEOSGetNumInteriorRings_r( geosctxt, geom );
holes = new PointSet*[nbHoles];
holes = new FeaturePart*[nbHoles];

for ( i = 0; i < nbHoles; i++ )
for ( int i = 0; i < nbHoles; ++i )
{
holes[i] = new PointSet();
holes[i]->holeOf = NULL;

const GEOSGeometry* interior = GEOSGetInteriorRingN_r( geosctxt, geom, i );
holes[i]->nbPoints = GEOSGetNumCoordinates_r( geosctxt, interior );
holes[i]->x = new double[holes[i]->nbPoints];
holes[i]->y = new double[holes[i]->nbPoints];

holes[i]->xmin = holes[i]->ymin = DBL_MAX;
holes[i]->xmax = holes[i]->ymax = -DBL_MAX;

coordSeq = GEOSGeom_getCoordSeq_r( geosctxt, interior );

for ( j = 0; j < holes[i]->nbPoints; j++ )
{
GEOSCoordSeq_getX_r( geosctxt, coordSeq, j, &holes[i]->x[j] );
GEOSCoordSeq_getY_r( geosctxt, coordSeq, j, &holes[i]->y[j] );

holes[i]->xmax = holes[i]->x[j] > holes[i]->xmax ? holes[i]->x[j] : holes[i]->xmax;
holes[i]->xmin = holes[i]->x[j] < holes[i]->xmin ? holes[i]->x[j] : holes[i]->xmin;

holes[i]->ymax = holes[i]->y[j] > holes[i]->ymax ? holes[i]->y[j] : holes[i]->ymax;
holes[i]->ymin = holes[i]->y[j] < holes[i]->ymin ? holes[i]->y[j] : holes[i]->ymin;
}

holes[i] = new FeaturePart( f, interior );
holes[i]->holeOf = NULL;
// possibly not needed. it's not done for the exterior ring, so I'm not sure
// why it's just done here...
reorderPolygon( holes[i]->nbPoints, holes[i]->x, holes[i]->y );
}
}
@@ -199,7 +177,7 @@ namespace pal
x = new double[nbPoints];
y = new double[nbPoints];

for ( i = 0; i < nbPoints; i++ )
for ( int i = 0; i < nbPoints; ++i )
{
GEOSCoordSeq_getX_r( geosctxt, coordSeq, i, &x[i] );
GEOSCoordSeq_getY_r( geosctxt, coordSeq, i, &y[i] );
@@ -259,7 +237,7 @@ namespace pal



Layer *FeaturePart::getLayer()
Layer* FeaturePart::layer()
{
return f->layer;
}
@@ -247,11 +247,9 @@ namespace pal
*/
const GEOSGeometry* getGeometry() const { return the_geom; }

/**
* \brief return the layer that feature belongs to
* \return the layer of the feature
/** Returns the layer that feature belongs to.
*/
Layer * getLayer();
Layer* layer();

/**
* \brief generic method to generate candidates
@@ -295,7 +293,7 @@ namespace pal
bool getAlwaysShow() { return f->alwaysShow; }

int getNumSelfObstacles() const { return nbHoles; }
PointSet* getSelfObstacle( int i ) { return holes[i]; }
FeaturePart* getSelfObstacle( int i ) { return holes[i]; }

/** Check whether this part is connected with some other part */
bool isConnected( FeaturePart* p2 );
@@ -310,7 +308,7 @@ namespace pal
Feature* f;

int nbHoles;
PointSet **holes;
FeaturePart **holes;

GEOSGeometry *the_geom;
bool ownsGeom;
@@ -97,12 +97,12 @@ namespace pal
y[3] = y1 + dy2;

// upside down ? (curved labels are always correct)
if ( feature->getLayer()->arrangement() != P_CURVED &&
if ( feature->layer()->arrangement() != P_CURVED &&
this->alpha > M_PI / 2 && this->alpha <= 3*M_PI / 2 )
{
bool uprightLabel = false;

switch ( feature->getLayer()->upsidedownLabels() )
switch ( feature->layer()->upsidedownLabels() )
{
case Layer::Upright:
uprightLabel = true;
@@ -388,7 +388,7 @@ namespace pal

QString LabelPosition::getLayerName() const
{
return feature->getLayer()->name();
return feature->layer()->name();
}

bool LabelPosition::costShrink( void *l, void *r )
@@ -402,17 +402,17 @@ namespace pal
}


bool LabelPosition::polygonObstacleCallback( PointSet *feat, void *ctx )
bool LabelPosition::polygonObstacleCallback( FeaturePart *obstacle, void *ctx )
{
PolygonCostCalculator *pCost = ( PolygonCostCalculator* ) ctx;

LabelPosition *lp = pCost->getLabel();
if (( feat == lp->feature ) || ( feat->getHoleOf() && feat->getHoleOf() != lp->feature ) )
if (( obstacle == lp->feature ) || ( obstacle->getHoleOf() && obstacle->getHoleOf() != lp->feature ) )
{
return true;
}

pCost->update( feat );
pCost->update( obstacle );

return true;
}
@@ -440,7 +440,7 @@ namespace pal

bool LabelPosition::pruneCallback( LabelPosition *lp, void *ctx )
{
PointSet *feat = (( PruneCtx* ) ctx )->obstacle;
FeaturePart *feat = (( PruneCtx* ) ctx )->obstacle;

if (( feat == lp->feature ) || ( feat->getHoleOf() && feat->getHoleOf() != lp->feature ) )
{
@@ -217,7 +217,7 @@ namespace pal
typedef struct
{
Pal* pal;
PointSet *obstacle;
FeaturePart *obstacle;
} PruneCtx;

/** Check whether the candidate in ctx overlap with obstacle feat */
@@ -247,7 +247,7 @@ namespace pal
static bool removeOverlapCallback( LabelPosition *lp, void *ctx );

// for polygon cost calculation
static bool polygonObstacleCallback( PointSet *feat, void *ctx );
static bool polygonObstacleCallback( pal::FeaturePart *obstacle, void *ctx );

protected:

@@ -52,6 +52,7 @@ namespace pal
: mName( lyrName )
, pal( pal )
, mObstacle( obstacle )
, mObstacleType( PolygonInterior )
, mActive( active )
, mLabelLayer( toLabel )
, mDisplayAll( displayAll )

0 comments on commit 3a44e29

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