Skip to content

Commit 94324f9

Browse files
committedMay 3, 2020
[pal][api] Add label candidate generation mechanism to generate candidates
outside of polygon features Based on a tweaked version of the logic presented by Rylov & Reimer from "A practical algorithm for the external annotation of area features" (2016), this placement mode generates a set of label candidates which sit at a set distance outside of a polygon's exterior ring. Designed for candidate generation for small polygons wrt label size, or in other cases where it's not possible or desirable to fit the label inside the polygon features itself
1 parent 6da9fa2 commit 94324f9

File tree

2 files changed

+205
-0
lines changed

2 files changed

+205
-0
lines changed
 

‎src/core/pal/feature.cpp

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
#include "costcalculator.h"
4242
#include "qgsgeometryutils.h"
4343
#include "qgslabeling.h"
44+
#include "qgspolygon.h"
4445
#include <QLinkedList>
4546
#include <cmath>
4647
#include <cfloat>
@@ -1797,6 +1798,202 @@ std::size_t FeaturePart::createCandidatesForPolygon( std::vector< std::unique_pt
17971798
return nbp;
17981799
}
17991800

1801+
std::size_t FeaturePart::createCandidatesOutsidePolygon( std::vector<std::unique_ptr<LabelPosition> > &lPos, Pal *pal )
1802+
{
1803+
// calculate distance between horizontal lines
1804+
const std::size_t maxPolygonCandidates = mLF->layer()->maximumPolygonLabelCandidates();
1805+
std::size_t candidatesCreated = 0;
1806+
1807+
double labelWidth = getLabelWidth();
1808+
double labelHeight = getLabelHeight();
1809+
double distanceToLabel = getLabelDistance();
1810+
const QgsMargins &visualMargin = mLF->visualMargin();
1811+
1812+
/*
1813+
* From Rylov & Reimer (2016) "A practical algorithm for the external annotation of area features":
1814+
*
1815+
* The list of rules adapted to the
1816+
* needs of externally labelling areal features is as follows:
1817+
* R1. Labels should be placed horizontally.
1818+
* R2. Label should be placed entirely outside at some
1819+
* distance from the area feature.
1820+
* R3. Name should not cross the boundary of its area
1821+
* feature.
1822+
* R4. The name should be placed in way that takes into
1823+
* account the shape of the feature by achieving a
1824+
* balance between the feature and its name, emphasizing their relationship.
1825+
* R5. The lettering to the right and slightly above the
1826+
* symbol is prioritized.
1827+
*
1828+
* In the following subsections we utilize four of the five rules
1829+
* for two subtasks of label placement, namely, for candidate
1830+
* positions generation (R1, R2, and R3) and for measuring their
1831+
* ‘goodness’ (R4). The rule R5 is applicable only in the case when
1832+
* the area of a polygonal feature is small and the feature can be
1833+
* treated and labelled as a point-feature
1834+
*/
1835+
1836+
/*
1837+
* QGIS approach (cite Dawson (2020) if you want ;) )
1838+
*
1839+
* We differ from the horizontal sweep line approach described by Rylov & Reimer and instead
1840+
* rely on just generating a set of points at regular intervals along the boundary of the polygon (exterior ring).
1841+
*
1842+
* In practice, this generates similar results as Rylov & Reimer, but has the additional benefits that:
1843+
* 1. It avoids the need to calculate intersections between the sweep line and the polygon
1844+
* 2. For horizontal or near horizontal segments, Rylov & Reimer propose generating evenly spaced points along
1845+
* these segments-- i.e. the same approach as we do for the whole polygon
1846+
* 3. It's easier to determine in advance exactly how many candidate positions we'll be generating, and accordingly
1847+
* we can easily pick the distance between points along the exterior ring so that the number of positions generated
1848+
* matches our target number (targetPolygonCandidates)
1849+
*/
1850+
1851+
// TO consider -- for very small polygons (wrt label size), treat them just like a point feature?
1852+
1853+
double cx, cy;
1854+
getCentroid( cx, cy, false );
1855+
1856+
GEOSContextHandle_t ctxt = QgsGeos::getGEOSHandler();
1857+
geos::unique_ptr buffer( GEOSBuffer_r( ctxt, geos(), mLF->distLabel(), 1 ) );
1858+
std::unique_ptr< QgsAbstractGeometry> gg( QgsGeos::fromGeos( buffer.get() ) );
1859+
1860+
const QgsPolygon *poly = qgsgeometry_cast< const QgsPolygon * >( gg.get() );
1861+
if ( !poly )
1862+
return candidatesCreated;
1863+
1864+
const QgsLineString *ring = qgsgeometry_cast< const QgsLineString *>( poly->exteriorRing() );
1865+
if ( !ring )
1866+
return candidatesCreated;
1867+
1868+
// we cheat here -- we don't use the polygon area when calculating the number of candidates, and rather use the perimeter (because that's more relevant,
1869+
// i.e a loooooong skinny polygon with small area should still generate a large number of candidates)
1870+
const double ringLength = ring->length();
1871+
const double circleArea = std::pow( ringLength, 2 ) / ( 4 * M_PI );
1872+
const std::size_t candidatesForArea = static_cast< std::size_t>( std::ceil( mLF->layer()->mPal->maximumPolygonCandidatesPerMapUnitSquared() * circleArea ) );
1873+
const std::size_t targetPolygonCandidates = std::max( static_cast< std::size_t >( 4 ), maxPolygonCandidates > 0 ? std::min( maxPolygonCandidates, candidatesForArea ) : candidatesForArea );
1874+
1875+
// assume each position generates one candidate
1876+
const double delta = ringLength / targetPolygonCandidates;
1877+
geos::unique_ptr geosPoint;
1878+
1879+
const double maxDistCentroidToLabelX = std::max( xmax - cx, cx - xmin ) + distanceToLabel;
1880+
const double maxDistCentroidToLabelY = std::max( ymax - cy, cy - ymin ) + distanceToLabel;
1881+
const double estimateOfMaxPossibleDistanceCentroidToLabel = std::sqrt( maxDistCentroidToLabelX * maxDistCentroidToLabelX + maxDistCentroidToLabelY * maxDistCentroidToLabelY );
1882+
1883+
// Satisfy R1: Labels should be placed horizontally.
1884+
const double labelAngle = 0;
1885+
1886+
std::size_t i = lPos.size();
1887+
auto addCandidate = [&]( double x, double y, QgsPalLayerSettings::PredefinedPointPosition position )
1888+
{
1889+
double labelX = 0;
1890+
double labelY = 0;
1891+
LabelPosition::Quadrant quadrant = LabelPosition::QuadrantAboveLeft;
1892+
1893+
// Satisfy R2: Label should be placed entirely outside at some distance from the area feature.
1894+
createCandidateAtOrderedPositionOverPoint( labelX, labelY, quadrant, x, y, labelWidth, labelHeight, position, distanceToLabel, visualMargin, 0, 0 );
1895+
1896+
std::unique_ptr< LabelPosition > candidate = qgis::make_unique< LabelPosition >( i, labelX, labelY, labelWidth, labelHeight, labelAngle, 0, this, false, quadrant );
1897+
if ( candidate->intersects( preparedGeom() ) )
1898+
{
1899+
// satisfy R3. Name should not cross the boundary of its area feature.
1900+
return;
1901+
}
1902+
1903+
// cost candidates by their distance to the feature's centroid (following Rylov & Reimer)
1904+
1905+
// Satisfy R4. The name should be placed in way that takes into
1906+
// account the shape of the feature by achieving a
1907+
// balance between the feature and its name, emphasizing their relationship.
1908+
1909+
1910+
// here we deviate a little from R&R, and instead of just calculating the centroid distance
1911+
// to centroid of label, we calculate the distance from the centroid to the nearest point on the label
1912+
1913+
const double centroidDistance = candidate->getDistanceToPoint( cx, cy );
1914+
const double centroidCost = centroidDistance / estimateOfMaxPossibleDistanceCentroidToLabel;
1915+
candidate->setCost( centroidCost );
1916+
1917+
lPos.emplace_back( std::move( candidate ) );
1918+
candidatesCreated++;
1919+
++i;
1920+
};
1921+
1922+
ring->visitPointsByRegularDistance( delta, [&]( double x, double y, double, double,
1923+
double startSegmentX, double startSegmentY, double, double,
1924+
double endSegmentX, double endSegmentY, double, double )
1925+
{
1926+
// get normal angle for segment
1927+
float angle = atan2( static_cast< float >( endSegmentY - startSegmentY ), static_cast< float >( endSegmentX - startSegmentX ) ) * 180 / M_PI;
1928+
if ( angle < 0 )
1929+
angle += 360;
1930+
1931+
// adapted fom Rylov & Reimer figure 9
1932+
if ( angle >= 0 && angle <= 5 )
1933+
{
1934+
addCandidate( x, y, QgsPalLayerSettings::TopMiddle );
1935+
addCandidate( x, y, QgsPalLayerSettings::TopLeft );
1936+
}
1937+
else if ( angle <= 85 )
1938+
{
1939+
addCandidate( x, y, QgsPalLayerSettings::TopLeft );
1940+
}
1941+
else if ( angle <= 90 )
1942+
{
1943+
addCandidate( x, y, QgsPalLayerSettings::TopLeft );
1944+
addCandidate( x, y, QgsPalLayerSettings::MiddleLeft );
1945+
}
1946+
1947+
else if ( angle <= 95 )
1948+
{
1949+
addCandidate( x, y, QgsPalLayerSettings::MiddleLeft );
1950+
addCandidate( x, y, QgsPalLayerSettings::BottomLeft );
1951+
}
1952+
else if ( angle <= 175 )
1953+
{
1954+
addCandidate( x, y, QgsPalLayerSettings::BottomLeft );
1955+
}
1956+
else if ( angle <= 180 )
1957+
{
1958+
addCandidate( x, y, QgsPalLayerSettings::BottomLeft );
1959+
addCandidate( x, y, QgsPalLayerSettings::BottomMiddle );
1960+
}
1961+
1962+
else if ( angle <= 185 )
1963+
{
1964+
addCandidate( x, y, QgsPalLayerSettings::BottomMiddle );
1965+
addCandidate( x, y, QgsPalLayerSettings::BottomRight );
1966+
}
1967+
else if ( angle <= 265 )
1968+
{
1969+
addCandidate( x, y, QgsPalLayerSettings::BottomRight );
1970+
}
1971+
else if ( angle <= 270 )
1972+
{
1973+
addCandidate( x, y, QgsPalLayerSettings::BottomRight );
1974+
addCandidate( x, y, QgsPalLayerSettings::MiddleRight );
1975+
}
1976+
else if ( angle <= 275 )
1977+
{
1978+
addCandidate( x, y, QgsPalLayerSettings::MiddleRight );
1979+
addCandidate( x, y, QgsPalLayerSettings::TopRight );
1980+
}
1981+
else if ( angle <= 355 )
1982+
{
1983+
addCandidate( x, y, QgsPalLayerSettings::TopRight );
1984+
}
1985+
else
1986+
{
1987+
addCandidate( x, y, QgsPalLayerSettings::TopRight );
1988+
addCandidate( x, y, QgsPalLayerSettings::TopMiddle );
1989+
}
1990+
1991+
return !pal->isCanceled();
1992+
} );
1993+
1994+
return candidatesCreated;
1995+
}
1996+
18001997
std::vector< std::unique_ptr< LabelPosition > > FeaturePart::createCandidates( Pal *pal )
18011998
{
18021999
std::vector< std::unique_ptr< LabelPosition > > lPos;

‎src/core/pal/feature.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,14 @@ namespace pal
267267
*/
268268
std::size_t createCandidatesForPolygon( std::vector<std::unique_ptr<LabelPosition> > &lPos, PointSet *mapShape, Pal *pal );
269269

270+
/**
271+
* Generate candidates outside of polygon features.
272+
* \param lPos pointer to an array of candidates, will be filled by generated candidates
273+
* \param pal point to pal settings object, for cancellation support
274+
* \returns the number of generated candidates
275+
*/
276+
std::size_t createCandidatesOutsidePolygon( std::vector<std::unique_ptr<LabelPosition> > &lPos, Pal *pal );
277+
270278
/**
271279
* Tests whether this feature part belongs to the same QgsLabelFeature as another
272280
* feature part.

0 commit comments

Comments
 (0)
Please sign in to comment.