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