From ed3e90ea89a5cbd4f71c0b662cb969bf3e2cfa86 Mon Sep 17 00:00:00 2001 From: Antoine Jacquin Date: Mon, 31 Aug 2026 19:02:14 +0200 Subject: [PATCH] Add visualization picker to zone generation and unify tile colors - Web map: multi-select picker in the generation bar (aspect, slope, positive openness, anisotropic openness, wavelet) passed to the API; the layer panel is restricted to the same shortlist (PANEL_VIZ) and a refresh button rebuilds the index when new layers appear on disk; jobs started outside the UI are now adopted into the visible queue - Uniform colors across tiles: openness/anisotropic/sailore now store local z-scores, and all renderers use fixed ranges (0-3 sigma, SVF 0-1 physical, slope 0-30 deg) instead of per-tile percentile stretches - Ray-tracing falls back to CPU when VRAM is exhausted so openness and SVF no longer fail silently on shared GPUs - build_index merges visualizations available at only one resolution into the displayed tile so in-progress layers stay visible - 11 new tests (142 passing) --- lidar_pipeline/index.py | 122 +++++++++++++++++++- lidar_pipeline/rendering.py | 48 ++++---- lidar_pipeline/tests/test_index.py | 27 +++++ lidar_pipeline/tests/test_visualizations.py | 40 +++++++ lidar_pipeline/tests/test_webapp.py | 94 +++++++++++++++ lidar_pipeline/visualizations.py | 38 +++++- lidar_pipeline/webapp.py | 80 ++++++++++++- 7 files changed, 412 insertions(+), 37 deletions(-) diff --git a/lidar_pipeline/index.py b/lidar_pipeline/index.py index e29f5c8..30441a1 100644 --- a/lidar_pipeline/index.py +++ b/lidar_pipeline/index.py @@ -54,10 +54,26 @@ VIZ_LABELS = { # Couche activée par défaut à l'ouverture de la carte. DEFAULT_VIZ = 'hillshade_multi' +# Couches proposées dans le panneau de couches et le sélecteur de génération +# (liste restreinte : base aspect + visualisations demandées). Les autres +# visualisations restent calculées et stockées mais ne sont pas proposées. +PANEL_VIZ = ('aspect', 'slope', 'positive_openness', 'aniso_open', 'wavelet') + +# Visualisations proposées dans la barre de génération de zones (noms +# d'étapes --only du pipeline) ; 'aspect' reste le choix par défaut (léger). +GEN_VIZ_CHOICES = [ + ('aspect', 'Aspect'), + ('slope', 'Pente'), + ('pos_open', 'Openness positive'), + ('aniso_open', 'Openness anisotropique'), + ('wavelet', 'Ondelette'), +] + # Visualisations découpées en sous-tuiles (quadrants 500 m) pour alléger la # carte — les autres viz restent disponibles en repli dalle entière. # Liste vide = découper toutes les visualisations disponibles. -_CARTO_SUBTILED_VIZ = ('aspect', 'hillshade_multi') +_CARTO_SUBTILED_VIZ = ('aspect', 'hillshade_multi', 'slope', 'positive_openness', + 'aniso_open', 'wavelet') # Encodage AVIF des sous-tuiles (benchmark sur dalles d'aspect réelles : # q55 ≈ −45 % vs WebP q82, visuellement propre sur rampes de couleur). @@ -617,10 +633,22 @@ def build_index(output_dir, output_format='avif'): # superposent exactement sur la carte et celle ajoutée en dernier dans le # DOM (la moins résolue, tri croissant) masque l'autre. best_by_pos = {} + tiles_by_pos = {} for t in tiles: key = (t['col'], t['row']) + tiles_by_pos.setdefault(key, []).append(t) if key not in best_by_pos or t['resolution'] < best_by_pos[key]['resolution']: best_by_pos[key] = t + # Complète chaque dalle affichée (résolution la plus fine) avec les + # visualisations produites uniquement à l'autre résolution : sans cela, + # une couche en cours de génération (passe 0,5 m faite, 0,2 m pas encore) + # reste invisible sur la carte et absente du panneau de couches. + for key, best in best_by_pos.items(): + for other in tiles_by_pos[key]: + if other is best: + continue + for viz_key, viz_info in other['viz'].items(): + best['viz'].setdefault(viz_key, viz_info) tiles = sorted(best_by_pos.values(), key=lambda t: (t['resolution'], -t['row'], t['col'])) @@ -755,21 +783,34 @@ def _write_assets(output_dir): def _render_html(tiles, global_bbox, all_viz_keys, output_format): """Construit la coquille HTML (données intégrées, styles/JS dans assets/).""" + # Restriction du panneau de couches à la liste choisie (PANEL_VIZ) : + # les autres visualisations existent sur disque mais ne sont pas proposées. + panel_keys = set(all_viz_keys) + if PANEL_VIZ is not None: + panel_keys &= set(PANEL_VIZ) # Ordre des couches dans le panneau (selon ordre préféré puis alpha). - ordered_viz = [v for v in _VIZ_FALLBACK_ORDER if v in all_viz_keys] - for v in sorted(all_viz_keys): + ordered_viz = [v for v in _VIZ_FALLBACK_ORDER if v in panel_keys] + for v in sorted(panel_keys): if v not in ordered_viz: ordered_viz.append(v) tiles_json = json.dumps(tiles, ensure_ascii=False) + # Options du sélecteur de visualisations de la barre de génération. + gen_viz_options = "\n".join( + f' ' + for name, label in GEN_VIZ_CHOICES) + viz_meta_json = json.dumps({ k: {'label': VIZ_LABELS.get(k, k)} for k in ordered_viz }, ensure_ascii=False) - # Couche activée par défaut : DEFAULT_VIZ si proposée, sinon la 1re - default_viz = DEFAULT_VIZ if DEFAULT_VIZ in ordered_viz else (ordered_viz[0] if ordered_viz else DEFAULT_VIZ) + # Couche activée par défaut : préférence globale, sinon aspect (base + # complète), sinon la 1re proposée. + default_viz = next((v for v in (DEFAULT_VIZ, 'aspect') if v in ordered_viz), + ordered_viz[0] if ordered_viz else DEFAULT_VIZ) stats_json = json.dumps({'default_viz': default_viz, 'n_tiles': len(tiles)}, ensure_ascii=False) @@ -781,6 +822,7 @@ def _render_html(tiles, global_bbox, all_viz_keys, output_format): tiles_json=tiles_json, viz_meta_json=viz_meta_json, stats_json=stats_json, + gen_viz_options=gen_viz_options, n_tiles=n_tiles, grid_w=grid_w, grid_h=grid_h, @@ -816,6 +858,7 @@ _HTML_TEMPLATE = """