Warning, file /education/kmplot/kmplot/view.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 /* 0002 KmPlot - a math. function plotter for the KDE-Desktop 0003 0004 SPDX-FileCopyrightText: 1998, 1999, 2000, 2002 Klaus-Dieter Möller <kd.moeller@t-online.de> 0005 SPDX-FileCopyrightText: 2006 David Saxton <david@bluehaze.org> 0006 0007 This file is part of the KDE Project. 0008 KmPlot is part of the KDE-EDU Project. 0009 0010 SPDX-License-Identifier: GPL-2.0-or-later 0011 0012 */ 0013 0014 #include "view.h" 0015 0016 #include <kmplot/config-kmplot.h> 0017 0018 // Qt includes 0019 #include <QAbstractTextDocumentLayout> 0020 #include <QBitmap> 0021 #include <QCursor> 0022 #include <QDataStream> 0023 #include <QElapsedTimer> 0024 #include <QList> 0025 #include <QMenu> 0026 #include <QPaintEvent> 0027 #include <QPainter> 0028 #include <QSlider> 0029 #include <QTextEdit> 0030 #include <QTimer> 0031 0032 // KDE includes 0033 #include <KActionCollection> 0034 #include <KMessageBox> 0035 0036 // local includes 0037 #include "functioneditor.h" 0038 #include "functiontools.h" 0039 #include "ksliderwindow.h" 0040 #include "maindlg.h" 0041 #include "parameteranimator.h" 0042 #include "settings.h" 0043 #include "viewadaptor.h" 0044 #include "xparser.h" 0045 0046 // other includes 0047 #include <assert.h> 0048 #include <cmath> 0049 #ifdef HAVE_IEEEFP_H 0050 #include <ieeefp.h> 0051 #endif 0052 0053 #if defined(Q_CC_MINGW) 0054 using namespace std; 0055 #endif 0056 0057 // does for real numbers what "%" does for integers 0058 double realModulo(double x, double mod) 0059 { 0060 return x - floor(x / mod) * mod; 0061 } 0062 0063 // BEGIN class View 0064 View *View::m_self = 0; 0065 0066 View::View(bool readOnly, QMenu *functionPopup, QWidget *parent) 0067 : QWidget(parent) 0068 , buffer(width(), height()) 0069 , m_popupMenu(functionPopup) 0070 , m_readonly(readOnly) 0071 , m_AccumulatedDelta(0) 0072 , m_viewportAnimation(new QPropertyAnimation(this, "viewport")) 0073 { 0074 assert(!m_self); // this class should only be constructed once 0075 m_self = this; 0076 setAttribute(Qt::WA_StaticContents); 0077 0078 m_haveRoot = false; 0079 emit updateRootValue(false, 0); 0080 m_xmin = m_xmax = m_ymin = m_ymax = 0.0; 0081 m_printHeaderTable = false; 0082 m_printBackground = false; 0083 m_printWidth = 0.0; 0084 m_printHeight = 0.0; 0085 m_stopCalculating = false; 0086 m_isDrawing = false; 0087 m_popupMenuStatus = NoPopup; 0088 m_zoomMode = Normal; 0089 m_prevCursor = CursorArrow; 0090 m_backgroundColor = Settings::backgroundcolor(); 0091 0092 m_textEdit = new KTextEdit; 0093 m_textEdit->setWordWrapMode(QTextOption::NoWrap); 0094 m_textEdit->setLineWrapMode(QTextEdit::NoWrap); 0095 m_textDocument = m_textEdit->document(); 0096 0097 m_mousePressTimer = new QElapsedTimer(); 0098 0099 new ViewAdaptor(this); 0100 QDBusConnection::sessionBus().registerObject("/view", this); 0101 0102 setMouseTracking(true); 0103 m_sliderWindow = 0; 0104 0105 m_popupMenuTitle = m_popupMenu->insertSection(MainDlg::self()->m_firstFunctionAction, ""); 0106 connect(XParser::self(), &XParser::functionRemoved, this, &View::functionRemoved); 0107 } 0108 0109 View::~View() 0110 { 0111 m_textEdit->deleteLater(); 0112 delete XParser::self(); 0113 delete m_mousePressTimer; 0114 } 0115 0116 void View::initDrawLabels() 0117 { 0118 m_labelFont = Settings::labelFont(); 0119 0120 for (int i = 0; i < LabelGridSize; ++i) 0121 for (int j = 0; j < LabelGridSize; ++j) 0122 m_usedDiagramArea[i][j] = false; 0123 0124 // Add the axis 0125 double x = xToPixel(0); 0126 double y = yToPixel(0); 0127 0128 double x0 = xToPixel(m_xmin); 0129 double x1 = xToPixel(m_xmax); 0130 double y0 = yToPixel(m_ymin); 0131 double y1 = yToPixel(m_ymax); 0132 0133 // x-axis 0134 markDiagramAreaUsed(QRectF(x - 20, y0, 40, y1 - y0)); 0135 0136 // y-axis 0137 markDiagramAreaUsed(QRectF(x0, y - 20, x1 - x0, 40)); 0138 } 0139 0140 double View::niceTicSpacing(double length_mm, double range) 0141 { 0142 Q_ASSERT_X(range > 0, "View::niceTicSpacing", "Range must be positive"); 0143 0144 if (length_mm <= 0) { 0145 // Don't assert, as we can at least handle this situation - and it can 0146 // happen with extreme zooms 0147 0148 qWarning() << "Non-positive length: length_mm=" << length_mm; 0149 length_mm = 120; 0150 } 0151 0152 // Custom case for trigonometric scaled 0153 if (qFuzzyCompare(range, 4 * M_PI)) 0154 return M_PI / 2; 0155 0156 // Aim to space the tics by around 16 mm 0157 double target = range * 16.0 / length_mm; 0158 0159 // The scaling required to bring target to a number between 1 and 10 0160 double scale = pow(10, -std::floor(log(target) / log(10.0))); 0161 0162 // Calculate the first digit of target, e.g. if target is 0.0352, then leading will be set to 3 0163 int leading = int(target * scale); 0164 0165 if (leading == 1) 0166 return 1 / scale; 0167 else if (leading >= 2 && leading <= 4) 0168 return 2 / scale; 0169 else 0170 return 5 / scale; 0171 } 0172 0173 double View::validatedTicSpacing(double spacing, double range, double pixels, double minPixels) 0174 { 0175 Q_ASSERT_X(range > 0, "View::validatedTicSpacing", "Range must be positive"); 0176 Q_ASSERT_X(minPixels > 0, "View::validatedTicSpacing", "MinPixels must be positive"); 0177 0178 spacing = qAbs(spacing); 0179 if (qFuzzyCompare(spacing, 0)) 0180 return 2.0 * range; 0181 0182 double factor; 0183 0184 // Make sure spacing between tics is at least minPixels 0185 pixels /= range / spacing; 0186 factor = pixels / minPixels; 0187 if (factor < 1.0) { 0188 int exponent; 0189 frexp(factor, &exponent); 0190 spacing = ldexp(spacing, -exponent + 1); 0191 } 0192 0193 // Make sure there are at least two tics 0194 factor = spacing / range; 0195 if (factor > 0.5) { 0196 int exponent; 0197 frexp(factor, &exponent); 0198 spacing = ldexp(spacing, -exponent - 1); 0199 } 0200 0201 return spacing; 0202 } 0203 0204 void View::initDrawing(QPaintDevice *device, PlotMedium medium) 0205 { 0206 switch (medium) { 0207 case SVG: 0208 case Screen: { 0209 m_clipRect = QRect(0, 0, width(), height()); 0210 break; 0211 } 0212 0213 case Printer: { 0214 double inchesPerMeter = 100.0 / 2.54; 0215 0216 int pixels_x = int(m_printWidth * device->logicalDpiX() * inchesPerMeter); 0217 int pixels_y = int(m_printHeight * device->logicalDpiY() * inchesPerMeter); 0218 0219 m_clipRect = QRect(0, 0, pixels_x, pixels_y); 0220 break; 0221 } 0222 0223 case Pixmap: { 0224 QPixmap *pic = static_cast<QPixmap *>(device); 0225 m_clipRect = pic->rect(); 0226 break; 0227 } 0228 } 0229 0230 if (m_clipRect.width() <= 0 || m_clipRect.height() <= 0) { 0231 qWarning() << "Invalid clip rect: m_clipRect=" << m_clipRect; 0232 return; 0233 } 0234 0235 // BEGIN get X/Y range 0236 m_xmin = XParser::self()->eval(Settings::xMin()); 0237 m_xmax = XParser::self()->eval(Settings::xMax()); 0238 0239 if (m_xmax <= m_xmin || !std::isfinite(m_xmin) || !std::isfinite(m_xmax)) { 0240 m_xmin = -8; 0241 m_xmax = +8; 0242 } 0243 0244 m_ymin = XParser::self()->eval(Settings::yMin()); 0245 m_ymax = XParser::self()->eval(Settings::yMax()); 0246 if (m_ymax <= m_ymin || !std::isfinite(m_ymin) || !std::isfinite(m_ymax)) { 0247 m_ymin = -8; 0248 m_ymax = +8; 0249 } 0250 // END get X/Y range 0251 0252 // BEGIN calculate scaling matrices 0253 m_realToPixel.reset(); 0254 m_realToPixel.scale(m_clipRect.width() / (m_xmax - m_xmin), m_clipRect.height() / (m_ymin - m_ymax)); 0255 m_realToPixel.translate(-m_xmin, -m_ymax); 0256 0257 m_pixelToReal = m_realToPixel.inverted(); 0258 // END calculate scaling matrices 0259 0260 // BEGIN get Tic Separation 0261 QFontMetricsF fm(Settings::axesFont(), device); 0262 if (Settings::xScalingMode() == 0) { 0263 double length = pixelsToMillimeters(xToPixel(m_xmax), device); 0264 double spacing = niceTicSpacing(length, m_xmax - m_xmin); 0265 ticSepX.updateExpression(spacing); 0266 } else { 0267 ticSepX.updateExpression(Settings::xScaling()); 0268 double spacing = ticSepX.value(); 0269 spacing = validatedTicSpacing(spacing, m_xmax - m_xmin, xToPixel(m_xmax), fm.lineSpacing()); 0270 ticSepX.updateExpression(spacing); 0271 } 0272 0273 if (Settings::yScalingMode() == 0) { 0274 double length = pixelsToMillimeters(yToPixel(m_ymin), device); 0275 double spacing = niceTicSpacing(length, m_ymax - m_ymin); 0276 ticSepY.updateExpression(spacing); 0277 } else { 0278 ticSepY.updateExpression(Settings::yScaling()); 0279 double spacing = ticSepY.value(); 0280 spacing = validatedTicSpacing(spacing, m_ymax - m_ymin, yToPixel(m_ymin), fm.lineSpacing()); 0281 ticSepY.updateExpression(spacing); 0282 } 0283 0284 ticStartX = ceil(m_xmin / ticSepX.value()) * ticSepX.value(); 0285 ticStartY = ceil(m_ymin / ticSepY.value()) * ticSepY.value(); 0286 // END get Tic Separation 0287 0288 // BEGIN get colours 0289 m_backgroundColor = Settings::backgroundcolor(); 0290 if (!m_backgroundColor.isValid()) 0291 m_backgroundColor = Qt::white; 0292 // END get colours 0293 0294 XParser::self()->setAngleMode((Parser::AngleMode)Settings::anglemode()); 0295 0296 initDrawLabels(); 0297 } 0298 0299 void View::draw(QPaintDevice *dev, PlotMedium medium) 0300 { 0301 if (m_isDrawing) 0302 return; 0303 0304 m_isDrawing = true; 0305 updateCursor(); 0306 initDrawing(dev, medium); 0307 0308 QPainter painter(dev); 0309 0310 switch (medium) { 0311 case SVG: 0312 case Screen: 0313 break; 0314 0315 case Printer: { 0316 if (m_printHeaderTable) 0317 drawHeaderTable(&painter); 0318 0319 painter.translate((dev->width() - m_clipRect.width()) / 2, 0); 0320 0321 if (m_printBackground) 0322 painter.fillRect(m_clipRect, m_backgroundColor); // draw a colored background 0323 0324 break; 0325 } 0326 0327 case Pixmap: { 0328 QPixmap *pic = static_cast<QPixmap *>(dev); 0329 pic->fill(m_backgroundColor); 0330 break; 0331 } 0332 } 0333 0334 painter.setClipRect(m_clipRect); 0335 0336 // BEGIN draw diagram background stuff 0337 painter.setRenderHint(QPainter::Antialiasing, true); 0338 0339 drawGrid(&painter); 0340 if (Settings::showAxes()) 0341 drawAxes(&painter); 0342 if (Settings::showLabel()) 0343 drawLabels(&painter); 0344 // END draw diagram background stuff 0345 0346 // BEGIN draw the functions 0347 m_stopCalculating = false; 0348 0349 // Antialiasing slows down rendering a lot, so turn it off if we are 0350 // sliding the view about 0351 painter.setRenderHint(QPainter::Antialiasing, m_zoomMode != Translating); 0352 0353 double at = -1; 0354 for (Function *function : qAsConst(XParser::self()->m_ufkt)) { 0355 at += 1; 0356 0357 if (m_stopCalculating) 0358 break; 0359 0360 // QDBusInterface( QDBus::sessionBus().baseService(), "/kmplot", "org.kde.kmplot.KmPlot" ).call( QDBus::Block, "setDrawProgress", at/numPlots ); 0361 0362 if (function->type() == Function::Implicit) 0363 drawImplicit(function, &painter); 0364 else 0365 drawFunction(function, &painter); 0366 } 0367 // QDBusInterface( QDBus::sessionBus().baseService(), "/kmplot", "org.kde.kmplot.KmPlot" ).call( QDBus::Block, "setDrawProgress", 1.0 ); 0368 0369 drawFunctionInfo(&painter); 0370 0371 m_isDrawing = false; 0372 // END draw the functions 0373 0374 // Reset are stuff back to the screen stuff 0375 initDrawing(&buffer, Screen); 0376 0377 updateCursor(); 0378 } 0379 0380 // BEGIN coordinate mapping functions 0381 QPointF View::toPixel(const QPointF &real, ClipBehaviour clipBehaviour, const QPointF &pixelIfNaN) 0382 { 0383 xclipflg = false; 0384 yclipflg = false; 0385 0386 QPointF pixel = m_realToPixel.map(real); 0387 double x = pixel.x(); 0388 double y = pixel.y(); 0389 0390 if (std::isnan(x)) { 0391 xclipflg = true; 0392 x = pixelIfNaN.x(); 0393 } else if (clipBehaviour == ClipAll) { 0394 if (x < 0) { 0395 xclipflg = true; 0396 x = 0; 0397 } else if (x > m_clipRect.right()) { 0398 xclipflg = true; 0399 x = m_clipRect.right(); 0400 } 0401 } else { 0402 if (std::isinf(x) && x < 0) 0403 x = 0; 0404 0405 else if (std::isinf(x) && x > 0) 0406 x = m_clipRect.right(); 0407 } 0408 0409 if (std::isnan(y)) { 0410 yclipflg = true; 0411 y = pixelIfNaN.y(); 0412 } else if (clipBehaviour == ClipAll) { 0413 if (y < 0) { 0414 yclipflg = true; 0415 y = 0; 0416 } else if (y > m_clipRect.bottom()) { 0417 yclipflg = true; 0418 y = m_clipRect.bottom(); 0419 } 0420 } else { 0421 if (std::isinf(y) && y < 0) 0422 y = 0; 0423 0424 else if (std::isinf(y) && y > 0) 0425 y = m_clipRect.bottom(); 0426 } 0427 0428 // Make sure that x and y are *reasonably* bounded at least, even if they're not infinite 0429 double min_x = -1e3 * m_clipRect.width(); 0430 double max_x = +1e3 * m_clipRect.width(); 0431 double min_y = -1e3 * m_clipRect.height(); 0432 double max_y = +1e3 * m_clipRect.height(); 0433 0434 if (x < min_x) 0435 x = min_x; 0436 else if (x > max_x) 0437 x = max_x; 0438 0439 if (y < min_y) 0440 y = min_y; 0441 else if (y > max_y) 0442 y = max_y; 0443 0444 return QPointF(x, y); 0445 } 0446 0447 double View::xToPixel(double x, ClipBehaviour clipBehaviour, double xIfNaN) 0448 { 0449 return toPixel(QPointF(x, 0), clipBehaviour, QPointF(xIfNaN, 0)).x(); 0450 } 0451 0452 double View::yToPixel(double y, ClipBehaviour clipBehaviour, double yIfNaN) 0453 { 0454 return toPixel(QPointF(0, y), clipBehaviour, QPointF(0, yIfNaN)).y(); 0455 } 0456 0457 QPointF View::toReal(const QPointF &pixel) 0458 { 0459 return m_pixelToReal.map(pixel); 0460 } 0461 0462 double View::xToReal(double x) 0463 { 0464 return toReal(QPointF(x, 0)).x(); 0465 } 0466 0467 double View::yToReal(double y) 0468 { 0469 return toReal(QPointF(0, y)).y(); 0470 } 0471 // END coordinate mapping functions 0472 0473 void View::drawAxes(QPainter *painter) 0474 { 0475 double axesLineWidth = millimetersToPixels(Settings::axesLineWidth(), painter->device()); 0476 double ticWidth = millimetersToPixels(Settings::ticWidth(), painter->device()); 0477 double ticLength = millimetersToPixels(Settings::ticLength(), painter->device()); 0478 QColor axesColor = Settings::axesColor(); 0479 0480 painter->save(); 0481 0482 double arrowWidth = ticLength * 1.4; 0483 double arrowLength = arrowWidth * 2.8; 0484 0485 painter->setPen(QPen(axesColor, axesLineWidth)); 0486 painter->setBrush(axesColor); 0487 0488 // BEGIN draw x axis 0489 double a = m_clipRect.right() - ticLength; 0490 double b = yToPixel(0.); 0491 0492 double b_max = m_clipRect.bottom() - ticLength; 0493 if (b < ticLength) 0494 b = ticLength; 0495 else if (b > b_max) 0496 b = b_max; 0497 0498 // horizontal line 0499 painter->Lineh(ticLength, b, a); 0500 0501 // arrow head 0502 if (Settings::showArrows()) { 0503 a = m_clipRect.right(); 0504 0505 QPolygonF p(3); 0506 p[0] = QPointF(a, b); 0507 p[1] = QPointF(a - arrowLength, b + arrowWidth); 0508 p[2] = QPointF(a - arrowLength, b - arrowWidth); 0509 painter->drawPolygon(p); 0510 } 0511 // END draw x axis 0512 0513 // BEGIN draw y axis 0514 a = xToPixel(0.); 0515 b = ticLength; 0516 0517 double a_max = m_clipRect.right() - ticLength; 0518 if (a < ticLength) 0519 a = ticLength; 0520 else if (a > a_max) 0521 a = a_max; 0522 0523 // vertical line 0524 painter->Linev(a, m_clipRect.bottom() - ticLength, b); 0525 0526 // arrow head 0527 if (Settings::showArrows()) { 0528 b = 0; 0529 0530 QPolygonF p(3); 0531 p[0] = QPointF(a, b); 0532 p[1] = QPointF(a - arrowWidth, b + arrowLength); 0533 p[2] = QPointF(a + arrowWidth, b + arrowLength); 0534 painter->drawPolygon(p); 0535 } 0536 // END draw y axis 0537 0538 painter->restore(); 0539 0540 painter->setPen(QPen(axesColor, ticWidth)); 0541 0542 double da = yToPixel(0) - ticLength; 0543 double db = yToPixel(0) + ticLength; 0544 double d = ticStartX; 0545 if (da < 0) { 0546 a = 0; 0547 b = 2 * ticLength; 0548 } else if (db > (double)m_clipRect.bottom()) { 0549 b = m_clipRect.bottom(); 0550 a = m_clipRect.bottom() - 2 * ticLength; 0551 } else { 0552 a = da; 0553 b = db; 0554 } 0555 0556 while (d < m_xmax - ticSepX.value() / 2.) { 0557 double d_pixel = xToPixel(d); 0558 if (d_pixel > ticLength) 0559 painter->Linev(xToPixel(d), a, b); 0560 d += ticSepX.value(); 0561 } 0562 0563 da = xToPixel(0) - ticLength; 0564 db = xToPixel(0) + ticLength; 0565 d = ticStartY; 0566 if (da < 0) { 0567 a = 0; 0568 b = 2 * ticLength; 0569 } else if (db > (double)m_clipRect.right()) { 0570 b = m_clipRect.right(); 0571 a = m_clipRect.right() - 2 * ticLength; 0572 } else { 0573 a = da; 0574 b = db; 0575 } 0576 0577 while (d < m_ymax - ticSepY.value() / 2.) { 0578 double d_pixel = yToPixel(d); 0579 if (d_pixel < m_clipRect.bottom() - ticLength) 0580 painter->Lineh(a, d_pixel, b); 0581 d += ticSepY.value(); 0582 } 0583 } 0584 0585 void View::drawGrid(QPainter *painter) 0586 { 0587 QColor gridColor = Settings::gridColor(); 0588 0589 double gridLineWidth = millimetersToPixels(Settings::gridLineWidth(), painter->device()); 0590 QPen pen(gridColor, gridLineWidth); 0591 0592 painter->setPen(pen); 0593 0594 enum GridStyle { GridNone, GridLines, GridCrosses, GridPolar }; 0595 GridStyle gridMode = (GridStyle)Settings::gridStyle(); 0596 0597 switch (gridMode) { 0598 case GridNone: 0599 break; 0600 0601 case GridLines: { 0602 for (double d = ticStartX; d <= m_xmax; d += ticSepX.value()) 0603 painter->Linev(xToPixel(d), m_clipRect.bottom(), 0); 0604 0605 for (double d = ticStartY; d <= m_ymax; d += ticSepY.value()) 0606 painter->Lineh(0, yToPixel(d), m_clipRect.right()); 0607 0608 break; 0609 } 0610 0611 case GridCrosses: { 0612 int const dx = 5; 0613 int const dy = 5; 0614 0615 for (double x = ticStartX; x < m_xmax; x += ticSepX.value()) { 0616 double a = xToPixel(x); 0617 for (double y = ticStartY; y < m_ymax; y += ticSepY.value()) { 0618 double b = yToPixel(y); 0619 painter->Lineh(a - dx, b, a + dx); 0620 painter->Linev(a, b - dy, b + dy); 0621 } 0622 } 0623 0624 break; 0625 } 0626 0627 case GridPolar: { 0628 // Note: 1.42 \approx sqrt(2) 0629 0630 double xMax = qMax(qAbs(m_xmin), qAbs(m_xmax)) * 1.42; 0631 double yMax = qMax(qAbs(m_ymin), qAbs(m_ymax)) * 1.42; 0632 double rMax = qMax(xMax, yMax); 0633 0634 // The furthest pixel away from the origin 0635 double pixelMax = qMax(xMax * m_realToPixel.m11(), yMax * m_realToPixel.m22()); 0636 0637 double ticSep = qMin(ticSepX.value(), ticSepY.value()); 0638 double r = ticSep; 0639 0640 while (r < rMax) { 0641 QRectF rect; 0642 rect.setTopLeft(toPixel(QPointF(-r, r), ClipInfinite)); 0643 rect.setBottomRight(toPixel(QPointF(r, -r), ClipInfinite)); 0644 painter->drawEllipse(rect); 0645 r += ticSep; 0646 } 0647 0648 for (double theta = 0; theta < 2.0 * M_PI; theta += M_PI / 12.0) { 0649 QPointF start = toPixel(QPointF(0, 0), ClipInfinite); 0650 QPointF end = start + QPointF(pixelMax * cos(theta), pixelMax * sin(theta)); 0651 0652 painter->drawLine(start, end); 0653 } 0654 0655 break; 0656 } 0657 } 0658 } 0659 0660 void View::drawLabels(QPainter *painter) 0661 { 0662 const QString xLabel = Settings::labelHorizontalAxis(); 0663 const QString yLabel = Settings::labelVerticalAxis(); 0664 0665 int const dx = 10; 0666 int const dy = 15; 0667 QFont const font = Settings::axesFont(); 0668 painter->setFont(font); 0669 m_textDocument->setDefaultFont(font); 0670 0671 double const x = xToPixel(0.); 0672 double const y = yToPixel(0.); 0673 0674 QRectF drawRect; 0675 0676 // Whether the x-axis is along the top of the view 0677 // and the y-axis is along the right edge of the view 0678 bool axesInTopRight = (m_ymax < ticSepY.value() && m_xmax < ticSepX.value()); 0679 0680 // for "x" label 0681 double endLabelWidth = 0; 0682 0683 int flags = Qt::AlignVCenter | Qt::TextDontClip | Qt::AlignRight; 0684 0685 // Draw x label 0686 if (axesInTopRight) 0687 drawRect = QRectF(xToPixel(m_xmax) - (3 * dx), y + dy, 0, 0); 0688 else if (m_ymin > -ticSepY.value()) 0689 drawRect = QRectF(xToPixel(m_xmax) - dx, y - dy, 0, 0); 0690 else 0691 drawRect = QRectF(xToPixel(m_xmax) - dx, y + dy, 0, 0); 0692 painter->drawText(drawRect, flags, xLabel); 0693 endLabelWidth = m_clipRect.right() - painter->boundingRect(drawRect, flags, xLabel).right(); 0694 0695 // Draw y label 0696 if (axesInTopRight) 0697 drawRect = QRectF(x - dx, yToPixel(m_ymax) + (2 * dy), 0, 0); 0698 else if (m_xmin > -ticSepX.value()) 0699 drawRect = QRectF(x + (2 * dx), yToPixel(m_ymax) + dy, 0, 0); 0700 else 0701 drawRect = QRectF(x - dx, yToPixel(m_ymax) + dy, 0, 0); 0702 painter->drawText(drawRect, flags, yLabel); 0703 0704 // Draw the numbers on the axes 0705 drawXAxisLabels(painter, pixelsToMillimeters(endLabelWidth, painter->device())); 0706 drawYAxisLabels(painter); 0707 } 0708 0709 /** 0710 * If \p d is a rational multiple of pi, then return a string appropriate for 0711 * displaying it in fraction form. 0712 */ 0713 QString tryPiFraction(double d, double sep) 0714 { 0715 // Avoid strange bug where get pi at large separation 0716 if (sep > 10) 0717 return QString(); 0718 0719 bool positive = d > 0; 0720 0721 d /= M_PI; 0722 if (!positive) 0723 d = -d; 0724 0725 if (d < 1e-2) 0726 return QString(); 0727 0728 // Try denominators from 1 to 6 0729 for (int denom = 1; denom <= 6; ++denom) { 0730 if (realModulo(d * denom, 1) > 1e-3 * sep) 0731 continue; 0732 0733 int num = qRound(d * denom); 0734 0735 QString s = positive ? "+" : QString(MinusSymbol); 0736 if (num != 1) 0737 s += QString::number(num); 0738 0739 s += PiSymbol; 0740 0741 if (denom != 1) 0742 s += '/' + QString::number(denom); 0743 0744 return s; 0745 } 0746 0747 return QString(); 0748 } 0749 0750 void View::drawXAxisLabels(QPainter *painter, double endLabelWidth_mm) 0751 { 0752 QColor axesColor = Settings::axesColor(); 0753 int const dy = 8; 0754 0755 double const y = yToPixel(0.); 0756 0757 // Used to ensure that labels aren't drawn too closely together 0758 // These numbers contain the pixel position of the left and right endpoints of the last label 0759 double last_x_start = -1e3; // (just a large negative number) 0760 double last_x_end = -1e3; // (just a large negative number) 0761 0762 // The strange label drawing order here is so that the labels eitherside 0763 // of zero are always drawn, and then the other labels are drawn if there 0764 // is enough space 0765 0766 bool first = true; 0767 bool forwards = true; 0768 double d = 0; 0769 0770 while (true) { 0771 if (first) { 0772 // Draw x>0 first 0773 d = qMax(ticSepX.value(), ticStartX); 0774 last_x_end = xToPixel(0); 0775 first = false; 0776 } else { 0777 if (forwards) { 0778 d += ticSepX.value(); 0779 if (d > m_xmax) { 0780 // Continue on other side 0781 d = qMin(-ticSepX.value(), ticStartX + floor((m_xmax - m_xmin) / ticSepX.value()) * ticSepX.value()); 0782 last_x_start = xToPixel(0); 0783 forwards = false; 0784 } 0785 } else { 0786 d -= ticSepX.value(); 0787 if (d < m_xmin) 0788 return; 0789 } 0790 } 0791 0792 // Don't draw too close to the left edge if the y axis is there 0793 if (m_xmin >= -ticSepX.value() && (d - m_xmin) <= ticSepX.value()) 0794 continue; 0795 0796 QString s = tryPiFraction(d, ticSepX.value()); 0797 0798 if (s.isEmpty()) 0799 s = posToString(d, ticSepX.value() * 5, View::ScientificFormat, axesColor).replace('.', QLocale().decimalPoint()); 0800 0801 m_textDocument->setHtml(s); 0802 double idealWidth = m_textDocument->idealWidth(); 0803 double idealHeight = m_textDocument->size().height(); 0804 0805 double x_pos = xToPixel(d) - (idealWidth / 2) - 4; 0806 if (x_pos < 0) 0807 continue; 0808 0809 double y_pos = y + dy; 0810 if ((y_pos + idealHeight) > m_clipRect.bottom()) 0811 y_pos = y - dy - idealHeight; 0812 0813 double x_start = x_pos; 0814 double x_end = x_start + idealWidth; 0815 0816 // Use a minimum spacing between labels 0817 if ((last_x_start < x_start) && pixelsToMillimeters(x_start - last_x_end, painter->device()) < 7) 0818 continue; 0819 if ((last_x_start > x_start) && pixelsToMillimeters(last_x_start - x_end, painter->device()) < 7) 0820 continue; 0821 0822 // Don't draw too close to the right edge (has x axis label) 0823 if (pixelsToMillimeters(m_clipRect.right() - x_end, painter->device()) < endLabelWidth_mm + 3) 0824 continue; 0825 0826 last_x_start = x_start; 0827 last_x_end = x_end; 0828 0829 QPointF drawPoint(x_pos, y_pos); 0830 painter->translate(drawPoint); 0831 m_textDocument->documentLayout()->draw(painter, QAbstractTextDocumentLayout::PaintContext()); 0832 painter->translate(-drawPoint); 0833 } 0834 } 0835 0836 void View::drawYAxisLabels(QPainter *painter) 0837 { 0838 QColor axesColor = Settings::axesColor(); 0839 int const dx = 12; 0840 0841 double const x = xToPixel(0.); 0842 0843 double d = ticStartY; 0844 long long n = (long long)ceil(m_ymin / ticSepY.value()); 0845 for (; d < m_ymax; d += ticSepY.value(), ++n) { 0846 // Don't draw zero 0847 if (n == 0) 0848 continue; 0849 0850 // Don't draw too close to top 0851 if ((m_ymax - d) <= ticSepY.value() * 0.6) 0852 continue; 0853 0854 // Don't draw too close to bottom if the x axis is there 0855 if (m_ymin > -ticSepY.value() && (d - m_ymin) <= ticSepY.value()) 0856 continue; 0857 0858 QString s = tryPiFraction(d, ticSepY.value()); 0859 0860 if (s.isEmpty()) 0861 s = posToString(d, ticSepY.value() * 5, View::ScientificFormat, axesColor).replace('.', QLocale().decimalPoint()); 0862 0863 m_textDocument->setHtml(s); 0864 0865 double idealWidth = m_textDocument->idealWidth(); 0866 double idealHeight = m_textDocument->size().height(); 0867 0868 QPointF drawPoint(0, yToPixel(d) - (idealHeight / 2)); 0869 0870 if (m_xmin > -ticSepX.value()) { 0871 drawPoint.setX(x + dx); 0872 } else { 0873 drawPoint.setX(x - dx - idealWidth); 0874 0875 if (drawPoint.x() < 0) { 0876 // Don't draw off the left edge of the screen 0877 drawPoint.setX(0); 0878 } 0879 } 0880 0881 // Shouldn't have the label cut off by the bottom of the view 0882 if (drawPoint.y() + idealHeight > m_clipRect.height()) 0883 continue; 0884 0885 painter->translate(drawPoint); 0886 m_textDocument->documentLayout()->draw(painter, QAbstractTextDocumentLayout::PaintContext()); 0887 painter->translate(-drawPoint); 0888 } 0889 } 0890 0891 double View::h(const Plot &plot) const 0892 { 0893 if ((plot.plotMode == Function::Integral) || (plot.function()->type() == Function::Differential)) 0894 return plot.function()->eq[0]->differentialStates.step().value(); 0895 0896 double dx = (m_xmax - m_xmin) / m_clipRect.width(); 0897 double dy = (m_ymax - m_ymin) / m_clipRect.height(); 0898 0899 switch (plot.function()->type()) { 0900 case Function::Cartesian: 0901 case Function::Differential: 0902 return dx; 0903 0904 case Function::Polar: 0905 case Function::Parametric: 0906 case Function::Implicit: 0907 return qMin(dx, dy); 0908 } 0909 0910 qWarning() << "Unknown coord\n"; 0911 return qMin(dx, dy); 0912 } 0913 0914 double View::value(const Plot &plot, int eq, double x, bool updateFunction) 0915 { 0916 Function *function = plot.function(); 0917 assert(function); 0918 0919 if (updateFunction) 0920 plot.updateFunction(); 0921 0922 Equation *equation = function->eq[eq]; 0923 0924 double dx = h(plot); 0925 DifferentialState *state = plot.state(); 0926 0927 return XParser::self()->derivative(plot.derivativeNumber(), equation, state, x, dx); 0928 } 0929 0930 QPointF View::realValue(const Plot &plot, double x, bool updateFunction) 0931 { 0932 Function *function = plot.function(); 0933 assert(function); 0934 0935 switch (function->type()) { 0936 case Function::Differential: 0937 case Function::Cartesian: { 0938 double y = value(plot, 0, x, updateFunction); 0939 return QPointF(x, y); 0940 } 0941 0942 case Function::Polar: { 0943 double y = value(plot, 0, x, updateFunction); 0944 return QPointF(y * lcos(x), y * lsin(x)); 0945 } 0946 0947 case Function::Parametric: { 0948 double X = value(plot, 0, x, updateFunction); 0949 double Y = value(plot, 1, x, updateFunction); 0950 return QPointF(X, Y); 0951 } 0952 0953 case Function::Implicit: { 0954 // Can only calculate the value when either x or y is fixed. 0955 assert(function->m_implicitMode != Function::UnfixedXY); 0956 0957 double val = value(plot, 0, x, updateFunction); 0958 0959 if (function->m_implicitMode == Function::FixedX) 0960 return QPointF(function->x, val); 0961 else 0962 return QPointF(val, function->y); 0963 } 0964 } 0965 0966 qWarning() << "Unknown function type!\n"; 0967 return QPointF(); 0968 } 0969 0970 double View::getXmin(Function *function, bool overlapEdge) 0971 { 0972 switch (function->type()) { 0973 case Function::Parametric: 0974 case Function::Polar: 0975 return function->dmin.value(); 0976 0977 case Function::Implicit: 0978 qWarning() << "You probably don't want to do this!\n"; 0979 // fall through 0980 0981 case Function::Differential: 0982 case Function::Cartesian: { 0983 double min = m_xmin; 0984 if (overlapEdge) 0985 min -= (m_xmax - m_xmin) * 0.02; 0986 0987 if (function->usecustomxmin) 0988 return qMax(min, function->dmin.value()); 0989 else 0990 return min; 0991 } 0992 } 0993 0994 return 0; 0995 } 0996 0997 double View::getXmax(Function *function, bool overlapEdge) 0998 { 0999 switch (function->type()) { 1000 case Function::Parametric: 1001 case Function::Polar: 1002 return function->dmax.value(); 1003 1004 case Function::Implicit: 1005 qWarning() << "You probably don't want to do this!\n"; 1006 // fall through 1007 1008 case Function::Differential: 1009 case Function::Cartesian: { 1010 double max = m_xmax; 1011 if (overlapEdge) 1012 max += (m_xmax - m_xmin) * 0.02; 1013 1014 if (function->usecustomxmax) 1015 return qMin(max, function->dmax.value()); 1016 else 1017 return max; 1018 } 1019 } 1020 1021 return 0; 1022 } 1023 1024 // #define DEBUG_IMPLICIT 1025 1026 #ifdef DEBUG_IMPLICIT 1027 // Used in profiling root finding 1028 int root_find_iterations; 1029 int root_find_requests; 1030 #endif 1031 1032 /** 1033 * For comparing points where two points close together are considered equal. 1034 */ 1035 class FuzzyPoint 1036 { 1037 public: 1038 FuzzyPoint(const QPointF &point) 1039 { 1040 x = point.x(); 1041 y = point.y(); 1042 } 1043 1044 FuzzyPoint(double x, double y) 1045 { 1046 FuzzyPoint::x = x; 1047 FuzzyPoint::y = y; 1048 } 1049 1050 bool operator<(const FuzzyPoint &other) const 1051 { 1052 double du = qAbs(other.x - x) / dx; 1053 double dv = qAbs(other.y - y) / dy; 1054 1055 bool x_eq = (du < 1); // Whether the x coordinates are considered equal 1056 bool y_eq = (dv < 1); // Whether the y coordinates are considered equal 1057 1058 if (x_eq && y_eq) { 1059 // Points are close together. 1060 return false; 1061 } 1062 1063 bool x_lt = !x_eq && (x < other.x); 1064 bool y_lt = !y_eq && (y < other.y); 1065 1066 return (x_lt || (x_eq && y_lt)); 1067 } 1068 1069 double x; 1070 double y; 1071 1072 static double dx; 1073 static double dy; 1074 }; 1075 typedef QMap<FuzzyPoint, QPointF> FuzzyPointMap; 1076 1077 double FuzzyPoint::dx = 0; 1078 double FuzzyPoint::dy = 0; 1079 1080 double SegmentMin = 0.1; 1081 double SegmentMax = 6.0; 1082 1083 // The viewable area is divided up into square*squares squares, and the curve 1084 // is traced around in each square. 1085 // NOTE: it is generally a good idea to make this number prime 1086 int squares = 19; 1087 1088 void View::drawImplicit(Function *function, QPainter *painter) 1089 { 1090 assert(function->type() == Function::Implicit); 1091 1092 #ifdef DEBUG_IMPLICIT 1093 QTime t; 1094 t.start(); 1095 1096 painter->setPen(Qt::black); 1097 1098 for (double i = 0; i <= squares; ++i) { 1099 double x = m_xmin + i * (m_xmax - m_xmin) / squares; 1100 double y = m_ymin + i * (m_ymax - m_ymin) / squares; 1101 1102 painter->drawLine(toPixel(QPointF(m_xmin, y), ClipInfinite), toPixel(QPointF(m_xmax, y), ClipInfinite)); 1103 painter->drawLine(toPixel(QPointF(x, m_ymin), ClipInfinite), toPixel(QPointF(x, m_ymax), ClipInfinite)); 1104 } 1105 1106 root_find_iterations = 0; 1107 root_find_requests = 0; 1108 #endif 1109 1110 // Need another function for investigating singular points 1111 Plot circular; 1112 QString fname("f(x)=0"); 1113 XParser::self()->fixFunctionName(fname, Equation::Cartesian, -1); 1114 circular.setFunctionID(XParser::self()->Parser::addFunction(fname, 0, Function::Cartesian)); 1115 assert(circular.function()); 1116 1117 const QList<Plot> plots = function->plots(); 1118 for (const Plot &plot : plots) { 1119 bool setAliased = false; 1120 if (plot.parameter.type() == Parameter::Animated) { 1121 // Don't use antialiasing, so that rendering is sped up 1122 if (painter->renderHints() & QPainter::Antialiasing) { 1123 setAliased = true; 1124 painter->setRenderHint(QPainter::Antialiasing, false); 1125 } 1126 } 1127 1128 painter->setPen(penForPlot(plot, painter)); 1129 1130 QList<QPointF> singular; 1131 1132 for (int i = 0; i <= squares; ++i) { 1133 double y = m_ymin + i * (m_ymax - m_ymin) / double(squares); 1134 1135 function->y = y; 1136 function->m_implicitMode = Function::FixedY; 1137 QList<double> roots = findRoots(plot, m_xmin, m_xmax, RoughRoot); 1138 1139 for (double x : qAsConst(roots)) { 1140 #ifdef DEBUG_IMPLICIT 1141 painter->setPen(QPen(Qt::red, painter->pen().width())); 1142 #endif 1143 drawImplicitInSquare(plot, painter, x, y, Qt::Horizontal, &singular); 1144 } 1145 1146 double x = m_xmin + i * (m_xmax - m_xmin) / double(squares); 1147 1148 function->x = x; 1149 function->m_implicitMode = Function::FixedX; 1150 roots = findRoots(plot, m_ymin, m_ymax, RoughRoot); 1151 1152 for (double y : qAsConst(roots)) { 1153 #ifdef DEBUG_IMPLICIT 1154 painter->setPen(QPen(Qt::blue, painter->pen().width())); 1155 #endif 1156 drawImplicitInSquare(plot, painter, x, y, Qt::Vertical, &singular); 1157 } 1158 } 1159 1160 // Sort out the implicit points 1161 FuzzyPointMap singularSorted; 1162 FuzzyPoint::dx = (m_xmax - m_xmin) * SegmentMin * 0.1 / m_clipRect.width(); 1163 FuzzyPoint::dy = (m_ymax - m_ymin) * SegmentMin * 0.1 / m_clipRect.height(); 1164 for (const QPointF &point : qAsConst(singular)) 1165 singularSorted.insert(point, point); 1166 singular = singularSorted.values(); 1167 1168 for (const QPointF &point : qAsConst(singular)) { 1169 // radius of circle around singular point 1170 double epsilon = qMin(FuzzyPoint::dx, FuzzyPoint::dy); 1171 1172 QString fstr; 1173 fstr = QString("%1(x)=%2(%3+%6*cos(x),%4+%6*sin(x)%5)") 1174 .arg(circular.function()->eq[0]->name()) 1175 .arg(function->eq[0]->name()) 1176 .arg(XParser::self()->number(point.x())) 1177 .arg(XParser::self()->number(point.y())) 1178 .arg(function->eq[0]->usesParameter() ? ',' + XParser::self()->number(function->k) : QString()) 1179 .arg(XParser::self()->number(epsilon)); 1180 1181 bool setFstrOk = circular.function()->eq[0]->setFstr(fstr); 1182 qDebug() << "------------ " << setFstrOk; 1183 assert(setFstrOk); 1184 1185 QList<double> roots = findRoots(circular, 0, 2 * M_PI / XParser::self()->radiansPerAngleUnit(), PreciseRoot); 1186 1187 #ifdef DEBUG_IMPLICIT 1188 qDebug() << "Singular point at (x,y)=(" << point.x() << ',' << point.y() << ")\n"; 1189 qDebug() << "fstr is " << fstr; 1190 qDebug() << "Found " << roots.size() << " roots.\n"; 1191 #endif 1192 1193 for (double t : qAsConst(roots)) { 1194 #ifdef DEBUG_IMPLICIT 1195 painter->setPen(QPen(Qt::green, painter->pen().width())); 1196 #endif 1197 double x = point.x() + epsilon * lcos(t); 1198 double y = point.y() + epsilon * lsin(t); 1199 drawImplicitInSquare(plot, painter, x, y, {}, &singular); 1200 } 1201 } 1202 1203 if (setAliased) 1204 painter->setRenderHint(QPainter::Antialiasing, true); 1205 } 1206 1207 #ifdef DEBUG_IMPLICIT 1208 if (root_find_requests != 0) 1209 qDebug() << "Average iterations in root finding was " << root_find_iterations / root_find_requests; 1210 qDebug() << "Time taken was " << t.elapsed(); 1211 #endif 1212 1213 XParser::self()->removeFunction(circular.functionID()); 1214 } 1215 1216 // static 1217 double View::maxSegmentLength(double curvature) 1218 { 1219 // Use a circle angle of 4 degrees to determine the maximum segment length 1220 // Also, limit the length to be between 0.1 and 6 pixels. 1221 1222 double arc = 4 * (M_PI / 180); 1223 1224 if (curvature < 0) 1225 curvature = -curvature; 1226 1227 if (curvature < 1e-20) 1228 return SegmentMax; // very large circle 1229 1230 double radius = 1.0 / curvature; 1231 1232 double segment = arc * radius; 1233 if (segment < SegmentMin) 1234 segment = SegmentMin; 1235 else if (segment > SegmentMax) 1236 segment = SegmentMax; 1237 1238 return segment; 1239 } 1240 1241 void View::drawImplicitInSquare(const Plot &plot, QPainter *painter, double x, double y, Qt::Orientations orientation, QList<QPointF> *singular) 1242 { 1243 plot.updateFunction(); 1244 Plot diff1 = plot; 1245 diff1.differentiate(); 1246 Plot diff2 = diff1; 1247 diff2.differentiate(); 1248 1249 #ifdef DEBUG_IMPLICIT 1250 painter->save(); 1251 painter->setPen(QPen(Qt::black, painter->pen().width())); 1252 QPointF tl = toPixel(QPointF(x, y), ClipInfinite) - QPoint(2, 2); 1253 painter->drawRect(QRectF(tl, QSizeF(4, 4))); 1254 painter->restore(); 1255 #endif 1256 1257 double x_side = (m_xmax - m_xmin) / squares; 1258 double y_side = (m_ymax - m_ymin) / squares; 1259 1260 // Use a square around the root to bound the tracing 1261 // To start with, assume that tracing will go up,right. But this 1262 // might not be so, so the upper/lower boundaries may be adjusted depending 1263 // on where the tracing ends up 1264 double x_lower, x_upper, y_lower, y_upper; 1265 if (orientation & Qt::Vertical) { 1266 x_lower = x; 1267 x_upper = x + x_side; 1268 } else { 1269 double x_prop = (x - m_xmin) / (m_xmax - m_xmin); 1270 x_lower = std::floor(x_prop * squares) * x_side + m_xmin; 1271 x_upper = x_lower + x_side; 1272 } 1273 if (orientation & Qt::Horizontal) { 1274 y_lower = y; 1275 y_upper = y + y_side; 1276 } else { 1277 double y_prop = (y - m_ymin) / (m_ymax - m_ymin); 1278 y_lower = std::floor(y_prop * squares) * y_side + m_ymin; 1279 y_upper = y_lower + y_side; 1280 } 1281 1282 // If during tracing, the root could not be found, then this will be set to true, 1283 // the route will be retraced using a smaller step size and it will attempt to find 1284 // a root again. If it fails for a second time, then tracing is finished. 1285 bool foundRootPreviously = true; 1286 1287 // Used for focal points. 1288 double prevAngle = 0; 1289 int switchCount = 0; 1290 1291 // This is so that the algorithm can "look ahead" to see what is coming up, 1292 // before drawing or committing itself to anything potentially bad 1293 QPointF prev2 = toPixel(QPointF(x, y), ClipInfinite); 1294 QPointF prev1 = prev2; 1295 1296 // Allow us to doubly retrace 1297 double prev_diff_x = 0; 1298 double prev_diff_y = 0; 1299 1300 for (int i = 0; i < 500; ++i) // allow a maximum of 500 traces (to prevent possibly infinite loop) 1301 { 1302 if (i == 500 - 1) { 1303 qDebug() << "Implicit: got to last iteration!\n"; 1304 } 1305 1306 // (dx, dy) is perpendicular to curve 1307 1308 plot.function()->x = x; 1309 plot.function()->y = y; 1310 1311 plot.function()->m_implicitMode = Function::FixedY; 1312 double dx = value(diff1, 0, x, false); 1313 1314 plot.function()->m_implicitMode = Function::FixedX; 1315 double dy = value(diff1, 0, y, false); 1316 1317 double k = pixelCurvature(plot, x, y); 1318 double segment_step = maxSegmentLength(k) * pow(0.5, switchCount); 1319 1320 // If we couldn't find a root in the previous iteration, it was possibly 1321 // because we were using too large a step size. So reduce the step size 1322 // and try again. 1323 if (!foundRootPreviously) 1324 segment_step = qMin(segment_step / 4, SegmentMin); 1325 1326 // qDebug() << "k="<<k<<" segment_step="<<segment_step; 1327 1328 QPointF p1 = toPixel(QPointF(x, y), ClipInfinite) * painter->transform(); 1329 QPointF p2 = toPixel(QPointF(x + dx, y + dy), ClipInfinite) * painter->transform(); 1330 double l = QLineF(p1, p2).length() / segment_step; 1331 1332 if (l == 0) { 1333 qDebug() << "length is zero!\n"; 1334 break; 1335 } 1336 1337 // (tx, ty) is tangent to the curve in the direction that we are tracing 1338 double tx = -dy / l; 1339 double ty = dx / l; 1340 1341 double angle = atan(ty / tx) + ((tx < 0) ? M_PI : 0); 1342 double diff = realModulo(angle - prevAngle, 2 * M_PI); 1343 1344 bool switchedDirection = (i > 0) && (diff > (3. / 4.) * M_PI) && (diff < (5. / 4.) * M_PI); 1345 if (switchedDirection) { 1346 // Why do I care about suddenly changing the direction? 1347 // Because the chances are, a attracting or repelling point has been reached. 1348 // Even if not, it suggests that a smaller step size is needed. If we have 1349 // switched direction and are already at the smallest step size, then note 1350 // the dodgy point for further investigation and give up for now 1351 1352 // qDebug() << "Switched direction: x="<<x<<" switchCount="<<switchCount<<" segment_step="<<segment_step<<" i="<<i; 1353 1354 // Use a step size much smaller than segment min to obtain good accuracy, 1355 // needed for investigating the point further 1356 if (segment_step <= SegmentMin * 0.01) { 1357 // Give up. Tell our parent function to investigate the point further 1358 *singular << QPointF(x, y); 1359 break; 1360 } 1361 1362 // Rewind the last tangent addition as well 1363 x -= prev_diff_x; 1364 y -= prev_diff_y; 1365 1366 prev_diff_x = 0; 1367 prev_diff_y = 0; 1368 1369 switchCount += 2; 1370 continue; 1371 } else { 1372 // Reset the stepping adjustment 1373 switchCount = qMax(0, switchCount - 1); 1374 prevAngle = angle; 1375 // qDebug() << "Didn't switch - x="<<x<<" segment_step="<<segment_step; 1376 } 1377 1378 if (i == 0) { 1379 // First trace; does the bounding square need adjusting? 1380 1381 if ((tx < 0) && (orientation & Qt::Vertical)) { 1382 x_lower -= x_side; 1383 x_upper -= x_side; 1384 } 1385 1386 if ((ty < 0) && (orientation & Qt::Horizontal)) { 1387 y_lower -= y_side; 1388 y_upper -= y_side; 1389 } 1390 } 1391 1392 // The maximum tangent length before we end up outside our bounding square 1393 double max_tx, max_ty; 1394 if (tx > 0) 1395 max_tx = x_upper - x; 1396 else 1397 max_tx = x - x_lower; 1398 if (ty > 0) 1399 max_ty = y_upper - y; 1400 else 1401 max_ty = y - y_lower; 1402 1403 // Does (tx,ty) need to be scaled to make sure the tangent stays inside the square? 1404 double scale = qMax((tx == 0) ? 0 : qAbs(tx) / max_tx, (ty == 0) ? 0 : qAbs(ty) / max_ty); 1405 bool outOfBounds = scale > 1; 1406 if (outOfBounds) { 1407 tx /= scale; 1408 ty /= scale; 1409 } 1410 1411 double x0 = x; 1412 double y0 = y; 1413 1414 x += tx; 1415 y += ty; 1416 1417 plot.function()->x = x; 1418 plot.function()->y = y; 1419 1420 double *coord = 0; 1421 if (qAbs(tx) > qAbs(ty)) { 1422 plot.function()->m_implicitMode = Function::FixedX; 1423 coord = &y; 1424 } else { 1425 plot.function()->m_implicitMode = Function::FixedY; 1426 coord = &x; 1427 } 1428 1429 bool found = findRoot(coord, plot, RoughRoot); 1430 if (!found) { 1431 if (foundRootPreviously) { 1432 #ifdef DEBUG_IMPLICIT 1433 qDebug() << "Could not find root!\n"; 1434 #endif 1435 1436 // Retrace our steps 1437 x = x0; 1438 y = y0; 1439 prev_diff_x = 0; 1440 prev_diff_y = 0; 1441 foundRootPreviously = false; 1442 continue; 1443 } else { 1444 qDebug() << "Couldn't find root - giving up.\n"; 1445 break; 1446 } 1447 } else 1448 foundRootPreviously = true; 1449 1450 prev_diff_x = x - x0; 1451 prev_diff_y = y - y0; 1452 1453 painter->drawLine(prev2, prev1); 1454 prev2 = prev1; 1455 prev1 = toPixel(QPointF(x, y), ClipInfinite); 1456 markDiagramPointUsed(prev1); 1457 1458 if (outOfBounds) 1459 break; 1460 } 1461 1462 // and the final line 1463 painter->drawLine(prev2, prev1); 1464 } 1465 1466 void View::drawFunction(Function *function, QPainter *painter) 1467 { 1468 if ((function->type() == Function::Differential) && (function->eq[0]->order() == 1) && function->plotAppearance(Function::Derivative0).showTangentField) { 1469 const QList<Plot> plots = function->plots(Function::PlotCombinations(Function::AllCombinations) & ~Function::DifferentInitialStates); 1470 for (const Plot &plot : plots) 1471 drawTangentField(plot, painter); 1472 } 1473 1474 const QList<Plot> plots = function->plots(); 1475 for (const Plot &plot : plots) 1476 drawPlot(plot, painter); 1477 } 1478 1479 void View::drawTangentField(const Plot &plot, QPainter *painter) 1480 { 1481 plot.updateFunction(); 1482 Function *function = plot.function(); 1483 1484 assert(function->type() == Function::Differential); 1485 // Can only draw tangent fields for first order differential equations 1486 assert(function->eq[0]->order() == 1); 1487 1488 painter->setPen(penForPlot(plot, painter)); 1489 1490 bool useParameter = function->eq[0]->usesParameter(); 1491 Vector v(useParameter ? 3 : 2); 1492 1493 if (useParameter) 1494 v[1] = function->k; 1495 1496 // For converting from real to pixels 1497 double sx = m_clipRect.width() / (m_xmax - m_xmin); 1498 double sy = m_clipRect.height() / (m_ymax - m_ymin); 1499 1500 for (double x = ticStartX; x <= m_xmax; x += ticSepX.value()) { 1501 v[0] = x; 1502 for (double y = ticStartY; y <= m_ymax; y += ticSepY.value()) { 1503 v[useParameter ? 2 : 1] = y; 1504 1505 double df = XParser::self()->fkt(function->eq[0], v) * (sy / sx); 1506 double theta = std::atan(df); 1507 double dx = std::cos(theta) * (ticSepX.value() / 8.0); 1508 double dy = std::sin(theta) * (ticSepY.value() / 8.0); 1509 1510 QPointF mid(x, y); 1511 QPointF diff(dx, dy); 1512 1513 painter->drawLine(toPixel(mid - diff), toPixel(mid + diff)); 1514 } 1515 } 1516 } 1517 1518 /** 1519 * Convenience function for drawing lines. Unfortunately, QPainter::drawPolyline 1520 * takes a long time to draw the line joins, which is only necessary when we are 1521 * using a fat pen. Therefore, draw each line individually if we are using a 1522 * thin pen to save time. 1523 */ 1524 void drawPolyline(QPainter *painter, const QPolygonF &points) 1525 { 1526 if (painter->pen().width() > 5) 1527 painter->drawPolyline(points); 1528 else if (points.size() >= 2) { 1529 QPointF prev = points.first(); 1530 for (int i = 1; i < points.size(); ++i) { 1531 // QPen pen( painter->pen() ); 1532 // pen.setColor( (i%2==0) ? Qt::red : Qt::blue ); 1533 // painter->setPen( pen ); 1534 1535 QPointF next = points[i]; 1536 painter->drawLine(prev, next); 1537 prev = next; 1538 } 1539 } 1540 } 1541 1542 /** 1543 * Speed up drawing by only drawing one line between each straightish section of the curve 1544 * These variable are used to determine when the curve can no longer be approximate by a 1545 * straight line as the new angle has changed too much 1546 */ 1547 class CurveApproximator 1548 { 1549 public: 1550 CurveApproximator(const QPolygonF &points) 1551 { 1552 assert(points.size() >= 2); 1553 reset(); 1554 1555 QPointF diff = points[points.size() - 2] - points.last(); 1556 currentAngle = atan2(diff.y(), diff.x()); 1557 approximatingCurve = true; 1558 } 1559 1560 CurveApproximator() 1561 { 1562 reset(); 1563 } 1564 1565 void reset() 1566 { 1567 currentAngle = 0; 1568 maxClockwise = 0; 1569 maxAnticlockwise = 0; 1570 maxDistance = 0; 1571 approximatingCurve = false; 1572 } 1573 1574 bool shouldDraw() const 1575 { 1576 return ((maxAnticlockwise + maxClockwise) * maxDistance) >= 0.5; 1577 } 1578 1579 void update(const QPolygonF &points) 1580 { 1581 // Should have at least two points in the list 1582 assert(points.size() >= 2); 1583 1584 QPointF p1 = points[points.size() - 2]; 1585 QPointF p2 = points.last(); 1586 1587 QPointF diff = p1 - p2; 1588 double angle = atan2(diff.y(), diff.x()); 1589 1590 double lineLength = QLineF(p1, p2).length(); 1591 if (lineLength > maxDistance) 1592 maxDistance = lineLength; 1593 1594 double clockwise = realModulo(currentAngle - angle, 2 * M_PI); 1595 double anticlockwise = realModulo(angle - currentAngle, 2 * M_PI); 1596 1597 bool goingClockwise = (clockwise < anticlockwise); 1598 1599 if (goingClockwise) { 1600 // anti-clockwise 1601 if (clockwise > maxClockwise) 1602 maxClockwise = clockwise; 1603 } else { 1604 // clockwise 1605 if (anticlockwise > maxAnticlockwise) 1606 maxAnticlockwise = anticlockwise; 1607 } 1608 } 1609 1610 double currentAngle; 1611 double maxClockwise; 1612 double maxAnticlockwise; 1613 double maxDistance; 1614 bool approximatingCurve; 1615 }; 1616 1617 void View::drawPlot(const Plot &plot, QPainter *painter) 1618 { 1619 plot.updateFunction(); 1620 Function *function = plot.function(); 1621 1622 // should use drawImplicit for implicit functions 1623 assert(function->type() != Function::Implicit); 1624 1625 double dmin = getXmin(function, true); 1626 double dmax = getXmax(function, true); 1627 1628 if (dmin >= dmax) 1629 return; 1630 1631 painter->save(); 1632 1633 // Bug in Qt 4.2 TP - QPainter::drawPolyline draws the background as well while printing 1634 // So for testing printing, use a brush where one can see the function being drawn 1635 painter->setBrush(Qt::white); 1636 1637 if ((plot.parameter.type() == Parameter::Animated) && (painter->renderHints() & QPainter::Antialiasing)) { 1638 // Don't use antialiasing, so that rendering is speeded up 1639 painter->setRenderHint(QPainter::Antialiasing, false); 1640 } 1641 1642 painter->setPen(penForPlot(plot, painter)); 1643 1644 // the 'middle' dx, which may be increased or decreased 1645 double max_dx = (dmax - dmin) / m_clipRect.width(); 1646 if ((function->type() == Function::Parametric) || (function->type() == Function::Polar)) 1647 max_dx *= 0.01; 1648 1649 // Increase speed while translating the view 1650 bool quickDraw = (m_zoomMode == Translating); 1651 if (quickDraw) 1652 max_dx *= 4.0; 1653 1654 double dx = max_dx; 1655 1656 double maxLength = quickDraw ? 8.0 : (function->plotAppearance(plot.plotMode).style == Qt::SolidLine) ? 4.0 : 1.5; 1657 double minLength = maxLength * 0.5; 1658 1659 bool drawIntegral = m_integralDrawSettings.draw && (m_integralDrawSettings.plot == plot); 1660 double totalLength = 0.0; // total pixel length; used for drawing dotted lines 1661 1662 bool p1Set = false; 1663 QPointF p1, p2; 1664 1665 CurveApproximator approximator; 1666 QPolygonF drawPoints; 1667 1668 double x = dmin; 1669 double prevX = x; // the value of x before last adding dx to it 1670 do { 1671 QPointF rv = realValue(plot, x, false); 1672 1673 // If we are currently plotting a differential equation, and it became infinite, 1674 // then skip x forward to a point where it is finite 1675 if (function->type() == Function::Differential && !XParser::self()->differentialFinite) { 1676 double new_x = XParser::self()->differentialDiverge; 1677 if (new_x > x) { 1678 x = new_x; 1679 prevX = x; 1680 1681 continue; 1682 } 1683 } 1684 1685 p2 = toPixel(rv, ClipInfinite); 1686 1687 if (xclipflg || yclipflg) { 1688 prevX = x; 1689 x += dx; 1690 1691 p1Set = false; // p1 wouldn't be finite (if we had set it) 1692 continue; 1693 } 1694 1695 if (!p1Set) { 1696 prevX = x; 1697 x += dx; 1698 1699 p1 = p2; 1700 p1Set = true; 1701 continue; 1702 } 1703 1704 // BEGIN adjust dx 1705 QRectF bound = QRectF(p1, QSizeF((p2 - p1).x(), (p2 - p1).y())).normalized(); 1706 double length = QLineF(p1, p2).length(); 1707 totalLength += length; 1708 1709 double min_mod = (function->type() == Function::Cartesian || function->type() == Function::Differential) ? 1e-2 : 5e-4; 1710 bool dxAtMinimum = (dx <= max_dx * min_mod); 1711 bool dxAtMaximum = (dx >= max_dx); 1712 bool dxTooBig = false; 1713 bool dxTooSmall = false; 1714 1715 if (QRectF(m_clipRect).intersects(bound)) { 1716 dxTooBig = !dxAtMinimum && (length > maxLength); 1717 dxTooSmall = !dxAtMaximum && (length < minLength); 1718 } else 1719 dxTooSmall = !dxAtMaximum; 1720 1721 if (dxTooBig) { 1722 dx *= 0.5; 1723 x = prevX + dx; 1724 totalLength -= length; 1725 continue; 1726 } 1727 1728 if (dxTooSmall) 1729 dx *= 2.0; 1730 // END adjust dx 1731 1732 if (drawIntegral && (x >= m_integralDrawSettings.dmin) && (x <= m_integralDrawSettings.dmax)) { 1733 double y0 = yToPixel(0); 1734 1735 /// \todo should draw the shape in one go 1736 1737 QPointF points[4]; 1738 points[0] = QPointF(p1.x(), y0); 1739 points[1] = QPointF(p2.x(), y0); 1740 points[2] = QPointF(p2.x(), p2.y()); 1741 points[3] = QPointF(p1.x(), p1.y()); 1742 1743 painter->drawPolygon(points, 4); 1744 } else if (penShouldDraw(totalLength, plot)) { 1745 if (drawPoints.isEmpty()) { 1746 drawPoints << p1; 1747 } else if (drawPoints.last() != p1) { 1748 drawPolyline(painter, drawPoints); 1749 drawPoints.clear(); 1750 drawPoints << p1; 1751 approximator.reset(); 1752 } 1753 1754 // The above code should guarantee that drawPoints isn't empty 1755 // But check it now in case I do something stupid 1756 assert(!drawPoints.isEmpty()); 1757 1758 if (!approximator.approximatingCurve) { 1759 // Cool, about to add another point. This defines the working angle of the line 1760 // approximation 1761 drawPoints << p2; 1762 approximator = CurveApproximator(drawPoints); 1763 } else { 1764 QPointF prev = drawPoints.last(); 1765 drawPoints.last() = p2; 1766 approximator.update(drawPoints); 1767 1768 // Allow a maximum deviation (in pixels) 1769 if (approximator.shouldDraw()) { 1770 // The approximation is too bad; will have to start again now 1771 drawPoints.last() = prev; 1772 drawPoints << p2; 1773 approximator = CurveApproximator(drawPoints); 1774 } 1775 } 1776 } 1777 1778 markDiagramPointUsed(p2); 1779 1780 p1 = p2; 1781 1782 Q_ASSERT(dx > 0); 1783 prevX = x; 1784 x += dx; 1785 } while (x <= dmax); 1786 1787 // qDebug() << "drawPoints.size()="<<drawPoints.size(); 1788 drawPolyline(painter, drawPoints); 1789 1790 painter->restore(); 1791 } 1792 1793 void View::drawFunctionInfo(QPainter *painter) 1794 { 1795 // Don't draw info if translating the view 1796 if (m_zoomMode == Translating) 1797 return; 1798 1799 // The names of the plots are drawn around the edge of the view, in a clockwise 1800 // direction, starting from the top-right. Picture the positions like this: 1801 // 1802 // 7 8 9 0 1803 // 6 1 1804 // 5 4 3 2 1805 1806 // Used for determining where to draw the next label indicating the plot name 1807 int plotNameAt = 0; 1808 1809 for (Function *function : qAsConst(XParser::self()->m_ufkt)) { 1810 if (m_stopCalculating) 1811 break; 1812 1813 for (const Plot &plot : function->plots()) { 1814 plot.updateFunction(); 1815 1816 // Draw extrema points? 1817 if ((function->type() == Function::Cartesian) && function->plotAppearance(plot.plotMode).showExtrema) { 1818 const QList<QPointF> stationaryPoints = findStationaryPoints(plot); 1819 for (const QPointF &realValue : stationaryPoints) { 1820 painter->setPen(QPen(Qt::black, millimetersToPixels(1.5, painter->device()))); 1821 painter->drawPoint(toPixel(realValue)); 1822 1823 QString x = posToString(realValue.x(), (m_xmax - m_xmin) / m_clipRect.width(), View::DecimalFormat); 1824 QString y = posToString(realValue.y(), (m_ymax - m_ymin) / m_clipRect.width(), View::DecimalFormat); 1825 1826 drawLabel(painter, 1827 plot.color(), 1828 realValue, 1829 i18nc("Extrema point", "x = %1 y = %2", x.replace('.', QLocale().decimalPoint()), y.replace('.', QLocale().decimalPoint()))); 1830 } 1831 } 1832 1833 // Show the name of the plot? 1834 if (function->plotAppearance(plot.plotMode).showPlotName) { 1835 double x, y; 1836 1837 double xmin = m_xmin + 0.1 * (m_xmax - m_xmin); 1838 double xmax = m_xmax - 0.1 * (m_xmax - m_xmin); 1839 double ymin = m_ymin + 0.1 * (m_ymax - m_ymin); 1840 double ymax = m_ymax - 0.1 * (m_ymax - m_ymin); 1841 1842 // Find out where on the outer edge of the view to draw it 1843 if (0 <= plotNameAt && plotNameAt <= 2) { 1844 x = xmax; 1845 y = ymax - (ymax - ymin) * plotNameAt / 2; 1846 } else if (3 <= plotNameAt && plotNameAt <= 5) { 1847 x = xmax - (xmax - xmin) * (plotNameAt - 2) / 3; 1848 y = ymin; 1849 } else if (6 <= plotNameAt && plotNameAt <= 7) { 1850 x = xmin; 1851 y = ymin + (ymax - ymin) * (plotNameAt - 5) / 2; 1852 } else { 1853 x = xmin + (xmax - xmin) * (plotNameAt - 7) / 3; 1854 y = ymax; 1855 } 1856 1857 plotNameAt = (plotNameAt + 1) % 10; 1858 1859 QPointF realPos; 1860 1861 if (function->type() == Function::Implicit) { 1862 findRoot(&x, &y, plot, RoughRoot); 1863 realPos = QPointF(x, y); 1864 } else { 1865 double t = getClosestPoint(QPointF(x, y), plot); 1866 realPos = realValue(plot, t, false); 1867 } 1868 1869 // If the closest point isn't in the view, then don't draw the label 1870 if (realPos.x() < m_xmin || realPos.x() > m_xmax || realPos.y() < m_ymin || realPos.y() > m_ymax) 1871 continue; 1872 1873 drawLabel(painter, plot.color(), realPos, plot.name()); 1874 } 1875 } 1876 } 1877 } 1878 1879 void View::drawLabel(QPainter *painter, const QColor &color, const QPointF &realPos, const QString &text) 1880 { 1881 QPalette palette; 1882 QColor outline = color; 1883 QColor background = outline.lighter(500); 1884 background.setAlpha(127); 1885 1886 QPointF pixelCenter = toPixel(realPos); 1887 QRectF rect(pixelCenter, QSizeF(1, 1)); 1888 1889 painter->setFont(m_labelFont); 1890 int flags = Qt::TextSingleLine | Qt::AlignLeft | Qt::AlignTop; 1891 rect = painter->boundingRect(rect, flags, text).adjusted(-7, -3, 4, 2); 1892 1893 // Try and find a nice place for inserting the rectangle 1894 int bestCost = int(1e7); 1895 QPointF bestCenter = realPos; 1896 for (double x = pixelCenter.x() - 300; x <= pixelCenter.x() + 300; x += 20) { 1897 for (double y = pixelCenter.y() - 300; y <= pixelCenter.y() + 300; y += 20) { 1898 QPointF center(x, y); 1899 rect.moveCenter(center); 1900 double length = (x - pixelCenter.x()) * (x - pixelCenter.x()) + (y - pixelCenter.y()) * (y - pixelCenter.y()); 1901 int cost = rectCost(rect) + int(length) / 100; 1902 1903 if (cost < bestCost) { 1904 bestCenter = center; 1905 bestCost = cost; 1906 } 1907 } 1908 } 1909 1910 rect.moveCenter(bestCenter); 1911 1912 markDiagramAreaUsed(rect); 1913 1914 painter->setBrush(background); 1915 painter->setPen(outline); 1916 painter->drawRoundedRect(rect, int(1000 / rect.width()), int(1000 / rect.height())); 1917 1918 // If the rectangle does not lie over realPos, then draw a line to realPos from the rectangle 1919 if (!rect.contains(pixelCenter)) { 1920 QPointF lineStart = bestCenter; 1921 QLineF line(pixelCenter, bestCenter); 1922 1923 QPointF intersect = bestCenter; 1924 1925 // Where does line intersect the rectangle? 1926 if (QLineF(rect.topLeft(), rect.topRight()).intersects(line, &intersect) == QLineF::BoundedIntersection) 1927 lineStart = intersect; 1928 else if (QLineF(rect.topRight(), rect.bottomRight()).intersects(line, &intersect) == QLineF::BoundedIntersection) 1929 lineStart = intersect; 1930 else if (QLineF(rect.bottomRight(), rect.bottomLeft()).intersects(line, &intersect) == QLineF::BoundedIntersection) 1931 lineStart = intersect; 1932 else if (QLineF(rect.bottomLeft(), rect.topLeft()).intersects(line, &intersect) == QLineF::BoundedIntersection) 1933 lineStart = intersect; 1934 1935 painter->drawLine(lineStart, pixelCenter); 1936 } 1937 1938 painter->setPen(Qt::black); 1939 painter->drawText(rect.adjusted(7, 3, -4, -2), flags, text); 1940 } 1941 1942 QRect View::usedDiagramRect(const QRectF &rect) const 1943 { 1944 double x0 = rect.left() / m_clipRect.width(); 1945 double x1 = rect.right() / m_clipRect.width(); 1946 1947 double y0 = rect.top() / m_clipRect.height(); 1948 double y1 = rect.bottom() / m_clipRect.height(); 1949 1950 int i0 = qMax(int(x0 * LabelGridSize), 0); 1951 int i1 = qMin(int(x1 * LabelGridSize), LabelGridSize - 1); 1952 int j0 = qMax(int(y0 * LabelGridSize), 0); 1953 int j1 = qMin(int(y1 * LabelGridSize), LabelGridSize - 1); 1954 1955 return QRect(i0, j0, i1 - i0 + 1, j1 - j0 + 1) & QRect(0, 0, LabelGridSize, LabelGridSize); 1956 } 1957 1958 void View::markDiagramAreaUsed(const QRectF &rect) 1959 { 1960 if (m_zoomMode == Translating) 1961 return; 1962 1963 QRect r = usedDiagramRect(rect); 1964 1965 for (int i = r.left(); i <= r.right(); ++i) 1966 for (int j = r.top(); j <= r.bottom(); ++j) 1967 m_usedDiagramArea[i][j] = true; 1968 } 1969 1970 void View::markDiagramPointUsed(const QPointF &point) 1971 { 1972 if (m_zoomMode == Translating) 1973 return; 1974 1975 double x = point.x() / m_clipRect.width(); 1976 double y = point.y() / m_clipRect.height(); 1977 1978 int i = int(x * LabelGridSize); 1979 int j = int(y * LabelGridSize); 1980 1981 if (i < 0 || i >= LabelGridSize || j < 0 || j >= LabelGridSize) 1982 return; 1983 1984 m_usedDiagramArea[i][j] = true; 1985 } 1986 1987 int View::rectCost(QRectF rect) const 1988 { 1989 rect = rect.normalized(); 1990 1991 int cost = 0; 1992 1993 // If the rectangle goes off the edge, mark it as very high cost) 1994 if (rect.intersects(m_clipRect)) { 1995 QRectF intersect = (rect & m_clipRect); 1996 cost += int(rect.width() * rect.height() - intersect.width() * intersect.height()); 1997 } else { 1998 // The rectangle is completely outside! 1999 cost += int(rect.width() * rect.height()); 2000 } 2001 2002 QRect r = usedDiagramRect(rect); 2003 2004 for (int i = r.left(); i <= r.right(); ++i) 2005 for (int j = r.top(); j <= r.bottom(); ++j) 2006 if (m_usedDiagramArea[i][j]) 2007 cost += 200; 2008 2009 return cost; 2010 } 2011 2012 bool View::penShouldDraw(double length, const Plot &plot) 2013 { 2014 // Always use a solid line when translating the view 2015 if (m_zoomMode == Translating) 2016 return true; 2017 2018 Function *function = plot.function(); 2019 2020 Qt::PenStyle style = function->plotAppearance(plot.plotMode).style; 2021 2022 double sepBig = 8.0; // separation distance between dashes 2023 double sepMid = 7.0; // separation between a dash and a dot 2024 double sepSmall = 6.5; // separation distance between dots 2025 double dash = 9.0; // length of a dash 2026 double dot = 3.5; // length of a dot 2027 2028 switch (style) { 2029 case Qt::NoPen: 2030 // *whatever*... 2031 return false; 2032 2033 case Qt::SolidLine: 2034 return true; 2035 2036 case Qt::DashLine: 2037 return realModulo(length, dash + sepBig) < dash; 2038 2039 case Qt::DotLine: 2040 return realModulo(length, dot + sepSmall) < dot; 2041 2042 case Qt::DashDotLine: { 2043 double at = realModulo(length, dash + sepMid + dot + sepMid); 2044 2045 if (at < dash) 2046 return true; 2047 if (at < (dash + sepMid)) 2048 return false; 2049 if (at < (dash + sepMid + dot)) 2050 return true; 2051 return false; 2052 } 2053 2054 case Qt::DashDotDotLine: { 2055 double at = realModulo(length, dash + sepMid + dot + sepSmall + dot + sepMid); 2056 2057 if (at < dash) 2058 return true; 2059 if (at < (dash + sepMid)) 2060 return false; 2061 if (at < (dash + sepMid + dot)) 2062 return true; 2063 if (at < (dash + sepMid + dot + sepSmall)) 2064 return false; 2065 if (at < (dash + sepMid + dot + sepSmall + dot)) 2066 return true; 2067 return false; 2068 } 2069 2070 case Qt::MPenStyle: 2071 case Qt::CustomDashLine: { 2072 assert(!"Do not know how to handle this style!"); 2073 return true; 2074 } 2075 } 2076 2077 assert(!"Unknown pen style!"); 2078 return true; 2079 } 2080 2081 QPen View::penForPlot(const Plot &plot, QPainter *painter) const 2082 { 2083 QPen pen; 2084 if (m_zoomMode == Translating) { 2085 // plot style is always a solid line when translating the view 2086 pen.setCapStyle(Qt::FlatCap); 2087 } else { 2088 pen.setCapStyle(Qt::RoundCap); 2089 // (the style will be set back to FlatCap if the plot style is a solid line) 2090 } 2091 2092 pen.setColor(plot.color()); 2093 2094 Function *ufkt = plot.function(); 2095 PlotAppearance appearance = ufkt->plotAppearance(plot.plotMode); 2096 2097 double lineWidth_mm = appearance.lineWidth; 2098 2099 if (appearance.style == Qt::SolidLine) 2100 pen.setCapStyle(Qt::FlatCap); 2101 2102 double width = millimetersToPixels(lineWidth_mm, painter->device()); 2103 pen.setWidthF(width); 2104 2105 return pen; 2106 } 2107 2108 double View::millimetersToPixels(double width_mm, QPaintDevice *device) const 2109 { 2110 // assert( device->logicalDpiX() == device->logicalDpiY() ); 2111 return device->logicalDpiX() * (width_mm / 25.4); 2112 } 2113 2114 double View::pixelsToMillimeters(double width_pixels, QPaintDevice *device) const 2115 { 2116 // assert( device->logicalDpiX() == device->logicalDpiY() ); 2117 return (width_pixels * 25.4) / device->logicalDpiX(); 2118 } 2119 2120 void View::drawHeaderTable(QPainter *painter) 2121 { 2122 painter->setFont(Settings::headerTableFont()); 2123 2124 QString alx = i18nc("%1=minimum value, %2=maximum value", "%1 to %2", Settings::xMin(), Settings::xMax()); 2125 QString aly = i18nc("%1=minimum value, %2=maximum value", "%1 to %2", Settings::yMin(), Settings::yMax()); 2126 2127 QString atx = i18nc("'E' is the distance between ticks on the axis", "1E = ") + ticSepX.expression(); 2128 QString aty = i18nc("'E' is the distance between ticks on the axis", "1E = ") + ticSepY.expression(); 2129 2130 QString text = "<div style=\"margin: 0 auto;\"><table border=\"1\" cellpadding=\"4\" cellspacing=\"0\">" 2131 "<tr><td><b>" + i18n("Parameters") + "</b></td><td><b>" + i18n("Plotting Range") + "</b></td><td><b>" + i18n("Axes Division") + "</b></td></tr>" 2132 "<tr><td><b>" + i18n("x-Axis:") + "</b></td><td>" + alx + "</td><td>" + atx + "</td></tr>" 2133 "<tr><td><b>" + i18n("y-Axis:") + "</b></td><td>" + aly + "</td><td>" + aty + "</td></tr>" 2134 "</table></div>"; 2135 2136 text += "<br><br><b>" + i18n("Functions:") + "</b><ul>"; 2137 2138 for (Function *function : qAsConst(XParser::self()->m_ufkt)) 2139 text += "<li>" + function->name().replace('\n', "<br>") + "</li>"; 2140 2141 text += "</ul>"; 2142 2143 m_textDocument->setHtml(text); 2144 m_textDocument->documentLayout()->draw(painter, QAbstractTextDocumentLayout::PaintContext()); 2145 2146 QRectF br = m_textDocument->documentLayout()->frameBoundingRect(m_textDocument->rootFrame()); 2147 painter->translate(0, br.height()); 2148 } 2149 2150 QList<QPointF> View::findStationaryPoints(const Plot &plot) 2151 { 2152 Plot plot2 = plot; 2153 plot2.differentiate(); 2154 2155 const QList<double> roots = findRoots(plot2, getXmin(plot.function()), getXmax(plot.function()), RoughRoot); 2156 2157 plot.updateFunction(); 2158 QList<QPointF> stationaryPoints; 2159 for (double x : roots) { 2160 QPointF real = realValue(plot, x, false); 2161 if (real.y() >= m_ymin && real.y() <= m_ymax) 2162 stationaryPoints << real; 2163 } 2164 2165 return stationaryPoints; 2166 } 2167 2168 QList<double> View::findRoots(const Plot &plot, double min, double max, RootAccuracy accuracy) 2169 { 2170 typedef QMap<double, double> DoubleMap; 2171 DoubleMap roots; 2172 2173 int count = 10; // number of points to (initially) check for roots 2174 2175 int prevNumRoots = 0; 2176 while (count < 1000) { 2177 // Use this to detect finding the same root. 2178 double prevX = 0.0; 2179 2180 double dx = (max - min) / double(count); 2181 for (int i = 0; i <= count; ++i) { 2182 double x = min + dx * i; 2183 2184 bool found = findRoot(&x, plot, accuracy); 2185 if (!found || x < min || x > max) 2186 continue; 2187 2188 if (!roots.isEmpty()) { 2189 // Check if already have a close root 2190 if (qAbs(x - prevX) <= (dx / 4)) 2191 continue; 2192 2193 DoubleMap::iterator nextIt = roots.lowerBound(x); 2194 if (nextIt == roots.end()) 2195 --nextIt; 2196 2197 double lower, upper; 2198 lower = upper = *nextIt; 2199 if (nextIt != roots.begin()) 2200 lower = *(--nextIt); 2201 2202 if ((qAbs(x - lower) <= (dx / 4)) || (qAbs(x - upper) <= (dx / 4))) 2203 continue; 2204 } 2205 2206 roots.insert(x, x); 2207 prevX = x; 2208 } 2209 2210 int newNumRoots = roots.size(); 2211 if (newNumRoots == prevNumRoots) 2212 break; 2213 2214 prevNumRoots = newNumRoots; 2215 count *= 4; 2216 } 2217 2218 return roots.keys(); 2219 } 2220 2221 void View::setupFindRoot(const Plot &plot, RootAccuracy accuracy, double *max_k, double *max_f, int *n) 2222 { 2223 plot.updateFunction(); 2224 2225 if (accuracy == PreciseRoot) { 2226 *max_k = 200; 2227 *max_f = 1e-14; 2228 } else { 2229 // Rough root 2230 *max_k = 10; 2231 *max_f = 1e-10; 2232 } 2233 2234 *n = 1 + plot.derivativeNumber(); 2235 } 2236 2237 bool View::findRoot(double *x, const Plot &plot, RootAccuracy accuracy) 2238 { 2239 #ifdef DEBUG_IMPLICIT 2240 root_find_requests++; 2241 #endif 2242 2243 double max_k, max_f; 2244 int n; 2245 setupFindRoot(plot, accuracy, &max_k, &max_f, &n); 2246 2247 Equation *eq = plot.function()->eq[0]; 2248 DifferentialState *state = plot.state(); 2249 2250 double h = qMin(m_xmax - m_xmin, m_ymax - m_ymin) * 1e-4; 2251 2252 double f = value(plot, 0, *x, false); 2253 int k; 2254 for (k = 0; k < max_k; ++k) { 2255 double df = XParser::self()->derivative(n, eq, state, *x, h); 2256 if (qAbs(df) < 1e-20) 2257 df = 1e-20 * ((df < 0) ? -1 : 1); 2258 2259 double dx = f / df; 2260 *x -= dx; 2261 f = value(plot, 0, *x, false); 2262 2263 if ((qAbs(f) <= max_f) && (qAbs(dx) <= (h * 1e-5))) 2264 break; 2265 } 2266 2267 #ifdef DEBUG_IMPLICIT 2268 root_find_iterations += k; 2269 #endif 2270 2271 // We continue calculating until |f| < max_f; this may result in k reaching 2272 // max_k. However, if |f| is reasonably small (even if reaching max_k), 2273 // we consider it a root. 2274 return (qAbs(f) < 1e-6); 2275 } 2276 2277 bool View::findRoot(double *x, double *y, const Plot &plot, RootAccuracy accuracy) 2278 { 2279 double max_k, max_f; 2280 int n; 2281 setupFindRoot(plot, accuracy, &max_k, &max_f, &n); 2282 2283 Function *function = plot.function(); 2284 Equation *eq = function->eq[0]; 2285 DifferentialState *state = plot.state(); 2286 2287 double hx = (m_xmax - m_xmin) * 1e-5; 2288 double hy = (m_ymax - m_ymin) * 1e-5; 2289 2290 function->y = *y; 2291 function->m_implicitMode = Function::FixedY; 2292 double f = value(plot, 0, *x, false); 2293 2294 for (int k = 0; k < max_k; ++k) { 2295 function->x = *x; 2296 function->y = *y; 2297 2298 function->m_implicitMode = Function::FixedY; 2299 double dfx = XParser::self()->derivative(n, eq, state, *x, hx); 2300 2301 function->m_implicitMode = Function::FixedX; 2302 double dfy = XParser::self()->derivative(n, eq, state, *y, hy); 2303 2304 double dff = dfx * dfx + dfy * dfy; 2305 if (dff < 1e-20) 2306 dff = 1e-20; 2307 2308 double dx = f * dfx / dff; 2309 *x -= dx; 2310 double dy = f * dfy / dff; 2311 *y -= dy; 2312 2313 function->y = *y; 2314 function->m_implicitMode = Function::FixedY; 2315 f = value(plot, 0, *x, false); 2316 2317 if ((qAbs(f) <= max_f) && (qAbs(dx) <= (hx * 1e-5)) && (qAbs(dy) <= (hy * 1e-5))) 2318 break; 2319 } 2320 2321 // We continue calculating until |f| < max_f; this may result in k reaching 2322 // max_k. However, if |f| is reasonably small (even if reaching max_k), 2323 // we consider it a root. 2324 return (qAbs(f) < 1e-6); 2325 } 2326 2327 void View::paintEvent(QPaintEvent *) 2328 { 2329 // Note: it is important to have this function call before we begin painting 2330 // as updateCrosshairPosition may set the statusbar text 2331 bool inBounds = updateCrosshairPosition(); 2332 2333 QPainter p; 2334 p.begin(this); 2335 2336 p.drawPixmap(QPoint(0, 0), buffer); 2337 2338 // the current cursor position in widget coordinates 2339 QPoint mousePos = mapFromGlobal(QCursor::pos()); 2340 2341 if ((m_zoomMode == ZoomInDrawing) || (m_zoomMode == ZoomOutDrawing)) { 2342 QPalette palette; 2343 QColor highlightColor = palette.color(QPalette::Highlight); 2344 QColor backgroundColor = highlightColor; 2345 backgroundColor.setAlpha(63); 2346 2347 p.setPen(highlightColor); 2348 p.setBrush(backgroundColor); 2349 2350 p.setBackgroundMode(Qt::OpaqueMode); 2351 p.setBackground(Qt::blue); 2352 2353 QRect rect(m_zoomRectangleStart, mousePos); 2354 p.drawRect(rect); 2355 } else if (m_zoomMode == AnimatingZoom) { 2356 QPointF tl(toPixel(m_animateZoomRect.topLeft())); 2357 QPointF br(toPixel(m_animateZoomRect.bottomRight())); 2358 p.drawRect(QRectF(tl, QSizeF(br.x() - tl.x(), br.y() - tl.y()))); 2359 } else if (shouldShowCrosshairs()) { 2360 Function *function = m_currentPlot.function(); 2361 2362 QPen pen; 2363 2364 if (function) { 2365 QColor functionColor = m_currentPlot.color(); 2366 pen.setColor(functionColor); 2367 p.setPen(pen); 2368 p.setRenderHint(QPainter::Antialiasing, true); 2369 2370 double x = m_crosshairPosition.x(); 2371 double y = m_crosshairPosition.y(); 2372 2373 // BEGIN calculate curvature, normal 2374 double k = 0; 2375 double normalAngle = 0; 2376 2377 switch (function->type()) { 2378 case Function::Parametric: 2379 case Function::Polar: 2380 normalAngle = pixelNormal(m_currentPlot, m_trace_x); 2381 k = pixelCurvature(m_currentPlot, m_trace_x); 2382 break; 2383 2384 case Function::Differential: 2385 case Function::Cartesian: 2386 case Function::Implicit: 2387 normalAngle = pixelNormal(m_currentPlot, x, y); 2388 k = pixelCurvature(m_currentPlot, x, y); 2389 break; 2390 } 2391 2392 if (k < 0) { 2393 k = -k; 2394 normalAngle += M_PI; 2395 } 2396 // END calculate curvature, normal 2397 2398 if (k > 1e-5 && Settings::detailedTracing() && inBounds) { 2399 p.save(); 2400 2401 // Transform the painter so that the center of the osculating circle is the origin, 2402 // with the normal line coming in from the left. 2403 QPointF center = m_crosshairPixelCoords + (1 / k) * QPointF(cos(normalAngle), sin(normalAngle)); 2404 p.translate(center); 2405 p.rotate(normalAngle * 180 / M_PI); 2406 2407 // draw osculating circle 2408 pen.setColor(functionColor); 2409 p.setPen(pen); 2410 p.drawEllipse(QRectF(-QPointF(1 / k, 1 / k), QSizeF(2 / k, 2 / k))); 2411 2412 // draw normal 2413 pen.setColor(functionColor); 2414 p.setPen(pen); 2415 p.setBrush(pen.color()); 2416 p.drawLine(QLineF(-1 / k, 0, 0, 0)); 2417 2418 // draw normal arrow 2419 QPolygonF arrowHead(3); 2420 arrowHead[0] = QPointF(0, 0); 2421 arrowHead[1] = QPointF(-3, -2); 2422 arrowHead[2] = QPointF(-3, +2); 2423 p.drawPolygon(arrowHead); 2424 2425 // draw tangent 2426 double tangent_scale = 1.2; // make the tangent look better 2427 p.drawLine(QLineF(-1 / k, -qMax(1 / k, qreal(15.)) * tangent_scale, -1 / k, qMax(1 / k, qreal(15.)) * tangent_scale)); 2428 2429 // draw perpendicular symbol 2430 QPolygonF perp(3); 2431 perp[0] = QPointF(-1 / k, 10); 2432 perp[1] = QPointF(-1 / k + 10, 10); 2433 perp[2] = QPointF(-1 / k + 10, 0); 2434 p.drawPolyline(perp); 2435 2436 // draw intersection blob 2437 p.drawRect(QRectF(-1 / k - 1, -1, 2, 2)); 2438 2439 p.restore(); 2440 2441 // Already show osculating circle, etc, so don't draw crosshairs quite so prominently 2442 functionColor.setAlpha(63); 2443 pen.setColor(functionColor); 2444 } 2445 } else { 2446 // Use an inverted background color for contrast 2447 QColor inverted = QColor(255 - m_backgroundColor.red(), 255 - m_backgroundColor.green(), 255 - m_backgroundColor.blue()); 2448 pen.setColor(inverted); 2449 } 2450 2451 p.setPen(pen); 2452 double x = m_crosshairPixelCoords.x(); 2453 double y = m_crosshairPixelCoords.y(); 2454 p.drawLine(QPointF(0, y), QPointF(m_clipRect.right(), y)); 2455 p.drawLine(QPointF(x, 0), QPointF(x, m_clipRect.height())); 2456 } 2457 2458 p.end(); 2459 } 2460 2461 double View::pixelNormal(const Plot &plot, double x, double y) 2462 { 2463 Function *f = plot.function(); 2464 assert(f); 2465 2466 plot.updateFunction(); 2467 2468 // For converting from real to pixels 2469 double sx = m_clipRect.width() / (m_xmax - m_xmin); 2470 double sy = m_clipRect.height() / (m_ymax - m_ymin); 2471 2472 double dx = 0; 2473 double dy = 0; 2474 2475 double h = this->h(plot); 2476 2477 int d0 = plot.derivativeNumber(); 2478 int d1 = d0 + 1; 2479 2480 switch (f->type()) { 2481 case Function::Differential: 2482 case Function::Cartesian: { 2483 double df = XParser::self()->derivative(d1, f->eq[0], plot.state(), x, h); 2484 return -atan(df * (sy / sx)) - (M_PI / 2); 2485 } 2486 2487 case Function::Implicit: { 2488 dx = XParser::self()->partialDerivative(d1, d0, f->eq[0], 0, x, y, h, h) / sx; 2489 dy = XParser::self()->partialDerivative(d0, d1, f->eq[0], 0, x, y, h, h) / sy; 2490 2491 double theta = -atan(dy / dx); 2492 2493 if (dx < 0) 2494 theta += M_PI; 2495 2496 theta += M_PI; 2497 2498 return theta; 2499 } 2500 2501 case Function::Polar: { 2502 double r = XParser::self()->derivative(d0, f->eq[0], 0, x, h); 2503 double dr = XParser::self()->derivative(d1, f->eq[0], 0, x, h); 2504 2505 dx = (dr * lcos(x) - r * lsin(x) * XParser::self()->radiansPerAngleUnit()) * sx; 2506 dy = (dr * lsin(x) + r * lcos(x) * XParser::self()->radiansPerAngleUnit()) * sy; 2507 break; 2508 } 2509 2510 case Function::Parametric: { 2511 dx = XParser::self()->derivative(d1, f->eq[0], 0, x, h) * sx; 2512 dy = XParser::self()->derivative(d1, f->eq[1], 0, x, h) * sy; 2513 break; 2514 } 2515 } 2516 2517 double theta = -atan(dy / dx) - (M_PI / 2); 2518 2519 if (dx < 0) 2520 theta += M_PI; 2521 2522 return theta; 2523 } 2524 2525 double View::pixelCurvature(const Plot &plot, double x, double y) 2526 { 2527 Function *f = plot.function(); 2528 2529 // For converting from real to pixels 2530 double sx = m_clipRect.width() / (m_xmax - m_xmin); 2531 double sy = m_clipRect.height() / (m_ymax - m_ymin); 2532 2533 double fdx = 0; 2534 double fdy = 0; 2535 double fddx = 0; 2536 double fddy = 0; 2537 double fdxy = 0; 2538 2539 double h = this->h(plot); 2540 2541 int d0 = plot.derivativeNumber(); 2542 int d1 = d0 + 1; 2543 int d2 = d0 + 2; 2544 2545 switch (f->type()) { 2546 case Function::Differential: 2547 case Function::Cartesian: { 2548 DifferentialState *state = plot.state(); 2549 2550 fdx = sx; 2551 fddx = 0; 2552 2553 fdy = XParser::self()->derivative(d1, f->eq[0], state, x, h) * sy; 2554 fddy = XParser::self()->derivative(d2, f->eq[0], state, x, h) * sy; 2555 2556 // qDebug() << "fdy="<<fdy<<" fddy="<<fddy; 2557 2558 break; 2559 } 2560 2561 case Function::Polar: { 2562 double r = XParser::self()->derivative(d0, f->eq[0], 0, x, h); 2563 double dr = XParser::self()->derivative(d1, f->eq[0], 0, x, h); 2564 double ddr = XParser::self()->derivative(d2, f->eq[0], 0, x, h); 2565 2566 fdx = (dr * lcos(x) - r * lsin(x) * XParser::self()->radiansPerAngleUnit()) * sx; 2567 fdy = (dr * lsin(x) + r * lcos(x) * XParser::self()->radiansPerAngleUnit()) * sy; 2568 2569 double rpau = XParser::self()->radiansPerAngleUnit(); 2570 2571 fddx = (ddr * lcos(x) - 2 * dr * lsin(x) * rpau - r * lcos(x) * rpau * rpau) * sx; 2572 fddy = (ddr * lsin(x) + 2 * dr * lcos(x) * rpau - r * lsin(x) * rpau * rpau) * sy; 2573 2574 break; 2575 } 2576 2577 case Function::Parametric: { 2578 fdx = XParser::self()->derivative(d1, f->eq[0], 0, x, h) * sx; 2579 fdy = XParser::self()->derivative(d1, f->eq[1], 0, x, h) * sy; 2580 2581 fddx = XParser::self()->derivative(d2, f->eq[0], 0, x, h) * sx; 2582 fddy = XParser::self()->derivative(d2, f->eq[1], 0, x, h) * sy; 2583 2584 break; 2585 } 2586 2587 case Function::Implicit: { 2588 fdx = XParser::self()->partialDerivative(d1, d0, f->eq[0], 0, x, y, h, h) / sx; 2589 fdy = XParser::self()->partialDerivative(d0, d1, f->eq[0], 0, x, y, h, h) / sy; 2590 2591 fddx = XParser::self()->partialDerivative(d2, d0, f->eq[0], 0, x, y, h, h) / (sx * sx); 2592 fddy = XParser::self()->partialDerivative(d0, d2, f->eq[0], 0, x, y, h, h) / (sy * sy); 2593 2594 fdxy = XParser::self()->partialDerivative(d1, d1, f->eq[0], 0, x, y, h, h) / (sx * sy); 2595 2596 break; 2597 } 2598 } 2599 2600 double mod = pow(fdx * fdx + fdy * fdy, 1.5); 2601 2602 switch (f->type()) { 2603 case Function::Differential: 2604 case Function::Cartesian: 2605 case Function::Parametric: 2606 case Function::Polar: 2607 return (fdx * fddy - fdy * fddx) / mod; 2608 2609 case Function::Implicit: 2610 return (fdx * fdx * fddy + fdy * fdy * fddx - 2 * fdx * fdy * fdxy) / mod; 2611 } 2612 2613 qCritical() << "Unknown function type!\n"; 2614 return 0; 2615 } 2616 2617 void View::resizeEvent(QResizeEvent *) 2618 { 2619 if (m_isDrawing) // stop drawing integrals 2620 { 2621 m_stopCalculating = true; // stop drawing 2622 return; 2623 } 2624 qreal dpr = devicePixelRatioF(); 2625 buffer = QPixmap(size() * dpr); 2626 buffer.setDevicePixelRatio(dpr); 2627 drawPlot(); 2628 } 2629 2630 void View::drawPlot() 2631 { 2632 if (buffer.width() == 0 || buffer.height() == 0) 2633 return; 2634 2635 buffer.fill(m_backgroundColor); 2636 draw(&buffer, Screen); 2637 update(); 2638 } 2639 2640 void View::focusOutEvent(QFocusEvent *) 2641 { 2642 // Redraw ourselves to get rid of the crosshair (if we had it)... 2643 QTimer::singleShot(0, this, SLOT(update())); 2644 QTimer::singleShot(0, this, &View::updateCursor); 2645 } 2646 2647 void View::focusInEvent(QFocusEvent *) 2648 { 2649 // Redraw ourselves to get the crosshair (if we should have it)... 2650 QTimer::singleShot(0, this, SLOT(update())); 2651 QTimer::singleShot(0, this, &View::updateCursor); 2652 } 2653 2654 bool View::crosshairPositionValid(Function *plot) const 2655 { 2656 if (!plot) 2657 return false; 2658 2659 // only relevant for cartesian plots - assume true for none 2660 if (plot->type() != Function::Cartesian) 2661 return true; 2662 2663 bool lowerOk = ((!plot->usecustomxmin) || (plot->usecustomxmin && m_crosshairPosition.x() > plot->dmin.value())); 2664 bool upperOk = ((!plot->usecustomxmax) || (plot->usecustomxmax && m_crosshairPosition.x() < plot->dmax.value())); 2665 2666 return lowerOk && upperOk; 2667 } 2668 2669 void View::mousePressEvent(QMouseEvent *e) 2670 { 2671 m_AccumulatedDelta = 0; 2672 m_mousePressTimer->start(); 2673 2674 // In general, we want to update the view 2675 update(); 2676 2677 if (m_popupMenuStatus != NoPopup) 2678 return; 2679 2680 if (m_isDrawing) { 2681 m_stopCalculating = true; // stop drawing 2682 return; 2683 } 2684 2685 if (m_zoomMode != Normal) { 2686 // If the user clicked with the right mouse button will zooming in or out, then cancel it 2687 if ((m_zoomMode == ZoomInDrawing) || (m_zoomMode == ZoomOutDrawing)) { 2688 m_zoomMode = Normal; 2689 } 2690 updateCursor(); 2691 return; 2692 } 2693 2694 m_haveRoot = false; 2695 2696 bool hadFunction = (m_currentPlot.functionID() != -1); 2697 2698 updateCrosshairPosition(); 2699 2700 if (!m_readonly && e->button() == Qt::RightButton) // clicking with the right mouse button 2701 { 2702 getPlotUnderMouse(); 2703 if (m_currentPlot.function()) { 2704 if (hadFunction) 2705 m_popupMenuStatus = PopupDuringTrace; 2706 else 2707 m_popupMenuStatus = Popup; 2708 2709 fillPopupMenu(); 2710 m_popupMenu->exec(QCursor::pos()); 2711 } 2712 return; 2713 } 2714 2715 if (e->button() != Qt::LeftButton) 2716 return; 2717 2718 if (m_currentPlot.functionID() >= 0) // disable trace mode if trace mode is enable 2719 { 2720 m_currentPlot.setFunctionID(-1); 2721 setStatusBar(QString(), RootSection); 2722 setStatusBar(QString(), FunctionSection); 2723 mouseMoveEvent(e); 2724 return; 2725 } 2726 2727 QPointF closestPoint = getPlotUnderMouse(); 2728 Function *function = m_currentPlot.function(); 2729 if (function) { 2730 QPointF ptd(toPixel(closestPoint)); 2731 QPoint globalPos = mapToGlobal(ptd.toPoint()); 2732 QCursor::setPos(globalPos); 2733 setStatusBar(m_currentPlot.name().replace('\n', " ; "), FunctionSection); 2734 return; 2735 } 2736 2737 // user didn't click on a plot; so we prepare to enter translation mode 2738 m_currentPlot.setFunctionID(-1); 2739 m_zoomMode = AboutToTranslate; 2740 m_prevDragMousePos = e->pos(); 2741 updateCursor(); 2742 } 2743 2744 void View::fillPopupMenu() 2745 { 2746 Function *function = m_currentPlot.function(); 2747 if (!function) 2748 return; 2749 2750 m_popupMenuTitle->setText(m_currentPlot.name().replace('\n', "; ")); 2751 2752 QAction *calcArea = MainDlg::self()->actionCollection()->action("grapharea"); 2753 QAction *maxValue = MainDlg::self()->actionCollection()->action("maximumvalue"); 2754 QAction *minValue = MainDlg::self()->actionCollection()->action("minimumvalue"); 2755 2756 m_popupMenu->removeAction(calcArea); 2757 m_popupMenu->removeAction(maxValue); 2758 m_popupMenu->removeAction(minValue); 2759 2760 if (function->type() == Function::Cartesian || function->type() == Function::Differential) { 2761 m_popupMenu->addAction(calcArea); 2762 m_popupMenu->addAction(maxValue); 2763 m_popupMenu->addAction(minValue); 2764 } 2765 } 2766 2767 QPointF View::getPlotUnderMouse() 2768 { 2769 m_currentPlot.setFunctionID(-1); 2770 m_trace_x = 0.0; 2771 2772 Plot bestPlot; 2773 2774 double best_distance = 1e30; // a nice large number 2775 QPointF best_cspos; 2776 2777 for (Function *function : qAsConst(XParser::self()->m_ufkt)) { 2778 const QList<Plot> plots = function->plots(); 2779 for (const Plot &plot : plots) { 2780 plot.updateFunction(); 2781 2782 double best_x = 0.0, distance; 2783 QPointF cspos; 2784 2785 if (function->type() == Function::Implicit) { 2786 double x = m_crosshairPosition.x(); 2787 double y = m_crosshairPosition.y(); 2788 findRoot(&x, &y, plot, PreciseRoot); 2789 2790 QPointF d = toPixel(QPointF(x, y), ClipInfinite) - toPixel(QPointF(m_crosshairPosition.x(), m_crosshairPosition.y()), ClipInfinite); 2791 2792 distance = std::sqrt(d.x() * d.x() + d.y() * d.y()); 2793 cspos = QPointF(x, y); 2794 } else { 2795 best_x = getClosestPoint(m_crosshairPosition, plot); 2796 distance = pixelDistance(m_crosshairPosition, plot, best_x, false); 2797 cspos = realValue(plot, best_x, false); 2798 } 2799 2800 if (distance < best_distance) { 2801 best_distance = distance; 2802 bestPlot = plot; 2803 m_trace_x = best_x; 2804 best_cspos = cspos; 2805 } 2806 } 2807 } 2808 2809 if (best_distance < 10.0) { 2810 m_currentPlot = bestPlot; 2811 m_crosshairPosition = best_cspos; 2812 return m_crosshairPosition; 2813 } else 2814 return QPointF(); 2815 } 2816 2817 double View::getClosestPoint(const QPointF &pos, const Plot &plot) 2818 { 2819 plot.updateFunction(); 2820 2821 double best_x = 0.0; 2822 2823 Function *function = plot.function(); 2824 assert(function->type() != Function::Implicit); // should use findRoot (3D version) for this 2825 2826 switch (function->type()) { 2827 case Function::Implicit: 2828 break; 2829 2830 case Function::Differential: 2831 case Function::Cartesian: { 2832 double best_pixel_x = m_clipRect.width() / 2; 2833 2834 QPointF pixelPos = toPixel(pos, ClipInfinite); 2835 2836 double dmin = getXmin(function); 2837 double dmax = getXmax(function); 2838 2839 double stepSize = (m_xmax - m_xmin) / m_clipRect.width(); 2840 2841 // Algorithm in use here: Work out the shortest distance between the 2842 // line joining (x0,y0) to (x1,y1) and the given point (real_x,real_y) 2843 2844 double x = dmin; 2845 double y0 = value(plot, 0, x, false); 2846 2847 double best_distance = 1e20; // a large distance 2848 2849 while (x <= dmax && (xToPixel(x) < best_pixel_x + best_distance)) { 2850 x += stepSize; 2851 2852 double y1 = value(plot, 0, x, false); 2853 2854 double _x0 = xToPixel(x - stepSize, ClipInfinite); 2855 double _x1 = xToPixel(x, ClipInfinite); 2856 2857 double _y0 = yToPixel(y0, ClipInfinite); 2858 double _y1 = yToPixel(y1, ClipInfinite); 2859 2860 double k = (_y1 - _y0) / (_x1 - _x0); 2861 2862 double closest_x, closest_y; 2863 if (k == 0) { 2864 closest_x = pixelPos.x(); 2865 closest_y = _y0; 2866 } else { 2867 closest_x = (pixelPos.y() + pixelPos.x() / k + _x0 * k - _y0) / (k + 1.0 / k); 2868 closest_y = (pixelPos.x() + pixelPos.y() * k + _y0 / k - _x0) / (k + 1.0 / k); 2869 } 2870 2871 bool valid = (x - 1.5 * stepSize <= xToReal(closest_x)) && (xToReal(closest_x) <= x + 0.5 * stepSize); 2872 2873 double dfx = closest_x - pixelPos.x(); 2874 double dfy = closest_y - pixelPos.y(); 2875 2876 double distance = sqrt(dfx * dfx + dfy * dfy); 2877 bool insideView = 0 <= closest_y && closest_y <= m_clipRect.height(); 2878 2879 if (distance < best_distance && insideView && valid) { 2880 best_distance = distance; 2881 best_pixel_x = closest_x; 2882 } 2883 2884 y0 = y1; 2885 } 2886 2887 best_x = xToReal(best_pixel_x); 2888 break; 2889 } 2890 2891 case Function::Polar: 2892 case Function::Parametric: { 2893 double minX = getXmin(function); 2894 double maxX = getXmax(function); 2895 double stepSize = 0.001; 2896 2897 while (stepSize > 0.0000009) { 2898 double best_distance = 1e20; // a large distance 2899 2900 double x = minX; 2901 while (x <= maxX) { 2902 double distance = pixelDistance(pos, plot, x, false); 2903 bool insideView = QRectF(m_clipRect).contains(toPixel(realValue(plot, x, false), ClipInfinite)); 2904 2905 if (distance < best_distance && insideView) { 2906 best_distance = distance; 2907 best_x = x; 2908 } 2909 2910 x += stepSize; 2911 } 2912 2913 minX = best_x - stepSize; 2914 maxX = best_x + stepSize; 2915 2916 stepSize *= 0.1; 2917 } 2918 break; 2919 } 2920 } 2921 2922 return best_x; 2923 } 2924 2925 double View::pixelDistance(const QPointF &pos, const Plot &plot, double x, bool updateFunction) 2926 { 2927 QPointF f = realValue(plot, x, updateFunction); 2928 QPointF df = toPixel(pos, ClipInfinite) - toPixel(f, ClipInfinite); 2929 2930 return std::sqrt(df.x() * df.x() + df.y() * df.y()); 2931 } 2932 2933 QString View::posToString(double x, double delta, PositionFormatting format, const QColor &color) const 2934 { 2935 delta = qAbs(delta); 2936 if (delta == 0) 2937 delta = 1; 2938 2939 QString numberText; 2940 2941 int decimalPlaces = 1 - int(log(delta) / log(10.0)); 2942 2943 // Avoid exponential format for smallish numbers 2944 if (0.01 < qAbs(x) && qAbs(x) < 10000) 2945 format = DecimalFormat; 2946 2947 switch (format) { 2948 case ScientificFormat: { 2949 int accuracy = 1 + decimalPlaces + int(log(qAbs(x)) / log(10.0)); 2950 if (accuracy < 2) { 2951 // Ensure a minimum of two significant digits 2952 accuracy = 2; 2953 } 2954 2955 QString number = QString::number(x, 'g', accuracy); 2956 if (number.contains('e')) { 2957 number.remove("+0"); 2958 number.remove('+'); 2959 number.replace("-0", MinusSymbol); 2960 2961 number.replace('e', QChar(215) + QString("10<sup>")); 2962 number.append("</sup>"); 2963 } 2964 if (x > 0.0) 2965 number.prepend('+'); 2966 2967 numberText = QString("<html><body><span style=\"color:%1;\">").arg(color.name()) + number + "</span></body></html>"; 2968 2969 break; 2970 } 2971 2972 case DecimalFormat: { 2973 if (decimalPlaces >= 0) 2974 numberText = QString::number(x, 'f', decimalPlaces); 2975 else 2976 numberText = QString::number(x * (pow(10.0, decimalPlaces)), 'f', 0) + QString(-decimalPlaces, '0'); 2977 2978 break; 2979 } 2980 } 2981 2982 numberText.replace('-', MinusSymbol); 2983 2984 return numberText; 2985 } 2986 2987 void View::mouseMoveEvent(QMouseEvent *e) 2988 { 2989 if (m_previousMouseMovePos != e->globalPos()) { 2990 m_AccumulatedDelta = 0; 2991 } 2992 m_previousMouseMovePos = e->globalPos(); 2993 m_AccumulatedDelta = 0; 2994 if (m_isDrawing || !e) 2995 return; 2996 2997 bool inBounds = updateCrosshairPosition(); 2998 if (!m_haveRoot) 2999 setStatusBar(QString(), RootSection); 3000 3001 QString sx, sy; 3002 3003 if (inBounds) { 3004 sx = i18n("x = %1", 3005 posToString(m_crosshairPosition.x(), (m_xmax - m_xmin) / m_clipRect.width(), View::DecimalFormat).replace('.', QLocale().decimalPoint())); 3006 sy = i18n("y = %1", 3007 posToString(m_crosshairPosition.y(), (m_ymax - m_ymin) / m_clipRect.width(), View::DecimalFormat).replace('.', QLocale().decimalPoint())); 3008 } else 3009 sx = sy = ""; 3010 3011 setStatusBar(sx, XSection); 3012 setStatusBar(sy, YSection); 3013 3014 if (e->buttons() & Qt::LeftButton) { 3015 if (m_zoomMode == ZoomIn) { 3016 m_zoomMode = ZoomInDrawing; 3017 m_zoomRectangleStart = e->pos(); 3018 } else if (m_zoomMode == ZoomOut) { 3019 m_zoomMode = ZoomOutDrawing; 3020 m_zoomRectangleStart = e->pos(); 3021 } else if (((m_zoomMode == AboutToTranslate) || (m_zoomMode == Translating)) && (e->pos() != m_prevDragMousePos)) { 3022 m_zoomMode = Translating; 3023 QPoint d = m_prevDragMousePos - e->pos(); 3024 m_prevDragMousePos = e->pos(); 3025 translateView(d.x(), d.y()); 3026 } 3027 } 3028 3029 if ((m_zoomMode == Normal) && (m_popupMenuStatus != NoPopup) && !m_popupMenu->isVisible()) { 3030 if (m_popupMenuStatus == Popup) 3031 m_currentPlot.setFunctionID(-1); 3032 m_popupMenuStatus = NoPopup; 3033 } 3034 3035 update(); 3036 updateCursor(); 3037 } 3038 3039 void View::leaveEvent(QEvent *) 3040 { 3041 setStatusBar("", XSection); 3042 setStatusBar("", YSection); 3043 3044 updateCrosshairPosition(); 3045 update(); 3046 } 3047 3048 void View::wheelEvent(QWheelEvent *e) 3049 { 3050 m_AccumulatedDelta += e->angleDelta().y(); 3051 3052 if (e->modifiers() & Qt::ControlModifier) { 3053 if (m_AccumulatedDelta >= QWheelEvent::DefaultDeltasPerStep) { 3054 zoomIn(e->position(), double(Settings::zoomInStep()) / 100.0); 3055 m_AccumulatedDelta = 0; 3056 } else if (m_AccumulatedDelta <= -QWheelEvent::DefaultDeltasPerStep) { 3057 zoomIn(e->position(), (double(Settings::zoomOutStep()) / 100.0) + 1.0); 3058 m_AccumulatedDelta = 0; 3059 } 3060 e->accept(); 3061 return; 3062 } else { 3063 m_AccumulatedDelta = 0; 3064 } 3065 QWidget::wheelEvent(e); 3066 } 3067 3068 bool View::updateCrosshairPosition() 3069 { 3070 QPointF mousePos = mapFromGlobal(QCursor::pos()); 3071 3072 bool out_of_bounds = false; // for the ypos 3073 3074 m_crosshairPosition = toReal(mousePos); 3075 3076 m_currentPlot.updateFunction(); 3077 Function *it = m_currentPlot.function(); 3078 3079 if (it && crosshairPositionValid(it) && (m_popupMenuStatus != Popup)) { 3080 // The user currently has a plot selected, with the mouse in a valid position 3081 3082 if ((it->type() == Function::Parametric) || (it->type() == Function::Polar)) { 3083 // Should we increase or decrease t to get closer to the mouse? 3084 double dx[2] = {-0.00001, +0.00001}; 3085 double d[] = {0.0, 0.0}; 3086 for (int i = 0; i < 2; ++i) 3087 d[i] = pixelDistance(m_crosshairPosition, m_currentPlot, m_trace_x + dx[i], false); 3088 3089 double prev_best = pixelDistance(m_crosshairPosition, m_currentPlot, m_trace_x, false); 3090 double current_dx = dx[(d[0] < d[1]) ? 0 : 1] * 1e3; 3091 3092 while (true) { 3093 double new_distance = pixelDistance(m_crosshairPosition, m_currentPlot, m_trace_x + current_dx, false); 3094 if (new_distance < prev_best) { 3095 prev_best = new_distance; 3096 m_trace_x += current_dx; 3097 } else { 3098 if (qAbs(current_dx) > 9e-10) 3099 current_dx *= 0.1; 3100 else 3101 break; 3102 } 3103 } 3104 3105 double min = getXmin(it); 3106 double max = getXmax(it); 3107 3108 if (m_trace_x > max) 3109 m_trace_x = max; 3110 3111 else if (m_trace_x < min) 3112 m_trace_x = min; 3113 3114 m_crosshairPosition = realValue(m_currentPlot, m_trace_x, false); 3115 } else if (it->type() == Function::Implicit) { 3116 double x = m_crosshairPosition.x(); 3117 double y = m_crosshairPosition.y(); 3118 findRoot(&x, &y, m_currentPlot, PreciseRoot); 3119 m_crosshairPosition = QPointF(x, y); 3120 } else { 3121 // cartesian or differential plot 3122 3123 m_crosshairPosition.setY(value(m_currentPlot, 0, m_crosshairPosition.x(), false)); 3124 mousePos.setY(yToPixel(m_crosshairPosition.y())); 3125 3126 if (m_crosshairPosition.y() < m_ymin || m_crosshairPosition.y() > m_ymax) // the ypoint is not visible 3127 { 3128 out_of_bounds = true; 3129 } else if ((fabs(yToReal(mousePos.y())) < (m_ymax - m_ymin) / 80) && (it->type() == Function::Cartesian || it->type() == Function::Differential)) { 3130 double x0 = m_crosshairPosition.x(); 3131 if (!m_haveRoot && findRoot(&x0, m_currentPlot, PreciseRoot)) { 3132 QString str = " "; 3133 str += i18nc("%1 is a subscript zero symbol", "root: x%1 = ", SubscriptZeroSymbol); 3134 setStatusBar(str + QLocale().toString(x0, 'f', 5), RootSection); 3135 m_haveRoot = true; 3136 emit updateRootValue(true, x0); 3137 } 3138 } else { 3139 m_haveRoot = false; 3140 emit updateRootValue(false, 0); 3141 } 3142 } 3143 3144 // For Cartesian plots, only adjust the cursor position if it is not at the ends of the view 3145 if (((it->type() != Function::Cartesian) && (it->type() != Function::Differential)) || m_clipRect.contains(mousePos.toPoint())) { 3146 mousePos = toPixel(m_crosshairPosition, ClipAll, mousePos); 3147 QPoint globalPos = mapToGlobal(mousePos.toPoint()); 3148 QCursor::setPos(globalPos); 3149 } 3150 } 3151 3152 m_crosshairPixelCoords = mousePos; 3153 3154 return !out_of_bounds && m_clipRect.contains(mousePos.toPoint()); 3155 } 3156 3157 void View::mouseReleaseEvent(QMouseEvent *e) 3158 { 3159 bool doDrawPlot = false; 3160 3161 // avoid zooming in if the zoom rectangle is very small and the mouse was 3162 // just pressed, which suggests that the user dragged the mouse accidentally 3163 QRect zoomRect = QRect(m_zoomRectangleStart, e->pos()).normalized(); 3164 int area = zoomRect.width() * zoomRect.height(); 3165 3166 if ((area <= 500) && (m_mousePressTimer->elapsed() < QApplication::startDragTime())) { 3167 if (m_zoomMode == ZoomInDrawing) 3168 m_zoomMode = ZoomIn; 3169 else if (m_zoomMode == ZoomOutDrawing) 3170 m_zoomMode = ZoomOut; 3171 } 3172 3173 switch (m_zoomMode) { 3174 case Normal: 3175 case AnimatingZoom: 3176 case AboutToTranslate: 3177 break; 3178 3179 case Translating: 3180 doDrawPlot = true; 3181 Settings::self()->save(); 3182 MainDlg::self()->requestSaveCurrentState(); 3183 break; 3184 3185 case ZoomIn: 3186 zoomIn(e->pos(), double(Settings::zoomInStep()) / 100.0); 3187 break; 3188 3189 case ZoomOut: 3190 zoomIn(e->pos(), (double(Settings::zoomOutStep()) / 100.0) + 1.0); 3191 break; 3192 3193 case ZoomInDrawing: 3194 zoomIn(zoomRect); 3195 break; 3196 3197 case ZoomOutDrawing: 3198 zoomOut(zoomRect); 3199 break; 3200 } 3201 3202 m_zoomMode = Normal; 3203 3204 if (doDrawPlot) 3205 drawPlot(); 3206 else 3207 update(); 3208 3209 updateCursor(); 3210 } 3211 3212 void View::zoomIn(const QPointF &mousePos, double zoomFactor) 3213 { 3214 QPointF real = toReal(mousePos); 3215 3216 double diffx = (m_xmax - m_xmin) * zoomFactor; 3217 double diffy = (m_ymax - m_ymin) * zoomFactor; 3218 3219 animateZoom(QRectF(real.x() - diffx, real.y() - diffy, 2.0 * diffx, 2.0 * diffy)); 3220 } 3221 3222 void View::zoomIn(const QRectF &zoomRect) 3223 { 3224 QPointF p = zoomRect.topLeft(); 3225 double real1x = xToReal(p.x()); 3226 double real1y = yToReal(p.y()); 3227 p = zoomRect.bottomRight(); 3228 double real2x = xToReal(p.x()); 3229 double real2y = yToReal(p.y()); 3230 3231 if (real1x > real2x) 3232 qSwap(real1x, real2x); 3233 if (real1y > real2y) 3234 qSwap(real1y, real2y); 3235 3236 animateZoom(QRectF(QPointF(real1x, real1y), QSizeF(real2x - real1x, real2y - real1y))); 3237 } 3238 3239 void View::zoomOut(const QRectF &zoomRect) 3240 { 3241 QPointF p = zoomRect.topLeft(); 3242 double _real1x = xToReal(p.x()); 3243 double _real1y = yToReal(p.y()); 3244 p = zoomRect.bottomRight(); 3245 double _real2x = xToReal(p.x()); 3246 double _real2y = yToReal(p.y()); 3247 3248 double kx = (_real1x - _real2x) / (m_xmin - m_xmax); 3249 double lx = _real1x - (kx * m_xmin); 3250 3251 double ky = (_real1y - _real2y) / (m_ymax - m_ymin); 3252 double ly = _real1y - (ky * m_ymax); 3253 3254 double real1x = (m_xmin - lx) / kx; 3255 double real2x = (m_xmax - lx) / kx; 3256 3257 double real1y = (m_ymax - ly) / ky; 3258 double real2y = (m_ymin - ly) / ky; 3259 3260 animateZoom(QRectF(QPointF(real1x, real1y), QSizeF(real2x - real1x, real2y - real1y))); 3261 } 3262 3263 void View::animateZoom(const QRectF &_newCoords) 3264 { 3265 QRectF oldCoords(m_xmin, m_ymin, m_xmax - m_xmin, m_ymax - m_ymin); 3266 QRectF newCoords(_newCoords.normalized()); 3267 3268 if (newCoords.left() == m_xmin && newCoords.right() == m_xmax && newCoords.top() == m_ymin && newCoords.bottom() == m_ymax) 3269 return; 3270 3271 m_zoomMode = AnimatingZoom; 3272 3273 if (style()->styleHint(QStyle::SH_Widget_Animate) && m_viewportAnimation->state() == QAbstractAnimation::Stopped) { 3274 m_viewportAnimation->setDuration(150); 3275 m_viewportAnimation->setEasingCurve(QEasingCurve::OutCubic); 3276 m_viewportAnimation->setStartValue(oldCoords); 3277 m_viewportAnimation->setEndValue(newCoords); 3278 m_viewportAnimation->start(); 3279 connect(m_viewportAnimation, &QPropertyAnimation::finished, [this, newCoords] { 3280 finishAnimation(newCoords); 3281 }); 3282 } else { 3283 finishAnimation(newCoords); 3284 } 3285 Settings::self()->save(); 3286 } 3287 3288 void View::finishAnimation(const QRectF &rect) 3289 { 3290 m_xmin = rect.left(); 3291 m_xmax = rect.right(); 3292 m_ymin = rect.top(); 3293 m_ymax = rect.bottom(); 3294 3295 Settings::setXMin(Parser::number(m_xmin)); 3296 Settings::setXMax(Parser::number(m_xmax)); 3297 Settings::setYMin(Parser::number(m_ymin)); 3298 Settings::setYMax(Parser::number(m_ymax)); 3299 MainDlg::self()->coordsDialog()->updateXYRange(); 3300 MainDlg::self()->requestSaveCurrentState(); 3301 3302 drawPlot(); // update all graphs 3303 3304 m_zoomMode = Normal; 3305 } 3306 3307 const QRectF View::getViewport() 3308 { 3309 return m_animateZoomRect; 3310 } 3311 3312 void View::setViewport(const QRectF &rect) 3313 { 3314 m_animateZoomRect = rect; 3315 repaint(); 3316 } 3317 3318 void View::translateView(int dx, int dy) 3319 { 3320 double rdx = xToReal(dx) - xToReal(0.0); 3321 double rdy = yToReal(dy) - yToReal(0.0); 3322 3323 m_xmin += rdx; 3324 m_xmax += rdx; 3325 m_ymin += rdy; 3326 m_ymax += rdy; 3327 3328 Settings::setXMin(Parser::number(m_xmin)); 3329 Settings::setXMax(Parser::number(m_xmax)); 3330 Settings::setYMin(Parser::number(m_ymin)); 3331 Settings::setYMax(Parser::number(m_ymax)); 3332 MainDlg::self()->coordsDialog()->updateXYRange(); 3333 3334 drawPlot(); // update all graphs 3335 } 3336 3337 void View::stopDrawing() 3338 { 3339 if (m_isDrawing) 3340 m_stopCalculating = true; 3341 } 3342 3343 QPointF View::findMinMaxValue(const Plot &plot, ExtremaType type, double dmin, double dmax) 3344 { 3345 Function *ufkt = plot.function(); 3346 assert((ufkt->type() == Function::Cartesian) || (ufkt->type() == Function::Differential)); 3347 Q_UNUSED(ufkt); 3348 3349 plot.updateFunction(); 3350 3351 Plot differentiated = plot; 3352 differentiated.differentiate(); 3353 QList<double> roots = findRoots(differentiated, dmin, dmax, RoughRoot); 3354 3355 // The minimum / maximum might occur at the end points 3356 roots << dmin << dmax; 3357 3358 double best = (type == Maximum) ? -HUGE_VAL : +HUGE_VAL; 3359 QPointF bestPoint; 3360 3361 for (double root : qAsConst(roots)) { 3362 QPointF rv = realValue(plot, root, false); 3363 if ((type == Maximum && rv.y() > best) || (type == Minimum && rv.y() < best)) { 3364 best = rv.y(); 3365 bestPoint = QPointF(rv.x(), rv.y()); 3366 } 3367 } 3368 3369 return bestPoint; 3370 } 3371 3372 void View::keyPressEvent(QKeyEvent *e) 3373 { 3374 // if a zoom operation is in progress, assume that the key press is to cancel it 3375 if (m_zoomMode != Normal) { 3376 m_zoomMode = Normal; 3377 update(); 3378 updateCursor(); 3379 return; 3380 } 3381 3382 if (m_isDrawing) { 3383 m_stopCalculating = true; 3384 return; 3385 } 3386 3387 if (m_currentPlot.functionID() == -1) 3388 return; 3389 3390 QMouseEvent *event = 0; 3391 if (e->key() == Qt::Key_Left) 3392 event = new QMouseEvent(QEvent::MouseMove, m_crosshairPixelCoords.toPoint() - QPoint(1, 1), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); 3393 else if (e->key() == Qt::Key_Right) 3394 event = new QMouseEvent(QEvent::MouseMove, m_crosshairPixelCoords.toPoint() + QPoint(1, 1), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); 3395 else if (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down) // switch graph in trace mode 3396 { 3397 /// \todo reimplement moving between plots 3398 #if 0 3399 QMap<int, Function*>::iterator it = XParser::self()->m_ufkt.find( m_currentPlot.functionID ); 3400 int const ke=(*it)->parameters.count(); 3401 if (ke>0) 3402 { 3403 m_currentFunctionParameter++; 3404 if (m_currentFunctionParameter >= ke) 3405 m_currentFunctionParameter=0; 3406 } 3407 if (m_currentFunctionParameter==0) 3408 { 3409 int const old_m_currentPlot.functionID=m_currentPlot.functionID; 3410 Function::PMode const old_m_currentPlot.plotMode = m_currentPlot.plotMode; 3411 bool start = true; 3412 bool found = false; 3413 while ( 1 ) 3414 { 3415 if ( old_m_currentPlot.functionID==m_currentPlot.functionID && !start) 3416 { 3417 m_currentPlot.plotMode=old_m_currentPlot.plotMode; 3418 break; 3419 } 3420 qDebug() << "m_currentPlot.functionID: " << m_currentPlot.functionID; 3421 switch ( (*it)->type() ) 3422 { 3423 case Function::Parametric: 3424 case Function::Polar: 3425 break; 3426 default: 3427 { 3428 //going through the function, the first and the second derivative 3429 for ( m_currentPlot.plotMode = (Function::PMode)0; m_currentPlot.plotMode < 3; m_currentPlot.plotMode = (Function::PMode)(m_currentPlot.plotMode+1) ) 3430 // for (m_currentPlot.plotMode=0;m_currentPlot.plotMode<3;m_currentPlot.plotMode++) 3431 { 3432 if (start) 3433 { 3434 if ( m_currentPlot.plotMode==Function::Derivative2) 3435 m_currentPlot.plotMode=Function::Derivative0; 3436 else 3437 m_currentPlot.plotMode = (Function::PMode)(old_m_currentPlot.plotMode+1); 3438 start=false; 3439 } 3440 qDebug() << " m_currentPlot.plotMode: " << (int)m_currentPlot.plotMode; 3441 3442 if ( (*it)->plotAppearance( m_currentPlot.plotMode ).visible ) 3443 found = true; 3444 3445 if (found) 3446 break; 3447 } 3448 break; 3449 } 3450 } 3451 if (found) 3452 break; 3453 3454 if ( ++it == XParser::self()->m_ufkt.end()) 3455 it = XParser::self()->m_ufkt.begin(); 3456 m_currentPlot.functionID = (*it)->id(); 3457 } 3458 } 3459 3460 qDebug() << "************************"; 3461 qDebug() << "m_currentPlot.functionID: " << (int)m_currentPlot.functionID; 3462 qDebug() << "m_currentPlot.plotMode: " << (int)m_currentPlot.plotMode; 3463 qDebug() << "m_currentFunctionParameter: " << m_currentFunctionParameter; 3464 3465 setStatusBar( (*it)->prettyName( m_currentPlot.plotMode ), FunctionSection ); 3466 3467 event = new QMouseEvent( QEvent::MouseMove, m_crosshairPixelCoords.toPoint(), Qt::LeftButton, Qt::LeftButton, 0 ); 3468 #else 3469 return; 3470 #endif 3471 } else if (e->key() == Qt::Key_Space) { 3472 event = new QMouseEvent(QEvent::MouseButtonPress, QCursor::pos(), Qt::RightButton, Qt::RightButton, Qt::NoModifier); 3473 mousePressEvent(event); 3474 delete event; 3475 return; 3476 } else { 3477 event = new QMouseEvent(QEvent::MouseButtonPress, m_crosshairPixelCoords.toPoint(), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); 3478 mousePressEvent(event); 3479 delete event; 3480 return; 3481 } 3482 mouseMoveEvent(event); 3483 delete event; 3484 } 3485 3486 double View::areaUnderGraph(IntegralDrawSettings s) 3487 { 3488 int sign = 1; 3489 if (s.dmax < s.dmin) { 3490 qSwap(s.dmin, s.dmax); 3491 sign = -1; 3492 } 3493 3494 else if (s.dmax == s.dmin) 3495 return 0; 3496 3497 Function *ufkt = s.plot.function(); 3498 assert(ufkt); 3499 3500 double dx = (s.dmax - s.dmin) / m_clipRect.width(); 3501 if (s.plot.plotMode == Function::Integral) { 3502 double max_dx = ufkt->eq[0]->differentialStates.step().value(); 3503 if (dx > max_dx) 3504 dx = max_dx; 3505 } 3506 3507 // Make sure that we calculate the exact area (instead of missing out a 3508 // vertical slither at the end) by making sure dx tiles the x-range 3509 // a whole number of times 3510 int intervals = qRound((s.dmax - s.dmin) / dx); 3511 dx = (s.dmax - s.dmin) / intervals; 3512 3513 double calculated_area = 0; 3514 double x = s.dmin; 3515 3516 s.plot.updateFunction(); 3517 3518 for (int i = 0; i <= intervals; ++i) { 3519 double y = value(s.plot, 0, x, false); 3520 3521 // Trapezoid rule for integrals: only add on half for the first and last value 3522 if ((i == 0) || (i == intervals)) 3523 calculated_area += 0.5 * dx * y; 3524 else 3525 calculated_area += dx * y; 3526 3527 x = x + dx; 3528 } 3529 3530 m_integralDrawSettings = s; 3531 m_integralDrawSettings.draw = true; 3532 drawPlot(); 3533 m_integralDrawSettings.draw = false; 3534 return calculated_area * sign; 3535 } 3536 3537 bool View::isCalculationStopped() 3538 { 3539 if (m_stopCalculating) { 3540 m_stopCalculating = false; 3541 return true; 3542 } else 3543 return false; 3544 } 3545 3546 void View::updateSliders() 3547 { 3548 bool needSliderWindow = false; 3549 for (Function *it : qAsConst(XParser::self()->m_ufkt)) { 3550 if (it->m_parameters.useSlider && !it->allPlotsAreHidden()) { 3551 needSliderWindow = true; 3552 break; 3553 } 3554 } 3555 3556 if (!needSliderWindow) { 3557 if (m_sliderWindow) 3558 m_sliderWindow->hide(); 3559 m_menuSliderAction->setChecked(false); 3560 return; 3561 } 3562 3563 if (!m_sliderWindow) { 3564 m_sliderWindow = new KSliderWindow(this); 3565 connect(m_sliderWindow, &KSliderWindow::valueChanged, this, QOverload<>::of(&View::drawPlot)); 3566 connect(m_sliderWindow, &KSliderWindow::windowClosed, this, &View::sliderWindowClosed); 3567 connect(m_sliderWindow, &KSliderWindow::finished, this, &View::sliderWindowClosed); 3568 } 3569 if (m_menuSliderAction->isChecked()) 3570 m_sliderWindow->show(); 3571 } 3572 3573 void View::sliderWindowClosed() 3574 { 3575 m_menuSliderAction->setChecked(false); // set the slider-item in the menu 3576 } 3577 3578 void View::functionRemoved(int id) 3579 { 3580 if (id == m_currentPlot.functionID()) { 3581 m_currentPlot.setFunctionID(-1); 3582 setStatusBar(QString(), RootSection); 3583 setStatusBar(QString(), FunctionSection); 3584 } 3585 } 3586 3587 void View::hideCurrentFunction() 3588 { 3589 if (m_currentPlot.functionID() == -1) 3590 return; 3591 3592 Function *ufkt = m_currentPlot.function(); 3593 ufkt->plotAppearance(m_currentPlot.plotMode).visible = false; 3594 3595 MainDlg::self()->functionEditor()->functionsChanged(); 3596 drawPlot(); 3597 MainDlg::self()->requestSaveCurrentState(); 3598 updateSliders(); 3599 if (m_currentPlot.functionID() == -1) 3600 return; 3601 if (ufkt->allPlotsAreHidden()) { 3602 m_currentPlot.setFunctionID(-1); 3603 QMouseEvent *event = new QMouseEvent(QMouseEvent::KeyPress, QCursor::pos(), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); 3604 mousePressEvent(event); // leave trace mode 3605 delete event; 3606 return; 3607 } else { 3608 QKeyEvent *event = new QKeyEvent(QKeyEvent::KeyPress, Qt::Key_Up, Qt::NoModifier); 3609 keyPressEvent(event); // change selected graph 3610 delete event; 3611 return; 3612 } 3613 } 3614 void View::removeCurrentPlot() 3615 { 3616 if (m_currentPlot.functionID() == -1) 3617 return; 3618 3619 Function *ufkt = m_currentPlot.function(); 3620 Function::Type function_type = ufkt->type(); 3621 if (!XParser::self()->removeFunction(ufkt)) 3622 return; 3623 3624 if (m_currentPlot.functionID() != -1) // if trace mode is enabled 3625 { 3626 m_currentPlot.setFunctionID(-1); 3627 QMouseEvent *event = new QMouseEvent(QMouseEvent::KeyPress, QCursor::pos(), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); 3628 mousePressEvent(event); // leave trace mode 3629 delete event; 3630 } 3631 3632 drawPlot(); 3633 if (function_type == Function::Cartesian) 3634 updateSliders(); 3635 MainDlg::self()->requestSaveCurrentState(); 3636 } 3637 3638 void View::animateFunction() 3639 { 3640 Function *f = m_currentPlot.function(); 3641 if (!f) 3642 return; 3643 3644 ParameterAnimator *anim = new ParameterAnimator(this, f); 3645 anim->show(); 3646 } 3647 3648 void View::editCurrentPlot() 3649 { 3650 MainDlg::self()->functionEditor()->setCurrentFunction(m_currentPlot.functionID()); 3651 } 3652 3653 void View::zoomIn() 3654 { 3655 m_zoomMode = ZoomIn; 3656 updateCursor(); 3657 } 3658 3659 void View::zoomOut() 3660 { 3661 m_zoomMode = ZoomOut; 3662 updateCursor(); 3663 } 3664 3665 void View::zoomToTrigonometric() 3666 { 3667 double rpau = XParser::self()->radiansPerAngleUnit(); 3668 animateZoom(QRectF(-2 * M_PI / rpau, -4.0, 4 * M_PI / rpau, 8.0)); 3669 } 3670 3671 void View::updateCursor() 3672 { 3673 Cursor newCursor = m_prevCursor; 3674 3675 if (m_isDrawing && (m_zoomMode != Translating)) 3676 newCursor = CursorWait; 3677 3678 else 3679 switch (m_zoomMode) { 3680 case AnimatingZoom: 3681 newCursor = CursorArrow; 3682 break; 3683 3684 case Normal: 3685 if (shouldShowCrosshairs()) { 3686 // Don't show any cursor if we're tracing a function or the crosshairs should be shown 3687 newCursor = CursorBlank; 3688 } else 3689 newCursor = CursorArrow; 3690 break; 3691 3692 case ZoomIn: 3693 case ZoomInDrawing: 3694 newCursor = CursorMagnify; 3695 break; 3696 3697 case ZoomOut: 3698 case ZoomOutDrawing: 3699 newCursor = CursorLessen; 3700 break; 3701 3702 case AboutToTranslate: 3703 case Translating: 3704 newCursor = CursorMove; 3705 break; 3706 } 3707 3708 if (newCursor == m_prevCursor) 3709 return; 3710 m_prevCursor = newCursor; 3711 3712 switch (newCursor) { 3713 case CursorWait: 3714 setCursor(Qt::WaitCursor); 3715 break; 3716 case CursorBlank: 3717 setCursor(Qt::BlankCursor); 3718 break; 3719 case CursorArrow: 3720 setCursor(Qt::ArrowCursor); 3721 break; 3722 case CursorCross: 3723 setCursor(Qt::CrossCursor); 3724 break; 3725 case CursorMagnify: 3726 setCursor(QCursor(QIcon::fromTheme("zoom-in").pixmap(48), 22, 15)); 3727 break; 3728 case CursorLessen: 3729 setCursor(QCursor(QIcon::fromTheme("zoom-out").pixmap(48), 22, 15)); 3730 break; 3731 case CursorMove: 3732 setCursor(Qt::SizeAllCursor); 3733 } 3734 } 3735 3736 bool View::shouldShowCrosshairs() const 3737 { 3738 switch (m_zoomMode) { 3739 case Normal: 3740 case ZoomIn: 3741 case ZoomOut: 3742 break; 3743 3744 case AnimatingZoom: 3745 case ZoomInDrawing: 3746 case ZoomOutDrawing: 3747 case AboutToTranslate: 3748 case Translating: 3749 return false; 3750 } 3751 3752 if (m_popupMenuStatus != NoPopup) 3753 return false; 3754 3755 Function *it = m_currentPlot.function(); 3756 3757 return (underMouse() && (!it || crosshairPositionValid(it))); 3758 } 3759 3760 bool View::event(QEvent *e) 3761 { 3762 if (e->type() == QEvent::WindowDeactivate && m_isDrawing) { 3763 m_stopCalculating = true; 3764 return true; 3765 } 3766 return QWidget::event(e); // send the information further 3767 } 3768 3769 void View::setStatusBar(const QString &t, StatusBarSection section) 3770 { 3771 QString text; 3772 if (section == FunctionSection) 3773 text = ' ' + t + ' '; 3774 else 3775 text = t; 3776 3777 if (m_readonly) // if KmPlot is shown as a KPart with e.g Konqueror, it is only possible to change the status bar in one way: to call setStatusBarText 3778 { 3779 m_statusBarText[section] = text; 3780 3781 QString text; 3782 for (int i = 0; i < 4; ++i) { 3783 if (m_statusBarText[i].isEmpty()) 3784 continue; 3785 3786 if (!text.isEmpty()) 3787 text.append(" | "); 3788 3789 text.append(m_statusBarText[i]); 3790 } 3791 3792 emit setStatusBarText(text); 3793 } else { 3794 QDBusReply<void> reply = QDBusInterface(QDBusConnection::sessionBus().baseService(), "/kmplot", "org.kde.kmplot.KmPlot") 3795 .call(QDBus::NoBlock, "setStatusBarText", text, (int)section); 3796 } 3797 } 3798 3799 void View::setPrintHeaderTable(bool status) 3800 { 3801 m_printHeaderTable = status; 3802 } 3803 3804 void View::setPrintBackground(bool status) 3805 { 3806 m_printBackground = status; 3807 } 3808 3809 void View::setPrintWidth(double width) 3810 { 3811 m_printWidth = width; 3812 } 3813 3814 void View::setPrintHeight(double height) 3815 { 3816 m_printHeight = height; 3817 } 3818 3819 QPointF View::getCrosshairPosition() const 3820 { 3821 return m_crosshairPosition; 3822 } 3823 3824 // END class View 3825 3826 // BEGIN class IntegralDrawSettings 3827 IntegralDrawSettings::IntegralDrawSettings() 3828 { 3829 dmin = dmax = 0.0; 3830 draw = false; 3831 } 3832 // END class IntegralDrawSettings