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