// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qabstract3daxis_p.h" #include "qsurface3dseries_p.h" #include "qsurfacedataproxy_p.h" QT_BEGIN_NAMESPACE /*! * \class QSurfaceDataProxy * \inmodule QtGraphs * \ingroup graphs_3D * \brief The QSurfaceDataProxy class is the data proxy for a 3D surface graph. * * A surface data proxy handles surface related data in rows. For this it * provides two auxiliary type aliases: QtGraphs::QSurfaceDataArray and * QtGraphs::QSurfaceDataRow. \c QSurfaceDataArray is a QList that * controls the rows. \c QSurfaceDataRow is a QList that contains * QSurfaceDataItem objects. For more information about how to feed the data to * the proxy, see the sample code in the Q3DSurfaceWidgetItem documentation. Since data is * stored in series, it is necessary to create a series associated with the * proxy before using these functions for the dataset. * * All rows must have the same number of items. * * QSurfaceDataProxy takes ownership of all \c QSurfaceDataRow objects passed to * it, whether directly or in a \c QSurfaceDataArray container. * To use surface data row pointers to directly modify data after adding the * array to the proxy, the appropriate signal must be emitted to update the * graph. * * To make a sensible surface, the x-value of each successive item in all rows * must be either ascending or descending throughout the row. Similarly, the * z-value of each successive item in all columns must be either ascending or * descending throughout the column. * * \note Currently only surfaces with straight rows and columns are fully * supported. Any row with items that do not have the exact same z-value or any * column with items that do not have the exact same x-value may get clipped * incorrectly if the whole surface does not completely fit within the visible * x-axis or z-axis ranges. * * \note Surfaces with less than two rows or columns are not considered valid * surfaces and will not be rendered. * * \note On some environments, surfaces with a lot of visible vertices may not * render, because they exceed the per-draw vertex count supported by the * graphics driver. This is mostly an issue on 32-bit and OpenGL ES2 platforms. * * \sa {Qt Graphs Data Handling with 3D} */ /*! * \typealias QSurfaceDataRow * \relates QSurfaceDataProxy * * A list of \l {QSurfaceDataItem} objects. */ /*! * \typealias QSurfaceDataArray * \relates QSurfaceDataProxy * * A list of pointers to \l {QSurfaceDataRow} objects. */ /*! * \qmltype SurfaceDataProxy * \inqmlmodule QtGraphs * \ingroup graphs_qml_3D * \nativetype QSurfaceDataProxy * \inherits AbstractDataProxy * \brief The data proxy for a 3D surface graph. * * This type handles surface data items. The data is arranged into rows and * columns, and all rows must have the same number of columns. * * This type is uncreatable, but contains properties that are exposed via * subtypes. * * For a more complete description, see QSurfaceDataProxy. * * \sa ItemModelSurfaceDataProxy, {Qt Graphs Data Handling with 3D} */ /*! * \qmlproperty int SurfaceDataProxy::rowCount * The number of rows in the data array. */ /*! * \qmlproperty int SurfaceDataProxy::columnCount * The number of columns in the data array. */ /*! * \qmlproperty Surface3DSeries SurfaceDataProxy::series * * The series this proxy is attached to. */ /*! \qmlsignal SurfaceDataProxy::rowCountChanged(int count) This signal is emitted when rowCount changes to \a count. */ /*! \qmlsignal SurfaceDataProxy::columnCountChanged(int count) This signal is emitted when columnCount changes to \a count. */ /*! \qmlsignal SurfaceDataProxy::seriesChanged(Surface3DSeries series) This signal is emitted when \l series changes to \a series. */ /*! * Constructs QSurfaceDataProxy with the given \a parent. */ QSurfaceDataProxy::QSurfaceDataProxy(QObject *parent) : QAbstractDataProxy(*(new QSurfaceDataProxyPrivate()), parent) {} /*! * \internal */ QSurfaceDataProxy::QSurfaceDataProxy(QSurfaceDataProxyPrivate &d, QObject *parent) : QAbstractDataProxy(d, parent) {} /*! * Deletes the surface data proxy. */ QSurfaceDataProxy::~QSurfaceDataProxy() {} /*! * \property QSurfaceDataProxy::series * * \brief The series this proxy is attached to. */ QSurface3DSeries *QSurfaceDataProxy::series() const { Q_D(const QSurfaceDataProxy); if (!d->series()) qWarning("Series needs to be created to access data members"); return static_cast(d->series()); } /*! * Clears the existing array and triggers the arrayReset() signal. */ void QSurfaceDataProxy::resetArray() { Q_D(QSurfaceDataProxy); d->resetArray(QSurfaceDataArray()); emit arrayReset(); emit rowCountChanged(rowCount()); emit columnCountChanged(columnCount()); } /*! * Sets the array from \a newArray. If the new array is equal to the * existing one, this function simply triggers the arrayReset() signal. */ void QSurfaceDataProxy::resetArray(QSurfaceDataArray newArray) { Q_D(QSurfaceDataProxy); if (!series()) return; if (series()->dataArray().data() != newArray.data()) d->resetArray(std::move(newArray)); emit arrayReset(); emit rowCountChanged(rowCount()); emit columnCountChanged(columnCount()); } /*! * Changes an existing row by replacing the row at the position \a rowIndex * with the new row specified by \a row. The new row can be the same as the * existing row already stored at the \a rowIndex. The new row must have * the same number of columns as the row it is replacing. */ void QSurfaceDataProxy::setRow(qsizetype rowIndex, QSurfaceDataRow row) { Q_D(QSurfaceDataProxy); d->setRow(rowIndex, std::move(row)); emit rowsChanged(rowIndex, 1); } /*! * Changes existing rows by replacing the rows starting at the position * \a rowIndex with the new rows specifies by \a rows. * The rows in the \a rows array can be the same as the existing rows already * stored at the \a rowIndex. The new rows must have the same number of columns * as the rows they are replacing. */ void QSurfaceDataProxy::setRows(qsizetype rowIndex, QSurfaceDataArray rows) { Q_D(QSurfaceDataProxy); d->setRows(rowIndex, std::move(rows)); emit rowsChanged(rowIndex, rows.size()); } /*! * Changes a single item at the position specified by \a rowIndex and * \a columnIndex to the item \a item. */ void QSurfaceDataProxy::setItem(qsizetype rowIndex, qsizetype columnIndex, QSurfaceDataItem item) { Q_D(QSurfaceDataProxy); d->setItem(rowIndex, columnIndex, std::move(item)); emit itemChanged(rowIndex, columnIndex); } /*! * Changes a single item at the position \a position to the item \a item. * The x-value of \a position indicates the row and the y-value indicates the * column. */ void QSurfaceDataProxy::setItem(QPoint position, QSurfaceDataItem item) { setItem(position.x(), position.y(), item); } /*! * Adds the new row \a row to the end of an array. The new row must have * the same number of columns as the rows in the initial array. * * Returns the index of the added row. */ qsizetype QSurfaceDataProxy::addRow(QSurfaceDataRow row) { Q_D(QSurfaceDataProxy); qsizetype addIndex = d->addRow(std::move(row)); emit rowsAdded(addIndex, 1); emit rowCountChanged(rowCount()); return addIndex; } /*! * Adds new \a rows to the end of an array. The new rows must have the same * number of columns as the rows in the initial array. * * Returns the index of the first added row. */ qsizetype QSurfaceDataProxy::addRows(QSurfaceDataArray rows) { Q_D(QSurfaceDataProxy); qsizetype addIndex = d->addRows(std::move(rows)); emit rowsAdded(addIndex, rows.size()); emit rowCountChanged(rowCount()); return addIndex; } /*! * Inserts the new row \a row into \a rowIndex. * If \a rowIndex is equal to the array size, the rows are added to the end of * the array. The new row must have the same number of columns as the rows in * the initial array. */ void QSurfaceDataProxy::insertRow(qsizetype rowIndex, QSurfaceDataRow row) { Q_D(QSurfaceDataProxy); d->insertRow(rowIndex, std::move(row)); emit rowsInserted(rowIndex, 1); emit rowCountChanged(rowCount()); } /*! * Inserts new \a rows into \a rowIndex. * If \a rowIndex is equal to the array size, the rows are added to the end of * the array. The new \a rows must have the same number of columns as the rows * in the initial array. */ void QSurfaceDataProxy::insertRows(qsizetype rowIndex, QSurfaceDataArray rows) { Q_D(QSurfaceDataProxy); d->insertRows(rowIndex, std::move(rows)); emit rowsInserted(rowIndex, rows.size()); emit rowCountChanged(rowCount()); } /*! * Removes the number of rows specified by \a removeCount starting at the * position \a rowIndex. Attempting to remove rows past the end of the * array does nothing. */ void QSurfaceDataProxy::removeRows(qsizetype rowIndex, qsizetype removeCount) { Q_D(QSurfaceDataProxy); if (rowIndex < rowCount() && removeCount >= 1) { d->removeRows(rowIndex, removeCount); emit rowsRemoved(rowIndex, removeCount); emit rowCountChanged(rowCount()); } } /*! * Returns the pointer to the item at the position specified by \a rowIndex and * \a columnIndex. It is guaranteed to be valid only * until the next call that modifies data. */ const QSurfaceDataItem &QSurfaceDataProxy::itemAt(qsizetype rowIndex, qsizetype columnIndex) const { const QSurfaceDataArray &dataArray = series()->dataArray(); Q_ASSERT(rowIndex >= 0 && rowIndex < dataArray.size()); const QSurfaceDataRow &dataRow = dataArray[rowIndex]; Q_ASSERT(columnIndex >= 0 && columnIndex < dataRow.size()); return dataRow.at(columnIndex); } /*! * Returns the pointer to the item at the position \a position. The x-value of * \a position indicates the row and the y-value indicates the column. The item * is guaranteed to be valid only until the next call that modifies data. */ const QSurfaceDataItem &QSurfaceDataProxy::itemAt(QPoint position) const { return itemAt(position.x(), position.y()); } /*! * \property QSurfaceDataProxy::rowCount * * \brief The number of rows in the data array. */ qsizetype QSurfaceDataProxy::rowCount() const { if (series()) return series()->dataArray().size(); else return 0; } /*! * \property QSurfaceDataProxy::columnCount * * \brief The number of columns in the data array. */ qsizetype QSurfaceDataProxy::columnCount() const { if (series() && series()->dataArray().size() > 0) return series()->dataArray().at(0).size(); else return 0; } /*! * \fn void QSurfaceDataProxy::arrayReset() * * This signal is emitted when the data array is reset. * If the contents of the whole array are changed without calling resetArray(), * this signal needs to be emitted to update the graph. */ /*! * \fn void QSurfaceDataProxy::rowsAdded(qsizetype startIndex, qsizetype count) * * This signal is emitted when the number of rows specified by \a count is * added, starting at the position \a startIndex. * If rows are added to the array without calling addRow() or addRows(), * this signal needs to be emitted to update the graph. */ /*! * \fn void QSurfaceDataProxy::rowsChanged(qsizetype startIndex, qsizetype count) * * This signal is emitted when the number of rows specified by \a count is * changed, starting at the position \a startIndex. * If rows are changed in the array without calling setRow() or setRows(), * this signal needs to be emitted to update the graph. */ /*! * \fn void QSurfaceDataProxy::rowsRemoved(qsizetype startIndex, qsizetype count) * * This signal is emitted when the number of rows specified by \a count is * removed, starting at the position \a startIndex. * * The index is the current array size if the rows were removed from the end of * the array. If rows are removed from the array without calling removeRows(), * this signal needs to be emitted to update the graph. */ /*! * \fn void QSurfaceDataProxy::rowsInserted(qsizetype startIndex, qsizetype count) * * This signal is emitted when the number of rows specified by \a count is * inserted at the position \a startIndex. * * If rows are inserted into the array without calling insertRow() or * insertRows(), this signal needs to be emitted to update the graph. */ /*! * \fn void QSurfaceDataProxy::itemChanged(qsizetype rowIndex, qsizetype columnIndex) * * This signal is emitted when the item at the position specified by \a rowIndex * and \a columnIndex changes. * If the item is changed in the array without calling setItem(), * this signal needs to be emitted to update the graph. */ // QSurfaceDataProxyPrivate QSurfaceDataProxyPrivate::QSurfaceDataProxyPrivate() : QAbstractDataProxyPrivate(QAbstractDataProxy::DataType::Surface) {} QSurfaceDataProxyPrivate::~QSurfaceDataProxyPrivate() {} void QSurfaceDataProxyPrivate::resetArray(QSurfaceDataArray &&newArray) { auto *surfaceSeries = static_cast(series()); if (newArray.data() != surfaceSeries->dataArray().data()) { surfaceSeries->clearArray(); surfaceSeries->setDataArray(newArray); } } void QSurfaceDataProxyPrivate::setRow(qsizetype rowIndex, QSurfaceDataRow &&row) { auto *surfaceSeries = static_cast(series()); Q_ASSERT(rowIndex >= 0 && rowIndex < surfaceSeries->dataArray().size()); Q_ASSERT(surfaceSeries->dataArray().at(rowIndex).size() == row.size()); if (row.data() != surfaceSeries->dataArray().at(rowIndex).data()) { surfaceSeries->clearRow(rowIndex); QSurfaceDataArray array = surfaceSeries->dataArray(); array[rowIndex] = row; surfaceSeries->setDataArray(array); } } void QSurfaceDataProxyPrivate::setRows(qsizetype rowIndex, QSurfaceDataArray &&rows) { auto *surfaceSeries = static_cast(series()); QSurfaceDataArray array = surfaceSeries->dataArray(); Q_ASSERT(rowIndex >= 0 && (rowIndex + rows.size()) <= array.size()); for (int i = 0; i < rows.size(); i++) { Q_ASSERT(surfaceSeries->dataArray().at(rowIndex).size() == rows.at(i).size()); if (rows.at(i).data() != array.at(rowIndex).data()) { surfaceSeries->clearRow(rowIndex); array[rowIndex] = rows.at(i); } rowIndex++; } surfaceSeries->setDataArray(array); } void QSurfaceDataProxyPrivate::setItem(qsizetype rowIndex, qsizetype columnIndex, QSurfaceDataItem &&item) { auto *surfaceSeries = static_cast(series()); Q_ASSERT(rowIndex >= 0 && rowIndex < surfaceSeries->dataArray().size()); QSurfaceDataArray array = surfaceSeries->dataArray(); QSurfaceDataRow &row = array[rowIndex]; Q_ASSERT(columnIndex < row.size()); row[columnIndex] = item; surfaceSeries->setDataArray(array); } qsizetype QSurfaceDataProxyPrivate::addRow(QSurfaceDataRow &&row) { auto *surfaceSeries = static_cast(series()); Q_ASSERT(surfaceSeries->dataArray().isEmpty() || surfaceSeries->dataArray().at(0).size() == row.size()); qsizetype currentSize = surfaceSeries->dataArray().size(); QSurfaceDataArray array = surfaceSeries->dataArray(); array.append(row); surfaceSeries->setDataArray(array); return currentSize; } qsizetype QSurfaceDataProxyPrivate::addRows(QSurfaceDataArray &&rows) { auto *surfaceSeries = static_cast(series()); qsizetype currentSize = surfaceSeries->dataArray().size(); QSurfaceDataArray array = surfaceSeries->dataArray(); for (int i = 0; i < rows.size(); i++) { Q_ASSERT(surfaceSeries->dataArray().isEmpty() || surfaceSeries->dataArray().at(0).size() == rows.at(i).size()); array.append(rows.at(i)); } surfaceSeries->setDataArray(array); return currentSize; } void QSurfaceDataProxyPrivate::insertRow(qsizetype rowIndex, QSurfaceDataRow &&row) { auto *surfaceSeries = static_cast(series()); Q_ASSERT(rowIndex >= 0 && rowIndex <= surfaceSeries->dataArray().size()); Q_ASSERT(surfaceSeries->dataArray().isEmpty() || surfaceSeries->dataArray().at(0).size() == row.size()); QSurfaceDataArray array = surfaceSeries->dataArray(); array.insert(rowIndex, row); surfaceSeries->setDataArray(array); } void QSurfaceDataProxyPrivate::insertRows(qsizetype rowIndex, QSurfaceDataArray &&rows) { auto *surfaceSeries = static_cast(series()); Q_ASSERT(rowIndex >= 0 && rowIndex <= surfaceSeries->dataArray().size()); QSurfaceDataArray array = surfaceSeries->dataArray(); for (int i = 0; i < rows.size(); i++) { Q_ASSERT(surfaceSeries->dataArray().isEmpty() || surfaceSeries->dataArray().at(0).size() == rows.at(i).size()); array.insert(rowIndex++, rows.at(i)); } surfaceSeries->setDataArray(array); } void QSurfaceDataProxyPrivate::removeRows(qsizetype rowIndex, qsizetype removeCount) { auto *surfaceSeries = static_cast(series()); Q_ASSERT(rowIndex >= 0); qsizetype maxRemoveCount = surfaceSeries->dataArray().size() - rowIndex; removeCount = qMin(removeCount, maxRemoveCount); QSurfaceDataArray array = surfaceSeries->dataArray(); for (int i = 0; i < removeCount; i++) { surfaceSeries->clearRow(rowIndex); array.removeAt(rowIndex); } surfaceSeries->setDataArray(array); } void QSurfaceDataProxyPrivate::limitValues(QVector3D &minValues, QVector3D &maxValues, QAbstract3DAxis *axisX, QAbstract3DAxis *axisY, QAbstract3DAxis *axisZ) const { float min = 0.0f; float max = 0.0f; auto *surfaceSeries = static_cast(series()); qsizetype rows = surfaceSeries->dataArray().size(); qsizetype columns = 0; if (rows) columns = surfaceSeries->dataArray().at(0).size(); if (rows && columns) { min = surfaceSeries->dataArray().at(0).at(0).y(); max = surfaceSeries->dataArray().at(0).at(0).y(); } for (int i = 0; i < rows; i++) { const QSurfaceDataRow &row = surfaceSeries->dataArray().at(i); if (!row.isEmpty()) { for (int j = 0; j < columns; j++) { float itemValue = surfaceSeries->dataArray().at(i).at(j).y(); if (qIsNaN(itemValue) || qIsInf(itemValue)) continue; if ((min > itemValue || (qIsNaN(min) || qIsInf(min))) && isValidValue(itemValue, axisY)) { min = itemValue; } if (max < itemValue || (qIsNaN(max) || qIsInf(max))) max = itemValue; } } } minValues.setY(min); maxValues.setY(max); if (columns) { // Have some defaults float xLow = surfaceSeries->dataArray().at(0).at(0).x(); float xHigh = surfaceSeries->dataArray().at(0).last().x(); float zLow = surfaceSeries->dataArray().at(0).at(0).z(); float zHigh = surfaceSeries->dataArray().last().at(0).z(); for (int i = 0; i < rows; i++) { for (int j = 0; j < columns; j++) { float zItemValue = surfaceSeries->dataArray().at(i).at(j).z(); if (qIsNaN(zItemValue) || qIsInf(zItemValue)) continue; else if (isValidValue(zItemValue, axisZ)) zLow = qMin(zLow, zItemValue); } if (!qIsNaN(zLow) && !qIsInf(zLow)) break; } for (qsizetype i = rows - 1; i >= 0; i--) { for (qsizetype j = 0; j < columns; j++) { float zItemValue = surfaceSeries->dataArray().at(i).at(j).z(); if (qIsNaN(zItemValue) || qIsInf(zItemValue)) continue; else if (isValidValue(zItemValue, axisZ)) { if (!qIsNaN(zHigh) && !qIsInf(zHigh)) zHigh = qMax(zHigh, zItemValue); else zHigh = zItemValue; } } if (!qIsNaN(zHigh) && !qIsInf(zHigh)) break; } for (qsizetype j = 0; j < columns; j++) { for (qsizetype i = 0; i < rows; i++) { float xItemValue = surfaceSeries->dataArray().at(i).at(j).x(); if (qIsNaN(xItemValue) || qIsInf(xItemValue)) continue; else if (isValidValue(xItemValue, axisX)) xLow = qMin(xLow, xItemValue); } if (!qIsNaN(xLow) && !qIsInf(xLow)) break; } for (qsizetype j = columns - 1; j >= 0; j--) { for (qsizetype i = 0; i < rows; i++) { float xItemValue = surfaceSeries->dataArray().at(i).at(j).x(); if (qIsNaN(xItemValue) || qIsInf(xItemValue)) { continue; } else if (isValidValue(xItemValue, axisX)) { if (!qIsNaN(xHigh) && !qIsInf(xHigh)) xHigh = qMax(xHigh, xItemValue); else xHigh = xItemValue; } } if (!qIsNaN(xHigh) && !qIsInf(xHigh)) break; } minValues.setX(xLow); minValues.setZ(zLow); maxValues.setX(xHigh); maxValues.setZ(zHigh); } else { minValues.setX(axisX->d_func()->allowZero() ? 0.0f : 1.0f); minValues.setZ(axisZ->d_func()->allowZero() ? 0.0f : 1.0f); maxValues.setX(axisX->d_func()->allowZero() ? 0.0f : 1.0f); maxValues.setZ(axisZ->d_func()->allowZero() ? 0.0f : 1.0f); } } bool QSurfaceDataProxyPrivate::isValidValue(float value, QAbstract3DAxis *axis) const { return (value > 0.0f || (value == 0.0f && axis->d_func()->allowZero()) || (value < 0.0f && axis->d_func()->allowNegatives())); } void QSurfaceDataProxyPrivate::setSeries(QAbstract3DSeries *series) { Q_Q(QSurfaceDataProxy); QAbstractDataProxyPrivate::setSeries(series); QSurface3DSeries *surfaceSeries = static_cast(series); emit q->seriesChanged(surfaceSeries); } QT_END_NAMESPACE