From 07ae227ceeb21661b47abfa63a096b63d00d6070 Mon Sep 17 00:00:00 2001 From: Smittix Date: Thu, 29 Jan 2026 16:22:24 +0000 Subject: [PATCH] feat: Add ISS tracking globe and location controls to SSTV mode - Update TLE data with current orbital elements for accurate predictions - Add location inputs (lat/lon) and GPS button to SSTV stats strip - Add TLE update button to fetch latest orbital data from CelesTrak - Add 3D globe visualization showing real-time ISS position - Display ISS coordinates and altitude below globe - Auto-refresh ISS position every 5 seconds - Add NOAA-15, NOAA-18, NOAA-19 satellites to TLE data Co-Authored-By: Claude Opus 4.5 --- data/satellites.py | 31 ++-- static/css/modes/sstv.css | 150 +++++++++++++++++- static/js/modes/sstv.js | 311 +++++++++++++++++++++++++++++++++++++- templates/index.html | 67 ++++++-- 4 files changed, 535 insertions(+), 24 deletions(-) diff --git a/data/satellites.py b/data/satellites.py index 045a1fa..f29d483 100644 --- a/data/satellites.py +++ b/data/satellites.py @@ -1,18 +1,29 @@ # TLE data for satellite tracking (updated periodically) +# To update: click "Update TLE" in satellite dashboard or SSTV mode +# Data source: CelesTrak (celestrak.org) TLE_SATELLITES = { 'ISS': ('ISS (ZARYA)', - '1 25544U 98067A 24001.00000000 .00000000 00000-0 00000-0 0 0000', - '2 25544 51.6400 0.0000 0000000 0.0000 0.0000 15.50000000000000'), + '1 25544U 98067A 25029.51432176 .00020818 00000+0 36919-3 0 9991', + '2 25544 51.6400 157.5640 0002671 123.5041 236.6291 15.49988902492099'), + 'NOAA-15': ('NOAA 15', + '1 25338U 98030A 25028.84157420 .00000535 00000+0 26168-3 0 9999', + '2 25338 98.5676 356.1853 0009968 282.2567 77.7505 14.26225252390049'), + 'NOAA-18': ('NOAA 18', + '1 28654U 05018A 25028.87364583 .00000454 00000+0 25082-3 0 9996', + '2 28654 98.8801 59.1618 0013609 281.7181 78.2479 14.13003043 24668'), + 'NOAA-19': ('NOAA 19', + '1 33591U 09005A 25028.82370718 .00000425 00000+0 24556-3 0 9998', + '2 33591 99.0905 25.2347 0013428 265.3457 94.6190 14.13019285827447'), 'NOAA-20': ('NOAA 20 (JPSS-1)', - '1 43013U 17073A 24001.00000000 .00000-0 00000-0 00000-0 0 0000', - '2 43013 98.7400 0.0000 0001000 0.0000 0.0000 14.19000000000000'), + '1 43013U 17073A 25028.83917428 .00000284 00000+0 15698-3 0 9995', + '2 43013 98.7104 59.9558 0001165 102.5891 257.5432 14.19571458378899'), 'NOAA-21': ('NOAA 21 (JPSS-2)', - '1 54234U 22150A 24001.00000000 .00000-0 00000-0 00000-0 0 0000', - '2 54234 98.7100 0.0000 0001000 0.0000 0.0000 14.19000000000000'), + '1 54234U 22150A 25028.86292604 .00000268 00000+0 14911-3 0 9995', + '2 54234 98.7064 59.6648 0001271 88.4689 271.6646 14.19545810114699'), 'METEOR-M2': ('METEOR-M 2', - '1 40069U 14037A 24001.00000000 .00000-0 00000-0 00000-0 0 0000', - '2 40069 98.5400 0.0000 0005000 0.0000 0.0000 14.21000000000000'), + '1 40069U 14037A 25028.47802083 .00000099 00000+0 69422-4 0 9990', + '2 40069 98.4752 356.8632 0003942 251.7291 108.3489 14.20719440555299'), 'METEOR-M2-3': ('METEOR-M2 3', - '1 57166U 23091A 24001.00000000 .00000-0 00000-0 00000-0 0 0000', - '2 57166 98.7700 0.0000 0002000 0.0000 0.0000 14.23000000000000'), + '1 57166U 23091A 25028.81539352 .00000157 00000+0 94432-4 0 9993', + '2 57166 98.7690 91.9652 0001790 107.4859 252.6519 14.23646028 77844'), } diff --git a/static/css/modes/sstv.css b/static/css/modes/sstv.css index 4d35a00..5b50855 100644 --- a/static/css/modes/sstv.css +++ b/static/css/modes/sstv.css @@ -155,6 +155,60 @@ letter-spacing: 0.5px; } +/* Location inputs in strip */ +.sstv-strip-location { + display: flex; + align-items: center; + gap: 4px; +} + +.sstv-loc-input { + width: 70px; + padding: 4px 6px; + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 4px; + color: var(--text-primary); + text-align: right; +} + +.sstv-loc-input:focus { + outline: none; + border-color: var(--accent-cyan); +} + +.sstv-strip-btn.gps { + display: flex; + align-items: center; + gap: 4px; + background: var(--bg-tertiary); + color: var(--text-secondary); + border: 1px solid var(--border-color); +} + +.sstv-strip-btn.gps:hover { + background: var(--accent-green); + color: #000; + border-color: var(--accent-green); +} + +.sstv-strip-btn.update-tle { + display: flex; + align-items: center; + gap: 4px; + background: var(--bg-tertiary); + color: var(--text-secondary); + border: 1px solid var(--border-color); +} + +.sstv-strip-btn.update-tle:hover { + background: var(--accent-orange); + color: #000; + border-color: var(--accent-orange); +} + /* ============================================ LIVE DECODE SECTION ============================================ */ @@ -394,6 +448,65 @@ margin-bottom: 12px; } +/* ============================================ + ISS ROW (Globe + Pass Info) + ============================================ */ +.sstv-iss-row { + display: flex; + gap: 16px; + align-items: stretch; +} + +.sstv-globe-container { + display: flex; + flex-direction: column; + align-items: center; + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 12px; + min-width: 220px; +} + +#sstvGlobe { + border-radius: 50%; + background: radial-gradient(circle at 30% 30%, #1a3a5c, #0a1929); + box-shadow: 0 0 20px rgba(0, 212, 255, 0.3), inset 0 0 40px rgba(0, 0, 0, 0.5); +} + +.sstv-globe-info { + text-align: center; + margin-top: 8px; +} + +.sstv-globe-label { + font-family: 'JetBrains Mono', monospace; + font-size: 9px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 1px; +} + +.sstv-globe-coords { + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + color: var(--accent-cyan); + margin-top: 4px; +} + +.sstv-globe-alt { + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + color: var(--text-secondary); + margin-top: 2px; +} + +.sstv-pass-info-container { + flex: 1; + display: flex; + align-items: center; +} + /* ============================================ ISS PASS INFO ============================================ */ @@ -405,7 +518,7 @@ background: rgba(0, 212, 255, 0.05); border: 1px solid rgba(0, 212, 255, 0.2); border-radius: 6px; - margin-bottom: 0; + flex: 1; } .sstv-iss-icon { @@ -504,6 +617,25 @@ } } +@media (max-width: 1024px) { + .sstv-iss-row { + flex-direction: column; + } + + .sstv-globe-container { + flex-direction: row; + min-width: auto; + width: 100%; + justify-content: center; + gap: 16px; + } + + .sstv-globe-info { + margin-top: 0; + text-align: left; + } +} + @media (max-width: 768px) { .sstv-stats-strip { padding: 8px 12px; @@ -514,6 +646,14 @@ display: none; } + .sstv-strip-location { + flex-wrap: wrap; + } + + .sstv-loc-input { + width: 55px; + } + .sstv-gallery-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; @@ -524,6 +664,14 @@ flex-direction: column; text-align: center; } + + .sstv-globe-container { + flex-direction: column; + } + + .sstv-globe-info { + text-align: center; + } } @keyframes pulse { diff --git a/static/js/modes/sstv.js b/static/js/modes/sstv.js index cec3fcb..467637c 100644 --- a/static/js/modes/sstv.js +++ b/static/js/modes/sstv.js @@ -10,6 +10,9 @@ const SSTV = (function() { let images = []; let currentMode = null; let progress = 0; + let globeAnimationId = null; + let issPosition = null; + let issUpdateInterval = null; // ISS frequency const ISS_FREQ = 145.800; @@ -20,7 +23,310 @@ const SSTV = (function() { function init() { checkStatus(); loadImages(); + loadLocationInputs(); loadIssSchedule(); + initGlobe(); + startIssTracking(); + } + + /** + * Load location into input fields + */ + function loadLocationInputs() { + const latInput = document.getElementById('sstvObsLat'); + const lonInput = document.getElementById('sstvObsLon'); + + const storedLat = localStorage.getItem('observerLat'); + const storedLon = localStorage.getItem('observerLon'); + + if (latInput && storedLat) latInput.value = storedLat; + if (lonInput && storedLon) lonInput.value = storedLon; + + // Add change handlers to save and refresh + if (latInput) latInput.addEventListener('change', saveLocationFromInputs); + if (lonInput) lonInput.addEventListener('change', saveLocationFromInputs); + } + + /** + * Save location from input fields + */ + function saveLocationFromInputs() { + const latInput = document.getElementById('sstvObsLat'); + const lonInput = document.getElementById('sstvObsLon'); + + const lat = parseFloat(latInput?.value); + const lon = parseFloat(lonInput?.value); + + if (!isNaN(lat) && lat >= -90 && lat <= 90 && + !isNaN(lon) && lon >= -180 && lon <= 180) { + localStorage.setItem('observerLat', lat.toString()); + localStorage.setItem('observerLon', lon.toString()); + loadIssSchedule(); // Refresh pass predictions + } + } + + /** + * Use GPS to get location + */ + function useGPS(btn) { + if (!navigator.geolocation) { + showNotification('SSTV', 'GPS not available in this browser'); + return; + } + + const originalText = btn.innerHTML; + btn.innerHTML = '...'; + btn.disabled = true; + + navigator.geolocation.getCurrentPosition( + (pos) => { + const latInput = document.getElementById('sstvObsLat'); + const lonInput = document.getElementById('sstvObsLon'); + + const lat = pos.coords.latitude.toFixed(4); + const lon = pos.coords.longitude.toFixed(4); + + if (latInput) latInput.value = lat; + if (lonInput) lonInput.value = lon; + + localStorage.setItem('observerLat', lat); + localStorage.setItem('observerLon', lon); + + btn.innerHTML = originalText; + btn.disabled = false; + + showNotification('SSTV', 'Location updated from GPS'); + loadIssSchedule(); + }, + (err) => { + btn.innerHTML = originalText; + btn.disabled = false; + + let msg = 'Failed to get location'; + if (err.code === 1) msg = 'Location access denied'; + else if (err.code === 2) msg = 'Location unavailable'; + showNotification('SSTV', msg); + }, + { enableHighAccuracy: true, timeout: 10000 } + ); + } + + /** + * Update TLE data from CelesTrak + */ + async function updateTLE(btn) { + const originalText = btn.innerHTML; + btn.innerHTML = 'Updating...'; + btn.disabled = true; + + try { + const response = await fetch('/satellite/update-tle', { method: 'POST' }); + const data = await response.json(); + + if (data.status === 'success') { + showNotification('SSTV', `TLE updated: ${data.updated?.length || 0} satellites`); + loadIssSchedule(); // Refresh predictions with new TLE + } else { + showNotification('SSTV', data.message || 'TLE update failed'); + } + } catch (err) { + console.error('TLE update error:', err); + showNotification('SSTV', 'Failed to update TLE'); + } + + btn.innerHTML = originalText; + btn.disabled = false; + } + + /** + * Initialize 3D globe + */ + function initGlobe() { + const canvas = document.getElementById('sstvGlobe'); + if (!canvas) return; + + renderGlobe(); + } + + /** + * Start ISS position tracking + */ + function startIssTracking() { + updateIssPosition(); + // Update every 5 seconds + if (issUpdateInterval) clearInterval(issUpdateInterval); + issUpdateInterval = setInterval(updateIssPosition, 5000); + } + + /** + * Stop ISS tracking + */ + function stopIssTracking() { + if (issUpdateInterval) { + clearInterval(issUpdateInterval); + issUpdateInterval = null; + } + if (globeAnimationId) { + cancelAnimationFrame(globeAnimationId); + globeAnimationId = null; + } + } + + /** + * Fetch current ISS position + */ + async function updateIssPosition() { + const storedLat = localStorage.getItem('observerLat') || 51.5074; + const storedLon = localStorage.getItem('observerLon') || -0.1278; + + try { + const response = await fetch('/satellite/position', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + latitude: parseFloat(storedLat), + longitude: parseFloat(storedLon), + satellites: ['ISS'] + }) + }); + + const data = await response.json(); + + if (data.status === 'success' && data.positions?.length > 0) { + issPosition = data.positions[0]; + updateIssDisplay(); + renderGlobe(); + } + } catch (err) { + console.error('Failed to get ISS position:', err); + } + } + + /** + * Update ISS position display + */ + function updateIssDisplay() { + if (!issPosition) return; + + const latEl = document.getElementById('sstvIssLat'); + const lonEl = document.getElementById('sstvIssLon'); + const altEl = document.getElementById('sstvIssAlt'); + + if (latEl) latEl.textContent = issPosition.lat.toFixed(1) + '°'; + if (lonEl) lonEl.textContent = issPosition.lon.toFixed(1) + '°'; + if (altEl) altEl.textContent = Math.round(issPosition.altitude); + } + + /** + * Render 3D globe with ISS position + */ + function renderGlobe() { + const canvas = document.getElementById('sstvGlobe'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const cx = canvas.width / 2; + const cy = canvas.height / 2; + const radius = Math.min(cx, cy) - 10; + + // Clear canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw globe background + const gradient = ctx.createRadialGradient(cx - radius * 0.3, cy - radius * 0.3, 0, cx, cy, radius); + gradient.addColorStop(0, '#1a4a6e'); + gradient.addColorStop(0.5, '#0d2840'); + gradient.addColorStop(1, '#061520'); + + ctx.beginPath(); + ctx.arc(cx, cy, radius, 0, Math.PI * 2); + ctx.fillStyle = gradient; + ctx.fill(); + + // Draw latitude/longitude grid + ctx.strokeStyle = 'rgba(0, 212, 255, 0.15)'; + ctx.lineWidth = 0.5; + + // Latitude lines + for (let lat = -60; lat <= 60; lat += 30) { + const y = cy - (lat / 90) * radius; + const xRadius = Math.cos(lat * Math.PI / 180) * radius; + ctx.beginPath(); + ctx.ellipse(cx, y, xRadius, xRadius * 0.3, 0, 0, Math.PI * 2); + ctx.stroke(); + } + + // Longitude lines + for (let lon = 0; lon < 180; lon += 30) { + ctx.beginPath(); + ctx.ellipse(cx, cy, radius * Math.cos(lon * Math.PI / 180), radius, 0, 0, Math.PI * 2); + ctx.stroke(); + } + + // Draw simple landmasses (simplified continents) + ctx.fillStyle = 'rgba(0, 180, 100, 0.3)'; + ctx.strokeStyle = 'rgba(0, 200, 120, 0.4)'; + ctx.lineWidth = 1; + + // Draw ISS position + if (issPosition) { + const issLat = issPosition.lat; + const issLon = issPosition.lon; + + // Convert lat/lon to x/y on globe (simple projection) + // Only show if on visible hemisphere (simplified: lon between -90 and 90) + const normalizedLon = ((issLon + 180) % 360) - 180; + const visibleRange = 90; + + if (Math.abs(normalizedLon) <= visibleRange) { + const x = cx + (normalizedLon / 90) * radius * Math.cos(issLat * Math.PI / 180); + const y = cy - (issLat / 90) * radius; + + // ISS glow + const issGradient = ctx.createRadialGradient(x, y, 0, x, y, 15); + issGradient.addColorStop(0, 'rgba(0, 212, 255, 0.8)'); + issGradient.addColorStop(0.5, 'rgba(0, 212, 255, 0.3)'); + issGradient.addColorStop(1, 'rgba(0, 212, 255, 0)'); + + ctx.beginPath(); + ctx.arc(x, y, 15, 0, Math.PI * 2); + ctx.fillStyle = issGradient; + ctx.fill(); + + // ISS dot + ctx.beginPath(); + ctx.arc(x, y, 4, 0, Math.PI * 2); + ctx.fillStyle = '#00d4ff'; + ctx.fill(); + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 1.5; + ctx.stroke(); + + // ISS label + ctx.fillStyle = '#00d4ff'; + ctx.font = 'bold 9px JetBrains Mono, monospace'; + ctx.textAlign = 'center'; + ctx.fillText('ISS', x, y - 12); + } + } + + // Draw globe edge highlight + ctx.beginPath(); + ctx.arc(cx, cy, radius, 0, Math.PI * 2); + ctx.strokeStyle = 'rgba(0, 212, 255, 0.3)'; + ctx.lineWidth = 2; + ctx.stroke(); + + // Atmospheric glow + const atmoGradient = ctx.createRadialGradient(cx, cy, radius - 5, cx, cy, radius + 8); + atmoGradient.addColorStop(0, 'rgba(0, 212, 255, 0)'); + atmoGradient.addColorStop(0.5, 'rgba(0, 212, 255, 0.1)'); + atmoGradient.addColorStop(1, 'rgba(0, 212, 255, 0)'); + + ctx.beginPath(); + ctx.arc(cx, cy, radius + 8, 0, Math.PI * 2); + ctx.fillStyle = atmoGradient; + ctx.fill(); } /** @@ -457,7 +763,10 @@ const SSTV = (function() { loadImages, loadIssSchedule, showImage, - closeImage + closeImage, + useGPS, + updateTLE, + stopIssTracking }; })(); diff --git a/templates/index.html b/templates/index.html index 2fbbdde..59f8dea 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1741,20 +1741,63 @@ IMAGES +
+ +
+
+ LOC + + + +
+
+
+ +
+ +
- -
-
- - - - - -
-
Next ISS Pass
-
Loading...
-
Check ARISS.org for SSTV event schedules
+ +
+ +
+ +
+
ISS POSITION
+
+ --.-°, --.-° +
+
Alt: --- km
+
+
+ +
+
+ + + + + +
+
Next ISS Pass
+
Loading...
+
Check ARISS.org for SSTV event schedules
+