Skip to content

Commit a8fdc13

Browse files
Add field outline and custom well picks colors to MapViewerFMU (#1311)
1 parent f489939 commit a8fdc13

File tree

6 files changed

+123
-35
lines changed

6 files changed

+123
-35
lines changed

webviz_subsurface/plugins/_map_viewer_fmu/_tmp_well_pick_provider.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import geojson
44
import pandas as pd
55

6+
from webviz_subsurface._utils.colors import hex_to_rgb
67
from webviz_subsurface._utils.enum_shim import StrEnum
78

89

@@ -64,10 +65,11 @@ def get_geojson(
6465
point = geojson.Point(coordinates=coords, validate=validate_geometry)
6566

6667
geocoll = geojson.GeometryCollection(geometries=[point])
67-
6868
properties = {
6969
"name": row[WellPickTableColumns.WELL],
7070
"attribute": str(row[attribute]),
71+
"point_color": hex_to_rgb(row.get("point_color", "#000")),
72+
"text_color": hex_to_rgb(row.get("text_color", "#000")),
7173
}
7274

7375
feature = geojson.Feature(

webviz_subsurface/plugins/_map_viewer_fmu/_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class LayerTypes(StrEnum):
99
WELLTOPSLAYER = "GeoJsonLayer"
1010
DRAWING = "DrawingLayer"
1111
FAULTPOLYGONS = "FaultPolygonsLayer"
12+
FIELD_OUTLINE = "GeoJsonLayer"
1213
GEOJSON = "GeoJsonLayer"
1314

1415

webviz_subsurface/plugins/_map_viewer_fmu/_utils.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import base64
22
import io
33
import math
4-
from typing import List
4+
from typing import Dict, List
55

6+
import geojson
7+
import xtgeo
68
from PIL import Image, ImageDraw
79

810

@@ -39,3 +41,18 @@ def create_colormap_image_string(
3941
draw.rectangle([(x_0, 0), (x_1, height)], fill=rgb_to_hex(color))
4042

4143
return f"data:image/png;base64,{image_to_base64(img)}"
44+
45+
46+
def xtgeo_polygons_to_geojson(polygons: xtgeo.Polygons) -> Dict:
47+
feature_arr = []
48+
for name, polygon in polygons.dataframe.groupby("POLY_ID"):
49+
coords = [list(zip(polygon.X_UTME, polygon.Y_UTMN))]
50+
feature = geojson.Feature(
51+
geometry=geojson.Polygon(coords),
52+
properties={
53+
"name": f"id:{name}",
54+
"color": [200, 200, 200],
55+
},
56+
)
57+
feature_arr.append(feature)
58+
return geojson.FeatureCollection(features=feature_arr)

webviz_subsurface/plugins/_map_viewer_fmu/callbacks.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import numpy as np
1111
import webviz_subsurface_components as wsc
12+
import xtgeo
1213
from dash import ALL, MATCH, Input, Output, State, callback, callback_context, no_update
1314
from dash.exceptions import PreventUpdate
1415
from webviz_config import EncodedFile
@@ -32,7 +33,7 @@
3233
from ._layer_model import DeckGLMapLayersModel
3334
from ._tmp_well_pick_provider import WellPickProvider
3435
from ._types import LayerTypes, SurfaceMode
35-
from ._utils import round_to_significant
36+
from ._utils import round_to_significant, xtgeo_polygons_to_geojson
3637
from .layout import (
3738
DefaultSettings,
3839
LayoutElements,
@@ -51,6 +52,8 @@ def plugin_callbacks(
5152
surface_server: Union[SurfaceArrayServer, SurfaceImageServer],
5253
ensemble_fault_polygons_providers: Dict[str, EnsembleFaultPolygonsProvider],
5354
fault_polygons_server: FaultPolygonsServer,
55+
field_outline_polygons: xtgeo.Polygons,
56+
field_outline_color: Tuple[float, float, float],
5457
map_surface_names_to_fault_polygons: Dict[str, str],
5558
well_picks_provider: Optional[WellPickProvider],
5659
fault_polygon_attribute: Optional[str],
@@ -518,9 +521,27 @@ def _update_map(
518521
layer_data={
519522
"data": well_picks_provider.get_geojson(
520523
selected_wells, horizon_name
521-
)
524+
),
525+
"getLineColor": "@@=properties.point_color",
526+
"getFillColor": "@@=properties.point_color",
527+
"getTextColor": "@@=properties.text_color",
528+
},
529+
)
530+
if (
531+
LayoutLabels.SHOW_FIELD_OUTLINE in options
532+
and field_outline_polygons is not None
533+
):
534+
layer_model.update_layer_by_id(
535+
layer_id=f"{LayoutElements.FIELD_OUTLINE_LAYER}-{idx}",
536+
layer_data={
537+
"data": xtgeo_polygons_to_geojson(field_outline_polygons),
538+
"filled": False,
539+
"depthTest": False,
540+
"lineWidthMinPixels": 2,
541+
"getLineColor": field_outline_color,
522542
},
523543
)
544+
524545
viewports = []
525546
view_annotations = []
526547
for idx, data in enumerate(surface_elements):
@@ -550,10 +571,13 @@ def _update_map(
550571
"show3D": False,
551572
"isSync": True,
552573
"layerIds": [
553-
f"{LayoutElements.MAP3D_LAYER}-{idx}"
554-
if isinstance(surface_server, SurfaceArrayServer)
555-
else f"{LayoutElements.COLORMAP_LAYER}-{idx}",
574+
(
575+
f"{LayoutElements.MAP3D_LAYER}-{idx}"
576+
if isinstance(surface_server, SurfaceArrayServer)
577+
else f"{LayoutElements.COLORMAP_LAYER}-{idx}"
578+
),
556579
f"{LayoutElements.FAULTPOLYGONS_LAYER}-{idx}",
580+
f"{LayoutElements.FIELD_OUTLINE_LAYER}-{idx}",
557581
f"{LayoutElements.WELLS_LAYER}-{idx}",
558582
],
559583
"name": make_viewport_label(data, tab_name, multi),
@@ -851,13 +875,15 @@ def _update_color_component_properties(
851875
"colormap": {"value": colormap, "options": colormaps},
852876
"color_range": {
853877
"value": color_range,
854-
"step": calculate_slider_step(
855-
min_value=value_range[0],
856-
max_value=value_range[1],
857-
steps=100,
858-
)
859-
if value_range[0] != value_range[1]
860-
else 0,
878+
"step": (
879+
calculate_slider_step(
880+
min_value=value_range[0],
881+
max_value=value_range[1],
882+
steps=100,
883+
)
884+
if value_range[0] != value_range[1]
885+
else 0
886+
),
861887
"range": value_range,
862888
},
863889
}

webviz_subsurface/plugins/_map_viewer_fmu/layout.py

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,17 @@ class LayoutElements(StrEnum):
3737
RANGE_RESET = "color-range-reset-button"
3838
RESET_BUTTOM_CLICK = "color-range-reset-stored-state"
3939
FAULTPOLYGONS = "fault-polygon-toggle"
40+
FIELD_OUTLINE_TOGGLE = "field-outline-toggle"
4041
WRAPPER = "wrapper-for-selector-component"
4142
COLORWRAPPER = "wrapper-for-color-selector-component"
4243
OPTIONS = "options"
4344

4445
COLORMAP_LAYER = "deckglcolormaplayer"
45-
HILLSHADING_LAYER = "deckglhillshadinglayer"
46+
4647
WELLS_LAYER = "deckglwelllayer"
4748
MAP3D_LAYER = "deckglmap3dlayer"
4849
FAULTPOLYGONS_LAYER = "deckglfaultpolygonslayer"
50+
FIELD_OUTLINE_LAYER = "deckglfieldoutlinelayer"
4951
REALIZATIONS_FILTER = "realization-filter-selector"
5052
OPTIONS_DIALOG = "options-dialog"
5153

@@ -69,8 +71,8 @@ class LayoutLabels(StrEnum):
6971
LINK = "🔗 Link"
7072
FAULTPOLYGONS = "Fault polygons"
7173
SHOW_FAULTPOLYGONS = "Show fault polygons"
74+
SHOW_FIELD_OUTLINE = "Show field outline"
7275
SHOW_WELLS = "Show wells"
73-
SHOW_HILLSHADING = "Hillshading"
7476
COMMON_SELECTIONS = "Options and global filters"
7577
REAL_FILTER = "Realization filter"
7678
WELL_FILTER = "Well filter"
@@ -183,7 +185,7 @@ def main_layout(
183185
realizations: List[int],
184186
color_tables: List[Dict],
185187
show_fault_polygons: bool = True,
186-
hillshading_enabled: bool = True,
188+
show_field_outline: bool = False,
187189
render_surfaces_as_images: bool = True,
188190
) -> html.Div:
189191
return html.Div(
@@ -240,9 +242,9 @@ def main_layout(
240242
DialogLayout(
241243
get_uuid,
242244
show_fault_polygons,
245+
show_field_outline,
243246
well_names,
244247
realizations,
245-
hillshading_enabled,
246248
),
247249
]
248250
)
@@ -304,18 +306,20 @@ def __init__(
304306
self,
305307
get_uuid: Callable,
306308
show_fault_polygons: bool,
309+
show_field_outline: bool,
307310
well_names: List[str],
308311
realizations: List[int],
309-
hillshading_enabled: bool = True,
310312
) -> None:
311-
checklist_options = [LayoutLabels.SHOW_HILLSHADING]
312-
checklist_values = (
313-
[LayoutLabels.SHOW_HILLSHADING] if hillshading_enabled else []
314-
)
313+
checklist_options = []
314+
checklist_values = []
315315
if show_fault_polygons:
316316
checklist_options.append(LayoutLabels.SHOW_FAULTPOLYGONS)
317317
checklist_values.append(LayoutLabels.SHOW_FAULTPOLYGONS)
318318

319+
if show_field_outline:
320+
checklist_options.append(LayoutLabels.SHOW_FIELD_OUTLINE)
321+
checklist_values.append(LayoutLabels.SHOW_FIELD_OUTLINE)
322+
319323
if well_names:
320324
checklist_options.append(LayoutLabels.SHOW_WELLS)
321325
checklist_values.append(LayoutLabels.SHOW_FAULTPOLYGONS)
@@ -358,9 +362,11 @@ def __init__(self, tab: Tabs, get_uuid: Callable, selector: str) -> None:
358362
clicked = selector in DefaultSettings.LINKED_SELECTORS.get(tab, [])
359363
super().__init__(
360364
id={
361-
"id": get_uuid(LayoutElements.LINK)
362-
if selector not in ["color_range", "colormap"]
363-
else get_uuid(LayoutElements.COLORLINK),
365+
"id": (
366+
get_uuid(LayoutElements.LINK)
367+
if selector not in ["color_range", "colormap"]
368+
else get_uuid(LayoutElements.COLORLINK)
369+
),
364370
"tab": tab,
365371
"selector": selector,
366372
},
@@ -570,9 +576,11 @@ def __init__(
570576
) -> None:
571577
super().__init__(
572578
style={
573-
"display": "none"
574-
if tab == Tabs.STATS and selector == MapSelector.MODE
575-
else "block"
579+
"display": (
580+
"none"
581+
if tab == Tabs.STATS and selector == MapSelector.MODE
582+
else "block"
583+
)
576584
},
577585
children=wcc.Selectors(
578586
label=label,
@@ -805,7 +813,13 @@ def update_map_layers(
805813
"parameters": {"depthTest": False},
806814
}
807815
)
808-
816+
layers.append(
817+
{
818+
"@@type": LayerTypes.FIELD_OUTLINE,
819+
"id": f"{LayoutElements.FIELD_OUTLINE_LAYER}-{idx}",
820+
"data": {"type": "FeatureCollection", "features": []},
821+
}
822+
)
809823
if include_well_layer:
810824
layers.append(
811825
{

webviz_subsurface/plugins/_map_viewer_fmu/map_viewer_fmu.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pathlib import Path
33
from typing import Callable, Dict, List, Optional, Tuple, Union
44

5+
import xtgeo
56
from dash import Dash, html
67
from webviz_config import WebvizPluginABC, WebvizSettings
78

@@ -18,7 +19,8 @@
1819
from webviz_subsurface._providers.ensemble_surface_provider.surface_image_server import (
1920
SurfaceImageServer,
2021
)
21-
from webviz_subsurface._utils.webvizstore_functions import read_csv
22+
from webviz_subsurface._utils.colors import hex_to_rgb
23+
from webviz_subsurface._utils.webvizstore_functions import get_path, read_csv
2224

2325
from ._tmp_well_pick_provider import WellPickProvider
2426
from .callbacks import plugin_callbacks
@@ -39,6 +41,8 @@ class MapViewerFMU(WebvizPluginABC):
3941
Default value is 'share/results/maps'.
4042
* **`well_pick_file`:** A csv file with well picks. See data input.
4143
* **`fault_polygon_attribute`:** Which set of fault polygons to use.
44+
* **`field_outline_polygons_file_path`:** Full filepath to a field outline polygons file.
45+
* **`field_outline_color:** Color of the field outline polygons (hex).
4246
* **`map_surface_names_to_well_pick_names`:** Allows mapping of file surface names
4347
to the relevant well pick name
4448
* **`map_surface_names_to_fault_polygons`:** Allows mapping of file surface names
@@ -69,7 +73,11 @@ class MapViewerFMU(WebvizPluginABC):
6973
01_drogon_ahm/realization-0/iter-0/share/results/polygons/\
7074
toptherys--gl_faultlines_extract_postprocess.pol) for an example.
7175
76+
Field outline polygons have the same format as fault polygons.
77+
7278
Well picks are provided as a csv file with columns `X_UTME,Y_UTMN,Z_TVDSS,MD,WELL,HORIZON`.
79+
Additionally the columns `point_color` and `text_color` can be used to specify the color of the
80+
point and text respectively. Use hex color codes for this (e.g. #ffffff).<br>
7381
See [wellpicks.csv](https://github.com/equinor/webviz-subsurface-testdata/tree/master/\
7482
observed_data/drogon_well_picks/wellpicks.csv) for an example.<br>
7583
Well picks can be exported from RMS using this script: [extract_well_picks_from_rms.py]\
@@ -91,6 +99,8 @@ def __init__(
9199
attributes: list = None,
92100
well_pick_file: Path = None,
93101
fault_polygon_attribute: Optional[str] = None,
102+
field_outline_polygons_file_path: Path = None,
103+
field_outline_color: str = "#e51000",
94104
map_surface_names_to_fault_polygons: Dict[str, str] = None,
95105
map_surface_names_to_well_pick_names: Dict[str, str] = None,
96106
rel_surface_folder: str = "share/results/maps",
@@ -155,7 +165,16 @@ def __init__(
155165
self._fault_polygons_server = FaultPolygonsServer.instance(app)
156166
for fault_polygons_provider in self._ensemble_fault_polygons_providers.values():
157167
self._fault_polygons_server.add_provider(fault_polygons_provider)
158-
168+
self.field_outline_polygons = None
169+
self.field_outline_polygons_file_path = field_outline_polygons_file_path
170+
if self.field_outline_polygons_file_path is not None:
171+
try:
172+
self.field_outline_polygons = xtgeo.polygons_from_file(
173+
get_path(self.field_outline_polygons_file_path)
174+
)
175+
except ValueError:
176+
print("Error reading field outline polygons file")
177+
self.field_outline_color = hex_to_rgb(field_outline_color)
159178
self.map_surface_names_to_fault_polygons = (
160179
map_surface_names_to_fault_polygons
161180
if map_surface_names_to_fault_polygons is not None
@@ -175,10 +194,13 @@ def layout(self) -> html.Div:
175194
reals.extend([x for x in provider.realizations() if x not in reals])
176195
return main_layout(
177196
get_uuid=self.uuid,
178-
well_names=self.well_pick_provider.well_names()
179-
if self.well_pick_provider is not None
180-
else [],
197+
well_names=(
198+
self.well_pick_provider.well_names()
199+
if self.well_pick_provider is not None
200+
else []
201+
),
181202
realizations=reals,
203+
show_field_outline=self.field_outline_polygons is not None,
182204
color_tables=self.color_tables,
183205
render_surfaces_as_images=self.render_surfaces_as_images,
184206
)
@@ -191,6 +213,8 @@ def set_callbacks(self) -> None:
191213
ensemble_fault_polygons_providers=self._ensemble_fault_polygons_providers,
192214
fault_polygon_attribute=self.fault_polygon_attribute,
193215
fault_polygons_server=self._fault_polygons_server,
216+
field_outline_polygons=self.field_outline_polygons,
217+
field_outline_color=self.field_outline_color,
194218
map_surface_names_to_fault_polygons=self.map_surface_names_to_fault_polygons,
195219
well_picks_provider=self.well_pick_provider,
196220
color_tables=self.color_tables,
@@ -202,4 +226,8 @@ def add_webvizstore(self) -> List[Tuple[Callable, list]]:
202226
store_functions = []
203227
if self.well_pick_file is not None:
204228
store_functions.append((read_csv, [{"csv_file": self.well_pick_file}]))
229+
if self.field_outline_polygons_file_path is not None:
230+
store_functions.append(
231+
(get_path, [{"path": self.field_outline_polygons_file_path}])
232+
)
205233
return store_functions

0 commit comments

Comments
 (0)