/* Banyan Pharmacy — Kolkata map: detailed vector basemap, pan (no zoom), floating HUD, uniform pins, in-pan tooltip (no jitter). */ const { useState: useStateM, useRef: useRefM, useEffect: useEffectM, useLayoutEffect } = React; /* zoom-control styles (injected once; pharma.css is the canonical home) */ if (typeof document !== "undefined" && !document.getElementById("map-zoom-css")) { const _mzs = document.createElement("style"); _mzs.id = "map-zoom-css"; _mzs.textContent = ` .map-zoom { position: absolute; right: 18px; bottom: 76px; z-index: 20; display: flex; flex-direction: column; background: rgba(251,248,242,0.92); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); border: 1px solid var(--rule); border-radius: 10px; overflow: hidden; box-shadow: var(--shadow-1); } .map-zoom .mz-btn { width: 36px; height: 36px; display: inline-flex; align-items: center; justify-content: center; background: transparent; border: 0; color: var(--body); cursor: pointer; transition: background 140ms, color 140ms; } .map-zoom .mz-btn + .mz-btn { border-top: 1px solid var(--rule); } .map-zoom .mz-btn:hover:not(:disabled) { background: var(--stone-deep); color: var(--clay-deep); } .map-zoom .mz-btn:active:not(:disabled) { transform: translateY(0.5px); } .map-zoom .mz-btn:disabled { color: var(--fade); cursor: default; }`; document.head.appendChild(_mzs); } /* ---- projection: lat/lng -> fixed-px map space -------------------------- */ const MAPW = 1280, MAPH = 1800; const LNG0 = 88.285, LNG1 = 88.445; // west .. east const LATT = 22.660, LATB = 22.450; // top(north) .. bottom(south) function proj(lat, lng) { return { x: ((lng - LNG0) / (LNG1 - LNG0)) * MAPW, y: ((LATT - lat) / (LATT - LATB)) * MAPH }; } const STATUS_COLOR = { healthy: "var(--green)", watch: "var(--amber)", atrisk: "var(--red)", critical: "var(--clay)" }; /* small per-store map nudges (cartographic legibility only) */ const NUDGE = { str_behala01: { dx: 50, dy: 0 } }; function pinXY(store) { const p = proj(store.lat, store.lng); const n = NUDGE[store.id]; return n ? { x: p.x + n.dx, y: p.y + n.dy } : p; } /* label offset so locality text doesn't sit on the dot */ const LABELPOS = { str_parkst01: { dx: 16, dy: 5, a: "start" }, str_gariah01: { dx: 16, dy: 5, a: "start" }, str_dhakur01: { dx: -16, dy: 16, a: "end" }, str_mukund01: { dx: 16, dy: 16, a: "start" }, str_ananda01: { dx: 16, dy: -8, a: "start" }, str_behala01: { dx: -16, dy: 5, a: "end" }, str_saltlk01: { dx: -16, dy: 6, a: "end" }, str_shyamb01: { dx: -16, dy: -8, a: "end" }, str_howrah01: { dx: -16, dy: 5, a: "end" }, }; /* ===================== the basemap art ================================== */ function Basemap() { return ( {/* land ground */} {/* ---- water bodies ---- */} {/* East Kolkata Wetlands (east/southeast) */} {/* Hooghly river — wide ribbon, N->S bending SW */} {/* lakes */} {/* Rabindra Sarobar */} {/* Subhash Sarobar */} {/* ---- parks (muted sage) ---- */} {/* Maidan */} {/* Salt Lake Central Park area */} {/* Tollygunge green */} {/* Alipore */} {/* ---- secondary street mesh (texture) ---- */} {/* ---- arterial roads (ochre with casing) ---- */} {[ ["M940 460 C 950 640 965 820 950 1000 C 935 1180 905 1340 890 1480 C 878 1580 872 1640 868 1700", 11], ["M700 510 C 665 640 600 740 560 860 C 535 950 545 1050 558 1150 C 572 1250 575 1340 562 1450 C 552 1540 548 1610 548 1700", 10], ["M770 545 C 870 460 1000 350 1140 250", 9], ["M545 1015 C 460 1085 360 1185 285 1290 C 235 1355 205 1420 180 1490", 9], ["M690 1050 C 780 1035 860 1010 940 985", 8], ["M700 640 C 760 720 800 820 820 920", 7], ["M700 510 C 720 410 742 310 762 200", 8], ["M150 560 C 230 585 290 605 322 622", 7], ["M150 720 C 230 700 285 665 300 645", 7], ["M430 985 C 560 1000 700 1010 700 1050", 6], ].map(([d, w], i) => ( ))} {/* bridges over the Hooghly */} {/* Howrah Bridge */} {/* Vidyasagar Setu */} {/* ---- railway (dashed dark) ---- */} {/* Howrah Station marks */} {/* ---- metro line 1 (dashed muted green) ---- */} {/* ===== map labels ===== */} Hooghly River EAST KOLKATA WETLANDS Rabindra Sarobar THE MAIDAN EM BYPASS VIP ROAD {[ ["Bagbazar", 700, 612], ["Ultadanga", 806, 612], ["Maniktala", 762, 740], ["Sealdah", 724, 956], ["BBD Bagh", 452, 856], ["Esplanade", 506, 992], ["Park Circus", 726, 1064], ["Bhowanipore", 556, 1148], ["Kalighat", 584, 1276], ["Alipore", 452, 1212], ["Ballygunge", 706, 1162], ["Jadavpur", 706, 1492], ["Tollygunge", 556, 1604], ["Garia", 648, 1716], ["New Alipore", 416, 1392], ["Garden Reach", 236, 1648], ["Shibpur", 150, 742], ["Beleghata", 884, 856], ["Tangra", 846, 1086], ["Topsia", 826, 1186], ["Dum Dum", 724, 296], ["Lake Town", 902, 452], ["New Town", 1190, 552], ["Bidhannagar", 1100, 720], ["Santoshpur", 768, 1556], ["Baranagar", 540, 220], ["Cossipore", 470, 360], ].map(([t, x, y]) => {t})} ); } function Lbl({ x, y, s = 17, c = "#6E6657", caps, children, anchor = "middle" }) { return ( {children} ); } /* ---- uniform store pin (shape constant; type lives in the tooltip) ----- */ function Pin({ store, status, onEnter, onLeave, onClick, focused, dim, showLabel }) { const { x, y } = pinXY(store); const color = STATUS_COLOR[status]; const lp = LABELPOS[store.id] || { dx: 16, dy: 5, a: "start" }; return ( onEnter && onEnter(store)} onMouseLeave={() => onLeave && onLeave()} onClick={(e) => { if (onClick) { e.stopPropagation(); onClick(store); } }} style={{ cursor: onClick ? "pointer" : "default" }}> {focused && } {/* format, quietly: cross = hospital · larger = flagship · dot = neighbourhood */} {store.format === "hospital" ? : } {showLabel && ( {store.locality} )} ); } /* ===================== interactive map (Dashboard hero) ================= */ function KolkataMap() { const app = window.useApp(); const vpRef = useRefM(null); const [pan, setPan] = useStateM({ x: 0, y: 0 }); const [scale, setScale] = useStateM(1); const [hover, setHover] = useStateM(null); const [dragging, setDragging] = useStateM(false); const [hintGone, setHintGone] = useStateM(false); const drag = useRefM(null); const moved = useRefM(false); const focusId = app.loopFocus && app.loopFocus.kind === "pin" ? app.loopFocus.id : null; const MINS = 0.75, MAXS = 2.4; const clamp = (x, y, s = scale) => { const vp = vpRef.current; const cw = vp ? vp.clientWidth : 1100, ch = vp ? vp.clientHeight : 700; return { x: Math.min(0, Math.max(cw - MAPW * s, x)), y: Math.min(0, Math.max(ch - MAPH * s, y)) }; }; const centerOn = (px, py, s = scale) => { const vp = vpRef.current; const cw = vp ? vp.clientWidth : 1100, ch = vp ? vp.clientHeight : 700; return clamp(cw / 2 - px * s, ch / 2 - py * s, s); }; /* zoom toward the viewport center, keeping that point fixed */ const zoomTo = (next) => { const s = Math.min(MAXS, Math.max(MINS, +(+next).toFixed(3))); if (s === scale) return; const vp = vpRef.current; const cw = vp ? vp.clientWidth : 1100, ch = vp ? vp.clientHeight : 700; const cx = (cw / 2 - pan.x) / scale, cy = (ch / 2 - pan.y) / scale; setScale(s); setPan(clamp(cw / 2 - cx * s, ch / 2 - cy * s, s)); }; useLayoutEffect(() => { const p = pinXY(D.storeById.str_parkst01); setPan(centerOn(p.x, p.y + 120)); }, []); useEffectM(() => { if (focusId && D.storeById[focusId]) { const p = pinXY(D.storeById[focusId]); setPan(centerOn(p.x, p.y)); } }, [focusId]); const onDown = (e) => { drag.current = { sx: e.clientX, sy: e.clientY, px: pan.x, py: pan.y }; moved.current = false; setDragging(true); setHover(null); e.currentTarget.setPointerCapture && e.currentTarget.setPointerCapture(e.pointerId); }; const onMove = (e) => { if (!drag.current) return; const dx = e.clientX - drag.current.sx, dy = e.clientY - drag.current.sy; if (Math.abs(dx) > 4 || Math.abs(dy) > 4) { moved.current = true; if (!hintGone) setHintGone(true); } setPan(clamp(drag.current.px + dx, drag.current.py + dy)); }; const onUp = () => { drag.current = null; setDragging(false); }; const kpis = (() => { const stage = app.stage; const atRisk = D.stores.filter((s) => { const st = window.storeStatusAt(s, stage); return st === "atrisk" || st === "critical"; }).length; return [ { k: "Stores at risk", v: atRisk, m: atRisk ? "Need attention" : "Chain steady", accent: !!atRisk }, { k: "Alerts open", v: window.alertsAt(stage).length, m: "Store-raised" }, { k: "Expiry · 30d", v: window.inr(D.expiryAtRisk), m: "1 batch flagged", small: true }, { k: "Pilots", v: 1, m: "Rehydra ORS" }, ]; })(); return (
{D.stores.filter((s) => s.id !== "str_barasa01").map((s) => { const status = window.storeStatusAt(s, app.stage); return setHover(null)} onClick={(st) => { if (!moved.current) app.nav({ name: "store", id: st.id }); }} />; })} {hover && (() => { const p = pinXY(hover); const status = window.storeStatusAt(hover, app.stage); return (
{hover.locality}
{D.FORMAT[hover.format].label} · {hover.region}
{D.HEALTH[status].label} {hover.openAlerts > 0 && · {hover.openAlerts} open alert{hover.openAlerts > 1 ? "s" : ""}}
Manager · {hover.manager}
); })()}
{/* ---------- floating HUD (does not pan) ---------- */}
{window.longDate(D.TODAY)}
Banyan Pharmacy · 10 stores across Kolkata
{kpis.map((c) => (
{c.k}
{c.v}
{c.m}
))}
Shelf-health · estimate
Healthy Watch At-risk Critical
Format
Hospital Flagship Neighbourhood
N
{!hintGone &&
Drag to explore · tap a store
}
); } /* ---- mini locator (store detail; static, fit-to-view) ------------------ */ function MapLocator({ store }) { const app = window.useApp(); const target = pinXY(store); const vw = 760, vh = 520; const vx = Math.max(0, Math.min(MAPW - vw, target.x - vw / 2)); const vy = Math.max(0, Math.min(MAPH - vh, target.y - vh / 2)); return (
{D.stores.filter((s) => s.id !== "str_barasa01").map((s) => { const status = window.storeStatusAt(s, app.stage); const sel = s.id === store.id; return ; })}
); } window.KolkataMap = KolkataMap; window.MapLocator = MapLocator; window.mapProj = proj;