diff --git a/src/backend/datasources/LiveDataSource.cpp b/src/backend/datasources/LiveDataSource.cpp index 8edc09f61..cba9ea0bc 100644 --- a/src/backend/datasources/LiveDataSource.cpp +++ b/src/backend/datasources/LiveDataSource.cpp @@ -1,951 +1,956 @@ /*************************************************************************** File : LiveDataSource.cpp Project : LabPlot Description : Represents live data source -------------------------------------------------------------------- Copyright : (C) 2009-2017 Alexander Semke (alexander.semke@web.de) Copyright : (C) 2017 Fabian Kristof (fkristofszabolcs@gmail.com) Copyright : (C) 2018 Stefan Gerlach (stefan.gerlach@uni.kn) ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, * * Boston, MA 02110-1301 USA * * * ***************************************************************************/ #include "backend/datasources/LiveDataSource.h" #include "backend/datasources/filters/AsciiFilter.h" #include "backend/datasources/filters/FITSFilter.h" #include "backend/datasources/filters/BinaryFilter.h" +#include "backend/datasources/filters/ROOTFilter.h" #include "backend/core/Project.h" #include "kdefrontend/spreadsheet/PlotDataDialog.h" #include "commonfrontend/spreadsheet/SpreadsheetView.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /*! \class LiveDataSource \brief Represents data stored in a file. Reading and writing is done with the help of appropriate I/O-filters. \ingroup datasources */ LiveDataSource::LiveDataSource(const QString& name, bool loading) : Spreadsheet(name, loading), m_updateTimer(new QTimer(this)) { initActions(); connect(m_updateTimer, &QTimer::timeout, this, &LiveDataSource::read); } LiveDataSource::~LiveDataSource() { //stop reading before deleting the objects pauseReading(); delete m_filter; delete m_fileSystemWatcher; delete m_file; delete m_localSocket; delete m_tcpSocket; delete m_serialPort; delete m_updateTimer; } /*! * depending on the update type, periodically or on data changes, starts the timer or activates the file watchers, respectively. */ void LiveDataSource::ready() { DEBUG("LiveDataSource::ready() update type = " << ENUM_TO_STRING(LiveDataSource, UpdateType, m_updateType) << ", interval = " << m_updateInterval); switch (m_updateType) { case TimeInterval: m_updateTimer->start(m_updateInterval); DEBUG("STARTED TIMER. REMAINING TIME = " << m_updateTimer->remainingTime()); break; case NewData: DEBUG("STARTING WATCHER"); watch(); } } void LiveDataSource::initActions() { m_reloadAction = new QAction(QIcon::fromTheme("view-refresh"), i18n("Reload"), this); connect(m_reloadAction, &QAction::triggered, this, &LiveDataSource::read); m_toggleLinkAction = new QAction(i18n("Link the file"), this); m_toggleLinkAction->setCheckable(true); connect(m_toggleLinkAction, &QAction::triggered, this, &LiveDataSource::linkToggled); m_plotDataAction = new QAction(QIcon::fromTheme("office-chart-line"), i18n("Plot data"), this); connect(m_plotDataAction, &QAction::triggered, this, &LiveDataSource::plotData); } QWidget* LiveDataSource::view() const { if (!m_partView) m_partView = new SpreadsheetView(const_cast(this), true); return m_partView; } /*! * \brief Returns a list with the names of the available ports */ QStringList LiveDataSource::availablePorts() { QStringList ports; qDebug() << "available ports count:" << QSerialPortInfo::availablePorts().size(); for (const QSerialPortInfo& sp : QSerialPortInfo::availablePorts()) { ports.append(sp.portName()); DEBUG(" port " << sp.portName().toStdString() << ": " << sp.systemLocation().toStdString() << sp.description().toStdString() << ' ' << sp.manufacturer().toStdString() << ' ' << sp.serialNumber().toStdString()); } // For Testing: // ports.append("/dev/pts/26"); return ports; } /*! * \brief Returns a list with the supported baud rates */ QStringList LiveDataSource::supportedBaudRates() { QStringList baudRates; for (const auto& baud : QSerialPortInfo::standardBaudRates()) baudRates.append(QString::number(baud)); return baudRates; } /*! * \brief Updates this data source at this moment */ void LiveDataSource::updateNow() { DEBUG("LiveDataSource::updateNow() update interval = " << m_updateInterval); m_updateTimer->stop(); read(); //restart the timer after update if (m_updateType == TimeInterval) m_updateTimer->start(m_updateInterval); } /*! * \brief Continue reading from the live data source after it was paused */ void LiveDataSource::continueReading() { m_paused = false; switch (m_updateType) { case TimeInterval: m_updateTimer->start(m_updateInterval); break; case NewData: connect(m_fileSystemWatcher, &QFileSystemWatcher::fileChanged, this, &LiveDataSource::read); } } /*! * \brief Pause the reading of the live data source */ void LiveDataSource::pauseReading() { m_paused = true; switch (m_updateType) { case TimeInterval: m_updateTimer->stop(); break; case NewData: disconnect(m_fileSystemWatcher, &QFileSystemWatcher::fileChanged, this, &LiveDataSource::read); } } void LiveDataSource::setFileName(const QString& name) { m_fileName = name; } QString LiveDataSource::fileName() const { return m_fileName; } /*! * \brief Sets the local socket's server name to name * \param name */ void LiveDataSource::setLocalSocketName(const QString& name) { m_localSocketName = name; } QString LiveDataSource::localSocketName() const { return m_localSocketName; } void LiveDataSource::setFileType(AbstractFileFilter::FileType type) { m_fileType = type; } AbstractFileFilter::FileType LiveDataSource::fileType() const { return m_fileType; } void LiveDataSource::setFilter(AbstractFileFilter* f) { delete m_filter; m_filter = f; } AbstractFileFilter* LiveDataSource::filter() const { return m_filter; } /*! sets whether the file should be watched or not. In the first case the data source will be automatically updated on file changes. */ void LiveDataSource::setFileWatched(bool b) { m_fileWatched = b; } bool LiveDataSource::isFileWatched() const { return m_fileWatched; } /*! * \brief Sets the serial port's baud rate * \param baudrate */ void LiveDataSource::setBaudRate(int baudrate) { m_baudRate = baudrate; } int LiveDataSource::baudRate() const { return m_baudRate; } /*! * \brief Sets the source's update interval to \c interval * \param interval */ void LiveDataSource::setUpdateInterval(int interval) { m_updateInterval = interval; if (!m_paused) m_updateTimer->start(m_updateInterval); } int LiveDataSource::updateInterval() const { return m_updateInterval; } /*! * \brief Sets how many values we should keep when keepLastValues is true * \param keepnvalues */ void LiveDataSource::setKeepNValues(int keepnvalues) { m_keepNValues = keepnvalues; } int LiveDataSource::keepNValues() const { return m_keepNValues; } /*! * \brief Sets the network socket's port to port * \param port */ void LiveDataSource::setPort(quint16 port) { m_port = port; } void LiveDataSource::setBytesRead(qint64 bytes) { m_bytesRead = bytes; } int LiveDataSource::bytesRead() const { return m_bytesRead; } int LiveDataSource::port() const { return m_port; } /*! * \brief Sets the serial port's name to name * \param name */ void LiveDataSource::setSerialPort(const QString& name) { m_serialPortName = name; } QString LiveDataSource::serialPortName() const { return m_serialPortName; } bool LiveDataSource::isPaused() const { return m_paused; } /*! * \brief Sets the sample size to size * \param size */ void LiveDataSource::setSampleSize(int size) { m_sampleSize = size; } int LiveDataSource::sampleSize() const { return m_sampleSize; } /*! * \brief Sets the source's type to sourcetype * \param sourcetype */ void LiveDataSource::setSourceType(SourceType sourcetype) { m_sourceType = sourcetype; } LiveDataSource::SourceType LiveDataSource::sourceType() const { return m_sourceType; } /*! * \brief Sets the source's reading type to readingType * \param readingType */ void LiveDataSource::setReadingType(ReadingType readingType) { m_readingType = readingType; } LiveDataSource::ReadingType LiveDataSource::readingType() const { return m_readingType; } /*! * \brief Sets the source's update type to updatetype and handles this change * \param updatetype */ void LiveDataSource::setUpdateType(UpdateType updatetype) { switch (updatetype) { case NewData: m_updateTimer->stop(); if (m_fileSystemWatcher == nullptr) watch(); else connect(m_fileSystemWatcher, &QFileSystemWatcher::fileChanged, this, &LiveDataSource::read); break; case TimeInterval: if (m_fileSystemWatcher) disconnect(m_fileSystemWatcher, &QFileSystemWatcher::fileChanged, this, &LiveDataSource::read); } m_updateType = updatetype; } LiveDataSource::UpdateType LiveDataSource::updateType() const { return m_updateType; } /*! * \brief Sets the network socket's host * \param host */ void LiveDataSource::setHost(const QString& host) { m_host = host.simplified(); } QString LiveDataSource::host() const { return m_host; } /*! sets whether only a link to the file is saved in the project file (\c b=true) or the whole content of the file (\c b=false). */ void LiveDataSource::setFileLinked(bool b) { m_fileLinked = b; } /*! returns \c true if only a link to the file is saved in the project file. \c false otherwise. */ bool LiveDataSource::isFileLinked() const { return m_fileLinked; } QIcon LiveDataSource::icon() const { QIcon icon; switch (m_fileType) { case AbstractFileFilter::Ascii: icon = QIcon::fromTheme("text-plain"); break; case AbstractFileFilter::Binary: icon = QIcon::fromTheme("application-octet-stream"); break; case AbstractFileFilter::Image: icon = QIcon::fromTheme("image-x-generic"); break; // TODO: missing icons case AbstractFileFilter::HDF5: case AbstractFileFilter::NETCDF: break; case AbstractFileFilter::FITS: icon = QIcon::fromTheme("kstars_fitsviewer"); break; case AbstractFileFilter::JSON: icon = QIcon::fromTheme("application-json"); break; case AbstractFileFilter::ROOT: case AbstractFileFilter::NgspiceRawAscii: case AbstractFileFilter::NgspiceRawBinary: break; } return icon; } QMenu* LiveDataSource::createContextMenu() { QMenu* menu = AbstractPart::createContextMenu(); QAction* firstAction = nullptr; // if we're populating the context menu for the project explorer, then //there're already actions available there. Skip the first title-action //and insert the action at the beginning of the menu. if (menu->actions().size() > 1) firstAction = menu->actions().at(1); menu->insertAction(firstAction, m_plotDataAction); menu->insertSeparator(firstAction); //TODO: doesn't always make sense... // if (!m_fileWatched) // menu->insertAction(firstAction, m_reloadAction); // // m_toggleWatchAction->setChecked(m_fileWatched); // menu->insertAction(firstAction, m_toggleWatchAction); // // m_toggleLinkAction->setChecked(m_fileLinked); // menu->insertAction(firstAction, m_toggleLinkAction); return menu; } //############################################################################## //################################# SLOTS #################################### //############################################################################## /* * called periodically or on new data changes (file changed, new data in the socket, etc.) */ void LiveDataSource::read() { DEBUG("\nLiveDataSource::read()"); if (!m_filter) return; //initialize the device (file, socket, serial port) when calling this function for the first time if (!m_prepared) { DEBUG(" Preparing device: update type = " << ENUM_TO_STRING(LiveDataSource, UpdateType, m_updateType)); switch (m_sourceType) { case FileOrPipe: m_file = new QFile(m_fileName); m_device = m_file; break; case NetworkTcpSocket: m_tcpSocket = new QTcpSocket(this); m_device = m_tcpSocket; m_tcpSocket->connectToHost(m_host, m_port, QIODevice::ReadOnly); connect(m_tcpSocket, &QTcpSocket::readyRead, this, &LiveDataSource::readyRead); connect(m_tcpSocket, static_cast(&QTcpSocket::error), this, &LiveDataSource::tcpSocketError); break; case NetworkUdpSocket: m_udpSocket = new QUdpSocket(this); m_device = m_udpSocket; m_udpSocket->bind(QHostAddress(m_host), m_port); m_udpSocket->connectToHost(m_host, 0, QUdpSocket::ReadOnly); // only connect to readyRead when update is on new data if (m_updateType == NewData) connect(m_udpSocket, &QUdpSocket::readyRead, this, &LiveDataSource::readyRead); connect(m_udpSocket, static_cast(&QUdpSocket::error), this, &LiveDataSource::tcpSocketError); break; case LocalSocket: m_localSocket = new QLocalSocket(this); m_device = m_localSocket; m_localSocket->connectToServer(m_localSocketName, QLocalSocket::ReadOnly); connect(m_localSocket, &QLocalSocket::readyRead, this, &LiveDataSource::readyRead); connect(m_localSocket, static_cast(&QLocalSocket::error), this, &LiveDataSource::localSocketError); break; case SerialPort: m_serialPort = new QSerialPort(this); m_device = m_serialPort; DEBUG(" Serial: " << m_serialPortName.toStdString() << ", " << m_baudRate); m_serialPort->setBaudRate(m_baudRate); m_serialPort->setPortName(m_serialPortName); m_serialPort->open(QIODevice::ReadOnly); // only connect to readyRead when update is on new data if (m_updateType == NewData) connect(m_serialPort, &QSerialPort::readyRead, this, &LiveDataSource::readyRead); connect(m_serialPort, static_cast(&QSerialPort::error), this, &LiveDataSource::serialPortError); break; case MQTT: break; } m_prepared = true; } qint64 bytes = 0; switch (m_sourceType) { case FileOrPipe: DEBUG("Reading FileOrPipe. type = " << ENUM_TO_STRING(AbstractFileFilter, FileType, m_fileType)); switch (m_fileType) { case AbstractFileFilter::Ascii: if (m_readingType == LiveDataSource::ReadingType::WholeFile) { static_cast(m_filter)->readFromLiveDevice(*m_file, this, 0); } else { bytes = static_cast(m_filter)->readFromLiveDevice(*m_file, this, m_bytesRead); m_bytesRead += bytes; DEBUG("Read " << bytes << " bytes, in total: " << m_bytesRead); } break; case AbstractFileFilter::Binary: //TODO: not implemented yet // bytes = qSharedPointerCast(m_filter)->readFromLiveDevice(*m_file, this, m_bytesRead); // m_bytesRead += bytes; + case AbstractFileFilter::ROOT: + case AbstractFileFilter::NgspiceRawAscii: + case AbstractFileFilter::NgspiceRawBinary: + //only re-reading of the whole file is supported + m_filter->readDataFromFile(m_fileName, this); + break; //TODO: other types not implemented yet case AbstractFileFilter::Image: case AbstractFileFilter::HDF5: case AbstractFileFilter::NETCDF: case AbstractFileFilter::FITS: case AbstractFileFilter::JSON: - case AbstractFileFilter::ROOT: - break; - case AbstractFileFilter::NgspiceRawAscii: - case AbstractFileFilter::NgspiceRawBinary: - //only re-reading of the whole file is supported - m_filter->readDataFromFile(m_fileName, this); break; } break; case NetworkTcpSocket: DEBUG("reading from TCP socket. state before abort = " << m_tcpSocket->state()); m_tcpSocket->abort(); m_tcpSocket->connectToHost(m_host, m_port, QIODevice::ReadOnly); DEBUG("reading from TCP socket. state after reconnect = " << m_tcpSocket->state()); break; case NetworkUdpSocket: DEBUG(" Reading from UDP socket. state = " << m_udpSocket->state()); // reading data here if (m_fileType == AbstractFileFilter::Ascii) static_cast(m_filter)->readFromLiveDeviceNotFile(*m_device, this); break; case LocalSocket: DEBUG(" Reading from local socket. state before abort = " << m_localSocket->state()); if (m_localSocket->state() == QLocalSocket::ConnectingState) m_localSocket->abort(); m_localSocket->connectToServer(m_localSocketName, QLocalSocket::ReadOnly); if (m_localSocket->waitForConnected()) m_localSocket->waitForReadyRead(); DEBUG(" Reading from local socket. state after reconnect = " << m_localSocket->state()); break; case SerialPort: DEBUG(" Reading from serial port"); // reading data here if (m_fileType == AbstractFileFilter::Ascii) static_cast(m_filter)->readFromLiveDeviceNotFile(*m_device, this); break; case MQTT: break; } } /*! * Slot for the signal that is emitted once every time new data is available for reading from the device (not UDP or Serial). * It will only be emitted again once new data is available, such as when a new payload of network data has arrived on the network socket, * or when a new block of data has been appended to your device. */ void LiveDataSource::readyRead() { DEBUG("LiveDataSource::readyRead() update type = " << ENUM_TO_STRING(LiveDataSource,UpdateType,m_updateType)); DEBUG(" REMAINING TIME = " << m_updateTimer->remainingTime()); if (m_fileType == AbstractFileFilter::Ascii) static_cast(m_filter)->readFromLiveDeviceNotFile(*m_device, this); //TODO: not implemented yet // else if (m_fileType == AbstractFileFilter::Binary) // dynamic_cast(m_filter)->readFromLiveDeviceNotFile(*m_device, this); //since we won't have the timer to call read() where we create new connections //for sequential devices in read() we just request data/connect to servers if (m_updateType == NewData) read(); } void LiveDataSource::localSocketError(QLocalSocket::LocalSocketError socketError) { Q_UNUSED(socketError); /*disconnect(m_localSocket, SIGNAL(error(QLocalSocket::LocalSocketError)), this, SLOT(localSocketError(QLocalSocket::LocalSocketError))); disconnect(m_localSocket, SIGNAL(readyRead()), this, SLOT(readyRead()));*/ /*switch (socketError) { case QLocalSocket::ServerNotFoundError: QMessageBox::critical(0, i18n("Local Socket Error"), i18n("The socket was not found. Please check the socket name.")); break; case QLocalSocket::ConnectionRefusedError: QMessageBox::critical(0, i18n("Local Socket Error"), i18n("The connection was refused by the peer")); break; case QLocalSocket::PeerClosedError: QMessageBox::critical(0, i18n("Local Socket Error"), i18n("The socket has closed the connection.")); break; default: QMessageBox::critical(0, i18n("Local Socket Error"), i18n("The following error occurred: %1.", m_localSocket->errorString())); }*/ } void LiveDataSource::tcpSocketError(QAbstractSocket::SocketError socketError) { Q_UNUSED(socketError); /*switch (socketError) { case QAbstractSocket::ConnectionRefusedError: QMessageBox::critical(0, i18n("TCP Socket Error"), i18n("The connection was refused by the peer. Make sure the server is running and check the host name and port settings.")); break; case QAbstractSocket::RemoteHostClosedError: QMessageBox::critical(0, i18n("TCP Socket Error"), i18n("The remote host closed the connection.")); break; case QAbstractSocket::HostNotFoundError: QMessageBox::critical(0, i18n("TCP Socket Error"), i18n("The host was not found. Please check the host name and port settings.")); break; default: QMessageBox::critical(0, i18n("TCP Socket Error"), i18n("The following error occurred: %1.", m_tcpSocket->errorString())); }*/ } void LiveDataSource::serialPortError(QSerialPort::SerialPortError serialPortError) { switch (serialPortError) { case QSerialPort::DeviceNotFoundError: QMessageBox::critical(nullptr, i18n("Serial Port Error"), i18n("Failed to open the device.")); break; case QSerialPort::PermissionError: QMessageBox::critical(nullptr, i18n("Serial Port Error"), i18n("Failed to open the device. Please check your permissions on this device.")); break; case QSerialPort::OpenError: QMessageBox::critical(nullptr, i18n("Serial Port Error"), i18n("Device already opened.")); break; case QSerialPort::NotOpenError: QMessageBox::critical(nullptr, i18n("Serial Port Error"), i18n("The device is not opened.")); break; case QSerialPort::ReadError: QMessageBox::critical(nullptr, i18n("Serial Port Error"), i18n("Failed to read data.")); break; case QSerialPort::ResourceError: QMessageBox::critical(nullptr, i18n("Serial Port Error"), i18n("Failed to read data. The device is removed.")); break; case QSerialPort::TimeoutError: QMessageBox::critical(nullptr, i18n("Serial Port Error"), i18n("The device timed out.")); break; #ifndef _MSC_VER //MSVC complains about the usage of deprecated enums, g++ and clang complain about missing enums case QSerialPort::ParityError: case QSerialPort::FramingError: case QSerialPort::BreakConditionError: #endif case QSerialPort::WriteError: case QSerialPort::UnsupportedOperationError: case QSerialPort::UnknownError: QMessageBox::critical(nullptr, i18n("Serial Port Error"), i18n("The following error occurred: %1.", m_serialPort->errorString())); break; case QSerialPort::NoError: break; } } void LiveDataSource::watchToggled() { m_fileWatched = !m_fileWatched; watch(); project()->setChanged(true); } void LiveDataSource::linkToggled() { m_fileLinked = !m_fileLinked; project()->setChanged(true); } /*! * Watch the file upon reading for changes if required * uses m_fileName for FileOrPipe and LocalSocket */ void LiveDataSource::watch() { DEBUG("LiveDataSource::watch() file name = " << m_fileName.toStdString()); if (m_fileWatched) { DEBUG(" file is watched"); if (!m_fileSystemWatcher) { m_fileSystemWatcher = new QFileSystemWatcher; connect(m_fileSystemWatcher, &QFileSystemWatcher::fileChanged, this, &LiveDataSource::read); } if (!m_fileSystemWatcher->files().contains(m_fileName)) m_fileSystemWatcher->addPath(m_fileName); } else { DEBUG(" file is not watched"); if (m_fileSystemWatcher) m_fileSystemWatcher->removePath(m_fileName); } } void LiveDataSource::plotData() { auto* dlg = new PlotDataDialog(this); dlg->exec(); } //############################################################################## //################## Serialization/Deserialization ########################### //############################################################################## /*! Saves as XML. */ void LiveDataSource::save(QXmlStreamWriter* writer) const { writer->writeStartElement("LiveDataSource"); writeBasicAttributes(writer); writeCommentElement(writer); //general writer->writeStartElement("general"); writer->writeAttribute("fileName", m_fileName); writer->writeAttribute("fileType", QString::number(m_fileType)); writer->writeAttribute("fileWatched", QString::number(m_fileWatched)); writer->writeAttribute("fileLinked", QString::number(m_fileLinked)); writer->writeAttribute("updateType", QString::number(m_updateType)); writer->writeAttribute("readingType", QString::number(m_readingType)); writer->writeAttribute("sourceType", QString::number(m_sourceType)); writer->writeAttribute("keepNValues", QString::number(m_keepNValues)); if (m_updateType == TimeInterval) writer->writeAttribute("updateInterval", QString::number(m_updateInterval)); if (m_readingType != TillEnd) writer->writeAttribute("sampleSize", QString::number(m_sampleSize)); switch (m_sourceType) { case SerialPort: writer->writeAttribute("baudRate", QString::number(m_baudRate)); writer->writeAttribute("serialPortName", m_serialPortName); break; case NetworkTcpSocket: case NetworkUdpSocket: writer->writeAttribute("host", m_host); writer->writeAttribute("port", QString::number(m_port)); break; case FileOrPipe: break; case LocalSocket: break; case MQTT: break; default: break; } writer->writeEndElement(); //filter m_filter->save(writer); //columns if (!m_fileLinked) { for (auto* col : children(IncludeHidden)) col->save(writer); } writer->writeEndElement(); // "LiveDataSource" } /*! Loads from XML. */ bool LiveDataSource::load(XmlStreamReader* reader, bool preview) { if (!readBasicAttributes(reader)) return false; KLocalizedString attributeWarning = ki18n("Attribute '%1' missing or empty, default value is used"); QXmlStreamAttributes attribs; QString str; while (!reader->atEnd()) { reader->readNext(); if (reader->isEndElement() && reader->name() == "LiveDataSource") break; if (!reader->isStartElement()) continue; if (reader->name() == "comment") { if (!readCommentElement(reader)) return false; } else if (reader->name() == "general") { attribs = reader->attributes(); str = attribs.value("fileName").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("fileName").toString()); else m_fileName = str; str = attribs.value("fileType").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("fileType").toString()); else m_fileType = (AbstractFileFilter::FileType)str.toInt(); str = attribs.value("fileWatched").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("fileWatched").toString()); else m_fileWatched = str.toInt(); str = attribs.value("fileLinked").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("fileLinked").toString()); else m_fileLinked = str.toInt(); str = attribs.value("updateType").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("updateType").toString()); else m_updateType = static_cast(str.toInt()); str = attribs.value("sourceType").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("sourceType").toString()); else m_sourceType = static_cast(str.toInt()); str = attribs.value("readingType").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("readingType").toString()); else m_readingType = static_cast(str.toInt()); if (m_updateType == TimeInterval) { str = attribs.value("updateInterval").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("updateInterval").toString()); else m_updateInterval = str.toInt(); } if (m_readingType != TillEnd) { str = attribs.value("sampleSize").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("sampleSize").toString()); else m_sampleSize = str.toInt(); } switch (m_sourceType) { case SerialPort: str = attribs.value("baudRate").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("baudRate").toString()); else m_baudRate = str.toInt(); str = attribs.value("serialPortName").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("serialPortName").toString()); else m_serialPortName = str; break; case NetworkTcpSocket: case NetworkUdpSocket: str = attribs.value("host").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("host").toString()); else m_host = str; str = attribs.value("port").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("port").toString()); else m_host = str; break; case MQTT: break; case FileOrPipe: break; case LocalSocket: break; default: break; } } else if (reader->name() == "asciiFilter") { setFilter(new AsciiFilter); if (!m_filter->load(reader)) return false; + } else if (reader->name() == "rootFilter") { + setFilter(new ROOTFilter); + if (!m_filter->load(reader)) + return false; } else if (reader->name() == "column") { Column* column = new Column("", AbstractColumn::Text); if (!column->load(reader, preview)) { delete column; setColumnCount(0); return false; } addChild(column); } else {// unknown element reader->raiseWarning(i18n("unknown element '%1'", reader->name().toString())); if (!reader->skipToEndElement()) return false; } } //read the content of the file if it was only linked if (m_fileLinked) this->read(); return !reader->hasError(); } diff --git a/src/backend/datasources/filters/ROOTFilter.cpp b/src/backend/datasources/filters/ROOTFilter.cpp index d4116d654..7bc6fa4f6 100644 --- a/src/backend/datasources/filters/ROOTFilter.cpp +++ b/src/backend/datasources/filters/ROOTFilter.cpp @@ -1,1365 +1,1409 @@ /*************************************************************************** File : ROOTFilter.cpp Project : LabPlot Description : ROOT(CERN) I/O-filter -------------------------------------------------------------------- Copyright : (C) 2018 by Christoph Roick (chrisito@gmx.de) Copyright : (C) 2018 by Stefan Gerlach (stefan.gerlach@uni.kn) ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, * * Boston, MA 02110-1301 USA * * * ***************************************************************************/ #include "backend/datasources/filters/ROOTFilter.h" #include "backend/datasources/filters/ROOTFilterPrivate.h" #include "backend/spreadsheet/Spreadsheet.h" #include "backend/core/column/Column.h" #include #include +#include #ifdef HAVE_ZIP #include #include #endif #include #include #include #include #include #include ROOTFilter::ROOTFilter():AbstractFileFilter(ROOT), d(new ROOTFilterPrivate) {} ROOTFilter::~ROOTFilter() = default; void ROOTFilter::readDataFromFile(const QString& fileName, AbstractDataSource* dataSource, AbstractFileFilter::ImportMode importMode) { d->readDataFromFile(fileName, dataSource, importMode); } void ROOTFilter::write(const QString& fileName, AbstractDataSource* dataSource) { d->write(fileName, dataSource); } void ROOTFilter::loadFilterSettings(const QString& filterName) { Q_UNUSED(filterName); } void ROOTFilter::saveFilterSettings(const QString& filterName) const { Q_UNUSED(filterName); } void ROOTFilter::setCurrentObject(const QString& object) { d->currentObject = object; } const QString ROOTFilter::currentObject() const { return d->currentObject; } QStringList ROOTFilter::listHistograms(const QString& fileName) const { return d->listHistograms(fileName); } QStringList ROOTFilter::listTrees(const QString& fileName) const { return d->listTrees(fileName); } QVector ROOTFilter::listLeaves(const QString& fileName, const QString& treeName) const { return d->listLeaves(fileName, treeName); } QVector ROOTFilter::previewCurrentObject(const QString& fileName, int first, int last) const { return d->previewCurrentObject(fileName, first, last); } int ROOTFilter::rowsInCurrentObject(const QString& fileName) const { return d->rowsInCurrentObject(fileName); } void ROOTFilter::setStartRow(const int s) { d->startRow = s; } int ROOTFilter::startRow() const { return d->startRow; } void ROOTFilter::setEndRow(const int e) { d->endRow = e; } int ROOTFilter::endRow() const { return d->endRow; } void ROOTFilter::setColumns(const QVector& columns) { d->columns = columns; } QVector ROOTFilter::columns() const { return d->columns; } void ROOTFilter::save(QXmlStreamWriter* writer) const { writer->writeStartElement("rootFilter"); - writer->writeAttribute("startRow", QString::number(d->startRow) ); - writer->writeAttribute("endRow", QString::number(d->endRow) ); + writer->writeAttribute("object", d->currentObject); + writer->writeAttribute("startRow", QString::number(d->startRow)); + writer->writeAttribute("endRow", QString::number(d->endRow)); + for (const auto & c : d->columns) { + writer->writeStartElement("column"); + for (const auto & s : c) + writer->writeTextElement("id", s); + writer->writeEndElement(); + } writer->writeEndElement(); } bool ROOTFilter::load(XmlStreamReader* reader) { QString attributeWarning = i18n("Attribute '%1' missing or empty, default value is used"); QXmlStreamAttributes attribs = reader->attributes(); // read attributes + d->currentObject = attribs.value("object").toString(); + if (d->currentObject.isEmpty()) + reader->raiseWarning(attributeWarning.arg("object")); + QString str = attribs.value("startRow").toString(); if (str.isEmpty()) - reader->raiseWarning(attributeWarning.arg("'startRow'")); + reader->raiseWarning(attributeWarning.arg("startRow")); else d->startRow = str.toInt(); str = attribs.value("endRow").toString(); if (str.isEmpty()) - reader->raiseWarning(attributeWarning.arg("'endRow'")); + reader->raiseWarning(attributeWarning.arg("endRow")); else d->endRow = str.toInt(); + d->columns.clear(); + while (reader->readNextStartElement()) { + if (reader->name() == "column") { + QStringList c; + while (reader->readNextStartElement()) { + if (reader->name() == "id") + c << reader->readElementText(); + else + reader->skipCurrentElement(); + } + if (!c.empty()) + d->columns << c; + } else + reader->skipCurrentElement(); + } + if (d->columns.empty()) + reader->raiseWarning(i18n("No column available")); + return true; } /**************** ROOTFilterPrivate implementation *******************/ ROOTFilterPrivate::ROOTFilterPrivate() = default; void ROOTFilterPrivate::readDataFromFile(const QString& fileName, AbstractDataSource* dataSource, AbstractFileFilter::ImportMode importMode) { DEBUG("ROOTFilterPrivate::readDataFromFile()"); setFile(fileName); QStringList typeobject = currentObject.split(':'); if (typeobject.size() < 2) return; if (typeobject.first() == QStringLiteral("Hist")) { typeobject.removeFirst(); auto bins = readHistogram(typeobject.join(':')); const int nbins = static_cast(bins.size()); // skip underflow and overflow bins by default int first = qMax(qAbs(startRow), 0); int last = endRow < 0 ? nbins - 1 : qMax(first - 1, qMin(endRow, nbins - 1)); QStringList headers; for (const auto& l : columns) { headers << l.last(); } QVector dataContainer; const int columnOffset = dataSource->prepareImport(dataContainer, importMode, last - first + 1, columns.size(), headers, QVector(columns.size(), AbstractColumn::Numeric)); // read data DEBUG(" reading " << first - last + 1 << " lines"); int c = 0; Spreadsheet* spreadsheet = dynamic_cast(dataSource); for (const auto& l : columns) { QVector& container = *static_cast*>(dataContainer[c]); if (l.first() == QStringLiteral("center")) { if (spreadsheet) spreadsheet->column(columnOffset + c)->setPlotDesignation(Column::X); for (int i = first; i <= last; ++i) container[i - first] = (i > 0 && i < nbins - 1) ? 0.5 * (bins[i].lowedge + bins[i + 1].lowedge) : i == 0 ? bins.front().lowedge // -infinity : -bins.front().lowedge; // +infinity } else if (l.first() == QStringLiteral("low")) { if (spreadsheet) spreadsheet->column(columnOffset + c)->setPlotDesignation(Column::X); for (int i = first; i <= last; ++i) container[i - first] = bins[i].lowedge; } else if (l.first() == QStringLiteral("content")) { if (spreadsheet) spreadsheet->column(columnOffset + c)->setPlotDesignation(Column::Y); for (int i = first; i <= last; ++i) container[i - first] = bins[i].content; } else if (l.first() == QStringLiteral("error")) { if (spreadsheet) spreadsheet->column(columnOffset + c)->setPlotDesignation(Column::YError); for (int i = first; i <= last; ++i) container[i - first] = std::sqrt(bins[i].sumw2); } ++c; } dataSource->finalizeImport(columnOffset, 0, columns.size() - 1, -1, QString(), importMode); } else if (typeobject.first() == QStringLiteral("Tree")) { typeobject.removeFirst(); const QString treeName = typeobject.join(':'); const int nentries = static_cast(currentROOTData->treeEntries(treeName.toStdString())); int first = qMax(qAbs(startRow), 0); int last = qMax(first - 1, qMin(endRow, nentries - 1)); QStringList headers; for (const auto& l : columns) { QString lastelement = l.back(), leaf = l.front(); bool isArray = false; if (lastelement.at(0) == '[' && lastelement.at(lastelement.size() - 1) == ']') { lastelement.mid(1, lastelement.length() - 2).toUInt(&isArray); } if (!isArray || l.count() == 2) headers << l.join(isArray ? QString() : QString(':')); else headers << l.first() + QChar(':') + l.at(1) + l.back(); } QVector dataContainer; const int columnOffset = dataSource->prepareImport(dataContainer, importMode, last - first + 1, columns.size(), headers, QVector(columns.size(), AbstractColumn::Numeric)); int c = 0; for (const auto& l : columns) { unsigned int element = 0; QString lastelement = l.back(), leaf = l.front(); bool isArray = false; if (lastelement.at(0) == '[' && lastelement.at(lastelement.size() - 1) == ']') { element = lastelement.mid(1, lastelement.length() - 2).toUInt(&isArray); if (!isArray) element = 0; if (l.count() > 2) leaf = l.at(1); } else if (l.count() > 1) leaf = l.at(1); QVector& container = *static_cast*>(dataContainer[c++]); auto data = readTree(treeName, l.first(), leaf, (int)element, last); for (int i = first; i <= last; ++i) container[i - first] = data[i]; } dataSource->finalizeImport(columnOffset, 0, columns.size() - 1, -1, QString(), importMode); } } void ROOTFilterPrivate::write(const QString& fileName, AbstractDataSource* dataSource) { Q_UNUSED(fileName); Q_UNUSED(dataSource); } QStringList ROOTFilterPrivate::listHistograms(const QString& fileName) { setFile(fileName); QStringList histList; for (const auto& hist : currentROOTData->listHistograms()) { histList << QString::fromStdString(hist); } return histList; } QStringList ROOTFilterPrivate::listTrees(const QString& fileName) { setFile(fileName); QStringList treeList; for (const auto& tree : currentROOTData->listTrees()) { treeList << QString::fromStdString(tree); } return treeList; } QVector ROOTFilterPrivate::listLeaves(const QString& fileName, const QString& treeName) { setFile(fileName); QVector leafList; for (const auto& leaf : currentROOTData->listLeaves(treeName.toStdString())) { leafList << QStringList(QString::fromStdString(leaf.branch)); if (leaf.branch != leaf.leaf) leafList.last() << QString::fromStdString(leaf.leaf); if (leaf.elements > 1) leafList.last() << QString("[%1]").arg(leaf.elements); } return leafList; } QVector ROOTFilterPrivate::previewCurrentObject(const QString& fileName, int first, int last) { DEBUG("ROOTFilterPrivate::previewCurrentObject()"); setFile(fileName); QStringList typeobject = currentObject.split(':'); if (typeobject.size() < 2) return QVector(1, QStringList()); if (typeobject.first() == QStringLiteral("Hist")) { typeobject.removeFirst(); auto bins = readHistogram(typeobject.join(':')); const int nbins = static_cast(bins.size()); last = qMin(nbins - 1, last); QVector preview(qMax(last - first + 2, 1)); DEBUG(" reading " << preview.size() - 1 << " lines"); // set headers for (const auto& l : columns) { preview.last() << l.last(); } // read data for (const auto& l : columns) { if (l.first() == QStringLiteral("center")) { for (int i = first; i <= last; ++i) preview[i - first] << QString::number( (i > 0 && i < nbins - 1) ? 0.5 * (bins[i].lowedge + bins[i + 1].lowedge) : i == 0 ? bins.front().lowedge // -infinity : -bins.front().lowedge); // +infinity } else if (l.first() == QStringLiteral("low")) { for (int i = first; i <= last; ++i) preview[i - first] << QString::number(bins[i].lowedge); } else if (l.first() == QStringLiteral("content")) { for (int i = first; i <= last; ++i) preview[i - first] << QString::number(bins[i].content); } else if (l.first() == QStringLiteral("error")) { for (int i = first; i <= last; ++i) preview[i - first] << QString::number(std::sqrt(bins[i].sumw2)); } } return preview; } else if (typeobject.first() == QStringLiteral("Tree")) { typeobject.removeFirst(); const QString treeName = typeobject.join(':'); last = qMin(last, currentROOTData->treeEntries(treeName.toStdString()) - 1); QVector preview(qMax(last - first + 2, 1)); DEBUG(" reading " << preview.size() - 1 << " lines"); // read data leaf by leaf and set headers for (const auto& l : columns) { unsigned int element = 0; QString lastelement = l.back(), leaf = l.front(); bool isArray = false; if (lastelement.at(0) == '[' && lastelement.at(lastelement.size() - 1) == ']') { element = lastelement.mid(1, lastelement.length() - 2).toUInt(&isArray); if (!isArray) element = 0; if (l.count() > 2) leaf = l.at(1); } else if (l.count() > 1) leaf = l.at(1); auto data = readTree(treeName, l.first(), leaf, (int)element, last); for (int i = first; i <= last; ++i) preview[i - first] << QString::number(data[i]); if (!isArray || l.count() == 2) preview.last() << l.join(isArray ? QString() : QString(':')); else preview.last() << l.first() + QChar(':') + l.at(1) + l.back(); } return preview; } else return QVector(1, QStringList()); } int ROOTFilterPrivate::rowsInCurrentObject(const QString& fileName) { setFile(fileName); QStringList typeobject = currentObject.split(':'); if (typeobject.size() < 2) return 0; if (typeobject.first() == QStringLiteral("Hist")) { typeobject.removeFirst(); QStringList nameindex = typeobject.join(':').split(';');; bool ok = nameindex.size() > 1; int cycle = ok ? nameindex.last().toInt(&ok) : 1; if (ok) { nameindex.removeLast(); } else { cycle = 1; } return currentROOTData->histogramBins(nameindex.join(';').toStdString(), cycle); } else if (typeobject.first() == QStringLiteral("Tree")) { typeobject.removeFirst(); return currentROOTData->treeEntries(typeobject.join(':').toStdString()); } else return 0; } void ROOTFilterPrivate::setFile(const QString& fileName) { - if (!currentROOTData || fileName != currentFile) { - currentFile = fileName; + QFileInfo file(fileName); + if (!file.exists()) { + currentObject.clear(); + columns.clear(); + currentROOTData.reset(); + return; + } + + QDateTime modified = file.lastModified(); + qint64 size = file.size(); + if (!currentROOTData || fileName != currentFile.name + || modified != currentFile.modified + || size != currentFile.size) { + currentFile.name = fileName; + currentFile.modified = modified; + currentFile.size = size; currentROOTData.reset(new ROOTData(fileName.toStdString())); } } std::vector ROOTFilterPrivate::readHistogram(const QString& histName) { QStringList nameindex = histName.split(';'); bool ok = nameindex.size() > 1; int cycle = ok ? nameindex.last().toInt(&ok) : 1; if (ok) { nameindex.removeLast(); } else { cycle = 1; } return currentROOTData->readHistogram(nameindex.join(';').toStdString(), cycle); } std::vector ROOTFilterPrivate::readTree(const QString& treeName, const QString& branchName, const QString& leafName, int element, int last) { return currentROOTData->listEntries(treeName.toStdString(), branchName.toStdString(), leafName.toStdString(), element, last + 1); } /******************** ROOTData implementation ************************/ namespace ROOTDataHelpers { /// Read value from stream template T read(std::ifstream& is) { union { T val; char buf[sizeof(T)]; } r; for (size_t i = 0; i < sizeof(T); ++i) { is.get(r.buf[sizeof(T) - i - 1]); } return r.val; } /// Read value from buffer template T read(char*& s) { union { T val; char buf[sizeof(T)]; } r; for (size_t i = 0; i < sizeof(T); ++i) { r.buf[sizeof(T) - i - 1] = *(s++); } return r.val; } /// Read value from buffer and cast to U template U readcast(char*& s) { return static_cast(read(s)); } /// Get version of ROOT object, obtain number of bytes in object short Version(char*& buffer, size_t& count) { // root/io/io/src/TBufferFile.cxx -> ReadVersion count = read(buffer); short version = (count & 0x40000000) ? read(buffer) : read(buffer -= 4); count = (count & 0x40000000) ? (count & ~0x40000000) - 2 : 2; return version; } /// Get version of ROOT object short Version(char*& buffer) { size_t c; return Version(buffer, c); } /// Skip ROOT object void Skip(char*& buffer, const size_t& n) { for (size_t i = 0; i < n; ++i) { size_t count; Version(buffer, count); buffer += count; } } /// Skip TObject header void SkipObject(char*& buffer) { Version(buffer); buffer += 8; } /// Get TString std::string String(char*& buffer) { // root/io/io/src/TBufferFile.cxx -> ReadTString size_t s = *(buffer++); if (s == 0) return std::string(); else { if (s == 0xFF) s = read(buffer); buffer += s; return std::string(buffer - s, buffer); } } /// Get the header of an object in TObjArray std::string readObject(char*& buf, char* const buf0, std::map& tags) { // root/io/io/src/TBufferFile.cxx -> ReadObjectAny std::string clname; unsigned int tag = read(buf); if (tag & 0x40000000) { tag = read(buf); if (tag == 0xFFFFFFFF) { tags[buf - buf0 - 2] = clname = buf; buf += clname.size() + 1; } else { clname = tags[tag & ~0x80000000]; } } return clname; } } using namespace ROOTDataHelpers; ROOTData::ROOTData(const std::string& filename) : filename(filename) { // The file structure is described in root/io/io/src/TFile.cxx std::ifstream is(filename, std::ifstream::binary); std::string root(4, 0); is.read(const_cast(root.data()), 4); if (root != "root") return; int fileVersion = read(is); long int pos = read(is); long int endpos = fileVersion < 1000000 ? read(is) : read(is); is.seekg(33); int compression = read(is); compression = compression > 0 ? compression : 0; while (is.good() && pos < endpos) { is.seekg(pos); int lcdata = read(is); if (lcdata == 0) { break; } if (lcdata < 0) { pos -= lcdata; continue; } short version = read(is); size_t ldata = read(is); is.seekg(4, is.cur); // skip the date size_t lkey = read(is); short cycle = read(is); is.seekg(version > 1000 ? 16 : 8, is.cur); // skip seek positions std::string cname(read(is), 0); is.read(&cname[0], cname.size()); std::string name(read(is), 0); is.read(&name[0], name.size()); std::string title(read(is), 0); is.read(&title[0], title.size()); ContentType type = Invalid; if (cname.size() == 4 && cname.substr(0, 3) == "TH1") { type = histType(cname[3]); } else if (cname == "TTree") type = Tree; else if (cname.substr(0, 7) == "TNtuple") type = NTuple; else if (cname == "TBasket") type = Basket; else if (cname == "TList" && name == "StreamerInfo") type = Streamer; if (type) { if (type == Basket) is.seekg(19, std::ifstream::cur); // TODO read info instead? KeyBuffer buffer; buffer.type = Invalid; // see root/io/io/src/TKey.cxx for reference int complib = 0; if (compression) { // Default: compression level // ZLIB: 100 + compression level // LZ4: 400 + compression level // do not rely on this, but read the header std::string lib(2, 0); is.read(&lib[0], 2); complib = lib == "ZL" ? 1 : lib == "XZ" ? 2 : lib == "CS" ? 3 : lib == "L4" ? 4 : 0; } if (complib > 0) { # ifdef HAVE_ZIP // see root/core/zip/src/RZip.cxx -> R__unzip const int method = is.get(); size_t chcdata = is.get(); chcdata |= (is.get() << 8); chcdata |= (is.get() << 16); size_t chdata = is.get(); chdata |= (is.get() << 8); chdata |= (is.get() << 16); if (chcdata == lcdata - lkey - 9 && chdata == ldata) { if (complib == 1 && method == Z_DEFLATED) { buffer = KeyBuffer{type, name, title, cycle, lkey, KeyBuffer::zlib, pos + lkey + 9, chcdata, chdata, 0}; } else if (complib == 4 && method == LZ4_versionNumber() / 10000) { buffer = KeyBuffer{type, name, title, cycle, lkey, KeyBuffer::lz4, pos + lkey + 9 + 8, chcdata - 8, chdata, 0}; } } # endif } else { buffer = KeyBuffer{type, name, title, cycle, lkey, KeyBuffer::none, pos + lkey, ldata, ldata, 0}; } switch (buffer.type) { case Basket: basketkeys.emplace(pos, buffer); break; case Tree: case NTuple: { auto it = treekeys.find(name); if (it != treekeys.end()) { // TTrees may be written several times, only consider last cycle if (buffer.cycle > it->second.cycle) { it->second = buffer; } } else treekeys.emplace(name, buffer); break; } case Streamer: readStreamerInfo(buffer); break; case Double: case Float: case Int: case Short: case Byte: histkeys.emplace(name + ';' + std::to_string(cycle), buffer); break; case Invalid: case Long: case Bool: case CString: break; } } pos += lcdata; } // Create default object structures if no StreamerInfo was found. // Obtained by running the following in ROOT with a file passed as an argument: // // _file0->GetStreamerInfoList()->Print() // // auto l = (TStreamerInfo*)_file0->GetStreamerInfoList()->At(ENTRYNUMBER); // l->Print(); // for (int i = 0; i < l->GetNelement(); ++i) { // auto e = l->GetElement(i); // e->Print(); // cout << e->GetFullName() << " " << e->GetTypeName() << " " << e->GetSize() << endl; // } static const StreamerInfo dummyobject{"Object", 0, std::string(), false, false}; if (!treekeys.empty()) { if (!streamerInfo.count("TTree")) { streamerInfo["TTree"] = {dummyobject, dummyobject, dummyobject, dummyobject, StreamerInfo{"fEntries", 8, std::string(), false, false}, StreamerInfo{std::string(), 5 * 8 + 4 * 4, std::string(), false, false}, StreamerInfo{"fNClusterRange", 4, std::string(), true, false}, StreamerInfo{std::string(), 6 * 8, std::string(), false, false}, StreamerInfo{"fNClusterRangeEnd", 8, "fNClusterRange", false, true}, StreamerInfo{"fNClusterSize", 8, "fNClusterRange", false, true}, StreamerInfo{"fBranches", 0, std::string(), false, false} }; } if (!streamerInfo.count("TBranch")) { streamerInfo["TBranch"] = {StreamerInfo{"TNamed", 0, std::string(), false, false}, dummyobject, StreamerInfo{std::string(), 3 * 4, std::string(), false, false}, StreamerInfo{"fWriteBasket", 4, std::string(), false, false}, StreamerInfo{std::string(), 8 + 4, std::string(), false, false}, StreamerInfo{"fMaxBaskets", 4, std::string(), true, false}, StreamerInfo{std::string(), 4 + 4 * 8, std::string(), false, false}, StreamerInfo{"fBranches", 0, std::string(), false, false}, StreamerInfo{"fLeaves", 0, std::string(), false, false}, StreamerInfo{"fBaskets", 0, std::string(), false, false}, StreamerInfo{"fBasketBytes", 4, "fMaxBaskets", false, true}, StreamerInfo{"fBasketEntry", 8, "fMaxBaskets", false, true}, StreamerInfo{"fBasketSeek", 8, "fMaxBaskets", false, true} }; } } if (!histkeys.empty()) { if (!streamerInfo.count("TH1")) { streamerInfo["TH1"] = {dummyobject, dummyobject, dummyobject, dummyobject, StreamerInfo{"fNcells", 4, std::string(), false, false}, StreamerInfo{"fXaxis", 0, std::string(), false, false}, StreamerInfo{"fYaxis", 0, std::string(), false, false}, StreamerInfo{"fZaxis", 0, std::string(), false, false}, StreamerInfo{std::string(), 2 * 2 + 8 * 8, std::string(), false, false}, dummyobject, StreamerInfo{"fSumw2", 0, std::string(), false, false}}; } if (!streamerInfo.count("TAxis")) { streamerInfo["TAxis"] = {dummyobject, dummyobject, StreamerInfo{"fNbins", 4, std::string(), false, false}, StreamerInfo{"fXmin", 8, std::string(), false, false}, StreamerInfo{"fXmax", 8, std::string(), false, false}, StreamerInfo{"fXbins", 0, std::string(), false, false}}; } } for (auto& tree : treekeys) readNEntries(tree.second); for (auto& hist : histkeys) readNBins(hist.second); } void ROOTData::readNBins(ROOTData::KeyBuffer& kbuffer) { std::string buffer = data(kbuffer); if (!buffer.empty()) { char* buf = &buffer[0]; std::map counts; Version(buf); // TH1(D/F/I/S/C) Version(buf); // TH1 advanceTo(buf, streamerInfo.find("TH1")->second, std::string(), "fNcells", counts); kbuffer.nrows = read(buf); // fNcells } } std::vector ROOTData::listHistograms() const { std::vector l; for (auto& n : histkeys) { l.emplace_back(n.first); } return l; } std::vector ROOTData::readHistogram(const std::string& name, int cycle) { auto it = histkeys.find(name + ';' + std::to_string(cycle)); if (it == histkeys.end()) return std::vector(); std::string buffer = data(it->second); if (!buffer.empty()) { char* buf = &buffer[0]; std::map counts; auto& streamerTH1 = streamerInfo.find("TH1")->second; auto& streamerTAxis = streamerInfo.find("TAxis")->second; size_t count; Version(buf); // TH1(D/F/I/S/C) Version(buf, count); // TH1 char* const dbuf = buf + count; advanceTo(buf, streamerTH1, std::string(), "fNcells", counts); std::vector r(read(buf)); // fNcells if (r.size() < 3) return std::vector(); r.front().lowedge = -std::numeric_limits::infinity(); advanceTo(buf, streamerTH1, "fNcells", "fXaxis", counts); // x-Axis Version(buf, count); // TAxis char* const nbuf = buf + count; advanceTo(buf, streamerTAxis, std::string(), "fNbins", counts); const int nbins = read(buf); advanceTo(buf, streamerTAxis, "fNbins", "fXmin", counts); const double xmin = read(buf); advanceTo(buf, streamerTAxis, "fXmin", "fXmax", counts); const double xmax = read(buf); advanceTo(buf, streamerTAxis, "fXmax", "fXbins", counts); const size_t nborders = read(buf); // TArrayD // root/core/cont/src/TArrayD.cxx -> Streamer if (nborders == r.size() - 1) { for (size_t i = 0; i < nborders; ++i) { r[i + 1].lowedge = read(buf); } } else { buf += sizeof(double) * nbins; const double scale = (xmax - xmin) / static_cast(nbins); for (size_t i = 0; i < r.size() - 1; ++i) { r[i + 1].lowedge = static_cast(i) * scale + xmin; } } buf = nbuf; // go beyond x-Axis advanceTo(buf, streamerTH1, "fXaxis", "fSumw2", counts); if (static_cast(read(buf)) == r.size()) { // TArrayD for (auto& b : r) b.sumw2 = read(buf); // always double } buf = dbuf; // skip to contents of TH1(D/F/I/S/C) if (static_cast(read(buf)) == r.size()) { auto readf = readType(it->second.type); for (auto& b : r) b.content = readf(buf); } return r; } else return std::vector(); } void ROOTData::readNEntries(ROOTData::KeyBuffer& kbuffer) { std::string buffer = data(kbuffer); if (!buffer.empty()) { char* buf = &buffer[0]; std::map counts; if (kbuffer.type == NTuple) Version(buf); // TNtuple(D) Version(buf); // TTree advanceTo(buf, streamerInfo.find("TTree")->second, std::string(), "fEntries", counts); kbuffer.nrows = read(buf); // fEntries } } std::vector ROOTData::listTrees() const { std::vector l; for (auto& n : treekeys) { l.emplace_back(n.first); } return l; } std::vector ROOTData::listLeaves(const std::string& treename) const { std::vector leaves; auto it = treekeys.find(treename); if (it == treekeys.end()) return leaves; std::ifstream is(filename, std::ifstream::binary); std::string datastring = data(it->second, is); if (datastring.empty()) return leaves; char* buf = &datastring[0]; char* const buf0 = buf - it->second.keylength; std::map counts; auto& streamerTBranch = streamerInfo.find("TBranch")->second; if (it->second.type == NTuple) Version(buf); // TNtuple(D) Version(buf); // TTree advanceTo(buf, streamerInfo.find("TTree")->second, std::string(), "fBranches", counts); // read the list of branches Version(buf); // TObjArray SkipObject(buf); String(buf); const size_t nbranches = read(buf); const size_t lowb = read(buf); std::map tags; for (size_t i = 0; i < nbranches; ++i) { std::string clname = readObject(buf, buf0, tags); size_t count; Version(buf, count); // TBranch or TBranchElement char* const nbuf = buf + count; if (i >= lowb) { if (clname == "TBranchElement") { Version(buf); // TBranch } advanceTo(buf, streamerTBranch, std::string(), "TNamed", counts); Version(buf); // TNamed SkipObject(buf); const std::string branch = String(buf); String(buf); // TODO add reading of nested branches (fBranches) advanceTo(buf, streamerTBranch, "TNamed", "fLeaves", counts); // fLeaves Version(buf); // TObjArray SkipObject(buf); String(buf); const size_t nleaves = read(buf); const size_t lowb = read(buf); for (size_t i = 0; i < nleaves; ++i) { std::string clname = readObject(buf, buf0, tags); Version(buf, count); // TLeaf(D/F/B/S/I/L/C/O) char* nbuf = buf + count; if (i >= lowb && clname.size() == 6 && clname.compare(0, 5, "TLeaf") == 0) { Version(buf); // TLeaf Version(buf); // TNamed SkipObject(buf); const std::string leafname = String(buf); String(buf); // title size_t elements = read(buf); int bytes = read(buf); if ((leafType(clname.back()) & 0xF) != bytes) qDebug() << "ROOTData: type " << clname.back() << " does not match its size!"; buf += 5; leaves.emplace_back(LeafInfo{branch, leafname, leafType(clname.back()), !read(buf), elements}); } buf = nbuf; } } buf = nbuf; } return leaves; } template std::vector ROOTData::listEntries(const std::string& treename, const std::string& branchname, const std::string& leafname, const size_t element, const size_t nentries) const { std::vector entries; auto it = treekeys.find(treename); if (it == treekeys.end()) return entries; std::ifstream is(filename, std::ifstream::binary); std::string datastring = data(it->second, is); if (datastring.empty()) return entries; char* buf = &datastring[0]; char* const buf0 = buf - it->second.keylength; std::map counts; auto& streamerTTree = streamerInfo.find("TTree")->second; auto& streamerTBranch = streamerInfo.find("TBranch")->second; if (it->second.type == NTuple) Version(buf); // TNtuple(D) Version(buf); // TTree advanceTo(buf, streamerTTree, std::string(), "fEntries", counts); entries.reserve(std::min(static_cast(read(buf)), nentries)); // reserve space (maximum for number of entries) advanceTo(buf, streamerTTree, "fEntries", "fBranches", counts); // read the list of branches Version(buf); // TObjArray SkipObject(buf); String(buf); const size_t nbranches = read(buf); const size_t lowb = read(buf); std::map tags; for (size_t i = 0; i < nbranches; ++i) { std::string clname = readObject(buf, buf0, tags); size_t count; Version(buf, count); // TBranch or TBranchElement char* const nbuf = buf + count; if (i >= lowb) { if (clname == "TBranchElement") { Version(buf); } Version(buf); // TNamed SkipObject(buf); const std::string currentbranch = String(buf); String(buf); advanceTo(buf, streamerTBranch, "TNamed", "fWriteBasket", counts); int fWriteBasket = read(buf); // TODO add reading of nested branches (fBranches) advanceTo(buf, streamerTBranch, "fWriteBasket", "fLeaves", counts); // fLeaves Version(buf); // TObjArray SkipObject(buf); String(buf); const size_t nleaves = read(buf); const size_t lowb = read(buf); int leafoffset = 0, leafcount = 0, leafcontent = 0, leafsize = 0; bool leafsign = false; ContentType leaftype = Invalid; for (size_t i = 0; i < nleaves; ++i) { std::string clname = readObject(buf, buf0, tags); Version(buf, count); // TLeaf(D/F/L/I/S/B/O/C/Element) char* nbuf = buf + count; if (currentbranch == branchname) { if (i >= lowb && clname.size() >= 5 && clname.compare(0, 5, "TLeaf") == 0) { Version(buf); // TLeaf Version(buf); // TNamed SkipObject(buf); const bool istheleaf = (clname.size() == 6 && leafname == String(buf)); String(buf); const int len = read(buf); const int size = read(buf); if (istheleaf) { leafoffset = leafcount; leafsize = size; leaftype = leafType(clname.back()); } leafcount += len * size; if (istheleaf) { leafcontent = leafcount - leafoffset; buf += 1; leafsign = !read(buf); } } } buf = nbuf; } if (leafcontent == 0) { buf = nbuf; continue; } if (static_cast(element) * leafsize >= leafcontent) { qDebug() << "ROOTData: " << leafname.c_str() << " only contains " << leafcontent / leafsize << " elements."; break; } advanceTo(buf, streamerTBranch, "fLeaves", "fBaskets", counts); // fBaskets (probably empty) Version(buf, count); // TObjArray char* const basketsbuf = buf += count + 1; // TODO there is one byte to be skipped in fBaskets, why is that? advanceTo(buf, streamerTBranch, "fBaskets", "fBasketEntry", counts); for (int i = 0; i <= fWriteBasket; ++i) { if (static_cast(read(buf)) > nentries) { fWriteBasket = i; break; } } // rewind to the end of fBaskets and look for the fBasketSeek array advanceTo(buf = basketsbuf, streamerTBranch, "fBaskets", "fBasketSeek", counts); auto readf = readType(leaftype, leafsign); for (int i = 0; i < fWriteBasket; ++i) { size_t pos = read(buf); auto it = basketkeys.find(pos); if (it != basketkeys.end()) { std::string basketbuffer = data(it->second); if (!basketbuffer.empty()) { char* bbuf = &basketbuffer[0]; char* const bufend = bbuf + basketbuffer.size(); while (bbuf + leafcount <= bufend && entries.size() < nentries) { bbuf += leafoffset + leafsize * element; entries.emplace_back(readf(bbuf)); bbuf += leafcount - leafsize * (element + 1) - leafoffset; } } } else { qDebug() << "ROOTData: fBasketSeek(" << i << "): " << pos << " (not available)"; } } } buf = nbuf; } return entries; } ROOTData::ContentType ROOTData::histType(const char type) { switch (type) { case 'D': return Double; case 'F': return Float; case 'I': return Int; case 'S': return Short; case 'C': return Byte; default: return Invalid; } } ROOTData::ContentType ROOTData::leafType(const char type) { switch (type) { case 'D': return Double; case 'F': return Float; case 'L': return Long; case 'I': return Int; case 'S': return Short; case 'B': return Byte; case 'O': return Bool; case 'C': return CString; default: return Invalid; } } template T (*ROOTData::readType(ROOTData::ContentType type, bool sign) const)(char*&) { switch (type) { case Double: return readcast; case Float: return readcast; case Long: return sign ? readcast : readcast; case Int: return sign ? readcast : readcast; case Short: return sign ? readcast : readcast; case Byte: return sign ? readcast : readcast; case Bool: return readcast; case CString: case Tree: case NTuple: case Basket: case Streamer: case Invalid: break; } return readcast; } std::string ROOTData::data(const ROOTData::KeyBuffer& buffer) const { std::ifstream is(filename, std::ifstream::binary); return data(buffer, is); } std::string ROOTData::data(const ROOTData::KeyBuffer& buffer, std::ifstream& is) const { std::string data(buffer.count, 0); is.seekg(buffer.start); if (buffer.compression == KeyBuffer::none) { is.read(&data[0], buffer.count); return data; #ifdef HAVE_ZIP } else if (buffer.compression == KeyBuffer::zlib) { std::string cdata(buffer.compressed_count, 0); is.read(&cdata[0], buffer.compressed_count); uLongf luncomp = buffer.count; if (uncompress((Bytef *)data.data(), &luncomp, (Bytef *)cdata.data(), cdata.size()) == Z_OK && data.size() == luncomp) return data; } else { std::string cdata(buffer.compressed_count, 0); is.read(&cdata[0], buffer.compressed_count); if (LZ4_decompress_safe(cdata.data(), const_cast(data.data()), buffer.compressed_count, buffer.count) == static_cast(buffer.count)) return data; #endif } return std::string(); } void ROOTData::readStreamerInfo(const ROOTData::KeyBuffer& buffer) { std::ifstream is(filename, std::ifstream::binary); std::string datastring = data(buffer, is); if (!datastring.empty()) { char* buf = &datastring[0]; char* const buf0 = buf - buffer.keylength; Version(buf); SkipObject(buf); // TCollection String(buf); const int nobj = read(buf); std::map tags; for (int i = 0; i < nobj; ++i) { std::string clname = readObject(buf, buf0, tags); size_t count; Version(buf, count); char* const nbuf = buf + count; if (clname == "TStreamerInfo") { Version(buf); SkipObject(buf); std::vector& sinfo = streamerInfo[String(buf)]; String(buf); buf += 8; // skip check sum and version clname = readObject(buf, buf0, tags); Version(buf, count); if (clname != "TObjArray") { buf += count; continue; } SkipObject(buf); // TObjArray String(buf); const int nobj = read(buf); const int lowb = read(buf); for (int i = 0; i < nobj; ++i) { std::string clname = readObject(buf, buf0, tags); Version(buf, count); char* const nbuf = buf + count; const bool isbasicpointer = clname == "TStreamerBasicPointer"; const bool ispointer = isbasicpointer || clname == "TStreamerObjectPointer"; if (i >= lowb) { if (ispointer || clname == "TStreamerBase" || clname == "TStreamerBasicType" || clname == "TStreamerObject" || clname == "TStreamerObjectAny" || clname == "TStreamerString" || clname == "TStreamerSTL") { Version(buf); // TStreamerXXX Version(buf); // TStreamerElement SkipObject(buf); const std::string name = String(buf); const std::string title = String(buf); int type = read(buf); size_t size = read(buf); if (clname.compare(0, 15, "TStreamerObject") == 0) size = 0; std::string counter; bool iscounter = false; if (ispointer) { if (!title.empty() && title.front() == '[') { const size_t endref = title.find(']', 1); if (endref != title.npos) { counter = title.substr(1, endref - 1); } } if (isbasicpointer) { // see root/io/io/inc/TStreamerInfo.h -> TStreamerInfo::EReadWrite switch (type - 40) { case 1: // char case 11: // unsigned char size = 1; break; case 2: // short case 12: // unsigned short case 19: // float16 size = 2; break; case 3: // int case 5: // float case 9: // double32 case 13: // unsigned int size = 4; break; case 4: // long case 8: // double case 14: // unsigned long case 16: // long case 17: // unsigned long size = 8; break; } } } else if (clname == "TStreamerBasicType") { iscounter = type == 6; // see root/io/io/inc/TStreamerInfo.h -> TStreamerInfo::EReadWrite } sinfo.emplace_back(StreamerInfo{name, size, counter, iscounter, ispointer}); } } buf = nbuf; } } else buf = nbuf; buf += 1; // trailing zero of TObjArray* } } else qDebug() << "ROOTData: Inflation failed!"; } bool ROOTData::advanceTo(char*& buf, const std::vector& objects, const std::string& current, const std::string& target, std::map& counts) { // The object structure can be retrieved from TFile::GetStreamerInfoList(). // Every ROOT object contains a version number which may include the byte count // for the object. The latter is currently assumed to be present to skip unused // objects. No checks are performed. The corresponding ROOT code is quite nested // but the actual readout is straight forward. auto it = objects.begin(); if (!current.empty()) { for (; it != objects.end(); ++it) { if (it->name == target) { return false; // target lies before current buffer position } else if (it->name == current) { ++it; break; } } } for (; it != objects.end(); ++it) { if (it->name == target) return true; if (it->size == 0) Skip(buf, 1); else if (it->iscounter) counts[it->name] = read(buf); else if (it->ispointer) { if (it->counter.empty()) buf += it->size + 1; else buf += it->size * counts[it->counter] + 1; } else buf += it->size; } return false; } // needs to be after ROOTDataHelpers namespace declaration QString ROOTFilter::fileInfoString(const QString& fileName) { DEBUG("ROOTFilter::fileInfoString()"); QString info; // The file structure is described in root/io/io/src/TFile.cxx std::ifstream is(fileName.toStdString(), std::ifstream::binary); std::string root(4, 0); is.read(const_cast(root.data()), 4); if (root != "root") { DEBUG(" Not a ROOT file. root = " << root); return i18n("Not a ROOT file"); } int version = read(is); info += i18n("File format version: %1", QString::number(version)); info += QLatin1String("
"); is.seekg(20); int freeBytes = read(is); int freeRecords = read(is); int namedBytes = read(is); char pointerBytes = read(is); info += i18n("FREE data record size: %1 bytes", QString::number(freeBytes)); info += QLatin1String("
"); info += i18n("Number of free data records: %1", QString::number(freeRecords)); info += QLatin1String("
"); info += i18n("TNamed size: %1 bytes", QString::number(namedBytes)); info += QLatin1String("
"); info += i18n("Size of file pointers: %1 bytes", QString::number(pointerBytes)); info += QLatin1String("
"); int compression = read(is); compression = compression > 0 ? compression : 0; info += i18n("Compression level and algorithm: %1", QString::number(compression)); info += QLatin1String("
"); is.seekg(41); int infoBytes = read(is); info += i18n("Size of TStreamerInfo record: %1 bytes", QString::number(infoBytes)); info += QLatin1String("
"); Q_UNUSED(fileName); return info; } diff --git a/src/backend/datasources/filters/ROOTFilterPrivate.h b/src/backend/datasources/filters/ROOTFilterPrivate.h index 828018ebc..7aaaad5f2 100644 --- a/src/backend/datasources/filters/ROOTFilterPrivate.h +++ b/src/backend/datasources/filters/ROOTFilterPrivate.h @@ -1,306 +1,311 @@ /*************************************************************************** File : ROOTFilterPrivate.h Project : LabPlot Description : Private implementation class for ROOTFilter. -------------------------------------------------------------------- Copyright : (C) 2018 Christoph Roick (chrisito@gmx.de) ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, * * Boston, MA 02110-1301 USA * * * ***************************************************************************/ #ifndef ROOTFILTERPRIVATE_H #define ROOTFILTERPRIVATE_H +#include #include #include #include #include #include class ROOTFilter; class QString; class QStringList; class AbstractDataSource; class AbstractColumn; /** * @brief Read TH1 histograms and TTrees from ROOT files without depending on ROOT libraries */ class ROOTData { public: /** * @brief Open ROOT file and save file positions of histograms and trees * * Also checks for the compression level. Currently the default ZLIB and LZ4 compression * types are supported. The TStreamerInfo is read if it is available, otherwise the * data structure as of ROOT v6.15 is used. No tests were performed with data written * prior to ROOT v5.34. * * @param[in] filename ROOT file to be read */ explicit ROOTData (const std::string& filename); /// Parameters to describe a bin struct BinPars { double content; double sumw2; double lowedge; }; /** * @brief Identifiers for different data types * * Histograms are identified by their bin type. The lowest byte indicates the size * of the numeric types for cross checks during the import. */ enum ContentType {Invalid = 0, Tree = 0x10, NTuple = 0x11, Basket = 0x20, Streamer = 0x30, Double = 0x48, Float = 0x54, Long = 0x68, Int = 0x74, Short = 0x82, Byte = 0x91, Bool = 0xA1, CString = 0xB0}; /// Information about leaf contents struct LeafInfo { std::string branch; std::string leaf; ContentType type; bool issigned; size_t elements; }; /** * @brief List available histograms in the ROOT file */ std::vector listHistograms() const; /** * @brief List available trees in the ROOT file */ std::vector listTrees() const; /** * @brief List information about data contained in leaves * * @param[in] treename Name of the tree */ std::vector listLeaves(const std::string& treename) const; /** * @brief Get entries of a leaf * * @param[in] treename Name of the tree * @param[in] branchname Name of the branch * @param[in] leafname Name of the leaf * @param[in] element Index, if leaf is an array * @param[in] nentries Maximum number of entries to be read */ template std::vector listEntries(const std::string& treename, const std::string& branchname, const std::string& leafname, const size_t element = 0, const size_t nentries = std::numeric_limits::max()) const; /** * @brief Get entries of a leaf with the same name as its branch * * @param[in] treename Name of the tree * @param[in] branchname Name of the branch * @param[in] nentries Maximum number of entries to be read */ template std::vector listEntries(const std::string& treename, const std::string& branchname, const size_t element = 0, const size_t nentries = std::numeric_limits::max()) const { return listEntries(treename, branchname, branchname, element, nentries); } /** * @brief Read histogram from file * * Jumps to memoized file position, decompresses the object if required and analyzes * the buffer. Overflow and underflow bins are included. * * @param[in] name Histogram name without cycle indicator * @param[in] cycle Indicator for object cycle */ std::vector readHistogram(const std::string& name, int cycle = 1); /** * @brief Get histogram title * * The title is stored in the buffer. No file access required. * * @param[in] name Histogram name without cycle indicator * @param[in] cycle Indicator for object cycle */ std::string histogramTitle(const std::string& name, int cycle = 1) { auto it = histkeys.find(name + ';' + std::to_string(cycle)); if (it != histkeys.end()) return it->second.title; else return std::string(); } /** * @brief Get number of bins in histogram * * The number of bins is stored in the buffer. No file access required. * * @param[in] name Histogram name without cycle indicator * @param[in] cycle Indicator for object cycle */ int histogramBins(const std::string& name, int cycle = 1) { auto it = histkeys.find(name + ';' + std::to_string(cycle)); if (it != histkeys.end()) return it->second.nrows; else return 0; } /** * @brief Get number of entries in tree * * The number of entries is stored in the buffer. No file access required. * * @param[in] name Tree name */ int treeEntries(const std::string& name) { auto it = treekeys.find(name); if (it != treekeys.end()) return it->second.nrows; else return 0; } private: struct KeyBuffer { ContentType type; std::string name; std::string title; int cycle; size_t keylength; enum CompressionType { none, zlib, lz4 } compression; size_t start; size_t compressed_count; size_t count; int nrows; }; struct StreamerInfo { std::string name; size_t size; std::string counter; bool iscounter; bool ispointer; }; /// Get data type from histogram identifier static ContentType histType(const char type); /// Get data type from leaf identifier static ContentType leafType(const char type); /// Get function to read a buffer of the specified type template T (*readType(ContentType type, bool sign = true) const)(char*&); /// Get the number of bins contained in a histogram void readNBins(KeyBuffer& buffer); /// Get the number of entries contained in a tree void readNEntries(KeyBuffer& buffer); /// Get buffer from file content at histogram position std::string data(const KeyBuffer& buffer) const; /// Get buffer from file content at histogram position, uses already opened stream std::string data(const KeyBuffer& buffer, std::ifstream& is) const; /// Load streamer information void readStreamerInfo(const KeyBuffer& buffer); /** * @brief Advance to an object inside a class according to streamer information * * The number of entries is stored in the buffer. No file access required. * * @param[in] buf Pointer to the current position in the class object * @param[in] objects A list of objects in the class defined by the streamer information * @param[in] current The name of the current object * @param[in] target The name of the object to be advanced to * @param[in] counts A list of the number of entries in objects of dynamic length; updated while reading */ static bool advanceTo(char*& buf, const std::vector& objects, const std::string& current, const std::string& target, std::map& counts); std::string filename; std::map histkeys, treekeys; std::map basketkeys; std::map > streamerInfo; }; class ROOTFilterPrivate { public: ROOTFilterPrivate(); /** * @brief Read data from the currently selected histogram * * The ROOT file is kept open until the file name is changed */ void readDataFromFile(const QString& fileName, AbstractDataSource* dataSource, AbstractFileFilter::ImportMode importMode); /// Currently writing to ROOT files is not supported void write(const QString& fileName, AbstractDataSource*); /// List names of histograms contained in ROOT file QStringList listHistograms(const QString& fileName); /// List names of trees contained in ROOT file QStringList listTrees(const QString& fileName); /// List names of leaves contained in ROOT tree QVector listLeaves(const QString& fileName, const QString& treeName); /// Get preview data of the currently set histogram QVector previewCurrentObject(const QString& fileName, int first, int last); /// Get the number of bins in the current histogram int rowsInCurrentObject(const QString& fileName); /// Identifier of the current histogram QString currentObject; /// First row to read (can be -1, skips the underflow bin 0) int startRow = -1; /// Last row to read (can be -1, skips the overflow bin) int endRow = -1; - /// Start column to read + /// Columns to read QVector columns; private: /// Checks and updates the current ROOT file path void setFile(const QString& fileName); /// Calls ReadHistogram from ROOTHist std::vector readHistogram(const QString& histName); /// Calls listEntries from ROOTHist std::vector readTree(const QString& treeName, const QString& branchName, const QString& leafName, int element, int last); - /// Currently set ROOT file path - QString currentFile; + /// Information about currently set ROOT file + struct { + QString name; + QDateTime modified; + qint64 size; + } currentFile; /// ROOTHist instance kept alive while currentFile does not change std::unique_ptr currentROOTData; }; #endif diff --git a/src/kdefrontend/datasources/ImportFileWidget.cpp b/src/kdefrontend/datasources/ImportFileWidget.cpp index 618227082..29c5deabd 100644 --- a/src/kdefrontend/datasources/ImportFileWidget.cpp +++ b/src/kdefrontend/datasources/ImportFileWidget.cpp @@ -1,2956 +1,2959 @@ /*************************************************************************** File : ImportFileWidget.cpp Project : LabPlot Description : import file data widget -------------------------------------------------------------------- Copyright : (C) 2009-2018 Stefan Gerlach (stefan.gerlach@uni.kn) Copyright : (C) 2009-2018 Alexander Semke (alexander.semke@web.de) Copyright : (C) 2017-2018 Fabian Kristof (fkristofszabolcs@gmail.com) Copyright : (C) 2018 Kovacs Ferencz (kferike98@gmail.com) ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, * * Boston, MA 02110-1301 USA * * * ***************************************************************************/ #include "ImportFileWidget.h" #include "FileInfoDialog.h" #include "backend/datasources/filters/filters.h" #include "AsciiOptionsWidget.h" #include "BinaryOptionsWidget.h" #include "HDF5OptionsWidget.h" #include "ImageOptionsWidget.h" #include "NetCDFOptionsWidget.h" #include "FITSOptionsWidget.h" #include "JsonOptionsWidget.h" #include "ROOTOptionsWidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_MQTT #include "kdefrontend/widgets/MQTTWillSettingsWidget.h" #include "MQTTConnectionManagerDialog.h" #include "MQTTConnectionManagerWidget.h" #include #include #include #include #include #include #include #endif /*! \class ImportFileWidget \brief Widget for importing data from a file. \ingroup kdefrontend */ ImportFileWidget::ImportFileWidget(QWidget* parent, bool liveDataSource, const QString& fileName) : QWidget(parent), m_fileName(fileName), m_liveDataSource(liveDataSource) #ifdef HAVE_MQTT , m_searchTimer(new QTimer(this)), m_connectTimeoutTimer(new QTimer(this)) #endif { ui.setupUi(this); ui.leFileName->setCompleter(new QCompleter(new QDirModel)); //add supported file types if (!liveDataSource) { ui.cbFileType->addItem(i18n("ASCII data"), AbstractFileFilter::Ascii); ui.cbFileType->addItem(i18n("Binary data"), AbstractFileFilter::Binary); ui.cbFileType->addItem(i18n("Image"), AbstractFileFilter::Image); #ifdef HAVE_HDF5 ui.cbFileType->addItem(i18n("Hierarchical Data Format 5 (HDF5)"), AbstractFileFilter::HDF5); #endif #ifdef HAVE_NETCDF ui.cbFileType->addItem(i18n("Network Common Data Format (NetCDF)"), AbstractFileFilter::NETCDF); #endif #ifdef HAVE_FITS ui.cbFileType->addItem(i18n("Flexible Image Transport System Data Format (FITS)"), AbstractFileFilter::FITS); #endif ui.cbFileType->addItem(i18n("JSON data"), AbstractFileFilter::JSON); #ifdef HAVE_ZIP ui.cbFileType->addItem(i18n("ROOT (CERN)"), AbstractFileFilter::ROOT); #endif ui.cbFileType->addItem(i18n("Ngspice RAW ASCII"), AbstractFileFilter::NgspiceRawAscii); ui.cbFileType->addItem(i18n("Ngspice RAW Binary"), AbstractFileFilter::NgspiceRawBinary); //hide widgets relevant for live data reading only ui.lSourceType->hide(); ui.cbSourceType->hide(); ui.gbUpdateOptions->hide(); } else { ui.cbFileType->addItem(i18n("ASCII data"), AbstractFileFilter::Ascii); ui.cbFileType->addItem(i18n("Binary data"), AbstractFileFilter::Binary); +#ifdef HAVE_ZIP + ui.cbFileType->addItem(i18n("ROOT (CERN)"), AbstractFileFilter::ROOT); +#endif ui.cbFileType->addItem(i18n("Ngspice RAW ASCII"), AbstractFileFilter::NgspiceRawAscii); ui.cbFileType->addItem(i18n("Ngspice RAW Binary"), AbstractFileFilter::NgspiceRawBinary); ui.lePort->setValidator( new QIntValidator(ui.lePort) ); ui.cbBaudRate->addItems(LiveDataSource::supportedBaudRates()); ui.cbSerialPort->addItems(LiveDataSource::availablePorts()); ui.tabWidget->removeTab(2); #ifdef HAVE_MQTT m_searchTimer->setInterval(10000); m_connectTimeoutTimer->setInterval(6000); #endif } QStringList filterItems {i18n("Automatic"), i18n("Custom")}; ui.cbFilter->addItems(filterItems); //hide options that will be activated on demand ui.gbOptions->hide(); ui.gbUpdateOptions->hide(); setMQTTVisible(false); ui.cbReadingType->addItem(i18n("Whole file"), LiveDataSource::WholeFile); ui.bOpen->setIcon( QIcon::fromTheme(QLatin1String("document-open")) ); ui.bFileInfo->setIcon( QIcon::fromTheme(QLatin1String("help-about")) ); ui.bManageFilters->setIcon( QIcon::fromTheme(QLatin1String("configure")) ); ui.bSaveFilter->setIcon( QIcon::fromTheme(QLatin1String("document-save")) ); ui.bRefreshPreview->setIcon( QIcon::fromTheme(QLatin1String("view-refresh")) ); ui.tvJson->header()->setSectionResizeMode(QHeaderView::ResizeToContents); ui.tvJson->setAlternatingRowColors(true); showJsonModel(false); // the table widget for preview m_twPreview = new QTableWidget(ui.tePreview); m_twPreview->verticalHeader()->hide(); m_twPreview->setEditTriggers(QTableWidget::NoEditTriggers); auto* layout = new QHBoxLayout; layout->addWidget(m_twPreview); ui.tePreview->setLayout(layout); m_twPreview->hide(); #ifdef HAVE_MQTT ui.cbSourceType->addItem(QLatin1String("MQTT")); m_configPath = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation).constFirst() + QLatin1String("MQTT_connections"); m_willSettings.enabled = false; m_willSettings.willRetain = false; m_willSettings.willQoS = 0; m_willSettings.willMessageType = MQTTClient::WillMessageType::OwnMessage; m_willSettings.willUpdateType = MQTTClient::WillUpdateType::TimePeriod; m_willSettings.willTimeInterval = 10000; m_willSettings.willStatistics.fill(false, 15); const int size = ui.leTopics->height(); ui.lTopicSearch->setPixmap( QIcon::fromTheme(QLatin1String("view-filter")).pixmap(size, size) ); ui.lSubscriptionSearch->setPixmap( QIcon::fromTheme(QLatin1String("view-filter")).pixmap(size, size) ); ui.bSubscribe->setIcon(ui.bSubscribe->style()->standardIcon(QStyle::SP_ArrowRight)); ui.bSubscribe->setToolTip(i18n("Subscribe selected topics")); ui.bUnsubscribe->setIcon(ui.bUnsubscribe->style()->standardIcon(QStyle::SP_ArrowLeft)); ui.bUnsubscribe->setToolTip(i18n("Unsubscribe selected topics")); ui.bManageConnections->setIcon(QIcon::fromTheme(QLatin1String("network-server"))); ui.bManageConnections->setToolTip(i18n("Manage MQTT connections")); ui.lLWT->setToolTip(i18n("Specify the 'Last Will and Testament' message (LWT). At least one topic has to be subscribed.")); ui.bLWT->setEnabled(false); ui.bLWT->setToolTip(i18n("Specify the 'Last Will and Testament' message (LWT). At least one topic has to be subscribed.")); ui.bLWT->setIcon(ui.bLWT->style()->standardIcon(QStyle::SP_FileDialogDetailedView)); QString info = i18n("Set the Quality of Service (QoS) for the subscription to define the guarantee of the message delivery:" "
    " "
  • 0 - deliver at most once
  • " "
  • 1 - deliver at least once
  • " "
  • 2 - deliver exactly once
  • " "
