Skip to content

Commit b83f6e3

Browse files
committedJan 13, 2016
Merge pull request #2665 from wonder-sk/auto-trace
[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
2 parents b0bfa5f + 17f85f6 commit b83f6e3

22 files changed

+2214
-11
lines changed
 

‎images/images.qrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,7 @@
535535
<file>flags/zh.png</file>
536536
<file>icons/qgis-icon-16x16_xmas.png</file>
537537
<file>icons/qgis-icon-60x60_xmas.png</file>
538+
<file>themes/default/mActionTracing.png</file>
538539
</qresource>
539540
<qresource prefix="/images/tips">
540541
<file alias="symbol_levels.png">qgis_tips/symbol_levels.png</file>
1.14 KB
Loading

‎images/themes/default/mActionTracing.svg

Lines changed: 524 additions & 0 deletions
Loading

‎python/core/core.sip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
%Include qgsstatisticalsummary.sip
121121
%Include qgsstringutils.sip
122122
%Include qgstolerance.sip
123+
%Include qgstracer.sip
123124
%Include qgsvectordataprovider.sip
124125
%Include qgsvectorfilewriter.sip
125126
%Include qgsvectorlayer.sip

‎python/core/qgssnappingutils.sip

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,18 @@ class QgsSnappingUtils : QObject
5959
/** Find out which strategy is used for indexing - by default hybrid indexing is used */
6060
IndexingStrategy indexingStrategy() const;
6161

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

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

71+
bool operator==( const QgsSnappingUtils::LayerConfig& other ) const;
72+
bool operator!=( const QgsSnappingUtils::LayerConfig& other ) const;
73+
7174
QgsVectorLayer* layer;
7275
QgsPointLocator::Types type;
7376
double tolerance;
@@ -88,6 +91,12 @@ class QgsSnappingUtils : QObject
8891
/** Read snapping configuration from the project */
8992
void readConfigFromProject();
9093

94+
signals:
95+
/** Emitted when snapping configuration has been changed
96+
* @note added in QGIS 2.14
97+
*/
98+
void configChanged();
99+
91100
protected:
92101
//! Called when starting to index - can be overridden and e.g. progress dialog can be provided
93102
virtual void prepareIndexStarting( int count );

‎python/core/qgstracer.sip

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/** \ingroup core
2+
* Utility class that construct a planar graph from the input vector
3+
* layers and provides shortest path search for tracing of existing
4+
* features.
5+
*
6+
* @note added in QGIS 2.14
7+
*/
8+
class QgsTracer : QObject
9+
{
10+
%TypeHeaderCode
11+
#include <qgstracer.h>
12+
%End
13+
14+
public:
15+
QgsTracer();
16+
~QgsTracer();
17+
18+
//! Get layers used for tracing
19+
QList<QgsVectorLayer*> layers() const;
20+
//! Set layers used for tracing
21+
void setLayers( const QList<QgsVectorLayer*>& layers );
22+
23+
//! Get CRS used for tracing
24+
QgsCoordinateReferenceSystem destinationCrs() const;
25+
//! Set CRS used for tracing
26+
void setDestinationCrs( const QgsCoordinateReferenceSystem& crs );
27+
28+
//! Get extent to which graph's features will be limited (empty extent means no limit)
29+
QgsRectangle extent() const;
30+
//! Set extent to which graph's features will be limited (empty extent means no limit)
31+
void setExtent( const QgsRectangle& extent );
32+
33+
//! Get maximum possible number of features in graph. If the number is exceeded, graph is not created.
34+
int maxFeatureCount() const;
35+
//! Get maximum possible number of features in graph. If the number is exceeded, graph is not created.
36+
void setMaxFeatureCount( int count );
37+
38+
//! Build the internal data structures. This may take some time
39+
//! depending on how big the input layers are. It is not necessary
40+
//! to call this method explicitly - it will be called by findShortestPath()
41+
//! if necessary.
42+
bool init();
43+
44+
//! Whether the internal data structures have been initialized
45+
bool isInitialized() const;
46+
47+
//! Possible errors that may happen when calling findShortestPath()
48+
enum PathError
49+
{
50+
ErrNone, //!< No error
51+
ErrTooManyFeatures, //!< Max feature count treshold was reached while reading features
52+
ErrPoint1, //!< Start point cannot be joined to the graph
53+
ErrPoint2, //!< End point cannot be joined to the graph
54+
ErrNoPath, //!< Points are not connected in the graph
55+
};
56+
57+
//! Given two points, find the shortest path and return points on the way.
58+
//! The optional "error" argument may receive error code (PathError enum) if it is not null
59+
//! @return array of points - trace of linestrings of other features (empty array one error)
60+
QVector<QgsPoint> findShortestPath( const QgsPoint& p1, const QgsPoint& p2, QgsTracer::PathError* error /Out/ = nullptr );
61+
62+
//! Find out whether the point is snapped to a vertex or edge (i.e. it can be used for tracing start/stop)
63+
bool isPointSnapped( const QgsPoint& pt );
64+
65+
protected:
66+
//! Allows derived classes to setup the settings just before the tracer is initialized.
67+
//! This allows the configuration to be set in a lazy way only when it is really necessary.
68+
//! Default implementation does nothing.
69+
virtual void configure();
70+
71+
protected slots:
72+
//! Destroy the existing graph structure if any (de-initialize)
73+
void invalidateGraph();
74+
};

‎python/gui/gui.sip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
%Include qgsmapcanvasmap.sip
9494
%Include qgsmapcanvassnapper.sip
9595
%Include qgsmapcanvassnappingutils.sip
96+
%Include qgsmapcanvastracer.sip
9697
%Include qgsmaplayeractionregistry.sip
9798
%Include qgsmaplayercombobox.sip
9899
%Include qgsmaplayermodel.sip

‎python/gui/qgsmapcanvastracer.sip

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/** \ingroup gui
2+
* Extension of QgsTracer that provides extra functionality:
3+
* - automatic updates of own configuration based on canvas settings
4+
* - reporting of issues to the user via message bar
5+
*
6+
* A simple registry of tracer instances associated to map canvas instances
7+
* is kept for convenience. (Map tools do not need to create their local
8+
* tracer instances and map canvas API is not "polluted" by this optional
9+
* functionality).
10+
*
11+
* @note added in QGIS 2.14
12+
*/
13+
class QgsMapCanvasTracer : QgsTracer
14+
{
15+
%TypeHeaderCode
16+
#include <qgsmapcanvastracer.h>
17+
%End
18+
19+
public:
20+
//! Create tracer associated with a particular map canvas, optionally message bar for reporting
21+
explicit QgsMapCanvasTracer( QgsMapCanvas* canvas, QgsMessageBar* messageBar = 0 );
22+
~QgsMapCanvasTracer();
23+
24+
//! Access to action that user may use to toggle tracing on/off
25+
QAction* actionEnableTracing();
26+
27+
//! Retrieve instance of this class associated with given canvas (if any).
28+
//! The class keeps a simple registry of tracers associated with map canvas
29+
//! instances for easier access to the common tracer by various map tools
30+
static QgsMapCanvasTracer* tracerForCanvas( QgsMapCanvas* canvas );
31+
32+
//! Report a path finding error to the user
33+
void reportError( QgsTracer::PathError err, bool addingVertex );
34+
35+
protected:
36+
//! Sets configuration from current snapping settings and canvas settings
37+
virtual void configure();
38+
};

‎src/app/qgisapp.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@
164164
#include "qgslogger.h"
165165
#include "qgsmapcanvas.h"
166166
#include "qgsmapcanvassnappingutils.h"
167+
#include "qgsmapcanvastracer.h"
167168
#include "qgsmaplayer.h"
168169
#include "qgsmaplayerregistry.h"
169170
#include "qgsmaplayerstyleguiutils.h"
@@ -556,6 +557,7 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, QWidget * parent,
556557
, mComposerManager( nullptr )
557558
, mpTileScaleWidget( nullptr )
558559
, mpGpsWidget( nullptr )
560+
, mTracer( nullptr )
559561
, mSnappingUtils( nullptr )
560562
, mProjectLastModified()
561563
, mWelcomePage( nullptr )
@@ -1024,6 +1026,7 @@ QgisApp::QgisApp()
10241026
, mMacrosWarn( nullptr )
10251027
, mUserInputDockWidget( nullptr )
10261028
, mVectorLayerTools( nullptr )
1029+
, mTracer( nullptr )
10271030
, mActionFilterLegend( nullptr )
10281031
, mLegendExpressionFilterButton( nullptr )
10291032
, mSnappingUtils( nullptr )
@@ -1104,6 +1107,8 @@ QgisApp::~QgisApp()
11041107

11051108
delete mComposerManager;
11061109

1110+
delete mTracer;
1111+
11071112
delete mVectorLayerTools;
11081113
delete mWelcomePage;
11091114

@@ -1975,6 +1980,9 @@ void QgisApp::createToolBars()
19751980

19761981
// Cad toolbar
19771982
mAdvancedDigitizeToolBar->insertAction( mActionUndo, mAdvancedDigitizingDockWidget->enableAction() );
1983+
1984+
mTracer = new QgsMapCanvasTracer( mMapCanvas, messageBar() );
1985+
mAdvancedDigitizeToolBar->insertAction( mActionUndo, mTracer->actionEnableTracing() );
19781986
}
19791987

