1use crate::model::DemTile;
2use crate::parser;
3use anyhow::{Context, Result};
4use std::fs::File;
5use std::io::{BufReader, Read};
6use std::path::{Path, PathBuf};
7use tracing::{debug, info, warn};
8use zip::ZipArchive;
9
10pub struct ZipHandler {
11 path: PathBuf,
12}
13
14impl ZipHandler {
15 pub fn new(path: impl AsRef<Path>) -> Self {
16 Self {
17 path: path.as_ref().to_path_buf(),
18 }
19 }
20
21 pub fn validate_filename(&self) -> Result<()> {
22 let filename = self
23 .path
24 .file_name()
25 .and_then(|n| n.to_str())
26 .context("Invalid filename")?;
27
28 if !filename.starts_with("FG-GML-") || !filename.ends_with(".zip") {
29 anyhow::bail!("Invalid ZIP filename format. Expected: FG-GML-****-DEM*A.zip");
30 }
31
32 let parts: Vec<&str> = filename.split('-').collect();
33 if parts.len() < 4 {
34 anyhow::bail!("Invalid filename format");
35 }
36
37 if !parts[3].starts_with("DEM") || !parts[3].ends_with("A") {
38 anyhow::bail!("Invalid DEM type in filename");
39 }
40
41 info!("ZIP file validated: {}", filename);
42 Ok(())
43 }
44
45 pub fn extract_xml_files(&self) -> Result<Vec<(String, Vec<u8>)>> {
46 let file = File::open(&self.path).context("Failed to open ZIP file")?;
47 let mut archive =
48 ZipArchive::new(BufReader::new(file)).context("Failed to read ZIP archive")?;
49
50 let mut xml_files = Vec::new();
51
52 for i in 0..archive.len() {
53 let mut file = archive
54 .by_index(i)
55 .context("Failed to access file in ZIP")?;
56
57 let name = file.name().to_string();
58
59 if name.ends_with(".xml") && !name.contains("__MACOSX") {
60 debug!("Found XML file: {}", name);
61
62 let mut contents = Vec::new();
63 file.read_to_end(&mut contents)
64 .context("Failed to read XML file from ZIP")?;
65
66 xml_files.push((name, contents));
67 }
68 }
69
70 if xml_files.is_empty() {
71 anyhow::bail!("No XML files found in ZIP archive");
72 }
73
74 info!("Found {} XML files in ZIP archive", xml_files.len());
75 Ok(xml_files)
76 }
77
78 pub fn process_all_tiles(&self) -> Result<Vec<DemTile>> {
79 self.validate_filename()?;
80 let xml_files = self.extract_xml_files()?;
81
82 use rayon::prelude::*;
83
84 let tiles: Vec<DemTile> = xml_files
85 .into_par_iter()
86 .filter_map(|(name, contents)| {
87 info!("Processing XML file: {}", name);
88
89 match parser::parse_dem_xml_from_bytes(&contents) {
90 Ok(tile) => {
91 debug!(
92 "Successfully parsed tile with mesh code: {:?}",
93 tile.metadata.meshcode
94 );
95 Some(tile)
96 }
97 Err(e) => {
98 warn!("Failed to parse XML file {}: {}", name, e);
99 None
100 }
101 }
102 })
103 .collect();
104
105 if tiles.is_empty() {
106 anyhow::bail!("Failed to parse any XML files from ZIP");
107 }
108
109 info!("Successfully processed {} tiles", tiles.len());
110 Ok(tiles)
111 }
112}
113
114#[derive(Debug, Clone)]
115pub struct MergedDemTile {
116 pub tiles: Vec<DemTile>,
117 pub merged_rows: usize,
118 pub merged_cols: usize,
119 pub merged_origin_lon: f64,
120 pub merged_origin_lat: f64,
121 pub merged_x_res: f64,
122 pub merged_y_res: f64,
123 pub merged_values: Vec<f32>,
124 pub crs_identifier: String,
125}
126
127impl MergedDemTile {
128 pub fn from_tiles(mut tiles: Vec<DemTile>) -> Result<Self> {
129 if tiles.is_empty() {
130 anyhow::bail!("Cannot merge empty tile list");
131 }
132
133 tiles.sort_by(|a, b| a.metadata.meshcode.cmp(&b.metadata.meshcode));
135
136 let first_tile = &tiles[0];
137 let crs = first_tile.metadata.crs_identifier.clone();
138
139 for tile in &tiles[1..] {
140 let tile_crs = &tile.metadata.crs_identifier;
141 if tile_crs != &crs {
142 anyhow::bail!("CRS mismatch: {} vs {}", crs, tile_crs);
143 }
144 }
145
146 let min_lon = tiles
147 .iter()
148 .map(|t| {
149 if t.start_point.0 > 0 {
150 t.origin_lon + (t.start_point.0 as f64 * t.x_res)
152 } else {
153 t.origin_lon
154 }
155 })
156 .min_by(|a, b| a.partial_cmp(b).unwrap())
157 .unwrap();
158 let max_lat = tiles
159 .iter()
160 .map(|t| t.origin_lat)
161 .max_by(|a, b| a.partial_cmp(b).unwrap())
162 .unwrap();
163
164 let max_lon = tiles
166 .iter()
167 .map(|t| {
168 let actual_end_col = if t.start_point.0 > 0 {
170 t.cols - 1 } else {
172 t.cols - 1 };
174 t.origin_lon + (actual_end_col as f64 * t.x_res)
175 })
176 .max_by(|a, b| a.partial_cmp(b).unwrap())
177 .unwrap();
178 let min_lat = tiles
179 .iter()
180 .map(|t| t.origin_lat - ((t.rows - 1) as f64 * t.y_res.abs()))
181 .min_by(|a, b| a.partial_cmp(b).unwrap())
182 .unwrap();
183
184 let x_res = first_tile.x_res;
185 let y_res = first_tile.y_res;
186
187 let merged_cols = ((max_lon - min_lon) / x_res).round() as usize + 1;
189 let merged_rows = ((max_lat - min_lat) / y_res.abs()).round() as usize + 1;
190
191 debug!(
192 "Merging tiles: min_lon={}, max_lon={}, min_lat={}, max_lat={}",
193 min_lon, max_lon, min_lat, max_lat
194 );
195 debug!("Resolution: x_res={}, y_res={}", x_res, y_res);
196 debug!("Merged size: {}x{}", merged_rows, merged_cols);
197
198 let mut merged_values = vec![-9999.0; merged_rows * merged_cols];
199
200 let mut tile_grid: std::collections::HashMap<(i32, i32), &DemTile> =
202 std::collections::HashMap::new();
203 let mut min_grid_x = i32::MAX;
204 let mut min_grid_y = i32::MAX;
205
206 for tile in &tiles {
207 let code = &tile.metadata.meshcode;
209 if code.len() >= 8 {
210 let last_two = &code[code.len() - 2..];
211 let two_digit_num = last_two.parse::<u32>().unwrap();
212 let grid_y = (two_digit_num / 10) as i32; let grid_x = (two_digit_num % 10) as i32; tile_grid.insert((grid_x, grid_y), tile);
215 min_grid_x = min_grid_x.min(grid_x);
216 min_grid_y = min_grid_y.min(grid_y);
217 }
218 }
219
220 for (i, tile) in tiles.iter().enumerate() {
221 let code = &tile.metadata.meshcode;
223 let (grid_x, grid_y) = if code.len() >= 8 {
224 let last_two = &code[code.len() - 2..];
225 let two_digit_num = last_two.parse::<u32>().unwrap_or(0);
226 let y = (two_digit_num / 10) as i32; let x = (two_digit_num % 10) as i32; (x, y)
229 } else {
230 (0, 0)
231 };
232
233 let actual_origin_lon = if tile.start_point.0 > 0 {
235 tile.origin_lon + (tile.start_point.0 as f64 * tile.x_res)
237 } else {
238 tile.origin_lon
239 };
240
241 let tile_width_with_overlap = tile.cols;
244 let tile_height_with_overlap = tile.rows;
245 let overlap = 1;
246 let effective_tile_width = tile_width_with_overlap - overlap;
247 let effective_tile_height = tile_height_with_overlap - overlap;
248
249 let grid_offset_x = (grid_x - min_grid_x) as usize;
251 let grid_offset_y = (grid_y - min_grid_y) as usize;
252
253 let max_grid_y = 9i32; let inverted_grid_y = (max_grid_y - grid_y) as usize;
258 let tile_col_offset = grid_offset_x * effective_tile_width;
259 let tile_row_offset = inverted_grid_y * effective_tile_height;
260
261 debug!("Tile {}: meshcode={}, grid=({},{}), inverted_y={}, origin=({}, {}), actual_origin_lon={}, offset=({}, {}), size={}x{}, start_point={:?}",
262 i,
263 tile.metadata.meshcode,
264 grid_x,
265 grid_y,
266 inverted_grid_y,
267 tile.origin_lon,
268 tile.origin_lat,
269 actual_origin_lon,
270 tile_col_offset,
271 tile_row_offset,
272 tile.rows,
273 tile.cols,
274 tile.start_point
275 );
276
277 let is_leftmost_in_grid = grid_x == min_grid_x || tile.start_point.0 > 0;
279 let is_topmost_in_grid = grid_y == min_grid_y;
280
281 if i < 20 {
282 debug!(
284 "Tile {} grid=({},{}), is_leftmost={}, is_topmost={}, offset=({},{})",
285 i,
286 grid_x,
287 grid_y,
288 is_leftmost_in_grid,
289 is_topmost_in_grid,
290 tile_col_offset,
291 tile_row_offset
292 );
293 }
294
295 for row in 0..tile.rows {
297 for col in 0..tile.cols {
298 if grid_x > min_grid_x && col == 0 {
301 continue;
302 }
303 if grid_y < 9 && row == 0 {
306 continue;
307 }
308
309 let src_idx = row * tile.cols + col;
310 if src_idx < tile.values.len() {
311 let value = tile.values[src_idx];
312
313 if value != -9999.0 {
315 let adjusted_row = if grid_y < 9 { row - 1 } else { row };
317 let adjusted_col = if grid_x > min_grid_x { col - 1 } else { col };
318
319 let dst_row = tile_row_offset + adjusted_row;
320 let dst_col = tile_col_offset + adjusted_col;
321
322 if dst_row < merged_rows && dst_col < merged_cols {
323 let dst_idx = dst_row * merged_cols + dst_col;
324
325 if i < 11 && (row < 2 || col < 2) {
327 debug!("Tile {} (grid {},{}) data[{},{}] = {} -> merged[{},{}] (skip_top={}, skip_left={})",
328 i, grid_x, grid_y, row, col, value, dst_row, dst_col,
329 grid_y < 9 && row == 0, grid_x > min_grid_x && col == 0);
330 }
331
332 merged_values[dst_idx] = value;
333 }
334 }
335 }
336 }
337 }
338 }
339
340 Ok(MergedDemTile {
341 tiles,
342 merged_rows,
343 merged_cols,
344 merged_origin_lon: min_lon,
345 merged_origin_lat: max_lat,
346 merged_x_res: x_res,
347 merged_y_res: y_res,
348 merged_values,
349 crs_identifier: crs,
350 })
351 }
352
353 pub fn to_dem_tile(&self) -> DemTile {
354 DemTile {
355 rows: self.merged_rows,
356 cols: self.merged_cols,
357 origin_lon: self.merged_origin_lon,
358 origin_lat: self.merged_origin_lat,
359 x_res: self.merged_x_res,
360 y_res: self.merged_y_res,
361 values: self.merged_values.clone(),
362 start_point: (0, 0),
363 metadata: crate::model::Metadata {
364 meshcode: format!("merged_{}", self.tiles.len()),
365 dem_type: self.tiles.first().unwrap().metadata.dem_type.clone(),
366 crs_identifier: self.crs_identifier.clone(),
367 },
368 }
369 }
370}