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