mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-26 00:02:43 -04:00
545 lines
17 KiB
C++
545 lines
17 KiB
C++
/***************************************************************************
|
|
qgssnappingutils.cpp
|
|
--------------------------------------
|
|
Date : November 2014
|
|
Copyright : (C) 2014 by Martin Dobias
|
|
Email : wonder dot sk at gmail dot com
|
|
***************************************************************************
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
#include "qgssnappingutils.h"
|
|
|
|
#include "qgsgeometry.h"
|
|
#include "qgsmaplayerregistry.h"
|
|
#include "qgsproject.h"
|
|
#include "qgsvectorlayer.h"
|
|
|
|
|
|
QgsSnappingUtils::QgsSnappingUtils( QObject* parent )
|
|
: QObject( parent )
|
|
, mCurrentLayer( nullptr )
|
|
, mSnapToMapMode( SnapCurrentLayer )
|
|
, mStrategy( IndexHybrid )
|
|
, mDefaultType( QgsPointLocator::Vertex )
|
|
, mDefaultTolerance( 10 )
|
|
, mDefaultUnit( QgsTolerance::Pixels )
|
|
, mSnapOnIntersection( false )
|
|
, mIsIndexing( false )
|
|
{
|
|
connect( QgsMapLayerRegistry::instance(), SIGNAL( layersWillBeRemoved( QStringList ) ), this, SLOT( onLayersWillBeRemoved( QStringList ) ) );
|
|
}
|
|
|
|
QgsSnappingUtils::~QgsSnappingUtils()
|
|
{
|
|
clearAllLocators();
|
|
}
|
|
|
|
|
|
QgsPointLocator* QgsSnappingUtils::locatorForLayer( QgsVectorLayer* vl )
|
|
{
|
|
if ( !vl )
|
|
return nullptr;
|
|
|
|
if ( !mLocators.contains( vl ) )
|
|
{
|
|
QgsPointLocator* vlpl = new QgsPointLocator( vl, destCRS() );
|
|
mLocators.insert( vl, vlpl );
|
|
}
|
|
return mLocators.value( vl );
|
|
}
|
|
|
|
void QgsSnappingUtils::clearAllLocators()
|
|
{
|
|
Q_FOREACH ( QgsPointLocator* vlpl, mLocators )
|
|
delete vlpl;
|
|
mLocators.clear();
|
|
|
|
Q_FOREACH ( QgsPointLocator* vlpl, mTemporaryLocators )
|
|
delete vlpl;
|
|
mTemporaryLocators.clear();
|
|
}
|
|
|
|
|
|
QgsPointLocator* QgsSnappingUtils::locatorForLayerUsingStrategy( QgsVectorLayer* vl, const QgsPoint& pointMap, double tolerance )
|
|
{
|
|
if ( willUseIndex( vl ) )
|
|
return locatorForLayer( vl );
|
|
else
|
|
return temporaryLocatorForLayer( vl, pointMap, tolerance );
|
|
}
|
|
|
|
QgsPointLocator* QgsSnappingUtils::temporaryLocatorForLayer( QgsVectorLayer* vl, const QgsPoint& pointMap, double tolerance )
|
|
{
|
|
if ( mTemporaryLocators.contains( vl ) )
|
|
delete mTemporaryLocators.take( vl );
|
|
|
|
QgsRectangle rect( pointMap.x() - tolerance, pointMap.y() - tolerance,
|
|
pointMap.x() + tolerance, pointMap.y() + tolerance );
|
|
QgsPointLocator* vlpl = new QgsPointLocator( vl, destCRS(), &rect );
|
|
mTemporaryLocators.insert( vl, vlpl );
|
|
return mTemporaryLocators.value( vl );
|
|
}
|
|
|
|
bool QgsSnappingUtils::willUseIndex( QgsVectorLayer* vl ) const
|
|
{
|
|
if ( vl->geometryType() == QGis::NoGeometry )
|
|
return false;
|
|
if ( mStrategy == IndexAlwaysFull )
|
|
return true;
|
|
else if ( mStrategy == IndexNeverFull )
|
|
return false;
|
|
else
|
|
{
|
|
if ( mHybridNonindexableLayers.contains( vl->id() ) )
|
|
return false;
|
|
|
|
// if the layer is too big, the locator will later stop indexing it after reaching a threshold
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
static QgsPointLocator::Match _findClosestSegmentIntersection( const QgsPoint& pt, const QgsPointLocator::MatchList& segments )
|
|
{
|
|
if ( segments.isEmpty() )
|
|
return QgsPointLocator::Match();
|
|
|
|
QSet<QgsPoint> endpoints;
|
|
|
|
// make a geometry
|
|
QList<QgsGeometry*> geoms;
|
|
Q_FOREACH ( const QgsPointLocator::Match& m, segments )
|
|
{
|
|
if ( m.hasEdge() )
|
|
{
|
|
QgsPolyline pl( 2 );
|
|
m.edgePoints( pl[0], pl[1] );
|
|
geoms << QgsGeometry::fromPolyline( pl );
|
|
endpoints << pl[0] << pl[1];
|
|
}
|
|
}
|
|
|
|
QgsGeometry* g = QgsGeometry::unaryUnion( geoms );
|
|
qDeleteAll( geoms );
|
|
|
|
// get intersection points
|
|
QList<QgsPoint> newPoints;
|
|
if ( g->wkbType() == QGis::WKBLineString )
|
|
{
|
|
Q_FOREACH ( const QgsPoint& p, g->asPolyline() )
|
|
{
|
|
if ( !endpoints.contains( p ) )
|
|
newPoints << p;
|
|
}
|
|
}
|
|
if ( g->wkbType() == QGis::WKBMultiLineString )
|
|
{
|
|
Q_FOREACH ( const QgsPolyline& pl, g->asMultiPolyline() )
|
|
{
|
|
Q_FOREACH ( const QgsPoint& p, pl )
|
|
{
|
|
if ( !endpoints.contains( p ) )
|
|
newPoints << p;
|
|
}
|
|
}
|
|
}
|
|
delete g;
|
|
|
|
if ( newPoints.isEmpty() )
|
|
return QgsPointLocator::Match();
|
|
|
|
// find the closest points
|
|
QgsPoint minP;
|
|
double minSqrDist = 1e20; // "infinity"
|
|
Q_FOREACH ( const QgsPoint& p, newPoints )
|
|
{
|
|
double sqrDist = pt.sqrDist( p.x(), p.y() );
|
|
if ( sqrDist < minSqrDist )
|
|
{
|
|
minSqrDist = sqrDist;
|
|
minP = p;
|
|
}
|
|
}
|
|
|
|
return QgsPointLocator::Match( QgsPointLocator::Vertex, nullptr, 0, sqrt( minSqrDist ), minP );
|
|
}
|
|
|
|
|
|
static void _replaceIfBetter( QgsPointLocator::Match& mBest, const QgsPointLocator::Match& mNew, double maxDistance )
|
|
{
|
|
// is other match relevant?
|
|
if ( !mNew.isValid() || mNew.distance() > maxDistance )
|
|
return;
|
|
|
|
// is other match actually better?
|
|
if ( mBest.isValid() && mBest.type() == mNew.type() && mBest.distance() - 10e-6 < mNew.distance() )
|
|
return;
|
|
|
|
// prefer vertex matches to edge matches (even if they are closer)
|
|
if ( mBest.type() == QgsPointLocator::Vertex && mNew.type() == QgsPointLocator::Edge )
|
|
return;
|
|
|
|
mBest = mNew; // the other match is better!
|
|
}
|
|
|
|
|
|
static void _updateBestMatch( QgsPointLocator::Match& bestMatch, const QgsPoint& pointMap, QgsPointLocator* loc, int type, double tolerance, QgsPointLocator::MatchFilter* filter )
|
|
{
|
|
if ( type & QgsPointLocator::Vertex )
|
|
{
|
|
_replaceIfBetter( bestMatch, loc->nearestVertex( pointMap, tolerance, filter ), tolerance );
|
|
}
|
|
if ( bestMatch.type() != QgsPointLocator::Vertex && ( type & QgsPointLocator::Edge ) )
|
|
{
|
|
_replaceIfBetter( bestMatch, loc->nearestEdge( pointMap, tolerance, filter ), tolerance );
|
|
}
|
|
}
|
|
|
|
|
|
QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QPoint& point, QgsPointLocator::MatchFilter* filter )
|
|
{
|
|
return snapToMap( mMapSettings.mapToPixel().toMapCoordinates( point ), filter );
|
|
}
|
|
|
|
QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPoint& pointMap, QgsPointLocator::MatchFilter* filter )
|
|
{
|
|
if ( !mMapSettings.hasValidSettings() )
|
|
return QgsPointLocator::Match();
|
|
|
|
if ( mSnapToMapMode == SnapCurrentLayer )
|
|
{
|
|
if ( !mCurrentLayer )
|
|
return QgsPointLocator::Match();
|
|
|
|
prepareIndex( QList<QgsVectorLayer*>() << mCurrentLayer );
|
|
|
|
// data from project
|
|
double tolerance = QgsTolerance::toleranceInProjectUnits( mDefaultTolerance, mCurrentLayer, mMapSettings, mDefaultUnit );
|
|
int type = mDefaultType;
|
|
|
|
// use ad-hoc locator
|
|
QgsPointLocator* loc = locatorForLayerUsingStrategy( mCurrentLayer, pointMap, tolerance );
|
|
if ( !loc )
|
|
return QgsPointLocator::Match();
|
|
|
|
QgsPointLocator::Match bestMatch;
|
|
_updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter );
|
|
|
|
if ( mSnapOnIntersection )
|
|
{
|
|
QgsPointLocator* locEdges = locatorForLayerUsingStrategy( mCurrentLayer, pointMap, tolerance );
|
|
QgsPointLocator::MatchList edges = locEdges->edgesInRect( pointMap, tolerance );
|
|
_replaceIfBetter( bestMatch, _findClosestSegmentIntersection( pointMap, edges ), tolerance );
|
|
}
|
|
|
|
return bestMatch;
|
|
}
|
|
else if ( mSnapToMapMode == SnapAdvanced )
|
|
{
|
|
QList<QgsVectorLayer*> layers;
|
|
Q_FOREACH ( const LayerConfig& layerConfig, mLayers )
|
|
layers << layerConfig.layer;
|
|
prepareIndex( layers );
|
|
|
|
QgsPointLocator::Match bestMatch;
|
|
QgsPointLocator::MatchList edges; // for snap on intersection
|
|
double maxSnapIntTolerance = 0;
|
|
|
|
Q_FOREACH ( const LayerConfig& layerConfig, mLayers )
|
|
{
|
|
double tolerance = QgsTolerance::toleranceInProjectUnits( layerConfig.tolerance, layerConfig.layer, mMapSettings, layerConfig.unit );
|
|
if ( QgsPointLocator* loc = locatorForLayerUsingStrategy( layerConfig.layer, pointMap, tolerance ) )
|
|
{
|
|
_updateBestMatch( bestMatch, pointMap, loc, layerConfig.type, tolerance, filter );
|
|
|
|
if ( mSnapOnIntersection )
|
|
{
|
|
edges << loc->edgesInRect( pointMap, tolerance );
|
|
maxSnapIntTolerance = qMax( maxSnapIntTolerance, tolerance );
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( mSnapOnIntersection )
|
|
_replaceIfBetter( bestMatch, _findClosestSegmentIntersection( pointMap, edges ), maxSnapIntTolerance );
|
|
|
|
return bestMatch;
|
|
}
|
|
else if ( mSnapToMapMode == SnapAllLayers )
|
|
{
|
|
// data from project
|
|
double tolerance = QgsTolerance::toleranceInProjectUnits( mDefaultTolerance, nullptr, mMapSettings, mDefaultUnit );
|
|
int type = mDefaultType;
|
|
|
|
QList<QgsVectorLayer*> layers;
|
|
Q_FOREACH ( const QString& layerID, mMapSettings.layers() )
|
|
if ( QgsVectorLayer* vl = qobject_cast<QgsVectorLayer*>( QgsMapLayerRegistry::instance()->mapLayer( layerID ) ) )
|
|
layers << vl;
|
|
prepareIndex( layers );
|
|
|
|
QgsPointLocator::MatchList edges; // for snap on intersection
|
|
QgsPointLocator::Match bestMatch;
|
|
|
|
Q_FOREACH ( QgsVectorLayer* vl, layers )
|
|
{
|
|
if ( QgsPointLocator* loc = locatorForLayerUsingStrategy( vl, pointMap, tolerance ) )
|
|
{
|
|
_updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter );
|
|
|
|
if ( mSnapOnIntersection )
|
|
edges << loc->edgesInRect( pointMap, tolerance );
|
|
}
|
|
}
|
|
|
|
if ( mSnapOnIntersection )
|
|
_replaceIfBetter( bestMatch, _findClosestSegmentIntersection( pointMap, edges ), tolerance );
|
|
|
|
return bestMatch;
|
|
}
|
|
|
|
return QgsPointLocator::Match();
|
|
}
|
|
|
|
|
|
void QgsSnappingUtils::prepareIndex( const QList<QgsVectorLayer*>& layers )
|
|
{
|
|
if ( mIsIndexing )
|
|
return;
|
|
mIsIndexing = true;
|
|
|
|
// check if we need to build any index
|
|
QList<QgsVectorLayer*> layersToIndex;
|
|
Q_FOREACH ( QgsVectorLayer* vl, layers )
|
|
{
|
|
if ( willUseIndex( vl ) && !locatorForLayer( vl )->hasIndex() )
|
|
layersToIndex << vl;
|
|
}
|
|
if ( !layersToIndex.isEmpty() )
|
|
{
|
|
// build indexes
|
|
QTime t;
|
|
t.start();
|
|
int i = 0;
|
|
prepareIndexStarting( layersToIndex.count() );
|
|
Q_FOREACH ( QgsVectorLayer* vl, layersToIndex )
|
|
{
|
|
QTime tt;
|
|
tt.start();
|
|
if ( !locatorForLayer( vl )->init( mStrategy == IndexHybrid ? 1000000 : -1 ) )
|
|
mHybridNonindexableLayers.insert( vl->id() );
|
|
QgsDebugMsg( QString( "Index init: %1 ms (%2)" ).arg( tt.elapsed() ).arg( vl->id() ) );
|
|
prepareIndexProgress( ++i );
|
|
}
|
|
QgsDebugMsg( QString( "Prepare index total: %1 ms" ).arg( t.elapsed() ) );
|
|
}
|
|
mIsIndexing = false;
|
|
}
|
|
|
|
|
|
QgsPointLocator::Match QgsSnappingUtils::snapToCurrentLayer( const QPoint& point, int type, QgsPointLocator::MatchFilter* filter )
|
|
{
|
|
if ( !mCurrentLayer )
|
|
return QgsPointLocator::Match();
|
|
|
|
QgsPoint pointMap = mMapSettings.mapToPixel().toMapCoordinates( point );
|
|
double tolerance = QgsTolerance::vertexSearchRadius( mMapSettings );
|
|
|
|
QgsPointLocator* loc = locatorForLayerUsingStrategy( mCurrentLayer, pointMap, tolerance );
|
|
if ( !loc )
|
|
return QgsPointLocator::Match();
|
|
|
|
QgsPointLocator::Match bestMatch;
|
|
_updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter );
|
|
return bestMatch;
|
|
}
|
|
|
|
void QgsSnappingUtils::setMapSettings( const QgsMapSettings& settings )
|
|
{
|
|
QString oldDestCRS = mMapSettings.hasCrsTransformEnabled() ? mMapSettings.destinationCrs().authid() : QString();
|
|
QString newDestCRS = settings.hasCrsTransformEnabled() ? settings.destinationCrs().authid() : QString();
|
|
mMapSettings = settings;
|
|
|
|
if ( newDestCRS != oldDestCRS )
|
|
clearAllLocators();
|
|
}
|
|
|
|
void QgsSnappingUtils::setCurrentLayer( QgsVectorLayer* layer )
|
|
{
|
|
mCurrentLayer = layer;
|
|
}
|
|
|
|
void QgsSnappingUtils::setSnapToMapMode( QgsSnappingUtils::SnapToMapMode mode )
|
|
{
|
|
if ( mSnapToMapMode == mode )
|
|
return;
|
|
|
|
mSnapToMapMode = mode;
|
|
emit configChanged();
|
|
}
|
|
|
|
void QgsSnappingUtils::setDefaultSettings( int type, double tolerance, QgsTolerance::UnitType unit )
|
|
{
|
|
// force map units - can't use layer units for just any layer
|
|
if ( unit == QgsTolerance::LayerUnits )
|
|
unit = QgsTolerance::ProjectUnits;
|
|
|
|
if ( mDefaultType == type && mDefaultTolerance == tolerance && mDefaultUnit == unit )
|
|
return;
|
|
|
|
mDefaultType = type;
|
|
mDefaultTolerance = tolerance;
|
|
mDefaultUnit = unit;
|
|
|
|
if ( mSnapToMapMode != SnapAdvanced ) // does not affect advanced mode
|
|
emit configChanged();
|
|
}
|
|
|
|
void QgsSnappingUtils::defaultSettings( int& type, double& tolerance, QgsTolerance::UnitType& unit )
|
|
{
|
|
type = mDefaultType;
|
|
tolerance = mDefaultTolerance;
|
|
unit = mDefaultUnit;
|
|
}
|
|
|
|
void QgsSnappingUtils::setLayers( const QList<QgsSnappingUtils::LayerConfig>& layers )
|
|
{
|
|
if ( mLayers == layers )
|
|
return;
|
|
|
|
mLayers = layers;
|
|
if ( mSnapToMapMode == SnapAdvanced ) // only affects advanced mode
|
|
emit configChanged();
|
|
}
|
|
|
|
void QgsSnappingUtils::setSnapOnIntersections( bool enabled )
|
|
{
|
|
if ( mSnapOnIntersection == enabled )
|
|
return;
|
|
|
|
mSnapOnIntersection = enabled;
|
|
emit configChanged();
|
|
}
|
|
|
|
const QgsCoordinateReferenceSystem* QgsSnappingUtils::destCRS()
|
|
{
|
|
return mMapSettings.hasCrsTransformEnabled() ? &mMapSettings.destinationCrs() : nullptr;
|
|
}
|
|
|
|
|
|
void QgsSnappingUtils::readConfigFromProject()
|
|
{
|
|
mSnapToMapMode = SnapCurrentLayer;
|
|
mLayers.clear();
|
|
|
|
QString snapMode = QgsProject::instance()->readEntry( "Digitizing", "/SnappingMode" );
|
|
|
|
int type = 0;
|
|
QString snapType = QgsProject::instance()->readEntry( "Digitizing", "/DefaultSnapType", QString( "off" ) );
|
|
if ( snapType == "to segment" )
|
|
type = QgsPointLocator::Edge;
|
|
else if ( snapType == "to vertex and segment" )
|
|
type = QgsPointLocator::Vertex | QgsPointLocator::Edge;
|
|
else if ( snapType == "to vertex" )
|
|
type = QgsPointLocator::Vertex;
|
|
double tolerance = QgsProject::instance()->readDoubleEntry( "Digitizing", "/DefaultSnapTolerance", 0 );
|
|
QgsTolerance::UnitType unit = static_cast< QgsTolerance::UnitType >( QgsProject::instance()->readNumEntry( "Digitizing", "/DefaultSnapToleranceUnit", QgsTolerance::ProjectUnits ) );
|
|
setDefaultSettings( type, tolerance, unit );
|
|
|
|
//snapping on intersection on?
|
|
setSnapOnIntersections( QgsProject::instance()->readNumEntry( "Digitizing", "/IntersectionSnapping", 0 ) );
|
|
|
|
//read snapping settings from project
|
|
bool snappingDefinedInProject, ok;
|
|
QStringList layerIdList = QgsProject::instance()->readListEntry( "Digitizing", "/LayerSnappingList", QStringList(), &snappingDefinedInProject );
|
|
QStringList enabledList = QgsProject::instance()->readListEntry( "Digitizing", "/LayerSnappingEnabledList", QStringList(), &ok );
|
|
QStringList toleranceList = QgsProject::instance()->readListEntry( "Digitizing", "/LayerSnappingToleranceList", QStringList(), &ok );
|
|
QStringList toleranceUnitList = QgsProject::instance()->readListEntry( "Digitizing", "/LayerSnappingToleranceUnitList", QStringList(), &ok );
|
|
QStringList snapToList = QgsProject::instance()->readListEntry( "Digitizing", "/LayerSnapToList", QStringList(), &ok );
|
|
|
|
// lists must have the same size, otherwise something is wrong
|
|
if ( layerIdList.size() != enabledList.size() ||
|
|
layerIdList.size() != toleranceList.size() ||
|
|
layerIdList.size() != toleranceUnitList.size() ||
|
|
layerIdList.size() != snapToList.size() )
|
|
return;
|
|
|
|
if ( !snappingDefinedInProject )
|
|
return; // nothing defined in project - use current layer
|
|
|
|
// Use snapping information from the project
|
|
if ( snapMode == "current_layer" )
|
|
mSnapToMapMode = SnapCurrentLayer;
|
|
else if ( snapMode == "all_layers" )
|
|
mSnapToMapMode = SnapAllLayers;
|
|
else // either "advanced" or empty (for background compatibility)
|
|
mSnapToMapMode = SnapAdvanced;
|
|
|
|
|
|
|
|
// load layers, tolerances, snap type
|
|
QStringList::const_iterator layerIt( layerIdList.constBegin() );
|
|
QStringList::const_iterator tolIt( toleranceList.constBegin() );
|
|
QStringList::const_iterator tolUnitIt( toleranceUnitList.constBegin() );
|
|
QStringList::const_iterator snapIt( snapToList.constBegin() );
|
|
QStringList::const_iterator enabledIt( enabledList.constBegin() );
|
|
for ( ; layerIt != layerIdList.constEnd(); ++layerIt, ++tolIt, ++tolUnitIt, ++snapIt, ++enabledIt )
|
|
{
|
|
// skip layer if snapping is not enabled
|
|
if ( *enabledIt != "enabled" )
|
|
continue;
|
|
|
|
QgsVectorLayer *vlayer = qobject_cast<QgsVectorLayer *>( QgsMapLayerRegistry::instance()->mapLayer( *layerIt ) );
|
|
if ( !vlayer || !vlayer->hasGeometryType() )
|
|
continue;
|
|
|
|
QgsPointLocator::Types t( *snapIt == "to_vertex" ? QgsPointLocator::Vertex :
|
|
( *snapIt == "to_segment" ? QgsPointLocator::Edge :
|
|
QgsPointLocator::Vertex | QgsPointLocator::Edge
|
|
)
|
|
);
|
|
mLayers.append( LayerConfig( vlayer, t, tolIt->toDouble(), static_cast< QgsTolerance::UnitType >( tolUnitIt->toInt() ) ) );
|
|
}
|
|
|
|
emit configChanged();
|
|
}
|
|
|
|
void QgsSnappingUtils::onLayersWillBeRemoved( const QStringList& layerIds )
|
|
{
|
|
// remove locators for layers that are going to be deleted
|
|
Q_FOREACH ( const QString& layerId, layerIds )
|
|
{
|
|
for ( LocatorsMap::iterator it = mLocators.begin(); it != mLocators.end(); )
|
|
{
|
|
if ( it.key()->id() == layerId )
|
|
{
|
|
delete it.value();
|
|
it = mLocators.erase( it );
|
|
}
|
|
else
|
|
{
|
|
++it;
|
|
}
|
|
}
|
|
|
|
for ( LocatorsMap::iterator it = mTemporaryLocators.begin(); it != mTemporaryLocators.end(); )
|
|
{
|
|
if ( it.key()->id() == layerId )
|
|
{
|
|
delete it.value();
|
|
it = mTemporaryLocators.erase( it );
|
|
}
|
|
else
|
|
{
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|