Upload files to "frontend/routes"
This commit is contained in:
parent
d1686cdc8e
commit
e6824ac5b2
5 changed files with 306 additions and 0 deletions
4
frontend/routes/__init__.py
Normal file
4
frontend/routes/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
from . import upload
|
||||||
|
from . import viewer
|
||||||
|
from . import admin
|
||||||
|
from . import crop
|
||||||
131
frontend/routes/admin.py
Normal file
131
frontend/routes/admin.py
Normal file
|
|
@ -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},
|
||||||
|
)
|
||||||
51
frontend/routes/crop.py
Normal file
51
frontend/routes/crop.py
Normal file
|
|
@ -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},
|
||||||
|
)
|
||||||
59
frontend/routes/upload.py
Normal file
59
frontend/routes/upload.py
Normal file
|
|
@ -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},
|
||||||
|
)
|
||||||
61
frontend/routes/viewer.py
Normal file
61
frontend/routes/viewer.py
Normal file
|
|
@ -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},
|
||||||
|
)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue