1 /***************************************************************************
2     qgsmaptoolscalefeature.cpp  -  map tool for scaling features by mouse drag
3     ---------------------
4     Date                 : December 2020
5     Copyright            : (C) 2020 by roya0045
6     Contact              : ping me on github
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 <QSettings>
17 #include <QEvent>
18 #include <QHBoxLayout>
19 #include <QKeyEvent>
20 #include <QLabel>
21 
22 #include <limits>
23 #include <cmath>
24 
25 #include "qgsadvanceddigitizingdockwidget.h"
26 #include "qgsmaptoolscalefeature.h"
27 #include "qgsfeatureiterator.h"
28 #include "qgsgeometry.h"
29 #include "qgslogger.h"
30 #include "qgsmapcanvas.h"
31 #include "qgsrubberband.h"
32 #include "qgsvectorlayer.h"
33 #include "qgstolerance.h"
34 #include "qgisapp.h"
35 #include "qgsspinbox.h"
36 #include "qgsdoublespinbox.h"
37 #include "qgssnapindicator.h"
38 #include "qgsmapmouseevent.h"
39 
40 
QgsScaleMagnetWidget(const QString & label,QWidget * parent)41 QgsScaleMagnetWidget::QgsScaleMagnetWidget( const QString &label, QWidget *parent )
42   : QWidget( parent )
43 {
44   mLayout = new QHBoxLayout( this );
45   mLayout->setContentsMargins( 0, 0, 0, 0 );
46   //mLayout->setAlignment( Qt::AlignLeft );
47   setLayout( mLayout );
48 
49   if ( !label.isEmpty() )
50   {
51     QLabel *lbl = new QLabel( label, this );
52     lbl->setAlignment( Qt::AlignRight | Qt::AlignCenter );
53     mLayout->addWidget( lbl );
54   }
55 
56   mScaleSpinBox = new QgsDoubleSpinBox( this );
57   mScaleSpinBox->setSingleStep( 0.5 );
58   mScaleSpinBox->setMinimum( 0 );
59   mScaleSpinBox->setValue( 1 );
60   mScaleSpinBox->setShowClearButton( false );
61   mScaleSpinBox->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Preferred );
62   mLayout->addWidget( mScaleSpinBox );
63 
64   // connect signals
65   mScaleSpinBox->installEventFilter( this );
66   connect( mScaleSpinBox, static_cast < void ( QgsDoubleSpinBox::* )( double ) > ( &QgsDoubleSpinBox::valueChanged ), this, &QgsScaleMagnetWidget::scaleSpinBoxValueChanged );
67 
68   // config focus
69   setFocusProxy( mScaleSpinBox );
70 }
71 
setScale(double scale)72 void QgsScaleMagnetWidget::setScale( double scale )
73 {
74   mScaleSpinBox->setValue( scale );
75 }
76 
scale() const77 double QgsScaleMagnetWidget::scale() const
78 {
79   return mScaleSpinBox->value();
80 }
81 
eventFilter(QObject * obj,QEvent * ev)82 bool QgsScaleMagnetWidget::eventFilter( QObject *obj, QEvent *ev )
83 {
84   if ( obj == mScaleSpinBox && ev->type() == QEvent::KeyPress )
85   {
86     QKeyEvent *event = static_cast<QKeyEvent *>( ev );
87     if ( event->key() == Qt::Key_Escape )
88     {
89       emit scaleEditingCanceled();
90       return true;
91     }
92     if ( event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return )
93     {
94       emit scaleEditingFinished( scale() );
95       return true;
96     }
97   }
98 
99   return false;
100 }
101 
scaleSpinBoxValueChanged(double scale)102 void QgsScaleMagnetWidget::scaleSpinBoxValueChanged( double scale )
103 {
104   emit scaleChanged( scale );
105 }
106 
107 //
108 // QgsMapToolScaleFeature
109 //
110 
QgsMapToolScaleFeature(QgsMapCanvas * canvas)111 QgsMapToolScaleFeature::QgsMapToolScaleFeature( QgsMapCanvas *canvas )
112   : QgsMapToolAdvancedDigitizing( canvas, QgisApp::instance()->cadDockWidget() )
113   , mSnapIndicator( std::make_unique< QgsSnapIndicator>( canvas ) )
114 {
115   mToolName = tr( "Scale feature" );
116 }
117 
~QgsMapToolScaleFeature()118 QgsMapToolScaleFeature::~QgsMapToolScaleFeature()
119 {
120   deleteScalingWidget();
121   mAnchorPoint.reset();
122   deleteRubberband();
123   mSnapIndicator->setMatch( QgsPointLocator::Match() );
124 }
125 
cadCanvasMoveEvent(QgsMapMouseEvent * e)126 void QgsMapToolScaleFeature::cadCanvasMoveEvent( QgsMapMouseEvent *e )
127 {
128   mSnapIndicator->setMatch( e->mapPointMatch() );
129   if ( mBaseDistance == 0 )
130   {
131     return;
132   }
133   if ( mScalingActive )
134   {
135     const double distance = mFeatureCenterMapCoords.distance( e->mapPoint() );
136     const double scale = distance / mBaseDistance; // min 0 or no limit?
137 
138     if ( mScalingWidget )
139     {
140       disconnect( mScalingWidget, &QgsScaleMagnetWidget::scaleChanged, this, &QgsMapToolScaleFeature::updateRubberband );
141       mScalingWidget->setScale( scale );
142       mScalingWidget->setFocus( Qt::TabFocusReason );
143       mScalingWidget->editor()->selectAll();
144       connect( mScalingWidget, &QgsScaleMagnetWidget::scaleChanged, this, &QgsMapToolScaleFeature::updateRubberband );
145     }
146     updateRubberband( scale );
147   }
148 }
149 
cadCanvasReleaseEvent(QgsMapMouseEvent * e)150 void QgsMapToolScaleFeature::cadCanvasReleaseEvent( QgsMapMouseEvent *e )
151 {
152   if ( !mCanvas )
153   {
154     return;
155   }
156 
157   QgsVectorLayer *vlayer = currentVectorLayer();
158   if ( !vlayer )
159   {
160     deleteScalingWidget();
161     deleteRubberband();
162     notifyNotVectorLayer();
163     mSnapIndicator->setMatch( QgsPointLocator::Match() );
164     mCadDockWidget->clear();
165     return;
166   }
167 
168   if ( e->button() == Qt::RightButton )
169   {
170     cancel();
171     return;
172   }
173 
174   // place anchor point on CTRL + click
175   if ( e->modifiers() & Qt::ControlModifier )
176   {
177     if ( !mAnchorPoint )
178     {
179       mAnchorPoint = std::make_unique<QgsVertexMarker>( mCanvas );
180       mAnchorPoint->setIconType( QgsVertexMarker::ICON_CROSS );
181     }
182     mAnchorPoint->setCenter( e->mapPoint() );
183     mFeatureCenterMapCoords = e->mapPoint();
184     cadDockWidget()->clear();
185     return;
186   }
187 
188   deleteScalingWidget();
189 
190   // Initialize scaling if not yet active
191   if ( !mScalingActive )
192   {
193     mScaling = 1;
194 
195     deleteRubberband();
196 
197     if ( !vlayer->isEditable() )
198     {
199       notifyNotEditableLayer();
200       return;
201     }
202 
203     const QgsPointXY layerCoords = toLayerCoordinates( vlayer, e->mapPoint() );
204     const double searchRadius = QgsTolerance::vertexSearchRadius( mCanvas->currentLayer(), mCanvas->mapSettings() );
205     const QgsRectangle selectRect( layerCoords.x() - searchRadius, layerCoords.y() - searchRadius,
206                                    layerCoords.x() + searchRadius, layerCoords.y() + searchRadius );
207 
208     mAutoSetAnchorPoint = false;
209     if ( !mAnchorPoint )
210     {
211       mAnchorPoint = std::make_unique<QgsVertexMarker>( mCanvas );
212       mAnchorPoint->setIconType( QgsVertexMarker::ICON_CROSS );
213       mAutoSetAnchorPoint = true;
214     }
215 
216     if ( vlayer->selectedFeatureCount() == 0 )
217     {
218       QgsFeatureIterator fit = vlayer->getFeatures( QgsFeatureRequest().setNoAttributes().setFilterRect( selectRect ) );
219 
220       //find the closest feature
221       const QgsGeometry pointGeometry = QgsGeometry::fromPointXY( layerCoords );
222       if ( pointGeometry.isNull() )
223       {
224         return;
225       }
226 
227       double minDistance = std::numeric_limits<double>::max();
228 
229       QgsFeature cf;
230       QgsFeature f;
231       while ( fit.nextFeature( f ) )
232       {
233         if ( f.hasGeometry() )
234         {
235           const double currentDistance = pointGeometry.distance( f.geometry() );
236           if ( currentDistance < minDistance )
237           {
238             minDistance = currentDistance;
239             cf = f;
240           }
241         }
242       }
243 
244       if ( minDistance == std::numeric_limits<double>::max() )
245       {
246         emit messageEmitted( tr( "Could not find a nearby feature in the current layer." ) );
247         if ( mAutoSetAnchorPoint )
248           mAnchorPoint.reset();
249         return;
250       }
251 
252       mExtent = cf.geometry().boundingBox();
253       if ( mAutoSetAnchorPoint )
254       {
255         mFeatureCenterMapCoords = toMapCoordinates( vlayer, mExtent.center() );
256         mAnchorPoint->setCenter( mFeatureCenterMapCoords );
257       }
258       else
259       {
260         mFeatureCenterMapCoords =  mAnchorPoint->center();
261       }
262 
263       mScaledFeatures.clear();
264       mScaledFeatures << cf.id(); //todo: take the closest feature, not the first one...
265       mOriginalGeometries << cf.geometry();
266 
267       mRubberBand = createRubberBand( vlayer->geometryType() );
268       mRubberBand->setToGeometry( cf.geometry(), vlayer );
269     }
270     else
271     {
272       mScaledFeatures = vlayer->selectedFeatureIds();
273 
274       mRubberBand = createRubberBand( vlayer->geometryType() );
275 
276       QgsFeature feat;
277       QgsFeatureIterator it = vlayer->getSelectedFeatures();
278       while ( it.nextFeature( feat ) )
279       {
280         mRubberBand->addGeometry( feat.geometry(), vlayer, false );
281         mOriginalGeometries << feat.geometry();
282       }
283       mRubberBand->updatePosition();
284       mRubberBand->update();
285     }
286 
287     mScalingActive = true;
288 
289     mBaseDistance = e->mapPoint().distance( mFeatureCenterMapCoords );
290     mScaling = 1.0;
291 
292     createScalingWidget();
293 
294     mScalingActive = true;
295 
296     return;
297   }
298 
299   applyScaling( mScaling );
300 }
301 
cancel()302 void QgsMapToolScaleFeature::cancel()
303 {
304   deleteScalingWidget();
305   deleteRubberband();
306   QgsVectorLayer *vlayer = currentVectorLayer();
307   if ( vlayer->selectedFeatureCount() == 0 || mAutoSetAnchorPoint )
308   {
309     mAnchorPoint.reset();
310   }
311   mScalingActive = false;
312   mSnapIndicator->setMatch( QgsPointLocator::Match() );
313   mCadDockWidget->clear();
314 }
315 
updateRubberband(double scale)316 void QgsMapToolScaleFeature::updateRubberband( double scale )
317 {
318   if ( mScalingActive && mRubberBand )
319   {
320     mScaling = scale;
321 
322     QgsVectorLayer *vlayer = currentVectorLayer();
323     if ( !vlayer )
324       return;
325 
326     const QgsPointXY layerCoords = toLayerCoordinates( vlayer, mFeatureCenterMapCoords );
327     QTransform t;
328     t.translate( layerCoords.x(), layerCoords.y() );
329     t.scale( mScaling, mScaling );
330     t.translate( -layerCoords.x(), -layerCoords.y() );
331 
332     mRubberBand->reset( vlayer->geometryType() );
333     for ( const QgsGeometry &originalGeometry : mOriginalGeometries )
334     {
335       QgsGeometry geom = originalGeometry;
336       geom.transform( t );
337       mRubberBand->addGeometry( geom, vlayer );
338     }
339   }
340 }
341 
applyScaling(double scale)342 void QgsMapToolScaleFeature::applyScaling( double scale )
343 {
344   mScaling = scale;
345   mScalingActive = false;
346 
347   QgsVectorLayer *vlayer = currentVectorLayer();
348   if ( !vlayer )
349   {
350     deleteRubberband();
351     notifyNotVectorLayer();
352     mSnapIndicator->setMatch( QgsPointLocator::Match() );
353     mCadDockWidget->clear();
354     return;
355   }
356 
357   //calculations for affine transformation
358 
359   vlayer->beginEditCommand( tr( "Features Scaled" ) );
360 
361   const QgsPointXY layerCoords = toLayerCoordinates( vlayer, mFeatureCenterMapCoords );
362   QTransform t;
363   t.translate( layerCoords.x(), layerCoords.y() );
364   t.scale( mScaling, mScaling );
365   t.translate( -layerCoords.x(), -layerCoords.y() );
366 
367   QgsFeatureRequest request;
368   request.setFilterFids( mScaledFeatures ).setNoAttributes();
369   QgsFeatureIterator fi = vlayer->getFeatures( request );
370   QgsFeature feat;
371   while ( fi.nextFeature( feat ) )
372   {
373     if ( !feat.hasGeometry() )
374       continue;
375 
376     QgsGeometry geom = feat.geometry();
377     if ( !( geom.transform( t ) == Qgis::GeometryOperationResult::Success ) )
378       continue;
379 
380     const QgsFeatureId id = feat.id();
381     vlayer->changeGeometry( id, geom );
382   }
383 
384   deleteScalingWidget();
385   deleteRubberband();
386   mSnapIndicator->setMatch( QgsPointLocator::Match() );
387   mCadDockWidget->clear();
388 
389   if ( mAutoSetAnchorPoint )
390     mAnchorPoint.reset();
391 
392   vlayer->endEditCommand();
393   vlayer->triggerRepaint();
394 }
395 
keyReleaseEvent(QKeyEvent * e)396 void QgsMapToolScaleFeature::keyReleaseEvent( QKeyEvent *e )
397 {
398   if ( mScalingActive && e->key() == Qt::Key_Escape )
399   {
400     cancel();
401     return;
402   }
403   QgsMapToolAdvancedDigitizing::keyReleaseEvent( e );
404 }
405 
activate()406 void QgsMapToolScaleFeature::activate()
407 {
408   QgsVectorLayer *vlayer = currentVectorLayer();
409   if ( !vlayer )
410   {
411     return;
412   }
413 
414   if ( !vlayer->isEditable() )
415   {
416     return;
417   }
418 
419   if ( vlayer->selectedFeatureCount() > 0 )
420   {
421     mExtent = vlayer->boundingBoxOfSelected();
422     mFeatureCenterMapCoords = toMapCoordinates( vlayer, mExtent.center() );
423 
424     mAnchorPoint = std::make_unique<QgsVertexMarker>( mCanvas );
425     mAnchorPoint->setIconType( QgsVertexMarker::ICON_CROSS );
426     mAnchorPoint->setCenter( mFeatureCenterMapCoords );
427   }
428   QgsMapToolAdvancedDigitizing::activate();
429 }
430 
deleteRubberband()431 void QgsMapToolScaleFeature::deleteRubberband()
432 {
433   delete mRubberBand;
434   mRubberBand = nullptr;
435 
436   mOriginalGeometries.clear();
437 }
438 
deactivate()439 void QgsMapToolScaleFeature::deactivate()
440 {
441   deleteScalingWidget();
442   mScalingActive = false;
443   mAnchorPoint.reset();
444   deleteRubberband();
445   mSnapIndicator->setMatch( QgsPointLocator::Match() );
446   QgsMapToolAdvancedDigitizing::deactivate();
447 }
448 
createScalingWidget()449 void QgsMapToolScaleFeature::createScalingWidget()
450 {
451   if ( !mCanvas )
452   {
453     return;
454   }
455 
456   deleteScalingWidget();
457 
458   mScalingWidget = new QgsScaleMagnetWidget( QStringLiteral( "Scaling:" ) );
459   QgisApp::instance()->addUserInputWidget( mScalingWidget );
460   mScalingWidget->setFocus( Qt::TabFocusReason );
461 
462   connect( mScalingWidget, &QgsScaleMagnetWidget::scaleChanged, this, &QgsMapToolScaleFeature::updateRubberband );
463   connect( mScalingWidget, &QgsScaleMagnetWidget::scaleEditingFinished, this, &QgsMapToolScaleFeature::applyScaling );
464   connect( mScalingWidget, &QgsScaleMagnetWidget::scaleEditingCanceled, this, &QgsMapToolScaleFeature::cancel );
465 }
466 
deleteScalingWidget()467 void QgsMapToolScaleFeature::deleteScalingWidget()
468 {
469   if ( mScalingWidget )
470   {
471     disconnect( mScalingWidget, &QgsScaleMagnetWidget::scaleChanged, this, &QgsMapToolScaleFeature::updateRubberband );
472     disconnect( mScalingWidget, &QgsScaleMagnetWidget::scaleEditingFinished, this, &QgsMapToolScaleFeature::applyScaling );
473     disconnect( mScalingWidget, &QgsScaleMagnetWidget::scaleEditingCanceled, this, &QgsMapToolScaleFeature::cancel );
474 
475     mScalingWidget->releaseKeyboard();
476     mScalingWidget->deleteLater();
477   }
478   mScalingWidget = nullptr;
479 }
480 
481