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