Skip to content

Commit

Permalink
Merge pull request #2665 from wonder-sk/auto-trace
Browse files Browse the repository at this point in the history
[FEATURE] Tracing of features (digitizing)

Tracing can be now used in various capturing map tools (add feature, add part, ...) including reshape and split tools.

Tracing is simply a new mode for these tools - when tracing is not enabled, the tools work as usual. When tracing is enabled (by clicking the new magnet icon or pressing T key), tools switch to tracing behavior:
- first click on a vertex/edge (must be snapped!) will start tracing - moving mouse on top of the map continuously updates the trace
- next click will confirm the trace and mark start of a new trace Tracing can be enabled/disabled anytime even while digitizing one feature, so it is possible to digitize some parts of the feature with tracing enabled and other parts with tracing disabled.

Tracing respects snapping configuration for the list of traceable layers.

If there are too many features in map display, tracing is disabled to avoid potentially long tracing structure preparation and large memory overhead. After zooming in or disabling some layers the tracing is enabled again.

Internally, things work like this:
- when tracing is requested, linestrings are extracted from vector layers, then noded (using GEOSNode to resolve all intersections) and finally a simple planar graph is built (vertices + edges)
- when tracing, endpoints are temporarily added to the graph (if not equal to one of existing vertices already) and Dijkstra's algorithm is run to get shortest path

Original specs for the curious ones (the interaction with QGIS is slightly improved from what has been specified): http://www.lutraconsulting.co.uk/crowdfunding/autotrace-phase-2/specification.pdf
  • Loading branch information
wonder-sk committed Jan 13, 2016
2 parents b0bfa5f + 17f85f6 commit b83f6e3
Show file tree
Hide file tree
Showing 22 changed files with 2,214 additions and 11 deletions.
1 change: 1 addition & 0 deletions images/images.qrc
Expand Up @@ -535,6 +535,7 @@
<file>flags/zh.png</file>
<file>icons/qgis-icon-16x16_xmas.png</file>
<file>icons/qgis-icon-60x60_xmas.png</file>
<file>themes/default/mActionTracing.png</file>
</qresource>
<qresource prefix="/images/tips">
<file alias="symbol_levels.png">qgis_tips/symbol_levels.png</file>
Expand Down
Binary file added images/themes/default/mActionTracing.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
524 changes: 524 additions & 0 deletions images/themes/default/mActionTracing.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions python/core/core.sip
Expand Up @@ -120,6 +120,7 @@
%Include qgsstatisticalsummary.sip
%Include qgsstringutils.sip
%Include qgstolerance.sip
%Include qgstracer.sip
%Include qgsvectordataprovider.sip
%Include qgsvectorfilewriter.sip
%Include qgsvectorlayer.sip
Expand Down
13 changes: 11 additions & 2 deletions python/core/qgssnappingutils.sip
Expand Up @@ -59,15 +59,18 @@ class QgsSnappingUtils : QObject
/** Find out which strategy is used for indexing - by default hybrid indexing is used */
IndexingStrategy indexingStrategy() const;

/** Configure options used when the mode is snap to current layer */
/** Configure options used when the mode is snap to current layer or to all layers */
void setDefaultSettings( int type, double tolerance, QgsTolerance::UnitType unit );
/** Query options used when the mode is snap to current layer */
/** Query options used when the mode is snap to current layer or to all layers */
void defaultSettings( int& type /Out/, double& tolerance /Out/, QgsTolerance::UnitType& unit /Out/ );

