Remplacer la fiche de dalle flottante par une bulle et une fiche détaillée dans le panneau

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Antoine Jacquin
2026-09-27 18:17:18 +02:00
parent a9196d0c97
commit 45455aec93
5 changed files with 132 additions and 40 deletions

View File

@ -1071,6 +1071,7 @@ def map_tile_info(lat: float, lng: float):
IGN se charge à part (/api/map/ign). IGN se charge à part (/api/map/ign).
""" """
from .index import _collect_tile_metadata, _res_suffix_str, scan_tiles from .index import _collect_tile_metadata, _res_suffix_str, scan_tiles
from .quality import read_quality
x, y = tiles_mod.wgs84_to_l93(lng, lat) x, y = tiles_mod.wgs84_to_l93(lng, lat)
col, row = int(x // 1000), int(y // 1000) + 1 col, row = int(x // 1000), int(y // 1000) + 1
x0, y0, x1, y1 = col * 1000, (row - 1) * 1000, (col + 1) * 1000, row * 1000 x0, y0, x1, y1 = col * 1000, (row - 1) * 1000, (col + 1) * 1000, row * 1000
@ -1084,6 +1085,10 @@ 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):

View File

@ -246,6 +246,28 @@ def test_map_tile_info(tmp_path, monkeypatch):
assert mapserve.map_tile_info(lat=45.0, lng=1.0)["found"] is False assert mapserve.map_tile_info(lat=45.0, lng=1.0)["found"] is False
def test_map_tile_includes_quality(tmp_path, monkeypatch):
"""/api/map/tile joint la qualité (densité, dates d'acquisition) si connue."""
mapserve = _setup(tmp_path, monkeypatch, layers=("relief_oriente",))
from lidar_pipeline.quality import write_quality, QUALITY_VERSION
name = "LHD_FXX_1054_6882_PTS_LAMB93_IGN69"
write_quality(tmp_path, name, {"version": QUALITY_VERSION, "ground_density": 12.5,
"density_grid": [[12.5]], "empty_fraction": 0.1,
"acq_start": "2021-03-02", "acq_end": "2021-03-02",
"acq_source": "gps"})
lat, lon = _center()
d = mapserve.map_tile_info(lat=lat, lng=lon)
assert d["quality"] == {"ground_density": 12.5, "acq_start": "2021-03-02",
"acq_end": "2021-03-02"}
def test_map_tile_quality_absent_is_none(tmp_path, monkeypatch):
"""Sans sidecar qualité, la clé vaut None (pas d'erreur)."""
mapserve = _setup(tmp_path, monkeypatch, layers=("relief_oriente",))
lat, lon = _center()
assert mapserve.map_tile_info(lat=lat, lng=lon)["quality"] is None
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
@ -302,6 +324,20 @@ def test_map_ui_tile_load_indicator_and_selection_pane():
assert "pane: 'tileSel'" in _MAP_JS assert "pane: 'tileSel'" in _MAP_JS
def test_ui_tile_popup_and_details_view():
"""La fiche de dalle est une bulle Leaflet + une vue détaillée dans le panneau."""
from lidar_pipeline.mapui import _MAP_CSS, _MAP_HTML, _MAP_JS
assert 'id="tilecard"' not in _MAP_HTML
for id_ in ("tileView", "tileBack", "tileTitle", "tileBody"):
assert f'id="{id_}"' in _MAP_HTML, id_
for fn in ("function showTilePopup(", "function openTileDetails(", "function closeTileViews("):
assert fn in _MAP_JS, fn
assert "L.popup(" in _MAP_JS and "Détails" in _MAP_JS
# Pas de bulle pendant le déplacement du cadre d'export ni un tracé
click = _MAP_JS[_MAP_JS.index("map.on('click'"):]
assert "genDrawing" in click[:400] and "PRINT.dragging" in click[:400]
def test_defaults_roundtrip(tmp_path, monkeypatch): def test_defaults_roundtrip(tmp_path, monkeypatch):
"""L'affichage réglé depuis la carte devient la configuration servie à tous.""" """L'affichage réglé depuis la carte devient la configuration servie à tous."""
import lidar_pipeline.mapserve as mapserve import lidar_pipeline.mapserve as mapserve

