QGIS API Documentation 3.99.0-Master (d270888f95f)
Loading...
Searching...
No Matches
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
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
20#include "qgslayoututils.h"
21#include "qgslinesymbol.h"
22#include "qgsnumericformat.h"
23#include "qgsscalebarsettings.h"
24#include "qgssymbol.h"
25#include "qgssymbollayerutils.h"
26#include "qgstextrenderer.h"
27
28#include <QFontMetricsF>
29#include <QPainter>
30#include <QString>
31
32using namespace Qt::StringLiterals;
33
34void QgsScaleBarRenderer::drawDefaultLabels( QgsRenderContext &context, const QgsScaleBarSettings &settings, const ScaleBarContext &scaleContext ) const
35{
36 if ( !context.painter() )
37 {
38 return;
39 }
40
41 QPainter *painter = context.painter();
42
43 painter->save();
44
45 const QgsTextFormat format = settings.textFormat();
46
47 QgsExpressionContextScope *scaleScope = new QgsExpressionContextScope( u"scalebar_text"_s );
48 const QgsExpressionContextScopePopper scopePopper( context.expressionContext(), scaleScope );
49
50 const QString firstLabel = firstLabelString( settings );
51 const QFontMetricsF fontMetrics = QgsTextRenderer::fontMetrics( context, format );
52 const double xOffset = fontMetrics.horizontalAdvance( firstLabel ) / 2.0;
53
54 const double scaledBoxContentSpace = context.convertToPainterUnits( settings.boxContentSpace(), Qgis::RenderUnit::Millimeters );
55 const double scaledLabelBarSpace = context.convertToPainterUnits( settings.labelBarSpace(), Qgis::RenderUnit::Millimeters );
56 double scaledHeight;
57 if ( ( scaleContext.flags & Flag::FlagUsesSubdivisionsHeight ) && ( settings.numberOfSubdivisions() > 1 ) && ( settings.subdivisionsHeight() > settings.height() ) )
58 {
60 }
61 else
62 {
63 scaledHeight = context.convertToPainterUnits( settings.height(), Qgis::RenderUnit::Millimeters );
64 }
65
66 double currentLabelNumber = 0.0;
67
68 const int nSegmentsLeft = settings.numberOfSegmentsLeft();
69 int segmentCounter = 0;
70
71 QString currentNumericLabel;
72 const QList<double> positions = segmentPositions( context, scaleContext, settings );
73
74 bool drawZero = true;
75 switch ( settings.labelHorizontalPlacement() )
76 {
78 drawZero = false;
79 break;
81 drawZero = true;
82 break;
83 }
84
85 const QgsNumericFormatContext numericContext;
86
87 for ( int i = 0; i < positions.size(); ++i )
88 {
89 if ( segmentCounter == 0 && nSegmentsLeft > 0 )
90 {
91 //label first left segment
92 currentNumericLabel = firstLabel;
93 }
94 else if ( segmentCounter != 0 && segmentCounter == nSegmentsLeft ) //reset label number to 0 if there are left segments
95 {
96 currentLabelNumber = 0.0;
97 }
98
99 if ( segmentCounter >= nSegmentsLeft )
100 {
101 currentNumericLabel = settings.numericFormat()->formatDouble( currentLabelNumber / settings.mapUnitsPerScaleBarUnit(), numericContext );
102 }
103
104 //don't draw label for intermediate left segments or the zero label when it needs to be skipped
105 if ( ( segmentCounter == 0 || segmentCounter >= nSegmentsLeft ) && ( currentNumericLabel != "0"_L1 || drawZero ) )
106 {
107 scaleScope->addVariable( QgsExpressionContextScope::StaticVariable( u"scale_value"_s, currentNumericLabel, true, false ) );
108 QPointF pos;
110 {
111 if ( segmentCounter == 0 )
112 {
113 // if the segment counter is zero with a non zero label, this is the left-of-zero label
114 pos.setX( context.convertToPainterUnits( positions.at( i ) + ( scaleContext.segmentWidth / 2 ), Qgis::RenderUnit::Millimeters ) );
115 }
116 else
117 {
118 pos.setX( context.convertToPainterUnits( positions.at( i ) - ( scaleContext.segmentWidth / 2 ), Qgis::RenderUnit::Millimeters ) );
119 }
120 }
121 else
122 {
123 pos.setX( context.convertToPainterUnits( positions.at( i ), Qgis::RenderUnit::Millimeters ) + xOffset );
124 }
125 pos.setY( fontMetrics.ascent() + scaledBoxContentSpace + ( settings.labelVerticalPlacement() == Qgis::ScaleBarDistanceLabelVerticalPlacement::BelowSegment ? scaledHeight + scaledLabelBarSpace : 0 ) );
126 QgsTextRenderer::drawText( pos, 0, Qgis::TextHorizontalAlignment::Center, QStringList() << currentNumericLabel, context, format );
127 }
128
129 if ( segmentCounter >= nSegmentsLeft )
130 {
131 currentLabelNumber += settings.unitsPerSegment();
132 }
133 ++segmentCounter;
134 }
135
136 //also draw the last label
137 if ( !positions.isEmpty() )
138 {
139 // note: this label is NOT centered over the end of the bar - rather the numeric portion
140 // of it is, without considering the unit label suffix. That's drawn at the end after
141 // horizontally centering just the numeric portion.
142 currentNumericLabel = settings.numericFormat()->formatDouble( currentLabelNumber / settings.mapUnitsPerScaleBarUnit(), numericContext );
143 scaleScope->addVariable( QgsExpressionContextScope::StaticVariable( u"scale_value"_s, currentNumericLabel, true, false ) );
144 QPointF pos;
145 pos.setY( fontMetrics.ascent() + scaledBoxContentSpace + ( settings.labelVerticalPlacement() == Qgis::ScaleBarDistanceLabelVerticalPlacement::BelowSegment ? scaledHeight + scaledLabelBarSpace : 0 ) );
147 {
148 pos.setX( context.convertToPainterUnits( positions.at( positions.size() - 1 ) + ( scaleContext.segmentWidth / 2 ), Qgis::RenderUnit::Millimeters ) + xOffset );
149 QgsTextRenderer::drawText( pos, 0, Qgis::TextHorizontalAlignment::Center, QStringList() << ( currentNumericLabel + ' ' + settings.unitLabel() ), context, format );
150 }
151 else
152 {
153 pos.setX( context.convertToPainterUnits( positions.at( positions.size() - 1 ) + scaleContext.segmentWidth, Qgis::RenderUnit::Millimeters ) + xOffset
154 - fontMetrics.horizontalAdvance( currentNumericLabel ) / 2.0 );
155 QgsTextRenderer::drawText( pos, 0, Qgis::TextHorizontalAlignment::Left, QStringList() << ( currentNumericLabel + ' ' + settings.unitLabel() ), context, format );
156 }
157 }
158
159 painter->restore();
160}
161
166
168{
169 return 100;
170}
171
173 const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const
174{
175 const QFont font = settings.textFormat().toQFont();
176
177 //consider centered first label
178 double firstLabelWidth = QgsLayoutUtils::textWidthMM( font, firstLabelString( settings ) );
180 {
181 if ( firstLabelWidth > scaleContext.segmentWidth )
182 {
183 firstLabelWidth = ( firstLabelWidth - scaleContext.segmentWidth ) / 2;
184 }
185 else
186 {
187 firstLabelWidth = 0.0;
188 }
189 }
190 else
191 {
192 firstLabelWidth = firstLabelWidth / 2;
193 }
194
195 //consider last number and label
196 const double largestLabelNumber = settings.numberOfSegments() * settings.unitsPerSegment() / settings.mapUnitsPerScaleBarUnit();
197 const QString largestNumberLabel = settings.numericFormat()->formatDouble( largestLabelNumber, QgsNumericFormatContext() );
198 const QString largestLabel = largestNumberLabel + ' ' + settings.unitLabel();
199 double largestLabelWidth;
201 {
202 largestLabelWidth = QgsLayoutUtils::textWidthMM( font, largestLabel );
203 if ( largestLabelWidth > scaleContext.segmentWidth )
204 {
205 largestLabelWidth = ( largestLabelWidth - scaleContext.segmentWidth ) / 2;
206 }
207 else
208 {
209 largestLabelWidth = 0.0;
210 }
211 }
212 else
213 {
214 largestLabelWidth = QgsLayoutUtils::textWidthMM( font, largestLabel ) - QgsLayoutUtils::textWidthMM( font, largestNumberLabel ) / 2;
215 }
216
217 const double totalBarLength = scaleContext.segmentWidth * ( settings.numberOfSegments() + ( settings.numberOfSegmentsLeft() > 0 ? 1 : 0 ) );
218
219 // this whole method is deprecated, so we can still call the deprecated settings.pen() getter
221 const double width = firstLabelWidth + totalBarLength + 2 * settings.pen().widthF() + largestLabelWidth + 2 * settings.boxContentSpace();
223
224 const double height = settings.height() + settings.labelBarSpace() + 2 * settings.boxContentSpace() + QgsLayoutUtils::fontAscentMM( font );
225
226 return QSizeF( width, height );
227}
228
230{
231 const double painterToMm = 1.0 / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters );
232 //consider centered first label
233 double firstLabelWidth = QgsTextRenderer::textWidth( context, settings.textFormat(), QStringList() << firstLabelString( settings ) ) * painterToMm;
234
236 {
237 if ( firstLabelWidth > scaleContext.segmentWidth )
238 {
239 firstLabelWidth = ( firstLabelWidth - scaleContext.segmentWidth ) / 2;
240 }
241 else
242 {
243 firstLabelWidth = 0.0;
244 }
245 }
246 else
247 {
248 firstLabelWidth = firstLabelWidth / 2;
249 }
250
251 //consider last number and label
252 const double largestLabelNumber = settings.numberOfSegments() * settings.unitsPerSegment() / settings.mapUnitsPerScaleBarUnit();
253 const QString largestNumberLabel = std::isnan( largestLabelNumber ) ? QString() : settings.numericFormat()->formatDouble( largestLabelNumber, QgsNumericFormatContext() );
254 const QString largestLabel = largestNumberLabel + ' ' + settings.unitLabel();
255 double largestLabelWidth;
257 {
258 largestLabelWidth = QgsTextRenderer::textWidth( context, settings.textFormat(), QStringList() << largestLabel ) * painterToMm;
259
260 if ( largestLabelWidth > scaleContext.segmentWidth )
261 {
262 largestLabelWidth = ( largestLabelWidth - scaleContext.segmentWidth ) / 2;
263 }
264 else
265 {
266 largestLabelWidth = 0.0;
267 }
268 }
269 else
270 {
271 largestLabelWidth = QgsTextRenderer::textWidth( context, settings.textFormat(), QStringList() << largestLabel ) * painterToMm
272 - QgsTextRenderer::textWidth( context, settings.textFormat(), QStringList() << largestNumberLabel ) * painterToMm / 2;
273 }
274
275 // segmentWidth can be NaN in extreme cases, eg trying to make a scalebar for a global map with a very small segment size (eg meters)
276 const double totalBarLength = std::isnan( scaleContext.segmentWidth ) ? 0
277 : scaleContext.segmentWidth * ( settings.numberOfSegments() + ( settings.numberOfSegmentsLeft() > 0 ? 1 : 0 ) );
278
279 double lineWidth = QgsSymbolLayerUtils::estimateMaxSymbolBleed( settings.lineSymbol(), context ) * 2;
280 // need to convert to mm
281 lineWidth /= context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters );
282
283 const double width = firstLabelWidth + totalBarLength + 2 * lineWidth + largestLabelWidth + 2 * settings.boxContentSpace();
284 double height;
285 if ( ( scaleContext.flags & Flag::FlagUsesSubdivisionsHeight ) && ( settings.numberOfSubdivisions() > 1 ) && ( settings.subdivisionsHeight() > settings.height() ) )
286 {
287 height = settings.subdivisionsHeight();
288 }
289 else
290 {
291 height = settings.height();
292 }
293
294 // TODO -- we technically should check the height of ALL labels here and take the maximum
295 height += settings.labelBarSpace() + 2 * settings.boxContentSpace() + QgsTextRenderer::textHeight( context, settings.textFormat(), QStringList() << largestLabel ) * painterToMm;
296
297 return QSizeF( width, height );
298}
299
301{
302 return false;
303}
304
306{
307 if ( settings.numberOfSegmentsLeft() > 0 )
308 {
309 return settings.numericFormat()->formatDouble( settings.unitsPerSegment() / settings.mapUnitsPerScaleBarUnit(), QgsNumericFormatContext() );
310 }
311 else
312 {
313 return settings.numericFormat()->formatDouble( 0, QgsNumericFormatContext() );
314 }
315}
316
318{
319 const QString firstLabel = firstLabelString( settings );
321 return QgsLayoutUtils::textWidthMM( settings.font(), firstLabel ) / 2.0;
323}
324
325double QgsScaleBarRenderer::firstLabelXOffset( const QgsScaleBarSettings &settings, const QgsRenderContext &context, const ScaleBarContext &scaleContext ) const
326{
327 const QString firstLabel = firstLabelString( settings );
328 double firstLabelWidth = QgsTextRenderer::textWidth( context, settings.textFormat(), QStringList() << firstLabel );
330 {
331 if ( firstLabelWidth > scaleContext.segmentWidth )
332 {
333 firstLabelWidth = ( firstLabelWidth - scaleContext.segmentWidth ) / 2;
334 }
335 else
336 {
337 firstLabelWidth = 0.0;
338 }
339 }
340 else
341 {
342 firstLabelWidth = firstLabelWidth / 2;
343 }
344 return firstLabelWidth;
345}
346
347QList<double> QgsScaleBarRenderer::segmentPositions( const ScaleBarContext &scaleContext, const QgsScaleBarSettings &settings ) const
348{
349 QList<double> positions;
350
351 // this whole method is deprecated, so calling a deprecated function is fine
353 double currentXCoord = settings.pen().widthF() + settings.boxContentSpace();
355
356 //left segments
357 const double leftSegmentSize = scaleContext.segmentWidth / settings.numberOfSegmentsLeft();
358 positions.reserve( settings.numberOfSegmentsLeft() + settings.numberOfSegments() );
359 for ( int i = 0; i < settings.numberOfSegmentsLeft(); ++i )
360 {
361 positions << currentXCoord;
362 currentXCoord += leftSegmentSize;
363 }
364
365 //right segments
366 for ( int i = 0; i < settings.numberOfSegments(); ++i )
367 {
368 positions << currentXCoord;
369 currentXCoord += scaleContext.segmentWidth;
370 }
371 return positions;
372}
373
375{
376 QList<double> positions;
377
378 double lineWidth = QgsSymbolLayerUtils::estimateMaxSymbolBleed( settings.lineSymbol(), context ) * 2.0;
379 // need to convert to mm
380 lineWidth /= context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters );
381
382 double currentXCoord = lineWidth + settings.boxContentSpace();
383
384 //left segments
385 const double leftSegmentSize = scaleContext.segmentWidth / settings.numberOfSegmentsLeft();
386 positions.reserve( settings.numberOfSegmentsLeft() + settings.numberOfSegments() );
387 for ( int i = 0; i < settings.numberOfSegmentsLeft(); ++i )
388 {
389 positions << currentXCoord;
390 currentXCoord += leftSegmentSize;
391 }
392
393 //right segments
394 for ( int i = 0; i < settings.numberOfSegments(); ++i )
395 {
396 positions << currentXCoord;
397 currentXCoord += scaleContext.segmentWidth;
398 }
399 return positions;
400}
401
402QList<double> QgsScaleBarRenderer::segmentWidths( const ScaleBarContext &scaleContext, const QgsScaleBarSettings &settings ) const
403{
404 QList<double> widths;
405 widths.reserve( settings.numberOfSegmentsLeft() + settings.numberOfSegments() );
406
407 //left segments
408 if ( settings.numberOfSegmentsLeft() > 0 )
409 {
410 const double leftSegmentSize = scaleContext.segmentWidth / settings.numberOfSegmentsLeft();
411 for ( int i = 0; i < settings.numberOfSegmentsLeft(); ++i )
412 {
413 widths << leftSegmentSize;
414 }
415 }
416
417 //right segments
418 for ( int i = 0; i < settings.numberOfSegments(); ++i )
419 {
420 widths << scaleContext.segmentWidth;
421 }
422
423 return widths;
424}
425
427{
428 return !std::isnan( segmentWidth );
429}
@ CenteredSegment
Labels are drawn centered relative to segment.
Definition qgis.h:5419
@ CenteredEdge
Labels are drawn centered relative to segment's edge.
Definition qgis.h:5418
@ BelowSegment
Labels are drawn below the scalebar.
Definition qgis.h:5405
@ Millimeters
Millimeters.
Definition qgis.h:5256
@ Center
Center align.
Definition qgis.h:3002
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.
double convertToPainterUnits(double size, Qgis::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale(), Qgis::RenderSubcomponentProperty property=Qgis::RenderSubcomponentProperty::Generic) const
Converts a size from the specified units to painter units (pixels).
QPainter * painter()
Returns the destination QPainter for the render operation.
QgsExpressionContext & expressionContext()
Gets the expression context.
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.
@ FlagUsesSubdivisionsHeight
Renderer uses the scalebar subdivisions height (see QgsScaleBarSettings::subdivisionsHeight() ).
Stores the appearance and layout settings for scalebar drawing with QgsScaleBarRenderer.
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.
Qgis::ScaleBarDistanceLabelVerticalPlacement labelVerticalPlacement() const
Returns the vertical placement of text labels.
double unitsPerSegment() const
Returns the number of scalebar units per segment.
QgsTextFormat & textFormat()
Returns the text format used for drawing text in the scalebar.
Q_DECL_DEPRECATED QPen pen() const
Returns the pen used for drawing outlines in the scalebar.
double boxContentSpace() const
Returns the spacing (margin) between the scalebar box and content in millimeters.
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...
Qgis::ScaleBarDistanceLabelHorizontalPlacement labelHorizontalPlacement() const
Returns the horizontal placement of text labels.
Q_DECL_DEPRECATED QFont font() const
Returns the font used for drawing text in the scalebar.
double labelBarSpace() const
Returns the spacing (in millimeters) between labels and 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.
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.
static void drawText(const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, bool drawAsOutlines=true, Qgis::TextVerticalAlignment vAlignment=Qgis::TextVerticalAlignment::Top, Qgis::TextRendererFlags flags=Qgis::TextRendererFlags(), Qgis::TextLayoutMode mode=Qgis::TextLayoutMode::Rectangle)
Draws text within a rectangle using the specified settings.
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 double textHeight(const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, Qgis::TextLayoutMode mode=Qgis::TextLayoutMode::Point, QFontMetricsF *fontMetrics=nullptr, Qgis::TextRendererFlags flags=Qgis::TextRendererFlags(), double maxLineWidth=0)
Returns the height of a text based on a given format.
#define Q_NOWARN_DEPRECATED_POP
Definition qgis.h:7451
#define Q_NOWARN_DEPRECATED_PUSH
Definition qgis.h:7450
Single variable definition for use within a QgsExpressionContextScope.
Contains parameters regarding scalebar calculations.
bool isValid() const
Returns true if the context has valid settings.
QgsScaleBarRenderer::Flags flags
Scalebar renderer flags.
double segmentWidth
The width, in millimeters, of each individual segment drawn.