use anyhow::Result;
use opencv::{core, imgproc, prelude::*};
use crate::{
config::RouteScale,
configs::RouteImageConfig,
types::{
drawer_data::{PositionRect, Rect, SizeRect},
fit_data::{LapData, RouteData},
},
utils::{
converter::{
convert_pace_to_sec, get_bounds, load_and_resize_image, pace_percentage,
string_space,
},
creator::image_creator,
element_drawer::Drawer,
read_file::fit_reader,
},
};
pub fn route_image(route_scale: RouteScale) -> Result<()> {
let RouteScale {
scale,
offset_x_percent,
offset_y_percent,
} = route_scale;
#[rustfmt::skip]
let (route, lap) = fit_reader("source/example.fit")?;
let RouteData {
paces: _,
gps_points: points,
distances: _,
} = route;
let LapData {
avg_heart_rate,
enhanced_avg_speed,
avg_step_length,
} = lap;
#[rustfmt::skip]
let (
(lat_min, lat_max),
(lon_min, lon_max)
) = get_bounds(&points);
let (bg_image, width, height) =
load_and_resize_image("source/example.jpg", 1080)?;
let output_file = "outputs/route.png";
let to_px = |lat: f64, lon: f64| -> core::Point {
let nx = if lon_max != lon_min {
(lon - lon_min) / (lon_max - lon_min)
} else {
0.5
};
let ny = if lat_max != lat_min {
(lat - lat_min) / (lat_max - lat_min)
} else {
0.5
};
let x = ((offset_x_percent + nx * scale) * width as f64) as i32;
#[rustfmt::skip]
let y = ((offset_y_percent + (1.0 - ny) * scale) * width as f64) as i32;
core::Point::new(x, y)
};
#[rustfmt::skip]
let pixel_points: Vec<core::Point> = points
.iter()
.map(|&(la, lo)| to_px(la, lo))
.collect();
let mut resized = Mat::default();
imgproc::resize(
&bg_image,
&mut resized,
core::Size::new(width, height),
0.0,
0.0,
imgproc::INTER_LANCZOS4,
)?;
let mut route_image = resized.clone();
let drawer = Drawer::new(width, height);
let font = crate::configs::video_config::Font::Simplex;
let font_scale = 0.5;
let thickness = 1;
let pace_seconds: Vec<f32> = enhanced_avg_speed
.iter()
.map(|p| convert_pace_to_sec(p))
.collect();
let start_x = width / 2;
let start_y = 100;
let min_val = *pace_seconds
.iter()
.min_by(|a, b| a.total_cmp(b))
.expect("Failed to find min pace");
let min_denominator = (min_val / 30.0).floor() * 30.0;
let bar_max_width = 200;
drawer
.header(
&mut route_image,
start_x,
start_y,
font_scale,
2,
font,
)
.expect("Failed to draw header!");
let green_color = drawer.color([0.0, 255.0, 0.0, 0.0]);
let white_color = drawer.color([255.0, 255.0, 255.0, 0.0]);
let size_of_speeds = enhanced_avg_speed.len();
for (i, pace) in enhanced_avg_speed.iter().enumerate() {
let size = drawer.text_size(pace, font_scale, thickness, font)?;
let x = start_x - size.width / 2;
let y = start_y + i as i32 * (size.height + 5);
let pace_space = string_space(size_of_speeds, i + 1, pace);
drawer
.text(
&mut route_image,
&pace_space,
x,
y,
font_scale,
thickness,
font,
white_color,
)
.expect("Failed to draw pace");
let hr = &format!("{}", avg_heart_rate[i]);
drawer
.text(
&mut route_image,
hr,
x + 300,
y,
font_scale,
thickness,
font,
white_color,
)
.expect("Failed to draw heart rate");
let lenght_meters = avg_step_length[i] / 10.0;
let stride_length = &format!("{}", lenght_meters);
drawer
.text(
&mut route_image,
stride_length,
x + 350,
y,
font_scale,
thickness,
font,
white_color,
)
.expect("Failed to draw stride length");
let percent = pace_percentage(min_denominator, pace_seconds[i]);
let bar_width = (percent * bar_max_width as f32) as i32;
let bar_height = size.height;
let bar_x = x + size.width + 60;
let bar_y = y - size.height;
let rect = Rect {
pos: PositionRect { x: bar_x, y: bar_y },
size: SizeRect {
width: bar_width,
height: bar_height,
},
};
drawer
.rectangle(&mut route_image, rect, green_color)
.expect("Failed to draw bar");
}
let red_color = drawer.color([0.0, 0.0, 255.0, 0.0]);
let pts = core::Vector::<core::Point>::from_iter(pixel_points.clone());
let mut all_pts = core::Vector::<core::Vector<core::Point>>::new();
all_pts.push(pts);
imgproc::polylines(
&mut route_image,
&all_pts,
false,
red_color,
2,
imgproc::LINE_AA,
0,
)?;
image_creator(output_file, &route_image)?;
println!(
"✅ Image created: {} with {} points",
output_file,
pixel_points.len()
);
Ok(())
}
pub fn image_route_with_config(config: RouteImageConfig) -> Result<()> {
let (route, lap) = fit_reader(&config.file_config.fit_file)?;
let RouteData {
paces: _,
gps_points: points,
distances: _,
} = route;
let LapData {
avg_heart_rate,
enhanced_avg_speed,
avg_step_length,
} = lap;
let ((lat_min, lat_max), (lon_min, lon_max)) = get_bounds(&points);
let (bg_image, width, height) = load_and_resize_image(
&config.file_config.background_image,
1080,
)?;
let to_px = |lat: f64, lon: f64| -> core::Point {
let nx = if lon_max != lon_min {
(lon - lon_min) / (lon_max - lon_min)
} else {
0.5
};
let ny = if lat_max != lat_min {
(lat - lat_min) / (lat_max - lat_min)
} else {
0.5
};
let x = ((config.route_scale.offset_x_percent
+ nx * config.route_scale.scale)
* width as f64) as i32;
let y = ((config.route_scale.offset_y_percent
+ (1.0 - ny) * config.route_scale.scale)
* width as f64) as i32;
core::Point::new(x, y)
};
let pixel_points: Vec<core::Point> =
points.iter().map(|&(la, lo)| to_px(la, lo)).collect();
let mut resized = Mat::default();
imgproc::resize(
&bg_image,
&mut resized,
core::Size::new(width, height),
0.0,
0.0,
imgproc::INTER_LANCZOS4,
)?;
let mut route_image = resized.clone();
let drawer = Drawer::new(width, height);
if config.show_lap_data {
if let Some(lap_config) = &config.lap_data {
let pace_seconds: Vec<f32> = enhanced_avg_speed
.iter()
.map(|p| convert_pace_to_sec(p))
.collect();
let start_x = (lap_config.position.0 * width as f64) as i32;
let start_y = (lap_config.position.1 * height as f64) as i32;
let min_val = *pace_seconds
.iter()
.min_by(|a, b| a.total_cmp(b))
.expect("Failed to find min pace");
let min_denominator = (min_val / 30.0).floor() * 30.0;
drawer
.header(
&mut route_image,
start_x,
start_y,
lap_config.font_scale,
2,
lap_config.font,
)
.expect("Failed to draw header!");
let text_color = drawer.color(lap_config.text_color.to_bgra());
let bar_color = drawer.color(config.colors.lap_bars);
let size_of_speeds = enhanced_avg_speed.len();
for (i, pace) in enhanced_avg_speed.iter().enumerate() {
let size = drawer.text_size(
pace,
lap_config.font_scale,
lap_config.thickness,
lap_config.font,
)?;
let x = start_x - size.width / 2;
let y = start_y + i as i32 * (size.height + 5);
let pace_space = string_space(size_of_speeds, i + 1, pace);
drawer
.text(
&mut route_image,
&pace_space,
x,
y,
lap_config.font_scale,
lap_config.thickness,
lap_config.font,
text_color,
)
.expect("Failed to draw pace");
if lap_config.show_heart_rate {
let hr = &format!("{}", avg_heart_rate[i]);
drawer
.text(
&mut route_image,
hr,
x + 300,
y,
lap_config.font_scale,
lap_config.thickness,
lap_config.font,
text_color,
)
.expect("Failed to draw heart rate");
}
if lap_config.show_stride_length {
let length_meters = avg_step_length[i] / 10.0;
let stride_length = &format!("{}", length_meters);
drawer
.text(
&mut route_image,
stride_length,
x + 350,
y,
lap_config.font_scale,
lap_config.thickness,
lap_config.font,
text_color,
)
.expect("Failed to draw stride length");
}
if lap_config.show_pace_bars {
let percent = pace_percentage(min_denominator, pace_seconds[i]);
let bar_width = (percent * 200.0) as i32;
let bar_height = size.height;
let bar_x = x + size.width + 60;
let bar_y = y - size.height;
let rect = Rect {
pos: PositionRect { x: bar_x, y: bar_y },
size: SizeRect {
width: bar_width,
height: bar_height,
},
};
drawer
.rectangle(&mut route_image, rect, bar_color)
.expect("Failed to draw bar");
}
}
}
}
let route_color = drawer.color(config.colors.route_line);
let pts = core::Vector::<core::Point>::from_iter(pixel_points.clone());
let mut all_pts = core::Vector::<core::Vector<core::Point>>::new();
all_pts.push(pts);
imgproc::polylines(
&mut route_image,
&all_pts,
false,
route_color,
config.line_thickness,
imgproc::LINE_AA,
0,
)?;
image_creator(
&config.file_config.output_file,
&route_image,
)?;
println!(
"✅ Image created: {} with {} points",
config.file_config.output_file,
pixel_points.len()
);
Ok(())
}