Skip to content

Commit

Permalink
[FEATURE] Tracing with optional offset
Browse files Browse the repository at this point in the history
Tracing button in the snapping toolbar gets extra menu where it is possible to set
offset that will be applied to the traced line. Offset value can be either positive
(right side) or negative (left side).
  • Loading branch information
wonder-sk committed Oct 10, 2017
1 parent 217c0e7 commit b140b29
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 5 deletions.
19 changes: 19 additions & 0 deletions python/core/qgstracer.sip
Expand Up @@ -60,6 +60,25 @@ Get extent to which graph's features will be limited (empty extent means no limi
void setExtent( const QgsRectangle &extent );
%Docstring
Set extent to which graph's features will be limited (empty extent means no limit)
%End

double offset() const;
%Docstring
.. versionadded:: 3.0
:rtype: float
%End
void setOffset( double offset );
%Docstring
.. versionadded:: 3.0
%End

void offsetParameters( int &quadSegments /Out/, int &joinStyle /Out/, double &miterLimit /Out/ );
%Docstring
.. versionadded:: 3.0
%End
void setOffsetParameters( int quadSegments, int joinStyle, double miterLimit );
%Docstring
.. versionadded:: 3.0
%End

int maxFeatureCount() const;
Expand Down
2 changes: 2 additions & 0 deletions src/app/qgisapp.cpp
Expand Up @@ -2285,6 +2285,8 @@ void QgisApp::createToolBars()

mTracer = new QgsMapCanvasTracer( mMapCanvas, messageBar() );
mTracer->setActionEnableTracing( mSnappingWidget->enableTracingAction() );
connect( mSnappingWidget->tracingOffsetSpinBox(), static_cast< void ( QgsDoubleSpinBox::* )( double ) >( &QgsDoubleSpinBox::valueChanged ),
this, [ = ]( double v ) { mTracer->setOffset( v ); } );

QList<QAction *> toolbarMenuActions;
// Set action names so that they can be used in customization
Expand Down
20 changes: 19 additions & 1 deletion src/app/qgssnappingwidget.cpp
Expand Up @@ -16,15 +16,17 @@

#include <QAction>
#include <QComboBox>
#include <QDoubleSpinBox>
#include <QFont>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QMenu>
#include <QToolBar>
#include <QToolButton>
#include <QWidgetAction>

