Promouvoir une dalle voisine déjà téléchargée au lieu de la retélécharger, nettoyer les doublons existants
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@ -216,6 +216,7 @@ def fetch_tiles(input_dir, specs, output_dir=None, only_viz=None, resolutions=(0
|
|||||||
Returns:
|
Returns:
|
||||||
Liste des chemins téléchargés.
|
Liste des chemins téléchargés.
|
||||||
"""
|
"""
|
||||||
|
from .dtm import EDGE_NEIGHBORS_DIRNAME
|
||||||
input_dir = Path(input_dir)
|
input_dir = Path(input_dir)
|
||||||
complete = None
|
complete = None
|
||||||
if output_dir is not None and only_viz and not force:
|
if output_dir is not None and only_viz and not force:
|
||||||
@ -223,6 +224,10 @@ def fetch_tiles(input_dir, specs, output_dir=None, only_viz=None, resolutions=(0
|
|||||||
keys = [step_to_keyword(v) for v in only_viz]
|
keys = [step_to_keyword(v) for v in only_viz]
|
||||||
complete = cells_with_all_viz(Path(output_dir) / "visualisations",
|
complete = cells_with_all_viz(Path(output_dir) / "visualisations",
|
||||||
keys, resolutions)
|
keys, resolutions)
|
||||||
|
# Explicite : jamais de promotion quand on télécharge directement DANS
|
||||||
|
# edge_neighbors/ (appel de _fetch_edge_neighbors avec input_dir=edge_dir)
|
||||||
|
# — sinon une dalle voisine pourrait se retrouver déplacée vers elle-même.
|
||||||
|
is_edge_dir = input_dir.name == EDGE_NEIGHBORS_DIRNAME
|
||||||
downloaded = []
|
downloaded = []
|
||||||
for col, row in specs:
|
for col, row in specs:
|
||||||
name = tile_filename(col, row)
|
name = tile_filename(col, row)
|
||||||
@ -232,6 +237,19 @@ def fetch_tiles(input_dir, specs, output_dir=None, only_viz=None, resolutions=(0
|
|||||||
logger.info(f" {name} : déjà présent dans input/ — aucun téléchargement")
|
logger.info(f" {name} : déjà présent dans input/ — aucun téléchargement")
|
||||||
_report_download(output_dir, tile, "skip", "déjà dans input/")
|
_report_download(output_dir, tile, "skip", "déjà dans input/")
|
||||||
continue
|
continue
|
||||||
|
if not is_edge_dir:
|
||||||
|
# dest.exists() ci-dessus ne teste que le nom exact : un ".part"
|
||||||
|
# voisin n'y correspond jamais, donc .exists() suffit à exclure
|
||||||
|
# un téléchargement voisin encore en cours.
|
||||||
|
neighbor_copy = input_dir / EDGE_NEIGHBORS_DIRNAME / name
|
||||||
|
if neighbor_copy.exists():
|
||||||
|
import os
|
||||||
|
os.replace(neighbor_copy, dest)
|
||||||
|
logger.info(f" {name} : déjà téléchargée comme voisine "
|
||||||
|
f"— déplacée dans input/")
|
||||||
|
_report_download(output_dir, tile, "skip",
|
||||||
|
"déjà téléchargée comme voisine — déplacée dans input/")
|
||||||
|
continue
|
||||||
if output_dir is not None and not force:
|
if output_dir is not None and not force:
|
||||||
if complete is not None:
|
if complete is not None:
|
||||||
if (col, row) in complete:
|
if (col, row) in complete:
|
||||||
|
|||||||
@ -496,6 +496,7 @@ class LidarArchaeoPipeline:
|
|||||||
from .dtm import _tile_coords, _NEIGHBOR_OFFSETS, EDGE_NEIGHBORS_DIRNAME
|
from .dtm import _tile_coords, _NEIGHBOR_OFFSETS, EDGE_NEIGHBORS_DIRNAME
|
||||||
from .fetch_ign import fetch_tiles, tile_filename
|
from .fetch_ign import fetch_tiles, tile_filename
|
||||||
edge_dir = self.input_dir / EDGE_NEIGHBORS_DIRNAME
|
edge_dir = self.input_dir / EDGE_NEIGHBORS_DIRNAME
|
||||||
|
self._cleanup_edge_neighbor_duplicates(edge_dir)
|
||||||
wanted = set()
|
wanted = set()
|
||||||
for laz_file in files:
|
for laz_file in files:
|
||||||
coords = _tile_coords(Path(laz_file).name)
|
coords = _tile_coords(Path(laz_file).name)
|
||||||
@ -523,6 +524,38 @@ class LidarArchaeoPipeline:
|
|||||||
logger.info(f"Raccord des bords : {len(fetched)}/{len(missing)} dalle(s) "
|
logger.info(f"Raccord des bords : {len(fetched)}/{len(missing)} dalle(s) "
|
||||||
f"voisine(s) téléchargée(s) ({time.time() - t0:.0f} s)")
|
f"voisine(s) téléchargée(s) ({time.time() - t0:.0f} s)")
|
||||||
|
|
||||||
|
def _cleanup_edge_neighbor_duplicates(self, edge_dir):
|
||||||
|
"""Supprime de edge_neighbors/ les dalles déjà présentes dans input/.
|
||||||
|
|
||||||
|
Une dalle téléchargée comme voisine avant d'être requise comme tuile
|
||||||
|
principale (ou l'inverse) peut se retrouver dupliquée aux deux
|
||||||
|
emplacements (avant que fetch_tiles ne promeuve les nouvelles
|
||||||
|
voisines) ; l'exemplaire d'input/ fait foi (_neighbor_laz_files
|
||||||
|
cherche dans input/ avant edge_neighbors/), le doublon ne sert donc
|
||||||
|
à rien et peut peser plusieurs centaines de Mo. Les fichiers ".part"
|
||||||
|
(téléchargement en cours) ne sont jamais touchés.
|
||||||
|
"""
|
||||||
|
from .dtm import EDGE_NEIGHBORS_DIRNAME
|
||||||
|
if not edge_dir.is_dir():
|
||||||
|
return
|
||||||
|
removed = 0
|
||||||
|
freed = 0
|
||||||
|
for path in edge_dir.iterdir():
|
||||||
|
if not path.is_file() or path.suffix == ".part" or path.name.endswith(".part"):
|
||||||
|
continue
|
||||||
|
twin = self.input_dir / path.name
|
||||||
|
if twin.exists():
|
||||||
|
try:
|
||||||
|
freed += path.stat().st_size
|
||||||
|
path.unlink()
|
||||||
|
removed += 1
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
if removed:
|
||||||
|
logger.info(f"Raccord des bords : {removed} doublon(s) supprimé(s) de "
|
||||||
|
f"{EDGE_NEIGHBORS_DIRNAME}/ (déjà dans input/, "
|
||||||
|
f"{freed / 1e6:.0f} Mo libérés)")
|
||||||
|
|
||||||
def _report(self, basename, phase, state, detail=None, res=None):
|
def _report(self, basename, phase, state, detail=None, res=None):
|
||||||
"""Émet un événement de progression (file de génération, best-effort)."""
|
"""Émet un événement de progression (file de génération, best-effort)."""
|
||||||
report_event(self.output_dir, basename, phase, state, detail=detail, res=res)
|
report_event(self.output_dir, basename, phase, state, detail=detail, res=res)
|
||||||
|
|||||||
@ -146,6 +146,66 @@ def test_fetch_tiles_downloads_incomplete_tile(tmp_path, monkeypatch):
|
|||||||
assert [p.name for p in result] == [fetch_ign.tile_filename(1054, 6882)]
|
assert [p.name for p in result] == [fetch_ign.tile_filename(1054, 6882)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_tiles_promotes_edge_neighbor_copy(tmp_path, monkeypatch):
|
||||||
|
"""Une dalle déjà téléchargée comme voisine est déplacée dans input/ sans réseau."""
|
||||||
|
from lidar_pipeline import fetch_ign
|
||||||
|
|
||||||
|
input_dir = tmp_path / "input"
|
||||||
|
input_dir.mkdir()
|
||||||
|
edge_dir = input_dir / "edge_neighbors"
|
||||||
|
edge_dir.mkdir()
|
||||||
|
name = fetch_ign.tile_filename(1055, 6882)
|
||||||
|
neighbor_copy = edge_dir / name
|
||||||
|
neighbor_copy.write_bytes(b"laz-voisine")
|
||||||
|
|
||||||
|
def fail(*args, **kwargs):
|
||||||
|
raise AssertionError("ne doit pas être appelé (pas de réseau attendu)")
|
||||||
|
|
||||||
|
monkeypatch.setattr(fetch_ign, "find_tile_url", fail)
|
||||||
|
monkeypatch.setattr(fetch_ign, "download_file", fail)
|
||||||
|
|
||||||
|
result = fetch_ign.fetch_tiles(input_dir, [(1055, 6882)])
|
||||||
|
|
||||||
|
assert not neighbor_copy.exists()
|
||||||
|
assert (input_dir / name).read_bytes() == b"laz-voisine"
|
||||||
|
assert result == [] # promotion = "skip", comme le cas déjà-présent
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_tiles_does_not_promote_when_called_on_edge_dir(tmp_path, monkeypatch):
|
||||||
|
"""Appel avec input_dir=edge_dir (cas _fetch_edge_neighbors) : jamais de promotion."""
|
||||||
|
from lidar_pipeline import fetch_ign
|
||||||
|
|
||||||
|
edge_dir = tmp_path / "input" / "edge_neighbors"
|
||||||
|
edge_dir.mkdir(parents=True)
|
||||||
|
inner_edge = edge_dir / "edge_neighbors"
|
||||||
|
name = fetch_ign.tile_filename(1055, 6882)
|
||||||
|
# Même si un dossier edge_neighbors/edge_neighbors/<name> existait par
|
||||||
|
# accident, il ne doit jamais être déplacé vers edge_dir/<name>.
|
||||||
|
inner_edge.mkdir()
|
||||||
|
(inner_edge / name).write_bytes(b"ne-doit-pas-bouger")
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_find_tile_url(col, row, timeout=20):
|
||||||
|
calls.append((col, row))
|
||||||
|
return "https://example.org/dalle.copc.laz"
|
||||||
|
|
||||||
|
def fake_download(url, dest, timeout=120, chunk=1024 * 1024):
|
||||||
|
from pathlib import Path
|
||||||
|
Path(dest).write_bytes(b"laz")
|
||||||
|
return 3
|
||||||
|
|
||||||
|
monkeypatch.setattr(fetch_ign, "find_tile_url", fake_find_tile_url)
|
||||||
|
monkeypatch.setattr(fetch_ign, "download_file", fake_download)
|
||||||
|
|
||||||
|
result = fetch_ign.fetch_tiles(edge_dir, [(1055, 6882)])
|
||||||
|
|
||||||
|
assert calls == [(1055, 6882)] # téléchargée normalement, pas promue
|
||||||
|
assert (inner_edge / name).read_bytes() == b"ne-doit-pas-bouger"
|
||||||
|
assert (edge_dir / name).read_bytes() == b"laz"
|
||||||
|
assert len(result) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_tiles_force_downloads_complete(tmp_path, monkeypatch):
|
def test_fetch_tiles_force_downloads_complete(tmp_path, monkeypatch):
|
||||||
"""force=True télécharge même une tuile complète (régénération avec --force)."""
|
"""force=True télécharge même une tuile complète (régénération avec --force)."""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@ -107,6 +107,46 @@ class TestFetchEdgeNeighbors:
|
|||||||
pipeline._fetch_edge_neighbors([lazh])
|
pipeline._fetch_edge_neighbors([lazh])
|
||||||
|
|
||||||
|
|
||||||
|
class TestCleanupEdgeNeighborDuplicates:
|
||||||
|
"""Nettoyage des doublons entre input/ et input/edge_neighbors/."""
|
||||||
|
|
||||||
|
def test_removes_true_duplicates_keeps_unique_and_part(self, tmp_path):
|
||||||
|
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
||||||
|
|
||||||
|
input_dir = tmp_path / "input"
|
||||||
|
input_dir.mkdir()
|
||||||
|
edge_dir = input_dir / "edge_neighbors"
|
||||||
|
edge_dir.mkdir()
|
||||||
|
|
||||||
|
dup_name = "LHD_FXX_1000_6779_PTS_LAMB93_IGN69.copc.laz"
|
||||||
|
unique_name = "LHD_FXX_1001_6779_PTS_LAMB93_IGN69.copc.laz"
|
||||||
|
part_name = "LHD_FXX_1002_6779_PTS_LAMB93_IGN69.copc.laz.part"
|
||||||
|
|
||||||
|
(input_dir / dup_name).write_bytes(b"authoritative")
|
||||||
|
(edge_dir / dup_name).write_bytes(b"duplicata")
|
||||||
|
(edge_dir / unique_name).write_bytes(b"voisine-unique")
|
||||||
|
(edge_dir / part_name).write_bytes(b"en-cours")
|
||||||
|
|
||||||
|
pipeline = LidarArchaeoPipeline(input_dir, tmp_path / "output",
|
||||||
|
edge_buffer=100.0)
|
||||||
|
pipeline._cleanup_edge_neighbor_duplicates(edge_dir)
|
||||||
|
|
||||||
|
assert not (edge_dir / dup_name).exists()
|
||||||
|
assert (input_dir / dup_name).read_bytes() == b"authoritative"
|
||||||
|
assert (edge_dir / unique_name).exists()
|
||||||
|
assert (edge_dir / part_name).exists()
|
||||||
|
|
||||||
|
def test_noop_when_edge_dir_missing(self, tmp_path):
|
||||||
|
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
||||||
|
|
||||||
|
input_dir = tmp_path / "input"
|
||||||
|
input_dir.mkdir()
|
||||||
|
pipeline = LidarArchaeoPipeline(input_dir, tmp_path / "output",
|
||||||
|
edge_buffer=100.0)
|
||||||
|
# Ne doit pas lever si edge_neighbors/ n'existe pas encore.
|
||||||
|
pipeline._cleanup_edge_neighbor_duplicates(input_dir / "edge_neighbors")
|
||||||
|
|
||||||
|
|
||||||
class TestLidarArchaeoPipeline:
|
class TestLidarArchaeoPipeline:
|
||||||
def test_init_creates_dirs(self, tmp_path):
|
def test_init_creates_dirs(self, tmp_path):
|
||||||
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
||||||
|
|||||||
Reference in New Issue
Block a user