Skip to content

Commit 9d8734c

Browse files
authored
Merge pull request #5195 from wonder-sk/tracing-with-offset
Tracing with offset
2 parents 217c0e7 + 863197f commit 9d8734c

13 files changed

+610
-8
lines changed

python/core/qgstracer.sip

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,33 @@ Get extent to which graph's features will be limited (empty extent means no limi
6060
void setExtent( const QgsRectangle &extent );
6161
%Docstring
6262
Set extent to which graph's features will be limited (empty extent means no limit)
63+
%End
64+
65+
double offset() const;
66+
%Docstring
67+
Get offset in map units that should be applied to the traced paths returned from findShortestPath().
68+
Positive offset for right side, negative offset for left side.
69+
.. versionadded:: 3.0
70+
:rtype: float
71+
%End
72+
73+
void setOffset( double offset );
74+
%Docstring
75+
Set offset in map units that should be applied to the traced paths returned from findShortestPath().
76+
Positive offset for right side, negative offset for left side.
77+
.. versionadded:: 3.0
78+
%End
79+
80+
void offsetParameters( int &quadSegments /Out/, int &joinStyle /Out/, double &miterLimit /Out/ );
81+
%Docstring
82+
Get extra parameters for offset curve algorithm (used when offset is non-zero)
83+
.. versionadded:: 3.0
84+
%End
85+
86+
void setOffsetParameters( int quadSegments, int joinStyle, double miterLimit );
87+
%Docstring
88+
Set extra parameters for offset curve algorithm (used when offset is non-zero)
89+
.. versionadded:: 3.0
6390
%End
6491

6592
int maxFeatureCount() const;

python/gui/qgsmaptooladvanceddigitizing.sip

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ Catch the mouse move event, filters it, transforms it to map coordinates and sen
108108
.. versionadded:: 3.0
109109
%End
110110

111+
public:
112+
111113
virtual void cadCanvasPressEvent( QgsMapMouseEvent *e );
112114
%Docstring
113115
Override this method when subclassing this class.

src/app/qgisapp.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,6 +1246,7 @@ QgisApp::QgisApp()
12461246
mLayerTreeView = new QgsLayerTreeView( this );
12471247
mUndoWidget = new QgsUndoWidget( nullptr, mMapCanvas );
12481248
mInfoBar = new QgsMessageBar( centralWidget() );
1249+
mAdvancedDigitizingDockWidget = new QgsAdvancedDigitizingDockWidget( mMapCanvas, this );
12491250
// More tests may need more members to be initialized
12501251
}
12511252

@@ -2285,6 +2286,8 @@ void QgisApp::createToolBars()
22852286

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

22892292
QList<QAction *> toolbarMenuActions;
22902293
// Set action names so that they can be used in customization

src/app/qgssnappingwidget.cpp

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,17 @@
1616

1717
#include <QAction>
1818
#include <QComboBox>
19-
#include <QDoubleSpinBox>
2019
#include <QFont>
2120
#include <QHBoxLayout>
2221
#include <QHeaderView>
22+
#include <QLabel>
2323
#include <QMenu>
2424
#include <QToolBar>
2525
#include <QToolButton>
26+
#include <QWidgetAction>
2627

