# -*- coding: utf-8 -*-
# Copyright 2022, SERTIT-ICube - France, https://sertit.unistra.fr/
# This file is part of eoreader project
# https://github.com/sertit/eoreader
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Vision-1 products.
See `here <http://www.engesat.com.br/wp-content/uploads/S5-ST-73-1-CN_2_9-Spec-Format-Produits-SPOT.pdf>`_
for more information.
"""
import logging
import time
from datetime import date, datetime, timedelta
from enum import unique
from pathlib import Path
from typing import Union
import geopandas as gpd
import numpy as np
import xarray as xr
from cloudpathlib import CloudPath
from lxml import etree
from rasterio import crs as riocrs
from sertit import files, rasters, vectors
from sertit.misc import ListEnum
from shapely.geometry import Polygon, box
from eoreader import cache
from eoreader.bands import BandNames, SpectralBand
from eoreader.bands import spectral_bands as spb
from eoreader.exceptions import InvalidProductError
from eoreader.products import VhrProduct
from eoreader.products.optical.optical_product import RawUnits
from eoreader.reader import Constellation
from eoreader.stac import GSD, ID, NAME, WV_MAX, WV_MIN
from eoreader.utils import DATETIME_FMT, EOREADER_NAME, simplify
LOGGER = logging.getLogger(EOREADER_NAME)
_DIMAP_BAND_MTD = {
spb.PAN: "PAN",
spb.GREEN: "XS1",
spb.RED: "XS2",
spb.NIR: "XS3",
spb.NARROW_NIR: "XS3",
spb.SWIR_1: "XS4",
}
# https://spot.cnes.fr/sites/default/files/migration/smsc/spot/calibration_synthesis_SPOT1245_ed1.pdf (3.3)
# HRVIR 1, 2
_SPOT4_E0 = {
spb.PAN: [1570.2, 1589],
spb.GREEN: [1842.9, 1850.9],
spb.RED: [1570.2, 1589],
spb.NIR: [1052.1, 1054.8],
spb.NARROW_NIR: [1052.1, 1054.8],
spb.SWIR_1: [235.84, 241.93],
}
# HRG 1, 2
_SPOT5_E0 = {
spb.PAN: [1764.2, 1775],
spb.GREEN: [1859.8, 1859.8],
spb.RED: [1575.3, 1577.6],
spb.NIR: [1043.9, 1048.2],
spb.NARROW_NIR: [1043.9, 1048.2],
spb.SWIR_1: [238.87, 237.78],
}
"""
The values of the normalized solar irradiance have been computed using WMO (World Meteorogical Organization) spectral solar irradiance.
"""
[docs]@unique
class Spot4BandCombination(ListEnum):
"""
Band combination for SPOT4 data
See `this <https://www.intelligence-airbusds.com/files/pmedia/public/r451_9_resolutionspectralmodes_uk_sept2010.pdf>`_ for more information.
"""
M = "M"
"""
"M" for Spot 4 PAN product (10 m)
"""
X = "X"
"""
"X" for Spot 4 multispectral product (3 bands, without SWIR) (20 m)
"""
I = "I" # noqa
"""
"I" for Spot 4 multispectral product (4 bands, with SWIR) (20 m)
"""
MX = "M+X"
"""
"M+X" for Spot 4 merge product (3 bands, without SWIR) (10 m)
"""
MI = "M+I"
"""
"M+I" for Spot 4 merge product (4 bands, with SWIR) (10 m)
"""
[docs]@unique
class Spot5BandCombination(ListEnum):
"""
Band combination for SPOT4/5 data
See `this <https://www.intelligence-airbusds.com/files/pmedia/public/r451_9_resolutionspectralmodes_uk_sept2010.pdf>`_ for more information.
"""
T = "T"
"""
"T" for supermode Spot data (2.5 m)
"""
HM = "HM"
"""
"HM" for Spot-5 PAN data (5 m)
"""
X = "X"
"""
"X" for 3 bands extracted from 4 bands (10 m)
"""
J = "J"
"""
"J" for Spot 5 multipectral product (4 bands, with SWIR) (10 m)
"""
HMX = "HM+X"
"""
"HM+X" for Spot 5 merge product (3 bands, without SWIR) (5 m)
"""
TX = "T+X"
"""
"T+X" for Spot 5 supermode and multipectral merge product (3 bands, without SWIR) (2.5 m)
"""
[docs]@unique
class Spot45ProductType(ListEnum):
"""
Product Type for SPOT4/5 data
See `here <http://www.engesat.com.br/wp-content/uploads/S5-ST-73-1-CN_2_9-Spec-Format-Produits-SPOT.pdf>`_ for more information.
"""
L0 = "0"
"""
Level-0
"""
L1A = "1A"
"""
Level-1A
"""
L1B = "1B"
"""
Level-1B
"""
L2A = "2A"
"""
Level-2A
"""
[docs]class Spot45Product(VhrProduct):
"""
Class of SPOT4/5 products.
See `here <http://www.engesat.com.br/wp-content/uploads/S5-ST-73-1-CN_2_9-Spec-Format-Produits-SPOT.pdf>`_
for more information.
"""
def _pre_init(self, **kwargs) -> None:
"""
Function used to pre_init the products
(setting needs_extraction and so on)
"""
root, _ = self.read_mtd()
mission_idx = int(root.findtext(".//MISSION_INDEX"))
if mission_idx == 4:
self._supermode_res = None
self._pan_res = 10.0
self._ms_res = 20.0
self.constellation = Constellation.SPOT4
elif mission_idx == 5:
self._supermode_res = 2.5
self._pan_res = 5.0
self._ms_res = 10.0
self.constellation = Constellation.SPOT5
else:
raise InvalidProductError("Mission index should be 4 or 5.")
self.needs_extraction = False
self._use_filename = True
self._proj_prod_type = [Spot45ProductType.L0]
# Raw units
rad_proc = root.findtext(".//RADIOMETRIC_PROCESSING").upper()
if rad_proc == "REFLECTANCE":
self._raw_units = RawUnits.REFL
elif rad_proc in "BASIC":
self._raw_units = RawUnits.DN
else:
self._raw_units = RawUnits.NONE
# Post init done by the super class
super()._pre_init(**kwargs)
def _set_band_combi(self) -> None:
"""
Set Band combination
"""
root, _ = self.read_mtd()
band_combi = root.findtext(".//SPECTRAL_PROCESSING")
if self.constellation == Constellation.SPOT4:
self.band_combi = Spot4BandCombination.from_value(band_combi)
else:
self.band_combi = Spot5BandCombination.from_value(band_combi)
def _post_init(self, **kwargs) -> None:
"""
Function used to post_init the products
(setting sensor type, band names and so on)
"""
if self.band_combi is None:
self._set_band_combi()
# Post init done by the super class
super()._post_init(**kwargs)
def _get_resolution(self) -> float:
"""
Get product default resolution (in meters)
"""
# Not Pansharpened images
if self.band_combi in [
Spot4BandCombination.X,
Spot4BandCombination.I,
Spot5BandCombination.X,
Spot5BandCombination.J,
]:
return self._ms_res
# Pansharpened images
elif self.band_combi in [
Spot4BandCombination.M,
Spot4BandCombination.MX,
Spot4BandCombination.MI,
Spot5BandCombination.HM,
Spot5BandCombination.HMX,
]:
return self._pan_res
# Supermode images
else:
return self._supermode_res
def _set_instrument(self) -> None:
"""
Set instrument
SPOT-4: https://space-test.oscar.wmo.int/oscar-test/instruments/view/hrvir
SPOT-5: https://space-test.oscar.wmo.int/oscar-test/instruments/view/hrg
"""
if self.constellation == Constellation.SPOT4:
self.instrument = "HRVIR"
else:
self.instrument = "HRG"
def _set_product_type(self) -> None:
"""
Set products type
See Vision-1_web_201906.pdf for more information.
"""
if self.product_type is None:
# Get MTD XML file
root, _ = self.read_mtd()
proc_lvl = root.findtext(".//SCENE_PROCESSING_LEVEL")
self.product_type = Spot45ProductType.from_value(proc_lvl)
# Manage not orthorectified product
if self.product_type == Spot45ProductType.L0:
self.is_ortho = False
def _map_bands(self) -> None:
"""
Map bands
"""
if self.constellation == Constellation.SPOT4:
# Create spectral bands
pan = SpectralBand(
eoreader_name=spb.PAN,
**{NAME: "PAN", ID: 1, GSD: self._pan_res, WV_MIN: 610, WV_MAX: 680},
)
green = SpectralBand(
eoreader_name=spb.GREEN,
**{NAME: "GREEN", ID: 3, GSD: self._ms_res, WV_MIN: 500, WV_MAX: 590},
)
red = SpectralBand(
eoreader_name=spb.RED,
**{NAME: "RED", ID: 2, GSD: self._ms_res, WV_MIN: 610, WV_MAX: 680},
)
nir = SpectralBand(
eoreader_name=spb.NIR,
**{NAME: "NIR", ID: 1, GSD: self._ms_res, WV_MIN: 790, WV_MAX: 890},
)
swir1 = SpectralBand(
eoreader_name=spb.SWIR_1,
**{
NAME: "SWIR_1",
ID: 4,
GSD: self._ms_res,
WV_MIN: 1580,
WV_MAX: 1750,
},
)
else:
# Create spectral bands
pan = SpectralBand(
eoreader_name=spb.PAN,
**{NAME: "PAN", ID: 1, GSD: self._pan_res, WV_MIN: 490, WV_MAX: 690},
)
green = SpectralBand(
eoreader_name=spb.GREEN,
**{NAME: "GREEN", ID: 3, GSD: self._ms_res, WV_MIN: 490, WV_MAX: 610},
)
red = SpectralBand(
eoreader_name=spb.RED,
**{NAME: "RED", ID: 2, GSD: self._ms_res, WV_MIN: 610, WV_MAX: 680},
)
nir = SpectralBand(
eoreader_name=spb.NIR,
**{NAME: "NIR", ID: 1, GSD: self._ms_res, WV_MIN: 780, WV_MAX: 890},
)
swir1 = SpectralBand(
eoreader_name=spb.SWIR_1,
**{NAME: "SWIR_1", ID: 4, GSD: 20.0, WV_MIN: 1580, WV_MAX: 1750},
)
# Manage bands of the product
if self.band_combi in [Spot4BandCombination.M, Spot5BandCombination.HM]:
self.bands.map_bands({spb.PAN: pan})
elif self.band_combi in [
Spot4BandCombination.X,
Spot5BandCombination.X,
]:
self.bands.map_bands(
{
spb.GREEN: green,
spb.RED: red,
spb.NIR: nir,
spb.NARROW_NIR: nir,
}
)
elif self.band_combi in [
Spot4BandCombination.MX,
Spot5BandCombination.HMX,
]:
self.bands.map_bands(
{
spb.GREEN: green.update(gsd=self._pan_res),
spb.RED: red.update(gsd=self._pan_res),
spb.NIR: nir.update(gsd=self._pan_res),
spb.NARROW_NIR: nir.update(gsd=self._pan_res),
}
)
elif self.band_combi == Spot5BandCombination.T:
self.bands.map_bands({spb.PAN: pan.update(gsd=self._supermode_res)})
elif self.band_combi == Spot5BandCombination.TX:
self.bands.map_bands(
{
spb.GREEN: green.update(gsd=self._supermode_res),
spb.RED: red.update(gsd=self._supermode_res),
spb.NIR: nir.update(gsd=self._supermode_res),
spb.NARROW_NIR: nir.update(gsd=self._supermode_res),
}
)
elif self.band_combi in [
Spot4BandCombination.I,
Spot5BandCombination.J,
]:
self.bands.map_bands(
{
spb.GREEN: green,
spb.RED: red,
spb.NIR: nir,
spb.NARROW_NIR: nir,
spb.SWIR_1: swir1,
}
)
elif self.band_combi == Spot4BandCombination.MI:
self.bands.map_bands(
{
spb.GREEN: green.update(gsd=self._pan_res),
spb.RED: red.update(gsd=self._pan_res),
spb.NIR: nir.update(gsd=self._pan_res),
spb.NARROW_NIR: nir.update(gsd=self._pan_res),
spb.SWIR_1: swir1.update(gsd=self._pan_res),
}
)
else:
raise InvalidProductError(
f"Unusual band combination: {self.band_combi.name}"
)
[docs] @cache
def crs(self) -> riocrs.CRS:
"""
Get UTM projection of the tile
.. code-block:: python
>>> from eoreader.reader import Reader
>>> path = r"IMG_PHR1B_PMS_001"
>>> prod = Reader().open(path)
>>> prod.crs()
CRS.from_epsg(32618)
Returns:
rasterio.crs.CRS: CRS object
"""
raw_crs = self._get_raw_crs()
if raw_crs.is_projected:
utm = raw_crs
else:
# Open metadata
root, _ = self.read_mtd()
# Open the Bounding_Polygon
vertices = list(root.iterfind(".//Dataset_Frame/Vertex"))
# Get the mean lon lat
lon = float(np.mean([float(v.findtext("FRAME_LON")) for v in vertices]))
lat = float(np.mean([float(v.findtext("FRAME_LAT")) for v in vertices]))
# Compute UTM crs from center long/lat
utm = vectors.corresponding_utm_projection(lon, lat)
utm = riocrs.CRS.from_string(utm)
return utm
def _get_raw_crs(self) -> riocrs.CRS:
"""
Get raw CRS of the tile
Returns:
rasterio.crs.CRS: CRS object
"""
# Open metadata
root, _ = self.read_mtd()
# Get CRS
crs_name = root.findtext(".//HORIZONTAL_CS_CODE")
if not crs_name:
raise InvalidProductError(
"Cannot find the CRS name (from GEOGRAPHIC_CS_CODE or HORIZONTAL_CS_CODE) type in the metadata file"
)
return riocrs.CRS.from_string(crs_name)
[docs] @cache
def extent(self, **kwargs) -> gpd.GeoDataFrame:
"""
Get UTM extent of the tile.
Returns:
gpd.GeoDataFrame: Extent in UTM
"""
# TODO: SAME AS DIMAP
# Get MTD XML file
root, _ = self.read_mtd()
# Compute extent corners
corners = [
[float(vertex.findtext("FRAME_LON")), float(vertex.findtext("FRAME_LAT"))]
for vertex in root.iterfind(".//Dataset_Frame/Vertex")
]
# When PRJ, Dataset_Frame is the footprint
ds_frame = gpd.GeoDataFrame(
geometry=[Polygon(corners)],
crs=vectors.WGS84,
).to_crs(self.crs())
extent = gpd.GeoDataFrame(
geometry=[box(*ds_frame.total_bounds)],
crs=self.crs(),
)
return extent
[docs] def get_datetime(self, as_datetime: bool = False) -> Union[str, datetime]:
"""
Get the product's acquisition datetime, with format :code:`YYYYMMDDTHHMMSS` <-> :code:`%Y%m%dT%H%M%S`
.. code-block:: python
>>> from eoreader.reader import Reader
>>> path = r"IMG_PHR1B_PMS_001"
>>> prod = Reader().open(path)
>>> prod.get_datetime(as_datetime=True)
datetime.datetime(2020, 5, 11, 2, 31, 58)
>>> prod.get_datetime(as_datetime=False)
'20200511T023158'
Args:
as_datetime (bool): Return the date as a datetime.datetime. If false, returns a string.
Returns:
Union[str, datetime.datetime]: Its acquisition datetime
"""
# TODO: SAME AS DIMAP
if self.datetime is None:
# Get MTD XML file
root, _ = self.read_mtd()
date_str = root.findtext(".//IMAGING_DATE")
time_str = root.findtext(".//IMAGING_TIME")
if not date_str or not time_str:
raise InvalidProductError(
"Cannot find the product imaging date and time in the metadata file."
)
# Convert to datetime
date_dt = date.fromisoformat(date_str)
time_dt = time.strptime(time_str, "%H:%M:%S")
date_str = (
f"{date_dt.strftime('%Y%m%d')}T{time.strftime('%H%M%S', time_dt)}"
)
if as_datetime:
date_str = datetime.strptime(date_str, DATETIME_FMT)
else:
date_str = self.datetime
if not as_datetime:
date_str = date_str.strftime(DATETIME_FMT)
return date_str
def _get_constellation(self) -> Constellation:
""" Getter of the constellation """
if self.split_name[0] == "SP04":
const = Constellation.SPOT4
elif self.split_name[0] == "SP05":
const = Constellation.SPOT5
else:
raise InvalidProductError(
f"Invalid name: {self.name}, should start with SP04 or SP05."
)
return const
def _get_name_constellation_specific(self) -> str:
"""
Set product real name from metadata
Returns:
str: True name of the product (from metadata)
"""
# Get MTD XML file
root, _ = self.read_mtd()
# Mission index
mission_idx = int(root.findtext(".//MISSION_INDEX"))
# Instrument
instrument = "HIR" if self.constellation == Constellation.SPOT4 else "HRG"
# Product type
self._set_band_combi() # Not set yet
band_combi = f"{self.band_combi.name:_<4}"
# Datetimes
dt = self.get_datetime(as_datetime=True)
start_dt = (dt - timedelta(seconds=4)).strftime(DATETIME_FMT)
end_dt = (dt + timedelta(seconds=4)).strftime(DATETIME_FMT)
# Unknown data
digit = 3 # TODO: what's this ?
suffix = "TOU_1234_eord"
# Create name
name = f"SP0{mission_idx}_{instrument}_{band_combi}_{digit}_{start_dt}_{end_dt}_{suffix}"
return name
[docs] @cache
def get_mean_sun_angles(self) -> (float, float):
"""
Get Mean Sun angles (Azimuth and Zenith angles)
.. code-block:: python
>>> from eoreader.reader import Reader
>>> path = r"IMG_PHR1A_PMS_001"
>>> prod = Reader().open(path)
>>> prod.get_mean_sun_angles()
(45.6624568841367, 30.219881316357643)
Returns:
(float, float): Mean Azimuth and Zenith angle
"""
# Get MTD XML file
root, _ = self.read_mtd()
# Open zenith and azimuth angle
elev_angle = float(root.findtext(".//SUN_ELEVATION"))
azimuth_angle = float(root.findtext(".//SUN_AZIMUTH"))
# From elevation to zenith
zenith_angle = 90.0 - elev_angle
return azimuth_angle, zenith_angle
[docs] @cache
def get_mean_viewing_angles(self) -> (float, float, float):
"""
Get Mean Viewing angles (azimuth, off-nadir and incidence angles)
.. code-block:: python
>>> from eoreader.reader import Reader
>>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
>>> prod = Reader().open(path)
>>> prod.get_mean_viewing_angles()
Returns:
(float, float, float): Mean azimuth, off-nadir and incidence angles
"""
# Get MTD XML file
root, _ = self.read_mtd()
# Open incidence and off-nadir angle
az = None
try:
incidence_angle = abs(float(root.findtext(".//INCIDENCE_ANGLE")))
except TypeError:
raise InvalidProductError(
"INCIDENCE_ANGLE or VIEWING_ANGLE not found in metadata!"
)
if self.constellation == Constellation.SPOT5:
try:
off_nadir = abs(float(root.findtext(".//VIEWING_ANGLE")))
except TypeError:
raise InvalidProductError("VIEWING_ANGLE not found in metadata!")
else:
# See: https://earth.esa.int/eogateway/missions/spot-4
orbit_height = 832000
earth_radius = 6378137
orbit_coeff = (earth_radius + orbit_height) / earth_radius
off_nadir = np.rad2deg(
np.arcsin(np.sin(np.deg2rad(90 - incidence_angle)) / orbit_coeff)
)
return az, off_nadir, incidence_angle
def _to_reflectance(
self,
band_arr: xr.DataArray,
path: Union[Path, CloudPath],
band: BandNames,
**kwargs,
) -> xr.DataArray:
"""
Converts band to reflectance
Args:
band_arr (xr.DataArray): Band array to convert
path (Union[CloudPath, Path]): Band path
band (BandNames): Band to read
**kwargs: Other keywords
Returns:
xr.DataArray: Band in reflectance
"""
if self._raw_units == RawUnits.REFL:
# Compute the correct radiometry of the band
original_dtype = band_arr.encoding.get("dtype", band_arr.dtype)
if original_dtype == "uint16":
band_arr /= 10000.0
elif self._raw_units == RawUnits.DN:
# Convert DN into radiance
band_arr = self._dn_to_toa_rad(band_arr, band)
# Convert radiance into reflectance
band_arr = self._toa_rad_to_toa_refl(band_arr, band)
else:
LOGGER.warning(
"The spectral properties of a SEAMLESS radiometric processed image "
"cannot be retrieved since the initial images have undergone "
"several radiometric adjustments for aesthetic rendering."
"Returned as is."
)
# To float32
if band_arr.dtype != np.float32:
band_arr = band_arr.astype(np.float32)
return band_arr
@cache
def _read_mtd(self) -> (etree._Element, dict):
"""
Read metadata and outputs the metadata XML root and its namespaces as a dict
Returns:
(etree._Element, dict): Metadata XML root and its namespaces as a dict
"""
mtd_from_path = "METADATA.DIM"
mtd_archived = r"METADATA\.DIM"
return self._read_mtd_xml(mtd_from_path, mtd_archived)
def _has_cloud_band(self, band: BandNames) -> bool:
"""
Does this product has the specified cloud band ?
"""
return False
def _open_clouds(
self,
bands: list,
resolution: float = None,
size: Union[list, tuple] = None,
**kwargs,
) -> dict:
"""
Load cloud files as xarrays.
Args:
bands (list): List of the wanted bands
resolution (int): Band resolution in meters
size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided.
kwargs: Additional arguments
Returns:
dict: Dictionary {band_name, band_xarray}
"""
return {}
def _get_tile_path(self) -> Union[CloudPath, Path]:
"""
Get the DIMAP filepath
Returns:
Union[CloudPath, Path]: DIMAP filepath
"""
return self._get_path("IMAGERY", "TIF")
def _dn_to_toa_rad(self, dn_arr: xr.DataArray, band: BandNames) -> xr.DataArray:
"""
Compute DN to TOA radiance
Args:
rad_arr (xr.DataArray): DN array
band (BandNames): Band
Returns:
xr.DataArray: TOA Radiance array
"""
band_mtd_str = _DIMAP_BAND_MTD[band]
# Get MTD XML file
root, _ = self.read_mtd()
# Convert DN to TOA radiance
# <MEASURE_DESC>Raw radiometric counts (DN) to TOA Radiance (L). Formulae L=DN/GAIN+BIAS</MEASURE_DESC>
try:
rad_gain = None
rad_bias = None
for br in root.iterfind(".//Spectral_Band_Info"):
if br.findtext("BAND_DESCRIPTION") == band_mtd_str:
rad_gain = float(br.findtext("PHYSICAL_GAIN"))
rad_bias = float(br.findtext("PHYSICAL_BIAS"))
break
if rad_gain is None or rad_bias is None:
raise TypeError
except TypeError:
raise InvalidProductError(
"PHYSICAL_GAIN and PHYSICAL_BIAS from Spectral_Band_Info not found in metadata!"
)
return dn_arr / rad_gain + rad_bias
def _toa_rad_to_toa_refl(
self, rad_arr: xr.DataArray, band: BandNames
) -> xr.DataArray:
"""
Compute TOA reflectance from TOA radiance
See
`here <https://spot.cnes.fr/sites/default/files/migration/smsc/spot/calibration_synthesis_SPOT1245_ed1.pdf>`_
for more information.
Args:
rad_arr (xr.DataArray): TOA Radiance array
band (BandNames): Band
Returns:
xr.DataArray: TOA Reflectance array
"""
# Get MTD XML file
root, _ = self.read_mtd()
# Get the solar irradiance value of raw radiometric Band (in watt/m2/micron)
instrument_idx = int(root.findtext(".//INSTRUMENT_INDEX")) - 1
if self.constellation == Constellation.SPOT4:
e0 = _SPOT4_E0[band][instrument_idx]
else:
e0 = _SPOT5_E0[band][instrument_idx]
# Compute the coefficient converting TOA radiance in TOA reflectance
dt = self._sun_earth_distance_variation()
_, sun_zen = self.get_mean_sun_angles()
rad_sun_zen = np.deg2rad(sun_zen)
# WARNING: d = 1 / sqrt(d(t))
toa_refl_coeff = np.pi / (e0 * dt * np.cos(rad_sun_zen))
# LOGGER.debug(f"rad to refl coeff = {toa_refl_coeff}")
return rad_arr.copy(data=toa_refl_coeff * rad_arr)
[docs] def get_quicklook_path(self) -> str:
"""
Get quicklook path if existing.
Returns:
str: Quicklook path
"""
quicklook_path = None
try:
if self.is_archived:
quicklook_path = files.get_archived_rio_path(
self.path, file_regex=r".*PREVIEW\.JPG"
)
else:
quicklook_path = str(next(self.path.glob("*PREVIEW.JPG")))
except (StopIteration, FileNotFoundError):
LOGGER.warning(f"No quicklook found in {self.condensed_name}")
return quicklook_path
def _get_job_id(self) -> str:
"""
Get VHR job ID
Returns:
str: VHR product ID
"""
# Get MTD XML file
root, _ = self.read_mtd()
return root.findtext(".//JOB_ID")
def _get_condensed_name(self) -> str:
"""
Get PlanetScope products condensed name ({date}_PLD_{product_type}_{band_combi}).
Returns:
str: Condensed name
"""
return f"{self.get_datetime()}_{self.constellation.name}_{self.product_type.name}_{self.band_combi.name}"