QGIS API Documentation 3.99.0-Master (26c88405ac0)
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 "qgsmapmouseevent.h"
26#include "qgsproject.h"
30#include "qgsrubberband.h"
31#include "qgssnapindicator.h"
32
33#include <QScreen>
34#include <QTransform>
35#include <QWindow>
36
37#include "moc_qgsmaptoolmodifyannotation.cpp"
38
40class QgsAnnotationItemNodesSpatialIndex : public RTree<int, float, 2, float>
41{
42 public:
43 void insert( int index, const QgsRectangle &bounds )
44 {
45 std::array<float, 4> scaledBounds = scaleBounds( bounds );
46 const float aMin[2] {
47 scaledBounds[0], scaledBounds[1]
48 };
49 const float aMax[2] {
50 scaledBounds[2], scaledBounds[3]
51 };
52 this->Insert(
53 aMin,
54 aMax,
55 index
56 );
57 }
58
65 void remove( int index, const QgsRectangle &bounds )
66 {
67 std::array<float, 4> scaledBounds = scaleBounds( bounds );
68 const float aMin[2] {
69 scaledBounds[0], scaledBounds[1]
70 };
71 const float aMax[2] {
72 scaledBounds[2], scaledBounds[3]
73 };
74 this->Remove(
75 aMin,
76 aMax,
77 index
78 );
79 }
80
86 bool intersects( const QgsRectangle &bounds, const std::function<bool( int index )> &callback ) const
87 {
88 std::array<float, 4> scaledBounds = scaleBounds( bounds );
89 const float aMin[2] {
90 scaledBounds[0], scaledBounds[1]
91 };
92 const float aMax[2] {
93 scaledBounds[2], scaledBounds[3]
94 };
95 this->Search(
96 aMin,
97 aMax,
98 callback
99 );
100 return true;
101 }
102
103 private:
104 std::array<float, 4> scaleBounds( const QgsRectangle &bounds ) const
105 {
106 return {
107 static_cast<float>( bounds.xMinimum() ),
108 static_cast<float>( bounds.yMinimum() ),
109 static_cast<float>( bounds.xMaximum() ),
110 static_cast<float>( bounds.yMaximum() )
111 };
112 }
113};
115
116
123
125
127{
128 mSnapIndicator->setMatch( QgsPointLocator::Match() );
129
130 clearHoveredItem();
131 clearSelectedItem();
133}
134
136{
137 mLastHoverPoint = event->originalPixelPoint();
138 event->snapPoint();
139 mSnapIndicator->setMatch( event->mapPointMatch() );
140
141 const QgsPointXY mapPoint = event->mapPoint();
142
144 QgsAnnotationLayer *layer = annotationLayerFromId( mSelectedItemLayerId );
145 context.setCurrentItemBounds( toLayerCoordinates( layer, mSelectedItemBounds ) );
146 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
147
148 switch ( mCurrentAction )
149 {
150 case Action::NoAction:
151 {
152 setHoveredItemFromPoint( mapPoint );
153 break;
154 }
155
156 case Action::MoveItem:
157 {
158 if ( QgsAnnotationItem *item = annotationItemFromId( mSelectedItemLayerId, mSelectedItemId ) )
159 {
160 const QgsVector delta = toLayerCoordinates( layer, event->mapPoint() ) - mMoveStartPointLayerCrs;
161
162 QgsAnnotationItemEditOperationTranslateItem operation( mSelectedItemId, delta.x(), delta.y(), event->pixelPoint().x() - mMoveStartPointPixels.x(), event->pixelPoint().y() - mMoveStartPointPixels.y() );
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 case Action::MoveNode:
180 {
181 if ( QgsAnnotationItem *item = annotationItemFromId( mSelectedItemLayerId, mSelectedItemId ) )
182 {
183 const QgsPointXY endPointLayer = toLayerCoordinates( layer, event->mapPoint() );
184 QgsAnnotationItemEditOperationMoveNode operation( mSelectedItemId, mTargetNode.id(), QgsPoint( mTargetNode.point() ), QgsPoint( endPointLayer ), event->pixelPoint().x() - mMoveStartPointPixels.x(), event->pixelPoint().y() - mMoveStartPointPixels.y() );
185 std::unique_ptr<QgsAnnotationItemEditOperationTransientResults> operationResults( item->transientEditResultsV2( &operation, context ) );
186 if ( operationResults )
187 {
188 mTemporaryRubberBand.reset( new QgsRubberBand( mCanvas, operationResults->representativeGeometry().type() ) );
189 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
190 mTemporaryRubberBand->setWidth( scaleFactor );
191 mTemporaryRubberBand->setToGeometry( operationResults->representativeGeometry(), layer->crs() );
192 }
193 else
194 {
195 mTemporaryRubberBand.reset();
196 }
197 }
198 break;
199 }
200 }
201}
202
204{
205 QgsAnnotationLayer *layer = annotationLayerFromId( mSelectedItemLayerId );
206
208 context.setCurrentItemBounds( toLayerCoordinates( layer, mSelectedItemBounds ) );
209 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
210
211 switch ( mCurrentAction )
212 {
213 case Action::NoAction:
214 {
215 if ( event->button() != Qt::LeftButton )
216 return;
217
218 if ( mHoveredItemId.isEmpty() || !mHoverRubberBand )
219 {
220 clearSelectedItem();
221 }
222 else if ( mHoveredItemId == mSelectedItemId && mHoveredItemLayerId == mSelectedItemLayerId )
223 {
224 // press is on selected item => move that item
225 if ( layer )
226 {
227 const QgsPointXY mapPoint = event->mapPoint();
228 QgsRectangle searchRect = QgsRectangle( mapPoint.x(), mapPoint.y(), mapPoint.x(), mapPoint.y() );
229 searchRect.grow( searchRadiusMU( canvas() ) );
230
231 QgsAnnotationItemNode hoveredNode;
232 double currentNodeDistance = std::numeric_limits<double>::max();
233 mHoveredItemNodesSpatialIndex->intersects( searchRect, [&hoveredNode, &currentNodeDistance, &mapPoint, this]( int index ) -> bool {
234 const QgsAnnotationItemNode &thisNode = mHoveredItemNodes.at( index );
235 const double nodeDistance = thisNode.point().sqrDist( mapPoint );
236 if ( nodeDistance < currentNodeDistance )
237 {
238 hoveredNode = thisNode;
239 currentNodeDistance = nodeDistance;
240 }
241 return true;
242 } );
243
244 mMoveStartPointCanvasCrs = mapPoint;
245 mMoveStartPointPixels = event->pixelPoint();
246 mMoveStartPointLayerCrs = toLayerCoordinates( layer, mMoveStartPointCanvasCrs );
247 if ( mHoverRubberBand )
248 mHoverRubberBand->hide();
249 if ( mSelectedRubberBand )
250 mSelectedRubberBand->hide();
251
252 if ( hoveredNode.point().isEmpty() )
253 {
254 mCurrentAction = Action::MoveItem;
255 }
256 else
257 {
258 mCurrentAction = Action::MoveNode;
259 mTargetNode = hoveredNode;
260 }
261 }
262 }
263 else
264 {
265 // press is on a different item to selected item => select that item
266 mSelectedItemId = mHoveredItemId;
267 mSelectedItemLayerId = mHoveredItemLayerId;
268 mSelectedItemBounds = mHoveredItemBounds;
269
270 if ( !mSelectedRubberBand )
271 createSelectedItemBand();
272
273 mSelectedRubberBand->copyPointsFrom( mHoverRubberBand );
274 mSelectedRubberBand->show();
275
276 setCursor( Qt::OpenHandCursor );
277
278 emit itemSelected( annotationLayerFromId( mSelectedItemLayerId ), mSelectedItemId );
279 }
280 break;
281 }
282
283 case Action::MoveItem:
284 {
285 if ( event->button() == Qt::RightButton )
286 {
287 mCurrentAction = Action::NoAction;
288 mTemporaryRubberBand.reset();
289 if ( mSelectedRubberBand )
290 {
291 mSelectedRubberBand->setTranslationOffset( 0, 0 );
292 mSelectedRubberBand->show();
293 }
294 mHoveredItemNodeRubberBands.clear();
295 setCursor( Qt::ArrowCursor );
296 }
297 else if ( event->button() == Qt::LeftButton )
298 {
299 // apply move
300 if ( layer )
301 {
302 const QgsVector delta = toLayerCoordinates( layer, event->mapPoint() ) - mMoveStartPointLayerCrs;
303
304 QgsAnnotationItemEditOperationTranslateItem operation( mSelectedItemId, delta.x(), delta.y(), event->pixelPoint().x() - mMoveStartPointPixels.x(), event->pixelPoint().y() - mMoveStartPointPixels.y() );
305 switch ( layer->applyEditV2( &operation, context ) )
306 {
309 mRefreshSelectedItemAfterRedraw = true;
310 break;
313 break;
314 }
315 }
316
317 mTemporaryRubberBand.reset();
318 mCurrentAction = Action::NoAction;
319 setCursor( Qt::ArrowCursor );
320 }
321 break;
322 }
323
324 case Action::MoveNode:
325 {
326 if ( event->button() == Qt::RightButton )
327 {
328 mCurrentAction = Action::NoAction;
329 mTemporaryRubberBand.reset();
330 mHoveredItemNodeRubberBands.clear();
331 mTemporaryRubberBand.reset();
332 setCursor( Qt::ArrowCursor );
333 }
334 else if ( event->button() == Qt::LeftButton )
335 {
336 if ( layer )
337 {
338 const QgsPointXY endPointLayer = toLayerCoordinates( layer, event->mapPoint() );
339 QgsAnnotationItemEditOperationMoveNode operation( mSelectedItemId, mTargetNode.id(), QgsPoint( mTargetNode.point() ), QgsPoint( endPointLayer ), event->pixelPoint().x() - mMoveStartPointPixels.x(), 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 if ( !layer || mSelectedItemId.isEmpty() )
439 return;
440
441 layer->removeItem( mSelectedItemId );
442 clearSelectedItem();
443 clearHoveredItem();
444 event->ignore(); // disable default shortcut handling
445 }
446 else if ( event->key() == Qt::Key_Left
447 || event->key() == Qt::Key_Right
448 || event->key() == Qt::Key_Up
449 || event->key() == Qt::Key_Down )
450 {
451 if ( !layer )
452 return;
453
454 const QSizeF deltaLayerCoordinates = deltaForKeyEvent( layer, mSelectedRubberBand->asGeometry().centroid().asPoint(), event );
455
456 QgsAnnotationItemEditOperationTranslateItem operation( mSelectedItemId, deltaLayerCoordinates.width(), deltaLayerCoordinates.height() );
457 switch ( layer->applyEditV2( &operation, context ) )
458 {
461 mRefreshSelectedItemAfterRedraw = true;
462 break;
465 break;
466 }
467 event->ignore(); // disable default shortcut handling (move map)
468 }
469 break;
470 }
471
472 case Action::MoveNode:
473 {
474 if ( event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace )
475 {
476 if ( layer )
477 {
478 QgsAnnotationItemEditOperationDeleteNode operation( mSelectedItemId, mTargetNode.id(), QgsPoint( mTargetNode.point() ) );
479 switch ( layer->applyEditV2( &operation, context ) )
480 {
483 mRefreshSelectedItemAfterRedraw = true;
484 break;
486 break;
489 break;
490 }
491 }
492
493 mTemporaryRubberBand.reset();
494 mHoveredItemNodeRubberBands.clear();
495 mHoveredItemNodes.clear();
496 mTemporaryRubberBand.reset();
497 mCurrentAction = Action::NoAction;
498 setCursor( Qt::ArrowCursor );
499 event->ignore(); // disable default shortcut handling (delete vector feature)
500 break;
501 }
502 [[fallthrough]];
503 }
504
505 case Action::MoveItem:
506 {
507 // warning -- fallthrough above!
508 if ( event->key() == Qt::Key_Escape )
509 {
510 mCurrentAction = Action::NoAction;
511 mTemporaryRubberBand.reset();
512 if ( mSelectedRubberBand )
513 {
514 mSelectedRubberBand->setTranslationOffset( 0, 0 );
515 mSelectedRubberBand->show();
516 }
517 mHoveredItemNodeRubberBands.clear();
518
519 setCursor( Qt::ArrowCursor );
520 }
521 break;
522 }
523 }
524}
525
526void QgsMapToolModifyAnnotation::onCanvasRefreshed()
527{
528 bool needsSelectedItemRefresh = mRefreshSelectedItemAfterRedraw;
529 if ( QgsAnnotationItem *item = annotationItemFromId( mSelectedItemLayerId, mSelectedItemId ) )
530 {
532 {
533 needsSelectedItemRefresh = true;
534 }
535 }
536
537 if ( needsSelectedItemRefresh )
538 {
539 const QgsRenderedItemResults *renderedItemResults = canvas()->renderedItemResults( false );
540 if ( !renderedItemResults )
541 {
542 return;
543 }
544
545 const QList<QgsRenderedItemDetails *> items = renderedItemResults->renderedItems();
546 auto it = std::find_if( items.begin(), items.end(), [this]( const QgsRenderedItemDetails *item ) {
547 if ( const QgsRenderedAnnotationItemDetails *annotationItem = dynamic_cast<const QgsRenderedAnnotationItemDetails *>( item ) )
548 {
549 if ( annotationItem->itemId() == mSelectedItemId && annotationItem->layerId() == mSelectedItemLayerId )
550 return true;
551 }
552 return false;
553 } );
554 if ( it != items.end() )
555 {
556 const QgsRectangle itemBounds = ( *it )->boundingBox();
557
558 setHoveredItem( dynamic_cast<const QgsRenderedAnnotationItemDetails *>( *it ), itemBounds );
559 if ( !mSelectedRubberBand )
560 createSelectedItemBand();
561
562 mSelectedRubberBand->copyPointsFrom( mHoverRubberBand );
563 mSelectedRubberBand->show();
564 mSelectedItemBounds = mHoveredItemBounds;
565 }
566 }
567 else
568 {
569 // recheck for hovered item at new mouse point
570 const QgsPointXY mapPoint = canvas()->mapSettings().mapToPixel().toMapCoordinates( mLastHoverPoint );
571 setHoveredItemFromPoint( mapPoint );
572 }
573 mRefreshSelectedItemAfterRedraw = false;
574}
575
576void QgsMapToolModifyAnnotation::setHoveredItem( const QgsRenderedAnnotationItemDetails *item, const QgsRectangle &itemMapBounds )
577{
578 mHoveredItemNodeRubberBands.clear();
579 if ( mHoveredNodeRubberBand )
580 mHoveredNodeRubberBand->hide();
581 mHoveredItemId = item->itemId();
582 mHoveredItemLayerId = item->layerId();
583 mHoveredItemBounds = itemMapBounds;
584 if ( !mHoverRubberBand )
585 createHoverBand();
586
587 mHoverRubberBand->show();
588
589 mHoverRubberBand->reset( Qgis::GeometryType::Line );
590 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMinimum(), itemMapBounds.yMinimum() ) );
591 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMaximum(), itemMapBounds.yMinimum() ) );
592 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMaximum(), itemMapBounds.yMaximum() ) );
593 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMinimum(), itemMapBounds.yMaximum() ) );
594 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMinimum(), itemMapBounds.yMinimum() ) );
595
596 QgsAnnotationLayer *layer = annotationLayerFromId( item->layerId() );
597 const QgsAnnotationItem *annotationItem = annotationItemFromId( item->layerId(), item->itemId() );
598 if ( !annotationItem )
599 return;
600
601 QgsCoordinateTransform layerToMapTransform = QgsCoordinateTransform( layer->crs(), canvas()->mapSettings().destinationCrs(), canvas()->mapSettings().transformContext() );
602
603 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
604
605 QgsAnnotationItemEditContext context;
606 context.setCurrentItemBounds( toLayerCoordinates( layer, itemMapBounds ) );
607 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
608
609 const QList<QgsAnnotationItemNode> itemNodes = annotationItem->nodesV2( context );
610 QgsRubberBand *vertexNodeBand = new QgsRubberBand( mCanvas, Qgis::GeometryType::Point );
611
612 vertexNodeBand->setIcon( QgsRubberBand::ICON_BOX );
613 vertexNodeBand->setWidth( scaleFactor );
614 vertexNodeBand->setIconSize( scaleFactor * 5 );
615 vertexNodeBand->setColor( QColor( 200, 0, 120, 255 ) );
616
617 QgsRubberBand *calloutNodeBand = new QgsRubberBand( mCanvas, Qgis::GeometryType::Point );
618 calloutNodeBand->setWidth( scaleFactor );
619 calloutNodeBand->setSecondaryStrokeColor( QColor( 255, 255, 255, 100 ) );
620 calloutNodeBand->setColor( QColor( 120, 200, 0, 255 ) );
621 calloutNodeBand->setIcon( QgsRubberBand::ICON_X );
622 calloutNodeBand->setIconSize( scaleFactor * 5 );
623
624 // store item nodes in a spatial index for quick searching
625 mHoveredItemNodesSpatialIndex = std::make_unique<QgsAnnotationItemNodesSpatialIndex>();
626 int index = 0;
627 mHoveredItemNodes.clear();
628 mHoveredItemNodes.reserve( itemNodes.size() );
629 for ( const QgsAnnotationItemNode &node : itemNodes )
630 {
631 QgsPointXY nodeMapPoint;
632 try
633 {
634 nodeMapPoint = layerToMapTransform.transform( node.point() );
635 }
636 catch ( QgsCsException & )
637 {
638 continue;
639 }
640
641 switch ( node.type() )
642 {
644 vertexNodeBand->addPoint( nodeMapPoint );
645 break;
646
648 calloutNodeBand->addPoint( nodeMapPoint );
649 break;
650 }
651
652 mHoveredItemNodesSpatialIndex->insert( index, QgsRectangle( nodeMapPoint.x(), nodeMapPoint.y(), nodeMapPoint.x(), nodeMapPoint.y() ) );
653
654 QgsAnnotationItemNode transformedNode = node;
655 transformedNode.setPoint( nodeMapPoint );
656 mHoveredItemNodes.append( transformedNode );
657
658 index++;
659 }
660
661 mHoveredItemNodeRubberBands.emplace_back( vertexNodeBand );
662 mHoveredItemNodeRubberBands.emplace_back( calloutNodeBand );
663}
664
665QSizeF QgsMapToolModifyAnnotation::deltaForKeyEvent( QgsAnnotationLayer *layer, const QgsPointXY &originalCanvasPoint, QKeyEvent *event )
666{
667 const double canvasDpi = canvas()->window()->windowHandle()->screen()->physicalDotsPerInch();
668
669 // increment used for cursor key item movement
670 double incrementPixels = 0.0;
671 if ( event->modifiers() & Qt::ShiftModifier )
672 {
673 //holding shift while pressing cursor keys results in a big step - 20 mm
674 incrementPixels = 20.0 / 25.4 * canvasDpi;
675 }
676 else if ( event->modifiers() & Qt::AltModifier )
677 {
678 //holding alt while pressing cursor keys results in a 1 pixel step
679 incrementPixels = 1;
680 }
681 else
682 {
683 // 5 mm
684 incrementPixels = 5.0 / 25.4 * canvasDpi;
685 }
686
687 double deltaXPixels = 0;
688 double deltaYPixels = 0;
689 switch ( event->key() )
690 {
691 case Qt::Key_Left:
692 deltaXPixels = -incrementPixels;
693 break;
694 case Qt::Key_Right:
695 deltaXPixels = incrementPixels;
696 break;
697 case Qt::Key_Up:
698 deltaYPixels = -incrementPixels;
699 break;
700 case Qt::Key_Down:
701 deltaYPixels = incrementPixels;
702 break;
703 default:
704 break;
705 }
706
707 const QgsPointXY beforeMoveMapPoint = canvas()->getCoordinateTransform()->toMapCoordinates( originalCanvasPoint.x(), originalCanvasPoint.y() );
708 const QgsPointXY beforeMoveLayerPoint = toLayerCoordinates( layer, beforeMoveMapPoint );
709
710 const QgsPointXY afterMoveCanvasPoint( originalCanvasPoint.x() + deltaXPixels, originalCanvasPoint.y() + deltaYPixels );
711 const QgsPointXY afterMoveMapPoint = canvas()->getCoordinateTransform()->toMapCoordinates( afterMoveCanvasPoint.x(), afterMoveCanvasPoint.y() );
712 const QgsPointXY afterMoveLayerPoint = toLayerCoordinates( layer, afterMoveMapPoint );
713
714 return QSizeF( afterMoveLayerPoint.x() - beforeMoveLayerPoint.x(), afterMoveLayerPoint.y() - beforeMoveLayerPoint.y() );
715}
716
717const QgsRenderedAnnotationItemDetails *QgsMapToolModifyAnnotation::findClosestItemToPoint( const QgsPointXY &mapPoint, const QList<const QgsRenderedAnnotationItemDetails *> &items, QgsRectangle &bounds )
718{
719 const QgsRenderedAnnotationItemDetails *closestItem = nullptr;
720 double closestItemDistance = std::numeric_limits<double>::max();
721 double closestItemArea = std::numeric_limits<double>::max();
722
723 for ( const QgsRenderedAnnotationItemDetails *item : items )
724 {
725 const QgsAnnotationItem *annotationItem = annotationItemFromId( item->layerId(), item->itemId() );
726 if ( !annotationItem )
727 continue;
728
729 const QgsRectangle itemBounds = item->boundingBox();
730 const double itemDistance = itemBounds.contains( mapPoint ) ? 0 : itemBounds.distance( mapPoint );
731 if ( !closestItem || itemDistance < closestItemDistance || ( itemDistance == closestItemDistance && itemBounds.area() < closestItemArea ) )
732 {
733 closestItem = item;
734 closestItemDistance = itemDistance;
735 closestItemArea = itemBounds.area();
736 bounds = itemBounds;
737 }
738 }
739 return closestItem;
740}
741
742QgsAnnotationLayer *QgsMapToolModifyAnnotation::annotationLayerFromId( const QString &layerId )
743{
744 QgsAnnotationLayer *layer = qobject_cast<QgsAnnotationLayer *>( QgsProject::instance()->mapLayer( layerId ) );
745 if ( !layer && layerId == QgsProject::instance()->mainAnnotationLayer()->id() )
747 return layer;
748}
749
750QgsAnnotationItem *QgsMapToolModifyAnnotation::annotationItemFromId( const QString &layerId, const QString &itemId )
751{
752 QgsAnnotationLayer *layer = annotationLayerFromId( layerId );
753 return layer ? layer->item( itemId ) : nullptr;
754}
755
756void QgsMapToolModifyAnnotation::setHoveredItemFromPoint( const QgsPointXY &mapPoint )
757{
758 QgsRectangle searchRect = QgsRectangle( mapPoint.x(), mapPoint.y(), mapPoint.x(), mapPoint.y() );
759 searchRect.grow( searchRadiusMU( canvas() ) );
760
761 const QgsRenderedItemResults *renderedItemResults = canvas()->renderedItemResults( false );
762 if ( !renderedItemResults )
763 {
764 clearHoveredItem();
765 return;
766 }
767
768 const QList<const QgsRenderedAnnotationItemDetails *> items = renderedItemResults->renderedAnnotationItemsInBounds( searchRect );
769 if ( items.empty() )
770 {
771 clearHoveredItem();
772 return;
773 }
774
775 // find closest item
776 QgsRectangle itemBounds;
777 const QgsRenderedAnnotationItemDetails *closestItem = findClosestItemToPoint( mapPoint, items, itemBounds );
778 if ( !closestItem )
779 {
780 clearHoveredItem();
781 return;
782 }
783
784 if ( closestItem->itemId() != mHoveredItemId || closestItem->layerId() != mHoveredItemLayerId )
785 {
786 setHoveredItem( closestItem, itemBounds );
787 }
788
789 // track hovered node too!... here we want to identify the closest node to the cursor position
790 QgsAnnotationItemNode hoveredNode;
791 if ( closestItem->itemId() == mSelectedItemId && closestItem->layerId() == mSelectedItemLayerId )
792 {
793 double currentNodeDistance = std::numeric_limits<double>::max();
794 mHoveredItemNodesSpatialIndex->intersects( searchRect, [&hoveredNode, &currentNodeDistance, &mapPoint, this]( int index ) -> bool {
795 if ( index >= mHoveredItemNodes.size() )
796 return false;
797
798 const QgsAnnotationItemNode &thisNode = mHoveredItemNodes.at( index );
799 const double nodeDistance = thisNode.point().sqrDist( mapPoint );
800 if ( nodeDistance < currentNodeDistance )
801 {
802 hoveredNode = thisNode;
803 currentNodeDistance = nodeDistance;
804 }
805 return true;
806 } );
807 }
808
809 if ( hoveredNode.point().isEmpty() )
810 {
811 // no hovered node
812 if ( mHoveredNodeRubberBand )
813 mHoveredNodeRubberBand->hide();
814 setCursor( mHoveredItemId == mSelectedItemId && mHoveredItemLayerId == mSelectedItemLayerId ? Qt::OpenHandCursor : Qt::ArrowCursor );
815 }
816 else
817 {
818 if ( !mHoveredNodeRubberBand )
819 createHoveredNodeBand();
820
821 mHoveredNodeRubberBand->reset( Qgis::GeometryType::Point );
822 mHoveredNodeRubberBand->addPoint( hoveredNode.point() );
823 mHoveredNodeRubberBand->show();
824
825 setCursor( hoveredNode.cursor() );
826 }
827}
828
829void QgsMapToolModifyAnnotation::clearHoveredItem()
830{
831 if ( mHoverRubberBand )
832 mHoverRubberBand->hide();
833 if ( mHoveredNodeRubberBand )
834 mHoveredNodeRubberBand->hide();
835
836 mHoveredItemId.clear();
837 mHoveredItemLayerId.clear();
838 mHoveredItemNodeRubberBands.clear();
839 mHoveredItemNodesSpatialIndex.reset();
840
841 setCursor( Qt::ArrowCursor );
842}
843
844void QgsMapToolModifyAnnotation::clearSelectedItem()
845{
846 if ( mSelectedRubberBand )
847 mSelectedRubberBand->hide();
848
849 const bool hadSelection = !mSelectedItemId.isEmpty();
850 mSelectedItemId.clear();
851 mSelectedItemLayerId.clear();
852 if ( hadSelection )
853 emit selectionCleared();
854}
855
856void QgsMapToolModifyAnnotation::createHoverBand()
857{
858 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
859
860 mHoverRubberBand.reset( new QgsRubberBand( mCanvas, Qgis::GeometryType::Line ) );
861 mHoverRubberBand->setWidth( scaleFactor );
862 mHoverRubberBand->setSecondaryStrokeColor( QColor( 255, 255, 255, 100 ) );
863 mHoverRubberBand->setColor( QColor( 100, 100, 100, 155 ) );
864}
865
866void QgsMapToolModifyAnnotation::createHoveredNodeBand()
867{
868 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
869
870 mHoveredNodeRubberBand.reset( new QgsRubberBand( mCanvas, Qgis::GeometryType::Point ) );
871 mHoveredNodeRubberBand->setIcon( QgsRubberBand::ICON_FULL_BOX );
872 mHoveredNodeRubberBand->setWidth( scaleFactor );
873 mHoveredNodeRubberBand->setIconSize( scaleFactor * 5 );
874 mHoveredNodeRubberBand->setColor( QColor( 200, 0, 120, 255 ) );
875}
876
877void QgsMapToolModifyAnnotation::createSelectedItemBand()
878{
879 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
880
881 mSelectedRubberBand.reset( new QgsRubberBand( mCanvas, Qgis::GeometryType::Line ) );
882 mSelectedRubberBand->setWidth( scaleFactor );
883 mSelectedRubberBand->setSecondaryStrokeColor( QColor( 255, 255, 255, 100 ) );
884 mSelectedRubberBand->setColor( QColor( 50, 50, 50, 200 ) );
885}
@ VertexHandle
Node is a handle for manipulating vertices.
Definition qgis.h:2508
@ CalloutHandle
Node is a handle for manipulating callouts.
Definition qgis.h:2509
@ ScaleDependentBoundingBox
Item's bounding box will vary depending on map scale.
Definition qgis.h:2465
@ Invalid
Operation has invalid parameters for the item, no change occurred.
Definition qgis.h:2521
@ Success
Item was modified successfully.
Definition qgis.h:2520
@ ItemCleared
The operation results in the item being cleared, and the item should be removed from the layer as a r...
Definition qgis.h:2522
@ Point
Points.
Definition qgis.h:359
@ Line
Lines.
Definition qgis.h:360
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.
QgsPointXY transform(const QgsPointXY &point, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Transform the point from the source CRS to the destination CRS.
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
QgsMapToolAdvancedDigitizing(QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWidget *cadDockWidget)
Creates an advanced digitizing maptool.
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:361
friend class QgsMapCanvas
Definition qgsmaptool.h:381
static double searchRadiusMU(const QgsRenderContext &context)
Gets search radius in map units for given context.
Represents 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.
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).
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:31
double y() const
Returns the vector's y-component.
Definition qgsvector.h:153
double x() const
Returns the vector's x-component.
Definition qgsvector.h:144