diff --git a/images/images.qrc b/images/images.qrc
index 73b6a7af4f8..d3cddce7053 100644
--- a/images/images.qrc
+++ b/images/images.qrc
@@ -535,6 +535,7 @@
flags/zh.png
icons/qgis-icon-16x16_xmas.png
icons/qgis-icon-60x60_xmas.png
+ themes/default/mActionTracing.png
qgis_tips/symbol_levels.png
diff --git a/images/themes/default/mActionTracing.png b/images/themes/default/mActionTracing.png
new file mode 100644
index 00000000000..616d2797414
Binary files /dev/null and b/images/themes/default/mActionTracing.png differ
diff --git a/images/themes/default/mActionTracing.svg b/images/themes/default/mActionTracing.svg
new file mode 100644
index 00000000000..134946fc4b5
--- /dev/null
+++ b/images/themes/default/mActionTracing.svg
@@ -0,0 +1,524 @@
+
+
+
+
diff --git a/python/core/core.sip b/python/core/core.sip
index c28ece50215..e6d6251e25e 100644
--- a/python/core/core.sip
+++ b/python/core/core.sip
@@ -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
diff --git a/python/core/qgssnappingutils.sip b/python/core/qgssnappingutils.sip
index f5722eb5a82..4b9ca6311c9 100644
--- a/python/core/qgssnappingutils.sip
+++ b/python/core/qgssnappingutils.sip
@@ -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;
@@ -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 );
diff --git a/python/core/qgstracer.sip b/python/core/qgstracer.sip
new file mode 100644
index 00000000000..df31060d93c
--- /dev/null
+++ b/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
+%End
+
+ public:
+ QgsTracer();
+ ~QgsTracer();
+
+ //! Get layers used for tracing
+ QList layers() const;
+ //! Set layers used for tracing
+ void setLayers( const QList& 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 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();
+};
diff --git a/python/gui/gui.sip b/python/gui/gui.sip
index 7c1ad3007b2..8fee489e0dd 100644
--- a/python/gui/gui.sip
+++ b/python/gui/gui.sip
@@ -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
diff --git a/python/gui/qgsmapcanvastracer.sip b/python/gui/qgsmapcanvastracer.sip
new file mode 100644
index 00000000000..21a1b77e256
--- /dev/null
+++ b/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
+%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();
+};
diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp
index d8010aa2edd..8a794ce97a0 100644
--- a/src/app/qgisapp.cpp
+++ b/src/app/qgisapp.cpp
@@ -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"
@@ -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 )
@@ -1024,6 +1026,7 @@ QgisApp::QgisApp()
, mMacrosWarn( nullptr )
, mUserInputDockWidget( nullptr )
, mVectorLayerTools( nullptr )
+ , mTracer( nullptr )
, mActionFilterLegend( nullptr )
, mLegendExpressionFilterButton( nullptr )
, mSnappingUtils( nullptr )
@@ -1104,6 +1107,8 @@ QgisApp::~QgisApp()
delete mComposerManager;
+ delete mTracer;
+
delete mVectorLayerTools;
delete mWelcomePage;
@@ -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()
@@ -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 );
@@ -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" ) );
diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h
index c93bf37ff9c..f8fb7bd6b8e 100644
--- a/src/app/qgisapp.h
+++ b/src/app/qgisapp.h
@@ -81,6 +81,7 @@ class QgsAdvancedDigitizingDockWidget;
class QgsSnappingDialog;
class QgsGPSInformationWidget;
class QgsStatisticalSummaryDockWidget;
+class QgsMapCanvasTracer;
class QgsDecorationItem;
@@ -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;
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index b72d7458ab4..fc74076db36 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -184,6 +184,7 @@ SET(QGIS_CORE_SRCS
qgssqlexpressioncompiler.cpp
qgsstatisticalsummary.cpp
qgsstringutils.cpp
+ qgstracer.cpp
qgstransaction.cpp
qgstextlabelfeature.cpp
qgstolerance.cpp
@@ -455,6 +456,7 @@ SET(QGIS_CORE_MOC_HDRS
qgsrelationmanager.h
qgsrunprocess.h
qgssnappingutils.h
+ qgstracer.h
qgstransaction.h
qgsvectordataprovider.h
qgsvectorlayercache.h
@@ -660,6 +662,7 @@ SET(QGIS_CORE_HDRS
qgsstringutils.h
qgstextlabelfeature.h
qgstolerance.h
+ qgstracer.h
qgsvectordataprovider.h
qgsvectorlayercache.h
diff --git a/src/core/qgssnappingutils.cpp b/src/core/qgssnappingutils.cpp
index 05c9d0e43be..e0f6968331c 100644
--- a/src/core/qgssnappingutils.cpp
+++ b/src/core/qgssnappingutils.cpp
@@ -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 )
@@ -387,6 +407,25 @@ void QgsSnappingUtils::defaultSettings( int& type, double& tolerance, QgsToleran
unit = mDefaultUnit;
}
+void QgsSnappingUtils::setLayers( const QList& 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;
@@ -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 )
diff --git a/src/core/qgssnappingutils.h b/src/core/qgssnappingutils.h
index 34b76c1fb52..c6ab96c729b 100644
--- a/src/core/qgssnappingutils.h
+++ b/src/core/qgssnappingutils.h
@@ -64,7 +64,7 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
const QgsMapSettings& mapSettings() const { return mMapSettings; }
/** Set current layer so that if mode is SnapCurrentLayer we know which layer to use */
- void setCurrentLayer( QgsVectorLayer* layer ) { mCurrentLayer = layer; }
+ void setCurrentLayer( QgsVectorLayer* layer );
QgsVectorLayer* currentLayer() const { return mCurrentLayer; }
@@ -79,7 +79,7 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
};
/** Set how the snapping to map is done */
- void setSnapToMapMode( SnapToMapMode mode ) { mSnapToMapMode = mode; }
+ void setSnapToMapMode( SnapToMapMode mode );
/** Find out how the snapping to map is done */
SnapToMapMode snapToMapMode() const { return mSnapToMapMode; }
@@ -95,9 +95,9 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
/** Find out which strategy is used for indexing - by default hybrid indexing is used */
IndexingStrategy indexingStrategy() const { return mStrategy; }
- /** 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, double& tolerance, QgsTolerance::UnitType& unit );
/**
@@ -107,6 +107,15 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
{
LayerConfig( QgsVectorLayer* l, const QgsPointLocator::Types& t, double tol, QgsTolerance::UnitType u ) : layer( l ), type( t ), tolerance( tol ), unit( u ) {}
+ bool operator==( const LayerConfig& other ) const
+ {
+ return layer == other.layer && type == other.type && tolerance == other.tolerance && unit == other.unit;
+ }
+ bool operator!=( const LayerConfig& other ) const
+ {
+ return !operator==( other );
+ }
+
//! The layer to configure.
QgsVectorLayer* layer;
//! To which geometry properties of this layers a snapping should happen.
@@ -118,12 +127,12 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
};
/** Set layers which will be used for snapping */
- void setLayers( const QList& layers ) { mLayers = layers; }
+ void setLayers( const QList& layers );
/** Query layers used for snapping */
QList layers() const { return mLayers; }
/** Set whether to consider intersections of nearby segments for snapping */
- void setSnapOnIntersections( bool enabled ) { mSnapOnIntersection = enabled; }
+ void setSnapOnIntersections( bool enabled );
/** Query whether to consider intersections of nearby segments for snapping */
bool snapOnIntersections() const { return mSnapOnIntersection; }
@@ -131,6 +140,12 @@ class CORE_EXPORT QgsSnappingUtils : public 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 ) { Q_UNUSED( count ); }
diff --git a/src/core/qgstracer.cpp b/src/core/qgstracer.cpp
new file mode 100644
index 00000000000..ef4c3193cb3
--- /dev/null
+++ b/src/core/qgstracer.cpp
@@ -0,0 +1,676 @@
+/***************************************************************************
+ qgstracer.cpp
+ --------------------------------------
+ Date : January 2016
+ Copyright : (C) 2016 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 "qgstracer.h"
+
+#include "qgsgeometry.h"
+#include "qgsgeometryutils.h"
+#include "qgslogger.h"
+#include "qgsvectorlayer.h"
+
+#include
+#include
+
+typedef std::pair queue_item; // first = vertex index, second = distance
+
+// utility comparator for queue items based on distance
+struct comp
+{
+ bool operator()( const queue_item &a, const queue_item &b )
+ {
+ return a.second > b.second;
+ }
+};
+
+
+// TODO: move to geometry utils
+double distance_2d( const QgsPolyline& coords )
+{
+ int np = coords.count();
+ if ( np == 0 )
+ return 0;
+
+ double x0 = coords[0].x(), y0 = coords[0].y();
+ double x1, y1;
+ double dist = 0;
+ for ( int i = 1; i < np; ++i )
+ {
+ x1 = coords[i].x();
+ y1 = coords[i].y();
+ dist += sqrt(( x1 - x0 ) * ( x1 - x0 ) + ( y1 - y0 ) * ( y1 - y0 ) );
+ x0 = x1;
+ y0 = y1;
+ }
+ return dist;
+}
+
+
+// TODO: move to geometry utils
+double closest_segment( const QgsPolyline& pl, const QgsPoint& pt, int& vertexAfter, double epsilon )
+{
+ double sqrDist = std::numeric_limits::max();
+ const QgsPoint* pldata = pl.constData();
+ int plcount = pl.count();
+ double prevX = pldata[0].x(), prevY = pldata[0].y();
+ double segmentPtX, segmentPtY;
+ for ( int i = 1; i < plcount; ++i )
+ {
+ double currentX = pldata[i].x();
+ double currentY = pldata[i].y();
+ double testDist = QgsGeometryUtils::sqrDistToLine( pt.x(), pt.y(), prevX, prevY, currentX, currentY, segmentPtX, segmentPtY, epsilon );
+ if ( testDist < sqrDist )
+ {
+ sqrDist = testDist;
+ vertexAfter = i;
+ }
+ prevX = currentX;
+ prevY = currentY;
+ }
+ return sqrDist;
+}
+
+/////
+
+/** Simple graph structure for shortest path search */
+struct QgsTracerGraph
+{
+ struct E // bidirectional edge
+ {
+ //! vertices that the edge connects
+ int v1, v2;
+ //! coordinates of the edge (including endpoints)
+ QVector coords;
+
+ int other_vertex( int v0 ) const { return v1 == v0 ? v2 : v1; }
+ double weight() const { return distance_2d( coords ); }
+ };
+
+ struct V
+ {
+ //! location of the vertex
+ QgsPoint pt;
+ //! indices of adjacent edges (used in Dijkstra algorithm)
+ QVector edges;
+ };
+
+ //! Vertices of the graph
+ QVector v;
+ //! Edges of the graph
+ QVector e;
+
+ //! Temporarily removed edges
+ QSet inactiveEdges;
+ //! Temporarily added vertices (for each there are two extra edges)
+ int joinedVertices;
+};
+
+
+QgsTracerGraph* make_graph( const QVector& edges )
+{
+ QgsTracerGraph* g = new QgsTracerGraph;
+ g->joinedVertices = 0;
+ QHash point2vertex;
+
+ foreach ( const QgsPolyline& line, edges )
+ {
+ QgsPoint p1( line[0] );
+ QgsPoint p2( line[line.count() - 1] );
+
+ int v1 = -1, v2 = -1;
+ // get or add vertex 1
+ if ( point2vertex.contains( p1 ) )
+ v1 = point2vertex.value( p1 );
+ else
+ {
+ v1 = g->v.count();
+ QgsTracerGraph::V v;
+ v.pt = p1;
+ g->v.append( v );
+ point2vertex[p1] = v1;
+ }
+
+ // get or add vertex 2
+ if ( point2vertex.contains( p2 ) )
+ v2 = point2vertex.value( p2 );
+ else
+ {
+ v2 = g->v.count();
+ QgsTracerGraph::V v;
+ v.pt = p2;
+ g->v.append( v );
+ point2vertex[p2] = v2;
+ }
+
+ // add edge
+ QgsTracerGraph::E e;
+ e.v1 = v1;
+ e.v2 = v2;
+ e.coords = line;
+ g->e.append( e );
+
+ // link edge to vertices
+ int eIdx = g->e.count() - 1;
+ g->v[v1].edges << eIdx;
+ g->v[v2].edges << eIdx;
+ }
+
+ return g;
+}
+
+
+QVector shortest_path( const QgsTracerGraph& g, int v1, int v2 )
+{
+ if ( v1 == -1 || v2 == -1 )
+ return QVector(); // invalid input
+
+ // priority queue to drive Dijkstra:
+ // first of the pair is vertex index, second is distance
+ std::priority_queue< queue_item, std::vector< queue_item >, comp > Q;
+
+ // shortest distances to each vertex
+ QVector D( g.v.count(), std::numeric_limits::max() );
+ D[v1] = 0;
+
+ // whether vertices have been already processed
+ QVector F( g.v.count() );
+
+ // using which edge there is shortest path to each vertex
+ QVector S( g.v.count(), -1 );
+
+ int u = -1;
+ Q.push( queue_item( v1, 0 ) );
+
+ while ( !Q.empty() )
+ {
+ u = Q.top().first; // new vertex to visit
+ Q.pop();
+
+ if ( u == v2 )
+ break; // we can stop now, there won't be a shorter path
+
+ if ( F[u] )
+ continue; // ignore previously added path which is actually longer
+
+ const QgsTracerGraph::V& vu = g.v[u];
+ const int* vuEdges = vu.edges.constData();
+ int count = vu.edges.count();
+ for ( int i = 0; i < count; ++i )
+ {
+ const QgsTracerGraph::E& edge = g.e[ vuEdges[i] ];
+ int v = edge.other_vertex( u );
+ double w = edge.weight();
+ if ( !F[v] && D[u] + w < D[v] )
+ {
+ // found a shorter way to the vertex
+ D[v] = D[u] + w;
+ S[v] = vuEdges[i];
+ Q.push( queue_item( v, D[v] ) );
+ }
+ }
+ F[u] = 1; // mark the vertex as processed (we know the fastest path to it)
+ }
+
+ if ( u != v2 ) // there's no path to the end vertex
+ return QVector();
+
+ //qDebug("dist %f", D[u]);
+
+ QVector points;
+ QList path;
+ while ( S[u] != -1 )
+ {
+ path << S[u];
+ const QgsTracerGraph::E& e = g.e[S[u]];
+ QVector edgePoints = e.coords;
+ if ( edgePoints[0] != g.v[u].pt )
+ std::reverse( edgePoints.begin(), edgePoints.end() );
+ if ( !points.isEmpty() )
+ points.remove( points.count() - 1 ); // chop last one (will be used from next edge)
+ points << edgePoints;
+ u = e.other_vertex( u );
+ }
+
+ std::reverse( path.begin(), path.end() );
+ //foreach (int x, path)
+ // qDebug("e: %d", x);
+
+ std::reverse( points.begin(), points.end() );
+ return points;
+}
+
+
+int point2vertex( const QgsTracerGraph& g, const QgsPoint& pt, double epsilon = 1e-6 )
+{
+ // TODO: use spatial index
+
+ for ( int i = 0; i < g.v.count(); ++i )
+ {
+ const QgsTracerGraph::V& v = g.v.at( i );
+ if ( v.pt == pt || ( fabs( v.pt.x() - pt.x() ) < epsilon && fabs( v.pt.y() - pt.y() ) < epsilon ) )
+ return i;
+ }
+
+ return -1;
+}
+
+
+int point2edge( const QgsTracerGraph& g, const QgsPoint& pt, int& lineVertexAfter, double epsilon = 1e-6 )
+{
+ int vertexAfter;
+
+ for ( int i = 0; i < g.e.count(); ++i )
+ {
+ if ( g.inactiveEdges.contains( i ) )
+ continue; // ignore temporarily disabled edges
+
+ const QgsTracerGraph::E& e = g.e.at( i );
+ double dist = closest_segment( e.coords, pt, vertexAfter, epsilon );
+ if ( dist == 0 )
+ {
+ lineVertexAfter = vertexAfter;
+ return i;
+ }
+ }
+ return -1;
+}
+
+
+void split_linestring( const QgsPolyline& points, const QgsPoint& pt, int lineVertexAfter, QgsPolyline& pts1, QgsPolyline& pts2 )
+{
+ int count1 = lineVertexAfter;
+ int count2 = points.count() - lineVertexAfter;
+
+ for ( int i = 0; i < count1; ++i )
+ pts1 << points[i];
+ if ( points[lineVertexAfter-1] != pt )
+ pts1 << pt; // repeat if not split exactly at that point
+
+ if ( pt != points[lineVertexAfter] )
+ pts2 << pt; // repeat if not split exactly at that point
+ for ( int i = 0; i < count2; ++i )
+ pts2 << points[i + lineVertexAfter];
+}
+
+
+int join_vertex_to_graph( QgsTracerGraph& g, const QgsPoint& pt )
+{
+ // find edge where the point is
+ int lineVertexAfter;
+ int eIdx = point2edge( g, pt, lineVertexAfter );
+
+ //qDebug("e: %d", eIdx);
+
+ if ( eIdx == -1 )
+ return -1;
+
+ const QgsTracerGraph::E& e = g.e[eIdx];
+ QgsTracerGraph::V& v1 = g.v[e.v1];
+ QgsTracerGraph::V& v2 = g.v[e.v2];
+
+ QgsPolyline out1, out2;
+ split_linestring( e.coords, pt, lineVertexAfter, out1, out2 );
+
+ int vIdx = g.v.count();
+ int e1Idx = g.e.count();
+ int e2Idx = e1Idx + 1;
+
+ // prepare new vertex and edges
+
+ QgsTracerGraph::V v;
+ v.pt = pt;
+ v.edges << e1Idx << e2Idx;
+
+ QgsTracerGraph::E e1;
+ e1.v1 = e.v1;
+ e1.v2 = vIdx;
+ e1.coords = out1;
+
+ QgsTracerGraph::E e2;
+ e2.v1 = vIdx;
+ e2.v2 = e.v2;
+ e2.coords = out2;
+
+ // update edge connectivity of existing vertices
+ v1.edges.replace( v1.edges.indexOf( eIdx ), e1Idx );
+ v2.edges.replace( v2.edges.indexOf( eIdx ), e2Idx );
+ g.inactiveEdges << eIdx;
+
+ // add new vertex and edges to the graph
+ g.v.append( v );
+ g.e.append( e1 );
+ g.e.append( e2 );
+ g.joinedVertices++;
+
+ return vIdx;
+}
+
+
+int point_in_graph( QgsTracerGraph& g, const QgsPoint& pt )
+{
+ // try to use existing vertex in the graph
+ int v = point2vertex( g, pt );
+ if ( v != -1 )
+ return v;
+
+ // try to add the vertex to an edge (may fail if point is not on edge)
+ return join_vertex_to_graph( g, pt );
+}
+
+
+void reset_graph( QgsTracerGraph& g )
+{
+ // remove extra vertices and edges
+ g.v.resize( g.v.count() - g.joinedVertices );
+ g.e.resize( g.e.count() - g.joinedVertices * 2 );
+ g.joinedVertices = 0;
+
+ // fix vertices of deactivated edges
+ foreach ( int eIdx, g.inactiveEdges )
+ {
+ if ( eIdx >= g.e.count() )
+ continue;
+ const QgsTracerGraph::E& e = g.e[eIdx];
+ QgsTracerGraph::V& v1 = g.v[e.v1];
+ for ( int i = 0; i < v1.edges.count(); ++i )
+ {
+ if ( v1.edges[i] >= g.e.count() )
+ v1.edges.remove( i-- );
+ }
+ v1.edges << eIdx;
+
+ QgsTracerGraph::V& v2 = g.v[e.v2];
+ for ( int i = 0; i < v2.edges.count(); ++i )
+ {
+ if ( v2.edges[i] >= g.e.count() )
+ v2.edges.remove( i-- );
+ }
+ v2.edges << eIdx;
+ }
+
+ g.inactiveEdges.clear();
+}
+
+
+void extract_linework( QgsGeometry* g, QgsMultiPolyline& mpl )
+{
+ switch ( QgsWKBTypes::flatType( g->geometry()->wkbType() ) )
+ {
+ case QgsWKBTypes::LineString:
+ mpl << g->asPolyline();
+ break;
+
+ case QgsWKBTypes::Polygon:
+ foreach ( const QgsPolyline& ring, g->asPolygon() )
+ mpl << ring;
+ break;
+
+ case QgsWKBTypes::MultiLineString:
+ foreach ( const QgsPolyline& linestring, g->asMultiPolyline() )
+ mpl << linestring;
+ break;
+
+ case QgsWKBTypes::MultiPolygon:
+ foreach ( const QgsPolygon& polygon, g->asMultiPolygon() )
+ foreach ( const QgsPolyline& ring, polygon )
+ mpl << ring;
+ break;
+
+ default:
+ break; // unkown type - do nothing
+ }
+}
+
+// -------------
+
+
+QgsTracer::QgsTracer()
+ : mGraph( 0 )
+ , mMaxFeatureCount( 0 )
+{
+}
+
+
+bool QgsTracer::initGraph()
+{
+ if ( mGraph )
+ return true; // already initialized
+
+ QgsFeature f;
+ QgsMultiPolyline mpl;
+
+ // extract linestrings
+
+ // TODO: use QgsPointLocator as a source for the linework
+
+ QTime t1, t2, t2a, t3;
+
+ t1.start();
+ int featuresCounted = 0;
+ foreach ( QgsVectorLayer* vl, mLayers )
+ {
+ QgsCoordinateTransform ct( vl->crs(), mCRS );
+
+ QgsFeatureRequest request;
+ request.setSubsetOfAttributes( QgsAttributeList() );
+ if ( !mExtent.isEmpty() )
+ request.setFilterRect( ct.transformBoundingBox( mExtent, QgsCoordinateTransform::ReverseTransform ) );
+
+ QgsFeatureIterator fi = vl->getFeatures( request );
+ while ( fi.nextFeature( f ) )
+ {
+ if ( !f.geometry() )
+ continue;
+
+ if ( !ct.isShortCircuited() )
+ {
+ try
+ {
+ f.geometry()->transform( ct );
+ }
+ catch ( QgsCsException& )
+ {
+ continue; // ignore if the transform failed
+ }
+ }
+
+ extract_linework( f.geometry(), mpl );
+
+ ++featuresCounted;
+ if ( mMaxFeatureCount != 0 && featuresCounted >= mMaxFeatureCount )
+ return false;
+ }
+ }
+ int timeExtract = t1.elapsed();
+
+ // resolve intersections
+
+ t2.start();
+
+#if 0
+ // without noding - if data are known to be noded beforehand
+ int timeNodingCall = 0;
+#else
+ QgsGeometry* all_geom = QgsGeometry::fromMultiPolyline( mpl );
+
+ t2a.start();
+ GEOSGeometry* all_noded = GEOSNode_r( QgsGeometry::getGEOSHandler(), all_geom->asGeos() );
+ int timeNodingCall = t2a.elapsed();
+
+ QgsGeometry* noded = new QgsGeometry;
+ noded->fromGeos( all_noded );
+ delete all_geom;
+
+ mpl = noded->asMultiPolyline();
+
+ delete noded;
+#endif
+
+ int timeNoding = t2.elapsed();
+
+ t3.start();
+
+ mGraph = make_graph( mpl );
+
+ int timeMake = t3.elapsed();
+
+ Q_UNUSED( timeExtract );
+ Q_UNUSED( timeNoding );
+ Q_UNUSED( timeNodingCall );
+ Q_UNUSED( timeMake );
+ QgsDebugMsg( QString( "tracer extract %1 ms, noding %2 ms (call %3 ms), make %4 ms" )
+ .arg( timeExtract ).arg( timeNoding ).arg( timeNodingCall ).arg( timeMake ) );
+ return true;
+}
+
+QgsTracer::~QgsTracer()
+{
+ invalidateGraph();
+}
+
+void QgsTracer::setLayers( const QList& layers )
+{
+ if ( mLayers == layers )
+ return;
+
+ foreach ( QgsVectorLayer* layer, mLayers )
+ {
+ disconnect( layer, SIGNAL( featureAdded( QgsFeatureId ) ), this, SLOT( onFeatureAdded( QgsFeatureId ) ) );
+ disconnect( layer, SIGNAL( featureDeleted( QgsFeatureId ) ), this, SLOT( onFeatureDeleted( QgsFeatureId ) ) );
+ disconnect( layer, SIGNAL( geometryChanged( QgsFeatureId, QgsGeometry& ) ), this, SLOT( onGeometryChanged( QgsFeatureId, QgsGeometry& ) ) );
+ }
+
+ mLayers = layers;
+
+ foreach ( QgsVectorLayer* layer, mLayers )
+ {
+ connect( layer, SIGNAL( featureAdded( QgsFeatureId ) ), this, SLOT( onFeatureAdded( QgsFeatureId ) ) );
+ connect( layer, SIGNAL( featureDeleted( QgsFeatureId ) ), this, SLOT( onFeatureDeleted( QgsFeatureId ) ) );
+ connect( layer, SIGNAL( geometryChanged( QgsFeatureId, QgsGeometry& ) ), this, SLOT( onGeometryChanged( QgsFeatureId, QgsGeometry& ) ) );
+ }
+
+ invalidateGraph();
+}
+
+void QgsTracer::setDestinationCrs( const QgsCoordinateReferenceSystem& crs )
+{
+ if ( mCRS == crs )
+ return;
+
+ mCRS = crs;
+ invalidateGraph();
+}
+
+void QgsTracer::setExtent( const QgsRectangle& extent )
+{
+ if ( mExtent == extent )
+ return;
+
+ mExtent = extent;
+ invalidateGraph();
+}
+
+bool QgsTracer::init()
+{
+ if ( mGraph )
+ return true;
+
+ // configuration from derived class?
+ configure();
+
+ return initGraph();
+}
+
+
+void QgsTracer::invalidateGraph()
+{
+ delete mGraph;
+ mGraph = 0;
+}
+
+void QgsTracer::onFeatureAdded( QgsFeatureId fid )
+{
+ Q_UNUSED( fid );
+ invalidateGraph();
+}
+
+void QgsTracer::onFeatureDeleted( QgsFeatureId fid )
+{
+ Q_UNUSED( fid );
+ invalidateGraph();
+}
+
+void QgsTracer::onGeometryChanged( QgsFeatureId fid, QgsGeometry& geom )
+{
+ Q_UNUSED( fid );
+ Q_UNUSED( geom );
+ invalidateGraph();
+}
+
+QVector QgsTracer::findShortestPath( const QgsPoint& p1, const QgsPoint& p2, PathError* error )
+{
+ init(); // does nothing if the graph exists already
+ if ( !mGraph )
+ {
+ if ( error ) *error = ErrTooManyFeatures;
+ return QVector();
+ }
+
+ QTime t;
+ t.start();
+ int v1 = point_in_graph( *mGraph, p1 );
+ int v2 = point_in_graph( *mGraph, p2 );
+ int tPrep = t.elapsed();
+
+ if ( v1 == -1 )
+ {
+ if ( error ) *error = ErrPoint1;
+ return QVector();
+ }
+ if ( v2 == -1 )
+ {
+ if ( error ) *error = ErrPoint2;
+ return QVector();
+ }
+
+ QTime t2;
+ t2.start();
+ QgsPolyline points = shortest_path( *mGraph, v1, v2 );
+ int tPath = t2.elapsed();
+
+ Q_UNUSED( tPrep );
+ Q_UNUSED( tPath );
+ QgsDebugMsg( QString( "path timing: prep %1 ms, path %2 ms" ).arg( tPrep ).arg( tPath ) );
+
+ reset_graph( *mGraph );
+
+ if ( error )
+ *error = points.isEmpty() ? ErrNoPath : ErrNone;
+
+ return points;
+}
+
+bool QgsTracer::isPointSnapped( const QgsPoint& pt )
+{
+ init(); // does nothing if the graph exists already
+ if ( !mGraph )
+ return false;
+
+ if ( point2vertex( *mGraph, pt ) != -1 )
+ return true;
+
+ int lineVertexAfter;
+ int e = point2edge( *mGraph, pt, lineVertexAfter );
+ return e != -1;
+}
diff --git a/src/core/qgstracer.h b/src/core/qgstracer.h
new file mode 100644
index 00000000000..433eb2d4089
--- /dev/null
+++ b/src/core/qgstracer.h
@@ -0,0 +1,125 @@
+/***************************************************************************
+ qgstracer.h
+ --------------------------------------
+ Date : January 2016
+ Copyright : (C) 2016 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. *
+ * *
+ ***************************************************************************/
+
+#ifndef QGSTRACER_H
+#define QGSTRACER_H
+
+class QgsVectorLayer;
+
+#include
+#include
+
+#include "qgscoordinatereferencesystem.h"
+#include "qgsfeature.h"
+#include "qgspoint.h"
+#include "qgsrectangle.h"
+
+struct QgsTracerGraph;
+
+/** \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 CORE_EXPORT QgsTracer : public QObject
+{
+ Q_OBJECT
+ public:
+ QgsTracer();
+ ~QgsTracer();
+
+ //! Get layers used for tracing
+ QList layers() const { return mLayers; }
+ //! Set layers used for tracing
+ void setLayers( const QList& layers );
+
+ //! Get CRS used for tracing
+ QgsCoordinateReferenceSystem destinationCrs() const { return mCRS; }
+ //! 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 { return mExtent; }
+ //! 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 { return mMaxFeatureCount; }
+ //! Get maximum possible number of features in graph. If the number is exceeded, graph is not created.
+ void setMaxFeatureCount( int count ) { mMaxFeatureCount = 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 { return mGraph != nullptr; }
+
+ //! 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 findShortestPath( const QgsPoint& p1, const QgsPoint& p2, PathError* error = 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();
+
+ private:
+ bool initGraph();
+
+ private slots:
+ void onFeatureAdded( QgsFeatureId fid );
+ void onFeatureDeleted( QgsFeatureId fid );
+ void onGeometryChanged( QgsFeatureId fid, QgsGeometry& geom );
+
+ private:
+ //! Graph data structure for path searching
+ QgsTracerGraph* mGraph;
+ //! Input layers for the graph building
+ QList mLayers;
+ //! Destination CRS in which graph is built and tracing done
+ QgsCoordinateReferenceSystem mCRS;
+ //! Extent for graph building (empty extent means no limit)
+ QgsRectangle mExtent;
+ //! Limit of how many features can be in the graph (0 means no limit).
+ //! This is to avoid possibly long graph preparation for complicated layers
+ int mMaxFeatureCount;
+};
+
+
+#endif // QGSTRACER_H
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt
index 0a9474d7afa..375ac02c8a8 100644
--- a/src/gui/CMakeLists.txt
+++ b/src/gui/CMakeLists.txt
@@ -217,6 +217,7 @@ SET(QGIS_GUI_SRCS
qgsmapcanvasmap.cpp
qgsmapcanvassnapper.cpp
qgsmapcanvassnappingutils.cpp
+ qgsmapcanvastracer.cpp
qgsmaplayeractionregistry.cpp
qgsmaplayercombobox.cpp
qgsmaplayermodel.cpp
@@ -351,6 +352,7 @@ SET(QGIS_GUI_MOC_HDRS
qgsmanageconnectionsdialog.h
qgsmapcanvas.h
qgsmapcanvassnappingutils.h
+ qgsmapcanvastracer.h
qgsmaplayeractionregistry.h
qgsmaplayercombobox.h
qgsmaplayermodel.h
@@ -566,6 +568,7 @@ SET(QGIS_GUI_HDRS
qgsmapcanvasmap.h
qgsmapcanvassnapper.h
qgsmapcanvassnappingutils.h
+ qgsmapcanvastracer.h
qgsmaptip.h
qgsmapmouseevent.h
qgsnumericsortlistviewitem.h
diff --git a/src/gui/qgsmapcanvastracer.cpp b/src/gui/qgsmapcanvastracer.cpp
new file mode 100644
index 00000000000..783c20cc64a
--- /dev/null
+++ b/src/gui/qgsmapcanvastracer.cpp
@@ -0,0 +1,131 @@
+#include "qgsmapcanvastracer.h"
+
+#include "qgsapplication.h"
+#include "qgsmapcanvas.h"
+#include "qgsmaplayerregistry.h"
+#include "qgsmessagebar.h"
+#include "qgsmessagebaritem.h"
+#include "qgssnappingutils.h"
+#include "qgsvectorlayer.h"
+
+#include
+
+QHash QgsMapCanvasTracer::sTracers;
+
+
+QgsMapCanvasTracer::QgsMapCanvasTracer( QgsMapCanvas* canvas, QgsMessageBar* messageBar )
+ : mCanvas( canvas )
+ , mMessageBar( messageBar )
+ , mLastMessage( nullptr )
+{
+ sTracers.insert( canvas, this );
+
+ // when things change we just invalidate the graph - and set up new parameters again only when necessary
+ connect( canvas, SIGNAL( destinationCrsChanged() ), this, SLOT( invalidateGraph() ) );
+ connect( canvas, SIGNAL( layersChanged() ), this, SLOT( invalidateGraph() ) );
+ connect( canvas, SIGNAL( extentsChanged() ), this, SLOT( invalidateGraph() ) );
+ connect( canvas, SIGNAL( currentLayerChanged( QgsMapLayer* ) ), this, SLOT( onCurrentLayerChanged() ) );
+ connect( canvas->snappingUtils(), SIGNAL( configChanged() ), this, SLOT( invalidateGraph() ) );
+
+ mActionEnableTracing = new QAction( QIcon( QgsApplication::getThemeIcon( "/mActionTracing.png" ) ), tr( "Enable Tracing" ), this );
+ mActionEnableTracing->setShortcut( Qt::Key_T );
+ mActionEnableTracing->setCheckable( true );
+
+ // arbitrarily chosen limit that should allow for fairly fast initialization
+ // of the underlying graph structure
+ setMaxFeatureCount( QSettings().value( "/qgis/digitizing/tracing_max_feature_count", 10000 ).toInt() );
+}
+
+QgsMapCanvasTracer::~QgsMapCanvasTracer()
+{
+ sTracers.remove( mCanvas );
+}
+
+QgsMapCanvasTracer* QgsMapCanvasTracer::tracerForCanvas( QgsMapCanvas* canvas )
+{
+ return sTracers.value( canvas, 0 );
+}
+
+void QgsMapCanvasTracer::reportError( QgsTracer::PathError err, bool addingVertex )
+{
+ if ( !mMessageBar )
+ return;
+
+ // remove previous message (if any)
+ mMessageBar->popWidget( mLastMessage );
+ mLastMessage = nullptr;
+
+ QString message;
+ switch ( err )
+ {
+ case ErrTooManyFeatures:
+ message = tr( "Disabled - there are too many features displayed. Try zooming in or disable some layers." );
+ break;
+ case ErrPoint1:
+ message = tr( "The start point needs to be snapped and in the visible map view" );
+ break;
+ case ErrPoint2:
+ if ( addingVertex )
+ message = tr( "The end point needs to be snapped" );
+ break;
+ case ErrNoPath:
+ if ( addingVertex )
+ message = tr( "Endpoints are not connected" );
+ break;
+ case ErrNone:
+ default:
+ break;
+ }
+
+ if ( message.isEmpty() )
+ return;
+
+ mLastMessage = new QgsMessageBarItem( tr( "Tracing" ), message, QgsMessageBar::WARNING,
+ QSettings().value( "/qgis/messageTimeout", 5 ).toInt() );
+ mMessageBar->pushItem( mLastMessage );
+}
+
+void QgsMapCanvasTracer::configure()
+{
+ setDestinationCrs( mCanvas->mapSettings().destinationCrs() );
+ setExtent( mCanvas->extent() );
+
+ QList layers;
+ QStringList visibleLayerIds = mCanvas->mapSettings().layers();
+
+ switch ( mCanvas->snappingUtils()->snapToMapMode() )
+ {
+ default:
+ case QgsSnappingUtils::SnapCurrentLayer:
+ {
+ QgsVectorLayer* vl = qobject_cast( mCanvas->currentLayer() );
+ if ( vl && visibleLayerIds.contains( vl->id() ) )
+ layers << vl;
+ }
+ break;
+ case QgsSnappingUtils::SnapAllLayers:
+ foreach ( const QString& layerId, visibleLayerIds )
+ {
+ QgsVectorLayer* vl = qobject_cast( QgsMapLayerRegistry::instance()->mapLayer( layerId ) );
+ if ( vl )
+ layers << vl;
+ }
+ break;
+ case QgsSnappingUtils::SnapAdvanced:
+ foreach ( const QgsSnappingUtils::LayerConfig& cfg, mCanvas->snappingUtils()->layers() )
+ {
+ if ( visibleLayerIds.contains( cfg.layer->id() ) )
+ layers << cfg.layer;
+ }
+ break;
+ }
+
+ setLayers( layers );
+}
+
+void QgsMapCanvasTracer::onCurrentLayerChanged()
+{
+ // no need to bother if we are not snapping
+ if ( mCanvas->snappingUtils()->snapToMapMode() == QgsSnappingUtils::SnapCurrentLayer )
+ invalidateGraph();
+}
diff --git a/src/gui/qgsmapcanvastracer.h b/src/gui/qgsmapcanvastracer.h
new file mode 100644
index 00000000000..d4168c38838
--- /dev/null
+++ b/src/gui/qgsmapcanvastracer.h
@@ -0,0 +1,60 @@
+#ifndef QGSMAPCANVASTRACER_H
+#define QGSMAPCANVASTRACER_H
+
+#include "qgstracer.h"
+
+class QAction;
+class QgsMapCanvas;
+class QgsMessageBar;
+class QgsMessageBarItem;
+
+/** \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 GUI_EXPORT QgsMapCanvasTracer : public QgsTracer
+{
+ Q_OBJECT
+
+ 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() { return mActionEnableTracing; }
+
+ //! 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( PathError err, bool addingVertex );
+
+ protected:
+ //! Sets configuration from current snapping settings and canvas settings
+ virtual void configure();
+
+ private slots:
+ void onCurrentLayerChanged();
+
+ private:
+ QgsMapCanvas* mCanvas;
+ QgsMessageBar* mMessageBar;
+ QgsMessageBarItem* mLastMessage;
+
+ QAction* mActionEnableTracing;
+
+ static QHash sTracers;
+};
+
+#endif // QGSMAPCANVASTRACER_H
diff --git a/src/gui/qgsmaptoolcapture.cpp b/src/gui/qgsmaptoolcapture.cpp
index dde3b41d861..6c8c9cba39b 100644
--- a/src/gui/qgsmaptoolcapture.cpp
+++ b/src/gui/qgsmaptoolcapture.cpp
@@ -21,6 +21,7 @@
#include "qgslinestringv2.h"
#include "qgslogger.h"
#include "qgsmapcanvas.h"
+#include "qgsmapcanvastracer.h"
#include "qgsmapmouseevent.h"
#include "qgsmaprenderer.h"
#include "qgspolygonv2.h"
@@ -134,6 +135,124 @@ void QgsMapToolCapture::currentLayerChanged( QgsMapLayer *layer )
}
}
+
+bool QgsMapToolCapture::tracingEnabled()
+{
+ QgsMapCanvasTracer* tracer = QgsMapCanvasTracer::tracerForCanvas( mCanvas );
+ return tracer && tracer->actionEnableTracing()->isChecked();
+}
+
+
+QgsPoint QgsMapToolCapture::tracingStartPoint()
+{
+ try
+ {
+ QgsMapLayer* layer = mCanvas->currentLayer();
+ if ( !layer )
+ return QgsPoint();
+ QgsPointV2 v = mCaptureCurve.endPoint();
+ return toMapCoordinates( layer, QgsPoint( v.x(), v.y() ) );
+ }
+ catch ( QgsCsException & )
+ {
+ QgsDebugMsg( "transformation to layer coordinate failed" );
+ return QgsPoint();
+ }
+}
+
+
+void QgsMapToolCapture::tracingMouseMove( QgsMapMouseEvent* e )
+{
+ if ( !e->isSnapped() )
+ return;
+
+ QgsPoint pt0 = tracingStartPoint();
+ if ( pt0 == QgsPoint() )
+ return;
+
+ QgsMapCanvasTracer* tracer = QgsMapCanvasTracer::tracerForCanvas( mCanvas );
+ if ( !tracer )
+ return; // this should not happen!
+
+ mTempRubberBand->reset( mCaptureMode == CapturePolygon ? QGis::Polygon : QGis::Line );
+
+ QgsTracer::PathError err;
+ QVector points = tracer->findShortestPath( pt0, e->mapPoint(), &err );
+ if ( points.isEmpty() )
+ {
+ tracer->reportError( err, false );
+ return;
+ }
+
+ if ( mCaptureMode == CapturePolygon )
+ mTempRubberBand->addPoint( *mRubberBand->getPoint( 0, 0 ), false );
+
+ // update rubberband
+ for ( int i = 0; i < points.count(); ++i )
+ mTempRubberBand->addPoint( points.at( i ), i == points.count() - 1 );
+}
+
+
+bool QgsMapToolCapture::tracingAddVertex( const QgsPoint& point )
+{
+ QgsMapCanvasTracer* tracer = QgsMapCanvasTracer::tracerForCanvas( mCanvas );
+ if ( !tracer )
+ return false; // this should not happen!
+
+ if ( mCaptureCurve.numPoints() == 0 )
+ {
+ if ( !tracer->init() )
+ {
+ tracer->reportError( QgsTracer::ErrTooManyFeatures, true );
+ return false;
+ }
+
+ // only accept first point if it is snapped to the graph (to vertex or edge)
+ bool res = tracer->isPointSnapped( point );
+ if ( res )
+ {
+ QgsPoint layerPoint;
+ nextPoint( point, layerPoint ); // assuming the transform went fine earlier
+
+ mRubberBand->addPoint( point );
+ mCaptureCurve.addVertex( QgsPointV2( layerPoint.x(), layerPoint.y() ) );
+ }
+ return res;
+ }
+
+ QgsPoint pt0 = tracingStartPoint();
+ if ( pt0 == QgsPoint() )
+ return false;
+
+ QgsTracer::PathError err;
+ QVector points = tracer->findShortestPath( pt0, point, &err );
+ if ( points.isEmpty() )
+ {
+ tracer->reportError( err, true );
+ return false; // ignore the vertex - can't find path to the end point!
+ }
+
+ // transform points
+ QList layerPoints;
+ QgsPoint lp; // in layer coords
+ for ( int i = 1; i < points.count(); ++i )
+ {
+ if ( nextPoint( points[i], lp ) != 0 )
+ return false;
+ layerPoints << QgsPointV2( lp.x(), lp.y() );
+ }
+
+ for ( int i = 1; i < points.count(); ++i )
+ {
+ if ( points[i] == points[i-1] )
+ continue; // avoid duplicate vertices if there are any
+ mRubberBand->addPoint( points[i], i == points.count() - 1 );
+ mCaptureCurve.addVertex( layerPoints[i-1] );
+ }
+ return true;
+}
+
+
void QgsMapToolCapture::cadCanvasMoveEvent( QgsMapMouseEvent * e )
{
QgsMapToolAdvancedDigitizing::cadCanvasMoveEvent( e );
@@ -165,9 +284,30 @@ void QgsMapToolCapture::cadCanvasMoveEvent( QgsMapMouseEvent * e )
mTempRubberBand->addPoint( point );
}
+
if ( mCaptureMode != CapturePoint && mTempRubberBand && mCapturing )
{
- mTempRubberBand->movePoint( point );
+ if ( tracingEnabled() && mCaptureCurve.numPoints() != 0 )
+ {
+ tracingMouseMove( e );
+ }
+ else
+ {
+ if ( mCaptureCurve.numPoints() > 0 &&
+ (( mCaptureMode == CaptureLine && mTempRubberBand->numberOfVertices() != 2 ) ||
+ ( mCaptureMode == CapturePolygon && mTempRubberBand->numberOfVertices() != 3 ) ) )
+ {
+ // fix temporary rubber band after tracing which may have added multiple points
+ mTempRubberBand->reset( mCaptureMode == CapturePolygon ? QGis::Polygon : QGis::Line );
+ if ( mCaptureMode == CapturePolygon )
+ mTempRubberBand->addPoint( *mRubberBand->getPoint( 0, 0 ), false );
+ QgsPointV2 pt = mCaptureCurve.endPoint();
+ mTempRubberBand->addPoint( QgsPoint( pt.x(), pt.y() ) );
+ mTempRubberBand->addPoint( point );
+ }
+ else
+ mTempRubberBand->movePoint( point );
+ }
}
} // mouseMoveEvent
@@ -220,8 +360,6 @@ int QgsMapToolCapture::addVertex( const QgsPoint& point )
{
mRubberBand = createRubberBand( mCaptureMode == CapturePolygon ? QGis::Polygon : QGis::Line );
}
- mRubberBand->addPoint( point );
- mCaptureCurve.addVertex( QgsPointV2( layerPoint.x(), layerPoint.y() ) );
if ( !mTempRubberBand )
{
@@ -231,6 +369,20 @@ int QgsMapToolCapture::addVertex( const QgsPoint& point )
{
mTempRubberBand->reset( mCaptureMode == CapturePolygon ? QGis::Polygon : QGis::Line );
}
+
+ if ( tracingEnabled() )
+ {
+ bool res = tracingAddVertex( point );
+ if ( !res )
+ return 1; // early exit if the point cannot be accepted
+ }
+ else
+ {
+ // ordinary digitizing
+ mRubberBand->addPoint( point );
+ mCaptureCurve.addVertex( QgsPointV2( layerPoint.x(), layerPoint.y() ) );
+ }
+
if ( mCaptureMode == CaptureLine )
{
mTempRubberBand->addPoint( point );
diff --git a/src/gui/qgsmaptoolcapture.h b/src/gui/qgsmaptoolcapture.h
index 6c27daddcad..2eb6284c217 100644
--- a/src/gui/qgsmaptoolcapture.h
+++ b/src/gui/qgsmaptoolcapture.h
@@ -140,6 +140,16 @@ class GUI_EXPORT QgsMapToolCapture : public QgsMapToolAdvancedDigitizing
*/
void closePolygon();
+ private:
+ //! whether tracing has been requested by the user
+ bool tracingEnabled();
+ //! first point that will be used as a start of the trace
+ QgsPoint tracingStartPoint();
+ //! handle of mouse movement when tracing enabled and capturing has started
+ void tracingMouseMove( QgsMapMouseEvent* e );
+ //! handle of addition of clicked point (with the rest of the trace) when tracing enabled
+ bool tracingAddVertex( const QgsPoint& point );
+
private:
/** Flag to indicate a map canvas capture operation is taking place */
bool mCapturing;
diff --git a/tests/src/core/CMakeLists.txt b/tests/src/core/CMakeLists.txt
index 676687d9900..74cc46303cc 100644
--- a/tests/src/core/CMakeLists.txt
+++ b/tests/src/core/CMakeLists.txt
@@ -170,6 +170,7 @@ ADD_QGIS_TEST(stringutilstest testqgsstringutils.cpp)
ADD_QGIS_TEST(stylev2test testqgsstylev2.cpp)
ADD_QGIS_TEST(svgmarkertest testqgssvgmarker.cpp)
ADD_QGIS_TEST(symbolv2test testqgssymbolv2.cpp)
+ADD_QGIS_TEST(tracertest testqgstracer.cpp)
#for some obscure reason calling this test "fontutils" kills the build on Ubuntu 15.10
ADD_QGIS_TEST(typographicstylingutils testqgsfontutils.cpp)
ADD_QGIS_TEST(vectordataprovidertest testqgsvectordataprovider.cpp)
diff --git a/tests/src/core/testqgstracer.cpp b/tests/src/core/testqgstracer.cpp
new file mode 100644
index 00000000000..98c1cf6ae96
--- /dev/null
+++ b/tests/src/core/testqgstracer.cpp
@@ -0,0 +1,323 @@
+/***************************************************************************
+ testqgslayertree.cpp
+ --------------------------------------
+ Date : January 2016
+ Copyright : (C) 2016 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
+
+#include
+#include
+#include
+#include
+#include
+
+class TestQgsTracer : public QObject
+{
+ Q_OBJECT
+ public:
+ private slots:
+ void initTestCase();
+ void cleanupTestCase();
+ void testSimple();
+ void testPolygon();
+ void testButterfly();
+ void testLayerUpdates();
+ void testExtent();
+ void testReprojection();
+
+ private:
+
+};
+
+namespace QTest
+{
+ template<>
+ char* toString( const QgsPoint& point )
+ {
+ QByteArray ba = "QgsPoint(" + QByteArray::number( point.x() ) +
+ ", " + QByteArray::number( point.y() ) + ")";
+ return qstrdup( ba.data() );
+ }
+}
+
+static QgsFeature make_feature( const QString& wkt )
+{
+ QgsFeature f;
+ f.setGeometry( QgsGeometry::fromWkt( wkt ) );
+ return f;
+}
+
+static QgsVectorLayer* make_layer( const QStringList& wkts )
+{
+ QgsVectorLayer* vl = new QgsVectorLayer( "LineString", "x", "memory" );
+ Q_ASSERT( vl->isValid() );
+
+ vl->startEditing();
+ foreach ( const QString& wkt, wkts )
+ {
+ QgsFeature f( make_feature( wkt ) );
+ vl->addFeature( f, false );
+ }
+ vl->commitChanges();
+
+ return vl;
+}
+
+void print_shortest_path( QgsTracer& tracer, const QgsPoint& p1, const QgsPoint& p2 )
+{
+ qDebug( "from (%f,%f) to (%f,%f)", p1.x(), p1.y(), p2.x(), p2.y() );
+ QVector points = tracer.findShortestPath( p1, p2 );
+
+ if ( points.isEmpty() )
+ qDebug( "no path!" );
+
+ foreach ( QgsPoint p, points )
+ qDebug( "p: %f %f", p.x(), p.y() );
+}
+
+
+
+void TestQgsTracer::initTestCase()
+{
+ QgsApplication::init();
+ QgsApplication::initQgis();
+
+}
+
+void TestQgsTracer::cleanupTestCase()
+{
+ QgsApplication::exitQgis();
+}
+
+void TestQgsTracer::testSimple()
+{
+ QStringList wkts;
+ wkts << "LINESTRING(0 0, 0 10)"
+ << "LINESTRING(0 0, 10 0)"
+ << "LINESTRING(0 10, 20 10)"
+ << "LINESTRING(10 0, 20 10)";
+
+ /* This shape - nearly a square (one side is shifted to have exactly one shortest
+ * path between corners):
+ * 0,10 +----+ 20,10
+ * | /
+ * 0,0 +--+ 10,0
+ */
+
+ QgsVectorLayer* vl = make_layer( wkts );
+
+ QgsTracer tracer;
+ tracer.setLayers( QList() << vl );
+
+ QgsPolyline points1 = tracer.findShortestPath( QgsPoint( 0, 0 ), QgsPoint( 20, 10 ) );
+ QCOMPARE( points1.count(), 3 );
+ QCOMPARE( points1[0], QgsPoint( 0, 0 ) );
+ QCOMPARE( points1[1], QgsPoint( 10, 0 ) );
+ QCOMPARE( points1[2], QgsPoint( 20, 10 ) );
+
+ // one joined point
+ QgsPolyline points2 = tracer.findShortestPath( QgsPoint( 5, 10 ), QgsPoint( 0, 0 ) );
+ QCOMPARE( points2.count(), 3 );
+ QCOMPARE( points2[0], QgsPoint( 5, 10 ) );
+ QCOMPARE( points2[1], QgsPoint( 0, 10 ) );
+ QCOMPARE( points2[2], QgsPoint( 0, 0 ) );
+
+ // two joined points
+ QgsPolyline points3 = tracer.findShortestPath( QgsPoint( 0, 1 ), QgsPoint( 11, 1 ) );
+ QCOMPARE( points3.count(), 4 );
+ QCOMPARE( points3[0], QgsPoint( 0, 1 ) );
+ QCOMPARE( points3[1], QgsPoint( 0, 0 ) );
+ QCOMPARE( points3[2], QgsPoint( 10, 0 ) );
+ QCOMPARE( points3[3], QgsPoint( 11, 1 ) );
+
+ // two joined points on one line
+ QgsPolyline points4 = tracer.findShortestPath( QgsPoint( 11, 1 ), QgsPoint( 19, 9 ) );
+ QCOMPARE( points4.count(), 2 );
+ QCOMPARE( points4[0], QgsPoint( 11, 1 ) );
+ QCOMPARE( points4[1], QgsPoint( 19, 9 ) );
+
+ // no path to (1,1)
+ QgsPolyline points5 = tracer.findShortestPath( QgsPoint( 0, 0 ), QgsPoint( 1, 1 ) );
+ QCOMPARE( points5.count(), 0 );
+
+ delete vl;
+}
+
+void TestQgsTracer::testPolygon()
+{
+ // the same shape as in testSimple() but with just one polygon ring
+ // to check extraction from polygons work + routing along one ring works
+
+ QStringList wkts;
+ wkts << "POLYGON((0 0, 0 10, 20 10, 10 0, 0 0))";
+
+ QgsVectorLayer* vl = make_layer( wkts );
+
+ QgsTracer tracer;
+ tracer.setLayers( QList() << vl );
+
+ QgsPolyline points = tracer.findShortestPath( QgsPoint( 1, 0 ), QgsPoint( 0, 1 ) );
+ QCOMPARE( points.count(), 3 );
+ QCOMPARE( points[0], QgsPoint( 1, 0 ) );
+ QCOMPARE( points[1], QgsPoint( 0, 0 ) );
+ QCOMPARE( points[2], QgsPoint( 0, 1 ) );
+
+ delete vl;
+}
+
+void TestQgsTracer::testButterfly()
+{
+ // checks whether tracer internally splits linestrings at intersections
+
+ QStringList wkts;
+ wkts << "LINESTRING(0 0, 0 10, 10 0, 10 10, 0 0)";
+
+ /* This shape (without a vertex where the linestring crosses itself):
+ * + + 10,10
+ * |\/|
+ * |/\|
+ * + +
+ * 0,0
+ */
+
+ QgsVectorLayer* vl = make_layer( wkts );
+
+ QgsTracer tracer;
+ tracer.setLayers( QList() << vl );
+
+ QgsPolyline points = tracer.findShortestPath( QgsPoint( 0, 0 ), QgsPoint( 10, 0 ) );
+
+ QCOMPARE( points.count(), 3 );
+ QCOMPARE( points[0], QgsPoint( 0, 0 ) );
+ QCOMPARE( points[1], QgsPoint( 5, 5 ) );
+ QCOMPARE( points[2], QgsPoint( 10, 0 ) );
+
+ delete vl;
+}
+
+void TestQgsTracer::testLayerUpdates()
+{
+ // check whether the tracer is updated on added/removed/changed features
+
+ // same shape as in testSimple()
+ QStringList wkts;
+ wkts << "LINESTRING(0 0, 0 10)"
+ << "LINESTRING(0 0, 10 0)"
+ << "LINESTRING(0 10, 20 10)"
+ << "LINESTRING(10 0, 20 10)";
+
+ QgsVectorLayer* vl = make_layer( wkts );
+
+ QgsTracer tracer;
+ tracer.setLayers( QList() << vl );
+ tracer.init();
+
+ QgsPolyline points1 = tracer.findShortestPath( QgsPoint( 10, 0 ), QgsPoint( 10, 10 ) );
+ QCOMPARE( points1.count(), 3 );
+ QCOMPARE( points1[0], QgsPoint( 10, 0 ) );
+ QCOMPARE( points1[1], QgsPoint( 20, 10 ) );
+ QCOMPARE( points1[2], QgsPoint( 10, 10 ) );
+
+ vl->startEditing();
+
+ // add a shortcut
+ QgsFeature f( make_feature( "LINESTRING(10 0, 10 10)" ) );
+ vl->addFeature( f );
+
+ QgsPolyline points2 = tracer.findShortestPath( QgsPoint( 10, 0 ), QgsPoint( 10, 10 ) );
+ QCOMPARE( points2.count(), 2 );
+ QCOMPARE( points2[0], QgsPoint( 10, 0 ) );
+ QCOMPARE( points2[1], QgsPoint( 10, 10 ) );
+
+ // delete the shortcut
+ vl->deleteFeature( f.id() );
+
+ QgsPolyline points3 = tracer.findShortestPath( QgsPoint( 10, 0 ), QgsPoint( 10, 10 ) );
+ QCOMPARE( points3.count(), 3 );
+ QCOMPARE( points3[0], QgsPoint( 10, 0 ) );
+ QCOMPARE( points3[1], QgsPoint( 20, 10 ) );
+ QCOMPARE( points3[2], QgsPoint( 10, 10 ) );
+
+ // make the shortcut again from a different feature
+ QgsGeometry* g = QgsGeometry::fromWkt( "LINESTRING(10 0, 10 10)" );
+ vl->changeGeometry( 2, g ); // change bottom line (second item in wkts)
+ delete g;
+
+ QgsPolyline points4 = tracer.findShortestPath( QgsPoint( 10, 0 ), QgsPoint( 10, 10 ) );
+ QCOMPARE( points4.count(), 2 );
+ QCOMPARE( points4[0], QgsPoint( 10, 0 ) );
+ QCOMPARE( points4[1], QgsPoint( 10, 10 ) );
+
+ QgsPolyline points5 = tracer.findShortestPath( QgsPoint( 0, 0 ), QgsPoint( 10, 0 ) );
+ QCOMPARE( points5.count(), 4 );
+ QCOMPARE( points5[0], QgsPoint( 0, 0 ) );
+ QCOMPARE( points5[1], QgsPoint( 0, 10 ) );
+ QCOMPARE( points5[2], QgsPoint( 10, 10 ) );
+ QCOMPARE( points5[3], QgsPoint( 10, 0 ) );
+
+ vl->rollBack();
+
+ delete vl;
+}
+
+void TestQgsTracer::testExtent()
+{
+ // check whether the tracer correctly handles the extent limitation
+
+ // same shape as in testSimple()
+ QStringList wkts;
+ wkts << "LINESTRING(0 0, 0 10)"
+ << "LINESTRING(0 0, 10 0)"
+ << "LINESTRING(0 10, 20 10)"
+ << "LINESTRING(10 0, 20 10)";
+
+ QgsVectorLayer* vl = make_layer( wkts );
+
+ QgsTracer tracer;
+ tracer.setLayers( QList() << vl );
+ tracer.setExtent( QgsRectangle( 0, 0, 5, 5 ) );
+ tracer.init();
+
+ QgsPolyline points1 = tracer.findShortestPath( QgsPoint( 0, 0 ), QgsPoint( 10, 0 ) );
+ QCOMPARE( points1.count(), 2 );
+ QCOMPARE( points1[0], QgsPoint( 0, 0 ) );
+ QCOMPARE( points1[1], QgsPoint( 10, 0 ) );
+
+ QgsPolyline points2 = tracer.findShortestPath( QgsPoint( 0, 0 ), QgsPoint( 20, 10 ) );
+ QCOMPARE( points2.count(), 0 );
+}
+
+void TestQgsTracer::testReprojection()
+{
+ QStringList wkts;
+ wkts << "LINESTRING(1 0, 2 0)";
+
+ QgsVectorLayer* vl = make_layer( wkts );
+
+ QgsCoordinateReferenceSystem dstCrs( "EPSG:3857" );
+ QgsCoordinateTransform ct( QgsCoordinateReferenceSystem( "EPSG:4326" ), dstCrs );
+ QgsPoint p1 = ct.transform( QgsPoint( 1, 0 ) );
+ QgsPoint p2 = ct.transform( QgsPoint( 2, 0 ) );
+
+ QgsTracer tracer;
+ tracer.setLayers( QList() << vl );
+ tracer.setDestinationCrs( dstCrs );
+ tracer.init();
+
+ QgsPolyline points1 = tracer.findShortestPath( p1, p2 );
+ QCOMPARE( points1.count(), 2 );
+}
+
+
+QTEST_MAIN( TestQgsTracer )
+#include "testqgstracer.moc"