"); ui.cbQos->setToolTip(info); #endif //TODO: implement save/load of user-defined settings later and activate these buttons again ui.bSaveFilter->hide(); ui.bManageFilters->hide(); } void ImportFileWidget::loadSettings() { m_suppressRefresh = true; //load last used settings QString confName; if (m_liveDataSource) confName = QLatin1String("LiveDataImport"); else confName = QLatin1String("FileImport"); KConfigGroup conf(KSharedConfig::openConfig(), confName); //read the source type first since settings in fileNameChanged() depend on this ui.cbSourceType->setCurrentIndex(conf.readEntry("SourceType").toInt()); //general settings AbstractFileFilter::FileType fileType = static_cast(conf.readEntry("Type", 0)); for (int i = 0; i < ui.cbFileType->count(); ++i) { if (static_cast(ui.cbFileType->itemData(i).toInt()) == fileType) { if (ui.cbFileType->currentIndex() == i) initOptionsWidget(); else ui.cbFileType->setCurrentIndex(i); break; } } ui.cbFilter->setCurrentIndex(conf.readEntry("Filter", 0)); filterChanged(ui.cbFilter->currentIndex()); // needed if filter is not changed if (m_fileName.isEmpty()) ui.leFileName->setText(conf.readEntry("LastImportedFile", "")); else ui.leFileName->setText(m_fileName); //live data related settings ui.cbBaudRate->setCurrentIndex(conf.readEntry("BaudRate").toInt()); ui.cbReadingType->setCurrentIndex(conf.readEntry("ReadingType").toInt()); ui.cbSerialPort->setCurrentIndex(conf.readEntry("SerialPort").toInt()); ui.cbUpdateType->setCurrentIndex(conf.readEntry("UpdateType").toInt()); ui.leHost->setText(conf.readEntry("Host","")); ui.sbKeepNValues->setValue(conf.readEntry("KeepNValues").toInt()); ui.lePort->setText(conf.readEntry("Port","")); ui.sbSampleSize->setValue(conf.readEntry("SampleSize").toInt()); ui.sbUpdateInterval->setValue(conf.readEntry("UpdateInterval").toInt()); #ifdef HAVE_MQTT //MQTT related settings m_initialisingMQTT = true; //read available connections readMQTTConnections(); ui.cbConnection->setCurrentIndex(ui.cbConnection->findText(conf.readEntry("Connection", ""))); m_willSettings.willRetain = conf.readEntry("mqttWillRetain").toInt(); m_willSettings.willUpdateType = static_cast(conf.readEntry("mqttWillUpdateType").toInt()); m_willSettings.willQoS = conf.readEntry("mqttWillQoS").toInt(); m_willSettings.willOwnMessage = conf.readEntry("mqttWillOwnMessage",""); m_willSettings.willTimeInterval = conf.readEntry("mqttWillUpdateInterval","").toInt(); const QString& willStatistics = conf.readEntry("mqttWillStatistics",""); const QStringList& statisticsList = willStatistics.split('|', QString::SplitBehavior::SkipEmptyParts); for (auto value : statisticsList) m_willSettings.willStatistics[value.toInt()] = true; m_willSettings.willMessageType = static_cast(conf.readEntry("mqttWillMessageType").toInt()); m_willSettings.enabled = conf.readEntry("mqttWillUse").toInt(); m_initialisingMQTT = false; #endif //initialize the slots after all settings were set in order to avoid unneeded refreshes initSlots(); //update the status of the widgets LiveDataSource::SourceType sourceType = currentSourceType(); sourceTypeChanged(sourceType); if (sourceType != LiveDataSource::FileOrPipe) fileTypeChanged(fileType); // only for FileOrPipe this was indirectly called by sourceTypeChanged readingTypeChanged(ui.cbReadingType->currentIndex()); //all set now, refresh the preview m_suppressRefresh = false; QTimer::singleShot(100, this, [=] () { refreshPreview(); }); } ImportFileWidget::~ImportFileWidget() { // save current settings QString confName; if (m_liveDataSource) confName = QLatin1String("LiveDataImport"); else confName = QLatin1String("FileImport"); KConfigGroup conf(KSharedConfig::openConfig(), confName); // general settings conf.writeEntry("Type", (int)currentFileType()); conf.writeEntry("Filter", ui.cbFilter->currentIndex()); conf.writeEntry("LastImportedFile", ui.leFileName->text()); //live data related settings conf.writeEntry("SourceType", (int)currentSourceType()); conf.writeEntry("UpdateType", ui.cbUpdateType->currentIndex()); conf.writeEntry("ReadingType", ui.cbReadingType->currentIndex()); conf.writeEntry("SampleSize", ui.sbSampleSize->value()); conf.writeEntry("KeepNValues", ui.sbKeepNValues->value()); conf.writeEntry("BaudRate", ui.cbBaudRate->currentIndex()); conf.writeEntry("SerialPort", ui.cbSerialPort->currentIndex()); conf.writeEntry("Host", ui.leHost->text()); conf.writeEntry("Port", ui.lePort->text()); conf.writeEntry("UpdateInterval", ui.sbUpdateInterval->value()); #ifdef HAVE_MQTT //MQTT related settings conf.writeEntry("Connection", ui.cbConnection->currentText()); conf.writeEntry("mqttWillMessageType", static_cast(m_willSettings.willMessageType)); conf.writeEntry("mqttWillUpdateType", static_cast(m_willSettings.willUpdateType)); conf.writeEntry("mqttWillQoS", QString::number(m_willSettings.willQoS)); conf.writeEntry("mqttWillOwnMessage", m_willSettings.willOwnMessage); conf.writeEntry("mqttWillUpdateInterval", QString::number(m_willSettings.willTimeInterval)); QString willStatistics; for (int i = 0; i < m_willSettings.willStatistics.size(); ++i) { if (m_willSettings.willStatistics[i]) willStatistics += QString::number(i)+ QLatin1Char('|'); } conf.writeEntry("mqttWillStatistics", willStatistics); conf.writeEntry("mqttWillRetain", static_cast(m_willSettings.willRetain)); conf.writeEntry("mqttWillUse", static_cast(m_willSettings.enabled)); #endif // data type specific settings if (m_asciiOptionsWidget) m_asciiOptionsWidget->saveSettings(); if (m_binaryOptionsWidget) m_binaryOptionsWidget->saveSettings(); if (m_imageOptionsWidget) m_imageOptionsWidget->saveSettings(); if (m_jsonOptionsWidget) m_jsonOptionsWidget->saveSettings(); } void ImportFileWidget::initSlots() { //SLOTs for the general part of the data source configuration connect(ui.cbSourceType, static_cast(&QComboBox::currentIndexChanged), this, static_cast(&ImportFileWidget::sourceTypeChanged)); connect(ui.leFileName, &QLineEdit::textChanged, this, static_cast(&ImportFileWidget::fileNameChanged)); connect(ui.leHost, &QLineEdit::textChanged, this, &ImportFileWidget::hostChanged); connect(ui.lePort, &QLineEdit::textChanged, this, &ImportFileWidget::portChanged); connect(ui.tvJson, &QTreeView::clicked, this, &ImportFileWidget::refreshPreview); connect(ui.bOpen, &QPushButton::clicked, this, &ImportFileWidget::selectFile); connect(ui.bFileInfo, &QPushButton::clicked, this, &ImportFileWidget::fileInfoDialog); connect(ui.bSaveFilter, &QPushButton::clicked, this, &ImportFileWidget::saveFilter); connect(ui.bManageFilters, &QPushButton::clicked, this, &ImportFileWidget::manageFilters); connect(ui.cbFileType, static_cast(&KComboBox::currentIndexChanged), this, &ImportFileWidget::fileTypeChanged); connect(ui.cbUpdateType, static_cast(&QComboBox::currentIndexChanged), this, &ImportFileWidget::updateTypeChanged); connect(ui.cbReadingType, static_cast(&QComboBox::currentIndexChanged), this, &ImportFileWidget::readingTypeChanged); connect(ui.cbFilter, static_cast(&KComboBox::activated), this, &ImportFileWidget::filterChanged); connect(ui.bRefreshPreview, &QPushButton::clicked, this, &ImportFileWidget::refreshPreview); #ifdef HAVE_MQTT connect(ui.cbConnection, static_cast(&QComboBox::currentIndexChanged), this, &ImportFileWidget::mqttConnectionChanged); connect(ui.bSubscribe, &QPushButton::clicked, this, &ImportFileWidget::mqttSubscribe); connect(ui.bUnsubscribe, &QPushButton::clicked, this,&ImportFileWidget::mqttUnsubscribe); connect(this, &ImportFileWidget::newTopic, this, &ImportFileWidget::setTopicCompleter); connect(m_searchTimer, &QTimer::timeout, this, &ImportFileWidget::topicTimeout); connect(m_connectTimeoutTimer, &QTimer::timeout, this, &ImportFileWidget::mqttConnectTimeout); connect(ui.cbFileType, static_cast(&QComboBox::currentIndexChanged), [this]() {emit checkFileType();}); connect(ui.leTopics, &QLineEdit::textChanged, this, &ImportFileWidget::scrollToTopicTreeItem); connect(ui.leSubscriptions, &QLineEdit::textChanged, this, &ImportFileWidget::scrollToSubsriptionTreeItem); connect(ui.bManageConnections, &QPushButton::clicked, this, &ImportFileWidget::showMQTTConnectionManager); connect(ui.bLWT, &QPushButton::clicked, this, &ImportFileWidget::showWillSettings); connect(ui.twTopics, &QTreeWidget::itemDoubleClicked, this, &ImportFileWidget::mqttAvailableTopicDoubleClicked); connect(ui.twSubscriptions, &QTreeWidget::itemDoubleClicked, this, &ImportFileWidget::mqttSubscribedTopicDoubleClicked); #endif } void ImportFileWidget::showAsciiHeaderOptions(bool b) { if (m_asciiOptionsWidget) m_asciiOptionsWidget->showAsciiHeaderOptions(b); } void ImportFileWidget::showJsonModel(bool b) { ui.tvJson->setVisible(b); ui.lField->setVisible(b); } void ImportFileWidget::showOptions(bool b) { ui.gbOptions->setVisible(b); if (m_liveDataSource) ui.gbUpdateOptions->setVisible(b); resize(layout()->minimumSize()); } QString ImportFileWidget::fileName() const { return ui.leFileName->text(); } QString ImportFileWidget::selectedObject() const { const QString& path = ui.leFileName->text(); //determine the file name only QString name = path.right(path.length() - path.lastIndexOf(QDir::separator()) - 1); //strip away the extension if available if (name.indexOf('.') != -1) name = name.left(name.lastIndexOf('.')); //for multi-dimensional formats like HDF, netCDF and FITS add the currently selected object const auto format = currentFileType(); if (format == AbstractFileFilter::HDF5) { const QStringList& hdf5Names = m_hdf5OptionsWidget->selectedNames(); if (hdf5Names.size()) name += hdf5Names.first(); //the names of the selected HDF5 objects already have '/' } else if (format == AbstractFileFilter::NETCDF) { const QStringList& names = m_netcdfOptionsWidget->selectedNames(); if (names.size()) name += QLatin1Char('/') + names.first(); } else if (format == AbstractFileFilter::FITS) { const QString& extensionName = m_fitsOptionsWidget->currentExtensionName(); if (!extensionName.isEmpty()) name += QLatin1Char('/') + extensionName; } else if (format == AbstractFileFilter::ROOT) { const QStringList& names = m_rootOptionsWidget->selectedNames(); if (names.size()) name += QLatin1Char('/') + names.first(); } return name; } /*! * returns \c true if the number of lines to be imported from the currently selected file is zero ("file is empty"), * returns \c false otherwise. */ bool ImportFileWidget::isFileEmpty() const { return m_fileEmpty; } QString ImportFileWidget::host() const { return ui.leHost->text(); } QString ImportFileWidget::port() const { return ui.lePort->text(); } QString ImportFileWidget::serialPort() const { return ui.cbSerialPort->currentText(); } int ImportFileWidget::baudRate() const { return ui.cbBaudRate->currentText().toInt(); } /*! saves the settings to the data source \c source. */ void ImportFileWidget::saveSettings(LiveDataSource* source) const { AbstractFileFilter::FileType fileType = currentFileType(); auto updateType = static_cast(ui.cbUpdateType->currentIndex()); LiveDataSource::SourceType sourceType = currentSourceType(); auto readingType = static_cast(ui.cbReadingType->currentIndex()); source->setComment( ui.leFileName->text() ); source->setFileType(fileType); currentFileFilter(); source->setFilter(m_currentFilter.release()); // pass ownership of the filter to the LiveDataSource source->setSourceType(sourceType); source->setReadingType(readingType); if (updateType == LiveDataSource::UpdateType::TimeInterval) source->setUpdateInterval(ui.sbUpdateInterval->value()); else source->setFileWatched(true); source->setKeepNValues(ui.sbKeepNValues->value()); source->setUpdateType(updateType); if (readingType != LiveDataSource::ReadingType::TillEnd) source->setSampleSize(ui.sbSampleSize->value()); switch (sourceType) { case LiveDataSource::SourceType::FileOrPipe: source->setFileName(ui.leFileName->text()); source->setFileLinked(ui.chbLinkFile->isChecked()); break; case LiveDataSource::SourceType::LocalSocket: source->setFileName(ui.leFileName->text()); source->setLocalSocketName(ui.leFileName->text()); break; case LiveDataSource::SourceType::NetworkTcpSocket: case LiveDataSource::SourceType::NetworkUdpSocket: source->setHost(ui.leHost->text()); source->setPort((quint16)ui.lePort->text().toInt()); break; case LiveDataSource::SourceType::SerialPort: source->setBaudRate(ui.cbBaudRate->currentText().toInt()); source->setSerialPort(ui.cbSerialPort->currentText()); break; case LiveDataSource::SourceType::MQTT: break; default: break; } } #ifdef HAVE_MQTT /*! saves the settings to the MQTTClient \c client. */ void ImportFileWidget::saveMQTTSettings(MQTTClient* client) const { DEBUG("ImportFileWidget::saveMQTTSettings"); MQTTClient::UpdateType updateType = static_cast(ui.cbUpdateType->currentIndex()); MQTTClient::ReadingType readingType = static_cast(ui.cbReadingType->currentIndex()); client->setComment(ui.leFileName->text()); currentFileFilter(); client->setFilter(static_cast(m_currentFilter.release())); // pass ownership of the filter to MQTTClient client->setReadingType(readingType); if (updateType == MQTTClient::UpdateType::TimeInterval) client->setUpdateInterval(ui.sbUpdateInterval->value()); client->setKeepNValues(ui.sbKeepNValues->value()); client->setUpdateType(updateType); if (readingType != MQTTClient::ReadingType::TillEnd) client->setSampleSize(ui.sbSampleSize->value()); client->setMQTTClientHostPort(m_client->hostname(), m_client->port()); KConfig config(m_configPath, KConfig::SimpleConfig); KConfigGroup group = config.group(ui.cbConnection->currentText()); bool useID = group.readEntry("UseID").toUInt(); bool useAuthentication = group.readEntry("UseAuthentication").toUInt(); client->setMQTTUseAuthentication(useAuthentication); if (useAuthentication) client->setMQTTClientAuthentication(m_client->username(), m_client->password()); client->setMQTTUseID(useID); if (useID) client->setMQTTClientId(m_client->clientId()); for (int i = 0; i < m_mqttSubscriptions.count(); ++i) client->addInitialMQTTSubscriptions(m_mqttSubscriptions[i]->topic(), m_mqttSubscriptions[i]->qos()); const bool retain = group.readEntry("Retain").toUInt(); client->setMQTTRetain(retain); client->setWillSettings(m_willSettings); } #endif /*! returns the currently used file type. */ AbstractFileFilter::FileType ImportFileWidget::currentFileType() const { return static_cast(ui.cbFileType->currentData().toInt()); } LiveDataSource::SourceType ImportFileWidget::currentSourceType() const { return static_cast(ui.cbSourceType->currentIndex()); } /*! returns the currently used filter. */ AbstractFileFilter* ImportFileWidget::currentFileFilter() const { DEBUG("ImportFileWidget::currentFileFilter()"); AbstractFileFilter::FileType fileType = currentFileType(); if (m_currentFilter && m_currentFilter->type() != fileType) m_currentFilter.reset(); switch (fileType) { case AbstractFileFilter::Ascii: { DEBUG(" ASCII"); if (!m_currentFilter) m_currentFilter.reset(new AsciiFilter); auto filter = static_cast(m_currentFilter.get()); if (ui.cbFilter->currentIndex() == 0) //"automatic" filter->setAutoModeEnabled(true); else if (ui.cbFilter->currentIndex() == 1) { //"custom" filter->setAutoModeEnabled(false); if (m_asciiOptionsWidget) m_asciiOptionsWidget->applyFilterSettings(filter); } else filter->loadFilterSettings(ui.cbFilter->currentText()); //save the data portion to import filter->setStartRow( ui.sbStartRow->value()); filter->setEndRow(ui.sbEndRow->value()); filter->setStartColumn(ui.sbStartColumn->value()); filter->setEndColumn(ui.sbEndColumn->value()); break; } case AbstractFileFilter::Binary: { DEBUG(" Binary"); if (!m_currentFilter) m_currentFilter.reset(new BinaryFilter); auto filter = static_cast(m_currentFilter.get()); if ( ui.cbFilter->currentIndex() == 0 ) //"automatic" filter->setAutoModeEnabled(true); else if (ui.cbFilter->currentIndex() == 1) { //"custom" filter->setAutoModeEnabled(false); if (m_binaryOptionsWidget) m_binaryOptionsWidget->applyFilterSettings(filter); } else { //TODO: load filter settings // filter->setFilterName( ui.cbFilter->currentText() ); } filter->setStartRow(ui.sbStartRow->value()); filter->setEndRow(ui.sbEndRow->value()); break; } case AbstractFileFilter::Image: { DEBUG(" Image"); if (!m_currentFilter) m_currentFilter.reset(new ImageFilter); auto filter = static_cast(m_currentFilter.get()); filter->setImportFormat(m_imageOptionsWidget->currentFormat()); filter->setStartRow(ui.sbStartRow->value()); filter->setEndRow(ui.sbEndRow->value()); filter->setStartColumn(ui.sbStartColumn->value()); filter->setEndColumn(ui.sbEndColumn->value()); break; } case AbstractFileFilter::HDF5: { DEBUG("ImportFileWidget::currentFileFilter(): HDF5"); if (!m_currentFilter) m_currentFilter.reset(new HDF5Filter); auto filter = static_cast(m_currentFilter.get()); QStringList names = selectedHDF5Names(); QDEBUG("ImportFileWidget::currentFileFilter(): selected HDF5 names =" << names); if (!names.isEmpty()) filter->setCurrentDataSetName(names[0]); filter->setStartRow(ui.sbStartRow->value()); filter->setEndRow(ui.sbEndRow->value()); filter->setStartColumn(ui.sbStartColumn->value()); filter->setEndColumn(ui.sbEndColumn->value()); DEBUG("ImportFileWidget::currentFileFilter(): OK"); break; } case AbstractFileFilter::NETCDF: { DEBUG(" NETCDF"); if (!m_currentFilter) m_currentFilter.reset(new NetCDFFilter); auto filter = static_cast(m_currentFilter.get()); if (!selectedNetCDFNames().isEmpty()) filter->setCurrentVarName(selectedNetCDFNames()[0]); filter->setStartRow(ui.sbStartRow->value()); filter->setEndRow(ui.sbEndRow->value()); filter->setStartColumn(ui.sbStartColumn->value()); filter->setEndColumn(ui.sbEndColumn->value()); break; } case AbstractFileFilter::FITS: { DEBUG(" FITS"); if (!m_currentFilter) m_currentFilter.reset(new FITSFilter); auto filter = static_cast(m_currentFilter.get()); filter->setStartRow(ui.sbStartRow->value()); filter->setEndRow(ui.sbEndRow->value()); filter->setStartColumn(ui.sbStartColumn->value()); filter->setEndColumn(ui.sbEndColumn->value()); break; } case AbstractFileFilter::JSON: { DEBUG(" JSON"); if (!m_currentFilter) m_currentFilter.reset(new JsonFilter); auto filter = static_cast(m_currentFilter.get()); m_jsonOptionsWidget->applyFilterSettings(filter, ui.tvJson->currentIndex()); filter->setStartRow(ui.sbStartRow->value()); filter->setEndRow(ui.sbEndRow->value()); filter->setStartColumn(ui.sbStartColumn->value()); filter->setEndColumn(ui.sbEndColumn->value()); break; } case AbstractFileFilter::ROOT: { DEBUG(" ROOT"); if (!m_currentFilter) m_currentFilter.reset(new ROOTFilter); auto filter = static_cast(m_currentFilter.get()); QStringList names = selectedROOTNames(); if (!names.isEmpty()) filter->setCurrentObject(names.first()); filter->setStartRow(m_rootOptionsWidget->startRow()); filter->setEndRow(m_rootOptionsWidget->endRow()); filter->setColumns(m_rootOptionsWidget->columns()); break; } case AbstractFileFilter::NgspiceRawAscii: { DEBUG(" NgspiceRawAscii"); if (!m_currentFilter) m_currentFilter.reset(new NgspiceRawAsciiFilter); auto filter = static_cast(m_currentFilter.get()); filter->setStartRow(ui.sbStartRow->value()); filter->setEndRow(ui.sbEndRow->value()); break; } case AbstractFileFilter::NgspiceRawBinary: { DEBUG(" NgspiceRawBinary"); if (!m_currentFilter) m_currentFilter.reset(new NgspiceRawBinaryFilter); auto filter = static_cast(m_currentFilter.get()); filter->setStartRow(ui.sbStartRow->value()); filter->setEndRow(ui.sbEndRow->value()); break; } } return m_currentFilter.get(); } /*! opens a file dialog and lets the user select the file data source. */ void ImportFileWidget::selectFile() { KConfigGroup conf(KSharedConfig::openConfig(), QLatin1String("ImportFileWidget")); const QString& dir = conf.readEntry(QLatin1String("LastDir"), ""); const QString& path = QFileDialog::getOpenFileName(this, i18n("Select the File Data Source"), dir); if (path.isEmpty()) //cancel was clicked in the file-dialog return; int pos = path.lastIndexOf(QDir::separator()); if (pos != -1) { QString newDir = path.left(pos); if (newDir != dir) conf.writeEntry(QLatin1String("LastDir"), newDir); } //process all events after the FileDialog was closed to repaint the widget //before we start calculating the preview QApplication::processEvents(QEventLoop::AllEvents, 0); ui.leFileName->setText(path); } /*! hides the MQTT related items of the widget */ void ImportFileWidget::setMQTTVisible(bool visible) { ui.lConnections->setVisible(visible); ui.cbConnection->setVisible(visible); ui.bManageConnections->setVisible(visible); ui.cbQos->setVisible(visible); //topics if (ui.cbConnection->currentIndex() != -1 && visible) { ui.lTopics->setVisible(true); ui.gbManageSubscriptions->setVisible(true); } else { ui.lTopics->setVisible(false); ui.gbManageSubscriptions->setVisible(false); } //will message ui.lLWT->setVisible(visible); ui.bLWT->setVisible(visible); } #ifdef HAVE_MQTT /*! * returns \c true if there is a valid connection to an MQTT broker and the user has subscribed to at least 1 topic, * returns \c false otherwise. */ bool ImportFileWidget::isMqttValid() { if (!m_client) return false; bool connected = (m_client->state() == QMqttClient::ClientState::Connected); bool subscribed = (ui.twSubscriptions->topLevelItemCount() > 0); bool fileTypeOk = false; if (this->currentFileType() == AbstractFileFilter::FileType::Ascii) fileTypeOk = true; return connected && subscribed && fileTypeOk; } /*! *\brief Checks if a topic contains another one * * \param superior the name of a topic * \param inferior the name of a topic * \return true if superior is equal to or contains(if superior contains wildcards) inferior, * false otherwise */ bool ImportFileWidget::checkTopicContains(const QString& superior, const QString& inferior) { if (superior == inferior) return true; if (!superior.contains('/')) return false; const QStringList& superiorList = superior.split('/', QString::SkipEmptyParts); const QStringList& inferiorList = inferior.split('/', QString::SkipEmptyParts); //a longer topic can't contain a shorter one if (superiorList.size() > inferiorList.size()) return false; bool ok = true; for (int i = 0; i < superiorList.size(); ++i) { if (superiorList.at(i) != inferiorList.at(i)) { if ((superiorList.at(i) != '+') && !(superiorList.at(i) == "#" && i == superiorList.size() - 1)) { //if the two topics differ, and the superior's current level isn't + or #(which can be only in the last position) //then superior can't contain inferior ok = false; break; } else if (i == superiorList.size() - 1 && (superiorList.at(i) == '+' && inferiorList.at(i) == "#") ) { //if the two topics differ at the last level //and the superior's current level is + while the inferior's is #(which can be only in the last position) //then superior can't contain inferior ok = false; break; } } } return ok; } /*! *\brief Returns the '+' wildcard containing topic name, which includes the given topic names * * \param first the name of a topic * \param second the name of a topic * \return The name of the common topic, if it exists, otherwise "" */ QString ImportFileWidget::checkCommonLevel(const QString& first, const QString& second) { const QStringList& firstList = first.split('/', QString::SkipEmptyParts); if (firstList.isEmpty()) return QString(); const QStringList& secondtList = second.split('/', QString::SkipEmptyParts); QString commonTopic = ""; //the two topics have to be the same size and can't be identic if (firstList.size() == secondtList.size() && (first != second)) { //the index where they differ int differIndex = -1; for (int i = 0; i < firstList.size(); ++i) { if (firstList.at(i) != secondtList.at(i)) { differIndex = i; break; } } //they can differ at only one level bool differ = false; if (differIndex > 0) { for (int j = differIndex + 1; j < firstList.size(); ++j) { if (firstList.at(j) != secondtList.at(j)) { differ = true; break; } } } else differ = true; if (!differ) { for (int i = 0; i < firstList.size(); ++i) { if (i != differIndex) { commonTopic.append(firstList.at(i)); } else { //we put '+' wildcard at the level where they differ commonTopic.append('+'); } if (i != firstList.size() - 1) commonTopic.append('/'); } } } qDebug() << "Common topic for " << first << " and " << second << " is: " << commonTopic; return commonTopic; } /*! *\brief Returns the index of level where the two topic names differ, if there is a common topic for them * * \param first the name of a topic * \param second the name of a topic * \return The index of the unequal level, if there is a common topic, otherwise -1 */ int ImportFileWidget::commonLevelIndex(const QString& first, const QString& second) { QStringList firstList = first.split('/', QString::SkipEmptyParts); QStringList secondtList = second.split('/', QString::SkipEmptyParts); QString commonTopic = ""; int differIndex = -1; if (!firstList.isEmpty()) { //the two topics have to be the same size and can't be identic if (firstList.size() == secondtList.size() && (first != second)) { //the index where they differ for (int i = 0; i < firstList.size(); ++i) { if (firstList.at(i) != secondtList.at(i)) { differIndex = i; break; } } //they can differ at only one level bool differ = false; if (differIndex > 0) { for (int j = differIndex + 1; j < firstList.size(); ++j) { if (firstList.at(j) != secondtList.at(j)) { differ = true; break; } } } else differ = true; if (!differ) { for (int i = 0; i < firstList.size(); ++i) { if (i != differIndex) commonTopic.append(firstList.at(i)); else commonTopic.append('+'); if (i != firstList.size() - 1) commonTopic.append('/'); } } } } //if there is a common topic we return the differIndex if (!commonTopic.isEmpty()) return differIndex; else return -1; } /*! *\brief Unsubscribes from the given topic, and removes any data connected to it * * \param topicName the name of a topic we want to unsubscribe from */ void ImportFileWidget::unsubscribeFromTopic(const QString& topicName) { if (topicName.isEmpty()) return; QMqttTopicFilter filter{topicName}; m_client->unsubscribe(filter); for (int i = 0; i< m_mqttSubscriptions.count(); ++i) if (m_mqttSubscriptions[i]->topic().filter() == topicName) { m_mqttSubscriptions.remove(i); break; } m_mqttReadyForPreview = false; QMapIterator i(m_messageArrived); while (i.hasNext()) { i.next(); if (checkTopicContains(topicName, i.key().name())) m_messageArrived.remove(i.key()); } QMapIterator j(m_lastMessage); while (j.hasNext()) { j.next(); if (checkTopicContains(topicName, j.key().name())) m_lastMessage.remove(j.key()); } for (int row = 0; row < ui.twSubscriptions->topLevelItemCount(); row++) { if (ui.twSubscriptions->topLevelItem(row)->text(0) == topicName) { ui.twSubscriptions->topLevelItem(row)->takeChildren(); ui.twSubscriptions->takeTopLevelItem(row); } } for (int i = 0; i < m_subscribedTopicNames.size(); ++i) { if (checkTopicContains(topicName, m_subscribedTopicNames[i])) { m_subscribedTopicNames.remove(i); i--; } } if (m_willSettings.willTopic == topicName) { QVector children; if (ui.twSubscriptions->topLevelItemCount() > 0) { findSubscriptionLeafChildren(children, ui.twSubscriptions->topLevelItem(0)); m_willSettings.willTopic = children[0]->text(0); } else m_willSettings.willTopic = ""; } //signals that there was a change among the subscribed topics emit subscriptionsChanged(); refreshPreview(); } /*! *\brief Adds to a # wildcard containing topic, every topic present in twTopics that the former topic contains * * \param topic pointer to the TreeWidgetItem which was selected before subscribing * \param subscription pointer to the TreeWidgetItem which represents the new subscirption, * we add all of the children to this item */ void ImportFileWidget::addSubscriptionChildren(QTreeWidgetItem * topic, QTreeWidgetItem * subscription) { //if the topic doesn't have any children we don't do anything if (topic->childCount() <= 0) return; for (int i = 0; i < topic->childCount(); ++i) { QTreeWidgetItem* temp = topic->child(i); QString name; //if it has children, then we add it as a # wildcrad containing topic if (topic->child(i)->childCount() > 0) { name.append(temp->text(0) + "/#"); while (temp->parent() != nullptr) { temp = temp->parent(); name.prepend(temp->text(0) + '/'); } } //if not then we simply add the topic itself else { name.append(temp->text(0)); while (temp->parent() != nullptr) { temp = temp->parent(); name.prepend(temp->text(0) + '/'); } } QStringList nameList; nameList.append(name); QTreeWidgetItem* childItem = new QTreeWidgetItem(nameList); subscription->addChild(childItem); //we use the function recursively on the given item addSubscriptionChildren(topic->child(i), childItem); } } /*! *\brief Fills the children vector, with the root item's (twSubscriptions) leaf children (meaning no wildcard containing topics) * * \param children vector of TreeWidgetItem pointers * \param root pointer to a TreeWidgetItem of twSubscriptions */ void ImportFileWidget::findSubscriptionLeafChildren(QVector& children, QTreeWidgetItem* root) { if (root->childCount() == 0) children.push_back(root); else for (int i = 0; i < root->childCount(); ++i) findSubscriptionLeafChildren(children, root->child(i)); } /*! *\brief Returns the amount of topics that the '+' wildcard will replace in the level position * * \param levelIdx the level currently being investigated * \param level the level where the new + wildcard will be placed * \param commonList the topic name split into levels * \param currentItem pointer to a TreeWidgetItem which represents the parent of the level * represented by levelIdx * \return returns the childCount, or -1 if some topics already represented by + wildcard have different * amount of children */ int ImportFileWidget::checkCommonChildCount(int levelIdx, int level, QStringList& commonList, QTreeWidgetItem* currentItem) { //we recursively check the number of children, until we get to level-1 if (levelIdx < level - 1) { if (commonList[levelIdx] != '+') { for (int j = 0; j < currentItem->childCount(); ++j) { if (currentItem->child(j)->text(0) == commonList[levelIdx]) { //if the level isn't represented by + wildcard we simply return the amount of children of the corresponding item, recursively return checkCommonChildCount(levelIdx + 1, level, commonList, currentItem->child(j)); } } } else { int childCount = -1; bool ok = true; //otherwise we check if every + wildcard represented topic has the same number of children, recursively for (int j = 0; j < currentItem->childCount(); ++j) { int temp = checkCommonChildCount(levelIdx + 1, level, commonList, currentItem->child(j)); if ((j > 0) && (temp != childCount)) { ok = false; break; } childCount = temp; } //if yes we return this number, otherwise -1 if (ok) return childCount; else return -1; } } else if (levelIdx == level - 1) { if (commonList[levelIdx] != '+') { for (int j = 0; j < currentItem->childCount(); ++j) { if (currentItem->child(j)->text(0) == commonList[levelIdx]) { //if the level isn't represented by + wildcard we simply return the amount of children of the corresponding item return currentItem->child(j)->childCount(); } } } else { int childCount = -1; bool ok = true; //otherwise we check if every + wildcard represented topic has the same number of children for (int j = 0; j < currentItem->childCount(); ++j) { if ((j > 0) && (currentItem->child(j)->childCount() != childCount)) { ok = false; break; } childCount = currentItem->child(j)->childCount(); } //if yes we return this number, otherwise -1 if (ok) return childCount; else return -1; } } else if (level == 1 && levelIdx == 1) return currentItem->childCount(); return -1; } /*! *\brief We search in twSubscriptions for topics that can be represented using + wildcards, then merge them. * We do this until there are no topics to merge */ void ImportFileWidget::manageCommonLevelSubscriptions() { bool foundEqual = false; do { foundEqual = false; QMap> equalTopicsMap; QVector equalTopics; //compare the subscriptions present in the TreeWidget for (int i = 0; i < ui.twSubscriptions->topLevelItemCount() - 1; ++i) { for (int j = i + 1; j < ui.twSubscriptions->topLevelItemCount(); ++j) { QString commonTopic = checkCommonLevel(ui.twSubscriptions->topLevelItem(i)->text(0), ui.twSubscriptions->topLevelItem(j)->text(0)); //if there is a common topic for the 2 compared topics, we add them to the map (using the common topic as key) if (!commonTopic.isEmpty()) { if (!equalTopicsMap[commonTopic].contains(ui.twSubscriptions->topLevelItem(i)->text(0))) { equalTopicsMap[commonTopic].push_back(ui.twSubscriptions->topLevelItem(i)->text(0)); } if (!equalTopicsMap[commonTopic].contains(ui.twSubscriptions->topLevelItem(j)->text(0))) { equalTopicsMap[commonTopic].push_back(ui.twSubscriptions->topLevelItem(j)->text(0)); } } } } if (!equalTopicsMap.isEmpty()) { qDebug()<<"Manage common topics"; QVector commonTopics; QMapIterator> topics(equalTopicsMap); //check for every map entry, if the found topics can be merged or not while (topics.hasNext()) { topics.next(); int level = commonLevelIndex(topics.value().last(), topics.value().first()); QStringList commonList = topics.value().first().split('/', QString::SkipEmptyParts); QTreeWidgetItem* currentItem = nullptr; //search the corresponding item to the common topics first level(root) for (int i = 0; i < ui.twTopics->topLevelItemCount(); ++i) { if (ui.twTopics->topLevelItem(i)->text(0) == commonList.first()) { currentItem = ui.twTopics->topLevelItem(i); break; } } if (!currentItem) break; //calculate the number of topics the new + wildcard could replace int childCount = checkCommonChildCount(1, level, commonList, currentItem); if (childCount > 0) { //if the number of topics found and the calculated number of topics is equal, the topics can be merged if (topics.value().size() == childCount) { qDebug() << "Found common topic to manage: " << topics.key(); foundEqual = true; commonTopics.push_back(topics.key()); } } } if (foundEqual) { //if there are more common topics, the topics of which can be merged, we choose the one which has the lowest level new '+' wildcard int lowestLevel = INT_MAX; int topicIdx = -1; for (int i = 0; i < commonTopics.size(); ++i) { int level = commonLevelIndex(equalTopicsMap[commonTopics[i]].first(), commonTopics[i]); if (level < lowestLevel) { topicIdx = i; lowestLevel = level; } } qDebug() << "Manage: " << commonTopics[topicIdx]; equalTopics.append(equalTopicsMap[commonTopics[topicIdx]]); //Add the common topic ("merging") QString commonTopic; commonTopic = checkCommonLevel(equalTopics.first(), equalTopics.last()); QStringList nameList; nameList.append(commonTopic); QTreeWidgetItem* newTopic = new QTreeWidgetItem(nameList); ui.twSubscriptions->addTopLevelItem(newTopic); QMqttTopicFilter filter {commonTopic}; QMqttSubscription *temp_subscription = m_client->subscribe(filter, static_cast (ui.cbQos->currentText().toUInt()) ); if (temp_subscription) { m_mqttSubscriptions.push_back(temp_subscription); connect(temp_subscription, &QMqttSubscription::messageReceived, this, &ImportFileWidget::mqttSubscriptionMessageReceived); emit subscriptionsChanged(); } //remove the "merged" topics for (int i = 0; i < equalTopics.size(); ++i) { for (int j = 0; j < ui.twSubscriptions->topLevelItemCount(); ++j) { if (ui.twSubscriptions->topLevelItem(j)->text(0) == equalTopics[i]) { newTopic->addChild(ui.twSubscriptions->takeTopLevelItem(j)); unsubscribeFromTopic(equalTopics[i]); break; } } } //remove any subscription that the new subscription contains for (int i = 0; i < ui.twSubscriptions->topLevelItemCount(); ++i) { if (checkTopicContains(commonTopic, ui.twSubscriptions->topLevelItem(i)->text(0)) && commonTopic != ui.twSubscriptions->topLevelItem(i)->text(0) ) { unsubscribeFromTopic(ui.twSubscriptions->topLevelItem(i)->text(0)); i--; } } } } } while (foundEqual); } /*! *\brief Fills twSubscriptions with the subscriptions made by the client */ void ImportFileWidget::updateSubscriptionTree() { DEBUG("ImportFileWidget::updateSubscriptionTree()"); ui.twSubscriptions->clear(); for (int i = 0; i < m_mqttSubscriptions.size(); ++i) { QStringList name; name.append(m_mqttSubscriptions[i]->topic().filter()); bool found = false; for (int j = 0; j < ui.twSubscriptions->topLevelItemCount(); ++j) { if (ui.twSubscriptions->topLevelItem(j)->text(0) == m_mqttSubscriptions[i]->topic().filter()) { found = true; break; } } if (!found) { //Add the subscription to the tree widget QTreeWidgetItem* newItem = new QTreeWidgetItem(name); ui.twSubscriptions->addTopLevelItem(newItem); name.clear(); name = m_mqttSubscriptions[i]->topic().filter().split('/', QString::SkipEmptyParts); //find the corresponding "root" item in twTopics QTreeWidgetItem* topic = nullptr; for (int j = 0; j < ui.twTopics->topLevelItemCount(); ++j) { if (ui.twTopics->topLevelItem(j)->text(0) == name[0]) { topic = ui.twTopics->topLevelItem(j); break; } } //restore the children of the subscription if (topic != nullptr && topic->childCount() > 0) { restoreSubscriptionChildren(topic, newItem, name, 1); } } } m_searching = false; } /*! *\brief Restores the children of a top level item in twSubscriptions if it contains wildcards * * \param topic pointer to a top level item in twTopics which represents the root of the subscription topic * \param subscription pointer to a top level item in twSubscriptions, this is the item whose children will be restored * \param list QStringList containing the levels of the subscription topic * \param level the level's number which is being investigated */ void ImportFileWidget::restoreSubscriptionChildren(QTreeWidgetItem * topic, QTreeWidgetItem * subscription, const QStringList& list, int level) { DEBUG("ImportFileWidget::restoreSubscriptionChildren"); if (list[level] != '+' && list[level] != "#" && level < list.size() - 1) { for (int i = 0; i < topic->childCount(); ++i) { //if the current level isn't + or # wildcard we recursively continue with the next level if (topic->child(i)->text(0) == list[level]) { restoreSubscriptionChildren(topic->child(i), subscription, list, level + 1); break; } } } else if (list[level] == '+') { for (int i = 0; i < topic->childCount(); ++i) { //determine the name of the topic, contained by the subscription QString name; name.append(topic->child(i)->text(0)); for (int j = level + 1; j < list.size(); ++j) { name.append('/' + list[j]); } QTreeWidgetItem* temp = topic->child(i); while (temp->parent() != nullptr) { temp = temp->parent(); name.prepend(temp->text(0) + '/'); } //Add the topic as child of the subscription QStringList nameList; nameList.append(name); QTreeWidgetItem* newItem = new QTreeWidgetItem(nameList); subscription->addChild(newItem); //Continue adding children recursively to the new item restoreSubscriptionChildren(topic->child(i), newItem, list, level + 1); } } else if (list[level] == "#") { //add the children of the # wildcard containing subscription addSubscriptionChildren(topic, subscription); } } /*! *\brief Updates the completer for leSubscriptions */ void ImportFileWidget::updateSubscriptionCompleter() { QStringList subscriptionList; for (int i = 0; i < ui.twSubscriptions->topLevelItemCount(); ++i) subscriptionList.append(ui.twSubscriptions->topLevelItem(i)->text(0)); if (!subscriptionList.isEmpty()) { m_subscriptionCompleter = new QCompleter(subscriptionList, this); m_subscriptionCompleter->setCompletionMode(QCompleter::PopupCompletion); m_subscriptionCompleter->setCaseSensitivity(Qt::CaseSensitive); ui.leSubscriptions->setCompleter(m_subscriptionCompleter); } else ui.leSubscriptions->setCompleter(nullptr); } #endif /************** SLOTS **************************************************************/ QString absolutePath(const QString& fileName) { #ifndef HAVE_WINDOWS // make absolute path // FIXME if (!fileName.isEmpty() && fileName.at(0) != QDir::separator()) return QDir::homePath() + QDir::separator() + fileName; #endif return fileName; } /*! called on file name changes. Determines the file format (ASCII, binary etc.), if the file exists, and activates the corresponding options. */ void ImportFileWidget::fileNameChanged(const QString& name) { DEBUG("ImportFileWidget::fileNameChanged()"); const QString fileName = absolutePath(name); bool fileExists = QFile::exists(fileName); if (fileExists) ui.leFileName->setStyleSheet(""); else ui.leFileName->setStyleSheet("QLineEdit{background:red;}"); ui.gbOptions->setEnabled(fileExists); ui.bManageFilters->setEnabled(fileExists); ui.cbFilter->setEnabled(fileExists); ui.cbFileType->setEnabled(fileExists); ui.bFileInfo->setEnabled(fileExists); ui.gbUpdateOptions->setEnabled(fileExists); if (!fileExists) { //file doesn't exist -> delete the content preview that is still potentially //available from the previously selected file ui.tePreview->clear(); m_twPreview->clear(); initOptionsWidget(); emit fileNameChanged(); return; } if (currentSourceType() == LiveDataSource::FileOrPipe) { const AbstractFileFilter::FileType fileType = AbstractFileFilter::fileType(fileName); for (int i = 0; i < ui.cbFileType->count(); ++i) { if (static_cast(ui.cbFileType->itemData(i).toInt()) == fileType) { // automatically select a new file type if (ui.cbFileType->currentIndex() != i) { ui.cbFileType->setCurrentIndex(i); // will call the slot fileTypeChanged which updates content and preview emit fileNameChanged(); return; } else { initOptionsWidget(); updateContent(fileName); break; } } } } emit fileNameChanged(); refreshPreview(); } /*! saves the current filter settings */ void ImportFileWidget::saveFilter() { bool ok; QString text = QInputDialog::getText(this, i18n("Save Filter Settings as"), i18n("Filter name:"), QLineEdit::Normal, i18n("new filter"), &ok); if (ok && !text.isEmpty()) { //TODO //AsciiFilter::saveFilter() } } /*! opens a dialog for managing all available predefined filters. */ void ImportFileWidget::manageFilters() { //TODO } /*! Depending on the selected file type, activates the corresponding options in the data portion tab and populates the combobox with the available pre-defined fllter settings for the selected type. */ void ImportFileWidget::fileTypeChanged(int index) { Q_UNUSED(index); AbstractFileFilter::FileType fileType = currentFileType(); DEBUG("ImportFileWidget::fileTypeChanged " << ENUM_TO_STRING(AbstractFileFilter, FileType, fileType)); initOptionsWidget(); //default ui.lFilter->show(); ui.cbFilter->show(); //different file types show different number of tabs in ui.tabWidget. //when switching from the previous file type we re-set the tab widget to its original state //and remove/add the required tabs further below for (int i = 0; icount(); ++i) ui.tabWidget->removeTab(0); ui.tabWidget->addTab(ui.tabDataFormat, i18n("Data format")); ui.tabWidget->addTab(ui.tabDataPreview, i18n("Preview")); ui.tabWidget->addTab(ui.tabDataPortion, i18n("Data portion to read")); ui.lPreviewLines->show(); ui.sbPreviewLines->show(); ui.lStartColumn->show(); ui.sbStartColumn->show(); ui.lEndColumn->show(); ui.sbEndColumn->show(); showJsonModel(false); switch (fileType) { case AbstractFileFilter::Ascii: break; case AbstractFileFilter::Binary: ui.lStartColumn->hide(); ui.sbStartColumn->hide(); ui.lEndColumn->hide(); ui.sbEndColumn->hide(); break; case AbstractFileFilter::ROOT: ui.tabWidget->removeTab(1); // falls through case AbstractFileFilter::HDF5: case AbstractFileFilter::NETCDF: case AbstractFileFilter::FITS: ui.lFilter->hide(); ui.cbFilter->hide(); // hide global preview tab. we have our own ui.tabWidget->setTabText(0, i18n("Data format && preview")); ui.tabWidget->removeTab(1); ui.tabWidget->setCurrentIndex(0); break; case AbstractFileFilter::Image: ui.lFilter->hide(); ui.cbFilter->hide(); ui.lPreviewLines->hide(); ui.sbPreviewLines->hide(); break; case AbstractFileFilter::NgspiceRawAscii: case AbstractFileFilter::NgspiceRawBinary: ui.lFilter->hide(); ui.cbFilter->hide(); ui.lStartColumn->hide(); ui.sbStartColumn->hide(); ui.lEndColumn->hide(); ui.sbEndColumn->hide(); ui.tabWidget->removeTab(0); ui.tabWidget->setCurrentIndex(0); break; case AbstractFileFilter::JSON: ui.lFilter->hide(); ui.cbFilter->hide(); showJsonModel(true); break; default: DEBUG("unknown file type"); } int lastUsedFilterIndex = ui.cbFilter->currentIndex(); ui.cbFilter->clear(); ui.cbFilter->addItem( i18n("Automatic") ); ui.cbFilter->addItem( i18n("Custom") ); //TODO: populate the combobox with the available pre-defined filter settings for the selected type ui.cbFilter->setCurrentIndex(lastUsedFilterIndex); filterChanged(lastUsedFilterIndex); if (currentSourceType() == LiveDataSource::FileOrPipe) { const QString& fileName = absolutePath(ui.leFileName->text()); if (QFile::exists(fileName)) updateContent(fileName); } //for file types other than ASCII and binary we support re-reading the whole file only //select "read whole file" and deactivate the combobox if (m_liveDataSource && (fileType != AbstractFileFilter::Ascii && fileType != AbstractFileFilter::Binary)) { ui.cbReadingType->setCurrentIndex(3); ui.cbReadingType->setEnabled(false); } else ui.cbReadingType->setEnabled(true); refreshPreview(); } // file type specific option widgets void ImportFileWidget::initOptionsWidget() { DEBUG("ImportFileWidget::initOptionsWidget for " << ENUM_TO_STRING(AbstractFileFilter, FileType, currentFileType())); switch (currentFileType()) { case AbstractFileFilter::Ascii: if (!m_asciiOptionsWidget) { QWidget* asciiw = new QWidget(); m_asciiOptionsWidget = std::unique_ptr(new AsciiOptionsWidget(asciiw)); m_asciiOptionsWidget->loadSettings(); ui.swOptions->addWidget(asciiw); } ui.swOptions->setCurrentWidget(m_asciiOptionsWidget->parentWidget()); break; case AbstractFileFilter::Binary: if (!m_binaryOptionsWidget) { QWidget* binaryw = new QWidget(); m_binaryOptionsWidget = std::unique_ptr(new BinaryOptionsWidget(binaryw)); ui.swOptions->addWidget(binaryw); m_binaryOptionsWidget->loadSettings(); } ui.swOptions->setCurrentWidget(m_binaryOptionsWidget->parentWidget()); break; case AbstractFileFilter::Image: if (!m_imageOptionsWidget) { QWidget* imagew = new QWidget(); m_imageOptionsWidget = std::unique_ptr(new ImageOptionsWidget(imagew)); ui.swOptions->addWidget(imagew); m_imageOptionsWidget->loadSettings(); } ui.swOptions->setCurrentWidget(m_imageOptionsWidget->parentWidget()); break; case AbstractFileFilter::HDF5: if (!m_hdf5OptionsWidget) { QWidget* hdf5w = new QWidget(); m_hdf5OptionsWidget = std::unique_ptr(new HDF5OptionsWidget(hdf5w, this)); ui.swOptions->addWidget(hdf5w); } else m_hdf5OptionsWidget->clear(); ui.swOptions->setCurrentWidget(m_hdf5OptionsWidget->parentWidget()); break; case AbstractFileFilter::NETCDF: if (!m_netcdfOptionsWidget) { QWidget* netcdfw = new QWidget(); m_netcdfOptionsWidget = std::unique_ptr(new NetCDFOptionsWidget(netcdfw, this)); ui.swOptions->insertWidget(AbstractFileFilter::NETCDF, netcdfw); } else m_netcdfOptionsWidget->clear(); ui.swOptions->setCurrentWidget(m_netcdfOptionsWidget->parentWidget()); break; case AbstractFileFilter::FITS: if (!m_fitsOptionsWidget) { QWidget* fitsw = new QWidget(); m_fitsOptionsWidget = std::unique_ptr(new FITSOptionsWidget(fitsw, this)); ui.swOptions->addWidget(fitsw); } else m_fitsOptionsWidget->clear(); ui.swOptions->setCurrentWidget(m_fitsOptionsWidget->parentWidget()); break; case AbstractFileFilter::JSON: if (!m_jsonOptionsWidget) { QWidget* jsonw = new QWidget(); m_jsonOptionsWidget = std::unique_ptr(new JsonOptionsWidget(jsonw, this)); ui.tvJson->setModel(m_jsonOptionsWidget->model()); ui.swOptions->addWidget(jsonw); m_jsonOptionsWidget->loadSettings(); } else m_jsonOptionsWidget->clearModel(); ui.swOptions->setCurrentWidget(m_jsonOptionsWidget->parentWidget()); break; case AbstractFileFilter::ROOT: if (!m_rootOptionsWidget) { QWidget* rootw = new QWidget(); m_rootOptionsWidget = std::unique_ptr(new ROOTOptionsWidget(rootw, this)); ui.swOptions->addWidget(rootw); } else m_rootOptionsWidget->clear(); ui.swOptions->setCurrentWidget(m_rootOptionsWidget->parentWidget()); break; case AbstractFileFilter::NgspiceRawAscii: case AbstractFileFilter::NgspiceRawBinary: break; } } const QStringList ImportFileWidget::selectedHDF5Names() const { return m_hdf5OptionsWidget->selectedNames(); } const QStringList ImportFileWidget::selectedNetCDFNames() const { return m_netcdfOptionsWidget->selectedNames(); } const QStringList ImportFileWidget::selectedFITSExtensions() const { return m_fitsOptionsWidget->selectedExtensions(); } const QStringList ImportFileWidget::selectedROOTNames() const { return m_rootOptionsWidget->selectedNames(); } /*! shows the dialog with the information about the file(s) to be imported. */ void ImportFileWidget::fileInfoDialog() { QStringList files = ui.leFileName->text().split(';'); auto* dlg = new FileInfoDialog(this); dlg->setFiles(files); dlg->exec(); } /*! enables the options if the filter "custom" was chosen. Disables the options otherwise. */ void ImportFileWidget::filterChanged(int index) { // ignore filter for these formats AbstractFileFilter::FileType fileType = currentFileType(); if (fileType != AbstractFileFilter::Ascii && fileType != AbstractFileFilter::Binary) { ui.swOptions->setEnabled(true); return; } if (index == 0) { // "automatic" ui.swOptions->setEnabled(false); ui.bSaveFilter->setEnabled(false); } else if (index == 1) { //custom ui.swOptions->setEnabled(true); ui.bSaveFilter->setEnabled(true); } else { // predefined filter settings were selected. //load and show them in the GUI. //TODO } } void ImportFileWidget::refreshPreview() { DEBUG("ImportFileWidget::refreshPreview()"); if (m_suppressRefresh) return; WAIT_CURSOR; QString fileName = absolutePath(ui.leFileName->text()); AbstractFileFilter::FileType fileType = currentFileType(); LiveDataSource::SourceType sourceType = currentSourceType(); int lines = ui.sbPreviewLines->value(); if (sourceType == LiveDataSource::SourceType::FileOrPipe) DEBUG("refreshPreview(): file name = " << fileName.toStdString()); // generic table widget if (fileType == AbstractFileFilter::Ascii || fileType == AbstractFileFilter::Binary || fileType == AbstractFileFilter::JSON || fileType == AbstractFileFilter::NgspiceRawAscii || fileType == AbstractFileFilter::NgspiceRawBinary) m_twPreview->show(); else m_twPreview->hide(); bool ok = true; QTableWidget* tmpTableWidget = m_twPreview; QVector importedStrings; QStringList vectorNameList; QVector columnModes; DEBUG("Data File Type: " << ENUM_TO_STRING(AbstractFileFilter, FileType, fileType)); switch (fileType) { case AbstractFileFilter::Ascii: { ui.tePreview->clear(); auto filter = static_cast(currentFileFilter()); DEBUG("Data Source Type: " << ENUM_TO_STRING(LiveDataSource, SourceType, sourceType)); switch (sourceType) { case LiveDataSource::SourceType::FileOrPipe: { importedStrings = filter->preview(fileName, lines); break; } case LiveDataSource::SourceType::LocalSocket: { QLocalSocket lsocket{this}; DEBUG("Local socket: CONNECT PREVIEW"); lsocket.connectToServer(fileName, QLocalSocket::ReadOnly); if (lsocket.waitForConnected()) { DEBUG("connected to local socket " << fileName.toStdString()); if (lsocket.waitForReadyRead()) importedStrings = filter->preview(lsocket); DEBUG("Local socket: DISCONNECT PREVIEW"); lsocket.disconnectFromServer(); // read-only socket is disconnected immediately (no waitForDisconnected()) } else { DEBUG("failed connect to local socket " << fileName.toStdString() << " - " << lsocket.errorString().toStdString()); } break; } case LiveDataSource::SourceType::NetworkTcpSocket: { QTcpSocket tcpSocket{this}; tcpSocket.connectToHost(host(), port().toInt(), QTcpSocket::ReadOnly); if (tcpSocket.waitForConnected()) { DEBUG("connected to TCP socket"); if ( tcpSocket.waitForReadyRead() ) importedStrings = filter->preview(tcpSocket); tcpSocket.disconnectFromHost(); } else { DEBUG("failed to connect to TCP socket " << " - " << tcpSocket.errorString().toStdString()); } break; } case LiveDataSource::SourceType::NetworkUdpSocket: { QUdpSocket udpSocket{this}; DEBUG("UDP Socket: CONNECT PREVIEW, state = " << udpSocket.state()); udpSocket.bind(QHostAddress(host()), port().toInt()); udpSocket.connectToHost(host(), 0, QUdpSocket::ReadOnly); if (udpSocket.waitForConnected()) { DEBUG(" connected to UDP socket " << host().toStdString() << ':' << port().toInt()); if (!udpSocket.waitForReadyRead(2000) ) DEBUG(" ERROR: not ready for read after 2 sec"); if (udpSocket.hasPendingDatagrams()) { DEBUG(" has pending data"); } else { DEBUG(" has no pending data"); } importedStrings = filter->preview(udpSocket); DEBUG("UDP Socket: DISCONNECT PREVIEW, state = " << udpSocket.state()); udpSocket.disconnectFromHost(); } else { DEBUG("failed to connect to UDP socket " << " - " << udpSocket.errorString().toStdString()); } break; } case LiveDataSource::SourceType::SerialPort: { QSerialPort sPort{this}; DEBUG(" Port: " << serialPort().toStdString() << ", Settings: " << baudRate() << ',' << sPort.dataBits() << ',' << sPort.parity() << ',' << sPort.stopBits()); sPort.setPortName(serialPort()); sPort.setBaudRate(baudRate()); if (sPort.open(QIODevice::ReadOnly)) { if (sPort.waitForReadyRead(2000)) importedStrings = filter->preview(sPort); else DEBUG(" ERROR: not ready for read after 2 sec"); sPort.close(); } else { DEBUG(" ERROR: failed to open serial port. error: " << sPort.error()); } break; } case LiveDataSource::SourceType::MQTT: { #ifdef HAVE_MQTT qDebug()<<"Start MQTT preview, is it ready:"<vectorNames().clear(); QMapIterator i(m_lastMessage); while (i.hasNext()) { i.next(); filter->MQTTPreview(importedStrings, QString(i.value().payload().data()), i.key().name() ); if (importedStrings.isEmpty()) break; } QMapIterator j(m_messageArrived); while (j.hasNext()) { j.next(); m_messageArrived[j.key()] = false; } m_mqttReadyForPreview = false; } #endif break; } } vectorNameList = filter->vectorNames(); columnModes = filter->columnModes(); break; } case AbstractFileFilter::Binary: { ui.tePreview->clear(); auto filter = static_cast(currentFileFilter()); importedStrings = filter->preview(fileName, lines); break; } case AbstractFileFilter::Image: { ui.tePreview->clear(); QImage image(fileName); QTextCursor cursor = ui.tePreview->textCursor(); cursor.insertImage(image); RESET_CURSOR; return; } case AbstractFileFilter::HDF5: { DEBUG("ImportFileWidget::refreshPreview: HDF5"); auto filter = static_cast(currentFileFilter()); lines = m_hdf5OptionsWidget->lines(); importedStrings = filter->readCurrentDataSet(fileName, nullptr, ok, AbstractFileFilter::Replace, lines); tmpTableWidget = m_hdf5OptionsWidget->previewWidget(); break; } case AbstractFileFilter::NETCDF: { auto filter = static_cast(currentFileFilter()); lines = m_netcdfOptionsWidget->lines(); importedStrings = filter->readCurrentVar(fileName, nullptr, AbstractFileFilter::Replace, lines); tmpTableWidget = m_netcdfOptionsWidget->previewWidget(); break; } case AbstractFileFilter::FITS: { auto filter = static_cast(currentFileFilter()); lines = m_fitsOptionsWidget->lines(); QString extensionName = m_fitsOptionsWidget->extensionName(&ok); if (!extensionName.isEmpty()) { DEBUG(" extension name = " << extensionName.toStdString()); fileName = extensionName; } DEBUG(" file name = " << fileName.toStdString()); bool readFitsTableToMatrix; importedStrings = filter->readChdu(fileName, &readFitsTableToMatrix, lines); emit checkedFitsTableToMatrix(readFitsTableToMatrix); tmpTableWidget = m_fitsOptionsWidget->previewWidget(); break; } case AbstractFileFilter::JSON: { ui.tePreview->clear(); m_jsonOptionsWidget->loadDocument(fileName); auto filter = static_cast(currentFileFilter()); m_jsonOptionsWidget->applyFilterSettings(filter, ui.tvJson->currentIndex()); importedStrings = filter->preview(fileName); vectorNameList = filter->vectorNames(); columnModes = filter->columnModes(); break; } case AbstractFileFilter::ROOT: { auto filter = static_cast(currentFileFilter()); lines = m_rootOptionsWidget->lines(); m_rootOptionsWidget->setNRows(filter->rowsInCurrentObject(fileName)); importedStrings = filter->previewCurrentObject( fileName, m_rootOptionsWidget->startRow(), qMin(m_rootOptionsWidget->startRow() + m_rootOptionsWidget->lines() - 1, m_rootOptionsWidget->endRow()) ); tmpTableWidget = m_rootOptionsWidget->previewWidget(); // the last vector element contains the column names vectorNameList = importedStrings.last(); importedStrings.removeLast(); columnModes = QVector(vectorNameList.size(), AbstractColumn::Numeric); break; } case AbstractFileFilter::NgspiceRawAscii: { ui.tePreview->clear(); auto filter = static_cast(currentFileFilter()); importedStrings = filter->preview(fileName, lines); vectorNameList = filter->vectorNames(); columnModes = filter->columnModes(); break; } case AbstractFileFilter::NgspiceRawBinary: { ui.tePreview->clear(); auto filter = static_cast(currentFileFilter()); importedStrings = filter->preview(fileName, lines); vectorNameList = filter->vectorNames(); columnModes = filter->columnModes(); break; } } // fill the table widget tmpTableWidget->setRowCount(0); tmpTableWidget->setColumnCount(0); if ( !importedStrings.isEmpty() ) { if (!ok) { // show imported strings as error message tmpTableWidget->setRowCount(1); tmpTableWidget->setColumnCount(1); auto* item = new QTableWidgetItem(); item->setText(importedStrings[0][0]); tmpTableWidget->setItem(0, 0, item); } else { //TODO: maxrows not used const int rows = qMax(importedStrings.size(), 1); const int maxColumns = 300; tmpTableWidget->setRowCount(rows); for (int i = 0; i < rows; ++i) { const int cols = importedStrings[i].size() > maxColumns ? maxColumns : importedStrings[i].size(); if (cols > tmpTableWidget->columnCount()) tmpTableWidget->setColumnCount(cols); for (int j = 0; j < cols; ++j) { auto* item = new QTableWidgetItem(importedStrings[i][j]); tmpTableWidget->setItem(i, j, item); } } // set header if columnMode available for (int i = 0; i < qMin(tmpTableWidget->columnCount(), columnModes.size()); ++i) { QString columnName = QString::number(i+1); if (i < vectorNameList.size()) columnName = vectorNameList[i]; auto* item = new QTableWidgetItem(columnName + QLatin1String(" {") + ENUM_TO_STRING(AbstractColumn, ColumnMode, columnModes[i]) + QLatin1String("}")); item->setTextAlignment(Qt::AlignLeft); item->setIcon(AbstractColumn::iconForMode(columnModes[i])); tmpTableWidget->setHorizontalHeaderItem(i, item); } } tmpTableWidget->horizontalHeader()->resizeSections(QHeaderView::ResizeToContents); m_fileEmpty = false; } else { m_fileEmpty = true; } emit previewRefreshed(); RESET_CURSOR; } void ImportFileWidget::updateContent(const QString& fileName) { DEBUG("ImportFileWidget::updateContent()"); if (auto filter = currentFileFilter()) { switch (filter->type()) { case AbstractFileFilter::HDF5: m_hdf5OptionsWidget->updateContent(static_cast(filter), fileName); break; case AbstractFileFilter::NETCDF: m_netcdfOptionsWidget->updateContent(static_cast(filter), fileName); break; case AbstractFileFilter::FITS: #ifdef HAVE_FITS m_fitsOptionsWidget->updateContent(static_cast(filter), fileName); #endif break; case AbstractFileFilter::ROOT: m_rootOptionsWidget->updateContent(static_cast(filter), fileName); break; case AbstractFileFilter::Ascii: case AbstractFileFilter::Binary: case AbstractFileFilter::Image: case AbstractFileFilter::JSON: case AbstractFileFilter::NgspiceRawAscii: case AbstractFileFilter::NgspiceRawBinary: break; } } } void ImportFileWidget::updateTypeChanged(int idx) { const auto UpdateType = static_cast(idx); switch (UpdateType) { case LiveDataSource::UpdateType::TimeInterval: ui.lUpdateInterval->show(); ui.sbUpdateInterval->show(); break; case LiveDataSource::UpdateType::NewData: ui.lUpdateInterval->hide(); ui.sbUpdateInterval->hide(); } } void ImportFileWidget::readingTypeChanged(int idx) { const auto readingType = static_cast(idx); const LiveDataSource::SourceType sourceType = currentSourceType(); if (sourceType == LiveDataSource::SourceType::NetworkTcpSocket || sourceType == LiveDataSource::SourceType::LocalSocket || sourceType == LiveDataSource::SourceType::SerialPort || readingType == LiveDataSource::ReadingType::TillEnd || readingType == LiveDataSource::ReadingType::WholeFile) { ui.lSampleSize->hide(); ui.sbSampleSize->hide(); } else { ui.lSampleSize->show(); ui.sbSampleSize->show(); } if (readingType == LiveDataSource::ReadingType::WholeFile) { ui.lKeepLastValues->hide(); ui.sbKeepNValues->hide(); } else { ui.lKeepLastValues->show(); ui.sbKeepNValues->show(); } } void ImportFileWidget::sourceTypeChanged(int idx) { const auto sourceType = static_cast(idx); // enable/disable "on new data"-option const auto* model = qobject_cast(ui.cbUpdateType->model()); QStandardItem* item = model->item(LiveDataSource::UpdateType::NewData); switch (sourceType) { case LiveDataSource::SourceType::FileOrPipe: ui.lFileName->show(); ui.leFileName->show(); ui.bFileInfo->show(); ui.bOpen->show(); ui.chbLinkFile->show(); //option for sample size are available for "continuously fixed" and "from end" reading options if (ui.cbReadingType->currentIndex() < 2) { ui.lSampleSize->show(); ui.sbSampleSize->show(); } else { ui.lSampleSize->hide(); ui.sbSampleSize->hide(); } ui.cbBaudRate->hide(); ui.lBaudRate->hide(); ui.lHost->hide(); ui.leHost->hide(); ui.lPort->hide(); ui.lePort->hide(); ui.cbSerialPort->hide(); ui.lSerialPort->hide(); item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); fileNameChanged(ui.leFileName->text()); ui.cbFileType->show(); ui.lFileType->show(); setMQTTVisible(false); break; case LiveDataSource::SourceType::NetworkTcpSocket: case LiveDataSource::SourceType::NetworkUdpSocket: ui.lHost->show(); ui.leHost->show(); ui.lePort->show(); ui.lPort->show(); if (sourceType == LiveDataSource::SourceType::NetworkTcpSocket) { ui.lSampleSize->hide(); ui.sbSampleSize->hide(); } else { ui.lSampleSize->show(); ui.sbSampleSize->show(); } ui.lBaudRate->hide(); ui.cbBaudRate->hide(); ui.lSerialPort->hide(); ui.cbSerialPort->hide(); ui.lFileName->hide(); ui.leFileName->hide(); ui.bFileInfo->hide(); ui.bOpen->hide(); ui.chbLinkFile->hide(); item->setFlags(item->flags() & ~(Qt::ItemIsSelectable | Qt::ItemIsEnabled)); ui.gbOptions->setEnabled(true); ui.bManageFilters->setEnabled(true); ui.cbFilter->setEnabled(true); ui.cbFileType->setEnabled(true); ui.cbFileType->show(); ui.lFileType->show(); setMQTTVisible(false); break; case LiveDataSource::SourceType::LocalSocket: ui.lFileName->show(); ui.leFileName->show(); ui.bOpen->show(); ui.lSampleSize->hide(); ui.sbSampleSize->hide(); ui.bFileInfo->hide(); ui.cbBaudRate->hide(); ui.lBaudRate->hide(); ui.lHost->hide(); ui.leHost->hide(); ui.lPort->hide(); ui.lePort->hide(); ui.cbSerialPort->hide(); ui.lSerialPort->hide(); ui.chbLinkFile->hide(); item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); ui.gbOptions->setEnabled(true); ui.bManageFilters->setEnabled(true); ui.cbFilter->setEnabled(true); ui.cbFileType->setEnabled(true); ui.cbFileType->show(); ui.lFileType->show(); setMQTTVisible(false); break; case LiveDataSource::SourceType::SerialPort: ui.lBaudRate->show(); ui.cbBaudRate->show(); ui.lSerialPort->show(); ui.cbSerialPort->show(); ui.lSampleSize->show(); ui.sbSampleSize->show(); ui.lHost->hide(); ui.leHost->hide(); ui.lePort->hide(); ui.lPort->hide(); ui.lFileName->hide(); ui.leFileName->hide(); ui.bFileInfo->hide(); ui.bOpen->hide(); ui.chbLinkFile->hide(); item->setFlags(item->flags() & ~(Qt::ItemIsSelectable | Qt::ItemIsEnabled)); ui.cbFileType->setEnabled(true); ui.cbFileType->show(); ui.gbOptions->setEnabled(true); ui.bManageFilters->setEnabled(true); ui.cbFilter->setEnabled(true); ui.lFileType->show(); setMQTTVisible(false); break; case LiveDataSource::SourceType::MQTT: #ifdef HAVE_MQTT item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); //for MQTT we read ascii data only, hide the file type options for (int i = 0; i < ui.cbFileType->count(); ++i) { if (static_cast(ui.cbFileType->itemData(i).toInt()) == AbstractFileFilter::Ascii) { if (ui.cbFileType->currentIndex() == i) initOptionsWidget(); else ui.cbFileType->setCurrentIndex(i); break; } } ui.cbFileType->hide(); ui.lFileType->hide(); ui.lBaudRate->hide(); ui.cbBaudRate->hide(); ui.lSerialPort->hide(); ui.cbSerialPort->hide(); ui.lHost->hide(); ui.leHost->hide(); ui.lPort->hide(); ui.lePort->hide(); ui.lFileName->hide(); ui.leFileName->hide(); ui.bFileInfo->hide(); ui.bOpen->hide(); ui.chbLinkFile->hide(); setMQTTVisible(true); ui.cbFileType->setEnabled(true); ui.gbOptions->setEnabled(true); ui.bManageFilters->setEnabled(true); ui.cbFilter->setEnabled(true); //in case there are already connections defined, //show the available topics for the currently selected connection mqttConnectionChanged(); #endif break; } //deactivate/activate options that are specific to file of pipe sources only auto* typeModel = qobject_cast(ui.cbFileType->model()); if (sourceType != LiveDataSource::FileOrPipe) { //deactivate file types other than ascii and binary for (int i = 2; i < ui.cbFileType->count(); ++i) typeModel->item(i)->setFlags(item->flags() & ~(Qt::ItemIsSelectable | Qt::ItemIsEnabled)); //"whole file" read option is available for file or pipe only, disable it typeModel = qobject_cast(ui.cbReadingType->model()); QStandardItem* item = typeModel->item(LiveDataSource::ReadingType::WholeFile); item->setFlags(item->flags() & ~(Qt::ItemIsSelectable | Qt::ItemIsEnabled)); //"update options" groupbox can be deactivated for "file and pipe" if the file is invalid. //Activate the groupbox when switching from "file and pipe" to a different source type. ui.gbUpdateOptions->setEnabled(true); } else { for (int i = 2; i < ui.cbFileType->count(); ++i) typeModel->item(i)->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); //enable "whole file" item for file or pipe typeModel = qobject_cast(ui.cbReadingType->model()); QStandardItem* item = typeModel->item(LiveDataSource::ReadingType::WholeFile); item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); } emit sourceTypeChanged(); refreshPreview(); } #ifdef HAVE_MQTT /*! *\brief called when a different MQTT connection is selected in the connection ComboBox. * connects to the MQTT broker according to the connection settings. */ void ImportFileWidget::mqttConnectionChanged() { if (m_initialisingMQTT) return; if (m_client == nullptr || m_client->state() == QMqttClient::ClientState::Disconnected) { if (ui.cbConnection->currentIndex() == -1) return; WAIT_CURSOR; delete m_client; m_client = new QMqttClient; connect(m_client, &QMqttClient::connected, this, &ImportFileWidget::onMqttConnect); connect(m_client, &QMqttClient::disconnected, this, &ImportFileWidget::onMqttDisconnect); connect(m_client, &QMqttClient::messageReceived, this, &ImportFileWidget::mqttMessageReceived); connect(m_client, &QMqttClient::errorChanged, this, &ImportFileWidget::mqttErrorChanged); //determine the current connection's settings KConfig config(m_configPath, KConfig::SimpleConfig); KConfigGroup group = config.group(ui.cbConnection->currentText()); m_client->setHostname(group.readEntry("Host")); m_client->setPort(group.readEntry("Port").toUInt()); const bool useID = group.readEntry("UseID").toUInt(); if (useID) m_client->setClientId(group.readEntry("ClientID")); const bool useAuthentication = group.readEntry("UseAuthentication").toUInt(); if (useAuthentication) { m_client->setUsername(group.readEntry("UserName")); m_client->setPassword(group.readEntry("Password")); } qDebug()<< "Use ID" << useID << " " << m_client->clientId(); qDebug()<< "Use authentication" << useAuthentication << " " << m_client->username() << " " << m_client->password(); qDebug()<< m_client->hostname() << " " << m_client->port(); qDebug()<< "Trying to connect"; m_connectTimeoutTimer->start(); m_client->connectToHost(); } else if (m_client->state() == QMqttClient::ClientState::Connected) { WAIT_CURSOR; qDebug()<<"Disconnecting from MQTT broker"; m_client->disconnectFromHost(); } } /*! *\brief called when the client connects to the broker successfully, it subscribes to every topic (# wildcard) * in order to later list every available topic */ void ImportFileWidget::onMqttConnect() { if (m_client->error() == QMqttClient::NoError) { m_connectTimeoutTimer->stop(); ui.gbManageSubscriptions->setVisible(true); //subscribing to every topic (# wildcard) in order to later list every available topic QMqttTopicFilter globalFilter{"#"}; m_mainSubscription = m_client->subscribe(globalFilter, 1); if (!m_mainSubscription) QMessageBox::critical(this, i18n("Couldn't subscribe"), i18n("Couldn't subscribe. Something went wrong")); } emit subscriptionsChanged(); RESET_CURSOR; } /*! *\brief called when the client disconnects from the broker successfully * removes every information about the former connection */ void ImportFileWidget::onMqttDisconnect() { DEBUG("Disconected from " << m_client->hostname().toStdString()); m_searchTimer->stop(); m_connectTimeoutTimer->stop(); ui.lTopics->hide(); ui.gbManageSubscriptions->hide(); ui.lLWT->hide(); ui.bLWT->hide(); ui.cbConnection->setItemText(ui.cbConnection->currentIndex(), ui.cbConnection->currentText() + " " + i18n("(Disconnected)")); m_mqttReadyForPreview = false; m_searching = false; delete m_topicCompleter; delete m_subscriptionCompleter; emit subscriptionsChanged(); RESET_CURSOR; QMessageBox::critical(this, i18n("Disconnected"), i18n("Disconnected from the broker '%1' before the connection was successful.", m_client->hostname())); } /*! *\brief When a leaf topic is double clicked in the topics tree widget we subscribe on that */ void ImportFileWidget::mqttAvailableTopicDoubleClicked(QTreeWidgetItem* item, int column) { Q_UNUSED(column) // Only for leaf topics if (item->childCount() == 0) mqttSubscribe(); } /*! *\brief When a leaf subscription is double clicked in the topics tree widget we unsubscribe */ void ImportFileWidget::mqttSubscribedTopicDoubleClicked(QTreeWidgetItem* item, int column) { Q_UNUSED(column) // Only for leaf subscriptions if (item->childCount() == 0) { mqttUnsubscribe(); } } /*! *\brief called when the subscribe button is pressed * subscribes to the topic represented by the current item of twTopics */ void ImportFileWidget::mqttSubscribe() { QTreeWidgetItem* item = ui.twTopics->currentItem(); if (!item) { QMessageBox::warning(this, i18n("Warning"), i18n("You didn't select any item from the Tree Widget")); return; } //determine the topic name that the current item represents QTreeWidgetItem* tempItem = item; QString name = item->text(0); if (item->childCount() != 0) name.append("/#"); while (tempItem->parent()) { tempItem = tempItem->parent(); name.prepend(tempItem->text(0) + '/'); } //check if the subscription already exists const QList& topLevelList = ui.twSubscriptions->findItems(name, Qt::MatchExactly); if (topLevelList.isEmpty() || topLevelList.first()->parent() != nullptr) { qDebug() << "Subscribe to: " << name; bool foundSuperior = false; for (int i = 0; i < ui.twSubscriptions->topLevelItemCount(); ++i) { //if the new subscirptions contains an already existing one, we remove the inferior one if (checkTopicContains(name, ui.twSubscriptions->topLevelItem(i)->text(0)) && name != ui.twSubscriptions->topLevelItem(i)->text(0)) { unsubscribeFromTopic(ui.twSubscriptions->topLevelItem(i)->text(0)); --i; continue; } //if there is a subscription containing the new one we set foundSuperior true if (checkTopicContains(ui.twSubscriptions->topLevelItem(i)->text(0), name) && name != ui.twSubscriptions->topLevelItem(i)->text(0)) { foundSuperior = true; qDebug()<<"Can't continue subscribe. Found superior for " << name <<" : "<< ui.twSubscriptions->topLevelItem(i)->text(0); break; } } //if there wasn't a superior subscription we can subscribe to the new topic if (!foundSuperior) { QStringList toplevelName; toplevelName.push_back(name); QTreeWidgetItem* newTopLevelItem = new QTreeWidgetItem(toplevelName); ui.twSubscriptions->addTopLevelItem(newTopLevelItem); const QMqttTopicFilter filter {name}; QMqttSubscription *tempSubscription = m_client->subscribe(filter, static_cast(ui.cbQos->currentText().toUInt()) ); if (tempSubscription) { m_mqttSubscriptions.push_back(tempSubscription); connect(tempSubscription, &QMqttSubscription::messageReceived, this, &ImportFileWidget::mqttSubscriptionMessageReceived); emit subscriptionsChanged(); } if (name.endsWith('#')) { //adding every topic that the subscription contains to twSubscriptions addSubscriptionChildren(item, newTopLevelItem); //if an already existing subscription contains a topic that the new subscription also contains //we decompose the already existing subscription //by unsubscribing from its topics, that are present in the new subscription as well const QStringList nameList = name.split('/', QString::SkipEmptyParts); const QString& root = nameList.first(); QVector children; for (int i = 0; i < ui.twSubscriptions->topLevelItemCount(); ++i) { if (ui.twSubscriptions->topLevelItem(i)->text(0).startsWith(root) && name != ui.twSubscriptions->topLevelItem(i)->text(0)) { children.clear(); //get the "leaf" children of the inspected subscription findSubscriptionLeafChildren(children, ui.twSubscriptions->topLevelItem(i)); for (int j = 0; j < children.size(); ++j) { if (checkTopicContains(name, children[j]->text(0))) { //if the new subscription contains a topic, we unsubscribe from it ui.twSubscriptions->setCurrentItem(children[j]); mqttUnsubscribe(); --i; } } } } } manageCommonLevelSubscriptions(); updateSubscriptionCompleter(); if (!ui.bLWT->isEnabled()) ui.bLWT->setEnabled(true); } else QMessageBox::warning(this, i18n("Warning"), i18n("You already subscribed to a topic containing this one")); } else QMessageBox::warning(this, i18n("Warning"), i18n("You already subscribed to this topic")); } /*! *\brief called when the unsubscribe button is pressed * unsubscribes from the topic represented by the current item of twSubscription */ void ImportFileWidget::mqttUnsubscribe() { QTreeWidgetItem* unsubscribeItem = ui.twSubscriptions->currentItem(); if (!unsubscribeItem) { QMessageBox::warning(this, i18n("Warning"), i18n("You didn't select any item from the Tree Widget")); return; } qDebug() << "Unsubscribe from: " << unsubscribeItem->text(0); //if it is a top level item, meaning a topic that we really subscribed to(not one that belongs to a subscription) //we can simply unsubscribe from it if (unsubscribeItem->parent() == nullptr) unsubscribeFromTopic(unsubscribeItem->text(0)); //otherwise we remove the selected item, but subscribe to every other topic, that was contained by //the selected item's parent subscription(top level item of twSubscriptions) else { while (unsubscribeItem->parent() != nullptr) { for (int i = 0; i < unsubscribeItem->parent()->childCount(); ++i) { if (unsubscribeItem->text(0) != unsubscribeItem->parent()->child(i)->text(0)) { const QMqttTopicFilter filter {unsubscribeItem->parent()->child(i)->text(0)}; QMqttSubscription *tempSubscription = m_client->subscribe(filter, static_cast(ui.cbQos->currentText().toUInt()) ); ui.twSubscriptions->addTopLevelItem(unsubscribeItem->parent()->takeChild(i)); if (tempSubscription) { m_mqttSubscriptions.push_back(tempSubscription); connect(tempSubscription, &QMqttSubscription::messageReceived, this, &ImportFileWidget::mqttSubscriptionMessageReceived); emit subscriptionsChanged(); } --i; } } unsubscribeItem = unsubscribeItem->parent(); } unsubscribeFromTopic(unsubscribeItem->text(0)); //check if any common topics were subscribed, if possible merge them manageCommonLevelSubscriptions(); } updateSubscriptionCompleter(); if (ui.twSubscriptions->topLevelItemCount() <= 0) ui.bLWT->setEnabled(false); } /*! *\brief called when the client receives a message * if the message arrived from a new topic, the topic is put in twTopics */ void ImportFileWidget::mqttMessageReceived(const QByteArray& message, const QMqttTopicName& topic) { Q_UNUSED(message); qDebug()<<"recieved " << topic.name(); if (m_addedTopics.contains(topic.name())) return; m_addedTopics.push_back(topic.name()); QStringList name; QString rootName; const QChar sep = '/'; if (topic.name().contains(sep)) { const QStringList& list = topic.name().split(sep, QString::SkipEmptyParts); if (!list.isEmpty()) { rootName = list.at(0); name.append(list.at(0)); int topItemIdx = -1; //check whether the first level of the topic can be found in twTopics for (int i = 0; i < ui.twTopics->topLevelItemCount(); ++i) { if (ui.twTopics->topLevelItem(i)->text(0) == list.at(0)) { topItemIdx = i; break; } } //if not we simply add every level of the topic to the tree if (topItemIdx < 0) { QTreeWidgetItem* currentItem = new QTreeWidgetItem(name); ui.twTopics->addTopLevelItem(currentItem); for (int i = 1; i < list.size(); ++i) { name.clear(); name.append(list.at(i)); currentItem->addChild(new QTreeWidgetItem(name)); currentItem = currentItem->child(0); } } //otherwise we search for the first level that isn't part of the tree, //then add every level of the topic to the tree from that certain level else { QTreeWidgetItem* currentItem = ui.twTopics->topLevelItem(topItemIdx); int listIdx = 1; for (; listIdx < list.size(); ++listIdx) { QTreeWidgetItem* childItem = nullptr; bool found = false; for (int j = 0; j < currentItem->childCount(); ++j) { childItem = currentItem->child(j); if (childItem->text(0) == list.at(listIdx)) { found = true; currentItem = childItem; break; } } if (!found) { //this is the level that isn't present in the tree break; } } //add every level to the tree starting with the first level that isn't part of the tree for (; listIdx < list.size(); ++listIdx) { name.clear(); name.append(list.at(listIdx)); currentItem->addChild(new QTreeWidgetItem(name)); currentItem = currentItem->child(currentItem->childCount() - 1); } } } } else { rootName = topic.name(); name.append(topic.name()); ui.twTopics->addTopLevelItem(new QTreeWidgetItem(name)); } //if a subscribed topic contains the new topic, we have to update twSubscriptions for (int i = 0; i < ui.twSubscriptions->topLevelItemCount(); ++i) { const QStringList subscriptionName = ui.twSubscriptions->topLevelItem(i)->text(0).split('/', QString::SkipEmptyParts); if (!subscriptionName.isEmpty()) { if (rootName == subscriptionName.first()) { updateSubscriptionTree(); break; } } } //signals that a newTopic was added, in order to fill the completer of leTopics emit newTopic(rootName); } /*! *\brief called when a new topic is added to the tree(twTopics) * appends the topic's root to the topicList if it isn't in the list already * then sets the completer for leTopics */ void ImportFileWidget::setTopicCompleter(const QString& topic) { if (!m_topicList.contains(topic)) { m_topicList.append(topic); if (!m_searching) { m_topicCompleter = new QCompleter(m_topicList, this); m_topicCompleter->setCompletionMode(QCompleter::PopupCompletion); m_topicCompleter->setCaseSensitivity(Qt::CaseSensitive); ui.leTopics->setCompleter(m_topicCompleter); } } } /*! *\brief called when 10 seconds passed since the last time the user searched for a certain root in twTopics * enables updating the completer for le */ void ImportFileWidget::topicTimeout() { m_searching = false; m_searchTimer->stop(); } /*! *\brief called when the client receives a message from a subscribed topic (that isn't the "#" wildcard) */ void ImportFileWidget::mqttSubscriptionMessageReceived(const QMqttMessage &msg) { qDebug()<<"message received from: "< i(m_messageArrived); while (i.hasNext()) { i.next(); if (i.value() == false ) { check = false; break; } } //if there is a message from every subscribed topic, we refresh the preview if (check) { m_mqttReadyForPreview = true; refreshPreview(); } } /*! *\brief called when use will message is changed in the settings widget * Updates the will settings */ void ImportFileWidget::useWillMessage(int state) { m_willSettings.enabled = (state == Qt::Checked); } /*! *\brief called when the selected will message type is changed in the settings widget * Updates the will settings */ void ImportFileWidget::willMessageTypeChanged(int type) { m_willSettings.willMessageType = static_cast(type); } /*! *\brief called when the selected will message' type's retain flag is changed in the settings widget * Updates the will settings */ void ImportFileWidget::willRetainChanged(bool retain) { m_willSettings.willRetain = retain; } /*! *\brief called when the selected will update interval is changed in the settings widget * Updates the will settings */ void ImportFileWidget::willTimeIntervalChanged(int interval) { m_willSettings.willTimeInterval = interval; } /*! *\brief called when the selected will own message is changed in the settings widget * Updates the will settings */ void ImportFileWidget::willOwnMessageChanged(const QString& msg) { m_willSettings.willOwnMessage = msg; } /*! *\brief called when the selected will topic is changed in the settings widget * Updates the will settings */ void ImportFileWidget::willTopicChanged(const QString& topic) { m_willSettings.willTopic = topic; } /*! *\brief called when the selected will statistics are changed in the settings widget * Updates the will settings */ void ImportFileWidget::willStatisticsChanged(int index) { if (index >= 0) m_willSettings.willStatistics[index] = !m_willSettings.willStatistics[index]; } /*! *\brief called when the selected will message's QoS is changed in the settings widget * Updates the will settings */ void ImportFileWidget::willQoSChanged(int qos) { m_willSettings.willQoS = qos; } /*! *\brief called when the selected will update type is changed in the settings widget * Updates the will settings */ void ImportFileWidget::willUpdateTypeChanged(int updateType) { qDebug() << "update type changed: " <(updateType); } /*! *\brief called when the clientError of the MQTT client changes * * \param clientError the current error of the client */ void ImportFileWidget::mqttErrorChanged(QMqttClient::ClientError clientError) { switch (clientError) { case QMqttClient::BadUsernameOrPassword: QMessageBox::critical(this, i18n("Couldn't connect"), i18n("Wrong username or password")); break; case QMqttClient::IdRejected: QMessageBox::critical(this, i18n("Couldn't connect"), i18n("The client ID wasn't accepted")); break; case QMqttClient::ServerUnavailable: QMessageBox::critical(this, i18n("Server unavailable"), i18n("The broker couldn't be reached.")); break; case QMqttClient::NotAuthorized: QMessageBox::critical(this, i18n("Not authorized"), i18n("The client is not authorized to connect.")); break; case QMqttClient::UnknownError: QMessageBox::critical(this, i18n("Unknown MQTT error"), i18n("An unknown error occurred.")); break; case QMqttClient::NoError: case QMqttClient::InvalidProtocolVersion: case QMqttClient::TransportInvalid: case QMqttClient::ProtocolViolation: break; default: break; } } /*! *\brief called when leTopics' text is changed * if the rootName can be found in twTopics, then we scroll it to the top of the tree widget * * \param rootName the current text of leTopics */ void ImportFileWidget::scrollToTopicTreeItem(const QString& rootName) { m_searching = true; m_searchTimer->start(); int topItemIdx = -1; for (int i = 0; i < ui.twTopics->topLevelItemCount(); ++i) if (ui.twTopics->topLevelItem(i)->text(0) == rootName) { topItemIdx = i; break; } if (topItemIdx >= 0) ui.twTopics->scrollToItem(ui.twTopics->topLevelItem(topItemIdx), QAbstractItemView::ScrollHint::PositionAtTop); } /*! *\brief called when leSubscriptions' text is changed * if the rootName can be found in twSubscriptions, then we scroll it to the top of the tree widget * * \param rootName the current text of leSubscriptions */ void ImportFileWidget::scrollToSubsriptionTreeItem(const QString& rootName) { m_searching = true; m_searchTimer->start(); int topItemIdx = -1; for (int i = 0; i < ui.twSubscriptions->topLevelItemCount(); ++i) if (ui.twSubscriptions->topLevelItem(i)->text(0) == rootName) { topItemIdx = i; break; } if (topItemIdx >= 0) ui.twSubscriptions->scrollToItem(ui.twSubscriptions->topLevelItem(topItemIdx), QAbstractItemView::ScrollHint::PositionAtTop); } /*! *\brief called when m_connectTimeoutTimer ticks, * meaning that the client couldn't connect to the broker in 5 seconds * disconnects the client, stops the timer, and warns the user */ void ImportFileWidget::mqttConnectTimeout() { m_connectionTimedOut = true; m_client->disconnectFromHost(); m_connectTimeoutTimer->stop(); RESET_CURSOR; QMessageBox::warning(this, i18n("Warning"), i18n("Connecting to the given broker timed out! Try changing the settings")); } /*! Shows the MQTT connection manager where the connections are created and edited. The selected connection is selected in the connection combo box in this widget. */ void ImportFileWidget::showMQTTConnectionManager() { bool previousConnectionChanged = false; MQTTConnectionManagerDialog* dlg = new MQTTConnectionManagerDialog(this, ui.cbConnection->currentText(), previousConnectionChanged); if (dlg->exec() == QDialog::Accepted) { //re-read the available connections to be in sync with the changes in MQTTConnectionManager m_initialisingMQTT = true; const QString& prevConn = ui.cbConnection->currentText(); ui.cbConnection->clear(); readMQTTConnections(); m_initialisingMQTT = false; //select the connection the user has selected in MQTTConnectionManager const QString& conn = dlg->connection(); int index = ui.cbConnection->findText(conn); if (conn != prevConn) {//Current connection isn't the previous one if (ui.cbConnection->currentIndex() != index) ui.cbConnection->setCurrentIndex(index); else mqttConnectionChanged(); } else if (dlg->initialConnectionChanged()) {//Current connection is the same with previous one but it changed if (ui.cbConnection->currentIndex() == index) mqttConnectionChanged(); else ui.cbConnection->setCurrentIndex(index); } else { //Previous connection wasn't changed m_initialisingMQTT = true; ui.cbConnection->setCurrentIndex(index); m_initialisingMQTT = false; } } delete dlg; } /*! loads all available saved MQTT nconnections */ void ImportFileWidget::readMQTTConnections() { qDebug()<< "ImportFileWidget: reading available MQTT connections"; KConfig config(m_configPath, KConfig::SimpleConfig); for (const auto& name : config.groupList()) ui.cbConnection->addItem(name); } /*! * \brief Shows the mqtt will settings widget, which allows the user to modify the will settings */ void ImportFileWidget::showWillSettings() { QMenu menu; QVector children; for (int i = 0; i < ui.twSubscriptions->topLevelItemCount(); ++i) findSubscriptionLeafChildren(children, ui.twSubscriptions->topLevelItem(i)); QVector topics; for (int i = 0; i < children.size(); ++i) topics.append(children[i]->text(0)); MQTTWillSettingsWidget willSettingsWidget(&menu, m_willSettings, topics); connect(&willSettingsWidget, &MQTTWillSettingsWidget::applyClicked, [this, &menu, &willSettingsWidget]() { this->useWillMessage(willSettingsWidget.will().enabled); this->willMessageTypeChanged(willSettingsWidget.will().willMessageType); this->updateTypeChanged(willSettingsWidget.will().willUpdateType); this->willRetainChanged(willSettingsWidget.will().willRetain); this->willTimeIntervalChanged(willSettingsWidget.will().willTimeInterval); this->willOwnMessageChanged(willSettingsWidget.will().willOwnMessage); this->willTopicChanged(willSettingsWidget.will().willTopic); this->willStatisticsChanged(willSettingsWidget.statisticsType()); menu.close(); }); QWidgetAction* widgetAction = new QWidgetAction(this); widgetAction->setDefaultWidget(&willSettingsWidget); menu.addAction(widgetAction); const QPoint pos(ui.bLWT->sizeHint().width(),ui.bLWT->sizeHint().height()); menu.exec(ui.bLWT->mapToGlobal(pos)); } #endif