19801988
void QgisApp::createStatusBar()
@@ -9681,6 +9689,7 @@ void QgisApp::activateDeactivateLayerRelatedActions( QgsMapLayer* layer )
96819689
mActionMergeFeatures->setEnabled( false );
96829690
mActionMergeFeatureAttributes->setEnabled( false );
96839691
mActionRotatePointSymbols->setEnabled( false );
9692+
mTracer->actionEnableTracing()->setEnabled( false );
96849693

96859694
mActionPinLabels->setEnabled( false );
96869695
mActionShowHideLabels->setEnabled( false );
@@ -9801,6 +9810,9 @@ void QgisApp::activateDeactivateLayerRelatedActions( QgsMapLayer* layer )
98019810
mActionRotateFeature->setEnabled( isEditable && canChangeGeometry );
98029811
mActionNodeTool->setEnabled( isEditable && canChangeGeometry );
98039812

9813+
mTracer->actionEnableTracing()->setEnabled( isEditable && canAddFeatures &&
9814+
( vlayer->geometryType() == QGis::Line || vlayer->geometryType() == QGis::Polygon ) );
9815+
98049816
if ( vlayer->geometryType() == QGis::Point )
98059817
{
98069818
mActionAddFeature->setIcon( QgsApplication::getThemeIcon( "/mActionCapturePoint.svg" ) );

‎src/app/qgisapp.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class QgsAdvancedDigitizingDockWidget;
8181
class QgsSnappingDialog;
8282
class QgsGPSInformationWidget;
8383
class QgsStatisticalSummaryDockWidget;
84+
class QgsMapCanvasTracer;
8485

8586
class QgsDecorationItem;
8687

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

16991700
QgsVectorLayerTools* mVectorLayerTools;
17001701

1702+
//! A class that facilitates tracing of features
1703+
QgsMapCanvasTracer* mTracer;
1704+
17011705
QAction* mActionFilterLegend;
17021706

17031707
QgsLegendFilterButton* mLegendExpressionFilterButton;

‎src/core/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ SET(QGIS_CORE_SRCS
184184
qgssqlexpressioncompiler.cpp
185185
qgsstatisticalsummary.cpp
186186
qgsstringutils.cpp
187+
qgstracer.cpp
187188
qgstransaction.cpp
188189
qgstextlabelfeature.cpp
189190
qgstolerance.cpp
@@ -455,6 +456,7 @@ SET(QGIS_CORE_MOC_HDRS
455456
qgsrelationmanager.h
456457
qgsrunprocess.h
457458
qgssnappingutils.h
459+
qgstracer.h
458460
qgstransaction.h
459461
qgsvectordataprovider.h
460462
qgsvectorlayercache.h
@@ -660,6 +662,7 @@ SET(QGIS_CORE_HDRS
660662
qgsstringutils.h
661663
qgstextlabelfeature.h
662664
qgstolerance.h
665+
qgstracer.h
663666

664667
qgsvectordataprovider.h
665668
qgsvectorlayercache.h

‎src/core/qgssnappingutils.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,15 +369,35 @@ void QgsSnappingUtils::setMapSettings( const QgsMapSettings& settings )
369369
clearAllLocators();
370370
}
371371

372+
void QgsSnappingUtils::setCurrentLayer( QgsVectorLayer* layer )
373+
{
374+
mCurrentLayer = layer;
375+
}
376+
377+
void QgsSnappingUtils::setSnapToMapMode( QgsSnappingUtils::SnapToMapMode mode )
378+
{
379+
if ( mSnapToMapMode == mode )
380+
return;
381+
382+
mSnapToMapMode = mode;
383+
emit configChanged();
384+
}
385+
372386
void QgsSnappingUtils::setDefaultSettings( int type, double tolerance, QgsTolerance::UnitType unit )
373387
{
374388
// force map units - can't use layer units for just any layer
375389
if ( unit == QgsTolerance::LayerUnits )
376390
unit = QgsTolerance::ProjectUnits;
377391

392+
if ( mDefaultType == type && mDefaultTolerance == tolerance && mDefaultUnit == unit )
393+
return;
394+
378395
mDefaultType = type;
379396
mDefaultTolerance = tolerance;
380397
mDefaultUnit = unit;
398+
399+
if ( mSnapToMapMode != SnapAdvanced ) // does not affect advanced mode
400+
emit configChanged();
381401
}
382402

383403
void QgsSnappingUtils::defaultSettings( int& type, double& tolerance, QgsTolerance::UnitType& unit )
@@ -387,6 +407,25 @@ void QgsSnappingUtils::defaultSettings( int& type, double& tolerance, QgsToleran
387407
unit = mDefaultUnit;
388408
}
389409

410+
void QgsSnappingUtils::setLayers( const QList<QgsSnappingUtils::LayerConfig>& layers )
411+
{
412+
if ( mLayers == layers )
413+
return;
414+
415+
mLayers = layers;
416+
if ( mSnapToMapMode == SnapAdvanced ) // only affects advanced mode
417+
emit configChanged();
418+
}
419+
420+
void QgsSnappingUtils::setSnapOnIntersections( bool enabled )
421+
{
422+
if ( mSnapOnIntersection == enabled )
423+
return;
424+
425+
mSnapOnIntersection = enabled;
426+
emit configChanged();
427+
}
428+
390429
const QgsCoordinateReferenceSystem* QgsSnappingUtils::destCRS()
391430
{
392431
return mMapSettings.hasCrsTransformEnabled() ? &mMapSettings.destinationCrs() : nullptr;
@@ -467,6 +506,7 @@ void QgsSnappingUtils::readConfigFromProject()
467506
mLayers.append( LayerConfig( vlayer, t, tolIt->toDouble(), static_cast< QgsTolerance::UnitType >( tolUnitIt->toInt() ) ) );
468507
}
469508

509+
emit configChanged();
470510
}
471511

472512
void QgsSnappingUtils::onLayersWillBeRemoved( const QStringList& layerIds )

‎src/core/qgssnappingutils.h

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
6464
const QgsMapSettings& mapSettings() const { return mMapSettings; }
6565

6666
/** Set current layer so that if mode is SnapCurrentLayer we know which layer to use */
67-
void setCurrentLayer( QgsVectorLayer* layer ) { mCurrentLayer = layer; }
67+
void setCurrentLayer( QgsVectorLayer* layer );
6868
QgsVectorLayer* currentLayer() const { return mCurrentLayer; }
6969

7070

@@ -79,7 +79,7 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
7979
};
8080

8181
/** Set how the snapping to map is done */
82-
void setSnapToMapMode( SnapToMapMode mode ) { mSnapToMapMode = mode; }
82+
void setSnapToMapMode( SnapToMapMode mode );
8383
/** Find out how the snapping to map is done */
8484
SnapToMapMode snapToMapMode() const { return mSnapToMapMode; }
8585

@@ -95,9 +95,9 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
9595
/** Find out which strategy is used for indexing - by default hybrid indexing is used */
9696
IndexingStrategy indexingStrategy() const { return mStrategy; }
9797

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

103103
/**
@@ -107,6 +107,15 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
107107
{
108108
LayerConfig( QgsVectorLayer* l, const QgsPointLocator::Types& t, double tol, QgsTolerance::UnitType u ) : layer( l ), type( t ), tolerance( tol ), unit( u ) {}
109109

110+
bool operator==( const LayerConfig& other ) const
111+
{
112+
return layer == other.layer && type == other.type && tolerance == other.tolerance && unit == other.unit;
113+
}
114+
bool operator!=( const LayerConfig& other ) const
115+
{
116+
return !operator==( other );
117+
}
118+
110119
//! The layer to configure.
111120
QgsVectorLayer* layer;
112121
//! To which geometry properties of this layers a snapping should happen.
@@ -118,19 +127,25 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
118127
};
119128

120129
/** Set layers which will be used for snapping */
121-
void setLayers( const QList<LayerConfig>& layers ) { mLayers = layers; }
130+
void setLayers( const QList<LayerConfig>& layers );
122131
/** Query layers used for snapping */
123132
QList<LayerConfig> layers() const { return mLayers; }
124133

125134
/** Set whether to consider intersections of nearby segments for snapping */
126-
void setSnapOnIntersections( bool enabled ) { mSnapOnIntersection = enabled; }
135+
void setSnapOnIntersections( bool enabled );
127136
/** Query whether to consider intersections of nearby segments for snapping */
128137
bool snapOnIntersections() const { return mSnapOnIntersection; }
129138

130139
public slots:
131140
/** Read snapping configuration from the project */
132141
void readConfigFromProject();
133142

143+
signals:
144+
/** Emitted when snapping configuration has been changed
145+
* @note added in QGIS 2.14
146+
*/
147+
void configChanged();
148+
134149
protected:
135150
//! Called when starting to index - can be overridden and e.g. progress dialog can be provided
136151
virtual void prepareIndexStarting( int count ) { Q_UNUSED( count ); }

‎src/core/qgstracer.cpp

Lines changed: 676 additions & 0 deletions
Large diffs are not rendered by default.

‎src/core/qgstracer.h

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/***************************************************************************
2+
qgstracer.h
3+
--------------------------------------
4+
Date : January 2016
5+
Copyright : (C) 2016 by Martin Dobias
6+
Email : wonder dot sk at gmail dot com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#ifndef QGSTRACER_H
17+
#define QGSTRACER_H
18+
19+
class QgsVectorLayer;
20+
21+
#include <QSet>
22+
#include <QVector>
23+
24+
#include "qgscoordinatereferencesystem.h"
25+
#include "qgsfeature.h"
26+
#include "qgspoint.h"
27+
#include "qgsrectangle.h"
28+
29+
struct QgsTracerGraph;
30+
31+
/** \ingroup core
32+
* Utility class that construct a planar graph from the input vector
33+
* layers and provides shortest path search for tracing of existing
34+
* features.
35+
*
36+
* @note added in QGIS 2.14
37+
*/
38+
class CORE_EXPORT QgsTracer : public QObject
39+
{
40+
Q_OBJECT
41+
public:
42+
QgsTracer();
43+
~QgsTracer();
44+
45+
//! Get layers used for tracing
46+
QList<QgsVectorLayer*> layers() const { return mLayers; }
47+
//! Set layers used for tracing
48+
void setLayers( const QList<QgsVectorLayer*>& layers );
49+
50+
//! Get CRS used for tracing
51+
QgsCoordinateReferenceSystem destinationCrs() const { return mCRS; }
52+
//! Set CRS used for tracing
53+
void setDestinationCrs( const QgsCoordinateReferenceSystem& crs );
54+
55+
//! Get extent to which graph's features will be limited (empty extent means no limit)
56+
QgsRectangle extent() const { return mExtent; }
57+
//! Set extent to which graph's features will be limited (empty extent means no limit)
58+
void setExtent( const QgsRectangle& extent );
59+
60+
//! Get maximum possible number of features in graph. If the number is exceeded, graph is not created.
61+
int maxFeatureCount() const { return mMaxFeatureCount; }
62+
//! Get maximum possible number of features in graph. If the number is exceeded, graph is not created.
63+
void setMaxFeatureCount( int count ) { mMaxFeatureCount = count; }
64+
65+
//! Build the internal data structures. This may take some time
66+
//! depending on how big the input layers are. It is not necessary
67+
//! to call this method explicitly - it will be called by findShortestPath()
68+
//! if necessary.
69+
bool init();
70+
71+
//! Whether the internal data structures have been initialized
72+
bool isInitialized() const { return mGraph != nullptr; }
73+
74+
//! Possible errors that may happen when calling findShortestPath()
75+
enum PathError
76+
{
77+
ErrNone, //!< No error
78+
ErrTooManyFeatures, //!< Max feature count treshold was reached while reading features
79+
ErrPoint1, //!< Start point cannot be joined to the graph
80+
ErrPoint2, //!< End point cannot be joined to the graph
81+
ErrNoPath, //!< Points are not connected in the graph
82+
};
83+
84+
//! Given two points, find the shortest path and return points on the way.
85+
//! The optional "error" argument may receive error code (PathError enum) if it is not null
86+
//! @return array of points - trace of linestrings of other features (empty array one error)
87+
QVector<QgsPoint> findShortestPath( const QgsPoint& p1, const QgsPoint& p2, PathError* error = nullptr );
88+
89+
//! Find out whether the point is snapped to a vertex or edge (i.e. it can be used for tracing start/stop)
90+
bool isPointSnapped( const QgsPoint& pt );
91+
92+
protected:
93+
//! Allows derived classes to setup the settings just before the tracer is initialized.
94+
//! This allows the configuration to be set in a lazy way only when it is really necessary.
95+
//! Default implementation does nothing.
96+
virtual void configure() {}
97+
98+
protected slots:
99+
//! Destroy the existing graph structure if any (de-initialize)
100+
void invalidateGraph();
101+
102+
private:
103+
bool initGraph();
104+
105+
private slots:
106+
void onFeatureAdded( QgsFeatureId fid );
107+
void onFeatureDeleted( QgsFeatureId fid );
108+
void onGeometryChanged( QgsFeatureId fid, QgsGeometry& geom );
109+
110+
private:
111+
//! Graph data structure for path searching
112+
QgsTracerGraph* mGraph;
113+
//! Input layers for the graph building
114+
QList<QgsVectorLayer*> mLayers;
115+
//! Destination CRS in which graph is built and tracing done
116+
QgsCoordinateReferenceSystem mCRS;
117+
//! Extent for graph building (empty extent means no limit)
118+
QgsRectangle mExtent;
119+
//! Limit of how many features can be in the graph (0 means no limit).
120+
//! This is to avoid possibly long graph preparation for complicated layers
121+
int mMaxFeatureCount;
122+
};
123+
124+
125+
#endif // QGSTRACER_H

‎src/gui/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ SET(QGIS_GUI_SRCS
217217
qgsmapcanvasmap.cpp
218218
qgsmapcanvassnapper.cpp
219219
qgsmapcanvassnappingutils.cpp
220+
qgsmapcanvastracer.cpp
220221
qgsmaplayeractionregistry.cpp
221222
qgsmaplayercombobox.cpp
222223
qgsmaplayermodel.cpp
@@ -351,6 +352,7 @@ SET(QGIS_GUI_MOC_HDRS
351352
qgsmanageconnectionsdialog.h
352353
qgsmapcanvas.h
353354
qgsmapcanvassnappingutils.h
355+
qgsmapcanvastracer.h
354356
qgsmaplayeractionregistry.h
355357
qgsmaplayercombobox.h
356358
qgsmaplayermodel.h
@@ -566,6 +568,7 @@ SET(QGIS_GUI_HDRS
566568
qgsmapcanvasmap.h
567569
qgsmapcanvassnapper.h
568570
qgsmapcanvassnappingutils.h
571+
qgsmapcanvastracer.h
569572
qgsmaptip.h
570573
qgsmapmouseevent.h
571574
qgsnumericsortlistviewitem.h

‎src/gui/qgsmapcanvastracer.cpp

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#include "qgsmapcanvastracer.h"
2+
3+
#include "qgsapplication.h"
4+
#include "qgsmapcanvas.h"
5+
#include "qgsmaplayerregistry.h"
6+
#include "qgsmessagebar.h"
7+
#include "qgsmessagebaritem.h"
8+
#include "qgssnappingutils.h"
9+
#include "qgsvectorlayer.h"
10+
11+
#include <QAction>
12+
13+
QHash<QgsMapCanvas*, QgsMapCanvasTracer*> QgsMapCanvasTracer::sTracers;
14+
15+
16+
QgsMapCanvasTracer::QgsMapCanvasTracer( QgsMapCanvas* canvas, QgsMessageBar* messageBar )
17+
: mCanvas( canvas )
18+
, mMessageBar( messageBar )
19+
, mLastMessage( nullptr )
20+
{
21+
sTracers.insert( canvas, this );
22+
23+
// when things change we just invalidate the graph - and set up new parameters again only when necessary
24+
connect( canvas, SIGNAL( destinationCrsChanged() ), this, SLOT( invalidateGraph() ) );
25+
connect( canvas, SIGNAL( layersChanged() ), this, SLOT( invalidateGraph() ) );
26+
connect( canvas, SIGNAL( extentsChanged() ), this, SLOT( invalidateGraph() ) );
27+
connect( canvas, SIGNAL( currentLayerChanged( QgsMapLayer* ) ), this, SLOT( onCurrentLayerChanged() ) );
28+
connect( canvas->snappingUtils(), SIGNAL( configChanged() ), this, SLOT( invalidateGraph() ) );
29+
30+
mActionEnableTracing = new QAction( QIcon( QgsApplication::getThemeIcon( "/mActionTracing.png" ) ), tr( "Enable Tracing" ), this );
31+
mActionEnableTracing->setShortcut( Qt::Key_T );
32+
mActionEnableTracing->setCheckable( true );
33+
34+
// arbitrarily chosen limit that should allow for fairly fast initialization
35+
// of the underlying graph structure
36+
setMaxFeatureCount( QSettings().value( "/qgis/digitizing/tracing_max_feature_count", 10000 ).toInt() );
37+
}
38+
39+
QgsMapCanvasTracer::~QgsMapCanvasTracer()
40+
{
41+
sTracers.remove( mCanvas );
42+
}
43+
44+
QgsMapCanvasTracer* QgsMapCanvasTracer::tracerForCanvas( QgsMapCanvas* canvas )
45+
{
46+
return sTracers.value( canvas, 0 );
47+
}
48+
49+
void QgsMapCanvasTracer::reportError( QgsTracer::PathError err, bool addingVertex )
50+
{
51+
if ( !mMessageBar )
52+
return;
53+
54+
// remove previous message (if any)
55+
mMessageBar->popWidget( mLastMessage );
56+
mLastMessage = nullptr;
57+
58+
QString message;
59+
switch ( err )
60+
{
61+
case ErrTooManyFeatures:
62+
message = tr( "Disabled - there are too many features displayed. Try zooming in or disable some layers." );
63+
break;
64+
case ErrPoint1:
65+
message = tr( "The start point needs to be snapped and in the visible map view" );
66+
break;
67+
case ErrPoint2:
68+
if ( addingVertex )
69+
message = tr( "The end point needs to be snapped" );
70+
break;
71+
case ErrNoPath:
72+
if ( addingVertex )
73+
message = tr( "Endpoints are not connected" );
74+
break;
75+
case ErrNone:
76+
default:
77+
break;
78+
}
79+
80+
if ( message.isEmpty() )
81+
return;
82+
83+
mLastMessage = new QgsMessageBarItem( tr( "Tracing" ), message, QgsMessageBar::WARNING,
84+
QSettings().value( "/qgis/messageTimeout", 5 ).toInt() );
85+
mMessageBar->pushItem( mLastMessage );
86+
}
87+
88+
void QgsMapCanvasTracer::configure()
89+
{
90+
setDestinationCrs( mCanvas->mapSettings().destinationCrs() );
91+
setExtent( mCanvas->extent() );
92+
93+
QList<QgsVectorLayer*> layers;
94+
QStringList visibleLayerIds = mCanvas->mapSettings().layers();
95+
96+
switch ( mCanvas->snappingUtils()->snapToMapMode() )
97+
{
98+
default:
99+
case QgsSnappingUtils::SnapCurrentLayer:
100+
{
101+
QgsVectorLayer* vl = qobject_cast<QgsVectorLayer*>( mCanvas->currentLayer() );
102+
if ( vl && visibleLayerIds.contains( vl->id() ) )
103+
layers << vl;
104+
}
105+
break;
106+
case QgsSnappingUtils::SnapAllLayers:
107+
foreach ( const QString& layerId, visibleLayerIds )
108+
{
109+
QgsVectorLayer* vl = qobject_cast<QgsVectorLayer*>( QgsMapLayerRegistry::instance()->mapLayer( layerId ) );
110+
if ( vl )
111+
layers << vl;
112+
}
113+
break;
114+
case QgsSnappingUtils::SnapAdvanced:
115+
foreach ( const QgsSnappingUtils::LayerConfig& cfg, mCanvas->snappingUtils()->layers() )
116+
{
117+
if ( visibleLayerIds.contains( cfg.layer->id() ) )
118+
layers << cfg.layer;
119+
}
120+
break;
121+
}
122+
123+
setLayers( layers );
124+
}
125+
126+
void QgsMapCanvasTracer::onCurrentLayerChanged()
127+
{
128+
// no need to bother if we are not snapping
129+
if ( mCanvas->snappingUtils()->snapToMapMode() == QgsSnappingUtils::SnapCurrentLayer )
130+
invalidateGraph();
131+
}

‎src/gui/qgsmapcanvastracer.h

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#ifndef QGSMAPCANVASTRACER_H
2+
#define QGSMAPCANVASTRACER_H
3+
4+
#include "qgstracer.h"
5+
6+
class QAction;
7+
class QgsMapCanvas;
8+
class QgsMessageBar;
9+
class QgsMessageBarItem;
10+
11+
/** \ingroup gui
12+
* Extension of QgsTracer that provides extra functionality:
13+
* - automatic updates of own configuration based on canvas settings
14+
* - reporting of issues to the user via message bar
15+
*
16+
* A simple registry of tracer instances associated to map canvas instances
17+
* is kept for convenience. (Map tools do not need to create their local
18+
* tracer instances and map canvas API is not "polluted" by this optional
19+
* functionality).
20+
*
21+
* @note added in QGIS 2.14
22+
*/
23+
class GUI_EXPORT QgsMapCanvasTracer : public QgsTracer
24+
{
25+
Q_OBJECT
26+
27+
public:
28+
//! Create tracer associated with a particular map canvas, optionally message bar for reporting
29+
explicit QgsMapCanvasTracer( QgsMapCanvas* canvas, QgsMessageBar* messageBar = 0 );
30+
~QgsMapCanvasTracer();
31+
32+
//! Access to action that user may use to toggle tracing on/off
33+
QAction* actionEnableTracing() { return mActionEnableTracing; }
34+
35+
//! Retrieve instance of this class associated with given canvas (if any).
36+
//! The class keeps a simple registry of tracers associated with map canvas
37+
//! instances for easier access to the common tracer by various map tools
38+
static QgsMapCanvasTracer* tracerForCanvas( QgsMapCanvas* canvas );
39+
40+
//! Report a path finding error to the user
41+
void reportError( PathError err, bool addingVertex );
42+
43+
protected:
44+
//! Sets configuration from current snapping settings and canvas settings
45+
virtual void configure();
46+
47+
private slots:
48+
void onCurrentLayerChanged();
49+
50+
private:
51+
QgsMapCanvas* mCanvas;
52+
QgsMessageBar* mMessageBar;
53+
QgsMessageBarItem* mLastMessage;
54+
55+
QAction* mActionEnableTracing;
56+
57+
static QHash<QgsMapCanvas*, QgsMapCanvasTracer*> sTracers;
58+
};
59+
60+
#endif // QGSMAPCANVASTRACER_H

‎src/gui/qgsmaptoolcapture.cpp

Lines changed: 155 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "qgslinestringv2.h"
2222
#include "qgslogger.h"
2323
#include "qgsmapcanvas.h"
24+
#include "qgsmapcanvastracer.h"
2425
#include "qgsmapmouseevent.h"
2526
#include "qgsmaprenderer.h"
2627
#include "qgspolygonv2.h"
@@ -134,6 +135,124 @@ void QgsMapToolCapture::currentLayerChanged( QgsMapLayer *layer )
134135
}
135136
}
136137

138+
139+
bool QgsMapToolCapture::tracingEnabled()
140+
{
141+
QgsMapCanvasTracer* tracer = QgsMapCanvasTracer::tracerForCanvas( mCanvas );
142+
return tracer && tracer->actionEnableTracing()->isChecked();
143+
}
144+
145+
146+
QgsPoint QgsMapToolCapture::tracingStartPoint()
147+
{
148+
try
149+
{
150+
QgsMapLayer* layer = mCanvas->currentLayer();
151+
if ( !layer )
152+
return QgsPoint();
153+
QgsPointV2 v = mCaptureCurve.endPoint();
154+
return toMapCoordinates( layer, QgsPoint( v.x(), v.y() ) );
155+
}
156+
catch ( QgsCsException & )
157+
{
158+
QgsDebugMsg( "transformation to layer coordinate failed" );
159+
return QgsPoint();
160+
}
161+
}
162+
163+
164+
void QgsMapToolCapture::tracingMouseMove( QgsMapMouseEvent* e )
165+
{
166+
if ( !e->isSnapped() )
167+
return;
168+
169+
QgsPoint pt0 = tracingStartPoint();
170+
if ( pt0 == QgsPoint() )
171+
return;
172+
173+
QgsMapCanvasTracer* tracer = QgsMapCanvasTracer::tracerForCanvas( mCanvas );
174+
if ( !tracer )
175+
return; // this should not happen!
176+
177+
mTempRubberBand->reset( mCaptureMode == CapturePolygon ? QGis::Polygon : QGis::Line );
178+
179+
QgsTracer::PathError err;
180+
QVector<QgsPoint> points = tracer->findShortestPath( pt0, e->mapPoint(), &err );
181+
if ( points.isEmpty() )
182+
{
183+
tracer->reportError( err, false );
184+
return;
185+
}
186+
187+
if ( mCaptureMode == CapturePolygon )
188+
mTempRubberBand->addPoint( *mRubberBand->getPoint( 0, 0 ), false );
189+
190+
// update rubberband
191+
for ( int i = 0; i < points.count(); ++i )
192+
mTempRubberBand->addPoint( points.at( i ), i == points.count() - 1 );
193+
}
194+
195+
196+
bool QgsMapToolCapture::tracingAddVertex( const QgsPoint& point )
197+
{
198+
QgsMapCanvasTracer* tracer = QgsMapCanvasTracer::tracerForCanvas( mCanvas );
199+
if ( !tracer )
200+
return false; // this should not happen!
201+
202+
if ( mCaptureCurve.numPoints() == 0 )
203+
{
204+
if ( !tracer->init() )
205+
{
206+
tracer->reportError( QgsTracer::ErrTooManyFeatures, true );
207+
return false;
208+
}
209+
210+
// only accept first point if it is snapped to the graph (to vertex or edge)
211+
bool res = tracer->isPointSnapped( point );
212+
if ( res )
213+
{
214+
QgsPoint layerPoint;
215+
nextPoint( point, layerPoint ); // assuming the transform went fine earlier
216+
217+
mRubberBand->addPoint( point );
218+
mCaptureCurve.addVertex( QgsPointV2( layerPoint.x(), layerPoint.y() ) );
219+
}
220+
return res;
221+
}
222+
223+
QgsPoint pt0 = tracingStartPoint();
224+
if ( pt0 == QgsPoint() )
225+
return false;
226+
227+
QgsTracer::PathError err;
228+
QVector<QgsPoint> points = tracer->findShortestPath( pt0, point, &err );
229+
if ( points.isEmpty() )
230+
{
231+
tracer->reportError( err, true );
232+
return false; // ignore the vertex - can't find path to the end point!
233+
}
234+
235+
// transform points
236+
QList<QgsPointV2> layerPoints;
237+
QgsPoint lp; // in layer coords
238+
for ( int i = 1; i < points.count(); ++i )
239+
{
240+
if ( nextPoint( points[i], lp ) != 0 )
241+
return false;
242+
layerPoints << QgsPointV2( lp.x(), lp.y() );
243+
}
244+
245+
for ( int i = 1; i < points.count(); ++i )
246+
{
247+
if ( points[i] == points[i-1] )
248+
continue; // avoid duplicate vertices if there are any
249+
mRubberBand->addPoint( points[i], i == points.count() - 1 );
250+
mCaptureCurve.addVertex( layerPoints[i-1] );
251+
}
252+
return true;
253+
}
254+
255+
137256
void QgsMapToolCapture::cadCanvasMoveEvent( QgsMapMouseEvent * e )
138257
{
139258
QgsMapToolAdvancedDigitizing::cadCanvasMoveEvent( e );
@@ -165,9 +284,30 @@ void QgsMapToolCapture::cadCanvasMoveEvent( QgsMapMouseEvent * e )
165284
mTempRubberBand->addPoint( point );
166285
}
167286

287+
168288
if ( mCaptureMode != CapturePoint && mTempRubberBand && mCapturing )
169289
{
170-
mTempRubberBand->movePoint( point );
290+
if ( tracingEnabled() && mCaptureCurve.numPoints() != 0 )
291+
{
292+
tracingMouseMove( e );
293+
}
294+
else
295+
{
296+
if ( mCaptureCurve.numPoints() > 0 &&
297+
(( mCaptureMode == CaptureLine && mTempRubberBand->numberOfVertices() != 2 ) ||
298+
( mCaptureMode == CapturePolygon && mTempRubberBand->numberOfVertices() != 3 ) ) )
299+
{
300+
// fix temporary rubber band after tracing which may have added multiple points
301+
mTempRubberBand->reset( mCaptureMode == CapturePolygon ? QGis::Polygon : QGis::Line );
302+
if ( mCaptureMode == CapturePolygon )
303+
mTempRubberBand->addPoint( *mRubberBand->getPoint( 0, 0 ), false );
304+
QgsPointV2 pt = mCaptureCurve.endPoint();
305+
mTempRubberBand->addPoint( QgsPoint( pt.x(), pt.y() ) );
306+
mTempRubberBand->addPoint( point );
307+
}
308+
else
309+
mTempRubberBand->movePoint( point );
310+
}
171311
}
172312
} // mouseMoveEvent
173313

@@ -220,8 +360,6 @@ int QgsMapToolCapture::addVertex( const QgsPoint& point )
220360
{
221361
mRubberBand = createRubberBand( mCaptureMode == CapturePolygon ? QGis::Polygon : QGis::Line );
222362
}
223-
mRubberBand->addPoint( point );
224-
mCaptureCurve.addVertex( QgsPointV2( layerPoint.x(), layerPoint.y() ) );
225363

226364
if ( !mTempRubberBand )
227365
{
@@ -231,6 +369,20 @@ int QgsMapToolCapture::addVertex( const QgsPoint& point )
231369
{
232370
mTempRubberBand->reset( mCaptureMode == CapturePolygon ? QGis::Polygon : QGis::Line );
233371
}
372+
373+
if ( tracingEnabled() )
374+
{
375+
bool res = tracingAddVertex( point );
376+
if ( !res )
377+
return 1; // early exit if the point cannot be accepted
378+
}
379+
else
380+
{
381+
// ordinary digitizing
382+
mRubberBand->addPoint( point );
383+
mCaptureCurve.addVertex( QgsPointV2( layerPoint.x(), layerPoint.y() ) );
384+
}
385+
234386
if ( mCaptureMode == CaptureLine )
235387
{
236388
mTempRubberBand->addPoint( point );

‎src/gui/qgsmaptoolcapture.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,16 @@ class GUI_EXPORT QgsMapToolCapture : public QgsMapToolAdvancedDigitizing
140140
*/
141141
void closePolygon();
142142

143+
private:
144+
//! whether tracing has been requested by the user
145+
bool tracingEnabled();
146+
//! first point that will be used as a start of the trace
147+
QgsPoint tracingStartPoint();
148+
//! handle of mouse movement when tracing enabled and capturing has started
149+
void tracingMouseMove( QgsMapMouseEvent* e );
150+
//! handle of addition of clicked point (with the rest of the trace) when tracing enabled
151+
bool tracingAddVertex( const QgsPoint& point );
152+
143153
private:
144154
/** Flag to indicate a map canvas capture operation is taking place */
145155
bool mCapturing;

‎tests/src/core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ ADD_QGIS_TEST(stringutilstest testqgsstringutils.cpp)
170170
ADD_QGIS_TEST(stylev2test testqgsstylev2.cpp)
171171
ADD_QGIS_TEST(svgmarkertest testqgssvgmarker.cpp)
172172
ADD_QGIS_TEST(symbolv2test testqgssymbolv2.cpp)
173+
ADD_QGIS_TEST(tracertest testqgstracer.cpp)
173174
#for some obscure reason calling this test "fontutils" kills the build on Ubuntu 15.10
174175
ADD_QGIS_TEST(typographicstylingutils testqgsfontutils.cpp)
175176
ADD_QGIS_TEST(vectordataprovidertest testqgsvectordataprovider.cpp)

‎tests/src/core/testqgstracer.cpp

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
/***************************************************************************
2+
testqgslayertree.cpp
3+
--------------------------------------
4+
Date : January 2016
5+
Copyright : (C) 2016 by Martin Dobias
6+
Email : wonder dot sk at gmail dot com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#include <QtTest/QtTest>
17+
18+
#include <qgsapplication.h>
19+
#include <qgsgeometry.h>
20+
#include <qgsmaplayerregistry.h>
21+
#include <qgstracer.h>
22+
#include <qgsvectorlayer.h>
23+
24+
class TestQgsTracer : public QObject
25+
{
26+
Q_OBJECT
27+
public:
28+
private slots:
29+
void initTestCase();
30+
void cleanupTestCase();
31+
void testSimple();
32+
void testPolygon();
33+
void testButterfly();
34+
void testLayerUpdates();
35+
void testExtent();
36+
void testReprojection();
37+
38+
private:
39+
40+
};
41+
42+
namespace QTest
43+
{
44+
template<>
45+
char* toString( const QgsPoint& point )
46+
{
47+
QByteArray ba = "QgsPoint(" + QByteArray::number( point.x() ) +
48+
", " + QByteArray::number( point.y() ) + ")";
49+
return qstrdup( ba.data() );
50+
}
51+
}
52+
53+
static QgsFeature make_feature( const QString& wkt )
54+
{
55+
QgsFeature f;
56+
f.setGeometry( QgsGeometry::fromWkt( wkt ) );
57+
return f;
58+
}
59+
60+
static QgsVectorLayer* make_layer( const QStringList& wkts )
61+
{
62+
QgsVectorLayer* vl = new QgsVectorLayer( "LineString", "x", "memory" );
63+
Q_ASSERT( vl->isValid() );
64+
65+
vl->startEditing();
66+
foreach ( const QString& wkt, wkts )
67+
{
68+
QgsFeature f( make_feature( wkt ) );
69+
vl->addFeature( f, false );
70+
}
71+
vl->commitChanges();
72+
73+
return vl;
74+
}
75+
76+
void print_shortest_path( QgsTracer& tracer, const QgsPoint& p1, const QgsPoint& p2 )
77+
{
78+
qDebug( "from (%f,%f) to (%f,%f)", p1.x(), p1.y(), p2.x(), p2.y() );
79+
QVector<QgsPoint> points = tracer.findShortestPath( p1, p2 );
80+
81+
if ( points.isEmpty() )
82+
qDebug( "no path!" );
83+
84+
foreach ( QgsPoint p, points )
85+
qDebug( "p: %f %f", p.x(), p.y() );
86+
}
87+
88+
89+
90+
void TestQgsTracer::initTestCase()
91+
{
92+
QgsApplication::init();
93+
QgsApplication::initQgis();
94+
95+
}
96+
97+
void TestQgsTracer::cleanupTestCase()
98+
{
99+
QgsApplication::exitQgis();
100+
}
101+
102+
void TestQgsTracer::testSimple()
103+
{
104+
QStringList wkts;
105+
wkts << "LINESTRING(0 0, 0 10)"
106+
<< "LINESTRING(0 0, 10 0)"
107+
<< "LINESTRING(0 10, 20 10)"
108+
<< "LINESTRING(10 0, 20 10)";
109+
110+
/* This shape - nearly a square (one side is shifted to have exactly one shortest
111+
* path between corners):
112+
* 0,10 +----+ 20,10
113+
* | /
114+
* 0,0 +--+ 10,0
115+
*/
116+
117+
QgsVectorLayer* vl = make_layer( wkts );
118+
119+
QgsTracer tracer;
120+
tracer.setLayers( QList<QgsVectorLayer*>() << vl );
121+
122+
QgsPolyline points1 = tracer.findShortestPath( QgsPoint( 0, 0 ), QgsPoint( 20, 10 ) );
123+
QCOMPARE( points1.count(), 3 );
124+
QCOMPARE( points1[0], QgsPoint( 0, 0 ) );
125+
QCOMPARE( points1[1], QgsPoint( 10, 0 ) );
126+
QCOMPARE( points1[2], QgsPoint( 20, 10 ) );
127+
128+
// one joined point
129+
QgsPolyline points2 = tracer.findShortestPath( QgsPoint( 5, 10 ), QgsPoint( 0, 0 ) );
130+
QCOMPARE( points2.count(), 3 );
131+
QCOMPARE( points2[0], QgsPoint( 5, 10 ) );
132+
QCOMPARE( points2[1], QgsPoint( 0, 10 ) );
133+
QCOMPARE( points2[2], QgsPoint( 0, 0 ) );
134+
135+
// two joined points
136+
QgsPolyline points3 = tracer.findShortestPath( QgsPoint( 0, 1 ), QgsPoint( 11, 1 ) );
137+
QCOMPARE( points3.count(), 4 );
138+
QCOMPARE( points3[0], QgsPoint( 0, 1 ) );
139+
QCOMPARE( points3[1], QgsPoint( 0, 0 ) );
140+
QCOMPARE( points3[2], QgsPoint( 10, 0 ) );
141+
QCOMPARE( points3[3], QgsPoint( 11, 1 ) );
142+
143+
// two joined points on one line
144+
QgsPolyline points4 = tracer.findShortestPath( QgsPoint( 11, 1 ), QgsPoint( 19, 9 ) );
145+
QCOMPARE( points4.count(), 2 );
146+
QCOMPARE( points4[0], QgsPoint( 11, 1 ) );
147+
QCOMPARE( points4[1], QgsPoint( 19, 9 ) );
148+
149+
// no path to (1,1)
150+
QgsPolyline points5 = tracer.findShortestPath( QgsPoint( 0, 0 ), QgsPoint( 1, 1 ) );
151+
QCOMPARE( points5.count(), 0 );
152+
153+
delete vl;
154+
}
155+
156+
void TestQgsTracer::testPolygon()
157+
{
158+
// the same shape as in testSimple() but with just one polygon ring
159+
// to check extraction from polygons work + routing along one ring works
160+
161+
QStringList wkts;
162+
wkts << "POLYGON((0 0, 0 10, 20 10, 10 0, 0 0))";
163+
164+
QgsVectorLayer* vl = make_layer( wkts );
165+
166+
QgsTracer tracer;
167+
tracer.setLayers( QList<QgsVectorLayer*>() << vl );
168+
169+
QgsPolyline points = tracer.findShortestPath( QgsPoint( 1, 0 ), QgsPoint( 0, 1 ) );
170+
QCOMPARE( points.count(), 3 );
171+
QCOMPARE( points[0], QgsPoint( 1, 0 ) );
172+
QCOMPARE( points[1], QgsPoint( 0, 0 ) );
173+
QCOMPARE( points[2], QgsPoint( 0, 1 ) );
174+
175+
delete vl;
176+
}
177+
178+
void TestQgsTracer::testButterfly()
179+
{
180+
// checks whether tracer internally splits linestrings at intersections
181+
182+
QStringList wkts;
183+
wkts << "LINESTRING(0 0, 0 10, 10 0, 10 10, 0 0)";
184+
185+
/* This shape (without a vertex where the linestring crosses itself):
186+
* + + 10,10
187+
* |\/|
188+
* |/\|
189+
* + +
190+
* 0,0
191+
*/
192+
193+
QgsVectorLayer* vl = make_layer( wkts );
194+
195+
QgsTracer tracer;
196+
tracer.setLayers( QList<QgsVectorLayer*>() << vl );
197+
198+
QgsPolyline points = tracer.findShortestPath( QgsPoint( 0, 0 ), QgsPoint( 10, 0 ) );
199+
200+
QCOMPARE( points.count(), 3 );
201+
QCOMPARE( points[0], QgsPoint( 0, 0 ) );
202+
QCOMPARE( points[1], QgsPoint( 5, 5 ) );
203+
QCOMPARE( points[2], QgsPoint( 10, 0 ) );
204+
205+
delete vl;
206+
}
207+
208+
void TestQgsTracer::testLayerUpdates()
209+
{
210+
// check whether the tracer is updated on added/removed/changed features
211+
212+
// same shape as in testSimple()
213+
QStringList wkts;
214+
wkts << "LINESTRING(0 0, 0 10)"
215+
<< "LINESTRING(0 0, 10 0)"
216+
<< "LINESTRING(0 10, 20 10)"
217+
<< "LINESTRING(10 0, 20 10)";
218+
219+
QgsVectorLayer* vl = make_layer( wkts );
220+
221+
QgsTracer tracer;
222+
tracer.setLayers( QList<QgsVectorLayer*>() << vl );
223+
tracer.init();
224+
225+
QgsPolyline points1 = tracer.findShortestPath( QgsPoint( 10, 0 ), QgsPoint( 10, 10 ) );
226+
QCOMPARE( points1.count(), 3 );
227+
QCOMPARE( points1[0], QgsPoint( 10, 0 ) );
228+
QCOMPARE( points1[1], QgsPoint( 20, 10 ) );
229+
QCOMPARE( points1[2], QgsPoint( 10, 10 ) );
230+
231+
vl->startEditing();
232+
233+
// add a shortcut
234+
QgsFeature f( make_feature( "LINESTRING(10 0, 10 10)" ) );
235+
vl->addFeature( f );
236+
237+
QgsPolyline points2 = tracer.findShortestPath( QgsPoint( 10, 0 ), QgsPoint( 10, 10 ) );
238+
QCOMPARE( points2.count(), 2 );
239+
QCOMPARE( points2[0], QgsPoint( 10, 0 ) );
240+
QCOMPARE( points2[1], QgsPoint( 10, 10 ) );
241+
242+
// delete the shortcut
243+
vl->deleteFeature( f.id() );
244+
245+
QgsPolyline points3 = tracer.findShortestPath( QgsPoint( 10, 0 ), QgsPoint( 10, 10 ) );
246+
QCOMPARE( points3.count(), 3 );
247+
QCOMPARE( points3[0], QgsPoint( 10, 0 ) );
248+
QCOMPARE( points3[1], QgsPoint( 20, 10 ) );
249+
QCOMPARE( points3[2], QgsPoint( 10, 10 ) );
250+
251+
// make the shortcut again from a different feature
252+
QgsGeometry* g = QgsGeometry::fromWkt( "LINESTRING(10 0, 10 10)" );
253+
vl->changeGeometry( 2, g ); // change bottom line (second item in wkts)
254+
delete g;
255+
256+
QgsPolyline points4 = tracer.findShortestPath( QgsPoint( 10, 0 ), QgsPoint( 10, 10 ) );
257+
QCOMPARE( points4.count(), 2 );
258+
QCOMPARE( points4[0], QgsPoint( 10, 0 ) );
259+
QCOMPARE( points4[1], QgsPoint( 10, 10 ) );
260+
261+
QgsPolyline points5 = tracer.findShortestPath( QgsPoint( 0, 0 ), QgsPoint( 10, 0 ) );
262+
QCOMPARE( points5.count(), 4 );
263+
QCOMPARE( points5[0], QgsPoint( 0, 0 ) );
264+
QCOMPARE( points5[1], QgsPoint( 0, 10 ) );
265+
QCOMPARE( points5[2], QgsPoint( 10, 10 ) );
266+
QCOMPARE( points5[3], QgsPoint( 10, 0 ) );
267+
268+
vl->rollBack();
269+
270+
delete vl;
271+
}
272+
273+
void TestQgsTracer::testExtent()
274+
{
275+
// check whether the tracer correctly handles the extent limitation
276+
277+
// same shape as in testSimple()
278+
QStringList wkts;
279+
wkts << "LINESTRING(0 0, 0 10)"
280+
<< "LINESTRING(0 0, 10 0)"
281+
<< "LINESTRING(0 10, 20 10)"
282+
<< "LINESTRING(10 0, 20 10)";
283+
284+
QgsVectorLayer* vl = make_layer( wkts );
285+
286+
QgsTracer tracer;
287+
tracer.setLayers( QList<QgsVectorLayer*>() << vl );
288+
tracer.setExtent( QgsRectangle( 0, 0, 5, 5 ) );
289+
tracer.init();
290+
291+
QgsPolyline points1 = tracer.findShortestPath( QgsPoint( 0, 0 ), QgsPoint( 10, 0 ) );
292+
QCOMPARE( points1.count(), 2 );
293+
QCOMPARE( points1[0], QgsPoint( 0, 0 ) );
294+
QCOMPARE( points1[1], QgsPoint( 10, 0 ) );
295+
296+
QgsPolyline points2 = tracer.findShortestPath( QgsPoint( 0, 0 ), QgsPoint( 20, 10 ) );
297+
QCOMPARE( points2.count(), 0 );
298+
}
299+
300+
void TestQgsTracer::testReprojection()
301+
{
302+
QStringList wkts;
303+
wkts << "LINESTRING(1 0, 2 0)";
304+
305+
QgsVectorLayer* vl = make_layer( wkts );
306+
307+
QgsCoordinateReferenceSystem dstCrs( "EPSG:3857" );
308+
QgsCoordinateTransform ct( QgsCoordinateReferenceSystem( "EPSG:4326" ), dstCrs );
309+
QgsPoint p1 = ct.transform( QgsPoint( 1, 0 ) );
310+
QgsPoint p2 = ct.transform( QgsPoint( 2, 0 ) );
311+
312+
QgsTracer tracer;
313+
tracer.setLayers( QList<QgsVectorLayer*>() << vl );
314+
tracer.setDestinationCrs( dstCrs );
315+
tracer.init();
316+
317+
QgsPolyline points1 = tracer.findShortestPath( p1, p2 );
318+
QCOMPARE( points1.count(), 2 );
319+
}
320+
321+
322+
QTEST_MAIN( TestQgsTracer )
323+
#include "testqgstracer.moc"

0 commit comments

Comments
 (0)
Please sign in to comment.