Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[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 3a44e29
Show file tree
Hide file tree
Showing 17 changed files with 183 additions and 80 deletions.
15 changes: 15 additions & 0 deletions python/core/qgspallabeling.sip
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
15 changes: 13 additions & 2 deletions src/app/qgslabelinggui.cpp
Expand Up @@ -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();
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 );
Expand Down
1 change: 1 addition & 0 deletions src/app/qgslabelinggui.h
Expand Up @@ -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 );
Expand Down
30 changes: 21 additions & 9 deletions src/core/pal/costcalculator.cpp
Expand Up @@ -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 )
Expand All @@ -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;
}

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

Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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 )
Expand Down Expand Up @@ -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 );
}
Expand Down
12 changes: 6 additions & 6 deletions src/core/pal/costcalculator.h
Expand Up @@ -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°)
Expand All @@ -49,7 +49,7 @@ namespace pal
public:
PolygonCostCalculator( LabelPosition *lp );

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

double getCost();

Expand Down
38 changes: 8 additions & 30 deletions src/core/pal/feature.cpp
Expand Up @@ -132,8 +132,6 @@ namespace pal

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

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

Expand All @@ -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 );
}
}
Expand All @@ -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] );
Expand Down Expand Up @@ -259,7 +237,7 @@ namespace pal



Layer *FeaturePart::getLayer()
Layer* FeaturePart::layer()
{
return f->layer;
}
Expand Down
10 changes: 4 additions & 6 deletions src/core/pal/feature.h
Expand Up @@ -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
Expand Down Expand Up @@ -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 );
Expand All @@ -310,7 +308,7 @@ namespace pal
Feature* f;

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

GEOSGeometry *the_geom;
bool ownsGeom;
Expand Down
14 changes: 7 additions & 7 deletions src/core/pal/labelposition.cpp
Expand Up @@ -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;
Expand Down Expand Up @@ -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 )
Expand All @@ -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;
}
Expand Down Expand Up @@ -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 ) )
{
Expand Down
4 changes: 2 additions & 2 deletions src/core/pal/labelposition.h
Expand Up @@ -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 */
Expand Down Expand Up @@ -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:

Expand Down
1 change: 1 addition & 0 deletions src/core/pal/layer.cpp
Expand Up @@ -52,6 +52,7 @@ namespace pal
: mName( lyrName )
, pal( pal )
, mObstacle( obstacle )
, mObstacleType( PolygonInterior )
, mActive( active )
, mLabelLayer( toLabel )
, mDisplayAll( displayAll )
Expand Down

0 comments on commit 3a44e29

Please sign in to comment.