Skip to content

Commit 24a9c11

Browse files
committed
feat(create): New feature : create Volume of Interest
1 parent 2bc4304 commit 24a9c11

File tree

5 files changed

+223
-1
lines changed

5 files changed

+223
-1
lines changed

src/opengeodeweb_back/routes/create/blueprint_create.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def create_aoi() -> flask.Response:
5454

5555
# Create vertices first
5656
for point in params.points:
57-
pp = opengeode.Point3D([point.x, point.y, params.z])
57+
# pp = opengeode.Point3D([point.x, point.y, params.z])
5858
builder.create_point(opengeode.Point3D([point.x, point.y, params.z]))
5959

6060
# Create edges between consecutive vertices and close the loop
@@ -69,3 +69,69 @@ def create_aoi() -> flask.Response:
6969
data=edged_curve,
7070
)
7171
return flask.make_response(result, 200)
72+
73+
74+
@routes.route(
75+
schemas_dict["create_voi"]["route"], methods=schemas_dict["create_voi"]["methods"]
76+
)
77+
def create_voi() -> flask.Response:
78+
"""Endpoint to create a Volume of Interest (VOI) as an EdgedCurve3D (a bounding box)."""
79+
print(f"create_voi : {flask.request=}", flush=True)
80+
utils_functions.validate_request(flask.request, schemas_dict["create_voi"])
81+
params = schemas.CreateVoi.from_dict(flask.request.get_json())
82+
83+
# 1. Simuler la récupération des coordonnées (X, Y) de l'AOI
84+
# ATTENTION : En l'absence de `utils_functions.get_data`, nous utilisons des
85+
# points de simulation pour construire la VOI. L'ID de l'AOI (params.aoi_id) est ignoré.
86+
aoi_vertices = [
87+
(0.0, 0.0),
88+
(10.0, 0.0),
89+
(10.0, 10.0),
90+
(0.0, 10.0),
91+
]
92+
93+
# 2. Créer le VOI (EdgedCurve3D)
94+
edged_curve = geode_functions.geode_object_class("EdgedCurve3D").create()
95+
builder = geode_functions.create_builder("EdgedCurve3D", edged_curve)
96+
builder.set_name(params.name)
97+
98+
z_min = params.z_min
99+
z_max = params.z_max
100+
101+
# 3. Créer les 8 vertices de la boîte (VOI)
102+
# Indices 0-3 (face inférieure Z_min), Indices 4-7 (face supérieure Z_max)
103+
104+
# Bottom face (Z_min) indices 0, 1, 2, 3
105+
for x, y in aoi_vertices:
106+
builder.create_point(opengeode.Point3D([x, y, z_min]))
107+
108+
# Top face (Z_max) indices 4, 5, 6, 7
109+
for x, y in aoi_vertices:
110+
builder.create_point(opengeode.Point3D([x, y, z_max]))
111+
112+
# 4. Créer les 12 arêtes
113+
114+
# Arêtes de la face inférieure: 0-1, 1-2, 2-3, 3-0
115+
bottom_edges = [(i, (i + 1) % 4) for i in range(4)]
116+
117+
# Arêtes de la face supérieure: 4-5, 5-6, 6-7, 7-4
118+
top_edges = [(i + 4, (i + 1) % 4 + 4) for i in range(4)]
119+
120+
# Arêtes verticales: 0-4, 1-5, 2-6, 3-7
121+
vertical_edges = [(i, i + 4) for i in range(4)]
122+
123+
all_edges = bottom_edges + top_edges + vertical_edges
124+
125+
for v1, v2 in all_edges:
126+
builder.create_edge_with_vertices(v1, v2)
127+
128+
# 5. Sauvegarder et obtenir les informations
129+
# Utilise generate_native_viewable... car update_native_viewable... n'est pas disponible.
130+
# L'ID optionnel (params.id) est donc ignoré, et une NOUVELLE entrée Data est créée.
131+
result = utils_functions.generate_native_viewable_and_light_viewable_from_object(
132+
geode_object="EdgedCurve3D",
133+
data=edged_curve,
134+
)
135+
136+
# Retourne l'ID de la nouvelle entrée Data créée
137+
return flask.make_response(result, 200)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from .create_point import *
22
from .create_aoi import *
3+
from .create_voi import *
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"route": "/create_voi",
3+
"methods": [
4+
"POST"
5+
],
6+
"type": "object",
7+
"properties": {
8+
"name": {
9+
"type": "string",
10+
"description": "Name of the VOI"
11+
},
12+
"aoi_id": {
13+
"type": "string",
14+
"description": "ID of the corresponding AOI from which to take X and Y coordinates"
15+
},
16+
"z_min": {
17+
"type": "number",
18+
"description": "Minimum Z coordinate for the VOI"
19+
},
20+
"z_max": {
21+
"type": "number",
22+
"description": "Maximum Z coordinate for the VOI"
23+
},
24+
"id": {
25+
"type": "string"
26+
}
27+
},
28+
"required": [
29+
"name",
30+
"aoi_id",
31+
"z_min",
32+
"z_max"
33+
],
34+
"additionalProperties": false
35+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from dataclasses_json import DataClassJsonMixin
2+
from dataclasses import dataclass
3+
from typing import Optional
4+
5+
6+
@dataclass
7+
class CreateVoi(DataClassJsonMixin):
8+
name: str
9+
"""Name of the VOI"""
10+
11+
aoi_id: str
12+
"""ID of the corresponding AOI from which to take X and Y coordinates"""
13+
14+
z_min: float
15+
"""Minimum Z coordinate for the VOI"""
16+
17+
z_max: float
18+
"""Maximum Z coordinate for the VOI"""
19+
20+
id: Optional[str] = None