#include "qgsapplication.h"
#include "qgsdoublespinbox.h"
#include "qgslayertreegroup.h"
#include "qgslayertree.h"
#include "qgslayertreeview.h"
Expand Down Expand Up @@ -148,6 +150,22 @@ QgsSnappingWidget::QgsSnappingWidget( QgsProject *project, QgsMapCanvas *canvas,
mEnableTracingAction->setShortcut( tr( "T", "Enable Tracing" ) );
mEnableTracingAction->setObjectName( QStringLiteral( "EnableTracingAction" ) );

mTracingOffsetSpinBox = new QgsDoubleSpinBox;
mTracingOffsetSpinBox->setRange( -1000000, 1000000 );
mTracingOffsetSpinBox->setDecimals( 6 );
mTracingOffsetSpinBox->setClearValue( 0 );
mTracingOffsetSpinBox->setClearValueMode( QgsDoubleSpinBox::CustomValue );
QMenu *tracingMenu = new QMenu( this );
QWidgetAction *widgetAction = new QWidgetAction( tracingMenu );
QVBoxLayout *tracingWidgetLayout = new QVBoxLayout;
tracingWidgetLayout->addWidget( new QLabel( "Offset" ) );
tracingWidgetLayout->addWidget( mTracingOffsetSpinBox );
QWidget *tracingWidget = new QWidget;
tracingWidget->setLayout( tracingWidgetLayout );
widgetAction->setDefaultWidget( tracingWidget );
tracingMenu->addAction( widgetAction );
mEnableTracingAction->setMenu( tracingMenu );

// layout
if ( mDisplayMode == ToolBar )
{
Expand Down
5 changes: 5 additions & 0 deletions src/app/qgssnappingwidget.h
Expand Up @@ -24,6 +24,7 @@ class QFont;
class QToolButton;
class QTreeView;

class QgsDoubleSpinBox;
class QgsLayerTreeGroup;
class QgsLayerTreeNode;
class QgsLayerTreeView;
Expand Down Expand Up @@ -74,6 +75,9 @@ class APP_EXPORT QgsSnappingWidget : public QWidget
*/
QAction *enableTracingAction() { return mEnableTracingAction; }

//! Returns spin box used to set offset for tracing
QgsDoubleSpinBox *tracingOffsetSpinBox() { return mTracingOffsetSpinBox; }

signals:
void snappingConfigChanged();

Expand Down Expand Up @@ -136,6 +140,7 @@ class APP_EXPORT QgsSnappingWidget : public QWidget
QAction *mTopologicalEditingAction = nullptr;
QAction *mIntersectionSnappingAction = nullptr;
QAction *mEnableTracingAction = nullptr;
QgsDoubleSpinBox *mTracingOffsetSpinBox = nullptr;
QTreeView *mLayerTreeView = nullptr;

void cleanGroup( QgsLayerTreeNode *node );
Expand Down
41 changes: 41 additions & 0 deletions src/core/qgstracer.cpp
Expand Up @@ -615,6 +615,25 @@ void QgsTracer::setExtent( const QgsRectangle &extent )
invalidateGraph();
}

void QgsTracer::setOffset( double offset )
{
mOffset = offset;
}

void QgsTracer::offsetParameters( int &quadSegments, int &joinStyle, double &miterLimit )
{
quadSegments = mOffsetSegments;
joinStyle = mOffsetJoinStyle;
miterLimit = mOffsetMiterLimit;
}

void QgsTracer::setOffsetParameters( int quadSegments, int joinStyle, double miterLimit )
{
mOffsetSegments = quadSegments;
mOffsetJoinStyle = joinStyle;
mOffsetMiterLimit = miterLimit;
}

bool QgsTracer::init()
{
if ( mGraph )
Expand Down Expand Up @@ -695,6 +714,28 @@ QVector<QgsPointXY> QgsTracer::findShortestPath( const QgsPointXY &p1, const Qgs

resetGraph( *mGraph );

if ( !points.isEmpty() && mOffset != 0 )
{
QList<QgsPointXY> pointsInput( points.toList() );
QgsLineString linestring( pointsInput );
std::unique_ptr<QgsGeometryEngine> linestringEngine( QgsGeometry::createGeometryEngine( &linestring ) );
std::unique_ptr<QgsAbstractGeometry> linestringOffset( linestringEngine->offsetCurve( mOffset, mOffsetSegments, mOffsetJoinStyle, mOffsetMiterLimit ) );
if ( QgsLineString *ls2 = qgsgeometry_cast<QgsLineString *>( linestringOffset.get() ) )
{
points.clear();
for ( int i = 0; i < ls2->numPoints(); ++i )
points << QgsPointXY( ls2->pointN( i ) );

// sometimes (with negative offset?) the resulting curve is reversed
if ( points.count() >= 2 )
{
double diff = points[0].distance( p1 );
if ( !qgsDoubleNear( diff, mOffset ) )
std::reverse( points.begin(), points.end() );
}
}
}

if ( error )
*error = points.isEmpty() ? ErrNoPath : ErrNone;

Expand Down
24 changes: 24 additions & 0 deletions src/core/qgstracer.h
Expand Up @@ -63,6 +63,22 @@ class CORE_EXPORT QgsTracer : public QObject
//! Set extent to which graph's features will be limited (empty extent means no limit)
void setExtent( const QgsRectangle &extent );

//! Get offset in map units that should be applied to the traced paths returned from findShortestPath().
//! Positive offset for right side, negative offset for left side.
//! \since QGIS 3.0
double offset() const { return mOffset; }
//! Set offset in map units that should be applied to the traced paths returned from findShortestPath().
//! Positive offset for right side, negative offset for left side.
//! \since QGIS 3.0
void setOffset( double offset );

//! Get extra parameters for offset curve algorithm (used when offset is non-zero)
//! \since QGIS 3.0
void offsetParameters( int &quadSegments SIP_OUT, int &joinStyle SIP_OUT, double &miterLimit SIP_OUT );
//! Set extra parameters for offset curve algorithm (used when offset is non-zero)
//! \since QGIS 3.0
void setOffsetParameters( int quadSegments, int joinStyle, double miterLimit );

//! 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.
Expand Down Expand Up @@ -138,6 +154,14 @@ class CORE_EXPORT QgsTracer : public QObject
//! Extent for graph building (empty extent means no limit)
QgsRectangle mExtent;

//! Offset in map units that should be applied to the traced paths
double mOffset = 0;
//! Offset parameter: Number of segments (approximation of circle quarter) when using round join style
int mOffsetSegments = 8;
//! Offset parameter: Join style (1 = round, 2 = miter, 3 = bevel)
int mOffsetJoinStyle = 2;
//! Offset parameter: Limit for miter join style
double mOffsetMiterLimit = 5.;
/**
* 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
Expand Down
23 changes: 19 additions & 4 deletions src/gui/qgsmaptoolcapture.cpp
Expand Up @@ -225,22 +225,37 @@ bool QgsMapToolCapture::tracingAddVertex( const QgsPointXY &point )
if ( points.isEmpty() )
return false; // ignore the vertex - can't find path to the end point!

// normally we skip the first vertex because it is already included, but if we
// use offset then we may need to include it
int indexStart = 1;
if ( !mCaptureCurve.isEmpty() )
{
QgsPoint lp; // in layer coords
if ( nextPoint( QgsPoint( points[0] ), lp ) != 0 )
return false;
QgsPoint last;
QgsVertexId::VertexType type;
mCaptureCurve.pointAt( mCaptureCurve.numPoints() - 1, last, type );
if ( last != lp )
indexStart = 0;
}

// transform points
QgsPointSequence layerPoints;
QgsPoint lp; // in layer coords
for ( int i = 1; i < points.count(); ++i )
for ( int i = 0; i < points.count(); ++i )
{
if ( nextPoint( QgsPoint( points[i] ), lp ) != 0 )
return false;
layerPoints << lp;
}

for ( int i = 1; i < points.count(); ++i )
for ( int i = indexStart; i < points.count(); ++i )
{
if ( points[i] == points[i - 1] )
if ( i > 0 && 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] );
mCaptureCurve.addVertex( layerPoints[i] );
mSnappingMatches.append( QgsPointLocator::Match() );
}

Expand Down
40 changes: 40 additions & 0 deletions tests/src/core/testqgstracer.cpp
Expand Up @@ -34,6 +34,7 @@ class TestQgsTracer : public QObject
void testExtent();
void testReprojection();
void testCurved();
void testOffset();

private:

Expand Down Expand Up @@ -351,6 +352,45 @@ void TestQgsTracer::testCurved()
delete vl;
}

void TestQgsTracer::testOffset()
{
QStringList wkts;
wkts << QStringLiteral( "LINESTRING(0 0, 0 10)" )
<< QStringLiteral( "LINESTRING(0 0, 10 0)" )
<< QStringLiteral( "LINESTRING(0 10, 20 10)" )
<< QStringLiteral( "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<QgsVectorLayer *>() << vl );

// curve on the right side
tracer.setOffset( -1 );
QgsPolyline points1 = tracer.findShortestPath( QgsPointXY( 0, 0 ), QgsPointXY( 20, 10 ) );
QCOMPARE( points1.count(), 3 );
QCOMPARE( points1[0], QgsPointXY( 0, -1 ) );
QCOMPARE( points1[1], QgsPointXY( 10 + sqrt( 2 ) - 1, -1 ) );
QCOMPARE( points1[2], QgsPointXY( 20 + sqrt( 2 ) / 2, 10 - sqrt( 2 ) / 2 ) );

// curve on the left side
tracer.setOffset( 1 );
QgsPolyline points2 = tracer.findShortestPath( QgsPointXY( 0, 0 ), QgsPointXY( 20, 10 ) );
QCOMPARE( points2.count(), 3 );
QCOMPARE( points2[0], QgsPointXY( 0, 1 ) );
QCOMPARE( points2[1], QgsPointXY( 10 - sqrt( 2 ) + 1, 1 ) );
QCOMPARE( points2[2], QgsPointXY( 20 - sqrt( 2 ) / 2, 10 + sqrt( 2 ) / 2 ) );

delete vl;
}


QGSTEST_MAIN( TestQgsTracer )
#include "testqgstracer.moc"

0 comments on commit b140b29

Please sign in to comment.