// ============================================================ // India Map — d3-geo topojson rendering + festival icon pins // Includes Tricolor mode for Republic Day (W4) & Independence Day (W32) // ============================================================ /* global React, d3, topojson, LOF_Icons */ const { useEffect, useMemo, useRef, useState } = React; const { FestivalIcon, pickIcon } = LOF_Icons; const STATE_ZONE = { 'Jammu and Kashmir':'North','Ladakh':'North','Himachal Pradesh':'North','Punjab':'North', 'Haryana':'North','Uttarakhand':'North','Delhi':'North','Chandigarh':'North', 'Rajasthan':'West','Gujarat':'West','Maharashtra':'West','Goa':'West', 'Dadra and Nagar Haveli and Daman and Diu':'West', 'Madhya Pradesh':'Central','Chhattisgarh':'Central','Uttar Pradesh':'Central', 'Bihar':'East','Jharkhand':'East','West Bengal':'East','Odisha':'East', 'Assam':'Northeast','Arunachal Pradesh':'Northeast','Nagaland':'Northeast', 'Manipur':'Northeast','Mizoram':'Northeast','Tripura':'Northeast', 'Meghalaya':'Northeast','Sikkim':'Northeast', 'Karnataka':'South','Kerala':'South','Tamil Nadu':'South','Andhra Pradesh':'South', 'Telangana':'South','Puducherry':'South', 'Lakshadweep':'Islands','Andaman and Nicobar Islands':'Islands', }; const ZONE_FILL = { North:'oklch(0.32 0.028 70)', West:'oklch(0.32 0.032 50)', Central:'oklch(0.32 0.026 40)', East:'oklch(0.32 0.026 25)', Northeast:'oklch(0.32 0.028 160)', South:'oklch(0.32 0.032 80)', Islands:'oklch(0.32 0.024 220)', }; const ZONE_FILL_BRIGHT = { North:'oklch(0.46 0.046 70)', West:'oklch(0.46 0.052 50)', Central:'oklch(0.46 0.044 40)', East:'oklch(0.46 0.044 25)', Northeast:'oklch(0.46 0.046 160)', South:'oklch(0.46 0.052 80)', Islands:'oklch(0.46 0.040 220)', }; const TRICOLOR_WEEKS = new Set([4, 32]); function IndiaMap({ topo, width, height, pins, week, hoveredPin, setHoveredPin, selectedPin, setSelectedPin, }) { const tricolor = TRICOLOR_WEEKS.has(week); const { features, path, stateCentroids, panCenter, bounds } = useMemo(() => { if (!topo) return {}; const fc = topojson.feature(topo, topo.objects.states); const proj = d3.geoMercator(); proj.fitExtent([[8, 8], [width - 8, height - 8]], fc); const pth = d3.geoPath(proj); const centroids = {}; fc.features.forEach((f) => { const c = d3.geoCentroid(f); const xy = proj(c); if (xy) centroids[f.properties.st_nm] = { x: xy[0], y: xy[1] }; }); const panCtr = proj([78.6, 22.5]); const b = pth.bounds(fc); return { features: fc.features, path: pth, stateCentroids: centroids, panCenter: { x: panCtr[0], y: panCtr[1] }, bounds: b, }; }, [topo, width, height]); const resolvedPins = useMemo(() => { if (!stateCentroids) return []; return pins .map((p) => { if (p.pan) return { ...p, x: panCenter.x, y: panCenter.y, isPan: true }; const c = stateCentroids[p.state]; if (!c) return null; return { ...p, x: c.x, y: c.y }; }) .filter(Boolean); }, [pins, stateCentroids, panCenter]); const pinNodes = useMemo(() => { const groups = new Map(); resolvedPins.forEach((p) => { const key = p.isPan ? '__pan__' : p.state; if (!groups.has(key)) groups.set(key, []); groups.get(key).push(p); }); const out = []; groups.forEach((items) => { const cx = items[0].x, cy = items[0].y; items.forEach((p, i) => { const n = items.length; const r = n === 1 ? 0 : 11; const a = (i / n) * Math.PI * 2 - Math.PI / 2; out.push({ ...p, x: cx + Math.cos(a) * r, y: cy + Math.sin(a) * r, clusterSize: n, }); }); }); return out; }, [resolvedPins]); const activeStates = useMemo(() => { const s = new Set(); resolvedPins.forEach((p) => { if (!p.isPan && p.state) s.add(p.state); }); return s; }, [resolvedPins]); if (!topo) return null; const hoveredNode = pinNodes.find((n) => n.id === hoveredPin || n.id === selectedPin); return (