QGIS API Documentation 3.43.0-Master (87898417f79)
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 void insert( int index, const QgsRectangle &bounds )
41 {
42 std::array<float, 4> scaledBounds = scaleBounds( bounds );
43 const float aMin[2] {
44 scaledBounds[0], scaledBounds[1]
45 };
46 const float aMax[2] {
47 scaledBounds[2], scaledBounds[3]
48 };
49 this->Insert(
50 aMin,
51 aMax,
52 index
53 );
54 }
55
62 void remove( int index, const QgsRectangle &bounds )
63 {
64 std::array<float, 4> scaledBounds = scaleBounds( bounds );
65 const float aMin[2] {
66 scaledBounds[0], scaledBounds[1]
67 };
68 const float aMax[2] {
69 scaledBounds[2], scaledBounds[3]
70 };
71 this->Remove(
72 aMin,
73 aMax,
74 index
75 );
76 }
77
83 bool intersects( const QgsRectangle &bounds, const std::function<bool( int index )> &callback ) const
84 {
85 std::array<float, 4> scaledBounds = scaleBounds( bounds );
86 const float aMin[2] {
87 scaledBounds[0], scaledBounds[1]
88 };
89 const float aMax[2] {
90 scaledBounds[2], scaledBounds[3]
91 };
92 this->Search(
93 aMin,
94 aMax,
95 callback
96 );
97 return true;
98 }
99
100 private:
101 std::array<float, 4> scaleBounds( const QgsRectangle &bounds ) const
102 {
103 return {
104 static_cast<float>( bounds.xMinimum() ),
105 static_cast<float>( bounds.yMinimum() ),
106 static_cast<float>( bounds.xMaximum() ),
107 static_cast<float>( bounds.yMaximum() )
108 };
109 }
110};
112
113
115 : QgsMapToolAdvancedDigitizing( canvas, cadDockWidget )
116 , mSnapIndicator( new QgsSnapIndicator( canvas ) )
117{
118 connect( QgsMapToolModifyAnnotation::canvas(), &QgsMapCanvas::mapCanvasRefreshed, this, &QgsMapToolModifyAnnotation::onCanvasRefreshed );
119}
120
122
124{
125 mSnapIndicator->setMatch( QgsPointLocator::Match() );
126
127 clearHoveredItem();
128 clearSelectedItem();
130}
131
133{
134 mLastHoverPoint = event->originalPixelPoint();
135 event->snapPoint();
136 mSnapIndicator->setMatch( event->mapPointMatch() );
137
138 const QgsPointXY mapPoint = event->mapPoint();
139
141 QgsAnnotationLayer *layer = annotationLayerFromId( mSelectedItemLayerId );
142 context.setCurrentItemBounds( toLayerCoordinates( layer, mSelectedItemBounds ) );
143 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
144
145 switch ( mCurrentAction )
146 {
147 case Action::NoAction:
148 {
149 setHoveredItemFromPoint( mapPoint );
150 break;
151 }
152
153 case Action::MoveItem:
154 {
155 if ( QgsAnnotationItem *item = annotationItemFromId( mSelectedItemLayerId, mSelectedItemId ) )
156 {
157 const QgsVector delta = toLayerCoordinates( layer, event->mapPoint() ) - mMoveStartPointLayerCrs;
158
159 QgsAnnotationItemEditOperationTranslateItem operation( mSelectedItemId, delta.x(), delta.y(), event->pixelPoint().x() - mMoveStartPointPixels.x(), event->pixelPoint().y() - mMoveStartPointPixels.y() );
160 std::unique_ptr<QgsAnnotationItemEditOperationTransientResults> operationResults( item->transientEditResultsV2( &operation, context ) );
161 if ( operationResults )
162 {
163 mTemporaryRubberBand.reset( new QgsRubberBand( mCanvas, operationResults->representativeGeometry().type() ) );
164 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
165 mTemporaryRubberBand->setWidth( scaleFactor );
166 mTemporaryRubberBand->setToGeometry( operationResults->representativeGeometry(), layer->crs() );
167 }
168 else
169 {
170 mTemporaryRubberBand.reset();
171 }
172 }
173 break;
174 }
175
176 case Action::MoveNode:
177 {
178 if ( QgsAnnotationItem *item = annotationItemFromId( mSelectedItemLayerId, mSelectedItemId ) )
179 {
180 const QgsPointXY endPointLayer = toLayerCoordinates( layer, event->mapPoint() );
181 QgsAnnotationItemEditOperationMoveNode operation( mSelectedItemId, mTargetNode.id(), QgsPoint( mTargetNode.point() ), QgsPoint( endPointLayer ), event->pixelPoint().x() - mMoveStartPointPixels.x(), event->pixelPoint().y() - mMoveStartPointPixels.y() );
182 std::unique_ptr<QgsAnnotationItemEditOperationTransientResults> operationResults( item->transientEditResultsV2( &operation, context ) );
183 if ( operationResults )
184 {
185 mTemporaryRubberBand.reset( new QgsRubberBand( mCanvas, operationResults->representativeGeometry().type() ) );
186 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
187 mTemporaryRubberBand->setWidth( scaleFactor );
188 mTemporaryRubberBand->setToGeometry( operationResults->representativeGeometry(), layer->crs() );
189 }
190 else
191 {
192 mTemporaryRubberBand.reset();
193 }
194 }
195 break;
196 }
197 }
198}
199
201{
202 QgsAnnotationLayer *layer = annotationLayerFromId( mSelectedItemLayerId );
203
205 context.setCurrentItemBounds( toLayerCoordinates( layer, mSelectedItemBounds ) );
206 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
207
208 switch ( mCurrentAction )
209 {
210 case Action::NoAction:
211 {
212 if ( event->button() != Qt::LeftButton )
213 return;
214
215 if ( mHoveredItemId.isEmpty() || !mHoverRubberBand )
216 {
217 clearSelectedItem();
218 }
219 else if ( mHoveredItemId == mSelectedItemId && mHoveredItemLayerId == mSelectedItemLayerId )
220 {
221 // press is on selected item => move that item
222 if ( layer )
223 {
224 const QgsPointXY mapPoint = event->mapPoint();
225 QgsRectangle searchRect = QgsRectangle( mapPoint.x(), mapPoint.y(), mapPoint.x(), mapPoint.y() );
226 searchRect.grow( searchRadiusMU( canvas() ) );
227
228 QgsAnnotationItemNode hoveredNode;
229 double currentNodeDistance = std::numeric_limits<double>::max();
230 mHoveredItemNodesSpatialIndex->intersects( searchRect, [&hoveredNode, &currentNodeDistance, &mapPoint, this]( int index ) -> bool {
231 const QgsAnnotationItemNode &thisNode = mHoveredItemNodes.at( index );
232 const double nodeDistance = thisNode.point().sqrDist( mapPoint );
233 if ( nodeDistance < currentNodeDistance )
234 {
235 hoveredNode = thisNode;
236 currentNodeDistance = nodeDistance;
237 }
238 return true;
239 } );
240
241 mMoveStartPointCanvasCrs = mapPoint;
242 mMoveStartPointPixels = event->pixelPoint();
243 mMoveStartPointLayerCrs = toLayerCoordinates( layer, mMoveStartPointCanvasCrs );
244 if ( mHoverRubberBand )
245 mHoverRubberBand->hide();
246 if ( mSelectedRubberBand )
247 mSelectedRubberBand->hide();
248
249 if ( hoveredNode.point().isEmpty() )
250 {
251 mCurrentAction = Action::MoveItem;
252 }
253 else
254 {
255 mCurrentAction = Action::MoveNode;
256 mTargetNode = hoveredNode;
257 }
258 }
259 }
260 else
261 {
262 // press is on a different item to selected item => select that item
263 mSelectedItemId = mHoveredItemId;
264 mSelectedItemLayerId = mHoveredItemLayerId;
265 mSelectedItemBounds = mHoveredItemBounds;
266
267 if ( !mSelectedRubberBand )
268 createSelectedItemBand();
269
270 mSelectedRubberBand->copyPointsFrom( mHoverRubberBand );
271 mSelectedRubberBand->show();
272
273 setCursor( Qt::OpenHandCursor );
274
275 emit itemSelected( annotationLayerFromId( mSelectedItemLayerId ), mSelectedItemId );
276 }
277 break;
278 }
279
280 case Action::MoveItem:
281 {
282 if ( event->button() == Qt::RightButton )
283 {
284 mCurrentAction = Action::NoAction;
285 mTemporaryRubberBand.reset();
286 if ( mSelectedRubberBand )
287 {
288 mSelectedRubberBand->setTranslationOffset( 0, 0 );
289 mSelectedRubberBand->show();
290 }
291 mHoveredItemNodeRubberBands.clear();
292 setCursor( Qt::ArrowCursor );
293 }
294 else if ( event->button() == Qt::LeftButton )
295 {
296 // apply move
297 if ( layer )
298 {
299 const QgsVector delta = toLayerCoordinates( layer, event->mapPoint() ) - mMoveStartPointLayerCrs;
300
301 QgsAnnotationItemEditOperationTranslateItem operation( mSelectedItemId, delta.x(), delta.y(), event->pixelPoint().x() - mMoveStartPointPixels.x(), event->pixelPoint().y() - mMoveStartPointPixels.y() );
302 switch ( layer->applyEditV2( &operation, context ) )
303 {
306 mRefreshSelectedItemAfterRedraw = true;
307 break;
310 break;
311 }
312 }
313
314 mTemporaryRubberBand.reset();
315 mCurrentAction = Action::NoAction;
316 setCursor( Qt::ArrowCursor );
317 }
318 break;
319 }
320
321 case Action::MoveNode:
322 {
323 if ( event->button() == Qt::RightButton )
324 {
325 mCurrentAction = Action::NoAction;
326 mTemporaryRubberBand.reset();
327 mHoveredItemNodeRubberBands.clear();
328 mTemporaryRubberBand.reset();
329 setCursor( Qt::ArrowCursor );
330 }
331 else if ( event->button() == Qt::LeftButton )
332 {
333 if ( layer )
334 {
335 const QgsPointXY endPointLayer = toLayerCoordinates( layer, event->mapPoint() );
336 QgsAnnotationItemEditOperationMoveNode operation( mSelectedItemId, mTargetNode.id(), QgsPoint( mTargetNode.point() ), QgsPoint( endPointLayer ), event->pixelPoint().x() - mMoveStartPointPixels.x(), event->pixelPoint().y() - mMoveStartPointPixels.y() );
337 switch ( layer->applyEditV2( &operation, context ) )
338 {
341 mRefreshSelectedItemAfterRedraw = true;
342 break;
343
346 break;
347 }
348 }
349
350 mTemporaryRubberBand.reset();
351 mHoveredItemNodeRubberBands.clear();
352 mHoveredItemNodes.clear();
353 mTemporaryRubberBand.reset();
354 mCurrentAction = Action::NoAction;
355 setCursor( Qt::ArrowCursor );
356 }
357 break;
358 }
359 }
360}
361
363{
364 switch ( mCurrentAction )
365 {
366 case Action::NoAction:
367 case Action::MoveItem:
368 {
369 if ( event->button() != Qt::LeftButton )
370 return;
371
372 mCurrentAction = Action::NoAction;
373 if ( mHoveredItemId == mSelectedItemId && mHoveredItemLayerId == mSelectedItemLayerId )
374 {
375 // double-click on selected item => add node
376 if ( QgsAnnotationLayer *layer = annotationLayerFromId( mSelectedItemLayerId ) )
377 {
378 const QgsPointXY layerPoint = toLayerCoordinates( layer, event->mapPoint() );
379 QgsAnnotationItemEditOperationAddNode operation( mSelectedItemId, QgsPoint( layerPoint ) );
381 context.setCurrentItemBounds( toLayerCoordinates( layer, mSelectedItemBounds ) );
382 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
383
384 switch ( layer->applyEditV2( &operation, context ) )
385 {
388 mRefreshSelectedItemAfterRedraw = true;
389 break;
390
393 break;
394 }
395 }
396 }
397 else
398 {
399 // press is on a different item to selected item => select that item
400 mSelectedItemId = mHoveredItemId;
401 mSelectedItemLayerId = mHoveredItemLayerId;
402 mSelectedItemBounds = mHoveredItemBounds;
403
404 if ( !mSelectedRubberBand )
405 createSelectedItemBand();
406
407 mSelectedRubberBand->copyPointsFrom( mHoverRubberBand );
408 mSelectedRubberBand->show();
409
410 setCursor( Qt::OpenHandCursor );
411
412 emit itemSelected( annotationLayerFromId( mSelectedItemLayerId ), mSelectedItemId );
413 }
414 break;
415 }
416
417 case Action::MoveNode:
418 break;
419 }
420}
421
423{
425 QgsAnnotationLayer *layer = annotationLayerFromId( mSelectedItemLayerId );
426 context.setCurrentItemBounds( toLayerCoordinates( layer, mSelectedItemBounds ) );
427 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
428
429 switch ( mCurrentAction )
430 {
431 case Action::NoAction:
432 {
433 if ( event->key() == Qt::Key_Backspace || event->key() == Qt::Key_Delete )
434 {
435 if ( !layer || mSelectedItemId.isEmpty() )
436 return;
437
438 layer->removeItem( mSelectedItemId );
439 clearSelectedItem();
440 clearHoveredItem();
441 event->ignore(); // disable default shortcut handling
442 }
443 else if ( event->key() == Qt::Key_Left
444 || event->key() == Qt::Key_Right
445 || event->key() == Qt::Key_Up
446 || event->key() == Qt::Key_Down )
447 {
448 if ( !layer )
449 return;
450
451 const QSizeF deltaLayerCoordinates = deltaForKeyEvent( layer, mSelectedRubberBand->asGeometry().centroid().asPoint(), event );
452
453 QgsAnnotationItemEditOperationTranslateItem operation( mSelectedItemId, deltaLayerCoordinates.width(), deltaLayerCoordinates.height() );
454 switch ( layer->applyEditV2( &operation, context ) )
455 {
458 mRefreshSelectedItemAfterRedraw = true;
459 break;
462 break;
463 }
464 event->ignore(); // disable default shortcut handling (move map)
465 }
466 break;
467 }
468
469 case Action::MoveNode:
470 {
471 if ( event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace )
472 {
473 if ( layer )
474 {
475 QgsAnnotationItemEditOperationDeleteNode operation( mSelectedItemId, mTargetNode.id(), QgsPoint( mTargetNode.point() ) );
476 switch ( layer->applyEditV2( &operation, context ) )
477 {
480 mRefreshSelectedItemAfterRedraw = true;
481 break;
483 break;
486 break;
487 }
488 }
489
490 mTemporaryRubberBand.reset();
491 mHoveredItemNodeRubberBands.clear();
492 mHoveredItemNodes.clear();
493 mTemporaryRubberBand.reset();
494 mCurrentAction = Action::NoAction;
495 setCursor( Qt::ArrowCursor );
496 event->ignore(); // disable default shortcut handling (delete vector feature)
497 break;
498 }
499 [[fallthrough]];
500 }
501
502 case Action::MoveItem:
503 {
504 // warning -- fallthrough above!
505 if ( event->key() == Qt::Key_Escape )
506 {
507 mCurrentAction = Action::NoAction;
508 mTemporaryRubberBand.reset();
509 if ( mSelectedRubberBand )
510 {
511 mSelectedRubberBand->setTranslationOffset( 0, 0 );
512 mSelectedRubberBand->show();
513 }
514 mHoveredItemNodeRubberBands.clear();
515
516 setCursor( Qt::ArrowCursor );
517 }
518 break;
519 }
520 }
521}
522
523void QgsMapToolModifyAnnotation::onCanvasRefreshed()
524{
525 bool needsSelectedItemRefresh = mRefreshSelectedItemAfterRedraw;
526 if ( QgsAnnotationItem *item = annotationItemFromId( mSelectedItemLayerId, mSelectedItemId ) )
527 {
529 {
530 needsSelectedItemRefresh = true;
531 }
532 }
533
534 if ( needsSelectedItemRefresh )
535 {
536 const QgsRenderedItemResults *renderedItemResults = canvas()->renderedItemResults( false );
537 if ( !renderedItemResults )
538 {
539 return;
540 }
541
542 const QList<QgsRenderedItemDetails *> items = renderedItemResults->renderedItems();
543 auto it = std::find_if( items.begin(), items.end(), [this]( const QgsRenderedItemDetails *item ) {
544 if ( const QgsRenderedAnnotationItemDetails *annotationItem = dynamic_cast<const QgsRenderedAnnotationItemDetails *>( item ) )
545 {
546 if ( annotationItem->itemId() == mSelectedItemId && annotationItem->layerId() == mSelectedItemLayerId )
547 return true;
548 }
549 return false;
550 } );
551 if ( it != items.end() )
552 {
553 const QgsRectangle itemBounds = ( *it )->boundingBox();
554
555 setHoveredItem( dynamic_cast<const QgsRenderedAnnotationItemDetails *>( *it ), itemBounds );
556 if ( !mSelectedRubberBand )
557 createSelectedItemBand();
558
559 mSelectedRubberBand->copyPointsFrom( mHoverRubberBand );
560 mSelectedRubberBand->show();
561 mSelectedItemBounds = mHoveredItemBounds;
562 }
563 }
564 else
565 {
566 // recheck for hovered item at new mouse point
567 const QgsPointXY mapPoint = canvas()->mapSettings().mapToPixel().toMapCoordinates( mLastHoverPoint );
568 setHoveredItemFromPoint( mapPoint );
569 }
570 mRefreshSelectedItemAfterRedraw = false;
571}
572
573void QgsMapToolModifyAnnotation::setHoveredItem( const QgsRenderedAnnotationItemDetails *item, const QgsRectangle &itemMapBounds )
574{
575 mHoveredItemNodeRubberBands.clear();
576 if ( mHoveredNodeRubberBand )
577 mHoveredNodeRubberBand->hide();
578 mHoveredItemId = item->itemId();
579 mHoveredItemLayerId = item->layerId();
580 mHoveredItemBounds = itemMapBounds;
581 if ( !mHoverRubberBand )
582 createHoverBand();
583
584 mHoverRubberBand->show();
585
586 mHoverRubberBand->reset( Qgis::GeometryType::Line );
587 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMinimum(), itemMapBounds.yMinimum() ) );
588 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMaximum(), itemMapBounds.yMinimum() ) );
589 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMaximum(), itemMapBounds.yMaximum() ) );
590 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMinimum(), itemMapBounds.yMaximum() ) );
591 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMinimum(), itemMapBounds.yMinimum() ) );
592
593 QgsAnnotationLayer *layer = annotationLayerFromId( item->layerId() );
594 const QgsAnnotationItem *annotationItem = annotationItemFromId( item->layerId(), item->itemId() );
595 if ( !annotationItem )
596 return;
597
598 QgsCoordinateTransform layerToMapTransform = QgsCoordinateTransform( layer->crs(), canvas()->mapSettings().destinationCrs(), canvas()->mapSettings().transformContext() );
599
600 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
601
603 context.setCurrentItemBounds( toLayerCoordinates( layer, itemMapBounds ) );
604 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
605
606 const QList<QgsAnnotationItemNode> itemNodes = annotationItem->nodesV2( context );
608
609 vertexNodeBand->setIcon( QgsRubberBand::ICON_BOX );
610 vertexNodeBand->setWidth( scaleFactor );
611 vertexNodeBand->setIconSize( scaleFactor * 5 );
612 vertexNodeBand->setColor( QColor( 200, 0, 120, 255 ) );
613
615 calloutNodeBand->setWidth( scaleFactor );
616 calloutNodeBand->setSecondaryStrokeColor( QColor( 255, 255, 255, 100 ) );
617 calloutNodeBand->setColor( QColor( 120, 200, 0, 255 ) );
618 calloutNodeBand->setIcon( QgsRubberBand::ICON_X );
619 calloutNodeBand->setIconSize( scaleFactor * 5 );
620
621 // store item nodes in a spatial index for quick searching
622 mHoveredItemNodesSpatialIndex = std::make_unique<QgsAnnotationItemNodesSpatialIndex>();
623 int index = 0;
624 mHoveredItemNodes.clear();
625 mHoveredItemNodes.reserve( itemNodes.size() );
626 for ( const QgsAnnotationItemNode &node : itemNodes )
627 {
628 QgsPointXY nodeMapPoint;
629 try
630 {
631 nodeMapPoint = layerToMapTransform.transform( node.point() );
632 }
633 catch ( QgsCsException & )
634 {
635 continue;
636 }
637
638 switch ( node.type() )
639 {
641 vertexNodeBand->addPoint( nodeMapPoint );
642 break;
643
645 calloutNodeBand->addPoint( nodeMapPoint );
646 break;
647 }
648
649 mHoveredItemNodesSpatialIndex->insert( index, QgsRectangle( nodeMapPoint.x(), nodeMapPoint.y(), nodeMapPoint.x(), nodeMapPoint.y() ) );
650
651 QgsAnnotationItemNode transformedNode = node;
652 transformedNode.setPoint( nodeMapPoint );
653 mHoveredItemNodes.append( transformedNode );
654
655 index++;
656 }
657
658 mHoveredItemNodeRubberBands.emplace_back( vertexNodeBand );
659 mHoveredItemNodeRubberBands.emplace_back( calloutNodeBand );
660}
661
662QSizeF QgsMapToolModifyAnnotation::deltaForKeyEvent( QgsAnnotationLayer *layer, const QgsPointXY &originalCanvasPoint, QKeyEvent *event )
663{
664 const double canvasDpi = canvas()->window()->windowHandle()->screen()->physicalDotsPerInch();
665
666 // increment used for cursor key item movement
667 double incrementPixels = 0.0;
668 if ( event->modifiers() & Qt::ShiftModifier )
669 {
670 //holding shift while pressing cursor keys results in a big step - 20 mm
671 incrementPixels = 20.0 / 25.4 * canvasDpi;
672 }
673 else if ( event->modifiers() & Qt::AltModifier )
674 {
675 //holding alt while pressing cursor keys results in a 1 pixel step
676 incrementPixels = 1;
677 }
678 else
679 {
680 // 5 mm
681 incrementPixels = 5.0 / 25.4 * canvasDpi;
682 }
683
684 double deltaXPixels = 0;
685 double deltaYPixels = 0;
686 switch ( event->key() )
687 {
688 case Qt::Key_Left:
689 deltaXPixels = -incrementPixels;
690 break;
691 case Qt::Key_Right:
692 deltaXPixels = incrementPixels;
693 break;
694 case Qt::Key_Up:
695 deltaYPixels = -incrementPixels;
696 break;
697 case Qt::Key_Down:
698 deltaYPixels = incrementPixels;
699 break;
700 default:
701 break;
702 }
703
704 const QgsPointXY beforeMoveMapPoint = canvas()->getCoordinateTransform()->toMapCoordinates( originalCanvasPoint.x(), originalCanvasPoint.y() );
705 const QgsPointXY beforeMoveLayerPoint = toLayerCoordinates( layer, beforeMoveMapPoint );
706
707 const QgsPointXY afterMoveCanvasPoint( originalCanvasPoint.x() + deltaXPixels, originalCanvasPoint.y() + deltaYPixels );
708 const QgsPointXY afterMoveMapPoint = canvas()->getCoordinateTransform()->toMapCoordinates( afterMoveCanvasPoint.x(), afterMoveCanvasPoint.y() );
709 const QgsPointXY afterMoveLayerPoint = toLayerCoordinates( layer, afterMoveMapPoint );
710
711 return QSizeF( afterMoveLayerPoint.x() - beforeMoveLayerPoint.x(), afterMoveLayerPoint.y() - beforeMoveLayerPoint.y() );
712}
713
714const QgsRenderedAnnotationItemDetails *QgsMapToolModifyAnnotation::findClosestItemToPoint( const QgsPointXY &mapPoint, const QList<const QgsRenderedAnnotationItemDetails *> &items, QgsRectangle &bounds )
715{
716 const QgsRenderedAnnotationItemDetails *closestItem = nullptr;
717 double closestItemDistance = std::numeric_limits<double>::max();
718 double closestItemArea = std::numeric_limits<double>::max();
719
720 for ( const QgsRenderedAnnotationItemDetails *item : items )
721 {
722 const QgsAnnotationItem *annotationItem = annotationItemFromId( item->layerId(), item->itemId() );
723 if ( !annotationItem )
724 continue;
725
726 const QgsRectangle itemBounds = item->boundingBox();
727 const double itemDistance = itemBounds.contains( mapPoint ) ? 0 : itemBounds.distance( mapPoint );
728 if ( !closestItem || itemDistance < closestItemDistance || ( itemDistance == closestItemDistance && itemBounds.area() < closestItemArea ) )
729 {
730 closestItem = item;
731 closestItemDistance = itemDistance;
732 closestItemArea = itemBounds.area();
733 bounds = itemBounds;
734 }
735 }
736 return closestItem;
737}
738
739QgsAnnotationLayer *QgsMapToolModifyAnnotation::annotationLayerFromId( const QString &layerId )
740{
741 QgsAnnotationLayer *layer = qobject_cast<QgsAnnotationLayer *>( QgsProject::instance()->mapLayer( layerId ) );
742 if ( !layer && layerId == QgsProject::instance()->mainAnnotationLayer()->id() )
744 return layer;
745}
746
747QgsAnnotationItem *QgsMapToolModifyAnnotation::annotationItemFromId( const QString &layerId, const QString &itemId )
748{
749 QgsAnnotationLayer *layer = annotationLayerFromId( layerId );
750 return layer ? layer->item( itemId ) : nullptr;
751}
752
753void QgsMapToolModifyAnnotation::setHoveredItemFromPoint( const QgsPointXY &mapPoint )
754{
755 QgsRectangle searchRect = QgsRectangle( mapPoint.x(), mapPoint.y(), mapPoint.x(), mapPoint.y() );
756 searchRect.grow( searchRadiusMU( canvas() ) );
757
758 const QgsRenderedItemResults *renderedItemResults = canvas()->renderedItemResults( false );
759 if ( !renderedItemResults )
760 {
761 clearHoveredItem();
762 return;
763 }
764
765 const QList<const QgsRenderedAnnotationItemDetails *> items = renderedItemResults->renderedAnnotationItemsInBounds( searchRect );
766 if ( items.empty() )
767 {
768 clearHoveredItem();
769 return;
770 }
771
772 // find closest item
773 QgsRectangle itemBounds;
774 const QgsRenderedAnnotationItemDetails *closestItem = findClosestItemToPoint( mapPoint, items, itemBounds );
775 if ( !closestItem )
776 {
777 clearHoveredItem();
778 return;
779 }
780
781 if ( closestItem->itemId() != mHoveredItemId || closestItem->layerId() != mHoveredItemLayerId )
782 {
783 setHoveredItem( closestItem, itemBounds );
784 }
785
786 // track hovered node too!... here we want to identify the closest node to the cursor position
787 QgsAnnotationItemNode hoveredNode;
788 if ( closestItem->itemId() == mSelectedItemId && closestItem->layerId() == mSelectedItemLayerId )
789 {
790 double currentNodeDistance = std::numeric_limits<double>::max();
791 mHoveredItemNodesSpatialIndex->intersects( searchRect, [&hoveredNode, &currentNodeDistance, &mapPoint, this]( int index ) -> bool {
792 if ( index >= mHoveredItemNodes.size() )
793 return false;
794
795 const QgsAnnotationItemNode &thisNode = mHoveredItemNodes.at( index );
796 const double nodeDistance = thisNode.point().sqrDist( mapPoint );
797 if ( nodeDistance < currentNodeDistance )
798 {
799 hoveredNode = thisNode;
800 currentNodeDistance = nodeDistance;
801 }
802 return true;
803 } );
804 }
805
806 if ( hoveredNode.point().isEmpty() )
807 {
808 // no hovered node
809 if ( mHoveredNodeRubberBand )
810 mHoveredNodeRubberBand->hide();
811 setCursor( mHoveredItemId == mSelectedItemId && mHoveredItemLayerId == mSelectedItemLayerId ? Qt::OpenHandCursor : Qt::ArrowCursor );
812 }
813 else
814 {
815 if ( !mHoveredNodeRubberBand )
816 createHoveredNodeBand();
817
818 mHoveredNodeRubberBand->reset( Qgis::GeometryType::Point );
819 mHoveredNodeRubberBand->addPoint( hoveredNode.point() );
820 mHoveredNodeRubberBand->show();
821
822 setCursor( hoveredNode.cursor() );
823 }
824}
825
826void QgsMapToolModifyAnnotation::clearHoveredItem()
827{
828 if ( mHoverRubberBand )
829 mHoverRubberBand->hide();
830 if ( mHoveredNodeRubberBand )
831 mHoveredNodeRubberBand->hide();
832
833 mHoveredItemId.clear();
834 mHoveredItemLayerId.clear();
835 mHoveredItemNodeRubberBands.clear();
836 mHoveredItemNodesSpatialIndex.reset();
837
838 setCursor( Qt::ArrowCursor );
839}
840
841void QgsMapToolModifyAnnotation::clearSelectedItem()
842{
843 if ( mSelectedRubberBand )
844 mSelectedRubberBand->hide();
845
846 const bool hadSelection = !mSelectedItemId.isEmpty();
847 mSelectedItemId.clear();
848 mSelectedItemLayerId.clear();
849 if ( hadSelection )
850 emit selectionCleared();
851}
852
853void QgsMapToolModifyAnnotation::createHoverBand()
854{
855 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
856
857 mHoverRubberBand.reset( new QgsRubberBand( mCanvas, Qgis::GeometryType::Line ) );
858 mHoverRubberBand->setWidth( scaleFactor );
859 mHoverRubberBand->setSecondaryStrokeColor( QColor( 255, 255, 255, 100 ) );
860 mHoverRubberBand->setColor( QColor( 100, 100, 100, 155 ) );
861}
862
863void QgsMapToolModifyAnnotation::createHoveredNodeBand()
864{
865 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
866
867 mHoveredNodeRubberBand.reset( new QgsRubberBand( mCanvas, Qgis::GeometryType::Point ) );
868 mHoveredNodeRubberBand->setIcon( QgsRubberBand::ICON_FULL_BOX );
869 mHoveredNodeRubberBand->setWidth( scaleFactor );
870 mHoveredNodeRubberBand->setIconSize( scaleFactor * 5 );
871 mHoveredNodeRubberBand->setColor( QColor( 200, 0, 120, 255 ) );
872}
873
874void QgsMapToolModifyAnnotation::createSelectedItemBand()
875{
876 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
877
878 mSelectedRubberBand.reset( new QgsRubberBand( mCanvas, Qgis::GeometryType::Line ) );
879 mSelectedRubberBand->setWidth( scaleFactor );
880 mSelectedRubberBand->setSecondaryStrokeColor( QColor( 255, 255, 255, 100 ) );
881 mSelectedRubberBand->setColor( QColor( 50, 50, 50, 200 ) );
882}
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.
QgsMapCanvas * canvas() const
returns pointer to the tool's map canvas
virtual void setCursor(const QCursor &cursor)
Sets a user defined cursor.
QPointer< QgsMapCanvas > mCanvas
The pointer to the map canvas.
Definition qgsmaptool.h:338
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