QGIS API Documentation 4.1.0-Master (5bf3c20f3c9)
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
19#include "RTree.h"
20#include "qgsannotationitem.h"
23#include "qgsannotationlayer.h"
24#include "qgsmapcanvas.h"
25#include "qgsproject.h"
29#include "qgsrubberband.h"
30#include "qgssnapindicator.h"
31
32#include <QScreen>
33#include <QTransform>
34#include <QWindow>
35
36#include "moc_qgsmaptoolmodifyannotation.cpp"
37
39class QgsAnnotationItemNodesSpatialIndex : public RTree<int, float, 2, float>
40{
41 public:
42 void insert( int index, const QgsRectangle &bounds )
43 {
44 std::array<float, 4> scaledBounds = scaleBounds( bounds );
45 const float aMin[2] { scaledBounds[0], scaledBounds[1] };
46 const float aMax[2] { scaledBounds[2], scaledBounds[3] };
47 this->Insert( aMin, aMax, index );
48 }
49
56 void remove( int index, const QgsRectangle &bounds )
57 {
58 std::array<float, 4> scaledBounds = scaleBounds( bounds );
59 const float aMin[2] { scaledBounds[0], scaledBounds[1] };
60 const float aMax[2] { scaledBounds[2], scaledBounds[3] };
61 this->Remove( aMin, aMax, index );
62 }
63
69 bool intersects( const QgsRectangle &bounds, const std::function<bool( int index )> &callback ) const
70 {
71 std::array<float, 4> scaledBounds = scaleBounds( bounds );
72 const float aMin[2] { scaledBounds[0], scaledBounds[1] };
73 const float aMax[2] { scaledBounds[2], scaledBounds[3] };
74 this->Search( aMin, aMax, callback );
75 return true;
76 }
77
78 private:
79 std::array<float, 4> scaleBounds( const QgsRectangle &bounds ) const
80 {
81 return { static_cast<float>( bounds.xMinimum() ), static_cast<float>( bounds.yMinimum() ), static_cast<float>( bounds.xMaximum() ), static_cast<float>( bounds.yMaximum() ) };
82 }
83};
85
86
93
95
97{
98 mSnapIndicator->setMatch( QgsPointLocator::Match() );
99
100 clearHoveredItem();
101 clearSelectedItem();
103}
104
106{
107 mLastHoverPoint = event->originalPixelPoint();
108 event->snapPoint();
109 mSnapIndicator->setMatch( event->mapPointMatch() );
110
111 const QgsPointXY mapPoint = event->mapPoint();
112
114 QgsAnnotationLayer *layer = annotationLayerFromId( mSelectedItemLayerId );
115 context.setCurrentItemBounds( toLayerCoordinates( layer, mSelectedItemBounds ) );
116 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
117
118 switch ( mCurrentAction )
119 {
120 case Action::NoAction:
121 {
122 setHoveredItemFromPoint( mapPoint );
123 break;
124 }
125
126 case Action::MoveItem:
127 {
128 if ( QgsAnnotationItem *item = annotationItemFromId( mSelectedItemLayerId, mSelectedItemId ) )
129 {
130 const QgsVector delta = toLayerCoordinates( layer, event->mapPoint() ) - mMoveStartPointLayerCrs;
131
133 operation( mSelectedItemId, delta.x(), delta.y(), event->pixelPoint().x() - mMoveStartPointPixels.x(), event->pixelPoint().y() - mMoveStartPointPixels.y() );
134 std::unique_ptr<QgsAnnotationItemEditOperationTransientResults> operationResults( item->transientEditResultsV2( &operation, context ) );
135 if ( operationResults )
136 {
137 mTemporaryRubberBand.reset( new QgsRubberBand( mCanvas, operationResults->representativeGeometry().type() ) );
138 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
139 mTemporaryRubberBand->setWidth( scaleFactor );
140 mTemporaryRubberBand->setToGeometry( operationResults->representativeGeometry(), layer->crs() );
141 }
142 else
143 {
144 mTemporaryRubberBand.reset();
145 }
146 }
147 break;
148 }
149
150 case Action::MoveNode:
151 {
152 if ( QgsAnnotationItem *item = annotationItemFromId( mSelectedItemLayerId, mSelectedItemId ) )
153 {
154 const QgsPointXY endPointLayer = toLayerCoordinates( layer, event->mapPoint() );
156 mSelectedItemId,
157 mTargetNode.id(),
158 QgsPoint( mTargetNode.point() ),
159 QgsPoint( endPointLayer ),
160 event->pixelPoint().x() - mMoveStartPointPixels.x(),
161 event->pixelPoint().y() - mMoveStartPointPixels.y()
162 );
163 std::unique_ptr<QgsAnnotationItemEditOperationTransientResults> operationResults( item->transientEditResultsV2( &operation, context ) );
164 if ( operationResults )
165 {
166 mTemporaryRubberBand.reset( new QgsRubberBand( mCanvas, operationResults->representativeGeometry().type() ) );
167 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
168 mTemporaryRubberBand->setWidth( scaleFactor );
169 mTemporaryRubberBand->setToGeometry( operationResults->representativeGeometry(), layer->crs() );
170 }
171 else
172 {
173 mTemporaryRubberBand.reset();
174 }
175 }
176 break;
177 }
178 }
179}
180
182{
183 QgsAnnotationLayer *layer = annotationLayerFromId( mSelectedItemLayerId );
184
186 context.setCurrentItemBounds( toLayerCoordinates( layer, mSelectedItemBounds ) );
187 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
188
189 switch ( mCurrentAction )
190 {
191 case Action::NoAction:
192 {
193 if ( event->button() != Qt::LeftButton )
194 return;
195
196 if ( mHoveredItemId.isEmpty() || !mHoverRubberBand )
197 {
198 clearSelectedItem();
199 }
200 else if ( mHoveredItemId == mSelectedItemId && mHoveredItemLayerId == mSelectedItemLayerId )
201 {
202 // press is on selected item => move that item
203 if ( layer )
204 {
205 const QgsPointXY mapPoint = event->mapPoint();
206 QgsRectangle searchRect = QgsRectangle( mapPoint.x(), mapPoint.y(), mapPoint.x(), mapPoint.y() );
207 searchRect.grow( searchRadiusMU( canvas() ) );
208
209 QgsAnnotationItemNode hoveredNode;
210 double currentNodeDistance = std::numeric_limits<double>::max();
211 mHoveredItemNodesSpatialIndex->intersects( searchRect, [&hoveredNode, &currentNodeDistance, &mapPoint, this]( int index ) -> bool {
212 const QgsAnnotationItemNode &thisNode = mHoveredItemNodes.at( index );
213 const double nodeDistance = thisNode.point().sqrDist( mapPoint );
214 if ( nodeDistance < currentNodeDistance )
215 {
216 hoveredNode = thisNode;
217 currentNodeDistance = nodeDistance;
218 }
219 return true;
220 } );
221
222 mMoveStartPointCanvasCrs = mapPoint;
223 mMoveStartPointPixels = event->pixelPoint();
224 mMoveStartPointLayerCrs = toLayerCoordinates( layer, mMoveStartPointCanvasCrs );
225 if ( mHoverRubberBand )
226 mHoverRubberBand->hide();
227 if ( mSelectedRubberBand )
228 mSelectedRubberBand->hide();
229
230 if ( hoveredNode.point().isEmpty() )
231 {
232 mCurrentAction = Action::MoveItem;
233 }
234 else
235 {
236 mCurrentAction = Action::MoveNode;
237 mTargetNode = hoveredNode;
238 }
239 }
240 }
241 else
242 {
243 // press is on a different item to selected item => select that item
244 mSelectedItemId = mHoveredItemId;
245 mSelectedItemLayerId = mHoveredItemLayerId;
246 mSelectedItemBounds = mHoveredItemBounds;
247
248 if ( !mSelectedRubberBand )
249 createSelectedItemBand();
250
251 mSelectedRubberBand->copyPointsFrom( mHoverRubberBand );
252 mSelectedRubberBand->show();
253
254 setCursor( Qt::OpenHandCursor );
255
256 emit itemSelected( annotationLayerFromId( mSelectedItemLayerId ), mSelectedItemId );
257 }
258 break;
259 }
260
261 case Action::MoveItem:
262 {
263 if ( event->button() == Qt::RightButton )
264 {
265 mCurrentAction = Action::NoAction;
266 mTemporaryRubberBand.reset();
267 if ( mSelectedRubberBand )
268 {
269 mSelectedRubberBand->setTranslationOffset( 0, 0 );
270 mSelectedRubberBand->show();
271 }
272 mHoveredItemNodeRubberBands.clear();
273 setCursor( Qt::ArrowCursor );
274 }
275 else if ( event->button() == Qt::LeftButton )
276 {
277 // apply move
278 if ( layer )
279 {
280 const QgsVector delta = toLayerCoordinates( layer, event->mapPoint() ) - mMoveStartPointLayerCrs;
281
283 operation( mSelectedItemId, delta.x(), delta.y(), event->pixelPoint().x() - mMoveStartPointPixels.x(), event->pixelPoint().y() - mMoveStartPointPixels.y() );
284 switch ( layer->applyEditV2( &operation, context ) )
285 {
288 mRefreshSelectedItemAfterRedraw = true;
289 break;
292 break;
293 }
294 }
295
296 mTemporaryRubberBand.reset();
297 mCurrentAction = Action::NoAction;
298 setCursor( Qt::ArrowCursor );
299 }
300 break;
301 }
302
303 case Action::MoveNode:
304 {
305 if ( event->button() == Qt::RightButton )
306 {
307 mCurrentAction = Action::NoAction;
308 mTemporaryRubberBand.reset();
309 mHoveredItemNodeRubberBands.clear();
310 mTemporaryRubberBand.reset();
311 setCursor( Qt::ArrowCursor );
312 }
313 else if ( event->button() == Qt::LeftButton )
314 {
315 if ( layer )
316 {
317 const QgsPointXY endPointLayer = toLayerCoordinates( layer, event->mapPoint() );
319 mSelectedItemId,
320 mTargetNode.id(),
321 QgsPoint( mTargetNode.point() ),
322 QgsPoint( endPointLayer ),
323 event->pixelPoint().x() - mMoveStartPointPixels.x(),
324 event->pixelPoint().y() - mMoveStartPointPixels.y()
325 );
326 switch ( layer->applyEditV2( &operation, context ) )
327 {
330 mRefreshSelectedItemAfterRedraw = true;
331 break;
332
335 break;
336 }
337 }
338
339 mTemporaryRubberBand.reset();
340 mHoveredItemNodeRubberBands.clear();
341 mHoveredItemNodes.clear();
342 mTemporaryRubberBand.reset();
343 mCurrentAction = Action::NoAction;
344 setCursor( Qt::ArrowCursor );
345 }
346 break;
347 }
348 }
349}
350
352{
353 switch ( mCurrentAction )
354 {
355 case Action::NoAction:
356 case Action::MoveItem:
357 {
358 if ( event->button() != Qt::LeftButton )
359 return;
360
361 mCurrentAction = Action::NoAction;
362 if ( mHoveredItemId == mSelectedItemId && mHoveredItemLayerId == mSelectedItemLayerId )
363 {
364 // double-click on selected item => add node
365 if ( QgsAnnotationLayer *layer = annotationLayerFromId( mSelectedItemLayerId ) )
366 {
367 const QgsPointXY layerPoint = toLayerCoordinates( layer, event->mapPoint() );
368 QgsAnnotationItemEditOperationAddNode operation( mSelectedItemId, QgsPoint( layerPoint ) );
370 context.setCurrentItemBounds( toLayerCoordinates( layer, mSelectedItemBounds ) );
371 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
372
373 switch ( layer->applyEditV2( &operation, context ) )
374 {
377 mRefreshSelectedItemAfterRedraw = true;
378 break;
379
382 break;
383 }
384 }
385 }
386 else
387 {
388 // press is on a different item to selected item => select that item
389 mSelectedItemId = mHoveredItemId;
390 mSelectedItemLayerId = mHoveredItemLayerId;
391 mSelectedItemBounds = mHoveredItemBounds;
392
393 if ( !mSelectedRubberBand )
394 createSelectedItemBand();
395
396 mSelectedRubberBand->copyPointsFrom( mHoverRubberBand );
397 mSelectedRubberBand->show();
398
399 setCursor( Qt::OpenHandCursor );
400
401 emit itemSelected( annotationLayerFromId( mSelectedItemLayerId ), mSelectedItemId );
402 }
403 break;
404 }
405
406 case Action::MoveNode:
407 break;
408 }
409}
410
412{
414 QgsAnnotationLayer *layer = annotationLayerFromId( mSelectedItemLayerId );
415 context.setCurrentItemBounds( toLayerCoordinates( layer, mSelectedItemBounds ) );
416 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
417
418 switch ( mCurrentAction )
419 {
420 case Action::NoAction:
421 {
422 if ( event->key() == Qt::Key_Backspace || event->key() == Qt::Key_Delete )
423 {
424 if ( !layer || mSelectedItemId.isEmpty() )
425 return;
426
427 layer->removeItem( mSelectedItemId );
428 clearSelectedItem();
429 clearHoveredItem();
430 event->ignore(); // disable default shortcut handling
431 }
432 else if ( event->key() == Qt::Key_Left || event->key() == Qt::Key_Right || event->key() == Qt::Key_Up || event->key() == Qt::Key_Down )
433 {
434 if ( !layer )
435 return;
436
437 const QSizeF deltaLayerCoordinates = deltaForKeyEvent( layer, mSelectedRubberBand->asGeometry().centroid().asPoint(), event );
438
439 QgsAnnotationItemEditOperationTranslateItem operation( mSelectedItemId, deltaLayerCoordinates.width(), deltaLayerCoordinates.height() );
440 switch ( layer->applyEditV2( &operation, context ) )
441 {
444 mRefreshSelectedItemAfterRedraw = true;
445 break;
448 break;
449 }
450 event->ignore(); // disable default shortcut handling (move map)
451 }
452 break;
453 }
454
455 case Action::MoveNode:
456 {
457 if ( event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace )
458 {
459 if ( layer )
460 {
461 QgsAnnotationItemEditOperationDeleteNode operation( mSelectedItemId, mTargetNode.id(), QgsPoint( mTargetNode.point() ) );
462 switch ( layer->applyEditV2( &operation, context ) )
463 {
466 mRefreshSelectedItemAfterRedraw = true;
467 break;
469 break;
472 break;
473 }
474 }
475
476 mTemporaryRubberBand.reset();
477 mHoveredItemNodeRubberBands.clear();
478 mHoveredItemNodes.clear();
479 mTemporaryRubberBand.reset();
480 mCurrentAction = Action::NoAction;
481 setCursor( Qt::ArrowCursor );
482 event->ignore(); // disable default shortcut handling (delete vector feature)
483 break;
484 }
485 [[fallthrough]];
486 }
487
488 case Action::MoveItem:
489 {
490 // warning -- fallthrough above!
491 if ( event->key() == Qt::Key_Escape )
492 {
493 mCurrentAction = Action::NoAction;
494 mTemporaryRubberBand.reset();
495 if ( mSelectedRubberBand )
496 {
497 mSelectedRubberBand->setTranslationOffset( 0, 0 );
498 mSelectedRubberBand->show();
499 }
500 mHoveredItemNodeRubberBands.clear();
501
502 setCursor( Qt::ArrowCursor );
503 }
504 break;
505 }
506 }
507}
508
509void QgsMapToolModifyAnnotation::onCanvasRefreshed()
510{
511 bool needsSelectedItemRefresh = mRefreshSelectedItemAfterRedraw;
512 if ( QgsAnnotationItem *item = annotationItemFromId( mSelectedItemLayerId, mSelectedItemId ) )
513 {
515 {
516 needsSelectedItemRefresh = true;
517 }
518 }
519
520 if ( needsSelectedItemRefresh )
521 {
522 const QgsRenderedItemResults *renderedItemResults = canvas()->renderedItemResults( false );
523 if ( !renderedItemResults )
524 {
525 return;
526 }
527
528 const QList<QgsRenderedItemDetails *> items = renderedItemResults->renderedItems();
529 auto it = std::find_if( items.begin(), items.end(), [this]( const QgsRenderedItemDetails *item ) {
530 if ( const QgsRenderedAnnotationItemDetails *annotationItem = dynamic_cast<const QgsRenderedAnnotationItemDetails *>( item ) )
531 {
532 if ( annotationItem->itemId() == mSelectedItemId && annotationItem->layerId() == mSelectedItemLayerId )
533 return true;
534 }
535 return false;
536 } );
537 if ( it != items.end() )
538 {
539 const QgsRectangle itemBounds = ( *it )->boundingBox();
540
541 setHoveredItem( dynamic_cast<const QgsRenderedAnnotationItemDetails *>( *it ), itemBounds );
542 if ( !mSelectedRubberBand )
543 createSelectedItemBand();
544
545 mSelectedRubberBand->copyPointsFrom( mHoverRubberBand );
546 mSelectedRubberBand->show();
547 mSelectedItemBounds = mHoveredItemBounds;
548 }
549 }
550 else
551 {
552 // recheck for hovered item at new mouse point
553 const QgsPointXY mapPoint = canvas()->mapSettings().mapToPixel().toMapCoordinates( mLastHoverPoint );
554 setHoveredItemFromPoint( mapPoint );
555 }
556 mRefreshSelectedItemAfterRedraw = false;
557}
558
559void QgsMapToolModifyAnnotation::setHoveredItem( const QgsRenderedAnnotationItemDetails *item, const QgsRectangle &itemMapBounds )
560{
561 mHoveredItemNodeRubberBands.clear();
562 if ( mHoveredNodeRubberBand )
563 mHoveredNodeRubberBand->hide();
564 mHoveredItemId = item->itemId();
565 mHoveredItemLayerId = item->layerId();
566 mHoveredItemBounds = itemMapBounds;
567 if ( !mHoverRubberBand )
568 createHoverBand();
569
570 mHoverRubberBand->show();
571
572 mHoverRubberBand->reset( Qgis::GeometryType::Line );
573 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMinimum(), itemMapBounds.yMinimum() ) );
574 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMaximum(), itemMapBounds.yMinimum() ) );
575 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMaximum(), itemMapBounds.yMaximum() ) );
576 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMinimum(), itemMapBounds.yMaximum() ) );
577 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMinimum(), itemMapBounds.yMinimum() ) );
578
579 QgsAnnotationLayer *layer = annotationLayerFromId( item->layerId() );
580 const QgsAnnotationItem *annotationItem = annotationItemFromId( item->layerId(), item->itemId() );
581 if ( !annotationItem )
582 return;
583
584 QgsCoordinateTransform layerToMapTransform = QgsCoordinateTransform( layer->crs(), canvas()->mapSettings().destinationCrs(), canvas()->mapSettings().transformContext() );
585
586 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
587
588 QgsAnnotationItemEditContext context;
589 context.setCurrentItemBounds( toLayerCoordinates( layer, itemMapBounds ) );
590 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
591
592 const QList<QgsAnnotationItemNode> itemNodes = annotationItem->nodesV2( context );
593 QgsRubberBand *vertexNodeBand = new QgsRubberBand( mCanvas, Qgis::GeometryType::Point );
594
595 vertexNodeBand->setIcon( QgsRubberBand::ICON_BOX );
596 vertexNodeBand->setWidth( scaleFactor );
597 vertexNodeBand->setIconSize( scaleFactor * 5 );
598 vertexNodeBand->setColor( QColor( 200, 0, 120, 255 ) );
599
600 QgsRubberBand *calloutNodeBand = new QgsRubberBand( mCanvas, Qgis::GeometryType::Point );
601 calloutNodeBand->setWidth( scaleFactor );
602 calloutNodeBand->setSecondaryStrokeColor( QColor( 255, 255, 255, 100 ) );
603 calloutNodeBand->setColor( QColor( 120, 200, 0, 255 ) );
604 calloutNodeBand->setIcon( QgsRubberBand::ICON_X );
605 calloutNodeBand->setIconSize( scaleFactor * 5 );
606
607 // store item nodes in a spatial index for quick searching
608 mHoveredItemNodesSpatialIndex = std::make_unique<QgsAnnotationItemNodesSpatialIndex>();
609 int index = 0;
610 mHoveredItemNodes.clear();
611 mHoveredItemNodes.reserve( itemNodes.size() );
612 for ( const QgsAnnotationItemNode &node : itemNodes )
613 {
614 QgsPointXY nodeMapPoint;
615 try
616 {
617 nodeMapPoint = layerToMapTransform.transform( node.point() );
618 }
619 catch ( QgsCsException & )
620 {
621 continue;
622 }
623
624 switch ( node.type() )
625 {
627 vertexNodeBand->addPoint( nodeMapPoint );
628 break;
629
631 calloutNodeBand->addPoint( nodeMapPoint );
632 break;
633 }
634
635 mHoveredItemNodesSpatialIndex->insert( index, QgsRectangle( nodeMapPoint.x(), nodeMapPoint.y(), nodeMapPoint.x(), nodeMapPoint.y() ) );
636
637 QgsAnnotationItemNode transformedNode = node;
638 transformedNode.setPoint( nodeMapPoint );
639 mHoveredItemNodes.append( transformedNode );
640
641 index++;
642 }
643
644 mHoveredItemNodeRubberBands.emplace_back( vertexNodeBand );
645 mHoveredItemNodeRubberBands.emplace_back( calloutNodeBand );
646}
647
648QSizeF QgsMapToolModifyAnnotation::deltaForKeyEvent( QgsAnnotationLayer *layer, const QgsPointXY &originalCanvasPoint, QKeyEvent *event )
649{
650 const double canvasDpi = canvas()->window()->windowHandle()->screen()->physicalDotsPerInch();
651
652 // increment used for cursor key item movement
653 double incrementPixels = 0.0;
654 if ( event->modifiers() & Qt::ShiftModifier )
655 {
656 //holding shift while pressing cursor keys results in a big step - 20 mm
657 incrementPixels = 20.0 / 25.4 * canvasDpi;
658 }
659 else if ( event->modifiers() & Qt::AltModifier )
660 {
661 //holding alt while pressing cursor keys results in a 1 pixel step
662 incrementPixels = 1;
663 }
664 else
665 {
666 // 5 mm
667 incrementPixels = 5.0 / 25.4 * canvasDpi;
668 }
669
670 double deltaXPixels = 0;
671 double deltaYPixels = 0;
672 switch ( event->key() )
673 {
674 case Qt::Key_Left:
675 deltaXPixels = -incrementPixels;
676 break;
677 case Qt::Key_Right:
678 deltaXPixels = incrementPixels;
679 break;
680 case Qt::Key_Up:
681 deltaYPixels = -incrementPixels;
682 break;
683 case Qt::Key_Down:
684 deltaYPixels = incrementPixels;
685 break;
686 default:
687 break;
688 }
689
690 const QgsPointXY beforeMoveMapPoint = canvas()->getCoordinateTransform()->toMapCoordinates( originalCanvasPoint.x(), originalCanvasPoint.y() );
691 const QgsPointXY beforeMoveLayerPoint = toLayerCoordinates( layer, beforeMoveMapPoint );
692
693 const QgsPointXY afterMoveCanvasPoint( originalCanvasPoint.x() + deltaXPixels, originalCanvasPoint.y() + deltaYPixels );
694 const QgsPointXY afterMoveMapPoint = canvas()->getCoordinateTransform()->toMapCoordinates( afterMoveCanvasPoint.x(), afterMoveCanvasPoint.y() );
695 const QgsPointXY afterMoveLayerPoint = toLayerCoordinates( layer, afterMoveMapPoint );
696
697 return QSizeF( afterMoveLayerPoint.x() - beforeMoveLayerPoint.x(), afterMoveLayerPoint.y() - beforeMoveLayerPoint.y() );
698}
699
700void QgsMapToolModifyAnnotation::setHoveredItemFromPoint( const QgsPointXY &mapPoint )
701{
702 QgsRectangle searchRect = QgsRectangle( mapPoint.x(), mapPoint.y(), mapPoint.x(), mapPoint.y() );
703 searchRect.grow( searchRadiusMU( canvas() ) );
704
705 const QgsRenderedItemResults *renderedItemResults = canvas()->renderedItemResults( false );
706 if ( !renderedItemResults )
707 {
708 clearHoveredItem();
709 return;
710 }
711
712 const QList<const QgsRenderedAnnotationItemDetails *> items = renderedItemResults->renderedAnnotationItemsInBounds( searchRect );
713 if ( items.empty() )
714 {
715 clearHoveredItem();
716 return;
717 }
718
719 // find closest item
720 QgsRectangle itemBounds;
721 const QgsRenderedAnnotationItemDetails *closestItem = findClosestItemToPoint( mapPoint, items, itemBounds );
722 if ( !closestItem )
723 {
724 clearHoveredItem();
725 return;
726 }
727
728 if ( closestItem->itemId() != mHoveredItemId || closestItem->layerId() != mHoveredItemLayerId )
729 {
730 setHoveredItem( closestItem, itemBounds );
731 }
732
733 // track hovered node too!... here we want to identify the closest node to the cursor position
734 QgsAnnotationItemNode hoveredNode;
735 if ( closestItem->itemId() == mSelectedItemId && closestItem->layerId() == mSelectedItemLayerId )
736 {
737 double currentNodeDistance = std::numeric_limits<double>::max();
738 mHoveredItemNodesSpatialIndex->intersects( searchRect, [&hoveredNode, &currentNodeDistance, &mapPoint, this]( int index ) -> bool {
739 if ( index >= mHoveredItemNodes.size() )
740 return false;
741
742 const QgsAnnotationItemNode &thisNode = mHoveredItemNodes.at( index );
743 const double nodeDistance = thisNode.point().sqrDist( mapPoint );
744 if ( nodeDistance < currentNodeDistance )
745 {
746 hoveredNode = thisNode;
747 currentNodeDistance = nodeDistance;
748 }
749 return true;
750 } );
751 }
752
753 if ( hoveredNode.point().isEmpty() )
754 {
755 // no hovered node
756 if ( mHoveredNodeRubberBand )
757 mHoveredNodeRubberBand->hide();
758 setCursor( mHoveredItemId == mSelectedItemId && mHoveredItemLayerId == mSelectedItemLayerId ? Qt::OpenHandCursor : Qt::ArrowCursor );
759 }
760 else
761 {
762 if ( !mHoveredNodeRubberBand )
763 createHoveredNodeBand();
764
765 mHoveredNodeRubberBand->reset( Qgis::GeometryType::Point );
766 mHoveredNodeRubberBand->addPoint( hoveredNode.point() );
767 mHoveredNodeRubberBand->show();
768
769 setCursor( hoveredNode.cursor() );
770 }
771}
772
773void QgsMapToolModifyAnnotation::clearHoveredItem()
774{
775 if ( mHoverRubberBand )
776 mHoverRubberBand->hide();
777 if ( mHoveredNodeRubberBand )
778 mHoveredNodeRubberBand->hide();
779
780 mHoveredItemId.clear();
781 mHoveredItemLayerId.clear();
782 mHoveredItemNodeRubberBands.clear();
783 mHoveredItemNodesSpatialIndex.reset();
784
785 setCursor( Qt::ArrowCursor );
786}
787
788void QgsMapToolModifyAnnotation::clearSelectedItem()
789{
790 if ( mSelectedRubberBand )
791 mSelectedRubberBand->hide();
792
793 const bool hadSelection = !mSelectedItemId.isEmpty();
794 mSelectedItemId.clear();
795 mSelectedItemLayerId.clear();
796 if ( hadSelection )
797 emit selectionCleared();
798}
799
800void QgsMapToolModifyAnnotation::createHoverBand()
801{
802 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
803
804 mHoverRubberBand.reset( new QgsRubberBand( mCanvas, Qgis::GeometryType::Line ) );
805 mHoverRubberBand->setWidth( scaleFactor );
806 mHoverRubberBand->setSecondaryStrokeColor( QColor( 255, 255, 255, 100 ) );
807 mHoverRubberBand->setColor( QColor( 100, 100, 100, 155 ) );
808}
809
810void QgsMapToolModifyAnnotation::createHoveredNodeBand()
811{
812 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
813
814 mHoveredNodeRubberBand.reset( new QgsRubberBand( mCanvas, Qgis::GeometryType::Point ) );
815 mHoveredNodeRubberBand->setIcon( QgsRubberBand::ICON_FULL_BOX );
816 mHoveredNodeRubberBand->setWidth( scaleFactor );
817 mHoveredNodeRubberBand->setIconSize( scaleFactor * 5 );
818 mHoveredNodeRubberBand->setColor( QColor( 200, 0, 120, 255 ) );
819}
820
821void QgsMapToolModifyAnnotation::createSelectedItemBand()
822{
823 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
824
825 mSelectedRubberBand.reset( new QgsRubberBand( mCanvas, Qgis::GeometryType::Line ) );
826 mSelectedRubberBand->setWidth( scaleFactor );
827 mSelectedRubberBand->setSecondaryStrokeColor( QColor( 255, 255, 255, 100 ) );
828 mSelectedRubberBand->setColor( QColor( 50, 50, 50, 200 ) );
829}
@ VertexHandle
Node is a handle for manipulating vertices.
Definition qgis.h:2597
@ CalloutHandle
Node is a handle for manipulating callouts.
Definition qgis.h:2598
@ ScaleDependentBoundingBox
Item's bounding box will vary depending on map scale.
Definition qgis.h:2554
@ Invalid
Operation has invalid parameters for the item, no change occurred.
Definition qgis.h:2610
@ Success
Item was modified successfully.
Definition qgis.h:2609
@ ItemCleared
The operation results in the item being cleared, and the item should be removed from the layer as a r...
Definition qgis.h:2611
@ Point
Points.
Definition qgis.h:380
@ Line
Lines.
Definition qgis.h:381
A dockable widget used to handle the CAD tools on top of a selection of map tools.
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.
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.
QgsAnnotationLayer * annotationLayerFromId(const QString &layerId)
Returns the annotation layer matching a given ID.
QgsAnnotationMapTool(QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWidget *cadDockWidget)
Constructor for QgsAnnotationMapTool.
const QgsRenderedAnnotationItemDetails * findClosestItemToPoint(const QgsPointXY &mapPoint, const QList< const QgsRenderedAnnotationItemDetails * > &items, QgsRectangle &bounds)
Returns the closest item from a list of annotation items to a given map point.
QgsAnnotationItem * annotationItemFromId(const QString &layerId, const QString &itemId)
Returns the annotation item matching a given pair of layer and item IDs.
QgsPointXY transform(const QgsPointXY &point, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Transform the point from the source CRS to the destination CRS.
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.
A mouse event which is the result of a user interaction with 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.
void deactivate() override
Unregisters this maptool from the cad dock widget.
virtual QgsMapLayer * layer() const
Returns the layer associated with the map tool.
QgsAdvancedDigitizingDockWidget * cadDockWidget() const
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.
QgsMapLayer * layer(const QString &id)
Returns the map layer with the matching ID, or nullptr if no layers could be found.
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:369
static double searchRadiusMU(const QgsRenderContext &context)
Gets search radius in map units for given context.
Represents a 2D point.
Definition qgspointxy.h:62
double sqrDist(double x, double y) const
Returns the squared distance between this point a specified x, y coordinate.
Definition qgspointxy.h:189
double y
Definition qgspointxy.h:66
double x
Definition qgspointxy.h:65
bool isEmpty() const
Returns true if the geometry is empty.
Definition qgspointxy.h:245
Point geometry type, with support for z-dimension and m-values.
Definition qgspoint.h:53
static QgsProject * instance()
Returns the QgsProject singleton instance.
void setDirty(bool b=true)
Flag the project as dirty (modified).
A rectangle specified with double values.
double xMinimum
double yMinimum
double xMaximum
void grow(double delta)
Grows the rectangle in place by the specified amount.
double yMaximum
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.
QString layerId() const
Returns the layer ID of the associated map layer.
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.
Responsible for drawing transient features (e.g.
void setIconSize(double iconSize)
Sets the size of the point icons.
void setWidth(double width)
Sets the width of the line.
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 setIcon(IconType icon)
Sets the icon type to highlight point geometries.
void addPoint(const QgsPointXY &p, bool doUpdate=true, int geometryIndex=0, int ringIndex=0)
Adds a vertex to the rubberband and update canvas.
Shows a snapping marker on map canvas for the current snapping match.
Represent a 2-dimensional vector.
Definition qgsvector.h:34
double y() const
Returns the vector's y-component.
Definition qgsvector.h:155
double x() const
Returns the vector's x-component.
Definition qgsvector.h:146