diff --git a/backend_py/libs/services/src/webviz_services/smda_access/drogon/_drogon_well_data.py b/backend_py/libs/services/src/webviz_services/smda_access/drogon/_drogon_well_data.py index 34f96418f2..76d3d4fd13 100644 --- a/backend_py/libs/services/src/webviz_services/smda_access/drogon/_drogon_well_data.py +++ b/backend_py/libs/services/src/webviz_services/smda_access/drogon/_drogon_well_data.py @@ -1,4 +1,5 @@ from typing import List +import numpy as np from ..types import WellboreTrajectory, WellboreHeader, WellborePick, StratigraphicColumn @@ -28,8 +29,12 @@ def get_drogon_well_headers() -> List[WellboreHeader]: wellbore_purpose="production", wellbore_status="active", current_track=1, - tvd_max=1774.5, + md_min=0.0, md_max=1799.5, + md_unit="m", + tvd_min=-25.0, + tvd_max=1774.5, + tvd_unit="m", kickoff_depth_md=None, kickoff_depth_tvd=None, parent_wellbore=None, @@ -46,8 +51,12 @@ def get_drogon_well_headers() -> List[WellboreHeader]: wellbore_purpose="production", wellbore_status="active", current_track=1, - tvd_max=1656.9874, + md_min=0.0, md_max=3578.5, + md_unit="m", + tvd_min=-49.0, + tvd_max=1656.9874, + tvd_unit="m", kickoff_depth_md=None, kickoff_depth_tvd=None, parent_wellbore=None, @@ -56,6 +65,22 @@ def get_drogon_well_headers() -> List[WellboreHeader]: def get_drogon_well_trajectories() -> List[WellboreTrajectory]: + # Original second wellbore data + original_tvd_msl = np.array([-49.0, 1293.4185, 1536.9384, 1616.4998, 1630.5153, 1656.9874]) + original_easting = np.array([463256.911, 463564.402, 463637.925, 463690.658, 463910.452, 464465.876]) + original_northing = np.array([5930542.294, 5931057.803, 5931184.235, 5931278.837, 5931688.122, 5932767.761]) + original_md = np.array([0.0, 1477.0, 1761.5, 1899.2601, 2363.9988, 3578.5]) + + # Create 100x more sample points using linear interpolation + num_original_points = len(original_md) + num_interpolated_points = (num_original_points - 1) * 100 + 1 + + # Interpolate based on MD (measured depth) as the independent variable + md_interp = np.linspace(original_md[0], original_md[-1], num_interpolated_points) + tvd_msl_interp = np.interp(md_interp, original_md, original_tvd_msl) + easting_interp = np.interp(md_interp, original_md, original_easting) + northing_interp = np.interp(md_interp, original_md, original_northing) + return [ WellboreTrajectory( wellbore_uuid="drogon_vertical", @@ -68,10 +93,10 @@ def get_drogon_well_trajectories() -> List[WellboreTrajectory]: WellboreTrajectory( wellbore_uuid="drogon_horizontal", unique_wellbore_identifier="55/33-A-4", - tvd_msl_arr=[-49.0, 1293.4185, 1536.9384, 1616.4998, 1630.5153, 1656.9874], - md_arr=[0.0, 1477.0, 1761.5, 1899.2601, 2363.9988, 3578.5], - easting_arr=[463256.911, 463564.402, 463637.925, 463690.658, 463910.452, 464465.876], - northing_arr=[5930542.294, 5931057.803, 5931184.235, 5931278.837, 5931688.122, 5932767.761], + tvd_msl_arr=tvd_msl_interp.tolist(), + md_arr=md_interp.tolist(), + easting_arr=easting_interp.tolist(), + northing_arr=northing_interp.tolist(), ), ] diff --git a/backend_py/libs/services/src/webviz_services/smda_access/smda_access.py b/backend_py/libs/services/src/webviz_services/smda_access/smda_access.py index 45e5b01559..6d6e6aa20f 100644 --- a/backend_py/libs/services/src/webviz_services/smda_access/smda_access.py +++ b/backend_py/libs/services/src/webviz_services/smda_access/smda_access.py @@ -175,8 +175,12 @@ async def get_wellbore_headers_async(self, field_identifier: str) -> List[Wellbo "well_northing", "depth_reference_point", "depth_reference_elevation", + "tvd_min", "tvd_max", + "tvd_unit", + "md_min", "md_max", + "md_unit", ] params = { "_projection": ",".join(projection), @@ -200,6 +204,7 @@ async def get_wellbore_headers_async(self, field_identifier: str) -> List[Wellbo "kickoff_depth_tvd", "parent_wellbore", ] + params = { "_projection": ",".join(projection), "_sort": "unique_wellbore_identifier", diff --git a/backend_py/libs/services/src/webviz_services/smda_access/types.py b/backend_py/libs/services/src/webviz_services/smda_access/types.py index d97195d7b4..e49c74ab95 100644 --- a/backend_py/libs/services/src/webviz_services/smda_access/types.py +++ b/backend_py/libs/services/src/webviz_services/smda_access/types.py @@ -87,8 +87,12 @@ class WellboreHeader(BaseModel): wellbore_purpose: str | None wellbore_status: str | None current_track: int - tvd_max: float - md_max: float + md_min: float | None = None + md_max: float | None = None + md_unit: str | None = None + tvd_min: float | None = None + tvd_max: float | None = None + tvd_unit: str | None = None kickoff_depth_md: float | None kickoff_depth_tvd: float | None parent_wellbore: str | None diff --git a/backend_py/libs/services/src/webviz_services/utils/surface_helpers.py b/backend_py/libs/services/src/webviz_services/utils/surface_helpers.py index 20b1039ce5..1c017906b7 100644 --- a/backend_py/libs/services/src/webviz_services/utils/surface_helpers.py +++ b/backend_py/libs/services/src/webviz_services/utils/surface_helpers.py @@ -1,9 +1,15 @@ from dataclasses import dataclass +from enum import StrEnum import numpy as np import xtgeo +from xtgeo import _cxtgeo +from xtgeo.common.constants import UNDEF_LIMIT + from numpy.typing import NDArray +from webviz_services.service_exceptions import InvalidParameterError, Service + def surface_to_float32_numpy_array(surface: xtgeo.RegularSurface) -> NDArray[np.float32]: masked_values = surface.values.astype(np.float32) @@ -48,3 +54,120 @@ def get_min_max_surface_values(surface: xtgeo.RegularSurface) -> MinMax | None: return None return MinMax(min=masked_min_val, max=masked_max_val) + + +class PickDirection(StrEnum): + """Direction of the pick relative to the surface""" + + UPWARD = "UPWARD" + DOWNWARD = "DOWNWARD" + + +@dataclass +class SurfaceWellPick: + """Surface pick data along a well trajectory""" + + unique_wellbore_identifier: str + x: float + y: float + z: float + md: float + direction: PickDirection + + +@dataclass +class WellTrajectory: + """ + Well trajectory defined by a set of (x, y, z) coordinates and measured depths (md). + + unique_wellbore_identifier: str + x_points: X-coordinates of well trajectory points. + y_points: Y-coordinates of well trajectory points. + z_points: Z-coordinates (depth values) of well trajectory points. + md_points: Measured depth values at each well trajectory point. + """ + + unique_wellbore_identifier: str + x_points: list[float] + y_points: list[float] + z_points: list[float] + md_points: list[float] + + +def get_surface_picks_for_well_trajectory_from_xtgeo( + surf: xtgeo.RegularSurface, + well_trajectory: WellTrajectory, +) -> list[SurfaceWellPick] | None: + """ + Calculate intersections (wellpicks) between a surface and a well trajectory. + + Uses the underlying xtgeo C-extension directly for performance. + + Note that this function performs interpolation internally to find the intersections. Thereby + it can be inaccurate calculations of picks if the length between a well trajectory point and + the next is large and the surface intersects between these points. + + """ + + # Ensure equal length arrays + if not ( + well_trajectory.x_points + and well_trajectory.y_points + and well_trajectory.z_points + and well_trajectory.md_points + and len(well_trajectory.x_points) + == len(well_trajectory.y_points) + == len(well_trajectory.z_points) + == len(well_trajectory.md_points) + ): + raise InvalidParameterError( + "Well trajectory point arrays must be non-empty and of equal length", Service.GENERAL + ) + + xarray = np.array(well_trajectory.x_points, dtype=np.float32) + yarray = np.array(well_trajectory.y_points, dtype=np.float32) + zarray = np.array(well_trajectory.z_points, dtype=np.float32) + mdarray = np.array(well_trajectory.md_points, dtype=np.float32) + + # nval = number of valid picks + # xres, yres, zres = arrays of x,y,z coordinates of picks + # mres = array of measured depth values of picks + # dres = array of direction indicators of picks (1=downward, 0=upward) + nval, xres, yres, zres, mres, dres = _cxtgeo.well_surf_picks( + xarray, + yarray, + zarray, + mdarray, + surf.ncol, + surf.nrow, + surf.xori, + surf.yori, + surf.xinc, + surf.yinc, + surf.yflip, + surf.rotation, + surf.npvalues1d, + xarray.size, + yarray.size, + zarray.size, + mdarray.size, + mdarray.size, + ) + if nval < 1: + return None + + mres[mres > UNDEF_LIMIT] = np.nan + + res: list[SurfaceWellPick] = [] + for i in range(nval): + res.append( + SurfaceWellPick( + unique_wellbore_identifier=well_trajectory.unique_wellbore_identifier, + x=xres[i], + y=yres[i], + z=zres[i], + md=mres[i], + direction=PickDirection.DOWNWARD if dres[i] == 1 else PickDirection.UPWARD, + ) + ) + return res diff --git a/backend_py/libs/services/src/webviz_services/utils/surfaces_well_trajectory_formation_segments.py b/backend_py/libs/services/src/webviz_services/utils/surfaces_well_trajectory_formation_segments.py new file mode 100644 index 0000000000..d2cebb6261 --- /dev/null +++ b/backend_py/libs/services/src/webviz_services/utils/surfaces_well_trajectory_formation_segments.py @@ -0,0 +1,263 @@ +from dataclasses import dataclass +import logging +from typing import Literal + +import numpy as np +import xtgeo + + +from webviz_services.service_exceptions import InvalidDataError, InvalidParameterError, Service + +from .surface_helpers import get_surface_picks_for_well_trajectory_from_xtgeo, PickDirection, SurfaceWellPick, WellTrajectory + + +LOGGER = logging.getLogger(__name__) + + +@dataclass +class CategorizedPick: + """Helper dataclass to associate a pick with its surface type.""" + + pick: SurfaceWellPick + surface_type: Literal["top", "bottom"] + + +@dataclass +class FormationSegment: + """ + Segment of a formation defined by top and bottom surface. + + The formation segment is defined by the md (measured depth) value along the well trajectory, + at enter and exit of the formation. + """ + + md_enter: float + md_exit: float + + +@dataclass +class WellTrajectoryFormationSegments: + """ + Segments of a well trajectory that intersects a formation defined by top and bottom surfaces. + + A well can enter and exit a formation multiple times, resulting in multiple segments. + + unique_wellbore_identifier: str + formation_segments: list[FormationSegment] + """ + + unique_wellbore_identifier: str + formation_segments: list[FormationSegment] + + +def create_well_trajectory_formation_segments( + well_trajectory: WellTrajectory, + top_depth_surface: xtgeo.RegularSurface, + bottom_depth_surface: xtgeo.RegularSurface | None = None, + surface_collapse_tolerance: float = 0.01, +) -> WellTrajectoryFormationSegments: + """ + Create formation segments for a well trajectory based on top and optional bottom surface. + + **Description:** + - The formation is defined by a top surface and an optional bottom surface. If bottom surface + is not provided, the formation is considered to extend to the end of the well trajectory. + + - Each segment is defined by the measured depth (md) values where the well enters and exits + the formation. If a well starts or ends inside the formation, the corresponding md value is + taken from the start or end of the well trajectory, respectively. + + - The function calculates well picks for the top and bottom surfaces using xtgeo, and then + create formation segments based on these picks and the well trajectory. The well picks + represent the intersection points of the well trajectory with provided surfaces, and + determines the segments of the well trajectory that lie within the formation defined by + these surfaces. + + Args: + well_trajectory (WellTrajectory): The well trajectory containing x, y, z, and md values. + top_depth_surface (xtgeo.RegularSurface): The top bounding depth surface of the formation. + bottom_depth_surface (xtgeo.RegularSurface): The optional bottom bounding depth surface of + the formation. + Returns: + WellTrajectoryFormationSegments: The segments where the well trajectory is within the + formation. With measured depth values at entry and exit. + """ + + # Compare topology of top and bottom surfaces (only if both surfaces are provided) + if bottom_depth_surface is not None: + if top_depth_surface.compare_topology(bottom_depth_surface) is False: + message = f"Top and bottom surfaces have different topology. Cannot compute formation segments for well {well_trajectory.unique_wellbore_identifier}." + LOGGER.warning(message) + raise InvalidParameterError(message, Service.GENERAL) + + # With equal topology, we can do a quick check to see if surfaces are interleaved + # or if top is actually above bottom + top_z_value = top_depth_surface.get_values1d() + bottom_z_value = bottom_depth_surface.get_values1d() + diff = bottom_z_value - top_z_value + if np.any(diff < -abs(surface_collapse_tolerance)): + message = ( + f"Surface depth validation failed when computing formation segments for well {well_trajectory.unique_wellbore_identifier}: " + f"computed depth difference is below the collapse tolerance ({surface_collapse_tolerance}). " + "This suggests interleaved surfaces or a top surface located below the bottom. " + "Review the surface inputs." + ) + LOGGER.warning(message) + raise InvalidParameterError(message, Service.GENERAL) + + top_picks = get_surface_picks_for_well_trajectory_from_xtgeo( + surf=top_depth_surface, + well_trajectory=well_trajectory, + ) + + bottom_picks: list[SurfaceWellPick] | None = [] + if bottom_depth_surface is not None: + bottom_picks = get_surface_picks_for_well_trajectory_from_xtgeo( + surf=bottom_depth_surface, + well_trajectory=well_trajectory, + ) + + # Allowed with empty bottom picks + if not top_picks: + return WellTrajectoryFormationSegments( + unique_wellbore_identifier=well_trajectory.unique_wellbore_identifier, formation_segments=[] + ) + + return WellTrajectoryFormationSegments( + unique_wellbore_identifier=well_trajectory.unique_wellbore_identifier, + formation_segments=_create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=well_trajectory, + top_surface_picks=top_picks, + bottom_surface_picks=bottom_picks, + ), + ) + + +def _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory: WellTrajectory, + top_surface_picks: list[SurfaceWellPick], + bottom_surface_picks: list[SurfaceWellPick] | None = None, +) -> list[FormationSegment]: + """ + Create formation segments for provided surface well picks and well trajectory. + + This function assumes that the provided picks are already handled to only include those + relevant for the formation of interest and provided well trajectory (i.e., well picks + from top and bottom surfaces). + + **Description:** + - The formation is defined by a top surface and an optional bottom surface. + + - The segments are created based on the provided top and bottom surface picks. If bottom surface + picks are not provided, the formation is considered to extend to the end of the well + trajectory. + + - Each segment is defined by the measured depth (md) values where the well enters and exits + the formation. If a well starts or ends inside the formation, the corresponding md value is + taken from the start or end of the well trajectory, respectively. + + - The function calculates well picks for a surface using xtgeo, i.e. intersection points of + the well trajectory with provided surface, and determines the segments of the well trajectory + that lie within the formation defined by these surfaces. + + - If outside the formation, the next crossing should be a segment entry into the formation by + top pick downward or bottom pick upward. + + - If inside the formation, the next crossing should be a segment exit from the formation by top + pick upward or bottom pick downward. + + The function uses pick directions and measured depths to correctly handle: + - Wells entering from above (through top) or below (through bottom) + - Wells starting inside the formation + - Wells crossing the same surface multiple times (e.g., horizontal wells through folded + formations) + - Unexpected pick sequences are logged as warnings: + - Consecutive entry picks without an exit + - Consecutive exit picks without a prior entry + + Args: + well_trajectory (WellTrajectory): The well trajectory containing x, y, z, and md values. + top_surface_picks (list[SurfaceWellPick]): The top surface well picks relevant for the + formation. + bottom_surface_picks (list[SurfaceWellPick]): The optional bottom surface well picks relevant + for the formation. + Returns: + WellTrajectoryFormationSegments: The segments where the well trajectory is within the formation. + With measured depth values at entry and exit. + """ + + # Combine and categorize picks + categorized_picks: list[CategorizedPick] = [] + if top_surface_picks: + categorized_picks.extend(CategorizedPick(pick=pick, surface_type="top") for pick in top_surface_picks) + if bottom_surface_picks: + categorized_picks.extend(CategorizedPick(pick=pick, surface_type="bottom") for pick in bottom_surface_picks) + + if not categorized_picks: + return [] + + # Sort picks by measured depth + categorized_picks.sort(key=lambda pt: pt.pick.md if pt.pick.md is not None else float("inf")) + + formation_segments = [] + md_enter = None + + # Handle if well starts inside formation based on first pick + # Well started inside if: + # - Exit up: top + upward + # - Exit down: bottom + downward + (first_pick, first_surface_type) = (categorized_picks[0].pick, categorized_picks[0].surface_type) + is_exiting_up = first_surface_type == "top" and first_pick.direction == PickDirection.UPWARD + is_exiting_down = first_surface_type == "bottom" and first_pick.direction == PickDirection.DOWNWARD + if is_exiting_up or is_exiting_down: + md_enter = well_trajectory.md_points[0] + + # Build formation segments based on picks + for elm in categorized_picks: + pick = elm.pick + surface_type = elm.surface_type + + # Determine if this pick represents entering or exiting the formation + is_entering = False + is_exiting = False + + if surface_type == "top": + # Top pick with DOWNWARD = entering formation (from above) + # Top pick with UPWARD = exiting formation (to above) + is_entering = pick.direction == PickDirection.DOWNWARD + is_exiting = pick.direction == PickDirection.UPWARD + else: # bottom + # Bottom pick with UPWARD = entering formation (from below) + # Bottom pick with DOWNWARD = exiting formation (to below) + is_entering = pick.direction == PickDirection.UPWARD + is_exiting = pick.direction == PickDirection.DOWNWARD + + if is_entering: + if md_enter is not None: + # This shouldn't happen - two consecutive entries without an exit + message = ( + f"Unexpected consecutive entry picks for well {well_trajectory.unique_wellbore_identifier} at MD {pick.md}. " + "This may indicate data quality issues with the surface picks." + ) + LOGGER.warning(message) + raise InvalidDataError(message, Service.GENERAL) + + md_enter = pick.md + elif is_exiting: + if md_enter is None: + # This shouldn't happen - exit without entry (unless we handled it as first pick) + message = ( + f"Unexpected exit pick without entry for well {well_trajectory.unique_wellbore_identifier} at MD {pick.md}. " + "This may indicate data quality issues with the surface picks." + ) + LOGGER.warning(message) + raise InvalidDataError(message, Service.GENERAL) + else: + formation_segments.append(FormationSegment(md_enter=md_enter, md_exit=pick.md)) + md_enter = None + + # If md_enter is still set, well ends inside formation + if md_enter is not None: + formation_segments.append(FormationSegment(md_enter=md_enter, md_exit=well_trajectory.md_points[-1])) + + return formation_segments diff --git a/backend_py/primary/primary/routers/surface/converters.py b/backend_py/primary/primary/routers/surface/converters.py index 029bb0eea2..aa2e314fec 100644 --- a/backend_py/primary/primary/routers/surface/converters.py +++ b/backend_py/primary/primary/routers/surface/converters.py @@ -9,9 +9,15 @@ from webviz_services.sumo_access.surface_types import SurfaceMetaSet from webviz_services.utils.surface_intersect_with_polyline import XtgeoSurfaceIntersectionPolyline from webviz_services.utils.surface_intersect_with_polyline import XtgeoSurfaceIntersectionResult -from webviz_services.utils.surface_helpers import surface_to_float32_numpy_array, get_min_max_surface_values +from webviz_services.utils.surface_helpers import ( + surface_to_float32_numpy_array, + get_min_max_surface_values, + SurfaceWellPick, + WellTrajectory, +) from webviz_services.utils.surface_to_png import surface_to_png_bytes_optimized from webviz_services.smda_access import StratigraphicUnit +from webviz_services.utils.surfaces_well_trajectory_formation_segments import WellTrajectoryFormationSegments from . import schemas @@ -202,3 +208,55 @@ def to_api_stratigraphic_unit( colorB=stratigraphic_unit.color_b, lithologyType=stratigraphic_unit.lithology_type, ) + + +def from_api_well_trajectory( + api_well_trajectory: schemas.WellTrajectory, +) -> WellTrajectory: + """ + Convert API well trajectory to service layer well trajectory + """ + return WellTrajectory( + x_points=api_well_trajectory.xPoints, + y_points=api_well_trajectory.yPoints, + z_points=api_well_trajectory.zPoints, + md_points=api_well_trajectory.mdPoints, + unique_wellbore_identifier=api_well_trajectory.uwi, + ) + + +def to_api_well_trajectory_formation_segments( + well_trajectory_formation_segments: WellTrajectoryFormationSegments, +) -> schemas.WellTrajectoryFormationSegments: + """ + Convert service layer well trajectory formation segments to API well trajectory + formation segments + """ + return schemas.WellTrajectoryFormationSegments( + uwi=well_trajectory_formation_segments.unique_wellbore_identifier, + formationSegments=[ + schemas.FormationSegment(mdEnter=fs.md_enter, mdExit=fs.md_exit) + for fs in well_trajectory_formation_segments.formation_segments + ], + ) + + +def to_api_surface_well_picks( + surface_well_picks: list[SurfaceWellPick], +) -> schemas.SurfaceWellPicks: + """ + Convert list of service layer surface well pick to list of API surface well pick + """ + + return schemas.SurfaceWellPicks( + picks=[ + schemas.SurfaceWellPick( + x=pick.x, + y=pick.y, + z=pick.z, + md=pick.md, + direction=schemas.PickDirection(pick.direction.value), + ) + for pick in surface_well_picks + ] + ) diff --git a/backend_py/primary/primary/routers/surface/router.py b/backend_py/primary/primary/routers/surface/router.py index 1bf791e5fe..7bfbde5fbe 100644 --- a/backend_py/primary/primary/routers/surface/router.py +++ b/backend_py/primary/primary/routers/surface/router.py @@ -20,6 +20,8 @@ from webviz_services.surface_query_service.surface_query_service import batch_sample_surface_in_points_async from webviz_services.surface_query_service.surface_query_service import RealizationSampleResult from webviz_services.service_exceptions import ServiceLayerException +from webviz_services.utils.surfaces_well_trajectory_formation_segments import create_well_trajectory_formation_segments +from webviz_services.utils.surface_helpers import get_surface_picks_for_well_trajectory_from_xtgeo from primary.auth.auth_helper import AuthHelper from primary.utils.response_perf_metrics import ResponsePerfMetrics @@ -169,38 +171,9 @@ async def get_surface_data( if not isinstance(addr, RealizationSurfaceAddress | ObservedSurfaceAddress | StatisticalSurfaceAddress): raise HTTPException(status_code=404, detail="Endpoint only supports address types REAL, OBS and STAT") - xtgeo_surf: xtgeo.RegularSurface | None = None - if addr.address_type == "REAL": - access = SurfaceAccess.from_ensemble_name(access_token, addr.case_uuid, addr.ensemble_name) - xtgeo_surf = await access.get_realization_surface_data_async( - real_num=addr.realization, - name=addr.name, - attribute=addr.attribute, - time_or_interval_str=addr.iso_time_or_interval, - ) - perf_metrics.record_lap("get-surf") - - elif addr.address_type == "STAT": - service_stat_func_to_compute = StatisticFunction.from_string_value(addr.stat_function) - if service_stat_func_to_compute is None: - raise HTTPException(status_code=404, detail="Invalid statistic requested") - - access = SurfaceAccess.from_ensemble_name(access_token, addr.case_uuid, addr.ensemble_name) - xtgeo_surf = await access.get_statistical_surface_data_async( - statistic_function=service_stat_func_to_compute, - name=addr.name, - attribute=addr.attribute, - realizations=addr.stat_realizations, - time_or_interval_str=addr.iso_time_or_interval, - ) - perf_metrics.record_lap("sumo-calc") - - elif addr.address_type == "OBS": - access = SurfaceAccess.from_case_uuid_no_ensemble(access_token, addr.case_uuid) - xtgeo_surf = await access.get_observed_surface_data_async( - name=addr.name, attribute=addr.attribute, time_or_interval_str=addr.iso_time_or_interval - ) - perf_metrics.record_lap("get-surf") + xtgeo_surf = await _get_xtgeo_surface_from_sumo_async( + access_token=access_token, surf_addr_str=surf_addr_str, perf_metrics=perf_metrics + ) if not xtgeo_surf: raise HTTPException(status_code=500, detail="Did not get a valid xtgeo surface from Sumo") @@ -214,6 +187,134 @@ async def get_surface_data( return surf_data_response +@router.post("/get_well_trajectory_picks_per_surface") +async def post_get_well_trajectory_picks_per_surface( + response: Response, + authenticated_user: Annotated[AuthenticatedUser, Depends(AuthHelper.get_authenticated_user)], + well_trajectory: Annotated[schemas.WellTrajectory, Body(embed=True)], + depth_surface_addr_str_list: Annotated[ + list[str], + Query( + description="List of surface address strings for depth surfaces. Supported address types are *REAL*, *OBS* and *STAT*" + ), + ], +) -> list[schemas.SurfaceWellPicks]: + """ + Get surface picks along a well trajectory for multiple depth surfaces. + + For each provided depth surface address, the intersections (picks) between the surface and the + well trajectory are calculated and returned. + + Returns a list of surface picks per depth surface, in the same order as the provided list of + depth surface address strings. + """ + if not depth_surface_addr_str_list: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="At least one depth surface address string must be provided", + ) + + perf_metrics = ResponsePerfMetrics(response) + access_token = authenticated_user.get_sumo_access_token() + + try: + async with asyncio.TaskGroup() as tg: + xtgeo_surface_tasks = [ + tg.create_task(_get_xtgeo_surface_from_sumo_async(access_token, surf_addr_str, perf_metrics)) + for surf_addr_str in depth_surface_addr_str_list + ] + + xtgeo_surfaces = [task.result() for task in xtgeo_surface_tasks] + except* ServiceLayerException as exc_group: + for exc in exc_group.exceptions: + raise exc from exc_group # Reraise the first exception + + perf_metrics.record_lap("get-surfaces") + + well_traj = converters.from_api_well_trajectory(well_trajectory) + well_picks_per_surface = [] + for xtgeo_surf in xtgeo_surfaces: + surface_picks = get_surface_picks_for_well_trajectory_from_xtgeo( + surf=xtgeo_surf, + well_trajectory=well_traj, + ) + + valid_picks = surface_picks if surface_picks is not None else [] + well_picks_per_surface.append(valid_picks) + perf_metrics.record_lap("sample-picks") + + result = [converters.to_api_surface_well_picks(surface_picks) for surface_picks in well_picks_per_surface] + + LOGGER.info(f"Got well trajectory surface picks in: {perf_metrics.to_string()}") + return result + + +@router.post("/get_well_trajectories_formation_segments") +async def post_get_well_trajectories_formation_segments( + response: Response, + authenticated_user: Annotated[AuthenticatedUser, Depends(AuthHelper.get_authenticated_user)], + well_trajectories: Annotated[list[schemas.WellTrajectory], Body(embed=True)], + top_depth_surf_addr_str: Annotated[ + str, + Query( + description="Surface address string for top bounding depth surface. Supported address types are *REAL*, *OBS* and *STAT*" + ), + ], + bottom_depth_surf_addr_str: Annotated[ + str | None, + Query( + description="Optional surface address string for bottom bounding depth surface. If not provided end of well trajectory" + " is used as lower bound for formation. Supported address types are *REAL*, *OBS* and *STAT*" + ), + ] = None, +) -> list[schemas.WellTrajectoryFormationSegments]: + """ + Get well trajectory formation segments. + + Provide a top bounding depth surface and an optional bottom bounding depth surface to define a + formation (area between two surfaces in depth). If bottom surface is not provided, the formation + is considered to extend down to the end of the well trajectory, i.e. end of well trajectory is + used as lower bound for formation. + + For each well trajectory, the segments where the well is within the formation are calculated and + returned. Each segment contains the measured depth (md) values where the well enters and exits + the formation. + + NOTE: Expecting depth surfaces, no verification is done to ensure that the surfaces are indeed + depth surfaces. + + """ + perf_metrics = ResponsePerfMetrics(response) + access_token = authenticated_user.get_sumo_access_token() + + top_xtgeo_surf = await _get_xtgeo_surface_from_sumo_async( + access_token=access_token, surf_addr_str=top_depth_surf_addr_str, perf_metrics=perf_metrics + ) + perf_metrics.record_lap("get-top-surf") + + bottom_xtgeo_surf = None + if bottom_depth_surf_addr_str: + bottom_xtgeo_surf = await _get_xtgeo_surface_from_sumo_async( + access_token=access_token, surf_addr_str=bottom_depth_surf_addr_str, perf_metrics=perf_metrics + ) + perf_metrics.record_lap("get-bottom-surf") + + per_well_trajectory_formation_segments = [] + for well in well_trajectories: + formation_segments = create_well_trajectory_formation_segments( + well_trajectory=converters.from_api_well_trajectory(well), + top_depth_surface=top_xtgeo_surf, + bottom_depth_surface=bottom_xtgeo_surf, + ) + per_well_trajectory_formation_segments.append( + converters.to_api_well_trajectory_formation_segments(formation_segments) + ) + + perf_metrics.record_lap("Create segments for all wells") + LOGGER.info(f"Got well trajectory formation segments in: {perf_metrics.to_string()}") + return per_well_trajectory_formation_segments + + @router.get("/statistical_surface_data/hybrid") async def get_statistical_surface_data_hybrid( # fmt:off @@ -472,3 +573,53 @@ def _resample_and_convert_to_surface_data_response( perf_metrics.record_lap("convert") return surf_data_response + + +async def _get_xtgeo_surface_from_sumo_async( + access_token: str, + surf_addr_str: str, + perf_metrics: ResponsePerfMetrics, +) -> xtgeo.RegularSurface: + """ + Retrieve an xtgeo RegularSurface from SUMO based on the provided surface address string. + """ + + addr = decode_surf_addr_str(surf_addr_str) + if not isinstance(addr, RealizationSurfaceAddress | ObservedSurfaceAddress | StatisticalSurfaceAddress): + raise HTTPException(status_code=404, detail="Endpoint only supports address types REAL, OBS and STAT") + + xtgeo_surf: xtgeo.RegularSurface | None = None + if addr.address_type == "REAL": + access = SurfaceAccess.from_ensemble_name(access_token, addr.case_uuid, addr.ensemble_name) + xtgeo_surf = await access.get_realization_surface_data_async( + real_num=addr.realization, + name=addr.name, + attribute=addr.attribute, + time_or_interval_str=addr.iso_time_or_interval, + ) + perf_metrics.record_lap("get-surf") + + elif addr.address_type == "STAT": + service_stat_func_to_compute = StatisticFunction.from_string_value(addr.stat_function) + if service_stat_func_to_compute is None: + raise HTTPException(status_code=404, detail="Invalid statistic requested") + + access = SurfaceAccess.from_ensemble_name(access_token, addr.case_uuid, addr.ensemble_name) + xtgeo_surf = await access.get_statistical_surface_data_async( + statistic_function=service_stat_func_to_compute, + name=addr.name, + attribute=addr.attribute, + realizations=addr.stat_realizations, + time_or_interval_str=addr.iso_time_or_interval, + ) + perf_metrics.record_lap("sumo-calc") + + elif addr.address_type == "OBS": + access = SurfaceAccess.from_case_uuid_no_ensemble(access_token, addr.case_uuid) + xtgeo_surf = await access.get_observed_surface_data_async( + name=addr.name, attribute=addr.attribute, time_or_interval_str=addr.iso_time_or_interval + ) + perf_metrics.record_lap("get-surf") + LOGGER.info(f"Got {addr.address_type} surface in: {perf_metrics.to_string()}") + + return xtgeo_surf diff --git a/backend_py/primary/primary/routers/surface/schemas.py b/backend_py/primary/primary/routers/surface/schemas.py index a6db9db87d..1eb6c3c1b4 100644 --- a/backend_py/primary/primary/routers/surface/schemas.py +++ b/backend_py/primary/primary/routers/surface/schemas.py @@ -1,4 +1,4 @@ -from enum import Enum +from enum import Enum, StrEnum from typing import List, Literal from pydantic import BaseModel, ConfigDict @@ -166,6 +166,79 @@ class PointSetXY(BaseModel): y_points: list[float] +class PickDirection(StrEnum): + """Direction of the pick relative to the surface""" + + UPWARD = "UPWARD" + DOWNWARD = "DOWNWARD" + + +class SurfaceWellPick(BaseModel): + """Surface pick data along a well trajectory + + md: Measured depth value at the pick point. + direction: Direction of the pick relative to the surface. + """ + + md: float + direction: PickDirection + + +class SurfaceWellPicks(BaseModel): + """Surface picks along a well trajectory for a specific surface. + + Each pick contains the measured depth and direction. + + """ + + picks: List[SurfaceWellPick] + + +class FormationSegment(BaseModel): + """ + Segment of a formation defined by top and bottom surface. + + The formation segment is defined by the md (measured depth) value along the well trajectory, + at enter and exit of the formation. + """ + + mdEnter: float + mdExit: float + + +class WellTrajectoryFormationSegments(BaseModel): + """ + Segments of a well trajectory that intersects a formation defined by top and bottom surfaces. + + A wellbore can enter and exit a formation multiple times, resulting in multiple segments. + + uniqueWellboreIdentifier: str + formationSegments: List[FormationSegment] + """ + + uwi: str + formationSegments: List[FormationSegment] + + +class WellTrajectory(BaseModel): + """ + Well trajectory defined by a set of (x, y, z) coordinates and measured depths (md). + + uwi: Unique wellbore identifier. + xPoints: X-coordinates of well trajectory points. + yPoints: Y-coordinates of well trajectory points. + zPoints: Z-coordinates (depth values) of well trajectory points. + mdPoints: Measured depth values at each well trajectory point. + + """ + + xPoints: List[float] + yPoints: List[float] + zPoints: List[float] + mdPoints: List[float] + uwi: str + + class StratigraphicUnit(BaseModel): """ Stratigraphic unit from SMDA diff --git a/backend_py/primary/primary/routers/well/converters.py b/backend_py/primary/primary/routers/well/converters.py index 379e7cd701..51120e8ac3 100644 --- a/backend_py/primary/primary/routers/well/converters.py +++ b/backend_py/primary/primary/routers/well/converters.py @@ -69,8 +69,12 @@ def convert_wellbore_header_to_schema( kickoffDepthMd=drilled_wellbore_header.kickoff_depth_md, kickoffDepthTvd=drilled_wellbore_header.kickoff_depth_tvd, parentWellbore=drilled_wellbore_header.parent_wellbore, - tvdMax=drilled_wellbore_header.tvd_max, + mdMin=drilled_wellbore_header.md_min, mdMax=drilled_wellbore_header.md_max, + mdUnit=drilled_wellbore_header.md_unit, + tvdMin=drilled_wellbore_header.tvd_min, + tvdMax=drilled_wellbore_header.tvd_max, + tvdUnit=drilled_wellbore_header.tvd_unit, ) diff --git a/backend_py/primary/primary/routers/well/schemas.py b/backend_py/primary/primary/routers/well/schemas.py index 7d9f89bc6b..7b038044e6 100644 --- a/backend_py/primary/primary/routers/well/schemas.py +++ b/backend_py/primary/primary/routers/well/schemas.py @@ -15,11 +15,15 @@ class WellboreHeader(BaseModel): wellborePurpose: str wellboreStatus: str currentTrack: int - tvdMax: float - mdMax: float kickoffDepthMd: float | None kickoffDepthTvd: float | None parentWellbore: str | None + mdMin: float | None = None + mdMax: float | None = None + mdUnit: str | None = None + tvdMin: float | None = None + tvdMax: float | None = None + tvdUnit: str | None = None class WellboreTrajectory(BaseModel): diff --git a/backend_py/primary/tests/unit/services/utils/test_surfaces_well_trajectory_formation_segments.py b/backend_py/primary/tests/unit/services/utils/test_surfaces_well_trajectory_formation_segments.py new file mode 100644 index 0000000000..d91ca7afe1 --- /dev/null +++ b/backend_py/primary/tests/unit/services/utils/test_surfaces_well_trajectory_formation_segments.py @@ -0,0 +1,410 @@ +import pytest + + +from webviz_services.utils.surfaces_well_trajectory_formation_segments import ( + _create_formation_segments_from_well_trajectory_and_picks, + WellTrajectory, +) +from webviz_services.utils.surface_helpers import PickDirection, SurfaceWellPick +from webviz_services.service_exceptions import InvalidDataError, Service + + +def create_well_trajectory(md_points: list[float]) -> WellTrajectory: + """Helper to create a simple vertical well trajectory for testing.""" + return WellTrajectory( + unique_wellbore_identifier="test-well", + x_points=[100.0] * len(md_points), + y_points=[200.0] * len(md_points), + z_points=md_points, # For simplicity, z equals md + md_points=md_points, + ) + + +def create_pick(md: float, direction: PickDirection) -> SurfaceWellPick: + """Helper to create a surface well pick.""" + return SurfaceWellPick( + unique_wellbore_identifier="test-well", + x=100.0, + y=200.0, + z=md, + md=md, + direction=direction, + ) + + +def test_well_enters_top_downward_and_exits_top_upward() -> None: + """Test well entering formation from above (top downward) and exiting upward (top upward).""" + trajectory = create_well_trajectory([0.0, 100.0, 200.0, 300.0, 400.0]) + top_picks = [ + create_pick(100.0, PickDirection.DOWNWARD), # Enter at 100 + create_pick(300.0, PickDirection.UPWARD), # Exit at 300 + ] + + segments = _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=None + ) + + assert len(segments) == 1 + assert segments[0].md_enter == 100.0 + assert segments[0].md_exit == 300.0 + + +def test_well_starts_inside_formation_top_upward() -> None: + """Test well starting inside formation, first pick is top upward (exiting).""" + trajectory = create_well_trajectory([0.0, 100.0, 200.0, 300.0, 400.0]) + top_picks = [ + create_pick(200.0, PickDirection.UPWARD), # Exit at 200 (started inside) + ] + + segments = _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=None + ) + + assert len(segments) == 1 + assert segments[0].md_enter == 0.0 # Started at well start + assert segments[0].md_exit == 200.0 + + +def test_well_enters_and_ends_inside_formation() -> None: + """Test well entering formation and ending inside (no exit pick).""" + trajectory = create_well_trajectory([0.0, 100.0, 200.0, 300.0, 400.0]) + top_picks = [ + create_pick(100.0, PickDirection.DOWNWARD), # Enter at 100 + # No exit pick - well ends inside + ] + + segments = _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=None + ) + + assert len(segments) == 1 + assert segments[0].md_enter == 100.0 + assert segments[0].md_exit == 400.0 # Ends at well end + + +def test_well_multiple_entries_and_exits() -> None: + """Test well with multiple entries and exits (e.g., horizontal well through folded formation).""" + trajectory = create_well_trajectory([0.0, 100.0, 200.0, 300.0, 400.0, 500.0, 600.0]) + top_picks = [ + create_pick(100.0, PickDirection.DOWNWARD), # Enter 1 + create_pick(200.0, PickDirection.UPWARD), # Exit 1 + create_pick(300.0, PickDirection.DOWNWARD), # Enter 2 + create_pick(400.0, PickDirection.UPWARD), # Exit 2 + ] + + segments = _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=None + ) + + assert len(segments) == 2 + assert segments[0].md_enter == 100.0 + assert segments[0].md_exit == 200.0 + assert segments[1].md_enter == 300.0 + assert segments[1].md_exit == 400.0 + + +def test_well_with_bottom_surface_enter_from_below() -> None: + """Test well entering from below (bottom upward) and exiting through bottom (bottom downward).""" + trajectory = create_well_trajectory([0.0, 100.0, 200.0, 300.0, 400.0]) + top_picks = [] + bottom_picks = [ + create_pick(100.0, PickDirection.UPWARD), # Enter from below at 100 + create_pick(300.0, PickDirection.DOWNWARD), # Exit through bottom at 300 + ] + + segments = _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=bottom_picks + ) + + assert len(segments) == 1 + assert segments[0].md_enter == 100.0 + assert segments[0].md_exit == 300.0 + + +def test_well_starts_inside_with_bottom_downward() -> None: + """Test well starting inside formation, first pick is bottom downward (exiting down).""" + trajectory = create_well_trajectory([0.0, 100.0, 200.0, 300.0, 400.0]) + top_picks = [] + bottom_picks = [ + create_pick(200.0, PickDirection.DOWNWARD), # Exit down at 200 (started inside) + ] + + segments = _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=bottom_picks + ) + + assert len(segments) == 1 + assert segments[0].md_enter == 0.0 # Started at well start + assert segments[0].md_exit == 200.0 + + +def test_well_with_both_top_and_bottom_surfaces() -> None: + """Test well with both top and bottom surfaces, entering from top and exiting through bottom.""" + trajectory = create_well_trajectory([0.0, 100.0, 200.0, 300.0, 400.0]) + top_picks = [ + create_pick(100.0, PickDirection.DOWNWARD), # Enter through top at 100 + ] + bottom_picks = [ + create_pick(300.0, PickDirection.DOWNWARD), # Exit through bottom at 300 + ] + + segments = _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=bottom_picks + ) + + assert len(segments) == 1 + assert segments[0].md_enter == 100.0 + assert segments[0].md_exit == 300.0 + + +def test_well_mixed_top_and_bottom_picks_raises_on_consecutive_entry() -> None: + """Test well with consecutive entry picks raises InvalidDataError.""" + trajectory = create_well_trajectory([0.0, 100.0, 200.0, 300.0, 400.0, 500.0]) + top_picks = [ + create_pick(100.0, PickDirection.DOWNWARD), # Enter through top at 100 + create_pick(400.0, PickDirection.DOWNWARD), # Enter through top at 400 (consecutive entry!) + ] + bottom_picks = [ + create_pick(200.0, PickDirection.DOWNWARD), # Exit through bottom at 200 + create_pick(450.0, PickDirection.UPWARD), # Enter from below at 450 (another consecutive entry!) + ] + + with pytest.raises(InvalidDataError) as exc_info: + _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=bottom_picks + ) + + assert ( + exc_info.value.message + == f"Unexpected consecutive entry picks for well {trajectory.unique_wellbore_identifier} at MD 450.0. This may indicate data quality issues with the surface picks." + ) + assert exc_info.value.service == Service.GENERAL + + +def test_well_mixed_top_and_bottom_picks_raises_on_consecutive_exits() -> None: + """Test well with mixed picks that creates consecutive exits raises InvalidDataError.""" + trajectory = create_well_trajectory([0.0, 100.0, 200.0, 300.0, 400.0, 500.0]) + top_picks = [ + create_pick(100.0, PickDirection.DOWNWARD), # Enter through top at 100 + create_pick(300.0, PickDirection.UPWARD), # Exit through top at 300 (consecutive exit!) + ] + bottom_picks = [ + create_pick(200.0, PickDirection.DOWNWARD), # Exit through bottom at 200 + ] + + # Sorted by MD: 100 (enter), 200 (exit), 300 (exit) - two consecutive exits! + with pytest.raises(InvalidDataError) as exc_info: + _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=bottom_picks + ) + + assert ( + exc_info.value.message + == f"Unexpected exit pick without entry for well {trajectory.unique_wellbore_identifier} at MD 300.0. This may indicate data quality issues with the surface picks." + ) + assert exc_info.value.service == Service.GENERAL + + +def test_well_mixed_top_and_bottom_picks_valid() -> None: + """Test well with valid mixed top and bottom picks creating a single segment.""" + trajectory = create_well_trajectory([0.0, 100.0, 200.0, 300.0, 400.0, 500.0]) + top_picks = [ + create_pick(100.0, PickDirection.DOWNWARD), # Enter through top at 100 + ] + bottom_picks = [ + create_pick(300.0, PickDirection.DOWNWARD), # Exit through bottom at 300 + ] + + segments = _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=bottom_picks + ) + + assert len(segments) == 1 + assert segments[0].md_enter == 100.0 + assert segments[0].md_exit == 300.0 + + +def test_well_starts_inside_ends_inside() -> None: + """Test well starting inside and ending inside (first pick is exit, no further picks).""" + trajectory = create_well_trajectory([0.0, 100.0, 200.0, 300.0, 400.0]) + top_picks = [ + create_pick(100.0, PickDirection.UPWARD), # Exit up at 100 (started inside) + create_pick(200.0, PickDirection.DOWNWARD), # Re-enter at 200 + # No exit - ends inside + ] + + segments = _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=None + ) + + assert len(segments) == 2 + assert segments[0].md_enter == 0.0 + assert segments[0].md_exit == 100.0 + assert segments[1].md_enter == 200.0 + assert segments[1].md_exit == 400.0 + + +def test_well_no_intersection() -> None: + """Test well with no picks (doesn't intersect formation).""" + trajectory = create_well_trajectory([0.0, 100.0, 200.0, 300.0, 400.0]) + top_picks = [] + bottom_picks = [] + + segments = _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=bottom_picks + ) + + assert len(segments) == 0 + + +def test_well_only_top_downward_no_bottom_surface() -> None: + """Test well entering through top with no bottom surface - formation extends to well end.""" + trajectory = create_well_trajectory([0.0, 100.0, 200.0, 300.0, 400.0]) + top_picks = [ + create_pick(150.0, PickDirection.DOWNWARD), # Enter at 150, no exit + ] + + segments = _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=None + ) + + assert len(segments) == 1 + assert segments[0].md_enter == 150.0 + assert segments[0].md_exit == 400.0 # Extends to end of trajectory + + +def test_consecutive_entry_picks_raises_exception() -> None: + """Test consecutive entry picks raises InvalidDataError for data quality issue.""" + trajectory = create_well_trajectory([0.0, 100.0, 200.0, 300.0, 400.0]) + top_picks = [ + create_pick(100.0, PickDirection.DOWNWARD), # Enter 1 + create_pick(150.0, PickDirection.DOWNWARD), # Enter 2 (consecutive entry!) + create_pick(300.0, PickDirection.UPWARD), # Exit + ] + + with pytest.raises(InvalidDataError) as exc_info: + _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=None + ) + + assert ( + exc_info.value.message + == f"Unexpected consecutive entry picks for well {trajectory.unique_wellbore_identifier} at MD 150.0. This may indicate data quality issues with the surface picks." + ) + assert exc_info.value.service == Service.GENERAL + + +def test_consecutive_exit_picks_raises_exception() -> None: + """Test consecutive exit picks without prior entry raises InvalidDataError for data quality issue.""" + trajectory = create_well_trajectory([0.0, 100.0, 200.0, 300.0, 400.0]) + top_picks = [ + create_pick(100.0, PickDirection.DOWNWARD), # Enter + create_pick(200.0, PickDirection.UPWARD), # Exit 1 + create_pick(250.0, PickDirection.UPWARD), # Exit 2 (consecutive exit without entry!) + ] + + with pytest.raises(InvalidDataError) as exc_info: + _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=None + ) + + assert ( + exc_info.value.message + == f"Unexpected exit pick without entry for well {trajectory.unique_wellbore_identifier} at MD 250.0. This may indicate data quality issues with the surface picks." + ) + assert exc_info.value.service == Service.GENERAL + + +def test_well_picks_unsorted_by_md() -> None: + """Test that picks are correctly sorted by MD even if provided out of order.""" + trajectory = create_well_trajectory([0.0, 100.0, 200.0, 300.0, 400.0]) + # Provide picks out of order + top_picks = [ + create_pick(300.0, PickDirection.UPWARD), # Exit at 300 + create_pick(100.0, PickDirection.DOWNWARD), # Enter at 100 + ] + + segments = _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=None + ) + + assert len(segments) == 1 + assert segments[0].md_enter == 100.0 + assert segments[0].md_exit == 300.0 + + +def test_complex_scenario_raises_on_consecutive_entries() -> None: + """Test complex scenario with consecutive entries raises InvalidDataError.""" + trajectory = create_well_trajectory([0.0, 50.0, 100.0, 150.0, 200.0, 250.0, 300.0, 350.0, 400.0]) + top_picks = [ + create_pick(100.0, PickDirection.DOWNWARD), # Enter through top at 100 + create_pick(250.0, PickDirection.UPWARD), # Exit through top at 250 + ] + bottom_picks = [ + create_pick(200.0, PickDirection.UPWARD), # Enter from below at 200 (consecutive entry!) + ] + + # Sorted by MD: 100 (enter), 200 (enter), 250 (exit) - two consecutive entries! + with pytest.raises(InvalidDataError) as exc_info: + _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=bottom_picks + ) + + assert ( + exc_info.value.message + == f"Unexpected consecutive entry picks for well {trajectory.unique_wellbore_identifier} at MD 200.0. This may indicate data quality issues with the surface picks." + ) + assert exc_info.value.service == Service.GENERAL + + +def test_complex_scenario_with_multiple_surfaces_valid() -> None: + """Test valid complex scenario with multiple crossings of both top and bottom surfaces.""" + trajectory = create_well_trajectory([0.0, 50.0, 100.0, 150.0, 200.0, 250.0, 300.0, 350.0, 400.0]) + top_picks = [ + create_pick(100.0, PickDirection.DOWNWARD), # Enter through top at 100 + create_pick(300.0, PickDirection.DOWNWARD), # Re-enter through top at 300 + ] + bottom_picks = [ + create_pick(200.0, PickDirection.DOWNWARD), # Exit through bottom at 200 + create_pick(350.0, PickDirection.DOWNWARD), # Exit through bottom at 350 + ] + + segments = _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=bottom_picks + ) + + # Should create two segments: 100-200 and 300-350 + assert len(segments) == 2 + assert segments[0].md_enter == 100.0 + assert segments[0].md_exit == 200.0 + assert segments[1].md_enter == 300.0 + assert segments[1].md_exit == 350.0 + + +def test_single_top_pick_downward_extends_to_end() -> None: + """Test single top downward pick with no bottom surface - formation extends to trajectory end.""" + trajectory = create_well_trajectory([0.0, 100.0, 200.0, 300.0]) + top_picks = [ + create_pick(100.0, PickDirection.DOWNWARD), + ] + + segments = _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=None + ) + + assert len(segments) == 1 + assert segments[0].md_enter == 100.0 + assert segments[0].md_exit == 300.0 + + +def test_well_entirely_inside_formation() -> None: + """Test well that starts and ends inside formation (no picks).""" + trajectory = create_well_trajectory([0.0, 100.0, 200.0, 300.0]) + top_picks = [] # No picks means well doesn't cross surfaces + + segments = _create_formation_segments_from_well_trajectory_and_picks( + well_trajectory=trajectory, top_surface_picks=top_picks, bottom_surface_picks=None + ) + + # With no picks, we can't determine if well is inside - returns empty + assert len(segments) == 0 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 760dcf9504..9dcad38224 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,8 @@ "name": "webviz", "version": "0.0.0", "dependencies": { + "@deck.gl/core": "^9.1.15", + "@deck.gl/layers": "^9.1.15", "@equinor/eds-core-react": "~0.48.0", "@equinor/esv-intersection": "^3.1.4", "@mui/base": "^5.0.0-beta.3", diff --git a/frontend/package.json b/frontend/package.json index 8614072dd3..df80e5ef3a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,6 +33,8 @@ "ts-key-enum v2 and v3 are the same, expect that v3 exports it as const enum. We cannot use v3 since we compile typescript with isolatedModules=true" ], "dependencies": { + "@deck.gl/core": "^9.1.15", + "@deck.gl/layers": "^9.1.15", "@equinor/eds-core-react": "~0.48.0", "@equinor/esv-intersection": "^3.1.4", "@mui/base": "^5.0.0-beta.3", diff --git a/frontend/src/api/autogen/@tanstack/react-query.gen.ts b/frontend/src/api/autogen/@tanstack/react-query.gen.ts index 02162034c4..fadbba3f06 100644 --- a/frontend/src/api/autogen/@tanstack/react-query.gen.ts +++ b/frontend/src/api/autogen/@tanstack/react-query.gen.ts @@ -96,6 +96,8 @@ import { postGetSampleSurfaceInPoints, postGetSeismicFence, postGetSurfaceIntersection, + postGetWellTrajectoriesFormationSegments, + postGetWellTrajectoryPicksPerSurface, postLogout, postRefreshFingerprintsForEnsembles, root, @@ -211,6 +213,12 @@ import type { PostGetSurfaceIntersectionData_api, PostGetSurfaceIntersectionError_api, PostGetSurfaceIntersectionResponse_api, + PostGetWellTrajectoriesFormationSegmentsData_api, + PostGetWellTrajectoriesFormationSegmentsError_api, + PostGetWellTrajectoriesFormationSegmentsResponse_api, + PostGetWellTrajectoryPicksPerSurfaceData_api, + PostGetWellTrajectoryPicksPerSurfaceError_api, + PostGetWellTrajectoryPicksPerSurfaceResponse_api, PostLogoutData_api, PostLogoutResponse_api, PostRefreshFingerprintsForEnsemblesData_api, @@ -871,6 +879,152 @@ export const getSurfaceDataOptions = (options: Options) }); }; +export const postGetWellTrajectoryPicksPerSurfaceQueryKey = ( + options: Options, +) => createQueryKey("postGetWellTrajectoryPicksPerSurface", options); + +/** + * Post Get Well Trajectory Picks Per Surface + * + * Get surface picks along a well trajectory for multiple depth surfaces. + * + * For each provided depth surface address, the intersections (picks) between the surface and the + * well trajectory are calculated and returned. + * + * Returns a list of surface picks per depth surface, in the same order as the provided list of + * depth surface address strings. + */ +export const postGetWellTrajectoryPicksPerSurfaceOptions = ( + options: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await postGetWellTrajectoryPicksPerSurface({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: postGetWellTrajectoryPicksPerSurfaceQueryKey(options), + }); +}; + +/** + * Post Get Well Trajectory Picks Per Surface + * + * Get surface picks along a well trajectory for multiple depth surfaces. + * + * For each provided depth surface address, the intersections (picks) between the surface and the + * well trajectory are calculated and returned. + * + * Returns a list of surface picks per depth surface, in the same order as the provided list of + * depth surface address strings. + */ +export const postGetWellTrajectoryPicksPerSurfaceMutation = ( + options?: Partial>, +): UseMutationOptions< + PostGetWellTrajectoryPicksPerSurfaceResponse_api, + AxiosError, + Options +> => { + const mutationOptions: UseMutationOptions< + PostGetWellTrajectoryPicksPerSurfaceResponse_api, + AxiosError, + Options + > = { + mutationFn: async (fnOptions) => { + const { data } = await postGetWellTrajectoryPicksPerSurface({ + ...options, + ...fnOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; + +export const postGetWellTrajectoriesFormationSegmentsQueryKey = ( + options: Options, +) => createQueryKey("postGetWellTrajectoriesFormationSegments", options); + +/** + * Post Get Well Trajectories Formation Segments + * + * Get well trajectory formation segments. + * + * Provide a top bounding depth surface and an optional bottom bounding depth surface to define a + * formation (area between two surfaces in depth). If bottom surface is not provided, the formation + * is considered to extend down to the end of the well trajectory, i.e. end of well trajectory is + * used as lower bound for formation. + * + * For each well trajectory, the segments where the well is within the formation are calculated and + * returned. Each segment contains the measured depth (md) values where the well enters and exits + * the formation. + * + * NOTE: Expecting depth surfaces, no verification is done to ensure that the surfaces are indeed + * depth surfaces. + */ +export const postGetWellTrajectoriesFormationSegmentsOptions = ( + options: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await postGetWellTrajectoriesFormationSegments({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: postGetWellTrajectoriesFormationSegmentsQueryKey(options), + }); +}; + +/** + * Post Get Well Trajectories Formation Segments + * + * Get well trajectory formation segments. + * + * Provide a top bounding depth surface and an optional bottom bounding depth surface to define a + * formation (area between two surfaces in depth). If bottom surface is not provided, the formation + * is considered to extend down to the end of the well trajectory, i.e. end of well trajectory is + * used as lower bound for formation. + * + * For each well trajectory, the segments where the well is within the formation are calculated and + * returned. Each segment contains the measured depth (md) values where the well enters and exits + * the formation. + * + * NOTE: Expecting depth surfaces, no verification is done to ensure that the surfaces are indeed + * depth surfaces. + */ +export const postGetWellTrajectoriesFormationSegmentsMutation = ( + options?: Partial>, +): UseMutationOptions< + PostGetWellTrajectoriesFormationSegmentsResponse_api, + AxiosError, + Options +> => { + const mutationOptions: UseMutationOptions< + PostGetWellTrajectoriesFormationSegmentsResponse_api, + AxiosError, + Options + > = { + mutationFn: async (fnOptions) => { + const { data } = await postGetWellTrajectoriesFormationSegments({ + ...options, + ...fnOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; + export const getStatisticalSurfaceDataHybridQueryKey = (options: Options) => createQueryKey("getStatisticalSurfaceDataHybrid", options); diff --git a/frontend/src/api/autogen/sdk.gen.ts b/frontend/src/api/autogen/sdk.gen.ts index f7b5364f2b..797cb1f5fc 100644 --- a/frontend/src/api/autogen/sdk.gen.ts +++ b/frontend/src/api/autogen/sdk.gen.ts @@ -251,6 +251,12 @@ import type { PostGetSurfaceIntersectionData_api, PostGetSurfaceIntersectionErrors_api, PostGetSurfaceIntersectionResponses_api, + PostGetWellTrajectoriesFormationSegmentsData_api, + PostGetWellTrajectoriesFormationSegmentsErrors_api, + PostGetWellTrajectoriesFormationSegmentsResponses_api, + PostGetWellTrajectoryPicksPerSurfaceData_api, + PostGetWellTrajectoryPicksPerSurfaceErrors_api, + PostGetWellTrajectoryPicksPerSurfaceResponses_api, PostLogoutData_api, PostLogoutResponses_api, PostRefreshFingerprintsForEnsemblesData_api, @@ -685,6 +691,70 @@ export const getSurfaceData = ( }); }; +/** + * Post Get Well Trajectory Picks Per Surface + * + * Get surface picks along a well trajectory for multiple depth surfaces. + * + * For each provided depth surface address, the intersections (picks) between the surface and the + * well trajectory are calculated and returned. + * + * Returns a list of surface picks per depth surface, in the same order as the provided list of + * depth surface address strings. + */ +export const postGetWellTrajectoryPicksPerSurface = ( + options: Options, +) => { + return (options.client ?? client).post< + PostGetWellTrajectoryPicksPerSurfaceResponses_api, + PostGetWellTrajectoryPicksPerSurfaceErrors_api, + ThrowOnError + >({ + responseType: "json", + url: "/surface/get_well_trajectory_picks_per_surface", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }); +}; + +/** + * Post Get Well Trajectories Formation Segments + * + * Get well trajectory formation segments. + * + * Provide a top bounding depth surface and an optional bottom bounding depth surface to define a + * formation (area between two surfaces in depth). If bottom surface is not provided, the formation + * is considered to extend down to the end of the well trajectory, i.e. end of well trajectory is + * used as lower bound for formation. + * + * For each well trajectory, the segments where the well is within the formation are calculated and + * returned. Each segment contains the measured depth (md) values where the well enters and exits + * the formation. + * + * NOTE: Expecting depth surfaces, no verification is done to ensure that the surfaces are indeed + * depth surfaces. + */ +export const postGetWellTrajectoriesFormationSegments = ( + options: Options, +) => { + return (options.client ?? client).post< + PostGetWellTrajectoriesFormationSegmentsResponses_api, + PostGetWellTrajectoriesFormationSegmentsErrors_api, + ThrowOnError + >({ + responseType: "json", + url: "/surface/get_well_trajectories_formation_segments", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }); +}; + /** * Get Statistical Surface Data Hybrid */ diff --git a/frontend/src/api/autogen/types.gen.ts b/frontend/src/api/autogen/types.gen.ts index 72cab54609..ec89a65b8e 100644 --- a/frontend/src/api/autogen/types.gen.ts +++ b/frontend/src/api/autogen/types.gen.ts @@ -102,6 +102,23 @@ export type BodyPostGetSurfaceIntersection_api = { cumulative_length_polyline: SurfaceIntersectionCumulativeLengthPolyline_api; }; +/** + * Body_post_get_well_trajectories_formation_segments + */ +export type BodyPostGetWellTrajectoriesFormationSegments_api = { + /** + * Well Trajectories + */ + well_trajectories: Array; +}; + +/** + * Body_post_get_well_trajectory_picks_per_surface + */ +export type BodyPostGetWellTrajectoryPicksPerSurface_api = { + well_trajectory: WellTrajectory_api; +}; + /** * BoundingBox2d */ @@ -540,6 +557,25 @@ export enum FlowRateType_api { WAT = "WAT", } +/** + * FormationSegment + * + * Segment of a formation defined by top and bottom surface. + * + * The formation segment is defined by the md (measured depth) value along the well trajectory, + * at enter and exit of the formation. + */ +export type FormationSegment_api = { + /** + * Mdenter + */ + mdEnter: number; + /** + * Mdexit + */ + mdExit: number; +}; + /** * Frequency */ @@ -1071,6 +1107,16 @@ export type PageSnapshotMetadata_api = { pageToken?: string | null; }; +/** + * PickDirection + * + * Direction of the pick relative to the surface + */ +export enum PickDirection_api { + UPWARD = "UPWARD", + DOWNWARD = "DOWNWARD", +} + /** * PointSetXY */ @@ -2232,6 +2278,36 @@ export enum SurfaceTimeType_api { INTERVAL = "INTERVAL", } +/** + * SurfaceWellPick + * + * Surface pick data along a well trajectory + * + * md: Measured depth value at the pick point. + * direction: Direction of the pick relative to the surface. + */ +export type SurfaceWellPick_api = { + /** + * Md + */ + md: number; + direction: PickDirection_api; +}; + +/** + * SurfaceWellPicks + * + * Surface picks along a well trajectory for a specific surface. + * + * Each pick contains the measured depth and direction. + */ +export type SurfaceWellPicks_api = { + /** + * Picks + */ + picks: Array; +}; + /** * THP */ @@ -2767,6 +2843,61 @@ export type WellProductionData_api = { waterProductionM3: number; }; +/** + * WellTrajectory + * + * Well trajectory defined by a set of (x, y, z) coordinates and measured depths (md). + * + * uwi: Unique wellbore identifier. + * xPoints: X-coordinates of well trajectory points. + * yPoints: Y-coordinates of well trajectory points. + * zPoints: Z-coordinates (depth values) of well trajectory points. + * mdPoints: Measured depth values at each well trajectory point. + */ +export type WellTrajectory_api = { + /** + * Xpoints + */ + xPoints: Array; + /** + * Ypoints + */ + yPoints: Array; + /** + * Zpoints + */ + zPoints: Array; + /** + * Mdpoints + */ + mdPoints: Array; + /** + * Uwi + */ + uwi: string; +}; + +/** + * WellTrajectoryFormationSegments + * + * Segments of a well trajectory that intersects a formation defined by top and bottom surfaces. + * + * A wellbore can enter and exit a formation multiple times, resulting in multiple segments. + * + * uniqueWellboreIdentifier: str + * formationSegments: List[FormationSegment] + */ +export type WellTrajectoryFormationSegments_api = { + /** + * Uwi + */ + uwi: string; + /** + * Formationsegments + */ + formationSegments: Array; +}; + /** * WellboreCasing * @@ -2915,14 +3046,6 @@ export type WellboreHeader_api = { * Currenttrack */ currentTrack: number; - /** - * Tvdmax - */ - tvdMax: number; - /** - * Mdmax - */ - mdMax: number; /** * Kickoffdepthmd */ @@ -2935,6 +3058,30 @@ export type WellboreHeader_api = { * Parentwellbore */ parentWellbore: string | null; + /** + * Mdmin + */ + mdMin?: number | null; + /** + * Mdmax + */ + mdMax?: number | null; + /** + * Mdunit + */ + mdUnit?: string | null; + /** + * Tvdmin + */ + tvdMin?: number | null; + /** + * Tvdmax + */ + tvdMax?: number | null; + /** + * Tvdunit + */ + tvdUnit?: string | null; }; /** @@ -4193,6 +4340,86 @@ export type GetSurfaceDataResponses_api = { export type GetSurfaceDataResponse_api = GetSurfaceDataResponses_api[keyof GetSurfaceDataResponses_api]; +export type PostGetWellTrajectoryPicksPerSurfaceData_api = { + body: BodyPostGetWellTrajectoryPicksPerSurface_api; + path?: never; + query: { + /** + * Depth Surface Addr Str List + * + * List of surface address strings for depth surfaces. Supported address types are *REAL*, *OBS* and *STAT* + */ + depth_surface_addr_str_list: Array; + zCacheBust?: string; + }; + url: "/surface/get_well_trajectory_picks_per_surface"; +}; + +export type PostGetWellTrajectoryPicksPerSurfaceErrors_api = { + /** + * Validation Error + */ + 422: HTTPValidationError_api; +}; + +export type PostGetWellTrajectoryPicksPerSurfaceError_api = + PostGetWellTrajectoryPicksPerSurfaceErrors_api[keyof PostGetWellTrajectoryPicksPerSurfaceErrors_api]; + +export type PostGetWellTrajectoryPicksPerSurfaceResponses_api = { + /** + * Response Post Get Well Trajectory Picks Per Surface + * + * Successful Response + */ + 200: Array; +}; + +export type PostGetWellTrajectoryPicksPerSurfaceResponse_api = + PostGetWellTrajectoryPicksPerSurfaceResponses_api[keyof PostGetWellTrajectoryPicksPerSurfaceResponses_api]; + +export type PostGetWellTrajectoriesFormationSegmentsData_api = { + body: BodyPostGetWellTrajectoriesFormationSegments_api; + path?: never; + query: { + /** + * Top Depth Surf Addr Str + * + * Surface address string for top bounding depth surface. Supported address types are *REAL*, *OBS* and *STAT* + */ + top_depth_surf_addr_str: string; + /** + * Bottom Depth Surf Addr Str + * + * Optional surface address string for bottom bounding depth surface. If not provided end of well trajectory is used as lower bound for formation. Supported address types are *REAL*, *OBS* and *STAT* + */ + bottom_depth_surf_addr_str?: string | null; + zCacheBust?: string; + }; + url: "/surface/get_well_trajectories_formation_segments"; +}; + +export type PostGetWellTrajectoriesFormationSegmentsErrors_api = { + /** + * Validation Error + */ + 422: HTTPValidationError_api; +}; + +export type PostGetWellTrajectoriesFormationSegmentsError_api = + PostGetWellTrajectoriesFormationSegmentsErrors_api[keyof PostGetWellTrajectoriesFormationSegmentsErrors_api]; + +export type PostGetWellTrajectoriesFormationSegmentsResponses_api = { + /** + * Response Post Get Well Trajectories Formation Segments + * + * Successful Response + */ + 200: Array; +}; + +export type PostGetWellTrajectoriesFormationSegmentsResponse_api = + PostGetWellTrajectoriesFormationSegmentsResponses_api[keyof PostGetWellTrajectoriesFormationSegmentsResponses_api]; + export type GetStatisticalSurfaceDataHybridData_api = { body?: never; path?: never; diff --git a/frontend/src/lib/utils/vec2.ts b/frontend/src/lib/utils/vec2.ts index 1ceac35906..0fd1612e58 100644 --- a/frontend/src/lib/utils/vec2.ts +++ b/frontend/src/lib/utils/vec2.ts @@ -7,6 +7,10 @@ export function vec2FromArray(array: ArrayLike | [number, number]): Vec2 return { x: array[0], y: array[1] }; } +export function vec2ToArray(vector: Vec2): [number, number] { + return [vector.x, vector.y]; +} + export function vec2FromPointerEvent(event: PointerEvent): Vec2 { return { x: event.pageX, y: event.pageY }; } diff --git a/frontend/src/modules/2DViewer/DataProviderFramework/customDataProviderImplementations/RealizationGridProvider.ts b/frontend/src/modules/2DViewer/DataProviderFramework/customDataProviderImplementations/RealizationGridProvider.ts index 112932333f..628d3c0ec1 100644 --- a/frontend/src/modules/2DViewer/DataProviderFramework/customDataProviderImplementations/RealizationGridProvider.ts +++ b/frontend/src/modules/2DViewer/DataProviderFramework/customDataProviderImplementations/RealizationGridProvider.ts @@ -9,7 +9,7 @@ import type { FetchDataParams, } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customDataProviderImplementation"; import type { DefineDependenciesArgs } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingsHandler"; -import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; +import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/utils"; import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; import type { RealizationGridData } from "@modules/_shared/DataProviderFramework/visualization/utils/types"; import { @@ -158,11 +158,11 @@ export class RealizationGridProvider defineDependencies({ helperDependency, - availableSettingsUpdater, + valueRangeUpdater, storedDataUpdater, queryClient, }: DefineDependenciesArgs) { - availableSettingsUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { + valueRangeUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { const fieldIdentifier = getGlobalSetting("fieldId"); const ensembles = getGlobalSetting("ensembles"); @@ -173,7 +173,7 @@ export class RealizationGridProvider return ensembleIdents; }); - availableSettingsUpdater(Setting.REALIZATION, ({ getLocalSetting, getGlobalSetting }) => { + valueRangeUpdater(Setting.REALIZATION, ({ getLocalSetting, getGlobalSetting }) => { const ensembleIdent = getLocalSetting(Setting.ENSEMBLE); const realizationFilterFunc = getGlobalSetting("realizationFilterFunction"); @@ -206,7 +206,7 @@ export class RealizationGridProvider }); }); - availableSettingsUpdater(Setting.GRID_NAME, ({ getHelperDependency }) => { + valueRangeUpdater(Setting.GRID_NAME, ({ getHelperDependency }) => { const data = getHelperDependency(realizationGridDataDep); if (!data) { @@ -218,7 +218,7 @@ export class RealizationGridProvider return availableGridNames; }); - availableSettingsUpdater(Setting.ATTRIBUTE, ({ getLocalSetting, getHelperDependency }) => { + valueRangeUpdater(Setting.ATTRIBUTE, ({ getLocalSetting, getHelperDependency }) => { const gridName = getLocalSetting(Setting.GRID_NAME); const data = getHelperDependency(realizationGridDataDep); @@ -236,7 +236,7 @@ export class RealizationGridProvider return availableGridAttributes; }); - availableSettingsUpdater(Setting.GRID_LAYER_K, ({ getLocalSetting, getHelperDependency }) => { + valueRangeUpdater(Setting.GRID_LAYER_K, ({ getLocalSetting, getHelperDependency }) => { const gridName = getLocalSetting(Setting.GRID_NAME); const data = getHelperDependency(realizationGridDataDep); @@ -253,7 +253,7 @@ export class RealizationGridProvider return availableGridLayers; }); - availableSettingsUpdater(Setting.TIME_OR_INTERVAL, ({ getLocalSetting, getHelperDependency }) => { + valueRangeUpdater(Setting.TIME_OR_INTERVAL, ({ getLocalSetting, getHelperDependency }) => { const gridName = getLocalSetting(Setting.GRID_NAME); const gridAttribute = getLocalSetting(Setting.ATTRIBUTE); const data = getHelperDependency(realizationGridDataDep); diff --git a/frontend/src/modules/2DViewer/DataProviderFramework/visualization/makeDrilledWellTrajectoriesLayer2D.ts b/frontend/src/modules/2DViewer/DataProviderFramework/visualization/makeDrilledWellTrajectoriesLayer2D.ts index 2c8a9b18f4..e8f73c8df7 100644 --- a/frontend/src/modules/2DViewer/DataProviderFramework/visualization/makeDrilledWellTrajectoriesLayer2D.ts +++ b/frontend/src/modules/2DViewer/DataProviderFramework/visualization/makeDrilledWellTrajectoriesLayer2D.ts @@ -1,4 +1,5 @@ import type { WellboreTrajectory_api } from "@api"; +import type { DrilledWellboreTrajectoriesStoredData } from "@modules/_shared/DataProviderFramework/dataProviders/implementations/DrilledWellboreTrajectoriesProvider"; import { makeDrilledWellTrajectoriesLayer } from "@modules/_shared/DataProviderFramework/visualization/deckgl/makeDrilledWellTrajectoriesLayer"; import type { TransformerArgs } from "@modules/_shared/DataProviderFramework/visualization/VisualizationAssembler"; @@ -6,7 +7,7 @@ import type { TransformerArgs } from "@modules/_shared/DataProviderFramework/vis // The shared visualizer function has all the general settings we want, so // we just inject an extra prop to disable the depth test. export function makeDrilledWellTrajectoriesLayer2D( - args: TransformerArgs, + args: TransformerArgs, ): ReturnType { const layer = makeDrilledWellTrajectoriesLayer(args); diff --git a/frontend/src/modules/2DViewer/DataProviderFramework/visualization/makeRichWellTrajectoriesLayer.ts b/frontend/src/modules/2DViewer/DataProviderFramework/visualization/makeRichWellTrajectoriesLayer.ts new file mode 100644 index 0000000000..1cad0b9370 --- /dev/null +++ b/frontend/src/modules/2DViewer/DataProviderFramework/visualization/makeRichWellTrajectoriesLayer.ts @@ -0,0 +1,212 @@ +import type { WellInjectionData_api, WellProductionData_api } from "@api"; +import type { + FormationSegmentData, + RichWellsLayerProps, + WellboreData, +} from "@modules/_shared/customDeckGlLayers/RichWellsLayer/RichWellsLayer"; +import { RichWellsLayer } from "@modules/_shared/customDeckGlLayers/RichWellsLayer/RichWellsLayer"; +import type { + DrilledWellboreTrajectoriesData, + DrilledWellboreTrajectoriesSettings, + DrilledWellboreTrajectoriesStoredData, +} from "@modules/_shared/DataProviderFramework/dataProviders/implementations/DrilledWellboreTrajectoriesProvider"; +import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; +import type { TransformerArgs } from "@modules/_shared/DataProviderFramework/visualization/VisualizationAssembler"; +import type { + WellFeature as BaseWellFeature, + GeoJsonWellProperties as BaseWellProperties, +} from "@webviz/subsurface-viewer/dist/layers/wells/types"; +import { parse, type Rgb } from "culori"; + +export type GeoWellProperties = BaseWellProperties & { + uuid: string; + uwi: string; + lineWidth: number; + wellHeadSize: number; +}; +export type GeoWellFeature = BaseWellFeature & { properties: GeoWellProperties }; + +export function makeRichWellTrajectoriesLayer({ + id, + isLoading, + getData, + getSetting, +}: TransformerArgs< + DrilledWellboreTrajectoriesSettings, + DrilledWellboreTrajectoriesData, + DrilledWellboreTrajectoriesStoredData +>): RichWellsLayer | null { + if (isLoading) return null; + + const wellboreTrajectoriesData = getData(); + + if (!wellboreTrajectoriesData) { + return null; + } + + const depthFilterType = getSetting(Setting.WELLBORE_DEPTH_FILTER_TYPE); + + // ************************** + // TODO: Segment filter settings is currently not optional. Making same top/bottom count as "unfiltered" + const formationFilter = getSetting(Setting.WELLBORE_DEPTH_FORMATION_FILTER); + const surfaceFilterTop = formationFilter?.topSurfaceName; + const surfaceFilterBtm = formationFilter?.baseSurfaceName; + + const shouldApplySegmentFilter = + depthFilterType === "surface_based" && surfaceFilterTop !== surfaceFilterBtm && surfaceFilterTop !== null; + // ************************** + + let tvdRange: RichWellsLayerProps["tvdFilterValue"] = undefined; + let mdRange: RichWellsLayerProps["mdFilterValue"] = undefined; + if (depthFilterType === "tvd_range") { + const tvdRangeSetting = getSetting(Setting.TVD_RANGE); + tvdRange = tvdRangeSetting ?? undefined; + } else if (depthFilterType === "md_range") { + const mdRangeSetting = getSetting(Setting.MD_RANGE); + mdRange = mdRangeSetting ?? undefined; + } + + // Get settings for flow data filtering + const pdmFilterType = getSetting(Setting.PDM_FILTER_TYPE); + const pdmFilterActive = pdmFilterType === "production_injection"; + const pdmFilter = getSetting(Setting.PDM_FILTER); + + const oilProdMin = pdmFilterActive ? (pdmFilter?.production.oil ?? null) : null; + const gasProdMin = pdmFilterActive ? (pdmFilter?.production.gas ?? null) : null; + const waterProdMin = pdmFilterActive ? (pdmFilter?.production.water ?? null) : null; + const waterInjMin = pdmFilterActive ? (pdmFilter?.injection.water ?? null) : null; + const gasInjMin = pdmFilterActive ? (pdmFilter?.injection.gas ?? null) : null; + + const wellboreData: WellboreData[] = []; + for (const wt of wellboreTrajectoriesData) { + if (wt.productionData) { + if (wt.productionData.oilProductionSm3 < (oilProdMin?.value ?? 0)) continue; + if (wt.productionData.gasProductionSm3 < (gasProdMin?.value ?? 0)) continue; + if (wt.productionData.waterProductionM3 < (waterProdMin?.value ?? 0)) continue; + } + if (wt.injectionData) { + if (wt.injectionData.waterInjection < (waterInjMin?.value ?? 0)) continue; + if (wt.injectionData.gasInjection < (gasInjMin?.value ?? 0)) continue; + } + + wellboreData.push({ + uuid: wt.wellboreUuid, + uniqueIdentifier: wt.uniqueWellboreIdentifier, + trajectory: wt, + + perforations: wt.perforations, + // TODO: Segments for the entire track, not just selected range? + formationSegments: wt.formationSegments.map( + (fs): FormationSegmentData => ({ + mdEnter: fs.mdEnter, + mdExit: fs.mdExit, + segmentIdent: "WITHIN_FILTER", + }), + ), + screens: wt.screens, + purpose: wt.wellborePurpose, + status: wt.wellboreStatus, + well: { + uuid: wt.wellUuid, + uniqueIdentifier: wt.uniqueWellIdentifier, + easting: wt.wellEasting, + northing: wt.wellNorthing, + }, + }); + } + + const wellsLayer = new RichWellsLayer({ + id, + data: wellboreData, + segmentFilterValue: shouldApplySegmentFilter ? ["WITHIN_FILTER"] : undefined, + tvdFilterValue: tvdRange, + mdFilterValue: mdRange, + + discardFilteredSections: true, + getWellColor: (wellboreUwi: string) => { + const productionData = + wellboreTrajectoriesData.find((wt) => wt.uniqueWellIdentifier === wellboreUwi)?.productionData ?? null; + const injectionData = + wellboreTrajectoriesData.find((wt) => wt.uniqueWellboreIdentifier === wellboreUwi)?.injectionData ?? + null; + const color = setColorByFlowData( + oilProdMin, + gasProdMin, + waterProdMin, + waterInjMin, + gasInjMin, + productionData, + injectionData, + ); + // Return color or default gray (shouldn't happen since we filter) + return color ?? { r: 128, g: 128, b: 128 }; + }, + }); + + return wellsLayer; +} + +function hexToRgb(hex: string): { r: number; g: number; b: number } { + const color = parse(hex); + + if (!color || !("r" in color && "g" in color && "b" in color)) { + // Fallback to gray if parsing fails + return { r: 128, g: 128, b: 128 }; + } + + const rgb = color as Rgb; + return { + r: Math.round(rgb.r * 255), + g: Math.round(rgb.g * 255), + b: Math.round(rgb.b * 255), + }; +} + +function setColorByFlowData( + oilProdMin: { value: number; color: string } | null, + gasProdMin: { value: number; color: string } | null, + waterProdMin: { value: number; color: string } | null, + waterInjMin: { value: number; color: string } | null, + gasInjMin: { value: number; color: string } | null, + productionData: Pick | null, + injectionData: Pick | null, +): { r: number; g: number; b: number } | null { + if (productionData) { + // Oil production + if (oilProdMin) { + if (productionData.oilProductionSm3 >= oilProdMin.value) { + return hexToRgb(oilProdMin.color); + } + } + // Gas production + if (gasProdMin) { + if (productionData.gasProductionSm3 >= gasProdMin.value) { + return hexToRgb(gasProdMin.color); + } + } + // Water production + if (waterProdMin) { + if (productionData.waterProductionM3 >= waterProdMin.value) { + return hexToRgb(waterProdMin.color); + } + } + } + + if (injectionData) { + // Water injection + if (waterInjMin) { + if (injectionData.waterInjection >= waterInjMin.value) { + return hexToRgb(waterInjMin.color); + } + } + // Gas injection + if (gasInjMin) { + if (injectionData.gasInjection >= gasInjMin.value) { + return hexToRgb(gasInjMin.color); + } + } + } + + // Default gray color + return null; +} diff --git a/frontend/src/modules/2DViewer/view/components/VisualizationAssemblerWrapper.tsx b/frontend/src/modules/2DViewer/view/components/VisualizationAssemblerWrapper.tsx index 35f13a7f04..eb4a1422f6 100644 --- a/frontend/src/modules/2DViewer/view/components/VisualizationAssemblerWrapper.tsx +++ b/frontend/src/modules/2DViewer/view/components/VisualizationAssemblerWrapper.tsx @@ -7,7 +7,7 @@ import { } from "@modules/_shared/components/SubsurfaceViewer/DpfSubsurfaceViewerWrapper"; import { DataProviderType } from "@modules/_shared/DataProviderFramework/dataProviders/dataProviderTypes"; import { DrilledWellborePicksProvider } from "@modules/_shared/DataProviderFramework/dataProviders/implementations/DrilledWellborePicksProvider"; -import { DrilledWellTrajectoriesProvider } from "@modules/_shared/DataProviderFramework/dataProviders/implementations/DrilledWellTrajectoriesProvider"; +import { DrilledWellboreTrajectoriesProvider } from "@modules/_shared/DataProviderFramework/dataProviders/implementations/DrilledWellboreTrajectoriesProvider"; import { FaultPolygonsProvider } from "@modules/_shared/DataProviderFramework/dataProviders/implementations/FaultPolygonsProvider"; import { RealizationPolygonsProvider } from "@modules/_shared/DataProviderFramework/dataProviders/implementations/RealizationPolygonsProvider"; import { @@ -51,9 +51,9 @@ import { import { CustomDataProviderType } from "../../DataProviderFramework/customDataProviderImplementations/dataProviderTypes"; import { RealizationGridProvider } from "../../DataProviderFramework/customDataProviderImplementations/RealizationGridProvider"; import { makeDrilledWellborePicksLayer2D } from "../../DataProviderFramework/visualization/makeDrilledWellborePicksLayer2D"; -import { makeDrilledWellTrajectoriesLayer2D } from "../../DataProviderFramework/visualization/makeDrilledWellTrajectoriesLayer2D"; import "../../DataProviderFramework/customDataProviderImplementations/registerAllDataProviders"; +import { makeRichWellTrajectoriesLayer } from "@modules/2DViewer/DataProviderFramework/visualization/makeRichWellTrajectoriesLayer"; const VISUALIZATION_ASSEMBLER = new VisualizationAssembler(); VISUALIZATION_ASSEMBLER.registerDataProviderTransformers( @@ -143,9 +143,9 @@ VISUALIZATION_ASSEMBLER.registerDataProviderTransformers( ); VISUALIZATION_ASSEMBLER.registerDataProviderTransformers( DataProviderType.DRILLED_WELL_TRAJECTORIES, - DrilledWellTrajectoriesProvider, + DrilledWellboreTrajectoriesProvider, { - transformToVisualization: makeDrilledWellTrajectoriesLayer2D, + transformToVisualization: makeRichWellTrajectoriesLayer, transformToBoundingBox: makeDrilledWellTrajectoriesBoundingBox, }, ); diff --git a/frontend/src/modules/3DViewer/DataProviderFramework/customDataProviderImplementations/RealizationGridProvider.ts b/frontend/src/modules/3DViewer/DataProviderFramework/customDataProviderImplementations/RealizationGridProvider.ts index bcfdb834c7..ed5c56c164 100644 --- a/frontend/src/modules/3DViewer/DataProviderFramework/customDataProviderImplementations/RealizationGridProvider.ts +++ b/frontend/src/modules/3DViewer/DataProviderFramework/customDataProviderImplementations/RealizationGridProvider.ts @@ -8,7 +8,7 @@ import type { FetchDataParams, } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customDataProviderImplementation"; import type { DefineDependenciesArgs } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingsHandler"; -import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; +import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/utils"; import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; import type { RealizationGridData } from "@modules/_shared/DataProviderFramework/visualization/utils/types"; import { @@ -156,10 +156,10 @@ export class RealizationGridProvider defineDependencies({ helperDependency, - availableSettingsUpdater, + valueRangeUpdater, queryClient, }: DefineDependenciesArgs) { - availableSettingsUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { + valueRangeUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { const fieldIdentifier = getGlobalSetting("fieldId"); const ensembles = getGlobalSetting("ensembles"); @@ -170,7 +170,7 @@ export class RealizationGridProvider return ensembleIdents; }); - availableSettingsUpdater(Setting.REALIZATION, ({ getLocalSetting, getGlobalSetting }) => { + valueRangeUpdater(Setting.REALIZATION, ({ getLocalSetting, getGlobalSetting }) => { const ensembleIdent = getLocalSetting(Setting.ENSEMBLE); const realizationFilterFunc = getGlobalSetting("realizationFilterFunction"); @@ -203,7 +203,7 @@ export class RealizationGridProvider }); }); - availableSettingsUpdater(Setting.GRID_NAME, ({ getHelperDependency }) => { + valueRangeUpdater(Setting.GRID_NAME, ({ getHelperDependency }) => { const data = getHelperDependency(realizationGridDataDep); if (!data) { @@ -215,7 +215,7 @@ export class RealizationGridProvider return availableGridNames; }); - availableSettingsUpdater(Setting.ATTRIBUTE, ({ getLocalSetting, getHelperDependency }) => { + valueRangeUpdater(Setting.ATTRIBUTE, ({ getLocalSetting, getHelperDependency }) => { const gridName = getLocalSetting(Setting.GRID_NAME); const data = getHelperDependency(realizationGridDataDep); @@ -233,7 +233,7 @@ export class RealizationGridProvider return availableGridAttributes; }); - availableSettingsUpdater(Setting.GRID_LAYER_RANGE, ({ getLocalSetting, getHelperDependency }) => { + valueRangeUpdater(Setting.GRID_LAYER_RANGE, ({ getLocalSetting, getHelperDependency }) => { const gridName = getLocalSetting(Setting.GRID_NAME); const data = getHelperDependency(realizationGridDataDep); @@ -246,14 +246,17 @@ export class RealizationGridProvider return NO_UPDATE; } - return [ - [0, gridDimensions.i_count - 1, 1], - [0, gridDimensions.j_count - 1, 1], - [0, gridDimensions.k_count - 1, 1], - ]; + return { + range: { + i: [0, gridDimensions.i_count - 1, 1], + j: [0, gridDimensions.j_count - 1, 1], + k: [0, gridDimensions.k_count - 1, 1], + }, + zones: gridDimensions.subgrids, + }; }); - availableSettingsUpdater(Setting.TIME_OR_INTERVAL, ({ getLocalSetting, getHelperDependency }) => { + valueRangeUpdater(Setting.TIME_OR_INTERVAL, ({ getLocalSetting, getHelperDependency }) => { const gridName = getLocalSetting(Setting.GRID_NAME); const gridAttribute = getLocalSetting(Setting.ATTRIBUTE); const data = getHelperDependency(realizationGridDataDep); diff --git a/frontend/src/modules/3DViewer/DataProviderFramework/customDataProviderImplementations/RealizationSeismicSlicesProvider.ts b/frontend/src/modules/3DViewer/DataProviderFramework/customDataProviderImplementations/RealizationSeismicSlicesProvider.ts index c97eefab45..a5add29916 100644 --- a/frontend/src/modules/3DViewer/DataProviderFramework/customDataProviderImplementations/RealizationSeismicSlicesProvider.ts +++ b/frontend/src/modules/3DViewer/DataProviderFramework/customDataProviderImplementations/RealizationSeismicSlicesProvider.ts @@ -13,10 +13,12 @@ import type { } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customDataProviderImplementation"; import type { DefineDependenciesArgs } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingsHandler"; import type { NullableStoredData } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/sharedTypes"; -import { type MakeSettingTypesMap, Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; +import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/utils"; +import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; import { type SeismicSliceData_trans, transformSeismicSlice } from "../utils/transformSeismicSlice"; + const realizationSeismicSlicesSettings = [ Setting.ENSEMBLE, Setting.REALIZATION, @@ -171,11 +173,11 @@ export class RealizationSeismicSlicesProvider defineDependencies({ helperDependency, - availableSettingsUpdater, + valueRangeUpdater, storedDataUpdater, queryClient, }: DefineDependenciesArgs): void { - availableSettingsUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { + valueRangeUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { const fieldIdentifier = getGlobalSetting("fieldId"); const ensembles = getGlobalSetting("ensembles"); @@ -186,7 +188,7 @@ export class RealizationSeismicSlicesProvider return ensembleIdents; }); - availableSettingsUpdater(Setting.REALIZATION, ({ getLocalSetting, getGlobalSetting }) => { + valueRangeUpdater(Setting.REALIZATION, ({ getLocalSetting, getGlobalSetting }) => { const ensembleIdent = getLocalSetting(Setting.ENSEMBLE); const realizationFilterFunc = getGlobalSetting("realizationFilterFunction"); @@ -237,7 +239,7 @@ export class RealizationSeismicSlicesProvider ); }); - availableSettingsUpdater(Setting.ATTRIBUTE, ({ getHelperDependency }) => { + valueRangeUpdater(Setting.ATTRIBUTE, ({ getHelperDependency }) => { const data = getHelperDependency(realizationSeismicCrosslineDataDep); if (!data) { @@ -251,7 +253,7 @@ export class RealizationSeismicSlicesProvider return availableSeismicAttributes; }); - availableSettingsUpdater(Setting.TIME_OR_INTERVAL, ({ getLocalSetting, getHelperDependency }) => { + valueRangeUpdater(Setting.TIME_OR_INTERVAL, ({ getLocalSetting, getHelperDependency }) => { const seismicAttribute = getLocalSetting(Setting.ATTRIBUTE); const data = getHelperDependency(realizationSeismicCrosslineDataDep); @@ -273,7 +275,7 @@ export class RealizationSeismicSlicesProvider return availableTimeOrIntervals; }); - availableSettingsUpdater(Setting.SEISMIC_SLICES, ({ getLocalSetting, getHelperDependency }) => { + valueRangeUpdater(Setting.SEISMIC_SLICES, ({ getLocalSetting, getHelperDependency }) => { const seismicAttribute = getLocalSetting(Setting.ATTRIBUTE); const timeOrInterval = getLocalSetting(Setting.TIME_OR_INTERVAL); const data = getHelperDependency(realizationSeismicCrosslineDataDep); diff --git a/frontend/src/modules/3DViewer/DataProviderFramework/visualization/makeSeismicIntersectionMeshLayer.ts b/frontend/src/modules/3DViewer/DataProviderFramework/visualization/makeSeismicIntersectionMeshLayer.ts index f6b5b088b0..e3cc809a44 100644 --- a/frontend/src/modules/3DViewer/DataProviderFramework/visualization/makeSeismicIntersectionMeshLayer.ts +++ b/frontend/src/modules/3DViewer/DataProviderFramework/visualization/makeSeismicIntersectionMeshLayer.ts @@ -34,11 +34,11 @@ export function makeSeismicIntersectionMeshLayer( IntersectionRealizationSeismicStoredData >, ): Layer | null { - const { id, name, getData, getSetting, getStoredData, getValueRange } = args; + const { id, name, getData, getSetting, getStoredData, getDataValueRange } = args; const fenceData = getData(); const colorScaleSpec = getSetting(Setting.COLOR_SCALE); const opacityPercent = (getSetting(Setting.OPACITY_PERCENT) ?? 100) / 100; - const valueRange = getValueRange(); + const valueRange = getDataValueRange(); const polyline = getStoredData("seismicFencePolylineWithSectionLengths"); if (!fenceData || !polyline) { diff --git a/frontend/src/modules/3DViewer/DataProviderFramework/visualization/makeSeismicSlicesLayer.ts b/frontend/src/modules/3DViewer/DataProviderFramework/visualization/makeSeismicSlicesLayer.ts index ad21408363..c58efd845c 100644 --- a/frontend/src/modules/3DViewer/DataProviderFramework/visualization/makeSeismicSlicesLayer.ts +++ b/frontend/src/modules/3DViewer/DataProviderFramework/visualization/makeSeismicSlicesLayer.ts @@ -216,14 +216,14 @@ export function makeSeismicSlicesLayer( RealizationSeismicSlicesStoredData >, ): Layer | null { - const { id, name, getData, getSetting, getStoredData, isLoading, getValueRange } = args; + const { id, name, getData, getSetting, getStoredData, isLoading, getDataValueRange } = args; const data = getData(); const colorScaleSpec = getSetting(Setting.COLOR_SCALE); const slicesSettings = getSetting(Setting.SEISMIC_SLICES); const slices = getStoredData("seismicSlices"); const seismicCubeMeta = getStoredData("seismicCubeMeta"); const opacityPercent = getSetting(Setting.OPACITY_PERCENT) ?? 100; - const valueRange = getValueRange(); + const valueRange = getDataValueRange(); if (!seismicCubeMeta || !slicesSettings) { return null; diff --git a/frontend/src/modules/3DViewer/view/components/VisualizationAssemblerWrapper.tsx b/frontend/src/modules/3DViewer/view/components/VisualizationAssemblerWrapper.tsx index 5e5922b623..d919474b43 100644 --- a/frontend/src/modules/3DViewer/view/components/VisualizationAssemblerWrapper.tsx +++ b/frontend/src/modules/3DViewer/view/components/VisualizationAssemblerWrapper.tsx @@ -22,7 +22,7 @@ import { } from "@modules/_shared/components/SubsurfaceViewer/DpfSubsurfaceViewerWrapper"; import { DataProviderType } from "@modules/_shared/DataProviderFramework/dataProviders/dataProviderTypes"; import { DrilledWellborePicksProvider } from "@modules/_shared/DataProviderFramework/dataProviders/implementations/DrilledWellborePicksProvider"; -import { DrilledWellTrajectoriesProvider } from "@modules/_shared/DataProviderFramework/dataProviders/implementations/DrilledWellTrajectoriesProvider"; +import { DrilledWellboreTrajectoriesProvider } from "@modules/_shared/DataProviderFramework/dataProviders/implementations/DrilledWellboreTrajectoriesProvider"; import { FaultPolygonsProvider } from "@modules/_shared/DataProviderFramework/dataProviders/implementations/FaultPolygonsProvider"; import { IntersectionRealizationGridProvider } from "@modules/_shared/DataProviderFramework/dataProviders/implementations/IntersectionRealizationGridProvider"; import { IntersectionRealizationSeismicProvider } from "@modules/_shared/DataProviderFramework/dataProviders/implementations/IntersectionRealizationSeismicProvider"; @@ -98,7 +98,7 @@ VISUALIZATION_ASSEMBLER.registerDataProviderTransformers( ); VISUALIZATION_ASSEMBLER.registerDataProviderTransformers( DataProviderType.DRILLED_WELL_TRAJECTORIES, - DrilledWellTrajectoriesProvider, + DrilledWellboreTrajectoriesProvider, { transformToVisualization: makeDrilledWellTrajectoriesLayer, transformToBoundingBox: makeDrilledWellTrajectoriesBoundingBox, diff --git a/frontend/src/modules/InplaceVolumesTable/view/utils/tableComponentUtils.ts b/frontend/src/modules/InplaceVolumesTable/view/utils/tableComponentUtils.ts index 10dfa2ac16..ad050cc889 100644 --- a/frontend/src/modules/InplaceVolumesTable/view/utils/tableComponentUtils.ts +++ b/frontend/src/modules/InplaceVolumesTable/view/utils/tableComponentUtils.ts @@ -3,6 +3,7 @@ import type { EnsembleSet } from "@framework/EnsembleSet"; import { RegularEnsemble } from "@framework/RegularEnsemble"; import { RegularEnsembleIdent } from "@framework/RegularEnsembleIdent"; import type { TableHeading, TableRow } from "@lib/components/TableDeprecated/table"; +import { PHASE_COLORS } from "@modules/_shared/constants/colors"; import { makeDistinguishableEnsembleDisplayName } from "@modules/_shared/ensembleNameUtils"; import { sortResultNameStrings } from "@modules/_shared/InplaceVolumes/sortResultNames"; import type { Column, Row } from "@modules/_shared/InplaceVolumes/Table"; @@ -153,13 +154,13 @@ function makeStyleFormattingFunc(column: Column): ((value: number | string | nul const style: React.CSSProperties = { textAlign: "right", fontWeight: "bold" }; if (value?.toString().toLowerCase() === "oil") { - style.color = "#0b8511"; + style.color = PHASE_COLORS.oil; } if (value?.toString().toLowerCase() === "water") { - style.color = "#0c24ab"; + style.color = PHASE_COLORS.water; } if (value?.toString().toLowerCase() === "gas") { - style.color = "#ab110c"; + style.color = PHASE_COLORS.gas; } return style; diff --git a/frontend/src/modules/Intersection/DataProviderFramework/annotations/makeColorScaleAnnotation.ts b/frontend/src/modules/Intersection/DataProviderFramework/annotations/makeColorScaleAnnotation.ts index 451ae27059..09c2d046b8 100644 --- a/frontend/src/modules/Intersection/DataProviderFramework/annotations/makeColorScaleAnnotation.ts +++ b/frontend/src/modules/Intersection/DataProviderFramework/annotations/makeColorScaleAnnotation.ts @@ -9,7 +9,7 @@ import { createGridColorScaleValues, createSeismicColorScaleValues } from "../ut function makeColorScaleAnnotation({ getSetting, - getValueRange, + getDataValueRange, id, isLoading, createColorScaleValues, @@ -18,7 +18,7 @@ function makeColorScaleAnnotation({ }): Annotation[] { const colorScale = getSetting(Setting.COLOR_SCALE)?.colorScale; const useCustomColorScaleBoundaries = getSetting(Setting.COLOR_SCALE)?.areBoundariesUserDefined ?? false; - const valueRange = getValueRange(); + const valueRange = getDataValueRange(); const attribute = getSetting(Setting.ATTRIBUTE); if (!colorScale || !valueRange || !attribute || isLoading) { diff --git a/frontend/src/modules/Intersection/DataProviderFramework/customDataProviderImplementations/EnsembleWellborePicksProvider.ts b/frontend/src/modules/Intersection/DataProviderFramework/customDataProviderImplementations/EnsembleWellborePicksProvider.ts index 7ed74d907f..72e219a71a 100644 --- a/frontend/src/modules/Intersection/DataProviderFramework/customDataProviderImplementations/EnsembleWellborePicksProvider.ts +++ b/frontend/src/modules/Intersection/DataProviderFramework/customDataProviderImplementations/EnsembleWellborePicksProvider.ts @@ -12,7 +12,8 @@ import type { FetchDataParams, } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customDataProviderImplementation"; import type { DefineDependenciesArgs } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingsHandler"; -import { type MakeSettingTypesMap, Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; +import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/utils"; +import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; const ensembleWellborePicksSettings = [ Setting.INTERSECTION, @@ -40,11 +41,11 @@ export class EnsembleWellborePicksProvider defineDependencies({ helperDependency, - availableSettingsUpdater, + valueRangeUpdater, queryClient, workbenchSession, }: DefineDependenciesArgs): void { - availableSettingsUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { + valueRangeUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { const fieldIdentifier = getGlobalSetting("fieldId"); const ensembles = getGlobalSetting("ensembles"); return getAvailableEnsembleIdentsForField(fieldIdentifier, ensembles); @@ -55,7 +56,7 @@ export class EnsembleWellborePicksProvider return fetchWellboreHeaders(ensembleIdent, abortSignal, workbenchSession, queryClient); }); - availableSettingsUpdater(Setting.INTERSECTION, ({ getHelperDependency, getGlobalSetting }) => { + valueRangeUpdater(Setting.INTERSECTION, ({ getHelperDependency, getGlobalSetting }) => { const wellboreHeaders = getHelperDependency(wellboreHeadersDep) ?? []; const intersectionPolylines = getGlobalSetting("intersectionPolylines"); const fieldIdentifier = getGlobalSetting("fieldId"); @@ -89,7 +90,7 @@ export class EnsembleWellborePicksProvider }); }); - availableSettingsUpdater(Setting.SMDA_INTERPRETER, ({ getHelperDependency }) => { + valueRangeUpdater(Setting.SMDA_INTERPRETER, ({ getHelperDependency }) => { const wellborePicks = getHelperDependency(wellborePicksDep); if (!wellborePicks) return []; @@ -100,7 +101,7 @@ export class EnsembleWellborePicksProvider return interpreters; }); - availableSettingsUpdater(Setting.WELLBORE_PICKS, ({ getLocalSetting, getHelperDependency }) => { + valueRangeUpdater(Setting.WELLBORE_PICKS, ({ getLocalSetting, getHelperDependency }) => { const wellborePicks = getHelperDependency(wellborePicksDep); const selectedInterpreter = getLocalSetting(Setting.SMDA_INTERPRETER); diff --git a/frontend/src/modules/Intersection/DataProviderFramework/customDataProviderImplementations/RealizationSurfacesProvider.ts b/frontend/src/modules/Intersection/DataProviderFramework/customDataProviderImplementations/RealizationSurfacesProvider.ts index b39963f9b4..26aa5d1edd 100644 --- a/frontend/src/modules/Intersection/DataProviderFramework/customDataProviderImplementations/RealizationSurfacesProvider.ts +++ b/frontend/src/modules/Intersection/DataProviderFramework/customDataProviderImplementations/RealizationSurfacesProvider.ts @@ -25,7 +25,7 @@ import type { FetchDataParams, } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customDataProviderImplementation"; import type { DefineDependenciesArgs } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingsHandler"; -import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; +import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/utils"; import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; import type { PolylineWithSectionLengths } from "@modules/_shared/Intersection/intersectionPolylineTypes"; @@ -111,7 +111,7 @@ export class RealizationSurfacesProvider defineDependencies({ helperDependency, - availableSettingsUpdater, + valueRangeUpdater, settingAttributesUpdater, queryClient, workbenchSession, @@ -124,13 +124,13 @@ export class RealizationSurfacesProvider return { enabled: isEnabled }; }); - availableSettingsUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { + valueRangeUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { const fieldIdentifier = getGlobalSetting("fieldId"); const ensembles = getGlobalSetting("ensembles"); return getAvailableEnsembleIdentsForField(fieldIdentifier, ensembles); }); - availableSettingsUpdater(Setting.REALIZATION, ({ getLocalSetting, getGlobalSetting }) => { + valueRangeUpdater(Setting.REALIZATION, ({ getLocalSetting, getGlobalSetting }) => { const ensembleIdent = getLocalSetting(Setting.ENSEMBLE); const realizationFilterFunc = getGlobalSetting("realizationFilterFunction"); return getAvailableRealizationsForEnsembleIdent(ensembleIdent, realizationFilterFunc); @@ -141,7 +141,7 @@ export class RealizationSurfacesProvider return fetchWellboreHeaders(ensembleIdent, abortSignal, workbenchSession, queryClient); }); - availableSettingsUpdater(Setting.INTERSECTION, ({ getHelperDependency, getGlobalSetting }) => { + valueRangeUpdater(Setting.INTERSECTION, ({ getHelperDependency, getGlobalSetting }) => { const wellboreHeaders = getHelperDependency(wellboreHeadersDep) ?? []; const intersectionPolylines = getGlobalSetting("intersectionPolylines"); const fieldIdentifier = getGlobalSetting("fieldId"); @@ -174,7 +174,7 @@ export class RealizationSurfacesProvider return surfaceMetadata; }); - availableSettingsUpdater(Setting.ATTRIBUTE, ({ getHelperDependency }) => { + valueRangeUpdater(Setting.ATTRIBUTE, ({ getHelperDependency }) => { const surfaceMetadataSet = getHelperDependency(surfaceMetadataSetDep); if (!surfaceMetadataSet) { return []; @@ -189,7 +189,7 @@ export class RealizationSurfacesProvider return Array.from(new Set(depthSurfacesMetadata.map((elm) => elm.attribute_name))).sort(); }); - availableSettingsUpdater(Setting.SURFACE_NAMES, ({ getLocalSetting, getHelperDependency }) => { + valueRangeUpdater(Setting.SURFACE_NAMES, ({ getLocalSetting, getHelperDependency }) => { const attribute = getLocalSetting(Setting.ATTRIBUTE); const surfaceMetadataSet = getHelperDependency(surfaceMetadataSetDep); diff --git a/frontend/src/modules/Intersection/DataProviderFramework/customDataProviderImplementations/SurfacesPerRealizationValuesProvider.ts b/frontend/src/modules/Intersection/DataProviderFramework/customDataProviderImplementations/SurfacesPerRealizationValuesProvider.ts index b36bf3f17a..704f296cc1 100644 --- a/frontend/src/modules/Intersection/DataProviderFramework/customDataProviderImplementations/SurfacesPerRealizationValuesProvider.ts +++ b/frontend/src/modules/Intersection/DataProviderFramework/customDataProviderImplementations/SurfacesPerRealizationValuesProvider.ts @@ -25,7 +25,7 @@ import type { FetchDataParams, } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customDataProviderImplementation"; import type { DefineDependenciesArgs } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingsHandler"; -import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; +import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/utils"; import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; import { createValidExtensionLength } from "../utils/extensionLengthUtils"; @@ -115,7 +115,7 @@ export class SurfacesPerRealizationValuesProvider defineDependencies({ helperDependency, - availableSettingsUpdater, + valueRangeUpdater, settingAttributesUpdater, queryClient, workbenchSession, @@ -128,13 +128,13 @@ export class SurfacesPerRealizationValuesProvider return { enabled: isEnabled }; }); - availableSettingsUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { + valueRangeUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { const fieldIdentifier = getGlobalSetting("fieldId"); const ensembles = getGlobalSetting("ensembles"); return getAvailableEnsembleIdentsForField(fieldIdentifier, ensembles); }); - availableSettingsUpdater(Setting.REALIZATIONS, ({ getLocalSetting, getGlobalSetting }) => { + valueRangeUpdater(Setting.REALIZATIONS, ({ getLocalSetting, getGlobalSetting }) => { const ensembleIdent = getLocalSetting(Setting.ENSEMBLE); const realizationFilterFunc = getGlobalSetting("realizationFilterFunction"); return getAvailableRealizationsForEnsembleIdent(ensembleIdent, realizationFilterFunc); @@ -145,7 +145,7 @@ export class SurfacesPerRealizationValuesProvider return fetchWellboreHeaders(ensembleIdent, abortSignal, workbenchSession, queryClient); }); - availableSettingsUpdater(Setting.INTERSECTION, ({ getHelperDependency, getGlobalSetting }) => { + valueRangeUpdater(Setting.INTERSECTION, ({ getHelperDependency, getGlobalSetting }) => { const wellboreHeaders = getHelperDependency(wellboreHeadersDep) ?? []; const intersectionPolylines = getGlobalSetting("intersectionPolylines"); const fieldIdentifier = getGlobalSetting("fieldId"); @@ -178,7 +178,7 @@ export class SurfacesPerRealizationValuesProvider return surfaceMetadata; }); - availableSettingsUpdater(Setting.ATTRIBUTE, ({ getHelperDependency }) => { + valueRangeUpdater(Setting.ATTRIBUTE, ({ getHelperDependency }) => { const surfaceMetadataSet = getHelperDependency(surfaceMetadataSetDep); if (!surfaceMetadataSet) { return []; @@ -193,7 +193,7 @@ export class SurfacesPerRealizationValuesProvider return Array.from(new Set(depthSurfacesMetadata.map((elm) => elm.attribute_name))).sort(); }); - availableSettingsUpdater(Setting.SURFACE_NAMES, ({ getLocalSetting, getHelperDependency }) => { + valueRangeUpdater(Setting.SURFACE_NAMES, ({ getLocalSetting, getHelperDependency }) => { const attribute = getLocalSetting(Setting.ATTRIBUTE); const surfaceMetadataSet = getHelperDependency(surfaceMetadataSetDep); diff --git a/frontend/src/modules/Intersection/DataProviderFramework/visualization/createGridLayerItemsMaker.ts b/frontend/src/modules/Intersection/DataProviderFramework/visualization/createGridLayerItemsMaker.ts index 25dbd385b7..3bad0b784f 100644 --- a/frontend/src/modules/Intersection/DataProviderFramework/visualization/createGridLayerItemsMaker.ts +++ b/frontend/src/modules/Intersection/DataProviderFramework/visualization/createGridLayerItemsMaker.ts @@ -21,7 +21,7 @@ export function createGridLayerItemsMaker({ getData, getSetting, getStoredData, - getValueRange, + getDataValueRange, }: TransformerArgs< IntersectionRealizationGridSettings, IntersectionRealizationGridData, @@ -35,7 +35,7 @@ export function createGridLayerItemsMaker({ const showGridLines = getSetting(Setting.SHOW_GRID_LINES); const selectedAttribute = getSetting(Setting.ATTRIBUTE); const sourcePolylineWithSectionLengths = getStoredData("polylineWithSectionLengths"); - const valueRange = getValueRange(); + const valueRange = getDataValueRange(); const extensionLength = createValidExtensionLength( getSetting(Setting.INTERSECTION), diff --git a/frontend/src/modules/Intersection/DataProviderFramework/visualization/createSeismicLayerItemsMaker.ts b/frontend/src/modules/Intersection/DataProviderFramework/visualization/createSeismicLayerItemsMaker.ts index bab7524e37..1e74c8e4ae 100644 --- a/frontend/src/modules/Intersection/DataProviderFramework/visualization/createSeismicLayerItemsMaker.ts +++ b/frontend/src/modules/Intersection/DataProviderFramework/visualization/createSeismicLayerItemsMaker.ts @@ -48,7 +48,7 @@ export function createSeismicLayerItemsMaker({ getData, getSetting, getStoredData, - getValueRange, + getDataValueRange, isLoading, name, }: TransformerArgs< @@ -62,7 +62,7 @@ export function createSeismicLayerItemsMaker({ const colorOpacityPercent = getSetting(Setting.OPACITY_PERCENT) ?? 100; const useCustomColorScaleBoundaries = getSetting(Setting.COLOR_SCALE)?.areBoundariesUserDefined ?? false; const attribute = getSetting(Setting.ATTRIBUTE); - const valueRange = getValueRange(); + const valueRange = getDataValueRange(); const extensionLength = createValidExtensionLength( getSetting(Setting.INTERSECTION), diff --git a/frontend/src/modules/Intersection/view/utils/createIntersectionReferenceSystem.ts b/frontend/src/modules/Intersection/view/utils/createIntersectionReferenceSystem.ts index 279deaf95d..055a366453 100644 --- a/frontend/src/modules/Intersection/view/utils/createIntersectionReferenceSystem.ts +++ b/frontend/src/modules/Intersection/view/utils/createIntersectionReferenceSystem.ts @@ -5,7 +5,7 @@ import type { IntersectionPolyline } from "@framework/userCreatedItems/Intersect /** * Create an intersection reference system using 3D coordinates from a wellbore trajectory. - * * + * * The reference system is created using the wellbore trajectory's easting, northing, and tvd_msl values. * Offset is set to the first md value in the trajectory. */ @@ -21,7 +21,15 @@ export function createIntersectionReferenceSystemFromWellTrajectory( } const depthOffset = wellboreTrajectory.mdArr[0]; - const intersectionReferenceSystem = new IntersectionReferenceSystem(path); + // The normalized length is the total MD distance along the wellbore + const totalMdLength = wellboreTrajectory.mdArr[wellboreTrajectory.mdArr.length - 1] - wellboreTrajectory.mdArr[0]; + + // Note: The reference system does not get array of md-values, only the 3D coordinates. + // Thereby it internally performs linear interpolation based on the 3D coordinates only. + // This can give inaccurate results for curved wellbores, or low number of sampling points. + const intersectionReferenceSystem = new IntersectionReferenceSystem(path, { + normalizedLength: totalMdLength, + }); intersectionReferenceSystem.offset = depthOffset; return intersectionReferenceSystem; diff --git a/frontend/src/modules/ModuleSerializedStateMap.ts b/frontend/src/modules/ModuleSerializedStateMap.ts index eed55b3788..bf32335390 100644 --- a/frontend/src/modules/ModuleSerializedStateMap.ts +++ b/frontend/src/modules/ModuleSerializedStateMap.ts @@ -32,6 +32,10 @@ export type ModuleSerializedStateMap = { settings?: Partial, view?: Partial, }, + "3DViewerNew": { + settings?: never, + view?: never, + }, "DbgWorkbenchSpy": { settings?: never, view?: never, @@ -44,6 +48,14 @@ export type ModuleSerializedStateMap = { settings?: Partial, view?: never, }, + "Grid3D": { + settings?: never, + view?: never, + }, + "Grid3DIntersection": { + settings?: never, + view?: never, + }, "InplaceVolumesPlot": { settings?: Partial, view?: never, @@ -52,6 +64,10 @@ export type ModuleSerializedStateMap = { settings?: Partial, view?: never, }, + "InplaceVolumetrics": { + settings?: never, + view?: never, + }, "Intersection": { settings?: Partial, view?: never, @@ -96,6 +112,10 @@ export type ModuleSerializedStateMap = { settings?: never, view?: never, }, + "SeismicIntersection": { + settings?: never, + view?: never, + }, "SensitivityPlot": { settings?: Partial, view?: never, @@ -104,15 +124,23 @@ export type ModuleSerializedStateMap = { settings?: Partial, view?: Partial, }, + "SimulationTimeSeriesMatrix": { + settings?: never, + view?: never, + }, "SimulationTimeSeriesSensitivity": { settings?: Partial, view?: Partial, }, + "StructuralUncertaintyIntersection": { + settings?: never, + view?: never, + }, "SubsurfaceMap": { settings?: never, view?: never, }, - "TopographicMap": { + "TimeSeriesParameterDistribution": { settings?: never, view?: never, }, diff --git a/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/plots/AreaPlotProvider.ts b/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/plots/AreaPlotProvider.ts index 34d6af68b8..8705450433 100644 --- a/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/plots/AreaPlotProvider.ts +++ b/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/plots/AreaPlotProvider.ts @@ -25,7 +25,7 @@ export class AreaPlotProvider defineDependencies(args: DefineDependenciesArgs) { defineBaseContinuousDependencies(args); - args.availableSettingsUpdater(Setting.PLOT_VARIANT, () => { + args.valueRangeUpdater(Setting.PLOT_VARIANT, () => { return ["area", "gradientfill"]; }); } diff --git a/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/plots/LinearPlotProvider.ts b/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/plots/LinearPlotProvider.ts index 16af836f15..5af4e9ccf9 100644 --- a/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/plots/LinearPlotProvider.ts +++ b/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/plots/LinearPlotProvider.ts @@ -26,7 +26,7 @@ export class LinearPlotProvider defineDependencies(args: DefineDependenciesArgs) { defineBaseContinuousDependencies(args); - args.availableSettingsUpdater(Setting.PLOT_VARIANT, () => { + args.valueRangeUpdater(Setting.PLOT_VARIANT, () => { return ["line", "linestep", "dot"]; }); } diff --git a/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/plots/StackedPlotProvider.ts b/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/plots/StackedPlotProvider.ts index 469becdb6f..0e16e22c4a 100644 --- a/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/plots/StackedPlotProvider.ts +++ b/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/plots/StackedPlotProvider.ts @@ -2,7 +2,8 @@ import type { WellboreLogCurveData_api, WellboreLogCurveHeader_api } from "@api" import { WellLogCurveSourceEnum_api, WellLogCurveTypeEnum_api, getWellboreLogCurveHeadersOptions } from "@api"; import type { CustomDataProviderImplementation } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customDataProviderImplementation"; import type { DefineDependenciesArgs } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingsHandler"; -import { type MakeSettingTypesMap, Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; +import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/utils"; +import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; import { baseDiscreteSettings, @@ -33,7 +34,7 @@ export class StackedPlotProvider // Uses the same external things as the other types defineDependencies(args: DefineDependenciesArgs) { - const { availableSettingsUpdater, helperDependency } = args; + const { valueRangeUpdater, helperDependency } = args; const headerQueryDeps = [ WellLogCurveSourceEnum_api.SSDL_WELL_LOG, @@ -54,7 +55,7 @@ export class StackedPlotProvider }), ); - availableSettingsUpdater(Setting.LOG_CURVE, ({ getHelperDependency, getGlobalSetting }) => { + valueRangeUpdater(Setting.LOG_CURVE, ({ getHelperDependency, getGlobalSetting }) => { const wellboreId = getGlobalSetting("wellboreUuid"); const allHeaderData = headerQueryDeps.flatMap((dep) => getHelperDependency(dep)); diff --git a/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/plots/_shared.ts b/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/plots/_shared.ts index 81c2178903..932561c92e 100644 --- a/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/plots/_shared.ts +++ b/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/plots/_shared.ts @@ -12,10 +12,8 @@ import type { FetchDataParams, } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customDataProviderImplementation"; import type { DefineDependenciesArgs } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingsHandler"; -import type { - MakeSettingTypesMap, - Settings, -} from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; +import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/utils"; +import type { Settings } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; export const baseSettings = [Setting.LOG_CURVE] as const; @@ -32,7 +30,7 @@ export const baseDiscreteSettings = [ ] as const; export function defineBaseContinuousDependencies(args: DefineDependenciesArgs) { - const { availableSettingsUpdater, helperDependency } = args; + const { valueRangeUpdater, helperDependency } = args; const curveHeaderQueryDep = helperDependency(async ({ getGlobalSetting, abortSignal }) => { const wellboreId = getGlobalSetting("wellboreUuid"); @@ -50,7 +48,7 @@ export function defineBaseContinuousDependencies(a }); }); - availableSettingsUpdater(Setting.LOG_CURVE, ({ getHelperDependency, getGlobalSetting }) => { + valueRangeUpdater(Setting.LOG_CURVE, ({ getHelperDependency, getGlobalSetting }) => { const wellboreId = getGlobalSetting("wellboreUuid"); const headerData = getHelperDependency(curveHeaderQueryDep); @@ -63,7 +61,7 @@ export function defineBaseContinuousDependencies(a export function verifyBasePlotSettings( accessor: DataProviderInformationAccessors, ): boolean { - const availableCurves = accessor.getAvailableSettingValues(Setting.LOG_CURVE) ?? []; + const availableCurves = accessor.getSettingValueRange(Setting.LOG_CURVE) ?? []; const selectedCurve = accessor.getSetting(Setting.LOG_CURVE); return !!selectedCurve && !!availableCurves.find((curve) => curve.curveName === selectedCurve.curveName); diff --git a/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/wellpicks/WellPicksProvider.ts b/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/wellpicks/WellPicksProvider.ts index 548150f9cb..0021b8d23f 100644 --- a/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/wellpicks/WellPicksProvider.ts +++ b/frontend/src/modules/WellLogViewer/DataProviderFramework/dataProviders/wellpicks/WellPicksProvider.ts @@ -8,7 +8,8 @@ import type { FetchDataParams, } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customDataProviderImplementation"; import type { DefineDependenciesArgs } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingsHandler"; -import { type MakeSettingTypesMap, Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; +import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/utils"; +import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; export const wellPickSettings = [Setting.STRAT_COLUMN, Setting.SMDA_INTERPRETER, Setting.WELLBORE_PICKS] as const; export type WellPickSettingTypes = typeof wellPickSettings; @@ -21,7 +22,7 @@ export class WellborePicksProvider // Uses the same external things as the other types defineDependencies(args: DefineDependenciesArgs) { - const { helperDependency, availableSettingsUpdater, queryClient } = args; + const { helperDependency, valueRangeUpdater, queryClient } = args; const columnOptions = helperDependency(({ getGlobalSetting, abortSignal }) => { const wellboreUuid = getGlobalSetting("wellboreUuid"); @@ -50,14 +51,14 @@ export class WellborePicksProvider }); }); - availableSettingsUpdater(Setting.STRAT_COLUMN, ({ getHelperDependency }) => { + valueRangeUpdater(Setting.STRAT_COLUMN, ({ getHelperDependency }) => { const columns = getHelperDependency(columnOptions); if (!columns) return []; return map(columns, "identifier"); }); - availableSettingsUpdater(Setting.SMDA_INTERPRETER, ({ getHelperDependency }) => { + valueRangeUpdater(Setting.SMDA_INTERPRETER, ({ getHelperDependency }) => { const wellPicks = getHelperDependency(wellPickOptions); if (!wellPicks) return []; @@ -67,7 +68,7 @@ export class WellborePicksProvider return keys(picksByInterpreter); }); - availableSettingsUpdater(Setting.WELLBORE_PICKS, ({ getLocalSetting, getHelperDependency }) => { + valueRangeUpdater(Setting.WELLBORE_PICKS, ({ getLocalSetting, getHelperDependency }) => { const wellPicks = getHelperDependency(wellPickOptions); const interpreter = getLocalSetting(Setting.SMDA_INTERPRETER); diff --git a/frontend/src/modules/WellLogViewer/DataProviderFramework/groups/ContinuousLogTrack.ts b/frontend/src/modules/WellLogViewer/DataProviderFramework/groups/ContinuousLogTrack.ts index e582e8593e..65e7527c45 100644 --- a/frontend/src/modules/WellLogViewer/DataProviderFramework/groups/ContinuousLogTrack.ts +++ b/frontend/src/modules/WellLogViewer/DataProviderFramework/groups/ContinuousLogTrack.ts @@ -1,5 +1,5 @@ import type { CustomGroupImplementationWithSettings } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customGroupImplementation"; -import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; +import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/utils"; import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; import { baseSettings } from "./_shared"; diff --git a/frontend/src/modules/WellLogViewer/DataProviderFramework/groups/DiscreteLogTrack.ts b/frontend/src/modules/WellLogViewer/DataProviderFramework/groups/DiscreteLogTrack.ts index 223e886669..bb8deb9612 100644 --- a/frontend/src/modules/WellLogViewer/DataProviderFramework/groups/DiscreteLogTrack.ts +++ b/frontend/src/modules/WellLogViewer/DataProviderFramework/groups/DiscreteLogTrack.ts @@ -1,5 +1,5 @@ import type { CustomGroupImplementationWithSettings } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customGroupImplementation"; -import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; +import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/utils"; import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; import { baseSettings } from "./_shared"; diff --git a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/DataProviderRegistry/_registerAllSharedDataProviders.ts b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/DataProviderRegistry/_registerAllSharedDataProviders.ts index 7c488ac2e8..0bf97714ef 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/DataProviderRegistry/_registerAllSharedDataProviders.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/DataProviderRegistry/_registerAllSharedDataProviders.ts @@ -1,6 +1,6 @@ import { DataProviderType } from "../dataProviderTypes"; import { DrilledWellborePicksProvider } from "../implementations/DrilledWellborePicksProvider"; -import { DrilledWellTrajectoriesProvider } from "../implementations/DrilledWellTrajectoriesProvider"; +import { DrilledWellboreTrajectoriesProvider } from "../implementations/DrilledWellboreTrajectoriesProvider"; import { FaultPolygonsProvider } from "../implementations/FaultPolygonsProvider"; import { IntersectionRealizationGridProvider } from "../implementations/IntersectionRealizationGridProvider"; import { @@ -18,7 +18,10 @@ import { SeismicSurfaceProvider, SeismicSurfaceType } from "../implementations/s import { DataProviderRegistry } from "./_DataProviderRegistry"; DataProviderRegistry.registerDataProvider(DataProviderType.DRILLED_WELLBORE_PICKS, DrilledWellborePicksProvider); -DataProviderRegistry.registerDataProvider(DataProviderType.DRILLED_WELL_TRAJECTORIES, DrilledWellTrajectoriesProvider); +DataProviderRegistry.registerDataProvider( + DataProviderType.DRILLED_WELL_TRAJECTORIES, + DrilledWellboreTrajectoriesProvider, +); DataProviderRegistry.registerDataProvider( DataProviderType.INTERSECTION_WITH_WELLBORE_EXTENSION_REALIZATION_GRID, IntersectionRealizationGridProvider, diff --git a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/DrilledWellTrajectoriesProvider.ts b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/DrilledWellTrajectoriesProvider.ts deleted file mode 100644 index 5fed3bfdc6..0000000000 --- a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/DrilledWellTrajectoriesProvider.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { isEqual } from "lodash"; - -import type { WellboreTrajectory_api } from "@api"; -import { getDrilledWellboreHeadersOptions, getWellTrajectoriesOptions } from "@api"; -import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; -import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; - -import type { - CustomDataProviderImplementation, - FetchDataParams, -} from "../../interfacesAndTypes/customDataProviderImplementation"; -import type { DefineDependenciesArgs } from "../../interfacesAndTypes/customSettingsHandler"; - -const drilledWellTrajectoriesSettings = [Setting.ENSEMBLE, Setting.SMDA_WELLBORE_HEADERS] as const; -type DrilledWellTrajectoriesSettings = typeof drilledWellTrajectoriesSettings; -type SettingsWithTypes = MakeSettingTypesMap; - -type DrilledWellTrajectoriesData = WellboreTrajectory_api[]; - -export class DrilledWellTrajectoriesProvider - implements CustomDataProviderImplementation -{ - settings = drilledWellTrajectoriesSettings; - - getDefaultName() { - return "Well Trajectories (Official)"; - } - - doSettingsChangesRequireDataRefetch(prevSettings: SettingsWithTypes, newSettings: SettingsWithTypes): boolean { - return !isEqual(prevSettings, newSettings); - } - - fetchData({ - getSetting, - getGlobalSetting, - fetchQuery, - }: FetchDataParams< - DrilledWellTrajectoriesSettings, - DrilledWellTrajectoriesData - >): Promise { - const fieldIdentifier = getGlobalSetting("fieldId"); - const selectedWellboreHeaders = getSetting(Setting.SMDA_WELLBORE_HEADERS); - let selectedWellboreUuids: string[] = []; - if (selectedWellboreHeaders) { - selectedWellboreUuids = selectedWellboreHeaders.map((header) => header.wellboreUuid); - } - - const queryOptions = getWellTrajectoriesOptions({ - query: { field_identifier: fieldIdentifier ?? "" }, - }); - - const promise = fetchQuery({ - ...queryOptions, - staleTime: 1800000, // TODO: Both stale and gcTime are set to 30 minutes for now since SMDA is quite slow for fields with many wells - this should be adjusted later - gcTime: 1800000, - }).then((response: DrilledWellTrajectoriesData) => { - return response.filter((trajectory) => selectedWellboreUuids.includes(trajectory.wellboreUuid)); - }); - - return promise; - } - - defineDependencies({ - helperDependency, - availableSettingsUpdater, - workbenchSession, - queryClient, - }: DefineDependenciesArgs) { - availableSettingsUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { - const fieldIdentifier = getGlobalSetting("fieldId"); - const ensembles = getGlobalSetting("ensembles"); - - const ensembleIdents = ensembles - .filter((ensemble) => ensemble.getFieldIdentifier() === fieldIdentifier) - .map((ensemble) => ensemble.getIdent()); - - return ensembleIdents; - }); - - const wellboreHeadersDep = helperDependency(async function fetchData({ getLocalSetting, abortSignal }) { - const ensembleIdent = getLocalSetting(Setting.ENSEMBLE); - - if (!ensembleIdent) { - return null; - } - - const ensembleSet = workbenchSession.getEnsembleSet(); - const ensemble = ensembleSet.findEnsemble(ensembleIdent); - - if (!ensemble) { - return null; - } - - const fieldIdentifier = ensemble.getFieldIdentifier(); - - return await queryClient.fetchQuery({ - ...getDrilledWellboreHeadersOptions({ - query: { field_identifier: fieldIdentifier }, - signal: abortSignal, - }), - }); - }); - availableSettingsUpdater(Setting.SMDA_WELLBORE_HEADERS, ({ getHelperDependency }) => { - const wellboreHeaders = getHelperDependency(wellboreHeadersDep); - - if (!wellboreHeaders) { - return []; - } - - return wellboreHeaders; - }); - } -} diff --git a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/DrilledWellborePicksProvider.ts b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/DrilledWellborePicksProvider.ts index c76cb26d35..bd6b6feaf3 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/DrilledWellborePicksProvider.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/DrilledWellborePicksProvider.ts @@ -6,7 +6,6 @@ import { getWellborePickIdentifiersOptions, getWellborePicksForPickIdentifierOptions, } from "@api"; -import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; import type { @@ -15,8 +14,9 @@ import type { FetchDataParams, } from "../../interfacesAndTypes/customDataProviderImplementation"; import type { DefineDependenciesArgs } from "../../interfacesAndTypes/customSettingsHandler"; +import type { MakeSettingTypesMap } from "../../interfacesAndTypes/utils"; -const drilledWellborePicksSettings = [Setting.ENSEMBLE, Setting.SMDA_WELLBORE_HEADERS, Setting.SURFACE_NAME] as const; +const drilledWellborePicksSettings = [Setting.ENSEMBLE, Setting.WELLBORES, Setting.SURFACE_NAME] as const; export type DrilledWellborePicksSettings = typeof drilledWellborePicksSettings; export type DrilledWellborePicksData = WellborePick_api[]; @@ -41,11 +41,8 @@ export class DrilledWellborePicksProvider getGlobalSetting, fetchQuery, }: FetchDataParams): Promise { - const selectedWellboreHeaders = getSetting(Setting.SMDA_WELLBORE_HEADERS); - let selectedWellboreUuids: string[] = []; - if (selectedWellboreHeaders) { - selectedWellboreUuids = selectedWellboreHeaders.map((header) => header.wellboreUuid); - } + const selectedWellbores = getSetting(Setting.WELLBORES) ?? []; + const selectedWellboreUuids = selectedWellbores.map((wb) => wb.wellboreUuid); const selectedPickIdentifier = getSetting(Setting.SURFACE_NAME); const fieldIdentifier = getGlobalSetting("fieldId"); @@ -66,7 +63,7 @@ export class DrilledWellborePicksProvider areCurrentSettingsValid({ getSetting, }: DataProviderInformationAccessors): boolean { - const smdaWellboreHeaders = getSetting(Setting.SMDA_WELLBORE_HEADERS); + const smdaWellboreHeaders = getSetting(Setting.WELLBORES); return ( getSetting(Setting.ENSEMBLE) !== null && smdaWellboreHeaders !== null && @@ -77,11 +74,11 @@ export class DrilledWellborePicksProvider defineDependencies({ helperDependency, - availableSettingsUpdater, + valueRangeUpdater, workbenchSession, queryClient, }: DefineDependenciesArgs) { - availableSettingsUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { + valueRangeUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { const fieldIdentifier = getGlobalSetting("fieldId"); const ensembles = getGlobalSetting("ensembles"); @@ -140,7 +137,7 @@ export class DrilledWellborePicksProvider }); }); - availableSettingsUpdater(Setting.SMDA_WELLBORE_HEADERS, ({ getHelperDependency }) => { + valueRangeUpdater(Setting.WELLBORES, ({ getHelperDependency }) => { const wellboreHeaders = getHelperDependency(wellboreHeadersDep); if (!wellboreHeaders) { @@ -150,7 +147,7 @@ export class DrilledWellborePicksProvider return wellboreHeaders; }); - availableSettingsUpdater(Setting.SURFACE_NAME, ({ getHelperDependency }) => { + valueRangeUpdater(Setting.SURFACE_NAME, ({ getHelperDependency }) => { const pickIdentifiers = getHelperDependency(pickIdentifiersDep); if (!pickIdentifiers) { diff --git a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/DrilledWellboreTrajectoriesProvider.ts b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/DrilledWellboreTrajectoriesProvider.ts new file mode 100644 index 0000000000..8404f53810 --- /dev/null +++ b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/DrilledWellboreTrajectoriesProvider.ts @@ -0,0 +1,635 @@ +import type { + FormationSegment_api, + WellboreCompletion_api, + WellboreHeader_api, + WellborePerforation_api, + WellboreTrajectory_api, + WellInjectionData_api, + WellProductionData_api, + WellTrajectory_api, + WellTrajectoryFormationSegments_api, +} from "@api"; +import { + getDrilledWellboreHeadersOptions, + getFieldPerforationsOptions, + getFieldScreensOptions, + getInjectionDataOptions, + getObservedSurfacesMetadataOptions, + getProductionDataOptions, + getRealizationSurfacesMetadataOptions, + getWellTrajectoriesOptions, + postGetWellTrajectoriesFormationSegmentsOptions, + SurfaceAttributeType_api, +} from "@api"; +import { sortStringArray } from "@lib/utils/arrays"; +import { + Setting, + type SettingTypeDefinitions, +} from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; +import { SurfaceAddressBuilder } from "@modules/_shared/Surface"; +import { encodeSurfAddrStr } from "@modules/_shared/Surface/surfaceAddress"; +import { isEqual } from "lodash"; + +import { NO_UPDATE } from "../../delegates/_utils/Dependency"; +import type { + AreSettingsValidArgs, + CustomDataProviderImplementation, + FetchDataParams, +} from "../../interfacesAndTypes/customDataProviderImplementation"; +import type { DefineDependenciesArgs } from "../../interfacesAndTypes/customSettingsHandler"; +import type { MakeSettingTypesMap } from "../../interfacesAndTypes/utils"; + +const drilledWellboreTrajectoriesSettings = [ + Setting.ENSEMBLE, + Setting.WELLBORES, + Setting.WELLBORE_DEPTH_FILTER_TYPE, + Setting.MD_RANGE, + Setting.TVD_RANGE, + Setting.WELLBORE_DEPTH_FILTER_ATTRIBUTE, + Setting.WELLBORE_DEPTH_FORMATION_FILTER, + Setting.PDM_FILTER_TYPE, + Setting.TIME_INTERVAL, + Setting.PDM_FILTER, +] as const; +export type DrilledWellboreTrajectoriesSettings = typeof drilledWellboreTrajectoriesSettings; +type SettingsWithTypes = MakeSettingTypesMap; + +export type DrilledWellboreTrajectoriesData = (WellboreHeader_api & + Omit & { + formationSegments: FormationSegment_api[]; + productionData: Omit | null; + injectionData: Omit | null; + perforations: WellborePerforation_api[]; + screens: WellboreCompletion_api[]; + })[]; + +export type DrilledWellboreTrajectoriesStoredData = { + productionData: WellProductionData_api[]; + injectionData: WellInjectionData_api[]; +}; + +export class DrilledWellboreTrajectoriesProvider + implements + CustomDataProviderImplementation< + DrilledWellboreTrajectoriesSettings, + DrilledWellboreTrajectoriesData, + DrilledWellboreTrajectoriesStoredData + > +{ + settings = drilledWellboreTrajectoriesSettings; + + getDefaultName() { + return "Well Trajectories (Official)"; + } + + areCurrentSettingsValid({ + getSetting, + }: AreSettingsValidArgs< + DrilledWellboreTrajectoriesSettings, + DrilledWellboreTrajectoriesData, + DrilledWellboreTrajectoriesStoredData + >): boolean { + if (!getSetting(Setting.ENSEMBLE)) { + return false; + } + + if (getSetting(Setting.WELLBORE_DEPTH_FILTER_TYPE) === "surface_based") { + if (!getSetting(Setting.WELLBORE_DEPTH_FILTER_ATTRIBUTE)) { + return false; + } + if (!getSetting(Setting.WELLBORE_DEPTH_FORMATION_FILTER)?.topSurfaceName) { + return false; + } + if (!getSetting(Setting.WELLBORE_DEPTH_FORMATION_FILTER)?.realizationNum) { + return false; + } + } + + return true; + } + + doSettingsChangesRequireDataRefetch( + prevSettings: SettingsWithTypes | null, + newSettings: SettingsWithTypes, + ): boolean { + // Only refetch when settings used in fetchData change + // Note: TIME_INTERVAL changes trigger refetch via stored data changes + return ( + !isEqual(prevSettings?.[Setting.ENSEMBLE], newSettings[Setting.ENSEMBLE]) || + !isEqual(prevSettings?.[Setting.WELLBORES], newSettings[Setting.WELLBORES]) || + !isEqual( + prevSettings?.[Setting.WELLBORE_DEPTH_FILTER_TYPE], + newSettings[Setting.WELLBORE_DEPTH_FILTER_TYPE], + ) || + !isEqual( + prevSettings?.[Setting.WELLBORE_DEPTH_FORMATION_FILTER], + newSettings[Setting.WELLBORE_DEPTH_FORMATION_FILTER], + ) || + !isEqual( + prevSettings?.[Setting.WELLBORE_DEPTH_FILTER_ATTRIBUTE], + newSettings[Setting.WELLBORE_DEPTH_FILTER_ATTRIBUTE], + ) + ); + } + + fetchData({ + getGlobalSetting, + getSetting, + getStoredData, + fetchQuery, + }: FetchDataParams< + DrilledWellboreTrajectoriesSettings, + DrilledWellboreTrajectoriesData, + DrilledWellboreTrajectoriesStoredData + >): Promise { + const promise = (async () => { + const fieldIdentifier = getGlobalSetting("fieldId"); + const ensembleIdent = getSetting(Setting.ENSEMBLE); + const selectedWellboreHeaders = getSetting(Setting.WELLBORES); + const depthFilterType = getSetting(Setting.WELLBORE_DEPTH_FILTER_TYPE); + const productionData = getStoredData("productionData"); + const injectionData = getStoredData("injectionData"); + + const selectedWellboreUuids = selectedWellboreHeaders?.map((wb) => wb.wellboreUuid) ?? []; + + const wellTrajectoriesQueryOptions = getWellTrajectoriesOptions({ + query: { field_identifier: fieldIdentifier ?? "" }, + }); + + const allWellTrajectories = await fetchQuery({ + ...wellTrajectoriesQueryOptions, + staleTime: 1800000, // TODO: Both stale and gcTime are set to 30 minutes for now since SMDA is quite slow for fields with many wells - this should be adjusted later + gcTime: 1800000, + }); + + const filteredWellTrajectories = allWellTrajectories.filter((traj) => + selectedWellboreUuids.includes(traj.wellboreUuid), + ); + + const perforationsQueryOptions = getFieldPerforationsOptions({ + query: { field_identifier: fieldIdentifier ?? "" }, + }); + + const allPerforations = await fetchQuery({ + ...perforationsQueryOptions, + }); + + const screensQueryOptions = getFieldScreensOptions({ + query: { field_identifier: fieldIdentifier ?? "" }, + }); + + const allScreens = await fetchQuery({ + ...screensQueryOptions, + }); + + const formationFilter = getSetting(Setting.WELLBORE_DEPTH_FORMATION_FILTER); + const surfaceAttribute = getSetting(Setting.WELLBORE_DEPTH_FILTER_ATTRIBUTE); + const formationSegments: WellTrajectoryFormationSegments_api[] = []; + if ( + depthFilterType === "surface_based" && + ensembleIdent && + formationFilter && + formationFilter.topSurfaceName && + formationFilter.realizationNum != null && + surfaceAttribute + ) { + const addrBuilder = new SurfaceAddressBuilder(); + const topSurfaceAddress = addrBuilder + .withEnsembleIdent(ensembleIdent) + .withName(formationFilter.topSurfaceName) + .withAttribute(surfaceAttribute) + .withRealization(formationFilter.realizationNum) + .buildRealizationAddress(); + + let bottomSurfaceAddressString: string | null = null; + if (formationFilter.baseSurfaceName) { + const bottomSurfaceAddress = addrBuilder + .withName(formationFilter.baseSurfaceName) + .buildRealizationAddress(); + bottomSurfaceAddressString = encodeSurfAddrStr(bottomSurfaceAddress); + } + + const convertedWellTrajectories: WellTrajectory_api[] = filteredWellTrajectories.map((traj) => { + return { + uwi: traj.uniqueWellboreIdentifier, + xPoints: traj.eastingArr, + yPoints: traj.northingArr, + zPoints: traj.tvdMslArr, + mdPoints: traj.mdArr, + }; + }); + + const formationSegmentsOptions = postGetWellTrajectoriesFormationSegmentsOptions({ + query: { + top_depth_surf_addr_str: encodeSurfAddrStr(topSurfaceAddress), + bottom_depth_surf_addr_str: bottomSurfaceAddressString, + }, + body: { + well_trajectories: convertedWellTrajectories, + }, + }); + + formationSegments.push( + ...(await fetchQuery({ + ...formationSegmentsOptions, + })), + ); + } + + const result: DrilledWellboreTrajectoriesData = []; + for (const traj of filteredWellTrajectories) { + const wellboreHeader = selectedWellboreHeaders?.find( + (header) => header.wellboreUuid === traj.wellboreUuid, + ); + + if (!wellboreHeader) { + continue; + } + + result.push({ + ...traj, + ...wellboreHeader, + formationSegments: + formationSegments.find((fs) => fs.uwi === traj.uniqueWellboreIdentifier)?.formationSegments ?? + [], + productionData: productionData?.find((pd) => pd.wellboreUuid === traj.wellboreUuid) ?? null, + injectionData: injectionData?.find((id) => id.wellboreUuid === traj.wellboreUuid) ?? null, + perforations: + allPerforations?.find((perf) => perf.wellboreUuid === traj.wellboreUuid)?.perforations ?? [], + screens: allScreens?.find((screen) => screen.wellboreUuid === traj.wellboreUuid)?.completions ?? [], + }); + } + + return result; + })(); + + return promise; + } + + defineDependencies({ + helperDependency, + valueRangeUpdater, + settingAttributesUpdater, + storedDataUpdater, + workbenchSession, + queryClient, + }: DefineDependenciesArgs) { + valueRangeUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { + const fieldIdentifier = getGlobalSetting("fieldId"); + const ensembles = getGlobalSetting("ensembles"); + + const ensembleIdents = ensembles + .filter((ensemble) => ensemble.getFieldIdentifier() === fieldIdentifier) + .map((ensemble) => ensemble.getIdent()); + + return ensembleIdents; + }); + + const wellboreHeadersDep = helperDependency(async function fetchData({ getLocalSetting, abortSignal }) { + const ensembleIdent = getLocalSetting(Setting.ENSEMBLE); + + if (!ensembleIdent) { + return null; + } + + const ensembleSet = workbenchSession.getEnsembleSet(); + const ensemble = ensembleSet.findEnsemble(ensembleIdent); + + if (!ensemble) { + return null; + } + + const fieldIdentifier = ensemble.getFieldIdentifier(); + + return await queryClient.fetchQuery({ + ...getDrilledWellboreHeadersOptions({ + query: { field_identifier: fieldIdentifier }, + signal: abortSignal, + }), + }); + }); + + valueRangeUpdater(Setting.WELLBORES, ({ getHelperDependency }) => { + const wellboreHeaders = getHelperDependency(wellboreHeadersDep); + + if (!wellboreHeaders) { + return []; + } + + return wellboreHeaders; + }); + + const realizationSurfaceMetadataDep = helperDependency(async ({ getLocalSetting, abortSignal }) => { + const ensembleIdent = getLocalSetting(Setting.ENSEMBLE); + + if (!ensembleIdent) { + return null; + } + + return await queryClient.fetchQuery({ + ...getRealizationSurfacesMetadataOptions({ + query: { + case_uuid: ensembleIdent.getCaseUuid(), + ensemble_name: ensembleIdent.getEnsembleName(), + }, + signal: abortSignal, + }), + }); + }); + + settingAttributesUpdater(Setting.MD_RANGE, ({ getLocalSetting }) => { + const filterType = getLocalSetting(Setting.WELLBORE_DEPTH_FILTER_TYPE); + return { + visible: filterType === "md_range", + }; + }); + + valueRangeUpdater(Setting.MD_RANGE, ({ getHelperDependency, getLocalSetting }) => { + const data = getHelperDependency(wellboreHeadersDep); + const selectedWellboreHeaders = getLocalSetting(Setting.WELLBORES); + + if (!data || !selectedWellboreHeaders) { + return NO_UPDATE; + } + + const filteredData = data.filter((header) => + selectedWellboreHeaders.some((wb) => wb.wellboreUuid === header.wellboreUuid), + ); + + if (filteredData.length === 0) { + return [0, 0, 1]; + } + + let globalMin = Number.POSITIVE_INFINITY; + let globalMax = Number.NEGATIVE_INFINITY; + + for (const header of filteredData) { + if (header.mdMin !== null && header.mdMin !== undefined) { + globalMin = Math.min(globalMin, header.mdMin); + } + if (header.mdMax !== null && header.mdMax !== undefined) { + globalMax = Math.max(globalMax, header.mdMax); + } + } + + if (globalMin === Number.POSITIVE_INFINITY || globalMax === Number.NEGATIVE_INFINITY) { + return [0, 0, 1]; + } + + return [globalMin, globalMax, 1]; + }); + + settingAttributesUpdater(Setting.TVD_RANGE, ({ getLocalSetting }) => { + const filterType = getLocalSetting(Setting.WELLBORE_DEPTH_FILTER_TYPE); + return { + visible: filterType === "tvd_range", + }; + }); + + valueRangeUpdater(Setting.TVD_RANGE, ({ getHelperDependency, getLocalSetting }) => { + const data = getHelperDependency(wellboreHeadersDep); + const selectedWellboreHeaders = getLocalSetting(Setting.WELLBORES); + + if (!data || !selectedWellboreHeaders) { + return NO_UPDATE; + } + + const filteredData = data.filter((header) => + selectedWellboreHeaders.some((wb) => wb.wellboreUuid === header.wellboreUuid), + ); + + if (filteredData.length === 0) { + return [0, 0, 1]; + } + + let globalMin = Number.POSITIVE_INFINITY; + let globalMax = Number.NEGATIVE_INFINITY; + + for (const header of filteredData) { + if (header.tvdMin !== null && header.tvdMin !== undefined) { + globalMin = Math.min(globalMin, header.tvdMin); + } + if (header.tvdMax !== null && header.tvdMax !== undefined) { + globalMax = Math.max(globalMax, header.tvdMax); + } + } + + if (globalMin === Number.POSITIVE_INFINITY || globalMax === Number.NEGATIVE_INFINITY) { + return [0, 0, 1]; + } + + return [globalMin, globalMax, 1]; + }); + + settingAttributesUpdater(Setting.WELLBORE_DEPTH_FILTER_ATTRIBUTE, ({ getLocalSetting }) => { + const filterType = getLocalSetting(Setting.WELLBORE_DEPTH_FILTER_TYPE); + return { + visible: filterType === "surface_based", + }; + }); + + valueRangeUpdater(Setting.WELLBORE_DEPTH_FILTER_ATTRIBUTE, ({ getHelperDependency }) => { + const data = getHelperDependency(realizationSurfaceMetadataDep); + + if (!data) { + return []; + } + + const availableAttributes = [ + ...Array.from( + new Set( + data.surfaces + .filter((surface) => surface.attribute_type === SurfaceAttributeType_api.DEPTH) + .map((surface) => surface.attribute_name), + ), + ), + ]; + + return availableAttributes; + }); + + settingAttributesUpdater(Setting.WELLBORE_DEPTH_FORMATION_FILTER, ({ getLocalSetting }) => { + const filterType = getLocalSetting(Setting.WELLBORE_DEPTH_FILTER_TYPE); + return { + visible: filterType === "surface_based", + }; + }); + + valueRangeUpdater( + Setting.WELLBORE_DEPTH_FORMATION_FILTER, + ({ getLocalSetting, getGlobalSetting, getHelperDependency }) => { + const ensembleIdent = getLocalSetting(Setting.ENSEMBLE); + const realizationFilterFunc = getGlobalSetting("realizationFilterFunction"); + const attribute = getLocalSetting(Setting.WELLBORE_DEPTH_FILTER_ATTRIBUTE); + const data = getHelperDependency(realizationSurfaceMetadataDep); + + const realizationNums: number[] = []; + const surfaceNamesInStratOrder: string[] = []; + + if (ensembleIdent) { + realizationNums.push(...realizationFilterFunc(ensembleIdent)); + } + + if (attribute && data) { + const availableSurfaceNames = [ + ...Array.from( + new Set( + data.surfaces + .filter((surface) => surface.attribute_name === attribute) + .map((el) => el.name), + ), + ), + ]; + surfaceNamesInStratOrder.push( + ...sortStringArray(availableSurfaceNames, data.surface_names_in_strat_order), + ); + } + + return { + surfaceNamesInStratOrder, + realizationNums, + }; + }, + ); + + settingAttributesUpdater(Setting.TIME_INTERVAL, ({ getLocalSetting }) => { + const pdmFilterType = getLocalSetting(Setting.PDM_FILTER_TYPE); + return { + visible: pdmFilterType === "production_injection", + }; + }); + + const observedSurfaceMetadataDep = helperDependency(async ({ getLocalSetting, abortSignal }) => { + const ensembleIdent = getLocalSetting(Setting.ENSEMBLE); + + if (!ensembleIdent) { + return null; + } + + return await queryClient.fetchQuery({ + ...getObservedSurfacesMetadataOptions({ + query: { + case_uuid: ensembleIdent.getCaseUuid(), + }, + signal: abortSignal, + }), + }); + }); + + valueRangeUpdater(Setting.TIME_INTERVAL, ({ getHelperDependency }) => { + const data = getHelperDependency(observedSurfaceMetadataDep); + + if (!data) { + return []; + } + + return data.time_intervals_iso_str; + }); + + settingAttributesUpdater(Setting.PDM_FILTER, ({ getLocalSetting }) => { + const pdmFilterType = getLocalSetting(Setting.PDM_FILTER_TYPE); + return { + visible: pdmFilterType === "production_injection", + }; + }); + + const productionDataDep = helperDependency(async function fetchData({ + getGlobalSetting, + getLocalSetting, + abortSignal, + }) { + const fieldIdentifier = getGlobalSetting("fieldId"); + const timeInterval = getLocalSetting(Setting.TIME_INTERVAL); + const startDate = timeInterval ? timeInterval.split("/")[0] : undefined; + const endDate = timeInterval ? timeInterval.split("/")[1] : undefined; + if (!fieldIdentifier || !startDate || !endDate) { + return []; + } + return await queryClient.fetchQuery({ + ...getProductionDataOptions({ + query: { + field_identifier: fieldIdentifier ?? "", + start_date: startDate ?? "", + end_date: endDate ?? "", + }, + signal: abortSignal, + }), + }); + }); + + storedDataUpdater("productionData", ({ getHelperDependency }) => { + const productionData = getHelperDependency(productionDataDep); + return productionData || []; + }); + + // Injection data dependency + const injectionDataDep = helperDependency(async function fetchData({ + getGlobalSetting, + getLocalSetting, + abortSignal, + }) { + const fieldIdentifier = getGlobalSetting("fieldId"); + const timeInterval = getLocalSetting(Setting.TIME_INTERVAL); + const startDate = timeInterval ? timeInterval.split("/")[0] : undefined; + const endDate = timeInterval ? timeInterval.split("/")[1] : undefined; + if (!fieldIdentifier || !startDate || !endDate) { + return []; + } + return await queryClient.fetchQuery({ + ...getInjectionDataOptions({ + query: { + field_identifier: fieldIdentifier ?? "", + start_date: startDate ?? "", + end_date: endDate ?? "", + }, + signal: abortSignal, + }), + }); + }); + + storedDataUpdater("injectionData", ({ getHelperDependency }) => { + const injectionData = getHelperDependency(injectionDataDep); + return injectionData || []; + }); + + valueRangeUpdater(Setting.PDM_FILTER, ({ getHelperDependency }) => { + const productionData = getHelperDependency(productionDataDep); + const injectionData = getHelperDependency(injectionDataDep); + + let maxOilProduction = 0; + let maxGasProduction = 0; + let maxWaterProduction = 0; + let maxGasInjection = 0; + let maxWaterInjection = 0; + + if (productionData) { + for (const record of productionData) { + maxOilProduction = Math.max(maxOilProduction, record.oilProductionSm3); + maxGasProduction = Math.max(maxGasProduction, record.gasProductionSm3); + maxWaterProduction = Math.max(maxWaterProduction, record.waterProductionM3); + } + } + + if (injectionData) { + for (const record of injectionData) { + maxGasInjection = Math.max(maxGasInjection, record.gasInjection); + maxWaterInjection = Math.max(maxWaterInjection, record.waterInjection); + } + } + + const valueRange: SettingTypeDefinitions[Setting.PDM_FILTER]["valueRange"] = { + production: { + oil: maxOilProduction, + gas: maxGasProduction, + water: maxWaterProduction, + }, + injection: { + gas: maxGasInjection, + water: maxWaterInjection, + }, + }; + + return valueRange; + }); + } +} diff --git a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/FaultPolygonsProvider.ts b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/FaultPolygonsProvider.ts index 2360a7f54f..a684e9f893 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/FaultPolygonsProvider.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/FaultPolygonsProvider.ts @@ -8,9 +8,10 @@ import type { FetchDataParams, } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customDataProviderImplementation"; import type { DefineDependenciesArgs } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingsHandler"; -import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; +import type { MakeSettingTypesMap } from "../../interfacesAndTypes/utils"; + const realizationPolygonsSettings = [ Setting.ENSEMBLE, Setting.REALIZATION, @@ -38,10 +39,10 @@ export class FaultPolygonsProvider defineDependencies({ helperDependency, - availableSettingsUpdater, + valueRangeUpdater, queryClient, }: DefineDependenciesArgs) { - availableSettingsUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { + valueRangeUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { const fieldIdentifier = getGlobalSetting("fieldId"); const ensembles = getGlobalSetting("ensembles"); @@ -52,7 +53,7 @@ export class FaultPolygonsProvider return ensembleIdents; }); - availableSettingsUpdater(Setting.REALIZATION, ({ getLocalSetting, getGlobalSetting }) => { + valueRangeUpdater(Setting.REALIZATION, ({ getLocalSetting, getGlobalSetting }) => { const ensembleIdent = getLocalSetting(Setting.ENSEMBLE); const realizationFilterFunc = getGlobalSetting("realizationFilterFunction"); @@ -84,7 +85,7 @@ export class FaultPolygonsProvider }); }); - availableSettingsUpdater(Setting.POLYGONS_ATTRIBUTE, ({ getHelperDependency }) => { + valueRangeUpdater(Setting.POLYGONS_ATTRIBUTE, ({ getHelperDependency }) => { const data = getHelperDependency(realizationPolygonsMetadataDep); if (!data) { @@ -100,7 +101,7 @@ export class FaultPolygonsProvider return availableAttributes; }); - availableSettingsUpdater(Setting.SURFACE_NAME, ({ getHelperDependency, getLocalSetting }) => { + valueRangeUpdater(Setting.SURFACE_NAME, ({ getHelperDependency, getLocalSetting }) => { const attribute = getLocalSetting(Setting.POLYGONS_ATTRIBUTE); const data = getHelperDependency(realizationPolygonsMetadataDep); diff --git a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/IntersectionRealizationGridProvider.ts b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/IntersectionRealizationGridProvider.ts index 0570daf2c3..3a93f7fe49 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/IntersectionRealizationGridProvider.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/IntersectionRealizationGridProvider.ts @@ -4,7 +4,6 @@ import { getGridModelsInfoOptions, postGetPolylineIntersectionOptions } from "@a import { IntersectionType } from "@framework/types/intersection"; import { makeCacheBustingQueryParam } from "@framework/utils/queryUtils"; import { assertNonNull } from "@lib/utils/assertNonNull"; -import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; import type { PolylineIntersection_trans } from "@modules/_shared/Intersection/gridIntersectionTransform"; import { transformPolylineIntersection } from "@modules/_shared/Intersection/gridIntersectionTransform"; @@ -16,6 +15,7 @@ import type { FetchDataParams, } from "../../interfacesAndTypes/customDataProviderImplementation"; import type { DefineDependenciesArgs } from "../../interfacesAndTypes/customSettingsHandler"; +import type { MakeSettingTypesMap } from "../../interfacesAndTypes/utils"; import { createIntersectionPolylineWithSectionLengthsForField, fetchWellboreHeaders, @@ -143,7 +143,7 @@ export class IntersectionRealizationGridProvider defineDependencies({ helperDependency, - availableSettingsUpdater, + valueRangeUpdater, settingAttributesUpdater, storedDataUpdater, queryClient, @@ -161,13 +161,13 @@ export class IntersectionRealizationGridProvider return { enabled: isEnabled, visible: true }; }); - availableSettingsUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { + valueRangeUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { const fieldIdentifier = getGlobalSetting("fieldId"); const ensembles = getGlobalSetting("ensembles"); return getAvailableEnsembleIdentsForField(fieldIdentifier, ensembles); }); - availableSettingsUpdater(Setting.REALIZATION, ({ getLocalSetting, getGlobalSetting }) => { + valueRangeUpdater(Setting.REALIZATION, ({ getLocalSetting, getGlobalSetting }) => { const ensembleIdent = getLocalSetting(Setting.ENSEMBLE); const realizationFilterFunc = getGlobalSetting("realizationFilterFunction"); return getAvailableRealizationsForEnsembleIdent(ensembleIdent, realizationFilterFunc); @@ -194,7 +194,7 @@ export class IntersectionRealizationGridProvider }); }); - availableSettingsUpdater(Setting.GRID_NAME, ({ getHelperDependency }) => { + valueRangeUpdater(Setting.GRID_NAME, ({ getHelperDependency }) => { const data = getHelperDependency(realizationGridDataDep); if (!data) { @@ -206,7 +206,7 @@ export class IntersectionRealizationGridProvider return availableGridNames; }); - availableSettingsUpdater(Setting.ATTRIBUTE, ({ getLocalSetting, getHelperDependency }) => { + valueRangeUpdater(Setting.ATTRIBUTE, ({ getLocalSetting, getHelperDependency }) => { const gridName = getLocalSetting(Setting.GRID_NAME); const data = getHelperDependency(realizationGridDataDep); @@ -229,7 +229,7 @@ export class IntersectionRealizationGridProvider return fetchWellboreHeaders(ensembleIdent, abortSignal, workbenchSession, queryClient); }); - availableSettingsUpdater(Setting.INTERSECTION, ({ getHelperDependency, getGlobalSetting }) => { + valueRangeUpdater(Setting.INTERSECTION, ({ getHelperDependency, getGlobalSetting }) => { const wellboreHeaders = getHelperDependency(wellboreHeadersDep) ?? []; const intersectionPolylines = getGlobalSetting("intersectionPolylines"); const fieldIdentifier = getGlobalSetting("fieldId"); @@ -241,7 +241,7 @@ export class IntersectionRealizationGridProvider return getAvailableIntersectionOptions(wellboreHeaders, fieldIntersectionPolylines); }); - availableSettingsUpdater(Setting.TIME_OR_INTERVAL, ({ getLocalSetting, getHelperDependency }) => { + valueRangeUpdater(Setting.TIME_OR_INTERVAL, ({ getLocalSetting, getHelperDependency }) => { const gridName = getLocalSetting(Setting.GRID_NAME); const gridAttribute = getLocalSetting(Setting.ATTRIBUTE); const data = getHelperDependency(realizationGridDataDep); diff --git a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/IntersectionRealizationSeismicProvider.ts b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/IntersectionRealizationSeismicProvider.ts index 0b18e8ca9c..77dc9e1657 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/IntersectionRealizationSeismicProvider.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/IntersectionRealizationSeismicProvider.ts @@ -12,7 +12,6 @@ import type { SeismicFenceData_trans } from "@modules/_shared/Intersection/seism import { transformSeismicFenceData } from "@modules/_shared/Intersection/seismicIntersectionTransform"; import { createSeismicFencePolylineFromPolylineXy } from "@modules/_shared/Intersection/seismicIntersectionUtils"; -import type { MakeSettingTypesMap } from "../../../DataProviderFramework/settings/settingsDefinitions"; import { Setting } from "../../../DataProviderFramework/settings/settingsDefinitions"; import type { CustomDataProviderImplementation, @@ -20,6 +19,7 @@ import type { FetchDataParams, } from "../../interfacesAndTypes/customDataProviderImplementation"; import type { DefineDependenciesArgs } from "../../interfacesAndTypes/customSettingsHandler"; +import type { MakeSettingTypesMap } from "../../interfacesAndTypes/utils"; import { createIntersectionPolylineWithSectionLengthsForField, fetchWellboreHeaders, @@ -165,7 +165,7 @@ export class IntersectionRealizationSeismicProvider defineDependencies({ helperDependency, - availableSettingsUpdater, + valueRangeUpdater, settingAttributesUpdater, queryClient, workbenchSession, @@ -178,13 +178,13 @@ export class IntersectionRealizationSeismicProvider return { enabled: isEnabled }; }); - availableSettingsUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { + valueRangeUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { const fieldIdentifier = getGlobalSetting("fieldId"); const ensembles = getGlobalSetting("ensembles"); return getAvailableEnsembleIdentsForField(fieldIdentifier, ensembles); }); - availableSettingsUpdater(Setting.REALIZATION, ({ getLocalSetting, getGlobalSetting }) => { + valueRangeUpdater(Setting.REALIZATION, ({ getLocalSetting, getGlobalSetting }) => { const ensembleIdent = getLocalSetting(Setting.ENSEMBLE); const realizationFilterFunc = getGlobalSetting("realizationFilterFunction"); return getAvailableRealizationsForEnsembleIdent(ensembleIdent, realizationFilterFunc); @@ -210,7 +210,7 @@ export class IntersectionRealizationSeismicProvider }); }); - availableSettingsUpdater(Setting.ATTRIBUTE, ({ getHelperDependency }) => { + valueRangeUpdater(Setting.ATTRIBUTE, ({ getHelperDependency }) => { const seismicCubeMetaList = getHelperDependency(ensembleSeismicCubeMetaListDep); if (!seismicCubeMetaList) { @@ -235,7 +235,7 @@ export class IntersectionRealizationSeismicProvider return fetchWellboreHeaders(ensembleIdent, abortSignal, workbenchSession, queryClient); }); - availableSettingsUpdater(Setting.INTERSECTION, ({ getHelperDependency, getGlobalSetting }) => { + valueRangeUpdater(Setting.INTERSECTION, ({ getHelperDependency, getGlobalSetting }) => { const wellboreHeaders = getHelperDependency(wellboreHeadersDep) ?? []; const intersectionPolylines = getGlobalSetting("intersectionPolylines"); const fieldIdentifier = getGlobalSetting("fieldId"); @@ -247,7 +247,7 @@ export class IntersectionRealizationSeismicProvider return getAvailableIntersectionOptions(wellboreHeaders, fieldIntersectionPolylines); }); - availableSettingsUpdater(Setting.TIME_OR_INTERVAL, ({ getLocalSetting, getHelperDependency }) => { + valueRangeUpdater(Setting.TIME_OR_INTERVAL, ({ getLocalSetting, getHelperDependency }) => { const seismicCubeMetaList = getHelperDependency(ensembleSeismicCubeMetaListDep); const seismicAttribute = getLocalSetting(Setting.ATTRIBUTE); diff --git a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/RealizationPolygonsProvider.ts b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/RealizationPolygonsProvider.ts index 74ea20ede7..7e52c88ce3 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/RealizationPolygonsProvider.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/RealizationPolygonsProvider.ts @@ -8,9 +8,10 @@ import type { FetchDataParams, } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customDataProviderImplementation"; import type { DefineDependenciesArgs } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingsHandler"; -import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; +import type { MakeSettingTypesMap } from "../../interfacesAndTypes/utils"; + const realizationPolygonsSettings = [ Setting.ENSEMBLE, Setting.REALIZATION, @@ -38,10 +39,10 @@ export class RealizationPolygonsProvider defineDependencies({ helperDependency, - availableSettingsUpdater, + valueRangeUpdater, queryClient, }: DefineDependenciesArgs) { - availableSettingsUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { + valueRangeUpdater(Setting.ENSEMBLE, ({ getGlobalSetting }) => { const fieldIdentifier = getGlobalSetting("fieldId"); const ensembles = getGlobalSetting("ensembles"); @@ -52,7 +53,7 @@ export class RealizationPolygonsProvider return ensembleIdents; }); - availableSettingsUpdater(Setting.REALIZATION, ({ getLocalSetting, getGlobalSetting }) => { + valueRangeUpdater(Setting.REALIZATION, ({ getLocalSetting, getGlobalSetting }) => { const ensembleIdent = getLocalSetting(Setting.ENSEMBLE); const realizationFilterFunc = getGlobalSetting("realizationFilterFunction"); @@ -84,7 +85,7 @@ export class RealizationPolygonsProvider }); }); - availableSettingsUpdater(Setting.POLYGONS_ATTRIBUTE, ({ getHelperDependency }) => { + valueRangeUpdater(Setting.POLYGONS_ATTRIBUTE, ({ getHelperDependency }) => { const data = getHelperDependency(realizationPolygonsMetadataDep); if (!data) { @@ -100,7 +101,7 @@ export class RealizationPolygonsProvider return availableAttributes; }); - availableSettingsUpdater(Setting.POLYGONS_NAME, ({ getHelperDependency, getLocalSetting }) => { + valueRangeUpdater(Setting.POLYGONS_NAME, ({ getHelperDependency, getLocalSetting }) => { const attribute = getLocalSetting(Setting.POLYGONS_ATTRIBUTE); const data = getHelperDependency(realizationPolygonsMetadataDep); diff --git a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/WellboreTrajectoriesProvider.ts b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/WellboreTrajectoriesProvider.ts new file mode 100644 index 0000000000..6c7eaa9baf --- /dev/null +++ b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/WellboreTrajectoriesProvider.ts @@ -0,0 +1,34 @@ +import type { WellboreTrajectory_api } from "@api"; +import type { MakeSettingTypesMap } from "../../interfacesAndTypes/utils"; +import { Setting } from "../../settings/settingsDefinitions"; +import type { CustomDataProviderImplementation } from "../../interfacesAndTypes/customDataProviderImplementation"; +import isEqual from "lodash-es/isEqual"; + +const WELL_TRAJECTORIES_SETTINGS = [Setting.ENSEMBLE]; +type WellTrajectoriesSettings = typeof WELL_TRAJECTORIES_SETTINGS; +type SettingsWithTypes = MakeSettingTypesMap; + +type WellboreTrajectoriesData = WellboreTrajectory_api[]; + +export class WellboreTrajectoriesProvider + implements CustomDataProviderImplementation +{ + settings = WELL_TRAJECTORIES_SETTINGS; + + getDefaultName() { + return "Well Trajectories"; + } + + doSettingsChangesRequireDataRefetch(prevSettings: SettingsWithTypes, newSettings: SettingsWithTypes): boolean { + return !isEqual(prevSettings, newSettings); + } + + fetchData(): Promise { + // This data provider does not fetch any data itself, it is only used to provide a common interface for settings + return Promise.resolve([]); + } + + defineDependencies() { + return []; + } +} diff --git a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/surfaceProviders/AttributeSurfaceProvider.ts b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/surfaceProviders/AttributeSurfaceProvider.ts index 3c7ec716b2..e2c281562d 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/surfaceProviders/AttributeSurfaceProvider.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/surfaceProviders/AttributeSurfaceProvider.ts @@ -21,7 +21,7 @@ import type { FetchDataParams, } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customDataProviderImplementation"; import type { DefineDependenciesArgs } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingsHandler"; -import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; +import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/utils"; import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; import { SurfaceAddressBuilder } from "@modules/_shared/Surface"; import { transformSurfaceData } from "@modules/_shared/Surface/queryDataTransforms"; @@ -126,7 +126,7 @@ export class AttributeSurfaceProvider } defineDependencies({ helperDependency, - availableSettingsUpdater, + valueRangeUpdater, settingAttributesUpdater, storedDataUpdater, workbenchSession, @@ -159,12 +159,12 @@ export class AttributeSurfaceProvider } return { enabled: false, visible: false }; }); - availableSettingsUpdater(Setting.REPRESENTATION, () => { + valueRangeUpdater(Setting.REPRESENTATION, () => { return [Representation.REALIZATION, Representation.ENSEMBLE_STATISTICS]; }); - availableSettingsUpdater(Setting.STATISTIC_FUNCTION, createStatisticFunctionUpdater()); - availableSettingsUpdater(Setting.ENSEMBLE, createEnsembleUpdater()); - availableSettingsUpdater(Setting.SENSITIVITY, createSensitivityUpdater(workbenchSession)); + valueRangeUpdater(Setting.STATISTIC_FUNCTION, createStatisticFunctionUpdater()); + valueRangeUpdater(Setting.ENSEMBLE, createEnsembleUpdater()); + valueRangeUpdater(Setting.SENSITIVITY, createSensitivityUpdater(workbenchSession)); const surfaceMetadataDep = helperDependency(async ({ getLocalSetting, abortSignal }) => { const ensembleIdent = getLocalSetting(Setting.ENSEMBLE); @@ -184,8 +184,8 @@ export class AttributeSurfaceProvider }), }); }); - availableSettingsUpdater(Setting.REALIZATION, createRealizationUpdater()); - availableSettingsUpdater(Setting.ATTRIBUTE, ({ getHelperDependency }) => { + valueRangeUpdater(Setting.REALIZATION, createRealizationUpdater()); + valueRangeUpdater(Setting.ATTRIBUTE, ({ getHelperDependency }) => { const data = getHelperDependency(surfaceMetadataDep); if (!data) { @@ -218,7 +218,7 @@ export class AttributeSurfaceProvider return availableAttributes; }); - availableSettingsUpdater(Setting.FORMATION_NAME, ({ getHelperDependency, getLocalSetting }) => { + valueRangeUpdater(Setting.FORMATION_NAME, ({ getHelperDependency, getLocalSetting }) => { const attribute = getLocalSetting(Setting.ATTRIBUTE); const data = getHelperDependency(surfaceMetadataDep); @@ -236,7 +236,7 @@ export class AttributeSurfaceProvider return sortStringArray(availableSurfaceNames, data.surface_names_in_strat_order); }); - availableSettingsUpdater(Setting.TIME_POINT, ({ getLocalSetting, getHelperDependency }) => { + valueRangeUpdater(Setting.TIME_POINT, ({ getLocalSetting, getHelperDependency }) => { const attribute = getLocalSetting(Setting.ATTRIBUTE); const formationName = getLocalSetting(Setting.FORMATION_NAME); const data = getHelperDependency(surfaceMetadataDep); @@ -251,7 +251,7 @@ export class AttributeSurfaceProvider return [SurfaceTimeType_api.NO_TIME]; }); - availableSettingsUpdater(Setting.TIME_INTERVAL, ({ getLocalSetting, getHelperDependency }) => { + valueRangeUpdater(Setting.TIME_INTERVAL, ({ getLocalSetting, getHelperDependency }) => { const attribute = getLocalSetting(Setting.ATTRIBUTE); const formationName = getLocalSetting(Setting.FORMATION_NAME); const data = getHelperDependency(surfaceMetadataDep); diff --git a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/surfaceProviders/DepthSurfaceProvider.ts b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/surfaceProviders/DepthSurfaceProvider.ts index a60b85b6a2..9b1e4c747f 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/surfaceProviders/DepthSurfaceProvider.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/surfaceProviders/DepthSurfaceProvider.ts @@ -20,12 +20,13 @@ import type { FetchDataParams, } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customDataProviderImplementation"; import type { DefineDependenciesArgs } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingsHandler"; -import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; +import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/utils"; import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; import { SurfaceAddressBuilder, type FullSurfaceAddress } from "@modules/_shared/Surface"; import { transformSurfaceData } from "@modules/_shared/Surface/queryDataTransforms"; import { encodeSurfAddrStr } from "@modules/_shared/Surface/surfaceAddress"; + import { Representation } from "../../../settings/implementations/RepresentationSetting"; import { @@ -94,7 +95,7 @@ export class DepthSurfaceProvider defineDependencies({ helperDependency, - availableSettingsUpdater, + valueRangeUpdater, settingAttributesUpdater, storedDataUpdater, workbenchSession, @@ -116,12 +117,12 @@ export class DepthSurfaceProvider return { enabled, visible: enabled }; }); - availableSettingsUpdater(Setting.REPRESENTATION, () => { + valueRangeUpdater(Setting.REPRESENTATION, () => { return [Representation.REALIZATION, Representation.ENSEMBLE_STATISTICS]; }); - availableSettingsUpdater(Setting.STATISTIC_FUNCTION, createStatisticFunctionUpdater()); - availableSettingsUpdater(Setting.ENSEMBLE, createEnsembleUpdater()); - availableSettingsUpdater(Setting.SENSITIVITY, createSensitivityUpdater(workbenchSession)); + valueRangeUpdater(Setting.STATISTIC_FUNCTION, createStatisticFunctionUpdater()); + valueRangeUpdater(Setting.ENSEMBLE, createEnsembleUpdater()); + valueRangeUpdater(Setting.SENSITIVITY, createSensitivityUpdater(workbenchSession)); const surfaceMetadataDep = helperDependency(async ({ getLocalSetting, abortSignal }) => { const ensembleIdent = getLocalSetting(Setting.ENSEMBLE); @@ -141,8 +142,8 @@ export class DepthSurfaceProvider }), }); }); - availableSettingsUpdater(Setting.REALIZATION, createRealizationUpdater()); - availableSettingsUpdater(Setting.DEPTH_ATTRIBUTE, ({ getHelperDependency }) => { + valueRangeUpdater(Setting.REALIZATION, createRealizationUpdater()); + valueRangeUpdater(Setting.DEPTH_ATTRIBUTE, ({ getHelperDependency }) => { const data = getHelperDependency(surfaceMetadataDep); if (!data) { @@ -160,7 +161,7 @@ export class DepthSurfaceProvider return availableAttributes; }); - availableSettingsUpdater(Setting.SURFACE_NAME, ({ getHelperDependency, getLocalSetting }) => { + valueRangeUpdater(Setting.SURFACE_NAME, ({ getHelperDependency, getLocalSetting }) => { const attribute = getLocalSetting(Setting.DEPTH_ATTRIBUTE); const data = getHelperDependency(surfaceMetadataDep); diff --git a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/surfaceProviders/SeismicSurfaceProvider.ts b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/surfaceProviders/SeismicSurfaceProvider.ts index 523872396b..42a503fa3d 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/surfaceProviders/SeismicSurfaceProvider.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/surfaceProviders/SeismicSurfaceProvider.ts @@ -22,12 +22,13 @@ import type { FetchDataParams, } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customDataProviderImplementation"; import type { DefineDependenciesArgs } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingsHandler"; -import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; +import type { MakeSettingTypesMap } from "@modules/_shared/DataProviderFramework/interfacesAndTypes/utils"; import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; import { SurfaceAddressBuilder, type FullSurfaceAddress } from "@modules/_shared/Surface"; import { transformSurfaceData } from "@modules/_shared/Surface/queryDataTransforms"; import { encodeSurfAddrStr } from "@modules/_shared/Surface/surfaceAddress"; + import { Representation } from "../../../settings/implementations/RepresentationSetting"; import { @@ -113,7 +114,7 @@ export class SeismicSurfaceProvider } defineDependencies({ helperDependency, - availableSettingsUpdater, + valueRangeUpdater, settingAttributesUpdater, storedDataUpdater, workbenchSession, @@ -146,7 +147,7 @@ export class SeismicSurfaceProvider } return { enabled: false, visible: false }; }); - availableSettingsUpdater(Setting.REPRESENTATION, () => { + valueRangeUpdater(Setting.REPRESENTATION, () => { if ( this._surfaceType === SeismicSurfaceType.SEISMIC_SURVEY || this._surfaceType === SeismicSurfaceType.SEISMIC_TIME_LAPSE @@ -155,9 +156,9 @@ export class SeismicSurfaceProvider } return [Representation.REALIZATION, Representation.ENSEMBLE_STATISTICS]; }); - availableSettingsUpdater(Setting.STATISTIC_FUNCTION, createStatisticFunctionUpdater()); - availableSettingsUpdater(Setting.ENSEMBLE, createEnsembleUpdater()); - availableSettingsUpdater(Setting.SENSITIVITY, createSensitivityUpdater(workbenchSession)); + valueRangeUpdater(Setting.STATISTIC_FUNCTION, createStatisticFunctionUpdater()); + valueRangeUpdater(Setting.ENSEMBLE, createEnsembleUpdater()); + valueRangeUpdater(Setting.SENSITIVITY, createSensitivityUpdater(workbenchSession)); const surfaceMetadataDep = helperDependency(async ({ getLocalSetting, abortSignal }) => { const ensembleIdent = getLocalSetting(Setting.ENSEMBLE); @@ -191,8 +192,8 @@ export class SeismicSurfaceProvider }), }); }); - availableSettingsUpdater(Setting.REALIZATION, createRealizationUpdater()); - availableSettingsUpdater(Setting.SEISMIC_ATTRIBUTE, ({ getHelperDependency }) => { + valueRangeUpdater(Setting.REALIZATION, createRealizationUpdater()); + valueRangeUpdater(Setting.SEISMIC_ATTRIBUTE, ({ getHelperDependency }) => { const data = getHelperDependency(surfaceMetadataDep); if (!data) { @@ -220,7 +221,7 @@ export class SeismicSurfaceProvider return availableAttributes; }); - availableSettingsUpdater(Setting.FORMATION_NAME, ({ getHelperDependency, getLocalSetting }) => { + valueRangeUpdater(Setting.FORMATION_NAME, ({ getHelperDependency, getLocalSetting }) => { const attribute = getLocalSetting(Setting.SEISMIC_ATTRIBUTE); const data = getHelperDependency(surfaceMetadataDep); @@ -238,7 +239,7 @@ export class SeismicSurfaceProvider return sortStringArray(availableSurfaceNames, data.surface_names_in_strat_order); }); - availableSettingsUpdater(Setting.TIME_POINT, ({ getLocalSetting, getHelperDependency }) => { + valueRangeUpdater(Setting.TIME_POINT, ({ getLocalSetting, getHelperDependency }) => { const attribute = getLocalSetting(Setting.SEISMIC_ATTRIBUTE); const surfaceName = getLocalSetting(Setting.FORMATION_NAME); const data = getHelperDependency(surfaceMetadataDep); @@ -253,7 +254,7 @@ export class SeismicSurfaceProvider return [SurfaceTimeType_api.NO_TIME]; }); - availableSettingsUpdater(Setting.TIME_INTERVAL, ({ getLocalSetting, getHelperDependency }) => { + valueRangeUpdater(Setting.TIME_INTERVAL, ({ getLocalSetting, getHelperDependency }) => { const attribute = getLocalSetting(Setting.SEISMIC_ATTRIBUTE); const surfaceName = getLocalSetting(Setting.FORMATION_NAME); const data = getHelperDependency(surfaceMetadataDep); diff --git a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/surfaceProviders/_commonSettingsUpdaters.ts b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/surfaceProviders/_commonSettingsUpdaters.ts index 7e1a5e103b..697578872f 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/surfaceProviders/_commonSettingsUpdaters.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/dataProviders/implementations/surfaceProviders/_commonSettingsUpdaters.ts @@ -7,7 +7,7 @@ import type { SensitivityNameCasePair } from "@modules/_shared/DataProviderFrame import { Setting } from "@modules/_shared/DataProviderFramework/settings/settingsDefinitions"; /** - * Creates an availableSettingsUpdater for Setting.ENSEMBLE that filters ensembles by the current field. + * Creates an valueRangeUpdater for Setting.ENSEMBLE that filters ensembles by the current field. */ export function createEnsembleUpdater() { return ({ getGlobalSetting }: any) => { @@ -23,7 +23,7 @@ export function createEnsembleUpdater() { } /** - * Creates an availableSettingsUpdater for Setting.SENSITIVITY that returns sensitivity name/case pairs + * Creates an valueRangeUpdater for Setting.SENSITIVITY that returns sensitivity name/case pairs * for the selected ensemble. */ export function createSensitivityUpdater(workbenchSession: WorkbenchSession) { @@ -54,7 +54,7 @@ export function createSensitivityUpdater(workbenchSession: WorkbenchSession) { } /** - * Creates an availableSettingsUpdater for Setting.REALIZATION that returns filtered realizations + * Creates an valueRangeUpdater for Setting.REALIZATION that returns filtered realizations * for the selected ensemble. */ export function createRealizationUpdater() { @@ -73,7 +73,7 @@ export function createRealizationUpdater() { } /** - * Creates an availableSettingsUpdater for Setting.STATISTIC_FUNCTION that returns all available + * Creates an valueRangeUpdater for Setting.STATISTIC_FUNCTION that returns all available * surface statistic functions. */ export function createStatisticFunctionUpdater() { diff --git a/frontend/src/modules/_shared/DataProviderFramework/delegates/SettingsContextDelegate.ts b/frontend/src/modules/_shared/DataProviderFramework/delegates/SettingsContextDelegate.ts index e37ac96558..27793bbf60 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/delegates/SettingsContextDelegate.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/delegates/SettingsContextDelegate.ts @@ -12,8 +12,8 @@ import { SettingTopic } from "../framework/SettingManager/SettingManager"; import type { CustomSettingsHandler, SettingAttributes, UpdateFunc } from "../interfacesAndTypes/customSettingsHandler"; import type { SerializedSettingsState } from "../interfacesAndTypes/serialization"; import type { NullableStoredData, StoredData } from "../interfacesAndTypes/sharedTypes"; -import type { AvailableValuesType, SettingsKeysFromTuple } from "../interfacesAndTypes/utils"; -import type { MakeSettingTypesMap, SettingTypes, Settings } from "../settings/settingsDefinitions"; +import type { MakeSettingTypesMap, SettingsKeysFromTuple } from "../interfacesAndTypes/utils"; +import type { Settings, SettingTypeDefinitions } from "../settings/settingsDefinitions"; import { Dependency } from "./_utils/Dependency"; @@ -65,8 +65,10 @@ export class SettingsContextDelegate< TStoredDataKey >; private _dataProviderManager: DataProviderManager; - private _settings: { [K in TSettingKey]: SettingManager } = {} as { - [K in TSettingKey]: SettingManager; + private _settings: { + [K in TSettingKey]: SettingManager; + } = {} as { + [K in TSettingKey]: SettingManager; }; private _publishSubscribeDelegate = new PublishSubscribeDelegate(); private _unsubscribeFunctionsManagerDelegate: UnsubscribeFunctionsManagerDelegate = @@ -222,9 +224,9 @@ export class SettingsContextDelegate< return invalidSettings; } - setAvailableValues(key: K, availableValues: AvailableValuesType): void { + setValueRange(key: K, valueRange: SettingTypeDefinitions[K]["valueRange"]): void { const settingDelegate = this._settings[key]; - settingDelegate.setAvailableValues(availableValues); + settingDelegate.setValueRange(valueRange); } setStoredData(key: K, data: TStoredData[K] | null): void { @@ -359,11 +361,16 @@ export class SettingsContextDelegate< return this.getDataProviderManager.bind(this)().getGlobalSetting(key); }; - const availableSettingsUpdater = ( + const valueRangeUpdater = ( settingKey: K, - updateFunc: UpdateFunc, TSettings, TSettingTypes, TSettingKey>, - ): Dependency, TSettings, TSettingTypes, TSettingKey> => { - const dependency = new Dependency, TSettings, TSettingTypes, TSettingKey>( + updateFunc: UpdateFunc, + ): Dependency => { + const dependency = new Dependency< + SettingTypeDefinitions[K]["valueRange"], + TSettings, + TSettingTypes, + TSettingKey + >( localSettingManagerGetter, globalSettingGetter, updateFunc, @@ -373,12 +380,12 @@ export class SettingsContextDelegate< ); this._dependencies.push(dependency); - dependency.subscribe((availableValues) => { - if (availableValues === null) { - this.setAvailableValues(settingKey, [] as unknown as AvailableValuesType); + dependency.subscribe((valueRange) => { + if (valueRange === null) { + this.setValueRange(settingKey, null as SettingTypeDefinitions[K]["valueRange"]); return; } - this.setAvailableValues(settingKey, availableValues); + this.setValueRange(settingKey, valueRange); this.handleSettingChanged(); }); @@ -415,6 +422,10 @@ export class SettingsContextDelegate< this._settings[settingKey].updateAttributes(attributes); }); + dependency.subscribeLoading(() => { + this.handleSettingChanged(); + }); + dependency.initialize(); return dependency; @@ -462,7 +473,7 @@ export class SettingsContextDelegate< getGlobalSetting: (settingName: T) => GlobalSettings[T]; getHelperDependency: ( dep: Dependency, - ) => TDep | null; + ) => Awaited | null; abortSignal: AbortSignal; }) => T, ) => { @@ -487,7 +498,7 @@ export class SettingsContextDelegate< if (this._customSettingsHandler.defineDependencies) { this._customSettingsHandler.defineDependencies({ - availableSettingsUpdater: availableSettingsUpdater.bind(this), + valueRangeUpdater: valueRangeUpdater.bind(this), settingAttributesUpdater: settingAttributesUpdater.bind(this), storedDataUpdater: storedDataUpdater.bind(this), helperDependency: helperDependency.bind(this), @@ -507,7 +518,9 @@ export class SettingsContextDelegate< for (const key in this._settings) { this._settings[key].beforeDestroy(); } - this._settings = {} as { [K in TSettingKey]: SettingManager }; + this._settings = {} as { + [K in TSettingKey]: SettingManager; + }; } private setStatus(status: SettingsContextStatus) { diff --git a/frontend/src/modules/_shared/DataProviderFramework/delegates/SharedSettingsDelegate.ts b/frontend/src/modules/_shared/DataProviderFramework/delegates/SharedSettingsDelegate.ts index 4def8c5761..e10a1881d4 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/delegates/SharedSettingsDelegate.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/delegates/SharedSettingsDelegate.ts @@ -10,15 +10,15 @@ import type { } from "../interfacesAndTypes/customSettingsHandler"; import type { Item } from "../interfacesAndTypes/entities"; import type { SerializedSettingsState } from "../interfacesAndTypes/serialization"; -import type { SettingsKeysFromTuple } from "../interfacesAndTypes/utils"; -import type { MakeSettingTypesMap, SettingTypes, Settings } from "../settings/settingsDefinitions"; +import type { MakeSettingTypesMap, SettingsKeysFromTuple } from "../interfacesAndTypes/utils"; +import type { Settings } from "../settings/settingsDefinitions"; import { Dependency } from "./_utils/Dependency"; export class SharedSettingsDelegate< TSettings extends Settings, TSettingTypes extends MakeSettingTypesMap = MakeSettingTypesMap, - TSettingKey extends SettingsKeysFromTuple = SettingsKeysFromTuple + TSettingKey extends SettingsKeysFromTuple = SettingsKeysFromTuple, > { private _externalSettingControllers: { [K in TSettingKey]: ExternalSettingController } = {} as { [K in TSettingKey]: ExternalSettingController; @@ -38,8 +38,8 @@ export class SharedSettingsDelegate< parentItem: Item, wrappedSettings: { [K in TSettingKey]: SettingManager }, customDependenciesDefinition?: ( - args: DefineBasicDependenciesArgs - ) => void + args: DefineBasicDependenciesArgs, + ) => void, ) { this._wrappedSettings = wrappedSettings; this._parentItem = parentItem; @@ -59,7 +59,7 @@ export class SharedSettingsDelegate< this.createDependencies(); } - getWrappedSettings(): { [K in TSettingKey]: SettingManager } { + getWrappedSettings(): { [K in TSettingKey]: SettingManager } { return this._wrappedSettings; } @@ -118,8 +118,8 @@ export class SharedSettingsDelegate< this._unsubscribeFunctionsManagerDelegate.registerUnsubscribeFunction( "dependencies", this._wrappedSettings[key].getPublishSubscribeDelegate().makeSubscriberFunction(SettingTopic.VALUE)( - handleChange - ) + handleChange, + ), ); this._unsubscribeFunctionsManagerDelegate.registerUnsubscribeFunction( @@ -130,21 +130,21 @@ export class SharedSettingsDelegate< if (!this._wrappedSettings[key].isLoading()) { handleChange(); } - }) + }), ); this._unsubscribeFunctionsManagerDelegate.registerUnsubscribeFunction( "dependencies", this._wrappedSettings[key] .getPublishSubscribeDelegate() - .makeSubscriberFunction(SettingTopic.IS_PERSISTED)(handleChange) + .makeSubscriberFunction(SettingTopic.IS_PERSISTED)(handleChange), ); this._unsubscribeFunctionsManagerDelegate.registerUnsubscribeFunction( "dependencies", this._wrappedSettings[key] .getPublishSubscribeDelegate() - .makeSubscriberFunction(SettingTopic.IS_INITIALIZED)(handleChange) + .makeSubscriberFunction(SettingTopic.IS_INITIALIZED)(handleChange), ); return handleChange; @@ -152,7 +152,7 @@ export class SharedSettingsDelegate< const makeGlobalSettingGetter = ( key: K, - handler: (value: GlobalSettings[K] | null) => void + handler: (value: GlobalSettings[K] | null) => void, ) => { const handleChange = (): void => { handler(this._parentItem.getItemDelegate().getDataProviderManager().getGlobalSetting(key)); @@ -163,7 +163,7 @@ export class SharedSettingsDelegate< .getItemDelegate() .getDataProviderManager() .getPublishSubscribeDelegate() - .makeSubscriberFunction(DataProviderManagerTopic.GLOBAL_SETTINGS)(handleChange) + .makeSubscriberFunction(DataProviderManagerTopic.GLOBAL_SETTINGS)(handleChange), ); return handleChange.bind(this); @@ -183,7 +183,7 @@ export class SharedSettingsDelegate< const settingAttributesUpdater = ( settingKey: K, - updateFunc: UpdateFunc, TSettings, TSettingTypes, TSettingKey> + updateFunc: UpdateFunc, TSettings, TSettingTypes, TSettingKey>, ): Dependency, TSettings, TSettingTypes, TSettingKey> => { const dependency = new Dependency, TSettings, TSettingTypes, TSettingKey>( localSettingManagerGetter.bind(this), @@ -191,7 +191,7 @@ export class SharedSettingsDelegate< updateFunc, makeLocalSettingGetter, loadingStateGetter, - makeGlobalSettingGetter + makeGlobalSettingGetter, ); this._dependencies.push(dependency); diff --git a/frontend/src/modules/_shared/DataProviderFramework/delegates/_utils/Dependency.ts b/frontend/src/modules/_shared/DataProviderFramework/delegates/_utils/Dependency.ts index 6c98764a78..51bb21738e 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/delegates/_utils/Dependency.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/delegates/_utils/Dependency.ts @@ -3,8 +3,8 @@ import { isCancelledError } from "@tanstack/react-query"; import type { GlobalSettings } from "../../framework/DataProviderManager/DataProviderManager"; import { SettingTopic, type SettingManager } from "../../framework/SettingManager/SettingManager"; import type { UpdateFunc } from "../../interfacesAndTypes/customSettingsHandler"; -import type { SettingsKeysFromTuple } from "../../interfacesAndTypes/utils"; -import type { MakeSettingTypesMap, Settings } from "../../settings/settingsDefinitions"; +import type { MakeSettingTypesMap, SettingsKeysFromTuple } from "../../interfacesAndTypes/utils"; +import type { Settings } from "../../settings/settingsDefinitions"; class DependencyLoadingError extends Error {} @@ -117,7 +117,9 @@ export class Dependency< } private getLocalSetting(settingName: K): TSettingTypes[K] { - if (!this._isInitialized) { + const setting = this._localSettingManagerGetter(settingName); + + if (!this._isInitialized && !setting.isStatic()) { this._numParentDependencies++; } @@ -138,7 +140,6 @@ export class Dependency< return this._cachedSettingsMap.get(settingName as string); } - const setting = this._localSettingManagerGetter(settingName); const value = setting.getValue(); this._cachedSettingsMap.set(settingName as string, value); diff --git a/frontend/src/modules/_shared/DataProviderFramework/framework/DataProvider/DataProvider.ts b/frontend/src/modules/_shared/DataProviderFramework/framework/DataProvider/DataProvider.ts index 001dadd0d8..2bd96e09c8 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/framework/DataProvider/DataProvider.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/framework/DataProvider/DataProvider.ts @@ -23,8 +23,8 @@ import type { import type { Item } from "../../interfacesAndTypes/entities"; import { type SerializedDataProvider, SerializedType } from "../../interfacesAndTypes/serialization"; import type { NullableStoredData, StoredData } from "../../interfacesAndTypes/sharedTypes"; -import type { SettingsKeysFromTuple } from "../../interfacesAndTypes/utils"; -import type { MakeSettingTypesMap, Settings } from "../../settings/settingsDefinitions"; +import type { MakeSettingTypesMap, SettingsKeysFromTuple } from "../../interfacesAndTypes/utils"; +import type { Settings } from "../../settings/settingsDefinitions"; import { type DataProviderManager, DataProviderManagerTopic } from "../DataProviderManager/DataProviderManager"; import { makeSettings } from "../utils/makeSettings"; @@ -313,7 +313,7 @@ export class DataProvider< this._publishSubscribeDelegate.notifySubscribers(DataProviderTopic.SUBORDINATED); } - getValueRange(): readonly [number, number] | null { + getDataValueRange(): readonly [number, number] | null { return this._valueRange; } @@ -376,8 +376,8 @@ export class DataProvider< makeAccessors(): DataProviderInformationAccessors { return { getSetting: (settingName) => this._settingsContextDelegate.getSettings()[settingName].getValue(), - getAvailableSettingValues: (settingName) => - this._settingsContextDelegate.getSettings()[settingName].getAvailableValues(), + getSettingValueRange: (settingName) => + this._settingsContextDelegate.getSettings()[settingName].getValueRange(), getGlobalSetting: (settingName) => this._dataProviderManager.getGlobalSetting(settingName), getStoredData: (key: keyof TStoredData) => this._settingsContextDelegate.getStoredData(key), getData: () => this._data, diff --git a/frontend/src/modules/_shared/DataProviderFramework/framework/DataProvider/DataProviderComponent.tsx b/frontend/src/modules/_shared/DataProviderFramework/framework/DataProvider/DataProviderComponent.tsx index be63bf6163..57d8d51c7f 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/framework/DataProvider/DataProviderComponent.tsx +++ b/frontend/src/modules/_shared/DataProviderFramework/framework/DataProvider/DataProviderComponent.tsx @@ -54,9 +54,12 @@ export function DataProviderComponent(props: DataProviderComponentProps): React. endAdornment={} >
*:nth-child(4n-3)]:bg-slate-50 [&>*:nth-child(4n-2)]:bg-slate-50", + { + hidden: !isExpanded, + }, + )} > {makeSettings(props.dataProvider.getSettingsContextDelegate().getSettings())}
diff --git a/frontend/src/modules/_shared/DataProviderFramework/framework/ExternalSettingController/ExternalSettingController.ts b/frontend/src/modules/_shared/DataProviderFramework/framework/ExternalSettingController/ExternalSettingController.ts index 859ac52b5d..9c195adab5 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/framework/ExternalSettingController/ExternalSettingController.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/framework/ExternalSettingController/ExternalSettingController.ts @@ -2,13 +2,7 @@ import { UnsubscribeFunctionsManagerDelegate } from "@lib/utils/UnsubscribeFunct import type { GroupDelegate } from "../../delegates/GroupDelegate"; import { instanceofItemGroup, type Item } from "../../interfacesAndTypes/entities"; -import type { AvailableValuesType } from "../../interfacesAndTypes/utils"; -import { - type Setting, - type SettingCategories, - type SettingTypes, - settingCategoryAvailableValuesIntersectionReducerMap, -} from "../../settings/settingsDefinitions"; +import { type Setting, type SettingTypeDefinitions } from "../../settings/settingsDefinitions"; import { DataProvider } from "../DataProvider/DataProvider"; import { DataProviderManagerTopic } from "../DataProviderManager/DataProviderManager"; import { Group } from "../Group/Group"; @@ -17,17 +11,23 @@ import { SharedSetting } from "../SharedSetting/SharedSetting"; export class ExternalSettingController< TSetting extends Setting, - TValue extends SettingTypes[TSetting] | null = SettingTypes[TSetting] | null, - TCategory extends SettingCategories[TSetting] = SettingCategories[TSetting], + TInternalValue extends SettingTypeDefinitions[TSetting]["internalValue"] | null = + | SettingTypeDefinitions[TSetting]["internalValue"] + | null, + TExternalValue extends SettingTypeDefinitions[TSetting]["externalValue"] | null = + | SettingTypeDefinitions[TSetting]["externalValue"] + | null, + TValueRange extends SettingTypeDefinitions[TSetting]["valueRange"] = SettingTypeDefinitions[TSetting]["valueRange"], > { private _parentItem: Item; - private _setting: SettingManager; - private _controlledSettings: Map> = new Map(); - private _availableValuesMap: Map> = new Map(); + private _setting: SettingManager; + private _controlledSettings: Map> = + new Map(); + private _valueRangesMap: Map = new Map(); private _unsubscribeFunctionsManagerDelegate: UnsubscribeFunctionsManagerDelegate = new UnsubscribeFunctionsManagerDelegate(); - constructor(parentItem: Item, setting: SettingManager) { + constructor(parentItem: Item, setting: SettingManager) { this._parentItem = parentItem; this._setting = setting; @@ -59,19 +59,19 @@ export class ExternalSettingController< return this._parentItem; } - registerSetting(settingManager: SettingManager): void { + registerSetting(settingManager: SettingManager): void { this._controlledSettings.set(settingManager.getId(), settingManager); settingManager.registerExternalSettingController(this); } - getSetting(): SettingManager { + getSetting(): SettingManager { return this._setting; } private findControlledSettingsRecursively( groupDelegate: GroupDelegate, thisItem?: Item, - ): SettingManager[] { + ): SettingManager[] { let children = groupDelegate.getChildren(); if (thisItem) { const position = children.indexOf(thisItem); @@ -79,7 +79,7 @@ export class ExternalSettingController< children = children.slice(position + 1, children.length); } } - const foundSettings: SettingManager[] = []; + const foundSettings: SettingManager[] = []; for (const child of children) { if (child instanceof DataProvider) { @@ -133,15 +133,12 @@ export class ExternalSettingController< continue; } this._controlledSettings.set(setting.getId(), setting); - this._availableValuesMap.set( - setting.getId(), - setting.getAvailableValues() as AvailableValuesType, - ); + this._valueRangesMap.set(setting.getId(), setting.getValueRange()); setting.registerExternalSettingController(this); } if (this._controlledSettings.size === 0) { - this._setting.setAvailableValues(null); + this._setting.setValueRange(null); return; } @@ -153,14 +150,14 @@ export class ExternalSettingController< setting.unregisterExternalSettingController(); } this._controlledSettings.clear(); - this._availableValuesMap.clear(); + this._valueRangesMap.clear(); } - setAvailableValues(settingId: string, availableValues: AvailableValuesType | null): void { - if (availableValues !== null) { - this._availableValuesMap.set(settingId, availableValues); + setAvailableValues(settingId: string, valueRange: TValueRange | null): void { + if (valueRange !== null) { + this._valueRangesMap.set(settingId, valueRange); } else { - this._availableValuesMap.delete(settingId); + this._valueRangesMap.delete(settingId); } this.makeIntersectionOfAvailableValues(); @@ -172,9 +169,7 @@ export class ExternalSettingController< return; } } - - const category = this._setting.getCategory(); - const reducerDefinition = settingCategoryAvailableValuesIntersectionReducerMap[category]; + const reducerDefinition = this._setting.getValueRangeReducerDefinition(); if (this._setting.isStatic()) { this._setting.maybeResetPersistedValue(); @@ -191,24 +186,24 @@ export class ExternalSettingController< } const { reducer, startingValue, isValid } = reducerDefinition; - let availableValues: AvailableValuesType = startingValue as AvailableValuesType; + let valueRange = startingValue; let index = 0; let isInvalid = false; - for (const value of this._availableValuesMap.values()) { + for (const value of this._valueRangesMap.values()) { if (value === null) { isInvalid = true; break; } - availableValues = reducer(availableValues as any, value as any, index++) as AvailableValuesType; + valueRange = reducer(valueRange, value, index++); } - if (!isValid(availableValues as any) || isInvalid) { - this._setting.setAvailableValues(null); + if (!isValid(valueRange as any) || isInvalid) { + this._setting.setValueRange(null); this._setting.setValue(null as any); return; } - this._setting.setAvailableValues(availableValues); + this._setting.setValueRange(valueRange); } } diff --git a/frontend/src/modules/_shared/DataProviderFramework/framework/Group/Group.ts b/frontend/src/modules/_shared/DataProviderFramework/framework/Group/Group.ts index 4979f37fe6..1c474c0ad3 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/framework/Group/Group.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/framework/Group/Group.ts @@ -13,8 +13,8 @@ import type { DefineBasicDependenciesArgs } from "../../interfacesAndTypes/custo import type { ItemGroup } from "../../interfacesAndTypes/entities"; import type { SerializedGroup, SerializedSettingsState } from "../../interfacesAndTypes/serialization"; import { SerializedType } from "../../interfacesAndTypes/serialization"; -import type { SettingsKeysFromTuple } from "../../interfacesAndTypes/utils"; -import type { MakeSettingTypesMap, SettingTypes, Settings } from "../../settings/settingsDefinitions"; +import type { MakeSettingTypesMap, SettingsKeysFromTuple } from "../../interfacesAndTypes/utils"; +import type { Settings } from "../../settings/settingsDefinitions"; import type { DataProviderManager } from "../DataProviderManager/DataProviderManager"; import type { SettingManager } from "../SettingManager/SettingManager"; import { makeSettings } from "../utils/makeSettings"; @@ -37,7 +37,7 @@ export function isGroup(obj: any): obj is Group { export type GroupParams< TSettingTypes extends Settings, - TSettings extends MakeSettingTypesMap = MakeSettingTypesMap + TSettings extends MakeSettingTypesMap = MakeSettingTypesMap, > = { dataProviderManager: DataProviderManager; color?: string; @@ -50,7 +50,7 @@ export type GroupParams< export class Group< TSettings extends Settings = [], TSettingTypes extends MakeSettingTypesMap = MakeSettingTypesMap, - TSettingKey extends SettingsKeysFromTuple = SettingsKeysFromTuple + TSettingKey extends SettingsKeysFromTuple = SettingsKeysFromTuple, > implements ItemGroup { private _itemDelegate: ItemDelegate; @@ -70,11 +70,11 @@ export class Group< this, makeSettings( customGroupImplementation.settings as unknown as TSettings, - customGroupImplementation.getDefaultSettingsValues?.() ?? {} + customGroupImplementation.getDefaultSettingsValues?.() ?? {}, ), customGroupImplementation.defineDependencies as unknown as | ((args: DefineBasicDependenciesArgs) => void) - | undefined + | undefined, ); } this._type = type; @@ -103,7 +103,7 @@ export class Group< return this._sharedSettingsDelegate; } - getWrappedSettings(): { [K in TSettingKey]: SettingManager } { + getWrappedSettings(): { [K in TSettingKey]: SettingManager } { if (!this._sharedSettingsDelegate) { throw new Error("Group does not have shared settings."); } diff --git a/frontend/src/modules/_shared/DataProviderFramework/framework/SettingManager/SettingManager.ts b/frontend/src/modules/_shared/DataProviderFramework/framework/SettingManager/SettingManager.ts index 448c0fd4a7..7f189d0814 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/framework/SettingManager/SettingManager.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/framework/SettingManager/SettingManager.ts @@ -9,49 +9,55 @@ import { UnsubscribeFunctionsManagerDelegate } from "@lib/utils/UnsubscribeFunct import type { CustomSettingImplementation } from "../../interfacesAndTypes/customSettingImplementation"; import type { SettingAttributes } from "../../interfacesAndTypes/customSettingsHandler"; -import type { AvailableValuesType, MakeAvailableValuesTypeBasedOnCategory } from "../../interfacesAndTypes/utils"; -import type { Setting, SettingCategories, SettingCategory, SettingTypes } from "../../settings/settingsDefinitions"; -import { settingCategoryFixupMap, settingCategoryIsValueValidMap } from "../../settings/settingsDefinitions"; +import type { Setting, SettingTypeDefinitions } from "../../settings/settingsDefinitions"; import type { ExternalSettingController } from "../ExternalSettingController/ExternalSettingController"; import { Group } from "../Group/Group"; import { SharedSetting } from "../SharedSetting/SharedSetting"; export enum SettingTopic { + INTERNAL_VALUE = "INTERNAL_VALUE", VALUE = "VALUE", VALUE_ABOUT_TO_BE_CHANGED = "VALUE_ABOUT_TO_BE_CHANGED", IS_VALID = "IS_VALID", - AVAILABLE_VALUES = "AVAILABLE_VALUES", + VALUE_RANGE = "VALUE_RANGE", IS_EXTERNALLY_CONTROLLED = "IS_EXTERNALLY_CONTROLLED", EXTERNAL_CONTROLLER_PROVIDER = "EXTERNAL_CONTROLLER_PROVIDER", IS_LOADING = "IS_LOADING", IS_INITIALIZED = "IS_INITIALIZED", IS_PERSISTED = "IS_PERSISTED", ATTRIBUTES = "ATTRIBUTES", + IS_PERSISTED_VALUE_VALID = "IS_PERSISTED_VALUE_VALID", } -export type SettingTopicPayloads = { - [SettingTopic.VALUE]: TValue; +export type SettingTopicPayloads = { + [SettingTopic.VALUE]: TExternalValue; + [SettingTopic.INTERNAL_VALUE]: TInternalValue; [SettingTopic.VALUE_ABOUT_TO_BE_CHANGED]: void; [SettingTopic.IS_VALID]: boolean; - [SettingTopic.AVAILABLE_VALUES]: MakeAvailableValuesTypeBasedOnCategory | null; + [SettingTopic.VALUE_RANGE]: TValueRange | null; [SettingTopic.IS_EXTERNALLY_CONTROLLED]: boolean; [SettingTopic.EXTERNAL_CONTROLLER_PROVIDER]: ExternalControllerProviderType | undefined; [SettingTopic.IS_LOADING]: boolean; [SettingTopic.IS_INITIALIZED]: boolean; [SettingTopic.IS_PERSISTED]: boolean; [SettingTopic.ATTRIBUTES]: SettingAttributes; + [SettingTopic.IS_PERSISTED_VALUE_VALID]: boolean; }; export type SettingManagerParams< TSetting extends Setting, - TValue extends SettingTypes[TSetting] | null, - TCategory extends SettingCategories[TSetting], + TInternalValue extends SettingTypeDefinitions[TSetting]["internalValue"] | null = + | SettingTypeDefinitions[TSetting]["internalValue"] + | null, + TExternalValue extends SettingTypeDefinitions[TSetting]["externalValue"] | null = + | SettingTypeDefinitions[TSetting]["externalValue"] + | null, + TValueRange extends SettingTypeDefinitions[TSetting]["valueRange"] = SettingTypeDefinitions[TSetting]["valueRange"], > = { type: TSetting; - category: TCategory; label: string; - defaultValue: TValue; - customSettingImplementation: CustomSettingImplementation; + defaultValue: TInternalValue; + customSettingImplementation: CustomSettingImplementation; }; export enum ExternalControllerProviderType { @@ -59,6 +65,9 @@ export enum ExternalControllerProviderType { SHARED_SETTING = "SHARED_SETTING", } +const NO_CACHE = Symbol("NO_CACHE"); +type NoCache = typeof NO_CACHE; + /* * The SettingManager class is responsible for managing a setting. * @@ -67,61 +76,79 @@ export enum ExternalControllerProviderType { */ export class SettingManager< TSetting extends Setting, - TValue extends SettingTypes[TSetting] | null = SettingTypes[TSetting] | null, - TCategory extends SettingCategories[TSetting] = SettingCategories[TSetting], -> implements PublishSubscribe> + TInternalValue extends SettingTypeDefinitions[TSetting]["internalValue"] | null = + | SettingTypeDefinitions[TSetting]["internalValue"] + | null, + TExternalValue extends SettingTypeDefinitions[TSetting]["externalValue"] | null = + | SettingTypeDefinitions[TSetting]["externalValue"] + | null, + TValueRange extends SettingTypeDefinitions[TSetting]["valueRange"] = SettingTypeDefinitions[TSetting]["valueRange"], +> implements PublishSubscribe> { private _id: string; private _type: TSetting; - private _category: TCategory; private _label: string; - private _customSettingImplementation: CustomSettingImplementation; - private _value: TValue; + private _customSettingImplementation: CustomSettingImplementation; + private _internalValue: TInternalValue; private _isValueValid: boolean = false; - private _publishSubscribeDelegate = new PublishSubscribeDelegate>(); - private _availableValues: AvailableValuesType | null = null; + private _publishSubscribeDelegate = new PublishSubscribeDelegate< + SettingTopicPayloads + >(); + private _valueRange: TValueRange | null = null; private _loading: boolean = false; private _initialized: boolean = false; - private _currentValueFromPersistence: TValue | null = null; + private _currentValueFromPersistence: TInternalValue | null = null; + private _currentValueFromPersistenceIsValid: boolean = true; private _isStatic: boolean; private _attributes: SettingAttributes = { enabled: true, visible: true, }; - private _externalController: ExternalSettingController | null = null; + private _externalController: ExternalSettingController< + TSetting, + TInternalValue, + TExternalValue, + TValueRange + > | null = null; private _unsubscribeFunctionsManagerDelegate: UnsubscribeFunctionsManagerDelegate = new UnsubscribeFunctionsManagerDelegate(); + private _cachedExternalValue: TExternalValue | null | NoCache = NO_CACHE; constructor({ type, - category, customSettingImplementation, defaultValue, label, - }: SettingManagerParams) { + }: SettingManagerParams) { this._id = v4(); this._type = type; - this._category = category; this._label = label; this._customSettingImplementation = customSettingImplementation; - this._value = defaultValue; + this._internalValue = defaultValue; this._isStatic = customSettingImplementation.getIsStatic?.() ?? false; if (this._isStatic) { - this.setValueValid(this.checkIfValueIsValid(this._value)); + this.setValueValid(this.checkIfValueIsValid(this._internalValue)); } } + getValueRangeReducerDefinition() { + if ("valueRangeIntersectionReducerDefinition" in this._customSettingImplementation) { + return this._customSettingImplementation.valueRangeIntersectionReducerDefinition ?? null; + } + return null; + } + registerExternalSettingController( - externalController: ExternalSettingController, + externalController: ExternalSettingController, ): void { this._externalController = externalController; - this._value = externalController.getSetting().getValue(); + this.setInternalValueAndInvalidateCache(externalController.getSetting().getInternalValue()); this._unsubscribeFunctionsManagerDelegate.registerUnsubscribeFunction( "external-setting-controller", externalController.getSetting().getPublishSubscribeDelegate().makeSubscriberFunction(SettingTopic.VALUE)( () => { this._publishSubscribeDelegate.notifySubscribers(SettingTopic.VALUE); - this._value = externalController.getSetting().getValue(); + this.setInternalValueAndInvalidateCache(externalController.getSetting().getInternalValue()); }, ), ); @@ -183,17 +210,27 @@ export class SettingManager< externalController .getSetting() .getPublishSubscribeDelegate() - .makeSubscriberFunction(SettingTopic.AVAILABLE_VALUES)(() => { - this._publishSubscribeDelegate.notifySubscribers(SettingTopic.AVAILABLE_VALUES); + .makeSubscriberFunction(SettingTopic.VALUE_RANGE)(() => { + this._publishSubscribeDelegate.notifySubscribers(SettingTopic.VALUE_RANGE); + }), + ); + this._unsubscribeFunctionsManagerDelegate.registerUnsubscribeFunction( + "external-setting-controller", + externalController + .getSetting() + .getPublishSubscribeDelegate() + .makeSubscriberFunction(SettingTopic.IS_PERSISTED_VALUE_VALID)(() => { + this._publishSubscribeDelegate.notifySubscribers(SettingTopic.IS_PERSISTED_VALUE_VALID); }), ); } unregisterExternalSettingController(): void { - this._value = this._externalController?.getSetting().getValue() ?? this._value; + const newInternalValue = this._externalController?.getSetting().getInternalValue() ?? this._internalValue; + this.setInternalValueAndInvalidateCache(newInternalValue); this._externalController = null; this._unsubscribeFunctionsManagerDelegate.unsubscribe("external-setting-controller"); - const shouldNotifyValueChanged = this.applyAvailableValues(); + const shouldNotifyValueChanged = this.applyValueRange(); if (shouldNotifyValueChanged) { this._publishSubscribeDelegate.notifySubscribers(SettingTopic.VALUE); } @@ -211,10 +248,6 @@ export class SettingManager< return this._type; } - getCategory(): TCategory { - return this._category; - } - getLabel(): string { return this._label; } @@ -236,16 +269,48 @@ export class SettingManager< this._publishSubscribeDelegate.notifySubscribers(SettingTopic.ATTRIBUTES); } - getValue(): TValue { + getInternalValue(): TInternalValue { if (this._externalController) { - return this._externalController.getSetting().getValue(); + return this._externalController.getSetting().getInternalValue(); } if (this._currentValueFromPersistence !== null) { return this._currentValueFromPersistence; } - return this._value; + return this._internalValue; + } + + getValue(): TExternalValue | null { + if (this._externalController) { + return this._externalController.getSetting().getValue(); + } + + let value = this._internalValue; + if (this._currentValueFromPersistence !== null) { + value = this._currentValueFromPersistence; + } + + if (!this._isStatic && this._valueRange === null) { + return null; + } + + // Return cached value if available + if (this._cachedExternalValue !== NO_CACHE) { + return this._cachedExternalValue; + } + + const mappingFunc = this._customSettingImplementation.mapInternalToExternalValue; + // Type assertion needed because: + // - Static settings accept `any` for valueRange (which can be null) + // - Dynamic settings require non-null valueRange, but we've already guarded against null above + // - TypeScript can't infer that the guard ensures non-null for dynamic settings at this point + const externalValue = mappingFunc.bind(this._customSettingImplementation)(value, this._valueRange as any); + + // Cache the computed external value + this._cachedExternalValue = externalValue; + + return externalValue; } isStatic(): boolean { @@ -254,19 +319,57 @@ export class SettingManager< serializeValue(): string { if (this._customSettingImplementation.serializeValue) { - return this._customSettingImplementation.serializeValue(this.getValue()); + return this._customSettingImplementation.serializeValue.bind(this._customSettingImplementation)( + this.getInternalValue(), + ); } - return JSON.stringify(this.getValue()); + return JSON.stringify(this.getInternalValue()); } deserializeValue(serializedValue: string): void { - if (this._customSettingImplementation.deserializeValue) { - this._currentValueFromPersistence = this._customSettingImplementation.deserializeValue(serializedValue); - return; + // Invalidate cache since _currentValueFromPersistence affects the value returned by getValue() + this._cachedExternalValue = NO_CACHE; + + try { + let deserializedValue; + if (this._customSettingImplementation.deserializeValue) { + deserializedValue = this._customSettingImplementation.deserializeValue.bind( + this._customSettingImplementation, + )(serializedValue); + } else { + deserializedValue = JSON.parse(serializedValue); + } + + // Validate parsed value has correct structure + if (!this.isDeserializedValueValidStructure(deserializedValue)) { + console.error( + `Deserialized value for setting "${this._label}" has invalid structure, resetting to null`, + ); + this._currentValueFromPersistence = null; + this.setPersistedValueIsValid(false); + this._publishSubscribeDelegate.notifySubscribers(SettingTopic.IS_PERSISTED); + return; + } + + this._currentValueFromPersistence = deserializedValue; + this.setPersistedValueIsValid(true); + this._publishSubscribeDelegate.notifySubscribers(SettingTopic.IS_PERSISTED); + } catch (error) { + console.error(`Failed to deserialize value for setting "${this._label}":`, error); + this._currentValueFromPersistence = null; + this.setPersistedValueIsValid(false); + this._publishSubscribeDelegate.notifySubscribers(SettingTopic.IS_PERSISTED); } + } - this._currentValueFromPersistence = JSON.parse(serializedValue); + /** + * Validates that a deserialized value has the correct structure/type. + * This is a basic structural validation before the value is used. + * Uses the required isValueValidStructure method from the custom implementation. + */ + private isDeserializedValueValidStructure(value: unknown): value is TInternalValue { + return this._customSettingImplementation.isValueValidStructure.bind(this._customSettingImplementation)(value); } isExternallyControlled(): boolean { @@ -287,25 +390,41 @@ export class SettingManager< if (this._externalController) { return this._externalController.getSetting().isPersistedValue(); } + + // Persisted value is not valid + if (!this._currentValueFromPersistenceIsValid) { + return true; + } return this._currentValueFromPersistence !== null; } + private setPersistedValueIsValid(isValid: boolean): void { + if (this._currentValueFromPersistenceIsValid === isValid) { + return; + } + + this._currentValueFromPersistenceIsValid = isValid; + this._publishSubscribeDelegate.notifySubscribers(SettingTopic.IS_PERSISTED_VALUE_VALID); + } + /* * This method is used to set the value of the setting. * It should only be called when a user is changing a setting. */ - setValue(value: TValue): void { - if (isEqual(this._value, value)) { + setValue(value: TInternalValue): void { + if (isEqual(this._internalValue, value)) { return; } this._currentValueFromPersistence = null; + this.setPersistedValueIsValid(true); this._publishSubscribeDelegate.notifySubscribers(SettingTopic.VALUE_ABOUT_TO_BE_CHANGED); - this._value = value; + this.setInternalValueAndInvalidateCache(value); - this.setValueValid(this.checkIfValueIsValid(this._value)); + this.setValueValid(this.checkIfValueIsValid(this._internalValue)); this._publishSubscribeDelegate.notifySubscribers(SettingTopic.VALUE); + this._publishSubscribeDelegate.notifySubscribers(SettingTopic.INTERNAL_VALUE); } setValueValid(isValueValid: boolean): void { @@ -355,7 +474,7 @@ export class SettingManager< } valueToRepresentation( - value: TValue, + value: TInternalValue, workbenchSession: WorkbenchSession, workbenchSettings: WorkbenchSettings, ): React.ReactNode { @@ -366,7 +485,9 @@ export class SettingManager< } if (this._customSettingImplementation.overriddenValueRepresentation) { - return this._customSettingImplementation.overriddenValueRepresentation({ + return this._customSettingImplementation.overriddenValueRepresentation.bind( + this._customSettingImplementation, + )({ value, workbenchSession, workbenchSettings, @@ -388,7 +509,9 @@ export class SettingManager< return "Value has no string representation"; } - makeSnapshotGetter(topic: T): () => SettingTopicPayloads[T] { + makeSnapshotGetter( + topic: T, + ): () => SettingTopicPayloads[T] { const externalController = this._externalController; if (externalController) { return (): any => { @@ -416,12 +539,14 @@ export class SettingManager< switch (topic) { case SettingTopic.VALUE: return this.getValue(); + case SettingTopic.INTERNAL_VALUE: + return this.getInternalValue(); case SettingTopic.VALUE_ABOUT_TO_BE_CHANGED: return; case SettingTopic.IS_VALID: return this._isValueValid; - case SettingTopic.AVAILABLE_VALUES: - return this._availableValues; + case SettingTopic.VALUE_RANGE: + return this._valueRange; case SettingTopic.IS_EXTERNALLY_CONTROLLED: return this._externalController !== null; case SettingTopic.EXTERNAL_CONTROLLER_PROVIDER: @@ -430,6 +555,8 @@ export class SettingManager< return this.isLoading(); case SettingTopic.IS_PERSISTED: return this.isPersistedValue(); + case SettingTopic.IS_PERSISTED_VALUE_VALID: + return this._currentValueFromPersistenceIsValid; case SettingTopic.IS_INITIALIZED: return this.isInitialized(); case SettingTopic.ATTRIBUTES: @@ -446,43 +573,37 @@ export class SettingManager< return this._publishSubscribeDelegate; } - getAvailableValues(): AvailableValuesType | null { + getValueRange(): TValueRange | null { if (this._externalController) { - return this._externalController.getSetting().getAvailableValues(); + return this._externalController.getSetting().getValueRange(); } - return this._availableValues as AvailableValuesType | null; + return this._valueRange; } maybeResetPersistedValue(): boolean { if (this._isStatic) { - if (this._currentValueFromPersistence !== null) { - this._value = this._currentValueFromPersistence; + if (this._currentValueFromPersistence !== null && this._currentValueFromPersistenceIsValid) { + this.setInternalValueAndInvalidateCache(this._currentValueFromPersistence); this._currentValueFromPersistence = null; this.setValueValid(true); } return true; } - if (this._currentValueFromPersistence === null || this._availableValues === null) { + if (this._currentValueFromPersistence === null || this._valueRange === null) { return false; } - let isPersistedValueValid = false; - const customIsValueValidFunction = this._customSettingImplementation.isValueValid; - if (customIsValueValidFunction) { - isPersistedValueValid = customIsValueValidFunction( - this._currentValueFromPersistence, - this._availableValues as any, - ); - } else { - isPersistedValueValid = settingCategoryIsValueValidMap[this._category]( - this._currentValueFromPersistence as any, - this._availableValues as any, - ); - } - if (isPersistedValueValid) { - this._value = this._currentValueFromPersistence; + const isPersistedValueValid = customIsValueValidFunction + ? customIsValueValidFunction.bind(this._customSettingImplementation)( + this._currentValueFromPersistence, + this._valueRange as any, + ) + : true; + + if (isPersistedValueValid && this._currentValueFromPersistenceIsValid) { + this.setInternalValueAndInvalidateCache(this._currentValueFromPersistence); this._currentValueFromPersistence = null; this.setValueValid(true); this._publishSubscribeDelegate.notifySubscribers(SettingTopic.VALUE); @@ -493,44 +614,45 @@ export class SettingManager< return false; } - private applyAvailableValues(): boolean { + private applyValueRange(): boolean { let valueChanged = false; - const valueFixedUp = !this.checkIfValueIsValid(this.getValue()) && this.maybeFixupValue(); + const valueFixedUp = !this.checkIfValueIsValid(this.getInternalValue()) && this.maybeFixupValue(); const persistedValueReset = this.maybeResetPersistedValue(); if (valueFixedUp || persistedValueReset) { valueChanged = true; } const prevIsValid = this._isValueValid; - this.setValueValid(this.checkIfValueIsValid(this.getValue())); + this.setValueValid(this.checkIfValueIsValid(this.getInternalValue())); this.setLoading(false); - const shouldNotifyValueChanged = valueChanged || this._isValueValid !== prevIsValid || this._value === null; + const shouldNotifyValueChanged = + valueChanged || this._isValueValid !== prevIsValid || this._internalValue === null; return shouldNotifyValueChanged; } - setAvailableValues(availableValues: AvailableValuesType | null): void { + setValueRange(valueRange: TValueRange | null): void { if (this._externalController) { - this._availableValues = availableValues; + this.setValueRangeAndInvalidateCache(valueRange); this.maybeResetPersistedValue(); this._loading = false; this.initialize(); - this._externalController.setAvailableValues(this.getId(), availableValues); + this._externalController.setAvailableValues(this.getId(), valueRange); return; } - if (isEqual(this._availableValues, availableValues) && this._initialized) { + if (isEqual(this._valueRange, valueRange) && this._initialized) { this.setLoading(false); return; } - this._availableValues = availableValues; + this.setValueRangeAndInvalidateCache(valueRange); - const shouldNotifyValueChanged = this.applyAvailableValues(); + const shouldNotifyValueChanged = this.applyValueRange(); this.initialize(); if (shouldNotifyValueChanged) { this._publishSubscribeDelegate.notifySubscribers(SettingTopic.VALUE); } - this._publishSubscribeDelegate.notifySubscribers(SettingTopic.AVAILABLE_VALUES); + this._publishSubscribeDelegate.notifySubscribers(SettingTopic.VALUE_RANGE); } makeComponent() { @@ -538,7 +660,7 @@ export class SettingManager< } private maybeFixupValue(): boolean { - if (this.checkIfValueIsValid(this._value)) { + if (this.checkIfValueIsValid(this._internalValue)) { return false; } @@ -546,38 +668,55 @@ export class SettingManager< return false; } - if (this._availableValues === null) { + if (this._valueRange === null) { return false; } - let candidate: TValue; - if (this._customSettingImplementation.fixupValue) { - candidate = this._customSettingImplementation.fixupValue(this._value, this._availableValues as any); - } else { - candidate = settingCategoryFixupMap[this._category]( - this._value as any, - this._availableValues as any, - ) as TValue; - } + const customFixupFunction = this._customSettingImplementation.fixupValue; - if (isEqual(candidate, this._value)) { + const candidate = customFixupFunction + ? customFixupFunction.bind(this._customSettingImplementation)(this._internalValue, this._valueRange as any) + : this._internalValue; + + if (isEqual(candidate, this._internalValue)) { return false; } - this._value = candidate; + this.setInternalValueAndInvalidateCache(candidate); return true; } - private checkIfValueIsValid(value: TValue): boolean { + private checkIfValueIsValid(value: TInternalValue): boolean { if (this._isStatic) { return true; } - if (this._availableValues === null) { + if (this._valueRange === null) { return false; } - if (this._customSettingImplementation.isValueValid) { - return this._customSettingImplementation.isValueValid(value, this._availableValues as any); - } else { - return settingCategoryIsValueValidMap[this._category](value as any, this._availableValues as any); + + const customIsValueValidFunction = this._customSettingImplementation.isValueValid; + + if (!customIsValueValidFunction) { + return true; } + + return customIsValueValidFunction.bind(this._customSettingImplementation)(value, this._valueRange as any); + } + + /** + * Sets the internal value and invalidates the external value cache. + * Use this instead of directly assigning to this._internalValue. + */ + private setInternalValueAndInvalidateCache(value: TInternalValue): void { + this._internalValue = value; + this._cachedExternalValue = NO_CACHE; + } + + /** + * Sets the value range and invalidates the external value cache. + * Use this instead of directly assigning to this._valueRange. + */ + private setValueRangeAndInvalidateCache(valueRange: TValueRange | null): void { + this._valueRange = valueRange; + this._cachedExternalValue = NO_CACHE; } } diff --git a/frontend/src/modules/_shared/DataProviderFramework/framework/SettingManager/SettingManagerComponent.tsx b/frontend/src/modules/_shared/DataProviderFramework/framework/SettingManager/SettingManagerComponent.tsx index cdcf102258..391e5f836f 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/framework/SettingManager/SettingManagerComponent.tsx +++ b/frontend/src/modules/_shared/DataProviderFramework/framework/SettingManager/SettingManagerComponent.tsx @@ -7,35 +7,32 @@ import { usePublishSubscribeTopicValue } from "@lib/utils/PublishSubscribeDelega import { resolveClassNames } from "@lib/utils/resolveClassNames"; import type { SettingComponentProps as SettingComponentPropsInterface } from "../../interfacesAndTypes/customSettingImplementation"; -import type { Setting, SettingCategories, SettingTypes } from "../../settings/settingsDefinitions"; +import type { Setting, SettingTypeDefinitions } from "../../settings/settingsDefinitions"; import { type DataProviderManager, DataProviderManagerTopic } from "../DataProviderManager/DataProviderManager"; import { ExternalControllerProviderType, SettingTopic } from "./SettingManager"; import type { SettingManager } from "./SettingManager"; -export type SettingComponentProps< - TSetting extends Setting, - TValue extends SettingTypes[TSetting], - TCategory extends SettingCategories[TSetting] = SettingCategories[TSetting], -> = { - setting: SettingManager; +export type SettingComponentProps = { + setting: SettingManager; manager: DataProviderManager; sharedSetting: boolean; }; export function SettingManagerComponent< TSetting extends Setting, - TValue extends SettingTypes[TSetting] = SettingTypes[TSetting], - TCategory extends SettingCategories[TSetting] = SettingCategories[TSetting], ->(props: SettingComponentProps): React.ReactNode { - const componentRef = React.useRef<(props: SettingComponentPropsInterface) => React.ReactNode>( + TValue extends + SettingTypeDefinitions[TSetting]["internalValue"] = SettingTypeDefinitions[TSetting]["internalValue"], +>(props: SettingComponentProps): React.ReactNode { + const componentRef = React.useRef<(props: SettingComponentPropsInterface) => React.ReactNode>( props.setting.makeComponent(), ); - const value = usePublishSubscribeTopicValue(props.setting, SettingTopic.VALUE); + const value = usePublishSubscribeTopicValue(props.setting, SettingTopic.INTERNAL_VALUE); const attributes = usePublishSubscribeTopicValue(props.setting, SettingTopic.ATTRIBUTES); const isValid = usePublishSubscribeTopicValue(props.setting, SettingTopic.IS_VALID); const isPersisted = usePublishSubscribeTopicValue(props.setting, SettingTopic.IS_PERSISTED); - const availableValues = usePublishSubscribeTopicValue(props.setting, SettingTopic.AVAILABLE_VALUES); + const isValidPersistedValue = usePublishSubscribeTopicValue(props.setting, SettingTopic.IS_PERSISTED_VALUE_VALID); + const valueRange = usePublishSubscribeTopicValue(props.setting, SettingTopic.VALUE_RANGE); const isExternallyControlled = usePublishSubscribeTopicValue(props.setting, SettingTopic.IS_EXTERNALLY_CONTROLLED); const externalControllerProvider = usePublishSubscribeTopicValue( props.setting, @@ -50,19 +47,27 @@ export function SettingManagerComponent< actuallyLoading = false; } - function handleValueChanged(newValue: TValue) { - props.setting.setValue(newValue); - } + const handleValueChanged = React.useCallback( + function handleValueChanged(newValue: TValue | null | ((prevValue: TValue | null) => TValue | null)) { + if (typeof newValue === "function") { + const updaterFunction = newValue; + const currentValue = props.setting.getValue() as TValue | null; + newValue = updaterFunction(currentValue); + } + props.setting.setValue(newValue); + }, + [props.setting], + ); if (!attributes.visible) { return null; } - if (props.sharedSetting && isInitialized && availableValues === null && !props.setting.isStatic()) { + if (props.sharedSetting && isInitialized && valueRange === null && !props.setting.isStatic()) { return ( -
{props.setting.getLabel()}
-
Empty intersection
+
{props.setting.getLabel()}
+
Empty intersection
); } @@ -85,7 +90,7 @@ export function SettingManagerComponent< -
+
{isValid ? valueAsString : No valid shared setting value}
@@ -94,7 +99,7 @@ export function SettingManagerComponent< return ( -
{props.setting.getLabel()}
+
{props.setting.getLabel()}
@@ -110,13 +115,13 @@ export function SettingManagerComponent< isValueValid={isValid} isOverridden={isExternallyControlled} overriddenValue={value} - availableValues={availableValues} + valueRange={valueRange} globalSettings={globalSettings} workbenchSession={props.manager.getWorkbenchSession()} workbenchSettings={props.manager.getWorkbenchSettings()} />
- {isPersisted && !isLoading && isInitialized && !isValid && ( + {isPersisted && isValidPersistedValue && !isLoading && isInitialized && !isValid && ( )} + {isPersisted && !isValidPersistedValue && ( + + + + Persisted value has invalid structure. + + + )}
diff --git a/frontend/src/modules/_shared/DataProviderFramework/framework/SharedSetting/SharedSetting.ts b/frontend/src/modules/_shared/DataProviderFramework/framework/SharedSetting/SharedSetting.ts index 8e28b567d9..54a9f0c43d 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/framework/SharedSetting/SharedSetting.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/framework/SharedSetting/SharedSetting.ts @@ -6,7 +6,7 @@ import type { Item, SharedSettingsProvider } from "../../interfacesAndTypes/enti import type { SerializedSharedSetting } from "../../interfacesAndTypes/serialization"; import { SerializedType } from "../../interfacesAndTypes/serialization"; import { SettingRegistry } from "../../settings/SettingRegistry"; -import type { Setting, SettingTypes } from "../../settings/settingsDefinitions"; +import type { Setting, SettingTypeDefinitions } from "../../settings/settingsDefinitions"; import { type DataProviderManager } from "../DataProviderManager/DataProviderManager"; import type { SettingManager } from "../SettingManager/SettingManager"; @@ -31,7 +31,7 @@ export class SharedSetting implements Item, SharedSett constructor( wrappedSettingType: TSetting, - defaultValue: SettingTypes[TSetting], + defaultValue: SettingTypeDefinitions[TSetting]["internalValue"], dataProviderManager: DataProviderManager, ) { const wrappedSetting = SettingRegistry.makeSetting(wrappedSettingType, defaultValue); diff --git a/frontend/src/modules/_shared/DataProviderFramework/framework/utils/makeSettings.ts b/frontend/src/modules/_shared/DataProviderFramework/framework/utils/makeSettings.ts index 9925ab6f21..b40cdf6fd3 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/framework/utils/makeSettings.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/framework/utils/makeSettings.ts @@ -1,23 +1,21 @@ - -import type { SettingsKeysFromTuple } from "../../interfacesAndTypes/utils"; +import type { MakeSettingTypesMap, SettingsKeysFromTuple } from "../../interfacesAndTypes/utils"; import { SettingRegistry } from "../../settings/SettingRegistry"; -import type { MakeSettingTypesMap, Settings, SettingTypes } from "../../settings/settingsDefinitions"; +import type { Settings, SettingTypeDefinitions } from "../../settings/settingsDefinitions"; import type { SettingManager } from "../SettingManager/SettingManager"; - export function makeSettings< const TSettings extends Settings, const TSettingTypes extends MakeSettingTypesMap, const TSettingKey extends SettingsKeysFromTuple = SettingsKeysFromTuple, ->( - settings: TSettings, - defaultValues: Partial -): { [K in TSettingKey]: SettingManager } { +>(settings: TSettings, defaultValues: Partial): { [K in TSettingKey]: SettingManager } { const returnValue: Record> = {} as Record>; for (const key of settings) { - returnValue[key] = SettingRegistry.makeSetting(key, defaultValues[key as keyof TSettingTypes] as SettingTypes[typeof key]); + returnValue[key] = SettingRegistry.makeSetting( + key, + defaultValues[key as keyof TSettingTypes] as SettingTypeDefinitions[typeof key]["internalValue"], + ); } return returnValue as { [K in TSettingKey]: SettingManager; }; -} \ No newline at end of file +} diff --git a/frontend/src/modules/_shared/DataProviderFramework/groups/implementations/IntersectionView.ts b/frontend/src/modules/_shared/DataProviderFramework/groups/implementations/IntersectionView.ts index c23a4d3199..bd09c574e4 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/groups/implementations/IntersectionView.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/groups/implementations/IntersectionView.ts @@ -1,9 +1,9 @@ import { IntersectionType } from "@framework/types/intersection"; -import type { MakeSettingTypesMap } from "../..//settings/settingsDefinitions"; import { Setting } from "../..//settings/settingsDefinitions"; import type { CustomGroupImplementationWithSettings } from "../../interfacesAndTypes/customGroupImplementation"; import type { DefineBasicDependenciesArgs } from "../../interfacesAndTypes/customSettingsHandler"; +import type { MakeSettingTypesMap } from "../../interfacesAndTypes/utils"; const intersectionViewSettings = [Setting.INTERSECTION, Setting.WELLBORE_EXTENSION_LENGTH] as const; export type IntersectionViewSettings = typeof intersectionViewSettings; diff --git a/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/customDataProviderImplementation.ts b/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/customDataProviderImplementation.ts index 53a712e85f..6491609402 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/customDataProviderImplementation.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/customDataProviderImplementation.ts @@ -4,11 +4,11 @@ import type { WorkbenchSession } from "@framework/WorkbenchSession"; import type { WorkbenchSettings } from "@framework/WorkbenchSettings"; import type { GlobalSettings } from "../framework/DataProviderManager/DataProviderManager"; -import type { MakeSettingTypesMap, Settings } from "../settings/settingsDefinitions"; +import type { Settings, SettingTypeDefinitions } from "../settings/settingsDefinitions"; import type { CustomSettingsHandler } from "./customSettingsHandler"; import type { NullableStoredData, StoredData } from "./sharedTypes"; -import type { AvailableValuesType, SettingsKeysFromTuple } from "./utils"; +import type { MakeSettingTypesMap, SettingsKeysFromTuple } from "./utils"; /** * This type is used to pass parameters to the fetchData method of a CustomDataProviderImplementation. @@ -19,7 +19,6 @@ export type DataProviderInformationAccessors< TData, TStoredData extends StoredData = Record, TSettingKey extends SettingsKeysFromTuple = SettingsKeysFromTuple, - TSettingTypes extends MakeSettingTypesMap = MakeSettingTypesMap, > = { /** * Access the data that the provider is currently storing. @@ -37,19 +36,19 @@ export type DataProviderInformationAccessors< * const value = getSetting("settingName"); * ``` */ - getSetting: (settingName: K) => TSettingTypes[K] | null; + getSetting: (settingName: K) => SettingTypeDefinitions[K]["externalValue"] | null; /** - * Access the available values of a setting. + * Access the value range of a setting. * @param settingName The name of the setting to access. - * @returns The available values of the setting. + * @returns The value range of the setting. * * @example * ```typescript - * const availableValues = getAvailableSettingValues("settingName"); + * const valueRange = getSettingValueRange("settingName"); * ``` */ - getAvailableSettingValues: (settingName: K) => AvailableValuesType | null; + getSettingValueRange: (settingName: K) => SettingTypeDefinitions[K]["valueRange"] | null; /** * Access the global settings of the data provider manager. @@ -95,8 +94,7 @@ export type AreSettingsValidArgs< TData, TStoredData extends StoredData = Record, TSettingKey extends SettingsKeysFromTuple = SettingsKeysFromTuple, - TSettingTypes extends MakeSettingTypesMap = MakeSettingTypesMap, -> = DataProviderInformationAccessors & { +> = DataProviderInformationAccessors & { reportError: (error: string) => void; }; diff --git a/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/customGroupImplementation.ts b/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/customGroupImplementation.ts index a6bdacdae2..6093011e0c 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/customGroupImplementation.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/customGroupImplementation.ts @@ -1,6 +1,7 @@ -import type { MakeSettingTypesMap, Settings } from "../settings/settingsDefinitions"; +import type { Settings } from "../settings/settingsDefinitions"; import type { DefineBasicDependenciesArgs } from "./customSettingsHandler"; +import type { MakeSettingTypesMap } from "./utils"; /** * This interface is describing what methods and members a custom group must implement. diff --git a/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingImplementation.ts b/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingImplementation.ts index 0a04f031a7..8c10d56d12 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingImplementation.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingImplementation.ts @@ -2,9 +2,6 @@ import type { WorkbenchSession } from "@framework/WorkbenchSession"; import type { WorkbenchSettings } from "@framework/WorkbenchSettings"; import type { GlobalSettings } from "../framework/DataProviderManager/DataProviderManager"; -import type { SettingCategory } from "../settings/settingsDefinitions"; - -import type { MakeAvailableValuesTypeBasedOnCategory } from "./utils"; export type OverriddenValueRepresentationArgs = { value: TValue; @@ -12,36 +9,95 @@ export type OverriddenValueRepresentationArgs = { workbenchSettings: WorkbenchSettings; }; -export type SettingComponentProps = { - onValueChange: (newValue: TValue) => void; - value: TValue; +// Base component props shared by both static and dynamic settings +type SettingComponentPropsBase = { + onValueChange: (newValue: TInternalValue | ((prevValue: TInternalValue) => TInternalValue)) => void; + value: TInternalValue; isValueValid: boolean; - overriddenValue: TValue | null; + overriddenValue: TInternalValue | null; isOverridden: boolean; - availableValues: MakeAvailableValuesTypeBasedOnCategory | null; workbenchSession: WorkbenchSession; workbenchSettings: WorkbenchSettings; globalSettings: GlobalSettings; }; -export interface CustomSettingImplementation { - defaultValue?: TValue; +// Component props for static settings (no valueRange) +export type StaticSettingComponentProps = SettingComponentPropsBase; + +// Component props for dynamic settings (with valueRange) +export type DynamicSettingComponentProps = SettingComponentPropsBase & { + valueRange: TValueRange; +}; + +// For backward compatibility - delegates to the correct type based on TValueRange +export type SettingComponentProps = [TValueRange] extends [never] + ? StaticSettingComponentProps + : DynamicSettingComponentProps; + +export type ValueRangeIntersectionReducerDefinition = { + startingValue: TStartingValue; + reducer: ( + accumulator: TValueRange | TStartingValue, + currentValueRange: TValueRange, + currentIndex: number, + ) => TValueRange; + isValid: (valueRange: TValueRange) => boolean; +}; + +// Base interface shared by both static and dynamic settings +type CustomSettingImplementationBase = { + defaultValue?: TInternalValue; + serializeValue?: (value: TInternalValue) => string; + deserializeValue?: (serializedValue: string) => TInternalValue; /** - * A static setting does not have any available values and is not dependent on any other settings. + * Type guard to validate that a deserialized value has the correct structure. + * This is used to catch malformed persisted values before they cause runtime errors. + * Return true if the value has the expected structure, false otherwise. + * Note: This should check the structure/type, not the validity of values within the structure. * - * @returns true if the setting is static, false otherwise. + * Implementation note: You can use the createStructureValidator helper to create + * a validator from a JTD schema, or write a custom validation function. */ - getIsStatic?: () => boolean; - makeComponent(): (props: SettingComponentProps) => React.ReactNode; - fixupValue?: ( - currentValue: TValue, - availableValues: MakeAvailableValuesTypeBasedOnCategory, - ) => TValue; - isValueValid?: ( - value: TValue, - availableValues: MakeAvailableValuesTypeBasedOnCategory, - ) => boolean; - serializeValue?: (value: TValue) => string; - deserializeValue?: (serializedValue: string) => TValue; - overriddenValueRepresentation?: (args: OverriddenValueRepresentationArgs) => React.ReactNode; -} + isValueValidStructure: (value: unknown) => value is TInternalValue; + overriddenValueRepresentation?: (args: OverriddenValueRepresentationArgs) => React.ReactNode; +}; + +/** + * Implementation for static settings (no valueRange). + * Static settings have fixed behavior and don't depend on external data. + */ +export type StaticSettingImplementation< + TInternalValue, + TExternalValue = TInternalValue, +> = CustomSettingImplementationBase & { + getIsStatic: () => boolean; + makeComponent(): (props: StaticSettingComponentProps) => React.ReactNode; + fixupValue?: (currentValue: TInternalValue) => TInternalValue; + isValueValid?: (value: TInternalValue) => boolean; + mapInternalToExternalValue: (internalValue: TInternalValue, valueRange: any) => TExternalValue; +}; + +/** + * Implementation for dynamic settings (with valueRange). + * Dynamic settings adapt their behavior based on available values (valueRange). + */ +export type DynamicSettingImplementation = + CustomSettingImplementationBase & { + getIsStatic?: () => boolean; + valueRangeIntersectionReducerDefinition: ValueRangeIntersectionReducerDefinition; + makeComponent(): (props: DynamicSettingComponentProps) => React.ReactNode; + fixupValue?: (currentValue: TInternalValue, valueRange: TValueRange) => TInternalValue; + isValueValid?: (value: TInternalValue, valueRange: TValueRange) => boolean; + mapInternalToExternalValue: (internalValue: TInternalValue, valueRange: TValueRange) => TExternalValue; + }; + +/** + * Main type for custom setting implementations. + * Automatically delegates to StaticSettingImplementation or DynamicSettingImplementation + * based on whether TValueRange is provided. + */ +export type CustomSettingImplementation = [ + TValueRange, +] extends [never] + ? StaticSettingImplementation + : DynamicSettingImplementation; diff --git a/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingsHandler.ts b/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingsHandler.ts index 01c1bdec3b..16a3501af0 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingsHandler.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/customSettingsHandler.ts @@ -5,10 +5,10 @@ import type { WorkbenchSettings } from "@framework/WorkbenchSettings"; import type { Dependency, NoUpdate } from "../delegates/_utils/Dependency"; import type { GlobalSettings } from "../framework/DataProviderManager/DataProviderManager"; -import type { MakeSettingTypesMap, Settings } from "../settings/settingsDefinitions"; +import type { Settings, SettingTypeDefinitions } from "../settings/settingsDefinitions"; import type { NullableStoredData, StoredData } from "./sharedTypes"; -import type { AvailableValuesType, SettingsKeysFromTuple } from "./utils"; +import type { MakeSettingTypesMap, SettingsKeysFromTuple } from "./utils"; export interface GetHelperDependency< TSettings extends Settings, @@ -55,10 +55,10 @@ export interface DefineDependenciesArgs< TKey extends SettingsKeysFromTuple = SettingsKeysFromTuple, TStoredDataKey extends keyof TStoredData = keyof TStoredData, > extends DefineBasicDependenciesArgs { - availableSettingsUpdater: ( + valueRangeUpdater: ( settingKey: TSettingKey, - update: UpdateFunc, TSettings, TSettingTypes, TKey>, - ) => Dependency, TSettings, TSettingTypes, TKey>; + update: UpdateFunc, + ) => Dependency; storedDataUpdater: ( key: K, update: UpdateFunc[TStoredDataKey], TSettings, TSettingTypes, TKey>, @@ -69,7 +69,7 @@ export interface DefineDependenciesArgs< getGlobalSetting: (settingName: T) => GlobalSettings[T]; getHelperDependency: ( helperDependency: Dependency, - ) => TDep | null; + ) => Awaited | null; abortSignal: AbortSignal; }) => T, ) => Dependency; @@ -102,19 +102,19 @@ export interface CustomSettingsHandler< /** * A method that defines the dependencies of the settings of the data provider. - * A dependency can either be an updater for the available values of a setting or a stored data object, or a helper dependency (e.g. a fetching operation). + * A dependency can either be an updater for the value range of a setting or a stored data object, or a helper dependency (e.g. a fetching operation). * * @param args An object containing the functions for defining the different dependencies. * * @example * ```typescript * defineDependencies({ - * availableSettingsUpdater, + * valueRangeUpdater, * storedDataUpdater, * helperDependency, * queryClient * }: DefineDependenciesArgs) { - * availableSettingsUpdater(SettingType.REALIZATION, ({ getGlobalSetting, getLocalSetting, getHelperDependency }) => { + * valueRangeUpdater(SettingType.REALIZATION, ({ getGlobalSetting, getLocalSetting, getHelperDependency }) => { * // Get global settings * const fieldIdentifier = getGlobalSetting("fieldId"); * @@ -127,8 +127,8 @@ export interface CustomSettingsHandler< * // Do something with the settings and data * ... * - * // Return the available values for the setting - * return availableValues; + * // Return the value range for the setting + * return valueRange; * }); * * // The same can be done with stored data diff --git a/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/utils.ts b/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/utils.ts index 8eff8a4d56..66868a8e97 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/utils.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/interfacesAndTypes/utils.ts @@ -1,39 +1,25 @@ -import type { - Setting, - SettingCategories, - SettingCategory, - SettingTypes, - Settings, -} from "../settings/settingsDefinitions"; +import type { Settings, SettingTypeDefinitions } from "../settings/settingsDefinitions"; -// Required when making "AvailableValuesType" for all settings in an object ("TSettings") -export type EachAvailableValuesType = TSetting extends Setting ? AvailableValuesType : never; +export type TupleIndices = Extract; +export type SettingsKeysFromTuple = TSettings[TupleIndices]; -// Returns an array of "TValue" if the "TValue" itself is not already an array -export type AvailableValuesType = MakeAvailableValuesTypeBasedOnCategory< - SettingTypes[TSetting], - SettingCategories[TSetting] ->; +export type MakeSettingTypesMap< + T extends readonly (keyof SettingTypeDefinitions)[], + AllowNull extends boolean = false, +> = + IsStrictlyAny extends true + ? any + : { + [K in T[number]]: AllowNull extends false + ? SettingTypeDefinitions[K]["externalValue"] + : SettingTypeDefinitions[K]["externalValue"] | null; + }; -export type MakeAvailableValuesTypeBasedOnCategory = TCategory extends - | SettingCategory.SINGLE_SELECT - | SettingCategory.MULTI_SELECT - ? RemoveUnknownFromArray> - : TCategory extends SettingCategory.NUMBER - ? [Exclude, Exclude] - : TCategory extends SettingCategory.BOOLEAN_NUMBER - ? [number, number] - : TCategory extends SettingCategory.XYZ_RANGE - ? [[number, number, number], [number, number, number], [number, number, number]] - : TCategory extends SettingCategory.NUMBER_WITH_STEP - ? [number, number, number] - : TCategory extends SettingCategory.XYZ_VALUES_WITH_VISIBILITY - ? [[number, number, number], [number, number, number], [number, number, number]] - : never; +// From: https://stackoverflow.com/a/50375286/62076 +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; -export type TupleIndices = Extract; -export type SettingsKeysFromTuple = TSettings[TupleIndices]; +// If T is `any` a union of both side of the condition is returned. +type UnionForAny = T extends never ? "A" : "B"; -// "MakeArrayIfNotArray" ypields "unknown[] | any[]" for "T = any" - we don't want "unknown[]" -type RemoveUnknownFromArray = T extends (infer U)[] ? ([unknown] extends [U] ? any[] : T) : T; -type MakeArrayIfNotArray = Exclude extends Array ? Array : Array>; +// Returns true if type is any, or false for any other type. +type IsStrictlyAny = UnionToIntersection> extends never ? true : false; diff --git a/frontend/src/modules/_shared/DataProviderFramework/settings/SettingRegistry/_SettingRegistry.ts b/frontend/src/modules/_shared/DataProviderFramework/settings/SettingRegistry/_SettingRegistry.ts index cd573bd83f..8e8c622f4c 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/settings/SettingRegistry/_SettingRegistry.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/settings/SettingRegistry/_SettingRegistry.ts @@ -1,8 +1,10 @@ import { SettingManager } from "../../framework/SettingManager/SettingManager"; -import type { CustomSettingImplementation } from "../../interfacesAndTypes/customSettingImplementation"; - -import type { Setting, SettingCategories, SettingTypes } from "./../settingsDefinitions"; -import { settingCategories } from "./../settingsDefinitions"; +import type { + CustomSettingImplementation, + DynamicSettingImplementation, + StaticSettingImplementation, +} from "../../interfacesAndTypes/customSettingImplementation"; +import type { Setting, SettingTypeDefinitions } from "../settingsDefinitions"; export class SettingRegistry { private static _registeredSettings: Map< @@ -10,7 +12,9 @@ export class SettingRegistry { { label: string; customSettingImplementation: { - new (customConstructorParameters?: any): CustomSettingImplementation; + new ( + customConstructorParameters?: any, + ): StaticSettingImplementation | DynamicSettingImplementation; }; customConstructorParameters?: any; } @@ -18,10 +22,23 @@ export class SettingRegistry { static registerSetting< TSetting extends Setting, - TValue extends SettingTypes[TSetting] = SettingTypes[TSetting], - TCategory extends SettingCategories[TSetting] = SettingCategories[TSetting], - TSettingImpl extends new (...args: any) => CustomSettingImplementation = { - new (params?: any): CustomSettingImplementation; + TSettingDef extends SettingTypeDefinitions[TSetting] = SettingTypeDefinitions[TSetting], + TSettingImpl extends new ( + ...args: any + ) => + | StaticSettingImplementation + | DynamicSettingImplementation< + TSettingDef["internalValue"], + TSettingDef["externalValue"], + TSettingDef["valueRange"] + > = { + new ( + params?: any, + ): CustomSettingImplementation< + TSettingDef["internalValue"], + TSettingDef["externalValue"], + TSettingDef["valueRange"] + >; }, >( type: TSetting, @@ -43,7 +60,7 @@ export class SettingRegistry { static makeSetting( type: TSetting, - defaultValue?: SettingTypes[TSetting], + defaultValue?: SettingTypeDefinitions[TSetting]["internalValue"], ): SettingManager { const stored = this._registeredSettings.get(type); if (!stored) { @@ -53,10 +70,13 @@ export class SettingRegistry { return new SettingManager({ type, - category: settingCategories[type], label: stored.label, defaultValue: defaultValue ?? customSettingImpl.defaultValue ?? null, - customSettingImplementation: customSettingImpl, + customSettingImplementation: customSettingImpl as CustomSettingImplementation< + SettingTypeDefinitions[TSetting]["internalValue"] | null, + SettingTypeDefinitions[TSetting]["externalValue"] | null, + SettingTypeDefinitions[TSetting]["valueRange"] + >, }); } } diff --git a/frontend/src/modules/_shared/DataProviderFramework/settings/SettingRegistry/_registerAllSettings.ts b/frontend/src/modules/_shared/DataProviderFramework/settings/SettingRegistry/_registerAllSettings.ts index 374dfdbe4c..f4af7095ef 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/settings/SettingRegistry/_registerAllSettings.ts +++ b/frontend/src/modules/_shared/DataProviderFramework/settings/SettingRegistry/_registerAllSettings.ts @@ -14,11 +14,13 @@ import { DropdownNumberSetting } from "../implementations/DropdownNumberSetting" import { DropdownStringSetting } from "../implementations/DropdownStringSetting"; import { EnsembleSetting } from "../implementations/EnsembleSetting"; import { GridLayerRangeSetting } from "../implementations/GridLayerRangeSetting"; -import { Direction as GridLayerDirection, GridLayerSetting } from "../implementations/GridLayerSetting"; import { InputNumberSetting } from "../implementations/InputNumberSetting"; import { IntersectionSetting } from "../implementations/IntersectionSetting"; import { LogCurveSetting } from "../implementations/LogCurveSetting"; +import { NumberRangeDropdownSetting } from "../implementations/NumberRangeDropdownSetting"; +import { PdmFilterSetting } from "../implementations/PdmFilterSetting"; import { PolygonVisualizationSetting } from "../implementations/PolygonVisualizationSetting"; +import { RadioGroupSetting } from "../implementations/RadioGroupSetting"; import { RepresentationSetting } from "../implementations/RepresentationSetting"; import { SeismicSliceSetting } from "../implementations/SeismicSliceSetting"; import { SelectNumberSetting } from "../implementations/SelectNumberSetting"; @@ -26,9 +28,11 @@ import { SelectStringSetting } from "../implementations/SelectStringSetting"; import { SensitivitySetting } from "../implementations/SensitivitySetting"; import { SingleColorSetting } from "../implementations/SingleColorSetting"; import { SliderNumberSetting } from "../implementations/SliderNumberSetting"; +import { SliderRangeSetting } from "../implementations/SliderRangeSetting"; import { StaticRotationSetting } from "../implementations/StaticRotationSetting"; import { StatisticFunctionSetting } from "../implementations/StatisticFunctionSetting"; import { TimeOrIntervalSetting } from "../implementations/TimeOrIntervalSetting"; +import { WellboreDepthFilterSetting } from "../implementations/WellboreDepthFilterSetting"; import { Setting } from "../settingsDefinitions"; import { SettingRegistry } from "./_SettingRegistry"; @@ -96,9 +100,7 @@ SettingRegistry.registerSetting(Setting.COLOR_SET, "Color Set", ColorSetSetting) SettingRegistry.registerSetting(Setting.CONTOURS, "Contours", BooleanNumberSetting, { customConstructorParameters: [{ min: 10, max: 200 }], }); -SettingRegistry.registerSetting(Setting.GRID_LAYER_K, "Grid Layer K", GridLayerSetting, { - customConstructorParameters: [GridLayerDirection.K], -}); +SettingRegistry.registerSetting(Setting.GRID_LAYER_K, "Grid Layer K", NumberRangeDropdownSetting); SettingRegistry.registerSetting(Setting.GRID_LAYER_RANGE, "Grid Ranges", GridLayerRangeSetting); SettingRegistry.registerSetting(Setting.GRID_NAME, "Grid Name", DropdownStringSetting); SettingRegistry.registerSetting(Setting.INTERSECTION, "Intersection", IntersectionSetting); @@ -122,7 +124,7 @@ SettingRegistry.registerSetting(Setting.SEISMIC_SLICES, "Seismic Slices", Seismi SettingRegistry.registerSetting(Setting.SENSITIVITY, "Sensitivity", SensitivitySetting); SettingRegistry.registerSetting(Setting.SHOW_GRID_LINES, "Show Grid Lines", BooleanSetting); SettingRegistry.registerSetting(Setting.SMDA_INTERPRETER, "SMDA Interpreter", DropdownStringSetting); -SettingRegistry.registerSetting(Setting.SMDA_WELLBORE_HEADERS, "SMDA Wellbore Headers", DrilledWellboresSetting); +SettingRegistry.registerSetting(Setting.WELLBORES, "Wellbores", DrilledWellboresSetting); SettingRegistry.registerSetting(Setting.STATISTIC_FUNCTION, "Statistic Function", StatisticFunctionSetting); SettingRegistry.registerSetting(Setting.STRAT_COLUMN, "Stratigraphic Column", DropdownStringSetting); SettingRegistry.registerSetting(Setting.SURFACE_NAME, "Surface Name", DropdownStringSetting); @@ -137,3 +139,36 @@ SettingRegistry.registerSetting(Setting.WELLBORE_EXTENSION_LENGTH, "Wellbore Ext }); SettingRegistry.registerSetting(Setting.WELLBORE_PICKS, "Wellbore Picks", DrilledWellborePicksSetting); SettingRegistry.registerSetting(Setting.REPRESENTATION, "Representation", RepresentationSetting); +SettingRegistry.registerSetting(Setting.WELLBORE_DEPTH_FILTER_TYPE, "Depth Filter", RadioGroupSetting, { + customConstructorParameters: [ + { + staticOptions: [ + { value: "none", label: "None" }, + { value: "md_range", label: "MD" }, + { value: "tvd_range", label: "TVD" }, + { value: "surface_based", label: "Surface" }, + ], + layout: "horizontal", + }, + ], +}); +SettingRegistry.registerSetting(Setting.MD_RANGE, "MD Range", SliderRangeSetting); +SettingRegistry.registerSetting(Setting.TVD_RANGE, "TVD Range", SliderRangeSetting); +SettingRegistry.registerSetting(Setting.WELLBORE_DEPTH_FILTER_ATTRIBUTE, "Surface Attribute", DropdownStringSetting); +SettingRegistry.registerSetting( + Setting.WELLBORE_DEPTH_FORMATION_FILTER, + "Formation Filter", + WellboreDepthFilterSetting, +); +SettingRegistry.registerSetting(Setting.PDM_FILTER, "Flow Data Cut-off", PdmFilterSetting); +SettingRegistry.registerSetting(Setting.PDM_FILTER_TYPE, "Flow Data Filter", RadioGroupSetting, { + customConstructorParameters: [ + { + staticOptions: [ + { value: "none", label: "None" }, + { value: "production_injection", label: "Production/Injection" }, + ], + layout: "horizontal", + }, + ], +}); diff --git a/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/BooleanNumberSetting.tsx b/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/BooleanNumberSetting.tsx index deb2cbd77b..741d4a353d 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/BooleanNumberSetting.tsx +++ b/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/BooleanNumberSetting.tsx @@ -8,36 +8,71 @@ import type { CustomSettingImplementation, SettingComponentProps, } from "../../interfacesAndTypes/customSettingImplementation"; -import type { MakeAvailableValuesTypeBasedOnCategory } from "../../interfacesAndTypes/utils"; -import type { SettingCategory } from "../settingsDefinitions"; type ValueType = { enabled: boolean; value: number; } | null; +type ValueRangeType = [number, number] | null; + type StaticProps = { min?: number; max?: number }; -export class BooleanNumberSetting implements CustomSettingImplementation { +export class BooleanNumberSetting implements CustomSettingImplementation { private _staticProps: StaticProps | null; + mapInternalToExternalValue(internalValue: ValueType): ValueType { + return internalValue; + } + + valueRangeIntersectionReducerDefinition = { + reducer: (accumulator: ValueRangeType, valueRange: ValueRangeType) => { + if (accumulator === null) { + return valueRange; + } + if (valueRange === null) { + return accumulator; + } + + const min = Math.max(accumulator[0], valueRange[0]); + const max = Math.min(accumulator[1], valueRange[1]); + + if (min > max) { + return null; + } + return [min, max] as ValueRangeType; + }, + startingValue: null, + isValid: (valueRange: ValueRangeType): boolean => { + if (valueRange === null) { + return true; + } + return valueRange[0] <= valueRange[1]; + }, + }; + constructor(props: StaticProps) { - if ( - props && - props.min != null && - props.max != null && - props.min > props.max - ) { + if (props && props.min != null && props.max != null && props.min > props.max) { throw new Error("Min value cannot be greater than max value"); } this._staticProps = props ?? null; } - isValueValid( - value: ValueType, - availableValues: MakeAvailableValuesTypeBasedOnCategory, - ): boolean { + isValueValidStructure(value: unknown): value is ValueType { + if (value === null) { + return true; + } + + if (typeof value !== "object" || Array.isArray(value)) { + return false; + } + + const v = value as Record; + return typeof v.enabled === "boolean" && typeof v.value === "number"; + } + + isValueValid(value: ValueType, valueRange: ValueRangeType): boolean { if (value === null) { return true; } @@ -54,11 +89,11 @@ export class BooleanNumberSetting implements CustomSettingImplementation, - ): ValueType { + fixupValue(currentValue: ValueType, valueRange: ValueRangeType): ValueType { if (currentValue === null) { // Default: boolean false, number at minimum value or 0 let defaultNumber = 0; if (this._staticProps) { defaultNumber = this._staticProps.min ?? 0; - } else if (availableValues) { - defaultNumber = availableValues[0]; + } else if (valueRange) { + defaultNumber = valueRange[0]; } return { enabled: false, @@ -100,12 +132,12 @@ export class BooleanNumberSetting implements CustomSettingImplementation) => React.ReactNode { + makeComponent(): (props: SettingComponentProps) => React.ReactNode { const isStatic = this.getIsStatic(); const staticProps = this._staticProps; - return function BooleanNumberSetting(props: SettingComponentProps) { - const defaultMin = isStatic ? (staticProps?.min ?? 0) : (props.availableValues?.[0] ?? 0); - + return function BooleanNumberSetting(props: SettingComponentProps) { + const defaultMin = isStatic ? (staticProps?.min ?? 0) : (props.valueRange?.[0] ?? 0); const { enabled, value } = props.value ?? { enabled: false, value: defaultMin }; - const min = isStatic ? (staticProps?.min ?? 0) : (props.availableValues?.[0] ?? 0); - const max = isStatic ? (staticProps?.max ?? 100) : (props.availableValues?.[1] ?? 100); + const min = isStatic ? (staticProps?.min ?? 0) : (props.valueRange?.[0] ?? 0); + const max = isStatic ? (staticProps?.max ?? 100) : (props.valueRange?.[1] ?? 100); function handleBooleanChange(e: ChangeEvent) { props.onValueChange({ enabled: e.target.checked, value }); diff --git a/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/BooleanSetting.tsx b/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/BooleanSetting.tsx index 5498d8dfcb..fa63287241 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/BooleanSetting.tsx +++ b/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/BooleanSetting.tsx @@ -7,10 +7,17 @@ import type { CustomSettingImplementation, SettingComponentProps, } from "../../interfacesAndTypes/customSettingImplementation"; -import type { SettingCategory } from "../settingsDefinitions"; type ValueType = boolean; -export class BooleanSetting implements CustomSettingImplementation { +export class BooleanSetting implements CustomSettingImplementation { + mapInternalToExternalValue(internalValue: ValueType): ValueType { + return internalValue; + } + + isValueValidStructure(value: unknown): value is ValueType { + return typeof value === "boolean"; + } + isValueValid(): boolean { return true; } @@ -19,8 +26,8 @@ export class BooleanSetting implements CustomSettingImplementation) => React.ReactNode { - return function BooleanSwitch(props: SettingComponentProps) { + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function BooleanSwitch(props: SettingComponentProps) { function handleChange(e: ChangeEvent) { props.onValueChange(e.target.checked); } diff --git a/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/ColorScaleSetting.tsx b/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/ColorScaleSetting.tsx index 3c0d9dd640..84b81aed0b 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/ColorScaleSetting.tsx +++ b/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/ColorScaleSetting.tsx @@ -11,11 +11,10 @@ import type { OverriddenValueRepresentationArgs, SettingComponentProps, } from "../../interfacesAndTypes/customSettingImplementation"; -import type { SettingCategory } from "../settingsDefinitions"; type ValueType = ColorScaleSpecification | null; -export class ColorScaleSetting implements CustomSettingImplementation { +export class ColorScaleSetting implements CustomSettingImplementation { defaultValue: ValueType; constructor(props?: { initialColorScale?: ColorScaleSpecification }) { @@ -30,14 +29,29 @@ export class ColorScaleSetting implements CustomSettingImplementation; + return ( + typeof v.areBoundariesUserDefined === "boolean" && typeof v.colorScale === "object" && v.colorScale !== null + ); + } + isValueValid(): boolean { return true; } @@ -51,7 +65,7 @@ export class ColorScaleSetting implements CustomSettingImplementation) => React.ReactNode { - return function ColorScaleSelectorDialog(props: SettingComponentProps) { + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function ColorScaleSelectorDialog(props: SettingComponentProps) { function handleChange(value: ColorScaleSpecification) { props.onValueChange(value); } diff --git a/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/ColorSetSetting.tsx b/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/ColorSetSetting.tsx index c352130169..a32be76df8 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/ColorSetSetting.tsx +++ b/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/ColorSetSetting.tsx @@ -10,21 +10,29 @@ import type { OverriddenValueRepresentationArgs, SettingComponentProps, } from "../../interfacesAndTypes/customSettingImplementation"; -import type { SettingCategory } from "../settingsDefinitions"; type ValueType = ColorSet | null; -export class ColorSetSetting implements CustomSettingImplementation { +export class ColorSetSetting implements CustomSettingImplementation { defaultValue: ValueType = new ColorSet(defaultColorPalettes[0]); - getLabel(): string { - return "Colors"; + mapInternalToExternalValue(internalValue: ValueType): ValueType { + return internalValue; } getIsStatic(): boolean { return true; } + isValueValidStructure(value: unknown): value is ValueType { + if (value === null) { + return true; + } + + // ColorSet is a class instance, check if it has the expected methods + return typeof value === "object" && value !== null && "getColorPalette" in value; + } + isValueValid(): boolean { return true; } @@ -39,8 +47,8 @@ export class ColorSetSetting implements CustomSettingImplementation) => React.ReactNode { - return function ColorScaleSelectorDialog(props: SettingComponentProps) { + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function ColorScaleSelectorDialog(props: SettingComponentProps) { function handleColorPaletteChange(value: ColorPalette) { const newColorSet = new ColorSet(value); props.onValueChange(newColorSet); diff --git a/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/DrilledWellborePicksSetting.tsx b/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/DrilledWellborePicksSetting.tsx index bb170cad70..8815bf9f9c 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/DrilledWellborePicksSetting.tsx +++ b/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/DrilledWellborePicksSetting.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import type React from "react"; import type { WellborePick_api } from "@api"; import type { SelectOption } from "@lib/components/Select"; @@ -8,62 +8,50 @@ import type { CustomSettingImplementation, SettingComponentProps, } from "../../interfacesAndTypes/customSettingImplementation"; -import type { MakeAvailableValuesTypeBasedOnCategory } from "../../interfacesAndTypes/utils"; -import type { SettingCategory } from "../settingsDefinitions"; +import { isStringArrayOrNull } from "../utils/structureValidation"; -type ValueType = WellborePick_api[] | null; +import { fixupValue, isValueValid, makeValueRangeIntersectionReducerDefinition } from "./_shared/arrayMultiSelect"; + +type InternalValueType = string[] | null; +type ExternalValueType = WellborePick_api[] | null; +type ValueRangeType = WellborePick_api[]; export class DrilledWellborePicksSetting - implements CustomSettingImplementation + implements CustomSettingImplementation { - defaultValue: ValueType = null; + defaultValue: InternalValueType = null; + valueRangeIntersectionReducerDefinition = makeValueRangeIntersectionReducerDefinition( + (a, b) => a.wellboreUuid === b.wellboreUuid, + ); - getLabel(): string { - return "Drilled wellbore picks"; + mapInternalToExternalValue(internalValue: InternalValueType, valueRange: ValueRangeType): ExternalValueType { + return valueRange.filter((pick) => internalValue?.includes(pick.pickIdentifier) ?? false); } - isValueValid( - currentValue: ValueType, - availableValues: MakeAvailableValuesTypeBasedOnCategory, - ): boolean { - if (!currentValue) { - return availableValues.length !== 0; - } - - // Check if every element in currentValue is in availableValues - const isValid = currentValue.every((value) => - availableValues.some( - (availableValue) => - availableValue.pickIdentifier === value.pickIdentifier && - availableValue.interpreter === value.interpreter, - ), - ); - - return isValid; + isValueValidStructure(value: unknown): value is InternalValueType { + return isStringArrayOrNull(value); } - fixupValue( - currentValue: ValueType, - availableValues: MakeAvailableValuesTypeBasedOnCategory, - ): ValueType { - if (!currentValue) { - return availableValues; + fixupValue(currentValue: InternalValueType, valueRange: ValueRangeType): InternalValueType { + const fixedValue = fixupValue(currentValue, valueRange, mappingFunc, "allAvailable"); + + if (fixedValue.length === 0) { + return valueRange.map(mappingFunc); } - // Filter new/available values with old/previously selected pickIdentifiers - const matchingNewValues = availableValues.filter((newValue) => - currentValue.some((oldValue) => oldValue.pickIdentifier === newValue.pickIdentifier), - ); + return fixedValue; + } - if (matchingNewValues.length === 0) { - return availableValues; + isValueValid(currentValue: InternalValueType, valueRange: ValueRangeType): boolean { + function mappingFunc(value: WellborePick_api): string { + return value.pickIdentifier; } - return matchingNewValues; + return isValueValid(currentValue, valueRange, mappingFunc); } - makeComponent(): (props: SettingComponentProps) => React.ReactNode { - return function DrilledWellborePicks(props: SettingComponentProps) { - const availableValues = props.availableValues ?? []; + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function DrilledWellborePicks(props: SettingComponentProps) { + const availableValues = props.valueRange ?? []; // Prevent duplicated pickIdentifiers in the options const uniquePickIdentifiers = Array.from(new Set(availableValues.map((ident) => ident.pickIdentifier))); @@ -73,24 +61,15 @@ export class DrilledWellborePicksSetting })); function handleChange(selectedIdentifiers: string[]) { - // Match all WellborePicks with selected pickIdentifiers - const selectedWellbores = availableValues.filter((elm) => - selectedIdentifiers.includes(elm.pickIdentifier), - ); - props.onValueChange(selectedWellbores); + props.onValueChange(selectedIdentifiers); } - const selectedValues = React.useMemo( - () => props.value?.map((ident) => ident.pickIdentifier) ?? [], - [props.value], - ); - return (
{ + valueRangeIntersectionReducerDefinition = makeValueRangeIntersectionReducerDefinition(); + + mapInternalToExternalValue(internalValue: ValueType): ValueType { + return internalValue; + } + + isValueValidStructure(value: unknown): value is ValueType { + return isNumberOrNull(value); + } + + isValueValid(value: ValueType, valueRange: ValueRangeType): boolean { + return isValueValid(value, valueRange, (v) => v); + } + + fixupValue(value: ValueType, valueRange: ValueRangeType): ValueType { + return fixupValue(value, valueRange, (v) => v); + } -export class DropdownNumberSetting implements CustomSettingImplementation { - makeComponent(): (props: SettingComponentProps) => React.ReactNode { - return function DropdownNumberSetting(props: SettingComponentProps) { - const availableValues = props.availableValues ?? []; + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function DropdownNumberSetting(props: SettingComponentProps) { + const availableValues = props.valueRange ?? []; const options: DropdownOption[] = availableValues.map((value) => { return { diff --git a/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/DropdownStringSetting.tsx b/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/DropdownStringSetting.tsx index 9adf84a8fa..b6c33e5e37 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/DropdownStringSetting.tsx +++ b/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/DropdownStringSetting.tsx @@ -7,12 +7,24 @@ import type { CustomSettingImplementation, SettingComponentProps, } from "../../interfacesAndTypes/customSettingImplementation"; -import type { SettingCategory } from "../settingsDefinitions"; +import { isStringOrNull } from "../utils/structureValidation"; + +import { fixupValue, isValueValid, makeValueRangeIntersectionReducerDefinition } from "./_shared/arraySingleSelect"; type ValueType = string | null; +type ValueRangeType = string[]; -export class DropdownStringSetting implements CustomSettingImplementation { +export class DropdownStringSetting implements CustomSettingImplementation { private _staticOptions: DropdownOptionOrGroup[] | null = null; + valueRangeIntersectionReducerDefinition = makeValueRangeIntersectionReducerDefinition(); + + mapInternalToExternalValue(internalValue: ValueType): ValueType { + return internalValue; + } + + isValueValidStructure(value: unknown): value is ValueType { + return isStringOrNull(value); + } constructor(props?: { options?: ValueType[] | DropdownOptionOrGroup[] }) { if (!props?.options) return; @@ -30,17 +42,25 @@ export class DropdownStringSetting implements CustomSettingImplementation) => React.ReactNode { + isValueValid(value: ValueType, valueRange: ValueRangeType): boolean { + return isValueValid(value, valueRange, (v) => v); + } + + fixupValue(value: ValueType, valueRange: ValueRangeType): ValueType { + return fixupValue(value, valueRange, (v) => v); + } + + makeComponent(): (props: SettingComponentProps) => React.ReactNode { const isStatic = this.getIsStatic(); const staticOptions = this._staticOptions; - return function DropdownStringSetting(props: SettingComponentProps) { + return function DropdownStringSetting(props: SettingComponentProps) { let options: DropdownOptionOrGroup[]; if (isStatic && staticOptions) { options = staticOptions; - } else if (!isStatic && props.availableValues) { - options = props.availableValues.map((value) => ({ + } else if (!isStatic && props.valueRange) { + options = props.valueRange.map((value) => ({ value: value, label: value === null ? "None" : value, })); diff --git a/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/EnsembleSetting.tsx b/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/EnsembleSetting.tsx index 21e5c238d4..8bf082a068 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/EnsembleSetting.tsx +++ b/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/EnsembleSetting.tsx @@ -9,14 +9,40 @@ import type { OverriddenValueRepresentationArgs, SettingComponentProps, } from "../../interfacesAndTypes/customSettingImplementation"; -import type { SettingCategory } from "../settingsDefinitions"; + +import { fixupValue, isValueValid, makeValueRangeIntersectionReducerDefinition } from "./_shared/arraySingleSelect"; type ValueType = RegularEnsembleIdent | null; -export class EnsembleSetting implements CustomSettingImplementation { +type ValueRangeType = RegularEnsembleIdent[]; + +export class EnsembleSetting implements CustomSettingImplementation { defaultValue: ValueType = null; + valueRangeIntersectionReducerDefinition = makeValueRangeIntersectionReducerDefinition(); + + mapInternalToExternalValue(internalValue: ValueType): ValueType { + return internalValue; + } + + isValueValidStructure(value: unknown): value is ValueType { + if (value === null) { + return true; + } + + // RegularEnsembleIdent is a class instance, check if it has expected methods + return typeof value === "object" && value !== null && "equals" in value && "toString" in value; + } + + isValueValid(value: ValueType, valueRange: ValueRangeType): boolean { + return isValueValid( + value, + valueRange, + (v) => v, + (a, b) => a.equals(b), + ); + } - getLabel(): string { - return "Ensemble"; + fixupValue(value: ValueType, valueRange: ValueRangeType): ValueType { + return fixupValue(value, valueRange, (v) => v); } serializeValue(value: ValueType): string { @@ -27,9 +53,9 @@ export class EnsembleSetting implements CustomSettingImplementation) => React.ReactNode { - return function EnsembleSelect(props: SettingComponentProps) { - const availableValues = props.availableValues ?? []; + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function EnsembleSelect(props: SettingComponentProps) { + const availableValues = props.valueRange ?? []; const ensembles = props.globalSettings.ensembles.filter((ensemble) => availableValues.some((value) => value.equals(ensemble.getIdent())), diff --git a/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/GridLayerRangeSetting.tsx b/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/GridLayerRangeSetting.tsx index c46f8c1a92..9807783047 100644 --- a/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/GridLayerRangeSetting.tsx +++ b/frontend/src/modules/_shared/DataProviderFramework/settings/implementations/GridLayerRangeSetting.tsx @@ -2,8 +2,11 @@ import React from "react"; import { cloneDeep, isEqual } from "lodash"; +import type { Grid3dZone_api } from "@api"; import { Button } from "@lib/components/Button"; +import { Dropdown } from "@lib/components/Dropdown"; import { Input } from "@lib/components/Input"; +import { RadioGroup } from "@lib/components/RadioGroup"; import { Slider } from "@lib/components/Slider"; import { useElementSize } from "@lib/hooks/useElementSize"; import { resolveClassNames } from "@lib/utils/resolveClassNames"; @@ -12,55 +15,251 @@ import type { CustomSettingImplementation, SettingComponentProps, } from "../../interfacesAndTypes/customSettingImplementation"; -import type { SettingCategory } from "../settingsDefinitions"; +import { isNumberTuple } from "../utils/structureValidation"; -type ValueType = [[number, number], [number, number], [number, number]] | null; -type Category = SettingCategory.XYZ_RANGE; +type InternalValueType = { + i: [number, number]; + j: [number, number]; + k: { type: "range"; range: [number, number] } | { type: "zone"; range: [number, number]; name: string }; +} | null; +type ExternalValueType = [[number, number], [number, number], [number, number]] | null; +type ValueRangeType = { + range: { i: [number, number, number]; j: [number, number, number]; k: [number, number, number] }; + zones: Grid3dZone_api[]; +} | null; -export class GridLayerRangeSetting implements CustomSettingImplementation { - defaultValue: ValueType = null; +export class GridLayerRangeSetting + implements CustomSettingImplementation +{ + defaultValue: InternalValueType = null; + valueRangeIntersectionReducerDefinition = { + reducer: (accumulator: ValueRangeType, valueRange: ValueRangeType, index: number) => { + if (index === 0) { + return valueRange; + } + + if (valueRange === null || accumulator === null) { + return null; + } + + const mergedRanges: NonNullable["range"] = { + i: [0, 0, 1], + j: [0, 0, 1], + k: [0, 0, 1], + }; + + for (const key of ["i", "j", "k"] as const) { + const min = Math.max(accumulator.range[key][0], valueRange?.range[key][0]); + const max = Math.min(accumulator.range[key][1], valueRange?.range[key][1]); + const step = Math.max(accumulator.range[key][2], valueRange?.range[key][2]); + + mergedRanges[key] = [min, max, step]; + } + + const mergedZones = accumulator.zones.filter((zoneA) => + valueRange.zones.some( + (zoneB) => + zoneA.name === zoneB.name && + zoneA.start_layer === zoneB.start_layer && + zoneA.end_layer === zoneB.end_layer, + ), + ); + + return { range: mergedRanges, zones: mergedZones }; + }, + startingValue: null, + isValid: (valueRange: ValueRangeType): boolean => { + if (valueRange === null) { + return false; + } + const { i: iRange, j: jRange, k: kRange } = valueRange.range; + return iRange[0] <= iRange[1] && jRange[0] <= jRange[1] && kRange[0] <= kRange[1]; + }, + }; + + isValueValidStructure(value: unknown): value is InternalValueType { + // null is always valid + if (value === null) { + return true; + } + + // Check if value is an object (not array, not null) + if (typeof value !== "object" || Array.isArray(value)) { + return false; + } + + const v = value as Record; - getLabel(): string { - return "Grid ranges"; + // Check 'i' property - must be [number, number] + if (!isNumberTuple(v.i, 2)) { + return false; + } + + // Check 'j' property - must be [number, number] + if (!isNumberTuple(v.j, 2)) { + return false; + } + + // Check 'k' property exists and is an object + if (typeof v.k !== "object" || v.k === null || Array.isArray(v.k)) { + return false; + } + + const k = v.k as Record; + + // Check 'k.type' is either "range" or "zone" + if (k.type !== "range" && k.type !== "zone") { + return false; + } + + // Check 'k.range' is [number, number] + if (!isNumberTuple(k.range, 2)) { + return false; + } + + // For zone type, check 'k.name' is a string + if (k.type === "zone" && typeof k.name !== "string") { + return false; + } + + return true; } - makeComponent(): (props: SettingComponentProps) => React.ReactNode { - return function RangeSlider(props: SettingComponentProps) { + mapInternalToExternalValue(internalValue: InternalValueType): ExternalValueType { + if (internalValue === null) { + return null; + } + + return [internalValue.i, internalValue.j, internalValue.k.range]; + } + + isValueValid(value: InternalValueType, valueRange: ValueRangeType): boolean { + if (value === null || valueRange === null) { + return false; + } + + const { i: iRange, j: jRange, k: kRange } = valueRange.range; + const [xmin, xmax] = value.i; + const [ymin, ymax] = value.j; + const type = value.k.type; + + if (type === "range") { + const [zmin, zmax] = value.k.range; + + return ( + xmin >= iRange[0] && + xmin <= iRange[1] && + xmax >= iRange[0] && + xmax <= iRange[1] && + ymin >= jRange[0] && + ymin <= jRange[1] && + ymax >= jRange[0] && + ymax <= jRange[1] && + zmin >= kRange[0] && + zmin <= kRange[1] && + zmax >= kRange[0] && + zmax <= kRange[1] + ); + } else if (type === "zone") { + const zoneName = value.k.name; + const [zmin, zmax] = value.k.range; + const zoneExists = valueRange.zones.some( + (zone) => zone.name === zoneName && zone.start_layer === zmin && zone.end_layer === zmax, + ); + if (!zoneExists) { + return false; + } + return true; + } + + throw new Error(`Unknown type: ${type}`); + } + + fixupValue(currentValue: InternalValueType, valueRange: ValueRangeType): InternalValueType { + if (valueRange === null) { + return null; + } + const { i: iRange, j: jRange, k: kRange } = valueRange.range; + + if (currentValue === null) { + return { + i: [iRange[0], iRange[1]], + j: [jRange[0], jRange[1]], + k: { type: "range", range: [kRange[0], kRange[1]] }, + }; + } + + const [iMin, iMax] = currentValue.i; + const [jMin, jMax] = currentValue.j; + + const newIRange: [number, number] = [Math.max(iRange[0], iMin), Math.min(iRange[1], iMax)]; + const newJRange: [number, number] = [Math.max(jRange[0], jMin), Math.min(jRange[1], jMax)]; + + const type = currentValue.k.type; + + if (type === "range") { + const [zmin, zmax] = currentValue.k.range; + return { + i: newIRange, + j: newJRange, + k: { type: "range", range: [Math.max(kRange[0], zmin), Math.min(kRange[1], zmax)] }, + }; + } + + if (type === "zone") { + const zoneName = currentValue.k.name; + const [zmin, zmax] = currentValue.k.range; + const zoneExists = valueRange.zones.some( + (zone) => zone.name === zoneName && zone.start_layer === zmin && zone.end_layer === zmax, + ); + if (zoneExists) { + return { i: newIRange, j: newJRange, k: { type: "zone", range: [zmin, zmax], name: zoneName } }; + } else { + return { + i: newIRange, + j: newJRange, + k: { type: "range", range: [Math.max(kRange[0], zmin), Math.min(kRange[1], zmax)] }, + }; + } + } + + throw new Error(`Unknown type: ${type}`); + } + + makeComponent(): (props: SettingComponentProps) => React.ReactNode { + return function RangeSlider(props: SettingComponentProps) { const divRef = React.useRef(null); const divSize = useElementSize(divRef); - const availableValues = props.availableValues ?? [ - [0, 0, 1], - [0, 0, 1], - [0, 0, 1], - ]; + const valueRange: NonNullable = props.valueRange ?? { + range: { i: [0, 0, 1], j: [0, 0, 1], k: [0, 0, 1] }, + zones: [], + }; - const [internalValue, setInternalValue] = React.useState< - [[number, number], [number, number], [number, number]] | null - >(cloneDeep(props.value)); - const [prevValue, setPrevValue] = React.useState(cloneDeep(props.value)); + const [internalValue, setInternalValue] = React.useState(cloneDeep(props.value)); + const [prevValue, setPrevValue] = React.useState(cloneDeep(props.value)); if (!isEqual(props.value, prevValue)) { setInternalValue(cloneDeep(props.value)); setPrevValue(cloneDeep(props.value)); } - function handleSliderChange(index: number, val: number[]) { - const newValue: [[number, number], [number, number], [number, number]] = [ - ...(internalValue ?? [ - [0, 0], - [0, 0], - [0, 0], - ]), - ]; - newValue[index] = val as [number, number]; + function handleSliderChange(key: keyof NonNullable, val: number[]) { + const newValue: InternalValueType = { + ...(internalValue ?? { i: [0, 0], j: [0, 0], k: { type: "range", range: [0, 0] } }), + }; + if (key === "k") { + newValue.k = { type: "range", range: [val[0], val[1]] }; + } else { + newValue[key] = val as [number, number]; + } setInternalValue(newValue); } - function handleInputChange(outerIndex: number, innerIndex: number, val: number) { - const min = availableValues[outerIndex][0]; - const max = availableValues[outerIndex][1]; - const step = availableValues[outerIndex][2]; + function handleInputChange(key: keyof NonNullable, innerIndex: number, val: number) { + const min = valueRange.range[key][0]; + const max = valueRange.range[key][1]; + const step = valueRange.range[key][2]; const allowedValues = Array.from( { length: Math.floor((max - min) / step) + 1 }, (_, i) => min + i * step, @@ -69,18 +268,19 @@ export class GridLayerRangeSetting implements CustomSettingImplementation, "k">)[] = ["i", "j"]; const hasChanges = !isEqual(internalValue, props.value); const MIN_SIZE = 250; let inputsVisible = true; @@ -94,43 +294,148 @@ export class GridLayerRangeSetting implements CustomSettingImplementation 0) { + const zone = valueRange.zones[0]; + newValue.k = { type: "zone", range: [zone.start_layer, zone.end_layer], name: zone.name }; + } else { + // No zones available, fallback to range + newValue.k = { type: "range", range: newValue.k.range }; + } + } + setInternalValue(newValue); + } + return ( <> -
- {labels.map((label, index) => ( -
-
{label}
+
+ {labels.map((label) => ( +
+
{label.toUpperCase()}
handleInputChange(index, 0, parseInt(e.target.value))} + value={internalValue?.[label][0] ?? valueRange.range[label][0]} + onChange={(e) => handleInputChange(label, 0, parseInt(e.target.value))} />
handleSliderChange(index, value as [number, number])} + min={valueRange.range[label][0]} + max={valueRange.range[label][1]} + onChange={(_, value) => handleSliderChange(label, value as [number, number])} value={ - internalValue?.[index] ?? [ - availableValues[index][0], - availableValues[index][1], + internalValue?.[label] ?? [ + valueRange.range[label][0], + valueRange.range[label][1], ] } valueLabelDisplay="auto" - step={availableValues[index][2]} + step={valueRange.range[label][2]} />
handleInputChange(index, 1, parseInt(e.target.value))} + value={internalValue?.[label][1] ?? valueRange.range[label][1]} + onChange={(e) => handleInputChange(label, 1, parseInt(e.target.value))} />
))} +
+
K
+
+ +
+
+
+
+ handleInputChange("k", 0, parseInt(e.target.value))} + /> +
+
+ handleSliderChange("k", value as [number, number])} + value={ + internalValue?.["k"].range ?? [ + valueRange.range["k"][0], + valueRange.range["k"][1], + ] + } + valueLabelDisplay="auto" + step={valueRange.range["k"][2]} + /> +
+
+ handleInputChange("k", 1, parseInt(e.target.value))} + /> +
+
+
+ ({ + label: zone.name, + value: zone.name, + }))} + value={internalValue?.["k"].type === "zone" ? internalValue.k.name : undefined} + onChange={(val) => { + const zone = valueRange.zones.find((z) => z.name === val); + if (zone) { + const newValue: InternalValueType = { + ...(internalValue ?? { + i: [0, 0], + j: [0, 0], + k: { type: "range", range: [0, 0] }, + }), + k: { + type: "zone", + range: [zone.start_layer, zone.end_layer], + name: zone.name, + }, + }; + setInternalValue(newValue); + } + }} + /> +