Split webapp for Raspberry Pi deployment, remote generation API and sync
La webapp (carte + vignettes) et la génération de tuiles se déploient sur deux machines : image légère Dockerfile.webapp (FastAPI + Pillow AVIF natif + pyproj) sur Raspberry Pi, pipeline complet sur la machine de traitement. LIDAR_GENERATION_URL délègue /api/generate, /api/preview et /api/status ; /api/sync ramène les tuiles par rsync puis régénère vignettes et index localement. Token partagé optionnel (LIDAR_API_TOKEN/LIDAR_REMOTE_TOKEN). Retire du dépôt les journaux internes (.swival, audit-findings) et les données (data/, notebooks/). Doc : docs/DEPLOY_WEBAPP.md.
This commit is contained in:
@ -214,7 +214,9 @@ def test_build_index_generates_html(tmp_path):
|
||||
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', 'svf'))
|
||||
_make_fake_viz_dir(vis_dir, "a", 1000, 6881,
|
||||
('aspect', 'slope', 'positive_openness',
|
||||
'wavelet', 'hillshade_multi', 'svf'))
|
||||
_make_fake_viz_dir(vis_dir, "b", 1001, 6881, ('hillshade_multi',))
|
||||
|
||||
result = build_index(output_dir)
|
||||
@ -235,19 +237,46 @@ def test_build_index_generates_html(tmp_path):
|
||||
assert (assets / "app.css").read_text(encoding='utf-8').startswith('/*')
|
||||
app_js = (assets / "app.js").read_text(encoding='utf-8')
|
||||
assert "Couches" in app_js or "layers" in app_js
|
||||
# Barre de génération : sélecteur multi-visualisations (wavelet, aniso_open,
|
||||
# openness positive, pente...) et envoi du champ viz à /api/generate
|
||||
# Barre de génération : sélecteur limité aux couches proposées dans le
|
||||
# panneau de visualisation (aspect + slope + openness + ondelette,
|
||||
# valeurs = noms d'étapes --only). La sélection est envoyée à /api/preview
|
||||
# (détection des tuiles incomplètes) comme à /api/generate.
|
||||
# Les couches hors panneau (svf, hillshade) n'y figurent pas.
|
||||
assert 'id="genViz"' in content
|
||||
assert 'value="wavelet"' in content
|
||||
assert 'value="aniso_open"' in content
|
||||
assert 'value="aniso_open"' not in content
|
||||
assert 'value="pos_open"' in content
|
||||
assert 'value="slope"' in content
|
||||
assert "genViz" in app_js and "viz: vizSel.length" in app_js
|
||||
assert 'value="svf"' not in content
|
||||
assert 'value="hillshade"' not in content
|
||||
assert "genViz" in app_js and "viz: selectedGenViz()" in app_js
|
||||
assert app_js.count("selectedGenViz()") >= 3 # définition + preview + generate
|
||||
# Adoption du dernier run au chargement (redémarrage du serveur) :
|
||||
# un run terminé réapparaît dans la file avec ses tuiles, sans doublon.
|
||||
assert "Dernière génération" in app_js
|
||||
assert "t.started === s.started" in app_js
|
||||
# Bouton « Compléter » : visualisations manquantes des dalles présentes
|
||||
# dans input/ (scope all_missing envoyé au preview et au generate).
|
||||
assert 'id="btnComplete"' in content
|
||||
assert "all_missing" in app_js and app_js.count("all_missing") >= 2
|
||||
assert 'assets/app.css' in content
|
||||
# Bouton d'actualisation des couches (détection des nouveautés sur disque)
|
||||
# Bouton d'actualisation des couches (détection des nouveautés sur disque) :
|
||||
# le ↻ resynchronise puis reconstruit la carte (mode webapp distante).
|
||||
assert 'id="layerRefresh"' in content
|
||||
assert "/api/layers" in app_js and "/api/rebuild" in app_js
|
||||
assert "/api/layers" in app_js and "/api/sync" in app_js
|
||||
assert 'assets/app.js' in content
|
||||
# Fin d'un run de génération : synchronisation puis rechargement (le
|
||||
# rechargement direct reste en secours si /api/sync est absent).
|
||||
assert "syncThenReload" in app_js
|
||||
# Tuiles en direct : la carte sonde /api/tiles pendant un run et fusionne
|
||||
# les tuiles terminées sans recharger (mode incrémental du pipeline).
|
||||
assert "/api/tiles" in app_js and "pollLiveTiles" in app_js
|
||||
assert "registerTile" in app_js and "mergeTilesData" in app_js
|
||||
# Export des données de carte (servi par /api/tiles)
|
||||
data = json.loads((output_dir / "index_tiles.json").read_text(encoding='utf-8'))
|
||||
assert data['tiles'] and data['tiles'][0]['corners']
|
||||
assert 'aspect' in data['viz_meta']
|
||||
assert data['stats']['n_tiles'] == len(data['tiles'])
|
||||
# Vérifie les vignettes générées
|
||||
thumb_dir = output_dir / "index_thumbs"
|
||||
assert thumb_dir.is_dir()
|
||||
@ -259,16 +288,53 @@ def test_subtiled_viz_covers_generation_choices():
|
||||
"""Les viz générables depuis la carte sont sous-tuilées (affichage fluide)."""
|
||||
from lidar_pipeline.index import _CARTO_SUBTILED_VIZ
|
||||
for key in ('aspect', 'hillshade_multi', 'slope', 'positive_openness',
|
||||
'aniso_open', 'wavelet'):
|
||||
'wavelet'):
|
||||
assert key in _CARTO_SUBTILED_VIZ
|
||||
|
||||
|
||||
def test_panel_restricted_to_requested_layers():
|
||||
"""Le panneau et le sélecteur ne proposent que la base aspect + les 4 couches demandées."""
|
||||
from lidar_pipeline.index import PANEL_VIZ, GEN_VIZ_CHOICES
|
||||
assert PANEL_VIZ == ('aspect', 'slope', 'positive_openness', 'aniso_open', 'wavelet')
|
||||
assert {name for name, _ in GEN_VIZ_CHOICES} == {
|
||||
'aspect', 'slope', 'pos_open', 'aniso_open', 'wavelet'}
|
||||
"""Le panneau et le sélecteur ne proposent que la base aspect + les 3 couches demandées."""
|
||||
from lidar_pipeline.index import PANEL_VIZ, KEYWORD_TO_STEP
|
||||
from lidar_pipeline.pipeline import VIZ_STEPS
|
||||
assert PANEL_VIZ == ('aspect', 'slope', 'positive_openness', 'wavelet')
|
||||
# Chaque couche du panneau correspond à une étape --only valide du pipeline
|
||||
steps = {name for name, _ in VIZ_STEPS}
|
||||
for key in PANEL_VIZ:
|
||||
assert KEYWORD_TO_STEP.get(key, key) in steps
|
||||
|
||||
|
||||
def test_build_index_merges_cross_resolution_viz(tmp_path):
|
||||
"""Une couche produite seulement à 0,5 m reste visible sur la dalle 0,2 m.
|
||||
|
||||
Le dernier run peut être interrompu entre les deux passes de résolution :
|
||||
la fusion inter-résolutions doit pointer vignettes et URLs vers le dossier
|
||||
d'origine de chaque fichier (dir_name), pas vers dir_path de la dalle
|
||||
affichée — sinon les vignettes échouent et la couche disparaît.
|
||||
"""
|
||||
from lidar_pipeline.index import build_index
|
||||
|
||||
output_dir = tmp_path / "output"
|
||||
vis_dir = output_dir / "visualisations"
|
||||
vis_dir.mkdir(parents=True)
|
||||
base = "LHD_FXX_1000_6881_PTS_LAMB93_IGN69"
|
||||
# Passe 0,5 m : aspect + slope ; passe 0,2 m : aspect seulement
|
||||
_make_fake_viz_dir(vis_dir, base, 1000, 6881, ('aspect', 'slope'))
|
||||
_make_fake_viz_dir(vis_dir, base, 1000, 6881, ('aspect',), res_suffix='_r0p2')
|
||||
|
||||
assert build_index(output_dir) is not None
|
||||
data = json.loads((output_dir / "index_tiles.json").read_text(encoding='utf-8'))
|
||||
# Une seule position affichée (résolution la plus fine), deux couches
|
||||
tiles = [t for t in data['tiles'] if (t['col'], t['row']) == (1000, 6881)]
|
||||
keys = set()
|
||||
for t in tiles:
|
||||
keys.update(t['viz'].keys())
|
||||
assert {'aspect', 'slope'} <= keys
|
||||
# Vignette slope générée + chaque thumb/full existe réellement sur disque
|
||||
assert (output_dir / "index_thumbs" / f"{base}_r0p2_slope.jpg").exists()
|
||||
for t in tiles:
|
||||
for v in t['viz'].values():
|
||||
assert (output_dir / v['thumb']).exists(), v['thumb']
|
||||
assert (output_dir / v['full']).exists(), v['full']
|
||||
|
||||
|
||||
def test_build_index_regenerates_stale_thumbnails(tmp_path):
|
||||
@ -449,3 +515,12 @@ def test_subtile_corners_grid():
|
||||
se_quad = _subtile_corners(corners, 1, 0, k)
|
||||
assert sw_quad[1] == se_quad[0]
|
||||
assert sw_quad[2] == se_quad[3]
|
||||
|
||||
|
||||
def test_approx_wgs84_to_l93_roundtrip():
|
||||
"""L'approximation affine WGS84→L93 est l'inverse exacte de L93→WGS84."""
|
||||
from lidar_pipeline.index import _approx_l93_to_wgs84, _approx_wgs84_to_l93
|
||||
for x, y in ((1054000.0, 6882000.0), (700000.0, 6600000.0), (950123.0, 6410456.0)):
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user