Branch data Line data Source code
1 : : /***************************************************************************
2 : : qgsspatialindex.cpp - wrapper class for spatial index library
3 : : ----------------------
4 : : begin : December 2006
5 : : copyright : (C) 2006 by Martin Dobias
6 : : email : wonder.sk at gmail dot com
7 : : ***************************************************************************
8 : : * *
9 : : * This program is free software; you can redistribute it and/or modify *
10 : : * it under the terms of the GNU General Public License as published by *
11 : : * the Free Software Foundation; either version 2 of the License, or *
12 : : * (at your option) any later version. *
13 : : * *
14 : : ***************************************************************************/
15 : :
16 : : #include "qgsspatialindex.h"
17 : :
18 : : #include "qgsgeometry.h"
19 : : #include "qgsfeature.h"
20 : : #include "qgsfeatureiterator.h"
21 : : #include "qgsrectangle.h"
22 : : #include "qgslogger.h"
23 : : #include "qgsfeaturesource.h"
24 : : #include "qgsfeedback.h"
25 : : #include "qgsspatialindexutils.h"
26 : :
27 : : #include <spatialindex/SpatialIndex.h>
28 : : #include <QMutex>
29 : : #include <QMutexLocker>
30 : :
31 : : using namespace SpatialIndex;
32 : :
33 : :
34 : :
35 : : /**
36 : : * \ingroup core
37 : : * \class QgisVisitor
38 : : * \brief Custom visitor that adds found features to list.
39 : : * \note not available in Python bindings
40 : : */
41 : 351 : class QgisVisitor : public SpatialIndex::IVisitor
42 : : {
43 : : public:
44 : 351 : explicit QgisVisitor( QList<QgsFeatureId> &list )
45 : 702 : : mList( list ) {}
46 : :
47 : 484 : void visitNode( const INode &n ) override
48 : 484 : { Q_UNUSED( n ) }
49 : :
50 : 699 : void visitData( const IData &d ) override
51 : : {
52 : 699 : mList.append( d.getIdentifier() );
53 : 699 : }
54 : :
55 : 0 : void visitData( std::vector<const IData *> &v ) override
56 : 0 : { Q_UNUSED( v ) }
57 : :
58 : : private:
59 : : QList<QgsFeatureId> &mList;
60 : : };
61 : :
62 : : /**
63 : : * \ingroup core
64 : : * \class QgsSpatialIndexCopyVisitor
65 : : * \note not available in Python bindings
66 : : */
67 : 0 : class QgsSpatialIndexCopyVisitor : public SpatialIndex::IVisitor
68 : : {
69 : : public:
70 : 0 : explicit QgsSpatialIndexCopyVisitor( SpatialIndex::ISpatialIndex *newIndex )
71 : 0 : : mNewIndex( newIndex ) {}
72 : :
73 : 0 : void visitNode( const INode &n ) override
74 : 0 : { Q_UNUSED( n ) }
75 : :
76 : 0 : void visitData( const IData &d ) override
77 : : {
78 : 0 : SpatialIndex::IShape *shape = nullptr;
79 : 0 : d.getShape( &shape );
80 : 0 : mNewIndex->insertData( 0, nullptr, *shape, d.getIdentifier() );
81 : 0 : delete shape;
82 : 0 : }
83 : :
84 : 0 : void visitData( std::vector<const IData *> &v ) override
85 : 0 : { Q_UNUSED( v ) }
86 : :
87 : : private:
88 : : SpatialIndex::ISpatialIndex *mNewIndex = nullptr;
89 : : };
90 : :
91 : : ///@cond PRIVATE
92 : 8 : class QgsNearestNeighborComparator : public INearestNeighborComparator
93 : : {
94 : : public:
95 : :
96 : 8 : QgsNearestNeighborComparator( const QHash< QgsFeatureId, QgsGeometry > *geometries, const QgsPointXY &point, double maxDistance )
97 : 8 : : mGeometries( geometries )
98 : 8 : , mGeom( QgsGeometry::fromPointXY( point ) )
99 : 8 : , mMaxDistance( maxDistance )
100 : 16 : {
101 : 8 : }
102 : :
103 : 0 : QgsNearestNeighborComparator( const QHash< QgsFeatureId, QgsGeometry > *geometries, const QgsGeometry &geometry, double maxDistance )
104 : 0 : : mGeometries( geometries )
105 : 0 : , mGeom( geometry )
106 : 0 : , mMaxDistance( maxDistance )
107 : 0 : {
108 : 0 : }
109 : :
110 : : const QHash< QgsFeatureId, QgsGeometry > *mGeometries = nullptr;
111 : : QgsGeometry mGeom;
112 : : double mMaxDistance = 0;
113 : : QSet< QgsFeatureId > mFeaturesOutsideMaxDistance;
114 : :
115 : 20 : double getMinimumDistance( const IShape &query, const IShape &entry ) override
116 : : {
117 : 20 : return query.getMinimumDistance( entry );
118 : : }
119 : :
120 : 60 : double getMinimumDistance( const IShape &query, const IData &data ) override
121 : : {
122 : : // start with the default implementation, which gives distance to bounding box only
123 : : IShape *pS;
124 : 60 : data.getShape( &pS );
125 : 60 : double dist = query.getMinimumDistance( *pS );
126 : 60 : delete pS;
127 : :
128 : : // if doing exact distance search, AND either no max distance specified OR the
129 : : // distance to the bounding box is less than the max distance, calculate the exact
130 : : // distance now.
131 : : // (note: if bounding box is already greater than the distance away then max distance, there's no
132 : : // point doing this more expensive calculation, since we can't possibly use this feature anyway!)
133 : 60 : if ( mGeometries && ( mMaxDistance <= 0.0 || dist <= mMaxDistance ) )
134 : : {
135 : 60 : QgsGeometry other = mGeometries->value( data.getIdentifier() );
136 : 60 : dist = other.distance( mGeom );
137 : 60 : }
138 : :
139 : 60 : if ( mMaxDistance > 0 && dist > mMaxDistance )
140 : : {
141 : : // feature is outside of maximum distance threshold. Flag it,
142 : : // but "trick" libspatialindex into considering it as just outside
143 : : // our max distance region. This means if there's no other closer features (i.e.,
144 : : // within our actual maximum search distance), the libspatialindex
145 : : // nearest neighbor test will use this feature and not needlessly continue hunting
146 : : // through the remaining more distant features in the index.
147 : : // TODO: add proper API to libspatialindex to allow a maximum search distance in
148 : : // nearest neighbor tests
149 : 0 : mFeaturesOutsideMaxDistance.insert( data.getIdentifier() );
150 : 0 : return mMaxDistance + 0.00000001;
151 : : }
152 : 60 : return dist;
153 : 60 : }
154 : : };
155 : :
156 : : /**
157 : : * \ingroup core
158 : : * \class QgsFeatureIteratorDataStream
159 : : * \brief Utility class for bulk loading of R-trees. Not a part of public API.
160 : : * \note not available in Python bindings
161 : : */
162 : : class QgsFeatureIteratorDataStream : public IDataStream
163 : : {
164 : : public:
165 : : //! constructor - needs to load all data to a vector for later access when bulk loading
166 : 14 : explicit QgsFeatureIteratorDataStream( const QgsFeatureIterator &fi, QgsFeedback *feedback = nullptr, QgsSpatialIndex::Flags flags = QgsSpatialIndex::Flags(),
167 : : const std::function< bool( const QgsFeature & ) > *callback = nullptr )
168 : 14 : : mFi( fi )
169 : 14 : , mFeedback( feedback )
170 : 14 : , mFlags( flags )
171 : 14 : , mCallback( callback )
172 : 28 : {
173 : 14 : readNextEntry();
174 : 14 : }
175 : :
176 : 14 : ~QgsFeatureIteratorDataStream() override
177 : 14 : {
178 : 14 : delete mNextData;
179 : 14 : }
180 : :
181 : : //! returns a pointer to the next entry in the stream or 0 at the end of the stream.
182 : 149 : IData *getNext() override
183 : : {
184 : 149 : if ( mFeedback && mFeedback->isCanceled() )
185 : 0 : return nullptr;
186 : :
187 : 149 : RTree::Data *ret = mNextData;
188 : 149 : mNextData = nullptr;
189 : 149 : readNextEntry();
190 : 149 : return ret;
191 : 149 : }
192 : :
193 : : //! returns true if there are more items in the stream.
194 : 191 : bool hasNext() override { return nullptr != mNextData; }
195 : :
196 : : //! returns the total number of entries available in the stream.
197 : 0 : uint32_t size() override { Q_ASSERT( false && "not available" ); return 0; }
198 : :
199 : : //! sets the stream pointer to the first entry, if possible.
200 : 0 : void rewind() override { Q_ASSERT( false && "not available" ); }
201 : :
202 : : QHash< QgsFeatureId, QgsGeometry > geometries;
203 : :
204 : : protected:
205 : 163 : void readNextEntry()
206 : : {
207 : 163 : QgsFeature f;
208 : 163 : SpatialIndex::Region r;
209 : : QgsFeatureId id;
210 : 163 : while ( mFi.nextFeature( f ) )
211 : : {
212 : 149 : if ( mCallback )
213 : : {
214 : 0 : bool res = ( *mCallback )( f );
215 : 0 : if ( !res )
216 : : {
217 : 0 : mNextData = nullptr;
218 : 0 : return;
219 : : }
220 : 0 : }
221 : 149 : if ( QgsSpatialIndex::featureInfo( f, r, id ) )
222 : : {
223 : 149 : mNextData = new RTree::Data( 0, nullptr, r, id );
224 : 149 : if ( mFlags & QgsSpatialIndex::FlagStoreFeatureGeometries )
225 : 0 : geometries.insert( f.id(), f.geometry() );
226 : 149 : return;
227 : : }
228 : : }
229 : 163 : }
230 : :
231 : : private:
232 : : QgsFeatureIterator mFi;
233 : 14 : RTree::Data *mNextData = nullptr;
234 : : QgsFeedback *mFeedback = nullptr;
235 : : QgsSpatialIndex::Flags mFlags = QgsSpatialIndex::Flags();
236 : : const std::function< bool( const QgsFeature & ) > *mCallback = nullptr;
237 : :
238 : : };
239 : :
240 : :
241 : : /**
242 : : * \ingroup core
243 : : * \class QgsSpatialIndexData
244 : : * \brief Data of spatial index that may be implicitly shared
245 : : * \note not available in Python bindings
246 : : */
247 : : class QgsSpatialIndexData : public QSharedData
248 : : {
249 : : public:
250 : 91 : QgsSpatialIndexData( QgsSpatialIndex::Flags flags )
251 : 91 : : mFlags( flags )
252 : 91 : {
253 : 91 : initTree();
254 : 91 : }
255 : :
256 : : QgsSpatialIndex::Flags mFlags = QgsSpatialIndex::Flags();
257 : :
258 : : QHash< QgsFeatureId, QgsGeometry > mGeometries;
259 : :
260 : : /**
261 : : * Constructor for QgsSpatialIndexData which bulk loads features from the specified feature iterator
262 : : * \a fi.
263 : : *
264 : : * The optional \a feedback object can be used to allow cancellation of bulk feature loading. Ownership
265 : : * of \a feedback is not transferred, and callers must take care that the lifetime of feedback exceeds
266 : : * that of the spatial index construction.
267 : : */
268 : 14 : explicit QgsSpatialIndexData( const QgsFeatureIterator &fi, QgsFeedback *feedback = nullptr, QgsSpatialIndex::Flags flags = QgsSpatialIndex::Flags(),
269 : : const std::function< bool( const QgsFeature & ) > *callback = nullptr )
270 : 14 : : mFlags( flags )
271 : 14 : {
272 : 14 : QgsFeatureIteratorDataStream fids( fi, feedback, mFlags, callback );
273 : 14 : initTree( &fids );
274 : 14 : if ( flags & QgsSpatialIndex::FlagStoreFeatureGeometries )
275 : 0 : mGeometries = fids.geometries;
276 : 14 : }
277 : :
278 : 0 : QgsSpatialIndexData( const QgsSpatialIndexData &other )
279 : 0 : : QSharedData( other )
280 : 0 : , mFlags( other.mFlags )
281 : 0 : , mGeometries( other.mGeometries )
282 : 0 : {
283 : 0 : QMutexLocker locker( &other.mMutex );
284 : :
285 : 0 : initTree();
286 : :
287 : : // copy R-tree data one by one (is there a faster way??)
288 : 0 : double low[] = { std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest() };
289 : 0 : double high[] = { std::numeric_limits<double>::max(), std::numeric_limits<double>::max() };
290 : 0 : SpatialIndex::Region query( low, high, 2 );
291 : 0 : QgsSpatialIndexCopyVisitor visitor( mRTree );
292 : 0 : other.mRTree->intersectsWithQuery( query, visitor );
293 : 0 : }
294 : :
295 : 102 : ~QgsSpatialIndexData()
296 : : {
297 : 102 : delete mRTree;
298 : 102 : delete mStorage;
299 : 102 : }
300 : :
301 : : QgsSpatialIndexData &operator=( const QgsSpatialIndexData &rh ) = delete;
302 : :
303 : 105 : void initTree( IDataStream *inputStream = nullptr )
304 : : {
305 : : // for now only memory manager
306 : 105 : mStorage = StorageManager::createNewMemoryStorageManager();
307 : :
308 : : // R-Tree parameters
309 : 105 : double fillFactor = 0.7;
310 : 105 : unsigned long indexCapacity = 10;
311 : 105 : unsigned long leafCapacity = 10;
312 : 105 : unsigned long dimension = 2;
313 : 105 : RTree::RTreeVariant variant = RTree::RV_RSTAR;
314 : :
315 : : // create R-tree
316 : : SpatialIndex::id_type indexId;
317 : :
318 : 105 : if ( inputStream && inputStream->hasNext() )
319 : 28 : mRTree = RTree::createAndBulkLoadNewRTree( RTree::BLM_STR, *inputStream, *mStorage, fillFactor, indexCapacity,
320 : 14 : leafCapacity, dimension, variant, indexId );
321 : : else
322 : 182 : mRTree = RTree::createNewRTree( *mStorage, fillFactor, indexCapacity,
323 : 91 : leafCapacity, dimension, variant, indexId );
324 : 105 : }
325 : :
326 : : //! Storage manager
327 : 105 : SpatialIndex::IStorageManager *mStorage = nullptr;
328 : :
329 : : //! R-tree containing spatial index
330 : 105 : SpatialIndex::ISpatialIndex *mRTree = nullptr;
331 : :
332 : : mutable QMutex mMutex;
333 : :
334 : : };
335 : :
336 : : ///@endcond
337 : :
338 : : // -------------------------------------------------------------------------
339 : :
340 : :
341 : 91 : QgsSpatialIndex::QgsSpatialIndex( QgsSpatialIndex::Flags flags )
342 : 182 : {
343 : 91 : d = new QgsSpatialIndexData( flags );
344 : 91 : }
345 : :
346 : 0 : QgsSpatialIndex::QgsSpatialIndex( const QgsFeatureIterator &fi, QgsFeedback *feedback, QgsSpatialIndex::Flags flags )
347 : 0 : {
348 : 0 : d = new QgsSpatialIndexData( fi, feedback, flags );
349 : 0 : }
350 : :
351 : : ///@cond PRIVATE // else throws a doxygen warning?
352 : 0 : QgsSpatialIndex::QgsSpatialIndex( const QgsFeatureIterator &fi, const std::function< bool( const QgsFeature & )> &callback, QgsSpatialIndex::Flags flags )
353 : 0 : {
354 : 0 : d = new QgsSpatialIndexData( fi, nullptr, flags, &callback );
355 : 0 : }
356 : : ///@endcond
357 : :
358 : 14 : QgsSpatialIndex::QgsSpatialIndex( const QgsFeatureSource &source, QgsFeedback *feedback, QgsSpatialIndex::Flags flags )
359 : 28 : {
360 : 14 : d = new QgsSpatialIndexData( source.getFeatures( QgsFeatureRequest().setNoAttributes() ), feedback, flags );
361 : 14 : }
362 : :
363 : 0 : QgsSpatialIndex::QgsSpatialIndex( const QgsSpatialIndex &other ) //NOLINT
364 : 0 : : d( other.d )
365 : 0 : {
366 : 0 : }
367 : :
368 : 103 : QgsSpatialIndex:: ~QgsSpatialIndex() //NOLINT
369 : 103 : {
370 : 103 : }
371 : :
372 : 22 : QgsSpatialIndex &QgsSpatialIndex::operator=( const QgsSpatialIndex &other )
373 : : {
374 : 22 : if ( this != &other )
375 : 22 : d = other.d;
376 : 22 : return *this;
377 : : }
378 : :
379 : 172 : bool QgsSpatialIndex::featureInfo( const QgsFeature &f, SpatialIndex::Region &r, QgsFeatureId &id )
380 : : {
381 : 172 : QgsRectangle rect;
382 : 172 : if ( !featureInfo( f, rect, id ) )
383 : 0 : return false;
384 : :
385 : 172 : r = QgsSpatialIndexUtils::rectangleToRegion( rect );
386 : 172 : return true;
387 : 172 : }
388 : :
389 : 1191 : bool QgsSpatialIndex::featureInfo( const QgsFeature &f, QgsRectangle &rect, QgsFeatureId &id )
390 : : {
391 : 1191 : if ( !f.hasGeometry() )
392 : 0 : return false;
393 : :
394 : 1191 : id = f.id();
395 : 1191 : rect = f.geometry().boundingBox();
396 : :
397 : 1191 : if ( !rect.isFinite() )
398 : 0 : return false;
399 : :
400 : 1191 : return true;
401 : 1191 : }
402 : :
403 : 1019 : bool QgsSpatialIndex::addFeature( QgsFeature &feature, QgsFeatureSink::Flags )
404 : : {
405 : 1019 : QgsRectangle rect;
406 : : QgsFeatureId id;
407 : 1019 : if ( !featureInfo( feature, rect, id ) )
408 : 0 : return false;
409 : :
410 : 1019 : if ( addFeature( id, rect ) )
411 : : {
412 : 1019 : if ( d->mFlags & QgsSpatialIndex::FlagStoreFeatureGeometries )
413 : : {
414 : 37 : QMutexLocker locker( &d->mMutex );
415 : 37 : d->mGeometries.insert( feature.id(), feature.geometry() );
416 : 37 : }
417 : 1019 : return true;
418 : : }
419 : 0 : return false;
420 : 1019 : }
421 : :
422 : 0 : bool QgsSpatialIndex::addFeatures( QgsFeatureList &features, QgsFeatureSink::Flags flags )
423 : : {
424 : 0 : QgsFeatureList::iterator fIt = features.begin();
425 : 0 : bool result = true;
426 : 0 : for ( ; fIt != features.end(); ++fIt )
427 : : {
428 : 0 : result = result && addFeature( *fIt, flags );
429 : 0 : }
430 : 0 : return result;
431 : : }
432 : :
433 : 0 : bool QgsSpatialIndex::insertFeature( const QgsFeature &f )
434 : : {
435 : 0 : QgsFeature feature( f );
436 : 0 : return addFeature( feature );
437 : 0 : }
438 : :
439 : 0 : bool QgsSpatialIndex::insertFeature( QgsFeatureId id, const QgsRectangle &bounds )
440 : : {
441 : 0 : return addFeature( id, bounds );
442 : : }
443 : :
444 : 1019 : bool QgsSpatialIndex::addFeature( QgsFeatureId id, const QgsRectangle &bounds )
445 : : {
446 : 1019 : SpatialIndex::Region r( QgsSpatialIndexUtils::rectangleToRegion( bounds ) );
447 : :
448 : 1019 : QMutexLocker locker( &d->mMutex );
449 : :
450 : : // TODO: handle possible exceptions correctly
451 : : try
452 : : {
453 : 1019 : d->mRTree->insertData( 0, nullptr, r, FID_TO_NUMBER( id ) );
454 : 1019 : return true;
455 : 0 : }
456 : : catch ( Tools::Exception &e )
457 : : {
458 : 0 : Q_UNUSED( e )
459 : 0 : QgsDebugMsg( QStringLiteral( "Tools::Exception caught: " ).arg( e.what().c_str() ) );
460 : 0 : }
461 : : catch ( const std::exception &e )
462 : : {
463 : 0 : Q_UNUSED( e )
464 : 0 : QgsDebugMsg( QStringLiteral( "std::exception caught: " ).arg( e.what() ) );
465 : 0 : }
466 : : catch ( ... )
467 : : {
468 : 0 : QgsDebugMsg( QStringLiteral( "unknown spatial index exception caught" ) );
469 : 0 : }
470 : :
471 : 0 : return false;
472 : 1019 : }
473 : :
474 : 23 : bool QgsSpatialIndex::deleteFeature( const QgsFeature &f )
475 : : {
476 : 23 : SpatialIndex::Region r;
477 : : QgsFeatureId id;
478 : 23 : if ( !featureInfo( f, r, id ) )
479 : 0 : return false;
480 : :
481 : 23 : QMutexLocker locker( &d->mMutex );
482 : : // TODO: handle exceptions
483 : 23 : if ( d->mFlags & QgsSpatialIndex::FlagStoreFeatureGeometries )
484 : 0 : d->mGeometries.remove( f.id() );
485 : 23 : return d->mRTree->deleteData( r, FID_TO_NUMBER( id ) );
486 : 23 : }
487 : :
488 : 343 : QList<QgsFeatureId> QgsSpatialIndex::intersects( const QgsRectangle &rect ) const
489 : : {
490 : 343 : QList<QgsFeatureId> list;
491 : 343 : QgisVisitor visitor( list );
492 : :
493 : 343 : SpatialIndex::Region r = QgsSpatialIndexUtils::rectangleToRegion( rect );
494 : :
495 : 343 : QMutexLocker locker( &d->mMutex );
496 : 343 : d->mRTree->intersectsWithQuery( r, visitor );
497 : :
498 : 343 : return list;
499 : 343 : }
500 : :
501 : 8 : QList<QgsFeatureId> QgsSpatialIndex::nearestNeighbor( const QgsPointXY &point, const int neighbors, const double maxDistance ) const
502 : : {
503 : 8 : QList<QgsFeatureId> list;
504 : 8 : QgisVisitor visitor( list );
505 : :
506 : 8 : double pt[2] = { point.x(), point.y() };
507 : 8 : Point p( pt, 2 );
508 : :
509 : 8 : QMutexLocker locker( &d->mMutex );
510 : 8 : QgsNearestNeighborComparator nnc( ( d->mFlags & QgsSpatialIndex::FlagStoreFeatureGeometries ) ? &d->mGeometries : nullptr,
511 : 8 : point, maxDistance );
512 : 8 : d->mRTree->nearestNeighborQuery( neighbors, p, visitor, nnc );
513 : :
514 : 8 : if ( maxDistance > 0 )
515 : : {
516 : : // trim features outside of max distance
517 : 0 : list.erase( std::remove_if( list.begin(), list.end(),
518 : 0 : [&nnc]( QgsFeatureId id )
519 : : {
520 : 0 : return nnc.mFeaturesOutsideMaxDistance.contains( id );
521 : 0 : } ), list.end() );
522 : 0 : }
523 : :
524 : 8 : return list;
525 : 8 : }
526 : :
527 : 0 : QList<QgsFeatureId> QgsSpatialIndex::nearestNeighbor( const QgsGeometry &geometry, int neighbors, double maxDistance ) const
528 : : {
529 : 0 : QList<QgsFeatureId> list;
530 : 0 : QgisVisitor visitor( list );
531 : :
532 : 0 : SpatialIndex::Region r = QgsSpatialIndexUtils::rectangleToRegion( geometry.boundingBox() );
533 : :
534 : 0 : QMutexLocker locker( &d->mMutex );
535 : 0 : QgsNearestNeighborComparator nnc( ( d->mFlags & QgsSpatialIndex::FlagStoreFeatureGeometries ) ? &d->mGeometries : nullptr,
536 : 0 : geometry, maxDistance );
537 : 0 : d->mRTree->nearestNeighborQuery( neighbors, r, visitor, nnc );
538 : :
539 : 0 : if ( maxDistance > 0 )
540 : : {
541 : : // trim features outside of max distance
542 : 0 : list.erase( std::remove_if( list.begin(), list.end(),
543 : 0 : [&nnc]( QgsFeatureId id )
544 : : {
545 : 0 : return nnc.mFeaturesOutsideMaxDistance.contains( id );
546 : 0 : } ), list.end() );
547 : 0 : }
548 : :
549 : 0 : return list;
550 : 0 : }
551 : :
552 : 8 : QgsGeometry QgsSpatialIndex::geometry( QgsFeatureId id ) const
553 : : {
554 : 8 : QMutexLocker locker( &d->mMutex );
555 : 8 : return d->mGeometries.value( id );
556 : 8 : }
557 : :
558 : 0 : QAtomicInt QgsSpatialIndex::refs() const
559 : : {
560 : 0 : return d->ref;
561 : : }
|