diff --git a/opengeodeweb_back_schemas.json b/opengeodeweb_back_schemas.json index ee3d0db..52453fb 100644 --- a/opengeodeweb_back_schemas.json +++ b/opengeodeweb_back_schemas.json @@ -1,6 +1,43 @@ { "opengeodeweb_back": { "create": { + "create_voi": { + "$id": "opengeodeweb_back/create/create_voi", + "route": "/create_voi", + "methods": [ + "POST" + ], + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the VOI" + }, + "aoi_id": { + "type": "string", + "description": "ID of the corresponding AOI" + }, + "z_min": { + "type": "number", + "description": "Minimum Z coordinate" + }, + "z_max": { + "type": "number", + "description": "Maximum Z coordinate" + }, + "id": { + "type": "string", + "description": "Optional ID for updating existing VOI" + } + }, + "required": [ + "name", + "aoi_id", + "z_min", + "z_max" + ], + "additionalProperties": false + }, "create_point": { "$id": "opengeodeweb_back/create/create_point", "route": "/create_point", @@ -61,8 +98,7 @@ ], "additionalProperties": false }, - "minItems": 4, - "maxItems": 4 + "minItems": 3 }, "z": { "type": "number" diff --git a/requirements.txt b/requirements.txt index f936c05..4e85944 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,4 +60,3 @@ werkzeug==3.1.2 # flask # flask-cors -opengeodeweb-microservice==1.*,>=1.0.7rc1 diff --git a/schemas.json b/schemas.json deleted file mode 100644 index 01bba35..0000000 --- a/schemas.json +++ /dev/null @@ -1,348 +0,0 @@ -{ - "opengeodeweb_back": { - "models": { - "vtm_component_indices": { - "$id": "opengeodeweb_back/models/vtm_component_indices", - "route": "/vtm_component_indices", - "methods": [ - "POST" - ], - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": [ - "id" - ], - "additionalProperties": false - }, - "mesh_components": { - "$id": "opengeodeweb_back/models/mesh_components", - "route": "/mesh_components", - "methods": [ - "POST" - ], - "type": "object", - "properties": { - "filename": { - "type": "string" - }, - "geode_object": { - "type": "string" - } - }, - "required": [ - "filename", - "geode_object" - ], - "additionalProperties": false - } - }, - "vertex_attribute_names": { - "$id": "opengeodeweb_back/vertex_attribute_names", - "route": "/vertex_attribute_names", - "methods": [ - "POST" - ], - "type": "object", - "properties": { - "input_geode_object": { - "type": "string", - "minLength": 1 - }, - "filename": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "input_geode_object", - "filename" - ], - "additionalProperties": false - }, - "upload_file": { - "$id": "opengeodeweb_back/upload_file", - "route": "/upload_file", - "methods": [ - "OPTIONS", - "PUT" - ], - "type": "object", - "properties": { - "filename": { - "type": "string", - "minLength": 1 - } - }, - "additionalProperties": false - }, - "texture_coordinates": { - "$id": "opengeodeweb_back/texture_coordinates", - "route": "/texture_coordinates", - "methods": [ - "POST" - ], - "type": "object", - "properties": { - "input_geode_object": { - "type": "string", - "minLength": 1 - }, - "filename": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "input_geode_object", - "filename" - ], - "additionalProperties": false - }, - "save_viewable_file": { - "$id": "opengeodeweb_back/save_viewable_file", - "route": "/save_viewable_file", - "methods": [ - "POST" - ], - "type": "object", - "properties": { - "input_geode_object": { - "type": "string", - "minLength": 1 - }, - "filename": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "input_geode_object", - "filename" - ], - "additionalProperties": false - }, - "polyhedron_attribute_names": { - "$id": "opengeodeweb_back/polyhedron_attribute_names", - "route": "/polyhedron_attribute_names", - "methods": [ - "POST" - ], - "type": "object", - "properties": { - "input_geode_object": { - "type": "string", - "minLength": 1 - }, - "filename": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "input_geode_object", - "filename" - ], - "additionalProperties": false - }, - "polygon_attribute_names": { - "$id": "opengeodeweb_back/polygon_attribute_names", - "route": "/polygon_attribute_names", - "methods": [ - "POST" - ], - "type": "object", - "properties": { - "input_geode_object": { - "type": "string", - "minLength": 1 - }, - "filename": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "input_geode_object", - "filename" - ], - "additionalProperties": false - }, - "ping": { - "$id": "opengeodeweb_back/ping", - "route": "/ping", - "methods": [ - "POST" - ], - "type": "object", - "properties": {}, - "required": [], - "additionalProperties": false - }, - "missing_files": { - "$id": "opengeodeweb_back/missing_files", - "route": "/missing_files", - "methods": [ - "POST" - ], - "type": "object", - "properties": { - "input_geode_object": { - "type": "string", - "minLength": 1 - }, - "filename": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "input_geode_object", - "filename" - ], - "additionalProperties": false - }, - "inspect_file": { - "$id": "opengeodeweb_back/inspect_file", - "route": "/inspect_file", - "methods": [ - "POST" - ], - "type": "object", - "properties": { - "filename": { - "type": "string", - "minLength": 1 - }, - "input_geode_object": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "filename", - "input_geode_object" - ], - "additionalProperties": false - }, - "geographic_coordinate_systems": { - "$id": "opengeodeweb_back/geographic_coordinate_systems", - "route": "/geographic_coordinate_systems", - "methods": [ - "POST" - ], - "type": "object", - "properties": { - "input_geode_object": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "input_geode_object" - ], - "additionalProperties": false - }, - "geode_objects_and_output_extensions": { - "$id": "opengeodeweb_back/geode_objects_and_output_extensions", - "route": "/geode_objects_and_output_extensions", - "methods": [ - "POST" - ], - "type": "object", - "properties": { - "input_geode_object": { - "type": "string", - "minLength": 1 - }, - "filename": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "input_geode_object", - "filename" - ], - "additionalProperties": false - }, - "create_point": { - "$id": "opengeodeweb_back/create_point", - "route": "/create_point", - "methods": [ - "POST" - ], - "type": "object", - "properties": { - "title": { - "type": "string", - "minLength": 1 - }, - "x": { - "type": "number" - }, - "y": { - "type": "number" - }, - "z": { - "type": "number" - } - }, - "required": [ - "title", - "x", - "y", - "z" - ], - "additionalProperties": false - }, - "allowed_objects": { - "$id": "opengeodeweb_back/allowed_objects", - "route": "/allowed_objects", - "methods": [ - "POST" - ], - "type": "object", - "properties": { - "filename": { - "type": "string", - "minLength": 1 - }, - "supported_feature": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "filename", - "supported_feature" - ], - "additionalProperties": false - }, - "allowed_files": { - "$id": "opengeodeweb_back/allowed_files", - "route": "/allowed_files", - "methods": [ - "POST" - ], - "type": "object", - "properties": { - "supported_feature": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "supported_feature" - ], - "additionalProperties": false - } - } -} \ No newline at end of file diff --git a/src/opengeodeweb_back/routes/create/blueprint_create.py b/src/opengeodeweb_back/routes/create/blueprint_create.py index 2398833..472038a 100644 --- a/src/opengeodeweb_back/routes/create/blueprint_create.py +++ b/src/opengeodeweb_back/routes/create/blueprint_create.py @@ -20,7 +20,6 @@ ) def create_point() -> flask.Response: """Endpoint to create a single point in 3D space.""" - print(f"create_point : {flask.request=}", flush=True) utils_functions.validate_request(flask.request, schemas_dict["create_point"]) params = schemas.CreatePoint.from_dict(flask.request.get_json()) @@ -43,7 +42,6 @@ def create_point() -> flask.Response: ) def create_aoi() -> flask.Response: """Endpoint to create an Area of Interest (AOI) as an EdgedCurve3D.""" - print(f"create_aoi : {flask.request=}", flush=True) utils_functions.validate_request(flask.request, schemas_dict["create_aoi"]) params = schemas.CreateAoi.from_dict(flask.request.get_json()) @@ -54,7 +52,6 @@ def create_aoi() -> flask.Response: # Create vertices first for point in params.points: - pp = opengeode.Point3D([point.x, point.y, params.z]) builder.create_point(opengeode.Point3D([point.x, point.y, params.z])) # Create edges between consecutive vertices and close the loop @@ -69,3 +66,49 @@ def create_aoi() -> flask.Response: data=edged_curve, ) return flask.make_response(result, 200) + + +@routes.route( + schemas_dict["create_voi"]["route"], + methods=schemas_dict["create_voi"]["methods"], +) +def create_voi() -> flask.Response: + """Endpoint to create a Volume of Interest (VOI) as an EdgedCurve3D (a bounding box/prism).""" + utils_functions.validate_request(flask.request, schemas_dict["create_voi"]) + params = schemas.CreateVoi.from_dict(flask.request.get_json()) + + aoi_data = geode_functions.get_data_info(params.aoi_id) + if not aoi_data: + flask.abort(404, f"AOI with id {params.aoi_id} not found") + + aoi_object = geode_functions.load_data(params.aoi_id) + + nb_points = aoi_object.nb_vertices() + + edged_curve = geode_functions.geode_object_class("EdgedCurve3D").create() + builder = geode_functions.create_builder("EdgedCurve3D", edged_curve) + builder.set_name(params.name) + + for point_id in range(nb_points): + aoi_point = aoi_object.point(point_id) + builder.create_point( + opengeode.Point3D([aoi_point.value(0), aoi_point.value(1), params.z_min]) + ) + + for point_id in range(nb_points): + aoi_point = aoi_object.point(point_id) + builder.create_point( + opengeode.Point3D([aoi_point.value(0), aoi_point.value(1), params.z_max]) + ) + + for point_id in range(nb_points): + next_point = (point_id + 1) % nb_points + builder.create_edge_with_vertices(point_id, next_point) + builder.create_edge_with_vertices(point_id + nb_points, next_point + nb_points) + builder.create_edge_with_vertices(point_id, point_id + nb_points) + + result = utils_functions.generate_native_viewable_and_light_viewable_from_object( + geode_object="EdgedCurve3D", + data=edged_curve, + ) + return flask.make_response(result, 200) diff --git a/src/opengeodeweb_back/routes/create/schemas/__init__.py b/src/opengeodeweb_back/routes/create/schemas/__init__.py index c9a26ab..3d3f9b7 100644 --- a/src/opengeodeweb_back/routes/create/schemas/__init__.py +++ b/src/opengeodeweb_back/routes/create/schemas/__init__.py @@ -1,2 +1,3 @@ +from .create_voi import * from .create_point import * from .create_aoi import * diff --git a/src/opengeodeweb_back/routes/create/schemas/create_aoi.json b/src/opengeodeweb_back/routes/create/schemas/create_aoi.json index 0dc7a3f..c932149 100644 --- a/src/opengeodeweb_back/routes/create/schemas/create_aoi.json +++ b/src/opengeodeweb_back/routes/create/schemas/create_aoi.json @@ -27,8 +27,7 @@ ], "additionalProperties": false }, - "minItems": 4, - "maxItems": 4 + "minItems": 3 }, "z": { "type": "number" diff --git a/src/opengeodeweb_back/routes/create/schemas/create_voi.json b/src/opengeodeweb_back/routes/create/schemas/create_voi.json new file mode 100644 index 0000000..1b3d630 --- /dev/null +++ b/src/opengeodeweb_back/routes/create/schemas/create_voi.json @@ -0,0 +1,36 @@ +{ + "route": "/create_voi", + "methods": [ + "POST" + ], + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the VOI" + }, + "aoi_id": { + "type": "string", + "description": "ID of the corresponding AOI" + }, + "z_min": { + "type": "number", + "description": "Minimum Z coordinate" + }, + "z_max": { + "type": "number", + "description": "Maximum Z coordinate" + }, + "id": { + "type": "string", + "description": "Optional ID for updating existing VOI" + } + }, + "required": [ + "name", + "aoi_id", + "z_min", + "z_max" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/src/opengeodeweb_back/routes/create/schemas/create_voi.py b/src/opengeodeweb_back/routes/create/schemas/create_voi.py new file mode 100644 index 0000000..f1a5b23 --- /dev/null +++ b/src/opengeodeweb_back/routes/create/schemas/create_voi.py @@ -0,0 +1,21 @@ +from dataclasses_json import DataClassJsonMixin +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class CreateVoi(DataClassJsonMixin): + aoi_id: str + """ID of the corresponding AOI""" + + name: str + """Name of the VOI""" + + z_max: float + """Maximum Z coordinate""" + + z_min: float + """Minimum Z coordinate""" + + id: Optional[str] = None + """Optional ID for updating existing VOI""" diff --git a/tests/test_create_routes.py b/tests/test_create_routes.py index 33aabf1..3e9eff7 100644 --- a/tests/test_create_routes.py +++ b/tests/test_create_routes.py @@ -30,6 +30,18 @@ def aoi_data() -> Dict[str, Any]: } +@pytest.fixture +def voi_data() -> Dict[str, Any]: + """Fixture for Volume of Interest (VOI) test data.""" + return { + "name": "test_voi", + "aoi_id": str(uuid.uuid4()), + "z_min": -50.0, + "z_max": 100.0, + "id": str(uuid.uuid4()), + } + + def test_create_point(client: FlaskClient, point_data: Dict[str, Any]) -> None: """Test the creation of a point with valid data.""" route: str = "/opengeodeweb_back/create/create_point" @@ -80,6 +92,32 @@ def test_create_aoi(client: FlaskClient, aoi_data: Dict[str, Any]) -> None: test_utils.test_route_wrong_params(client, route, lambda: aoi_data.copy()) # type: ignore +def test_create_voi(client: FlaskClient, voi_data: Dict[str, Any]) -> None: + """Test the creation of a VOI with valid data (including optional id).""" + route: str = "/opengeodeweb_back/create/create_voi" + + response = client.post(route, json=voi_data) + assert response.status_code == 200 + + response_data: Any = response.json + assert "id" in response_data + assert "name" in response_data + assert response_data["name"] == voi_data["name"] + assert response_data["object_type"] == "mesh" + assert response_data["geode_object"] == "EdgedCurve3D" + + voi_data_no_id = voi_data.copy() + del voi_data_no_id["id"] + response = client.post(route, json=voi_data_no_id) + assert response.status_code == 200 + assert response.json["name"] == voi_data["name"] + + voi_data_required_only = voi_data.copy() + del voi_data_required_only["id"] + + test_utils.test_route_wrong_params(client, route, lambda: voi_data_required_only.copy()) # type: ignore + + def test_create_point_with_invalid_data(client: FlaskClient) -> None: """Test the point creation endpoint with invalid data.""" route: str = "/opengeodeweb_back/create/create_point" @@ -130,6 +168,28 @@ def test_create_aoi_with_invalid_data( assert response.status_code == 400 +def test_create_voi_with_invalid_data( + client: FlaskClient, voi_data: Dict[str, Any] +) -> None: + """Test the VOI creation endpoint with invalid data.""" + route: str = "/opengeodeweb_back/create/create_voi" + + # Test with non-numeric z_min + invalid_data: Dict[str, Any] = {**voi_data, "z_min": "not_a_number"} + response = client.post(route, json=invalid_data) + assert response.status_code == 400 + + # Test with non-numeric z_max + invalid_data = {**voi_data, "z_max": "not_a_number"} + response = client.post(route, json=invalid_data) + assert response.status_code == 400 + + # Test with invalid aoi_id format (e.g., not a string/uuid) + invalid_data = {**voi_data, "aoi_id": 12345} + response = client.post(route, json=invalid_data) + assert response.status_code == 400 + + def test_create_point_file_generation( client: FlaskClient, point_data: Dict[str, Any] ) -> None: @@ -208,3 +268,43 @@ def test_create_aoi_file_generation( # Verify file extensions assert response_data["native_file_name"].endswith(".og_edc3d") assert response_data["viewable_file_name"].endswith(".vtp") + + +def test_create_voi_file_generation( + client: FlaskClient, voi_data: Dict[str, Any] +) -> None: + """Test that the VOI creation generates the correct files.""" + route: str = "/opengeodeweb_back/create/create_voi" + + # Make the request + response = client.post(route, json=voi_data) + assert response.status_code == 200 + response_data: Any = response.json + + # Get the data folder path for this specific ID + DATA_FOLDER_PATH: str = client.application.config["DATA_FOLDER_PATH"] + data_id: str = response_data["id"] + data_folder: str = os.path.join(DATA_FOLDER_PATH, data_id) + + # Check that the data folder exists + assert os.path.exists(data_folder) + assert os.path.isdir(data_folder) + + # Check native file exists + native_file_path: str = os.path.join(data_folder, response_data["native_file_name"]) + assert os.path.exists(native_file_path) + + # Check viewable file exists + viewable_file_path: str = os.path.join( + data_folder, response_data["viewable_file_name"] + ) + assert os.path.exists(viewable_file_path) + + # Check light viewable file exists if present + if "binary_light_viewable" in response_data: + light_viewable_file_path: str = os.path.join(data_folder, "light_viewable.vtp") + assert os.path.exists(light_viewable_file_path) + + # Verify file extensions (VOI uses EdgedCurve3D like AOI) + assert response_data["native_file_name"].endswith(".og_edc3d") + assert response_data["viewable_file_name"].endswith(".vtp")