Files
lidar_rendu/lidar_pipeline/tests/test_export_pdf.py
2026-09-27 15:47:12 +02:00

244 lines
10 KiB
Python
Raw 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 de l'export PDF (planche d'impression terrain)."""
import pytest
def test_layout_landscape_a4_dimensions():
from lidar_pipeline.export_pdf import layout
lay = layout("A4", "paysage", 2000)
assert (lay.page_w, lay.page_h) == (297.0, 210.0) and lay.dpi == 300
assert lay.map_w > 150 and lay.map_h > 150
assert lay.panel_x >= lay.map_x + lay.map_w # bandeau à droite
assert lay.map_x + lay.map_w <= lay.page_w and lay.map_y + lay.map_h <= lay.page_h
def test_layout_portrait_a3_panel_below():
from lidar_pipeline.export_pdf import layout
lay = layout("A3", "portrait", 5000)
assert (lay.page_w, lay.page_h) == (297.0, 420.0) and lay.dpi == 250
assert lay.panel_y + lay.panel_h <= lay.map_y # bandeau en bas
@pytest.mark.parametrize("args", [("A5", "paysage", 2000), ("A4", "biais", 2000),
("A4", "paysage", 1234)])
def test_layout_rejects_invalid(args):
from lidar_pipeline.export_pdf import layout
with pytest.raises(ValueError):
layout(*args)
def test_map_bbox_matches_paper_times_scale():
from lidar_pipeline.export_pdf import layout, map_bbox
lay = layout("A4", "paysage", 2000)
b = map_bbox(652500.0, 6861500.0, lay)
assert abs((b[2] - b[0]) - lay.map_w / 1000 * 2000) < 1e-6
assert abs((b[3] - b[1]) - lay.map_h / 1000 * 2000) < 1e-6
assert abs((b[0] + b[2]) / 2 - 652500.0) < 1e-6
def test_pixel_size_and_grid_step():
from lidar_pipeline.export_pdf import grid_step, layout, pixel_size
assert pixel_size(layout("A4", "paysage", 1000)) == 0.2 # plafonné au natif
assert abs(pixel_size(layout("A3", "paysage", 10000)) - 10000 * 0.0254 / 250) < 1e-9
assert [grid_step(s) for s in (1000, 2000, 5000, 10000)] == [100, 100, 500, 1000]
def test_center_l93_rejects_non_finite():
from lidar_pipeline.export_pdf import center_l93
with pytest.raises(ValueError):
center_l93(float("nan"), 2.0)
def test_frame_roundtrip():
from lidar_pipeline.export_pdf import center_l93, frame
lat, lon = 48.85, 2.35
f = frame(lat, lon, "A4", "paysage", 2000)
cx, cy = center_l93(lat, lon)
assert abs(f["cx"] - cx) < 1e-6 and abs(f["cy"] - cy) < 1e-6
assert len(f["corners"]) == 4
nw, ne, se, sw = f["corners"]
assert nw[0] > sw[0] and ne[1] > nw[1] # [lat, lon] : nord en haut, est à droite
assert f["width_m"] > f["height_m"] > 0
def test_export_modules_import_without_numpy():
import subprocess, sys
code = ("import sys; sys.modules['numpy'] = None; "
"import lidar_pipeline.export_pdf, lidar_pipeline.tiles; print('ok')")
r = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True)
assert r.returncode == 0, r.stderr
def _dalle(tmp_path, col, row, color=(200, 30, 30), px=100, layer="relief_oriente"):
from lidar_pipeline import index, tiles
from lidar_pipeline.tests.test_tiles import _make_dalle
_make_dalle(tmp_path, col, row, [layer], color=color, px=px)
tiles.source_index(tmp_path, force=True)
def test_nodata_color_matches_visualizations():
from lidar_pipeline.export_pdf import NODATA_RGB
from lidar_pipeline.visualizations import RELIEF_NODATA_RGB
assert NODATA_RGB == tuple(RELIEF_NODATA_RGB)
def test_compose_l93_full_cell(tmp_path, monkeypatch):
from lidar_pipeline import index
from lidar_pipeline.export_pdf import compose_l93
monkeypatch.setattr(index, "PANEL_VIZ", None)
_dalle(tmp_path, 1054, 6882)
img, mask, cells = compose_l93(tmp_path, (1054200.0, 6881200.0, 1054400.0, 6881300.0), 1.0)
assert img.size == (200, 100) and cells == [(1054, 6882)]
assert mask.getextrema() == (255, 255)
r, g, b = img.getpixel((100, 50))
assert r > 150 and g < 80 and b < 80
def test_compose_l93_half_outside_is_white_hatched(tmp_path, monkeypatch):
from lidar_pipeline import index
from lidar_pipeline.export_pdf import compose_l93
monkeypatch.setattr(index, "PANEL_VIZ", None)
_dalle(tmp_path, 1054, 6882)
# moitié ouest dans la dalle, moitié est hors données
img, mask, cells = compose_l93(tmp_path, (1054900.0, 6881400.0, 1055100.0, 6881500.0), 1.0)
assert mask.getpixel((50, 50)) == 255 and mask.getpixel((150, 50)) == 0
east = img.crop((110, 0, 200, 100)).convert("L").getextrema()
assert east[1] == 255 and east[0] < 255 # blanc + hachures
def test_compose_l93_nodata_pixels_masked(tmp_path, monkeypatch):
from lidar_pipeline import index
from lidar_pipeline.export_pdf import NODATA_RGB, compose_l93
monkeypatch.setattr(index, "PANEL_VIZ", None)
_dalle(tmp_path, 1054, 6882, color=NODATA_RGB)
img, mask, cells = compose_l93(tmp_path, (1054200.0, 6881200.0, 1054400.0, 6881300.0), 1.0)
assert mask.getextrema() == (0, 0)
def test_compose_l93_no_data_returns_empty_cells(tmp_path):
from lidar_pipeline.export_pdf import compose_l93
img, mask, cells = compose_l93(tmp_path, (0.0, 0.0, 100.0, 100.0), 1.0)
assert cells == [] and mask.getextrema() == (0, 0)
def test_lab_to_rgb_matches_numpy_reference():
import numpy as np
from lidar_pipeline.export_pdf import lab_to_rgb
from lidar_pipeline.visualizations import _lab_to_srgb
for L, a, b in [(64, 30, -20), (20, 0, 0), (90, -10, 40), (50, 60, 0)]:
ref = tuple(int(round(v * 255)) for v in _lab_to_srgb(
np.array(L, float), np.array(a, float), np.array(b, float)))
assert all(abs(p - q) <= 1 for p, q in zip(lab_to_rgb(L, a, b), ref))
def test_rose_color_distinct_orientations():
from lidar_pipeline.export_pdf import rose_color
colors = {rose_color(c) for c in (0, 90, 180, 270)}
assert len(colors) == 4
def test_density_color_classes():
from lidar_pipeline.export_pdf import DENSITY_CLASSES, density_color
assert density_color(0.0) == DENSITY_CLASSES[0][2]
assert density_color(50.0) == DENSITY_CLASSES[-1][2]
def test_pdf_text_replaces_unencodable():
from lidar_pipeline.export_pdf import _pdf_text
assert _pdf_text("Relief orienté — 1:2 000 ≥ 🚀") == "Relief orienté — 1:2 000 ? ?"
def _q(density, start="2023-03-15", end="2023-03-17", empty=0.1):
return {"version": 1, "ground_density": density,
"density_grid": [[density] * 20 for _ in range(20)],
"empty_fraction": empty, "acq_start": start, "acq_end": end,
"acq_source": "gps"}
def test_zone_quality_aggregates_two_cells():
from lidar_pipeline.export_pdf import zone_quality
table = {"LHD_FXX_1054_6882_PTS_LAMB93_IGN69": _q(4.0, "2023-03-15", "2023-03-15"),
"LHD_FXX_1055_6882_PTS_LAMB93_IGN69": _q(8.0, "2023-04-02", "2023-04-03", 0.3)}
bbox = (1054500.0, 6881500.0, 1055500.0, 6881600.0) # moitié de chaque dalle
z = zone_quality(bbox, table, [(1054, 6882), (1055, 6882)])
assert abs(z["density_mean"] - 6.0) < 1e-6 and z["density_min"] == 4.0
assert abs(z["empty_fraction"] - 0.2) < 1e-6
assert (z["acq_start"], z["acq_end"]) == ("2023-03-15", "2023-04-03")
assert z["missing_relief"] == [] and z["missing_quality"] == []
assert len(z["grid_cells"]) == 2 * 10 * 2 # 10 mailles en x × 2 en y par dalle
def test_zone_quality_missing_cells():
from lidar_pipeline.export_pdf import zone_quality
bbox = (1054500.0, 6881500.0, 1055500.0, 6881600.0)
z = zone_quality(bbox, {}, [(1054, 6882)])
assert z["density_mean"] is None and z["acq_start"] is None
assert z["missing_quality"] == [(1054, 6882), (1055, 6882)]
assert z["missing_relief"] == [(1055, 6882)]
def _mediabox(pdf):
import re
m = re.search(rb"/MediaBox \[\s*0 0 ([\d.]+) ([\d.]+)\s*\]", pdf)
return float(m.group(1)), float(m.group(2))
def _cell_center_wgs84(col, row):
from lidar_pipeline.tiles import _transformer
lon, lat = _transformer("EPSG:2154", "EPSG:4326").transform(col * 1000 + 500, (row - 1) * 1000 + 500)
return lat, lon
def test_build_pdf_a4_landscape(tmp_path, monkeypatch):
from lidar_pipeline import index
from lidar_pipeline.export_pdf import build_pdf
from lidar_pipeline.quality import write_quality
monkeypatch.setattr(index, "PANEL_VIZ", None)
_dalle(tmp_path, 1054, 6882, px=200)
write_quality(tmp_path, "LHD_FXX_1054_6882_PTS_LAMB93_IGN69", _q(6.5))
lat, lon = _cell_center_wgs84(1054, 6882)
pdf, name = build_pdf(tmp_path, lat, lon, "A4", "paysage", 2000,
title="Prospection 🚀 bois", compress=False)
assert pdf.startswith(b"%PDF")
w, h = _mediabox(pdf)
assert abs(w - 841.89) < 0.5 and abs(h - 595.28) < 0.5
assert name.startswith("relief_1054.") and name.endswith("_1-2000.pdf")
for s in (b"Prospection ? bois", b"1:2 000", b"Densit", b"2023-03-15", b"LiDAR HD"):
assert s in pdf, s
def test_build_pdf_a3_portrait_size(tmp_path, monkeypatch):
from lidar_pipeline import index
from lidar_pipeline.export_pdf import build_pdf
monkeypatch.setattr(index, "PANEL_VIZ", None)
_dalle(tmp_path, 1054, 6882)
lat, lon = _cell_center_wgs84(1054, 6882)
pdf, _name = build_pdf(tmp_path, lat, lon, "A3", "portrait", 5000)
w, h = _mediabox(pdf)
assert abs(w - 841.89) < 0.5 and abs(h - 1190.55) < 0.5
def test_build_pdf_partial_zone_hatched(tmp_path, monkeypatch):
"""Zone à cheval sur le bord des données : planche produite, dalle absente listée."""
from lidar_pipeline import index
from lidar_pipeline.export_pdf import build_pdf
from lidar_pipeline.tiles import _transformer
monkeypatch.setattr(index, "PANEL_VIZ", None)
_dalle(tmp_path, 1054, 6882)
lon, lat = _transformer("EPSG:2154", "EPSG:4326").transform(1055000.0, 6881500.0)
pdf, _ = build_pdf(tmp_path, lat, lon, "A4", "paysage", 2000, compress=False)
assert pdf.startswith(b"%PDF") and b"1055_6882" in pdf
assert b"non renseign" in pdf
def test_build_pdf_no_data_raises(tmp_path):
import pytest
from lidar_pipeline.export_pdf import NoDataError, build_pdf
with pytest.raises(NoDataError):
build_pdf(tmp_path, 46.5, 2.5, "A4", "paysage", 2000)
def test_fmt_int():
from lidar_pipeline.export_pdf import _fmt_int
assert _fmt_int(2000) == "2 000" and _fmt_int(500) == "500"