From e6824ac5b27d65047ce5cc938f4ce65638eebe95 Mon Sep 17 00:00:00 2001 From: Thierry Date: Wed, 1 Apr 2026 21:34:58 +0200 Subject: [PATCH] Upload files to "frontend/routes" --- frontend/routes/__init__.py | 4 ++ frontend/routes/admin.py | 131 ++++++++++++++++++++++++++++++++++++ frontend/routes/crop.py | 51 ++++++++++++++ frontend/routes/upload.py | 59 ++++++++++++++++ frontend/routes/viewer.py | 61 +++++++++++++++++ 5 files changed, 306 insertions(+) create mode 100644 frontend/routes/__init__.py create mode 100644 frontend/routes/admin.py create mode 100644 frontend/routes/crop.py create mode 100644 frontend/routes/upload.py create mode 100644 frontend/routes/viewer.py diff --git a/frontend/routes/__init__.py b/frontend/routes/__init__.py new file mode 100644 index 0000000..2f141a8 --- /dev/null +++ b/frontend/routes/__init__.py @@ -0,0 +1,4 @@ +from . import upload +from . import viewer +from . import admin +from . import crop \ No newline at end of file diff --git a/frontend/routes/admin.py b/frontend/routes/admin.py new file mode 100644 index 0000000..c54f04c --- /dev/null +++ b/frontend/routes/admin.py @@ -0,0 +1,131 @@ +from fastapi import APIRouter, Request, HTTPException +from fastapi.responses import HTMLResponse +import api_client +import config +import json +from pathlib import Path + +router = APIRouter() + + +@router.get("/backend-config", response_class=HTMLResponse) +async def backend_config(request: Request): + """Affiche le formulaire de configuration du backend""" + current_backend_url = config.BACKEND_URL + current_potree_url = config.POTREE_URL + return request.app.state.templates.TemplateResponse( + "partials/backend_config.html", + {"request": request, "current_backend_url": current_backend_url, "current_potree_url": current_potree_url}, + ) + + +@router.post("/backend-config", response_class=HTMLResponse) +async def save_backend_config(request: Request): + """Sauvegarde les nouvelles URLs du backend et Potree""" + form_data = await request.form() + new_backend_url = form_data.get("backend_url", "").strip().rstrip("/") + new_potree_url = form_data.get("potree_url", "").strip().rstrip("/") + + if not new_backend_url: + return request.app.state.templates.TemplateResponse( + "partials/backend_config.html", + {"request": request, "current_backend_url": config.BACKEND_URL, "current_potree_url": config.POTREE_URL, "error": "URL du backend vide"}, + ) + + # Sauvegarder dans le fichier JSON + config_file = Path(__file__).parent.parent / "config" / "backend.json" + config_file.parent.mkdir(parents=True, exist_ok=True) + + config_data = {} + if new_backend_url: + config_data["backend_url"] = new_backend_url + if new_potree_url: + config_data["potree_url"] = new_potree_url + + with open(config_file, "w") as f: + json.dump(config_data, f, indent=2) + + # Mettre à jour les variables module + config.BACKEND_URL = new_backend_url + config.POTREE_URL = new_potree_url + + return request.app.state.templates.TemplateResponse( + "partials/backend_config.html", + {"request": request, "current_backend_url": new_backend_url, "current_potree_url": new_potree_url, "success": True}, + ) + + +@router.get("/list", response_class=HTMLResponse) +async def admin_list(request: Request): + """Affiche la liste de tous les nuages de points""" + pointclouds = await api_client.list_pointclouds() + return request.app.state.templates.TemplateResponse( + "partials/cloud_list.html", + {"request": request, "pointclouds": pointclouds}, + ) + + +@router.get("/debug/{pc_id}", response_class=HTMLResponse) +async def admin_debug(request: Request, pc_id: str): + """Affiche les informations de debug pour un nuage""" + debug_info = await api_client.get_debug(pc_id) + return request.app.state.templates.TemplateResponse( + "partials/debug_panel.html", + {"request": request, "pc_id": pc_id, "data": debug_info}, + ) + + +@router.delete("/delete/{pc_id}", response_class=HTMLResponse) +async def admin_delete(request: Request, pc_id: str): + """Supprime un nuage de points""" + try: + result = await api_client.delete_pointcloud(pc_id) + return request.app.state.templates.TemplateResponse( + "partials/upload_result.html", + {"request": request, "result": { + "id": pc_id, + "filename": f"{pc_id}.las", + "size_mb": 0, + "conversion_time_seconds": 0, + }, "error": None}, + ) + except Exception as e: + return request.app.state.templates.TemplateResponse( + "partials/upload_result.html", + {"request": request, "error": str(e), "result": None}, + ) + + +@router.post("/crop/{pc_id}", response_class=HTMLResponse) +async def admin_crop(request: Request, pc_id: str, box: dict): + """ + Crop le nuage de points avec PDAL. + + Args: + pc_id: ID du nuage de points à cropper + box: dict avec les coordonnées de la box 3D + {"minX", "minY", "minZ", "maxX", "maxY", "maxZ"} + """ + try: + result = await api_client.crop_pointcloud(pc_id, box) + + if result.get("ok"): + return request.app.state.templates.TemplateResponse( + "partials/upload_result.html", + {"request": request, "result": { + "id": result.get("id"), + "filename": f"{pc_id}_cropped.las", + "size_mb": result.get("size_mb", 0), + "conversion_time_seconds": result.get("conversion_time_seconds", 0), + }, "error": None}, + ) + else: + return request.app.state.templates.TemplateResponse( + "partials/upload_result.html", + {"request": request, "error": result.get("detail", "Erreur inconnue"), "result": None}, + ) + except Exception as e: + return request.app.state.templates.TemplateResponse( + "partials/upload_result.html", + {"request": request, "error": str(e), "result": None}, + ) \ No newline at end of file diff --git a/frontend/routes/crop.py b/frontend/routes/crop.py new file mode 100644 index 0000000..d52651c --- /dev/null +++ b/frontend/routes/crop.py @@ -0,0 +1,51 @@ +from fastapi import APIRouter, Request, HTTPException +from fastapi.responses import HTMLResponse +import api_client +import config + +router = APIRouter() + + +@router.get("/crop", response_class=HTMLResponse) +async def crop_ui(request: Request, pc_id: str): + """Interface utilisateur pour le crop du nuage de points""" + embed_url = f"{config.BACKEND_URL}/viewer-embed/{pc_id}" + return request.app.state.templates.TemplateResponse( + "partials/crop.html", + {"request": request, "pc_id": pc_id, "embed_url": embed_url}, + ) + + +@router.post("/crop", response_class=HTMLResponse) +async def crop(request: Request, pc_id: str, box: dict): + """ + Traite la requête de crop : + 1. Envoie la box 3D au backend + 2. Le backend utilise PDAL pour cropper le nuage + 3. Retourne le nouveau pc_id pour l'affichage + """ + try: + # Envoi de la requête de crop au backend + result = await api_client.crop_pointcloud(pc_id, box) + + if result.get("ok"): + new_pc_id = result.get("id") + return request.app.state.templates.TemplateResponse( + "partials/upload_result.html", + {"request": request, "result": { + "id": new_pc_id, + "filename": f"{pc_id}_cropped.las", + "size_mb": result.get("size_mb", 0), + "conversion_time_seconds": result.get("conversion_time_seconds", 0), + }, "error": None}, + ) + else: + return request.app.state.templates.TemplateResponse( + "partials/upload_result.html", + {"request": request, "error": result.get("detail", "Erreur inconnue"), "result": None}, + ) + except Exception as e: + return request.app.state.templates.TemplateResponse( + "partials/upload_result.html", + {"request": request, "error": str(e), "result": None}, + ) \ No newline at end of file diff --git a/frontend/routes/upload.py b/frontend/routes/upload.py new file mode 100644 index 0000000..c0fbfbb --- /dev/null +++ b/frontend/routes/upload.py @@ -0,0 +1,59 @@ +# routes/upload.py + +from fastapi import APIRouter, Request, UploadFile, File, HTTPException +from fastapi.responses import HTMLResponse, RedirectResponse +from pathlib import Path +import api_client +import config + +router = APIRouter() + + +@router.get("/", response_class=RedirectResponse) +async def root(): + """Redirige / vers /upload""" + return RedirectResponse(url="/upload") + + +@router.get("/upload", response_class=HTMLResponse) +async def index(request: Request): + return request.app.state.templates.TemplateResponse( + "index.html", + {"request": request, "active_tab": "upload"}, + ) + + +@router.get("/health-check", response_class=HTMLResponse) +async def health_check(request: Request): + try: + data = await api_client.check_health() + return request.app.state.templates.TemplateResponse( + "partials/health_status.html", + {"request": request, "ok": True, "entwine_available": data.get("entwine_available", False), "disk_free_gb": data.get("disk_free_gb", "?")}, + ) + except Exception as e: + return request.app.state.templates.TemplateResponse( + "partials/health_status.html", + {"request": request, "ok": False, "error": str(e)}, + ) + + +@router.post("/upload", response_class=HTMLResponse) +async def upload(request: Request, file: UploadFile = File(...)): + suffix = Path(file.filename).suffix.lower() + if suffix not in config.SUPPORTED_EXTENSIONS: + return request.app.state.templates.TemplateResponse( + "partials/upload_result.html", + {"request": request, "error": f"Format non supporté : {suffix}. Formats acceptés : {', '.join(config.SUPPORTED_EXTENSIONS)}", "result": None}, + ) + try: + data = await api_client.upload_file(file.filename, await file.read()) + return request.app.state.templates.TemplateResponse( + "partials/upload_result.html", + {"request": request, "result": data, "error": None}, + ) + except Exception as e: + return request.app.state.templates.TemplateResponse( + "partials/upload_result.html", + {"request": request, "error": str(e), "result": None}, + ) \ No newline at end of file diff --git a/frontend/routes/viewer.py b/frontend/routes/viewer.py new file mode 100644 index 0000000..dba7780 --- /dev/null +++ b/frontend/routes/viewer.py @@ -0,0 +1,61 @@ +from fastapi import APIRouter, Request +from fastapi.responses import HTMLResponse +import config +import api_client +from pathlib import Path +import shutil +import os +from datetime import datetime + +router = APIRouter() + + +@router.get("/viewer/list", response_class=HTMLResponse) +async def viewer_list(request: Request): + """Liste les nuages de points disponibles - Endpoint autonome frontend""" + pointclouds = [] + + # Récupérer la liste des nuages directement depuis le système de fichiers + ept_dir = Path(config.EPT_DIR) + if ept_dir.exists(): + for item in sorted(ept_dir.iterdir(), key=lambda x: x.stat().st_ctime, reverse=True): + if item.is_dir(): + # Lire le manifeste + manifest_path = item / "manifest.json" + manifest = {} + if manifest_path.exists(): + try: + manifest = {"ept_dir": item.name} + except: + pass + + # Calculer la taille et le nombre de fichiers + total_size = 0 + file_count = 0 + for f in item.rglob("*"): + if f.is_file(): + total_size += f.stat().st_size + file_count += 1 + + if file_count > 0: + pointclouds.append({ + "id": item.name, + "size_mb": round(total_size / (1024 * 1024), 2), + "file_count": file_count, + "manifest": manifest, + "created": item.stat().st_ctime, + }) + + return request.app.state.templates.TemplateResponse( + "partials/cloud_list.html", + {"request": request, "pointclouds": pointclouds}, + ) + + +@router.get("/viewer/{pc_id}", response_class=HTMLResponse) +async def viewer(request: Request, pc_id: str): + embed_url = f"{config.POTREE_URL}/viewer-embed/{pc_id}" + return request.app.state.templates.TemplateResponse( + "partials/viewer.html", + {"request": request, "pc_id": pc_id, "embed_url": embed_url}, + )