Complete GeoIntel visual system and portfolio case study
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from reportlab.lib.colors import HexColor, white
|
||||
from reportlab.lib.enums import TA_LEFT
|
||||
from reportlab.lib.pagesizes import A4, landscape
|
||||
from reportlab.lib.styles import ParagraphStyle
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.platypus import Paragraph
|
||||
from PIL import Image
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUTPUT = ROOT / "output" / "pdf" / "geointel-case-study.pdf"
|
||||
ASSETS = ROOT / "docs" / "assets" / "portfolio"
|
||||
PUBLIC = ROOT / "frontend" / "public" / "portfolio"
|
||||
|
||||
PAGE = landscape(A4)
|
||||
W, H = PAGE
|
||||
INK = HexColor("#082f2b")
|
||||
TEAL = HexColor("#0d7f70")
|
||||
MINT = HexColor("#7ce0c8")
|
||||
PALE = HexColor("#edf8f5")
|
||||
MUTED = HexColor("#667b76")
|
||||
AMBER = HexColor("#d9a94f")
|
||||
|
||||
|
||||
def register_fonts() -> tuple[str, str]:
|
||||
font_root = ROOT / "frontend" / "node_modules" / "@fontsource" / "manrope" / "files"
|
||||
regular = font_root / "manrope-latin-500-normal.woff"
|
||||
bold = font_root / "manrope-latin-700-normal.woff"
|
||||
# ReportLab does not load WOFF. Use stable system fonts when TTF is absent.
|
||||
if regular.suffix.lower() == ".ttf" and regular.exists():
|
||||
pdfmetrics.registerFont(TTFont("GeoIntel", str(regular)))
|
||||
pdfmetrics.registerFont(TTFont("GeoIntelBold", str(bold)))
|
||||
return "GeoIntel", "GeoIntelBold"
|
||||
return "Helvetica", "Helvetica-Bold"
|
||||
|
||||
|
||||
FONT, FONT_BOLD = register_fonts()
|
||||
|
||||
|
||||
def image_cover(c: canvas.Canvas, path: Path, x: float, y: float, w: float, h: float) -> None:
|
||||
with Image.open(path) as image:
|
||||
iw, ih = image.size
|
||||
scale = max(w / iw, h / ih)
|
||||
draw_w, draw_h = iw * scale, ih * scale
|
||||
c.saveState()
|
||||
clip = c.beginPath()
|
||||
clip.rect(x, y, w, h)
|
||||
c.clipPath(clip, stroke=0, fill=0)
|
||||
c.drawImage(ImageReader(str(path)), x + (w - draw_w) / 2, y + (h - draw_h) / 2, draw_w, draw_h, mask="auto")
|
||||
c.restoreState()
|
||||
|
||||
|
||||
def title(c: canvas.Canvas, kicker: str, heading: str, sub: str | None = None) -> None:
|
||||
c.setFillColor(TEAL)
|
||||
c.setFont(FONT_BOLD, 8)
|
||||
c.drawString(42, H - 44, kicker.upper())
|
||||
c.setFillColor(INK)
|
||||
c.setFont(FONT_BOLD, 25)
|
||||
c.drawString(42, H - 78, heading)
|
||||
if sub:
|
||||
c.setFillColor(MUTED)
|
||||
c.setFont(FONT, 9.5)
|
||||
c.drawString(42, H - 99, sub)
|
||||
|
||||
|
||||
def footer(c: canvas.Canvas, number: int) -> None:
|
||||
c.setStrokeColor(HexColor("#d7e8e3"))
|
||||
c.line(42, 27, W - 42, 27)
|
||||
c.setFillColor(MUTED)
|
||||
c.setFont(FONT, 7)
|
||||
c.drawString(42, 14, "GeoIntel - evidence-first GeoAI for Belgium and the Belgian North Sea")
|
||||
c.drawRightString(W - 42, 14, f"{number:02d}")
|
||||
|
||||
|
||||
def paragraph(c: canvas.Canvas, text: str, x: float, y: float, w: float, size: float = 10, color=INK, leading: float | None = None) -> float:
|
||||
style = ParagraphStyle("body", fontName=FONT, fontSize=size, leading=leading or size * 1.45, textColor=color, alignment=TA_LEFT)
|
||||
p = Paragraph(text, style)
|
||||
_, ph = p.wrap(w, H)
|
||||
p.drawOn(c, x, y - ph)
|
||||
return y - ph
|
||||
|
||||
|
||||
def metric(c: canvas.Canvas, x: float, y: float, value: str, label: str, width: float) -> None:
|
||||
c.setFillColor(PALE)
|
||||
c.roundRect(x, y, width, 55, 9, fill=1, stroke=0)
|
||||
c.setFillColor(TEAL)
|
||||
c.setFont(FONT_BOLD, 18)
|
||||
c.drawString(x + 13, y + 27, value)
|
||||
c.setFillColor(MUTED)
|
||||
c.setFont(FONT_BOLD, 7)
|
||||
c.drawString(x + 13, y + 12, label.upper())
|
||||
|
||||
|
||||
def draw_cover(c: canvas.Canvas) -> None:
|
||||
image_cover(c, PUBLIC / "geointel-dark-case-study-cover.png", 0, 0, W, H)
|
||||
c.setFillColor(HexColor("#031916"))
|
||||
c.rect(0, 0, W * 0.55, H, fill=1, stroke=0)
|
||||
c.setFillColor(MINT)
|
||||
c.setFont(FONT_BOLD, 9)
|
||||
c.drawString(48, H - 72, "PORTFOLIO CASE STUDY / 2026")
|
||||
c.setFillColor(white)
|
||||
c.setFont(FONT_BOLD, 35)
|
||||
c.drawString(48, H - 135, "GeoIntel")
|
||||
c.setFont(FONT_BOLD, 21)
|
||||
c.drawString(48, H - 171, "Van kaartlaag naar")
|
||||
c.setFillColor(MINT)
|
||||
c.drawString(48, H - 198, "aantoonbaar inzicht.")
|
||||
paragraph(c, "Een kaartgerichte GeoAI-workbench waarin officiële bronnen, GIS, PyTorch en kwaliteitsbewijs één reproduceerbare keten vormen.", 48, H - 228, 315, 11, HexColor("#c6ddd7"), 17)
|
||||
c.setFillColor(HexColor("#173b36"))
|
||||
c.roundRect(48, 47, 290, 48, 9, fill=1, stroke=0)
|
||||
c.setFillColor(white)
|
||||
c.setFont(FONT_BOLD, 8)
|
||||
c.drawString(62, 74, "BELGIË + BELGISCHE NOORDZEE")
|
||||
c.setFillColor(HexColor("#9ec8bd"))
|
||||
c.setFont(FONT, 7.5)
|
||||
c.drawString(62, 59, "React · FastAPI · PostGIS · PyTorch · NVIDIA CUDA")
|
||||
|
||||
|
||||
def draw_challenge(c: canvas.Canvas) -> None:
|
||||
c.setFillColor(white); c.rect(0, 0, W, H, fill=1, stroke=0)
|
||||
title(c, "01 / Productvraag", "Niet alleen zien, maar kunnen verdedigen")
|
||||
y = paragraph(c, "Geoportalen tonen vaak lagen en losse metingen. GeoIntel maakt van bron, gebied, analyse en bewijs één operationele workflow.", 42, H - 128, 330, 13, INK, 19)
|
||||
cards = [
|
||||
("01", "Context behouden", "Autoriteit, meetmoment, CRS en dekking blijven zichtbaar."),
|
||||
("02", "AI begrenzen", "Modelversie, confidence en validatiescope reizen mee."),
|
||||
("03", "Kwaliteit bewijzen", "Objectbewijs en fouten zijn vóór export inspecteerbaar."),
|
||||
]
|
||||
for index, (num, head, body) in enumerate(cards):
|
||||
cy = y - 42 - index * 78
|
||||
c.setFillColor(PALE); c.roundRect(42, cy - 54, 330, 64, 10, fill=1, stroke=0)
|
||||
c.setFillColor(TEAL); c.setFont(FONT_BOLD, 8); c.drawString(56, cy - 6, num)
|
||||
c.setFillColor(INK); c.setFont(FONT_BOLD, 11); c.drawString(86, cy - 7, head)
|
||||
paragraph(c, body, 86, cy - 18, 266, 8.2, MUTED, 12)
|
||||
image_cover(c, ASSETS / "geointel-landing-hero.png", 410, 53, 385, 415)
|
||||
footer(c, 2)
|
||||
|
||||
|
||||
def draw_experience(c: canvas.Canvas) -> None:
|
||||
c.setFillColor(HexColor("#f5faf8")); c.rect(0, 0, W, H, fill=1, stroke=0)
|
||||
title(c, "02 / Experience design", "De kaart blijft het werkblad", "Rustige hiërarchie, zichtbare herkomst en bewijs op het moment dat het nodig is.")
|
||||
image_cover(c, ASSETS / "geointel-workbench-map.png", 42, 73, 485, 360)
|
||||
image_cover(c, ASSETS / "geointel-workbench-quality.png", 547, 73, 248, 360)
|
||||
metric(c, 42, 440, "1", "ruimtelijke context", 160)
|
||||
metric(c, 214, 440, "7+", "bronfamilies", 160)
|
||||
metric(c, 386, 440, "QA", "vóór export", 160)
|
||||
footer(c, 3)
|
||||
|
||||
|
||||
def draw_ai(c: canvas.Canvas) -> None:
|
||||
c.setFillColor(INK); c.rect(0, 0, W, H, fill=1, stroke=0)
|
||||
c.setFillColor(MINT); c.setFont(FONT_BOLD, 8); c.drawString(42, H - 44, "03 / PYTORCH + NVIDIA")
|
||||
c.setFillColor(white); c.setFont(FONT_BOLD, 25); c.drawString(42, H - 78, "Van bronpixel naar gecontroleerde detectie")
|
||||
c.setFillColor(HexColor("#b8d4cd")); c.setFont(FONT, 9); c.drawString(42, H - 99, "Elke schakel bewaart de context die nodig is om het resultaat opnieuw te beoordelen.")
|
||||
stages = [
|
||||
("01", "Orthofoto", "CRS + dekking"),
|
||||
("02", "Beeldtegels", "overlap + tile-id"),
|
||||
("03", "NVIDIA GPU", "CUDA + model"),
|
||||
("04", "Detecties", "confidence + geometrie"),
|
||||
("05", "QA-bewijs", "precision + IoU"),
|
||||
]
|
||||
x0, gap, bw = 42, 13, 139
|
||||
c.setStrokeColor(HexColor("#4bb99f")); c.setLineWidth(2); c.line(95, 285, 746, 285)
|
||||
for i, (num, head, body) in enumerate(stages):
|
||||
x = x0 + i * (bw + gap)
|
||||
c.setFillColor(HexColor("#12443d")); c.roundRect(x, 205, bw, 160, 12, fill=1, stroke=0)
|
||||
c.setFillColor(MINT); c.setFont(FONT_BOLD, 8); c.drawString(x + 14, 340, num)
|
||||
c.setFillColor(white); c.setFont(FONT_BOLD, 10); c.drawString(x + 14, 295, head)
|
||||
c.setFillColor(HexColor("#9fc9bf")); c.setFont(FONT, 7.4); c.drawString(x + 14, 275, body)
|
||||
c.setFillColor(MINT if i < 3 else AMBER); c.circle(x + bw / 2, 230, 6, fill=1, stroke=0)
|
||||
paragraph(c, "Fail-closed runtime: wanneer CUDA vereist maar niet beschikbaar is, of wanneer een lokaal model ontbreekt, toont GeoIntel een expliciete niet-geconfigureerde toestand. Productie-AI wordt nooit gesimuleerd.", 42, 166, 735, 11, HexColor("#d7ebe6"), 17)
|
||||
footer(c, 4)
|
||||
|
||||
|
||||
def draw_architecture(c: canvas.Canvas) -> None:
|
||||
c.setFillColor(white); c.rect(0, 0, W, H, fill=1, stroke=0)
|
||||
title(c, "04 / Architectuur", "Eén keten, expliciete verantwoordelijkheden")
|
||||
nodes = [
|
||||
(55, 270, 130, 66, "React + MapLibre", "kaartwerkruimte"),
|
||||
(220, 270, 130, 66, "FastAPI", "contractlaag"),
|
||||
(385, 355, 150, 66, "GeoPandas / Rasterio", "ruimtelijke verwerking"),
|
||||
(385, 185, 150, 66, "Redis + RQ", "hervatbare jobs"),
|
||||
(570, 270, 150, 66, "PyTorch + CUDA", "beeldanalyse"),
|
||||
(570, 90, 150, 66, "PostGIS + artifacts", "resultaat + provenance"),
|
||||
]
|
||||
for x, y, w, h, head, body in nodes:
|
||||
c.setFillColor(PALE if "PyTorch" not in head else HexColor("#0d5047")); c.roundRect(x, y, w, h, 10, fill=1, stroke=0)
|
||||
c.setFillColor(white if "PyTorch" in head else INK); c.setFont(FONT_BOLD, 9); c.drawString(x + 12, y + 39, head)
|
||||
c.setFillColor(HexColor("#b9d8d0") if "PyTorch" in head else MUTED); c.setFont(FONT, 7.2); c.drawString(x + 12, y + 21, body)
|
||||
c.setStrokeColor(TEAL); c.setLineWidth(1.5)
|
||||
arrows = [((185,303),(220,303)),((350,303),(385,388)),((350,303),(385,218)),((535,388),(645,336)),((535,218),(570,303)),((645,270),(645,156))]
|
||||
for (x1,y1),(x2,y2) in arrows:
|
||||
c.line(x1,y1,x2,y2); c.circle(x2,y2,2.5,fill=1,stroke=0)
|
||||
paragraph(c, "PostGIS bewaart querybare geometrie en lifecycle-records. Grote raster-, mask- en modelbestanden blijven versioned artifacts; de database bewaart hun identiteit, checksum en provenance.", 55, 150, 460, 9.5, MUTED, 15)
|
||||
footer(c, 5)
|
||||
|
||||
|
||||
def draw_outcomes(c: canvas.Canvas) -> None:
|
||||
image_cover(c, PUBLIC / "geointel-dark-case-study-cover.png", 0, 0, W, H)
|
||||
c.setFillColor(HexColor("#041d19")); c.rect(0, 0, W, H, fill=1, stroke=0)
|
||||
c.setFillColor(MINT); c.setFont(FONT_BOLD, 8); c.drawString(42, H - 44, "05 / RESULTAAT")
|
||||
c.setFillColor(white); c.setFont(FONT_BOLD, 27); c.drawString(42, H - 82, "Een professionele GeoAI-workbench")
|
||||
c.setFillColor(HexColor("#c0dad4")); c.setFont(FONT, 10); c.drawString(42, H - 104, "Nationaal/maritiem van scope, lokaal controleerbaar in bewijs.")
|
||||
outcomes = [
|
||||
("Map-first", "De gebruiker blijft in dezelfde ruimtelijke context."),
|
||||
("Evidence-first", "QA en provenance zijn onderdeel van het resultaat."),
|
||||
("GPU-ready", "NVIDIA CUDA is expliciet geconfigureerd en gevalideerd."),
|
||||
("Accessible motion", "Animatie verduidelijkt toestand en respecteert reduced motion."),
|
||||
]
|
||||
for i, (head, body) in enumerate(outcomes):
|
||||
x = 42 + (i % 2) * 375
|
||||
y = 330 - (i // 2) * 105
|
||||
c.setFillColor(HexColor("#123f38")); c.roundRect(x, y, 350, 78, 11, fill=1, stroke=0)
|
||||
c.setFillColor(MINT); c.setFont(FONT_BOLD, 11); c.drawString(x + 16, y + 48, head)
|
||||
paragraph(c, body, x + 16, y + 38, 316, 8, HexColor("#b8d2cc"), 12)
|
||||
c.setFillColor(white); c.setFont(FONT_BOLD, 11); c.drawString(42, 58, "GeoIntel · Jens / ITWorx.tech")
|
||||
c.setFillColor(HexColor("#8fb9af")); c.setFont(FONT, 8); c.drawString(42, 42, "Belgium and the Belgian North Sea · v1.0.0")
|
||||
|
||||
|
||||
def build() -> None:
|
||||
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
c = canvas.Canvas(str(OUTPUT), pagesize=PAGE, pageCompression=1)
|
||||
c.setTitle("GeoIntel - Evidence-first GeoAI case study")
|
||||
c.setAuthor("Jens / ITWorx.tech")
|
||||
c.setSubject("Portfolio case study for the GeoIntel Belgium and Belgian North Sea workbench")
|
||||
for draw in (draw_cover, draw_challenge, draw_experience, draw_ai, draw_architecture, draw_outcomes):
|
||||
draw(c)
|
||||
c.showPage()
|
||||
c.save()
|
||||
print(OUTPUT)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
Reference in New Issue
Block a user