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
65 changes: 65 additions & 0 deletions api/src/helpers/dataset_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,25 @@ pub const COPERNICUSSLA_CONFIG: DatasetConfig = DatasetConfig {
allowed_data_vars: &["sla", "adt", "ugosa", "ugos", "vgosa", "vgos"],
};

/// CCMP wind is a sea-surface product: single vertical level modeled as
/// a one-element levels array of 0.0, same as OI SST and Copernicus SLA.
/// See the comment on `OISST_LEVELS` for the mechanics.
pub const CCMPWIND_LEVELS: &[f64] = &[0.0];

/// Configuration for the CCMP wind timeseries dataset.
///
/// Surface-only, global coverage. Tile size and radius cap deliberately
/// match Copernicus SLA / OI SST (5° / 100 km) — same uniformity
/// argument, same "relax once usage informs us" caveat. Four variables:
/// the wind vector components, wind speed, and observation count.
pub const CCMPWIND_CONFIG: DatasetConfig = DatasetConfig {
tile_degrees: 5.0,
max_radius_meters: 100_000.0, // 100 km — same starting cap as the others
levels: CCMPWIND_LEVELS,
coverage_bbox: None,
allowed_data_vars: &["uwnd", "vwnd", "ws", "nobs"],
};

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -338,4 +357,50 @@ mod tests {
&["sla", "adt", "ugosa", "ugos", "vgosa", "vgos"]
);
}

// ---- CCMP wind config invariants (mirror the Copernicus SLA checks) ----

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

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

#[test]
fn ccmpwind_has_exactly_one_surface_level() {
// Surface wind product; the single-element levels array keeps
// the tile generator on the no-special-case path (see OI SST).
assert_eq!(CCMPWIND_CONFIG.levels.len(), 1);
assert!((CCMPWIND_CONFIG.levels[0] - 0.0).abs() < 1e-9);
}

#[test]
fn ccmpwind_has_global_coverage() {
// CCMP is a global gridded product; no coverage_bbox skip available.
assert!(CCMPWIND_CONFIG.coverage_bbox.is_none());
}

#[test]
fn ccmpwind_advertises_all_four_variables() {
// Wind vector components, speed, and observation count. If the
// upstream product adds or drops a variable this list (and the
// meta doc's data_info) must move together.
assert_eq!(
CCMPWIND_CONFIG.allowed_data_vars,
&["uwnd", "vwnd", "ws", "nobs"]
);
}
}
108 changes: 108 additions & 0 deletions api/src/helpers/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,114 @@ impl IsTimeseriesMeta for CopernicusSlaMeta {
}
}

// ccmp wind //////////////////////////////////////////////////////////////////

/// One spatial cell of the CCMP wind grid. Surface-only (no vertical
/// dimension; `level` is always `0.0`), exactly the Copernicus SLA
/// shape. `data` holds the timeseries per variable — up to four (uwnd,
/// vwnd, ws, nobs), ordered per the meta doc's `data_info`.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CcmpWindSchema {
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 the other surface grids, 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 CcmpWindSchema {
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 CCMP wind dataset. Same layout as
/// `CopernicusSlaMeta` / `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 CcmpWindMeta {
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 CcmpWindMeta {
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
48 changes: 45 additions & 3 deletions api/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ use dataset_config::{DatasetConfig, DatasetSource};
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));
static CCMPWIND_SOURCE: Lazy<Mutex<Option<DatasetSource>>> = Lazy::new(|| Mutex::new(None));

// ---- route handlers --------------------------------------------------------
//
Expand Down Expand Up @@ -133,6 +134,27 @@ async fn copernicussla_handler(
.await
}

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

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

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

/// Generic body of the `/timeseries/{dataset}` endpoint. Parameterized by
Expand Down Expand Up @@ -547,17 +569,34 @@ async fn main() -> std::io::Result<()> {
enabled_copernicussla = true;
}

if !enabled_bsose && !enabled_oisst && !enabled_copernicussla {
let mut enabled_ccmpwind = false;
if let Some(client) = dataset_client("MONGODB_URI_CCMPWIND").await {
let ccmpwind = load_dataset_source::<schema::CcmpWindMeta>(
client,
"argo",
"ccmpwind",
"timeseriesMeta",
"ccmpwind",
)
.await
.expect("failed to load CCMP wind dataset source at startup");
*CCMPWIND_SOURCE.lock().unwrap() = Some(ccmpwind);
enabled_ccmpwind = true;
}

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

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