Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions api/src/helpers/dataset_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,27 @@ pub const OISST_CONFIG: DatasetConfig = DatasetConfig {
allowed_data_vars: &["sst"],
};

/// Copernicus SLA is a sea-surface product: like OI SST, a single
/// vertical level modeled as a one-element levels array of 0.0 so the
/// tile_generator / filter_composer path works unchanged. See the
/// comment on `OISST_LEVELS` for the mechanics.
pub const COPERNICUSSLA_LEVELS: &[f64] = &[0.0];

/// Configuration for the Copernicus sea level anomaly timeseries dataset.
///
/// Surface-only, global coverage. Tile size and radius cap deliberately
/// match OI SST (5° / 100 km) — same uniformity argument, same "relax
/// once usage informs us" caveat. Six variables: sea level anomaly,
/// absolute dynamic topography, and the geostrophic velocity components
/// for each (u/v, anomaly and absolute).
pub const COPERNICUSSLA_CONFIG: DatasetConfig = DatasetConfig {
tile_degrees: 5.0,
max_radius_meters: 100_000.0, // 100 km — same starting cap as OI SST
levels: COPERNICUSSLA_LEVELS,
coverage_bbox: None,
allowed_data_vars: &["sla", "adt", "ugosa", "ugos", "vgosa", "vgos"],
};

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -268,4 +289,51 @@ mod tests {
// OI SST is global; no coverage_bbox skip available.
assert!(OISST_CONFIG.coverage_bbox.is_none());
}

// ---- Copernicus SLA config invariants (mirror the OI SST checks) -------

#[test]
fn copernicussla_tile_degrees_is_positive_and_divides_a_hemisphere() {
assert!(COPERNICUSSLA_CONFIG.tile_degrees > 0.0);
assert!(
(180.0_f64 % COPERNICUSSLA_CONFIG.tile_degrees).abs() < 1e-9,
"tile_degrees should evenly divide 180° for clean global coverage"
);
assert!(
(360.0_f64 % COPERNICUSSLA_CONFIG.tile_degrees).abs() < 1e-9,
"tile_degrees should evenly divide 360° for clean global coverage"
);
}

#[test]
fn copernicussla_max_radius_is_positive_and_subhemispheric() {
assert!(COPERNICUSSLA_CONFIG.max_radius_meters > 0.0);
assert!(COPERNICUSSLA_CONFIG.max_radius_meters < 1.0e7);
}

#[test]
fn copernicussla_has_exactly_one_surface_level() {
// Sea level anomaly is by construction a surface product; the
// single-element levels array keeps the tile generator on the
// no-special-case path (see OI SST).
assert_eq!(COPERNICUSSLA_CONFIG.levels.len(), 1);
assert!((COPERNICUSSLA_CONFIG.levels[0] - 0.0).abs() < 1e-9);
}

#[test]
fn copernicussla_has_global_coverage() {
// Altimetry-derived SLA is global; no coverage_bbox skip available.
assert!(COPERNICUSSLA_CONFIG.coverage_bbox.is_none());
}

#[test]
fn copernicussla_advertises_all_six_variables() {
// sla/adt plus u/v geostrophic velocities in anomaly and absolute
// flavours. If the upstream product adds or drops a variable this
// list (and the meta doc's data_info) must move together.
assert_eq!(
COPERNICUSSLA_CONFIG.allowed_data_vars,
&["sla", "adt", "ugosa", "ugos", "vgosa", "vgos"]
);
}
}
107 changes: 107 additions & 0 deletions api/src/helpers/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,113 @@ impl IsTimeseriesMeta for OisstMeta {
}
}

// copernicus sla /////////////////////////////////////////////////////////////

/// One spatial cell of the Copernicus sea level anomaly grid. Surface-only
/// (no vertical dimension; `level` is always `0.0`), exactly the OI SST
/// shape. `data` holds the timeseries per variable — up to six (sla, adt,
/// ugosa, ugos, vgosa, vgos), ordered per the meta doc's `data_info`.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CopernicusSlaSchema {
pub(crate) _id: String,
// Reachable from main.rs (batchmeta branch reads `metadata()`), so
// `pub` for symmetry with the other schemas.
pub metadata: Vec<String>,
pub(crate) basin: f64,
pub(crate) geolocation: GeoJSONPoint,
pub(crate) level: f64,
// Omitted from the response when empty (no `data=` qsp); see the
// matching annotation on `BsoseSchema.data` for the full reasoning.
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) data: Vec<Vec<f64>>,
// Like OI SST, data docs don't carry `timeseries` or `data_info` of
// their own — both are populated at request time per the
// response-shape rule (see the annotations on `OisstSchema`).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) timeseries: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) data_info: Option<DataInfo>,
}

