1 /*
2 KmPlot - a math. function plotter for the KDE-Desktop
3
4 SPDX-FileCopyrightText: 1998, 1999, 2000, 2002 Klaus-Dieter Möller <kd.moeller@t-online.de>
5 SPDX-FileCopyrightText: 2006 David Saxton <david@bluehaze.org>
6
7 This file is part of the KDE Project.
8 KmPlot is part of the KDE-EDU Project.
9
10 SPDX-License-Identifier: GPL-2.0-or-later
11
12 */
13
14 #include "view.h"
15
16 #include <kmplot/config-kmplot.h>
17
18 // Qt includes
19 #include <QAbstractTextDocumentLayout>
20 #include <QBitmap>
21 #include <QCursor>
22 #include <QDataStream>
23 #include <QList>
24 #include <QMenu>
25 #include <QPaintEvent>
26 #include <QPainter>
27 #include <QSlider>
28 #include <QTextEdit>
29 #include <QElapsedTimer>
30 #include <QTimer>
31
32 // KDE includes
33 #include <KActionCollection>
34 #include <KMessageBox>
35
36 // local includes
37 #include "functioneditor.h"
38 #include "functiontools.h"
39 #include "settings.h"
40 #include "ksliderwindow.h"
41 #include "maindlg.h"
42 #include "parameteranimator.h"
43 #include "viewadaptor.h"
44 #include "xparser.h"
45
46
47 // other includes
48 #include <assert.h>
49 #include <cmath>
50 #ifdef HAVE_IEEEFP_H
51 #include <ieeefp.h>
52 #endif
53
54 #if defined(Q_CC_MINGW)
55 using namespace std;
56 #endif
57
58 // does for real numbers what "%" does for integers
realModulo(double x,double mod)59 double realModulo( double x, double mod )
60 {
61 return x - floor(x/mod)*mod;
62 }
63
64
65 //BEGIN class View
66 View * View::m_self = 0;
67
View(bool readOnly,QMenu * functionPopup,QWidget * parent)68 View::View( bool readOnly, QMenu * functionPopup, QWidget* parent )
69 : QWidget( parent ),
70 buffer( width(), height() ),
71 m_popupMenu( functionPopup ),
72 m_readonly( readOnly ),
73 m_AccumulatedDelta(0),
74 m_viewportAnimation( new QPropertyAnimation( this, "viewport" ) )
75 {
76 assert( !m_self ); // this class should only be constructed once
77 m_self = this;
78 setAttribute( Qt::WA_StaticContents );
79
80 m_haveRoot = false;
81 emit updateRootValue( false, 0 );
82 m_xmin = m_xmax = m_ymin = m_ymax = 0.0;
83 m_printHeaderTable = false;
84 m_printBackground = false;
85 m_printWidth = 0.0;
86 m_printHeight = 0.0;
87 m_stopCalculating = false;
88 m_isDrawing = false;
89 m_popupMenuStatus = NoPopup;
90 m_zoomMode = Normal;
91 m_prevCursor = CursorArrow;
92 m_backgroundColor = Settings::backgroundcolor();
93
94 m_textEdit = new KTextEdit;
95 m_textEdit->setWordWrapMode( QTextOption::NoWrap );
96 m_textEdit->setLineWrapMode( QTextEdit::NoWrap );
97 m_textDocument = m_textEdit->document();
98
99 m_mousePressTimer = new QElapsedTimer();
100
101 new ViewAdaptor(this);
102 QDBusConnection::sessionBus().registerObject("/view", this);
103
104 setMouseTracking( true );
105 m_sliderWindow = 0;
106
107 m_popupMenuTitle = m_popupMenu->insertSection( MainDlg::self()->m_firstFunctionAction, "" );
108 connect(XParser::self(), &XParser::functionRemoved, this, &View::functionRemoved);
109 }
110
111
~View()112 View::~View()
113 {
114 m_textEdit->deleteLater();
115 delete XParser::self();
116 delete m_mousePressTimer;
117 }
118
119
initDrawLabels()120 void View::initDrawLabels()
121 {
122 m_labelFont = Settings::labelFont();
123
124 for ( int i = 0; i < LabelGridSize; ++i )
125 for ( int j = 0; j < LabelGridSize; ++j )
126 m_usedDiagramArea[i][j] = false;
127
128 // Add the axis
129 double x = xToPixel( 0 );
130 double y = yToPixel( 0 );
131
132 double x0 = xToPixel( m_xmin );
133 double x1 = xToPixel( m_xmax );
134 double y0 = yToPixel( m_ymin );
135 double y1 = yToPixel( m_ymax );
136
137 // x-axis
138 markDiagramAreaUsed( QRectF( x-20, y0, 40, y1-y0 ) );
139
140 // y-axis
141 markDiagramAreaUsed( QRectF( x0, y-20, x1-x0, 40 ) );
142 }
143
144
niceTicSpacing(double length_mm,double range)145 double View::niceTicSpacing( double length_mm, double range )
146 {
147 Q_ASSERT_X( range > 0, "View::niceTicSpacing", "Range must be positive" );
148
149 if ( length_mm <= 0 )
150 {
151 // Don't assert, as we can at least handle this situation - and it can
152 // happen with extreme zooms
153
154 qWarning() << "Non-positive length: length_mm="<<length_mm;
155 length_mm = 120;
156 }
157
158 // Custom case for trigonometric scaled
159 if ( qFuzzyCompare( range, 4*M_PI ) )
160 return M_PI/2;
161
162 // Aim to space the tics by around 16 mm
163 double target = range * 16.0 / length_mm;
164
165 // The scaling required to bring target to a number between 1 and 10
166 double scale = pow( 10, -std::floor(log(target)/log(10.0)) );
167
168 // Calculate the first digit of target, e.g. if target is 0.0352, then leading will be set to 3
169 int leading = int(target * scale);
170
171 if ( leading == 1 )
172 return 1/scale;
173 else if ( leading >= 2 && leading <= 4 )
174 return 2/scale;
175 else
176 return 5/scale;
177 }
178
179
validatedTicSpacing(double spacing,double range,double pixels,double minPixels)180 double View::validatedTicSpacing( double spacing, double range, double pixels, double minPixels )
181 {
182 Q_ASSERT_X( range > 0, "View::validatedTicSpacing", "Range must be positive" );
183 Q_ASSERT_X( minPixels > 0, "View::validatedTicSpacing", "MinPixels must be positive" );
184
185 spacing = qAbs( spacing );
186 if ( qFuzzyCompare( spacing, 0 ) )
187 return 2.0 * range;
188
189 double factor;
190
191 // Make sure spacing between tics is at least minPixels
192 pixels /= range / spacing;
193 factor = pixels / minPixels;
194 if ( factor < 1.0 ) {
195 int exponent;
196 frexp( factor, &exponent );
197 spacing = ldexp( spacing, -exponent + 1 );
198 }
199
200 // Make sure there are at least two tics
201 factor = spacing / range;
202 if ( factor > 0.5 ) {
203 int exponent;
204 frexp( factor, &exponent );
205 spacing = ldexp( spacing, -exponent - 1);
206 }
207
208 return spacing;
209 }
210
211
initDrawing(QPaintDevice * device,PlotMedium medium)212 void View::initDrawing( QPaintDevice * device, PlotMedium medium )
213 {
214 switch ( medium )
215 {
216 case SVG:
217 case Screen:
218 {
219 m_clipRect = QRect( 0, 0, width(), height() );
220 break;
221 }
222
223 case Printer:
224 {
225 double inchesPerMeter = 100.0/2.54;
226
227 int pixels_x = int(m_printWidth * device->logicalDpiX() * inchesPerMeter);
228 int pixels_y = int(m_printHeight * device->logicalDpiY() * inchesPerMeter);
229
230 m_clipRect = QRect( 0, 0, pixels_x, pixels_y );
231 break;
232 }
233
234 case Pixmap:
235 {
236 QPixmap * pic = static_cast<QPixmap*>(device);
237 m_clipRect = pic->rect();
238 break;
239 }
240 }
241
242
243 if ( m_clipRect.width() <= 0 || m_clipRect.height() <= 0 )
244 {
245 qWarning() << "Invalid clip rect: m_clipRect="<<m_clipRect;
246 return;
247 }
248
249
250 //BEGIN get X/Y range
251 m_xmin = XParser::self()->eval( Settings::xMin() );
252 m_xmax = XParser::self()->eval( Settings::xMax() );
253
254 if ( m_xmax <= m_xmin || !std::isfinite(m_xmin) || !std::isfinite(m_xmax) )
255 {
256 m_xmin = -8;
257 m_xmax = +8;
258 }
259
260 m_ymin = XParser::self()->eval( Settings::yMin() );
261 m_ymax = XParser::self()->eval( Settings::yMax() );
262 if ( m_ymax <= m_ymin || !std::isfinite(m_ymin) || !std::isfinite(m_ymax) )
263 {
264 m_ymin = -8;
265 m_ymax = +8;
266 }
267 //END get X/Y range
268
269
270 //BEGIN calculate scaling matrices
271 m_realToPixel.reset();
272 m_realToPixel.scale( m_clipRect.width()/(m_xmax-m_xmin), m_clipRect.height()/(m_ymin-m_ymax) );
273 m_realToPixel.translate( -m_xmin, -m_ymax );
274
275 m_pixelToReal = m_realToPixel.inverted();
276 //END calculate scaling matrices
277
278
279
280 //BEGIN get Tic Separation
281 QFontMetricsF fm( Settings::axesFont(), device );
282 if ( Settings::xScalingMode() == 0 )
283 {
284 double length = pixelsToMillimeters( xToPixel( m_xmax ), device );
285 double spacing = niceTicSpacing( length, m_xmax-m_xmin );
286 ticSepX.updateExpression( spacing );
287 }
288 else
289 {
290 ticSepX.updateExpression( Settings::xScaling() );
291 double spacing = ticSepX.value();
292 spacing = validatedTicSpacing( spacing, m_xmax-m_xmin, xToPixel( m_xmax ), fm.lineSpacing());
293 ticSepX.updateExpression( spacing );
294 }
295
296 if ( Settings::yScalingMode() == 0 )
297 {
298 double length = pixelsToMillimeters( yToPixel( m_ymin ), device );
299 double spacing = niceTicSpacing( length, m_ymax-m_ymin );
300 ticSepY.updateExpression( spacing );
301 }
302 else
303 {
304 ticSepY.updateExpression( Settings::yScaling() );
305 double spacing = ticSepY.value();
306 spacing = validatedTicSpacing( spacing, m_ymax-m_ymin, yToPixel( m_ymin ), fm.lineSpacing());
307 ticSepY.updateExpression( spacing );
308 }
309
310 ticStartX = ceil(m_xmin/ticSepX.value())*ticSepX.value();
311 ticStartY = ceil(m_ymin/ticSepY.value())*ticSepY.value();
312 //END get Tic Separation
313
314
315
316 //BEGIN get colours
317 m_backgroundColor = Settings::backgroundcolor();
318 if ( !m_backgroundColor.isValid() )
319 m_backgroundColor = Qt::white;
320 //END get colours
321
322
323 XParser::self()->setAngleMode( (Parser::AngleMode)Settings::anglemode() );
324
325 initDrawLabels();
326 }
327
328
draw(QPaintDevice * dev,PlotMedium medium)329 void View::draw( QPaintDevice * dev, PlotMedium medium )
330 {
331 if ( m_isDrawing )
332 return;
333
334 m_isDrawing = true;
335 updateCursor();
336 initDrawing( dev, medium );
337
338 QPainter painter( dev );
339
340 switch ( medium )
341 {
342 case SVG:
343 case Screen:
344 break;
345
346 case Printer:
347 {
348 if ( m_printHeaderTable )
349 drawHeaderTable( &painter );
350
351 painter.translate( (dev->width() - m_clipRect.width()) / 2, 0 );
352
353 if ( m_printBackground )
354 painter.fillRect( m_clipRect, m_backgroundColor); //draw a colored background
355
356 break;
357 }
358
359 case Pixmap:
360 {
361 QPixmap * pic = static_cast<QPixmap*>(dev);
362 pic->fill(m_backgroundColor);
363 break;
364 }
365 }
366
367 painter.setClipRect( m_clipRect );
368
369
370 //BEGIN draw diagram background stuff
371 painter.setRenderHint( QPainter::Antialiasing, true );
372
373 drawGrid( &painter );
374 if ( Settings::showAxes() )
375 drawAxes( &painter );
376 if( Settings::showLabel() )
377 drawLabels( &painter );
378 //END draw diagram background stuff
379
380
381 //BEGIN draw the functions
382 m_stopCalculating = false;
383
384 // Antialiasing slows down rendering a lot, so turn it off if we are
385 // sliding the view about
386 painter.setRenderHint( QPainter::Antialiasing, m_zoomMode != Translating );
387
388 double at = -1;
389 for ( Function * function : qAsConst(XParser::self()->m_ufkt) )
390 {
391 at += 1;
392
393 if ( m_stopCalculating )
394 break;
395
396 // QDBusInterface( QDBus::sessionBus().baseService(), "/kmplot", "org.kde.kmplot.KmPlot" ).call( QDBus::Block, "setDrawProgress", at/numPlots );
397
398 if ( function->type() == Function::Implicit )
399 drawImplicit( function, & painter );
400 else
401 drawFunction( function, & painter );
402 }
403 // QDBusInterface( QDBus::sessionBus().baseService(), "/kmplot", "org.kde.kmplot.KmPlot" ).call( QDBus::Block, "setDrawProgress", 1.0 );
404
405 drawFunctionInfo( & painter );
406
407 m_isDrawing=false;
408 //END draw the functions
409
410 // Reset are stuff back to the screen stuff
411 initDrawing( & buffer, Screen );
412
413 updateCursor();
414 }
415
416
417 //BEGIN coordinate mapping functions
toPixel(const QPointF & real,ClipBehaviour clipBehaviour,const QPointF & pixelIfNaN)418 QPointF View::toPixel( const QPointF & real, ClipBehaviour clipBehaviour, const QPointF & pixelIfNaN )
419 {
420 xclipflg = false;
421 yclipflg = false;
422
423 QPointF pixel = m_realToPixel.map( real );
424 double x = pixel.x();
425 double y = pixel.y();
426
427 if ( std::isnan(x) )
428 {
429 xclipflg = true;
430 x = pixelIfNaN.x();
431 }
432 else if ( clipBehaviour == ClipAll )
433 {
434 if ( x<0 )
435 {
436 xclipflg = true;
437 x = 0;
438 }
439 else if ( x > m_clipRect.right() )
440 {
441 xclipflg = true;
442 x = m_clipRect.right();
443 }
444 }
445 else
446 {
447 if ( std::isinf(x) && x<0 )
448 x = 0;
449
450 else if ( std::isinf(x) && x>0 )
451 x = m_clipRect.right();
452 }
453
454 if ( std::isnan(y) )
455 {
456 yclipflg = true;
457 y = pixelIfNaN.y();
458 }
459 else if ( clipBehaviour == ClipAll )
460 {
461 if ( y<0 )
462 {
463 yclipflg = true;
464 y = 0;
465 }
466 else if ( y > m_clipRect.bottom() )
467 {
468 yclipflg = true;
469 y = m_clipRect.bottom();
470 }
471 }
472 else
473 {
474 if ( std::isinf(y) && y<0 )
475 y = 0;
476
477 else if ( std::isinf(y) && y>0 )
478 y = m_clipRect.bottom();
479 }
480
481 // Make sure that x and y are *reasonably* bounded at least, even if they're not infinite
482 double min_x = -1e3 * m_clipRect.width();
483 double max_x = +1e3 * m_clipRect.width();
484 double min_y = -1e3 * m_clipRect.height();
485 double max_y = +1e3 * m_clipRect.height();
486
487 if ( x < min_x )
488 x = min_x;
489 else if ( x > max_x )
490 x = max_x;
491
492 if ( y < min_y )
493 y = min_y;
494 else if ( y > max_y )
495 y = max_y;
496
497 return QPointF( x, y );
498 }
499
xToPixel(double x,ClipBehaviour clipBehaviour,double xIfNaN)500 double View::xToPixel( double x, ClipBehaviour clipBehaviour, double xIfNaN )
501 {
502 return toPixel( QPointF( x, 0 ), clipBehaviour, QPointF( xIfNaN, 0 ) ).x();
503 }
504
yToPixel(double y,ClipBehaviour clipBehaviour,double yIfNaN)505 double View::yToPixel( double y, ClipBehaviour clipBehaviour, double yIfNaN )
506 {
507 return toPixel( QPointF( 0, y ), clipBehaviour, QPointF( 0, yIfNaN ) ).y();
508 }
509
510
toReal(const QPointF & pixel)511 QPointF View::toReal( const QPointF & pixel )
512 {
513 return m_pixelToReal.map( pixel );
514 }
515
xToReal(double x)516 double View::xToReal( double x )
517 {
518 return toReal( QPointF( x, 0 ) ).x();
519 }
520
yToReal(double y)521 double View::yToReal( double y )
522 {
523 return toReal( QPointF( 0, y ) ).y();
524 }
525 //END coordinate mapping functions
526
527
drawAxes(QPainter * painter)528 void View::drawAxes( QPainter* painter )
529 {
530 double axesLineWidth = millimetersToPixels( Settings::axesLineWidth(), painter->device() );
531 double ticWidth = millimetersToPixels( Settings::ticWidth(), painter->device() );
532 double ticLength = millimetersToPixels( Settings::ticLength(), painter->device() );
533 QColor axesColor = Settings::axesColor();
534
535 painter->save();
536
537 double arrowWidth = ticLength*1.4;
538 double arrowLength = arrowWidth*2.8;
539
540 painter->setPen( QPen( axesColor, axesLineWidth ) );
541 painter->setBrush( axesColor );
542
543 //BEGIN draw x axis
544 double a = m_clipRect.right()-ticLength;
545 double b = yToPixel(0.);
546
547 double b_max = m_clipRect.bottom() - ticLength;
548 if ( b < ticLength )
549 b = ticLength;
550 else if ( b > b_max )
551 b = b_max;
552
553 // horizontal line
554 painter->Lineh(ticLength, b, a);
555
556 // arrow head
557 if ( Settings::showArrows())
558 {
559 a = m_clipRect.right();
560
561 QPolygonF p(3);
562 p[0] = QPointF( a, b );
563 p[1] = QPointF( a-arrowLength, b+arrowWidth );
564 p[2] = QPointF( a-arrowLength, b-arrowWidth );
565 painter->drawPolygon( p );
566 }
567 //END draw x axis
568
569
570 //BEGIN draw y axis
571 a = xToPixel(0.);
572 b = ticLength;
573
574 double a_max = m_clipRect.right() - ticLength;
575 if ( a < ticLength )
576 a = ticLength;
577 else if ( a > a_max )
578 a = a_max;
579
580 // vertical line
581 painter->Linev(a, m_clipRect.bottom()-ticLength, b);
582
583 // arrow head
584 if ( Settings::showArrows() )
585 {
586 b = 0;
587
588 QPolygonF p(3);
589 p[0] = QPointF( a, b );
590 p[1] = QPointF( a-arrowWidth, b+arrowLength );
591 p[2] = QPointF( a+arrowWidth, b+arrowLength );
592 painter->drawPolygon( p );
593 }
594 //END draw y axis
595
596 painter->restore();
597
598 painter->setPen( QPen( axesColor, ticWidth ) );
599
600 double da = yToPixel(0)-ticLength;
601 double db = yToPixel(0)+ticLength;
602 double d = ticStartX;
603 if (da<0)
604 {
605 a = 0;
606 b = 2*ticLength;
607 }
608 else if(db>(double)m_clipRect.bottom())
609 {
610 b = m_clipRect.bottom();
611 a = m_clipRect.bottom()-2*ticLength;
612 }
613 else
614 {
615 a = da;
616 b = db;
617 }
618
619 while(d<m_xmax-ticSepX.value()/2.)
620 {
621 double d_pixel = xToPixel(d);
622 if ( d_pixel > ticLength )
623 painter->Linev(xToPixel(d), a, b);
624 d+=ticSepX.value();
625 }
626
627 da = xToPixel(0)-ticLength;
628 db = xToPixel(0)+ticLength;
629 d = ticStartY;
630 if (da<0)
631 {
632 a = 0;
633 b = 2*ticLength;
634 }
635 else if(db>(double)m_clipRect.right())
636 {
637 b = m_clipRect.right();
638 a = m_clipRect.right()-2*ticLength;
639 }
640 else
641 {
642 a = da;
643 b = db;
644 }
645
646 while(d<m_ymax-ticSepY.value()/2.)
647 {
648 double d_pixel = yToPixel(d);
649 if ( d_pixel < m_clipRect.bottom()-ticLength )
650 painter->Lineh(a, d_pixel, b);
651 d+=ticSepY.value();
652 }
653 }
654
655
drawGrid(QPainter * painter)656 void View::drawGrid( QPainter* painter )
657 {
658 QColor gridColor = Settings::gridColor();
659
660 double gridLineWidth = millimetersToPixels( Settings::gridLineWidth(), painter->device() );
661 QPen pen( gridColor, gridLineWidth );
662
663 painter->setPen(pen);
664
665 enum GridStyle
666 {
667 GridNone,
668 GridLines,
669 GridCrosses,
670 GridPolar
671 };
672 GridStyle gridMode = (GridStyle)Settings::gridStyle();
673
674 switch ( gridMode )
675 {
676 case GridNone:
677 break;
678
679 case GridLines:
680 {
681 for ( double d = ticStartX; d <= m_xmax; d += ticSepX.value() )
682 painter->Linev(xToPixel(d), m_clipRect.bottom(), 0);
683
684 for ( double d = ticStartY; d <= m_ymax; d += ticSepY.value() )
685 painter->Lineh(0, yToPixel(d), m_clipRect.right());
686
687 break;
688 }
689
690 case GridCrosses:
691 {
692 int const dx = 5;
693 int const dy = 5;
694
695 for( double x = ticStartX; x<m_xmax; x+=ticSepX.value() )
696 {
697 double a = xToPixel(x);
698 for( double y=ticStartY; y<m_ymax; y+=ticSepY.value())
699 {
700 double b = yToPixel(y);
701 painter->Lineh(a-dx, b, a+dx);
702 painter->Linev(a, b-dy, b+dy);
703 }
704 }
705
706 break;
707 }
708
709 case GridPolar:
710 {
711 // Note: 1.42 \approx sqrt(2)
712
713 double xMax = qMax( qAbs(m_xmin), qAbs(m_xmax) ) * 1.42;
714 double yMax = qMax( qAbs(m_ymin), qAbs(m_ymax) ) * 1.42;
715 double rMax = qMax( xMax, yMax );
716
717 // The furthest pixel away from the origin
718 double pixelMax = qMax( xMax*m_realToPixel.m11(), yMax*m_realToPixel.m22() );
719
720 double ticSep = qMin( ticSepX.value(), ticSepY.value() );
721 double r = ticSep;
722
723 while ( r < rMax )
724 {
725 QRectF rect;
726 rect.setTopLeft( toPixel( QPointF( -r, r ), ClipInfinite ) );
727 rect.setBottomRight( toPixel( QPointF( r, -r ), ClipInfinite ) );
728 painter->drawEllipse( rect );
729 r += ticSep;
730 }
731
732 for ( double theta = 0; theta < 2.0*M_PI; theta += M_PI/12.0 )
733 {
734 QPointF start = toPixel( QPointF( 0, 0 ), ClipInfinite );
735 QPointF end = start + QPointF( pixelMax * cos(theta), pixelMax * sin(theta) );
736
737 painter->drawLine( start, end );
738 }
739
740 break;
741 }
742 }
743 }
744
745
drawLabels(QPainter * painter)746 void View::drawLabels( QPainter *painter )
747 {
748 const QString xLabel = Settings::labelHorizontalAxis();
749 const QString yLabel = Settings::labelVerticalAxis();
750
751 int const dx=10;
752 int const dy=15;
753 QFont const font = Settings::axesFont();
754 painter->setFont(font);
755 m_textDocument->setDefaultFont( font );
756
757 double const x=xToPixel(0.);
758 double const y=yToPixel(0.);
759
760
761 QRectF drawRect;
762
763 // Whether the x-axis is along the top of the view
764 // and the y-axis is along the right edge of the view
765 bool axesInTopRight = ( m_ymax<ticSepY.value() && m_xmax<ticSepX.value() );
766
767 // for "x" label
768 double endLabelWidth = 0;
769
770 int flags = Qt::AlignVCenter|Qt::TextDontClip|Qt::AlignRight;
771
772 // Draw x label
773 if ( axesInTopRight )
774 drawRect = QRectF( xToPixel(m_xmax)-(3*dx), y+dy, 0, 0 );
775 else if (m_ymin>-ticSepY.value())
776 drawRect = QRectF( xToPixel(m_xmax)-dx, y-dy, 0, 0 );
777 else
778 drawRect = QRectF( xToPixel(m_xmax)-dx, y+dy, 0, 0 );
779 painter->drawText( drawRect, flags, xLabel );
780 endLabelWidth = m_clipRect.right() - painter->boundingRect( drawRect, flags, xLabel ).right();
781
782
783 // Draw y label
784 if ( axesInTopRight )
785 drawRect = QRectF( x-dx, yToPixel(m_ymax)+(2*dy), 0, 0 );
786 else if (m_xmin>-ticSepX.value())
787 drawRect = QRectF( x+(2*dx), yToPixel(m_ymax)+dy, 0, 0 );
788 else
789 drawRect = QRectF( x-dx, yToPixel(m_ymax)+dy, 0, 0 );
790 painter->drawText( drawRect, flags, yLabel );
791
792
793
794 // Draw the numbers on the axes
795 drawXAxisLabels( painter, pixelsToMillimeters( endLabelWidth, painter->device() ) );
796 drawYAxisLabels( painter );
797 }
798
799
800 /**
801 * If \p d is a rational multiple of pi, then return a string appropriate for
802 * displaying it in fraction form.
803 */
tryPiFraction(double d,double sep)804 QString tryPiFraction( double d, double sep )
805 {
806 // Avoid strange bug where get pi at large separation
807 if ( sep > 10 )
808 return QString();
809
810 bool positive = d > 0;
811
812 d /= M_PI;
813 if ( !positive )
814 d = -d;
815
816 if ( d < 1e-2 )
817 return QString();
818
819 // Try denominators from 1 to 6
820 for ( int denom = 1; denom <= 6; ++denom )
821 {
822 if ( realModulo( d * denom, 1 ) > 1e-3*sep )
823 continue;
824
825 int num = qRound( d * denom );
826
827 QString s = positive ? "+" : QString(MinusSymbol);
828 if ( num != 1 )
829 s += QString::number( num );
830
831 s += PiSymbol;
832
833 if ( denom != 1 )
834 s += '/' + QString::number( denom );
835
836 return s;
837 }
838
839 return QString();
840 }
841
842
drawXAxisLabels(QPainter * painter,double endLabelWidth_mm)843 void View::drawXAxisLabels( QPainter *painter, double endLabelWidth_mm )
844 {
845 QColor axesColor = Settings::axesColor();
846 int const dy = 8;
847
848 double const y = yToPixel(0.);
849
850 // Used to ensure that labels aren't drawn too closely together
851 // These numbers contain the pixel position of the left and right endpoints of the last label
852 double last_x_start = -1e3; // (just a large negative number)
853 double last_x_end = -1e3; // (just a large negative number)
854
855
856 // The strange label drawing order here is so that the labels eitherside
857 // of zero are always drawn, and then the other labels are drawn if there
858 // is enough space
859
860 bool first = true;
861 bool forwards = true;
862 double d = 0;
863
864 while ( true )
865 {
866 if ( first )
867 {
868 // Draw x>0 first
869 d = qMax( ticSepX.value(), ticStartX );
870 last_x_end = xToPixel(0);
871 first = false;
872 }
873 else
874 {
875 if ( forwards )
876 {
877 d += ticSepX.value();
878 if ( d > m_xmax )
879 {
880 // Continue on other side
881 d = qMin( -ticSepX.value(), ticStartX + floor((m_xmax-m_xmin)/ticSepX.value())*ticSepX.value() );
882 last_x_start = xToPixel(0);
883 forwards = false;
884 }
885 }
886 else
887 {
888 d -= ticSepX.value();
889 if ( d < m_xmin )
890 return;
891 }
892 }
893
894 // Don't draw too close to the left edge if the y axis is there
895 if ( m_xmin >= -ticSepX.value() && (d-m_xmin) <= ticSepX.value() )
896 continue;
897
898 QString s = tryPiFraction( d, ticSepX.value() );
899
900 if ( s.isEmpty() )
901 s = posToString( d, ticSepX.value()*5, View::ScientificFormat, axesColor ).replace('.', QLocale().decimalPoint());
902
903 m_textDocument->setHtml( s );
904 double idealWidth = m_textDocument->idealWidth();
905 double idealHeight = m_textDocument->size().height();
906
907 double x_pos = xToPixel(d)-(idealWidth/2)-4;
908 if ( x_pos < 0 )
909 continue;
910
911 double y_pos = y+dy;
912 if ( (y_pos+idealHeight) > m_clipRect.bottom() )
913 y_pos = y-dy-idealHeight;
914
915 double x_start = x_pos;
916 double x_end = x_start + idealWidth;
917
918 // Use a minimum spacing between labels
919 if ( (last_x_start < x_start) && pixelsToMillimeters( x_start - last_x_end, painter->device() ) < 7 )
920 continue;
921 if ( (last_x_start > x_start) && pixelsToMillimeters( last_x_start - x_end, painter->device() ) < 7 )
922 continue;
923
924 // Don't draw too close to the right edge (has x axis label)
925 if ( pixelsToMillimeters( m_clipRect.right()-x_end, painter->device() ) < endLabelWidth_mm+3 )
926 continue;
927
928 last_x_start = x_start;
929 last_x_end = x_end;
930
931 QPointF drawPoint( x_pos, y_pos );
932 painter->translate( drawPoint );
933 m_textDocument->documentLayout()->draw( painter, QAbstractTextDocumentLayout::PaintContext() );
934 painter->translate( -drawPoint );
935 }
936 }
937
938
drawYAxisLabels(QPainter * painter)939 void View::drawYAxisLabels( QPainter *painter )
940 {
941 QColor axesColor = Settings::axesColor();
942 int const dx = 12;
943
944 double const x=xToPixel(0.);
945
946 double d = ticStartY;
947 long long n = (long long)ceil(m_ymin/ticSepY.value());
948 for( ; d<m_ymax; d += ticSepY.value(), ++n )
949 {
950 // Don't draw zero
951 if ( n == 0 )
952 continue;
953
954 // Don't draw too close to top
955 if ( (m_ymax-d) <= ticSepY.value()*0.6 )
956 continue;
957
958 // Don't draw too close to bottom if the x axis is there
959 if ( m_ymin > -ticSepY.value() && (d-m_ymin) <= ticSepY.value() )
960 continue;
961
962 QString s = tryPiFraction( d, ticSepY.value() );
963
964 if ( s.isEmpty() )
965 s = posToString( d, ticSepY.value()*5, View::ScientificFormat, axesColor ).replace('.', QLocale().decimalPoint());
966
967 m_textDocument->setHtml( s );
968
969 double idealWidth = m_textDocument->idealWidth();
970 double idealHeight = m_textDocument->size().height();
971
972 QPointF drawPoint( 0, yToPixel(d)-(idealHeight/2) );
973
974 if ( m_xmin > -ticSepX.value() )
975 {
976 drawPoint.setX( x+dx );
977 }
978 else
979 {
980 drawPoint.setX( x-dx-idealWidth );
981
982 if ( drawPoint.x() < 0 )
983 {
984 // Don't draw off the left edge of the screen
985 drawPoint.setX( 0 );
986 }
987 }
988
989 // Shouldn't have the label cut off by the bottom of the view
990 if ( drawPoint.y() + idealHeight > m_clipRect.height() )
991 continue;
992
993 painter->translate( drawPoint );
994 m_textDocument->documentLayout()->draw( painter, QAbstractTextDocumentLayout::PaintContext() );
995 painter->translate( -drawPoint );
996 }
997 }
998
999
h(const Plot & plot) const1000 double View::h( const Plot & plot ) const
1001 {
1002 if ( (plot.plotMode == Function::Integral) || (plot.function()->type() == Function::Differential) )
1003 return plot.function()->eq[0]->differentialStates.step().value();
1004
1005 double dx = (m_xmax-m_xmin)/m_clipRect.width();
1006 double dy = (m_ymax-m_ymin)/m_clipRect.height();
1007
1008 switch ( plot.function()->type() )
1009 {
1010 case Function::Cartesian:
1011 case Function::Differential:
1012 return dx;
1013
1014 case Function::Polar:
1015 case Function::Parametric:
1016 case Function::Implicit:
1017 return qMin( dx, dy );
1018 }
1019
1020 qWarning() << "Unknown coord\n";
1021 return qMin( dx, dy );
1022 }
1023
1024
value(const Plot & plot,int eq,double x,bool updateFunction)1025 double View::value( const Plot & plot, int eq, double x, bool updateFunction )
1026 {
1027 Function * function = plot.function();
1028 assert( function );
1029
1030 if ( updateFunction )
1031 plot.updateFunction();
1032
1033 Equation * equation = function->eq[eq];
1034
1035 double dx = h( plot );
1036 DifferentialState * state = plot.state();
1037
1038 return XParser::self()->derivative( plot.derivativeNumber(), equation, state, x, dx );
1039 }
1040
1041
realValue(const Plot & plot,double x,bool updateFunction)1042 QPointF View::realValue( const Plot & plot, double x, bool updateFunction )
1043 {
1044 Function * function = plot.function();
1045 assert( function );
1046
1047 switch ( function->type() )
1048 {
1049 case Function::Differential:
1050 case Function::Cartesian:
1051 {
1052 double y = value( plot, 0, x, updateFunction );
1053 return QPointF( x, y );
1054 }
1055
1056 case Function::Polar:
1057 {
1058 double y = value( plot, 0, x, updateFunction );
1059 return QPointF( y * lcos(x), y * lsin(x) );
1060 }
1061
1062 case Function::Parametric:
1063 {
1064 double X = value( plot, 0, x, updateFunction );
1065 double Y = value( plot, 1, x, updateFunction );
1066 return QPointF( X, Y );
1067 }
1068
1069 case Function::Implicit:
1070 {
1071 // Can only calculate the value when either x or y is fixed.
1072 assert( function->m_implicitMode != Function::UnfixedXY );
1073
1074 double val = value( plot, 0, x, updateFunction );
1075
1076 if ( function->m_implicitMode == Function::FixedX )
1077 return QPointF( function->x, val );
1078 else
1079 return QPointF( val, function->y );
1080 }
1081 }
1082
1083 qWarning() << "Unknown function type!\n";
1084 return QPointF();
1085 }
1086
1087
getXmin(Function * function,bool overlapEdge)1088 double View::getXmin( Function * function, bool overlapEdge )
1089 {
1090 switch ( function->type() )
1091 {
1092 case Function::Parametric:
1093 case Function::Polar:
1094 return function->dmin.value();
1095
1096 case Function::Implicit:
1097 qWarning() << "You probably don't want to do this!\n";
1098 // fall through
1099
1100 case Function::Differential:
1101 case Function::Cartesian:
1102 {
1103 double min = m_xmin;
1104 if ( overlapEdge )
1105 min -= (m_xmax-m_xmin)*0.02;
1106
1107 if ( function->usecustomxmin )
1108 return qMax( min, function->dmin.value() );
1109 else
1110 return min;
1111 }
1112 }
1113
1114 return 0;
1115 }
1116
1117
getXmax(Function * function,bool overlapEdge)1118 double View::getXmax( Function * function, bool overlapEdge )
1119 {
1120 switch ( function->type() )
1121 {
1122 case Function::Parametric:
1123 case Function::Polar:
1124 return function->dmax.value();
1125
1126 case Function::Implicit:
1127 qWarning() << "You probably don't want to do this!\n";
1128 // fall through
1129
1130 case Function::Differential:
1131 case Function::Cartesian:
1132 {
1133 double max = m_xmax;
1134 if ( overlapEdge )
1135 max += (m_xmax-m_xmin)*0.02;
1136
1137 if ( function->usecustomxmax )
1138 return qMin( max, function->dmax.value() );
1139 else
1140 return max;
1141 }
1142 }
1143
1144 return 0;
1145 }
1146
1147 // #define DEBUG_IMPLICIT
1148
1149 #ifdef DEBUG_IMPLICIT
1150 // Used in profiling root finding
1151 int root_find_iterations;
1152 int root_find_requests;
1153 #endif
1154
1155
1156 /**
1157 * For comparing points where two points close together are considered equal.
1158 */
1159 class FuzzyPoint
1160 {
1161 public:
FuzzyPoint(const QPointF & point)1162 FuzzyPoint( const QPointF & point )
1163 {
1164 x = point.x();
1165 y = point.y();
1166 }
1167
1168
FuzzyPoint(double x,double y)1169 FuzzyPoint( double x, double y )
1170 {
1171 FuzzyPoint::x = x;
1172 FuzzyPoint::y = y;
1173 }
1174
1175
operator <(const FuzzyPoint & other) const1176 bool operator < ( const FuzzyPoint & other ) const
1177 {
1178 double du = qAbs(other.x - x) / dx;
1179 double dv = qAbs(other.y - y) / dy;
1180
1181 bool x_eq = (du < 1); // Whether the x coordinates are considered equal
1182 bool y_eq = (dv < 1); // Whether the y coordinates are considered equal
1183
1184 if ( x_eq && y_eq )
1185 {
1186 // Points are close together.
1187 return false;
1188 }
1189
1190 bool x_lt = !x_eq && (x < other.x);
1191 bool y_lt = !y_eq && (y < other.y);
1192
1193 return ( x_lt || (x_eq && y_lt) );
1194 }
1195
1196 double x;
1197 double y;
1198
1199 static double dx;
1200 static double dy;
1201 };
1202 typedef QMap< FuzzyPoint, QPointF > FuzzyPointMap;
1203
1204 double FuzzyPoint::dx = 0;
1205 double FuzzyPoint::dy = 0;
1206
1207
1208 double SegmentMin = 0.1;
1209 double SegmentMax = 6.0;
1210
1211
1212 // The viewable area is divided up into square*squares squares, and the curve
1213 // is traced around in each square.
1214 // NOTE: it is generally a good idea to make this number prime
1215 int squares = 19;
1216
drawImplicit(Function * function,QPainter * painter)1217 void View::drawImplicit( Function * function, QPainter * painter )
1218 {
1219 assert( function->type() == Function::Implicit );
1220
1221 #ifdef DEBUG_IMPLICIT
1222 QTime t;
1223 t.start();
1224
1225 painter->setPen( Qt::black );
1226
1227 for ( double i = 0; i <= squares; ++i )
1228 {
1229 double x = m_xmin + i * (m_xmax-m_xmin)/squares;
1230 double y = m_ymin + i * (m_ymax-m_ymin)/squares;
1231
1232 painter->drawLine( toPixel( QPointF( m_xmin, y ), ClipInfinite ), toPixel( QPointF( m_xmax, y ), ClipInfinite ) );
1233 painter->drawLine( toPixel( QPointF( x, m_ymin ), ClipInfinite ), toPixel( QPointF( x, m_ymax ), ClipInfinite ) );
1234 }
1235
1236 root_find_iterations = 0;
1237 root_find_requests = 0;
1238 #endif
1239
1240 // Need another function for investigating singular points
1241 Plot circular;
1242 QString fname( "f(x)=0" );
1243 XParser::self()->fixFunctionName( fname, Equation::Cartesian, -1 );
1244 circular.setFunctionID( XParser::self()->Parser::addFunction( fname, 0, Function::Cartesian ) );
1245 assert( circular.function() );
1246
1247 const QList< Plot > plots = function->plots();
1248 for ( const Plot &plot : plots )
1249 {
1250 bool setAliased = false;
1251 if ( plot.parameter.type() == Parameter::Animated )
1252 {
1253 // Don't use antialiasing, so that rendering is sped up
1254 if ( painter->renderHints() & QPainter::Antialiasing )
1255 {
1256 setAliased = true;
1257 painter->setRenderHint( QPainter::Antialiasing, false );
1258 }
1259 }
1260
1261 painter->setPen( penForPlot( plot, painter ) );
1262
1263 QList<QPointF> singular;
1264
1265 for ( int i = 0; i <= squares; ++i )
1266 {
1267 double y = m_ymin + i*(m_ymax-m_ymin)/double(squares);
1268
1269 function->y = y;
1270 function->m_implicitMode = Function::FixedY;
1271 QList<double> roots = findRoots( plot, m_xmin, m_xmax, RoughRoot );
1272
1273 for ( double x : qAsConst(roots) )
1274 {
1275 #ifdef DEBUG_IMPLICIT
1276 painter->setPen( QPen( Qt::red, painter->pen().width() ) );
1277 #endif
1278 drawImplicitInSquare( plot, painter, x, y, Qt::Horizontal, & singular );
1279 }
1280
1281
1282
1283 double x = m_xmin + i*(m_xmax-m_xmin)/double(squares);
1284
1285 function->x = x;
1286 function->m_implicitMode = Function::FixedX;
1287 roots = findRoots( plot, m_ymin, m_ymax, RoughRoot );
1288
1289 for ( double y : qAsConst(roots) )
1290 {
1291 #ifdef DEBUG_IMPLICIT
1292 painter->setPen( QPen( Qt::blue, painter->pen().width() ) );
1293 #endif
1294 drawImplicitInSquare( plot, painter, x, y, Qt::Vertical, & singular );
1295 }
1296 }
1297
1298 // Sort out the implicit points
1299 FuzzyPointMap singularSorted;
1300 FuzzyPoint::dx = (m_xmax-m_xmin) * SegmentMin * 0.1 / m_clipRect.width();
1301 FuzzyPoint::dy = (m_ymax-m_ymin) * SegmentMin * 0.1 / m_clipRect.height();
1302 for ( const QPointF &point : qAsConst(singular) )
1303 singularSorted.insert( point, point );
1304 singular = singularSorted.values();
1305
1306 for ( const QPointF &point : qAsConst(singular) )
1307 {
1308 // radius of circle around singular point
1309 double epsilon = qMin( FuzzyPoint::dx, FuzzyPoint::dy );
1310
1311 QString fstr;
1312 fstr = QString("%1(x)=%2(%3+%6*cos(x),%4+%6*sin(x)%5)")
1313 .arg( circular.function()->eq[0]->name() )
1314 .arg( function->eq[0]->name() )
1315 .arg( XParser::self()->number( point.x() ) )
1316 .arg( XParser::self()->number( point.y() ) )
1317 .arg( function->eq[0]->usesParameter() ? ',' + XParser::self()->number( function->k ) : QString() )
1318 .arg( XParser::self()->number( epsilon ) );
1319
1320 bool setFstrOk = circular.function()->eq[0]->setFstr( fstr );
1321 qDebug() << "------------ " << setFstrOk;
1322 assert( setFstrOk );
1323
1324 QList<double> roots = findRoots( circular, 0, 2*M_PI / XParser::self()->radiansPerAngleUnit(), PreciseRoot );
1325
1326 #ifdef DEBUG_IMPLICIT
1327 qDebug() << "Singular point at (x,y)=("<<point.x()<<','<<point.y()<<")\n";
1328 qDebug() << "fstr is " << fstr;
1329 qDebug() << "Found " << roots.size() << " roots.\n";
1330 #endif
1331
1332 for ( double t : qAsConst(roots) )
1333 {
1334 #ifdef DEBUG_IMPLICIT
1335 painter->setPen( QPen( Qt::green, painter->pen().width() ) );
1336 #endif
1337 double x = point.x() + epsilon * lcos(t);
1338 double y = point.y() + epsilon * lsin(t);
1339 drawImplicitInSquare( plot, painter, x, y, {}, & singular );
1340 }
1341 }
1342
1343
1344 if ( setAliased )
1345 painter->setRenderHint( QPainter::Antialiasing, true );
1346 }
1347
1348 #ifdef DEBUG_IMPLICIT
1349 if ( root_find_requests != 0 )
1350 qDebug() << "Average iterations in root finding was " << root_find_iterations/root_find_requests;
1351 qDebug() << "Time taken was " << t.elapsed();
1352 #endif
1353
1354 XParser::self()->removeFunction( circular.functionID() );
1355 }
1356
1357
1358 // static
maxSegmentLength(double curvature)1359 double View::maxSegmentLength( double curvature )
1360 {
1361 // Use a circle angle of 4 degrees to determine the maximum segment length
1362 // Also, limit the length to be between 0.1 and 6 pixels.
1363
1364 double arc = 4 * (M_PI / 180);
1365
1366 if ( curvature < 0 )
1367 curvature = -curvature;
1368
1369 if ( curvature < 1e-20 )
1370 return SegmentMax; // very large circle
1371
1372 double radius = 1.0/curvature;
1373
1374 double segment = arc * radius;
1375 if ( segment < SegmentMin )
1376 segment = SegmentMin;
1377 else if ( segment > SegmentMax )
1378 segment = SegmentMax;
1379
1380 return segment;
1381 }
1382
1383
drawImplicitInSquare(const Plot & plot,QPainter * painter,double x,double y,Qt::Orientations orientation,QList<QPointF> * singular)1384 void View::drawImplicitInSquare( const Plot & plot, QPainter * painter, double x, double y, Qt::Orientations orientation, QList<QPointF> * singular )
1385 {
1386 plot.updateFunction();
1387 Plot diff1 = plot;
1388 diff1.differentiate();
1389 Plot diff2 = diff1;
1390 diff2.differentiate();
1391
1392 #ifdef DEBUG_IMPLICIT
1393 painter->save();
1394 painter->setPen( QPen( Qt::black, painter->pen().width() ) );
1395 QPointF tl = toPixel( QPointF( x, y ), ClipInfinite ) - QPoint( 2, 2 );
1396 painter->drawRect( QRectF( tl, QSizeF( 4, 4 ) ) );
1397 painter->restore();
1398 #endif
1399
1400 double x_side = (m_xmax-m_xmin)/squares;
1401 double y_side = (m_ymax-m_ymin)/squares;
1402
1403 // Use a square around the root to bound the tracing
1404 // To start with, assume that tracing will go up,right. But this
1405 // might not be so, so the upper/lower boundaries may be adjusted depending
1406 // on where the tracing ends up
1407 double x_lower, x_upper, y_lower, y_upper;
1408 if ( orientation & Qt::Vertical )
1409 {
1410 x_lower = x;
1411 x_upper = x + x_side;
1412 }
1413 else
1414 {
1415 double x_prop = (x-m_xmin)/(m_xmax-m_xmin);
1416 x_lower = std::floor( x_prop * squares ) * x_side + m_xmin;
1417 x_upper = x_lower + x_side;
1418 }
1419 if ( orientation & Qt::Horizontal )
1420 {
1421 y_lower = y;
1422 y_upper = y + y_side;
1423 }
1424 else
1425 {
1426 double y_prop = (y-m_ymin)/(m_ymax-m_ymin);
1427 y_lower = std::floor( y_prop * squares ) * y_side + m_ymin;
1428 y_upper = y_lower + y_side;
1429 }
1430
1431 // If during tracing, the root could not be found, then this will be set to true,
1432 // the route will be retraced using a smaller step size and it will attempt to find
1433 // a root again. If it fails for a second time, then tracing is finished.
1434 bool foundRootPreviously = true;
1435
1436 // Used for focal points.
1437 double prevAngle = 0;
1438 int switchCount = 0;
1439
1440 // This is so that the algorithm can "look ahead" to see what is coming up,
1441 // before drawing or committing itself to anything potentially bad
1442 QPointF prev2 = toPixel( QPointF( x, y ), ClipInfinite );
1443 QPointF prev1 = prev2;
1444
1445 // Allow us to doubly retrace
1446 double prev_diff_x = 0;
1447 double prev_diff_y = 0;
1448
1449 for ( int i = 0; i < 500; ++i ) // allow a maximum of 500 traces (to prevent possibly infinite loop)
1450 {
1451 if ( i == 500 - 1 )
1452 {
1453 qDebug() << "Implicit: got to last iteration!\n";
1454 }
1455
1456 // (dx, dy) is perpendicular to curve
1457
1458 plot.function()->x = x;
1459 plot.function()->y = y;
1460
1461 plot.function()->m_implicitMode = Function::FixedY;
1462 double dx = value( diff1, 0, x, false );
1463
1464 plot.function()->m_implicitMode = Function::FixedX;
1465 double dy = value( diff1, 0, y, false );
1466
1467 double k = pixelCurvature( plot, x, y );
1468 double segment_step = maxSegmentLength( k ) * pow( 0.5, switchCount );
1469
1470 // If we couldn't find a root in the previous iteration, it was possibly
1471 // because we were using too large a step size. So reduce the step size
1472 // and try again.
1473 if ( !foundRootPreviously )
1474 segment_step = qMin( segment_step/4, SegmentMin );
1475
1476 // qDebug() << "k="<<k<<" segment_step="<<segment_step;
1477
1478 QPointF p1 = toPixel( QPointF( x, y ), ClipInfinite ) * painter->transform();
1479 QPointF p2 = toPixel( QPointF( x+dx, y+dy ), ClipInfinite ) * painter->transform();
1480 double l = QLineF( p1, p2 ).length() / segment_step;
1481
1482 if ( l == 0 )
1483 {
1484 qDebug() << "length is zero!\n";
1485 break;
1486 }
1487
1488 // (tx, ty) is tangent to the curve in the direction that we are tracing
1489 double tx = -dy/l;
1490 double ty = dx/l;
1491
1492 double angle = atan(ty/tx) + ((tx<0) ? M_PI : 0);
1493 double diff = realModulo( angle-prevAngle, 2*M_PI );
1494
1495 bool switchedDirection = (i > 0) && (diff > (3./4.)*M_PI) && (diff < (5./4.)*M_PI);
1496 if ( switchedDirection )
1497 {
1498 // Why do I care about suddenly changing the direction?
1499 // Because the chances are, a attracting or repelling point has been reached.
1500 // Even if not, it suggests that a smaller step size is needed. If we have
1501 // switched direction and are already at the smallest step size, then note
1502 // the dodgy point for further investigation and give up for now
1503
1504 // qDebug() << "Switched direction: x="<<x<<" switchCount="<<switchCount<<" segment_step="<<segment_step<<" i="<<i;
1505
1506 // Use a step size much smaller than segment min to obtain good accuracy,
1507 // needed for investigating the point further
1508 if ( segment_step <= SegmentMin * 0.01 )
1509 {
1510 // Give up. Tell our parent function to investigate the point further
1511 *singular << QPointF( x, y );
1512 break;
1513 }
1514
1515 // Rewind the last tangent addition as well
1516 x -= prev_diff_x;
1517 y -= prev_diff_y;
1518
1519 prev_diff_x = 0;
1520 prev_diff_y = 0;
1521
1522 switchCount += 2;
1523 continue;
1524 }
1525 else
1526 {
1527 // Reset the stepping adjustment
1528 switchCount = qMax( 0, switchCount-1 );
1529 prevAngle = angle;
1530 // qDebug() << "Didn't switch - x="<<x<<" segment_step="<<segment_step;
1531 }
1532
1533 if ( i == 0 )
1534 {
1535 // First trace; does the bounding square need adjusting?
1536
1537 if ( (tx < 0) && (orientation & Qt::Vertical) )
1538 {
1539 x_lower -= x_side;
1540 x_upper -= x_side;
1541 }
1542
1543 if ( (ty < 0) && (orientation & Qt::Horizontal) )
1544 {
1545 y_lower -= y_side;
1546 y_upper -= y_side;
1547 }
1548 }
1549
1550
1551 // The maximum tangent length before we end up outside our bounding square
1552 double max_tx, max_ty;
1553 if ( tx > 0 )
1554 max_tx = x_upper - x;
1555 else
1556 max_tx = x - x_lower;
1557 if ( ty > 0 )
1558 max_ty = y_upper - y;
1559 else
1560 max_ty = y - y_lower;
1561
1562
1563 // Does (tx,ty) need to be scaled to make sure the tangent stays inside the square?
1564 double scale = qMax( (tx==0) ? 0 : qAbs(tx)/max_tx, (ty==0) ? 0 : qAbs(ty)/max_ty );
1565 bool outOfBounds = scale > 1;
1566 if ( outOfBounds )
1567 {
1568 tx /= scale;
1569 ty /= scale;
1570 }
1571
1572 double x0 = x;
1573 double y0 = y;
1574
1575 x += tx;
1576 y += ty;
1577
1578 plot.function()->x = x;
1579 plot.function()->y = y;
1580
1581 double * coord = 0;
1582 if ( qAbs(tx) > qAbs(ty) )
1583 {
1584 plot.function()->m_implicitMode = Function::FixedX;
1585 coord = & y;
1586 }
1587 else
1588 {
1589 plot.function()->m_implicitMode = Function::FixedY;
1590 coord = & x;
1591 }
1592
1593 bool found = findRoot( coord, plot, RoughRoot );
1594 if ( !found )
1595 {
1596 if ( foundRootPreviously )
1597 {
1598 #ifdef DEBUG_IMPLICIT
1599 qDebug() << "Could not find root!\n";
1600 #endif
1601
1602 // Retrace our steps
1603 x = x0;
1604 y = y0;
1605 prev_diff_x = 0;
1606 prev_diff_y = 0;
1607 foundRootPreviously = false;
1608 continue;
1609 }
1610 else
1611 {
1612 qDebug() << "Couldn't find root - giving up.\n";
1613 break;
1614 }
1615 }
1616 else
1617 foundRootPreviously = true;
1618
1619 prev_diff_x = x - x0;
1620 prev_diff_y = y - y0;
1621
1622 painter->drawLine( prev2, prev1 );
1623 prev2 = prev1;
1624 prev1 = toPixel( QPointF( x, y ), ClipInfinite );
1625 markDiagramPointUsed( prev1 );
1626
1627 if ( outOfBounds )
1628 break;
1629 }
1630
1631 // and the final line
1632 painter->drawLine( prev2, prev1 );
1633 }
1634
1635
drawFunction(Function * function,QPainter * painter)1636 void View::drawFunction( Function * function, QPainter * painter )
1637 {
1638 if ( (function->type() == Function::Differential) &&
1639 (function->eq[0]->order() == 1) &&
1640 function->plotAppearance( Function::Derivative0 ).showTangentField )
1641 {
1642 const QList<Plot> plots = function->plots( Function::PlotCombinations(Function::AllCombinations) & ~Function::DifferentInitialStates );
1643 for ( const Plot &plot : plots )
1644 drawTangentField( plot, painter );
1645 }
1646
1647 const QList<Plot> plots = function->plots();
1648 for ( const Plot &plot : plots )
1649 drawPlot( plot, painter );
1650
1651 }
1652
1653
drawTangentField(const Plot & plot,QPainter * painter)1654 void View::drawTangentField( const Plot & plot, QPainter * painter )
1655 {
1656 plot.updateFunction();
1657 Function * function = plot.function();
1658
1659 assert( function->type() == Function::Differential );
1660 // Can only draw tangent fields for first order differential equations
1661 assert( function->eq[0]->order() == 1 );
1662
1663 painter->setPen( penForPlot( plot, painter ) );
1664
1665 bool useParameter = function->eq[0]->usesParameter();
1666 Vector v( useParameter ? 3 : 2 );
1667
1668 if ( useParameter )
1669 v[1] = function->k;
1670
1671 // For converting from real to pixels
1672 double sx = m_clipRect.width() / (m_xmax - m_xmin);
1673 double sy = m_clipRect.height() / (m_ymax - m_ymin);
1674
1675 for ( double x = ticStartX; x <= m_xmax; x += ticSepX.value() )
1676 {
1677 v[0] = x;
1678 for ( double y = ticStartY; y <= m_ymax; y += ticSepY.value() )
1679 {
1680 v[ useParameter ? 2 : 1 ] = y;
1681
1682 double df = XParser::self()->fkt( function->eq[0], v ) * (sy / sx);
1683 double theta = std::atan( df );
1684 double dx = std::cos( theta ) * (ticSepX.value() / 8.0);
1685 double dy = std::sin( theta ) * (ticSepY.value() / 8.0);
1686
1687 QPointF mid( x, y );
1688 QPointF diff( dx, dy );
1689
1690 painter->drawLine( toPixel( mid-diff ), toPixel( mid+diff ) );
1691 }
1692 }
1693 }
1694
1695
1696 /**
1697 * Convenience function for drawing lines. Unfortunately, QPainter::drawPolyline
1698 * takes a long time to draw the line joins, which is only necessary when we are
1699 * using a fat pen. Therefore, draw each line individually if we are using a
1700 * thin pen to save time.
1701 */
drawPolyline(QPainter * painter,const QPolygonF & points)1702 void drawPolyline( QPainter * painter, const QPolygonF & points )
1703 {
1704 if ( painter->pen().width() > 5 )
1705 painter->drawPolyline( points );
1706 else if ( points.size() >= 2 )
1707 {
1708 QPointF prev = points.first();
1709 for ( int i = 1; i < points.size(); ++i )
1710 {
1711 // QPen pen( painter->pen() );
1712 // pen.setColor( (i%2==0) ? Qt::red : Qt::blue );
1713 // painter->setPen( pen );
1714
1715 QPointF next = points[i];
1716 painter->drawLine( prev, next );
1717 prev = next;
1718 }
1719 }
1720 }
1721
1722 /**
1723 * Speed up drawing by only drawing one line between each straightish section of the curve
1724 * These variable are used to determine when the curve can no longer be approximate by a
1725 * straight line as the new angle has changed too much
1726 */
1727 class CurveApproximator
1728 {
1729 public:
CurveApproximator(const QPolygonF & points)1730 CurveApproximator( const QPolygonF & points )
1731 {
1732 assert( points.size() >= 2 );
1733 reset();
1734
1735 QPointF diff = points[ points.size() - 2 ] - points.last();
1736 currentAngle = atan2( diff.y(), diff.x() );
1737 approximatingCurve = true;
1738 }
1739
CurveApproximator()1740 CurveApproximator() { reset(); }
1741
reset()1742 void reset()
1743 {
1744 currentAngle = 0;
1745 maxClockwise = 0;
1746 maxAnticlockwise = 0;
1747 maxDistance = 0;
1748 approximatingCurve = false;
1749 }
1750
1751
shouldDraw() const1752 bool shouldDraw() const
1753 {
1754 return ((maxAnticlockwise + maxClockwise) * maxDistance) >= 0.5;
1755 }
1756
1757
update(const QPolygonF & points)1758 void update( const QPolygonF & points )
1759 {
1760 // Should have at least two points in the list
1761 assert( points.size() >= 2 );
1762
1763 QPointF p1 = points[ points.size() - 2 ];
1764 QPointF p2 = points.last();
1765
1766 QPointF diff = p1 - p2;
1767 double angle = atan2( diff.y(), diff.x() );
1768
1769 double lineLength = QLineF( p1, p2 ).length();
1770 if ( lineLength > maxDistance )
1771 maxDistance = lineLength;
1772
1773 double clockwise = realModulo( currentAngle-angle, 2*M_PI );
1774 double anticlockwise = realModulo( angle-currentAngle, 2*M_PI );
1775
1776 bool goingClockwise = (clockwise < anticlockwise);
1777
1778 if ( goingClockwise )
1779 {
1780 // anti-clockwise
1781 if ( clockwise > maxClockwise )
1782 maxClockwise = clockwise;
1783 }
1784 else
1785 {
1786 // clockwise
1787 if ( anticlockwise > maxAnticlockwise )
1788 maxAnticlockwise = anticlockwise;
1789 }
1790 }
1791
1792 double currentAngle;
1793 double maxClockwise;
1794 double maxAnticlockwise;
1795 double maxDistance;
1796 bool approximatingCurve;
1797 };
1798
1799
drawPlot(const Plot & plot,QPainter * painter)1800 void View::drawPlot( const Plot & plot, QPainter *painter )
1801 {
1802 plot.updateFunction();
1803 Function * function = plot.function();
1804
1805 // should use drawImplicit for implicit functions
1806 assert( function->type() != Function::Implicit );
1807
1808 double dmin = getXmin( function, true );
1809 double dmax = getXmax( function, true );
1810
1811 if ( dmin >= dmax )
1812 return;
1813
1814 painter->save();
1815
1816 // Bug in Qt 4.2 TP - QPainter::drawPolyline draws the background as well while printing
1817 // So for testing printing, use a brush where one can see the function being drawn
1818 painter->setBrush( Qt::white );
1819
1820 if ( (plot.parameter.type() == Parameter::Animated) && (painter->renderHints() & QPainter::Antialiasing) )
1821 {
1822 // Don't use antialiasing, so that rendering is speeded up
1823 painter->setRenderHint( QPainter::Antialiasing, false );
1824 }
1825
1826 painter->setPen( penForPlot( plot, painter ) );
1827
1828 // the 'middle' dx, which may be increased or decreased
1829 double max_dx = (dmax-dmin)/m_clipRect.width();
1830 if ( (function->type() == Function::Parametric) || (function->type() == Function::Polar) )
1831 max_dx *= 0.01;
1832
1833 // Increase speed while translating the view
1834 bool quickDraw = ( m_zoomMode == Translating );
1835 if ( quickDraw )
1836 max_dx *= 4.0;
1837
1838 double dx = max_dx;
1839
1840 double maxLength = quickDraw ? 8.0 : (function->plotAppearance( plot.plotMode ).style == Qt::SolidLine) ? 4.0 : 1.5;
1841 double minLength = maxLength * 0.5;
1842
1843 bool drawIntegral = m_integralDrawSettings.draw && (m_integralDrawSettings.plot == plot);
1844 double totalLength = 0.0; // total pixel length; used for drawing dotted lines
1845
1846 bool p1Set = false;
1847 QPointF p1, p2;
1848
1849 CurveApproximator approximator;
1850 QPolygonF drawPoints;
1851
1852 double x = dmin;
1853 double prevX = x; // the value of x before last adding dx to it
1854 do
1855 {
1856 QPointF rv = realValue( plot, x, false );
1857
1858 // If we are currently plotting a differential equation, and it became infinite,
1859 // then skip x forward to a point where it is finite
1860 if ( function->type() == Function::Differential && !XParser::self()->differentialFinite )
1861 {
1862 double new_x = XParser::self()->differentialDiverge;
1863 if ( new_x > x )
1864 {
1865 x = new_x;
1866 prevX = x;
1867
1868 continue;
1869 }
1870 }
1871
1872 p2 = toPixel( rv, ClipInfinite );
1873
1874 if ( xclipflg || yclipflg )
1875 {
1876 prevX = x;
1877 x += dx;
1878
1879 p1Set = false; // p1 wouldn't be finite (if we had set it)
1880 continue;
1881 }
1882
1883 if ( !p1Set )
1884 {
1885 prevX = x;
1886 x += dx;
1887
1888 p1 = p2;
1889 p1Set = true;
1890 continue;
1891 }
1892
1893
1894 //BEGIN adjust dx
1895 QRectF bound = QRectF( p1, QSizeF( (p2-p1).x(), (p2-p1).y() ) ).normalized();
1896 double length = QLineF( p1, p2 ).length();
1897 totalLength += length;
1898
1899 double min_mod = (function->type() == Function::Cartesian || function->type() == Function::Differential) ? 1e-2 : 5e-4;
1900 bool dxAtMinimum = (dx <= max_dx*min_mod);
1901 bool dxAtMaximum = (dx >= max_dx);
1902 bool dxTooBig = false;
1903 bool dxTooSmall = false;
1904
1905 if ( QRectF(m_clipRect).intersects( bound ) )
1906 {
1907 dxTooBig = !dxAtMinimum && (length > maxLength);
1908 dxTooSmall = !dxAtMaximum && (length < minLength);
1909 }
1910 else
1911 dxTooSmall = !dxAtMaximum;
1912
1913 if ( dxTooBig )
1914 {
1915 dx *= 0.5;
1916 x = prevX + dx;
1917 totalLength -= length;
1918 continue;
1919 }
1920
1921 if ( dxTooSmall )
1922 dx *= 2.0;
1923 //END adjust dx
1924
1925
1926 if ( drawIntegral && (x >= m_integralDrawSettings.dmin) && (x <= m_integralDrawSettings.dmax) )
1927 {
1928 double y0 = yToPixel( 0 );
1929
1930 /// \todo should draw the shape in one go
1931
1932 QPointF points[4];
1933 points[0] = QPointF( p1.x(), y0 );
1934 points[1] = QPointF( p2.x(), y0 );
1935 points[2] = QPointF( p2.x(), p2.y() );
1936 points[3] = QPointF( p1.x(), p1.y() );
1937
1938 painter->drawPolygon( points, 4 );
1939 }
1940 else if ( penShouldDraw( totalLength, plot ) )
1941 {
1942 if ( drawPoints.isEmpty() )
1943 {
1944 drawPoints << p1;
1945 }
1946 else if ( drawPoints.last() != p1 )
1947 {
1948 drawPolyline( painter, drawPoints );
1949 drawPoints.clear();
1950 drawPoints << p1;
1951 approximator.reset();
1952 }
1953
1954 // The above code should guarantee that drawPoints isn't empty
1955 // But check it now in case I do something stupid
1956 assert( !drawPoints.isEmpty() );
1957
1958 if ( !approximator.approximatingCurve )
1959 {
1960 // Cool, about to add another point. This defines the working angle of the line
1961 // approximation
1962 drawPoints << p2;
1963 approximator = CurveApproximator( drawPoints );
1964 }
1965 else
1966 {
1967 QPointF prev = drawPoints.last();
1968 drawPoints.last() = p2;
1969 approximator.update( drawPoints );
1970
1971 // Allow a maximum deviation (in pixels)
1972 if ( approximator.shouldDraw() )
1973 {
1974 // The approximation is too bad; will have to start again now
1975 drawPoints.last() = prev;
1976 drawPoints << p2;
1977 approximator = CurveApproximator( drawPoints );
1978 }
1979 }
1980 }
1981
1982 markDiagramPointUsed( p2 );
1983
1984 p1 = p2;
1985
1986 Q_ASSERT( dx > 0 );
1987 prevX = x;
1988 x += dx;
1989 }
1990 while ( x <= dmax );
1991
1992 // qDebug() << "drawPoints.size()="<<drawPoints.size();
1993 drawPolyline( painter, drawPoints );
1994
1995 painter->restore();
1996 }
1997
1998
drawFunctionInfo(QPainter * painter)1999 void View::drawFunctionInfo( QPainter * painter )
2000 {
2001 // Don't draw info if translating the view
2002 if ( m_zoomMode == Translating )
2003 return;
2004
2005 // The names of the plots are drawn around the edge of the view, in a clockwise
2006 // direction, starting from the top-right. Picture the positions like this:
2007 //
2008 // 7 8 9 0
2009 // 6 1
2010 // 5 4 3 2
2011
2012 // Used for determining where to draw the next label indicating the plot name
2013 int plotNameAt = 0;
2014
2015 for ( Function * function : qAsConst(XParser::self()->m_ufkt) )
2016 {
2017 if ( m_stopCalculating )
2018 break;
2019
2020 for ( const Plot &plot : function->plots() )
2021 {
2022 plot.updateFunction();
2023
2024 // Draw extrema points?
2025 if ( (function->type() == Function::Cartesian) && function->plotAppearance( plot.plotMode ).showExtrema )
2026 {
2027 const QList<QPointF> stationaryPoints = findStationaryPoints( plot );
2028 for ( const QPointF &realValue : stationaryPoints )
2029 {
2030 painter->setPen( QPen( Qt::black, millimetersToPixels( 1.5, painter->device() ) ) );
2031 painter->drawPoint( toPixel( realValue ) );
2032
2033 QString x = posToString( realValue.x(), (m_xmax-m_xmin)/m_clipRect.width(), View::DecimalFormat );
2034 QString y = posToString( realValue.y(), (m_ymax-m_ymin)/m_clipRect.width(), View::DecimalFormat );
2035
2036 drawLabel( painter, plot.color(), realValue, i18nc( "Extrema point", "x = %1 y = %2", x.replace('.', QLocale().decimalPoint()), y.replace('.', QLocale().decimalPoint()) ) );
2037 }
2038 }
2039
2040 // Show the name of the plot?
2041 if ( function->plotAppearance( plot.plotMode ).showPlotName )
2042 {
2043 double x, y;
2044
2045 double xmin = m_xmin + 0.1 * (m_xmax-m_xmin);
2046 double xmax = m_xmax - 0.1 * (m_xmax-m_xmin);
2047 double ymin = m_ymin + 0.1 * (m_ymax-m_ymin);
2048 double ymax = m_ymax - 0.1 * (m_ymax-m_ymin);
2049
2050 // Find out where on the outer edge of the view to draw it
2051 if ( 0 <= plotNameAt && plotNameAt <= 2 )
2052 {
2053 x = xmax;
2054 y = ymax - (ymax-ymin)*plotNameAt/2;
2055 }
2056 else if ( 3 <= plotNameAt && plotNameAt <= 5 )
2057 {
2058 x = xmax - (xmax-xmin)*(plotNameAt-2)/3;
2059 y = ymin;
2060 }
2061 else if ( 6 <= plotNameAt && plotNameAt <= 7 )
2062 {
2063 x = xmin;
2064 y = ymin + (ymax-ymin)*(plotNameAt-5)/2;
2065 }
2066 else
2067 {
2068 x = xmin + (xmax-xmin)*(plotNameAt-7)/3;
2069 y = ymax;
2070 }
2071
2072 plotNameAt = (plotNameAt+1) % 10;
2073
2074 QPointF realPos;
2075
2076 if ( function->type() == Function::Implicit )
2077 {
2078 findRoot( & x, & y, plot, RoughRoot );
2079 realPos = QPointF( x, y );
2080 }
2081 else
2082 {
2083 double t = getClosestPoint( QPointF( x, y ), plot );
2084 realPos = realValue( plot, t, false );
2085 }
2086
2087 // If the closest point isn't in the view, then don't draw the label
2088 if ( realPos.x() < m_xmin || realPos.x() > m_xmax || realPos.y() < m_ymin || realPos.y() > m_ymax )
2089 continue;
2090
2091 drawLabel( painter, plot.color(), realPos, plot.name() );
2092 }
2093 }
2094 }
2095 }
2096
2097
drawLabel(QPainter * painter,const QColor & color,const QPointF & realPos,const QString & text)2098 void View::drawLabel( QPainter * painter, const QColor & color, const QPointF & realPos, const QString & text )
2099 {
2100 QPalette palette;
2101 QColor outline = color;
2102 QColor background = outline.lighter( 500 );
2103 background.setAlpha( 127 );
2104
2105
2106 QPointF pixelCenter = toPixel( realPos );
2107 QRectF rect( pixelCenter, QSizeF( 1, 1 ) );
2108
2109 painter->setFont( m_labelFont );
2110 int flags = Qt::TextSingleLine | Qt::AlignLeft | Qt::AlignTop;
2111 rect = painter->boundingRect( rect, flags, text ).adjusted( -7, -3, 4, 2 );
2112
2113 // Try and find a nice place for inserting the rectangle
2114 int bestCost = int(1e7);
2115 QPointF bestCenter = realPos;
2116 for ( double x = pixelCenter.x() - 300; x <= pixelCenter.x() + 300; x += 20 )
2117 {
2118 for ( double y = pixelCenter.y() - 300; y <= pixelCenter.y() + 300; y += 20 )
2119 {
2120 QPointF center( x, y ) ;
2121 rect.moveCenter( center );
2122 double length = (x-pixelCenter.x())*(x-pixelCenter.x()) + (y-pixelCenter.y())*(y-pixelCenter.y());
2123 int cost = rectCost( rect ) + int(length)/100;
2124
2125 if ( cost < bestCost )
2126 {
2127 bestCenter = center;
2128 bestCost = cost;
2129 }
2130 }
2131 }
2132
2133 rect.moveCenter( bestCenter );
2134
2135 markDiagramAreaUsed( rect );
2136
2137 painter->setBrush( background );
2138 painter->setPen( outline );
2139 painter->drawRoundedRect( rect, int(1000/rect.width()), int(1000/rect.height()) );
2140
2141
2142 // If the rectangle does not lie over realPos, then draw a line to realPos from the rectangle
2143 if ( ! rect.contains( pixelCenter ) )
2144 {
2145 QPointF lineStart = bestCenter;
2146 QLineF line( pixelCenter, bestCenter );
2147
2148 QPointF intersect = bestCenter;
2149
2150 // Where does line intersect the rectangle?
2151 if ( QLineF( rect.topLeft(), rect.topRight() ).intersect( line, & intersect ) == QLineF::BoundedIntersection )
2152 lineStart = intersect;
2153 else if ( QLineF( rect.topRight(), rect.bottomRight() ).intersect( line, & intersect ) == QLineF::BoundedIntersection )
2154 lineStart = intersect;
2155 else if ( QLineF( rect.bottomRight(), rect.bottomLeft() ).intersect( line, & intersect ) == QLineF::BoundedIntersection )
2156 lineStart = intersect;
2157 else if ( QLineF( rect.bottomLeft(), rect.topLeft() ).intersect( line, & intersect ) == QLineF::BoundedIntersection )
2158 lineStart = intersect;
2159
2160 painter->drawLine( lineStart, pixelCenter );
2161 }
2162
2163
2164 painter->setPen( Qt::black );
2165 painter->drawText( rect.adjusted( 7, 3, -4, -2 ), flags, text );
2166 }
2167
2168
usedDiagramRect(const QRectF & rect) const2169 QRect View::usedDiagramRect( const QRectF & rect ) const
2170 {
2171 double x0 = rect.left() / m_clipRect.width();
2172 double x1 = rect.right() / m_clipRect.width();
2173
2174 double y0 = rect.top() / m_clipRect.height();
2175 double y1 = rect.bottom() / m_clipRect.height();
2176
2177 int i0 = qMax( int( x0 * LabelGridSize ), 0 );
2178 int i1 = qMin( int( x1 * LabelGridSize ), LabelGridSize-1 );
2179 int j0 = qMax( int( y0 * LabelGridSize ), 0 );
2180 int j1 = qMin( int( y1 * LabelGridSize ), LabelGridSize-1 );
2181
2182 return QRect( i0, j0, i1-i0+1, j1-j0+1 ) & QRect( 0, 0, LabelGridSize, LabelGridSize );
2183 }
2184
2185
markDiagramAreaUsed(const QRectF & rect)2186 void View::markDiagramAreaUsed( const QRectF & rect )
2187 {
2188 if ( m_zoomMode == Translating )
2189 return;
2190
2191 QRect r = usedDiagramRect( rect );
2192
2193 for ( int i = r.left(); i <= r.right(); ++i )
2194 for ( int j = r.top(); j <= r.bottom(); ++j )
2195 m_usedDiagramArea[i][j] = true;
2196 }
2197
2198
markDiagramPointUsed(const QPointF & point)2199 void View::markDiagramPointUsed( const QPointF & point )
2200 {
2201 if ( m_zoomMode == Translating )
2202 return;
2203
2204 double x = point.x() / m_clipRect.width();
2205 double y = point.y() / m_clipRect.height();
2206
2207 int i = int( x * LabelGridSize );
2208 int j = int( y * LabelGridSize );
2209
2210 if ( i<0 || i>=LabelGridSize || j<0 || j>=LabelGridSize )
2211 return;
2212
2213 m_usedDiagramArea[i][j] = true;
2214 }
2215
2216
rectCost(QRectF rect) const2217 int View::rectCost( QRectF rect ) const
2218 {
2219 rect = rect.normalized();
2220
2221 int cost = 0;
2222
2223 // If the rectangle goes off the edge, mark it as very high cost)
2224 if ( rect.intersects( m_clipRect ) )
2225 {
2226 QRectF intersect = (rect & m_clipRect);
2227 cost += int(rect.width() * rect.height() - intersect.width() * intersect.height());
2228 }
2229 else
2230 {
2231 // The rectangle is completely outside!
2232 cost += int(rect.width() * rect.height());
2233 }
2234
2235
2236 QRect r = usedDiagramRect( rect );
2237
2238 for ( int i = r.left(); i <= r.right(); ++i )
2239 for ( int j = r.top(); j <= r.bottom(); ++j )
2240 if ( m_usedDiagramArea[i][j] )
2241 cost += 200;
2242
2243 return cost;
2244 }
2245
2246
penShouldDraw(double length,const Plot & plot)2247 bool View::penShouldDraw( double length, const Plot & plot )
2248 {
2249 // Always use a solid line when translating the view
2250 if ( m_zoomMode == Translating )
2251 return true;
2252
2253 Function * function = plot.function();
2254
2255 Qt::PenStyle style = function->plotAppearance( plot.plotMode ).style;
2256
2257 double sepBig = 8.0; // separation distance between dashes
2258 double sepMid = 7.0; // separation between a dash and a dot
2259 double sepSmall = 6.5; // separation distance between dots
2260 double dash = 9.0; // length of a dash
2261 double dot = 3.5; // length of a dot
2262
2263 switch ( style )
2264 {
2265 case Qt::NoPen:
2266 // *whatever*...
2267 return false;
2268
2269 case Qt::SolidLine:
2270 return true;
2271
2272 case Qt::DashLine:
2273 return realModulo( length, dash + sepBig ) < dash;
2274
2275 case Qt::DotLine:
2276 return realModulo( length, dot + sepSmall ) < dot;
2277
2278 case Qt::DashDotLine:
2279 {
2280 double at = realModulo( length, dash + sepMid + dot + sepMid );
2281
2282 if ( at < dash )
2283 return true;
2284 if ( at < (dash + sepMid) )
2285 return false;
2286 if ( at < (dash + sepMid + dot) )
2287 return true;
2288 return false;
2289 }
2290
2291 case Qt::DashDotDotLine:
2292 {
2293 double at = realModulo( length, dash + sepMid + dot + sepSmall + dot + sepMid );
2294
2295 if ( at < dash )
2296 return true;
2297 if ( at < (dash + sepMid) )
2298 return false;
2299 if ( at < (dash + sepMid + dot) )
2300 return true;
2301 if ( at < (dash + sepMid + dot + sepSmall) )
2302 return false;
2303 if ( at < (dash + sepMid + dot + sepSmall + dot) )
2304 return true;
2305 return false;
2306 }
2307
2308 case Qt::MPenStyle:
2309 case Qt::CustomDashLine:
2310 {
2311 assert( ! "Do not know how to handle this style!" );
2312 return true;
2313 }
2314 }
2315
2316 assert( ! "Unknown pen style!" );
2317 return true;
2318 }
2319
2320
penForPlot(const Plot & plot,QPainter * painter) const2321 QPen View::penForPlot( const Plot & plot, QPainter * painter ) const
2322 {
2323 QPen pen;
2324 if ( m_zoomMode == Translating )
2325 {
2326 // plot style is always a solid line when translating the view
2327 pen.setCapStyle( Qt::FlatCap );
2328 }
2329 else
2330 {
2331 pen.setCapStyle( Qt::RoundCap );
2332 // (the style will be set back to FlatCap if the plot style is a solid line)
2333 }
2334
2335 pen.setColor( plot.color() );
2336
2337 Function * ufkt = plot.function();
2338 PlotAppearance appearance = ufkt->plotAppearance( plot.plotMode );
2339
2340 double lineWidth_mm = appearance.lineWidth;
2341
2342 if ( appearance.style == Qt::SolidLine )
2343 pen.setCapStyle( Qt::FlatCap );
2344
2345 double width = millimetersToPixels( lineWidth_mm, painter->device() );
2346 pen.setWidthF( width );
2347
2348 return pen;
2349 }
2350
2351
millimetersToPixels(double width_mm,QPaintDevice * device) const2352 double View::millimetersToPixels( double width_mm, QPaintDevice * device ) const
2353 {
2354 // assert( device->logicalDpiX() == device->logicalDpiY() );
2355 return device->logicalDpiX() * (width_mm/25.4);
2356 }
2357
2358
pixelsToMillimeters(double width_pixels,QPaintDevice * device) const2359 double View::pixelsToMillimeters( double width_pixels, QPaintDevice * device ) const
2360 {
2361 // assert( device->logicalDpiX() == device->logicalDpiY() );
2362 return (width_pixels * 25.4) / device->logicalDpiX();
2363 }
2364
2365
drawHeaderTable(QPainter * painter)2366 void View::drawHeaderTable( QPainter *painter )
2367 {
2368 painter->setFont( Settings::headerTableFont() );
2369
2370 QString alx = i18nc("%1=minimum value, %2=maximum value", "%1 to %2", Settings::xMin(), Settings::xMax());
2371 QString aly = i18nc("%1=minimum value, %2=maximum value", "%1 to %2", Settings::yMin(), Settings::yMax());
2372
2373 QString atx = i18nc("'E' is the distance between ticks on the axis", "1E = ") + ticSepX.expression();
2374 QString aty = i18nc("'E' is the distance between ticks on the axis", "1E = ") + ticSepY.expression();
2375
2376 QString text = "<div style=\"margin: 0 auto;\"><table border=\"1\" cellpadding=\"4\" cellspacing=\"0\">"
2377 "<tr><td><b>" + i18n("Parameters") + "</b></td><td><b>" + i18n("Plotting Range") + "</b></td><td><b>" + i18n("Axes Division") + "</b></td></tr>"
2378 "<tr><td><b>" + i18n("x-Axis:") + "</b></td><td>" + alx + "</td><td>" + atx + "</td></tr>"
2379 "<tr><td><b>" + i18n("y-Axis:") + "</b></td><td>" + aly + "</td><td>" + aty + "</td></tr>"
2380 "</table></div>";
2381
2382 text += "<br><br><b>" + i18n("Functions:") + "</b><ul>";
2383
2384 for ( Function * function : qAsConst(XParser::self()->m_ufkt) )
2385 text += "<li>" + function->name().replace( '\n', "<br>" ) + "</li>";
2386
2387 text += "</ul>";
2388
2389 m_textDocument->setHtml( text );
2390 m_textDocument->documentLayout()->draw( painter, QAbstractTextDocumentLayout::PaintContext() );
2391
2392 QRectF br = m_textDocument->documentLayout()->frameBoundingRect( m_textDocument->rootFrame() );
2393 painter->translate( 0, br.height() );
2394 }
2395
2396
findStationaryPoints(const Plot & plot)2397 QList< QPointF > View::findStationaryPoints( const Plot & plot )
2398 {
2399 Plot plot2 = plot;
2400 plot2.differentiate();
2401
2402 const QList< double > roots = findRoots( plot2, getXmin( plot.function() ), getXmax( plot.function() ), RoughRoot );
2403
2404 plot.updateFunction();
2405 QList< QPointF > stationaryPoints;
2406 for ( double x : roots )
2407 {
2408 QPointF real = realValue( plot, x, false );
2409 if ( real.y() >= m_ymin && real.y() <= m_ymax )
2410 stationaryPoints << real;
2411 }
2412
2413 return stationaryPoints;
2414 }
2415
2416
findRoots(const Plot & plot,double min,double max,RootAccuracy accuracy)2417 QList< double > View::findRoots( const Plot & plot, double min, double max, RootAccuracy accuracy )
2418 {
2419 typedef QMap< double, double > DoubleMap;
2420 DoubleMap roots;
2421
2422 int count = 10; // number of points to (initially) check for roots
2423
2424 int prevNumRoots = 0;
2425 while ( count < 1000 )
2426 {
2427 // Use this to detect finding the same root.
2428 double prevX = 0.0;
2429
2430 double dx = (max-min) / double(count);
2431 for ( int i = 0; i <= count; ++i )
2432 {
2433 double x = min + dx*i;
2434
2435 bool found = findRoot( & x, plot, accuracy );
2436 if ( !found || x < min || x > max )
2437 continue;
2438
2439 if ( !roots.isEmpty() )
2440 {
2441 // Check if already have a close root
2442 if ( qAbs(x-prevX) <= (dx/4) )
2443 continue;
2444
2445 DoubleMap::iterator nextIt = roots.lowerBound(x);
2446 if ( nextIt == roots.end() )
2447 --nextIt;
2448
2449 double lower, upper;
2450 lower = upper = *nextIt;
2451 if ( nextIt != roots.begin() )
2452 lower = *(--nextIt);
2453
2454 if ( (qAbs(x-lower) <= (dx/4)) || (qAbs(x-upper) <= (dx/4)) )
2455 continue;
2456 }
2457
2458 roots.insert( x, x );
2459 prevX = x;
2460 }
2461
2462 int newNumRoots = roots.size();
2463 if ( newNumRoots == prevNumRoots )
2464 break;
2465
2466 prevNumRoots = newNumRoots;
2467 count *= 4;
2468 }
2469
2470 return roots.keys();
2471 }
2472
2473
setupFindRoot(const Plot & plot,RootAccuracy accuracy,double * max_k,double * max_f,int * n)2474 void View::setupFindRoot( const Plot & plot, RootAccuracy accuracy, double * max_k, double * max_f, int * n )
2475 {
2476 plot.updateFunction();
2477
2478 if ( accuracy == PreciseRoot )
2479 {
2480 *max_k = 200;
2481 *max_f = 1e-14;
2482 }
2483 else
2484 {
2485 // Rough root
2486 *max_k = 10;
2487 *max_f = 1e-10;
2488 }
2489
2490 *n = 1 + plot.derivativeNumber();
2491 }
2492
2493
findRoot(double * x,const Plot & plot,RootAccuracy accuracy)2494 bool View::findRoot( double * x, const Plot & plot, RootAccuracy accuracy )
2495 {
2496 #ifdef DEBUG_IMPLICIT
2497 root_find_requests++;
2498 #endif
2499
2500 double max_k, max_f;
2501 int n;
2502 setupFindRoot( plot, accuracy, & max_k, & max_f, & n );
2503
2504 Equation * eq = plot.function()->eq[0];
2505 DifferentialState * state = plot.state();
2506
2507 double h = qMin( m_xmax-m_xmin, m_ymax-m_ymin ) * 1e-4;
2508
2509 double f = value( plot, 0, *x, false );
2510 int k;
2511 for ( k=0; k < max_k; ++k )
2512 {
2513 double df = XParser::self()->derivative( n, eq, state, *x, h );
2514 if ( qAbs(df) < 1e-20 )
2515 df = 1e-20 * ((df < 0) ? -1 : 1);
2516
2517 double dx = f / df;
2518 *x -= dx;
2519 f = value( plot, 0, *x, false );
2520
2521 if ( (qAbs(f) <= max_f) && (qAbs(dx) <= (h*1e-5)) )
2522 break;
2523 }
2524
2525 #ifdef DEBUG_IMPLICIT
2526 root_find_iterations += k;
2527 #endif
2528
2529 // We continue calculating until |f| < max_f; this may result in k reaching
2530 // max_k. However, if |f| is reasonably small (even if reaching max_k),
2531 // we consider it a root.
2532 return ( qAbs(f) < 1e-6 );
2533 }
2534
2535
findRoot(double * x,double * y,const Plot & plot,RootAccuracy accuracy)2536 bool View::findRoot( double * x, double * y, const Plot & plot, RootAccuracy accuracy )
2537 {
2538 double max_k, max_f;
2539 int n;
2540 setupFindRoot( plot, accuracy, & max_k, & max_f, & n );
2541
2542 Function * function = plot.function();
2543 Equation * eq = function->eq[0];
2544 DifferentialState * state = plot.state();
2545
2546 double hx = (m_xmax-m_xmin) * 1e-5;
2547 double hy = (m_ymax-m_ymin) * 1e-5;
2548
2549 function->y = *y;
2550 function->m_implicitMode = Function::FixedY;
2551 double f = value( plot, 0, *x, false );
2552
2553 for ( int k=0; k < max_k; ++k )
2554 {
2555 function->x = *x;
2556 function->y = *y;
2557
2558 function->m_implicitMode = Function::FixedY;
2559 double dfx = XParser::self()->derivative( n, eq, state, *x, hx );
2560
2561 function->m_implicitMode = Function::FixedX;
2562 double dfy = XParser::self()->derivative( n, eq, state, *y, hy );
2563
2564 double dff = dfx*dfx + dfy*dfy;
2565 if ( dff < 1e-20 )
2566 dff = 1e-20;
2567
2568 double dx = f * dfx / dff;
2569 *x -= dx;
2570 double dy = f * dfy / dff;
2571 *y -= dy;
2572
2573 function->y = *y;
2574 function->m_implicitMode = Function::FixedY;
2575 f = value( plot, 0, *x, false );
2576
2577 if ( (qAbs(f) <= max_f) && (qAbs(dx) <= (hx*1e-5)) && (qAbs(dy) <= (hy*1e-5)) )
2578 break;
2579 }
2580
2581 // We continue calculating until |f| < max_f; this may result in k reaching
2582 // max_k. However, if |f| is reasonably small (even if reaching max_k),
2583 // we consider it a root.
2584 return ( qAbs(f) < 1e-6 );
2585 }
2586
2587
2588
paintEvent(QPaintEvent *)2589 void View::paintEvent(QPaintEvent *)
2590 {
2591 // Note: it is important to have this function call before we begin painting
2592 // as updateCrosshairPosition may set the statusbar text
2593 bool inBounds = updateCrosshairPosition();
2594
2595 QPainter p;
2596 p.begin(this);
2597
2598 p.drawPixmap( QPoint( 0, 0 ), buffer );
2599
2600 // the current cursor position in widget coordinates
2601 QPoint mousePos = mapFromGlobal( QCursor::pos() );
2602
2603 if ( (m_zoomMode == ZoomInDrawing) || (m_zoomMode == ZoomOutDrawing) )
2604 {
2605 QPalette palette;
2606 QColor highlightColor = palette.color( QPalette::Highlight );
2607 QColor backgroundColor = highlightColor;
2608 backgroundColor.setAlpha( 63 );
2609
2610 p.setPen( highlightColor );
2611 p.setBrush( backgroundColor );
2612
2613 p.setBackgroundMode (Qt::OpaqueMode);
2614 p.setBackground (Qt::blue);
2615
2616 QRect rect( m_zoomRectangleStart, mousePos );
2617 p.drawRect( rect );
2618 }
2619 else if ( m_zoomMode == AnimatingZoom )
2620 {
2621 QPointF tl( toPixel( m_animateZoomRect.topLeft() ) );
2622 QPointF br( toPixel( m_animateZoomRect.bottomRight() ) );
2623 p.drawRect( QRectF( tl, QSizeF( br.x()-tl.x(), br.y()-tl.y() ) ) );
2624 }
2625 else if ( shouldShowCrosshairs() )
2626 {
2627 Function * function = m_currentPlot.function();
2628
2629 QPen pen;
2630
2631 if ( function )
2632 {
2633 QColor functionColor = m_currentPlot.color();
2634 pen.setColor( functionColor );
2635 p.setPen( pen );
2636 p.setRenderHint( QPainter::Antialiasing, true );
2637
2638 double x = m_crosshairPosition.x();
2639 double y = m_crosshairPosition.y();
2640
2641 //BEGIN calculate curvature, normal
2642 double k = 0;
2643 double normalAngle = 0;
2644
2645 switch ( function->type() )
2646 {
2647 case Function::Parametric:
2648 case Function::Polar:
2649 normalAngle = pixelNormal( m_currentPlot, m_trace_x );
2650 k = pixelCurvature( m_currentPlot, m_trace_x );
2651 break;
2652
2653 case Function::Differential:
2654 case Function::Cartesian:
2655 case Function::Implicit:
2656 normalAngle = pixelNormal( m_currentPlot, x, y );
2657 k = pixelCurvature( m_currentPlot, x, y );
2658 break;
2659 }
2660
2661 if ( k < 0 )
2662 {
2663 k = -k;
2664 normalAngle += M_PI;
2665 }
2666 //END calculate curvature, normal
2667
2668 if ( k > 1e-5 && Settings::detailedTracing() && inBounds )
2669 {
2670 p.save();
2671
2672 // Transform the painter so that the center of the osculating circle is the origin,
2673 // with the normal line coming in from the left.
2674 QPointF center = m_crosshairPixelCoords + (1/k) * QPointF( cos( normalAngle ), sin( normalAngle ) );
2675 p.translate( center );
2676 p.rotate( normalAngle * 180 / M_PI );
2677
2678 // draw osculating circle
2679 pen.setColor( functionColor );
2680 p.setPen( pen );
2681 p.drawEllipse( QRectF( -QPointF( 1/k, 1/k ), QSizeF( 2/k, 2/k ) ) );
2682
2683 // draw normal
2684 pen.setColor( functionColor );
2685 p.setPen( pen );
2686 p.setBrush( pen.color() );
2687 p.drawLine( QLineF( -1/k, 0, 0, 0 ) );
2688
2689 // draw normal arrow
2690 QPolygonF arrowHead(3);
2691 arrowHead[0] = QPointF( 0, 0 );
2692 arrowHead[1] = QPointF( -3, -2 );
2693 arrowHead[2] = QPointF( -3, +2 );
2694 p.drawPolygon( arrowHead );
2695
2696 // draw tangent
2697 double tangent_scale = 1.2; // make the tangent look better
2698 p.drawLine( QLineF( -1/k, -qMax( 1/k, qreal(15.) ) * tangent_scale, -1/k, qMax( 1/k, qreal(15.) ) * tangent_scale ) );
2699
2700 // draw perpendicular symbol
2701 QPolygonF perp(3);
2702 perp[0] = QPointF( -1/k, 10 );
2703 perp[1] = QPointF( -1/k + 10, 10 );
2704 perp[2] = QPointF( -1/k + 10, 0 );
2705 p.drawPolyline( perp );
2706
2707 // draw intersection blob
2708 p.drawRect( QRectF( -1/k-1, -1, 2, 2 ) );
2709
2710 p.restore();
2711
2712 // Already show osculating circle, etc, so don't draw crosshairs quite so prominently
2713 functionColor.setAlpha( 63 );
2714 pen.setColor( functionColor );
2715 }
2716 }
2717 else
2718 {
2719 // Use an inverted background color for contrast
2720 QColor inverted = QColor( 255-m_backgroundColor.red(), 255-m_backgroundColor.green(), 255-m_backgroundColor.blue() );
2721 pen.setColor( inverted );
2722 }
2723
2724 p.setPen( pen );
2725 double x = m_crosshairPixelCoords.x();
2726 double y = m_crosshairPixelCoords.y();
2727 p.drawLine( QPointF( 0, y ), QPointF( m_clipRect.right(), y ) );
2728 p.drawLine( QPointF( x, 0 ), QPointF( x, m_clipRect.height() ) );
2729 }
2730
2731 p.end();
2732 }
2733
2734
pixelNormal(const Plot & plot,double x,double y)2735 double View::pixelNormal( const Plot & plot, double x, double y )
2736 {
2737 Function * f = plot.function();
2738 assert( f );
2739
2740 plot.updateFunction();
2741
2742 // For converting from real to pixels
2743 double sx = m_clipRect.width() / (m_xmax - m_xmin);
2744 double sy = m_clipRect.height() / (m_ymax - m_ymin);
2745
2746 double dx = 0;
2747 double dy = 0;
2748
2749 double h = this->h( plot );
2750
2751 int d0 = plot.derivativeNumber();
2752 int d1 = d0+1;
2753
2754 switch ( f->type() )
2755 {
2756 case Function::Differential:
2757 case Function::Cartesian:
2758 {
2759 double df = XParser::self()->derivative( d1, f->eq[0], plot.state(), x, h );
2760 return -atan( df * (sy/sx) ) - (M_PI/2);
2761 }
2762
2763 case Function::Implicit:
2764 {
2765 dx = XParser::self()->partialDerivative( d1, d0, f->eq[0], 0, x, y, h, h ) / sx;
2766 dy = XParser::self()->partialDerivative( d0, d1, f->eq[0], 0, x, y, h, h ) / sy;
2767
2768 double theta = -atan( dy / dx );
2769
2770 if ( dx < 0 )
2771 theta += M_PI;
2772
2773 theta += M_PI;
2774
2775 return theta;
2776 }
2777
2778 case Function::Polar:
2779 {
2780 double r = XParser::self()->derivative( d0, f->eq[0], 0, x, h );
2781 double dr = XParser::self()->derivative( d1, f->eq[0], 0, x, h );
2782
2783 dx = (dr * lcos(x) - r * lsin(x) * XParser::self()->radiansPerAngleUnit()) * sx;
2784 dy = (dr * lsin(x) + r * lcos(x) * XParser::self()->radiansPerAngleUnit()) * sy;
2785 break;
2786 }
2787
2788 case Function::Parametric:
2789 {
2790 dx = XParser::self()->derivative( d1, f->eq[0], 0, x, h ) * sx;
2791 dy = XParser::self()->derivative( d1, f->eq[1], 0, x, h ) * sy;
2792 break;
2793 }
2794 }
2795
2796 double theta = - atan( dy / dx ) - (M_PI/2);
2797
2798 if ( dx < 0 )
2799 theta += M_PI;
2800
2801 return theta;
2802 }
2803
2804
pixelCurvature(const Plot & plot,double x,double y)2805 double View::pixelCurvature( const Plot & plot, double x, double y )
2806 {
2807 Function * f = plot.function();
2808
2809 // For converting from real to pixels
2810 double sx = m_clipRect.width() / (m_xmax - m_xmin);
2811 double sy = m_clipRect.height() / (m_ymax - m_ymin);
2812
2813 double fdx = 0;
2814 double fdy = 0;
2815 double fddx = 0;
2816 double fddy = 0;
2817 double fdxy = 0;
2818
2819 double h = this->h( plot );
2820
2821 int d0 = plot.derivativeNumber();
2822 int d1 = d0+1;
2823 int d2 = d0+2;
2824
2825 switch ( f->type() )
2826 {
2827 case Function::Differential:
2828 case Function::Cartesian:
2829 {
2830 DifferentialState * state = plot.state();
2831
2832 fdx = sx;
2833 fddx = 0;
2834
2835 fdy = XParser::self()->derivative( d1, f->eq[0], state, x, h ) * sy;
2836 fddy = XParser::self()->derivative( d2, f->eq[0], state, x, h) * sy;
2837
2838 // qDebug() << "fdy="<<fdy<<" fddy="<<fddy;
2839
2840 break;
2841 }
2842
2843 case Function::Polar:
2844 {
2845 double r = XParser::self()->derivative( d0, f->eq[0], 0, x, h );
2846 double dr = XParser::self()->derivative( d1, f->eq[0], 0, x, h );
2847 double ddr = XParser::self()->derivative( d2, f->eq[0], 0, x, h );
2848
2849 fdx = (dr * lcos(x) - r * lsin(x) * XParser::self()->radiansPerAngleUnit()) * sx;
2850 fdy = (dr * lsin(x) + r * lcos(x) * XParser::self()->radiansPerAngleUnit()) * sy;
2851
2852 double rpau = XParser::self()->radiansPerAngleUnit();
2853
2854 fddx = (ddr * lcos(x) - 2 * dr * lsin(x) * rpau - r * lcos(x) * rpau*rpau) * sx;
2855 fddy = (ddr * lsin(x) + 2 * dr * lcos(x) * rpau - r * lsin(x) * rpau*rpau) * sy;
2856
2857 break;
2858 }
2859
2860 case Function::Parametric:
2861 {
2862 fdx = XParser::self()->derivative( d1, f->eq[0], 0, x, h ) * sx;
2863 fdy = XParser::self()->derivative( d1, f->eq[1], 0, x, h ) * sy;
2864
2865 fddx = XParser::self()->derivative( d2, f->eq[0], 0, x, h ) * sx;
2866 fddy = XParser::self()->derivative( d2, f->eq[1], 0, x, h ) * sy;
2867
2868 break;
2869 }
2870
2871 case Function::Implicit:
2872 {
2873 fdx = XParser::self()->partialDerivative( d1, d0, f->eq[0], 0, x, y, h, h ) / sx;
2874 fdy = XParser::self()->partialDerivative( d0, d1, f->eq[0], 0, x, y, h, h ) / sy;
2875
2876 fddx = XParser::self()->partialDerivative( d2, d0, f->eq[0], 0, x, y, h, h ) / (sx*sx);
2877 fddy = XParser::self()->partialDerivative( d0, d2, f->eq[0], 0, x, y, h, h ) / (sy*sy);
2878
2879 fdxy = XParser::self()->partialDerivative( d1, d1, f->eq[0], 0, x, y, h, h ) / (sx*sy);
2880
2881
2882 break;
2883 }
2884 }
2885
2886 double mod = pow( fdx*fdx + fdy*fdy, 1.5 );
2887
2888 switch ( f->type() )
2889 {
2890 case Function::Differential:
2891 case Function::Cartesian:
2892 case Function::Parametric:
2893 case Function::Polar:
2894 return (fdx * fddy - fdy * fddx) / mod;
2895
2896 case Function::Implicit:
2897 return ( fdx*fdx*fddy + fdy*fdy*fddx - 2*fdx*fdy*fdxy ) / mod;
2898 }
2899
2900 qCritical() << "Unknown function type!\n";
2901 return 0;
2902 }
2903
2904
resizeEvent(QResizeEvent *)2905 void View::resizeEvent(QResizeEvent *)
2906 {
2907 if (m_isDrawing) //stop drawing integrals
2908 {
2909 m_stopCalculating = true; //stop drawing
2910 return;
2911 }
2912 qreal dpr = devicePixelRatioF();
2913 buffer = QPixmap( size() * dpr );
2914 buffer.setDevicePixelRatio(dpr);
2915 drawPlot();
2916 }
2917
2918
drawPlot()2919 void View::drawPlot()
2920 {
2921 if ( buffer.width() == 0 || buffer.height() == 0 )
2922 return;
2923
2924 buffer.fill(m_backgroundColor);
2925 draw(&buffer, Screen );
2926 update();
2927 }
2928
2929
focusOutEvent(QFocusEvent *)2930 void View::focusOutEvent( QFocusEvent * )
2931 {
2932 // Redraw ourselves to get rid of the crosshair (if we had it)...
2933 QTimer::singleShot( 0, this, SLOT(update()) );
2934 QTimer::singleShot( 0, this, &View::updateCursor );
2935 }
2936
2937
focusInEvent(QFocusEvent *)2938 void View::focusInEvent( QFocusEvent * )
2939 {
2940 // Redraw ourselves to get the crosshair (if we should have it)...
2941 QTimer::singleShot( 0, this, SLOT(update()) );
2942 QTimer::singleShot( 0, this, &View::updateCursor );
2943 }
2944
2945
crosshairPositionValid(Function * plot) const2946 bool View::crosshairPositionValid( Function * plot ) const
2947 {
2948 if ( !plot )
2949 return false;
2950
2951 // only relevant for cartesian plots - assume true for none
2952 if ( plot->type() != Function::Cartesian )
2953 return true;
2954
2955 bool lowerOk = ((!plot->usecustomxmin) || (plot->usecustomxmin && m_crosshairPosition.x()>plot->dmin.value()));
2956 bool upperOk = ((!plot->usecustomxmax) || (plot->usecustomxmax && m_crosshairPosition.x()<plot->dmax.value()));
2957
2958 return lowerOk && upperOk;
2959 }
2960
2961
mousePressEvent(QMouseEvent * e)2962 void View::mousePressEvent(QMouseEvent *e)
2963 {
2964 m_AccumulatedDelta = 0;
2965 m_mousePressTimer->start();
2966
2967 // In general, we want to update the view
2968 update();
2969
2970 if ( m_popupMenuStatus != NoPopup )
2971 return;
2972
2973 if (m_isDrawing)
2974 {
2975 m_stopCalculating = true; //stop drawing
2976 return;
2977 }
2978
2979 if ( m_zoomMode != Normal )
2980 {
2981 // If the user clicked with the right mouse button will zooming in or out, then cancel it
2982 if ( (m_zoomMode == ZoomInDrawing) ||
2983 (m_zoomMode == ZoomOutDrawing) )
2984 {
2985 m_zoomMode = Normal;
2986 }
2987 updateCursor();
2988 return;
2989 }
2990
2991 m_haveRoot = false;
2992
2993 bool hadFunction = (m_currentPlot.functionID() != -1 );
2994
2995 updateCrosshairPosition();
2996
2997 if( !m_readonly && e->button()==Qt::RightButton) //clicking with the right mouse button
2998 {
2999 getPlotUnderMouse();
3000 if ( m_currentPlot.function() )
3001 {
3002 if ( hadFunction )
3003 m_popupMenuStatus = PopupDuringTrace;
3004 else
3005 m_popupMenuStatus = Popup;
3006
3007 fillPopupMenu();
3008 m_popupMenu->exec( QCursor::pos() );
3009 }
3010 return;
3011 }
3012
3013 if(e->button()!=Qt::LeftButton)
3014 return;
3015
3016 if ( m_currentPlot.functionID() >= 0 ) //disable trace mode if trace mode is enable
3017 {
3018 m_currentPlot.setFunctionID( -1 );
3019 setStatusBar( QString(), RootSection );
3020 setStatusBar( QString(), FunctionSection );
3021 mouseMoveEvent(e);
3022 return;
3023 }
3024
3025 QPointF closestPoint = getPlotUnderMouse();
3026 Function * function = m_currentPlot.function();
3027 if ( function )
3028 {
3029 QPointF ptd( toPixel( closestPoint ) );
3030 QPoint globalPos = mapToGlobal( ptd.toPoint() );
3031 QCursor::setPos( globalPos );
3032 setStatusBar( m_currentPlot.name().replace( '\n', " ; " ), FunctionSection );
3033 return;
3034 }
3035
3036 // user didn't click on a plot; so we prepare to enter translation mode
3037 m_currentPlot.setFunctionID( -1 );
3038 m_zoomMode = AboutToTranslate;
3039 m_prevDragMousePos = e->pos();
3040 updateCursor();
3041 }
3042
3043
fillPopupMenu()3044 void View::fillPopupMenu( )
3045 {
3046 Function * function = m_currentPlot.function();
3047 if ( !function )
3048 return;
3049
3050 m_popupMenuTitle->setText( m_currentPlot.name().replace( '\n', "; " ) );
3051
3052 QAction *calcArea = MainDlg::self()->actionCollection()->action("grapharea");
3053 QAction *maxValue = MainDlg::self()->actionCollection()->action("maximumvalue");
3054 QAction *minValue = MainDlg::self()->actionCollection()->action("minimumvalue");
3055
3056 m_popupMenu->removeAction(calcArea);
3057 m_popupMenu->removeAction(maxValue);
3058 m_popupMenu->removeAction(minValue);
3059
3060 if ( function->type() == Function::Cartesian || function->type() == Function::Differential )
3061 {
3062 m_popupMenu->addAction(calcArea);
3063 m_popupMenu->addAction(maxValue);
3064 m_popupMenu->addAction(minValue);
3065 }
3066 }
3067
3068
getPlotUnderMouse()3069 QPointF View::getPlotUnderMouse()
3070 {
3071 m_currentPlot.setFunctionID( -1 );
3072 m_trace_x = 0.0;
3073
3074 Plot bestPlot;
3075
3076 double best_distance = 1e30; // a nice large number
3077 QPointF best_cspos;
3078
3079 for ( Function * function : qAsConst(XParser::self()->m_ufkt) )
3080 {
3081 const QList< Plot > plots = function->plots();
3082 for ( const Plot &plot : plots )
3083 {
3084 plot.updateFunction();
3085
3086 double best_x = 0.0, distance;
3087 QPointF cspos;
3088
3089 if ( function->type() == Function::Implicit )
3090 {
3091 double x = m_crosshairPosition.x();
3092 double y = m_crosshairPosition.y();
3093 findRoot( & x, & y, plot, PreciseRoot );
3094
3095 QPointF d = toPixel( QPointF( x, y ), ClipInfinite ) - toPixel( QPointF( m_crosshairPosition.x(), m_crosshairPosition.y() ), ClipInfinite );
3096
3097 distance = std::sqrt( d.x()*d.x() + d.y()*d.y() );
3098 cspos = QPointF( x, y );
3099 }
3100 else
3101 {
3102 best_x = getClosestPoint( m_crosshairPosition, plot );
3103 distance = pixelDistance( m_crosshairPosition, plot, best_x, false );
3104 cspos = realValue( plot, best_x, false );
3105 }
3106
3107 if ( distance < best_distance )
3108 {
3109 best_distance = distance;
3110 bestPlot = plot;
3111 m_trace_x = best_x;
3112 best_cspos = cspos;
3113 }
3114 }
3115 }
3116
3117 if ( best_distance < 10.0 )
3118 {
3119 m_currentPlot = bestPlot;
3120 m_crosshairPosition = best_cspos;
3121 return m_crosshairPosition;
3122 }
3123 else
3124 return QPointF();
3125 }
3126
3127
getClosestPoint(const QPointF & pos,const Plot & plot)3128 double View::getClosestPoint( const QPointF & pos, const Plot & plot )
3129 {
3130 plot.updateFunction();
3131
3132 double best_x = 0.0;
3133
3134 Function * function = plot.function();
3135 assert( function->type() != Function::Implicit ); // should use findRoot (3D version) for this
3136
3137 switch ( function->type() )
3138 {
3139 case Function::Implicit:
3140 break;
3141
3142 case Function::Differential:
3143 case Function::Cartesian:
3144 {
3145 double best_pixel_x = m_clipRect.width() / 2;
3146
3147 QPointF pixelPos = toPixel( pos, ClipInfinite );
3148
3149 double dmin = getXmin( function );
3150 double dmax = getXmax( function );
3151
3152 double stepSize = (m_xmax-m_xmin)/m_clipRect.width();
3153
3154 // Algorithm in use here: Work out the shortest distance between the
3155 // line joining (x0,y0) to (x1,y1) and the given point (real_x,real_y)
3156
3157 double x = dmin;
3158 double y0 = value( plot, 0, x, false );
3159
3160 double best_distance = 1e20; // a large distance
3161
3162 while ( x <= dmax && (xToPixel(x) < best_pixel_x+best_distance) )
3163 {
3164 x += stepSize;
3165
3166 double y1 = value( plot, 0, x, false );
3167
3168 double _x0 = xToPixel( x-stepSize, ClipInfinite );
3169 double _x1 = xToPixel( x, ClipInfinite );
3170
3171 double _y0 = yToPixel( y0, ClipInfinite );
3172 double _y1 = yToPixel( y1, ClipInfinite );
3173
3174 double k = (_y1-_y0)/(_x1-_x0);
3175
3176 double closest_x, closest_y;
3177 if ( k == 0 )
3178 {
3179 closest_x = pixelPos.x();
3180 closest_y = _y0;
3181 }
3182 else
3183 {
3184 closest_x = (pixelPos.y() + pixelPos.x()/k + _x0*k - _y0) / (k + 1.0/k);
3185 closest_y = (pixelPos.x() + pixelPos.y()*k + _y0/k - _x0) / (k + 1.0/k);
3186 }
3187
3188 bool valid = (x-1.5*stepSize <= xToReal(closest_x)) && (xToReal(closest_x) <= x+0.5*stepSize);
3189
3190 double dfx = closest_x - pixelPos.x();
3191 double dfy = closest_y - pixelPos.y();
3192
3193 double distance = sqrt( dfx*dfx + dfy*dfy );
3194 bool insideView = 0 <= closest_y && closest_y <= m_clipRect.height();
3195
3196 if ( distance < best_distance && insideView && valid )
3197 {
3198 best_distance = distance;
3199 best_pixel_x = closest_x;
3200 }
3201
3202 y0 = y1;
3203 }
3204
3205 best_x = xToReal( best_pixel_x );
3206 break;
3207 }
3208
3209 case Function::Polar:
3210 case Function::Parametric:
3211 {
3212 double minX = getXmin( function );
3213 double maxX = getXmax( function );
3214 double stepSize = 0.001;
3215
3216 while ( stepSize > 0.0000009 )
3217 {
3218 double best_distance = 1e20; // a large distance
3219
3220 double x = minX;
3221 while ( x <= maxX )
3222 {
3223 double distance = pixelDistance( pos, plot, x, false );
3224 bool insideView = QRectF(m_clipRect).contains( toPixel( realValue( plot, x, false ), ClipInfinite ) );
3225
3226 if ( distance < best_distance && insideView )
3227 {
3228 best_distance = distance;
3229 best_x = x;
3230 }
3231
3232 x += stepSize;
3233 }
3234
3235 minX = best_x - stepSize;
3236 maxX = best_x + stepSize;
3237
3238 stepSize *= 0.1;
3239 }
3240 break;
3241 }
3242 }
3243
3244 return best_x;
3245 }
3246
3247
pixelDistance(const QPointF & pos,const Plot & plot,double x,bool updateFunction)3248 double View::pixelDistance( const QPointF & pos, const Plot & plot, double x, bool updateFunction )
3249 {
3250 QPointF f = realValue( plot, x, updateFunction );
3251 QPointF df = toPixel( pos, ClipInfinite ) - toPixel( f, ClipInfinite );
3252
3253 return std::sqrt( df.x()*df.x() + df.y()*df.y() );
3254 }
3255
3256
posToString(double x,double delta,PositionFormatting format,const QColor & color) const3257 QString View::posToString( double x, double delta, PositionFormatting format, const QColor &color ) const
3258 {
3259 delta = qAbs(delta);
3260 if ( delta == 0 )
3261 delta = 1;
3262
3263 QString numberText;
3264
3265 int decimalPlaces = 1-int(log(delta)/log(10.0));
3266
3267 // Avoid exponential format for smallish numbers
3268 if ( 0.01 < qAbs(x) && qAbs(x) < 10000 )
3269 format = DecimalFormat;
3270
3271 switch ( format )
3272 {
3273 case ScientificFormat:
3274 {
3275 int accuracy = 1 + decimalPlaces + int(log(qAbs(x))/log(10.0));
3276 if ( accuracy < 2 )
3277 {
3278 // Ensure a minimum of two significant digits
3279 accuracy = 2;
3280 }
3281
3282 QString number = QString::number( x, 'g', accuracy );
3283 if ( number.contains( 'e' ) )
3284 {
3285 number.remove( "+0" );
3286 number.remove( '+' );
3287 number.replace( "-0", MinusSymbol );
3288
3289 number.replace( 'e', QChar(215) + QString("10<sup>") );
3290 number.append( "</sup>" );
3291 }
3292 if ( x > 0.0 )
3293 number.prepend('+');
3294
3295 numberText = QString("<html><body><span style=\"color:%1;\">").arg( color.name() ) + number + "</span></body></html>";
3296
3297 break;
3298 }
3299
3300 case DecimalFormat:
3301 {
3302 if ( decimalPlaces >= 0 )
3303 numberText = QString::number( x, 'f', decimalPlaces );
3304 else
3305 numberText = QString::number( x*(pow(10.0,decimalPlaces)), 'f', 0 ) + QString( -decimalPlaces, '0' );
3306
3307 break;
3308 }
3309 }
3310
3311 numberText.replace( '-', MinusSymbol );
3312
3313 return numberText;
3314 }
3315
3316
mouseMoveEvent(QMouseEvent * e)3317 void View::mouseMoveEvent(QMouseEvent *e)
3318 {
3319 if ( m_previousMouseMovePos != e->globalPos() )
3320 {
3321 m_AccumulatedDelta = 0;
3322 }
3323 m_previousMouseMovePos = e->globalPos();
3324 m_AccumulatedDelta = 0;
3325 if ( m_isDrawing || !e)
3326 return;
3327
3328 bool inBounds = updateCrosshairPosition();
3329 if ( !m_haveRoot )
3330 setStatusBar( QString(), RootSection );
3331
3332 QString sx, sy;
3333
3334 if ( inBounds )
3335 {
3336 sx = i18n( "x = %1", posToString( m_crosshairPosition.x(), (m_xmax-m_xmin)/m_clipRect.width(), View::DecimalFormat ).replace('.', QLocale().decimalPoint()) );
3337 sy = i18n( "y = %1", posToString( m_crosshairPosition.y(), (m_ymax-m_ymin)/m_clipRect.width(), View::DecimalFormat ).replace('.', QLocale().decimalPoint()) );
3338 }
3339 else
3340 sx = sy = "";
3341
3342 setStatusBar( sx, XSection );
3343 setStatusBar( sy, YSection );
3344
3345 if ( e->buttons() & Qt::LeftButton )
3346 {
3347 if ( m_zoomMode == ZoomIn )
3348 {
3349 m_zoomMode = ZoomInDrawing;
3350 m_zoomRectangleStart = e->pos();
3351 }
3352 else if ( m_zoomMode == ZoomOut )
3353 {
3354 m_zoomMode = ZoomOutDrawing;
3355 m_zoomRectangleStart = e->pos();
3356 }
3357 else if ( ((m_zoomMode == AboutToTranslate) || (m_zoomMode == Translating)) &&
3358 (e->pos() != m_prevDragMousePos) )
3359 {
3360 m_zoomMode = Translating;
3361 QPoint d = m_prevDragMousePos - e->pos();
3362 m_prevDragMousePos = e->pos();
3363 translateView( d.x(), d.y() );
3364 }
3365 }
3366
3367 if ( (m_zoomMode == Normal) &&
3368 (m_popupMenuStatus != NoPopup) &&
3369 !m_popupMenu->isVisible() )
3370 {
3371 if ( m_popupMenuStatus==Popup)
3372 m_currentPlot.setFunctionID( -1 );
3373 m_popupMenuStatus = NoPopup;
3374 }
3375
3376 update();
3377 updateCursor();
3378 }
3379
3380
leaveEvent(QEvent *)3381 void View::leaveEvent(QEvent *)
3382 {
3383 setStatusBar( "", XSection );
3384 setStatusBar( "", YSection );
3385
3386 updateCrosshairPosition();
3387 update();
3388 }
3389
3390
wheelEvent(QWheelEvent * e)3391 void View::wheelEvent(QWheelEvent *e)
3392 {
3393 m_AccumulatedDelta += e->angleDelta().y();
3394
3395 if (e->modifiers() & Qt::ControlModifier)
3396 {
3397 if (m_AccumulatedDelta >= QWheelEvent::DefaultDeltasPerStep)
3398 {
3399 zoomIn( e->pos(), double(Settings::zoomInStep())/100.0 );
3400 m_AccumulatedDelta = 0;
3401 }
3402 else if (m_AccumulatedDelta <= -QWheelEvent::DefaultDeltasPerStep)
3403 {
3404 zoomIn( e->pos(), (double(Settings::zoomOutStep())/100.0) + 1.0 );
3405 m_AccumulatedDelta = 0;
3406 }
3407 e->accept();
3408 return;
3409 }
3410 else
3411 {
3412 m_AccumulatedDelta = 0;
3413 }
3414 QWidget::wheelEvent(e);
3415 }
3416
3417
updateCrosshairPosition()3418 bool View::updateCrosshairPosition()
3419 {
3420 QPointF mousePos = mapFromGlobal( QCursor::pos() );
3421
3422 bool out_of_bounds = false; // for the ypos
3423
3424 m_crosshairPosition = toReal( mousePos );
3425
3426 m_currentPlot.updateFunction();
3427 Function * it = m_currentPlot.function();
3428
3429 if ( it && crosshairPositionValid( it ) && (m_popupMenuStatus != Popup) )
3430 {
3431 // The user currently has a plot selected, with the mouse in a valid position
3432
3433 if ( (it->type() == Function::Parametric) ||
3434 (it->type() == Function::Polar) )
3435 {
3436
3437 // Should we increase or decrease t to get closer to the mouse?
3438 double dx[2] = { -0.00001, +0.00001 };
3439 double d[] = { 0.0, 0.0 };
3440 for ( int i = 0; i < 2; ++ i )
3441 d[i] = pixelDistance( m_crosshairPosition, m_currentPlot, m_trace_x + dx[i], false );
3442
3443 double prev_best = pixelDistance( m_crosshairPosition, m_currentPlot, m_trace_x, false );
3444 double current_dx = dx[(d[0] < d[1]) ? 0 : 1]*1e3;
3445
3446 while ( true )
3447 {
3448 double new_distance = pixelDistance( m_crosshairPosition, m_currentPlot, m_trace_x + current_dx, false );
3449 if ( new_distance < prev_best )
3450 {
3451 prev_best = new_distance;
3452 m_trace_x += current_dx;
3453 }
3454 else
3455 {
3456 if ( qAbs(current_dx) > 9e-10 )
3457 current_dx *= 0.1;
3458 else
3459 break;
3460 }
3461 }
3462
3463 double min = getXmin( it );
3464 double max = getXmax( it );
3465
3466 if ( m_trace_x > max )
3467 m_trace_x = max;
3468
3469 else if ( m_trace_x < min )
3470 m_trace_x = min;
3471
3472 m_crosshairPosition = realValue( m_currentPlot, m_trace_x, false );
3473 }
3474 else if ( it->type() == Function::Implicit )
3475 {
3476 double x = m_crosshairPosition.x();
3477 double y = m_crosshairPosition.y();
3478 findRoot( & x, & y, m_currentPlot, PreciseRoot );
3479 m_crosshairPosition = QPointF( x, y );
3480 }
3481 else
3482 {
3483 // cartesian or differential plot
3484
3485 m_crosshairPosition.setY( value( m_currentPlot, 0, m_crosshairPosition.x(), false ) );
3486 mousePos.setY( yToPixel( m_crosshairPosition.y() ));
3487
3488 if ( m_crosshairPosition.y()<m_ymin || m_crosshairPosition.y()>m_ymax) //the ypoint is not visible
3489 {
3490 out_of_bounds = true;
3491 }
3492 else if ( (fabs(yToReal(mousePos.y())) < (m_ymax-m_ymin)/80) && (it->type() == Function::Cartesian || it->type() == Function::Differential) )
3493 {
3494 double x0 = m_crosshairPosition.x();
3495 if ( !m_haveRoot && findRoot( &x0, m_currentPlot, PreciseRoot ) )
3496 {
3497 QString str=" ";
3498 str += i18nc("%1 is a subscript zero symbol", "root: x%1 = ", SubscriptZeroSymbol);
3499 setStatusBar( str+QLocale().toString( x0, 'f', 5 ), RootSection );
3500 m_haveRoot=true;
3501 emit updateRootValue( true, x0 );
3502 }
3503 }
3504 else
3505 {
3506 m_haveRoot=false;
3507 emit updateRootValue( false, 0 );
3508 }
3509 }
3510
3511 // For Cartesian plots, only adjust the cursor position if it is not at the ends of the view
3512 if ( ((it->type() != Function::Cartesian) && (it->type() != Function::Differential)) || m_clipRect.contains( mousePos.toPoint() ) )
3513 {
3514 mousePos = toPixel( m_crosshairPosition, ClipAll, mousePos );
3515 QPoint globalPos = mapToGlobal( mousePos.toPoint() );
3516 QCursor::setPos( globalPos );
3517 }
3518 }
3519
3520 m_crosshairPixelCoords = mousePos;
3521
3522 return !out_of_bounds && m_clipRect.contains( mousePos.toPoint() );
3523 }
3524
3525
mouseReleaseEvent(QMouseEvent * e)3526 void View::mouseReleaseEvent ( QMouseEvent * e )
3527 {
3528 bool doDrawPlot = false;
3529
3530 // avoid zooming in if the zoom rectangle is very small and the mouse was
3531 // just pressed, which suggests that the user dragged the mouse accidentally
3532 QRect zoomRect = QRect( m_zoomRectangleStart, e->pos() ).normalized();
3533 int area = zoomRect.width() * zoomRect.height();
3534
3535 if ( (area <= 500) && (m_mousePressTimer->elapsed() < QApplication::startDragTime()) )
3536 {
3537 if ( m_zoomMode == ZoomInDrawing )
3538 m_zoomMode = ZoomIn;
3539 else if ( m_zoomMode == ZoomOutDrawing )
3540 m_zoomMode = ZoomOut;
3541 }
3542
3543 switch ( m_zoomMode )
3544 {
3545 case Normal:
3546 case AnimatingZoom:
3547 case AboutToTranslate:
3548 break;
3549
3550 case Translating:
3551 doDrawPlot = true;
3552 Settings::self()->save();
3553 MainDlg::self()->requestSaveCurrentState();
3554 break;
3555
3556 case ZoomIn:
3557 zoomIn( e->pos(), double(Settings::zoomInStep())/100.0 );
3558 break;
3559
3560 case ZoomOut:
3561 zoomIn( e->pos(), (double(Settings::zoomOutStep())/100.0) + 1.0 );
3562 break;
3563
3564 case ZoomInDrawing:
3565 zoomIn( zoomRect );
3566 break;
3567
3568 case ZoomOutDrawing:
3569 zoomOut( zoomRect );
3570 break;
3571 }
3572
3573 m_zoomMode = Normal;
3574
3575 if ( doDrawPlot )
3576 drawPlot();
3577 else
3578 update();
3579
3580 updateCursor();
3581 }
3582
3583
zoomIn(const QPoint & mousePos,double zoomFactor)3584 void View::zoomIn( const QPoint & mousePos, double zoomFactor )
3585 {
3586 QPointF real = toReal( mousePos );
3587
3588 double diffx = (m_xmax-m_xmin)*zoomFactor;
3589 double diffy = (m_ymax-m_ymin)*zoomFactor;
3590
3591 animateZoom( QRectF( real.x()-diffx, real.y()-diffy, 2.0*diffx, 2.0*diffy ) );
3592 }
3593
3594
zoomIn(const QRectF & zoomRect)3595 void View::zoomIn( const QRectF & zoomRect )
3596 {
3597 QPointF p = zoomRect.topLeft();
3598 double real1x = xToReal(p.x() );
3599 double real1y = yToReal(p.y() );
3600 p = zoomRect.bottomRight();
3601 double real2x = xToReal(p.x() );
3602 double real2y = yToReal(p.y() );
3603
3604 if ( real1x > real2x )
3605 qSwap( real1x, real2x );
3606 if ( real1y > real2y )
3607 qSwap( real1y, real2y );
3608
3609 animateZoom( QRectF( QPointF( real1x, real1y ), QSizeF( real2x-real1x, real2y-real1y ) ) );
3610 }
3611
3612
zoomOut(const QRectF & zoomRect)3613 void View::zoomOut( const QRectF & zoomRect )
3614 {
3615 QPointF p = zoomRect.topLeft();
3616 double _real1x = xToReal(p.x() );
3617 double _real1y = yToReal(p.y() );
3618 p = zoomRect.bottomRight();
3619 double _real2x = xToReal(p.x() );
3620 double _real2y = yToReal(p.y() );
3621
3622 double kx = (_real1x-_real2x)/(m_xmin-m_xmax);
3623 double lx = _real1x - (kx * m_xmin);
3624
3625 double ky = (_real1y-_real2y)/(m_ymax-m_ymin);
3626 double ly = _real1y - (ky * m_ymax);
3627
3628 double real1x = (m_xmin-lx)/kx;
3629 double real2x = (m_xmax-lx)/kx;
3630
3631 double real1y = (m_ymax-ly)/ky;
3632 double real2y = (m_ymin-ly)/ky;
3633
3634 animateZoom( QRectF( QPointF( real1x, real1y ), QSizeF( real2x-real1x, real2y-real1y ) ) );
3635 }
3636
3637
animateZoom(const QRectF & _newCoords)3638 void View::animateZoom( const QRectF & _newCoords )
3639 {
3640 QRectF oldCoords( m_xmin, m_ymin, m_xmax-m_xmin, m_ymax-m_ymin );
3641 QRectF newCoords( _newCoords.normalized() );
3642
3643 if ( newCoords.left() == m_xmin &&
3644 newCoords.right() == m_xmax &&
3645 newCoords.top() == m_ymin &&
3646 newCoords.bottom() == m_ymax )
3647 return;
3648
3649 m_zoomMode = AnimatingZoom;
3650
3651 if ( style()->styleHint(QStyle::SH_Widget_Animate) && m_viewportAnimation->state() == QAbstractAnimation::Stopped )
3652 {
3653 m_viewportAnimation->setDuration( 150 );
3654 m_viewportAnimation->setEasingCurve( QEasingCurve::OutCubic );
3655 m_viewportAnimation->setStartValue( oldCoords );
3656 m_viewportAnimation->setEndValue( newCoords );
3657 m_viewportAnimation->start();
3658 connect(m_viewportAnimation, &QPropertyAnimation::finished, [this, newCoords]
3659 {
3660 finishAnimation( newCoords );
3661 });
3662 }
3663 else
3664 {
3665 finishAnimation( newCoords );
3666 }
3667 Settings::self()->save();
3668 }
3669
finishAnimation(const QRectF & rect)3670 void View::finishAnimation( const QRectF & rect )
3671 {
3672 m_xmin = rect.left();
3673 m_xmax = rect.right();
3674 m_ymin = rect.top();
3675 m_ymax = rect.bottom();
3676
3677 Settings::setXMin( Parser::number( m_xmin ) );
3678 Settings::setXMax( Parser::number( m_xmax ) );
3679 Settings::setYMin( Parser::number( m_ymin ) );
3680 Settings::setYMax( Parser::number( m_ymax ) );
3681 MainDlg::self()->coordsDialog()->updateXYRange();
3682 MainDlg::self()->requestSaveCurrentState();
3683
3684 drawPlot(); //update all graphs
3685
3686 m_zoomMode = Normal;
3687 }
3688
getViewport()3689 const QRectF View::getViewport()
3690 {
3691 return m_animateZoomRect;
3692 }
3693
setViewport(const QRectF & rect)3694 void View::setViewport( const QRectF & rect )
3695 {
3696 m_animateZoomRect = rect;
3697 repaint();
3698 }
3699
3700
translateView(int dx,int dy)3701 void View::translateView( int dx, int dy )
3702 {
3703 double rdx = xToReal( dx ) - xToReal( 0.0 );
3704 double rdy = yToReal( dy ) - yToReal( 0.0 );
3705
3706 m_xmin += rdx;
3707 m_xmax += rdx;
3708 m_ymin += rdy;
3709 m_ymax += rdy;
3710
3711 Settings::setXMin( Parser::number( m_xmin ) );
3712 Settings::setXMax( Parser::number( m_xmax ) );
3713 Settings::setYMin( Parser::number( m_ymin ) );
3714 Settings::setYMax( Parser::number( m_ymax ) );
3715 MainDlg::self()->coordsDialog()->updateXYRange();
3716
3717 drawPlot(); //update all graphs
3718 }
3719
3720
stopDrawing()3721 void View::stopDrawing()
3722 {
3723 if (m_isDrawing)
3724 m_stopCalculating = true;
3725 }
3726
3727
findMinMaxValue(const Plot & plot,ExtremaType type,double dmin,double dmax)3728 QPointF View::findMinMaxValue( const Plot & plot, ExtremaType type, double dmin, double dmax )
3729 {
3730 Function * ufkt = plot.function();
3731 assert( (ufkt->type() == Function::Cartesian) || (ufkt->type() == Function::Differential) );
3732 Q_UNUSED(ufkt);
3733
3734 plot.updateFunction();
3735
3736 Plot differentiated = plot;
3737 differentiated.differentiate();
3738 QList<double> roots = findRoots( differentiated, dmin, dmax, RoughRoot );
3739
3740 // The minimum / maximum might occur at the end points
3741 roots << dmin << dmax;
3742
3743 double best = (type == Maximum) ? -HUGE_VAL : +HUGE_VAL;
3744 QPointF bestPoint;
3745
3746 for ( double root : qAsConst(roots) )
3747 {
3748 QPointF rv = realValue( plot, root, false );
3749 if ( (type == Maximum && rv.y() > best) || (type == Minimum && rv.y() < best) )
3750 {
3751 best = rv.y();
3752 bestPoint = QPointF(rv.x(), rv.y());
3753 }
3754 }
3755
3756 return bestPoint;
3757 }
3758
3759
keyPressEvent(QKeyEvent * e)3760 void View::keyPressEvent( QKeyEvent * e )
3761 {
3762 // if a zoom operation is in progress, assume that the key press is to cancel it
3763 if ( m_zoomMode != Normal )
3764 {
3765 m_zoomMode = Normal;
3766 update();
3767 updateCursor();
3768 return;
3769 }
3770
3771 if (m_isDrawing)
3772 {
3773 m_stopCalculating=true;
3774 return;
3775 }
3776
3777 if ( m_currentPlot.functionID() == -1 )
3778 return;
3779
3780 QMouseEvent * event = 0;
3781 if (e->key() == Qt::Key_Left )
3782 event = new QMouseEvent( QEvent::MouseMove, m_crosshairPixelCoords.toPoint() - QPoint(1,1), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier );
3783 else if (e->key() == Qt::Key_Right )
3784 event = new QMouseEvent( QEvent::MouseMove, m_crosshairPixelCoords.toPoint() + QPoint(1,1), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier );
3785 else if (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down) //switch graph in trace mode
3786 {
3787 /// \todo reimplement moving between plots
3788 #if 0
3789 QMap<int, Function*>::iterator it = XParser::self()->m_ufkt.find( m_currentPlot.functionID );
3790 int const ke=(*it)->parameters.count();
3791 if (ke>0)
3792 {
3793 m_currentFunctionParameter++;
3794 if (m_currentFunctionParameter >= ke)
3795 m_currentFunctionParameter=0;
3796 }
3797 if (m_currentFunctionParameter==0)
3798 {
3799 int const old_m_currentPlot.functionID=m_currentPlot.functionID;
3800 Function::PMode const old_m_currentPlot.plotMode = m_currentPlot.plotMode;
3801 bool start = true;
3802 bool found = false;
3803 while ( 1 )
3804 {
3805 if ( old_m_currentPlot.functionID==m_currentPlot.functionID && !start)
3806 {
3807 m_currentPlot.plotMode=old_m_currentPlot.plotMode;
3808 break;
3809 }
3810 qDebug() << "m_currentPlot.functionID: " << m_currentPlot.functionID;
3811 switch ( (*it)->type() )
3812 {
3813 case Function::Parametric:
3814 case Function::Polar:
3815 break;
3816 default:
3817 {
3818 //going through the function, the first and the second derivative
3819 for ( m_currentPlot.plotMode = (Function::PMode)0; m_currentPlot.plotMode < 3; m_currentPlot.plotMode = (Function::PMode)(m_currentPlot.plotMode+1) )
3820 // for (m_currentPlot.plotMode=0;m_currentPlot.plotMode<3;m_currentPlot.plotMode++)
3821 {
3822 if (start)
3823 {
3824 if ( m_currentPlot.plotMode==Function::Derivative2)
3825 m_currentPlot.plotMode=Function::Derivative0;
3826 else
3827 m_currentPlot.plotMode = (Function::PMode)(old_m_currentPlot.plotMode+1);
3828 start=false;
3829 }
3830 qDebug() << " m_currentPlot.plotMode: " << (int)m_currentPlot.plotMode;
3831
3832 if ( (*it)->plotAppearance( m_currentPlot.plotMode ).visible )
3833 found = true;
3834
3835 if (found)
3836 break;
3837 }
3838 break;
3839 }
3840 }
3841 if (found)
3842 break;
3843
3844 if ( ++it == XParser::self()->m_ufkt.end())
3845 it = XParser::self()->m_ufkt.begin();
3846 m_currentPlot.functionID = (*it)->id();
3847 }
3848 }
3849
3850 qDebug() << "************************";
3851 qDebug() << "m_currentPlot.functionID: " << (int)m_currentPlot.functionID;
3852 qDebug() << "m_currentPlot.plotMode: " << (int)m_currentPlot.plotMode;
3853 qDebug() << "m_currentFunctionParameter: " << m_currentFunctionParameter;
3854
3855 setStatusBar( (*it)->prettyName( m_currentPlot.plotMode ), FunctionSection );
3856
3857 event = new QMouseEvent( QEvent::MouseMove, m_crosshairPixelCoords.toPoint(), Qt::LeftButton, Qt::LeftButton, 0 );
3858 #else
3859 return;
3860 #endif
3861 }
3862 else if ( e->key() == Qt::Key_Space )
3863 {
3864 event = new QMouseEvent( QEvent::MouseButtonPress, QCursor::pos(), Qt::RightButton, Qt::RightButton, Qt::NoModifier );
3865 mousePressEvent(event);
3866 delete event;
3867 return;
3868 }
3869 else
3870 {
3871 event = new QMouseEvent( QEvent::MouseButtonPress, m_crosshairPixelCoords.toPoint(), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier );
3872 mousePressEvent(event);
3873 delete event;
3874 return;
3875 }
3876 mouseMoveEvent(event);
3877 delete event;
3878 }
3879
3880
areaUnderGraph(IntegralDrawSettings s)3881 double View::areaUnderGraph( IntegralDrawSettings s )
3882 {
3883 int sign = 1;
3884 if ( s.dmax < s.dmin )
3885 {
3886 qSwap( s.dmin, s.dmax );
3887 sign = -1;
3888 }
3889
3890 else if ( s.dmax == s.dmin )
3891 return 0;
3892
3893 Function * ufkt = s.plot.function();
3894 assert( ufkt );
3895
3896 double dx = (s.dmax-s.dmin)/m_clipRect.width();
3897 if ( s.plot.plotMode == Function::Integral )
3898 {
3899 double max_dx = ufkt->eq[0]->differentialStates.step().value();
3900 if ( dx > max_dx )
3901 dx = max_dx;
3902 }
3903
3904 // Make sure that we calculate the exact area (instead of missing out a
3905 // vertical slither at the end) by making sure dx tiles the x-range
3906 // a whole number of times
3907 int intervals = qRound( (s.dmax-s.dmin)/dx );
3908 dx = (s.dmax-s.dmin) / intervals;
3909
3910 double calculated_area=0;
3911 double x = s.dmin;
3912
3913 s.plot.updateFunction();
3914
3915 for ( int i = 0; i <= intervals; ++i )
3916 {
3917 double y = value( s.plot, 0, x, false );
3918
3919 // Trapezoid rule for integrals: only add on half for the first and last value
3920 if ( (i == 0) || (i == intervals) )
3921 calculated_area += 0.5*dx*y;
3922 else
3923 calculated_area += dx*y;
3924
3925 x=x+dx;
3926 }
3927
3928 m_integralDrawSettings = s;
3929 m_integralDrawSettings.draw = true;
3930 drawPlot();
3931 m_integralDrawSettings.draw = false;
3932 return calculated_area * sign;
3933 }
3934
isCalculationStopped()3935 bool View::isCalculationStopped()
3936 {
3937 if ( m_stopCalculating)
3938 {
3939 m_stopCalculating = false;
3940 return true;
3941 }
3942 else
3943 return false;
3944 }
3945
updateSliders()3946 void View::updateSliders()
3947 {
3948 bool needSliderWindow = false;
3949 for ( Function * it : qAsConst(XParser::self()->m_ufkt) )
3950 {
3951 if ( it->m_parameters.useSlider && !it->allPlotsAreHidden() )
3952 {
3953 needSliderWindow = true;
3954 break;
3955 }
3956 }
3957
3958 if ( !needSliderWindow )
3959 {
3960 if ( m_sliderWindow )
3961 m_sliderWindow->hide();
3962 m_menuSliderAction->setChecked( false );
3963 return;
3964 }
3965
3966 if ( !m_sliderWindow )
3967 {
3968 m_sliderWindow = new KSliderWindow( this );
3969 connect( m_sliderWindow, &KSliderWindow::valueChanged, this, QOverload<>::of(&View::drawPlot) );
3970 connect( m_sliderWindow, &KSliderWindow::windowClosed, this, &View::sliderWindowClosed );
3971 connect( m_sliderWindow, &KSliderWindow::finished, this, &View::sliderWindowClosed );
3972 }
3973 if ( m_menuSliderAction->isChecked() )
3974 m_sliderWindow->show();
3975 }
3976
sliderWindowClosed()3977 void View::sliderWindowClosed()
3978 {
3979 m_menuSliderAction->setChecked( false ); //set the slider-item in the menu
3980 }
3981
functionRemoved(int id)3982 void View::functionRemoved( int id )
3983 {
3984 if ( id == m_currentPlot.functionID() )
3985 {
3986 m_currentPlot.setFunctionID( -1 );
3987 setStatusBar( QString(), RootSection );
3988 setStatusBar( QString(), FunctionSection );
3989 }
3990 }
3991
hideCurrentFunction()3992 void View::hideCurrentFunction()
3993 {
3994 if ( m_currentPlot.functionID() == -1 )
3995 return;
3996
3997 Function * ufkt = m_currentPlot.function();
3998 ufkt->plotAppearance( m_currentPlot.plotMode ).visible = false;
3999
4000 MainDlg::self()->functionEditor()->functionsChanged();
4001 drawPlot();
4002 MainDlg::self()->requestSaveCurrentState();
4003 updateSliders();
4004 if ( m_currentPlot.functionID() == -1 )
4005 return;
4006 if ( ufkt->allPlotsAreHidden() )
4007 {
4008 m_currentPlot.setFunctionID( -1 );
4009 QMouseEvent *event = new QMouseEvent( QMouseEvent::KeyPress, QCursor::pos(), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier );
4010 mousePressEvent(event); //leave trace mode
4011 delete event;
4012 return;
4013 }
4014 else
4015 {
4016 QKeyEvent *event = new QKeyEvent( QKeyEvent::KeyPress, Qt::Key_Up, Qt::NoModifier );
4017 keyPressEvent(event); //change selected graph
4018 delete event;
4019 return;
4020 }
4021 }
removeCurrentPlot()4022 void View::removeCurrentPlot()
4023 {
4024 if ( m_currentPlot.functionID() == -1 )
4025 return;
4026
4027 Function * ufkt = m_currentPlot.function();
4028 Function::Type function_type = ufkt->type();
4029 if (!XParser::self()->removeFunction( ufkt ))
4030 return;
4031
4032 if ( m_currentPlot.functionID() != -1 ) // if trace mode is enabled
4033 {
4034 m_currentPlot.setFunctionID( -1 );
4035 QMouseEvent *event = new QMouseEvent( QMouseEvent::KeyPress, QCursor::pos(), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier );
4036 mousePressEvent(event); //leave trace mode
4037 delete event;
4038 }
4039
4040 drawPlot();
4041 if ( function_type == Function::Cartesian )
4042 updateSliders();
4043 MainDlg::self()->requestSaveCurrentState();
4044 }
4045
4046
animateFunction()4047 void View::animateFunction()
4048 {
4049 Function * f = m_currentPlot.function();
4050 if ( !f )
4051 return;
4052
4053 ParameterAnimator * anim = new ParameterAnimator( this, f );
4054 anim->show();
4055 }
4056
4057
editCurrentPlot()4058 void View::editCurrentPlot()
4059 {
4060 MainDlg::self()->functionEditor()->setCurrentFunction( m_currentPlot.functionID() );
4061 }
4062
4063
zoomIn()4064 void View::zoomIn()
4065 {
4066 m_zoomMode = ZoomIn;
4067 updateCursor();
4068 }
4069
4070
zoomOut()4071 void View::zoomOut()
4072 {
4073 m_zoomMode = ZoomOut;
4074 updateCursor();
4075 }
4076
4077
zoomToTrigonometric()4078 void View::zoomToTrigonometric()
4079 {
4080 double rpau = XParser::self()->radiansPerAngleUnit();
4081 animateZoom( QRectF( -2*M_PI/rpau, -4.0, 4*M_PI/rpau, 8.0 ) );
4082 }
4083
4084
updateCursor()4085 void View::updateCursor()
4086 {
4087 Cursor newCursor = m_prevCursor;
4088
4089 if ( m_isDrawing && (m_zoomMode != Translating) )
4090 newCursor = CursorWait;
4091
4092 else switch (m_zoomMode)
4093 {
4094 case AnimatingZoom:
4095 newCursor = CursorArrow;
4096 break;
4097
4098 case Normal:
4099 if ( shouldShowCrosshairs() )
4100 {
4101 // Don't show any cursor if we're tracing a function or the crosshairs should be shown
4102 newCursor = CursorBlank;
4103 }
4104 else
4105 newCursor = CursorArrow;
4106 break;
4107
4108 case ZoomIn:
4109 case ZoomInDrawing:
4110 newCursor = CursorMagnify;
4111 break;
4112
4113 case ZoomOut:
4114 case ZoomOutDrawing:
4115 newCursor = CursorLessen;
4116 break;
4117
4118 case AboutToTranslate:
4119 case Translating:
4120 newCursor = CursorMove;
4121 break;
4122 }
4123
4124 if ( newCursor == m_prevCursor )
4125 return;
4126 m_prevCursor = newCursor;
4127
4128 switch ( newCursor )
4129 {
4130 case CursorWait:
4131 setCursor( Qt::WaitCursor );
4132 break;
4133 case CursorBlank:
4134 setCursor( Qt::BlankCursor );
4135 break;
4136 case CursorArrow:
4137 setCursor( Qt::ArrowCursor );
4138 break;
4139 case CursorCross:
4140 setCursor( Qt::CrossCursor );
4141 break;
4142 case CursorMagnify:
4143 setCursor( QCursor( QIcon::fromTheme( "zoom-in").pixmap(48), 22, 15 ) );
4144 break;
4145 case CursorLessen:
4146 setCursor( QCursor( QIcon::fromTheme( "zoom-out").pixmap(48), 22, 15 ) );
4147 break;
4148 case CursorMove:
4149 setCursor( Qt::SizeAllCursor );
4150
4151 }
4152 }
4153
4154
shouldShowCrosshairs() const4155 bool View::shouldShowCrosshairs() const
4156 {
4157 switch ( m_zoomMode )
4158 {
4159 case Normal:
4160 case ZoomIn:
4161 case ZoomOut:
4162 break;
4163
4164 case AnimatingZoom:
4165 case ZoomInDrawing:
4166 case ZoomOutDrawing:
4167 case AboutToTranslate:
4168 case Translating:
4169 return false;
4170 }
4171
4172 if ( m_popupMenuStatus != NoPopup )
4173 return false;
4174
4175 Function * it = m_currentPlot.function();
4176
4177 return ( underMouse() && (!it || crosshairPositionValid( it )) );
4178 }
4179
4180
event(QEvent * e)4181 bool View::event( QEvent * e )
4182 {
4183 if ( e->type() == QEvent::WindowDeactivate && m_isDrawing)
4184 {
4185 m_stopCalculating = true;
4186 return true;
4187 }
4188 return QWidget::event(e); //send the information further
4189 }
4190
4191
setStatusBar(const QString & t,StatusBarSection section)4192 void View::setStatusBar( const QString & t, StatusBarSection section )
4193 {
4194 QString text;
4195 if ( section == FunctionSection )
4196 text = ' ' + t + ' ';
4197 else
4198 text = t;
4199
4200 if ( m_readonly) //if KmPlot is shown as a KPart with e.g Konqueror, it is only possible to change the status bar in one way: to call setStatusBarText
4201 {
4202 m_statusBarText[ section ] = text;
4203
4204 QString text;
4205 for ( int i = 0; i < 4; ++i )
4206 {
4207 if ( m_statusBarText[i].isEmpty() )
4208 continue;
4209
4210 if ( !text.isEmpty() )
4211 text.append( " | " );
4212
4213 text.append( m_statusBarText[i] );
4214 }
4215
4216 emit setStatusBarText(text);
4217 }
4218 else
4219 {
4220 QDBusReply<void> reply = QDBusInterface( QDBusConnection::sessionBus().baseService(), "/kmplot", "org.kde.kmplot.KmPlot" ).call( QDBus::NoBlock, "setStatusBarText", text, (int)section );
4221 }
4222 }
4223
setPrintHeaderTable(bool status)4224 void View::setPrintHeaderTable( bool status )
4225 {
4226 m_printHeaderTable = status;
4227 }
4228
setPrintBackground(bool status)4229 void View::setPrintBackground( bool status )
4230 {
4231 m_printBackground = status;
4232 }
4233
setPrintWidth(double width)4234 void View::setPrintWidth( double width )
4235 {
4236 m_printWidth = width;
4237 }
4238
setPrintHeight(double height)4239 void View::setPrintHeight( double height )
4240 {
4241 m_printHeight = height;
4242 }
4243
getCrosshairPosition() const4244 QPointF View::getCrosshairPosition() const
4245 {
4246 return m_crosshairPosition;
4247 }
4248
4249 //END class View
4250
4251
4252
4253 //BEGIN class IntegralDrawSettings
IntegralDrawSettings()4254 IntegralDrawSettings::IntegralDrawSettings()
4255 {
4256 dmin = dmax = 0.0;
4257 draw = false;
4258 }
4259 //END class IntegralDrawSettings
4260