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