File indexing completed on 2024-09-01 13:10:07
0001 /************************************************************************************* 0002 * Copyright (C) 2011 by Aleix Pol <aleixpol@kde.org> * 0003 * Copyright (C) 2012-2013 by Percy Camilo T. Aucahuasi <percy.camilo.ta@gmail.com> * 0004 * * 0005 * This program is free software; you can redistribute it and/or * 0006 * modify it under the terms of the GNU General Public License * 0007 * as published by the Free Software Foundation; either version 2 * 0008 * of the License, or (at your option) any later version. * 0009 * * 0010 * This program is distributed in the hope that it will be useful, * 0011 * but WITHOUT ANY WARRANTY; without even the implied warranty of * 0012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 0013 * GNU General Public License for more details. * 0014 * * 0015 * You should have received a copy of the GNU General Public License * 0016 * along with this program; if not, write to the Free Software * 0017 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * 0018 *************************************************************************************/ 0019 0020 #include "plotter2d.h" 0021 0022 #include "planecurve.h" 0023 0024 #include "plotsmodel.h" 0025 #include "private/utils/mathutils.h" 0026 0027 #include <cmath> 0028 #include <QPalette> 0029 #include <QPainter> 0030 #include <QFontDatabase> 0031 #include <QDebug> 0032 #include <qnumeric.h> 0033 0034 #if defined(HAVE_IEEEFP_H) 0035 #include <ieeefp.h> 0036 // bool qIsInf(double x) { return !finite(x) && x==x; } 0037 #endif 0038 0039 using namespace Analitza; 0040 0041 // #define DEBUG_GRAPH 0042 0043 class Analitza::Plotter2DPrivate : public QObject { 0044 public: 0045 Plotter2DPrivate(Plotter2D* q) : q(q) {} 0046 0047 QAbstractItemModel* m_model = nullptr; 0048 qreal m_dpr = 1.; 0049 Plotter2D* const q; 0050 0051 void forceRepaint() { q->forceRepaint(); } 0052 void addFuncs(const QModelIndex& parent, int start, int end) { q->updateFunctions(parent, start, end); } 0053 void updateFuncs(const QModelIndex& start, const QModelIndex& end) { q->updateFunctions(QModelIndex(), start.row(), end.row()); } 0054 void setModel(QAbstractItemModel* f); 0055 }; 0056 0057 QColor const Plotter2D::m_axeColor(100,100,255); //TODO convert from const to param/attr and make setAxisColor(Qt::oriantation, qcolor) 0058 QColor const Plotter2D::m_derivativeColor(90,90,160); //TODO remove derivative logic from plotter2d, move it to other module 0059 QString const Plotter2D::PiSymbol(QChar(0x03C0)); 0060 QString const Plotter2D::DegreeSymbol(QChar(0x00B0)); 0061 QString const Plotter2D::GradianSymbol(QChar(0x1D4D)); 0062 double const Plotter2D::Pi6 = M_PI_2/3.0; 0063 double const Plotter2D::Pi12 = Plotter2D::Pi6*0.5; 0064 double const Plotter2D::Pi36 = Plotter2D::Pi6/6.0; 0065 const double Plotter2D::ZoomInFactor = 0.97/2; // (regular mouse wheel forward value) / 2 0066 const double Plotter2D::ZoomOutFactor = 2*1.03; // 2 x (regular mouse wheel backward value) 0067 0068 struct Plotter2D::GridInfo 0069 { 0070 double inc, xini, yini, xend, yend; 0071 int incLabelSkip; 0072 // sub5 flag is used for draw sub 5 intervals instead of 4 0073 int subinc; // true if inc=5*pow(10,n) (so, in this case, we draw 5 sub intervals, not 4) 0074 // we need scale for ticks, and for labels, thicks can be minor, labels no: so 0075 // this is necessary for minor ticks and for minir grid (but not for labels, so when draw labels we need ) 0076 int nxiniticks, nyiniticks, nxendticks, nyendticks; // nxini = floor(viewport.left()/inc), so xini is nxini*inc ... and so on 0077 int nxinilabels, nyinilabels, nxendlabels, nyendlabels; // nxini = floor(viewport.left()/inc), so xini is nxini*inc ... and so on 0078 }; 0079 0080 Plotter2D::Plotter2D(const QSizeF& size) 0081 : m_showGrid(true) 0082 , m_showMinorGrid(false) 0083 , m_gridColor(QColor(Qt::lightGray).lighter(120)) 0084 , m_backgroundColor(Qt::white) 0085 , m_autoGridStyle(true) 0086 , m_gridStyleHint(Squares) 0087 , rang_x(0) 0088 , rang_y(0) 0089 , m_keepRatio(true) 0090 , m_dirty(true) 0091 , m_size(size) 0092 , d(new Plotter2DPrivate(this)) 0093 , m_angleMode(Radian) 0094 , m_scaleMode(Linear) 0095 , m_showTicks(Qt::Vertical|Qt::Horizontal) 0096 , m_showTickLabels(Qt::Vertical|Qt::Horizontal) 0097 , m_showMinorTicks(false) 0098 , m_showAxes(Qt::Vertical|Qt::Horizontal) 0099 , m_showPolarAxis(false) 0100 , m_showPolarAngles(false) 0101 , m_axisXLabel(QStringLiteral("x")) 0102 , m_axisYLabel(QStringLiteral("y")) 0103 {} 0104 0105 Plotter2D::~Plotter2D() 0106 { 0107 delete d; 0108 } 0109 0110 void Plotter2D::setGridStyleHint(GridStyle suggestedgs) 0111 { 0112 m_gridStyleHint = suggestedgs; 0113 0114 forceRepaint(); 0115 } 0116 0117 const QColor Plotter2D::computeSubGridColor() const 0118 { 0119 //impl. details: since any kde user can create any palette style, we need this hard/magic numbres 0120 // because there is no way to guess, however, this code covers almost any case, 0121 // it was tested with more then 35 color styles, and all give good results. 0122 0123 QColor col = m_gridColor; 0124 0125 if (m_backgroundColor.value() < 200) 0126 { 0127 if (m_gridColor.value() < 40) 0128 col.setHsv(col.hsvHue(), col.hsvSaturation(), m_gridColor.value()-15); 0129 else 0130 col.setHsv(col.hsvHue(), col.hsvSaturation(), m_gridColor.value()>=255?120:m_gridColor.value()-10); 0131 } 0132 else // e.g: background color white and grid color black 0133 if (m_backgroundColor.value() < 245) 0134 col.setHsv(col.hsvHue(), col.hsvSaturation(), m_backgroundColor.value()-(m_backgroundColor.value()-200)/3); 0135 else 0136 col.setHsv(col.hsvHue(), col.hsvSaturation(), m_backgroundColor.value()-(m_backgroundColor.value()-200)/8); 0137 0138 return col; 0139 } 0140 0141 const Plotter2D::GridInfo Plotter2D::getGridInfo() const 0142 { 0143 GridInfo ret; 0144 0145 if (m_scaleMode == Linear) { 0146 const double val = log10(qMax(viewport.width(), -viewport.height())); 0147 const double diff = val-floor(val); 0148 const double magnitude = pow(10, floor(val)-1); 0149 0150 ret.inc = magnitude; 0151 ret.incLabelSkip = diff < 0.5 ? 1 : 3; 0152 } else { 0153 ret.inc = M_PI; 0154 ret.incLabelSkip = 1; 0155 } 0156 0157 ret.subinc = 4; 0158 0159 ret.nxinilabels = std::floor(viewport.left()/ret.inc); 0160 ret.nyinilabels = std::floor(viewport.bottom()/ret.inc); 0161 ret.nxendlabels = std::ceil(viewport.right()/ret.inc); 0162 ret.nyendlabels = std::ceil(viewport.top()/ret.inc); 0163 0164 ret.xini = ret.nxinilabels*ret.inc; 0165 ret.yini = ret.nyinilabels*ret.inc; 0166 ret.xend = ret.nxendlabels*ret.inc; 0167 ret.yend = ret.nyendlabels*ret.inc; 0168 0169 const bool drawminor = m_showMinorGrid || m_showMinorTicks; 0170 const double nfactor = drawminor ? ret.subinc : 1; 0171 0172 ret.nxiniticks = nfactor*ret.nxinilabels; 0173 ret.nyiniticks = nfactor*ret.nyinilabels; 0174 ret.nxendticks = nfactor*ret.nxendlabels; 0175 ret.nyendticks = nfactor*ret.nyendlabels; 0176 0177 return ret; 0178 } 0179 0180 void Plotter2D::drawGrid(QPaintDevice *qpd) 0181 { 0182 QPainter p; 0183 p.begin(qpd); 0184 0185 int current=currentFunction(); 0186 PlotItem* plot = itemAt(current); 0187 0188 GridStyle t = Squares; // default for Cartesian coord. sys. 0189 0190 if (plot && plot->coordinateSystem() == Polar) 0191 t = Circles; 0192 0193 if (!m_autoGridStyle) 0194 t = m_gridStyleHint; 0195 0196 drawAxes(&p, t); 0197 } 0198 0199 void Plotter2D::drawAxes(QPainter* painter, GridStyle gridStyle) const 0200 { 0201 GridInfo grid = getGridInfo(); 0202 0203 switch (gridStyle) 0204 { 0205 case Circles: drawCircles(painter, grid, gridStyle); break; 0206 default: drawSquares(painter, grid, gridStyle); break; 0207 } 0208 0209 drawMainAxes(painter); 0210 //NOTE always draw the ticks at the end: avoid the grid lines overlap the ticks text 0211 drawGridTickLabels(painter, grid, gridStyle); 0212 } 0213 0214 void Plotter2D::drawMainAxes(QPainter* painter) const 0215 { 0216 const QFontMetrics fm(painter->font()); 0217 const QPen axesPen(m_axeColor, 1, Qt::SolidLine); 0218 const QPointF center = toWidget(QPointF(0.,0.)); 0219 0220 painter->setPen(axesPen); 0221 painter->setBrush(axesPen.color()); 0222 0223 const int startAngleX = 150*16; 0224 const int startAngleY = 240*16; 0225 const int spanAngle = 60*16; 0226 const QPointF Xright(this->width(), center.y()); 0227 const QPointF Ytop(center.x(), 0.); 0228 const double arrowWidth=15., arrowHeight=4.; 0229 const QPointF dpx(arrowWidth, arrowHeight); 0230 const QPointF dpy(arrowHeight, arrowWidth); 0231 const QRectF rectX(Xright+dpx, Xright-dpx); 0232 const QRectF rectY(Ytop+dpy, Ytop-dpy); 0233 0234 if (m_showAxes&Qt::Horizontal) 0235 { 0236 painter->drawLine(QPointF(0., center.y()), Xright); 0237 0238 painter->setRenderHint(QPainter::Antialiasing, true); 0239 painter->drawPie(rectX, startAngleX, spanAngle); 0240 } 0241 0242 if (m_showAxes&Qt::Vertical) 0243 { 0244 painter->drawLine(Ytop, QPointF(center.x(), this->height())); 0245 0246 painter->setRenderHint(QPainter::Antialiasing, true); 0247 painter->drawPie(rectY, startAngleY, spanAngle); 0248 } 0249 0250 painter->setRenderHint(QPainter::Antialiasing, true); 0251 // axis labels 0252 QFont labelsfont = painter->font(); 0253 labelsfont.setBold(true); 0254 0255 painter->drawText(Xright.x() - fm.boundingRect(m_axisXLabel).width() - 5, center.y() - 20, m_axisXLabel); 0256 painter->drawText(center.x() + 5, Ytop.y() + fm.height() + 15, m_axisYLabel); 0257 0258 //write coords 0259 QString rightBound=QString::number(viewport.right()); 0260 int width=painter->fontMetrics().boundingRect(rightBound).width(); 0261 0262 painter->drawText(QPointF(3.+this->width()/2., 13. ), QString::number(viewport.top())); 0263 painter->drawText(QPointF(3.+this->width()/2., this->height()-5. ), QString::number(viewport.bottom())); 0264 painter->drawText(QPointF(8. , this->height()/2.-5.), QString::number(viewport.left())); 0265 painter->drawText(QPointF(this->width()-width, this->height()/2.-5.), rightBound); 0266 //EO write coords 0267 } 0268 0269 const QString Plotter2D::computeAngleLabelByFrac(unsigned int n, unsigned int d) const 0270 { 0271 QString s; 0272 0273 switch (m_angleMode) 0274 { 0275 case Radian: 0276 { 0277 s = (n==1) ? QString() : QString::number(n); 0278 s += PiSymbol; 0279 s += (d==1) ? QString() : '/'+QString::number(d); 0280 } 0281 break; 0282 case Degree: s = QString::number(radiansToDegrees(n*M_PI/d))+DegreeSymbol; break; 0283 case Gradian: s = QString::number(radiansToGradians(n*M_PI/d))+GradianSymbol; break; 0284 } 0285 0286 return s; 0287 } 0288 0289 const QString Plotter2D::computeAngleLabelByStep(unsigned int k, unsigned int step) const 0290 { 0291 QString s; 0292 0293 switch (m_angleMode) 0294 { 0295 case Radian: 0296 { 0297 s = (k==1) ? ((step==1) ? QLatin1String("") : QString::number(step)) : QString::number(k*step); 0298 s += PiSymbol; 0299 } 0300 break; 0301 case Degree: s = QString::number(radiansToDegrees(k*step*M_PI))+DegreeSymbol; break; 0302 case Gradian: s = QString::number(radiansToGradians(k*step*M_PI))+GradianSymbol; break; 0303 } 0304 0305 return s; 0306 } 0307 0308 void Plotter2D::drawCartesianTickLabels(QPainter* painter, const Plotter2D::GridInfo& gridinfo, CartesianAxis axis) const 0309 { 0310 Q_ASSERT(axis == XAxis || axis == Analitza::YAxis); 0311 0312 const bool isxaxis = (axis == XAxis); 0313 const double fontHeight = painter->fontMetrics().height(); 0314 0315 const bool incbig = (M_PI <= gridinfo.inc); 0316 const unsigned int bigstep = std::floor(gridinfo.inc/M_PI); 0317 const unsigned int step = std::ceil(M_PI/gridinfo.inc); 0318 0319 QString s; 0320 0321 const int from = (axis == Analitza::XAxis) ? gridinfo.nxinilabels : gridinfo.nyinilabels; 0322 const int to = (axis == Analitza::XAxis) ? gridinfo.nxendlabels : gridinfo.nyendlabels; 0323 0324 painter->save(); 0325 QFont tickFont = QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont); 0326 painter->setFont(tickFont); 0327 painter->setPen(QPen(QPalette().text().color())); 0328 for (int i = from; i <= to; ++i) 0329 { 0330 if (i == 0 || i%gridinfo.incLabelSkip!=0) continue; 0331 0332 double newval = i*gridinfo.inc; 0333 0334 const QPointF p = isxaxis 0335 ? toWidget(QPointF(newval, 0.0)) 0336 : toWidget(QPointF(0.0, newval)); 0337 0338 switch (m_scaleMode) 0339 { 0340 case Linear: 0341 s = QString::number(newval); 0342 break; 0343 case Trigonometric: 0344 { 0345 s = (i < 0) ? QString(QLatin1Char('-')) : QString(); 0346 0347 if (incbig) 0348 s += computeAngleLabelByStep(qAbs(i), bigstep); 0349 else 0350 { 0351 const QPair<unsigned int, unsigned int> frac = simplifyFraction(qAbs(i), step); 0352 0353 s += computeAngleLabelByFrac(frac.first, frac.second); 0354 } 0355 } 0356 break; 0357 } 0358 0359 const int swidth = painter->fontMetrics().boundingRect(s).width(); 0360 if (isxaxis) 0361 painter->drawText(p.x() - swidth/2, p.y()+fontHeight, s); 0362 else 0363 painter->drawText(p.x() - swidth - fontHeight/2, p.y()+fontHeight/2, s); 0364 } 0365 painter->restore(); 0366 } 0367 0368 void Plotter2D::drawPolarTickLabels(QPainter* painter, const Plotter2D::GridInfo& gridinfo) const 0369 { 0370 unsigned int k = 1; 0371 0372 painter->setPen(m_gridColor); 0373 0374 //TODO if minor 0375 const double newinc = gridinfo.inc/(gridinfo.subinc); // inc with sub intervals 0376 0377 //x 0378 // we assume 0 belongs to interval [gridinfo.xini, gridinfo.xend] 0379 double maxh = qMax(std::abs(gridinfo.xini), std::abs(gridinfo.xend)); 0380 int i = std::ceil(maxh/newinc)/2; 0381 double x = i*newinc; 0382 0383 if (std::abs(gridinfo.xend) <= std::abs(gridinfo.xini)) 0384 x *= -1.0; 0385 0386 // if 0 doesn't belongs to interval [gridinfo.xini, gridinfo.xend] 0387 if (((gridinfo.xend < 0.0) && (gridinfo.xini < 0.0)) || 0388 ((gridinfo.xend > 0.0) && (gridinfo.xini > 0.0))) 0389 { 0390 maxh = gridinfo.xend - gridinfo.xini; 0391 i = std::ceil(maxh/newinc)/2; 0392 0393 x = gridinfo.xini + i*newinc; 0394 } 0395 0396 //y 0397 // we assume 0 belongs to interval [gridinfo.yini, gridinfo.yend] 0398 maxh = qMax(std::abs(gridinfo.yini), std::abs(gridinfo.yend)); 0399 i = std::ceil(maxh/newinc)/2; 0400 double y = i*newinc; 0401 0402 if (std::abs(gridinfo.yend) <= std::abs(gridinfo.yini)) 0403 y *= -1.0; 0404 0405 // if 0 doesn't belongs to interval [gridinfo.xini, gridinfo.xend] 0406 if (((gridinfo.yend < 0.0) && (gridinfo.yini < 0.0)) || 0407 ((gridinfo.yend > 0.0) && (gridinfo.yini > 0.0))) 0408 { 0409 maxh = gridinfo.yend - gridinfo.yini; 0410 i = std::ceil(maxh/newinc)/2; 0411 0412 y = gridinfo.yini + i*newinc; 0413 } 0414 0415 const double r = qMax(std::abs(x), std::abs(y)); // radius 0416 const unsigned short axisxseparation = 16; // distance between tick text and x-axis 0417 const float axispolarseparation = axisxseparation*0.5; 0418 0419 double h = Pi6; // pi/6, then 12 rounds 0420 double t = 0.0; 0421 0422 unsigned int turns = 12; // 12 turns in [0, 2pi], since h=pi/6 0423 0424 // if we are far from origin then sudivide angles by pi/12 0425 if (!viewport.contains(QPointF(0.0, 0.0))) 0426 { 0427 h = Pi12; 0428 turns = 24; // 24 turns in [0, 2pi], since h=pi/12 0429 } 0430 0431 k = 0; 0432 0433 const unsigned int drawoverxaxiscount = turns/2; 0434 const unsigned int drawnextyaxiscount = turns/4; 0435 const unsigned int halfturns = drawoverxaxiscount; // or turns/2; 0436 0437 for (uint j = 0; j < turns; ++j, ++k, t += h) // Pi6 then 24 turns 0438 { 0439 const QPair<unsigned int, unsigned int> frac = simplifyFraction(k, halfturns); 0440 0441 QString s = (k != 0) ? computeAngleLabelByFrac(frac.first, frac.second) : QStringLiteral("0"); 0442 QPointF p(r*std::cos(t), r*std::sin(t)); 0443 0444 if (viewport.contains(p)) 0445 { 0446 0447 if (k % drawoverxaxiscount == 0) // draw 0, pi over x 0448 painter->drawText(toWidget(p)+QPointF(0.0, -axispolarseparation), s); 0449 else 0450 if (k % drawnextyaxiscount == 0) // draw pi/2, 3pi/2 next to y 0451 painter->drawText(toWidget(p)+QPointF(axispolarseparation, 0.0), s); 0452 else 0453 painter->drawText(toWidget(p), s); 0454 } 0455 } 0456 } 0457 0458 void Plotter2D::drawGridTickLabels(QPainter* painter, const GridInfo& gridinfo, GridStyle gridStyle) const 0459 { 0460 if (m_showTickLabels & Qt::Horizontal) 0461 drawCartesianTickLabels(painter, gridinfo, XAxis); 0462 0463 if (m_showTickLabels & Qt::Vertical) 0464 drawCartesianTickLabels(painter, gridinfo, YAxis); 0465 0466 if ((gridStyle == Circles) && m_showPolarAngles && m_showGrid) // draw labels for angles (polar axis) 0467 drawPolarTickLabels(painter, gridinfo); 0468 } 0469 0470 void Plotter2D::drawCircles(QPainter* painter, const GridInfo& gridinfo, GridStyle gridStyle) const 0471 { 0472 Q_ASSERT(gridStyle == Analitza::Circles); 0473 painter->setRenderHint(QPainter::Antialiasing, false); 0474 0475 const QPen textPen = QPen(QPalette().text().color()); 0476 const QPen gridPen(m_gridColor); 0477 const QPen subGridPen(computeSubGridColor()); 0478 0479 const unsigned short nsubinc = gridinfo.subinc; // count for draw sub intervals 0480 const bool drawminor = m_showMinorGrid || m_showMinorTicks; 0481 const double inc = drawminor ? gridinfo.inc/nsubinc : gridinfo.inc; // if show minor, then inc with sub intervals 0482 0483 if (m_showGrid) 0484 { 0485 const qreal until = qMax(qMax(qAbs(viewport.left()), qAbs(viewport.right())), qMax(qAbs(viewport.top()), qAbs(viewport.bottom())))*M_SQRT2; 0486 0487 int k = 1; 0488 0489 painter->setPen(gridPen); 0490 0491 for (double i = inc; i < until; i += inc, ++k) 0492 { 0493 if (i == 0.0) continue; 0494 0495 QPointF p(toWidget(QPointF(i,i))); 0496 QPointF p2(toWidget(QPointF(-i,-i))); 0497 QRectF er(p.x(),p.y(), p2.x()-p.x(), p2.y()-p.y()); 0498 0499 if ((k % nsubinc == 0) || !drawminor) // intervals 0500 { 0501 painter->setPen(gridPen); 0502 painter->drawEllipse(er); 0503 } 0504 else if (m_showMinorGrid)// sub intervals 0505 { 0506 painter->setPen(subGridPen); 0507 painter->drawEllipse(er); 0508 } 0509 } 0510 0511 if (m_showPolarAxis) // draw polar rays 0512 { 0513 double h = Pi12; // 2pi/(Pi/12) = 24 steps/turns 0514 const double r = std::abs(until); // radius 0515 const QPointF origin = toWidget(QPointF(0,0)); 0516 0517 int k = 1; 0518 unsigned int alternatesubgridcount = 2; 0519 unsigned int dontdrawataxiscount = 5; 0520 unsigned int steps = 24; // or turns 0521 0522 // if we are far from origin then sudivide angles by 5 degrees 0523 if (!viewport.contains(QPointF(0.0, 0.0))) 0524 { 0525 h = Pi36; // 2pi/(Pi/36) = 72 steps 0526 steps = 72; // or turns 0527 alternatesubgridcount = 3; 0528 dontdrawataxiscount = 17; 0529 } 0530 0531 double t = h; 0532 for (uint j = 1; j <= steps; ++j, ++k, t += h) 0533 { 0534 if (j % alternatesubgridcount == 0) 0535 painter->setPen(gridPen); 0536 else if (m_showMinorGrid) 0537 { 0538 painter->setPen(subGridPen); 0539 0540 QPointF p = toWidget(QPointF(r*std::cos(t), r*std::sin(t))); 0541 painter->drawLine(origin, p); 0542 0543 if (k % dontdrawataxiscount == 0) // avoid draw the ray at 0,pi/2,pi,3pi/2 0544 { 0545 t += h; 0546 ++j; 0547 } 0548 } 0549 } 0550 } 0551 } 0552 0553 //BEGIN draw ticks 0554 if (m_showTickLabels & Qt::Horizontal) 0555 { 0556 painter->setPen(textPen); 0557 const QPointF yoffset(0.,-3.); 0558 0559 for (int j = gridinfo.nxiniticks, i = 0; j < gridinfo.nxendticks; ++j, ++i) 0560 { 0561 if (j == 0) continue; 0562 0563 const double x = j*inc; 0564 const QPointF p = toWidget(QPointF(x, 0.)); 0565 0566 if ((i % nsubinc == 0) || !drawminor || m_showMinorTicks) 0567 painter->drawLine(p, p+yoffset); 0568 } 0569 } 0570 0571 if (m_showTickLabels & Qt::Vertical) 0572 { 0573 painter->setPen(textPen); 0574 const QPointF xoffset(3.,0.); 0575 0576 for (int j = gridinfo.nyiniticks, i = 0; j < gridinfo.nyendticks; ++j, ++i) 0577 { 0578 if (j == 0) continue; 0579 0580 const double y = j*inc; 0581 0582 const QPointF p = toWidget(QPointF(0., y)); 0583 0584 if ((i % nsubinc == 0) || !(m_showMinorGrid || m_showMinorTicks) || m_showMinorTicks) 0585 painter->drawLine(p, p+xoffset); 0586 } 0587 } 0588 //END draw ticks 0589 } 0590 0591 void Plotter2D::drawSquares(QPainter* painter, const GridInfo& gridinfo, GridStyle gridStyle) const 0592 { 0593 painter->setRenderHint(QPainter::Antialiasing, false); 0594 0595 const QPen gridPen(m_gridColor); 0596 const QPen subGridPen(computeSubGridColor()); 0597 const QPen gridPenBold(gridPen.brush(), 2); 0598 const QPen subGridPenBold(subGridPen.brush(), gridPenBold.widthF()); 0599 0600 const unsigned short nsubinc = gridinfo.subinc; // count for draw sub intervals 0601 const bool drawminor = m_showMinorGrid || m_showMinorTicks; 0602 const double inc = drawminor? gridinfo.inc/nsubinc : gridinfo.inc; // if show minor, then inc with sub intervals 0603 0604 for (int n = gridinfo.nxiniticks, i = 0; n < gridinfo.nxendticks; ++n, ++i) 0605 { 0606 if (n == 0) continue; 0607 0608 const double x = n*inc; 0609 const QPointF p = toWidget(QPointF(x, 0.)); 0610 0611 if (m_showGrid && gridStyle == Crosses) 0612 { 0613 const double crossside = 5; 0614 const double centx = p.x(); 0615 0616 for (int j = gridinfo.nyiniticks, k = 0; j < gridinfo.nyendticks; ++j, ++k) 0617 { 0618 if (j == 0) continue; 0619 0620 const double y = j*inc; 0621 const QPointF py = toWidget(QPointF(0., y)); 0622 const double centy = py.y(); 0623 0624 if (((i % nsubinc == 0) && (k % nsubinc == 0)) || !drawminor) // intervals 0625 { 0626 painter->setPen(gridPenBold); 0627 painter->drawLine(QPointF(centx-crossside, centy), QPointF(centx+crossside, centy)); // horizontal 0628 painter->drawLine(QPointF(centx, centy-crossside), QPointF(centx, centy+crossside)); // vertical 0629 } 0630 else if (m_showMinorGrid)// sub intervals 0631 { 0632 painter->setPen(subGridPenBold); 0633 painter->drawLine(QPointF(centx-crossside, centy), QPointF(centx+crossside, centy)); // horizontal 0634 painter->drawLine(QPointF(centx, centy-crossside), QPointF(centx, centy+crossside)); // vertical 0635 } 0636 } 0637 } 0638 0639 if ((i % nsubinc == 0) || !drawminor) // intervals 0640 { 0641 if (m_showGrid && (gridStyle == Squares || gridStyle == VerticalLines)) 0642 { 0643 painter->setPen(gridPen); 0644 painter->drawLine(QPointF(p.x(), height()), QPointF(p.x(), 0.)); 0645 } 0646 0647 if ((m_showTicks & Qt::Horizontal)) 0648 { 0649 painter->setPen(QPen(QPalette().text().color())); 0650 painter->drawLine(p, p+QPointF(0.,-3.)); 0651 } 0652 } 0653 else // sub intervals 0654 { 0655 if (m_showGrid && m_showMinorGrid && (gridStyle == Squares || gridStyle == VerticalLines)) 0656 { 0657 painter->setPen(subGridPen); 0658 painter->drawLine(QPointF(p.x(), height()), QPointF(p.x(), 0.)); 0659 } 0660 0661 if ((m_showTicks & Qt::Horizontal) && m_showMinorTicks) 0662 { 0663 painter->setPen(QPen(QPalette().text().color())); 0664 painter->drawLine(p, p+QPointF(0.,-3.)); 0665 } 0666 } 0667 } 0668 0669 for (int j = gridinfo.nyiniticks, i = 0; j < gridinfo.nyendticks; ++j, ++i) 0670 { 0671 if (j == 0) continue; 0672 0673 const double y = j*inc; 0674 const QPointF p = toWidget(QPointF(0., y)); 0675 0676 if ((i % nsubinc == 0) || !drawminor) // intervals 0677 { 0678 if (m_showGrid && (gridStyle == Squares || gridStyle == HorizontalLines)) 0679 { 0680 painter->setPen(gridPen); 0681 painter->drawLine(QPointF(0., p.y()), QPointF(width(), p.y())); 0682 } 0683 0684 if ((m_showTicks & Qt::Horizontal)) 0685 { 0686 painter->setPen(QPen(QPalette().text().color())); 0687 painter->drawLine(p, p+QPointF(3.,0.)); 0688 } 0689 } 0690 else // sub intervals 0691 { 0692 if (m_showGrid && m_showMinorGrid && (gridStyle == Squares || gridStyle == HorizontalLines)) 0693 { 0694 painter->setPen(subGridPen); 0695 painter->drawLine(QPointF(0., p.y()), QPointF(width(), p.y())); 0696 } 0697 0698 if ((m_showTicks & Qt::Horizontal) && m_showMinorTicks) 0699 { 0700 painter->setPen(QPen(QPalette().text().color())); 0701 painter->drawLine(p, p+QPointF(3.,0.)); 0702 } 0703 } 0704 } 0705 } 0706 0707 PlotItem* Plotter2D::itemAt(int row) const 0708 { 0709 if (!d->m_model) 0710 return nullptr; 0711 0712 QModelIndex pi = d->m_model->index(row, 0); 0713 0714 if (!pi.isValid()) 0715 return nullptr; 0716 0717 PlotItem* plot = pi.data(PlotsModel::PlotRole).value<PlotItem*>(); 0718 0719 if (plot->spaceDimension() != Dim2D) 0720 return nullptr; 0721 0722 return plot; 0723 } 0724 0725 void Plotter2D::drawFunctions(QPaintDevice *qpd) 0726 { 0727 drawGrid(qpd); 0728 QPen pfunc(QColor(0,150,0), 2); 0729 0730 QPainter p; 0731 p.begin(qpd); 0732 p.setPen(pfunc); 0733 0734 if (!d->m_model || m_dirty) 0735 return; 0736 0737 p.setRenderHint(QPainter::Antialiasing, true); 0738 0739 const int current=currentFunction(); 0740 const int dpr = qMax<int>(1, qRound(qpd->logicalDpiX()/100.)) << 1; 0741 0742 for (int k = 0; k < d->m_model->rowCount(); ++k ) 0743 { 0744 PlaneCurve* curve = dynamic_cast<PlaneCurve *>(itemAt(k)); 0745 0746 //NOTE from GSOC: not all valid plots always has points (so, we need to check if points().isEmpty() too) 0747 if (!curve || !curve->isVisible() || curve->points().isEmpty()) 0748 continue; 0749 0750 pfunc.setColor(curve->color()); 0751 pfunc.setWidth((k==current)+dpr); 0752 pfunc.setStyle(Qt::SolidLine); 0753 p.setPen(pfunc); 0754 0755 const QVector<QPointF> &vect=curve->points(); 0756 QVector<int> jumps=curve->jumps(); 0757 0758 unsigned int pointsCount = vect.count(); 0759 QPointF ultim = toWidget(vect.at(0)); 0760 0761 int nextjump; 0762 nextjump = jumps.isEmpty() ? -1 : jumps.first(); 0763 if (!jumps.isEmpty()) jumps.remove(0); 0764 // #define DEBUG_GRAPH 1 0765 #ifdef DEBUG_GRAPH 0766 qDebug() << "---------" << jumps.count()+1; 0767 #endif 0768 for(unsigned int j=0; j<pointsCount; ++j) 0769 { 0770 const QPointF act = toWidget(vect.at(j)); 0771 0772 // qDebug() << "xxxxx" << act << ultim << qIsNaN(act.y()) << qIsNaN(ultim.y()); 0773 if(qIsInf(act.y()) && !qIsNaN(act.y())) qDebug() << "trying to plot from a NaN value" << act << ultim; 0774 else if(qIsInf(act.y()) && qIsNaN(act.y())) qDebug() << "trying to plot to a NaN value"; 0775 0776 const bool bothinf=(qIsInf(ultim.y()) && qIsInf(act.y())) || (qIsInf(ultim.x()) && qIsInf(act.x())); 0777 if(!bothinf && !qIsNaN(act.y()) && !qIsNaN(ultim.y()) && nextjump!=int(j)) { 0778 if(qIsInf(ultim.y())) { 0779 if(act.y()<0) ultim.setY(0); 0780 if(act.y()>0) ultim.setY(qpd->height()); 0781 } 0782 // 0783 QPointF act2(act); 0784 if(qIsInf(act2.y())) { 0785 if(ultim.y()<0) act2.setY(0); 0786 if(ultim.y()>0) act2.setY(qpd->height()); 0787 } 0788 0789 // qDebug() << "xxxxx" << act2 << ultim << qIsNaN(act2.y()) << qIsNaN(ultim.y()); 0790 0791 p.drawLine(ultim, act2); 0792 0793 #ifdef DEBUG_GRAPH 0794 QPen dpen(Qt::red); 0795 dpen.setWidth(3); 0796 p.setPen(dpen); 0797 p.drawPoint(ultim); 0798 p.setPen(pfunc); 0799 #endif 0800 } else if(nextjump==int(j)) { 0801 do { 0802 if(nextjump!=int(j)) 0803 p.drawPoint(act); 0804 0805 nextjump = jumps.isEmpty() ? -1 : jumps.first(); 0806 if (!jumps.isEmpty()) jumps.remove(0); 0807 0808 } while(!jumps.isEmpty() && jumps.first()==nextjump+1); 0809 0810 #ifdef DEBUG_GRAPH 0811 qDebug() << "jumpiiiiiing" << ultim << toWidget(vect.at(j)); 0812 QPen dpen(Qt::blue); 0813 dpen.setWidth(2); 0814 p.setPen(dpen); 0815 p.drawLine(QLineF(QPointF(act.x(), height()/2-10), QPointF(act.x(), height()/2+10))); 0816 p.setPen(pfunc); 0817 #endif 0818 } 0819 0820 ultim=act; 0821 } 0822 } 0823 0824 p.end(); 0825 } 0826 0827 void Plotter2D::updateFunctions(const QModelIndex & parent, int start, int end) 0828 { 0829 if (!d->m_model || parent.isValid()) 0830 return; 0831 0832 QRectF viewportFixed = viewport; 0833 viewportFixed.setTopLeft(viewport.bottomLeft()); 0834 viewportFixed.setHeight(std::fabs(viewport.height())); 0835 0836 for(int i=start; i<=end; i++) 0837 { 0838 PlaneCurve* curve = dynamic_cast<PlaneCurve *>(itemAt(i)); 0839 0840 if (!curve || !curve->isVisible()) 0841 continue; 0842 0843 curve->update(viewportFixed); 0844 } 0845 0846 m_dirty = false; // m_dirty = false means that the we will not recalculate functions points 0847 0848 forceRepaint(); 0849 } 0850 0851 QPair<QPointF, QString> Plotter2D::calcImage(const QPointF& ndp) const 0852 { 0853 if (!d->m_model || currentFunction() == -1) 0854 return QPair<QPointF, QString>(); 0855 0856 PlaneCurve* curve = dynamic_cast<PlaneCurve*>(itemAt(currentFunction())); 0857 0858 if (curve && curve->isVisible()) 0859 return curve->image(ndp); 0860 0861 return QPair<QPointF, QString>(); 0862 } 0863 0864 QRectF Plotter2D::normalizeUserViewport(const QRectF &uvp) 0865 { 0866 QRectF normalizeduvp = uvp; 0867 rang_x = width()/normalizeduvp.width(); 0868 rang_y = height()/normalizeduvp.height(); 0869 0870 if (m_keepRatio && rang_x != rang_y) 0871 { 0872 rang_y=rang_x=qMin(std::fabs(rang_x), std::fabs(rang_y)); 0873 0874 if(rang_y>0.) rang_y=-rang_y; 0875 if(rang_x<0.) rang_x=-rang_x; 0876 0877 double newW=width()/rang_x, newH=height()/rang_x; 0878 0879 double mx=(uvp.width()-newW)/2.; 0880 double my=(uvp.height()-newH)/2.; 0881 0882 normalizeduvp.setLeft(uvp.left()+mx); 0883 normalizeduvp.setTop(uvp.bottom()-my); 0884 normalizeduvp.setWidth(newW); 0885 normalizeduvp.setHeight(-newH); //WARNING why negative distance? 0886 0887 //Commented because precision could make the program crash 0888 // Q_ASSERT(uvp.center() == viewport.center()); 0889 } 0890 0891 return normalizeduvp; 0892 } 0893 0894 void Plotter2D::updateScale(bool repaint) 0895 { 0896 viewport = normalizeUserViewport(userViewport); 0897 0898 if (repaint) { 0899 if (d->m_model && d->m_model->rowCount()>0) 0900 updateFunctions(QModelIndex(), 0, d->m_model->rowCount()-1); 0901 else 0902 forceRepaint(); 0903 } 0904 } 0905 0906 void Plotter2D::setViewport(const QRectF& vp, bool repaint) 0907 { 0908 userViewport = vp; 0909 0910 Q_ASSERT(userViewport.top()>userViewport.bottom()); 0911 Q_ASSERT(userViewport.right()>userViewport.left()); 0912 0913 updateScale(repaint); 0914 0915 viewportChanged(); 0916 } 0917 0918 QLineF Plotter2D::slope(const QPointF& dp) const 0919 { 0920 if (!d->m_model || currentFunction() == -1) 0921 return QLineF(); 0922 0923 PlaneCurve* plot = dynamic_cast<PlaneCurve*>(itemAt(currentFunction())); 0924 0925 if (plot && plot->isVisible()) 0926 { 0927 QLineF ret = plot->tangent(dp); 0928 0929 if (ret.isNull() && currentFunction()>=0) 0930 { 0931 QPointF a = calcImage(dp-QPointF(.1,.1)).first; 0932 QPointF b = calcImage(dp+QPointF(.1,.1)).first; 0933 0934 ret = slopeToLine((a.y()-b.y())/(a.x()-b.x())); 0935 } 0936 0937 return ret; 0938 } 0939 0940 return QLineF(); 0941 } 0942 0943 QLineF Plotter2D::toWidget(const QLineF &f) const 0944 { 0945 return QLineF(toWidget(f.p1()), toWidget(f.p2())); 0946 } 0947 0948 QPointF Plotter2D::toWidget(const QPointF& p) const 0949 { 0950 double left=-viewport.left(), top=-viewport.top(); 0951 return QPointF((left + p.x()) * rang_x / d->m_dpr, (top + p.y()) * rang_y / d->m_dpr); 0952 } 0953 0954 QPointF Plotter2D::fromWidget(const QPoint& p) const 0955 { 0956 double negativePartX = -viewport.left(); 0957 double negativePartY = -viewport.top(); 0958 0959 return QPointF(p.x()/(rang_x*d->m_dpr)-negativePartX, p.y()/(rang_y*d->m_dpr)-negativePartY); 0960 } 0961 0962 QPointF Plotter2D::toViewport(const QPoint &mv) const 0963 { 0964 return QPointF(mv.x()/rang_x, mv.y()/rang_y); 0965 } 0966 0967 void Plotter2D::moveViewport(const QPoint& delta) 0968 { 0969 QPointF rel = toViewport(delta); 0970 QRectF viewport = currentViewport(); 0971 0972 viewport.moveLeft(viewport.left() - rel.x()); 0973 viewport.moveTop(viewport.top() - rel.y()); 0974 setViewport(viewport); 0975 } 0976 0977 void Plotter2D::scaleViewport(qreal scale, const QPoint& center, bool repaint) 0978 { 0979 QPointF p = fromWidget(center); 0980 QSizeF ns = viewport.size()*scale; 0981 QRectF nv(viewport.topLeft(), ns); 0982 0983 setViewport(nv, false); //NOTE here isn't necessary to repaint, thus false 0984 0985 QPointF p2 = p-fromWidget(center); 0986 nv.translate(p2); 0987 setViewport(nv, repaint); 0988 } 0989 0990 void Plotter2D::setKeepAspectRatio(bool ar) 0991 { 0992 m_keepRatio=ar; 0993 updateScale(true); 0994 } 0995 0996 void Analitza::Plotter2D::setModel(QAbstractItemModel* f) 0997 { 0998 d->setModel(f); 0999 } 1000 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 1026 void Analitza::Plotter2D::setDevicePixelRatio(qreal dpr) 1027 { 1028 d->m_dpr = dpr; 1029 } 1030 1031 void Plotter2D::setPaintedSize(const QSize& size) 1032 { 1033 m_size=size; 1034 updateScale(true); 1035 } 1036 1037 void Plotter2D::setXAxisLabel(const QString &label) 1038 { 1039 m_axisXLabel = label; 1040 forceRepaint(); 1041 } 1042 1043 void Plotter2D::setYAxisLabel(const QString &label) 1044 { 1045 m_axisYLabel = label; 1046 forceRepaint(); 1047 } 1048 1049 void Plotter2D::zoomIn(bool repaint) 1050 { 1051 scaleViewport(ZoomInFactor, QPoint(m_size.width()*0.5, m_size.height()*0.5), repaint); 1052 } 1053 1054 void Plotter2D::zoomOut(bool repaint) 1055 { 1056 scaleViewport(ZoomOutFactor, QPoint(m_size.width()*0.5, m_size.height()*0.5), repaint); 1057 } 1058 1059 void Plotter2D::setShowMinorGrid(bool mt) 1060 { 1061 m_showMinorGrid=mt; 1062 forceRepaint(); 1063 } 1064 1065 void Plotter2D::setShowGrid(bool show) 1066 { 1067 if (m_showGrid != show) { 1068 m_showGrid=show; 1069 forceRepaint(); 1070 1071 showGridChanged(); 1072 } 1073 } 1074 1075 QAbstractItemModel* Plotter2D::model() const 1076 { 1077 return d->m_model; 1078 }