/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the Qt Data Visualization module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:GPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 or (at your option) any later version ** approved by the KDE Free Qt Foundation. The licenses are as published by ** the Free Software Foundation and appearing in the file LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qheightmapsurfacedataproxy_p.h" QT_BEGIN_NAMESPACE_DATAVISUALIZATION // Default ranges correspond value axis defaults const float defaultMinValue = 0.0f; const float defaultMaxValue = 10.0f; /*! * \class QHeightMapSurfaceDataProxy * \inmodule QtDataVisualization * \brief Base proxy class for Q3DSurface. * \since QtDataVisualization 1.0 * * QHeightMapSurfaceDataProxy takes care of surface related height map data handling. It provides a * way to give a height map to be visualized as a surface plot. * * Since height maps do not contain values for X or Z axes, those values need to be given * separately using minXValue, maxXValue, minZValue, and maxZValue properties. X-value corresponds * to image horizontal direction and Z-value to the vertical. Setting any of these * properties triggers asynchronous re-resolving of any existing height map. * * \sa QSurfaceDataProxy, {Qt Data Visualization Data Handling} */ /*! * \qmltype HeightMapSurfaceDataProxy * \inqmlmodule QtDataVisualization * \since QtDataVisualization 1.0 * \ingroup datavisualization_qml * \instantiates QHeightMapSurfaceDataProxy * \inherits SurfaceDataProxy * \brief Base proxy type for Surface3D. * * HeightMapSurfaceDataProxy takes care of surface related height map data handling. It provides a * way to give a height map to be visualized as a surface plot. * * For more complete description, see QHeightMapSurfaceDataProxy. * * \sa {Qt Data Visualization Data Handling} */ /*! * \qmlproperty string HeightMapSurfaceDataProxy::heightMapFile * * A file with a height map image to be visualized. Setting this property replaces current data * with height map data. * * There are several formats the image file can be given in, but if it is not in a directly usable * format, a conversion is made. \note If the result seems wrong, the automatic conversion failed * and you should try converting the image yourself before setting it. Preferred format is * QImage::Format_RGB32 in grayscale. * * The height of the image is read from the red component of the pixels if the image is in grayscale, * otherwise it is an average calculated from red, green and blue components of the pixels. Using * grayscale images may improve data conversion speed for large images. * * Since height maps do not contain values for X or Z axes, those values need to be given * separately using minXValue, maxXValue, minZValue, and maxZValue properties. X-value corresponds * to image horizontal direction and Z-value to the vertical. Setting any of these * properties triggers asynchronous re-resolving of any existing height map. * * Not recommended formats: all mono formats (for example QImage::Format_Mono). */ /*! * \qmlproperty real HeightMapSurfaceDataProxy::minXValue * * The minimum X value for the generated surface points. Defaults to \c{0.0}. * When setting this property the corresponding maximum value is adjusted if necessary, * to ensure that the range remains valid. */ /*! * \qmlproperty real HeightMapSurfaceDataProxy::maxXValue * * The maximum X value for the generated surface points. Defaults to \c{10.0}. * When setting this property the corresponding minimum value is adjusted if necessary, * to ensure that the range remains valid. */ /*! * \qmlproperty real HeightMapSurfaceDataProxy::minZValue * * The minimum Z value for the generated surface points. Defaults to \c{0.0}. * When setting this property the corresponding maximum value is adjusted if necessary, * to ensure that the range remains valid. */ /*! * \qmlproperty real HeightMapSurfaceDataProxy::maxZValue * * The maximum Z value for the generated surface points. Defaults to \c{10.0}. * When setting this property the corresponding minimum value is adjusted if necessary, * to ensure that the range remains valid. */ /*! * Constructs QHeightMapSurfaceDataProxy with the given \a parent. */ QHeightMapSurfaceDataProxy::QHeightMapSurfaceDataProxy(QObject *parent) : QSurfaceDataProxy(new QHeightMapSurfaceDataProxyPrivate(this), parent) { } /*! * Constructs QHeightMapSurfaceDataProxy with the given \a image and \a parent. Height map is set * by calling setHeightMap() with \a image. * * \sa heightMap */ QHeightMapSurfaceDataProxy::QHeightMapSurfaceDataProxy(const QImage &image, QObject *parent) : QSurfaceDataProxy(new QHeightMapSurfaceDataProxyPrivate(this), parent) { setHeightMap(image); } /*! * Constructs QHeightMapSurfaceDataProxy from the given image \a filename and \a parent. Height map is set * by calling setHeightMapFile() with \a filename. * * \sa heightMapFile */ QHeightMapSurfaceDataProxy::QHeightMapSurfaceDataProxy(const QString &filename, QObject *parent) : QSurfaceDataProxy(new QHeightMapSurfaceDataProxyPrivate(this), parent) { setHeightMapFile(filename); } /*! * \internal */ QHeightMapSurfaceDataProxy::QHeightMapSurfaceDataProxy( QHeightMapSurfaceDataProxyPrivate *d, QObject *parent) : QSurfaceDataProxy(d, parent) { } /*! * Destroys QHeightMapSurfaceDataProxy. */ QHeightMapSurfaceDataProxy::~QHeightMapSurfaceDataProxy() { } /*! * \property QHeightMapSurfaceDataProxy::heightMap * * A height map \a image to be visualized. Setting this property replaces current data with height * map data. * * There are several formats the \a image can be given in, but if it is not in a directly usable * format, a conversion is made. \note If the result seems wrong, the automatic conversion failed * and you should try converting the \a image yourself before setting it. Preferred format is * QImage::Format_RGB32 in grayscale. * * The height of the \a image is read from the red component of the pixels if the \a image is in * grayscale, otherwise it is an average calculated from red, green, and blue components of the * pixels. Using grayscale images may improve data conversion speed for large images. * * Not recommended formats: all mono formats (for example QImage::Format_Mono). * * The height map is resolved asynchronously. QSurfaceDataProxy::arrayReset() is emitted when the * data has been resolved. */ void QHeightMapSurfaceDataProxy::setHeightMap(const QImage &image) { dptr()->m_heightMap = image; // We do resolving asynchronously to make qml onArrayReset handlers actually get the initial reset if (!dptr()->m_resolveTimer.isActive()) dptr()->m_resolveTimer.start(0); } QImage QHeightMapSurfaceDataProxy::heightMap() const { return dptrc()->m_heightMap; } /*! * \property QHeightMapSurfaceDataProxy::heightMapFile * * A file with a height map image to be visualized. Setting this property replaces current data * with height map data. * * \sa heightMap */ void QHeightMapSurfaceDataProxy::setHeightMapFile(const QString &filename) { dptr()->m_heightMapFile = filename; setHeightMap(QImage(filename)); emit heightMapFileChanged(filename); } QString QHeightMapSurfaceDataProxy::heightMapFile() const { return dptrc()->m_heightMapFile; } /*! * A convenience function for setting all minimum (\a minX and \a minZ) and maximum * (\a maxX and \a maxZ) values at the same time. The minimum values must be smaller than the * corresponding maximum value. Otherwise the values get adjusted so that they are valid. */ void QHeightMapSurfaceDataProxy::setValueRanges(float minX, float maxX, float minZ, float maxZ) { dptr()->setValueRanges(minX, maxX, minZ, maxZ); } /*! * \property QHeightMapSurfaceDataProxy::minXValue * * The minimum X value for the generated surface points. Defaults to \c{0.0}. * When setting this property the corresponding maximum value is adjusted if necessary, * to ensure that the range remains valid. */ void QHeightMapSurfaceDataProxy::setMinXValue(float min) { dptr()->setMinXValue(min); } float QHeightMapSurfaceDataProxy::minXValue() const { return dptrc()->m_minXValue; } /*! * \property QHeightMapSurfaceDataProxy::maxXValue * * The maximum X value for the generated surface points. Defaults to \c{10.0}. * When setting this property the corresponding minimum value is adjusted if necessary, * to ensure that the range remains valid. */ void QHeightMapSurfaceDataProxy::setMaxXValue(float max) { dptr()->setMaxXValue(max); } float QHeightMapSurfaceDataProxy::maxXValue() const { return dptrc()->m_maxXValue; } /*! * \property QHeightMapSurfaceDataProxy::minZValue * * The minimum Z value for the generated surface points. Defaults to \c{0.0}. * When setting this property the corresponding maximum value is adjusted if necessary, * to ensure that the range remains valid. */ void QHeightMapSurfaceDataProxy::setMinZValue(float min) { dptr()->setMinZValue(min); } float QHeightMapSurfaceDataProxy::minZValue() const { return dptrc()->m_minZValue; } /*! * \property QHeightMapSurfaceDataProxy::maxZValue * * The maximum Z value for the generated surface points. Defaults to \c{10.0}. * When setting this property the corresponding minimum value is adjusted if necessary, * to ensure that the range remains valid. */ void QHeightMapSurfaceDataProxy::setMaxZValue(float max) { dptr()->setMaxZValue(max); } float QHeightMapSurfaceDataProxy::maxZValue() const { return dptrc()->m_maxZValue; } /*! * \internal */ QHeightMapSurfaceDataProxyPrivate *QHeightMapSurfaceDataProxy::dptr() { return static_cast(d_ptr.data()); } /*! * \internal */ const QHeightMapSurfaceDataProxyPrivate *QHeightMapSurfaceDataProxy::dptrc() const { return static_cast(d_ptr.data()); } // QHeightMapSurfaceDataProxyPrivate QHeightMapSurfaceDataProxyPrivate::QHeightMapSurfaceDataProxyPrivate(QHeightMapSurfaceDataProxy *q) : QSurfaceDataProxyPrivate(q), m_minXValue(defaultMinValue), m_maxXValue(defaultMaxValue), m_minZValue(defaultMinValue), m_maxZValue(defaultMaxValue) { m_resolveTimer.setSingleShot(true); QObject::connect(&m_resolveTimer, &QTimer::timeout, this, &QHeightMapSurfaceDataProxyPrivate::handlePendingResolve); } QHeightMapSurfaceDataProxyPrivate::~QHeightMapSurfaceDataProxyPrivate() { } QHeightMapSurfaceDataProxy *QHeightMapSurfaceDataProxyPrivate::qptr() { return static_cast(q_ptr); } void QHeightMapSurfaceDataProxyPrivate::setValueRanges(float minX, float maxX, float minZ, float maxZ) { bool minXChanged = false; bool maxXChanged = false; bool minZChanged = false; bool maxZChanged = false; if (m_minXValue != minX) { m_minXValue = minX; minXChanged = true; } if (m_minZValue != minZ) { m_minZValue = minZ; minZChanged = true; } if (m_maxXValue != maxX || minX >= maxX) { if (minX >= maxX) { m_maxXValue = minX + 1.0f; qWarning() << "Warning: Tried to set invalid range for X value range." " Range automatically adjusted to a valid one:" << minX << "-" << maxX << "-->" << m_minXValue << "-" << m_maxXValue; } else { m_maxXValue = maxX; } maxXChanged = true; } if (m_maxZValue != maxZ || minZ >= maxZ) { if (minZ >= maxZ) { m_maxZValue = minZ + 1.0f; qWarning() << "Warning: Tried to set invalid range for Z value range." " Range automatically adjusted to a valid one:" << minZ << "-" << maxZ << "-->" << m_minZValue << "-" << m_maxZValue; } else { m_maxZValue = maxZ; } maxZChanged = true; } if (minXChanged) emit qptr()->minXValueChanged(m_minXValue); if (minZChanged) emit qptr()->minZValueChanged(m_minZValue); if (maxXChanged) emit qptr()->maxXValueChanged(m_maxXValue); if (maxZChanged) emit qptr()->maxZValueChanged(m_maxZValue); if ((minXChanged || minZChanged || maxXChanged || maxZChanged) && !m_resolveTimer.isActive()) m_resolveTimer.start(0); } void QHeightMapSurfaceDataProxyPrivate::setMinXValue(float min) { if (min != m_minXValue) { bool maxChanged = false; if (min >= m_maxXValue) { float oldMax = m_maxXValue; m_maxXValue = min + 1.0f; qWarning() << "Warning: Tried to set minimum X to equal or larger than maximum X for" " value range. Maximum automatically adjusted to a valid one:" << oldMax << "-->" << m_maxXValue; maxChanged = true; } m_minXValue = min; emit qptr()->minXValueChanged(m_minXValue); if (maxChanged) emit qptr()->maxXValueChanged(m_maxXValue); if (!m_resolveTimer.isActive()) m_resolveTimer.start(0); } } void QHeightMapSurfaceDataProxyPrivate::setMaxXValue(float max) { if (m_maxXValue != max) { bool minChanged = false; if (max <= m_minXValue) { float oldMin = m_minXValue; m_minXValue = max - 1.0f; qWarning() << "Warning: Tried to set maximum X to equal or smaller than minimum X for" " value range. Minimum automatically adjusted to a valid one:" << oldMin << "-->" << m_minXValue; minChanged = true; } m_maxXValue = max; emit qptr()->maxXValueChanged(m_maxXValue); if (minChanged) emit qptr()->minXValueChanged(m_minXValue); if (!m_resolveTimer.isActive()) m_resolveTimer.start(0); } } void QHeightMapSurfaceDataProxyPrivate::setMinZValue(float min) { if (min != m_minZValue) { bool maxChanged = false; if (min >= m_maxZValue) { float oldMax = m_maxZValue; m_maxZValue = min + 1.0f; qWarning() << "Warning: Tried to set minimum Z to equal or larger than maximum Z for" " value range. Maximum automatically adjusted to a valid one:" << oldMax << "-->" << m_maxZValue; maxChanged = true; } m_minZValue = min; emit qptr()->minZValueChanged(m_minZValue); if (maxChanged) emit qptr()->maxZValueChanged(m_maxZValue); if (!m_resolveTimer.isActive()) m_resolveTimer.start(0); } } void QHeightMapSurfaceDataProxyPrivate::setMaxZValue(float max) { if (m_maxZValue != max) { bool minChanged = false; if (max <= m_minZValue) { float oldMin = m_minZValue; m_minZValue = max - 1.0f; qWarning() << "Warning: Tried to set maximum Z to equal or smaller than minimum Z for" " value range. Minimum automatically adjusted to a valid one:" << oldMin << "-->" << m_minZValue; minChanged = true; } m_maxZValue = max; emit qptr()->maxZValueChanged(m_maxZValue); if (minChanged) emit qptr()->minZValueChanged(m_minZValue); if (!m_resolveTimer.isActive()) m_resolveTimer.start(0); } } void QHeightMapSurfaceDataProxyPrivate::handlePendingResolve() { QImage heightImage = m_heightMap; // Convert to RGB32 to be sure we're reading the right bytes if (heightImage.format() != QImage::Format_RGB32) heightImage = heightImage.convertToFormat(QImage::Format_RGB32); uchar *bits = heightImage.bits(); int imageHeight = heightImage.height(); int imageWidth = heightImage.width(); int bitCount = imageWidth * 4 * (imageHeight - 1); int widthBits = imageWidth * 4; float height = 0; // Do not recreate array if dimensions have not changed QSurfaceDataArray *dataArray = m_dataArray; if (imageWidth != qptr()->columnCount() || imageHeight != dataArray->size()) { dataArray = new QSurfaceDataArray; dataArray->reserve(imageHeight); for (int i = 0; i < imageHeight; i++) { QSurfaceDataRow *newProxyRow = new QSurfaceDataRow(imageWidth); dataArray->append(newProxyRow); } } float xMul = (m_maxXValue - m_minXValue) / float(imageWidth - 1); float zMul = (m_maxZValue - m_minZValue) / float(imageHeight - 1); // Last row and column are explicitly set to max values, as relying // on multiplier can cause rounding errors, resulting in the value being // slightly over the specified maximum, which in turn can lead to it not // getting rendered. int lastRow = imageHeight - 1; int lastCol = imageWidth - 1; if (heightImage.isGrayscale()) { // Grayscale, it's enough to read Red byte for (int i = 0; i < imageHeight; i++, bitCount -= widthBits) { QSurfaceDataRow &newRow = *dataArray->at(i); float zVal; if (i == lastRow) zVal = m_maxZValue; else zVal = (float(i) * zMul) + m_minZValue; int j = 0; for (; j < lastCol; j++) newRow[j].setPosition(QVector3D((float(j) * xMul) + m_minXValue, float(bits[bitCount + (j * 4)]), zVal)); newRow[j].setPosition(QVector3D(m_maxXValue, float(bits[bitCount + (j * 4)]), zVal)); } } else { // Not grayscale, we'll need to calculate height from RGB for (int i = 0; i < imageHeight; i++, bitCount -= widthBits) { QSurfaceDataRow &newRow = *dataArray->at(i); float zVal; if (i == lastRow) zVal = m_maxZValue; else zVal = (float(i) * zMul) + m_minZValue; int j = 0; int nextpixel = 0; for (; j < lastCol; j++) { nextpixel = j * 4; height = (float(bits[bitCount + nextpixel]) + float(bits[1 + bitCount + nextpixel]) + float(bits[2 + bitCount + nextpixel])); newRow[j].setPosition(QVector3D((float(j) * xMul) + m_minXValue, height / 3.0f, zVal)); } nextpixel = j * 4; height = (float(bits[bitCount + nextpixel]) + float(bits[1 + bitCount + nextpixel]) + float(bits[2 + bitCount + nextpixel])); newRow[j].setPosition(QVector3D(m_maxXValue, height / 3.0f, zVal)); } } qptr()->resetArray(dataArray); emit qptr()->heightMapChanged(m_heightMap); } QT_END_NAMESPACE_DATAVISUALIZATION