diff --git a/backend_py/primary/primary/routers/surface/converters.py b/backend_py/primary/primary/routers/surface/converters.py index e2f982111..6580fad97 100644 --- a/backend_py/primary/primary/routers/surface/converters.py +++ b/backend_py/primary/primary/routers/surface/converters.py @@ -1,3 +1,5 @@ +import base64 + import numpy as np import xtgeo from numpy.typing import NDArray @@ -8,49 +10,99 @@ from primary.services.utils.surface_intersect_with_polyline import XtgeoSurfaceIntersectionPolyline from primary.services.utils.surface_intersect_with_polyline import XtgeoSurfaceIntersectionResult from primary.services.utils.surface_to_float32 import surface_to_float32_numpy_array +from primary.services.utils.surface_to_png import surface_to_png_bytes_optimized from . import schemas -def resample_property_surface_to_mesh_surface( - mesh_surface: xtgeo.RegularSurface, property_surface: xtgeo.RegularSurface +def resample_to_surface_def( + source_surface: xtgeo.RegularSurface, target_surface_def: schemas.SurfaceDef ) -> xtgeo.RegularSurface: """ - Regrid property surface to mesh surface if topology is different + Returns resampled surface if the target surface definition differs from the grid definition of the source surface. + If the grid definitions are equal, returns the source surface unmodified. """ - if mesh_surface.compare_topology(property_surface): - return property_surface + target_surface = xtgeo.RegularSurface( + ncol=target_surface_def.npoints_x, + nrow=target_surface_def.npoints_y, + xinc=target_surface_def.inc_x, + yinc=target_surface_def.inc_y, + xori=target_surface_def.origin_utm_x, + yori=target_surface_def.origin_utm_y, + rotation=target_surface_def.rot_deg, + ) + + if target_surface.compare_topology(source_surface): + return source_surface - mesh_surface.resample(property_surface) - return mesh_surface + target_surface.resample(source_surface) + return target_surface -def to_api_surface_data(xtgeo_surf: xtgeo.RegularSurface) -> schemas.SurfaceData: +def to_api_surface_data_float(xtgeo_surf: xtgeo.RegularSurface) -> schemas.SurfaceDataFloat: """ - Create API SurfaceData from xtgeo regular surface + Create API SurfaceDataFloat from xtgeo regular surface """ float32_np_arr: NDArray[np.float32] = surface_to_float32_numpy_array(xtgeo_surf) values_b64arr = b64_encode_float_array_as_float32(float32_np_arr) - return schemas.SurfaceData( - x_ori=xtgeo_surf.xori, - y_ori=xtgeo_surf.yori, - x_count=xtgeo_surf.ncol, - y_count=xtgeo_surf.nrow, - x_inc=xtgeo_surf.xinc, - y_inc=xtgeo_surf.yinc, - x_min=xtgeo_surf.xmin, - x_max=xtgeo_surf.xmax, - y_min=xtgeo_surf.ymin, - y_max=xtgeo_surf.ymax, - val_min=xtgeo_surf.values.min(), - val_max=xtgeo_surf.values.max(), + surface_def = schemas.SurfaceDef( + npoints_x=xtgeo_surf.ncol, + npoints_y=xtgeo_surf.nrow, + inc_x=xtgeo_surf.xinc, + inc_y=xtgeo_surf.yinc, + origin_utm_x=xtgeo_surf.xori, + origin_utm_y=xtgeo_surf.yori, rot_deg=xtgeo_surf.rotation, + ) + + trans_bb_utm = schemas.BoundingBox2d( + min_x=xtgeo_surf.xmin, min_y=xtgeo_surf.ymin, max_x=xtgeo_surf.xmax, max_y=xtgeo_surf.ymax + ) + + return schemas.SurfaceDataFloat( + format="float", + surface_def=surface_def, + transformed_bbox_utm=trans_bb_utm, + value_min=xtgeo_surf.values.min(), + value_max=xtgeo_surf.values.max(), values_b64arr=values_b64arr, ) +def to_api_surface_data_png(xtgeo_surf: xtgeo.RegularSurface) -> schemas.SurfaceDataPng: + """ + Create API SurfaceDataPng from xtgeo regular surface + """ + + png_bytes: bytes = surface_to_png_bytes_optimized(xtgeo_surf) + png_bytes_base64 = base64.b64encode(png_bytes).decode("ascii") + + surface_def = schemas.SurfaceDef( + npoints_x=xtgeo_surf.ncol, + npoints_y=xtgeo_surf.nrow, + inc_x=xtgeo_surf.xinc, + inc_y=xtgeo_surf.yinc, + origin_utm_x=xtgeo_surf.xori, + origin_utm_y=xtgeo_surf.yori, + rot_deg=xtgeo_surf.rotation, + ) + + trans_bb_utm = schemas.BoundingBox2d( + min_x=xtgeo_surf.xmin, min_y=xtgeo_surf.ymin, max_x=xtgeo_surf.xmax, max_y=xtgeo_surf.ymax + ) + + return schemas.SurfaceDataPng( + format="png", + surface_def=surface_def, + transformed_bbox_utm=trans_bb_utm, + value_min=xtgeo_surf.values.min(), + value_max=xtgeo_surf.values.max(), + png_image_base64=png_bytes_base64, + ) + + def to_api_surface_meta_set( sumo_surf_meta_set: SurfaceMetaSet, ordered_stratigraphic_surfaces: list[StratigraphicSurface] ) -> schemas.SurfaceMetaSet: diff --git a/backend_py/primary/primary/routers/surface/dependencies.py b/backend_py/primary/primary/routers/surface/dependencies.py new file mode 100644 index 000000000..7ad6e05fe --- /dev/null +++ b/backend_py/primary/primary/routers/surface/dependencies.py @@ -0,0 +1,42 @@ +import logging +from typing import Annotated + +from fastapi import Query +from fastapi.exceptions import RequestValidationError +from pydantic import ValidationError + +from primary.utils.query_string_utils import decode_key_val_str + +from . import schemas + +LOGGER = logging.getLogger(__name__) + + +def get_resample_to_param_from_json( + # fmt:off + resample_to_def_json_str: Annotated[str | None, Query(description="Definition of the surface onto which the data should be resampled. Should be a serialized JSON representation of a SurfaceDef object")] = None + # fmt:on +) -> schemas.SurfaceDef | None: + if resample_to_def_json_str is None: + return None + + try: + surf_def_obj: schemas.SurfaceDef = schemas.SurfaceDef.model_validate_json(resample_to_def_json_str) + except ValidationError as exc: + raise RequestValidationError(errors=exc.errors()) from exc + + return surf_def_obj + + +def get_resample_to_param_from_keyval_str( + # fmt:off + resample_to_def_str: Annotated[str | None, Query(description="Definition of the surface onto which the data should be resampled. *SurfaceDef* object properties encoded as a `KeyValStr` string.")] = None + # fmt:on +) -> schemas.SurfaceDef | None: + if resample_to_def_str is None: + return None + + prop_dict = decode_key_val_str(resample_to_def_str) + surf_def_obj = schemas.SurfaceDef.model_validate(prop_dict) + + return surf_def_obj diff --git a/backend_py/primary/primary/routers/surface/router.py b/backend_py/primary/primary/routers/surface/router.py index 06c93a271..44d485578 100644 --- a/backend_py/primary/primary/routers/surface/router.py +++ b/backend_py/primary/primary/routers/surface/router.py @@ -1,9 +1,8 @@ import asyncio import logging -from typing import List, Optional +from typing import Annotated, List, Optional, Literal -from fastapi import APIRouter, Depends, HTTPException, Query, Response, Body -from webviz_pkg.core_utils.perf_timer import PerfTimer +from fastapi import APIRouter, Depends, HTTPException, Query, Response, Body, status from webviz_pkg.core_utils.perf_metrics import PerfMetrics from primary.services.sumo_access.case_inspector import CaseInspector @@ -15,12 +14,16 @@ from primary.services.utils.surface_intersect_with_polyline import intersect_surface_with_polyline from primary.services.utils.authenticated_user import AuthenticatedUser from primary.auth.auth_helper import AuthHelper -from primary.utils.response_perf_metrics import ResponsePerfMetrics from primary.services.surface_query_service.surface_query_service import batch_sample_surface_in_points_async from primary.services.surface_query_service.surface_query_service import RealizationSampleResult +from primary.utils.response_perf_metrics import ResponsePerfMetrics from . import converters from . import schemas +from . import dependencies + +from .surface_address import RealizationSurfaceAddress, ObservedSurfaceAddress, StatisticalSurfaceAddress +from .surface_address import decode_surf_addr_str LOGGER = logging.getLogger(__name__) @@ -28,6 +31,31 @@ router = APIRouter() +GENERAL_SURF_ADDR_DOC_STR = """ + +--- +*General description of the types of surface addresses that exist. The specific address types supported by this endpoint can be a subset of these.* + +- *REAL* - Realization surface address. Addresses a specific realization surface within an ensemble. Always specifies a single realization number +- *OBS* - Observed surface address. Addresses an observed surface which is not associated with any specific ensemble. +- *STAT* - Statistical surface address. Fully specifies a statistical surface, including the statistic function and which realizations to include. +- *PARTIAL* - Partial surface address. Similar to a realization surface address, but does not include a specific realization number. + +Structure of the different types of address strings: + +``` +REAL~~~~~~~~~~[~~] +STAT~~~~~~~~~~~~[~~] +OBS~~~~~~~~ +PARTIAL~~~~~~~~[~~] +``` + +The `` component in a *STAT* address contains the list of realizations to include in the statistics +encoded as a `UintListStr` or "*" to include all realizations. + +""" + + @router.get("/realization_surfaces_metadata/") async def get_realization_surfaces_metadata( response: Response, @@ -93,185 +121,77 @@ async def get_observed_surfaces_metadata( return api_surf_meta_set -@router.get("/realization_surface_data/") -async def get_realization_surface_data( +@router.get("/surface_data", description="Get surface data for the specified surface." + GENERAL_SURF_ADDR_DOC_STR) +async def get_surface_data( + # fmt:off response: Response, - authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), - case_uuid: str = Query(description="Sumo case uuid"), - ensemble_name: str = Query(description="Ensemble name"), - realization_num: int = Query(description="Realization number"), - name: str = Query(description="Surface name"), - attribute: str = Query(description="Surface attribute"), - time_or_interval: Optional[str] = Query(None, description="Time point or time interval string"), -) -> schemas.SurfaceData: + authenticated_user: Annotated[AuthenticatedUser, Depends(AuthHelper.get_authenticated_user)], + surf_addr_str: Annotated[str, Query(description="Surface address string, supported address types are *REAL*, *OBS* and *STAT*")], + data_format: Annotated[Literal["float", "png"], Query(description="Format of binary data in the response")] = "float", + resample_to: Annotated[schemas.SurfaceDef | None, Depends(dependencies.get_resample_to_param_from_keyval_str)] = None, + # fmt:on +) -> schemas.SurfaceDataFloat | schemas.SurfaceDataPng: perf_metrics = ResponsePerfMetrics(response) - access = SurfaceAccess.from_case_uuid(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) - xtgeo_surf = await access.get_realization_surface_data_async( - real_num=realization_num, name=name, attribute=attribute, time_or_interval_str=time_or_interval - ) - perf_metrics.record_lap("get-surf") - - if not xtgeo_surf: - raise HTTPException(status_code=404, detail="Surface not found") + access_token = authenticated_user.get_sumo_access_token() - surf_data_response = converters.to_api_surface_data(xtgeo_surf) - perf_metrics.record_lap("convert") + 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") - LOGGER.info(f"Loaded realization surface in: {perf_metrics.to_string()}") - - return surf_data_response - - -@router.get("/observed_surface_data/") -async def get_observed_surface_data( - response: Response, - authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), - case_uuid: str = Query(description="Sumo case uuid"), - name: str = Query(description="Surface name"), - attribute: str = Query(description="Surface attribute"), - time_or_interval: str = Query(description="Time point or time interval string"), -) -> schemas.SurfaceData: - perf_metrics = ResponsePerfMetrics(response) - - access = SurfaceAccess.from_case_uuid_no_iteration(authenticated_user.get_sumo_access_token(), case_uuid) - xtgeo_surf = await access.get_observed_surface_data_async( - name=name, attribute=attribute, time_or_interval_str=time_or_interval - ) - perf_metrics.record_lap("get-surf") - - if not xtgeo_surf: - raise HTTPException(status_code=404, detail="Surface not found") - - surf_data_response = converters.to_api_surface_data(xtgeo_surf) - perf_metrics.record_lap("convert") - - LOGGER.info(f"Loaded observed surface in: {perf_metrics.to_string()}") - - return surf_data_response - - -@router.get("/statistical_surface_data/") -async def get_statistical_surface_data( - response: Response, - authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), - case_uuid: str = Query(description="Sumo case uuid"), - ensemble_name: str = Query(description="Ensemble name"), - statistic_function: schemas.SurfaceStatisticFunction = Query(description="Statistics to calculate"), - name: str = Query(description="Surface name"), - attribute: str = Query(description="Surface attribute"), - time_or_interval: Optional[str] = Query(None, description="Time point or time interval string"), -) -> schemas.SurfaceData: - perf_metrics = ResponsePerfMetrics(response) - - access = SurfaceAccess.from_case_uuid(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) - - service_stat_func_to_compute = StatisticFunction.from_string_value(statistic_function) - if service_stat_func_to_compute is None: - raise HTTPException(status_code=404, detail="Invalid statistic requested") - - xtgeo_surf = await access.get_statistical_surface_data_async( - statistic_function=service_stat_func_to_compute, - name=name, - attribute=attribute, - time_or_interval_str=time_or_interval, - ) - perf_metrics.record_lap("sumo-calc") - - if not xtgeo_surf: - raise HTTPException(status_code=404, detail="Could not find or compute surface") - - surf_data_response: schemas.SurfaceData = converters.to_api_surface_data(xtgeo_surf) - perf_metrics.record_lap("convert") - - LOGGER.info(f"Calculated statistical surface in: {perf_metrics.to_string()}") - - return surf_data_response - - -# pylint: disable=too-many-arguments -@router.get("/property_surface_resampled_to_static_surface/") -async def get_property_surface_resampled_to_static_surface( - response: Response, - authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), - case_uuid: str = Query(description="Sumo case uuid"), - ensemble_name: str = Query(description="Ensemble name"), - realization_num_mesh: int = Query(description="Realization number"), - name_mesh: str = Query(description="Surface name"), - attribute_mesh: str = Query(description="Surface attribute"), - realization_num_property: int = Query(description="Realization number"), - name_property: str = Query(description="Surface name"), - attribute_property: str = Query(description="Surface attribute"), - time_or_interval_property: Optional[str] = Query(None, description="Time point or time interval string"), -) -> schemas.SurfaceData: - perf_metrics = ResponsePerfMetrics(response) - - access = SurfaceAccess.from_case_uuid(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) - xtgeo_surf_mesh = await access.get_realization_surface_data_async( - real_num=realization_num_mesh, name=name_mesh, attribute=attribute_mesh - ) - perf_metrics.record_lap("mesh-surf") - - xtgeo_surf_property = await access.get_realization_surface_data_async( - real_num=realization_num_property, - name=name_property, - attribute=attribute_property, - time_or_interval_str=time_or_interval_property, - ) - perf_metrics.record_lap("prop-surf") - - if not xtgeo_surf_mesh or not xtgeo_surf_property: - raise HTTPException(status_code=404, detail="Surface not found") - - resampled_surface = converters.resample_property_surface_to_mesh_surface(xtgeo_surf_mesh, xtgeo_surf_property) - perf_metrics.record_lap("resample") - - surf_data_response: schemas.SurfaceData = converters.to_api_surface_data(resampled_surface) - perf_metrics.record_lap("convert") - - LOGGER.info(f"Loaded property surface in: {perf_metrics.to_string()}") - - return surf_data_response + if addr.address_type == "REAL": + access = SurfaceAccess.from_case_uuid(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") + if not xtgeo_surf: + raise HTTPException(status_code=404, detail="Could not get realization surface") + elif addr.address_type == "STAT": + if addr.stat_realizations is not None: + raise HTTPException(status_code=501, detail="Statistics with specific realizations not yet supported") -@router.get("/property_surface_resampled_to_statistical_static_surface/") -async def get_property_surface_resampled_to_statistical_static_surface( - authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), - case_uuid: str = Query(description="Sumo case uuid"), - ensemble_name: str = Query(description="Ensemble name"), - statistic_function: schemas.SurfaceStatisticFunction = Query(description="Statistics to calculate"), - name_mesh: str = Query(description="Surface name"), - attribute_mesh: str = Query(description="Surface attribute"), - # statistic_function_property: schemas.SurfaceStatisticFunction = Query(description="Statistics to calculate"), - name_property: str = Query(description="Surface name"), - attribute_property: str = Query(description="Surface attribute"), - time_or_interval_property: Optional[str] = Query(None, description="Time point or time interval string"), -) -> schemas.SurfaceData: - timer = PerfTimer() + 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_case_uuid(authenticated_user.get_sumo_access_token(), case_uuid, ensemble_name) - service_stat_func_to_compute = StatisticFunction.from_string_value(statistic_function) - if service_stat_func_to_compute is not None: - xtgeo_surf_mesh = await access.get_statistical_surface_data_async( + access = SurfaceAccess.from_case_uuid(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=name_mesh, - attribute=attribute_mesh, + name=addr.name, + attribute=addr.attribute, + time_or_interval_str=addr.iso_time_or_interval, ) - xtgeo_surf_property = await access.get_statistical_surface_data_async( - statistic_function=service_stat_func_to_compute, - name=name_property, - attribute=attribute_property, - time_or_interval_str=time_or_interval_property, + perf_metrics.record_lap("sumo-calc") + if not xtgeo_surf: + raise HTTPException(status_code=404, detail="Could not get or compute statistical surface") + + elif addr.address_type == "OBS": + access = SurfaceAccess.from_case_uuid_no_iteration(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") + if not xtgeo_surf: + raise HTTPException(status_code=404, detail="Could not get observed surface") - if not xtgeo_surf_mesh or not xtgeo_surf_property: - raise HTTPException(status_code=404, detail="Surface not found") + if resample_to is not None: + xtgeo_surf = converters.resample_to_surface_def(xtgeo_surf, resample_to) + perf_metrics.record_lap("resample") - resampled_surface = converters.resample_property_surface_to_mesh_surface(xtgeo_surf_mesh, xtgeo_surf_property) + surf_data_response: schemas.SurfaceDataFloat | schemas.SurfaceDataPng + if data_format == "float": + surf_data_response = converters.to_api_surface_data_float(xtgeo_surf) + elif data_format == "png": + surf_data_response = converters.to_api_surface_data_png(xtgeo_surf) - surf_data_response = converters.to_api_surface_data(resampled_surface) + perf_metrics.record_lap("convert") - LOGGER.info(f"Loaded property surface and created image, total time: {timer.elapsed_ms()}ms") + LOGGER.info(f"Got {addr.address_type} surface in: {perf_metrics.to_string()}") return surf_data_response @@ -347,6 +267,36 @@ async def post_sample_surface_in_points( return intersections +@router.get("/delta_surface_data") +async def get_delta_surface_data( + # fmt:off + response: Response, + authenticated_user: Annotated[AuthenticatedUser, Depends(AuthHelper.get_authenticated_user)], + surf_a_addr_str: Annotated[str, Query(description="Address string of surface A, supported types: *REAL*, *OBS* and *STAT*")], + surf_b_addr_str: Annotated[str, Query(description="Address string of surface B, supported types: *REAL*, *OBS* and *STAT*")], + data_format: Annotated[Literal["float", "png"], Query(description="Format of binary data in the response")] = "float", + resample_to: Annotated[schemas.SurfaceDef | None, Depends(dependencies.get_resample_to_param_from_keyval_str)] = None, + # fmt:on +) -> list[schemas.SurfaceDataFloat]: + raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED) + + +@router.get("/misfit_surface_data") +async def get_misfit_surface_data( + # fmt:off + response: Response, + authenticated_user: Annotated[AuthenticatedUser, Depends(AuthHelper.get_authenticated_user)], + obs_surf_addr_str: Annotated[str, Query(description="Address of observed surface, only supported address type is *OBS*")], + sim_surf_addr_str: Annotated[str, Query(description="Address of simulated surface, supported type is *PARTIAL*")], + statistic_functions: Annotated[list[schemas.SurfaceStatisticFunction], Query(description="Statistics to calculate")], + realizations: Annotated[list[int], Query(description="Realization numbers")], + data_format: Annotated[Literal["float", "png"], Query(description="Format of binary data in the response")] = "float", + resample_to: Annotated[schemas.SurfaceDef | None, Depends(dependencies.get_resample_to_param_from_keyval_str)] = None, + # fmt:on +) -> list[schemas.SurfaceDataFloat]: + raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED) + + async def _get_stratigraphic_units_for_case_async( authenticated_user: AuthenticatedUser, case_uuid: str ) -> list[StratigraphicUnit]: diff --git a/backend_py/primary/primary/routers/surface/schemas.py b/backend_py/primary/primary/routers/surface/schemas.py index 42e9315c6..3c5e8be4e 100644 --- a/backend_py/primary/primary/routers/surface/schemas.py +++ b/backend_py/primary/primary/routers/surface/schemas.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import List +from typing import List, Literal from pydantic import BaseModel, ConfigDict from webviz_pkg.core_utils.b64 import B64FloatArray @@ -77,23 +77,47 @@ class SurfaceMetaSet(BaseModel): surface_names_in_strat_order: list[str] -class SurfaceData(BaseModel): - x_ori: float - y_ori: float - x_count: int - y_count: int - x_inc: float - y_inc: float - x_min: float - x_max: float - y_min: float - y_max: float - val_min: float - val_max: float - rot_deg: float +class BoundingBox2d(BaseModel): + model_config = ConfigDict(extra="forbid") + + min_x: float + min_y: float + max_x: float + max_y: float + + +class SurfaceDef(BaseModel): + model_config = ConfigDict(extra="forbid") + + npoints_x: int # number of grid points in the x direction + npoints_y: int # number of grid points in the y direction + inc_x: float # increment/spacing between points in x direction + inc_y: float # increment/spacing between points in y direction + origin_utm_x: float # x-coordinate of the origin of the surface grid in UTM + origin_utm_y: float # y-coordinate of the origin of the surface grid in UTM + rot_deg: float # rotation of surface in degrees around origin, rotation is counter-clockwise from the x-axis + + +class SurfaceDataBase(BaseModel): + model_config = ConfigDict(extra="forbid") + + format: Literal["float", "png"] + surface_def: SurfaceDef + transformed_bbox_utm: BoundingBox2d + value_min: float + value_max: float + + +class SurfaceDataFloat(SurfaceDataBase): + format: Literal["float"] = "float" values_b64arr: B64FloatArray +class SurfaceDataPng(SurfaceDataBase): + format: Literal["png"] = "png" + png_image_base64: str + + class SurfaceIntersectionData(BaseModel): """ Definition of a surface intersection made from a set of (x, y) coordinates. diff --git a/backend_py/primary/primary/routers/surface/surface_address.py b/backend_py/primary/primary/routers/surface/surface_address.py new file mode 100644 index 000000000..92160658e --- /dev/null +++ b/backend_py/primary/primary/routers/surface/surface_address.py @@ -0,0 +1,305 @@ +from typing import Literal, ClassVar, TypeGuard +from dataclasses import dataclass + +from primary.utils.query_string_utils import encode_as_uint_list_str, decode_uint_list_str + +_ADDR_COMP_DELIMITER = "~~" + + +@dataclass(frozen=True) +class RealizationSurfaceAddress: + address_type: ClassVar[Literal["REAL"]] = "REAL" + case_uuid: str + ensemble_name: str + name: str + attribute: str + realization: int + iso_time_or_interval: str | None + + def __post_init__(self) -> None: + if not self.case_uuid: + raise ValueError("RealizationSurfaceAddress.case_uuid must be a non-empty string") + if not self.ensemble_name: + raise ValueError("RealSurfAddr.ensemble_name must be a non-empty string") + if not self.name: + raise ValueError("RealizationSurfaceAddress.name must be a non-empty string") + if not self.attribute: + raise ValueError("RealizationSurfaceAddress.attribute must be a non-empty string") + if not isinstance(self.realization, int): + raise ValueError("RealizationSurfaceAddress.realization must be an integer") + if self.iso_time_or_interval and len(self.iso_time_or_interval) == 0: + raise ValueError("RealizationSurfaceAddress.iso_time_or_interval must be None or a non-empty string") + + @classmethod + def from_addr_str(cls, addr_str: str) -> "RealizationSurfaceAddress": + component_arr = addr_str.split(_ADDR_COMP_DELIMITER) + if len(component_arr) == 1: + raise ValueError("Could not parse string as a surface address") + + addr_type_str = component_arr[0] + if addr_type_str != "REAL": + raise ValueError("Wrong surface address type") + + if len(component_arr) < 6: + raise ValueError("Too few components in realization address string") + + case_uuid = component_arr[1] + ensemble_name = component_arr[2] + surface_name = component_arr[3] + attribute_name = component_arr[4] + realization = int(component_arr[5]) + + iso_date_or_interval: str | None = None + if len(component_arr) > 6 and len(component_arr[6]) > 0: + iso_date_or_interval = component_arr[6] + + return cls(case_uuid, ensemble_name, surface_name, attribute_name, realization, iso_date_or_interval) + + def to_addr_str(self) -> str: + component_arr = ["REAL", self.case_uuid, self.ensemble_name, self.name, self.attribute, str(self.realization)] + if self.iso_time_or_interval: + component_arr.append(self.iso_time_or_interval) + + _assert_that_no_components_contain_delimiter(component_arr) + + return _ADDR_COMP_DELIMITER.join(component_arr) + + +@dataclass(frozen=True) +class ObservedSurfaceAddress: + address_type: ClassVar[Literal["OBS"]] = "OBS" + case_uuid: str + name: str + attribute: str + iso_time_or_interval: str + + def __post_init__(self) -> None: + if not self.case_uuid: + raise ValueError("ObservedSurfaceAddress.case_uuid must be a non-empty string") + if not self.name: + raise ValueError("ObservedSurfaceAddress.name must be a non-empty string") + if not self.attribute: + raise ValueError("ObservedSurfaceAddress.attribute must be a non-empty string") + if not self.iso_time_or_interval: + raise ValueError("ObservedSurfaceAddress.iso_time_or_interval must non-empty string") + + @classmethod + def from_addr_str(cls, addr_str: str) -> "ObservedSurfaceAddress": + component_arr = addr_str.split(_ADDR_COMP_DELIMITER) + if len(component_arr) == 1: + raise ValueError("Could not parse string as a surface address") + + addr_type_str = component_arr[0] + if addr_type_str != "OBS": + raise ValueError("Wrong surface address type") + + if len(component_arr) < 5: + raise ValueError("Too few components in observed address string") + + case_uuid = component_arr[1] + surface_name = component_arr[2] + attribute_name = component_arr[3] + iso_date_or_interval = component_arr[4] + + return cls(case_uuid, surface_name, attribute_name, iso_date_or_interval) + + def to_addr_str(self) -> str: + component_arr = ["OBS", self.case_uuid, self.name, self.attribute, self.iso_time_or_interval] + + _assert_that_no_components_contain_delimiter(component_arr) + + return _ADDR_COMP_DELIMITER.join(component_arr) + + +@dataclass(frozen=True) +class StatisticalSurfaceAddress: + address_type: ClassVar[Literal["STAT"]] = "STAT" + case_uuid: str + ensemble_name: str + name: str + attribute: str + stat_function: Literal["MEAN", "STD", "MIN", "MAX", "P10", "P90", "P50"] + stat_realizations: list[int] | None + iso_time_or_interval: str | None + + def __post_init__(self) -> None: + if not self.case_uuid: + raise ValueError("StatisticalSurfaceAddress.case_uuid must be a non-empty string") + if not self.ensemble_name: + raise ValueError("StatisticalSurfaceAddress.ensemble_name must be a non-empty string") + if not self.name: + raise ValueError("StatisticalSurfaceAddress.name must be a non-empty string") + if not self.attribute: + raise ValueError("StatisticalSurfaceAddress.attribute must be a non-empty string") + if not self.stat_function: + raise ValueError("StatisticalSurfaceAddress.statistic_function must be a non-empty string") + if self.stat_realizations and not isinstance(self.stat_realizations, list): + raise ValueError("StatisticalSurfaceAddress.realizations must be None or a list of integers") + if self.iso_time_or_interval and len(self.iso_time_or_interval) == 0: + raise ValueError("StatisticalSurfaceAddress.iso_time_or_interval must be None or a non-empty string") + + @classmethod + def from_addr_str(cls, addr_str: str) -> "StatisticalSurfaceAddress": + component_arr = addr_str.split(_ADDR_COMP_DELIMITER) + if len(component_arr) == 1: + raise ValueError("Could not parse string as a surface address") + + addr_type_str = component_arr[0] + if addr_type_str != "STAT": + raise ValueError("Wrong surface address type") + + if len(component_arr) < 7: + raise ValueError("Too few components in statistical address string") + + case_uuid = component_arr[1] + ensemble_name = component_arr[2] + surface_name = component_arr[3] + attribute_name = component_arr[4] + statistic_function = component_arr[5] + realizations_str = component_arr[6] + + iso_date_or_interval: str | None = None + if len(component_arr) > 7 and len(component_arr[7]) > 0: + iso_date_or_interval = component_arr[7] + + realizations: list[int] | None = None + if realizations_str != "*": + realizations = decode_uint_list_str(realizations_str) + + if not _is_valid_statistic_function(statistic_function): + raise ValueError("Invalid statistic function") + + return cls( + case_uuid, + ensemble_name, + surface_name, + attribute_name, + statistic_function, + realizations, + iso_date_or_interval, + ) + + def to_addr_str(self) -> str: + realizations_str = "*" + if self.stat_realizations is not None: + realizations_str = encode_as_uint_list_str(self.stat_realizations) + + component_arr = [ + "STAT", + self.case_uuid, + self.ensemble_name, + self.name, + self.attribute, + self.stat_function, + realizations_str, + ] + if self.iso_time_or_interval: + component_arr.append(self.iso_time_or_interval) + + _assert_that_no_components_contain_delimiter(component_arr) + + return _ADDR_COMP_DELIMITER.join(component_arr) + + +@dataclass(frozen=True) +class PartialSurfaceAddress: + address_type: ClassVar[Literal["PARTIAL"]] = "PARTIAL" + case_uuid: str + ensemble_name: str + name: str + attribute: str + iso_time_or_interval: str | None + + def __post_init__(self) -> None: + if not self.case_uuid: + raise ValueError("PartialSurfaceAddress.case_uuid must be a non-empty string") + if not self.ensemble_name: + raise ValueError("PartialSurfaceAddress.ensemble_name must be a non-empty string") + if not self.name: + raise ValueError("PartialSurfaceAddress.name must be a non-empty string") + if not self.attribute: + raise ValueError("PartialSurfaceAddress.attribute must be a non-empty string") + if self.iso_time_or_interval and len(self.iso_time_or_interval) == 0: + raise ValueError("PartialSurfaceAddress.iso_time_or_interval must be None or a non-empty string") + + @classmethod + def from_addr_str(cls, addr_str: str) -> "PartialSurfaceAddress": + component_arr = addr_str.split(_ADDR_COMP_DELIMITER) + if len(component_arr) == 1: + raise ValueError("Could not parse string as a surface address") + + addr_type_str = component_arr[0] + if addr_type_str != "PARTIAL": + raise ValueError("Wrong surface address type") + + if len(component_arr) < 5: + raise ValueError("Too few components in partial address string") + + case_uuid = component_arr[1] + ensemble_name = component_arr[2] + surface_name = component_arr[3] + attribute_name = component_arr[4] + + iso_date_or_interval: str | None = None + if len(component_arr) > 5 and len(component_arr[5]) > 0: + iso_date_or_interval = component_arr[5] + + return cls(case_uuid, ensemble_name, surface_name, attribute_name, iso_date_or_interval) + + def to_addr_str(self) -> str: + component_arr = ["PARTIAL", self.case_uuid, self.ensemble_name, self.name, self.attribute] + if self.iso_time_or_interval: + component_arr.append(self.iso_time_or_interval) + + _assert_that_no_components_contain_delimiter(component_arr) + + return _ADDR_COMP_DELIMITER.join(component_arr) + + +def peek_surface_address_type(addr_str: str) -> Literal["REAL", "OBS", "STAT", "PARTIAL"] | None: + component_arr = addr_str.split(_ADDR_COMP_DELIMITER) + if len(component_arr) < 1: + return None + + addr_type_str = component_arr[0] + if addr_type_str == "REAL": + return "REAL" + if addr_type_str == "OBS": + return "OBS" + if addr_type_str == "STAT": + return "STAT" + if addr_type_str == "PARTIAL": + return "PARTIAL" + + return None + + +def decode_surf_addr_str( + addr_str: str, +) -> RealizationSurfaceAddress | ObservedSurfaceAddress | StatisticalSurfaceAddress | PartialSurfaceAddress: + addr_type = peek_surface_address_type(addr_str) + if addr_type is None: + raise ValueError("Unknown or missing surface address type") + + if addr_type == "REAL": + return RealizationSurfaceAddress.from_addr_str(addr_str) + if addr_type == "OBS": + return ObservedSurfaceAddress.from_addr_str(addr_str) + if addr_type == "STAT": + return StatisticalSurfaceAddress.from_addr_str(addr_str) + if addr_type == "PARTIAL": + return PartialSurfaceAddress.from_addr_str(addr_str) + + raise ValueError(f"Unsupported surface address type {addr_type=}") + + +def _is_valid_statistic_function( + stat_func_str: str, +) -> TypeGuard[Literal["MEAN", "STD", "MIN", "MAX", "P10", "P90", "P50"]]: + return stat_func_str in ["MEAN", "STD", "MIN", "MAX", "P10", "P90", "P50"] + + +def _assert_that_no_components_contain_delimiter(component_arr: list[str]) -> None: + for comp in component_arr: + if _ADDR_COMP_DELIMITER in comp: + raise ValueError(f"Address component contains delimiter, offending component: {comp}") diff --git a/backend_py/primary/primary/utils/exception_handlers.py b/backend_py/primary/primary/utils/exception_handlers.py index 988921077..af6c1d623 100644 --- a/backend_py/primary/primary/utils/exception_handlers.py +++ b/backend_py/primary/primary/utils/exception_handlers.py @@ -52,11 +52,15 @@ def my_request_validation_error_handler(request: Request, exc: RequestValidation if type(loc) is list: loc = ",".join(loc) + # We're seeing some cases where the input is not JSON serializable, so we'll just always convert it to a + # string to avoid triggering an exception when we try and call json.dumps() further down. + input_as_str = str(err.get("input")) + simplified_err_arr.append( { "msg": err.get("msg"), "loc": loc, - "input": err.get("input"), + "input": input_as_str, } ) diff --git a/backend_py/primary/primary/utils/query_string_utils.py b/backend_py/primary/primary/utils/query_string_utils.py index 1d903a32c..bc8fca08c 100644 --- a/backend_py/primary/primary/utils/query_string_utils.py +++ b/backend_py/primary/primary/utils/query_string_utils.py @@ -70,7 +70,7 @@ def decode_uint_list_str(int_arr_str: str) -> list[int]: return ret_arr -def encode_as_uint_list_str(int_list: list[int]) -> str: +def encode_as_uint_list_str(unsigned_int_list: list[int]) -> str: """ Encode a list of unsigned integers into a UintListStr formatted string. Single integers are represented as themselves, while consecutive integers are represented as a - range. @@ -80,17 +80,21 @@ def encode_as_uint_list_str(int_list: list[int]) -> str: Example: [1, 2, 3, 5, 6, 7, 10] -> "1-3!5-7!10" """ - if not int_list: + if not unsigned_int_list: return "" # Remove duplicates and sort - int_list = sorted(set(int_list)) + unsigned_int_list = sorted(set(unsigned_int_list)) + + # Verify that all integers are unsigned by checking first element in the now sorted list + if unsigned_int_list[0] < 0: + raise ValueError("List contains negative integers") encoded_parts = [] - start_val = int_list[0] + start_val = unsigned_int_list[0] end_val = start_val - for val in int_list[1:]: + for val in unsigned_int_list[1:]: if val == end_val + 1: end_val = val else: diff --git a/backend_py/primary/tests/unit/routers/test_surface_address.py b/backend_py/primary/tests/unit/routers/test_surface_address.py new file mode 100644 index 000000000..4ae147c41 --- /dev/null +++ b/backend_py/primary/tests/unit/routers/test_surface_address.py @@ -0,0 +1,103 @@ +from primary.routers.surface.surface_address import RealizationSurfaceAddress +from primary.routers.surface.surface_address import ObservedSurfaceAddress +from primary.routers.surface.surface_address import StatisticalSurfaceAddress +from primary.routers.surface.surface_address import PartialSurfaceAddress +from primary.routers.surface.surface_address import decode_surf_addr_str + + +def test_enc_dec_realization_address_no_time() -> None: + addr0 = RealizationSurfaceAddress("UUID123", "iter-0", "surf.name", "my attr name", -1, None) + addr_str = addr0.to_addr_str() + print(f"\n{addr_str=}") + addr1 = RealizationSurfaceAddress.from_addr_str(addr_str) + assert addr0 == addr1 + + +def test_enc_dec_realization_address_with_time() -> None: + addr0 = RealizationSurfaceAddress("UUID123", "iter-0", "surf.name", "my attr name", -1, "2024-01-31T00:00:00Z") + addr_str = addr0.to_addr_str() + print(f"\n{addr_str=}") + addr1 = RealizationSurfaceAddress.from_addr_str(addr_str) + assert addr0 == addr1 + + +def test_enc_dec_observed_address() -> None: + addr0 = ObservedSurfaceAddress("UUID123", "surf.name", "my attr name", "2024-01-31T00:00:00Z") + addr_str = addr0.to_addr_str() + print(f"\n{addr_str=}") + addr1 = ObservedSurfaceAddress.from_addr_str(addr_str) + assert addr0 == addr1 + + +def test_enc_dec_statistical_address_with_real_no_time() -> None: + addr0 = StatisticalSurfaceAddress("UUID123", "iter-0", "surf.name", "my attr name", "MEAN", [1, 2, 3, 5], None) + addr_str = addr0.to_addr_str() + print(f"\n{addr_str=}") + addr1 = StatisticalSurfaceAddress.from_addr_str(addr_str) + assert addr0 == addr1 + + +def test_enc_dec_statistical_address_with_real_with_time() -> None: + addr0 = StatisticalSurfaceAddress( + "UUID123", "iter-0", "surf.name", "my attr name", "MEAN", [1, 2, 3, 5], "2024-01-31T00:00:00Z" + ) + addr_str = addr0.to_addr_str() + print(f"\n{addr_str=}") + addr1 = StatisticalSurfaceAddress.from_addr_str(addr_str) + assert addr0 == addr1 + + +def test_enc_dec_statistical_address_with_empty_real_no_time() -> None: + addr0 = StatisticalSurfaceAddress("UUID123", "iter-0", "surf.name", "my attr name", "MEAN", [], None) + addr_str = addr0.to_addr_str() + print(f"\n{addr_str=}") + addr1 = StatisticalSurfaceAddress.from_addr_str(addr_str) + assert addr0 == addr1 + + +def test_enc_dec_statistical_address_with_wildcard_real_no_time() -> None: + addr0 = StatisticalSurfaceAddress("UUID123", "iter-0", "surf.name", "my attr name", "MEAN", None, None) + addr_str = addr0.to_addr_str() + print(f"\n{addr_str=}") + addr1 = StatisticalSurfaceAddress.from_addr_str(addr_str) + assert addr0 == addr1 + + +def test_enc_dec_statistical_address_with_wildcard_real_with_time() -> None: + addr0 = StatisticalSurfaceAddress( + "UUID123", "iter-0", "surf.name", "my attr name", "MEAN", None, "2024-01-31T00:00:00Z" + ) + addr_str = addr0.to_addr_str() + print(f"\n{addr_str=}") + addr1 = StatisticalSurfaceAddress.from_addr_str(addr_str) + assert addr0 == addr1 + + +def test_enc_dec_partial_address_no_time() -> None: + addr0 = PartialSurfaceAddress("UUID123", "iter-0", "surf.name", "my attr name", None) + addr_str = addr0.to_addr_str() + print(f"\n{addr_str=}") + addr1 = PartialSurfaceAddress.from_addr_str(addr_str) + assert addr0 == addr1 + + +def test_enc_dec_partial_address_with_time() -> None: + addr0 = PartialSurfaceAddress("UUID123", "iter-0", "surf.name", "my attr name", "2024-01-31T00:00:00Z") + addr_str = addr0.to_addr_str() + print(f"\n{addr_str=}") + addr1 = PartialSurfaceAddress.from_addr_str(addr_str) + assert addr0 == addr1 + + +def test_decode_surf_addr_str() -> None: + real_addr = decode_surf_addr_str("REAL~~UUID123~~iter-0~~surf.name~~my attr name~~-1") + assert real_addr.address_type == "REAL" + + obs_addr = decode_surf_addr_str("OBS~~UUID123~~surf.name~~my attr name~~2024-01-31T00:00:00Z") + assert obs_addr.address_type == "OBS" + + stat_addr = decode_surf_addr_str("STAT~~UUID123~~iter-0~~surf.name~~my attr name~~MEAN~~1-3!5") + assert stat_addr.address_type == "STAT" + + partial_addr = decode_surf_addr_str("PARTIAL~~UUID123~~iter-0~~surf.name~~my attr name") + assert partial_addr.address_type == "PARTIAL" diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index a07a2e2a2..2a591446f 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -17,6 +17,7 @@ export type { Body_post_get_polyline_intersection as Body_post_get_polyline_inte export type { Body_post_get_seismic_fence as Body_post_get_seismic_fence_api } from './models/Body_post_get_seismic_fence'; export type { Body_post_get_surface_intersection as Body_post_get_surface_intersection_api } from './models/Body_post_get_surface_intersection'; export type { Body_post_sample_surface_in_points as Body_post_sample_surface_in_points_api } from './models/Body_post_sample_surface_in_points'; +export type { BoundingBox2d as BoundingBox2d_api } from './models/BoundingBox2d'; export type { BoundingBox3d as BoundingBox3d_api } from './models/BoundingBox3d'; export type { CaseInfo as CaseInfo_api } from './models/CaseInfo'; export type { Completions as Completions_api } from './models/Completions'; @@ -68,7 +69,9 @@ export type { StratigraphicUnit as StratigraphicUnit_api } from './models/Strati export type { SummaryVectorDateObservation as SummaryVectorDateObservation_api } from './models/SummaryVectorDateObservation'; export type { SummaryVectorObservations as SummaryVectorObservations_api } from './models/SummaryVectorObservations'; export { SurfaceAttributeType as SurfaceAttributeType_api } from './models/SurfaceAttributeType'; -export type { SurfaceData as SurfaceData_api } from './models/SurfaceData'; +export type { SurfaceDataFloat as SurfaceDataFloat_api } from './models/SurfaceDataFloat'; +export type { SurfaceDataPng as SurfaceDataPng_api } from './models/SurfaceDataPng'; +export type { SurfaceDef as SurfaceDef_api } from './models/SurfaceDef'; export type { SurfaceIntersectionCumulativeLengthPolyline as SurfaceIntersectionCumulativeLengthPolyline_api } from './models/SurfaceIntersectionCumulativeLengthPolyline'; export type { SurfaceIntersectionData as SurfaceIntersectionData_api } from './models/SurfaceIntersectionData'; export type { SurfaceMeta as SurfaceMeta_api } from './models/SurfaceMeta'; diff --git a/frontend/src/api/models/BoundingBox2d.ts b/frontend/src/api/models/BoundingBox2d.ts new file mode 100644 index 000000000..417c9c21e --- /dev/null +++ b/frontend/src/api/models/BoundingBox2d.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type BoundingBox2d = { + min_x: number; + min_y: number; + max_x: number; + max_y: number; +}; + diff --git a/frontend/src/api/models/SurfaceData.ts b/frontend/src/api/models/SurfaceData.ts deleted file mode 100644 index 1088b5158..000000000 --- a/frontend/src/api/models/SurfaceData.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* generated using openapi-typescript-codegen -- do no edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { B64FloatArray } from './B64FloatArray'; -export type SurfaceData = { - x_ori: number; - y_ori: number; - x_count: number; - y_count: number; - x_inc: number; - y_inc: number; - x_min: number; - x_max: number; - y_min: number; - y_max: number; - val_min: number; - val_max: number; - rot_deg: number; - values_b64arr: B64FloatArray; -}; - diff --git a/frontend/src/api/models/SurfaceDataFloat.ts b/frontend/src/api/models/SurfaceDataFloat.ts new file mode 100644 index 000000000..b14910067 --- /dev/null +++ b/frontend/src/api/models/SurfaceDataFloat.ts @@ -0,0 +1,16 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { B64FloatArray } from './B64FloatArray'; +import type { BoundingBox2d } from './BoundingBox2d'; +import type { SurfaceDef } from './SurfaceDef'; +export type SurfaceDataFloat = { + format: any; + surface_def: SurfaceDef; + transformed_bbox_utm: BoundingBox2d; + value_min: number; + value_max: number; + values_b64arr: B64FloatArray; +}; + diff --git a/frontend/src/api/models/SurfaceDataPng.ts b/frontend/src/api/models/SurfaceDataPng.ts new file mode 100644 index 000000000..d0b11904b --- /dev/null +++ b/frontend/src/api/models/SurfaceDataPng.ts @@ -0,0 +1,15 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { BoundingBox2d } from './BoundingBox2d'; +import type { SurfaceDef } from './SurfaceDef'; +export type SurfaceDataPng = { + format: any; + surface_def: SurfaceDef; + transformed_bbox_utm: BoundingBox2d; + value_min: number; + value_max: number; + png_image_base64: string; +}; + diff --git a/frontend/src/api/models/SurfaceDef.ts b/frontend/src/api/models/SurfaceDef.ts new file mode 100644 index 000000000..c1fae8d2b --- /dev/null +++ b/frontend/src/api/models/SurfaceDef.ts @@ -0,0 +1,14 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type SurfaceDef = { + npoints_x: number; + npoints_y: number; + inc_x: number; + inc_y: number; + origin_utm_x: number; + origin_utm_y: number; + rot_deg: number; +}; + diff --git a/frontend/src/api/services/SurfaceService.ts b/frontend/src/api/services/SurfaceService.ts index caf24795c..5dd76c84f 100644 --- a/frontend/src/api/services/SurfaceService.ts +++ b/frontend/src/api/services/SurfaceService.ts @@ -4,7 +4,8 @@ /* eslint-disable */ import type { Body_post_get_surface_intersection } from '../models/Body_post_get_surface_intersection'; import type { Body_post_sample_surface_in_points } from '../models/Body_post_sample_surface_in_points'; -import type { SurfaceData } from '../models/SurfaceData'; +import type { SurfaceDataFloat } from '../models/SurfaceDataFloat'; +import type { SurfaceDataPng } from '../models/SurfaceDataPng'; import type { SurfaceIntersectionData } from '../models/SurfaceIntersectionData'; import type { SurfaceMetaSet } from '../models/SurfaceMetaSet'; import type { SurfaceRealizationSampleValues } from '../models/SurfaceRealizationSampleValues'; @@ -59,183 +60,46 @@ export class SurfaceService { }); } /** - * Get Realization Surface Data - * @param caseUuid Sumo case uuid - * @param ensembleName Ensemble name - * @param realizationNum Realization number - * @param name Surface name - * @param attribute Surface attribute - * @param timeOrInterval Time point or time interval string - * @returns SurfaceData Successful Response - * @throws ApiError - */ - public getRealizationSurfaceData( - caseUuid: string, - ensembleName: string, - realizationNum: number, - name: string, - attribute: string, - timeOrInterval?: (string | null), - ): CancelablePromise { - return this.httpRequest.request({ - method: 'GET', - url: '/surface/realization_surface_data/', - query: { - 'case_uuid': caseUuid, - 'ensemble_name': ensembleName, - 'realization_num': realizationNum, - 'name': name, - 'attribute': attribute, - 'time_or_interval': timeOrInterval, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * Get Observed Surface Data - * @param caseUuid Sumo case uuid - * @param name Surface name - * @param attribute Surface attribute - * @param timeOrInterval Time point or time interval string - * @returns SurfaceData Successful Response - * @throws ApiError - */ - public getObservedSurfaceData( - caseUuid: string, - name: string, - attribute: string, - timeOrInterval: string, - ): CancelablePromise { - return this.httpRequest.request({ - method: 'GET', - url: '/surface/observed_surface_data/', - query: { - 'case_uuid': caseUuid, - 'name': name, - 'attribute': attribute, - 'time_or_interval': timeOrInterval, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * Get Statistical Surface Data - * @param caseUuid Sumo case uuid - * @param ensembleName Ensemble name - * @param statisticFunction Statistics to calculate - * @param name Surface name - * @param attribute Surface attribute - * @param timeOrInterval Time point or time interval string - * @returns SurfaceData Successful Response - * @throws ApiError - */ - public getStatisticalSurfaceData( - caseUuid: string, - ensembleName: string, - statisticFunction: SurfaceStatisticFunction, - name: string, - attribute: string, - timeOrInterval?: (string | null), - ): CancelablePromise { - return this.httpRequest.request({ - method: 'GET', - url: '/surface/statistical_surface_data/', - query: { - 'case_uuid': caseUuid, - 'ensemble_name': ensembleName, - 'statistic_function': statisticFunction, - 'name': name, - 'attribute': attribute, - 'time_or_interval': timeOrInterval, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * Get Property Surface Resampled To Static Surface - * @param caseUuid Sumo case uuid - * @param ensembleName Ensemble name - * @param realizationNumMesh Realization number - * @param nameMesh Surface name - * @param attributeMesh Surface attribute - * @param realizationNumProperty Realization number - * @param nameProperty Surface name - * @param attributeProperty Surface attribute - * @param timeOrIntervalProperty Time point or time interval string - * @returns SurfaceData Successful Response - * @throws ApiError - */ - public getPropertySurfaceResampledToStaticSurface( - caseUuid: string, - ensembleName: string, - realizationNumMesh: number, - nameMesh: string, - attributeMesh: string, - realizationNumProperty: number, - nameProperty: string, - attributeProperty: string, - timeOrIntervalProperty?: (string | null), - ): CancelablePromise { - return this.httpRequest.request({ - method: 'GET', - url: '/surface/property_surface_resampled_to_static_surface/', - query: { - 'case_uuid': caseUuid, - 'ensemble_name': ensembleName, - 'realization_num_mesh': realizationNumMesh, - 'name_mesh': nameMesh, - 'attribute_mesh': attributeMesh, - 'realization_num_property': realizationNumProperty, - 'name_property': nameProperty, - 'attribute_property': attributeProperty, - 'time_or_interval_property': timeOrIntervalProperty, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * Get Property Surface Resampled To Statistical Static Surface - * @param caseUuid Sumo case uuid - * @param ensembleName Ensemble name - * @param statisticFunction Statistics to calculate - * @param nameMesh Surface name - * @param attributeMesh Surface attribute - * @param nameProperty Surface name - * @param attributeProperty Surface attribute - * @param timeOrIntervalProperty Time point or time interval string - * @returns SurfaceData Successful Response + * Get Surface Data + * Get surface data for the specified surface. + * + * --- + * *General description of the types of surface addresses that exist. The specific address types supported by this endpoint can be a subset of these.* + * + * - *REAL* - Realization surface address. Addresses a specific realization surface within an ensemble. Always specifies a single realization number + * - *OBS* - Observed surface address. Addresses an observed surface which is not associated with any specific ensemble. + * - *STAT* - Statistical surface address. Fully specifies a statistical surface, including the statistic function and which realizations to include. + * - *PARTIAL* - Partial surface address. Similar to a realization surface address, but does not include a specific realization number. + * + * Structure of the different types of address strings: + * + * ``` + * REAL~~~~~~~~~~[~~] + * STAT~~~~~~~~~~~~[~~] + * OBS~~~~~~~~ + * PARTIAL~~~~~~~~[~~] + * ``` + * + * The `` component in a *STAT* address contains the list of realizations to include in the statistics + * encoded as a `UintListStr` or "*" to include all realizations. + * @param surfAddrStr Surface address string, supported address types are *REAL*, *OBS* and *STAT* + * @param dataFormat Format of binary data in the response + * @param resampleToDefStr Definition of the surface onto which the data should be resampled. *SurfaceDef* object properties encoded as a `KeyValStr` string. + * @returns any Successful Response * @throws ApiError */ - public getPropertySurfaceResampledToStatisticalStaticSurface( - caseUuid: string, - ensembleName: string, - statisticFunction: SurfaceStatisticFunction, - nameMesh: string, - attributeMesh: string, - nameProperty: string, - attributeProperty: string, - timeOrIntervalProperty?: (string | null), - ): CancelablePromise { + public getSurfaceData( + surfAddrStr: string, + dataFormat: 'float' | 'png' = 'float', + resampleToDefStr?: (string | null), + ): CancelablePromise<(SurfaceDataFloat | SurfaceDataPng)> { return this.httpRequest.request({ method: 'GET', - url: '/surface/property_surface_resampled_to_statistical_static_surface/', + url: '/surface/surface_data', query: { - 'case_uuid': caseUuid, - 'ensemble_name': ensembleName, - 'statistic_function': statisticFunction, - 'name_mesh': nameMesh, - 'attribute_mesh': attributeMesh, - 'name_property': nameProperty, - 'attribute_property': attributeProperty, - 'time_or_interval_property': timeOrIntervalProperty, + 'surf_addr_str': surfAddrStr, + 'data_format': dataFormat, + 'resample_to_def_str': resampleToDefStr, }, errors: { 422: `Validation Error`, @@ -321,4 +185,68 @@ export class SurfaceService { }, }); } + /** + * Get Delta Surface Data + * @param surfAAddrStr Address string of surface A, supported types: *REAL*, *OBS* and *STAT* + * @param surfBAddrStr Address string of surface B, supported types: *REAL*, *OBS* and *STAT* + * @param dataFormat Format of binary data in the response + * @param resampleToDefStr Definition of the surface onto which the data should be resampled. *SurfaceDef* object properties encoded as a `KeyValStr` string. + * @returns SurfaceDataFloat Successful Response + * @throws ApiError + */ + public getDeltaSurfaceData( + surfAAddrStr: string, + surfBAddrStr: string, + dataFormat: 'float' | 'png' = 'float', + resampleToDefStr?: (string | null), + ): CancelablePromise> { + return this.httpRequest.request({ + method: 'GET', + url: '/surface/delta_surface_data', + query: { + 'surf_a_addr_str': surfAAddrStr, + 'surf_b_addr_str': surfBAddrStr, + 'data_format': dataFormat, + 'resample_to_def_str': resampleToDefStr, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Get Misfit Surface Data + * @param obsSurfAddrStr Address of observed surface, only supported address type is *OBS* + * @param simSurfAddrStr Address of simulated surface, supported type is *PARTIAL* + * @param statisticFunctions Statistics to calculate + * @param realizations Realization numbers + * @param dataFormat Format of binary data in the response + * @param resampleToDefStr Definition of the surface onto which the data should be resampled. *SurfaceDef* object properties encoded as a `KeyValStr` string. + * @returns SurfaceDataFloat Successful Response + * @throws ApiError + */ + public getMisfitSurfaceData( + obsSurfAddrStr: string, + simSurfAddrStr: string, + statisticFunctions: Array, + realizations: Array, + dataFormat: 'float' | 'png' = 'float', + resampleToDefStr?: (string | null), + ): CancelablePromise> { + return this.httpRequest.request({ + method: 'GET', + url: '/surface/misfit_surface_data', + query: { + 'obs_surf_addr_str': obsSurfAddrStr, + 'sim_surf_addr_str': simSurfAddrStr, + 'statistic_functions': statisticFunctions, + 'realizations': realizations, + 'data_format': dataFormat, + 'resample_to_def_str': resampleToDefStr, + }, + errors: { + 422: `Validation Error`, + }, + }); + } } diff --git a/frontend/src/modules/Map/MapSettings.tsx b/frontend/src/modules/Map/MapSettings.tsx index bd82c2913..3e1a2cd3e 100644 --- a/frontend/src/modules/Map/MapSettings.tsx +++ b/frontend/src/modules/Map/MapSettings.tsx @@ -15,7 +15,7 @@ import { Label } from "@lib/components/Label"; import { QueryStateWrapper } from "@lib/components/QueryStateWrapper"; import { RadioGroup } from "@lib/components/RadioGroup"; import { Select, SelectOption } from "@lib/components/Select"; -import { SurfaceAddress, SurfaceAddressFactory, SurfaceDirectory, SurfaceTimeType } from "@modules/_shared/Surface"; +import { FullSurfaceAddress, SurfaceAddressBuilder, SurfaceDirectory, SurfaceTimeType } from "@modules/_shared/Surface"; import { useObservedSurfacesMetadataQuery, useRealizationSurfacesMetadataQuery } from "@modules/_shared/Surface"; import { usePropagateApiErrorToStatusWriter } from "@modules/_shared/hooks/usePropagateApiErrorToStatusWriter"; @@ -96,23 +96,26 @@ export function MapSettings(props: ModuleSettingsProps) { } React.useEffect(function propagateSurfaceSelectionToView() { - let surfaceAddress: SurfaceAddress | null = null; + let surfaceAddress: FullSurfaceAddress | null = null; if (computedEnsembleIdent && computedSurfaceName && computedSurfaceAttribute) { - const addrFactory = new SurfaceAddressFactory( - computedEnsembleIdent.getCaseUuid(), - computedEnsembleIdent.getEnsembleName(), - computedSurfaceName, - computedSurfaceAttribute, - computedTimeOrInterval - ); - if (aggregation === null) { + const addrBuilder = new SurfaceAddressBuilder(); + addrBuilder.withEnsembleIdent(computedEnsembleIdent); + addrBuilder.withName(computedSurfaceName); + addrBuilder.withAttribute(computedSurfaceAttribute); + if (computedTimeOrInterval) { + addrBuilder.withTimeOrInterval(computedTimeOrInterval); + } + + if (aggregation) { + addrBuilder.withStatisticFunction(aggregation); + surfaceAddress = addrBuilder.buildStatisticalAddress(); + } else { if (useObserved) { - surfaceAddress = addrFactory.createObservedAddress(); + surfaceAddress = addrBuilder.buildObservedAddress(); } else { - surfaceAddress = addrFactory.createRealizationAddress(realizationNum); + addrBuilder.withRealization(realizationNum); + surfaceAddress = addrBuilder.buildRealizationAddress(); } - } else { - surfaceAddress = addrFactory.createStatisticalAddress(aggregation); } } diff --git a/frontend/src/modules/Map/MapState.ts b/frontend/src/modules/Map/MapState.ts index 512686952..630749b84 100644 --- a/frontend/src/modules/Map/MapState.ts +++ b/frontend/src/modules/Map/MapState.ts @@ -1,5 +1,5 @@ -import { SurfaceAddress } from "@modules/_shared/Surface/surfaceAddress"; +import { FullSurfaceAddress } from "@modules/_shared/Surface"; export interface MapState { - surfaceAddress: SurfaceAddress | null; + surfaceAddress: FullSurfaceAddress | null; } diff --git a/frontend/src/modules/Map/MapView.tsx b/frontend/src/modules/Map/MapView.tsx index 79bd23027..4ed8b498b 100644 --- a/frontend/src/modules/Map/MapView.tsx +++ b/frontend/src/modules/Map/MapView.tsx @@ -1,7 +1,9 @@ import React from "react"; +import { SurfaceDef_api } from "@api"; import { ModuleViewProps } from "@framework/Module"; import { useViewStatusWriter } from "@framework/StatusWriter"; +import { Vec2, rotatePoint2Around } from "@lib/utils/vec2"; import { ContentError, ContentInfo } from "@modules/_shared/components/ContentMessage"; import { usePropagateApiErrorToStatusWriter } from "@modules/_shared/hooks/usePropagateApiErrorToStatusWriter"; import { useSurfaceDataQueryByAddress } from "@modules_shared/Surface"; @@ -14,7 +16,9 @@ export function MapView(props: ModuleViewProps) { const surfaceAddress = props.viewContext.useStoreValue("surfaceAddress"); const statusWriter = useViewStatusWriter(props.viewContext); - const surfDataQuery = useSurfaceDataQueryByAddress(surfaceAddress); + + //const surfDataQuery = useSurfaceDataQueryByAddress(surfaceAddress, "png", null, true); + const surfDataQuery = useSurfaceDataQueryByAddress(surfaceAddress, "float", null, true); const isLoading = surfDataQuery.isFetching; statusWriter.setLoading(isLoading); @@ -23,6 +27,7 @@ export function MapView(props: ModuleViewProps) { usePropagateApiErrorToStatusWriter(surfDataQuery, statusWriter); const surfData = surfDataQuery.data; + return (
{hasError ? ( @@ -37,14 +42,14 @@ export function MapView(props: ModuleViewProps) { layers={[ { "@@type": "MapLayer", + "@@typedArraySupport": true, id: "mesh-layer", - // Drop conversion as soon as SubsurfaceViewer accepts typed arrays - meshData: Array.from(surfData.valuesFloat32Arr), + meshData: surfData.valuesFloat32Arr, frame: { - origin: [surfData.x_ori, surfData.y_ori], - count: [surfData.x_count, surfData.y_count], - increment: [surfData.x_inc, surfData.y_inc], - rotDeg: surfData.rot_deg, + origin: [surfData.surface_def.origin_utm_x, surfData.surface_def.origin_utm_y], + count: [surfData.surface_def.npoints_x, surfData.surface_def.npoints_y], + increment: [surfData.surface_def.inc_x, surfData.surface_def.inc_y], + rotDeg: surfData.surface_def.rot_deg, }, contours: [0, 100], @@ -54,9 +59,42 @@ export function MapView(props: ModuleViewProps) { smoothShading: true, colorMapName: "Physics", }, + // { + // // Experiment with showing PNG image in a ColormapLayer + // "@@type": "ColormapLayer", + // id: "image-layer", + // image: `data:image/png;base64,${surfData.png_image_base64}`, + // bounds: _calcBoundsForRotationAroundUpperLeftCorner(surfData.surface_def), + // rotDeg: surfData.surface_def.rot_deg, + // valueRange: [surfData.value_min, surfData.value_max], + // colorMapName: "Physics", + // }, ]} /> )}
); } + +// Calculate Deck.gl style bounds that are suitable for usage with a rotated image in the ColormapLayer, +// which expects rotation to be specified around the upper left corner of the image. +// +// The ColormapLayer derives from deck.gl's BitmapLayer, which expects bounds in the form [left, bottom, right, top] +// +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function _calcBoundsForRotationAroundUpperLeftCorner(surfDef: SurfaceDef_api): number[] { + const width = (surfDef.npoints_x - 1) * surfDef.inc_x; + const height = (surfDef.npoints_y - 1) * surfDef.inc_y; + const orgRotPoint: Vec2 = { x: surfDef.origin_utm_x, y: surfDef.origin_utm_y }; + const orgTopLeft: Vec2 = { x: surfDef.origin_utm_x, y: surfDef.origin_utm_y + height }; + + const transTopLeft: Vec2 = rotatePoint2Around(orgTopLeft, orgRotPoint, (surfDef.rot_deg * Math.PI) / 180); + const tLeft = transTopLeft.x; + const tBottom = transTopLeft.y - height; + const tRight = transTopLeft.x + width; + const tTop = transTopLeft.y; + + const bounds = [tLeft, tBottom, tRight, tTop]; + + return bounds; +} diff --git a/frontend/src/modules/SubsurfaceMap/_utils/index.ts b/frontend/src/modules/SubsurfaceMap/_utils/index.ts index defa64dda..56569192c 100644 --- a/frontend/src/modules/SubsurfaceMap/_utils/index.ts +++ b/frontend/src/modules/SubsurfaceMap/_utils/index.ts @@ -6,5 +6,5 @@ export { createAxesLayer, createWellBoreHeaderLayer, } from "./subsurfaceMap"; -export type { SurfaceMeta, SurfaceMeshLayerSettings, ViewSettings } from "./subsurfaceMap"; +export type { SurfaceMeshLayerSettings, ViewSettings } from "./subsurfaceMap"; export { createContinuousColorScaleForMap } from "./color"; diff --git a/frontend/src/modules/SubsurfaceMap/_utils/subsurfaceMap.ts b/frontend/src/modules/SubsurfaceMap/_utils/subsurfaceMap.ts index dff51a55f..0944214de 100644 --- a/frontend/src/modules/SubsurfaceMap/_utils/subsurfaceMap.ts +++ b/frontend/src/modules/SubsurfaceMap/_utils/subsurfaceMap.ts @@ -1,4 +1,4 @@ -import { PolygonData_api, WellboreTrajectory_api } from "@api"; +import { SurfaceDef_api, PolygonData_api, WellboreTrajectory_api } from "@api"; export type SurfaceMeshLayerSettings = { contours?: boolean | number[]; @@ -10,21 +10,6 @@ export type SurfaceMeshLayerSettings = { export type ViewSettings = { show3d: boolean; }; -export type SurfaceMeta = { - x_ori: number; - y_ori: number; - x_count: number; - y_count: number; - x_inc: number; - y_inc: number; - x_min: number; - x_max: number; - y_min: number; - y_max: number; - val_min: number; - val_max: number; - rot_deg: number; -}; const defaultSurfaceSettings: SurfaceMeshLayerSettings = { contours: false, @@ -52,22 +37,23 @@ export function createAxesLayer( }; } export function createSurfaceMeshLayer( - surfaceMeta: SurfaceMeta, - mesh_data: number[], + surfaceDef: SurfaceDef_api, + mesh_data: Float32Array, surfaceSettings?: SurfaceMeshLayerSettings | null, - property_data?: number[] | null + property_data?: Float32Array | null ): Record { surfaceSettings = surfaceSettings || defaultSurfaceSettings; return { "@@type": "MapLayer", + "@@typedArraySupport": true, id: "mesh-layer", meshData: mesh_data, propertiesData: property_data, frame: { - origin: [surfaceMeta.x_ori, surfaceMeta.y_ori], - count: [surfaceMeta.x_count, surfaceMeta.y_count], - increment: [surfaceMeta.x_inc, surfaceMeta.y_inc], - rotDeg: surfaceMeta.rot_deg, + origin: [surfaceDef.origin_utm_x, surfaceDef.origin_utm_y], + count: [surfaceDef.npoints_x, surfaceDef.npoints_y], + increment: [surfaceDef.inc_x, surfaceDef.inc_y], + rotDeg: surfaceDef.rot_deg, }, contours: surfaceSettings.contours || false, diff --git a/frontend/src/modules/SubsurfaceMap/queryHooks.tsx b/frontend/src/modules/SubsurfaceMap/queryHooks.tsx deleted file mode 100644 index b9cdaed6a..000000000 --- a/frontend/src/modules/SubsurfaceMap/queryHooks.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { SurfaceData_api } from "@api"; -import { apiService } from "@framework/ApiService"; -import { SurfaceAddress } from "@modules_shared/Surface"; -import { SurfaceData_trans, transformSurfaceData } from "@modules_shared/Surface/queryDataTransforms"; -import { QueryFunction, QueryKey, UseQueryResult, useQuery } from "@tanstack/react-query"; - -const STALE_TIME = 60 * 1000; -const CACHE_TIME = 60 * 1000; - -export function usePropertySurfaceDataByQueryAddress( - meshSurfAddr: SurfaceAddress | null, - propertySurfAddr: SurfaceAddress | null, - enabled: boolean -): UseQueryResult { - function dummyApiCall(): Promise { - return new Promise((_resolve, reject) => { - reject(null); - }); - } - - let queryFn: QueryFunction | null = null; - let queryKey: QueryKey | null = null; - - if (!propertySurfAddr || !meshSurfAddr) { - queryKey = ["getSurfaceData_DUMMY_ALWAYS_DISABLED"]; - queryFn = dummyApiCall; - enabled = false; - } else if (meshSurfAddr.addressType === "realization" && propertySurfAddr.addressType === "realization") { - queryKey = [ - "getPropertySurfaceResampledToStaticSurface", - meshSurfAddr.caseUuid, - meshSurfAddr.ensemble, - meshSurfAddr.realizationNum, - meshSurfAddr.name, - meshSurfAddr.attribute, - propertySurfAddr.realizationNum, - propertySurfAddr.name, - propertySurfAddr.attribute, - propertySurfAddr.isoDateOrInterval, - ]; - queryFn = () => - apiService.surface.getPropertySurfaceResampledToStaticSurface( - meshSurfAddr.caseUuid, - meshSurfAddr.ensemble, - meshSurfAddr.realizationNum, - meshSurfAddr.name, - meshSurfAddr.attribute, - propertySurfAddr.realizationNum, - propertySurfAddr.name, - propertySurfAddr.attribute, - propertySurfAddr.isoDateOrInterval - ); - } else if (meshSurfAddr.addressType === "statistical" && propertySurfAddr.addressType === "statistical") { - queryKey = [ - "getPropertySurfaceResampledToStaticSurface", - meshSurfAddr.caseUuid, - meshSurfAddr.ensemble, - meshSurfAddr.statisticFunction, - meshSurfAddr.name, - meshSurfAddr.attribute, - // propertySurfAddr.statisticFunction, - propertySurfAddr.name, - propertySurfAddr.attribute, - propertySurfAddr.isoDateOrInterval, - ]; - queryFn = () => - apiService.surface.getPropertySurfaceResampledToStatisticalStaticSurface( - meshSurfAddr.caseUuid, - meshSurfAddr.ensemble, - meshSurfAddr.statisticFunction, - meshSurfAddr.name, - meshSurfAddr.attribute, - // propertySurfAddr.statisticFunction, - propertySurfAddr.name, - propertySurfAddr.attribute, - propertySurfAddr.isoDateOrInterval - ); - } else { - throw new Error("Invalid surface address type"); - } - - return useQuery({ - queryKey: queryKey, - queryFn: queryFn, - select: transformSurfaceData, - staleTime: STALE_TIME, - gcTime: CACHE_TIME, - enabled: enabled, - }); -} diff --git a/frontend/src/modules/SubsurfaceMap/settings.tsx b/frontend/src/modules/SubsurfaceMap/settings.tsx index bd4340e5b..b0297ec73 100644 --- a/frontend/src/modules/SubsurfaceMap/settings.tsx +++ b/frontend/src/modules/SubsurfaceMap/settings.tsx @@ -17,7 +17,8 @@ import { QueryStateWrapper } from "@lib/components/QueryStateWrapper"; import { RadioGroup } from "@lib/components/RadioGroup"; import { Select, SelectOption } from "@lib/components/Select"; import { PolygonsAddress, PolygonsDirectory, usePolygonsDirectoryQuery } from "@modules/_shared/Polygons"; -import { SurfaceAddress, SurfaceAddressFactory, SurfaceDirectory, SurfaceTimeType } from "@modules/_shared/Surface"; +import { RealizationSurfaceAddress, StatisticalSurfaceAddress } from "@modules/_shared/Surface"; +import { SurfaceAddressBuilder, SurfaceDirectory, SurfaceTimeType } from "@modules/_shared/Surface"; import { useRealizationSurfacesMetadataQuery } from "@modules/_shared/Surface"; import { useDrilledWellboreHeadersQuery } from "@modules/_shared/WellBore/queryHooks"; @@ -225,21 +226,19 @@ export function Settings({ settingsContext, workbenchSession, workbenchServices React.useEffect( function propagateMeshSurfaceSelectionToView() { - let surfAddr: SurfaceAddress | null = null; + let surfAddr: RealizationSurfaceAddress | StatisticalSurfaceAddress | null = null; if (computedEnsembleIdent && computedMeshSurfaceName && computedMeshSurfaceAttribute) { - const addrFactory = new SurfaceAddressFactory( - computedEnsembleIdent.getCaseUuid(), - computedEnsembleIdent.getEnsembleName(), - computedMeshSurfaceName, - computedMeshSurfaceAttribute, - null - ); - - if (aggregation === null) { - surfAddr = addrFactory.createRealizationAddress(realizationNum); + const addrBuilder = new SurfaceAddressBuilder(); + addrBuilder.withEnsembleIdent(computedEnsembleIdent); + addrBuilder.withName(computedMeshSurfaceName); + addrBuilder.withAttribute(computedMeshSurfaceAttribute); + if (aggregation) { + addrBuilder.withStatisticFunction(aggregation); + surfAddr = addrBuilder.buildStatisticalAddress(); } else { - surfAddr = addrFactory.createStatisticalAddress(aggregation); + addrBuilder.withRealization(realizationNum); + surfAddr = addrBuilder.buildRealizationAddress(); } } @@ -260,24 +259,23 @@ export function Settings({ settingsContext, workbenchSession, workbenchServices ); React.useEffect( function propagatePropertySurfaceSelectionToView() { - let surfAddr: SurfaceAddress | null = null; + let surfAddr: RealizationSurfaceAddress | StatisticalSurfaceAddress | null = null; if (!usePropertySurface) { settingsContext.getStateStore().setValue("propertySurfaceAddress", surfAddr); return; } if (computedEnsembleIdent && computedPropertySurfaceName && computedPropertySurfaceAttribute) { - const addrFactory = new SurfaceAddressFactory( - computedEnsembleIdent.getCaseUuid(), - computedEnsembleIdent.getEnsembleName(), - computedPropertySurfaceName, - computedPropertySurfaceAttribute, - computedPropertyTimeOrInterval - ); - - if (aggregation === null) { - surfAddr = addrFactory.createRealizationAddress(realizationNum); + const addrBuilder = new SurfaceAddressBuilder(); + addrBuilder.withEnsembleIdent(computedEnsembleIdent); + addrBuilder.withName(computedPropertySurfaceName); + addrBuilder.withAttribute(computedPropertySurfaceAttribute); + addrBuilder.withTimeOrInterval(computedPropertyTimeOrInterval); + if (aggregation) { + addrBuilder.withStatisticFunction(aggregation); + surfAddr = addrBuilder.buildStatisticalAddress(); } else { - surfAddr = addrFactory.createStatisticalAddress(aggregation); + addrBuilder.withRealization(realizationNum); + surfAddr = addrBuilder.buildRealizationAddress(); } } diff --git a/frontend/src/modules/SubsurfaceMap/state.ts b/frontend/src/modules/SubsurfaceMap/state.ts index 810f1b7d6..45c5ba0cf 100644 --- a/frontend/src/modules/SubsurfaceMap/state.ts +++ b/frontend/src/modules/SubsurfaceMap/state.ts @@ -1,11 +1,11 @@ import { PolygonsAddress } from "@modules/_shared/Polygons/polygonsAddress"; -import { SurfaceAddress } from "@modules/_shared/Surface"; +import { RealizationSurfaceAddress, StatisticalSurfaceAddress } from "@modules/_shared/Surface"; import { SurfaceMeshLayerSettings, ViewSettings } from "./_utils"; export interface state { - meshSurfaceAddress: SurfaceAddress | null; - propertySurfaceAddress: SurfaceAddress | null; + meshSurfaceAddress: RealizationSurfaceAddress | StatisticalSurfaceAddress | null; + propertySurfaceAddress: RealizationSurfaceAddress | StatisticalSurfaceAddress | null; polygonsAddress: PolygonsAddress | null; selectedWellUuids: string[]; surfaceSettings: SurfaceMeshLayerSettings | null; diff --git a/frontend/src/modules/SubsurfaceMap/view.tsx b/frontend/src/modules/SubsurfaceMap/view.tsx index c52835c78..9f587acf0 100644 --- a/frontend/src/modules/SubsurfaceMap/view.tsx +++ b/frontend/src/modules/SubsurfaceMap/view.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { PolygonData_api, WellboreTrajectory_api } from "@api"; +import { BoundingBox2d_api, PolygonData_api, SurfaceDef_api, WellboreTrajectory_api } from "@api"; import { ContinuousLegend } from "@emerson-eps/color-tables"; import { EnsembleIdent } from "@framework/EnsembleIdent"; import { ModuleViewProps } from "@framework/Module"; @@ -16,7 +16,6 @@ import { useSurfaceDataQueryByAddress } from "@modules_shared/Surface"; import { ViewAnnotation } from "@webviz/subsurface-viewer/dist/components/ViewAnnotation"; import { - SurfaceMeta, createAxesLayer, createContinuousColorScaleForMap, createNorthArrowLayer, @@ -26,7 +25,6 @@ import { createWellboreTrajectoryLayer, } from "./_utils"; import { SyncedSubsurfaceViewer } from "./components/SyncedSubsurfaceViewer"; -import { usePropertySurfaceDataByQueryAddress } from "./queryHooks"; import { state } from "./state"; type Bounds = [number, number, number, number]; @@ -34,9 +32,9 @@ type Bounds = [number, number, number, number]; const updateViewPortBounds = ( existingViewPortBounds: Bounds | undefined, resetBounds: boolean, - surfaceMeta: SurfaceMeta + surfaceBB: BoundingBox2d_api ): Bounds => { - const updatedBounds: Bounds = [surfaceMeta.x_min, surfaceMeta.y_min, surfaceMeta.x_max, surfaceMeta.y_max]; + const updatedBounds: Bounds = [surfaceBB.min_x, surfaceBB.min_y, surfaceBB.max_x, surfaceBB.max_y]; if (!existingViewPortBounds || resetBounds) { console.debug("updateViewPortBounds: no existing bounds, returning updated bounds"); @@ -57,6 +55,7 @@ const updateViewPortBounds = ( // Otherwise, return the existing bounds return existingViewPortBounds; }; + //----------------------------------------------------------------------------------------------------------- export function View({ viewContext, workbenchSettings, workbenchServices, workbenchSession }: ModuleViewProps) { const myInstanceIdStr = viewContext.getInstanceIdString(); @@ -87,10 +86,15 @@ export function View({ viewContext, workbenchSettings, workbenchServices, workbe const colorTables = createContinuousColorScaleForMap(surfaceColorScale); const show3D: boolean = viewSettings?.show3d ?? true; - const meshSurfDataQuery = useSurfaceDataQueryByAddress(meshSurfAddr); + const meshSurfDataQuery = useSurfaceDataQueryByAddress(meshSurfAddr, "float", null, true); - const hasMeshSurfData = meshSurfDataQuery?.data ? true : false; - const propertySurfDataQuery = usePropertySurfaceDataByQueryAddress(meshSurfAddr, propertySurfAddr, hasMeshSurfData); + let hasMeshSurfData = false; + let resampleTo: SurfaceDef_api | null = null; + if (meshSurfDataQuery.data) { + hasMeshSurfData = true; + resampleTo = meshSurfDataQuery.data.surface_def; + } + const propertySurfDataQuery = useSurfaceDataQueryByAddress(propertySurfAddr, "float", resampleTo, hasMeshSurfData); let fieldIdentifier: null | string = null; if (meshSurfAddr) { @@ -106,31 +110,22 @@ export function View({ viewContext, workbenchSettings, workbenchServices, workbe // Mesh data query should only trigger update if the property surface address is not set or if the property surface data is loaded if (meshSurfDataQuery.data && !propertySurfAddr) { - // Drop conversion as soon as SubsurfaceViewer accepts typed arrays - const newMeshData = Array.from(meshSurfDataQuery.data.valuesFloat32Arr); - - const newSurfaceMetaData: SurfaceMeta = { ...meshSurfDataQuery.data }; const surfaceLayer: Record = createSurfaceMeshLayer( - newSurfaceMetaData, - newMeshData, + meshSurfDataQuery.data.surface_def, + meshSurfDataQuery.data.valuesFloat32Arr, surfaceSettings ); newLayers.push(surfaceLayer); - colorRange = [meshSurfDataQuery.data.val_min, meshSurfDataQuery.data.val_max]; + colorRange = [meshSurfDataQuery.data.value_min, meshSurfDataQuery.data.value_max]; } else if (meshSurfDataQuery.data && propertySurfDataQuery.data) { - // Drop conversion as soon as SubsurfaceViewer accepts typed arrays - const newMeshData = Array.from(meshSurfDataQuery.data.valuesFloat32Arr); - const newPropertyData = Array.from(propertySurfDataQuery.data.valuesFloat32Arr); - - const newSurfaceMetaData: SurfaceMeta = { ...meshSurfDataQuery.data }; const surfaceLayer: Record = createSurfaceMeshLayer( - newSurfaceMetaData, - newMeshData, + meshSurfDataQuery.data.surface_def, + meshSurfDataQuery.data.valuesFloat32Arr, surfaceSettings, - newPropertyData + propertySurfDataQuery.data.valuesFloat32Arr ); newLayers.push(surfaceLayer); - colorRange = [propertySurfDataQuery.data.val_min, propertySurfDataQuery.data.val_max]; + colorRange = [propertySurfDataQuery.data.value_min, propertySurfDataQuery.data.value_max]; } // Calculate viewport bounds and axes layer from the surface bounds. @@ -138,17 +133,17 @@ export function View({ viewContext, workbenchSettings, workbenchServices, workbe React.useEffect(() => { if (meshSurfDataQuery.data) { - const newSurfaceMetaData: SurfaceMeta = { ...meshSurfDataQuery.data }; + const surfaceBoundingBox = meshSurfDataQuery.data.transformed_bbox_utm; - setviewPortBounds(updateViewPortBounds(viewportBounds, resetBounds, newSurfaceMetaData)); + setviewPortBounds(updateViewPortBounds(viewportBounds, resetBounds, surfaceBoundingBox)); toggleResetBounds(false); const axesLayer: Record = createAxesLayer([ - newSurfaceMetaData.x_min, - newSurfaceMetaData.y_min, + surfaceBoundingBox.min_x, + surfaceBoundingBox.min_y, 0, - newSurfaceMetaData.x_max, - newSurfaceMetaData.y_max, + surfaceBoundingBox.max_x, + surfaceBoundingBox.max_y, 3500, ]); setAxesLayer(axesLayer); diff --git a/frontend/src/modules/_shared/Surface/SurfaceAddressBuilder.ts b/frontend/src/modules/_shared/Surface/SurfaceAddressBuilder.ts new file mode 100644 index 000000000..1f7b301fa --- /dev/null +++ b/frontend/src/modules/_shared/Surface/SurfaceAddressBuilder.ts @@ -0,0 +1,196 @@ +import { SurfaceStatisticFunction_api } from "@api"; +import { EnsembleIdent } from "@framework/EnsembleIdent"; + +import { SurfaceAddressType } from "./surfaceAddress"; +import { ObservedSurfaceAddress, RealizationSurfaceAddress, StatisticalSurfaceAddress } from "./surfaceAddress"; +import { AnySurfaceAddress, PartialSurfaceAddress } from "./surfaceAddress"; +import { encodeSurfAddrStr } from "./surfaceAddress"; + +export class SurfaceAddressBuilder { + private _addrType: SurfaceAddressType | null = null; + private _caseUuid: string | null = null; + private _ensemble: string | null = null; + private _name: string | null = null; + private _attribute: string | null = null; + private _realizationNum: number | null = null; + private _isoTimeOrInterval: string | null = null; + private _statisticFunction: SurfaceStatisticFunction_api | null = null; + + withType(addrType: SurfaceAddressType): this { + this._addrType = addrType; + return this; + } + + withEnsembleIdent(ensembleIdent: EnsembleIdent): this { + this._caseUuid = ensembleIdent.getCaseUuid(); + this._ensemble = ensembleIdent.getEnsembleName(); + return this; + } + + withName(name: string): this { + this._name = name; + return this; + } + + withAttribute(attribute: string): this { + this._attribute = attribute; + return this; + } + + withTimeOrInterval(isoTimeOrInterval: string | null): this { + this._isoTimeOrInterval = isoTimeOrInterval; + return this; + } + + withRealization(realization: number): this { + this._realizationNum = realization; + return this; + } + + withStatisticFunction(statisticFunction: SurfaceStatisticFunction_api): this { + this._statisticFunction = statisticFunction; + return this; + } + + buildRealizationAddress(): RealizationSurfaceAddress { + if (this._addrType && this._addrType !== "REAL") { + throw new Error("Address type is already set to another type than REAL"); + } + + if (this._realizationNum == null) { + throw new Error("Realization number not set"); + } + + this.assertThatCommonPropertiesAreSet(true); + + const retObj: RealizationSurfaceAddress = { + addressType: "REAL", + caseUuid: this._caseUuid!, + ensemble: this._ensemble!, + name: this._name!, + attribute: this._attribute!, + realizationNum: this._realizationNum, + isoTimeOrInterval: this._isoTimeOrInterval, + }; + return retObj; + } + + buildObservedAddress(): ObservedSurfaceAddress { + if (this._addrType && this._addrType !== "OBS") { + throw new Error("Address type is already set to another type than OBS"); + } + + if (!this._isoTimeOrInterval) { + throw new Error("Time or interval not set"); + } + + this.assertThatCommonPropertiesAreSet(false); + + const retObj: ObservedSurfaceAddress = { + addressType: "OBS", + caseUuid: this._caseUuid!, + name: this._name!, + attribute: this._attribute!, + isoTimeOrInterval: this._isoTimeOrInterval, + }; + return retObj; + } + + buildStatisticalAddress(): StatisticalSurfaceAddress { + if (this._addrType && this._addrType !== "STAT") { + throw new Error("Address type is already set to another type than STAT"); + } + + if (this._statisticFunction == null) { + throw new Error("Statistic function not set"); + } + + this.assertThatCommonPropertiesAreSet(true); + + const retObj: StatisticalSurfaceAddress = { + addressType: "STAT", + caseUuid: this._caseUuid!, + ensemble: this._ensemble!, + name: this._name!, + attribute: this._attribute!, + statFunction: this._statisticFunction, + statRealizations: null, + isoTimeOrInterval: this._isoTimeOrInterval, + }; + return retObj; + } + + buildPartialAddress(): PartialSurfaceAddress { + if (this._addrType && this._addrType !== "PARTIAL") { + throw new Error("Address type is already set to another type than PARTIAL"); + } + + this.assertThatCommonPropertiesAreSet(true); + + const retObj: PartialSurfaceAddress = { + addressType: "PARTIAL", + caseUuid: this._caseUuid!, + ensemble: this._ensemble!, + name: this._name!, + attribute: this._attribute!, + isoTimeOrInterval: this._isoTimeOrInterval, + }; + return retObj; + } + + buildAddress(): AnySurfaceAddress { + if (!this._addrType) { + throw new Error("Address type not set"); + } + + switch (this._addrType) { + case "REAL": + return this.buildRealizationAddress(); + case "OBS": + return this.buildObservedAddress(); + case "STAT": + return this.buildStatisticalAddress(); + case "PARTIAL": + return this.buildPartialAddress(); + default: + throw new Error("Invalid address type"); + } + } + + buildAddressNoThrow(): AnySurfaceAddress | null { + try { + return this.buildAddress(); + } catch (e) { + return null; + } + } + + buildAddrStr(): string { + const addr = this.buildAddress(); + return encodeSurfAddrStr(addr); + } + + buildAddrStrNoThrow(): string | null { + try { + const addr = this.buildAddress(); + return encodeSurfAddrStr(addr); + } catch (e) { + return null; + } + } + + private assertThatCommonPropertiesAreSet(requireEnsemble: boolean): void { + if (!this._caseUuid) { + throw new Error("Case UUID not set"); + } + if (requireEnsemble && !this._ensemble) { + throw new Error("Ensemble name not set"); + } + if (!this._name) { + throw new Error("Surface name not set"); + } + if (!this._attribute) { + throw new Error("Surface attribute not set"); + } + } +} diff --git a/frontend/src/modules/_shared/Surface/index.ts b/frontend/src/modules/_shared/Surface/index.ts index ae33a121c..c65fb77e1 100644 --- a/frontend/src/modules/_shared/Surface/index.ts +++ b/frontend/src/modules/_shared/Surface/index.ts @@ -1,5 +1,7 @@ export { SurfaceDirectory, SurfaceTimeType } from "./surfaceDirectory"; -export { useRealizationSurfacesMetadataQuery, useObservedSurfacesMetadataQuery, useSurfaceDataQueryByAddress } from "./queryHooks"; export type { SurfaceDirectoryOptions } from "./surfaceDirectory"; -export type { SurfaceAddress } from "./surfaceAddress"; -export { SurfaceAddressFactory } from "./surfaceAddress"; +export type { RealizationSurfaceAddress, ObservedSurfaceAddress, StatisticalSurfaceAddress } from "./surfaceAddress"; +export type { FullSurfaceAddress, PartialSurfaceAddress, AnySurfaceAddress } from "./surfaceAddress"; +export { SurfaceAddressBuilder } from "./SurfaceAddressBuilder"; +export { useRealizationSurfacesMetadataQuery, useObservedSurfacesMetadataQuery } from "./queryHooks"; +export { useSurfaceDataQuery, useSurfaceDataQueryByAddress } from "./queryHooks"; diff --git a/frontend/src/modules/_shared/Surface/queryDataTransforms.ts b/frontend/src/modules/_shared/Surface/queryDataTransforms.ts index 1377d68be..723703395 100644 --- a/frontend/src/modules/_shared/Surface/queryDataTransforms.ts +++ b/frontend/src/modules/_shared/Surface/queryDataTransforms.ts @@ -1,23 +1,30 @@ -import { SurfaceData_api } from "@api"; - +import { SurfaceDataFloat_api } from "@api"; +import { SurfaceDataPng_api } from "@api"; import { b64DecodeFloatArrayToFloat32 } from "@modules_shared/base64"; // Data structure for transformed data // Remove the base64 encoded data and replace with a Float32Array -export type SurfaceData_trans = Omit & { +export type SurfaceDataFloat_trans = Omit & { valuesFloat32Arr: Float32Array; }; -export function transformSurfaceData(apiData: SurfaceData_api): SurfaceData_trans { +export function transformSurfaceData( + apiData: SurfaceDataFloat_api | SurfaceDataPng_api +): SurfaceDataFloat_trans | SurfaceDataPng_api { const startTS = performance.now(); - const { values_b64arr, ...untransformedData } = apiData; - const dataFloat32Arr = b64DecodeFloatArrayToFloat32(values_b64arr); + if ("values_b64arr" in apiData) { + const { values_b64arr, ...untransformedData } = apiData; + const dataFloat32Arr = b64DecodeFloatArrayToFloat32(values_b64arr); + + console.debug(`transformSurfaceData() took: ${(performance.now() - startTS).toFixed(1)}ms`); - console.debug(`transformSurfaceData() took: ${(performance.now() - startTS).toFixed(1)}ms`); + return { + ...untransformedData, + valuesFloat32Arr: dataFloat32Arr, + }; + } - return { - ...untransformedData, - valuesFloat32Arr: dataFloat32Arr, - }; + // No transformation needed for PNG + return apiData; } diff --git a/frontend/src/modules/_shared/Surface/queryHooks.ts b/frontend/src/modules/_shared/Surface/queryHooks.ts index 84a46be2c..ee531521c 100644 --- a/frontend/src/modules/_shared/Surface/queryHooks.ts +++ b/frontend/src/modules/_shared/Surface/queryHooks.ts @@ -1,9 +1,12 @@ -import { SurfaceData_api, SurfaceMetaSet_api } from "@api"; +import { SurfaceDef_api, SurfaceMetaSet_api } from "@api"; +import { SurfaceDataPng_api } from "@api"; import { apiService } from "@framework/ApiService"; -import { QueryFunction, QueryKey, UseQueryResult, useQuery } from "@tanstack/react-query"; +import { encodePropertiesAsKeyValStr } from "@lib/utils/queryStringUtils"; +import { UseQueryResult, useQuery } from "@tanstack/react-query"; -import { SurfaceData_trans, transformSurfaceData } from "./queryDataTransforms"; -import { SurfaceAddress } from "./surfaceAddress"; +import { SurfaceDataFloat_trans, transformSurfaceData } from "./queryDataTransforms"; +import { FullSurfaceAddress } from "./surfaceAddress"; +import { encodeSurfAddrStr, peekSurfaceAddressType } from "./surfaceAddress"; const STALE_TIME = 60 * 1000; const CACHE_TIME = 60 * 1000; @@ -31,82 +34,45 @@ export function useObservedSurfacesMetadataQuery(caseUuid: string | undefined): }); } -export function useSurfaceDataQueryByAddress(surfAddr: SurfaceAddress | null): UseQueryResult { - function dummyApiCall(): Promise { - return new Promise((_resolve, reject) => { - reject(null); - }); +export function useSurfaceDataQuery(surfAddrStr: string | null, format: "float", resampleTo: SurfaceDef_api | null, allowEnable: boolean): UseQueryResult; // prettier-ignore +export function useSurfaceDataQuery(surfAddrStr: string | null, format: "png", resampleTo: SurfaceDef_api | null, allowEnable: boolean): UseQueryResult; // prettier-ignore +export function useSurfaceDataQuery(surfAddrStr: string | null, format: "float" | "png", resampleTo: SurfaceDef_api | null, allowEnable: boolean): UseQueryResult; // prettier-ignore +export function useSurfaceDataQuery( + surfAddrStr: string | null, + format: "float" | "png", + resampleTo: SurfaceDef_api | null, + allowEnable: boolean +): UseQueryResult { + if (surfAddrStr) { + const surfAddrType = peekSurfaceAddressType(surfAddrStr); + if (surfAddrType !== "OBS" && surfAddrType !== "REAL" && surfAddrType !== "STAT") { + throw new Error("Invalid surface address type for surface data query"); + } } - let queryFn: QueryFunction | null = null; - let queryKey: QueryKey | null = null; - - if (surfAddr === null) { - queryKey = ["getSurfaceData_DUMMY_ALWAYS_DISABLED"]; - queryFn = dummyApiCall; - } else if (surfAddr.addressType === "realization") { - queryKey = [ - "getRealizationSurfaceData", - surfAddr.caseUuid, - surfAddr.ensemble, - surfAddr.realizationNum, - surfAddr.name, - surfAddr.attribute, - surfAddr.isoDateOrInterval, - ]; - queryFn = () => - apiService.surface.getRealizationSurfaceData( - surfAddr.caseUuid, - surfAddr.ensemble, - surfAddr.realizationNum, - surfAddr.name, - surfAddr.attribute, - surfAddr.isoDateOrInterval ?? undefined - ); - } else if (surfAddr.addressType === "observed") { - queryKey = [ - "getObservedSurfaceData", - surfAddr.caseUuid, - surfAddr.name, - surfAddr.attribute, - surfAddr.isoDateOrInterval, - ]; - queryFn = () => - apiService.surface.getObservedSurfaceData( - surfAddr.caseUuid, - surfAddr.name, - surfAddr.attribute, - surfAddr.isoDateOrInterval ?? "" - ); - } else if (surfAddr.addressType === "statistical") { - queryKey = [ - "getStatisticalSurfaceData", - surfAddr.caseUuid, - surfAddr.ensemble, - surfAddr.statisticFunction, - surfAddr.name, - surfAddr.attribute, - surfAddr.isoDateOrInterval, - ]; - queryFn = () => - apiService.surface.getStatisticalSurfaceData( - surfAddr.caseUuid, - surfAddr.ensemble, - surfAddr.statisticFunction, - surfAddr.name, - surfAddr.attribute, - surfAddr.isoDateOrInterval ?? undefined - ); - } else { - throw new Error("Invalid surface address type"); + let resampleToKeyValStr: string | null = null; + if (resampleTo) { + resampleToKeyValStr = encodePropertiesAsKeyValStr(resampleTo); } return useQuery({ - queryKey: queryKey, - queryFn: queryFn, + queryKey: ["getSurfaceData", surfAddrStr, resampleToKeyValStr, format], + queryFn: () => apiService.surface.getSurfaceData(surfAddrStr ?? "", format, resampleToKeyValStr), select: transformSurfaceData, staleTime: STALE_TIME, gcTime: CACHE_TIME, - enabled: Boolean(surfAddr), + enabled: allowEnable && Boolean(surfAddrStr), }); } + +export function useSurfaceDataQueryByAddress(surfAddr: FullSurfaceAddress | null, format: "float", resampleTo: SurfaceDef_api | null, allowEnable: boolean): UseQueryResult; // prettier-ignore +export function useSurfaceDataQueryByAddress(surfAddr: FullSurfaceAddress | null, format: "png", resampleTo: SurfaceDef_api | null, allowEnable: boolean): UseQueryResult; // prettier-ignore +export function useSurfaceDataQueryByAddress( + surfAddr: FullSurfaceAddress | null, + format: "float" | "png", + resampleTo: SurfaceDef_api | null, + allowEnable: boolean +): UseQueryResult { + const surfAddrStr = surfAddr ? encodeSurfAddrStr(surfAddr) : null; + return useSurfaceDataQuery(surfAddrStr, format, resampleTo, allowEnable); +} diff --git a/frontend/src/modules/_shared/Surface/surfaceAddress.ts b/frontend/src/modules/_shared/Surface/surfaceAddress.ts index 254a2ddf9..3587869fc 100644 --- a/frontend/src/modules/_shared/Surface/surfaceAddress.ts +++ b/frontend/src/modules/_shared/Surface/surfaceAddress.ts @@ -1,89 +1,141 @@ import { SurfaceStatisticFunction_api } from "@api"; +import { encodeAsUintListStr } from "@lib/utils/queryStringUtils"; export interface RealizationSurfaceAddress { - addressType: "realization"; + addressType: "REAL"; caseUuid: string; ensemble: string; name: string; attribute: string; realizationNum: number; - isoDateOrInterval: string | null; + isoTimeOrInterval: string | null; } export interface ObservedSurfaceAddress { - addressType: "observed"; + addressType: "OBS"; caseUuid: string; - ensemble: string; name: string; attribute: string; - isoDateOrInterval: string | null; + isoTimeOrInterval: string; } export interface StatisticalSurfaceAddress { - addressType: "statistical"; + addressType: "STAT"; caseUuid: string; ensemble: string; name: string; attribute: string; - isoDateOrInterval: string | null; - statisticFunction: SurfaceStatisticFunction_api; + statFunction: SurfaceStatisticFunction_api; + statRealizations: number[] | null; + isoTimeOrInterval: string | null; } -export type SurfaceAddress = RealizationSurfaceAddress | ObservedSurfaceAddress | StatisticalSurfaceAddress; +export interface PartialSurfaceAddress { + addressType: "PARTIAL"; + caseUuid: string; + ensemble: string; + name: string; + attribute: string; + isoTimeOrInterval: string | null; +} + +export type FullSurfaceAddress = RealizationSurfaceAddress | ObservedSurfaceAddress | StatisticalSurfaceAddress; + +export type AnySurfaceAddress = + | RealizationSurfaceAddress + | ObservedSurfaceAddress + | StatisticalSurfaceAddress + | PartialSurfaceAddress; + +const SurfaceAddressTypeValues = ["REAL", "OBS", "STAT", "PARTIAL"] as const; +export type SurfaceAddressType = (typeof SurfaceAddressTypeValues)[number]; -export function makeSurfaceAddressString(addr: SurfaceAddress): string { - const valueArr = Object.values(addr); - const str = valueArr.join("--"); - return str; +const ADDR_COMP_DELIMITER = "~~"; + +export function encodeRealizationSurfAddrStr(addr: Omit): string { + const componentArr = ["REAL", addr.caseUuid, addr.ensemble, addr.name, addr.attribute, addr.realizationNum]; + if (addr.isoTimeOrInterval !== null) { + componentArr.push(addr.isoTimeOrInterval); + } + + assertThatNoComponentsContainDelimiter(componentArr); + + const addrStr = componentArr.join(ADDR_COMP_DELIMITER); + return addrStr; } -export class SurfaceAddressFactory { - private _caseUuid: string; - private _ensemble: string; - private _name: string; - private _attribute: string; - private _isoDateOrInterval: string | null; - - constructor(caseUuid: string, ensemble: string, name: string, attribute: string, isoDateOrInterval: string | null) { - this._caseUuid = caseUuid; - this._ensemble = ensemble; - this._name = name; - this._attribute = attribute; - this._isoDateOrInterval = isoDateOrInterval; +export function encodeObservedSurfAddrStr(addr: Omit): string { + const componentArr = ["OBS", addr.caseUuid, addr.name, addr.attribute]; + if (addr.isoTimeOrInterval !== null) { + componentArr.push(addr.isoTimeOrInterval); } - createRealizationAddress(realizationNum: number): RealizationSurfaceAddress { - return { - addressType: "realization", - caseUuid: this._caseUuid, - ensemble: this._ensemble, - name: this._name, - attribute: this._attribute, - realizationNum: realizationNum, - isoDateOrInterval: this._isoDateOrInterval, - }; + assertThatNoComponentsContainDelimiter(componentArr); + + const addrStr = componentArr.join(ADDR_COMP_DELIMITER); + return addrStr; +} + +export function encodeStatisticalSurfAddrStr(addr: Omit): string { + let realStr = "*"; + if (addr.statRealizations != null) { + realStr = encodeAsUintListStr(addr.statRealizations); } - createObservedAddress(): ObservedSurfaceAddress { - return { - addressType: "observed", - caseUuid: this._caseUuid, - ensemble: this._ensemble, - name: this._name, - attribute: this._attribute, - isoDateOrInterval: this._isoDateOrInterval, - }; + const componentArr = ["STAT", addr.caseUuid, addr.ensemble, addr.name, addr.attribute, addr.statFunction, realStr]; + if (addr.isoTimeOrInterval !== null) { + componentArr.push(addr.isoTimeOrInterval); } - createStatisticalAddress(statFunction: SurfaceStatisticFunction_api): StatisticalSurfaceAddress { - return { - addressType: "statistical", - caseUuid: this._caseUuid, - ensemble: this._ensemble, - name: this._name, - attribute: this._attribute, - isoDateOrInterval: this._isoDateOrInterval, - statisticFunction: statFunction, - }; + assertThatNoComponentsContainDelimiter(componentArr); + + const addrStr = componentArr.join(ADDR_COMP_DELIMITER); + return addrStr; +} + +export function encodePartialSurfAddrStr(addr: Omit): string { + const componentArr = ["PARTIAL", addr.caseUuid, addr.ensemble, addr.name, addr.attribute]; + if (addr.isoTimeOrInterval !== null) { + componentArr.push(addr.isoTimeOrInterval); + } + + assertThatNoComponentsContainDelimiter(componentArr); + + const addrStr = componentArr.join(ADDR_COMP_DELIMITER); + return addrStr; +} + +export function encodeSurfAddrStr(addr: AnySurfaceAddress): string { + switch (addr.addressType) { + case "REAL": + return encodeRealizationSurfAddrStr(addr); + case "OBS": + return encodeObservedSurfAddrStr(addr); + case "STAT": + return encodeStatisticalSurfAddrStr(addr); + case "PARTIAL": + return encodePartialSurfAddrStr(addr); + default: + throw new Error("Invalid address type"); + } +} + +export function peekSurfaceAddressType(surfAddrStr: string): SurfaceAddressType | null { + const addrTypeStr = surfAddrStr.split(ADDR_COMP_DELIMITER)[0]; + + const foundAddrType = SurfaceAddressTypeValues.find((val) => val === addrTypeStr); + if (!foundAddrType) { + return null; + } + + return foundAddrType; +} + + +function assertThatNoComponentsContainDelimiter(componentArr: Array): void { + for (const comp of componentArr) { + if (typeof comp === "string" && comp.includes(ADDR_COMP_DELIMITER)) { + throw new Error(`Address component contains delimiter, offending component: ${comp}`); + } } } diff --git a/nginx.conf b/nginx.conf index 0d9a0aea7..abf8c59e1 100644 --- a/nginx.conf +++ b/nginx.conf @@ -57,6 +57,8 @@ http { proxy_http_version 1.1; root /usr/share/nginx/dist; add_header Cache-Control "no-cache"; + # At the moment we need the "connect-src 'self' data:"" entry in order to use PNG images as data format + # add_header Content-Security-Policy "default-src 'self'; connect-src 'self' data:; style-src 'self' 'unsafe-inline' https://cdn.eds.equinor.com; script-src 'self' 'unsafe-eval' blob:; font-src https://cdn.eds.equinor.com; img-src 'self' data:; form-action 'self'; base-uri 'none'; frame-ancestors 'none';"; add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline' https://cdn.eds.equinor.com; script-src 'self' 'unsafe-eval' blob:; font-src https://cdn.eds.equinor.com; img-src 'self' data:; form-action 'self'; base-uri 'none'; frame-ancestors 'none';"; add_header X-Content-Type-Options "nosniff"; add_header X-Frame-Options "DENY";