Files
lidar_rendu/lidar_pipeline/tests/test_index.py
Antoine fb892ea9f2 Translate the whole project to English and fix outdated comments and help
Comments, docstrings, logs, CLI help, map UI, legends, PDF sheet, scripts,
compose files and AGENTS.md are now English. Data keys stay unchanged
(relief_oriente, densite_sol, visualisations/, API JSON keys, link params).
Wrong comments and help defaults found along the way are corrected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:16:45 +02:00

668 lines
27 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests for the tile catalog (index.py)."""
import json
from pathlib import Path
def test_parse_basename_coords_valid():
"""Parse the coordinates of a valid LHD basename."""
from lidar_pipeline.index import parse_basename_coords
assert parse_basename_coords("LHD_FXX_1000_6881_PTS_LAMB93_IGN69") == (1000, 6881)
assert parse_basename_coords("LHD_FXX_1049_6895_PTS_LAMB93_IGN69") == (1049, 6895)
def test_parse_basename_coords_with_res_suffix():
"""Directory names with a resolution suffix can be parsed too."""
from lidar_pipeline.index import parse_basename_coords
assert parse_basename_coords("LHD_FXX_1000_6881_PTS_LAMB93_IGN69_r0p2") == (1000, 6881)
def test_parse_basename_coords_invalid():
"""Non-LHD names return None."""
from lidar_pipeline.index import parse_basename_coords
assert parse_basename_coords("random_dir") is None
assert parse_basename_coords("DTM") is None
assert parse_basename_coords("") is None
def test_strip_res_suffix_primary():
"""Directory without a suffix = primary resolution (0.5)."""
from lidar_pipeline.index import _strip_res_suffix
base, res = _strip_res_suffix("LHD_FXX_1000_6881_PTS_LAMB93_IGN69")
assert base == "LHD_FXX_1000_6881_PTS_LAMB93_IGN69"
assert res == 0.5
def test_strip_res_suffix_multi():
"""Directory with the _r0p2 suffix = resolution 0.2."""
from lidar_pipeline.index import _strip_res_suffix
base, res = _strip_res_suffix("LHD_FXX_1000_6881_PTS_LAMB93_IGN69_r0p2")
assert base == "LHD_FXX_1000_6881_PTS_LAMB93_IGN69"
assert res == 0.2
def test_compute_bbox():
"""Compute the bounding box of a set of tiles."""
from lidar_pipeline.index import compute_bbox
tiles = [
{'col': 1000, 'row': 6881},
{'col': 1001, 'row': 6882},
{'col': 1002, 'row': 6880},
]
bbox = compute_bbox(tiles)
assert bbox == {'min_col': 1000, 'max_col': 1002, 'min_row': 6880, 'max_row': 6882}
def test_compute_bbox_empty():
"""No tile → None."""
from lidar_pipeline.index import compute_bbox
assert compute_bbox([]) is None
def _make_fake_viz_dir(vis_dir, basename, col, row, viz_keys=('hillshade_multi', 'svf'), ext='webp', res_suffix=''):
"""Create a fake visualization directory with small images.
The resolution suffix only appears in the directory name (mirroring
the pipeline: files stay prefixed with the bare basename).
"""
from PIL import Image as PILImage
import numpy as np
dir_name = f"LHD_FXX_{col}_{row}_PTS_LAMB93_IGN69{res_suffix}"
tile_dir = Path(vis_dir) / dir_name
tile_dir.mkdir(parents=True, exist_ok=True)
for v in viz_keys:
arr = np.random.randint(0, 255, (50, 50, 3), dtype=np.uint8)
img = PILImage.fromarray(arr)
fname = f"LHD_FXX_{col}_{row}_PTS_LAMB93_IGN69_{v}.{ext}"
img.save(str(tile_dir / fname), format='WEBP', quality=80)
return tile_dir
def test_scan_tiles(tmp_path):
"""scan_tiles detects the tile directories and their visualizations."""
from lidar_pipeline.index import scan_tiles
vis_dir = tmp_path / "visualisations"
vis_dir.mkdir()
_make_fake_viz_dir(vis_dir, "a", 1000, 6881, ('hillshade_multi', 'svf'))
_make_fake_viz_dir(vis_dir, "b", 1001, 6881, ('hillshade_multi',))
tiles = scan_tiles(vis_dir)
assert len(tiles) == 2
names = sorted(t['dir_name'] for t in tiles)
assert "LHD_FXX_1000_6881_PTS_LAMB93_IGN69" in names
assert "LHD_FXX_1001_6881_PTS_LAMB93_IGN69" in names
# Check that the visualizations are detected
t0 = next(t for t in tiles if t['col'] == 1000)
assert 'hillshade_multi' in t0['viz']
assert 'svf' in t0['viz']
def test_scan_tiles_ignores_non_lhd(tmp_path):
"""Non-LHD directories (e.g. temp, DTM) are ignored."""
from lidar_pipeline.index import scan_tiles
vis_dir = tmp_path / "visualisations"
vis_dir.mkdir()
(vis_dir / "random_folder").mkdir()
_make_fake_viz_dir(vis_dir, "a", 1000, 6881)
tiles = scan_tiles(vis_dir)
assert len(tiles) == 1
assert tiles[0]['col'] == 1000
def test_scan_tiles_multi_resolution(tmp_path):
"""Directories with a resolution suffix are decoded correctly."""
from lidar_pipeline.index import scan_tiles
vis_dir = tmp_path / "visualisations"
vis_dir.mkdir()
_make_fake_viz_dir(vis_dir, "a", 1000, 6881, res_suffix='')
_make_fake_viz_dir(vis_dir, "a", 1000, 6881, res_suffix='_r0p2')
tiles = scan_tiles(vis_dir)
assert len(tiles) == 2
resolutions = sorted(t['resolution'] for t in tiles)
assert resolutions == [0.2, 0.5]
def test_res_suffix_str():
"""The resolution suffix mirrors the pipeline naming."""
from lidar_pipeline.index import _res_suffix_str
assert _res_suffix_str(0.5) == ''
assert _res_suffix_str(0.2) == '_r0p2'
def test_collect_tile_metadata(tmp_path):
"""The metadata read the DTM method and the viz dates/sizes."""
import os
from datetime import datetime
from lidar_pipeline.index import _collect_tile_metadata
basename = "LHD_FXX_1000_6881_PTS_LAMB93_IGN69"
tile_dir = tmp_path / "visualisations" / basename
tile_dir.mkdir(parents=True)
viz_file = tile_dir / f"{basename}_hillshade_multi.webp"
viz_file.write_bytes(b"fake")
dtm_dir = tmp_path / "DTM"
dtm_dir.mkdir()
method_file = dtm_dir / f"{basename}_dtm_method.txt"
method_file.write_text("ign", encoding="utf-8")
# Deterministic dates: method.txt older than the viz
os.utime(method_file, (1600000000, 1600000000))
os.utime(viz_file, (1700000000, 1700000000))
fmt = lambda ts: datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M')
tile = {
'basename': basename, 'resolution': 0.5,
'dir_path': str(tile_dir),
'viz': {'hillshade_multi': {'filename': viz_file.name, 'ext': 'webp'}},
}
meta = _collect_tile_metadata(tile, dtm_dir)
assert meta['method'] == 'ign'
assert meta['generated'] == fmt(1600000000)
assert meta['viz']['hillshade_multi']['size'] == 4
assert meta['viz']['hillshade_multi']['date'] == fmt(1700000000)
def test_collect_tile_metadata_resolution_suffix(tmp_path):
"""A 0.2 m tile reads its dedicated _dtm_r0p2_method.txt sidecar."""
from lidar_pipeline.index import _collect_tile_metadata
basename = "LHD_FXX_1000_6881_PTS_LAMB93_IGN69"
tile_dir = tmp_path / "visualisations" / (basename + "_r0p2")
tile_dir.mkdir(parents=True)
dtm_dir = tmp_path / "DTM"
dtm_dir.mkdir()
(dtm_dir / f"{basename}_dtm_r0p2_method.txt").write_text("smrf", encoding="utf-8")
tile = {'basename': basename, 'resolution': 0.2,
'dir_path': str(tile_dir),
'viz': {}}
meta = _collect_tile_metadata(tile, dtm_dir)
assert meta['method'] == 'smrf'
# The date comes from the sidecar (written right after the DTM is created)
assert meta['generated'] is not None
assert meta['viz'] == {}
def test_collect_tile_metadata_fallback_date(tmp_path):
"""Without a DTM sidecar, the generation date falls back to the oldest viz file."""
from lidar_pipeline.index import _collect_tile_metadata
basename = "LHD_FXX_1000_6881_PTS_LAMB93_IGN69"
tile_dir = tmp_path / "visualisations" / basename
tile_dir.mkdir(parents=True)
f = tile_dir / f"{basename}_svf.webp"
f.write_bytes(b"x")
tile = {'basename': basename, 'resolution': 0.5,
'dir_path': str(tile_dir),
'viz': {'svf': {'filename': f.name, 'ext': 'webp'}}}
meta = _collect_tile_metadata(tile, tmp_path / "DTM")
assert meta['method'] is None
assert meta['generated'] is not None
def test_build_index_generates_inventory(tmp_path):
"""build_index generates the index_tiles.json inventory and the thumbnails."""
from lidar_pipeline.index import build_index
output_dir = tmp_path / "output"
vis_dir = output_dir / "visualisations"
vis_dir.mkdir(parents=True)
_make_fake_viz_dir(vis_dir, "a", 1000, 6881,
('aspect', 'slope', 'positive_openness',
'wavelet', 'hillshade_multi', 'svf'))
_make_fake_viz_dir(vis_dir, "b", 1001, 6881, ('hillshade_multi',))
result = build_index(output_dir)
assert result is not None
inv_path = Path(result)
assert inv_path.exists()
assert inv_path.name == "index_tiles.json"
# The old HTML interface was removed: no more shell or assets
assert not (output_dir / "index.html").exists()
assert not (output_dir / "assets").exists()
# Inventory served by /api/tiles to the lightweight machines: tiles with corners,
# layer labels, counter.
data = json.loads(inv_path.read_text(encoding='utf-8'))
assert data['tiles'] and data['tiles'][0]['corners']
assert 'aspect' in data['viz_meta']
assert data['viz_meta']['aspect']['label']
assert data['stats']['n_tiles'] == len(data['tiles'])
# Check the generated thumbnails
thumb_dir = output_dir / "index_thumbs"
assert thumb_dir.is_dir()
thumbs = list(thumb_dir.glob("*.jpg"))
assert len(thumbs) >= 2 # at least hillshade for each tile
def test_mid_thumbnails_generated(tmp_path):
"""640 px intermediate thumbnail generated and referenced in the index."""
from lidar_pipeline.index import build_index
output_dir = tmp_path / "output"
vis_dir = output_dir / "visualisations"
vis_dir.mkdir(parents=True)
_make_fake_viz_dir(vis_dir, "a", 1000, 6881, ('hillshade_multi',))
assert build_index(output_dir) is not None
data = json.loads((output_dir / "index_tiles.json").read_text(encoding='utf-8'))
viz = data['tiles'][0]['viz']['hillshade_multi']
assert 'mid' in viz
# The URL carries a ?v= suffix (cache busting): path without it
assert (output_dir / viz['mid'].split('?')[0]).exists()
assert viz['mid'].split('?')[0].endswith("_mid.jpg")
def test_subtiles_cover_all_layers(tmp_path):
"""Every layer of a 0.2 m tile is cut into sub-tiles (no more short list)."""
from lidar_pipeline.index import _CARTO_SUBTILED_VIZ, build_index
assert _CARTO_SUBTILED_VIZ == ()
output_dir = tmp_path / "output"
vis_dir = output_dir / "visualisations"
vis_dir.mkdir(parents=True)
_make_fake_viz_dir(vis_dir, "a", 1000, 6881,
('hillshade_multi', 'svf', 'topo'), res_suffix='_r0p2')
assert build_index(output_dir) is not None
sub_dir = output_dir / "index_subtiles"
for viz in ('hillshade_multi', 'svf', 'topo'):
avif = sub_dir / f"LHD_FXX_1000_6881_PTS_LAMB93_IGN69_r0p2_{viz}_0_0.avif"
assert avif.exists(), viz
# The index references the sub-tiles for each layer, not the whole-tile fallback
data = json.loads((output_dir / "index_tiles.json").read_text(encoding='utf-8'))
subs = [t for t in data['tiles'] if t.get('sub_k') == 2]
assert len(subs) == 4
for t in subs:
for viz in ('hillshade_multi', 'svf', 'topo'):
assert t['viz'][viz]['full'].startswith("index_subtiles/"), viz
def test_panel_restricted_to_kept_layers():
"""The panel is restricted to the kept layers (PANEL_VIZ): the default
main layer and the precision layer are in it."""
from lidar_pipeline.index import (PANEL_VIZ, DEFAULT_VIZ, PRECISION_VIZ, VIEW_MODES,
DEFAULT_VIEW_MODE, KEYWORD_TO_STEP, default_main_layer)
from lidar_pipeline.pipeline import VIZ_STEPS
assert DEFAULT_VIZ in PANEL_VIZ and PRECISION_VIZ in PANEL_VIZ
assert DEFAULT_VIEW_MODE in VIEW_MODES
assert default_main_layer(["densite_sol", "aspect"]) == "aspect"
assert default_main_layer(["densite_sol"]) is None
# Each kept layer matches a valid --only step
steps = {name for name, _ in VIZ_STEPS}
for key in PANEL_VIZ:
assert KEYWORD_TO_STEP.get(key, key) in steps
def test_build_index_merges_cross_resolution_viz(tmp_path):
"""A layer produced only at 0.5 m stays visible on the 0.2 m tile.
The last run may have been interrupted between the two resolution passes:
the cross-resolution merge must point thumbnails and URLs to the original
directory of each file (dir_name), not to the dir_path of the displayed
tile — otherwise the thumbnails fail and the layer disappears.
"""
from lidar_pipeline.index import build_index
output_dir = tmp_path / "output"
vis_dir = output_dir / "visualisations"
vis_dir.mkdir(parents=True)
base = "LHD_FXX_1000_6881_PTS_LAMB93_IGN69"
# 0.5 m pass: aspect + slope; 0.2 m pass: aspect only
_make_fake_viz_dir(vis_dir, base, 1000, 6881, ('aspect', 'slope'))
_make_fake_viz_dir(vis_dir, base, 1000, 6881, ('aspect',), res_suffix='_r0p2')
assert build_index(output_dir) is not None
data = json.loads((output_dir / "index_tiles.json").read_text(encoding='utf-8'))
# A single displayed position (finest resolution), two layers
tiles = [t for t in data['tiles'] if (t['col'], t['row']) == (1000, 6881)]
keys = set()
for t in tiles:
keys.update(t['viz'].keys())
assert {'aspect', 'slope'} <= keys
# Slope thumbnail generated + every thumb/full really exists on disk
assert (output_dir / "index_thumbs" / f"{base}_r0p2_slope.jpg").exists()
for t in tiles:
for v in t['viz'].values():
assert (output_dir / v['thumb'].split('?')[0]).exists(), v['thumb']
assert (output_dir / v['full'].split('?')[0]).exists(), v['full']
def test_build_index_regenerates_stale_thumbnails(tmp_path):
"""A recomputed tile (newer source) regenerates its thumbnail."""
import os
import time
import numpy as np
from PIL import Image as PILImage
from lidar_pipeline.index import build_index
output_dir = tmp_path / "output"
vis_dir = output_dir / "visualisations"
vis_dir.mkdir(parents=True)
tile_dir = _make_fake_viz_dir(vis_dir, "a", 1000, 6881, ('hillshade_multi',))
assert build_index(output_dir) is not None
thumb_path = output_dir / "index_thumbs" / "LHD_FXX_1000_6881_PTS_LAMB93_IGN69_hillshade_multi.jpg"
assert thumb_path.exists()
m1 = thumb_path.stat().st_mtime
# Tile recomputed: source rewritten with a newer mtime
src = tile_dir / "LHD_FXX_1000_6881_PTS_LAMB93_IGN69_hillshade_multi.webp"
arr = np.random.randint(0, 255, (50, 50, 3), dtype=np.uint8)
PILImage.fromarray(arr).save(str(src), format='WEBP', quality=80)
os.utime(src, (m1 + 5, m1 + 5))
assert build_index(output_dir) is not None
m2 = thumb_path.stat().st_mtime
assert m2 > m1 # thumbnail regenerated
# Source not modified since → no needless regeneration
os.utime(src, (time.time() - 10, time.time() - 10))
assert build_index(output_dir) is not None
assert thumb_path.stat().st_mtime == m2
def test_build_subtiles_regenerates_stale_crops(tmp_path):
"""A recomputed 0.2 m tile regenerates its sub-tiles (per visualization)."""
import os
from lidar_pipeline.index import build_index
output_dir = tmp_path / "output"
vis_dir = output_dir / "visualisations"
vis_dir.mkdir(parents=True)
tile_dir = _make_fake_viz_dir(vis_dir, "a", 1000, 6881,
('hillshade_multi', 'aspect'), res_suffix='_r0p2')
assert build_index(output_dir) is not None
sub_dir = output_dir / "index_subtiles"
hill_avif = sub_dir / "LHD_FXX_1000_6881_PTS_LAMB93_IGN69_r0p2_hillshade_multi_0_0.avif"
aspect_avif = sub_dir / "LHD_FXX_1000_6881_PTS_LAMB93_IGN69_r0p2_aspect_0_0.avif"
assert hill_avif.exists() and aspect_avif.exists()
hill_mid = sub_dir / "LHD_FXX_1000_6881_PTS_LAMB93_IGN69_r0p2_hillshade_multi_0_0_mid.webp"
assert hill_mid.exists()
m_hill_1 = hill_avif.stat().st_mtime
m_aspect_1 = aspect_avif.stat().st_mtime
# Recomputation: only the hillshade source is newer
src = tile_dir / "LHD_FXX_1000_6881_PTS_LAMB93_IGN69_hillshade_multi.webp"
os.utime(src, (m_hill_1 + 5, m_hill_1 + 5))
assert build_index(output_dir) is not None
assert hill_avif.stat().st_mtime > m_hill_1 # hillshade sub-tiles regenerated
assert aspect_avif.stat().st_mtime == m_aspect_1 # aspect untouched
def test_urls_versioned_for_cache_busting(tmp_path):
"""Image URLs change when a tile is regenerated.
Versioned URLs (?v=) are served with an immutable cache: a
recomputation MUST change the URL to force a reload, including
live during a run (the inventory is rewritten after each tile). Each URL
carries the mtime of ITS file (see test_urls_versioned_by_served_file_mtime).
"""
import os
import re
from lidar_pipeline.index import build_index
output_dir = tmp_path / "output"
vis_dir = output_dir / "visualisations"
vis_dir.mkdir(parents=True)
tile_dir = _make_fake_viz_dir(vis_dir, "a", 1000, 6881, ('hillshade_multi',))
assert build_index(output_dir) is not None
def read_urls():
data = json.loads((output_dir / "index_tiles.json").read_text(encoding='utf-8'))
viz = data['tiles'][0]['viz']['hillshade_multi']
return viz['thumb'], viz['mid'], viz['full']
thumb1, mid1, full1 = read_urls()
for url in (thumb1, mid1, full1):
assert re.search(r"\?v=\d+$", url), url # versioned (ms mtime)
assert (output_dir / url.split('?')[0]).exists(), url
# Same source, rebuild → identical URLs (browser cache kept)
assert build_index(output_dir) is not None
assert read_urls() == (thumb1, mid1, full1)
# Regeneration: newer source mtime → new URLs
src = tile_dir / "LHD_FXX_1000_6881_PTS_LAMB93_IGN69_hillshade_multi.webp"
m = src.stat().st_mtime
os.utime(src, (m + 10, m + 10))
assert build_index(output_dir) is not None
thumb2, mid2, full2 = read_urls()
assert (thumb2, mid2, full2) != (thumb1, mid1, full1)
assert thumb2.split('?')[0] == thumb1.split('?')[0] # same file, new version
def test_subtile_urls_versioned(tmp_path):
"""The URLs of the 0.2 m sub-tiles are versioned too (?v=)."""
import re
from lidar_pipeline.index import build_index
output_dir = tmp_path / "output"
vis_dir = output_dir / "visualisations"
vis_dir.mkdir(parents=True)
_make_fake_viz_dir(vis_dir, "a", 1000, 6881, ('hillshade_multi',),
res_suffix='_r0p2')
assert build_index(output_dir) is not None
data = json.loads((output_dir / "index_tiles.json").read_text(encoding='utf-8'))
subs = [t for t in data['tiles'] if t.get('sub_k') == 2]
assert subs
for t in subs:
for v in t['viz'].values():
for url in (v['thumb'], v['mid'], v['full']):
assert re.search(r"\?v=\d+$", url), url
assert (output_dir / url.split('?')[0]).exists(), url
def test_build_index_empty_returns_none(tmp_path):
"""No tile → build_index returns None without crashing."""
from lidar_pipeline.index import build_index
output_dir = tmp_path / "output"
(output_dir / "visualisations").mkdir(parents=True)
result = build_index(output_dir)
assert result is None
def test_attach_gps_bounds():
"""attach_gps_bounds adds ordered GPS bounds (metropolitan France)."""
from lidar_pipeline.index import attach_gps_bounds
tiles = [{'col': 1000, 'row': 6881}, {'col': 1042, 'row': 6900}]
attach_gps_bounds(tiles)
for t in tiles:
assert 'bounds' in t
(lat_s, lon_w), (lat_n, lon_e) = t['bounds']
assert lat_n > lat_s
assert lon_e > lon_w
# Metropolitan France
assert 41 < lat_s < 51
assert -5 < lon_w < 10
def test_attach_gps_bounds_row_is_north_edge():
"""The file row number = NORTH edge (IGN LiDAR HD convention).
Checked against the DTM bounds: X ∈ [col, col+1] km, Y ∈ [row-1, row] km.
The historical regression placed Y ∈ [row, row+1] (1 km too far north).
"""
from rasterio.warp import transform as warp_transform
from lidar_pipeline.index import attach_gps_bounds
col, row = 1054, 6882
tiles = [{'col': col, 'row': row}]
attach_gps_bounds(tiles)
corners = tiles[0]['corners']
# Exact reference of the real cell: SW, SE, NE, NW
xs = [col * 1000, (col + 1) * 1000, (col + 1) * 1000, col * 1000]
ys = [(row - 1) * 1000, (row - 1) * 1000, row * 1000, row * 1000]
lons, lats = warp_transform('EPSG:2154', 'EPSG:4326', xs, ys)
for k in range(4):
assert abs(corners[k][0] - lats[k]) < 1e-9
assert abs(corners[k][1] - lons[k]) < 1e-9
# The old convention (row = south edge) would be off by about 1 km
lat_n = max(c[0] for c in corners)
assert abs(lat_n - max(lats)) < 1e-9 # north edge = Y = row×1000
def test_pick_display_viz_prefers_hillshade():
"""The default viz choice prefers hillshade_multi."""
from lidar_pipeline.index import _pick_display_viz
assert _pick_display_viz(['svf', 'hillshade_multi', 'slope']) == 'hillshade_multi'
assert _pick_display_viz(['svf', 'slope']) == 'svf'
assert _pick_display_viz(['topo']) == 'topo'
def test_subdivision_k():
"""0.5 m/px (2000 px) stays whole; 0.2 m/px (5000 px) is split 2×2."""
from lidar_pipeline.index import _subdivision_k
assert _subdivision_k(0.5) == 1
assert _subdivision_k(0.2) == 2
assert _subdivision_k(1.0) == 1
def test_subtile_corners_grid():
"""The sub-tiles rebuild the tile grid exactly."""
from lidar_pipeline.index import _subtile_corners
corners = [[10.0, 2.0], [10.0, 3.0], [11.0, 3.0], [11.0, 2.0]] # SW SE NE NW
k = 2
sw_quad = _subtile_corners(corners, 0, 0, k) # south-west quadrant
ne_quad = _subtile_corners(corners, 1, 1, k) # north-east quadrant
# The SW quadrant shares the SW corner of the tile
assert sw_quad[0] == corners[0]
# The NE quadrant shares the NE corner of the tile
assert ne_quad[2] == corners[2]
# The SW quadrant has its NE corner at the center of the tile
assert sw_quad[2] == [10.5, 2.5]
# Adjacency: east edge of SW = west edge of SE (0,0)-(1,0)
se_quad = _subtile_corners(corners, 1, 0, k)
assert sw_quad[1] == se_quad[0]
assert sw_quad[2] == se_quad[3]
def test_approx_wgs84_to_l93_roundtrip():
"""The affine WGS84→L93 approximation is the exact inverse of L93→WGS84."""
from lidar_pipeline.index import _approx_l93_to_wgs84, _approx_wgs84_to_l93
for x, y in ((1054000.0, 6882000.0), (700000.0, 6600000.0), (950123.0, 6410456.0)):
lon, lat = _approx_l93_to_wgs84(x, y)
x2, y2 = _approx_wgs84_to_l93(lon, lat)
assert abs(x2 - x) < 1e-6 and abs(y2 - y) < 1e-6
def test_subtile_corners_shared_edges_exact():
"""Shared edges: two neighboring sub-tiles get the SAME point.
Without sharing, the bilinear interpolation is evaluated twice on
different fractions and the edges miss by less than a pixel
— broken seam (white line) at medium zoom.
"""
from lidar_pipeline.index import _subtile_corners
corners = [[10.0, 2.0], [10.0, 3.0], [11.0, 3.0], [11.0, 2.0]]
k = 4
a = _subtile_corners(corners, 0, 0, k)
b = _subtile_corners(corners, 1, 0, k)
assert a[1] == b[0] # east edge of (0,0) = west edge of (1,0)
assert a[2] == b[3]
c = _subtile_corners(corners, 0, 1, k)
assert a[3] == c[0] # north edge of (0,0) = south edge of (0,1)
d = _subtile_corners(corners, 3, 3, k)
assert d[2] == corners[2] # tile NE corner shared as is
def test_overview_corners_shared_between_tiles(tmp_path):
"""A tile's corner is also its neighbor's (same point).
Two E-W adjacent tiles share a vertical edge: the NE of the western
tile = the NW of the eastern tile. Without sharing, the bilinear interpolations
of neighboring sub-tiles miss by less than a pixel — broken seam
at medium zoom.
"""
from lidar_pipeline.index import build_index
output_dir = tmp_path / "output"
vis_dir = output_dir / "visualisations"
vis_dir.mkdir(parents=True)
for col in (1000, 1001):
_make_fake_viz_dir(vis_dir, "a", col, 6881, ('hillshade_multi',))
assert build_index(output_dir) is not None
data = json.loads((output_dir / "index_tiles.json").read_text(encoding='utf-8'))
tiles = {t['col']: t for t in data['tiles']}
a_ne = tiles[1000]['corners'][2] # NE of the western tile
b_nw = tiles[1001]['corners'][3] # NW of the eastern tile
assert a_ne == b_nw, f"shared corners diverge: {a_ne} != {b_nw}"
def test_urls_versioned_by_served_file_mtime(tmp_path):
"""?v= follows the mtime of the SERVED file — prerequisite of the immutable cache.
A thumbnail recomputed later (unchanged source) changes content:
its URL must change too, without touching those of the intact files.
"""
from lidar_pipeline.index import build_index
output_dir = tmp_path / "output"
vis_dir = output_dir / "visualisations"
vis_dir.mkdir(parents=True)
_make_fake_viz_dir(vis_dir, "a", 1000, 6881, ('hillshade_multi',))
# 0.2 m: the tile goes through sub-tiling (own thumbnails/mid/AVIF)
_make_fake_viz_dir(vis_dir, "b", 1001, 6881, ('hillshade_multi',),
res_suffix='_r0p2')
assert build_index(output_dir) is not None
data = json.loads((output_dir / "index_tiles.json").read_text(encoding='utf-8'))
import time
for t in data['tiles']:
for viz in t['viz'].values():
for tier in ('thumb', 'mid', 'full'):
url = viz.get(tier)
if not url or '?v=' not in url:
continue
path, v = url.split('?v=')
assert v.isdigit()
# The version is the ms mtime of the served file itself
# (1 s tolerance: file system granularity).
assert abs(int(v) - int((output_dir / path).stat().st_mtime * 1000)) < 1000
# 0.5 m thumbnail recomputed later: only ITS URL changes.
tiles05 = [t for t in data['tiles'] if t.get('resolution') == 0.5]
url_before = tiles05[0]['viz']['hillshade_multi']['thumb']
thumb_path = output_dir / url_before.split('?')[0]
time.sleep(0.05)
from PIL import Image as PILImage
PILImage.new('RGB', (30, 30), (200, 10, 10)).save(str(thumb_path), format='JPEG')
assert build_index(output_dir) is not None
data2 = json.loads((output_dir / "index_tiles.json").read_text(encoding='utf-8'))
tiles05b = [t for t in data2['tiles'] if t.get('resolution') == 0.5]
url_after = tiles05b[0]['viz']['hillshade_multi']['thumb']
assert url_after.split('?')[0] == url_before.split('?')[0]
assert url_after != url_before
def test_inventory_contains_quality_table(tmp_path):
import json
from lidar_pipeline.index import build_index
from lidar_pipeline.quality import write_quality
from lidar_pipeline.tests.test_tiles import _make_dalle
base = _make_dalle(tmp_path, 1054, 6882, ["relief_oriente"])
write_quality(tmp_path, base, {"version": 1, "ground_density": 7.5})
build_index(tmp_path, "webp")
inv = json.loads((tmp_path / "index_tiles.json").read_text(encoding="utf-8"))
assert inv["quality"] == {base: {"version": 1, "ground_density": 7.5}}