From a8fdc138f9d4c6802be584fb91805c767609e54c Mon Sep 17 00:00:00 2001
From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com>
Date: Fri, 20 Dec 2024 09:22:38 +0100
Subject: [PATCH] Add field outline and custom well picks colors to
MapViewerFMU (#1311)
---
.../_tmp_well_pick_provider.py | 4 +-
.../plugins/_map_viewer_fmu/_types.py | 1 +
.../plugins/_map_viewer_fmu/_utils.py | 19 ++++++-
.../plugins/_map_viewer_fmu/callbacks.py | 50 ++++++++++++++-----
.../plugins/_map_viewer_fmu/layout.py | 46 +++++++++++------
.../plugins/_map_viewer_fmu/map_viewer_fmu.py | 38 ++++++++++++--
6 files changed, 123 insertions(+), 35 deletions(-)
diff --git a/webviz_subsurface/plugins/_map_viewer_fmu/_tmp_well_pick_provider.py b/webviz_subsurface/plugins/_map_viewer_fmu/_tmp_well_pick_provider.py
index 0ed96028b..6b5aa9e77 100644
--- a/webviz_subsurface/plugins/_map_viewer_fmu/_tmp_well_pick_provider.py
+++ b/webviz_subsurface/plugins/_map_viewer_fmu/_tmp_well_pick_provider.py
@@ -3,6 +3,7 @@
import geojson
import pandas as pd
+from webviz_subsurface._utils.colors import hex_to_rgb
from webviz_subsurface._utils.enum_shim import StrEnum
@@ -64,10 +65,11 @@ def get_geojson(
point = geojson.Point(coordinates=coords, validate=validate_geometry)
geocoll = geojson.GeometryCollection(geometries=[point])
-
properties = {
"name": row[WellPickTableColumns.WELL],
"attribute": str(row[attribute]),
+ "point_color": hex_to_rgb(row.get("point_color", "#000")),
+ "text_color": hex_to_rgb(row.get("text_color", "#000")),
}
feature = geojson.Feature(
diff --git a/webviz_subsurface/plugins/_map_viewer_fmu/_types.py b/webviz_subsurface/plugins/_map_viewer_fmu/_types.py
index 7f786e743..3535d47f9 100644
--- a/webviz_subsurface/plugins/_map_viewer_fmu/_types.py
+++ b/webviz_subsurface/plugins/_map_viewer_fmu/_types.py
@@ -9,6 +9,7 @@ class LayerTypes(StrEnum):
WELLTOPSLAYER = "GeoJsonLayer"
DRAWING = "DrawingLayer"
FAULTPOLYGONS = "FaultPolygonsLayer"
+ FIELD_OUTLINE = "GeoJsonLayer"
GEOJSON = "GeoJsonLayer"
diff --git a/webviz_subsurface/plugins/_map_viewer_fmu/_utils.py b/webviz_subsurface/plugins/_map_viewer_fmu/_utils.py
index 6e5d29d0a..6fe552194 100644
--- a/webviz_subsurface/plugins/_map_viewer_fmu/_utils.py
+++ b/webviz_subsurface/plugins/_map_viewer_fmu/_utils.py
@@ -1,8 +1,10 @@
import base64
import io
import math
-from typing import List
+from typing import Dict, List
+import geojson
+import xtgeo
from PIL import Image, ImageDraw
@@ -39,3 +41,18 @@ def create_colormap_image_string(
draw.rectangle([(x_0, 0), (x_1, height)], fill=rgb_to_hex(color))
return f"data:image/png;base64,{image_to_base64(img)}"
+
+
+def xtgeo_polygons_to_geojson(polygons: xtgeo.Polygons) -> Dict:
+ feature_arr = []
+ for name, polygon in polygons.dataframe.groupby("POLY_ID"):
+ coords = [list(zip(polygon.X_UTME, polygon.Y_UTMN))]
+ feature = geojson.Feature(
+ geometry=geojson.Polygon(coords),
+ properties={
+ "name": f"id:{name}",
+ "color": [200, 200, 200],
+ },
+ )
+ feature_arr.append(feature)
+ return geojson.FeatureCollection(features=feature_arr)
diff --git a/webviz_subsurface/plugins/_map_viewer_fmu/callbacks.py b/webviz_subsurface/plugins/_map_viewer_fmu/callbacks.py
index 85f08e7a2..8f8e61d0d 100644
--- a/webviz_subsurface/plugins/_map_viewer_fmu/callbacks.py
+++ b/webviz_subsurface/plugins/_map_viewer_fmu/callbacks.py
@@ -9,6 +9,7 @@
import numpy as np
import webviz_subsurface_components as wsc
+import xtgeo
from dash import ALL, MATCH, Input, Output, State, callback, callback_context, no_update
from dash.exceptions import PreventUpdate
from webviz_config import EncodedFile
@@ -32,7 +33,7 @@
from ._layer_model import DeckGLMapLayersModel
from ._tmp_well_pick_provider import WellPickProvider
from ._types import LayerTypes, SurfaceMode
-from ._utils import round_to_significant
+from ._utils import round_to_significant, xtgeo_polygons_to_geojson
from .layout import (
DefaultSettings,
LayoutElements,
@@ -51,6 +52,8 @@ def plugin_callbacks(
surface_server: Union[SurfaceArrayServer, SurfaceImageServer],
ensemble_fault_polygons_providers: Dict[str, EnsembleFaultPolygonsProvider],
fault_polygons_server: FaultPolygonsServer,
+ field_outline_polygons: xtgeo.Polygons,
+ field_outline_color: Tuple[float, float, float],
map_surface_names_to_fault_polygons: Dict[str, str],
well_picks_provider: Optional[WellPickProvider],
fault_polygon_attribute: Optional[str],
@@ -518,9 +521,27 @@ def _update_map(
layer_data={
"data": well_picks_provider.get_geojson(
selected_wells, horizon_name
- )
+ ),
+ "getLineColor": "@@=properties.point_color",
+ "getFillColor": "@@=properties.point_color",
+ "getTextColor": "@@=properties.text_color",
+ },
+ )
+ if (
+ LayoutLabels.SHOW_FIELD_OUTLINE in options
+ and field_outline_polygons is not None
+ ):
+ layer_model.update_layer_by_id(
+ layer_id=f"{LayoutElements.FIELD_OUTLINE_LAYER}-{idx}",
+ layer_data={
+ "data": xtgeo_polygons_to_geojson(field_outline_polygons),
+ "filled": False,
+ "depthTest": False,
+ "lineWidthMinPixels": 2,
+ "getLineColor": field_outline_color,
},
)
+
viewports = []
view_annotations = []
for idx, data in enumerate(surface_elements):
@@ -550,10 +571,13 @@ def _update_map(
"show3D": False,
"isSync": True,
"layerIds": [
- f"{LayoutElements.MAP3D_LAYER}-{idx}"
- if isinstance(surface_server, SurfaceArrayServer)
- else f"{LayoutElements.COLORMAP_LAYER}-{idx}",
+ (
+ f"{LayoutElements.MAP3D_LAYER}-{idx}"
+ if isinstance(surface_server, SurfaceArrayServer)
+ else f"{LayoutElements.COLORMAP_LAYER}-{idx}"
+ ),
f"{LayoutElements.FAULTPOLYGONS_LAYER}-{idx}",
+ f"{LayoutElements.FIELD_OUTLINE_LAYER}-{idx}",
f"{LayoutElements.WELLS_LAYER}-{idx}",
],
"name": make_viewport_label(data, tab_name, multi),
@@ -851,13 +875,15 @@ def _update_color_component_properties(
"colormap": {"value": colormap, "options": colormaps},
"color_range": {
"value": color_range,
- "step": calculate_slider_step(
- min_value=value_range[0],
- max_value=value_range[1],
- steps=100,
- )
- if value_range[0] != value_range[1]
- else 0,
+ "step": (
+ calculate_slider_step(
+ min_value=value_range[0],
+ max_value=value_range[1],
+ steps=100,
+ )
+ if value_range[0] != value_range[1]
+ else 0
+ ),
"range": value_range,
},
}
diff --git a/webviz_subsurface/plugins/_map_viewer_fmu/layout.py b/webviz_subsurface/plugins/_map_viewer_fmu/layout.py
index 492b55499..1203324bf 100644
--- a/webviz_subsurface/plugins/_map_viewer_fmu/layout.py
+++ b/webviz_subsurface/plugins/_map_viewer_fmu/layout.py
@@ -37,15 +37,17 @@ class LayoutElements(StrEnum):
RANGE_RESET = "color-range-reset-button"
RESET_BUTTOM_CLICK = "color-range-reset-stored-state"
FAULTPOLYGONS = "fault-polygon-toggle"
+ FIELD_OUTLINE_TOGGLE = "field-outline-toggle"
WRAPPER = "wrapper-for-selector-component"
COLORWRAPPER = "wrapper-for-color-selector-component"
OPTIONS = "options"
COLORMAP_LAYER = "deckglcolormaplayer"
- HILLSHADING_LAYER = "deckglhillshadinglayer"
+
WELLS_LAYER = "deckglwelllayer"
MAP3D_LAYER = "deckglmap3dlayer"
FAULTPOLYGONS_LAYER = "deckglfaultpolygonslayer"
+ FIELD_OUTLINE_LAYER = "deckglfieldoutlinelayer"
REALIZATIONS_FILTER = "realization-filter-selector"
OPTIONS_DIALOG = "options-dialog"
@@ -69,8 +71,8 @@ class LayoutLabels(StrEnum):
LINK = "🔗 Link"
FAULTPOLYGONS = "Fault polygons"
SHOW_FAULTPOLYGONS = "Show fault polygons"
+ SHOW_FIELD_OUTLINE = "Show field outline"
SHOW_WELLS = "Show wells"
- SHOW_HILLSHADING = "Hillshading"
COMMON_SELECTIONS = "Options and global filters"
REAL_FILTER = "Realization filter"
WELL_FILTER = "Well filter"
@@ -183,7 +185,7 @@ def main_layout(
realizations: List[int],
color_tables: List[Dict],
show_fault_polygons: bool = True,
- hillshading_enabled: bool = True,
+ show_field_outline: bool = False,
render_surfaces_as_images: bool = True,
) -> html.Div:
return html.Div(
@@ -240,9 +242,9 @@ def main_layout(
DialogLayout(
get_uuid,
show_fault_polygons,
+ show_field_outline,
well_names,
realizations,
- hillshading_enabled,
),
]
)
@@ -304,18 +306,20 @@ def __init__(
self,
get_uuid: Callable,
show_fault_polygons: bool,
+ show_field_outline: bool,
well_names: List[str],
realizations: List[int],
- hillshading_enabled: bool = True,
) -> None:
- checklist_options = [LayoutLabels.SHOW_HILLSHADING]
- checklist_values = (
- [LayoutLabels.SHOW_HILLSHADING] if hillshading_enabled else []
- )
+ checklist_options = []
+ checklist_values = []
if show_fault_polygons:
checklist_options.append(LayoutLabels.SHOW_FAULTPOLYGONS)
checklist_values.append(LayoutLabels.SHOW_FAULTPOLYGONS)
+ if show_field_outline:
+ checklist_options.append(LayoutLabels.SHOW_FIELD_OUTLINE)
+ checklist_values.append(LayoutLabels.SHOW_FIELD_OUTLINE)
+
if well_names:
checklist_options.append(LayoutLabels.SHOW_WELLS)
checklist_values.append(LayoutLabels.SHOW_FAULTPOLYGONS)
@@ -358,9 +362,11 @@ def __init__(self, tab: Tabs, get_uuid: Callable, selector: str) -> None:
clicked = selector in DefaultSettings.LINKED_SELECTORS.get(tab, [])
super().__init__(
id={
- "id": get_uuid(LayoutElements.LINK)
- if selector not in ["color_range", "colormap"]
- else get_uuid(LayoutElements.COLORLINK),
+ "id": (
+ get_uuid(LayoutElements.LINK)
+ if selector not in ["color_range", "colormap"]
+ else get_uuid(LayoutElements.COLORLINK)
+ ),
"tab": tab,
"selector": selector,
},
@@ -570,9 +576,11 @@ def __init__(
) -> None:
super().__init__(
style={
- "display": "none"
- if tab == Tabs.STATS and selector == MapSelector.MODE
- else "block"
+ "display": (
+ "none"
+ if tab == Tabs.STATS and selector == MapSelector.MODE
+ else "block"
+ )
},
children=wcc.Selectors(
label=label,
@@ -805,7 +813,13 @@ def update_map_layers(
"parameters": {"depthTest": False},
}
)
-
+ layers.append(
+ {
+ "@@type": LayerTypes.FIELD_OUTLINE,
+ "id": f"{LayoutElements.FIELD_OUTLINE_LAYER}-{idx}",
+ "data": {"type": "FeatureCollection", "features": []},
+ }
+ )
if include_well_layer:
layers.append(
{
diff --git a/webviz_subsurface/plugins/_map_viewer_fmu/map_viewer_fmu.py b/webviz_subsurface/plugins/_map_viewer_fmu/map_viewer_fmu.py
index 21b386343..5116f929b 100644
--- a/webviz_subsurface/plugins/_map_viewer_fmu/map_viewer_fmu.py
+++ b/webviz_subsurface/plugins/_map_viewer_fmu/map_viewer_fmu.py
@@ -2,6 +2,7 @@
from pathlib import Path
from typing import Callable, Dict, List, Optional, Tuple, Union
+import xtgeo
from dash import Dash, html
from webviz_config import WebvizPluginABC, WebvizSettings
@@ -18,7 +19,8 @@
from webviz_subsurface._providers.ensemble_surface_provider.surface_image_server import (
SurfaceImageServer,
)
-from webviz_subsurface._utils.webvizstore_functions import read_csv
+from webviz_subsurface._utils.colors import hex_to_rgb
+from webviz_subsurface._utils.webvizstore_functions import get_path, read_csv
from ._tmp_well_pick_provider import WellPickProvider
from .callbacks import plugin_callbacks
@@ -39,6 +41,8 @@ class MapViewerFMU(WebvizPluginABC):
Default value is 'share/results/maps'.
* **`well_pick_file`:** A csv file with well picks. See data input.
* **`fault_polygon_attribute`:** Which set of fault polygons to use.
+* **`field_outline_polygons_file_path`:** Full filepath to a field outline polygons file.
+* **`field_outline_color:** Color of the field outline polygons (hex).
* **`map_surface_names_to_well_pick_names`:** Allows mapping of file surface names
to the relevant well pick name
* **`map_surface_names_to_fault_polygons`:** Allows mapping of file surface names
@@ -69,7 +73,11 @@ class MapViewerFMU(WebvizPluginABC):
01_drogon_ahm/realization-0/iter-0/share/results/polygons/\
toptherys--gl_faultlines_extract_postprocess.pol) for an example.
+Field outline polygons have the same format as fault polygons.
+
Well picks are provided as a csv file with columns `X_UTME,Y_UTMN,Z_TVDSS,MD,WELL,HORIZON`.
+Additionally the columns `point_color` and `text_color` can be used to specify the color of the
+point and text respectively. Use hex color codes for this (e.g. #ffffff).
See [wellpicks.csv](https://github.com/equinor/webviz-subsurface-testdata/tree/master/\
observed_data/drogon_well_picks/wellpicks.csv) for an example.
Well picks can be exported from RMS using this script: [extract_well_picks_from_rms.py]\
@@ -91,6 +99,8 @@ def __init__(
attributes: list = None,
well_pick_file: Path = None,
fault_polygon_attribute: Optional[str] = None,
+ field_outline_polygons_file_path: Path = None,
+ field_outline_color: str = "#e51000",
map_surface_names_to_fault_polygons: Dict[str, str] = None,
map_surface_names_to_well_pick_names: Dict[str, str] = None,
rel_surface_folder: str = "share/results/maps",
@@ -155,7 +165,16 @@ def __init__(
self._fault_polygons_server = FaultPolygonsServer.instance(app)
for fault_polygons_provider in self._ensemble_fault_polygons_providers.values():
self._fault_polygons_server.add_provider(fault_polygons_provider)
-
+ self.field_outline_polygons = None
+ self.field_outline_polygons_file_path = field_outline_polygons_file_path
+ if self.field_outline_polygons_file_path is not None:
+ try:
+ self.field_outline_polygons = xtgeo.polygons_from_file(
+ get_path(self.field_outline_polygons_file_path)
+ )
+ except ValueError:
+ print("Error reading field outline polygons file")
+ self.field_outline_color = hex_to_rgb(field_outline_color)
self.map_surface_names_to_fault_polygons = (
map_surface_names_to_fault_polygons
if map_surface_names_to_fault_polygons is not None
@@ -175,10 +194,13 @@ def layout(self) -> html.Div:
reals.extend([x for x in provider.realizations() if x not in reals])
return main_layout(
get_uuid=self.uuid,
- well_names=self.well_pick_provider.well_names()
- if self.well_pick_provider is not None
- else [],
+ well_names=(
+ self.well_pick_provider.well_names()
+ if self.well_pick_provider is not None
+ else []
+ ),
realizations=reals,
+ show_field_outline=self.field_outline_polygons is not None,
color_tables=self.color_tables,
render_surfaces_as_images=self.render_surfaces_as_images,
)
@@ -191,6 +213,8 @@ def set_callbacks(self) -> None:
ensemble_fault_polygons_providers=self._ensemble_fault_polygons_providers,
fault_polygon_attribute=self.fault_polygon_attribute,
fault_polygons_server=self._fault_polygons_server,
+ field_outline_polygons=self.field_outline_polygons,
+ field_outline_color=self.field_outline_color,
map_surface_names_to_fault_polygons=self.map_surface_names_to_fault_polygons,
well_picks_provider=self.well_pick_provider,
color_tables=self.color_tables,
@@ -202,4 +226,8 @@ def add_webvizstore(self) -> List[Tuple[Callable, list]]:
store_functions = []
if self.well_pick_file is not None:
store_functions.append((read_csv, [{"csv_file": self.well_pick_file}]))
+ if self.field_outline_polygons_file_path is not None:
+ store_functions.append(
+ (get_path, [{"path": self.field_outline_polygons_file_path}])
+ )
return store_functions