"""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'
\n' f' \n' f' \n' f'
' ) layer_controls_html = '\n'.join(layer_controls) # Serialize data for JS bounds_json = json.dumps(bounds_wgs84) layers_json = json.dumps(layers, ensure_ascii=False) colormaps_json = json.dumps(LAYER_COLORMAPS) center_lng = (bounds_wgs84['west'] + bounds_wgs84['east']) / 2 center_lat = (bounds_wgs84['south'] + bounds_wgs84['north']) / 2 # Build the full HTML using string concatenation to avoid f-string issues with {z}/{x}/{y} html_parts = [] html_parts.append(f''' LiDAR Archéologique — {basename}

LiDAR Archéologique

{basename}
Res: {resolution}m/px | EPSG:2154

Fond de carte

Visualisations

{layer_controls_html}
''') html = ''.join(html_parts) # Write viewer HTML viewer_dir = output_vis_dir / 'viewer' viewer_dir.mkdir(exist_ok=True) viewer_file = viewer_dir / f"{basename}.html" with open(viewer_file, 'w', encoding='utf-8') as f: f.write(html) logger.info(f" Viewer: {viewer_file}") return viewer_file