Skip to content

Commit

Permalink
Implement SAR extension. stac-utils#33
Browse files Browse the repository at this point in the history
  • Loading branch information
schwehr committed Oct 21, 2020
1 parent eaf8b42 commit f843e88
Show file tree
Hide file tree
Showing 3 changed files with 319 additions and 1 deletion.
3 changes: 2 additions & 1 deletion pystac/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class STACError(Exception):
import pystac.extensions.label
import pystac.extensions.pointcloud
import pystac.extensions.projection
import pystac.extensions.sar
import pystac.extensions.single_file_stac
import pystac.extensions.timestamps
import pystac.extensions.version
Expand All @@ -42,7 +43,7 @@ class STACError(Exception):
STAC_EXTENSIONS = extensions.base.RegisteredSTACExtensions([
extensions.eo.EO_EXTENSION_DEFINITION, extensions.label.LABEL_EXTENSION_DEFINITION,
extensions.pointcloud.POINTCLOUD_EXTENSION_DEFINITION,
extensions.projection.PROJECTION_EXTENSION_DEFINITION,
extensions.projection.PROJECTION_EXTENSION_DEFINITION, extensions.sar.SAR_EXTENSION_DEFINITION,
extensions.single_file_stac.SFS_EXTENSION_DEFINITION,
extensions.timestamps.TIMESTAMPS_EXTENSION_DEFINITION,
extensions.version.VERSION_EXTENSION_DEFINITION, extensions.view.VIEW_EXTENSION_DEFINITION
Expand Down
218 changes: 218 additions & 0 deletions pystac/extensions/sar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
"""Implement the STAC Synthetic-Aperture Radar (SAR) Extension.
https://github.com/radiantearth/stac-spec/tree/dev/extensions/sar
"""

# TODO(schwehr): Document.

import enum
from typing import List, Optional, TypeVar

import pystac
from pystac import Extensions
from pystac import item
from pystac.extensions import base

# Required
INSTRUMENT_MODE = 'sar:instrument_mode'
FREQUENCY_BAND = 'sar:frequency_band'
POLARIZATIONS = 'sar:polarizations'
PRODUCT_TYPE = 'sar:product_type'

# Not required
CENTER_FREQUENCY = 'sar:center_frequency'
RESOLUTION_RANGE = 'sar:resolution_range'
RESOLUTION_AZIMUTH = 'sar:resolution_azimuth'
PIXEL_SPACING_RANGE = 'sar:pixel_spacing_range'
PIXEL_SPACING_AZIMUTH = 'sar:pixel_spacing_azimuth'
LOOKS_RANGE = 'sar:looks_range'
LOOKS_AZIMUTH = 'sar:looks_azimuth'
LOOKS_EQUIVALENT_NUMBER = 'sar:looks_equivalent_number'
OBSERVATION_DIRECTION = 'sar:observation_direction'

SarItemExtType = TypeVar('SarItemExtType')


class FrequencyBand(enum.Enum):
P = 'P'
L = 'L'
S = 'S'
C = 'C'
X = 'X'
KU = 'Ku'
K = 'K'
KA = 'Ka'


class Polarization(enum.Enum):
HH = 'HH'
VV = 'VV'
HV = 'HV'
VH = 'VH'


class ObservationDirection(enum.Enum):
LEFT = 'left'
RIGHT = 'right'


class SarItemExt(base.ItemExtension):
"""Add sar properties to a STAC Item."""
def __init__(self, an_item: item.Item) -> None:
self.item = an_item

def apply(self,
instrument_mode: str,
frequency_band: str,
polarizations: List[Polarization],
product_type: str,
center_frequency: Optional[float] = None,
resolution_range: Optional[float] = None,
resolution_azimuth: Optional[float] = None,
pixel_spacing_range: Optional[float] = None,
pixel_spacing_azimuth: Optional[float] = None,
looks_range: Optional[int] = None,
looks_azimuth: Optional[int] = None,
looks_equivalent_number: Optional[float] = None,
observation_direction: Optional[ObservationDirection] = None):
self.instrument_mode = instrument_mode
self.frequency_band = frequency_band
self.polarizations = polarizations
self.product_type = product_type
if center_frequency:
self.center_frequency = center_frequency
if resolution_range:
self.resolution_range = resolution_range
if resolution_azimuth:
self.resolution_azimuth = resolution_azimuth
if pixel_spacing_range:
self.pixel_spacing_range = pixel_spacing_range
if pixel_spacing_azimuth:
self.pixel_spacing_azimuth = pixel_spacing_azimuth
if looks_range:
self.looks_range = looks_range
if looks_azimuth:
self.looks_azimuth = looks_azimuth
if looks_equivalent_number:
self.looks_equivalent_number = looks_equivalent_number
if observation_direction:
self.observation_direction = observation_direction

@classmethod
def from_item(cls: SarItemExtType, an_item: item.Item) -> SarItemExtType:
return cls(an_item)

@classmethod
def _object_links(cls) -> List:
return []

@property
def instrument_mode(self) -> str:
return self.item.properties.get(INSTRUMENT_MODE)

@instrument_mode.setter
def instrument_mode(self, v: str) -> None:
self.item.properties[INSTRUMENT_MODE] = v

@property
def frequency_band(self) -> FrequencyBand:
return FrequencyBand(self.item.properties.get(FREQUENCY_BAND))

@frequency_band.setter
def frequency_band(self, v: FrequencyBand) -> None:
self.item.properties[FREQUENCY_BAND] = v.value

@property
def polarizations(self) -> List[Polarization]:
return [Polarization(v) for v in self.item.properties.get(POLARIZATIONS)]

@polarizations.setter
def polarizations(self, values: List[Polarization]) -> None:
if not isinstance(values, list):
raise pystac.STACError(f'polarizations must be a list. Invalid "{values}"')
self.item.properties[POLARIZATIONS] = [v.value for v in values]

@property
def product_type(self) -> str:
return self.item.properties.get(PRODUCT_TYPE)

@product_type.setter
def product_type(self, v: str) -> None:
self.item.properties[PRODUCT_TYPE] = v

@property
def center_frequency(self) -> float:
return self.item.properties.get(CENTER_FREQUENCY)

@center_frequency.setter
def center_frequency(self, v: float) -> None:
self.item.properties[CENTER_FREQUENCY] = v

@property
def resolution_range(self) -> float:
return self.item.properties.get(RESOLUTION_RANGE)

@resolution_range.setter
def resolution_range(self, v: float) -> None:
self.item.properties[RESOLUTION_RANGE] = v

@property
def resolution_azimuth(self) -> float:
return self.item.properties.get(RESOLUTION_AZIMUTH)

@resolution_azimuth.setter
def resolution_azimuth(self, v: float) -> None:
self.item.properties[RESOLUTION_AZIMUTH] = v

@property
def pixel_spacing_range(self) -> float:
return self.item.properties.get(PIXEL_SPACING_RANGE)

@pixel_spacing_range.setter
def pixel_spacing_range(self, v: float) -> None:
self.item.properties[PIXEL_SPACING_RANGE] = v

@property
def pixel_spacing_azimuth(self) -> float:
return self.item.properties.get(PIXEL_SPACING_AZIMUTH)

@pixel_spacing_azimuth.setter
def pixel_spacing_azimuth(self, v: float) -> None:
self.item.properties[PIXEL_SPACING_AZIMUTH] = v

@property
def looks_range(self) -> int:
return self.item.properties.get(LOOKS_RANGE)

@looks_range.setter
def looks_range(self, v: int) -> None:
self.item.properties[LOOKS_RANGE] = v

@property
def looks_azimuth(self) -> int:
return self.item.properties.get(LOOKS_AZIMUTH)

@looks_azimuth.setter
def looks_azimuth(self, v: int) -> None:
self.item.properties[LOOKS_AZIMUTH] = v

@property
def looks_equivalent_number(self) -> float:
return self.item.properties.get(LOOKS_EQUIVALENT_NUMBER)

@looks_equivalent_number.setter
def looks_equivalent_number(self, v: float) -> None:
self.item.properties[LOOKS_EQUIVALENT_NUMBER] = v

@property
def observation_direction(self) -> ObservationDirection:
return ObservationDirection(self.item.properties.get(OBSERVATION_DIRECTION))

@observation_direction.setter
def observation_direction(self, v: ObservationDirection) -> None:
self.item.properties[OBSERVATION_DIRECTION] = v.value


SAR_EXTENSION_DEFINITION = base.ExtensionDefinition(Extensions.SAR, [
base.ExtendedObject(item.Item, SarItemExt),
])
99 changes: 99 additions & 0 deletions tests/extensions/test_sar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Tests for pystac.extensions.sar."""

import datetime
import unittest

import pystac
from pystac.extensions import sar


def make_item() -> pystac.Item:
asset_id = 'my/items/2011'
start = datetime.datetime(2020, 11, 7)
item = pystac.Item(id=asset_id, geometry=None, bbox=None, datetime=start, properties={})

item.ext.enable(pystac.Extensions.SAR)
return item


class SarItemExtTest(unittest.TestCase):
def setUp(self):
super().setUp()
self.item = make_item()
self.item.ext.enable(pystac.Extensions.SAR)

def test_stac_extensions(self):
self.assertEqual([pystac.Extensions.SAR], self.item.stac_extensions)

def test_required(self):
mode = 'Nonesense mode'
frequency_band = sar.FrequencyBand.P
polarizations = [sar.Polarization.HV, sar.Polarization.VH]
product_type = 'Some product'
self.item.ext.sar.apply(mode, frequency_band, polarizations, product_type)
self.assertEqual(mode, self.item.ext.sar.instrument_mode)
self.assertIn(sar.INSTRUMENT_MODE, self.item.properties)

self.assertEqual(frequency_band, self.item.ext.sar.frequency_band)
self.assertIn(sar.FREQUENCY_BAND, self.item.properties)

self.assertEqual(polarizations, self.item.ext.sar.polarizations)
self.assertIn(sar.POLARIZATIONS, self.item.properties)

self.assertEqual(product_type, self.item.ext.sar.product_type)
self.assertIn(sar.PRODUCT_TYPE, self.item.properties)

self.item.validate()

def test_all(self):
mode = 'WV'
frequency_band = sar.FrequencyBand.KA
polarizations = [sar.Polarization.VV, sar.Polarization.HH]
product_type = 'Some product'
center_frequency = 1.2
resolution_range = 3.1
resolution_azimuth = 4.1
pixel_spacing_range = 5.1
pixel_spacing_azimuth = 6.1
looks_range = 7
looks_azimuth = 8
looks_equivalent_number = 9.1
observation_direction = sar.ObservationDirection.LEFT

self.item.ext.sar.apply(mode, frequency_band, polarizations, product_type, center_frequency,
resolution_range, resolution_azimuth, pixel_spacing_range,
pixel_spacing_azimuth, looks_range, looks_azimuth,
looks_equivalent_number, observation_direction)

self.assertEqual(center_frequency, self.item.ext.sar.center_frequency)
self.assertIn(sar.CENTER_FREQUENCY, self.item.properties)

self.assertEqual(resolution_range, self.item.ext.sar.resolution_range)
self.assertIn(sar.RESOLUTION_RANGE, self.item.properties)

self.assertEqual(resolution_azimuth, self.item.ext.sar.resolution_azimuth)
self.assertIn(sar.RESOLUTION_AZIMUTH, self.item.properties)

self.assertEqual(pixel_spacing_range, self.item.ext.sar.pixel_spacing_range)
self.assertIn(sar.PIXEL_SPACING_RANGE, self.item.properties)

self.assertEqual(pixel_spacing_azimuth, self.item.ext.sar.pixel_spacing_azimuth)
self.assertIn(sar.PIXEL_SPACING_AZIMUTH, self.item.properties)

self.assertEqual(looks_range, self.item.ext.sar.looks_range)
self.assertIn(sar.LOOKS_RANGE, self.item.properties)

self.assertEqual(looks_azimuth, self.item.ext.sar.looks_azimuth)
self.assertIn(sar.LOOKS_AZIMUTH, self.item.properties)

self.assertEqual(looks_equivalent_number, self.item.ext.sar.looks_equivalent_number)
self.assertIn(sar.LOOKS_EQUIVALENT_NUMBER, self.item.properties)

self.assertEqual(observation_direction, self.item.ext.sar.observation_direction)
self.assertIn(sar.OBSERVATION_DIRECTION, self.item.properties)

self.item.validate()


if __name__ == '__main__':
unittest.main()

0 comments on commit f843e88

Please sign in to comment.