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