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