Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ProjectionExtension v2 (proj:epsg -> proj:code) #1287

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/tutorials/creating-a-landsat-stac.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -2526,7 +2526,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
"version": "3.11.6"
}
},
"nbformat": 4,
Expand Down
2 changes: 1 addition & 1 deletion pystac/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ def ext(self) -> AssetExt:

Example::

asset.ext.proj.epsg = 4326
asset.ext.proj.code = "EPSG:4326"
"""
from pystac.extensions.ext import AssetExt

Expand Down
29 changes: 20 additions & 9 deletions pystac/extensions/eo.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,19 +656,30 @@ def migrate(
]
del obj["properties"][f"eo:{field}"]

# eo:epsg became proj:epsg
# eo:epsg became proj:epsg in Projection Extension <2.0.0 and became
# proj:code in Projection Extension 2.0.0
eo_epsg = PREFIX + "epsg"
proj_epsg = projection.PREFIX + "epsg"
if eo_epsg in obj["properties"] and proj_epsg not in obj["properties"]:
obj["properties"][proj_epsg] = obj["properties"].pop(eo_epsg)
proj_code = projection.PREFIX + "code"
if (
eo_epsg in obj["properties"]
and proj_epsg not in obj["properties"]
and proj_code not in obj["properties"]
):
obj["stac_extensions"] = obj.get("stac_extensions", [])
if (
projection.ProjectionExtension.get_schema_uri()
not in obj["stac_extensions"]
if set(obj["stac_extensions"]).intersection(
projection.ProjectionExtensionHooks.pre_2
):
obj["stac_extensions"].append(
projection.ProjectionExtension.get_schema_uri()
)
obj["properties"][proj_epsg] = obj["properties"].pop(eo_epsg)
else:
obj["properties"][
proj_code
] = f"EPSG:{obj['properties'].pop(eo_epsg)}"
if not projection.ProjectionExtensionHooks().has_extension(obj):
obj["stac_extensions"].append(
projection.ProjectionExtension.get_schema_uri()
)

if not any(prop.startswith(PREFIX) for prop in obj["properties"]):
obj["stac_extensions"].remove(EOExtension.get_schema_uri())

Expand Down
8 changes: 8 additions & 0 deletions pystac/extensions/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import TYPE_CHECKING, Any

import pystac
from pystac.extensions.base import VERSION_REGEX
from pystac.serialization.identify import STACJSONDescription, STACVersionID

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

def has_extension(self, obj: dict[str, Any]) -> bool:
schema_startswith = VERSION_REGEX.split(self.schema_uri)[0] + "/"
return any(
uri.startswith(schema_startswith) or uri in self.prev_extension_ids
for uri in obj.get("stac_extensions", [])
)

def migrate(
self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription
) -> None:
Expand Down
102 changes: 88 additions & 14 deletions pystac/extensions/projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,20 @@
SummariesExtension,
)
from pystac.extensions.hooks import ExtensionHooks
from pystac.serialization.identify import STACJSONDescription, STACVersionID

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

SCHEMA_URI: str = "https://stac-extensions.github.io/projection/v1.1.0/schema.json"
SCHEMA_URI: str = "https://stac-extensions.github.io/projection/v2.0.0/schema.json"
SCHEMA_URIS: list[str] = [
"https://stac-extensions.github.io/projection/v1.0.0/schema.json",
"https://stac-extensions.github.io/projection/v1.1.0/schema.json",
SCHEMA_URI,
]
PREFIX: str = "proj:"

# Field names
CODE_PROP: str = PREFIX + "code"
EPSG_PROP: str = PREFIX + "epsg"
WKT2_PROP: str = PREFIX + "wkt2"
PROJJSON_PROP: str = PREFIX + "projjson"
Expand Down Expand Up @@ -66,7 +69,9 @@ class ProjectionExtension(

def apply(
self,
epsg: int | None,
*,
epsg: int | None = None,
code: str | None = None,
wkt2: str | None = None,
projjson: dict[str, Any] | None = None,
geometry: dict[str, Any] | None = None,
Expand All @@ -78,7 +83,10 @@ def apply(
"""Applies Projection extension properties to the extended Item.

