Branch data Line data Source code
1 : : /***************************************************************************
2 : : qgscallout.cpp
3 : : ----------------
4 : : begin : July 2019
5 : : copyright : (C) 2019 Nyall Dawson
6 : : email : nyall dot dawson at gmail 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 "qgscallout.h"
19 : : #include "qgsrendercontext.h"
20 : : #include "qgssymbol.h"
21 : : #include "qgslinesymbollayer.h"
22 : : #include "qgsfillsymbollayer.h"
23 : : #include "qgssymbollayerutils.h"
24 : : #include "qgsxmlutils.h"
25 : : #include "qgslinestring.h"
26 : : #include "qgslogger.h"
27 : : #include "qgsgeos.h"
28 : : #include "qgsgeometryutils.h"
29 : : #include "qgscircularstring.h"
30 : : #include "qgsshapegenerator.h"
31 : : #include <QPainter>
32 : : #include <mutex>
33 : :
34 : 5 : QgsPropertiesDefinition QgsCallout::sPropertyDefinitions;
35 : :
36 : 0 : void QgsCallout::initPropertyDefinitions()
37 : : {
38 : 0 : const QString origin = QStringLiteral( "callouts" );
39 : :
40 : 0 : sPropertyDefinitions = QgsPropertiesDefinition
41 : 0 : {
42 : 0 : { QgsCallout::MinimumCalloutLength, QgsPropertyDefinition( "MinimumCalloutLength", QObject::tr( "Minimum callout length" ), QgsPropertyDefinition::DoublePositive, origin ) },
43 : 0 : { QgsCallout::OffsetFromAnchor, QgsPropertyDefinition( "OffsetFromAnchor", QObject::tr( "Offset from feature" ), QgsPropertyDefinition::DoublePositive, origin ) },
44 : 0 : { QgsCallout::OffsetFromLabel, QgsPropertyDefinition( "OffsetFromLabel", QObject::tr( "Offset from label" ), QgsPropertyDefinition::DoublePositive, origin ) },
45 : 0 : { QgsCallout::DrawCalloutToAllParts, QgsPropertyDefinition( "DrawCalloutToAllParts", QObject::tr( "Draw lines to all feature parts" ), QgsPropertyDefinition::Boolean, origin ) },
46 : 0 : { QgsCallout::AnchorPointPosition, QgsPropertyDefinition( "AnchorPointPosition", QgsPropertyDefinition::DataTypeString, QObject::tr( "Feature's anchor point position" ), QObject::tr( "string " ) + "[<b>pole_of_inaccessibility</b>|<b>point_on_exterior</b>|<b>point_on_surface</b>|<b>centroid</b>]", origin ) },
47 : 0 : {
48 : 0 : QgsCallout::LabelAnchorPointPosition, QgsPropertyDefinition( "LabelAnchorPointPosition", QgsPropertyDefinition::DataTypeString, QObject::tr( "Label's anchor point position" ), QObject::tr( "string " ) + "[<b>point_on_exterior</b>|<b>centroid</b>|<b>TL</b>=Top left|<b>T</b>=Top middle|"
49 : : "<b>TR</b>=Top right|<br>"
50 : : "<b>L</b>=Left|<b>R</b>=Right|<br>"
51 : : "<b>BL</b>=Bottom left|<b>B</b>=Bottom middle|"
52 : : "<b>BR</b>=Bottom right]", origin )
53 : : },
54 : 0 : { QgsCallout::OriginX, QgsPropertyDefinition( "OriginX", QObject::tr( "Callout origin (X)" ), QgsPropertyDefinition::Double, origin ) },
55 : 0 : { QgsCallout::OriginY, QgsPropertyDefinition( "OriginY", QObject::tr( "Callout origin (Y)" ), QgsPropertyDefinition::Double, origin ) },
56 : 0 : { QgsCallout::DestinationX, QgsPropertyDefinition( "DestinationX", QObject::tr( "Callout destination (X)" ), QgsPropertyDefinition::Double, origin ) },
57 : 0 : { QgsCallout::DestinationY, QgsPropertyDefinition( "DestinationY", QObject::tr( "Callout destination (Y)" ), QgsPropertyDefinition::Double, origin ) },
58 : 0 : { QgsCallout::Curvature, QgsPropertyDefinition( "Curvature", QObject::tr( "Callout line curvature" ), QgsPropertyDefinition::Double, origin ) },
59 : 0 : {
60 : 0 : QgsCallout::Orientation, QgsPropertyDefinition( "Orientation", QgsPropertyDefinition::DataTypeString, QObject::tr( "Callout curve orientation" ), QObject::tr( "string " ) + "[<b>auto</b>|<b>clockwise</b>|<b>counterclockwise</b>]", origin )
61 : : },
62 : 0 : {
63 : 0 : QgsCallout::Margins, QgsPropertyDefinition( "Margins", QgsPropertyDefinition::DataTypeString, QObject::tr( "Margins" ), QObject::tr( "string of four doubles '<b>top,right,bottom,left</b>' or array of doubles <b>[top, right, bottom, left]</b>" ) )
64 : : },
65 : 0 : { QgsCallout::WedgeWidth, QgsPropertyDefinition( "WedgeWidth", QObject::tr( "Wedge width" ), QgsPropertyDefinition::DoublePositive, origin ) },
66 : : };
67 : 0 : }
68 : :
69 : :
70 : 0 : QgsCallout::QgsCallout()
71 : 0 : {
72 : 0 : }
73 : :
74 : 0 : QVariantMap QgsCallout::properties( const QgsReadWriteContext & ) const
75 : : {
76 : 0 : QVariantMap props;
77 : 0 : props.insert( QStringLiteral( "enabled" ), mEnabled ? "1" : "0" );
78 : 0 : props.insert( QStringLiteral( "anchorPoint" ), encodeAnchorPoint( mAnchorPoint ) );
79 : 0 : props.insert( QStringLiteral( "labelAnchorPoint" ), encodeLabelAnchorPoint( mLabelAnchorPoint ) );
80 : 0 : props.insert( QStringLiteral( "ddProperties" ), mDataDefinedProperties.toVariant( propertyDefinitions() ) );
81 : 0 : return props;
82 : 0 : }
83 : :
84 : 0 : void QgsCallout::readProperties( const QVariantMap &props, const QgsReadWriteContext & )
85 : : {
86 : 0 : mEnabled = props.value( QStringLiteral( "enabled" ), QStringLiteral( "0" ) ).toInt();
87 : 0 : mAnchorPoint = decodeAnchorPoint( props.value( QStringLiteral( "anchorPoint" ), QString() ).toString() );
88 : 0 : mLabelAnchorPoint = decodeLabelAnchorPoint( props.value( QStringLiteral( "labelAnchorPoint" ), QString() ).toString() );
89 : 0 : mDataDefinedProperties.loadVariant( props.value( QStringLiteral( "ddProperties" ) ), propertyDefinitions() );
90 : 0 : }
91 : :
92 : 0 : bool QgsCallout::saveProperties( QDomDocument &doc, QDomElement &element, const QgsReadWriteContext &context ) const
93 : : {
94 : 0 : if ( element.isNull() )
95 : : {
96 : 0 : return false;
97 : : }
98 : :
99 : 0 : QDomElement calloutPropsElement = QgsXmlUtils::writeVariant( properties( context ), doc );
100 : :
101 : 0 : QDomElement calloutElement = doc.createElement( QStringLiteral( "callout" ) );
102 : 0 : calloutElement.setAttribute( QStringLiteral( "type" ), type() );
103 : 0 : calloutElement.appendChild( calloutPropsElement );
104 : :
105 : 0 : element.appendChild( calloutElement );
106 : 0 : return true;
107 : 0 : }
108 : :
109 : 0 : void QgsCallout::restoreProperties( const QDomElement &element, const QgsReadWriteContext &context )
110 : : {
111 : 0 : const QVariantMap props = QgsXmlUtils::readVariant( element.firstChildElement() ).toMap();
112 : 0 : readProperties( props, context );
113 : 0 : }
114 : :
115 : 0 : void QgsCallout::startRender( QgsRenderContext & )
116 : : {
117 : :
118 : 0 : }
119 : 0 : void QgsCallout::stopRender( QgsRenderContext & )
120 : : {
121 : :
122 : 0 : }
123 : :
124 : 0 : QSet<QString> QgsCallout::referencedFields( const QgsRenderContext &context ) const
125 : : {
126 : 0 : mDataDefinedProperties.prepare( context.expressionContext() );
127 : 0 : return mDataDefinedProperties.referencedFields( context.expressionContext() );
128 : : }
129 : :
130 : 0 : QgsCallout::DrawOrder QgsCallout::drawOrder() const
131 : : {
132 : 0 : return OrderBelowAllLabels;
133 : : }
134 : :
135 : 0 : void QgsCallout::render( QgsRenderContext &context, const QRectF &rect, const double angle, const QgsGeometry &anchor, QgsCalloutContext &calloutContext )
136 : : {
137 : 0 : if ( !mEnabled )
138 : 0 : return;
139 : :
140 : : #if 0 // for debugging
141 : : QPainter *painter = context.painter();
142 : : painter->save();
143 : : painter->setRenderHint( QPainter::Antialiasing, false );
144 : : painter->translate( rect.center() );
145 : : painter->rotate( -angle );
146 : :
147 : : painter->setBrush( QColor( 255, 0, 0, 100 ) );
148 : : painter->setPen( QColor( 255, 0, 0, 150 ) );
149 : :
150 : : painter->drawRect( rect.width() * -0.5, rect.height() * -0.5, rect.width(), rect.height() );
151 : : painter->restore();
152 : :
153 : : painter->setBrush( QColor( 0, 255, 0, 100 ) );
154 : : painter->setPen( QColor( 0, 255, 0, 150 ) );
155 : :
156 : : painter->drawRect( anchor.boundingBox( ).buffered( 30 ).toRectF() );
157 : : #endif
158 : :
159 : 0 : draw( context, rect, angle, anchor, calloutContext );
160 : 0 : }
161 : :
162 : 0 : void QgsCallout::setEnabled( bool enabled )
163 : : {
164 : 0 : mEnabled = enabled;
165 : 0 : }
166 : :
167 : 0 : QgsPropertiesDefinition QgsCallout::propertyDefinitions()
168 : : {
169 : : static std::once_flag initialized;
170 : 0 : std::call_once( initialized, [ = ]( )
171 : : {
172 : 0 : initPropertyDefinitions();
173 : 0 : } );
174 : 0 : return sPropertyDefinitions;
175 : : }
176 : :
177 : 0 : QgsCallout::AnchorPoint QgsCallout::decodeAnchorPoint( const QString &name, bool *ok )
178 : : {
179 : 0 : if ( ok )
180 : 0 : *ok = true;
181 : 0 : QString cleaned = name.toLower().trimmed();
182 : :
183 : 0 : if ( cleaned == QLatin1String( "pole_of_inaccessibility" ) )
184 : 0 : return PoleOfInaccessibility;
185 : 0 : else if ( cleaned == QLatin1String( "point_on_exterior" ) )
186 : 0 : return PointOnExterior;
187 : 0 : else if ( cleaned == QLatin1String( "point_on_surface" ) )
188 : 0 : return PointOnSurface;
189 : 0 : else if ( cleaned == QLatin1String( "centroid" ) )
190 : 0 : return Centroid;
191 : :
192 : 0 : if ( ok )
193 : 0 : *ok = false;
194 : 0 : return PoleOfInaccessibility;
195 : 0 : }
196 : :
197 : 0 : QString QgsCallout::encodeAnchorPoint( AnchorPoint anchor )
198 : : {
199 : 0 : switch ( anchor )
200 : : {
201 : : case PoleOfInaccessibility:
202 : 0 : return QStringLiteral( "pole_of_inaccessibility" );
203 : : case PointOnExterior:
204 : 0 : return QStringLiteral( "point_on_exterior" );
205 : : case PointOnSurface:
206 : 0 : return QStringLiteral( "point_on_surface" );
207 : : case Centroid:
208 : 0 : return QStringLiteral( "centroid" );
209 : : }
210 : 0 : return QString();
211 : 0 : }
212 : :
213 : 0 : QString QgsCallout::encodeLabelAnchorPoint( QgsCallout::LabelAnchorPoint anchor )
214 : : {
215 : 0 : switch ( anchor )
216 : : {
217 : : case LabelPointOnExterior:
218 : 0 : return QStringLiteral( "point_on_exterior" );
219 : : case LabelCentroid:
220 : 0 : return QStringLiteral( "centroid" );
221 : : case LabelTopLeft:
222 : 0 : return QStringLiteral( "tl" );
223 : : case LabelTopMiddle:
224 : 0 : return QStringLiteral( "t" );
225 : : case LabelTopRight:
226 : 0 : return QStringLiteral( "tr" );
227 : : case LabelMiddleLeft:
228 : 0 : return QStringLiteral( "l" );
229 : : case LabelMiddleRight:
230 : 0 : return QStringLiteral( "r" );
231 : : case LabelBottomLeft:
232 : 0 : return QStringLiteral( "bl" );
233 : : case LabelBottomMiddle:
234 : 0 : return QStringLiteral( "b" );
235 : : case LabelBottomRight:
236 : 0 : return QStringLiteral( "br" );
237 : : }
238 : :
239 : 0 : return QString();
240 : 0 : }
241 : :
242 : 0 : QgsCallout::LabelAnchorPoint QgsCallout::decodeLabelAnchorPoint( const QString &name, bool *ok )
243 : : {
244 : 0 : if ( ok )
245 : 0 : *ok = true;
246 : 0 : QString cleaned = name.toLower().trimmed();
247 : :
248 : 0 : if ( cleaned == QLatin1String( "point_on_exterior" ) )
249 : 0 : return LabelPointOnExterior;
250 : 0 : else if ( cleaned == QLatin1String( "centroid" ) )
251 : 0 : return LabelCentroid;
252 : 0 : else if ( cleaned == QLatin1String( "tl" ) )
253 : 0 : return LabelTopLeft;
254 : 0 : else if ( cleaned == QLatin1String( "t" ) )
255 : 0 : return LabelTopMiddle;
256 : 0 : else if ( cleaned == QLatin1String( "tr" ) )
257 : 0 : return LabelTopRight;
258 : 0 : else if ( cleaned == QLatin1String( "l" ) )
259 : 0 : return LabelMiddleLeft;
260 : 0 : else if ( cleaned == QLatin1String( "r" ) )
261 : 0 : return LabelMiddleRight;
262 : 0 : else if ( cleaned == QLatin1String( "bl" ) )
263 : 0 : return LabelBottomLeft;
264 : 0 : else if ( cleaned == QLatin1String( "b" ) )
265 : 0 : return LabelBottomMiddle;
266 : 0 : else if ( cleaned == QLatin1String( "br" ) )
267 : 0 : return LabelBottomRight;
268 : :
269 : 0 : if ( ok )
270 : 0 : *ok = false;
271 : 0 : return LabelPointOnExterior;
272 : 0 : }
273 : :
274 : 0 : QgsGeometry QgsCallout::labelAnchorGeometry( const QRectF &rect, const double angle, LabelAnchorPoint anchor ) const
275 : : {
276 : 0 : QgsGeometry label;
277 : 0 : switch ( anchor )
278 : : {
279 : : case LabelPointOnExterior:
280 : 0 : label = QgsGeometry::fromRect( rect );
281 : 0 : break;
282 : :
283 : : case LabelCentroid:
284 : 0 : label = QgsGeometry::fromRect( rect ).centroid();
285 : 0 : break;
286 : :
287 : : case LabelTopLeft:
288 : 0 : label = QgsGeometry::fromPointXY( QgsPointXY( rect.bottomLeft() ) );
289 : 0 : break;
290 : :
291 : : case LabelTopMiddle:
292 : 0 : label = QgsGeometry::fromPointXY( QgsPointXY( ( rect.left() + rect.right() ) / 2.0, rect.bottom() ) );
293 : 0 : break;
294 : :
295 : : case LabelTopRight:
296 : 0 : label = QgsGeometry::fromPointXY( QgsPointXY( rect.bottomRight() ) );
297 : 0 : break;
298 : :
299 : : case LabelMiddleLeft:
300 : 0 : label = QgsGeometry::fromPointXY( QgsPointXY( rect.left(), ( rect.top() + rect.bottom() ) / 2.0 ) );
301 : 0 : break;
302 : :
303 : : case LabelMiddleRight:
304 : 0 : label = QgsGeometry::fromPointXY( QgsPointXY( rect.right(), ( rect.top() + rect.bottom() ) / 2.0 ) );
305 : 0 : break;
306 : :
307 : : case LabelBottomLeft:
308 : 0 : label = QgsGeometry::fromPointXY( QgsPointXY( rect.topLeft() ) );
309 : 0 : break;
310 : :
311 : : case LabelBottomMiddle:
312 : 0 : label = QgsGeometry::fromPointXY( QgsPointXY( ( rect.left() + rect.right() ) / 2.0, rect.top() ) );
313 : 0 : break;
314 : :
315 : : case LabelBottomRight:
316 : 0 : label = QgsGeometry::fromPointXY( QgsPointXY( rect.topRight() ) );
317 : 0 : break;
318 : : }
319 : :
320 : 0 : label.rotate( angle, rect.topLeft() );
321 : 0 : return label;
322 : 0 : }
323 : :
324 : 0 : QgsGeometry QgsCallout::calloutLabelPoint( const QRectF &rect, const double angle, QgsCallout::LabelAnchorPoint anchor, QgsRenderContext &context, const QgsCallout::QgsCalloutContext &calloutContext, bool &pinned ) const
325 : : {
326 : 0 : pinned = false;
327 : 0 : if ( dataDefinedProperties().isActive( QgsCallout::OriginX ) && dataDefinedProperties().isActive( QgsCallout::OriginY ) )
328 : : {
329 : 0 : bool ok = false;
330 : 0 : const double x = dataDefinedProperties().valueAsDouble( QgsCallout::OriginX, context.expressionContext(), 0, &ok );
331 : 0 : if ( ok )
332 : : {
333 : 0 : const double y = dataDefinedProperties().valueAsDouble( QgsCallout::OriginY, context.expressionContext(), 0, &ok );
334 : 0 : if ( ok )
335 : : {
336 : 0 : pinned = true;
337 : : // data defined label point, use it directly
338 : 0 : QgsGeometry labelPoint = QgsGeometry::fromPointXY( QgsPointXY( x, y ) );
339 : : try
340 : : {
341 : 0 : labelPoint.transform( calloutContext.originalFeatureToMapTransform( context ) );
342 : 0 : labelPoint.transform( context.mapToPixel().transform() );
343 : 0 : }
344 : : catch ( QgsCsException & )
345 : : {
346 : 0 : return QgsGeometry();
347 : 0 : }
348 : 0 : return labelPoint;
349 : 0 : }
350 : 0 : }
351 : 0 : }
352 : :
353 : 0 : QgsGeometry label;
354 : 0 : switch ( anchor )
355 : : {
356 : : case LabelPointOnExterior:
357 : 0 : label = QgsGeometry::fromRect( rect );
358 : 0 : break;
359 : :
360 : : case LabelCentroid:
361 : 0 : label = QgsGeometry::fromRect( rect ).centroid();
362 : 0 : break;
363 : :
364 : : case LabelTopLeft:
365 : 0 : label = QgsGeometry::fromPointXY( QgsPointXY( rect.bottomLeft() ) );
366 : 0 : break;
367 : :
368 : : case LabelTopMiddle:
369 : 0 : label = QgsGeometry::fromPointXY( QgsPointXY( ( rect.left() + rect.right() ) / 2.0, rect.bottom() ) );
370 : 0 : break;
371 : :
372 : : case LabelTopRight:
373 : 0 : label = QgsGeometry::fromPointXY( QgsPointXY( rect.bottomRight() ) );
374 : 0 : break;
375 : :
376 : : case LabelMiddleLeft:
377 : 0 : label = QgsGeometry::fromPointXY( QgsPointXY( rect.left(), ( rect.top() + rect.bottom() ) / 2.0 ) );
378 : 0 : break;
379 : :
380 : : case LabelMiddleRight:
381 : 0 : label = QgsGeometry::fromPointXY( QgsPointXY( rect.right(), ( rect.top() + rect.bottom() ) / 2.0 ) );
382 : 0 : break;
383 : :
384 : : case LabelBottomLeft:
385 : 0 : label = QgsGeometry::fromPointXY( QgsPointXY( rect.topLeft() ) );
386 : 0 : break;
387 : :
388 : : case LabelBottomMiddle:
389 : 0 : label = QgsGeometry::fromPointXY( QgsPointXY( ( rect.left() + rect.right() ) / 2.0, rect.top() ) );
390 : 0 : break;
391 : :
392 : : case LabelBottomRight:
393 : 0 : label = QgsGeometry::fromPointXY( QgsPointXY( rect.topRight() ) );
394 : 0 : break;
395 : : }
396 : :
397 : 0 : label.rotate( angle, rect.topLeft() );
398 : 0 : return label;
399 : 0 : }
400 : :
401 : 0 : QgsGeometry QgsCallout::calloutLineToPart( const QgsGeometry &labelGeometry, const QgsAbstractGeometry *partGeometry, QgsRenderContext &context, const QgsCalloutContext &calloutContext, bool &pinned ) const
402 : : {
403 : 0 : pinned = false;
404 : 0 : AnchorPoint anchor = anchorPoint();
405 : 0 : const QgsAbstractGeometry *evaluatedPartAnchor = partGeometry;
406 : 0 : std::unique_ptr< QgsAbstractGeometry > tempPartAnchor;
407 : :
408 : 0 : if ( dataDefinedProperties().isActive( QgsCallout::DestinationX ) && dataDefinedProperties().isActive( QgsCallout::DestinationY ) )
409 : : {
410 : 0 : bool ok = false;
411 : 0 : const double x = dataDefinedProperties().valueAsDouble( QgsCallout::DestinationX, context.expressionContext(), 0, &ok );
412 : 0 : if ( ok )
413 : : {
414 : 0 : const double y = dataDefinedProperties().valueAsDouble( QgsCallout::DestinationY, context.expressionContext(), 0, &ok );
415 : 0 : if ( ok )
416 : : {
417 : 0 : pinned = true;
418 : 0 : tempPartAnchor = std::make_unique< QgsPoint >( QgsWkbTypes::Point, x, y );
419 : 0 : evaluatedPartAnchor = tempPartAnchor.get();
420 : : try
421 : : {
422 : 0 : tempPartAnchor->transform( calloutContext.originalFeatureToMapTransform( context ) );
423 : 0 : tempPartAnchor->transform( context.mapToPixel().transform() );
424 : 0 : }
425 : : catch ( QgsCsException & )
426 : : {
427 : 0 : evaluatedPartAnchor = partGeometry;
428 : 0 : }
429 : 0 : }
430 : 0 : }
431 : 0 : }
432 : :
433 : 0 : if ( dataDefinedProperties().isActive( QgsCallout::AnchorPointPosition ) )
434 : : {
435 : 0 : QString encodedAnchor = encodeAnchorPoint( anchor );
436 : 0 : context.expressionContext().setOriginalValueVariable( encodedAnchor );
437 : 0 : anchor = decodeAnchorPoint( dataDefinedProperties().valueAsString( QgsCallout::AnchorPointPosition, context.expressionContext(), encodedAnchor ) );
438 : 0 : }
439 : :
440 : 0 : QgsGeometry line;
441 : 0 : QgsGeos labelGeos( labelGeometry.constGet() );
442 : :
443 : 0 : switch ( QgsWkbTypes::geometryType( evaluatedPartAnchor->wkbType() ) )
444 : : {
445 : : case QgsWkbTypes::PointGeometry:
446 : : case QgsWkbTypes::LineGeometry:
447 : : {
448 : 0 : line = labelGeos.shortestLine( evaluatedPartAnchor );
449 : 0 : break;
450 : : }
451 : :
452 : : case QgsWkbTypes::PolygonGeometry:
453 : : {
454 : 0 : if ( labelGeos.intersects( evaluatedPartAnchor ) )
455 : 0 : return QgsGeometry();
456 : :
457 : : // ideally avoid this unwanted clone in future. For now we need it because poleOfInaccessibility/pointOnSurface are
458 : : // only available to QgsGeometry objects
459 : 0 : QgsGeometry evaluatedPartAnchorGeom( evaluatedPartAnchor->clone() );
460 : 0 : switch ( anchor )
461 : : {
462 : 0 : case QgsCallout::PoleOfInaccessibility:
463 : 0 : line = labelGeos.shortestLine( evaluatedPartAnchorGeom.poleOfInaccessibility( std::max( evaluatedPartAnchor->boundingBox().width(), evaluatedPartAnchor->boundingBox().height() ) / 20.0 ) ); // really rough (but quick) pole of inaccessibility
464 : 0 : break;
465 : 0 : case QgsCallout::PointOnSurface:
466 : 0 : line = labelGeos.shortestLine( evaluatedPartAnchorGeom.pointOnSurface() );
467 : 0 : break;
468 : : case QgsCallout::PointOnExterior:
469 : 0 : line = labelGeos.shortestLine( evaluatedPartAnchor );
470 : 0 : break;
471 : : case QgsCallout::Centroid:
472 : 0 : line = labelGeos.shortestLine( evaluatedPartAnchorGeom.centroid() );
473 : 0 : break;
474 : : }
475 : : break;
476 : 0 : }
477 : :
478 : : case QgsWkbTypes::NullGeometry:
479 : : case QgsWkbTypes::UnknownGeometry:
480 : 0 : return QgsGeometry(); // shouldn't even get here..
481 : : }
482 : 0 : return line;
483 : 0 : }
484 : :
485 : : //
486 : : // QgsCallout::QgsCalloutContext
487 : : //
488 : :
489 : 0 : QgsCoordinateTransform QgsCallout::QgsCalloutContext::originalFeatureToMapTransform( const QgsRenderContext &renderContext ) const
490 : : {
491 : 0 : if ( !mOriginalFeatureToMapTransform.isValid() )
492 : : {
493 : : // lazy initialization, only create if needed...
494 : 0 : mOriginalFeatureToMapTransform = QgsCoordinateTransform( originalFeatureCrs, renderContext.coordinateTransform().destinationCrs(), renderContext.transformContext() );
495 : 0 : }
496 : 0 : return mOriginalFeatureToMapTransform;
497 : 0 : }
498 : :
499 : :
500 : : //
501 : : // QgsSimpleLineCallout
502 : : //
503 : :
504 : 0 : QgsSimpleLineCallout::QgsSimpleLineCallout()
505 : 0 : {
506 : 0 : mLineSymbol = std::make_unique< QgsLineSymbol >( QgsSymbolLayerList() << new QgsSimpleLineSymbolLayer( QColor( 60, 60, 60 ), .3 ) );
507 : :
508 : 0 : }
509 : :
510 : 0 : QgsSimpleLineCallout::~QgsSimpleLineCallout() = default;
511 : :
512 : 0 : QgsSimpleLineCallout::QgsSimpleLineCallout( const QgsSimpleLineCallout &other )
513 : 0 : : QgsCallout( other )
514 : 0 : , mLineSymbol( other.mLineSymbol ? other.mLineSymbol->clone() : nullptr )
515 : 0 : , mMinCalloutLength( other.mMinCalloutLength )
516 : 0 : , mMinCalloutLengthUnit( other.mMinCalloutLengthUnit )
517 : 0 : , mMinCalloutLengthScale( other.mMinCalloutLengthScale )
518 : 0 : , mOffsetFromAnchorDistance( other.mOffsetFromAnchorDistance )
519 : 0 : , mOffsetFromAnchorUnit( other.mOffsetFromAnchorUnit )
520 : 0 : , mOffsetFromAnchorScale( other.mOffsetFromAnchorScale )
521 : 0 : , mOffsetFromLabelDistance( other.mOffsetFromLabelDistance )
522 : 0 : , mOffsetFromLabelUnit( other.mOffsetFromLabelUnit )
523 : 0 : , mOffsetFromLabelScale( other.mOffsetFromLabelScale )
524 : 0 : , mDrawCalloutToAllParts( other.mDrawCalloutToAllParts )
525 : 0 : {
526 : :
527 : 0 : }
528 : :
529 : 0 : QgsCallout *QgsSimpleLineCallout::create( const QVariantMap &properties, const QgsReadWriteContext &context )
530 : : {
531 : 0 : std::unique_ptr< QgsSimpleLineCallout > callout = std::make_unique< QgsSimpleLineCallout >();
532 : 0 : callout->readProperties( properties, context );
533 : 0 : return callout.release();
534 : 0 : }
535 : :
536 : 0 : QString QgsSimpleLineCallout::type() const
537 : : {
538 : 0 : return QStringLiteral( "simple" );
539 : : }
540 : :
541 : 0 : QgsSimpleLineCallout *QgsSimpleLineCallout::clone() const
542 : : {
543 : 0 : return new QgsSimpleLineCallout( *this );
544 : 0 : }
545 : :
546 : 0 : QVariantMap QgsSimpleLineCallout::properties( const QgsReadWriteContext &context ) const
547 : : {
548 : 0 : QVariantMap props = QgsCallout::properties( context );
549 : :
550 : 0 : if ( mLineSymbol )
551 : : {
552 : 0 : props[ QStringLiteral( "lineSymbol" ) ] = QgsSymbolLayerUtils::symbolProperties( mLineSymbol.get() );
553 : 0 : }
554 : 0 : props[ QStringLiteral( "minLength" ) ] = mMinCalloutLength;
555 : 0 : props[ QStringLiteral( "minLengthUnit" ) ] = QgsUnitTypes::encodeUnit( mMinCalloutLengthUnit );
556 : 0 : props[ QStringLiteral( "minLengthMapUnitScale" ) ] = QgsSymbolLayerUtils::encodeMapUnitScale( mMinCalloutLengthScale );
557 : :
558 : 0 : props[ QStringLiteral( "offsetFromAnchor" ) ] = mOffsetFromAnchorDistance;
559 : 0 : props[ QStringLiteral( "offsetFromAnchorUnit" ) ] = QgsUnitTypes::encodeUnit( mOffsetFromAnchorUnit );
560 : 0 : props[ QStringLiteral( "offsetFromAnchorMapUnitScale" ) ] = QgsSymbolLayerUtils::encodeMapUnitScale( mOffsetFromAnchorScale );
561 : 0 : props[ QStringLiteral( "offsetFromLabel" ) ] = mOffsetFromLabelDistance;
562 : 0 : props[ QStringLiteral( "offsetFromLabelUnit" ) ] = QgsUnitTypes::encodeUnit( mOffsetFromLabelUnit );
563 : 0 : props[ QStringLiteral( "offsetFromLabelMapUnitScale" ) ] = QgsSymbolLayerUtils::encodeMapUnitScale( mOffsetFromLabelScale );
564 : :
565 : 0 : props[ QStringLiteral( "drawToAllParts" ) ] = mDrawCalloutToAllParts;
566 : :
567 : 0 : return props;
568 : 0 : }
569 : :
570 : 0 : void QgsSimpleLineCallout::readProperties( const QVariantMap &props, const QgsReadWriteContext &context )
571 : : {
572 : 0 : QgsCallout::readProperties( props, context );
573 : :
574 : 0 : const QString lineSymbolDef = props.value( QStringLiteral( "lineSymbol" ) ).toString();
575 : 0 : QDomDocument doc( QStringLiteral( "symbol" ) );
576 : 0 : doc.setContent( lineSymbolDef );
577 : 0 : QDomElement symbolElem = doc.firstChildElement( QStringLiteral( "symbol" ) );
578 : 0 : std::unique_ptr< QgsLineSymbol > lineSymbol( QgsSymbolLayerUtils::loadSymbol< QgsLineSymbol >( symbolElem, context ) );
579 : 0 : if ( lineSymbol )
580 : 0 : mLineSymbol = std::move( lineSymbol );
581 : :
582 : 0 : mMinCalloutLength = props.value( QStringLiteral( "minLength" ), 0 ).toDouble();
583 : 0 : mMinCalloutLengthUnit = QgsUnitTypes::decodeRenderUnit( props.value( QStringLiteral( "minLengthUnit" ) ).toString() );
584 : 0 : mMinCalloutLengthScale = QgsSymbolLayerUtils::decodeMapUnitScale( props.value( QStringLiteral( "minLengthMapUnitScale" ) ).toString() );
585 : :
586 : 0 : mOffsetFromAnchorDistance = props.value( QStringLiteral( "offsetFromAnchor" ), 0 ).toDouble();
587 : 0 : mOffsetFromAnchorUnit = QgsUnitTypes::decodeRenderUnit( props.value( QStringLiteral( "offsetFromAnchorUnit" ) ).toString() );
588 : 0 : mOffsetFromAnchorScale = QgsSymbolLayerUtils::decodeMapUnitScale( props.value( QStringLiteral( "offsetFromAnchorMapUnitScale" ) ).toString() );
589 : 0 : mOffsetFromLabelDistance = props.value( QStringLiteral( "offsetFromLabel" ), 0 ).toDouble();
590 : 0 : mOffsetFromLabelUnit = QgsUnitTypes::decodeRenderUnit( props.value( QStringLiteral( "offsetFromLabelUnit" ) ).toString() );
591 : 0 : mOffsetFromLabelScale = QgsSymbolLayerUtils::decodeMapUnitScale( props.value( QStringLiteral( "offsetFromLabelMapUnitScale" ) ).toString() );
592 : :
593 : 0 : mDrawCalloutToAllParts = props.value( QStringLiteral( "drawToAllParts" ), false ).toBool();
594 : 0 : }
595 : :
596 : 0 : void QgsSimpleLineCallout::startRender( QgsRenderContext &context )
597 : : {
598 : 0 : QgsCallout::startRender( context );
599 : 0 : if ( mLineSymbol )
600 : 0 : mLineSymbol->startRender( context );
601 : 0 : }
602 : :
603 : 0 : void QgsSimpleLineCallout::stopRender( QgsRenderContext &context )
604 : : {
605 : 0 : QgsCallout::stopRender( context );
606 : 0 : if ( mLineSymbol )
607 : 0 : mLineSymbol->stopRender( context );
608 : 0 : }
609 : :
610 : 0 : QSet<QString> QgsSimpleLineCallout::referencedFields( const QgsRenderContext &context ) const
611 : : {
612 : 0 : QSet<QString> fields = QgsCallout::referencedFields( context );
613 : 0 : if ( mLineSymbol )
614 : 0 : fields.unite( mLineSymbol->usedAttributes( context ) );
615 : 0 : return fields;
616 : 0 : }
617 : :
618 : 0 : QgsLineSymbol *QgsSimpleLineCallout::lineSymbol()
619 : : {
620 : 0 : return mLineSymbol.get();
621 : : }
622 : :
623 : 0 : void QgsSimpleLineCallout::setLineSymbol( QgsLineSymbol *symbol )
624 : : {
625 : 0 : mLineSymbol.reset( symbol );
626 : 0 : }
627 : :
628 : 0 : void QgsSimpleLineCallout::draw( QgsRenderContext &context, const QRectF &rect, const double angle, const QgsGeometry &anchor, QgsCalloutContext &calloutContext )
629 : : {
630 : 0 : LabelAnchorPoint labelAnchor = labelAnchorPoint();
631 : 0 : if ( dataDefinedProperties().isActive( QgsCallout::LabelAnchorPointPosition ) )
632 : : {
633 : 0 : QString encodedAnchor = encodeLabelAnchorPoint( labelAnchor );
634 : 0 : context.expressionContext().setOriginalValueVariable( encodedAnchor );
635 : 0 : labelAnchor = decodeLabelAnchorPoint( dataDefinedProperties().valueAsString( QgsCallout::LabelAnchorPointPosition, context.expressionContext(), encodedAnchor ) );
636 : 0 : }
637 : :
638 : 0 : bool originPinned = false;
639 : 0 : const QgsGeometry label = calloutLabelPoint( rect, angle, labelAnchor, context, calloutContext, originPinned );
640 : 0 : if ( label.isNull() )
641 : 0 : return;
642 : :
643 : 0 : auto drawCalloutLine = [this, &context, &calloutContext, &label, &rect, angle, &anchor, originPinned]( const QgsAbstractGeometry * partAnchor )
644 : : {
645 : 0 : bool destinationPinned = false;
646 : 0 : QgsGeometry line = calloutLineToPart( label, partAnchor, context, calloutContext, destinationPinned );
647 : 0 : if ( line.isEmpty() )
648 : 0 : return;
649 : :
650 : 0 : const double lineLength = line.length();
651 : 0 : if ( qgsDoubleNear( lineLength, 0 ) )
652 : 0 : return;
653 : :
654 : 0 : double minLength = mMinCalloutLength;
655 : 0 : if ( dataDefinedProperties().isActive( QgsCallout::MinimumCalloutLength ) )
656 : : {
657 : 0 : context.expressionContext().setOriginalValueVariable( minLength );
658 : 0 : minLength = dataDefinedProperties().valueAsDouble( QgsCallout::MinimumCalloutLength, context.expressionContext(), minLength );
659 : 0 : }
660 : 0 : double minLengthPixels = context.convertToPainterUnits( minLength, mMinCalloutLengthUnit, mMinCalloutLengthScale );
661 : 0 : if ( minLengthPixels > 0 && lineLength < minLengthPixels )
662 : 0 : return; // too small!
663 : :
664 : 0 : std::unique_ptr< QgsCurve > calloutCurve( createCalloutLine( qgsgeometry_cast< const QgsLineString * >( line.constGet() )->startPoint(),
665 : 0 : qgsgeometry_cast< const QgsLineString * >( line.constGet() )->endPoint(), context, rect, angle, anchor, calloutContext ) );
666 : :
667 : 0 : double offsetFromAnchor = mOffsetFromAnchorDistance;
668 : 0 : if ( dataDefinedProperties().isActive( QgsCallout::OffsetFromAnchor ) )
669 : : {
670 : 0 : context.expressionContext().setOriginalValueVariable( offsetFromAnchor );
671 : 0 : offsetFromAnchor = dataDefinedProperties().valueAsDouble( QgsCallout::OffsetFromAnchor, context.expressionContext(), offsetFromAnchor );
672 : 0 : }
673 : 0 : const double offsetFromAnchorPixels = context.convertToPainterUnits( offsetFromAnchor, mOffsetFromAnchorUnit, mOffsetFromAnchorScale );
674 : :
675 : 0 : double offsetFromLabel = mOffsetFromLabelDistance;
676 : 0 : if ( dataDefinedProperties().isActive( QgsCallout::OffsetFromLabel ) )
677 : : {
678 : 0 : context.expressionContext().setOriginalValueVariable( offsetFromLabel );
679 : 0 : offsetFromLabel = dataDefinedProperties().valueAsDouble( QgsCallout::OffsetFromLabel, context.expressionContext(), offsetFromLabel );
680 : 0 : }
681 : 0 : const double offsetFromLabelPixels = context.convertToPainterUnits( offsetFromLabel, mOffsetFromLabelUnit, mOffsetFromLabelScale );
682 : 0 : if ( offsetFromAnchorPixels > 0 || offsetFromLabelPixels > 0 )
683 : : {
684 : 0 : calloutCurve.reset( calloutCurve->curveSubstring( offsetFromLabelPixels, calloutCurve->length() - offsetFromAnchorPixels ) );
685 : 0 : }
686 : :
687 : 0 : const QPolygonF points = calloutCurve->asQPolygonF();
688 : :
689 : 0 : if ( points.empty() )
690 : 0 : return;
691 : :
692 : 0 : QgsCalloutPosition position;
693 : 0 : position.setOrigin( context.mapToPixel().toMapCoordinates( points.at( 0 ).x(), points.at( 0 ).y() ).toQPointF() );
694 : 0 : position.setOriginIsPinned( originPinned );
695 : 0 : position.setDestination( context.mapToPixel().toMapCoordinates( points.constLast().x(), points.constLast().y() ).toQPointF() );
696 : 0 : position.setDestinationIsPinned( destinationPinned );
697 : 0 : calloutContext.addCalloutPosition( position );
698 : 0 :
699 : 0 : mLineSymbol->renderPolyline( points, nullptr, context );
700 : 0 : };
701 : 0 :
702 : 0 : bool toAllParts = mDrawCalloutToAllParts;
703 : 0 : if ( dataDefinedProperties().isActive( QgsCallout::DrawCalloutToAllParts ) )
704 : : {
705 : 0 : context.expressionContext().setOriginalValueVariable( toAllParts );
706 : 0 : toAllParts = dataDefinedProperties().valueAsBool( QgsCallout::DrawCalloutToAllParts, context.expressionContext(), toAllParts );
707 : 0 : }
708 : :
709 : 0 : if ( calloutContext.allFeaturePartsLabeled || !toAllParts )
710 : 0 : drawCalloutLine( anchor.constGet() );
711 : : else
712 : : {
713 : 0 : for ( auto it = anchor.const_parts_begin(); it != anchor.const_parts_end(); ++it )
714 : 0 : drawCalloutLine( *it );
715 : : }
716 : 0 : }
717 : :
718 : 0 : QgsCurve *QgsSimpleLineCallout::createCalloutLine( const QgsPoint &start, const QgsPoint &end, QgsRenderContext &, const QRectF &, const double, const QgsGeometry &, QgsCallout::QgsCalloutContext & ) const
719 : : {
720 : 0 : return new QgsLineString( start, end );
721 : 0 : }
722 : :
723 : : //
724 : : // QgsManhattanLineCallout
725 : : //
726 : :
727 : 0 : QgsManhattanLineCallout::QgsManhattanLineCallout()
728 : 0 : {
729 : 0 : }
730 : :
731 : 0 : QgsManhattanLineCallout::QgsManhattanLineCallout( const QgsManhattanLineCallout &other )
732 : 0 : : QgsSimpleLineCallout( other )
733 : 0 : {
734 : :
735 : 0 : }
736 : :
737 : :
738 : 0 : QgsCallout *QgsManhattanLineCallout::create( const QVariantMap &properties, const QgsReadWriteContext &context )
739 : : {
740 : 0 : std::unique_ptr< QgsManhattanLineCallout > callout = std::make_unique< QgsManhattanLineCallout >();
741 : 0 : callout->readProperties( properties, context );
742 : 0 : return callout.release();
743 : 0 : }
744 : :
745 : 0 : QString QgsManhattanLineCallout::type() const
746 : : {
747 : 0 : return QStringLiteral( "manhattan" );
748 : : }
749 : :
750 : 0 : QgsManhattanLineCallout *QgsManhattanLineCallout::clone() const
751 : : {
752 : 0 : return new QgsManhattanLineCallout( *this );
753 : 0 : }
754 : :
755 : 0 : QgsCurve *QgsManhattanLineCallout::createCalloutLine( const QgsPoint &start, const QgsPoint &end, QgsRenderContext &, const QRectF &, const double, const QgsGeometry &, QgsCallout::QgsCalloutContext & ) const
756 : : {
757 : 0 : QgsPoint mid1 = QgsPoint( start.x(), end.y() );
758 : 0 : return new QgsLineString( QVector< QgsPoint >() << start << mid1 << end );
759 : 0 : }
760 : :
761 : :
762 : : //
763 : : // QgsCurvedLineCallout
764 : : //
765 : :
766 : 0 : QgsCurvedLineCallout::QgsCurvedLineCallout()
767 : 0 : {
768 : 0 : }
769 : :
770 : 0 : QgsCurvedLineCallout::QgsCurvedLineCallout( const QgsCurvedLineCallout &other )
771 : 0 : : QgsSimpleLineCallout( other )
772 : 0 : , mOrientation( other.mOrientation )
773 : 0 : , mCurvature( other.mCurvature )
774 : 0 : {
775 : :
776 : 0 : }
777 : :
778 : 0 : QgsCallout *QgsCurvedLineCallout::create( const QVariantMap &properties, const QgsReadWriteContext &context )
779 : : {
780 : 0 : std::unique_ptr< QgsCurvedLineCallout > callout = std::make_unique< QgsCurvedLineCallout >();
781 : 0 : callout->readProperties( properties, context );
782 : :
783 : 0 : callout->setCurvature( properties.value( QStringLiteral( "curvature" ), 0.1 ).toDouble() );
784 : 0 : callout->setOrientation( decodeOrientation( properties.value( QStringLiteral( "orientation" ), QStringLiteral( "auto" ) ).toString() ) );
785 : :
786 : 0 : return callout.release();
787 : 0 : }
788 : :
789 : 0 : QString QgsCurvedLineCallout::type() const
790 : : {
791 : 0 : return QStringLiteral( "curved" );
792 : : }
793 : :
794 : 0 : QgsCurvedLineCallout *QgsCurvedLineCallout::clone() const
795 : : {
796 : 0 : return new QgsCurvedLineCallout( *this );
797 : 0 : }
798 : :
799 : 0 : QVariantMap QgsCurvedLineCallout::properties( const QgsReadWriteContext &context ) const
800 : : {
801 : 0 : QVariantMap props = QgsSimpleLineCallout::properties( context );
802 : 0 : props.insert( QStringLiteral( "curvature" ), mCurvature );
803 : 0 : props.insert( QStringLiteral( "orientation" ), encodeOrientation( mOrientation ) );
804 : 0 : return props;
805 : 0 : }
806 : :
807 : 0 : QgsCurve *QgsCurvedLineCallout::createCalloutLine( const QgsPoint &start, const QgsPoint &end, QgsRenderContext &context, const QRectF &rect, const double, const QgsGeometry &, QgsCallout::QgsCalloutContext & ) const
808 : : {
809 : 0 : double curvature = mCurvature * 100;
810 : 0 : if ( dataDefinedProperties().isActive( QgsCallout::Curvature ) )
811 : : {
812 : 0 : context.expressionContext().setOriginalValueVariable( curvature );
813 : 0 : curvature = dataDefinedProperties().valueAsDouble( QgsCallout::Curvature, context.expressionContext(), curvature );
814 : 0 : }
815 : :
816 : 0 : Orientation orientation = mOrientation;
817 : 0 : if ( dataDefinedProperties().isActive( QgsCallout::Orientation ) )
818 : : {
819 : 0 : bool ok = false;
820 : 0 : const QString orientationString = dataDefinedProperties().property( QgsCallout::Orientation ).valueAsString( context.expressionContext(), QString(), &ok );
821 : 0 : if ( ok )
822 : : {
823 : 0 : orientation = decodeOrientation( orientationString );
824 : 0 : }
825 : 0 : }
826 : :
827 : 0 : if ( orientation == Automatic )
828 : : {
829 : : // to calculate automatically the best curve orientation, we first check which side of the label bounding box
830 : : // the callout origin is nearest to
831 : 0 : switch ( QgsGeometryUtils::closestSideOfRectangle( rect.right(), rect.bottom(), rect.left(), rect.top(), start.x(), start.y() ) )
832 : : {
833 : : case 1:
834 : : // closest to bottom
835 : 0 : if ( qgsDoubleNear( end.x(), start.x() ) )
836 : : {
837 : : // if vertical line, we bend depending on whether the line sits towards the left or right side of the label
838 : 0 : if ( start.x() < ( rect.left() + 0.5 * rect.width() ) )
839 : 0 : orientation = CounterClockwise;
840 : : else
841 : 0 : orientation = Clockwise;
842 : 0 : }
843 : 0 : else if ( end.x() > start.x() )
844 : 0 : orientation = CounterClockwise;
845 : : else
846 : 0 : orientation = Clockwise;
847 : 0 : break;
848 : :
849 : : case 2:
850 : : // closest to bottom-right
851 : 0 : if ( end.x() < start.x() )
852 : 0 : orientation = Clockwise;
853 : 0 : else if ( end.y() < start.y() )
854 : 0 : orientation = CounterClockwise;
855 : 0 : else if ( end.x() - start.x() < end.y() - start.y() )
856 : 0 : orientation = Clockwise;
857 : : else
858 : 0 : orientation = CounterClockwise;
859 : 0 : break;
860 : :
861 : : case 3:
862 : : // closest to right
863 : 0 : if ( qgsDoubleNear( end.y(), start.y() ) )
864 : : {
865 : : // if horizontal line, we bend depending on whether the line sits towards the top or bottom side of the label
866 : 0 : if ( start.y() < ( rect.top() + 0.5 * rect.height() ) )
867 : 0 : orientation = Clockwise;
868 : : else
869 : 0 : orientation = CounterClockwise;
870 : 0 : }
871 : 0 : else if ( end.y() < start.y() )
872 : 0 : orientation = CounterClockwise;
873 : : else
874 : 0 : orientation = Clockwise;
875 : 0 : break;
876 : :
877 : : case 4:
878 : : // closest to top-right
879 : 0 : if ( end.x() < start.x() )
880 : 0 : orientation = CounterClockwise;
881 : 0 : else if ( end.y() > start.y() )
882 : 0 : orientation = Clockwise;
883 : 0 : else if ( end.x() - start.x() < start.y() - end.y() )
884 : 0 : orientation = CounterClockwise;
885 : : else
886 : 0 : orientation = Clockwise;
887 : 0 : break;
888 : :
889 : : case 5:
890 : : // closest to top
891 : 0 : if ( qgsDoubleNear( end.x(), start.x() ) )
892 : : {
893 : : // if vertical line, we bend depending on whether the line sits towards the left or right side of the label
894 : 0 : if ( start.x() < ( rect.left() + 0.5 * rect.width() ) )
895 : 0 : orientation = Clockwise;
896 : : else
897 : 0 : orientation = CounterClockwise;
898 : 0 : }
899 : 0 : else if ( end.x() < start.x() )
900 : 0 : orientation = CounterClockwise;
901 : : else
902 : 0 : orientation = Clockwise;
903 : 0 : break;
904 : :
905 : : case 6:
906 : : // closest to top-left
907 : 0 : if ( end.x() > start.x() )
908 : 0 : orientation = Clockwise;
909 : 0 : else if ( end.y() > start.y() )
910 : 0 : orientation = CounterClockwise;
911 : 0 : else if ( start.x() - end.x() < start.y() - end.y() )
912 : 0 : orientation = Clockwise;
913 : : else
914 : 0 : orientation = CounterClockwise;
915 : 0 : break;
916 : :
917 : : case 7:
918 : : //closest to left
919 : 0 : if ( qgsDoubleNear( end.y(), start.y() ) )
920 : : {
921 : : // if horizontal line, we bend depending on whether the line sits towards the top or bottom side of the label
922 : 0 : if ( start.y() < ( rect.top() + 0.5 * rect.height() ) )
923 : 0 : orientation = CounterClockwise;
924 : : else
925 : 0 : orientation = Clockwise;
926 : 0 : }
927 : 0 : else if ( end.y() > start.y() )
928 : 0 : orientation = CounterClockwise;
929 : : else
930 : 0 : orientation = Clockwise;
931 : 0 : break;
932 : :
933 : : case 8:
934 : : //closest to bottom-left
935 : 0 : if ( end.x() > start.x() )
936 : 0 : orientation = CounterClockwise;
937 : 0 : else if ( end.y() < start.y() )
938 : 0 : orientation = Clockwise;
939 : 0 : else if ( start.x() - end.x() < end.y() - start.y() )
940 : 0 : orientation = CounterClockwise;
941 : : else
942 : 0 : orientation = Clockwise;
943 : 0 : break;
944 : : }
945 : 0 : }
946 : :
947 : : // turn the line into a curved line. We do this by creating a circular string from the callout line's
948 : : // start to end point, where the curve point is in the middle of the callout line and perpendicularly offset
949 : : // by a proportion of the overall callout line length
950 : 0 : const double distance = ( orientation == Clockwise ? 1 : -1 ) * start.distance( end ) * curvature / 100.0;
951 : : double midX, midY;
952 : 0 : QgsGeometryUtils::perpendicularOffsetPointAlongSegment( start.x(), start.y(), end.x(), end.y(), 0.5, distance, &midX, &midY );
953 : :
954 : 0 : return new QgsCircularString( start, QgsPoint( midX, midY ), end );
955 : 0 : }
956 : :
957 : 0 : QgsCurvedLineCallout::Orientation QgsCurvedLineCallout::decodeOrientation( const QString &string )
958 : : {
959 : 0 : const QString cleaned = string.toLower().trimmed();
960 : 0 : if ( cleaned == QLatin1String( "auto" ) )
961 : 0 : return Automatic;
962 : 0 : if ( cleaned == QLatin1String( "clockwise" ) )
963 : 0 : return Clockwise;
964 : 0 : if ( cleaned == QLatin1String( "counterclockwise" ) )
965 : 0 : return CounterClockwise;
966 : 0 : return Automatic;
967 : 0 : }
968 : :
969 : 0 : QString QgsCurvedLineCallout::encodeOrientation( QgsCurvedLineCallout::Orientation orientation )
970 : : {
971 : 0 : switch ( orientation )
972 : : {
973 : : case QgsCurvedLineCallout::Automatic:
974 : 0 : return QStringLiteral( "auto" );
975 : : case QgsCurvedLineCallout::Clockwise:
976 : 0 : return QStringLiteral( "clockwise" );
977 : : case QgsCurvedLineCallout::CounterClockwise:
978 : 0 : return QStringLiteral( "counterclockwise" );
979 : : }
980 : 0 : return QString();
981 : 0 : }
982 : :
983 : 0 : QgsCurvedLineCallout::Orientation QgsCurvedLineCallout::orientation() const
984 : : {
985 : 0 : return mOrientation;
986 : : }
987 : :
988 : 0 : void QgsCurvedLineCallout::setOrientation( Orientation orientation )
989 : : {
990 : 0 : mOrientation = orientation;
991 : 0 : }
992 : :
993 : 0 : double QgsCurvedLineCallout::curvature() const
994 : : {
995 : 0 : return mCurvature;
996 : : }
997 : :
998 : 0 : void QgsCurvedLineCallout::setCurvature( double curvature )
999 : : {
1000 : 0 : mCurvature = curvature;
1001 : 0 : }
1002 : :
1003 : :
1004 : :
1005 : : //
1006 : : // QgsBalloonCallout
1007 : : //
1008 : :
1009 : 0 : QgsBalloonCallout::QgsBalloonCallout()
1010 : 0 : {
1011 : 0 : mFillSymbol = std::make_unique< QgsFillSymbol >( QgsSymbolLayerList() << new QgsSimpleFillSymbolLayer( QColor( 255, 200, 60 ) ) );
1012 : 0 : }
1013 : :
1014 : 0 : QgsBalloonCallout::~QgsBalloonCallout() = default;
1015 : :
1016 : 0 : QgsBalloonCallout::QgsBalloonCallout( const QgsBalloonCallout &other )
1017 : 0 : : QgsCallout( other )
1018 : 0 : , mFillSymbol( other.mFillSymbol ? other.mFillSymbol->clone() : nullptr )
1019 : 0 : , mOffsetFromAnchorDistance( other.mOffsetFromAnchorDistance )
1020 : 0 : , mOffsetFromAnchorUnit( other.mOffsetFromAnchorUnit )
1021 : 0 : , mOffsetFromAnchorScale( other.mOffsetFromAnchorScale )
1022 : 0 : , mMargins( other.mMargins )
1023 : 0 : , mMarginUnit( other.mMarginUnit )
1024 : 0 : , mWedgeWidth( other.mWedgeWidth )
1025 : 0 : , mWedgeWidthUnit( other.mWedgeWidthUnit )
1026 : 0 : , mWedgeWidthScale( other.mWedgeWidthScale )
1027 : 0 : {
1028 : :
1029 : 0 : }
1030 : :
1031 : 0 : QgsCallout *QgsBalloonCallout::create( const QVariantMap &properties, const QgsReadWriteContext &context )
1032 : : {
1033 : 0 : std::unique_ptr< QgsBalloonCallout > callout = std::make_unique< QgsBalloonCallout >();
1034 : 0 : callout->readProperties( properties, context );
1035 : 0 : return callout.release();
1036 : 0 : }
1037 : :
1038 : 0 : QString QgsBalloonCallout::type() const
1039 : : {
1040 : 0 : return QStringLiteral( "balloon" );
1041 : : }
1042 : :
1043 : 0 : QgsBalloonCallout *QgsBalloonCallout::clone() const
1044 : : {
1045 : 0 : return new QgsBalloonCallout( *this );
1046 : 0 : }
1047 : :
1048 : 0 : QVariantMap QgsBalloonCallout::properties( const QgsReadWriteContext &context ) const
1049 : : {
1050 : 0 : QVariantMap props = QgsCallout::properties( context );
1051 : :
1052 : 0 : if ( mFillSymbol )
1053 : : {
1054 : 0 : props[ QStringLiteral( "fillSymbol" ) ] = QgsSymbolLayerUtils::symbolProperties( mFillSymbol.get() );
1055 : 0 : }
1056 : :
1057 : 0 : props[ QStringLiteral( "offsetFromAnchor" ) ] = mOffsetFromAnchorDistance;
1058 : 0 : props[ QStringLiteral( "offsetFromAnchorUnit" ) ] = QgsUnitTypes::encodeUnit( mOffsetFromAnchorUnit );
1059 : 0 : props[ QStringLiteral( "offsetFromAnchorMapUnitScale" ) ] = QgsSymbolLayerUtils::encodeMapUnitScale( mOffsetFromAnchorScale );
1060 : :
1061 : 0 : props[ QStringLiteral( "margins" ) ] = mMargins.toString();
1062 : 0 : props[ QStringLiteral( "marginsUnit" ) ] = QgsUnitTypes::encodeUnit( mMarginUnit );
1063 : 0 :
1064 : 0 : props[ QStringLiteral( "wedgeWidth" ) ] = mWedgeWidth;
1065 : 0 : props[ QStringLiteral( "wedgeWidthUnit" ) ] = QgsUnitTypes::encodeUnit( mWedgeWidthUnit );
1066 : 0 : props[ QStringLiteral( "wedgeWidthMapUnitScale" ) ] = QgsSymbolLayerUtils::encodeMapUnitScale( mWedgeWidthScale );
1067 : :
1068 : 0 : return props;
1069 : 0 : }
1070 : 0 :
1071 : 0 : void QgsBalloonCallout::readProperties( const QVariantMap &props, const QgsReadWriteContext &context )
1072 : : {
1073 : 0 : QgsCallout::readProperties( props, context );
1074 : :
1075 : 0 : const QString fillSymbolDef = props.value( QStringLiteral( "fillSymbol" ) ).toString();
1076 : 0 : QDomDocument doc( QStringLiteral( "symbol" ) );
1077 : 0 : doc.setContent( fillSymbolDef );
1078 : 0 : QDomElement symbolElem = doc.firstChildElement( QStringLiteral( "symbol" ) );
1079 : 0 : std::unique_ptr< QgsFillSymbol > fillSymbol( QgsSymbolLayerUtils::loadSymbol< QgsFillSymbol >( symbolElem, context ) );
1080 : 0 : if ( fillSymbol )
1081 : 0 : mFillSymbol = std::move( fillSymbol );
1082 : :
1083 : 0 : mOffsetFromAnchorDistance = props.value( QStringLiteral( "offsetFromAnchor" ), 0 ).toDouble();
1084 : 0 : mOffsetFromAnchorUnit = QgsUnitTypes::decodeRenderUnit( props.value( QStringLiteral( "offsetFromAnchorUnit" ) ).toString() );
1085 : 0 : mOffsetFromAnchorScale = QgsSymbolLayerUtils::decodeMapUnitScale( props.value( QStringLiteral( "offsetFromAnchorMapUnitScale" ) ).toString() );
1086 : :
1087 : 0 : mMargins = QgsMargins::fromString( props.value( QStringLiteral( "margins" ) ).toString() );
1088 : 0 : mMarginUnit = QgsUnitTypes::decodeRenderUnit( props.value( QStringLiteral( "marginsUnit" ) ).toString() );
1089 : :
1090 : 0 : mWedgeWidth = props.value( QStringLiteral( "wedgeWidth" ), 2.64 ).toDouble();
1091 : 0 : mWedgeWidthUnit = QgsUnitTypes::decodeRenderUnit( props.value( QStringLiteral( "wedgeWidthUnit" ) ).toString() );
1092 : 0 : mWedgeWidthScale = QgsSymbolLayerUtils::decodeMapUnitScale( props.value( QStringLiteral( "wedgeWidthMapUnitScale" ) ).toString() );
1093 : 0 : }
1094 : :
1095 : 0 : void QgsBalloonCallout::startRender( QgsRenderContext &context )
1096 : : {
1097 : 0 : QgsCallout::startRender( context );
1098 : 0 : if ( mFillSymbol )
1099 : 0 : mFillSymbol->startRender( context );
1100 : 0 : }
1101 : :
1102 : 0 : void QgsBalloonCallout::stopRender( QgsRenderContext &context )
1103 : : {
1104 : 0 : QgsCallout::stopRender( context );
1105 : 0 : if ( mFillSymbol )
1106 : 0 : mFillSymbol->stopRender( context );
1107 : 0 : }
1108 : :
1109 : 0 : QSet<QString> QgsBalloonCallout::referencedFields( const QgsRenderContext &context ) const
1110 : : {
1111 : 0 : QSet<QString> fields = QgsCallout::referencedFields( context );
1112 : 0 : if ( mFillSymbol )
1113 : 0 : fields.unite( mFillSymbol->usedAttributes( context ) );
1114 : 0 : return fields;
1115 : 0 : }
1116 : :
1117 : 0 : QgsFillSymbol *QgsBalloonCallout::fillSymbol()
1118 : : {
1119 : 0 : return mFillSymbol.get();
1120 : : }
1121 : :
1122 : 0 : void QgsBalloonCallout::setFillSymbol( QgsFillSymbol *symbol )
1123 : : {
1124 : 0 : mFillSymbol.reset( symbol );
1125 : 0 : }
1126 : :
1127 : 0 : void QgsBalloonCallout::draw( QgsRenderContext &context, const QRectF &rect, const double, const QgsGeometry &anchor, QgsCalloutContext &calloutContext )
1128 : : {
1129 : 0 : bool destinationIsPinned = false;
1130 : 0 : QgsGeometry line = calloutLineToPart( QgsGeometry::fromRect( rect ), anchor.constGet(), context, calloutContext, destinationIsPinned );
1131 : :
1132 : 0 : double offsetFromAnchor = mOffsetFromAnchorDistance;
1133 : 0 : if ( dataDefinedProperties().isActive( QgsCallout::OffsetFromAnchor ) )
1134 : : {
1135 : 0 : context.expressionContext().setOriginalValueVariable( offsetFromAnchor );
1136 : 0 : offsetFromAnchor = dataDefinedProperties().valueAsDouble( QgsCallout::OffsetFromAnchor, context.expressionContext(), offsetFromAnchor );
1137 : 0 : }
1138 : 0 : const double offsetFromAnchorPixels = context.convertToPainterUnits( offsetFromAnchor, mOffsetFromAnchorUnit, mOffsetFromAnchorScale );
1139 : :
1140 : 0 : if ( offsetFromAnchorPixels > 0 )
1141 : : {
1142 : 0 : if ( const QgsLineString *ls = qgsgeometry_cast< const QgsLineString * >( line.constGet() ) )
1143 : : {
1144 : 0 : line = QgsGeometry( ls->curveSubstring( 0, ls->length() - offsetFromAnchorPixels ) );
1145 : 0 : }
1146 : 0 : }
1147 : :
1148 : 0 : QgsPointXY destination;
1149 : 0 : QgsPointXY origin;
1150 : 0 : if ( const QgsLineString *ls = qgsgeometry_cast< const QgsLineString * >( line.constGet() ) )
1151 : : {
1152 : 0 : origin = ls->startPoint();
1153 : 0 : destination = ls->endPoint();
1154 : 0 : }
1155 : : else
1156 : : {
1157 : 0 : destination = QgsPointXY( rect.center() );
1158 : : }
1159 : :
1160 : 0 : const QPolygonF points = getPoints( context, destination, rect );
1161 : 0 : if ( points.empty() )
1162 : 0 : return;
1163 : :
1164 : 0 : if ( !origin.isEmpty() )
1165 : : {
1166 : 0 : QgsCalloutPosition position;
1167 : 0 : position.setOrigin( context.mapToPixel().toMapCoordinates( origin.x(), origin.y() ).toQPointF() );
1168 : 0 : position.setOriginIsPinned( false );
1169 : 0 : position.setDestination( context.mapToPixel().toMapCoordinates( destination.x(), destination.y() ).toQPointF() );
1170 : 0 : position.setDestinationIsPinned( destinationIsPinned );
1171 : 0 : calloutContext.addCalloutPosition( position );
1172 : 0 : }
1173 : :
1174 : 0 : mFillSymbol->renderPolygon( points, nullptr, nullptr, context );
1175 : 0 : }
1176 : :
1177 : 0 : QPolygonF QgsBalloonCallout::getPoints( QgsRenderContext &context, QgsPointXY origin, QRectF rect ) const
1178 : : {
1179 : 0 : double segmentPointWidth = mWedgeWidth;
1180 : 0 : if ( dataDefinedProperties().isActive( QgsCallout::WedgeWidth ) )
1181 : : {
1182 : 0 : context.expressionContext().setOriginalValueVariable( segmentPointWidth );
1183 : 0 : segmentPointWidth = dataDefinedProperties().valueAsDouble( QgsCallout::WedgeWidth, context.expressionContext(), segmentPointWidth );
1184 : 0 : }
1185 : 0 : segmentPointWidth = context.convertToPainterUnits( segmentPointWidth, mWedgeWidthUnit, mWedgeWidthScale );
1186 : :
1187 : 0 : double left = mMargins.left();
1188 : 0 : double right = mMargins.right();
1189 : 0 : double top = mMargins.top();
1190 : 0 : double bottom = mMargins.bottom();
1191 : :
1192 : 0 : if ( dataDefinedProperties().isActive( QgsCallout::Margins ) )
1193 : : {
1194 : 0 : const QVariant value = dataDefinedProperties().value( QgsCallout::Margins, context.expressionContext() );
1195 : 0 : if ( !value.isNull() )
1196 : : {
1197 : 0 : if ( value.type() == QVariant::List )
1198 : : {
1199 : 0 : const QVariantList list = value.toList();
1200 : 0 : if ( list.size() == 4 )
1201 : : {
1202 : 0 : bool topOk = false;
1203 : 0 : bool rightOk = false;
1204 : 0 : bool bottomOk = false;
1205 : 0 : bool leftOk = false;
1206 : 0 : double evaluatedTop = list.at( 0 ).toDouble( &topOk );
1207 : 0 : double evaluatedRight = list.at( 1 ).toDouble( &rightOk );
1208 : 0 : double evaluatedBottom = list.at( 2 ).toDouble( &bottomOk );
1209 : 0 : double evaluatedLeft = list.at( 3 ).toDouble( &leftOk );
1210 : 0 : if ( topOk && rightOk && bottomOk && leftOk )
1211 : : {
1212 : 0 : left = evaluatedLeft;
1213 : 0 : top = evaluatedTop;
1214 : 0 : right = evaluatedRight;
1215 : 0 : bottom = evaluatedBottom;
1216 : 0 : }
1217 : 0 : }
1218 : 0 : }
1219 : : else
1220 : : {
1221 : 0 : const QStringList list = value.toString().trimmed().split( ',' );
1222 : 0 : if ( list.count() == 4 )
1223 : : {
1224 : 0 : bool topOk = false;
1225 : 0 : bool rightOk = false;
1226 : 0 : bool bottomOk = false;
1227 : 0 : bool leftOk = false;
1228 : 0 : double evaluatedTop = list.at( 0 ).toDouble( &topOk );
1229 : 0 : double evaluatedRight = list.at( 1 ).toDouble( &rightOk );
1230 : 0 : double evaluatedBottom = list.at( 2 ).toDouble( &bottomOk );
1231 : 0 : double evaluatedLeft = list.at( 3 ).toDouble( &leftOk );
1232 : 0 : if ( topOk && rightOk && bottomOk && leftOk )
1233 : : {
1234 : 0 : left = evaluatedLeft;
1235 : 0 : top = evaluatedTop;
1236 : 0 : right = evaluatedRight;
1237 : 0 : bottom = evaluatedBottom;
1238 : 0 : }
1239 : 0 : }
1240 : 0 : }
1241 : 0 : }
1242 : 0 : }
1243 : :
1244 : 0 : const double marginLeft = context.convertToPainterUnits( left, mMarginUnit );
1245 : 0 : const double marginRight = context.convertToPainterUnits( right, mMarginUnit );
1246 : 0 : const double marginTop = context.convertToPainterUnits( top, mMarginUnit );
1247 : 0 : const double marginBottom = context.convertToPainterUnits( bottom, mMarginUnit );
1248 : :
1249 : 0 : const QRectF expandedRect( rect.left() - marginLeft, rect.top() + marginBottom,
1250 : 0 : rect.width() + marginLeft + marginRight,
1251 : 0 : rect.height() - marginTop - marginBottom );
1252 : :
1253 : : // IMPORTANT -- check for degenerate height is >=0, because QRectF are not normalized and we are using painter
1254 : : // coordinates with descending vertical axis!
1255 : 0 : if ( expandedRect.width() <= 0 || expandedRect.height() >= 0 )
1256 : 0 : return QPolygonF();
1257 : :
1258 : 0 : return QgsShapeGenerator::createBalloon( origin, expandedRect, segmentPointWidth );
1259 : 0 : }
|