1use std::borrow::Cow;
2use std::path::PathBuf;
3use std::sync::Arc;
4use std::time::Duration;
5
6use arrow_array::RecordBatch;
7use arrow_array::array::ArrayRef;
8use arrow_array::array::StringArray;
9use arrow_cast::display::ArrayFormatter;
10use bevy_math::DVec2;
11use chrono::DateTime;
12use chrono::NaiveDateTime;
13use chrono::NaiveTime;
14use chrono::Utc;
15use serde::Deserialize;
16use serde::Deserializer;
17use serde::Serialize;
18use serde::Serializer;
19use serde::de::Error;
20use tracing::info;
21use tracing::warn;
22
23#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Default)]
24#[serde(untagged)]
25pub enum UserValue {
26 #[default]
27 #[serde(serialize_with = "serde::ser::Serializer::serialize_none")]
28 None,
29 DateTime(NaiveDateTime),
31 Time(NaiveTime),
32 Duration(Duration),
33 Text(String),
34 Number(f64),
35 Bool(bool),
36 Files(Vec<PathBuf>),
37 Notes(Vec<Note>),
38 Table(TableData),
39 GeoPos(GeoPos),
40 PlotRegions(Vec<PlotRegion>),
41}
42
43#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
44pub struct Note {
45 timestamp: DateTime<Utc>,
46 pub text: String,
47}
48
49impl Note {
50 pub fn new(text: impl Into<String>) -> Self {
51 Self::with_timestamp(Utc::now(), text.into())
52 }
53 pub fn with_timestamp(timestamp: DateTime<Utc>, text: impl Into<String>) -> Self {
54 Self {
55 timestamp,
56 text: text.into(),
57 }
58 }
59
60 pub fn preview(&self) -> Cow<'_, str> {
61 if let Some((i, _)) = self.text.char_indices().nth(8) {
62 Cow::Borrowed(&self.text[..i])
63 } else {
64 Cow::Owned(self.timestamp.with_timezone(&chrono::Local).to_string())
65 }
66 }
67}
68
69#[derive(Debug, Clone, PartialEq)]
70pub struct TableData {
71 pub batch: RecordBatch,
72}
73
74#[derive(Deserialize, Serialize, Debug, Clone)]
75struct JsonTableData {
76 #[serde(default)]
77 pub columns: Vec<String>,
78 #[serde(deserialize_with = "deserialize_string_array", default)]
79 pub data: Vec<Vec<String>>,
80}
81
82impl<'w> Deserialize<'w> for TableData {
83 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
84 where
85 D: Deserializer<'w>,
86 {
87 let data = JsonTableData::deserialize(deserializer)?;
88
89 info!(
90 "Parsed table data: {} columns, {} rows",
91 data.columns.len(),
92 data.data.len()
93 );
94
95 if data.columns.len() != data.data.len() {
96 warn!(
97 "number of columns does not match shape of data: {} != {}",
98 data.columns.len(),
99 data.data.len()
100 );
101 }
102
103 let columns = std::iter::zip(data.data, data.columns)
104 .map(|(column, colname)| (colname, Arc::new(StringArray::from(column)) as ArrayRef));
105
106 let batch = RecordBatch::try_from_iter(columns).map_err(D::Error::custom)?;
107 Ok(Self { batch })
108 }
109}
110
111impl Serialize for TableData {
112 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
113 where
114 S: serde::Serializer,
115 {
116 let columns = self
117 .batch
118 .schema_ref()
119 .fields()
120 .iter()
121 .map(|field| field.name().clone())
122 .collect();
123 let data = self
124 .batch
125 .columns()
126 .iter()
127 .filter_map(|col| {
128 let Ok(formatter) = ArrayFormatter::try_new(col, &Default::default()) else {
129 return None;
130 };
131 Some(
132 (0..col.len())
133 .map(|i| formatter.value(i).to_string())
134 .collect::<Vec<_>>(),
135 )
136 })
137 .collect();
138 let value = JsonTableData { columns, data };
139 value.serialize(serializer)
140 }
141}
142
143pub fn deserialize_string_array<'de, D>(deserializer: D) -> Result<Vec<Vec<String>>, D::Error>
144where
145 D: Deserializer<'de>,
146{
147 let raw_data: Vec<Vec<Option<String>>> = Vec::deserialize(deserializer)?;
148 Ok(raw_data
149 .into_iter()
150 .map(|row| {
151 row.into_iter()
152 .map(|cell| cell.unwrap_or_default())
153 .collect()
154 })
155 .collect())
156}
157
158#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
159pub struct PlotRegion {
160 pub start_timestamp: f64,
161 pub end_timestamp: f64,
162 pub color: Color,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq)]
166pub struct Color(ecolor::Color32);
167
168impl Color {
169 pub const fn as_egui_color(self) -> ecolor::Color32 {
170 self.0
171 }
172}
173
174impl From<Color> for ecolor::Color32 {
175 fn from(value: Color) -> Self {
176 value.as_egui_color()
177 }
178}
179
180impl<'de> Deserialize<'de> for Color {
181 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
182 where
183 D: Deserializer<'de>,
184 {
185 let input = String::deserialize(deserializer)?;
186 let hex = input.trim().to_lowercase();
187
188 if !hex.starts_with('#') {
189 return Err(D::Error::custom(
190 "hex colors must start with the `#` character",
191 ));
192 }
193
194 let color = match ecolor::Color32::from_hex(&hex) {
195 Ok(color) => color,
196 Err(ecolor::ParseHexColorError::MissingHash) => {
197 return Err(D::Error::custom("hex color is missing `#` prefix"));
199 }
200 Err(ecolor::ParseHexColorError::InvalidLength) => {
201 return Err(D::Error::custom(
202 "hex color has invalid length — must be 7 or 9 characters (e.g., `#RRGGBB` or `#RRGGBBAA`)",
203 ));
204 }
205 Err(ecolor::ParseHexColorError::InvalidInt(e)) => {
206 return Err(D::Error::custom(format!("failed to parse hex digits: {e}")));
207 }
208 };
209
210 Ok(Color(color))
211 }
212}
213
214impl Serialize for Color {
215 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
216 where
217 S: Serializer,
218 {
219 let hex_string = format!(
220 "#{:02x}{:02x}{:02x}{:02x}",
221 self.0.r(),
222 self.0.g(),
223 self.0.b(),
224 self.0.a()
225 );
226 serializer.serialize_str(&hex_string)
227 }
228}
229
230#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Default)]
231pub struct GeoPos {
232 pub latitude: f64,
233 pub longitude: f64,
234}
235
236impl From<DVec2> for GeoPos {
237 fn from(value: DVec2) -> Self {
238 Self {
239 latitude: value.y,
240 longitude: value.x,
241 }
242 }
243}
244
245impl From<GeoPos> for DVec2 {
246 fn from(value: GeoPos) -> Self {
247 Self {
248 x: value.longitude,
249 y: value.latitude,
250 }
251 }
252}
253
254impl std::fmt::Display for GeoPos {
255 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 write!(f, "{},{}", self.latitude, self.longitude)
257 }
258}
259
260const DATE_TIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S";
261const TIME_FORMAT: &str = "%H:%M:%S";
262
263impl UserValue {
264 pub fn string_mut(&mut self) -> &mut String {
265 match *self {
266 Self::None => self.insert_string(String::new()),
267 Self::DateTime(ref value) => self.insert_string(format!("{value:?}")),
268 Self::Time(ref value) => self.insert_string(format!("{value:?}")),
269 Self::Duration(ref value) => self.insert_string(format!("{value:?}")),
270 Self::Text(ref mut value) => value,
271 Self::Number(value) => self.insert_string(value.to_string()),
272 Self::Bool(value) => self.insert_string(value.to_string()),
273 Self::Files(ref files) => {
274 self.insert_string(fold_str(files.iter().map(|p| p.to_string_lossy())))
275 }
276 Self::Notes(ref notes) => self.insert_string(fold_str(notes.iter().map(|n| &n.text))),
277 Self::Table(ref table) => {
278 self.insert_string(serde_json::to_string(table).unwrap_or_else(|e| e.to_string()))
279 }
280 Self::GeoPos(ref geo_pos) => self.insert_string(geo_pos.to_string()),
281 Self::PlotRegions(ref regions) => {
282 self.insert_string(serde_json::to_string(regions).unwrap_or_else(|e| e.to_string()))
283 }
284 }
285 }
286
287 pub fn number_mut(&mut self, fallback: f64) -> &mut f64 {
288 match *self {
289 Self::Number(ref mut value) => value,
290 Self::Text(ref value) => {
291 let value = value.parse::<f64>().unwrap_or(fallback);
292 self.insert_number(value)
293 }
294 Self::Bool(value) => self.insert_number(if value { 1.0 } else { 0.0 }),
295 Self::None
296 | Self::Files(_)
297 | Self::Notes(_)
298 | Self::Table(_)
299 | Self::PlotRegions(_)
300 | Self::DateTime(_)
301 | Self::Time(_)
302 | Self::Duration(_)
303 | Self::GeoPos(_) => self.insert_number(fallback),
304 }
305 }
306
307 pub fn bool_mut(&mut self, fallback: bool) -> &mut bool {
308 match *self {
309 Self::Bool(ref mut value) => value,
310 Self::Text(ref value) => {
311 let value = value.to_lowercase().parse::<bool>().unwrap_or(fallback);
312 self.insert_bool(value)
313 }
314 Self::Number(value) => self.insert_bool(value != 0.0),
315 Self::None
316 | Self::Files(_)
317 | Self::Notes(_)
318 | Self::Table(_)
319 | Self::PlotRegions(_)
320 | Self::DateTime(_)
321 | Self::Time(_)
322 | Self::Duration(_)
323 | Self::GeoPos(_) => self.insert_bool(fallback),
324 }
325 }
326
327 pub fn files_mut(&mut self, fallback: impl FnOnce() -> Vec<PathBuf>) -> &mut Vec<PathBuf> {
328 match *self {
329 Self::Files(ref mut files) => files,
330 _ => self.insert_files(fallback()),
331 }
332 }
333
334 pub fn notes_mut(&mut self, fallback: impl FnOnce() -> Vec<Note>) -> &mut Vec<Note> {
335 match *self {
336 Self::Notes(ref mut notes) => notes,
337 _ => self.insert_notes(fallback()),
338 }
339 }
340
341 pub fn table_mut(&mut self, fallback: impl FnOnce() -> TableData) -> &mut TableData {
342 match *self {
343 Self::Table(ref mut table) => table,
344 _ => self.insert_table(fallback()),
345 }
346 }
347
348 pub fn geo_pos_mut(&mut self, fallback: impl FnOnce() -> GeoPos) -> &mut GeoPos {
349 match *self {
350 Self::GeoPos(ref mut geo_pos) => geo_pos,
351 _ => self.insert_geo_pos(fallback()),
352 }
353 }
354
355 pub fn as_bool(&self) -> Option<bool> {
356 match *self {
357 Self::Bool(value) => Some(value),
358 Self::Text(ref value) => value.to_lowercase().parse::<bool>().ok(),
359 Self::Number(value) => Some(value != 0.0),
360 Self::None
361 | Self::Files(_)
362 | Self::Notes(_)
363 | Self::Table(_)
364 | Self::PlotRegions(_)
365 | Self::DateTime(_)
366 | Self::Time(_)
367 | Self::Duration(_)
368 | Self::GeoPos(_) => None,
369 }
370 }
371
372 pub fn as_table(&self) -> Option<&TableData> {
373 match *self {
374 Self::Table(ref value) => Some(value),
375 _ => None,
376 }
377 }
378
379 pub fn as_str(&self) -> Option<Cow<'_, str>> {
380 match *self {
381 Self::Text(ref text) => Some(Cow::Borrowed(text)),
382 Self::None => Some(Cow::Borrowed("")),
383 Self::Bool(value) => Some(Cow::Owned(value.to_string())),
384 Self::Number(value) => Some(Cow::Owned(value.to_string())),
385 Self::DateTime(value) => Some(Cow::Owned(value.to_string())),
386 Self::Time(value) => Some(Cow::Owned(value.to_string())),
387 Self::Duration(value) => Some(Cow::Owned(format!("{value:?}"))),
388 Self::GeoPos(value) => Some(Cow::Owned(value.to_string())),
389 Self::Files(_) | Self::Notes(_) | Self::Table(_) | Self::PlotRegions(_) => None,
390 }
391 }
392
393 pub fn as_date_time(&self) -> Option<NaiveDateTime> {
394 match *self {
395 Self::DateTime(value) => Some(value),
396 Self::Text(ref text) => NaiveDateTime::parse_from_str(text, DATE_TIME_FORMAT).ok(),
397 Self::Number(timestamp) => chrono::DateTime::from_timestamp(timestamp as i64, 0)
398 .map(|dt| dt.with_timezone(&chrono::Local).naive_local()),
399 _ => None,
400 }
401 }
402
403 pub fn as_time(&self) -> Option<NaiveTime> {
404 match *self {
405 Self::Time(value) => Some(value),
406 Self::Text(ref text) => NaiveTime::parse_from_str(text, TIME_FORMAT).ok(),
407 Self::Number(timestamp) => chrono::DateTime::from_timestamp(timestamp as i64, 0)
408 .map(|dt| dt.with_timezone(&chrono::Local).naive_local().time()),
409 _ => None,
410 }
411 }
412
413 pub fn as_geo_pos(&self) -> Option<GeoPos> {
414 match *self {
415 Self::GeoPos(value) => Some(value),
416 _ => None,
417 }
418 }
419
420 pub fn as_plot_regions(&self) -> Option<&[PlotRegion]> {
421 match *self {
422 Self::PlotRegions(ref regions) => Some(regions),
423 _ => None,
424 }
425 }
426
427 pub fn insert_string(&mut self, value: String) -> &mut String {
428 *self = Self::Text(value);
429 match self {
430 Self::Text(value) => value,
431 _ => unreachable!(),
432 }
433 }
434 pub fn insert_number(&mut self, value: f64) -> &mut f64 {
435 *self = Self::Number(value);
436 match self {
437 Self::Number(value) => value,
438 _ => unreachable!(),
439 }
440 }
441 pub fn insert_bool(&mut self, value: bool) -> &mut bool {
442 *self = Self::Bool(value);
443 match self {
444 Self::Bool(value) => value,
445 _ => unreachable!(),
446 }
447 }
448 pub fn insert_files(&mut self, value: Vec<PathBuf>) -> &mut Vec<PathBuf> {
449 *self = Self::Files(value);
450 match self {
451 Self::Files(value) => value,
452 _ => unreachable!(),
453 }
454 }
455 pub fn insert_notes(&mut self, value: Vec<Note>) -> &mut Vec<Note> {
456 *self = Self::Notes(value);
457 match self {
458 Self::Notes(value) => value,
459 _ => unreachable!(),
460 }
461 }
462 pub fn insert_table(&mut self, value: TableData) -> &mut TableData {
463 *self = Self::Table(value);
464 match self {
465 Self::Table(value) => value,
466 _ => unreachable!(),
467 }
468 }
469 pub fn insert_datetime(&mut self, value: NaiveDateTime) -> &mut NaiveDateTime {
470 *self = Self::DateTime(value);
471 match self {
472 Self::DateTime(value) => value,
473 _ => unreachable!(),
474 }
475 }
476 pub fn insert_time(&mut self, value: NaiveTime) -> &mut NaiveTime {
477 *self = Self::Time(value);
478 match self {
479 Self::Time(value) => value,
480 _ => unreachable!(),
481 }
482 }
483 pub fn insert_geo_pos(&mut self, value: GeoPos) -> &mut GeoPos {
484 *self = Self::GeoPos(value);
485 match self {
486 Self::GeoPos(value) => value,
487 _ => unreachable!(),
488 }
489 }
490}
491
492impl From<String> for UserValue {
493 fn from(value: String) -> Self {
494 Self::Text(value)
495 }
496}
497
498impl From<f32> for UserValue {
499 fn from(value: f32) -> Self {
500 Self::Number(value.into())
501 }
502}
503
504impl From<f64> for UserValue {
505 fn from(value: f64) -> Self {
506 Self::Number(value)
507 }
508}
509
510impl From<bool> for UserValue {
511 fn from(value: bool) -> Self {
512 Self::Bool(value)
513 }
514}
515
516impl From<NaiveDateTime> for UserValue {
517 fn from(value: NaiveDateTime) -> Self {
518 Self::DateTime(value)
519 }
520}
521
522impl From<NaiveTime> for UserValue {
523 fn from(value: NaiveTime) -> Self {
524 Self::Time(value)
525 }
526}
527
528impl From<Duration> for UserValue {
529 fn from(value: Duration) -> Self {
530 Self::Duration(value)
531 }
532}
533
534impl From<GeoPos> for UserValue {
535 fn from(value: GeoPos) -> Self {
536 Self::GeoPos(value)
537 }
538}
539
540fn fold_str<S: Into<String> + AsRef<str>>(iter: impl IntoIterator<Item = S>) -> String {
541 iter.into_iter().fold(String::new(), |mut value, str| {
542 if value.is_empty() {
543 str.into()
544 } else {
545 value.push('\n');
546 value.push_str(str.as_ref());
547 value
548 }
549 })
550}
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555
556 #[test]
557 fn serialize_none() {
558 assert_eq!(
559 serde_yaml::to_value(UserValue::None).unwrap(),
560 serde_yaml::Value::Null
561 );
562 assert_eq!(
563 UserValue::None,
564 serde_yaml::from_value(serde_yaml::Value::Null).unwrap(),
565 );
566 }
567
568 #[test]
569 fn serialize_date_time() {
570 let date_time =
571 NaiveDateTime::parse_from_str("2025-5-15T4:46:12", DATE_TIME_FORMAT).unwrap();
572
573 assert_eq!(
574 serde_yaml::to_value(UserValue::DateTime(date_time)).unwrap(),
575 serde_yaml::Value::String("2025-05-15T04:46:12".to_owned())
576 );
577 assert_eq!(
578 UserValue::DateTime(date_time),
579 serde_yaml::from_str("2025-05-15T04:46:12").unwrap()
580 );
581 }
582
583 #[test]
584 fn serialize_time() {
585 let date_time = NaiveTime::parse_from_str("04:46:12", TIME_FORMAT).unwrap();
586
587 assert_eq!(
588 serde_yaml::to_value(UserValue::Time(date_time)).unwrap(),
589 serde_yaml::Value::String("04:46:12".to_owned())
590 );
591 assert_eq!(
592 UserValue::Time(date_time),
593 serde_yaml::from_str("04:46:12").unwrap()
594 );
595 }
596
597 #[test]
598 #[should_panic = "data did not match any variant"]
599 fn deserialize_error() {
600 let _: UserValue = serde_yaml::from_value(serde_yaml::Value::Mapping(
601 serde_yaml::Mapping::from_iter([
602 (
603 serde_yaml::Value::String("foo".into()),
604 serde_yaml::Value::String("bar".into()),
605 ),
606 ]),
608 ))
609 .unwrap();
610 }
611}