Args:
epsg : REQUIRED. EPSG code of the datasource.
epsg : Code of the datasource. Example: 4326. One of ``code`` and
``epsg`` must be provided.
code : Code of the datasource. Example: "EPSG:4326". One of ``code`` and
``epsg`` must be provided.
wkt2 : WKT2 string representing the Coordinate Reference
System (CRS) that the ``geometry`` and ``bbox`` fields represent
projjson : PROJJSON dict representing the
Expand All @@ -97,7 +105,15 @@ def apply(
transform : The affine transformation coefficients for
the default grid
"""
self.epsg = epsg
if epsg is not None and code is not None:
raise KeyError(
"Only one of the options ``code`` and ``epsg`` should be specified."
)
elif epsg:
self.epsg = epsg
else:
self.code = code

self.wkt2 = wkt2
self.projjson = projjson
self.geometry = geometry
Expand All @@ -118,11 +134,33 @@ def epsg(self) -> int | None:
It should also be set to ``None`` if a CRS exists, but for which there is no
valid EPSG code.
"""
return self._get_property(EPSG_PROP, int)
if self.code is not None and self.code.startswith("EPSG:"):
return int(self.code.replace("EPSG:", ""))
return None

@epsg.setter
def epsg(self, v: int | None) -> None:
self._set_property(EPSG_PROP, v, pop_if_none=False)
if v is None:
self.code = None
else:
self.code = f"EPSG:{v}"

@property
def code(self) -> str | None:
"""Get or set the code of the datasource.

Added in version 2.0.0 of this extension replacing "proj:epsg".

Projection codes are identified by a string. The `proj <https://proj.org/>`_
library defines projections using "authority:code", e.g., "EPSG:4326" or
"IAU_2015:30100". Different projection authorities may define different
string formats.
"""
return self._get_property(CODE_PROP, str)

@code.setter
def code(self, v: int | None) -> None:
self._set_property(CODE_PROP, v, pop_if_none=False)

@property
def wkt2(self) -> str | None:
Expand Down Expand Up @@ -169,13 +207,13 @@ def crs_string(self) -> str | None:
This string can be used to feed, e.g., ``rasterio.crs.CRS.from_string``.
The string is determined by the following heuristic:

1. If an EPSG code is set, return "EPSG:{code}", else
1. If a code is set, return the code string, else
2. If wkt2 is set, return the WKT string, else,
3. If projjson is set, return the projjson as a string, else,
4. Return None
"""
if self.epsg:
return f"EPSG:{self.epsg}"
if self.code:
return self.code
elif self.wkt2:
return self.wkt2
elif self.projjson:
Expand All @@ -190,7 +228,7 @@ def geometry(self) -> dict[str, Any] | None:
This dict should be formatted according the Polygon object format specified in
`RFC 7946, sections 3.1.6 <https://tools.ietf.org/html/rfc7946>`_,
except not necessarily in EPSG:4326 as required by RFC7946. Specified based on
the ``epsg``, ``projjson`` or ``wkt2`` fields (not necessarily EPSG:4326).
the ``code``, ``projjson`` or ``wkt2`` fields (not necessarily EPSG:4326).
Ideally, this will be represented by a Polygon with five coordinates, as the
item in the asset data CRS should be a square aligned to the original CRS grid.
"""
Expand All @@ -205,7 +243,7 @@ def bbox(self) -> list[float] | None:
"""Get or sets the bounding box of the assets represented by this item in the
asset data CRS.

Specified as 4 or 6 coordinates based on the CRS defined in the ``epsg``,
Specified as 4 or 6 coordinates based on the CRS defined in the ``code``,
``projjson`` or ``wkt2`` properties. First two numbers are coordinates of the
lower left corner, followed by coordinates of upper right corner, e.g.,
``[west, south, east, north]``, ``[xmin, ymin, xmax, ymax]``,
Expand Down Expand Up @@ -383,16 +421,32 @@ class SummariesProjectionExtension(SummariesExtension):
defined in the :stac-ext:`Projection Extension <projection>`.
"""

@property
def code(self) -> list[str] | None:
"""Get or sets the summary of :attr:`ProjectionExtension.code` values
for this Collection.
"""
return self.summaries.get_list(CODE_PROP)

@code.setter
def code(self, v: list[str] | None) -> None:
self._set_summary(CODE_PROP, v)

@property
def epsg(self) -> list[int] | None:
"""Get or sets the summary of :attr:`ProjectionExtension.epsg` values
"""Get the summary of :attr:`ProjectionExtension.epsg` values
for this Collection.
"""
return self.summaries.get_list(EPSG_PROP)
if self.code is None:
return None
return [int(code.replace("EPSG:", "")) for code in self.code if "EPSG:" in code]

@epsg.setter
def epsg(self, v: list[int] | None) -> None:
self._set_summary(EPSG_PROP, v)
if v is None:
self.code = None
else:
self.code = [f"EPSG:{epsg}" for epsg in v]


class ProjectionExtensionHooks(ExtensionHooks):
Expand All @@ -402,7 +456,27 @@ class ProjectionExtensionHooks(ExtensionHooks):
"projection",
*[uri for uri in SCHEMA_URIS if uri != SCHEMA_URI],
}
pre_2 = {
"proj",
"projection",
"https://stac-extensions.github.io/projection/v1.0.0/schema.json",
"https://stac-extensions.github.io/projection/v1.1.0/schema.json",
}
stac_object_types = {pystac.STACObjectType.ITEM}

def migrate(
self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription
) -> None:
if not self.has_extension(obj):
return

# proj:epsg moved to proj:code
if "proj:epsg" in obj["properties"]:
epsg = obj["properties"]["proj:epsg"]
obj["properties"]["proj:code"] = f"EPSG:{epsg}"
del obj["properties"]["proj:epsg"]

super().migrate(obj, version, info)


PROJECTION_EXTENSION_HOOKS: ExtensionHooks = ProjectionExtensionHooks()
2 changes: 1 addition & 1 deletion pystac/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ def ext(self) -> ItemExt:

Example::

item.ext.proj.epsg = 4326
item.ext.proj.code = "EPSG:4326"
"""
from pystac.extensions.ext import ItemExt

Expand Down
Loading
Loading