///////////////////////////////////////////////////////////////////////////////// // // Copyright (C) 2026- Equinor ASA // // ResInsight is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY // WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. // // See the GNU General Public License at // for more details. // ///////////////////////////////////////////////////////////////////////////////// #include "RimGroupNetworkPlot.h" #include "RimGroupNetworkData.h" #include "RimGroupNetworkDataCollection.h" #include "RimGroupNetworkNode.h" #include "RiaPreferences.h" #include "Summary/RiaSummaryPlotTools.h" #include "RifSummaryReaderInterface.h" #include "RimOilField.h" #include "RimProject.h" #include "RimSummaryCase.h" #include "RimSummaryPlot.h" #include "RiuGroupNetworkWidget.h" #include "RiuPlotMainWindowTools.h" #include "cafCmdFeatureMenuBuilder.h" #include "cafFontTools.h" #include #include namespace caf { template <> void AppEnum::setUp() { using Metric = RifGroupNetworkSummaryData::EdgeMetric; addItem( Metric::OIL_RATE, "OIL_RATE", "Oil Rate" ); addItem( Metric::GAS_RATE, "GAS_RATE", "Gas Rate" ); addItem( Metric::WATER_RATE, "WATER_RATE", "Water Rate" ); addItem( Metric::WATER_INJ_RATE, "WATER_INJ_RATE", "Water Injection Rate" ); addItem( Metric::GAS_INJ_RATE, "GAS_INJ_RATE", "Gas Injection Rate" ); setDefault( Metric::OIL_RATE ); } template <> void AppEnum::setUp() { using Metric = RifGroupNetworkSummaryData::NodeMetric; addItem( Metric::PRESSURE, "PRESSURE", "Pressure" ); addItem( Metric::BHP, "BHP", "Bottom Hole Pressure" ); addItem( Metric::CONTROL_MODE, "CONTROL_MODE", "Well Control Mode" ); setDefault( Metric::PRESSURE ); } } // namespace caf CAF_PDM_SOURCE_INIT( RimGroupNetworkPlot, "RimGroupNetworkPlot" ); namespace { QString dateText( std::time_t date ) { return RimGroupNetworkData::dateText( date ); } QString valueWithUnit( std::optional value, const std::string& unit ) { if ( !value ) return {}; QString text = RimGroupNetworkGraph::formatValue( *value ); if ( !unit.empty() ) text += " " + QString::fromStdString( unit ); return text; } // The colours used for the edge values in the group tree plot of webviz QColor edgeMetricColor( RifGroupNetworkSummaryData::EdgeMetric metric ) { using Metric = RifGroupNetworkSummaryData::EdgeMetric; switch ( metric ) { case Metric::OIL_RATE: return QColor( "#60be6c" ); case Metric::GAS_RATE: return QColor( "#c5221c" ); case Metric::WATER_RATE: return QColor( "#0d1b9e" ); case Metric::WATER_INJ_RATE: return QColor( "#00c3ff" ); case Metric::GAS_INJ_RATE: return QColor( "#d6397a" ); } return QColor( "#5c5c5c" ); } const std::vector allEdgeMetrics = { RifGroupNetworkSummaryData::EdgeMetric::OIL_RATE, RifGroupNetworkSummaryData::EdgeMetric::GAS_RATE, RifGroupNetworkSummaryData::EdgeMetric::WATER_RATE, RifGroupNetworkSummaryData::EdgeMetric::WATER_INJ_RATE, RifGroupNetworkSummaryData::EdgeMetric::GAS_INJ_RATE }; const std::vector allNodeMetrics = { RifGroupNetworkSummaryData::NodeMetric::PRESSURE, RifGroupNetworkSummaryData::NodeMetric::BHP, RifGroupNetworkSummaryData::NodeMetric::CONTROL_MODE }; } // namespace //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RimGroupNetworkPlot::RimGroupNetworkPlot() : networkChanged( this ) { CAF_PDM_InitObject( "Production Network", ":/summary/components/images/production-network.svg" ); setDeletable( true ); CAF_PDM_InitField( &m_name, "Name", QString( "Production Network" ), "Name" ); CAF_PDM_InitFieldNoDefault( &m_data, "Data", "Data Source" ); CAF_PDM_InitFieldNoDefault( &m_edgeMetric, "EdgeMetric", "Edge Value" ); CAF_PDM_InitFieldNoDefault( &m_nodeMetric, "NodeMetric", "Node Value" ); CAF_PDM_InitField( &m_showProducers, "ShowProducers", true, "Producers" ); CAF_PDM_InitField( &m_showInjectors, "ShowInjectors", true, "Injectors" ); CAF_PDM_InitField( &m_showOther, "ShowOther", true, "Other" ); CAF_PDM_InitField( &m_timeStepIndex, "TimeStepIndex", 0, "Time Step" ); CAF_PDM_InitFieldNoDefault( &m_collapsedNodes, "CollapsedNodes", "Collapsed Nodes" ); m_collapsedNodes.uiCapability()->setUiHidden( true ); CAF_PDM_InitFieldNoDefault( &m_selectedNode, "SelectedNode", "Selected Node" ); m_selectedNode = new RimGroupNetworkNode(); m_selectedNode.uiCapability()->setUiHidden( true ); m_selectedNode.xmlCapability()->disableIO(); dockAsPlotWindow(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RimGroupNetworkPlot::~RimGroupNetworkPlot() { removeWindowFromDock(); deleteViewWidget(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- int RimGroupNetworkPlot::id() const { return -1; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QWidget* RimGroupNetworkPlot::viewWidget() { return m_viewWidget; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::zoomAll() { } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- int RimGroupNetworkPlot::fontSize() const { return caf::FontTools::absolutePointSize( RiaPreferences::current()->defaultPlotFontSize() ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::updateFonts() { } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QWidget* RimGroupNetworkPlot::createViewWidget( QWidget* mainWindowParent ) { m_viewWidget = new RiuGroupNetworkWidget( mainWindowParent ); m_viewWidget->setNetwork( this ); QObject::connect( m_viewWidget, &RiuGroupNetworkWidget::nodeSelected, m_viewWidget, [this]( const QString& nodeName ) { selectNode( nodeName ); } ); return m_viewWidget; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::deleteViewWidget() { if ( m_viewWidget ) { m_viewWidget->hide(); m_viewWidget->setParent( nullptr ); delete m_viewWidget; m_viewWidget = nullptr; } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::onLoadDataAndUpdate() { updateDockWindowVisibility(); networkChanged.send(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::assignIdIfNecessary() { } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimGroupNetworkPlot::name() const { return m_name(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::setDataSource( RimGroupNetworkData* data ) { disconnectFromData( m_data() ); m_data = data; if ( data ) m_name = data->name(); connectToData(); resetTimeStep(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::vector RimGroupNetworkPlot::graphNodes() const { const RigGroupNetwork::TreeRevision* tree = currentTree(); if ( !tree ) return {}; const size_t timeStep = currentSummaryTimeStep(); const auto* data = summaryData(); const auto edgeUnit = data ? data->edgeUnit( m_edgeMetric() ) : std::string(); const auto nodeUnit = data ? data->nodeUnit( m_nodeMetric() ) : std::string(); const auto edgeColor = edgeMetricColor( m_edgeMetric() ); std::vector nodes; for ( const RigGroupNetwork::Edge& edge : tree->edges ) { RimGroupNetworkGraph::Node node; node.name = QString::fromStdString( edge.child ); node.parent = QString::fromStdString( edge.parent ); node.isWell = edge.isWell(); node.edgeColor = edgeColor; if ( data ) { const auto category = data->category( edge.child, edge.isWell() ); node.isProducer = category.isProducer; node.isInjector = category.isInjector; node.edgeValue = data->edgeValue( edge.child, edge.isWell(), m_edgeMetric(), timeStep ); node.edgeText = valueWithUnit( node.edgeValue, edgeUnit ); const auto nodeValue = data->nodeValue( edge.child, edge.isWell(), m_nodeMetric(), timeStep ); node.valueUnit = QString::fromStdString( nodeUnit ); node.valueNumber = nodeValue ? RimGroupNetworkGraph::formatValue( *nodeValue ) : QString(); node.valueText = valueWithUnit( nodeValue, nodeUnit ); } node.toolTip = toolTip( edge, node ); nodes.push_back( node ); } RimGroupNetworkGraph::Filter filter; filter.showProducers = m_showProducers(); filter.showInjectors = m_showInjectors(); filter.showOther = m_showOther(); filter.collapsedNodes.insert( m_collapsedNodes().begin(), m_collapsedNodes().end() ); return RimGroupNetworkGraph::visibleNodes( nodes, filter ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimGroupNetworkPlot::statusText() const { if ( !m_data ) return "No data source selected"; return m_data->statusText( currentTree() ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QStringList RimGroupNetworkPlot::timeStepTexts() const { QStringList texts; for ( std::time_t date : timeSteps() ) { texts.append( dateText( date ) ); } return texts; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- int RimGroupNetworkPlot::timeStepIndex() const { const int count = static_cast( timeSteps().size() ); return count == 0 ? 0 : std::clamp( m_timeStepIndex(), 0, count - 1 ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::setTimeStepIndex( int index ) { const int count = static_cast( timeSteps().size() ); index = count == 0 ? 0 : std::clamp( index, 0, count - 1 ); if ( index == m_timeStepIndex() ) return; m_timeStepIndex = index; m_timeStepIndex.uiCapability()->updateConnectedEditors(); networkChanged.send(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- bool RimGroupNetworkPlot::isCollapsed( const QString& nodeName ) const { const auto& collapsed = m_collapsedNodes(); return std::find( collapsed.begin(), collapsed.end(), nodeName ) != collapsed.end(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::setCollapsed( const QString& nodeName, bool collapsed ) { std::vector nodes = m_collapsedNodes(); nodes.erase( std::remove( nodes.begin(), nodes.end(), nodeName ), nodes.end() ); if ( collapsed ) nodes.push_back( nodeName ); m_collapsedNodes = nodes; networkChanged.send(); } //-------------------------------------------------------------------------------------------------- /// Collapse all nodes below the root, so only the root and its children are shown //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::collapseAll() { std::vector nodes; if ( const RigGroupNetwork::TreeRevision* tree = currentTree() ) { std::set parents; for ( const RigGroupNetwork::Edge& edge : tree->edges ) { parents.insert( edge.parent ); } for ( const RigGroupNetwork::Edge& edge : tree->edges ) { if ( !edge.isRoot() && parents.count( edge.child ) > 0 ) nodes.push_back( QString::fromStdString( edge.child ) ); } } m_collapsedNodes = nodes; networkChanged.send(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::expandAll() { m_collapsedNodes = std::vector(); networkChanged.send(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::selectNode( const QString& nodeName ) { const RigGroupNetwork::Edge* edge = nodeName.isEmpty() ? nullptr : findEdge( nodeName ); if ( !edge ) { m_selectedNode->clear(); m_selectedNode->updateConnectedEditors(); m_selectedNode.uiCapability()->updateConnectedEditors(); return; } QString nodeType = edge->isWell() ? "Well" : "Group"; if ( summaryData() ) { const auto category = summaryData()->category( edge->child, edge->isWell() ); if ( category.isProducer && category.isInjector ) nodeType += " (producer and injector)"; else if ( category.isProducer ) nodeType += " (producer)"; else if ( category.isInjector ) nodeType += " (injector)"; } m_selectedNode->setNodeName( nodeName ); m_selectedNode->setDescription( nodeType, QString::fromStdString( edge->parent ), QString::fromStdString( RigGroupNetwork::keywordText( edge->keyword ) ) ); m_selectedNode->setValues( propertiesText( *edge ), valueTexts( *edge ) ); m_selectedNode->updateConnectedEditors(); m_selectedNode.uiCapability()->updateConnectedEditors(); RiuPlotMainWindowTools::selectAsCurrentItem( m_selectedNode() ); } //-------------------------------------------------------------------------------------------------- /// Plot the rates and pressures of a node //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::createSummaryPlot( const QString& nodeName ) { const RigGroupNetwork::Edge* edge = findEdge( nodeName ); RimSummaryCase* summaryCase = m_data ? m_data->summaryCase() : nullptr; if ( !edge || !summaryData() || !summaryCase || !summaryCase->summaryReader() ) return; const auto& available = summaryCase->summaryReader()->allResultAddresses(); std::set addresses; for ( auto metric : allEdgeMetrics ) { addresses.insert( summaryData()->edgeAddress( edge->child, edge->isWell(), metric ) ); } for ( auto metric : { RifGroupNetworkSummaryData::NodeMetric::PRESSURE, RifGroupNetworkSummaryData::NodeMetric::BHP } ) { addresses.insert( summaryData()->nodeAddress( edge->child, edge->isWell(), metric ) ); } std::set plotAddresses; for ( const auto& address : addresses ) { if ( address.isValid() && available.count( address ) > 0 ) plotAddresses.insert( address ); } if ( plotAddresses.empty() ) return; RimSummaryPlot* plot = RiaSummaryPlotTools::createPlot( plotAddresses, { summaryCase }, {} ); RiaSummaryPlotTools::createAndAppendSingleSummaryMultiPlot( plot ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- caf::PdmFieldHandle* RimGroupNetworkPlot::userDescriptionField() { return &m_name; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) { RimViewWindow::fieldChangedByUi( changedField, oldValue, newValue ); if ( changedField == &m_showWindow ) return; if ( changedField == &m_data ) { auto* previousData = dynamic_cast( oldValue.value>().rawPtr() ); disconnectFromData( previousData ); connectToData(); resetTimeStep(); } else if ( changedField == &m_name ) { updateWindowTitle(); return; } networkChanged.send(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QList RimGroupNetworkPlot::calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) { QList options; if ( fieldNeedingOptions == &m_data ) { if ( auto* project = RimProject::current() ) { if ( auto* oilField = project->activeOilField() ) { if ( RimGroupNetworkDataCollection* collection = oilField->groupNetworkCollection() ) { for ( RimGroupNetworkData* data : collection->items() ) { options.push_back( caf::PdmOptionItemInfo( data->name(), data ) ); } } } } } else if ( fieldNeedingOptions == &m_timeStepIndex ) { const QStringList texts = timeStepTexts(); for ( int i = 0; i < texts.size(); ++i ) { options.push_back( caf::PdmOptionItemInfo( texts[i], i ) ); } } return options; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) { uiOrdering.add( &m_name ); uiOrdering.add( &m_data ); caf::PdmUiGroup* valueGroup = uiOrdering.addNewGroup( "Values" ); valueGroup->add( &m_timeStepIndex ); valueGroup->add( &m_edgeMetric ); valueGroup->add( &m_nodeMetric ); caf::PdmUiGroup* filterGroup = uiOrdering.addNewGroup( "Show Nodes" ); filterGroup->add( &m_showProducers ); filterGroup->add( &m_showInjectors ); filterGroup->add( &m_showOther ); uiOrdering.skipRemainingFields( true ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::initAfterRead() { connectToData(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const { menuBuilder << "RicNewGroupNetworkPlotFeature"; menuBuilder.addSeparator(); menuBuilder << "RicDeleteItemFeature"; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::connectToData() { if ( m_data ) m_data->dataChanged.connect( this, &RimGroupNetworkPlot::onDataChanged ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::disconnectFromData( RimGroupNetworkData* data ) { if ( data ) data->dataChanged.disconnect( this ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::onDataChanged( const caf::SignalEmitter* emitter ) { resetTimeStep(); updateConnectedEditors(); networkChanged.send(); } //-------------------------------------------------------------------------------------------------- /// Show the last time step of the data //-------------------------------------------------------------------------------------------------- void RimGroupNetworkPlot::resetTimeStep() { const auto& steps = timeSteps(); m_timeStepIndex = steps.empty() ? 0 : static_cast( steps.size() ) - 1; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- const RifGroupNetworkSummaryData* RimGroupNetworkPlot::summaryData() const { return m_data ? m_data->summaryData() : nullptr; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- const RigGroupNetwork::TreeRevision* RimGroupNetworkPlot::currentTree() const { if ( !m_data ) return nullptr; return m_data->treeAtDate( currentDate() ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- const RigGroupNetwork::Edge* RimGroupNetworkPlot::findEdge( const QString& nodeName ) const { const RigGroupNetwork::TreeRevision* tree = currentTree(); if ( !tree ) return nullptr; const std::string name = nodeName.toStdString(); for ( const RigGroupNetwork::Edge& edge : tree->edges ) { if ( edge.child == name ) return &edge; } return nullptr; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::time_t RimGroupNetworkPlot::currentDate() const { const auto& steps = timeSteps(); if ( steps.empty() ) return 0; return steps[timeStepIndex()]; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- size_t RimGroupNetworkPlot::currentSummaryTimeStep() const { return static_cast( timeStepIndex() ); } //-------------------------------------------------------------------------------------------------- /// The summary time steps, or the dates of the network definitions if there is no summary data //-------------------------------------------------------------------------------------------------- const std::vector& RimGroupNetworkPlot::timeSteps() const { static const std::vector noTimeSteps; if ( !m_data ) return noTimeSteps; return m_data->timeSteps(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimGroupNetworkPlot::valueTexts( const RigGroupNetwork::Edge& edge ) const { if ( !summaryData() ) return "No summary case"; const size_t timeStep = currentSummaryTimeStep(); QStringList lines; lines.append( QString( "Values at %1" ).arg( dateText( currentDate() ) ) ); for ( auto metric : allEdgeMetrics ) { const auto value = summaryData()->edgeValue( edge.child, edge.isWell(), metric, timeStep ); if ( !value ) continue; lines.append( QString( "%1 (%2): %3" ) .arg( QString::fromStdString( RifGroupNetworkSummaryData::edgeMetricText( metric ) ) ) .arg( QString::fromStdString( summaryData()->edgeAddress( edge.child, edge.isWell(), metric ).vectorName() ) ) .arg( valueWithUnit( value, summaryData()->edgeUnit( metric ) ) ) ); } for ( auto metric : allNodeMetrics ) { const auto value = summaryData()->nodeValue( edge.child, edge.isWell(), metric, timeStep ); if ( !value ) continue; lines.append( QString( "%1 (%2): %3" ) .arg( QString::fromStdString( RifGroupNetworkSummaryData::nodeMetricText( metric ) ) ) .arg( QString::fromStdString( summaryData()->nodeAddress( edge.child, edge.isWell(), metric ).vectorName() ) ) .arg( valueWithUnit( value, summaryData()->nodeUnit( metric ) ) ) ); } if ( lines.size() == 1 ) lines.append( "No summary vectors found for the node" ); return lines.join( "\n" ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimGroupNetworkPlot::toolTip( const RigGroupNetwork::Edge& edge, const RimGroupNetworkGraph::Node& node ) const { QStringList lines; lines.append( QString( "%1 (%2)" ).arg( node.name.toHtmlEscaped(), edge.isWell() ? "well" : "group" ) ); if ( !edge.isRoot() ) lines.append( QString( "Parent: %1" ).arg( node.parent.toHtmlEscaped() ) ); lines.append( QString( "Defined by: %1" ).arg( QString::fromStdString( RigGroupNetwork::keywordText( edge.keyword ) ).toHtmlEscaped() ) ); const QString properties = propertiesText( edge ); if ( !properties.isEmpty() ) lines.append( properties ); if ( summaryData() ) { const QString edgeName = QString::fromStdString( RifGroupNetworkSummaryData::edgeMetricText( m_edgeMetric() ) ); const QString nodeName = QString::fromStdString( RifGroupNetworkSummaryData::nodeMetricText( m_nodeMetric() ) ); lines.append( QString( "%1: %2" ).arg( edgeName, node.edgeText.isEmpty() ? "-" : node.edgeText ) ); lines.append( QString( "%1: %2" ).arg( nodeName, node.valueText.isEmpty() ? "-" : node.valueText ) ); } return lines.join( "
" ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimGroupNetworkPlot::propertiesText( const RigGroupNetwork::Edge& edge ) { QStringList texts; for ( const auto& [key, value] : edge.properties ) { texts.append( QString( "%1: %2" ).arg( QString::fromStdString( key ).toHtmlEscaped() ).arg( value ) ); } return texts.join( ", " ); }