use crate::query::{DbConnection, ImageQuery};
use anyhow::{bail, Result};
use chrono::NaiveDate;
use glob::glob;
use log::debug;
use spinners::{Spinner, Spinners};
use stac::{Item, ItemCollection};
use std::fmt::{self, Debug};
use std::process::Command;
use std::{path::PathBuf, str::FromStr};
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use strum_macros::EnumString;
use strum_macros::{Display, EnumIter};
#[derive(Debug, PartialEq, EnumString, Clone)]
#[allow(non_camel_case_types)]
pub enum Satellite {
l5,
l7,
l8,
l9,
lz,
ce,
cf,
cv,
}
#[derive(Debug, PartialEq, EnumString, Clone)]
#[allow(non_camel_case_types)]
pub enum Instrument {
tm,
ms,
ol,
}
#[derive(Debug, PartialEq, EnumString, Clone)]
#[allow(non_camel_case_types)]
pub enum Product {
re,
pa,
th,
}
#[derive(Debug, PartialEq, EnumString, Clone, EnumIter)]
#[allow(non_camel_case_types)]
pub enum Extension {
tif,
img,
gpkg,
meta,
vrt,
}
#[derive(Debug, PartialEq, Clone)]
pub struct QvfFilenames {
pub qvf_filenames: Vec<QvfFilename>,
}
#[derive(Debug, PartialEq, Clone)]
pub struct QvfFilenamesBuilder {
qvf_filenames: Vec<QvfFilename>,
}
impl QvfFilenamesBuilder {
pub fn from_folder(folder: &Path) -> QvfFilenamesBuilder {
let mut sources = Vec::new();
for ext in Extension::iter() {
let pattern = format!("{}/*.{ext:?}", folder.to_str().unwrap());
let source_files: Result<Vec<_>, _> = glob(&pattern)
.expect("Failed to read glob pattern")
.collect();
let source_files: Vec<PathBuf> = source_files.unwrap();
for s in source_files {
sources.push(s);
}
}
let mut result = Vec::new();
for sf in sources.iter() {
let file = sf.to_str().unwrap();
result.push(QvfFilename::from_str(file).unwrap())
}
QvfFilenamesBuilder {
qvf_filenames: result,
}
}
pub fn build(self) -> QvfFilenames {
let mut qs = Vec::new();
for q in self.qvf_filenames {
qs.push(q)
}
QvfFilenames { qvf_filenames: qs }
}
}
pub enum QvfFields {
Satellite,
Instrument,
Product,
Scene,
Date,
StageCode,
Zone,
ExtraFields,
}
#[derive(Debug, PartialEq, Clone, Copy, PartialOrd, Eq, Hash)]
pub struct QvfDateRange {
pub start: NaiveDate,
pub end: NaiveDate,
}
#[derive(Debug, PartialEq, Clone, Copy, PartialOrd, Display, Eq, Hash)]
pub enum QvfDate {
Date(NaiveDate),
DateRange(QvfDateRange),
}
impl QvfDate {
pub fn format(&self, formatter: &str) -> String {
let formated = match self {
QvfDate::Date(d) => d.format(formatter).to_string(),
QvfDate::DateRange(d) => {
let s = d.start.format(formatter).to_string();
let e = d.end.format(formatter).to_string();
format!("m{s}{e}")
}
};
formated
}
}
impl From<QvfDate> for NaiveDate {
fn from(item: QvfDate) -> Self {
match item {
QvfDate::Date(d) => d,
QvfDate::DateRange(d) => d.start,
}
}
}
#[derive(Debug, PartialEq, Copy, Clone, clap::ValueEnum, EnumIter, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Collection {
Landsat5,
Landsat7,
Landsat8,
Landsat9,
LandsatAll,
Sentinel2,
}
#[derive(Debug, PartialEq, Clone)]
pub enum ImageType {
Scene,
Mosaic,
}
#[derive(Debug, PartialEq, Clone)]
pub struct QvfFilename {
pub satellite: Satellite,
pub instrument: Instrument,
pub product: Product,
pub scene: String,
pub date: QvfDate,
pub stage_code: String,
pub zone: String,
pub extension: Extension,
pub collection: Collection,
pub image_type: ImageType,
pub location: Option<PathBuf>,
pub extra_fields: Option<Vec<String>>,
}
impl FromStr for QvfFilename {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<QvfFilename> {
let path = PathBuf::from(string);
let location = path.parent();
let location = location.map(PathBuf::from);
let string = path.file_name().unwrap().to_str().unwrap();
let parts: Vec<&str> = string.split('.').collect();
let name: &str = parts[0];
let ext: &str = parts[1];
let qvf_parts: Vec<&str> = name.split('_').collect();
let satellite = Satellite::from_str(&qvf_parts[0][0..2])?;
let instrument = Instrument::from_str(&qvf_parts[0][2..4])?;
let product = Product::from_str(&qvf_parts[0][4..6])?;
let scene = qvf_parts[1].to_string();
let extra_fields: Option<Vec<String>> = if qvf_parts.len() > 4 {
let fields: Vec<String> = qvf_parts[4..].iter().map(|f| f.to_string()).collect();
Some(fields)
} else {
None
};
let date = if qvf_parts[2].len() == 8 {
let d = NaiveDate::parse_from_str(qvf_parts[2], "%Y%m%d")
.expect("Could not parse the date!");
QvfDate::Date(d)
} else if qvf_parts[2].len() == 13 {
let start = NaiveDate::parse_from_str(&format!("{}01", &qvf_parts[2][1..7]), "%Y%m%d")
.expect("Could not parse the date!");
let end = NaiveDate::parse_from_str(&format!("{}01", &qvf_parts[2][7..]), "%Y%m%d")
.expect("Could not parse the date!");
QvfDate::DateRange(QvfDateRange { end, start })
} else if qvf_parts[2].len() == 17 {
let start = NaiveDate::parse_from_str(&qvf_parts[2][1..9], "%Y%m%d")
.expect("Could not parse the date!");
let end = NaiveDate::parse_from_str(&qvf_parts[2][9..], "%Y%m%d")
.expect("Could not parse the date!");
QvfDate::DateRange(QvfDateRange { end, start })
} else {
bail!("Could not parse the date!")
};
let stage_code = qvf_parts[3][0..3].to_string();
let zone = qvf_parts[3][3..5].to_string();
let extension = Extension::from_str(ext)?;
let image_type = match satellite {
Satellite::cv | Satellite::lz => ImageType::Mosaic,
Satellite::l5
| Satellite::l7
| Satellite::l8
| Satellite::ce
| Satellite::cf
| Satellite::l9 => ImageType::Scene,
};
let collection = match satellite {
Satellite::l5 | Satellite::l7 | Satellite::l8 | Satellite::lz | Satellite::l9 => {
Collection::Landsat8
}
Satellite::ce | Satellite::cf | Satellite::cv => Collection::Sentinel2,
};
let qvf_fn = QvfFilename {
satellite,
instrument,
product,
scene,
date,
stage_code,
zone,
extension,
image_type,
collection,
location,
extra_fields,
};
Ok(qvf_fn)
}
}
impl fmt::Display for QvfFilename {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{:?}{:?}{:?}_{}_{}_{}{}.{:?}",
self.satellite,
self.instrument,
self.product,
self.scene,
self.date,
self.stage_code,
self.zone,
self.extension
)
}
}
impl QvfFilenames {
pub fn sort_by(&mut self, field: QvfFields) {
let sorted = match field {
QvfFields::Satellite => todo!(),
QvfFields::Instrument => todo!(),
QvfFields::Product => todo!(),
QvfFields::Scene => todo!(),
QvfFields::Date => {
let mut sorted = self.qvf_filenames.clone();
sorted.sort_by(|a, b| {
let date_a = &a.date;
let date_b = &b.date;
date_a.partial_cmp(date_b).unwrap()
});
sorted
}
QvfFields::StageCode => todo!(),
QvfFields::Zone => todo!(),
QvfFields::ExtraFields => todo!(),
};
self.qvf_filenames = sorted;
}
pub fn filter_by_stage(&mut self, value: &str) {
let names = self.qvf_filenames.clone();
let filtered = names
.into_iter()
.filter(|q| q.stage_code == value)
.collect();
self.qvf_filenames = filtered;
}
pub fn from_image_query(query: ImageQuery, stage_codes: &[&str]) -> Self {
let db = DbConnection::new();
db.query_imagery(query, stage_codes)
}
pub fn recall(self, to: &Path) -> Result<Vec<PathBuf>> {
match self.qvf_filenames.len() {
0 => {
bail!("Nothing to be recalled")
}
_ => {
let mut sp = Spinner::new(
Spinners::Earth,
format!(
"Recaling {} files, please be patient.",
self.qvf_filenames.len()
),
);
let mut to_recall = Vec::new();
let mut recalled = Vec::new();
for q in self.qvf_filenames {
let base_dir = q.clone().qv_dir().unwrap();
let file_name = q.name();
let tr = base_dir.join(&file_name);
let r = to.join(file_name);
to_recall.push(tr);
recalled.push(r);
}
Command::new("dmget")
.args(&to_recall)
.spawn()
.expect("Failed to start recalling")
.wait()
.expect("Failed recall");
for (t, r) in to_recall.iter().zip(recalled.iter()) {
if !r.exists() {
debug!("Copy from: {t:?} to: {r:?}");
std::fs::copy(t, r).expect("Could not copy files");
} else {
debug!("{r:?} alredy in dst folder");
}
}
sp.stop();
Ok(recalled)
}
}
}
pub fn to_feature_collection(&self) -> ItemCollection {
let mut items = Vec::new();
for qvf_file in &self.qvf_filenames {
let p = qvf_file.location.clone().unwrap();
let name = qvf_file.name();
let item = gdal_to_stac_item(p.join(name)).unwrap();
items.push(item);
}
let item_collection = items.into();
debug!("Item_collection {:?}", item_collection);
item_collection
}
}
use anyhow::Context;
use gdal::Dataset;
use stac::{Asset, Href};
use std::path::Path;
use serde_json::{Map, Value};
pub fn gdal_to_stac_item<P: AsRef<Path>>(path: P) -> Result<Item> {
let path_str = path.as_ref().to_str().context("Invalid path")?;
let ds =
Dataset::open(&path).with_context(|| format!("Failed to open dataset at {}", path_str))?;
let size = ds.raster_size();
let (width, height) = size;
let geo_transform = ds.geo_transform().context("Failed to read geo transform")?;
let _projection = ds.projection();
let min_x = geo_transform[0];
let max_y = geo_transform[3];
let max_x = min_x + geo_transform[1] * width as f64;
let min_y = max_y + geo_transform[5] * height as f64;
let _bbox = [min_x, min_y, max_x, max_y];
let geometry = Some(geojson::Geometry::new(geojson::Value::Polygon(vec![vec![
vec![min_x, min_y],
vec![min_x, max_y],
vec![max_x, max_y],
vec![max_x, min_y],
vec![min_x, min_y],
]])));
let datetime = Some("2025-01-01T00:00:00Z".parse()?);
let id = path
.as_ref()
.file_stem()
.and_then(|s| s.to_str())
.unwrap()
.to_string();
let eo_bands = Value::Array(vec![Value::Object({
let mut band = Map::new();
band.insert(
"name".to_string(),
Value::String("test_band_name".to_string()),
);
band
})]);
let mut item = Item::new(id);
item.geometry = geometry;
item.properties.datetime = datetime;
let mut asset = Asset::new(Href::from(path_str));
let mut map = Map::new();
map.insert("eo:bands".to_string(), eo_bands);
asset.additional_fields = map;
item.assets.insert("data".to_string(), asset);
Ok(item)
}
impl QvfFilename {
pub fn change_satellite(mut self, satellite: Satellite) -> Self {
self.satellite = satellite;
self
}
pub fn change_instrument(mut self, instrument: Instrument) -> Self {
self.instrument = instrument;
self
}
pub fn change_product(mut self, product: Product) -> Self {
self.product = product;
self
}
pub fn change_date(mut self, date: QvfDate) -> Self {
self.date = date;
self
}
pub fn change_stage_code(mut self, code: String) -> Self {
self.stage_code = code;
self
}
pub fn change_zone(mut self, zone: String) -> Self {
self.zone = zone;
self
}
pub fn change_extension(mut self, extension: Extension) -> Self {
self.extension = extension;
self
}
#[must_use]
pub fn is_landsat(&self) -> bool {
matches!(
self.satellite,
Satellite::l5 | Satellite::l7 | Satellite::l8 | Satellite::l9 | Satellite::lz
)
}
#[must_use]
pub fn is_sentinel(&self) -> bool {
matches!(
self.satellite,
Satellite::ce | Satellite::cf | Satellite::cv
)
}
#[must_use]
pub fn exist_on_filestore(&self) -> bool {
let mut location = self.qv_dir().unwrap();
location.push(self.name());
debug!("location, {location:?}");
location.exists()
}
pub fn name(&self) -> String {
match self.image_type {
ImageType::Scene => {
let name = if let Some(extra) = &self.extra_fields {
let ef = extra.join("_");
format!(
"{:?}{:?}{:?}_{}_{}_{}{}_{}.{:?}",
self.satellite,
self.instrument,
self.product,
self.scene,
self.date.format("%Y%m%d"),
self.stage_code,
self.zone,
ef,
self.extension,
)
} else {
format!(
"{:?}{:?}{:?}_{}_{}_{}{}.{:?}",
self.satellite,
self.instrument,
self.product,
self.scene,
self.date.format("%Y%m%d"),
self.stage_code,
self.zone,
self.extension
)
};
name
}
ImageType::Mosaic => {
let name = if let Some(extra) = &self.extra_fields {
let ef = extra.join("_");
format!(
"{:?}{:?}{:?}_{}_{}_{}{}_{}.{:?}",
self.satellite,
self.instrument,
self.product,
self.scene,
self.date.format("%Y%m"),
self.stage_code,
self.zone,
ef,
self.extension,
)
} else {
format!(
"{:?}{:?}{:?}_{}_{}_{}{}.{:?}",
self.satellite,
self.instrument,
self.product,
self.scene,
self.date.format("%Y%m"),
self.stage_code,
self.zone,
self.extension
)
};
name
}
}
}
pub fn to_pathbuff(&self) -> PathBuf {
let def_root = PathBuf::from(".");
let p = self.location.clone().unwrap_or(def_root);
p.join(self.name())
}
pub fn qv_dir(&self) -> Result<PathBuf> {
let filestore_topdirs = PathBuf::from("/apollo");
let default_collections = PathBuf::from("imagery/rsc/");
let dirname = match self.collection {
Collection::Sentinel2 => match self.image_type {
ImageType::Scene => {
let scene_group = PathBuf::from(&self.scene[0..4]);
let scene = PathBuf::from(&self.scene);
let scene_dir = scene_group.join(scene);
let date_dir = PathBuf::from(&self.date.format("%Y").to_string())
.join(self.date.format("%Y%m"));
PathBuf::from("sentinel2").join(scene_dir).join(date_dir)
}
ImageType::Mosaic => PathBuf::from("."), },
Collection::Landsat8
| Collection::Landsat5
| Collection::Landsat7
| Collection::Landsat9 => {
let scene_group = match self.instrument {
Instrument::ms => "landsat45mss",
Instrument::tm => "landsat57tm",
Instrument::ol => "landsat57tm",
};
match self.image_type {
ImageType::Scene => {
let wrs_dir = "wrs2";
let path = &self.scene[1..4];
let row = &self.scene[5..8];
let path_row_dir = format!("{path}_{row}");
let date_dir = PathBuf::from(&self.date.format("%Y").to_string())
.join(self.date.format("%Y%m"));
PathBuf::from("landsat")
.join(scene_group)
.join(wrs_dir)
.join(path_row_dir)
.join(date_dir)
}
ImageType::Mosaic => todo!(),
}
}
Collection::LandsatAll => todo!(),
};
let dirname = filestore_topdirs.join(default_collections).join(dirname);
Ok(dirname)
}
pub fn recall(self, to: PathBuf) -> Result<PathBuf> {
let mut sp = Spinner::new(Spinners::Dots9, "Waiting for 3 seconds".into());
let base_dir = self.clone().qv_dir().unwrap();
let file_name = self.name();
let to_recall = base_dir.join(&file_name);
Command::new("dmget")
.arg(&to_recall)
.spawn()
.expect("Failed to start recalling")
.wait()
.expect("Failed recall");
let recalled = to.join(file_name);
std::fs::copy(to_recall, &recalled).expect("Could not copy files");
sp.stop();
Ok(recalled)
}
}