mirror of
https://github.com/OPM/ResInsight.git
synced 2026-08-26 21:27:15 -05:00
#14192 Python: Update discrete property color legend in place instead of delete/create
Address review feedback on #14199: deleting and recreating the legend left views bound to the old legend pointing at a deleted object. Add RimColorLegendCollection::updateColorLegend and a matching UpdateColorLegend scriptable method that mutate the existing custom legend (name and items) so referring views keep their binding and are notified once. A new legend is only created when none is registered for the result or the registered legend is a standard legend. set_discrete_property_category_names now performs a single atomic call instead of delete + create + per-item appends. The empty-dict removal path still uses DeleteColorLegend.
This commit is contained in:
committed by
Magne Sjaastad
parent
5a083331b0
commit
a4fe22508a
@@ -101,6 +101,21 @@ void RimColorLegend::appendColorLegendItem( RimColorLegendItem* colorLegendItem
|
||||
onColorLegendItemHasChanged();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Replace all color legend items. Takes ownership of the new items.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimColorLegend::setColorLegendItems( const std::vector<RimColorLegendItem*>& colorLegendItems )
|
||||
{
|
||||
m_colorLegendItems.deleteChildren();
|
||||
|
||||
for ( auto colorLegendItem : colorLegendItems )
|
||||
{
|
||||
m_colorLegendItems.push_back( colorLegendItem );
|
||||
}
|
||||
|
||||
onColorLegendItemHasChanged();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -54,6 +54,7 @@ public:
|
||||
void addReorderCapability();
|
||||
|
||||
void appendColorLegendItem( RimColorLegendItem* colorLegendItem );
|
||||
void setColorLegendItems( const std::vector<RimColorLegendItem*>& colorLegendItems );
|
||||
std::vector<RimColorLegendItem*> colorLegendItems() const;
|
||||
|
||||
cvf::Color3ubArray colorArray() const;
|
||||
|
||||
@@ -114,6 +114,50 @@ RimColorLegend* RimColorLegendCollection::createColorLegend( const QString& colo
|
||||
return colorLegend;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Update the custom color legend registered as default for the given result in place, so that
|
||||
/// objects referring to the legend keep their binding. Creates and registers a new custom legend
|
||||
/// if none exists or the registered legend is a standard legend. If colors is empty, palette
|
||||
/// colors are assigned automatically.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimColorLegend* RimColorLegendCollection::updateColorLegend( const RimCase* rimCase,
|
||||
const QString& resultName,
|
||||
const QString& colorLegendName,
|
||||
const std::vector<int>& categoryValues,
|
||||
const std::vector<QString>& categoryNames,
|
||||
const std::vector<cvf::Color3f>& colors )
|
||||
{
|
||||
CAF_ASSERT( categoryValues.size() == categoryNames.size() );
|
||||
CAF_ASSERT( colors.empty() || colors.size() == categoryValues.size() );
|
||||
|
||||
auto legend = findDefaultLegendForResult( rimCase, resultName );
|
||||
if ( !legend || isStandardColorLegend( legend ) )
|
||||
{
|
||||
legend = new RimColorLegend();
|
||||
appendCustomColorLegend( legend );
|
||||
}
|
||||
|
||||
legend->setColorLegendName( colorLegendName );
|
||||
|
||||
auto paletteColors = RiaColorTables::categoryPaletteColors().color3ubArray();
|
||||
|
||||
std::vector<RimColorLegendItem*> items;
|
||||
for ( size_t i = 0; i < categoryValues.size(); i++ )
|
||||
{
|
||||
cvf::Color3f color = colors.empty() ? cvf::Color3f( paletteColors[i % paletteColors.size()] ) : colors[i];
|
||||
|
||||
auto item = new RimColorLegendItem();
|
||||
item->setValues( categoryNames[i], categoryValues[i], color );
|
||||
items.push_back( item );
|
||||
}
|
||||
|
||||
legend->setColorLegendItems( items );
|
||||
|
||||
setDefaultColorLegendForResult( rimCase, resultName, legend );
|
||||
|
||||
return legend;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include "cafPdmField.h"
|
||||
#include "cafPdmObject.h"
|
||||
|
||||
#include "cvfColor3.h"
|
||||
|
||||
class RimCase;
|
||||
class RimColorLegend;
|
||||
class RimColorLegendItem;
|
||||
@@ -50,6 +52,12 @@ public:
|
||||
void deleteCustomColorLegends();
|
||||
|
||||
RimColorLegend* createColorLegend( const QString& colorLegendName, const std::map<int, QString>& valuesAndNames );
|
||||
RimColorLegend* updateColorLegend( const RimCase* rimCase,
|
||||
const QString& resultName,
|
||||
const QString& colorLegendName,
|
||||
const std::vector<int>& categoryValues,
|
||||
const std::vector<QString>& categoryNames,
|
||||
const std::vector<cvf::Color3f>& colors );
|
||||
void createColorLegendFromFormationNames( RimFormationNames* rimFormationNames );
|
||||
void deleteColorLegend( const RimCase* rimCase, const QString& resultName );
|
||||
void setDefaultColorLegendForResult( const RimCase* rimCase, const QString& resultName, RimColorLegend* colorLegend );
|
||||
|
||||
@@ -105,6 +105,54 @@ QString RimcColorLegend_addColorLegendItem::classKeywordReturnedType() const
|
||||
return RimColorLegendItem::classKeywordStatic();
|
||||
}
|
||||
|
||||
CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimColorLegendCollection, RimcColorLegendCollection_updateColorLegend, "UpdateColorLegend" );
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimcColorLegendCollection_updateColorLegend::RimcColorLegendCollection_updateColorLegend( caf::PdmObjectHandle* self )
|
||||
: caf::PdmObjectCreationMethod( self )
|
||||
{
|
||||
CAF_PDM_InitObject( "Update Color Legend", "", "", "Update the color legend bound to a (case, resultName) pair in place, creating it if needed" );
|
||||
|
||||
CAF_PDM_InitScriptableFieldNoDefault( &m_case, "Case", "Case" );
|
||||
CAF_PDM_InitScriptableField( &m_resultName, "ResultName", QString(), "Result Name" );
|
||||
CAF_PDM_InitScriptableField( &m_legendName, "LegendName", QString(), "Legend Name" );
|
||||
CAF_PDM_InitScriptableFieldNoDefault( &m_categoryValues, "CategoryValues", "Category Values" );
|
||||
CAF_PDM_InitScriptableFieldNoDefault( &m_categoryNames, "CategoryNames", "Category Names" );
|
||||
CAF_PDM_InitScriptableFieldNoDefault( &m_colors, "Colors", "Colors" );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::expected<caf::PdmObjectHandle*, QString> RimcColorLegendCollection_updateColorLegend::execute()
|
||||
{
|
||||
auto collection = self<RimColorLegendCollection>();
|
||||
if ( !collection ) return std::unexpected( "No color legend collection found" );
|
||||
|
||||
if ( !m_case() ) return std::unexpected( "No case provided" );
|
||||
|
||||
if ( m_categoryValues().size() != m_categoryNames().size() )
|
||||
return std::unexpected( "CategoryValues and CategoryNames must have matching sizes" );
|
||||
|
||||
if ( !m_colors().empty() && m_colors().size() != m_categoryValues().size() )
|
||||
return std::unexpected( "Colors must be empty or match the size of CategoryValues" );
|
||||
|
||||
auto legend = collection->updateColorLegend( m_case(), m_resultName(), m_legendName(), m_categoryValues(), m_categoryNames(), m_colors() );
|
||||
collection->updateConnectedEditors();
|
||||
|
||||
return legend;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimcColorLegendCollection_updateColorLegend::classKeywordReturnedType() const
|
||||
{
|
||||
return RimColorLegend::classKeywordStatic();
|
||||
}
|
||||
|
||||
CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimColorLegendCollection,
|
||||
RimcColorLegendCollection_setDefaultColorLegendForResult,
|
||||
"SetDefaultColorLegendForResult" );
|
||||
|
||||
@@ -67,6 +67,28 @@ private:
|
||||
caf::PdmField<cvf::Color3f> m_color;
|
||||
};
|
||||
|
||||
//==================================================================================================
|
||||
///
|
||||
//==================================================================================================
|
||||
class RimcColorLegendCollection_updateColorLegend : public caf::PdmObjectCreationMethod
|
||||
{
|
||||
CAF_PDM_HEADER_INIT;
|
||||
|
||||
public:
|
||||
RimcColorLegendCollection_updateColorLegend( caf::PdmObjectHandle* self );
|
||||
|
||||
std::expected<caf::PdmObjectHandle*, QString> execute() override;
|
||||
QString classKeywordReturnedType() const override;
|
||||
|
||||
private:
|
||||
caf::PdmPtrField<RimCase*> m_case;
|
||||
caf::PdmField<QString> m_resultName;
|
||||
caf::PdmField<QString> m_legendName;
|
||||
caf::PdmField<std::vector<int>> m_categoryValues;
|
||||
caf::PdmField<std::vector<QString>> m_categoryNames;
|
||||
caf::PdmField<std::vector<cvf::Color3f>> m_colors;
|
||||
};
|
||||
|
||||
//==================================================================================================
|
||||
///
|
||||
//==================================================================================================
|
||||
|
||||
@@ -3,8 +3,9 @@ Category name/color binding for discrete (INTEGER) grid cell results.
|
||||
|
||||
Provides a convenience API on Case for labeling integer result values with
|
||||
human-readable names and optional colors. Internally, this reuses the existing
|
||||
ColorLegend infrastructure: a new custom ColorLegend is created and registered
|
||||
as the default legend for the (case, resultName) pair. When the property is
|
||||
ColorLegend infrastructure: a custom ColorLegend is registered as the default
|
||||
legend for the (case, resultName) pair, and repeated calls update that legend
|
||||
in place so views referencing it keep their binding. When the property is
|
||||
shown in a 3D view, the legend's item names are used as the category labels.
|
||||
"""
|
||||
|
||||
@@ -55,10 +56,14 @@ def set_discrete_property_category_names(
|
||||
data_type="INTEGER") or set_active_cell_property(..., data_type="INTEGER")
|
||||
to display text labels instead of raw integers in the 3D view legend.
|
||||
|
||||
Repeated calls for the same property update the existing color legend in
|
||||
place, so views referencing the legend keep their binding.
|
||||
|
||||
Arguments:
|
||||
property_name (str): Name of the discrete property result.
|
||||
value_names (Dict[int, str]): Mapping from integer value to label.
|
||||
An empty dict removes any existing mapping for this property.
|
||||
Labels must not contain commas. An empty dict removes any
|
||||
existing mapping for this property.
|
||||
value_colors (Optional[Dict[int, str]]): Optional per-value colors as
|
||||
strings accepted by QColor (e.g. "red", "#ff8800"). Values without
|
||||
a color entry get an auto-assigned palette color.
|
||||
@@ -66,7 +71,7 @@ def set_discrete_property_category_names(
|
||||
Defaults to the property name.
|
||||
|
||||
Returns:
|
||||
The created ColorLegend, or None if value_names was empty.
|
||||
The created or updated ColorLegend, or None if value_names was empty.
|
||||
"""
|
||||
project = self.ancestor(Project)
|
||||
if project is None:
|
||||
@@ -76,35 +81,35 @@ def set_discrete_property_category_names(
|
||||
if collection is None:
|
||||
raise RuntimeError("Could not find ColorLegendCollection in project")
|
||||
|
||||
collection.delete_color_legend(case=self, result_name=property_name)
|
||||
|
||||
if not value_names:
|
||||
collection.delete_color_legend(case=self, result_name=property_name)
|
||||
return None
|
||||
|
||||
name = legend_name if legend_name else property_name
|
||||
legend = collection.create_color_legend(name=name)
|
||||
|
||||
category_values = []
|
||||
category_names = []
|
||||
category_colors = []
|
||||
colors = value_colors or {}
|
||||
palette_index = 0
|
||||
for value, label in value_names.items():
|
||||
for value, label in sorted(value_names.items()):
|
||||
color = colors.get(value)
|
||||
if color is None:
|
||||
color = _DEFAULT_PALETTE[palette_index % len(_DEFAULT_PALETTE)]
|
||||
palette_index += 1
|
||||
legend.add_color_legend_item(
|
||||
category_value=value,
|
||||
category_name=label,
|
||||
color=color,
|
||||
)
|
||||
category_values.append(value)
|
||||
category_names.append(label)
|
||||
category_colors.append(color)
|
||||
|
||||
collection.set_default_color_legend_for_result(
|
||||
return collection.update_color_legend(
|
||||
case=self,
|
||||
result_name=property_name,
|
||||
color_legend=legend,
|
||||
legend_name=name,
|
||||
category_values=category_values,
|
||||
category_names=category_names,
|
||||
colors=category_colors,
|
||||
)
|
||||
|
||||
return legend
|
||||
|
||||
|
||||
def _find_default_legend(case: Case, property_name: str) -> Optional[ColorLegend]:
|
||||
"""Look up the color legend bound to (case, property_name).
|
||||
|
||||
@@ -130,3 +130,43 @@ def test_discrete_property_category_no_duplicate_legend(rips_instance, initializ
|
||||
1: "Shale",
|
||||
2: "Coal",
|
||||
}
|
||||
|
||||
|
||||
def test_discrete_property_category_update_preserves_legend_identity(
|
||||
rips_instance, initialize_test
|
||||
):
|
||||
case_path = dataroot.PATH + "/TEST10K_FLT_LGR_NNC/TEST10K_FLT_LGR_NNC.EGRID"
|
||||
case = rips_instance.project.load_case(path=case_path)
|
||||
assert case is not None
|
||||
|
||||
collection = rips_instance.project.color_legend_collection()
|
||||
|
||||
def legend_count():
|
||||
return len(collection.descendants(rips.ColorLegend))
|
||||
|
||||
legend1 = case.set_discrete_property_category_names(
|
||||
property_name="FACIES", value_names={0: "Sand", 1: "Shale"}
|
||||
)
|
||||
assert legend1 is not None
|
||||
after_first = legend_count()
|
||||
|
||||
# A repeated call must update the existing legend object in place so that
|
||||
# views referencing it keep their binding.
|
||||
new_names = {0: "Sandstone", 1: "Shale", 2: "Coal"}
|
||||
new_colors = {0: "#e6c878", 1: "#646464", 2: "#202020"}
|
||||
legend2 = case.set_discrete_property_category_names(
|
||||
property_name="FACIES", value_names=new_names, value_colors=new_colors
|
||||
)
|
||||
assert legend2.address() == legend1.address()
|
||||
assert legend_count() == after_first
|
||||
|
||||
# The items are fully replaced, with no leftovers from the first call.
|
||||
assert case.discrete_property_category_names("FACIES") == new_names
|
||||
assert case.discrete_property_category_colors("FACIES") == new_colors
|
||||
|
||||
# The legend can be renamed in place as well.
|
||||
legend3 = case.set_discrete_property_category_names(
|
||||
property_name="FACIES", value_names=new_names, legend_name="My Facies"
|
||||
)
|
||||
assert legend3.address() == legend1.address()
|
||||
assert legend3.color_legend_name == "My Facies"
|
||||
|
||||
Reference in New Issue
Block a user