1 /***************************************************************************
2                               qgsmaptooloffsetcurve.cpp
3     ------------------------------------------------------------
4     begin                : February 2012
5     copyright            : (C) 2012 by Marco Hugentobler
6     email                : marco dot hugentobler at sourcepole dot ch
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 <QGraphicsProxyWidget>
17 #include <QGridLayout>
18 #include <QLabel>
19 
20 #include "qgsdoublespinbox.h"
21 #include "qgsfeatureiterator.h"
22 #include "qgsmaptooloffsetcurve.h"
23 #include "qgsmapcanvas.h"
24 #include "qgsproject.h"
25 #include "qgsrubberband.h"
26 #include "qgssnappingutils.h"
27 #include "qgsvectorlayer.h"
28 #include "qgssnapindicator.h"
29 #include "qgssnappingconfig.h"
30 #include "qgssettingsregistrycore.h"
31 #include "qgisapp.h"
32 #include "qgsmapmouseevent.h"
33 #include "qgslogger.h"
34 #include "qgsvectorlayerutils.h"
35 
QgsMapToolOffsetCurve(QgsMapCanvas * canvas)36 QgsMapToolOffsetCurve::QgsMapToolOffsetCurve( QgsMapCanvas *canvas )
37   : QgsMapToolEdit( canvas )
38   , mSnapIndicator( std::make_unique< QgsSnapIndicator >( canvas ) )
39 {
40   mToolName = tr( "Map tool offset curve" );
41 }
42 
~QgsMapToolOffsetCurve()43 QgsMapToolOffsetCurve::~QgsMapToolOffsetCurve()
44 {
45   cancel();
46 }
47 
keyPressEvent(QKeyEvent * e)48 void QgsMapToolOffsetCurve::keyPressEvent( QKeyEvent *e )
49 {
50   if ( e && e->key() == Qt::Key_Escape && !e->isAutoRepeat() )
51   {
52     cancel();
53   }
54   else
55   {
56     QgsMapToolEdit::keyPressEvent( e );
57   }
58 }
59 
60 
canvasReleaseEvent(QgsMapMouseEvent * e)61 void QgsMapToolOffsetCurve::canvasReleaseEvent( QgsMapMouseEvent *e )
62 {
63   mCtrlHeldOnFirstClick = false;
64 
65   if ( e->button() == Qt::RightButton )
66   {
67     cancel();
68     return;
69   }
70 
71   if ( mOriginalGeometry.isNull() )
72   {
73     // first click, get feature to modify
74     deleteRubberBandAndGeometry();
75     mGeometryModified = false;
76 
77     QgsPointLocator::Match match;
78 
79     if ( e->modifiers() & Qt::ControlModifier )
80     {
81       match = mCanvas->snappingUtils()->snapToMap( e->pos(), nullptr );
82     }
83     else
84     {
85       match = mCanvas->snappingUtils()->snapToCurrentLayer( e->pos(),
86               QgsPointLocator::Types( QgsPointLocator::Edge | QgsPointLocator::Area ) );
87     }
88 
89     if ( auto *lLayer = match.layer() )
90     {
91       mSourceLayer = lLayer;
92       QgsFeature fet;
93       if ( lLayer->getFeatures( QgsFeatureRequest( match.featureId() ) ).nextFeature( fet ) )
94       {
95         mSourceFeature = fet;
96         mCtrlHeldOnFirstClick = ( e->modifiers() & Qt::ControlModifier ); //no geometry modification if ctrl is pressed
97         prepareGeometry( match, fet );
98         mRubberBand = createRubberBand();
99         if ( mRubberBand )
100         {
101           mRubberBand->setToGeometry( mManipulatedGeometry, lLayer );
102         }
103         mModifiedFeature = fet.id();
104         createUserInputWidget();
105 
106         const bool hasZ = QgsWkbTypes::hasZ( mSourceLayer->wkbType() );
107         const bool hasM = QgsWkbTypes::hasZ( mSourceLayer->wkbType() );
108         if ( hasZ || hasM )
109         {
110           emit messageEmitted( QStringLiteral( "layer %1 has %2%3%4 geometry. %2%3%4 values be set to 0 when using offset tool." )
111                                .arg( mSourceLayer->name(),
112                                      hasZ ? QStringLiteral( "Z" ) : QString(),
113                                      hasZ && hasM ? QStringLiteral( "/" ) : QString(),
114                                      hasM ? QStringLiteral( "M" ) : QString() )
115                                , Qgis::MessageLevel::Warning );
116         }
117       }
118     }
119 
120     if ( mOriginalGeometry.isNull() )
121     {
122       emit messageEmitted( tr( "Could not find a nearby feature in any vector layer." ) );
123       cancel();
124     }
125   }
126   else
127   {
128     // second click - apply changes
129     const double offset = calculateOffset( e->snapPoint() );
130     applyOffset( offset, e->modifiers() );
131   }
132 }
133 
applyOffsetFromWidget(double offset,Qt::KeyboardModifiers modifiers)134 void QgsMapToolOffsetCurve::applyOffsetFromWidget( double offset, Qt::KeyboardModifiers modifiers )
135 {
136   if ( mSourceLayer && !mOriginalGeometry.isNull() && !qgsDoubleNear( offset, 0 ) )
137   {
138     mGeometryModified = true;
139     applyOffset( offset, modifiers );
140   }
141 }
142 
applyOffset(double offset,Qt::KeyboardModifiers modifiers)143 void QgsMapToolOffsetCurve::applyOffset( double offset, Qt::KeyboardModifiers modifiers )
144 {
145   if ( !mSourceLayer || offset == 0.0 )
146   {
147     cancel();
148     return;
149   }
150 
151   updateGeometryAndRubberBand( offset );
152 
153   // no modification
154   if ( !mGeometryModified )
155   {
156     cancel();
157     return;
158   }
159 
160   if ( mModifiedPart >= 0 )
161   {
162     QgsGeometry geometry;
163     int partIndex = 0;
164     const QgsWkbTypes::Type geomType = mOriginalGeometry.wkbType();
165     if ( QgsWkbTypes::geometryType( geomType ) == QgsWkbTypes::LineGeometry )
166     {
167       QgsMultiPolylineXY newMultiLine;
168       const QgsMultiPolylineXY multiLine = mOriginalGeometry.asMultiPolyline();
169       QgsMultiPolylineXY::const_iterator it = multiLine.constBegin();
170       for ( ; it != multiLine.constEnd(); ++it )
171       {
172         if ( partIndex == mModifiedPart )
173         {
174           newMultiLine.append( mModifiedGeometry.asPolyline() );
175         }
176         else
177         {
178           newMultiLine.append( *it );
179         }
180         partIndex++;
181       }
182       geometry = QgsGeometry::fromMultiPolylineXY( newMultiLine );
183     }
184     else
185     {
186       QgsMultiPolygonXY newMultiPoly;
187       const QgsMultiPolygonXY multiPoly = mOriginalGeometry.asMultiPolygon();
188       QgsMultiPolygonXY::const_iterator multiPolyIt = multiPoly.constBegin();
189       for ( ; multiPolyIt != multiPoly.constEnd(); ++multiPolyIt )
190       {
191         if ( partIndex == mModifiedPart )
192         {
193           if ( mModifiedGeometry.isMultipart() )
194           {
195             // not a ring
196             if ( mModifiedRing <= 0 )
197             {
198               // part became mulitpolygon, that means discard original rings from the part
199               newMultiPoly += mModifiedGeometry.asMultiPolygon();
200             }
201             else
202             {
203               // ring became multipolygon, oh boy!
204               QgsPolygonXY newPoly;
205               int ringIndex = 0;
206               QgsPolygonXY::const_iterator polyIt = multiPolyIt->constBegin();
207               for ( ; polyIt != multiPolyIt->constEnd(); ++polyIt )
208               {
209                 if ( ringIndex == mModifiedRing )
210                 {
211                   const QgsMultiPolygonXY ringParts = mModifiedGeometry.asMultiPolygon();
212                   QgsPolygonXY newRings;
213                   QgsMultiPolygonXY::const_iterator ringIt = ringParts.constBegin();
214                   for ( ; ringIt != ringParts.constEnd(); ++ringIt )
215                   {
216                     // the different parts of the new rings cannot have rings themselves
217                     newRings.append( ringIt->at( 0 ) );
218                   }
219                   newPoly += newRings;
220                 }
221                 else
222                 {
223                   newPoly.append( *polyIt );
224                 }
225                 ringIndex++;
226               }
227               newMultiPoly.append( newPoly );
228             }
229           }
230           else
231           {
232             // original part had no ring
233             if ( mModifiedRing == -1 )
234             {
235               newMultiPoly.append( mModifiedGeometry.asPolygon() );
236             }
237             else
238             {
239               QgsPolygonXY newPoly;
240               int ringIndex = 0;
241               QgsPolygonXY::const_iterator polyIt = multiPolyIt->constBegin();
242               for ( ; polyIt != multiPolyIt->constEnd(); ++polyIt )
243               {
244                 if ( ringIndex == mModifiedRing )
245                 {
246                   newPoly.append( mModifiedGeometry.asPolygon().at( 0 ) );
247                 }
248                 else
249                 {
250                   newPoly.append( *polyIt );
251                 }
252                 ringIndex++;
253               }
254               newMultiPoly.append( newPoly );
255             }
256           }
257         }
258         else
259         {
260           newMultiPoly.append( *multiPolyIt );
261         }
262         partIndex++;
263       }
264       geometry = QgsGeometry::fromMultiPolygonXY( newMultiPoly );
265     }
266     geometry.convertToMultiType();
267     mModifiedGeometry = geometry;
268   }
269   else if ( mModifiedRing >= 0 )
270   {
271     // original geometry had some rings
272     if ( mModifiedGeometry.isMultipart() )
273     {
274       // not a ring
275       if ( mModifiedRing == 0 )
276       {
277         // polygon became mulitpolygon, that means discard original rings from the part
278         // keep the modified geometry as is
279       }
280       else
281       {
282         QgsPolygonXY newPoly;
283         const QgsPolygonXY poly = mOriginalGeometry.asPolygon();
284 
285         // ring became multipolygon, oh boy!
286         int ringIndex = 0;
287         QgsPolygonXY::const_iterator polyIt = poly.constBegin();
288         for ( ; polyIt != poly.constEnd(); ++polyIt )
289         {
290           if ( ringIndex == mModifiedRing )
291           {
292             const QgsMultiPolygonXY ringParts = mModifiedGeometry.asMultiPolygon();
293             QgsPolygonXY newRings;
294             QgsMultiPolygonXY::const_iterator ringIt = ringParts.constBegin();
295             for ( ; ringIt != ringParts.constEnd(); ++ringIt )
296             {
297               // the different parts of the new rings cannot have rings themselves
298               newRings.append( ringIt->at( 0 ) );
299             }
300             newPoly += newRings;
301           }
302           else
303           {
304             newPoly.append( *polyIt );
305           }
306           ringIndex++;
307         }
308         mModifiedGeometry = QgsGeometry::fromPolygonXY( newPoly );
309       }
310     }
311     else
312     {
313       // simple case where modified geom is a polygon (not multi)
314       QgsPolygonXY newPoly;
315       const QgsPolygonXY poly = mOriginalGeometry.asPolygon();
316 
317       int ringIndex = 0;
318       QgsPolygonXY::const_iterator polyIt = poly.constBegin();
319       for ( ; polyIt != poly.constEnd(); ++polyIt )
320       {
321         if ( ringIndex == mModifiedRing )
322         {
323           newPoly.append( mModifiedGeometry.asPolygon().at( 0 ) );
324         }
325         else
326         {
327           newPoly.append( *polyIt );
328         }
329         ringIndex++;
330       }
331       mModifiedGeometry = QgsGeometry::fromPolygonXY( newPoly );
332     }
333   }
334 
335   if ( !mModifiedGeometry.isGeosValid() )
336   {
337     emit messageEmitted( tr( "Generated geometry is not valid." ), Qgis::MessageLevel::Critical );
338     // no cancel, continue editing.
339     return;
340   }
341 
342   QgsVectorLayer *destLayer = qobject_cast< QgsVectorLayer * >( canvas()->currentLayer() );
343   if ( !destLayer )
344     return;
345 
346   destLayer->beginEditCommand( tr( "Offset curve" ) );
347 
348   bool editOk = true;
349   if ( !mCtrlHeldOnFirstClick && !( modifiers & Qt::ControlModifier ) )
350   {
351     editOk = destLayer->changeGeometry( mModifiedFeature, mModifiedGeometry );
352   }
353   else
354   {
355     const QgsCoordinateTransform ct( mSourceLayer->crs(), destLayer->crs(), QgsProject::instance() );
356     try
357     {
358       QgsGeometry g = mModifiedGeometry;
359       g.transform( ct );
360 
361       QgsFeature f = mSourceFeature;
362       f.setGeometry( g );
363 
364       // auto convert source feature attributes to destination attributes, make geometry compatible
365       // note that this may result in multiple features, e.g. if inserting multipart feature into single-part layer
366       const QgsFeatureList features = QgsVectorLayerUtils::makeFeatureCompatible( f, destLayer );
367       for ( const QgsFeature &feature : features )
368       {
369         QgsAttributeMap attrs;
370         for ( int idx = 0; idx < destLayer->fields().count(); ++idx )
371         {
372           if ( !feature.attribute( idx ).isNull() )
373             attrs[idx] = feature.attribute( idx );
374         }
375 
376         QgsExpressionContext context = destLayer->createExpressionContext();
377         // use createFeature to ensure default values and provider side constraints are respected
378         f = QgsVectorLayerUtils::createFeature( destLayer, feature.geometry(), attrs, &context );
379 
380         editOk = editOk && destLayer->addFeature( f );
381       }
382     }
383     catch ( QgsCsException & )
384     {
385       editOk = false;
386     }
387   }
388 
389   if ( editOk )
390   {
391     destLayer->endEditCommand();
392   }
393   else
394   {
395     destLayer->destroyEditCommand();
396     emit messageEmitted( QStringLiteral( "Could not apply offset" ), Qgis::MessageLevel::Critical );
397   }
398 
399   deleteRubberBandAndGeometry();
400   deleteUserInputWidget();
401   destLayer->triggerRepaint();
402   mSourceLayer = nullptr;
403 }
404 
cancel()405 void QgsMapToolOffsetCurve::cancel()
406 {
407   deleteUserInputWidget();
408   deleteRubberBandAndGeometry();
409   mSourceLayer = nullptr;
410 }
411 
calculateOffset(const QgsPointXY & mapPoint)412 double QgsMapToolOffsetCurve::calculateOffset( const QgsPointXY &mapPoint )
413 {
414   double offset = 0.0;
415   if ( mSourceLayer )
416   {
417     //get offset from current position rectangular to feature
418     const QgsPointXY layerCoords = toLayerCoordinates( mSourceLayer, mapPoint );
419 
420     QgsPointXY minDistPoint;
421     int beforeVertex;
422     int leftOf = 0;
423 
424     offset = std::sqrt( mManipulatedGeometry.closestSegmentWithContext( layerCoords, minDistPoint, beforeVertex, &leftOf ) );
425     if ( QgsWkbTypes::geometryType( mManipulatedGeometry.wkbType() ) == QgsWkbTypes::LineGeometry )
426     {
427       offset = leftOf < 0 ? offset : -offset;
428     }
429     else
430     {
431       offset = mManipulatedGeometry.contains( &layerCoords ) ? -offset : offset;
432     }
433   }
434   return offset;
435 }
436 
canvasMoveEvent(QgsMapMouseEvent * e)437 void QgsMapToolOffsetCurve::canvasMoveEvent( QgsMapMouseEvent *e )
438 {
439   if ( mOriginalGeometry.isNull() || !mRubberBand )
440   {
441     QgsPointLocator::Match match;
442     if ( e->modifiers() & Qt::ControlModifier )
443     {
444       match = mCanvas->snappingUtils()->snapToMap( e->pos(), nullptr );
445     }
446     else
447     {
448       match = mCanvas->snappingUtils()->snapToCurrentLayer( e->pos(),
449               QgsPointLocator::Types( QgsPointLocator::Edge | QgsPointLocator::Area ) );
450     }
451     mSnapIndicator->setMatch( match );
452     return;
453   }
454 
455   mGeometryModified = true;
456 
457   const QgsPointXY mapPoint = e->snapPoint();
458   mSnapIndicator->setMatch( e->mapPointMatch() );
459 
460   const double offset = calculateOffset( mapPoint );
461 
462   if ( mUserInputWidget )
463   {
464     disconnect( mUserInputWidget, &QgsOffsetUserWidget::offsetChanged, this, &QgsMapToolOffsetCurve::updateGeometryAndRubberBand );
465     mUserInputWidget->setOffset( offset );
466     connect( mUserInputWidget, &QgsOffsetUserWidget::offsetChanged, this, &QgsMapToolOffsetCurve::updateGeometryAndRubberBand );
467     mUserInputWidget->setFocus( Qt::TabFocusReason );
468     mUserInputWidget->editor()->selectAll();
469   }
470 
471   //create offset geometry using geos
472   updateGeometryAndRubberBand( offset );
473 }
474 
prepareGeometry(const QgsPointLocator::Match & match,QgsFeature & snappedFeature)475 void QgsMapToolOffsetCurve::prepareGeometry( const QgsPointLocator::Match &match, QgsFeature &snappedFeature )
476 {
477   QgsVectorLayer *vl = match.layer();
478   if ( !vl )
479   {
480     return;
481   }
482 
483   mOriginalGeometry = QgsGeometry();
484   mManipulatedGeometry = QgsGeometry();
485   mModifiedPart = -1;
486   mModifiedRing = -1;
487 
488   //assign feature part by vertex number (snap to vertex) or by before vertex number (snap to segment)
489   const QgsGeometry geom = snappedFeature.geometry();
490   if ( geom.isNull() )
491   {
492     return;
493   }
494   mOriginalGeometry = geom;
495 
496   const QgsWkbTypes::Type geomType = geom.wkbType();
497   if ( QgsWkbTypes::geometryType( geomType ) == QgsWkbTypes::LineGeometry )
498   {
499     if ( !geom.isMultipart() )
500     {
501       mManipulatedGeometry = geom;
502     }
503     else
504     {
505       const int vertex = match.vertexIndex();
506       QgsVertexId vertexId;
507       geom.vertexIdFromVertexNr( vertex, vertexId );
508       mModifiedPart = vertexId.part;
509 
510       const QgsMultiPolylineXY multiLine = geom.asMultiPolyline();
511       mManipulatedGeometry = QgsGeometry::fromPolylineXY( multiLine.at( mModifiedPart ) );
512     }
513   }
514   else if ( QgsWkbTypes::geometryType( geomType ) == QgsWkbTypes::PolygonGeometry )
515   {
516     if ( !match.hasEdge() && !match.hasVertex() && match.hasArea() )
517     {
518       if ( !geom.isMultipart() )
519       {
520         mManipulatedGeometry = geom;
521       }
522       else
523       {
524         // get the correct part
525         QgsMultiPolygonXY mpolygon = geom.asMultiPolygon();
526         for ( int part = 0; part < mpolygon.count(); part++ ) // go through the polygons
527         {
528           const QgsPolygonXY &polygon = mpolygon[part];
529           const QgsGeometry partGeo = QgsGeometry::fromPolygonXY( polygon );
530           const QgsPointXY layerCoords = match.point();
531           if ( partGeo.contains( &layerCoords ) )
532           {
533             mModifiedPart = part;
534             mManipulatedGeometry = partGeo;
535           }
536         }
537       }
538     }
539     else if ( match.hasEdge() || match.hasVertex() )
540     {
541       const int vertex = match.vertexIndex();
542       QgsVertexId vertexId;
543       geom.vertexIdFromVertexNr( vertex, vertexId );
544       QgsDebugMsgLevel( QString::number( vertexId.ring ), 2 );
545 
546       if ( !geom.isMultipart() )
547       {
548         const QgsPolygonXY poly = geom.asPolygon();
549         // if has rings
550         if ( poly.count() > 0 )
551         {
552           mModifiedRing = vertexId.ring;
553           mManipulatedGeometry = QgsGeometry::fromPolygonXY( QgsPolygonXY() << poly.at( mModifiedRing ) );
554         }
555         else
556         {
557           mManipulatedGeometry = QgsGeometry::fromPolygonXY( poly );
558         }
559 
560       }
561       else
562       {
563         mModifiedPart = vertexId.part;
564         // get part, get ring
565         const QgsMultiPolygonXY multiPoly = geom.asMultiPolygon();
566         // if has rings
567         if ( multiPoly.at( mModifiedPart ).count() > 0 )
568         {
569           mModifiedRing = vertexId.ring;
570           mManipulatedGeometry = QgsGeometry::fromPolygonXY( QgsPolygonXY() << multiPoly.at( mModifiedPart ).at( mModifiedRing ) );
571         }
572         else
573         {
574           mManipulatedGeometry = QgsGeometry::fromPolygonXY( multiPoly.at( mModifiedPart ) );
575         }
576       }
577     }
578   }
579 }
580 
createUserInputWidget()581 void QgsMapToolOffsetCurve::createUserInputWidget()
582 {
583   deleteUserInputWidget();
584 
585   mUserInputWidget = new QgsOffsetUserWidget();
586   mUserInputWidget->setPolygonMode( QgsWkbTypes::geometryType( mOriginalGeometry.wkbType() ) != QgsWkbTypes::LineGeometry );
587   QgisApp::instance()->addUserInputWidget( mUserInputWidget );
588   mUserInputWidget->setFocus( Qt::TabFocusReason );
589 
590   connect( mUserInputWidget, &QgsOffsetUserWidget::offsetChanged, this, &QgsMapToolOffsetCurve::updateGeometryAndRubberBand );
591   connect( mUserInputWidget, &QgsOffsetUserWidget::offsetEditingFinished, this, &QgsMapToolOffsetCurve::applyOffsetFromWidget );
592   connect( mUserInputWidget, &QgsOffsetUserWidget::offsetEditingCanceled, this, &QgsMapToolOffsetCurve::cancel );
593 
594   connect( mUserInputWidget, &QgsOffsetUserWidget::offsetConfigChanged, this, [ = ] {updateGeometryAndRubberBand( mUserInputWidget->offset() );} );
595 }
596 
deleteUserInputWidget()597 void QgsMapToolOffsetCurve::deleteUserInputWidget()
598 {
599   if ( mUserInputWidget )
600   {
601     disconnect( mUserInputWidget, &QgsOffsetUserWidget::offsetChanged, this, &QgsMapToolOffsetCurve::updateGeometryAndRubberBand );
602     disconnect( mUserInputWidget, &QgsOffsetUserWidget::offsetEditingFinished, this, &QgsMapToolOffsetCurve::applyOffsetFromWidget );
603     disconnect( mUserInputWidget, &QgsOffsetUserWidget::offsetEditingCanceled, this, &QgsMapToolOffsetCurve::cancel );
604     mUserInputWidget->releaseKeyboard();
605     mUserInputWidget->deleteLater();
606   }
607   mUserInputWidget = nullptr;
608 }
609 
deleteRubberBandAndGeometry()610 void QgsMapToolOffsetCurve::deleteRubberBandAndGeometry()
611 {
612   mOriginalGeometry.set( nullptr );
613   mManipulatedGeometry.set( nullptr );
614   delete mRubberBand;
615   mRubberBand = nullptr;
616 }
617 
updateGeometryAndRubberBand(double offset)618 void QgsMapToolOffsetCurve::updateGeometryAndRubberBand( double offset )
619 {
620   if ( !mRubberBand || mOriginalGeometry.isNull() )
621   {
622     return;
623   }
624 
625   if ( !mSourceLayer )
626   {
627     return;
628   }
629 
630   QgsGeometry offsetGeom;
631   const Qgis::JoinStyle joinStyle = QgsSettingsRegistryCore::settingsDigitizingOffsetJoinStyle.value();
632   const int quadSegments = QgsSettingsRegistryCore::settingsDigitizingOffsetQuadSeg.value();
633   const double miterLimit = QgsSettingsRegistryCore::settingsDigitizingOffsetMiterLimit.value();
634   const Qgis::EndCapStyle capStyle = QgsSettingsRegistryCore::settingsDigitizingOffsetCapStyle.value();
635 
636 
637   if ( QgsWkbTypes::geometryType( mOriginalGeometry.wkbType() ) == QgsWkbTypes::LineGeometry )
638   {
639     offsetGeom = mManipulatedGeometry.offsetCurve( offset, quadSegments, joinStyle, miterLimit );
640   }
641   else
642   {
643     offsetGeom = mManipulatedGeometry.buffer( offset, quadSegments, capStyle, joinStyle, miterLimit );
644   }
645 
646   if ( offsetGeom.isNull() )
647   {
648     deleteRubberBandAndGeometry();
649     deleteUserInputWidget();
650     mSourceLayer = nullptr;
651     mGeometryModified = false;
652     emit messageDiscarded();
653     emit messageEmitted( tr( "Creating offset geometry failed: %1" ).arg( offsetGeom.lastError() ), Qgis::MessageLevel::Critical );
654   }
655   else
656   {
657     mModifiedGeometry = offsetGeom;
658     mRubberBand->setToGeometry( mModifiedGeometry, mSourceLayer );
659   }
660 }
661 
662 
663 // ******************
664 // Offset User Widget
665 
QgsOffsetUserWidget(QWidget * parent)666 QgsOffsetUserWidget::QgsOffsetUserWidget( QWidget *parent )
667   : QWidget( parent )
668 {
669   setupUi( this );
670 
671   mOffsetSpinBox->setDecimals( 6 );
672   mOffsetSpinBox->setClearValue( 0.0 );
673 
674   // fill comboboxes
675   mJoinStyleComboBox->addItem( tr( "Round" ), static_cast< int >( Qgis::JoinStyle::Round ) );
676   mJoinStyleComboBox->addItem( tr( "Miter" ), static_cast< int >( Qgis::JoinStyle::Miter ) );
677   mJoinStyleComboBox->addItem( tr( "Bevel" ), static_cast< int >( Qgis::JoinStyle::Bevel ) );
678   mCapStyleComboBox->addItem( tr( "Round" ), static_cast< int >( Qgis::EndCapStyle::Round ) );
679   mCapStyleComboBox->addItem( tr( "Flat" ), static_cast< int >( Qgis::EndCapStyle::Flat ) );
680   mCapStyleComboBox->addItem( tr( "Square" ), static_cast< int >( Qgis::EndCapStyle::Square ) );
681 
682   const Qgis::JoinStyle joinStyle = QgsSettingsRegistryCore::settingsDigitizingOffsetJoinStyle.value();
683   const int quadSegments = QgsSettingsRegistryCore::settingsDigitizingOffsetQuadSeg.value();
684   const double miterLimit = QgsSettingsRegistryCore::settingsDigitizingOffsetMiterLimit.value();
685   const Qgis::EndCapStyle capStyle = QgsSettingsRegistryCore::settingsDigitizingOffsetCapStyle.value();
686 
687   mJoinStyleComboBox->setCurrentIndex( mJoinStyleComboBox->findData( static_cast< int >( joinStyle ) ) );
688   mQuadrantSpinBox->setValue( quadSegments );
689   mQuadrantSpinBox->setClearValue( 8 );
690   mMiterLimitSpinBox->setValue( miterLimit );
691   mMiterLimitSpinBox->setClearValue( 5.0 );
692   mCapStyleComboBox->setCurrentIndex( mCapStyleComboBox->findData( static_cast< int >( capStyle ) ) );
693 
694   // connect signals
695   mOffsetSpinBox->installEventFilter( this );
696   connect( mOffsetSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsOffsetUserWidget::offsetChanged );
697 
698   connect( mJoinStyleComboBox, static_cast < void ( QComboBox::* )( int ) > ( &QComboBox::currentIndexChanged ), this, [ = ] { QgsSettingsRegistryCore::settingsDigitizingOffsetJoinStyle.setValue( static_cast< Qgis::JoinStyle >( mJoinStyleComboBox->currentData().toInt() ) ); emit offsetConfigChanged(); } );
699   connect( mQuadrantSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, [ = ]( const int quadSegments ) { QgsSettingsRegistryCore::settingsDigitizingOffsetQuadSeg.setValue( quadSegments ); emit offsetConfigChanged(); } );
700   connect( mMiterLimitSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, [ = ]( double miterLimit ) { QgsSettingsRegistryCore::settingsDigitizingOffsetMiterLimit.setValue( miterLimit ); emit offsetConfigChanged(); } );
701   connect( mCapStyleComboBox, static_cast < void ( QComboBox::* )( int ) > ( &QComboBox::currentIndexChanged ), this, [ = ] { QgsSettingsRegistryCore::settingsDigitizingOffsetCapStyle.setValue( static_cast< Qgis::EndCapStyle >( mCapStyleComboBox->currentData().toInt() ) ); emit offsetConfigChanged(); } );
702 
703   const bool showAdvanced = QgsSettingsRegistryCore::settingsDigitizingOffsetShowAdvanced.value();
704   mShowAdvancedButton->setChecked( showAdvanced );
705   mAdvancedConfigWidget->setVisible( showAdvanced );
706   connect( mShowAdvancedButton, &QToolButton::clicked, mAdvancedConfigWidget, &QWidget::setVisible );
707   connect( mShowAdvancedButton, &QToolButton::clicked, this, [ = ]( const bool clicked ) {QgsSettingsRegistryCore::settingsDigitizingConvertToCurveDistanceTolerance.setValue( clicked );} );
708 
709   // config focus
710   setFocusProxy( mOffsetSpinBox );
711 }
712 
setOffset(double offset)713 void QgsOffsetUserWidget::setOffset( double offset )
714 {
715   mOffsetSpinBox->setValue( offset );
716 }
717 
offset()718 double QgsOffsetUserWidget::offset()
719 {
720   return mOffsetSpinBox->value();
721 }
722 
setPolygonMode(bool polygon)723 void QgsOffsetUserWidget::setPolygonMode( bool polygon )
724 {
725   mCapStyleLabel->setEnabled( polygon );
726   mCapStyleComboBox->setEnabled( polygon );
727 }
728 
eventFilter(QObject * obj,QEvent * ev)729 bool QgsOffsetUserWidget::eventFilter( QObject *obj, QEvent *ev )
730 {
731   if ( obj == mOffsetSpinBox && ev->type() == QEvent::KeyPress )
732   {
733     QKeyEvent *event = static_cast<QKeyEvent *>( ev );
734     if ( event->key() == Qt::Key_Escape )
735     {
736       emit offsetEditingCanceled();
737       return true;
738     }
739     if ( event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return )
740     {
741       emit offsetEditingFinished( offset(), event->modifiers() );
742       return true;
743     }
744   }
745 
746   return false;
747 }
748