|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>SkyWatch - Real-time Plane Tracker</title> |
|
<meta name="description" content="Track real planes flying above your location"> |
|
<meta name="theme-color" content="#1e40af"> |
|
<link rel="manifest" href="/manifest.json"> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
<style> |
|
@keyframes pulse { |
|
0%, 100% { opacity: 1; } |
|
50% { opacity: 0.5; } |
|
} |
|
.animate-pulse { |
|
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; |
|
} |
|
.notification-badge { |
|
position: absolute; |
|
top: -5px; |
|
right: -5px; |
|
background-color: #ef4444; |
|
color: white; |
|
border-radius: 9999px; |
|
width: 20px; |
|
height: 20px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
font-size: 12px; |
|
font-weight: bold; |
|
} |
|
.plane-icon { |
|
transform: rotate(var(--rotation)); |
|
} |
|
.map-container { |
|
height: 300px; |
|
background-color: #e5e7eb; |
|
position: relative; |
|
overflow: hidden; |
|
} |
|
.map-plane { |
|
position: absolute; |
|
transition: all 1s ease-out; |
|
} |
|
.radar-sweep { |
|
position: absolute; |
|
top: 50%; |
|
left: 50%; |
|
width: 100%; |
|
height: 100%; |
|
border-radius: 50%; |
|
transform: translate(-50%, -50%); |
|
background: conic-gradient(from 0deg, rgba(59, 130, 246, 0.1) 0%, rgba(59, 130, 246, 0.3) 50%, transparent 50%); |
|
animation: radar-rotate 4s linear infinite; |
|
pointer-events: none; |
|
} |
|
@keyframes radar-rotate { |
|
from { transform: translate(-50%, -50%) rotate(0deg); } |
|
to { transform: translate(-50%, -50%) rotate(360deg); } |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-100 min-h-screen"> |
|
<div class="max-w-md mx-auto bg-white shadow-lg rounded-b-xl overflow-hidden"> |
|
|
|
<header class="bg-blue-800 text-white p-4 relative"> |
|
<div class="flex justify-between items-center"> |
|
<div> |
|
<h1 class="text-xl font-bold">SkyWatch</h1> |
|
<p class="text-sm opacity-80" id="location-status">Detecting your location...</p> |
|
</div> |
|
<div class="flex space-x-3"> |
|
<button id="notification-btn" class="relative p-2 rounded-full hover:bg-blue-700 transition"> |
|
<i class="fas fa-bell"></i> |
|
<span id="notification-count" class="notification-badge hidden">0</span> |
|
</button> |
|
<button id="refresh-btn" class="p-2 rounded-full hover:bg-blue-700 transition"> |
|
<i class="fas fa-sync-alt"></i> |
|
</button> |
|
</div> |
|
</div> |
|
</header> |
|
|
|
|
|
<main class="p-4"> |
|
|
|
<div class="map-container rounded-lg mb-4 relative"> |
|
<div id="radar-center" class="absolute top-1/2 left-1/2 w-3 h-3 bg-blue-600 rounded-full transform -translate-x-1/2 -translate-y-1/2 z-10"></div> |
|
<div id="map-planes" class="relative z-0"></div> |
|
<div class="radar-sweep"></div> |
|
</div> |
|
|
|
|
|
<div id="current-plane" class="bg-blue-50 rounded-lg p-3 mb-4 hidden"> |
|
<div class="flex justify-between items-center mb-2"> |
|
<h3 class="font-bold text-blue-800">Plane Above You Now</h3> |
|
<span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full">LIVE</span> |
|
</div> |
|
<div class="flex items-center space-x-3"> |
|
<div class="bg-blue-100 p-2 rounded-full"> |
|
<i class="fas fa-plane text-blue-600"></i> |
|
</div> |
|
<div class="flex-1"> |
|
<div class="flex justify-between"> |
|
<span class="font-medium" id="current-callsign">--</span> |
|
<span class="text-sm" id="current-altitude">-- ft</span> |
|
</div> |
|
<div class="text-sm text-gray-600" id="current-airline">--</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="mb-2 flex justify-between items-center"> |
|
<h2 class="font-bold text-gray-800">Upcoming Planes</h2> |
|
<span class="text-xs bg-gray-200 text-gray-800 px-2 py-1 rounded-full" id="planes-count">0 planes</span> |
|
</div> |
|
<div id="upcoming-planes" class="space-y-3"> |
|
<div class="text-center py-8 text-gray-500" id="loading-planes"> |
|
<i class="fas fa-plane animate-pulse text-2xl mb-2"></i> |
|
<p>Scanning for planes...</p> |
|
</div> |
|
</div> |
|
</main> |
|
|
|
|
|
<footer class="bg-gray-50 p-3 border-t flex justify-between text-xs text-gray-500"> |
|
<div> |
|
<span id="last-updated">Last updated: --</span> |
|
</div> |
|
<div> |
|
<span id="api-status">API: Offline</span> |
|
</div> |
|
</footer> |
|
</div> |
|
|
|
|
|
<div id="notification-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> |
|
<div class="bg-white rounded-lg w-full max-w-sm mx-4 max-h-[80vh] overflow-y-auto"> |
|
<div class="p-4 border-b flex justify-between items-center"> |
|
<h3 class="font-bold">Plane Alerts</h3> |
|
<button id="close-notifications" class="text-gray-500 hover:text-gray-700"> |
|
<i class="fas fa-times"></i> |
|
</button> |
|
</div> |
|
<div id="notification-list" class="divide-y"> |
|
<div class="p-4 text-center text-gray-500"> |
|
No alerts yet |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="install-prompt" class="fixed bottom-4 left-0 right-0 flex justify-center hidden"> |
|
<div class="bg-blue-600 text-white rounded-lg shadow-lg p-4 mx-4 max-w-md flex items-center justify-between"> |
|
<div class="flex items-center"> |
|
<i class="fas fa-plane-departure text-xl mr-3"></i> |
|
<div> |
|
<p class="font-medium">Install SkyWatch</p> |
|
<p class="text-sm opacity-90">Get real-time plane alerts</p> |
|
</div> |
|
</div> |
|
<button id="install-btn" class="bg-white text-blue-600 px-3 py-1 rounded-full text-sm font-medium">Install</button> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
if ('serviceWorker' in navigator) { |
|
window.addEventListener('load', () => { |
|
navigator.serviceWorker.register('/sw.js').then(registration => { |
|
console.log('ServiceWorker registration successful'); |
|
}).catch(err => { |
|
console.log('ServiceWorker registration failed: ', err); |
|
}); |
|
}); |
|
} |
|
|
|
|
|
const state = { |
|
location: null, |
|
planes: [], |
|
notifications: [], |
|
lastUpdated: null, |
|
currentPlane: null, |
|
deferredPrompt: null, |
|
apiKeys: { |
|
opensky: null, |
|
adsbexchange: null |
|
} |
|
}; |
|
|
|
|
|
const elements = { |
|
locationStatus: document.getElementById('location-status'), |
|
currentPlaneCard: document.getElementById('current-plane'), |
|
currentCallsign: document.getElementById('current-callsign'), |
|
currentAltitude: document.getElementById('current-altitude'), |
|
currentAirline: document.getElementById('current-airline'), |
|
upcomingPlanes: document.getElementById('upcoming-planes'), |
|
loadingPlanes: document.getElementById('loading-planes'), |
|
planesCount: document.getElementById('planes-count'), |
|
lastUpdated: document.getElementById('last-updated'), |
|
apiStatus: document.getElementById('api-status'), |
|
notificationBtn: document.getElementById('notification-btn'), |
|
notificationCount: document.getElementById('notification-count'), |
|
notificationModal: document.getElementById('notification-modal'), |
|
notificationList: document.getElementById('notification-list'), |
|
closeNotifications: document.getElementById('close-notifications'), |
|
refreshBtn: document.getElementById('refresh-btn'), |
|
installPrompt: document.getElementById('install-prompt'), |
|
installBtn: document.getElementById('install-btn'), |
|
mapPlanes: document.getElementById('map-planes'), |
|
radarCenter: document.getElementById('radar-center') |
|
}; |
|
|
|
|
|
elements.notificationBtn.addEventListener('click', showNotifications); |
|
elements.closeNotifications.addEventListener('click', hideNotifications); |
|
elements.refreshBtn.addEventListener('click', refreshData); |
|
elements.installBtn.addEventListener('click', installApp); |
|
|
|
|
|
window.addEventListener('beforeinstallprompt', (e) => { |
|
e.preventDefault(); |
|
state.deferredPrompt = e; |
|
elements.installPrompt.classList.remove('hidden'); |
|
}); |
|
|
|
function installApp() { |
|
if (state.deferredPrompt) { |
|
state.deferredPrompt.prompt(); |
|
state.deferredPrompt.userChoice.then((choiceResult) => { |
|
if (choiceResult.outcome === 'accepted') { |
|
elements.installPrompt.classList.add('hidden'); |
|
} |
|
state.deferredPrompt = null; |
|
}); |
|
} |
|
} |
|
|
|
|
|
function getLocation() { |
|
elements.locationStatus.textContent = 'Detecting your location...'; |
|
|
|
if (navigator.geolocation) { |
|
navigator.geolocation.getCurrentPosition( |
|
position => { |
|
state.location = { |
|
lat: position.coords.latitude, |
|
lon: position.coords.longitude, |
|
accuracy: position.coords.accuracy |
|
}; |
|
elements.locationStatus.textContent = `📍 ${state.location.lat.toFixed(4)}, ${state.location.lon.toFixed(4)}`; |
|
getPlaneData(); |
|
}, |
|
error => { |
|
console.error('Geolocation error:', error); |
|
elements.locationStatus.textContent = 'Location access denied. Using default location.'; |
|
|
|
state.location = { lat: 40.7128, lon: -74.0060 }; |
|
getPlaneData(); |
|
}, |
|
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 0 } |
|
); |
|
} else { |
|
elements.locationStatus.textContent = 'Geolocation not supported. Using default location.'; |
|
state.location = { lat: 40.7128, lon: -74.0060 }; |
|
getPlaneData(); |
|
} |
|
} |
|
|
|
|
|
async function getPlaneData() { |
|
elements.loadingPlanes.classList.remove('hidden'); |
|
elements.upcomingPlanes.innerHTML = ''; |
|
elements.upcomingPlanes.appendChild(elements.loadingPlanes); |
|
elements.apiStatus.textContent = 'API: Loading...'; |
|
|
|
if(!state.location) { |
|
return; |
|
} |
|
|
|
try { |
|
|
|
const range = 1; |
|
const bbox = [ |
|
state.location.lat - range, |
|
state.location.lon - range, |
|
state.location.lat + range, |
|
state.location.lon + range |
|
]; |
|
|
|
console.log('---state', state); |
|
console.log('---bbox', bbox); |
|
|
|
|
|
|
|
let response = await fetch(`https://opensky-network.org/api/states/all?lamin=${bbox[0]}&lomin=${bbox[1]}&lamax=${bbox[2]}&lomax=${bbox[3]}`); |
|
|
|
if (!response.ok) { |
|
|
|
throw new Error('OpenSky API failed, trying ADSBExchange'); |
|
} |
|
|
|
const data = await response.json(); |
|
state.planes = data.states || []; |
|
|
|
if (state.planes.length === 0) { |
|
|
|
await getADSBExchangeData(); |
|
} else { |
|
processPlaneData(); |
|
elements.apiStatus.textContent = 'API: OpenSky'; |
|
elements.apiStatus.className = elements.apiStatus.className.replace('text-red-500', 'text-green-500'); |
|
} |
|
} catch (error) { |
|
console.error('Error fetching plane data:', error); |
|
|
|
await getADSBExchangeData(); |
|
} |
|
} |
|
|
|
|
|
async function getADSBExchangeData() { |
|
try { |
|
|
|
const response = await fetch(`https://adsbexchange-com1.p.rapidapi.com/v2/lat/${state.location.lat}/lon/${state.location.lon}/dist/50/`, { |
|
headers: { |
|
'X-RapidAPI-Key': 'your-rapidapi-key-here', |
|
'X-RapidAPI-Host': 'adsbexchange-com1.p.rapidapi.com' |
|
} |
|
}); |
|
|
|
if (!response.ok) throw new Error('ADSBExchange API failed'); |
|
|
|
const data = await response.json(); |
|
|
|
state.planes = data.ac.map(plane => [ |
|
plane.hex, |
|
plane.flight, |
|
null, |
|
null, |
|
null, |
|
plane.lon, |
|
plane.lat, |
|
plane.altitude, |
|
false, |
|
plane.speed, |
|
plane.track, |
|
plane.vrate, |
|
null, null, null, null, null, null |
|
]); |
|
|
|
processPlaneData(); |
|
elements.apiStatus.textContent = 'API: ADSBExchange'; |
|
elements.apiStatus.className = elements.apiStatus.className.replace('text-red-500', 'text-green-500'); |
|
} catch (error) { |
|
console.error('Error fetching ADSBExchange data:', error); |
|
elements.apiStatus.textContent = 'API: Offline'; |
|
elements.apiStatus.className += ' text-red-500'; |
|
|
|
elements.loadingPlanes.innerHTML = ` |
|
<div class="text-center py-8 text-red-500"> |
|
<i class="fas fa-exclamation-triangle text-2xl mb-2"></i> |
|
<p>Failed to load plane data</p> |
|
<button class="mt-2 text-blue-600 text-sm font-medium">Retry</button> |
|
</div> |
|
`; |
|
elements.loadingPlanes.querySelector('button').addEventListener('click', getPlaneData); |
|
} |
|
} |
|
|
|
|
|
function processPlaneData() { |
|
const now = new Date(); |
|
state.lastUpdated = now; |
|
elements.lastUpdated.textContent = `Last updated: ${now.toLocaleTimeString()}`; |
|
|
|
|
|
const nearbyPlanes = state.planes |
|
.filter(plane => plane[5] && plane[6]) |
|
.map(plane => ({ |
|
icao24: plane[0], |
|
callsign: plane[1]?.trim() || 'N/A', |
|
origin: plane[2] || 'N/A', |
|
time: plane[3] || Date.now() / 1000, |
|
lastContact: plane[4] || Date.now() / 1000, |
|
longitude: plane[5], |
|
latitude: plane[6], |
|
altitude: plane[7] || 0, |
|
onGround: plane[8], |
|
velocity: plane[9] || 0, |
|
heading: plane[10] || 0, |
|
verticalRate: plane[11] || 0, |
|
airline: getAirlineFromCallsign(plane[1]?.trim()) |
|
})) |
|
.filter(plane => !plane.onGround && plane.altitude > 1000) |
|
.sort((a, b) => { |
|
|
|
const distA = getDistance(state.location.lat, state.location.lon, a.latitude, a.longitude); |
|
const distB = getDistance(state.location.lat, state.location.lon, b.latitude, b.longitude); |
|
return distA - distB; |
|
}); |
|
|
|
|
|
const overheadPlane = nearbyPlanes.find(plane => { |
|
const distance = getDistance(state.location.lat, state.location.lon, plane.latitude, plane.longitude); |
|
return distance < 0.5; |
|
}); |
|
|
|
if (overheadPlane) { |
|
showCurrentPlane(overheadPlane); |
|
|
|
|
|
if (!state.notifications.some(n => n.icao24 === overheadPlane.icao24)) { |
|
addNotification(overheadPlane); |
|
showAlert(overheadPlane); |
|
} |
|
} else { |
|
hideCurrentPlane(); |
|
} |
|
|
|
|
|
const upcomingPlanes = nearbyPlanes.slice(0, 5); |
|
displayUpcomingPlanes(upcomingPlanes); |
|
|
|
|
|
updateRadarMap(nearbyPlanes.slice(0, 10)); |
|
} |
|
|
|
|
|
function showCurrentPlane(plane) { |
|
state.currentPlane = plane; |
|
elements.currentPlaneCard.classList.remove('hidden'); |
|
elements.currentCallsign.textContent = plane.callsign; |
|
elements.currentAltitude.textContent = `${Math.round(plane.altitude * 0.3048)} m (${Math.round(plane.altitude)} ft)`; |
|
elements.currentAirline.textContent = plane.airline || 'Unknown airline'; |
|
} |
|
|
|
function hideCurrentPlane() { |
|
state.currentPlane = null; |
|
elements.currentPlaneCard.classList.add('hidden'); |
|
} |
|
|
|
|
|
function displayUpcomingPlanes(planes) { |
|
elements.loadingPlanes.classList.add('hidden'); |
|
elements.planesCount.textContent = `${planes.length} ${planes.length === 1 ? 'plane' : 'planes'}`; |
|
|
|
if (planes.length === 0) { |
|
elements.upcomingPlanes.innerHTML = ` |
|
<div class="text-center py-8 text-gray-500"> |
|
<i class="fas fa-plane-slash text-2xl mb-2"></i> |
|
<p>No planes detected nearby</p> |
|
</div> |
|
`; |
|
return; |
|
} |
|
|
|
elements.upcomingPlanes.innerHTML = ''; |
|
|
|
planes.forEach(plane => { |
|
const distance = getDistance(state.location.lat, state.location.lon, plane.latitude, plane.longitude); |
|
const timeToOverhead = distance / (plane.velocity * 0.514444); |
|
|
|
const planeEl = document.createElement('div'); |
|
planeEl.className = 'bg-white rounded-lg p-3 shadow-sm border'; |
|
planeEl.innerHTML = ` |
|
<div class="flex items-center space-x-3"> |
|
<div class="bg-blue-100 p-2 rounded-full"> |
|
<i class="fas fa-plane text-blue-600 plane-icon" style="--rotation: ${plane.heading}deg"></i> |
|
</div> |
|
<div class="flex-1"> |
|
<div class="flex justify-between"> |
|
<span class="font-medium">${plane.callsign}</span> |
|
<span class="text-sm">${Math.round(plane.altitude * 0.3048)} m</span> |
|
</div> |
|
<div class="text-sm text-gray-600">${plane.airline || 'Unknown airline'}</div> |
|
<div class="flex justify-between mt-1 text-xs"> |
|
<span>${distance < 1 ? `${Math.round(distance * 1000)} m away` : `${distance.toFixed(1)} km away`}</span> |
|
<span>ETA: ${timeToOverhead > 0 ? `${Math.round(timeToOverhead / 60)} min` : 'now'}</span> |
|
</div> |
|
</div> |
|
</div> |
|
`; |
|
elements.upcomingPlanes.appendChild(planeEl); |
|
}); |
|
} |
|
|
|
|
|
function updateRadarMap(planes) { |
|
elements.mapPlanes.innerHTML = ''; |
|
|
|
|
|
const radarRange = 10; |
|
|
|
planes.forEach(plane => { |
|
const distance = getDistance(state.location.lat, state.location.lon, plane.latitude, plane.longitude); |
|
if (distance > radarRange) return; |
|
|
|
|
|
const bearing = getBearing(state.location.lat, state.location.lon, plane.latitude, plane.longitude); |
|
const scale = distance / radarRange; |
|
|
|
|
|
const x = Math.cos(bearing * Math.PI / 180) * scale * 150; |
|
const y = Math.sin(bearing * Math.PI / 180) * scale * 150; |
|
|
|
const planeEl = document.createElement('div'); |
|
planeEl.className = 'map-plane'; |
|
planeEl.style.left = `calc(50% + ${x}px)`; |
|
planeEl.style.top = `calc(50% + ${y}px)`; |
|
planeEl.innerHTML = ` |
|
<div class="w-6 h-6 flex items-center justify-center"> |
|
<i class="fas fa-plane text-blue-600 text-xs plane-icon" style="--rotation: ${plane.heading}deg"></i> |
|
</div> |
|
`; |
|
elements.mapPlanes.appendChild(planeEl); |
|
}); |
|
} |
|
|
|
|
|
function addNotification(plane) { |
|
const notification = { |
|
id: Date.now(), |
|
icao24: plane.icao24, |
|
callsign: plane.callsign, |
|
altitude: plane.altitude, |
|
time: new Date(), |
|
read: false |
|
}; |
|
|
|
state.notifications.unshift(notification); |
|
updateNotificationBadge(); |
|
|
|
|
|
localStorage.setItem('skywatch-notifications', JSON.stringify(state.notifications)); |
|
} |
|
|
|
|
|
function showAlert(plane) { |
|
if (Notification.permission === 'granted') { |
|
new Notification(`✈️ Plane overhead: ${plane.callsign}`, { |
|
body: `Altitude: ${Math.round(plane.altitude)} ft\nAirline: ${plane.airline || 'Unknown'}`, |
|
icon: '/icon-192x192.png' |
|
}); |
|
} else if (Notification.permission !== 'denied') { |
|
Notification.requestPermission().then(permission => { |
|
if (permission === 'granted') { |
|
showAlert(plane); |
|
} |
|
}); |
|
} |
|
} |
|
|
|
|
|
function updateNotificationBadge() { |
|
const unreadCount = state.notifications.filter(n => !n.read).length; |
|
if (unreadCount > 0) { |
|
elements.notificationCount.textContent = unreadCount; |
|
elements.notificationCount.classList.remove('hidden'); |
|
} else { |
|
elements.notificationCount.classList.add('hidden'); |
|
} |
|
} |
|
|
|
|
|
function showNotifications() { |
|
|
|
state.notifications.forEach(n => n.read = true); |
|
updateNotificationBadge(); |
|
|
|
|
|
elements.notificationList.innerHTML = ''; |
|
|
|
if (state.notifications.length === 0) { |
|
elements.notificationList.innerHTML = '<div class="p-4 text-center text-gray-500">No alerts yet</div>'; |
|
} else { |
|
state.notifications.forEach(notification => { |
|
const notificationEl = document.createElement('div'); |
|
notificationEl.className = 'p-4'; |
|
notificationEl.innerHTML = ` |
|
<div class="flex items-start space-x-3"> |
|
<div class="bg-blue-100 p-2 rounded-full mt-1"> |
|
<i class="fas fa-plane text-blue-600"></i> |
|
</div> |
|
<div class="flex-1"> |
|
<div class="font-medium">${notification.callsign}</div> |
|
<div class="text-sm text-gray-600">Altitude: ${Math.round(notification.altitude)} ft</div> |
|
<div class="text-xs text-gray-500 mt-1">${new Date(notification.time).toLocaleString()}</div> |
|
</div> |
|
</div> |
|
`; |
|
elements.notificationList.appendChild(notificationEl); |
|
}); |
|
} |
|
|
|
elements.notificationModal.classList.remove('hidden'); |
|
} |
|
|
|
function hideNotifications() { |
|
elements.notificationModal.classList.add('hidden'); |
|
} |
|
|
|
|
|
function refreshData() { |
|
elements.refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>'; |
|
getPlaneData(); |
|
setTimeout(() => { |
|
elements.refreshBtn.innerHTML = '<i class="fas fa-sync-alt"></i>'; |
|
}, 1000); |
|
} |
|
|
|
|
|
function getDistance(lat1, lon1, lat2, lon2) { |
|
|
|
const R = 6371; |
|
const dLat = (lat2 - lat1) * Math.PI / 180; |
|
const dLon = (lon2 - lon1) * Math.PI / 180; |
|
const a = |
|
Math.sin(dLat/2) * Math.sin(dLat/2) + |
|
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * |
|
Math.sin(dLon/2) * Math.sin(dLon/2); |
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); |
|
return R * c; |
|
} |
|
|
|
function getBearing(lat1, lon1, lat2, lon2) { |
|
|
|
const y = Math.sin(lon2 - lon1) * Math.cos(lat2); |
|
const x = Math.cos(lat1) * Math.sin(lat2) - |
|
Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1); |
|
return (Math.atan2(y, x) * 180 / Math.PI + 360) % 360; |
|
} |
|
|
|
function getAirlineFromCallsign(callsign) { |
|
if (!callsign) return null; |
|
|
|
|
|
const airlineCode = callsign.substring(0, 3).toUpperCase(); |
|
|
|
|
|
const airlines = { |
|
'UAL': 'United Airlines', |
|
'AAL': 'American Airlines', |
|
'DAL': 'Delta Air Lines', |
|
'SWA': 'Southwest Airlines', |
|
'JBU': 'JetBlue', |
|
'FDX': 'FedEx', |
|
'UPS': 'UPS Airlines', |
|
'AFR': 'Air France', |
|
'BAW': 'British Airways', |
|
'DLH': 'Lufthansa', |
|
'KLM': 'KLM Royal Dutch Airlines', |
|
'QFA': 'Qantas', |
|
'SIA': 'Singapore Airlines', |
|
'THY': 'Turkish Airlines', |
|
'UAE': 'Emirates', |
|
'VIR': 'Virgin Atlantic', |
|
'RYR': 'Ryanair', |
|
'EZY': 'EasyJet', |
|
'WZZ': 'Wizz Air', |
|
'AFL': 'Aeroflot', |
|
'ANA': 'All Nippon Airways', |
|
'JAL': 'Japan Airlines', |
|
'CAL': 'China Airlines', |
|
'CPA': 'Cathay Pacific', |
|
'CES': 'China Eastern', |
|
'CSN': 'China Southern', |
|
'KAL': 'Korean Air', |
|
'MAS': 'Malaysia Airlines', |
|
'QTR': 'Qatar Airways', |
|
'SVA': 'Saudia', |
|
'THA': 'Thai Airways' |
|
}; |
|
|
|
return airlines[airlineCode] || null; |
|
} |
|
|
|
|
|
function loadNotifications() { |
|
const savedNotifications = localStorage.getItem('skywatch-notifications'); |
|
if (savedNotifications) { |
|
state.notifications = JSON.parse(savedNotifications); |
|
updateNotificationBadge(); |
|
} |
|
} |
|
|
|
|
|
function init() { |
|
loadNotifications(); |
|
getLocation(); |
|
|
|
|
|
if (window.matchMedia('(display-mode: standalone)').matches) { |
|
elements.installPrompt.classList.add('hidden'); |
|
} |
|
|
|
|
|
setInterval(refreshData, 30000); |
|
} |
|
|
|
|
|
init(); |
|
</script> |
|
|
|
|
|
<script> |
|
|
|
const CACHE_NAME = 'skywatch-v1'; |
|
const ASSETS = [ |
|
'/', |
|
'/index.html', |
|
'/icon-192x192.png', |
|
'/icon-512x512.png', |
|
'/manifest.json' |
|
]; |
|
|
|
self.addEventListener('install', event => { |
|
event.waitUntil( |
|
caches.open(CACHE_NAME) |
|
.then(cache => cache.addAll(ASSETS)) |
|
.then(() => self.skipWaiting()) |
|
); |
|
}); |
|
|
|
self.addEventListener('activate', event => { |
|
event.waitUntil( |
|
caches.keys().then(keys => |
|
Promise.all( |
|
keys.filter(key => key !== CACHE_NAME) |
|
.map(key => caches.delete(key)) |
|
) |
|
).then(() => self.clients.claim()) |
|
); |
|
}); |
|
|
|
self.addEventListener('fetch', event => { |
|
event.respondWith( |
|
caches.match(event.request) |
|
.then(response => response || fetch(event.request)) |
|
); |
|
}); |
|
</script> |
|
|
|
|
|
<script> |
|
|
|
const manifest = { |
|
"name": "SkyWatch", |
|
"short_name": "SkyWatch", |
|
"description": "Track planes flying above your location in real-time", |
|
"start_url": "/", |
|
"display": "standalone", |
|
"background_color": "#1e40af", |
|
"theme_color": "#1e40af", |
|
"icons": [ |
|
{ |
|
"src": "/icon-192x192.png", |
|
"sizes": "192x192", |
|
"type": "image/png" |
|
}, |
|
{ |
|
"src": "/icon-512x512.png", |
|
"sizes": "512x512", |
|
"type": "image/png" |
|
} |
|
] |
|
}; |
|
|
|
const blob = new Blob([JSON.stringify(manifest)], { type: 'application/json' }); |
|
const manifestUrl = URL.createObjectURL(blob); |
|
const link = document.createElement('link'); |
|
link.rel = 'manifest'; |
|
link.href = manifestUrl; |
|
document.head.appendChild(link); |
|
</script> |
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=vs4vijay/skywatch" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> |
|
</html> |