/* 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 (
{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
Shelf-health · estimate
Healthy
Watch
At-risk
Critical
Format
Hospital
Flagship
Neighbourhood
{!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 (
);
}
window.KolkataMap = KolkataMap;
window.MapLocator = MapLocator;
window.mapProj = proj;