use crate::drawings::{Drawing, DrawingToolType};
use egui::Color32;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredCoord {
pub ts_ms: i64,
pub price: f64,
}
impl StoredCoord {
pub fn new(ts_ms: i64, price: f64) -> Self {
Self { ts_ms, price }
}
pub fn from_screen<F, G>(x: f64, y: f64, x_to_time: F, y_to_price: G) -> Self
where
F: Fn(f64) -> i64,
G: Fn(f64) -> f64,
{
Self {
ts_ms: x_to_time(x),
price: y_to_price(y),
}
}
pub fn to_screen<F, G>(&self, time_to_x: F, price_to_y: G) -> (f64, f64)
where
F: Fn(i64) -> f64,
G: Fn(f64) -> f64,
{
(time_to_x(self.ts_ms), price_to_y(self.price))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredColor {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl From<Color32> for StoredColor {
fn from(color: Color32) -> Self {
Self {
r: color.r(),
g: color.g(),
b: color.b(),
a: color.a(),
}
}
}
impl From<[u8; 4]> for StoredColor {
fn from(color: [u8; 4]) -> Self {
Self {
r: color[0],
g: color[1],
b: color[2],
a: color[3],
}
}
}
impl From<StoredColor> for Color32 {
fn from(color: StoredColor) -> Self {
Color32::from_rgba_unmultiplied(color.r, color.g, color.b, color.a)
}
}
impl From<StoredColor> for [u8; 4] {
fn from(color: StoredColor) -> Self {
[color.r, color.g, color.b, color.a]
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredDrawing {
pub id: String,
pub tool_type: String,
pub points: Vec<StoredCoord>,
pub color: StoredColor,
pub line_width: f32,
pub selected: bool,
pub locked: bool,
pub visible: bool,
pub properties: HashMap<String, StoredValue>,
pub created_at: i64,
pub modified_at: i64,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum StoredValue {
Bool(bool),
Int(i64),
Float(f64),
String(String),
Array(Vec<StoredValue>),
}
impl StoredDrawing {
pub fn new(id: impl Into<String>, tool_type: impl Into<String>) -> Self {
let now = chrono::Utc::now().timestamp_millis();
Self {
id: id.into(),
tool_type: tool_type.into(),
points: Vec::new(),
color: StoredColor::from(Color32::WHITE),
line_width: 1.0,
selected: false,
locked: false,
visible: true,
properties: HashMap::new(),
created_at: now,
modified_at: now,
notes: None,
}
}
pub fn add_point(&mut self, point: StoredCoord) {
self.points.push(point);
self.modified_at = chrono::Utc::now().timestamp_millis();
}
pub fn set_property(&mut self, key: impl Into<String>, value: StoredValue) {
self.properties.insert(key.into(), value);
self.modified_at = chrono::Utc::now().timestamp_millis();
}
pub fn get_property(&self, key: &str) -> Option<&StoredValue> {
self.properties.get(key)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DrawingStorage {
pub version: u32,
pub symbol: String,
pub timeframe: Option<String>,
pub drawings: Vec<StoredDrawing>,
pub metadata: StorageMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageMetadata {
pub created_at: i64,
pub modified_at: i64,
pub app_version: Option<String>,
}
impl Default for StorageMetadata {
fn default() -> Self {
let now = chrono::Utc::now().timestamp_millis();
Self {
created_at: now,
modified_at: now,
app_version: None,
}
}
}
impl DrawingStorage {
pub const CURRENT_VERSION: u32 = 1;
pub fn new(symbol: impl Into<String>) -> Self {
Self {
version: Self::CURRENT_VERSION,
symbol: symbol.into(),
timeframe: None,
drawings: Vec::new(),
metadata: StorageMetadata::default(),
}
}
pub fn with_timeframe(mut self, timeframe: impl Into<String>) -> Self {
self.timeframe = Some(timeframe.into());
self
}
pub fn add_drawing(&mut self, drawing: StoredDrawing) {
self.drawings.push(drawing);
self.metadata.modified_at = chrono::Utc::now().timestamp_millis();
}
pub fn remove_drawing(&mut self, id: &str) -> bool {
let len_before = self.drawings.len();
self.drawings.retain(|d| d.id != id);
if self.drawings.len() != len_before {
self.metadata.modified_at = chrono::Utc::now().timestamp_millis();
true
} else {
false
}
}
pub fn get_drawing(&self, id: &str) -> Option<&StoredDrawing> {
self.drawings.iter().find(|d| d.id == id)
}
pub fn get_drawing_mut(&mut self, id: &str) -> Option<&mut StoredDrawing> {
self.drawings.iter_mut().find(|d| d.id == id)
}
pub fn drawings(&self) -> &[StoredDrawing] {
&self.drawings
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
let mut storage: Self = serde_json::from_str(json)?;
storage.migrate();
Ok(storage)
}
fn migrate(&mut self) {
self.version = Self::CURRENT_VERSION;
}
pub fn clear(&mut self) {
self.drawings.clear();
self.metadata.modified_at = chrono::Utc::now().timestamp_millis();
}
}
pub fn drawing_to_stored<F, G>(drawing: &Drawing, x_to_time: F, y_to_price: G) -> StoredDrawing
where
F: Fn(f64) -> i64,
G: Fn(f64) -> f64,
{
let mut stored = StoredDrawing::new(drawing.id.to_string(), drawing.tool_type.as_str());
for point in &drawing.points {
stored.add_point(StoredCoord::from_screen(
point.x as f64,
point.y as f64,
&x_to_time,
&y_to_price,
));
}
stored.color = drawing.color.into();
stored.line_width = drawing.stroke_width;
match drawing.tool_type {
DrawingToolType::FibonacciRetracement => {
stored.set_property("show_levels", StoredValue::Bool(true));
}
DrawingToolType::Measure => {
stored.set_property("show_measurement", StoredValue::Bool(true));
}
_ => {}
}
if let Some(ref text) = drawing.text {
stored.set_property("text", StoredValue::String(text.clone()));
}
stored
}
pub fn stored_to_drawing<F, G>(
stored: &StoredDrawing,
time_to_x: F,
price_to_y: G,
next_id: usize,
) -> Option<Drawing>
where
F: Fn(i64) -> f64,
G: Fn(f64) -> f64,
{
let tool_type = stored.tool_type.parse::<DrawingToolType>().ok()?;
let points: Vec<egui::Pos2> = stored
.points
.iter()
.map(|p| {
let (x, y) = p.to_screen(&time_to_x, &price_to_y);
egui::Pos2::new(x as f32, y as f32)
})
.collect();
let mut drawing = Drawing::new(next_id, tool_type);
drawing.points = points;
drawing.color = stored.color.clone().into();
drawing.stroke_width = stored.line_width;
drawing.completed = true;
if let Some(StoredValue::String(text)) = stored.get_property("text") {
drawing.text = Some(text.clone());
}
Some(drawing)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stored_coord() {
let coord = StoredCoord::new(1704067200000, 100.50);
assert_eq!(coord.ts_ms, 1704067200000);
assert!((coord.price - 100.50).abs() < 0.01);
}
#[test]
fn test_stored_color_conversion() {
let color = Color32::from_rgb(255, 128, 64);
let stored: StoredColor = color.into();
let back: Color32 = stored.into();
assert_eq!(color, back);
}
#[test]
fn test_drawing_storage() {
let mut storage = DrawingStorage::new("AAPL");
let mut drawing = StoredDrawing::new("1", "TrendLine");
drawing.add_point(StoredCoord::new(1704067200000, 100.0));
drawing.add_point(StoredCoord::new(1704153600000, 110.0));
storage.add_drawing(drawing);
assert_eq!(storage.drawings().len(), 1);
assert!(storage.get_drawing("1").is_some());
}
#[test]
fn test_json_serialization() {
let mut storage = DrawingStorage::new("AAPL");
let mut drawing = StoredDrawing::new("1", "TrendLine");
drawing.add_point(StoredCoord::new(1704067200000, 100.0));
drawing.set_property("extended", StoredValue::Bool(true));
storage.add_drawing(drawing);
let json = storage.to_json().unwrap();
let loaded = DrawingStorage::from_json(&json).unwrap();
assert_eq!(loaded.symbol, "AAPL");
assert_eq!(loaded.drawings().len(), 1);
}
#[test]
fn test_remove_drawing() {
let mut storage = DrawingStorage::new("AAPL");
storage.add_drawing(StoredDrawing::new("1", "TrendLine"));
storage.add_drawing(StoredDrawing::new("2", "HorizontalLine"));
assert!(storage.remove_drawing("1"));
assert_eq!(storage.drawings().len(), 1);
assert!(!storage.remove_drawing("1")); }
}