Une mosaïque d overview par zone géographique, pas une grille globale
This commit is contained in:
@ -917,43 +917,52 @@ def _build_subtiles(tile, offered_viz_keys, output_dir, sub_dir_name):
|
|||||||
return usable
|
return usable
|
||||||
|
|
||||||
|
|
||||||
def _build_overview(dalles, output_dir):
|
def _build_overview(zone_records, output_dir):
|
||||||
"""Assemble la mosaïque d'overview par couche pour la vue dézoomée.
|
"""Assemble les mosaïques d'overview par couche pour la vue dézoomée.
|
||||||
|
|
||||||
Une image WebP unique couvre toute la grille L93 des dalles (les cases
|
Une image WebP par (zone géographique × couche), couvrant la grille L93
|
||||||
sans dalle restent transparentes), assemblée depuis les vignettes
|
des dalles de la zone (cases sans dalle transparentes), assemblée depuis
|
||||||
index_thumbs (256 px/km) : au dézoom l'interface l'affiche à la place des
|
les vignettes index_thumbs (256 px/km) : au dézoom l'interface l'affiche
|
||||||
vignettes individuelles — 1 requête et ~30 Mo décodés par couche au lieu
|
à la place des vignettes individuelles — 1 requête et ~30 Mo décodés par
|
||||||
de centaines. Résolution plafonnée à 256 px/km et 3072 px de côté :
|
couche et par zone au lieu de centaines. Une mosaïque par zone : les
|
||||||
l'overview n'est affiché que tant qu'il reste au moins 1:1 avec l'écran,
|
grilles distantes (plusieurs secteurs, parfois à des centaines de km)
|
||||||
jamais étiré donc jamais flou.
|
n'ont pas de coins partagés et surtout pas la même échelle utile.
|
||||||
|
Résolution plafonnée à 256 px/km et 3072 px de côté : l'overview n'est
|
||||||
|
affiché que tant qu'il reste au moins 1:1 avec l'écran, jamais étiré
|
||||||
|
donc jamais flou.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dalles: enregistrements de dalles (zone_tile_records) avec col/row,
|
zone_records: enregistrements par zone ({'tiles': [dalle...]}) avec
|
||||||
corners WGS84 et viz[k]['thumb'] (URL relative à output_dir).
|
col/row, corners WGS84 et viz[k]['thumb'] (URL relative).
|
||||||
output_dir: dossier de sortie racine.
|
output_dir: dossier de sortie racine.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
{viz_key: {url, w, h, px_per_km, km_w, km_h, corners}} ou {} si
|
{viz_key: {'items': [{url, w, h, px_per_km, km_w, km_h, corners}]}}
|
||||||
grille trop petite, coins manquants ou PIL indisponible.
|
ou {} si aucune zone assez grande ou PIL indisponible.
|
||||||
"""
|
"""
|
||||||
if len(dalles) < _OVERVIEW_MIN_DALLES:
|
|
||||||
return {}
|
|
||||||
try:
|
try:
|
||||||
from PIL import Image as PILImage
|
from PIL import Image as PILImage
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.warning("PIL indisponible — overview non générée")
|
logger.warning("PIL indisponible — overview non générée")
|
||||||
return {}
|
return {}
|
||||||
have = [t for t in dalles if t.get('corners')]
|
try:
|
||||||
if not have:
|
resample = PILImage.Resampling.LANCZOS
|
||||||
return {}
|
except AttributeError:
|
||||||
|
resample = getattr(PILImage, 'LANCZOS', 1)
|
||||||
|
out_dir = output_dir / _OVERVIEW_DIR
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
overview = {}
|
||||||
|
for zone in zone_records:
|
||||||
|
have = [t for t in zone['tiles'] if t.get('corners')]
|
||||||
|
if len(have) < _OVERVIEW_MIN_DALLES:
|
||||||
|
continue
|
||||||
min_col = min(t['col'] for t in have)
|
min_col = min(t['col'] for t in have)
|
||||||
max_col = max(t['col'] for t in have)
|
max_col = max(t['col'] for t in have)
|
||||||
min_row = min(t['row'] for t in have)
|
min_row = min(t['row'] for t in have)
|
||||||
max_row = max(t['row'] for t in have)
|
max_row = max(t['row'] for t in have)
|
||||||
# Lattice exact des coins (km L93 → WGS84) : les coins partagés de dalles
|
# Lattice exact des coins (km L93 → WGS84) : les coins partagés de
|
||||||
# voisines proviennent de la même transformation pyproj → cohérents, la
|
# dalles voisines proviennent de la même transformation pyproj →
|
||||||
# mosaïque s'emboîte exactement comme les vignettes individuelles.
|
# cohérents, la mosaïque s'emboîte exactement comme les vignettes.
|
||||||
lattice = {}
|
lattice = {}
|
||||||
for t in have:
|
for t in have:
|
||||||
c = t['corners']
|
c = t['corners']
|
||||||
@ -964,24 +973,20 @@ def _build_overview(dalles, output_dir):
|
|||||||
quad = [(min_col, min_row), (max_col + 1, min_row),
|
quad = [(min_col, min_row), (max_col + 1, min_row),
|
||||||
(max_col + 1, max_row + 1), (min_col, max_row + 1)]
|
(max_col + 1, max_row + 1), (min_col, max_row + 1)]
|
||||||
if any(k not in lattice for k in quad):
|
if any(k not in lattice for k in quad):
|
||||||
return {}
|
logger.debug(f"Overview : coins de grille absents pour "
|
||||||
|
f"{zone.get('label', '?')} — zone ignorée")
|
||||||
|
continue
|
||||||
km_w = max_col + 1 - min_col
|
km_w = max_col + 1 - min_col
|
||||||
km_h = max_row + 1 - min_row
|
km_h = max_row + 1 - min_row
|
||||||
px_per_km = max(4, min(_OVERVIEW_PX_PER_KM,
|
px_per_km = max(4, min(_OVERVIEW_PX_PER_KM,
|
||||||
_OVERVIEW_MAX_PX // max(km_w, km_h)))
|
_OVERVIEW_MAX_PX // max(km_w, km_h)))
|
||||||
w, h = km_w * px_per_km, km_h * px_per_km
|
w, h = km_w * px_per_km, km_h * px_per_km
|
||||||
out_dir = output_dir / _OVERVIEW_DIR
|
corners = [lattice[k] for k in quad]
|
||||||
out_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
try:
|
|
||||||
resample = PILImage.Resampling.LANCZOS
|
|
||||||
except AttributeError:
|
|
||||||
resample = getattr(PILImage, 'LANCZOS', 1)
|
|
||||||
viz_keys = []
|
viz_keys = []
|
||||||
for t in have:
|
for t in have:
|
||||||
for k in t['viz']:
|
for k in t['viz']:
|
||||||
if k not in viz_keys:
|
if k not in viz_keys:
|
||||||
viz_keys.append(k)
|
viz_keys.append(k)
|
||||||
overview = {}
|
|
||||||
for viz_key in viz_keys:
|
for viz_key in viz_keys:
|
||||||
canvas = PILImage.new('RGBA', (w, h), (0, 0, 0, 0))
|
canvas = PILImage.new('RGBA', (w, h), (0, 0, 0, 0))
|
||||||
pasted = 0
|
pasted = 0
|
||||||
@ -1004,18 +1009,19 @@ def _build_overview(dalles, output_dir):
|
|||||||
pasted += 1
|
pasted += 1
|
||||||
if not pasted:
|
if not pasted:
|
||||||
continue
|
continue
|
||||||
out = out_dir / f"{viz_key}.webp"
|
stem = f"{viz_key}_{min_col}_{min_row}"
|
||||||
|
out = out_dir / f"{stem}.webp"
|
||||||
try:
|
try:
|
||||||
canvas.save(str(out), format='WEBP', quality=_OVERVIEW_QUALITY)
|
canvas.save(str(out), format='WEBP', quality=_OVERVIEW_QUALITY)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Overview {viz_key} non écrite : {e}")
|
logger.warning(f"Overview {stem} non écrite : {e}")
|
||||||
continue
|
continue
|
||||||
overview[viz_key] = {
|
overview.setdefault(viz_key, {'items': []})['items'].append({
|
||||||
'url': f"{_OVERVIEW_DIR}/{out.name}{_url_version(_mtime(out))}",
|
'url': f"{_OVERVIEW_DIR}/{out.name}{_url_version(_mtime(out))}",
|
||||||
'w': w, 'h': h, 'px_per_km': px_per_km,
|
'w': w, 'h': h, 'px_per_km': px_per_km,
|
||||||
'km_w': km_w, 'km_h': km_h,
|
'km_w': km_w, 'km_h': km_h,
|
||||||
'corners': [lattice[k] for k in quad],
|
'corners': corners,
|
||||||
}
|
})
|
||||||
return overview
|
return overview
|
||||||
|
|
||||||
|
|
||||||
@ -1256,14 +1262,15 @@ def build_index(output_dir, output_format='avif'):
|
|||||||
if n_sous:
|
if n_sous:
|
||||||
logger.info(f" {n_sous} sous-tuiles générée(s) pour {n_dalles} dalle(s)")
|
logger.info(f" {n_sous} sous-tuiles générée(s) pour {n_dalles} dalle(s)")
|
||||||
|
|
||||||
# Mosaïque d'overview (vue dézoomée) : une image par couche couvrant
|
# Mosaïques d'overview (vue dézoomée) : une image par zone et par couche,
|
||||||
# toute la grille, assemblée depuis les vignettes des dalles.
|
# assemblée depuis les vignettes des dalles de la zone.
|
||||||
dalle_records = [t for zr in zone_records for t in zr['tiles']]
|
overview = _build_overview(zone_records, output_dir)
|
||||||
overview = _build_overview(dalle_records, output_dir)
|
|
||||||
if overview:
|
if overview:
|
||||||
first = next(iter(overview.values()))
|
n_items = sum(len(v['items']) for v in overview.values())
|
||||||
logger.info(f" Overview : {len(overview)} couche(s) "
|
first = next(iter(overview.values()))['items'][0]
|
||||||
f"({first['w']}×{first['h']} px, {first['px_per_km']} px/km)")
|
logger.info(f" Overview : {len(overview)} couche(s) × zones, "
|
||||||
|
f"{n_items} mosaïque(s) (ex. {first['w']}×{first['h']} px, "
|
||||||
|
f"{first['px_per_km']} px/km)")
|
||||||
|
|
||||||
# Export des données de carte (index_tiles.json) : la webapp les sert via
|
# Export des données de carte (index_tiles.json) : la webapp les sert via
|
||||||
# /api/tiles pour afficher les tuiles terminées en direct, sans recharger
|
# /api/tiles pour afficher les tuiles terminées en direct, sans recharger
|
||||||
@ -2358,7 +2365,7 @@ recomputeGridBounds();
|
|||||||
// et sans trou : le gagnant se pré-décode (setImgSrc), et le perdant ne
|
// et sans trou : le gagnant se pré-décode (setImgSrc), et le perdant ne
|
||||||
// s'efface (fondu 300 ms) puis ne libère son src qu'une fois le gagnant
|
// s'efface (fondu 300 ms) puis ne libère son src qu'une fois le gagnant
|
||||||
// affiché.
|
// affiché.
|
||||||
let ovEntry = null; // marqueur overview (même structure qu'une tuile)
|
const ovEntries = []; // un marqueur par mosaïque de zone (même structure qu'une tuile)
|
||||||
let ovOn = false; // palier courant = overview ?
|
let ovOn = false; // palier courant = overview ?
|
||||||
let ovLoser = null; // bascule en cours : {imgs, winners, since, phase}
|
let ovLoser = null; // bascule en cours : {imgs, winners, since, phase}
|
||||||
const OV_HOLD_POLL_MS = 120; // sondage « gagnant affiché ? »
|
const OV_HOLD_POLL_MS = 120; // sondage « gagnant affiché ? »
|
||||||
@ -2369,12 +2376,30 @@ const OV_HOLD_CAP_MS = 2500; // délai maxi avant de libérer quand même
|
|||||||
// couche (pas de const OVERVIEW) — alias sûr, jamais de référence directe.
|
// couche (pas de const OVERVIEW) — alias sûr, jamais de référence directe.
|
||||||
const OVM = (typeof OVERVIEW !== 'undefined' && OVERVIEW) ? OVERVIEW : {};
|
const OVM = (typeof OVERVIEW !== 'undefined' && OVERVIEW) ? OVERVIEW : {};
|
||||||
|
|
||||||
const ovMeta = () => { for (const k in OVM) return OVM[k]; return null; };
|
// Géométrie des mosaïques (une par zone ; toutes les couches la partagent,
|
||||||
|
// au tri près — d'où l'appariement par coin SW dans ovItemFor).
|
||||||
|
const ovItems = () => {
|
||||||
|
for (const k in OVM) return (OVM[k] && OVM[k].items) || [];
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
const ovItemFor = (k, e) => {
|
||||||
|
const items = (OVM[k] && OVM[k].items) || [];
|
||||||
|
return items.find(it => it.corners[0][0] === e.meta.corners[0][0]
|
||||||
|
&& it.corners[0][1] === e.meta.corners[0][1]) || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let ovSig = null; // géométrie enregistrée (anti-recréation au merge live)
|
||||||
|
|
||||||
function registerOverview() {
|
function registerOverview() {
|
||||||
const meta = ovMeta();
|
const items = ovItems();
|
||||||
if (!meta || ovEntry || !map) return;
|
if (!items.length || !map) return;
|
||||||
// Ancre = coin NW de la grille : la matrice mappe l'image depuis (0,0).
|
const sig = JSON.stringify(items.map(i => i.corners));
|
||||||
|
if (sig === ovSig) return;
|
||||||
|
ovSig = sig;
|
||||||
|
for (const e of ovEntries) e.mk.remove();
|
||||||
|
ovEntries.length = 0;
|
||||||
|
items.forEach(meta => {
|
||||||
|
// Ancre = coin NW de la zone : la matrice mappe l'image depuis (0,0).
|
||||||
const nw = meta.corners[3];
|
const nw = meta.corners[3];
|
||||||
const icon = L.divIcon({ className: 'ltile-anchor',
|
const icon = L.divIcon({ className: 'ltile-anchor',
|
||||||
html: '<div class="ltile loverview"></div>',
|
html: '<div class="ltile loverview"></div>',
|
||||||
@ -2384,11 +2409,13 @@ function registerOverview() {
|
|||||||
pane: 'lidarTiles', zIndexOffset: -100000
|
pane: 'lidarTiles', zIndexOffset: -100000
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
const el = mk.getElement();
|
const el = mk.getElement();
|
||||||
ovEntry = {
|
ovEntries.push({
|
||||||
mk: mk, div: el ? el.querySelector('.ltile') : null, imgs: {},
|
mk: mk, div: el ? el.querySelector('.ltile') : null, imgs: {},
|
||||||
|
meta: meta,
|
||||||
latlngs: meta.corners.map(c => L.latLng(c[0], c[1])),
|
latlngs: meta.corners.map(c => L.latLng(c[0], c[1])),
|
||||||
bounds: L.latLngBounds(meta.corners)
|
bounds: L.latLngBounds(meta.corners)
|
||||||
};
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Homographie 4 correspondances → matrix3d CSS (colonne-major), ou null.
|
// Homographie 4 correspondances → matrix3d CSS (colonne-major), ou null.
|
||||||
@ -2425,34 +2452,40 @@ function quadMatrix3d(src, dst) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function layoutOverview() {
|
function layoutOverview() {
|
||||||
if (!ovEntry || !ovEntry.div) return;
|
|
||||||
const meta = ovMeta();
|
|
||||||
if (!meta) return;
|
|
||||||
const zoom = map.getZoom();
|
const zoom = map.getZoom();
|
||||||
const anchor = map.project(ovEntry.latlngs[3], zoom);
|
for (const e of ovEntries) {
|
||||||
|
if (!e.div) continue;
|
||||||
|
const meta = e.meta;
|
||||||
|
const anchor = map.project(e.latlngs[3], zoom);
|
||||||
// Coins image : NW(0,0) NE(w,0) SE(w,h) SW(0,h) — soit corners[3,2,1,0].
|
// Coins image : NW(0,0) NE(w,0) SE(w,h) SW(0,h) — soit corners[3,2,1,0].
|
||||||
const dst = [3, 2, 1, 0].map(i => {
|
const dst = [3, 2, 1, 0].map(i => {
|
||||||
const p = map.project(ovEntry.latlngs[i], zoom);
|
const p = map.project(e.latlngs[i], zoom);
|
||||||
return [p.x - anchor.x, p.y - anchor.y];
|
return [p.x - anchor.x, p.y - anchor.y];
|
||||||
});
|
});
|
||||||
const m = quadMatrix3d([[0, 0], [meta.w, 0], [meta.w, meta.h], [0, meta.h]], dst);
|
const m = quadMatrix3d([[0, 0], [meta.w, 0], [meta.w, meta.h], [0, meta.h]], dst);
|
||||||
const div = ovEntry.div;
|
const div = e.div;
|
||||||
div.style.width = meta.w + 'px';
|
div.style.width = meta.w + 'px';
|
||||||
div.style.height = meta.h + 'px';
|
div.style.height = meta.h + 'px';
|
||||||
div.style.transformOrigin = '0 0';
|
div.style.transformOrigin = '0 0';
|
||||||
div.style.transform = m
|
div.style.transform = m
|
||||||
? 'matrix3d(' + m.join(',') + ')'
|
? 'matrix3d(' + m.join(',') + ')'
|
||||||
: 'translate(-9999px, -9999px)'; // homographie insoluble : hors écran
|
: 'translate(-9999px, -9999px)'; // homographie insoluble : hors écran
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overview actif tant qu'un kilomètre à l'écran ne dépasse pas la résolution
|
// Overview actif tant qu'un kilomètre à l'écran ne dépasse pas la résolution
|
||||||
// de la mosaïque (affichée au pire à 1:1, jamais étirée donc jamais floue).
|
// d'UNE QUELCONQUE des mosaïques (bascule globale, conservatrice : la zone à
|
||||||
|
// la résolution la plus grossière décide — affichée au pire à 1:1, jamais
|
||||||
|
// étirée donc jamais floue).
|
||||||
function overviewWanted() {
|
function overviewWanted() {
|
||||||
const meta = ovMeta();
|
const items = ovItems();
|
||||||
if (!meta || !map._loaded) return false;
|
if (!items.length || !ovEntries.length || !map._loaded) return false;
|
||||||
const zoom = map.getZoom();
|
const zoom = map.getZoom();
|
||||||
const sw = map.project(meta.corners[0], zoom), se = map.project(meta.corners[1], zoom);
|
for (const it of items) {
|
||||||
return Math.hypot(se.x - sw.x, se.y - sw.y) / meta.km_w <= meta.px_per_km;
|
const sw = map.project(it.corners[0], zoom), se = map.project(it.corners[1], zoom);
|
||||||
|
if (Math.hypot(se.x - sw.x, se.y - sw.y) / it.km_w > it.px_per_km) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Libération du palier perdant : seulement quand le gagnant est affiché
|
// Libération du palier perdant : seulement quand le gagnant est affiché
|
||||||
@ -2571,24 +2604,25 @@ function syncTileLayers() {
|
|||||||
img.style.mixBlendMode = state.layers.blend[k] || 'normal';
|
img.style.mixBlendMode = state.layers.blend[k] || 'normal';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Overview : mêmes couches actives, une <img> par couche disponible.
|
// Overview : mêmes couches actives, une <img> par couche et par mosaïque.
|
||||||
if (ovEntry && ovEntry.div) {
|
for (const e of ovEntries) {
|
||||||
for (const k of Object.keys(ovEntry.imgs)) {
|
if (!e.div) continue;
|
||||||
if (act.includes(k) && OVM[k]) continue;
|
for (const k of Object.keys(e.imgs)) {
|
||||||
ovEntry.imgs[k].src = '';
|
if (act.includes(k) && ovItemFor(k, e)) continue;
|
||||||
ovEntry.imgs[k].remove();
|
e.imgs[k].src = '';
|
||||||
delete ovEntry.imgs[k];
|
e.imgs[k].remove();
|
||||||
|
delete e.imgs[k];
|
||||||
}
|
}
|
||||||
act.forEach((k, i) => {
|
act.forEach((k, i) => {
|
||||||
if (!OVM[k]) return;
|
if (!ovItemFor(k, e)) return;
|
||||||
let img = ovEntry.imgs[k];
|
let img = e.imgs[k];
|
||||||
if (!img) {
|
if (!img) {
|
||||||
img = document.createElement('img');
|
img = document.createElement('img');
|
||||||
img.dataset.layer = k;
|
img.dataset.layer = k;
|
||||||
img.alt = '';
|
img.alt = '';
|
||||||
img.decoding = 'async';
|
img.decoding = 'async';
|
||||||
ovEntry.div.appendChild(img);
|
e.div.appendChild(img);
|
||||||
ovEntry.imgs[k] = img;
|
e.imgs[k] = img;
|
||||||
}
|
}
|
||||||
img.classList.remove('fading');
|
img.classList.remove('fading');
|
||||||
img.style.zIndex = String(i + 1);
|
img.style.zIndex = String(i + 1);
|
||||||
@ -2652,7 +2686,7 @@ function updateImages() {
|
|||||||
// Bascule de palier overview ↔ vignettes (uniquement ici : zoomend/moveend,
|
// Bascule de palier overview ↔ vignettes (uniquement ici : zoomend/moveend,
|
||||||
// jamais pendant l'animation de zoom). À la bascule le perdant garde son
|
// jamais pendant l'animation de zoom). À la bascule le perdant garde son
|
||||||
// src (ovTick le libère une fois le gagnant affiché) : pas de trou.
|
// src (ovTick le libère une fois le gagnant affiché) : pas de trou.
|
||||||
const wantOv = ovEntry ? overviewWanted() : false;
|
const wantOv = ovEntries.length ? overviewWanted() : false;
|
||||||
if (wantOv !== ovOn) {
|
if (wantOv !== ovOn) {
|
||||||
if (ovLoser) {
|
if (ovLoser) {
|
||||||
clearTimeout(ovLoser.timer);
|
clearTimeout(ovLoser.timer);
|
||||||
@ -2706,11 +2740,12 @@ function updateImages() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Overview : chargée quand son palier est actif, perdante sinon.
|
// Overview : chargée quand son palier est actif, perdante sinon.
|
||||||
if (ovEntry) {
|
for (const e of ovEntries) {
|
||||||
for (const k in ovEntry.imgs) {
|
for (const k in e.imgs) {
|
||||||
const img = ovEntry.imgs[k];
|
const img = e.imgs[k];
|
||||||
if (ovOn && OVM[k]) {
|
const item = ovOn ? ovItemFor(k, e) : null;
|
||||||
plan.push([img, OVM[k].url]);
|
if (item) {
|
||||||
|
plan.push([img, item.url]);
|
||||||
} else if (img.getAttribute('src') !== null) {
|
} else if (img.getAttribute('src') !== null) {
|
||||||
img.dataset.want = '';
|
img.dataset.want = '';
|
||||||
if (ovLoser && !img.dataset.loser) {
|
if (ovLoser && !img.dataset.loser) {
|
||||||
@ -4287,13 +4322,12 @@ function mergeTilesData(data) {
|
|||||||
let ovChanged = false;
|
let ovChanged = false;
|
||||||
if (data.overview) {
|
if (data.overview) {
|
||||||
for (const k of Object.keys(data.overview)) {
|
for (const k of Object.keys(data.overview)) {
|
||||||
const cur = OVM[k];
|
if (JSON.stringify(OVM[k]) === JSON.stringify(data.overview[k])) continue;
|
||||||
if (cur && cur.url === data.overview[k].url) continue;
|
|
||||||
OVM[k] = data.overview[k];
|
OVM[k] = data.overview[k];
|
||||||
ovChanged = true;
|
ovChanged = true;
|
||||||
}
|
}
|
||||||
if (ovChanged && !ovEntry) {
|
if (ovChanged) {
|
||||||
registerOverview();
|
registerOverview(); // (re)crée les marqueurs si la géométrie a changé
|
||||||
layoutOverview();
|
layoutOverview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -715,7 +715,9 @@ def test_overview_generated_for_large_grids(tmp_path):
|
|||||||
data = json.loads((output_dir / "index_tiles.json").read_text(encoding='utf-8'))
|
data = json.loads((output_dir / "index_tiles.json").read_text(encoding='utf-8'))
|
||||||
ov = data['overview']
|
ov = data['overview']
|
||||||
assert 'hillshade_multi' in ov and 'svf' in ov
|
assert 'hillshade_multi' in ov and 'svf' in ov
|
||||||
meta = ov['hillshade_multi']
|
items = ov['hillshade_multi']['items']
|
||||||
|
assert len(items) == 1 # une seule zone géographique
|
||||||
|
meta = items[0]
|
||||||
assert (output_dir / meta['url'].split('?')[0]).is_file()
|
assert (output_dir / meta['url'].split('?')[0]).is_file()
|
||||||
assert meta['km_w'] == 3 and meta['km_h'] == 2
|
assert meta['km_w'] == 3 and meta['km_h'] == 2
|
||||||
assert meta['w'] == 3 * meta['px_per_km']
|
assert meta['w'] == 3 * meta['px_per_km']
|
||||||
@ -724,7 +726,33 @@ def test_overview_generated_for_large_grids(tmp_path):
|
|||||||
# La coquille HTML embarque la constante pour l'interface
|
# La coquille HTML embarque la constante pour l'interface
|
||||||
html = (output_dir / "index.html").read_text(encoding='utf-8')
|
html = (output_dir / "index.html").read_text(encoding='utf-8')
|
||||||
assert "const OVERVIEW" in html
|
assert "const OVERVIEW" in html
|
||||||
assert "index_overview/hillshade_multi.webp" in html
|
assert "index_overview/hillshade_multi_" in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_overview_one_mosaic_per_zone(tmp_path):
|
||||||
|
"""Zones géographiques distantes : une mosaïque PAR ZONE (pas une grille
|
||||||
|
globale étirée sur des centaines de km aux coins introuvables)."""
|
||||||
|
from lidar_pipeline.index import build_index
|
||||||
|
|
||||||
|
output_dir = tmp_path / "output"
|
||||||
|
vis_dir = output_dir / "visualisations"
|
||||||
|
vis_dir.mkdir(parents=True)
|
||||||
|
# Deux clusters de 2×3 dalles, à ~400 km l'un de l'autre
|
||||||
|
for col in (1000, 1001, 1002):
|
||||||
|
for row in (6881, 6882):
|
||||||
|
_make_fake_viz_dir(vis_dir, "a", col, row, ('hillshade_multi',))
|
||||||
|
for col in (1400, 1401, 1402):
|
||||||
|
for row in (6481, 6482):
|
||||||
|
_make_fake_viz_dir(vis_dir, "b", col, row, ('hillshade_multi',))
|
||||||
|
|
||||||
|
assert build_index(output_dir) is not None
|
||||||
|
data = json.loads((output_dir / "index_tiles.json").read_text(encoding='utf-8'))
|
||||||
|
items = data['overview']['hillshade_multi']['items']
|
||||||
|
assert len(items) == 2
|
||||||
|
assert items[0]['km_w'] == 3 and items[0]['km_h'] == 2
|
||||||
|
assert items[1]['km_w'] == 3 and items[1]['km_h'] == 2
|
||||||
|
# Les deux mosaïques sont distinctes (coins différents)
|
||||||
|
assert items[0]['corners'] != items[1]['corners']
|
||||||
|
|
||||||
|
|
||||||
def test_overview_skipped_for_small_grids(tmp_path):
|
def test_overview_skipped_for_small_grids(tmp_path):
|
||||||
|
|||||||
Reference in New Issue
Block a user