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 @@ + + + + + GIS icon theme 0.2 + + + + + image/svg+xml + + GIS icon theme 0.2 + + + Robert Szczepanek + + + + + Robert Szczepanek + + + + + GIS icons + + + GIS icons + http://robert.szczepanek.pl/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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"