blob: 1d762f888e736d22e84a776a9d906203e770a562 [file] [log] [blame]
/****************************************************************************
**
** 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
*
* \brief The height map image to be visualized.
*/
/*!
* Replaces current data with the height map data specified by \a image.
*
* 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
*
* \brief The name of the file with a height map image to be visualized.
*/
/*!
* Replaces current data with height map data from the file specified by
* \a filename.
*
* \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
*
* \brief 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
*
* \brief 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
*
* \brief 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
*
* \brief 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<QHeightMapSurfaceDataProxyPrivate *>(d_ptr.data());
}
/*!
* \internal
*/
const QHeightMapSurfaceDataProxyPrivate *QHeightMapSurfaceDataProxy::dptrc() const
{
return static_cast<const QHeightMapSurfaceDataProxyPrivate *>(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<QHeightMapSurfaceDataProxy *>(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