struct LayerConfig
{
LayerConfig( QgsVectorLayer* l, const QgsPointLocator::Types& t, double tol, QgsTolerance::UnitType u );

bool operator==( const QgsSnappingUtils::LayerConfig& other ) const;
bool operator!=( const QgsSnappingUtils::LayerConfig& other ) const;

QgsVectorLayer* layer;
QgsPointLocator::Types type;
double tolerance;
Expand All @@ -88,6 +91,12 @@ class QgsSnappingUtils : QObject
/** Read snapping configuration from the project */
void readConfigFromProject();

signals:
/** Emitted when snapping configuration has been changed
* @note added in QGIS 2.14
*/
void configChanged();

protected:
//! Called when starting to index - can be overridden and e.g. progress dialog can be provided
virtual void prepareIndexStarting( int count );
Expand Down
74 changes: 74 additions & 0 deletions python/core/qgstracer.sip
@@ -0,0 +1,74 @@
/** \ingroup core
* Utility class that construct a planar graph from the input vector
* layers and provides shortest path search for tracing of existing
* features.
*
* @note added in QGIS 2.14
*/
class QgsTracer : QObject
{
%TypeHeaderCode
#include <qgstracer.h>
%End

public:
QgsTracer();
~QgsTracer();

//! Get layers used for tracing
QList<QgsVectorLayer*> layers() const;
//! Set layers used for tracing
void setLayers( const QList<QgsVectorLayer*>& layers );

//! Get CRS used for tracing
QgsCoordinateReferenceSystem destinationCrs() const;
//! Set CRS used for tracing
void setDestinationCrs( const QgsCoordinateReferenceSystem& crs );

//! Get extent to which graph's features will be limited (empty extent means no limit)
QgsRectangle extent() const;
//! Set extent to which graph's features will be limited (empty extent means no limit)
void setExtent( const QgsRectangle& extent );

//! Get maximum possible number of features in graph. If the number is exceeded, graph is not created.
int maxFeatureCount() const;
//! Get maximum possible number of features in graph. If the number is exceeded, graph is not created.
void setMaxFeatureCount( int count );

//! Build the internal data structures. This may take some time
//! depending on how big the input layers are. It is not necessary
//! to call this method explicitly - it will be called by findShortestPath()
//! if necessary.
bool init();

//! Whether the internal data structures have been initialized
bool isInitialized() const;

//! Possible errors that may happen when calling findShortestPath()
enum PathError
{
ErrNone, //!< No error
ErrTooManyFeatures, //!< Max feature count treshold was reached while reading features
ErrPoint1, //!< Start point cannot be joined to the graph
ErrPoint2, //!< End point cannot be joined to the graph
ErrNoPath, //!< Points are not connected in the graph
};

//! Given two points, find the shortest path and return points on the way.
//! The optional "error" argument may receive error code (PathError enum) if it is not null
//! @return array of points - trace of linestrings of other features (empty array one error)
QVector<QgsPoint> findShortestPath( const QgsPoint& p1, const QgsPoint& p2, QgsTracer::PathError* error /Out/ = nullptr );

//! Find out whether the point is snapped to a vertex or edge (i.e. it can be used for tracing start/stop)
bool isPointSnapped( const QgsPoint& pt );

protected:
//! Allows derived classes to setup the settings just before the tracer is initialized.
//! This allows the configuration to be set in a lazy way only when it is really necessary.
//! Default implementation does nothing.
virtual void configure();

protected slots:
//! Destroy the existing graph structure if any (de-initialize)
void invalidateGraph();
};
1 change: 1 addition & 0 deletions python/gui/gui.sip
Expand Up @@ -93,6 +93,7 @@
%Include qgsmapcanvasmap.sip
%Include qgsmapcanvassnapper.sip
%Include qgsmapcanvassnappingutils.sip
%Include qgsmapcanvastracer.sip
%Include qgsmaplayeractionregistry.sip
%Include qgsmaplayercombobox.sip
%Include qgsmaplayermodel.sip
Expand Down
38 changes: 38 additions & 0 deletions python/gui/qgsmapcanvastracer.sip
@@ -0,0 +1,38 @@
/** \ingroup gui
* Extension of QgsTracer that provides extra functionality:
* - automatic updates of own configuration based on canvas settings
* - reporting of issues to the user via message bar
*
* A simple registry of tracer instances associated to map canvas instances
* is kept for convenience. (Map tools do not need to create their local
* tracer instances and map canvas API is not "polluted" by this optional
* functionality).
*
* @note added in QGIS 2.14
*/
class QgsMapCanvasTracer : QgsTracer
{
%TypeHeaderCode
#include <qgsmapcanvastracer.h>
%End

public:
//! Create tracer associated with a particular map canvas, optionally message bar for reporting
explicit QgsMapCanvasTracer( QgsMapCanvas* canvas, QgsMessageBar* messageBar = 0 );
~QgsMapCanvasTracer();

//! Access to action that user may use to toggle tracing on/off
QAction* actionEnableTracing();

//! Retrieve instance of this class associated with given canvas (if any).
//! The class keeps a simple registry of tracers associated with map canvas
//! instances for easier access to the common tracer by various map tools
static QgsMapCanvasTracer* tracerForCanvas( QgsMapCanvas* canvas );

//! Report a path finding error to the user
void reportError( QgsTracer::PathError err, bool addingVertex );

protected:
//! Sets configuration from current snapping settings and canvas settings
virtual void configure();
};
12 changes: 12 additions & 0 deletions src/app/qgisapp.cpp
Expand Up @@ -164,6 +164,7 @@
#include "qgslogger.h"
#include "qgsmapcanvas.h"
#include "qgsmapcanvassnappingutils.h"
#include "qgsmapcanvastracer.h"
#include "qgsmaplayer.h"
#include "qgsmaplayerregistry.h"
#include "qgsmaplayerstyleguiutils.h"
Expand Down Expand Up @@ -556,6 +557,7 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, QWidget * parent,
, mComposerManager( nullptr )
, mpTileScaleWidget( nullptr )
, mpGpsWidget( nullptr )
, mTracer( nullptr )
, mSnappingUtils( nullptr )
, mProjectLastModified()
, mWelcomePage( nullptr )
Expand Down Expand Up @@ -1024,6 +1026,7 @@ QgisApp::QgisApp()
, mMacrosWarn( nullptr )
, mUserInputDockWidget( nullptr )
, mVectorLayerTools( nullptr )
, mTracer( nullptr )
, mActionFilterLegend( nullptr )
, mLegendExpressionFilterButton( nullptr )
, mSnappingUtils( nullptr )
Expand Down Expand Up @@ -1104,6 +1107,8 @@ QgisApp::~QgisApp()

delete mComposerManager;

delete mTracer;

delete mVectorLayerTools;
delete mWelcomePage;

Expand Down Expand Up @@ -1975,6 +1980,9 @@ void QgisApp::createToolBars()

// Cad toolbar
mAdvancedDigitizeToolBar->insertAction( mActionUndo, mAdvancedDigitizingDockWidget->enableAction() );

mTracer = new QgsMapCanvasTracer( mMapCanvas, messageBar() );
mAdvancedDigitizeToolBar->insertAction( mActionUndo, mTracer->actionEnableTracing() );
}

