KChart

KChartCartesianGrid.cpp
1 /*
2  * SPDX-FileCopyrightText: 2001-2015 Klaralvdalens Datakonsult AB. All rights reserved.
3  *
4  * This file is part of the KD Chart library.
5  *
6  * SPDX-License-Identifier: GPL-2.0-or-later
7  */
8 
9 #include "KChartCartesianGrid.h"
10 #include "KChartAbstractCartesianDiagram.h"
11 #include "KChartPaintContext.h"
12 #include "KChartPainterSaver_p.h"
13 #include "KChartPrintingParameters.h"
14 #include "KChartFrameAttributes.h"
15 #include "KChartCartesianAxis_p.h"
16 #include "KChartMath_p.h"
17 
18 #include <QPainter>
19 #include <QPainterPath>
20 
21 using namespace KChart;
22 
23 CartesianGrid::CartesianGrid()
24  : AbstractGrid(), m_minsteps( 2 ), m_maxsteps( 12 )
25 {
26 }
27 
28 CartesianGrid::~CartesianGrid()
29 {
30 }
31 
32 int CartesianGrid::minimalSteps() const
33 {
34  return m_minsteps;
35 }
36 
37 void CartesianGrid::setMinimalSteps(int minsteps)
38 {
39  m_minsteps = minsteps;
40 }
41 
42 int CartesianGrid::maximalSteps() const
43 {
44  return m_maxsteps;
45 }
46 
47 void CartesianGrid::setMaximalSteps(int maxsteps)
48 {
49  m_maxsteps = maxsteps;
50 }
51 
53 {
54  CartesianCoordinatePlane* plane = qobject_cast< CartesianCoordinatePlane* >( context->coordinatePlane() );
55  const GridAttributes gridAttrsX( plane->gridAttributes( Qt::Horizontal ) );
56  const GridAttributes gridAttrsY( plane->gridAttributes( Qt::Vertical ) );
57  if ( !gridAttrsX.isGridVisible() && !gridAttrsX.isSubGridVisible() &&
58  !gridAttrsY.isGridVisible() && !gridAttrsY.isSubGridVisible() ) {
59  return;
60  }
61  // This plane is used for translating the coordinates - not for the data boundaries
62  QPainter *p = context->painter();
63  PainterSaver painterSaver( p );
64  // sharedAxisMasterPlane() changes the painter's coordinate transformation(!)
65  plane = qobject_cast< CartesianCoordinatePlane* >( plane->sharedAxisMasterPlane( context->painter() ) );
66  Q_ASSERT_X ( plane, "CartesianGrid::drawGrid",
67  "Bad function call: PaintContext::coodinatePlane() NOT a cartesian plane." );
68 
69  // update the calculated mDataDimensions before using them
70  updateData( context->coordinatePlane() ); // this, in turn, calls our calculateGrid().
71  Q_ASSERT_X ( mDataDimensions.count() == 2, "CartesianGrid::drawGrid",
72  "Error: updateData did not return exactly two dimensions." );
73  if ( !isBoundariesValid( mDataDimensions ) ) {
74  return;
75  }
76 
77  const DataDimension dimX = mDataDimensions.first();
78  const DataDimension dimY = mDataDimensions.last();
79  const bool isLogarithmicX = dimX.calcMode == AbstractCoordinatePlane::Logarithmic;
80  const bool isLogarithmicY = dimY.calcMode == AbstractCoordinatePlane::Logarithmic;
81 
82  qreal minValueX = qMin( dimX.start, dimX.end );
83  qreal maxValueX = qMax( dimX.start, dimX.end );
84  qreal minValueY = qMin( dimY.start, dimY.end );
85  qreal maxValueY = qMax( dimY.start, dimY.end );
86  {
87  bool adjustXLower = !isLogarithmicX && gridAttrsX.adjustLowerBoundToGrid();
88  bool adjustXUpper = !isLogarithmicX && gridAttrsX.adjustUpperBoundToGrid();
89  bool adjustYLower = !isLogarithmicY && gridAttrsY.adjustLowerBoundToGrid();
90  bool adjustYUpper = !isLogarithmicY && gridAttrsY.adjustUpperBoundToGrid();
91  AbstractGrid::adjustLowerUpperRange( minValueX, maxValueX, dimX.stepWidth, adjustXLower, adjustXUpper );
92  AbstractGrid::adjustLowerUpperRange( minValueY, maxValueY, dimY.stepWidth, adjustYLower, adjustYUpper );
93  }
94 
95  if ( plane->frameAttributes().isVisible() ) {
96  const qreal radius = plane->frameAttributes().cornerRadius();
97  QPainterPath path;
98  path.addRoundedRect( QRectF( plane->translate( QPointF( minValueX, minValueY ) ),
99  plane->translate( QPointF( maxValueX, maxValueY ) ) ),
100  radius, radius );
101  context->painter()->setClipPath( path );
102  }
103 
104  /* TODO features from old code:
105  - MAYBE coarsen the main grid when it gets crowded (do it in calculateGrid or here?)
106  if ( ! dimX.isCalculated ) {
107  while ( screenRangeX / numberOfUnitLinesX <= MinimumPixelsBetweenLines ) {
108  dimX.stepWidth *= 10.0;
109  dimX.subStepWidth *= 10.0;
110  numberOfUnitLinesX = qAbs( dimX.distance() / dimX.stepWidth );
111  }
112  }
113  - MAYBE deactivate the sub-grid when it gets crowded
114  if ( dimX.subStepWidth && (screenRangeX / (dimX.distance() / dimX.subStepWidth)
115  <= MinimumPixelsBetweenLines) ) {
116  // de-activating grid sub steps: not enough space
117  dimX.subStepWidth = 0.0;
118  }
119  */
120 
121  for ( int i = 0; i < 2; i++ ) {
122  XySwitch xy( i == 1 ); // first iteration paints the X grid lines, second paints the Y grid lines
123  const GridAttributes& gridAttrs = xy( gridAttrsX, gridAttrsY );
124  bool hasMajorLines = gridAttrs.isGridVisible();
125  bool hasMinorLines = hasMajorLines && gridAttrs.isSubGridVisible();
126  if ( !hasMajorLines && !hasMinorLines ) {
127  continue;
128  }
129 
130  const DataDimension& dimension = xy( dimX, dimY );
131  const bool drawZeroLine = dimension.isCalculated && gridAttrs.zeroLinePen().style() != Qt::NoPen;
132 
133  QPointF lineStart = QPointF( minValueX, minValueY ); // still need transformation to screen space
134  QPointF lineEnd = QPointF( maxValueX, maxValueY );
135 
136  TickIterator it( xy.isY, dimension, gridAttrs.linesOnAnnotations(),
137  hasMajorLines, hasMinorLines, plane );
138  for ( ; !it.isAtEnd(); ++it ) {
139  if ( !gridAttrs.isOuterLinesVisible() &&
140  ( it.areAlmostEqual( it.position(), xy( minValueX, minValueY ) ) ||
141  it.areAlmostEqual( it.position(), xy( maxValueX, maxValueY ) ) ) ) {
142  continue;
143  }
144  xy.lvalue( lineStart.rx(), lineStart.ry() ) = it.position();
145  xy.lvalue( lineEnd.rx(), lineEnd.ry() ) = it.position();
146  QPointF transLineStart = plane->translate( lineStart );
147  QPointF transLineEnd = plane->translate( lineEnd );
148  if ( ISNAN( transLineStart.x() ) || ISNAN( transLineStart.y() ) ||
149  ISNAN( transLineEnd.x() ) || ISNAN( transLineEnd.y() ) ) {
150  // ### can we catch NaN problems earlier, wasting fewer cycles?
151  continue;
152  }
153  if ( it.position() == 0.0 && drawZeroLine ) {
154  p->setPen( PrintingParameters::scalePen( gridAttrsX.zeroLinePen() ) );
155  } else if ( it.type() == TickIterator::MinorTick ) {
156  p->setPen( PrintingParameters::scalePen( gridAttrs.subGridPen() ) );
157  } else {
158  p->setPen( PrintingParameters::scalePen( gridAttrs.gridPen() ) );
159  }
160  p->drawLine( transLineStart, transLineEnd );
161  }
162  }
163 }
164 
165 
166 DataDimensionsList CartesianGrid::calculateGrid( const DataDimensionsList& rawDataDimensions ) const
167 {
168  Q_ASSERT_X ( rawDataDimensions.count() == 2, "CartesianGrid::calculateGrid",
169  "Error: calculateGrid() expects a list with exactly two entries." );
170 
171  CartesianCoordinatePlane* plane = qobject_cast< CartesianCoordinatePlane* >( mPlane );
172  Q_ASSERT_X ( plane, "CartesianGrid::calculateGrid",
173  "Error: PaintContext::calculatePlane() called, but no cartesian plane set." );
174 
175  DataDimensionsList l( rawDataDimensions );
176 #if 0
177  qDebug() << Q_FUNC_INFO << "initial grid X-range:" << l.first().start << "->" << l.first().end
178  << " substep width:" << l.first().subStepWidth;
179  qDebug() << Q_FUNC_INFO << "initial grid Y-range:" << l.last().start << "->" << l.last().end
180  << " substep width:" << l.last().subStepWidth;
181 #endif
182  // rule: Returned list is either empty, or it is providing two
183  // valid dimensions, complete with two non-Zero step widths.
184  if ( isBoundariesValid( l ) ) {
185  const QPointF translatedBottomLeft( plane->translateBack( plane->geometry().bottomLeft() ) );
186  const QPointF translatedTopRight( plane->translateBack( plane->geometry().topRight() ) );
187 
188  const GridAttributes gridAttrsX( plane->gridAttributes( Qt::Horizontal ) );
189  const GridAttributes gridAttrsY( plane->gridAttributes( Qt::Vertical ) );
190 
191  const DataDimension dimX
192  = calculateGridXY( l.first(), Qt::Horizontal,
193  gridAttrsX.adjustLowerBoundToGrid(),
194  gridAttrsX.adjustUpperBoundToGrid() );
195  if ( dimX.stepWidth ) {
196  //qDebug("CartesianGrid::calculateGrid() l.last().start: %f l.last().end: %f", l.last().start, l.last().end);
197  //qDebug(" l.first().start: %f l.first().end: %f", l.first().start, l.first().end);
198 
199  // one time for the min/max value
200  const DataDimension minMaxY
201  = calculateGridXY( l.last(), Qt::Vertical,
202  gridAttrsY.adjustLowerBoundToGrid(),
203  gridAttrsY.adjustUpperBoundToGrid() );
204 
205  if ( plane->autoAdjustGridToZoom()
206  && plane->axesCalcModeY() == CartesianCoordinatePlane::Linear
207  && plane->zoomFactorY() > 1.0 )
208  {
209  l.last().start = translatedBottomLeft.y();
210  l.last().end = translatedTopRight.y();
211  }
212  // and one other time for the step width
213  const DataDimension dimY
214  = calculateGridXY( l.last(), Qt::Vertical,
215  gridAttrsY.adjustLowerBoundToGrid(),
216  gridAttrsY.adjustUpperBoundToGrid() );
217  if ( dimY.stepWidth ) {
218  l.first().start = dimX.start;
219  l.first().end = dimX.end;
220  l.first().stepWidth = dimX.stepWidth;
221  l.first().subStepWidth = dimX.subStepWidth;
222  l.last().start = minMaxY.start;
223  l.last().end = minMaxY.end;
224  l.last().stepWidth = dimY.stepWidth;
225  l.last().subStepWidth = dimY.subStepWidth;
226  //qDebug() << "CartesianGrid::calculateGrid() final grid y-range:" << l.last().end - l.last().start << " step width:" << l.last().stepWidth << endl;
227  // calculate some reasonable subSteps if the
228  // user did not set the sub grid but did set
229  // the stepWidth.
230 
231  // FIXME (Johannes)
232  // the last (y) dimension is not always the dimension for the ordinate!
233  // since there's no way to check for the orientation of this dimension here,
234  // we cannot automatically assume substep values
235  //if ( dimY.subStepWidth == 0 )
236  // l.last().subStepWidth = dimY.stepWidth/2;
237  //else
238  // l.last().subStepWidth = dimY.subStepWidth;
239  }
240  }
241  }
242 #if 0
243  qDebug() << Q_FUNC_INFO << "final grid X-range:" << l.first().start << "->" << l.first().end
244  << " substep width:" << l.first().subStepWidth;
245  qDebug() << Q_FUNC_INFO << "final grid Y-range:" << l.last().start << "->" << l.last().end
246  << " substep width:" << l.last().subStepWidth;
247 #endif
248  return l;
249 }
250 
251 qreal fastPow10( int x )
252 {
253  qreal res = 1.0;
254  if ( 0 <= x ) {
255  for ( int i = 1; i <= x; ++i )
256  res *= 10.0;
257  } else {
258  for ( int i = -1; i >= x; --i )
259  res *= 0.1;
260  }
261  return res;
262 }
263 
264 #ifdef Q_OS_WIN
265 #define trunc(x) ((int)(x))
266 #endif
267 
268 DataDimension CartesianGrid::calculateGridXY(
269  const DataDimension& rawDataDimension,
270  Qt::Orientation orientation,
271  bool adjustLower, bool adjustUpper ) const
272 {
273  CartesianCoordinatePlane* const plane = dynamic_cast<CartesianCoordinatePlane*>( mPlane );
274  if ( ( orientation == Qt::Vertical && plane->autoAdjustVerticalRangeToData() >= 100 ) ||
275  ( orientation == Qt::Horizontal && plane->autoAdjustHorizontalRangeToData() >= 100 ) ) {
276  adjustLower = false;
277  adjustUpper = false;
278  }
279 
280  DataDimension dim( rawDataDimension );
281  if ( dim.isCalculated && dim.start != dim.end ) {
282  if ( dim.calcMode == AbstractCoordinatePlane::Linear ) {
283  // linear ( == not-logarithmic) calculation
284  if ( dim.stepWidth == 0.0 ) {
285  QList<qreal> granularities;
286  switch ( dim.sequence ) {
287  case KChartEnums::GranularitySequence_10_20:
288  granularities << 1.0 << 2.0;
289  break;
290  case KChartEnums::GranularitySequence_10_50:
291  granularities << 1.0 << 5.0;
292  break;
293  case KChartEnums::GranularitySequence_25_50:
294  granularities << 2.5 << 5.0;
295  break;
296  case KChartEnums::GranularitySequence_125_25:
297  granularities << 1.25 << 2.5;
298  break;
299  case KChartEnums::GranularitySequenceIrregular:
300  granularities << 1.0 << 1.25 << 2.0 << 2.5 << 5.0;
301  break;
302  }
303  //qDebug("CartesianGrid::calculateGridXY() dim.start: %f dim.end: %f", dim.start, dim.end);
304  calculateStepWidth(
305  dim.start, dim.end, granularities, orientation,
306  dim.stepWidth, dim.subStepWidth,
307  adjustLower, adjustUpper );
308  }
309  // if needed, adjust start/end to match the step width:
310  //qDebug() << "CartesianGrid::calculateGridXY() has 1st linear range: min " << dim.start << " and max" << dim.end;
311 
312  AbstractGrid::adjustLowerUpperRange( dim.start, dim.end, dim.stepWidth,
313  adjustLower, adjustUpper );
314  //qDebug() << "CartesianGrid::calculateGridXY() returns linear range: min " << dim.start << " and max" << dim.end;
315  } else {
316  // logarithmic calculation with negative values
317  if ( dim.end <= 0 )
318  {
319  qreal min;
320  const qreal minRaw = qMin( dim.start, dim.end );
321  const int minLog = -static_cast<int>(trunc( log10( -minRaw ) ) );
322  if ( minLog >= 0 )
323  min = qMin( minRaw, -std::numeric_limits< qreal >::epsilon() );
324  else
325  min = -fastPow10( -(minLog-1) );
326 
327  qreal max;
328  const qreal maxRaw = qMin( -std::numeric_limits< qreal >::epsilon(), qMax( dim.start, dim.end ) );
329  const int maxLog = -static_cast<int>(ceil( log10( -maxRaw ) ) );
330  if ( maxLog >= 0 )
331  max = -1;
332  else if ( fastPow10( -maxLog ) < maxRaw )
333  max = -fastPow10( -(maxLog+1) );
334  else
335  max = -fastPow10( -maxLog );
336  if ( adjustLower )
337  dim.start = min;
338  if ( adjustUpper )
339  dim.end = max;
340  dim.stepWidth = -pow( 10.0, ceil( log10( qAbs( max - min ) / 10.0 ) ) );
341  }
342  // logarithmic calculation (ignoring all negative values)
343  else
344  {
345  qreal min;
346  const qreal minRaw = qMax( qMin( dim.start, dim.end ), qreal( 0.0 ) );
347  const int minLog = static_cast<int>(trunc( log10( minRaw ) ) );
348  if ( minLog <= 0 && dim.end < 1.0 )
349  min = qMax( minRaw, std::numeric_limits< qreal >::epsilon() );
350  else if ( minLog <= 0 )
351  min = qMax( qreal(0.00001), dim.start );
352  else
353  min = fastPow10( minLog-1 );
354 
355  // Uh oh. Logarithmic scaling doesn't work with a lower or upper
356  // bound being 0.
357  const bool zeroBound = dim.start == 0.0 || dim.end == 0.0;
358 
359  qreal max;
360  const qreal maxRaw = qMax( qMax( dim.start, dim.end ), qreal( 0.0 ) );
361  const int maxLog = static_cast<int>(ceil( log10( maxRaw ) ) );
362  if ( maxLog <= 0 )
363  max = 1;
364  else if ( fastPow10( maxLog ) < maxRaw )
365  max = fastPow10( maxLog+1 );
366  else
367  max = fastPow10( maxLog );
368  if ( adjustLower || zeroBound )
369  dim.start = min;
370  if ( adjustUpper || zeroBound )
371  dim.end = max;
372  dim.stepWidth = pow( 10.0, ceil( log10( qAbs( max - min ) / 10.0 ) ) );
373  }
374  }
375  } else {
376  //qDebug() << "CartesianGrid::calculateGridXY() returns stepWidth 1.0 !!";
377  // Do not ignore the user configuration
378  dim.stepWidth = dim.stepWidth ? dim.stepWidth : 1.0;
379  }
380  return dim;
381 }
382 
383 
384 static void calculateSteps(
385  qreal start_, qreal end_, const QList<qreal>& list,
386  int minSteps, int maxSteps,
387  int power,
388  qreal& steps, qreal& stepWidth,
389  bool adjustLower, bool adjustUpper )
390 {
391  //qDebug("-----------------------------------\nstart: %f end: %f power-of-ten: %i", start_, end_, power);
392 
393  qreal distance = 0.0;
394  steps = 0.0;
395 
396  const int lastIdx = list.count()-1;
397  for ( int i = 0; i <= lastIdx; ++i ) {
398  const qreal testStepWidth = list.at(lastIdx - i) * fastPow10( power );
399  //qDebug( "testing step width: %f", testStepWidth);
400  qreal start = qMin( start_, end_ );
401  qreal end = qMax( start_, end_ );
402  //qDebug("pre adjusting start: %f end: %f", start, end);
403  AbstractGrid::adjustLowerUpperRange( start, end, testStepWidth, adjustLower, adjustUpper );
404  //qDebug("post adjusting start: %f end: %f", start, end);
405 
406  const qreal testDistance = qAbs(end - start);
407  const qreal testSteps = testDistance / testStepWidth;
408 
409  //qDebug() << "testDistance:" << testDistance << " distance:" << distance;
410  if ( (minSteps <= testSteps) && (testSteps <= maxSteps)
411  && ( (steps == 0.0) || (testDistance <= distance) ) ) {
412  steps = testSteps;
413  stepWidth = testStepWidth;
414  distance = testDistance;
415  //qDebug( "start: %f end: %f step width: %f steps: %f distance: %f", start, end, stepWidth, steps, distance);
416  }
417  }
418 }
419 
420 
421 void CartesianGrid::calculateStepWidth(
422  qreal start_, qreal end_,
423  const QList<qreal>& granularities,
424  Qt::Orientation orientation,
425  qreal& stepWidth, qreal& subStepWidth,
426  bool adjustLower, bool adjustUpper ) const
427 {
428  Q_UNUSED( orientation );
429 
430  Q_ASSERT_X ( granularities.count(), "CartesianGrid::calculateStepWidth",
431  "Error: The list of GranularitySequence values is empty." );
432  QList<qreal> list( granularities );
433  std::sort(list.begin(), list.end());
434 
435  const qreal start = qMin( start_, end_);
436  const qreal end = qMax( start_, end_);
437  const qreal distance = end - start;
438  //qDebug( "raw data start: %f end: %f", start, end);
439 
440  qreal steps;
441  int power = 0;
442  while ( list.last() * fastPow10( power ) < distance ) {
443  ++power;
444  };
445  // We have the sequence *two* times in the calculation test list,
446  // so we will be sure to find the best match:
447  const int count = list.count();
448  QList<qreal> testList;
449 
450  for ( int dec = -1; dec == -1 || fastPow10( dec + 1 ) >= distance; --dec )
451  for ( int i = 0; i < count; ++i )
452  testList << list.at(i) * fastPow10( dec );
453 
454  testList << list;
455 
456  do {
457  calculateSteps( start, end, testList, m_minsteps, m_maxsteps, power,
458  steps, stepWidth,
459  adjustLower, adjustUpper );
460  --power;
461  } while ( steps == 0.0 );
462  ++power;
463  //qDebug( "steps calculated: stepWidth: %f steps: %f", stepWidth, steps);
464 
465  // find the matching sub-grid line width in case it is
466  // not set by the user
467 
468  if ( subStepWidth == 0.0 ) {
469  if ( stepWidth == list.first() * fastPow10( power ) ) {
470  subStepWidth = list.last() * fastPow10( power-1 );
471  //qDebug("A");
472  } else if ( stepWidth == list.first() * fastPow10( power-1 ) ) {
473  subStepWidth = list.last() * fastPow10( power-2 );
474  //qDebug("B");
475  } else {
476  qreal smallerStepWidth = list.first();
477  for ( int i = 1; i < list.count(); ++i ) {
478  if ( stepWidth == list.at( i ) * fastPow10( power ) ) {
479  subStepWidth = smallerStepWidth * fastPow10( power );
480  break;
481  }
482  if ( stepWidth == list.at( i ) * fastPow10( power-1 ) ) {
483  subStepWidth = smallerStepWidth * fastPow10( power-1 );
484  break;
485  }
486  smallerStepWidth = list.at( i );
487  }
488  }
489  }
490  //qDebug("CartesianGrid::calculateStepWidth() found stepWidth %f (%f steps) and sub-stepWidth %f", stepWidth, steps, subStepWidth);
491 }
void addRoundedRect(const QRectF &rect, qreal xRadius, qreal yRadius, Qt::SizeMode mode)
T & first()
DataDimensionsList updateData(AbstractCoordinatePlane *plane)
Returns the cached result of data calculation.
void setPen(const QColor &color)
const GridAttributes gridAttributes(Qt::Orientation orientation) const
static void adjustLowerUpperRange(qreal &start, qreal &end, qreal stepWidth, bool adjustLower, bool adjustUpper)
Adjusts start and/or end so that they are a multiple of stepWidth.
int count(const T &value) const const
AbstractCoordinatePlane * sharedAxisMasterPlane(QPainter *p=nullptr) override
reimpl
Q_SCRIPTABLE Q_NOREPLY void start()
unsigned int autoAdjustVerticalRangeToData() const
Returns the maximal allowed percent of the vertical space covered by the coordinate plane that may be...
Stores information about painting diagrams.
KIOFILEWIDGETS_EXPORT QStringList list(const QString &fileClass)
const QPointF translate(const QPointF &diagramPoint) const override
Translate the given point in value space coordinates to a position in pixel space.
Abstract base class for grid classes: cartesian, polar, ...
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
Qt::PenStyle style() const const
Horizontal
const T & at(int i) const const
QRect geometry() const override
pure virtual in QLayoutItem
QTextStream & dec(QTextStream &stream)
unsigned int autoAdjustHorizontalRangeToData() const
Returns the maximal allowed percent of the horizontal space covered by the coordinate plane that may ...
A set of attributes controlling the appearance of grids.
QPoint bottomLeft() const const
void drawGrid(PaintContext *context) override
Doing the actual drawing.
T & last()
static bool isBoundariesValid(const QRectF &r)
Checks whether both coordinates of r are valid according to isValueValid.
qreal x() const const
qreal y() const const
QPoint topRight() const const
void setClipPath(const QPainterPath &path, Qt::ClipOperation operation)
void drawLine(const QLineF &line)
QList::iterator begin()
qreal & rx()
qreal & ry()
bool autoAdjustGridToZoom() const
Return the status of the built-in grid adjusting feature.
QList::iterator end()
Helper class for one dimension of data, e.g.
const QList< QKeySequence > & end()
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Sat Aug 13 2022 04:04:52 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.