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