mirror of
https://github.com/smittix/intercept.git
synced 2026-04-25 15:20:00 -07:00
Apply global map theme updates and UI improvements
This commit is contained in:
@@ -36,6 +36,7 @@ const BtLocate = (function() {
|
||||
let autoFollowEnabled = true;
|
||||
let smoothingEnabled = true;
|
||||
let lastRenderedDetectionKey = null;
|
||||
let pendingHeatSync = false;
|
||||
|
||||
const MAX_HEAT_POINTS = 1200;
|
||||
const MAX_TRAIL_POINTS = 1200;
|
||||
@@ -63,6 +64,23 @@ const BtLocate = (function() {
|
||||
},
|
||||
};
|
||||
|
||||
function getMapContainer() {
|
||||
if (!map || typeof map.getContainer !== 'function') return null;
|
||||
return map.getContainer();
|
||||
}
|
||||
|
||||
function isMapContainerVisible() {
|
||||
const container = getMapContainer();
|
||||
if (!container) return false;
|
||||
if (container.offsetWidth <= 0 || container.offsetHeight <= 0) return false;
|
||||
if (container.style && container.style.display === 'none') return false;
|
||||
if (typeof window.getComputedStyle === 'function') {
|
||||
const style = window.getComputedStyle(container);
|
||||
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function init() {
|
||||
loadOverlayPreferences();
|
||||
syncOverlayControls();
|
||||
@@ -71,20 +89,21 @@ const BtLocate = (function() {
|
||||
// Re-invalidate map on re-entry and ensure tiles are present
|
||||
if (map) {
|
||||
setTimeout(() => {
|
||||
map.invalidateSize();
|
||||
// Re-apply user's tile layer if tiles were lost
|
||||
let hasTiles = false;
|
||||
map.eachLayer(layer => {
|
||||
if (layer instanceof L.TileLayer) hasTiles = true;
|
||||
});
|
||||
if (!hasTiles && typeof Settings !== 'undefined' && Settings.createTileLayer) {
|
||||
Settings.createTileLayer().addTo(map);
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
checkStatus();
|
||||
return;
|
||||
}
|
||||
safeInvalidateMap();
|
||||
// Re-apply user's tile layer if tiles were lost
|
||||
let hasTiles = false;
|
||||
map.eachLayer(layer => {
|
||||
if (layer instanceof L.TileLayer) hasTiles = true;
|
||||
});
|
||||
if (!hasTiles && typeof Settings !== 'undefined' && Settings.createTileLayer) {
|
||||
Settings.createTileLayer().addTo(map);
|
||||
}
|
||||
flushPendingHeatSync();
|
||||
}, 150);
|
||||
}
|
||||
checkStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Init map
|
||||
const mapEl = document.getElementById('btLocateMap');
|
||||
@@ -107,7 +126,13 @@ const BtLocate = (function() {
|
||||
ensureHeatLayer();
|
||||
syncMovementLayer();
|
||||
syncHeatLayer();
|
||||
setTimeout(() => map.invalidateSize(), 100);
|
||||
map.on('resize moveend zoomend', () => {
|
||||
flushPendingHeatSync();
|
||||
});
|
||||
setTimeout(() => {
|
||||
safeInvalidateMap();
|
||||
flushPendingHeatSync();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Init RSSI chart canvas
|
||||
@@ -432,7 +457,12 @@ const BtLocate = (function() {
|
||||
// Map marker
|
||||
let mapPointAdded = false;
|
||||
if (d.lat != null && d.lon != null) {
|
||||
mapPointAdded = addMapMarker(d, { suppressFollow: options.suppressFollow === true });
|
||||
try {
|
||||
mapPointAdded = addMapMarker(d, { suppressFollow: options.suppressFollow === true });
|
||||
} catch (error) {
|
||||
console.warn('[BtLocate] Map update skipped:', error);
|
||||
mapPointAdded = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Update stats
|
||||
@@ -535,7 +565,11 @@ const BtLocate = (function() {
|
||||
}
|
||||
syncHeatLayer();
|
||||
|
||||
if (autoFollowEnabled && !options.suppressFollow) {
|
||||
if (!isMapRenderable()) {
|
||||
safeInvalidateMap();
|
||||
}
|
||||
const canFollowMap = isMapRenderable();
|
||||
if (autoFollowEnabled && !options.suppressFollow && canFollowMap) {
|
||||
if (!gpsLocked) {
|
||||
gpsLocked = true;
|
||||
map.setView([lat, lon], Math.max(map.getZoom(), 16));
|
||||
@@ -631,7 +665,11 @@ const BtLocate = (function() {
|
||||
const latestGps = trailPoints[trailPoints.length - 1];
|
||||
gpsLocked = true;
|
||||
const targetZoom = Math.max(map.getZoom(), 15);
|
||||
map.setView([latestGps.lat, latestGps.lon], targetZoom);
|
||||
if (isMapRenderable()) {
|
||||
map.setView([latestGps.lat, latestGps.lon], targetZoom);
|
||||
} else {
|
||||
pendingHeatSync = true;
|
||||
}
|
||||
}
|
||||
syncMovementLayer();
|
||||
syncStrongestMarker();
|
||||
@@ -667,7 +705,15 @@ const BtLocate = (function() {
|
||||
confidenceCircle = null;
|
||||
}
|
||||
if (heatLayer) {
|
||||
heatLayer.setLatLngs([]);
|
||||
try {
|
||||
if (isMapRenderable()) {
|
||||
heatLayer.setLatLngs([]);
|
||||
} else {
|
||||
pendingHeatSync = true;
|
||||
}
|
||||
} catch (error) {
|
||||
pendingHeatSync = true;
|
||||
}
|
||||
}
|
||||
updateStrongestInfo(null);
|
||||
updateConfidenceInfo(null);
|
||||
@@ -817,14 +863,54 @@ const BtLocate = (function() {
|
||||
if (!map) return;
|
||||
ensureHeatLayer();
|
||||
if (!heatLayer) return;
|
||||
heatLayer.setLatLngs(heatPoints);
|
||||
if (heatmapEnabled) {
|
||||
if (!map.hasLayer(heatLayer)) {
|
||||
heatLayer.addTo(map);
|
||||
}
|
||||
} else if (map.hasLayer(heatLayer)) {
|
||||
map.removeLayer(heatLayer);
|
||||
if (!isMapContainerVisible()) {
|
||||
pendingHeatSync = true;
|
||||
return;
|
||||
}
|
||||
if (!isMapRenderable()) {
|
||||
safeInvalidateMap();
|
||||
if (!isMapRenderable()) {
|
||||
pendingHeatSync = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
heatLayer.setLatLngs(heatPoints);
|
||||
if (heatmapEnabled) {
|
||||
if (!map.hasLayer(heatLayer)) {
|
||||
heatLayer.addTo(map);
|
||||
}
|
||||
} else if (map.hasLayer(heatLayer)) {
|
||||
map.removeLayer(heatLayer);
|
||||
}
|
||||
pendingHeatSync = false;
|
||||
} catch (error) {
|
||||
pendingHeatSync = true;
|
||||
if (map.hasLayer(heatLayer)) {
|
||||
map.removeLayer(heatLayer);
|
||||
}
|
||||
console.warn('[BtLocate] Heatmap redraw deferred:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function isMapRenderable() {
|
||||
if (!map || !isMapContainerVisible()) return false;
|
||||
if (typeof map.getSize === 'function') {
|
||||
const size = map.getSize();
|
||||
if (!size || size.x <= 0 || size.y <= 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function safeInvalidateMap() {
|
||||
if (!map || !isMapContainerVisible()) return false;
|
||||
map.invalidateSize({ pan: false, animate: false });
|
||||
return true;
|
||||
}
|
||||
|
||||
function flushPendingHeatSync() {
|
||||
if (!pendingHeatSync) return;
|
||||
syncHeatLayer();
|
||||
}
|
||||
|
||||
function syncMovementLayer() {
|
||||
@@ -1473,9 +1559,14 @@ const BtLocate = (function() {
|
||||
.catch(err => console.error('[BtLocate] Clear trail error:', err));
|
||||
}
|
||||
|
||||
function invalidateMap() {
|
||||
if (map) map.invalidateSize();
|
||||
}
|
||||
function invalidateMap() {
|
||||
if (safeInvalidateMap()) {
|
||||
flushPendingHeatSync();
|
||||
syncMovementLayer();
|
||||
syncStrongestMarker();
|
||||
updateConfidenceLayer();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
|
||||
@@ -12,11 +12,12 @@ const SSTV = (function() {
|
||||
let progress = 0;
|
||||
let issMap = null;
|
||||
let issMarker = null;
|
||||
let issTrackLine = null;
|
||||
let issPosition = null;
|
||||
let issUpdateInterval = null;
|
||||
let countdownInterval = null;
|
||||
let nextPassData = null;
|
||||
let issTrackLine = null;
|
||||
let issPosition = null;
|
||||
let issUpdateInterval = null;
|
||||
let countdownInterval = null;
|
||||
let nextPassData = null;
|
||||
let pendingMapInvalidate = false;
|
||||
|
||||
// ISS frequency
|
||||
const ISS_FREQ = 145.800;
|
||||
@@ -37,15 +38,31 @@ const SSTV = (function() {
|
||||
/**
|
||||
* Initialize the SSTV mode
|
||||
*/
|
||||
function init() {
|
||||
checkStatus();
|
||||
loadImages();
|
||||
loadLocationInputs();
|
||||
loadIssSchedule();
|
||||
initMap();
|
||||
startIssTracking();
|
||||
startCountdown();
|
||||
}
|
||||
function init() {
|
||||
checkStatus();
|
||||
loadImages();
|
||||
loadLocationInputs();
|
||||
loadIssSchedule();
|
||||
initMap();
|
||||
startIssTracking();
|
||||
startCountdown();
|
||||
// Ensure Leaflet recomputes dimensions after the SSTV pane becomes visible.
|
||||
setTimeout(() => invalidateMap(), 80);
|
||||
setTimeout(() => invalidateMap(), 260);
|
||||
}
|
||||
|
||||
function isMapContainerVisible() {
|
||||
if (!issMap || typeof issMap.getContainer !== 'function') return false;
|
||||
const container = issMap.getContainer();
|
||||
if (!container) return false;
|
||||
if (container.offsetWidth <= 0 || container.offsetHeight <= 0) return false;
|
||||
if (container.style && container.style.display === 'none') return false;
|
||||
if (typeof window.getComputedStyle === 'function') {
|
||||
const style = window.getComputedStyle(container);
|
||||
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load location into input fields
|
||||
@@ -172,9 +189,9 @@ const SSTV = (function() {
|
||||
/**
|
||||
* Initialize Leaflet map for ISS tracking
|
||||
*/
|
||||
async function initMap() {
|
||||
const mapContainer = document.getElementById('sstvIssMap');
|
||||
if (!mapContainer || issMap) return;
|
||||
async function initMap() {
|
||||
const mapContainer = document.getElementById('sstvIssMap');
|
||||
if (!mapContainer || issMap) return;
|
||||
|
||||
// Create map
|
||||
issMap = L.map('sstvIssMap', {
|
||||
@@ -214,13 +231,21 @@ const SSTV = (function() {
|
||||
issMarker = L.marker([0, 0], { icon: issIcon }).addTo(issMap);
|
||||
|
||||
// Create ground track line
|
||||
issTrackLine = L.polyline([], {
|
||||
color: '#00d4ff',
|
||||
weight: 2,
|
||||
opacity: 0.6,
|
||||
dashArray: '5, 5'
|
||||
}).addTo(issMap);
|
||||
}
|
||||
issTrackLine = L.polyline([], {
|
||||
color: '#00d4ff',
|
||||
weight: 2,
|
||||
opacity: 0.6,
|
||||
dashArray: '5, 5'
|
||||
}).addTo(issMap);
|
||||
|
||||
issMap.on('resize moveend zoomend', () => {
|
||||
if (pendingMapInvalidate) invalidateMap();
|
||||
});
|
||||
|
||||
// Initial layout passes for first-time mode load.
|
||||
setTimeout(() => invalidateMap(), 40);
|
||||
setTimeout(() => invalidateMap(), 180);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start ISS position tracking
|
||||
@@ -429,8 +454,9 @@ const SSTV = (function() {
|
||||
/**
|
||||
* Update map with ISS position
|
||||
*/
|
||||
function updateMap() {
|
||||
if (!issMap || !issPosition) return;
|
||||
function updateMap() {
|
||||
if (!issMap || !issPosition) return;
|
||||
if (pendingMapInvalidate) invalidateMap();
|
||||
|
||||
const lat = issPosition.lat;
|
||||
const lon = issPosition.lon;
|
||||
@@ -490,9 +516,13 @@ const SSTV = (function() {
|
||||
issTrackLine.setLatLngs(segments.length > 0 ? segments : []);
|
||||
}
|
||||
|
||||
// Pan map to follow ISS
|
||||
issMap.panTo([lat, lon], { animate: true, duration: 0.5 });
|
||||
}
|
||||
// Pan map to follow ISS only when the map pane is currently renderable.
|
||||
if (isMapContainerVisible()) {
|
||||
issMap.panTo([lat, lon], { animate: true, duration: 0.5 });
|
||||
} else {
|
||||
pendingMapInvalidate = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check current decoder status
|
||||
@@ -1305,13 +1335,27 @@ const SSTV = (function() {
|
||||
/**
|
||||
* Show status message
|
||||
*/
|
||||
function showStatusMessage(message, type) {
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('SSTV', message);
|
||||
} else {
|
||||
console.log(`[SSTV ${type}] ${message}`);
|
||||
}
|
||||
}
|
||||
function showStatusMessage(message, type) {
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('SSTV', message);
|
||||
} else {
|
||||
console.log(`[SSTV ${type}] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate ISS map size after pane/layout changes.
|
||||
*/
|
||||
function invalidateMap() {
|
||||
if (!issMap) return false;
|
||||
if (!isMapContainerVisible()) {
|
||||
pendingMapInvalidate = true;
|
||||
return false;
|
||||
}
|
||||
issMap.invalidateSize({ pan: false, animate: false });
|
||||
pendingMapInvalidate = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
@@ -1326,11 +1370,12 @@ const SSTV = (function() {
|
||||
deleteAllImages,
|
||||
downloadImage,
|
||||
useGPS,
|
||||
updateTLE,
|
||||
stopIssTracking,
|
||||
stopCountdown
|
||||
};
|
||||
})();
|
||||
updateTLE,
|
||||
stopIssTracking,
|
||||
stopCountdown,
|
||||
invalidateMap
|
||||
};
|
||||
})();
|
||||
|
||||
// Initialize when DOM is ready (will be called by selectMode)
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Weather Satellite Mode
|
||||
/**
|
||||
* Weather Satellite Mode
|
||||
* NOAA APT and Meteor LRPT decoder interface with auto-scheduler,
|
||||
* polar plot, mercator map, countdown, and timeline.
|
||||
*/
|
||||
* polar plot, styled real-world map, countdown, and timeline.
|
||||
*/
|
||||
|
||||
const WeatherSat = (function() {
|
||||
// State
|
||||
@@ -11,39 +11,73 @@ const WeatherSat = (function() {
|
||||
let images = [];
|
||||
let passes = [];
|
||||
let selectedPassIndex = -1;
|
||||
let currentSatellite = null;
|
||||
let countdownInterval = null;
|
||||
let currentSatellite = null;
|
||||
let countdownInterval = null;
|
||||
let schedulerEnabled = false;
|
||||
let groundMap = null;
|
||||
let groundTrackLayer = null;
|
||||
let groundOverlayLayer = null;
|
||||
let groundGridLayer = null;
|
||||
let satCrosshairMarker = null;
|
||||
let observerMarker = null;
|
||||
let consoleEntries = [];
|
||||
let consoleEntries = [];
|
||||
let consoleCollapsed = false;
|
||||
let currentPhase = 'idle';
|
||||
let consoleAutoHideTimer = null;
|
||||
let currentModalFilename = null;
|
||||
let locationListenersAttached = false;
|
||||
let consoleAutoHideTimer = null;
|
||||
let currentModalFilename = null;
|
||||
let locationListenersAttached = false;
|
||||
|
||||
/**
|
||||
* Initialize the Weather Satellite mode
|
||||
*/
|
||||
function init() {
|
||||
checkStatus();
|
||||
loadImages();
|
||||
loadLocationInputs();
|
||||
loadPasses();
|
||||
function init() {
|
||||
checkStatus();
|
||||
loadImages();
|
||||
loadLocationInputs();
|
||||
loadPasses();
|
||||
startCountdownTimer();
|
||||
checkSchedulerStatus();
|
||||
initGroundMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load observer location into input fields
|
||||
*/
|
||||
initGroundMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get observer coordinates from shared location or local storage.
|
||||
*/
|
||||
function getObserverCoords() {
|
||||
let lat;
|
||||
let lon;
|
||||
|
||||
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
|
||||
const shared = ObserverLocation.getShared();
|
||||
lat = Number(shared?.lat);
|
||||
lon = Number(shared?.lon);
|
||||
} else {
|
||||
lat = Number(localStorage.getItem('observerLat'));
|
||||
lon = Number(localStorage.getItem('observerLon'));
|
||||
}
|
||||
|
||||
if (!isFinite(lat) || !isFinite(lon)) return null;
|
||||
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) return null;
|
||||
return { lat, lon };
|
||||
}
|
||||
|
||||
/**
|
||||
* Center the ground map on current observer coordinates when available.
|
||||
*/
|
||||
function centerGroundMapOnObserver(zoom = 1) {
|
||||
if (!groundMap) return;
|
||||
const observer = getObserverCoords();
|
||||
if (!observer) return;
|
||||
const lat = Math.max(-85, Math.min(85, observer.lat));
|
||||
const lon = normalizeLon(observer.lon);
|
||||
groundMap.setView([lat, lon], zoom, { animate: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Load observer location into input fields
|
||||
*/
|
||||
function loadLocationInputs() {
|
||||
const latInput = document.getElementById('wxsatObsLat');
|
||||
const latInput = document.getElementById('wxsatObsLat');
|
||||
const lonInput = document.getElementById('wxsatObsLon');
|
||||
|
||||
let storedLat = localStorage.getItem('observerLat');
|
||||
@@ -80,13 +114,14 @@ const WeatherSat = (function() {
|
||||
!isNaN(lon) && lon >= -180 && lon <= 180) {
|
||||
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
|
||||
ObserverLocation.setShared({ lat, lon });
|
||||
} else {
|
||||
localStorage.setItem('observerLat', lat.toString());
|
||||
localStorage.setItem('observerLon', lon.toString());
|
||||
}
|
||||
loadPasses();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem('observerLat', lat.toString());
|
||||
localStorage.setItem('observerLon', lon.toString());
|
||||
}
|
||||
loadPasses();
|
||||
centerGroundMapOnObserver(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use GPS for location
|
||||
@@ -119,11 +154,12 @@ const WeatherSat = (function() {
|
||||
localStorage.setItem('observerLon', lon);
|
||||
}
|
||||
|
||||
btn.innerHTML = originalText;
|
||||
btn.disabled = false;
|
||||
showNotification('Weather Sat', 'Location updated');
|
||||
loadPasses();
|
||||
},
|
||||
btn.innerHTML = originalText;
|
||||
btn.disabled = false;
|
||||
showNotification('Weather Sat', 'Location updated');
|
||||
loadPasses();
|
||||
centerGroundMapOnObserver(1);
|
||||
},
|
||||
(err) => {
|
||||
btn.innerHTML = originalText;
|
||||
btn.disabled = false;
|
||||
@@ -749,118 +785,140 @@ const WeatherSat = (function() {
|
||||
ctx.fillText(Math.round(maxEl) + '\u00b0', cx + r * maxR * Math.cos(maxAz), cy + r * maxR * Math.sin(maxAz) - 8);
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Ground Track Map
|
||||
// ========================
|
||||
|
||||
/**
|
||||
* Initialize Leaflet ground track map
|
||||
*/
|
||||
function initGroundMap() {
|
||||
// ========================
|
||||
// Ground Track Map
|
||||
// ========================
|
||||
|
||||
/**
|
||||
* Initialize styled real-world map panel.
|
||||
*/
|
||||
async function initGroundMap() {
|
||||
const container = document.getElementById('wxsatGroundMap');
|
||||
if (!container || groundMap) return;
|
||||
if (!container) return;
|
||||
if (typeof L === 'undefined') return;
|
||||
const observer = getObserverCoords();
|
||||
const defaultCenter = observer
|
||||
? [Math.max(-85, Math.min(85, observer.lat)), normalizeLon(observer.lon)]
|
||||
: [12, 0];
|
||||
const defaultZoom = 1;
|
||||
|
||||
groundMap = L.map(container, {
|
||||
center: [20, 0],
|
||||
zoom: 2,
|
||||
zoomControl: false,
|
||||
attributionControl: false,
|
||||
crs: L.CRS.EPSG3857, // Web Mercator projection
|
||||
});
|
||||
|
||||
// Check tile provider from settings
|
||||
let tileUrl = 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png';
|
||||
try {
|
||||
const provider = localStorage.getItem('tileProvider');
|
||||
if (provider === 'osm') {
|
||||
tileUrl = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
L.tileLayer(tileUrl, { maxZoom: 10 }).addTo(groundMap);
|
||||
if (!groundMap) {
|
||||
groundMap = L.map(container, {
|
||||
center: defaultCenter,
|
||||
zoom: defaultZoom,
|
||||
minZoom: 1,
|
||||
maxZoom: 7,
|
||||
zoomControl: false,
|
||||
attributionControl: false,
|
||||
worldCopyJump: true,
|
||||
preferCanvas: true,
|
||||
});
|
||||
|
||||
groundTrackLayer = L.layerGroup().addTo(groundMap);
|
||||
groundOverlayLayer = L.layerGroup().addTo(groundMap);
|
||||
if (typeof Settings !== 'undefined' && Settings.createTileLayer) {
|
||||
await Settings.init();
|
||||
Settings.createTileLayer().addTo(groundMap);
|
||||
Settings.registerMap(groundMap);
|
||||
} else {
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
subdomains: 'abcd',
|
||||
maxZoom: 18,
|
||||
noWrap: false,
|
||||
crossOrigin: true,
|
||||
className: 'tile-layer-cyan',
|
||||
}).addTo(groundMap);
|
||||
}
|
||||
|
||||
const selected = getSelectedPass();
|
||||
if (selected) {
|
||||
updateGroundTrack(selected);
|
||||
} else {
|
||||
updateSatelliteCrosshair(null);
|
||||
groundGridLayer = L.layerGroup().addTo(groundMap);
|
||||
addStyledGridOverlay(groundGridLayer);
|
||||
|
||||
groundTrackLayer = L.layerGroup().addTo(groundMap);
|
||||
groundOverlayLayer = L.layerGroup().addTo(groundMap);
|
||||
}
|
||||
|
||||
// Delayed invalidation to fix sizing
|
||||
setTimeout(() => { if (groundMap) groundMap.invalidateSize(); }, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update ground track on the map
|
||||
*/
|
||||
function updateGroundTrack(pass) {
|
||||
if (!groundMap || !groundTrackLayer) return;
|
||||
|
||||
groundTrackLayer.clearLayers();
|
||||
if (!pass) {
|
||||
updateSatelliteCrosshair(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const track = pass.groundTrack;
|
||||
if (!track || track.length === 0) {
|
||||
updateSatelliteCrosshair(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const color = pass.mode === 'LRPT' ? '#00ff88' : '#00d4ff';
|
||||
|
||||
// Draw polyline
|
||||
const latlngs = track.map(p => [p.lat, p.lon]);
|
||||
L.polyline(latlngs, { color, weight: 2, opacity: 0.8 }).addTo(groundTrackLayer);
|
||||
|
||||
// Start marker
|
||||
L.circleMarker(latlngs[0], {
|
||||
radius: 5, color: '#00ff88', fillColor: '#00ff88', fillOpacity: 1, weight: 0,
|
||||
}).addTo(groundTrackLayer);
|
||||
|
||||
// End marker
|
||||
L.circleMarker(latlngs[latlngs.length - 1], {
|
||||
radius: 5, color: '#ff4444', fillColor: '#ff4444', fillOpacity: 1, weight: 0,
|
||||
}).addTo(groundTrackLayer);
|
||||
|
||||
// Observer marker
|
||||
let obsLat, obsLon;
|
||||
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
|
||||
const shared = ObserverLocation.getShared();
|
||||
obsLat = shared?.lat;
|
||||
obsLon = shared?.lon;
|
||||
} else {
|
||||
obsLat = parseFloat(localStorage.getItem('observerLat'));
|
||||
obsLon = parseFloat(localStorage.getItem('observerLon'));
|
||||
}
|
||||
const lat = obsLat;
|
||||
const lon = obsLon;
|
||||
if (!isNaN(lat) && !isNaN(lon)) {
|
||||
L.circleMarker([lat, lon], {
|
||||
radius: 6, color: '#ffbb00', fillColor: '#ffbb00', fillOpacity: 0.8, weight: 1,
|
||||
}).addTo(groundTrackLayer);
|
||||
}
|
||||
|
||||
// Fit bounds
|
||||
try {
|
||||
const bounds = L.latLngBounds(latlngs);
|
||||
if (!isNaN(lat) && !isNaN(lon)) bounds.extend([lat, lon]);
|
||||
groundMap.fitBounds(bounds, { padding: [20, 20] });
|
||||
} catch (e) {}
|
||||
|
||||
updateSatelliteCrosshair(pass);
|
||||
setTimeout(() => {
|
||||
if (!groundMap) return;
|
||||
groundMap.invalidateSize(false);
|
||||
groundMap.setView(defaultCenter, defaultZoom, { animate: false });
|
||||
updateGroundTrack(getSelectedPass());
|
||||
}, 140);
|
||||
}
|
||||
|
||||
function updateMercatorInfo(text) {
|
||||
const infoEl = document.getElementById('wxsatMercatorInfo');
|
||||
/**
|
||||
* Update map panel subtitle.
|
||||
*/
|
||||
function updateProjectionInfo(text) {
|
||||
const infoEl = document.getElementById('wxsatMapInfo');
|
||||
if (infoEl) infoEl.textContent = text || '--';
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize longitude to [-180, 180).
|
||||
*/
|
||||
function normalizeLon(value) {
|
||||
const lon = Number(value);
|
||||
if (!isFinite(lon)) return 0;
|
||||
return ((((lon + 180) % 360) + 360) % 360) - 180;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build track segments that do not cross the date line.
|
||||
*/
|
||||
function buildTrackSegments(track) {
|
||||
const segments = [];
|
||||
let currentSegment = [];
|
||||
|
||||
track.forEach((point) => {
|
||||
const lat = Number(point?.lat);
|
||||
const lon = normalizeLon(point?.lon);
|
||||
if (!isFinite(lat) || !isFinite(lon)) return;
|
||||
|
||||
if (currentSegment.length > 0) {
|
||||
const prevLon = currentSegment[currentSegment.length - 1][1];
|
||||
if (Math.abs(lon - prevLon) > 180) {
|
||||
if (currentSegment.length > 1) segments.push(currentSegment);
|
||||
currentSegment = [];
|
||||
}
|
||||
}
|
||||
|
||||
currentSegment.push([lat, lon]);
|
||||
});
|
||||
|
||||
if (currentSegment.length > 1) segments.push(currentSegment);
|
||||
return segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a subtle graticule over the base map for a cyber/wireframe look.
|
||||
*/
|
||||
function addStyledGridOverlay(layer) {
|
||||
if (!layer || typeof L === 'undefined') return;
|
||||
layer.clearLayers();
|
||||
|
||||
for (let lon = -180; lon <= 180; lon += 30) {
|
||||
const line = [];
|
||||
for (let lat = -85; lat <= 85; lat += 5) line.push([lat, lon]);
|
||||
L.polyline(line, {
|
||||
color: '#4ed2ff',
|
||||
weight: lon % 60 === 0 ? 1.1 : 0.8,
|
||||
opacity: lon % 60 === 0 ? 0.2 : 0.12,
|
||||
interactive: false,
|
||||
lineCap: 'round',
|
||||
}).addTo(layer);
|
||||
}
|
||||
|
||||
for (let lat = -75; lat <= 75; lat += 15) {
|
||||
const line = [];
|
||||
for (let lon = -180; lon <= 180; lon += 5) line.push([lat, lon]);
|
||||
L.polyline(line, {
|
||||
color: '#5be7ff',
|
||||
weight: lat % 30 === 0 ? 1.1 : 0.8,
|
||||
opacity: lat % 30 === 0 ? 0.2 : 0.12,
|
||||
interactive: false,
|
||||
lineCap: 'round',
|
||||
}).addTo(layer);
|
||||
}
|
||||
}
|
||||
|
||||
function clearSatelliteCrosshair() {
|
||||
if (!groundOverlayLayer || !satCrosshairMarker) return;
|
||||
groundOverlayLayer.removeLayer(satCrosshairMarker);
|
||||
@@ -870,8 +928,8 @@ const WeatherSat = (function() {
|
||||
function createSatelliteCrosshairIcon() {
|
||||
return L.divIcon({
|
||||
className: 'wxsat-crosshair-icon',
|
||||
iconSize: [26, 26],
|
||||
iconAnchor: [13, 13],
|
||||
iconSize: [30, 30],
|
||||
iconAnchor: [15, 15],
|
||||
html: `
|
||||
<div class="wxsat-crosshair-marker">
|
||||
<span class="wxsat-crosshair-h"></span>
|
||||
@@ -883,6 +941,92 @@ const WeatherSat = (function() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update selected ground track and redraw map overlays.
|
||||
*/
|
||||
function updateGroundTrack(pass) {
|
||||
if (!groundMap || !groundTrackLayer) return;
|
||||
|
||||
groundTrackLayer.clearLayers();
|
||||
observerMarker = null;
|
||||
|
||||
if (!pass) {
|
||||
clearSatelliteCrosshair();
|
||||
updateProjectionInfo('--');
|
||||
return;
|
||||
}
|
||||
|
||||
const track = pass?.groundTrack;
|
||||
if (!Array.isArray(track) || track.length === 0) {
|
||||
clearSatelliteCrosshair();
|
||||
updateProjectionInfo(`${pass.name || pass.satellite || '--'} --`);
|
||||
return;
|
||||
}
|
||||
|
||||
const color = pass.mode === 'LRPT' ? '#27ffc6' : '#58ddff';
|
||||
const glowClass = pass.mode === 'LRPT' ? 'wxsat-pass-track lrpt' : 'wxsat-pass-track apt';
|
||||
const segments = buildTrackSegments(track);
|
||||
const validPoints = track
|
||||
.map((point) => [Number(point?.lat), normalizeLon(point?.lon)])
|
||||
.filter((point) => isFinite(point[0]) && isFinite(point[1]));
|
||||
|
||||
segments.forEach((segment) => {
|
||||
L.polyline(segment, {
|
||||
color,
|
||||
weight: 2.3,
|
||||
opacity: 0.9,
|
||||
className: glowClass,
|
||||
interactive: false,
|
||||
lineJoin: 'round',
|
||||
}).addTo(groundTrackLayer);
|
||||
});
|
||||
|
||||
if (validPoints.length > 0) {
|
||||
L.circleMarker(validPoints[0], {
|
||||
radius: 4.5,
|
||||
color: '#00ffa2',
|
||||
fillColor: '#00ffa2',
|
||||
fillOpacity: 0.95,
|
||||
weight: 0,
|
||||
interactive: false,
|
||||
}).addTo(groundTrackLayer);
|
||||
|
||||
L.circleMarker(validPoints[validPoints.length - 1], {
|
||||
radius: 4.5,
|
||||
color: '#ff5e5e',
|
||||
fillColor: '#ff5e5e',
|
||||
fillOpacity: 0.95,
|
||||
weight: 0,
|
||||
interactive: false,
|
||||
}).addTo(groundTrackLayer);
|
||||
}
|
||||
|
||||
let obsLat;
|
||||
let obsLon;
|
||||
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
|
||||
const shared = ObserverLocation.getShared();
|
||||
obsLat = shared?.lat;
|
||||
obsLon = shared?.lon;
|
||||
} else {
|
||||
obsLat = parseFloat(localStorage.getItem('observerLat'));
|
||||
obsLon = parseFloat(localStorage.getItem('observerLon'));
|
||||
}
|
||||
|
||||
if (isFinite(obsLat) && isFinite(obsLon)) {
|
||||
observerMarker = L.circleMarker([obsLat, obsLon], {
|
||||
radius: 5.5,
|
||||
color: '#ffd45b',
|
||||
fillColor: '#ffd45b',
|
||||
fillOpacity: 0.8,
|
||||
weight: 1,
|
||||
className: 'wxsat-observer-marker',
|
||||
interactive: false,
|
||||
}).addTo(groundTrackLayer);
|
||||
}
|
||||
|
||||
updateSatelliteCrosshair(pass);
|
||||
}
|
||||
|
||||
function getSelectedPass() {
|
||||
if (selectedPassIndex < 0 || selectedPassIndex >= passes.length) return null;
|
||||
return passes[selectedPassIndex];
|
||||
@@ -938,41 +1082,44 @@ const WeatherSat = (function() {
|
||||
|
||||
if (!pass) {
|
||||
clearSatelliteCrosshair();
|
||||
updateMercatorInfo('--');
|
||||
updateProjectionInfo('--');
|
||||
return;
|
||||
}
|
||||
|
||||
const position = getSatellitePositionForPass(pass);
|
||||
if (!position) {
|
||||
clearSatelliteCrosshair();
|
||||
updateMercatorInfo(`${pass.name || pass.satellite || '--'} --`);
|
||||
updateProjectionInfo(`${pass.name || pass.satellite || '--'} --`);
|
||||
return;
|
||||
}
|
||||
|
||||
const latlng = [position.lat, position.lon];
|
||||
const latlng = [position.lat, normalizeLon(position.lon)];
|
||||
if (!satCrosshairMarker) {
|
||||
satCrosshairMarker = L.marker(latlng, {
|
||||
icon: createSatelliteCrosshairIcon(),
|
||||
interactive: false,
|
||||
keyboard: false,
|
||||
zIndexOffset: 800,
|
||||
zIndexOffset: 900,
|
||||
}).addTo(groundOverlayLayer);
|
||||
} else {
|
||||
satCrosshairMarker.setLatLng(latlng);
|
||||
}
|
||||
|
||||
const tooltipText = `${pass.name || pass.satellite || 'Satellite'} ${position.lat.toFixed(2)}°, ${position.lon.toFixed(2)}°`;
|
||||
const infoText =
|
||||
`${pass.name || pass.satellite || 'Satellite'} ` +
|
||||
`${position.lat.toFixed(2)}°, ${normalizeLon(position.lon).toFixed(2)}°`;
|
||||
updateProjectionInfo(infoText);
|
||||
|
||||
if (!satCrosshairMarker.getTooltip()) {
|
||||
satCrosshairMarker.bindTooltip(tooltipText, {
|
||||
satCrosshairMarker.bindTooltip(infoText, {
|
||||
direction: 'top',
|
||||
offset: [0, -10],
|
||||
opacity: 0.9,
|
||||
offset: [0, -12],
|
||||
opacity: 0.92,
|
||||
className: 'wxsat-map-tooltip',
|
||||
});
|
||||
} else {
|
||||
satCrosshairMarker.setTooltipContent(tooltipText);
|
||||
satCrosshairMarker.setTooltipContent(infoText);
|
||||
}
|
||||
|
||||
updateMercatorInfo(tooltipText);
|
||||
}
|
||||
|
||||
// ========================
|
||||
@@ -1502,14 +1649,19 @@ const WeatherSat = (function() {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate ground map size (call after container becomes visible)
|
||||
*/
|
||||
function invalidateMap() {
|
||||
if (groundMap) {
|
||||
setTimeout(() => groundMap.invalidateSize(), 100);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Invalidate ground map size (call after container becomes visible)
|
||||
*/
|
||||
function invalidateMap() {
|
||||
setTimeout(() => {
|
||||
if (!groundMap) {
|
||||
initGroundMap();
|
||||
return;
|
||||
}
|
||||
groundMap.invalidateSize(false);
|
||||
updateGroundTrack(getSelectedPass());
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Decoder Console
|
||||
|
||||
@@ -27,7 +27,7 @@ const KIWI_SAMPLE_RATE = 12000;
|
||||
|
||||
// ============== INITIALIZATION ==============
|
||||
|
||||
function initWebSDR() {
|
||||
async function initWebSDR() {
|
||||
if (websdrInitialized) {
|
||||
if (websdrMap) {
|
||||
setTimeout(() => websdrMap.invalidateSize(), 100);
|
||||
@@ -51,11 +51,18 @@ function initWebSDR() {
|
||||
maxBoundsViscosity: 1.0,
|
||||
});
|
||||
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© OpenStreetMap contributors © CARTO',
|
||||
subdomains: 'abcd',
|
||||
maxZoom: 19,
|
||||
}).addTo(websdrMap);
|
||||
if (typeof Settings !== 'undefined' && Settings.createTileLayer) {
|
||||
await Settings.init();
|
||||
Settings.createTileLayer().addTo(websdrMap);
|
||||
Settings.registerMap(websdrMap);
|
||||
} else {
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© OpenStreetMap contributors © CARTO',
|
||||
subdomains: 'abcd',
|
||||
maxZoom: 19,
|
||||
className: 'tile-layer-cyan',
|
||||
}).addTo(websdrMap);
|
||||
}
|
||||
|
||||
// Match background to tile ocean color so any remaining edge is seamless
|
||||
mapEl.style.background = '#1a1d29';
|
||||
|
||||
Reference in New Issue
Block a user