Skip to content

Commit 8019b1d

Browse files
jsignellgadomski
andcommitted
feat: add MGRS Extension
From code in stactools-package for sentinel2 Co-authored-by: Pete Gadomski <[email protected]>
1 parent b0c4f25 commit 8019b1d

File tree

8 files changed

+447
-0
lines changed

8 files changed

+447
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- `sort_links_by_id` to Catalog `get_child()` and `modify_links` to `get_stac_objects()` ([#1064](https://github.com/stac-utils/pystac/pull/1064))
88
- `*ids` to Catalog and Collection `get_items()` for only including the provided ids in the iterator ([#1075](https://github.com/stac-utils/pystac/pull/1075))
99
- `recursive` to Catalog and Collection `get_items()` to walk the sub-catalogs and sub-collections ([#1075](https://github.com/stac-utils/pystac/pull/1075))
10+
- MGRS Extension ([#1088](https://github.com/stac-utils/pystac/pull/1088))
1011

1112
### Changed
1213

docs/api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ PySTAC provides support for the following STAC Extensions:
9696
* :mod:`File Info <pystac.extensions.file>`
9797
* :mod:`Item Assets <pystac.extensions.item_assets>`
9898
* :mod:`Label <pystac.extensions.label>`
99+
* :mod:`MGRS <pystac.extensions.mgrs>`
99100
* :mod:`Point Cloud <pystac.extensions.pointcloud>`
100101
* :mod:`Projection <pystac.extensions.projection>`
101102
* :mod:`Raster <pystac.extensions.raster>`

docs/api/extensions/mgrs.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
pystac.extensions.mgrs
2+
============================
3+
4+
.. automodule:: pystac.extensions.mgrs
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:

pystac/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
import pystac.extensions.file
9090
import pystac.extensions.item_assets
9191
import pystac.extensions.label
92+
import pystac.extensions.mgrs
9293
import pystac.extensions.pointcloud
9394
import pystac.extensions.projection
9495
import pystac.extensions.sar
@@ -107,6 +108,7 @@
107108
pystac.extensions.file.FILE_EXTENSION_HOOKS,
108109
pystac.extensions.item_assets.ITEM_ASSETS_EXTENSION_HOOKS,
109110
pystac.extensions.label.LABEL_EXTENSION_HOOKS,
111+
pystac.extensions.mgrs.MGRS_EXTENSION_HOOKS,
110112
pystac.extensions.pointcloud.POINTCLOUD_EXTENSION_HOOKS,
111113
pystac.extensions.projection.PROJECTION_EXTENSION_HOOKS,
112114
pystac.extensions.sar.SAR_EXTENSION_HOOKS,

pystac/extensions/mgrs.py

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
"""Implements the :stac-ext:`MGRS Extension <mgrs>`."""
2+
3+
import re
4+
from typing import Any, Dict, FrozenSet, Optional, Pattern, Set, Union
5+
6+
import pystac
7+
from pystac.extensions.base import ExtensionManagementMixin, PropertiesExtension
8+
from pystac.extensions.hooks import ExtensionHooks
9+
10+
SCHEMA_URI: str = "https://stac-extensions.github.io/mgrs/v1.0.0/schema.json"
11+
PREFIX: str = "mgrs:"
12+
13+
# Field names
14+
LATITUDE_BAND_PROP: str = PREFIX + "latitude_band" # required
15+
GRID_SQUARE_PROP: str = PREFIX + "grid_square" # required
16+
UTM_ZONE_PROP: str = PREFIX + "utm_zone"
17+
18+
LATITUDE_BANDS: FrozenSet[str] = frozenset(
19+
{
20+
"C",
21+
"D",
22+
"E",
23+
"F",
24+
"G",
25+
"H",
26+
"J",
27+
"K",
28+
"L",
29+
"M",
30+
"N",
31+
"P",
32+
"Q",
33+
"R",
34+
"S",
35+
"T",
36+
"U",
37+
"V",
38+
"W",
39+
"X",
40+
}
41+
)
42+
43+
UTM_ZONES: FrozenSet[int] = frozenset(
44+
{
45+
1,
46+
2,
47+
3,
48+
4,
49+
5,
50+
6,
51+
7,
52+
8,
53+
9,
54+
10,
55+
11,
56+
12,
57+
13,
58+
14,
59+
15,
60+
16,
61+
17,
62+
18,
63+
19,
64+
20,
65+
21,
66+
22,
67+
23,
68+
24,
69+
25,
70+
26,
71+
27,
72+
28,
73+
29,
74+
30,
75+
31,
76+
32,
77+
33,
78+
34,
79+
35,
80+
36,
81+
37,
82+
38,
83+
39,
84+
40,
85+
41,
86+
42,
87+
43,
88+
44,
89+
45,
90+
46,
91+
47,
92+
48,
93+
49,
94+
50,
95+
51,
96+
52,
97+
53,
98+
54,
99+
55,
100+
56,
101+
57,
102+
58,
103+
59,
104+
60,
105+
}
106+
)
107+
108+
GRID_SQUARE_REGEX: str = (
109+
r"[ABCDEFGHJKLMNPQRSTUVWXYZ][ABCDEFGHJKLMNPQRSTUV](\d{2}|\d{4}|\d{6}|\d{8}|\d{10})?"
110+
)
111+
GRID_SQUARE_PATTERN: Pattern[str] = re.compile(GRID_SQUARE_REGEX)
112+
113+
114+
def validated_latitude_band(v: str) -> str:
115+
if not isinstance(v, str):
116+
raise ValueError("Invalid MGRS latitude band: must be str")
117+
if v not in LATITUDE_BANDS:
118+
raise ValueError(f"Invalid MGRS latitude band: {v} is not in {LATITUDE_BANDS}")
119+
return v
120+
121+
122+
def validated_grid_square(v: str) -> str:
123+
if not isinstance(v, str):
124+
raise ValueError("Invalid MGRS grid square identifier: must be str")
125+
if not GRID_SQUARE_PATTERN.fullmatch(v):
126+
raise ValueError(
127+
f"Invalid MGRS grid square identifier: {v}"
128+
f" does not match the regex {GRID_SQUARE_REGEX}"
129+
)
130+
return v
131+
132+
133+
def validated_utm_zone(v: Optional[int]) -> Optional[int]:
134+
if v is not None and not isinstance(v, int):
135+
raise ValueError("Invalid MGRS utm zone: must be None or int")
136+
if v is not None and v not in UTM_ZONES:
137+
raise ValueError(f"Invalid MGRS UTM zone: {v} is not in {UTM_ZONES}")
138+
return v
139+
140+
141+
class MgrsExtension(
142+
PropertiesExtension,
143+
ExtensionManagementMixin[Union[pystac.Item, pystac.Collection]],
144+
):
145+
"""A concrete implementation of :class:`MgrsExtension` on an :class:`~pystac.Item`
146+
that extends the properties of the Item to include properties defined in the
147+
:stac-ext:`MGRS Extension <mgrs>`.
148+
149+
This class should generally not be instantiated directly. Instead, call
150+
:meth:`MgrsExtension.ext` on an :class:`~pystac.Item` to extend it.
151+
152+
.. code-block:: python
153+
154+
>>> item: pystac.Item = ...
155+
>>> proj_ext = MgrsExtension.ext(item)
156+
"""
157+
158+
item: pystac.Item
159+
"""The :class:`~pystac.Item` being extended."""
160+
161+
properties: Dict[str, Any]
162+
"""The :class:`~pystac.Item` properties, including extension properties."""
163+
164+
def __init__(self, item: pystac.Item):
165+
self.item = item
166+
self.properties = item.properties
167+
168+
def __repr__(self) -> str:
169+
return "<ItemMgrsExtension Item id={}>".format(self.item.id)
170+
171+
def apply(
172+
self,
173+
latitude_band: str,
174+
grid_square: str,
175+
utm_zone: Optional[int] = None,
176+
) -> None:
177+
"""Applies MGRS extension properties to the extended Item.
178+
179+
Args:
180+
latitude_band : REQUIRED. The latitude band of the Item's centroid.
181+
grid_square : REQUIRED. MGRS grid square of the Item's centroid.
182+
utm_zone : The UTM Zone of the Item centroid.
183+
"""
184+
self.latitude_band = validated_latitude_band(latitude_band)
185+
self.grid_square = validated_grid_square(grid_square)
186+
self.utm_zone = validated_utm_zone(utm_zone)
187+
188+
@property
189+
def latitude_band(self) -> Optional[str]:
190+
"""Get or sets the latitude band of the datasource."""
191+
return self._get_property(LATITUDE_BAND_PROP, str)
192+
193+
@latitude_band.setter
194+
def latitude_band(self, v: str) -> None:
195+
self._set_property(
196+
LATITUDE_BAND_PROP, validated_latitude_band(v), pop_if_none=False
197+
)
198+
199+
@property
200+
def grid_square(self) -> Optional[str]:
201+
"""Get or sets the latitude band of the datasource."""
202+
return self._get_property(GRID_SQUARE_PROP, str)
203+
204+
@grid_square.setter
205+
def grid_square(self, v: str) -> None:
206+
self._set_property(
207+
GRID_SQUARE_PROP, validated_grid_square(v), pop_if_none=False
208+
)
209+
210+
@property
211+
def utm_zone(self) -> Optional[int]:
212+
"""Get or sets the latitude band of the datasource."""
213+
return self._get_property(UTM_ZONE_PROP, int)
214+
215+
@utm_zone.setter
216+
def utm_zone(self, v: Optional[int]) -> None:
217+
self._set_property(UTM_ZONE_PROP, validated_utm_zone(v), pop_if_none=True)
218+
219+
@classmethod
220+
def get_schema_uri(cls) -> str:
221+
return SCHEMA_URI
222+
223+
@classmethod
224+
def ext(cls, obj: pystac.Item, add_if_missing: bool = False) -> "MgrsExtension":
225+
"""Extends the given STAC Object with properties from the :stac-ext:`MGRS
226+
Extension <mgrs>`.
227+
228+
This extension can be applied to instances of :class:`~pystac.Item`.
229+
230+
Raises:
231+
232+
pystac.ExtensionTypeError : If an invalid object type is passed.
233+
"""
234+
if isinstance(obj, pystac.Item):
235+
cls.validate_has_extension(obj, add_if_missing)
236+
return MgrsExtension(obj)
237+
else:
238+
raise pystac.ExtensionTypeError(
239+
f"MGRS Extension does not apply to type '{type(obj).__name__}'"
240+
)
241+
242+
243+
class MgrsExtensionHooks(ExtensionHooks):
244+
schema_uri: str = SCHEMA_URI
245+
prev_extension_ids: Set[str] = set()
246+
stac_object_types = {pystac.STACObjectType.ITEM}
247+
248+
249+
MGRS_EXTENSION_HOOKS: ExtensionHooks = MgrsExtensionHooks()

tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# TODO move all test case code to this file
22

3+
from pathlib import Path
34
from datetime import datetime
45

56
import pytest
@@ -9,6 +10,9 @@
910
from .utils import ARBITRARY_BBOX, ARBITRARY_EXTENT, ARBITRARY_GEOM, TestCases
1011

1112

13+
here = Path(__file__).resolve().parent
14+
15+
1216
@pytest.fixture
1317
def catalog() -> Catalog:
1418
return Catalog("test-catalog", "A test catalog")
@@ -38,3 +42,7 @@ def test_case_8_collection() -> Collection:
3842
def projection_landsat8_item() -> Item:
3943
path = TestCases.get_path("data-files/projection/example-landsat8.json")
4044
return Item.from_file(path)
45+
46+
47+
def get_data_file(rel_path: str) -> str:
48+
return str(here / "data-files" / rel_path)

tests/data-files/mgrs/item.json

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"stac_version": "1.0.0",
3+
"stac_extensions": [
4+
"https://stac-extensions.github.io/mgrs/v1.0.0/schema.json"
5+
],
6+
"type": "Feature",
7+
"id": "item",
8+
"bbox": [
9+
172.9,
10+
1.3,
11+
173,
12+
1.4
13+
],
14+
"geometry": {
15+
"type": "Polygon",
16+
"coordinates": [
17+
[
18+
[
19+
172.9,
20+
1.3
21+
],
22+
[
23+
173,
24+
1.3
25+
],
26+
[
27+
173,
28+
1.4
29+
],
30+
[
31+
172.9,
32+
1.4
33+
],
34+
[
35+
172.9,
36+
1.3
37+
]
38+
]
39+
]
40+
},
41+
"properties": {
42+
"datetime": "2020-12-11T22:38:32Z",
43+
"mgrs:utm_zone": 13,
44+
"mgrs:latitude_band": "X",
45+
"mgrs:grid_square": "DH"
46+
},
47+
"links": [],
48+
"assets": {
49+
"data": {
50+
"href": "https://example.com/examples/file.xyz"
51+
}
52+
}
53+
}

0 commit comments

Comments
 (0)