Vue dézoomée en mosaïque d'overview et cache immutable des images versionnées
This commit is contained in:
@ -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
|
||||
|
||||
Reference in New Issue
Block a user