QGIS API Documentation  3.24.2-Tisler (13c1a02865)
qgslayoutsnapper.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgslayoutsnapper.cpp
3  --------------------
4  begin : July 2017
5  copyright : (C) 2017 by Nyall Dawson
6  email : nyall dot dawson at gmail dot com
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 
17 #include "qgslayoutsnapper.h"
18 #include "qgslayout.h"
19 #include "qgsreadwritecontext.h"
20 #include "qgsproject.h"
22 #include "qgssettings.h"
23 
25  : mLayout( layout )
26 {
27  QgsSettings s;
28  mTolerance = s.value( QStringLiteral( "LayoutDesigner/defaultSnapTolerancePixels" ), 5, QgsSettings::Gui ).toInt();
29 }
30 
32 {
33  return mLayout;
34 }
35 
36 void QgsLayoutSnapper::setSnapTolerance( const int snapTolerance )
37 {
38  mTolerance = snapTolerance;
39 }
40 
41 void QgsLayoutSnapper::setSnapToGrid( bool enabled )
42 {
43  mSnapToGrid = enabled;
44 }
45 
47 {
48  mSnapToGuides = enabled;
49 }
50 
52 {
53  mSnapToItems = enabled;
54 }
55 
56 QPointF QgsLayoutSnapper::snapPoint( QPointF point, double scaleFactor, bool &snapped, QGraphicsLineItem *horizontalSnapLine, QGraphicsLineItem *verticalSnapLine,
57  const QList< QgsLayoutItem * > *ignoreItems ) const
58 {
59  snapped = false;
60 
61  // highest priority - guides
62  bool snappedXToGuides = false;
63  double newX = snapPointToGuides( point.x(), Qt::Vertical, scaleFactor, snappedXToGuides );
64  if ( snappedXToGuides )
65  {
66  snapped = true;
67  point.setX( newX );
68  if ( verticalSnapLine )
69  verticalSnapLine->setVisible( false );
70  }
71  bool snappedYToGuides = false;
72  double newY = snapPointToGuides( point.y(), Qt::Horizontal, scaleFactor, snappedYToGuides );
73  if ( snappedYToGuides )
74  {
75  snapped = true;
76  point.setY( newY );
77  if ( horizontalSnapLine )
78  horizontalSnapLine->setVisible( false );
79  }
80 
81  bool snappedXToItems = false;
82  bool snappedYToItems = false;
83  if ( !snappedXToGuides )
84  {
85  newX = snapPointToItems( point.x(), Qt::Horizontal, scaleFactor, ignoreItems ? *ignoreItems : QList< QgsLayoutItem * >(), snappedXToItems, verticalSnapLine );
86  if ( snappedXToItems )
87  {
88  snapped = true;
89  point.setX( newX );
90  }
91  }
92  if ( !snappedYToGuides )
93  {
94  newY = snapPointToItems( point.y(), Qt::Vertical, scaleFactor, ignoreItems ? *ignoreItems : QList< QgsLayoutItem * >(), snappedYToItems, horizontalSnapLine );
95  if ( snappedYToItems )
96  {
97  snapped = true;
98  point.setY( newY );
99  }
100  }
101 
102  bool snappedXToGrid = false;
103  bool snappedYToGrid = false;
104  QPointF res = snapPointToGrid( point, scaleFactor, snappedXToGrid, snappedYToGrid );
105  if ( snappedXToGrid && !snappedXToGuides && !snappedXToItems )
106  {
107  snapped = true;
108  point.setX( res.x() );
109  }
110  if ( snappedYToGrid && !snappedYToGuides && !snappedYToItems )
111  {
112  snapped = true;
113  point.setY( res.y() );
114  }
115 
116  return point;
117 }
118 
119 QRectF QgsLayoutSnapper::snapRect( const QRectF &rect, double scaleFactor, bool &snapped, QGraphicsLineItem *horizontalSnapLine, QGraphicsLineItem *verticalSnapLine, const QList<QgsLayoutItem *> *ignoreItems ) const
120 {
121  snapped = false;
122  QRectF snappedRect = rect;
123 
124  QList< double > xCoords;
125  xCoords << rect.left() << rect.center().x() << rect.right();
126  QList< double > yCoords;
127  yCoords << rect.top() << rect.center().y() << rect.bottom();
128 
129  // highest priority - guides
130  bool snappedXToGuides = false;
131  double deltaX = snapPointsToGuides( xCoords, Qt::Vertical, scaleFactor, snappedXToGuides );
132  if ( snappedXToGuides )
133  {
134  snapped = true;
135  snappedRect.translate( deltaX, 0 );
136  if ( verticalSnapLine )
137  verticalSnapLine->setVisible( false );
138  }
139  bool snappedYToGuides = false;
140  double deltaY = snapPointsToGuides( yCoords, Qt::Horizontal, scaleFactor, snappedYToGuides );
141  if ( snappedYToGuides )
142  {
143  snapped = true;
144  snappedRect.translate( 0, deltaY );
145  if ( horizontalSnapLine )
146  horizontalSnapLine->setVisible( false );
147  }
148 
149  bool snappedXToItems = false;
150  bool snappedYToItems = false;
151  if ( !snappedXToGuides )
152  {
153  deltaX = snapPointsToItems( xCoords, Qt::Horizontal, scaleFactor, ignoreItems ? *ignoreItems : QList< QgsLayoutItem * >(), snappedXToItems, verticalSnapLine );
154  if ( snappedXToItems )
155  {
156  snapped = true;
157  snappedRect.translate( deltaX, 0 );
158  }
159  }
160  if ( !snappedYToGuides )
161  {
162  deltaY = snapPointsToItems( yCoords, Qt::Vertical, scaleFactor, ignoreItems ? *ignoreItems : QList< QgsLayoutItem * >(), snappedYToItems, horizontalSnapLine );
163  if ( snappedYToItems )
164  {
165  snapped = true;
166  snappedRect.translate( 0, deltaY );
167  }
168  }
169 
170  bool snappedXToGrid = false;
171  bool snappedYToGrid = false;
172  QList< QPointF > points;
173  points << rect.topLeft() << rect.topRight() << rect.bottomLeft() << rect.bottomRight();
174  QPointF res = snapPointsToGrid( points, scaleFactor, snappedXToGrid, snappedYToGrid );
175  if ( snappedXToGrid && !snappedXToGuides && !snappedXToItems )
176  {
177  snapped = true;
178  snappedRect.translate( res.x(), 0 );
179  }
180  if ( snappedYToGrid && !snappedYToGuides && !snappedYToItems )
181  {
182  snapped = true;
183  snappedRect.translate( 0, res.y() );
184  }
185 
186  return snappedRect;
187 }
188 
189 QPointF QgsLayoutSnapper::snapPointToGrid( QPointF point, double scaleFactor, bool &snappedX, bool &snappedY ) const
190 {
191  QPointF delta = snapPointsToGrid( QList< QPointF >() << point, scaleFactor, snappedX, snappedY );
192  return point + delta;
193 }
194 
195 QPointF QgsLayoutSnapper::snapPointsToGrid( const QList<QPointF> &points, double scaleFactor, bool &snappedX, bool &snappedY ) const
196 {
197  snappedX = false;
198  snappedY = false;
199  if ( !mLayout || !mSnapToGrid )
200  {
201  return QPointF( 0, 0 );
202  }
203  const QgsLayoutGridSettings &grid = mLayout->gridSettings();
204  if ( grid.resolution().length() <= 0 )
205  return QPointF( 0, 0 );
206 
207  double deltaX = 0;
208  double deltaY = 0;
209  double smallestDiffX = std::numeric_limits<double>::max();
210  double smallestDiffY = std::numeric_limits<double>::max();
211  for ( QPointF point : points )
212  {
213  //calculate y offset to current page
214  QPointF pagePoint = mLayout->pageCollection()->positionOnPage( point );
215 
216  double yPage = pagePoint.y(); //y-coordinate relative to current page
217  double yAtTopOfPage = mLayout->pageCollection()->page( mLayout->pageCollection()->pageNumberForPoint( point ) )->pos().y();
218 
219  //snap x coordinate
220  double gridRes = mLayout->convertToLayoutUnits( grid.resolution() );
221  QPointF gridOffset = mLayout->convertToLayoutUnits( grid.offset() );
222  int xRatio = static_cast< int >( ( point.x() - gridOffset.x() ) / gridRes + 0.5 ); //NOLINT
223  int yRatio = static_cast< int >( ( yPage - gridOffset.y() ) / gridRes + 0.5 ); //NOLINT
224 
225  double xSnapped = xRatio * gridRes + gridOffset.x();
226  double ySnapped = yRatio * gridRes + gridOffset.y() + yAtTopOfPage;
227 
228  double currentDiffX = std::fabs( xSnapped - point.x() );
229  if ( currentDiffX < smallestDiffX )
230  {
231  smallestDiffX = currentDiffX;
232  deltaX = xSnapped - point.x();
233  }
234 
235  double currentDiffY = std::fabs( ySnapped - point.y() );
236  if ( currentDiffY < smallestDiffY )
237  {
238  smallestDiffY = currentDiffY;
239  deltaY = ySnapped - point.y();
240  }
241  }
242 
243  //convert snap tolerance from pixels to layout units
244  double alignThreshold = mTolerance / scaleFactor;
245 
246  QPointF delta( 0, 0 );
247  if ( smallestDiffX <= alignThreshold )
248  {
249  //snap distance is inside of tolerance
250  snappedX = true;
251  delta.setX( deltaX );
252  }
253  if ( smallestDiffY <= alignThreshold )
254  {
255  //snap distance is inside of tolerance
256  snappedY = true;
257  delta.setY( deltaY );
258  }
259 
260  return delta;
261 }
262 
263 double QgsLayoutSnapper::snapPointToGuides( double original, Qt::Orientation orientation, double scaleFactor, bool &snapped ) const
264 {
265  double delta = snapPointsToGuides( QList< double >() << original, orientation, scaleFactor, snapped );
266  return original + delta;
267 }
268 
269 double QgsLayoutSnapper::snapPointsToGuides( const QList<double> &points, Qt::Orientation orientation, double scaleFactor, bool &snapped ) const
270 {
271  snapped = false;
272  if ( !mLayout || !mSnapToGuides )
273  {
274  return 0;
275  }
276 
277  //convert snap tolerance from pixels to layout units
278  double alignThreshold = mTolerance / scaleFactor;
279 
280  double bestDelta = 0;
281  double smallestDiff = std::numeric_limits<double>::max();
282 
283  for ( double p : points )
284  {
285  const auto constGuides = mLayout->guides().guides( orientation );
286  for ( QgsLayoutGuide *guide : constGuides )
287  {
288  double guidePos = guide->layoutPosition();
289  double diff = std::fabs( p - guidePos );
290  if ( diff < smallestDiff )
291  {
292  smallestDiff = diff;
293  bestDelta = guidePos - p;
294  }
295  }
296  }
297 
298  if ( smallestDiff <= alignThreshold )
299  {
300  snapped = true;
301  return bestDelta;
302  }
303  else
304  {
305  return 0;
306  }
307 }
308 
309 double QgsLayoutSnapper::snapPointToItems( double original, Qt::Orientation orientation, double scaleFactor, const QList<QgsLayoutItem *> &ignoreItems, bool &snapped,
310  QGraphicsLineItem *snapLine ) const
311 {
312  double delta = snapPointsToItems( QList< double >() << original, orientation, scaleFactor, ignoreItems, snapped, snapLine );
313  return original + delta;
314 }
315 
316 double QgsLayoutSnapper::snapPointsToItems( const QList<double> &points, Qt::Orientation orientation, double scaleFactor, const QList<QgsLayoutItem *> &ignoreItems, bool &snapped, QGraphicsLineItem *snapLine ) const
317 {
318  snapped = false;
319  if ( !mLayout || !mSnapToItems )
320  {
321  if ( snapLine )
322  snapLine->setVisible( false );
323  return 0;
324  }
325 
326  double alignThreshold = mTolerance / scaleFactor;
327 
328  double bestDelta = 0;
329  double smallestDiff = std::numeric_limits<double>::max();
330  double closest = 0;
331  const QList<QGraphicsItem *> itemList = mLayout->items();
332  QList< double > currentCoords;
333  for ( QGraphicsItem *item : itemList )
334  {
335  QgsLayoutItem *currentItem = dynamic_cast< QgsLayoutItem *>( item );
336  if ( !currentItem || ignoreItems.contains( currentItem ) )
337  continue;
338  if ( currentItem->type() == QgsLayoutItemRegistry::LayoutGroup )
339  continue; // don't snap to group bounds, instead we snap to group item bounds
340  if ( !currentItem->isVisible() )
341  continue; // don't snap to invisible items
342 
343  QRectF itemRect;
344  if ( dynamic_cast<const QgsLayoutItemPage *>( currentItem ) )
345  {
346  //if snapping to paper use the paper item's rect rather then the bounding rect,
347  //since we want to snap to the page edge and not any outlines drawn around the page
348  itemRect = currentItem->mapRectToScene( currentItem->rect() );
349  }
350  else
351  {
352  itemRect = currentItem->mapRectToScene( currentItem->rectWithFrame() );
353  }
354 
355  currentCoords.clear();
356  switch ( orientation )
357  {
358  case Qt::Horizontal:
359  {
360  currentCoords << itemRect.left();
361  currentCoords << itemRect.right();
362  currentCoords << itemRect.center().x();
363  break;
364  }
365 
366  case Qt::Vertical:
367  {
368  currentCoords << itemRect.top();
369  currentCoords << itemRect.center().y();
370  currentCoords << itemRect.bottom();
371  break;
372  }
373  }
374 
375  for ( double val : std::as_const( currentCoords ) )
376  {
377  for ( double p : points )
378  {
379  double dist = std::fabs( p - val );
380  if ( dist <= alignThreshold && dist < smallestDiff )
381  {
382  snapped = true;
383  smallestDiff = dist;
384  bestDelta = val - p;
385  closest = val;
386  }
387  }
388  }
389  }
390 
391  if ( snapLine )
392  {
393  if ( snapped )
394  {
395  snapLine->setVisible( true );
396  switch ( orientation )
397  {
398  case Qt::Vertical:
399  {
400  snapLine->setLine( QLineF( -100000, closest, 100000, closest ) );
401  break;
402  }
403 
404  case Qt::Horizontal:
405  {
406  snapLine->setLine( QLineF( closest, -100000, closest, 100000 ) );
407  break;
408  }
409  }
410  }
411  else
412  {
413  snapLine->setVisible( false );
414  }
415  }
416 
417  return bestDelta;
418 }
419 
420 
421 bool QgsLayoutSnapper::writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext & ) const
422 {
423  QDomElement element = document.createElement( QStringLiteral( "Snapper" ) );
424 
425  element.setAttribute( QStringLiteral( "tolerance" ), mTolerance );
426  element.setAttribute( QStringLiteral( "snapToGrid" ), mSnapToGrid );
427  element.setAttribute( QStringLiteral( "snapToGuides" ), mSnapToGuides );
428  element.setAttribute( QStringLiteral( "snapToItems" ), mSnapToItems );
429 
430  parentElement.appendChild( element );
431  return true;
432 }
433 
434 bool QgsLayoutSnapper::readXml( const QDomElement &e, const QDomDocument &, const QgsReadWriteContext & )
435 {
436  QDomElement element = e;
437  if ( element.nodeName() != QLatin1String( "Snapper" ) )
438  {
439  element = element.firstChildElement( QStringLiteral( "Snapper" ) );
440  }
441 
442  if ( element.nodeName() != QLatin1String( "Snapper" ) )
443  {
444  return false;
445  }
446 
447  mTolerance = element.attribute( QStringLiteral( "tolerance" ), QStringLiteral( "5" ) ).toInt();
448  mSnapToGrid = element.attribute( QStringLiteral( "snapToGrid" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
449  mSnapToGuides = element.attribute( QStringLiteral( "snapToGuides" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
450  mSnapToItems = element.attribute( QStringLiteral( "snapToItems" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
451  return true;
452 }
Contains settings relating to the appearance, spacing and offset for layout grids.
QgsLayoutMeasurement resolution() const
Returns the page/snap grid resolution.
QgsLayoutPoint offset() const
Returns the offset of the page/snap grid.
QList< QgsLayoutGuide * > guides()
Returns a list of all guides contained in the collection.
Contains the configuration for a single snap guide used by a layout.
Item representing the paper in a layout.
Base class for graphical items within a QgsLayout.
virtual QRectF rectWithFrame() const
Returns the item's rectangular bounds, including any bleed caused by the item's frame.
int type() const override
Returns a unique graphics item type identifier.
double length() const
Returns the length of the measurement.
int pageNumberForPoint(QPointF point) const
Returns the page number corresponding to a point in the layout (in layout units).
QPointF positionOnPage(QPointF point) const
Returns the position within a page of a point in the layout (in layout units).
QgsLayoutItemPage * page(int pageNumber)
Returns a specific page (by pageNumber) from the collection.
QPointF snapPoint(QPointF point, double scaleFactor, bool &snapped, QGraphicsLineItem *horizontalSnapLine=nullptr, QGraphicsLineItem *verticalSnapLine=nullptr, const QList< QgsLayoutItem * > *ignoreItems=nullptr) const
Snaps a layout coordinate point.
void setSnapToItems(bool enabled)
Sets whether snapping to items is enabled.
QPointF snapPointsToGrid(const QList< QPointF > &points, double scaleFactor, bool &snappedX, bool &snappedY) const
Snaps a set of points to the grid.
QRectF snapRect(const QRectF &rect, double scaleFactor, bool &snapped, QGraphicsLineItem *horizontalSnapLine=nullptr, QGraphicsLineItem *verticalSnapLine=nullptr, const QList< QgsLayoutItem * > *ignoreItems=nullptr) const
Snaps a layout coordinate rect.
int snapTolerance() const
Returns the snap tolerance (in pixels) to use when snapping.
double snapPointsToItems(const QList< double > &points, Qt::Orientation orientation, double scaleFactor, const QList< QgsLayoutItem * > &ignoreItems, bool &snapped, QGraphicsLineItem *snapLine=nullptr) const
Snaps a set of points to the item bounds.
bool readXml(const QDomElement &gridElement, const QDomDocument &document, const QgsReadWriteContext &context) override
Sets the snapper's state from a DOM element.
void setSnapToGuides(bool enabled)
Sets whether snapping to guides is enabled.
QgsLayoutSnapper(QgsLayout *layout)
Constructor for QgsLayoutSnapper, attached to the specified layout.
void setSnapTolerance(int snapTolerance)
Sets the snap tolerance (in pixels) to use when snapping.
double snapPointsToGuides(const QList< double > &points, Qt::Orientation orientation, double scaleFactor, bool &snapped) const
Snaps a set of points to the guides.
void setSnapToGrid(bool enabled)
Sets whether snapping to grid is enabled.
QgsLayout * layout() override
Returns the layout the object belongs to.
double snapPointToItems(double original, Qt::Orientation orientation, double scaleFactor, const QList< QgsLayoutItem * > &ignoreItems, bool &snapped, QGraphicsLineItem *snapLine=nullptr) const
Snaps an original layout coordinate to the item bounds.
double snapPointToGuides(double original, Qt::Orientation orientation, double scaleFactor, bool &snapped) const
Snaps an original layout coordinate to the guides.
QPointF snapPointToGrid(QPointF point, double scaleFactor, bool &snappedX, bool &snappedY) const
Snaps a layout coordinate point to the grid.
bool writeXml(QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context) const override
Stores the snapper's state in a DOM element.
Base class for layouts, which can contain items such as maps, labels, scalebars, etc.
Definition: qgslayout.h:51
QgsLayoutGridSettings & gridSettings()
Returns a reference to the layout's grid settings, which stores settings relating to grid appearance,...
Definition: qgslayout.h:419
QgsLayoutPageCollection * pageCollection()
Returns a pointer to the layout's page collection, which stores and manages page items in the layout.
Definition: qgslayout.cpp:459
QgsLayoutGuideCollection & guides()
Returns a reference to the layout's guide collection, which manages page snap guides.
Definition: qgslayout.cpp:385
double convertToLayoutUnits(QgsLayoutMeasurement measurement) const
Converts a measurement into the layout's native units.
Definition: qgslayout.cpp:329
The class is used as a container of context for various read/write operations on other objects.
This class is a composition of two QSettings instances:
Definition: qgssettings.h:62
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.