1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
use std::{collections::HashSet, ffi::OsString, fs, io, ops::Range, path::Path, sync::LazyLock};

use ::error::Context;
use image::{imageops, ImageBuffer, ImageError, Rgba, RgbaImage};
use itertools::Itertools;
use path_macro::path;
use rayon::iter::ParallelIterator;
use regex::Regex;
use rs3cache_backend::error::{self, CacheResult};
use rs3cache_utils::bar::Render;

use crate::{cli::Config, renderers::scale};

static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?P<p>\d+)(?:_)(?P<i>\d+)(?:_)(?P<j>\d+)(?:\.png)").expect("Regex is cursed."));

/// Given a folder and a range of zoom levels, recursively creates tiles for all zoom levels.
pub fn render_zoom_levels(config: &Config, name: &str, mapid: i32, range: Range<i8>, backfill: [u8; 4]) -> CacheResult<()> {
    let zoom_levels = range.rev();
    for zoom in zoom_levels {
        let path = path!(config.output / name / format!("{mapid}/{zoom}"));
        fs::create_dir_all(&path).context(error::Io { path })?;

        let new_tile_coordinates = get_future_filenames(config, name, mapid, zoom + 1)?.into_iter();

        let func = |((p, i, j), _)| {
            let img = make_tile(&config.output, name, mapid, zoom, p, i, j, backfill)?;
            let path = path!(config.output / &name / format!("{mapid}/{zoom}/{p}_{i}_{j}.png"));

            match img.save(&path) {
                Ok(()) => {}
                Err(ImageError::IoError(e)) => return Err(e).context(error::Io { path }),
                Err(other) => panic!("{other}"),
            };
            Ok(())
        };

        new_tile_coordinates.render(format!("{name} zoom level {zoom}")).try_for_each(func)?;
    }
    Ok(())
}

fn to_coordinates(text: OsString) -> (i32, i32, i32) {
    let caps = RE.captures(text.to_str().unwrap()).unwrap();
    let p = caps.name("p").unwrap().as_str().parse::<i32>().unwrap();
    let i = caps.name("i").unwrap().as_str().parse::<i32>().unwrap();
    let j = caps.name("j").unwrap().as_str().parse::<i32>().unwrap();
    (p, i, j)
}

fn make_tile(
    folder: impl AsRef<Path>,
    name: &str,
    mapid: i32,
    target_zoom: i8,
    target_plane: i32,
    target_i: i32,
    target_j: i32,
    backfill: [u8; 4],
) -> CacheResult<ImageBuffer<Rgba<u8>, Vec<u8>>> {
    let mut base = RgbaImage::from_fn(512, 512, |_, _| Rgba(backfill));

    let files = get_files(folder, name, mapid, target_zoom, target_plane, target_i, target_j);

    for ((di, dj), img) in files {
        match img {
            Ok(f) => {
                let img = f.into_rgba8();
                imageops::overlay(&mut base, &img, (256 * di) as i64, 256 * (1 - dj) as i64);
            }
            // can be missing; if so, swallow
            Err(ImageError::IoError(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
            Err(other_error) => panic!("{other_error}"),
        }
    }
    let scaled = scale::resize_half(base);
    Ok(scaled)
}

fn get_files(
    folder: impl AsRef<Path>,
    name: &str,
    mapid: i32,
    target_zoom: i8,
    target_plane: i32,
    target_i: i32,
    target_j: i32,
) -> [((i32, i32), Result<image::DynamicImage, ImageError>); 4] {
    [(0, 0), (0, 1), (1, 0), (1, 1)].map(|(di, dj)| {
        let i = (target_i << 1) + di;
        let j = (target_j << 1) + dj;
        let zoom = target_zoom + 1;
        let filename = path!(folder / name / format!("{mapid}/{zoom}/{target_plane}_{i}_{j}.png"));
        ((di, dj), image::open(filename))
    })
}

fn get_future_filenames(config: &Config, name: &str, mapid: i32, zoom: i8) -> CacheResult<HashSet<(i32, i32, i32)>> {
    let path = path!(config.output / name / format!("{mapid}/{zoom}"));

    fs::read_dir(&path)
        .with_context(|| error::Io { path: path.clone() })?
        .map_ok(|entry| {
            let name = entry.file_name();
            let (p, i, j) = to_coordinates(name);
            (p, i >> 1, j >> 1)
        })
        .collect::<Result<_, _>>()
        .context(error::Io { path })
}