// ui.js - UI update functions let statusTimeoutId = null; let statusHideTimeoutId = null; const STATUS_BASE_CLASS = "earth-status-message"; const STATUS_DISPLAY_MS = 3000; const STATUS_FADE_MS = 280; let statusQueue = []; let statusBusy = false; let loadingActive = false; let loadingLockedWidth = 0; let pendingLoadingMessage = ""; function createStatusEntry(message, type = "info") { return { message, type }; } function getElement(id) { return document.getElementById(id); } function setElementDisplay(element, visible, displayValue = "block") { if (!element) return; element.style.display = visible ? displayValue : "none"; } function clearStatusTimers() { if (statusTimeoutId) { clearTimeout(statusTimeoutId); statusTimeoutId = null; } if (statusHideTimeoutId) { clearTimeout(statusHideTimeoutId); statusHideTimeoutId = null; } } function clearLoadingWidthLock(statusEl) { loadingLockedWidth = 0; if (statusEl) { statusEl.style.minWidth = ""; } } function updateLoadingWidthLock(statusEl) { if (!statusEl || !loadingActive) return; const nextWidth = Math.ceil(statusEl.getBoundingClientRect().width || statusEl.scrollWidth || 0); if (nextWidth <= 0) return; loadingLockedWidth = Math.max(loadingLockedWidth, nextWidth); statusEl.style.minWidth = `${loadingLockedWidth}px`; } function buildStatusContent(statusEl, message, type) { statusEl.innerHTML = ""; const indicator = document.createElement("span"); indicator.className = "earth-status-indicator"; indicator.setAttribute("aria-hidden", "true"); const dotCount = type === "loading" ? 3 : 1; for (let i = 0; i < dotCount; i++) { const dot = document.createElement("span"); dot.className = "earth-status-dot"; indicator.appendChild(dot); } const text = document.createElement("span"); text.className = "earth-status-text"; text.textContent = message; statusEl.appendChild(indicator); statusEl.appendChild(text); } function buildPersistentErrorContent(errorEl, message) { buildStatusContent(errorEl, message, "error"); } function hideStatusElement(statusEl, onHidden) { statusEl.classList.remove("visible"); statusHideTimeoutId = setTimeout(() => { if (!statusEl.classList.contains("visible")) { setElementDisplay(statusEl, false); statusEl.className = STATUS_BASE_CLASS; statusEl.innerHTML = ""; } statusHideTimeoutId = null; if (typeof onHidden === "function") { onHidden(); } }, STATUS_FADE_MS); } function processStatusQueue() { if (loadingActive || statusBusy || statusQueue.length === 0) return; const next = statusQueue.shift(); if (!next) return; startTransientStatus(next.message, next.type); } function startTransientStatus(message, type = "info") { const statusEl = getElement("status-message"); if (!statusEl) return; clearStatusTimers(); statusBusy = true; buildStatusContent(statusEl, message, type); statusEl.className = `${STATUS_BASE_CLASS} ${type}`; setElementDisplay(statusEl, true, "inline-flex"); statusEl.offsetHeight; statusEl.classList.add("visible"); statusTimeoutId = setTimeout(() => { hideStatusElement(statusEl, () => { statusBusy = false; processStatusQueue(); }); statusTimeoutId = null; }, STATUS_DISPLAY_MS); } // Show status message export function showStatusMessage(message, type = "info") { if (loadingActive) { statusQueue.unshift(createStatusEntry(message, type)); return; } startTransientStatus(message, type); } export function queueStatusMessage(message, type = "info") { statusQueue.push(createStatusEntry(message, type)); processStatusQueue(); } // Update coordinates display export function updateCoordinatesDisplay(lat, lon, alt = 0) { const longitudeEl = getElement("longitude-value"); const latitudeEl = getElement("latitude-value"); const mouseCoordsEl = getElement("mouse-coords"); if (longitudeEl) longitudeEl.textContent = lon.toFixed(2) + "°"; if (latitudeEl) latitudeEl.textContent = lat.toFixed(2) + "°"; if (mouseCoordsEl) { mouseCoordsEl.textContent = `鼠标: ${lat.toFixed(2)}°, ${lon.toFixed(2)}°`; } } // Update zoom display export function updateZoomDisplay(zoomLevel, distance) { const percent = Math.round(zoomLevel * 100); const zoomValueEl = getElement("zoom-value"); const zoomLevelEl = getElement("zoom-level"); const slider = getElement("zoom-slider"); const cameraDistanceEl = getElement("camera-distance"); if (zoomValueEl) { const tooltip = zoomValueEl.querySelector(".tooltip"); const label = `${percent}%`; if (zoomValueEl.firstChild?.nodeType === Node.TEXT_NODE) { zoomValueEl.firstChild.nodeValue = label; } else { zoomValueEl.insertBefore(document.createTextNode(label), tooltip || null); } } if (zoomLevelEl) zoomLevelEl.textContent = "缩放: " + percent + "%"; if (slider) slider.value = zoomLevel; if (cameraDistanceEl) cameraDistanceEl.textContent = distance + " km"; } // Update earth stats export function updateEarthStats(stats) { const cableCountEl = getElement("cable-count"); const landingPointCountEl = getElement("landing-point-count"); const bgpAnomalyCountEl = getElement("bgp-anomaly-count"); const bgpCollectorCountEl = getElement("bgp-collector-count"); const bgpStatusSummaryEl = getElement("bgp-status-summary"); const terrainStatusEl = getElement("terrain-status"); const textureQualityEl = getElement("texture-quality"); if (cableCountEl) cableCountEl.textContent = stats.cableCount || 0; if (landingPointCountEl) landingPointCountEl.textContent = stats.landingPointCount || 0; if (bgpAnomalyCountEl) bgpAnomalyCountEl.textContent = stats.bgpAnomalyCount || 0; if (bgpCollectorCountEl) bgpCollectorCountEl.textContent = stats.bgpCollectorCount || 0; if (bgpStatusSummaryEl) bgpStatusSummaryEl.textContent = stats.bgpStatusSummary || "-"; if (terrainStatusEl) terrainStatusEl.textContent = stats.terrainOn ? "开启" : "关闭"; if (textureQualityEl) textureQualityEl.textContent = stats.textureQuality || "8K 卫星图"; } // Show/hide loading via status message export function setLoading(loading) { const statusEl = getElement("status-message"); if (!statusEl) return; if (loading) { clearStatusTimers(); loadingActive = true; statusBusy = false; clearLoadingWidthLock(statusEl); buildStatusContent( statusEl, pendingLoadingMessage || "正在加载...", "loading", ); pendingLoadingMessage = ""; statusEl.className = `${STATUS_BASE_CLASS} loading`; setElementDisplay(statusEl, true, "inline-flex"); statusEl.offsetHeight; statusEl.classList.add("visible"); requestAnimationFrame(() => { updateLoadingWidthLock(statusEl); }); } else { pendingLoadingMessage = ""; if (!statusEl.classList.contains("loading")) { loadingActive = false; clearLoadingWidthLock(statusEl); processStatusQueue(); return; } loadingActive = false; hideStatusElement(statusEl, () => { clearLoadingWidthLock(statusEl); statusBusy = false; processStatusQueue(); }); } } export function setLoadingMessage(title) { const statusEl = getElement("status-message"); if (!statusEl || !statusEl.classList.contains("loading")) { pendingLoadingMessage = title; return; } const textEl = statusEl.querySelector(".earth-status-text"); if (textEl) { textEl.textContent = title; requestAnimationFrame(() => { updateLoadingWidthLock(statusEl); }); } } // Show tooltip export function showTooltip(x, y, content) { const tooltip = getElement("tooltip"); if (!tooltip) return; tooltip.innerHTML = content; tooltip.style.left = x + "px"; tooltip.style.top = y + "px"; setElementDisplay(tooltip, true); } // Hide tooltip export function hideTooltip() { const tooltip = getElement("tooltip"); if (tooltip) { setElementDisplay(tooltip, false); } } // Show error message export function showError(message) { const errorEl = getElement("error-message"); if (!errorEl) return; buildPersistentErrorContent(errorEl, message); errorEl.className = `${STATUS_BASE_CLASS} earth-error-message error`; setElementDisplay(errorEl, true, "inline-flex"); errorEl.offsetHeight; errorEl.classList.add("visible"); } // Hide error message export function hideError() { const errorEl = getElement("error-message"); if (errorEl) { errorEl.classList.remove("visible"); setElementDisplay(errorEl, false); errorEl.className = "earth-error-message"; errorEl.innerHTML = ""; } } export function clearUiState() { clearStatusTimers(); statusQueue = []; statusBusy = false; loadingActive = false; pendingLoadingMessage = ""; const statusEl = getElement("status-message"); if (statusEl) { statusEl.className = STATUS_BASE_CLASS; setElementDisplay(statusEl, false); statusEl.innerHTML = ""; clearLoadingWidthLock(statusEl); } hideTooltip(); hideError(); }