Vessel ETA Calculator
Pro + Compass

Developed By MSTL: Anil Pillai
Live GPS N/S E/W, UTC/LT, Compass & history. Offline ready.
CONTAINER → ← BULK CARRIER
Current Position
Enter DD MM SS.s - N/S E/W in box No fix
Destination
Speed & Time
Manual local time. Auto-fills from GPS if you use Live GPS. Empty = Now. No UTC needed.
Options
Great-Circle 3440.065 NM
Sea-Lane +6% offline
Editable
WGS84 R=6371.0088km = 3440.065 NM Haversine 100% accurate. DD MM SS.s boxes fixed. Sea lane default 6% but you can change 0-15% as needed.
Local+UTC
UTC Only
N
Compass
Distance
Time To Go
Initial Brg
Final Brg
Departure
Source
DEFAULT: Offline All-In-One Map (OSM)Always works
Map loading
OPTIONAL: Google Map (needs internet)
} function finalBearing(a,b,c,d){return (initBearing(c,d,a,b)+180)%360;} function fillDMS(lat,lon,acc){ const la=decToDms(lat,true);const lo=decToDms(lon,false); document.getElementById('curLatD').value=la.deg;document.getElementById('curLatM').value=la.min;document.getElementById('curLatS').value=la.sec.toFixed(3);document.getElementById('curLatH').value=la.hem; document.getElementById('curLonD').value=lo.deg;document.getElementById('curLonM').value=lo.min;document.getElementById('curLonS').value=lo.sec.toFixed(3);document.getElementById('curLonH').value=lo.hem; document.getElementById('posBadge').textContent='Live GPS ±'+Math.round(acc)+'m';document.getElementById('posBadge').style.background='#dff0e4'; document.getElementById('gpsAcc').textContent='Fix: '+acc.toFixed(1)+'m'; document.getElementById('timeObtained').value=new Date().toISOString().slice(0,16); lastGPS={lat,lon,acc};} function useLiveGPS(){ const b=document.getElementById('gpsBtn');const err=document.getElementById('err'); err.style.display='block';err.textContent='Requesting GPS...'; if(!navigator.geolocation){err.textContent='❌ No GPS support';return;} b.textContent='Locating...'; navigator.geolocation.getCurrentPosition(p=>{fillDMS(p.coords.latitude,p.coords.longitude,p.coords.accuracy);b.textContent='📍 Live GPS ✓';err.style.display='none';plotCurrentOnly();},e=>{ let msg='GPS Error: '+e.message; if(e.code===1) msg='❌ Permission DENIED - Settings > Chrome > Location > Allow'; if(e.code===2) msg='❌ GPS unavailable - Turn ON Location'; if(e.code===3) msg='❌ Timeout - Go open deck'; err.textContent=msg;b.textContent='📍 Live GPS'; },{enableHighAccuracy:true,timeout:20000,maximumAge:0}); } function toggleWatchGPS(){ const b=document.getElementById('watchBtn');const err=document.getElementById('err'); if(watchId!==null){navigator.geolocation.clearWatch(watchId);watchId=null;b.textContent='Watch OFF';b.classList.remove('watch-on');err.style.display='none';return;} if(!navigator.geolocation){err.style.display='block';err.textContent='No GPS';return;} b.textContent='Watch ON...';b.classList.add('watch-on');err.style.display='block';err.textContent='Watch ON - Tracking...'; watchId=navigator.geolocation.watchPosition(p=>{fillDMS(p.coords.latitude,p.coords.longitude,p.coords.accuracy);b.textContent='Watch ON ('+Math.round(p.coords.accuracy)+'m) LIVE';err.textContent='Watch LIVE ±'+Math.round(p.coords.accuracy)+'m';plotCurrentOnly();},e=>{err.textContent='Watch Error: '+e.message;},{enableHighAccuracy:true,maximumAge:1000,timeout:20000}); } function showErr(m){const er=document.getElementById('err');er.textContent=m;er.style.display='block';setTimeout(()=>er.style.display='none',5000);} let gmap=null,gCurMarker=null,gDestMarker=null,gLine=null,gVisible=false; let lastPlot=null; function toggleGMap(){ const w=document.getElementById('gmapWrap'); const b=document.getElementById('gToggle'); if(!gVisible){ w.style.display='block'; b.textContent='Hide Google Map'; gVisible=true; initGMap(); setTimeout(()=>{ if(gmap) gmap.invalidateSize(); if(lastPlot) plotBothG(lastPlot.a,lastPlot.b,lastPlot.c,lastPlot.d,lastPlot.dist); },300); } else{ w.style.display='none'; b.textContent='Show Google Map'; gVisible=false; } } function initGMap(){ if(gmap) return; try{ gmap=L.map('gmap',{zoomControl:false}).setView([11.7,43.2],5); L.control.zoom({position:'bottomright'}).addTo(gmap); L.tileLayer('https://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}',{maxZoom:20, subdomains:['mt0','mt1','mt2','mt3']}).addTo(gmap); document.getElementById('gmapInfo').textContent='Google Map ready (online)'; }catch(e){ document.getElementById('gmap').innerHTML='
Google Map needs internet
'; } } function plotBothG(a,b,c,d,dist){ if(!gmap) return; try{ if(gCurMarker) gmap.removeLayer(gCurMarker); if(gDestMarker) gmap.removeLayer(gDestMarker); if(gLine) gmap.removeLayer(gLine); gCurMarker=L.marker([a,b],{icon:L.divIcon({html:'
',iconSize:[18,18],className:''})}).addTo(gmap); gDestMarker=L.marker([c,d],{icon:L.divIcon({html:'
',iconSize:[18,18],className:''})}).addTo(gmap); gLine=L.polyline([[a,b],[c,d]],{color:'#ff7a00',weight:3,dashArray:'8,8'}).addTo(gmap); gmap.fitBounds([[a,b],[c,d]],{padding:[40,40]});}catch(e){} } function centerGMap(){ if(gCurMarker&&gmap) gmap.setView(gCurMarker.getLatLng(),10); } function initMap(){if(map)return;try{map=L.map('map',{zoomControl:false}).setView([11.7,43.2],5);L.control.zoom({position:'bottomright'}).addTo(map);L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:18}).addTo(map);document.getElementById('mapInfo').textContent='Map ready';}catch(e){document.getElementById('map').innerHTML='
OFFLINE MODE
Map needs online first
';}setTimeout(()=>{if(map)try{map.invalidateSize()}catch(e){}},500);} function plotCurrentOnly(){initMap();const lat=dmsToDec(curLatD.value,curLatM.value,curLatS.value,curLatH.value);const lon=dmsToDec(curLonD.value,curLonM.value,curLonS.value,curLonH.value);if(isNaN(lat))return;if(typeof L==='undefined')return;if(curMarker)map.removeLayer(curMarker);curMarker=L.marker([lat,lon],{icon:L.divIcon({html:'
',iconSize:[18,18],className:''})}).addTo(map);map.setView([lat,lon],9);} function plotBoth(a,b,c,d,dist){initMap();if(typeof L==='undefined')return;if(curMarker)map.removeLayer(curMarker);if(destMarker)map.removeLayer(destMarker);if(line)map.removeLayer(line);curMarker=L.marker([a,b],{icon:L.divIcon({html:'
CUR
',iconSize:[18,28],className:''})}).addTo(map);destMarker=L.marker([c,d],{icon:L.divIcon({html:'
DEST
',iconSize:[18,28],className:''})}).addTo(map);line=L.polyline([[a,b],[c,d]],{color:'#c23c2a',weight:3,dashArray:'8,8'}).addTo(map);line.bindTooltip(dist.toFixed(2)+' NM',{permanent:true});map.fitBounds([[a,b],[c,d]],{padding:[40,40]});setTimeout(()=>{if(map)map.invalidateSize()},200); lastPlot={a,b,c,d,dist}; if(gVisible){ initGMap(); plotBothG(a,b,c,d,dist);} } function centerMap(){if(curMarker&&map)map.setView(curMarker.getLatLng(),10);} function openInPsyberia(){const a=dmsToDec(curLatD.value,curLatM.value,curLatS.value,curLatH.value);const b=dmsToDec(curLonD.value,curLonM.value,curLonS.value,curLonH.value);const c=dmsToDec(destLatD.value,destLatM.value,destLatS.value,destLatH.value);const d=dmsToDec(destLonD.value,destLonM.value,destLonS.value,destLonH.value);if(isNaN(a)){alert('Enter Current');return;}if(!isNaN(c))window.location.href=`geo:${a},${b}?q=${c},${d}(DEST)&z=8`;else window.location.href=`geo:${a},${b}?q=${a},${b}(CUR)&z=12`;} function updateClock(){document.getElementById('utcLocal').textContent='UTC:'+new Date().toISOString().substring(11,19)+' | LT:'+new Date().toLocaleTimeString('en-GB',{timeZone:'Africa/Nairobi'});}setInterval(updateClock,1000);updateClock(); function calculateETA(){const a=dmsToDec(curLatD.value,curLatM.value,curLatS.value,curLatH.value);const b=dmsToDec(curLonD.value,curLonM.value,curLonS.value,curLonH.value);const c=dmsToDec(destLatD.value,destLatM.value,destLatS.value,destLatH.value);const d=dmsToDec(destLonD.value,destLonM.value,destLonS.value,destLonH.value);const sog=parseFloat(document.getElementById('sog').value);const timeObt=document.getElementById('timeObtained').value;let dep=timeObt?new Date(timeObt):new Date();if([a,b,c,d,sog].some(v=>isNaN(v))){showErr('Fill all DD MM SS.s + SOG');return;}let dist=haversineNM(a,b,c,d);let note='GC 3440.065 NM';if(mode==='sea'){let pct=parseFloat(document.getElementById('seaPercent').value); if(isNaN(pct)) pct=6; dist*=1+pct/100; note='Sea-lane +'+pct+'% offline';}const br=initBearing(a,b,c,d);const fbr=finalBearing(a,b,c,d);const hrs=dist/sog;const eta=new Date(dep.getTime()+hrs*3600*1000);const lf=new Intl.DateTimeFormat('en-GB',{timeZone:'Africa/Nairobi',hour:'2-digit',minute:'2-digit',day:'2-digit',month:'short',year:'numeric'});const uf=new Intl.DateTimeFormat('en-GB',{timeZone:'UTC',hour:'2-digit',minute:'2-digit',day:'2-digit',month:'short',year:'numeric'});document.getElementById('etaMain').textContent=window.dMode==='local'?lf.format(eta)+' Local':uf.format(eta)+' UTC';document.getElementById('etaSub').innerHTML=(window.dMode==='local'?'UTC: '+uf.format(eta):'Local: '+lf.format(eta))+'
'+note;document.getElementById('sDist').textContent=dist.toFixed(2)+' nm';const mins=Math.round(hrs*60);document.getElementById('sTime').textContent=Math.floor(mins/1440)+'d '+Math.floor((mins%1440)/60)+'h '+mins%60+'m';document.getElementById('sIB').textContent=br.toFixed(1)+'°T';document.getElementById('sFB').textContent=fbr.toFixed(1)+'°T';document.getElementById('sDep').textContent=(timeObt?'Obt: ':'Now: ')+dep.toLocaleString();document.getElementById('sSrc').textContent=lastGPS?'Live GPS ±'+Math.round(lastGPS.acc)+'m':'Manual';document.getElementById('result').classList.add('show');document.getElementById('compArrow').style.transform=`translate(-50%,-100%) rotate(${br}deg)`;document.getElementById('compText').textContent=br.toFixed(1)+'°T';plotBoth(a,b,c,d,dist);saveHist({curLatD:curLatD.value,curLatM:curLatM.value,curLatS:curLatS.value,curLatH:curLatH.value,curLonD:curLonD.value,curLonM:curLonM.value,curLonS:curLonS.value,curLonH:curLonH.value,destLatD:destLatD.value,destLatM:destLatM.value,destLatS:destLatS.value,destLatH:destLatH.value,destLonD:destLonD.value,destLonM:destLonM.value,destLonS:destLonS.value,destLonH:destLonH.value,sog:sog,dist:dist.toFixed(2)+' nm',date:new Date().toLocaleString()});document.getElementById('result').scrollIntoView({behavior:'smooth'});} function saveHist(e){let h=JSON.parse(localStorage.getItem('v2')||'[]');h.unshift(e);h=h.slice(0,10);localStorage.setItem('v2',JSON.stringify(h));renderHist();} function renderHist(){let h=JSON.parse(localStorage.getItem('v2')||'[]');const l=document.getElementById('histList');if(!h.length){l.innerHTML='
No history
';return;}l.innerHTML=h.map((it,i)=>`
${it.curLatH}${it.curLatD}°${it.curLatM}'${it.curLatS}" → ${it.destLatH}${it.destLatD}°
${it.date} • ${it.dist}
`).join('');} function loadHist(i){let h=JSON.parse(localStorage.getItem('v2')||'[]')[i];if(!h)return;Object.keys(h).forEach(k=>{if(document.getElementById(k))document.getElementById(k).value=h[k];});window.scrollTo({top:0,behavior:'smooth'});} function delHist(i){let h=JSON.parse(localStorage.getItem('v2')||'[]');h.splice(i,1);localStorage.setItem('v2',JSON.stringify(h));renderHist();} function clearAllHist(){localStorage.removeItem('v2');renderHist();} setTimeout(()=>{ const order=['curLatD','curLatM','curLatS','curLatH','curLonD','curLonM','curLonS','curLonH','destLatD','destLatM','destLatS','destLatH','destLonD','destLonM','destLonS','destLonH','sog']; function nxt(id){let i=order.indexOf(id);return i>=0&&i{ let el=document.getElementById(id);if(!el)return; el.addEventListener('focus',function(){this.select();}); el.addEventListener('click',function(){this.select(); if(this.value!==''){ let cur=this.value; setTimeout(()=>{ if(el.value===cur) el.value=''; },120); }}); el.addEventListener('input',function(){ let v=this.value.trim(); if(!v) return; let max=2; if(id.includes('LonD')) max=3; if(id.includes('S') && id!=='sog') max=2; if(id==='sog') max=2; if(id.includes('H')){ let n=nxt(id); if(n) setTimeout(()=>{let ne=document.getElementById(n); if(ne){ne.focus(); ne.select();}},60); return; } if(v.replace('.','').length>=max){ let n=nxt(id); if(n){ let ne=document.getElementById(n); if(ne) setTimeout(()=>{ne.focus(); ne.select();},80); }} }); }); },700); window.addEventListener('beforeinstallprompt', e=>{e.preventDefault();deferredPrompt=e;document.getElementById('installBar').classList.add('show');}); async function installApp(){if(deferredPrompt){deferredPrompt.prompt();await deferredPrompt.userChoice;deferredPrompt=null;document.getElementById('installBar').classList.remove('show');}else{alert('Chrome menu > Install app');}}