From b46fd0d9525e4adca682842804bea86b7db943b8 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:22:13 +0100 Subject: [PATCH] wip --- .../_tmp_well_pick_provider.py | 2 + .../plugins/_map_viewer_fmu/_types.py | 1 + .../plugins/_map_viewer_fmu/_utils.py | 19 ++++++- .../plugins/_map_viewer_fmu/callbacks.py | 49 ++++++++++++++----- .../plugins/_map_viewer_fmu/layout.py | 46 +++++++++++------ .../plugins/_map_viewer_fmu/map_viewer_fmu.py | 32 ++++++++++-- 6 files changed, 116 insertions(+), 33 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..0aec0f337 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 @@ -68,6 +68,8 @@ def get_geojson( properties = { "name": row[WellPickTableColumns.WELL], "attribute": str(row[attribute]), + "line_color": row.get("line_color", [0, 0, 0]), + "text_color": row.get("text_color", [0, 0, 0]), } 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..7e28b1019 100644 --- a/webviz_subsurface/plugins/_map_viewer_fmu/_utils.py +++ b/webviz_subsurface/plugins/_map_viewer_fmu/_utils.py @@ -1,9 +1,11 @@ import base64 import io import math -from typing import List +from typing import List, Dict from PIL import Image, ImageDraw +import geojson +import xtgeo def round_to_significant(val: float, sig: int = 4) -> float: @@ -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..b3e887788 100644 --- a/webviz_subsurface/plugins/_map_viewer_fmu/callbacks.py +++ b/webviz_subsurface/plugins/_map_viewer_fmu/callbacks.py @@ -8,6 +8,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union import numpy as np +import xtgeo import webviz_subsurface_components as wsc from dash import ALL, MATCH, Input, Output, State, callback, callback_context, no_update from dash.exceptions import PreventUpdate @@ -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: List[int], map_surface_names_to_fault_polygons: Dict[str, str], well_picks_provider: Optional[WellPickProvider], fault_polygon_attribute: Optional[str], @@ -518,9 +521,26 @@ def _update_map( layer_data={ "data": well_picks_provider.get_geojson( selected_wells, horizon_name - ) + ), + "getLineColor": "@@=properties.color", + "getTextColor": "@@=properties.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 +570,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 +874,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..6960ec232 100644 --- a/webviz_subsurface/plugins/_map_viewer_fmu/map_viewer_fmu.py +++ b/webviz_subsurface/plugins/_map_viewer_fmu/map_viewer_fmu.py @@ -3,6 +3,7 @@ from typing import Callable, Dict, List, Optional, Tuple, Union from dash import Dash, html +import xtgeo from webviz_config import WebvizPluginABC, WebvizSettings from webviz_subsurface._providers import ( @@ -39,6 +40,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 (rgb). * **`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,6 +72,8 @@ 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`. See [wellpicks.csv](https://github.com/equinor/webviz-subsurface-testdata/tree/master/\ observed_data/drogon_well_picks/wellpicks.csv) for an example.
@@ -91,6 +96,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: List[int] = [200, 200, 200], 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 +162,15 @@ 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 + if field_outline_polygons_file_path is not None: + try: + self.field_outline_polygons = xtgeo.Polygons( + read_csv(field_outline_polygons_file_path) + ) + except: + print("Error reading field outline polygons file") + self.field_outline_color = 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 +190,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 +209,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 +222,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( + (read_csv, [{"csv_file": self.field_outline_polygons_file_path}]) + ) return store_functions