QGIS/src/core/qgssnappingutils.cpp
Martin Dobias 0ea6a3d15f Support different indexing strategies in QgsSnappingUtils
Huge layers will not use all the memory - at the expense of slow queries
2015-01-20 10:21:55 +07:00

405 lines
14 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 ( mStrategy == IndexAlwaysFull )
return locatorForLayer( vl );
else if ( mStrategy == IndexNeverFull )
return temporaryLocatorForLayer( vl, pointMap, tolerance );
else // Hybrid
{
if ( vl->pendingFeatureCount() > 100000 )
return temporaryLocatorForLayer( vl, pointMap, tolerance );
else
return locatorForLayer( vl );
}
}
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 );
}
static QgsPointLocator::Match _findClosestSegmentIntersection( const QgsPoint& pt, const QgsPointLocator::MatchList& segments )
{
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 _updateBestMatch( QgsPointLocator::Match& bestMatch, const QgsPoint& pointMap, QgsPointLocator* loc, int type, double tolerance, QgsPointLocator::MatchFilter* filter )
{
// when filter is used we can't use just the closest match (NN queries do not support filters)
// TODO: could be optimized with a new call nearestVertexInTolerance() / nearestEdgeInTolerance()
// so that we do not waste time gathering matches that are not needed
if ( type & QgsPointLocator::Vertex )
{
if ( filter )
{
QgsPointLocator::MatchList lst = loc->verticesInTolerance( pointMap, tolerance, filter );
if ( !lst.isEmpty() )
bestMatch.replaceIfBetter( lst.first(), tolerance );
}
else
bestMatch.replaceIfBetter( loc->nearestVertex( pointMap ), tolerance );
}
if ( type & QgsPointLocator::Edge )
{
if ( filter )
{
QgsPointLocator::MatchList lst = loc->edgesInTolerance( pointMap, tolerance, filter );
if ( !lst.isEmpty() )
bestMatch.replaceIfBetter( lst.first(), tolerance );
}
else
bestMatch.replaceIfBetter( loc->nearestEdge( pointMap ), 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();
// data from project
double tolerance = QgsTolerance::toleranceInMapUnits( mDefaultTolerance, mMapSettings, mDefaultUnit );
int type = mDefaultType;
// use ad-hoc locator
QgsPointLocator* loc = locatorForLayerUsingStrategy( mCurrentLayer, pointMap, tolerance );
if ( !loc )
return QgsPointLocator::Match();
loc->init( QgsPointLocator::Vertex | QgsPointLocator::Edge );
QgsPointLocator::Match bestMatch;
_updateBestMatch( bestMatch, pointMap, loc, type, tolerance, filter );
if ( mSnapOnIntersection )
{
QgsPointLocator* locEdges = locatorForLayerUsingStrategy( mCurrentLayer, pointMap, tolerance );
QgsPointLocator::MatchList edges = locEdges->edgesInTolerance( pointMap, tolerance );
bestMatch.replaceIfBetter( _findClosestSegmentIntersection( pointMap, edges ), tolerance );
}
return bestMatch;
}
else if ( mSnapToMapMode == SnapPerLayerConfig )
{
QgsPointLocator::Match bestMatch;
QgsPointLocator::MatchList edges; // for snap on intersection
double maxSnapIntTolerance = 0;
foreach ( const LayerConfig& layerConfig, mLayers )
{
double tolerance = QgsTolerance::toleranceInMapUnits( layerConfig.tolerance, mMapSettings, layerConfig.unit );
if ( QgsPointLocator* loc = locatorForLayerUsingStrategy( layerConfig.layer, pointMap, tolerance ) )
{
loc->init( layerConfig.type );
_updateBestMatch( bestMatch, pointMap, loc, layerConfig.type, tolerance, filter );
if ( mSnapOnIntersection )
{
edges << loc->edgesInTolerance( pointMap, tolerance );
maxSnapIntTolerance = qMax( maxSnapIntTolerance, tolerance );
}
}
}
if ( mSnapOnIntersection )
bestMatch.replaceIfBetter( _findClosestSegmentIntersection( pointMap, edges ), maxSnapIntTolerance );
return bestMatch;
}
return QgsPointLocator::Match();
}
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();
loc->init( type );
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 )
{
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", 0 );
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 // either "advanced" or empty (for background compatibility)
mSnapToMapMode = SnapPerLayerConfig;
// 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;
}
}
}
}