QGIS API Documentation  3.22.4-Białowieża (ce8e65e95e)
qgsscalebarrenderer.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsscalebarrenderer.cpp
3  -----------------------
4  begin : June 2008
5  copyright : (C) 2008 by Marco Hugentobler
6  email : [email protected]
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 "qgsscalebarrenderer.h"
18 #include "qgsscalebarsettings.h"
19 #include "qgslayoututils.h"
20 #include "qgstextrenderer.h"
22 #include "qgsnumericformat.h"
23 #include "qgssymbol.h"
24 #include "qgssymbollayerutils.h"
25 #include "qgslinesymbol.h"
26 
27 #include <QFontMetricsF>
28 #include <QPainter>
29 
30 void QgsScaleBarRenderer::drawDefaultLabels( QgsRenderContext &context, const QgsScaleBarSettings &settings, const ScaleBarContext &scaleContext ) const
31 {
32  if ( !context.painter() )
33  {
34  return;
35  }
36 
37  QPainter *painter = context.painter();
38 
39  painter->save();
40 
41  const QgsTextFormat format = settings.textFormat();
42 
43  QgsExpressionContextScope *scaleScope = new QgsExpressionContextScope( QStringLiteral( "scalebar_text" ) );
44  const QgsExpressionContextScopePopper scopePopper( context.expressionContext(), scaleScope );
45 
46  const QString firstLabel = firstLabelString( settings );
47  const QFontMetricsF fontMetrics = QgsTextRenderer::fontMetrics( context, format );
48  const double xOffset = fontMetrics.horizontalAdvance( firstLabel ) / 2.0;
49 
50  const double scaledBoxContentSpace = context.convertToPainterUnits( settings.boxContentSpace(), QgsUnitTypes::RenderMillimeters );
51  const double scaledLabelBarSpace = context.convertToPainterUnits( settings.labelBarSpace(), QgsUnitTypes::RenderMillimeters );
52  double scaledHeight;
53  if ( ( scaleContext.flags & Flag::FlagUsesSubdivisionsHeight ) && ( settings.numberOfSubdivisions() > 1 ) && ( settings.subdivisionsHeight() > settings.height() ) )
54  {
55  scaledHeight = context.convertToPainterUnits( settings.subdivisionsHeight(), QgsUnitTypes::RenderMillimeters );
56  }
57  else
58  {
59  scaledHeight = context.convertToPainterUnits( settings.height(), QgsUnitTypes::RenderMillimeters );
60  }
61 
62  double currentLabelNumber = 0.0;
63 
64  const int nSegmentsLeft = settings.numberOfSegmentsLeft();
65  int segmentCounter = 0;
66 
67  QString currentNumericLabel;
68  const QList<double> positions = segmentPositions( context, scaleContext, settings );
69 
70  bool drawZero = true;
71  switch ( settings.labelHorizontalPlacement() )
72  {
74  drawZero = false;
75  break;
77  drawZero = true;
78  break;
79  }
80 
81  const QgsNumericFormatContext numericContext;
82 
83  for ( int i = 0; i < positions.size(); ++i )
84  {
85  if ( segmentCounter == 0 && nSegmentsLeft > 0 )
86  {
87  //label first left segment
88  currentNumericLabel = firstLabel;
89  }
90  else if ( segmentCounter != 0 && segmentCounter == nSegmentsLeft ) //reset label number to 0 if there are left segments
91  {
92  currentLabelNumber = 0.0;
93  }
94 
95  if ( segmentCounter >= nSegmentsLeft )
96  {
97  currentNumericLabel = settings.numericFormat()->formatDouble( currentLabelNumber / settings.mapUnitsPerScaleBarUnit(), numericContext );
98  }
99 
100  //don't draw label for intermediate left segments or the zero label when it needs to be skipped
101  if ( ( segmentCounter == 0 || segmentCounter >= nSegmentsLeft ) && ( currentNumericLabel != QLatin1String( "0" ) || drawZero ) )
102  {
103  scaleScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "scale_value" ), currentNumericLabel, true, false ) );
104  QPointF pos;
106  {
107  if ( segmentCounter == 0 )
108  {
109  // if the segment counter is zero with a non zero label, this is the left-of-zero label
110  pos.setX( context.convertToPainterUnits( positions.at( i ) + ( scaleContext.segmentWidth / 2 ), QgsUnitTypes::RenderMillimeters ) );
111  }
112  else
113  {
114  pos.setX( context.convertToPainterUnits( positions.at( i ) - ( scaleContext.segmentWidth / 2 ), QgsUnitTypes::RenderMillimeters ) );
115  }
116  }
117  else
118  {
119  pos.setX( context.convertToPainterUnits( positions.at( i ), QgsUnitTypes::RenderMillimeters ) + xOffset );
120  }
121  pos.setY( fontMetrics.ascent() + scaledBoxContentSpace + ( settings.labelVerticalPlacement() == QgsScaleBarSettings::LabelBelowSegment ? scaledHeight + scaledLabelBarSpace : 0 ) );
122  QgsTextRenderer::drawText( pos, 0, QgsTextRenderer::AlignCenter, QStringList() << currentNumericLabel, context, format );
123  }
124 
125  if ( segmentCounter >= nSegmentsLeft )
126  {
127  currentLabelNumber += settings.unitsPerSegment();
128  }
129  ++segmentCounter;
130  }
131 
132  //also draw the last label
133  if ( !positions.isEmpty() )
134  {
135  // note: this label is NOT centered over the end of the bar - rather the numeric portion
136  // of it is, without considering the unit label suffix. That's drawn at the end after
137  // horizontally centering just the numeric portion.
138  currentNumericLabel = settings.numericFormat()->formatDouble( currentLabelNumber / settings.mapUnitsPerScaleBarUnit(), numericContext );
139  scaleScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "scale_value" ), currentNumericLabel, true, false ) );
140  QPointF pos;
141  pos.setY( fontMetrics.ascent() + scaledBoxContentSpace + ( settings.labelVerticalPlacement() == QgsScaleBarSettings::LabelBelowSegment ? scaledHeight + scaledLabelBarSpace : 0 ) );
143  {
144  pos.setX( context.convertToPainterUnits( positions.at( positions.size() - 1 ) + ( scaleContext.segmentWidth / 2 ), QgsUnitTypes::RenderMillimeters ) + xOffset );
145  QgsTextRenderer::drawText( pos, 0, QgsTextRenderer::AlignCenter, QStringList() << ( currentNumericLabel + ' ' + settings.unitLabel() ), context, format );
146  }
147  else
148  {
149  pos.setX( context.convertToPainterUnits( positions.at( positions.size() - 1 ) + scaleContext.segmentWidth, QgsUnitTypes::RenderMillimeters ) + xOffset
150  - fontMetrics.horizontalAdvance( currentNumericLabel ) / 2.0 );
151  QgsTextRenderer::drawText( pos, 0, QgsTextRenderer::AlignLeft, QStringList() << ( currentNumericLabel + ' ' + settings.unitLabel() ), context, format );
152  }
153  }
154 
155  painter->restore();
156 }
157 
158 QgsScaleBarRenderer::Flags QgsScaleBarRenderer::flags() const
159 {
160  return QgsScaleBarRenderer::Flags();
161 }
162 
164 {
165  return 100;
166 }
167 
169  const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const
170 {
171  const QFont font = settings.textFormat().toQFont();
172 
173  //consider centered first label
174  double firstLabelWidth = QgsLayoutUtils::textWidthMM( font, firstLabelString( settings ) );
176  {
177  if ( firstLabelWidth > scaleContext.segmentWidth )
178  {
179  firstLabelWidth = ( firstLabelWidth - scaleContext.segmentWidth ) / 2;
180  }
181  else
182  {
183  firstLabelWidth = 0.0;
184  }
185  }
186  else
187  {
188  firstLabelWidth = firstLabelWidth / 2;
189  }
190 
191  //consider last number and label
192  const double largestLabelNumber = settings.numberOfSegments() * settings.unitsPerSegment() / settings.mapUnitsPerScaleBarUnit();
193  const QString largestNumberLabel = settings.numericFormat()->formatDouble( largestLabelNumber, QgsNumericFormatContext() );
194  const QString largestLabel = largestNumberLabel + ' ' + settings.unitLabel();
195  double largestLabelWidth;
197  {
198  largestLabelWidth = QgsLayoutUtils::textWidthMM( font, largestLabel );
199  if ( largestLabelWidth > scaleContext.segmentWidth )
200  {
201  largestLabelWidth = ( largestLabelWidth - scaleContext.segmentWidth ) / 2;
202  }
203  else
204  {
205  largestLabelWidth = 0.0;
206  }
207  }
208  else
209  {
210  largestLabelWidth = QgsLayoutUtils::textWidthMM( font, largestLabel ) - QgsLayoutUtils::textWidthMM( font, largestNumberLabel ) / 2;
211  }
212 
213  const double totalBarLength = scaleContext.segmentWidth * ( settings.numberOfSegments() + ( settings.numberOfSegmentsLeft() > 0 ? 1 : 0 ) );
214 
215  // this whole method is deprecated, so we can still call the deprecated settings.pen() getter
217  const double width = firstLabelWidth + totalBarLength + 2 * settings.pen().widthF() + largestLabelWidth + 2 * settings.boxContentSpace();
219 
220  const double height = settings.height() + settings.labelBarSpace() + 2 * settings.boxContentSpace() + QgsLayoutUtils::fontAscentMM( font );
221 
222  return QSizeF( width, height );
223 }
224 
226 {
227  const double painterToMm = 1.0 / context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters );
228  //consider centered first label
229  double firstLabelWidth = QgsTextRenderer::textWidth( context, settings.textFormat(), QStringList() << firstLabelString( settings ) ) * painterToMm;
230 
232  {
233  if ( firstLabelWidth > scaleContext.segmentWidth )
234  {
235  firstLabelWidth = ( firstLabelWidth - scaleContext.segmentWidth ) / 2;
236  }
237  else
238  {
239  firstLabelWidth = 0.0;
240  }
241  }
242  else
243  {
244  firstLabelWidth = firstLabelWidth / 2;
245  }
246 
247  //consider last number and label
248  const double largestLabelNumber = settings.numberOfSegments() * settings.unitsPerSegment() / settings.mapUnitsPerScaleBarUnit();
249  const QString largestNumberLabel = settings.numericFormat()->formatDouble( largestLabelNumber, QgsNumericFormatContext() );
250  const QString largestLabel = largestNumberLabel + ' ' + settings.unitLabel();
251  double largestLabelWidth;
253  {
254  largestLabelWidth = QgsTextRenderer::textWidth( context, settings.textFormat(), QStringList() << largestLabel ) * painterToMm;
255 
256  if ( largestLabelWidth > scaleContext.segmentWidth )
257  {
258  largestLabelWidth = ( largestLabelWidth - scaleContext.segmentWidth ) / 2;
259  }
260  else
261  {
262  largestLabelWidth = 0.0;
263  }
264  }
265  else
266  {
267  largestLabelWidth = QgsTextRenderer::textWidth( context, settings.textFormat(), QStringList() << largestLabel ) * painterToMm
268  - QgsTextRenderer::textWidth( context, settings.textFormat(), QStringList() << largestNumberLabel ) * painterToMm / 2;
269  }
270 
271  const double totalBarLength = scaleContext.segmentWidth * ( settings.numberOfSegments() + ( settings.numberOfSegmentsLeft() > 0 ? 1 : 0 ) );
272 
273  double lineWidth = QgsSymbolLayerUtils::estimateMaxSymbolBleed( settings.lineSymbol(), context ) * 2;
274  // need to convert to mm
275  lineWidth /= context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters );
276 
277  const double width = firstLabelWidth + totalBarLength + 2 * lineWidth + largestLabelWidth + 2 * settings.boxContentSpace();
278  double height;
279  if ( ( scaleContext.flags & Flag::FlagUsesSubdivisionsHeight ) && ( settings.numberOfSubdivisions() > 1 ) && ( settings.subdivisionsHeight() > settings.height() ) )
280  {
281  height = settings.subdivisionsHeight();
282  }
283  else
284  {
285  height = settings.height();
286  }
287 
288  // TODO -- we technically should check the height of ALL labels here and take the maximum
289  height += settings.labelBarSpace() + 2 * settings.boxContentSpace() + QgsTextRenderer::textHeight( context, settings.textFormat(), QStringList() << largestLabel ) * painterToMm;
290 
291  return QSizeF( width, height );
292 }
293 
295 {
296  return false;
297 }
298 
300 {
301  if ( settings.numberOfSegmentsLeft() > 0 )
302  {
303  return settings.numericFormat()->formatDouble( settings.unitsPerSegment() / settings.mapUnitsPerScaleBarUnit(), QgsNumericFormatContext() );
304  }
305  else
306  {
307  return settings.numericFormat()->formatDouble( 0, QgsNumericFormatContext() );
308  }
309 }
310 
312 {
313  const QString firstLabel = firstLabelString( settings );
315  return QgsLayoutUtils::textWidthMM( settings.font(), firstLabel ) / 2.0;
317 }
318 
319 double QgsScaleBarRenderer::firstLabelXOffset( const QgsScaleBarSettings &settings, const QgsRenderContext &context, const ScaleBarContext &scaleContext ) const
320 {
321  const QString firstLabel = firstLabelString( settings );
322  double firstLabelWidth = QgsTextRenderer::textWidth( context, settings.textFormat(), QStringList() << firstLabel );
324  {
325  if ( firstLabelWidth > scaleContext.segmentWidth )
326  {
327  firstLabelWidth = ( firstLabelWidth - scaleContext.segmentWidth ) / 2;
328  }
329  else
330  {
331  firstLabelWidth = 0.0;
332  }
333  }
334  else
335  {
336  firstLabelWidth = firstLabelWidth / 2;
337  }
338  return firstLabelWidth;
339 }
340 
341 QList<double> QgsScaleBarRenderer::segmentPositions( const ScaleBarContext &scaleContext, const QgsScaleBarSettings &settings ) const
342 {
343  QList<double> positions;
344 
345  // this whole method is deprecated, so calling a deprecated function is fine
347  double currentXCoord = settings.pen().widthF() + settings.boxContentSpace();
349 
350  //left segments
351  const double leftSegmentSize = scaleContext.segmentWidth / settings.numberOfSegmentsLeft();
352  positions.reserve( settings.numberOfSegmentsLeft() + settings.numberOfSegments() );
353  for ( int i = 0; i < settings.numberOfSegmentsLeft(); ++i )
354  {
355  positions << currentXCoord;
356  currentXCoord += leftSegmentSize;
357  }
358 
359  //right segments
360  for ( int i = 0; i < settings.numberOfSegments(); ++i )
361  {
362  positions << currentXCoord;
363  currentXCoord += scaleContext.segmentWidth;
364  }
365  return positions;
366 }
367 
368 QList<double> QgsScaleBarRenderer::segmentPositions( QgsRenderContext &context, const QgsScaleBarRenderer::ScaleBarContext &scaleContext, const QgsScaleBarSettings &settings ) const
369 {
370  QList<double> positions;
371 
372  double lineWidth = QgsSymbolLayerUtils::estimateMaxSymbolBleed( settings.lineSymbol(), context ) * 2.0;
373  // need to convert to mm
374  lineWidth /= context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters );
375 
376  double currentXCoord = lineWidth + settings.boxContentSpace();
377 
378  //left segments
379  const double leftSegmentSize = scaleContext.segmentWidth / settings.numberOfSegmentsLeft();
380  positions.reserve( settings.numberOfSegmentsLeft() + settings.numberOfSegments() );
381  for ( int i = 0; i < settings.numberOfSegmentsLeft(); ++i )
382  {
383  positions << currentXCoord;
384  currentXCoord += leftSegmentSize;
385  }
386 
387  //right segments
388  for ( int i = 0; i < settings.numberOfSegments(); ++i )
389  {
390  positions << currentXCoord;
391  currentXCoord += scaleContext.segmentWidth;
392  }
393  return positions;
394 }
395 
396 QList<double> QgsScaleBarRenderer::segmentWidths( const ScaleBarContext &scaleContext, const QgsScaleBarSettings &settings ) const
397 {
398  QList<double> widths;
399  widths.reserve( settings.numberOfSegmentsLeft() + settings.numberOfSegments() );
400 
401  //left segments
402  if ( settings.numberOfSegmentsLeft() > 0 )
403  {
404  const double leftSegmentSize = scaleContext.segmentWidth / settings.numberOfSegmentsLeft();
405  for ( int i = 0; i < settings.numberOfSegmentsLeft(); ++i )
406  {
407  widths << leftSegmentSize;
408  }
409  }
410 
411  //right segments
412  for ( int i = 0; i < settings.numberOfSegments(); ++i )
413  {
414  widths << scaleContext.segmentWidth;
415  }
416 
417  return widths;
418 }
RAII class to pop scope from an expression context on destruction.
Single scope for storing variables and functions for use within a QgsExpressionContext.
void addVariable(const QgsExpressionContextScope::StaticVariable &variable)
Adds a variable into the context scope.
static double fontAscentMM(const QFont &font)
Calculates a font ascent in millimeters, including workarounds for QT font rendering issues.
static double textWidthMM(const QFont &font, const QString &text)
Calculate a font width in millimeters for a text string, including workarounds for QT font rendering ...
A context for numeric formats.
virtual QString formatDouble(double value, const QgsNumericFormatContext &context) const =0
Returns a formatted string representation of a numeric double value.
Contains information about the context of a rendering operation.
QPainter * painter()
Returns the destination QPainter for the render operation.
QgsExpressionContext & expressionContext()
Gets the expression context.
double convertToPainterUnits(double size, QgsUnitTypes::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale(), Qgis::RenderSubcomponentProperty property=Qgis::RenderSubcomponentProperty::Generic) const
Converts a size from the specified units to painter units (pixels).
@ FlagUsesSubdivisionsHeight
Renderer uses the scalebar subdivisions height (see QgsScaleBarSettings::subdivisionsHeight() )
virtual Flags flags() const
Returns the scalebar rendering flags, which dictates the renderer's behavior.
virtual bool applyDefaultSettings(QgsScaleBarSettings &settings) const
Applies any default settings relating to the scalebar to the passed settings object.
virtual int sortKey() const
Returns a sorting key value, where renderers with a lower sort key will be shown earlier in lists.
void drawDefaultLabels(QgsRenderContext &context, const QgsScaleBarSettings &settings, const QgsScaleBarRenderer::ScaleBarContext &scaleContext) const
Draws default scalebar labels using the specified settings and scaleContext to a destination render c...
virtual Q_DECL_DEPRECATED QSizeF calculateBoxSize(const QgsScaleBarSettings &settings, const QgsScaleBarRenderer::ScaleBarContext &scaleContext) const
Calculates the required box size (in millimeters) for a scalebar using the specified settings and sca...
QString firstLabelString(const QgsScaleBarSettings &settings) const
Returns the text used for the first label in the scalebar.
QList< double > segmentWidths(const QgsScaleBarRenderer::ScaleBarContext &scaleContext, const QgsScaleBarSettings &settings) const
Returns a list of widths of each segment of the scalebar.
Q_DECL_DEPRECATED QList< double > segmentPositions(const QgsScaleBarRenderer::ScaleBarContext &scaleContext, const QgsScaleBarSettings &settings) const
Returns a list of positions for each segment within the scalebar.
Q_DECL_DEPRECATED double firstLabelXOffset(const QgsScaleBarSettings &settings) const
Returns the x-offset (in millimeters) used for the first label in the scalebar.
The QgsScaleBarSettings class stores the appearance and layout settings for scalebar drawing with Qgs...
double subdivisionsHeight() const
Returns the scalebar subdivisions height (in millimeters) for segments included in the right part of ...
QgsLineSymbol * lineSymbol() const
Returns the line symbol used to render the scalebar (only used for some scalebar types).
const QgsNumericFormat * numericFormat() const
Returns the numeric format used for numbers in the scalebar.
int numberOfSegments() const
Returns the number of segments included in the scalebar.
double unitsPerSegment() const
Returns the number of scalebar units per segment.
Q_DECL_DEPRECATED QPen pen() const
Returns the pen used for drawing outlines in the scalebar.
LabelHorizontalPlacement labelHorizontalPlacement() const
Returns the horizontal placement of text labels.
double boxContentSpace() const
Returns the spacing (margin) between the scalebar box and content in millimeters.
LabelVerticalPlacement labelVerticalPlacement() const
Returns the vertical placement of text labels.
QString unitLabel() const
Returns the label for units.
int numberOfSubdivisions() const
Returns the number of subdivisions for segments included in the right part of the scalebar (only used...
Q_DECL_DEPRECATED QFont font() const
Returns the font used for drawing text in the scalebar.
@ LabelCenteredSegment
Labels are drawn centered relative to segment.
@ LabelCenteredEdge
Labels are drawn centered relative to segment's edge.
@ LabelBelowSegment
Labels are drawn below the scalebar.
double labelBarSpace() const
Returns the spacing (in millimeters) between labels and the scalebar.
QgsTextFormat & textFormat()
Returns the text format used for drawing text in the scalebar.
double height() const
Returns the scalebar height (in millimeters).
int numberOfSegmentsLeft() const
Returns the number of segments included in the left part of the scalebar.
double mapUnitsPerScaleBarUnit() const
Returns the number of map units per scale bar unit used by the scalebar.
static double estimateMaxSymbolBleed(QgsSymbol *symbol, const QgsRenderContext &context)
Returns the maximum estimated bleed for the symbol.
Container for all settings relating to text rendering.
Definition: qgstextformat.h:41
QFont toQFont() const
Returns a QFont matching the relevant settings from this text format.
static double textWidth(const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, QFontMetricsF *fontMetrics=nullptr)
Returns the width of a text based on a given format.
@ AlignLeft
Left align.
@ AlignCenter
Center align.
static double textHeight(const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, DrawMode mode=Point, QFontMetricsF *fontMetrics=nullptr)
Returns the height of a text based on a given format.
static QFontMetricsF fontMetrics(QgsRenderContext &context, const QgsTextFormat &format, double scaleFactor=1.0)
Returns the font metrics for the given text format, when rendered in the specified render context.
static void drawText(const QRectF &rect, double rotation, HAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, bool drawAsOutlines=true, VAlignment vAlignment=AlignTop)
Draws text within a rectangle using the specified settings.
@ RenderMillimeters
Millimeters.
Definition: qgsunittypes.h:169
#define Q_NOWARN_DEPRECATED_POP
Definition: qgis.h:1742
#define Q_NOWARN_DEPRECATED_PUSH
Definition: qgis.h:1741
Single variable definition for use within a QgsExpressionContextScope.
Contains parameters regarding scalebar calculations.
Flags flags
Scalebar renderer flags.
double segmentWidth
The width, in millimeters, of each individual segment drawn.