QGIS API Documentation 4.0.0-Norrköping (1ddcee3d0e4)
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(
126 fontMetrics.ascent() + scaledBoxContentSpace + ( settings.labelVerticalPlacement() == Qgis::ScaleBarDistanceLabelVerticalPlacement::BelowSegment ? scaledHeight + scaledLabelBarSpace : 0 )
127 );
128 QgsTextRenderer::drawText( pos, 0, Qgis::TextHorizontalAlignment::Center, QStringList() << currentNumericLabel, context, format );
129 }
130
131 if ( segmentCounter >= nSegmentsLeft )
132 {
133 currentLabelNumber += settings.unitsPerSegment();
134 }
135 ++segmentCounter;
136 }
137
138 //also draw the last label
139 if ( !positions.isEmpty() )
140 {
141 // note: this label is NOT centered over the end of the bar - rather the numeric portion
142 // of it is, without considering the unit label suffix. That's drawn at the end after
143 // horizontally centering just the numeric portion.
144 currentNumericLabel = settings.numericFormat()->formatDouble( currentLabelNumber / settings.mapUnitsPerScaleBarUnit(), numericContext );
145 scaleScope->addVariable( QgsExpressionContextScope::StaticVariable( u"scale_value"_s, currentNumericLabel, true, false ) );
146 QPointF pos;
147 pos.setY( fontMetrics.ascent() + scaledBoxContentSpace + ( settings.labelVerticalPlacement() == Qgis::ScaleBarDistanceLabelVerticalPlacement::BelowSegment ? scaledHeight + scaledLabelBarSpace : 0 ) );
149 {
150 pos.setX( context.convertToPainterUnits( positions.at( positions.size() - 1 ) + ( scaleContext.segmentWidth / 2 ), Qgis::RenderUnit::Millimeters ) + xOffset );
151 QgsTextRenderer::drawText( pos, 0, Qgis::TextHorizontalAlignment::Center, QStringList() << ( currentNumericLabel + ' ' + settings.unitLabel() ), context, format );
152 }
153 else
154 {
155 pos.setX(
156 context.convertToPainterUnits( positions.at( positions.size() - 1 ) + scaleContext.segmentWidth, Qgis::RenderUnit::Millimeters ) + xOffset - fontMetrics.horizontalAdvance( currentNumericLabel ) / 2.0
157 );
158 QgsTextRenderer::drawText( pos, 0, Qgis::TextHorizontalAlignment::Left, QStringList() << ( currentNumericLabel + ' ' + settings.unitLabel() ), context, format );
159 }
160 }
161
162 painter->restore();
163}
164
169
171{
172 return 100;
173}
174
176{
177 const QFont font = settings.textFormat().toQFont();
178
179 //consider centered first label
180 double firstLabelWidth = QgsLayoutUtils::textWidthMM( font, firstLabelString( settings ) );
182 {
183 if ( firstLabelWidth > scaleContext.segmentWidth )
184 {
185 firstLabelWidth = ( firstLabelWidth - scaleContext.segmentWidth ) / 2;
186 }
187 else
188 {
189 firstLabelWidth = 0.0;
190 }
191 }
192 else
193 {
194 firstLabelWidth = firstLabelWidth / 2;
195 }
196
197 //consider last number and label
198 const double largestLabelNumber = settings.numberOfSegments() * settings.unitsPerSegment() / settings.mapUnitsPerScaleBarUnit();
199 const QString largestNumberLabel = settings.numericFormat()->formatDouble( largestLabelNumber, QgsNumericFormatContext() );
200 const QString largestLabel = largestNumberLabel + ' ' + settings.unitLabel();
201 double largestLabelWidth;
203 {
204 largestLabelWidth = QgsLayoutUtils::textWidthMM( font, largestLabel );
205 if ( largestLabelWidth > scaleContext.segmentWidth )
206 {
207 largestLabelWidth = ( largestLabelWidth - scaleContext.segmentWidth ) / 2;
208 }
209 else
210 {
211 largestLabelWidth = 0.0;
212 }
213 }
214 else
215 {
216 largestLabelWidth = QgsLayoutUtils::textWidthMM( font, largestLabel ) - QgsLayoutUtils::textWidthMM( font, largestNumberLabel ) / 2;
217 }
218
219 const double totalBarLength = scaleContext.segmentWidth * ( settings.numberOfSegments() + ( settings.numberOfSegmentsLeft() > 0 ? 1 : 0 ) );
220
221 // this whole method is deprecated, so we can still call the deprecated settings.pen() getter
223 const double width = firstLabelWidth + totalBarLength + 2 * settings.pen().widthF() + largestLabelWidth + 2 * settings.boxContentSpace();
225
226 const double height = settings.height() + settings.labelBarSpace() + 2 * settings.boxContentSpace() + QgsLayoutUtils::fontAscentMM( font );
227
228 return QSizeF( width, height );
229}
230
232{
233 const double painterToMm = 1.0 / context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters );
234 //consider centered first label
235 double firstLabelWidth = QgsTextRenderer::textWidth( context, settings.textFormat(), QStringList() << firstLabelString( settings ) ) * painterToMm;
236
238 {
239 if ( firstLabelWidth > scaleContext.segmentWidth )
240 {
241 firstLabelWidth = ( firstLabelWidth - scaleContext.segmentWidth ) / 2;
242 }
243 else
244 {
245 firstLabelWidth = 0.0;
246 }
247 }
248 else
249 {
250 firstLabelWidth = firstLabelWidth / 2;
251 }
252
253 //consider last number and label
254 const double largestLabelNumber = settings.numberOfSegments() * settings.unitsPerSegment() / settings.mapUnitsPerScaleBarUnit();
255 const QString largestNumberLabel = std::isnan( largestLabelNumber ) ? QString() : settings.numericFormat()->formatDouble( largestLabelNumber, QgsNumericFormatContext() );
256 const QString largestLabel = largestNumberLabel + ' ' + settings.unitLabel();
257 double largestLabelWidth;
259 {
260 largestLabelWidth = QgsTextRenderer::textWidth( context, settings.textFormat(), QStringList() << largestLabel ) * painterToMm;
261
262 if ( largestLabelWidth > scaleContext.segmentWidth )
263 {
264 largestLabelWidth = ( largestLabelWidth - scaleContext.segmentWidth ) / 2;
265 }
266 else
267 {
268 largestLabelWidth = 0.0;
269 }
270 }
271 else
272 {
273 largestLabelWidth = QgsTextRenderer::textWidth( context, settings.textFormat(), QStringList() << largestLabel ) * painterToMm
274 - QgsTextRenderer::textWidth( context, settings.textFormat(), QStringList() << largestNumberLabel ) * painterToMm / 2;
275 }
276
277 // 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)
278 const double totalBarLength = std::isnan( scaleContext.segmentWidth ) ? 0 : scaleContext.segmentWidth * ( settings.numberOfSegments() + ( settings.numberOfSegmentsLeft() > 0 ? 1 : 0 ) );
279
280 double lineWidth = QgsSymbolLayerUtils::estimateMaxSymbolBleed( settings.lineSymbol(), context ) * 2;
281 // need to convert to mm
282 lineWidth /= context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters );
283
284 const double width = firstLabelWidth + totalBarLength + 2 * lineWidth + largestLabelWidth + 2 * settings.boxContentSpace();
285 double height;
286 if ( ( scaleContext.flags & Flag::FlagUsesSubdivisionsHeight ) && ( settings.numberOfSubdivisions() > 1 ) && ( settings.subdivisionsHeight() > settings.height() ) )
287 {
288 height = settings.subdivisionsHeight();
289 }
290 else
291 {
292 height = settings.height();
293 }
294
295 // TODO -- we technically should check the height of ALL labels here and take the maximum
296 height += settings.labelBarSpace() + 2 * settings.boxContentSpace() + QgsTextRenderer::textHeight( context, settings.textFormat(), QStringList() << largestLabel ) * painterToMm;
297
298 return QSizeF( width, height );
299}
300
302{
303 return false;
304}
305
307{
308 if ( settings.numberOfSegmentsLeft() > 0 )
309 {
310 return settings.numericFormat()->formatDouble( settings.unitsPerSegment() / settings.mapUnitsPerScaleBarUnit(), QgsNumericFormatContext() );
311 }
312 else
313 {
314 return settings.numericFormat()->formatDouble( 0, QgsNumericFormatContext() );
315 }
316}
317
319{
320 const QString firstLabel = firstLabelString( settings );
322 return QgsLayoutUtils::textWidthMM( settings.font(), firstLabel ) / 2.0;
324}
325
326double QgsScaleBarRenderer::firstLabelXOffset( const QgsScaleBarSettings &settings, const QgsRenderContext &context, const ScaleBarContext &scaleContext ) const
327{
328 const QString firstLabel = firstLabelString( settings );
329 double firstLabelWidth = QgsTextRenderer::textWidth( context, settings.textFormat(), QStringList() << firstLabel );
331 {
332 if ( firstLabelWidth > scaleContext.segmentWidth )
333 {
334 firstLabelWidth = ( firstLabelWidth - scaleContext.segmentWidth ) / 2;
335 }
336 else
337 {
338 firstLabelWidth = 0.0;
339 }
340 }
341 else
342 {
343 firstLabelWidth = firstLabelWidth / 2;
344 }
345 return firstLabelWidth;
346}
347
348QList<double> QgsScaleBarRenderer::segmentPositions( const ScaleBarContext &scaleContext, const QgsScaleBarSettings &settings ) const
349{
350 QList<double> positions;
351
352 // this whole method is deprecated, so calling a deprecated function is fine
354 double currentXCoord = settings.pen().widthF() + settings.boxContentSpace();
356
357 //left segments
358 const double leftSegmentSize = scaleContext.segmentWidth / settings.numberOfSegmentsLeft();
359 positions.reserve( settings.numberOfSegmentsLeft() + settings.numberOfSegments() );
360 for ( int i = 0; i < settings.numberOfSegmentsLeft(); ++i )
361 {
362 positions << currentXCoord;
363 currentXCoord += leftSegmentSize;
364 }
365
366 //right segments
367 for ( int i = 0; i < settings.numberOfSegments(); ++i )
368 {
369 positions << currentXCoord;
370 currentXCoord += scaleContext.segmentWidth;
371 }
372 return positions;
373}
374
376{
377 QList<double> positions;
378
379 double lineWidth = QgsSymbolLayerUtils::estimateMaxSymbolBleed( settings.lineSymbol(), context ) * 2.0;
380 // need to convert to mm
381 lineWidth /= context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters );
382
383 double currentXCoord = lineWidth + settings.boxContentSpace();
384
385 //left segments
386 const double leftSegmentSize = scaleContext.segmentWidth / settings.numberOfSegmentsLeft();
387 positions.reserve( settings.numberOfSegmentsLeft() + settings.numberOfSegments() );
388 for ( int i = 0; i < settings.numberOfSegmentsLeft(); ++i )
389 {
390 positions << currentXCoord;
391 currentXCoord += leftSegmentSize;
392 }
393
394 //right segments
395 for ( int i = 0; i < settings.numberOfSegments(); ++i )
396 {
397 positions << currentXCoord;
398 currentXCoord += scaleContext.segmentWidth;
399 }
400 return positions;
401}
402
403QList<double> QgsScaleBarRenderer::segmentWidths( const ScaleBarContext &scaleContext, const QgsScaleBarSettings &settings ) const
404{
405 QList<double> widths;
406 widths.reserve( settings.numberOfSegmentsLeft() + settings.numberOfSegments() );
407
408 //left segments
409 if ( settings.numberOfSegmentsLeft() > 0 )
410 {
411 const double leftSegmentSize = scaleContext.segmentWidth / settings.numberOfSegmentsLeft();
412 for ( int i = 0; i < settings.numberOfSegmentsLeft(); ++i )
413 {
414 widths << leftSegmentSize;
415 }
416 }
417
418 //right segments
419 for ( int i = 0; i < settings.numberOfSegments(); ++i )
420 {
421 widths << scaleContext.segmentWidth;
422 }
423
424 return widths;
425}
426
428{
429 return !std::isnan( segmentWidth );
430}
@ CenteredSegment
Labels are drawn centered relative to segment.
Definition qgis.h:5509
@ CenteredEdge
Labels are drawn centered relative to segment's edge.
Definition qgis.h:5508
@ BelowSegment
Labels are drawn below the scalebar.
Definition qgis.h:5495
@ Millimeters
Millimeters.
Definition qgis.h:5341
@ Center
Center align.
Definition qgis.h:3045
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:7504
#define Q_NOWARN_DEPRECATED_PUSH
Definition qgis.h:7503
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.