File size: 5,097 Bytes
8f8a793
 
 
 
 
 
 
 
 
300744a
823ca50
 
8f8a793
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/* shared.js -- common runtime for all module pages:
   top navigation, Pyodide bootstrap, editable-code runner, plot helpers. */

const NAV_ITEMS = [
  ["index.html", "导航 Home"],
  ["m00_overview.html", "0 水量平衡"],
  ["m01_forcing.html", "1 Forcing"],
  ["m02_pet.html", "2 PET"],
  ["m05_runoff.html", "5 产流"],
  ["m06_response.html", "6 Response"],
  ["m07_routing.html", "7 Routing·方法"],
  ["m08_channel.html", "8a Routing·空间"],
];

function renderNav(here) {
  const nav = document.createElement("div");
  nav.className = "topnav";
  const inner = document.createElement("div");
  inner.className = "wrap";
  const brand = document.createElement("span");
  brand.className = "brand";
  brand.textContent = "💧 HydroModel Builder";
  inner.appendChild(brand);
  for (const [href, label] of NAV_ITEMS) {
    // skip links to pages that don't exist yet (marked with data-disabled below)
    const a = document.createElement("a");
    a.href = href;
    a.textContent = label;
    if (here === href) a.className = "here";
    inner.appendChild(a);
  }
  nav.appendChild(inner);
  document.body.prepend(nav);
}

/* ---------- Pyodide bootstrap (one shared promise) ---------- */
let _pyodidePromise = null;
function getPyodide(statusEl) {
  if (!_pyodidePromise) {
    if (statusEl) statusEl.textContent = "正在加载 Python 运行时 (~10s 首次) …";
    _pyodidePromise = loadPyodide().then(async (py) => {
      await py.loadPackage("numpy");
      if (statusEl) statusEl.textContent = "Python (numpy) 就绪 — 可改代码后重跑";
      return py;
    });
  }
  return _pyodidePromise;
}

/* ---------- Editable-code runner ----------
   makeRunner({codeEl, runBtn, resetBtn, statusEl, errEl, buildScript, onResult})
   - codeEl: <textarea> holding user-editable Python
   - buildScript(userCode, params) -> full python source to exec; must set __out
   - onResult(jsValue, params): update plots/readouts
   - params(): collect current slider values                              */
function makeRunner(cfg) {
  const defaultCode = cfg.codeEl.value;
  async function run() {
    const py = await getPyodide(cfg.statusEl);
    if (cfg.errEl) cfg.errEl.innerHTML = "";
    cfg.runBtn.disabled = true;
    try {
      const params = cfg.params ? cfg.params() : {};
      const src = cfg.buildScript(cfg.codeEl.value, params);
      const t0 = performance.now();
      py.runPython(src);
      const ms = performance.now() - t0;
      const out = py.globals.get("__out");
      const js = out.toJs({ dict_converter: Object.fromEntries });
      if (out.destroy) out.destroy();
      cfg.onResult(js, params, ms);
    } catch (e) {
      if (cfg.errEl)
        cfg.errEl.innerHTML =
          '<div class="err">运行出错 / error:\n' + (e && e.message ? e.message : e) + "</div>";
    } finally {
      cfg.runBtn.disabled = false;
    }
  }
  cfg.runBtn.addEventListener("click", run);
  if (cfg.resetBtn)
    cfg.resetBtn.addEventListener("click", () => {
      cfg.codeEl.value = defaultCode;
      run();
    });
  if (cfg.autorun !== false) getPyodide(cfg.statusEl).then(run);
  return run;
}

/* ---------- slider helper: live label + rerun on release ---------- */
function bindSlider(id, labelId, fmt, onchange) {
  const el = document.getElementById(id);
  const lab = document.getElementById(labelId);
  const f = fmt || ((v) => v);
  const upd = () => (lab.textContent = f(parseFloat(el.value)));
  el.addEventListener("input", upd);
  if (onchange) el.addEventListener("change", onchange);
  upd();
  return el;
}

/* ---------- plot helpers ---------- */
const PLOT_CFG = { responsive: true, displayModeBar: false };
function linePlot(div, traces, ylab, xlab, extra) {
  Plotly.react(
    div,
    traces,
    Object.assign(
      {
        margin: { t: 10, r: 10, b: 40, l: 55 },
        legend: { orientation: "h", y: 1.13 },
        xaxis: { title: xlab || "day" },
        yaxis: { title: ylab || "" },
      },
      extra || {}
    ),
    PLOT_CFG
  );
}

/* version tabs: makeTabs(containerId, [{key,label,crest}], onSwitch) */
function makeTabs(containerId, versions, onSwitch) {
  const box = document.getElementById(containerId);
  box.className = "vtabs";
  let current = versions[0].key;
  for (const v of versions) {
    const b = document.createElement("button");
    b.innerHTML = v.label + (v.crest ? '<span class="crest-mini">CREST</span>' : "");
    b.dataset.key = v.key;
    if (v.key === current) b.className = "on";
    b.addEventListener("click", () => {
      current = v.key;
      for (const c of box.children) c.className = c.dataset.key === current ? "on" : "";
      onSwitch(current);
    });
    box.appendChild(b);
  }
  return () => current;
}

function footer() {
  const f = document.createElement("div");
  f.className = "foot";
  f.innerHTML =
    'Made with Claude Code · hydrologic-model-builder skill · data: TU Delft CIE4431 Hesperange · anchor: <a href="https://github.com/HyDROSLab/EF5">CREST/EF5</a>';
  document.querySelector(".wrap:last-of-type")?.appendChild(f) || document.body.appendChild(f);
}