Ajouter la géométrie de planche PDF, l'accès aux sources par emprise L93 et reportlab
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
119
lidar_pipeline/export_pdf.py
Normal file
119
lidar_pipeline/export_pdf.py
Normal file
@ -0,0 +1,119 @@
|
||||
"""Export PDF d'une zone : planche d'impression terrain du relief orienté.
|
||||
|
||||
Composée directement en Lambert 93 depuis les sources de la pyramide
|
||||
(tiles.py) — échelle exacte, quadrillage aligné sur les dalles — puis
|
||||
dessinée avec reportlab (texte, grille et légende vectoriels). Tourne dans
|
||||
l'image légère : Pillow + pyproj + reportlab, sans numpy.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
logger = logging.getLogger("lidar")
|
||||
|
||||
LAYER = "relief_oriente"
|
||||
PAPERS_MM = {"A4": (210.0, 297.0), "A3": (297.0, 420.0)}
|
||||
ORIENTS = ("portrait", "paysage")
|
||||
SCALES = (1000, 2000, 5000, 10000)
|
||||
DPI = {"A4": 300, "A3": 250} # borne la mémoire du Pi (~36 Mo en A3)
|
||||
NATIVE_RES_M = 0.2
|
||||
MARGIN_MM = 10.0 # bord non imprimable
|
||||
ANNOT_MM = 7.0 # bande des coordonnées autour de la carte
|
||||
PANEL_SIDE_MM = 64.0 # bandeau à droite (paysage)
|
||||
PANEL_BOTTOM_MM = 72.0 # bandeau en bas (portrait)
|
||||
_GRID_STEPS = {1000: 100, 2000: 100, 5000: 500, 10000: 1000}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Layout:
|
||||
"""Géométrie de la planche, en mm, origine en bas à gauche (reportlab)."""
|
||||
paper: str
|
||||
orient: str
|
||||
scale: int
|
||||
dpi: int
|
||||
page_w: float
|
||||
page_h: float
|
||||
map_x: float
|
||||
map_y: float
|
||||
map_w: float
|
||||
map_h: float
|
||||
panel_x: float
|
||||
panel_y: float
|
||||
panel_w: float
|
||||
panel_h: float
|
||||
|
||||
|
||||
def layout(paper, orient, scale):
|
||||
"""Géométrie d'une planche ; ValueError si un réglage est invalide."""
|
||||
if paper not in PAPERS_MM:
|
||||
raise ValueError(f"format inconnu : {paper} (A4 ou A3)")
|
||||
if orient not in ORIENTS:
|
||||
raise ValueError(f"orientation inconnue : {orient} (portrait ou paysage)")
|
||||
try:
|
||||
scale = int(scale)
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError(f"échelle invalide : {scale}") from None
|
||||
if scale not in SCALES:
|
||||
raise ValueError("échelle non proposée : 1:" + str(scale)
|
||||
+ " (1:1000, 1:2000, 1:5000 ou 1:10000)")
|
||||
w, h = PAPERS_MM[paper]
|
||||
if orient == "paysage":
|
||||
w, h = h, w
|
||||
inner = MARGIN_MM + ANNOT_MM
|
||||
if orient == "paysage":
|
||||
map_w = w - 2 * inner - PANEL_SIDE_MM
|
||||
map_h = h - 2 * inner
|
||||
return Layout(paper, orient, scale, DPI[paper], w, h, inner, inner, map_w, map_h,
|
||||
w - MARGIN_MM - PANEL_SIDE_MM, MARGIN_MM, PANEL_SIDE_MM, h - 2 * MARGIN_MM)
|
||||
map_w = w - 2 * inner
|
||||
map_h = h - 2 * inner - PANEL_BOTTOM_MM
|
||||
return Layout(paper, orient, scale, DPI[paper], w, h, inner, MARGIN_MM + PANEL_BOTTOM_MM + ANNOT_MM,
|
||||
map_w, map_h, MARGIN_MM, MARGIN_MM, w - 2 * MARGIN_MM, PANEL_BOTTOM_MM)
|
||||
|
||||
|
||||
def center_l93(lat, lon):
|
||||
"""Centre WGS84 → Lambert 93 ; ValueError si non fini."""
|
||||
from .tiles import wgs84_to_l93
|
||||
lat, lon = float(lat), float(lon)
|
||||
if not (math.isfinite(lat) and math.isfinite(lon)):
|
||||
raise ValueError("coordonnées du centre invalides")
|
||||
cx, cy = wgs84_to_l93(lon, lat)
|
||||
if not (math.isfinite(cx) and math.isfinite(cy)):
|
||||
raise ValueError("centre hors du domaine Lambert 93")
|
||||
return cx, cy
|
||||
|
||||
|
||||
def map_bbox(cx, cy, lay):
|
||||
"""Emprise terrain L93 de la zone carte (papier × échelle)."""
|
||||
half_w = lay.map_w / 1000.0 * lay.scale / 2
|
||||
half_h = lay.map_h / 1000.0 * lay.scale / 2
|
||||
return (cx - half_w, cy - half_h, cx + half_w, cy + half_h)
|
||||
|
||||
|
||||
def pixel_size(lay):
|
||||
"""Taille terrain d'un pixel imprimé (m), jamais plus fine que le natif."""
|
||||
return max(NATIVE_RES_M, lay.scale * 0.0254 / lay.dpi)
|
||||
|
||||
|
||||
def grid_step(scale):
|
||||
"""Pas du quadrillage L93 (m) selon l'échelle."""
|
||||
return _GRID_STEPS[int(scale)]
|
||||
|
||||
|
||||
def _to_wgs84(x, y):
|
||||
from .tiles import _transformer
|
||||
lon, lat = _transformer("EPSG:2154", "EPSG:4326").transform(x, y)
|
||||
return lat, lon
|
||||
|
||||
|
||||
def frame(lat, lon, paper, orient, scale):
|
||||
"""Cadre imprimable pour la carte : emprise L93 et coins WGS84 (NO, NE, SE, SO)."""
|
||||
lay = layout(paper, orient, scale)
|
||||
cx, cy = center_l93(lat, lon)
|
||||
b = map_bbox(cx, cy, lay)
|
||||
corners = [_to_wgs84(b[0], b[3]), _to_wgs84(b[2], b[3]),
|
||||
_to_wgs84(b[2], b[1]), _to_wgs84(b[0], b[1])]
|
||||
return {"cx": cx, "cy": cy, "bbox_l93": list(b),
|
||||
"corners": [[round(a, 7), round(o, 7)] for a, o in corners],
|
||||
"width_m": round(b[2] - b[0]), "height_m": round(b[3] - b[1])}
|
||||
Reference in New Issue
Block a user