diff --git a/opengeodeweb_back_schemas.json b/opengeodeweb_back_schemas.json index 62d0cfda..d4492cd0 100644 --- a/opengeodeweb_back_schemas.json +++ b/opengeodeweb_back_schemas.json @@ -627,6 +627,12 @@ }, "no_data": { "type": "boolean" + }, + "time_steps": { + "type": "array", + "items": { + "type": "number" + } } }, "required": [ @@ -637,7 +643,8 @@ "max_value", "min_values", "max_values", - "no_data" + "no_data", + "time_steps" ], "additionalProperties": false } @@ -969,6 +976,12 @@ }, "no_data": { "type": "boolean" + }, + "time_steps": { + "type": "array", + "items": { + "type": "number" + } } }, "required": [ @@ -979,7 +992,8 @@ "max_value", "min_values", "max_values", - "no_data" + "no_data", + "time_steps" ], "additionalProperties": false } @@ -1046,6 +1060,12 @@ }, "no_data": { "type": "boolean" + }, + "time_steps": { + "type": "array", + "items": { + "type": "number" + } } }, "required": [ @@ -1056,7 +1076,8 @@ "max_value", "min_values", "max_values", - "no_data" + "no_data", + "time_steps" ], "additionalProperties": false } @@ -1156,6 +1177,12 @@ }, "no_data": { "type": "boolean" + }, + "time_steps": { + "type": "array", + "items": { + "type": "number" + } } }, "required": [ @@ -1166,7 +1193,8 @@ "max_value", "min_values", "max_values", - "no_data" + "no_data", + "time_steps" ], "additionalProperties": false } @@ -1243,6 +1271,12 @@ }, "no_data": { "type": "boolean" + }, + "time_steps": { + "type": "array", + "items": { + "type": "number" + } } }, "required": [ @@ -1253,7 +1287,8 @@ "max_value", "min_values", "max_values", - "no_data" + "no_data", + "time_steps" ], "additionalProperties": false } @@ -1330,6 +1365,12 @@ }, "no_data": { "type": "boolean" + }, + "time_steps": { + "type": "array", + "items": { + "type": "number" + } } }, "required": [ @@ -1340,7 +1381,8 @@ "max_value", "min_values", "max_values", - "no_data" + "no_data", + "time_steps" ], "additionalProperties": false } @@ -1417,6 +1459,12 @@ }, "no_data": { "type": "boolean" + }, + "time_steps": { + "type": "array", + "items": { + "type": "number" + } } }, "required": [ @@ -1427,7 +1475,8 @@ "max_value", "min_values", "max_values", - "no_data" + "no_data", + "time_steps" ], "additionalProperties": false } @@ -1745,6 +1794,12 @@ }, "no_data": { "type": "boolean" + }, + "time_steps": { + "type": "array", + "items": { + "type": "number" + } } }, "required": [ @@ -1755,7 +1810,8 @@ "max_value", "min_values", "max_values", - "no_data" + "no_data", + "time_steps" ], "additionalProperties": false } @@ -1822,6 +1878,12 @@ }, "no_data": { "type": "boolean" + }, + "time_steps": { + "type": "array", + "items": { + "type": "number" + } } }, "required": [ @@ -1832,7 +1894,8 @@ "max_value", "min_values", "max_values", - "no_data" + "no_data", + "time_steps" ], "additionalProperties": false } diff --git a/opengeodeweb_back_typed_schemas.d.ts b/opengeodeweb_back_typed_schemas.d.ts index 998c8a15..80526eb8 100644 --- a/opengeodeweb_back_typed_schemas.d.ts +++ b/opengeodeweb_back_typed_schemas.d.ts @@ -152,6 +152,7 @@ export interface VertexAttributeNamesResponseAttribute { min_values: number[]; nb_items: number; no_data: boolean; + time_steps: number[]; } export interface ValidateParams { @@ -235,6 +236,7 @@ export interface PolyhedronAttributeNamesResponseAttribute { min_values: number[]; nb_items: number; no_data: boolean; + time_steps: number[]; } export interface PolygonAttributeNamesParams { @@ -254,6 +256,7 @@ export interface PolygonAttributeNamesResponseAttribute { min_values: number[]; nb_items: number; no_data: boolean; + time_steps: number[]; } export interface PingParams { @@ -281,6 +284,7 @@ export interface ModelComponentVertexAttributeNamesResponseAttribute { min_values: number[]; nb_items: number; no_data: boolean; + time_steps: number[]; } export interface ModelComponentPolyhedronAttributeNamesParams { @@ -301,6 +305,7 @@ export interface ModelComponentPolyhedronAttributeNamesResponseAttribute { min_values: number[]; nb_items: number; no_data: boolean; + time_steps: number[]; } export interface ModelComponentPolygonAttributeNamesParams { @@ -321,6 +326,7 @@ export interface ModelComponentPolygonAttributeNamesResponseAttribute { min_values: number[]; nb_items: number; no_data: boolean; + time_steps: number[]; } export interface ModelComponentEdgeAttributeNamesParams { @@ -341,6 +347,7 @@ export interface ModelComponentEdgeAttributeNamesResponseAttribute { min_values: number[]; nb_items: number; no_data: boolean; + time_steps: number[]; } export interface MissingFilesParams { @@ -422,6 +429,7 @@ export interface EdgeAttributeNamesResponseAttribute { min_values: number[]; nb_items: number; no_data: boolean; + time_steps: number[]; } export interface CellAttributeNamesParams { @@ -441,6 +449,7 @@ export interface CellAttributeNamesResponseAttribute { min_values: number[]; nb_items: number; no_data: boolean; + time_steps: number[]; } export interface AllowedObjectsParams { diff --git a/requirements.txt b/requirements.txt index 4be9286b..419f6e74 100644 --- a/requirements.txt +++ b/requirements.txt @@ -70,4 +70,3 @@ werkzeug==3.1.8 # flask # flask-cors -opengeodeweb-microservice==1.*,>=1.3.2rc1 diff --git a/src/opengeodeweb_back/routes/blueprint_routes.py b/src/opengeodeweb_back/routes/blueprint_routes.py index 29a24dae..8c4f5afe 100644 --- a/src/opengeodeweb_back/routes/blueprint_routes.py +++ b/src/opengeodeweb_back/routes/blueprint_routes.py @@ -246,15 +246,24 @@ def texture_coordinates( return schemas.TextureCoordinatesResponse(texture_coordinates=texture_coordinates) -def extract_valid_attribute_values( +def _attribute_ids_for_name( + attribute_manager: og.AttributeManager, attribute_name: str +) -> list[og.uuid]: + time_steps = attribute_manager.time_steps(attribute_name) + if time_steps: + return [time_step.attribute_id for time_step in time_steps] + attribute_ids = attribute_manager.attribute_ids_matching_name(attribute_name) + if not isinstance(attribute_ids, list) or not attribute_ids: + return [] + return attribute_ids[:1] + + +def _extract_valid_values_from_attribute( attribute_manager: og.AttributeManager, - attribute_name: str, + attribute_id: og.uuid, item_index: int, ) -> tuple[list[float], bool]: - attribute_ids = attribute_manager.attribute_ids_matching_name(attribute_name) - if not isinstance(attribute_ids, list): - return [], False - attribute = attribute_manager.find_generic_attribute(attribute_ids[0]) + attribute = attribute_manager.find_generic_attribute(attribute_id) if ( attribute is None or not attribute.is_genericable() @@ -287,48 +296,98 @@ def extract_valid_attribute_values( return valid_values, has_nan +def extract_valid_attribute_values( + attribute_manager: og.AttributeManager, + attribute_name: str, + item_index: int, +) -> tuple[list[float], bool]: + valid_values: list[float] = [] + has_nan = False + for attribute_id in _attribute_ids_for_name(attribute_manager, attribute_name): + step_values, step_has_nan = _extract_valid_values_from_attribute( + attribute_manager, attribute_id, item_index + ) + valid_values.extend(step_values) + has_nan = has_nan or step_has_nan + return valid_values, has_nan + + +def _common_time_steps( + attribute_managers: list[og.AttributeManager], attribute_name: str +) -> list[float] | None: + times = [time_step.time for time_step in attribute_managers[0].time_steps(attribute_name)] + for attribute_manager in attribute_managers[1:]: + other_times = [time_step.time for time_step in attribute_manager.time_steps(attribute_name)] + if other_times != times: + return None + return times + + +def _attribute_ranges( + attribute_managers: list[og.AttributeManager], + attribute_name: str, + nb_items: int, +) -> tuple[list[float], list[float], bool]: + min_values: list[float] = [] + max_values: list[float] = [] + attribute_has_nan = False + for item_index in range(nb_items): + valid_values: list[float] = [] + for attribute_manager in attribute_managers: + extracted_values, has_nan = extract_valid_attribute_values( + attribute_manager, attribute_name, item_index + ) + valid_values.extend(extracted_values) + attribute_has_nan = attribute_has_nan or has_nan + if valid_values: + min_values.append(min(valid_values)) + max_values.append(max(valid_values)) + return min_values, max_values, attribute_has_nan + + def attributes_metadata( manager: og.AttributeManager | list[og.AttributeManager], ) -> list[dict[str, str | int | float | bool | list[float]]]: attribute_managers = manager if isinstance(manager, list) else [manager] attributes: list[dict[str, str | int | float | bool | list[float]]] = [] first_manager = attribute_managers[0] + listed_time_series: set[str] = set() for attribute_id in first_manager.attribute_ids(): attribute = first_manager.find_generic_attribute(attribute_id) if attribute is None: continue attribute_name = attribute.name() - if attribute_name is None: + if attribute_name is None or attribute_name in listed_time_series: continue if not attribute.is_genericable() or not attribute.properties().transferable: continue + time_steps = _common_time_steps(attribute_managers, attribute_name) + if time_steps is None: + continue + if time_steps: + listed_time_series.add(attribute_name) nb_items = attribute.nb_items() - min_values, max_values = [], [] - attribute_has_nan = False - for item_index in range(nb_items): - valid_values: list[float] = [] - for attribute_manager in attribute_managers: - extracted_values, has_nan = extract_valid_attribute_values( - attribute_manager, attribute_name, item_index - ) - valid_values.extend(extracted_values) - if has_nan: - attribute_has_nan = True - if valid_values: - min_values.append(min(valid_values)) - max_values.append(max(valid_values)) + min_values, max_values, attribute_has_nan = _attribute_ranges( + attribute_managers, attribute_name, nb_items + ) if not min_values or not max_values: continue + series_id = ( + first_manager.time_steps(attribute_name)[0].attribute_id.string() + if time_steps + else attribute_id.string() + ) attributes.append( { "attribute_name": attribute_name, - "attribute_id": attribute_id.string(), + "attribute_id": series_id, "nb_items": nb_items, "min_value": min(min_values), "max_value": max(max_values), "min_values": min_values, "max_values": max_values, "no_data": attribute_has_nan, + "time_steps": time_steps, } ) return attributes diff --git a/src/opengeodeweb_back/routes/schemas/cell_attribute_names.json b/src/opengeodeweb_back/routes/schemas/cell_attribute_names.json index 455c9b6f..3ad8852c 100644 --- a/src/opengeodeweb_back/routes/schemas/cell_attribute_names.json +++ b/src/opengeodeweb_back/routes/schemas/cell_attribute_names.json @@ -52,6 +52,12 @@ }, "no_data": { "type": "boolean" + }, + "time_steps": { + "type": "array", + "items": { + "type": "number" + } } }, "required": [ @@ -62,7 +68,8 @@ "max_value", "min_values", "max_values", - "no_data" + "no_data", + "time_steps" ], "additionalProperties": false } diff --git a/src/opengeodeweb_back/routes/schemas/cell_attribute_names.py b/src/opengeodeweb_back/routes/schemas/cell_attribute_names.py index 2bc778b0..a3d7e47d 100644 --- a/src/opengeodeweb_back/routes/schemas/cell_attribute_names.py +++ b/src/opengeodeweb_back/routes/schemas/cell_attribute_names.py @@ -27,6 +27,7 @@ def __post_init__(self) -> None: min_values: List[float] nb_items: int no_data: bool + time_steps: List[float] @dataclass diff --git a/src/opengeodeweb_back/routes/schemas/edge_attribute_names.json b/src/opengeodeweb_back/routes/schemas/edge_attribute_names.json index 275e06a5..b6375a20 100644 --- a/src/opengeodeweb_back/routes/schemas/edge_attribute_names.json +++ b/src/opengeodeweb_back/routes/schemas/edge_attribute_names.json @@ -52,6 +52,12 @@ }, "no_data": { "type": "boolean" + }, + "time_steps": { + "type": "array", + "items": { + "type": "number" + } } }, "required": [ @@ -62,7 +68,8 @@ "max_value", "min_values", "max_values", - "no_data" + "no_data", + "time_steps" ], "additionalProperties": false } diff --git a/src/opengeodeweb_back/routes/schemas/edge_attribute_names.py b/src/opengeodeweb_back/routes/schemas/edge_attribute_names.py index 67b00d5a..0790e94f 100644 --- a/src/opengeodeweb_back/routes/schemas/edge_attribute_names.py +++ b/src/opengeodeweb_back/routes/schemas/edge_attribute_names.py @@ -27,6 +27,7 @@ def __post_init__(self) -> None: min_values: List[float] nb_items: int no_data: bool + time_steps: List[float] @dataclass diff --git a/src/opengeodeweb_back/routes/schemas/model_component_edge_attribute_names.json b/src/opengeodeweb_back/routes/schemas/model_component_edge_attribute_names.json index 4b7e2ca9..22f40634 100644 --- a/src/opengeodeweb_back/routes/schemas/model_component_edge_attribute_names.json +++ b/src/opengeodeweb_back/routes/schemas/model_component_edge_attribute_names.json @@ -62,6 +62,12 @@ }, "no_data": { "type": "boolean" + }, + "time_steps": { + "type": "array", + "items": { + "type": "number" + } } }, "required": [ @@ -72,7 +78,8 @@ "max_value", "min_values", "max_values", - "no_data" + "no_data", + "time_steps" ], "additionalProperties": false } diff --git a/src/opengeodeweb_back/routes/schemas/model_component_edge_attribute_names.py b/src/opengeodeweb_back/routes/schemas/model_component_edge_attribute_names.py index 7b6bc017..ef9e062a 100644 --- a/src/opengeodeweb_back/routes/schemas/model_component_edge_attribute_names.py +++ b/src/opengeodeweb_back/routes/schemas/model_component_edge_attribute_names.py @@ -28,6 +28,7 @@ def __post_init__(self) -> None: min_values: List[float] nb_items: int no_data: bool + time_steps: List[float] @dataclass diff --git a/src/opengeodeweb_back/routes/schemas/model_component_polygon_attribute_names.json b/src/opengeodeweb_back/routes/schemas/model_component_polygon_attribute_names.json index 174dbd26..8318ba04 100644 --- a/src/opengeodeweb_back/routes/schemas/model_component_polygon_attribute_names.json +++ b/src/opengeodeweb_back/routes/schemas/model_component_polygon_attribute_names.json @@ -62,6 +62,12 @@ }, "no_data": { "type": "boolean" + }, + "time_steps": { + "type": "array", + "items": { + "type": "number" + } } }, "required": [ @@ -72,7 +78,8 @@ "max_value", "min_values", "max_values", - "no_data" + "no_data", + "time_steps" ], "additionalProperties": false } diff --git a/src/opengeodeweb_back/routes/schemas/model_component_polygon_attribute_names.py b/src/opengeodeweb_back/routes/schemas/model_component_polygon_attribute_names.py index e2b0622c..817dedbd 100644 --- a/src/opengeodeweb_back/routes/schemas/model_component_polygon_attribute_names.py +++ b/src/opengeodeweb_back/routes/schemas/model_component_polygon_attribute_names.py @@ -28,6 +28,7 @@ def __post_init__(self) -> None: min_values: List[float] nb_items: int no_data: bool + time_steps: List[float] @dataclass diff --git a/src/opengeodeweb_back/routes/schemas/model_component_polyhedron_attribute_names.json b/src/opengeodeweb_back/routes/schemas/model_component_polyhedron_attribute_names.json index b4403082..ec4085cc 100644 --- a/src/opengeodeweb_back/routes/schemas/model_component_polyhedron_attribute_names.json +++ b/src/opengeodeweb_back/routes/schemas/model_component_polyhedron_attribute_names.json @@ -62,6 +62,12 @@ }, "no_data": { "type": "boolean" + }, + "time_steps": { + "type": "array", + "items": { + "type": "number" + } } }, "required": [ @@ -72,7 +78,8 @@ "max_value", "min_values", "max_values", - "no_data" + "no_data", + "time_steps" ], "additionalProperties": false } diff --git a/src/opengeodeweb_back/routes/schemas/model_component_polyhedron_attribute_names.py b/src/opengeodeweb_back/routes/schemas/model_component_polyhedron_attribute_names.py index 2e0925c5..b04841aa 100644 --- a/src/opengeodeweb_back/routes/schemas/model_component_polyhedron_attribute_names.py +++ b/src/opengeodeweb_back/routes/schemas/model_component_polyhedron_attribute_names.py @@ -28,6 +28,7 @@ def __post_init__(self) -> None: min_values: List[float] nb_items: int no_data: bool + time_steps: List[float] @dataclass diff --git a/src/opengeodeweb_back/routes/schemas/model_component_vertex_attribute_names.json b/src/opengeodeweb_back/routes/schemas/model_component_vertex_attribute_names.json index e5a82e74..13cdc94f 100644 --- a/src/opengeodeweb_back/routes/schemas/model_component_vertex_attribute_names.json +++ b/src/opengeodeweb_back/routes/schemas/model_component_vertex_attribute_names.json @@ -62,6 +62,12 @@ }, "no_data": { "type": "boolean" + }, + "time_steps": { + "type": "array", + "items": { + "type": "number" + } } }, "required": [ @@ -72,7 +78,8 @@ "max_value", "min_values", "max_values", - "no_data" + "no_data", + "time_steps" ], "additionalProperties": false } diff --git a/src/opengeodeweb_back/routes/schemas/model_component_vertex_attribute_names.py b/src/opengeodeweb_back/routes/schemas/model_component_vertex_attribute_names.py index ecc61ded..d76ffb00 100644 --- a/src/opengeodeweb_back/routes/schemas/model_component_vertex_attribute_names.py +++ b/src/opengeodeweb_back/routes/schemas/model_component_vertex_attribute_names.py @@ -28,6 +28,7 @@ def __post_init__(self) -> None: min_values: List[float] nb_items: int no_data: bool + time_steps: List[float] @dataclass diff --git a/src/opengeodeweb_back/routes/schemas/polygon_attribute_names.json b/src/opengeodeweb_back/routes/schemas/polygon_attribute_names.json index a069701b..0f0e044f 100644 --- a/src/opengeodeweb_back/routes/schemas/polygon_attribute_names.json +++ b/src/opengeodeweb_back/routes/schemas/polygon_attribute_names.json @@ -52,6 +52,12 @@ }, "no_data": { "type": "boolean" + }, + "time_steps": { + "type": "array", + "items": { + "type": "number" + } } }, "required": [ @@ -62,7 +68,8 @@ "max_value", "min_values", "max_values", - "no_data" + "no_data", + "time_steps" ], "additionalProperties": false } diff --git a/src/opengeodeweb_back/routes/schemas/polygon_attribute_names.py b/src/opengeodeweb_back/routes/schemas/polygon_attribute_names.py index eaf20d1d..c775c94b 100644 --- a/src/opengeodeweb_back/routes/schemas/polygon_attribute_names.py +++ b/src/opengeodeweb_back/routes/schemas/polygon_attribute_names.py @@ -27,6 +27,7 @@ def __post_init__(self) -> None: min_values: List[float] nb_items: int no_data: bool + time_steps: List[float] @dataclass diff --git a/src/opengeodeweb_back/routes/schemas/polyhedron_attribute_names.json b/src/opengeodeweb_back/routes/schemas/polyhedron_attribute_names.json index e527a2be..aed82bd2 100644 --- a/src/opengeodeweb_back/routes/schemas/polyhedron_attribute_names.json +++ b/src/opengeodeweb_back/routes/schemas/polyhedron_attribute_names.json @@ -52,6 +52,12 @@ }, "no_data": { "type": "boolean" + }, + "time_steps": { + "type": "array", + "items": { + "type": "number" + } } }, "required": [ @@ -62,7 +68,8 @@ "max_value", "min_values", "max_values", - "no_data" + "no_data", + "time_steps" ], "additionalProperties": false } diff --git a/src/opengeodeweb_back/routes/schemas/polyhedron_attribute_names.py b/src/opengeodeweb_back/routes/schemas/polyhedron_attribute_names.py index d455d4c7..f1fb06f5 100644 --- a/src/opengeodeweb_back/routes/schemas/polyhedron_attribute_names.py +++ b/src/opengeodeweb_back/routes/schemas/polyhedron_attribute_names.py @@ -27,6 +27,7 @@ def __post_init__(self) -> None: min_values: List[float] nb_items: int no_data: bool + time_steps: List[float] @dataclass diff --git a/src/opengeodeweb_back/routes/schemas/vertex_attribute_names.json b/src/opengeodeweb_back/routes/schemas/vertex_attribute_names.json index 9939d854..eba134e8 100644 --- a/src/opengeodeweb_back/routes/schemas/vertex_attribute_names.json +++ b/src/opengeodeweb_back/routes/schemas/vertex_attribute_names.json @@ -52,6 +52,12 @@ }, "no_data": { "type": "boolean" + }, + "time_steps": { + "type": "array", + "items": { + "type": "number" + } } }, "required": [ @@ -62,7 +68,8 @@ "max_value", "min_values", "max_values", - "no_data" + "no_data", + "time_steps" ], "additionalProperties": false } diff --git a/src/opengeodeweb_back/routes/schemas/vertex_attribute_names.py b/src/opengeodeweb_back/routes/schemas/vertex_attribute_names.py index ad20057b..45fba1d1 100644 --- a/src/opengeodeweb_back/routes/schemas/vertex_attribute_names.py +++ b/src/opengeodeweb_back/routes/schemas/vertex_attribute_names.py @@ -27,6 +27,7 @@ def __post_init__(self) -> None: min_values: List[float] nb_items: int no_data: bool + time_steps: List[float] @dataclass diff --git a/tests/data/time_series.og_psf3d b/tests/data/time_series.og_psf3d new file mode 100644 index 00000000..b60d3e77 Binary files /dev/null and b/tests/data/time_series.og_psf3d differ diff --git a/tests/test_routes.py b/tests/test_routes.py index 78d601a4..556d2032 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -350,6 +350,8 @@ def test_vertex_attribute_names(client: FlaskClient) -> None: assert "max_values" in attribute assert "no_data" in attribute assert isinstance(attribute["no_data"], bool) + assert "time_steps" in attribute + assert isinstance(attribute["time_steps"], list) def test_cell_attribute_names(client: FlaskClient) -> None: @@ -733,6 +735,8 @@ def _assert_attributes_response(response: TestResponse) -> None: assert "max_values" in attribute assert "no_data" in attribute assert isinstance(attribute["no_data"], bool) + assert "time_steps" in attribute + assert isinstance(attribute["time_steps"], list) def test_model_component_vertex_attribute_names(client: FlaskClient) -> None: @@ -853,3 +857,32 @@ def test_component_id_length_is_strict(client: FlaskClient) -> None: model_id, _ = _load_brep_components(client) response = client.post(route, json={"id": model_id, "component_ids": ["a" * 32]}) assert response.status_code == 400 + + +def test_vertex_attribute_names_time_series(client: FlaskClient) -> None: + route = "/opengeodeweb_back/vertex_attribute_names" + + with client.application.app_context(): + file = str(data_dir / "time_series.og_psf3d") + data = Data.create( + geode_id=DUMMY_GEODE_ID, + geode_object=GeodePolygonalSurface3D.geode_object_type(), + viewer_object=GeodePolygonalSurface3D.viewer_type(), + viewer_elements_type=GeodePolygonalSurface3D.viewer_elements_type(), + ) + data.native_file = file + session = get_session() + if session: + session.commit() + + data_path = Path(geode_functions.data_file_path(data.id, data.native_file)) + data_path.parent.mkdir(parents=True, exist_ok=True) + assert data_path.exists(), f"File not found at {data_path}" + response = client.post(route, json={"id": data.id}) + assert response.status_code == 200 + attributes = response.get_json()["attributes"] + temperature = [ + attribute for attribute in attributes if attribute["attribute_name"] == "temperature" + ] + assert len(temperature) == 1 + assert temperature[0]["time_steps"] == [0.5, 1.0, 2.0]