Skip to content

Commit 2261b55

Browse files
ProjectionExtension v2 (proj:epsg -> proj:code) (#1287)
* Update for projection extension v2 (proj:epsg -> proj:code) * Add tests for old version of the extension * add new fields config to fix summaries * precommit touch up * Rebuild cassettes * Tidy up fields - remove dev changes * Test errors * Revert changes to docs notebook --------- Co-authored-by: KeynesYouDigit <[email protected]>
1 parent ab747be commit 2261b55

File tree

304 files changed

+10325
-9531
lines changed

Some content is hidden

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

304 files changed

+10325
-9531
lines changed

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: 19 additions & 8 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()
673+
obj["properties"][proj_epsg] = obj["properties"].pop(eo_epsg)
674+
else:
675+
obj["properties"][proj_code] = (
676+
f"EPSG:{obj['properties'].pop(eo_epsg)}"
671677
)
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 & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,20 @@
2121
SummariesExtension,
2222
)
2323
from pystac.extensions.hooks import ExtensionHooks
24+
from pystac.serialization.identify import STACJSONDescription, STACVersionID
2425

2526
T = TypeVar("T", pystac.Item, pystac.Asset, pystac.ItemAssetDefinition)
2627

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

3436
# Field names
37+
CODE_PROP: str = PREFIX + "code"
3538
EPSG_PROP: str = PREFIX + "epsg"
3639
WKT2_PROP: str = PREFIX + "wkt2"
3740
PROJJSON_PROP: str = PREFIX + "projjson"
@@ -65,7 +68,9 @@ class ProjectionExtension(
6568

6669
def apply(
6770
self,
68-
epsg: int | None,
71+
*,
72+
epsg: int | None = None,
73+
code: str | None = None,
6974
wkt2: str | None = None,
7075
projjson: dict[str, Any] | None = None,
7176
geometry: dict[str, Any] | None = None,
@@ -77,7 +82,10 @@ def apply(
7782
"""Applies Projection extension properties to the extended Item.
7883
7984
Args:
80-
epsg : REQUIRED. EPSG code of the datasource.
85+
epsg : Code of the datasource. Example: 4326. One of ``code`` and
86+
``epsg`` must be provided.
87+
code : Code of the datasource. Example: "EPSG:4326". One of ``code`` and
88+
``epsg`` must be provided.
8189
wkt2 : WKT2 string representing the Coordinate Reference
8290
System (CRS) that the ``geometry`` and ``bbox`` fields represent
8391
projjson : PROJJSON dict representing the
@@ -96,7 +104,15 @@ def apply(
96104
transform : The affine transformation coefficients for
97105
the default grid
98106
"""
99-
self.epsg = epsg
107+
if epsg is not None and code is not None:
108+
raise KeyError(
109+
"Only one of the options ``code`` and ``epsg`` should be specified."
110+
)
111+
elif epsg:
112+
self.epsg = epsg
113+
else:
114+
self.code = code
115+
100116
self.wkt2 = wkt2
101117
self.projjson = projjson
102118
self.geometry = geometry
@@ -117,11 +133,33 @@ def epsg(self) -> int | None:
117133
It should also be set to ``None`` if a CRS exists, but for which there is no
118134
valid EPSG code.
119135
"""
120-
return self._get_property(EPSG_PROP, int)
136+
if self.code is not None and self.code.startswith("EPSG:"):
137+
return int(self.code.replace("EPSG:", ""))
138+
return None
121139

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

126164
@property
127165
def wkt2(self) -> str | None:
@@ -168,13 +206,13 @@ def crs_string(self) -> str | None:
168206
This string can be used to feed, e.g., ``rasterio.crs.CRS.from_string``.
169207
The string is determined by the following heuristic:
170208
171-
1. If an EPSG code is set, return "EPSG:{code}", else
209+
1. If a code is set, return the code string, else
172210
2. If wkt2 is set, return the WKT string, else,
173211
3. If projjson is set, return the projjson as a string, else,
174212
4. Return None
175213
"""
176-
if self.epsg:
177-
return f"EPSG:{self.epsg}"
214+
if self.code:
215+
return self.code
178216
elif self.wkt2:
179217
return self.wkt2
180218
elif self.projjson:
@@ -189,7 +227,7 @@ def geometry(self) -> dict[str, Any] | None:
189227
This dict should be formatted according the Polygon object format specified in
190228
`RFC 7946, sections 3.1.6 <https://tools.ietf.org/html/rfc7946>`_,
191229
except not necessarily in EPSG:4326 as required by RFC7946. Specified based on
192-
the ``epsg``, ``projjson`` or ``wkt2`` fields (not necessarily EPSG:4326).
230+
the ``code``, ``projjson`` or ``wkt2`` fields (not necessarily EPSG:4326).
193231
Ideally, this will be represented by a Polygon with five coordinates, as the
194232
item in the asset data CRS should be a square aligned to the original CRS grid.
195233
"""
@@ -204,7 +242,7 @@ def bbox(self) -> list[float] | None:
204242
"""Get or sets the bounding box of the assets represented by this item in the
205243
asset data CRS.
206244
207-
Specified as 4 or 6 coordinates based on the CRS defined in the ``epsg``,
245+
Specified as 4 or 6 coordinates based on the CRS defined in the ``code``,
208246
``projjson`` or ``wkt2`` properties. First two numbers are coordinates of the
209247
lower left corner, followed by coordinates of upper right corner, e.g.,
210248
``[west, south, east, north]``, ``[xmin, ymin, xmax, ymax]``,
@@ -382,16 +420,32 @@ class SummariesProjectionExtension(SummariesExtension):
382420
defined in the :stac-ext:`Projection Extension <projection>`.
383421
"""
384422

423+
@property
424+
def code(self) -> list[str] | None:
425+
"""Get or sets the summary of :attr:`ProjectionExtension.code` values
426+
for this Collection.
427+
"""
428+
return self.summaries.get_list(CODE_PROP)
429+
430+
@code.setter
431+
def code(self, v: list[str] | None) -> None:
432+
self._set_summary(CODE_PROP, v)
433+
385434
@property
386435
def epsg(self) -> list[int] | None:
387-
"""Get or sets the summary of :attr:`ProjectionExtension.epsg` values
436+
"""Get the summary of :attr:`ProjectionExtension.epsg` values
388437
for this Collection.
389438
"""
390-
return self.summaries.get_list(EPSG_PROP)
439+
if self.code is None:
440+
return None
441+
return [int(code.replace("EPSG:", "")) for code in self.code if "EPSG:" in code]
391442

392443
@epsg.setter
393444
def epsg(self, v: list[int] | None) -> None:
394-
self._set_summary(EPSG_PROP, v)
445+
if v is None:
446+
self.code = None
447+
else:
448+
self.code = [f"EPSG:{epsg}" for epsg in v]
395449

396450

397451
class ProjectionExtensionHooks(ExtensionHooks):
@@ -401,7 +455,27 @@ class ProjectionExtensionHooks(ExtensionHooks):
401455
"projection",
402456
*[uri for uri in SCHEMA_URIS if uri != SCHEMA_URI],
403457
}
458+
pre_2 = {
459+
"proj",
460+
"projection",
461+
"https://stac-extensions.github.io/projection/v1.0.0/schema.json",
462+
"https://stac-extensions.github.io/projection/v1.1.0/schema.json",
463+
}
404464
stac_object_types = {pystac.STACObjectType.ITEM}
405465

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

407481
PROJECTION_EXTENSION_HOOKS: ExtensionHooks = ProjectionExtensionHooks()

pystac/item.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ def ext(self) -> ItemExt:
524524
525525
Example::
526526
527-
item.ext.proj.epsg = 4326
527+
item.ext.proj.code = "EPSG:4326"
528528
"""
529529
from pystac.extensions.ext import ItemExt
530530

pystac/static/fields-normalized.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

scripts/pull-static

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
set -e
44

5-
VERSION="1.0.0-rc.1"
5+
VERSION="1.5.0" #v1.5.0-beta.2 should work or no??
66
SRC="https://cdn.jsdelivr.net/npm/@radiantearth/stac-fields@$VERSION/fields-normalized.json"
77
HERE=$(dirname "$0")
88
DEST=$(dirname "$HERE")/pystac/static/fields-normalized.json

0 commit comments

Comments
 (0)