View File

@ -58,13 +58,6 @@ html, body { height: 100%; overflow: hidden; background: var(--bg); color: var(-
−30 % de CPU (moteur de rendu + GPU) en déplacement, mesuré sous Chrome. */ −30 % de CPU (moteur de rendu + GPU) en déplacement, mesuré sous Chrome. */
.leaflet-tile-pane .leaflet-layer.base-dark { filter: brightness(0.38) contrast(1.1) saturate(0.7); } .leaflet-tile-pane .leaflet-layer.base-dark { filter: brightness(0.38) contrast(1.1) saturate(0.7); }
.card { position: fixed; z-index: var(--z-panel); background: var(--surface); color: var(--text);
border: 1px solid var(--border); border-radius: var(--r-md);
box-shadow: var(--shadow-lg);
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }
/* Fiche de dalle : à droite du panneau (ordinateur), en haut (téléphone). */
#tilecard { left: max(var(--sp-3), var(--panel-w)); top: 12px; width: 340px; max-width: calc(100vw - 24px);
max-height: calc(100vh - 24px); overflow-y: auto; }
.info-sec { margin-top: 10px; } .info-sec { margin-top: 10px; }
.info-sec h4 { margin: 0 0 4px; font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.06em; .info-sec h4 { margin: 0 0 4px; font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.06em;
text-transform: uppercase; color: var(--muted); } text-transform: uppercase; color: var(--muted); }
@ -76,10 +69,14 @@ html, body { height: 100%; overflow: hidden; background: var(--bg); color: var(-
.info-dl:hover { background: color-mix(in srgb, var(--accent) 30%, transparent); } .info-dl:hover { background: color-mix(in srgb, var(--accent) 30%, transparent); }
.rose-cap { font-size: var(--fs-xs); color: var(--text); line-height: 1.3; } .rose-cap { font-size: var(--fs-xs); color: var(--text); line-height: 1.3; }
.rose-cap span { color: var(--muted); font-size: var(--fs-xs); } .rose-cap span { color: var(--muted); font-size: var(--fs-xs); }
.card-head { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-bottom: 1px solid var(--border); } .tile-head .sec-title { margin: 0; flex: 1; }
.card-title { font-size: var(--fs-sm); font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); } .leaflet-popup-content-wrapper, .leaflet-popup-tip { background: var(--surface); color: var(--text);
.card-body { padding: 6px 8px 8px; max-height: min(60vh, 520px); overflow-y: auto; } box-shadow: var(--shadow-lg); }
.card-foot { display: flex; gap: 8px; padding: 8px 4px 2px; margin-top: 6px; border-top: 1px solid var(--border); } .leaflet-popup-content-wrapper { border-radius: var(--r-md); border: 1px solid var(--border); }
.leaflet-popup-content { margin: var(--sp-2) var(--sp-3); font-size: var(--fs-sm); line-height: 1.45; }
.leaflet-container a.leaflet-popup-close-button { color: var(--muted); }
.tile-pop b { font-size: var(--fs-md); }
.tile-pop .row { margin-top: var(--sp-2); }
/* --- composants communs --------------------------------------------------- */ /* --- composants communs --------------------------------------------------- */
.btn, button { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-1); .btn, button { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-1);
@ -205,7 +202,6 @@ body.drawing .leaflet-container { cursor: crosshair !important; }
background: rgba(0,0,0,0.35); border: 1px solid var(--border); border-radius: var(--r-sm); background: rgba(0,0,0,0.35); 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.accent { color: var(--accent); border-color: var(--accent); }
#tools button.busy { color: var(--accent); border-color: var(--accent); } #tools button.busy { color: var(--accent); border-color: var(--accent); }
/* Cadres des dalles du run : orange pulsant = rendu en cours, bleu = en /* Cadres des dalles du run : orange pulsant = rendu en cours, bleu = en
attente, rouge = échec. Les dalles faites n'ont pas de cadre. */ attente, rouge = échec. Les dalles faites n'ont pas de cadre. */
@ -254,6 +250,7 @@ body.panel-collapsed { --panel-w: 84px; }
#panel[data-sheet="closed"] { height: 72px; } #panel[data-sheet="closed"] { height: 72px; }
#panel[data-sheet="half"] { height: 45vh; } #panel[data-sheet="half"] { height: 45vh; }
#panel[data-sheet="full"] { height: calc(100vh - var(--sp-4)); } #panel[data-sheet="full"] { height: calc(100vh - var(--sp-4)); }
#panel[data-sheet="full"] { height: calc(100dvh - var(--sp-4)); }
#panel[data-sheet="closed"] .panel-body { display: none; } #panel[data-sheet="closed"] .panel-body { display: none; }
#panelCollapse { display: none; } #panelCollapse { display: none; }
.sheet-grip { display: flex; justify-content: center; padding: var(--sp-2) 0 0; cursor: grab; touch-action: none; } .sheet-grip { display: flex; justify-content: center; padding: var(--sp-2) 0 0; cursor: grab; touch-action: none; }

View File

@ -74,7 +74,7 @@
<div class="row"><button id="printHere" class="btn" <div class="row"><button id="printHere" class="btn"
title="Placer le cadre au centre de la vue">⌖ Centrer ici</button> title="Placer le cadre au centre de la vue">⌖ Centrer ici</button>
<button id="printFit" class="btn" title="Zoomer sur le cadre">⤢ Voir le cadre</button></div> <button id="printFit" class="btn" title="Zoomer sur le cadre">⤢ Voir le cadre</button></div>
<div class="card-foot"><button id="printGo" class="btn btn-primary">Exporter le PDF</button> <div class="row"><button id="printGo" class="btn btn-primary">Exporter le PDF</button>
<span id="printMsg" class="print-msg"></span></div> <span id="printMsg" class="print-msg"></span></div>
</section> </section>
<section id="tab-gen" role="tabpanel" hidden> <section id="tab-gen" role="tabpanel" hidden>
@ -88,14 +88,14 @@
<label class="chk"><input type="checkbox" id="genRegen"> Régénérer les tuiles existantes</label> <label class="chk"><input type="checkbox" id="genRegen"> Régénérer les tuiles existantes</label>
<div class="meta">Sol : classification IGN · bords raccordés aux dalles voisines (bande 100 m)</div> <div class="meta">Sol : classification IGN · bords raccordés aux dalles voisines (bande 100 m)</div>
</div> </div>
<div class="card-foot" id="genActions"> <div class="row" id="genActions">
<button id="genGo" class="btn btn-primary" title="Télécharge (géoplateforme IGN) puis traite les dalles sélectionnées">Générer</button> <button id="genGo" class="btn btn-primary" title="Télécharge (géoplateforme IGN) puis traite les dalles sélectionnées">Générer</button>
<button id="genClear" title="Vider la sélection de dalles">Effacer la sélection</button> <button id="genClear" title="Vider la sélection de dalles">Effacer la sélection</button>
</div> </div>
<div id="genRun" hidden> <div id="genRun" hidden>
<div class="meta" id="genProgress"></div> <div class="meta" id="genProgress"></div>
<pre id="genLog" class="gen-log"></pre> <pre id="genLog" class="gen-log"></pre>
<div class="card-foot"> <div class="row">
<button id="genStop" title="SIGTERM au pipeline (nettoyage propre), SIGKILL en repli">Arrêter</button> <button id="genStop" title="SIGTERM au pipeline (nettoyage propre), SIGKILL en repli">Arrêter</button>
<button id="genQueueClear" title="Retirer les demandes en attente (le run en cours continue)">Vider la file</button> <button id="genQueueClear" title="Retirer les demandes en attente (le run en cours continue)">Vider la file</button>
</div> </div>
@ -109,17 +109,14 @@
<div class="row"><button id="btnDefault" class="btn" title="Figer l'affichage courant (couche principale, mode, opacité de la précision, fond) comme configuration par défaut de tous les navigateurs">★ Définir par défaut</button> <div class="row"><button id="btnDefault" class="btn" title="Figer l'affichage courant (couche principale, mode, opacité de la précision, fond) comme configuration par défaut de tous les navigateurs">★ Définir par défaut</button>
<button id="btnReset" class="btn" title="Revenir à la configuration par défaut">↺ Réinitialiser</button></div> <button id="btnReset" class="btn" title="Revenir à la configuration par défaut">↺ Réinitialiser</button></div>
</section> </section>
<section id="tileView" hidden>
<div class="row tile-head"><button id="tileBack" class="btn btn-ghost">← Retour</button>
<span class="sec-title" id="tileTitle">Dalle</span></div>
<div id="tileBody"></div>
</section>
</div> </div>
</aside> </aside>
<div id="tilecard" class="card" hidden>
<div class="card-head">
<span class="card-title" id="tileTitle">Dalle</span>
<button id="tileClose" class="btn-icon" title="Fermer">×</button>
</div>
<div class="card-body" id="tileBody"></div>
</div>
<nav id="tools"> <nav id="tools">
<button id="zoomIn" title="Zoomer">+</button> <button id="zoomIn" title="Zoomer">+</button>
<button id="zoomOut" title="Dézoomer">−</button> <button id="zoomOut" title="Dézoomer">−</button>

View File

@ -467,6 +467,17 @@ function fmtDate(v, withTime) {
return d.toLocaleString('fr-FR', opt); return d.toLocaleString('fr-FR', opt);
} }
function fmtDensity(q) {
if (!q || q.ground_density == null) return 'non renseignée';
return q.ground_density.toFixed(q.ground_density < 10 ? 1 : 0).replace('.', ',') + ' pts sol/m²';
}
function fmtAcq(q) {
if (!q || !q.acq_start) return 'non renseignée';
return q.acq_end && q.acq_end.slice(0, 10) !== q.acq_start.slice(0, 10)
? fmtDate(q.acq_start) + ' → ' + fmtDate(q.acq_end) : fmtDate(q.acq_start);
}
function kv(rows) { function kv(rows) {
return '<dl class="info-kv">' + rows.filter(r => r[1] != null && r[1] !== '') return '<dl class="info-kv">' + rows.filter(r => r[1] != null && r[1] !== '')
.map(r => '<dt>' + r[0] + '</dt><dd>' + r[1] + '</dd>').join('') + '</dl>'; .map(r => '<dt>' + r[0] + '</dt><dd>' + r[1] + '</dd>').join('') + '</dl>';
@ -523,7 +534,7 @@ function calageSection(c) {
function renderTileCard(data, ign) { function renderTileCard(data, ign) {
const regenBtn = (GEN.available && GEN.allowed) const regenBtn = (GEN.available && GEN.allowed)
? '<div class="card-foot"><button id="tileRegen" class="btn" title="Ajouter cette dalle à la sélection de génération (téléchargement IGN + traitement)">↻ ' + (data.found ? 'Régénérer' : 'Générer') + ' cette dalle</button></div>' ? '<div class="row"><button id="tileRegen" class="btn" title="Ajouter cette dalle à la sélection de génération (téléchargement IGN + traitement)">↻ ' + (data.found ? 'Régénérer' : 'Générer') + ' cette dalle</button></div>'
: ''; : '';
el('tileTitle').textContent = 'Dalle ' + data.col + ' · ' + data.row; el('tileTitle').textContent = 'Dalle ' + data.col + ' · ' + data.row;
const bb = data.l93_bbox || []; const bb = data.l93_bbox || [];
@ -551,7 +562,6 @@ function renderTileCard(data, ign) {
el('tileBody').innerHTML = html + regenBtn; el('tileBody').innerHTML = html + regenBtn;
const btn = el('tileRegen'); const btn = el('tileRegen');
if (btn) btn.addEventListener('click', genAddCellFromQuery); if (btn) btn.addEventListener('click', genAddCellFromQuery);
el('tilecard').hidden = false;
} }
function showTileInfo(data, latlng) { function showTileInfo(data, latlng) {
@ -559,10 +569,8 @@ function showTileInfo(data, latlng) {
if (!data) { if (!data) {
el('tileTitle').textContent = 'Dalle'; el('tileTitle').textContent = 'Dalle';
el('tileBody').innerHTML = '<div class="meta">Serveur injoignable : réessayez dans un instant.</div>'; el('tileBody').innerHTML = '<div class="meta">Serveur injoignable : réessayez dans un instant.</div>';
el('tilecard').hidden = false;
return; return;
} }
selectTileOutline(data);
renderTileCard(data, null); renderTileCard(data, null);
// Fiche IGN à part : la sélection s'affiche sans attendre le catalogue. // Fiche IGN à part : la sélection s'affiche sans attendre le catalogue.
const key = data.col + '_' + data.row; const key = data.col + '_' + data.row;
@ -573,6 +581,54 @@ function showTileInfo(data, latlng) {
.then(ign => { if (showTileInfo.current === key) renderTileCard(data, ign); }); .then(ign => { if (showTileInfo.current === key) renderTileCard(data, ign); });
} }
let tilePopup = null;
let tileReturnTab = null; // onglet à rouvrir après « ← Retour »
function showTilePopup(data, latlng) {
lastTileQuery = latlng || lastTileQuery;
if (tilePopup) tilePopup.remove();
const html = !data
? '<div class="tile-pop">Serveur injoignable : réessayez dans un instant.</div>'
: '<div class="tile-pop"><b>Dalle ' + data.col + ' · ' + data.row + '</b><br>'
+ (data.found ? '' : 'Pas encore de rendu<br>')
+ 'Acquisition : ' + fmtAcq(data.quality) + '<br>'
+ 'Densité : ' + fmtDensity(data.quality)
+ '<div class="row"><button class="btn" id="tilePopDetails">Détails</button></div></div>';
tilePopup = L.popup({ maxWidth: 260, autoPanPaddingTopLeft: [isPhone() ? 12 : el('panel').getBoundingClientRect().right + 12, 12] })
.setLatLng(latlng).setContent(html).openOn(map);
if (!data) return;
selectTileOutline(data);
showTilePopup.data = data;
const btn = el('tilePopDetails');
if (btn) btn.addEventListener('click', openTileDetails);
tilePopup.on('remove', () => { if (el('tileView').hidden && tileSel) { map.removeLayer(tileSel); tileSel = null; } });
}
function openTileDetails() {
const data = showTilePopup.data;
if (!data) return;
if (tilePopup) { tilePopup.off('remove'); tilePopup.remove(); tilePopup = null; }
tileReturnTab = PANEL.tab;
for (const t of ['view', 'print', 'gen', 'share']) el('tab-' + t).hidden = true;
el('tileView').hidden = false;
if (isPhone() && PANEL.sheet === 'closed') setSheet('half');
if (!isPhone() && PANEL.collapsed) setCollapsed(false);
showTileInfo(data, lastTileQuery);
}
// Ferme la bulle, sinon la fiche ; renvoie true si quelque chose a été fermé.
function closeTileViews() {
if (tilePopup) { tilePopup.remove(); tilePopup = null; return true; }
if (!el('tileView').hidden) {
el('tileView').hidden = true;
if (tileSel) { map.removeLayer(tileSel); tileSel = null; }
openTab(tileReturnTab || 'view');
return true;
}
return false;
}
el('tileBack').addEventListener('click', closeTileViews);
// --- rose des vents du relief orienté -------------------------------------- // --- rose des vents du relief orienté --------------------------------------
// Mêmes couleurs que visualizations._relief_lut : teinte CIELAB = orientation // Mêmes couleurs que visualizations._relief_lut : teinte CIELAB = orientation
// (aspect = orientation boussole + 90°, repère arctan2(dy, dx) du rendu), // (aspect = orientation boussole + 90°, repère arctan2(dy, dx) du rendu),
@ -761,8 +817,8 @@ function genAddCellFromQuery() {
genApi('GET', 'api/cell?lat=' + lastTileQuery.lat + '&lng=' + lastTileQuery.lng) genApi('GET', 'api/cell?lat=' + lastTileQuery.lat + '&lng=' + lastTileQuery.lng)
.then(d => { .then(d => {
genAddCells([{ col: d.col, row: d.row, corners: d.corners }]); genAddCells([{ col: d.col, row: d.row, corners: d.corners }]);
closeTileViews();
genOpenCard('1 dalle sélectionnée — options puis « Générer ».'); genOpenCard('1 dalle sélectionnée — options puis « Générer ».');
el('tilecard').hidden = true;
}) })
.catch(msg => toast('Échec : ' + msg.message, 6000)); .catch(msg => toast('Échec : ' + msg.message, 6000));
} }
@ -949,7 +1005,8 @@ el('genQueueClear').addEventListener('click', () =>
// se déplace librement autour, on le fait glisser à la souris ou au doigt. // se déplace librement autour, on le fait glisser à la souris ou au doigt.
// Géométrie exacte calculée par le serveur (projection Lambert 93). // Géométrie exacte calculée par le serveur (projection Lambert 93).
const PRINT_LS = 'lidar-print'; const PRINT_LS = 'lidar-print';
const PRINT = { on: false, rect: null, handle: null, seq: 0, center: null, corners: null, drag: null }; const PRINT = { on: false, rect: null, handle: null, seq: 0, center: null, corners: null, drag: null,
dragging: false };
map.createPane('printFrame').style.zIndex = '690'; map.createPane('printFrame').style.zIndex = '690';
function printSettings() { function printSettings() {
@ -1026,6 +1083,7 @@ function printRefreshFrame(fit) {
// stables pendant le défilement automatique), // stables pendant le défilement automatique),
// géométrie exacte redemandée au serveur au relâchement. // géométrie exacte redemandée au serveur au relâchement.
function printDragStart() { function printDragStart() {
PRINT.dragging = true;
PRINT.drag = { start: map.latLngToLayerPoint(PRINT.handle.getLatLng()), PRINT.drag = { start: map.latLngToLayerPoint(PRINT.handle.getLatLng()),
corners: PRINT.corners.map(c => map.latLngToLayerPoint(c)) }; corners: PRINT.corners.map(c => map.latLngToLayerPoint(c)) };
} }
@ -1043,6 +1101,7 @@ function printDragEnd() {
PRINT.corners = PRINT.rect.getLatLngs()[0]; PRINT.corners = PRINT.rect.getLatLngs()[0];
printSave(); printSave();
printRefreshFrame(); printRefreshFrame();
setTimeout(() => { PRINT.dragging = false; }, 300);
} }
function printCenterHere() { function printCenterHere() {
@ -1140,7 +1199,9 @@ function panelRender() {
b.hidden = !tabs.includes(b.dataset.tab); b.hidden = !tabs.includes(b.dataset.tab);
b.setAttribute('aria-selected', String(b.dataset.tab === PANEL.tab)); b.setAttribute('aria-selected', String(b.dataset.tab === PANEL.tab));
}); });
for (const t of ['view', 'print', 'gen', 'share']) el('tab-' + t).hidden = t !== PANEL.tab; if (el('tileView').hidden) {
for (const t of ['view', 'print', 'gen', 'share']) el('tab-' + t).hidden = t !== PANEL.tab;
}
const panel = el('panel'); const panel = el('panel');
// Repli en bande d'icônes : ordinateur seulement (le téléphone a son volet). // Repli en bande d'icônes : ordinateur seulement (le téléphone a son volet).
const collapsed = PANEL.collapsed && !isPhone(); const collapsed = PANEL.collapsed && !isPhone();
@ -1157,6 +1218,7 @@ function panelRender() {
function openTab(tab) { function openTab(tab) {
if (!panelTabs().includes(tab)) return; if (!panelTabs().includes(tab)) return;
el('tileView').hidden = true;
const before = PANEL.sheet; const before = PANEL.sheet;
// Onglet actif retouché sur téléphone : le volet change de hauteur. // Onglet actif retouché sur téléphone : le volet change de hauteur.
if (isPhone() && tab === PANEL.tab && PANEL.sheet !== 'closed') { if (isPhone() && tab === PANEL.tab && PANEL.sheet !== 'closed') {
@ -1216,9 +1278,6 @@ window.addEventListener('resize', panelRender); // hauteur du volet en vh (rot
grip.addEventListener('pointercancel', end); grip.addEventListener('pointercancel', end);
})(); })();
// Fiche de dalle fermée par Échap (provisoire : la Task 4 la remplace).
function closeTileViews() { if (el('tilecard').hidden) return false; el('tilecard').hidden = true; return true; }
// --- démarrage ------------------------------------------------------------ // --- démarrage ------------------------------------------------------------
function fitData() { function fitData() {
if (META.bounds) { if (META.bounds) {
@ -1258,7 +1317,7 @@ fetch('api/map/meta').then(r => r.json()).then(meta => {
panelRender(); panelRender();
if (show) { genUpdateStatus(d); genPollNow(); } if (show) { genUpdateStatus(d); genPollNow(); }
}).catch(() => {}); }).catch(() => {});
}).catch(() => toast('Serveur de tuiles injoignable.', 10000)); }).catch(() => { toast('Serveur de tuiles injoignable.', 10000); panelRender(); });
let applyingHash = false; let applyingHash = false;
window.addEventListener('hashchange', () => { window.addEventListener('hashchange', () => {
@ -1278,17 +1337,15 @@ window.addEventListener('hashchange', () => {
map.on('moveend zoomend', saveHashSoon); map.on('moveend zoomend', saveHashSoon);
map.on('click', (e) => { map.on('click', (e) => {
if (genDrawing || Date.now() < genIgnoreClickUntil) return; // Pas de bulle pendant un tracé de zone ni juste après le relâchement de
// la poignée du cadre d'export.
if (genDrawing || Date.now() < genIgnoreClickUntil || PRINT.dragging) return;
fetch('api/map/tile?lat=' + e.latlng.lat + '&lng=' + e.latlng.lng) fetch('api/map/tile?lat=' + e.latlng.lat + '&lng=' + e.latlng.lng)
.then(r => r.ok ? r.json() : null) .then(r => r.ok ? r.json() : null)
.catch(() => null) .catch(() => null)
.then(d => showTileInfo(d, e.latlng)); .then(d => showTilePopup(d, e.latlng));
}); });
el('tileClose').addEventListener('click', () => {
el('tilecard').hidden = true;
if (tileSel) { map.removeLayer(tileSel); tileSel = null; }
});
el('btnShare').addEventListener('click', () => { el('btnShare').addEventListener('click', () => {
const url = location.origin + location.pathname + viewHash(true); const url = location.origin + location.pathname + viewHash(true);
history.replaceState(null, '', url); history.replaceState(null, '', url);