diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 61ed56b..1023720 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,14 +5,10 @@ on: pull_request: workflow_dispatch: -env: - # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) - BUILD_TYPE: Release - # for matrix check https://docs.github.com/en/actions/reference/specifications-for-github-hosted-runners jobs: prepare_matrix: - runs-on: ubuntu-latest + runs-on: ubuntu-slim outputs: matrix: ${{ steps.matrix_setup.outputs.matrix }} steps: @@ -34,7 +30,7 @@ jobs: steps: - name: Checkout the source - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: recursive ref: ${{ github.event.pull_request.head.ref }} @@ -45,9 +41,9 @@ jobs: sudo xcode-select -switch /Applications/Xcode_${{matrix.build-xcode-version}}.app - name: Setup python version - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: - python-version: "3.11" + python-version: "3.12" - name: Start ssh key agent uses: webfactory/ssh-agent@v0.9.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index b9674d0..39453a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,17 +15,7 @@ PROJECT(${PROJECT} # ----------------------------------------------------------------------------- # CMake Options # ----------------------------------------------------------------------------- -set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) set(CMAKE_INCLUDE_CURRENT_DIR ON) -set(CMAKE_AUTORCC ON) -set(CMAKE_AUTOMOC ON) - -if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DWIN32 /EHsc /MP /permissive- /Zc:__cplusplus") - set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MDd") - set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} /MD") - set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MD") -endif() # ----------------------------------------------------------------------------- # Dependencies @@ -33,6 +23,7 @@ endif() find_package(Qt6 COMPONENTS Widgets WebEngineWidgets OpenGL OpenGLWidgets REQUIRED) find_package(ManiVault COMPONENTS Core PointData ClusterData ColorData ImageData CONFIG QUIET) +mv_project_defaults() # ----------------------------------------------------------------------------- # Source files @@ -116,9 +107,10 @@ target_include_directories(${PROJECT} PRIVATE "${ManiVault_INCLUDE_DIR}") # ----------------------------------------------------------------------------- target_compile_features(${PROJECT} PRIVATE cxx_std_20) -if(MV_UNITY_BUILD) - set_target_properties(${PROJECT} PROPERTIES UNITY_BUILD ON) -endif() +set_target_properties(${PROJECT} PROPERTIES + AUTOMOC ON + UNITY_BUILD ${MV_UNITY_BUILD} +) # ----------------------------------------------------------------------------- # Target library linking diff --git a/PluginInfo.json b/PluginInfo.json index a09ee50..9dc9e34 100644 --- a/PluginInfo.json +++ b/PluginInfo.json @@ -2,7 +2,7 @@ "name" : "Scatterplot View", "version" : { "plugin" : "1.0.0", - "core" : ["1.3"] + "core" : ["1.5"] }, "type" : "View", "dependencies" : ["Points"] diff --git a/conanfile.py b/conanfile.py index 3b84dde..a8ba72b 100644 --- a/conanfile.py +++ b/conanfile.py @@ -105,7 +105,7 @@ def generate(self): tc.variables["ManiVault_DIR"] = manivault_dir # Set some build options - tc.variables["MV_UNITY_BUILD"] = "ON" + tc.cache_variables["MV_UNITY_BUILD"] = True tc.generate() @@ -120,7 +120,6 @@ def build(self): cmake = self._configure_cmake() cmake.build(build_type="RelWithDebInfo") - cmake.build(build_type="Release") def package(self): package_dir = pathlib.Path(self.build_folder, "package") @@ -138,23 +137,9 @@ def package(self): relWithDebInfo_dir, ] ) - subprocess.run( - [ - "cmake", - "--install", - self.build_folder, - "--config", - "Release", - "--prefix", - release_dir, - ] - ) self.copy(pattern="*", src=package_dir) def package_info(self): self.cpp_info.relwithdebinfo.libdirs = ["RelWithDebInfo/lib"] self.cpp_info.relwithdebinfo.bindirs = ["RelWithDebInfo/Plugins", "RelWithDebInfo"] self.cpp_info.relwithdebinfo.includedirs = ["RelWithDebInfo/include", "RelWithDebInfo"] - self.cpp_info.release.libdirs = ["Release/lib"] - self.cpp_info.release.bindirs = ["Release/Plugins", "Release"] - self.cpp_info.release.includedirs = ["Release/include", "Release"] diff --git a/src/ColoringAction.cpp b/src/ColoringAction.cpp index c3fca1f..a0859e1 100644 --- a/src/ColoringAction.cpp +++ b/src/ColoringAction.cpp @@ -16,7 +16,10 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : _colorByModel(this), _colorByAction(this, "Color by"), _constantColorAction(this, "Constant color", DEFAULT_CONSTANT_COLOR), + _colorSpaceAction(this, "Color space", { "Scalar (1D)", "Duo (2D)", "RGB" }, "Scalar (1D)"), _dimensionAction(this, "Dimension"), + _dimensionAction2(this, "Dimension 2"), + _dimensionAction3(this, "Dimension 3"), _colorMap1DAction(this, "1D Color map"), _colorMap2DAction(this, "2D Color map") { @@ -26,9 +29,14 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : addAction(&_colorByAction); addAction(&_constantColorAction); - addAction(&_colorMap2DAction); + addAction(&_colorSpaceAction); addAction(&_colorMap1DAction); + addAction(&_colorMap2DAction); addAction(&_dimensionAction); + addAction(&_dimensionAction2); + addAction(&_dimensionAction3); + + _colorSpaceAction.setToolTip("Color space for data-driven coloring"); _scatterplotPlugin->getWidget().addAction(&_colorByAction); _scatterplotPlugin->getWidget().addAction(&_dimensionAction); @@ -86,28 +94,51 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : connect(&_currentColorPointsDataset, &Dataset::dataDimensionsChanged, this, [this]() { if (_currentColorPointsDataset.isValid()) { _dimensionAction.setPointsDataset(_currentColorPointsDataset); + _dimensionAction2.setPointsDataset(_currentColorPointsDataset); + _dimensionAction3.setPointsDataset(_currentColorPointsDataset); updateScatterPlotWidgetColors(); } }); _dimensionAction.setPointsDataset(_currentColorPointsDataset); + _dimensionAction2.setPointsDataset(_currentColorPointsDataset); + _dimensionAction3.setPointsDataset(_currentColorPointsDataset); + + // Auto-select the color space for datasets with exactly two or three channels + if (!mv::projects().isOpeningProject()) { + const auto numDimensions = static_cast(_currentColorPointsDataset->getNumDimensions()); + + if (numDimensions == 2) { + _colorSpaceAction.setCurrentIndex(1); // Duo (2D) + applyDefaultChannels(); // also apply defaults when the index was already Duo + } + else if (numDimensions == 3) { + _colorSpaceAction.setCurrentIndex(2); // RGB + applyDefaultChannels(); // also apply defaults when the index was already RGB + } + } } else { _dimensionAction.setPointsDataset(Dataset()); + _dimensionAction2.setPointsDataset(Dataset()); + _dimensionAction3.setPointsDataset(Dataset()); } } else { _dimensionAction.setPointsDataset(Dataset()); + _dimensionAction2.setPointsDataset(Dataset()); + _dimensionAction3.setPointsDataset(Dataset()); } - //_dimensionAction.setVisible(currentColorDatasetTypeIsPointType); emit currentColorDatasetChanged(currentColorDataset); } else { _dimensionAction.setPointsDataset(Dataset()); - //_dimensionAction.setVisible(false); + _dimensionAction2.setPointsDataset(Dataset()); + _dimensionAction3.setPointsDataset(Dataset()); } + updateChannelActionsReadOnly(); updateScatterPlotWidgetColors(); updateScatterplotWidgetColorMap(); updateColorMapActionScalarRange(); @@ -142,7 +173,25 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : updateColorMapActionsReadOnly(); updateColorMapActionScalarRange(); }); - + + connect(&_dimensionAction2, &DimensionPickerAction::currentDimensionIndexChanged, this, [this](const int32_t& currentDimensionIndex) { + updateScatterPlotWidgetColors(); + }); + + connect(&_dimensionAction3, &DimensionPickerAction::currentDimensionIndexChanged, this, [this](const int32_t& currentDimensionIndex) { + updateScatterPlotWidgetColors(); + }); + + connect(&_colorSpaceAction, &OptionAction::currentIndexChanged, this, [this](const std::int32_t& currentIndex) { + if (!mv::projects().isOpeningProject()) + applyDefaultChannels(); + + updateChannelActionsReadOnly(); + updateScatterPlotWidgetColors(); + updateScatterplotWidgetColorMap(); + updateColorMapActionsReadOnly(); + }); + connect(&_constantColorAction, &ColorAction::colorChanged, this, &ColoringAction::updateScatterplotWidgetColorMap); connect(&_colorMap1DAction, &ColorMapAction::imageChanged, this, &ColoringAction::updateScatterplotWidgetColorMap); connect(&_colorMap2DAction, &ColorMapAction::imageChanged, this, &ColoringAction::updateScatterplotWidgetColorMap); @@ -160,6 +209,7 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : updateScatterplotWidgetColorMap(); updateColorMapActionScalarRange(); + updateChannelActionsReadOnly(); _scatterplotPlugin->getScatterplotWidget().setColoringMode(ScatterplotWidget::ColoringMode::Constant); } @@ -251,10 +301,38 @@ void ColoringAction::updateScatterPlotWidgetColors() if (currentColorDataset->getDataType() == ClusterType) _scatterplotPlugin->loadColors(currentColorDataset.get()); else { - const auto currentDimensionIndex = _dimensionAction.getCurrentDimensionIndex(); + const auto dimension1 = _dimensionAction.getCurrentDimensionIndex(); + + if (dimension1 < 0) + return; + + switch (_colorSpaceAction.getCurrentIndex()) + { + case 1: // Duo (2D) + { + const auto dimension2 = _dimensionAction2.getCurrentDimensionIndex(); + + if (dimension2 >= 0) + _scatterplotPlugin->loadColors2D(currentColorDataset.get(), dimension1, dimension2); + + break; + } + + case 2: // RGB + { + const auto dimension2 = _dimensionAction2.getCurrentDimensionIndex(); + const auto dimension3 = _dimensionAction3.getCurrentDimensionIndex(); - if (currentDimensionIndex >= 0) - _scatterplotPlugin->loadColors(currentColorDataset.get(), _dimensionAction.getCurrentDimensionIndex()); + if (dimension2 >= 0 && dimension3 >= 0) + _scatterplotPlugin->loadColorsRGB(currentColorDataset.get(), dimension1, dimension2, dimension3); + + break; + } + + default: // Scalar (1D) + _scatterplotPlugin->loadColors(currentColorDataset.get(), dimension1); + break; + } } updateScatterplotWidgetColorMap(); @@ -297,7 +375,13 @@ void ColoringAction::updateScatterplotWidgetColorMap() scatterplotWidget.setColoringMode(ScatterplotWidget::ColoringMode::Scatter); } else { - scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().mirrored(false, true)); + const auto currentColorDataset = getCurrentColorDataset(); + const bool isDuo = currentColorDataset.isValid() && currentColorDataset->getDataType() == PointType && _colorSpaceAction.getCurrentIndex() == 1; + + if (isDuo) + scatterplotWidget.setColorMap(_colorMap2DAction.getColorMapImage().flipped(Qt::Vertical)); + else + scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().flipped(Qt::Vertical)); } break; @@ -324,9 +408,22 @@ void ColoringAction::updateScatterplotWidgetColorMap() void ColoringAction::updateScatterPlotWidgetColorMapRange() { + auto& scatterplotWidget = _scatterplotPlugin->getScatterplotWidget(); + + // The adjustable 1D color-map range only drives the (channel 1) scalar range for 1D scalar coloring. + // In Duo/RGB the color channels each use their own automatically-computed range, so leave channel 1 + // untouched here (otherwise identical channels would normalize differently and produce a color tint). + if (scatterplotWidget.getRenderMode() == ScatterplotWidget::SCATTERPLOT) { + const auto currentColorDataset = getCurrentColorDataset(); + const bool isPointsSource = currentColorDataset.isValid() && currentColorDataset->getDataType() == PointType; + + if (isPointsSource && _colorSpaceAction.getCurrentIndex() != 0) // Duo (1) or RGB (2) + return; + } + const auto& rangeAction = _colorMap1DAction.getRangeAction(ColorMapAction::Axis::X); - _scatterplotPlugin->getScatterplotWidget().setColorMapRange(rangeAction.getMinimum(), rangeAction.getMaximum()); + scatterplotWidget.setColorMapRange(rangeAction.getMinimum(), rangeAction.getMaximum()); } bool ColoringAction::shouldEnableColorMap() const @@ -351,9 +448,71 @@ bool ColoringAction::shouldEnableColorMap() const void ColoringAction::updateColorMapActionsReadOnly() { const auto currentIndex = _colorByAction.getCurrentIndex(); + const bool isPointsSource = currentIndex >= 2 && _currentColorPointsDataset.isValid(); + const bool isDuo = isPointsSource && _colorSpaceAction.getCurrentIndex() == 1; + const bool isRGB = isPointsSource && _colorSpaceAction.getCurrentIndex() == 2; - _colorMap1DAction.setEnabled(shouldEnableColorMap() && (currentIndex >= 2)); - _colorMap2DAction.setEnabled(shouldEnableColorMap() && (currentIndex == 1)); + _colorMap1DAction.setEnabled(shouldEnableColorMap() && (currentIndex >= 2) && !isDuo && !isRGB); + _colorMap2DAction.setEnabled(shouldEnableColorMap() && (currentIndex == 1 || isDuo)); +} + +void ColoringAction::updateChannelActionsReadOnly() +{ + const auto currentIndex = _colorByAction.getCurrentIndex(); + const auto colorSpace = _colorSpaceAction.getCurrentIndex(); + + const bool isPointsSource = currentIndex >= 2 && _currentColorPointsDataset.isValid(); + + const bool isDuo = isPointsSource && colorSpace == 1; // Duo (2D) + const bool isRGB = isPointsSource && colorSpace == 2; // RGB + + // All actions remain visible; only their enabled state reflects the current coloring mode. + + // Constant color: only usable in constant mode + _constantColorAction.setEnabled(currentIndex == 0); + + // Color space selector: only usable for a points color source + _colorSpaceAction.setEnabled(isPointsSource); + + // Dimension pickers: channel 1 for any points source, channel 2 for Duo/RGB, channel 3 for RGB only + _dimensionAction.setEnabled(isPointsSource); + _dimensionAction2.setEnabled(isDuo || isRGB); + _dimensionAction3.setEnabled(isRGB); +} + +void ColoringAction::applyDefaultChannels() +{ + if (!_currentColorPointsDataset.isValid()) + return; + + const auto numDimensions = static_cast(_currentColorPointsDataset->getNumDimensions()); + + switch (_colorSpaceAction.getCurrentIndex()) + { + case 1: // Duo (2D): default to the first two channels + { + if (numDimensions >= 2) { + _dimensionAction.setCurrentDimensionIndex(0); + _dimensionAction2.setCurrentDimensionIndex(1); + } + + break; + } + + case 2: // RGB: default to the first three channels + { + if (numDimensions >= 3) { + _dimensionAction.setCurrentDimensionIndex(0); + _dimensionAction2.setCurrentDimensionIndex(1); + _dimensionAction3.setCurrentDimensionIndex(2); + } + + break; + } + + default: + break; + } } void ColoringAction::connectToPublicAction(WidgetAction* publicAction, bool recursive) @@ -368,7 +527,10 @@ void ColoringAction::connectToPublicAction(WidgetAction* publicAction, bool recu if (recursive) { actions().connectPrivateActionToPublicAction(&_colorByAction, &publicColoringAction->getColorByAction(), recursive); actions().connectPrivateActionToPublicAction(&_constantColorAction, &publicColoringAction->getConstantColorAction(), recursive); + actions().connectPrivateActionToPublicAction(&_colorSpaceAction, &publicColoringAction->getColorSpaceAction(), recursive); actions().connectPrivateActionToPublicAction(&_dimensionAction, &publicColoringAction->getDimensionAction(), recursive); + actions().connectPrivateActionToPublicAction(&_dimensionAction2, &publicColoringAction->getDimensionAction2(), recursive); + actions().connectPrivateActionToPublicAction(&_dimensionAction3, &publicColoringAction->getDimensionAction3(), recursive); actions().connectPrivateActionToPublicAction(&_colorMap1DAction, &publicColoringAction->getColorMap1DAction(), recursive); actions().connectPrivateActionToPublicAction(&_colorMap2DAction, &publicColoringAction->getColorMap2DAction(), recursive); } @@ -384,7 +546,10 @@ void ColoringAction::disconnectFromPublicAction(bool recursive) if (recursive) { actions().disconnectPrivateActionFromPublicAction(&_colorByAction, recursive); actions().disconnectPrivateActionFromPublicAction(&_constantColorAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_colorSpaceAction, recursive); actions().disconnectPrivateActionFromPublicAction(&_dimensionAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_dimensionAction2, recursive); + actions().disconnectPrivateActionFromPublicAction(&_dimensionAction3, recursive); actions().disconnectPrivateActionFromPublicAction(&_colorMap2DAction, recursive); } @@ -395,11 +560,22 @@ void ColoringAction::fromVariantMap(const QVariantMap& variantMap) { GroupAction::fromVariantMap(variantMap); + // Restore the color source first so the dimension pickers are targeted at the right dataset, + // then restore the color space and channels, and finally the color maps. _colorByAction.fromParentVariantMap(variantMap); _constantColorAction.fromParentVariantMap(variantMap); _dimensionAction.fromParentVariantMap(variantMap); + _dimensionAction2.fromParentVariantMap(variantMap, true); + _dimensionAction3.fromParentVariantMap(variantMap, true); + _colorSpaceAction.fromParentVariantMap(variantMap, true); _colorMap1DAction.fromParentVariantMap(variantMap); _colorMap2DAction.fromParentVariantMap(variantMap); + + // Apply the fully-restored coloring state + updateChannelActionsReadOnly(); + updateScatterPlotWidgetColors(); + updateScatterplotWidgetColorMap(); + updateColorMapActionsReadOnly(); } QVariantMap ColoringAction::toVariantMap() const @@ -408,7 +584,10 @@ QVariantMap ColoringAction::toVariantMap() const _colorByAction.insertIntoVariantMap(variantMap); _constantColorAction.insertIntoVariantMap(variantMap); + _colorSpaceAction.insertIntoVariantMap(variantMap); _dimensionAction.insertIntoVariantMap(variantMap); + _dimensionAction2.insertIntoVariantMap(variantMap); + _dimensionAction3.insertIntoVariantMap(variantMap); _colorMap1DAction.insertIntoVariantMap(variantMap); _colorMap2DAction.insertIntoVariantMap(variantMap); diff --git a/src/ColoringAction.h b/src/ColoringAction.h index 4a1d480..a1de452 100644 --- a/src/ColoringAction.h +++ b/src/ColoringAction.h @@ -66,6 +66,12 @@ class ColoringAction : public VerticalGroupAction /** Update the color by action options */ void updateColorByActionOptions(); + /** Enable/disable the color space and channel picker actions for the current coloring mode */ + void updateChannelActionsReadOnly(); + + /** Set the dimension pickers to sensible defaults (the first channels) for the current color space */ + void applyDefaultChannels(); + /** Update the colors of the points in the scatter plot widget */ void updateScatterPlotWidgetColors(); @@ -119,7 +125,10 @@ class ColoringAction : public VerticalGroupAction OptionAction& getColorByAction() { return _colorByAction; } ColorAction& getConstantColorAction() { return _constantColorAction; } + OptionAction& getColorSpaceAction() { return _colorSpaceAction; } DimensionPickerAction& getDimensionAction() { return _dimensionAction; } + DimensionPickerAction& getDimensionAction2() { return _dimensionAction2; } + DimensionPickerAction& getDimensionAction3() { return _dimensionAction3; } ColorMapAction& getColorMap1DAction() { return _colorMap1DAction; } ColorMapAction& getColorMap2DAction() { return _colorMap2DAction; } @@ -131,7 +140,10 @@ class ColoringAction : public VerticalGroupAction ColorSourceModel _colorByModel; /** Color by model (model input for the color by action) */ OptionAction _colorByAction; /** Action for picking the coloring type */ ColorAction _constantColorAction; /** Action for picking the constant color */ - DimensionPickerAction _dimensionAction; /** Dimension picker action */ + OptionAction _colorSpaceAction; /** Color space for data coloring (Scalar 1D / Duo 2D / RGB) */ + DimensionPickerAction _dimensionAction; /** Dimension picker action (color channel 1) */ + DimensionPickerAction _dimensionAction2; /** Dimension picker action (color channel 2, for Duo/RGB) */ + DimensionPickerAction _dimensionAction3; /** Dimension picker action (color channel 3, for RGB) */ ColorMap1DAction _colorMap1DAction; /** One-dimensional color map action */ ColorMap2DAction _colorMap2DAction; /** Two-dimensional color map action */ Dataset _currentColorPointsDataset; /** Current color dataset */ @@ -145,4 +157,4 @@ class ColoringAction : public VerticalGroupAction Q_DECLARE_METATYPE(ColoringAction) -inline const auto coloringActionMetaTypeId = qRegisterMetaType("ColoringAction"); \ No newline at end of file +inline const auto coloringActionMetaTypeId = qRegisterMetaType("ColoringAction"); diff --git a/src/DatasetsAction.cpp b/src/DatasetsAction.cpp index 3cc6381..2523689 100644 --- a/src/DatasetsAction.cpp +++ b/src/DatasetsAction.cpp @@ -5,7 +5,7 @@ #include #include -#include +#include using namespace mv; using namespace mv::gui; @@ -21,38 +21,44 @@ DatasetsAction::DatasetsAction(QObject* parent, const QString& title) : setConfigurationFlag(WidgetAction::ConfigurationFlag::ForceCollapsedInGroup); setLabelSizingType(LabelSizingType::Auto); - addAction(&_positionDatasetPickerAction); - addAction(&_colorDatasetPickerAction); - - _positionDatasetPickerAction.setFilterFunction([this](mv::Dataset dataset) -> bool { - return dataset->getDataType() == PointType; - }); - - _colorDatasetPickerAction.setFilterFunction([this](mv::Dataset dataset) -> bool { - return (dataset->getDataType() == PointType || dataset->getDataType() == ColorType || dataset->getDataType() == ClusterType); - }); - - auto scatterplotPlugin = dynamic_cast(parent->parent()); + Q_ASSERT(scatterplotPlugin); + if (scatterplotPlugin == nullptr) return; - connect(&_positionDatasetPickerAction, &DatasetPickerAction::datasetPicked, [this, scatterplotPlugin](Dataset pickedDataset) -> void { - scatterplotPlugin->getPositionDataset() = pickedDataset; - }); + auto& settingsAction = *dynamic_cast(parent); + auto& plotAction = settingsAction.getPlotAction(); + auto& pointPlotAction = plotAction.getPointPlotAction(); - connect(&scatterplotPlugin->getPositionDataset(), &Dataset::changed, this, [this](DatasetImpl* dataset) -> void { - _positionDatasetPickerAction.setCurrentDataset(dataset); - }); - - connect(&_colorDatasetPickerAction, &DatasetPickerAction::datasetPicked, [this, scatterplotPlugin](Dataset pickedDataset) -> void { - scatterplotPlugin->getSettingsAction().getColoringAction().setCurrentColorDataset(pickedDataset); - }); + addAction(&_positionDatasetPickerAction); + addAction(&_colorDatasetPickerAction); + addAction(new LabelProxyAction(this, "Size", const_cast(&pointPlotAction.getSizeAction().getSourceDatasetPickerAction()))); + addAction(new LabelProxyAction(this, "Opacity", const_cast(&pointPlotAction.getOpacityAction().getSourceDatasetPickerAction()))); + + _positionDatasetPickerAction.setDefaultWidgetFlag(OptionAction::Clearable); + _colorDatasetPickerAction.setDefaultWidgetFlag(OptionAction::Clearable); - connect(&scatterplotPlugin->getSettingsAction().getColoringAction(), &ColoringAction::currentColorDatasetChanged, this, [this](Dataset currentColorDataset) -> void { - _colorDatasetPickerAction.setCurrentDataset(currentColorDataset); + setupDatasetPickerActions(scatterplotPlugin); + + const auto invalidateFilters = [this, scatterplotPlugin]() -> void { + _colorDatasetPickerAction.invalidateFilter(); + }; + + connect(&_positionDatasetPickerAction, &DatasetPickerAction::datasetPicked, [this, scatterplotPlugin, invalidateFilters](Dataset pickedDataset) -> void { + invalidateFilters(); }); + + const auto resetAuxilliaryDatasets = [this, &pointPlotAction]() -> void { + _colorDatasetPickerAction.setCurrentIndex(-1); + + pointPlotAction.getSizeAction().setCurrentSourceIndex(ScalarSourceModel::DefaultRow::Constant); + pointPlotAction.getOpacityAction().setCurrentSourceIndex(ScalarSourceModel::DefaultRow::Constant); + }; + + connect(&scatterplotPlugin->getPositionDataset(), &Dataset::changed, this, resetAuxilliaryDatasets); + connect(&scatterplotPlugin->getPositionSourceDataset(), &Dataset::changed, this, resetAuxilliaryDatasets); } void DatasetsAction::connectToPublicAction(WidgetAction* publicAction, bool recursive) @@ -101,4 +107,68 @@ QVariantMap DatasetsAction::toVariantMap() const _colorDatasetPickerAction.insertIntoVariantMap(variantMap); return variantMap; -} \ No newline at end of file +} + +void DatasetsAction::setupDatasetPickerActions(ScatterplotPlugin* scatterplotPlugin) +{ + setupPositionDatasetPickerAction(scatterplotPlugin); + setupColorDatasetPickerAction(scatterplotPlugin); +} + +void DatasetsAction::setupPositionDatasetPickerAction(ScatterplotPlugin* scatterplotPlugin) +{ + _positionDatasetPickerAction.setFilterFunction([this](mv::Dataset dataset) -> bool { + return dataset->getDataType() == PointType; + }); + + connect(&_positionDatasetPickerAction, &DatasetPickerAction::datasetPicked, [this, scatterplotPlugin](Dataset pickedDataset) -> void { + scatterplotPlugin->getPositionDataset() = pickedDataset; + }); + + connect(&scatterplotPlugin->getPositionDataset(), &Dataset::changed, this, [this](DatasetImpl* dataset) -> void { + _positionDatasetPickerAction.setCurrentDataset(dataset); + }); +} + +void DatasetsAction::setupColorDatasetPickerAction(ScatterplotPlugin* scatterplotPlugin) +{ + auto& settingsAction = *dynamic_cast(parent()); + + _colorDatasetPickerAction.setFilterFunction([this, scatterplotPlugin](mv::Dataset dataset) -> bool { + if (!(dataset->getDataType() == PointType || dataset->getDataType() == ColorType || dataset->getDataType() == ClusterType)) + return false; + + const auto positionDataset = scatterplotPlugin->getPositionDataset(); + + if (!positionDataset.isValid()) + return false; + + return true; + }); + + auto& coloringAction = settingsAction.getColoringAction(); + + connect(&_colorDatasetPickerAction, &DatasetPickerAction::datasetPicked, [this, &coloringAction, scatterplotPlugin](Dataset pickedDataset) -> void { + if (_colorDataset.isValid()) + disconnect(&_colorDataset, &Dataset<>::guiNameChanged, this, nullptr); + + _colorDataset = pickedDataset; + + connect(&_colorDataset, &Dataset<>::guiNameChanged, scatterplotPlugin, &ScatterplotPlugin::updateHeadsUpDisplay); + + coloringAction.setCurrentColorDataset(pickedDataset); + + if (!pickedDataset.isValid()) + coloringAction.getColorByAction().setCurrentIndex(0); + }); + + connect(&settingsAction.getColoringAction(), &ColoringAction::currentColorDatasetChanged, this, [this](Dataset currentColorDataset) -> void { + _colorDatasetPickerAction.setCurrentDataset(currentColorDataset); + }); +} + +void DatasetsAction::invalidateDatasetPickerActionFilters() +{ + _positionDatasetPickerAction.invalidateFilter(); + _colorDatasetPickerAction.invalidateFilter(); +} diff --git a/src/DatasetsAction.h b/src/DatasetsAction.h index 1730a06..89a666b 100644 --- a/src/DatasetsAction.h +++ b/src/DatasetsAction.h @@ -20,6 +20,8 @@ class DatasetsAction : public GroupAction */ Q_INVOKABLE DatasetsAction(QObject* parent, const QString& title); + mv::Dataset getColorDataset() { return _colorDataset; } + protected: // Linking /** @@ -39,7 +41,7 @@ class DatasetsAction : public GroupAction /** * Load widget action from variant map - * @param Variant map representation of the widget action + * @param variantMap Variant map representation of the widget action */ void fromVariantMap(const QVariantMap& variantMap) override; @@ -49,17 +51,42 @@ class DatasetsAction : public GroupAction */ QVariantMap toVariantMap() const override; +protected: // Dataset picker action setup + + /** + * Set up the dataset picker actions with the datasets from the scatter plot plugin + * @param scatterplotPlugin Pointer to scatter plot plugin whose datasets are used to populate the dataset picker actions + */ + void setupDatasetPickerActions(ScatterplotPlugin* scatterplotPlugin); + + /** + * Set up the position dataset picker action with the position datasets from the scatter plot plugin + * @param scatterplotPlugin Pointer to scatter plot plugin whose position datasets are used to populate the dataset picker action + */ + void setupPositionDatasetPickerAction(ScatterplotPlugin* scatterplotPlugin); + + /** + * Set up the color dataset picker action with the color datasets from the scatter plot plugin + * @param scatterplotPlugin Pointer to scatter plot plugin whose color datasets are used to populate the dataset picker action + */ + void setupColorDatasetPickerAction(ScatterplotPlugin* scatterplotPlugin); + + /** Update the filters of the dataset picker actions based on the current datasets in the scatter plot plugin */ + void invalidateDatasetPickerActionFilters(); + public: // Action getters DatasetPickerAction& getPositionDatasetPickerAction() { return _positionDatasetPickerAction; } DatasetPickerAction& getColorDatasetPickerAction() { return _colorDatasetPickerAction; } private: - ScatterplotPlugin* _scatterplotPlugin; /** Pointer to scatter plot plugin */ - DatasetPickerAction _positionDatasetPickerAction; /** Dataset picker action for position dataset */ - DatasetPickerAction _colorDatasetPickerAction; /** Dataset picker action for color dataset */ + ScatterplotPlugin* _scatterplotPlugin; /** Pointer to scatter plot plugin */ + DatasetPickerAction _positionDatasetPickerAction; /** Dataset picker action for position dataset */ + DatasetPickerAction _colorDatasetPickerAction; /** Dataset picker action for color dataset */ + mv::Dataset _colorDataset; /** Smart pointer to dataset used for coloring (if any) */ friend class mv::AbstractActionsManager; + friend class ScatterplotPlugin; }; Q_DECLARE_METATYPE(DatasetsAction) diff --git a/src/DensityPlotAction.cpp b/src/DensityPlotAction.cpp index 114d806..00fde50 100644 --- a/src/DensityPlotAction.cpp +++ b/src/DensityPlotAction.cpp @@ -30,7 +30,7 @@ void DensityPlotAction::initialize(ScatterplotPlugin* scatterplotPlugin) _scatterplotPlugin = scatterplotPlugin; const auto computeDensity = [this]() -> void { - if (static_cast(_scatterplotPlugin->getSettingsAction().getRenderModeAction().getCurrentIndex()) == ScatterplotWidget::RenderMode::SCATTERPLOT) + if (static_cast(dynamic_cast(parent()->parent())->getRenderModeAction().getCurrentIndex()) == ScatterplotWidget::RenderMode::SCATTERPLOT) return; _scatterplotPlugin->getScatterplotWidget().setSigma(_sigmaAction.getValue()); @@ -38,7 +38,7 @@ void DensityPlotAction::initialize(ScatterplotPlugin* scatterplotPlugin) const auto maxDensity = _scatterplotPlugin->getScatterplotWidget().getDensityRenderer().getMaxDensity(); if (maxDensity > 0) - _scatterplotPlugin->getSettingsAction().getColoringAction().getColorMap1DAction().getRangeAction(ColorMapAction::Axis::X).setRange({ 0.0f, maxDensity }); + dynamic_cast(parent()->parent())->getColoringAction().getColorMap1DAction().getRangeAction(ColorMapAction::Axis::X).setRange({ 0.0f, maxDensity }); }; connect(&_sigmaAction, &DecimalAction::valueChanged, this, computeDensity); @@ -59,7 +59,7 @@ void DensityPlotAction::initialize(ScatterplotPlugin* scatterplotPlugin) computeDensity(); }); - connect(&_scatterplotPlugin->getSettingsAction().getRenderModeAction(), &OptionAction::currentIndexChanged, this, computeDensity); + connect(&dynamic_cast(parent()->parent())->getRenderModeAction(), &OptionAction::currentIndexChanged, this, computeDensity); updateSigmaAction(); computeDensity(); diff --git a/src/MappingUtils.cpp b/src/MappingUtils.cpp index e238064..5483a24 100644 --- a/src/MappingUtils.cpp +++ b/src/MappingUtils.cpp @@ -14,7 +14,7 @@ #include #include -std::pair getSelectionMapping(const mv::Dataset& source, const mv::Dataset& target, LinkedDataCondition checkMapping) { +std::pair getSelectionMapping(const mv::Dataset& source, const mv::Dataset& target, LinkedDataCondition checkMapping) { const std::vector& linkedDatas = source->getLinkedData(); if (linkedDatas.empty()) @@ -34,18 +34,18 @@ std::pair getSelectionMapping(const mv::Dat return { nullptr, 0 }; } -std::pair getSelectionMappingColorsToPositions(const mv::Dataset& colors, const mv::Dataset& positions) { - auto testTargetAndParent = [](const mv::LinkedData& linkedData, const mv::Dataset& positions) -> bool { +std::pair getSelectionMappingColorsToPositions(const mv::Dataset& colors, const mv::Dataset& positions) { + auto testTargetAndParent = [](const mv::LinkedData& linkedData, const mv::Dataset& positions_) -> bool { const mv::Dataset mapTargetData = linkedData.getTargetDataset(); - return mapTargetData == positions || parentHasSameNumPoints(mapTargetData, positions); + return mapTargetData == positions_ || parentHasSameNumPoints(mapTargetData, positions_); }; return getSelectionMapping(colors, positions, testTargetAndParent); } -std::pair getSelectionMappingPositionsToColors(const mv::Dataset& positions, const mv::Dataset& colors) { - auto testTarget = [](const mv::LinkedData& linkedData, const mv::Dataset& colors) -> bool { - return linkedData.getTargetDataset() == colors; +std::pair getSelectionMappingPositionsToColors(const mv::Dataset& positions, const mv::Dataset& colors) { + auto testTarget = [](const mv::LinkedData& linkedData, const mv::Dataset& colors_) -> bool { + return linkedData.getTargetDataset() == colors_; }; auto [mapping, numTargetPoints] = getSelectionMapping(positions, colors, testTarget); @@ -58,7 +58,7 @@ std::pair getSelectionMappingPositionsToCol return { mapping, numTargetPoints }; } -std::pair getSelectionMappingPositionSourceToColors(const mv::Dataset& positions, const mv::Dataset& colors) { +std::pair getSelectionMappingPositionSourceToColors(const mv::Dataset& positions, const mv::Dataset& colors) { if (!positions->isDerivedData()) return { nullptr, 0 }; @@ -77,7 +77,7 @@ bool checkSurjectiveMapping(const mv::LinkedData& linkedData, const std::uint32_ std::uint32_t count = 0; for (const auto& [key, vec] : linkedMap) { - for (std::uint32_t val : vec) { + for (const std::uint32_t val : vec) { if (val >= numPointsInTarget) continue; // Skip values that are too large if (!found[val]) { diff --git a/src/MappingUtils.h b/src/MappingUtils.h index 62a4cba..6229dcb 100644 --- a/src/MappingUtils.h +++ b/src/MappingUtils.h @@ -39,17 +39,17 @@ using LinkedDataCondition = std::function getSelectionMapping(const mv::Dataset& source, const mv::Dataset& target, LinkedDataCondition checkMapping); +std::pair getSelectionMapping(const mv::Dataset& source, const mv::Dataset& target, LinkedDataCondition checkMapping); // Returns a mapping (linked data) from colors whose target is positions or whose target's parent has the same number of points as positions -std::pair getSelectionMappingColorsToPositions(const mv::Dataset& colors, const mv::Dataset& positions); +std::pair getSelectionMappingColorsToPositions(const mv::Dataset& colors, const mv::Dataset& positions); // Returns a mapping (linked data) from positions whose target is colors or // a mapping from positions' parent whose target is colors if the number of data points match -std::pair getSelectionMappingPositionsToColors(const mv::Dataset& positions, const mv::Dataset& colors); +std::pair getSelectionMappingPositionsToColors(const mv::Dataset& positions, const mv::Dataset& colors); // Returns a mapping (linked data) from positions' source data whose target is colors -std::pair getSelectionMappingPositionSourceToColors(const mv::Dataset& positions, const mv::Dataset& colors); +std::pair getSelectionMappingPositionSourceToColors(const mv::Dataset& positions, const mv::Dataset& colors); // Check if the mapping is surjective, i.e. hits all elements in the target bool checkSurjectiveMapping(const mv::LinkedData& linkedData, const std::uint32_t numPointsInTarget); diff --git a/src/PointPlotAction.cpp b/src/PointPlotAction.cpp index e5c33ab..2425ab6 100644 --- a/src/PointPlotAction.cpp +++ b/src/PointPlotAction.cpp @@ -162,6 +162,10 @@ void PointPlotAction::initialize(ScatterplotPlugin* scatterplotPlugin) connect(&_opacityAction, &ScalarAction::magnitudeChanged, this, &PointPlotAction::updateScatterPlotWidgetPointOpacityScalars); connect(&_opacityAction, &ScalarAction::offsetChanged, this, &PointPlotAction::updateScatterPlotWidgetPointOpacityScalars); connect(&_opacityAction, &ScalarAction::sourceSelectionChanged, this, &PointPlotAction::updateScatterPlotWidgetPointOpacityScalars); + connect(&_opacityAction.getSourceDatasetPickerAction(), &DatasetPickerAction::datasetPicked, this, [this](Dataset<> picked) -> void { + setCurrentPointOpacityDataset(Dataset(picked)); + }); + connect(&_opacityAction, &ScalarAction::sourceDataChanged, this, &PointPlotAction::updateScatterPlotWidgetPointOpacityScalars); connect(&_opacityAction, &ScalarAction::scalarRangeChanged, this, &PointPlotAction::updateScatterPlotWidgetPointOpacityScalars); } diff --git a/src/ScalarAction.cpp b/src/ScalarAction.cpp index 0c8ef8d..24f86b6 100644 --- a/src/ScalarAction.cpp +++ b/src/ScalarAction.cpp @@ -9,7 +9,8 @@ using namespace mv::gui; ScalarAction::ScalarAction(QObject* parent, const QString& title, const float& minimum /*= 0.0f*/, const float& maximum /*= 100.0f*/, const float& value /*= 0.0f*/) : GroupAction(parent, title), _magnitudeAction(this, title, minimum, maximum, value), - _sourceAction(this, QString("%1 source").arg(title)) + _sourceAction(this, QString("%1 source").arg(title)), + _sourceDatasetPickerAction(this, "Source dataset") { setDefaultWidgetFlags(GroupAction::Horizontal); setShowLabels(false); @@ -17,8 +18,60 @@ ScalarAction::ScalarAction(QObject* parent, const QString& title, const float& m addAction(&_magnitudeAction); addAction(&_sourceAction); + _sourceDatasetPickerAction.setDefaultWidgetFlag(OptionAction::Clearable); + connect(&_sourceAction.getPickerAction(), &OptionAction::currentIndexChanged, this, [this](const std::uint32_t& currentIndex) { - emit sourceSelectionChanged(currentIndex); + bool emitSourceSelectionChanged = true; + + if (currentIndex >= ScalarSourceModel::DefaultRow::DatasetStart) { + _sourceDatasetPickerAction.setCurrentDataset(_sourceAction.getModel().getDataset(currentIndex)); + + if (auto scatterplotPlugin = dynamic_cast(findPluginAncestor())) { + auto positionDataset = scatterplotPlugin->getPositionDataset(); + auto scalarSourcePointsDataset = Dataset(getCurrentDataset()); + + if (scalarSourcePointsDataset.isValid() && positionDataset.isValid()) { + const auto numScalars = scalarSourcePointsDataset->getNumPoints(); + const auto numPositions = positionDataset->getNumPoints(); + + if (numScalars != numPositions) { + emitSourceSelectionChanged = false; + + scatterplotPlugin->addNotification(QString("The number of points in the scalar source dataset does not match the number of points in the position dataset. (numPositions=%1, numScalars:%2)").arg(QString::number(numPositions), QString::number(numScalars))); + } + } + } + } else { + _sourceDatasetPickerAction.setCurrentIndex(-1); + } + + if (emitSourceSelectionChanged) + emit sourceSelectionChanged(currentIndex); + }); + + connect(&_sourceDatasetPickerAction, &DatasetPickerAction::datasetPicked, this, [this](const Dataset<>& dataset) { + if (_currentDataset.isValid()) { + disconnect(&_currentDataset, &Dataset<>::dataChanged, this, nullptr); + disconnect(&_currentDataset, &Dataset<>::guiNameChanged, this, nullptr); + } + + _currentDataset = dataset; + + const auto datasetRowIndex = _sourceAction.getModel().getRowIndex(_currentDataset); + + _sourceAction.getPickerAction().setCurrentIndex(_currentDataset.isValid() ? datasetRowIndex : ScalarSourceModel::DefaultRow::Constant); + + if (!_currentDataset.isValid()) + return; + + connect(&_currentDataset, &Dataset<>::dataChanged, this, [this]() -> void { + emit sourceDataChanged(getCurrentDataset()); + }); + + connect(&_currentDataset, &Dataset<>::guiNameChanged, this, [this]() -> void { + if (auto scatterplotPlugin = dynamic_cast(findPluginAncestor())) + scatterplotPlugin->updateHeadsUpDisplay(); + }); }); connect(&_magnitudeAction, &DecimalAction::valueChanged, this, [this](const float& value) { @@ -43,6 +96,7 @@ void ScalarAction::addDataset(const Dataset& dataset) sourceModel.addDataset(dataset); + /* TODO: this connection is not removed when the dataset is removed from the model, but that should not cause any issues since the dataset will be invalid and the connection will not do anything in that case connect(&sourceModel.getDatasets().last(), &Dataset::dataChanged, this, [this, dataset]() { const auto currentDataset = getCurrentDataset(); @@ -55,6 +109,8 @@ void ScalarAction::addDataset(const Dataset& dataset) emit sourceDataChanged(dataset); }); +*/ + connect(&_magnitudeAction, &DecimalAction::valueChanged, this, [this, dataset](const float& value) { emit magnitudeChanged(value); }); @@ -67,25 +123,15 @@ void ScalarAction::removeAllDatasets() Dataset ScalarAction::getCurrentDataset() { - auto& scalarSourceModel = _sourceAction.getModel(); - - const auto currentSourceIndex = _sourceAction.getPickerAction().getCurrentIndex(); - - if (currentSourceIndex < ScalarSourceModel::DefaultRow::DatasetStart) - return Dataset(); - - return scalarSourceModel.getDataset(currentSourceIndex); + return _sourceDatasetPickerAction.getCurrentDataset(); } void ScalarAction::setCurrentDataset(const Dataset& dataset) { - const auto datasetRowIndex = _sourceAction.getModel().rowIndex(dataset); - - if (datasetRowIndex >= 0) - _sourceAction.getPickerAction().setCurrentIndex(datasetRowIndex); + _sourceDatasetPickerAction.setCurrentDataset(dataset); } -void ScalarAction::setCurrentSourceIndex(bool sourceIndex) +void ScalarAction::setCurrentSourceIndex(std::int32_t sourceIndex) { _sourceAction.getPickerAction().setCurrentIndex(sourceIndex); } @@ -141,6 +187,7 @@ void ScalarAction::fromVariantMap(const QVariantMap& variantMap) _magnitudeAction.fromParentVariantMap(variantMap); _sourceAction.fromParentVariantMap(variantMap); + //_sourceDatasetPickerAction.fromParentVariantMap(variantMap); } QVariantMap ScalarAction::toVariantMap() const @@ -149,6 +196,7 @@ QVariantMap ScalarAction::toVariantMap() const _magnitudeAction.insertIntoVariantMap(variantMap); _sourceAction.insertIntoVariantMap(variantMap); + //_sourceDatasetPickerAction.insertIntoVariantMap(variantMap); return variantMap; -} \ No newline at end of file +} diff --git a/src/ScalarAction.h b/src/ScalarAction.h index 39ee126..35e92bb 100644 --- a/src/ScalarAction.h +++ b/src/ScalarAction.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include "ScalarSourceAction.h" @@ -51,7 +52,7 @@ class ScalarAction : public GroupAction * Set the current source index * @param sourceIndex Source index */ - void setCurrentSourceIndex(bool sourceIndex); + void setCurrentSourceIndex(std::int32_t sourceIndex); /** Determines whether the scalar source is a constant */ bool isSourceConstant() const; @@ -81,7 +82,7 @@ class ScalarAction : public GroupAction /** * Load widget action from variant map - * @param Variant map representation of the widget action + * @param variantMap Variant map representation of the widget action */ void fromVariantMap(const QVariantMap& variantMap) override; @@ -96,11 +97,13 @@ class ScalarAction : public GroupAction DecimalAction& getMagnitudeAction() { return _magnitudeAction; } ScalarSourceAction& getSourceAction() { return _sourceAction; } + const DatasetPickerAction& getSourceDatasetPickerAction() { return _sourceDatasetPickerAction; } + signals: /** * Signals that the source selection changed - * @param sourceIndex Index of the selected source (0 is constant, 1 is selection, above is a dataset) + * @param sourceSelectionIndex Index of the selected source (0 is constant, 1 is selection, above is a dataset) */ void sourceSelectionChanged(const std::uint32_t& sourceSelectionIndex); @@ -125,13 +128,15 @@ class ScalarAction : public GroupAction /** * Signals that the scalar offset changed - * @param magnitude Scalar magnitude + * @param offset Scalar offset */ void offsetChanged(const float& offset); private: - DecimalAction _magnitudeAction; /** Scalar magnitude action */ - ScalarSourceAction _sourceAction; /** Scalar source action */ + DatasetPickerAction _sourceDatasetPickerAction; /** Dataset picker action for source dataset selection */ + DecimalAction _magnitudeAction; /** Scalar magnitude action */ + ScalarSourceAction _sourceAction; /** Scalar source action */ + Dataset<> _currentDataset; /** Cached current dataset (if any) */ friend class mv::AbstractActionsManager; }; diff --git a/src/ScalarSourceAction.h b/src/ScalarSourceAction.h index 5387c31..c4e808a 100644 --- a/src/ScalarSourceAction.h +++ b/src/ScalarSourceAction.h @@ -54,7 +54,7 @@ class ScalarSourceAction : public GroupAction /** * Load widget action from variant map - * @param Variant map representation of the widget action + * @param variantMap Variant map representation of the widget action */ void fromVariantMap(const QVariantMap& variantMap) override; diff --git a/src/ScalarSourceModel.cpp b/src/ScalarSourceModel.cpp index d3220ce..3d0657d 100644 --- a/src/ScalarSourceModel.cpp +++ b/src/ScalarSourceModel.cpp @@ -8,189 +8,236 @@ using namespace mv; using namespace mv::util; ScalarSourceModel::ScalarSourceModel(QObject* parent /*= nullptr*/) : - QAbstractListModel(parent), + QStandardItemModel(parent), _showFullPathName(true) { + appendRow(Row(*this, {})); // Constant source + appendRow(Row(*this, {})); // Selection source } -int ScalarSourceModel::rowCount(const QModelIndex& parent /*= QModelIndex()*/) const +ScalarSourceModel::Item::Item(const ScalarSourceModel& scalarSourceModel, const mv::Dataset<>& scalarDataset) : + _scalarSourceModel(scalarSourceModel), + _scalarDataset(scalarDataset) { - // Constant point size option plus the number of available datasets - return _datasets.count() + DefaultRow::DatasetStart; } -int ScalarSourceModel::rowIndex(const Dataset& dataset) const +QVariant ScalarSourceModel::Item::data(int role) const { - // Only proceed if we have a valid dataset - if (!dataset.isValid()) - return -1; + const auto rowIndex = row(); + + switch (role) + { + // Return ruler icon for constant point size and dataset icon otherwise + case Qt::DecorationRole: + { + if (rowIndex == DefaultRow::Constant) + return StyledIcon("ruler"); + + if (rowIndex == DefaultRow::Selection) + return StyledIcon("mouse-pointer"); + + if (rowIndex >= DefaultRow::DatasetStart) + return _scalarDataset->icon(); + + break; + } - // Return the index of the dataset and add one for the constant point size option - return _datasets.indexOf(dataset) + DefaultRow::DatasetStart; + // Return 'Constant' for constant point size and dataset (full path) GUI name otherwise + case Qt::DisplayRole: + { + if (rowIndex >= DefaultRow::DatasetStart) + { + if (rowIndex == 2) + return _scalarDataset->text(); + else + return getScalarSourceModel().getShowFullPathName() ? getScalarDataset()->getLocation() : getScalarDataset()->getGuiName(); + } + else { + if (rowIndex == DefaultRow::Constant) + return "Constant"; + + if (rowIndex == DefaultRow::Selection) + return "Selection"; + } + } + + default: + break; + } + + return {}; } -int ScalarSourceModel::columnCount(const QModelIndex& parent /*= QModelIndex()*/) const +const ScalarSourceModel& ScalarSourceModel::Item::getScalarSourceModel() const { - return 1; + return _scalarSourceModel; } -QVariant ScalarSourceModel::data(const QModelIndex& index, int role) const +const mv::Dataset<>& ScalarSourceModel::Item::getScalarDataset() const { - // Get row/column of and smart pointer to the dataset - const auto row = index.row(); - const auto column = index.column(); - const auto scalarDataset = getDataset(row); + return _scalarDataset; +} + +ScalarSourceModel::NameItem::NameItem(const ScalarSourceModel& scalarSourceModel, const mv::Dataset<>& scalarDataset) : + Item(scalarSourceModel, scalarDataset) +{ + connect(&const_cast&>(getScalarDataset()), &Dataset<>::guiNameChanged, this, [this]() { + emitDataChanged(); + }); +} + +QVariant ScalarSourceModel::NameItem::data(int role) const +{ + const auto rowIndex = row(); switch (role) { - // Return ruler icon for constant point size and dataset icon otherwise - case Qt::DecorationRole: - { - if (row == DefaultRow::Constant) - return StyledIcon("ruler"); + case Qt::DisplayRole: + case Qt::EditRole: + { + if (rowIndex == DefaultRow::Constant) + return "Constant"; - if (row == DefaultRow::Selection) - return StyledIcon("mouse-pointer"); + if (rowIndex == DefaultRow::Selection) + return "Selection"; - if (row >= DefaultRow::DatasetStart) - return scalarDataset->icon(); + if (rowIndex >= DefaultRow::DatasetStart) + return getScalarDataset()->getGuiName(); break; - } + } - // Return 'Constant' for constant point size and dataset (full path) GUI name otherwise - case Qt::DisplayRole: + case Qt::ToolTipRole: { - if (row >= DefaultRow::DatasetStart) - { - if (row == 2) - return scalarDataset->text(); - else - return _showFullPathName ? scalarDataset->getLocation() : scalarDataset->text(); - } else { - if (row == DefaultRow::Constant) - return "Constant"; - - if (row == DefaultRow::Selection) - return "Selection"; - } + if (rowIndex == DefaultRow::Constant) + return "Constant"; + + if (rowIndex == DefaultRow::Selection) + return "Selection"; + + if (rowIndex >= DefaultRow::DatasetStart) + return getScalarDataset()->getLocation(); break; } - default: - break; + default: + break; } - return QVariant(); + return Item::data(role); } -void ScalarSourceModel::addDataset(const Dataset& dataset) +QVariant ScalarSourceModel::IdItem::data(int role) const { - // Avoid duplicates - if (hasDataset(dataset)) - return; + const auto rowIndex = row(); - // Insert row into model - beginInsertRows(QModelIndex(), rowCount(), rowCount()); + switch (role) { - // Add the dataset - _datasets << dataset; - } - endInsertRows(); + case Qt::DisplayRole: + case Qt::EditRole: + { + if (rowIndex == DefaultRow::Constant || rowIndex == DefaultRow::Selection) + return ""; - // Get smart pointer to last added dataset - auto& addedDataset = _datasets.last(); + if (rowIndex >= DefaultRow::DatasetStart) + return getScalarDataset()->getId(); - // Remove a dataset from the model when it is about to be deleted - connect(&addedDataset, &Dataset::aboutToBeRemoved, this, [this, &addedDataset]() { - removeDataset(addedDataset); - }); + break; + } - // Notify others that the model has updated when the dataset GUI name changes - connect(addedDataset.get(), &DatasetImpl::textChanged, this, [this, &addedDataset]() { + case Qt::ToolTipRole: + { + if (rowIndex == DefaultRow::Constant || rowIndex == DefaultRow::Selection) + return ""; - // Get row index of the dataset - const auto colorDatasetRowIndex = rowIndex(addedDataset); + if (rowIndex >= DefaultRow::DatasetStart) + return "ID: " + getScalarDataset()->getId(); - // Only proceed if we found a valid row index - if (colorDatasetRowIndex < 0) - return; + break; + } - // Establish model index - const auto modelIndex = index(colorDatasetRowIndex, 0); + default: + break; + } - // Only proceed if we have a valid model index - if (!modelIndex.isValid()) - return; + return Item::data(role); +} - // Notify others that the data changed - emit dataChanged(modelIndex, modelIndex); - }); +void ScalarSourceModel::addDataset(const Dataset& dataset) +{ + if (hasDataset(dataset)) + return; + + appendRow(Row(*this, dataset)); } bool ScalarSourceModel::hasDataset(const Dataset& dataset) const { - return rowIndex(dataset) >= DefaultRow::DatasetStart; + if (!dataset.isValid()) + return false; + else + return !match(index(0, static_cast(Column::Id)), Qt::EditRole, dataset->getId(), 1, Qt::MatchExactly).isEmpty(); } void ScalarSourceModel::removeDataset(const Dataset& dataset) { - // Get row index of the dataset - const auto datasetRowIndex = rowIndex(dataset); + if (!hasDataset(dataset)) + return; - // Update model - beginRemoveRows(QModelIndex(), datasetRowIndex, datasetRowIndex); - { - // Remove dataset from internal vector - _datasets.removeOne(dataset); - } - endRemoveRows(); + const auto matches = match(index(0, static_cast(Column::Id)), Qt::EditRole, dataset->getId(), 1, Qt::MatchExactly); + + if (!matches.isEmpty()) + removeRow(matches.first().row()); } void ScalarSourceModel::removeAllDatasets() { - // Remove row from model - beginRemoveRows(QModelIndex(), 0, rowCount() - DefaultRow::DatasetStart); - { - // Remove all datasets - _datasets.clear(); - } - endRemoveRows(); - - // And update model data with altered datasets - updateData(); + removeRows(DefaultRow::DatasetStart, rowCount() - DefaultRow::DatasetStart); } -const Datasets& ScalarSourceModel::getDatasets() const +Datasets ScalarSourceModel::getDatasets() const { - return _datasets; + Datasets datasets; + + for (int rowIndex = DefaultRow::DatasetStart; rowIndex < rowCount(); ++rowIndex) + { + if (auto item = dynamic_cast(itemFromIndex(index(rowIndex, 0)))) { + const auto dataset = item->getScalarDataset(); + + if (dataset.isValid()) + datasets.append(dataset); + } + } + + return datasets; } Dataset ScalarSourceModel::getDataset(const std::int32_t& rowIndex) const { - // Return empty smart pointer when out of range - if (rowIndex < DefaultRow::DatasetStart || rowIndex > (DefaultRow::DatasetStart + _datasets.count())) - return Dataset(); + if (auto item = dynamic_cast(itemFromIndex(index(rowIndex, 0)))) + return item->getScalarDataset(); - // Subtract the constant point size row - return _datasets[rowIndex - DefaultRow::DatasetStart]; + return {}; } void ScalarSourceModel::setDatasets(const Datasets& datasets) { - // Notify others that the model layout is about to be changed - emit layoutAboutToBeChanged(); - - // Add datasets for (const auto& dataset : datasets) addDataset(dataset); +} + +std::int32_t ScalarSourceModel::getRowIndex(const Dataset& dataset) const +{ + if (!dataset.isValid()) + return -1; + + const auto matches = match(index(0, static_cast(Column::Id)), Qt::EditRole, dataset->getId(), 1, Qt::MatchExactly); - // Notify others that the model layout is changed - emit layoutChanged(); + if (!matches.isEmpty()) + return matches.first().row(); - // And update model data with datasets - updateData(); + return -1; } bool ScalarSourceModel::getShowFullPathName() const @@ -201,23 +248,4 @@ bool ScalarSourceModel::getShowFullPathName() const void ScalarSourceModel::setShowFullPathName(const bool& showFullPathName) { _showFullPathName = showFullPathName; - - updateData(); -} - -void ScalarSourceModel::updateData() -{ - // Update the datasets string list model - for (auto dataset : _datasets) { - - // Continue if the dataset is not valid - if (!dataset.isValid()) - continue; - - // Get dataset model index - const auto datasetModelIndex = index(_datasets.indexOf(dataset), 0); - - // Notify others that the data changed - emit dataChanged(datasetModelIndex, datasetModelIndex); - } } diff --git a/src/ScalarSourceModel.h b/src/ScalarSourceModel.h index 9fd86e7..b6eb766 100644 --- a/src/ScalarSourceModel.h +++ b/src/ScalarSourceModel.h @@ -2,7 +2,7 @@ #include "Dataset.h" -#include +#include using namespace mv; @@ -13,7 +13,7 @@ using namespace mv; * * @author Thomas Kroes */ -class ScalarSourceModel : public QAbstractListModel +class ScalarSourceModel : public QStandardItemModel { protected: @@ -21,6 +21,7 @@ class ScalarSourceModel : public QAbstractListModel ScalarSourceModel(QObject* parent = nullptr); public: + /** Default scalar options */ enum DefaultRow { Constant, /** Scale by constant */ @@ -28,36 +29,127 @@ class ScalarSourceModel : public QAbstractListModel DatasetStart /** Start row of the dataset(s) */ }; -public: + /** Model columns */ + enum class Column { + Name, /** Scalar dataset name */ + Id, /** Globally unique scalar dataset identifier */ - /** - * Get the number of row - * @param parent Parent model index - * @return Number of rows in the model - */ - int rowCount(const QModelIndex& parent = QModelIndex()) const; + Count + }; - /** - * Get the row index of a dataset - * @param parent Parent model index - * @return Row index of the dataset - */ - int rowIndex(const Dataset& dataset) const; +protected: - /** - * Get the number of columns - * @param parent Parent model index - * @return Number of columns in the model - */ - int columnCount(const QModelIndex& parent = QModelIndex()) const; + /** Base standard model item class for a dataset */ + class Item : public QStandardItem { + public: + + /** + * Construct with reference to \p scalarSourceModel and pointer to \p scalarDataset + * @param scalarSourceModel Reference to the scalar source model + * @param scalarDataset Pointer to scalar dataset (maybe nullptr) + */ + Item(const ScalarSourceModel& scalarSourceModel, const mv::Dataset& scalarDataset); + + /** + * Get model data for \p role + * @return Data for \p role in variant form + */ + QVariant data(int role = Qt::UserRole + 1) const override; + + /** + * Get reference to the scalar source model + * @return Reference to the scalar source model + */ + const ScalarSourceModel& getScalarSourceModel() const; + + /** + * Get the scalar dataset associated with this item (if any) + * @return Pointer to scalar dataset (maybe nullptr) + */ + const mv::Dataset<>& getScalarDataset() const; + + private: + const ScalarSourceModel& _scalarSourceModel; /** Reference to the scalar source model */ + mv::Dataset<> _scalarDataset; /** Pointer to scalar dataset (maybe nullptr) */ + }; - /** - * Get data - * @param index Model index to query - * @param role Data role - * @return Data - */ - QVariant data(const QModelIndex& index, int role) const; + /** Standard model item class for displaying the dataset GUI name */ + class NameItem final : public Item, public QObject { + public: + + /** + * Construct with reference to \p scalarSourceModel and pointer to \p scalarDataset + * @param scalarSourceModel Reference to the scalar source model + * @param scalarDataset Pointer to scalar dataset (maybe nullptr) + */ + NameItem(const ScalarSourceModel& scalarSourceModel, const mv::Dataset<>& scalarDataset); + + /** + * Get model data for \p role + * @return Data for \p role in variant form + */ + QVariant data(int role = Qt::UserRole + 1) const override; + + /** + * Get header data for \p orientation and \p role + * @param orientation Horizontal/vertical + * @param role Data role + * @return Header data + */ + static QVariant headerData(Qt::Orientation orientation, int role) { + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + return "Name"; + + case Qt::ToolTipRole: + return "Dataset name"; + + default: + break; + } + + return {}; + } + }; + + /** Standard model item class for displaying the dataset GUI ID */ + class IdItem final : public Item { + public: + + /** No need for specialized constructor */ + using Item::Item; + + /** + * Get model data for \p role + * @return Data for \p role in variant form + */ + QVariant data(int role = Qt::UserRole + 1) const override; + + /** + * Get header data for \p orientation and \p role + * @param orientation Horizontal/vertical + * @param role Data role + * @return Header data + */ + static QVariant headerData(Qt::Orientation orientation, int role) { + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + return "ID"; + + case Qt::ToolTipRole: + return "Dataset unique identifier"; + + default: + break; + } + + return {}; + } + }; + +public: /** * Add a dataset @@ -85,7 +177,7 @@ class ScalarSourceModel : public QAbstractListModel * Get datasets * @return Vector of smart pointers to datasets */ - const Datasets& getDatasets() const; + Datasets getDatasets() const; /** * Get dataset at the specified row index @@ -100,6 +192,13 @@ class ScalarSourceModel : public QAbstractListModel */ void setDatasets(const Datasets& datasets); + /** + * Get row index of the specified \p dataset + * @param dataset Smart pointer to dataset + * @return Row index of the dataset, or -1 if the dataset is not in the model + */ + std::int32_t getRowIndex(const Dataset& dataset) const; + /** Get whether to show the full path name in the GUI */ bool getShowFullPathName() const; @@ -109,11 +208,26 @@ class ScalarSourceModel : public QAbstractListModel */ void setShowFullPathName(const bool& showFullPathName); - /** Updates the model from the datasets */ - void updateData(); +protected: + + /** Convenience class for combining items in a row */ + class Row final : public QList + { + public: + + /** + * Construct with pointer to \p scalarDataset + * @param scalarDataset Pointer to scalar dataset (maybe nullptr) + */ + Row(const ScalarSourceModel& scalarSourceModel, const mv::Dataset<>& scalarDataset) : + QList() + { + append(new NameItem(scalarSourceModel, scalarDataset)); + append(new IdItem(scalarSourceModel, scalarDataset)); + } + }; protected: - Datasets _datasets; /** Datasets used to size the scatter plot points with */ bool _showFullPathName; /** Whether to show the full path name in the GUI */ friend class ScalarAction; diff --git a/src/ScatterplotPlugin.cpp b/src/ScatterplotPlugin.cpp index 1e35b7c..2f18488 100644 --- a/src/ScatterplotPlugin.cpp +++ b/src/ScatterplotPlugin.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -50,8 +51,8 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : _dropWidget(nullptr), _scatterPlotWidget(new ScatterplotWidget(this)), _numPoints(0), - _settingsAction(this, "Settings"), - _primaryToolbarAction(this, "Primary Toolbar") + _settingsAction(new SettingsAction(this, "Settings")), + _primaryToolbarAction(new HorizontalToolbarAction(this, "Primary Toolbar")) { setObjectName("Scatterplot"); @@ -83,25 +84,25 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : getWidget().setFocusPolicy(Qt::ClickFocus); - _primaryToolbarAction.addAction(&_settingsAction.getDatasetsAction()); - _primaryToolbarAction.addAction(&_settingsAction.getRenderModeAction(), 3, GroupAction::Horizontal); - _primaryToolbarAction.addAction(&_settingsAction.getPositionAction(), 1, GroupAction::Horizontal); - _primaryToolbarAction.addAction(&_settingsAction.getPlotAction(), 2, GroupAction::Horizontal); - _primaryToolbarAction.addAction(&_settingsAction.getColoringAction()); - _primaryToolbarAction.addAction(&_settingsAction.getSubsetAction()); - _primaryToolbarAction.addAction(&_settingsAction.getClusteringAction()); - _primaryToolbarAction.addAction(&_settingsAction.getSelectionAction()); - _primaryToolbarAction.addAction(&getSamplerAction()); + _primaryToolbarAction->addAction(&_settingsAction->getDatasetsAction()); + _primaryToolbarAction->addAction(&_settingsAction->getRenderModeAction(), 3, GroupAction::Horizontal); + _primaryToolbarAction->addAction(&_settingsAction->getPositionAction(), 1, GroupAction::Horizontal); + _primaryToolbarAction->addAction(&_settingsAction->getPlotAction(), 2, GroupAction::Horizontal); + _primaryToolbarAction->addAction(&_settingsAction->getColoringAction()); + _primaryToolbarAction->addAction(&_settingsAction->getSubsetAction()); + _primaryToolbarAction->addAction(&_settingsAction->getClusteringAction()); + _primaryToolbarAction->addAction(&_settingsAction->getSelectionAction()); + _primaryToolbarAction->addAction(&getSamplerAction()); auto focusSelectionAction = new ToggleAction(this, "Focus selection"); focusSelectionAction->setIconByName("mouse-pointer"); connect(focusSelectionAction, &ToggleAction::toggled, this, [this](bool toggled) -> void { - _settingsAction.getPlotAction().getPointPlotAction().getFocusSelection().setChecked(toggled); + _settingsAction->getPlotAction().getPointPlotAction().getFocusSelection().setChecked(toggled); }); - connect(&_settingsAction.getPlotAction().getPointPlotAction().getFocusSelection(), &ToggleAction::toggled, this, [this, focusSelectionAction](bool toggled) -> void { + connect(&_settingsAction->getPlotAction().getPointPlotAction().getFocusSelection(), &ToggleAction::toggled, this, [this, focusSelectionAction](bool toggled) -> void { focusSelectionAction->setChecked(toggled); }); @@ -114,13 +115,13 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : connect(_scatterPlotWidget, &ScatterplotWidget::renderModeChanged, this, updateReadOnly); connect(&_positionDataset, &Dataset::changed, this, updateReadOnly); - //_secondaryToolbarAction.addAction(&_settingsAction.getMiscellaneousAction()); + //_secondaryToolbarAction.addAction(&_settingsAction->getMiscellaneousAction()); connect(_scatterPlotWidget, &ScatterplotWidget::customContextMenuRequested, this, [this](const QPoint& point) { if (!_positionDataset.isValid()) return; - auto contextMenu = _settingsAction.getContextMenu(); + auto contextMenu = _settingsAction->getContextMenu(); contextMenu->addSeparator(); @@ -138,10 +139,10 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : if (datasetsMimeData == nullptr) return dropRegions; - if (datasetsMimeData->getDatasets().count() > 1) + if (datasetsMimeData->getDatasetsCount() != 1) return dropRegions; - const auto dataset = datasetsMimeData->getDatasets().first(); + const auto& dataset = datasetsMimeData->getDatasetsRef().first(); const auto datasetGuiName = dataset->text(); const auto datasetId = dataset->getId(); const auto dataType = dataset->getDataType(); @@ -165,7 +166,8 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : // Load as point positions when no dataset is currently loaded dropRegions << new DropWidget::DropRegion(this, "Point position", description, "map-marker-alt", true, [this, candidateDataset]() { _positionDataset = candidateDataset; - }); + _settingsAction->getColoringAction().setCurrentColorDataset(nullptr); + }); } else { if (_positionDataset != candidateDataset && candidateDataset->getNumDimensions() >= 2) { @@ -173,8 +175,9 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : // The number of points is equal, so offer the option to replace the existing points dataset dropRegions << new DropWidget::DropRegion(this, "Point position", description, "map-marker-alt", true, [this, candidateDataset]() { _positionDataset = candidateDataset; - }); - } + _settingsAction->getColoringAction().setCurrentColorDataset(nullptr); + }); + } // Accept for recoloring: // 1. data with the same number of points @@ -198,7 +201,7 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : if (hasSameNumPoints || hasSameNumPointsAsFull || hasSelectionMapping) { // Offer the option to use the points dataset as source for points colors dropRegions << new DropWidget::DropRegion(this, "Point color", QString("Colorize %1 points with %2").arg(_positionDataset->text(), candidateDataset->text()), "palette", true, [this, candidateDataset]() { - _settingsAction.getColoringAction().setCurrentColorDataset(candidateDataset); // calls addColorDataset internally + _settingsAction->getColoringAction().setCurrentColorDataset(candidateDataset); // calls addColorDataset internally }); } @@ -207,12 +210,12 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : if (hasSameNumPoints) { // Offer the option to use the points dataset as source for points size dropRegions << new DropWidget::DropRegion(this, "Point size", QString("Size %1 points with %2").arg(_positionDataset->text(), candidateDataset->text()), "ruler-horizontal", true, [this, candidateDataset]() { - _settingsAction.getPlotAction().getPointPlotAction().setCurrentPointSizeDataset(candidateDataset); + _settingsAction->getPlotAction().getPointPlotAction().setCurrentPointSizeDataset(candidateDataset); }); // Offer the option to use the points dataset as source for points opacity dropRegions << new DropWidget::DropRegion(this, "Point opacity", QString("Set %1 points opacity with %2").arg(_positionDataset->text(), candidateDataset->text()), "brush", true, [this, candidateDataset]() { - _settingsAction.getPlotAction().getPointPlotAction().setCurrentPointOpacityDataset(candidateDataset); + _settingsAction->getPlotAction().getPointPlotAction().setCurrentPointOpacityDataset(candidateDataset); }); } } @@ -230,11 +233,11 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : // Only allow user to color by clusters when there is a positions dataset loaded if (_positionDataset.isValid()) { - if (_settingsAction.getColoringAction().hasColorDataset(candidateDataset)) { + if (_settingsAction->getColoringAction().hasColorDataset(candidateDataset)) { // The clusters dataset is already loaded dropRegions << new DropWidget::DropRegion(this, "Color", description, "palette", true, [this, candidateDataset]() { - _settingsAction.getColoringAction().setCurrentColorDataset(candidateDataset); + _settingsAction->getColoringAction().setCurrentColorDataset(candidateDataset); }); } else { @@ -242,13 +245,13 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : { // Check to set whether the number of data points comprised throughout all clusters is the same number // as the number of data points in the dataset we are trying to color - int totalNumIndices = 0; + std::uint64_t totalNumIndices = 0; for (const Cluster& cluster : candidateDataset->getClusters()) { totalNumIndices += cluster.getIndices().size(); } - int totalNumPoints = 0; + std::uint64_t totalNumPoints = 0; if (_positionDataset->isDerivedData()) totalNumPoints = _positionSourceDataset->getFullDataset()->getNumPoints(); else @@ -258,8 +261,8 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : { // Use the clusters set for points color dropRegions << new DropWidget::DropRegion(this, "Color", description, "palette", true, [this, candidateDataset]() { - _settingsAction.getColoringAction().addColorDataset(candidateDataset); - _settingsAction.getColoringAction().setCurrentColorDataset(candidateDataset); + _settingsAction->getColoringAction().addColorDataset(candidateDataset); + _settingsAction->getColoringAction().setCurrentColorDataset(candidateDataset); }); } else @@ -280,7 +283,7 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : return dropRegions; }); - auto& selectionAction = _settingsAction.getSelectionAction(); + auto& selectionAction = _settingsAction->getSelectionAction(); getSamplerAction().initialize(this, &selectionAction.getPixelSelectionAction(), &selectionAction.getSamplerPixelSelectionAction()); getSamplerAction().getEnabledAction().setChecked(false); @@ -288,6 +291,8 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : getLearningCenterAction().addVideos(QStringList({ "Practitioner", "Developer" })); setOverlayActionsTargetWidget(_scatterPlotWidget); + + connect(&mv::projects(), &AbstractProjectManager::projectOpened, this, &ScatterplotPlugin::updateHeadsUpDisplay); } ScatterplotPlugin::~ScatterplotPlugin() @@ -302,7 +307,7 @@ void ScatterplotPlugin::init() layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); - layout->addWidget(_primaryToolbarAction.createWidget(&getWidget())); + layout->addWidget(_primaryToolbarAction->createWidget(&getWidget())); layout->addWidget(_scatterPlotWidget, 100); auto& navigationAction = _scatterPlotWidget->getPointRendererNavigator().getNavigationAction(); @@ -311,7 +316,7 @@ void ScatterplotPlugin::init() layout->addWidget(navigationWidget); layout->setAlignment(navigationWidget, Qt::AlignCenter); - navigationAction.setParent(&_settingsAction); + navigationAction.setParent(_settingsAction); } getWidget().setLayout(layout); @@ -334,18 +339,24 @@ void ScatterplotPlugin::init() connect(&getSamplerAction(), &ViewPluginSamplerAction::sampleContextRequested, this, &ScatterplotPlugin::samplePoints); connect(&_positionDataset, &Dataset::changed, this, &ScatterplotPlugin::positionDatasetChanged); - connect(&_positionDataset, &Dataset::dataChanged, this, &ScatterplotPlugin::updateData); + connect(&_positionDataset, &Dataset::dataChanged, this, [this]() -> void { + updateData(); + updateHeadsUpDisplay(); + }); connect(&_positionDataset, &Dataset::dataSelectionChanged, this, &ScatterplotPlugin::updateSelection); + connect(&_positionDataset, &Dataset<>::guiNameChanged, this, &ScatterplotPlugin::updateHeadsUpDisplay); - _scatterPlotWidget->installEventFilter(this); - - getLearningCenterAction().getViewPluginOverlayWidget()->setTargetWidget(_scatterPlotWidget); + connect(&_settingsAction->getMiscellaneousAction().getBackgroundColorAction(), &ColorAction::colorChanged, this, &ScatterplotPlugin::updateHeadsUpDisplayTextColor); connect(&getScatterplotWidget().getPointRendererNavigator().getNavigationAction().getZoomSelectionAction(), &TriggerAction::triggered, this, [this]() -> void { if (_selectionBoundaries.isValid()) _scatterPlotWidget->getPointRendererNavigator().setZoomRectangleWorld(_selectionBoundaries); }); + _scatterPlotWidget->installEventFilter(this); + + getLearningCenterAction().getViewPluginOverlayWidget()->setTargetWidget(_scatterPlotWidget); + #ifdef VIEW_SAMPLING_HTML getSamplerAction().setHtmlViewGeneratorFunction([this](const ViewPluginSamplerAction::SampleContext& toolTipContext) -> QString { QStringList localPointIndices, globalPointIndices; @@ -390,37 +401,32 @@ void ScatterplotPlugin::init() }); #endif - updateHeadsUpDisplay(); - - connect(&_positionDataset, &Dataset<>::changed, this, &ScatterplotPlugin::updateHeadsUpDisplay); - connect(&_positionDataset, &Dataset<>::guiNameChanged, this, &ScatterplotPlugin::updateHeadsUpDisplay); - connect(&_positionDataset, &Dataset<>::guiNameChanged, this, &ScatterplotPlugin::updateHeadsUpDisplay); - - const auto currentColorDatasetChanged = [this](Dataset currentColorDataset) -> void { - if (_colorDataset == currentColorDataset) - return; + auto& datasetsAction = _settingsAction->getDatasetsAction(); + auto& pointPlotAction = _settingsAction->getPlotAction().getPointPlotAction(); - if (_colorDataset.isValid()) - disconnect(&_colorDataset, &Dataset<>::guiNameChanged, this, nullptr); + connect(&datasetsAction.getPositionDatasetPickerAction(), &DatasetPickerAction::currentIndexChanged, this, &ScatterplotPlugin::updateHeadsUpDisplay); + connect(&datasetsAction.getColorDatasetPickerAction(), &DatasetPickerAction::currentIndexChanged, this, &ScatterplotPlugin::updateHeadsUpDisplay); + connect(&pointPlotAction.getSizeAction(), &ScalarAction::sourceSelectionChanged, this, &ScatterplotPlugin::updateHeadsUpDisplay); + connect(&pointPlotAction.getOpacityAction(), &ScalarAction::sourceSelectionChanged, this, &ScatterplotPlugin::updateHeadsUpDisplay); - _colorDataset = currentColorDataset; - - connect(&_colorDataset, &Dataset<>::guiNameChanged, this, &ScatterplotPlugin::updateHeadsUpDisplay); + updateHeadsUpDisplay(); + updateHeadsUpDisplayTextColor(); - updateHeadsUpDisplay(); - }; + if (mv::projects().isOpeningProject()) { + connect(&mv::projects(), &AbstractProjectManager::projectOpened, this, [this, &datasetsAction]() -> void { + datasetsAction.invalidateDatasetPickerActionFilters(); + }); + } else { + datasetsAction.invalidateDatasetPickerActionFilters(); + } - connect(&_settingsAction.getColoringAction(), &ColoringAction::currentColorDatasetChanged, this, currentColorDatasetChanged); - connect(&_settingsAction.getColoringAction().getColorByAction(), &OptionAction::currentIndexChanged, this, [this, currentColorDatasetChanged](const std::int32_t& currentIndex) -> void { - currentColorDatasetChanged(_settingsAction.getColoringAction().getCurrentColorDataset()); + connect(&_settingsAction->getPlotAction().getPointPlotAction().getSizeAction(), &ScalarAction::sourceDataChanged, this, [this]() -> void { + _scatterPlotWidget->update(); }); - connect(&_settingsAction.getPlotAction().getPointPlotAction().getSizeAction(), &ScalarAction::sourceDataChanged, this, &ScatterplotPlugin::updateHeadsUpDisplay); - connect(&_settingsAction.getPlotAction().getPointPlotAction().getOpacityAction(), &ScalarAction::sourceDataChanged, this, &ScatterplotPlugin::updateHeadsUpDisplay); - - updateHeadsUpDisplayTextColor(); - - connect(&_settingsAction.getMiscellaneousAction().getBackgroundColorAction(), &ColorAction::colorChanged, this, &ScatterplotPlugin::updateHeadsUpDisplayTextColor); + connect(&_settingsAction->getPlotAction().getPointPlotAction().getOpacityAction(), &ScalarAction::sourceDataChanged, this, [this]() -> void { + _scatterPlotWidget->update(); + }); } void ScatterplotPlugin::loadData(const Datasets& datasets) @@ -433,7 +439,7 @@ void ScatterplotPlugin::loadData(const Datasets& datasets) _positionDataset = datasets.first(); // And set the coloring mode to constant - _settingsAction.getColoringAction().getColorByAction().setCurrentIndex(0); + _settingsAction->getColoringAction().getColorByAction().setCurrentIndex(0); } void ScatterplotPlugin::createSubset(const bool& fromSourceData /*= false*/, const QString& name /*= ""*/) @@ -458,7 +464,7 @@ void ScatterplotPlugin::selectPoints() auto& pixelSelectionTool = _scatterPlotWidget->getPixelSelectionTool(); - auto renderer = _settingsAction.getRenderModeAction().getCurrentIndex() > 0 ? dynamic_cast(&_scatterPlotWidget->_densityRenderer) : dynamic_cast(&_scatterPlotWidget->_pointRenderer); + auto renderer = _settingsAction->getRenderModeAction().getCurrentIndex() > 0 ? dynamic_cast(&_scatterPlotWidget->_densityRenderer) : dynamic_cast(&_scatterPlotWidget->_pointRenderer); auto& navigator = renderer->getNavigator(); // Only proceed with a valid points position dataset and when the pixel selection tool is active @@ -564,7 +570,7 @@ void ScatterplotPlugin::selectPoints() auto& navigationAction = navigator.getNavigationAction(); - navigationAction.getZoomSelectionAction().setEnabled(!targetSelectionIndices.empty() && !navigationAction.getFreezeNavigation().isChecked()); + navigationAction.getZoomSelectionAction().setEnabled(!targetSelectionIndices.empty() && navigationAction.isNavigationActive()); _positionDataset->setSelectionIndices(targetSelectionIndices); @@ -657,11 +663,11 @@ void ScatterplotPlugin::samplePoints() _scatterPlotWidget->update(); - auto& coloringAction = _settingsAction.getColoringAction(); + auto& coloringAction = _settingsAction->getColoringAction(); getSamplerAction().setSampleContext({ { "PositionDatasetID", _positionDataset.getDatasetId() }, - { "ColorDatasetID", _settingsAction.getColoringAction().getCurrentColorDataset().getDatasetId() }, + { "ColorDatasetID", _settingsAction->getColoringAction().getCurrentColorDataset().getDatasetId() }, { "LocalPointIndices", localPointIndices }, { "GlobalPointIndices", globalPointIndices }, { "Distances", distances }, @@ -670,7 +676,7 @@ void ScatterplotPlugin::samplePoints() { "ColorMap1D", coloringAction.getColorMap1DAction().getColorMapImage() }, { "ColorMap2D", coloringAction.getColorMap2DAction().getColorMapImage() }, { "ColorDimensionIndex", coloringAction.getDimensionAction().getCurrentDimensionAction().getCurrentIndex() }, - { "RenderMode", _settingsAction.getRenderModeAction().getCurrentText() } + { "RenderMode", _settingsAction->getRenderModeAction().getCurrentText() } }); } @@ -707,16 +713,16 @@ void ScatterplotPlugin::positionDatasetChanged() updateData(); } -void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std::uint32_t& dimensionIndex) +bool ScatterplotPlugin::mapColorScalars(const Dataset& pointsColor, const std::uint32_t& dimensionIndex, std::vector& colorScalars) { // Only proceed with valid points dataset if (!pointsColor.isValid()) - return; + return false; const auto numColorPoints = pointsColor->getNumPoints(); // Generate point colorScalars for color mapping - std::vector colorScalars = {}; + colorScalars.clear(); pointsColor->extractDataForDimension(colorScalars, dimensionIndex); // If number of points do not match, use a mapping @@ -750,7 +756,7 @@ void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std const mv::SelectionMap::Map& mapColorsToPositions = selectionMapping->getMapping().getMap(); for (const auto& [fromColorID, vecOfPositionIDs] : mapColorsToPositions) { - for (std::uint32_t toPositionID : vecOfPositionIDs) { + for (const std::uint32_t toPositionID : vecOfPositionIDs) { mappedColorScalars[toPositionID] = colorScalars[fromColorID]; } } @@ -770,7 +776,7 @@ void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std for (const auto& [fromPositionID, vecOfColorIDs] : mapPositionsToColors) { if (mappedColorScalars[fromPositionID] != std::numeric_limits::lowest()) continue; - for (std::uint32_t toColorID : vecOfColorIDs) { + for (const std::uint32_t toColorID : vecOfColorIDs) { mappedColorScalars[fromPositionID] = colorScalars[toColorID]; } } @@ -809,14 +815,12 @@ void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std } catch (const std::exception& e) { - qDebug() << "ScatterplotPlugin::loadColors: mapping failed -> " << e.what(); - _settingsAction.getColoringAction().getColorByAction().setCurrentIndex(0); // reset to color by constant - return; + qDebug() << "ScatterplotPlugin::mapColorScalars: mapping failed -> " << e.what(); + return false; } catch (...) { - qDebug() << "ScatterplotPlugin::loadColors: mapping failed for an unknown reason."; - _settingsAction.getColoringAction().getColorByAction().setCurrentIndex(0); // reset to color by constant - return; + qDebug() << "ScatterplotPlugin::mapColorScalars: mapping failed for an unknown reason."; + return false; } std::swap(mappedColorScalars, colorScalars); @@ -824,11 +828,66 @@ void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std assert(colorScalars.size() == _numPoints); + return true; +} + +void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std::uint32_t& dimensionIndex) +{ + std::vector colorScalars = {}; + + if (!mapColorScalars(pointsColor, dimensionIndex, colorScalars)) { + _settingsAction->getColoringAction().getColorByAction().setCurrentIndex(0); // reset to color by constant + return; + } + // Assign colorScalars and scalar effect _scatterPlotWidget->setScalars(colorScalars); _scatterPlotWidget->setScalarEffect(PointEffect::Color); - _settingsAction.getColoringAction().updateColorMapActionScalarRange(); + _settingsAction->getColoringAction().updateColorMapActionScalarRange(); + + // Render + getWidget().update(); +} + +void ScatterplotPlugin::loadColors2D(const Dataset& pointsColor, const std::uint32_t& dimensionIndexX, const std::uint32_t& dimensionIndexY) +{ + std::vector colorScalarsX = {}; + std::vector colorScalarsY = {}; + + if (!mapColorScalars(pointsColor, dimensionIndexX, colorScalarsX) || + !mapColorScalars(pointsColor, dimensionIndexY, colorScalarsY)) { + _settingsAction->getColoringAction().getColorByAction().setCurrentIndex(0); // reset to color by constant + return; + } + + // Assign both channels and the two-channel 2D coloring effect + _scatterPlotWidget->setScalars(colorScalarsX); + _scatterPlotWidget->setScalars2(colorScalarsY); + _scatterPlotWidget->setScalarEffect(PointEffect::Color2DChannels); + + // Render + getWidget().update(); +} + +void ScatterplotPlugin::loadColorsRGB(const Dataset& pointsColor, const std::uint32_t& dimensionIndexR, const std::uint32_t& dimensionIndexG, const std::uint32_t& dimensionIndexB) +{ + std::vector colorScalarsR = {}; + std::vector colorScalarsG = {}; + std::vector colorScalarsB = {}; + + if (!mapColorScalars(pointsColor, dimensionIndexR, colorScalarsR) || + !mapColorScalars(pointsColor, dimensionIndexG, colorScalarsG) || + !mapColorScalars(pointsColor, dimensionIndexB, colorScalarsB)) { + _settingsAction->getColoringAction().getColorByAction().setCurrentIndex(0); // reset to color by constant + return; + } + + // Assign the three channels and the RGB coloring effect + _scatterPlotWidget->setScalars(colorScalarsR); + _scatterPlotWidget->setScalars2(colorScalarsG); + _scatterPlotWidget->setScalars3(colorScalarsB); + _scatterPlotWidget->setScalarEffect(PointEffect::ColorRGB); // Render getWidget().update(); @@ -841,7 +900,7 @@ void ScatterplotPlugin::loadColors(const Dataset& clusters) return; // Get global indices from the position dataset - int totalNumPoints = 0; + std::uint64_t totalNumPoints = 0; if (_positionDataset->isDerivedData()) totalNumPoints = _positionSourceDataset->getFullDataset()->getNumPoints(); else @@ -908,8 +967,8 @@ void ScatterplotPlugin::updateData() if (_positionDataset.isValid()) { // Get the selected dimensions to use as X and Y dimension in the plot - const auto xDim = _settingsAction.getPositionAction().getDimensionX(); - const auto yDim = _settingsAction.getPositionAction().getDimensionY(); + const auto xDim = _settingsAction->getPositionAction().getDimensionX(); + const auto yDim = _settingsAction->getPositionAction().getDimensionY(); // If one of the dimensions was not set, do not draw anything if (xDim < 0 || yDim < 0) @@ -918,8 +977,8 @@ void ScatterplotPlugin::updateData() // Ensure that if positionDataset has now more points, the additional points are plotted if (_numPoints != _positionDataset->getNumPoints()) { - _settingsAction.getPlotAction().getPointPlotAction().updateScatterPlotWidgetPointSizeScalars(); - _settingsAction.getPlotAction().getPointPlotAction().updateScatterPlotWidgetPointOpacityScalars(); + _settingsAction->getPlotAction().getPointPlotAction().updateScatterPlotWidgetPointSizeScalars(); + _settingsAction->getPlotAction().getPointPlotAction().updateScatterPlotWidgetPointOpacityScalars(); } // Determine number of points depending on if its a full dataset or a subset @@ -997,11 +1056,11 @@ void ScatterplotPlugin::updateSelection() _scatterPlotWidget->update(); - auto& coloringAction = _settingsAction.getColoringAction(); + auto& coloringAction = _settingsAction->getColoringAction(); getSamplerAction().setSampleContext({ { "PositionDatasetID", _positionDataset.getDatasetId() }, - { "ColorDatasetID", _settingsAction.getColoringAction().getCurrentColorDataset().getDatasetId() }, + { "ColorDatasetID", _settingsAction->getColoringAction().getCurrentColorDataset().getDatasetId() }, { "LocalPointIndices", localPointIndices }, { "GlobalPointIndices", globalPointIndices }, { "Distances", QVariantList()}, @@ -1010,15 +1069,20 @@ void ScatterplotPlugin::updateSelection() { "ColorMap1D", coloringAction.getColorMap1DAction().getColorMapImage() }, { "ColorMap2D", coloringAction.getColorMap2DAction().getColorMapImage() }, { "ColorDimensionIndex", coloringAction.getDimensionAction().getCurrentDimensionAction().getCurrentIndex() }, - { "RenderMode", _settingsAction.getRenderModeAction().getCurrentText() } + { "RenderMode", _settingsAction->getRenderModeAction().getCurrentText() } }); } } void ScatterplotPlugin::updateHeadsUpDisplay() { + if (mv::projects().isOpeningProject()) + return; + getHeadsUpDisplayAction().removeAllHeadsUpDisplayItems(); + auto& coloringAction = _settingsAction->getColoringAction(); + if (_positionDataset.isValid()) { const auto datasetsItem = getHeadsUpDisplayAction().addHeadsUpDisplayItem("Datasets", "", ""); @@ -1029,12 +1093,14 @@ void ScatterplotPlugin::updateHeadsUpDisplay() getHeadsUpDisplayAction().addHeadsUpDisplayItem(QString("%1 by:").arg(metaDataName), data->getGuiName(), "", itemPtr); }; - if (_settingsAction.getColoringAction().getColorByAction().getCurrentIndex() >= 2) - addMetaDataToHeadsUpDisplay("Color", _colorDataset, datasetsItem); + if (coloringAction.getColorByAction().getCurrentIndex() >= 2) + addMetaDataToHeadsUpDisplay("Color", coloringAction.getCurrentColorDataset(), datasetsItem); - addMetaDataToHeadsUpDisplay("Size", _settingsAction.getPlotAction().getPointPlotAction().getSizeAction().getCurrentDataset(), datasetsItem); - addMetaDataToHeadsUpDisplay("Opacity", _settingsAction.getPlotAction().getPointPlotAction().getOpacityAction().getCurrentDataset(), datasetsItem); + auto& pointPlotAction = _settingsAction->getPlotAction().getPointPlotAction(); + //qDebug() << "ScatterplotPlugin::updateHeadsUpDisplay: point size dataset: " << pointPlotAction.getSizeAction().getCurrentDataset().isValid() << ", opacity dataset: " << pointPlotAction.getOpacityAction().getCurrentDataset().isValid(); + addMetaDataToHeadsUpDisplay("Size", pointPlotAction.getSizeAction().getCurrentDataset(), datasetsItem); + addMetaDataToHeadsUpDisplay("Opacity", pointPlotAction.getOpacityAction().getCurrentDataset(), datasetsItem); } else { getHeadsUpDisplayAction().addHeadsUpDisplayItem("No datasets loaded", "", ""); } @@ -1046,7 +1112,7 @@ void ScatterplotPlugin::updateHeadsUpDisplayTextColor() if (auto headsUpDisplayWidgetTreeView = headsUpDisplayWidget->findChild("TreeView")) { QPalette palette = headsUpDisplayWidgetTreeView->palette(); - palette.setColor(QPalette::Text, _settingsAction.getMiscellaneousAction().getBackgroundColorAction().getColor().lightnessF() > .5f ? Qt::black : Qt::white); + palette.setColor(QPalette::Text, _settingsAction->getMiscellaneousAction().getBackgroundColorAction().getColor().lightnessF() > .5f ? Qt::black : Qt::white); headsUpDisplayWidgetTreeView->setPalette(palette); } @@ -1063,15 +1129,10 @@ void ScatterplotPlugin::fromVariantMap(const QVariantMap& variantMap) pointRenderer.getNavigator().resetView(true); - _primaryToolbarAction.fromParentVariantMap(variantMap); - _settingsAction.fromParentVariantMap(variantMap); - - updateHeadsUpDisplay(); + _primaryToolbarAction->fromParentVariantMap(variantMap); + _settingsAction->fromParentVariantMap(variantMap); if (pointRenderer.getNavigator().getNavigationAction().getSerializationCountFrom() == 0) { - qDebug() << "Resetting view"; - - _scatterPlotWidget->update(); } @@ -1082,8 +1143,8 @@ QVariantMap ScatterplotPlugin::toVariantMap() const { QVariantMap variantMap = ViewPlugin::toVariantMap(); - _primaryToolbarAction.insertIntoVariantMap(variantMap); - _settingsAction.insertIntoVariantMap(variantMap); + _primaryToolbarAction->insertIntoVariantMap(variantMap); + _settingsAction->insertIntoVariantMap(variantMap); return variantMap; } diff --git a/src/ScatterplotPlugin.h b/src/ScatterplotPlugin.h index 4155039..ef32af8 100644 --- a/src/ScatterplotPlugin.h +++ b/src/ScatterplotPlugin.h @@ -63,6 +63,23 @@ class ScatterplotPlugin : public ViewPlugin */ void loadColors(const Dataset& points, const std::uint32_t& dimensionIndex); + /** + * Load 2D color from two dimensions of a points dataset (mapped through the 2D color map) + * @param points Smart pointer to points dataset + * @param dimensionIndexX Index of the dimension mapped to the color map x-axis + * @param dimensionIndexY Index of the dimension mapped to the color map y-axis + */ + void loadColors2D(const Dataset& points, const std::uint32_t& dimensionIndexX, const std::uint32_t& dimensionIndexY); + + /** + * Load RGB color from three dimensions of a points dataset + * @param points Smart pointer to points dataset + * @param dimensionIndexR Index of the dimension mapped to red + * @param dimensionIndexG Index of the dimension mapped to green + * @param dimensionIndexB Index of the dimension mapped to blue + */ + void loadColorsRGB(const Dataset& points, const std::uint32_t& dimensionIndexR, const std::uint32_t& dimensionIndexG, const std::uint32_t& dimensionIndexB); + /** * Load color from clusters dataset * @param clusters Smart pointer to clusters dataset @@ -88,14 +105,16 @@ class ScatterplotPlugin : public ViewPlugin /** Get reference to the scatter plot widget */ ScatterplotWidget& getScatterplotWidget(); - SettingsAction& getSettingsAction() { return _settingsAction; } + SettingsAction& getSettingsAction() { return *_settingsAction; } private: void updateData(); void updateSelection(); + void updateHeadsUpDisplayTextColor(); + +public: void updateHeadsUpDisplay(); - void updateHeadsUpDisplayTextColor(); public: // Serialization @@ -112,16 +131,26 @@ class ScatterplotPlugin : public ViewPlugin QVariantMap toVariantMap() const override; private: - mv::gui::DropWidget* _dropWidget; /** Widget for dropping datasets */ - ScatterplotWidget* _scatterPlotWidget; /** The visualization widget */ - Dataset _positionDataset; /** Smart pointer to points dataset for point position */ - Dataset _positionSourceDataset; /** Smart pointer to source of the points dataset for point position (if any) */ - Dataset _colorDataset; /** Smart pointer to dataset used for coloring (if any) */ - std::vector _positions; /** Point positions */ - unsigned int _numPoints; /** Number of point positions */ - SettingsAction _settingsAction; /** Group action for all settings */ - HorizontalToolbarAction _primaryToolbarAction; /** Horizontal toolbar for primary content */ - QRectF _selectionBoundaries; /** Boundaries of the selection */ + + /** + * Extract dimension \p dimensionIndex from \p pointsColor and map it into the position dataset's point space + * @param pointsColor Smart pointer to the color points dataset + * @param dimensionIndex Index of the dimension to extract + * @param colorScalars Output vector of scalars, sized to the number of position points on success + * @return Boolean determining whether the mapping succeeded + */ + bool mapColorScalars(const Dataset& pointsColor, const std::uint32_t& dimensionIndex, std::vector& colorScalars); + +private: + mv::gui::DropWidget* _dropWidget; /** Widget for dropping datasets */ + ScatterplotWidget* _scatterPlotWidget; /** The visualization widget */ + Dataset _positionDataset; /** Smart pointer to points dataset for point position */ + Dataset _positionSourceDataset; /** Smart pointer to source of the points dataset for point position (if any) */ + std::vector _positions; /** Point positions */ + std::uint64_t _numPoints; /** Number of point positions */ + QPointer _settingsAction; /** Group action for all settings */ + QPointer _primaryToolbarAction; /** Horizontal toolbar for primary content */ + QRectF _selectionBoundaries; /** Boundaries of the selection */ static const std::int32_t LAZY_UPDATE_INTERVAL = 2; diff --git a/src/ScatterplotWidget.cpp b/src/ScatterplotWidget.cpp index ff6f6f8..837ed7c 100644 --- a/src/ScatterplotWidget.cpp +++ b/src/ScatterplotWidget.cpp @@ -328,7 +328,21 @@ void ScatterplotWidget::setHighlights(const std::vector& highlights, const void ScatterplotWidget::setScalars(const std::vector& scalars) { _pointRenderer.setColorChannelScalars(scalars); - + + update(); +} + +void ScatterplotWidget::setScalars2(const std::vector& scalars) +{ + _pointRenderer.setColorChannel2Scalars(scalars); + + update(); +} + +void ScatterplotWidget::setScalars3(const std::vector& scalars) +{ + _pointRenderer.setColorChannel3Scalars(scalars); + update(); } diff --git a/src/ScatterplotWidget.h b/src/ScatterplotWidget.h index 9e73b49..4441bd6 100644 --- a/src/ScatterplotWidget.h +++ b/src/ScatterplotWidget.h @@ -80,6 +80,12 @@ class ScatterplotWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_C void setHighlights(const std::vector& highlights, const std::int32_t& numSelectedPoints); void setScalars(const std::vector& scalars); + /** Set the second color scalar channel (used for 2D and RGB coloring) */ + void setScalars2(const std::vector& scalars); + + /** Set the third color scalar channel (used for RGB coloring) */ + void setScalars3(const std::vector& scalars); + /** * Set colors for each individual data point * @param colors Vector of colors (size must match that of the loaded points dataset) diff --git a/src/SettingsAction.cpp b/src/SettingsAction.cpp index 740da78..f0739dc 100644 --- a/src/SettingsAction.cpp +++ b/src/SettingsAction.cpp @@ -70,12 +70,14 @@ void SettingsAction::fromVariantMap(const QVariantMap& variantMap) _plotAction.fromParentVariantMap(variantMap); _positionAction.fromParentVariantMap(variantMap); _coloringAction.fromParentVariantMap(variantMap); + _subsetAction.fromParentVariantMap(variantMap, true); + _clusteringAction.fromParentVariantMap(variantMap, true); _renderModeAction.fromParentVariantMap(variantMap); _selectionAction.fromParentVariantMap(variantMap); _miscellaneousAction.fromParentVariantMap(variantMap); if (variantMap.contains("PointRendererNavigation")) - _scatterplotPlugin->getScatterplotWidget().getPointRendererNavigator().getNavigationAction().fromVariantMap(variantMap["PointRendererNavigation"].toMap()); + _scatterplotPlugin->getScatterplotWidget().getPointRendererNavigator().getNavigationAction().fromVariantMap(variantMap["PointRendererNavigation"].toMap()); if (variantMap.contains("DensityRendererNavigation")) _scatterplotPlugin->getScatterplotWidget().getDensityRendererNavigator().getNavigationAction().fromVariantMap(variantMap["DensityRendererNavigation"].toMap()); @@ -90,6 +92,8 @@ QVariantMap SettingsAction::toVariantMap() const _plotAction.insertIntoVariantMap(variantMap); _positionAction.insertIntoVariantMap(variantMap); _coloringAction.insertIntoVariantMap(variantMap); + _subsetAction.insertIntoVariantMap(variantMap); + _clusteringAction.insertIntoVariantMap(variantMap); _selectionAction.insertIntoVariantMap(variantMap); _miscellaneousAction.insertIntoVariantMap(variantMap); diff --git a/src/SettingsAction.h b/src/SettingsAction.h index 99a22f7..a6b0ba9 100644 --- a/src/SettingsAction.h +++ b/src/SettingsAction.h @@ -80,4 +80,4 @@ class SettingsAction : public GroupAction ExportAction _exportAction; /** Action for exporting */ MiscellaneousAction _miscellaneousAction; /** Action for miscellaneous settings */ DatasetsAction _datasetsAction; /** Action for picking dataset(s) */ -}; \ No newline at end of file +};