QGIS API Documentation  3.24.2-Tisler (13c1a02865)
labelposition.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 "layer.h"
31 #include "pal.h"
32 #include "costcalculator.h"
33 #include "feature.h"
34 #include "geomfunction.h"
35 #include "labelposition.h"
36 #include "qgsgeos.h"
37 #include "qgsmessagelog.h"
38 #include <cmath>
39 #include <cfloat>
40 
41 using namespace pal;
42 
43 LabelPosition::LabelPosition( int id, double x1, double y1, double w, double h, double alpha, double cost, FeaturePart *feature, bool isReversed, Quadrant quadrant )
44  : id( id )
45  , feature( feature )
46  , probFeat( 0 )
47  , nbOverlap( 0 )
48  , alpha( alpha )
49  , w( w )
50  , h( h )
51  , partId( -1 )
52  , reversed( isReversed )
53  , upsideDown( false )
54  , quadrant( quadrant )
55  , mCost( cost )
56  , mHasObstacleConflict( false )
57  , mUpsideDownCharCount( 0 )
58 {
59  type = GEOS_POLYGON;
60  nbPoints = 4;
61  x.resize( nbPoints );
62  y.resize( nbPoints );
63 
64  // alpha take his value bw 0 and 2*pi rad
65  while ( this->alpha > 2 * M_PI )
66  this->alpha -= 2 * M_PI;
67 
68  while ( this->alpha < 0 )
69  this->alpha += 2 * M_PI;
70 
71  const double beta = this->alpha + M_PI_2;
72 
73  double dx1, dx2, dy1, dy2;
74 
75  dx1 = std::cos( this->alpha ) * w;
76  dy1 = std::sin( this->alpha ) * w;
77 
78  dx2 = std::cos( beta ) * h;
79  dy2 = std::sin( beta ) * h;
80 
81  x[0] = x1;
82  y[0] = y1;
83 
84  x[1] = x1 + dx1;
85  y[1] = y1 + dy1;
86 
87  x[2] = x1 + dx1 + dx2;
88  y[2] = y1 + dy1 + dy2;
89 
90  x[3] = x1 + dx2;
91  y[3] = y1 + dy2;
92 
93  // upside down ? (curved labels are always correct)
94  if ( !feature->layer()->isCurved() &&
95  this->alpha > M_PI_2 && this->alpha <= 3 * M_PI_2 )
96  {
98  {
99  // Turn label upsidedown by inverting boundary points
100  double tx, ty;
101 
102  tx = x[0];
103  ty = y[0];
104 
105  x[0] = x[2];
106  y[0] = y[2];
107 
108  x[2] = tx;
109  y[2] = ty;
110 
111  tx = x[1];
112  ty = y[1];
113 
114  x[1] = x[3];
115  y[1] = y[3];
116 
117  x[3] = tx;
118  y[3] = ty;
119 
120  if ( this->alpha < M_PI )
121  this->alpha += M_PI;
122  else
123  this->alpha -= M_PI;
124 
125  // labels with text shown upside down are not classified as upsideDown,
126  // only those whose boundary points have been inverted
127  upsideDown = true;
128  }
129  }
130 
131  for ( int i = 0; i < nbPoints; ++i )
132  {
133  xmin = std::min( xmin, x[i] );
134  xmax = std::max( xmax, x[i] );
135  ymin = std::min( ymin, y[i] );
136  ymax = std::max( ymax, y[i] );
137  }
138 }
139 
141  : PointSet( other )
142 {
143  id = other.id;
144  mCost = other.mCost;
145  feature = other.feature;
146  probFeat = other.probFeat;
147  nbOverlap = other.nbOverlap;
148 
149  alpha = other.alpha;
150  w = other.w;
151  h = other.h;
152 
153  if ( other.mNextPart )
154  mNextPart = std::make_unique< LabelPosition >( *other.mNextPart );
155 
156  partId = other.partId;
157  upsideDown = other.upsideDown;
158  reversed = other.reversed;
159  quadrant = other.quadrant;
160  mHasObstacleConflict = other.mHasObstacleConflict;
161  mUpsideDownCharCount = other.mUpsideDownCharCount;
162 }
163 
164 bool LabelPosition::isIn( double *bbox )
165 {
166  int i;
167 
168  for ( i = 0; i < 4; i++ )
169  {
170  if ( x[i] >= bbox[0] && x[i] <= bbox[2] &&
171  y[i] >= bbox[1] && y[i] <= bbox[3] )
172  return true;
173  }
174 
175  if ( mNextPart )
176  return mNextPart->isIn( bbox );
177  else
178  return false;
179 }
180 
181 bool LabelPosition::isIntersect( double *bbox )
182 {
183  int i;
184 
185  for ( i = 0; i < 4; i++ )
186  {
187  if ( x[i] >= bbox[0] && x[i] <= bbox[2] &&
188  y[i] >= bbox[1] && y[i] <= bbox[3] )
189  return true;
190  }
191 
192  if ( mNextPart )
193  return mNextPart->isIntersect( bbox );
194  else
195  return false;
196 }
197 
198 bool LabelPosition::intersects( const GEOSPreparedGeometry *geometry )
199 {
200  if ( !mGeos )
201  createGeosGeom();
202 
203  try
204  {
205  if ( GEOSPreparedIntersects_r( QgsGeos::getGEOSHandler(), geometry, mGeos ) == 1 )
206  {
207  return true;
208  }
209  else if ( mNextPart )
210  {
211  return mNextPart->intersects( geometry );
212  }
213  }
214  catch ( GEOSException &e )
215  {
216  qWarning( "GEOS exception: %s", e.what() );
217  QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
218  return false;
219  }
220 
221  return false;
222 }
223 
224 bool LabelPosition::within( const GEOSPreparedGeometry *geometry )
225 {
226  if ( !mGeos )
227  createGeosGeom();
228 
229  try
230  {
231  if ( GEOSPreparedContains_r( QgsGeos::getGEOSHandler(), geometry, mGeos ) != 1 )
232  {
233  return false;
234  }
235  else if ( mNextPart )
236  {
237  return mNextPart->within( geometry );
238  }
239  }
240  catch ( GEOSException &e )
241  {
242  qWarning( "GEOS exception: %s", e.what() );
243  QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
244  return false;
245  }
246 
247  return true;
248 }
249 
250 bool LabelPosition::isInside( double *bbox )
251 {
252  for ( int i = 0; i < 4; i++ )
253  {
254  if ( !( x[i] >= bbox[0] && x[i] <= bbox[2] &&
255  y[i] >= bbox[1] && y[i] <= bbox[3] ) )
256  return false;
257  }
258 
259  if ( mNextPart )
260  return mNextPart->isInside( bbox );
261  else
262  return true;
263 }
264 
266 {
267  if ( this->probFeat == lp->probFeat ) // bugfix #1
268  return false; // always overlaping itself !
269 
270  if ( !nextPart() && !lp->nextPart() )
271  {
272  if ( qgsDoubleNear( alpha, 0 ) && qgsDoubleNear( lp->alpha, 0 ) )
273  {
274  // simple case -- both candidates are oriented to axis, so shortcut with easy calculation
275  return boundingBoxIntersects( lp );
276  }
277  }
278 
279  return isInConflictMultiPart( lp );
280 }
281 
282 bool LabelPosition::isInConflictMultiPart( const LabelPosition *lp ) const
283 {
284  if ( !mMultipartGeos )
285  createMultiPartGeosGeom();
286 
287  if ( !lp->mMultipartGeos )
288  lp->createMultiPartGeosGeom();
289 
290  GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler();
291  try
292  {
293  const bool result = ( GEOSPreparedIntersects_r( geosctxt, preparedMultiPartGeom(), lp->mMultipartGeos ) == 1 );
294  return result;
295  }
296  catch ( GEOSException &e )
297  {
298  qWarning( "GEOS exception: %s", e.what() );
299  QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
300  return false;
301  }
302 
303  return false;
304 }
305 
306 int LabelPosition::partCount() const
307 {
308  if ( mNextPart )
309  return mNextPart->partCount() + 1;
310  else
311  return 1;
312 }
313 
314 void LabelPosition::offsetPosition( double xOffset, double yOffset )
315 {
316  for ( int i = 0; i < 4; i++ )
317  {
318  x[i] += xOffset;
319  y[i] += yOffset;
320  }
321 
322  if ( mNextPart )
323  mNextPart->offsetPosition( xOffset, yOffset );
324 
325  invalidateGeos();
326 }
327 
329 {
330  return id;
331 }
332 
333 double LabelPosition::getX( int i ) const
334 {
335  return ( i >= 0 && i < 4 ? x[i] : -1 );
336 }
337 
338 double LabelPosition::getY( int i ) const
339 {
340  return ( i >= 0 && i < 4 ? y[i] : -1 );
341 }
342 
344 {
345  return alpha;
346 }
347 
349 {
350  if ( mCost >= 1 )
351  {
352  mCost -= int ( mCost ); // label cost up to 1
353  }
354 }
355 
357 {
358  return feature;
359 }
360 
361 void LabelPosition::getBoundingBox( double amin[2], double amax[2] ) const
362 {
363  if ( mNextPart )
364  {
365  mNextPart->getBoundingBox( amin, amax );
366  }
367  else
368  {
369  amin[0] = std::numeric_limits<double>::max();
370  amax[0] = std::numeric_limits<double>::lowest();
371  amin[1] = std::numeric_limits<double>::max();
372  amax[1] = std::numeric_limits<double>::lowest();
373  }
374  for ( int c = 0; c < 4; c++ )
375  {
376  if ( x[c] < amin[0] )
377  amin[0] = x[c];
378  if ( x[c] > amax[0] )
379  amax[0] = x[c];
380  if ( y[c] < amin[1] )
381  amin[1] = y[c];
382  if ( y[c] > amax[1] )
383  amax[1] = y[c];
384  }
385 }
386 
388 {
389  mHasObstacleConflict = conflicts;
390  if ( mNextPart )
391  mNextPart->setConflictsWithObstacle( conflicts );
392 }
393 
395 {
396  mHasHardConflict = conflicts;
397  if ( mNextPart )
398  mNextPart->setHasHardObstacleConflict( conflicts );
399 }
400 
402 {
403  double amin[2];
404  double amax[2];
405  getBoundingBox( amin, amax );
406  index.remove( this, QgsRectangle( amin[0], amin[1], amax[0], amax[1] ) );
407 }
408 
410 {
411  double amin[2];
412  double amax[2];
413  getBoundingBox( amin, amax );
414  index.insert( this, QgsRectangle( amin[0], amin[1], amax[0], amax[1] ) );
415 }
416 
417 
418 void LabelPosition::createMultiPartGeosGeom() const
419 {
420  GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler();
421 
422  std::vector< const GEOSGeometry * > geometries;
423  const LabelPosition *tmp1 = this;
424  while ( tmp1 )
425  {
426  const GEOSGeometry *partGeos = tmp1->geos();
427  if ( !GEOSisEmpty_r( geosctxt, partGeos ) )
428  geometries.emplace_back( partGeos );
429  tmp1 = tmp1->nextPart();
430  }
431 
432  const std::size_t partCount = geometries.size();
433  GEOSGeometry **geomarr = new GEOSGeometry*[ partCount ];
434  for ( std::size_t i = 0; i < partCount; ++i )
435  {
436  geomarr[i ] = GEOSGeom_clone_r( geosctxt, geometries[i] );
437  }
438 
439  mMultipartGeos = GEOSGeom_createCollection_r( geosctxt, GEOS_MULTIPOLYGON, geomarr, partCount );
440  delete [] geomarr;
441 }
442 
443 const GEOSPreparedGeometry *LabelPosition::preparedMultiPartGeom() const
444 {
445  if ( !mMultipartGeos )
446  createMultiPartGeosGeom();
447 
448  if ( !mMultipartPreparedGeos )
449  {
450  mMultipartPreparedGeos = GEOSPrepare_r( QgsGeos::getGEOSHandler(), mMultipartGeos );
451  }
452  return mMultipartPreparedGeos;
453 }
454 
455 double LabelPosition::getDistanceToPoint( double xp, double yp ) const
456 {
457  //first check if inside, if so then distance is -1
458  bool contains = false;
459  if ( alpha == 0 )
460  {
461  // easy case -- horizontal label
462  contains = x[0] <= xp && x[1] >= xp && y[0] <= yp && y[2] >= yp;
463  }
464  else
465  {
466  contains = containsPoint( xp, yp );
467  }
468 
469  double distance = -1;
470  if ( !contains )
471  {
472  if ( alpha == 0 )
473  {
474  const double dx = std::max( std::max( x[0] - xp, 0.0 ), xp - x[1] );
475  const double dy = std::max( std::max( y[0] - yp, 0.0 ), yp - y[2] );
476  distance = std::sqrt( dx * dx + dy * dy );
477  }
478  else
479  {
480  distance = std::sqrt( minDistanceToPoint( xp, yp ) );
481  }
482  }
483 
484  if ( mNextPart && distance > 0 )
485  return std::min( distance, mNextPart->getDistanceToPoint( xp, yp ) );
486 
487  return distance;
488 }
489 
491 {
492  if ( !mGeos )
493  createGeosGeom();
494 
495  if ( !line->mGeos )
496  line->createGeosGeom();
497 
498  GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler();
499  try
500  {
501  if ( GEOSPreparedIntersects_r( geosctxt, line->preparedGeom(), mGeos ) == 1 )
502  {
503  return true;
504  }
505  else if ( mNextPart )
506  {
507  return mNextPart->crossesLine( line );
508  }
509  }
510  catch ( GEOSException &e )
511  {
512  qWarning( "GEOS exception: %s", e.what() );
513  QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
514  return false;
515  }
516 
517  return false;
518 }
519 
521 {
522  if ( !mGeos )
523  createGeosGeom();
524 
525  if ( !polygon->mGeos )
526  polygon->createGeosGeom();
527 
528  GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler();
529  try
530  {
531  if ( GEOSPreparedIntersects_r( geosctxt, polygon->preparedGeom(), mGeos ) == 1
532  && GEOSPreparedContains_r( geosctxt, polygon->preparedGeom(), mGeos ) != 1 )
533  {
534  return true;
535  }
536  else if ( mNextPart )
537  {
538  return mNextPart->crossesBoundary( polygon );
539  }
540  }
541  catch ( GEOSException &e )
542  {
543  qWarning( "GEOS exception: %s", e.what() );
544  QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
545  return false;
546  }
547 
548  return false;
549 }
550 
552 {
553  //effectively take the average polygon intersection cost for all label parts
554  const double totalCost = polygonIntersectionCostForParts( polygon );
555  const int n = partCount();
556  return std::ceil( totalCost / n );
557 }
558 
560 {
561  if ( !mGeos )
562  createGeosGeom();
563 
564  if ( !polygon->mGeos )
565  polygon->createGeosGeom();
566 
567  GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler();
568  try
569  {
570  if ( GEOSPreparedIntersects_r( geosctxt, polygon->preparedGeom(), mGeos ) == 1 )
571  {
572  return true;
573  }
574  }
575  catch ( GEOSException &e )
576  {
577  qWarning( "GEOS exception: %s", e.what() );
578  QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
579  }
580 
581  if ( mNextPart )
582  {
583  return mNextPart->intersectsWithPolygon( polygon );
584  }
585  else
586  {
587  return false;
588  }
589 }
590 
591 double LabelPosition::polygonIntersectionCostForParts( PointSet *polygon ) const
592 {
593  if ( !mGeos )
594  createGeosGeom();
595 
596  if ( !polygon->mGeos )
597  polygon->createGeosGeom();
598 
599  GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler();
600  double cost = 0;
601  try
602  {
603  if ( GEOSPreparedIntersects_r( geosctxt, polygon->preparedGeom(), mGeos ) == 1 )
604  {
605  //at least a partial intersection
606  cost += 1;
607 
608  double px, py;
609 
610  // check each corner
611  for ( int i = 0; i < 4; ++i )
612  {
613  px = x[i];
614  py = y[i];
615 
616  for ( int a = 0; a < 2; ++a ) // and each middle of segment
617  {
618  if ( polygon->containsPoint( px, py ) )
619  cost++;
620  px = ( x[i] + x[( i + 1 ) % 4] ) / 2.0;
621  py = ( y[i] + y[( i + 1 ) % 4] ) / 2.0;
622  }
623  }
624 
625  px = ( x[0] + x[2] ) / 2.0;
626  py = ( y[0] + y[2] ) / 2.0;
627 
628  //check the label center. if covered by polygon, cost of 4
629  if ( polygon->containsPoint( px, py ) )
630  cost += 4;
631  }
632  }
633  catch ( GEOSException &e )
634  {
635  qWarning( "GEOS exception: %s", e.what() );
636  QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
637  }
638 
639  //maintain scaling from 0 -> 12
640  cost = 12.0 * cost / 13.0;
641 
642  if ( mNextPart )
643  {
644  cost += mNextPart->polygonIntersectionCostForParts( polygon );
645  }
646 
647  return cost;
648 }
649 
651 {
652  double angleDiff = 0.0;
653  double angleLast = 0.0;
654  LabelPosition *tmp = this;
655  while ( tmp )
656  {
657  if ( tmp != this ) // not first?
658  {
659  double diff = std::fabs( tmp->getAlpha() - angleLast );
660  if ( diff > 2 * M_PI )
661  diff -= 2 * M_PI;
662  diff = std::min( diff, 2 * M_PI - diff ); // difference 350 deg is actually just 10 deg...
663  angleDiff += diff;
664  }
665 
666  angleLast = tmp->getAlpha();
667  tmp = tmp->nextPart();
668  }
669  return angleDiff;
670 }
A rtree spatial index for use in the pal labeling engine.
Definition: palrtree.h:36
void insert(T *data, const QgsRectangle &bounds)
Inserts new data into the spatial index, with the specified bounds.
Definition: palrtree.h:59
void remove(T *data, const QgsRectangle &bounds)
Removes existing data from the spatial index, with the specified bounds.
Definition: palrtree.h:78
static GEOSContextHandle_t getGEOSHandler()
Definition: qgsgeos.cpp:3222
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
A rectangle specified with double values.
Definition: qgsrectangle.h:42
Main class to handle feature.
Definition: feature.h:65
bool onlyShowUprightLabels() const
Returns true if feature's label must be displayed upright.
Definition: feature.cpp:2232
Layer * layer()
Returns the layer that feature belongs to.
Definition: feature.cpp:156
LabelPosition is a candidate feature label position.
Definition: labelposition.h:56
bool intersectsWithPolygon(PointSet *polygon) const
Returns true if any intersection between polygon and position exists.
bool isInConflict(const LabelPosition *ls) const
Check whether or not this overlap with another labelPosition.
bool isIntersect(double *bbox)
Is the labelposition intersect the bounding-box ?
double getAlpha() const
Returns the angle to rotate text (in rad).
Quadrant
Position of label candidate relative to feature.
Definition: labelposition.h:66
LabelPosition::Quadrant quadrant
double angleDifferential()
Returns the angle differential of all LabelPosition parts.
void removeFromIndex(PalRtree< LabelPosition > &index)
Removes the label position from the specified index.
bool isInside(double *bbox)
Is the labelposition inside the bounding-box ?
bool crossesLine(PointSet *line) const
Returns true if this label crosses the specified line.
int getId() const
Returns the id.
FeaturePart * feature
void validateCost()
Make sure the cost is less than 1.
bool isIn(double *bbox)
Is the labelposition in the bounding-box ? (intersect or inside????)
double cost() const
Returns the candidate label position's geographical cost.
void setConflictsWithObstacle(bool conflicts)
Sets whether the position is marked as conflicting with an obstacle feature.
bool intersects(const GEOSPreparedGeometry *geometry)
Returns true if the label position intersects a geometry.
void setHasHardObstacleConflict(bool conflicts)
Sets whether the position is marked as having a hard conflict with an obstacle feature.
bool crossesBoundary(PointSet *polygon) const
Returns true if this label crosses the boundary of the specified polygon.
LabelPosition * nextPart() const
Returns the next part of this label position (i.e.
void offsetPosition(double xOffset, double yOffset)
Shift the label by specified offset.
double getDistanceToPoint(double xp, double yp) const
Gets distance from this label to a point. If point lies inside, returns negative number.
FeaturePart * getFeaturePart() const
Returns the feature corresponding to this labelposition.
const GEOSPreparedGeometry * preparedMultiPartGeom() const
Returns a prepared GEOS representation of all label parts as a multipolygon.
double getX(int i=0) const
Returns the down-left x coordinate.
void getBoundingBox(double amin[2], double amax[2]) const
Returns bounding box - amin: xmin,ymin - amax: xmax,ymax.
double getY(int i=0) const
Returns the down-left y coordinate.
bool within(const GEOSPreparedGeometry *geometry)
Returns true if the label position is within a geometry.
void insertIntoIndex(PalRtree< LabelPosition > &index)
Inserts the label position into the specified index.
int polygonIntersectionCost(PointSet *polygon) const
Returns cost of position intersection with polygon (testing area of intersection and center).
bool isCurved() const
Returns true if the layer has curved labels.
Definition: layer.h:182
The underlying raw pal geometry class.
Definition: pointset.h:77
friend class LabelPosition
Definition: pointset.h:79
double ymax
Definition: pointset.h:261
double ymin
Definition: pointset.h:260
void createGeosGeom() const
Definition: pointset.cpp:100
std::vector< double > y
Definition: pointset.h:231
bool boundingBoxIntersects(const PointSet *other) const
Returns true if the bounding box of this pointset intersects the bounding box of another pointset.
Definition: pointset.cpp:989
std::vector< double > x
Definition: pointset.h:230
const GEOSPreparedGeometry * preparedGeom() const
Definition: pointset.cpp:150
GEOSGeometry * mGeos
Definition: pointset.h:234
double xmin
Definition: pointset.h:258
const GEOSGeometry * geos() const
Returns the point set's GEOS geometry.
Definition: pointset.cpp:1077
void invalidateGeos()
Definition: pointset.cpp:162
double minDistanceToPoint(double px, double py, double *rx=nullptr, double *ry=nullptr) const
Returns the squared minimum distance between the point set geometry and the point (px,...
Definition: pointset.cpp:853
double xmax
Definition: pointset.h:259
bool containsPoint(double x, double y) const
Tests whether point set contains a specified point.
Definition: pointset.cpp:266
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:1578