1 /*************************************************************************************
2  *  Copyright (C) 2011 by Aleix Pol <aleixpol@kde.org>                               *
3  *  Copyright (C) 2012-2013 by Percy Camilo T. Aucahuasi <percy.camilo.ta@gmail.com> *
4  *                                                                                   *
5  *  This program is free software; you can redistribute it and/or                    *
6  *  modify it under the terms of the GNU General Public License                      *
7  *  as published by the Free Software Foundation; either version 2                   *
8  *  of the License, or (at your option) any later version.                           *
9  *                                                                                   *
10  *  This program is distributed in the hope that it will be useful,                  *
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of                   *
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                    *
13  *  GNU General Public License for more details.                                     *
14  *                                                                                   *
15  *  You should have received a copy of the GNU General Public License                *
16  *  along with this program; if not, write to the Free Software                      *
17  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA   *
18  *************************************************************************************/
19 
20 #include "plotter2d.h"
21 
22 #include "planecurve.h"
23 
24 #include "plotsmodel.h"
25 #include "private/utils/mathutils.h"
26 
27 #include <cmath>
28 #include <QPalette>
29 #include <QPainter>
30 #include <QFontDatabase>
31 #include <QDebug>
32 #include <qnumeric.h>
33 
34 #if defined(HAVE_IEEEFP_H)
35 #include <ieeefp.h>
36 // bool qIsInf(double x) { return !finite(x) && x==x; }
37 #endif
38 
39 using namespace Analitza;
40 
41 // #define DEBUG_GRAPH
42 
43 class Analitza::Plotter2DPrivate : public QObject {
44 public:
Plotter2DPrivate(Plotter2D * q)45     Plotter2DPrivate(Plotter2D* q) : q(q) {}
46 
47     QAbstractItemModel* m_model = nullptr;
48     qreal m_dpr = 1.;
49     Plotter2D* const q;
50 
forceRepaint()51     void forceRepaint() { q->forceRepaint(); }
addFuncs(const QModelIndex & parent,int start,int end)52     void addFuncs(const QModelIndex& parent, int start, int end) { q->updateFunctions(parent, start, end); }
updateFuncs(const QModelIndex & start,const QModelIndex & end)53     void updateFuncs(const QModelIndex& start, const QModelIndex& end) { q->updateFunctions(QModelIndex(), start.row(), end.row()); }
54     void setModel(QAbstractItemModel* f);
55 };
56 
57 QColor const Plotter2D::m_axeColor(100,100,255); //TODO convert from const to param/attr and make setAxisColor(Qt::oriantation, qcolor)
58 QColor const Plotter2D::m_derivativeColor(90,90,160); //TODO remove derivative logic from plotter2d, move it to other module
59 QString const Plotter2D::PiSymbol(QChar(0x03C0));
60 QString const Plotter2D::DegreeSymbol(QChar(0x00B0));
61 QString const Plotter2D::GradianSymbol(QChar(0x1D4D));
62 double const Plotter2D::Pi6 = M_PI_2/3.0;
63 double const Plotter2D::Pi12 = Plotter2D::Pi6*0.5;
64 double const Plotter2D::Pi36 = Plotter2D::Pi6/6.0;
65 const double Plotter2D::ZoomInFactor = 0.97/2; // (regular mouse wheel forward value) / 2
66 const double Plotter2D::ZoomOutFactor = 2*1.03; // 2 x (regular mouse wheel backward value)
67 
68 struct Plotter2D::GridInfo
69 {
70     double inc, xini, yini, xend, yend;
71     int incLabelSkip;
72     // sub5 flag is used for draw sub 5 intervals instead of 4
73     int subinc; // true if inc=5*pow(10,n) (so, in this case, we draw 5 sub intervals, not 4)
74     // we need scale for ticks, and for labels, thicks can be minor, labels no: so
75     // this is necessary for minor ticks and for minir grid (but not for labels, so when draw labels we need )
76     int nxiniticks, nyiniticks, nxendticks, nyendticks; // nxini = floor(viewport.left()/inc), so xini is nxini*inc ... and so on
77     int nxinilabels, nyinilabels, nxendlabels, nyendlabels; // nxini = floor(viewport.left()/inc), so xini is nxini*inc ... and so on
78 };
79 
Plotter2D(const QSizeF & size)80 Plotter2D::Plotter2D(const QSizeF& size)
81     : m_showGrid(true)
82     , m_showMinorGrid(false)
83     , m_gridColor(QColor(Qt::lightGray).lighter(120))
84     , m_backgroundColor(Qt::white)
85     , m_autoGridStyle(true)
86     , m_gridStyleHint(Squares)
87     , rang_x(0)
88     , rang_y(0)
89     , m_keepRatio(true)
90     , m_dirty(true)
91     , m_size(size)
92     , d(new Plotter2DPrivate(this))
93     , m_angleMode(Radian)
94     , m_scaleMode(Linear)
95     , m_showTicks(Qt::Vertical|Qt::Horizontal)
96     , m_showTickLabels(Qt::Vertical|Qt::Horizontal)
97     , m_showMinorTicks(false)
98     , m_showAxes(Qt::Vertical|Qt::Horizontal)
99     , m_showPolarAxis(false)
100     , m_showPolarAngles(false)
101     , m_axisXLabel(QStringLiteral("x"))
102     , m_axisYLabel(QStringLiteral("y"))
103 {}
104 
~Plotter2D()105 Plotter2D::~Plotter2D()
106 {
107     delete d;
108 }
109 
setGridStyleHint(GridStyle suggestedgs)110 void Plotter2D::setGridStyleHint(GridStyle suggestedgs)
111 {
112     m_gridStyleHint = suggestedgs;
113 
114     forceRepaint();
115 }
116 
computeSubGridColor() const117 const QColor Plotter2D::computeSubGridColor() const
118 {
119     //impl. details: since any kde user can create any palette style, we need this hard/magic numbres
120     // because there is no way to guess, however, this code covers almost any case,
121     // it was tested with more then 35 color styles, and all give good results.
122 
123     QColor col = m_gridColor;
124 
125     if (m_backgroundColor.value() < 200)
126     {
127         if (m_gridColor.value() < 40)
128             col.setHsv(col.hsvHue(), col.hsvSaturation(), m_gridColor.value()-15);
129         else
130             col.setHsv(col.hsvHue(), col.hsvSaturation(), m_gridColor.value()>=255?120:m_gridColor.value()-10);
131     }
132     else // e.g: background color white and grid color black
133         if (m_backgroundColor.value() < 245)
134             col.setHsv(col.hsvHue(), col.hsvSaturation(), m_backgroundColor.value()-(m_backgroundColor.value()-200)/3);
135         else
136             col.setHsv(col.hsvHue(), col.hsvSaturation(), m_backgroundColor.value()-(m_backgroundColor.value()-200)/8);
137 
138     return col;
139 }
140 
getGridInfo() const141 const Plotter2D::GridInfo Plotter2D::getGridInfo() const
142 {
143     GridInfo ret;
144 
145     if (m_scaleMode == Linear) {
146         const double val = log10(qMax(viewport.width(), -viewport.height()));
147         const double diff = val-floor(val);
148         const double magnitude = pow(10, floor(val)-1);
149 
150         ret.inc = magnitude;
151         ret.incLabelSkip = diff < 0.5 ? 1 : 3;
152     } else {
153         ret.inc = M_PI;
154         ret.incLabelSkip = 1;
155     }
156 
157     ret.subinc = 4;
158 
159     ret.nxinilabels = std::floor(viewport.left()/ret.inc);
160     ret.nyinilabels = std::floor(viewport.bottom()/ret.inc);
161     ret.nxendlabels = std::ceil(viewport.right()/ret.inc);
162     ret.nyendlabels = std::ceil(viewport.top()/ret.inc);
163 
164     ret.xini = ret.nxinilabels*ret.inc;
165     ret.yini = ret.nyinilabels*ret.inc;
166     ret.xend = ret.nxendlabels*ret.inc;
167     ret.yend = ret.nyendlabels*ret.inc;
168 
169     const bool drawminor = m_showMinorGrid || m_showMinorTicks;
170     const double nfactor = drawminor ? ret.subinc : 1;
171 
172     ret.nxiniticks = nfactor*ret.nxinilabels;
173     ret.nyiniticks = nfactor*ret.nyinilabels;
174     ret.nxendticks = nfactor*ret.nxendlabels;
175     ret.nyendticks = nfactor*ret.nyendlabels;
176 
177     return ret;
178 }
179 
drawGrid(QPaintDevice * qpd)180 void Plotter2D::drawGrid(QPaintDevice *qpd)
181 {
182     QPainter p;
183     p.begin(qpd);
184 
185     int current=currentFunction();
186     PlotItem* plot = itemAt(current);
187 
188     GridStyle t = Squares; // default for Cartesian coord. sys.
189 
190     if (plot && plot->coordinateSystem() == Polar)
191         t = Circles;
192 
193     if (!m_autoGridStyle)
194         t = m_gridStyleHint;
195 
196     drawAxes(&p, t);
197 }
198 
drawAxes(QPainter * painter,GridStyle gridStyle) const199 void Plotter2D::drawAxes(QPainter* painter, GridStyle gridStyle) const
200 {
201     GridInfo grid = getGridInfo();
202 
203     switch (gridStyle)
204     {
205         case Circles: drawCircles(painter, grid, gridStyle); break;
206         default: drawSquares(painter, grid, gridStyle); break;
207     }
208 
209     drawMainAxes(painter);
210     //NOTE always draw the ticks at the end: avoid the grid lines overlap the ticks text
211     drawGridTickLabels(painter, grid, gridStyle);
212 }
213 
drawMainAxes(QPainter * painter) const214 void Plotter2D::drawMainAxes(QPainter* painter) const
215 {
216     const QFontMetrics fm(painter->font());
217     const QPen axesPen(m_axeColor, 1, Qt::SolidLine);
218     const QPointF center = toWidget(QPointF(0.,0.));
219 
220     painter->setPen(axesPen);
221     painter->setBrush(axesPen.color());
222 
223     const int startAngleX = 150*16;
224     const int startAngleY = 240*16;
225     const int spanAngle = 60*16;
226     const QPointF Xright(this->width(), center.y());
227     const QPointF Ytop(center.x(), 0.);
228     const double arrowWidth=15., arrowHeight=4.;
229     const QPointF dpx(arrowWidth, arrowHeight);
230     const QPointF dpy(arrowHeight, arrowWidth);
231     const QRectF rectX(Xright+dpx, Xright-dpx);
232     const QRectF rectY(Ytop+dpy, Ytop-dpy);
233 
234     if (m_showAxes&Qt::Horizontal)
235     {
236         painter->drawLine(QPointF(0., center.y()), Xright);
237 
238         painter->setRenderHint(QPainter::Antialiasing, true);
239         painter->drawPie(rectX, startAngleX, spanAngle);
240     }
241 
242     if (m_showAxes&Qt::Vertical)
243     {
244         painter->drawLine(Ytop, QPointF(center.x(), this->height()));
245 
246         painter->setRenderHint(QPainter::Antialiasing, true);
247         painter->drawPie(rectY, startAngleY, spanAngle);
248     }
249 
250     painter->setRenderHint(QPainter::Antialiasing, true);
251     // axis labels
252     QFont labelsfont = painter->font();
253     labelsfont.setBold(true);
254 
255     painter->drawText(Xright.x() - fm.boundingRect(m_axisXLabel).width() - 5, center.y() - 20, m_axisXLabel);
256     painter->drawText(center.x() + 5, Ytop.y() + fm.height() + 15, m_axisYLabel);
257 
258     //write coords
259     QString rightBound=QString::number(viewport.right());
260     int width=painter->fontMetrics().boundingRect(rightBound).width();
261 
262     painter->drawText(QPointF(3.+this->width()/2., 13.                 ), QString::number(viewport.top()));
263     painter->drawText(QPointF(3.+this->width()/2., this->height()-5.   ), QString::number(viewport.bottom()));
264     painter->drawText(QPointF(8.                 , this->height()/2.-5.), QString::number(viewport.left()));
265     painter->drawText(QPointF(this->width()-width, this->height()/2.-5.), rightBound);
266     //EO write coords
267 }
268 
computeAngleLabelByFrac(unsigned int n,unsigned int d) const269 const QString Plotter2D::computeAngleLabelByFrac(unsigned int n, unsigned int d) const
270 {
271     QString s;
272 
273     switch (m_angleMode)
274     {
275         case Radian:
276         {
277             s = (n==1) ? QString() : QString::number(n);
278             s += PiSymbol;
279             s += (d==1) ? QString() : '/'+QString::number(d);
280         }
281         break;
282         case Degree: s = QString::number(radiansToDegrees(n*M_PI/d))+DegreeSymbol; break;
283         case Gradian:  s = QString::number(radiansToGradians(n*M_PI/d))+GradianSymbol; break;
284     }
285 
286     return s;
287 }
288 
computeAngleLabelByStep(unsigned int k,unsigned int step) const289 const QString Plotter2D::computeAngleLabelByStep(unsigned int k, unsigned int step) const
290 {
291     QString s;
292 
293     switch (m_angleMode)
294     {
295         case Radian:
296         {
297             s = (k==1) ? ((step==1) ? QLatin1String("") : QString::number(step)) : QString::number(k*step);
298             s += PiSymbol;
299         }
300         break;
301         case Degree: s = QString::number(radiansToDegrees(k*step*M_PI))+DegreeSymbol; break;
302         case Gradian:  s = QString::number(radiansToGradians(k*step*M_PI))+GradianSymbol; break;
303     }
304 
305     return s;
306 }
307 
drawCartesianTickLabels(QPainter * painter,const Plotter2D::GridInfo & gridinfo,CartesianAxis axis) const308 void Plotter2D::drawCartesianTickLabels(QPainter* painter, const Plotter2D::GridInfo& gridinfo, CartesianAxis axis) const
309 {
310     Q_ASSERT(axis == XAxis || axis == Analitza::YAxis);
311 
312     const bool isxaxis = (axis == XAxis);
313     const double fontHeight = painter->fontMetrics().height();
314 
315     const bool incbig = (M_PI <= gridinfo.inc);
316     const unsigned int bigstep = std::floor(gridinfo.inc/M_PI);
317     const unsigned int step = std::ceil(M_PI/gridinfo.inc);
318 
319     QString s;
320 
321     const int from = (axis == Analitza::XAxis) ? gridinfo.nxinilabels : gridinfo.nyinilabels;
322     const int to = (axis == Analitza::XAxis) ? gridinfo.nxendlabels : gridinfo.nyendlabels;
323 
324     painter->save();
325     QFont tickFont = QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont);
326     painter->setFont(tickFont);
327     painter->setPen(QPen(QPalette().text().color()));
328     for (int i = from; i <= to; ++i)
329     {
330         if (i == 0 || i%gridinfo.incLabelSkip!=0) continue;
331 
332         double newval = i*gridinfo.inc;
333 
334         const QPointF p = isxaxis
335             ? toWidget(QPointF(newval, 0.0))
336             : toWidget(QPointF(0.0, newval));
337 
338         switch (m_scaleMode)
339         {
340             case Linear:
341                 s = QString::number(newval);
342                 break;
343             case Trigonometric:
344             {
345                 s = (i < 0) ? QString(QLatin1Char('-')) : QString();
346 
347                 if (incbig)
348                     s += computeAngleLabelByStep(qAbs(i), bigstep);
349                 else
350                 {
351                     const QPair<unsigned int, unsigned int> frac = simplifyFraction(qAbs(i), step);
352 
353                     s += computeAngleLabelByFrac(frac.first, frac.second);
354                 }
355             }
356             break;
357         }
358 
359         const int swidth = painter->fontMetrics().boundingRect(s).width();
360         if (isxaxis)
361             painter->drawText(p.x() - swidth/2, p.y()+fontHeight, s);
362         else
363             painter->drawText(p.x() - swidth - fontHeight/2, p.y()+fontHeight/2, s);
364     }
365     painter->restore();
366 }
367 
drawPolarTickLabels(QPainter * painter,const Plotter2D::GridInfo & gridinfo) const368 void Plotter2D::drawPolarTickLabels(QPainter* painter, const Plotter2D::GridInfo& gridinfo) const
369 {
370     unsigned int k = 1;
371 
372     painter->setPen(m_gridColor);
373 
374     //TODO if minor
375     const double newinc = gridinfo.inc/(gridinfo.subinc); // inc with sub intervals
376 
377     //x
378     // we assume 0 belongs to interval [gridinfo.xini, gridinfo.xend]
379     double maxh = qMax(std::abs(gridinfo.xini), std::abs(gridinfo.xend));
380     int i = std::ceil(maxh/newinc)/2;
381     double x = i*newinc;
382 
383     if (std::abs(gridinfo.xend) <= std::abs(gridinfo.xini))
384         x *= -1.0;
385 
386     // if 0 doesn't belongs to interval [gridinfo.xini, gridinfo.xend]
387     if (((gridinfo.xend < 0.0) && (gridinfo.xini < 0.0)) ||
388         ((gridinfo.xend > 0.0) && (gridinfo.xini > 0.0)))
389     {
390         maxh = gridinfo.xend - gridinfo.xini;
391         i = std::ceil(maxh/newinc)/2;
392 
393         x = gridinfo.xini + i*newinc;
394     }
395 
396     //y
397     // we assume 0 belongs to interval [gridinfo.yini, gridinfo.yend]
398     maxh = qMax(std::abs(gridinfo.yini), std::abs(gridinfo.yend));
399     i = std::ceil(maxh/newinc)/2;
400     double y = i*newinc;
401 
402     if (std::abs(gridinfo.yend) <= std::abs(gridinfo.yini))
403         y *= -1.0;
404 
405     // if 0 doesn't belongs to interval [gridinfo.xini, gridinfo.xend]
406     if (((gridinfo.yend < 0.0) && (gridinfo.yini < 0.0)) ||
407         ((gridinfo.yend > 0.0) && (gridinfo.yini > 0.0)))
408     {
409         maxh = gridinfo.yend - gridinfo.yini;
410         i = std::ceil(maxh/newinc)/2;
411 
412         y = gridinfo.yini + i*newinc;
413     }
414 
415     const double r = qMax(std::abs(x), std::abs(y)); // radius
416     const unsigned short axisxseparation = 16; // distance between tick text and x-axis
417     const float axispolarseparation = axisxseparation*0.5;
418 
419     double h = Pi6; // pi/6, then 12 rounds
420     double t = 0.0;
421 
422     unsigned int turns = 12; // 12 turns in [0, 2pi], since h=pi/6
423 
424     // if we are far from origin then sudivide angles by pi/12
425     if (!viewport.contains(QPointF(0.0, 0.0)))
426     {
427         h = Pi12;
428         turns = 24; // 24 turns in [0, 2pi], since h=pi/12
429     }
430 
431     k = 0;
432 
433     const unsigned int drawoverxaxiscount = turns/2;
434     const unsigned int drawnextyaxiscount = turns/4;
435     const unsigned int halfturns = drawoverxaxiscount; // or turns/2;
436 
437     for (uint j = 0; j < turns; ++j, ++k, t += h) // Pi6 then 24 turns
438     {
439         const QPair<unsigned int, unsigned int> frac = simplifyFraction(k, halfturns);
440 
441         QString s = (k != 0) ? computeAngleLabelByFrac(frac.first, frac.second) : QStringLiteral("0");
442         QPointF p(r*std::cos(t), r*std::sin(t));
443 
444         if (viewport.contains(p))
445         {
446 
447             if (k % drawoverxaxiscount == 0) // draw 0, pi over x
448                 painter->drawText(toWidget(p)+QPointF(0.0, -axispolarseparation), s);
449             else
450                 if (k % drawnextyaxiscount == 0) // draw pi/2, 3pi/2 next to y
451                     painter->drawText(toWidget(p)+QPointF(axispolarseparation, 0.0), s);
452                 else
453                     painter->drawText(toWidget(p), s);
454         }
455     }
456 }
457 
drawGridTickLabels(QPainter * painter,const GridInfo & gridinfo,GridStyle gridStyle) const458 void Plotter2D::drawGridTickLabels(QPainter* painter, const GridInfo& gridinfo, GridStyle gridStyle) const
459 {
460     if (m_showTickLabels & Qt::Horizontal)
461         drawCartesianTickLabels(painter, gridinfo, XAxis);
462 
463     if (m_showTickLabels & Qt::Vertical)
464         drawCartesianTickLabels(painter, gridinfo, YAxis);
465 
466     if ((gridStyle == Circles) && m_showPolarAngles && m_showGrid) // draw labels for angles (polar axis)
467         drawPolarTickLabels(painter, gridinfo);
468 }
469 
drawCircles(QPainter * painter,const GridInfo & gridinfo,GridStyle gridStyle) const470 void Plotter2D::drawCircles(QPainter* painter, const GridInfo& gridinfo, GridStyle gridStyle) const
471 {
472     Q_ASSERT(gridStyle == Analitza::Circles);
473     painter->setRenderHint(QPainter::Antialiasing, false);
474 
475     const QPen textPen = QPen(QPalette().text().color());
476     const QPen gridPen(m_gridColor);
477     const QPen subGridPen(computeSubGridColor());
478 
479     const unsigned short nsubinc = gridinfo.subinc; // count for draw sub intervals
480     const bool drawminor = m_showMinorGrid || m_showMinorTicks;
481     const double inc = drawminor ? gridinfo.inc/nsubinc : gridinfo.inc; // if show minor, then inc with sub intervals
482 
483     if (m_showGrid)
484     {
485         const qreal until = qMax(qMax(qAbs(viewport.left()), qAbs(viewport.right())), qMax(qAbs(viewport.top()), qAbs(viewport.bottom())))*M_SQRT2;
486 
487         int k = 1;
488 
489         painter->setPen(gridPen);
490 
491         for (double i = inc; i < until; i += inc, ++k)
492         {
493             if (i == 0.0) continue;
494 
495             QPointF p(toWidget(QPointF(i,i)));
496             QPointF p2(toWidget(QPointF(-i,-i)));
497             QRectF er(p.x(),p.y(), p2.x()-p.x(), p2.y()-p.y());
498 
499             if ((k % nsubinc == 0) || !drawminor) // intervals
500             {
501                 painter->setPen(gridPen);
502                 painter->drawEllipse(er);
503             }
504             else if (m_showMinorGrid)// sub intervals
505             {
506                 painter->setPen(subGridPen);
507                 painter->drawEllipse(er);
508             }
509         }
510 
511         if (m_showPolarAxis) // draw polar rays
512         {
513             double h = Pi12; // 2pi/(Pi/12) = 24 steps/turns
514             const double r = std::abs(until); // radius
515             const QPointF origin = toWidget(QPointF(0,0));
516 
517             int k = 1;
518             unsigned int alternatesubgridcount = 2;
519             unsigned int dontdrawataxiscount = 5;
520             unsigned int steps = 24; // or turns
521 
522             // if we are far from origin then sudivide angles by 5 degrees
523             if (!viewport.contains(QPointF(0.0, 0.0)))
524             {
525                 h = Pi36; // 2pi/(Pi/36) = 72 steps
526                 steps = 72; // or turns
527                 alternatesubgridcount = 3;
528                 dontdrawataxiscount = 17;
529             }
530 
531             double t = h;
532             for (uint j = 1; j <= steps; ++j, ++k, t += h)
533             {
534                 if (j % alternatesubgridcount == 0)
535                     painter->setPen(gridPen);
536                 else if (m_showMinorGrid)
537                 {
538                     painter->setPen(subGridPen);
539 
540                     QPointF p = toWidget(QPointF(r*std::cos(t), r*std::sin(t)));
541                     painter->drawLine(origin, p);
542 
543                     if (k % dontdrawataxiscount == 0) // avoid draw the ray at 0,pi/2,pi,3pi/2
544                     {
545                         t += h;
546                         ++j;
547                     }
548                 }
549             }
550         }
551     }
552 
553     //BEGIN draw ticks
554     if (m_showTickLabels & Qt::Horizontal)
555     {
556         painter->setPen(textPen);
557         const QPointF yoffset(0.,-3.);
558 
559         for (int j = gridinfo.nxiniticks, i = 0; j < gridinfo.nxendticks; ++j, ++i)
560         {
561             if (j == 0) continue;
562 
563             const double x = j*inc;
564             const QPointF p = toWidget(QPointF(x, 0.));
565 
566             if ((i % nsubinc == 0) || !drawminor || m_showMinorTicks)
567                 painter->drawLine(p, p+yoffset);
568         }
569     }
570 
571     if (m_showTickLabels & Qt::Vertical)
572     {
573         painter->setPen(textPen);
574         const QPointF xoffset(3.,0.);
575 
576         for (int j = gridinfo.nyiniticks, i = 0; j < gridinfo.nyendticks; ++j, ++i)
577         {
578             if (j == 0) continue;
579 
580             const double y = j*inc;
581 
582             const QPointF p = toWidget(QPointF(0., y));
583 
584             if ((i % nsubinc == 0) || !(m_showMinorGrid || m_showMinorTicks) || m_showMinorTicks)
585                 painter->drawLine(p, p+xoffset);
586         }
587     }
588     //END draw ticks
589 }
590 
drawSquares(QPainter * painter,const GridInfo & gridinfo,GridStyle gridStyle) const591 void Plotter2D::drawSquares(QPainter* painter, const GridInfo& gridinfo, GridStyle gridStyle) const
592 {
593     painter->setRenderHint(QPainter::Antialiasing, false);
594 
595     const QPen gridPen(m_gridColor);
596     const QPen subGridPen(computeSubGridColor());
597     const QPen gridPenBold(gridPen.brush(), 2);
598     const QPen subGridPenBold(subGridPen.brush(), gridPenBold.widthF());
599 
600     const unsigned short nsubinc = gridinfo.subinc; // count for draw sub intervals
601     const bool drawminor = m_showMinorGrid || m_showMinorTicks;
602     const double inc = drawminor? gridinfo.inc/nsubinc : gridinfo.inc; // if show minor, then inc with sub intervals
603 
604     for (int n = gridinfo.nxiniticks, i = 0; n < gridinfo.nxendticks; ++n, ++i)
605     {
606         if (n == 0) continue;
607 
608         const double x = n*inc;
609         const QPointF p = toWidget(QPointF(x, 0.));
610 
611         if (m_showGrid && gridStyle == Crosses)
612         {
613             const double crossside = 5;
614             const double centx = p.x();
615 
616             for (int j = gridinfo.nyiniticks, k = 0; j < gridinfo.nyendticks; ++j, ++k)
617             {
618                 if (j == 0) continue;
619 
620                 const double y = j*inc;
621                 const QPointF py = toWidget(QPointF(0., y));
622                 const double centy = py.y();
623 
624                 if (((i % nsubinc == 0) && (k % nsubinc == 0)) || !drawminor) // intervals
625                 {
626                     painter->setPen(gridPenBold);
627                     painter->drawLine(QPointF(centx-crossside, centy), QPointF(centx+crossside, centy)); // horizontal
628                     painter->drawLine(QPointF(centx, centy-crossside), QPointF(centx, centy+crossside)); // vertical
629                 }
630                 else if (m_showMinorGrid)// sub intervals
631                 {
632                     painter->setPen(subGridPenBold);
633                     painter->drawLine(QPointF(centx-crossside, centy), QPointF(centx+crossside, centy)); // horizontal
634                     painter->drawLine(QPointF(centx, centy-crossside), QPointF(centx, centy+crossside)); // vertical
635                 }
636             }
637         }
638 
639         if ((i % nsubinc == 0) || !drawminor) // intervals
640         {
641             if (m_showGrid && (gridStyle == Squares || gridStyle == VerticalLines))
642             {
643                 painter->setPen(gridPen);
644                 painter->drawLine(QPointF(p.x(), height()), QPointF(p.x(), 0.));
645             }
646 
647             if ((m_showTicks & Qt::Horizontal))
648             {
649                 painter->setPen(QPen(QPalette().text().color()));
650                 painter->drawLine(p, p+QPointF(0.,-3.));
651             }
652         }
653         else // sub intervals
654         {
655             if (m_showGrid && m_showMinorGrid && (gridStyle == Squares || gridStyle == VerticalLines))
656             {
657                 painter->setPen(subGridPen);
658                 painter->drawLine(QPointF(p.x(), height()), QPointF(p.x(), 0.));
659             }
660 
661             if ((m_showTicks & Qt::Horizontal) && m_showMinorTicks)
662             {
663                 painter->setPen(QPen(QPalette().text().color()));
664                 painter->drawLine(p, p+QPointF(0.,-3.));
665             }
666         }
667     }
668 
669     for (int j = gridinfo.nyiniticks, i = 0; j < gridinfo.nyendticks; ++j, ++i)
670     {
671         if (j == 0) continue;
672 
673         const double y = j*inc;
674         const QPointF p = toWidget(QPointF(0., y));
675 
676         if ((i % nsubinc == 0) || !drawminor) // intervals
677         {
678             if (m_showGrid && (gridStyle == Squares || gridStyle == HorizontalLines))
679             {
680                 painter->setPen(gridPen);
681                 painter->drawLine(QPointF(0., p.y()), QPointF(width(), p.y()));
682             }
683 
684             if ((m_showTicks & Qt::Horizontal))
685             {
686                 painter->setPen(QPen(QPalette().text().color()));
687                 painter->drawLine(p, p+QPointF(3.,0.));
688             }
689         }
690         else // sub intervals
691         {
692             if (m_showGrid && m_showMinorGrid && (gridStyle == Squares || gridStyle == HorizontalLines))
693             {
694                 painter->setPen(subGridPen);
695                 painter->drawLine(QPointF(0., p.y()), QPointF(width(), p.y()));
696             }
697 
698             if ((m_showTicks & Qt::Horizontal) && m_showMinorTicks)
699             {
700                 painter->setPen(QPen(QPalette().text().color()));
701                 painter->drawLine(p, p+QPointF(3.,0.));
702             }
703         }
704     }
705 }
706 
itemAt(int row) const707 PlotItem* Plotter2D::itemAt(int row) const
708 {
709     if (!d->m_model)
710         return nullptr;
711 
712     QModelIndex pi = d->m_model->index(row, 0);
713 
714     if (!pi.isValid())
715         return nullptr;
716 
717     PlotItem* plot = pi.data(PlotsModel::PlotRole).value<PlotItem*>();
718 
719     if (plot->spaceDimension() != Dim2D)
720         return nullptr;
721 
722     return plot;
723 }
724 
drawFunctions(QPaintDevice * qpd)725 void Plotter2D::drawFunctions(QPaintDevice *qpd)
726 {
727     drawGrid(qpd);
728     QPen pfunc(QColor(0,150,0), 2);
729 
730     QPainter p;
731     p.begin(qpd);
732     p.setPen(pfunc);
733 
734     if (!d->m_model || m_dirty)
735         return;
736 
737     p.setRenderHint(QPainter::Antialiasing, true);
738 
739     const int current=currentFunction();
740     const int dpr = qMax<int>(1, qRound(qpd->logicalDpiX()/100.)) << 1;
741 
742     for (int k = 0; k < d->m_model->rowCount(); ++k )
743     {
744         PlaneCurve* curve = dynamic_cast<PlaneCurve *>(itemAt(k));
745 
746         //NOTE from GSOC: not all valid plots always has points (so, we need to check if points().isEmpty() too)
747         if (!curve || !curve->isVisible() || curve->points().isEmpty())
748             continue;
749 
750         pfunc.setColor(curve->color());
751         pfunc.setWidth((k==current)+dpr);
752         pfunc.setStyle(Qt::SolidLine);
753         p.setPen(pfunc);
754 
755         const QVector<QPointF> &vect=curve->points();
756         QVector<int> jumps=curve->jumps();
757 
758         unsigned int pointsCount = vect.count();
759         QPointF ultim = toWidget(vect.at(0));
760 
761         int nextjump;
762         nextjump = jumps.isEmpty() ? -1 : jumps.first();
763         if (!jumps.isEmpty()) jumps.remove(0);
764 // #define DEBUG_GRAPH 1
765 #ifdef DEBUG_GRAPH
766         qDebug() << "---------" << jumps.count()+1;
767 #endif
768         for(unsigned int j=0; j<pointsCount; ++j)
769         {
770             const QPointF act = toWidget(vect.at(j));
771 
772 //          qDebug() << "xxxxx" << act << ultim << qIsNaN(act.y()) << qIsNaN(ultim.y());
773             if(qIsInf(act.y()) && !qIsNaN(act.y())) qDebug() << "trying to plot from a NaN value" << act << ultim;
774             else if(qIsInf(act.y()) && qIsNaN(act.y())) qDebug() << "trying to plot to a NaN value";
775 
776             const bool bothinf=(qIsInf(ultim.y()) && qIsInf(act.y())) || (qIsInf(ultim.x()) && qIsInf(act.x()));
777             if(!bothinf && !qIsNaN(act.y()) && !qIsNaN(ultim.y()) && nextjump!=int(j)) {
778                 if(qIsInf(ultim.y())) {
779                     if(act.y()<0) ultim.setY(0);
780                     if(act.y()>0) ultim.setY(qpd->height());
781                 }
782 //
783                 QPointF act2(act);
784                 if(qIsInf(act2.y())) {
785                     if(ultim.y()<0) act2.setY(0);
786                     if(ultim.y()>0) act2.setY(qpd->height());
787                 }
788 
789 //              qDebug() << "xxxxx" << act2 << ultim << qIsNaN(act2.y()) << qIsNaN(ultim.y());
790 
791                 p.drawLine(ultim, act2);
792 
793 #ifdef DEBUG_GRAPH
794                 QPen dpen(Qt::red);
795                 dpen.setWidth(3);
796                 p.setPen(dpen);
797                 p.drawPoint(ultim);
798                 p.setPen(pfunc);
799 #endif
800             } else if(nextjump==int(j)) {
801                 do {
802                     if(nextjump!=int(j))
803                         p.drawPoint(act);
804 
805                     nextjump = jumps.isEmpty() ? -1 : jumps.first();
806                     if (!jumps.isEmpty()) jumps.remove(0);
807 
808                 } while(!jumps.isEmpty() && jumps.first()==nextjump+1);
809 
810 #ifdef DEBUG_GRAPH
811                 qDebug() << "jumpiiiiiing" << ultim << toWidget(vect.at(j));
812                 QPen dpen(Qt::blue);
813                 dpen.setWidth(2);
814                 p.setPen(dpen);
815                 p.drawLine(QLineF(QPointF(act.x(), height()/2-10), QPointF(act.x(), height()/2+10)));
816                 p.setPen(pfunc);
817 #endif
818             }
819 
820             ultim=act;
821         }
822     }
823 
824     p.end();
825 }
826 
updateFunctions(const QModelIndex & parent,int start,int end)827 void Plotter2D::updateFunctions(const QModelIndex & parent, int start, int end)
828 {
829     if (!d->m_model || parent.isValid())
830         return;
831 
832     QRectF viewportFixed = viewport;
833     viewportFixed.setTopLeft(viewport.bottomLeft());
834     viewportFixed.setHeight(std::fabs(viewport.height()));
835 
836     for(int i=start; i<=end; i++)
837     {
838         PlaneCurve* curve = dynamic_cast<PlaneCurve *>(itemAt(i));
839 
840         if (!curve || !curve->isVisible())
841             continue;
842 
843         curve->update(viewportFixed);
844     }
845 
846     m_dirty = false; // m_dirty = false means that the we will not recalculate functions points
847 
848     forceRepaint();
849 }
850 
calcImage(const QPointF & ndp) const851 QPair<QPointF, QString> Plotter2D::calcImage(const QPointF& ndp) const
852 {
853     if (!d->m_model || currentFunction() == -1)
854         return QPair<QPointF, QString>();
855 
856     PlaneCurve* curve = dynamic_cast<PlaneCurve*>(itemAt(currentFunction()));
857 
858     if (curve && curve->isVisible())
859         return curve->image(ndp);
860 
861     return QPair<QPointF, QString>();
862 }
863 
normalizeUserViewport(const QRectF & uvp)864 QRectF Plotter2D::normalizeUserViewport(const QRectF &uvp)
865 {
866     QRectF normalizeduvp = uvp;
867     rang_x = width()/normalizeduvp.width();
868     rang_y = height()/normalizeduvp.height();
869 
870     if (m_keepRatio && rang_x != rang_y)
871     {
872         rang_y=rang_x=qMin(std::fabs(rang_x), std::fabs(rang_y));
873 
874         if(rang_y>0.) rang_y=-rang_y;
875         if(rang_x<0.) rang_x=-rang_x;
876 
877         double newW=width()/rang_x, newH=height()/rang_x;
878 
879         double mx=(uvp.width()-newW)/2.;
880         double my=(uvp.height()-newH)/2.;
881 
882         normalizeduvp.setLeft(uvp.left()+mx);
883         normalizeduvp.setTop(uvp.bottom()-my);
884         normalizeduvp.setWidth(newW);
885         normalizeduvp.setHeight(-newH); //WARNING why negative distance?
886 
887         //Commented because precision could make the program crash
888 //      Q_ASSERT(uvp.center() == viewport.center());
889     }
890 
891     return normalizeduvp;
892 }
893 
updateScale(bool repaint)894 void Plotter2D::updateScale(bool repaint)
895 {
896     viewport = normalizeUserViewport(userViewport);
897 
898     if (repaint) {
899         if (d->m_model && d->m_model->rowCount()>0)
900             updateFunctions(QModelIndex(), 0, d->m_model->rowCount()-1);
901         else
902             forceRepaint();
903     }
904 }
905 
setViewport(const QRectF & vp,bool repaint)906 void Plotter2D::setViewport(const QRectF& vp, bool repaint)
907 {
908     userViewport = vp;
909 
910     Q_ASSERT(userViewport.top()>userViewport.bottom());
911     Q_ASSERT(userViewport.right()>userViewport.left());
912 
913     updateScale(repaint);
914 
915     viewportChanged();
916 }
917 
slope(const QPointF & dp) const918 QLineF Plotter2D::slope(const QPointF& dp) const
919 {
920     if (!d->m_model || currentFunction() == -1)
921         return QLineF();
922 
923     PlaneCurve* plot = dynamic_cast<PlaneCurve*>(itemAt(currentFunction()));
924 
925     if (plot && plot->isVisible())
926     {
927         QLineF ret = plot->tangent(dp);
928 
929         if (ret.isNull() && currentFunction()>=0)
930         {
931             QPointF a = calcImage(dp-QPointF(.1,.1)).first;
932             QPointF b = calcImage(dp+QPointF(.1,.1)).first;
933 
934             ret = slopeToLine((a.y()-b.y())/(a.x()-b.x()));
935         }
936 
937         return ret;
938     }
939 
940     return QLineF();
941 }
942 
toWidget(const QLineF & f) const943 QLineF Plotter2D::toWidget(const QLineF &f) const
944 {
945     return QLineF(toWidget(f.p1()), toWidget(f.p2()));
946 }
947 
toWidget(const QPointF & p) const948 QPointF Plotter2D::toWidget(const QPointF& p) const
949 {
950     double left=-viewport.left(), top=-viewport.top();
951     return QPointF((left + p.x()) * rang_x / d->m_dpr,  (top + p.y()) * rang_y / d->m_dpr);
952 }
953 
fromWidget(const QPoint & p) const954 QPointF Plotter2D::fromWidget(const QPoint& p) const
955 {
956     double negativePartX = -viewport.left();
957     double negativePartY = -viewport.top();
958 
959     return QPointF(p.x()/(rang_x*d->m_dpr)-negativePartX, p.y()/(rang_y*d->m_dpr)-negativePartY);
960 }
961 
toViewport(const QPoint & mv) const962 QPointF Plotter2D::toViewport(const QPoint &mv) const
963 {
964     return QPointF(mv.x()/rang_x, mv.y()/rang_y);
965 }
966 
moveViewport(const QPoint & delta)967 void Plotter2D::moveViewport(const QPoint& delta)
968 {
969     QPointF rel = toViewport(delta);
970     QRectF viewport = currentViewport();
971 
972     viewport.moveLeft(viewport.left() - rel.x());
973     viewport.moveTop(viewport.top() - rel.y());
974     setViewport(viewport);
975 }
976 
scaleViewport(qreal scale,const QPoint & center,bool repaint)977 void Plotter2D::scaleViewport(qreal scale, const QPoint& center, bool repaint)
978 {
979     QPointF p = fromWidget(center);
980     QSizeF ns = viewport.size()*scale;
981     QRectF nv(viewport.topLeft(), ns);
982 
983     setViewport(nv, false); //NOTE here isn't necessary to repaint, thus false
984 
985     QPointF p2 = p-fromWidget(center);
986     nv.translate(p2);
987     setViewport(nv, repaint);
988 }
989 
setKeepAspectRatio(bool ar)990 void Plotter2D::setKeepAspectRatio(bool ar)
991 {
992     m_keepRatio=ar;
993     updateScale(true);
994 }
995 
setModel(QAbstractItemModel * f)996 void Analitza::Plotter2D::setModel(QAbstractItemModel* f)
997 {
998     d->setModel(f);
999 }
1000 
setModel(QAbstractItemModel * f)1001 void Plotter2DPrivate::setModel(QAbstractItemModel* f)
1002 {
1003     if(m_model == f)
1004         return;
1005 
1006 
1007     if (m_model) {
1008         disconnect(m_model, &QAbstractItemModel::dataChanged, this, &Plotter2DPrivate::updateFuncs);
1009         disconnect(m_model, &QAbstractItemModel::rowsInserted, this, &Plotter2DPrivate::addFuncs);
1010         disconnect(m_model, &QAbstractItemModel::rowsRemoved, this, &Plotter2DPrivate::forceRepaint);
1011     }
1012 
1013     m_model = f;
1014 
1015     if (m_model) {
1016         connect(m_model, &QAbstractItemModel::dataChanged, this, &Plotter2DPrivate::updateFuncs);
1017         connect(m_model, &QAbstractItemModel::rowsInserted, this, &Plotter2DPrivate::addFuncs);
1018         connect(m_model, &QAbstractItemModel::rowsRemoved, this, &Plotter2DPrivate::forceRepaint);
1019 
1020         q->updateFunctions({}, 0, m_model->rowCount());
1021     } else {
1022         q->forceRepaint();
1023     }
1024 }
1025 
setDevicePixelRatio(qreal dpr)1026 void Analitza::Plotter2D::setDevicePixelRatio(qreal dpr)
1027 {
1028     d->m_dpr = dpr;
1029 }
1030 
setPaintedSize(const QSize & size)1031 void Plotter2D::setPaintedSize(const QSize& size)
1032 {
1033     m_size=size;
1034     updateScale(true);
1035 }
1036 
setXAxisLabel(const QString & label)1037 void Plotter2D::setXAxisLabel(const QString &label)
1038 {
1039     m_axisXLabel = label;
1040     forceRepaint();
1041 }
1042 
setYAxisLabel(const QString & label)1043 void Plotter2D::setYAxisLabel(const QString &label)
1044 {
1045     m_axisYLabel = label;
1046     forceRepaint();
1047 }
1048 
zoomIn(bool repaint)1049 void Plotter2D::zoomIn(bool repaint)
1050 {
1051     scaleViewport(ZoomInFactor, QPoint(m_size.width()*0.5, m_size.height()*0.5), repaint);
1052 }
1053 
zoomOut(bool repaint)1054 void Plotter2D::zoomOut(bool repaint)
1055 {
1056     scaleViewport(ZoomOutFactor, QPoint(m_size.width()*0.5, m_size.height()*0.5), repaint);
1057 }
1058 
setShowMinorGrid(bool mt)1059 void Plotter2D::setShowMinorGrid(bool mt)
1060 {
1061     m_showMinorGrid=mt;
1062     forceRepaint();
1063 }
1064 
setShowGrid(bool show)1065 void Plotter2D::setShowGrid(bool show)
1066 {
1067     if (m_showGrid != show) {
1068         m_showGrid=show;
1069         forceRepaint();
1070 
1071         showGridChanged();
1072     }
1073 }
1074 
model() const1075 QAbstractItemModel* Plotter2D::model() const
1076 {
1077     return d->m_model;
1078 }
1079