frinZ 4.4.0

fringe search for Yamaguchi Interferometer and Japanese VLBI Network
Documentation
use crate::args::Args;
use crate::header::parse_header;
use crate::png_compress::{compress_png_with_mode, CompressQuality};
use crate::read::read_visibility_data;
use crate::utils::radec2azalt;
use chrono::{DateTime, Duration, TimeZone, Timelike, Utc};
use plotters::coord::Shift;
use plotters::prelude::*;
use std::error::Error;
use std::fs::{self, File};
use std::io::{Cursor, Read};
use std::path::Path;

const CANVAS_SIZE: (u32, u32) = (1400, 900);

pub fn run_uptime_plot(args: &Args) -> Result<(), Box<dyn Error>> {
    let input_path = match &args.input {
        Some(path) => path,
        None => {
            return Err("Error: --uptimeplot requires an --input file.".into());
        }
    };

    let mut file = File::open(input_path)?;
    let mut buffer = Vec::new();
    file.read_to_end(&mut buffer)?;

    let mut cursor = Cursor::new(buffer.as_slice());
    let header = parse_header(&mut cursor)?;

    let mut data_cursor = Cursor::new(buffer.as_slice());
    let (_, start_time, effective_integ_time) =
        read_visibility_data(&mut data_cursor, &header, 1, args.skip, 0, false, &[])?;

    if effective_integ_time <= 0.0 {
        return Err("Effective integration time is zero; cannot generate uptime plot.".into());
    }

    let total_sectors_available = header.number_of_sector.saturating_sub(args.skip);
    let sectors_to_use = if args.length > 0 {
        args.length.min(total_sectors_available)
    } else {
        total_sectors_available
    };

    if sectors_to_use <= 0 {
        return Err("No sectors available for uptime plot after applying skip/length.".into());
    }

    let duration_seconds = (sectors_to_use as f32 * effective_integ_time)
        .round()
        .max(1.0) as usize;

    let parent_dir = input_path.parent().unwrap_or_else(|| Path::new(""));
    let output_dir = parent_dir.join("frinZ").join("uptime");
    fs::create_dir_all(&output_dir)?;
    let base_stem = input_path
        .file_stem()
        .and_then(|s| s.to_str())
        .ok_or("Invalid input filename")?;

    let observation_points_station1 = generate_time_series(
        start_time,
        duration_seconds,
        header.station1_position,
        header.source_position_ra,
        header.source_position_dec,
    );

    let observation_points_station2 = generate_time_series(
        start_time,
        duration_seconds,
        header.station2_position,
        header.source_position_ra,
        header.source_position_dec,
    );

    let day_points_station1 = generate_full_day_series(
        start_time,
        header.station1_position,
        header.source_position_ra,
        header.source_position_dec,
    );

    let day_points_station2 = generate_full_day_series(
        start_time,
        header.station2_position,
        header.source_position_ra,
        header.source_position_dec,
    );

    let plot_path = output_dir.join(format!("{}_uptime.png", base_stem));
    draw_uptime_plot(
        &plot_path,
        &observation_points_station1,
        &observation_points_station2,
        &day_points_station1,
        &day_points_station2,
        header.station1_name.trim(),
        header.station2_name.trim(),
    )?;

    println!("Generated uptime plot at {:?}", plot_path);

    Ok(())
}

struct UptimeSeries {
    az: Vec<(f64, f64)>,
    el: Vec<(f64, f64)>,
}

fn generate_time_series(
    start_time: DateTime<Utc>,
    duration_seconds: usize,
    station_position: [f64; 3],
    source_ra: f64,
    source_dec: f64,
) -> UptimeSeries {
    compute_series(
        start_time,
        duration_seconds,
        station_position,
        source_ra,
        source_dec,
    )
}

fn generate_full_day_series(
    reference_time: DateTime<Utc>,
    station_position: [f64; 3],
    source_ra: f64,
    source_dec: f64,
) -> UptimeSeries {
    let day_start = reference_time
        .date_naive()
        .and_hms_opt(0, 0, 0)
        .map(|naive| Utc.from_utc_datetime(&naive))
        .unwrap_or(reference_time);

    let mut azimuth = Vec::with_capacity(24 * 60 / 3 + 1);
    let mut elevation = Vec::with_capacity(24 * 60 / 3 + 1);

    for minute in (0..=(24 * 60)).step_by(3) {
        let current_time = day_start + Duration::seconds((minute * 60) as i64);
        let ut_hour = current_time.hour() as f64
            + current_time.minute() as f64 / 60.0
            + current_time.second() as f64 / 3600.0;

        let (az_deg, el_deg, _) = radec2azalt(
            [
                station_position[0] as f32,
                station_position[1] as f32,
                station_position[2] as f32,
            ],
            current_time,
            source_ra as f32,
            source_dec as f32,
        );

        azimuth.push((ut_hour, wrap_azimuth(az_deg as f64)));
        elevation.push((ut_hour, el_deg as f64));
    }

    UptimeSeries {
        az: azimuth,
        el: elevation,
    }
}

