| <!DOCTYPE html>
|
| <html lang="en">
|
| <head>
|
| <meta charset="UTF-8">
|
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| <title>ATMOS β ERA5 Climate Downscaling Β· India</title>
|
| <link rel="preconnect" href="https://fonts.googleapis.com">
|
| <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;900&family=Fira+Code:wght@300;400;500&display=swap" rel="stylesheet">
|
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
| <style>
|
|
|
| *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
| :root{
|
| --bg:#080f1a;
|
| --bg2:#0b1422;
|
| --glass:rgba(11,20,34,.72);
|
| --glass2:rgba(15,26,45,.85);
|
| --border:rgba(255,255,255,.06);
|
| --border2:rgba(255,255,255,.1);
|
| --teal:#00e5cc;
|
| --teal2:rgba(0,229,204,.12);
|
| --gold:#f5a623;
|
| --gold2:rgba(245,166,35,.12);
|
| --jade:#00f5a0;
|
| --jade2:rgba(0,245,160,.1);
|
| --rose:#ff5f7e;
|
| --violet:#b57aff;
|
| --txt:#c8daf0;
|
| --txt2:#7a98bc;
|
| --txt3:#3d5470;
|
| --display:'Outfit',sans-serif;
|
| --mono:'Fira Code',monospace;
|
| --r:6px;
|
| }
|
|
|
| html,body{height:100%;background:var(--bg);color:var(--txt);font-family:var(--display);overflow:hidden}
|
|
|
|
|
| body::before{
|
| content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;
|
| background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='1'/%3E%3C/svg%3E");
|
| opacity:.022;mix-blend-mode:overlay;
|
| }
|
|
|
|
|
| body::after{
|
| content:'';position:fixed;inset:0;pointer-events:none;z-index:0;
|
| background-image:
|
| linear-gradient(rgba(0,229,204,.03) 1px,transparent 1px),
|
| linear-gradient(90deg,rgba(0,229,204,.03) 1px,transparent 1px);
|
| background-size:40px 40px;
|
| }
|
|
|
| #app{display:grid;grid-template-rows:58px 1fr 168px;height:100vh;position:relative;z-index:1}
|
|
|
|
|
| #hdr{
|
| display:flex;align-items:center;padding:0 24px;gap:0;
|
| background:var(--glass2);
|
| border-bottom:1px solid var(--border);
|
| backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
|
| position:relative;flex-shrink:0;z-index:100;
|
| }
|
|
|
| #hdr::after{
|
| content:'';position:absolute;bottom:-1px;left:0;right:0;height:1px;
|
| background:linear-gradient(90deg,transparent,var(--teal),var(--gold),var(--jade),transparent);
|
| opacity:.5;
|
| }
|
|
|
|
|
| #logo{display:flex;align-items:center;gap:10px;margin-right:28px}
|
| #logo-mark{
|
| width:30px;height:30px;border-radius:8px;
|
| background:linear-gradient(135deg,var(--teal),var(--jade));
|
| display:flex;align-items:center;justify-content:center;
|
| font-family:var(--mono);font-size:10px;font-weight:500;color:#000;
|
| letter-spacing:-.02em;flex-shrink:0;
|
| }
|
| #logo-text{display:flex;flex-direction:column;gap:1px}
|
| #logo-name{font-family:var(--display);font-size:15px;font-weight:700;color:#fff;letter-spacing:-.02em;line-height:1}
|
| #logo-sub{font-family:var(--mono);font-size:8px;color:var(--txt3);letter-spacing:.08em;text-transform:uppercase;line-height:1}
|
|
|
|
|
| .hdr-div{width:1px;height:28px;background:var(--border2);margin:0 20px;flex-shrink:0}
|
|
|
|
|
| .chip{
|
| display:flex;align-items:center;gap:5px;padding:4px 10px;
|
| border-radius:20px;font-family:var(--mono);font-size:8px;font-weight:500;
|
| letter-spacing:.06em;text-transform:uppercase;white-space:nowrap;
|
| border:1px solid;flex-shrink:0;
|
| }
|
| .chip-teal{background:var(--teal2);border-color:rgba(0,229,204,.25);color:var(--teal)}
|
| .chip-gold{background:var(--gold2);border-color:rgba(245,166,35,.25);color:var(--gold)}
|
| .chip-jade{background:var(--jade2);border-color:rgba(0,245,160,.25);color:var(--jade)}
|
| .chip-violet{background:rgba(181,122,255,.1);border-color:rgba(181,122,255,.25);color:var(--violet)}
|
| .chip-rose{background:rgba(255,95,126,.08);border-color:rgba(255,95,126,.22);color:var(--rose)}
|
| .chip-dot{width:5px;height:5px;border-radius:50%;background:currentColor;flex-shrink:0}
|
|
|
|
|
| #hdr-status{margin-left:auto;display:flex;align-items:center;gap:16px;flex-shrink:0}
|
| #live-clock{font-family:var(--mono);font-size:10px;color:var(--txt2);
|
| display:flex;flex-direction:column;align-items:flex-end;gap:1px;line-height:1}
|
| #hdr-date{font-size:9px;color:var(--txt3)}
|
| #hdr-season{font-size:9px;letter-spacing:.06em;text-transform:uppercase}
|
| .status-pill{
|
| display:flex;align-items:center;gap:7px;padding:5px 12px;
|
| background:var(--glass);border:1px solid var(--border2);border-radius:20px;
|
| }
|
| .status-light{width:6px;height:6px;border-radius:50%;flex-shrink:0;transition:all .4s}
|
| .status-light.amber{background:#f5a623;box-shadow:0 0 8px #f5a623}
|
| .status-light.green{background:var(--jade);box-shadow:0 0 8px var(--jade);animation:pulse 2.5s infinite}
|
| .status-light.red{background:var(--rose);box-shadow:0 0 8px var(--rose)}
|
| @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
| #status-text{font-family:var(--mono);font-size:9px;color:var(--txt2)}
|
|
|
| #cache-bar-wrap{
|
| display:flex;align-items:center;gap:8px;padding:3px 14px;
|
| border-top:1px solid var(--border);background:rgba(8,15,26,.9);
|
| position:absolute;bottom:0;left:0;right:0;z-index:200;
|
| font-family:var(--mono);font-size:7px;color:var(--txt3);
|
| transition:opacity .3s;
|
| }
|
| #cache-bar-wrap.hidden{opacity:0;pointer-events:none}
|
| #cache-progress-bar{flex:1;height:2px;background:rgba(255,255,255,.06);border-radius:1px}
|
| #cache-progress-fill{height:100%;border-radius:1px;background:linear-gradient(90deg,var(--teal),var(--jade));transition:width .5s;width:0%}
|
| #cache-bar-label{white-space:nowrap;color:var(--txt3)}
|
| #btn-build-cache{
|
| font-family:var(--mono);font-size:7px;font-weight:600;letter-spacing:.08em;
|
| padding:3px 9px;border-radius:10px;cursor:pointer;text-transform:uppercase;
|
| background:rgba(0,229,204,.08);border:1px solid rgba(0,229,204,.25);color:var(--teal);
|
| transition:all .15s;flex-shrink:0;
|
| }
|
| #btn-build-cache:hover{background:rgba(0,229,204,.18)}
|
| #btn-build-cache:disabled{opacity:.35;cursor:not-allowed}
|
| #ram-badge{
|
| font-family:var(--mono);font-size:7px;padding:2px 7px;border-radius:8px;
|
| border:1px solid var(--border);color:var(--txt3);white-space:nowrap;flex-shrink:0;
|
| }
|
|
|
|
|
|
|
|
|
| #maps{display:grid;grid-template-columns:1fr 1fr;position:relative;overflow:hidden;padding-bottom:24px}
|
| .map-wrap{position:relative;overflow:hidden}
|
|
|
| .map-wrap:first-child{border-right:1px solid var(--border2)}
|
|
|
|
|
| .map-lbl{
|
| position:absolute;top:16px;left:50%;transform:translateX(-50%);z-index:900;
|
| font-family:var(--mono);font-size:9px;font-weight:500;letter-spacing:.1em;
|
| padding:5px 14px;pointer-events:none;text-transform:uppercase;
|
| background:rgba(8,15,26,.78);backdrop-filter:blur(12px);
|
| border:1px solid;border-radius:20px;white-space:nowrap;
|
| }
|
| .lbl-lr{border-color:rgba(245,166,35,.3);color:var(--gold)}
|
| .lbl-pred{border-color:rgba(0,229,204,.3);color:var(--teal)}
|
| .map-res{
|
| position:absolute;bottom:14px;left:50%;transform:translateX(-50%);z-index:900;
|
| font-family:var(--mono);font-size:8px;color:var(--txt3);
|
| background:rgba(8,15,26,.78);backdrop-filter:blur(8px);
|
| padding:3px 10px;border-radius:12px;border:1px solid var(--border);white-space:nowrap;
|
| }
|
| #map-lr,#map-pred{width:100%;height:100%}
|
| .leaflet-container{background:#050d18!important}
|
| .leaflet-control-attribution{display:none!important}
|
|
|
|
|
| #unc-btn{
|
| position:absolute;bottom:48px;right:14px;z-index:900;
|
| font-family:var(--mono);font-size:8px;font-weight:500;letter-spacing:.08em;
|
| padding:5px 12px;border-radius:16px;cursor:pointer;text-transform:uppercase;
|
| background:rgba(8,15,26,.78);backdrop-filter:blur(12px);
|
| border:1px solid rgba(181,122,255,.3);color:var(--violet);
|
| transition:all .2s;
|
| }
|
| #unc-btn:hover{background:rgba(181,122,255,.12);border-color:rgba(181,122,255,.5)}
|
| #unc-btn.active{background:rgba(181,122,255,.18);border-color:var(--violet);color:#fff}
|
|
|
| #unc-info{
|
| position:absolute;top:52px;right:14px;z-index:901;display:none;
|
| font-family:var(--mono);font-size:8px;
|
| background:rgba(8,15,26,.92);backdrop-filter:blur(16px);
|
| border:1px solid rgba(181,122,255,.3);color:var(--violet);
|
| padding:8px 12px;border-radius:var(--r);line-height:1.9;
|
| }
|
|
|
|
|
| #spinner{
|
| position:absolute;inset:0;display:none;align-items:center;justify-content:center;
|
| background:rgba(8,15,26,.65);backdrop-filter:blur(4px);z-index:2000;
|
| flex-direction:column;gap:12px;
|
| }
|
| #spinner.show{display:flex}
|
| .spin-ring{
|
| width:36px;height:36px;border-radius:50%;
|
| border:1.5px solid rgba(0,229,204,.15);
|
| border-top-color:var(--teal);
|
| animation:rot .8s linear infinite;
|
| }
|
| @keyframes rot{to{transform:rotate(360deg)}}
|
| .spin-txt{font-family:var(--mono);font-size:8px;color:var(--teal);letter-spacing:.1em;text-transform:uppercase}
|
|
|
|
|
| #pt-tip{
|
| position:fixed;display:none;
|
| background:rgba(8,15,26,.95);backdrop-filter:blur(20px);
|
| border:1px solid var(--border2);border-radius:var(--r);
|
| padding:12px 16px;font-family:var(--mono);font-size:10px;line-height:1.8;
|
| color:var(--txt);z-index:3000;pointer-events:none;max-width:200px;
|
| box-shadow:0 8px 32px rgba(0,0,0,.6);
|
| }
|
| .pt-coord{color:var(--teal);font-weight:500;font-size:9px;letter-spacing:.04em}
|
| .pt-row{display:flex;justify-content:space-between;gap:16px}
|
| .pt-lbl{color:var(--txt3)}
|
| .pt-val-gold{color:var(--gold);font-weight:500}
|
| .pt-val-jade{color:var(--jade);font-weight:500}
|
|
|
|
|
| #bottom{
|
| display:grid;
|
| grid-template-columns:200px 1fr 1fr 1fr 1fr 1fr;
|
| gap:1px;background:var(--border);
|
| border-top:1px solid var(--border);flex-shrink:0;overflow:hidden;
|
| }
|
| .panel-cell{background:var(--bg2);overflow:hidden;min-width:0;display:flex;flex-direction:column;justify-content:center}
|
|
|
|
|
| #ctrl-cell{padding:12px 16px;gap:10px}
|
| .ctrl-row{display:flex;align-items:center;gap:8px;width:100%}
|
| .ctrl-lbl{font-family:var(--mono);font-size:7px;color:var(--txt3);letter-spacing:.1em;
|
| text-transform:uppercase;white-space:nowrap;width:48px;flex-shrink:0}
|
|
|
|
|
| #btn-play{
|
| width:32px;height:32px;border-radius:50%;border:1.5px solid var(--teal);
|
| background:rgba(0,229,204,.08);color:var(--teal);cursor:pointer;
|
| display:flex;align-items:center;justify-content:center;flex-shrink:0;
|
| transition:all .2s;font-size:10px;
|
| }
|
| #btn-play:hover{background:rgba(0,229,204,.18);box-shadow:0 0 12px rgba(0,229,204,.2)}
|
| #btn-play.playing{border-color:var(--rose);color:var(--rose);background:rgba(255,95,126,.08)}
|
| #btn-play.playing:hover{background:rgba(255,95,126,.18)}
|
|
|
| #speed-sel{
|
| background:rgba(255,255,255,.04);border:1px solid var(--border2);
|
| color:var(--txt2);padding:3px 6px;font-family:var(--mono);font-size:8px;
|
| cursor:pointer;border-radius:4px;outline:none;
|
| }
|
| #t-val{font-family:var(--mono);font-size:9px;color:var(--txt3);margin-left:auto;white-space:nowrap}
|
|
|
|
|
| input[type=range]{
|
| -webkit-appearance:none;appearance:none;width:100%;height:2px;
|
| background:rgba(255,255,255,.08);border-radius:1px;outline:none;cursor:pointer;
|
| flex:1;
|
| }
|
| input[type=range]::-webkit-slider-thumb{
|
| -webkit-appearance:none;width:12px;height:12px;border-radius:50%;
|
| background:var(--teal);border:none;cursor:pointer;
|
| box-shadow:0 0 8px rgba(0,229,204,.4);transition:transform .15s;
|
| }
|
| input[type=range]::-webkit-slider-thumb:hover{transform:scale(1.3)}
|
| input#op-slider::-webkit-slider-thumb{background:var(--txt2);box-shadow:none}
|
|
|
|
|
| .stat-cell{padding:14px 18px;justify-content:center}
|
| .stat-label{font-family:var(--mono);font-size:7px;color:var(--txt3);letter-spacing:.12em;
|
| text-transform:uppercase;margin-bottom:4px;line-height:1}
|
| .stat-number{font-family:var(--display);font-size:26px;font-weight:600;line-height:1;
|
| letter-spacing:-.02em;transition:color .3s}
|
| .stat-unit{font-size:11px;font-weight:400;color:var(--txt3);margin-left:2px}
|
| .stat-sub{font-family:var(--mono);font-size:8px;color:var(--txt3);margin-top:3px;line-height:1}
|
| .c-teal{color:var(--teal)}.c-gold{color:var(--gold)}.c-jade{color:var(--jade)}
|
| .c-violet{color:var(--violet)}.c-rose{color:var(--rose)}
|
|
|
|
|
| .stat-cell::before{
|
| content:'';display:block;height:1.5px;width:28px;
|
| border-radius:1px;margin-bottom:10px;flex-shrink:0;
|
| }
|
| .stat-cell.acc-gold::before{background:var(--gold)}
|
| .stat-cell.acc-jade::before{background:var(--jade)}
|
| .stat-cell.acc-violet::before{background:var(--violet)}
|
|
|
|
|
| .chart-cell{padding:0;position:relative;background:var(--bg2)}
|
| .chart-label{
|
| position:absolute;top:8px;left:12px;z-index:2;pointer-events:none;
|
| font-family:var(--mono);font-size:7px;color:var(--txt3);letter-spacing:.1em;text-transform:uppercase;
|
| }
|
| .chart-cell canvas{width:100%;height:100%;display:block}
|
| </style>
|
| </head>
|
| <body>
|
| <div id="app">
|
|
|
|
|
| <header id="hdr">
|
| <div id="logo">
|
| <div id="logo-mark">AI</div>
|
| <div id="logo-text">
|
| <div id="logo-name">ATMOS</div>
|
| <div id="logo-sub">Climate Downscaling System</div>
|
| </div>
|
| </div>
|
|
|
| <div class="hdr-div"></div>
|
|
|
| <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
| <span class="chip chip-gold"><span class="chip-dot"></span>ERA5 Β· 0.25Β° Β· 28 km/px</span>
|
| <span class="chip chip-teal"><span class="chip-dot"></span>AI Β· 0.0625Β° Β· 7 km/px Β· 4ΓSR</span>
|
| <span class="chip chip-jade">Swin2SR Ensemble Β· 24.2M INT8</span>
|
| <span class="chip chip-violet">TTA Uncertainty</span>
|
| <span class="chip chip-rose">PSD Analysis</span>
|
| </div>
|
|
|
| <div id="hdr-status">
|
| <div id="live-clock">
|
| <span id="hdr-date" style="color:var(--txt2)">β</span>
|
| <span id="hdr-season">β</span>
|
| </div>
|
| <div class="status-pill">
|
| <div id="status-dot" class="status-light amber"></div>
|
| <span id="status-text">Connecting</span>
|
| </div>
|
| <div class="status-pill" style="flex-direction:column;gap:4px;padding:6px 12px;min-width:140px">
|
| <div style="display:flex;justify-content:space-between;align-items:center;width:100%">
|
| <span style="font-family:var(--mono);font-size:7px;color:var(--txt3);letter-spacing:.08em;text-transform:uppercase">Cache</span>
|
| <span id="cache-pct" style="font-family:var(--mono);font-size:8px;color:var(--teal);font-weight:500">0%</span>
|
| </div>
|
| <div style="width:100%;height:2px;background:rgba(255,255,255,.06);border-radius:1px">
|
| <div id="cache-bar" style="height:2px;background:var(--teal);border-radius:1px;width:0%;transition:width .5s"></div>
|
| </div>
|
| <div style="display:flex;justify-content:space-between;align-items:center;width:100%">
|
| <span style="font-family:var(--mono);font-size:7px;color:var(--txt3);letter-spacing:.08em;text-transform:uppercase">RAM</span>
|
| <span id="ram-pct" style="font-family:var(--mono);font-size:8px;color:var(--gold)">β</span>
|
| </div>
|
| <div style="width:100%;height:2px;background:rgba(255,255,255,.06);border-radius:1px">
|
| <div id="ram-bar" style="height:2px;background:var(--gold);border-radius:1px;width:0%;transition:width .5s"></div>
|
| </div>
|
| </div>
|
| </div>
|
| </header>
|
|
|
|
|
| <div id="maps">
|
| <div class="map-wrap">
|
| <div class="map-lbl lbl-lr">ERA5 Baseline Β· 0.25Β°</div>
|
| <div id="map-lr"></div>
|
| <div class="map-res" id="res-lr">129 Γ 121</div>
|
| </div>
|
| <div class="map-wrap">
|
| <div class="map-lbl lbl-pred">Ensemble AI Β· 0.0625Β° Β· Mask+Phys+Elev</div>
|
| <div id="map-pred"></div>
|
| <div class="map-res" id="res-pred">516 Γ 484</div>
|
| <button id="unc-btn" onclick="toggleUncertainty()">⬑ Uncertainty</button>
|
| <div id="unc-info"></div>
|
| </div>
|
| <div id="spinner">
|
| <div class="spin-ring"></div>
|
| <span class="spin-txt">Computing</span>
|
| </div>
|
| </div>
|
|
|
|
|
| <div id="bottom">
|
|
|
|
|
| <div class="panel-cell" id="ctrl-cell">
|
| <div class="ctrl-row">
|
| <button id="btn-play" title="Play/Pause">βΆ</button>
|
| <select id="speed-sel">
|
| <option value="2000">0.5Γ</option>
|
| <option value="1000" selected>1Γ</option>
|
| <option value="500">2Γ</option>
|
| <option value="200">5Γ</option>
|
| </select>
|
| <span id="t-val">T: 0</span>
|
| </div>
|
| <div class="ctrl-row">
|
| <span class="ctrl-lbl">Time</span>
|
| <input type="range" id="t-slider" min="0" max="8783" value="0">
|
| </div>
|
| <div class="ctrl-row">
|
| <span class="ctrl-lbl">Opacity</span>
|
| <input type="range" id="op-slider" min="20" max="95" value="80">
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="panel-cell stat-cell acc-gold">
|
| <div class="stat-label">Mean Temperature</div>
|
| <div class="stat-number c-gold" id="s-mean">β<span class="stat-unit">K</span></div>
|
| <div class="stat-sub" id="s-mean-c">β Β°C</div>
|
| </div>
|
|
|
|
|
| <div class="panel-cell stat-cell acc-jade">
|
| <div class="stat-label">AI Sharpness Gain</div>
|
| <div class="stat-number c-jade" id="s-sharp">β</div>
|
| <div class="stat-sub">Laplacian βΒ² ratio</div>
|
| </div>
|
|
|
|
|
| <div class="panel-cell stat-cell acc-violet">
|
| <div class="stat-label">Prediction Uncertainty</div>
|
| <div class="stat-number c-violet" id="s-unc">β<span class="stat-unit">K</span></div>
|
| <div class="stat-sub" id="s-unc-sub">TTA 8-jitter mean</div>
|
| </div>
|
|
|
|
|
| <div class="panel-cell chart-cell">
|
| <div class="chart-label">Annual Cycle Β· click map</div>
|
| <canvas id="ts-canvas"></canvas>
|
| </div>
|
|
|
|
|
| <div class="panel-cell chart-cell">
|
| <div class="chart-label" style="color:rgba(255,95,126,.6)">Power Spectrum Β· ERA5 vs AI</div>
|
| <canvas id="psd-canvas"></canvas>
|
| </div>
|
|
|
| </div>
|
| </div>
|
|
|
| <div id="pt-tip"></div>
|
|
|
| <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
| <script>
|
|
|
| const API='/api', BOUNDS=[[6,68],[38,98]], CENTER=[22,82];
|
| const CITIES=[
|
| ['New Delhi',28.6,77.2],['Mumbai',19.1,72.9],['Hyderabad',17.4,78.5],
|
| ['Kukatpally',17.5,78.4],['Bengaluru',12.97,77.6],['Chennai',13.1,80.3],
|
| ['Kolkata',22.6,88.4],['Jaipur',26.9,75.8],['Ahmedabad',23.0,72.6],
|
| ['Pune',18.5,73.9],['Bhopal',23.3,77.4],['Lucknow',26.8,80.9],
|
| ['Patna',25.6,85.1],['Guwahati',26.2,91.7],['Shimla',31.1,77.2],
|
| ['Srinagar',34.1,74.8],['Thiruvananthapuram',8.5,76.9],['Bhubaneswar',20.3,85.8],
|
| ];
|
| let meta=null,tIdx=0,isPlaying=false,playTimer=null;
|
| let overlayLR=null,overlayPred=null,overlayUnc=null,opacity=0.80,uncActive=false;
|
|
|
|
|
| const TILE='https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png';
|
| const mapLR=L.map('map-lr',{zoomControl:false,attributionControl:false}).setView(CENTER,5);
|
| const mapPred=L.map('map-pred',{zoomControl:false,attributionControl:false}).setView(CENTER,5);
|
| L.tileLayer(TILE,{subdomains:'abcd',maxZoom:18}).addTo(mapLR);
|
| L.tileLayer(TILE,{subdomains:'abcd',maxZoom:18}).addTo(mapPred);
|
| L.control.zoom({position:'bottomright'}).addTo(mapPred);
|
|
|
| let syncing=false;
|
| function syncMaps(s,d){if(syncing)return;syncing=true;d.setView(s.getCenter(),s.getZoom(),{animate:false});syncing=false;}
|
| mapLR.on('move',()=>syncMaps(mapLR,mapPred));
|
| mapPred.on('move',()=>syncMaps(mapPred,mapLR));
|
|
|
|
|
| fetch('https://raw.githubusercontent.com/datameet/maps/master/States/India_States.geojson')
|
| .then(r=>r.json()).then(gj=>{
|
| const s={color:'rgba(255,255,255,.07)',weight:1,fillOpacity:0};
|
| [mapLR,mapPred].forEach(m=>L.geoJSON(gj,{style:s}).addTo(m));
|
| }).catch(()=>{});
|
|
|
|
|
| function addCities(m){
|
| CITIES.forEach(([n,lat,lon])=>{
|
| const k=n==='Kukatpally';
|
| const mj=['New Delhi','Mumbai','Hyderabad','Chennai','Kolkata','Bengaluru'].includes(n);
|
| const c=k?'#ff5f7e':mj?'#00e5cc':'#3d5470';
|
| const r=k?5:mj?4:3;
|
| L.circleMarker([lat,lon],{radius:r,color:c,weight:k?2:1.5,fillColor:c,fillOpacity:.9})
|
| .bindTooltip(n,{permanent:false,direction:'top',offset:[0,-4],
|
| className:'leaflet-tooltip-custom'}).addTo(m);
|
| });
|
| }
|
| addCities(mapLR); addCities(mapPred);
|
|
|
|
|
| const style=document.createElement('style');
|
| style.textContent=`.leaflet-tooltip-custom{background:rgba(8,15,26,.92);border:1px solid rgba(0,229,204,.3);color:#c8daf0;font-family:'Fira Code',monospace;font-size:9px;padding:3px 8px;border-radius:4px;box-shadow:none;}`;
|
| document.head.appendChild(style);
|
| </script>
|
| <script>
|
|
|
| function setupCanvas(id){
|
| const c=document.getElementById(id);
|
| const W=c.offsetWidth||180,H=c.offsetHeight||148;
|
| c.width=W*devicePixelRatio; c.height=H*devicePixelRatio;
|
| const ctx=c.getContext('2d'); ctx.scale(devicePixelRatio,devicePixelRatio);
|
| return {c,ctx,W,H};
|
| }
|
|
|
|
|
| function drawTimeseries(vals_C){
|
| const {ctx,W,H}=setupCanvas('ts-canvas');
|
| ctx.clearRect(0,0,W,H);
|
| if(!vals_C||vals_C.length<2) return;
|
| const pad={t:22,b:18,l:6,r:6};
|
| const cw=W-pad.l-pad.r, ch=H-pad.t-pad.b;
|
| const mn=Math.min(...vals_C), mx=Math.max(...vals_C), rng=mx-mn||1;
|
| const px=i=>pad.l+i/(vals_C.length-1)*cw;
|
| const py=v=>pad.t+ch*(1-(v-mn)/rng);
|
|
|
| const bands=[
|
| {s:0,e:.17,c:'rgba(0,100,255,.06)',lbl:'WIN'},
|
| {s:.17,e:.42,c:'rgba(245,166,35,.05)',lbl:'PRE'},
|
| {s:.42,e:.75,c:'rgba(0,200,100,.06)',lbl:'MON'},
|
| {s:.75,e:1,c:'rgba(0,150,255,.05)',lbl:'POS'},
|
| ];
|
| bands.forEach(({s,e,c,lbl})=>{
|
| ctx.fillStyle=c; ctx.fillRect(pad.l+s*cw,pad.t,(e-s)*cw,ch);
|
| ctx.font=`${6*devicePixelRatio}px Fira Code`; ctx.fillStyle='rgba(61,84,112,.8)';
|
| ctx.fillText(lbl, pad.l+(s+.01)*cw, pad.t+8);
|
| });
|
|
|
| ctx.setLineDash([2,4]);
|
| [.25,.5,.75].forEach(f=>{
|
| const y=pad.t+ch*f;
|
| ctx.beginPath(); ctx.moveTo(pad.l,y); ctx.lineTo(pad.l+cw,y);
|
| ctx.strokeStyle='rgba(255,255,255,.04)'; ctx.lineWidth=.5; ctx.stroke();
|
| });
|
| ctx.setLineDash([]);
|
|
|
| ctx.beginPath();
|
| vals_C.forEach((v,i)=>{const x=px(i),y=py(v);i?ctx.lineTo(x,y):ctx.moveTo(x,y);});
|
| ctx.lineTo(px(vals_C.length-1),pad.t+ch); ctx.lineTo(pad.l,pad.t+ch); ctx.closePath();
|
| const grad=ctx.createLinearGradient(0,pad.t,0,pad.t+ch);
|
| grad.addColorStop(0,'rgba(0,229,204,.15)'); grad.addColorStop(1,'rgba(0,229,204,.01)');
|
| ctx.fillStyle=grad; ctx.fill();
|
|
|
| ctx.beginPath();
|
| vals_C.forEach((v,i)=>{const x=px(i),y=py(v);i?ctx.lineTo(x,y):ctx.moveTo(x,y);});
|
| ctx.strokeStyle='rgba(0,229,204,.9)'; ctx.lineWidth=1.3; ctx.stroke();
|
|
|
| ctx.font=`${7*devicePixelRatio}px Fira Code`; ctx.fillStyle='rgba(122,152,188,.7)';
|
| ctx.fillText(mn.toFixed(1)+'Β°C',pad.l,H-3);
|
| ctx.textAlign='right'; ctx.fillText(mx.toFixed(1)+'Β°C',W-pad.r,H-3); ctx.textAlign='left';
|
| }
|
|
|
|
|
| function drawPSD(data){
|
| const {ctx,W,H}=setupCanvas('psd-canvas');
|
| ctx.clearRect(0,0,W,H);
|
| if(!data||!data.wavelengths_km) return;
|
| const pad={t:22,b:18,l:6,r:6};
|
| const cw=W-pad.l-pad.r, ch=H-pad.t-pad.b;
|
| const wl=data.wavelengths_km,p0=data.psd_era5,p1=data.psd_ai;
|
| const n=wl.length; if(n<2) return;
|
| const allV=[...p0,...p1]; const mn=Math.min(...allV),mx=Math.max(...allV),rng=mx-mn||1;
|
|
|
| const px=i=>pad.l+(1-i/(n-1))*cw;
|
| const py=v=>pad.t+ch*(1-(v-mn)/rng);
|
|
|
| ctx.setLineDash([2,4]);
|
| [.25,.5,.75].forEach(f=>{
|
| const y=pad.t+ch*f;
|
| ctx.beginPath();ctx.moveTo(pad.l,y);ctx.lineTo(pad.l+cw,y);
|
| ctx.strokeStyle='rgba(255,255,255,.04)';ctx.lineWidth=.5;ctx.stroke();
|
| });
|
| ctx.setLineDash([]);
|
|
|
| ctx.beginPath();
|
| p1.forEach((v,i)=>{i?ctx.lineTo(px(i),py(v)):ctx.moveTo(px(i),py(v));});
|
| for(let i=n-1;i>=0;i--) ctx.lineTo(px(i),py(p0[i]));
|
| ctx.closePath(); ctx.fillStyle='rgba(0,229,204,.07)'; ctx.fill();
|
|
|
| ctx.beginPath();
|
| p0.forEach((v,i)=>{i?ctx.lineTo(px(i),py(v)):ctx.moveTo(px(i),py(v));});
|
| ctx.strokeStyle='rgba(245,166,35,.55)'; ctx.lineWidth=1.2; ctx.stroke();
|
|
|
| ctx.beginPath();
|
| p1.forEach((v,i)=>{i?ctx.lineTo(px(i),py(v)):ctx.moveTo(px(i),py(v));});
|
| ctx.strokeStyle='rgba(0,229,204,.9)'; ctx.lineWidth=1.5; ctx.stroke();
|
|
|
| ctx.font=`${7*devicePixelRatio}px Fira Code`; ctx.fillStyle='rgba(122,152,188,.6)';
|
| ctx.fillText('500km',pad.l,H-3); ctx.textAlign='right';
|
| ctx.fillText('10km',W-pad.r,H-3); ctx.textAlign='left';
|
| ctx.fillStyle='rgba(0,229,204,.75)'; ctx.fillText('AI',W-pad.r-18,pad.t+10);
|
| ctx.fillStyle='rgba(245,166,35,.65)'; ctx.fillText('ERA5',W-pad.r-44,pad.t+10);
|
| }
|
| </script>
|
| <script>
|
|
|
| const spinner=document.getElementById('spinner');
|
|
|
| async function loadFrame(t){
|
| tIdx=t;
|
| document.getElementById('t-val').textContent='T:'+String(t).padStart(5,' ');
|
| document.getElementById('hdr-date').textContent=getDateStr(t);
|
| document.getElementById('hdr-season').textContent=getSeason(t);
|
| if(!isPlaying) spinner.classList.add('show');
|
| const ts=Date.now();
|
| let done=0;
|
| const finish=()=>{if(++done===2)spinner.classList.remove('show');};
|
| const i1=new Image();
|
| i1.onload=()=>{if(overlayLR)mapLR.removeLayer(overlayLR);overlayLR=L.imageOverlay(`${API}/map/lr/${t}.png?_=${ts}`,BOUNDS,{opacity,interactive:false}).addTo(mapLR);finish();};
|
| i1.onerror=finish; i1.src=`${API}/map/lr/${t}.png?_=${ts}`;
|
| const i2=new Image();
|
| i2.onload=()=>{if(overlayPred)mapPred.removeLayer(overlayPred);overlayPred=L.imageOverlay(`${API}/map/pred/${t}.png?_=${ts}`,BOUNDS,{opacity,interactive:false}).addTo(mapPred);finish();};
|
| i2.onerror=finish; i2.src=`${API}/map/pred/${t}.png?_=${ts}`;
|
| if(uncActive) loadUncertainty(t);
|
| fetch(`${API}/cache/status`).then(r=>r.json()).then(d=>{
|
| const pct=d.progress_pct||0;
|
| document.getElementById('cache-pct').textContent=d.ready?'Ready':pct.toFixed(0)+'%';
|
| document.getElementById('cache-bar').style.width=pct+'%';
|
| document.getElementById('cache-bar').style.background=d.ready?'var(--jade)':'var(--teal)';
|
| document.getElementById('ram-pct').textContent=d.ram_pct+'%';
|
| document.getElementById('ram-bar').style.width=d.ram_pct+'%';
|
| const rbar=document.getElementById('ram-bar');
|
| rbar.style.background=d.ram_pct>90?'var(--rose)':d.ram_pct>75?'var(--gold)':'var(--jade)';
|
| }).catch(()=>{});
|
| fetch(`${API}/stats/${t}`).then(r=>r.json()).then(d=>{
|
| const K=d.mean_temp_K||0;
|
| setNum('s-mean', K.toFixed(1), 'K');
|
| document.getElementById('s-mean-c').textContent=(K-273.15).toFixed(1)+' Β°C';
|
| const sg=d.sharpness_gain_pct;
|
| setNum('s-sharp', (sg>0?'+':'')+sg+'%');
|
| }).catch(()=>{});
|
| fetch(`${API}/psd/${t}`).then(r=>r.json()).then(drawPSD).catch(()=>{});
|
| }
|
|
|
|
|
| function setNum(id, txt, unit=''){
|
| const el=document.getElementById(id);
|
| el.style.opacity='.4'; el.style.transform='translateY(4px)';
|
| el.style.transition='opacity .25s,transform .25s';
|
| setTimeout(()=>{
|
| el.innerHTML=txt+(unit?`<span class="stat-unit">${unit}</span>`:'');
|
| el.style.opacity='1'; el.style.transform='translateY(0)';
|
| },120);
|
| }
|
|
|
|
|
| function toggleUncertainty(){
|
| uncActive=!uncActive;
|
| document.getElementById('unc-btn').classList.toggle('active',uncActive);
|
| if(uncActive) loadUncertainty(tIdx);
|
| else{if(overlayUnc){mapPred.removeLayer(overlayUnc);overlayUnc=null;}document.getElementById('unc-info').style.display='none';}
|
| }
|
| function loadUncertainty(t){
|
| const url=`${API}/uncertainty/${t}.png?_=${Date.now()}`;
|
| const img=new Image();
|
| img.onload=()=>{if(overlayUnc)mapPred.removeLayer(overlayUnc);overlayUnc=L.imageOverlay(url,BOUNDS,{opacity:.65,interactive:false}).addTo(mapPred);};
|
| img.src=url;
|
| fetch(`${API}/uncertainty/${t}/stats`).then(r=>r.json()).then(d=>{
|
| setNum('s-unc',d.mean_uncertainty_K?.toFixed(3)||'β','K');
|
| document.getElementById('s-unc-sub').textContent='p95 = '+(d.p95_uncertainty_K?.toFixed(3)||'β')+'K';
|
| const info=document.getElementById('unc-info');
|
| info.style.display='block';
|
| info.innerHTML=`Mean ${d.mean_uncertainty_K?.toFixed(3)}K Β· Max ${d.max_uncertainty_K?.toFixed(3)}K Β· ${d.high_uncertainty_pct}% high`;
|
| }).catch(()=>{});
|
| }
|
|
|
|
|
| function handleClick(e){
|
| const lat=e.latlng.lat.toFixed(3),lon=e.latlng.lng.toFixed(3);
|
| const tip=document.getElementById('pt-tip');
|
| fetch(`${API}/point/${tIdx}/${lat}/${lon}`).then(r=>r.json()).then(d=>{
|
| tip.style.display='block';
|
| tip.style.left=(e.originalEvent.clientX+14)+'px';
|
| tip.style.top=(e.originalEvent.clientY-10)+'px';
|
| tip.innerHTML=`<div class="pt-coord">${lat}Β°N ${lon}Β°E</div>
|
| <div class="pt-row"><span class="pt-lbl">ERA5</span><span class="pt-val-gold">${d.era5_C} Β°C</span></div>
|
| <div class="pt-row"><span class="pt-lbl">AI</span><span class="pt-val-jade">${d.pred_C} Β°C</span></div>`;
|
| }).catch(()=>{});
|
| fetch(`${API}/timeseries/${lat}/${lon}?step=48`).then(r=>r.json())
|
| .then(d=>drawTimeseries(d.values_C)).catch(()=>{});
|
| }
|
| mapLR.on('click',handleClick);
|
| mapPred.on('click',handleClick);
|
| document.addEventListener('keydown',()=>document.getElementById('pt-tip').style.display='none');
|
|
|
|
|
| document.getElementById('t-slider').addEventListener('input',e=>{if(isPlaying)togglePlay();loadFrame(parseInt(e.target.value));});
|
| document.getElementById('op-slider').addEventListener('input',e=>{
|
| opacity=parseInt(e.target.value)/100;
|
| if(overlayLR)overlayLR.setOpacity(opacity);
|
| if(overlayPred)overlayPred.setOpacity(opacity);
|
| });
|
| function togglePlay(){
|
| isPlaying=!isPlaying;
|
| const btn=document.getElementById('btn-play');
|
| if(isPlaying){
|
| btn.textContent='βΈ'; btn.classList.add('playing');
|
| const spd=parseInt(document.getElementById('speed-sel').value);
|
| playTimer=setInterval(()=>{const n=(tIdx+4)%(meta?.timesteps||8784);document.getElementById('t-slider').value=n;loadFrame(n);},spd);
|
| } else {
|
| btn.textContent='βΆ'; btn.classList.remove('playing'); clearInterval(playTimer);
|
| }
|
| }
|
| document.getElementById('btn-play').addEventListener('click',togglePlay);
|
| document.getElementById('speed-sel').addEventListener('change',()=>{if(isPlaying){togglePlay();togglePlay();}});
|
|
|
|
|
| function getSeason(t){const m=Math.floor((t/8784)*12);if(m<2||m===11)return'Winter';if(m<5)return'Pre-Monsoon';if(m<9)return'Monsoon';return'Post-Monsoon';}
|
| function getDateStr(t){const d=new Date('2020-01-01T00:00:00Z');d.setUTCHours(d.getUTCHours()+t);return d.toUTCString().slice(5,16)+' '+String(d.getUTCHours()).padStart(2,'0')+':00 UTC';}
|
|
|
|
|
| let _retryTimer = null;
|
| async function init(){
|
| const dot=document.getElementById('status-dot');
|
| const stxt=document.getElementById('status-text');
|
| try{
|
| const r=await fetch(`${API}/meta`); meta=await r.json();
|
| if(meta.output_shape) document.getElementById('res-pred').textContent=meta.output_shape[0]+'Γ'+meta.output_shape[1];
|
| document.getElementById('t-slider').max=(meta.timesteps||8784)-1;
|
| mapLR.fitBounds(BOUNDS,{padding:[4,4]});
|
| mapPred.fitBounds(BOUNDS,{padding:[4,4]});
|
| dot.className='status-light green'; stxt.textContent='Live';
|
| if(_retryTimer){clearInterval(_retryTimer);_retryTimer=null;}
|
| loadFrame(0);
|
| startCachePolling();
|
| } catch(e){
|
| dot.className='status-light red'; stxt.textContent='Offline β retryingβ¦';
|
| spinner.classList.remove('show');
|
|
|
| if(!_retryTimer) _retryTimer=setInterval(()=>init(), 3000);
|
| }
|
| }
|
| init();
|
|
|
|
|
| let cachePoller = null;
|
| let lastInferMs = null;
|
|
|
| function updateCacheBar(d) {
|
| const wrap = document.getElementById('cache-bar-wrap');
|
| const fill = document.getElementById('cache-progress-fill');
|
| const label = document.getElementById('cache-bar-label');
|
| const ram = document.getElementById('ram-badge');
|
| const btn = document.getElementById('btn-build-cache');
|
|
|
| if (!wrap) return;
|
|
|
| const pct = d.progress_pct || 0;
|
| const ready = d.ready;
|
| const running = pct > 0 && !ready;
|
| const idle = pct === 0 && !ready;
|
|
|
| fill.style.width = pct + '%';
|
| ram.textContent = 'RAM ' + (d.ram_pct || 'β') + '%';
|
|
|
| if (ready) {
|
| label.textContent = 'β Cache complete β all ' + d.total_frames + ' frames instant';
|
| fill.style.background = 'var(--jade)';
|
| btn.disabled = true; btn.textContent = 'β Done';
|
| wrap.classList.remove('hidden');
|
|
|
| setTimeout(() => wrap.classList.add('hidden'), 5000);
|
| clearInterval(cachePoller); cachePoller = null;
|
| } else if (running) {
|
| const eta = d.eta_sec;
|
| const etaStr = eta ? (Math.floor(eta/60)+'m '+String(eta%60).padStart(2,'0')+'s') : 'β¦';
|
| label.textContent = 'Building cache: ' + pct.toFixed(1) + '% β ETA ' + etaStr;
|
| fill.style.background = 'linear-gradient(90deg,var(--teal),var(--jade))';
|
| btn.disabled = true; btn.textContent = 'β³ Building';
|
| wrap.classList.remove('hidden');
|
| } else {
|
|
|
| const inf = lastInferMs ? (' Β· live inference ~' + Math.round(lastInferMs/1000) + 's/frame') : '';
|
| label.textContent = 'Frame cache: idle' + inf;
|
| wrap.classList.remove('hidden');
|
| btn.disabled = false; btn.textContent = 'β‘ Build Cache';
|
| }
|
| }
|
|
|
| function startCachePolling() {
|
| if (cachePoller) return;
|
| cachePoller = setInterval(() => {
|
| fetch(API + '/cache/status').then(r => r.json()).then(updateCacheBar).catch(() => {});
|
| }, 6000);
|
|
|
| fetch(API + '/cache/status').then(r => r.json()).then(updateCacheBar).catch(() => {});
|
| }
|
|
|
| async function triggerCacheBuild() {
|
| const btn = document.getElementById('btn-build-cache');
|
| btn.disabled = true; btn.textContent = 'Startingβ¦';
|
| try {
|
| const r = await fetch(API + '/cache/build', {method:'POST'});
|
| const d = await r.json();
|
| if (d.status === 'started') {
|
| btn.textContent = 'β³ Building';
|
| startCachePolling();
|
| } else if (d.status === 'already_running') {
|
| btn.textContent = 'β³ Running';
|
| } else if (d.status === 'already_complete') {
|
| btn.textContent = 'β Done';
|
| }
|
| } catch(e) {
|
| btn.disabled = false; btn.textContent = 'β‘ Build Cache';
|
| }
|
| }
|
|
|
|
|
| </script>
|
| </body>
|
| </html>
|
|
|