use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::error::BoxDynError;
use sqlx::postgres::{PgTypeInfo, PgValueRef};
use uuid::Uuid;
const POINT_BYTE_LEN: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Position(pub f64, pub f64);
impl sqlx::Type<sqlx::Postgres> for Position {
fn compatible(ty: &PgTypeInfo) -> bool {
*ty == Self::type_info()
}
fn type_info() -> PgTypeInfo {
PgTypeInfo::with_name("POINT")
}
}
#[allow(
clippy::as_conversions,
clippy::indexing_slicing,
clippy::std_instead_of_core,
clippy::big_endian_bytes,
clippy::float_arithmetic,
reason = "byte slicing, Box cast, and BE f64 decode required for PostgreSQL POINT wire format"
)]
impl<'de> sqlx::Decode<'de, sqlx::Postgres> for Position {
fn decode(value: PgValueRef<'de>) -> Result<Self, BoxDynError> {
let buf = <&[u8] as sqlx::Decode<sqlx::Postgres>>::decode(value)?;
if buf.len() < POINT_BYTE_LEN {
return Err("POINT requires 16 bytes".into());
}
let (x_bytes, rest) = buf.split_at(8_usize);
let x = f64::from_be_bytes(
x_bytes
.try_into()
.map_err(|err| Box::new(err) as BoxDynError)?,
);
let y = f64::from_be_bytes(
rest[..8]
.try_into()
.map_err(|err| Box::new(err) as BoxDynError)?,
);
Ok(Self(x, y))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Annotation {
pub bearing_time_record_specification_id: Option<Uuid>,
pub confidence: Option<f64>,
pub created_datetime: Option<DateTime<Utc>>,
pub duration_seconds: Option<i32>,
pub feed_context: Option<serde_json::Value>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub notes: Option<String>,
pub ontology_class_id: Option<Uuid>,
pub organization_id: Option<Uuid>,
pub parent_annotation_id: Option<Uuid>,
pub platform_id: Uuid,
pub spectrogram_specification_id: Option<Uuid>,
pub time_coordinates: Vec<DateTime<Utc>>,
pub value_coordinates: Vec<f64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AudioSpecification {
pub bit_depth: i64,
pub channel_count: i64,
pub channel_index: i64,
pub created_datetime: Option<DateTime<Utc>>,
pub encoding: String,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub organization_id: Option<Uuid>,
pub sample_rate: i64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BeamgramSpecification {
pub center_bearing: Option<f64>,
pub center_bin_width: Option<f64>,
pub created_datetime: Option<DateTime<Utc>>,
pub elevation_increment: Option<f64>,
pub fft_sample_count: Option<i32>,
pub id: Option<Uuid>,
pub lower_elevation: Option<f64>,
pub max_frequency: Option<f64>,
pub min_frequency: Option<f64>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub normalizer: Option<String>,
pub organization_id: Option<Uuid>,
pub update_rate_ms: Option<i64>,
pub upper_elevation: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BearingTimeRecordSpecification {
pub amplitude_unit_mode: Option<String>,
pub baffle_bearing_list: Option<Vec<f64>>,
pub bearing_bin_count: Option<i32>,
pub created_datetime: Option<DateTime<Utc>>,
pub elevation_increment: Option<f64>,
pub fft_sample_count: Option<i32>,
pub focus_range: Option<String>,
pub frequency_spacing: Option<f64>,
pub heading_data_type: Option<String>,
pub heading_vector_index: Option<i32>,
pub id: Option<Uuid>,
pub lower_elevation: Option<f64>,
pub max_frequency: Option<f64>,
pub max_pixel: Option<i32>,
pub min_frequency: Option<f64>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub normalizer: Option<String>,
pub organization_id: Option<Uuid>,
pub update_rate_ms: Option<i64>,
pub upper_elevation: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DataRow {
pub created_datetime: Option<DateTime<Utc>>,
pub data_stream_id: Uuid,
pub data_type: String,
pub datetime: DateTime<Utc>,
pub modified_datetime: Option<DateTime<Utc>>,
pub specification_id: Uuid,
pub vector: Vec<f64>,
pub vector_end_bound: f64,
pub vector_start_bound: f64,
}
#[allow(
clippy::exhaustive_enums,
reason = "variants mirror a closed Postgres ENUM, so exhaustive matching downstream is desirable"
)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
#[sqlx(
type_name = "horizon_public.data_stream_query_source",
rename_all = "lowercase"
)]
#[serde(rename_all = "lowercase")]
pub enum DataStreamQuerySource {
#[default]
Postgres,
Trino,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DataStream {
pub created_datetime: Option<DateTime<Utc>>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub organization_id: Option<Uuid>,
pub platform_id: Option<Uuid>,
#[serde(default)]
pub query_source: DataStreamQuerySource,
}
impl DataStream {
#[must_use]
pub fn from_platform_and_name(platform_id: Uuid, data_stream_name: &str) -> Self {
let name = capitalize(data_stream_name);
Self {
created_datetime: None,
id: Some(data_stream_uuid(platform_id, data_stream_name)),
modified_datetime: None,
name: Some(name),
organization_id: None,
platform_id: Some(platform_id),
query_source: DataStreamQuerySource::Postgres,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MetadataRow {
pub altitude: Option<f64>,
pub created_datetime: Option<DateTime<Utc>>,
pub data_stream_id: Uuid,
pub datetime: DateTime<Utc>,
pub heading: Option<f64>,
pub latitude: Option<f64>,
pub longitude: Option<f64>,
pub modified_datetime: Option<DateTime<Utc>>,
pub pitch: Option<f64>,
pub roll: Option<f64>,
pub speed: Option<f64>,
pub speed_over_ground: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Mission {
pub created_datetime: Option<DateTime<Utc>>,
pub end_datetime: Option<DateTime<Utc>>,
pub free_text: Option<String>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub organization_id: Option<Uuid>,
pub position: Option<Position>,
pub start_datetime: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MissionPlatform {
pub created_datetime: Option<DateTime<Utc>>,
pub id: Option<Uuid>,
pub mission_id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub organization_id: Option<Uuid>,
pub platform_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ontology {
pub created_datetime: Option<DateTime<Utc>>,
pub description: Option<String>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub organization_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OntologyClass {
pub created_datetime: Option<DateTime<Utc>>,
pub description: Option<String>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub ontology_id: Uuid,
pub order: Option<i32>,
pub organization_id: Option<Uuid>,
pub parent_id: Option<Uuid>,
pub relationship_type: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Platform {
pub created_datetime: Option<DateTime<Utc>>,
pub end_datetime: Option<DateTime<Utc>>,
pub free_text: Option<String>,
pub id: Option<Uuid>,
pub kind_id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub organization_id: Option<Uuid>,
pub position: Option<Position>,
pub start_datetime: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlatformAudioSpecification {
pub audio_specification_id: Uuid,
pub created_datetime: Option<DateTime<Utc>>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub organization_id: Option<Uuid>,
pub platform_id: Uuid,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlatformBeamgramSpecification {
pub beamgram_specification_id: Option<Uuid>,
pub created_datetime: Option<DateTime<Utc>>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub organization_id: Option<Uuid>,
pub platform_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlatformBearingTimeRecordSpecification {
pub bearing_time_record_specification_id: Option<Uuid>,
pub created_datetime: Option<DateTime<Utc>>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub organization_id: Option<Uuid>,
pub platform_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlatformInformation {
pub created_datetime: Option<DateTime<Utc>>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub organization_id: Option<Uuid>,
pub platform_id: Uuid,
pub properties: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlatformKind {
pub created_datetime: Option<DateTime<Utc>>,
pub id: Option<Uuid>,
pub image_url: Option<String>,
pub long_description: Option<String>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub organization_id: Option<Uuid>,
pub short_description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlatformSpectrogramSpecification {
pub created_datetime: Option<DateTime<Utc>>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub organization_id: Option<Uuid>,
pub platform_id: Option<Uuid>,
pub spectrogram_specification_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DirectionalSpectrogramSpecification {
pub bearing_lagging_rows: Option<i64>,
pub bearing_leading_rows: Option<i64>,
pub created_datetime: Option<DateTime<Utc>>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub organization_id: Option<Uuid>,
pub spectrogram_specification_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SpectrogramSpecification {
pub amplitude_unit_mode: Option<String>,
pub channel: Option<i32>,
pub channel_role: Option<String>,
pub created_datetime: Option<DateTime<Utc>>,
pub fft_sample_count: Option<i32>,
pub fft_sample_overlap_count: Option<i32>,
pub frequency_spacing: Option<f64>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub organization_id: Option<Uuid>,
}
#[must_use]
pub fn capitalize(s: &str) -> String {
let mut chars = s.chars();
chars.next().map_or_else(String::new, |first| {
let upper: String = first.to_uppercase().collect();
format!("{upper}{}", chars.as_str().to_lowercase())
})
}
#[must_use]
pub fn data_stream_uuid(platform_id: Uuid, data_stream_name: &str) -> Uuid {
let name = capitalize(data_stream_name);
name_to_uuid(&format!("{platform_id}:{name}"))
}
#[must_use]
pub fn name_to_uuid(name: &str) -> Uuid {
const NAMESPACE: Uuid = Uuid::from_bytes([
0x13, 0xdb, 0x1b, 0xb5, 0x27, 0xac, 0x4b, 0xf7, 0x81, 0x86, 0x10, 0x43, 0x34, 0x7c, 0x42,
0x47,
]);
let normalized = name.trim().to_lowercase();
Uuid::new_v5(&NAMESPACE, normalized.as_bytes())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, reason = "tests use unwrap for brevity")]
mod tests {
use super::*;
#[test]
fn capitalize_matches_python() {
assert_eq!(capitalize("hydrophone"), "Hydrophone");
assert_eq!(capitalize("Hydrophone"), "Hydrophone");
assert_eq!(capitalize("hYDROPHONE"), "Hydrophone");
assert_eq!(capitalize("HYDROPHONE"), "Hydrophone");
assert_eq!(capitalize(""), "");
assert_eq!(capitalize("a"), "A");
}
#[test]
fn data_stream_from_platform_and_name() {
let platform_id = Uuid::new_v4();
let ds = DataStream::from_platform_and_name(platform_id, "audio");
assert_eq!(ds.platform_id, Some(platform_id));
assert_eq!(ds.name.as_deref(), Some("Audio"));
assert!(ds.id.is_some());
let ds2 = DataStream::from_platform_and_name(platform_id, "audio");
assert_eq!(ds.id, ds2.id);
}
#[test]
fn data_stream_uuid_is_deterministic() {
let platform_id = Uuid::new_v4();
let a = data_stream_uuid(platform_id, "audio");
let b = data_stream_uuid(platform_id, "audio");
assert_eq!(a, b);
}
#[test]
fn data_stream_uuid_matches_from_platform_and_name() {
let platform_id = Uuid::new_v4();
let ds = DataStream::from_platform_and_name(platform_id, "audio");
assert_eq!(ds.id, Some(data_stream_uuid(platform_id, "audio")));
}
#[test]
fn mission_serde_round_trip() {
let mission = Mission {
created_datetime: None,
end_datetime: None,
free_text: None,
id: Some(Uuid::new_v4()),
modified_datetime: None,
name: Some("Test Mission".to_owned()),
organization_id: None,
position: Some(Position(1.5_f64, 2.5_f64)),
start_datetime: Some(Utc::now()),
};
let json = serde_json::to_string(&mission).unwrap();
let deserialized: Mission = serde_json::from_str(&json).unwrap();
assert_eq!(mission, deserialized);
}
#[test]
fn name_to_uuid_is_deterministic() {
let a = name_to_uuid("test");
let b = name_to_uuid("test");
assert_eq!(a, b);
}
#[test]
fn name_to_uuid_matches_python_sdk() {
let expected = Uuid::parse_str("e32be881-c656-5b8e-8dd7-889066ac1c83").unwrap();
assert_eq!(name_to_uuid("Test Platform"), expected);
let expected2 = Uuid::parse_str("eaeebe45-0eaa-59e4-9b79-7ceda69789e2").unwrap();
assert_eq!(name_to_uuid("hello world"), expected2);
}
#[test]
fn name_to_uuid_normalizes_whitespace_and_case() {
let a = name_to_uuid(" Hello World ");
let b = name_to_uuid("hello world");
assert_eq!(a, b);
}
#[test]
fn platform_information_serde_with_json_properties() {
let info = PlatformInformation {
created_datetime: None,
id: None,
modified_datetime: None,
organization_id: None,
platform_id: Uuid::new_v4(),
properties: serde_json::json!({"key": "value", "nested": {"a": 1_i32}}),
};
let json = serde_json::to_string(&info).unwrap();
let deserialized: PlatformInformation = serde_json::from_str(&json).unwrap();
assert_eq!(info, deserialized);
}
}