Borner le comblement du MNT à l'enveloppe des points, ajouter la couche précision et un affichage relief/précision, encoder les sous-tuiles une seule fois en q75

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Antoine Jacquin
2026-09-27 15:07:04 +02:00
parent c5097e21a0
commit 90dbe61f3b
17 changed files with 994 additions and 593 deletions

View File

@ -878,11 +878,12 @@ def tilejson(layer: str, request: Request):
# ---------------------------------------------------------------------------
# Configuration par défaut de la pile de couches
# Configuration d'affichage par défaut
# ---------------------------------------------------------------------------
# L'état réglé depuis la carte (ordre, visibilité, opacité, fusion) peut être
# figé comme configuration servie à TOUT nouveau navigateur : le réglage utile
# ne se perd pas dans un localStorage et se partage sans lien.
# L'affichage réglé depuis la carte (couche principale, mode relief /
# précision / les deux, opacité de la précision, fond) peut être figé comme
# configuration servie à TOUT nouveau navigateur : le réglage utile ne se perd
# pas dans un localStorage et se partage sans lien.
DEFAULTS_FILE = OUTPUT_DIR / ".map-defaults.json"
_defaults_lock = threading.Lock()
@ -903,38 +904,26 @@ def _clamp01(value, fallback=1.0):
return fallback
BLEND_MODES = ("normal", "multiply", "screen", "overlay", "soft-light",
"hard-light", "difference", "luminosity")
def _sanitize_defaults(req):
"""Retient ce qui est connu et borné : couches réelles, opacités 0–1, fusions valides."""
known = set(tiles_mod.available_layers(OUTPUT_DIR))
order = [k for k in (req.order or []) if k in known]
for key in sorted(known - set(order)):
order.append(key)
on = [k for k in (req.on or []) if k in known]
opacity = {k: _clamp01(v) for k, v in (req.opacity or {}).items() if k in known}
blend = {k: v for k, v in (req.blend or {}).items()
if k in known and v in BLEND_MODES}
"""Retient ce qui est connu et borné : couche réelle, mode valide, opacités 0–1."""
from .index import DEFAULT_PRECISION_OPACITY, PRECISION_VIZ, VIEW_MODES
known = set(tiles_mod.available_layers(OUTPUT_DIR)) - {PRECISION_VIZ}
base_in = req.base if isinstance(req.base, dict) else {}
base = {"on": bool(base_in.get("on", True)),
"opacity": _clamp01(base_in.get("opacity"), 0.85),
"dark": bool(base_in.get("dark", True))}
stack = req.stack_blend if req.stack_blend in BLEND_MODES else "normal"
return {"order": order, "on": on, "opacity": opacity, "blend": blend,
"base": base, "stack_blend": stack, "saved_at": time.time()}
return {"main": req.main if req.main in known else None,
"mode": req.mode if req.mode in VIEW_MODES else "relief",
"precision_opacity": _clamp01(req.precision_opacity, DEFAULT_PRECISION_OPACITY),
"base": base, "saved_at": time.time()}
class DefaultsRequest(BaseModel):
order: list = Field(default_factory=list,
description="ordre de pile, du bas vers le haut")
on: list = Field(default_factory=list, description="couches allumées")
opacity: dict = Field(default_factory=dict, description="opacité par couche, 0–1")
blend: dict = Field(default_factory=dict, description="mode de fusion par couche")
main: Optional[str] = Field(None, description="couche d'affichage principal")
mode: str = Field("relief", description="relief | precision | both")
precision_opacity: float = Field(0.6, description="opacité de la précision en mode both, 0–1")
base: dict = Field(default_factory=dict,
description="fond de carte : on, opacity, dark")
stack_blend: str = Field("normal", description="fusion de la pile sur le fond")
@app.get("/api/map/defaults")
@ -952,8 +941,8 @@ def set_defaults(req: DefaultsRequest):
DEFAULTS_FILE.parent.mkdir(parents=True, exist_ok=True)
tmp.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
os.replace(tmp, DEFAULTS_FILE)
logger.info(f"Configuration de pile par défaut enregistrée : "
f"{len(data['on'])} couche(s) allumée(s)")
logger.info(f"Configuration d'affichage par défaut enregistrée : "
f"{data['main'] or 'principale du registre'}, mode {data['mode']}")
return {"enregistré": True, "defaults": data}
@ -973,28 +962,26 @@ def clear_defaults():
@app.get("/api/map/meta")
def map_meta():
"""Couches, zooms, emprise et version — tout l'état initial de l'interface."""
from .index import DEFAULT_BLEND, DEFAULT_OPACITY, default_layers_present
from .index import (DEFAULT_PRECISION_OPACITY, DEFAULT_VIEW_MODE, PRECISION_VIZ,
VIEW_MODES, default_main_layer)
infos = _layer_infos()
keys = [i["key"] for i in infos]
saved = _load_defaults()
saved = _load_defaults() or {}
# Configuration figée depuis la carte, sinon réglages du registre (index.py).
# Couches figées toutes disparues (ex. passage au seul relief orienté) :
# registre, sinon un nouveau navigateur arrive sur une carte sans LiDAR.
saved_on = [k for k in saved["on"] if k in keys] if saved else []
if saved and (saved_on or not saved["on"]):
default_layers = saved_on
else:
default_layers = default_layers_present(keys)
default_opacity = saved["opacity"] if saved else dict(DEFAULT_OPACITY)
default_blend = saved["blend"] if saved else dict(DEFAULT_BLEND)
# Couche figée disparue, ou fichier de l'ancienne pile (clés order/on) :
# couche principale du registre.
main = saved.get("main")
if main not in keys or main == PRECISION_VIZ:
main = default_main_layer(keys)
mode = saved.get("mode") if saved.get("mode") in VIEW_MODES else DEFAULT_VIEW_MODE
return {
"layers": infos,
"default_layers": default_layers,
"default_opacity": default_opacity,
"default_blend": default_blend,
"default_order": saved["order"] if saved else None,
"default_base": saved["base"] if saved else None,
"default_stack_blend": saved["stack_blend"] if saved else "normal",
"default_main": main,
"precision_layer": PRECISION_VIZ if PRECISION_VIZ in keys else None,
"default_mode": mode,
"default_precision_opacity": _clamp01(saved.get("precision_opacity"),
DEFAULT_PRECISION_OPACITY),
"default_base": saved.get("base"),
"defaults_saved": bool(saved),
# L'interface consomme des tuiles 512 px (2× moins de requêtes qu'en
# 256 : décisif en HTTP/1.1) ; les clients OSM gardent le 256 canonique.