mirror of
https://github.com/smittix/intercept.git
synced 2026-06-20 19:28:26 -07:00
Merge main into misc-fixes and address PR #202 review
Sync with upstream main and fix required items from review: - updateTimelineLabels() now uses InterceptTime API (getTimezone/getIANA) instead of the stale selectedTimezone/TZ_MAP globals that were removed during the earlier InterceptTime refactor — fixes ReferenceError on TZ change and pass refresh. - Remove profiles: [basic] from the intercept service in docker-compose.yml so bare `docker compose up -d` still starts the main service. Profile-gated services (intercept-history, adsb_db) stay as-is.
This commit is contained in:
+139
-111
@@ -36,6 +36,8 @@ const BluetoothMode = (function() {
|
||||
|
||||
// Device list filter
|
||||
let currentDeviceFilter = 'all';
|
||||
let sortBy = 'rssi';
|
||||
let sortListenersBound = false;
|
||||
let currentSearchTerm = '';
|
||||
let visibleDeviceCount = 0;
|
||||
let pendingDeviceFlush = false;
|
||||
@@ -118,6 +120,7 @@ const BluetoothMode = (function() {
|
||||
|
||||
// Initialize device list filters
|
||||
initDeviceFilters();
|
||||
initSortControls();
|
||||
initListInteractions();
|
||||
|
||||
// Set initial panel states
|
||||
@@ -129,7 +132,7 @@ const BluetoothMode = (function() {
|
||||
*/
|
||||
function initDeviceFilters() {
|
||||
if (filterListenersBound) return;
|
||||
const filterContainer = document.getElementById('btDeviceFilters');
|
||||
const filterContainer = document.getElementById('btFilterGroup');
|
||||
if (filterContainer) {
|
||||
filterContainer.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.bt-filter-btn');
|
||||
@@ -158,17 +161,27 @@ const BluetoothMode = (function() {
|
||||
filterListenersBound = true;
|
||||
}
|
||||
|
||||
function initSortControls() {
|
||||
if (sortListenersBound) return;
|
||||
sortListenersBound = true;
|
||||
const sortGroup = document.getElementById('btSortGroup');
|
||||
if (!sortGroup) return;
|
||||
sortGroup.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.bt-sort-btn');
|
||||
if (!btn) return;
|
||||
const sort = btn.dataset.sort;
|
||||
if (!sort) return;
|
||||
sortBy = sort;
|
||||
sortGroup.querySelectorAll('.bt-sort-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
renderAllDevices();
|
||||
});
|
||||
}
|
||||
|
||||
function initListInteractions() {
|
||||
if (listListenersBound) return;
|
||||
if (deviceContainer) {
|
||||
deviceContainer.addEventListener('click', (event) => {
|
||||
const locateBtn = event.target.closest('.bt-locate-btn[data-locate-id]');
|
||||
if (locateBtn) {
|
||||
event.preventDefault();
|
||||
locateById(locateBtn.dataset.locateId);
|
||||
return;
|
||||
}
|
||||
|
||||
const row = event.target.closest('.bt-device-row[data-bt-device-id]');
|
||||
if (!row) return;
|
||||
selectDevice(row.dataset.btDeviceId);
|
||||
@@ -1008,6 +1021,15 @@ const BluetoothMode = (function() {
|
||||
const statusText = document.getElementById('statusText');
|
||||
if (statusDot) statusDot.classList.toggle('running', scanning);
|
||||
if (statusText) statusText.textContent = scanning ? 'Scanning...' : 'Idle';
|
||||
|
||||
// Drive the per-panel scan indicator
|
||||
const scanDot = document.getElementById('btScanIndicator')?.querySelector('.bt-scan-dot');
|
||||
const scanText = document.getElementById('btScanIndicator')?.querySelector('.bt-scan-text');
|
||||
if (scanDot) scanDot.style.display = scanning ? 'inline-block' : 'none';
|
||||
if (scanText) {
|
||||
scanText.textContent = scanning ? 'SCANNING' : 'IDLE';
|
||||
scanText.classList.toggle('active', scanning);
|
||||
}
|
||||
}
|
||||
|
||||
function resetStats() {
|
||||
@@ -1366,10 +1388,30 @@ const BluetoothMode = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-render all devices in the current sort order, then re-apply the active filter.
|
||||
*/
|
||||
function renderAllDevices() {
|
||||
if (!deviceContainer) return;
|
||||
if (devices.size === 0) return;
|
||||
deviceContainer.innerHTML = '';
|
||||
|
||||
const sorted = [...devices.values()].sort((a, b) => {
|
||||
if (sortBy === 'rssi') return (b.rssi_current ?? -100) - (a.rssi_current ?? -100);
|
||||
if (sortBy === 'name') return (a.name || '\uFFFF').localeCompare(b.name || '\uFFFF');
|
||||
if (sortBy === 'seen') return (b.seen_count || 0) - (a.seen_count || 0);
|
||||
if (sortBy === 'distance') return (a.estimated_distance_m ?? 9999) - (b.estimated_distance_m ?? 9999);
|
||||
return 0;
|
||||
});
|
||||
|
||||
sorted.forEach(device => renderDevice(device, false));
|
||||
applyDeviceFilter();
|
||||
if (selectedDeviceId) highlightSelectedDevice(selectedDeviceId);
|
||||
}
|
||||
|
||||
function createSimpleDeviceCard(device) {
|
||||
const protocol = device.protocol || 'ble';
|
||||
const rssi = device.rssi_current;
|
||||
const rssiColor = getRssiColor(rssi);
|
||||
const inBaseline = device.in_baseline || false;
|
||||
const isNew = !inBaseline;
|
||||
const hasName = !!device.name;
|
||||
@@ -1380,58 +1422,69 @@ const BluetoothMode = (function() {
|
||||
const agentName = device._agent || 'Local';
|
||||
const seenBefore = device.seen_before === true;
|
||||
|
||||
// Calculate RSSI bar width (0-100%)
|
||||
// RSSI typically ranges from -100 (weak) to -30 (very strong)
|
||||
// Signal bar
|
||||
const rssiPercent = rssi != null ? Math.max(0, Math.min(100, ((rssi + 100) / 70) * 100)) : 0;
|
||||
const fillClass = rssi == null ? 'weak'
|
||||
: rssi >= -60 ? 'strong'
|
||||
: rssi >= -75 ? 'medium' : 'weak';
|
||||
|
||||
const displayName = device.name || formatDeviceId(device.address);
|
||||
const name = escapeHtml(displayName);
|
||||
const addr = escapeHtml(isUuidAddress(device) ? formatAddress(device) : (device.address || 'Unknown'));
|
||||
const mfr = device.manufacturer_name ? escapeHtml(device.manufacturer_name) : '';
|
||||
const seenCount = device.seen_count || 0;
|
||||
const searchIndex = [
|
||||
displayName,
|
||||
device.address,
|
||||
device.manufacturer_name,
|
||||
device.tracker_name,
|
||||
device.tracker_type,
|
||||
agentName
|
||||
displayName, device.address, device.manufacturer_name,
|
||||
device.tracker_name, device.tracker_type, agentName
|
||||
].filter(Boolean).join(' ').toLowerCase();
|
||||
|
||||
// Protocol badge - compact
|
||||
// Protocol badge
|
||||
const protoBadge = protocol === 'ble'
|
||||
? '<span class="bt-proto-badge ble">BLE</span>'
|
||||
: '<span class="bt-proto-badge classic">CLASSIC</span>';
|
||||
|
||||
// Tracker badge - show if device is detected as tracker
|
||||
// Tracker badge
|
||||
let trackerBadge = '';
|
||||
if (isTracker) {
|
||||
const confColor = trackerConfidence === 'high' ? '#ef4444' :
|
||||
trackerConfidence === 'medium' ? '#f97316' : '#eab308';
|
||||
const confBg = trackerConfidence === 'high' ? 'rgba(239,68,68,0.15)' :
|
||||
trackerConfidence === 'medium' ? 'rgba(249,115,22,0.15)' : 'rgba(234,179,8,0.15)';
|
||||
const typeLabel = trackerType === 'airtag' ? 'AirTag' :
|
||||
trackerType === 'tile' ? 'Tile' :
|
||||
trackerType === 'samsung_smarttag' ? 'SmartTag' :
|
||||
trackerType === 'findmy_accessory' ? 'FindMy' :
|
||||
trackerType === 'chipolo' ? 'Chipolo' : 'TRACKER';
|
||||
trackerBadge = '<span class="bt-tracker-badge" style="background:' + confBg + ';color:' + confColor + ';font-size:9px;padding:1px 4px;border-radius:3px;margin-left:4px;font-weight:600;">' + typeLabel + '</span>';
|
||||
const confColor = trackerConfidence === 'high' ? '#ef4444'
|
||||
: trackerConfidence === 'medium' ? '#f97316' : '#eab308';
|
||||
const confBg = trackerConfidence === 'high' ? 'rgba(239,68,68,0.15)'
|
||||
: trackerConfidence === 'medium' ? 'rgba(249,115,22,0.15)' : 'rgba(234,179,8,0.15)';
|
||||
const typeLabel = trackerType === 'airtag' ? 'AirTag'
|
||||
: trackerType === 'tile' ? 'Tile'
|
||||
: trackerType === 'samsung_smarttag' ? 'SmartTag'
|
||||
: trackerType === 'findmy_accessory' ? 'FindMy'
|
||||
: trackerType === 'chipolo' ? 'Chipolo' : 'TRACKER';
|
||||
trackerBadge = '<span class="bt-tracker-badge" style="background:' + confBg + ';color:' + confColor
|
||||
+ ';font-size:9px;padding:1px 5px;border-radius:3px;font-weight:600;">' + typeLabel + '</span>';
|
||||
}
|
||||
|
||||
// IRK badge - show if paired IRK is available
|
||||
let irkBadge = '';
|
||||
if (device.has_irk) {
|
||||
irkBadge = '<span class="bt-irk-badge">IRK</span>';
|
||||
}
|
||||
// IRK badge
|
||||
const irkBadge = device.has_irk ? '<span class="bt-irk-badge">IRK</span>' : '';
|
||||
|
||||
// Risk badge - show if risk score is significant
|
||||
// Risk badge
|
||||
let riskBadge = '';
|
||||
if (riskScore >= 0.3) {
|
||||
const riskColor = riskScore >= 0.5 ? '#ef4444' : '#f97316';
|
||||
riskBadge = '<span class="bt-risk-badge" style="color:' + riskColor + ';font-size:8px;margin-left:4px;font-weight:600;">' + Math.round(riskScore * 100) + '% RISK</span>';
|
||||
riskBadge = '<span class="bt-risk-badge" style="color:' + riskColor
|
||||
+ ';font-size:8px;font-weight:600;">' + Math.round(riskScore * 100) + '% RISK</span>';
|
||||
}
|
||||
|
||||
// Status indicator
|
||||
// MAC cluster badge
|
||||
const clusterBadge = device.mac_cluster_count > 1
|
||||
? '<span class="bt-mac-cluster-badge">' + device.mac_cluster_count + ' MACs</span>'
|
||||
: '';
|
||||
|
||||
// Flag badges (top-right, before status dot)
|
||||
const hFlags = device.heuristic_flags || [];
|
||||
let flagBadges = '';
|
||||
if (device.is_persistent || hFlags.includes('persistent'))
|
||||
flagBadges += '<span class="bt-flag-badge persistent">PERSIST</span>';
|
||||
if (device.is_beacon_like || hFlags.includes('beacon_like'))
|
||||
flagBadges += '<span class="bt-flag-badge beacon-like">BEACON</span>';
|
||||
if (device.is_strong_stable || hFlags.includes('strong_stable'))
|
||||
flagBadges += '<span class="bt-flag-badge strong-stable">STABLE</span>';
|
||||
|
||||
// Status dot
|
||||
let statusDot;
|
||||
if (isTracker && trackerConfidence === 'high') {
|
||||
statusDot = '<span class="bt-status-dot tracker" style="background:#ef4444;"></span>';
|
||||
@@ -1441,74 +1494,55 @@ const BluetoothMode = (function() {
|
||||
statusDot = '<span class="bt-status-dot known"></span>';
|
||||
}
|
||||
|
||||
// Distance display
|
||||
// Bottom meta
|
||||
const metaLabel = mfr || addr; // already HTML-escaped
|
||||
const distM = device.estimated_distance_m;
|
||||
let distStr = '';
|
||||
if (distM != null) {
|
||||
distStr = '~' + distM.toFixed(1) + 'm';
|
||||
}
|
||||
const distStr = distM != null ? '~' + distM.toFixed(1) + 'm' : '';
|
||||
let metaHtml = '<span>' + metaLabel + '</span>';
|
||||
if (distStr) metaHtml += '<span>' + distStr + '</span>';
|
||||
metaHtml += '<span class="bt-row-rssi ' + fillClass + '">' + (rssi != null ? rssi : '—') + '</span>';
|
||||
if (seenBefore) metaHtml += '<span class="bt-history-badge">SEEN</span>';
|
||||
if (agentName !== 'Local')
|
||||
metaHtml += '<span class="agent-badge agent-remote" style="font-size:8px;padding:1px 4px;">'
|
||||
+ escapeHtml(agentName) + '</span>';
|
||||
|
||||
// Behavioral flag badges
|
||||
const hFlags = device.heuristic_flags || [];
|
||||
let flagBadges = '';
|
||||
if (device.is_persistent || hFlags.includes('persistent')) {
|
||||
flagBadges += '<span class="bt-flag-badge persistent">PERSIST</span>';
|
||||
}
|
||||
if (device.is_beacon_like || hFlags.includes('beacon_like')) {
|
||||
flagBadges += '<span class="bt-flag-badge beacon-like">BEACON</span>';
|
||||
}
|
||||
if (device.is_strong_stable || hFlags.includes('strong_stable')) {
|
||||
flagBadges += '<span class="bt-flag-badge strong-stable">STABLE</span>';
|
||||
}
|
||||
// Left border colour
|
||||
const borderColor = isTracker && trackerConfidence === 'high' ? '#ef4444'
|
||||
: isTracker ? '#f97316'
|
||||
: rssi != null && rssi >= -60 ? 'var(--accent-green)'
|
||||
: rssi != null && rssi >= -75 ? 'var(--accent-amber, #eab308)'
|
||||
: 'var(--accent-red)';
|
||||
|
||||
// MAC cluster badge
|
||||
let clusterBadge = '';
|
||||
if (device.mac_cluster_count > 1) {
|
||||
clusterBadge = '<span class="bt-mac-cluster-badge">' + device.mac_cluster_count + ' MACs</span>';
|
||||
}
|
||||
|
||||
// Build secondary info line
|
||||
let secondaryParts = [addr];
|
||||
if (mfr) secondaryParts.push(mfr);
|
||||
if (distStr) secondaryParts.push(distStr);
|
||||
secondaryParts.push('Seen ' + seenCount + '×');
|
||||
if (seenBefore) secondaryParts.push('<span class="bt-history-badge">SEEN BEFORE</span>');
|
||||
// Add agent name if not Local
|
||||
if (agentName !== 'Local') {
|
||||
secondaryParts.push('<span class="agent-badge agent-remote" style="font-size:8px;padding:1px 4px;">' + escapeHtml(agentName) + '</span>');
|
||||
}
|
||||
const secondaryInfo = secondaryParts.join(' · ');
|
||||
|
||||
// Row border color - highlight trackers in red/orange
|
||||
const borderColor = isTracker && trackerConfidence === 'high' ? '#ef4444' :
|
||||
isTracker ? '#f97316' : rssiColor;
|
||||
|
||||
return '<div class="bt-device-row' + (isTracker ? ' is-tracker' : '') + '" data-bt-device-id="' + escapeAttr(device.device_id) + '" data-is-new="' + isNew + '" data-has-name="' + hasName + '" data-rssi="' + (rssi || -100) + '" data-is-tracker="' + isTracker + '" data-search="' + escapeAttr(searchIndex) + '" role="button" tabindex="0" data-keyboard-activate="true" style="border-left-color:' + borderColor + ';">' +
|
||||
'<div class="bt-row-main">' +
|
||||
'<div class="bt-row-left">' +
|
||||
protoBadge +
|
||||
'<span class="bt-device-name">' + name + '</span>' +
|
||||
trackerBadge +
|
||||
irkBadge +
|
||||
riskBadge +
|
||||
flagBadges +
|
||||
clusterBadge +
|
||||
'</div>' +
|
||||
'<div class="bt-row-right">' +
|
||||
'<div class="bt-rssi-container">' +
|
||||
'<div class="bt-rssi-bar-bg"><div class="bt-rssi-bar" style="width:' + rssiPercent + '%;background:' + rssiColor + ';"></div></div>' +
|
||||
'<span class="bt-rssi-value" style="color:' + rssiColor + ';">' + (rssi != null ? rssi : '--') + '</span>' +
|
||||
'</div>' +
|
||||
statusDot +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="bt-row-secondary">' + secondaryInfo + '</div>' +
|
||||
'<div class="bt-row-actions">' +
|
||||
'<button type="button" class="bt-locate-btn" data-locate-id="' + escapeAttr(device.device_id) + '">' +
|
||||
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg>' +
|
||||
'Locate</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
return '<div class="bt-device-row' + (isTracker ? ' is-tracker' : '') + '"'
|
||||
+ ' data-bt-device-id="' + escapeAttr(device.device_id) + '"'
|
||||
+ ' data-is-new="' + isNew + '"'
|
||||
+ ' data-has-name="' + hasName + '"'
|
||||
+ ' data-rssi="' + (rssi ?? -100) + '"'
|
||||
+ ' data-is-tracker="' + isTracker + '"'
|
||||
+ ' data-search="' + escapeAttr(searchIndex) + '"'
|
||||
+ ' role="button" tabindex="0" data-keyboard-activate="true"'
|
||||
+ ' style="border-left-color:' + borderColor + ';">'
|
||||
// Top line
|
||||
+ '<div class="bt-row-top">'
|
||||
+ '<div class="bt-row-top-left">'
|
||||
+ protoBadge
|
||||
+ '<span class="bt-row-name' + (hasName ? '' : ' bt-unnamed') + '">' + name + '</span>'
|
||||
+ trackerBadge + irkBadge + riskBadge + clusterBadge
|
||||
+ '</div>'
|
||||
+ '<div class="bt-row-top-right">'
|
||||
+ flagBadges + statusDot
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
// Bottom line
|
||||
+ '<div class="bt-row-bottom">'
|
||||
+ '<div class="bt-signal-bar-wrap">'
|
||||
+ '<div class="bt-signal-track">'
|
||||
+ '<div class="bt-signal-fill ' + fillClass + '" style="width:' + rssiPercent.toFixed(1) + '%"></div>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '<div class="bt-row-meta">' + metaHtml + '</div>'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
function getRssiColor(rssi) {
|
||||
@@ -1756,14 +1790,8 @@ const BluetoothMode = (function() {
|
||||
mac_cluster_count: device.mac_cluster_count || 0
|
||||
};
|
||||
|
||||
// If BtLocate is already loaded, hand off directly
|
||||
if (typeof BtLocate !== 'undefined') {
|
||||
BtLocate.handoff(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
// Switch to bt_locate mode first — this loads the script, styles,
|
||||
// and initializes the module. Then hand off the device data.
|
||||
// Always switch to bt_locate mode first (loads script + styles if needed,
|
||||
// initializes the module), then hand off device data.
|
||||
if (typeof switchMode === 'function') {
|
||||
switchMode('bt_locate').then(function() {
|
||||
if (typeof BtLocate !== 'undefined') {
|
||||
|
||||
@@ -1792,11 +1792,6 @@ const BtLocate = (function() {
|
||||
const irkInput = document.getElementById('btLocateIrk');
|
||||
if (irkInput) irkInput.value = deviceInfo.irk_hex;
|
||||
}
|
||||
|
||||
// Switch to bt_locate mode
|
||||
if (typeof switchMode === 'function') {
|
||||
switchMode('bt_locate');
|
||||
}
|
||||
}
|
||||
|
||||
function clearHandoff() {
|
||||
|
||||
+74
-64
@@ -13,12 +13,14 @@ const SSTV = (function() {
|
||||
let issMap = null;
|
||||
let issMarker = null;
|
||||
let issTrackLine = null;
|
||||
let issTrackPast = null;
|
||||
let issPosition = null;
|
||||
let issUpdateInterval = null;
|
||||
let countdownInterval = null;
|
||||
let nextPassData = null;
|
||||
let pendingMapInvalidate = false;
|
||||
let locationListenersAttached = false;
|
||||
let issUpdateInterval = null;
|
||||
let issTrackInterval = null;
|
||||
let countdownInterval = null;
|
||||
let nextPassData = null;
|
||||
let pendingMapInvalidate = false;
|
||||
let locationListenersAttached = false;
|
||||
|
||||
// ISS frequency
|
||||
const ISS_FREQ = 145.800;
|
||||
@@ -93,12 +95,12 @@ const SSTV = (function() {
|
||||
if (latInput && storedLat) latInput.value = storedLat;
|
||||
if (lonInput && storedLon) lonInput.value = storedLon;
|
||||
|
||||
if (!locationListenersAttached) {
|
||||
if (latInput) latInput.addEventListener('change', saveLocationFromInputs);
|
||||
if (lonInput) lonInput.addEventListener('change', saveLocationFromInputs);
|
||||
locationListenersAttached = true;
|
||||
}
|
||||
}
|
||||
if (!locationListenersAttached) {
|
||||
if (latInput) latInput.addEventListener('change', saveLocationFromInputs);
|
||||
if (lonInput) lonInput.addEventListener('change', saveLocationFromInputs);
|
||||
locationListenersAttached = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save location from input fields
|
||||
@@ -250,12 +252,19 @@ const SSTV = (function() {
|
||||
// Create ISS marker (will be positioned when we get data)
|
||||
issMarker = L.marker([0, 0], { icon: issIcon }).addTo(issMap);
|
||||
|
||||
// Create ground track line
|
||||
// Past track (dimmer, solid)
|
||||
issTrackPast = L.polyline([], {
|
||||
color: '#00d4ff',
|
||||
weight: 1.5,
|
||||
opacity: 0.3,
|
||||
}).addTo(issMap);
|
||||
|
||||
// Future track (brighter, dashed)
|
||||
issTrackLine = L.polyline([], {
|
||||
color: '#00d4ff',
|
||||
weight: 2,
|
||||
opacity: 0.6,
|
||||
dashArray: '5, 5'
|
||||
opacity: 0.7,
|
||||
dashArray: '6, 4'
|
||||
}).addTo(issMap);
|
||||
|
||||
issMap.on('resize moveend zoomend', () => {
|
||||
@@ -272,9 +281,12 @@ const SSTV = (function() {
|
||||
*/
|
||||
function startIssTracking() {
|
||||
updateIssPosition();
|
||||
// Update every 5 seconds
|
||||
updateIssTrack();
|
||||
if (issUpdateInterval) clearInterval(issUpdateInterval);
|
||||
issUpdateInterval = setInterval(updateIssPosition, 5000);
|
||||
// Track refreshes every 5 minutes — one orbit is ~93 min so this keeps it current
|
||||
if (issTrackInterval) clearInterval(issTrackInterval);
|
||||
issTrackInterval = setInterval(updateIssTrack, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -285,6 +297,52 @@ const SSTV = (function() {
|
||||
clearInterval(issUpdateInterval);
|
||||
issUpdateInterval = null;
|
||||
}
|
||||
if (issTrackInterval) {
|
||||
clearInterval(issTrackInterval);
|
||||
issTrackInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and render the ISS ground track from the backend (TLE-propagated).
|
||||
*/
|
||||
async function updateIssTrack() {
|
||||
try {
|
||||
const response = await fetch('/sstv/iss-track');
|
||||
const data = await response.json();
|
||||
if (data.status !== 'ok' || !issTrackLine || !issTrackPast) return;
|
||||
|
||||
const pastPts = [], futurePts = [];
|
||||
for (const pt of data.track) {
|
||||
(pt.past ? pastPts : futurePts).push([pt.lat, pt.lon]);
|
||||
}
|
||||
|
||||
// Split future track at antimeridian crossings to avoid long horizontal lines
|
||||
const futureSegments = _splitAtAntimeridian(futurePts);
|
||||
const pastSegments = _splitAtAntimeridian(pastPts);
|
||||
|
||||
issTrackLine.setLatLngs(futureSegments);
|
||||
issTrackPast.setLatLngs(pastSegments);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch ISS track:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Split an array of [lat, lon] points into segments at antimeridian crossings.
|
||||
*/
|
||||
function _splitAtAntimeridian(points) {
|
||||
const segments = [];
|
||||
let current = [];
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
if (i > 0 && Math.abs(points[i][1] - points[i - 1][1]) > 180) {
|
||||
if (current.length > 1) segments.push(current);
|
||||
current = [];
|
||||
}
|
||||
current.push(points[i]);
|
||||
}
|
||||
if (current.length > 1) segments.push(current);
|
||||
return segments;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -486,55 +544,7 @@ const SSTV = (function() {
|
||||
issMarker.setLatLng([lat, lon]);
|
||||
}
|
||||
|
||||
// Calculate and draw ground track
|
||||
if (issTrackLine) {
|
||||
const trackPoints = [];
|
||||
const inclination = 51.6; // ISS orbital inclination in degrees
|
||||
|
||||
// Generate orbit track points
|
||||
for (let offset = -180; offset <= 180; offset += 3) {
|
||||
let trackLon = lon + offset;
|
||||
|
||||
// Normalize longitude
|
||||
while (trackLon > 180) trackLon -= 360;
|
||||
while (trackLon < -180) trackLon += 360;
|
||||
|
||||
// Calculate latitude based on orbital inclination
|
||||
const phase = (offset / 360) * 2 * Math.PI;
|
||||
const currentPhase = Math.asin(Math.max(-1, Math.min(1, lat / inclination)));
|
||||
let trackLat = inclination * Math.sin(phase + currentPhase);
|
||||
|
||||
// Clamp to valid range
|
||||
trackLat = Math.max(-inclination, Math.min(inclination, trackLat));
|
||||
|
||||
trackPoints.push([trackLat, trackLon]);
|
||||
}
|
||||
|
||||
// Split track at antimeridian to avoid line across map
|
||||
const segments = [];
|
||||
let currentSegment = [];
|
||||
|
||||
for (let i = 0; i < trackPoints.length; i++) {
|
||||
if (i > 0) {
|
||||
const prevLon = trackPoints[i - 1][1];
|
||||
const currLon = trackPoints[i][1];
|
||||
if (Math.abs(currLon - prevLon) > 180) {
|
||||
// Crossed antimeridian
|
||||
if (currentSegment.length > 0) {
|
||||
segments.push(currentSegment);
|
||||
}
|
||||
currentSegment = [];
|
||||
}
|
||||
}
|
||||
currentSegment.push(trackPoints[i]);
|
||||
}
|
||||
if (currentSegment.length > 0) {
|
||||
segments.push(currentSegment);
|
||||
}
|
||||
|
||||
// Use only the longest segment or combine if needed
|
||||
issTrackLine.setLatLngs(segments.length > 0 ? segments : []);
|
||||
}
|
||||
// Track is fetched separately by updateIssTrack() via /sstv/iss-track
|
||||
|
||||
// Pan map to follow ISS only when the map pane is currently renderable.
|
||||
if (isMapContainerVisible()) {
|
||||
|
||||
@@ -1561,21 +1561,22 @@ const WeatherSat = (function() {
|
||||
const spans = labels.querySelectorAll('span');
|
||||
if (spans.length !== hours.length) return;
|
||||
|
||||
const tz = typeof InterceptTime !== 'undefined' ? InterceptTime.getTimezone() : 'UTC';
|
||||
const ianaName = typeof InterceptTime !== 'undefined' ? InterceptTime.getIANA() : undefined;
|
||||
|
||||
hours.forEach((h, i) => {
|
||||
if (selectedTimezone === 'UTC' || selectedTimezone === 'local') {
|
||||
spans[i].textContent = h === 24 ? '24:00' : `${String(h).padStart(2, '0')}:00`;
|
||||
if (h === 24) {
|
||||
spans[i].textContent = '24:00';
|
||||
return;
|
||||
}
|
||||
if (tz === 'UTC' || tz === 'local') {
|
||||
spans[i].textContent = `${String(h).padStart(2, '0')}:00`;
|
||||
} else {
|
||||
// Show timezone-adjusted labels
|
||||
const d = new Date();
|
||||
d.setHours(h, 0, 0, 0);
|
||||
const tz = TZ_MAP[selectedTimezone];
|
||||
const opts = { hour: '2-digit', minute: '2-digit', hour12: false };
|
||||
if (tz) opts.timeZone = tz;
|
||||
if (h === 24) {
|
||||
spans[i].textContent = '24:00';
|
||||
} else {
|
||||
spans[i].textContent = d.toLocaleTimeString(undefined, opts).slice(0, 5);
|
||||
}
|
||||
if (ianaName) opts.timeZone = ianaName;
|
||||
spans[i].textContent = d.toLocaleTimeString(undefined, opts).slice(0, 5);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+1917
-1876
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user