File indexing completed on 2024-04-21 11:13:56

0001 /*
0002     SPDX-FileCopyrightText: 2010 Miha Čančula <miha.cancula@gmail.com>
0003     SPDX-FileCopyrightText: 2018-2022 by Alexander Semke (alexander.semke@web.de)
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "octaveexpression.h"
0009 #include "octavesession.h"
0010 #include "defaultvariablemodel.h"
0011 #include <helpresult.h>
0012 #include "imageresult.h"
0013 #include "settings.h"
0014 #include "textresult.h"
0015 
0016 #include <QApplication>
0017 #include <QDesktopWidget>
0018 #include <QDebug>
0019 #include <QFile>
0020 #include <QDir>
0021 #include <QFileSystemWatcher>
0022 #include <QRegularExpression>
0023 
0024 #include <KLocalizedString>
0025 
0026 static const QString printCommandTemplate = QString::fromLatin1("print(\"%1\", \"-S%2,%3\")");
0027 
0028 static const QStringList plotCommands({
0029     QLatin1String("plot"), QLatin1String("semilogx"), QLatin1String("semilogy"),
0030     QLatin1String("loglog"), QLatin1String("polar"), QLatin1String("contour"),
0031     QLatin1String("bar"), QLatin1String("stairs"), QLatin1String("errorbar"),
0032     QLatin1String("sombrero"), QLatin1String("hist"), QLatin1String("fplot"),
0033     QLatin1String("imshow"), QLatin1String("stem"), QLatin1String("stem3"),
0034     QLatin1String("scatter"), QLatin1String("pareto"), QLatin1String("rose"),
0035     QLatin1String("pie"), QLatin1String("quiver"), QLatin1String("compass"),
0036     QLatin1String("feather"), QLatin1String("pcolor"), QLatin1String("area"),
0037     QLatin1String("fill"), QLatin1String("comet"), QLatin1String("plotmatrix"),
0038     /* 3d-plots */
0039     QLatin1String("plot3"), QLatin1String("mesh"), QLatin1String("meshc"),
0040     QLatin1String("meshz"), QLatin1String("surf"), QLatin1String("surfc"),
0041     QLatin1String("surfl"), QLatin1String("surfnorm"), QLatin1String("isosurface"),
0042     QLatin1String("isonormals"), QLatin1String("isocaps"),
0043     /* 3d-plots defined by a function */
0044     QLatin1String("ezplot3"), QLatin1String("ezmesh"), QLatin1String("ezmeshc"),
0045     QLatin1String("ezsurf"), QLatin1String("ezsurfc"), QLatin1String("cantor_plot2d"),
0046     QLatin1String("cantor_plot3d")});
0047 
0048 const QStringList OctaveExpression::plotExtensions({
0049     QLatin1String("pdf"),
0050     QLatin1String("svg"),
0051     QLatin1String("png")
0052 });
0053 
0054 OctaveExpression::OctaveExpression(Cantor::Session* session, bool internal): Expression(session, internal)
0055 {
0056 }
0057 
0058 OctaveExpression::~OctaveExpression()
0059 {
0060 }
0061 
0062 void OctaveExpression::evaluate()
0063 {
0064     m_plotFilename.clear();
0065 
0066     m_finished = false;
0067     m_plotPending = false;
0068 
0069     session()->enqueueExpression(this);
0070 }
0071 
0072 QString OctaveExpression::internalCommand()
0073 {
0074     QString cmd = command();
0075     auto* octaveSession = static_cast<OctaveSession*>(session());
0076 
0077     if (!isInternal())
0078     {
0079         QStringList cmdWords = cmd.split(QRegularExpression(QStringLiteral("\\b")), QString::SkipEmptyParts);
0080         if (!cmdWords.contains(QLatin1String("help")) && !cmdWords.contains(QLatin1String("completion_matches")))
0081         {
0082             for (const QString& plotCmd : plotCommands)
0083             {
0084                 if (!cmdWords.contains(plotCmd))
0085                     continue;
0086 
0087                 if (octaveSession->isIntegratedPlotsEnabled())
0088                 {
0089                     if (!cmd.endsWith(QLatin1Char(';')) && !cmd.endsWith(QLatin1Char(',')))
0090                         cmd += QLatin1Char(',');
0091 
0092                     m_plotFilename = octaveSession->plotFilePrefixPath() + QString::number(id()) + QLatin1String(".") + plotExtensions[OctaveSettings::inlinePlotFormat()];
0093 
0094                     int w, h;
0095                     if (OctaveSettings::inlinePlotFormat() == 0 || OctaveSettings::inlinePlotFormat() == 1) // for vector formats like PDF and SVG the size for 'print'  is provided in points
0096                     {
0097                         w = OctaveSettings::plotWidth() / 2.54 * 72;
0098                         h = OctaveSettings::plotHeight() / 2.54 * 72;
0099                     }
0100                     else // for raster formats the size for 'print' is provided in pixels
0101                     {
0102                         w = OctaveSettings::plotWidth() / 2.54 * QApplication::desktop()->physicalDpiX();
0103                         h = OctaveSettings::plotHeight() / 2.54 * QApplication::desktop()->physicalDpiX();
0104                     }
0105                     cmd += printCommandTemplate.arg(m_plotFilename, QString::number(w), QString::number(h));
0106 
0107                     auto* watcher = fileWatcher();
0108                     if (!watcher->files().isEmpty())
0109                         watcher->removePaths(watcher->files());
0110 
0111                     // add path works only for existing paths, so create the file
0112                     QFile file(m_plotFilename);
0113                     if (file.open(QFile::WriteOnly))
0114                     {
0115                         file.close();
0116                         watcher->addPath(m_plotFilename);
0117                         m_plotPending = true;
0118                         connect(watcher, &QFileSystemWatcher::fileChanged, this, &OctaveExpression::imageChanged,  Qt::UniqueConnection);
0119                     }
0120 
0121                     cmd = QLatin1String("figure('visible','off');") + cmd;
0122                 }
0123                 else
0124                     cmd = QLatin1String("figure('visible','on');") + cmd;
0125 
0126                 break;
0127             }
0128         }
0129     }
0130 
0131     // We need to remove all comments here, because below we merge all strings to one long string
0132     // Otherwise, all code after line with comment will be commented out after merging
0133     // So, this small state machine removes all comments.
0134     // status:
0135     // 0 - command mode
0136     // 1 - string mode for '
0137     // 2 - string mode for "
0138     // 3 - comment mode
0139     // 4 - comment character after '
0140 
0141     QString tmpCmd;
0142     QString tmpComment;
0143     int status = 0;
0144     for (int i = 0; i < cmd.size(); i++)
0145     {
0146         const char ch = cmd[i].toLatin1();
0147         if (status == 0 && (ch == '#' || ch == '%'))
0148             status = 3;
0149         else if (status == 0 && ch == '\'')
0150             status = 1;
0151         else if (status == 4 && ch == '\'')
0152         {
0153             tmpCmd += tmpComment;
0154             tmpComment.clear();
0155             status = 0;
0156         }
0157         else if (status == 0 && ch == '"')
0158             status = 2;
0159         else if (status == 1 && ch == '\'')
0160             status = 0;
0161         else if (status == 1 && (ch == '#' || ch == '%'))
0162             status = 4;
0163         else if (status == 2 && ch == '"')
0164             status = 0;
0165         else if (ch == '\n')
0166         {
0167             tmpComment.clear();
0168             status = 0;
0169         }
0170 
0171         if (status == 4)
0172             tmpComment += cmd[i];
0173         else if (status != 3)
0174             tmpCmd += cmd[i];
0175     }
0176 
0177     // Remove "\n" in the beginning of the command, if present
0178     while(tmpCmd[0] == QLatin1Char('\n'))
0179         tmpCmd.remove(0, 1);
0180 
0181     cmd = tmpCmd;
0182     cmd.replace(QLatin1String(";\n"), QLatin1String(";"));
0183     cmd.replace(QLatin1Char('\n'), QLatin1Char(','));
0184     cmd += QLatin1Char('\n');
0185 
0186     return cmd;
0187 }
0188 
0189 void OctaveExpression::parseOutput(const QString& output)
0190 {
0191     if (output.size() > 200)
0192         qDebug() << "parseOutput: " << output.left(200) << "...";
0193     else
0194         qDebug() << "parseOutput: " << output;
0195 
0196     if (!output.trimmed().isEmpty())
0197     {
0198         // TODO: what about help in comment? printf with '... help ...'?
0199         // This must be corrected.
0200         if (command().contains(QLatin1String("help")))
0201             addResult(new Cantor::HelpResult(output));
0202         else
0203             addResult(new Cantor::TextResult(output));
0204     }
0205 
0206     m_finished = true;
0207     if (!m_plotPending)
0208         setStatus(Done);
0209 }
0210 
0211 void OctaveExpression::parseError(const QString& error)
0212 {
0213     if (error.startsWith(QLatin1String("warning: ")))
0214     {
0215         // It's warning, so add as result
0216         addResult(new Cantor::TextResult(error));
0217     }
0218     else
0219     {
0220         setErrorMessage(error);
0221         setStatus(Error);
0222     }
0223 }
0224 
0225 void OctaveExpression::imageChanged()
0226 {
0227     QFile file(m_plotFilename);
0228     if(!file.open(QIODevice::ReadOnly)/* || file.size() <= 0*/)
0229     {
0230         m_plotPending = false;
0231         setResult(new Cantor::TextResult(i18n("Invalid image file generated.")));
0232         setStatus(Error);
0233         return;
0234     }
0235 
0236     const QUrl& url = QUrl::fromLocalFile(m_plotFilename);
0237     auto* newResult = new Cantor::ImageResult(url);
0238 
0239     bool found = false;
0240     for (int i = 0; i < results().size(); i++)
0241         if (results()[i]->type() == newResult->type())
0242         {
0243             replaceResult(i, newResult);
0244             found = true;
0245         }
0246 
0247     if (!found)
0248         addResult(newResult);
0249 
0250     m_plotPending = false;
0251 
0252     if (m_finished && status() == Expression::Computing)
0253         setStatus(Done);
0254 }