Centrer la carte sur le GPS du téléphone (HTTPS terminé par le proxy)
Le bouton ⌖ recentre la carte sur la position GPS du téléphone. L'API Geolocation exige un contexte sécurisé : la webapp sert désormais du HTTP et le TLS est porté par le reverse proxy (Traefik, labels dans l'override local) ; le webapp est transparent au proxy (URLs relatives, X-Forwarded-For pour la restriction LIDAR_REGEN_CIDR). Ajoute le modèle d'override docker-compose.webapp.override.yml.example, un rebuild d'index à la demande et met à jour doc/déploiement ; les tuiles sont servies à la demande.
This commit is contained in:
@ -7,9 +7,8 @@ le téléchargement IGN). Endpoints :
|
||||
GET /api/status → état de la génération en cours (ou dernière terminée)
|
||||
GET /api/tiles → données de carte (index_tiles.json) pour l'affichage
|
||||
en direct ; ?stamp=X → réponse allégée si inchangé
|
||||
POST /api/sync → synchroniser les tuiles (LIDAR_SYNC_CMD, ex. rsync
|
||||
depuis la machine de traitement) puis régénérer la
|
||||
carte en arrière-plan
|
||||
POST /api/sync → régénérer l'index local (vignettes + carte) en
|
||||
arrière-plan (les tuiles sont servies à la demande)
|
||||
POST /api/preview → cellules 1 km intersectant une bbox WGS84, OU (option
|
||||
all_missing=true) toutes les dalles LHD présentes dans
|
||||
input/ qui manquent au moins une visualisation demandée.
|
||||
@ -48,10 +47,11 @@ Architecture deux machines (cf. docs/DEPLOY_WEBAPP.md) : la webapp légère
|
||||
(sans PDAL/GPU, ex. Raspberry Pi) délègue la génération à la machine de
|
||||
traitement via LIDAR_GENERATION_URL — /api/generate, /api/preview et
|
||||
/api/status sont transmis tels quels au service distant, qui exécute la même
|
||||
webapp avec le pipeline complet. Les résultats reviennent par la commande de
|
||||
synchronisation LIDAR_SYNC_CMD (rsync), puis /api/sync régénère vignettes et
|
||||
carte localement. LIDAR_API_TOKEN (machine de traitement) + LIDAR_REMOTE_TOKEN
|
||||
(webapp légère) protègent optionnellement les appels distants.
|
||||
webapp avec le pipeline complet. Les images produites sont servies à la
|
||||
demande depuis GENERATION_URL (cache local peuplé au fil des consultations)
|
||||
puis /api/sync régénère vignettes et carte localement. LIDAR_API_TOKEN
|
||||
(machine de traitement) + LIDAR_REMOTE_TOKEN (webapp légère) protègent
|
||||
optionnellement les appels distants.
|
||||
"""
|
||||
|
||||
import json
|
||||
@ -90,9 +90,6 @@ GENERATION_URL = (os.environ.get("LIDAR_GENERATION_URL") or "").rstrip("/")
|
||||
# (LIDAR_REMOTE_TOKEN ici, LIDAR_API_TOKEN là-bas). Optionnel.
|
||||
REMOTE_TOKEN = os.environ.get("LIDAR_REMOTE_TOKEN") or None
|
||||
API_TOKEN = os.environ.get("LIDAR_API_TOKEN") or None
|
||||
# Commande shell qui ramène les tuiles traitées dans output/ local (rsync
|
||||
# depuis la machine de traitement). Exécutée par /api/sync avant le rebuild.
|
||||
SYNC_CMD = os.environ.get("LIDAR_SYNC_CMD") or None
|
||||
# Réseau autorisé à lancer la (re)génération des tuiles (/api/generate) :
|
||||
# liste de CIDR séparés par virgules, vide = restriction levée. Par défaut :
|
||||
# boucle locale + plages privées RFC1918 — couvre le LAN, la machine hôte
|
||||
@ -105,10 +102,6 @@ SYNC_CMD = os.environ.get("LIDAR_SYNC_CMD") or None
|
||||
REGEN_CIDR = (os.environ.get(
|
||||
"LIDAR_REGEN_CIDR",
|
||||
"127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16") or "").strip()
|
||||
# Cache local périodique : relance la synchronisation + le rebuild toutes
|
||||
# les N secondes (0 = désactivé). Sert au conteneur webapp seul, qui maintient
|
||||
# sa copie des tuiles de la machine de traitement.
|
||||
AUTO_SYNC_SECONDS = float(os.environ.get("LIDAR_AUTO_SYNC_SECONDS", "0") or 0)
|
||||
|
||||
# Résolution générée par /api/generate (cf. _build_command) : 0,2 m seule,
|
||||
# la 0,5 m n'est plus produite. La détection des tuiles à compléter exige
|
||||
@ -134,10 +127,6 @@ if GENERATION_URL:
|
||||
else:
|
||||
logger.info("Backend de génération des tuiles : local — consultation "
|
||||
"autonome du cache, sans LIDAR_GENERATION_URL")
|
||||
if SYNC_CMD:
|
||||
logger.info(f"Cache local des tuiles : LIDAR_SYNC_CMD"
|
||||
+ (f" (rafraîchi toutes les {AUTO_SYNC_SECONDS:g}s)"
|
||||
if AUTO_SYNC_SECONDS > 0 else " (à la demande)"))
|
||||
|
||||
# assets/ (CSS/JS de l'interface) est créé dès le démarrage pour que le monteur
|
||||
# statique soit actif même avant la première génération de l'index.
|
||||
@ -145,11 +134,11 @@ _assets_dir = OUTPUT_DIR / "assets"
|
||||
_assets_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# --- Cache à la demande : la visualisation peuple le cache local -------------
|
||||
# Sans LIDAR_SYNC_CMD (pas de rsync), les images viennent de la machine de
|
||||
# traitement par HTTP : une image absente du disque — ou plus ancienne que la
|
||||
# version ?v= référencée (mtime ms de la source sur le worker) — est téléchar-
|
||||
# gée depuis GENERATION_URL, écrite sur disque puis servie. Requêtes concur-
|
||||
# rentes dédupliquées (verrou par fichier), flux réseau plafonné (sémaphore).
|
||||
# Les images viennent de la machine de traitement par HTTP : une image absente
|
||||
# du disque — ou plus ancienne que la version ?v= référencée (mtime ms de la
|
||||
# source sur le worker) — est téléchargée depuis GENERATION_URL, écrite sur
|
||||
# disque puis servie. Requêtes concurrentes dédupliquées (verrou par fichier),
|
||||
# flux réseau plafonné (sémaphore).
|
||||
from fastapi.staticfiles import StaticFiles as _StaticFiles
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
@ -782,91 +771,46 @@ def tiles_data(stamp: Optional[float] = None):
|
||||
}
|
||||
|
||||
|
||||
# --- Synchronisation + rebuild de l'index en arrière-plan -------------------
|
||||
# --- Rebuild de l'index en arrière-plan --------------------------------------
|
||||
# Les tuiles sont servies à la demande (cache peuplé depuis la machine de
|
||||
# traitement) : /api/sync ne fait plus qu'en régénérer l'index local
|
||||
# (vignettes + carte).
|
||||
_rebuild = {"running": False, "error": None, "done": None, "phase": None}
|
||||
|
||||
|
||||
def _start_rebuild(with_sync):
|
||||
"""Lance en arrière-plan : LIDAR_SYNC_CMD (si demandé) puis build_index.
|
||||
|
||||
Sur la webapp légère, with_sync=True ramène d'abord les tuiles traitées
|
||||
par la machine distante (rsync) ; l'échec éventuel de la synchronisation
|
||||
est remonté dans l'état sans empêcher le rebuild des données locales.
|
||||
"""
|
||||
def _start_rebuild():
|
||||
"""Lance en arrière-plan le rebuild de l'index (vignettes + carte)."""
|
||||
if _rebuild["running"]:
|
||||
raise HTTPException(409, "une synchronisation/rebuild est déjà en cours")
|
||||
raise HTTPException(409, "un rebuild est déjà en cours")
|
||||
|
||||
def _run():
|
||||
_rebuild["running"] = True
|
||||
_rebuild["error"] = None
|
||||
_rebuild["done"] = None
|
||||
sync_error = None
|
||||
if with_sync and SYNC_CMD:
|
||||
_rebuild["phase"] = "sync"
|
||||
try:
|
||||
r = subprocess.run(SYNC_CMD, shell=True, capture_output=True,
|
||||
text=True, timeout=4 * 3600)
|
||||
if r.returncode != 0:
|
||||
tail = (r.stderr or r.stdout or "").strip()[-500:]
|
||||
sync_error = (f"synchronisation échouée (rc={r.returncode})"
|
||||
+ (f" : {tail}" if tail else ""))
|
||||
except Exception as e: # noqa: BLE001 — remonté à l'UI via l'état
|
||||
sync_error = f"synchronisation échouée : {e}"
|
||||
_rebuild["phase"] = "index"
|
||||
try:
|
||||
from .index import build_index
|
||||
build_index(OUTPUT_DIR)
|
||||
except Exception as e: # noqa: BLE001 — remonté à l'UI via l'état
|
||||
_rebuild["error"] = f"{sync_error} ; {e}" if sync_error else str(e)
|
||||
else:
|
||||
_rebuild["error"] = sync_error
|
||||
_rebuild["error"] = str(e)
|
||||
finally:
|
||||
_rebuild["running"] = False
|
||||
_rebuild["phase"] = None
|
||||
_rebuild["done"] = time.time()
|
||||
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
return {"demarré": True, "sync": bool(with_sync and SYNC_CMD)}
|
||||
|
||||
|
||||
def _auto_sync_once():
|
||||
"""Un cycle de cache local : synchronisation + rebuild, si rien ne tourne.
|
||||
|
||||
Retourne True si un cycle a été lancé, False si un rebuild était déjà en
|
||||
cours (le prochain cycle rattrapera).
|
||||
"""
|
||||
if _rebuild["running"]:
|
||||
return False
|
||||
try:
|
||||
_start_rebuild(with_sync=True)
|
||||
return True
|
||||
except HTTPException:
|
||||
return False # course perdue contre un lancement manuel : tant pis
|
||||
|
||||
|
||||
def _auto_sync_loop():
|
||||
"""Boucle du cache local : synchronise les tuiles périodiquement."""
|
||||
while True:
|
||||
time.sleep(AUTO_SYNC_SECONDS)
|
||||
_auto_sync_once()
|
||||
|
||||
|
||||
# Conteneur webapp seul (cache local des tuiles) : lancer la boucle de
|
||||
# synchronisation en arrière-plan si demandé. Sans LIDAR_SYNC_CMD il n'y a
|
||||
# rien à synchroniser — pas de boucle (juste le rebuild à la demande).
|
||||
if AUTO_SYNC_SECONDS > 0 and SYNC_CMD:
|
||||
threading.Thread(target=_auto_sync_loop, daemon=True,
|
||||
name="cache-tuiles").start()
|
||||
return {"demarré": True}
|
||||
|
||||
|
||||
@app.post("/api/sync", dependencies=[Depends(_require_token)])
|
||||
def sync_and_rebuild():
|
||||
"""Synchronise les tuiles (LIDAR_SYNC_CMD) puis régénère la carte en arrière-plan.
|
||||
"""Régénère l'index local (vignettes + carte) en arrière-plan.
|
||||
|
||||
Sans LIDAR_SYNC_CMD (machine de traitement locale), se réduit à un simple
|
||||
rebuild de l'index — même comportement que /api/rebuild.
|
||||
Les tuiles étant servies à la demande, il n'y a plus de synchronisation à
|
||||
exécuter : /api/sync se réduit à un rebuild de l'index (même
|
||||
comportement que /api/rebuild).
|
||||
"""
|
||||
return _start_rebuild(with_sync=True)
|
||||
return _start_rebuild()
|
||||
|
||||
|
||||
@app.get("/api/sync")
|
||||
@ -877,7 +821,7 @@ def sync_status():
|
||||
@app.post("/api/rebuild", dependencies=[Depends(_require_token)])
|
||||
def rebuild_index():
|
||||
"""Régénère la carte (index.html, vignettes, sous-tuiles) en arrière-plan."""
|
||||
return _start_rebuild(with_sync=False)
|
||||
return _start_rebuild()
|
||||
|
||||
|
||||
@app.get("/api/rebuild")
|
||||
@ -1090,9 +1034,9 @@ def export_tiles(req: ExportRequest):
|
||||
"""Assemble des dalles adjacentes en image ou PDF (export.py).
|
||||
|
||||
Local par nature : les tuiles assemblées sont celles du cache output/
|
||||
(la webapp légère possède sa copie via rsync) — aucune délégation à la
|
||||
machine de traitement. Un seul export à la fois (CPU/mémoire limités sur
|
||||
Raspberry Pi).
|
||||
(la webapp légère les possède via le cache à la demande) — aucune
|
||||
délégation à la machine de traitement. Un seul export à la fois
|
||||
(CPU/mémoire limités sur Raspberry Pi).
|
||||
"""
|
||||
import re as _re
|
||||
from .export import EXPORT_FORMATS, build_export
|
||||
@ -1153,4 +1097,13 @@ def export_file(name: str):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=PORT)
|
||||
# HTTPS optionnel : la géolocalisation du navigateur (bouton ⌖, centrage
|
||||
# GPS depuis un téléphone) n'est disponible qu'en contexte sécurisé.
|
||||
# Définir LIDAR_SSL_CERTFILE + LIDAR_SSL_KEYFILE (certificat auto-signé)
|
||||
# pour servir la carte en https://<hôte>:PORT ; le téléphone accepte
|
||||
# l'avertissement « certificat non fiable » puis le GPS fonctionne.
|
||||
_ssl_cert = os.environ.get("LIDAR_SSL_CERTFILE")
|
||||
_ssl_key = os.environ.get("LIDAR_SSL_KEYFILE")
|
||||
_ssl_kw = ({"ssl_certfile": _ssl_cert, "ssl_keyfile": _ssl_key}
|
||||
if _ssl_cert and _ssl_key else {})
|
||||
uvicorn.run(app, host="0.0.0.0", port=PORT, **_ssl_kw)
|
||||
|
||||
Reference in New Issue
Block a user