Skip to content

Commit 8bcea61

Browse files
Add Spatial Zarr Convention models and metadata (#100)
* feat: add Spatial Zarr Convention models and metadata - Introduced models for the Spatial Zarr Convention in spatial.py. - Defined SpatialConvention and SpatialConventionMetadata classes with relevant metadata. - Implemented Spatial model with fields for dimensions, bounding box, transformation type, transformation matrix, shape, and registration. - Utilized Pydantic for data validation and serialization. * fix: enhance multiscales metadata handling with fallback for bounds and transform * Update eopf-geozarr version to 0.5.1.dev18+g4528bb6b0.d20251216 in uv.lock * fix: improve formatting and consistency in geospatial metadata classes * feat: add spatial properties to overview level metadata and update type definitions * feat: implement tests for Spatial Zarr Convention models and metadata * feat: enhance Proj and ProjConventionMetadata tests for serialization and validation * feat: add validation for non-empty dimensions in Spatial model * fix: remove redundant imports of ProjConventionMetadata and SpatialConventionMetadata in s2_multiscale.py * feat: add spatial properties and projection information to Zarr convention metadata for S2A, S2B, and S2C examples * feat: update spatial metadata and derivation chains in Zarr examples for improved consistency * fix: remove redundant version field from MultiscaleMeta and Multiscales classes * refactor: streamline spatial metadata calculation and error handling in write_geo_metadata function * fix: remove redundant version field from multiscales and optimized geozarr example JSON files * fix: improve transform calculation logging for multiscales metadata * fix: add validation for spatial transform data to prevent all-zero entries * fix: remove redundant spatial transform entries from optimized geozarr example JSON files * fix: allow spatial_transform to be None in OverviewLevelJSON type definition * fix: refactor ScaleLevel initialization and remove redundant spatial_transform entries
1 parent f53084b commit 8bcea61

16 files changed

Lines changed: 3169 additions & 2045 deletions

.vscode/launch.json

Lines changed: 359 additions & 317 deletions
Large diffs are not rendered by default.
Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,61 @@
11
"""
2-
Models for the GeoProj Zarr Convention
2+
Models for the Proj Zarr Convention (v1.0)
33
"""
44

55
from __future__ import annotations
66

7-
from typing import Literal, Self
7+
from typing import Literal
88

99
from pydantic import BaseModel, Field, model_validator
1010
from typing_extensions import TypedDict
1111

12-
from eopf_geozarr.data_api.geozarr.common import is_none
12+
from eopf_geozarr.data_api.geozarr.common import ZarrConventionMetadata, is_none
1313
from eopf_geozarr.data_api.geozarr.projjson import ProjJSON # noqa: TC001
1414

15-
GEO_PROJ_UUID: Literal["f17cb550-5864-4468-aeb7-f3180cfb622f"] = (
16-
"f17cb550-5864-4468-aeb7-f3180cfb622f"
17-
)
15+
PROJ_UUID: Literal["f17cb550-5864-4468-aeb7-f3180cfb622f"] = "f17cb550-5864-4468-aeb7-f3180cfb622f"
1816

1917

20-
class GeoProjConvention(TypedDict):
21-
version: Literal["0.1.0"]
22-
schema: Literal[
23-
"https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v0.1.0/schema.json"
18+
class ProjConvention(TypedDict):
19+
uuid: Literal["f17cb550-5864-4468-aeb7-f3180cfb622f"]
20+
name: Literal["proj:"]
21+
schema_url: Literal[
22+
"https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json"
2423
]
25-
name: Literal["geo-proj"]
24+
spec_url: Literal["https://github.com/zarr-experimental/geo-proj/blob/v1/README.md"]
2625
description: Literal["Coordinate reference system information for geospatial data"]
27-
spec: Literal["https://github.com/zarr-experimental/geo-proj/blob/v0.1.0/README.md"]
2826

2927

30-
GeoProjConventions = TypedDict( # type: ignore[misc]
31-
"GeoProjConventions", {GEO_PROJ_UUID: GeoProjConvention}, closed=False
32-
)
28+
class ProjConventionMetadata(ZarrConventionMetadata):
29+
uuid: Literal["f17cb550-5864-4468-aeb7-f3180cfb622f"] = PROJ_UUID
30+
name: Literal["proj:"] = "proj:"
31+
schema_url: Literal[
32+
"https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json"
33+
] = "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json"
34+
spec_url: Literal["https://github.com/zarr-experimental/geo-proj/blob/v1/README.md"] = (
35+
"https://github.com/zarr-experimental/geo-proj/blob/v1/README.md"
36+
)
37+
description: Literal["Coordinate reference system information for geospatial data"] = (
38+
"Coordinate reference system information for geospatial data"
39+
)
3340

3441

35-
class GeoProj(BaseModel):
42+
class Proj(BaseModel):
43+
# At least one of code, wkt2, or projjson must be provided
3644
code: str | None = Field(None, alias="proj:code", exclude_if=is_none)
3745
wkt2: str | None = Field(None, alias="proj:wkt2", exclude_if=is_none)
3846
projjson: ProjJSON | None = Field(None, alias="proj:projjson", exclude_if=is_none)
39-
spatial_dimensions: tuple[str, str] = Field(alias="proj:spatial_dimensions")
40-
transform: tuple[float, float, float, float, float, float] | None = Field(
41-
None, alias="proj:transform", exclude_if=is_none
42-
)
43-
bbox: tuple[float, float, float, float] | None = Field(
44-
None, alias="proj:bbox", exclude_if=is_none
45-
)
46-
shape: tuple[int, int] | None = Field(None, alias="proj:shape", exclude_if=is_none)
4747

4848
model_config = {"extra": "allow", "serialize_by_alias": True}
4949

5050
@model_validator(mode="after")
51-
def ensure_required_conditional_attributes(self) -> Self:
52-
if self.code is None and self.wkt2 is None and self.projjson is None:
53-
raise ValueError("One of 'code', 'wkt2', or 'projjson' must be provided.")
51+
def validate_at_least_one_crs(self) -> Proj:
52+
"""Validate that at least one CRS field is provided"""
53+
if not any([self.code, self.wkt2, self.projjson]):
54+
raise ValueError(
55+
"At least one of proj:code, proj:wkt2, or proj:projjson must be provided"
56+
)
5457
return self
58+
59+
60+
# Backwards compatibility alias
61+
GeoProj = Proj

src/eopf_geozarr/data_api/geozarr/multiscales/zcm.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class Multiscales(BaseModel):
103103

104104

105105
class MultiscalesJSON(TypedDict):
106+
version: NotRequired[str]
106107
layout: tuple[ScaleLevelJSON, ...]
107108
resampling_method: NotRequired[str]
108109

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""
2+
Models for the Spatial Zarr Convention
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from typing import Literal
8+
9+
from pydantic import BaseModel, Field, model_validator
10+
from typing_extensions import TypedDict
11+
12+
from eopf_geozarr.data_api.geozarr.common import ZarrConventionMetadata, is_none
13+
14+
SPATIAL_UUID: Literal["689b58e2-cf7b-45e0-9fff-9cfc0883d6b4"] = (
15+
"689b58e2-cf7b-45e0-9fff-9cfc0883d6b4"
16+
)
17+
18+
19+
class SpatialConvention(TypedDict):
20+
uuid: Literal["689b58e2-cf7b-45e0-9fff-9cfc0883d6b4"]
21+
name: Literal["spatial:"]
22+
schema_url: Literal[
23+
"https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json"
24+
]
25+
spec_url: Literal["https://github.com/zarr-conventions/spatial/blob/v1/README.md"]
26+
description: Literal["Spatial coordinate and transformation information"]
27+
28+
29+
class SpatialConventionMetadata(ZarrConventionMetadata):
30+
uuid: Literal["689b58e2-cf7b-45e0-9fff-9cfc0883d6b4"] = SPATIAL_UUID
31+
name: Literal["spatial:"] = "spatial:"
32+
schema_url: Literal[
33+
"https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json"
34+
] = "https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json"
35+
spec_url: Literal["https://github.com/zarr-conventions/spatial/blob/v1/README.md"] = (
36+
"https://github.com/zarr-conventions/spatial/blob/v1/README.md"
37+
)
38+
description: Literal["Spatial coordinate and transformation information"] = (
39+
"Spatial coordinate and transformation information"
40+
)
41+
42+
43+
class Spatial(BaseModel):
44+
dimensions: list[str] = Field(alias="spatial:dimensions") # Required field
45+
bbox: list[float] | None = Field(None, alias="spatial:bbox", exclude_if=is_none)
46+
transform_type: str = Field("affine", alias="spatial:transform_type")
47+
transform: list[float] | None = Field(None, alias="spatial:transform", exclude_if=is_none)
48+
shape: list[int] | None = Field(None, alias="spatial:shape", exclude_if=is_none)
49+
registration: str = Field("pixel", alias="spatial:registration")
50+
51+
model_config = {"extra": "allow", "serialize_by_alias": True}
52+
53+
@model_validator(mode="after")
54+
def validate_dimensions_not_empty(self) -> Spatial:
55+
"""Validate that dimensions list is not empty."""
56+
if not self.dimensions:
57+
raise ValueError("spatial:dimensions must contain at least one dimension")
58+
return self

0 commit comments

Comments
 (0)