🚢 Vessel ETA Calculator V8 FIXED

Offline Ready • GPS Live • Waypoints • LT/UTC
Current Position
Enter DD MM SS.s No fix
Destination
Waypoints (Optional)
Add waypoints if vessel NOT sailing straight. ETA sum of legs: CUR->WP1->WP2->...->DEST
No waypoints - straight route. Click + Add WP
Speed & Time
Enter LT. Auto-fills from GPS. Shows both UTC & LT. Empty=Now.
LT: --
UTC: --
Options
Great-Circle
Sea-Lane +6%
Editable 0-15%
Grouped as per same % effective.
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)
`).join(''); setupAutoJump(); } function getWpDec(i){ const ld=document.getElementById('wp'+i+'_latD'); if(!ld) return null; const la=dmsToDec(ld.value,document.getElementById('wp'+i+'_latM').value,document.getElementById('wp'+i+'_latS').value,document.getElementById('wp'+i+'_latH').value); const lo=dmsToDec(document.getElementById('wp'+i+'_lonD').value,document.getElementById('wp'+i+'_lonM').value,document.getElementById('wp'+i+'_lonS').value,document.getElementById('wp'+i+'_lonH').value); if(isNaN(la)||isNaN(lo)) return null; return {lat:la,lon:lo}; } function toRad(d){return d*Math.PI/180}function toDeg(r){return r*180/Math.PI} function dmsToDec(d,m,s,h){if(d===''||d===null)return NaN;let dd=parseFloat(d||0)+parseFloat(m||0)/60+parseFloat(s||0)/3600;if(h==='S'||h==='W')dd=-dd;return dd} function decToDms(dec,isLat){let abs=Math.abs(dec);let d=Math.floor(abs);let mf=(abs-d)*60;let m=Math.floor(mf);let s=(mf-m)*60;let h=isLat?(dec>=0?'N':'S'):(dec>=0?'E':'W');return{deg:d,min:m,sec:s,hem:h}} function haversineNM(a,b,c,d){const R=6371.0088;const dLat=toRad(c-a);const dLon=toRad(d-b);const x=Math.sin(dLat/2)**2+Math.cos(toRad(a))*Math.cos(toRad(c))*Math.sin(dLon/2)**2;return (R*2*Math.atan2(Math.sqrt(x),Math.sqrt(1-x)))/1.852} function initBearing(a,b,c,d){const y=Math.sin(toRad(d-b))*Math.cos(toRad(c));const x=Math.cos(toRad(a))*Math.sin(toRad(c))-Math.sin(toRad(a))*Math.cos(toRad(c))*Math.cos(toRad(d-b));return (toDeg(Math.atan2(y,x))+360)%360} 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'; const now=new Date();const iso=new Date(now.getTime()-now.getTimezoneOffset()*60000).toISOString().slice(0,16); document.getElementById('timeObtained').value=iso;updateTimePreview(); 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',6000);} 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='OSM Ready (Offline)';}catch(e){document.getElementById('map').innerHTML='
OFFLINE MODE
';}setTimeout(()=>{if(map)try{map.invalidateSize()}catch(e){}},500);} 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 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(window.lastRoute)plotBothGMulti(window.lastRoute,window.lastDist);},300);}else{w.style.display='none';b.textContent='Show Google Map';gVisible=false;}} function centerMap(){if(curMarker&&map)map.setView(curMarker.getLatLng(),10);} function centerGMap(){if(gCurMarker&&gmap)gmap.setView(gCurMarker.getLatLng(),10);} 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)||isNaN(lon))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 plotBothGMulti(route,totalDist){ if(!gmap)return;try{ if(gCurMarker)gmap.removeLayer(gCurMarker);if(gDestMarker)gmap.removeLayer(gDestMarker);if(gLine)gmap.removeLayer(gLine);gWpMarkers.forEach(m=>{try{gmap.removeLayer(m)}catch(e){}});gWpMarkers=[]; let latlngs=route.map(r=>[r.lat,r.lon]);gLine=L.polyline(latlngs,{color:'#ff7a00',weight:3,dashArray:'8,8'}).addTo(gmap); route.forEach((r,i)=>{let col=i===0?'#c23c2a':(i===route.length-1?'#0b2540':'#ff7a00');let mk=L.marker([r.lat,r.lon],{icon:L.divIcon({html:'
',iconSize:[14,14],className:''})}).addTo(gmap);if(i===0)gCurMarker=mk;else if(i===route.length-1)gDestMarker=mk;else gWpMarkers.push(mk);}); gmap.fitBounds(latlngs,{padding:[30,30]});}catch(e){} } function plotBothMulti(route,totalDist){ initMap();if(typeof L==='undefined')return; if(curMarker)map.removeLayer(curMarker);if(destMarker)map.removeLayer(destMarker);if(line)map.removeLayer(line);wpMarkers.forEach(m=>{try{map.removeLayer(m)}catch(e){}});wpMarkers=[]; let latlngs=route.map(r=>[r.lat,r.lon]);line=L.polyline(latlngs,{color:'#c23c2a',weight:3,dashArray:'8,8'}).addTo(map); route.forEach((r,i)=>{let col=i===0?'#c23c2a':(i===route.length-1?'#0b2540':'#ff7a00');let lab=r.label;let mk=L.marker([r.lat,r.lon],{icon:L.divIcon({html:'
'+lab+'
',iconSize:[16,24],className:''})}).addTo(map);if(i===0)curMarker=mk;else if(i===route.length-1)destMarker=mk;else wpMarkers.push(mk);}); map.fitBounds(latlngs,{padding:[40,40]});setTimeout(()=>{if(map)map.invalidateSize()},200);window.lastRoute=route;window.lastDist=totalDist;if(gVisible){initGMap();plotBothGMulti(route,totalDist);} } 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(){const el=document.getElementById('utcLocal');if(el)el.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 route=[];route.push({lat:a,lon:b,label:'CUR'}); let wpValid=[];for(let i=0;i'+note+'
Total Dist: '+totalDist.toFixed(2)+' NM GC + '+((dist/totalDist-1)*100).toFixed(1)+'% = '+dist.toFixed(2)+' NM
Departure LT: '+ltDep+' | UTC: '+utcDep; if(legs.length>1){subHtml+='

Legs ('+legs.length+'):
'+legs.map((l,idx)=>'Leg '+(idx+1)+' '+l.from+'->'+l.to+': '+l.dist.toFixed(2)+' NM').join('
');} document.getElementById('etaSub').innerHTML=subHtml; document.getElementById('sDist').textContent=dist.toFixed(2)+' nm ('+totalDist.toFixed(2)+' GC)';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 LT: ':'Now LT: ')+ltDep+' / UTC: '+utcDep;document.getElementById('sSrc').textContent=lastGPS?'Live GPS ±'+Math.round(lastGPS.acc)+'m':'Manual UTC/LT';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';plotBothMulti(route,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(),wps:wpValid.length});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();} function setupAutoJump(){ const order=['curLatD','curLatM','curLatS','curLatH','curLonD','curLonM','curLonS','curLonH','destLatD','destLatM','destLatS','destLatH','destLonD','destLonM','destLonS','destLonH','sog']; let fullOrder=[...order]; for(let i=0;i{ let el=document.getElementById(id);if(!el||el.dataset.jump)return; el.dataset.jump='1'; 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')||id.includes('lonD'))max=3;if(id==='sog')max=3;if(id.includes('_latS')||id.includes('_lonS')||id.includes('LatS')||id.includes('LonS'))max=6; if(id.includes('H')){let idx=fullOrder.indexOf(id);let n=fullOrder[idx+1];if(n)setTimeout(()=>{let ne=document.getElementById(n);if(ne){ne.focus();ne.select();}},60);return;} if(id.includes('S')){if(v.length>=4){let idx=fullOrder.indexOf(id);let n=fullOrder[idx+1];if(n){let ne=document.getElementById(n);if(ne)setTimeout(()=>{ne.focus();ne.select();},80);}}return;} if(v.replace('.','').length>=max){let idx=fullOrder.indexOf(id);let n=fullOrder[idx+1];if(n){let ne=document.getElementById(n);if(ne)setTimeout(()=>{ne.focus();ne.select();},80);}} }); }); } 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');}} window.addEventListener('load',()=>{renderHist();initMap();setupAutoJump();updateSeaInfo();});