2728
#include "qgsapplication.h"
29+
#include "qgsdoublespinbox.h"
2830
#include "qgslayertreegroup.h"
2931
#include "qgslayertree.h"
3032
#include "qgslayertreeview.h"
@@ -148,6 +150,22 @@ QgsSnappingWidget::QgsSnappingWidget( QgsProject *project, QgsMapCanvas *canvas,
148150
mEnableTracingAction->setShortcut( tr( "T", "Enable Tracing" ) );
149151
mEnableTracingAction->setObjectName( QStringLiteral( "EnableTracingAction" ) );
150152

153+
mTracingOffsetSpinBox = new QgsDoubleSpinBox;
154+
mTracingOffsetSpinBox->setRange( -1000000, 1000000 );
155+
mTracingOffsetSpinBox->setDecimals( 6 );
156+
mTracingOffsetSpinBox->setClearValue( 0 );
157+
mTracingOffsetSpinBox->setClearValueMode( QgsDoubleSpinBox::CustomValue );
158+
QMenu *tracingMenu = new QMenu( this );
159+
QWidgetAction *widgetAction = new QWidgetAction( tracingMenu );
160+
QVBoxLayout *tracingWidgetLayout = new QVBoxLayout;
161+
tracingWidgetLayout->addWidget( new QLabel( "Offset" ) );
162+
tracingWidgetLayout->addWidget( mTracingOffsetSpinBox );
163+
QWidget *tracingWidget = new QWidget;
164+
tracingWidget->setLayout( tracingWidgetLayout );
165+
widgetAction->setDefaultWidget( tracingWidget );
166+
tracingMenu->addAction( widgetAction );
167+
mEnableTracingAction->setMenu( tracingMenu );
168+
151169
// layout
152170
if ( mDisplayMode == ToolBar )
153171
{

src/app/qgssnappingwidget.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class QFont;
2424
class QToolButton;
2525
class QTreeView;
2626

27+
class QgsDoubleSpinBox;
2728
class QgsLayerTreeGroup;
2829
class QgsLayerTreeNode;
2930
class QgsLayerTreeView;
@@ -74,6 +75,9 @@ class APP_EXPORT QgsSnappingWidget : public QWidget
7475
*/
7576
QAction *enableTracingAction() { return mEnableTracingAction; }
7677

78+
//! Returns spin box used to set offset for tracing
79+
QgsDoubleSpinBox *tracingOffsetSpinBox() { return mTracingOffsetSpinBox; }
80+
7781
signals:
7882
void snappingConfigChanged();
7983

@@ -136,6 +140,7 @@ class APP_EXPORT QgsSnappingWidget : public QWidget
136140
QAction *mTopologicalEditingAction = nullptr;
137141
QAction *mIntersectionSnappingAction = nullptr;
138142
QAction *mEnableTracingAction = nullptr;
143+
QgsDoubleSpinBox *mTracingOffsetSpinBox = nullptr;
139144
QTreeView *mLayerTreeView = nullptr;
140145

141146
void cleanGroup( QgsLayerTreeNode *node );

src/core/qgstracer.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,25 @@ void QgsTracer::setExtent( const QgsRectangle &extent )
615615
invalidateGraph();
616616
}
617617

618+
void QgsTracer::setOffset( double offset )
619+
{
620+
mOffset = offset;
621+
}
622+
623+
void QgsTracer::offsetParameters( int &quadSegments, int &joinStyle, double &miterLimit )
624+
{
625+
quadSegments = mOffsetSegments;
626+
joinStyle = mOffsetJoinStyle;
627+
miterLimit = mOffsetMiterLimit;
628+
}
629+
630+
void QgsTracer::setOffsetParameters( int quadSegments, int joinStyle, double miterLimit )
631+
{
632+
mOffsetSegments = quadSegments;
633+
mOffsetJoinStyle = joinStyle;
634+
mOffsetMiterLimit = miterLimit;
635+
}
636+
618637
bool QgsTracer::init()
619638
{
620639
if ( mGraph )
@@ -695,6 +714,30 @@ QVector<QgsPointXY> QgsTracer::findShortestPath( const QgsPointXY &p1, const Qgs
695714

696715
resetGraph( *mGraph );
697716

717+
if ( !points.isEmpty() && mOffset != 0 )
718+
{
719+
QList<QgsPointXY> pointsInput( points.toList() );
720+
QgsLineString linestring( pointsInput );
721+
std::unique_ptr<QgsGeometryEngine> linestringEngine( QgsGeometry::createGeometryEngine( &linestring ) );
722+
std::unique_ptr<QgsAbstractGeometry> linestringOffset( linestringEngine->offsetCurve( mOffset, mOffsetSegments, mOffsetJoinStyle, mOffsetMiterLimit ) );
723+
if ( QgsLineString *ls2 = qgsgeometry_cast<QgsLineString *>( linestringOffset.get() ) )
724+
{
725+
points.clear();
726+
for ( int i = 0; i < ls2->numPoints(); ++i )
727+
points << QgsPointXY( ls2->pointN( i ) );
728+
729+
// sometimes (with negative offset?) the resulting curve is reversed
730+
if ( points.count() >= 2 )
731+
{
732+
QgsPointXY res1 = points.first(), res2 = points.last();
733+
double diffNormal = res1.distance( p1 ) + res2.distance( p2 );
734+
double diffReversed = res1.distance( p2 ) + res2.distance( p1 );
735+
if ( diffReversed < diffNormal )
736+
std::reverse( points.begin(), points.end() );
737+
}
738+
}
739+
}
740+
698741
if ( error )
699742
*error = points.isEmpty() ? ErrNoPath : ErrNone;
700743

src/core/qgstracer.h

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,32 @@ class CORE_EXPORT QgsTracer : public QObject
6363
//! Set extent to which graph's features will be limited (empty extent means no limit)
6464
void setExtent( const QgsRectangle &extent );
6565

66+
/**
67+
* Get offset in map units that should be applied to the traced paths returned from findShortestPath().
68+
* Positive offset for right side, negative offset for left side.
69+
* \since QGIS 3.0
70+
*/
71+
double offset() const { return mOffset; }
72+
73+
/**
74+
* Set offset in map units that should be applied to the traced paths returned from findShortestPath().
75+
* Positive offset for right side, negative offset for left side.
76+
* \since QGIS 3.0
77+
*/
78+
void setOffset( double offset );
79+
80+
/**
81+
* Get extra parameters for offset curve algorithm (used when offset is non-zero)
82+
* \since QGIS 3.0
83+
*/
84+
void offsetParameters( int &quadSegments SIP_OUT, int &joinStyle SIP_OUT, double &miterLimit SIP_OUT );
85+
86+
/**
87+
* Set extra parameters for offset curve algorithm (used when offset is non-zero)
88+
* \since QGIS 3.0
89+
*/
90+
void setOffsetParameters( int quadSegments, int joinStyle, double miterLimit );
91+
6692
//! Get maximum possible number of features in graph. If the number is exceeded, graph is not created.
6793
int maxFeatureCount() const { return mMaxFeatureCount; }
6894
//! Get maximum possible number of features in graph. If the number is exceeded, graph is not created.
@@ -138,6 +164,15 @@ class CORE_EXPORT QgsTracer : public QObject
138164
//! Extent for graph building (empty extent means no limit)
139165
QgsRectangle mExtent;
140166

167+
//! Offset in map units that should be applied to the traced paths
168+
double mOffset = 0;
169+
//! Offset parameter: Number of segments (approximation of circle quarter) when using round join style
170+
int mOffsetSegments = 8;
171+
//! Offset parameter: Join style (1 = round, 2 = miter, 3 = bevel)
172+
int mOffsetJoinStyle = 2;
173+
//! Offset parameter: Limit for miter join style
174+
double mOffsetMiterLimit = 5.;
175+
141176
/**
142177
* Limit of how many features can be in the graph (0 means no limit).
143178
* This is to avoid possibly long graph preparation for complicated layers

src/gui/qgsmaptooladvanceddigitizing.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ class GUI_EXPORT QgsMapToolAdvancedDigitizing : public QgsMapToolEdit
106106
*/
107107
void setAutoSnapEnabled( bool enabled ) { mAutoSnapEnabled = enabled; }
108108

109+
public:
110+
109111
/**
110112
* Override this method when subclassing this class.
111113
* This will receive adapted events from the cad system whenever a

src/gui/qgsmaptoolcapture.cpp

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ QgsPointXY QgsMapToolCapture::tracingStartPoint()
142142
QgsMapLayer *layer = mCanvas->currentLayer();
143143
if ( !layer )
144144
return QgsPointXY();
145+
146+
// if we have starting point from previous trace, then preferably use that one
147+
// (useful when tracing with offset)
148+
if ( mTracingStartPoint != QgsPointXY() )
149+
return mTracingStartPoint;
150+
145151
QgsPoint v = mCaptureCurve.endPoint();
146152
return toMapCoordinates( layer, QgsPointXY( v.x(), v.y() ) );
147153
}
@@ -179,6 +185,26 @@ bool QgsMapToolCapture::tracingMouseMove( QgsMapMouseEvent *e )
179185
if ( mCaptureMode == CapturePolygon )
180186
mTempRubberBand->addPoint( *mRubberBand->getPoint( 0, 0 ), false );
181187

188+
// if there is offset, we need to fix the rubber bands to make sure they are aligned correctly.
189+
// There are two cases we need to sort out:
190+
// 1. the last point of mRubberBand may need to be moved off the traced curve to respect the offset
191+
// 2. extra first point of mTempRubberBand may be needed if there is gap between where mRubberBand ends and trace starts
192+
if ( mRubberBand->numberOfVertices() != 0 )
193+
{
194+
QgsPointXY lastPoint = *mRubberBand->getPoint( 0, mRubberBand->numberOfVertices() - 1 );
195+
if ( lastPoint == pt0 && points[0] != lastPoint )
196+
{
197+
// if rubber band had just one point, for some strange reason it contains the point twice
198+
// we only want to move the last point if there are multiple points already
199+
if ( mRubberBand->numberOfVertices() > 2 || ( mRubberBand->numberOfVertices() == 2 && *mRubberBand->getPoint( 0, 0 ) != *mRubberBand->getPoint( 0, 1 ) ) )
200+
mRubberBand->movePoint( points[0] );
201+
}
202+
else
203+
{
204+
mTempRubberBand->addPoint( lastPoint, false );
205+
}
206+
}
207+
182208
// update rubberband
183209
for ( int i = 0; i < points.count(); ++i )
184210
mTempRubberBand->addPoint( points.at( i ), i == points.count() - 1 );
@@ -225,22 +251,53 @@ bool QgsMapToolCapture::tracingAddVertex( const QgsPointXY &point )
225251
if ( points.isEmpty() )
226252
return false; // ignore the vertex - can't find path to the end point!
227253

254+
if ( !mCaptureCurve.isEmpty() )
255+
{
256+
QgsPoint lp; // in layer coords
257+
if ( nextPoint( QgsPoint( pt0 ), lp ) != 0 )
258+
return false;
259+
QgsPoint last;
260+
QgsVertexId::VertexType type;
261+
mCaptureCurve.pointAt( mCaptureCurve.numPoints() - 1, last, type );
262+
if ( last == lp )
263+
{
264+
// remove the last point in the curve if it is the same as our first point
265+
if ( mCaptureCurve.numPoints() != 2 )
266+
mCaptureCurve.deleteVertex( QgsVertexId( 0, 0, mCaptureCurve.numPoints() - 1 ) );
267+
else
268+
{
269+
// there is a strange behavior in deleteVertex() that with just two points
270+
// the whole curve is cleared - so we need to do this little dance to work it around
271+
QgsPoint first = mCaptureCurve.startPoint();
272+
mCaptureCurve.clear();
273+
mCaptureCurve.addVertex( first );
274+
}
275+
// for unknown reasons, rubber band has 2 points even if only one point has been added - handle that case
276+
if ( mRubberBand->numberOfVertices() == 2 && *mRubberBand->getPoint( 0, 0 ) == *mRubberBand->getPoint( 0, 1 ) )
277+
mRubberBand->removeLastPoint();
278+
mRubberBand->removeLastPoint();
279+
mSnappingMatches.removeLast();
280+
}
281+
}
282+
228283
// transform points
229284
QgsPointSequence layerPoints;
230285
QgsPoint lp; // in layer coords
231-
for ( int i = 1; i < points.count(); ++i )
286+
for ( int i = 0; i < points.count(); ++i )
232287
{
233288
if ( nextPoint( QgsPoint( points[i] ), lp ) != 0 )
234289
return false;
235290
layerPoints << lp;
236291
}
237292

238-
for ( int i = 1; i < points.count(); ++i )
293+
for ( int i = 0; i < points.count(); ++i )
239294
{
240-
if ( points[i] == points[i - 1] )
295+
if ( i == 0 && !mCaptureCurve.isEmpty() && mCaptureCurve.endPoint() == layerPoints[0] )
296+
continue; // avoid duplicate of the first vertex
297+
if ( i > 0 && points[i] == points[i - 1] )
241298
continue; // avoid duplicate vertices if there are any
242299
mRubberBand->addPoint( points[i], i == points.count() - 1 );
243-
mCaptureCurve.addVertex( layerPoints[i - 1] );
300+
mCaptureCurve.addVertex( layerPoints[i] );
244301
mSnappingMatches.append( QgsPointLocator::Match() );
245302
}
246303

@@ -291,9 +348,7 @@ void QgsMapToolCapture::cadCanvasMoveEvent( QgsMapMouseEvent *e )
291348

292349
if ( !hasTrace )
293350
{
294-
if ( mCaptureCurve.numPoints() > 0 &&
295-
( ( mCaptureMode == CaptureLine && mTempRubberBand->numberOfVertices() != 2 ) ||
296-
( mCaptureMode == CapturePolygon && mTempRubberBand->numberOfVertices() != 3 ) ) )
351+
if ( mCaptureCurve.numPoints() > 0 )
297352
{
298353
// fix temporary rubber band after tracing which may have added multiple points
299354
mTempRubberBand->reset( mCaptureMode == CapturePolygon ? QgsWkbTypes::PolygonGeometry : QgsWkbTypes::LineGeometry );
@@ -303,6 +358,10 @@ void QgsMapToolCapture::cadCanvasMoveEvent( QgsMapMouseEvent *e )
303358
QgsPointXY mapPt = toMapCoordinates( qobject_cast<QgsVectorLayer *>( mCanvas->currentLayer() ), QgsPointXY( pt.x(), pt.y() ) );
304359
mTempRubberBand->addPoint( mapPt );
305360
mTempRubberBand->addPoint( point );
361+
362+
// fix existing rubber band after tracing - the last point may have been moved if using offset
363+
if ( mRubberBand->numberOfVertices() )
364+
mRubberBand->movePoint( mapPt );
306365
}
307366
else
308367
mTempRubberBand->movePoint( point );
@@ -419,6 +478,10 @@ int QgsMapToolCapture::addVertex( const QgsPointXY &point, const QgsPointLocator
419478
traceCreated = tracingAddVertex( point );
420479
}
421480

481+
// keep new tracing start point if we created a trace. This is useful when tracing with
482+
// offset so that the user stays "snapped"
483+
mTracingStartPoint = traceCreated ? point : QgsPointXY();
484+
422485
if ( !traceCreated )
423486
{
424487
// ordinary digitizing
@@ -499,6 +562,8 @@ QList<QgsPointLocator::Match> QgsMapToolCapture::snappingMatches() const
499562

500563
void QgsMapToolCapture::undo()
501564
{
565+
mTracingStartPoint = QgsPointXY();
566+
502567
if ( mRubberBand )
503568
{
504569
int rubberBandSize = mRubberBand->numberOfVertices();
@@ -585,6 +650,8 @@ void QgsMapToolCapture::stopCapturing()
585650

586651
mGeomErrors.clear();
587652

653+
mTracingStartPoint = QgsPointXY();
654+
588655
#ifdef Q_OS_WIN
589656
Q_FOREACH ( QWidget *w, qApp->topLevelWidgets() )
590657
{

0 commit comments

Comments
 (0)