Complete GeoIntel visual system and portfolio case study
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Jens
2026-07-28 11:10:35 +02:00
parent 61e2505940
commit 0cd84fa5c5
19 changed files with 784 additions and 6 deletions
+240
View File
@@ -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()