Files
intercept/templates/satellite_dashboard.html
2026-02-04 01:10:42 +00:00

1089 lines
46 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SATELLITE COMMAND // iNTERCEPT - See the Invisible</title>
<!-- Fonts - Conditional CDN/Local loading -->
{% if offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %}
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
{% endif %}
<!-- Leaflet.js - Conditional CDN/Local loading -->
{% if offline_settings.assets_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/leaflet/leaflet.css') }}">
<script src="{{ url_for('static', filename='vendor/leaflet/leaflet.js') }}"></script>
{% else %}
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
{% endif %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/satellite_dashboard.css') }}">
<script>
window.INTERCEPT_SHARED_OBSERVER_LOCATION = {{ shared_observer_location | tojson }};
</script>
<script src="{{ url_for('static', filename='js/core/observer-location.js') }}"></script>
</head>
<body>
<div class="grid-bg"></div>
<div class="scanline"></div>
<header class="header">
<div class="logo">
SATELLITE COMMAND
<span>// iNTERCEPT - See the Invisible</span>
</div>
<div class="stats-badges">
<div class="stat-badge">
<span class="value" id="statTracked">7</span>
<span class="label">satellites</span>
</div>
<div class="stat-badge">
<span class="value highlight" id="statVisible">0</span>
<span class="label">visible</span>
</div>
<div class="stat-badge">
<span class="value" id="statPasses">0</span>
<span class="label">passes</span>
</div>
<div class="stat-badge">
<span class="value" id="statMaxEl">0</span>
<span class="label">best el</span>
</div>
</div>
<div class="status-bar">
<!-- Location Source Selector -->
<div class="location-selector" id="locationSection">
<span class="location-label">Location:</span>
<select id="locationSource" class="location-select" title="Select observer location">
<option value="local">Local (This Device)</option>
</select>
<span class="location-status-dot online" id="locationStatusDot"></span>
</div>
<div class="status-item">
<div class="status-dot" id="trackingDot"></div>
<span id="trackingStatus">TRACKING</span>
</div>
<div class="datetime" id="utcTime">--:--:-- UTC</div>
<a href="/?mode=satellite" class="back-link">Main Dashboard</a>
</div>
</header>
{% set active_mode = 'satellite' %}
{% include 'partials/nav.html' with context %}
<main class="dashboard">
<!-- Polar Plot -->
<div class="panel polar-container">
<div class="panel-header">
<span>SKY VIEW // POLAR PLOT</span>
<div class="panel-indicator"></div>
</div>
<div class="panel-content">
<canvas id="polarPlot"></canvas>
</div>
</div>
<!-- Ground Track Map -->
<div class="panel map-container">
<div class="panel-header">
<span>GROUND TRACK // WORLD VIEW</span>
<div class="panel-indicator"></div>
</div>
<div class="panel-content" style="padding: 0;">
<div id="groundMap"></div>
</div>
</div>
<!-- Sidebar -->
<div class="sidebar">
<!-- Satellite Selector -->
<div class="satellite-selector">
<label>TARGET:</label>
<select id="satSelect" onchange="onSatelliteChange()">
<option value="25544">ISS (ZARYA)</option>
<option value="25338">NOAA 15</option>
<option value="28654">NOAA 18</option>
<option value="33591">NOAA 19</option>
<option value="40069">METEOR-M2</option>
<option value="43013">NOAA 20</option>
<option value="54234">METEOR-M2-3</option>
</select>
</div>
<!-- Countdown -->
<div class="panel countdown-panel">
<div class="panel-header">
<span>NEXT PASS</span>
<div class="panel-indicator"></div>
</div>
<div class="countdown-display">
<div class="next-pass-label">Incoming Signal</div>
<div class="satellite-name" id="countdownSat">AWAITING DATA</div>
<div class="countdown-grid">
<div class="countdown-block">
<div class="countdown-value" id="countDays">--</div>
<div class="countdown-label">Days</div>
</div>
<div class="countdown-block">
<div class="countdown-value" id="countHours">--</div>
<div class="countdown-label">Hours</div>
</div>
<div class="countdown-block">
<div class="countdown-value" id="countMins">--</div>
<div class="countdown-label">Mins</div>
</div>
<div class="countdown-block">
<div class="countdown-value" id="countSecs">--</div>
<div class="countdown-label">Secs</div>
</div>
</div>
</div>
</div>
<!-- Telemetry -->
<div class="panel telemetry-panel">
<div class="panel-header">
<span>LIVE TELEMETRY</span>
<div class="panel-indicator"></div>
</div>
<div class="panel-content">
<div class="telemetry-rows">
<div class="telemetry-item">
<div class="telemetry-label">Latitude</div>
<div class="telemetry-value" id="telLat">---.----</div>
</div>
<div class="telemetry-item">
<div class="telemetry-label">Longitude</div>
<div class="telemetry-value" id="telLon">---.----</div>
</div>
<div class="telemetry-item">
<div class="telemetry-label">Altitude</div>
<div class="telemetry-value" id="telAlt">--- km</div>
</div>
<div class="telemetry-item">
<div class="telemetry-label">Elevation</div>
<div class="telemetry-value" id="telEl">--.-</div>
</div>
<div class="telemetry-item">
<div class="telemetry-label">Azimuth</div>
<div class="telemetry-value" id="telAz">---.-</div>
</div>
<div class="telemetry-item">
<div class="telemetry-label">Distance</div>
<div class="telemetry-value" id="telDist">---- km</div>
</div>
</div>
</div>
</div>
<!-- Pass List -->
<div class="panel pass-list">
<div class="panel-header">
<span>UPCOMING PASSES <span id="passCount" style="color: var(--accent-cyan);"></span></span>
<div class="panel-indicator"></div>
</div>
<div class="panel-content">
<div class="pass-list-content" id="passList">
<div style="text-align:center;color:var(--text-secondary);padding:20px;">
Calculating passes...
</div>
</div>
</div>
</div>
</div>
<!-- Controls Bar -->
<div class="controls-bar">
<div class="control-group">
<span class="control-label">Lat:</span>
<input type="number" id="obsLat" value="51.5074" step="0.0001">
</div>
<div class="control-group">
<span class="control-label">Lon:</span>
<input type="number" id="obsLon" value="-0.1278" step="0.0001">
</div>
<button class="btn" onclick="getLocation()">GPS</button>
<button class="btn primary" onclick="calculatePasses()">CALCULATE</button>
</div>
</main>
<style>
/* Location selector styles */
.location-selector {
display: flex;
align-items: center;
gap: 6px;
margin-right: 15px;
}
.location-label {
font-size: 11px;
color: var(--text-secondary, #8899aa);
font-family: var(--font-mono);
}
.location-select {
background: rgba(0, 40, 60, 0.8);
border: 1px solid rgba(0, 200, 255, 0.3);
color: #e0f7ff;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-family: var(--font-mono);
cursor: pointer;
min-width: 140px;
}
.location-select:focus {
outline: none;
border-color: #00d4ff;
}
.location-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.location-status-dot.online {
background: #00ff88;
box-shadow: 0 0 6px #00ff88;
}
.location-status-dot.offline {
background: #ff4444;
box-shadow: 0 0 6px #ff4444;
}
</style>
<script>
// Check if embedded mode
const urlParams = new URLSearchParams(window.location.search);
const isEmbedded = urlParams.get('embedded') === 'true';
// Dashboard state
let passes = [];
let selectedPass = null;
let groundMap = null;
let satMarker = null;
let trackLine = null;
let observerMarker = null;
let orbitTrack = null;
let selectedSatellite = 25544;
let currentLocationSource = 'local';
let agents = [];
const satellites = {
25544: { name: 'ISS (ZARYA)', color: '#00ffff' },
25338: { name: 'NOAA 15', color: '#00ff00' },
28654: { name: 'NOAA 18', color: '#ff6600' },
33591: { name: 'NOAA 19', color: '#ff3366' },
40069: { name: 'METEOR-M2', color: '#9370DB' },
43013: { name: 'NOAA 20', color: '#00ffaa' },
54234: { name: 'METEOR-M2-3', color: '#ff00ff' }
};
function onSatelliteChange() {
const select = document.getElementById('satSelect');
selectedSatellite = parseInt(select.value);
const satName = satellites[selectedSatellite]?.name || 'Unknown';
document.getElementById('trackingStatus').textContent = 'ACQUIRING';
document.getElementById('trackingDot').style.background = 'var(--accent-orange)';
selectedPass = null;
passes = [];
if (groundMap) {
if (trackLine) { groundMap.removeLayer(trackLine); trackLine = null; }
if (satMarker) { groundMap.removeLayer(satMarker); satMarker = null; }
if (orbitTrack) { groundMap.removeLayer(orbitTrack); orbitTrack = null; }
}
calculatePasses();
}
function setupEmbeddedMode() {
if (isEmbedded) {
// Hide back link when embedded
const backLink = document.querySelector('.back-link');
if (backLink) backLink.style.display = 'none';
// Add embedded class to body for CSS adjustments
document.body.classList.add('embedded');
// Compact the header slightly
const header = document.querySelector('.header');
if (header) header.style.padding = '10px 20px';
// Hide decorative elements
const gridBg = document.querySelector('.grid-bg');
const scanline = document.querySelector('.scanline');
if (gridBg) gridBg.style.display = 'none';
if (scanline) scanline.style.display = 'none';
}
}
function applySharedObserverLocation() {
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
const shared = ObserverLocation.getShared();
if (shared) {
const latInput = document.getElementById('obsLat');
const lonInput = document.getElementById('obsLon');
if (latInput) latInput.value = shared.lat.toFixed(4);
if (lonInput) lonInput.value = shared.lon.toFixed(4);
return true;
}
}
return false;
}
document.addEventListener('DOMContentLoaded', () => {
setupEmbeddedMode();
const usedShared = applySharedObserverLocation();
initGroundMap();
updateClock();
setInterval(updateClock, 1000);
setInterval(updateCountdown, 1000);
setInterval(updateRealTimePositions, 5000);
loadAgents();
if (!usedShared) {
getLocation();
}
});
async function loadAgents() {
try {
const response = await fetch('/controller/agents');
const data = await response.json();
if (data.status === 'success' && data.agents) {
agents = data.agents;
populateLocationSelector();
}
} catch (err) {
console.log('No agents available (controller not running)');
}
}
function populateLocationSelector() {
const select = document.getElementById('locationSource');
if (!select) return;
// Keep local option, add agents with GPS
agents.forEach(agent => {
const option = document.createElement('option');
option.value = 'agent-' + agent.id;
option.textContent = agent.name;
if (agent.gps_coords) {
option.textContent += ' (GPS)';
}
select.appendChild(option);
});
select.addEventListener('change', onLocationSourceChange);
}
async function onLocationSourceChange() {
const select = document.getElementById('locationSource');
const value = select.value;
currentLocationSource = value;
const statusDot = document.getElementById('locationStatusDot');
if (value === 'local') {
// Use local GPS
statusDot.className = 'location-status-dot online';
getLocation();
} else if (value.startsWith('agent-')) {
// Fetch agent's GPS position
const agentId = value.replace('agent-', '');
try {
statusDot.className = 'location-status-dot online';
const response = await fetch(`/controller/agents/${agentId}/status`);
const data = await response.json();
if (data.status === 'success' && data.result) {
const agentStatus = data.result;
if (agentStatus.gps_position) {
const gps = agentStatus.gps_position;
document.getElementById('obsLat').value = gps.lat.toFixed(4);
document.getElementById('obsLon').value = gps.lon.toFixed(4);
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
ObserverLocation.setShared({ lat: gps.lat, lon: gps.lon });
}
// Update observer marker label
const agent = agents.find(a => a.id == agentId);
if (agent) {
console.log(`Using GPS from agent: ${agent.name} (${gps.lat.toFixed(4)}, ${gps.lon.toFixed(4)})`);
}
calculatePasses();
} else {
alert('Agent does not have GPS data available');
statusDot.className = 'location-status-dot offline';
}
}
} catch (err) {
console.error('Failed to get agent GPS:', err);
statusDot.className = 'location-status-dot offline';
alert('Failed to connect to agent');
}
}
}
function updateClock() {
const now = new Date();
document.getElementById('utcTime').textContent =
now.toISOString().substring(11, 19) + ' UTC';
}
function initGroundMap() {
groundMap = L.map('groundMap', {
center: [20, 0],
zoom: 2,
minZoom: 1,
maxZoom: 10,
worldCopyJump: true
});
// Use settings manager for tile layer (allows runtime changes)
window.groundMap = groundMap;
if (typeof Settings !== 'undefined' && Settings.createTileLayer) {
Settings.createTileLayer().addTo(groundMap);
Settings.registerMap(groundMap);
} else {
L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> &copy; <a href="https://carto.com/">CARTO</a>',
maxZoom: 19,
subdomains: 'abcd'
}).addTo(groundMap);
}
const lat = parseFloat(document.getElementById('obsLat')?.value);
const lon = parseFloat(document.getElementById('obsLon')?.value);
if (!Number.isNaN(lat) && !Number.isNaN(lon)) {
groundMap.setView([lat, lon], 3);
}
}
function getLocation() {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(pos => {
const lat = pos.coords.latitude;
const lon = pos.coords.longitude;
document.getElementById('obsLat').value = lat.toFixed(4);
document.getElementById('obsLon').value = lon.toFixed(4);
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
ObserverLocation.setShared({ lat, lon });
}
calculatePasses();
}, () => {
calculatePasses();
});
} else {
calculatePasses();
}
}
async function calculatePasses() {
const lat = parseFloat(document.getElementById('obsLat').value);
const lon = parseFloat(document.getElementById('obsLon').value);
const satName = satellites[selectedSatellite]?.name || 'Unknown';
try {
const response = await fetch('/satellite/predict', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
latitude: lat,
longitude: lon,
hours: 48,
minEl: 5,
satellites: [selectedSatellite]
})
});
const data = await response.json();
if (data.status === 'success') {
passes = data.passes;
renderPassList();
updateStats();
if (passes.length > 0) {
selectPass(0);
}
updateObserverMarker(lat, lon);
document.getElementById('trackingStatus').textContent = 'TRACKING';
document.getElementById('trackingDot').style.background = 'var(--accent-green)';
} else {
document.getElementById('trackingStatus').textContent = 'ERROR';
document.getElementById('trackingDot').style.background = 'var(--accent-red)';
}
} catch (err) {
console.error('Pass calculation error:', err);
document.getElementById('trackingStatus').textContent = 'OFFLINE';
document.getElementById('trackingDot').style.background = 'var(--accent-red)';
}
}
function renderPassList() {
const container = document.getElementById('passList');
const countEl = document.getElementById('passCount');
if (passes.length === 0) {
container.innerHTML = '<div style="text-align:center;color:var(--text-secondary);padding:20px;">No passes found</div>';
if (countEl) countEl.textContent = '';
return;
}
if (countEl) countEl.textContent = `(${passes.length})`;
container.innerHTML = passes.slice(0, 10).map((pass, idx) => {
const quality = pass.maxEl >= 60 ? 'excellent' : pass.maxEl >= 30 ? 'good' : 'fair';
const qualityText = pass.maxEl >= 60 ? 'EXCELLENT' : pass.maxEl >= 30 ? 'GOOD' : 'FAIR';
const time = pass.startTime.split(' ')[1] || pass.startTime;
return `
<div class="pass-item ${selectedPass === idx ? 'active' : ''}" onclick="selectPass(${idx})">
<div class="pass-item-header">
<span class="pass-sat-name">${pass.satellite}</span>
<span class="pass-quality ${quality}">${qualityText}</span>
</div>
<div class="pass-item-details">
<span class="pass-time">${time}</span>
<span>${pass.maxEl.toFixed(0)}° · ${pass.duration} min</span>
</div>
</div>
`;
}).join('');
}
function selectPass(idx) {
selectedPass = idx;
renderPassList();
const pass = passes[idx];
if (!pass) return;
drawPolarPlot(pass);
updateGroundTrack(pass);
updateTelemetry(pass);
updateRealTimePositions(true);
}
function drawPolarPlot(pass) {
const canvas = document.getElementById('polarPlot');
const ctx = canvas.getContext('2d');
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height - 20;
const cx = canvas.width / 2;
const cy = canvas.height / 2;
const radius = Math.max(10, Math.min(cx, cy) - 40);
ctx.fillStyle = '#0a0a0f';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Elevation rings
ctx.strokeStyle = 'rgba(0, 212, 255, 0.15)';
ctx.lineWidth = 1;
for (let el = 30; el <= 90; el += 30) {
const r = radius * (1 - el / 90);
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.stroke();
ctx.fillStyle = 'rgba(0, 212, 255, 0.4)';
ctx.font = '10px JetBrains Mono';
ctx.fillText(el + '°', cx + 5, cy - r + 12);
}
// Horizon
ctx.strokeStyle = 'rgba(0, 212, 255, 0.3)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.stroke();
// Cardinal lines
ctx.strokeStyle = 'rgba(0, 212, 255, 0.1)';
ctx.lineWidth = 1;
for (let az = 0; az < 360; az += 45) {
const angle = (az - 90) * Math.PI / 180;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + radius * Math.cos(angle), cy + radius * Math.sin(angle));
ctx.stroke();
}
// Cardinal labels
ctx.font = 'bold 14px Orbitron';
const labels = [
{ text: 'N', az: 0, color: '#ff4444' },
{ text: 'E', az: 90, color: '#00d4ff' },
{ text: 'S', az: 180, color: '#00d4ff' },
{ text: 'W', az: 270, color: '#00d4ff' }
];
labels.forEach(l => {
const angle = (l.az - 90) * Math.PI / 180;
const x = cx + (radius + 20) * Math.cos(angle);
const y = cy + (radius + 20) * Math.sin(angle);
ctx.fillStyle = l.color;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(l.text, x, y);
});
// Pass trajectory
if (pass && pass.trajectory) {
ctx.strokeStyle = pass.color || '#00d4ff';
ctx.lineWidth = 3;
ctx.setLineDash([8, 4]);
ctx.beginPath();
let maxElPoint = null;
let maxEl = 0;
pass.trajectory.forEach((pt, i) => {
const r = radius * (1 - pt.el / 90);
const angle = (pt.az - 90) * Math.PI / 180;
const x = cx + r * Math.cos(angle);
const y = cy + r * Math.sin(angle);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
if (pt.el > maxEl) {
maxEl = pt.el;
maxElPoint = { x, y };
}
});
ctx.stroke();
ctx.setLineDash([]);
if (maxElPoint) {
ctx.beginPath();
ctx.arc(maxElPoint.x, maxElPoint.y, 8, 0, Math.PI * 2);
ctx.fillStyle = pass.color || '#00d4ff';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
}
}
}
function updateGroundTrack(pass) {
if (!groundMap) return;
if (trackLine) { groundMap.removeLayer(trackLine); trackLine = null; }
if (satMarker) { groundMap.removeLayer(satMarker); satMarker = null; }
if (orbitTrack) { groundMap.removeLayer(orbitTrack); orbitTrack = null; }
if (pass && pass.groundTrack) {
const segments = [];
let currentSegment = [];
for (let i = 0; i < pass.groundTrack.length; i++) {
const p = pass.groundTrack[i];
if (currentSegment.length > 0) {
const prevLon = currentSegment[currentSegment.length - 1][1];
const crossesAntimeridian = (prevLon > 90 && p.lon < -90) || (prevLon < -90 && p.lon > 90);
if (crossesAntimeridian) {
if (currentSegment.length >= 1) segments.push(currentSegment);
currentSegment = [];
}
}
currentSegment.push([p.lat, p.lon]);
}
if (currentSegment.length >= 1) segments.push(currentSegment);
trackLine = L.layerGroup();
const allCoords = [];
segments.forEach(seg => {
L.polyline(seg, {
color: pass.color || '#00d4ff',
weight: 4,
opacity: 1.0
}).addTo(trackLine);
allCoords.push(...seg);
});
trackLine.addTo(groundMap);
if (pass.currentPos) {
const satIcon = L.divIcon({
className: 'sat-marker',
html: `<div style="width: 16px; height: 16px; background: ${pass.color || '#00d4ff'}; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 20px ${pass.color || '#00d4ff'};"></div>`,
iconSize: [16, 16],
iconAnchor: [8, 8]
});
satMarker = L.marker([pass.currentPos.lat, pass.currentPos.lon], { icon: satIcon })
.addTo(groundMap)
.bindPopup(`<b>${pass.name}</b><br>Alt: ${pass.currentPos.alt?.toFixed(0)} km`);
}
if (allCoords.length > 0) {
groundMap.fitBounds(L.latLngBounds(allCoords), { padding: [30, 30] });
}
}
}
function updateObserverMarker(lat, lon) {
if (!groundMap) return;
if (observerMarker) groundMap.removeLayer(observerMarker);
// Determine location label
let locationLabel = 'Local Observer';
if (currentLocationSource && currentLocationSource.startsWith('agent-')) {
const agentId = currentLocationSource.replace('agent-', '');
const agent = agents.find(a => a.id == agentId);
if (agent) {
locationLabel = agent.name;
}
}
const obsIcon = L.divIcon({
className: 'obs-marker',
html: `<div style="width: 12px; height: 12px; background: #ff9500; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 15px #ff9500;"></div>`,
iconSize: [12, 12],
iconAnchor: [6, 6]
});
observerMarker = L.marker([lat, lon], { icon: obsIcon })
.addTo(groundMap)
.bindPopup(`<b>${locationLabel}</b><br>${lat.toFixed(4)}°, ${lon.toFixed(4)}°`);
}
function updateStats() {
document.getElementById('statTracked').textContent = Object.keys(satellites).length;
document.getElementById('statPasses').textContent = passes.length;
const maxEl = passes.reduce((max, p) => Math.max(max, p.maxEl || 0), 0);
document.getElementById('statMaxEl').textContent = maxEl.toFixed(0) + '°';
}
function updateTelemetry(pass) {
if (!pass || !pass.currentPos) {
document.getElementById('telLat').textContent = '---.----';
document.getElementById('telLon').textContent = '---.----';
document.getElementById('telAlt').textContent = '--- km';
document.getElementById('telEl').textContent = '--.-';
document.getElementById('telAz').textContent = '---.-';
document.getElementById('telDist').textContent = '---- km';
return;
}
const pos = pass.currentPos;
document.getElementById('telLat').textContent = (pos.lat || 0).toFixed(4) + '°';
document.getElementById('telLon').textContent = (pos.lon || 0).toFixed(4) + '°';
document.getElementById('telAlt').textContent = (pos.alt || 0).toFixed(0) + ' km';
document.getElementById('telEl').textContent = (pos.el || 0).toFixed(1) + '°';
document.getElementById('telAz').textContent = (pos.az || 0).toFixed(1) + '°';
document.getElementById('telDist').textContent = (pos.dist || 0).toFixed(0) + ' km';
}
function updateCountdown() {
if (!passes || passes.length === 0) {
document.getElementById('countdownSat').textContent = 'NO PASSES FOUND';
document.getElementById('countDays').textContent = '--';
document.getElementById('countHours').textContent = '--';
document.getElementById('countMins').textContent = '--';
document.getElementById('countSecs').textContent = '--';
return;
}
const now = new Date();
let nextPass = null;
for (const pass of passes) {
const start = new Date(pass.startTimeISO);
if (start > now) {
nextPass = pass;
break;
}
}
if (!nextPass) nextPass = passes[0];
document.getElementById('countdownSat').textContent = nextPass.satellite;
const passTime = new Date(nextPass.startTimeISO);
const diff = passTime - now;
if (diff <= 0) {
document.getElementById('countDays').textContent = '00';
document.getElementById('countHours').textContent = '00';
document.getElementById('countMins').textContent = '00';
document.getElementById('countSecs').textContent = '00';
return;
}
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const mins = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const secs = Math.floor((diff % (1000 * 60)) / 1000);
document.getElementById('countDays').textContent = days.toString().padStart(2, '0');
document.getElementById('countHours').textContent = hours.toString().padStart(2, '0');
document.getElementById('countMins').textContent = mins.toString().padStart(2, '0');
document.getElementById('countSecs').textContent = secs.toString().padStart(2, '0');
const elements = ['countDays', 'countHours', 'countMins', 'countSecs'].map(id => document.getElementById(id));
if (diff < 60000) {
elements.forEach(el => el.classList.add('active'));
} else {
elements.forEach(el => el.classList.remove('active'));
}
}
async function updateRealTimePositions(fitBoundsToOrbit = false) {
const lat = parseFloat(document.getElementById('obsLat').value);
const lon = parseFloat(document.getElementById('obsLon').value);
let targetSatellite = selectedSatellite;
let satColor = satellites[selectedSatellite]?.color || '#00d4ff';
if (selectedPass !== null && passes[selectedPass]) {
const pass = passes[selectedPass];
targetSatellite = pass.satellite;
satColor = pass.color || satColor;
}
try {
const response = await fetch('/satellite/position', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
latitude: lat,
longitude: lon,
satellites: [targetSatellite],
includeTrack: true
})
});
const data = await response.json();
if (data.status === 'success' && data.positions.length > 0) {
const pos = data.positions[0];
document.getElementById('telLat').textContent = pos.lat.toFixed(4) + '°';
document.getElementById('telLon').textContent = pos.lon.toFixed(4) + '°';
document.getElementById('telAlt').textContent = pos.altitude.toFixed(0) + ' km';
document.getElementById('telEl').textContent = pos.elevation.toFixed(1) + '°';
document.getElementById('telAz').textContent = pos.azimuth.toFixed(1) + '°';
document.getElementById('telDist').textContent = pos.distance.toFixed(0) + ' km';
document.getElementById('statVisible').textContent = pos.elevation > 0 ? '1' : '0';
if (groundMap) {
if (satMarker) groundMap.removeLayer(satMarker);
const satIcon = L.divIcon({
className: 'sat-marker-live',
html: `<div style="width: 20px; height: 20px; background: ${satColor}; border-radius: 50%; border: 3px solid #fff; box-shadow: 0 0 20px ${satColor}, 0 0 40px ${satColor};"></div>`,
iconSize: [20, 20],
iconAnchor: [10, 10]
});
satMarker = L.marker([pos.lat, pos.lon], { icon: satIcon }).addTo(groundMap);
}
if (pos.track && groundMap) {
if (orbitTrack) groundMap.removeLayer(orbitTrack);
const segments = [];
let currentSegment = [];
for (let i = 0; i < pos.track.length; i++) {
const p = pos.track[i];
if (currentSegment.length > 0) {
const prevLon = currentSegment[currentSegment.length - 1][1];
const crossesAntimeridian = (prevLon > 90 && p.lon < -90) || (prevLon < -90 && p.lon > 90);
if (crossesAntimeridian) {
if (currentSegment.length >= 1) segments.push(currentSegment);
currentSegment = [];
}
}
currentSegment.push([p.lat, p.lon]);
}
if (currentSegment.length >= 1) segments.push(currentSegment);
orbitTrack = L.layerGroup();
const allOrbitCoords = [];
segments.forEach(seg => {
L.polyline(seg, {
color: satColor,
weight: 2,
opacity: 0.6,
dashArray: '5, 5'
}).addTo(orbitTrack);
allOrbitCoords.push(...seg);
});
orbitTrack.addTo(groundMap);
if (fitBoundsToOrbit && allOrbitCoords.length > 0) {
allOrbitCoords.push([lat, lon]);
groundMap.fitBounds(L.latLngBounds(allOrbitCoords), { padding: [30, 30] });
}
}
if (selectedPass !== null && passes[selectedPass]) {
drawPolarPlot(passes[selectedPass]);
drawCurrentPositionOnPolar(pos.azimuth, pos.elevation, satColor);
} else {
drawPolarPlotWithPosition(pos.azimuth, pos.elevation, satColor);
}
}
} catch (err) {
console.error('Position update error:', err);
}
}
function drawPolarPlotWithPosition(az, el, color) {
const canvas = document.getElementById('polarPlot');
const ctx = canvas.getContext('2d');
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height - 20;
const cx = canvas.width / 2;
const cy = canvas.height / 2;
const radius = Math.max(10, Math.min(cx, cy) - 40);
ctx.fillStyle = '#0a0a0f';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = 'rgba(0, 212, 255, 0.15)';
ctx.lineWidth = 1;
for (let elRing = 30; elRing <= 90; elRing += 30) {
const r = radius * (1 - elRing / 90);
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.stroke();
ctx.fillStyle = 'rgba(0, 212, 255, 0.4)';
ctx.font = '10px JetBrains Mono';
ctx.fillText(elRing + '°', cx + 5, cy - r + 12);
}
ctx.strokeStyle = 'rgba(0, 212, 255, 0.3)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.stroke();
ctx.strokeStyle = 'rgba(0, 212, 255, 0.1)';
ctx.lineWidth = 1;
for (let azLine = 0; azLine < 360; azLine += 45) {
const angle = (azLine - 90) * Math.PI / 180;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + radius * Math.cos(angle), cy + radius * Math.sin(angle));
ctx.stroke();
}
ctx.font = 'bold 14px Orbitron';
const labels = [
{ text: 'N', az: 0, color: '#ff4444' },
{ text: 'E', az: 90, color: '#00d4ff' },
{ text: 'S', az: 180, color: '#00d4ff' },
{ text: 'W', az: 270, color: '#00d4ff' }
];
labels.forEach(l => {
const angle = (l.az - 90) * Math.PI / 180;
const x = cx + (radius + 20) * Math.cos(angle);
const y = cy + (radius + 20) * Math.sin(angle);
ctx.fillStyle = l.color;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(l.text, x, y);
});
if (el > -5) {
const posEl = Math.max(0, el);
const r = radius * (1 - posEl / 90);
const angle = (az - 90) * Math.PI / 180;
const x = cx + r * Math.cos(angle);
const y = cy + r * Math.sin(angle);
const gradient = ctx.createRadialGradient(x, y, 0, x, y, 25);
gradient.addColorStop(0, color);
gradient.addColorStop(1, 'transparent');
ctx.fillStyle = gradient;
ctx.globalAlpha = 0.4;
ctx.beginPath();
ctx.arc(x, y, 25, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.arc(x, y, 10, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 3;
ctx.stroke();
ctx.font = 'bold 11px Orbitron';
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.fillText(satellites[selectedSatellite]?.name || 'SAT', x, y - 20);
ctx.font = '10px JetBrains Mono';
ctx.fillStyle = el > 0 ? '#00ff88' : '#ff4444';
ctx.fillText(el.toFixed(1) + '°', x, y + 25);
} else {
ctx.font = '12px Rajdhani';
ctx.fillStyle = '#ff4444';
ctx.textAlign = 'center';
ctx.fillText('BELOW HORIZON', cx, cy + radius + 35);
}
}
function drawCurrentPositionOnPolar(az, el, color) {
const canvas = document.getElementById('polarPlot');
const ctx = canvas.getContext('2d');
const cx = canvas.width / 2;
const cy = canvas.height / 2;
const radius = Math.max(10, Math.min(cx, cy) - 40);
if (el > -5) {
const posEl = Math.max(0, el);
const r = radius * (1 - posEl / 90);
const angle = (az - 90) * Math.PI / 180;
const x = cx + r * Math.cos(angle);
const y = cy + r * Math.sin(angle);
const gradient = ctx.createRadialGradient(x, y, 0, x, y, 25);
gradient.addColorStop(0, color);
gradient.addColorStop(1, 'transparent');
ctx.fillStyle = gradient;
ctx.globalAlpha = 0.4;
ctx.beginPath();
ctx.arc(x, y, 25, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.arc(x, y, 10, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 3;
ctx.stroke();
ctx.font = 'bold 11px Orbitron';
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.fillText(satellites[selectedSatellite]?.name || 'SAT', x, y - 20);
ctx.font = '10px JetBrains Mono';
ctx.fillStyle = el > 0 ? '#00ff88' : '#ff4444';
ctx.fillText(el.toFixed(1) + '°', x, y + 25);
}
}
</script>
<script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script>
</body>
</html>