Skip to content

Commit 839dadd

Browse files
committed
Update for projection extension v2 (proj:epsg -> proj:code)
1 parent c5a79c7 commit 839dadd

File tree

42 files changed

+966
-3452
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+966
-3452
lines changed

docs/tutorials/creating-a-landsat-stac.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2526,7 +2526,7 @@
25262526
"name": "python",
25272527
"nbconvert_exporter": "python",
25282528
"pygments_lexer": "ipython3",
2529-
"version": "3.11.3"
2529+
"version": "3.11.6"
25302530
}
25312531
},
25322532
"nbformat": 4,

pystac/asset.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ def ext(self) -> AssetExt:
267267
268268
Example::
269269
270-
asset.ext.proj.epsg = 4326
270+
asset.ext.proj.code = "EPSG:4326"
271271
"""
272272
from pystac.extensions.ext import AssetExt
273273

pystac/extensions/eo.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -656,19 +656,30 @@ def migrate(
656656
]
657657
del obj["properties"][f"eo:{field}"]
658658

659-
# eo:epsg became proj:epsg
659+
# eo:epsg became proj:epsg in Projection Extension <2.0.0 and became
660+
# proj:code in Projection Extension 2.0.0
660661
eo_epsg = PREFIX + "epsg"
661662
proj_epsg = projection.PREFIX + "epsg"
662-
if eo_epsg in obj["properties"] and proj_epsg not in obj["properties"]:
663-
obj["properties"][proj_epsg] = obj["properties"].pop(eo_epsg)
663+
proj_code = projection.PREFIX + "code"
664+
if (
665+
eo_epsg in obj["properties"]
666+
and proj_epsg not in obj["properties"]
667+
and proj_code not in obj["properties"]
668+
):
664669
obj["stac_extensions"] = obj.get("stac_extensions", [])
665-
if (
666-
projection.ProjectionExtension.get_schema_uri()
667-
not in obj["stac_extensions"]
670+
if set(obj["stac_extensions"]).intersection(
671+
projection.ProjectionExtensionHooks.pre_2
668672
):
669-
obj["stac_extensions"].append(
670-
projection.ProjectionExtension.get_schema_uri()
671-
)
673+
obj["properties"][proj_epsg] = obj["properties"].pop(eo_epsg)
674+
else:
675+
obj["properties"][
676+
proj_code
677+
] = f"EPSG:{obj['properties'].pop(eo_epsg)}"
678+
if not projection.ProjectionExtensionHooks().has_extension(obj):
679+
obj["stac_extensions"].append(
680+
projection.ProjectionExtension.get_schema_uri()
681+
)
682+
672683
if not any(prop.startswith(PREFIX) for prop in obj["properties"]):
673684
obj["stac_extensions"].remove(EOExtension.get_schema_uri())
674685

pystac/extensions/hooks.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import TYPE_CHECKING, Any
77

88
import pystac
9+
from pystac.extensions.base import VERSION_REGEX
910
from pystac.serialization.identify import STACJSONDescription, STACVersionID
1011

1112
if TYPE_CHECKING:
@@ -43,6 +44,13 @@ def _get_stac_object_types(self) -> set[str]:
4344
def get_object_links(self, obj: STACObject) -> list[str | pystac.RelType] | None:
4445
return None
4546

47+
def has_extension(self, obj: dict[str, Any]) -> bool:
48+
schema_startswith = VERSION_REGEX.split(self.schema_uri)[0] + "/"
49+
return any(
50+
uri.startswith(schema_startswith) or uri in self.prev_extension_ids
51+
for uri in obj.get("stac_extensions", [])
52+
)
53+
4654
def migrate(
4755
self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription
4856
) -> None:

pystac/extensions/projection.py

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,20 @@
2222
SummariesExtension,
2323
)
2424
from pystac.extensions.hooks import ExtensionHooks
25+
from pystac.serialization.identify import STACJSONDescription, STACVersionID
2526

2627
T = TypeVar("T", pystac.Item, pystac.Asset, item_assets.AssetDefinition)
2728

28-
SCHEMA_URI: str = "https://stac-extensions.github.io/projection/v1.1.0/schema.json"
29+
SCHEMA_URI: str = "https://stac-extensions.github.io/projection/v2.0.0/schema.json"
2930
SCHEMA_URIS: list[str] = [
3031
"https://stac-extensions.github.io/projection/v1.0.0/schema.json",
32+
"https://stac-extensions.github.io/projection/v1.1.0/schema.json",
3133
SCHEMA_URI,
3234
]
3335
PREFIX: str = "proj:"
3436

3537
# Field names
38+
CODE_PROP: str = PREFIX + "code"
3639
EPSG_PROP: str = PREFIX + "epsg"
3740
WKT2_PROP: str = PREFIX + "wkt2"
3841
PROJJSON_PROP: str = PREFIX + "projjson"
@@ -66,7 +69,9 @@ class ProjectionExtension(
6669

6770
def apply(
6871
self,
69-
epsg: int | None,
72+
*,
73+
epsg: int | None = None,
74+
code: str | None = None,
7075
wkt2: str | None = None,
7176
projjson: dict[str, Any] | None = None,
7277
geometry: dict[str, Any] | None = None,
@@ -78,7 +83,10 @@ def apply(
7883
"""Applies Projection extension properties to the extended Item.
7984
8085
Args:
81-
epsg : REQUIRED. EPSG code of the datasource.
86+
epsg : Code of the datasource. Example: 4326. One of ``code`` and
87+
``epsg`` must be provided.
88+
code : Code of the datasource. Example: "EPSG:4326". One of ``code`` and
89+
``epsg`` must be provided.
8290
wkt2 : WKT2 string representing the Coordinate Reference
8391
System (CRS) that the ``geometry`` and ``bbox`` fields represent
8492
projjson : PROJJSON dict representing the
@@ -97,7 +105,15 @@ def apply(
97105
transform : The affine transformation coefficients for
98106
the default grid
99107
"""
100-
self.epsg = epsg
108+
if epsg is not None and code is not None:
109+
raise KeyError(
110+
"Only one of the options ``code`` and ``epsg`` should be specified."
111+
)
112+
elif epsg:
113+
self.epsg = epsg
114+
else:
115+
self.code = code
116+
101117
self.wkt2 = wkt2
102118
self.projjson = projjson
103119
self.geometry = geometry
@@ -118,11 +134,34 @@ def epsg(self) -> int | None:
118134
It should also be set to ``None`` if a CRS exists, but for which there is no
119135
valid EPSG code.
120136
"""
137+
if self.code is not None and self.code.startswith("EPSG:"):
138+
return int(self.code.replace("EPSG:", ""))
121139
return self._get_property(EPSG_PROP, int)
122140

123141
@epsg.setter
124142
def epsg(self, v: int | None) -> None:
125-
self._set_property(EPSG_PROP, v, pop_if_none=False)
143+
self._set_property(EPSG_PROP, None)
144+
if v is None:
145+
self.code = None
146+
else:
147+
self.code = f"EPSG:{v}"
148+
149+
@property
150+
def code(self) -> str | None:
151+
"""Get or set the code of the datasource.
152+
153+
Added in version 2.0.0 of this extension replacing "proj:epsg".
154+
155+
Projection codes are identified by a string. The `proj <https://proj.org/>`_
156+
library defines projections using "authority:code", e.g., "EPSG:4326" or
157+
"IAU_2015:30100". Different projection authorities may define different
158+
string formats.
159+
"""
160+
return self._get_property(CODE_PROP, str)
161+
162+
@code.setter
163+
def code(self, v: int | None) -> None:
164+
self._set_property(CODE_PROP, v, pop_if_none=False)
126165

127166
@property
128167
def wkt2(self) -> str | None:
@@ -169,13 +208,13 @@ def crs_string(self) -> str | None:
169208
This string can be used to feed, e.g., ``rasterio.crs.CRS.from_string``.
170209
The string is determined by the following heuristic:
171210
172-
1. If an EPSG code is set, return "EPSG:{code}", else
211+
1. If a code is set, return the code string, else
173212
2. If wkt2 is set, return the WKT string, else,
174213
3. If projjson is set, return the projjson as a string, else,
175214
4. Return None
176215
"""
177-
if self.epsg:
178-
return f"EPSG:{self.epsg}"
216+
if self.code:
217+
return self.code
179218
elif self.wkt2:
180219
return self.wkt2
181220
elif self.projjson:
@@ -190,7 +229,7 @@ def geometry(self) -> dict[str, Any] | None:
190229
This dict should be formatted according the Polygon object format specified in
191230
`RFC 7946, sections 3.1.6 <https://tools.ietf.org/html/rfc7946>`_,
192231
except not necessarily in EPSG:4326 as required by RFC7946. Specified based on
193-
the ``epsg``, ``projjson`` or ``wkt2`` fields (not necessarily EPSG:4326).
232+
the ``code``, ``projjson`` or ``wkt2`` fields (not necessarily EPSG:4326).
194233
Ideally, this will be represented by a Polygon with five coordinates, as the
195234
item in the asset data CRS should be a square aligned to the original CRS grid.
196235
"""
@@ -205,7 +244,7 @@ def bbox(self) -> list[float] | None:
205244
"""Get or sets the bounding box of the assets represented by this item in the
206245
asset data CRS.
207246
208-
Specified as 4 or 6 coordinates based on the CRS defined in the ``epsg``,
247+
Specified as 4 or 6 coordinates based on the CRS defined in the ``code``,
209248
``projjson`` or ``wkt2`` properties. First two numbers are coordinates of the
210249
lower left corner, followed by coordinates of upper right corner, e.g.,
211250
``[west, south, east, north]``, ``[xmin, ymin, xmax, ymax]``,
@@ -383,16 +422,32 @@ class SummariesProjectionExtension(SummariesExtension):
383422
defined in the :stac-ext:`Projection Extension <projection>`.
384423
"""
385424

425+
@property
426+
def code(self) -> list[str] | None:
427+
"""Get or sets the summary of :attr:`ProjectionExtension.code` values
428+
for this Collection.
429+
"""
430+
return self.summaries.get_list(CODE_PROP)
431+
432+
@code.setter
433+
def code(self, v: list[str] | None) -> None:
434+
self._set_summary(CODE_PROP, v)
435+
386436
@property
387437
def epsg(self) -> list[int] | None:
388-
"""Get or sets the summary of :attr:`ProjectionExtension.epsg` values
438+
"""Get the summary of :attr:`ProjectionExtension.epsg` values
389439
for this Collection.
390440
"""
391-
return self.summaries.get_list(EPSG_PROP)
441+
if self.code is None:
442+
return None
443+
return [int(code.replace("EPSG:", "")) for code in self.code if "EPSG:" in code]
392444

393445
@epsg.setter
394446
def epsg(self, v: list[int] | None) -> None:
395-
self._set_summary(EPSG_PROP, v)
447+
if v is None:
448+
self.code = None
449+
else:
450+
self.code = [f"EPSG:{epsg}" for epsg in v]
396451

397452

398453
class ProjectionExtensionHooks(ExtensionHooks):
@@ -402,7 +457,27 @@ class ProjectionExtensionHooks(ExtensionHooks):
402457
"projection",
403458
*[uri for uri in SCHEMA_URIS if uri != SCHEMA_URI],
404459
}
460+
pre_2 = {
461+
"proj",
462+
"projection",
463+
"https://stac-extensions.github.io/projection/v1.0.0/schema.json",
464+
"https://stac-extensions.github.io/projection/v1.1.0/schema.json",
465+
}
405466
stac_object_types = {pystac.STACObjectType.ITEM}
406467

468+
def migrate(
469+
self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription
470+
) -> None:
471+
if not self.has_extension(obj):
472+
return
473+
474+
# proj:epsg moved to proj:code
475+
if "proj:epsg" in obj["properties"]:
476+
epsg = obj["properties"]["proj:epsg"]
477+
obj["properties"]["proj:code"] = f"EPSG:{epsg}"
478+
del obj["properties"]["proj:epsg"]
479+
480+
super().migrate(obj, version, info)
481+
407482

408483
PROJECTION_EXTENSION_HOOKS: ExtensionHooks = ProjectionExtensionHooks()

pystac/item.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ def ext(self) -> ItemExt:
496496
497497
Example::
498498
499-
item.ext.proj.epsg = 4326
499+
item.ext.proj.code = "EPSG:4326"
500500
"""
501501
from pystac.extensions.ext import ItemExt
502502

0 commit comments

Comments
 (0)