Vue dézoomée en mosaïque d'overview et cache immutable des images versionnées

This commit is contained in:
Antoine Jacquin
2026-09-20 00:26:18 +02:00
parent 3236d56a4b
commit 342a0d2213
5 changed files with 563 additions and 50 deletions

View File

@ -512,11 +512,10 @@ def test_build_subtiles_regenerates_stale_crops(tmp_path):
def test_urls_versioned_for_cache_busting(tmp_path):
"""Les URLs images changent quand une tuile est régénérée.
Les images sont servies par des montages statiques sans Cache-Control :
sans version d'URL, le navigateur garde son ancien rendu en cache même
après recalcul. Chaque URL (vignette, intermédiaire, pleine) porte la
mtime de sa source en ?v= — un recalcul change l'URL et force le
rechargement, y compris en direct pendant un run (--incremental-index).
Les URLs versionnées (?v=) sont servies avec un cache immutable : un
recalcul DOIT changer l'URL pour forcer le rechargement, y compris en
direct pendant un run (--incremental-index). Chaque URL porte la mtime
de SON fichier (cf. test_urls_versioned_by_served_file_mtime).
"""
import os
import re
@ -700,3 +699,90 @@ def test_approx_wgs84_to_l93_roundtrip():
lon, lat = _approx_l93_to_wgs84(x, y)
x2, y2 = _approx_wgs84_to_l93(lon, lat)
assert abs(x2 - x) < 1e-6 and abs(y2 - y) < 1e-6
def test_overview_generated_for_large_grids(tmp_path):
"""Grille >= 6 dalles : une mosaïque d'overview par couche + le payload."""
from lidar_pipeline.index import build_index
output_dir = tmp_path / "output"
vis_dir = output_dir / "visualisations"
vis_dir.mkdir(parents=True)
for col in (1000, 1001, 1002):
for row in (6881, 6882):
_make_fake_viz_dir(vis_dir, "t", col, row, ('hillshade_multi', 'svf'))
assert build_index(output_dir) is not None
data = json.loads((output_dir / "index_tiles.json").read_text(encoding='utf-8'))
ov = data['overview']
assert 'hillshade_multi' in ov and 'svf' in ov
meta = ov['hillshade_multi']
assert (output_dir / meta['url'].split('?')[0]).is_file()
assert meta['km_w'] == 3 and meta['km_h'] == 2
assert meta['w'] == 3 * meta['px_per_km']
assert meta['h'] == 2 * meta['px_per_km']
assert len(meta['corners']) == 4
# La coquille HTML embarque la constante pour l'interface
html = (output_dir / "index.html").read_text(encoding='utf-8')
assert "const OVERVIEW" in html
assert "index_overview/hillshade_multi.webp" in html
def test_overview_skipped_for_small_grids(tmp_path):
"""Grille < 6 dalles : pas d'overview, les vignettes suffisent."""
from lidar_pipeline.index import build_index
output_dir = tmp_path / "output"
vis_dir = output_dir / "visualisations"
vis_dir.mkdir(parents=True)
_make_fake_viz_dir(vis_dir, "a", 1000, 6881, ('hillshade_multi',))
_make_fake_viz_dir(vis_dir, "b", 1001, 6881, ('hillshade_multi',))
assert build_index(output_dir) is not None
data = json.loads((output_dir / "index_tiles.json").read_text(encoding='utf-8'))
assert data['overview'] == {}
def test_urls_versioned_by_served_file_mtime(tmp_path):
"""?v= suit la mtime du fichier SERVI — prérequis du cache immutable.
Une vignette recalculée après coup (source inchangée) change de contenu :
son URL doit changer aussi, sans toucher celles des fichiers intacts.
"""
from lidar_pipeline.index import build_index
output_dir = tmp_path / "output"
vis_dir = output_dir / "visualisations"
vis_dir.mkdir(parents=True)
_make_fake_viz_dir(vis_dir, "a", 1000, 6881, ('hillshade_multi',))
# 0,2 m : la dalle passe par le sous-tuilage (vignettes/mid/AVIF propres)
_make_fake_viz_dir(vis_dir, "b", 1001, 6881, ('hillshade_multi',),
res_suffix='_r0p2')
assert build_index(output_dir) is not None
data = json.loads((output_dir / "index_tiles.json").read_text(encoding='utf-8'))
import time
for t in data['tiles']:
for viz in t['viz'].values():
for tier in ('thumb', 'mid', 'full'):
url = viz.get(tier)
if not url or '?v=' not in url:
continue
path, v = url.split('?v=')
assert v.isdigit()
# La version est la mtime ms du fichier servi lui-même
# (tolérance 1 s : granularité du système de fichiers).
assert abs(int(v) - int((output_dir / path).stat().st_mtime * 1000)) < 1000
# Vignette 0,5 m recalculée après coup : seule SON URL change.
tiles05 = [t for t in data['tiles'] if t.get('resolution') == 0.5]
url_before = tiles05[0]['viz']['hillshade_multi']['thumb']
thumb_path = output_dir / url_before.split('?')[0]
time.sleep(0.05)
from PIL import Image as PILImage
PILImage.new('RGB', (30, 30), (200, 10, 10)).save(str(thumb_path), format='JPEG')
assert build_index(output_dir) is not None
data2 = json.loads((output_dir / "index_tiles.json").read_text(encoding='utf-8'))
tiles05b = [t for t in data2['tiles'] if t.get('resolution') == 0.5]
url_after = tiles05b[0]['viz']['hillshade_multi']['thumb']
assert url_after.split('?')[0] == url_before.split('?')[0]
assert url_after != url_before