Interface web cartographique: COG + TiTiler + viewer MapLibre

- Ajout de convert_to_cog() et generate_cog_metadata() dans rendering.py
- Nouveau module viewer.py: génération HTML MapLibre GL JS avec couches et opacité
- Nouveau module server.py: serveur FastAPI avec TiTiler pour tuiles COG
- Pipeline: étapes 5 (COGs) et 6 (viewer web) après le rapport PDF
- CLI: flag --no-viewer pour désactiver la génération du viewer
- run.sh: commande 'serve' pour démarrer le serveur sur port 8000
- Dockerfile: ajout de rio-cogeo, titiler.core, fastapi, uvicorn, piexif
- setup.py: point d'entrée lidar-server

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jacquin Antoine
2026-05-10 17:15:37 +02:00
parent 2986400a0a
commit f01683819c
9 changed files with 779 additions and 21 deletions

View File

@ -527,6 +527,146 @@ def tif_to_png(tif_file, vis_dir, resolution, keep_tif=False):
return None
def convert_to_cog(tif_file, cog_dir):
"""Convert a GeoTIFF to Cloud Optimized GeoTIFF for web map serving.
Args:
tif_file: Path to input GeoTIFF.
cog_dir: Directory for output COG file.
Returns:
Path to output COG file, or None on failure.
"""
cog_file = cog_dir / tif_file.name
if cog_file.exists():
logger.debug(f" COG déjà existant: {cog_file.name}")
return cog_file
try:
from rio_cogeo.cogeo import cog_translate
from rio_cogeo.profiles import cog_profiles
with rasterio.open(tif_file) as src:
is_rgb = src.count >= 3
# Use deflate compression profile
profile = dict(cog_profiles["deflate"]) # Make a mutable copy
if not is_rgb:
# Single-band terrain data: keep float32 for continuous values
profile.update(dtype="float32")
cog_translate(str(tif_file), str(cog_file), profile, quiet=True)
logger.debug(f" COG créé: {cog_file.name}")
return cog_file
except ImportError:
logger.warning(" rio-cogeo non disponible — COG non généré")
return None
except Exception as e:
logger.warning(f" Erreur conversion COG: {e}")
return None
def generate_cog_metadata(vis_dir, basename):
"""Generate metadata JSON for all visualization layers.
Scans the COG directory for files and reads their geographic bounds.
Creates a metadata.json with WGS84 bounds and layer info for the web viewer.
Args:
vis_dir: Directory containing COG files (vis_dir/basename/cog/).
basename: Base name for the LiDAR tile.
Returns:
Path to metadata.json, or None on failure.
"""
import json
cog_dir = vis_dir / basename / "cog"
if not cog_dir.exists():
return None
# Find the DTM to get geographic bounds
# The COG files inherit bounds from the DTM, so we can read any COG
cog_files = sorted(cog_dir.glob("*.tif"))
if not cog_files:
return None
# Read bounds from first COG file
try:
ref_cog = cog_files[0]
with rasterio.open(ref_cog) as src:
bounds = src.bounds
crs = src.crs
if crs and HAS_WARP:
l93_xs = [bounds.left, bounds.right]
l93_ys = [bounds.top, bounds.bottom]
lons, lats = warp_transform(crs, 'EPSG:4326', l93_xs, l93_ys)
bounds_wgs84 = {
'west': float(min(lons)),
'south': float(min(lats)),
'east': float(max(lons)),
'north': float(max(lats)),
}
else:
# Fallback: use Lambert 93 bounds directly
bounds_wgs84 = {
'west': float(bounds.left),
'south': float(bounds.bottom),
'east': float(bounds.right),
'north': float(bounds.top),
}
bounds_l93 = {
'min_x': float(bounds.left),
'min_y': float(bounds.bottom),
'max_x': float(bounds.right),
'max_y': float(bounds.top),
}
resolution = float(abs(src.transform.a))
except Exception as e:
logger.warning(f" Erreur lecture bounds COG: {e}")
return None
# Build layer list with titles
layers = []
for cog_file in cog_files:
name = cog_file.stem.replace(basename + "_", "")
# Find matching title from COLORMAPS or RGB_LEGENDS
title = name.replace("_", " ").title()
for key in sorted(COLORMAPS.keys(), key=len, reverse=True):
if key in name:
title = COLORMAPS[key]['title']
break
for key in RGB_LEGENDS:
if key in name:
title = RGB_LEGENDS[key]['title']
break
layers.append({
'name': name,
'title': title,
'file': cog_file.name,
})
metadata = {
'basename': basename,
'bounds_l93': bounds_l93,
'bounds_wgs84': bounds_wgs84,
'resolution': resolution,
'layers': layers,
}
metadata_file = vis_dir / basename / "metadata.json"
with open(metadata_file, 'w') as f:
json.dump(metadata, f, indent=2, ensure_ascii=False)
logger.info(f" Métadonnées: {len(layers)} couches, bounds={bounds_wgs84}")
return metadata_file
def generate_pdf_report(basename, vis_dir, pdf_dir, resolution):
"""Generate A3 PDF report for a LiDAR file with all visualizations.