Dessiner la planche PDF : quadrillage L93, légende, encart qualité et cartouche
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -305,3 +305,287 @@ def zone_quality(bbox, table, cells_with_relief):
|
||||
"missing_quality": missing_q,
|
||||
"grid_cells": grid_cells,
|
||||
}
|
||||
|
||||
|
||||
class NoDataError(Exception):
|
||||
"""Aucune dalle du relief orienté dans l'emprise demandée."""
|
||||
|
||||
|
||||
_JPEG_QUALITY = 90
|
||||
_SCALEBAR_STEPS = (10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000)
|
||||
|
||||
|
||||
def _fmt_int(n):
|
||||
return f"{int(n):,}".replace(",", " ")
|
||||
|
||||
|
||||
def _hex(rgb):
|
||||
return "#%02x%02x%02x" % tuple(rgb)
|
||||
|
||||
|
||||
def _convergence_deg(cx, cy):
|
||||
"""Angle (°) du nord géographique par rapport au nord du quadrillage L93."""
|
||||
lat0, lon0 = _to_wgs84(cx, cy)
|
||||
lat1, lon1 = _to_wgs84(cx, cy + 100.0)
|
||||
return math.degrees(math.atan2((lon1 - lon0) * math.cos(math.radians(lat0)), lat1 - lat0))
|
||||
|
||||
|
||||
def _panel_boxes(lay):
|
||||
"""Rectangles (x, y, w, h) mm du bandeau : légende, qualité, cartouche."""
|
||||
gap = 4.0
|
||||
if lay.orient == "paysage":
|
||||
h = (lay.panel_h - 2 * gap) / 3
|
||||
x, w = lay.panel_x, lay.panel_w
|
||||
top = lay.panel_y + lay.panel_h
|
||||
return [(x, top - h, w, h), (x, top - 2 * h - gap, w, h), (x, lay.panel_y, w, h)]
|
||||
w = (lay.panel_w - 2 * gap) / 3
|
||||
y, h = lay.panel_y, lay.panel_h
|
||||
return [(lay.panel_x, y, w, h), (lay.panel_x + w + gap, y, w, h),
|
||||
(lay.panel_x + 2 * (w + gap), y, w, h)]
|
||||
|
||||
|
||||
def _draw_grid(c, lay, bbox, mm):
|
||||
"""Quadrillage L93 + valeurs en marge + coins WGS84."""
|
||||
from reportlab.lib.colors import black
|
||||
step = grid_step(lay.scale)
|
||||
sx = lay.map_w / (bbox[2] - bbox[0])
|
||||
sy = lay.map_h / (bbox[3] - bbox[1])
|
||||
c.saveState()
|
||||
c.setStrokeColor(black); c.setStrokeAlpha(0.55); c.setLineWidth(0.3)
|
||||
c.setFont("Helvetica", 5.5); c.setFillColor(black)
|
||||
x = math.ceil(bbox[0] / step) * step
|
||||
while x <= bbox[2]:
|
||||
px = (lay.map_x + (x - bbox[0]) * sx) * mm
|
||||
c.line(px, lay.map_y * mm, px, (lay.map_y + lay.map_h) * mm)
|
||||
label = f"{x / 1000:.3f}".replace(".", ",")
|
||||
c.drawCentredString(px, (lay.map_y - 3.2) * mm, label)
|
||||
c.drawCentredString(px, (lay.map_y + lay.map_h + 1.4) * mm, label)
|
||||
x += step
|
||||
y = math.ceil(bbox[1] / step) * step
|
||||
while y <= bbox[3]:
|
||||
py = (lay.map_y + (y - bbox[1]) * sy) * mm
|
||||
c.line(lay.map_x * mm, py, (lay.map_x + lay.map_w) * mm, py)
|
||||
label = f"{y / 1000:.3f}".replace(".", ",")
|
||||
c.saveState(); c.translate((lay.map_x - 1.4) * mm, py); c.rotate(90)
|
||||
c.drawCentredString(0, 0, label); c.restoreState()
|
||||
c.saveState(); c.translate((lay.map_x + lay.map_w + 3.2) * mm, py); c.rotate(90)
|
||||
c.drawCentredString(0, 0, label); c.restoreState()
|
||||
y += step
|
||||
c.restoreState()
|
||||
c.setFont("Helvetica", 5.5)
|
||||
for (gx, gy), (px, py, align) in (
|
||||
((bbox[0], bbox[3]), (lay.map_x, lay.map_y + lay.map_h + 4.2, "l")),
|
||||
((bbox[2], bbox[3]), (lay.map_x + lay.map_w, lay.map_y + lay.map_h + 4.2, "r")),
|
||||
((bbox[0], bbox[1]), (lay.map_x, lay.map_y - 6.2, "l")),
|
||||
((bbox[2], bbox[1]), (lay.map_x + lay.map_w, lay.map_y - 6.2, "r"))):
|
||||
lat, lon = _to_wgs84(gx, gy)
|
||||
txt = f"{lat:.5f} N {lon:.5f} E"
|
||||
(c.drawString if align == "l" else c.drawRightString)(px * mm, py * mm, txt)
|
||||
c.setFont("Helvetica", 5.5)
|
||||
c.drawString(lay.map_x * mm, (lay.map_y + lay.map_h + 6.2) * mm,
|
||||
_pdf_text(f"Quadrillage Lambert 93 (km), pas {_fmt_int(step)} m - coins en WGS84"))
|
||||
|
||||
|
||||
def _draw_legend(c, box, mm):
|
||||
"""Barre de clarté + rose des orientations + texte de VIZ_LEGENDS."""
|
||||
from reportlab.lib.colors import HexColor, black, white
|
||||
from .index import VIZ_LEGENDS
|
||||
x, y, w, h = box
|
||||
c.setFillColor(black); c.setFont("Helvetica-Bold", 8)
|
||||
c.drawString(x * mm, (y + h - 4) * mm, _pdf_text("Légende - relief orienté"))
|
||||
# Barre de clarté (L* 20 → 90, gris neutre)
|
||||
bx, by, bw, bh = x, y + h - 13, min(w, 55.0), 4.0
|
||||
n = 40
|
||||
for k in range(n):
|
||||
L = 20 + 70 * k / (n - 1)
|
||||
c.setFillColor(HexColor(_hex(lab_to_rgb(L, 0, 0))))
|
||||
c.rect((bx + bw * k / n) * mm, by * mm, (bw / n + 0.05) * mm, bh * mm, stroke=0, fill=1)
|
||||
c.setFillColor(black); c.setFont("Helvetica", 6)
|
||||
c.drawString(bx * mm, (by - 2.8) * mm, _pdf_text("creux, fossé"))
|
||||
c.drawRightString((bx + bw) * mm, (by - 2.8) * mm, _pdf_text("bosse, crête"))
|
||||
c.drawString(bx * mm, (by + bh + 0.8) * mm, _pdf_text("Clarté = micro-relief"))
|
||||
# Rose des orientations (couleur = orientation de la pente)
|
||||
r_out, r_in = 11.0, 5.0
|
||||
rcx, rcy = x + r_out + 2, by - 5 - r_out - 2
|
||||
for deg in range(0, 360, 5):
|
||||
c.setFillColor(HexColor(_hex(rose_color(deg))))
|
||||
start = 90 - deg - 2.5
|
||||
c.wedge((rcx - r_out) * mm, (rcy - r_out) * mm, (rcx + r_out) * mm, (rcy + r_out) * mm,
|
||||
start, 5.2, stroke=0, fill=1)
|
||||
c.setFillColor(white)
|
||||
c.circle(rcx * mm, rcy * mm, r_in * mm, stroke=0, fill=1)
|
||||
c.setFillColor(black); c.setFont("Helvetica-Bold", 5.5)
|
||||
for label, deg in (("N", 0), ("E", 90), ("S", 180), ("O", 270)):
|
||||
a = math.radians(deg)
|
||||
c.drawCentredString((rcx + (r_out + 2) * math.sin(a)) * mm,
|
||||
(rcy + (r_out + 2) * math.cos(a) - 0.8) * mm, label)
|
||||
c.setFont("Helvetica", 6)
|
||||
c.drawString((rcx + r_out + 5) * mm, (rcy + 2) * mm, _pdf_text("Teinte = orientation"))
|
||||
c.drawString((rcx + r_out + 5) * mm, (rcy - 1) * mm, _pdf_text("de la pente"))
|
||||
# Texte de légende
|
||||
ty = rcy - r_out - 6
|
||||
c.setFont("Helvetica", 5.8)
|
||||
for line in VIZ_LEGENDS[LAYER]["legend"].split("\n"):
|
||||
for part in line.split(" | "):
|
||||
if ty < y + 1:
|
||||
return
|
||||
c.drawString(x * mm, ty * mm, _pdf_text(part))
|
||||
ty -= 2.9
|
||||
|
||||
|
||||
def _draw_quality(c, box, bbox, zq, mm):
|
||||
"""Encart qualité : miniature de densité sol + chiffres clés."""
|
||||
from reportlab.lib.colors import HexColor, black, Color
|
||||
x, y, w, h = box
|
||||
c.setFillColor(black); c.setFont("Helvetica-Bold", 8)
|
||||
c.drawString(x * mm, (y + h - 4) * mm, _pdf_text("Qualité des données"))
|
||||
# Miniature : emprise de la zone, mailles 50 m colorées par classe
|
||||
avail_w, avail_h = w * 0.45, h - 10
|
||||
k = min(avail_w / (bbox[2] - bbox[0]), avail_h / (bbox[3] - bbox[1]))
|
||||
mw, mh = (bbox[2] - bbox[0]) * k, (bbox[3] - bbox[1]) * k
|
||||
mx, my = x, y + h - 7 - mh
|
||||
c.setFillColor(Color(0.85, 0.85, 0.85))
|
||||
c.rect(mx * mm, my * mm, mw * mm, mh * mm, stroke=0, fill=1) # gris = non renseigné
|
||||
for gx0, gy0, gx1, gy1, v in zq["grid_cells"]:
|
||||
x0, x1 = max(gx0, bbox[0]), min(gx1, bbox[2])
|
||||
y0, y1 = max(gy0, bbox[1]), min(gy1, bbox[3])
|
||||
c.setFillColor(HexColor(density_color(v)))
|
||||
c.rect((mx + (x0 - bbox[0]) * k) * mm, (my + (y0 - bbox[1]) * k) * mm,
|
||||
((x1 - x0) * k + 0.02) * mm, ((y1 - y0) * k + 0.02) * mm, stroke=0, fill=1)
|
||||
c.setStrokeColor(black); c.setLineWidth(0.4)
|
||||
c.rect(mx * mm, my * mm, mw * mm, mh * mm, stroke=1, fill=0)
|
||||
# Classes
|
||||
lx, ly = x + mw + 3, y + h - 8
|
||||
c.setFont("Helvetica", 5.8)
|
||||
c.drawString(lx * mm, ly * mm, _pdf_text("Points sol / m²"))
|
||||
for low, label, color in DENSITY_CLASSES:
|
||||
ly -= 3.2
|
||||
c.setFillColor(HexColor(color)); c.rect(lx * mm, ly * mm, 3 * mm, 2.2 * mm, stroke=0, fill=1)
|
||||
c.setFillColor(black); c.drawString((lx + 4) * mm, (ly + 0.4) * mm, _pdf_text(label))
|
||||
ly -= 3.2
|
||||
c.setFillColor(Color(0.85, 0.85, 0.85)); c.rect(lx * mm, ly * mm, 3 * mm, 2.2 * mm, stroke=0, fill=1)
|
||||
c.setFillColor(black); c.drawString((lx + 4) * mm, (ly + 0.4) * mm, _pdf_text("non renseigné"))
|
||||
# Chiffres clés
|
||||
lines = []
|
||||
if zq["density_mean"] is None:
|
||||
lines.append("Densité sol : non renseigné")
|
||||
else:
|
||||
lines.append(f"Densité sol moyenne : {zq['density_mean']:.1f} pts/m²".replace(".", ","))
|
||||
lines.append(f"Maille la plus faible (50 m) : {zq['density_min']:.1f} pts/m²".replace(".", ","))
|
||||
lines.append(f"Surface sans point sol (interpolée) : {zq['empty_fraction'] * 100:.0f} %")
|
||||
if zq["acq_start"] is None:
|
||||
lines.append("Acquisition : non renseigné")
|
||||
else:
|
||||
period = zq["acq_start"] if zq["acq_start"] == zq["acq_end"] else \
|
||||
f"{zq['acq_start']} au {zq['acq_end']}"
|
||||
label = "Acquisition" if zq["acq_sources"] == {"gps"} else "Date de production du fichier"
|
||||
lines.append(f"{label} : {period}")
|
||||
if zq["missing_relief"]:
|
||||
lines.append("Sans relief : " + ", ".join(f"{c_}_{r_}" for c_, r_ in zq["missing_relief"]))
|
||||
if zq["missing_quality"]:
|
||||
lines.append("Qualité non renseignée : " + ", ".join(
|
||||
f"{c_}_{r_}" for c_, r_ in zq["missing_quality"]))
|
||||
ty = min(my, ly) - 3.5
|
||||
c.setFont("Helvetica", 5.8)
|
||||
for line in lines:
|
||||
if ty < y + 1:
|
||||
break
|
||||
c.drawString(x * mm, ty * mm, _pdf_text(line))
|
||||
ty -= 2.9
|
||||
|
||||
|
||||
def _draw_cartouche(c, box, lay, bbox, cx, cy, title, now, mm):
|
||||
"""Titre, échelle graphique et numérique, nord, date, source."""
|
||||
from reportlab.lib.colors import black, white
|
||||
x, y, w, h = box
|
||||
c.setFillColor(black); c.setFont("Helvetica-Bold", 10)
|
||||
c.drawString(x * mm, (y + h - 5) * mm, _pdf_text(title)[:80])
|
||||
c.setFont("Helvetica", 7)
|
||||
c.drawString(x * mm, (y + h - 9) * mm,
|
||||
_pdf_text(f"Échelle 1:{_fmt_int(lay.scale)} - {lay.paper} {lay.orient} - {lay.dpi} dpi"))
|
||||
# Échelle graphique : longueur ronde ≤ 40 % de la largeur du bloc
|
||||
max_m = w * 0.4 / 1000 * lay.scale
|
||||
length = max((s for s in _SCALEBAR_STEPS if s <= max_m), default=_SCALEBAR_STEPS[0])
|
||||
bar_mm = length / lay.scale * 1000
|
||||
bx, by = x, y + h - 15
|
||||
for k in range(4):
|
||||
c.setFillColor(black if k % 2 == 0 else white)
|
||||
c.rect((bx + bar_mm * k / 4) * mm, by * mm, bar_mm / 4 * mm, 1.6 * mm, stroke=1, fill=1)
|
||||
c.setFillColor(black); c.setFont("Helvetica", 6)
|
||||
c.drawString(bx * mm, (by - 2.8) * mm, "0")
|
||||
c.drawRightString((bx + bar_mm) * mm, (by - 2.8) * mm, f"{_fmt_int(length)} m")
|
||||
# Flèche du nord géographique (la carte est orientée nord du quadrillage)
|
||||
gamma = _convergence_deg(cx, cy)
|
||||
ax, ay = x + w - 8, y + h - 12
|
||||
c.saveState(); c.translate(ax * mm, ay * mm); c.rotate(-gamma)
|
||||
p = c.beginPath(); p.moveTo(0, 5 * mm); p.lineTo(-1.8 * mm, -3 * mm); p.lineTo(0, -1.5 * mm)
|
||||
p.lineTo(1.8 * mm, -3 * mm); p.close()
|
||||
c.drawPath(p, stroke=0, fill=1)
|
||||
c.setFont("Helvetica-Bold", 6); c.drawCentredString(0, 6 * mm, "N")
|
||||
c.restoreState()
|
||||
c.setFont("Helvetica", 5.5)
|
||||
c.drawRightString((x + w) * mm, (ay - 6) * mm,
|
||||
_pdf_text(f"convergence L93 {gamma:+.2f}°".replace(".", ",")))
|
||||
ty = by - 7
|
||||
c.setFont("Helvetica", 6)
|
||||
for line in (f"Centre L93 : X {_fmt_int(round(cx))} m Y {_fmt_int(round(cy))} m",
|
||||
f"Zone : {_fmt_int(round(bbox[2] - bbox[0]))} x {_fmt_int(round(bbox[3] - bbox[1]))} m",
|
||||
f"Exporté le {now:%d/%m/%Y %H:%M}",
|
||||
"Source : LiDAR HD (c) IGN - rendu lidar_rendu"):
|
||||
if ty < y + 1:
|
||||
break
|
||||
c.drawString(x * mm, ty * mm, _pdf_text(line))
|
||||
ty -= 3.0
|
||||
|
||||
|
||||
def build_pdf(output_dir, lat, lon, paper="A4", orient="paysage", scale=2000,
|
||||
title=None, now=None, compress=True):
|
||||
"""Planche PDF d'une zone. Returns (octets PDF, nom de fichier).
|
||||
|
||||
Raises:
|
||||
ValueError: réglage invalide.
|
||||
NoDataError: aucune dalle du relief dans l'emprise.
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.pdfgen import canvas as rl_canvas
|
||||
from io import BytesIO
|
||||
from .quality import load_quality_table
|
||||
|
||||
lay = layout(paper, orient, scale)
|
||||
cx, cy = center_l93(lat, lon)
|
||||
bbox = map_bbox(cx, cy, lay)
|
||||
img, _mask, cells = compose_l93(output_dir, bbox, pixel_size(lay))
|
||||
if not cells:
|
||||
raise NoDataError("aucune dalle du relief orienté dans cette zone")
|
||||
now = now or datetime.now()
|
||||
zq = zone_quality(bbox, load_quality_table(output_dir), cells)
|
||||
title = (title or "").strip()[:120] or \
|
||||
"Relief orienté - " + ", ".join(f"{c_}_{r_}" for c_, r_ in cells[:4]) + \
|
||||
(" ..." if len(cells) > 4 else "")
|
||||
|
||||
buf = BytesIO()
|
||||
c = rl_canvas.Canvas(buf, pagesize=(lay.page_w * mm, lay.page_h * mm),
|
||||
pageCompression=1 if compress else 0)
|
||||
c.setTitle(_pdf_text(title)); c.setAuthor("lidar_rendu")
|
||||
# Image carte en JPEG (incorporée telle quelle : PDF léger)
|
||||
fd, jpg = tempfile.mkstemp(suffix=".jpg")
|
||||
os.close(fd)
|
||||
try:
|
||||
img.save(jpg, format="JPEG", quality=_JPEG_QUALITY, subsampling=0)
|
||||
del img
|
||||
c.drawImage(jpg, lay.map_x * mm, lay.map_y * mm, lay.map_w * mm, lay.map_h * mm)
|
||||
finally:
|
||||
os.unlink(jpg)
|
||||
c.setLineWidth(0.6)
|
||||
c.rect(lay.map_x * mm, lay.map_y * mm, lay.map_w * mm, lay.map_h * mm, stroke=1, fill=0)
|
||||
_draw_grid(c, lay, bbox, mm)
|
||||
legend_box, quality_box, cart_box = _panel_boxes(lay)
|
||||
_draw_legend(c, legend_box, mm)
|
||||
_draw_quality(c, quality_box, bbox, zq, mm)
|
||||
_draw_cartouche(c, cart_box, lay, bbox, cx, cy, title, now, mm)
|
||||
c.showPage(); c.save()
|
||||
name = f"relief_{cx / 1000:.3f}_{cy / 1000:.3f}_1-{lay.scale}.pdf"
|
||||
return buf.getvalue(), name
|
||||
|
||||
Reference in New Issue
Block a user