"""Implements the :stac-ext:`MGRS Extension <mgrs>`."""
import re
from re import Pattern
from typing import Any, Literal, Union
import pystac
from pystac.extensions.base import ExtensionManagementMixin, PropertiesExtension
from pystac.extensions.hooks import ExtensionHooks
SCHEMA_URI: str = "https://stac-extensions.github.io/mgrs/v1.0.0/schema.json"
SCHEMA_STARTSWITH: str = "https://stac-extensions.github.io/mgrs/"
PREFIX: str = "mgrs:"
# Field names
LATITUDE_BAND_PROP: str = PREFIX + "latitude_band" # required
GRID_SQUARE_PROP: str = PREFIX + "grid_square" # required
UTM_ZONE_PROP: str = PREFIX + "utm_zone"
LATITUDE_BANDS: frozenset[str] = frozenset(
{
"C",
"D",
"E",
"F",
"G",
"H",
"J",
"K",
"L",
"M",
"N",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
}
)
UTM_ZONES: frozenset[int] = frozenset(
{
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
}
)
GRID_SQUARE_REGEX: str = (
r"[ABCDEFGHJKLMNPQRSTUVWXYZ][ABCDEFGHJKLMNPQRSTUV](\d{2}|\d{4}|\d{6}|\d{8}|\d{10})?"
)
GRID_SQUARE_PATTERN: Pattern[str] = re.compile(GRID_SQUARE_REGEX)
[docs]
def validated_latitude_band(v: str) -> str:
if not isinstance(v, str):
raise ValueError("Invalid MGRS latitude band: must be str")
if v not in LATITUDE_BANDS:
raise ValueError(f"Invalid MGRS latitude band: {v} is not in {LATITUDE_BANDS}")
return v
[docs]
def validated_grid_square(v: str) -> str:
if not isinstance(v, str):
raise ValueError("Invalid MGRS grid square identifier: must be str")
if not GRID_SQUARE_PATTERN.fullmatch(v):
raise ValueError(
f"Invalid MGRS grid square identifier: {v}"
f" does not match the regex {GRID_SQUARE_REGEX}"
)
return v
[docs]
def validated_utm_zone(v: int | None) -> int | None:
if v is not None and not isinstance(v, int):
raise ValueError("Invalid MGRS utm zone: must be None or int")
if v is not None and v not in UTM_ZONES:
raise ValueError(f"Invalid MGRS UTM zone: {v} is not in {UTM_ZONES}")
return v
[docs]
class MgrsExtension(
PropertiesExtension,
ExtensionManagementMixin[Union[pystac.Item, pystac.Collection]],
):
"""A concrete implementation of :class:`MgrsExtension` on an :class:`~pystac.Item`
that extends the properties of the Item to include properties defined in the
:stac-ext:`MGRS Extension <mgrs>`.
This class should generally not be instantiated directly. Instead, call
:meth:`MgrsExtension.ext` on an :class:`~pystac.Item` to extend it.
.. code-block:: python
>>> item: pystac.Item = ...
>>> proj_ext = MgrsExtension.ext(item)
"""
name: Literal["mgrs"] = "mgrs"
item: pystac.Item
"""The :class:`~pystac.Item` being extended."""
properties: dict[str, Any]
"""The :class:`~pystac.Item` properties, including extension properties."""
def __init__(self, item: pystac.Item):
self.item = item
self.properties = item.properties
def __repr__(self) -> str:
return f"<ItemMgrsExtension Item id={self.item.id}>"
[docs]
def apply(
self,
latitude_band: str,
grid_square: str,
utm_zone: int | None = None,
) -> None:
"""Applies MGRS extension properties to the extended Item.
Args:
latitude_band : REQUIRED. The latitude band of the Item's centroid.
grid_square : REQUIRED. MGRS grid square of the Item's centroid.
utm_zone : The UTM Zone of the Item centroid.
"""
self.latitude_band = validated_latitude_band(latitude_band)
self.grid_square = validated_grid_square(grid_square)
self.utm_zone = validated_utm_zone(utm_zone)
@property
def latitude_band(self) -> str | None:
"""Get or sets the latitude band of the datasource."""
return self._get_property(LATITUDE_BAND_PROP, str)
@latitude_band.setter
def latitude_band(self, v: str) -> None:
self._set_property(
LATITUDE_BAND_PROP, validated_latitude_band(v), pop_if_none=False
)
@property
def grid_square(self) -> str | None:
"""Get or sets the latitude band of the datasource."""
return self._get_property(GRID_SQUARE_PROP, str)
@grid_square.setter
def grid_square(self, v: str) -> None:
self._set_property(
GRID_SQUARE_PROP, validated_grid_square(v), pop_if_none=False
)
@property
def utm_zone(self) -> int | None:
"""Get or sets the latitude band of the datasource."""
return self._get_property(UTM_ZONE_PROP, int)
@utm_zone.setter
def utm_zone(self, v: int | None) -> None:
self._set_property(UTM_ZONE_PROP, validated_utm_zone(v), pop_if_none=True)
[docs]
@classmethod
def get_schema_uri(cls) -> str:
return SCHEMA_URI
[docs]
@classmethod
def ext(cls, obj: pystac.Item, add_if_missing: bool = False) -> "MgrsExtension":
"""Extends the given STAC Object with properties from the :stac-ext:`MGRS
Extension <mgrs>`.
This extension can be applied to instances of :class:`~pystac.Item`.
Raises:
pystac.ExtensionTypeError : If an invalid object type is passed.
"""
if isinstance(obj, pystac.Item):
cls.ensure_has_extension(obj, add_if_missing)
return MgrsExtension(obj)
else:
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
[docs]
class MgrsExtensionHooks(ExtensionHooks):
schema_uri: str = SCHEMA_URI
prev_extension_ids: set[str] = set()
stac_object_types = {pystac.STACObjectType.ITEM}
MGRS_EXTENSION_HOOKS: ExtensionHooks = MgrsExtensionHooks()