1use crate::config::Config;
2use crate::ftp::FtpClient;
3use crate::persistence::Database;
4use anyhow::{anyhow, Context, Result};
5use chrono::{DateTime, Duration, NaiveDateTime, Utc};
6use clap::Parser;
7use image::codecs::png::PngDecoder;
8use image::io::Reader as ImageReader;
9use image::{imageops, DynamicImage, ImageOutputFormat, Rgba, RgbaImage};
10use mpvipc::*;
11use serde::{Deserialize, Serialize};
12use std::ffi::OsString;
13use std::fmt::Display;
14use std::fs;
15use std::io::BufWriter;
16use std::path::{Path, PathBuf};
17use std::process::Child;
18use std::str::FromStr;
19use std::thread::sleep;
20use strum::{Display, EnumCount};
21use strum_macros::{EnumIter, EnumString};
22use suppaftp::list::File;
23use tracing::{debug, info, warn};
24
25pub type RadarId = u32;
26
27#[derive(Clone, Debug)]
28pub struct Radar {
29 pub id: RadarId,
30 pub name: String,
31 pub longitude: f32,
32 pub latitude: f32,
33 pub full_name: String,
34 pub state: String,
35 pub r#type: String,
36 pub group: bool,
37}
38
39impl From<RadarData> for Radar {
40 fn from(data: RadarData) -> Self {
41 Self {
42 id: data.id as RadarId,
43 name: data.name,
44 longitude: data.longitude,
45 latitude: data.latitude,
46 full_name: data.full_name,
47 state: data.state,
48 r#type: data.r#type,
49 group: match data.group.as_str() {
50 "Yes" => true,
51 "No" => false,
52 _ => false,
53 },
54 }
55 }
56}
57
58#[derive(Debug, Deserialize, Serialize)]
59pub struct RadarData {
60 pub name: String,
61 pub longitude: f32,
62 pub latitude: f32,
63 pub id: f64,
64 pub full_name: String,
65 pub idrnn0name: String,
66 pub idrnn1name: String,
67 pub state: String,
68 pub r#type: String,
69 pub group: String,
70 pub status: String,
71 pub archive: String,
72 pub location_id: f64,
73}
74
75#[derive(
76 Clone,
77 Copy,
78 Debug,
79 Display,
80 EnumIter,
81 Ord,
82 PartialEq,
83 PartialOrd,
84 Eq,
85 Deserialize,
86 Serialize,
87 clap::ValueEnum,
88)]
89pub enum RadarType {
90 #[serde(rename = "64km")]
91 #[clap(name = "64km")]
92 SixtyFourKm,
93 #[clap(name = "128km")]
94 #[serde(rename = "128km")]
95 OneTwentyEightKm,
96 #[serde(rename = "256km")]
97 #[clap(name = "256km")]
98 TwoFiftySixKm,
99 #[serde(rename = "512km")]
100 #[clap(name = "512km")]
101 FiveTwelveKm,
102 #[serde(rename = "doppler")]
103 #[clap(name = "doppler")]
104 DopplerWind,
105 #[serde(rename = "5min")]
106 #[clap(name = "5min")]
107 AccumulatedFiveMin,
108 #[serde(rename = "1hour")]
109 #[clap(name = "1hour")]
110 AccumulatedOneHour,
111 #[serde(rename = "since9")]
112 #[clap(name = "since9")]
113 AccumulatedSinceNine,
114 #[serde(rename = "24hour")]
115 #[clap(name = "24hour")]
116 AccumulatedPreviousTwentyFour,
117}
118
119impl RadarType {
120 pub fn from_id(id: char) -> Result<Self> {
121 let radar_type = match id {
122 '1' => Self::FiveTwelveKm,
123 '2' => Self::TwoFiftySixKm,
124 '3' => Self::OneTwentyEightKm,
125 '4' => Self::SixtyFourKm,
126 'I' => Self::DopplerWind,
127 'A' => Self::AccumulatedFiveMin,
128 'B' => Self::AccumulatedOneHour,
129 'C' => Self::AccumulatedSinceNine,
130 'D' => Self::AccumulatedPreviousTwentyFour,
131 _ => return Err(anyhow!("{id} is not a valid radar type ID")),
132 };
133
134 Ok(radar_type)
135 }
136
137 pub fn id(&self) -> char {
138 match self {
139 Self::FiveTwelveKm => '1',
140 Self::TwoFiftySixKm => '2',
141 Self::OneTwentyEightKm => '3',
142 Self::SixtyFourKm => '4',
143 Self::DopplerWind => 'I',
144 Self::AccumulatedFiveMin => 'A',
145 Self::AccumulatedOneHour => 'B',
146 Self::AccumulatedSinceNine => 'C',
147 Self::AccumulatedPreviousTwentyFour => 'D',
148 }
149 }
150
151 pub fn size(&self) -> Self {
152 match self {
153 Self::DopplerWind
154 | Self::AccumulatedFiveMin
155 | Self::AccumulatedOneHour
156 | Self::AccumulatedSinceNine
157 | Self::AccumulatedPreviousTwentyFour => Self::OneTwentyEightKm,
158 _ => *self,
159 }
160 }
161
162 pub fn update_frequency(self) -> Duration {
163 match self {
164 Self::AccumulatedSinceNine => Duration::minutes(15),
165 Self::AccumulatedPreviousTwentyFour => Duration::days(1),
166 _ => Duration::minutes(5),
167 }
168 }
169
170 pub fn check_after(self) -> Duration {
172 match self {
173 Self::AccumulatedSinceNine => Duration::minutes(15),
174 Self::AccumulatedPreviousTwentyFour => Duration::minutes(10),
175 _ => Duration::minutes(2),
176 }
177 }
178
179 pub fn min_image_count(self) -> i32 {
180 match self {
181 Self::AccumulatedPreviousTwentyFour => 10,
182 Self::AccumulatedSinceNine => 30,
183 _ => 18,
184 }
185 }
186
187 pub fn legend_type(&self) -> RadarLegendType {
188 match self {
189 Self::SixtyFourKm
190 | Self::OneTwentyEightKm
191 | Self::TwoFiftySixKm
192 | Self::FiveTwelveKm => RadarLegendType::Rainfall,
193 Self::DopplerWind => RadarLegendType::DopplerWind,
194 _ => RadarLegendType::AccumulatedRainfall,
195 }
196 }
197}
198
199#[derive(Debug, Clone, Display, EnumIter)]
200pub enum RadarLegendType {
201 Rainfall,
202 AccumulatedRainfall,
203 DopplerWind,
204}
205
206impl RadarLegendType {
207 pub fn id(&self) -> u8 {
208 match self {
209 Self::Rainfall => 0,
210 Self::AccumulatedRainfall => 1,
211 Self::DopplerWind => 2,
212 }
213 }
214}
215
216#[derive(Debug, Clone)]
218pub struct RadarImageLegend {
219 pub r#type: RadarLegendType,
220 pub png_buf: Vec<u8>,
221}
222
223#[derive(Debug)]
225pub struct RadarImageFeatureLayer {
226 pub feature: RadarImageFeature,
227 pub size: RadarType,
228 pub radar_id: RadarId,
229 pub png_buf: Vec<u8>,
230 pub filename: String,
231}
232
233#[derive(
234 Clone,
235 Display,
236 EnumString,
237 EnumIter,
238 EnumCount,
239 Debug,
240 PartialEq,
241 Deserialize,
242 Serialize,
243 clap::ValueEnum,
244)]
245#[strum(serialize_all = "lowercase")]
246#[serde(rename_all = "lowercase")]
247pub enum RadarImageFeature {
248 Background,
250 Topography,
251 Range,
252 Waterways,
253 Roads,
254 #[strum(serialize = "wthrDistricts")]
255 #[serde(rename = "wthrDistricts")]
256 #[clap(name = "wthrDistricts")]
257 ForecastDistricts,
258 Rail,
259 Catchments,
260 Locations,
261}
262
263impl RadarImageFeatureLayer {
264 pub fn from_filename(name: &str) -> Result<Self> {
265 let (first, last) = get_dot_indices(name)?;
267 let radar_type_id: char = name[first - 1..first].parse()?;
268 let feature = &name[first + 1..last];
269
270 Ok(Self {
271 feature: RadarImageFeature::from_str(feature)?,
272 size: RadarType::from_id(radar_type_id)?,
273 radar_id: name[3..first - 1].parse()?,
274 png_buf: Vec::new(),
275 filename: name.to_string(),
276 })
277 }
278}
279
280#[derive(Debug, Clone)]
282pub struct RadarImageDataLayer {
283 pub radar_type: RadarType,
284 pub png_buf: Vec<u8>,
285 pub radar_id: RadarId,
286 pub datetime: DateTime<Utc>,
287 pub filename: String,
288}
289
290impl PartialEq for RadarImageDataLayer {
291 fn eq(&self, other: &Self) -> bool {
292 self.filename == other.filename
293 }
294}
295
296impl RadarImageDataLayer {
297 pub fn from_filename(name: &str) -> Result<Self> {
298 let (first, last) = get_dot_indices(name)?;
300 let radar_type_id: char = name[first - 1..first].parse()?;
301 let timestamp = &name[first + 3..last];
302 let utc = NaiveDateTime::parse_from_str(timestamp, "%Y%m%d%H%M")?;
303
304 Ok(Self {
305 radar_type: RadarType::from_id(radar_type_id)?,
306 png_buf: Vec::new(),
307 radar_id: name[3..first - 1].parse()?,
308 datetime: DateTime::from_naive_utc_and_offset(utc, Utc),
309 filename: name.to_string(),
310 })
311 }
312
313 pub fn next_datetime(&self) -> DateTime<Utc> {
314 self.datetime + self.radar_type.update_frequency()
315 }
316
317 pub fn expected_next(&self) -> Self {
318 Self {
319 radar_id: self.radar_id,
320 radar_type: self.radar_type,
321 png_buf: Vec::new(),
322 datetime: self.next_datetime(),
323 filename: self.next_filename(),
324 }
325 }
326
327 pub fn next_filename(&self) -> String {
328 format!(
329 "IDR{:02}{1}.T.{2}.png",
330 self.radar_id,
331 self.radar_type.id(),
332 self.next_datetime().format("%Y%m%d%H%M")
333 )
334 }
335}
336
337fn get_dot_indices(name: &str) -> Result<(usize, usize)> {
338 let err = Err(anyhow!("{name} is not a valid radar image file"));
339 let Some(first_dot) = name.find('.') else {
340 return err;
341 };
342 let Some(last_dot) = name.rfind('.') else {
343 return err;
344 };
345 if first_dot == last_dot {
346 return err;
347 };
348 Ok((first_dot, last_dot))
349}
350
351#[derive(Clone, Parser, Debug, Deserialize, Serialize)]
352pub struct RadarImageOptions {
353 pub features: Vec<RadarImageFeature>,
354 pub max_frames: Option<u64>,
355 pub radar_types: Vec<RadarType>,
356 pub remove_header: bool,
357 pub create_png: bool,
358 pub create_apng: bool,
359 pub frame_delay_ms: u16,
360 pub image_dir: PathBuf,
361 pub force: bool,
362 pub open_mpv: bool,
363 pub mpv_args: Vec<String>,
364 pub mpv_ipc_dir: PathBuf,
365}
366
367impl Default for RadarImageOptions {
368 fn default() -> Self {
369 Self {
370 features: vec![
371 RadarImageFeature::Background,
372 RadarImageFeature::Topography,
373 RadarImageFeature::Range,
374 RadarImageFeature::Locations,
375 ],
376 frame_delay_ms: 200,
377 max_frames: Some(24),
378 radar_types: vec![RadarType::OneTwentyEightKm],
379 remove_header: false,
380 create_png: true,
381 create_apng: false,
382 force: false,
383 open_mpv: false,
384 mpv_args: vec![
385 "--stop-screensaver=no".into(),
386 "--geometry=1024x1114".into(),
387 "--auto-window-resize=no".into(),
388 "--loop-playlist".into(),
389 "--no-osc".into(),
390 "--no-osd-bar".into(),
391 "--really-quiet".into(),
392 ],
393 mpv_ipc_dir: Config::default_dirs().run.join("mpv-ipc"),
394 image_dir: Config::default_dirs().state.join("radar-images"),
395 }
396 }
397}
398
399#[derive(Debug)]
400pub struct RadarImageComponents {
401 pub legend: RadarImageLegend,
402 pub feature_layers: Vec<RadarImageFeatureLayer>,
403 pub data_layers: Vec<RadarImageDataLayer>,
404}
405
406#[derive(Debug, Clone)]
407pub struct RadarImageFrame {
408 pub radar_type: RadarType,
409 pub image: DynamicImage,
410 pub radar_id: RadarId,
411 pub datetime: DateTime<Utc>,
412 pub path: PathBuf,
413}
414
415impl RadarImageFrame {
416 pub fn from_path(path: PathBuf) -> Result<Self> {
417 let name = path.file_name().unwrap().to_string_lossy();
418 let (first, last) = get_dot_indices(&name)?;
419 let radar_type_id: char = name[first - 1..first].parse()?;
420 let timestamp = &name[first + 3..last];
421 let utc = NaiveDateTime::parse_from_str(timestamp, "%Y%m%d%H%M")?;
422 let image = ImageReader::open(&path)?.decode()?;
423
424 Ok(Self {
425 radar_type: RadarType::from_id(radar_type_id)?,
426 radar_id: name[3..first - 1].parse()?,
427 datetime: DateTime::from_naive_utc_and_offset(utc, Utc),
428 path,
429 image,
430 })
431 }
432}
433
434impl PartialEq for RadarImageFrame {
435 fn eq(&self, other: &Self) -> bool {
436 self.path == other.path
437 }
438}
439
440fn decode_png(png_buf: &[u8]) -> Result<DynamicImage> {
441 let decoder = PngDecoder::new(png_buf)?;
442 let img = DynamicImage::from_decoder(decoder)?;
443 Ok(img)
444}
445
446pub fn get_radar_image_managers<'a>(
447 id: RadarId,
448 db: &'a mut Database,
449 ftp: &'a mut FtpClient,
450 opts: &'a RadarImageOptions,
451) -> Result<Vec<RadarImageManager>> {
452 let mut managers = Vec::new();
453
454 for radar_type in &opts.radar_types {
455 let feature_layers = if let Ok(layers) = db.get_radar_feature_layers(id, radar_type) {
456 layers
457 } else {
458 info!(
459 "Fetching feature layers for IDR{:02}{}",
460 id,
461 radar_type.id()
462 );
463 let layers = ftp.get_radar_feature_layers(id, *radar_type)?;
464 db.insert_radar_feature_layers(&layers)?;
465 layers
466 };
467
468 let data_layers =
469 if let Ok(layers) = db.get_radar_data_layers(id, radar_type, opts.max_frames) {
470 layers
471 } else {
472 Vec::new()
473 };
474
475 let legend = db.get_radar_legend(radar_type)?;
476 let manager = RadarImageManager::new(
477 id,
478 *radar_type,
479 legend,
480 data_layers,
481 feature_layers,
482 opts.clone(),
483 )?;
484 managers.push(manager);
485 }
486 Ok(managers)
487}
488
489pub fn fetch_new_data_layers(
490 id: RadarId,
491 db: &mut Database,
492 radar_type: &RadarType,
493 ftp: &mut FtpClient,
494 ftp_files: &mut Vec<File>,
495 opts: &RadarImageOptions,
496) -> Result<Vec<RadarImageDataLayer>> {
497 let now = Utc::now();
498
499 let mut new_layers = Vec::new();
500 let mut check_ftp_files = false;
501
502 let existing_names = db.get_radar_data_layer_names(radar_type)?;
503 info!(
504 "Fetching new data layers for IDR{:02}{}",
505 id,
506 radar_type.id()
507 );
508 if let Some(last_name) = existing_names.last() {
509 let mut last_layer = &RadarImageDataLayer::from_filename(last_name).unwrap();
510 loop {
511 let time_since_last = now - last_layer.datetime;
512 if time_since_last > radar_type.update_frequency() * radar_type.min_image_count() {
513 check_ftp_files = true;
514 break;
515 }
516
517 let mut next_layer = last_layer.expected_next();
518 if now < next_layer.datetime + next_layer.radar_type.check_after() {
519 ftp.keepalive()?;
520 break;
521 }
522
523 match ftp.get_radar_data_png(&next_layer.filename) {
527 Ok(png_buf) => {
528 next_layer.png_buf = png_buf;
529 new_layers.push(next_layer);
530 last_layer = new_layers.last().unwrap();
531 }
532 Err(e) => {
533 debug!(
534 "Failed to download radar data layer {}. {e}",
535 next_layer.filename
536 );
537 if now
538 > next_layer.datetime
539 + next_layer.radar_type.check_after()
540 + Duration::minutes(1)
541 {
542 check_ftp_files = true;
543 }
544 break;
545 }
546 }
547 }
548 } else {
549 check_ftp_files = true;
550 }
551
552 if check_ftp_files {
553 if ftp_files.is_empty() {
554 for file in ftp.list_radar_data_layers()? {
555 ftp_files.push(file);
556 }
557 }
558 let last_layer = existing_names
559 .last()
560 .map(|n| RadarImageDataLayer::from_filename(n).unwrap());
561 let mut todo = Vec::new();
562 let prefix = format!("IDR{id:02}{}", radar_type.id());
563 let mut count = 0;
564 for file in ftp_files.iter().filter(|f| f.name().starts_with(&prefix)) {
565 count += 1;
566 let layer = RadarImageDataLayer::from_filename(file.name())?;
567 if existing_names.contains(&layer.filename) || new_layers.contains(&layer) {
568 continue;
569 }
570 if let Some(ref last) = last_layer {
571 if layer.datetime < last.datetime {
572 continue;
573 }
574 }
575 todo.push(layer);
576 }
577 if let Some(max_frames) = opts.max_frames {
578 if todo.len() > max_frames as usize {
579 let idx = todo.len() - max_frames as usize;
580 todo.drain(..idx);
581 }
582 }
583 for layer in todo.iter_mut() {
584 layer.png_buf = ftp.get_radar_data_png(&layer.filename)?;
585 }
586 new_layers.append(&mut todo);
587 if count == 0 {
588 warn!(
589 "No images for {radar_type} found on FTP. \
590 Some radars have limited data available. Consider adjusting your config"
591 );
592 }
593 }
594 Ok(new_layers)
595}
596
597pub fn update_radar_images(
598 managers: &mut Vec<RadarImageManager>,
599 db: &mut Database,
600 ftp: &mut FtpClient,
601) -> Result<DateTime<Utc>> {
602 let mut ftp_files = Vec::new();
604 let mut next_datetimes = Vec::new();
605 for m in managers {
606 let new_data_layers =
607 fetch_new_data_layers(m.radar_id, db, &m.radar_type, ftp, &mut ftp_files, &m.opts)?;
608 if !new_data_layers.is_empty() {
609 db.insert_radar_data_layers(&new_data_layers)?;
610 m.add_data_layers(new_data_layers);
611 }
612 if let Some(last) = m.data_layers.last() {
613 next_datetimes.push(last.next_datetime() + last.radar_type.check_after());
614 }
615 }
616 let next_layer_due = next_datetimes.into_iter().min().unwrap();
617 let next_check = if next_layer_due > Utc::now() {
618 next_layer_due
619 } else {
620 Utc::now() + Duration::seconds(60)
621 };
622 Ok(next_check)
623}
624
625pub struct RadarImageManager {
626 image_dir: PathBuf,
627 radar_type: RadarType,
628 radar_id: RadarId,
629 legend: RadarImageLegend,
630 feature_layers: Vec<RadarImageFeatureLayer>,
631 data_layers: Vec<RadarImageDataLayer>,
632 frames: Vec<RadarImageFrame>,
633 pub opts: RadarImageOptions,
634 mpv: Option<MpvRadarViewer>,
635}
636
637impl Display for RadarImageManager {
638 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
639 write!(f, "IDR{:02}{}", self.radar_id, self.radar_type.id())
640 }
641}
642
643impl RadarImageManager {
644 pub fn new(
645 radar_id: RadarId,
646 radar_type: RadarType,
647 legend: RadarImageLegend,
648 data_layers: Vec<RadarImageDataLayer>,
649 feature_layers: Vec<RadarImageFeatureLayer>,
650 opts: RadarImageOptions,
651 ) -> Result<Self> {
652 let mpv = MpvRadarViewer::from_opts(radar_id, radar_type, &opts)?;
653 let image_dirname = format!("IDR{:02}{}", radar_id, radar_type.id());
654 let image_dir = opts.image_dir.join(image_dirname);
655 let mut frames = Vec::new();
656 if let Ok(entries) = fs::read_dir(&image_dir) {
657 for entry in entries {
658 if let Ok(frame) = RadarImageFrame::from_path(entry.unwrap().path()) {
659 frames.push(frame);
660 }
661 }
662 }
663
664 Ok(Self {
665 mpv,
666 radar_id,
667 radar_type,
668 legend,
669 data_layers,
670 feature_layers,
671 frames,
672 image_dir,
673 opts,
674 })
675 }
676
677 fn construct_frames(&mut self) -> Result<()> {
678 let mut bottom_layer = decode_png(&self.legend.png_buf)?;
679 let mut top_layer = DynamicImage::ImageRgba8(RgbaImage::new(512, 512));
680
681 if self.opts.force {
682 self.frames.clear();
683 }
684
685 let mut todo = Vec::new();
686 for layer in &self.data_layers {
687 if !self.frames.iter().any(|f| f.datetime == layer.datetime) {
688 todo.push(layer);
689 }
690 }
691
692 if todo.is_empty() {
693 return Ok(());
694 }
695
696 for feature in &self.opts.features {
697 match feature {
698 RadarImageFeature::Background | RadarImageFeature::Topography => {
699 self.overlay_feature(&mut bottom_layer, feature)?
700 }
701 _ => self.overlay_feature(&mut top_layer, feature)?,
702 }
703 }
704
705 for layer in todo {
706 debug!("Constructing frame for {}", layer.filename);
707 let mut data_layer = decode_png(&layer.png_buf)?;
708 if self.opts.remove_header {
709 self.remove_header(&mut data_layer);
710 }
711 let mut final_image = bottom_layer.clone();
712 imageops::overlay(&mut final_image, &data_layer, 0, 0);
713 imageops::overlay(&mut final_image, &top_layer, 0, 0);
714 let frame = RadarImageFrame {
715 radar_id: layer.radar_id,
716 radar_type: layer.radar_type,
717 path: self.image_dir.join(&layer.filename),
718 datetime: layer.datetime,
719 image: final_image,
720 };
721 self.frames.push(frame);
722 }
723 Ok(())
724 }
725
726 fn remove_header(&self, image: &mut DynamicImage) {
727 let DynamicImage::ImageRgba8(ref mut rgba_image) = image else {
728 return;
729 };
730 for y in 0..16 {
731 for x in 0..512 {
732 rgba_image.put_pixel(x, y, Rgba([0, 0, 0, 0]));
733 }
734 }
735 }
736
737 fn overlay_feature(&self, base: &mut DynamicImage, feature: &RadarImageFeature) -> Result<()> {
738 let Some(layer) = self.feature_layers.iter().find(|l| l.feature == *feature) else {
739 warn!("Missing {feature} feature");
740 return Ok(());
741 };
742 imageops::overlay(base, &decode_png(&layer.png_buf)?, 0, 0);
743 Ok(())
744 }
745
746 fn sort_frames(&mut self) {
747 self.frames
748 .sort_by(|a, b| a.datetime.partial_cmp(&b.datetime).unwrap());
749 }
750
751 fn remove_images(&mut self, idx: usize) -> Result<Vec<RadarImageDataLayer>> {
752 let didx = idx.min(self.data_layers.len());
754 let removed = self.data_layers.drain(..didx).collect();
755 for frame in self.frames.drain(..idx) {
756 if frame.path.exists() {
757 debug!("Deleting old radar image {}", frame.path.display());
758 fs::remove_file(&frame.path)?;
759 }
760 }
761 Ok(removed)
762 }
763
764 pub fn prune(&mut self) -> Result<Vec<RadarImageDataLayer>> {
765 let mut removed = Vec::new();
766 self.sort_frames();
767 if let Some(max_frames) = self.opts.max_frames {
768 if self.frames.len() > max_frames as usize {
769 let idx = self.frames.len() - max_frames as usize;
770 debug!("{self} frame limit of {max_frames} exceeded by {idx}");
771 removed.extend(self.remove_images(idx)?);
772 }
773 }
774
775 let mut iter = self.frames.iter().enumerate().peekable();
776 while let Some((idx, frame)) = iter.next() {
777 if let Some((_, next_frame)) = iter.peek() {
778 let gap = next_frame.datetime - frame.datetime;
779 let max_gap = frame.radar_type.update_frequency() * 4;
780 if next_frame.datetime - frame.datetime > max_gap {
781 debug!(
782 "{self} has gap of {} minutes between images. Removing old images",
783 gap.num_minutes()
784 );
785 removed.extend(self.remove_images(idx + 1)?);
786 break;
787 }
788 }
789 }
790 Ok(removed)
791 }
792 pub fn write_pngs(&mut self) -> Result<()> {
793 self.construct_frames()?;
794 fs::create_dir_all(&self.image_dir)?;
795 for frame in &self.frames {
796 if frame.path.exists() && !self.opts.force {
797 continue;
798 }
799 let file = fs::File::create(&frame.path)?;
800 let mut writer = BufWriter::new(file);
801 frame.image.write_to(&mut writer, ImageOutputFormat::Png)?;
802 }
803 Ok(())
804 }
805
806 pub fn add_data_layers(&mut self, mut layers: Vec<RadarImageDataLayer>) {
807 self.data_layers.append(&mut layers);
808 }
809
810 pub fn open_images(&mut self) -> Result<()> {
811 self.write_pngs()?;
812 self.sort_frames();
813 let paths: Vec<&Path> = self.frames.iter().map(|f| f.path.as_path()).collect();
814 let mpv = self.mpv.as_mut().unwrap();
815 mpv.open_images(&paths, &self.opts.mpv_args)?;
816 Ok(())
817 }
818
819 pub fn create_apng(&mut self) -> Result<()> {
820 self.construct_frames()?;
821 let start = self.frames.first().unwrap().datetime.format("%Y%m%d%H%M");
822 let end = self.frames.last().unwrap().datetime.format("%Y%m%d%H%M");
823 let filename = format!("{}.T.{}-{}.png", self, start, end);
824 let path = self.image_dir.join(filename);
825 let out_file = fs::File::create(path)?;
826 let mut writer = BufWriter::new(out_file);
827 let mut pngs = Vec::new();
828 for frame in &self.frames {
829 let png = apng::load_dynamic_image(frame.image.clone())?;
830 pngs.push(png);
831 }
832
833 let config = apng::create_config(&pngs, None).unwrap();
834 let mut encoder = apng::Encoder::new(&mut writer, config).unwrap();
835 let apng_frame = apng::Frame {
836 delay_num: Some(self.opts.frame_delay_ms),
837 delay_den: Some(1000),
838 ..Default::default()
839 };
840 encoder.encode_all(pngs, Some(&apng_frame)).unwrap();
841
842 Ok(())
843 }
844}
845
846struct MpvRadarViewer {
847 handle: Option<Child>,
848 socket_path: PathBuf,
849 radar_id: RadarId,
850 radar_type: RadarType,
851 frame_delay: f32,
852}
853
854impl MpvRadarViewer {
855 pub fn from_opts(
856 radar_id: RadarId,
857 radar_type: RadarType,
858 opts: &RadarImageOptions,
859 ) -> Result<Option<Self>> {
860 if opts.open_mpv {
861 let output = std::process::Command::new("mpv")
862 .arg("--version")
863 .output()
864 .context("Failed to open MPV")?;
865
866 let stdout = String::from_utf8_lossy(&output.stdout);
867 if !output.status.success() {
868 return Err(anyhow!("Failed to open MPV. {stdout}"));
869 } else {
870 debug!("Using {}", stdout);
871 }
872
873 let socket_name = format!("IDR{:02}{}.sock", radar_id, radar_type.id());
874 Ok(Some(Self {
875 radar_id,
876 radar_type,
877 socket_path: opts.mpv_ipc_dir.join(socket_name),
878 handle: None,
879 frame_delay: opts.frame_delay_ms as f32 / 1000.0,
880 }))
881 } else {
882 Ok(None)
883 }
884 }
885
886 pub fn open_images(&mut self, paths: &[&Path], args: &[String]) -> Result<()> {
887 let mut mpv_is_running = false;
888 if let Some(ref mut handle) = self.handle {
889 match handle.try_wait() {
890 Ok(Some(status)) => {
891 warn!("MPV {} exited with: {}", self.socket_path.display(), status);
892 }
893 Ok(None) => mpv_is_running = true,
894 Err(e) => warn!("Error waiting for MPV {} {e}", self.socket_path.display()),
895 }
896 }
897 if !mpv_is_running {
898 self.start(paths, args)?;
899 }
900 let mpv = self.connect()?;
901 mpv.playlist_clear()?;
902 let command = MpvCommand::LoadFile {
903 file: paths[0].to_string_lossy().to_string(),
904 option: PlaylistAddOptions::Replace,
905 };
906 mpv.run_command(command)?;
907 for path in &paths[1..] {
908 let command = MpvCommand::LoadFile {
909 file: path.to_string_lossy().to_string(),
910 option: PlaylistAddOptions::Append,
911 };
912 mpv.run_command(command).unwrap();
913 }
914 Ok(())
915 }
916
917 fn connect(&self) -> Result<Mpv> {
918 let mut attempts = 0;
919 let mpv = loop {
920 match Mpv::connect(&self.socket_path.to_string_lossy()) {
921 Ok(mpv) => break mpv,
922 _ => {
923 sleep(Duration::milliseconds(20).to_std().unwrap());
924 attempts += 1;
925 if attempts > 10 {
926 return Err(anyhow!(
927 "Failed to connect to MPV at {}",
928 self.socket_path.display()
929 ));
930 }
931 }
932 }
933 };
934 Ok(mpv)
935 }
936
937 fn start(&mut self, image_paths: &[&Path], args: &[String]) -> Result<()> {
938 let mut ipc_arg = OsString::from("--input-ipc-server=");
939 ipc_arg.push(&self.socket_path);
940 let app_id = format!("mpv-radar-IDR{:02}{}", self.radar_id, self.radar_type.id());
941 fs::create_dir_all(self.socket_path.parent().unwrap())?;
942 let child = std::process::Command::new("mpv")
943 .arg(ipc_arg)
944 .arg(format!("--wayland-app-id={app_id}"))
945 .arg(format!("--x11-name={app_id}"))
946 .arg(format!("--image-display-duration={}", self.frame_delay))
947 .arg("--loop-file=no")
948 .args(args)
949 .args(image_paths)
950 .spawn()
951 .unwrap();
952
953 self.handle = Some(child);
954 Ok(())
955 }
956}