Corriger la revue stage1 de la refonte : fiche de dalle, thème du fond, cadre d'export, tokens de couleur
Retour/Échap depuis la fiche détaillée ne retouche plus la hauteur du volet téléphone (openTab remplacé par un rétablissement direct de l'onglet) ; le fond OSM suit le thème sauf choix explicite (migration du localStorage et des défauts serveur en v3, plus de dark:true hérité de l'ex-webapp) et part du bon thème dès sa création (plus de flash sombre) ; l'onglet Export mémorisé ne recadre plus la vue au premier rendu (lien partagé préservé) et referme son cadre pendant que la fiche détaillée est ouverte ; la qualité affichée suit le nom réel de la dalle rendue ; un second clic referme la fiche au lieu de déplacer son contour derrière elle ; couleurs en dur restantes tokenisées (--tile-sel, --gps, .gen-log) et lisibles en thème clair ; localStorage corrompu (lidar-panel, lidar-print) n'y casse plus l'interface. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -20,7 +20,7 @@
|
|||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
- **Un seul serveur web : `mapserve.py`** (image `lidar-maps`, port 8975 léger / 8973 worker). L'ancienne webapp (`webapp.py`, `export.py`, index.html/`_APP_JS`) a été supprimée : la génération de tuiles (portée de la webapp historique — `/api/preview`, `/api/generate`, `/api/status`, `/api/stop`, `/api/queue/clear`, `/api/cell`) vit dans `mapserve.py`, l'interface dans `web/map.{html,css,js}` (relus par `mapui.py`, constantes `_MAP_*` conservées, écrites par `write_map_assets()` et bâchées dans les images). Sur l'image légère sans `LIDAR_GENERATION_URL`, `/api/status` répond `available: false` et l'interface masque l'onglet Génération.
|
- **Un seul serveur web : `mapserve.py`** (image `lidar-maps`, port 8975 léger / 8973 worker). L'ancienne webapp (`webapp.py`, `export.py`, index.html/`_APP_JS`) a été supprimée : la génération de tuiles (portée de la webapp historique — `/api/preview`, `/api/generate`, `/api/status`, `/api/stop`, `/api/queue/clear`, `/api/cell`) vit dans `mapserve.py`, l'interface dans `web/map.{html,css,js}` (relus par `mapui.py`, constantes `_MAP_*` conservées, écrites par `write_map_assets()` et bâchées dans les images). Sur l'image légère sans `LIDAR_GENERATION_URL`, `/api/status` répond `available: false` et l'interface masque l'onglet Génération.
|
||||||
- **Interface en panneau unique** (`web/map.{html,css,js}`) : un seul panneau à onglets Affichage / Export PDF / Génération (masqué si le générateur est indisponible ou non autorisé) / Partager, remplaçant l'ancienne pile de blocs empilés. Sous 720 px de large, le panneau devient un volet en bas d'écran à trois hauteurs (`closed`/`half`/`full`, poignée `#sheetGrip` glissée ou simplement touchée, ou onglet actif retouché). Un clic sur une dalle ouvre une bulle (date, densité) → « Détails » bascule une fiche complète dans le panneau (« ← Retour » rouvre l'onglet précédent) ; Échap ferme la bulle, puis la fiche, puis replie le panneau (bande d'icônes sur ordinateur, volet fermé sur téléphone — la bande reste utilisable, un clic sur un onglet redéplie le panneau). Raccourcis clavier : 1–4 (onglets, sans effet si l'onglet est masqué), P (mode d'affichage suivant), Échap. Deux thèmes clair/sombre (`lidar-theme`, `auto` par défaut, suit le système) et tous les réglages communs posés sur `:root` en variables CSS (aucune couleur en dur dans les composants). Clés `localStorage` (via `lsGet`/`lsSet`, silencieux en navigation privée) : `lidarMapView_v2` (vue/couches), `lidar-print` (réglages d'export), `lidar-panel` (onglet/hauteur du volet), `lidar-theme`.
|
- **Interface en panneau unique** (`web/map.{html,css,js}`) : un seul panneau à onglets Affichage / Export PDF / Génération (masqué si le générateur est indisponible ou non autorisé) / Partager, remplaçant l'ancienne pile de blocs empilés. Sous 720 px de large, le panneau devient un volet en bas d'écran à trois hauteurs (`closed`/`half`/`full`, poignée `#sheetGrip` glissée ou simplement touchée, ou onglet actif retouché). Un clic sur une dalle ouvre une bulle (date, densité) → « Détails » bascule une fiche complète dans le panneau (« ← Retour » rouvre l'onglet précédent) ; Échap ferme la bulle, puis la fiche, puis replie le panneau (bande d'icônes sur ordinateur, volet fermé sur téléphone — la bande reste utilisable, un clic sur un onglet redéplie le panneau). Raccourcis clavier : 1–4 (onglets, sans effet si l'onglet est masqué), P (mode d'affichage suivant), Échap. Deux thèmes clair/sombre (`lidar-theme`, `auto` par défaut, suit le système) et tous les réglages communs posés sur `:root` en variables CSS (aucune couleur en dur dans les composants). Clés `localStorage` (via `lsGet`/`lsSet`, silencieux en navigation privée) : `lidarMapView_v2` (vue/couches), `lidar-print` (réglages d'export), `lidar-panel` (onglet, hauteur du volet, repli en bande d'icônes), `lidar-theme`.
|
||||||
- **`index.py` = catalogue + registres partagés** (plus d'interface) : `VIZ_LABELS`/`VIZ_LEGENDS`, défauts d'affichage (`DEFAULT_VIZ`/`PRECISION_VIZ`/`VIEW_MODES`), `PANEL_VIZ`/`KEYWORD_TO_STEP`, `scan_tiles`/`cells_with_all_viz`, vignettes + sous-tuiles + inventaire `index_tiles.json` (`build_index`). L'inventaire est servi par `/api/tiles` de mapserve aux machines légères (`LIDAR_SOURCE_URL`).
|
- **`index.py` = catalogue + registres partagés** (plus d'interface) : `VIZ_LABELS`/`VIZ_LEGENDS`, défauts d'affichage (`DEFAULT_VIZ`/`PRECISION_VIZ`/`VIEW_MODES`), `PANEL_VIZ`/`KEYWORD_TO_STEP`, `scan_tiles`/`cells_with_all_viz`, vignettes + sous-tuiles + inventaire `index_tiles.json` (`build_index`). L'inventaire est servi par `/api/tiles` de mapserve aux machines légères (`LIDAR_SOURCE_URL`).
|
||||||
- **Generation is 0.2 m only** (policy): `/api/generate` (`GENERATE_RESOLUTIONS` in `mapserve.py`), the compose `process` command and the CLI `-r` default all produce 0.2 m exclusively; 0.5 m stays available via explicit `-r 0.5`. Completeness detection (`complete_cells`) requires the viz at 0.2 m only.
|
- **Generation is 0.2 m only** (policy): `/api/generate` (`GENERATE_RESOLUTIONS` in `mapserve.py`), the compose `process` command and the CLI `-r` default all produce 0.2 m exclusively; 0.5 m stays available via explicit `-r 0.5`. Completeness detection (`complete_cells`) requires the viz at 0.2 m only.
|
||||||
- **Génération du nord au sud** : les tuiles sont traitées par ligne décroissante (row = nord en km), colonnes croissantes — `find_laz_files` (pipeline.py) pour les passes batch et `_resolve_request` (mapserve.py) pour les runs lancés depuis la carte. Les workers prennent les fichiers dans l'ordre de soumission : la carte se remplit de haut en bas pendant un run (`--file` explicite au CLI = ordre utilisateur préservé). Parallélisme de génération : `LIDAR_WORKERS` (10 dans les compose ; `auto` sinon).
|
- **Génération du nord au sud** : les tuiles sont traitées par ligne décroissante (row = nord en km), colonnes croissantes — `find_laz_files` (pipeline.py) pour les passes batch et `_resolve_request` (mapserve.py) pour les runs lancés depuis la carte. Les workers prennent les fichiers dans l'ordre de soumission : la carte se remplit de haut en bas pendant un run (`--file` explicite au CLI = ordre utilisateur préservé). Parallélisme de génération : `LIDAR_WORKERS` (10 dans les compose ; `auto` sinon).
|
||||||
|
|||||||
@ -911,11 +911,15 @@ def _sanitize_defaults(req):
|
|||||||
base_in = req.base if isinstance(req.base, dict) else {}
|
base_in = req.base if isinstance(req.base, dict) else {}
|
||||||
base = {"on": bool(base_in.get("on", True)),
|
base = {"on": bool(base_in.get("on", True)),
|
||||||
"opacity": _clamp01(base_in.get("opacity"), 0.85),
|
"opacity": _clamp01(base_in.get("opacity"), 0.85),
|
||||||
"dark": bool(base_in.get("dark", True))}
|
# None = pas de choix explicite (le fond suit le thème côté carte) ;
|
||||||
|
# seule une valeur booléenne reçue devient un choix figé.
|
||||||
|
"dark": None if base_in.get("dark") is None else bool(base_in.get("dark"))}
|
||||||
|
# v: 3 distingue ce format (dark éventuellement None = pas choisi) de
|
||||||
|
# l'ancien, qui écrivait toujours dark:true (voir map_meta).
|
||||||
return {"main": req.main if req.main in known else None,
|
return {"main": req.main if req.main in known else None,
|
||||||
"mode": req.mode if req.mode in VIEW_MODES else "relief",
|
"mode": req.mode if req.mode in VIEW_MODES else "relief",
|
||||||
"precision_opacity": _clamp01(req.precision_opacity, DEFAULT_PRECISION_OPACITY),
|
"precision_opacity": _clamp01(req.precision_opacity, DEFAULT_PRECISION_OPACITY),
|
||||||
"base": base, "saved_at": time.time()}
|
"base": base, "v": 3, "saved_at": time.time()}
|
||||||
|
|
||||||
|
|
||||||
class DefaultsRequest(BaseModel):
|
class DefaultsRequest(BaseModel):
|
||||||
@ -974,6 +978,11 @@ def map_meta():
|
|||||||
if main not in keys or main == PRECISION_VIZ:
|
if main not in keys or main == PRECISION_VIZ:
|
||||||
main = default_main_layer(keys)
|
main = default_main_layer(keys)
|
||||||
mode = saved.get("mode") if saved.get("mode") in VIEW_MODES else DEFAULT_VIEW_MODE
|
mode = saved.get("mode") if saved.get("mode") in VIEW_MODES else DEFAULT_VIEW_MODE
|
||||||
|
# Fichier d'avant v3 (ex-webapp) : base.dark:true systématique, jamais un
|
||||||
|
# vrai choix — servi comme "non choisi" (None, le fond suit le thème).
|
||||||
|
base_default = saved.get("base")
|
||||||
|
if isinstance(base_default, dict) and saved.get("v") != 3:
|
||||||
|
base_default = dict(base_default, dark=None)
|
||||||
return {
|
return {
|
||||||
"layers": infos,
|
"layers": infos,
|
||||||
"default_main": main,
|
"default_main": main,
|
||||||
@ -981,7 +990,7 @@ def map_meta():
|
|||||||
"default_mode": mode,
|
"default_mode": mode,
|
||||||
"default_precision_opacity": _clamp01(saved.get("precision_opacity"),
|
"default_precision_opacity": _clamp01(saved.get("precision_opacity"),
|
||||||
DEFAULT_PRECISION_OPACITY),
|
DEFAULT_PRECISION_OPACITY),
|
||||||
"default_base": saved.get("base"),
|
"default_base": base_default,
|
||||||
"defaults_saved": bool(saved),
|
"defaults_saved": bool(saved),
|
||||||
# L'interface consomme des tuiles 512 px (2× moins de requêtes qu'en
|
# 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.
|
# 256 : décisif en HTTP/1.1) ; les clients OSM gardent le 256 canonique.
|
||||||
@ -1085,16 +1094,20 @@ def map_tile_info(lat: float, lng: float):
|
|||||||
# coins NO, NE, SE, SO (lat, lng) : cadre de sélection sur la carte
|
# coins NO, NE, SE, SO (lat, lng) : cadre de sélection sur la carte
|
||||||
"corners": [[round(la, 7), round(lo, 7)] for lo, la in zip(lons, lats)],
|
"corners": [[round(la, 7), round(lo, 7)] for lo, la in zip(lons, lats)],
|
||||||
}
|
}
|
||||||
q = read_quality(OUTPUT_DIR, base["name"])
|
|
||||||
base["quality"] = ({"ground_density": q.get("ground_density"),
|
|
||||||
"acq_start": q.get("acq_start"), "acq_end": q.get("acq_end")}
|
|
||||||
if q else None)
|
|
||||||
best = None
|
best = None
|
||||||
for t in scan_tiles(OUTPUT_DIR / "visualisations"):
|
for t in scan_tiles(OUTPUT_DIR / "visualisations"):
|
||||||
if (t["col"], t["row"]) != (col, row):
|
if (t["col"], t["row"]) != (col, row):
|
||||||
continue
|
continue
|
||||||
if best is None or t["resolution"] < best["resolution"]:
|
if best is None or t["resolution"] < best["resolution"]:
|
||||||
best = t
|
best = t
|
||||||
|
# Qualité (densité, dates) : sous le nom réel de la dalle rendue quand il y
|
||||||
|
# en a une (best["basename"] peut différer du nom nominal calculé depuis
|
||||||
|
# col/row) ; nom nominal sinon, faute de mieux.
|
||||||
|
quality_name = best["basename"] if best else base["name"]
|
||||||
|
q = read_quality(OUTPUT_DIR, quality_name)
|
||||||
|
base["quality"] = ({"ground_density": q.get("ground_density"),
|
||||||
|
"acq_start": q.get("acq_start"), "acq_end": q.get("acq_end")}
|
||||||
|
if q else None)
|
||||||
if best is None:
|
if best is None:
|
||||||
return {"found": False, **base}
|
return {"found": False, **base}
|
||||||
meta = _collect_tile_metadata(best, OUTPUT_DIR / "DTM")
|
meta = _collect_tile_metadata(best, OUTPUT_DIR / "DTM")
|
||||||
|
|||||||
@ -268,6 +268,30 @@ def test_map_tile_quality_absent_is_none(tmp_path, monkeypatch):
|
|||||||
assert mapserve.map_tile_info(lat=lat, lng=lon)["quality"] is None
|
assert mapserve.map_tile_info(lat=lat, lng=lon)["quality"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_map_tile_quality_uses_real_basename(tmp_path, monkeypatch):
|
||||||
|
"""La qualité lue est celle du nom réel de la dalle rendue (best["basename"]),
|
||||||
|
pas celle du nom nominal calculé depuis col/row — ils peuvent différer."""
|
||||||
|
import lidar_pipeline.mapserve as mapserve
|
||||||
|
from lidar_pipeline import index as index_mod
|
||||||
|
from lidar_pipeline.quality import write_quality, QUALITY_VERSION
|
||||||
|
_setup(tmp_path, monkeypatch, layers=("relief_oriente",))
|
||||||
|
nominal = "LHD_FXX_1054_6882_PTS_LAMB93_IGN69"
|
||||||
|
real = nominal + "_r0p2"
|
||||||
|
write_quality(tmp_path, real, {"version": QUALITY_VERSION, "ground_density": 42.0,
|
||||||
|
"density_grid": [[42.0]], "empty_fraction": 0.0,
|
||||||
|
"acq_start": "2023-01-01", "acq_end": "2023-01-01",
|
||||||
|
"acq_source": "gps"})
|
||||||
|
fake_tile = {"col": 1054, "row": 6882, "resolution": 0.2, "basename": real, "viz": {}}
|
||||||
|
monkeypatch.setattr(index_mod, "scan_tiles", lambda d: [fake_tile])
|
||||||
|
monkeypatch.setattr(index_mod, "_collect_tile_metadata",
|
||||||
|
lambda t, dtm_dir: {"method": None, "generated": None, "viz": {}})
|
||||||
|
lat, lon = _center()
|
||||||
|
d = mapserve.map_tile_info(lat=lat, lng=lon)
|
||||||
|
assert d["found"] is True
|
||||||
|
assert d["name"] == real
|
||||||
|
assert d["quality"]["ground_density"] == 42.0
|
||||||
|
|
||||||
|
|
||||||
def test_healthz_and_assets(tmp_path, monkeypatch):
|
def test_healthz_and_assets(tmp_path, monkeypatch):
|
||||||
"""Sonde de santé et assets de l'interface (revalidés, jamais immuables)."""
|
"""Sonde de santé et assets de l'interface (revalidés, jamais immuables)."""
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
@ -414,6 +438,43 @@ def test_defaults_sanitised(tmp_path, monkeypatch):
|
|||||||
assert data["main"] is None # la précision n'est pas une principale
|
assert data["main"] is None # la précision n'est pas une principale
|
||||||
|
|
||||||
|
|
||||||
|
def test_defaults_dark_none_is_not_a_choice(tmp_path, monkeypatch):
|
||||||
|
"""base.dark omis/None n'est pas un choix : il reste None (le fond suit le
|
||||||
|
thème côté carte), distinct d'un choix explicite (True/False persistent)."""
|
||||||
|
import lidar_pipeline.mapserve as mapserve
|
||||||
|
_setup(tmp_path, monkeypatch, layers=("relief_oriente",))
|
||||||
|
monkeypatch.setattr(mapserve, "DEFAULTS_FILE", tmp_path / ".map-defaults.json")
|
||||||
|
|
||||||
|
req = mapserve.DefaultsRequest(main="relief_oriente",
|
||||||
|
base={"on": True, "opacity": 0.4})
|
||||||
|
saved = mapserve.set_defaults(req)["defaults"]
|
||||||
|
assert saved["base"]["dark"] is None
|
||||||
|
assert saved["v"] == 3
|
||||||
|
assert mapserve.map_meta()["default_base"]["dark"] is None
|
||||||
|
|
||||||
|
# Choix explicite : conservé tel quel
|
||||||
|
req = mapserve.DefaultsRequest(main="relief_oriente",
|
||||||
|
base={"on": True, "opacity": 0.4, "dark": True})
|
||||||
|
mapserve.set_defaults(req)
|
||||||
|
assert mapserve.map_meta()["default_base"]["dark"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_defaults_legacy_dark_true_becomes_unset(tmp_path, monkeypatch):
|
||||||
|
"""Fichier d'avant v3 (ex-webapp, base.dark:true systématique) : servi avec
|
||||||
|
dark None (« non choisi »), jamais imposé comme choix de l'utilisateur."""
|
||||||
|
import json
|
||||||
|
import lidar_pipeline.mapserve as mapserve
|
||||||
|
_setup(tmp_path, monkeypatch, layers=("relief_oriente",))
|
||||||
|
defaults = tmp_path / ".map-defaults.json"
|
||||||
|
monkeypatch.setattr(mapserve, "DEFAULTS_FILE", defaults)
|
||||||
|
defaults.write_text(json.dumps({
|
||||||
|
"main": "relief_oriente", "mode": "relief",
|
||||||
|
"base": {"on": True, "opacity": 0.85, "dark": True}}), encoding="utf-8")
|
||||||
|
meta = mapserve.map_meta()
|
||||||
|
assert meta["default_base"]["dark"] is None
|
||||||
|
assert meta["default_base"]["opacity"] == 0.85
|
||||||
|
|
||||||
|
|
||||||
def test_ui_applies_server_defaults():
|
def test_ui_applies_server_defaults():
|
||||||
"""L'interface part des défauts du serveur et sait les (re)définir."""
|
"""L'interface part des défauts du serveur et sait les (re)définir."""
|
||||||
from lidar_pipeline.mapui import _MAP_HTML, _MAP_JS
|
from lidar_pipeline.mapui import _MAP_HTML, _MAP_JS
|
||||||
@ -426,6 +487,90 @@ def test_ui_applies_server_defaults():
|
|||||||
assert "lsDel(LS_KEY)" in _MAP_JS
|
assert "lsDel(LS_KEY)" in _MAP_JS
|
||||||
|
|
||||||
|
|
||||||
|
def test_ui_base_dark_follows_theme_unless_chosen():
|
||||||
|
"""base.dark ne vaut jamais un choix implicite : None = suit le thème.
|
||||||
|
|
||||||
|
Client : defaultState garde None (au lieu de forcer un booléen) ; loadState
|
||||||
|
ignore le base.dark d'un ancien lidarMapView_v2 (avant v3, où l'ex-webapp
|
||||||
|
persistait dark:true à chaque sauvegarde) ; saveState écrit v: 3. La classe
|
||||||
|
CSS du fond OSM part du thème courant, pas d'un « base-dark » figé."""
|
||||||
|
from lidar_pipeline.mapui import _MAP_JS
|
||||||
|
assert "dark: b.dark == null ? null : !!b.dark" in _MAP_JS
|
||||||
|
assert "if (s.v !== 3) delete sBase.dark;" in _MAP_JS
|
||||||
|
assert "lsSet(LS_KEY, Object.assign({ v: 3 }, STATE))" in _MAP_JS
|
||||||
|
assert "className: themeIsDark() ? 'base-dark' : ''" in _MAP_JS
|
||||||
|
|
||||||
|
|
||||||
|
def test_ui_phone_return_and_escape_keep_sheet_height():
|
||||||
|
"""« ← Retour » et Échap depuis la fiche de dalle rétablissent l'onglet
|
||||||
|
précédent SANS repasser par openTab (qui retoucherait la hauteur du volet
|
||||||
|
en re-touchant l'onglet déjà actif) : la fiche ferme via un rétablissement
|
||||||
|
direct de PANEL.tab/want, partagé par closeTileViews et showTilePopup."""
|
||||||
|
from lidar_pipeline.mapui import _MAP_JS
|
||||||
|
assert "function restoreTabAfterDetails()" in _MAP_JS
|
||||||
|
restore = _MAP_JS[_MAP_JS.index("function restoreTabAfterDetails("):]
|
||||||
|
assert "PANEL.tab = PANEL.want = tileReturnTab || 'view';" in restore[:300]
|
||||||
|
assert "openTab(" not in restore[:300]
|
||||||
|
close_tiles = _MAP_JS[_MAP_JS.index("function closeTileViews("):]
|
||||||
|
assert "restoreTabAfterDetails()" in close_tiles[:250]
|
||||||
|
# Second clic sur la carte pendant que la fiche est ouverte : referme la
|
||||||
|
# fiche plutôt que de bouger le contour derrière elle (M5)
|
||||||
|
popup_fn = _MAP_JS[_MAP_JS.index("function showTilePopup("):]
|
||||||
|
assert "restoreTabAfterDetails()" in popup_fn[:400]
|
||||||
|
|
||||||
|
|
||||||
|
def test_ui_saved_print_tab_does_not_rezoom_shared_link():
|
||||||
|
"""L'onglet Export mémorisé ne recadre pas la vue au tout premier rendu du
|
||||||
|
panneau (sinon il écrase le zoom d'un lien partagé ou de l'URL initiale) :
|
||||||
|
BOOTSTRAPPING passe à faux juste après ce premier rendu."""
|
||||||
|
from lidar_pipeline.mapui import _MAP_JS
|
||||||
|
assert "let BOOTSTRAPPING = true;" in _MAP_JS
|
||||||
|
assert "printSetOpen(e.detail.visible === 'print', !BOOTSTRAPPING);" in _MAP_JS
|
||||||
|
boot = _MAP_JS[_MAP_JS.index("panelRender();\n BOOTSTRAPPING = false;"):]
|
||||||
|
assert boot.startswith("panelRender();\n BOOTSTRAPPING = false;")
|
||||||
|
|
||||||
|
|
||||||
|
def test_ui_details_view_hides_print_frame():
|
||||||
|
"""Ouvrir la fiche détaillée pendant que l'onglet Export est affiché
|
||||||
|
referme le cadre d'export (sinon cadre + poignée restent visibles
|
||||||
|
derrière la fiche)."""
|
||||||
|
from lidar_pipeline.mapui import _MAP_JS
|
||||||
|
open_details = _MAP_JS[_MAP_JS.index("function openTileDetails("):]
|
||||||
|
assert "printSetOpen(false);" in open_details[:600]
|
||||||
|
|
||||||
|
|
||||||
|
def test_ui_colors_are_tokenised():
|
||||||
|
"""Plus de couleur en dur pour le contour de sélection, le marqueur GPS et
|
||||||
|
le journal de génération (AGENTS.md « aucune couleur en dur dans les
|
||||||
|
composants ») ; #1b1b1b reste en dur sur la poignée d'export (contraste
|
||||||
|
fixe voulu sur --print), documenté en commentaire."""
|
||||||
|
from lidar_pipeline.mapui import _MAP_CSS, _MAP_JS
|
||||||
|
assert "color: cssVar('--tile-sel')" in _MAP_JS
|
||||||
|
assert "'#ffd24a'" not in _MAP_JS
|
||||||
|
assert "cssVar('--gps')" in _MAP_JS
|
||||||
|
assert "'#39d98a'" not in _MAP_JS
|
||||||
|
assert "--tile-sel:" in _MAP_CSS and "--gps:" in _MAP_CSS
|
||||||
|
assert ".gen-log { font:" in _MAP_CSS
|
||||||
|
gen_log = _MAP_CSS[_MAP_CSS.index(".gen-log {"):]
|
||||||
|
assert "rgba(0,0,0,0.35)" not in gen_log[:250]
|
||||||
|
assert "var(--surface-2)" in gen_log[:250] and "var(--text)" in gen_log[:250]
|
||||||
|
assert "#1b1b1b" in _MAP_CSS # volontaire, commenté juste au-dessus
|
||||||
|
|
||||||
|
|
||||||
|
def test_ui_guards_corrupt_local_storage():
|
||||||
|
"""Une valeur corrompue dans lidar-panel/lidar-print (autre appli sur la
|
||||||
|
même origine, quota dépassé en écriture…) ne casse pas l'interface :
|
||||||
|
whitelist de type/valeur plutôt qu'un usage direct de lsGet."""
|
||||||
|
from lidar_pipeline.mapui import _MAP_JS
|
||||||
|
assert "function _sanitizedPanelLS()" in _MAP_JS
|
||||||
|
panel_ls = _MAP_JS[_MAP_JS.index("function _sanitizedPanelLS("):]
|
||||||
|
assert "typeof s.tab === 'string'" in panel_ls[:700]
|
||||||
|
assert "typeof s.collapsed === 'boolean'" in panel_ls[:700]
|
||||||
|
assert "['closed', 'half', 'full'].includes(s.sheet)" in panel_ls[:700]
|
||||||
|
print_load = _MAP_JS[_MAP_JS.index("function printLoad("):]
|
||||||
|
assert "raw && typeof raw === 'object' && !Array.isArray(raw)" in print_load[:400]
|
||||||
|
|
||||||
|
|
||||||
def test_ui_uses_standard_tilelayer():
|
def test_ui_uses_standard_tilelayer():
|
||||||
"""L'interface s'appuie sur le LOD natif de Leaflet, pas sur un palier maison."""
|
"""L'interface s'appuie sur le LOD natif de Leaflet, pas sur un palier maison."""
|
||||||
from lidar_pipeline.mapui import _MAP_JS, render_html, ui_version
|
from lidar_pipeline.mapui import _MAP_JS, render_html, ui_version
|
||||||
|
|||||||
@ -19,6 +19,10 @@
|
|||||||
--gen-pending: #0b8fc0;
|
--gen-pending: #0b8fc0;
|
||||||
--gen-failed: #c8452c;
|
--gen-failed: #c8452c;
|
||||||
--gen-sel: var(--accent);
|
--gen-sel: var(--accent);
|
||||||
|
/* Contour de sélection de dalle : plus sombre en thème clair (lisible sur
|
||||||
|
le fond OSM clair), jaune franc en thème sombre. */
|
||||||
|
--tile-sel: #b8860b;
|
||||||
|
--gps: #39d98a;
|
||||||
--fs-xs: 11px; --fs-sm: 12px; --fs-md: 13px; --fs-lg: 16px;
|
--fs-xs: 11px; --fs-sm: 12px; --fs-md: 13px; --fs-lg: 16px;
|
||||||
--r-sm: 6px; --r-md: 10px; --r-full: 999px;
|
--r-sm: 6px; --r-md: 10px; --r-full: 999px;
|
||||||
--sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
|
--sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
|
||||||
@ -34,6 +38,7 @@
|
|||||||
--border: rgba(255, 255, 255, 0.12); --text: #d9dee6; --muted: #8b94a3;
|
--border: rgba(255, 255, 255, 0.12); --text: #d9dee6; --muted: #8b94a3;
|
||||||
--accent: #5aa9e6; --on-accent: #0b0e13; --track: rgba(255, 255, 255, 0.18);
|
--accent: #5aa9e6; --on-accent: #0b0e13; --track: rgba(255, 255, 255, 0.18);
|
||||||
--print: #ffd166; --gen-running: #f4a261; --gen-pending: #4cc9f0; --gen-failed: #e76f51;
|
--print: #ffd166; --gen-running: #f4a261; --gen-pending: #4cc9f0; --gen-failed: #e76f51;
|
||||||
|
--tile-sel: #ffd24a;
|
||||||
--shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.4); --shadow-lg: 0 6px 24px rgba(0, 0, 0, 0.45);
|
--shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.4); --shadow-lg: 0 6px 24px rgba(0, 0, 0, 0.45);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,6 +48,7 @@
|
|||||||
--border: rgba(255, 255, 255, 0.12); --text: #d9dee6; --muted: #8b94a3;
|
--border: rgba(255, 255, 255, 0.12); --text: #d9dee6; --muted: #8b94a3;
|
||||||
--accent: #5aa9e6; --on-accent: #0b0e13; --track: rgba(255, 255, 255, 0.18);
|
--accent: #5aa9e6; --on-accent: #0b0e13; --track: rgba(255, 255, 255, 0.18);
|
||||||
--print: #ffd166; --gen-running: #f4a261; --gen-pending: #4cc9f0; --gen-failed: #e76f51;
|
--print: #ffd166; --gen-running: #f4a261; --gen-pending: #4cc9f0; --gen-failed: #e76f51;
|
||||||
|
--tile-sel: #ffd24a;
|
||||||
--shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.4); --shadow-lg: 0 6px 24px rgba(0, 0, 0, 0.45);
|
--shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.4); --shadow-lg: 0 6px 24px rgba(0, 0, 0, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,6 +194,10 @@ input[type="range"]::-moz-range-thumb { width: 10px; height: 10px; border-radius
|
|||||||
|
|
||||||
/* --- génération de tuiles ------------------------------------------------ */
|
/* --- génération de tuiles ------------------------------------------------ */
|
||||||
.print-msg { font-size: var(--fs-sm); margin-left: 8px; }
|
.print-msg { font-size: var(--fs-sm); margin-left: 8px; }
|
||||||
|
/* #1b1b1b (glyphe + bordure) reste en dur volontairement : --print est un jaune/ambre
|
||||||
|
proche dans les deux thèmes (#d99a00 clair, #ffd166 sombre), un texte sombre fixe
|
||||||
|
y garde un contraste correct dans les deux cas — un token --text/--on-accent
|
||||||
|
changerait de sens (clair en thème sombre) et casserait ce contraste. */
|
||||||
.print-handle { display: flex; align-items: center; justify-content: center; cursor: move;
|
.print-handle { display: flex; align-items: center; justify-content: center; cursor: move;
|
||||||
font-size: var(--fs-lg); color: #1b1b1b; background: var(--print); border: 2px solid #1b1b1b;
|
font-size: var(--fs-lg); color: #1b1b1b; background: var(--print); border: 2px solid #1b1b1b;
|
||||||
border-radius: var(--r-full); box-shadow: 0 1px 6px rgba(0,0,0,0.5); }
|
border-radius: var(--r-full); box-shadow: 0 1px 6px rgba(0,0,0,0.5); }
|
||||||
@ -198,8 +208,8 @@ body.drawing .leaflet-container { cursor: crosshair !important; }
|
|||||||
.chk { display: flex; gap: 7px; align-items: center; font-size: var(--fs-xs);
|
.chk { display: flex; gap: 7px; align-items: center; font-size: var(--fs-xs);
|
||||||
color: var(--muted); margin: 4px 0; cursor: pointer; }
|
color: var(--muted); margin: 4px 0; cursor: pointer; }
|
||||||
.chk input { accent-color: var(--accent); }
|
.chk input { accent-color: var(--accent); }
|
||||||
.gen-log { font: var(--fs-xs)/1.5 ui-monospace, "SF Mono", Menlo, monospace; color: var(--muted);
|
.gen-log { font: var(--fs-xs)/1.5 ui-monospace, "SF Mono", Menlo, monospace; color: var(--text);
|
||||||
background: rgba(0,0,0,0.35); border: 1px solid var(--border); border-radius: var(--r-sm);
|
background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r-sm);
|
||||||
padding: 6px 8px; max-height: 130px; overflow-y: auto; white-space: pre-wrap;
|
padding: 6px 8px; max-height: 130px; overflow-y: auto; white-space: pre-wrap;
|
||||||
word-break: break-all; margin: 6px 0; }
|
word-break: break-all; margin: 6px 0; }
|
||||||
#tools button.busy { color: var(--accent); border-color: var(--accent); }
|
#tools button.busy { color: var(--accent); border-color: var(--accent); }
|
||||||
|
|||||||
@ -44,6 +44,10 @@ const PREC_LEVELS = 16;
|
|||||||
|
|
||||||
let META = null;
|
let META = null;
|
||||||
let STATE = null;
|
let STATE = null;
|
||||||
|
// Vrai jusqu'au premier rendu du panneau au démarrage : évite qu'un onglet
|
||||||
|
// Export mémorisé recadre la vue par-dessus un lien partagé ou l'URL initiale
|
||||||
|
// (cf. printSetOpen).
|
||||||
|
let BOOTSTRAPPING = true;
|
||||||
|
|
||||||
// --- thème ------------------------------------------------------------------
|
// --- thème ------------------------------------------------------------------
|
||||||
// auto = suit le système ; ☀/☾ force clair ou sombre ; 3ᵉ appui : auto.
|
// auto = suit le système ; ☀/☾ force clair ou sombre ; 3ᵉ appui : auto.
|
||||||
@ -174,7 +178,10 @@ ZoomAnimTileLayer.addInitHook(function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const base = new ZoomAnimTileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
const base = new ZoomAnimTileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
maxZoom: 19, maxNativeZoom: 19, keepBuffer: 2, className: 'base-dark',
|
// Classe posée dès la création d'après le thème courant (pas un « dark » figé) :
|
||||||
|
// sinon le fond flashe sombre en thème clair le temps que /api/map/meta réponde
|
||||||
|
// (et pour de bon si l'appel échoue). applyLayers() la corrige ensuite.
|
||||||
|
maxZoom: 19, maxNativeZoom: 19, keepBuffer: 2, className: themeIsDark() ? 'base-dark' : '',
|
||||||
// Sinon _setView recrée un niveau en cours d'animation malgré noUpdate.
|
// Sinon _setView recrée un niveau en cours d'animation malgré noUpdate.
|
||||||
updateWhenZooming: false,
|
updateWhenZooming: false,
|
||||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||||
@ -210,7 +217,9 @@ function defaultState(meta) {
|
|||||||
precOpacity: clamp01(meta.default_precision_opacity, 0.6),
|
precOpacity: clamp01(meta.default_precision_opacity, 0.6),
|
||||||
base: { on: b.on !== undefined ? !!b.on : true,
|
base: { on: b.on !== undefined ? !!b.on : true,
|
||||||
opacity: b.opacity !== undefined ? Number(b.opacity) : 0.85,
|
opacity: b.opacity !== undefined ? Number(b.opacity) : 0.85,
|
||||||
dark: b.dark !== undefined ? !!b.dark : null },
|
// null = suit le thème (choix explicite seulement si b.dark est déjà
|
||||||
|
// un booléen posé côté serveur).
|
||||||
|
dark: b.dark == null ? null : !!b.dark },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,10 +228,15 @@ function loadState(meta) {
|
|||||||
let st = def;
|
let st = def;
|
||||||
const s = lsGet(LS_KEY, null);
|
const s = lsGet(LS_KEY, null);
|
||||||
if (s) {
|
if (s) {
|
||||||
|
// Ancien format (avant v3) : l'ex-webapp persistait base.dark:true à chaque
|
||||||
|
// sauvegarde, jamais un vrai choix de l'utilisateur — ignoré ici (le fond
|
||||||
|
// suit alors le thème) ; v3 seulement écrit un dark explicite (voir saveState).
|
||||||
|
const sBase = Object.assign({}, s.base || {});
|
||||||
|
if (s.v !== 3) delete sBase.dark;
|
||||||
st = { main: validMain(meta, s.main) || def.main,
|
st = { main: validMain(meta, s.main) || def.main,
|
||||||
mode: validMode(s.mode) || def.mode,
|
mode: validMode(s.mode) || def.mode,
|
||||||
precOpacity: clamp01(s.precOpacity, def.precOpacity),
|
precOpacity: clamp01(s.precOpacity, def.precOpacity),
|
||||||
base: Object.assign({}, def.base, s.base || {}) };
|
base: Object.assign({}, def.base, sBase) };
|
||||||
}
|
}
|
||||||
// Lien partagé : prime sur l'état local (les anciens liens &L=… de la
|
// Lien partagé : prime sur l'état local (les anciens liens &L=… de la
|
||||||
// pile de couches sont ignorés, la vue s'ouvre alors sur l'état courant).
|
// pile de couches sont ignorés, la vue s'ouvre alors sur l'état courant).
|
||||||
@ -236,7 +250,9 @@ function loadState(meta) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveState() {
|
function saveState() {
|
||||||
lsSet(LS_KEY, STATE);
|
// v: 3 marque un format où base.dark, quand posé, est un vrai choix de
|
||||||
|
// l'utilisateur (voir loadState) — jamais mélangé à STATE lui-même.
|
||||||
|
lsSet(LS_KEY, Object.assign({ v: 3 }, STATE));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- hash de vue ----------------------------------------------------------
|
// --- hash de vue ----------------------------------------------------------
|
||||||
@ -488,7 +504,7 @@ function selectTileOutline(data) {
|
|||||||
if (!data || !data.corners) return;
|
if (!data || !data.corners) return;
|
||||||
// Panneau dédié au-dessus de la pile LiDAR : dans l'overlayPane par défaut
|
// Panneau dédié au-dessus de la pile LiDAR : dans l'overlayPane par défaut
|
||||||
// (z-index 400, comme lidarStack), le contour passait sous les tuiles.
|
// (z-index 400, comme lidarStack), le contour passait sous les tuiles.
|
||||||
tileSel = L.polygon(data.corners, { pane: 'tileSel', color: '#ffd24a', weight: 2,
|
tileSel = L.polygon(data.corners, { pane: 'tileSel', color: cssVar('--tile-sel'), weight: 2,
|
||||||
fill: false, dashArray: '6 4',
|
fill: false, dashArray: '6 4',
|
||||||
interactive: false }).addTo(map);
|
interactive: false }).addTo(map);
|
||||||
}
|
}
|
||||||
@ -586,6 +602,10 @@ let tileReturnTab = null; // onglet à rouvrir après « ← Retour »
|
|||||||
|
|
||||||
function showTilePopup(data, latlng) {
|
function showTilePopup(data, latlng) {
|
||||||
lastTileQuery = latlng || lastTileQuery;
|
lastTileQuery = latlng || lastTileQuery;
|
||||||
|
// Un nouveau clic pendant que la fiche détaillée d'une AUTRE dalle est
|
||||||
|
// ouverte : on la referme (sinon le contour bouge sur la carte alors que
|
||||||
|
// la fiche affiche encore l'ancienne dalle).
|
||||||
|
if (!el('tileView').hidden) restoreTabAfterDetails();
|
||||||
if (tilePopup) tilePopup.remove();
|
if (tilePopup) tilePopup.remove();
|
||||||
const html = !data
|
const html = !data
|
||||||
? '<div class="tile-pop">Serveur injoignable : réessayez dans un instant.</div>'
|
? '<div class="tile-pop">Serveur injoignable : réessayez dans un instant.</div>'
|
||||||
@ -614,6 +634,10 @@ function openTileDetails() {
|
|||||||
tileReturnTab = PANEL.tab;
|
tileReturnTab = PANEL.tab;
|
||||||
for (const t of ['view', 'print', 'gen', 'share']) el('tab-' + t).hidden = true;
|
for (const t of ['view', 'print', 'gen', 'share']) el('tab-' + t).hidden = true;
|
||||||
el('tileView').hidden = false;
|
el('tileView').hidden = false;
|
||||||
|
// La fiche masque l'onglet Export : son cadre et sa poignée n'ont rien à
|
||||||
|
// faire sur la carte tant qu'elle est ouverte (restoreTabAfterDetails les
|
||||||
|
// rétablit via panel:tab en revenant sur l'onglet Export).
|
||||||
|
printSetOpen(false);
|
||||||
if (isPhone() && PANEL.sheet === 'closed') setSheet('half');
|
if (isPhone() && PANEL.sheet === 'closed') setSheet('half');
|
||||||
if (!isPhone() && PANEL.collapsed) setCollapsed(false);
|
if (!isPhone() && PANEL.collapsed) setCollapsed(false);
|
||||||
showTileInfo(data, lastTileQuery);
|
showTileInfo(data, lastTileQuery);
|
||||||
@ -627,12 +651,22 @@ function hideTileView() {
|
|||||||
if (tileSel) { map.removeLayer(tileSel); tileSel = null; }
|
if (tileSel) { map.removeLayer(tileSel); tileSel = null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Referme la fiche détaillée et rétablit l'onglet d'origine SANS passer par
|
||||||
|
// openTab : celui-ci retouche la hauteur du volet quand l'onglet visé est déjà
|
||||||
|
// l'onglet actif (règle « re-toucher l'onglet actif ») — ce qui n'a pas lieu
|
||||||
|
// d'être ici, on rouvre simplement ce qui était affiché avant.
|
||||||
|
function restoreTabAfterDetails() {
|
||||||
|
hideTileView();
|
||||||
|
PANEL.tab = PANEL.want = tileReturnTab || 'view';
|
||||||
|
panelSave();
|
||||||
|
panelRender();
|
||||||
|
}
|
||||||
|
|
||||||
// Ferme la bulle, sinon la fiche ; renvoie true si quelque chose a été fermé.
|
// Ferme la bulle, sinon la fiche ; renvoie true si quelque chose a été fermé.
|
||||||
function closeTileViews() {
|
function closeTileViews() {
|
||||||
if (tilePopup) { tilePopup.remove(); tilePopup = null; return true; }
|
if (tilePopup) { tilePopup.remove(); tilePopup = null; return true; }
|
||||||
if (!el('tileView').hidden) {
|
if (!el('tileView').hidden) {
|
||||||
hideTileView();
|
restoreTabAfterDetails();
|
||||||
openTab(tileReturnTab || 'view');
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -1039,12 +1073,18 @@ function printSave() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function printLoad() {
|
function printLoad() {
|
||||||
const s = lsGet(PRINT_LS, {});
|
const raw = lsGet(PRINT_LS, {});
|
||||||
|
// Valeur corrompue (clé partagée avec une autre appli, quota dépassé
|
||||||
|
// pendant l'écriture…) : objet non conforme ignoré plutôt que planté.
|
||||||
|
const s = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
||||||
for (const [k, id] of [['paper', 'printPaper'], ['orient', 'printOrient'],
|
for (const [k, id] of [['paper', 'printPaper'], ['orient', 'printOrient'],
|
||||||
['scale', 'printScale'], ['title', 'printTitle']]) {
|
['scale', 'printScale'], ['title', 'printTitle']]) {
|
||||||
if (s[k] != null) el(id).value = s[k];
|
if (typeof s[k] === 'string') el(id).value = s[k];
|
||||||
|
}
|
||||||
|
if (Array.isArray(s.center) && s.center.length === 2 &&
|
||||||
|
s.center.every((n) => typeof n === 'number' && Number.isFinite(n))) {
|
||||||
|
PRINT.center = L.latLng(s.center);
|
||||||
}
|
}
|
||||||
if (Array.isArray(s.center) && s.center.length === 2) PRINT.center = L.latLng(s.center);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recadre sur le cadre en laissant libre la place du panneau (colonne à
|
// Recadre sur le cadre en laissant libre la place du panneau (colonne à
|
||||||
@ -1122,13 +1162,16 @@ function printCenterHere() {
|
|||||||
|
|
||||||
// Cadre affiché tant que l'onglet Export PDF est visible (événement panel:tab,
|
// Cadre affiché tant que l'onglet Export PDF est visible (événement panel:tab,
|
||||||
// émis à chaque rendu du panneau : on ne fait rien si l'état est inchangé).
|
// émis à chaque rendu du panneau : on ne fait rien si l'état est inchangé).
|
||||||
function printSetOpen(on) {
|
// fit : recadrer la vue sur le cadre une fois posé — faux au tout premier
|
||||||
|
// rendu (onglet Export mémorisé) pour ne pas écraser le zoom d'un lien
|
||||||
|
// partagé ou de l'URL au chargement.
|
||||||
|
function printSetOpen(on, fit) {
|
||||||
if (PRINT.on === on) return;
|
if (PRINT.on === on) return;
|
||||||
PRINT.on = on;
|
PRINT.on = on;
|
||||||
if (on) {
|
if (on) {
|
||||||
// cadre gardé s'il est encore dans la vue, sinon posé au centre
|
// cadre gardé s'il est encore dans la vue, sinon posé au centre
|
||||||
if (!PRINT.center || !map.getBounds().contains(PRINT.center)) PRINT.center = map.getCenter();
|
if (!PRINT.center || !map.getBounds().contains(PRINT.center)) PRINT.center = map.getCenter();
|
||||||
printRefreshFrame(true);
|
printRefreshFrame(fit !== false);
|
||||||
} else {
|
} else {
|
||||||
if (PRINT.rect) PRINT.rect.remove();
|
if (PRINT.rect) PRINT.rect.remove();
|
||||||
if (PRINT.handle) PRINT.handle.remove();
|
if (PRINT.handle) PRINT.handle.remove();
|
||||||
@ -1174,13 +1217,25 @@ el('printTitle').addEventListener('change', printSave);
|
|||||||
printLoad();
|
printLoad();
|
||||||
|
|
||||||
document.addEventListener('panel:tab', (e) => {
|
document.addEventListener('panel:tab', (e) => {
|
||||||
printSetOpen(e.detail.visible === 'print');
|
printSetOpen(e.detail.visible === 'print', !BOOTSTRAPPING);
|
||||||
if (e.detail.visible !== 'gen' && genDrawing) genSetDrawing(false);
|
if (e.detail.visible !== 'gen' && genDrawing) genSetDrawing(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- panneau à onglets --------------------------------------------------------
|
// --- panneau à onglets --------------------------------------------------------
|
||||||
const PANEL_LS = 'lidar-panel';
|
const PANEL_LS = 'lidar-panel';
|
||||||
const PANEL = Object.assign({ tab: 'view', collapsed: false, sheet: 'half' }, lsGet(PANEL_LS, {}));
|
// Ne retient que les clés connues, du bon type et (pour sheet) dans la liste
|
||||||
|
// valide : une valeur corrompue casserait sinon le CSS (panel.dataset.sheet
|
||||||
|
// inconnu de map.css) ou l'onglet affiché.
|
||||||
|
function _sanitizedPanelLS() {
|
||||||
|
const raw = lsGet(PANEL_LS, {});
|
||||||
|
const s = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
||||||
|
const out = {};
|
||||||
|
if (typeof s.tab === 'string') out.tab = s.tab;
|
||||||
|
if (typeof s.collapsed === 'boolean') out.collapsed = s.collapsed;
|
||||||
|
if (['closed', 'half', 'full'].includes(s.sheet)) out.sheet = s.sheet;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
const PANEL = Object.assign({ tab: 'view', collapsed: false, sheet: 'half' }, _sanitizedPanelLS());
|
||||||
// want = onglet choisi (mémorisé) ; tab = onglet affiché. Génération encore
|
// want = onglet choisi (mémorisé) ; tab = onglet affiché. Génération encore
|
||||||
// inconnue au démarrage (api/status) : Affichage en attendant, puis l'onglet
|
// inconnue au démarrage (api/status) : Affichage en attendant, puis l'onglet
|
||||||
// choisi revient dès qu'il est proposé.
|
// choisi revient dès qu'il est proposé.
|
||||||
@ -1314,6 +1369,7 @@ fetch('api/map/meta').then(r => r.json()).then(meta => {
|
|||||||
renderPanel();
|
renderPanel();
|
||||||
renderBaseRow();
|
renderBaseRow();
|
||||||
panelRender();
|
panelRender();
|
||||||
|
BOOTSTRAPPING = false;
|
||||||
renderUseCard();
|
renderUseCard();
|
||||||
genFillViz();
|
genFillViz();
|
||||||
// Droits de génération : boutons affichés si le serveur peut générer
|
// Droits de génération : boutons affichés si le serveur peut générer
|
||||||
@ -1449,7 +1505,7 @@ el('gps').addEventListener('click', () => {
|
|||||||
map.setView(ll, Math.max(map.getZoom(), 16));
|
map.setView(ll, Math.max(map.getZoom(), 16));
|
||||||
if (window._gpsMarker) window._gpsMarker.remove();
|
if (window._gpsMarker) window._gpsMarker.remove();
|
||||||
window._gpsMarker = L.circleMarker(ll, {
|
window._gpsMarker = L.circleMarker(ll, {
|
||||||
radius: 6, color: '#39d98a', fillColor: '#39d98a', fillOpacity: 0.9
|
radius: 6, color: cssVar('--gps'), fillColor: cssVar('--gps'), fillOpacity: 0.9
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
btn.classList.remove('busy');
|
btn.classList.remove('busy');
|
||||||
|
|||||||
Reference in New Issue
Block a user