diff --git a/requirements.in b/requirements.in index 56aa9904..3f540f85 100644 --- a/requirements.in +++ b/requirements.in @@ -1,8 +1,8 @@ -opengeode-core==17.3.7 +opengeode-core==17.4.4 opengeode-io==7.7.0 opengeode-inspector==6.11.6 opengeode-inspector-geosciences==1.1.0 -opengeode-geosciences==9.6.0 +opengeode-geosciences==9.6.1 opengeode-geosciencesio==5.9.0 geode-common==33.24.2 geode-viewables==3.4.0 diff --git a/requirements.txt b/requirements.txt index 26be316f..b0223a5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,7 +31,7 @@ markupsafe>=3 # flask # jinja2 # werkzeug -opengeode-core==17.3.7 +opengeode-core==17.4.4 # via # -r requirements.in # geode-common @@ -41,7 +41,7 @@ opengeode-core==17.3.7 # opengeode-inspector # opengeode-inspector-geosciences # opengeode-io -opengeode-geosciences==9.6.0 +opengeode-geosciences==9.6.1 # via # -r requirements.in # geode-viewables @@ -66,4 +66,3 @@ werkzeug==3.1.8 # flask # flask-cors -opengeodeweb-microservice==1.*,>=1.1.4 diff --git a/src/opengeodeweb_back/geode_objects/__init__.py b/src/opengeodeweb_back/geode_objects/__init__.py index d1329a1d..5eb46f65 100644 --- a/src/opengeodeweb_back/geode_objects/__init__.py +++ b/src/opengeodeweb_back/geode_objects/__init__.py @@ -31,6 +31,7 @@ from .geode_cross_section import GeodeCrossSection from .geode_implicit_structural_model import GeodeImplicitStructuralModel from .geode_implicit_cross_section import GeodeImplicitCrossSection +from .geode_horizon_stack3d import GeodeHorizonStack3D geode_objects: dict[GeodeObjectType, type[GeodeObject]] = { "VertexSet": GeodeVertexSet, @@ -58,4 +59,5 @@ "CrossSection": GeodeCrossSection, "ImplicitStructuralModel": GeodeImplicitStructuralModel, "ImplicitCrossSection": GeodeImplicitCrossSection, + "HorizonStack3D": GeodeHorizonStack3D, } diff --git a/src/opengeodeweb_back/geode_objects/geode_horizon_stack3d.py b/src/opengeodeweb_back/geode_objects/geode_horizon_stack3d.py new file mode 100644 index 00000000..7069e61c --- /dev/null +++ b/src/opengeodeweb_back/geode_objects/geode_horizon_stack3d.py @@ -0,0 +1,103 @@ +# Standard library imports +from __future__ import annotations +from typing import Any + +# Third party imports +import opengeode as og +import opengeode_geosciences as og_geosciences +from opengeodeweb_microservice.database.data_types import GeodeModelType + +# Local application imports +from .geode_model import GeodeModel, ComponentRegistry + + +class GeodeHorizonStack3D(GeodeModel): + horizon_stack: og_geosciences.HorizonsStack3D + + def __init__( + self, horizon_stack: og_geosciences.HorizonsStack3D | None = None + ) -> None: + self.horizon_stack = ( + horizon_stack + if horizon_stack is not None + else og_geosciences.HorizonsStack3D() + ) + super().__init__(self.horizon_stack) + + @classmethod + def geode_object_type(cls) -> GeodeModelType: + return "HorizonStack3D" + + def native_extension(self) -> str: + return self.horizon_stack.native_extension() + + @classmethod + def is_3D(cls) -> bool: + return True + + @classmethod + def is_viewable(cls) -> bool: + return False + + def builder(self) -> og_geosciences.HorizonsStackBuilder3D: + return og_geosciences.HorizonsStackBuilder3D(self.horizon_stack) + + @classmethod + def load(cls, filename: str) -> GeodeHorizonStack3D: + return GeodeHorizonStack3D(og_geosciences.load_horizons_stack3D(filename)) + + @classmethod + def additional_files(cls, filename: str) -> og.AdditionalFiles: + return og_geosciences.horizons_stack_additional_files3D(filename) + + @classmethod + def is_loadable(cls, filename: str) -> og.Percentage: + return og_geosciences.is_horizons_stack_loadable3D(filename) + + @classmethod + def input_extensions(cls) -> list[str]: + return og_geosciences.HorizonsStackInputFactory3D.list_creators() + + @classmethod + def output_extensions(cls) -> list[str]: + return og_geosciences.HorizonsStackOutputFactory3D.list_creators() + + @classmethod + def object_priority(cls, filename: str) -> int: + return og_geosciences.horizons_stack_object_priority3D(filename) + + def is_saveable(self, filename: str) -> bool: + return og_geosciences.is_horizons_stack_saveable3D(self.horizon_stack, filename) + + def save(self, filename: str) -> list[str]: + return og_geosciences.save_horizons_stack3D(self.horizon_stack, filename) + + def save_viewable(self, filename_without_extension: str) -> str: + return "" + + def save_light_viewable(self, filename_without_extension: str) -> str: + return "" + + def mesh_components(self) -> ComponentRegistry: + return {} + + def collection_components(self) -> ComponentRegistry: + return {} + + def boundaries(self, id: og.uuid) -> list[og.ComponentID]: + return [] + + def internals(self, id: og.uuid) -> list[og.ComponentID]: + return [] + + def items(self, id: og.uuid) -> list[og.ComponentID]: + return [] + + def component(self, id: og.uuid) -> og.Component3D: + raise NotImplementedError("HorizonStack3D has no mesh components") + + def inspect(self) -> Any: + return None + + def validate(self) -> Any: + return None diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index ae1c6818..264816f5 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -226,7 +226,7 @@ def save_viewable_file() -> flask.Response: ) params = schemas.SaveViewableFile.from_dict(json_data) return flask.make_response( - utils_functions.generate_native_viewable_and_light_viewable_from_file( + utils_functions.generate_files_from_file( geode_object_type=geode_object_type(params.geode_object_type), input_file=params.filename, ), diff --git a/src/opengeodeweb_back/routes/create/blueprint_create.py b/src/opengeodeweb_back/routes/create/blueprint_create.py index f325aa71..460931c9 100644 --- a/src/opengeodeweb_back/routes/create/blueprint_create.py +++ b/src/opengeodeweb_back/routes/create/blueprint_create.py @@ -35,9 +35,7 @@ def point_set() -> flask.Response: for point in params.points: builder.create_point(opengeode.Point3D([point.x, point.y, point.z])) - result = utils_functions.generate_native_viewable_and_light_viewable_from_object( - pointset - ) + result = utils_functions.generate_files_from_object(pointset) return flask.make_response(result, 200) @@ -61,9 +59,7 @@ def edged_curve() -> flask.Response: for edge in params.edges: builder.create_edge_with_vertices(edge[0], edge[1]) - result = utils_functions.generate_native_viewable_and_light_viewable_from_object( - edged_curve_obj - ) + result = utils_functions.generate_files_from_object(edged_curve_obj) return flask.make_response(result, 200) @@ -89,7 +85,5 @@ def polygonal_surface() -> flask.Response: builder.compute_polygon_adjacencies() - result = utils_functions.generate_native_viewable_and_light_viewable_from_object( - polygonal_surface_obj - ) + result = utils_functions.generate_files_from_object(polygonal_surface_obj) return flask.make_response(result, 200) diff --git a/src/opengeodeweb_back/utils_functions.py b/src/opengeodeweb_back/utils_functions.py index 34e5cf3b..dff82a1a 100644 --- a/src/opengeodeweb_back/utils_functions.py +++ b/src/opengeodeweb_back/utils_functions.py @@ -198,20 +198,22 @@ def create_data_folder_from_id(data_id: str) -> str: def model_components( - data_id: str, model: GeodeModel, viewable_file: str + data_id: str, model: GeodeModel, viewable_file: str | None ) -> dict[str, Any]: - vtm_file_path = geode_functions.data_file_path(data_id, viewable_file) - tree = ET.parse(vtm_file_path) - root = tree.find("vtkMultiBlockDataSet") - if root is None: - flask.abort(500, "Failed to read viewable file") - uuid_to_flat_index = {} - current_index = 0 - assert root is not None - for elem in root.iter(): - if "uuid" in elem.attrib and elem.tag == "DataSet": - uuid_to_flat_index[elem.attrib["uuid"]] = current_index - current_index += 1 + uuid_to_flat_index: dict[str, int] = {} + if viewable_file: + vtm_file_path = geode_functions.data_file_path(data_id, viewable_file) + tree = ET.parse(vtm_file_path) + root = tree.find("vtkMultiBlockDataSet") + if root is None: + flask.abort(500, "Failed to read viewable file") + current_index = 0 + assert root is not None + for elem in root.iter(): + if "uuid" in elem.attrib and elem.tag == "DataSet": + uuid_to_flat_index[elem.attrib["uuid"]] = current_index + current_index += 1 + model_mesh_components = model.mesh_components() mesh_components = [] for mesh_component, ids in model_mesh_components.items(): @@ -270,44 +272,59 @@ def save_all_viewables_and_return_info( data_path: str, ) -> dict[str, Any]: with ThreadPoolExecutor() as executor: - native_files, viewable_path, light_path = executor.map( - lambda args: args[0](args[1]), - [ - ( - geode_object.save, - os.path.join( - data_path, "native." + geode_object.native_extension() + tasks: list[tuple[Callable[[str], Any], str]] = [ + ( + geode_object.save, + os.path.join(data_path, "native." + geode_object.native_extension()), + ) + ] + if geode_object.is_viewable(): + tasks.extend( + [ + (geode_object.save_viewable, os.path.join(data_path, "viewable")), + ( + geode_object.save_light_viewable, + os.path.join(data_path, "light_viewable"), ), - ), - (geode_object.save_viewable, os.path.join(data_path, "viewable")), - ( - geode_object.save_light_viewable, - os.path.join(data_path, "light_viewable"), - ), - ], - ) - with open(light_path, "rb") as f: - binary_light_viewable = f.read() + ] + ) + + results = list(executor.map(lambda args: args[0](args[1]), tasks)) + native_files = results[0] + if geode_object.is_viewable(): + viewable_path = results[1] + light_path = results[2] + with open(light_path, "rb") as f: + binary_light_viewable = f.read() + binary_light_viewable_str = binary_light_viewable.decode("utf-8") + data.viewable_file = os.path.basename(viewable_path) + data.light_viewable_file = os.path.basename(light_path) + else: + binary_light_viewable_str = None + data.viewable_file = None + data.light_viewable_file = None + data.native_file = os.path.basename(native_files[0]) - data.viewable_file = os.path.basename(viewable_path) - data.light_viewable_file = os.path.basename(light_path) assert data.native_file is not None - assert data.viewable_file is not None - assert data.light_viewable_file is not None + if geode_object.is_viewable(): + assert data.viewable_file is not None + assert data.light_viewable_file is not None name = geode_object.identifier.name() if not name: flask.abort(400, "Geode object has no name defined.") response: dict[str, Any] = { "native_file": data.native_file, - "viewable_file": data.viewable_file, "id": data.id, "name": name, "viewer_type": data.viewer_object, - "binary_light_viewable": binary_light_viewable.decode("utf-8"), + "is_viewable": geode_object.is_viewable(), "geode_object_type": data.geode_object, } + if geode_object.is_viewable(): + response["viewable_file"] = data.viewable_file + response["binary_light_viewable"] = binary_light_viewable_str if isinstance(geode_object, GeodeVertexSet): response["nb_vertices"] = geode_object.vertex_set.nb_vertices() if isinstance(geode_object, GeodeModel): @@ -315,7 +332,7 @@ def save_all_viewables_and_return_info( return response -def generate_native_viewable_and_light_viewable_from_object( +def generate_files_from_object( geode_object: GeodeObject, ) -> dict[str, Any]: data = Data.create( @@ -327,7 +344,7 @@ def generate_native_viewable_and_light_viewable_from_object( return save_all_viewables_and_return_info(geode_object, data, data_path) -def generate_native_viewable_and_light_viewable_from_file( +def generate_files_from_file( geode_object_type: GeodeObjectType, input_file: str ) -> dict[str, Any]: generic_geode_object = geode_objects[geode_object_type] diff --git a/tests/data/test.og_hst3d b/tests/data/test.og_hst3d new file mode 100644 index 00000000..f007de2e Binary files /dev/null and b/tests/data/test.og_hst3d differ diff --git a/tests/test_utils_functions.py b/tests/test_utils_functions.py index c041c4a8..fff7ff85 100644 --- a/tests/test_utils_functions.py +++ b/tests/test_utils_functions.py @@ -162,18 +162,14 @@ def test_save_all_viewables_commits_to_db(client: FlaskClient) -> None: assert db_entry_before.native_file == result["native_file"] -def test_generate_native_viewable_and_light_viewable_from_object( +def test_generate_files_from_object( client: FlaskClient, ) -> None: app = client.application with app.app_context(): geode_object = GeodeBRep.load(os.path.join(data_dir, "test.og_brep")) - result = ( - utils_functions.generate_native_viewable_and_light_viewable_from_object( - geode_object - ) - ) + result = utils_functions.generate_files_from_object(geode_object) assert isinstance(result, dict) assert isinstance(result["native_file"], str) @@ -197,12 +193,12 @@ def test_generate_native_viewable_and_light_viewable_from_object( assert os.path.exists(os.path.join(data_path, data.light_viewable_file)) -def test_generate_native_viewable_and_light_viewable_from_file( +def test_generate_files_from_file( client: FlaskClient, ) -> None: app = client.application with app.app_context(): - result = utils_functions.generate_native_viewable_and_light_viewable_from_file( + result = utils_functions.generate_files_from_file( GeodeBRep.geode_object_type(), "test.og_brep" ) @@ -218,12 +214,12 @@ def test_generate_native_viewable_and_light_viewable_from_file( assert isinstance(result["binary_light_viewable"], str) -def test_generate_native_viewable_and_light_viewable_from_file_with_multi_dots( +def test_generate_files_from_file_with_multi_dots( client: FlaskClient, ) -> None: app = client.application with app.app_context(): - result = utils_functions.generate_native_viewable_and_light_viewable_from_file( + result = utils_functions.generate_files_from_file( GeodeBRep.geode_object_type(), "cube.test.og_brep" ) assert result["name"] == "cube.test"