"""Web map viewer generator for LiDAR visualizations. Generates a self-contained HTML file with MapLibre GL JS that displays all visualization layers with opacity controls and IGN/OSM basemaps. Served by TiTiler for COG tile access. """ import json import logging from pathlib import Path logger = logging.getLogger("lidar") # Layer ordering for the viewer panel (archaeological priority) LAYER_ORDER = [ 'hillshade_multi', 'slope', 'aspect', 'curvature', 'svf', 'lrm', 'positive_openness', 'negative_openness', 'mslrm', 'tpi', 'sailore', 'roughness', 'anomalies', 'wavelet', 'flow', 'ortho', 'topo', ] # Colormaps for TiTiler rendering of single-band COGs LAYER_COLORMAPS = { 'hillshade_multi': 'gray', 'slope': 'inferno', 'aspect': 'turbo', 'curvature': 'RdYlBu_r', 'svf': 'viridis', 'lrm': 'RdBu_r', 'positive_openness': 'YlOrBr', 'negative_openness': 'GnBu_r', 'mslrm': 'RdBu_r', 'tpi': 'BrBG', 'sailore': 'seismic', 'roughness': 'magma', 'anomalies': 'coolwarm', 'wavelet': 'cividis', 'flow': 'Blues', } def generate_viewer(basename, vis_dir, output_vis_dir, titiler_url='http://localhost:8000'): """Generate a MapLibre GL JS viewer HTML file for the LiDAR tile. Args: basename: Base name for the LiDAR tile. vis_dir: Per-file visualization directory (vis_dir/basename/). output_vis_dir: Parent visualization directory for viewer output. titiler_url: Base URL of the TiTiler server. Returns: Path to the generated HTML file, or None on failure. """ # Read metadata.json metadata_file = vis_dir / "metadata.json" if not metadata_file.exists(): logger.error(f" Métadonnées manquantes: {metadata_file}") return None with open(metadata_file) as f: metadata = json.load(f) bounds_wgs84 = metadata['bounds_wgs84'] resolution = metadata.get('resolution', 0.5) # Determine which files are RGB (ortho/topo) layers = [] for layer in metadata['layers']: is_rgb = 'ortho' in layer['name'] or 'topo' in layer['name'] layers.append({ 'name': layer['name'], 'title': layer['title'], 'file': layer['file'], 'is_rgb': is_rgb, }) # Sort layers by archaeological priority def layer_sort_key(l): name = l['name'] for i, key in enumerate(LAYER_ORDER): if key in name: return i return len(LAYER_ORDER) layers.sort(key=layer_sort_key) # COG path prefix for TiTiler (absolute path inside Docker container) cog_path_prefix = f'/data/output/visualisations/{basename}/cog' # Build layer controls HTML layer_controls = [] for layer in layers: checked = 'checked' if layer['name'] == 'hillshade_multi' else '' initial_opacity = 100 if layer['name'] == 'hillshade_multi' else 0 layer_type = 'RGB' if layer['is_rgb'] else 'DEM' layer_controls.append( f'