From 41fd33677ec54e0e8101ab50a2d17649bf328942 Mon Sep 17 00:00:00 2001 From: "Kari B. Skjerve" Date: Tue, 9 May 2023 13:52:41 +0200 Subject: [PATCH] feat: lithology track with pattern --- examples/well_correlation_data/__init__.py | 0 .../well_correlation_panel_testapp.py | 80 ++ .../well_log_testdata.py | 891 ++++++++++++++++++ .../example-data/discrete-facies-test.json | 79 ++ .../synclog_template_lithologytrack.json | 186 ++++ .../WellLogViewer/SyncLogViewer.stories.jsx | 77 ++ .../WellLogViewer/SyncLogViewer.tsx | 16 +- .../components/LithologyTrack.tsx | 254 +++++ .../components/WellLogTemplateTypes.ts | 3 +- .../WellLogViewer/components/WellLogView.tsx | 22 +- .../components/WellLogViewer/utils/tracks.ts | 160 +++- .../src/lib/inputSchema/WellLogTemplate.json | 3 +- 12 files changed, 1733 insertions(+), 38 deletions(-) create mode 100644 examples/well_correlation_data/__init__.py create mode 100644 examples/well_correlation_data/well_correlation_panel_testapp.py create mode 100644 examples/well_correlation_data/well_log_testdata.py create mode 100644 react/src/demo/example-data/discrete-facies-test.json create mode 100644 react/src/demo/example-data/synclog_template_lithologytrack.json create mode 100644 react/src/lib/components/WellLogViewer/components/LithologyTrack.tsx diff --git a/examples/well_correlation_data/__init__.py b/examples/well_correlation_data/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/well_correlation_data/well_correlation_panel_testapp.py b/examples/well_correlation_data/well_correlation_panel_testapp.py new file mode 100644 index 000000000..38cc8baa1 --- /dev/null +++ b/examples/well_correlation_data/well_correlation_panel_testapp.py @@ -0,0 +1,80 @@ +from pathlib import Path + +import dash +import flask + +from examples.well_correlation_data.well_log_testdata import ( + axis_mnemos, + axisTitles, + color_tables, + lithology_info_table, + patterns, + patternsTable, + spacers, + templates, + wellDistances, + welllogs_two_wells, + wellpickFlatting, + wellpicks, +) +from webviz_subsurface_components import SyncLogViewer + +slv = SyncLogViewer( + id="WellCorrelation-viewer", + welllogs=welllogs_two_wells, + wellpicks=wellpicks, + patterns=patterns, + spacers=spacers, + wellDistances=wellDistances, + templates=templates, + wellpickFlatting=wellpickFlatting, + colorTables=color_tables, + patternsTable=patternsTable, # {'patternSize': 24, 'patterns': [], 'names': []}, + axisTitles=axisTitles, + axisMnemos=axis_mnemos, + syncContentDomain=False, + syncContentSelection=True, + syncTrackPos=True, + syncTemplate=True, + horizontal=False, + viewTitles=True, + welllogOptions={"wellpickColorFill": False, "wellpickPatternFill": False}, + lithologyInfoTable=lithology_info_table, + # spacerOptions={ + # "wellpickColorFill": True, + # "wellpickPatternFill": True + # } +) + +static_image_route = "/static/" + +app = dash.Dash(__name__) +app.layout = dash.html.Div( + id="app-id", + style={ + "height": "100%", + "width": "80%", + "position": "absolute", + }, + children=[ + slv, + ], +) + + +@app.server.route("/static/.gif") +def serve_image(image_path): + image_name = "{}.gif".format(image_path) + # Images are located in react/src/demo/example-data/pattern + return flask.send_from_directory( + Path(__file__).parent.parent.parent / "react/src/demo/example-data/patterns", + image_name, + ) + + +if __name__ == "__main__": + app.run_server( + host="localhost", + port=8000, + debug=True, + ) diff --git a/examples/well_correlation_data/well_log_testdata.py b/examples/well_correlation_data/well_log_testdata.py new file mode 100644 index 000000000..cc7d9d625 --- /dev/null +++ b/examples/well_correlation_data/well_log_testdata.py @@ -0,0 +1,891 @@ +from enum import Enum + +import numpy as np + + +class Curves(str, Enum): + vsh = "VSH" + swt = "SWT" + pres_form = "PRES_FORM" + net_flag = "NET_FLAG" + gr = "GR" + rhob = "RHOB" + nphi = "NPHI" + phit = "PHIT" + klogh = "KLOGH" + core_plug_poro = "Core plug PORO" + core_plug_permx = "Core plug PERMX" + core_plug_permz = "Core plug PERMZ" + hkl = "HKL" + md = "MD" + tvd = "TVD" + lithofacies = "Lithofacies" + + +axis_mnemos = { + "md": ["DEPTH", "DEPT", "MD", "TDEP", "MD_RKB"], + "tvd": ["TVD", "TVDSS", "DVER", "TVD_MSL"], + "tstd": ["TST", "TSTD"], +} + +templates = [ + { + "name": "Template 1", + "scale": {"primary": "md", "allowSecondary": True}, + "tracks": [ + { + "plots": [ + { + "name": Curves.lithofacies.value, + "style": Curves.lithofacies.value, + } + ] + }, + { + "plots": [ + { + "name": Curves.lithofacies.value, + "style": f"{Curves.lithofacies.value}2", + } + ] + }, + { + "plots": [ + { + "name": Curves.vsh.value, + "color": "green", + "type": "area", + "fill": "green", + "domain": [0, 1], + } + ] + }, + { + "plots": [ + { + "name": Curves.swt.value, + "style": Curves.swt.value, + "domain": [1.0, 0.0], + } + ] + }, + { + "plots": [ + { + "name": Curves.pres_form.value, + "style": Curves.pres_form.value, + "domain": [200, 500], + } + ] + }, + { + "plots": [ + { + "name": Curves.net_flag.value, + "style": Curves.net_flag.value, + "domain": [0, 20], + } + ] + }, + { + "plots": [ + { + "name": Curves.gr.value, + "style": Curves.gr.value, + "domain": [0, 150], + } + ] + }, + { + "plots": [ + { + "name": Curves.rhob.value, + "style": Curves.rhob.value, + "domain": [0.5, 2.95], + } + ] + }, + { + "plots": [ + { + "name": Curves.nphi.value, + "style": Curves.nphi.value, + "domain": [0.85, -0.15], + } + ] + }, + { + "title": "RHOB vs NPHI", + "plots": [ + { + "name": Curves.rhob.value, + "name2": Curves.nphi.value, + "type": "differential", + "scale": "linear", + "color": "red", + "color2": "blue", + "fill": "grey", + "fill2": "yellow", + }, + ], + "domain": [ + -5.0, + 5.0, + ], # TODO: Looks like domain is not working with "differential" plot. See wsc issue #1453 + }, + { + "plots": [ + {"name": Curves.phit.value, "color": "#0000FF"}, + { + "name": Curves.core_plug_poro.value, + "style": Curves.core_plug_poro.value, + }, + ] + }, + { + "plots": [ + {"name": Curves.klogh.value, "style": Curves.klogh.value}, + { + "name": Curves.core_plug_permx.value, + "style": Curves.core_plug_permx.value, + }, + { + "name": Curves.core_plug_permz.value, + "style": Curves.core_plug_permz.value, + }, + ] + }, + ], + "styles": [ + { + "name": Curves.hkl.value, + "type": "gradientfill", + "colorTable": "Physics", + "color": "green", + }, + { + "name": Curves.md.value, + "scale": "linear", + "type": "area", + "color": "blue", + "fill": "green", + }, + {"name": Curves.swt.value, "scale": "linear", "color": "blue"}, + { + "name": Curves.net_flag.value, + "type": "area", + "color": "#B3B300", + "fill": "#B3B300", + }, + { + "name": Curves.klogh.value, + "color": "#00008B", + "scale": "log", + }, + {"name": Curves.pres_form.value, "color": "black", "type": "dot"}, + { + "name": Curves.core_plug_poro.value, + "scale": "linear", + "color": "black", + "type": "dot", + }, + { + "name": Curves.core_plug_permx.value, + "scale": "log", + "color": "black", + "type": "dot", + }, + { + "name": Curves.core_plug_permz.value, + "scale": "log", + "color": "red", + "type": "dot", + }, + { + "name": Curves.nphi.value, + "color": "blue", + }, + { + "name": Curves.rhob.value, + "color": "red", + }, + { + "name": Curves.gr.value, + "type": "gradientfill", + "colorTable": "YellowDarkGreen", + "color": "#006400", + }, + { + "name": Curves.lithofacies.value, + "type": "stacked", + "colorTable": "Stratigraphy", + }, + { + "name": f"{Curves.lithofacies.value}2", + "type": "canvas", + "colorTable": "Stratigraphy", + }, + ], + }, +] + +wellpick_name = "HORIZON" + +stratigraphy_color_table = { + "name": "Stratigraphy", + "discrete": True, + "colorNaN": [255, 64, 64], + "colors": [ + [0, 255, 120, 61], + [1, 255, 193, 0], + [2, 255, 155, 76], + [3, 255, 223, 161], + [4, 226, 44, 118], + [5, 255, 243, 53], + [6, 255, 212, 179], + [7, 255, 155, 23], + [8, 255, 246, 117], + [9, 255, 241, 0], + [10, 255, 211, 178], + [11, 255, 173, 128], + [12, 248, 152, 0], + [13, 154, 89, 24], + [14, 0, 138, 185], + [15, 82, 161, 40], + [16, 219, 228, 163], + [17, 0, 119, 64], + [18, 0, 110, 172], + [19, 116, 190, 230], + [20, 0, 155, 212], + [21, 0, 117, 190], + [22, 143, 40, 112], + [23, 220, 153, 190], + [24, 226, 44, 118], + [25, 126, 40, 111], + [26, 73, 69, 43], + [27, 203, 63, 42], + [28, 255, 198, 190], + [29, 135, 49, 45], + [30, 150, 136, 120], + [31, 198, 182, 175], + [32, 166, 154, 145], + [33, 191, 88, 22], + [34, 255, 212, 179], + [35, 251, 139, 105], + [36, 154, 89, 24], + [37, 186, 222, 200], + [38, 0, 124, 140], + [39, 87, 84, 83], + ], +} +color_tables = [ + { + "name": "Physics", + "discrete": False, + "colors": [ + [0, 255, 0, 0], + [0.25, 255, 255, 0], + [0.5, 0, 255, 0], + [0.75, 0, 255, 255], + [1, 0, 0, 255], + ], + "colorNaN": [255, 255, 255], + "description": "Full options color table", + "colorBelow": [255, 0, 0], + "colorAbove": [0, 0, 255], + }, + { + "name": "YellowDarkGreen", + "discrete": False, + "colors": [[0.0, 255, 255, 0], [1.0, 0, 100, 0]], + "colorNaN": [255, 255, 255], + "description": "Full options color table", + "colorBelow": [255, 0, 0], + "colorAbove": [0, 0, 255], + }, + stratigraphy_color_table, +] + + +# Need to choose between formations or lithology coloring/pattern +wellpick_formations = { + "name": wellpick_name, + "colorTables": [stratigraphy_color_table], + "color": "Stratigraphy", + "wellpick": { + "header": {"name": "Set 1", "well": "Well 1"}, + "curves": [ + { + "name": Curves.md.value, + "description": None, + "quantity": "m", + "unit": "M", + "valueType": "float", + "dimensions": 1, + }, + { + "name": wellpick_name, + "description": None, + "quantity": None, + "unit": "M", + "valueType": "string", + "dimensions": 1, + }, + ], + "data": [[1811.0, "FM 1"], [2450.0, "FM 2"], [3200, "FM 3"]], + "metadata_discrete": { + wellpick_name: { + "attributes": ["color", "code"], + "objects": { + "FM 1": [[0, 0, 255, 255], 0], + "FM 2": [[0, 255, 0, 255], 1], + "FM 3": [[255, 0, 0, 255], 2], + }, + } + }, + }, +} + +archelem_codes = [0, 1, 2] +stratigraphy_color_map = {c[0]: c[1:] for c in stratigraphy_color_table["colors"]} # type: ignore +pattern_opacity = 255 +archelem_start_depths = [1900, 2400, 3000] +last_archelem_end_depth = 3500 +archelem_names = ["AE_1", "AE_2", "AE_3"] +wellpicks_lithology = { + "name": wellpick_name, + "colorTables": [stratigraphy_color_table], + "color": "Stratigraphy", + "wellpick": { + "header": {"name": "Set 1", "well": "Well 2"}, + "curves": [ + { + "name": Curves.md.value, + "quantity": "m", + "unit": "M", + "valueType": "float", + "dimensions": 1, + }, + {"name": wellpick_name, "valueType": "string", "dimensions": 1}, + ], + "data": [ + [a, b] + for a, b in zip( + archelem_start_depths + [last_archelem_end_depth], + archelem_names + [archelem_names[-1] + "_stop"], + ) + ], # Need dummy at the end to make lower bound for last entry + "metadata_discrete": { + wellpick_name: { + "attributes": ["color", "code"], + "objects": { + archelem_names[lf_code]: [ + stratigraphy_color_map[lf_code] + [pattern_opacity], + lf_code, + ] + for lf_code in archelem_codes + }, + } + }, + }, +} +wellpicks = [wellpicks_lithology, wellpicks_lithology] + +# Reuse pattern for several to avoid too much gif-images in repo +patterns = [ + [archelem_names[0], 0], + [archelem_names[1], 1], + [archelem_names[2], 2], +] + +patternsTable = { + "patternSize": 24, + "patternImages": [ + "static/Anhydrite.gif", + "static/Bitumenious.gif", + "static/Browncoal.gif", + ], + "names": [ + "Anhydrite", + "Bitumenious", + "Browncoal", + ], +} + +# Do not specify '0' to make this appear as "undefined" in viewer (intentional to test how not defined litho-values look) +lithology_info_table = { + "codes": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"], + "names": [ + "Anhydrite", + "Bitumenious", + "Browncoal", + "Calcareous Dolostone", + "Chalk", + "Clay", + "Coal", + "Conglomerate", + "Diamond_lines", + "Dolomitic_limestone", + ], + "images": [ + "/static/Anhydrite.gif", + "/static/Bitumenious.gif", + "/static/Browncoal.gif", + "/static/Calcareous_dolostone.gif", + "/static/Chalk.gif", + "/static/Clay.gif", + "/static/Coal.gif", + "/static/Conglomerate.gif", + "/static/Diamond_lines.gif", + "/static/Dolomitic_limestone.gif", + ], + "colors": [ + [255, 193, 0], + [255, 155, 76], + [255, 223, 161], + [204, 153, 255], + [101, 167, 64], + [255, 243, 53], + [5, 255, 243, 53], + [6, 255, 212, 179], + [7, 255, 155, 23], + [8, 255, 246, 117], + [9, 255, 241, 0], + [10, 255, 211, 178], + ], +} + +wellpickFlatting = [None, None] +# wellpickFlatting = [ +# "Litho_" +# "Hor_2", +# "Hor_4" +# ] +spacers = [312] + +wellDistances = {"units": "m", "distances": [200]} +axisTitles = {"md": Curves.md.value, "tvd": Curves.tvd.value} + +nan = np.nan + +md_dummy = [ + 1513, + 1627, + 1743, + 1857, + 1970, + 2086, + 2199, + 2313, + 2429, + 2542, + 2656, + 2771, + 2885, + 2999, + 3112, + 3228, + 3342, + 3456, + 3572, +] + +n = len(md_dummy) # 19 + +tvd_dummy = [ + 1488, + 1602, + 1716, + 1830, + 1945, + 2058, + 2175, + 2288, + 2402, + 2518, + 2631, + 2744, + 2860, + 2973, + 3088, + 3202, + 3316, + 3432, + 3546, +] + +phit_dummy = [ + 0.221, + 0.231, + 0.22, + 0.212, + 0.24, + 0.244, + 0.23, + 0.141, + 0.168, + 0.199, + 0.193, + 0.174, + 0.187, + 0.238, + 0.219, + 0.21, + nan, + 0.17, + 0.167, +] + +gr_dummy = [ + 33, + 36, + 27.0, + 29.0, + 23.0, + nan, + nan, + 34.0, + 33.0, + 28.0, + 31.0, + 29.0, + 23.0, + 26.3, + 24.0, + 27.0, + 32.5, + 80.0, + 99.0, +] + +vsh_dummy = [ + 0.607, + 0.025, + 0.227, + 0.295, + 0.454, + 0.293, + 0.501, + 0.346, + 0.228, + 0.753, + 0.159, + 0.736, + 0.054, + 0.57, + 0.13, + 0.356, + 0.572, + 0.757, + 0.092, +] + +nphi_dummy = [ + 0.238, + 0.09, + 0.114, + 0.306, + 0.301, + 0.522, + 0.141, + 0.313, + 0.523, + 0.649, + nan, + 0.094, + 0.551, + 0.556, + nan, + 0.267, + 0.176, + 0.287, + 0.221, +] + +rhob_dummy = [ + 1.949, + 1.599, + 1.689, + nan, + 2.568, + 2.687, + 2.616, + 2.206, + 2.316, + 1.643, + 2.5, + 1.604, + 2.182, + 1.931, + nan, + 2.404, + 2.898, + 2.034, + 1.954, +] + +swt_dummy = [ + 0.51, + 0.472, + 0.134, + 0.786, + 0.524, + 0.851, + 0.468, + 0.136, + 0.351, + 0.627, + 0.217, + 0.136, + 0.825, + 0.295, + 0.781, + 0.268, + 0.423, + 0.934, + 0.809, +] + +klogh_dummy = [ + 0.027, + 0.041, + 0.122, + 0.014, + 0.05, + 0.015, + 0.037, + 0.031, + 0.01, + 0.15, + 0.011, + 0.272, + 0.035, + 0.029, + 0.025, + 0.031, + 0.184, + 0.022, + 0.14, +] + +coreplug_poro_dummy = [ + 0.078, + 0.032, + 0.177, + 0.166, + 0.101, + 0.263, + 0.044, + 0.111, + 0.137, + 0.305, + 0.1, + 0.261, + 0.261, + 0.186, + 0.074, + 0.167, + 0.057, + 0.283, + 0.263, +] +coreplug_permx_dummy = [ + 9220.508, + 31.435, + 52.207, + 3.242, + 2.401, + 674.43, + 75.688, + 0.181, + 6852.265, + 457.26, + 2504.588, + 7816.093, + 0.109, + 1268.735, + 0.123, + 0.167, + 114.998, + 595.884, + 604.471, +] +coreplug_permz_dummy = [ + 985.104, + 1.12, + 0.157, + 0.03, + 1.439, + 0.878, + 1.218, + 4232.84, + 2.315, + 1899.883, + 467.649, + 0.953, + 5.073, + 1.413, + 0.495, + 91.568, + 53.463, + 567.812, + 213.266, +] + +lithofacies_dummy = [2, 8, 10, 1, 4, 0, 4, 8, 1, 9, 0, 0, 4, 8, 1, 8, 8, 7, 1] + +WELL_NAME = "Well name" +single_well_header = { + "name": WELL_NAME, + "well": WELL_NAME, + "startIndex": md_dummy[0], + "endIndex": md_dummy[-1], + "step": 38.086, +} + +welllogs_phit_gr_vsh_nphi_rhob_swt_coreplugs = { + "header": single_well_header, + "curves": [ + { + "name": Curves.md.value, + "description": None, + "quantity": None, + "unit": "m", + "valueType": "float", + "dimensions": 1, + }, + { + "name": Curves.tvd.value, + "description": None, + "quantity": None, + "unit": "m", + "valueType": "float", + "dimensions": 1, + }, + { + "name": Curves.phit.value, + "description": None, + "quantity": None, + "unit": "", + "valueType": "float", + "dimensions": 1, + }, + { + "name": Curves.gr.value, + "description": None, + "quantity": None, + "unit": "", + "valueType": "float", + "dimensions": 1, + }, + { + "name": Curves.vsh.value, + "description": None, + "quantity": None, + "unit": "", + "valueType": "float", + "dimensions": 1, + }, + { + "name": Curves.nphi.value, + "description": None, + "quantity": None, + "unit": "", + "valueType": "float", + "dimensions": 1, + }, + { + "name": Curves.rhob.value, + "description": None, + "quantity": None, + "unit": "", + "valueType": "float", + "dimensions": 1, + }, + { + "name": Curves.swt.value, + "description": None, + "quantity": None, + "unit": "", + "valueType": "float", + "dimensions": 1, + }, + { + "name": Curves.klogh.value, + "description": None, + "quantity": None, + "unit": "", + "valueType": "float", + "dimensions": 1, + }, + { + "name": Curves.core_plug_poro.value, + "description": None, + "quantity": None, + "unit": "", + "valueType": "float", + "dimensions": 1, + }, + { + "name": Curves.core_plug_permx.value, + "description": None, + "quantity": None, + "unit": "", + "valueType": "float", + "dimensions": 1, + }, + { + "name": Curves.core_plug_permz.value, + "description": None, + "quantity": None, + "unit": "", + "valueType": "float", + "dimensions": 1, + }, + { + "name": Curves.lithofacies.value, + "description": "discrete", + "quantity": "DISC", + "unit": "DISC", + "valueType": "integer", + "dimensions": 1, + }, + ], + "data": [ + [ + md, + tvd, + phit, + gr, + vsh, + nphi, + rhob, + swt, + klogh, + cp_poro, + cp_permx, + cp_permy, + facies, + ] + for md, tvd, phit, gr, vsh, nphi, rhob, swt, klogh, cp_poro, cp_permx, cp_permy, facies in zip( + md_dummy, + tvd_dummy, + phit_dummy, + gr_dummy, + vsh_dummy, + nphi_dummy, + rhob_dummy, + swt_dummy, + klogh_dummy, + coreplug_poro_dummy, + coreplug_permx_dummy, + coreplug_permz_dummy, + lithofacies_dummy, + ) + ], +} + +welllogs_two_wells = [ + welllogs_phit_gr_vsh_nphi_rhob_swt_coreplugs, + welllogs_phit_gr_vsh_nphi_rhob_swt_coreplugs, +] diff --git a/react/src/demo/example-data/discrete-facies-test.json b/react/src/demo/example-data/discrete-facies-test.json new file mode 100644 index 000000000..38370fe36 --- /dev/null +++ b/react/src/demo/example-data/discrete-facies-test.json @@ -0,0 +1,79 @@ +[ + { + "header": { + "name": "LIS1 .001", + "well": "15/9-19A", + "operator": "STATOIL", + "source": "Converted from LIS by Log Studio 4.87 - Petroware AS", + "startIndex": 2179, + "endIndex": 4131, + "step": 1 + }, + "curves": [ + { + "name": "MD", + "description": "continuous", + "quantity": "m", + "unit": "m", + "valueType": "float", + "dimensions": 1 + }, + { + "name": "ZONELOG", + "description": "discrete", + "quantity": "DISC", + "unit": "DISC", + "valueType": "integer", + "dimensions": 1 + }, + { + "name": "FACIES", + "description": "discrete", + "quantity": "DISC", + "unit": "DISC", + "valueType": "integer", + "dimensions": 1 + }, + { + "name": "PORO_TOT", + "description": "continuous", + "quantity": "", + "unit": "", + "valueType": "float", + "dimensions": 1 + } + ], + "data": [ + [ + 1400, + 2, + 2, + 0.247 + ], + [ + 2179, + 2, + 1, + 0.247 + ], + [ + 2180.6093750344276, + 3, + 2, + 0.137 + ], + [ + 3541.6093750344276, + 3, + 3, + 0.237 + ], + [ + 4000, + 4, + 5, + 0.337 + ] + ] + } +] \ No newline at end of file diff --git a/react/src/demo/example-data/synclog_template_lithologytrack.json b/react/src/demo/example-data/synclog_template_lithologytrack.json new file mode 100644 index 000000000..7b2e48a84 --- /dev/null +++ b/react/src/demo/example-data/synclog_template_lithologytrack.json @@ -0,0 +1,186 @@ +{ + "name": "Template 1", + "scale": { + "primary": "tvd", + "allowSecondary": true + }, + "tracks": [ + { + "plots": [ + { + "name": "ZONELOG", + "style": "discrete" + } + ] + }, + { + "plots": [ + { + "name": "PORO" + }, + { + "name": "NTG" + }, + { + "name": "SW" + } + ] + }, + { + "plots": [ + { + "name": "FACIES", + "style": "discretecanvas" + } + ] + }, + { + "plots": [ + { + "name": "MFOA" + } + ] + }, + { + "plots": [ + { + "name": "FACIES", + "style": "discrete" + } + ] + }, + { + "plots": [ + { + "name": "DD_VOLUME" + } + ] + }, + { + "plots": [ + { + "name": "TEMP" + } + ] + }, + { + "plots": [ + { + "name": "BITSIZE" + } + ] + }, + { + "plots": [ + { + "name": "GRSIM" + } + ] + }, + { + "plots": [ + { + "name": "RACESHM" + } + ] + }, + { + "plots": [ + { + "name": "SW", + "type": "line" + } + ] + }, + { + "plots": [ + { + "name": "MFIA", + "type": "dot" + } + ] + }, + { + "plots": [ + { + "name": "MDIA", + "style": "MD" + } + ] + }, + { + "plots": [ + { + "name": "MTOA" + } + ] + }, + { + "plots": [ + { + "name": "MTIA" + } + ] + }, + { + "plots": [ + { + "name": "ECDT" + } + ] + }, + { + "plots": [ + { + "name": "BDTI" + } + ] + }, + { + "plots": [ + { + "name": "BDDI" + } + ] + }, + { + "plots": [ + { + "name": "BRVC" + } + ] + }, + { + "plots": [ + { + "name": "TCTI" + } + ] + } + ], + "styles": [ + { + "name": "HKL", + "type": "gradientfill", + "colorTable": "Physics", + "color": "green" + }, + { + "name": "MD", + "scale": "linear", + "type": "area", + "color": "blue", + "fill": "green" + }, + { + "name": "discretecanvas", + "type": "canvas", + "colorTable": "Stratigraphy" + }, + { + "name": "discrete", + "type": "stacked", + "colorTable": "Stratigraphy" + } + ] +} diff --git a/react/src/lib/components/WellLogViewer/SyncLogViewer.stories.jsx b/react/src/lib/components/WellLogViewer/SyncLogViewer.stories.jsx index 60b85e2cd..fadb1d3f9 100644 --- a/react/src/lib/components/WellLogViewer/SyncLogViewer.stories.jsx +++ b/react/src/lib/components/WellLogViewer/SyncLogViewer.stories.jsx @@ -359,3 +359,80 @@ Default.args = { wellpickPatternFill: true, }, }; + +const lithologyInfoTable = { + codes: ["1", "2", "3", "4", "5"], + names: patternNamesEnglish, + images: patternImages, + colors: [ + [255, 193, 0], + [255, 155, 76], + [255, 223, 161], + [204, 153, 255], + [101, 167, 64], + [255, 243, 53], + ], +}; + +export const LithofaciesTrack = Template.bind({}); +LithofaciesTrack.args = { + id: "Sync-Log-Viewer-litho", + syncTrackPos: true, + syncContentDomain: true, + syncContentSelection: true, + syncTemplate: true, + horizontal: false, + + welllogs: [ + require("../../../demo/example-data/discrete-facies-test.json")[0], + require("../../../demo/example-data/L916MUD.json")[0], + require("../../../demo/example-data/Lis1.json")[0], + ], + templates: [ + require("../../../demo/example-data/synclog_template_lithologytrack.json"), + require("../../../demo/example-data/synclog_template.json"), + require("../../../demo/example-data/synclog_template.json"), + ], + colorTables: colorTables, + lithologyInfoTable: lithologyInfoTable, + wellpicks: [ + { + wellpick: require("../../../demo/example-data/wellpicks.json")[0], + name: "HORIZON", + colorTables: require("../../../demo/example-data/wellpick_colors.json"), + color: "Stratigraphy", + }, + { + wellpick: require("../../../demo/example-data/wellpicks.json")[1], + name: "HORIZON", + colorTables: require("../../../demo/example-data/wellpick_colors.json"), + color: "Stratigraphy", + }, + { + wellpick: require("../../../demo/example-data/wellpicks.json")[0], + name: "HORIZON", + colorTables: require("../../../demo/example-data/wellpick_colors.json"), + color: "Stratigraphy", + }, + ], + wellpickFlatting: ["Hor_2", "Hor_4"], + spacers: [312, 255], + wellDistances: { + units: "m", + distances: [2048.3, 512.7], + }, + + axisTitles: axisTitles, + axisMnemos: axisMnemos, + + viewTitles: true, // show default welllog view titles (a wellname from the welllog) + + welllogOptions: { + wellpickColorFill: false, + wellpickPatternFill: false, + }, + spacerOptions: { + wellpickColorFill: false, + wellpickPatternFill: false, + }, +}; diff --git a/react/src/lib/components/WellLogViewer/SyncLogViewer.tsx b/react/src/lib/components/WellLogViewer/SyncLogViewer.tsx index 12beb923f..ef9302904 100644 --- a/react/src/lib/components/WellLogViewer/SyncLogViewer.tsx +++ b/react/src/lib/components/WellLogViewer/SyncLogViewer.tsx @@ -36,7 +36,7 @@ import { LogViewer } from "@equinor/videx-wellog"; import { Info, InfoOptions } from "./components/InfoTypes"; import { isEqualRanges } from "./components/WellLogView"; -//import { boolean } from "mathjs"; +import { LithologyInfoTable } from "./components/LithologyTrack"; export function isEqualArrays( // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -70,9 +70,14 @@ interface Props { * Prop containing color table data. */ colorTables: ColorTable[]; + /** + * Table of codes, names, patterns and color for lithology (canvas) tracks + */ + lithologyInfoTable?: LithologyInfoTable; /** * Set to true for default titles or to array of individial welllog titles */ + viewTitles?: boolean | (boolean | string | JSX.Element)[]; /** @@ -175,6 +180,9 @@ export const argTypesSyncLogViewerProp = { colorTables: { description: "Prop containing color table data.", }, + lithologyInfoTable: { + description: "Code, name, color and image for lithology tracks", + }, wellpicks: { description: "Well Picks data array", }, @@ -851,6 +859,7 @@ class SyncLogViewer extends Component { viewTitle={viewTitle} template={template} colorTables={this.props.colorTables} + lithologyInfoTable={this.props.lithologyInfoTable} wellpick={this.props.wellpicks?.[index]} patternsTable={this.props.patternsTable} patterns={this.props.patterns} @@ -1069,7 +1078,10 @@ SyncLogViewer.propTypes = { * Prop containing color table data */ colorTables: PropTypes.array.isRequired, - + /** + * Table of codes, names, patterns and color for lithology (canvas) tracks + */ + lithologyInfoTable: PropTypes.object, /** * Well Picks data array */ diff --git a/react/src/lib/components/WellLogViewer/components/LithologyTrack.tsx b/react/src/lib/components/WellLogViewer/components/LithologyTrack.tsx new file mode 100644 index 000000000..249a9ff58 --- /dev/null +++ b/react/src/lib/components/WellLogViewer/components/LithologyTrack.tsx @@ -0,0 +1,254 @@ +import { StackedTrackOptions } from "@equinor/videx-wellog/dist/tracks/stack/interfaces"; +import { Scale } from "@equinor/videx-wellog/dist/common/interfaces"; +import { setProps, StackedTrack } from "@equinor/videx-wellog"; +import { + OnMountEvent, + OnRescaleEvent, + OnUpdateEvent, +} from "@equinor/videx-wellog/dist/tracks/interfaces"; +import { select } from "d3-selection"; + +interface RGBColor { + r: number; + g: number; + b: number; + a?: number; +} + +interface LithologyTrackDataRow { + from: number; + to: number; + name?: string | number; + color?: RGBColor; +} + +// TODO: change to more map/dict structure? (keys correspond to data values) +// { +// 1: {name: string, imagePath: string, color: RGBColor}, +// 32: {name: string, imagePath: string, color: RGBColor}, +// } +export interface LithologyInfoTable { + codes: (string | number)[]; + names: string[]; // For writing on track + secondaryNames?: string[]; + images?: string[]; + colors?: ([number, number, number, number] | [number, number, number])[]; +} + +// As LithologyTrack subclasses StackedTrack which has "data: Promise | Function | any;", include "any" as type to avoid problems when using class +export interface LithologyTrackOptions extends StackedTrackOptions { + lithologyInfoTable?: LithologyInfoTable; + /* eslint-disable */ + data?: LithologyTrackDataRow[] | Promise | any; +} + +export class LithologyTrack extends StackedTrack { + lithologyInfo: LithologyInfoTable; + patterns: Map; // TODO: fix type + ctx: CanvasRenderingContext2D | undefined; + + constructor(id: string | number, props: LithologyTrackOptions) { + super(id, props); + this.lithologyInfo = props.lithologyInfoTable as LithologyInfoTable; // TODO - ensure table is given and valid + this.patterns = new Map(); + } + + loadPatterns(): void { + const { data } = this; + if (!data) return; + + // Find unique canvas code names in data for this track. Later only load images for used codes + const uniqueCodes = [ + ...new Set(data.map((item: LithologyTrackDataRow) => item.name)), + ] as (string | number)[]; // TODO: why doesn't typescript understand this itself? + setupLithologyInfoMap(this.lithologyInfo); + uniqueCodes.forEach((code) => { + const pattern = lithologyInfoMap.get(code); + // const pattern = patterns.find(pattern => code === pattern.code) + if (pattern?.patternImage) { + // Check if we have loaded pattern + if (!this.patterns.get(code)) { + // Temporarily set solid color while we get image to avoid fetching multiple times + this.patterns.set(code, "#eee"); + // Create pattern + const patternImage = new Image(); + patternImage.src = pattern.patternImage; + patternImage.onload = () => { + this.patterns.set( + code, + this.ctx?.createPattern( + patternImage, + "repeat" + ) as CanvasPattern + ); + }; + } + } + }); + } + + plot(): void { + super.plot(); + const { ctx, scale: yscale, data, patterns } = this; + + if (!ctx || !data) return; + const rectangles = scaleData(yscale, data); + const { width: rectWidth, clientWidth, clientHeight } = ctx.canvas; + ctx.clearRect(0, 0, clientWidth, clientHeight); + rectangles.forEach((rectangle: LithologyTrackDataRowScaled) => { + // Save/restore to move the pattern, if not the pattern will look odd when scrolling + ctx.save(); + // Translate context to draw position + ctx.translate(0, rectangle.yFrom); + + const nameColorPattern = lithologyInfoMap.get( + rectangle.lithologyCode + ); + // Draw rect at the origin of the context + const rectHeight = rectangle.yTo - rectangle.yFrom; + + // Background color + // Color from colorImageName map input, not from data originating from overall colormaps input! + ctx.fillStyle = `rgb(${nameColorPattern?.color?.r}, ${nameColorPattern?.color?.g},${nameColorPattern?.color?.b})`; // `rgb(${rectangle.color.r}, ${rectangle.color.g},${rectangle.color.b})`; + ctx.fillRect(0, 0, rectWidth, rectHeight); + // Pattern + ctx.fillStyle = patterns.get(rectangle.lithologyCode) || "#eee"; + ctx.fillRect(0, 0, rectWidth, rectHeight); + + // Overlay color for text + const fractionTextWidth = 0.2; + ctx.fillStyle = `rgb(${nameColorPattern?.color?.r}, ${nameColorPattern?.color?.g},${nameColorPattern?.color?.b})`; + ctx.fillRect( + rectWidth * 0.5, + 0, + rectWidth * fractionTextWidth, + rectHeight + ); + ctx.restore(); + + ctx.save(); + // Rotate before adding text + ctx.translate( + rectWidth * 0.5 + rectWidth * fractionTextWidth * 0.1, + rectangle.yFrom + rectHeight / 2 + ); + ctx.rotate(Math.PI / 2); + ctx.textAlign = "center"; + ctx.font = `bold ${0.9 * rectWidth * fractionTextWidth}px serif`; //"bold 10px serif"; + ctx.fillText( + `${nameColorPattern?.lithologyName}`, + 0, + fractionTextWidth / 2, + rectHeight + ); + ctx.restore(); + }); + } + + onMount(trackEvent: OnMountEvent): void { + super.onMount(trackEvent); + const canvas = select(trackEvent.elm) + .append("canvas") + .style("position", "absolute"); + this.ctx = canvas.node()?.getContext("2d") ?? undefined; + const { options } = this; + if (options.data) { + options.data().then( + (data: LithologyTrackDataRow[]) => { + this.data = data; + this.plot(); + }, + (error: Error | string) => super.onError(error) + ); + } + } + + onRescale(rescaleEvent: OnRescaleEvent): void { + super.onRescale(rescaleEvent); + this.plot(); + } + + onUpdate(event: OnUpdateEvent): void { + super.onUpdate(event); + const { ctx, elm } = this; + + if (ctx) { + const canvas = select(ctx.canvas); + const props = { + styles: { + width: `${elm.clientWidth}px`, + height: `${elm.clientHeight}px`, + }, + attrs: { + width: elm.clientWidth, + height: elm.clientHeight, + }, + }; + setProps(canvas, props); + } + this.plot(); + } + onDataLoaded(): void { + this.loadPatterns(); + this.plot(); + } +} + +interface LithologyTrackDataRowScaled { + yFrom: number; + yTo: number; + lithologyCode: number | string; + color: RGBColor; +} + +function scaleData(scale: Scale, data: LithologyTrackDataRow[]) { + if (!data) return []; + + function scale_to_and_from_depths( + rect: LithologyTrackDataRowScaled[], + item: LithologyTrackDataRow + ) { + rect.push({ + yFrom: scale(item.from), + yTo: scale(item.to), + lithologyCode: item.name as number | string, + color: item.color as RGBColor, + }); + return rect as LithologyTrackDataRowScaled[]; + } + return data.reduce( + scale_to_and_from_depths, + [] as LithologyTrackDataRowScaled[] + ); +} + +// Map that all tracks may use, not belonging to a specific track instance +const lithologyInfoMap = new Map(); + +interface PatternMapEntry { + code: string | number; + patternImage: string; + lithologyName?: string; + color?: { r: number; g: number; b: number }; +} + +function setupLithologyInfoMap(lithologyInfo: LithologyInfoTable) { + lithologyInfo.codes.map((e, i) => { + if (!lithologyInfoMap.has(e)) { + lithologyInfoMap.set(e, { + code: e, + lithologyName: lithologyInfo.names[i], + patternImage: lithologyInfo.images + ? lithologyInfo.images[i] + : undefined, + color: lithologyInfo.colors + ? { + r: lithologyInfo.colors[i][0], + g: lithologyInfo.colors[i][1], + b: lithologyInfo.colors[i][2], + } + : undefined, + } as PatternMapEntry); + } + }); +} diff --git a/react/src/lib/components/WellLogViewer/components/WellLogTemplateTypes.ts b/react/src/lib/components/WellLogViewer/components/WellLogTemplateTypes.ts index cf45e1fb3..51ee8cf97 100644 --- a/react/src/lib/components/WellLogViewer/components/WellLogTemplateTypes.ts +++ b/react/src/lib/components/WellLogViewer/components/WellLogTemplateTypes.ts @@ -8,7 +8,8 @@ export type TemplatePlotTypes = | "area" | "differential" | "gradientfill" - | "stacked"; + | "stacked" + | "canvas"; export type CSSColor = string; // rgbhexcolor pattern: "^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$" diff --git a/react/src/lib/components/WellLogViewer/components/WellLogView.tsx b/react/src/lib/components/WellLogViewer/components/WellLogView.tsx index 1083428a2..ea5c64fbf 100644 --- a/react/src/lib/components/WellLogViewer/components/WellLogView.tsx +++ b/react/src/lib/components/WellLogViewer/components/WellLogView.tsx @@ -64,6 +64,8 @@ import { getSelectedTrackIndices, setSelectedTrackIndices, } from "../utils/log-viewer"; +import { Info } from "./InfoTypes"; +import { LithologyInfoTable } from "./LithologyTrack"; const rubberBandSize = 9; const rubberBandOffset = rubberBandSize / 2; @@ -698,14 +700,16 @@ function setTracksToController( axes: AxesInfo, welllog: WellLog | undefined, // JSON Log Format template: Template, // JSON - colorTables: ColorTable[] // JSON + colorTables: ColorTable[], // JSON + lithologyInfoTable?: LithologyInfoTable ): ScaleInterpolator { const { tracks, minmaxPrimaryAxis, primaries, secondaries } = createTracks( welllog, axes, template.tracks, template.styles, - colorTables + colorTables, + lithologyInfoTable ); logController.reset(); const scaleInterpolator = createScaleInterpolator(primaries, secondaries); @@ -923,8 +927,6 @@ export interface WellLogController { getTemplate(): Template; } -import { Info } from "./InfoTypes"; - export interface WellLogViewOptions { /** * Fill with color between well picks @@ -968,6 +970,11 @@ export interface WellLogViewProps { */ template: Template; + /** + * Table of codes, names, patterns and color for lithology (canvas) tracks + */ + lithologyInfoTable?: LithologyInfoTable; + /** * Prop containing color table data for discrete well logs */ @@ -1077,6 +1084,10 @@ export const argTypesWellLogViewProp = { colorTables: { description: "Prop containing color table data for discrete well logs.", }, + lithologyInfoTable: { + description: + "Lithology code, lithology name, image reference and color for lithology tracks", + }, wellpick: { description: "Well Picks data", }, @@ -1446,7 +1457,8 @@ class WellLogView axes, this.props.welllog, this.template, - this.props.colorTables + this.props.colorTables, + this.props.lithologyInfoTable ); addWellPickOverlay(this.logController, this); } diff --git a/react/src/lib/components/WellLogViewer/utils/tracks.ts b/react/src/lib/components/WellLogViewer/utils/tracks.ts index c8d2b48d0..2469dd37a 100644 --- a/react/src/lib/components/WellLogViewer/utils/tracks.ts +++ b/react/src/lib/components/WellLogViewer/utils/tracks.ts @@ -57,6 +57,13 @@ import { updateLegendRows } from "./log-viewer"; import { deepCopy } from "./deepcopy"; +import { createPlotType } from "@equinor/videx-wellog"; +import { defaultPlotFactory } from "@equinor/videx-wellog"; +import { + LithologyInfoTable, + LithologyTrack, + LithologyTrackOptions, +} from "../components/LithologyTrack"; export function indexOfElementByName(array: Named[], name: string): number { if (array && name) { const nameUpper = name.toUpperCase(); @@ -262,7 +269,7 @@ function isValidPlotType(plotType: string): boolean { "area", "differential", "gradientfill", - + "canvas", "stacked", ].indexOf(plotType) >= 0 ); @@ -1061,9 +1068,6 @@ export function getDiscreteMeta( return null; // something went wrong } -import { createPlotType } from "@equinor/videx-wellog"; -import { defaultPlotFactory } from "@equinor/videx-wellog"; - const plotFactory: PlotFactory = { ...defaultPlotFactory, gradientfill: createPlotType(GradientFillPlot), @@ -1285,6 +1289,80 @@ function addGraphTrack( info.tracks.push(track); } } +function addLithologyTrack( + info: TracksInfo, + welllog: WellLog, + curves: WellLogCurve[], + data: WellLogDataRow[], + iPrimaryAxis: number, + templateTrack: TemplateTrack, + templateStyles?: TemplateStyle[], + colorTables?: ColorTable[], + lithologInfoTable?: LithologyInfoTable +): void { + const templatePlot = templateTrack.plots[0]; + const name = templatePlot.name; + + const iCurve = indexOfElementByName(curves, name); + if (iCurve < 0) return; // curve not found + const curve = curves[iCurve]; + + const dimensions = curve.dimensions === undefined ? 1 : curve.dimensions; + if (dimensions !== 1) return; + + const plotData = preparePlotData(data, iCurve, iPrimaryAxis); + checkMinMax(info.minmaxPrimaryAxis, plotData.minmaxPrimaryAxis); + + // make full props + const templatePlotProps = getTemplatePlotProps( + templatePlot, + templateStyles + ); + const templateTrackFullPlot: TemplateTrack = deepCopy(templateTrack); + + templateTrackFullPlot.title = makeTrackHeader(welllog, templateTrack); + templateTrackFullPlot.plots[0].type = templatePlotProps.type; + + // curve.valueType === "integer", "string" + const logColor = templatePlotProps.colorTable; + let colorTable: ColorTable | undefined = undefined; + if (logColor) { + if (colorTables) { + colorTable = colorTables.find( + (colorTable) => colorTable.name == logColor + ); + if (!colorTable) + console.error("Missed '" + logColor + "' color table"); + } else { + console.error( + "No color tables file given for '" + logColor + "' color table" + ); + } + } else { + console.error("No color table given in template plot props"); + } + const meta = getDiscreteMeta(welllog, name); + if (!meta && curve.valueType == "integer") + console.log( + "Discrete meta information for '" + + name + + "' not found. Use default" + ); + + const showLines = true; + const options: LithologyTrackOptions = { + abbr: name, // name of the only plot + legendConfig: stackLegendConfig, + data: createStackData.bind(null, plotData.data, colorTable, meta), + showLabels: true, + showLines: showLines, + lithologyInfoTable: lithologInfoTable, + }; + setStackedTrackOptionFromTemplate(options, templateTrackFullPlot); + const track = new LithologyTrack(undefined as unknown as number, options); + updateStackedTrackScale(track); + info.tracks.push(track); +} function addStackedTrack( info: TracksInfo, welllog: WellLog, @@ -1359,7 +1437,7 @@ function addStackedTrack( info.tracks.push(track); } -function isStackedTemplateTrack( +function getTemplateTrackType( templateTrack: TemplateTrack, templateStyles?: TemplateStyle[] ) { @@ -1370,7 +1448,7 @@ function isStackedTemplateTrack( templatePlot, templateStyles ); - return templatePlotProps.type === "stacked"; + return templatePlotProps.type; } export function createTracks( @@ -1378,7 +1456,8 @@ export function createTracks( axes: AxesInfo, templateTracks: TemplateTrack[], // Part of JSON templateStyles?: TemplateStyle[], // Part of JSON - colorTables?: ColorTable[] // JSON + colorTables?: ColorTable[], // JSON + lithologyInfoTable?: LithologyInfoTable ): TracksInfo { const info = new TracksInfo(); if (welllog) { @@ -1394,28 +1473,51 @@ export function createTracks( if (templateTracks) { for (const templateTrack of templateTracks) { - if (isStackedTemplateTrack(templateTrack, templateStyles)) { - addStackedTrack( - info, - welllog, - curves, - data, - iPrimaryAxis, - templateTrack, - templateStyles, - colorTables - ); - } else { - addGraphTrack( - info, - welllog, - curves, - data, - iPrimaryAxis, - templateTrack, - templateStyles, - colorTables - ); + const trackType = getTemplateTrackType( + templateTrack, + templateStyles + ); + switch (trackType) { + case "stacked": { + addStackedTrack( + info, + welllog, + curves, + data, + iPrimaryAxis, + templateTrack, + templateStyles, + colorTables + ); + break; + } + case "canvas": { + addLithologyTrack( + info, + welllog, + curves, + data, + iPrimaryAxis, + templateTrack, + templateStyles, + colorTables, + lithologyInfoTable + ); + break; + } + default: { + addGraphTrack( + info, + welllog, + curves, + data, + iPrimaryAxis, + templateTrack, + templateStyles, + colorTables + ); + break; + } } } } diff --git a/react/src/lib/inputSchema/WellLogTemplate.json b/react/src/lib/inputSchema/WellLogTemplate.json index 15dcf1459..ecb703b02 100644 --- a/react/src/lib/inputSchema/WellLogTemplate.json +++ b/react/src/lib/inputSchema/WellLogTemplate.json @@ -45,7 +45,8 @@ "area", "differential", "gradientfill", - "stacked" + "stacked", + "canvas" ], "default": "line" },