Source code for eoio.readers.landsat.layout
"""eoio.readers.landsat.layout - Helper for Landsat product layout parsing."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Set
import glob
import re
import os
[docs]
class LandsatLayoutError(ValueError):
"""Raised when a Landsat layout is missing expected files/structure."""
_BAND_TOKEN_RE = re.compile(r"\bB(?P<band>(?:[1-9]))\b") # e.g. B2, B8
[docs]
@dataclass(frozen=True)
class LandsatLayout:
"""
Layout helper for Landsat products (e.g. Landsat 8 OLI/TIRS L1/L2).
Responsible for filesystem/path logic. No heavy dependencies.
"""
path: str
def __post_init__(self) -> None:
"""Validate the product path."""
p = Path(self.path)
if not p.exists():
raise LandsatLayoutError(f"Landsat path not found: {self.path}")
if not p.is_dir():
raise LandsatLayoutError(f"Landsat path is not a directory: {self.path}")
@property
def product_dir(self) -> Path:
return Path(self.path)
[docs]
def default_meas_vars(self) -> List[str]:
"""
Return the default measurement variables for Landsat to read if user does not specify.
:return: List of default measurement variables inferred from filenames.
"""
return list(self.available_band_tokens())
[docs]
def available_band_tokens(self) -> Set[str]:
"""
Return the unique set of available band tokens based on the product files, e.g.: {'B1', 'B2', 'B3', ...'B9'}
:return: Set of available band tokens.
"""
tif_files = self.tif_band_files()
return {match.group("band") for f in tif_files if (match := _BAND_TOKEN_RE.search(Path(f).stem))}
[docs]
def tif_band_files(self, meas_vars=None) -> dict:
"""
Return a list of all TIFF files containing band data in the product directory.
:return: List of TIFF band file paths.
"""
files = glob.glob(os.path.join(self.product_dir, "*B*.TIF"))
if meas_vars is None:
meas_vars = [
"B1",
"B2",
"B3",
"B4",
"B5",
"B6",
"B7",
"B8",
"B9",
"B10",
"B11",
]
filtered_files = {}
for f in files:
stem = Path(f).stem
for band in meas_vars:
if band in stem:
filtered_files[band] = f
break
if not filtered_files:
raise LandsatLayoutError(f"No TIFF band files found for meas_vars {meas_vars} in: {self.product_dir}")
return filtered_files
[docs]
def get_angle_files(self) -> Dict[str, str]:
"""
Return a dictionary of angle file paths for illumination and viewing angles.
:return: Dict of angle file paths.
:raises LandsatLayoutError: If any expected angle file is missing.
"""
angle_files = {}
required_files = {
"viewing_zenith_angle": "*_VZA.tif",
"solar_zenith_angle": "*_SZA.tif",
"viewing_azimuth_angle": "*_VAA.tif",
"solar_azimuth_angle": "*_SAA.tif",
}
for key, pattern in required_files.items():
candidates = glob.glob(os.path.join(self.product_dir, pattern))
if not candidates:
raise LandsatLayoutError(f"Missing expected angle file matching '{pattern}' in: {self.product_dir}")
angle_files[key] = candidates[0]
return angle_files
if __name__ == "__main__":
pass