QGIS API Documentation 3.99.0-Master (c03dd32cbdd)
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 switch ( mLF->curvedLabelMode() )
1500 {
1504 return createDefaultCurvedCandidatesAlongLine( lPos, mapShape, allowOverrun, pal );
1506 return createCurvedCandidateWithCharactersAtVertices( lPos, mapShape, pal );
1507 }
1509}
1510
1511std::size_t FeaturePart::createDefaultCurvedCandidatesAlongLine( std::vector<std::unique_ptr<LabelPosition> > &lPos, PointSet *mapShape, bool allowOverrun, Pal *pal )
1512{
1513 const QgsPrecalculatedTextMetrics *li = qgis::down_cast< QgsTextLabelFeature *>( mLF )->textMetrics();
1514 const int characterCount = li->count();
1515
1516 bool stretchWordSpacingToFit = mLF->curvedLabelMode() == Qgis::CurvedLabelMode::StretchWordSpacingToFitLine;
1517 double totalCharacterWidth = 0;
1518 int spaceCount = 0;
1519 for ( int i = 0; i < characterCount; ++i )
1520 {
1521 totalCharacterWidth += li->characterWidth( i );
1522 if ( stretchWordSpacingToFit && li->grapheme( i ) == ' ' )
1523 {
1524 spaceCount++;
1525 }
1526 }
1527 if ( spaceCount == 0 )
1528 {
1529 // if no spaces in the label, disable stretch word spacing to fit mode and fallback to standard curved placement
1530 stretchWordSpacingToFit = false;
1531 }
1532
1533 const bool stretchCharacterSpacingToFit = mLF->curvedLabelMode() == Qgis::CurvedLabelMode::StretchCharacterSpacingToFitLine;
1534 const bool usingStretchToFitMode = stretchCharacterSpacingToFit || stretchWordSpacingToFit;
1535
1536 // TODO - we may need an explicit penalty for overhanging labels. Currently, they are penalized just because they
1537 // are further from the line center, so non-overhanging placements are picked where possible.
1538
1539 std::unique_ptr< PointSet > expanded;
1540 double shapeLength = mapShape->length();
1541
1542 // in stretch modes we force allowOverrun to false, as we fit the text exactly
1543 // to the actual line length
1544 if ( totalRepeats() > 1 || usingStretchToFitMode )
1545 allowOverrun = false;
1546
1547 geos::unique_ptr originalPoint;
1548 if ( !usingStretchToFitMode )
1549 {
1550 // unless in strict mode, label overrun should NEVER exceed the label length (or labels would sit off in space).
1551 // in fact, let's require that a minimum of 5% of the label text has to sit on the feature,
1552 // as we don't want a label sitting right at the start or end corner of a line
1553 double overrun = 0;
1554 switch ( mLF->lineAnchorType() )
1555 {
1557 overrun = std::min( mLF->overrunDistance(), totalCharacterWidth * 0.95 );
1558 break;
1560 // in strict mode, we force sufficient overrun to ensure label will always "fit", even if it's placed
1561 // 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
1562 overrun = std::max( mLF->overrunDistance(), totalCharacterWidth * 1.05 );
1563 break;
1564 }
1565
1566 if ( totalCharacterWidth > shapeLength )
1567 {
1568 if ( !allowOverrun || shapeLength < totalCharacterWidth - 2 * overrun )
1569 {
1570 // label doesn't fit on this line, don't waste time trying to make candidates
1571 return 0;
1572 }
1573 }
1574
1575 // calculate the anchor point for the original line shape as a GEOS point.
1576 // this must be done BEFORE we account for overrun by extending the shape!
1577 originalPoint = mapShape->interpolatePoint( shapeLength * mLF->lineAnchorPercent() );
1578
1579 if ( allowOverrun && overrun > 0 )
1580 {
1581 // expand out line on either side to fit label
1582 expanded = mapShape->clone();
1583 expanded->extendLineByDistance( overrun, overrun, mLF->overrunSmoothDistance() );
1584 mapShape = expanded.get();
1585 shapeLength += 2 * overrun;
1586 }
1587 }
1588
1589 Qgis::LabelLinePlacementFlags flags = mLF->arrangementFlags();
1590 if ( flags == 0 )
1591 flags = Qgis::LabelLinePlacementFlag::OnLine; // default flag
1592 const bool hasAboveBelowLinePlacement = flags & Qgis::LabelLinePlacementFlag::AboveLine || flags & Qgis::LabelLinePlacementFlag::BelowLine;
1593 const double offsetDistance = mLF->distLabel() + li->characterHeight( 0 ) / 2;
1594 std::unique_ptr< PointSet > mapShapeOffsetPositive;
1595 bool positiveShapeHasNegativeDistance = false;
1596 std::unique_ptr< PointSet > mapShapeOffsetNegative;
1597 bool negativeShapeHasNegativeDistance = false;
1598 if ( hasAboveBelowLinePlacement && !qgsDoubleNear( offsetDistance, 0 ) )
1599 {
1600 // create offsetted map shapes to be used for above and below line placements
1602 mapShapeOffsetPositive = mapShape->clone();
1604 mapShapeOffsetNegative = mapShape->clone();
1605 if ( offsetDistance >= 0.0 || !( flags & Qgis::LabelLinePlacementFlag::MapOrientation ) )
1606 {
1607 if ( mapShapeOffsetPositive )
1608 mapShapeOffsetPositive->offsetCurveByDistance( offsetDistance );
1609 positiveShapeHasNegativeDistance = offsetDistance < 0;
1610 if ( mapShapeOffsetNegative )
1611 mapShapeOffsetNegative->offsetCurveByDistance( offsetDistance * -1 );
1612 negativeShapeHasNegativeDistance = offsetDistance > 0;
1613 }
1614 else
1615 {
1616 // In case of a negative offset distance, above line placement switch to below line and vice versa
1619 {
1620 flags &= ~static_cast< int >( Qgis::LabelLinePlacementFlag::AboveLine );
1622 }
1625 {
1626 flags &= ~static_cast< int >( Qgis::LabelLinePlacementFlag::BelowLine );
1628 }
1629 if ( mapShapeOffsetPositive )
1630 mapShapeOffsetPositive->offsetCurveByDistance( offsetDistance * -1 );
1631 positiveShapeHasNegativeDistance = offsetDistance > 0;
1632 if ( mapShapeOffsetNegative )
1633 mapShapeOffsetNegative->offsetCurveByDistance( offsetDistance );
1634 negativeShapeHasNegativeDistance = offsetDistance < 0;
1635 }
1636 }
1637
1638 const QgsLabelLineSettings::AnchorTextPoint textPoint = mLF->lineAnchorTextPoint();
1639
1640 std::vector< std::unique_ptr< LabelPosition >> positions;
1641 std::unique_ptr< LabelPosition > backupPlacement;
1642 for ( PathOffset offset : { PositiveOffset, NoOffset, NegativeOffset } )
1643 {
1644 PointSet *currentMapShape = nullptr;
1645 if ( offset == PositiveOffset && hasAboveBelowLinePlacement )
1646 {
1647 currentMapShape = mapShapeOffsetPositive.get();
1648 }
1649 if ( offset == NoOffset && flags & Qgis::LabelLinePlacementFlag::OnLine )
1650 {
1651 currentMapShape = mapShape;
1652 }
1653 if ( offset == NegativeOffset && hasAboveBelowLinePlacement )
1654 {
1655 currentMapShape = mapShapeOffsetNegative.get();
1656 }
1657 if ( !currentMapShape )
1658 continue;
1659
1660 // distance calculation
1661 const auto [ pathDistances, totalDistance ] = currentMapShape->edgeDistances();
1662 if ( qgsDoubleNear( totalDistance, 0.0 ) )
1663 continue;
1664
1665 double lineAnchorPoint = 0;
1666 if ( !usingStretchToFitMode )
1667 {
1668 if ( originalPoint )
1669 {
1670 // the actual anchor point for the offset curves is the closest point on those offset curves
1671 // to the anchor point on the original line. This avoids anchor points which differ greatly
1672 // on the positive/negative offset lines due to line curvature.
1673 lineAnchorPoint = currentMapShape->lineLocatePoint( originalPoint.get() );
1674 }
1675 else
1676 {
1677 lineAnchorPoint = totalDistance * mLF->lineAnchorPercent();
1678 if ( offset == NegativeOffset )
1679 lineAnchorPoint = totalDistance - lineAnchorPoint;
1680 }
1681 }
1682
1683 if ( pal->isCanceled() )
1684 return 0;
1685
1686 const std::size_t candidateTargetCount = maximumLineCandidates();
1687 double delta = std::max( li->characterHeight( 0 ) / 6, totalDistance / candidateTargetCount );
1688
1689 // generate curved labels
1690 double distanceAlongLineToStartCandidate = 0;
1691 bool singleCandidateOnly = false;
1692 double additionalCharacterSpacing = 0.0;
1693 double additionalWordSpacing = 0.0;
1694 if ( usingStretchToFitMode )
1695 {
1696 // calculate required expansion/compression of spacing
1697 double extraSpace = totalDistance - totalCharacterWidth;
1698
1699 // add a little bit of additional tolerance -- if we try to aim EXACTLY
1700 // for the end of the line, then we risk precision issues pushing us PAST
1701 // the end of the line and the string being truncated
1702 if ( extraSpace > 0 )
1703 extraSpace *= 0.995;
1704 else
1705 extraSpace *= 1.005;
1706
1707 if ( stretchWordSpacingToFit )
1708 {
1709 if ( spaceCount > 0 )
1710 additionalWordSpacing = extraSpace / spaceCount;
1711 else
1712 continue; // cannot stretch a single word
1713 }
1714 else
1715 {
1716 if ( characterCount > 1 )
1717 additionalCharacterSpacing = extraSpace / ( characterCount - 1 );
1718 }
1719
1720 // force a single candidate covering the whole line starting at 0
1721 distanceAlongLineToStartCandidate = 0;
1722 delta = totalDistance + 1.0; // (ensure loop runs exactly once)
1723 singleCandidateOnly = true;
1724 }
1725 else
1726 {
1727 switch ( mLF->lineAnchorType() )
1728 {
1730 break;
1731
1733 switch ( textPoint )
1734 {
1736 distanceAlongLineToStartCandidate = std::clamp( lineAnchorPoint, 0.0, totalDistance * 0.999 );
1737 break;
1739 distanceAlongLineToStartCandidate = std::clamp( lineAnchorPoint - getLabelWidth() / 2, 0.0, totalDistance * 0.999 - getLabelWidth() / 2 );
1740 break;
1742 distanceAlongLineToStartCandidate = std::clamp( lineAnchorPoint - getLabelWidth(), 0.0, totalDistance * 0.999 - getLabelWidth() ) ;
1743 break;
1745 // not possible here
1746 break;
1747 }
1748 singleCandidateOnly = true;
1749 break;
1750 }
1751 }
1752
1753 bool hasTestedFirstPlacement = false;
1754 for ( ; distanceAlongLineToStartCandidate <= totalDistance; distanceAlongLineToStartCandidate += delta )
1755 {
1756 if ( singleCandidateOnly && hasTestedFirstPlacement )
1757 break;
1758
1759 if ( pal->isCanceled() )
1760 return 0;
1761
1762 hasTestedFirstPlacement = true;
1763 // placements may need to be reversed if using map orientation and the line has right-to-left direction
1764 bool labeledLineSegmentIsRightToLeft = false;
1766 Qgis::CurvedTextFlags curvedTextFlags;
1767 if ( onlyShowUprightLabels() && ( !singleCandidateOnly || !( flags & Qgis::LabelLinePlacementFlag::MapOrientation ) ) )
1769
1770 std::unique_ptr< LabelPosition > labelPosition = curvedPlacementAtOffset( currentMapShape, pathDistances, direction, distanceAlongLineToStartCandidate, labeledLineSegmentIsRightToLeft, !singleCandidateOnly, curvedTextFlags, additionalCharacterSpacing, additionalWordSpacing );
1771 if ( !labelPosition )
1772 {
1773 continue;
1774 }
1775
1776
1777 bool isBackupPlacementOnly = false;
1779 {
1780 if ( ( currentMapShape == mapShapeOffsetPositive.get() && positiveShapeHasNegativeDistance )
1781 || ( currentMapShape == mapShapeOffsetNegative.get() && negativeShapeHasNegativeDistance ) )
1782 {
1783 labeledLineSegmentIsRightToLeft = !labeledLineSegmentIsRightToLeft;
1784 }
1785
1786 if ( ( offset != NoOffset ) && !labeledLineSegmentIsRightToLeft && !( flags & Qgis::LabelLinePlacementFlag::AboveLine ) )
1787 {
1788 if ( singleCandidateOnly && offset == PositiveOffset )
1789 isBackupPlacementOnly = true;
1790 else
1791 continue;
1792 }
1793 if ( ( offset != NoOffset ) && labeledLineSegmentIsRightToLeft && !( flags & Qgis::LabelLinePlacementFlag::BelowLine ) )
1794 {
1795 if ( singleCandidateOnly && offset == PositiveOffset )
1796 isBackupPlacementOnly = true;
1797 else
1798 continue;
1799 }
1800 }
1801
1802 backupPlacement.reset();
1803
1804 // evaluate cost
1805 const double angleDiff = labelPosition->angleDifferential();
1806 const double angleDiffAvg = characterCount > 1 ? ( angleDiff / ( characterCount - 1 ) ) : 0; // <0, pi> but pi/8 is much already
1807
1808 // if anchor placement is towards start or end of line, we need to slightly tweak the costs to ensure that the
1809 // anchor weighting is sufficient to push labels towards start/end
1810 const bool anchorIsFlexiblePlacement = !singleCandidateOnly && mLF->lineAnchorPercent() > 0.1 && mLF->lineAnchorPercent() < 0.9;
1811 double cost = angleDiffAvg / 100; // <0, 0.031 > but usually <0, 0.003 >
1812 if ( cost < 0.0001 )
1813 cost = 0.0001;
1814
1815 // for stretch-to-fit modes we ignore anchor distance cost as we always fit the whole line
1816 if ( !usingStretchToFitMode )
1817 {
1818 // penalize positions which are further from the line's anchor point
1819 double labelTextAnchor = 0;
1820 switch ( textPoint )
1821 {
1823 labelTextAnchor = distanceAlongLineToStartCandidate;
1824 break;
1826 labelTextAnchor = distanceAlongLineToStartCandidate + getLabelWidth() / 2;
1827 break;
1829 labelTextAnchor = distanceAlongLineToStartCandidate + getLabelWidth();
1830 break;
1832 // not possible here
1833 break;
1834 }
1835 double costCenter = std::fabs( lineAnchorPoint - labelTextAnchor ) / totalDistance; // <0, 0.5>
1836 cost += costCenter / ( anchorIsFlexiblePlacement ? 100 : 10 ); // < 0, 0.005 >, or <0, 0.05> if preferring placement close to start/end of line
1837 }
1838
1839 const bool isBelow = ( offset != NoOffset ) && labeledLineSegmentIsRightToLeft;
1840 if ( isBelow )
1841 {
1842 // add additional cost for on line placement
1843 cost += 0.001;
1844 }
1845 else if ( offset == NoOffset )
1846 {
1847 // add additional cost for below line placement
1848 cost += 0.002;
1849 }
1850
1851 labelPosition->setCost( cost );
1852
1853 auto p = std::make_unique< LabelPosition >( *labelPosition );
1854 if ( p && mLF->permissibleZonePrepared() )
1855 {
1856 bool within = true;
1857 LabelPosition *currentPos = p.get();
1858 while ( within && currentPos )
1859 {
1860 within = GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), currentPos->getX(), currentPos->getY(), currentPos->getWidth(), currentPos->getHeight(), currentPos->getAlpha() );
1861 currentPos = currentPos->nextPart();
1862 }
1863 if ( !within )
1864 {
1865 p.reset();
1866 }
1867 }
1868
1869 if ( p )
1870 {
1871 if ( isBackupPlacementOnly )
1872 backupPlacement = std::move( p );
1873 else
1874 positions.emplace_back( std::move( p ) );
1875 }
1876 }
1877 }
1878
1879 for ( std::unique_ptr< LabelPosition > &pos : positions )
1880 {
1881 lPos.emplace_back( std::move( pos ) );
1882 }
1883
1884 if ( backupPlacement )
1885 lPos.emplace_back( std::move( backupPlacement ) );
1886
1887 return positions.size();
1888}
1889
1890std::size_t FeaturePart::createCurvedCandidateWithCharactersAtVertices( std::vector<std::unique_ptr<LabelPosition> > &lPos, PointSet *mapShape, Pal *pal )
1891{
1892 const QgsPrecalculatedTextMetrics *metrics = qgis::down_cast< QgsTextLabelFeature * >( mLF )->textMetrics();
1893
1894 const int characterCount = metrics->count();
1895 const int vertexCount = mapShape->getNumPoints();
1896 if ( characterCount == 0 || vertexCount == 0 )
1897 return 0;
1898
1899 const double distLabel = mLF->distLabel();
1900
1901 std::unique_ptr< LabelPosition > firstPosition;
1902 LabelPosition *previousPosition = nullptr;
1903
1904 int vertexIndex = 0;
1905 int characterIndex = -1;
1906 for ( ; vertexIndex < vertexCount; ++vertexIndex )
1907 {
1908 if ( pal->isCanceled() )
1909 return 0;
1910
1911 bool isWhiteSpace = true;
1912 while ( isWhiteSpace )
1913 {
1914 characterIndex++;
1915 if ( characterIndex >= characterCount )
1916 break;
1917
1918 isWhiteSpace = metrics->grapheme( characterIndex ).trimmed().isEmpty() || metrics->grapheme( characterIndex ) == '\t';
1919 }
1920
1921 if ( characterIndex >= characterCount )
1922 break;
1923
1924 double x = mapShape->x[vertexIndex];
1925 double y = mapShape->y[vertexIndex];
1926
1927 // use the angle of the segment starting at the current vertex
1928 // if it is the last vertex then reuse the angle of the preceding segment
1929 double angle = 0.0;
1930 if ( vertexIndex < vertexCount - 1 )
1931 {
1932 angle = std::atan2( mapShape->y[vertexIndex + 1] - y, mapShape->x[vertexIndex + 1] - x );
1933 }
1934 else if ( vertexIndex > 0 )
1935 {
1936 angle = std::atan2( y - mapShape->y[vertexIndex - 1], x - mapShape->x[vertexIndex - 1] );
1937 }
1938 if ( !qgsDoubleNear( distLabel, 0.0 ) )
1939 {
1940 x -= std::sin( angle ) * distLabel;
1941 y += std::cos( angle ) * distLabel;
1942 }
1943
1944 const double width = metrics->characterWidth( characterIndex );
1945 const double height = metrics->characterHeight( characterIndex );
1946 auto currentPosition = std::make_unique< LabelPosition >( 0, x, y, width, height, angle, 0.0001, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over );
1947 currentPosition->setPartId( characterIndex );
1948
1949 if ( !firstPosition )
1950 {
1951 firstPosition = std::move( currentPosition );
1952 previousPosition = firstPosition.get();
1953 }
1954 else
1955 {
1956 LabelPosition *rawCurrent = currentPosition.get();
1957 previousPosition->setNextPart( std::move( currentPosition ) );
1958 previousPosition = rawCurrent;
1959 }
1960 }
1961
1962 if ( !firstPosition )
1963 return 0;
1964
1965 if ( mLF->permissibleZonePrepared() )
1966 {
1967 bool within = true;
1968 LabelPosition *currentPos = firstPosition.get();
1969 while ( within && currentPos )
1970 {
1971 within = GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), currentPos->getX(), currentPos->getY(), currentPos->getWidth(), currentPos->getHeight(), currentPos->getAlpha() );
1972 currentPos = currentPos->nextPart();
1973 }
1974 if ( !within )
1975 {
1976 return 0;
1977 }
1978 }
1979
1980 lPos.emplace_back( std::move( firstPosition ) );
1981 return 1;
1982}
1983
1984/*
1985 * seg 2
1986 * pt3 ____________pt2
1987 * ¦ ¦
1988 * ¦ ¦
1989 * seg 3 ¦ BBOX ¦ seg 1
1990 * ¦ ¦
1991 * ¦____________¦
1992 * pt0 seg 0 pt1
1993 *
1994 */
1995
1996std::size_t FeaturePart::createCandidatesForPolygon( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, Pal *pal )
1997{
1998 double labelWidth = getLabelWidth();
1999 double labelHeight = getLabelHeight();
2000
2001 const std::size_t maxPolygonCandidates = mLF->layer()->maximumPolygonLabelCandidates();
2002 const std::size_t targetPolygonCandidates = maxPolygonCandidates > 0 ? std::min( maxPolygonCandidates, static_cast< std::size_t>( std::ceil( mLF->layer()->mPal->maximumPolygonCandidatesPerMapUnitSquared() * area() ) ) )
2003 : 0;
2004
2005 const double totalArea = area();
2006
2007 mapShape->parent = nullptr;
2008
2009 if ( pal->isCanceled() )
2010 return 0;
2011
2012 QVector<PointSet *> shapes_final = splitPolygons( mapShape, labelWidth, labelHeight );
2013#if 0
2014 QgsDebugMsgLevel( u"PAL split polygons resulted in:"_s, 2 );
2015 for ( PointSet *ps : shapes_final )
2016 {
2017 QgsDebugMsgLevel( ps->toWkt(), 2 );
2018 }
2019#endif
2020
2021 std::size_t nbp = 0;
2022
2023 if ( !shapes_final.isEmpty() )
2024 {
2025 int id = 0; // ids for candidates
2026 double dlx, dly; // delta from label center and bottom-left corner
2027 double alpha = 0.0; // rotation for the label
2028 double px, py;
2029
2030 double beta;
2031 double diago = std::sqrt( labelWidth * labelWidth / 4.0 + labelHeight * labelHeight / 4 );
2032 double rx, ry;
2033 std::vector< OrientedConvexHullBoundingBox > boxes;
2034 boxes.reserve( shapes_final.size() );
2035
2036 // Compute bounding box for each finalShape
2037 while ( !shapes_final.isEmpty() )
2038 {
2039 PointSet *shape = shapes_final.takeFirst();
2040 bool ok = false;
2042 if ( ok )
2043 boxes.emplace_back( box );
2044
2045 if ( shape->parent )
2046 delete shape;
2047 }
2048
2049 if ( pal->isCanceled() )
2050 return 0;
2051
2052 double densityX = 1.0 / std::sqrt( mLF->layer()->mPal->maximumPolygonCandidatesPerMapUnitSquared() );
2053 double densityY = densityX;
2054 int numTry = 0;
2055
2056 //fit in polygon only mode slows down calculation a lot, so if it's enabled
2057 //then use a smaller limit for number of iterations
2058 int maxTry = mLF->permissibleZonePrepared() ? 7 : 10;
2059
2060 std::size_t numberCandidatesGenerated = 0;
2061
2062 do
2063 {
2064 for ( OrientedConvexHullBoundingBox &box : boxes )
2065 {
2066 // there is two possibilities here:
2067 // 1. no maximum candidates for polygon setting is in effect (i.e. maxPolygonCandidates == 0). In that case,
2068 // we base our dx/dy on the current maximumPolygonCandidatesPerMapUnitSquared value. That should give us the desired
2069 // density of candidates straight up. Easy!
2070 // 2. a maximum candidate setting IS in effect. In that case, we want to generate a good initial estimate for dx/dy
2071 // which gives us a good spatial coverage of the polygon while roughly matching the desired maximum number of candidates.
2072 // If dx/dy is too small, then too many candidates will be generated, which is both slow AND results in poor coverage of the
2073 // polygon (after culling candidates to the max number, only those clustered around the polygon's pole of inaccessibility
2074 // will remain).
2075 double dx = densityX;
2076 double dy = densityY;
2077 if ( numTry == 0 && maxPolygonCandidates > 0 )
2078 {
2079 // scale maxPolygonCandidates for just this convex hull
2080 const double boxArea = box.width * box.length;
2081 double maxThisBox = targetPolygonCandidates * boxArea / totalArea;
2082 dx = std::max( dx, std::sqrt( boxArea / maxThisBox ) * 0.8 );
2083 dy = dx;
2084 }
2085
2086 if ( pal->isCanceled() )
2087 return numberCandidatesGenerated;
2088
2089 if ( ( box.length * box.width ) > ( xmax - xmin ) * ( ymax - ymin ) * 5 )
2090 {
2091 // Very Large BBOX (should never occur)
2092 continue;
2093 }
2094
2095 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Horizontal && mLF->permissibleZonePrepared() )
2096 {
2097 //check width/height of bbox is sufficient for label
2098 if ( mLF->permissibleZone().boundingBox().width() < labelWidth ||
2099 mLF->permissibleZone().boundingBox().height() < labelHeight )
2100 {
2101 //no way label can fit in this box, skip it
2102 continue;
2103 }
2104 }
2105
2106 bool enoughPlace = false;
2107 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Free )
2108 {
2109 enoughPlace = true;
2110 px = ( box.x[0] + box.x[2] ) / 2 - labelWidth;
2111 py = ( box.y[0] + box.y[2] ) / 2 - labelHeight;
2112 int i, j;
2113
2114 // Virtual label: center on bbox center, label size = 2x original size
2115 // alpha = 0.
2116 // If all corner are in bbox then place candidates horizontaly
2117 for ( rx = px, i = 0; i < 2; rx = rx + 2 * labelWidth, i++ )
2118 {
2119 for ( ry = py, j = 0; j < 2; ry = ry + 2 * labelHeight, j++ )
2120 {
2121 if ( !mapShape->containsPoint( rx, ry ) )
2122 {
2123 enoughPlace = false;
2124 break;
2125 }
2126 }
2127 if ( !enoughPlace )
2128 {
2129 break;
2130 }
2131 }
2132
2133 } // arrangement== FREE ?
2134
2135 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Horizontal || enoughPlace )
2136 {
2137 alpha = 0.0; // HORIZ
2138 }
2139 else if ( box.length > 1.5 * labelWidth && box.width > 1.5 * labelWidth )
2140 {
2141 if ( box.alpha <= M_PI_4 )
2142 {
2143 alpha = box.alpha;
2144 }
2145 else
2146 {
2147 alpha = box.alpha - M_PI_2;
2148 }
2149 }
2150 else if ( box.length > box.width )
2151 {
2152 alpha = box.alpha - M_PI_2;
2153 }
2154 else
2155 {
2156 alpha = box.alpha;
2157 }
2158
2159 beta = std::atan2( labelHeight, labelWidth ) + alpha;
2160
2161
2162 //alpha = box->alpha;
2163
2164 // delta from label center and down-left corner
2165 dlx = std::cos( beta ) * diago;
2166 dly = std::sin( beta ) * diago;
2167
2168 double px0 = box.width / 2.0;
2169 double py0 = box.length / 2.0;
2170
2171 px0 -= std::ceil( px0 / dx ) * dx;
2172 py0 -= std::ceil( py0 / dy ) * dy;
2173
2174 for ( px = px0; px <= box.width; px += dx )
2175 {
2176 if ( pal->isCanceled() )
2177 break;
2178
2179 for ( py = py0; py <= box.length; py += dy )
2180 {
2181
2182 rx = std::cos( box.alpha ) * px + std::cos( box.alpha - M_PI_2 ) * py;
2183 ry = std::sin( box.alpha ) * px + std::sin( box.alpha - M_PI_2 ) * py;
2184
2185 rx += box.x[0];
2186 ry += box.y[0];
2187
2188 if ( mLF->permissibleZonePrepared() )
2189 {
2190 if ( GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), rx - dlx, ry - dly, labelWidth, labelHeight, alpha ) )
2191 {
2192 // cost is set to minimal value, evaluated later
2193 lPos.emplace_back( std::make_unique< LabelPosition >( id++, rx - dlx, ry - dly, labelWidth, labelHeight, alpha, 0.0001, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over ) );
2194 numberCandidatesGenerated++;
2195 }
2196 }
2197 else
2198 {
2199 // TODO - this should be an intersection test, not just a contains test of the candidate centroid
2200 // because in some cases we would want to allow candidates which mostly overlap the polygon even though
2201 // their centroid doesn't overlap (e.g. a "U" shaped polygon)
2202 // but the bugs noted in CostCalculator currently prevent this
2203 if ( mapShape->containsPoint( rx, ry ) )
2204 {
2205 auto potentialCandidate = std::make_unique< LabelPosition >( id++, rx - dlx, ry - dly, labelWidth, labelHeight, alpha, 0.0001, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over );
2206 // cost is set to minimal value, evaluated later
2207 lPos.emplace_back( std::move( potentialCandidate ) );
2208 numberCandidatesGenerated++;
2209 }
2210 }
2211 }
2212 }
2213 } // forall box
2214
2215 nbp = numberCandidatesGenerated;
2216 if ( maxPolygonCandidates > 0 && nbp < targetPolygonCandidates )
2217 {
2218 densityX /= 2;
2219 densityY /= 2;
2220 numTry++;
2221 }
2222 else
2223 {
2224 break;
2225 }
2226 }
2227 while ( numTry < maxTry );
2228
2229 nbp = numberCandidatesGenerated;
2230 }
2231 else
2232 {
2233 nbp = 0;
2234 }
2235
2236 return nbp;
2237}
2238
2239std::size_t FeaturePart::createCandidatesOutsidePolygon( std::vector<std::unique_ptr<LabelPosition> > &lPos, Pal *pal )
2240{
2241 // calculate distance between horizontal lines
2242 const std::size_t maxPolygonCandidates = mLF->layer()->maximumPolygonLabelCandidates();
2243 std::size_t candidatesCreated = 0;
2244
2245 double labelWidth = getLabelWidth();
2246 double labelHeight = getLabelHeight();
2247 double distanceToLabel = getLabelDistance();
2248 const QgsMargins &visualMargin = mLF->visualMargin();
2249
2250 /*
2251 * From Rylov & Reimer (2016) "A practical algorithm for the external annotation of area features":
2252 *
2253 * The list of rules adapted to the
2254 * needs of externally labelling areal features is as follows:
2255 * R1. Labels should be placed horizontally.
2256 * R2. Label should be placed entirely outside at some
2257 * distance from the area feature.
2258 * R3. Name should not cross the boundary of its area
2259 * feature.
2260 * R4. The name should be placed in way that takes into
2261 * account the shape of the feature by achieving a
2262 * balance between the feature and its name, emphasizing their relationship.
2263 * R5. The lettering to the right and slightly above the
2264 * symbol is prioritized.
2265 *
2266 * In the following subsections we utilize four of the five rules
2267 * for two subtasks of label placement, namely, for candidate
2268 * positions generation (R1, R2, and R3) and for measuring their
2269 * ‘goodness’ (R4). The rule R5 is applicable only in the case when
2270 * the area of a polygonal feature is small and the feature can be
2271 * treated and labelled as a point-feature
2272 */
2273
2274 /*
2275 * QGIS approach (cite Dawson (2020) if you want ;) )
2276 *
2277 * We differ from the horizontal sweep line approach described by Rylov & Reimer and instead
2278 * rely on just generating a set of points at regular intervals along the boundary of the polygon (exterior ring).
2279 *
2280 * In practice, this generates similar results as Rylov & Reimer, but has the additional benefits that:
2281 * 1. It avoids the need to calculate intersections between the sweep line and the polygon
2282 * 2. For horizontal or near horizontal segments, Rylov & Reimer propose generating evenly spaced points along
2283 * these segments-- i.e. the same approach as we do for the whole polygon
2284 * 3. It's easier to determine in advance exactly how many candidate positions we'll be generating, and accordingly
2285 * we can easily pick the distance between points along the exterior ring so that the number of positions generated
2286 * matches our target number (targetPolygonCandidates)
2287 */
2288
2289 // TO consider -- for very small polygons (wrt label size), treat them just like a point feature?
2290
2291 double cx, cy;
2292 getCentroid( cx, cy, false );
2293
2294 GEOSContextHandle_t ctxt = QgsGeosContext::get();
2295
2296 // be a bit sneaky and only buffer out 50% here, and then do the remaining 50% when we make the label candidate itself.
2297 // this avoids candidates being created immediately over the buffered ring and always intersecting with it...
2298 geos::unique_ptr buffer( GEOSBuffer_r( ctxt, geos(), distanceToLabel * 0.5, 1 ) );
2299 std::unique_ptr< QgsAbstractGeometry> gg( QgsGeos::fromGeos( buffer.get() ) );
2300
2301 geos::prepared_unique_ptr preparedBuffer( GEOSPrepare_r( ctxt, buffer.get() ) );
2302
2303 const QgsPolygon *poly = qgsgeometry_cast< const QgsPolygon * >( gg.get() );
2304 if ( !poly )
2305 return candidatesCreated;
2306
2308 if ( !ring )
2309 return candidatesCreated;
2310
2311 // 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,
2312 // i.e a loooooong skinny polygon with small area should still generate a large number of candidates)
2313 const double ringLength = ring->length();
2314 const double circleArea = std::pow( ringLength, 2 ) / ( 4 * M_PI );
2315 const std::size_t candidatesForArea = static_cast< std::size_t>( std::ceil( mLF->layer()->mPal->maximumPolygonCandidatesPerMapUnitSquared() * circleArea ) );
2316 const std::size_t targetPolygonCandidates = std::max( static_cast< std::size_t >( 16 ), maxPolygonCandidates > 0 ? std::min( maxPolygonCandidates, candidatesForArea ) : candidatesForArea );
2317
2318 // assume each position generates one candidate
2319 const double delta = ringLength / targetPolygonCandidates;
2320 geos::unique_ptr geosPoint;
2321
2322 const double maxDistCentroidToLabelX = std::max( xmax - cx, cx - xmin ) + distanceToLabel;
2323 const double maxDistCentroidToLabelY = std::max( ymax - cy, cy - ymin ) + distanceToLabel;
2324 const double estimateOfMaxPossibleDistanceCentroidToLabel = std::sqrt( maxDistCentroidToLabelX * maxDistCentroidToLabelX + maxDistCentroidToLabelY * maxDistCentroidToLabelY );
2325
2326 // Satisfy R1: Labels should be placed horizontally.
2327 const double labelAngle = 0;
2328
2329 std::size_t i = lPos.size();
2330 auto addCandidate = [&]( double x, double y, Qgis::LabelPredefinedPointPosition position )
2331 {
2332 double labelX = 0;
2333 double labelY = 0;
2335
2336 // Satisfy R2: Label should be placed entirely outside at some distance from the area feature.
2337 createCandidateAtOrderedPositionOverPoint( labelX, labelY, quadrant, x, y, labelWidth, labelHeight, position, distanceToLabel * 0.5, visualMargin, 0, 0, labelAngle );
2338
2339 auto candidate = std::make_unique< LabelPosition >( i, labelX, labelY, labelWidth, labelHeight, labelAngle, 0, this, LabelPosition::LabelDirectionToLine::SameDirection, quadrant );
2340 if ( candidate->intersects( preparedBuffer.get() ) )
2341 {
2342 // satisfy R3. Name should not cross the boundary of its area feature.
2343
2344 // actually, we use the buffered geometry here, because a label shouldn't be closer to the polygon then the minimum distance value
2345 return;
2346 }
2347
2348 // cost candidates by their distance to the feature's centroid (following Rylov & Reimer)
2349
2350 // Satisfy R4. The name should be placed in way that takes into
2351 // account the shape of the feature by achieving a
2352 // balance between the feature and its name, emphasizing their relationship.
2353
2354
2355 // here we deviate a little from R&R, and instead of just calculating the centroid distance
2356 // to centroid of label, we calculate the distance from the centroid to the nearest point on the label
2357
2358 const double centroidDistance = candidate->getDistanceToPoint( cx, cy, false );
2359 const double centroidCost = centroidDistance / estimateOfMaxPossibleDistanceCentroidToLabel;
2360 candidate->setCost( centroidCost );
2361
2362 lPos.emplace_back( std::move( candidate ) );
2363 candidatesCreated++;
2364 ++i;
2365 };
2366
2367 ring->visitPointsByRegularDistance( delta, [&]( double x, double y, double, double,
2368 double startSegmentX, double startSegmentY, double, double,
2369 double endSegmentX, double endSegmentY, double, double )
2370 {
2371 // get normal angle for segment
2372 float angle = atan2( static_cast< float >( endSegmentY - startSegmentY ), static_cast< float >( endSegmentX - startSegmentX ) ) * 180 / M_PI;
2373 if ( angle < 0 )
2374 angle += 360;
2375
2376 // adapted fom Rylov & Reimer figure 9
2377 if ( angle >= 0 && angle <= 5 )
2378 {
2381 }
2382 else if ( angle <= 85 )
2383 {
2385 }
2386 else if ( angle <= 90 )
2387 {
2390 }
2391
2392 else if ( angle <= 95 )
2393 {
2396 }
2397 else if ( angle <= 175 )
2398 {
2400 }
2401 else if ( angle <= 180 )
2402 {
2405 }
2406
2407 else if ( angle <= 185 )
2408 {
2411 }
2412 else if ( angle <= 265 )
2413 {
2415 }
2416 else if ( angle <= 270 )
2417 {
2420 }
2421 else if ( angle <= 275 )
2422 {
2425 }
2426 else if ( angle <= 355 )
2427 {
2429 }
2430 else
2431 {
2434 }
2435
2436 return !pal->isCanceled();
2437 } );
2438
2439 return candidatesCreated;
2440}
2441
2442std::vector< std::unique_ptr< LabelPosition > > FeaturePart::createCandidates( Pal *pal )
2443{
2444 std::vector< std::unique_ptr< LabelPosition > > lPos;
2445 double angleInRadians = mLF->hasFixedAngle() ? mLF->fixedAngle() : 0.0;
2446
2447 if ( mLF->hasFixedPosition() )
2448 {
2449 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 ) );
2450 }
2451 else
2452 {
2453 switch ( type )
2454 {
2455 case GEOS_POINT:
2456 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::OrderedPositionsAroundPoint )
2457 createCandidatesAtOrderedPositionsOverPoint( x[0], y[0], lPos, angleInRadians );
2458 else if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::OverPoint || mLF->hasFixedQuadrant() )
2459 createCandidatesOverPoint( x[0], y[0], lPos, angleInRadians );
2460 else
2461 createCandidatesAroundPoint( x[0], y[0], lPos, angleInRadians );
2462 break;
2463
2464 case GEOS_LINESTRING:
2465 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Horizontal )
2467 else if ( mLF->layer()->isCurved() )
2468 createCurvedCandidatesAlongLine( lPos, this, true, pal );
2469 else
2470 createCandidatesAlongLine( lPos, this, true, pal );
2471 break;
2472
2473 case GEOS_POLYGON:
2474 {
2475 const double labelWidth = getLabelWidth();
2476 const double labelHeight = getLabelHeight();
2477
2478 const bool allowOutside = mLF->polygonPlacementFlags() & Qgis::LabelPolygonPlacementFlag::AllowPlacementOutsideOfPolygon;
2479 const bool allowInside = mLF->polygonPlacementFlags() & Qgis::LabelPolygonPlacementFlag::AllowPlacementInsideOfPolygon;
2480 //check width/height of bbox is sufficient for label
2481
2482 if ( ( allowOutside && !allowInside ) || ( mLF->layer()->arrangement() == Qgis::LabelPlacement::OutsidePolygons ) )
2483 {
2484 // only allowed to place outside of polygon
2486 }
2487 else if ( allowOutside && ( std::fabs( xmax - xmin ) < labelWidth ||
2488 std::fabs( ymax - ymin ) < labelHeight ) )
2489 {
2490 //no way label can fit in this polygon -- shortcut and only place label outside
2492 }
2493 else
2494 {
2495 std::size_t created = 0;
2496 if ( allowInside )
2497 {
2498 switch ( mLF->layer()->arrangement() )
2499 {
2501 {
2502 double cx, cy;
2503 getCentroid( cx, cy, mLF->layer()->centroidInside() );
2504 if ( qgsDoubleNear( mLF->distLabel(), 0.0 ) )
2505 created += createCandidateCenteredOverPoint( cx, cy, lPos, angleInRadians );
2506 created += createCandidatesAroundPoint( cx, cy, lPos, angleInRadians );
2507 break;
2508 }
2510 {
2511 double cx, cy;
2512 getCentroid( cx, cy, mLF->layer()->centroidInside() );
2513 created += createCandidatesOverPoint( cx, cy, lPos, angleInRadians );
2514 break;
2515 }
2517 created += createCandidatesAlongLine( lPos, this, false, pal );
2518 break;
2520 created += createCurvedCandidatesAlongLine( lPos, this, false, pal );
2521 break;
2522 default:
2523 created += createCandidatesForPolygon( lPos, this, pal );
2524 break;
2525 }
2526 }
2527
2528 if ( allowOutside )
2529 {
2530 // add fallback for labels outside the polygon
2532
2533 if ( created > 0 )
2534 {
2535 // TODO (maybe) increase cost for outside placements (i.e. positions at indices >= created)?
2536 // From my initial testing this doesn't seem necessary
2537 }
2538 }
2539 }
2540 }
2541 }
2542 }
2543
2544 return lPos;
2545}
2546
2547void FeaturePart::addSizePenalty( std::vector< std::unique_ptr< LabelPosition > > &lPos, double bbx[4], double bby[4] ) const
2548{
2549 if ( !mGeos )
2551
2552 GEOSContextHandle_t ctxt = QgsGeosContext::get();
2553 int geomType = GEOSGeomTypeId_r( ctxt, mGeos );
2554
2555 double sizeCost = 0;
2556 if ( geomType == GEOS_LINESTRING )
2557 {
2558 const double l = length();
2559 if ( l <= 0 )
2560 return; // failed to calculate length
2561 double bbox_length = std::max( bbx[2] - bbx[0], bby[2] - bby[0] );
2562 if ( l >= bbox_length / 4 )
2563 return; // the line is longer than quarter of height or width - don't penalize it
2564
2565 sizeCost = 1 - ( l / ( bbox_length / 4 ) ); // < 0,1 >
2566 }
2567 else if ( geomType == GEOS_POLYGON )
2568 {
2569 const double a = area();
2570 if ( a <= 0 )
2571 return;
2572 double bbox_area = ( bbx[2] - bbx[0] ) * ( bby[2] - bby[0] );
2573 if ( a >= bbox_area / 16 )
2574 return; // covers more than 1/16 of our view - don't penalize it
2575
2576 sizeCost = 1 - ( a / ( bbox_area / 16 ) ); // < 0, 1 >
2577 }
2578 else
2579 return; // no size penalty for points
2580
2581// apply the penalty
2582 for ( std::unique_ptr< LabelPosition > &pos : lPos )
2583 {
2584 pos->setCost( pos->cost() + sizeCost / 100 );
2585 }
2586}
2587
2589{
2590 if ( !nbPoints || !p2->nbPoints )
2591 return false;
2592
2593 // here we only care if the lines start or end at the other line -- we don't want to test
2594 // touches as that is true for "T" type joins!
2595 const double x1first = x.front();
2596 const double x1last = x.back();
2597 const double x2first = p2->x.front();
2598 const double x2last = p2->x.back();
2599 const double y1first = y.front();
2600 const double y1last = y.back();
2601 const double y2first = p2->y.front();
2602 const double y2last = p2->y.back();
2603
2604 const bool p2startTouches = ( qgsDoubleNear( x1first, x2first ) && qgsDoubleNear( y1first, y2first ) )
2605 || ( qgsDoubleNear( x1last, x2first ) && qgsDoubleNear( y1last, y2first ) );
2606
2607 const bool p2endTouches = ( qgsDoubleNear( x1first, x2last ) && qgsDoubleNear( y1first, y2last ) )
2608 || ( qgsDoubleNear( x1last, x2last ) && qgsDoubleNear( y1last, y2last ) );
2609 // only one endpoint can touch, not both
2610 if ( ( !p2startTouches && !p2endTouches ) || ( p2startTouches && p2endTouches ) )
2611 return false;
2612
2613 // now we know that we have one line endpoint touching only, but there's still a chance
2614 // that the other side of p2 may touch the original line NOT at the other endpoint
2615 // so we need to check that this point doesn't intersect
2616 const double p2otherX = p2startTouches ? x2last : x2first;
2617 const double p2otherY = p2startTouches ? y2last : y2first;
2618
2619 GEOSContextHandle_t geosctxt = QgsGeosContext::get();
2620
2621 try
2622 {
2623#if GEOS_VERSION_MAJOR>3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR>=12 )
2624 return ( GEOSPreparedIntersectsXY_r( geosctxt, preparedGeom(), p2otherX, p2otherY ) != 1 );
2625#else
2626 GEOSCoordSequence *coord = GEOSCoordSeq_create_r( geosctxt, 1, 2 );
2627 GEOSCoordSeq_setXY_r( geosctxt, coord, 0, p2otherX, p2otherY );
2628 geos::unique_ptr p2OtherEnd( GEOSGeom_createPoint_r( geosctxt, coord ) );
2629 return ( GEOSPreparedIntersects_r( geosctxt, preparedGeom(), p2OtherEnd.get() ) != 1 );
2630#endif
2631 }
2632 catch ( QgsGeosException &e )
2633 {
2634 qWarning( "GEOS exception: %s", e.what() );
2635 QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
2636 return false;
2637 }
2638}
2639
2641{
2642 if ( !mGeos )
2644 if ( !other->mGeos )
2645 other->createGeosGeom();
2646
2647 GEOSContextHandle_t ctxt = QgsGeosContext::get();
2648 try
2649 {
2650 GEOSGeometry *g1 = GEOSGeom_clone_r( ctxt, mGeos );
2651 GEOSGeometry *g2 = GEOSGeom_clone_r( ctxt, other->mGeos );
2652 GEOSGeometry *geoms[2] = { g1, g2 };
2653 geos::unique_ptr g( GEOSGeom_createCollection_r( ctxt, GEOS_MULTILINESTRING, geoms, 2 ) );
2654 geos::unique_ptr gTmp( GEOSLineMerge_r( ctxt, g.get() ) );
2655
2656 if ( GEOSGeomTypeId_r( ctxt, gTmp.get() ) != GEOS_LINESTRING )
2657 {
2658 // sometimes it's not possible to merge lines (e.g. they don't touch at endpoints)
2659 return false;
2660 }
2662
2663 // set up new geometry
2664 mGeos = gTmp.release();
2665 mOwnsGeom = true;
2666
2667 deleteCoords();
2668 qDeleteAll( mHoles );
2669 mHoles.clear();
2671 return true;
2672 }
2673 catch ( QgsGeosException &e )
2674 {
2675 qWarning( "GEOS exception: %s", e.what() );
2676 QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
2677 return false;
2678 }
2679}
2680
2682{
2683 if ( mLF->alwaysShow() )
2684 {
2685 //if feature is set to always show, bump the priority up by orders of magnitude
2686 //so that other feature's labels are unlikely to be placed over the label for this feature
2687 //(negative numbers due to how pal::extract calculates inactive cost)
2688 return -0.2;
2689 }
2690
2691 return mLF->priority() >= 0 ? mLF->priority() : mLF->layer()->priority();
2692}
2693
2695{
2696 bool result = false;
2697
2698 switch ( mLF->layer()->upsidedownLabels() )
2699 {
2701 result = true;
2702 break;
2704 // upright only dynamic labels
2705 if ( !hasFixedRotation() || ( !hasFixedPosition() && fixedAngle() == 0.0 ) )
2706 {
2707 result = true;
2708 }
2709 break;
2711 break;
2712 }
2713 return result;
2714}
@ StretchCharacterSpacingToFitLine
Increases (or decreases) the character spacing used for each label in order to fit the entire text ov...
Definition qgis.h:1249
@ Default
Default curved placement, characters are placed in an optimal position along the line....
Definition qgis.h:1247
@ StretchWordSpacingToFitLine
Increases (or decreases) the word spacing used for each label in order to fit the entire text over th...
Definition qgis.h:1250
@ PlaceCharactersAtVertices
Each individual character from the label text is placed such that their left-baseline position is loc...
Definition qgis.h:1248
@ BelowLine
Labels can be placed below a line feature. Unless MapOrientation is also specified this mode respects...
Definition qgis.h:1338
@ MapOrientation
Signifies that the AboveLine and BelowLine flags should respect the map's orientation rather than the...
Definition qgis.h:1339
@ OnLine
Labels can be placed directly over a line feature.
Definition qgis.h:1336
@ AboveLine
Labels can be placed above a line feature. Unless MapOrientation is also specified this mode respects...
Definition qgis.h:1337
@ FromSymbolBounds
Offset distance applies from rendered symbol bounds.
Definition qgis.h:1303
LabelPrioritization
Label prioritization.
Definition qgis.h:1213
@ PreferCloser
Prefer closer labels, falling back to alternate positions before larger distances.
Definition qgis.h:1214
@ PreferPositionOrdering
Prefer labels follow position ordering, falling back to more distance labels before alternate positio...
Definition qgis.h:1215
@ OverPoint
Arranges candidates over a point (or centroid of a polygon), or at a preset offset from the point....
Definition qgis.h:1229
@ AroundPoint
Arranges candidates in a circle around a point (or centroid of a polygon). Applies to point or polygo...
Definition qgis.h:1228
@ Line
Arranges candidates parallel to a generalised line representing the feature or parallel to a polygon'...
Definition qgis.h:1230
@ Free
Arranges candidates scattered throughout a polygon feature. Candidates are rotated to respect the pol...
Definition qgis.h:1233
@ OrderedPositionsAroundPoint
Candidates are placed in predefined positions around a point. Preference is given to positions with g...
Definition qgis.h:1234
@ Horizontal
Arranges horizontal candidates scattered throughout a polygon feature. Applies to polygon layers only...
Definition qgis.h:1232
@ PerimeterCurved
Arranges candidates following the curvature of a polygon's boundary. Applies to polygon layers only.
Definition qgis.h:1235
@ OutsidePolygons
Candidates are placed outside of polygon boundaries. Applies to polygon layers only.
Definition qgis.h:1236
@ AllowPlacementInsideOfPolygon
Labels can be placed inside a polygon feature.
Definition qgis.h:1362
@ AllowPlacementOutsideOfPolygon
Labels can be placed outside of a polygon feature.
Definition qgis.h:1361
QFlags< LabelLinePlacementFlag > LabelLinePlacementFlags
Line placement flags, which control how candidates are generated for a linear feature.
Definition qgis.h:1350
LabelQuadrantPosition
Label quadrant positions.
Definition qgis.h:1315
@ AboveRight
Above right.
Definition qgis.h:1318
@ BelowLeft
Below left.
Definition qgis.h:1322
@ Above
Above center.
Definition qgis.h:1317
@ BelowRight
Below right.
Definition qgis.h:1324
@ Right
Right middle.
Definition qgis.h:1321
@ AboveLeft
Above left.
Definition qgis.h:1316
@ Below
Below center.
Definition qgis.h:1323
@ Over
Center middle.
Definition qgis.h:1320
@ TreatWhitespaceAsCollision
Treat overlapping whitespace text in labels and whitespace overlapping obstacles as collisions.
Definition qgis.h:1202
@ IgnoreWhitespaceCollisions
Ignore overlapping whitespace text in labels and whitespace overlapping obstacles.
Definition qgis.h:1203
@ UprightCharactersOnly
Permit upright characters only. If not present then upside down text placement is permitted.
Definition qgis.h:3053
QFlags< CurvedTextFlag > CurvedTextFlags
Flags controlling behavior of curved text generation.
Definition qgis.h:3063
LabelPredefinedPointPosition
Positions for labels when using the Qgis::LabelPlacement::OrderedPositionsAroundPoint placement mode.
Definition qgis.h:1262
@ OverPoint
Label directly centered over point.
Definition qgis.h:1275
@ MiddleLeft
Label on left of point.
Definition qgis.h:1268
@ TopRight
Label on top-right of point.
Definition qgis.h:1267
@ MiddleRight
Label on right of point.
Definition qgis.h:1269
@ TopSlightlyRight
Label on top of point, slightly right of center.
Definition qgis.h:1266
@ TopMiddle
Label directly above point.
Definition qgis.h:1265
@ BottomSlightlyLeft
Label below point, slightly left of center.
Definition qgis.h:1271
@ BottomRight
Label on bottom right of point.
Definition qgis.h:1274
@ BottomLeft
Label on bottom-left of point.
Definition qgis.h:1270
@ BottomSlightlyRight
Label below point, slightly right of center.
Definition qgis.h:1273
@ TopLeft
Label on top-left of point.
Definition qgis.h:1263
@ BottomMiddle
Label directly below point.
Definition qgis.h:1272
@ TopSlightlyLeft
Label on top of point, slightly left of center.
Definition qgis.h:1264
@ FlipUpsideDownLabels
Upside-down labels (90 <= angle < 270) are shown upright.
Definition qgis.h:1384
@ AlwaysAllowUpsideDown
Show upside down for all labels, including dynamic ones.
Definition qgis.h:1386
@ AllowUpsideDownWhenRotationIsDefined
Show upside down when rotation is layer- or data-defined.
Definition qgis.h:1385
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:1575
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(), Qgis::StringFormat format=Qgis::StringFormat::PlainText)
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:2239
bool hasFixedRotation() const
Returns true if the feature's label has a fixed rotation.
Definition feature.h:305
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:296
QList< FeaturePart * > mHoles
Definition feature.h:372
double getLabelDistance() const
Returns the distance from the anchor point to the label.
Definition feature.h:302
~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:311
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:1890
std::size_t createCandidatesForPolygon(std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, Pal *pal)
Generate candidates for polygon features.
Definition feature.cpp:1996
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:1511
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:308
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:2640
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:2694
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:371
double getLabelWidth(double angle=0.0) const
Returns the width of the label, optionally taking an angle (in radians) into account.
Definition feature.h:291
QgsLabelFeature * feature()
Returns the parent feature.
Definition feature.h:90
std::vector< std::unique_ptr< LabelPosition > > createCandidates(Pal *pal)
Generates a list of candidate positions for labels for this feature.
Definition feature.cpp:2442
bool isConnected(FeaturePart *p2)
Check whether this part is connected with some other part.
Definition feature.cpp:2588
Layer * layer()
Returns the layer that feature belongs to.
Definition feature.cpp:164
PathOffset
Path offset variances used in curved placement.
Definition feature.h:67
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:2547
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:2681
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:163
Main Pal labeling class.
Definition pal.h:88
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:79
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:261
double ymin
Definition pointset.h:260
double area() const
Returns area of polygon geometry.
bool isClosed() const
Returns true if pointset is closed.
PointSet * holeOf
Definition pointset.h:241
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:231
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:82
std::vector< double > x
Definition pointset.h:230
const GEOSPreparedGeometry * preparedGeom() const
Definition pointset.cpp:157
GEOSGeometry * mGeos
Definition pointset.h:234
double xmin
Definition pointset.h:258
const GEOSGeometry * geos() const
Returns the point set's GEOS geometry.
void invalidateGeos() const
Definition pointset.cpp:169
friend class FeaturePart
Definition pointset.h:78
double xmax
Definition pointset.h:259
bool containsPoint(double x, double y) const
Tests whether point set contains a specified point.
Definition pointset.cpp: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:242
int getNumPoints() const
Definition pointset.h:177
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:7526
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:6937
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:60
struct GEOSGeom_t GEOSGeometry
Definition util.h:42