"""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}}