Skip to main content

japan_dem/
zip_handler.rs

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        // タイルをメッシュコードでソート(西から東、南から北の順)
134        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                    // start_pointがある場合、実際のデータ開始位置
151                    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        // 実際のデータ範囲を考慮した計算(隣接タイルは1ピクセル重複)
165        let max_lon = tiles
166            .iter()
167            .map(|t| {
168                // start_pointがある場合は、実際のデータ終了位置を計算
169                let actual_end_col = if t.start_point.0 > 0 {
170                    t.cols - 1 // 最後の列まで
171                } else {
172                    t.cols - 1 // 通常のタイルも最後の列まで
173                };
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        // 結合後のサイズを計算
188        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        // タイルをグリッド位置でグループ化
201        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            // メッシュコードから位置を推定(下2桁がYX)
208            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; // 十の位がY
213                let grid_x = (two_digit_num % 10) as i32; // 一の位がX
214                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            // グリッド位置を取得(下2桁がYX)
222            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; // 十の位がY
227                let x = (two_digit_num % 10) as i32; // 一の位がX
228                (x, y)
229            } else {
230                (0, 0)
231            };
232
233            // タイルの左端位置を計算(start_pointを考慮)
234            let actual_origin_lon = if tile.start_point.0 > 0 {
235                // start_pointがある場合、実際のデータは右側にずれている
236                tile.origin_lon + (tile.start_point.0 as f64 * tile.x_res)
237            } else {
238                tile.origin_lon
239            };
240
241            // グリッド位置からオフセットを計算(重複を考慮)
242            // 実際のタイルサイズを使用
243            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            // グリッド位置に基づくオフセット計算(重複を考慮)
250            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            // メッシュコードのY座標は北が大きい(Y=9が最北端)
254            // 画像の行インデックスは上から下へ増加する
255            // したがって、Y座標が大きいタイルほど行インデックスは小さくなる
256            let max_grid_y = 9i32; // 10x10グリッドの最大Y座標
257            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            // グリッド位置に基づいて、左端/上端かどうかを判定
278            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                // 最初の20タイルでデバッグ
283                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            // データコピー(重複を考慮)
296            for row in 0..tile.rows {
297                for col in 0..tile.cols {
298                    // 重複ピクセルのスキップ判定
299                    // グリッド内で左端でない場合、左端の列をスキップ
300                    if grid_x > min_grid_x && col == 0 {
301                        continue;
302                    }
303                    // グリッド内で上端でない場合、上端の行をスキップ
304                    // Y座標が大きいほど北なので、grid_y < max_grid_yの場合は上端ではない
305                    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                        // NoDataでない値のみを書き込む
314                        if value != -9999.0 {
315                            // 調整後の位置を計算(スキップした分を考慮)
316                            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                                // デバッグ: 境界部分のデータ
326                                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}