Branch data Line data Source code
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 "palexception.h"
34 : : #include "palstat.h"
35 : : #include "costcalculator.h"
36 : : #include "feature.h"
37 : : #include "geomfunction.h"
38 : : #include "labelposition.h"
39 : : #include "problem.h"
40 : : #include "pointset.h"
41 : : #include "internalexception.h"
42 : : #include "util.h"
43 : : #include "palrtree.h"
44 : : #include "qgssettings.h"
45 : : #include <cfloat>
46 : : #include <list>
47 : :
48 : : using namespace pal;
49 : :
50 : 0 : Pal::Pal()
51 : : {
52 : 0 : QgsSettings settings;
53 : 0 : mGlobalCandidatesLimitPoint = settings.value( QStringLiteral( "rendering/label_candidates_limit_points" ), 0, QgsSettings::Core ).toInt();
54 : 0 : mGlobalCandidatesLimitLine = settings.value( QStringLiteral( "rendering/label_candidates_limit_lines" ), 0, QgsSettings::Core ).toInt();
55 : 0 : mGlobalCandidatesLimitPolygon = settings.value( QStringLiteral( "rendering/label_candidates_limit_polygons" ), 0, QgsSettings::Core ).toInt();
56 : 0 : }
57 : :
58 : 0 : Pal::~Pal() = default;
59 : :
60 : 0 : void Pal::removeLayer( Layer *layer )
61 : : {
62 : 0 : if ( !layer )
63 : 0 : return;
64 : :
65 : 0 : mMutex.lock();
66 : :
67 : 0 : for ( auto it = mLayers.begin(); it != mLayers.end(); ++it )
68 : : {
69 : 0 : if ( it->second.get() == layer )
70 : : {
71 : 0 : mLayers.erase( it );
72 : 0 : break;
73 : : }
74 : 0 : }
75 : 0 : mMutex.unlock();
76 : 0 : }
77 : :
78 : 0 : Layer *Pal::addLayer( QgsAbstractLabelProvider *provider, const QString &layerName, QgsPalLayerSettings::Placement arrangement, double defaultPriority, bool active, bool toLabel, bool displayAll )
79 : : {
80 : 0 : mMutex.lock();
81 : :
82 : : Q_ASSERT( mLayers.find( provider ) == mLayers.end() );
83 : :
84 : 0 : std::unique_ptr< Layer > layer = std::make_unique< Layer >( provider, layerName, arrangement, defaultPriority, active, toLabel, this, displayAll );
85 : 0 : Layer *res = layer.get();
86 : 0 : mLayers.insert( std::pair<QgsAbstractLabelProvider *, std::unique_ptr< Layer >>( provider, std::move( layer ) ) );
87 : 0 : mMutex.unlock();
88 : :
89 : 0 : return res;
90 : 0 : }
91 : :
92 : 0 : std::unique_ptr<Problem> Pal::extract( const QgsRectangle &extent, const QgsGeometry &mapBoundary )
93 : : {
94 : : // expand out the incoming buffer by 1000x -- that's the visible map extent, yet we may be getting features which exceed this extent
95 : : // (while 1000x may seem excessive here, this value is only used for scaling coordinates in the spatial indexes
96 : : // and the consequence of inserting coordinates outside this extent is worse than the consequence of setting this value too large.)
97 : 0 : const QgsRectangle maxCoordinateExtentForSpatialIndices = extent.buffered( std::max( extent.width(), extent.height() ) * 1000 );
98 : :
99 : : // to store obstacles
100 : 0 : PalRtree< FeaturePart > obstacles( maxCoordinateExtentForSpatialIndices );
101 : 0 : PalRtree< LabelPosition > allCandidatesFirstRound( maxCoordinateExtentForSpatialIndices );
102 : 0 : std::vector< FeaturePart * > allObstacleParts;
103 : 0 : std::unique_ptr< Problem > prob = std::make_unique< Problem >( maxCoordinateExtentForSpatialIndices );
104 : :
105 : : double bbx[4];
106 : : double bby[4];
107 : :
108 : 0 : bbx[0] = bbx[3] = prob->mMapExtentBounds[0] = extent.xMinimum();
109 : 0 : bby[0] = bby[1] = prob->mMapExtentBounds[1] = extent.yMinimum();
110 : 0 : bbx[1] = bbx[2] = prob->mMapExtentBounds[2] = extent.xMaximum();
111 : 0 : bby[2] = bby[3] = prob->mMapExtentBounds[3] = extent.yMaximum();
112 : :
113 : 0 : prob->pal = this;
114 : :
115 : 0 : std::list< std::unique_ptr< Feats > > features;
116 : :
117 : : // prepare map boundary
118 : 0 : geos::unique_ptr mapBoundaryGeos( QgsGeos::asGeos( mapBoundary ) );
119 : 0 : geos::prepared_unique_ptr mapBoundaryPrepared( GEOSPrepare_r( QgsGeos::getGEOSHandler(), mapBoundaryGeos.get() ) );
120 : :
121 : 0 : int obstacleCount = 0;
122 : :
123 : : // first step : extract features from layers
124 : :
125 : 0 : std::size_t previousFeatureCount = 0;
126 : 0 : int previousObstacleCount = 0;
127 : :
128 : 0 : QStringList layersWithFeaturesInBBox;
129 : :
130 : 0 : QMutexLocker palLocker( &mMutex );
131 : 0 : for ( const auto &it : mLayers )
132 : : {
133 : 0 : Layer *layer = it.second.get();
134 : 0 : if ( !layer )
135 : : {
136 : : // invalid layer name
137 : 0 : continue;
138 : : }
139 : :
140 : : // only select those who are active
141 : 0 : if ( !layer->active() )
142 : 0 : continue;
143 : :
144 : : // check for connected features with the same label text and join them
145 : 0 : if ( layer->mergeConnectedLines() )
146 : 0 : layer->joinConnectedFeatures();
147 : :
148 : 0 : if ( isCanceled() )
149 : 0 : return nullptr;
150 : :
151 : 0 : layer->chopFeaturesAtRepeatDistance();
152 : :
153 : 0 : if ( isCanceled() )
154 : 0 : return nullptr;
155 : :
156 : 0 : QMutexLocker locker( &layer->mMutex );
157 : :
158 : : // generate candidates for all features
159 : 0 : for ( FeaturePart *featurePart : std::as_const( layer->mFeatureParts ) )
160 : : {
161 : 0 : if ( isCanceled() )
162 : 0 : break;
163 : :
164 : : // Holes of the feature are obstacles
165 : 0 : for ( int i = 0; i < featurePart->getNumSelfObstacles(); i++ )
166 : : {
167 : 0 : FeaturePart *selfObstacle = featurePart->getSelfObstacle( i );
168 : 0 : obstacles.insert( selfObstacle, selfObstacle->boundingBox() );
169 : 0 : allObstacleParts.emplace_back( selfObstacle );
170 : :
171 : 0 : if ( !featurePart->getSelfObstacle( i )->getHoleOf() )
172 : : {
173 : : //ERROR: SHOULD HAVE A PARENT!!!!!
174 : 0 : }
175 : 0 : }
176 : :
177 : : // generate candidates for the feature part
178 : 0 : std::vector< std::unique_ptr< LabelPosition > > candidates = featurePart->createCandidates( this );
179 : :
180 : 0 : if ( isCanceled() )
181 : 0 : break;
182 : :
183 : : // purge candidates that are outside the bbox
184 : 0 : candidates.erase( std::remove_if( candidates.begin(), candidates.end(), [&mapBoundaryPrepared, this]( std::unique_ptr< LabelPosition > &candidate )
185 : : {
186 : 0 : if ( showPartialLabels() )
187 : 0 : return !candidate->intersects( mapBoundaryPrepared.get() );
188 : : else
189 : 0 : return !candidate->within( mapBoundaryPrepared.get() );
190 : 0 : } ), candidates.end() );
191 : :
192 : 0 : if ( isCanceled() )
193 : 0 : break;
194 : :
195 : 0 : if ( !candidates.empty() )
196 : : {
197 : 0 : for ( std::unique_ptr< LabelPosition > &candidate : candidates )
198 : : {
199 : 0 : candidate->insertIntoIndex( allCandidatesFirstRound );
200 : : }
201 : :
202 : 0 : std::sort( candidates.begin(), candidates.end(), CostCalculator::candidateSortGrow );
203 : :
204 : : // valid features are added to fFeats
205 : 0 : std::unique_ptr< Feats > ft = std::make_unique< Feats >();
206 : 0 : ft->feature = featurePart;
207 : 0 : ft->shape = nullptr;
208 : 0 : ft->candidates = std::move( candidates );
209 : 0 : ft->priority = featurePart->calculatePriority();
210 : 0 : features.emplace_back( std::move( ft ) );
211 : 0 : }
212 : : else
213 : : {
214 : : // no candidates, so generate a default "point on surface" one
215 : 0 : std::unique_ptr< LabelPosition > unplacedPosition = featurePart->createCandidatePointOnSurface( featurePart );
216 : 0 : if ( !unplacedPosition )
217 : 0 : continue;
218 : :
219 : 0 : if ( layer->displayAll() )
220 : : {
221 : : // if we are displaying all labels, we throw the default candidate in too
222 : 0 : unplacedPosition->insertIntoIndex( allCandidatesFirstRound );
223 : 0 : candidates.emplace_back( std::move( unplacedPosition ) );
224 : :
225 : : // valid features are added to fFeats
226 : 0 : std::unique_ptr< Feats > ft = std::make_unique< Feats >();
227 : 0 : ft->feature = featurePart;
228 : 0 : ft->shape = nullptr;
229 : 0 : ft->candidates = std::move( candidates );
230 : 0 : ft->priority = featurePart->calculatePriority();
231 : 0 : features.emplace_back( std::move( ft ) );
232 : 0 : }
233 : : else
234 : : {
235 : : // not displaying all labels for this layer, so it goes into the unlabeled feature list
236 : 0 : prob->positionsWithNoCandidates()->emplace_back( std::move( unplacedPosition ) );
237 : : }
238 : 0 : }
239 : 0 : }
240 : 0 : if ( isCanceled() )
241 : 0 : return nullptr;
242 : :
243 : : // collate all layer obstacles
244 : 0 : for ( FeaturePart *obstaclePart : std::as_const( layer->mObstacleParts ) )
245 : : {
246 : 0 : if ( isCanceled() )
247 : 0 : break; // do not continue searching
248 : :
249 : : // insert into obstacles
250 : 0 : obstacles.insert( obstaclePart, obstaclePart->boundingBox() );
251 : 0 : allObstacleParts.emplace_back( obstaclePart );
252 : 0 : obstacleCount++;
253 : 0 : }
254 : :
255 : 0 : if ( isCanceled() )
256 : 0 : return nullptr;
257 : :
258 : 0 : locker.unlock();
259 : 0 :
260 : 0 : if ( features.size() - previousFeatureCount > 0 || obstacleCount > previousObstacleCount )
261 : : {
262 : 0 : layersWithFeaturesInBBox << layer->name();
263 : 0 : }
264 : 0 : previousFeatureCount = features.size();
265 : 0 : previousObstacleCount = obstacleCount;
266 : 0 : }
267 : 0 : palLocker.unlock();
268 : 0 :
269 : 0 : if ( isCanceled() )
270 : 0 : return nullptr;
271 : 0 :
272 : 0 : prob->mLayerCount = layersWithFeaturesInBBox.size();
273 : 0 : prob->labelledLayersName = layersWithFeaturesInBBox;
274 : 0 :
275 : 0 : prob->mFeatureCount = features.size();
276 : 0 : prob->mTotalCandidates = 0;
277 : 0 : prob->mFeatNbLp.resize( prob->mFeatureCount );
278 : 0 : prob->mFeatStartId.resize( prob->mFeatureCount );
279 : 0 : prob->mInactiveCost.resize( prob->mFeatureCount );
280 : :
281 : 0 : if ( !features.empty() )
282 : : {
283 : : // Filtering label positions against obstacles
284 : 0 : for ( FeaturePart *obstaclePart : allObstacleParts )
285 : : {
286 : 0 : if ( isCanceled() )
287 : 0 : break; // do not continue searching
288 : :
289 : 0 : allCandidatesFirstRound.intersects( obstaclePart->boundingBox(), [obstaclePart, this]( const LabelPosition * candidatePosition ) -> bool
290 : : {
291 : : // test whether we should ignore this obstacle for the candidate. We do this if:
292 : : // 1. it's not a hole, and the obstacle belongs to the same label feature as the candidate (e.g.,
293 : : // features aren't obstacles for their own labels)
294 : : // 2. it IS a hole, and the hole belongs to a different label feature to the candidate (e.g., holes
295 : : // are ONLY obstacles for the labels of the feature they belong to)
296 : 0 : if ( ( !obstaclePart->getHoleOf() && candidatePosition->getFeaturePart()->hasSameLabelFeatureAs( obstaclePart ) )
297 : 0 : || ( obstaclePart->getHoleOf() && !candidatePosition->getFeaturePart()->hasSameLabelFeatureAs( dynamic_cast< FeaturePart * >( obstaclePart->getHoleOf() ) ) ) )
298 : : {
299 : 0 : return true;
300 : : }
301 : :
302 : 0 : CostCalculator::addObstacleCostPenalty( const_cast< LabelPosition * >( candidatePosition ), obstaclePart, this );
303 : :
304 : 0 : return true;
305 : 0 : } );
306 : : }
307 : :
308 : 0 : if ( isCanceled() )
309 : : {
310 : 0 : return nullptr;
311 : : }
312 : :
313 : 0 : int idlp = 0;
314 : 0 : for ( std::size_t i = 0; i < prob->mFeatureCount; i++ ) /* for each feature into prob */
315 : : {
316 : 0 : std::unique_ptr< Feats > feat = std::move( features.front() );
317 : 0 : features.pop_front();
318 : :
319 : 0 : prob->mFeatStartId[i] = idlp;
320 : 0 : prob->mInactiveCost[i] = std::pow( 2, 10 - 10 * feat->priority );
321 : :
322 : 0 : std::size_t maxCandidates = 0;
323 : 0 : switch ( feat->feature->getGeosType() )
324 : : {
325 : : case GEOS_POINT:
326 : : // this is usually 0, i.e. no maximum
327 : 0 : maxCandidates = feat->feature->maximumPointCandidates();
328 : 0 : break;
329 : :
330 : : case GEOS_LINESTRING:
331 : 0 : maxCandidates = feat->feature->maximumLineCandidates();
332 : 0 : break;
333 : :
334 : : case GEOS_POLYGON:
335 : 0 : maxCandidates = std::max( static_cast< std::size_t >( 16 ), feat->feature->maximumPolygonCandidates() );
336 : 0 : break;
337 : : }
338 : :
339 : 0 : if ( isCanceled() )
340 : 0 : return nullptr;
341 : :
342 : 0 : auto pruneHardConflicts = [&]
343 : : {
344 : 0 : switch ( mPlacementVersion )
345 : : {
346 : : case QgsLabelingEngineSettings::PlacementEngineVersion1:
347 : 0 : break;
348 : :
349 : : case QgsLabelingEngineSettings::PlacementEngineVersion2:
350 : : {
351 : : // v2 placement rips out candidates where the candidate cost is too high when compared to
352 : : // their inactive cost
353 : :
354 : : // note, we start this at the SECOND candidate (you'll see why after this loop)
355 : 0 : feat->candidates.erase( std::remove_if( feat->candidates.begin() + 1, feat->candidates.end(), [ & ]( std::unique_ptr< LabelPosition > &candidate )
356 : : {
357 : 0 : if ( candidate->hasHardObstacleConflict() )
358 : : {
359 : 0 : return true;
360 : : }
361 : 0 : return false;
362 : 0 : } ), feat->candidates.end() );
363 : :
364 : 0 : if ( feat->candidates.size() == 1 && feat->candidates[ 0 ]->hasHardObstacleConflict() && !feat->feature->layer()->displayAll() )
365 : : {
366 : : // we've going to end up removing ALL candidates for this label. Oh well, that's allowed. We just need to
367 : : // make sure we move this last candidate to the unplaced labels list
368 : 0 : prob->positionsWithNoCandidates()->emplace_back( std::move( feat->candidates.front() ) );
369 : 0 : feat->candidates.clear();
370 : 0 : }
371 : : }
372 : 0 : }
373 : 0 : };
374 : :
375 : : // if we're not showing all labels (including conflicts) for this layer, then we prune the candidates
376 : : // upfront to avoid extra work...
377 : 0 : if ( !feat->feature->layer()->displayAll() )
378 : : {
379 : 0 : pruneHardConflicts();
380 : 0 : }
381 : :
382 : 0 : if ( feat->candidates.empty() )
383 : 0 : continue;
384 : :
385 : : // calculate final costs
386 : 0 : CostCalculator::finalizeCandidatesCosts( feat.get(), bbx, bby );
387 : :
388 : : // sort candidates list, best label to worst
389 : 0 : std::sort( feat->candidates.begin(), feat->candidates.end(), CostCalculator::candidateSortGrow );
390 : :
391 : : // but if we ARE showing all labels (including conflicts), let's go ahead and prune them now.
392 : : // Since we've calculated all their costs and sorted them, if we've hit the situation that ALL
393 : : // candidates have conflicts, then at least when we pick the first candidate to display it will be
394 : : // the lowest cost (i.e. best possible) overlapping candidate...
395 : 0 : if ( feat->feature->layer()->displayAll() )
396 : : {
397 : 0 : pruneHardConflicts();
398 : 0 : }
399 : :
400 : :
401 : : // only keep the 'maxCandidates' best candidates
402 : 0 : if ( maxCandidates > 0 && feat->candidates.size() > maxCandidates )
403 : : {
404 : 0 : feat->candidates.resize( maxCandidates );
405 : 0 : }
406 : :
407 : 0 : if ( isCanceled() )
408 : 0 : return nullptr;
409 : :
410 : : // update problem's # candidate
411 : 0 : prob->mFeatNbLp[i] = static_cast< int >( feat->candidates.size() );
412 : 0 : prob->mTotalCandidates += static_cast< int >( feat->candidates.size() );
413 : :
414 : : // add all candidates into a rtree (to speed up conflicts searching)
415 : 0 : for ( std::unique_ptr< LabelPosition > &candidate : feat->candidates )
416 : : {
417 : 0 : candidate->insertIntoIndex( prob->allCandidatesIndex() );
418 : 0 : candidate->setProblemIds( static_cast< int >( i ), idlp++ );
419 : : }
420 : 0 : features.emplace_back( std::move( feat ) );
421 : 0 : }
422 : :
423 : 0 : int nbOverlaps = 0;
424 : :
425 : : double amin[2];
426 : : double amax[2];
427 : 0 : while ( !features.empty() ) // for each feature
428 : : {
429 : 0 : if ( isCanceled() )
430 : 0 : return nullptr;
431 : :
432 : 0 : std::unique_ptr< Feats > feat = std::move( features.front() );
433 : 0 : features.pop_front();
434 : :
435 : 0 : for ( std::unique_ptr< LabelPosition > &candidate : feat->candidates )
436 : : {
437 : 0 : std::unique_ptr< LabelPosition > lp = std::move( candidate );
438 : :
439 : 0 : lp->resetNumOverlaps();
440 : :
441 : : // make sure that candidate's cost is less than 1
442 : 0 : lp->validateCost();
443 : :
444 : : //prob->feat[idlp] = j;
445 : :
446 : : // lookup for overlapping candidate
447 : 0 : lp->getBoundingBox( amin, amax );
448 : 0 : prob->allCandidatesIndex().intersects( QgsRectangle( amin[0], amin[1], amax[0], amax[1] ), [&lp]( const LabelPosition * lp2 )->bool
449 : : {
450 : 0 : if ( lp->isInConflict( lp2 ) )
451 : : {
452 : 0 : lp->incrementNumOverlaps();
453 : 0 : }
454 : :
455 : 0 : return true;
456 : :
457 : : } );
458 : :
459 : 0 : nbOverlaps += lp->getNumOverlaps();
460 : :
461 : 0 : prob->addCandidatePosition( std::move( lp ) );
462 : :
463 : 0 : if ( isCanceled() )
464 : 0 : return nullptr;
465 : 0 : }
466 : 0 : }
467 : 0 : nbOverlaps /= 2;
468 : 0 : prob->mAllNblp = prob->mTotalCandidates;
469 : 0 : prob->mNbOverlap = nbOverlaps;
470 : 0 : }
471 : :
472 : 0 : return prob;
473 : 0 : }
474 : :
475 : 0 : void Pal::registerCancellationCallback( Pal::FnIsCanceled fnCanceled, void *context )
476 : : {
477 : 0 : fnIsCanceled = fnCanceled;
478 : 0 : fnIsCanceledContext = context;
479 : 0 : }
480 : :
481 : 0 : std::unique_ptr<Problem> Pal::extractProblem( const QgsRectangle &extent, const QgsGeometry &mapBoundary )
482 : : {
483 : 0 : return extract( extent, mapBoundary );
484 : : }
485 : :
486 : 0 : QList<LabelPosition *> Pal::solveProblem( Problem *prob, bool displayAll, QList<LabelPosition *> *unlabeled )
487 : : {
488 : 0 : if ( !prob )
489 : 0 : return QList<LabelPosition *>();
490 : :
491 : 0 : prob->reduce();
492 : :
493 : : try
494 : : {
495 : 0 : prob->chain_search();
496 : 0 : }
497 : : catch ( InternalException::Empty & )
498 : : {
499 : 0 : return QList<LabelPosition *>();
500 : 0 : }
501 : :
502 : 0 : return prob->getSolution( displayAll, unlabeled );
503 : 0 : }
504 : :
505 : 0 : void Pal::setMinIt( int min_it )
506 : : {
507 : 0 : if ( min_it >= 0 )
508 : 0 : mTabuMinIt = min_it;
509 : 0 : }
510 : :
511 : 0 : void Pal::setMaxIt( int max_it )
512 : : {
513 : 0 : if ( max_it > 0 )
514 : 0 : mTabuMaxIt = max_it;
515 : 0 : }
516 : :
517 : 0 : void Pal::setPopmusicR( int r )
518 : : {
519 : 0 : if ( r > 0 )
520 : 0 : mPopmusicR = r;
521 : 0 : }
522 : :
523 : 0 : void Pal::setEjChainDeg( int degree )
524 : : {
525 : 0 : this->mEjChainDeg = degree;
526 : 0 : }
527 : :
528 : 0 : void Pal::setTenure( int tenure )
529 : : {
530 : 0 : this->mTenure = tenure;
531 : 0 : }
532 : :
533 : 0 : void Pal::setCandListSize( double fact )
534 : : {
535 : 0 : this->mCandListSize = fact;
536 : 0 : }
537 : :
538 : 0 : void Pal::setShowPartialLabels( bool show )
539 : : {
540 : 0 : this->mShowPartialLabels = show;
541 : 0 : }
542 : :
543 : 0 : QgsLabelingEngineSettings::PlacementEngineVersion Pal::placementVersion() const
544 : : {
545 : 0 : return mPlacementVersion;
546 : : }
547 : :
548 : 0 : void Pal::setPlacementVersion( QgsLabelingEngineSettings::PlacementEngineVersion placementVersion )
549 : : {
550 : 0 : mPlacementVersion = placementVersion;
551 : 0 : }
552 : :
553 : 0 : int Pal::getMinIt()
554 : : {
555 : 0 : return mTabuMaxIt;
556 : : }
557 : :
558 : 0 : int Pal::getMaxIt()
559 : : {
560 : 0 : return mTabuMinIt;
561 : : }
562 : :
563 : 0 : bool Pal::showPartialLabels() const
564 : : {
565 : 0 : return mShowPartialLabels;
566 : : }
|