Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
66fb98e
fix(docs): correct relative path for embedded 3-D twin iframes
harsh-pandhe May 24, 2026
bcacb1b
feat(case-study): engineer trial corridor (India, 805 m)
harsh-pandhe May 24, 2026
7716aa6
feat(case-study): pivot India trial to Bhimashankar jig-back (2.18 km…
harsh-pandhe May 24, 2026
81802cf
fix(ui): surface 'currently loaded' label so Demo vs sidebar runs are…
harsh-pandhe May 24, 2026
c4de74c
feat(io): alignment_to_kml for Google Earth + QGIS visualisation guide
harsh-pandhe May 24, 2026
2e3c197
feat(p72-74): cable rides terrain, station vocabulary, one-button ZIP
harsh-pandhe May 24, 2026
bec251b
feat(p76): corridor API — coords-in, permit-pack out
harsh-pandhe May 24, 2026
c5b969c
feat(p77): Next.js SPA scaffold — corridor form + result + downloads
harsh-pandhe May 24, 2026
3d7792a
feat(p78): map-first corridor input — drop two pins, get an alignment
harsh-pandhe May 24, 2026
1e1e8bf
feat(p79): Cesium 3-D viewer in the result panel
harsh-pandhe May 24, 2026
666bad5
feat(p80): interactive Plotly charts on the result page
harsh-pandhe May 24, 2026
389231a
feat(p81): AI input via local Ollama — plain-English → corridor request
harsh-pandhe May 24, 2026
396f865
feat(p86): AI refine chat — 'make it cheaper' re-runs the GA
harsh-pandhe May 24, 2026
7dc072f
feat(p85): client-side walkthrough video — record + download .webm
harsh-pandhe May 24, 2026
321a3f1
fix(web): pin Node 20 + document the oxide native-binding npm bug
harsh-pandhe May 24, 2026
2acfa25
feat(p95): KML cable alignment v2 — absolute mode by default + safety…
harsh-pandhe May 24, 2026
4cc5b90
docs: README — web product section + POST_TRIAL_PLAN_V2 pointer
harsh-pandhe May 24, 2026
6490953
feat(p98): refine v2 — before/after diff, history stack, recalibrated…
harsh-pandhe May 24, 2026
2832b57
feat(p93): inline document previews — PDF / CSV tables / KML / GeoJSON
harsh-pandhe May 24, 2026
d68c88d
feat(p94): UI overhaul — hero, cards, typography, theme toggle
harsh-pandhe May 24, 2026
1037ddc
feat(p96): advanced editor — every engine knob in the SPA
harsh-pandhe May 24, 2026
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
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,57 @@ See [`docs/tagline/tagline.md`](docs/tagline/tagline.md). Reproduce with

---

## Web product (P77+) — what the engineer actually clicks

`web/` is a Next.js 16 SPA backed by a FastAPI corridor endpoint on
the same box. No CLI on the engineer's path.

| | |
|---|---|
| **AI input (P81)** | Local Ollama (default `gemma4:e4b`) parses *"ropeway from Shidighat to Bhimashankar plateau, jig-back"* into a structured corridor request. |
| **Map input (P78)** | Leaflet + OpenStreetMap. Drop two pins, drag to fine-tune. |
| **Build (P76)** | `POST /api/v1/corridor` → auto-fetches the Copernicus tile (P47) → runs the GA → writes KML / DXF / LandXML / GeoJSON / PNG / PDF / ZIP → job-status URL the SPA polls. |
| **3-D view (P79)** | Cesium globe, KML loaded as a DataSource, auto-fly to corridor. With `NEXT_PUBLIC_CESIUM_TOKEN` set → Cesium World Terrain + Bing satellite. |
| **Walkthrough video (P85)** | Client-side `MediaRecorder` on the Cesium canvas + scripted 12-second camera flight → downloadable `.webm`. |
| **Interactive charts (P80)** | Plotly: elevation profile + cable + supports / GA convergence / per-span tension. |
| **Refine chat (P86)** | "make it cheaper" / "fewer towers" / "switch to 3S" → Ollama → adjusted `CostWeights` + warm-start → new GA job. |
| **Permit pack (P82)** | One-button ZIP, plus per-artifact downloads. |
| **KML alignment (P95)** | Cable shipped at true WGS-84 elevation + 5 m safety buffer so GE-stylised terrain never clips it. `relativeToGround` opt-in available. |

Local dev:

```bash
source ~/.nvm/nvm.sh && nvm use 20 # Next 16 requires Node ≥ 20
cd web && npm install && npm run dev # http://localhost:3000

# in another terminal:
source .venv/bin/activate && ropeway serve # FastAPI :8000

# and:
ollama serve # Ollama for /api/v1/ask + /api/v1/refine
```

Recovery if Tailwind's native binary mis-installs:
`rm -rf web/node_modules web/package-lock.json web/.next && cd web && npm install`.

---

## Phase plans

The two planning docs that drive everything else:

* **[`docs/POST_TRIAL_PLAN.md`](docs/POST_TRIAL_PLAN.md)** — the H1/H2/H3 plan
written the day of the first engineer trial. H1 + H2 are shipped.
* **[`docs/POST_TRIAL_PLAN_V2.md`](docs/POST_TRIAL_PLAN_V2.md)** — feedback-loop 1
after the trial. Waves A (visualisation correctness, includes the
P95 KML fix that just shipped), B (inline document previews —
P93), C (UI overhaul — P94), D (full-control editor + manual tower
drag + refine v2 + saved projects — P96–P99).

Original phase plan (engine waves P1–P32): [`docs/PHASE_PLAN.md`](docs/PHASE_PLAN.md).

---

## Quickstart

```bash
Expand Down
84 changes: 82 additions & 2 deletions app/streamlit_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ def _load_terrain():
st.session_state["profile"] = profile
st.session_state["patch"] = patch
st.session_state["cfg"] = cfg
st.session_state["loaded_label"] = (
f"sidebar {mode} corridor ({optimizer_mode})"
)
# Tower-height edits start equal to the optimized layout.
st.session_state["edited_heights"] = [t.height for t in st.session_state["alignment"].towers]

Expand All @@ -250,6 +253,10 @@ def _load_terrain():
eval_res = st.session_state["eval"]
history_best, history_avg = st.session_state["history"]

# "What is currently loaded" — surfaces in every tab so it's never
# ambiguous which corridor the Optimize / 3-D / Projects tabs show.
_loaded_label = st.session_state.get("loaded_label", "default synthetic 3 km corridor")


# ---- Tab 0: Demo (civil-engineer-friendly entry point) ----
with tab_demo:
Expand Down Expand Up @@ -277,6 +284,10 @@ def _load_terrain():
"Roosevelt Island Tramway (NYC) — urban jig-back":
dict(start=(-73.9580, 40.7570), end=(-73.9495, 40.7610),
system="jigback", note="Queensboro Bridge fly-over."),
"Bhimashankar pilgrim ropeway (India) — Jyotirlinga, 2.18 km":
dict(start=(73.5123812, 19.0691450), end=(73.5330820, 19.0700494),
system="jigback",
note="2026-05-25 engineer trial. Western Ghats pilgrim line, Shidighat → Bhimashankar plateau. 742 m terminal rise over 2.18 km (34 % slope). Jig-back, single intermediate tower."),
"Mexico City L2 + Caracas (new) — try a corridor":
dict(start=(-99.04, 19.34), end=(-99.00, 19.37),
system="mgd", note="Click two map points to override."),
Expand Down Expand Up @@ -404,6 +415,7 @@ def _load_terrain():
st.session_state["alignment"] = align
st.session_state["eval"] = eval_res_demo
st.session_state["history"] = (result.history_best, result.history_avg)
st.session_state["loaded_label"] = f"{preset_name} (system: {demo_system})"

progress.progress(1.0, text="Done.")

Expand All @@ -427,14 +439,81 @@ def _load_terrain():
fig_c, _ = plot_convergence(result.history_best, result.history_avg)
st.pyplot(fig_c, width="stretch")

# ---- P74: one-button "build me everything" ZIP ----
st.markdown("---")
st.markdown("### Download the permit pack")
st.caption(
"Single ZIP with KML (Google Earth), DXF (AutoCAD), LandXML, "
"GeoJSON (QGIS), BoM, capex estimate, tower schedule, "
"convergence + alignment PNGs."
)

import io as _io
import zipfile
from datetime import datetime as _dt
from ropeway.bom import build_bom as _build_bom
from ropeway.cost import Region as _Region, estimate_cost as _est_cost
from ropeway.dxf_export import alignment_to_dxf as _to_dxf
from ropeway.io import (
alignment_to_csv as _to_csv,
alignment_to_geojson as _to_geojson,
alignment_to_kml as _to_kml,
)
from ropeway.landxml import alignment_to_landxml as _to_landxml
from ropeway.multi_rope import RopewaySystemType as _RST

slug = "".join(c if c.isalnum() else "_" for c in preset_name).strip("_").lower()
with _io.BytesIO() as buf:
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
import tempfile as _tmp
with _tmp.TemporaryDirectory() as td:
td_p = Path(td)
_to_kml(align, profile, td_p / "alignment.kml",
project_name=preset_name, segments=eval_res_demo.segments)
_to_dxf(align, profile, td_p / "alignment.dxf",
project_name=preset_name)
_to_landxml(align, profile, td_p / "alignment.landxml")
_to_geojson(align, profile, td_p / "alignment.geojson")
_to_csv(align, profile, td_p / "towers.csv")

sys_enum_map = {"jigback": _RST.JIG_BACK, "mgd": _RST.MGD,
"bgd": _RST.BGD, "3s": _RST.TGD_3S,
"chair": _RST.CHAIRLIFT, "funitel": _RST.FUNITEL}
bom_obj = _build_bom(align, project_name=preset_name,
system=sys_enum_map[demo_system], cfg=cfg_demo)
(td_p / "bom.csv").write_text(bom_obj.as_csv())
est_obj = _est_cost(bom_obj, region=_Region.EMERGING)
(td_p / "cost_estimate.csv").write_text(est_obj.as_csv())

fig_a.savefig(td_p / "alignment.png", dpi=140)
fig_c.savefig(td_p / "convergence.png", dpi=140)

for f in sorted(td_p.iterdir()):
zf.write(f, arcname=f"{slug}/{f.name}")

zip_bytes = buf.getvalue()

stamp = _dt.utcnow().strftime("%Y%m%d_%H%M")
st.download_button(
label="⬇ Download permit pack (ZIP)",
data=zip_bytes,
file_name=f"ropeway_{slug}_{stamp}.zip",
mime="application/zip",
type="primary",
use_container_width=True,
key="demo_zip_download",
)

st.caption(
"Open the **3-D Digital Twin** tab above for the interactive view, "
"or the **Optimize** tab for downloads (DXF, LandXML, PDF, BoM, GeoJSON)."
"Need the interactive 3-D viewer? Open the **3-D Digital Twin** tab. "
"Need a knob-tunable run? **Advanced** users open the sidebar — but the "
"sidebar `Run optimization` button **overwrites** this Demo result."
)


# ---- Tab 1: Optimize ----
with tab_opt:
st.caption(f"Currently loaded: **{_loaded_label}**")
rep = eval_res.report
n_int = max(0, len(alignment.towers) - 2)
pphpd = _pphpd(float(line_speed), float(cfg.seat_spacing_m),
Expand Down Expand Up @@ -487,6 +566,7 @@ def _load_terrain():

# ---- Tab 2: 3-D Digital Twin ----
with tab_3d:
st.caption(f"Currently loaded: **{_loaded_label}**")
try:
from stpyvista import stpyvista
_HAVE_STPYVISTA = True
Expand Down
Loading
Loading