vs4vijay commited on
Commit
7eec6c7
·
verified ·
1 Parent(s): ac3040a

Add 2 files

Browse files
Files changed (2) hide show
  1. README.md +6 -4
  2. index.html +751 -19
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Skywatch
3
- emoji: 📚
4
  colorFrom: red
5
- colorTo: purple
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: skywatch
3
+ emoji: 🐳
4
  colorFrom: red
5
+ colorTo: red
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,751 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SkyWatch - Plane Tracker</title>
7
+ <meta name="description" content="Track planes flying above your location in real-time">
8
+ <meta name="theme-color" content="#1e40af">
9
+ <link rel="manifest" href="/manifest.json">
10
+ <script src="https://cdn.tailwindcss.com"></script>
11
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
12
+ <style>
13
+ @keyframes pulse {
14
+ 0%, 100% { opacity: 1; }
15
+ 50% { opacity: 0.5; }
16
+ }
17
+ .animate-pulse {
18
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
19
+ }
20
+ .notification-badge {
21
+ position: absolute;
22
+ top: -5px;
23
+ right: -5px;
24
+ background-color: #ef4444;
25
+ color: white;
26
+ border-radius: 9999px;
27
+ width: 20px;
28
+ height: 20px;
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ font-size: 12px;
33
+ font-weight: bold;
34
+ }
35
+ .plane-icon {
36
+ transform: rotate(var(--rotation));
37
+ }
38
+ .map-container {
39
+ height: 300px;
40
+ background-color: #e5e7eb;
41
+ position: relative;
42
+ overflow: hidden;
43
+ }
44
+ .map-plane {
45
+ position: absolute;
46
+ transition: all 1s ease-out;
47
+ }
48
+ .radar-sweep {
49
+ position: absolute;
50
+ top: 50%;
51
+ left: 50%;
52
+ width: 100%;
53
+ height: 100%;
54
+ border-radius: 50%;
55
+ transform: translate(-50%, -50%);
56
+ background: conic-gradient(from 0deg, rgba(59, 130, 246, 0.1) 0%, rgba(59, 130, 246, 0.3) 50%, transparent 50%);
57
+ animation: radar-rotate 4s linear infinite;
58
+ pointer-events: none;
59
+ }
60
+ @keyframes radar-rotate {
61
+ from { transform: translate(-50%, -50%) rotate(0deg); }
62
+ to { transform: translate(-50%, -50%) rotate(360deg); }
63
+ }
64
+ </style>
65
+ </head>
66
+ <body class="bg-gray-100 min-h-screen">
67
+ <div class="max-w-md mx-auto bg-white shadow-lg rounded-b-xl overflow-hidden">
68
+ <!-- Header -->
69
+ <header class="bg-blue-800 text-white p-4 relative">
70
+ <div class="flex justify-between items-center">
71
+ <div>
72
+ <h1 class="text-xl font-bold">SkyWatch</h1>
73
+ <p class="text-sm opacity-80" id="location-status">Detecting your location...</p>
74
+ </div>
75
+ <div class="flex space-x-3">
76
+ <button id="notification-btn" class="relative p-2 rounded-full hover:bg-blue-700 transition">
77
+ <i class="fas fa-bell"></i>
78
+ <span id="notification-count" class="notification-badge hidden">0</span>
79
+ </button>
80
+ <button id="refresh-btn" class="p-2 rounded-full hover:bg-blue-700 transition">
81
+ <i class="fas fa-sync-alt"></i>
82
+ </button>
83
+ </div>
84
+ </div>
85
+ </header>
86
+
87
+ <!-- Main Content -->
88
+ <main class="p-4">
89
+ <!-- Radar Map -->
90
+ <div class="map-container rounded-lg mb-4 relative">
91
+ <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>
92
+ <div id="map-planes" class="relative z-0"></div>
93
+ <div class="radar-sweep"></div>
94
+ </div>
95
+
96
+ <!-- Current Plane Info -->
97
+ <div id="current-plane" class="bg-blue-50 rounded-lg p-3 mb-4 hidden">
98
+ <div class="flex justify-between items-center mb-2">
99
+ <h3 class="font-bold text-blue-800">Plane Above You Now</h3>
100
+ <span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full">LIVE</span>
101
+ </div>
102
+ <div class="flex items-center space-x-3">
103
+ <div class="bg-blue-100 p-2 rounded-full">
104
+ <i class="fas fa-plane text-blue-600"></i>
105
+ </div>
106
+ <div class="flex-1">
107
+ <div class="flex justify-between">
108
+ <span class="font-medium" id="current-callsign">--</span>
109
+ <span class="text-sm" id="current-altitude">-- ft</span>
110
+ </div>
111
+ <div class="text-sm text-gray-600" id="current-airline">--</div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+
116
+ <!-- Upcoming Planes -->
117
+ <div class="mb-2 flex justify-between items-center">
118
+ <h2 class="font-bold text-gray-800">Upcoming Planes</h2>
119
+ <span class="text-xs bg-gray-200 text-gray-800 px-2 py-1 rounded-full" id="planes-count">0 planes</span>
120
+ </div>
121
+ <div id="upcoming-planes" class="space-y-3">
122
+ <div class="text-center py-8 text-gray-500" id="loading-planes">
123
+ <i class="fas fa-plane animate-pulse text-2xl mb-2"></i>
124
+ <p>Scanning for planes...</p>
125
+ </div>
126
+ </div>
127
+ </main>
128
+
129
+ <!-- Footer -->
130
+ <footer class="bg-gray-50 p-3 border-t flex justify-between text-xs text-gray-500">
131
+ <div>
132
+ <span id="last-updated">Last updated: --</span>
133
+ </div>
134
+ <div>
135
+ <span id="api-status">API: Offline</span>
136
+ </div>
137
+ </footer>
138
+ </div>
139
+
140
+ <!-- Notification Modal -->
141
+ <div id="notification-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
142
+ <div class="bg-white rounded-lg w-full max-w-sm mx-4 max-h-[80vh] overflow-y-auto">
143
+ <div class="p-4 border-b flex justify-between items-center">
144
+ <h3 class="font-bold">Plane Alerts</h3>
145
+ <button id="close-notifications" class="text-gray-500 hover:text-gray-700">
146
+ <i class="fas fa-times"></i>
147
+ </button>
148
+ </div>
149
+ <div id="notification-list" class="divide-y">
150
+ <div class="p-4 text-center text-gray-500">
151
+ No alerts yet
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </div>
156
+
157
+ <!-- Install Prompt -->
158
+ <div id="install-prompt" class="fixed bottom-4 left-0 right-0 flex justify-center hidden">
159
+ <div class="bg-blue-600 text-white rounded-lg shadow-lg p-4 mx-4 max-w-md flex items-center justify-between">
160
+ <div class="flex items-center">
161
+ <i class="fas fa-plane-departure text-xl mr-3"></i>
162
+ <div>
163
+ <p class="font-medium">Install SkyWatch</p>
164
+ <p class="text-sm opacity-90">Get real-time plane alerts</p>
165
+ </div>
166
+ </div>
167
+ <button id="install-btn" class="bg-white text-blue-600 px-3 py-1 rounded-full text-sm font-medium">Install</button>
168
+ </div>
169
+ </div>
170
+
171
+ <script>
172
+ // Service Worker Registration
173
+ if ('serviceWorker' in navigator) {
174
+ window.addEventListener('load', () => {
175
+ navigator.serviceWorker.register('/sw.js').then(registration => {
176
+ console.log('ServiceWorker registration successful');
177
+ }).catch(err => {
178
+ console.log('ServiceWorker registration failed: ', err);
179
+ });
180
+ });
181
+ }
182
+
183
+ // App state
184
+ const state = {
185
+ location: null,
186
+ planes: [],
187
+ notifications: [],
188
+ lastUpdated: null,
189
+ currentPlane: null,
190
+ deferredPrompt: null
191
+ };
192
+
193
+ // DOM Elements
194
+ const elements = {
195
+ locationStatus: document.getElementById('location-status'),
196
+ currentPlaneCard: document.getElementById('current-plane'),
197
+ currentCallsign: document.getElementById('current-callsign'),
198
+ currentAltitude: document.getElementById('current-altitude'),
199
+ currentAirline: document.getElementById('current-airline'),
200
+ upcomingPlanes: document.getElementById('upcoming-planes'),
201
+ loadingPlanes: document.getElementById('loading-planes'),
202
+ planesCount: document.getElementById('planes-count'),
203
+ lastUpdated: document.getElementById('last-updated'),
204
+ apiStatus: document.getElementById('api-status'),
205
+ notificationBtn: document.getElementById('notification-btn'),
206
+ notificationCount: document.getElementById('notification-count'),
207
+ notificationModal: document.getElementById('notification-modal'),
208
+ notificationList: document.getElementById('notification-list'),
209
+ closeNotifications: document.getElementById('close-notifications'),
210
+ refreshBtn: document.getElementById('refresh-btn'),
211
+ installPrompt: document.getElementById('install-prompt'),
212
+ installBtn: document.getElementById('install-btn'),
213
+ mapPlanes: document.getElementById('map-planes'),
214
+ radarCenter: document.getElementById('radar-center')
215
+ };
216
+
217
+ // Event Listeners
218
+ elements.notificationBtn.addEventListener('click', showNotifications);
219
+ elements.closeNotifications.addEventListener('click', hideNotifications);
220
+ elements.refreshBtn.addEventListener('click', refreshData);
221
+ elements.installBtn.addEventListener('click', installApp);
222
+
223
+ // Install PWA prompt
224
+ window.addEventListener('beforeinstallprompt', (e) => {
225
+ e.preventDefault();
226
+ state.deferredPrompt = e;
227
+ elements.installPrompt.classList.remove('hidden');
228
+ });
229
+
230
+ function installApp() {
231
+ if (state.deferredPrompt) {
232
+ state.deferredPrompt.prompt();
233
+ state.deferredPrompt.userChoice.then((choiceResult) => {
234
+ if (choiceResult.outcome === 'accepted') {
235
+ elements.installPrompt.classList.add('hidden');
236
+ }
237
+ state.deferredPrompt = null;
238
+ });
239
+ }
240
+ }
241
+
242
+ // Get user location
243
+ function getLocation() {
244
+ elements.locationStatus.textContent = 'Detecting your location...';
245
+
246
+ if (navigator.geolocation) {
247
+ navigator.geolocation.getCurrentPosition(
248
+ position => {
249
+ state.location = {
250
+ lat: position.coords.latitude,
251
+ lon: position.coords.longitude,
252
+ accuracy: position.coords.accuracy
253
+ };
254
+ elements.locationStatus.textContent = `📍 ${state.location.lat.toFixed(4)}, ${state.location.lon.toFixed(4)}`;
255
+ getPlaneData();
256
+ },
257
+ error => {
258
+ console.error('Geolocation error:', error);
259
+ elements.locationStatus.textContent = 'Location access denied. Using default location.';
260
+ // Default to New York if location access is denied
261
+ state.location = { lat: 40.7128, lon: -74.0060 };
262
+ getPlaneData();
263
+ },
264
+ { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }
265
+ );
266
+ } else {
267
+ elements.locationStatus.textContent = 'Geolocation not supported. Using default location.';
268
+ state.location = { lat: 40.7128, lon: -74.0060 };
269
+ getPlaneData();
270
+ }
271
+ }
272
+
273
+ // Fetch plane data from OpenSky Network API
274
+ async function getPlaneData() {
275
+ elements.loadingPlanes.classList.remove('hidden');
276
+ elements.upcomingPlanes.innerHTML = '';
277
+ elements.upcomingPlanes.appendChild(elements.loadingPlanes);
278
+ elements.apiStatus.textContent = 'API: Loading...';
279
+
280
+ try {
281
+ // In a real app, you would use the actual OpenSky Network API
282
+ // For this demo, we'll use mock data
283
+ // const response = await fetch(`https://opensky-network.org/api/states/all?lamin=${state.location.lat - 1}&lomin=${state.location.lon - 1}&lamax=${state.location.lat + 1}&lomax=${state.location.lon + 1}`);
284
+ // const data = await response.json();
285
+
286
+ // Mock data for demonstration
287
+ const mockPlanes = generateMockPlanes(state.location.lat, state.location.lon);
288
+ state.planes = mockPlanes.states;
289
+
290
+ processPlaneData();
291
+ elements.apiStatus.textContent = 'API: Online';
292
+ elements.apiStatus.className = elements.apiStatus.className.replace('text-red-500', 'text-green-500');
293
+ } catch (error) {
294
+ console.error('Error fetching plane data:', error);
295
+ elements.apiStatus.textContent = 'API: Offline';
296
+ elements.apiStatus.className += ' text-red-500';
297
+
298
+ elements.loadingPlanes.innerHTML = `
299
+ <div class="text-center py-8 text-red-500">
300
+ <i class="fas fa-exclamation-triangle text-2xl mb-2"></i>
301
+ <p>Failed to load plane data</p>
302
+ <button class="mt-2 text-blue-600 text-sm font-medium">Retry</button>
303
+ </div>
304
+ `;
305
+ elements.loadingPlanes.querySelector('button').addEventListener('click', getPlaneData);
306
+ }
307
+ }
308
+
309
+ // Process plane data and update UI
310
+ function processPlaneData() {
311
+ const now = new Date();
312
+ state.lastUpdated = now;
313
+ elements.lastUpdated.textContent = `Last updated: ${now.toLocaleTimeString()}`;
314
+
315
+ // Filter and sort planes by distance and time
316
+ const nearbyPlanes = state.planes
317
+ .filter(plane => plane[5] && plane[6]) // has position
318
+ .map(plane => ({
319
+ icao24: plane[0],
320
+ callsign: plane[1]?.trim() || 'N/A',
321
+ origin: plane[2] || 'N/A',
322
+ time: plane[3] || Date.now() / 1000,
323
+ lastContact: plane[4] || Date.now() / 1000,
324
+ longitude: plane[5],
325
+ latitude: plane[6],
326
+ altitude: plane[7] || 0,
327
+ onGround: plane[8],
328
+ velocity: plane[9] || 0,
329
+ heading: plane[10] || 0,
330
+ verticalRate: plane[11] || 0,
331
+ airline: getAirlineFromCallsign(plane[1]?.trim())
332
+ }))
333
+ .filter(plane => !plane.onGround && plane.altitude > 1000) // only airborne planes
334
+ .sort((a, b) => {
335
+ // Sort by distance to user
336
+ const distA = getDistance(state.location.lat, state.location.lon, a.latitude, a.longitude);
337
+ const distB = getDistance(state.location.lat, state.location.lon, b.latitude, b.longitude);
338
+ return distA - distB;
339
+ });
340
+
341
+ // Check for planes directly overhead
342
+ const overheadPlane = nearbyPlanes.find(plane => {
343
+ const distance = getDistance(state.location.lat, state.location.lon, plane.latitude, plane.longitude);
344
+ return distance < 0.5; // within 0.5 km
345
+ });
346
+
347
+ if (overheadPlane) {
348
+ showCurrentPlane(overheadPlane);
349
+
350
+ // Check if we've already notified about this plane
351
+ if (!state.notifications.some(n => n.icao24 === overheadPlane.icao24)) {
352
+ addNotification(overheadPlane);
353
+ showAlert(overheadPlane);
354
+ }
355
+ } else {
356
+ hideCurrentPlane();
357
+ }
358
+
359
+ // Display upcoming planes (next 5 closest)
360
+ const upcomingPlanes = nearbyPlanes.slice(0, 5);
361
+ displayUpcomingPlanes(upcomingPlanes);
362
+
363
+ // Update radar map
364
+ updateRadarMap(nearbyPlanes.slice(0, 10));
365
+ }
366
+
367
+ // Display current plane above user
368
+ function showCurrentPlane(plane) {
369
+ state.currentPlane = plane;
370
+ elements.currentPlaneCard.classList.remove('hidden');
371
+ elements.currentCallsign.textContent = plane.callsign;
372
+ elements.currentAltitude.textContent = `${Math.round(plane.altitude * 0.3048)} m (${Math.round(plane.altitude)} ft)`;
373
+ elements.currentAirline.textContent = plane.airline || 'Unknown airline';
374
+ }
375
+
376
+ function hideCurrentPlane() {
377
+ state.currentPlane = null;
378
+ elements.currentPlaneCard.classList.add('hidden');
379
+ }
380
+
381
+ // Display upcoming planes list
382
+ function displayUpcomingPlanes(planes) {
383
+ elements.loadingPlanes.classList.add('hidden');
384
+ elements.planesCount.textContent = `${planes.length} ${planes.length === 1 ? 'plane' : 'planes'}`;
385
+
386
+ if (planes.length === 0) {
387
+ elements.upcomingPlanes.innerHTML = `
388
+ <div class="text-center py-8 text-gray-500">
389
+ <i class="fas fa-plane-slash text-2xl mb-2"></i>
390
+ <p>No planes detected nearby</p>
391
+ </div>
392
+ `;
393
+ return;
394
+ }
395
+
396
+ elements.upcomingPlanes.innerHTML = '';
397
+
398
+ planes.forEach(plane => {
399
+ const distance = getDistance(state.location.lat, state.location.lon, plane.latitude, plane.longitude);
400
+ const timeToOverhead = distance / (plane.velocity * 0.514444); // knots to m/s
401
+
402
+ const planeEl = document.createElement('div');
403
+ planeEl.className = 'bg-white rounded-lg p-3 shadow-sm border';
404
+ planeEl.innerHTML = `
405
+ <div class="flex items-center space-x-3">
406
+ <div class="bg-blue-100 p-2 rounded-full">
407
+ <i class="fas fa-plane text-blue-600 plane-icon" style="--rotation: ${plane.heading}deg"></i>
408
+ </div>
409
+ <div class="flex-1">
410
+ <div class="flex justify-between">
411
+ <span class="font-medium">${plane.callsign}</span>
412
+ <span class="text-sm">${Math.round(plane.altitude * 0.3048)} m</span>
413
+ </div>
414
+ <div class="text-sm text-gray-600">${plane.airline || 'Unknown airline'}</div>
415
+ <div class="flex justify-between mt-1 text-xs">
416
+ <span>${distance < 1 ? `${Math.round(distance * 1000)} m away` : `${distance.toFixed(1)} km away`}</span>
417
+ <span>ETA: ${timeToOverhead > 0 ? `${Math.round(timeToOverhead / 60)} min` : 'now'}</span>
418
+ </div>
419
+ </div>
420
+ </div>
421
+ `;
422
+ elements.upcomingPlanes.appendChild(planeEl);
423
+ });
424
+ }
425
+
426
+ // Update radar map with plane positions
427
+ function updateRadarMap(planes) {
428
+ elements.mapPlanes.innerHTML = '';
429
+
430
+ // Radar range in km
431
+ const radarRange = 10;
432
+
433
+ planes.forEach(plane => {
434
+ const distance = getDistance(state.location.lat, state.location.lon, plane.latitude, plane.longitude);
435
+ if (distance > radarRange) return;
436
+
437
+ // Calculate position on radar (relative to center)
438
+ const bearing = getBearing(state.location.lat, state.location.lon, plane.latitude, plane.longitude);
439
+ const scale = distance / radarRange;
440
+
441
+ // Position plane on radar (max 150px from center)
442
+ const x = Math.cos(bearing * Math.PI / 180) * scale * 150;
443
+ const y = Math.sin(bearing * Math.PI / 180) * scale * 150;
444
+
445
+ const planeEl = document.createElement('div');
446
+ planeEl.className = 'map-plane';
447
+ planeEl.style.left = `calc(50% + ${x}px)`;
448
+ planeEl.style.top = `calc(50% + ${y}px)`;
449
+ planeEl.innerHTML = `
450
+ <div class="w-6 h-6 flex items-center justify-center">
451
+ <i class="fas fa-plane text-blue-600 text-xs plane-icon" style="--rotation: ${plane.heading}deg"></i>
452
+ </div>
453
+ `;
454
+ elements.mapPlanes.appendChild(planeEl);
455
+ });
456
+ }
457
+
458
+ // Add notification for a plane
459
+ function addNotification(plane) {
460
+ const notification = {
461
+ id: Date.now(),
462
+ icao24: plane.icao24,
463
+ callsign: plane.callsign,
464
+ altitude: plane.altitude,
465
+ time: new Date(),
466
+ read: false
467
+ };
468
+
469
+ state.notifications.unshift(notification);
470
+ updateNotificationBadge();
471
+
472
+ // Store notifications in localStorage
473
+ localStorage.setItem('skywatch-notifications', JSON.stringify(state.notifications));
474
+ }
475
+
476
+ // Show alert for a plane
477
+ function showAlert(plane) {
478
+ if (Notification.permission === 'granted') {
479
+ new Notification(`✈️ Plane overhead: ${plane.callsign}`, {
480
+ body: `Altitude: ${Math.round(plane.altitude)} ft\nAirline: ${plane.airline || 'Unknown'}`,
481
+ icon: '/icon-192x192.png'
482
+ });
483
+ } else if (Notification.permission !== 'denied') {
484
+ Notification.requestPermission().then(permission => {
485
+ if (permission === 'granted') {
486
+ showAlert(plane);
487
+ }
488
+ });
489
+ }
490
+ }
491
+
492
+ // Update notification badge
493
+ function updateNotificationBadge() {
494
+ const unreadCount = state.notifications.filter(n => !n.read).length;
495
+ if (unreadCount > 0) {
496
+ elements.notificationCount.textContent = unreadCount;
497
+ elements.notificationCount.classList.remove('hidden');
498
+ } else {
499
+ elements.notificationCount.classList.add('hidden');
500
+ }
501
+ }
502
+
503
+ // Show notifications modal
504
+ function showNotifications() {
505
+ // Mark all notifications as read
506
+ state.notifications.forEach(n => n.read = true);
507
+ updateNotificationBadge();
508
+
509
+ // Update notification list
510
+ elements.notificationList.innerHTML = '';
511
+
512
+ if (state.notifications.length === 0) {
513
+ elements.notificationList.innerHTML = '<div class="p-4 text-center text-gray-500">No alerts yet</div>';
514
+ } else {
515
+ state.notifications.forEach(notification => {
516
+ const notificationEl = document.createElement('div');
517
+ notificationEl.className = 'p-4';
518
+ notificationEl.innerHTML = `
519
+ <div class="flex items-start space-x-3">
520
+ <div class="bg-blue-100 p-2 rounded-full mt-1">
521
+ <i class="fas fa-plane text-blue-600"></i>
522
+ </div>
523
+ <div class="flex-1">
524
+ <div class="font-medium">${notification.callsign}</div>
525
+ <div class="text-sm text-gray-600">Altitude: ${Math.round(notification.altitude)} ft</div>
526
+ <div class="text-xs text-gray-500 mt-1">${new Date(notification.time).toLocaleString()}</div>
527
+ </div>
528
+ </div>
529
+ `;
530
+ elements.notificationList.appendChild(notificationEl);
531
+ });
532
+ }
533
+
534
+ elements.notificationModal.classList.remove('hidden');
535
+ }
536
+
537
+ function hideNotifications() {
538
+ elements.notificationModal.classList.add('hidden');
539
+ }
540
+
541
+ // Refresh data
542
+ function refreshData() {
543
+ elements.refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
544
+ getPlaneData();
545
+ setTimeout(() => {
546
+ elements.refreshBtn.innerHTML = '<i class="fas fa-sync-alt"></i>';
547
+ }, 1000);
548
+ }
549
+
550
+ // Helper functions
551
+ function getDistance(lat1, lon1, lat2, lon2) {
552
+ // Haversine formula to calculate distance in km
553
+ const R = 6371; // Earth radius in km
554
+ const dLat = (lat2 - lat1) * Math.PI / 180;
555
+ const dLon = (lon2 - lon1) * Math.PI / 180;
556
+ const a =
557
+ Math.sin(dLat/2) * Math.sin(dLat/2) +
558
+ Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
559
+ Math.sin(dLon/2) * Math.sin(dLon/2);
560
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
561
+ return R * c;
562
+ }
563
+
564
+ function getBearing(lat1, lon1, lat2, lon2) {
565
+ // Calculate bearing (direction) between two points
566
+ const y = Math.sin(lon2 - lon1) * Math.cos(lat2);
567
+ const x = Math.cos(lat1) * Math.sin(lat2) -
568
+ Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1);
569
+ return (Math.atan2(y, x) * 180 / Math.PI + 360) % 360;
570
+ }
571
+
572
+ function getAirlineFromCallsign(callsign) {
573
+ if (!callsign) return null;
574
+
575
+ // Extract airline code from callsign (first 3 letters usually)
576
+ const airlineCode = callsign.substring(0, 3).toUpperCase();
577
+
578
+ // Mock airline database
579
+ const airlines = {
580
+ 'UAL': 'United Airlines',
581
+ 'AAL': 'American Airlines',
582
+ 'DAL': 'Delta Air Lines',
583
+ 'SWA': 'Southwest Airlines',
584
+ 'JBU': 'JetBlue',
585
+ 'FDX': 'FedEx',
586
+ 'UPS': 'UPS Airlines',
587
+ 'AFR': 'Air France',
588
+ 'BAW': 'British Airways',
589
+ 'DLH': 'Lufthansa',
590
+ 'KLM': 'KLM Royal Dutch Airlines',
591
+ 'QFA': 'Qantas',
592
+ 'SIA': 'Singapore Airlines',
593
+ 'THY': 'Turkish Airlines',
594
+ 'UAE': 'Emirates',
595
+ 'VIR': 'Virgin Atlantic'
596
+ };
597
+
598
+ return airlines[airlineCode] || null;
599
+ }
600
+
601
+ // Generate mock plane data for demonstration
602
+ function generateMockPlanes(userLat, userLon) {
603
+ const now = Date.now() / 1000;
604
+ const mockPlanes = [];
605
+
606
+ // Generate 8-15 mock planes
607
+ const planeCount = 8 + Math.floor(Math.random() * 7);
608
+
609
+ for (let i = 0; i < planeCount; i++) {
610
+ // Generate random position within 50km radius
611
+ const distance = 0.5 + Math.random() * 50; // 0.5-50 km
612
+ const bearing = Math.random() * 360;
613
+
614
+ // Calculate position
615
+ const R = 6371; // Earth radius in km
616
+ const lat1 = userLat * Math.PI / 180;
617
+ const lon1 = userLon * Math.PI / 180;
618
+ const d = distance / R;
619
+
620
+ const lat2 = Math.asin(Math.sin(lat1) * Math.cos(d) +
621
+ Math.cos(lat1) * Math.sin(d) * Math.cos(bearing));
622
+ const lon2 = lon1 + Math.atan2(Math.sin(bearing) * Math.sin(d) * Math.cos(lat1),
623
+ Math.cos(d) - Math.sin(lat1) * Math.sin(lat2));
624
+
625
+ const planeLat = lat2 * 180 / Math.PI;
626
+ const planeLon = lon2 * 180 / Math.PI;
627
+
628
+ // Generate plane data
629
+ const callsigns = ['UAL123', 'AAL456', 'DAL789', 'SWA101', 'JBU202', 'FDX303', 'UPS404', 'AFR505', 'BAW606', 'DLH707'];
630
+ const callsign = callsigns[Math.floor(Math.random() * callsigns.length)];
631
+
632
+ mockPlanes.push([
633
+ Math.random().toString(36).substring(2, 10).toUpperCase(), // icao24
634
+ callsign, // callsign
635
+ null, // origin country
636
+ now - Math.random() * 3600, // time position
637
+ now - Math.random() * 60, // last contact
638
+ planeLon, // longitude
639
+ planeLat, // latitude
640
+ 1000 + Math.random() * 35000, // altitude (ft)
641
+ false, // on ground
642
+ 200 + Math.random() * 500, // velocity (knots)
643
+ Math.random() * 360, // heading
644
+ -20 + Math.random() * 40, // vertical rate
645
+ null, null, null, null, null, null
646
+ ]);
647
+ }
648
+
649
+ return { states: mockPlanes };
650
+ }
651
+
652
+ // Load notifications from localStorage
653
+ function loadNotifications() {
654
+ const savedNotifications = localStorage.getItem('skywatch-notifications');
655
+ if (savedNotifications) {
656
+ state.notifications = JSON.parse(savedNotifications);
657
+ updateNotificationBadge();
658
+ }
659
+ }
660
+
661
+ // Initialize app
662
+ function init() {
663
+ loadNotifications();
664
+ getLocation();
665
+
666
+ // Check for PWA installation
667
+ if (window.matchMedia('(display-mode: standalone)').matches) {
668
+ elements.installPrompt.classList.add('hidden');
669
+ }
670
+
671
+ // Auto-refresh every 30 seconds
672
+ setInterval(refreshData, 30000);
673
+ }
674
+
675
+ // Start the app
676
+ init();
677
+ </script>
678
+
679
+ <!-- Service Worker (would be in a separate file in production) -->
680
+ <script>
681
+ // This would normally be in a separate sw.js file
682
+ const CACHE_NAME = 'skywatch-v1';
683
+ const ASSETS = [
684
+ '/',
685
+ '/index.html',
686
+ '/icon-192x192.png',
687
+ '/icon-512x512.png',
688
+ '/manifest.json'
689
+ ];
690
+
691
+ self.addEventListener('install', event => {
692
+ event.waitUntil(
693
+ caches.open(CACHE_NAME)
694
+ .then(cache => cache.addAll(ASSETS))
695
+ .then(() => self.skipWaiting())
696
+ );
697
+ });
698
+
699
+ self.addEventListener('activate', event => {
700
+ event.waitUntil(
701
+ caches.keys().then(keys =>
702
+ Promise.all(
703
+ keys.filter(key => key !== CACHE_NAME)
704
+ .map(key => caches.delete(key))
705
+ )
706
+ ).then(() => self.clients.claim())
707
+ );
708
+ });
709
+
710
+ self.addEventListener('fetch', event => {
711
+ event.respondWith(
712
+ caches.match(event.request)
713
+ .then(response => response || fetch(event.request))
714
+ );
715
+ });
716
+ </script>
717
+
718
+ <!-- Manifest (would be in a separate file in production) -->
719
+ <script>
720
+ // This would normally be in a separate manifest.json file
721
+ const manifest = {
722
+ "name": "SkyWatch",
723
+ "short_name": "SkyWatch",
724
+ "description": "Track planes flying above your location in real-time",
725
+ "start_url": "/",
726
+ "display": "standalone",
727
+ "background_color": "#1e40af",
728
+ "theme_color": "#1e40af",
729
+ "icons": [
730
+ {
731
+ "src": "/icon-192x192.png",
732
+ "sizes": "192x192",
733
+ "type": "image/png"
734
+ },
735
+ {
736
+ "src": "/icon-512x512.png",
737
+ "sizes": "512x512",
738
+ "type": "image/png"
739
+ }
740
+ ]
741
+ };
742
+
743
+ const blob = new Blob([JSON.stringify(manifest)], { type: 'application/json' });
744
+ const manifestUrl = URL.createObjectURL(blob);
745
+ const link = document.createElement('link');
746
+ link.rel = 'manifest';
747
+ link.href = manifestUrl;
748
+ document.head.appendChild(link);
749
+ </script>
750
+ <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>
751
+ </html>