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 os
[docs]
class LandsatLayoutError(ValueError):
"""Raised when a Landsat layout is missing expected files/structure."""
[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', ...'B11'}
:return: Set of available band tokens.
"""
return set(self.tif_band_files().keys())
[docs]
def tif_band_files(self, meas_vars=None) -> dict:
"""
Return a mapping of band name to TIFF file path for the requested bands.
Uses exact stem-suffix matching (``stem.endswith("_<band>")``), so ``B1``
never accidentally matches ``B10`` or ``B11``.
:return: Dict of {band_name: file_path}.
"""
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 stem.endswith(f"_{band}"):
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.
Matches both uppercase (.TIF) and lowercase (.tif) extensions so the method
works on case-sensitive filesystems (e.g. OneDrive on macOS/Linux).
:return: Dict of angle file paths.
:raises LandsatLayoutError: If any expected angle file is missing.
"""
angle_files = {}
required_files = {
"viewing_zenith_angle": "_VZA",
"solar_zenith_angle": "_SZA",
"viewing_azimuth_angle": "_VAA",
"solar_azimuth_angle": "_SAA",
}
for key, suffix in required_files.items():
candidates = glob.glob(os.path.join(self.product_dir, f"*{suffix}.TIF")) or glob.glob(
os.path.join(self.product_dir, f"*{suffix}.tif")
)
if not candidates:
raise LandsatLayoutError(f"Missing expected angle file matching '*{suffix}.TIF' in: {self.product_dir}")
angle_files[key] = candidates[0]
return angle_files
if __name__ == "__main__":
pass