Revision control
++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses
++
++ #include "scatter3dcontroller_p.h"
++ #include "scatter3drenderer_p.h"
++ #include "qvalue3daxis_p.h"
++ #include "qscatterdataproxy_p.h"
++ #include "qscatter3dseries_p.h"
++ #include <QtCore/QMutexLocker>
++
++ QT_BEGIN_NAMESPACE
++
++ static const int insertRemoveRecordReserveSize = 31;
++
++ Scatter3DController::Scatter3DController(QRect boundRect, Q3DScene *scene)
++ : Abstract3DController(boundRect, scene),
++ m_renderer(0),
++ m_selectedItem(invalidSelectionIndex()),
++ m_selectedItemSeries(0),
++ m_recordInsertsAndRemoves(false)
++ {
++ // Setting a null axis creates a new default axis according to orientation and graph type.
++ // Note: These cannot be set in Abstract3DController constructor, as they will call virtual
++ // functions implemented by subclasses.
++ setAxisX(0);
++ setAxisY(0);
++ setAxisZ(0);
++ }
++
++ Scatter3DController::~Scatter3DController()
++ {
++ }
++
++ void Scatter3DController::initializeOpenGL()
++ {
++ QMutexLocker mutexLocker(&m_renderMutex);
++
++ // Initialization is called multiple times when Qt Quick components are used
++ if (isInitialized())
++ return;
++
++ m_renderer = new Scatter3DRenderer(this);
++ setRenderer(m_renderer);
++
++ mutexLocker.unlock();
++ synchDataToRenderer();
++
++ emitNeedRender();
++ }
++
++ void Scatter3DController::synchDataToRenderer()
++ {
++ QMutexLocker mutexLocker(&m_renderMutex);
++
++ if (!isInitialized())
++ return;
++
++ Abstract3DController::synchDataToRenderer();
++
++ // Notify changes to renderer
++ if (m_changeTracker.itemChanged) {
++ m_renderer->updateItems(m_changedItems);
++ m_changeTracker.itemChanged = false;
++ m_changedItems.clear();
++ }
++
++ if (m_changeTracker.selectedItemChanged) {
++ m_renderer->updateSelectedItem(m_selectedItem, m_selectedItemSeries);
++ m_changeTracker.selectedItemChanged = false;
++ }
++ }
++
++ void Scatter3DController::addSeries(QAbstract3DSeries *series)
++ {
++ Q_ASSERT(series && series->type() == QAbstract3DSeries::SeriesTypeScatter);
++
++ Abstract3DController::addSeries(series);
++
++ QScatter3DSeries *scatterSeries = static_cast<QScatter3DSeries *>(series);
++ if (scatterSeries->selectedItem() != invalidSelectionIndex())
++ setSelectedItem(scatterSeries->selectedItem(), scatterSeries);
++ }
++
++ void Scatter3DController::removeSeries(QAbstract3DSeries *series)
++ {
++ bool wasVisible = (series && series->d_ptr->m_controller == this && series->isVisible());
++
++ Abstract3DController::removeSeries(series);
++
++ if (m_selectedItemSeries == series)
++ setSelectedItem(invalidSelectionIndex(), 0);
++
++ if (wasVisible)
++ adjustAxisRanges();
++ }
++
++ QList<QScatter3DSeries *> Scatter3DController::scatterSeriesList()
++ {
++ QList<QAbstract3DSeries *> abstractSeriesList = seriesList();
++ QList<QScatter3DSeries *> scatterSeriesList;
++ for (auto abstractSeries: abstractSeriesList) {
++ QScatter3DSeries *scatterSeries = qobject_cast<QScatter3DSeries *>(abstractSeries);
++ if (scatterSeries)
++ scatterSeriesList.append(scatterSeries);
++ }
++
++ return scatterSeriesList;
++ }
++
++ void Scatter3DController::handleArrayReset()
++ {
++ QScatter3DSeries *series;
++ if (qobject_cast<QScatterDataProxy *>(sender()))
++ series = static_cast<QScatterDataProxy *>(sender())->series();
++ else
++ series = static_cast<QScatter3DSeries *>(sender());
++
++ if (series->isVisible()) {
++ adjustAxisRanges();
++ m_isDataDirty = true;
++ }
++ if (!m_changedSeriesList.contains(series))
++ m_changedSeriesList.append(series);
++ setSelectedItem(m_selectedItem, m_selectedItemSeries);
++ series->d_ptr->markItemLabelDirty();
++ emitNeedRender();
++ }
++
++ void Scatter3DController::handleItemsAdded(int startIndex, int count)
++ {
++ Q_UNUSED(startIndex);
++ Q_UNUSED(count);
++ QScatter3DSeries *series = static_cast<QScatterDataProxy *>(sender())->series();
++ if (series->isVisible()) {
++ adjustAxisRanges();
++ m_isDataDirty = true;
++ }
++ if (!m_changedSeriesList.contains(series))
++ m_changedSeriesList.append(series);
++ emitNeedRender();
++ }
++
++ void Scatter3DController::handleItemsChanged(int startIndex, int count)
++ {
++ QScatter3DSeries *series = static_cast<QScatterDataProxy *>(sender())->series();
++ int oldChangeCount = m_changedItems.size();
++ if (!oldChangeCount)
++ m_changedItems.reserve(count);
++
++ for (int i = 0; i < count; i++) {
++ bool newItem = true;
++ int candidate = startIndex + i;
++ for (int j = 0; j < oldChangeCount; j++) {
++ const ChangeItem &oldChangeItem = m_changedItems.at(j);
++ if (oldChangeItem.index == candidate && series == oldChangeItem.series) {
++ newItem = false;
++ break;
++ }
++ }
++ if (newItem) {
++ ChangeItem newChangeItem = {series, candidate};
++ m_changedItems.append(newChangeItem);
++ if (series == m_selectedItemSeries && m_selectedItem == candidate)
++ series->d_ptr->markItemLabelDirty();
++ }
++ }
++
++ if (count) {
++ m_changeTracker.itemChanged = true;
++ if (series->isVisible())
++ adjustAxisRanges();
++ emitNeedRender();
++ }
++ }
++
++ void Scatter3DController::handleItemsRemoved(int startIndex, int count)
++ {
++ Q_UNUSED(startIndex);
++ Q_UNUSED(count);
++ QScatter3DSeries *series = static_cast<QScatterDataProxy *>(sender())->series();
++ if (series == m_selectedItemSeries) {
++ // If items removed from selected series before the selection, adjust the selection
++ int selectedItem = m_selectedItem;
++ if (startIndex <= selectedItem) {
++ if ((startIndex + count) > selectedItem)
++ selectedItem = -1; // Selected item removed
++ else
++ selectedItem -= count; // Move selected item down by amount of item removed
++
++ setSelectedItem(selectedItem, m_selectedItemSeries);
++ }
++ }
++
++ if (series->isVisible()) {
++ adjustAxisRanges();
++ m_isDataDirty = true;
++ }
++ if (!m_changedSeriesList.contains(series))
++ m_changedSeriesList.append(series);
++
++ if (m_recordInsertsAndRemoves) {
++ InsertRemoveRecord record(false, startIndex, count, series);
++ m_insertRemoveRecords.append(record);
++ }
++
++ emitNeedRender();
++ }
++
++ void Scatter3DController::handleItemsInserted(int startIndex, int count)
++ {
++ Q_UNUSED(startIndex);
++ Q_UNUSED(count);
++ QScatter3DSeries *series = static_cast<QScatterDataProxy *>(sender())->series();
++ if (series == m_selectedItemSeries) {
++ // If items inserted to selected series before the selection, adjust the selection
++ int selectedItem = m_selectedItem;
++ if (startIndex <= selectedItem) {
++ selectedItem += count;
++ setSelectedItem(selectedItem, m_selectedItemSeries);
++ }
++ }
++
++ if (series->isVisible()) {
++ adjustAxisRanges();
++ m_isDataDirty = true;
++ }
++ if (!m_changedSeriesList.contains(series))
++ m_changedSeriesList.append(series);
++
++ if (m_recordInsertsAndRemoves) {
++ InsertRemoveRecord record(true, startIndex, count, series);
++ m_insertRemoveRecords.append(record);
++ }
++
++ emitNeedRender();
++ }
++
++ void Scatter3DController::startRecordingRemovesAndInserts()
++ {
++ m_recordInsertsAndRemoves = false;
++
++ if (m_scene->selectionQueryPosition() != Q3DScene::invalidSelectionPoint()) {
++ m_recordInsertsAndRemoves = true;
++ if (m_insertRemoveRecords.size()) {
++ m_insertRemoveRecords.clear();
++ // Reserve some space for remove/insert records to avoid unnecessary reallocations.
++ m_insertRemoveRecords.reserve(insertRemoveRecordReserveSize);
++ }
++ }
++ }
++
++ void Scatter3DController::handleAxisAutoAdjustRangeChangedInOrientation(
++ QAbstract3DAxis::AxisOrientation orientation, bool autoAdjust)
++ {
++ Q_UNUSED(orientation);
++ Q_UNUSED(autoAdjust);
++ adjustAxisRanges();
++ }
++
++ void Scatter3DController::handleAxisRangeChangedBySender(QObject *sender)
++ {
++ Abstract3DController::handleAxisRangeChangedBySender(sender);
++
++ // Update selected index - may be moved offscreen
++ setSelectedItem(m_selectedItem, m_selectedItemSeries);
++ }
++
++ void Scatter3DController::handlePendingClick()
++ {
++ int index = m_renderer->clickedIndex();
++ QScatter3DSeries *series = static_cast<QScatter3DSeries *>(m_renderer->clickedSeries());
++
++ // Adjust position according to recorded events
++ int recordCount = m_insertRemoveRecords.size();
++ if (recordCount) {
++ for (int i = 0; i < recordCount; i++) {
++ const InsertRemoveRecord &record = m_insertRemoveRecords.at(i);
++ if (series == record.m_series && record.m_startIndex <= index) {
++ if (record.m_isInsert) {
++ index += record.m_count;
++ } else {
++ if ((record.m_startIndex + record.m_count) > index) {
++ index = -1; // Selected row removed
++ break;
++ } else {
++ index -= record.m_count; // Move selected item down by amount of items removed
++ }
++ }
++ }
++ }
++ }
++
++ setSelectedItem(index, series);
++
++ Abstract3DController::handlePendingClick();
++
++ m_renderer->resetClickedStatus();
++ }
++
++ void Scatter3DController::setSelectionMode(QAbstract3DGraph::SelectionFlags mode)
++ {
++ // We only support single item selection mode and no selection mode
++ if (mode != QAbstract3DGraph::SelectionItem && mode != QAbstract3DGraph::SelectionNone) {
++ qWarning("Unsupported selection mode - only none and item selection modes are supported.");
++ return;
++ }
++
++ Abstract3DController::setSelectionMode(mode);
++ }
++
++ void Scatter3DController::setSelectedItem(int index, QScatter3DSeries *series)
++ {
++ const QScatterDataProxy *proxy = 0;
++
++ // Series may already have been removed, so check it before setting the selection.
++ if (!m_seriesList.contains(series))
++ series = 0;
++
++ if (series)
++ proxy = series->dataProxy();
++
++ if (!proxy || index < 0 || index >= proxy->itemCount())
++ index = invalidSelectionIndex();
++
++ if (index != m_selectedItem || series != m_selectedItemSeries) {
++ bool seriesChanged = (series != m_selectedItemSeries);
++ m_selectedItem = index;
++ m_selectedItemSeries = series;
++ m_changeTracker.selectedItemChanged = true;
++
++ // Clear selection from other series and finally set new selection to the specified series
++ for (auto otherSeries: m_seriesList) {
++ QScatter3DSeries *scatterSeries = static_cast<QScatter3DSeries *>(otherSeries);
++ if (scatterSeries != m_selectedItemSeries)
++ scatterSeries->dptr()->setSelectedItem(invalidSelectionIndex());
++ }
++ if (m_selectedItemSeries)
++ m_selectedItemSeries->dptr()->setSelectedItem(m_selectedItem);
++
++ if (seriesChanged)
++ emit selectedSeriesChanged(m_selectedItemSeries);
++
++ emitNeedRender();
++ }
++ }
++
++ void Scatter3DController::clearSelection()
++ {
++ setSelectedItem(invalidSelectionIndex(), 0);
++ }
++
++ void Scatter3DController::adjustAxisRanges()
++ {
++ QValue3DAxis *valueAxisX = static_cast<QValue3DAxis *>(m_axisX);
++ QValue3DAxis *valueAxisY = static_cast<QValue3DAxis *>(m_axisY);
++ QValue3DAxis *valueAxisZ = static_cast<QValue3DAxis *>(m_axisZ);
++ bool adjustX = (valueAxisX && valueAxisX->isAutoAdjustRange());
++ bool adjustY = (valueAxisY && valueAxisY->isAutoAdjustRange());
++ bool adjustZ = (valueAxisZ && valueAxisZ->isAutoAdjustRange());
++
++ if (adjustX || adjustY || adjustZ) {
++ float minValueX = 0.0f;
++ float maxValueX = 0.0f;
++ float minValueY = 0.0f;
++ float maxValueY = 0.0f;
++ float minValueZ = 0.0f;
++ float maxValueZ = 0.0f;
++ int seriesCount = m_seriesList.size();
++ for (int series = 0; series < seriesCount; series++) {
++ const QScatter3DSeries *scatterSeries =
++ static_cast<QScatter3DSeries *>(m_seriesList.at(series));
++ const QScatterDataProxy *proxy = scatterSeries->dataProxy();
++ if (scatterSeries->isVisible() && proxy) {
++ QVector3D minLimits;
++ QVector3D maxLimits;
++ proxy->dptrc()->limitValues(minLimits, maxLimits, valueAxisX, valueAxisY, valueAxisZ);
++ if (adjustX) {
++ if (!series) {
++ // First series initializes the values
++ minValueX = minLimits.x();
++ maxValueX = maxLimits.x();
++ } else {
++ minValueX = qMin(minValueX, minLimits.x());
++ maxValueX = qMax(maxValueX, maxLimits.x());
++ }
++ }
++ if (adjustY) {
++ if (!series) {
++ // First series initializes the values
++ minValueY = minLimits.y();
++ maxValueY = maxLimits.y();
++ } else {
++ minValueY = qMin(minValueY, minLimits.y());
++ maxValueY = qMax(maxValueY, maxLimits.y());
++ }
++ }
++ if (adjustZ) {
++ if (!series) {
++ // First series initializes the values
++ minValueZ = minLimits.z();
++ maxValueZ = maxLimits.z();
++ } else {
++ minValueZ = qMin(minValueZ, minLimits.z());
++ maxValueZ = qMax(maxValueZ, maxLimits.z());
++ }
++ }
++ }
++ }
++
++ static const float adjustmentRatio = 20.0f;
++ static const float defaultAdjustment = 1.0f;
++
++ if (adjustX) {
++ // If all points at same coordinate, need to default to some valid range
++ float adjustment = 0.0f;
++ if (minValueX == maxValueX) {
++ if (adjustZ) {
++ // X and Z are linked to have similar unit size, so choose the valid range based on it
++ if (minValueZ == maxValueZ)
++ adjustment = defaultAdjustment;
++ else
++ adjustment = qAbs(maxValueZ - minValueZ) / adjustmentRatio;
++ } else {
++ if (valueAxisZ)
++ adjustment = qAbs(valueAxisZ->max() - valueAxisZ->min()) / adjustmentRatio;
++ else
++ adjustment = defaultAdjustment;
++ }
++ }
++ valueAxisX->dptr()->setRange(minValueX - adjustment, maxValueX + adjustment, true);
++ }
++ if (adjustY) {
++ // If all points at same coordinate, need to default to some valid range
++ // Y-axis unit is not dependent on other axes, so simply adjust +-1.0f
++ float adjustment = 0.0f;
++ if (minValueY == maxValueY)
++ adjustment = defaultAdjustment;
++ valueAxisY->dptr()->setRange(minValueY - adjustment, maxValueY + adjustment, true);
++ }
++ if (adjustZ) {
++ // If all points at same coordinate, need to default to some valid range
++ float adjustment = 0.0f;
++ if (minValueZ == maxValueZ) {
++ if (adjustX) {
++ // X and Z are linked to have similar unit size, so choose the valid range based on it
++ if (minValueX == maxValueX)
++ adjustment = defaultAdjustment;
++ else
++ adjustment = qAbs(maxValueX - minValueX) / adjustmentRatio;
++ } else {
++ if (valueAxisX)
++ adjustment = qAbs(valueAxisX->max() - valueAxisX->min()) / adjustmentRatio;
++ else
++ adjustment = defaultAdjustment;
++ }
++ }
++ valueAxisZ->dptr()->setRange(minValueZ - adjustment, maxValueZ + adjustment, true);
++ }
++ }
++ }
++
++ QT_END_NAMESPACE