impl IsTimeseries for CopernicusSlaSchema {
fn get_timeseries(&self) -> bool {
return true;
}

fn data(&mut self) -> &mut Vec<Vec<f64>> {
&mut self.data
}

fn set_data(&mut self, data: Vec<Vec<f64>>) {
self.data = data;
}

fn timeseries(&mut self) -> Option<&mut Vec<String>> {
self.timeseries.as_mut()
}

fn set_timeseries(&mut self, timeseries: Vec<String>) {
self.timeseries = Some(timeseries);
}

fn data_info(&mut self) -> Option<DataInfo> {
self.data_info.clone()
}

fn set_data_info(&mut self, data_info: Option<DataInfo>) {
self.data_info = data_info;
}

fn _id(&self) -> String {
self._id.clone()
}

fn longitude(&self) -> f64 {
self.geolocation.coordinates[0]
}

fn latitude(&self) -> f64 {
self.geolocation.coordinates[1]
}

fn level(&self) -> f64 {
self.level
}

fn metadata(&self) -> Vec<String> {
self.metadata.clone()
}
}

/// Metadata doc for the Copernicus SLA dataset. Same layout as
/// `OisstMeta` — `data_info` lives here (per-dataset default) rather than
/// on every data doc, and the `source` / `lattice` substructures follow
/// the same pipeline conventions, so those structs are reused directly.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CopernicusSlaMeta {
pub(crate) _id: String,
pub(crate) data_type: String,
pub data_info: DataInfo,
pub(crate) date_updated_argovis: BsonDateTime,
pub timeseries: Vec<BsonDateTime>,
pub(crate) source: Vec<OisstSourceMeta>,
pub(crate) lattice: Lattice,
}

impl IsTimeseriesMeta for CopernicusSlaMeta {
fn get_timeseries_meta(&self) -> bool {
return true;
}

fn timeseries(&self) -> Vec<BsonDateTime> {
self.timeseries.clone()
}

fn data_info(&self) -> DataInfo {
self.data_info.clone()
}
}

// ///////////////////////////////////////////////////////////////////////////

#[derive(Deserialize, Debug, Clone)]
Expand Down
47 changes: 44 additions & 3 deletions api/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ use dataset_config::{DatasetConfig, DatasetSource};
// load-and-register block in main() and a route handler above.
static BSOSE_SOURCE: Lazy<Mutex<Option<DatasetSource>>> = Lazy::new(|| Mutex::new(None));
static OISST_SOURCE: Lazy<Mutex<Option<DatasetSource>>> = Lazy::new(|| Mutex::new(None));
static COPERNICUSSLA_SOURCE: Lazy<Mutex<Option<DatasetSource>>> = Lazy::new(|| Mutex::new(None));

// ---- route handlers --------------------------------------------------------
//
Expand Down Expand Up @@ -111,6 +112,27 @@ async fn oisst_handler(
.await
}

#[get("/timeseries/copernicussla")]
async fn copernicussla_handler(
req: HttpRequest,
query_params: web::Query<serde_json::Value>,
) -> impl Responder {
let source = COPERNICUSSLA_SOURCE
.lock()
.unwrap()
.as_ref()
.expect("COPERNICUSSLA_SOURCE not initialized at startup")
.clone();

serve_timeseries::<schema::CopernicusSlaSchema>(
req,
query_params.into_inner(),
&dataset_config::COPERNICUSSLA_CONFIG,
&source,
)
.await
}

// ---- generic timeseries handler --------------------------------------------

/// Generic body of the `/timeseries/{dataset}` endpoint. Parameterized by
Expand Down Expand Up @@ -510,16 +532,32 @@ async fn main() -> std::io::Result<()> {
enabled_oisst = true;
}

if !enabled_bsose && !enabled_oisst {
let mut enabled_copernicussla = false;
if let Some(client) = dataset_client("MONGODB_URI_COPERNICUSSLA").await {
let copernicussla = load_dataset_source::<schema::CopernicusSlaMeta>(
client,
"argo",
"copernicusSLA",
"timeseriesMeta",
"sea_level_anomaly",
)
.await
.expect("failed to load Copernicus SLA dataset source at startup");
*COPERNICUSSLA_SOURCE.lock().unwrap() = Some(copernicussla);
enabled_copernicussla = true;
}

if !enabled_bsose && !enabled_oisst && !enabled_copernicussla {
eprintln!(
"warning: no datasets enabled. Set at least one of \
MONGODB_URI_BSOSE / MONGODB_URI_NOAAOISST."
MONGODB_URI_BSOSE / MONGODB_URI_NOAAOISST / MONGODB_URI_COPERNICUSSLA."
);
} else {
println!(
"Datasets enabled:{}{}",
"Datasets enabled:{}{}{}",
if enabled_bsose { " bsose" } else { "" },
if enabled_oisst { " noaaoisst" } else { "" },
if enabled_copernicussla { " copernicussla" } else { "" },
);
}

Expand All @@ -534,6 +572,9 @@ async fn main() -> std::io::Result<()> {
if enabled_oisst {
cfg.service(oisst_handler);
}
if enabled_copernicussla {
cfg.service(copernicussla_handler);
}
})
})
.bind(("0.0.0.0", 8080))?
Expand Down
Loading