diff --git a/ApplicationExeCode/Resources/Download.svg b/ApplicationExeCode/Resources/Download.svg
new file mode 100644
index 0000000000..49f95179e8
--- /dev/null
+++ b/ApplicationExeCode/Resources/Download.svg
@@ -0,0 +1,11 @@
+
+
+
diff --git a/ApplicationExeCode/Resources/ResInsight.qrc b/ApplicationExeCode/Resources/ResInsight.qrc
index 8d214434cc..8ba437ff64 100644
--- a/ApplicationExeCode/Resources/ResInsight.qrc
+++ b/ApplicationExeCode/Resources/ResInsight.qrc
@@ -237,6 +237,7 @@
Erase.svg
Close.svg
Refresh.svg
+ Download.svg
CellFilter.png
FilterFunction.svg
FilterParameter.svg
diff --git a/ApplicationLibCode/Application/Tools/Cloud/CMakeLists_files.cmake b/ApplicationLibCode/Application/Tools/Cloud/CMakeLists_files.cmake
index b34b6e5153..d45aeb8295 100644
--- a/ApplicationLibCode/Application/Tools/Cloud/CMakeLists_files.cmake
+++ b/ApplicationLibCode/Application/Tools/Cloud/CMakeLists_files.cmake
@@ -6,6 +6,7 @@ set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/RiaSumoExplore.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaSumoGrid.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaSumoSummary.cpp
+ ${CMAKE_CURRENT_LIST_DIR}/RiaSumoPolygons.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaConnectorTools.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaOsduConnector.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaOAuthHttpServerReplyHandler.cpp
diff --git a/ApplicationLibCode/Application/Tools/Cloud/RiaCloudApiService.cpp b/ApplicationLibCode/Application/Tools/Cloud/RiaCloudApiService.cpp
index 99f7bc927a..b4cf06269f 100644
--- a/ApplicationLibCode/Application/Tools/Cloud/RiaCloudApiService.cpp
+++ b/ApplicationLibCode/Application/Tools/Cloud/RiaCloudApiService.cpp
@@ -29,6 +29,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -75,6 +76,10 @@ RiaCloudApiService::RiaCloudApiService( const QString& serverAddress, int wanted
, m_consecutiveFailures( 0 )
, m_isResponding( false )
{
+ // Never route loopback requests through a system/corporate proxy, which can add several seconds
+ // per request.
+ m_networkAccessManager->setProxy( QNetworkProxy::NoProxy );
+
m_healthTimer.setInterval( healthCheckIntervalMs );
connect( &m_healthTimer, &QTimer::timeout, this, &RiaCloudApiService::onHealthCheck );
@@ -245,6 +250,11 @@ bool RiaCloudApiService::waitUntilResponding( int timeoutMs )
if ( !isRunning() ) start();
+ // Nested event loops below let the periodic health check fire and restart() the service while this
+ // call is still waiting for it to boot. Stop both timers for the duration of the wait to avoid that.
+ m_startupTimer.stop();
+ m_healthTimer.stop();
+
QElapsedTimer elapsed;
elapsed.start();
@@ -297,6 +307,10 @@ bool RiaCloudApiService::waitUntilResponding( int timeoutMs )
RiaLogging::error( std::format( "Cloud API service: not responding after {} ms, giving up.", elapsed.elapsed() ) );
+ // Resume background health-checking if the process is still running, so it can still recover on
+ // its own without another blocking wait.
+ if ( isRunning() ) m_healthTimer.start();
+
return false;
}
diff --git a/ApplicationLibCode/Application/Tools/Cloud/RiaSumoConnector.cpp b/ApplicationLibCode/Application/Tools/Cloud/RiaSumoConnector.cpp
index a88dc2875e..fdba4fa041 100644
--- a/ApplicationLibCode/Application/Tools/Cloud/RiaSumoConnector.cpp
+++ b/ApplicationLibCode/Application/Tools/Cloud/RiaSumoConnector.cpp
@@ -55,6 +55,7 @@ RiaSumoConnector::RiaSumoConnector( QObject* parent,
, m_explore( *this )
, m_grid( *this )
, m_summary( *this )
+ , m_polygons( *this )
{
// The transfer thread runs the network requests issued by the blocking wrappers, so the calling thread can
// wait for them without dispatching events. The context object gives us something with transfer thread
@@ -102,6 +103,14 @@ RiaSumoSummary& RiaSumoConnector::summary()
return m_summary;
}
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+RiaSumoPolygons& RiaSumoConnector::polygons()
+{
+ return m_polygons;
+}
+
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
diff --git a/ApplicationLibCode/Application/Tools/Cloud/RiaSumoConnector.h b/ApplicationLibCode/Application/Tools/Cloud/RiaSumoConnector.h
index 0ceed3c88e..fbfd6f6ce9 100644
--- a/ApplicationLibCode/Application/Tools/Cloud/RiaSumoConnector.h
+++ b/ApplicationLibCode/Application/Tools/Cloud/RiaSumoConnector.h
@@ -22,6 +22,7 @@
#include "RiaSumoDefines.h"
#include "RiaSumoExplore.h"
#include "RiaSumoGrid.h"
+#include "RiaSumoPolygons.h"
#include "RiaSumoSummary.h"
#include
@@ -75,6 +76,9 @@ public:
// The summary data of a case.
RiaSumoSummary& summary();
+ // The polygon results of an ensemble.
+ RiaSumoPolygons& polygons();
+
// Transport used by the data specific delegates. Every request goes through the transfer thread, so
// the calling thread waits without dispatching events.
// Issue a GET against the local cloud API service and return the response body. Takes the path only,
@@ -179,9 +183,10 @@ private:
private:
std::function m_serverUrlProvider;
- RiaSumoExplore m_explore;
- RiaSumoGrid m_grid;
- RiaSumoSummary m_summary;
+ RiaSumoExplore m_explore;
+ RiaSumoGrid m_grid;
+ RiaSumoSummary m_summary;
+ RiaSumoPolygons m_polygons;
// Transfers run on their own thread so the calling thread can wait without dispatching events. Waiting on
// a nested event loop on the GUI thread let the view update code re-enter a load that was still running,
diff --git a/ApplicationLibCode/Application/Tools/Cloud/RiaSumoPolygons.cpp b/ApplicationLibCode/Application/Tools/Cloud/RiaSumoPolygons.cpp
new file mode 100644
index 0000000000..7c18f5a8b2
--- /dev/null
+++ b/ApplicationLibCode/Application/Tools/Cloud/RiaSumoPolygons.cpp
@@ -0,0 +1,162 @@
+/////////////////////////////////////////////////////////////////////////////////
+//
+// 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 "RiaSumoPolygons.h"
+
+#include "RiaLogging.h"
+#include "RiaSumoConnector.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+RiaSumoPolygons::RiaSumoPolygons( RiaSumoConnector& connector )
+ : m_connector( connector )
+{
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+SumoPolygonDirectory RiaSumoPolygons::polygonResultDirectory( const SumoCaseId& caseId, const QString& ensembleName )
+{
+ const QString encodedEnsembleName = QUrl::toPercentEncoding( ensembleName );
+ const QString path = QString( "/cases/%1/ensembles/%2/polygon_result_directory" ).arg( caseId.get() ).arg( encodedEnsembleName );
+
+ return parseDirectory( m_connector.getBlocking( path, "Loading polygon result directory from Sumo" ) );
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+std::vector RiaSumoPolygons::polygonsData( const SumoCaseId& caseId,
+ const QString& ensembleName,
+ int realization,
+ SumoPolygonResult polygonResult,
+ const QString& name,
+ const QString& contactType )
+{
+ const QString encodedEnsembleName = QUrl::toPercentEncoding( ensembleName );
+
+ QUrlQuery query;
+ query.addQueryItem( "realization", QString::number( realization ) );
+ query.addQueryItem( "polygon_result", polygonResultKey( polygonResult ) );
+ if ( polygonResult != SumoPolygonResult::FieldOutline && !name.isEmpty() )
+ {
+ query.addQueryItem( "name", name );
+ }
+ if ( polygonResult == SumoPolygonResult::FluidContactOutline && !contactType.isEmpty() )
+ {
+ query.addQueryItem( "contact_type", contactType );
+ }
+
+ const QString path =
+ QString( "/cases/%1/ensembles/%2/polygons_data?%3" ).arg( caseId.get() ).arg( encodedEnsembleName ).arg( query.toString( QUrl::FullyEncoded ) );
+
+ return parsePolygonsData( m_connector.getBlocking( path, "Loading polygon data from Sumo" ) );
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+QString RiaSumoPolygons::polygonResultKey( SumoPolygonResult polygonResult )
+{
+ switch ( polygonResult )
+ {
+ case SumoPolygonResult::FieldOutline:
+ return "field_outline";
+ case SumoPolygonResult::StructureDepthFaultLines:
+ return "structure_depth_fault_lines";
+ case SumoPolygonResult::FluidContactOutline:
+ return "fluid_contact_outline";
+ }
+
+ return {};
+}
+
+//--------------------------------------------------------------------------------------------------
+/// Parses the polygon_result_directory response: a JSON object keyed by category, each value a list of
+/// { name } or { name, contactType } objects. A category with no matches is omitted by the API.
+//--------------------------------------------------------------------------------------------------
+SumoPolygonDirectory RiaSumoPolygons::parseDirectory( const QByteArray& body )
+{
+ SumoPolygonDirectory directory;
+
+ QJsonDocument doc = QJsonDocument::fromJson( body );
+ QJsonObject obj = doc.object();
+
+ auto parseMetaArray = []( const QJsonArray& array )
+ {
+ std::vector result;
+ for ( const QJsonValue& value : array )
+ {
+ QJsonObject metaObj = value.toObject();
+ result.push_back( SumoPolygonMeta{ metaObj["name"].toString(), metaObj["contactType"].toString() } );
+ }
+ return result;
+ };
+
+ directory.fieldOutline = parseMetaArray( obj[polygonResultKey( SumoPolygonResult::FieldOutline )].toArray() );
+ directory.structureDepthFaultLines = parseMetaArray( obj[polygonResultKey( SumoPolygonResult::StructureDepthFaultLines )].toArray() );
+ directory.fluidContactOutline = parseMetaArray( obj[polygonResultKey( SumoPolygonResult::FluidContactOutline )].toArray() );
+
+ return directory;
+}
+
+//--------------------------------------------------------------------------------------------------
+/// Parses the polygons_data response: a JSON array of { xUtmEArr, yUtmNArray, zTvdSSArray, polyId, name }.
+//--------------------------------------------------------------------------------------------------
+std::vector RiaSumoPolygons::parsePolygonsData( const QByteArray& body )
+{
+ std::vector polygonDataList;
+
+ QJsonDocument doc = QJsonDocument::fromJson( body );
+ QJsonArray jsonArray = doc.array();
+
+ for ( const QJsonValue& value : jsonArray )
+ {
+ QJsonObject obj = value.toObject();
+
+ SumoPolygonData data;
+ data.name = obj["name"].toString();
+
+ // polyId can be an int or a string on the wire; keep it as a string either way.
+ const QJsonValue polyIdValue = obj["polyId"];
+ data.polyId = polyIdValue.isString() ? polyIdValue.toString() : QString::number( polyIdValue.toInt() );
+
+ for ( const QJsonValue& x : obj["xUtmEArr"].toArray() )
+ data.xArr.push_back( x.toDouble() );
+ for ( const QJsonValue& y : obj["yUtmNArray"].toArray() )
+ data.yArr.push_back( y.toDouble() );
+ for ( const QJsonValue& z : obj["zTvdSSArray"].toArray() )
+ data.zArr.push_back( z.toDouble() );
+
+ polygonDataList.push_back( data );
+ }
+
+ RiaLogging::debug( std::format( "Polygon data count : {}", polygonDataList.size() ) );
+
+ return polygonDataList;
+}
diff --git a/ApplicationLibCode/Application/Tools/Cloud/RiaSumoPolygons.h b/ApplicationLibCode/Application/Tools/Cloud/RiaSumoPolygons.h
new file mode 100644
index 0000000000..972bd8a2d8
--- /dev/null
+++ b/ApplicationLibCode/Application/Tools/Cloud/RiaSumoPolygons.h
@@ -0,0 +1,94 @@
+/////////////////////////////////////////////////////////////////////////////////
+//
+// 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.
+//
+/////////////////////////////////////////////////////////////////////////////////
+
+#pragma once
+
+#include "RiaSumoDefines.h"
+
+#include
+#include
+
+#include
+
+class RiaSumoConnector;
+
+// Category of a polygon result, matching ri-cloud-api's PolygonResult enum.
+enum class SumoPolygonResult
+{
+ FieldOutline,
+ StructureDepthFaultLines,
+ FluidContactOutline
+};
+
+// One named polygon result. contactType is set only for fluid contact outline entries.
+struct SumoPolygonMeta
+{
+ QString name;
+ QString contactType;
+};
+
+// Polygon results available for an ensemble, categorized as ri-cloud-api reports them. An absent
+// category is left empty.
+struct SumoPolygonDirectory
+{
+ std::vector fieldOutline;
+ std::vector structureDepthFaultLines;
+ std::vector fluidContactOutline;
+};
+
+// Decoded geometry of one polygon (one POLY_ID group), already in [x,y,z] form -- ri-cloud-api
+// owns the Sumo schema and returns plain coordinate arrays.
+struct SumoPolygonData
+{
+ QString name;
+ QString polyId;
+ std::vector xArr;
+ std::vector yArr;
+ std::vector zArr;
+};
+
+//==================================================================================================
+/// Polygon results of a Sumo ensemble: what is available (polygon_result_directory) and decoded
+/// coordinates of a chosen one (polygons_data). Both endpoints return plain JSON, so there is no
+/// blob id/download step, unlike grid and summary data.
+//==================================================================================================
+class RiaSumoPolygons
+{
+public:
+ explicit RiaSumoPolygons( RiaSumoConnector& connector );
+
+ SumoPolygonDirectory polygonResultDirectory( const SumoCaseId& caseId, const QString& ensembleName );
+
+ // name is ignored for FieldOutline. contactType is required only for FluidContactOutline.
+ std::vector polygonsData( const SumoCaseId& caseId,
+ const QString& ensembleName,
+ int realization,
+ SumoPolygonResult polygonResult,
+ const QString& name = QString(),
+ const QString& contactType = QString() );
+
+ // Query-parameter/JSON-key spelling of a polygon result category.
+ static QString polygonResultKey( SumoPolygonResult polygonResult );
+
+private:
+ static SumoPolygonDirectory parseDirectory( const QByteArray& body );
+ static std::vector parsePolygonsData( const QByteArray& body );
+
+private:
+ RiaSumoConnector& m_connector;
+};
diff --git a/ApplicationLibCode/CMakeLists.txt b/ApplicationLibCode/CMakeLists.txt
index 1d8a2f7f96..58dccbac4f 100644
--- a/ApplicationLibCode/CMakeLists.txt
+++ b/ApplicationLibCode/CMakeLists.txt
@@ -127,6 +127,7 @@ list(
ProjectDataModel/CellFilters/CMakeLists_files.cmake
ProjectDataModel/ProcessControl/CMakeLists_files.cmake
ProjectDataModel/Polygons/CMakeLists_files.cmake
+ ProjectDataModel/Polygons/Cloud/CMakeLists_files.cmake
ProjectDataModel/WellLog/CMakeLists_files.cmake
ProjectDataModel/WellLog/WellLogTrack/CMakeLists_files.cmake
ProjectDataModel/WellMeasurement/CMakeLists_files.cmake
diff --git a/ApplicationLibCode/Commands/PolygonCommands/CMakeLists_files.cmake b/ApplicationLibCode/Commands/PolygonCommands/CMakeLists_files.cmake
index 3a8903c349..48d73ccd2c 100644
--- a/ApplicationLibCode/Commands/PolygonCommands/CMakeLists_files.cmake
+++ b/ApplicationLibCode/Commands/PolygonCommands/CMakeLists_files.cmake
@@ -2,6 +2,8 @@ set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/RicCreatePolygonFeature.cpp
${CMAKE_CURRENT_LIST_DIR}/RicImportPolygonFileFeature.cpp
${CMAKE_CURRENT_LIST_DIR}/RicReloadPolygonFileFeature.cpp
+ ${CMAKE_CURRENT_LIST_DIR}/RicAddCloudPolygonSourceFeature.cpp
+ ${CMAKE_CURRENT_LIST_DIR}/RicReloadPolygonCloudAddressFeature.cpp
${CMAKE_CURRENT_LIST_DIR}/RicDuplicatePolygonFeature.cpp
${CMAKE_CURRENT_LIST_DIR}/RicExportPolygonCsvFeature.cpp
${CMAKE_CURRENT_LIST_DIR}/RicExportPolygonPolFeature.cpp
diff --git a/ApplicationLibCode/Commands/PolygonCommands/RicAddCloudPolygonSourceFeature.cpp b/ApplicationLibCode/Commands/PolygonCommands/RicAddCloudPolygonSourceFeature.cpp
new file mode 100644
index 0000000000..6934a1e6e9
--- /dev/null
+++ b/ApplicationLibCode/Commands/PolygonCommands/RicAddCloudPolygonSourceFeature.cpp
@@ -0,0 +1,81 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// 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 "RicAddCloudPolygonSourceFeature.h"
+
+#include "RiaLogging.h"
+
+#include "Cloud/RimCloudDataSourceCollection.h"
+#include "Cloud/RimSumoDataSource.h"
+
+#include "Polygons/Cloud/RimPolygonCloudSource.h"
+#include "Polygons/RimPolygonCollection.h"
+#include "RimTools.h"
+
+#include "Riu3DMainWindowTools.h"
+
+#include "cafSelectionManagerTools.h"
+
+#include
+
+CAF_CMD_SOURCE_INIT( RicAddCloudPolygonSourceFeature, "RicAddCloudPolygonSourceFeature" );
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+void RicAddCloudPolygonSourceFeature::onActionTriggered( bool isChecked )
+{
+ RimPolygonCollection* polygonCollection = nullptr;
+
+ auto selected = caf::selectedObjectsByTypeStrict();
+ if ( !selected.empty() ) polygonCollection = selected.front();
+
+ if ( !polygonCollection ) polygonCollection = RimTools::polygonCollection();
+ if ( !polygonCollection ) return;
+
+ auto dataSources = RimCloudDataSourceCollection::instance()->sumoDataSources();
+ if ( dataSources.empty() )
+ {
+ RiaLogging::warning( "No Sumo data sources configured. Add one before adding a cloud polygon source." );
+ return;
+ }
+
+ // Preselect the first Data Source + Realization as a convenience default (Apply is still
+ // required to fetch the directory and build the tree).
+ auto* source = new RimPolygonCloudSource();
+
+ source->setDataSource( dataSources.front() );
+ auto realizations = source->availableRealizationIdsForOverride();
+ if ( !realizations.empty() ) source->setBaseRealization( realizations.front() );
+
+ polygonCollection->addPolygonCloudSource( source );
+
+ polygonCollection->uiCapability()->updateAllRequiredEditors();
+
+ Riu3DMainWindowTools::setExpanded( source );
+ Riu3DMainWindowTools::selectAsCurrentItem( source );
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+void RicAddCloudPolygonSourceFeature::setupActionLook( QAction* actionToSetup )
+{
+ actionToSetup->setText( "Add Cloud Polygon Source" );
+ actionToSetup->setIcon( QIcon( ":/PolylinesFromFile16x16.png" ) );
+}
diff --git a/ApplicationLibCode/Commands/PolygonCommands/RicAddCloudPolygonSourceFeature.h b/ApplicationLibCode/Commands/PolygonCommands/RicAddCloudPolygonSourceFeature.h
new file mode 100644
index 0000000000..56e3f68a7a
--- /dev/null
+++ b/ApplicationLibCode/Commands/PolygonCommands/RicAddCloudPolygonSourceFeature.h
@@ -0,0 +1,34 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// 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.
+//
+/////////////////////////////////////////////////////////////////////////////////
+
+#pragma once
+
+#include "cafCmdFeature.h"
+
+//==================================================================================================
+/// "Add Cloud Polygon Source": creates a RimPolygonCloudSource with a preselected Sumo data
+/// source and base realization; the user clicks "Apply" to build the browsable directory tree.
+//==================================================================================================
+class RicAddCloudPolygonSourceFeature : public caf::CmdFeature
+{
+ CAF_CMD_HEADER_INIT;
+
+protected:
+ void onActionTriggered( bool isChecked ) override;
+ void setupActionLook( QAction* actionToSetup ) override;
+};
diff --git a/ApplicationLibCode/Commands/PolygonCommands/RicReloadPolygonCloudAddressFeature.cpp b/ApplicationLibCode/Commands/PolygonCommands/RicReloadPolygonCloudAddressFeature.cpp
new file mode 100644
index 0000000000..34ce67dacd
--- /dev/null
+++ b/ApplicationLibCode/Commands/PolygonCommands/RicReloadPolygonCloudAddressFeature.cpp
@@ -0,0 +1,65 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright (C) 2025 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 "RicReloadPolygonCloudAddressFeature.h"
+
+#include "Polygons/Cloud/RimPolygonCloudAddress.h"
+
+#include "cafSelectionManager.h"
+#include "cafSelectionManagerTools.h"
+
+#include
+
+CAF_CMD_SOURCE_INIT( RicReloadPolygonCloudAddressFeature, "RicReloadPolygonCloudAddressFeature" );
+
+//--------------------------------------------------------------------------------------------------
+/// Reloads whatever is currently visualized for the selected address(es): re-fetches base data
+/// and any cached realizations. Does not rebuild the owning source's directory tree.
+//--------------------------------------------------------------------------------------------------
+void RicReloadPolygonCloudAddressFeature::onActionTriggered( bool isChecked )
+{
+ auto cloudAddresses = caf::selectedObjectsByType();
+
+ for ( auto address : cloudAddresses )
+ {
+ std::vector cachedRealizations = address->cachedRealizations();
+
+ if ( address->hasBaseData() )
+ {
+ address->evictBaseData();
+ address->ensureBaseFetched();
+ }
+
+ for ( int realization : cachedRealizations )
+ {
+ address->evictRealizationIfUnused( realization );
+ address->ensureRealizationFetched( realization );
+ }
+
+ address->updateAllRequiredEditors();
+ }
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+void RicReloadPolygonCloudAddressFeature::setupActionLook( QAction* actionToSetup )
+{
+ actionToSetup->setText( "Reload" );
+ actionToSetup->setIcon( QIcon( ":/Refresh.svg" ) );
+}
diff --git a/ApplicationLibCode/Commands/PolygonCommands/RicReloadPolygonCloudAddressFeature.h b/ApplicationLibCode/Commands/PolygonCommands/RicReloadPolygonCloudAddressFeature.h
new file mode 100644
index 0000000000..b4f8f01fc6
--- /dev/null
+++ b/ApplicationLibCode/Commands/PolygonCommands/RicReloadPolygonCloudAddressFeature.h
@@ -0,0 +1,33 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright (C) 2025 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.
+//
+/////////////////////////////////////////////////////////////////////////////////
+
+#pragma once
+
+#include "cafCmdFeature.h"
+
+//==================================================================================================
+///
+//==================================================================================================
+class RicReloadPolygonCloudAddressFeature : public caf::CmdFeature
+{
+ CAF_CMD_HEADER_INIT;
+
+protected:
+ void onActionTriggered( bool isChecked ) override;
+ void setupActionLook( QAction* actionToSetup ) override;
+};
diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/CMakeLists_files.cmake b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/CMakeLists_files.cmake
new file mode 100644
index 0000000000..122be7df8f
--- /dev/null
+++ b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/CMakeLists_files.cmake
@@ -0,0 +1,8 @@
+set(SOURCE_GROUP_SOURCE_FILES
+ ${CMAKE_CURRENT_LIST_DIR}/RimPolygonCloudAddress.cpp
+ ${CMAKE_CURRENT_LIST_DIR}/RimCloudPolygon.cpp
+ ${CMAKE_CURRENT_LIST_DIR}/RimPolygonCloudFolder.cpp
+ ${CMAKE_CURRENT_LIST_DIR}/RimPolygonCloudSource.cpp
+)
+
+list(APPEND CODE_SOURCE_FILES ${SOURCE_GROUP_SOURCE_FILES})
diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimCloudPolygon.cpp b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimCloudPolygon.cpp
new file mode 100644
index 0000000000..bfdc02a4cd
--- /dev/null
+++ b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimCloudPolygon.cpp
@@ -0,0 +1,108 @@
+/////////////////////////////////////////////////////////////////////////////////
+//
+// 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 "RimCloudPolygon.h"
+
+CAF_PDM_SOURCE_INIT( RimCloudPolygon, "RimCloudPolygon" );
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+RimCloudPolygon::RimCloudPolygon()
+{
+ CAF_PDM_InitObject( "Sumo Polygon", ":/PolylinesFromFile16x16.png" );
+
+ CAF_PDM_InitFieldNoDefault( &m_caseId, "SumoCaseId", "Case Id" );
+ m_caseId.uiCapability()->setUiHidden( true );
+ CAF_PDM_InitFieldNoDefault( &m_ensembleName, "SumoEnsembleName", "Ensemble Name" );
+ m_ensembleName.uiCapability()->setUiHidden( true );
+ CAF_PDM_InitField( &m_realization, "SumoRealization", -1, "Realization" );
+ m_realization.uiCapability()->setUiHidden( true );
+ CAF_PDM_InitFieldNoDefault( &m_polygonResult, "SumoPolygonResult", "Polygon Result" );
+ m_polygonResult.uiCapability()->setUiHidden( true );
+ CAF_PDM_InitFieldNoDefault( &m_sumoName, "SumoPolygonName", "Sumo Name" );
+ m_sumoName.uiCapability()->setUiHidden( true );
+ CAF_PDM_InitFieldNoDefault( &m_contactType, "SumoContactType", "Fluid Contact Type" );
+ m_contactType.uiCapability()->setUiHidden( true );
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+void RimCloudPolygon::setSumoIdentity( const QString& caseId,
+ const QString& ensembleName,
+ int realization,
+ const QString& polygonResult,
+ const QString& sumoName,
+ const QString& contactType )
+{
+ m_caseId = caseId;
+ m_ensembleName = ensembleName;
+ m_realization = realization;
+ m_polygonResult = polygonResult;
+ m_sumoName = sumoName;
+ m_contactType = contactType;
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+QString RimCloudPolygon::caseId() const
+{
+ return m_caseId();
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+QString RimCloudPolygon::ensembleName() const
+{
+ return m_ensembleName();
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+int RimCloudPolygon::realization() const
+{
+ return m_realization();
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+QString RimCloudPolygon::polygonResult() const
+{
+ return m_polygonResult();
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+QString RimCloudPolygon::sumoName() const
+{
+ return m_sumoName();
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+QString RimCloudPolygon::contactType() const
+{
+ return m_contactType();
+}
diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimCloudPolygon.h b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimCloudPolygon.h
new file mode 100644
index 0000000000..996a06cbab
--- /dev/null
+++ b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimCloudPolygon.h
@@ -0,0 +1,64 @@
+/////////////////////////////////////////////////////////////////////////////////
+//
+// 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.
+//
+/////////////////////////////////////////////////////////////////////////////////
+
+#pragma once
+
+#include "Polygons/RimPolygon.h"
+
+#include "cafPdmField.h"
+
+//==================================================================================================
+///
+/// A RimPolygon fetched from Sumo, stamped with the full Sumo identity (case, ensemble,
+/// realization, polygon result, name, contact type) it was fetched with -- a self-contained,
+/// immutable snapshot that a RimPolygonCloudAddress can hand out for any realization without
+/// external bookkeeping or mutation.
+///
+/// Identity fields are hidden from the UI (traceability/debugging only) -- editing happens on
+/// RimPolygonCloudAddress.
+///
+//==================================================================================================
+class RimCloudPolygon : public RimPolygon
+{
+ CAF_PDM_HEADER_INIT;
+
+public:
+ RimCloudPolygon();
+
+ void setSumoIdentity( const QString& caseId,
+ const QString& ensembleName,
+ int realization,
+ const QString& polygonResult,
+ const QString& sumoName,
+ const QString& contactType );
+
+ QString caseId() const;
+ QString ensembleName() const;
+ int realization() const;
+ QString polygonResult() const;
+ QString sumoName() const;
+ QString contactType() const;
+
+private:
+ caf::PdmField m_caseId;
+ caf::PdmField m_ensembleName;
+ caf::PdmField m_realization;
+ caf::PdmField m_polygonResult;
+ caf::PdmField m_sumoName;
+ caf::PdmField m_contactType;
+};
diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudAddress.cpp b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudAddress.cpp
new file mode 100644
index 0000000000..baea0d8f33
--- /dev/null
+++ b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudAddress.cpp
@@ -0,0 +1,456 @@
+/////////////////////////////////////////////////////////////////////////////////
+//
+// 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 "RimPolygonCloudAddress.h"
+
+#include "RiaApplication.h"
+#include "RiaLogging.h"
+
+#include "Cloud/RiaSumoConnector.h"
+#include "Cloud/RimSumoDataSource.h"
+
+#include "Polygons/Cloud/RimCloudPolygon.h"
+#include "Polygons/Cloud/RimPolygonCloudSource.h"
+#include "Polygons/RimPolygon.h"
+#include "Polygons/RimPolygonInViewCollection.h"
+
+#include "cafCmdFeatureMenuBuilder.h"
+#include "cafPdmUiTreeAttributes.h"
+
+#include
+
+#include
+
+CAF_PDM_SOURCE_INIT( RimPolygonCloudAddress, "RimPolygonCloudAddress" );
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+RimPolygonCloudAddress::RimPolygonCloudAddress()
+ : objectChanged( this )
+{
+ CAF_PDM_InitObject( "Sumo Polygon Address", ":/CloudBlobs.svg" );
+
+ // Inherited fields, redeclared here so this subclass gets its own ui/xml keyword.
+ CAF_PDM_InitFieldNoDefault( &m_collectionName, "Name", "Name" );
+ CAF_PDM_InitFieldNoDefault( &m_subCollections, "SubCollections", "Subcollections" );
+ m_subCollections.uiCapability()->setUiHidden( true );
+ CAF_PDM_InitFieldNoDefault( &m_items, "Polygons", "Polygons" );
+
+ // Fixed identity, set once via configureIdentity() -- read-only in the UI.
+ CAF_PDM_InitField( &m_polygonResult, "PolygonResult", QString( "field_outline" ), "Polygon Result" );
+ m_polygonResult.uiCapability()->setUiReadOnly( true );
+ CAF_PDM_InitField( &m_name, "PolygonName", QString(), "Name" );
+ m_name.uiCapability()->setUiReadOnly( true );
+ CAF_PDM_InitField( &m_contactType, "ContactType", QString(), "Fluid Contact Type" );
+ m_contactType.uiCapability()->setUiReadOnly( true );
+
+ setDeletable( false );
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+RimPolygonCloudAddress::~RimPolygonCloudAddress()
+{
+ for ( auto& [realization, polygons] : m_realizationCache )
+ {
+ for ( auto* polygon : polygons )
+ {
+ delete polygon;
+ }
+ }
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+void RimPolygonCloudAddress::configureIdentity( SumoPolygonResult polygonResult, const QString& sumoName, const QString& contactType )
+{
+ m_polygonResult = RiaSumoPolygons::polygonResultKey( polygonResult );
+ m_name = sumoName;
+ m_contactType = contactType;
+
+ updateName();
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+SumoPolygonResult RimPolygonCloudAddress::polygonResult() const
+{
+ if ( m_polygonResult() == RiaSumoPolygons::polygonResultKey( SumoPolygonResult::StructureDepthFaultLines ) )
+ {
+ return SumoPolygonResult::StructureDepthFaultLines;
+ }
+ if ( m_polygonResult() == RiaSumoPolygons::polygonResultKey( SumoPolygonResult::FluidContactOutline ) )
+ {
+ return SumoPolygonResult::FluidContactOutline;
+ }
+ return SumoPolygonResult::FieldOutline;
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+QString RimPolygonCloudAddress::sumoName() const
+{
+ return m_name();
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+QString RimPolygonCloudAddress::contactType() const
+{
+ return m_contactType();
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+RimPolygonCloudSource* RimPolygonCloudAddress::owningSource() const
+{
+ return firstAncestorOfType();
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+bool RimPolygonCloudAddress::hasBaseData() const
+{
+ return !items().empty();
+}
+
+//--------------------------------------------------------------------------------------------------
+/// Idempotent no-op if already fetched.
+//--------------------------------------------------------------------------------------------------
+void RimPolygonCloudAddress::ensureBaseFetched()
+{
+ if ( hasBaseData() ) return;
+
+ auto* source = owningSource();
+ if ( !source ) return;
+
+ auto fetchedPolygons = fetchPolygonsFromSumo( source->baseRealization() );
+
+ m_items.deleteChildren();
+ m_items.setValue( fetchedPolygons );
+ for ( auto* polygon : fetchedPolygons )
+ {
+ ensureUniquePolygonName( polygon );
+ }
+
+ if ( fetchedPolygons.empty() )
+ {
+ RiaLogging::warning( "No polygons found for Sumo polygon address: " + name().toStdString() );
+ }
+ else
+ {
+ RiaLogging::info(
+ QString( "Fetched %1 polygon(s) from Sumo for address: %2" ).arg( fetchedPolygons.size() ).arg( name() ).toStdString() );
+ }
+
+ // Structural refresh needed: the tree node already exists with zero children.
+ uiCapability()->updateAllRequiredEditors();
+
+ objectChanged.send();
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+void RimPolygonCloudAddress::evictBaseData()
+{
+ if ( !hasBaseData() ) return;
+
+ m_items.deleteChildren();
+ uiCapability()->updateAllRequiredEditors();
+ objectChanged.send();
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+bool RimPolygonCloudAddress::hasDataForRealization( int realization ) const
+{
+ auto it = m_realizationCache.find( realization );
+ return it != m_realizationCache.end() && !it->second.empty();
+}
+
+//--------------------------------------------------------------------------------------------------
+/// Idempotent no-op if already cached.
+//--------------------------------------------------------------------------------------------------
+void RimPolygonCloudAddress::ensureRealizationFetched( int realization )
+{
+ if ( m_realizationCache.find( realization ) != m_realizationCache.end() ) return;
+
+ auto fetchedPolygons = fetchPolygonsFromSumo( realization );
+ for ( auto* polygon : fetchedPolygons )
+ {
+ ensureUniquePolygonName( polygon );
+ }
+
+ m_realizationCache[realization] = fetchedPolygons;
+
+ if ( fetchedPolygons.empty() )
+ {
+ RiaLogging::warning(
+ QString( "No polygons found for Sumo polygon address: %1 (realization %2)" ).arg( name() ).arg( realization ).toStdString() );
+ }
+ else
+ {
+ RiaLogging::info( QString( "Fetched %1 polygon(s) from Sumo for address: %2 (realization %3)" )
+ .arg( fetchedPolygons.size() )
+ .arg( name() )
+ .arg( realization )
+ .toStdString() );
+ }
+}
+
+//--------------------------------------------------------------------------------------------------
+/// Pure read, never fetches. Empty if not cached, see ensureRealizationFetched().
+//--------------------------------------------------------------------------------------------------
+std::vector RimPolygonCloudAddress::cachedItemsForRealization( int realization ) const
+{
+ auto it = m_realizationCache.find( realization );
+ if ( it == m_realizationCache.end() ) return {};
+ return it->second;
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+void RimPolygonCloudAddress::evictRealizationIfUnused( int realization )
+{
+ auto it = m_realizationCache.find( realization );
+ if ( it == m_realizationCache.end() ) return;
+
+ for ( auto* polygon : it->second )
+ {
+ delete polygon;
+ }
+ m_realizationCache.erase( it );
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+std::vector RimPolygonCloudAddress::cachedRealizations() const
+{
+ std::vector realizations;
+ for ( const auto& [realization, polygons] : m_realizationCache )
+ {
+ realizations.push_back( realization );
+ }
+ return realizations;
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+QString RimPolygonCloudAddress::name() const
+{
+ QString nameCandidate = m_collectionName.value();
+ if ( !nameCandidate.isEmpty() ) return nameCandidate;
+
+ return "Sumo Polygon Address";
+}
+
+//--------------------------------------------------------------------------------------------------
+/// Realization groups are only ever added programmatically.
+//--------------------------------------------------------------------------------------------------
+bool RimPolygonCloudAddress::canAddSubCollection() const
+{
+ return false;
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+RimPolygonContainer* RimPolygonCloudAddress::addNewSubCollection()
+{
+ return nullptr;
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+void RimPolygonCloudAddress::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering )
+{
+ uiOrdering.add( &m_collectionName );
+ uiOrdering.add( &m_polygonResult );
+
+ const bool isFieldOutline = ( m_polygonResult() == RiaSumoPolygons::polygonResultKey( SumoPolygonResult::FieldOutline ) );
+ const bool isFluidContact = ( m_polygonResult() == RiaSumoPolygons::polygonResultKey( SumoPolygonResult::FluidContactOutline ) );
+
+ m_name.uiCapability()->setUiHidden( isFieldOutline );
+ uiOrdering.add( &m_name );
+
+ m_contactType.uiCapability()->setUiHidden( !isFluidContact );
+ uiOrdering.add( &m_contactType );
+
+ uiOrdering.skipRemainingFields();
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+void RimPolygonCloudAddress::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const
+{
+ menuBuilder << "RicReloadPolygonCloudAddressFeature";
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+void RimPolygonCloudAddress::defineObjectEditorAttribute( QString uiConfigName, caf::PdmUiEditorAttribute* attribute )
+{
+ if ( hasBaseData() ) return;
+
+ auto* treeItemAttribute = dynamic_cast( attribute );
+ if ( !treeItemAttribute ) return;
+
+ // Clickable download icon -- fetches base data directly from the tree.
+ auto downloadTag = caf::PdmUiTreeViewItemAttribute::createTag();
+ downloadTag->icon = caf::IconProvider( ":/Download.svg" );
+ downloadTag->toolTip = "Fetch polygon data";
+ downloadTag->clicked.connect( this, &RimPolygonCloudAddress::onDownloadTagClicked );
+ treeItemAttribute->tags.push_back( std::move( downloadTag ) );
+
+ // Plain "not fetched" marker, no colored pill.
+ auto textTag = caf::PdmUiTreeViewItemAttribute::createTag();
+ textTag->text = "[Not fetched]";
+ textTag->bgColor = QColor( Qt::white );
+ textTag->fgColor = QColor( Qt::darkGray );
+ treeItemAttribute->tags.push_back( std::move( textTag ) );
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+void RimPolygonCloudAddress::onDownloadTagClicked( const caf::SignalEmitter* emitter, size_t index )
+{
+ ensureBaseFetched();
+
+ updateAllRequiredEditors();
+ objectChanged.send();
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+RiaSumoConnector* RimPolygonCloudAddress::sumoConnector()
+{
+ return RiaApplication::instance()->makeSumoConnector();
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+void RimPolygonCloudAddress::updateName()
+{
+ QString name;
+
+ if ( m_polygonResult() == RiaSumoPolygons::polygonResultKey( SumoPolygonResult::FieldOutline ) )
+ {
+ // Only one, unnamed field outline leaf -- use the category label itself.
+ name = polygonResultLabel( SumoPolygonResult::FieldOutline );
+ }
+ else if ( m_polygonResult() == RiaSumoPolygons::polygonResultKey( SumoPolygonResult::StructureDepthFaultLines ) )
+ {
+ // Owning folder already conveys the category.
+ name = m_name();
+ }
+ else if ( m_polygonResult() == RiaSumoPolygons::polygonResultKey( SumoPolygonResult::FluidContactOutline ) )
+ {
+ // Owning folder chain already conveys category and name -- show contact type only.
+ name = m_contactType();
+ }
+
+ setCollectionName( name );
+}
+
+//--------------------------------------------------------------------------------------------------
+///
+//--------------------------------------------------------------------------------------------------
+QString RimPolygonCloudAddress::polygonResultLabel( SumoPolygonResult polygonResult )
+{
+ switch ( polygonResult )
+ {
+ case SumoPolygonResult::FieldOutline:
+ return "Field Outline";
+ case SumoPolygonResult::StructureDepthFaultLines:
+ return "Structure Depth Fault Lines";
+ case SumoPolygonResult::FluidContactOutline:
+ return "Fluid Contact Outline";
+ }
+
+ return {};
+}
+
+//--------------------------------------------------------------------------------------------------
+/// Fetches from Sumo for the given realization, using this address's own fixed identity.
+//--------------------------------------------------------------------------------------------------
+std::vector RimPolygonCloudAddress::fetchPolygonsFromSumo( int realization )
+{
+ std::vector polygons;
+
+ auto* source = owningSource();
+ if ( !source ) return polygons;
+
+ auto* dataSource = source->dataSource();
+ auto* connector = sumoConnector();
+ if ( !dataSource || !connector ) return polygons;
+
+ auto polygonDataList = connector->polygons().polygonsData( dataSource->caseId(),
+ dataSource->ensembleName(),
+ realization,
+ polygonResult(),
+ m_name(),
+ m_contactType() );
+
+ const bool nameGroupsAreDistinct = polygonDataList.size() > 1;
+
+ for ( const auto& data : polygonDataList )
+ {
+ auto* polygon = new RimCloudPolygon();
+ polygon->disableStorageOfPolygonPoints();
+ polygon->setReadOnly( true );
+ polygon->setDeletable( false );
+ polygon->setSumoIdentity( dataSource->caseId().get(), dataSource->ensembleName(), realization, m_polygonResult(), m_name(), m_contactType() );
+
+ QString polygonName = data.name.isEmpty() ? name() : data.name;
+ if ( nameGroupsAreDistinct ) polygonName = QString( "%1 (ID: %2)" ).arg( polygonName ).arg( data.polyId );
+ polygon->setName( polygonName );
+
+ std::vector points;
+ const size_t pointCount = std::min( { data.xArr.size(), data.yArr.size(), data.zArr.size() } );
+ points.reserve( pointCount );
+ for ( size_t i = 0; i < pointCount; i++ )
+ {
+ // Sumo's depth is positive-down; ResInsight's domain z is elevation (negative-down).
+ points.emplace_back( data.xArr[i], data.yArr[i], -data.zArr[i] );
+ }
+ polygon->setPointsInDomainCoords( points );
+
+ polygons.push_back( polygon );
+ }
+
+ return polygons;
+}
diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudAddress.h b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudAddress.h
new file mode 100644
index 0000000000..953d765958
--- /dev/null
+++ b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudAddress.h
@@ -0,0 +1,114 @@
+/////////////////////////////////////////////////////////////////////////////////
+//
+// 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.
+//
+/////////////////////////////////////////////////////////////////////////////////
+
+#pragma once
+
+#include "Polygons/RimPolygonContainer.h"
+
+#include "Cloud/RiaSumoPolygons.h"
+
+#include "cafPdmField.h"
+#include "cafSignal.h"
+
+#include