QGIS API Documentation 4.3.0-Master (d57cc4a041c)
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 <cmath>
20
21#include "RTree.h"
22#include "qgsannotationitem.h"
25#include "qgsannotationlayer.h"
27#include "qgslogger.h"
28#include "qgsmapcanvas.h"
29#include "qgsmaptopixel.h"
30#include "qgsproject.h"
34#include "qgsrubberband.h"
35#include "qgssnapindicator.h"
36
37#include <QScreen>
38#include <QString>
39#include <QTransform>
40#include <QWindow>
41
42#include "moc_qgsmaptoolmodifyannotation.cpp"
43
44using namespace Qt::StringLiterals;
45
47class QgsAnnotationItemNodesSpatialIndex : public RTree<int, float, 2, float>
48{
49 public:
50 void insert( int index, const QgsRectangle &bounds )
51 {
52 std::array<float, 4> scaledBounds = scaleBounds( bounds );
53 const float aMin[2] { scaledBounds[0], scaledBounds[1] };
54 const float aMax[2] { scaledBounds[2], scaledBounds[3] };
55 this->Insert( aMin, aMax, index );
56 }
57
64 void remove( int index, const QgsRectangle &bounds )
65 {
66 std::array<float, 4> scaledBounds = scaleBounds( bounds );
67 const float aMin[2] { scaledBounds[0], scaledBounds[1] };
68 const float aMax[2] { scaledBounds[2], scaledBounds[3] };
69 this->Remove( aMin, aMax, index );
70 }
71
77 bool intersects( const QgsRectangle &bounds, const std::function<bool( int index )> &callback ) const
78 {
79 std::array<float, 4> scaledBounds = scaleBounds( bounds );
80 const float aMin[2] { scaledBounds[0], scaledBounds[1] };
81 const float aMax[2] { scaledBounds[2], scaledBounds[3] };
82 this->Search( aMin, aMax, callback );
83 return true;
84 }
85
86 private:
87 std::array<float, 4> scaleBounds( const QgsRectangle &bounds ) const
88 {
89 return { static_cast<float>( bounds.xMinimum() ), static_cast<float>( bounds.yMinimum() ), static_cast<float>( bounds.xMaximum() ), static_cast<float>( bounds.yMaximum() ) };
90 }
91};
93
94
95QgsRectangle QgsMapToolModifyAnnotation::reconstructRotatedResizeBounds( const QgsMapToPixel *mapToPixel, double angle, const QgsPointXY &fixedMapPoint, const QgsPointXY &cursorMapPoint )
96{
97 const QgsPointXY fixedPixel = mapToPixel->transform( fixedMapPoint );
98 const QgsPointXY cursorPixel = mapToPixel->transform( cursorMapPoint );
99
100 // Un-rotating the diagonal between the fixed and dragged corners
101 QTransform unrotate;
102 unrotate.rotate( -angle );
103 const QPointF diagonal = unrotate.map( QPointF( cursorPixel.x() - fixedPixel.x(), cursorPixel.y() - fixedPixel.y() ) );
104 const double widthPixels = std::fabs( diagonal.x() );
105 const double heightPixels = std::fabs( diagonal.y() );
106 const QPointF centerPixel( ( fixedPixel.x() + cursorPixel.x() ) / 2.0, ( fixedPixel.y() + cursorPixel.y() ) / 2.0 );
107
108 return QgsAnnotationRectItem::boundsFromPixelRect( mapToPixel, centerPixel, widthPixels, heightPixels );
109}
110
111QgsPointXY QgsMapToolModifyAnnotation::oppositeVertexMapPoint( const QList<QgsAnnotationItemNode> &nodes, int draggedVertex, bool &found )
112{
113 found = false;
114 const int fixedVertex = ( draggedVertex + 2 ) % 4;
115 for ( const QgsAnnotationItemNode &node : nodes )
116 {
117 if ( node.id().part == 0 && node.id().vertex == fixedVertex )
118 {
119 found = true;
120 return node.point();
121 }
122 }
123 return QgsPointXY();
124}
125
126std::optional<QgsRectangle> QgsMapToolModifyAnnotation::rotatedResizeLayerBounds( const QgsAnnotationRectItem *rectItem, QgsAnnotationLayer *layer, const QgsPointXY &cursorMapPoint )
127{
128 const int draggedVertex = mTargetNode.id().vertex;
129 const double angle = rectItem->appliedRotation( canvas()->mapSettings().rotation() );
130 if ( rectItem->placementMode() != Qgis::AnnotationPlacementMode::SpatialBounds || mTargetNode.id().part != 0 || draggedVertex < 0 || draggedVertex >= 4 || qgsDoubleNear( angle, 0 ) )
131 return std::nullopt;
132
133 bool foundFixed = false;
134 const QgsPointXY fixedMapPoint = oppositeVertexMapPoint( mHoveredItemNodes, draggedVertex, foundFixed );
135 if ( !foundFixed )
136 return std::nullopt;
137
138 const QgsRectangle newMapBounds = reconstructRotatedResizeBounds( canvas()->getCoordinateTransform(), angle, fixedMapPoint, cursorMapPoint );
139 const QgsRectangle newLayerBounds = toLayerCoordinates( layer, newMapBounds );
140 if ( qgsDoubleNear( newLayerBounds.width(), 0 ) || qgsDoubleNear( newLayerBounds.height(), 0 ) )
141 return std::nullopt;
142
143 return newLayerBounds;
144}
145
146
153
155
157{
158 mSnapIndicator->setMatch( QgsPointLocator::Match() );
159
160 clearHoveredItem();
161 clearSelectedItem();
163}
164
166{
167 mLastHoverPoint = event->originalPixelPoint();
168 event->snapPoint();
169 mSnapIndicator->setMatch( event->mapPointMatch() );
170
171 const QgsPointXY mapPoint = event->mapPoint();
172
174 QgsAnnotationLayer *layer = annotationLayerFromId( mSelectedItemLayerId );
175 context.setCurrentItemBounds( toLayerCoordinates( layer, mSelectedItemBounds ) );
176 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
177
178 switch ( mCurrentAction )
179 {
180 case Action::NoAction:
181 {
182 setHoveredItemFromPoint( mapPoint );
183 break;
184 }
185
186 case Action::MoveItem:
187 {
188 if ( QgsAnnotationItem *item = annotationItemFromId( mSelectedItemLayerId, mSelectedItemId ) )
189 {
190 const QgsVector delta = toLayerCoordinates( layer, event->mapPoint() ) - mMoveStartPointLayerCrs;
191
193 operation( mSelectedItemId, delta.x(), delta.y(), event->pixelPoint().x() - mMoveStartPointPixels.x(), event->pixelPoint().y() - mMoveStartPointPixels.y() );
194 std::unique_ptr<QgsAnnotationItemEditOperationTransientResults> operationResults( item->transientEditResultsV2( &operation, context ) );
195 if ( operationResults )
196 {
197 mTemporaryRubberBand = make_qobject_unique<QgsRubberBand>( mCanvas, operationResults->representativeGeometry().type() );
198 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
199 mTemporaryRubberBand->setWidth( scaleFactor );
200 mTemporaryRubberBand->setToGeometry( operationResults->representativeGeometry(), layer->crs() );
201 }
202 else
203 {
204 mTemporaryRubberBand.reset();
205 }
206 }
207 break;
208 }
209
210 case Action::MoveNode:
211 {
212 if ( QgsAnnotationItem *item = annotationItemFromId( mSelectedItemLayerId, mSelectedItemId ) )
213 {
214 bool previewDone = false;
215
216 if ( const QgsAnnotationRectItem *rectItem = dynamic_cast<const QgsAnnotationRectItem *>( item ) )
217 {
218 if ( const std::optional<QgsRectangle> newLayerBounds = rotatedResizeLayerBounds( rectItem, layer, event->mapPoint() ) )
219 {
220 const QgsGeometry preview = rectItem->rotatedBoundsGeometry( *newLayerBounds, context.renderContext() );
221 mTemporaryRubberBand = make_qobject_unique<QgsRubberBand>( mCanvas, preview.type() );
222 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
223 mTemporaryRubberBand->setWidth( scaleFactor );
224 mTemporaryRubberBand->setToGeometry( preview, layer->crs() );
225 previewDone = true;
226 }
227 }
228
229 if ( !previewDone )
230 {
231 const QgsPointXY endPointLayer = toLayerCoordinates( layer, event->mapPoint() );
233 mSelectedItemId,
234 mTargetNode.id(),
235 QgsPoint( mTargetNode.point() ),
236 QgsPoint( endPointLayer ),
237 event->pixelPoint().x() - mMoveStartPointPixels.x(),
238 event->pixelPoint().y() - mMoveStartPointPixels.y()
239 );
240 std::unique_ptr<QgsAnnotationItemEditOperationTransientResults> operationResults( item->transientEditResultsV2( &operation, context ) );
241 if ( operationResults )
242 {
243 mTemporaryRubberBand = make_qobject_unique<QgsRubberBand>( mCanvas, operationResults->representativeGeometry().type() );
244 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
245 mTemporaryRubberBand->setWidth( scaleFactor );
246 mTemporaryRubberBand->setToGeometry( operationResults->representativeGeometry(), layer->crs() );
247 }
248 else
249 {
250 mTemporaryRubberBand.reset();
251 }
252 }
253 }
254 break;
255 }
256 }
257}
258
260{
261 QgsAnnotationLayer *layer = annotationLayerFromId( mSelectedItemLayerId );
262
264 context.setCurrentItemBounds( toLayerCoordinates( layer, mSelectedItemBounds ) );
265 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
266
267 switch ( mCurrentAction )
268 {
269 case Action::NoAction:
270 {
271 if ( event->button() != Qt::LeftButton )
272 return;
273
274 if ( mHoveredItemId.isEmpty() || !mHoverRubberBand )
275 {
276 clearSelectedItem();
277 }
278 else if ( mHoveredItemId == mSelectedItemId && mHoveredItemLayerId == mSelectedItemLayerId )
279 {
280 // press is on selected item => move that item
281 if ( layer )
282 {
283 const QgsPointXY mapPoint = event->mapPoint();
284 QgsRectangle searchRect = QgsRectangle( mapPoint.x(), mapPoint.y(), mapPoint.x(), mapPoint.y() );
285 searchRect.grow( searchRadiusMU( canvas() ) );
286
287 QgsAnnotationItemNode hoveredNode;
288 double currentNodeDistance = std::numeric_limits<double>::max();
289 mHoveredItemNodesSpatialIndex->intersects( searchRect, [&hoveredNode, &currentNodeDistance, &mapPoint, this]( int index ) -> bool {
290 const QgsAnnotationItemNode &thisNode = mHoveredItemNodes.at( index );
291 const double nodeDistance = thisNode.point().sqrDist( mapPoint );
292 if ( nodeDistance < currentNodeDistance )
293 {
294 hoveredNode = thisNode;
295 currentNodeDistance = nodeDistance;
296 }
297 return true;
298 } );
299
300 mMoveStartPointCanvasCrs = mapPoint;
301 mMoveStartPointPixels = event->pixelPoint();
302 mMoveStartPointLayerCrs = toLayerCoordinates( layer, mMoveStartPointCanvasCrs );
303 if ( mHoverRubberBand )
304 mHoverRubberBand->hide();
305 if ( mSelectedRubberBand )
306 mSelectedRubberBand->hide();
307
308 if ( hoveredNode.point().isEmpty() )
309 {
310 mCurrentAction = Action::MoveItem;
311 }
312 else
313 {
314 mCurrentAction = Action::MoveNode;
315 mTargetNode = hoveredNode;
316 }
317 }
318 }
319 else
320 {
321 // press is on a different item to selected item => select that item
322 mSelectedItemId = mHoveredItemId;
323 mSelectedItemLayerId = mHoveredItemLayerId;
324 mSelectedItemBounds = mHoveredItemBounds;
325
326 if ( !mSelectedRubberBand )
327 createSelectedItemBand();
328
329 mSelectedRubberBand->copyPointsFrom( mHoverRubberBand );
330 mSelectedRubberBand->show();
331
332 setCursor( Qt::OpenHandCursor );
333
334 emit itemSelected( annotationLayerFromId( mSelectedItemLayerId ), mSelectedItemId );
335 }
336 break;
337 }
338
339 case Action::MoveItem:
340 {
341 if ( event->button() == Qt::RightButton )
342 {
343 mCurrentAction = Action::NoAction;
344 mTemporaryRubberBand.reset();
345 if ( mSelectedRubberBand )
346 {
347 mSelectedRubberBand->setTranslationOffset( 0, 0 );
348 mSelectedRubberBand->show();
349 }
350 mHoveredItemNodeRubberBands.clear();
351 setCursor( Qt::ArrowCursor );
352 }
353 else if ( event->button() == Qt::LeftButton )
354 {
355 // apply move
356 if ( layer )
357 {
358 const QgsVector delta = toLayerCoordinates( layer, event->mapPoint() ) - mMoveStartPointLayerCrs;
359
361 operation( mSelectedItemId, delta.x(), delta.y(), event->pixelPoint().x() - mMoveStartPointPixels.x(), event->pixelPoint().y() - mMoveStartPointPixels.y() );
362 switch ( layer->applyEditV2( &operation, context ) )
363 {
366 mRefreshSelectedItemAfterRedraw = true;
367 break;
370 break;
371 }
372 }
373
374 mTemporaryRubberBand.reset();
375 mCurrentAction = Action::NoAction;
376 setCursor( Qt::ArrowCursor );
377 }
378 break;
379 }
380
381 case Action::MoveNode:
382 {
383 if ( event->button() == Qt::RightButton )
384 {
385 mCurrentAction = Action::NoAction;
386 mTemporaryRubberBand.reset();
387 mHoveredItemNodeRubberBands.clear();
388 mTemporaryRubberBand.reset();
389 setCursor( Qt::ArrowCursor );
390 }
391 else if ( event->button() == Qt::LeftButton )
392 {
393 if ( layer )
394 {
395 bool handled = false;
396
397 const QgsAnnotationItem *resizedItem = annotationItemFromId( mSelectedItemLayerId, mSelectedItemId );
398 if ( const QgsAnnotationRectItem *rectItem = dynamic_cast<const QgsAnnotationRectItem *>( resizedItem ) )
399 {
400 if ( const std::optional<QgsRectangle> newLayerBounds = rotatedResizeLayerBounds( rectItem, layer, event->mapPoint() ) )
401 {
402 QgsAnnotationItemEditOperationSetItemBounds operation( mSelectedItemId, *newLayerBounds );
403 if ( layer->applyEditV2( &operation, context ) == Qgis::AnnotationItemEditOperationResult::Success )
404 {
405 handled = true;
407 mRefreshSelectedItemAfterRedraw = true;
408 }
409 }
410 }
411
412 if ( !handled )
413 {
414 const QgsPointXY endPointLayer = toLayerCoordinates( layer, event->mapPoint() );
416 mSelectedItemId,
417 mTargetNode.id(),
418 QgsPoint( mTargetNode.point() ),
419 QgsPoint( endPointLayer ),
420 event->pixelPoint().x() - mMoveStartPointPixels.x(),
421 event->pixelPoint().y() - mMoveStartPointPixels.y()
422 );
423 switch ( layer->applyEditV2( &operation, context ) )
424 {
427 mRefreshSelectedItemAfterRedraw = true;
428 break;
429
432 break;
433 }
434 }
435 }
436
437 mTemporaryRubberBand.reset();
438 mHoveredItemNodeRubberBands.clear();
439 mHoveredItemNodes.clear();
440 mTemporaryRubberBand.reset();
441 mCurrentAction = Action::NoAction;
442 setCursor( Qt::ArrowCursor );
443 }
444 break;
445 }
446 }
447}
448
450{
451 switch ( mCurrentAction )
452 {
453 case Action::NoAction:
454 case Action::MoveItem:
455 {
456 if ( event->button() != Qt::LeftButton )
457 return;
458
459 mCurrentAction = Action::NoAction;
460 if ( mHoveredItemId == mSelectedItemId && mHoveredItemLayerId == mSelectedItemLayerId )
461 {
462 // double-click on selected item => add node
463 if ( QgsAnnotationLayer *layer = annotationLayerFromId( mSelectedItemLayerId ) )
464 {
465 const QgsPointXY layerPoint = toLayerCoordinates( layer, event->mapPoint() );
466 QgsAnnotationItemEditOperationAddNode operation( mSelectedItemId, QgsPoint( layerPoint ) );
468 context.setCurrentItemBounds( toLayerCoordinates( layer, mSelectedItemBounds ) );
469 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
470
471 switch ( layer->applyEditV2( &operation, context ) )
472 {
475 mRefreshSelectedItemAfterRedraw = true;
476 break;
477
480 break;
481 }
482 }
483 }
484 else
485 {
486 // press is on a different item to selected item => select that item
487 mSelectedItemId = mHoveredItemId;
488 mSelectedItemLayerId = mHoveredItemLayerId;
489 mSelectedItemBounds = mHoveredItemBounds;
490
491 if ( !mSelectedRubberBand )
492 createSelectedItemBand();
493
494 mSelectedRubberBand->copyPointsFrom( mHoverRubberBand );
495 mSelectedRubberBand->show();
496
497 setCursor( Qt::OpenHandCursor );
498
499 emit itemSelected( annotationLayerFromId( mSelectedItemLayerId ), mSelectedItemId );
500 }
501 break;
502 }
503
504 case Action::MoveNode:
505 break;
506 }
507}
508
510{
512 QgsAnnotationLayer *layer = annotationLayerFromId( mSelectedItemLayerId );
513 context.setCurrentItemBounds( toLayerCoordinates( layer, mSelectedItemBounds ) );
514 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
515
516 switch ( mCurrentAction )
517 {
518 case Action::NoAction:
519 {
520 if ( event->key() == Qt::Key_Backspace || event->key() == Qt::Key_Delete )
521 {
522 if ( !layer || mSelectedItemId.isEmpty() )
523 return;
524
525 layer->removeItem( mSelectedItemId );
526 clearSelectedItem();
527 clearHoveredItem();
528 event->ignore(); // disable default shortcut handling
529 }
530 else if ( event->key() == Qt::Key_Left || event->key() == Qt::Key_Right || event->key() == Qt::Key_Up || event->key() == Qt::Key_Down )
531 {
532 if ( !layer )
533 return;
534
535 const QSizeF deltaLayerCoordinates = deltaForKeyEvent( layer, mSelectedRubberBand->asGeometry().centroid().asPoint(), event );
536
537 QgsAnnotationItemEditOperationTranslateItem operation( mSelectedItemId, deltaLayerCoordinates.width(), deltaLayerCoordinates.height() );
538 switch ( layer->applyEditV2( &operation, context ) )
539 {
542 mRefreshSelectedItemAfterRedraw = true;
543 break;
546 break;
547 }
548 event->ignore(); // disable default shortcut handling (move map)
549 }
550 break;
551 }
552
553 case Action::MoveNode:
554 {
555 if ( event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace )
556 {
557 if ( layer )
558 {
559 QgsAnnotationItemEditOperationDeleteNode operation( mSelectedItemId, mTargetNode.id(), QgsPoint( mTargetNode.point() ) );
560 switch ( layer->applyEditV2( &operation, context ) )
561 {
564 mRefreshSelectedItemAfterRedraw = true;
565 break;
567 break;
570 break;
571 }
572 }
573
574 mTemporaryRubberBand.reset();
575 mHoveredItemNodeRubberBands.clear();
576 mHoveredItemNodes.clear();
577 mTemporaryRubberBand.reset();
578 mCurrentAction = Action::NoAction;
579 setCursor( Qt::ArrowCursor );
580 event->ignore(); // disable default shortcut handling (delete vector feature)
581 break;
582 }
583 [[fallthrough]];
584 }
585
586 case Action::MoveItem:
587 {
588 // warning -- fallthrough above!
589 if ( event->key() == Qt::Key_Escape )
590 {
591 mCurrentAction = Action::NoAction;
592 mTemporaryRubberBand.reset();
593 if ( mSelectedRubberBand )
594 {
595 mSelectedRubberBand->setTranslationOffset( 0, 0 );
596 mSelectedRubberBand->show();
597 }
598 mHoveredItemNodeRubberBands.clear();
599
600 setCursor( Qt::ArrowCursor );
601 }
602 break;
603 }
604 }
605}
606
607void QgsMapToolModifyAnnotation::onCanvasRefreshed()
608{
609 bool needsSelectedItemRefresh = mRefreshSelectedItemAfterRedraw;
610 if ( QgsAnnotationItem *item = annotationItemFromId( mSelectedItemLayerId, mSelectedItemId ) )
611 {
613 {
614 needsSelectedItemRefresh = true;
615 }
616 }
617
618 if ( needsSelectedItemRefresh )
619 {
620 const QgsRenderedItemResults *renderedItemResults = canvas()->renderedItemResults( false );
621 if ( !renderedItemResults )
622 {
623 return;
624 }
625
626 const QList<QgsRenderedItemDetails *> items = renderedItemResults->renderedItems();
627 auto it = std::find_if( items.begin(), items.end(), [this]( const QgsRenderedItemDetails *item ) {
628 if ( const QgsRenderedAnnotationItemDetails *annotationItem = dynamic_cast<const QgsRenderedAnnotationItemDetails *>( item ) )
629 {
630 if ( annotationItem->itemId() == mSelectedItemId && annotationItem->layerId() == mSelectedItemLayerId )
631 return true;
632 }
633 return false;
634 } );
635 if ( it != items.end() )
636 {
637 const QgsRectangle itemBounds = ( *it )->boundingBox();
638
639 setHoveredItem( dynamic_cast<const QgsRenderedAnnotationItemDetails *>( *it ), itemBounds );
640 if ( !mSelectedRubberBand )
641 createSelectedItemBand();
642
643 mSelectedRubberBand->copyPointsFrom( mHoverRubberBand );
644 mSelectedRubberBand->show();
645 mSelectedItemBounds = mHoveredItemBounds;
646 }
647 }
648 else
649 {
650 // recheck for hovered item at new mouse point
651 const QgsPointXY mapPoint = canvas()->mapSettings().mapToPixel().toMapCoordinates( mLastHoverPoint );
652 setHoveredItemFromPoint( mapPoint );
653 }
654 mRefreshSelectedItemAfterRedraw = false;
655}
656
657void QgsMapToolModifyAnnotation::setHoveredItem( const QgsRenderedAnnotationItemDetails *item, const QgsRectangle &itemMapBounds )
658{
659 mHoveredItemNodeRubberBands.clear();
660 if ( mHoveredNodeRubberBand )
661 mHoveredNodeRubberBand->hide();
662 mHoveredItemId = item->itemId();
663 mHoveredItemLayerId = item->layerId();
664 mHoveredItemBounds = itemMapBounds;
665 if ( !mHoverRubberBand )
666 createHoverBand();
667
668 mHoverRubberBand->show();
669
670 const QgsAnnotationItem *annotationItem = annotationItemFromId( item->layerId(), item->itemId() );
671 if ( !annotationItem )
672 {
673 // fall back to a plain axis-aligned hover rectangle
674 mHoverRubberBand->reset( Qgis::GeometryType::Line );
675 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMinimum(), itemMapBounds.yMinimum() ) );
676 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMaximum(), itemMapBounds.yMinimum() ) );
677 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMaximum(), itemMapBounds.yMaximum() ) );
678 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMinimum(), itemMapBounds.yMaximum() ) );
679 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMinimum(), itemMapBounds.yMinimum() ) );
680 return;
681 }
682
683 QgsAnnotationLayer *layer = annotationLayerFromId( item->layerId() );
684 const QgsCoordinateTransform layerToMapTransform = QgsCoordinateTransform( layer->crs(), canvas()->mapSettings().destinationCrs(), canvas()->mapSettings().transformContext() );
685
686 // Rotate corners around the center in pixel space. Node points already went
687 // through mapToPixel (which includes map rotation), so only the item's own
688 // rotation is added here. The callout anchor is fixed and never rotated.
689 const QgsAnnotationRectItem *rectItem = dynamic_cast<const QgsAnnotationRectItem *>( annotationItem );
690 const double mapRotation = canvas()->mapSettings().rotation();
691 const double frameRotation = rectItem ? rectItem->appliedRotation( mapRotation ) - mapRotation : 0;
692 const QgsMapToPixel *mapToPixel = canvas()->getCoordinateTransform();
693 bool rotated = rectItem && !qgsDoubleNear( frameRotation, 0 );
694
695 QgsPointXY centerMap;
696 if ( rotated )
697 {
698 try
699 {
700 centerMap = layerToMapTransform.transform( rectItem->bounds().center() );
701 }
702 catch ( QgsCsException & )
703 {
704 // no reliable pivot, so fall back to an unrotated band
705 QgsDebugError( u"Error transforming annotation item center"_s );
706 rotated = false;
707 }
708 }
709
710 QTransform rotationTransform;
711 if ( rotated )
712 {
713 const QgsPointXY centerPixel = mapToPixel->transform( centerMap );
714 rotationTransform.translate( centerPixel.x(), centerPixel.y() );
715 rotationTransform.rotate( frameRotation );
716 rotationTransform.translate( -centerPixel.x(), -centerPixel.y() );
717 }
718
719 auto rotateMapPoint = [&]( const QgsPointXY &p ) -> QgsPointXY {
720 if ( !rotated )
721 return p;
722 const QgsPointXY pPixel = mapToPixel->transform( p );
723 const QPointF r = rotationTransform.map( QPointF( pPixel.x(), pPixel.y() ) );
724 return mapToPixel->toMapCoordinates( r.x(), r.y() );
725 };
726
727 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
728
729 QgsAnnotationItemEditContext context;
730 context.setCurrentItemBounds( toLayerCoordinates( layer, itemMapBounds ) );
731 context.setRenderContext( QgsRenderContext::fromMapSettings( canvas()->mapSettings() ) );
732
733 const QList<QgsAnnotationItemNode> itemNodes = annotationItem->nodesV2( context );
734 QgsRubberBand *vertexNodeBand = new QgsRubberBand( mCanvas, Qgis::GeometryType::Point );
735
736 vertexNodeBand->setIcon( Qgis::RubberBandIconType::Box );
737 vertexNodeBand->setWidth( scaleFactor );
738 vertexNodeBand->setIconSize( scaleFactor * 5 );
739 vertexNodeBand->setColor( QColor( 200, 0, 120, 255 ) );
740
741 QgsRubberBand *calloutNodeBand = new QgsRubberBand( mCanvas, Qgis::GeometryType::Point );
742 calloutNodeBand->setWidth( scaleFactor );
743 calloutNodeBand->setSecondaryStrokeColor( QColor( 255, 255, 255, 100 ) );
744 calloutNodeBand->setColor( QColor( 120, 200, 0, 255 ) );
745 calloutNodeBand->setIcon( Qgis::RubberBandIconType::CrossX );
746 calloutNodeBand->setIconSize( scaleFactor * 5 );
747
748 // store item nodes in a spatial index for quick searching
749 mHoveredItemNodesSpatialIndex = std::make_unique<QgsAnnotationItemNodesSpatialIndex>();
750 int index = 0;
751 mHoveredItemNodes.clear();
752 mHoveredItemNodes.reserve( itemNodes.size() );
753 QVector<QgsPointXY> vertexFramePoints;
754 for ( const QgsAnnotationItemNode &node : itemNodes )
755 {
756 QgsPointXY nodeMapPoint;
757 try
758 {
759 nodeMapPoint = layerToMapTransform.transform( node.point() );
760 }
761 catch ( QgsCsException & )
762 {
763 continue;
764 }
765
766 switch ( node.type() )
767 {
769 // vertex handles rotate together with the item body
770 nodeMapPoint = rotateMapPoint( nodeMapPoint );
771 vertexNodeBand->addPoint( nodeMapPoint );
772 vertexFramePoints.append( nodeMapPoint );
773 break;
774
776 calloutNodeBand->addPoint( nodeMapPoint );
777 break;
778 }
779
780 mHoveredItemNodesSpatialIndex->insert( index, QgsRectangle( nodeMapPoint.x(), nodeMapPoint.y(), nodeMapPoint.x(), nodeMapPoint.y() ) );
781
782 QgsAnnotationItemNode transformedNode = node;
783 transformedNode.setPoint( nodeMapPoint );
784 mHoveredItemNodes.append( transformedNode );
785
786 index++;
787 }
788
789 mHoveredItemNodeRubberBands.emplace_back( vertexNodeBand );
790 mHoveredItemNodeRubberBands.emplace_back( calloutNodeBand );
791
792 // Draw the hover frame. For spatial-bounds rectangles it follows the four
793 // (rotated) corner handles; other modes use the axis-aligned bounds.
794 mHoverRubberBand->reset( Qgis::GeometryType::Line );
795 if ( rectItem && rectItem->placementMode() == Qgis::AnnotationPlacementMode::SpatialBounds && vertexFramePoints.size() == 4 )
796 {
797 for ( const QgsPointXY &framePoint : vertexFramePoints )
798 mHoverRubberBand->addPoint( framePoint );
799 mHoverRubberBand->addPoint( vertexFramePoints.constFirst() );
800 }
801 else
802 {
803 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMinimum(), itemMapBounds.yMinimum() ) );
804 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMaximum(), itemMapBounds.yMinimum() ) );
805 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMaximum(), itemMapBounds.yMaximum() ) );
806 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMinimum(), itemMapBounds.yMaximum() ) );
807 mHoverRubberBand->addPoint( QgsPointXY( itemMapBounds.xMinimum(), itemMapBounds.yMinimum() ) );
808 }
809}
810
811QSizeF QgsMapToolModifyAnnotation::deltaForKeyEvent( QgsAnnotationLayer *layer, const QgsPointXY &originalCanvasPoint, QKeyEvent *event )
812{
813 const double canvasDpi = canvas()->window()->windowHandle()->screen()->physicalDotsPerInch();
814
815 // increment used for cursor key item movement
816 double incrementPixels = 0.0;
817 if ( event->modifiers() & Qt::ShiftModifier )
818 {
819 //holding shift while pressing cursor keys results in a big step - 20 mm
820 incrementPixels = 20.0 / 25.4 * canvasDpi;
821 }
822 else if ( event->modifiers() & Qt::AltModifier )
823 {
824 //holding alt while pressing cursor keys results in a 1 pixel step
825 incrementPixels = 1;
826 }
827 else
828 {
829 // 5 mm
830 incrementPixels = 5.0 / 25.4 * canvasDpi;
831 }
832
833 double deltaXPixels = 0;
834 double deltaYPixels = 0;
835 switch ( event->key() )
836 {
837 case Qt::Key_Left:
838 deltaXPixels = -incrementPixels;
839 break;
840 case Qt::Key_Right:
841 deltaXPixels = incrementPixels;
842 break;
843 case Qt::Key_Up:
844 deltaYPixels = -incrementPixels;
845 break;
846 case Qt::Key_Down:
847 deltaYPixels = incrementPixels;
848 break;
849 default:
850 break;
851 }
852
853 const QgsPointXY beforeMoveMapPoint = canvas()->getCoordinateTransform()->toMapCoordinates( originalCanvasPoint.x(), originalCanvasPoint.y() );
854 const QgsPointXY beforeMoveLayerPoint = toLayerCoordinates( layer, beforeMoveMapPoint );
855
856 const QgsPointXY afterMoveCanvasPoint( originalCanvasPoint.x() + deltaXPixels, originalCanvasPoint.y() + deltaYPixels );
857 const QgsPointXY afterMoveMapPoint = canvas()->getCoordinateTransform()->toMapCoordinates( afterMoveCanvasPoint.x(), afterMoveCanvasPoint.y() );
858 const QgsPointXY afterMoveLayerPoint = toLayerCoordinates( layer, afterMoveMapPoint );
859
860 return QSizeF( afterMoveLayerPoint.x() - beforeMoveLayerPoint.x(), afterMoveLayerPoint.y() - beforeMoveLayerPoint.y() );
861}
862
863void QgsMapToolModifyAnnotation::setHoveredItemFromPoint( const QgsPointXY &mapPoint )
864{
865 QgsRectangle searchRect = QgsRectangle( mapPoint.x(), mapPoint.y(), mapPoint.x(), mapPoint.y() );
866 searchRect.grow( searchRadiusMU( canvas() ) );
867
868 const QgsRenderedItemResults *renderedItemResults = canvas()->renderedItemResults( false );
869 if ( !renderedItemResults )
870 {
871 clearHoveredItem();
872 return;
873 }
874
875 const QList<const QgsRenderedAnnotationItemDetails *> items = renderedItemResults->renderedAnnotationItemsInBounds( searchRect );
876 if ( items.empty() )
877 {
878 clearHoveredItem();
879 return;
880 }
881
882 // find closest item
883 QgsRectangle itemBounds;
884 const QgsRenderedAnnotationItemDetails *closestItem = findClosestItemToPoint( mapPoint, items, itemBounds );
885 if ( !closestItem )
886 {
887 clearHoveredItem();
888 return;
889 }
890
891 if ( closestItem->itemId() != mHoveredItemId || closestItem->layerId() != mHoveredItemLayerId )
892 {
893 setHoveredItem( closestItem, itemBounds );
894 }
895
896 // track hovered node too!... here we want to identify the closest node to the cursor position
897 QgsAnnotationItemNode hoveredNode;
898 if ( closestItem->itemId() == mSelectedItemId && closestItem->layerId() == mSelectedItemLayerId )
899 {
900 double currentNodeDistance = std::numeric_limits<double>::max();
901 mHoveredItemNodesSpatialIndex->intersects( searchRect, [&hoveredNode, &currentNodeDistance, &mapPoint, this]( int index ) -> bool {
902 if ( index >= mHoveredItemNodes.size() )
903 return false;
904
905 const QgsAnnotationItemNode &thisNode = mHoveredItemNodes.at( index );
906 const double nodeDistance = thisNode.point().sqrDist( mapPoint );
907 if ( nodeDistance < currentNodeDistance )
908 {
909 hoveredNode = thisNode;
910 currentNodeDistance = nodeDistance;
911 }
912 return true;
913 } );
914 }
915
916 if ( hoveredNode.point().isEmpty() )
917 {
918 // no hovered node
919 if ( mHoveredNodeRubberBand )
920 mHoveredNodeRubberBand->hide();
921 setCursor( mHoveredItemId == mSelectedItemId && mHoveredItemLayerId == mSelectedItemLayerId ? Qt::OpenHandCursor : Qt::ArrowCursor );
922 }
923 else
924 {
925 if ( !mHoveredNodeRubberBand )
926 createHoveredNodeBand();
927
928 mHoveredNodeRubberBand->reset( Qgis::GeometryType::Point );
929 mHoveredNodeRubberBand->addPoint( hoveredNode.point() );
930 mHoveredNodeRubberBand->show();
931
932 setCursor( hoveredNode.cursor() );
933 }
934}
935
936void QgsMapToolModifyAnnotation::clearHoveredItem()
937{
938 if ( mHoverRubberBand )
939 mHoverRubberBand->hide();
940 if ( mHoveredNodeRubberBand )
941 mHoveredNodeRubberBand->hide();
942
943 mHoveredItemId.clear();
944 mHoveredItemLayerId.clear();
945 mHoveredItemNodeRubberBands.clear();
946 mHoveredItemNodesSpatialIndex.reset();
947
948 setCursor( Qt::ArrowCursor );
949}
950
951void QgsMapToolModifyAnnotation::clearSelectedItem()
952{
953 if ( mSelectedRubberBand )
954 mSelectedRubberBand->hide();
955
956 const bool hadSelection = !mSelectedItemId.isEmpty();
957 mSelectedItemId.clear();
958 mSelectedItemLayerId.clear();
959 if ( hadSelection )
960 emit selectionCleared();
961}
962
963void QgsMapToolModifyAnnotation::createHoverBand()
964{
965 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
966
968 mHoverRubberBand->setWidth( scaleFactor );
969 mHoverRubberBand->setSecondaryStrokeColor( QColor( 255, 255, 255, 100 ) );
970 mHoverRubberBand->setColor( QColor( 100, 100, 100, 155 ) );
971}
972
973void QgsMapToolModifyAnnotation::createHoveredNodeBand()
974{
975 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
976
978 mHoveredNodeRubberBand->setIcon( Qgis::RubberBandIconType::BoxFilled );
979 mHoveredNodeRubberBand->setWidth( scaleFactor );
980 mHoveredNodeRubberBand->setIconSize( scaleFactor * 5 );
981 mHoveredNodeRubberBand->setColor( QColor( 200, 0, 120, 255 ) );
982}
983
984void QgsMapToolModifyAnnotation::createSelectedItemBand()
985{
986 const double scaleFactor = canvas()->fontMetrics().xHeight() * .2;
987
989 mSelectedRubberBand->setWidth( scaleFactor );
990 mSelectedRubberBand->setSecondaryStrokeColor( QColor( 255, 255, 255, 100 ) );
991 mSelectedRubberBand->setColor( QColor( 50, 50, 50, 200 ) );
992}
@ VertexHandle
Node is a handle for manipulating vertices.
Definition qgis.h:2695
@ CalloutHandle
Node is a handle for manipulating callouts.
Definition qgis.h:2696
@ ScaleDependentBoundingBox
Item's bounding box will vary depending on map scale.
Definition qgis.h:2652
@ Invalid
Operation has invalid parameters for the item, no change occurred.
Definition qgis.h:2708
@ Success
Item was modified successfully.
Definition qgis.h:2707
@ ItemCleared
The operation results in the item being cleared, and the item should be removed from the layer as a r...
Definition qgis.h:2709
@ Box
A box is used to highlight points (□).
Definition qgis.h:7048
@ BoxFilled
A filled box is used to highlight points (■).
Definition qgis.h:7050
@ CrossX
A cross is used to highlight points (x).
Definition qgis.h:7047
@ Point
Points.
Definition qgis.h:380
@ Line
Lines.
Definition qgis.h:381
@ SpatialBounds
Item is rendered inside fixed spatial bounds, and size will depend on map scale.
Definition qgis.h:2668
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.
QgsRenderContext renderContext() const
Returns the render context associated with the edit operation.
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 setting the bounds of an item.
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.
Abstract base class for annotation items which render annotations in a rectangular shape.
QgsGeometry rotatedBoundsGeometry(const QgsRectangle &layerBounds, const QgsRenderContext &renderContext) const
Returns the polygon geometry (in layer coordinates) for the item occupying the given layerBounds,...
static QgsRectangle boundsFromPixelRect(const QgsMapToPixel *mapToPixel, const QPointF &centerPixel, double widthPixels, double heightPixels)
Returns the axis-aligned bounds, in map coordinates, of a rectangle centered on centerPixel with size...
double appliedRotation(double mapRotation) const
Returns the effective on-screen rotation of the item, in degrees clockwise.
QgsRectangle bounds() const
Returns the bounds of the item.
Qgis::AnnotationPlacementMode placementMode() const
Returns the placement mode for the item.
QgsPointXY transform(const QgsPointXY &point, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Transform the point from the source CRS to the destination CRS.
A geometry is the spatial representation of a feature.
Qgis::GeometryType type
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.
const QgsMapSettings & mapSettings() const
Gets access to properties used for map rendering.
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.
double rotation() const
Returns the rotation of the resulting map image, in degrees clockwise.
Perform transforms between map coordinates and device coordinates.
QgsPointXY toMapCoordinates(int x, int y) const
Transforms device coordinates to map (world) coordinates.
QgsPointXY transform(const QgsPointXY &p) const
Transforms a point p from map (world) coordinates to device 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:403
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
QgsPointXY center
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.
void setIconSize(double iconSize)
Sets the size of the point icons.
void setIcon(Qgis::RubberBandIconType icon)
Sets the icon type to highlight point geometries.
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.
void setColor(const QColor &color)
Sets the color for the rubberband.
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
double ANALYSIS_EXPORT angle(QgsPoint *p1, QgsPoint *p2, QgsPoint *p3, QgsPoint *p4)
Calculates the angle between two segments (in 2 dimension, z-values are ignored).
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:7544
#define QgsDebugError(str)
Definition qgslogger.h:71
constexpr QObjectUniquePtr< Tp > make_qobject_unique(Args &&...args)
Create an object owned by a QObjectUniquePtr.