void QgisApp::createStatusBar()
Expand Down Expand Up @@ -9681,6 +9689,7 @@ void QgisApp::activateDeactivateLayerRelatedActions( QgsMapLayer* layer )
mActionMergeFeatures->setEnabled( false );
mActionMergeFeatureAttributes->setEnabled( false );
mActionRotatePointSymbols->setEnabled( false );
mTracer->actionEnableTracing()->setEnabled( false );

mActionPinLabels->setEnabled( false );
mActionShowHideLabels->setEnabled( false );
Expand Down Expand Up @@ -9801,6 +9810,9 @@ void QgisApp::activateDeactivateLayerRelatedActions( QgsMapLayer* layer )
mActionRotateFeature->setEnabled( isEditable && canChangeGeometry );
mActionNodeTool->setEnabled( isEditable && canChangeGeometry );

mTracer->actionEnableTracing()->setEnabled( isEditable && canAddFeatures &&
( vlayer->geometryType() == QGis::Line || vlayer->geometryType() == QGis::Polygon ) );

if ( vlayer->geometryType() == QGis::Point )
{
mActionAddFeature->setIcon( QgsApplication::getThemeIcon( "/mActionCapturePoint.svg" ) );
Expand Down
4 changes: 4 additions & 0 deletions src/app/qgisapp.h
Expand Up @@ -81,6 +81,7 @@ class QgsAdvancedDigitizingDockWidget;
class QgsSnappingDialog;
class QgsGPSInformationWidget;
class QgsStatisticalSummaryDockWidget;
class QgsMapCanvasTracer;

class QgsDecorationItem;

Expand Down Expand Up @@ -1698,6 +1699,9 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow

QgsVectorLayerTools* mVectorLayerTools;

//! A class that facilitates tracing of features
QgsMapCanvasTracer* mTracer;

QAction* mActionFilterLegend;

QgsLegendFilterButton* mLegendExpressionFilterButton;
Expand Down
3 changes: 3 additions & 0 deletions src/core/CMakeLists.txt
Expand Up @@ -184,6 +184,7 @@ SET(QGIS_CORE_SRCS
qgssqlexpressioncompiler.cpp
qgsstatisticalsummary.cpp
qgsstringutils.cpp
qgstracer.cpp
qgstransaction.cpp
qgstextlabelfeature.cpp
qgstolerance.cpp
Expand Down Expand Up @@ -455,6 +456,7 @@ SET(QGIS_CORE_MOC_HDRS
qgsrelationmanager.h
qgsrunprocess.h
qgssnappingutils.h
qgstracer.h
qgstransaction.h
qgsvectordataprovider.h
qgsvectorlayercache.h
Expand Down Expand Up @@ -660,6 +662,7 @@ SET(QGIS_CORE_HDRS
qgsstringutils.h
qgstextlabelfeature.h
qgstolerance.h
qgstracer.h

qgsvectordataprovider.h
qgsvectorlayercache.h
Expand Down
40 changes: 40 additions & 0 deletions src/core/qgssnappingutils.cpp
Expand Up @@ -369,15 +369,35 @@ void QgsSnappingUtils::setMapSettings( const QgsMapSettings& settings )
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 )
Expand All @@ -387,6 +407,25 @@ void QgsSnappingUtils::defaultSettings( int& type, double& tolerance, QgsToleran
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;
Expand Down Expand Up @@ -467,6 +506,7 @@ void QgsSnappingUtils::readConfigFromProject()
mLayers.append( LayerConfig( vlayer, t, tolIt->toDouble(), static_cast< QgsTolerance::UnitType >( tolUnitIt->toInt() ) ) );
}

emit configChanged();
}

void QgsSnappingUtils::onLayersWillBeRemoved( const QStringList& layerIds )
Expand Down

0 comments on commit b83f6e3

Please sign in to comment.