Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> | |
| <title>GPS Dashboard Pro</title> | |
| <style> | |
| :root{ | |
| --bg:#0a0b0d; | |
| --panel:#16181c; | |
| --good:#00ff9d; | |
| --mid:#ffaa00; | |
| --bad:#ff4444; | |
| } | |
| body{ | |
| margin:0; | |
| background:var(--bg); | |
| color:#fff; | |
| font-family:system-ui; | |
| display:flex; | |
| flex-direction:column; | |
| height:100dvh; | |
| } | |
| /* MAIN SPEED */ | |
| .main{ | |
| flex:2; | |
| display:flex; | |
| flex-direction:column; | |
| justify-content:center; | |
| align-items:center; | |
| } | |
| #speed{ | |
| font-size:clamp(90px,30vw,180px); | |
| font-weight:900; | |
| margin:0; | |
| letter-spacing:-2px; | |
| } | |
| .unit{ | |
| color:#777; | |
| letter-spacing:6px; | |
| } | |
| /* STATUS BAR */ | |
| .status{ | |
| text-align:center; | |
| padding:10px; | |
| font-size:12px; | |
| letter-spacing:2px; | |
| background:rgba(255,255,255,0.05); | |
| } | |
| /* ADDRESS */ | |
| .address{ | |
| text-align:center; | |
| padding:10px; | |
| font-size:14px; | |
| color:#ccc; | |
| border-bottom:1px solid #222; | |
| } | |
| /* GRID */ | |
| .panel{ | |
| background:var(--panel); | |
| padding:12px; | |
| } | |
| .grid{ | |
| display:grid; | |
| grid-template-columns:repeat(3,1fr); | |
| gap:12px; | |
| font-size:11px; | |
| } | |
| .item{ | |
| text-align:center; | |
| } | |
| .label{ | |
| color:#666; | |
| font-size:10px; | |
| } | |
| .value{ | |
| font-family:monospace; | |
| font-size:13px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="main"> | |
| <p id="speed">0.0</p> | |
| <p class="unit">KM/H</p> | |
| </div> | |
| <div id="status" class="status">INITIALIZING GPS...</div> | |
| <div id="address" class="address">Detecting address...</div> | |
| <div class="panel"> | |
| <div class="grid"> | |
| <div class="item"><div class="label">Heading</div><div id="head" class="value">---</div></div> | |
| <div class="item"><div class="label">Accuracy</div><div id="acc" class="value">---</div></div> | |
| <div class="item"><div class="label">Altitude</div><div id="alt" class="value">---</div></div> | |
| <div class="item"><div class="label">Lat</div><div id="lat" class="value">---</div></div> | |
| <div class="item"><div class="label">Lon</div><div id="lon" class="value">---</div></div> | |
| <div class="item"><div class="label">Confidence</div><div id="conf" class="value">---</div></div> | |
| </div> | |
| </div> | |
| <script> | |
| // --- ELEMENTS --- | |
| const speedEl = document.getElementById("speed"); | |
| const statusEl = document.getElementById("status"); | |
| const addressEl = document.getElementById("address"); | |
| const headEl = document.getElementById("head"); | |
| const accEl = document.getElementById("acc"); | |
| const altEl = document.getElementById("alt"); | |
| const latEl = document.getElementById("lat"); | |
| const lonEl = document.getElementById("lon"); | |
| const confEl = document.getElementById("conf"); | |
| // --- STATE --- | |
| let state={ | |
| lastFix:null, | |
| emaSpeed:null, | |
| firstGood:false, | |
| lastHeading:null, | |
| lastGeoKey:"", | |
| lastGeoTime:0 | |
| }; | |
| // --- HAVERSINE --- | |
| function dist(a,b){ | |
| const R=6371000; | |
| const toRad=x=>x*Math.PI/180; | |
| const dLat=toRad(b[0]-a[0]); | |
| const dLon=toRad(b[1]-a[1]); | |
| const x=Math.sin(dLat/2)**2+ | |
| Math.cos(toRad(a[0]))*Math.cos(toRad(b[0]))* | |
| Math.sin(dLon/2)**2; | |
| return 2*R*Math.asin(Math.sqrt(x)); | |
| } | |
| // --- SAFE DERIVED --- | |
| function safeDerived(c,ts){ | |
| if(!state.lastFix) return null; | |
| const dt=(ts-state.lastFix.ts)/1000; | |
| if(dt<0.8||dt>5) return null; | |
| const d=dist( | |
| [state.lastFix.lat,state.lastFix.lon], | |
| [c.latitude,c.longitude] | |
| ); | |
| if(d<3) return 0; | |
| const s=d/dt; | |
| if(s>55) return null; | |
| return s; | |
| } | |
| // --- BEST SPEED --- | |
| function bestSpeed(c,ts){ | |
| if(c.speed && c.speed>0.5) return c.speed; | |
| const d=safeDerived(c,ts); | |
| return d ?? 0; | |
| } | |
| // --- SMOOTH --- | |
| function smooth(v){ | |
| if(state.emaSpeed===null){ | |
| state.emaSpeed=v; | |
| return v; | |
| } | |
| const a=v>state.emaSpeed?0.35:0.2; | |
| state.emaSpeed=a*v+(1-a)*state.emaSpeed; | |
| if(state.emaSpeed<0.2) state.emaSpeed=0; | |
| return state.emaSpeed; | |
| } | |
| // --- HEADING --- | |
| function stableHeading(h,s){ | |
| if(s<1.5) return state.lastHeading; | |
| if(h==null) return state.lastHeading; | |
| if(state.lastHeading==null){ | |
| state.lastHeading=h; | |
| return h; | |
| } | |
| let diff=h-state.lastHeading; | |
| if(diff>180) diff-=360; | |
| if(diff<-180) diff+=360; | |
| state.lastHeading+=diff*0.25; | |
| return state.lastHeading; | |
| } | |
| function headingText(h){ | |
| if(h==null) return "---"; | |
| const d=['N','NE','E','SE','S','SW','W','NW']; | |
| return d[Math.round(h/45)%8]+" "+Math.round(h)+"°"; | |
| } | |
| // --- CONF --- | |
| function conf(acc){ | |
| if(acc<=10) return ["HIGH","#00ff9d"]; | |
| if(acc<=30) return ["MED","#ffaa00"]; | |
| return ["LOW","#ff4444"]; | |
| } | |
| // --- ADDRESS --- | |
| async function updateAddress(lat,lon){ | |
| const key=lat.toFixed(3)+","+lon.toFixed(3); | |
| const now=Date.now(); | |
| if(key===state.lastGeoKey) return; | |
| if(now-state.lastGeoTime<20000) return; | |
| state.lastGeoKey=key; | |
| state.lastGeoTime=now; | |
| try{ | |
| const r=await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}`); | |
| const d=await r.json(); | |
| addressEl.textContent=d.display_name?.split(",").slice(0,3).join(",") || "Unknown"; | |
| }catch{} | |
| } | |
| // --- MAIN --- | |
| navigator.geolocation.watchPosition((pos)=>{ | |
| const c=pos.coords; | |
| const ts=Date.now(); | |
| let v=bestSpeed(c,ts); | |
| let final; | |
| if(!state.firstGood){ | |
| if(v>1.5 && v<40){ | |
| state.firstGood=true; | |
| final=v; | |
| }else final=0; | |
| }else{ | |
| final=smooth(v); | |
| } | |
| const kmh=(final*3.6).toFixed(1); | |
| const [label,color]=conf(c.accuracy); | |
| speedEl.textContent=kmh; | |
| speedEl.style.color=color; | |
| statusEl.textContent=`CONFIDENCE: ${label}`; | |
| statusEl.style.color=color; | |
| accEl.textContent=Math.round(c.accuracy)+"m"; | |
| altEl.textContent=c.altitude!=null?Math.round(c.altitude)+"m":"N/A"; | |
| latEl.textContent=c.latitude.toFixed(4); | |
| lonEl.textContent=c.longitude.toFixed(4); | |
| const h=stableHeading(c.heading,kmh); | |
| headEl.textContent=headingText(h); | |
| confEl.textContent=label; | |
| updateAddress(c.latitude,c.longitude); | |
| state.lastFix={lat:c.latitude,lon:c.longitude,ts}; | |
| },()=>{},{ | |
| enableHighAccuracy:true, | |
| maximumAge:5000, | |
| timeout:10000 | |
| }); | |
| </script> | |
| </body> | |
| </html> |