Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ print(results.nlargest(10, 'final_score'))
python scripts/run_pipeline.py
```

### Interactive GUI (no code)

Run the model from a desktop app with native file pickers — select inputs,
enter who is running it and any remarks, and get outputs plus a full run log:

```bash
pip install -r requirements.txt # includes pywebview
python app/run_model_gui.py
```

See **[app/RUN_MODEL_GUI.md](app/RUN_MODEL_GUI.md)** for details.

---

## Project Structure
Expand Down
77 changes: 77 additions & 0 deletions app/RUN_MODEL_GUI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Run Model GUI

An interactive desktop application for running the full hub prioritization
pipeline without touching code. The interface is built with HTML/JS and rendered
in a **native window** via [pywebview](https://pywebview.flowrl.com/), so it can
open real OS folder/file pickers.

## What it does

1. **Select inputs** — either pick a single folder and let the app auto-detect
every input file by name, or pick each file individually with a native
"Browse…" dialog.
2. **Record who & why** — enter your name and free-text remarks describing the
run.
3. **Run the pipeline** — outputs (CSV, GeoJSON, interactive map) are written to
an output directory of your choice.
4. **Run log** — every run writes `run_log.json` and `run_log.txt` into the
output folder, recording:
- run id + start/finish timestamps + duration
- who ran it and their remarks
- every input file used, with size, last-modified time and SHA-256 checksum
- the options used and the output files produced
- a results summary (hub counts by tier) and success/error status

## Install

```bash
pip install -r requirements.txt
```

`pywebview` needs a rendering backend:

- **Windows** — uses the built-in Edge WebView2 (usually already present).
- **macOS** — uses the built-in WebKit (no extra install).
- **Linux** — install one backend, e.g.
`pip install pyqt5 pyqtwebengine` **or** the system
`python3-gi gir1.2-webkit2-4.1` packages.

## Run

```bash
python app/run_model_gui.py
```

## Input files

| Field | Required | Typical filename |
|-------|----------|------------------|
| Transit nodes (CSV) | ✅ | `All_nodes+lines.csv` |
| Lines & planned modes (CSV) | ✅ | `Lines_and_Planned_Mode.csv` |
| Demand forecast (Excel/CSV) | optional | `Demand_2050_all.xlsx` |
| Metro areas (SHP) | optional | `metro.shp` |
| Districts (SHP) | optional | `districts.shp` |
| TAZ zones / demographics (SHP) | optional | `TAZ_2050.shp` |
| Bus terminals (SHP) | optional | `bus_terminals.shp` |

Directory auto-detection matches files by extension and name keywords, so naming
your files close to the conventions above lets the app find them automatically.

## Output

Default output directory is `data/results/run_<timestamp>/`. Each run is
self-contained in its own folder, including the intermediate artefacts
(`processed/`), the final outputs, the streaming `run_<timestamp>.log`, and the
`run_log.json` / `run_log.txt` manifest.

## How it fits the code

The GUI is a thin front-end over `scripts/run_complete_pipeline.py`:

- `RunConfig` — describes one run (inputs, output dir, options, metadata).
- `run_pipeline(config)` — the shared entry point used by both the GUI and the
command line (`python scripts/run_complete_pipeline.py`).
- `resolve_inputs_from_directory(dir)` — powers the folder auto-detect.

Running the script directly still works exactly as before, using the default
paths under `data/raw/`.
192 changes: 192 additions & 0 deletions app/gui/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// Front-end logic for the Run Model GUI.
// Communicates with the Python backend via window.pywebview.api.

let FIELDS = []; // [{field,label,required}]
const selected = {}; // field -> path
let pollTimer = null;

function $(id) { return document.getElementById(id); }

// Wait until pywebview has injected the API
window.addEventListener('pywebviewready', init);

async function init() {
FIELDS = await window.pywebview.api.get_input_fields();
buildFileRows();
$('outputDir').value = await window.pywebview.api.default_output_dir();

document.querySelectorAll('input[name="mode"]').forEach(r =>
r.addEventListener('change', applyMode));
applyMode();
}

function applyMode() {
const mode = document.querySelector('input[name="mode"]:checked').value;
$('dirMode').style.display = (mode === 'dir') ? 'block' : 'none';
// In directory mode the per-file Browse buttons are hidden (auto-detected);
// in files mode they are shown.
document.querySelectorAll('.fileBrowse').forEach(b => {
b.style.display = (mode === 'files') ? 'inline-block' : 'none';
});
}

function buildFileRows() {
const tbody = $('fileRows');
tbody.innerHTML = '';
FIELDS.forEach(f => {
const tr = document.createElement('tr');
const tag = f.required
? '<span class="tag req">required</span>'
: '<span class="tag opt">optional</span>';
tr.innerHTML = `
<td>${f.label}${tag}</td>
<td><span class="filename missing" id="fn_${f.field}">— not selected —</span></td>
<td><button class="btn ghost fileBrowse" onclick="browseFile('${f.field}')">Browse…</button></td>
`;
tbody.appendChild(tr);
});
}

function setFile(field, path) {
selected[field] = path || null;
const el = $('fn_' + field);
if (path) {
el.textContent = path;
el.classList.remove('missing');
} else {
el.textContent = '— not selected —';
el.classList.add('missing');
}
}

async function browseFile(field) {
const path = await window.pywebview.api.pick_file(field);
if (path) setFile(field, path);
}

async function browseDir() {
const dir = await window.pywebview.api.pick_folder();
if (!dir) return;
$('dirPath').value = dir;
await rescan();
}

async function rescan() {
const dir = $('dirPath').value;
if (!dir) { alert('Choose a directory first.'); return; }
const res = await window.pywebview.api.scan_directory(dir);
if (!res.ok) { alert('Scan failed: ' + res.error); return; }
FIELDS.forEach(f => setFile(f.field, res.matches[f.field]));
}

async function browseOutput() {
const dir = await window.pywebview.api.pick_folder();
if (dir) $('outputDir').value = dir;
}

async function runModel() {
const runBy = $('runBy').value.trim();
if (!runBy) { alert('Please enter your name before running.'); return; }

// Validate required inputs are present
const missing = FIELDS.filter(f => f.required && !selected[f.field]).map(f => f.label);
if (missing.length) { alert('Missing required inputs:\n- ' + missing.join('\n- ')); return; }

const payload = {
inputs: { ...selected },
output_dir: $('outputDir').value.trim(),
run_by: runBy,
remarks: $('remarks').value,
skip_demand_data: $('skip_demand_data').checked,
skip_spatial_layers: $('skip_spatial_layers').checked,
skip_demographics: $('skip_demographics').checked,
run_mc_distribution: $('run_mc_distribution').checked,
};

const res = await window.pywebview.api.start_run(payload);
if (!res.ok) { setStatus(res.error, 'err'); return; }

$('runBtn').disabled = true;
$('progressCard').style.display = 'block';
$('resultPanel').style.display = 'none';
$('logBox').textContent = '';
setStatus('Running…');
pollTimer = setInterval(poll, 1000);
}

function setStatus(text, kind) {
const el = $('runStatus');
el.textContent = text || '';
el.className = 'status' + (kind ? ' ' + kind : '');
}

async function poll() {
const s = await window.pywebview.api.poll();
const box = $('logBox');
box.textContent = s.log || '';
box.scrollTop = box.scrollHeight;

if (s.done) {
clearInterval(pollTimer);
$('runBtn').disabled = false;
if (s.error) {
setStatus('Run failed', 'err');
} else {
setStatus('Run complete', 'ok');
}
renderResult(s.manifest, s.error);
}
}

function renderResult(manifest, error) {
const panel = $('resultPanel');
panel.style.display = 'block';
if (!manifest) {
panel.innerHTML = `<div class="banner err">Run finished but no run log was found.${
error ? ' Error: ' + escapeHtml(error) : ''}</div>`;
return;
}

const ok = manifest.status === 'success';
let html = `<div class="banner ${ok ? 'ok' : 'err'}">
${ok ? '✅ Pipeline complete' : '❌ Pipeline failed'} —
run ${escapeHtml(manifest.run_id)} by ${escapeHtml(manifest.run_by)}
(${manifest.duration_seconds}s)
${manifest.error_message ? '<br>' + escapeHtml(manifest.error_message) : ''}
</div>`;

const sum = manifest.results_summary || {};
if (sum.total_hubs != null) {
html += '<div class="summary">';
html += `<div class="stat"><b>${sum.total_hubs}</b><span>total hubs</span></div>`;
const byTier = sum.hubs_by_tier || {};
Object.keys(byTier).forEach(t => {
html += `<div class="stat"><b>${byTier[t]}</b><span>${escapeHtml(t)}</span></div>`;
});
html += '</div>';
}

html += `<p class="hint">Output directory:</p>
<ul class="outlist">
<li><span class="filename">${escapeHtml(manifest.output_dir)}</span>
<button class="btn ghost" onclick="openPath('${jsstr(manifest.output_dir)}')">Open folder</button></li>`;
(manifest.outputs || []).forEach(p => {
html += `<li><span class="filename">${escapeHtml(p)}</span>
<button class="btn ghost" onclick="openPath('${jsstr(p)}')">Open</button></li>`;
});
html += '</ul>';

panel.innerHTML = html;
}

async function openPath(path) {
await window.pywebview.api.open_path(path);
}

function escapeHtml(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function jsstr(s) {
return String(s == null ? '' : s).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
}
94 changes: 94 additions & 0 deletions app/gui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hub Prioritization - Run Model</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="app">
<header>
<h1>🚆 Hub Prioritization — Run Model</h1>
<p class="subtitle">Select inputs, run the pipeline, and produce outputs with a full run log.</p>
</header>

<!-- 1. Run metadata -->
<section class="card">
<h2>1. Run details</h2>
<div class="row">
<label for="runBy">Your name <span class="req">*</span></label>
<input type="text" id="runBy" placeholder="e.g. Ohad Cohen">
</div>
<div class="row">
<label for="remarks">Remarks / description</label>
<textarea id="remarks" rows="3"
placeholder="Describe this run: scenario, data version, what you're testing..."></textarea>
</div>
</section>

<!-- 2. Inputs -->
<section class="card">
<h2>2. Input data</h2>
<div class="modeToggle">
<label><input type="radio" name="mode" value="dir" checked> Select a directory (auto-detect files)</label>
<label><input type="radio" name="mode" value="files"> Select files individually</label>
</div>

<!-- Directory mode -->
<div id="dirMode" class="modePanel">
<div class="row inline">
<input type="text" id="dirPath" placeholder="Choose a folder containing the input files..." readonly>
<button class="btn" onclick="browseDir()">Browse…</button>
<button class="btn ghost" onclick="rescan()">Re-scan</button>
</div>
<p class="hint">The folder is scanned for the expected files by name and extension.</p>
</div>

<!-- File table (shared by both modes) -->
<table class="files">
<thead>
<tr><th>Input</th><th>File</th><th></th></tr>
</thead>
<tbody id="fileRows"><!-- filled by JS --></tbody>
</table>
</section>

<!-- 3. Options -->
<section class="card">
<h2>3. Options</h2>
<div class="opts">
<label><input type="checkbox" id="skip_demand_data"> Skip demand data</label>
<label><input type="checkbox" id="skip_spatial_layers"> Skip spatial layers</label>
<label><input type="checkbox" id="skip_demographics"> Skip demographics</label>
<label><input type="checkbox" id="run_mc_distribution"> Run Monte Carlo distribution analysis</label>
</div>
</section>

<!-- 4. Output -->
<section class="card">
<h2>4. Output location</h2>
<div class="row inline">
<input type="text" id="outputDir" placeholder="Output directory...">
<button class="btn" onclick="browseOutput()">Browse…</button>
</div>
<p class="hint">Outputs (CSV, GeoJSON, map) and a run log (run_log.json / run_log.txt) are written here.</p>
</section>

<!-- Run -->
<section class="runbar">
<button class="btn run" id="runBtn" onclick="runModel()">▶ Run model</button>
<span id="runStatus" class="status"></span>
</section>

<!-- Progress / results -->
<section class="card" id="progressCard" style="display:none;">
<h2>Progress</h2>
<pre id="logBox" class="logbox"></pre>
<div id="resultPanel" style="display:none;"></div>
</section>
</div>

<script src="app.js"></script>
</body>
</html>
Loading