fn compute_series(
    start_time: DateTime<Utc>,
    duration_seconds: usize,
    station_position: [f64; 3],
    source_ra: f64,
    source_dec: f64,
) -> UptimeSeries {
    let mut azimuth = Vec::with_capacity(duration_seconds + 1);
    let mut elevation = Vec::with_capacity(duration_seconds + 1);

    for idx in 0..=duration_seconds {
        let current_time = start_time + Duration::seconds(idx as i64);
        let ut_hour = current_time.hour() as f64
            + current_time.minute() as f64 / 60.0
            + current_time.second() as f64 / 3600.0;

        let (az_deg, el_deg, _) = radec2azalt(
            [
                station_position[0] as f32,
                station_position[1] as f32,
                station_position[2] as f32,
            ],
            current_time,
            source_ra as f32,
            source_dec as f32,
        );

        azimuth.push((ut_hour, wrap_azimuth(az_deg as f64)));
        elevation.push((ut_hour, el_deg as f64));
    }

    UptimeSeries {
        az: azimuth,
        el: elevation,
    }
}

fn wrap_azimuth(mut value: f64) -> f64 {
    while value < 0.0 {
        value += 360.0;
    }
    while value >= 360.0 {
        value -= 360.0;
    }
    value
}

fn draw_uptime_plot(
    path: &Path,
    observation_station1: &UptimeSeries,
    observation_station2: &UptimeSeries,
    full_day_station1: &UptimeSeries,
    full_day_station2: &UptimeSeries,
    station1_label: &str,
    station2_label: &str,
) -> Result<(), Box<dyn Error>> {
    let root = BitMapBackend::new(path, CANVAS_SIZE).into_drawing_area();
    root.fill(&WHITE)?;
    let areas = root.split_evenly((2, 1));

    let x_range = 0.0f64..24.0f64;
    let y_range_az = 0.0f64..360.0f64;
    let y_range_el = 0.0f64..90.0f64;

    draw_single_chart(
        &areas[0],
        &observation_station1.az,
        &observation_station2.az,
        &full_day_station1.az,
        &full_day_station2.az,
        x_range.clone(),
        y_range_az,
        "AZ [deg]",
        station1_label,
        station2_label,
    )?;

    draw_single_chart(
        &areas[1],
        &observation_station1.el,
        &observation_station2.el,
        &full_day_station1.el,
        &full_day_station2.el,
        x_range,
        y_range_el,
        "EL [deg]",
        station1_label,
        station2_label,
    )?;

    root.present()?;
    compress_png_with_mode(path, CompressQuality::Low);
    Ok(())
}

fn draw_single_chart(
    area: &DrawingArea<BitMapBackend, Shift>,
    observation_station1: &[(f64, f64)],
    observation_station2: &[(f64, f64)],
    full_day_station1: &[(f64, f64)],
    full_day_station2: &[(f64, f64)],
    x_range: std::ops::Range<f64>,
    y_range: std::ops::Range<f64>,
    y_label: &str,
    station1_label: &str,
    station2_label: &str,
) -> Result<(), Box<dyn Error>> {
    let mut chart = ChartBuilder::on(area)
        .margin(15)
        .x_label_area_size(50)
        .y_label_area_size(70)
        .build_cartesian_2d(x_range.clone(), y_range.clone())?;

    let y_step = if y_range.end <= 100.0 { 10.0 } else { 30.0 };
    let y_labels = ((y_range.end - y_range.start) / y_step).round() as usize + 1;

    chart
        .configure_mesh()
        .x_desc("UT [hours]")
        .y_desc(y_label)
        .x_label_formatter(&|v| format!("{:.0}", v))
        .y_label_formatter(&|v| format!("{:.0}", v))
        .x_labels(25)
        .y_labels(y_labels)
        .light_line_style(&WHITE.mix(0.0))
        .bold_line_style(&BLACK.mix(0.2))
        .draw()?;

    if !full_day_station1.is_empty() {
        let point_style = Into::<ShapeStyle>::into(BLUE.mix(0.35)).stroke_width(1);
        chart.draw_series(PointSeries::of_element(
            full_day_station1.iter().copied(),
            4,
            point_style,
            &|coord, size, style| EmptyElement::at(coord) + Cross::new((0, 0), size, style),
        ))?;
    }

    if !observation_station1.is_empty() {
        let point_style = Into::<ShapeStyle>::into(BLUE).stroke_width(2);
        chart
            .draw_series(PointSeries::of_element(
                observation_station1.iter().copied(),
                4,
                point_style,
                &|coord, size, style| EmptyElement::at(coord) + Cross::new((0, 0), size, style),
            ))?
            .label(station1_label.to_string())
            .legend(|(x, y)| {
                Cross::new(
                    (x + 10, y),
                    6,
                    Into::<ShapeStyle>::into(BLUE).stroke_width(2),
                )
            });
    }

    if !full_day_station2.is_empty() {
        let point_style = Into::<ShapeStyle>::into(BLUE.mix(0.35)).stroke_width(1);
        chart.draw_series(PointSeries::of_element(
            full_day_station2.iter().copied(),
            4,
            point_style,
            &|coord, size, style| EmptyElement::at(coord) + Circle::new((0, 0), size, style),
        ))?;
    }

    if !observation_station2.is_empty() {
        let point_style = Into::<ShapeStyle>::into(BLUE).stroke_width(2);
        chart
            .draw_series(PointSeries::of_element(
                observation_station2.iter().copied(),
                4,
                point_style,
                &|coord, size, style| {
                    EmptyElement::at(coord) + Circle::new((0, 0), size, style.filled())
                },
            ))?
            .label(station2_label.to_string())
            .legend(|(x, y)| Circle::new((x + 10, y), 4, Into::<ShapeStyle>::into(BLUE).filled()));
    }

    chart
        .configure_series_labels()
        .label_font(("sans-serif", 20))
        .border_style(&BLACK)
        .background_style(&WHITE.mix(0.7))
        .draw()?;

    Ok(())
}