diff --git a/lidar_pipeline/tests/test_mapserve.py b/lidar_pipeline/tests/test_mapserve.py index 2a38b89..be2209c 100644 --- a/lidar_pipeline/tests/test_mapserve.py +++ b/lidar_pipeline/tests/test_mapserve.py @@ -522,21 +522,45 @@ def test_ui_phone_return_and_escape_keep_sheet_height(): 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.""" + BOOTSTRAPPING passe à faux juste après CE premier rendu — succès ET échec + du chargement de /api/map/meta (sinon un serveur injoignable au démarrage + bloque BOOTSTRAPPING à vrai pour toujours : plus aucun recadrage, même + après une ouverture explicite ultérieure de l'onglet).""" 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;") + assert "printSetOpen(e.detail.visible === 'print', !BOOTSTRAPPING && !RESTORING_TAB);" in _MAP_JS + then_branch = _MAP_JS[_MAP_JS.index("panelRender();\n BOOTSTRAPPING = false;"):] + assert then_branch.startswith("panelRender();\n BOOTSTRAPPING = false;") + catch_branch = _MAP_JS[_MAP_JS.index(".catch(() => {\n toast('Serveur de tuiles injoignable."):] + assert "BOOTSTRAPPING = false;" in catch_branch[:400] 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).""" + derrière la fiche) — et le reste tant que la fiche est ouverte, quel que + soit ce qui redéclenche un rendu du panneau (grip, redimensionnement, + changement phoneQuery…) puisque panelVisibleTab() renvoie null tant que + #tileView n'est pas masqué.""" from lidar_pipeline.mapui import _MAP_JS open_details = _MAP_JS[_MAP_JS.index("function openTileDetails("):] assert "printSetOpen(false);" in open_details[:600] + visible_tab = _MAP_JS[_MAP_JS.index("function panelVisibleTab("):] + assert "if (!el('tileView').hidden) return null;" in visible_tab[:300] + + +def test_ui_restoring_tab_does_not_refit_or_recentre_print_frame(): + """Rétablir l'onglet Export (retour de la fiche détaillée) rouvre le + cadre là où il était, sans recadrer la vue ni le recentrer sur l'écran — + au contraire d'une ouverture explicite de l'onglet par l'utilisateur.""" + from lidar_pipeline.mapui import _MAP_JS + assert "let RESTORING_TAB = false;" in _MAP_JS + restore = _MAP_JS[_MAP_JS.index("function restoreTabAfterDetails("):] + assert "RESTORING_TAB = true;" in restore[:400] + assert "panelRender();\n RESTORING_TAB = false;" in restore[:400] + open_fn = _MAP_JS[_MAP_JS.index("function printSetOpen("):] + assert "explicit && !map.getBounds().contains(PRINT.center)" in open_fn[:700] + assert "printRefreshFrame(!!explicit);" in open_fn[:700] def test_ui_colors_are_tokenised(): diff --git a/lidar_pipeline/web/map.js b/lidar_pipeline/web/map.js index 3977237..f203b1a 100644 --- a/lidar_pipeline/web/map.js +++ b/lidar_pipeline/web/map.js @@ -46,8 +46,15 @@ let META = 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). +// (cf. printSetOpen). Remis à faux dans le .then() ET le .catch() du chargement +// de /api/map/meta (sinon un serveur injoignable au démarrage le laisse +// bloqué à vrai pour toujours : l'onglet Export ne recadrerait plus jamais). let BOOTSTRAPPING = true; +// Vrai le temps d'un panelRender() qui RÉTABLIT un onglet déjà connu +// (restoreTabAfterDetails, retour de la fiche détaillée) plutôt que d'en +// ouvrir un nouveau à la demande de l'utilisateur : le cadre d'export ne doit +// alors ni recadrer la vue, ni recentrer le cadre lui-même (cf. printSetOpen). +let RESTORING_TAB = false; // --- thème ------------------------------------------------------------------ // auto = suit le système ; ☀/☾ force clair ou sombre ; 3ᵉ appui : auto. @@ -659,7 +666,12 @@ function restoreTabAfterDetails() { hideTileView(); PANEL.tab = PANEL.want = tileReturnTab || 'view'; panelSave(); + // Ce rendu RÉTABLIT l'onglet Export tel qu'il était (cadre à sa place, + // vue inchangée) : ni recadrage de la vue, ni recentrage du cadre — cf. + // printSetOpen, qui lit ce drapeau via le listener panel:tab. + RESTORING_TAB = true; panelRender(); + RESTORING_TAB = false; } // Ferme la bulle, sinon la fiche ; renvoie true si quelque chose a été fermé. @@ -1162,16 +1174,23 @@ function printCenterHere() { // 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é). -// 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) { +// explicit : vrai seulement pour une ouverture voulue par l'utilisateur +// (clic sur l'onglet, raccourci clavier) — faux pour un rendu qui RÉTABLIT +// un état déjà connu (démarrage avec onglet mémorisé, retour de la fiche +// détaillée) : dans ce cas ni recadrage de la vue sur le cadre, ni +// recentrage du cadre lui-même sur la vue (sinon un cadre posé ailleurs sur +// le terrain saute au centre de l'écran, ou le zoom d'un lien partagé est +// écrasé — cf. printRefreshFrame(fit)). +function printSetOpen(on, explicit) { if (PRINT.on === on) return; PRINT.on = on; if (on) { - // 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(); - printRefreshFrame(fit !== false); + // Cadre recentré sur la vue seulement s'il est hors champ ET que + // l'ouverture est explicite ; sinon gardé où il était (ou posé au centre + // s'il n'a jamais existé — premier passage sans lidar-print sauvegardé). + if (!PRINT.center) PRINT.center = map.getCenter(); + else if (explicit && !map.getBounds().contains(PRINT.center)) PRINT.center = map.getCenter(); + printRefreshFrame(!!explicit); } else { if (PRINT.rect) PRINT.rect.remove(); if (PRINT.handle) PRINT.handle.remove(); @@ -1217,7 +1236,7 @@ el('printTitle').addEventListener('change', printSave); printLoad(); document.addEventListener('panel:tab', (e) => { - printSetOpen(e.detail.visible === 'print', !BOOTSTRAPPING); + printSetOpen(e.detail.visible === 'print', !BOOTSTRAPPING && !RESTORING_TAB); if (e.detail.visible !== 'gen' && genDrawing) genSetDrawing(false); }); @@ -1249,8 +1268,14 @@ function panelTabs() { return ['view', 'print', 'gen', 'share'].filter(t => t !== 'gen' || (GEN.available && GEN.allowed)); } -// Onglet réellement visible, ou null (panneau replié, volet fermé). +// Onglet réellement visible, ou null (panneau replié, volet fermé, ou fiche +// détaillée affichée par-dessus — auquel cas AUCUN onglet n'est à l'écran, +// pas même celui mémorisé : panel:tab doit fermer le cadre d'export s'il +// était ouvert, quel que soit ce qui a déclenché ce rendu du panneau — grip +// glissée/touchée, redimensionnement, changement phoneQuery, le +// setTimeout(panelRender, 220) d'openTab…). function panelVisibleTab() { + if (!el('tileView').hidden) return null; if (isPhone() ? PANEL.sheet === 'closed' : PANEL.collapsed) return null; return PANEL.tab; } @@ -1383,7 +1408,14 @@ fetch('api/map/meta').then(r => r.json()).then(meta => { panelRender(); if (show) { genUpdateStatus(d); genPollNow(); } }).catch(() => {}); -}).catch(() => { toast('Serveur de tuiles injoignable.', 10000); panelRender(); }); +}).catch(() => { + toast('Serveur de tuiles injoignable.', 10000); + panelRender(); + // Ce rendu-là compte aussi comme le premier (échec du chargement initial) : + // sans ce reset, un onglet Export mémorisé ne recadrerait plus JAMAIS la + // vue, même après une ouverture explicite ultérieure. + BOOTSTRAPPING = false; +}); let applyingHash = false; window.addEventListener('hashchange', () => {