1 /***************************************************************************
2 qgsmaptoolpinlabels.cpp
3 -----------------------
4 begin : 2012-07-12
5 copyright : (C) 2012 by Larry Shaffer
6 email : larrys at dakotacarto dot com
7 ***************************************************************************/
8
9 /***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
18 #include "qgsmaptoolpinlabels.h"
19
20 #include "qgisapp.h"
21 #include "qgsapplication.h"
22 #include "qgsmapcanvas.h"
23 #include "qgsproject.h"
24 #include "qgsvectorlayer.h"
25 #include "qgsmapmouseevent.h"
26 #include "qgsmaptoolselectutils.h"
27 #include "qgsrubberband.h"
28 #include "qgslogger.h"
29
30
QgsMapToolPinLabels(QgsMapCanvas * canvas)31 QgsMapToolPinLabels::QgsMapToolPinLabels( QgsMapCanvas *canvas )
32 : QgsMapToolLabel( canvas )
33 , mDragging( false )
34 , mShowPinned( false )
35
36 {
37 mToolName = tr( "Pin labels" );
38
39 connect( QgisApp::instance()->actionToggleEditing(), &QAction::triggered, this, &QgsMapToolPinLabels::updatePinnedLabels );
40 connect( canvas, &QgsMapCanvas::renderComplete, this, &QgsMapToolPinLabels::highlightPinnedLabels );
41 }
42
~QgsMapToolPinLabels()43 QgsMapToolPinLabels::~QgsMapToolPinLabels()
44 {
45 delete mRubberBand;
46 removePinnedHighlights();
47 }
48
canvasPressEvent(QgsMapMouseEvent * e)49 void QgsMapToolPinLabels::canvasPressEvent( QgsMapMouseEvent *e )
50 {
51 Q_UNUSED( e )
52 mSelectRect.setRect( 0, 0, 0, 0 );
53 mSelectRect.setTopLeft( e->pos() );
54 mSelectRect.setBottomRight( e->pos() );
55 mRubberBand = new QgsRubberBand( mCanvas, QgsWkbTypes::PolygonGeometry );
56 }
57
canvasMoveEvent(QgsMapMouseEvent * e)58 void QgsMapToolPinLabels::canvasMoveEvent( QgsMapMouseEvent *e )
59 {
60 if ( e->buttons() != Qt::LeftButton )
61 return;
62
63 if ( !mDragging )
64 {
65 mDragging = true;
66 mSelectRect.setTopLeft( e->pos() );
67 }
68 mSelectRect.setBottomRight( e->pos() );
69 QgsMapToolSelectUtils::setRubberBand( mCanvas, mSelectRect, mRubberBand );
70 }
71
canvasReleaseEvent(QgsMapMouseEvent * e)72 void QgsMapToolPinLabels::canvasReleaseEvent( QgsMapMouseEvent *e )
73 {
74 //if the user simply clicked without dragging a rect
75 //we will fabricate a small 1x1 pix rect and then continue
76 //as if they had dragged a rect
77 if ( !mDragging )
78 {
79 mSelectRect.setLeft( e->pos().x() - 1 );
80 mSelectRect.setRight( e->pos().x() + 1 );
81 mSelectRect.setTop( e->pos().y() - 1 );
82 mSelectRect.setBottom( e->pos().y() + 1 );
83 }
84 else
85 {
86 // Set valid values for rectangle's width and height
87 if ( mSelectRect.width() == 1 )
88 {
89 mSelectRect.setLeft( mSelectRect.left() + 1 );
90 }
91 if ( mSelectRect.height() == 1 )
92 {
93 mSelectRect.setBottom( mSelectRect.bottom() + 1 );
94 }
95 }
96
97 if ( mRubberBand )
98 {
99 QgsMapToolSelectUtils::setRubberBand( mCanvas, mSelectRect, mRubberBand );
100
101 QgsGeometry selectGeom = mRubberBand->asGeometry();
102 QgsRectangle ext = selectGeom.boundingBox();
103
104 pinUnpinLabels( ext, e );
105
106 mRubberBand->reset( QgsWkbTypes::PolygonGeometry );
107 delete mRubberBand;
108 mRubberBand = nullptr;
109 }
110
111 mDragging = false;
112 }
113
showPinnedLabels(bool show)114 void QgsMapToolPinLabels::showPinnedLabels( bool show )
115 {
116 mShowPinned = show;
117 if ( mShowPinned )
118 {
119 QgsDebugMsg( QStringLiteral( "Toggling on pinned label highlighting" ) );
120 highlightPinnedLabels();
121 }
122 else
123 {
124 QgsDebugMsg( QStringLiteral( "Toggling off pinned label highlighting" ) );
125 removePinnedHighlights();
126 }
127 }
128
129 // public slot to update pinned label highlights on layer edit mode change
updatePinnedLabels()130 void QgsMapToolPinLabels::updatePinnedLabels()
131 {
132 if ( mShowPinned )
133 {
134 QgsDebugMsg( QStringLiteral( "Updating highlighting due to layer editing mode change" ) );
135 highlightPinnedLabels();
136 }
137 }
138
highlightLabel(const QgsLabelPosition & labelpos,const QString & id,const QColor & color)139 void QgsMapToolPinLabels::highlightLabel( const QgsLabelPosition &labelpos,
140 const QString &id,
141 const QColor &color )
142 {
143 QgsRubberBand *rb = new QgsRubberBand( mCanvas, QgsWkbTypes::PolygonGeometry );
144 rb->addPoint( labelpos.cornerPoints.at( 0 ) );
145 rb->addPoint( labelpos.cornerPoints.at( 1 ) );
146 rb->addPoint( labelpos.cornerPoints.at( 2 ) );
147 rb->addPoint( labelpos.cornerPoints.at( 3 ) );
148 rb->addPoint( labelpos.cornerPoints.at( 0 ) );
149 rb->setColor( color );
150 rb->setWidth( 0 );
151 rb->show();
152
153 mHighlights.insert( id, rb );
154 }
155
156 // public slot to render highlight rectangles around pinned labels
highlightPinnedLabels()157 void QgsMapToolPinLabels::highlightPinnedLabels()
158 {
159 removePinnedHighlights();
160
161 if ( !mShowPinned )
162 {
163 return;
164 }
165
166 QgsDebugMsg( QStringLiteral( "Highlighting pinned labels" ) );
167
168 // get list of all drawn labels from all layers within given extent
169 const QgsLabelingResults *labelingResults = mCanvas->labelingResults();
170 if ( !labelingResults )
171 {
172 QgsDebugMsg( QStringLiteral( "No labeling engine" ) );
173 return;
174 }
175
176 QgsRectangle ext = mCanvas->extent();
177 QgsDebugMsg( QStringLiteral( "Getting labels from canvas extent" ) );
178
179 QList<QgsLabelPosition> labelPosList = labelingResults->labelsWithinRect( ext );
180
181 QApplication::setOverrideCursor( Qt::WaitCursor );
182 QList<QgsLabelPosition>::const_iterator it;
183 for ( it = labelPosList.constBegin() ; it != labelPosList.constEnd(); ++it )
184 {
185 const QgsLabelPosition &pos = *it;
186
187 mCurrentLabel = LabelDetails( pos );
188
189 if ( isPinned() )
190 {
191 QString labelStringID = QStringLiteral( "%0|%1|%2" ).arg( QString::number( pos.isDiagram ), pos.layerID, QString::number( pos.featureId ) );
192
193 // don't highlight again
194 if ( mHighlights.contains( labelStringID ) )
195 {
196 continue;
197 }
198
199 QColor lblcolor = QColor( 54, 129, 255, 63 );
200 QgsMapLayer *layer = QgsProject::instance()->mapLayer( pos.layerID );
201 if ( !layer )
202 {
203 continue;
204 }
205 QgsVectorLayer *vlayer = qobject_cast<QgsVectorLayer *>( layer );
206 if ( !vlayer )
207 {
208 QgsDebugMsg( QStringLiteral( "Failed to cast to vector layer" ) );
209 continue;
210 }
211 if ( vlayer->isEditable() )
212 {
213 lblcolor = QColor( 54, 129, 0, 63 );
214 }
215
216 highlightLabel( pos, labelStringID, lblcolor );
217 }
218 }
219 QApplication::restoreOverrideCursor();
220 }
221
removePinnedHighlights()222 void QgsMapToolPinLabels::removePinnedHighlights()
223 {
224 QApplication::setOverrideCursor( Qt::BusyCursor );
225 const auto constMHighlights = mHighlights;
226 for ( QgsRubberBand *rb : constMHighlights )
227 {
228 delete rb;
229 }
230 mHighlights.clear();
231 QApplication::restoreOverrideCursor();
232 }
233
pinUnpinLabels(const QgsRectangle & ext,QMouseEvent * e)234 void QgsMapToolPinLabels::pinUnpinLabels( const QgsRectangle &ext, QMouseEvent *e )
235 {
236 bool doUnpin = e->modifiers() & Qt::ShiftModifier;
237 bool toggleUnpinOrPin = e->modifiers() & Qt::ControlModifier;
238
239 // get list of all drawn labels from all layers within, or touching, chosen extent
240 const QgsLabelingResults *labelingResults = mCanvas->labelingResults();
241 if ( !labelingResults )
242 {
243 QgsDebugMsg( QStringLiteral( "No labeling engine" ) );
244 return;
245 }
246
247 QList<QgsLabelPosition> labelPosList = labelingResults->labelsWithinRect( ext );
248
249 bool labelChanged = false;
250 QList<QgsLabelPosition>::const_iterator it;
251 for ( it = labelPosList.constBegin() ; it != labelPosList.constEnd(); ++it )
252 {
253 const QgsLabelPosition &pos = *it;
254
255 mCurrentLabel = LabelDetails( pos );
256
257 if ( !mCurrentLabel.valid )
258 {
259 QgsDebugMsg( QStringLiteral( "Failed to get label details" ) );
260 continue;
261 }
262
263 // unpin label
264 if ( isPinned() && ( doUnpin || toggleUnpinOrPin ) )
265 {
266 // unpin previously pinned label (set attribute table fields to NULL)
267 if ( pinUnpinCurrentFeature( false ) )
268 {
269 labelChanged = true;
270 }
271 else
272 {
273 QgsDebugMsg( QStringLiteral( "Unpin failed for layer" ) );
274 }
275 }
276 // pin label
277 else if ( !isPinned() && ( !doUnpin || toggleUnpinOrPin ) )
278 {
279 // pin label's location, and optionally rotation, to attribute table
280 if ( pinUnpinCurrentFeature( true ) )
281 {
282 labelChanged = true;
283 }
284 else
285 {
286 QgsDebugMsg( QStringLiteral( "Pin failed for layer" ) );
287 }
288 }
289 }
290
291 if ( labelChanged )
292 {
293 mCurrentLabel.layer->triggerRepaint();
294
295 if ( !mShowPinned )
296 {
297 // toggle it on (pin-unpin tool doesn't work well without it)
298 QgisApp::instance()->actionShowPinnedLabels()->setChecked( true );
299 }
300 }
301 }
302
pinUnpinCurrentLabel(bool pin)303 bool QgsMapToolPinLabels::pinUnpinCurrentLabel( bool pin )
304 {
305 QgsVectorLayer *vlayer = mCurrentLabel.layer;
306 const QgsLabelPosition &labelpos = mCurrentLabel.pos;
307
308 // skip diagrams
309 if ( labelpos.isDiagram )
310 {
311 QgsDebugMsg( QStringLiteral( "Label is diagram, skipping" ) );
312 return false;
313 }
314
315 // verify attribute table has x, y fields mapped
316 int xCol, yCol;
317 double xPosOrig, yPosOrig;
318 bool xSuccess, ySuccess;
319
320 if ( !currentLabelDataDefinedPosition( xPosOrig, xSuccess, yPosOrig, ySuccess, xCol, yCol ) )
321 {
322 QgsDebugMsgLevel( QStringLiteral( "Label X or Y column not mapped, skipping" ), 2 );
323 return false;
324 }
325
326 // rotation field is optional, but will be used if available, unless data exists
327 int rCol;
328 bool rSuccess = false;
329 double defRot;
330
331 bool hasRCol = currentLabelDataDefinedRotation( defRot, rSuccess, rCol, true );
332
333 // get whether to preserve predefined rotation data during label pin/unpin operations
334 bool preserveRot = currentLabelPreserveRotation();
335
336 // edit attribute table
337 int fid = labelpos.featureId;
338
339 bool writeFailed = false;
340 QString labelText = currentLabelText( 24 );
341
342 if ( pin )
343 {
344
345 // QgsPointXY labelpoint = labelpos.cornerPoints.at( 0 );
346
347 QgsPointXY referencePoint;
348 if ( !currentLabelRotationPoint( referencePoint, !preserveRot, false ) )
349 {
350 referencePoint.setX( labelpos.labelRect.xMinimum() );
351 referencePoint.setY( labelpos.labelRect.yMinimum() );
352 }
353
354 double labelX = referencePoint.x();
355 double labelY = referencePoint.y();
356 double labelR = labelpos.rotation * 180 / M_PI;
357
358 // transform back to layer crs
359 QgsPointXY transformedPoint = mCanvas->mapSettings().mapToLayerCoordinates( vlayer, referencePoint );
360 labelX = transformedPoint.x();
361 labelY = transformedPoint.y();
362
363 vlayer->beginEditCommand( tr( "Pinned label" ) + QStringLiteral( " '%1'" ).arg( labelText ) );
364 writeFailed = !vlayer->changeAttributeValue( fid, xCol, labelX );
365 if ( !vlayer->changeAttributeValue( fid, yCol, labelY ) )
366 writeFailed = true;
367 if ( hasRCol && !preserveRot )
368 {
369 if ( !vlayer->changeAttributeValue( fid, rCol, labelR ) )
370 writeFailed = true;
371 }
372 vlayer->endEditCommand();
373 }
374 else
375 {
376 vlayer->beginEditCommand( tr( "Unpinned label" ) + QStringLiteral( " '%1'" ).arg( labelText ) );
377 writeFailed = !vlayer->changeAttributeValue( fid, xCol, QVariant( QString() ) );
378 if ( !vlayer->changeAttributeValue( fid, yCol, QVariant( QString() ) ) )
379 writeFailed = true;
380 if ( hasRCol && !preserveRot )
381 {
382 if ( !vlayer->changeAttributeValue( fid, rCol, QVariant( QString() ) ) )
383 writeFailed = true;
384 }
385 vlayer->endEditCommand();
386 }
387
388 if ( writeFailed )
389 {
390 QgsDebugMsg( QStringLiteral( "Write to attribute table failed" ) );
391
392 #if 0
393 QgsDebugMsg( QStringLiteral( "Undoing and removing failed command from layer's undo stack" ) );
394 int lastCmdIndx = vlayer->undoStack()->count();
395 const QgsUndoCommand *lastCmd = qobject_cast<const QgsUndoCommand *>( vlayer->undoStack()->command( lastCmdIndx ) );
396 if ( lastCmd )
397 {
398 vlayer->undoEditCommand( lastCmd );
399 delete vlayer->undoStack()->command( lastCmdIndx );
400 }
401 #endif
402
403 return false;
404 }
405
406 return true;
407 }
408
pinUnpinCurrentFeature(bool pin)409 bool QgsMapToolPinLabels::pinUnpinCurrentFeature( bool pin )
410 {
411 bool rc = false;
412
413 if ( ! mCurrentLabel.pos.isDiagram )
414 rc = pinUnpinCurrentLabel( pin );
415 else
416 rc = pinUnpinCurrentDiagram( pin );
417
418 return rc;
419 }
420
pinUnpinCurrentDiagram(bool pin)421 bool QgsMapToolPinLabels::pinUnpinCurrentDiagram( bool pin )
422 {
423
424 // skip diagrams
425 if ( ! mCurrentLabel.pos.isDiagram )
426 return false;
427
428 // verify attribute table has x, y fields mapped
429 int xCol, yCol;
430 double xPosOrig, yPosOrig;
431 bool xSuccess, ySuccess;
432
433 if ( !currentLabelDataDefinedPosition( xPosOrig, xSuccess, yPosOrig, ySuccess, xCol, yCol ) )
434 return false;
435
436 // edit attribute table
437 QgsVectorLayer *vlayer = mCurrentLabel.layer;
438 int fid = mCurrentLabel.pos.featureId;
439
440 bool writeFailed = false;
441 QString labelText = currentLabelText( 24 );
442
443 if ( pin )
444 {
445 QgsPointXY referencePoint = mCurrentLabel.pos.labelRect.center();
446 double labelX = referencePoint.x();
447 double labelY = referencePoint.y();
448
449 // transform back to layer crs
450 QgsPointXY transformedPoint = mCanvas->mapSettings().mapToLayerCoordinates( vlayer, referencePoint );
451 labelX = transformedPoint.x();
452 labelY = transformedPoint.y();
453
454 vlayer->beginEditCommand( tr( "Pinned diagram" ) + QStringLiteral( " '%1'" ).arg( labelText ) );
455 writeFailed = !vlayer->changeAttributeValue( fid, xCol, labelX );
456 if ( !vlayer->changeAttributeValue( fid, yCol, labelY ) )
457 writeFailed = true;
458 vlayer->endEditCommand();
459 }
460 else
461 {
462 vlayer->beginEditCommand( tr( "Unpinned diagram" ) + QStringLiteral( " '%1'" ).arg( labelText ) );
463 writeFailed = !vlayer->changeAttributeValue( fid, xCol, QVariant( QString() ) );
464 if ( !vlayer->changeAttributeValue( fid, yCol, QVariant( QString() ) ) )
465 writeFailed = true;
466 vlayer->endEditCommand();
467 }
468
469 return !writeFailed;
470 }
471