mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-16 00:03:12 -04:00
Until now "map units" in snapping config dialog actually referred to layer units. From now on, "map units" refer to project's CRS units. Where appropriate, it is possible to choose "layer units" that refer to layer CRS units. Projects from older versions of QGIS should be handled seamlessly (tolerances in layer units will be still handled in layer units, not project units). In API, QgsTolerance::MapUnits is deprecated as it is unclear to what units it refers to (should be ProjectUnits, but actually it is LayerUnits)
484 lines
16 KiB
C++
484 lines
16 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( 0 )
|
|
, mSnapToMapMode( SnapCurrentLayer )
|
|
, mStrategy( IndexHybrid )
|
|
, mDefaultType( QgsPointLocator::Vertex )
|
|
, mDefaultTolerance( 10 )
|
|
, mDefaultUnit( QgsTolerance::Pixels )
|
|
, mSnapOnIntersection( false )
|
|
{
|
|
connect( QgsMapLayerRegistry::instance(), SIGNAL( layersWillBeRemoved( QStringList ) ), this, SLOT( onLayersWillBeRemoved( QStringList ) ) );
|
|
}
|
|
|
|
QgsSnappingUtils::~QgsSnappingUtils()
|
|
{
|
|
clearAllLocators();
|
|
}
|
|
|
|
|
|
QgsPointLocator* QgsSnappingUtils::locatorForLayer( QgsVectorLayer* vl )
|
|
{
|
|
if ( !vl )
|
|
return 0;
|
|
|
|
if ( !mLocators.contains( vl ) )
|
|
{
|
|
QgsPointLocator* vlpl = new QgsPointLocator( vl, destCRS() );
|
|
mLocators.insert( vl, vlpl );
|
|
}
|
|
return mLocators.value( vl );
|
|
}
|
|
|
|
void QgsSnappingUtils::clearAllLocators()
|
|
{
|
|
foreach ( QgsPointLocator* vlpl, mLocators )
|
|
delete vlpl;
|
|
mLocators.clear();
|
|
|
|
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 ( 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;
|
|
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 )
|
|
{
|
|
foreach ( const QgsPoint& p, g->asPolyline() )
|
|
{
|
|
if ( !endpoints.contains( p ) )
|
|
newPoints << p;
|
|
}
|
|
}
|
|
if ( g->wkbType() == QGis::WKBMultiLineString )
|
|
{
|
|
foreach ( const QgsPolyline& pl, g->asMultiPolyline() )
|
|
{
|
|
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"
|
|
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, 0, 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 )
|
|
{
|
|
Q_ASSERT( mMapSettings.hasValidSettings() );
|
|
|
|
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;
|
|
foreach ( const LayerConfig& layerConfig, mLayers )
|
|
layers << layerConfig.layer;
|
|
prepareIndex( layers );
|
|
|
|
QgsPointLocator::Match bestMatch;
|
|
QgsPointLocator::MatchList edges; // for snap on intersection
|
|
double maxSnapIntTolerance = 0;
|
|
|
|
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, 0, mMapSettings, mDefaultUnit );
|
|
int type = mDefaultType;
|
|
|
|
QList<QgsVectorLayer*> layers;
|
|
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;
|
|
|
|
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 )
|
|
{
|
|
// check if we need to build any index
|
|
QList<QgsVectorLayer*> layersToIndex;
|
|
foreach ( QgsVectorLayer* vl, layers )
|
|
{
|
|
if ( willUseIndex( vl ) && !locatorForLayer( vl )->hasIndex() )
|
|
layersToIndex << vl;
|
|
}
|
|
if ( layersToIndex.isEmpty() )
|
|
return;
|
|
|
|
// build indexes
|
|
QTime t; t.start();
|
|
int i = 0;
|
|
prepareIndexStarting( layersToIndex.count() );
|
|
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() ) );
|
|
}
|
|
|
|
|
|
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::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;
|
|
|
|
mDefaultType = type;
|
|
mDefaultTolerance = tolerance;
|
|
mDefaultUnit = unit;
|
|
}
|
|
|
|
void QgsSnappingUtils::defaultSettings( int& type, double& tolerance, QgsTolerance::UnitType& unit )
|
|
{
|
|
type = mDefaultType;
|
|
tolerance = mDefaultTolerance;
|
|
unit = mDefaultUnit;
|
|
}
|
|
|
|
const QgsCoordinateReferenceSystem* QgsSnappingUtils::destCRS()
|
|
{
|
|
return mMapSettings.hasCrsTransformEnabled() ? &mMapSettings.destinationCrs() : 0;
|
|
}
|
|
|
|
|
|
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 = ( 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;
|
|
|
|
int t = ( *snapIt == "to_vertex" ? QgsPointLocator::Vertex :
|
|
( *snapIt == "to_segment" ? QgsPointLocator::Edge :
|
|
QgsPointLocator::Vertex | QgsPointLocator::Edge ) );
|
|
mLayers.append( LayerConfig( vlayer, t, tolIt->toDouble(), ( QgsTolerance::UnitType ) tolUnitIt->toInt() ) );
|
|
}
|
|
|
|
}
|
|
|
|
void QgsSnappingUtils::onLayersWillBeRemoved( QStringList layerIds )
|
|
{
|
|
// remove locators for layers that are going to be deleted
|
|
foreach ( QString layerId, layerIds )
|
|
{
|
|
for ( LocatorsMap::const_iterator it = mLocators.constBegin(); it != mLocators.constEnd(); ++it )
|
|
{
|
|
if ( it.key()->id() == layerId )
|
|
{
|
|
delete mLocators.take( it.key() );
|
|
continue;
|
|
}
|
|
}
|
|
|
|
for ( LocatorsMap::const_iterator it = mTemporaryLocators.constBegin(); it != mTemporaryLocators.constEnd(); ++it )
|
|
{
|
|
if ( it.key()->id() == layerId )
|
|
{
|
|
delete mTemporaryLocators.take( it.key() );
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|