Alléger la carte : rugosité retirée, vignettes 160 px, cap mémoire
Retire la couche Rugosité du panneau (DEFAULT_LAYERS restreint panneau, presets et sélecteur de génération). Réduit la mémoire du navigateur : vignettes de sous-tuiles à 160 px (régénérées sans ré-encoder les AVIF), pleine résolution plafonnée à 24 images les plus proches du centre, src des <img> libéré hors écran. Les changements de palier pré-décodent l'image avant échange (zoom et pan sans flash).
This commit is contained in:
@ -201,7 +201,7 @@ VIZ_LEGENDS = {
|
||||
# Couches activées par défaut à l'ouverture de la carte : seules celles-ci
|
||||
# sont allumées, toutes les autres sont désactivées (mais restent accessibles
|
||||
# via le panneau). Ordre = pile basse → haute.
|
||||
DEFAULT_LAYERS = ('slope', 'aspect', 'positive_openness', 'roughness')
|
||||
DEFAULT_LAYERS = ('slope', 'aspect', 'positive_openness')
|
||||
|
||||
# Couche « principale » (mise en avant ; index_tiles.json + affichage en
|
||||
# direct de la webapp).
|
||||
@ -273,6 +273,12 @@ _CARTO_SUBTILED_VIZ = ()
|
||||
_SUBTILE_AVIF_QUALITY = 55
|
||||
_SUBTILE_AVIF_SPEED = 8
|
||||
|
||||
# Vignette de sous-tuile (px) : à la vue d'ensemble chaque sous-tuile active
|
||||
# décode sa vignette en navigateur — 160 px couvre l'affichage jusqu'à ~220 px
|
||||
# à l'écran et réduit la mémoire décodée d'un facteur ~2,5 vs 256 px. La taille
|
||||
# est encodée dans le nom de fichier : la changer invalide le cache.
|
||||
_SUBTILE_THUMB_PX = 160
|
||||
|
||||
# Couches à contenu photographique ou traits fins (orthophoto, carte topo) :
|
||||
# q55, calé sur des rampes de couleur, bave textes et linéaires — qualité
|
||||
# relevée pour ces couches uniquement.
|
||||
@ -751,6 +757,18 @@ def _build_subtiles(tile, offered_viz_keys, output_dir, sub_dir_name):
|
||||
|
||||
try:
|
||||
resample = getattr(PILImage, 'LANCZOS', 1)
|
||||
thumb_suffix = f"_thumb{_SUBTILE_THUMB_PX}.webp"
|
||||
|
||||
def _save_thumb(quad, stem):
|
||||
"""Écrit la vignette de sous-tuile (redimensionnée à _SUBTILE_THUMB_PX)."""
|
||||
scale = min(1.0, _SUBTILE_THUMB_PX / max(quad.size))
|
||||
out = quad
|
||||
if scale < 1.0:
|
||||
out = quad.resize((max(1, int(quad.size[0] * scale)),
|
||||
max(1, int(quad.size[1] * scale))), resample)
|
||||
out.save(str(output_dir / sub_dir_name / (stem + thumb_suffix)),
|
||||
format='WEBP', quality=80)
|
||||
|
||||
for viz_key in offered_viz_keys:
|
||||
info = tile['viz'].get(viz_key)
|
||||
if not info:
|
||||
@ -765,12 +783,33 @@ def _build_subtiles(tile, offered_viz_keys, output_dir, sub_dir_name):
|
||||
# porte un suffixe ?v= d'invalidation : on l'ôte pour le chemin.
|
||||
src = output_dir / info['full'].split('?')[0]
|
||||
src_mtime = _mtime(src)
|
||||
all_exist = all(
|
||||
_cached_file_fresh(output_dir / sub_dir_name / (stem + '.avif'), src_mtime)
|
||||
and _cached_file_fresh(output_dir / sub_dir_name / (stem + '_thumb.webp'), src_mtime)
|
||||
and _cached_file_fresh(output_dir / sub_dir_name / (stem + '_mid.webp'), src_mtime)
|
||||
for stem in stems.values())
|
||||
if not all_exist:
|
||||
|
||||
def _fresh(name):
|
||||
return _cached_file_fresh(output_dir / sub_dir_name / name, src_mtime)
|
||||
|
||||
# AVIF + vignette intermédiaire d'un côté, vignettes de l'autre :
|
||||
# un changement de taille de vignette (dalle source inchangée)
|
||||
# re-découpe depuis les AVIF de sous-tuiles déjà présents, sans
|
||||
# ré-encoder ces derniers (des minutes de CPU par rebuild).
|
||||
heavy_fresh = all(_fresh(stem + '.avif') and _fresh(stem + '_mid.webp')
|
||||
for stem in stems.values())
|
||||
thumbs_fresh = all(_fresh(stem + thumb_suffix) for stem in stems.values())
|
||||
if heavy_fresh and not thumbs_fresh:
|
||||
logger.info(f" Vignettes de sous-tuiles recalculées : "
|
||||
f"{tile['dir_name']}/{viz_key} ({len(stems)} découpages)")
|
||||
try:
|
||||
for stem in stems.values():
|
||||
with PILImage.open(str(output_dir / sub_dir_name / (stem + '.avif'))) as q:
|
||||
q.load()
|
||||
if q.mode not in ('RGB', 'L'):
|
||||
q = q.convert('RGB')
|
||||
_save_thumb(q, stem)
|
||||
# Vignette 256 px d'une génération précédente
|
||||
(output_dir / sub_dir_name / (stem + '_thumb.webp')).unlink(missing_ok=True)
|
||||
except Exception as e:
|
||||
logger.debug(f"Re-découpe des vignettes impossible ({viz_key}) : {e}")
|
||||
heavy_fresh = False
|
||||
if not heavy_fresh:
|
||||
logger.info(f" Sous-tuiles recalculées : {tile['dir_name']}/{viz_key} "
|
||||
f"({len(stems)} découpages)")
|
||||
try:
|
||||
@ -799,8 +838,10 @@ def _build_subtiles(tile, offered_viz_keys, output_dir, sub_dir_name):
|
||||
f"repli dalle entière pour {viz_key} : {e}")
|
||||
cut_failed = True
|
||||
break
|
||||
# Supprime l'ancien crop .webp d'une génération précédente
|
||||
# Supprime les anciens crops d'une génération précédente
|
||||
# (repli .webp, vignette 256 px sans taille dans le nom)
|
||||
(output_dir / sub_dir_name / (stem + '.webp')).unlink(missing_ok=True)
|
||||
(output_dir / sub_dir_name / (stem + '_thumb.webp')).unlink(missing_ok=True)
|
||||
mid_scale = min(1.0, _MID_THUMB_SIZE / max(quad.size))
|
||||
mid_img = quad
|
||||
if mid_scale < 1.0:
|
||||
@ -808,19 +849,14 @@ def _build_subtiles(tile, offered_viz_keys, output_dir, sub_dir_name):
|
||||
max(1, int(quad.size[1] * mid_scale))), resample)
|
||||
mid_img.save(str(output_dir / sub_dir_name / (stem + '_mid.webp')),
|
||||
format='WEBP', quality=82)
|
||||
scale = min(1.0, 256 / max(quad.size))
|
||||
if scale < 1.0:
|
||||
quad = quad.resize((max(1, int(quad.size[0] * scale)),
|
||||
max(1, int(quad.size[1] * scale))), resample)
|
||||
quad.save(str(output_dir / sub_dir_name / (stem + '_thumb.webp')),
|
||||
format='WEBP', quality=80)
|
||||
_save_thumb(quad, stem)
|
||||
if cut_failed:
|
||||
_fallback_full_dalle(entries, viz_key, info)
|
||||
continue
|
||||
v = _url_version(src_mtime)
|
||||
for (i, j), stem in stems.items():
|
||||
entries[(i, j)]['viz'][viz_key] = {
|
||||
'thumb': f"{sub_dir_name}/{stem}_thumb.webp{v}",
|
||||
'thumb': f"{sub_dir_name}/{stem}{thumb_suffix}{v}",
|
||||
'mid': f"{sub_dir_name}/{stem}_mid.webp{v}",
|
||||
'full': f"{sub_dir_name}/{stem}.avif{v}",
|
||||
}
|
||||
@ -2092,6 +2128,7 @@ function syncTileLayers() {
|
||||
img.dataset.layer = k;
|
||||
img.alt = '';
|
||||
img.decoding = 'async';
|
||||
img.loading = 'lazy';
|
||||
div.appendChild(img);
|
||||
entry.imgs[k] = img;
|
||||
}
|
||||
@ -2102,38 +2139,96 @@ function syncTileLayers() {
|
||||
updateImages();
|
||||
}
|
||||
|
||||
// Bascule vignette (256 px) ↔ vignette intermédiaire (640 px) ↔ image
|
||||
// pleine résolution selon le zoom : en vue d'ensemble la vignette suffit ;
|
||||
// au-delà de ~900 px à l'écran on charge l'image complète pour conserver
|
||||
// les détails, l'intermédiaire faisant la transition. En dézoomant on
|
||||
// retombe sur la vignette pour libérer la mémoire. Visibilité et tailles
|
||||
// sont calculées en coordonnées projetées : aucun reflow.
|
||||
const PRELOAD_MARGIN_PX = 300;
|
||||
const FULL_RES_PX = 900;
|
||||
// Bascule vignette ↔ vignette intermédiaire (640 px) ↔ image pleine
|
||||
// résolution selon le zoom, avec trois garde-fous mémoire :
|
||||
// - hors de l'écran (+ marge de préchargement), l'<img> est vidée de son
|
||||
// src : ni téléchargement ni décodage pour les tuiles éloignées (et à
|
||||
// l'ouverture de la page, seules les tuiles visibles se chargent) ;
|
||||
// - la pleine résolution (≈ 25 Mo décodés par AVIF 2500²) ne se charge
|
||||
// que pour les sous-tuiles à l'écran — l'anneau de 150 px ne précharge
|
||||
// que le palier intermédiaire, pour que les tuiles qui entrent à l'écran
|
||||
// lors d'un pan soient déjà nettes ;
|
||||
// - le nombre d'images pleine résolution est plafonné (FULL_CAP) : au-delà,
|
||||
// les sous-tuiles les plus éloignées du centre retombent sur le palier
|
||||
// intermédiaire. En dézoomant, chaque palier libère la mémoire décodée.
|
||||
// Les changements de palier passent par setImgSrc : l'image est pré-décodée
|
||||
// en tâche de fond puis échangée d'un coup — jamais de flash blanc au zoom
|
||||
// ou au pan. Visibilité et tailles sont calculées en coordonnées projetées :
|
||||
// aucun reflow.
|
||||
const PRELOAD_MARGIN_PX = 300; // anneau de vignettes autour de l'écran
|
||||
const MID_MARGIN_PX = 150; // anneau du palier intermédiaire (pan fluide)
|
||||
const BIG_PX = 220; // en dessous, la vignette suffit
|
||||
const FULL_RES_PX = 900; // en dessous, la vignette intermédiaire
|
||||
const FULL_CAP = 24; // images pleine résolution max, toutes couches
|
||||
|
||||
// Change le src d'une <img> sans flash : l'image est d'abord téléchargée et
|
||||
// décodée hors écran, le src n'est posé qu'une fois prête (le cache du
|
||||
// navigateur sert alors la bitmap décodée instantanément). dataset.want
|
||||
// invalide l'échange si un updateImages plus récent a décidé d'un autre
|
||||
// palier entre-temps (pan rapide pendant le décodage).
|
||||
function setImgSrc(img, url) {
|
||||
if (img.src.endsWith(url)) { img.dataset.want = url; return; }
|
||||
img.dataset.want = url;
|
||||
const pre = new Image();
|
||||
pre.decoding = 'async';
|
||||
pre.src = url;
|
||||
const apply = () => { if (img.dataset.want === url) img.src = url; };
|
||||
if (pre.decode) pre.decode().then(apply, apply);
|
||||
else apply();
|
||||
}
|
||||
|
||||
function updateImages() {
|
||||
if (!map._loaded) return; // vue pas encore posée (fitView) : rien à faire
|
||||
const zoom = map.getZoom();
|
||||
const b = map.getBounds();
|
||||
const swP = map.project(b.getSouthWest(), zoom);
|
||||
const neP = map.project(b.getNorthEast(), zoom);
|
||||
const visBounds = L.latLngBounds(
|
||||
map.unproject(L.point(swP.x - PRELOAD_MARGIN_PX, swP.y + PRELOAD_MARGIN_PX), zoom),
|
||||
map.unproject(L.point(neP.x + PRELOAD_MARGIN_PX, neP.y - PRELOAD_MARGIN_PX), zoom));
|
||||
const vp = map.getBounds();
|
||||
const swP = map.project(vp.getSouthWest(), zoom);
|
||||
const neP = map.project(vp.getNorthEast(), zoom);
|
||||
const ctrP = L.point((swP.x + neP.x) / 2, (swP.y + neP.y) / 2);
|
||||
const expand = (margin) => L.latLngBounds(
|
||||
map.unproject(L.point(swP.x - margin, swP.y + margin), zoom),
|
||||
map.unproject(L.point(neP.x + margin, neP.y - margin), zoom));
|
||||
const visBounds = expand(PRELOAD_MARGIN_PX);
|
||||
const midBounds = expand(MID_MARGIN_PX);
|
||||
// Candidats pleine résolution : triés du plus proche au plus lointain du
|
||||
// centre, seuls les FULL_CAP premiers l'obtiennent.
|
||||
const fullWant = [];
|
||||
const plan = [];
|
||||
for (const entry of markers.values()) {
|
||||
const visible = visBounds.intersects(entry.bounds);
|
||||
const onScreen = vp.intersects(entry.bounds);
|
||||
const near = midBounds.intersects(entry.bounds);
|
||||
const p = tileSize(entry);
|
||||
const big = visible && (p.w > 300 || p.h > 300);
|
||||
const big = near && (p.w > BIG_PX || p.h > BIG_PX);
|
||||
const dist = map.project(entry.bounds.getCenter(), zoom).distanceTo(ctrP);
|
||||
for (const k in entry.imgs) {
|
||||
const v = entry.t.viz[k];
|
||||
if (!v) continue;
|
||||
// Les couches quasi transparentes n'ont pas besoin de pleine résolution
|
||||
let want = v.thumb;
|
||||
if (big && state.layers.opacity[k] > 0.15) {
|
||||
want = (v.mid && Math.max(p.w, p.h) < FULL_RES_PX) ? v.mid : v.full;
|
||||
}
|
||||
const img = entry.imgs[k];
|
||||
if (!img.src.endsWith(want)) img.src = want;
|
||||
const v = entry.t.viz[k];
|
||||
if (!visible || !v) { plan.push([img, null]); continue; }
|
||||
let want = v.thumb;
|
||||
// Les couches quasi transparentes n'ont pas besoin de pleine résolution
|
||||
if (big && state.layers.opacity[k] > 0.15) {
|
||||
if (!onScreen) {
|
||||
want = v.mid || v.thumb; // anneau : préchargement intermédiaire
|
||||
} else if (Math.max(p.w, p.h) < FULL_RES_PX) {
|
||||
want = v.mid || v.full;
|
||||
} else {
|
||||
fullWant.push({ img: img, v: v, dist: dist });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
plan.push([img, want]);
|
||||
}
|
||||
}
|
||||
fullWant.sort((a, b) => a.dist - b.dist);
|
||||
fullWant.forEach((c, i) => plan.push(
|
||||
[c.img, i < FULL_CAP ? c.v.full : (c.v.mid || c.v.thumb)]));
|
||||
for (const pair of plan) {
|
||||
const img = pair[0], want = pair[1];
|
||||
if (!want) {
|
||||
img.dataset.want = '';
|
||||
if (img.getAttribute('src') !== null) img.removeAttribute('src');
|
||||
} else {
|
||||
setImgSrc(img, want);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2310,11 +2405,8 @@ const BUILTIN_PRESETS = [
|
||||
{ label: 'Cavités', on: ['slope', 'positive_openness'],
|
||||
opacity: { slope: 0.5, positive_openness: 1 },
|
||||
osm: { on: false } },
|
||||
{ label: 'Rugosité', on: ['roughness'],
|
||||
opacity: {},
|
||||
osm: { on: true, opacity: 0.3 } },
|
||||
{ label: 'Tout', on: ['slope', 'aspect', 'positive_openness', 'roughness'],
|
||||
opacity: { slope: 0.8, aspect: 0.8, positive_openness: 0.55, roughness: 0.8 },
|
||||
{ label: 'Tout', on: ['slope', 'aspect', 'positive_openness'],
|
||||
opacity: { slope: 0.8, aspect: 0.8, positive_openness: 0.55 },
|
||||
osm: { on: true, opacity: 0.85, dark: true } },
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user