1 /*
2  * Copyright (C) 2001-2015 Klaralvdalens Datakonsult AB.  All rights reserved.
3  *
4  * This file is part of the KD Chart library.
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License as
8  * published by the Free Software Foundation; either version 2 of
9  * the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
18  */
19 
20 #include "KChartStockDiagram_p.h"
21 
22 #include "KChartPainterSaver_p.h"
23 
24 using namespace KChart;
25 
26 
27 class Q_DECL_HIDDEN StockDiagram::Private::ThreeDPainter
28 {
29 public:
30     struct ThreeDProperties {
31         qreal depth;
32         qreal angle;
33         bool useShadowColors;
34     };
35 
ThreeDPainter(QPainter * p)36     ThreeDPainter( QPainter *p )
37         : painter( p ) {};
38 
39     QPolygonF drawTwoDLine( const QLineF &line, const QPen &pen,
40                             const ThreeDProperties &props );
41     QPolygonF drawThreeDLine( const QLineF &line, const QBrush &brush,
42                               const QPen &pen, const ThreeDProperties &props );
43     QPolygonF drawThreeDRect( const QRectF &rect, const QBrush &brush,
44                               const QPen &pen, const ThreeDProperties &props );
45 
46 private:
47     QPointF projectPoint( const QPointF &point, qreal depth, qreal angle ) const;
48     QColor calcShadowColor( const QColor &color, qreal angle ) const;
49 
50     QPainter *painter;
51 };
52 
53 /*
54  * Projects a point in 3D space
55  *
56  * @param depth The distance from the point and the projected point
57  * @param angle The angle the projected point is rotated by around the original point
58  */
projectPoint(const QPointF & point,qreal depth,qreal angle) const59 QPointF StockDiagram::Private::ThreeDPainter::projectPoint( const QPointF &point, qreal depth, qreal angle ) const
60 {
61     const qreal angleInRad = DEGTORAD( angle );
62     const qreal distX = depth * cos( angleInRad );
63     // Y coordinates are reversed on our coordinate plane
64     const qreal distY = depth * -sin( angleInRad );
65 
66     return QPointF( point.x() + distX, point.y() + distY );
67 }
68 
69 /*
70  * Returns the shadow color for a given color, depending on the angle of rotation
71  *
72  * @param color The color to calculate the shadow color for
73  * @param angle The angle that the colored area is rotated by
74  */
calcShadowColor(const QColor & color,qreal angle) const75 QColor StockDiagram::Private::ThreeDPainter::calcShadowColor( const QColor &color, qreal angle ) const
76 {
77     // The shadow factor determines to how many percent the brightness
78     // of the color can be reduced. That is, the darkest shadow color
79     // is color * shadowFactor.
80     const qreal shadowFactor = 0.5;
81     const qreal sinAngle = 1.0 - qAbs( sin( DEGTORAD( angle ) ) ) * shadowFactor;
82     return QColor( qRound( color.red()   * sinAngle ),
83                    qRound( color.green() * sinAngle ),
84                    qRound( color.blue()  * sinAngle ) );
85 }
86 
87 /*
88  * Draws a 2D line in 3D space by painting it with a z-coordinate of props.depth / 2.0
89  *
90  * @param line The line to draw
91  * @param pen The pen to use to draw the line
92  * @param props The 3D properties to draw the line with
93  * @return The drawn line, but with a width of 2px, as a polygon
94  */
drawTwoDLine(const QLineF & line,const QPen & pen,const ThreeDProperties & props)95 QPolygonF StockDiagram::Private::ThreeDPainter::drawTwoDLine( const QLineF &line, const QPen &pen,
96                                                               const ThreeDProperties &props )
97 {
98     // Restores the painting properties when destroyed
99     PainterSaver painterSaver( painter );
100 
101     // The z coordinate to use (i.e., at what depth to draw the line)
102     const qreal z = props.depth / 2.0;
103 
104     // Projec the 2D points of the line in 3D
105     const QPointF deepP1 = projectPoint( line.p1(), z, props.angle );
106     const QPointF deepP2 = projectPoint( line.p2(), z, props.angle );
107 
108     // The drawn line with a width of 2px
109     QPolygonF threeDArea;
110     // The offset of the line "borders" from the center to each side
111     const QPointF offset( 0.0, 1.0 );
112     threeDArea << deepP1 - offset << deepP2 - offset
113                << deepP1 + offset << deepP2 + offset << deepP1 - offset;
114 
115     painter->setPen( pen );
116     painter->drawLine( QLineF( deepP1, deepP2 ) );
117 
118     return threeDArea;
119 }
120 
121 /*
122  * Draws an ordinary line in 3D by expanding it in the z-axis by the given depth.
123  *
124  * @param line The line to draw
125  * @param brush The brush to fill the resulting polygon with
126  * @param pen The pen to paint the borders of the resulting polygon with
127  * @param props The 3D properties to draw the line with
128  * @return The 3D shape drawn
129  */
drawThreeDLine(const QLineF & line,const QBrush & brush,const QPen & pen,const ThreeDProperties & props)130 QPolygonF StockDiagram::Private::ThreeDPainter::drawThreeDLine( const QLineF &line, const QBrush &brush,
131                                                                 const QPen &pen, const ThreeDProperties &props )
132 {
133     // Restores the painting properties when destroyed
134     PainterSaver painterSaver( painter );
135 
136     const QPointF p1 = line.p1();
137     const QPointF p2 = line.p2();
138 
139     // Project the 2D points of the line in 3D
140     const QPointF deepP1 = projectPoint( p1, props.depth, props.angle );
141     const QPointF deepP2 = projectPoint( p2, props.depth, props.angle );
142 
143     // The result is a 3D representation of the 2D line
144     QPolygonF threeDArea;
145     threeDArea << p1 << p2 << deepP2 << deepP1 << p1;
146 
147     // Use shadow colors if ThreeDProperties::useShadowColors is set
148     // Note: Setting a new color on a brush or pen does not effect gradients or textures
149     if ( props.useShadowColors ) {
150         QBrush shadowBrush( brush );
151         QPen shadowPen( pen );
152         shadowBrush.setColor( calcShadowColor( brush.color(), props.angle ) );
153         shadowPen.setColor( calcShadowColor( pen.color(), props.angle ) );
154         painter->setBrush( shadowBrush );
155         painter->setPen( shadowPen );
156     } else {
157         painter->setBrush( brush );
158         painter->setPen( pen );
159     }
160 
161     painter->drawPolygon( threeDArea );
162 
163     return threeDArea;
164 }
165 
166 /*
167  * Draws a 3D cuboid by extending a 2D rectangle in the z-axis
168  *
169  * @param rect The rectangle to draw
170  * @param brush The brush fill the surfaces of the cuboid with
171  * @param pen The pen to draw the edges with
172  * @param props The 3D properties to use for drawing the cuboid
173  * @return The drawn cuboid as a polygon
174  */
drawThreeDRect(const QRectF & rect,const QBrush & brush,const QPen & pen,const ThreeDProperties & props)175 QPolygonF StockDiagram::Private::ThreeDPainter::drawThreeDRect( const QRectF &rect, const QBrush &brush,
176                                                                 const QPen &pen, const ThreeDProperties &props )
177 {
178     // Restores the painting properties when destroyed
179     PainterSaver painterSaver( painter );
180 
181     // Make sure that the top really is the top
182     const QRectF normalizedRect = rect.normalized();
183 
184     // Calculate all the four sides of the rectangle
185     const QLineF topSide = QLineF( normalizedRect.topLeft(), normalizedRect.topRight() );
186     const QLineF bottomSide = QLineF( normalizedRect.bottomLeft(), normalizedRect.bottomRight() );
187     const QLineF leftSide = QLineF( normalizedRect.topLeft(), normalizedRect.bottomLeft() );
188     const QLineF rightSide = QLineF( normalizedRect.topRight(), normalizedRect.bottomRight() );
189 
190     QPolygonF drawnPolygon;
191 
192     // Shorter names are easier on the eyes
193     const qreal angle = props.angle;
194 
195     // Only top and right side is visible
196     if ( angle >= 0.0 && angle < 90.0 ) {
197         drawnPolygon = drawnPolygon.united( drawThreeDLine( topSide, brush, pen, props ) );
198         drawnPolygon = drawnPolygon.united( drawThreeDLine( rightSide, brush, pen, props ) );
199     // Only top and left side is visible
200     } else if ( angle >= 90.0 && angle < 180.0 ) {
201         drawnPolygon = drawnPolygon.united( drawThreeDLine( topSide, brush, pen, props ) );
202         drawnPolygon = drawnPolygon.united( drawThreeDLine( leftSide, brush, pen, props ) );
203     // Only bottom and left side is visible
204     } else if ( angle >= 180.0 && angle < 270.0 ) {
205         drawnPolygon = drawnPolygon.united( drawThreeDLine( bottomSide, brush, pen, props ) );
206         drawnPolygon = drawnPolygon.united( drawThreeDLine( leftSide, brush, pen, props ) );
207     // Only bottom and right side is visible
208     } else if ( angle >= 270.0 && angle <= 360.0 ) {
209         drawnPolygon = drawnPolygon.united( drawThreeDLine( bottomSide, brush, pen, props ) );
210         drawnPolygon = drawnPolygon.united( drawThreeDLine( rightSide, brush, pen, props ) );
211     }
212 
213     // Draw the front side
214     painter->setPen( pen );
215     painter->setBrush( brush );
216     painter->drawRect( normalizedRect );
217 
218     return drawnPolygon;
219 }
220 
221 
Private()222 StockDiagram::Private::Private()
223     : AbstractCartesianDiagram::Private()
224 {
225 }
226 
Private(const Private & r)227 StockDiagram::Private::Private( const Private& r )
228     : AbstractCartesianDiagram::Private( r )
229 {
230 }
231 
~Private()232 StockDiagram::Private::~Private()
233 {
234 }
235 
236 /*
237  * Projects a point onto the coordinate plane
238  *
239  * @param context The context to paint the point in
240  * @point The point to project onto the coordinate plane
241  * @return The projected point
242  */
projectPoint(PaintContext * context,const QPointF & point) const243 QPointF StockDiagram::Private::projectPoint( PaintContext *context, const QPointF &point ) const
244 {
245     return context->coordinatePlane()->translate( QPointF( point.x() + 0.5, point.y() ) );
246 }
247 
248 /*
249  * Projects a candlestick onto the coordinate plane
250  *
251  * @param context The context to paint the candlestick in
252  * @param low The
253  */
projectCandlestick(PaintContext * context,const QPointF & open,const QPointF & close,qreal width) const254 QRectF StockDiagram::Private::projectCandlestick( PaintContext *context, const QPointF &open, const QPointF &close, qreal width ) const
255 {
256     const QPointF leftHighPoint = context->coordinatePlane()->translate( QPointF( close.x() + 0.5 - width / 2.0, close.y() ) );
257     const QPointF rightLowPoint = context->coordinatePlane()->translate( QPointF( open.x() + 0.5 + width / 2.0, open.y() ) );
258     const QPointF rightHighPoint = context->coordinatePlane()->translate( QPointF( close.x() + 0.5 + width / 2.0, close.y() ) );
259 
260     return QRectF( leftHighPoint, QSizeF( rightHighPoint.x() - leftHighPoint.x(),
261                                           rightLowPoint.y() - leftHighPoint.y() ) );
262 }
263 
drawOHLCBar(int dataset,const CartesianDiagramDataCompressor::DataPoint & open,const CartesianDiagramDataCompressor::DataPoint & high,const CartesianDiagramDataCompressor::DataPoint & low,const CartesianDiagramDataCompressor::DataPoint & close,PaintContext * context)264 void StockDiagram::Private::drawOHLCBar( int dataset, const CartesianDiagramDataCompressor::DataPoint &open,
265         const CartesianDiagramDataCompressor::DataPoint &high,
266         const CartesianDiagramDataCompressor::DataPoint &low,
267         const CartesianDiagramDataCompressor::DataPoint &close,
268         PaintContext *context )
269 {
270     // Note: A row in the model is a column in a StockDiagram
271     const int col = low.index.row();
272 
273     StockBarAttributes attr = stockDiagram()->stockBarAttributes( col );
274     ThreeDBarAttributes threeDAttr = stockDiagram()->threeDBarAttributes( col );
275     const qreal tickLength = attr.tickLength();
276 
277     const QPointF leftOpenPoint( open.key + 0.5 - tickLength, open.value );
278     const QPointF rightOpenPoint( open.key + 0.5, open.value );
279     const QPointF highPoint( high.key + 0.5, high.value );
280     const QPointF lowPoint( low.key + 0.5, low.value );
281     const QPointF leftClosePoint( close.key + 0.5, close.value );
282     const QPointF rightClosePoint( close.key + 0.5 + tickLength, close.value );
283 
284     bool reversedOrder = false;
285     // If 3D mode is enabled, we have to make sure the z-order is right
286     if ( threeDAttr.isEnabled() ) {
287         const int angle = threeDAttr.angle();
288         // Z-order is from right to left
289         if ( ( angle >= 0 && angle < 90 ) || ( angle >= 180 && angle < 270 ) )
290             reversedOrder = true;
291         // Z-order is from left to right
292         if ( ( angle >= 90 && angle < 180 ) || ( angle >= 270 && angle <= 360 ) )
293             reversedOrder = false;
294     }
295 
296     if ( reversedOrder ) {
297         if ( !open.hidden )
298             drawLine( dataset, col, leftOpenPoint, rightOpenPoint, context ); // Open marker
299         if ( !low.hidden && !high.hidden )
300             drawLine( dataset, col, lowPoint, highPoint, context ); // Low-High line
301         if ( !close.hidden )
302             drawLine( dataset, col, leftClosePoint, rightClosePoint, context ); // Close marker
303     } else {
304         if ( !close.hidden )
305             drawLine( dataset, col, leftClosePoint, rightClosePoint, context ); // Close marker
306         if ( !low.hidden && !high.hidden )
307             drawLine( dataset, col, lowPoint, highPoint, context ); // Low-High line
308         if ( !open.hidden )
309             drawLine( dataset, col, leftOpenPoint, rightOpenPoint, context ); // Open marker
310     }
311 
312     LabelPaintCache lpc;
313     if ( !open.hidden ) {
314         addLabel( &lpc, diagram->attributesModel()->mapToSource( open.index ), nullptr,
315                             PositionPoints( leftOpenPoint ), Position::South, Position::South, open.value );
316     }
317     if ( !high.hidden ) {
318         addLabel( &lpc, diagram->attributesModel()->mapToSource( high.index ), nullptr,
319                             PositionPoints( highPoint ), Position::South, Position::South, high.value );
320     }
321     if ( !low.hidden ) {
322         addLabel( &lpc, diagram->attributesModel()->mapToSource( low.index ), nullptr,
323                             PositionPoints( lowPoint ), Position::South, Position::South, low.value );
324     }
325     if ( !close.hidden ) {
326         addLabel( &lpc, diagram->attributesModel()->mapToSource( close.index ), nullptr,
327                             PositionPoints( rightClosePoint ), Position::South, Position::South, close.value );
328     }
329     paintDataValueTextsAndMarkers( context, lpc, false );
330 }
331 
332 /*
333   * Draws a line connecting the low and the high value of an OHLC chart
334   *
335   * @param low The low data point
336   * @param high The high data point
337   * @param context The context to draw the candlestick in
338   */
drawCandlestick(int,const CartesianDiagramDataCompressor::DataPoint & open,const CartesianDiagramDataCompressor::DataPoint & high,const CartesianDiagramDataCompressor::DataPoint & low,const CartesianDiagramDataCompressor::DataPoint & close,PaintContext * context)339 void StockDiagram::Private::drawCandlestick( int /*dataset*/, const CartesianDiagramDataCompressor::DataPoint &open,
340                                              const CartesianDiagramDataCompressor::DataPoint &high,
341                                              const CartesianDiagramDataCompressor::DataPoint &low,
342                                              const CartesianDiagramDataCompressor::DataPoint &close,
343                                              PaintContext *context )
344 {
345     PainterSaver painterSaver( context->painter() );
346 
347     // Note: A row in the model is a column in a StockDiagram, and the other way around
348     const int row = low.index.row();
349     const int col = low.index.column();
350 
351     QPointF bottomCandlestickPoint;
352     QPointF topCandlestickPoint;
353     QBrush brush;
354     QPen pen;
355     bool drawLowerLine;
356     bool drawCandlestick = !open.hidden && !close.hidden;
357     bool drawUpperLine;
358 
359     // Find out if we need to paint a down-trend or up-trend candlestick
360     // and set brush and pen accordingly
361     // Also, determine what the top and bottom points of the candlestick are
362     if ( open.value <= close.value ) {
363         pen = stockDiagram()->upTrendCandlestickPen( row );
364         brush = stockDiagram()->upTrendCandlestickBrush( row );
365         bottomCandlestickPoint = QPointF( open.key, open.value );
366         topCandlestickPoint = QPointF( close.key, close.value );
367         drawLowerLine = !low.hidden && !open.hidden;
368         drawUpperLine = !low.hidden && !close.hidden;
369     } else {
370         pen = stockDiagram()->downTrendCandlestickPen( row );
371         brush = stockDiagram()->downTrendCandlestickBrush( row );
372         bottomCandlestickPoint = QPointF( close.key, close.value );
373         topCandlestickPoint = QPointF( open.key, open.value );
374         drawLowerLine = !low.hidden && !close.hidden;
375         drawUpperLine = !low.hidden && !open.hidden;
376     }
377 
378     StockBarAttributes attr = stockDiagram()->stockBarAttributes( col );
379     ThreeDBarAttributes threeDAttr = stockDiagram()->threeDBarAttributes( col );
380 
381     const QPointF lowPoint = projectPoint( context, QPointF( low.key, low.value ) );
382     const QPointF highPoint = projectPoint( context, QPointF( high.key, high.value ) );
383     const QLineF lowerLine = QLineF( lowPoint, projectPoint( context, bottomCandlestickPoint ) );
384     const QLineF upperLine = QLineF( projectPoint( context, topCandlestickPoint ), highPoint );
385 
386     // Convert the data point into coordinates on the coordinate plane
387     QRectF candlestick = projectCandlestick( context, bottomCandlestickPoint,
388                                              topCandlestickPoint, attr.candlestickWidth() );
389 
390     // Remember the drawn polygon to add it to the ReverseMapper later
391     QPolygonF drawnPolygon;
392 
393     // Use the ThreeDPainter class to draw a 3D candlestick
394     if ( threeDAttr.isEnabled() ) {
395         ThreeDPainter threeDPainter( context->painter() );
396 
397         ThreeDPainter::ThreeDProperties threeDProps;
398         threeDProps.depth = threeDAttr.depth();
399         threeDProps.angle = threeDAttr.angle();
400         threeDProps.useShadowColors = threeDAttr.useShadowColors();
401 
402         // If the perspective angle is within [0,180], we paint from bottom to top,
403         // otherwise from top to bottom to ensure the correct z order
404         if ( threeDProps.angle > 0.0 && threeDProps.angle < 180.0 ) {
405             if ( drawLowerLine )
406                 drawnPolygon = threeDPainter.drawTwoDLine( lowerLine, pen, threeDProps );
407             if ( drawCandlestick )
408                 drawnPolygon = threeDPainter.drawThreeDRect( candlestick, brush, pen, threeDProps );
409             if ( drawUpperLine )
410             drawnPolygon = threeDPainter.drawTwoDLine( upperLine, pen, threeDProps );
411         } else {
412             if ( drawUpperLine )
413                 drawnPolygon = threeDPainter.drawTwoDLine( upperLine, pen, threeDProps );
414             if ( drawCandlestick )
415                 drawnPolygon = threeDPainter.drawThreeDRect( candlestick, brush, pen, threeDProps );
416             if ( drawLowerLine )
417                 drawnPolygon = threeDPainter.drawTwoDLine( lowerLine, pen, threeDProps );
418         }
419     } else {
420         QPainter *const painter = context->painter();
421         painter->setBrush( brush );
422         painter->setPen( pen );
423         if ( drawLowerLine )
424             painter->drawLine( lowerLine );
425         if ( drawUpperLine )
426             painter->drawLine( upperLine );
427         if ( drawCandlestick )
428             painter->drawRect( candlestick );
429 
430         // The 2D representation is the projected candlestick itself
431         drawnPolygon = candlestick;
432 
433         // FIXME: Add lower and upper line to reverse mapper
434     }
435 
436     LabelPaintCache lpc;
437     if ( !low.hidden )
438         addLabel( &lpc, diagram->attributesModel()->mapToSource( low.index ), nullptr,
439                   PositionPoints( lowPoint ), Position::South, Position::South, low.value );
440     if ( drawCandlestick ) {
441         // Both, the open as well as the close value are represented by this candlestick
442         reverseMapper.addPolygon( row, openValueColumn(), drawnPolygon );
443         reverseMapper.addPolygon( row, closeValueColumn(), drawnPolygon );
444 
445         addLabel( &lpc, diagram->attributesModel()->mapToSource( open.index ), nullptr,
446                   PositionPoints( candlestick.bottomRight() ), Position::South, Position::South, open.value );
447         addLabel( &lpc, diagram->attributesModel()->mapToSource( close.index ), nullptr,
448                   PositionPoints( candlestick.topRight() ), Position::South, Position::South, close.value );
449     }
450     if ( !high.hidden )
451         addLabel( &lpc, diagram->attributesModel()->mapToSource( high.index ), nullptr,
452                   PositionPoints( highPoint ), Position::South, Position::South, high.value );
453 
454     paintDataValueTextsAndMarkers( context, lpc, false );
455 }
456 
457 /*
458   * Draws a line connecting two points
459   *
460   * @param col The column of the diagram to paint the line in
461   * @param point1 The first point
462   * @param point2 The second point
463   * @param context The context to draw the low-high line in
464   */
drawLine(int dataset,int col,const QPointF & point1,const QPointF & point2,PaintContext * context)465 void StockDiagram::Private::drawLine( int dataset, int col, const QPointF &point1, const QPointF &point2, PaintContext *context )
466 {
467     PainterSaver painterSaver( context->painter() );
468 
469     // A row in the model is a column in the diagram
470     const int modelRow = col;
471     const int modelCol = 0;
472 
473     const QPen pen = diagram->pen( dataset );
474     const QBrush brush = diagram->brush( dataset );
475     const ThreeDBarAttributes threeDBarAttr = stockDiagram()->threeDBarAttributes( col );
476 
477     QPointF transP1 = context->coordinatePlane()->translate( point1 );
478     QPointF transP2 = context->coordinatePlane()->translate( point2 );
479     QLineF line = QLineF( transP1, transP2 );
480 
481     if ( threeDBarAttr.isEnabled() ) {
482         ThreeDPainter::ThreeDProperties threeDProps;
483         threeDProps.angle = threeDBarAttr.angle();
484         threeDProps.depth = threeDBarAttr.depth();
485         threeDProps.useShadowColors = threeDBarAttr.useShadowColors();
486 
487         ThreeDPainter painter( context->painter() );
488         reverseMapper.addPolygon( modelCol, modelRow, painter.drawThreeDLine( line, brush, pen, threeDProps ) );
489     } else {
490         context->painter()->setPen( pen );
491         //context->painter()->setBrush( brush );
492         reverseMapper.addLine( modelCol, modelRow, transP1, transP2 );
493         context->painter()->drawLine( line );
494     }
495 }
496 
497 /*
498  * Returns the column of the open value in the model
499  *
500  * @return The column of the open value
501  */
openValueColumn() const502 int StockDiagram::Private::openValueColumn() const
503 {
504     // Return an invalid column if diagram has no open values
505     return type == HighLowClose ? -1 : 0;
506 }
507 
508 /*
509  * Returns the column of the high value in the model
510  *
511  * @return The column of the high value
512  */
highValueColumn() const513 int StockDiagram::Private::highValueColumn() const
514 {
515     return type == HighLowClose ? 0 : 1;
516 }
517 
518 /*
519  * Returns the column of the low value in the model
520  *
521  * @return The column of the low value
522  */
lowValueColumn() const523 int StockDiagram::Private::lowValueColumn() const
524 {
525     return type == HighLowClose ? 1 : 2;
526 }
527 
528 /*
529  * Returns the column of the close value in the model
530  *
531  * @return The column of the close value
532  */
closeValueColumn() const533 int StockDiagram::Private::closeValueColumn() const
534 {
535     return type == HighLowClose ? 2 : 3;
536 }
537 
538