QGIS API Documentation 3.41.0-Master (3440c17df1d)
Loading...
Searching...
No Matches
qgsmaptoolmodifyannotation.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsmaptoolmodifyannotation.cpp
3 ----------------
4 copyright : (C) 2021 by Nyall Dawson
5 email : nyall dot dawson at gmail dot com
6 ***************************************************************************/
7
8/***************************************************************************
9 * *
10 * This program is free software; you can redistribute it and/or modify *
11 * it under the terms of the GNU General Public License as published by *
12 * the Free Software Foundation; either version 2 of the License, or *
13 * (at your option) any later version. *
14 * *
15 ***************************************************************************/
16
18#include "moc_qgsmaptoolmodifyannotation.cpp"
19#include "qgsrubberband.h"
20#include "qgsmapmouseevent.h"
21#include "qgsmapcanvas.h"
24#include "qgsannotationlayer.h"
25#include "qgsproject.h"
27#include "qgsannotationitem.h"
30#include "qgssnapindicator.h"
31#include "RTree.h"
32#include <QTransform>
33#include <QWindow>
34#include <QScreen>
35
37class QgsAnnotationItemNodesSpatialIndex : public RTree<int, float, 2, float>
38{
39 public:
40
41 void insert( int index, const QgsRectangle &bounds )
42 {
43 std::array< float, 4 > scaledBounds = scaleBounds( bounds );
44 this->Insert(
45 {
46 scaledBounds[0], scaledBounds[ 1]
47 },
48 {
49 scaledBounds[2], scaledBounds[3]
50 },
51 index );
52 }
53
60 void remove( int index, const QgsRectangle &bounds )
61 {
62 std::array< float, 4 > scaledBounds = scaleBounds( bounds );
63 this->Remove(
64 {
65 scaledBounds[0], scaledBounds[ 1]
66 },
67 {
68 scaledBounds[2], scaledBounds[3]
69 },
70 index );
71 }
72
78 bool intersects( const QgsRectangle &bounds, const std::function< bool( int index )> &callback ) const
79 {
80 std::array< float, 4 > scaledBounds = scaleBounds( bounds );
81 this->Search(
82 {
83 scaledBounds[0], scaledBounds[ 1]
84 },
85 {
86 scaledBounds[2], scaledBounds[3]
87 },
88 callback );
89 return true;
90 }
91
92 private:
93 std::array<float, 4> scaleBounds( const QgsRectangle &bounds ) const
94 {
95 return
96 {
97 static_cast< float >( bounds.xMinimum() ),
98 static_cast< float >( bounds.yMinimum() ),
99 static_cast< float >( bounds.xMaximum() ),
100 static_cast< float >( bounds.yMaximum() )
101 };
102 }
103};
105
106
108 : QgsMapToolAdvancedDigitizing( canvas, cadDockWidget )
109 , mSnapIndicator( new QgsSnapIndicator( canvas ) )
110{
111
112 connect( QgsMapToolModifyAnnotation::canvas(), &QgsMapCanvas::mapCanvasRefreshed, this, &QgsMapToolModifyAnnotation::onCanvasRefreshed );
113}
114
116
118{
119 mSnapIndicator->setMatch( QgsPointLocator::Match() );
120
121 clearHoveredItem();
122 clearSelectedItem();
124}
125
127{
128 mLastHoverPoint = event->originalPixelPoint();
129 event->snapPoint();
130 mSnapIndicator->setMatch( event->mapPointMatch() );
131
132 const QgsPointXY mapPoint = event->mapPoint();
133
135 QgsAnnotationLayer *layer = annotationLayerFromId( mSelectedItemLayerId );
136 context.setCurrentItemBounds( toLayerCoordinates( layer, mSelectedItemBounds ) );
137 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
138
139 switch ( mCurrentAction )
140 {
141 case Action::NoAction:
142 {
143 setHoveredItemFromPoint( mapPoint );
144 break;
145 }
146
147 case Action::MoveItem:
148 {
149 if ( QgsAnnotationItem *item = annotationItemFromId( mSelectedItemLayerId, mSelectedItemId ) )
150 {
151 const QgsVector delta = toLayerCoordinates( layer, event->mapPoint() ) - mMoveStartPointLayerCrs;
152
153 QgsAnnotationItemEditOperationTranslateItem operation( mSelectedItemId, delta.x(), delta.y(),
154 event->pixelPoint().x() - mMoveStartPointPixels.x(),
155 event->pixelPoint().y() - mMoveStartPointPixels.y() );
156 std::unique_ptr< QgsAnnotationItemEditOperationTransientResults > operationResults( item->transientEditResultsV2( &operation, context ) );
157 if ( operationResults )
158 {
159 mTemporaryRubberBand.reset( new QgsRubberBand( mCanvas, operationResults->representativeGeometry().type() ) );
160 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
161 mTemporaryRubberBand->setWidth( scaleFactor );
162 mTemporaryRubberBand->setToGeometry( operationResults->representativeGeometry(), layer->crs() );
163 }
164 else
165 {
166 mTemporaryRubberBand.reset();
167 }
168 }
169 break;
170 }
171
172 case Action::MoveNode:
173 {
174 if ( QgsAnnotationItem *item = annotationItemFromId( mSelectedItemLayerId, mSelectedItemId ) )
175 {
176 const QgsPointXY endPointLayer = toLayerCoordinates( layer, event->mapPoint() );
177 QgsAnnotationItemEditOperationMoveNode operation( mSelectedItemId, mTargetNode.id(), QgsPoint( mTargetNode.point() ), QgsPoint( endPointLayer ),
178 event->pixelPoint().x() - mMoveStartPointPixels.x(),
179 event->pixelPoint().y() - mMoveStartPointPixels.y() );
180 std::unique_ptr< QgsAnnotationItemEditOperationTransientResults > operationResults( item->transientEditResultsV2( &operation, context ) );
181 if ( operationResults )
182 {
183 mTemporaryRubberBand.reset( new QgsRubberBand( mCanvas, operationResults->representativeGeometry().type() ) );
184 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
185 mTemporaryRubberBand->setWidth( scaleFactor );
186 mTemporaryRubberBand->setToGeometry( operationResults->representativeGeometry(), layer->crs() );
187 }
188 else
189 {
190 mTemporaryRubberBand.reset();
191 }
192 }
193 break;
194 }
195 }
196}
197
199{
200 QgsAnnotationLayer *layer = annotationLayerFromId( mSelectedItemLayerId );
201
203 context.setCurrentItemBounds( toLayerCoordinates( layer, mSelectedItemBounds ) );
204 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
205
206 switch ( mCurrentAction )
207 {
208 case Action::NoAction:
209 {
210 if ( event->button() != Qt::LeftButton )
211 return;
212
213 if ( mHoveredItemId.isEmpty() || !mHoverRubberBand )
214 {
215 clearSelectedItem();
216 }
217 else if ( mHoveredItemId == mSelectedItemId && mHoveredItemLayerId == mSelectedItemLayerId )
218 {
219 // press is on selected item => move that item
220 if ( layer )
221 {
222 const QgsPointXY mapPoint = event->mapPoint();
223 QgsRectangle searchRect = QgsRectangle( mapPoint.x(), mapPoint.y(), mapPoint.x(), mapPoint.y() );
224 searchRect.grow( searchRadiusMU( canvas() ) );
225
226 QgsAnnotationItemNode hoveredNode;
227 double currentNodeDistance = std::numeric_limits< double >::max();
228 mHoveredItemNodesSpatialIndex->intersects( searchRect, [&hoveredNode, &currentNodeDistance, &mapPoint, this]( int index )-> bool
229 {
230 const QgsAnnotationItemNode &thisNode = mHoveredItemNodes.at( index );
231 const double nodeDistance = thisNode.point().sqrDist( mapPoint );
232 if ( nodeDistance < currentNodeDistance )
233 {
234 hoveredNode = thisNode;
235 currentNodeDistance = nodeDistance;
236 }
237 return true;
238 } );
239
240 mMoveStartPointCanvasCrs = mapPoint;
241 mMoveStartPointPixels = event->pixelPoint();
242 mMoveStartPointLayerCrs = toLayerCoordinates( layer, mMoveStartPointCanvasCrs );
243 if ( mHoverRubberBand )
244 mHoverRubberBand->hide();
245 if ( mSelectedRubberBand )
246 mSelectedRubberBand->hide();
247
248 if ( hoveredNode.point().isEmpty() )
249 {
250 mCurrentAction = Action::MoveItem;
251 }
252 else
253 {
254 mCurrentAction = Action::MoveNode;
255 mTargetNode = hoveredNode;
256 }
257 }
258 }
259 else
260 {
261 // press is on a different item to selected item => select that item
262 mSelectedItemId = mHoveredItemId;
263 mSelectedItemLayerId = mHoveredItemLayerId;
264 mSelectedItemBounds = mHoveredItemBounds;
265
266 if ( !mSelectedRubberBand )
267 createSelectedItemBand();
268
269 mSelectedRubberBand->copyPointsFrom( mHoverRubberBand );
270 mSelectedRubberBand->show();
271
272 setCursor( Qt::OpenHandCursor );
273
274 emit itemSelected( annotationLayerFromId( mSelectedItemLayerId ), mSelectedItemId );
275 }
276 break;
277 }
278
279 case Action::MoveItem:
280 {
281 if ( event->button() == Qt::RightButton )
282 {
283 mCurrentAction = Action::NoAction;
284 mTemporaryRubberBand.reset();
285 if ( mSelectedRubberBand )
286 {
287 mSelectedRubberBand->setTranslationOffset( 0, 0 );
288 mSelectedRubberBand->show();
289 }
290 mHoveredItemNodeRubberBands.clear();
291 setCursor( Qt::ArrowCursor );
292 }
293 else if ( event->button() == Qt::LeftButton )
294 {
295 // apply move
296 if ( layer )
297 {
298 const QgsVector delta = toLayerCoordinates( layer, event->mapPoint() ) - mMoveStartPointLayerCrs;
299
300 QgsAnnotationItemEditOperationTranslateItem operation( mSelectedItemId, delta.x(), delta.y(),
301 event->pixelPoint().x() - mMoveStartPointPixels.x(),
302 event->pixelPoint().y() - mMoveStartPointPixels.y() );
303 switch ( layer->applyEditV2( &operation, context ) )
304 {
307 mRefreshSelectedItemAfterRedraw = true;
308 break;
311 break;
312 }
313 }
314
315 mTemporaryRubberBand.reset();
316 mCurrentAction = Action::NoAction;
317 setCursor( Qt::ArrowCursor );
318 }
319 break;
320 }
321
322 case Action::MoveNode:
323 {
324 if ( event->button() == Qt::RightButton )
325 {
326 mCurrentAction = Action::NoAction;
327 mTemporaryRubberBand.reset();
328 mHoveredItemNodeRubberBands.clear();
329 mTemporaryRubberBand.reset();
330 setCursor( Qt::ArrowCursor );
331 }
332 else if ( event->button() == Qt::LeftButton )
333 {
334 if ( layer )
335 {
336 const QgsPointXY endPointLayer = toLayerCoordinates( layer, event->mapPoint() );
337 QgsAnnotationItemEditOperationMoveNode operation( mSelectedItemId, mTargetNode.id(), QgsPoint( mTargetNode.point() ), QgsPoint( endPointLayer ),
338 event->pixelPoint().x() - mMoveStartPointPixels.x(),
339 event->pixelPoint().y() - mMoveStartPointPixels.y() );
340 switch ( layer->applyEditV2( &operation, context ) )
341 {
344 mRefreshSelectedItemAfterRedraw = true;
345 break;
346
349 break;
350 }
351 }
352
353 mTemporaryRubberBand.reset();
354 mHoveredItemNodeRubberBands.clear();
355 mHoveredItemNodes.clear();
356 mTemporaryRubberBand.reset();
357 mCurrentAction = Action::NoAction;
358 setCursor( Qt::ArrowCursor );
359 }
360 break;
361 }
362 }
363}
364
366{
367 switch ( mCurrentAction )
368 {
369 case Action::NoAction:
370 case Action::MoveItem:
371 {
372 if ( event->button() != Qt::LeftButton )
373 return;
374
375 mCurrentAction = Action::NoAction;
376 if ( mHoveredItemId == mSelectedItemId && mHoveredItemLayerId == mSelectedItemLayerId )
377 {
378 // double-click on selected item => add node
379 if ( QgsAnnotationLayer *layer = annotationLayerFromId( mSelectedItemLayerId ) )
380 {
381 const QgsPointXY layerPoint = toLayerCoordinates( layer, event->mapPoint() );
382 QgsAnnotationItemEditOperationAddNode operation( mSelectedItemId, QgsPoint( layerPoint ) );
384 context.setCurrentItemBounds( toLayerCoordinates( layer, mSelectedItemBounds ) );
385 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
386
387 switch ( layer->applyEditV2( &operation, context ) )
388 {
391 mRefreshSelectedItemAfterRedraw = true;
392 break;
393
396 break;
397 }
398 }
399 }
400 else
401 {
402 // press is on a different item to selected item => select that item
403 mSelectedItemId = mHoveredItemId;
404 mSelectedItemLayerId = mHoveredItemLayerId;
405 mSelectedItemBounds = mHoveredItemBounds;
406
407 if ( !mSelectedRubberBand )
408 createSelectedItemBand();
409
410 mSelectedRubberBand->copyPointsFrom( mHoverRubberBand );
411 mSelectedRubberBand->show();
412
413 setCursor( Qt::OpenHandCursor );
414
415 emit itemSelected( annotationLayerFromId( mSelectedItemLayerId ), mSelectedItemId );
416 }
417 break;
418 }
419
420 case Action::MoveNode:
421 break;
422 }
423}
424
426{
428 QgsAnnotationLayer *layer = annotationLayerFromId( mSelectedItemLayerId );
429 context.setCurrentItemBounds( toLayerCoordinates( layer, mSelectedItemBounds ) );
430 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
431
432 switch ( mCurrentAction )
433 {
434 case Action::NoAction:
435 {
436 if ( event->key() == Qt::Key_Backspace || event->key() == Qt::Key_Delete )
437 {
438
439 if ( !layer || mSelectedItemId.isEmpty() )
440 return;
441
442 layer->removeItem( mSelectedItemId );
443 clearSelectedItem();
444 clearHoveredItem();
445 event->ignore(); // disable default shortcut handling
446 }
447 else if ( event->key() == Qt::Key_Left
448 || event->key() == Qt::Key_Right
449 || event->key() == Qt::Key_Up
450 || event->key() == Qt::Key_Down )
451 {
452 if ( !layer )
453 return;
454
455 const QSizeF deltaLayerCoordinates = deltaForKeyEvent( layer, mSelectedRubberBand->asGeometry().centroid().asPoint(), event );
456
457 QgsAnnotationItemEditOperationTranslateItem operation( mSelectedItemId, deltaLayerCoordinates.width(), deltaLayerCoordinates.height() );
458 switch ( layer->applyEditV2( &operation, context ) )
459 {
462 mRefreshSelectedItemAfterRedraw = true;
463 break;
466 break;
467 }
468 event->ignore(); // disable default shortcut handling (move map)
469 }
470 break;
471 }
472
473 case Action::MoveNode:
474 {
475 if ( event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace )
476 {
477 if ( layer )
478 {
479 QgsAnnotationItemEditOperationDeleteNode operation( mSelectedItemId, mTargetNode.id(), QgsPoint( mTargetNode.point() ) );
480 switch ( layer->applyEditV2( &operation, context ) )
481 {
484 mRefreshSelectedItemAfterRedraw = true;
485 break;
487 break;
490 break;
491 }
492 }
493
494 mTemporaryRubberBand.reset();
495 mHoveredItemNodeRubberBands.clear();
496 mHoveredItemNodes.clear();
497 mTemporaryRubberBand.reset();
498 mCurrentAction = Action::NoAction;
499 setCursor( Qt::ArrowCursor );
500 event->ignore(); // disable default shortcut handling (delete vector feature)
501 break;
502 }
503 [[fallthrough]];
504 }
505
506 case Action::MoveItem:
507 {
508 // warning -- fallthrough above!
509 if ( event->key() == Qt::Key_Escape )
510 {
511 mCurrentAction = Action::NoAction;
512 mTemporaryRubberBand.reset();
513 if ( mSelectedRubberBand )
514 {
515 mSelectedRubberBand->setTranslationOffset( 0, 0 );
516 mSelectedRubberBand->show();
517 }
518 mHoveredItemNodeRubberBands.clear();
519
520 setCursor( Qt::ArrowCursor );
521 }
522 break;
523 }
524 }
525}
526
527void QgsMapToolModifyAnnotation::onCanvasRefreshed()
528{
529 bool needsSelectedItemRefresh = mRefreshSelectedItemAfterRedraw;
530 if ( QgsAnnotationItem *item = annotationItemFromId( mSelectedItemLayerId, mSelectedItemId ) )
531 {
533 {
534 needsSelectedItemRefresh = true;
535 }
536 }
537
538 if ( needsSelectedItemRefresh )
539 {
540 const QgsRenderedItemResults *renderedItemResults = canvas()->renderedItemResults( false );
541 if ( !renderedItemResults )
542 {
543 return;
544 }
545
546 const QList<QgsRenderedItemDetails *> items = renderedItemResults->renderedItems();
547 auto it = std::find_if( items.begin(), items.end(), [this]( const QgsRenderedItemDetails * item )
548 {
549 if ( const QgsRenderedAnnotationItemDetails *annotationItem = dynamic_cast< const QgsRenderedAnnotationItemDetails *>( item ) )
550 {
551 if ( annotationItem->itemId() == mSelectedItemId && annotationItem->layerId() == mSelectedItemLayerId )
552 return true;
553 }
554 return false;
555 } );
556 if ( it != items.end() )
557 {
558 const QgsRectangle itemBounds = ( *it )->boundingBox();
559
560 setHoveredItem( dynamic_cast< const QgsRenderedAnnotationItemDetails *>( *it ), itemBounds );
561 if ( !mSelectedRubberBand )
562 createSelectedItemBand();
563
564 mSelectedRubberBand->copyPointsFrom( mHoverRubberBand );
565 mSelectedRubberBand->show();
566 mSelectedItemBounds = mHoveredItemBounds;
567 }
568 }
569 else
570 {
571 // recheck for hovered item at new mouse point
572 const QgsPointXY mapPoint = canvas()->mapSettings().mapToPixel().toMapCoordinates( mLastHoverPoint );
573 setHoveredItemFromPoint( mapPoint );
574 }
575 mRefreshSelectedItemAfterRedraw = false;
576}
577
578void QgsMapToolModifyAnnotation::setHoveredItem( const QgsRenderedAnnotationItemDetails *item, const QgsRectangle &itemMapBounds )
579{
580 mHoveredItemNodeRubberBands.clear();
581 if ( mHoveredNodeRubberBand )
582 mHoveredNodeRubberBand->hide();
583 mHoveredItemId = item->itemId();
584 mHoveredItemLayerId = item->layerId();
585 mHoveredItemBounds = itemMapBounds;
586 if ( !mHoverRubberBand )
587 createHoverBand();
588
589 mHoverRubberBand->show();
590
591 mHoverRubberBand->reset( Qgis::GeometryType::Line );
592 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMinimum(), itemMapBounds.yMinimum() ) );
593 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMaximum(), itemMapBounds.yMinimum() ) );
594 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMaximum(), itemMapBounds.yMaximum() ) );
595 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMinimum(), itemMapBounds.yMaximum() ) );
596 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMinimum(), itemMapBounds.yMinimum() ) );
597
598 QgsAnnotationLayer *layer = annotationLayerFromId( item->layerId() );
599 const QgsAnnotationItem *annotationItem = annotationItemFromId( item->layerId(), item->itemId() );
600 if ( !annotationItem )
601 return;
602
603 QgsCoordinateTransform layerToMapTransform = QgsCoordinateTransform( layer->crs(), canvas()->mapSettings().destinationCrs(), canvas()->mapSettings().transformContext() );
604
605 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
606
608 context.setCurrentItemBounds( toLayerCoordinates( layer, itemMapBounds ) );
609 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
610
611 const QList< QgsAnnotationItemNode > itemNodes = annotationItem->nodesV2( context );
613
614 vertexNodeBand->setIcon( QgsRubberBand::ICON_BOX );
615 vertexNodeBand->setWidth( scaleFactor );
616 vertexNodeBand->setIconSize( scaleFactor * 5 );
617 vertexNodeBand->setColor( QColor( 200, 0, 120, 255 ) );
618
620 calloutNodeBand->setWidth( scaleFactor );
621 calloutNodeBand->setSecondaryStrokeColor( QColor( 255, 255, 255, 100 ) );
622 calloutNodeBand->setColor( QColor( 120, 200, 0, 255 ) );
623 calloutNodeBand->setIcon( QgsRubberBand::ICON_X );
624 calloutNodeBand->setIconSize( scaleFactor * 5 );
625
626 // store item nodes in a spatial index for quick searching
627 mHoveredItemNodesSpatialIndex = std::make_unique< QgsAnnotationItemNodesSpatialIndex >();
628 int index = 0;
629 mHoveredItemNodes.clear();
630 mHoveredItemNodes.reserve( itemNodes.size() );
631 for ( const QgsAnnotationItemNode &node : itemNodes )
632 {
633 QgsPointXY nodeMapPoint;
634 try
635 {
636 nodeMapPoint = layerToMapTransform.transform( node.point() );
637 }
638 catch ( QgsCsException & )
639 {
640 continue;
641 }
642
643 switch ( node.type() )
644 {
646 vertexNodeBand->addPoint( nodeMapPoint );
647 break;
648
650 calloutNodeBand->addPoint( nodeMapPoint );
651 break;
652 }
653
654 mHoveredItemNodesSpatialIndex->insert( index, QgsRectangle( nodeMapPoint.x(), nodeMapPoint.y(),
655 nodeMapPoint.x(), nodeMapPoint.y() ) );
656
657 QgsAnnotationItemNode transformedNode = node;
658 transformedNode.setPoint( nodeMapPoint );
659 mHoveredItemNodes.append( transformedNode );
660
661 index++;
662 }
663
664 mHoveredItemNodeRubberBands.emplace_back( vertexNodeBand );
665 mHoveredItemNodeRubberBands.emplace_back( calloutNodeBand );
666}
667
668QSizeF QgsMapToolModifyAnnotation::deltaForKeyEvent( QgsAnnotationLayer *layer, const QgsPointXY &originalCanvasPoint, QKeyEvent *event )
669{
670 const double canvasDpi = canvas()->window()->windowHandle()->screen()->physicalDotsPerInch();
671
672 // increment used for cursor key item movement
673 double incrementPixels = 0.0;
674 if ( event->modifiers() & Qt::ShiftModifier )
675 {
676 //holding shift while pressing cursor keys results in a big step - 20 mm
677 incrementPixels = 20.0 / 25.4 * canvasDpi;
678 }
679 else if ( event->modifiers() & Qt::AltModifier )
680 {
681 //holding alt while pressing cursor keys results in a 1 pixel step
682 incrementPixels = 1;
683 }
684 else
685 {
686 // 5 mm
687 incrementPixels = 5.0 / 25.4 * canvasDpi;
688 }
689
690 double deltaXPixels = 0;
691 double deltaYPixels = 0;
692 switch ( event->key() )
693 {
694 case Qt::Key_Left:
695 deltaXPixels = -incrementPixels;
696 break;
697 case Qt::Key_Right:
698 deltaXPixels = incrementPixels;
699 break;
700 case Qt::Key_Up:
701 deltaYPixels = -incrementPixels;
702 break;
703 case Qt::Key_Down:
704 deltaYPixels = incrementPixels;
705 break;
706 default:
707 break;
708 }
709
710 const QgsPointXY beforeMoveMapPoint = canvas()->getCoordinateTransform()->toMapCoordinates( originalCanvasPoint.x(), originalCanvasPoint.y() );
711 const QgsPointXY beforeMoveLayerPoint = toLayerCoordinates( layer, beforeMoveMapPoint );
712
713 const QgsPointXY afterMoveCanvasPoint( originalCanvasPoint.x() + deltaXPixels, originalCanvasPoint.y() + deltaYPixels );
714 const QgsPointXY afterMoveMapPoint = canvas()->getCoordinateTransform()->toMapCoordinates( afterMoveCanvasPoint.x(), afterMoveCanvasPoint.y() );
715 const QgsPointXY afterMoveLayerPoint = toLayerCoordinates( layer, afterMoveMapPoint );
716
717 return QSizeF( afterMoveLayerPoint.x() - beforeMoveLayerPoint.x(), afterMoveLayerPoint.y() - beforeMoveLayerPoint.y() );
718}
719
720const QgsRenderedAnnotationItemDetails *QgsMapToolModifyAnnotation::findClosestItemToPoint( const QgsPointXY &mapPoint, const QList<const QgsRenderedAnnotationItemDetails *> &items, QgsRectangle &bounds )
721{
722 const QgsRenderedAnnotationItemDetails *closestItem = nullptr;
723 double closestItemDistance = std::numeric_limits< double >::max();
724 double closestItemArea = std::numeric_limits< double >::max();
725
726 for ( const QgsRenderedAnnotationItemDetails *item : items )
727 {
728 const QgsAnnotationItem *annotationItem = annotationItemFromId( item->layerId(), item->itemId() );
729 if ( !annotationItem )
730 continue;
731
732 const QgsRectangle itemBounds = item->boundingBox();
733 const double itemDistance = itemBounds.contains( mapPoint ) ? 0 : itemBounds.distance( mapPoint );
734 if ( !closestItem || itemDistance < closestItemDistance || ( itemDistance == closestItemDistance && itemBounds.area() < closestItemArea ) )
735 {
736 closestItem = item;
737 closestItemDistance = itemDistance;
738 closestItemArea = itemBounds.area();
739 bounds = itemBounds;
740 }
741 }
742 return closestItem;
743}
744
745QgsAnnotationLayer *QgsMapToolModifyAnnotation::annotationLayerFromId( const QString &layerId )
746{
747 QgsAnnotationLayer *layer = qobject_cast< QgsAnnotationLayer * >( QgsProject::instance()->mapLayer( layerId ) );
748 if ( !layer && layerId == QgsProject::instance()->mainAnnotationLayer()->id() )
750 return layer;
751}
752
753QgsAnnotationItem *QgsMapToolModifyAnnotation::annotationItemFromId( const QString &layerId, const QString &itemId )
754{
755 QgsAnnotationLayer *layer = annotationLayerFromId( layerId );
756 return layer ? layer->item( itemId ) : nullptr;
757}
758
759void QgsMapToolModifyAnnotation::setHoveredItemFromPoint( const QgsPointXY &mapPoint )
760{
761 QgsRectangle searchRect = QgsRectangle( mapPoint.x(), mapPoint.y(), mapPoint.x(), mapPoint.y() );
762 searchRect.grow( searchRadiusMU( canvas() ) );
763
764 const QgsRenderedItemResults *renderedItemResults = canvas()->renderedItemResults( false );
765 if ( !renderedItemResults )
766 {
767 clearHoveredItem();
768 return;
769 }
770
771 const QList<const QgsRenderedAnnotationItemDetails *> items = renderedItemResults->renderedAnnotationItemsInBounds( searchRect );
772 if ( items.empty() )
773 {
774 clearHoveredItem();
775 return;
776 }
777
778 // find closest item
779 QgsRectangle itemBounds;
780 const QgsRenderedAnnotationItemDetails *closestItem = findClosestItemToPoint( mapPoint, items, itemBounds );
781 if ( !closestItem )
782 {
783 clearHoveredItem();
784 return;
785 }
786
787 if ( closestItem->itemId() != mHoveredItemId || closestItem->layerId() != mHoveredItemLayerId )
788 {
789 setHoveredItem( closestItem, itemBounds );
790 }
791
792 // track hovered node too!... here we want to identify the closest node to the cursor position
793 QgsAnnotationItemNode hoveredNode;
794 if ( closestItem->itemId() == mSelectedItemId && closestItem->layerId() == mSelectedItemLayerId )
795 {
796 double currentNodeDistance = std::numeric_limits< double >::max();
797 mHoveredItemNodesSpatialIndex->intersects( searchRect, [&hoveredNode, &currentNodeDistance, &mapPoint, this]( int index )-> bool
798 {
799 if ( index >= mHoveredItemNodes.size() )
800 return false;
801
802 const QgsAnnotationItemNode &thisNode = mHoveredItemNodes.at( index );
803 const double nodeDistance = thisNode.point().sqrDist( mapPoint );
804 if ( nodeDistance < currentNodeDistance )
805 {
806 hoveredNode = thisNode;
807 currentNodeDistance = nodeDistance;
808 }
809 return true;
810 } );
811 }
812
813 if ( hoveredNode.point().isEmpty() )
814 {
815 // no hovered node
816 if ( mHoveredNodeRubberBand )
817 mHoveredNodeRubberBand->hide();
818 setCursor( mHoveredItemId == mSelectedItemId && mHoveredItemLayerId == mSelectedItemLayerId ? Qt::OpenHandCursor : Qt::ArrowCursor );
819 }
820 else
821 {
822 if ( !mHoveredNodeRubberBand )
823 createHoveredNodeBand();
824
825 mHoveredNodeRubberBand->reset( Qgis::GeometryType::Point );
826 mHoveredNodeRubberBand->addPoint( hoveredNode.point() );
827 mHoveredNodeRubberBand->show();
828
829 setCursor( hoveredNode.cursor() );
830 }
831}
832
833void QgsMapToolModifyAnnotation::clearHoveredItem()
834{
835 if ( mHoverRubberBand )
836 mHoverRubberBand->hide();
837 if ( mHoveredNodeRubberBand )
838 mHoveredNodeRubberBand->hide();
839
840 mHoveredItemId.clear();
841 mHoveredItemLayerId.clear();
842 mHoveredItemNodeRubberBands.clear();
843 mHoveredItemNodesSpatialIndex.reset();
844
845 setCursor( Qt::ArrowCursor );
846}
847
848void QgsMapToolModifyAnnotation::clearSelectedItem()
849{
850 if ( mSelectedRubberBand )
851 mSelectedRubberBand->hide();
852
853 const bool hadSelection = !mSelectedItemId.isEmpty();
854 mSelectedItemId.clear();
855 mSelectedItemLayerId.clear();
856 if ( hadSelection )
857 emit selectionCleared();
858}
859
860void QgsMapToolModifyAnnotation::createHoverBand()
861{
862 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
863
864 mHoverRubberBand.reset( new QgsRubberBand( mCanvas, Qgis::GeometryType::Line ) );
865 mHoverRubberBand->setWidth( scaleFactor );
866 mHoverRubberBand->setSecondaryStrokeColor( QColor( 255, 255, 255, 100 ) );
867 mHoverRubberBand->setColor( QColor( 100, 100, 100, 155 ) );
868}
869
870void QgsMapToolModifyAnnotation::createHoveredNodeBand()
871{
872 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
873
874 mHoveredNodeRubberBand.reset( new QgsRubberBand( mCanvas, Qgis::GeometryType::Point ) );
875 mHoveredNodeRubberBand->setIcon( QgsRubberBand::ICON_FULL_BOX );
876 mHoveredNodeRubberBand->setWidth( scaleFactor );
877 mHoveredNodeRubberBand->setIconSize( scaleFactor * 5 );
878 mHoveredNodeRubberBand->setColor( QColor( 200, 0, 120, 255 ) );
879}
880
881void QgsMapToolModifyAnnotation::createSelectedItemBand()
882{
883 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
884
885 mSelectedRubberBand.reset( new QgsRubberBand( mCanvas, Qgis::GeometryType::Line ) );
886 mSelectedRubberBand->setWidth( scaleFactor );
887 mSelectedRubberBand->setSecondaryStrokeColor( QColor( 255, 255, 255, 100 ) );
888 mSelectedRubberBand->setColor( QColor( 50, 50, 50, 200 ) );
889}
890
void reset(T *p=nullptr)
Will reset the managed pointer to p.
@ VertexHandle
Node is a handle for manipulating vertices.
@ CalloutHandle
Node is a handle for manipulating callouts.
@ ScaleDependentBoundingBox
Item's bounding box will vary depending on map scale.
@ Invalid
Operation has invalid parameters for the item, no change occurred.
@ Success
Item was modified successfully.
@ ItemCleared
The operation results in the item being cleared, and the item should be removed from the layer as a r...
The QgsAdvancedDigitizingDockWidget class is a dockable widget used to handle the CAD tools on top of...
Encapsulates the context for an annotation item edit operation.
void setCurrentItemBounds(const QgsRectangle &bounds)
Sets the current rendered bounds of the item, in the annotation layer's CRS.
void setRenderContext(const QgsRenderContext &context)
Sets the render context associated with the edit operation.
Annotation item edit operation consisting of adding a node.
Annotation item edit operation consisting of deleting a node.
Annotation item edit operation consisting of moving a node.
Annotation item edit operation consisting of translating (moving) an item.
Contains information about a node used for editing an annotation item.
void setPoint(QgsPointXY point)
Sets the node's position, in geographic coordinates.
QgsPointXY point() const
Returns the node's position, in geographic coordinates.
Qt::CursorShape cursor() const
Returns the mouse cursor shape to use when hovering the node.
QgsVertexId id() const
Returns the ID number of the node, used for uniquely identifying the node in the item.
Abstract base class for annotation items which are drawn with QgsAnnotationLayers.
virtual QList< QgsAnnotationItemNode > nodesV2(const QgsAnnotationItemEditContext &context) const
Returns the nodes for the item, used for editing the item.
Represents a map layer containing a set of georeferenced annotations, e.g.
Class for doing transforms between two map coordinate systems.
QgsPointXY transform(const QgsPointXY &point, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Transform the point from the source CRS to the destination CRS.
Custom exception class for Coordinate Reference System related exceptions.
QgsPointXY asPoint() const
Returns the contents of the geometry as a 2-dimensional point.
QgsGeometry centroid() const
Returns the center of mass of a geometry.
Map canvas is a class for displaying all GIS data types on a canvas.
const QgsRenderedItemResults * renderedItemResults(bool allowOutdatedResults=true) const
Gets access to the rendered item results (may be nullptr), which includes the results of rendering an...
void mapCanvasRefreshed()
Emitted when canvas finished a refresh request.
const QgsMapToPixel * getCoordinateTransform()
Gets the current coordinate transform.
QgsCoordinateReferenceSystem crs
Definition qgsmaplayer.h:83
A QgsMapMouseEvent is the result of a user interaction with the mouse on a QgsMapCanvas.
QgsPointXY mapPoint() const
mapPoint returns the point in coordinates
QPoint pixelPoint() const
The snapped mouse cursor in pixel coordinates.
QgsPointLocator::Match mapPointMatch() const
Returns the matching data from the most recently snapped point.
QgsPointXY toMapCoordinates(int x, int y) const
Transforms device coordinates to map (world) coordinates.
The QgsMapToolAdvancedDigitizing class is a QgsMapTool which gives event directly in map coordinates ...
void deactivate() override
Unregisters this maptool from the cad dock widget.
virtual QgsMapLayer * layer() const
Returns the layer associated with the map tool.
void itemSelected(QgsAnnotationLayer *layer, const QString &itemId)
Emitted when the selected item is changed.
void cadCanvasPressEvent(QgsMapMouseEvent *event) override
Override this method when subclassing this class.
void keyPressEvent(QKeyEvent *event) override
Key event for overriding. Default implementation does nothing.
~QgsMapToolModifyAnnotation() override
void deactivate() override
Unregisters this maptool from the cad dock widget.
void canvasDoubleClickEvent(QgsMapMouseEvent *event) override
Mouse double-click event for overriding. Default implementation does nothing.
void selectionCleared()
Emitted when the selected item is cleared;.
QgsMapToolModifyAnnotation(QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWidget *cadDockWidget)
Constructor for QgsMapToolModifyAnnotation.
void cadCanvasMoveEvent(QgsMapMouseEvent *event) override
Override this method when subclassing this class.
QgsPoint toLayerCoordinates(const QgsMapLayer *layer, const QgsPoint &point)
Transforms a point from map coordinates to layer coordinates.
QPointer< QgsMapCanvas > mCanvas
The pointer to the map canvas.
Definition qgsmaptool.h:341
QgsMapCanvas * canvas() const
returns pointer to the tool's map canvas
virtual void setCursor(const QCursor &cursor)
Sets a user defined cursor.
static double searchRadiusMU(const QgsRenderContext &context)
Gets search radius in map units for given context.
A class to represent a 2D point.
Definition qgspointxy.h:60
double sqrDist(double x, double y) const
Returns the squared distance between this point a specified x, y coordinate.
Definition qgspointxy.h:186
double y
Definition qgspointxy.h:64
double x
Definition qgspointxy.h:63
bool isEmpty() const
Returns true if the geometry is empty.
Definition qgspointxy.h:242
Point geometry type, with support for z-dimension and m-values.
Definition qgspoint.h:49
static QgsProject * instance()
Returns the QgsProject singleton instance.
QgsAnnotationLayer * mainAnnotationLayer()
Returns the main annotation layer associated with the project.
void setDirty(bool b=true)
Flag the project as dirty (modified).
A rectangle specified with double values.
bool contains(const QgsRectangle &rect) const
Returns true when rectangle contains other rectangle.
double xMinimum
double yMinimum
double xMaximum
void grow(double delta)
Grows the rectangle in place by the specified amount.
double yMaximum
double distance(const QgsPointXY &point) const
Returns the distance from point to the nearest point on the boundary of the rectangle.
static QgsRenderContext fromMapSettings(const QgsMapSettings &mapSettings)
create initialized QgsRenderContext instance from given QgsMapSettings
Contains information about a rendered annotation item.
QString itemId() const
Returns the item ID of the associated annotation item.
Base class for detailed information about a rendered item.
QString layerId() const
Returns the layer ID of the associated map layer.
QgsRectangle boundingBox() const
Returns the bounding box of the item (in map units).
Stores collated details of rendered items during a map rendering operation.
QList< const QgsRenderedAnnotationItemDetails * > renderedAnnotationItemsInBounds(const QgsRectangle &bounds) const
Returns a list with details of the rendered annotation items within the specified bounds.
QList< QgsRenderedItemDetails * > renderedItems() const
Returns a list of all rendered items.
A class for drawing transient features (e.g.
void setIconSize(double iconSize)
Sets the size of the point icons.
QgsGeometry asGeometry() const
Returns the rubberband as a Geometry.
void setWidth(double width)
Sets the width of the line.
void reset(Qgis::GeometryType geometryType=Qgis::GeometryType::Line)
Clears all the geometries in this rubberband.
void setSecondaryStrokeColor(const QColor &color)
Sets a secondary stroke color for the rubberband which will be drawn under the main stroke color.
@ ICON_X
A cross is used to highlight points (x)
@ ICON_FULL_BOX
A full box is used to highlight points (■)
@ ICON_BOX
A box is used to highlight points (□)
void setColor(const QColor &color)
Sets the color for the rubberband.
void setToGeometry(const QgsGeometry &geom, QgsVectorLayer *layer)
Sets this rubber band to geom.
void setIcon(IconType icon)
Sets the icon type to highlight point geometries.
void copyPointsFrom(const QgsRubberBand *other)
Copies the points from another rubber band.
void setTranslationOffset(double dx, double dy)
Adds translation to original coordinates (all in map coordinates)
void addPoint(const QgsPointXY &p, bool doUpdate=true, int geometryIndex=0, int ringIndex=0)
Adds a vertex to the rubberband and update canvas.
Class that shows snapping marker on map canvas for the current snapping match.
A class to represent a vector.
Definition qgsvector.h:30
double y() const
Returns the vector's y-component.
Definition qgsvector.h:152
double x() const
Returns the vector's x-component.
Definition qgsvector.h:143