QGIS API Documentation 3.28.14-Firenze (exported)
Loading...
Searching...
No Matches
feature.cpp
Go to the documentation of this file.
1/*
2 * libpal - Automated Placement of Labels Library
3 *
4 * Copyright (C) 2008 Maxence Laurent, MIS-TIC, HEIG-VD
5 * University of Applied Sciences, Western Switzerland
6 * http://www.hes-so.ch
7 *
8 * Contact:
9 * maxence.laurent <at> heig-vd <dot> ch
10 * or
11 * eric.taillard <at> heig-vd <dot> ch
12 *
13 * This file is part of libpal.
14 *
15 * libpal is free software: you can redistribute it and/or modify
16 * it under the terms of the GNU General Public License as published by
17 * the Free Software Foundation, either version 3 of the License, or
18 * (at your option) any later version.
19 *
20 * libpal is distributed in the hope that it will be useful,
21 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 * GNU General Public License for more details.
24 *
25 * You should have received a copy of the GNU General Public License
26 * along with libpal. If not, see <http://www.gnu.org/licenses/>.
27 *
28 */
29
30#include "pal.h"
31#include "layer.h"
32#include "feature.h"
33#include "geomfunction.h"
34#include "labelposition.h"
35#include "pointset.h"
36
37#include "qgis.h"
38#include "qgsgeometry.h"
39#include "qgsgeos.h"
40#include "qgstextlabelfeature.h"
41#include "qgsmessagelog.h"
42#include "qgsgeometryutils.h"
43#include "qgslabeling.h"
44#include "qgspolygon.h"
46
47#include <QLinkedList>
48#include <cmath>
49#include <cfloat>
50
51using namespace pal;
52
53FeaturePart::FeaturePart( QgsLabelFeature *feat, const GEOSGeometry *geom )
54 : mLF( feat )
55{
56 // we'll remove const, but we won't modify that geometry
57 mGeos = const_cast<GEOSGeometry *>( geom );
58 mOwnsGeom = false; // geometry is owned by Feature class
59
60 extractCoords( geom );
61
62 holeOf = nullptr;
63 for ( int i = 0; i < mHoles.count(); i++ )
64 {
65 mHoles.at( i )->holeOf = this;
66 }
67
68}
69
71 : PointSet( other )
72 , mLF( other.mLF )
73{
74 for ( const FeaturePart *hole : std::as_const( other.mHoles ) )
75 {
76 mHoles << new FeaturePart( *hole );
77 mHoles.last()->holeOf = this;
78 }
79}
80
82{
83 // X and Y are deleted in PointSet
84
85 qDeleteAll( mHoles );
86 mHoles.clear();
87}
88
89void FeaturePart::extractCoords( const GEOSGeometry *geom )
90{
91 const GEOSCoordSequence *coordSeq = nullptr;
92 GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler();
93
94 type = GEOSGeomTypeId_r( geosctxt, geom );
95
96 if ( type == GEOS_POLYGON )
97 {
98 if ( GEOSGetNumInteriorRings_r( geosctxt, geom ) > 0 )
99 {
100 int numHoles = GEOSGetNumInteriorRings_r( geosctxt, geom );
101
102 for ( int i = 0; i < numHoles; ++i )
103 {
104 const GEOSGeometry *interior = GEOSGetInteriorRingN_r( geosctxt, geom, i );
105 FeaturePart *hole = new FeaturePart( mLF, interior );
106 hole->holeOf = nullptr;
107 // possibly not needed. it's not done for the exterior ring, so I'm not sure
108 // why it's just done here...
109 GeomFunction::reorderPolygon( hole->x, hole->y );
110
111 mHoles << hole;
112 }
113 }
114
115 // use exterior ring for the extraction of coordinates that follows
116 geom = GEOSGetExteriorRing_r( geosctxt, geom );
117 }
118 else
119 {
120 qDeleteAll( mHoles );
121 mHoles.clear();
122 }
123
124 // find out number of points
125 nbPoints = GEOSGetNumCoordinates_r( geosctxt, geom );
126 coordSeq = GEOSGeom_getCoordSeq_r( geosctxt, geom );
127
128 // initialize bounding box
129 xmin = ymin = std::numeric_limits<double>::max();
130 xmax = ymax = std::numeric_limits<double>::lowest();
131
132 // initialize coordinate arrays
133 deleteCoords();
134 x.resize( nbPoints );
135 y.resize( nbPoints );
136
137#if GEOS_VERSION_MAJOR>3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR>=10 )
138 GEOSCoordSeq_copyToArrays_r( geosctxt, coordSeq, x.data(), y.data(), nullptr, nullptr );
139 auto xminmax = std::minmax_element( x.begin(), x.end() );
140 xmin = *xminmax.first;
141 xmax = *xminmax.second;
142 auto yminmax = std::minmax_element( y.begin(), y.end() );
143 ymin = *yminmax.first;
144 ymax = *yminmax.second;
145#else
146 for ( int i = 0; i < nbPoints; ++i )
147 {
148 GEOSCoordSeq_getXY_r( geosctxt, coordSeq, i, &x[i], &y[i] );
149
150 xmax = x[i] > xmax ? x[i] : xmax;
151 xmin = x[i] < xmin ? x[i] : xmin;
152
153 ymax = y[i] > ymax ? y[i] : ymax;
154 ymin = y[i] < ymin ? y[i] : ymin;
155 }
156#endif
157}
158
160{
161 return mLF->layer();
162}
163
165{
166 return mLF->id();
167}
168
170{
172}
173
175{
176 if ( mCachedMaxLineCandidates > 0 )
177 return mCachedMaxLineCandidates;
178
179 const double l = length();
180 if ( l > 0 )
181 {
182 const std::size_t candidatesForLineLength = static_cast< std::size_t >( std::ceil( mLF->layer()->mPal->maximumLineCandidatesPerMapUnit() * l ) );
183 const std::size_t maxForLayer = mLF->layer()->maximumLineLabelCandidates();
184 if ( maxForLayer == 0 )
185 mCachedMaxLineCandidates = candidatesForLineLength;
186 else
187 mCachedMaxLineCandidates = std::min( candidatesForLineLength, maxForLayer );
188 }
189 else
190 {
191 mCachedMaxLineCandidates = 1;
192 }
193 return mCachedMaxLineCandidates;
194}
195
197{
198 if ( mCachedMaxPolygonCandidates > 0 )
199 return mCachedMaxPolygonCandidates;
200
201 const double a = area();
202 if ( a > 0 )
203 {
204 const std::size_t candidatesForArea = static_cast< std::size_t >( std::ceil( mLF->layer()->mPal->maximumPolygonCandidatesPerMapUnitSquared() * a ) );
205 const std::size_t maxForLayer = mLF->layer()->maximumPolygonLabelCandidates();
206 if ( maxForLayer == 0 )
207 mCachedMaxPolygonCandidates = candidatesForArea;
208 else
209 mCachedMaxPolygonCandidates = std::min( candidatesForArea, maxForLayer );
210 }
211 else
212 {
213 mCachedMaxPolygonCandidates = 1;
214 }
215 return mCachedMaxPolygonCandidates;
216}
217
219{
220 if ( !part )
221 return false;
222
223 if ( mLF->layer()->name() != part->layer()->name() )
224 return false;
225
226 if ( mLF->id() == part->featureId() )
227 return true;
228
229 // any part of joined features are also treated as having the same label feature
230 int connectedFeatureId = mLF->layer()->connectedFeatureId( mLF->id() );
231 return connectedFeatureId >= 0 && connectedFeatureId == mLF->layer()->connectedFeatureId( part->featureId() );
232}
233
234LabelPosition::Quadrant FeaturePart::quadrantFromOffset() const
235{
236 QPointF quadOffset = mLF->quadOffset();
237 qreal quadOffsetX = quadOffset.x(), quadOffsetY = quadOffset.y();
238
239 if ( quadOffsetX < 0 )
240 {
241 if ( quadOffsetY < 0 )
242 {
244 }
245 else if ( quadOffsetY > 0 )
246 {
248 }
249 else
250 {
252 }
253 }
254 else if ( quadOffsetX > 0 )
255 {
256 if ( quadOffsetY < 0 )
257 {
259 }
260 else if ( quadOffsetY > 0 )
261 {
263 }
264 else
265 {
267 }
268 }
269 else
270 {
271 if ( quadOffsetY < 0 )
272 {
274 }
275 else if ( quadOffsetY > 0 )
276 {
278 }
279 else
280 {
282 }
283 }
284}
285
287{
288 return mTotalRepeats;
289}
290
291void FeaturePart::setTotalRepeats( int totalRepeats )
292{
293 mTotalRepeats = totalRepeats;
294}
295
296std::size_t FeaturePart::createCandidateCenteredOverPoint( double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle )
297{
298 // get from feature
299 double labelW = getLabelWidth( angle );
300 double labelH = getLabelHeight( angle );
301
302 double cost = 0.00005;
303 int id = lPos.size();
304
305 double xdiff = -labelW / 2.0;
306 double ydiff = -labelH / 2.0;
307
309
310 double lx = x + xdiff;
311 double ly = y + ydiff;
312
314 {
315 if ( !GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), lx, ly, labelW, labelH, angle ) )
316 {
317 return 0;
318 }
319 }
320
321 lPos.emplace_back( std::make_unique< LabelPosition >( id, lx, ly, labelW, labelH, angle, cost, this, false, LabelPosition::QuadrantOver ) );
322 return 1;
323}
324
325std::size_t FeaturePart::createCandidatesOverPoint( double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle )
326{
327 // get from feature
328 double labelW = getLabelWidth( angle );
329 double labelH = getLabelHeight( angle );
330
331 double cost = 0.0001;
332 int id = lPos.size();
333
334 double xdiff = -labelW / 2.0;
335 double ydiff = -labelH / 2.0;
336
338
339 if ( !qgsDoubleNear( mLF->quadOffset().x(), 0.0 ) )
340 {
341 xdiff += labelW / 2.0 * mLF->quadOffset().x();
342 }
343 if ( !qgsDoubleNear( mLF->quadOffset().y(), 0.0 ) )
344 {
345 ydiff += labelH / 2.0 * mLF->quadOffset().y();
346 }
347
348 if ( ! mLF->hasFixedPosition() )
349 {
350 if ( !qgsDoubleNear( angle, 0.0 ) )
351 {
352 double xd = xdiff * std::cos( angle ) - ydiff * std::sin( angle );
353 double yd = xdiff * std::sin( angle ) + ydiff * std::cos( angle );
354 xdiff = xd;
355 ydiff = yd;
356 }
357 }
358
360 {
361 //if in "around point" placement mode, then we use the label distance to determine
362 //the label's offset
363 if ( qgsDoubleNear( mLF->quadOffset().x(), 0.0 ) )
364 {
365 ydiff += mLF->quadOffset().y() * mLF->distLabel();
366 }
367 else if ( qgsDoubleNear( mLF->quadOffset().y(), 0.0 ) )
368 {
369 xdiff += mLF->quadOffset().x() * mLF->distLabel();
370 }
371 else
372 {
373 xdiff += mLF->quadOffset().x() * M_SQRT1_2 * mLF->distLabel();
374 ydiff += mLF->quadOffset().y() * M_SQRT1_2 * mLF->distLabel();
375 }
376 }
377 else
378 {
379 if ( !qgsDoubleNear( mLF->positionOffset().x(), 0.0 ) )
380 {
381 xdiff += mLF->positionOffset().x();
382 }
383 if ( !qgsDoubleNear( mLF->positionOffset().y(), 0.0 ) )
384 {
385 ydiff += mLF->positionOffset().y();
386 }
387 }
388
389 double lx = x + xdiff;
390 double ly = y + ydiff;
391
393 {
394 if ( !GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), lx, ly, labelW, labelH, angle ) )
395 {
396 return 0;
397 }
398 }
399
400 lPos.emplace_back( std::make_unique< LabelPosition >( id, lx, ly, labelW, labelH, angle, cost, this, false, quadrantFromOffset() ) );
401 return 1;
402}
403
404std::unique_ptr<LabelPosition> FeaturePart::createCandidatePointOnSurface( PointSet *mapShape )
405{
406 double px, py;
407 try
408 {
409 GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler();
410 geos::unique_ptr pointGeom( GEOSPointOnSurface_r( geosctxt, mapShape->geos() ) );
411 if ( pointGeom )
412 {
413 const GEOSCoordSequence *coordSeq = GEOSGeom_getCoordSeq_r( geosctxt, pointGeom.get() );
414 unsigned int nPoints = 0;
415 GEOSCoordSeq_getSize_r( geosctxt, coordSeq, &nPoints );
416 if ( nPoints == 0 )
417 return nullptr;
418 GEOSCoordSeq_getXY_r( geosctxt, coordSeq, 0, &px, &py );
419 }
420 }
421 catch ( GEOSException &e )
422 {
423 qWarning( "GEOS exception: %s", e.what() );
424 QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
425 return nullptr;
426 }
427
428 return std::make_unique< LabelPosition >( 0, px, py, getLabelWidth(), getLabelHeight(), 0.0, 0.0, this, false, LabelPosition::QuadrantOver );
429}
430
431void createCandidateAtOrderedPositionOverPoint( double &labelX, double &labelY, LabelPosition::Quadrant &quadrant, double x, double y, double labelWidth, double labelHeight, Qgis::LabelPredefinedPointPosition position, double distanceToLabel, const QgsMargins &visualMargin, double symbolWidthOffset, double symbolHeightOffset, double angle )
432{
433 double alpha = 0.0;
434 double deltaX = 0;
435 double deltaY = 0;
436
437 switch ( position )
438 {
441 alpha = 3 * M_PI_4;
442 deltaX = -labelWidth + visualMargin.right() - symbolWidthOffset;
443 deltaY = -visualMargin.bottom() + symbolHeightOffset;
444 break;
445
447 quadrant = LabelPosition::QuadrantAboveRight; //right quadrant, so labels are left-aligned
448 alpha = M_PI_2;
449 deltaX = -labelWidth / 4.0 - visualMargin.left();
450 deltaY = -visualMargin.bottom() + symbolHeightOffset;
451 break;
452
455 alpha = M_PI_2;
456 deltaX = -labelWidth / 2.0;
457 deltaY = -visualMargin.bottom() + symbolHeightOffset;
458 break;
459
461 quadrant = LabelPosition::QuadrantAboveLeft; //left quadrant, so labels are right-aligned
462 alpha = M_PI_2;
463 deltaX = -labelWidth * 3.0 / 4.0 + visualMargin.right();
464 deltaY = -visualMargin.bottom() + symbolHeightOffset;
465 break;
466
469 alpha = M_PI_4;
470 deltaX = - visualMargin.left() + symbolWidthOffset;
471 deltaY = -visualMargin.bottom() + symbolHeightOffset;
472 break;
473
476 alpha = M_PI;
477 deltaX = -labelWidth + visualMargin.right() - symbolWidthOffset;
478 deltaY = -labelHeight / 2.0;// TODO - should this be adjusted by visual margin??
479 break;
480
483 alpha = 0.0;
484 deltaX = -visualMargin.left() + symbolWidthOffset;
485 deltaY = -labelHeight / 2.0;// TODO - should this be adjusted by visual margin??
486 break;
487
490 alpha = 5 * M_PI_4;
491 deltaX = -labelWidth + visualMargin.right() - symbolWidthOffset;
492 deltaY = -labelHeight + visualMargin.top() - symbolHeightOffset;
493 break;
494
496 quadrant = LabelPosition::QuadrantBelowRight; //right quadrant, so labels are left-aligned
497 alpha = 3 * M_PI_2;
498 deltaX = -labelWidth / 4.0 - visualMargin.left();
499 deltaY = -labelHeight + visualMargin.top() - symbolHeightOffset;
500 break;
501
504 alpha = 3 * M_PI_2;
505 deltaX = -labelWidth / 2.0;
506 deltaY = -labelHeight + visualMargin.top() - symbolHeightOffset;
507 break;
508
510 quadrant = LabelPosition::QuadrantBelowLeft; //left quadrant, so labels are right-aligned
511 alpha = 3 * M_PI_2;
512 deltaX = -labelWidth * 3.0 / 4.0 + visualMargin.right();
513 deltaY = -labelHeight + visualMargin.top() - symbolHeightOffset;
514 break;
515
518 alpha = 7 * M_PI_4;
519 deltaX = -visualMargin.left() + symbolWidthOffset;
520 deltaY = -labelHeight + visualMargin.top() - symbolHeightOffset;
521 break;
522 }
523
524 // Take care of the label angle when creating candidates. See pr comments #44944 for details
525 // https://github.com/qgis/QGIS/pull/44944#issuecomment-914670088
526 QTransform transformRotation;
527 transformRotation.rotate( angle * 180 / M_PI );
528 transformRotation.map( deltaX, deltaY, &deltaX, &deltaY );
529
530 //have bearing, distance - calculate reference point
531 double referenceX = std::cos( alpha ) * distanceToLabel + x;
532 double referenceY = std::sin( alpha ) * distanceToLabel + y;
533
534 labelX = referenceX + deltaX;
535 labelY = referenceY + deltaY;
536}
537
538std::size_t FeaturePart::createCandidatesAtOrderedPositionsOverPoint( double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle )
539{
540 const QVector< Qgis::LabelPredefinedPointPosition > positions = mLF->predefinedPositionOrder();
541 double labelWidth = getLabelWidth( angle );
542 double labelHeight = getLabelHeight( angle );
543 double distanceToLabel = getLabelDistance();
544 const QgsMargins &visualMargin = mLF->visualMargin();
545
546 double symbolWidthOffset{ 0 };
547 double symbolHeightOffset{ 0 };
548
550 {
551 // Multi?
552 if ( mLF->feature().geometry().constParts().hasNext() )
553 {
554 const QgsGeometry geom{ QgsGeos::fromGeos( mLF->geometry() ) };
555 symbolWidthOffset = ( mLF->symbolSize().width() - geom.boundingBox().width() ) / 2.0;
556 symbolHeightOffset = ( mLF->symbolSize().height() - geom.boundingBox().height() ) / 2.0;
557 }
558 else
559 {
560 symbolWidthOffset = mLF->symbolSize().width() / 2.0;
561 symbolHeightOffset = mLF->symbolSize().height() / 2.0;
562 }
563 }
564
565 double cost = 0.0001;
566 std::size_t i = lPos.size();
567
568 const std::size_t maxNumberCandidates = mLF->layer()->maximumPointLabelCandidates();
569 std::size_t created = 0;
570 for ( Qgis::LabelPredefinedPointPosition position : positions )
571 {
573
574 double labelX = 0;
575 double labelY = 0;
576 createCandidateAtOrderedPositionOverPoint( labelX, labelY, quadrant, x, y, labelWidth, labelHeight, position, distanceToLabel, visualMargin, symbolWidthOffset, symbolHeightOffset, angle );
577
578 if ( ! mLF->permissibleZonePrepared() || GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), labelX, labelY, labelWidth, labelHeight, angle ) )
579 {
580 lPos.emplace_back( std::make_unique< LabelPosition >( i, labelX, labelY, labelWidth, labelHeight, angle, cost, this, false, quadrant ) );
581 created++;
582 //TODO - tweak
583 cost += 0.001;
584 if ( maxNumberCandidates > 0 && created >= maxNumberCandidates )
585 break;
586 }
587 ++i;
588 }
589
590 return created;
591}
592
593std::size_t FeaturePart::createCandidatesAroundPoint( double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle )
594{
595 double labelWidth = getLabelWidth( angle );
596 double labelHeight = getLabelHeight( angle );
597 double distanceToLabel = getLabelDistance();
598
599 std::size_t maxNumberCandidates = mLF->layer()->maximumPointLabelCandidates();
600 if ( maxNumberCandidates == 0 )
601 maxNumberCandidates = 16;
602
603 int icost = 0;
604 int inc = 2;
605 int id = lPos.size();
606
607 double candidateAngleIncrement = 2 * M_PI / maxNumberCandidates; /* angle bw 2 pos */
608
609 /* various angles */
610 double a90 = M_PI_2;
611 double a180 = M_PI;
612 double a270 = a180 + a90;
613 double a360 = 2 * M_PI;
614
615 double gamma1, gamma2;
616
617 if ( distanceToLabel > 0 )
618 {
619 gamma1 = std::atan2( labelHeight / 2, distanceToLabel + labelWidth / 2 );
620 gamma2 = std::atan2( labelWidth / 2, distanceToLabel + labelHeight / 2 );
621 }
622 else
623 {
624 gamma1 = gamma2 = a90 / 3.0;
625 }
626
627 if ( gamma1 > a90 / 3.0 )
628 gamma1 = a90 / 3.0;
629
630 if ( gamma2 > a90 / 3.0 )
631 gamma2 = a90 / 3.0;
632
633 std::size_t numberCandidatesGenerated = 0;
634
635 std::size_t i;
636 double angleToCandidate;
637 for ( i = 0, angleToCandidate = M_PI_4; i < maxNumberCandidates; i++, angleToCandidate += candidateAngleIncrement )
638 {
639 double deltaX = 0.0;
640 double deltaY = 0.0;
641
642 if ( angleToCandidate > a360 )
643 angleToCandidate -= a360;
644
646
647 if ( angleToCandidate < gamma1 || angleToCandidate > a360 - gamma1 ) // on the right
648 {
649 deltaX = distanceToLabel;
650 double iota = ( angleToCandidate + gamma1 );
651 if ( iota > a360 - gamma1 )
652 iota -= a360;
653
654 //ly += -yrm/2.0 + tan(alpha)*(distlabel + xrm/2);
655 deltaY = -labelHeight + labelHeight * iota / ( 2 * gamma1 );
656
658 }
659 else if ( angleToCandidate < a90 - gamma2 ) // top-right
660 {
661 deltaX = distanceToLabel * std::cos( angleToCandidate );
662 deltaY = distanceToLabel * std::sin( angleToCandidate );
664 }
665 else if ( angleToCandidate < a90 + gamma2 ) // top
666 {
667 //lx += -xrm/2.0 - tan(alpha+a90)*(distlabel + yrm/2);
668 deltaX = -labelWidth * ( angleToCandidate - a90 + gamma2 ) / ( 2 * gamma2 );
669 deltaY = distanceToLabel;
671 }
672 else if ( angleToCandidate < a180 - gamma1 ) // top left
673 {
674 deltaX = distanceToLabel * std::cos( angleToCandidate ) - labelWidth;
675 deltaY = distanceToLabel * std::sin( angleToCandidate );
677 }
678 else if ( angleToCandidate < a180 + gamma1 ) // left
679 {
680 deltaX = -distanceToLabel - labelWidth;
681 //ly += -yrm/2.0 - tan(alpha)*(distlabel + xrm/2);
682 deltaY = - ( angleToCandidate - a180 + gamma1 ) * labelHeight / ( 2 * gamma1 );
684 }
685 else if ( angleToCandidate < a270 - gamma2 ) // down - left
686 {
687 deltaX = distanceToLabel * std::cos( angleToCandidate ) - labelWidth;
688 deltaY = distanceToLabel * std::sin( angleToCandidate ) - labelHeight;
690 }
691 else if ( angleToCandidate < a270 + gamma2 ) // down
692 {
693 deltaY = -distanceToLabel - labelHeight;
694 //lx += -xrm/2.0 + tan(alpha+a90)*(distlabel + yrm/2);
695 deltaX = -labelWidth + ( angleToCandidate - a270 + gamma2 ) * labelWidth / ( 2 * gamma2 );
697 }
698 else if ( angleToCandidate < a360 ) // down - right
699 {
700 deltaX = distanceToLabel * std::cos( angleToCandidate );
701 deltaY = distanceToLabel * std::sin( angleToCandidate ) - labelHeight;
703 }
704
705 // Take care of the label angle when creating candidates. See pr comments #44944 for details
706 // https://github.com/qgis/QGIS/pull/44944#issuecomment-914670088
707 QTransform transformRotation;
708 transformRotation.rotate( angle * 180 / M_PI );
709 transformRotation.map( deltaX, deltaY, &deltaX, &deltaY );
710
711 double labelX = x + deltaX;
712 double labelY = y + deltaY;
713
714 double cost;
715
716 if ( maxNumberCandidates == 1 )
717 cost = 0.0001;
718 else
719 cost = 0.0001 + 0.0020 * double( icost ) / double( maxNumberCandidates - 1 );
720
721
723 {
724 if ( !GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), labelX, labelY, labelWidth, labelHeight, angle ) )
725 {
726 continue;
727 }
728 }
729
730 lPos.emplace_back( std::make_unique< LabelPosition >( id + i, labelX, labelY, labelWidth, labelHeight, angle, cost, this, false, quadrant ) );
731 numberCandidatesGenerated++;
732
733 icost += inc;
734
735 if ( icost == static_cast< int >( maxNumberCandidates ) )
736 {
737 icost = static_cast< int >( maxNumberCandidates ) - 1;
738 inc = -2;
739 }
740 else if ( icost > static_cast< int >( maxNumberCandidates ) )
741 {
742 icost = static_cast< int >( maxNumberCandidates ) - 2;
743 inc = -2;
744 }
745
746 }
747
748 return numberCandidatesGenerated;
749}
750
751std::size_t FeaturePart::createCandidatesAlongLine( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, bool allowOverrun, Pal *pal )
752{
753 if ( allowOverrun )
754 {
755 double shapeLength = mapShape->length();
756 if ( totalRepeats() > 1 && shapeLength < getLabelWidth() )
757 return 0;
758 else if ( shapeLength < getLabelWidth() - 2 * std::min( getLabelWidth(), mLF->overrunDistance() ) )
759 {
760 // label doesn't fit on this line, don't waste time trying to make candidates
761 return 0;
762 }
763 }
764
765 //prefer to label along straightish segments:
766 std::size_t candidates = 0;
767
769 candidates = createCandidatesAlongLineNearStraightSegments( lPos, mapShape, pal );
770
771 const std::size_t candidateTargetCount = maximumLineCandidates();
772 if ( candidates < candidateTargetCount )
773 {
774 // but not enough candidates yet, so fallback to labeling near whole line's midpoint
775 candidates = createCandidatesAlongLineNearMidpoint( lPos, mapShape, candidates > 0 ? 0.01 : 0.0, pal );
776 }
777 return candidates;
778}
779
780std::size_t FeaturePart::createHorizontalCandidatesAlongLine( std::vector<std::unique_ptr<LabelPosition> > &lPos, PointSet *mapShape, Pal *pal )
781{
782 const double labelWidth = getLabelWidth();
783 const double labelHeight = getLabelHeight();
784
785 PointSet *line = mapShape;
786 int nbPoints = line->nbPoints;
787 std::vector< double > &x = line->x;
788 std::vector< double > &y = line->y;
789
790 std::vector< double > segmentLengths( nbPoints - 1 ); // segments lengths distance bw pt[i] && pt[i+1]
791 std::vector< double >distanceToSegment( nbPoints ); // absolute distance bw pt[0] and pt[i] along the line
792
793 double totalLineLength = 0.0; // line length
794 for ( int i = 0; i < line->nbPoints - 1; i++ )
795 {
796 if ( i == 0 )
797 distanceToSegment[i] = 0;
798 else
799 distanceToSegment[i] = distanceToSegment[i - 1] + segmentLengths[i - 1];
800
801 segmentLengths[i] = GeomFunction::dist_euc2d( x[i], y[i], x[i + 1], y[i + 1] );
802 totalLineLength += segmentLengths[i];
803 }
804 distanceToSegment[line->nbPoints - 1] = totalLineLength;
805
806 const std::size_t candidateTargetCount = maximumLineCandidates();
807 double lineStepDistance = 0;
808
809 const double lineAnchorPoint = totalLineLength * mLF->lineAnchorPercent();
810 double currentDistanceAlongLine = lineStepDistance;
811 switch ( mLF->lineAnchorType() )
812 {
814 lineStepDistance = totalLineLength / ( candidateTargetCount + 1 ); // distance to move along line with each candidate
815 break;
816
818 currentDistanceAlongLine = lineAnchorPoint;
819 lineStepDistance = -1;
820 break;
821 }
822
824
825 double candidateCenterX, candidateCenterY;
826 int i = 0;
827 while ( currentDistanceAlongLine <= totalLineLength )
828 {
829 if ( pal->isCanceled() )
830 {
831 return lPos.size();
832 }
833
834 line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine, &candidateCenterX, &candidateCenterY );
835
836 // penalize positions which are further from the line's anchor point
837 double cost = std::fabs( lineAnchorPoint - currentDistanceAlongLine ) / totalLineLength; // <0, 0.5>
838 cost /= 1000; // < 0, 0.0005 >
839
840 double labelX = 0;
841 switch ( textPoint )
842 {
844 labelX = candidateCenterX;
845 break;
847 labelX = candidateCenterX - labelWidth / 2;
848 break;
850 labelX = candidateCenterX - labelWidth;
851 break;
853 // not possible here
854 break;
855 }
856 lPos.emplace_back( std::make_unique< LabelPosition >( i, labelX, candidateCenterY - labelHeight / 2, labelWidth, labelHeight, 0, cost, this, false, LabelPosition::QuadrantOver ) );
857
858 currentDistanceAlongLine += lineStepDistance;
859
860 i++;
861
862 if ( lineStepDistance < 0 )
863 break;
864 }
865
866 return lPos.size();
867}
868
869std::size_t FeaturePart::createCandidatesAlongLineNearStraightSegments( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, Pal *pal )
870{
871 double labelWidth = getLabelWidth();
872 double labelHeight = getLabelHeight();
873 double distanceLineToLabel = getLabelDistance();
874 QgsLabeling::LinePlacementFlags flags = mLF->arrangementFlags();
875 if ( flags == 0 )
876 flags = QgsLabeling::LinePlacementFlag::OnLine; // default flag
877
878 // first scan through the whole line and look for segments where the angle at a node is greater than 45 degrees - these form a "hard break" which labels shouldn't cross over
879 QVector< int > extremeAngleNodes;
880 PointSet *line = mapShape;
881 int numberNodes = line->nbPoints;
882 std::vector< double > &x = line->x;
883 std::vector< double > &y = line->y;
884
885 // closed line? if so, we need to handle the final node angle
886 bool closedLine = qgsDoubleNear( x[0], x[ numberNodes - 1] ) && qgsDoubleNear( y[0], y[numberNodes - 1 ] );
887 for ( int i = 1; i <= numberNodes - ( closedLine ? 1 : 2 ); ++i )
888 {
889 double x1 = x[i - 1];
890 double x2 = x[i];
891 double x3 = x[ i == numberNodes - 1 ? 1 : i + 1]; // wraparound for closed linestrings
892 double y1 = y[i - 1];
893 double y2 = y[i];
894 double y3 = y[ i == numberNodes - 1 ? 1 : i + 1]; // wraparound for closed linestrings
895 if ( qgsDoubleNear( y2, y3 ) && qgsDoubleNear( x2, x3 ) )
896 continue;
897 if ( qgsDoubleNear( y1, y2 ) && qgsDoubleNear( x1, x2 ) )
898 continue;
899 double vertexAngle = M_PI - ( std::atan2( y3 - y2, x3 - x2 ) - std::atan2( y2 - y1, x2 - x1 ) );
900 vertexAngle = QgsGeometryUtils::normalizedAngle( vertexAngle );
901
902 // extreme angles form more than 45 degree angle at a node - these are the ones we don't want labels to cross
903 if ( vertexAngle < M_PI * 135.0 / 180.0 || vertexAngle > M_PI * 225.0 / 180.0 )
904 extremeAngleNodes << i;
905 }
906 extremeAngleNodes << numberNodes - 1;
907
908 if ( extremeAngleNodes.isEmpty() )
909 {
910 // no extreme angles - createCandidatesAlongLineNearMidpoint will be more appropriate
911 return 0;
912 }
913
914 // calculate lengths of segments, and work out longest straight-ish segment
915 std::vector< double > segmentLengths( numberNodes - 1 ); // segments lengths distance bw pt[i] && pt[i+1]
916 std::vector< double > distanceToSegment( numberNodes ); // absolute distance bw pt[0] and pt[i] along the line
917 double totalLineLength = 0.0;
918 QVector< double > straightSegmentLengths;
919 QVector< double > straightSegmentAngles;
920 straightSegmentLengths.reserve( extremeAngleNodes.size() + 1 );
921 straightSegmentAngles.reserve( extremeAngleNodes.size() + 1 );
922 double currentStraightSegmentLength = 0;
923 double longestSegmentLength = 0;
924 int segmentIndex = 0;
925 double segmentStartX = x[0];
926 double segmentStartY = y[0];
927 for ( int i = 0; i < numberNodes - 1; i++ )
928 {
929 if ( i == 0 )
930 distanceToSegment[i] = 0;
931 else
932 distanceToSegment[i] = distanceToSegment[i - 1] + segmentLengths[i - 1];
933
934 segmentLengths[i] = GeomFunction::dist_euc2d( x[i], y[i], x[i + 1], y[i + 1] );
935 totalLineLength += segmentLengths[i];
936 if ( extremeAngleNodes.contains( i ) )
937 {
938 // at an extreme angle node, so reset counters
939 straightSegmentLengths << currentStraightSegmentLength;
940 straightSegmentAngles << QgsGeometryUtils::normalizedAngle( std::atan2( y[i] - segmentStartY, x[i] - segmentStartX ) );
941 longestSegmentLength = std::max( longestSegmentLength, currentStraightSegmentLength );
942 segmentIndex++;
943 currentStraightSegmentLength = 0;
944 segmentStartX = x[i];
945 segmentStartY = y[i];
946 }
947 currentStraightSegmentLength += segmentLengths[i];
948 }
949 distanceToSegment[line->nbPoints - 1] = totalLineLength;
950 straightSegmentLengths << currentStraightSegmentLength;
951 straightSegmentAngles << QgsGeometryUtils::normalizedAngle( std::atan2( y[numberNodes - 1] - segmentStartY, x[numberNodes - 1] - segmentStartX ) );
952 longestSegmentLength = std::max( longestSegmentLength, currentStraightSegmentLength );
953 const double lineAnchorPoint = totalLineLength * mLF->lineAnchorPercent();
954
955 if ( totalLineLength < labelWidth )
956 {
957 return 0; //createCandidatesAlongLineNearMidpoint will be more appropriate
958 }
959
961
962 const std::size_t candidateTargetCount = maximumLineCandidates();
963 double lineStepDistance = ( totalLineLength - labelWidth ); // distance to move along line with each candidate
964 lineStepDistance = std::min( std::min( labelHeight, labelWidth ), lineStepDistance / candidateTargetCount );
965
966 double distanceToEndOfSegment = 0.0;
967 int lastNodeInSegment = 0;
968 // finally, loop through all these straight segments. For each we create candidates along the straight segment.
969 for ( int i = 0; i < straightSegmentLengths.count(); ++i )
970 {
971 currentStraightSegmentLength = straightSegmentLengths.at( i );
972 double currentSegmentAngle = straightSegmentAngles.at( i );
973 lastNodeInSegment = extremeAngleNodes.at( i );
974 double distanceToStartOfSegment = distanceToEndOfSegment;
975 distanceToEndOfSegment = distanceToSegment[ lastNodeInSegment ];
976 double distanceToCenterOfSegment = 0.5 * ( distanceToEndOfSegment + distanceToStartOfSegment );
977
978 if ( currentStraightSegmentLength < labelWidth )
979 // can't fit a label on here
980 continue;
981
982 double currentDistanceAlongLine = distanceToStartOfSegment;
983 double candidateStartX, candidateStartY, candidateEndX, candidateEndY;
984 double candidateLength = 0.0;
985 double cost = 0.0;
986 double angle = 0.0;
987 double beta = 0.0;
988
989 //calculate some cost penalties
990 double segmentCost = 1.0 - ( distanceToEndOfSegment - distanceToStartOfSegment ) / longestSegmentLength; // 0 -> 1 (lower for longer segments)
991 double segmentAngleCost = 1 - std::fabs( std::fmod( currentSegmentAngle, M_PI ) - M_PI_2 ) / M_PI_2; // 0 -> 1, lower for more horizontal segments
992
993 while ( currentDistanceAlongLine + labelWidth < distanceToEndOfSegment )
994 {
995 if ( pal->isCanceled() )
996 {
997 return lPos.size();
998 }
999
1000 // calculate positions along linestring corresponding to start and end of current label candidate
1001 line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine, &candidateStartX, &candidateStartY );
1002 line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine + labelWidth, &candidateEndX, &candidateEndY );
1003
1004 candidateLength = std::sqrt( ( candidateEndX - candidateStartX ) * ( candidateEndX - candidateStartX ) + ( candidateEndY - candidateStartY ) * ( candidateEndY - candidateStartY ) );
1005
1006
1007 // LOTS OF DIFFERENT COSTS TO BALANCE HERE - feel free to tweak these, but please add a unit test
1008 // which covers the situation you are adjusting for (e.g., "given equal length lines, choose the more horizontal line")
1009
1010 cost = candidateLength / labelWidth;
1011 if ( cost > 0.98 )
1012 cost = 0.0001;
1013 else
1014 {
1015 // jaggy line has a greater cost
1016 cost = ( 1 - cost ) / 100; // ranges from 0.0001 to 0.01 (however a cost 0.005 is already a lot!)
1017 }
1018
1019 const double labelCenter = currentDistanceAlongLine + labelWidth / 2.0;
1020 double labelTextAnchor = 0;
1021 switch ( textPoint )
1022 {
1024 labelTextAnchor = currentDistanceAlongLine;
1025 break;
1027 labelTextAnchor = currentDistanceAlongLine + labelWidth / 2.0;
1028 break;
1030 labelTextAnchor = currentDistanceAlongLine + labelWidth;
1031 break;
1033 // not possible here
1034 break;
1035 }
1036
1037 const bool placementIsFlexible = mLF->lineAnchorPercent() > 0.1 && mLF->lineAnchorPercent() < 0.9;
1038 // penalize positions which are further from the straight segments's midpoint
1039 if ( placementIsFlexible )
1040 {
1041 // only apply this if labels are being placed toward the center of overall lines -- otherwise it messes with the distance from anchor cost
1042 double costCenter = 2 * std::fabs( labelCenter - distanceToCenterOfSegment ) / ( distanceToEndOfSegment - distanceToStartOfSegment ); // 0 -> 1
1043 cost += costCenter * 0.0005; // < 0, 0.0005 >
1044 }
1045
1046 if ( !closedLine )
1047 {
1048 // penalize positions which are further from line anchor point of whole linestring (by default the middle of the line)
1049 // this only applies to non closed linestrings, since the middle of a closed linestring is effectively arbitrary
1050 // and irrelevant to labeling
1051 double costLineCenter = 2 * std::fabs( labelTextAnchor - lineAnchorPoint ) / totalLineLength; // 0 -> 1
1052 cost += costLineCenter * 0.0005; // < 0, 0.0005 >
1053 }
1054
1055 if ( placementIsFlexible )
1056 {
1057 cost += segmentCost * 0.0005; // prefer labels on longer straight segments
1058 cost += segmentAngleCost * 0.0001; // prefer more horizontal segments, but this is less important than length considerations
1059 }
1060
1061 if ( qgsDoubleNear( candidateEndY, candidateStartY ) && qgsDoubleNear( candidateEndX, candidateStartX ) )
1062 {
1063 angle = 0.0;
1064 }
1065 else
1066 angle = std::atan2( candidateEndY - candidateStartY, candidateEndX - candidateStartX );
1067
1068 labelWidth = getLabelWidth( angle );
1069 labelHeight = getLabelHeight( angle );
1070 beta = angle + M_PI_2;
1071
1073 {
1074 // find out whether the line direction for this candidate is from right to left
1075 bool isRightToLeft = ( angle > M_PI_2 || angle <= -M_PI_2 );
1076 // meaning of above/below may be reversed if using map orientation and the line has right-to-left direction
1077 bool reversed = ( ( flags & QgsLabeling::LinePlacementFlag::MapOrientation ) ? isRightToLeft : false );
1078 bool aboveLine = ( !reversed && ( flags & QgsLabeling::LinePlacementFlag::AboveLine ) ) || ( reversed && ( flags & QgsLabeling::LinePlacementFlag::BelowLine ) );
1079 bool belowLine = ( !reversed && ( flags & QgsLabeling::LinePlacementFlag::BelowLine ) ) || ( reversed && ( flags & QgsLabeling::LinePlacementFlag::AboveLine ) );
1080
1081 if ( belowLine )
1082 {
1083 if ( !mLF->permissibleZonePrepared() || GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), candidateStartX - std::cos( beta ) * ( distanceLineToLabel + labelHeight ), candidateStartY - std::sin( beta ) * ( distanceLineToLabel + labelHeight ), labelWidth, labelHeight, angle ) )
1084 {
1085 const double candidateCost = cost + ( reversed ? 0 : 0.001 );
1086 lPos.emplace_back( std::make_unique< LabelPosition >( i, candidateStartX - std::cos( beta ) * ( distanceLineToLabel + labelHeight ), candidateStartY - std::sin( beta ) * ( distanceLineToLabel + labelHeight ), labelWidth, labelHeight, angle, candidateCost, this, isRightToLeft, LabelPosition::QuadrantOver ) ); // Line
1087 }
1088 }
1089 if ( aboveLine )
1090 {
1091 if ( !mLF->permissibleZonePrepared() || GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), candidateStartX + std::cos( beta ) *distanceLineToLabel, candidateStartY + std::sin( beta ) *distanceLineToLabel, labelWidth, labelHeight, angle ) )
1092 {
1093 const double candidateCost = cost + ( !reversed ? 0 : 0.001 ); // no extra cost for above line placements
1094 lPos.emplace_back( std::make_unique< LabelPosition >( i, candidateStartX + std::cos( beta ) *distanceLineToLabel, candidateStartY + std::sin( beta ) *distanceLineToLabel, labelWidth, labelHeight, angle, candidateCost, this, isRightToLeft, LabelPosition::QuadrantOver ) ); // Line
1095 }
1096 }
1098 {
1099 if ( !mLF->permissibleZonePrepared() || GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), candidateStartX - labelHeight * std::cos( beta ) / 2, candidateStartY - labelHeight * std::sin( beta ) / 2, labelWidth, labelHeight, angle ) )
1100 {
1101 const double candidateCost = cost + 0.002;
1102 lPos.emplace_back( std::make_unique< LabelPosition >( i, candidateStartX - labelHeight * std::cos( beta ) / 2, candidateStartY - labelHeight * std::sin( beta ) / 2, labelWidth, labelHeight, angle, candidateCost, this, isRightToLeft, LabelPosition::QuadrantOver ) ); // Line
1103 }
1104 }
1105 }
1107 {
1108 lPos.emplace_back( std::make_unique< LabelPosition >( i, candidateStartX - labelWidth / 2, candidateStartY - labelHeight / 2, labelWidth, labelHeight, 0, cost, this, false, LabelPosition::QuadrantOver ) ); // Line
1109 }
1110 else
1111 {
1112 // an invalid arrangement?
1113 }
1114
1115 currentDistanceAlongLine += lineStepDistance;
1116 }
1117 }
1118
1119 return lPos.size();
1120}
1121
1122std::size_t FeaturePart::createCandidatesAlongLineNearMidpoint( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, double initialCost, Pal *pal )
1123{
1124 double distanceLineToLabel = getLabelDistance();
1125
1126 double labelWidth = getLabelWidth();
1127 double labelHeight = getLabelHeight();
1128
1129 double angle;
1130 double cost;
1131
1132 QgsLabeling::LinePlacementFlags flags = mLF->arrangementFlags();
1133 if ( flags == 0 )
1134 flags = QgsLabeling::LinePlacementFlag::OnLine; // default flag
1135
1136 PointSet *line = mapShape;
1137 int nbPoints = line->nbPoints;
1138 std::vector< double > &x = line->x;
1139 std::vector< double > &y = line->y;
1140
1141 std::vector< double > segmentLengths( nbPoints - 1 ); // segments lengths distance bw pt[i] && pt[i+1]
1142 std::vector< double >distanceToSegment( nbPoints ); // absolute distance bw pt[0] and pt[i] along the line
1143
1144 double totalLineLength = 0.0; // line length
1145 for ( int i = 0; i < line->nbPoints - 1; i++ )
1146 {
1147 if ( i == 0 )
1148 distanceToSegment[i] = 0;
1149 else
1150 distanceToSegment[i] = distanceToSegment[i - 1] + segmentLengths[i - 1];
1151
1152 segmentLengths[i] = GeomFunction::dist_euc2d( x[i], y[i], x[i + 1], y[i + 1] );
1153 totalLineLength += segmentLengths[i];
1154 }
1155 distanceToSegment[line->nbPoints - 1] = totalLineLength;
1156
1157 double lineStepDistance = ( totalLineLength - labelWidth ); // distance to move along line with each candidate
1158 double currentDistanceAlongLine = 0;
1159
1161
1162 const std::size_t candidateTargetCount = maximumLineCandidates();
1163
1164 if ( totalLineLength > labelWidth )
1165 {
1166 lineStepDistance = std::min( std::min( labelHeight, labelWidth ), lineStepDistance / candidateTargetCount );
1167 }
1168 else if ( !line->isClosed() ) // line length < label width => centering label position
1169 {
1170 currentDistanceAlongLine = - ( labelWidth - totalLineLength ) / 2.0;
1171 lineStepDistance = -1;
1172 totalLineLength = labelWidth;
1173 }
1174 else
1175 {
1176 // closed line, not long enough for label => no candidates!
1177 currentDistanceAlongLine = std::numeric_limits< double >::max();
1178 }
1179
1180 const double lineAnchorPoint = totalLineLength * std::min( 0.99, mLF->lineAnchorPercent() ); // don't actually go **all** the way to end of line, just very close to!
1181
1182 switch ( mLF->lineAnchorType() )
1183 {
1185 break;
1186
1188 switch ( textPoint )
1189 {
1191 currentDistanceAlongLine = std::min( lineAnchorPoint, totalLineLength * 0.99 - labelWidth );
1192 break;
1194 currentDistanceAlongLine = std::min( lineAnchorPoint - labelWidth / 2, totalLineLength * 0.99 - labelWidth );
1195 break;
1197 currentDistanceAlongLine = std::min( lineAnchorPoint - labelWidth, totalLineLength * 0.99 - labelWidth );
1198 break;
1200 // not possible here
1201 break;
1202 }
1203 lineStepDistance = -1;
1204 break;
1205 }
1206
1207 double candidateLength;
1208 double beta;
1209 double candidateStartX, candidateStartY, candidateEndX, candidateEndY;
1210 int i = 0;
1211 while ( currentDistanceAlongLine <= totalLineLength - labelWidth || mLF->lineAnchorType() == QgsLabelLineSettings::AnchorType::Strict )
1212 {
1213 if ( pal->isCanceled() )
1214 {
1215 return lPos.size();
1216 }
1217
1218 // calculate positions along linestring corresponding to start and end of current label candidate
1219 line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine, &candidateStartX, &candidateStartY );
1220 line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine + labelWidth, &candidateEndX, &candidateEndY );
1221
1222 if ( currentDistanceAlongLine < 0 )
1223 {
1224 // label is bigger than line, use whole available line
1225 candidateLength = std::sqrt( ( x[nbPoints - 1] - x[0] ) * ( x[nbPoints - 1] - x[0] )
1226 + ( y[nbPoints - 1] - y[0] ) * ( y[nbPoints - 1] - y[0] ) );
1227 }
1228 else
1229 {
1230 candidateLength = std::sqrt( ( candidateEndX - candidateStartX ) * ( candidateEndX - candidateStartX ) + ( candidateEndY - candidateStartY ) * ( candidateEndY - candidateStartY ) );
1231 }
1232
1233 cost = candidateLength / labelWidth;
1234 if ( cost > 0.98 )
1235 cost = 0.0001;
1236 else
1237 {
1238 // jaggy line has a greater cost
1239 cost = ( 1 - cost ) / 100; // ranges from 0.0001 to 0.01 (however a cost 0.005 is already a lot!)
1240 }
1241
1242 // penalize positions which are further from the line's anchor point
1243 double textAnchorPoint = 0;
1244 switch ( textPoint )
1245 {
1247 textAnchorPoint = currentDistanceAlongLine;
1248 break;
1250 textAnchorPoint = currentDistanceAlongLine + labelWidth / 2;
1251 break;
1253 textAnchorPoint = currentDistanceAlongLine + labelWidth;
1254 break;
1256 // not possible here
1257 break;
1258 }
1259 double costCenter = std::fabs( lineAnchorPoint - textAnchorPoint ) / totalLineLength; // <0, 0.5>
1260 cost += costCenter / 1000; // < 0, 0.0005 >
1261 cost += initialCost;
1262
1263 if ( qgsDoubleNear( candidateEndY, candidateStartY ) && qgsDoubleNear( candidateEndX, candidateStartX ) )
1264 {
1265 angle = 0.0;
1266 }
1267 else
1268 angle = std::atan2( candidateEndY - candidateStartY, candidateEndX - candidateStartX );
1269
1270 labelWidth = getLabelWidth( angle );
1271 labelHeight = getLabelHeight( angle );
1272 beta = angle + M_PI_2;
1273
1275 {
1276 // find out whether the line direction for this candidate is from right to left
1277 bool isRightToLeft = ( angle > M_PI_2 || angle <= -M_PI_2 );
1278 // meaning of above/below may be reversed if using map orientation and the line has right-to-left direction
1279 bool reversed = ( ( flags & QgsLabeling::LinePlacementFlag::MapOrientation ) ? isRightToLeft : false );
1280 bool aboveLine = ( !reversed && ( flags & QgsLabeling::LinePlacementFlag::AboveLine ) ) || ( reversed && ( flags & QgsLabeling::LinePlacementFlag::BelowLine ) );
1281 bool belowLine = ( !reversed && ( flags & QgsLabeling::LinePlacementFlag::BelowLine ) ) || ( reversed && ( flags & QgsLabeling::LinePlacementFlag::AboveLine ) );
1282
1283 if ( aboveLine )
1284 {
1285 if ( !mLF->permissibleZonePrepared() || GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), candidateStartX + std::cos( beta ) *distanceLineToLabel, candidateStartY + std::sin( beta ) *distanceLineToLabel, labelWidth, labelHeight, angle ) )
1286 {
1287 const double candidateCost = cost + ( !reversed ? 0 : 0.001 ); // no extra cost for above line placements
1288 lPos.emplace_back( std::make_unique< LabelPosition >( i, candidateStartX + std::cos( beta ) *distanceLineToLabel, candidateStartY + std::sin( beta ) *distanceLineToLabel, labelWidth, labelHeight, angle, candidateCost, this, isRightToLeft, LabelPosition::QuadrantOver ) ); // Line
1289 }
1290 }
1291 if ( belowLine )
1292 {
1293 if ( !mLF->permissibleZonePrepared() || GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), candidateStartX - std::cos( beta ) * ( distanceLineToLabel + labelHeight ), candidateStartY - std::sin( beta ) * ( distanceLineToLabel + labelHeight ), labelWidth, labelHeight, angle ) )
1294 {
1295 const double candidateCost = cost + ( !reversed ? 0.001 : 0 );
1296 lPos.emplace_back( std::make_unique< LabelPosition >( i, candidateStartX - std::cos( beta ) * ( distanceLineToLabel + labelHeight ), candidateStartY - std::sin( beta ) * ( distanceLineToLabel + labelHeight ), labelWidth, labelHeight, angle, candidateCost, this, isRightToLeft, LabelPosition::QuadrantOver ) ); // Line
1297 }
1298 }
1300 {
1301 if ( !mLF->permissibleZonePrepared() || GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), candidateStartX - labelHeight * std::cos( beta ) / 2, candidateStartY - labelHeight * std::sin( beta ) / 2, labelWidth, labelHeight, angle ) )
1302 {
1303 const double candidateCost = cost + 0.002;
1304 lPos.emplace_back( std::make_unique< LabelPosition >( i, candidateStartX - labelHeight * std::cos( beta ) / 2, candidateStartY - labelHeight * std::sin( beta ) / 2, labelWidth, labelHeight, angle, candidateCost, this, isRightToLeft, LabelPosition::QuadrantOver ) ); // Line
1305 }
1306 }
1307 }
1309 {
1310 lPos.emplace_back( std::make_unique< LabelPosition >( i, candidateStartX - labelWidth / 2, candidateStartY - labelHeight / 2, labelWidth, labelHeight, 0, cost, this, false, LabelPosition::QuadrantOver ) ); // Line
1311 }
1312 else
1313 {
1314 // an invalid arrangement?
1315 }
1316
1317 currentDistanceAlongLine += lineStepDistance;
1318
1319 i++;
1320
1321 if ( lineStepDistance < 0 )
1322 break;
1323 }
1324
1325 return lPos.size();
1326}
1327
1328std::unique_ptr< LabelPosition > FeaturePart::curvedPlacementAtOffset( PointSet *mapShape, const std::vector< double> &pathDistances, QgsTextRendererUtils::LabelLineDirection direction, const double offsetAlongLine, bool &labeledLineSegmentIsRightToLeft, bool applyAngleConstraints, bool uprightOnly )
1329{
1330 const QgsPrecalculatedTextMetrics *metrics = qgis::down_cast< QgsTextLabelFeature * >( mLF )->textMetrics();
1331 Q_ASSERT( metrics );
1332
1333 const double maximumCharacterAngleInside = applyAngleConstraints ? std::fabs( qgis::down_cast< QgsTextLabelFeature *>( mLF )->maximumCharacterAngleInside() ) : -1;
1334 const double maximumCharacterAngleOutside = applyAngleConstraints ? std::fabs( qgis::down_cast< QgsTextLabelFeature *>( mLF )->maximumCharacterAngleOutside() ) : -1;
1335
1336 std::unique_ptr< QgsTextRendererUtils::CurvePlacementProperties > placement(
1337 QgsTextRendererUtils::generateCurvedTextPlacement( *metrics, mapShape->x.data(), mapShape->y.data(), mapShape->nbPoints, pathDistances, offsetAlongLine, direction, maximumCharacterAngleInside, maximumCharacterAngleOutside, uprightOnly )
1338 );
1339
1340 labeledLineSegmentIsRightToLeft = !uprightOnly ? placement->labeledLineSegmentIsRightToLeft : placement->flippedCharacterPlacementToGetUprightLabels;
1341
1342 if ( placement->graphemePlacement.empty() )
1343 return nullptr;
1344
1345 auto it = placement->graphemePlacement.constBegin();
1346 std::unique_ptr< LabelPosition > firstPosition = std::make_unique< LabelPosition >( 0, it->x, it->y, it->width, it->height, it->angle, 0.0001, this, false, LabelPosition::QuadrantOver );
1347 firstPosition->setUpsideDownCharCount( placement->upsideDownCharCount );
1348 firstPosition->setPartId( it->graphemeIndex );
1349 LabelPosition *previousPosition = firstPosition.get();
1350 it++;
1351 while ( it != placement->graphemePlacement.constEnd() )
1352 {
1353 std::unique_ptr< LabelPosition > position = std::make_unique< LabelPosition >( 0, it->x, it->y, it->width, it->height, it->angle, 0.0001, this, false, LabelPosition::QuadrantOver );
1354 position->setPartId( it->graphemeIndex );
1355
1356 LabelPosition *nextPosition = position.get();
1357 previousPosition->setNextPart( std::move( position ) );
1358 previousPosition = nextPosition;
1359 it++;
1360 }
1361
1362 return firstPosition;
1363}
1364
1365std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, bool allowOverrun, Pal *pal )
1366{
1367 const QgsPrecalculatedTextMetrics *li = qgis::down_cast< QgsTextLabelFeature *>( mLF )->textMetrics();
1368 Q_ASSERT( li );
1369
1370 // label info must be present
1371 if ( !li )
1372 return 0;
1373
1374 const int characterCount = li->count();
1375 if ( characterCount == 0 )
1376 return 0;
1377
1378 // TODO - we may need an explicit penalty for overhanging labels. Currently, they are penalized just because they
1379 // are further from the line center, so non-overhanding placements are picked where possible.
1380
1381 double totalCharacterWidth = 0;
1382 for ( int i = 0; i < characterCount; ++i )
1383 totalCharacterWidth += li->characterWidth( i );
1384
1385 std::unique_ptr< PointSet > expanded;
1386 double shapeLength = mapShape->length();
1387
1388 if ( totalRepeats() > 1 )
1389 allowOverrun = false;
1390
1391 // unless in strict mode, label overrun should NEVER exceed the label length (or labels would sit off in space).
1392 // in fact, let's require that a minimum of 5% of the label text has to sit on the feature,
1393 // as we don't want a label sitting right at the start or end corner of a line
1394 double overrun = 0;
1395 switch ( mLF->lineAnchorType() )
1396 {
1398 overrun = std::min( mLF->overrunDistance(), totalCharacterWidth * 0.95 );
1399 break;
1401 // in strict mode, we force sufficient overrun to ensure label will always "fit", even if it's placed
1402 // so that the label start sits right on the end of the line OR the label end sits right on the start of the line
1403 overrun = std::max( mLF->overrunDistance(), totalCharacterWidth * 1.05 );
1404 break;
1405 }
1406
1407 if ( totalCharacterWidth > shapeLength )
1408 {
1409 if ( !allowOverrun || shapeLength < totalCharacterWidth - 2 * overrun )
1410 {
1411 // label doesn't fit on this line, don't waste time trying to make candidates
1412 return 0;
1413 }
1414 }
1415
1416 // calculate the anchor point for the original line shape as a GEOS point.
1417 // this must be done BEFORE we account for overrun by extending the shape!
1418 const geos::unique_ptr originalPoint = mapShape->interpolatePoint( shapeLength * mLF->lineAnchorPercent() );
1419
1420 if ( allowOverrun && overrun > 0 )
1421 {
1422 // expand out line on either side to fit label
1423 expanded = mapShape->clone();
1424 expanded->extendLineByDistance( overrun, overrun, mLF->overrunSmoothDistance() );
1425 mapShape = expanded.get();
1426 shapeLength += 2 * overrun;
1427 }
1428
1429 QgsLabeling::LinePlacementFlags flags = mLF->arrangementFlags();
1430 if ( flags == 0 )
1431 flags = QgsLabeling::LinePlacementFlag::OnLine; // default flag
1432 const bool hasAboveBelowLinePlacement = flags & QgsLabeling::LinePlacementFlag::AboveLine || flags & QgsLabeling::LinePlacementFlag::BelowLine;
1433 const double offsetDistance = mLF->distLabel() + li->characterHeight( 0 ) / 2;
1434 std::unique_ptr< PointSet > mapShapeOffsetPositive;
1435 bool positiveShapeHasNegativeDistance = false;
1436 std::unique_ptr< PointSet > mapShapeOffsetNegative;
1437 bool negativeShapeHasNegativeDistance = false;
1438 if ( hasAboveBelowLinePlacement && !qgsDoubleNear( offsetDistance, 0 ) )
1439 {
1440 // create offseted map shapes to be used for above and below line placements
1442 mapShapeOffsetPositive = mapShape->clone();
1444 mapShapeOffsetNegative = mapShape->clone();
1445 if ( offsetDistance >= 0.0 || !( flags & QgsLabeling::LinePlacementFlag::MapOrientation ) )
1446 {
1447 if ( mapShapeOffsetPositive )
1448 mapShapeOffsetPositive->offsetCurveByDistance( offsetDistance );
1449 positiveShapeHasNegativeDistance = offsetDistance < 0;
1450 if ( mapShapeOffsetNegative )
1451 mapShapeOffsetNegative->offsetCurveByDistance( offsetDistance * -1 );
1452 negativeShapeHasNegativeDistance = offsetDistance > 0;
1453 }
1454 else
1455 {
1456 // In case of a negative offset distance, above line placement switch to below line and vice versa
1459 {
1460 flags &= ~QgsLabeling::LinePlacementFlag::AboveLine;
1462 }
1465 {
1466 flags &= ~QgsLabeling::LinePlacementFlag::BelowLine;
1468 }
1469 if ( mapShapeOffsetPositive )
1470 mapShapeOffsetPositive->offsetCurveByDistance( offsetDistance * -1 );
1471 positiveShapeHasNegativeDistance = offsetDistance > 0;
1472 if ( mapShapeOffsetNegative )
1473 mapShapeOffsetNegative->offsetCurveByDistance( offsetDistance );
1474 negativeShapeHasNegativeDistance = offsetDistance < 0;
1475 }
1476 }
1477
1479
1480 std::vector< std::unique_ptr< LabelPosition >> positions;
1481 std::unique_ptr< LabelPosition > backupPlacement;
1482 for ( PathOffset offset : { PositiveOffset, NoOffset, NegativeOffset } )
1483 {
1484 PointSet *currentMapShape = nullptr;
1485 if ( offset == PositiveOffset && hasAboveBelowLinePlacement )
1486 {
1487 currentMapShape = mapShapeOffsetPositive.get();
1488 }
1489 if ( offset == NoOffset && flags & QgsLabeling::LinePlacementFlag::OnLine )
1490 {
1491 currentMapShape = mapShape;
1492 }
1493 if ( offset == NegativeOffset && hasAboveBelowLinePlacement )
1494 {
1495 currentMapShape = mapShapeOffsetNegative.get();
1496 }
1497 if ( !currentMapShape )
1498 continue;
1499
1500 // distance calculation
1501 const auto [ pathDistances, totalDistance ] = currentMapShape->edgeDistances();
1502 if ( qgsDoubleNear( totalDistance, 0.0 ) )
1503 continue;
1504
1505 double lineAnchorPoint = 0;
1506 if ( originalPoint && offset != NoOffset )
1507 {
1508 // the actual anchor point for the offset curves is the closest point on those offset curves
1509 // to the anchor point on the original line. This avoids anchor points which differ greatly
1510 // on the positive/negative offset lines due to line curvature.
1511 lineAnchorPoint = currentMapShape->lineLocatePoint( originalPoint.get() );
1512 }
1513 else
1514 {
1515 lineAnchorPoint = totalDistance * mLF->lineAnchorPercent();
1516 if ( offset == NegativeOffset )
1517 lineAnchorPoint = totalDistance - lineAnchorPoint;
1518 }
1519
1520 if ( pal->isCanceled() )
1521 return 0;
1522
1523 const std::size_t candidateTargetCount = maximumLineCandidates();
1524 double delta = std::max( li->characterHeight( 0 ) / 6, totalDistance / candidateTargetCount );
1525
1526 // generate curved labels
1527 double distanceAlongLineToStartCandidate = 0;
1528 bool singleCandidateOnly = false;
1529 switch ( mLF->lineAnchorType() )
1530 {
1532 break;
1533
1535 switch ( textPoint )
1536 {
1538 distanceAlongLineToStartCandidate = std::clamp( lineAnchorPoint, 0.0, totalDistance * 0.999 );
1539 break;
1541 distanceAlongLineToStartCandidate = std::clamp( lineAnchorPoint - getLabelWidth() / 2, 0.0, totalDistance * 0.999 - getLabelWidth() / 2 );
1542 break;
1544 distanceAlongLineToStartCandidate = std::clamp( lineAnchorPoint - getLabelWidth(), 0.0, totalDistance * 0.999 - getLabelWidth() ) ;
1545 break;
1547 // not possible here
1548 break;
1549 }
1550 singleCandidateOnly = true;
1551 break;
1552 }
1553
1554 bool hasTestedFirstPlacement = false;
1555 for ( ; distanceAlongLineToStartCandidate <= totalDistance; distanceAlongLineToStartCandidate += delta )
1556 {
1557 if ( singleCandidateOnly && hasTestedFirstPlacement )
1558 break;
1559
1560 if ( pal->isCanceled() )
1561 return 0;
1562
1563 hasTestedFirstPlacement = true;
1564 // placements may need to be reversed if using map orientation and the line has right-to-left direction
1565 bool labeledLineSegmentIsRightToLeft = false;
1567 std::unique_ptr< LabelPosition > labelPosition = curvedPlacementAtOffset( currentMapShape, pathDistances, direction, distanceAlongLineToStartCandidate, labeledLineSegmentIsRightToLeft, !singleCandidateOnly,
1568 onlyShowUprightLabels() && ( !singleCandidateOnly || !( flags & QgsLabeling::LinePlacementFlag::MapOrientation ) ) );
1569
1570 if ( !labelPosition )
1571 {
1572 continue;
1573 }
1574
1575
1576 bool isBackupPlacementOnly = false;
1578 {
1579 if ( ( currentMapShape == mapShapeOffsetPositive.get() && positiveShapeHasNegativeDistance )
1580 || ( currentMapShape == mapShapeOffsetNegative.get() && negativeShapeHasNegativeDistance ) )
1581 {
1582 labeledLineSegmentIsRightToLeft = !labeledLineSegmentIsRightToLeft;
1583 }
1584
1585 if ( ( offset != NoOffset ) && !labeledLineSegmentIsRightToLeft && !( flags & QgsLabeling::LinePlacementFlag::AboveLine ) )
1586 {
1587 if ( singleCandidateOnly && offset == PositiveOffset )
1588 isBackupPlacementOnly = true;
1589 else
1590 continue;
1591 }
1592 if ( ( offset != NoOffset ) && labeledLineSegmentIsRightToLeft && !( flags & QgsLabeling::LinePlacementFlag::BelowLine ) )
1593 {
1594 if ( singleCandidateOnly && offset == PositiveOffset )
1595 isBackupPlacementOnly = true;
1596 else
1597 continue;
1598 }
1599 }
1600
1601 backupPlacement.reset();
1602
1603 // evaluate cost
1604 const double angleDiff = labelPosition->angleDifferential();
1605 const double angleDiffAvg = characterCount > 1 ? ( angleDiff / ( characterCount - 1 ) ) : 0; // <0, pi> but pi/8 is much already
1606
1607 // if anchor placement is towards start or end of line, we need to slightly tweak the costs to ensure that the
1608 // anchor weighting is sufficient to push labels towards start/end
1609 const bool anchorIsFlexiblePlacement = !singleCandidateOnly && mLF->lineAnchorPercent() > 0.1 && mLF->lineAnchorPercent() < 0.9;
1610 double cost = angleDiffAvg / 100; // <0, 0.031 > but usually <0, 0.003 >
1611 if ( cost < 0.0001 )
1612 cost = 0.0001;
1613
1614 // penalize positions which are further from the line's anchor point
1615 double labelTextAnchor = 0;
1616 switch ( textPoint )
1617 {
1619 labelTextAnchor = distanceAlongLineToStartCandidate;
1620 break;
1622 labelTextAnchor = distanceAlongLineToStartCandidate + getLabelWidth() / 2;
1623 break;
1625 labelTextAnchor = distanceAlongLineToStartCandidate + getLabelWidth();
1626 break;
1628 // not possible here
1629 break;
1630 }
1631 double costCenter = std::fabs( lineAnchorPoint - labelTextAnchor ) / totalDistance; // <0, 0.5>
1632 cost += costCenter / ( anchorIsFlexiblePlacement ? 100 : 10 ); // < 0, 0.005 >, or <0, 0.05> if preferring placement close to start/end of line
1633
1634 const bool isBelow = ( offset != NoOffset ) && labeledLineSegmentIsRightToLeft;
1635 if ( isBelow )
1636 {
1637 // add additional cost for on line placement
1638 cost += 0.001;
1639 }
1640 else if ( offset == NoOffset )
1641 {
1642 // add additional cost for below line placement
1643 cost += 0.002;
1644 }
1645
1646 labelPosition->setCost( cost );
1647
1648 std::unique_ptr< LabelPosition > p = std::make_unique< LabelPosition >( *labelPosition );
1649 if ( p && mLF->permissibleZonePrepared() )
1650 {
1651 bool within = true;
1652 LabelPosition *currentPos = p.get();
1653 while ( within && currentPos )
1654 {
1655 within = GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), currentPos->getX(), currentPos->getY(), currentPos->getWidth(), currentPos->getHeight(), currentPos->getAlpha() );
1656 currentPos = currentPos->nextPart();
1657 }
1658 if ( !within )
1659 {
1660 p.reset();
1661 }
1662 }
1663
1664 if ( p )
1665 {
1666 if ( isBackupPlacementOnly )
1667 backupPlacement = std::move( p );
1668 else
1669 positions.emplace_back( std::move( p ) );
1670 }
1671 }
1672 }
1673
1674 for ( std::unique_ptr< LabelPosition > &pos : positions )
1675 {
1676 lPos.emplace_back( std::move( pos ) );
1677 }
1678
1679 if ( backupPlacement )
1680 lPos.emplace_back( std::move( backupPlacement ) );
1681
1682 return positions.size();
1683}
1684
1685/*
1686 * seg 2
1687 * pt3 ____________pt2
1688 * ¦ ¦
1689 * ¦ ¦
1690 * seg 3 ¦ BBOX ¦ seg 1
1691 * ¦ ¦
1692 * ¦____________¦
1693 * pt0 seg 0 pt1
1694 *
1695 */
1696
1697std::size_t FeaturePart::createCandidatesForPolygon( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, Pal *pal )
1698{
1699 double labelWidth = getLabelWidth();
1700 double labelHeight = getLabelHeight();
1701
1702 const std::size_t maxPolygonCandidates = mLF->layer()->maximumPolygonLabelCandidates();
1703 const std::size_t targetPolygonCandidates = maxPolygonCandidates > 0 ? std::min( maxPolygonCandidates, static_cast< std::size_t>( std::ceil( mLF->layer()->mPal->maximumPolygonCandidatesPerMapUnitSquared() * area() ) ) )
1704 : 0;
1705
1706 const double totalArea = area();
1707
1708 mapShape->parent = nullptr;
1709
1710 if ( pal->isCanceled() )
1711 return 0;
1712
1713 QLinkedList<PointSet *> shapes_final = splitPolygons( mapShape, labelWidth, labelHeight );
1714#if 0
1715 QgsDebugMsg( QStringLiteral( "PAL split polygons resulted in:" ) );
1716 for ( PointSet *ps : shapes_final )
1717 {
1718 QgsDebugMsg( ps->toWkt() );
1719 }
1720#endif
1721
1722 std::size_t nbp = 0;
1723
1724 if ( !shapes_final.isEmpty() )
1725 {
1726 int id = 0; // ids for candidates
1727 double dlx, dly; // delta from label center and bottom-left corner
1728 double alpha = 0.0; // rotation for the label
1729 double px, py;
1730
1731 double beta;
1732 double diago = std::sqrt( labelWidth * labelWidth / 4.0 + labelHeight * labelHeight / 4 );
1733 double rx, ry;
1734 std::vector< OrientedConvexHullBoundingBox > boxes;
1735 boxes.reserve( shapes_final.size() );
1736
1737 // Compute bounding box for each finalShape
1738 while ( !shapes_final.isEmpty() )
1739 {
1740 PointSet *shape = shapes_final.takeFirst();
1741 bool ok = false;
1743 if ( ok )
1744 boxes.emplace_back( box );
1745
1746 if ( shape->parent )
1747 delete shape;
1748 }
1749
1750 if ( pal->isCanceled() )
1751 return 0;
1752
1753 double densityX = 1.0 / std::sqrt( mLF->layer()->mPal->maximumPolygonCandidatesPerMapUnitSquared() );
1754 double densityY = densityX;
1755 int numTry = 0;
1756
1757 //fit in polygon only mode slows down calculation a lot, so if it's enabled
1758 //then use a smaller limit for number of iterations
1759 int maxTry = mLF->permissibleZonePrepared() ? 7 : 10;
1760
1761 std::size_t numberCandidatesGenerated = 0;
1762
1763 do
1764 {
1765 for ( OrientedConvexHullBoundingBox &box : boxes )
1766 {
1767 // there is two possibilities here:
1768 // 1. no maximum candidates for polygon setting is in effect (i.e. maxPolygonCandidates == 0). In that case,
1769 // we base our dx/dy on the current maximumPolygonCandidatesPerMapUnitSquared value. That should give us the desired
1770 // density of candidates straight up. Easy!
1771 // 2. a maximum candidate setting IS in effect. In that case, we want to generate a good initial estimate for dx/dy
1772 // which gives us a good spatial coverage of the polygon while roughly matching the desired maximum number of candidates.
1773 // If dx/dy is too small, then too many candidates will be generated, which is both slow AND results in poor coverage of the
1774 // polygon (after culling candidates to the max number, only those clustered around the polygon's pole of inaccessibility
1775 // will remain).
1776 double dx = densityX;
1777 double dy = densityY;
1778 if ( numTry == 0 && maxPolygonCandidates > 0 )
1779 {
1780 // scale maxPolygonCandidates for just this convex hull
1781 const double boxArea = box.width * box.length;
1782 double maxThisBox = targetPolygonCandidates * boxArea / totalArea;
1783 dx = std::max( dx, std::sqrt( boxArea / maxThisBox ) * 0.8 );
1784 dy = dx;
1785 }
1786
1787 if ( pal->isCanceled() )
1788 return numberCandidatesGenerated;
1789
1790 if ( ( box.length * box.width ) > ( xmax - xmin ) * ( ymax - ymin ) * 5 )
1791 {
1792 // Very Large BBOX (should never occur)
1793 continue;
1794 }
1795
1797 {
1798 //check width/height of bbox is sufficient for label
1799 if ( mLF->permissibleZone().boundingBox().width() < labelWidth ||
1800 mLF->permissibleZone().boundingBox().height() < labelHeight )
1801 {
1802 //no way label can fit in this box, skip it
1803 continue;
1804 }
1805 }
1806
1807 bool enoughPlace = false;
1809 {
1810 enoughPlace = true;
1811 px = ( box.x[0] + box.x[2] ) / 2 - labelWidth;
1812 py = ( box.y[0] + box.y[2] ) / 2 - labelHeight;
1813 int i, j;
1814
1815 // Virtual label: center on bbox center, label size = 2x original size
1816 // alpha = 0.
1817 // If all corner are in bbox then place candidates horizontaly
1818 for ( rx = px, i = 0; i < 2; rx = rx + 2 * labelWidth, i++ )
1819 {
1820 for ( ry = py, j = 0; j < 2; ry = ry + 2 * labelHeight, j++ )
1821 {
1822 if ( !mapShape->containsPoint( rx, ry ) )
1823 {
1824 enoughPlace = false;
1825 break;
1826 }
1827 }
1828 if ( !enoughPlace )
1829 {
1830 break;
1831 }
1832 }
1833
1834 } // arrangement== FREE ?
1835
1836 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Horizontal || enoughPlace )
1837 {
1838 alpha = 0.0; // HORIZ
1839 }
1840 else if ( box.length > 1.5 * labelWidth && box.width > 1.5 * labelWidth )
1841 {
1842 if ( box.alpha <= M_PI_4 )
1843 {
1844 alpha = box.alpha;
1845 }
1846 else
1847 {
1848 alpha = box.alpha - M_PI_2;
1849 }
1850 }
1851 else if ( box.length > box.width )
1852 {
1853 alpha = box.alpha - M_PI_2;
1854 }
1855 else
1856 {
1857 alpha = box.alpha;
1858 }
1859
1860 beta = std::atan2( labelHeight, labelWidth ) + alpha;
1861
1862
1863 //alpha = box->alpha;
1864
1865 // delta from label center and down-left corner
1866 dlx = std::cos( beta ) * diago;
1867 dly = std::sin( beta ) * diago;
1868
1869 double px0 = box.width / 2.0;
1870 double py0 = box.length / 2.0;
1871
1872 px0 -= std::ceil( px0 / dx ) * dx;
1873 py0 -= std::ceil( py0 / dy ) * dy;
1874
1875 for ( px = px0; px <= box.width; px += dx )
1876 {
1877 if ( pal->isCanceled() )
1878 break;
1879
1880 for ( py = py0; py <= box.length; py += dy )
1881 {
1882
1883 rx = std::cos( box.alpha ) * px + std::cos( box.alpha - M_PI_2 ) * py;
1884 ry = std::sin( box.alpha ) * px + std::sin( box.alpha - M_PI_2 ) * py;
1885
1886 rx += box.x[0];
1887 ry += box.y[0];
1888
1890 {
1891 if ( GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), rx - dlx, ry - dly, labelWidth, labelHeight, alpha ) )
1892 {
1893 // cost is set to minimal value, evaluated later
1894 lPos.emplace_back( std::make_unique< LabelPosition >( id++, rx - dlx, ry - dly, labelWidth, labelHeight, alpha, 0.0001, this, false, LabelPosition::QuadrantOver ) );
1895 numberCandidatesGenerated++;
1896 }
1897 }
1898 else
1899 {
1900 // TODO - this should be an intersection test, not just a contains test of the candidate centroid
1901 // because in some cases we would want to allow candidates which mostly overlap the polygon even though
1902 // their centroid doesn't overlap (e.g. a "U" shaped polygon)
1903 // but the bugs noted in CostCalculator currently prevent this
1904 if ( mapShape->containsPoint( rx, ry ) )
1905 {
1906 std::unique_ptr< LabelPosition > potentialCandidate = std::make_unique< LabelPosition >( id++, rx - dlx, ry - dly, labelWidth, labelHeight, alpha, 0.0001, this, false, LabelPosition::QuadrantOver );
1907 // cost is set to minimal value, evaluated later
1908 lPos.emplace_back( std::move( potentialCandidate ) );
1909 numberCandidatesGenerated++;
1910 }
1911 }
1912 }
1913 }
1914 } // forall box
1915
1916 nbp = numberCandidatesGenerated;
1917 if ( maxPolygonCandidates > 0 && nbp < targetPolygonCandidates )
1918 {
1919 densityX /= 2;
1920 densityY /= 2;
1921 numTry++;
1922 }
1923 else
1924 {
1925 break;
1926 }
1927 }
1928 while ( numTry < maxTry );
1929
1930 nbp = numberCandidatesGenerated;
1931 }
1932 else
1933 {
1934 nbp = 0;
1935 }
1936
1937 return nbp;
1938}
1939
1940std::size_t FeaturePart::createCandidatesOutsidePolygon( std::vector<std::unique_ptr<LabelPosition> > &lPos, Pal *pal )
1941{
1942 // calculate distance between horizontal lines
1943 const std::size_t maxPolygonCandidates = mLF->layer()->maximumPolygonLabelCandidates();
1944 std::size_t candidatesCreated = 0;
1945
1946 double labelWidth = getLabelWidth();
1947 double labelHeight = getLabelHeight();
1948 double distanceToLabel = getLabelDistance();
1949 const QgsMargins &visualMargin = mLF->visualMargin();
1950
1951 /*
1952 * From Rylov & Reimer (2016) "A practical algorithm for the external annotation of area features":
1953 *
1954 * The list of rules adapted to the
1955 * needs of externally labelling areal features is as follows:
1956 * R1. Labels should be placed horizontally.
1957 * R2. Label should be placed entirely outside at some
1958 * distance from the area feature.
1959 * R3. Name should not cross the boundary of its area
1960 * feature.
1961 * R4. The name should be placed in way that takes into
1962 * account the shape of the feature by achieving a
1963 * balance between the feature and its name, emphasizing their relationship.
1964 * R5. The lettering to the right and slightly above the
1965 * symbol is prioritized.
1966 *
1967 * In the following subsections we utilize four of the five rules
1968 * for two subtasks of label placement, namely, for candidate
1969 * positions generation (R1, R2, and R3) and for measuring their
1970 * ‘goodness’ (R4). The rule R5 is applicable only in the case when
1971 * the area of a polygonal feature is small and the feature can be
1972 * treated and labelled as a point-feature
1973 */
1974
1975 /*
1976 * QGIS approach (cite Dawson (2020) if you want ;) )
1977 *
1978 * We differ from the horizontal sweep line approach described by Rylov & Reimer and instead
1979 * rely on just generating a set of points at regular intervals along the boundary of the polygon (exterior ring).
1980 *
1981 * In practice, this generates similar results as Rylov & Reimer, but has the additional benefits that:
1982 * 1. It avoids the need to calculate intersections between the sweep line and the polygon
1983 * 2. For horizontal or near horizontal segments, Rylov & Reimer propose generating evenly spaced points along
1984 * these segments-- i.e. the same approach as we do for the whole polygon
1985 * 3. It's easier to determine in advance exactly how many candidate positions we'll be generating, and accordingly
1986 * we can easily pick the distance between points along the exterior ring so that the number of positions generated
1987 * matches our target number (targetPolygonCandidates)
1988 */
1989
1990 // TO consider -- for very small polygons (wrt label size), treat them just like a point feature?
1991
1992 double cx, cy;
1993 getCentroid( cx, cy, false );
1994
1995 GEOSContextHandle_t ctxt = QgsGeos::getGEOSHandler();
1996
1997 // be a bit sneaky and only buffer out 50% here, and then do the remaining 50% when we make the label candidate itself.
1998 // this avoids candidates being created immediately over the buffered ring and always intersecting with it...
1999 geos::unique_ptr buffer( GEOSBuffer_r( ctxt, geos(), distanceToLabel * 0.5, 1 ) );
2000 std::unique_ptr< QgsAbstractGeometry> gg( QgsGeos::fromGeos( buffer.get() ) );
2001
2002 geos::prepared_unique_ptr preparedBuffer( GEOSPrepare_r( ctxt, buffer.get() ) );
2003
2004 const QgsPolygon *poly = qgsgeometry_cast< const QgsPolygon * >( gg.get() );
2005 if ( !poly )
2006 return candidatesCreated;
2007
2008 const QgsLineString *ring = qgsgeometry_cast< const QgsLineString *>( poly->exteriorRing() );
2009 if ( !ring )
2010 return candidatesCreated;
2011
2012 // 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,
2013 // i.e a loooooong skinny polygon with small area should still generate a large number of candidates)
2014 const double ringLength = ring->length();
2015 const double circleArea = std::pow( ringLength, 2 ) / ( 4 * M_PI );
2016 const std::size_t candidatesForArea = static_cast< std::size_t>( std::ceil( mLF->layer()->mPal->maximumPolygonCandidatesPerMapUnitSquared() * circleArea ) );
2017 const std::size_t targetPolygonCandidates = std::max( static_cast< std::size_t >( 16 ), maxPolygonCandidates > 0 ? std::min( maxPolygonCandidates, candidatesForArea ) : candidatesForArea );
2018
2019 // assume each position generates one candidate
2020 const double delta = ringLength / targetPolygonCandidates;
2021 geos::unique_ptr geosPoint;
2022
2023 const double maxDistCentroidToLabelX = std::max( xmax - cx, cx - xmin ) + distanceToLabel;
2024 const double maxDistCentroidToLabelY = std::max( ymax - cy, cy - ymin ) + distanceToLabel;
2025 const double estimateOfMaxPossibleDistanceCentroidToLabel = std::sqrt( maxDistCentroidToLabelX * maxDistCentroidToLabelX + maxDistCentroidToLabelY * maxDistCentroidToLabelY );
2026
2027 // Satisfy R1: Labels should be placed horizontally.
2028 const double labelAngle = 0;
2029
2030 std::size_t i = lPos.size();
2031 auto addCandidate = [&]( double x, double y, Qgis::LabelPredefinedPointPosition position )
2032 {
2033 double labelX = 0;
2034 double labelY = 0;
2036
2037 // Satisfy R2: Label should be placed entirely outside at some distance from the area feature.
2038 createCandidateAtOrderedPositionOverPoint( labelX, labelY, quadrant, x, y, labelWidth, labelHeight, position, distanceToLabel * 0.5, visualMargin, 0, 0, labelAngle );
2039
2040 std::unique_ptr< LabelPosition > candidate = std::make_unique< LabelPosition >( i, labelX, labelY, labelWidth, labelHeight, labelAngle, 0, this, false, quadrant );
2041 if ( candidate->intersects( preparedBuffer.get() ) )
2042 {
2043 // satisfy R3. Name should not cross the boundary of its area feature.
2044
2045 // actually, we use the buffered geometry here, because a label shouldn't be closer to the polygon then the minimum distance value
2046 return;
2047 }
2048
2049 // cost candidates by their distance to the feature's centroid (following Rylov & Reimer)
2050
2051 // Satisfy R4. The name should be placed in way that takes into
2052 // account the shape of the feature by achieving a
2053 // balance between the feature and its name, emphasizing their relationship.
2054
2055
2056 // here we deviate a little from R&R, and instead of just calculating the centroid distance
2057 // to centroid of label, we calculate the distance from the centroid to the nearest point on the label
2058
2059 const double centroidDistance = candidate->getDistanceToPoint( cx, cy );
2060 const double centroidCost = centroidDistance / estimateOfMaxPossibleDistanceCentroidToLabel;
2061 candidate->setCost( centroidCost );
2062
2063 lPos.emplace_back( std::move( candidate ) );
2064 candidatesCreated++;
2065 ++i;
2066 };
2067
2068 ring->visitPointsByRegularDistance( delta, [&]( double x, double y, double, double,
2069 double startSegmentX, double startSegmentY, double, double,
2070 double endSegmentX, double endSegmentY, double, double )
2071 {
2072 // get normal angle for segment
2073 float angle = atan2( static_cast< float >( endSegmentY - startSegmentY ), static_cast< float >( endSegmentX - startSegmentX ) ) * 180 / M_PI;
2074 if ( angle < 0 )
2075 angle += 360;
2076
2077 // adapted fom Rylov & Reimer figure 9
2078 if ( angle >= 0 && angle <= 5 )
2079 {
2082 }
2083 else if ( angle <= 85 )
2084 {
2086 }
2087 else if ( angle <= 90 )
2088 {
2091 }
2092
2093 else if ( angle <= 95 )
2094 {
2097 }
2098 else if ( angle <= 175 )
2099 {
2101 }
2102 else if ( angle <= 180 )
2103 {
2106 }
2107
2108 else if ( angle <= 185 )
2109 {
2112 }
2113 else if ( angle <= 265 )
2114 {
2116 }
2117 else if ( angle <= 270 )
2118 {
2121 }
2122 else if ( angle <= 275 )
2123 {
2126 }
2127 else if ( angle <= 355 )
2128 {
2130 }
2131 else
2132 {
2135 }
2136
2137 return !pal->isCanceled();
2138 } );
2139
2140 return candidatesCreated;
2141}
2142
2143std::vector< std::unique_ptr< LabelPosition > > FeaturePart::createCandidates( Pal *pal )
2144{
2145 std::vector< std::unique_ptr< LabelPosition > > lPos;
2146 double angleInRadians = mLF->hasFixedAngle() ? mLF->fixedAngle() : 0.0;
2147
2148 if ( mLF->hasFixedPosition() )
2149 {
2150 lPos.emplace_back( std::make_unique< LabelPosition> ( 0, mLF->fixedPosition().x(), mLF->fixedPosition().y(), getLabelWidth( angleInRadians ), getLabelHeight( angleInRadians ), angleInRadians, 0.0, this, false, LabelPosition::Quadrant::QuadrantOver ) );
2151 }
2152 else
2153 {
2154 switch ( type )
2155 {
2156 case GEOS_POINT:
2158 createCandidatesAtOrderedPositionsOverPoint( x[0], y[0], lPos, angleInRadians );
2160 createCandidatesOverPoint( x[0], y[0], lPos, angleInRadians );
2161 else
2162 createCandidatesAroundPoint( x[0], y[0], lPos, angleInRadians );
2163 break;
2164
2165 case GEOS_LINESTRING:
2168 else if ( mLF->layer()->isCurved() )
2169 createCurvedCandidatesAlongLine( lPos, this, true, pal );
2170 else
2171 createCandidatesAlongLine( lPos, this, true, pal );
2172 break;
2173
2174 case GEOS_POLYGON:
2175 {
2176 const double labelWidth = getLabelWidth();
2177 const double labelHeight = getLabelHeight();
2178
2181 //check width/height of bbox is sufficient for label
2182
2183 if ( ( allowOutside && !allowInside ) || ( mLF->layer()->arrangement() == Qgis::LabelPlacement::OutsidePolygons ) )
2184 {
2185 // only allowed to place outside of polygon
2187 }
2188 else if ( allowOutside && ( std::fabs( xmax - xmin ) < labelWidth ||
2189 std::fabs( ymax - ymin ) < labelHeight ) )
2190 {
2191 //no way label can fit in this polygon -- shortcut and only place label outside
2193 }
2194 else
2195 {
2196 std::size_t created = 0;
2197 if ( allowInside )
2198 {
2199 switch ( mLF->layer()->arrangement() )
2200 {
2202 {
2203 double cx, cy;
2204 getCentroid( cx, cy, mLF->layer()->centroidInside() );
2205 if ( qgsDoubleNear( mLF->distLabel(), 0.0 ) )
2206 created += createCandidateCenteredOverPoint( cx, cy, lPos, angleInRadians );
2207 created += createCandidatesAroundPoint( cx, cy, lPos, angleInRadians );
2208 break;
2209 }
2211 {
2212 double cx, cy;
2213 getCentroid( cx, cy, mLF->layer()->centroidInside() );
2214 created += createCandidatesOverPoint( cx, cy, lPos, angleInRadians );
2215 break;
2216 }
2218 created += createCandidatesAlongLine( lPos, this, false, pal );
2219 break;
2221 created += createCurvedCandidatesAlongLine( lPos, this, false, pal );
2222 break;
2223 default:
2224 created += createCandidatesForPolygon( lPos, this, pal );
2225 break;
2226 }
2227 }
2228
2229 if ( allowOutside )
2230 {
2231 // add fallback for labels outside the polygon
2233
2234 if ( created > 0 )
2235 {
2236 // TODO (maybe) increase cost for outside placements (i.e. positions at indices >= created)?
2237 // From my initial testing this doesn't seem necessary
2238 }
2239 }
2240 }
2241 }
2242 }
2243 }
2244
2245 return lPos;
2246}
2247
2248void FeaturePart::addSizePenalty( std::vector< std::unique_ptr< LabelPosition > > &lPos, double bbx[4], double bby[4] ) const
2249{
2250 if ( !mGeos )
2252
2253 GEOSContextHandle_t ctxt = QgsGeos::getGEOSHandler();
2254 int geomType = GEOSGeomTypeId_r( ctxt, mGeos );
2255
2256 double sizeCost = 0;
2257 if ( geomType == GEOS_LINESTRING )
2258 {
2259 const double l = length();
2260 if ( l <= 0 )
2261 return; // failed to calculate length
2262 double bbox_length = std::max( bbx[2] - bbx[0], bby[2] - bby[0] );
2263 if ( l >= bbox_length / 4 )
2264 return; // the line is longer than quarter of height or width - don't penalize it
2265
2266 sizeCost = 1 - ( l / ( bbox_length / 4 ) ); // < 0,1 >
2267 }
2268 else if ( geomType == GEOS_POLYGON )
2269 {
2270 const double a = area();
2271 if ( a <= 0 )
2272 return;
2273 double bbox_area = ( bbx[2] - bbx[0] ) * ( bby[2] - bby[0] );
2274 if ( a >= bbox_area / 16 )
2275 return; // covers more than 1/16 of our view - don't penalize it
2276
2277 sizeCost = 1 - ( a / ( bbox_area / 16 ) ); // < 0, 1 >
2278 }
2279 else
2280 return; // no size penalty for points
2281
2282// apply the penalty
2283 for ( std::unique_ptr< LabelPosition > &pos : lPos )
2284 {
2285 pos->setCost( pos->cost() + sizeCost / 100 );
2286 }
2287}
2288
2290{
2291 if ( !nbPoints || !p2->nbPoints )
2292 return false;
2293
2294 // here we only care if the lines start or end at the other line -- we don't want to test
2295 // touches as that is true for "T" type joins!
2296 const double x1first = x.front();
2297 const double x1last = x.back();
2298 const double x2first = p2->x.front();
2299 const double x2last = p2->x.back();
2300 const double y1first = y.front();
2301 const double y1last = y.back();
2302 const double y2first = p2->y.front();
2303 const double y2last = p2->y.back();
2304
2305 const bool p2startTouches = ( qgsDoubleNear( x1first, x2first ) && qgsDoubleNear( y1first, y2first ) )
2306 || ( qgsDoubleNear( x1last, x2first ) && qgsDoubleNear( y1last, y2first ) );
2307
2308 const bool p2endTouches = ( qgsDoubleNear( x1first, x2last ) && qgsDoubleNear( y1first, y2last ) )
2309 || ( qgsDoubleNear( x1last, x2last ) && qgsDoubleNear( y1last, y2last ) );
2310 // only one endpoint can touch, not both
2311 if ( ( !p2startTouches && !p2endTouches ) || ( p2startTouches && p2endTouches ) )
2312 return false;
2313
2314 // now we know that we have one line endpoint touching only, but there's still a chance
2315 // that the other side of p2 may touch the original line NOT at the other endpoint
2316 // so we need to check that this point doesn't intersect
2317 const double p2otherX = p2startTouches ? x2last : x2first;
2318 const double p2otherY = p2startTouches ? y2last : y2first;
2319
2320 GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler();
2321
2322 GEOSCoordSequence *coord = GEOSCoordSeq_create_r( geosctxt, 1, 2 );
2323 GEOSCoordSeq_setXY_r( geosctxt, coord, 0, p2otherX, p2otherY );
2324
2325 geos::unique_ptr p2OtherEnd( GEOSGeom_createPoint_r( geosctxt, coord ) );
2326 try
2327 {
2328 return ( GEOSPreparedIntersects_r( geosctxt, preparedGeom(), p2OtherEnd.get() ) != 1 );
2329 }
2330 catch ( GEOSException &e )
2331 {
2332 qWarning( "GEOS exception: %s", e.what() );
2333 QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
2334 return false;
2335 }
2336}
2337
2339{
2340 if ( !mGeos )
2342 if ( !other->mGeos )
2343 other->createGeosGeom();
2344
2345 GEOSContextHandle_t ctxt = QgsGeos::getGEOSHandler();
2346 try
2347 {
2348 GEOSGeometry *g1 = GEOSGeom_clone_r( ctxt, mGeos );
2349 GEOSGeometry *g2 = GEOSGeom_clone_r( ctxt, other->mGeos );
2350 GEOSGeometry *geoms[2] = { g1, g2 };
2351 geos::unique_ptr g( GEOSGeom_createCollection_r( ctxt, GEOS_MULTILINESTRING, geoms, 2 ) );
2352 geos::unique_ptr gTmp( GEOSLineMerge_r( ctxt, g.get() ) );
2353
2354 if ( GEOSGeomTypeId_r( ctxt, gTmp.get() ) != GEOS_LINESTRING )
2355 {
2356 // sometimes it's not possible to merge lines (e.g. they don't touch at endpoints)
2357 return false;
2358 }
2360
2361 // set up new geometry
2362 mGeos = gTmp.release();
2363 mOwnsGeom = true;
2364
2365 deleteCoords();
2366 qDeleteAll( mHoles );
2367 mHoles.clear();
2369 return true;
2370 }
2371 catch ( GEOSException &e )
2372 {
2373 qWarning( "GEOS exception: %s", e.what() );
2374 QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
2375 return false;
2376 }
2377}
2378
2380{
2381 if ( mLF->alwaysShow() )
2382 {
2383 //if feature is set to always show, bump the priority up by orders of magnitude
2384 //so that other feature's labels are unlikely to be placed over the label for this feature
2385 //(negative numbers due to how pal::extract calculates inactive cost)
2386 return -0.2;
2387 }
2388
2389 return mLF->priority() >= 0 ? mLF->priority() : mLF->layer()->priority();
2390}
2391
2393{
2394 bool result = false;
2395
2396 switch ( mLF->layer()->upsidedownLabels() )
2397 {
2399 result = true;
2400 break;
2402 // upright only dynamic labels
2403 if ( !hasFixedRotation() || ( !hasFixedPosition() && fixedAngle() == 0.0 ) )
2404 {
2405 result = true;
2406 }
2407 break;
2409 break;
2410 }
2411 return result;
2412}
@ FromSymbolBounds
Offset distance applies from rendered symbol bounds.
@ OverPoint
Arranges candidates over a point (or centroid of a polygon), or at a preset offset from the point....
@ AroundPoint
Arranges candidates in a circle around a point (or centroid of a polygon). Applies to point or polygo...
@ Line
Arranges candidates parallel to a generalised line representing the feature or parallel to a polygon'...
@ Free
Arranges candidates scattered throughout a polygon feature. Candidates are rotated to respect the pol...
@ OrderedPositionsAroundPoint
Candidates are placed in predefined positions around a point. Preference is given to positions with g...
@ Horizontal
Arranges horizontal candidates scattered throughout a polygon feature. Applies to polygon layers only...
@ PerimeterCurved
Arranges candidates following the curvature of a polygon's boundary. Applies to polygon layers only.
@ OutsidePolygons
Candidates are placed outside of polygon boundaries. Applies to polygon layers only....
LabelPredefinedPointPosition
Positions for labels when using the Qgis::LabelPlacement::OrderedPositionsAroundPoint placement mode.
Definition qgis.h:583
@ MiddleLeft
Label on left of point.
@ TopRight
Label on top-right of point.
@ MiddleRight
Label on right of point.
@ TopSlightlyRight
Label on top of point, slightly right of center.
@ TopMiddle
Label directly above point.
@ BottomSlightlyLeft
Label below point, slightly left of center.
@ BottomRight
Label on bottom right of point.
@ BottomLeft
Label on bottom-left of point.
@ BottomSlightlyRight
Label below point, slightly right of center.
@ TopLeft
Label on top-left of point.
@ BottomMiddle
Label directly below point.
@ TopSlightlyLeft
Label on top of point, slightly left of center.
@ FlipUpsideDownLabels
Upside-down labels (90 <= angle < 270) are shown upright.
@ AlwaysAllowUpsideDown
Show upside down for all labels, including dynamic ones.
@ AllowUpsideDownWhenRotationIsDefined
Show upside down when rotation is layer- or data-defined.
const QgsCurve * exteriorRing() const
Returns the curve polygon's exterior ring.
QgsGeometry geometry
Definition qgsfeature.h:67
bool hasNext() const
Find out whether there are more parts.
static double normalizedAngle(double angle)
Ensures that an angle is in the range 0 <= angle < 2 pi.
A geometry is the spatial representation of a feature.
QgsGeometryConstPartIterator constParts() const
Returns Java-style iterator for traversal of parts of the geometry.
QgsRectangle boundingBox() const
Returns the bounding box of the geometry.
static std::unique_ptr< QgsAbstractGeometry > fromGeos(const GEOSGeometry *geos)
Create a geometry from a GEOSGeometry.
Definition qgsgeos.cpp:1402
static GEOSContextHandle_t getGEOSHandler()
Definition qgsgeos.cpp:3464
The QgsLabelFeature class describes a feature that should be used within the labeling engine.
double overrunSmoothDistance() const
Returns the distance (in map units) with which the ends of linear features are averaged over when cal...
double fixedAngle() const
Angle in radians of the fixed angle (relevant only if hasFixedAngle() returns true)
const QSizeF & symbolSize() const
Returns the size of the rendered symbol associated with this feature, if applicable.
QVector< Qgis::LabelPredefinedPointPosition > predefinedPositionOrder() const
Returns the priority ordered list of predefined positions for label candidates.
QgsLabeling::PolygonPlacementFlags polygonPlacementFlags() const
Returns the polygon placement flags, which dictate how polygon labels can be placed.
QgsPointXY positionOffset() const
Applies only to "offset from point" placement strategy.
bool hasFixedQuadrant() const
Returns whether the quadrant for the label is fixed.
bool hasFixedAngle() const
Whether the label should use a fixed angle instead of using angle from automatic placement.
pal::Layer * layer() const
Gets PAL layer of the label feature. Should be only used internally in PAL.
QgsLabeling::LinePlacementFlags arrangementFlags() const
Returns the feature's arrangement flags.
bool alwaysShow() const
Whether label should be always shown (sets very high label priority)
double lineAnchorPercent() const
Returns the percent along the line at which labels should be placed, for line labels only.
const GEOSPreparedGeometry * permissibleZonePrepared() const
Returns a GEOS prepared geometry representing the label's permissibleZone().
QgsLabelLineSettings::AnchorType lineAnchorType() const
Returns the line anchor type, which dictates how the lineAnchorPercent() setting is handled.
double distLabel() const
Applies to "around point" placement strategy or linestring features.
GEOSGeometry * geometry() const
Gets access to the associated geometry.
QPointF quadOffset() const
Applies to "offset from point" placement strategy and "around point" (in case hasFixedQuadrant() retu...
void setAnchorPosition(const QgsPointXY &anchorPosition)
In case of quadrand or aligned positioning, this is set to the anchor point.
QgsFeature feature() const
Returns the original feature associated with this label.
QgsFeatureId id() const
Identifier of the label (unique within the parent label provider)
double overrunDistance() const
Returns the permissible distance (in map units) which labels are allowed to overrun the start or end ...
double priority() const
Returns the feature's labeling priority.
QgsGeometry permissibleZone() const
Returns the label's permissible zone geometry.
bool hasFixedPosition() const
Whether the label should use a fixed position instead of being automatically placed.
QgsLabelLineSettings::AnchorTextPoint lineAnchorTextPoint() const
Returns the line anchor text point, which dictates which part of the label text should be placed at t...
const QgsMargins & visualMargin() const
Returns the visual margin for the label feature.
Qgis::LabelOffsetType offsetType() const
Returns the offset type, which determines how offsets and distance to label behaves.
QgsPointXY fixedPosition() const
Coordinates of the fixed position (relevant only if hasFixedPosition() returns true)
@ Strict
Line anchor is a strict placement, and other placements are not permitted.
@ HintOnly
Line anchor is a hint for preferred placement only, but other placements close to the hint are permit...
AnchorTextPoint
Anchor point of label text.
@ EndOfText
Anchor using end of text.
@ StartOfText
Anchor using start of text.
@ CenterOfText
Anchor using center of text.
@ FollowPlacement
Automatically set the anchor point based on the lineAnchorPercent() value. Values <25% will use the s...
Contains constants and enums relating to labeling.
Definition qgslabeling.h:32
@ AboveLine
Labels can be placed above a line feature. Unless MapOrientation is also specified this mode respects...
Definition qgslabeling.h:41
@ OnLine
Labels can be placed directly over a line feature.
Definition qgslabeling.h:40
@ MapOrientation
Signifies that the AboveLine and BelowLine flags should respect the map's orientation rather than the...
Definition qgslabeling.h:43
@ BelowLine
Labels can be placed below a line feature. Unless MapOrientation is also specified this mode respects...
Definition qgslabeling.h:42
@ AllowPlacementInsideOfPolygon
Labels can be placed inside a polygon feature.
Definition qgslabeling.h:55
@ AllowPlacementOutsideOfPolygon
Labels can be placed outside of a polygon feature.
Definition qgslabeling.h:54
Line string geometry type, with support for z-dimension and m-values.
double length() const override
Returns the planar, 2-dimensional length of the geometry.
void visitPointsByRegularDistance(double distance, const std::function< bool(double x, double y, double z, double m, double startSegmentX, double startSegmentY, double startSegmentZ, double startSegmentM, double endSegmentX, double endSegmentY, double endSegmentZ, double endSegmentM) > &visitPoint) const
Visits regular points along the linestring, spaced by distance.
The QgsMargins class defines the four margins of a rectangle.
Definition qgsmargins.h:38
double top() const
Returns the top margin.
Definition qgsmargins.h:78
double right() const
Returns the right margin.
Definition qgsmargins.h:84
double bottom() const
Returns the bottom margin.
Definition qgsmargins.h:90
double left() const
Returns the left margin.
Definition qgsmargins.h:72
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
A class to represent a 2D point.
Definition qgspointxy.h:59
double y
Definition qgspointxy.h:63
double x
Definition qgspointxy.h:62
Polygon geometry type.
Definition qgspolygon.h:34
Contains precalculated properties regarding text metrics for text to be renderered at a later stage.
int count() const
Returns the total number of characters.
double characterWidth(int position) const
Returns the width of the character at the specified position.
double characterHeight(int position) const
Returns the character height of the character at the specified position (actually font metrics height...
double width() const
Returns the width of the rectangle.
double height() const
Returns the height of the rectangle.
LabelLineDirection
Controls behavior of curved text with respect to line directions.
@ FollowLineDirection
Curved text placement will respect the line direction and ignore painter orientation.
@ RespectPainterOrientation
Curved text will be placed respecting the painter orientation, and the actual line direction will be ...
static CurvePlacementProperties * generateCurvedTextPlacement(const QgsPrecalculatedTextMetrics &metrics, const double *x, const double *y, int numPoints, const std::vector< double > &pathDistances, double offsetAlongLine, LabelLineDirection direction=RespectPainterOrientation, double maxConcaveAngle=-1, double maxConvexAngle=-1, bool uprightOnly=true)
Calculates curved text placement properties.
Main class to handle feature.
Definition feature.h:65
FeaturePart(QgsLabelFeature *lf, const GEOSGeometry *geom)
Creates a new generic feature.
Definition feature.cpp:53
std::size_t createCandidatesAroundPoint(double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle)
Generate candidates for point feature, located around a specified point.
Definition feature.cpp:593
std::size_t createCandidatesOutsidePolygon(std::vector< std::unique_ptr< LabelPosition > > &lPos, Pal *pal)
Generate candidates outside of polygon features.
Definition feature.cpp:1940
bool hasFixedRotation() const
Returns true if the feature's label has a fixed rotation.
Definition feature.h:281
double getLabelHeight(double angle=0.0) const
Returns the height of the label, optionally taking an angle (in radians) into account.
Definition feature.h:272
QList< FeaturePart * > mHoles
Definition feature.h:348
double getLabelDistance() const
Returns the distance from the anchor point to the label.
Definition feature.h:278
~FeaturePart() override
Deletes the feature.
Definition feature.cpp:81
bool hasFixedPosition() const
Returns true if the feature's label has a fixed position.
Definition feature.h:287
std::size_t createCandidatesForPolygon(std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, Pal *pal)
Generate candidates for polygon features.
Definition feature.cpp:1697
void setTotalRepeats(int repeats)
Returns the total number of repeating labels associated with this label.
Definition feature.cpp:291
std::size_t maximumPolygonCandidates() const
Returns the maximum number of polygon candidates to generate for this feature.
Definition feature.cpp:196
QgsFeatureId featureId() const
Returns the unique ID of the feature.
Definition feature.cpp:164
std::size_t createCandidatesAlongLineNearStraightSegments(std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, Pal *pal)
Generate candidates for line feature, by trying to place candidates towards the middle of the longest...
Definition feature.cpp:869
bool hasSameLabelFeatureAs(FeaturePart *part) const
Tests whether this feature part belongs to the same QgsLabelFeature as another feature part.
Definition feature.cpp:218
double fixedAngle() const
Returns the fixed angle for the feature's label.
Definition feature.h:284
std::size_t maximumLineCandidates() const
Returns the maximum number of line candidates to generate for this feature.
Definition feature.cpp:174
std::size_t createHorizontalCandidatesAlongLine(std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, Pal *pal)
Generate horizontal candidates for line feature.
Definition feature.cpp:780
std::size_t createCandidatesAlongLine(std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, bool allowOverrun, Pal *pal)
Generate candidates for line feature.
Definition feature.cpp:751
bool mergeWithFeaturePart(FeaturePart *other)
Merge other (connected) part with this one and save the result in this part (other is unchanged).
Definition feature.cpp:2338
std::size_t createCurvedCandidatesAlongLine(std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, bool allowOverrun, Pal *pal)
Generate curved candidates for line features.
Definition feature.cpp:1365
bool onlyShowUprightLabels() const
Returns true if feature's label must be displayed upright.
Definition feature.cpp:2392
std::size_t createCandidatesOverPoint(double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle)
Generate one candidate over or offset the specified point.
Definition feature.cpp:325
std::unique_ptr< LabelPosition > createCandidatePointOnSurface(PointSet *mapShape)
Creates a single candidate using the "point on sruface" algorithm.
Definition feature.cpp:404
QgsLabelFeature * mLF
Definition feature.h:347
double getLabelWidth(double angle=0.0) const
Returns the width of the label, optionally taking an angle (in radians) into account.
Definition feature.h:267
QgsLabelFeature * feature()
Returns the parent feature.
Definition feature.h:94
std::unique_ptr< LabelPosition > curvedPlacementAtOffset(PointSet *mapShape, const std::vector< double > &pathDistances, QgsTextRendererUtils::LabelLineDirection direction, double distance, bool &labeledLineSegmentIsRightToLeft, bool applyAngleConstraints, bool uprightOnly)
Returns the label position for a curved label at a specific offset along a path.
Definition feature.cpp:1328
std::vector< std::unique_ptr< LabelPosition > > createCandidates(Pal *pal)
Generates a list of candidate positions for labels for this feature.
Definition feature.cpp:2143
bool isConnected(FeaturePart *p2)
Check whether this part is connected with some other part.
Definition feature.cpp:2289
Layer * layer()
Returns the layer that feature belongs to.
Definition feature.cpp:159
PathOffset
Path offset variances used in curved placement.
Definition feature.h:71
int totalRepeats() const
Returns the total number of repeating labels associated with this label.
Definition feature.cpp:286
std::size_t createCandidatesAlongLineNearMidpoint(std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, double initialCost=0.0, Pal *pal=nullptr)
Generate candidates for line feature, by trying to place candidates as close as possible to the line'...
Definition feature.cpp:1122
void addSizePenalty(std::vector< std::unique_ptr< LabelPosition > > &lPos, double bbx[4], double bby[4]) const
Increases the cost of the label candidates for this feature, based on the size of the feature.
Definition feature.cpp:2248
void extractCoords(const GEOSGeometry *geom)
read coordinates from a GEOS geom
Definition feature.cpp:89
double calculatePriority() const
Calculates the priority for the feature.
Definition feature.cpp:2379
std::size_t createCandidatesAtOrderedPositionsOverPoint(double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle)
Generates candidates following a prioritized list of predefined positions around a point.
Definition feature.cpp:538
std::size_t createCandidateCenteredOverPoint(double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle)
Generate one candidate centered over the specified point.
Definition feature.cpp:296
std::size_t maximumPointCandidates() const
Returns the maximum number of point candidates to generate for this feature.
Definition feature.cpp:169
static bool reorderPolygon(std::vector< double > &x, std::vector< double > &y)
Reorder points to have cross prod ((x,y)[i], (x,y)[i+1), point) > 0 when point is outside.
static double dist_euc2d(double x1, double y1, double x2, double y2)
static bool containsCandidate(const GEOSPreparedGeometry *geom, double x, double y, double width, double height, double alpha)
Returns true if a GEOS prepared geometry totally contains a label candidate.
LabelPosition is a candidate feature label position.
double getAlpha() const
Returns the angle to rotate text (in radians).
Quadrant
Position of label candidate relative to feature.
double getHeight() const
void setNextPart(std::unique_ptr< LabelPosition > next)
Sets the next part of this label position (i.e.
double getWidth() const
double getX(int i=0) const
Returns the down-left x coordinate.
double getY(int i=0) const
Returns the down-left y coordinate.
LabelPosition * nextPart() const
Returns the next part of this label position (i.e.
A set of features which influence the labeling process.
Definition layer.h:63
QString name() const
Returns the layer's name.
Definition layer.h:162
std::size_t maximumPolygonLabelCandidates() const
Returns the maximum number of polygon label candidates to generate for features in this layer.
Definition layer.h:139
Pal * mPal
Definition layer.h:323
int connectedFeatureId(QgsFeatureId featureId) const
Returns the connected feature ID for a label feature ID, which is unique for all features which have ...
Definition layer.cpp:360
Qgis::LabelPlacement arrangement() const
Returns the layer's arrangement policy.
Definition layer.h:168
std::size_t maximumPointLabelCandidates() const
Returns the maximum number of point label candidates to generate for features in this layer.
Definition layer.h:97
Qgis::UpsideDownLabelHandling upsidedownLabels() const
Returns how upside down labels are handled within the layer.
Definition layer.h:269
bool centroidInside() const
Returns whether labels placed at the centroid of features within the layer are forced to be placed in...
Definition layer.h:285
bool isCurved() const
Returns true if the layer has curved labels.
Definition layer.h:173
double priority() const
Returns the layer's priority, between 0 and 1.
Definition layer.h:243
std::size_t maximumLineLabelCandidates() const
Returns the maximum number of line label candidates to generate for features in this layer.
Definition layer.h:118
Main Pal labeling class.
Definition pal.h:80
double maximumLineCandidatesPerMapUnit() const
Returns the maximum number of line label candidate positions per map unit.
Definition pal.h:170
double maximumPolygonCandidatesPerMapUnitSquared() const
Returns the maximum number of polygon label candidate positions per map unit squared.
Definition pal.h:184
The underlying raw pal geometry class.
Definition pointset.h:77
geos::unique_ptr interpolatePoint(double distance) const
Returns a GEOS geometry representing the point interpolated on the shape by distance.
std::unique_ptr< PointSet > clone() const
Returns a copy of the point set.
Definition pointset.cpp:265
double lineLocatePoint(const GEOSGeometry *point) const
Returns the distance along the geometry closest to the specified GEOS point.
OrientedConvexHullBoundingBox computeConvexHullOrientedBoundingBox(bool &ok)
Computes an oriented bounding box for the shape's convex hull.
Definition pointset.cpp:718
double length() const
Returns length of line geometry.
void deleteCoords()
Definition pointset.cpp:232
double ymax
Definition pointset.h:261
double ymin
Definition pointset.h:260
double area() const
Returns area of polygon geometry.
bool isClosed() const
Returns true if pointset is closed.
PointSet * holeOf
Definition pointset.h:241
void createGeosGeom() const
Definition pointset.cpp:99
void getPointByDistance(double *d, double *ad, double dl, double *px, double *py) const
Gets a point a set distance along a line geometry.
Definition pointset.cpp:975
std::vector< double > y
Definition pointset.h:231
void getCentroid(double &px, double &py, bool forceInside=false) const
Definition pointset.cpp:917
std::vector< double > x
Definition pointset.h:230
const GEOSPreparedGeometry * preparedGeom() const
Definition pointset.cpp:154
GEOSGeometry * mGeos
Definition pointset.h:234
double xmin
Definition pointset.h:258
const GEOSGeometry * geos() const
Returns the point set's GEOS geometry.
void invalidateGeos() const
Definition pointset.cpp:166
friend class FeaturePart
Definition pointset.h:78
double xmax
Definition pointset.h:259
bool containsPoint(double x, double y) const
Tests whether point set contains a specified point.
Definition pointset.cpp:270
std::tuple< std::vector< double >, double > edgeDistances() const
Returns a vector of edge distances as well as its total length.
PointSet * parent
Definition pointset.h:242
static QLinkedList< PointSet * > splitPolygons(PointSet *inputShape, double labelWidth, double labelHeight)
Split a polygon using some random logic into some other polygons.
Definition pointset.cpp:294
void createCandidateAtOrderedPositionOverPoint(double &labelX, double &labelY, LabelPosition::Quadrant &quadrant, double x, double y, double labelWidth, double labelHeight, Qgis::LabelPredefinedPointPosition position, double distanceToLabel, const QgsMargins &visualMargin, double symbolWidthOffset, double symbolHeightOffset, double angle)
Definition feature.cpp:431
std::unique_ptr< const GEOSPreparedGeometry, GeosDeleter > prepared_unique_ptr
Scoped GEOS prepared geometry pointer.
Definition qgsgeos.h:79
std::unique_ptr< GEOSGeometry, GeosDeleter > unique_ptr
Scoped GEOS pointer.
Definition qgsgeos.h:74
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition qgis.h:2527
qint64 QgsFeatureId
64 bit feature ids negative numbers are used for uncommitted/newly added features
#define QgsDebugMsg(str)
Definition qgslogger.h:38
Represents the minimum area, oriented bounding box surrounding a convex hull.
Definition pointset.h:60