| <!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> |
| <title>Interactive India Map</title> |
| <link rel="icon" type="image/x-icon" href="/public/Misinfo.ico"> |
| <style> |
| :root{--bg:#0f1720;--panel:#0b1220;--accent:#3b82f6} |
| body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background:#0f172a;color:#e2e8f0;margin:0;padding:0;display:flex;height:100vh;overflow:hidden;overflow-x:hidden;max-width:100vw} |
| #left{flex:1;display:flex;flex-direction:column} |
| header{padding:12px 16px;background:linear-gradient(90deg,rgba(255,255,255,0.02),transparent);display:flex;gap:12px;align-items:center} |
| header h1{font-size:18px;margin:0} |
| #mapwrap{flex:1;overflow:hidden;position:relative} |
| #svgroot{width:100%;height:100%;touch-action:none;cursor:grab;background:linear-gradient(180deg,#071027, #082035)} |
| #tooltip{position:absolute;pointer-events:none;padding:8px 10px;background:#fff;color:#041026;border-radius:6px;font-size:13px;box-shadow:0 6px 18px rgba(4,8,16,0.6);display:none} |
| aside{width:320px;background:var(--panel);border-left:1px solid rgba(255,255,255,0.03);padding:18px;box-sizing:border-box} |
| .state-name{font-size:20px;font-weight:600;margin-bottom:6px} |
| .meta{font-size:13px;opacity:0.85} |
| .controls{display:flex;gap:8px;align-items:center} |
| .btn{background:transparent;border:1px solid rgba(255,255,255,0.06);padding:6px 8px;border-radius:8px;color:inherit;cursor:pointer} |
| input[type=search]{flex:1;padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.06);background:rgba(255,255,255,0.02);color:inherit} |
| .legend{margin-top:12px;font-size:13px} |
| .hint{margin-top:18px;opacity:0.7;font-size:13px} |
| |
| .map-path{fill:#88A4BC;stroke:#ffffff;stroke-width:0.6} |
| .map-path:hover{fill:#3B729F} |
| .selected{fill:var(--accent) !important} |
| footer{padding-top:12px;font-size:12px;opacity:0.8} |
| |
| |
| @media (max-width: 768px) { |
| body { display: block; } |
| #left { width: 100%; height: 60vh; } |
| aside { |
| width: 100%; |
| border-left: none; |
| border-top: 1px solid rgba(255,255,255,0.1); |
| padding: 12px; |
| height: 40vh; |
| overflow-y: auto; |
| } |
| header { padding: 8px 12px; } |
| header h1 { font-size: 16px; } |
| .controls { gap: 8px; } |
| input[type=search] { font-size: 14px; } |
| .btn { font-size: 12px; padding: 4px 6px; } |
| .state-name { font-size: 18px; } |
| .meta { font-size: 12px; } |
| .legend { font-size: 12px; } |
| .hint { font-size: 12px; } |
| } |
| |
| @media (max-width: 480px) { |
| #left { height: 50vh; } |
| aside { height: 50vh; padding: 8px; } |
| header { padding: 6px 8px; } |
| header h1 { font-size: 14px; } |
| .controls { flex-direction: column; gap: 6px; } |
| input[type=search] { padding: 6px; } |
| .btn { padding: 3px 6px; font-size: 11px; } |
| .state-name { font-size: 16px; } |
| .meta { font-size: 11px; } |
| .legend { font-size: 11px; margin-top: 8px; } |
| .hint { font-size: 11px; margin-top: 12px; } |
| footer { font-size: 10px; } |
| } |
| </style> |
| </head> |
| <body> |
| <div id="left"> |
| <header> |
| <h1>Interactive India Map</h1> |
| <div class="controls" style="flex:1"> |
| <input id="search" type="search" placeholder="Search state (e.g. Karnataka)" /> |
| <button id="reset" class="btn">Reset</button> |
| </div> |
| </header> |
| <div id="mapwrap"> |
| <div id="svgroot">Loading map...</div> |
| <div id="tooltip"></div> |
| </div> |
| </div> |
| <aside> |
| <div class="state-name" id="stateName">Click a state</div> |
| <div class="meta" id="stateDesc">State description will appear here. Hover or click a state on the map.</div> |
| <div class="legend" id="legend"></div> |
| <div class="hint">Use mouse wheel to zoom, drag to pan. On mobile: pinch to zoom (if supported).</div> |
| <footer>Files required: <code>in.svg</code> and <code>mapdata.js</code> in same folder.</footer> |
| </aside> |
|
|
| <script> |
| |
| const svgRoot = document.getElementById('svgroot'); |
| const tooltip = document.getElementById('tooltip'); |
| const stateNameEl = document.getElementById('stateName'); |
| const stateDescEl = document.getElementById('stateDesc'); |
| const searchInput = document.getElementById('search'); |
| const resetBtn = document.getElementById('reset'); |
| |
| |
| async function loadSVG(){ |
| try{ |
| const res = await fetch('in.svg'); |
| if(!res.ok) throw new Error('SVG not found. Put in.svg beside this HTML file.'); |
| const text = await res.text(); |
| svgRoot.innerHTML = text; |
| initMap(); |
| }catch(e){ |
| svgRoot.innerHTML = '<div style="padding:18px;color:salmon">Error loading in.svg: '+e.message+'</div>'; |
| } |
| } |
| |
| |
| function loadMapData(){ |
| return new Promise((resolve)=>{ |
| if(window.simplemaps_countrymap_mapdata){ |
| resolve(window.simplemaps_countrymap_mapdata); |
| }else{ |
| |
| const s = document.createElement('script'); |
| s.src = 'mapdata.js'; |
| s.onload = ()=> resolve(window.simplemaps_countrymap_mapdata || {}); |
| s.onerror = ()=> resolve({}); |
| document.head.appendChild(s); |
| } |
| }) |
| } |
| |
| |
| let scale = 1, tx = 0, ty = 0; |
| let isPanning = false, startX=0, startY=0; |
| let svgEl; |
| let mapData = {}; |
| let selectedEl = null; |
| |
| function initMap(){ |
| |
| svgEl = svgRoot.querySelector('svg'); |
| if(!svgEl){ svgRoot.innerHTML = '<div style="padding:18px;color:salmon">in.svg does not contain an <svg> root.</div>'; return; } |
| |
| const paths = svgEl.querySelectorAll('path, polygon, rect, circle'); |
| paths.forEach(p => { |
| if(!p.id) return; |
| p.classList.add('map-path'); |
| }); |
| |
| |
| let g = document.createElementNS('http://www.w3.org/2000/svg','g'); |
| |
| while(svgEl.firstChild){ g.appendChild(svgEl.firstChild); } |
| svgEl.appendChild(g); |
| svgEl.dataset.viewbox = svgEl.getAttribute('viewBox') || ''; |
| |
| |
| svgRoot.addEventListener('wheel', e=>{ |
| e.preventDefault(); |
| const delta = -e.deltaY * 0.001; |
| const oldScale = scale; |
| scale = Math.min(8, Math.max(0.5, scale * (1 + delta))); |
| |
| const rect = svgRoot.getBoundingClientRect(); |
| const cx = e.clientX - rect.left; |
| const cy = e.clientY - rect.top; |
| tx -= (cx/oldScale - cx/scale); |
| ty -= (cy/oldScale - cy/scale); |
| updateTransform(); |
| }, {passive:false}); |
| |
| svgRoot.addEventListener('mousedown', e=>{ |
| isPanning = true; startX = e.clientX; startY = e.clientY; svgRoot.style.cursor='grabbing'; |
| }); |
| window.addEventListener('mousemove', e=>{ |
| if(!isPanning) return; |
| tx += (e.clientX - startX)/scale; ty += (e.clientY - startY)/scale; startX = e.clientX; startY = e.clientY; updateTransform(); |
| }); |
| window.addEventListener('mouseup', ()=>{ isPanning=false; svgRoot.style.cursor='grab'; }); |
| |
| |
| loadMapData().then(md=>{ |
| mapData = md || {}; |
| attachStateHandlers(); |
| }); |
| |
| |
| svgRoot.addEventListener('mousemove', e=>{ |
| const t = e.target; |
| if(t && t.classList && t.classList.contains('map-path')){ |
| const id = t.id; |
| const info = lookupStateInfo(id); |
| showTooltip(e.clientX, e.clientY, info.name+(info.desc?('\n'+info.desc):'')); |
| } else { hideTooltip(); } |
| }); |
| |
| svgRoot.addEventListener('mouseleave', hideTooltip); |
| |
| |
| svgRoot.addEventListener('click', e=>{ |
| const t = e.target; |
| if(t && t.classList && t.classList.contains('map-path')){ |
| selectState(t.id, t); |
| } |
| }); |
| |
| |
| searchInput.addEventListener('keydown', e=>{ if(e.key==='Enter') doSearch(); }); |
| resetBtn.addEventListener('click', resetView); |
| } |
| |
| function updateTransform(){ |
| const g = svgEl.querySelector('g'); |
| g.setAttribute('transform', `translate(${tx} ${ty}) scale(${scale})`); |
| } |
| |
| function showTooltip(cx, cy, html){ |
| tooltip.style.display='block'; |
| tooltip.innerText = html; |
| const mapRect = svgRoot.getBoundingClientRect(); |
| const left = Math.min(mapRect.width - 180, Math.max(8, cx - mapRect.left + 12)); |
| const top = Math.max(8, cy - mapRect.top + 12); |
| tooltip.style.left = left+'px'; tooltip.style.top = top+'px'; |
| } |
| function hideTooltip(){ tooltip.style.display='none'; } |
| |
| function lookupStateInfo(id){ |
| const empty = {name:id || 'Unknown', desc:''}; |
| if(!id) return empty; |
| if(mapData && mapData.state_specific && mapData.state_specific[id]){ |
| const s = mapData.state_specific[id]; |
| return {name: s.name || id, desc: (s.description && s.description!=='default')?s.description:''}; |
| } |
| |
| return {name: id, desc:''}; |
| } |
| |
| function selectState(id, el){ |
| if(selectedEl) selectedEl.classList.remove('selected'); |
| selectedEl = el; |
| selectedEl.classList.add('selected'); |
| const info = lookupStateInfo(id); |
| stateNameEl.innerText = info.name || id; |
| stateDescEl.innerText = info.desc || (mapData.main_settings && mapData.main_settings.state_description) || 'No description available.'; |
| } |
| |
| function attachStateHandlers(){ |
| |
| if(mapData && mapData.legend && Array.isArray(mapData.legend.entries) && mapData.legend.entries.length){ |
| const legend = document.getElementById('legend'); |
| legend.innerHTML = '<strong>Legend</strong><br>' + mapData.legend.entries.join(', '); |
| } |
| |
| |
| const paths = svgEl.querySelectorAll('.map-path'); |
| paths.forEach(p=>{ |
| p.style.cursor='pointer'; |
| |
| const info = lookupStateInfo(p.id); |
| p.setAttribute('data-name', info.name||p.id); |
| |
| if(!p.getAttribute('fill') || p.getAttribute('fill')==='none'){ |
| p.setAttribute('fill','#88A4BC'); |
| } |
| }); |
| } |
| |
| function doSearch(){ |
| const q = searchInput.value.trim().toLowerCase(); |
| if(!q) return; |
| |
| const m = mapData.state_specific || {}; |
| for(const k of Object.keys(m)){ |
| const name = (m[k].name||'').toLowerCase(); |
| if(name.includes(q) || k.toLowerCase()===q){ |
| |
| const el = svgEl.querySelector('#'+CSS.escape(k)); |
| if(el){ |
| |
| const bbox = el.getBBox(); |
| |
| const rect = svgRoot.getBoundingClientRect(); |
| const cx = bbox.x + bbox.width/2; const cy = bbox.y + bbox.height/2; |
| |
| const targetScreenX = rect.width/2; const targetScreenY = rect.height/2; |
| tx = targetScreenX/scale - cx; ty = targetScreenY/scale - cy; |
| updateTransform(); |
| selectState(k, el); |
| return; |
| } |
| } |
| } |
| alert('No state found for "'+q+'"'); |
| } |
| |
| function resetView(){ scale = 1; tx = 0; ty = 0; updateTransform(); if(selectedEl) { selectedEl.classList.remove('selected'); selectedEl=null; stateNameEl.innerText='Click a state'; stateDescEl.innerText='State description will appear here.'} } |
| |
| |
| loadSVG(); |
| </script> |
| </body> |
| </html> |
|
|