codex/past-exam-of-the-year-1-of-the-computer-science-course-of-the-university-of-oxford/2016/imperative-main-gym-singleton.py

past-exam-of-the-year-1-of-the-computer-science-course-of-the-university-of-oxford/2016/imperative-main-gym-singleton.py
"""UML class diagram; Python 3.14.4, matplotlib 3.10.7, numpy 2.3.5.
Run from the desired output directory. Writes its basename plus .png to CWD.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle, FancyArrowPatch

EXTENDED = True
fig, ax = plt.subplots(figsize=(14, 10 if EXTENDED else 8), dpi=100)
fig.patch.set_facecolor("white")
ax.set_facecolor("white")
ax.set_xlim(0, 14)
ax.set_ylim(0, 10 if EXTENDED else 8)
ax.axis("off")

def box(x, y, w, title, rows, h=None, tint="#eef5fc"):
    h = h or (0.72 + 0.31 * len(rows))
    ax.add_patch(Rectangle((x-w/2, y-h/2), w, h, facecolor="white", edgecolor="#263547", lw=1.3, zorder=2))
    ax.add_patch(Rectangle((x-w/2, y+h/2-0.58), w, 0.58, facecolor=tint, edgecolor="#263547", lw=1.1, zorder=2))
    ax.text(x, y+h/2-0.29, title, ha="center", va="center", fontsize=11, weight="bold", zorder=3)
    for j, row in enumerate(rows):
        ax.text(x-w/2+0.12, y+h/2-0.79-0.31*j, row, ha="left", va="center", fontsize=9, family="DejaVu Sans Mono", zorder=3)
    return (x-w/2, x+w/2, y-h/2, y+h/2)

def arrow(points, realization=False, label=None, label_xy=None):
    for a,b in zip(points[:-2],points[1:-1]):
        ax.plot([a[0],b[0]],[a[1],b[1]], color="#46566b", lw=1.1, ls="--" if realization else "-", zorder=1)
    patch=FancyArrowPatch(points[-2],points[-1], arrowstyle="-|>" if realization else "->", mutation_scale=15, lw=1.2, linestyle="--" if realization else "-", edgecolor="#46566b", facecolor="white", zorder=1)
    ax.add_patch(patch)
    if label:
        ax.text(*label_xy, label, ha="center", va="center", fontsize=9, color="#253a55", bbox=dict(facecolor="white",edgecolor="none",pad=1),zorder=4)

if not EXTENDED:
    eq=box(3.1,5.3,5.8,"«trait» Equipment",[
      "+ name:String; age:Int; weight:Int",
      "- _time:Int; _heartRate:Int",
      "- _functionalities:List[Functionality]",
      "+ time:Int; time_=(Int):Unit",
      "+ heartRate:Int; heartRate_=(Int):Unit",
      "+ addFunctionality(f):Unit",
      "+ deleteFunctionality(f):List[Functionality]",
      "+ notifyFunctionalities():Unit",
      "+ updateUserMeasures(Int,Int):Unit"])
    fu=box(10.1,6.2,3.6,"«trait» Functionality",["+ update():Unit"])
    gy=box(3.1,1.65,5.8,"GymEquipment",[
      "+ name:String; age:Int; weight:Int",
      "+ updateUserMeasures(Int,Int):Unit"])
    pe=box(8.0,2.3,3.4,"Performance",["- e:GymEquipment","+ update():Unit"])
    pr=box(11.95,2.3,3.4,"Prediction",["- e:GymEquipment","+ update():Unit"])
    arrow([(3.1,gy[3]),(3.1,eq[2])],True)
    arrow([(8,pe[3]),(8,4.9),(9.3,fu[2])],True)
    arrow([(11.95,pr[3]),(11.95,4.9),(10.9,fu[2])],True)
    arrow([(eq[1],6.2),(fu[0],6.2)],label="registered observers: 0..*",label_xy=(7.1,6.6))
    arrow([(pe[0],2.3),(6.1,2.3),(gy[1],2.0)],label="e: 1",label_xy=(6.3,1.95))
    arrow([(11.95,pr[2]),(11.95,.45),(3.1,.45),(3.1,gy[2])],label="e: 1",label_xy=(8.6,.45))
    ax.text(7,7.75,"Original gym observer design",ha="center",fontsize=15,weight="bold")
else:
    eq=box(3.0,7.0,5.6,"«trait» Equipment",[
      "+ name:String; age:Int; weight:Int",
      "- _time:Int; _heartRate:Int",
      "- _functionalities:List[Functionality]",
      "+ time:Int; time_=(Int):Unit",
      "+ heartRate:Int; heartRate_=(Int):Unit",
      "+ addFunctionality(f):Unit",
      "+ deleteFunctionality(f):List[Functionality]",
      "+ notifyFunctionalities():Unit",
      "+ updateUserMeasures(Int,Int):Unit"])
    fu=box(10.0,8.4,3.7,"«trait» Functionality",["+ update():Unit"])
    gy=box(3.0,2.8,5.6,"GymEquipment",[
      "+ name:String; age:Int; weight:Int",
      "+ updateUserMeasures(Int,Int):Unit"])
    pe=box(7.85,4.9,3.4,"Performance",["- e:GymEquipment","- calories:Int","+ update():Unit"])
    pr=box(11.8,4.9,3.4,"Prediction",["- e:GymEquipment","+ update():Unit"])
    bo=box(9.8,1.8,6.6,"«object / singleton» GymStandings",[
      "- scores:Map[GymEquipment,Int]",
      "+ record(e:GymEquipment,score:Int):Unit",
      "+ remove(e:GymEquipment):Unit",
      "+ leaders:List[(GymEquipment,Int)]"],tint="#e7f4e9")
    arrow([(3,gy[3]),(3,eq[2])],True)
    arrow([(7.85,pe[3]),(7.85,7.2),(9.2,fu[2])],True)
    arrow([(11.8,pr[3]),(11.8,7.2),(10.8,fu[2])],True)
    arrow([(eq[1],8.4),(fu[0],8.4)],label="observers: 0..*",label_xy=(7,8.75))
    arrow([(pe[0],4.9),(5.95,4.9),(5.95,2.8),(gy[1],2.8)],label="e: 1",label_xy=(6.3,3.7))
    arrow([(13.5,4.9),(13.8,4.9),(13.8,.25),(.15,.25),(.15,2.8),(gy[0],2.8)],label="Prediction.e: 1",label_xy=(3.2,.25))
    arrow([(7.85,pe[2]),(7.85,bo[3])],label="reports current score",label_xy=(9.4,3.5))
    arrow([(bo[0],1.8),(5.9,1.8),(4.4,gy[2])],label="keys: 0..* equipment",label_xy=(4.5,1.3))
    ax.text(7,9.7,"Gym-wide standings through one shared object",ha="center",fontsize=15,weight="bold")
ax.text(7,0.02 if not EXTENDED else .02,"Dashed hollow triangle: realizes trait     Arrow: association / report dependency",ha="center",va="bottom",fontsize=9,color="#46566b")
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white", transparent=False, bbox_inches="tight", pad_inches=.13)
plt.close(fig)