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:
@ -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):
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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; }
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user