tests/test_create_routes.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ def aoi_data() -> Dict[str, Any]:
2929
"z": 0.0,
3030
}
3131

32+
@pytest.fixture
33+
def voi_data() -> Dict[str, Any]:
34+
"""Fixture for Volume of Interest (VOI) test data."""
35+
return {
36+
"name": "test_voi",
37+
"aoi_id": str(uuid.uuid4()),
38+
"z_min": -50.0,
39+
"z_max": 100.0,
40+
"id": str(uuid.uuid4()),
41+
}
42+
3243

3344
def test_create_point(client: FlaskClient, point_data: Dict[str, Any]) -> None:
3445
"""Test the creation of a point with valid data."""
@@ -80,6 +91,33 @@ def test_create_aoi(client: FlaskClient, aoi_data: Dict[str, Any]) -> None:
8091
test_utils.test_route_wrong_params(client, route, lambda: aoi_data.copy()) # type: ignore
8192

8293

94+
def test_create_voi(client: FlaskClient, voi_data: Dict[str, Any]) -> None:
95+
"""Test the creation of a VOI with valid data (including optional id)."""
96+
route: str = "/opengeodeweb_back/create/create_voi"
97+
98+
response = client.post(route, json=voi_data)
99+
assert response.status_code == 200
100+
101+
response_data: Any = response.json
102+
assert "id" in response_data
103+
assert "name" in response_data
104+
assert response_data["name"] == voi_data["name"]
105+
assert response_data["object_type"] == "mesh"
106+
assert response_data["geode_object"] == "EdgedCurve3D"
107+
108+
voi_data_no_id = voi_data.copy()
109+
del voi_data_no_id["id"]
110+
response = client.post(route, json=voi_data_no_id)
111+
assert response.status_code == 200
112+
assert response.json["name"] == voi_data["name"]
113+
114+
voi_data_required_only = voi_data.copy()
115+
del voi_data_required_only["id"]
116+
117+
test_utils.test_route_wrong_params(client, route, lambda: voi_data_required_only.copy()) # type: ignore
118+
119+
120+
83121
def test_create_point_with_invalid_data(client: FlaskClient) -> None:
84122
"""Test the point creation endpoint with invalid data."""
85123
route: str = "/opengeodeweb_back/create/create_point"
@@ -130,6 +168,28 @@ def test_create_aoi_with_invalid_data(
130168
assert response.status_code == 400
131169

132170

171+
def test_create_voi_with_invalid_data(
172+
client: FlaskClient, voi_data: Dict[str, Any]
173+
) -> None:
174+
"""Test the VOI creation endpoint with invalid data."""
175+
route: str = "/opengeodeweb_back/create/create_voi"
176+
177+
# Test with non-numeric z_min
178+
invalid_data: Dict[str, Any] = {**voi_data, "z_min": "not_a_number"}
179+
response = client.post(route, json=invalid_data)
180+
assert response.status_code == 400
181+
182+
# Test with non-numeric z_max
183+
invalid_data = {**voi_data, "z_max": "not_a_number"}
184+
response = client.post(route, json=invalid_data)
185+
assert response.status_code == 400
186+
187+
# Test with invalid aoi_id format (e.g., not a string/uuid)
188+
invalid_data = {**voi_data, "aoi_id": 12345}
189+
response = client.post(route, json=invalid_data)
190+
assert response.status_code == 400
191+
192+
133193
def test_create_point_file_generation(
134194
client: FlaskClient, point_data: Dict[str, Any]
135195
) -> None:
@@ -208,3 +268,43 @@ def test_create_aoi_file_generation(
208268
# Verify file extensions
209269
assert response_data["native_file_name"].endswith(".og_edc3d")
210270
assert response_data["viewable_file_name"].endswith(".vtp")
271+
272+
273+
def test_create_voi_file_generation(
274+
client: FlaskClient, voi_data: Dict[str, Any]
275+
) -> None:
276+
"""Test that the VOI creation generates the correct files."""
277+
route: str = "/opengeodeweb_back/create/create_voi"
278+
279+
# Make the request
280+
response = client.post(route, json=voi_data)
281+
assert response.status_code == 200
282+
response_data: Any = response.json
283+
284+
# Get the data folder path for this specific ID
285+
DATA_FOLDER_PATH: str = client.application.config["DATA_FOLDER_PATH"]
286+
data_id: str = response_data["id"]
287+
data_folder: str = os.path.join(DATA_FOLDER_PATH, data_id)
288+
289+
# Check that the data folder exists
290+
assert os.path.exists(data_folder)
291+
assert os.path.isdir(data_folder)
292+
293+
# Check native file exists
294+
native_file_path: str = os.path.join(data_folder, response_data["native_file_name"])
295+
assert os.path.exists(native_file_path)
296+
297+
# Check viewable file exists
298+
viewable_file_path: str = os.path.join(
299+
data_folder, response_data["viewable_file_name"]
300+
)
301+
assert os.path.exists(viewable_file_path)
302+
303+
# Check light viewable file exists if present
304+
if "binary_light_viewable" in response_data:
305+
light_viewable_file_path: str = os.path.join(data_folder, "light_viewable.vtp")
306+
assert os.path.exists(light_viewable_file_path)
307+
308+
# Verify file extensions (VOI uses EdgedCurve3D like AOI)
309+
assert response_data["native_file_name"].endswith(".og_edc3d")
310+
assert response_data["viewable_file_name"].endswith(".vtp")

0 commit comments

Comments
 (0)