1// operatorPanel.js 2// Operator selection panel for choosing visible satellite operators, supports select/reset/search. 3import { ORBIT_TYPE_LABELS } from './ui.js'; 4 5// ââ Object type breakdown from server (SATCAT OBJECT_TYPE) ââ 6// Prefers window.typeBreakdown (set by main.js from same API), falls back to own fetch 7let _typeBreakdown = null; // { payload, rocketBody, debris, unknown } 8 9async function _fetchTypeBreakdown() { 10 // If main.js already fetched it, use that 11 if (window.typeBreakdown) { 12 _typeBreakdown = window.typeBreakdown; 13 return; 14 } 15 try { 16 const res = await fetch('/api/stats/tracking'); 17 if (!res.ok) return; 18 const data = await res.json(); 19 if (data.typeBreakdown) { 20 _typeBreakdown = data.typeBreakdown; 21 } 22 } catch { /* silent â will retry on next interval */ } 23} 24// Fetch on load (slight delay to let main.js set window.typeBreakdown first) 25setTimeout(_fetchTypeBreakdown, 3000); 26setInterval(_fetchTypeBreakdown, 10 * 60 * 1000); 27 28// --- Operator panel styles are now in style.css (no JS injection needed) --- 29function injectOperatorPanelStyles() { /* styles in style.css */ } 30 31export default (function() { 32 let panel, toggleBtn, operatorList = [], selected = new Set(), onChange; 33 34 35// --- SEARCH NORMALIZER & ALIAS DB (UPDATED BLOCK) --- 36function normalize(s) { 37 let out = (s || "").toLowerCase(); 38 39 // Canonical replacements to unify common variants 40 out = out 41 .replace(/\bunited states of america\b/g, "usa") 42 .replace(/\bu\.?\s*s\.?\s*a\.?\b/g, "usa") 43 .replace(/\bunited states\b/g, "us") 44 .replace(/\bu\.?\s*s\.?\b/g, "us") 45 .replace(/\bdepartment of defense\b/g, "dod") 46 .replace(/\bministry of defence\b/g, "mod") 47 .replace(/\bpeople'?s? liberation army\b/g, "pla") 48 .replace(/\bnational aeronautics and space administration\b/g, "nasa") 49 .replace(/\bindian space research organisation\b/g, "isro") 50 .replace(/\bchina national space administration\b/g, "cnsa"); 51 52 // Strip accents, punctuation, collapse spaces 53 out = out 54 .normalize("NFD").replace(/[\u0300-\u036f]/g, "") 55 .replace(/[^a-z0-9\s]/g, " ") 56 .replace(/\s+/g, " ") 57 .trim(); 58 59 return out; 60} 61 62// Aliases map: alias/keyword => array of canonical operator names in your list. 63// TIP: keep operator strings EXACTLY as they appear in your operatorList/allOps 64const OPERATOR_ALIASES_RAW = { 65 // ===== GPS / NAVSTAR (GNSS - USA) ===== 66 "gps": [ 67 "US Space Force", 68 "United States Space Force", 69 "USAF", 70 "United States Air Force", 71 "United States Department of Defense", 72 "Department of Defense", 73 "DoD", 74 "Navstar", 75 "Navstar GPS" 76 ], 77 "navstar": [ 78 "US Space Force", 79 "United States Space Force", 80 "USAF", 81 "United States Air Force", 82 "United States Department of Defense", 83 "Department of Defense", 84 "DoD", 85 "Navstar GPS" 86 ], 87 "usg": [ 88 "United States Air Force", 89 "US Space Force", 90 "United States Department of Defense", 91 "Department of Defense", 92 "DoD", 93 "National Reconnaissance Office", 94 "National Aeronautics and Space Administration" 95 ], 96 "dod": [ 97 "United States Department of Defense", 98 "Department of Defense", 99 "DoD", 100 "US Space Force", 101 "United States Space Force", 102 "USAF", 103 "United States Air Force" 104 ], 105 "usaf": ["USAF", "United States Air Force"], 106 "ussf": ["US Space Force", "United States Space Force"], 107 108 "gnss": ["United States Air Force", "US Space Force", "European Union", "European Space Agency", "Roscosmos", "Russian Federation", "CNSA", "China National Space Administration", "BeiDou", "ISRO", "Indian Space Research Organisation", "QZSS", "Cabinet Office Japan"], 109 "navigation": ["United States Air Force", "US Space Force", "European Union", "European Space Agency", "Roscosmos", "Russian Federation", "CNSA", "China National Space Administration", "ISRO", "Indian Space Research Organisation", "Cabinet Office Japan"], 110 "satnav": ["United States Air Force", "US Space Force", "European Union", "European Space Agency", "Roscosmos", "Russian Federation", "CNSA", "China National Space Administration", "ISRO", "Indian Space Research Organisation", "Cabinet Office Japan"], 111 112 "galileo": ["European Union", "European Space Agency", "EUSPA", "European Union Agency for the Space Programme"], 113 "euspa": ["European Union", "European Space Agency", "EUSPA", "European Union Agency for the Space Programme"], 114 "glonass": ["Roscosmos", "Russian Federation"], 115 "beidou": ["CNSA", "China National Space Administration"], 116 "bds": ["CNSA", "China National Space Administration"], 117 "compass": ["CNSA", "China National Space Administration"], 118 "irnss": ["ISRO", "Indian Space Research Organisation"], 119 "navic": ["ISRO", "Indian Space Research Organisation"], 120 "qzss": ["Cabinet Office Japan"], 121 122 // SBAS (augmentation) 123 "waas": ["Federal Aviation Administration", "NOAA", "United States Department of Transportation"], 124 "egnos": ["European Union", "European Space Agency", "EUSPA", "European Union Agency for the Space Programme"], 125 "msas": ["Japan Meteorological Agency", "Cabinet Office Japan"], 126 "gagan": ["ISRO", "Indian Space Research Organisation"], 127 "sdcm": ["Roscosmos", "Russian Federation"], 128 "kass": ["Korea Aerospace Research Institute"], 129 130 // ===== LEO mega-constellations / broadband ===== 131 "starlink": ["SpaceX"], 132 "spacex": ["SpaceX"], 133 "oneweb": ["OneWeb"], 134 135 // â Kuiper: keep this STRICT so we don't accidentally catch "AMAZONAS" (GEO) 136"kuiper": [ 137 "Amazon LEO", 138 "Amazon LEO Systems", 139 "Amazon Kuiper", 140 "Amazon Kuiper Systems LLC", 141 "Project Kuiper" 142], 143"amazon leo": [ 144 "Amazon LEO", 145 "Amazon LEO Systems", 146 "Amazon Kuiper", 147 "Amazon Kuiper Systems LLC", 148 "Project Kuiper" 149], 150// â Keep "amazon" STRICT so we don't accidentally catch "AMAZONAS" (GEO) 151"amazon": [ 152 "Amazon LEO", 153 "Amazon LEO Systems", 154 "Amazon Kuiper", 155 "Amazon Kuiper Systems LLC", 156 "Project Kuiper" 157], 158 159 "o3b": ["SES"], 160 "mpower": ["SES"], 161 162 // MSS / L-band, satphone, narrowband 163 "iridium": ["Iridium"], 164 "globalstar": ["Globalstar"], 165 "orbcomm": ["ORBCOMM"], 166 "thuraya": ["Thuraya"], 167 "yahsat": ["Yahsat"], 168 169 // GEO FSS/VHTS operators 170 "inmarsat": ["Inmarsat"], 171 "intelsat": ["Intelsat"], 172 "ses": ["SES"], 173 "eutelsat": ["Eutelsat"], 174 "viasat": ["Viasat"],
175 "hughes": ["Hughes Network Systems"], 176 "jupiter": ["Hughes Network Systems"], 177 "telesat": ["Telesat"], 178 "optus": ["Optus"], 179 "arabsat": ["Arabsat"], 180 "nilesat": ["Nilesat"], 181 "hispasat": ["Hispasat"], 182 "hellas": ["Hellas Sat"], 183 "azercosmos": ["Azercosmos"], 184 "turksat": ["Turksat"], 185 "chinasat": ["China Satcom", "ChinaSat"], 186 "apstar": ["APT Satellite"], 187 "jcsat": ["SKY Perfect JSAT"], 188 "superbird": ["SKY Perfect JSAT"], 189 "bsat": ["Broadcasting Satellite System Corporation"], 190 "asiasat": ["AsiaSat"], 191 "kacific": ["Kacific"], 192 193 // ===== Earth Observation (EO), imaging, SAR ===== 194 "eo": ["European Space Agency", "CNES", "DLR", "JAXA", "ISRO", "CNSA", "USGS", "NOAA", "Planet", "Maxar", "BlackSky", "Satellogic", "ICEYE", "Capella Space", "GHGSat", "Spire"], 195 "earth observation": ["European Space Agency", "CNES", "DLR", "JAXA", "ISRO", "CNSA", "USGS", "NOAA", "Planet", "Maxar", "BlackSky", "Satellogic", "ICEYE", "Capella Space", "GHGSat", "Spire"], 196 "imaging": ["Maxar", "Planet", "BlackSky", "Satellogic", "Spire", "ICEYE", "Capella Space"], 197 "worldview": ["Maxar"], 198 "geoeye": ["Maxar"], 199 "skysat": ["Planet"], 200 "dove": ["Planet"], 201 "flock": ["Planet"], 202 "sar": ["ICEYE", "Capella Space"], 203 "capella": ["Capella Space"], 204 "iceye": ["ICEYE"], 205 "blacksky": ["BlackSky"], 206 "satellogic": ["Satellogic"], 207 "spire": ["Spire Global"], 208 "ghgsat": ["GHGSat"], 209 "copernicus": ["European Space Agency", "European Union"], 210 "sentinel": ["European Space Agency", "European Union"], 211 "landsat": ["USGS", "NASA"], 212 "modis": ["NASA", "NOAA"], 213 214 // Weather 215 "weather": ["NOAA", "EUMETSAT", "JMA", "CMA", "KMA", "ISRO"], 216 "goes": ["NOAA"], 217 "noaa": ["NOAA"], 218 "meteosat": ["EUMETSAT"], 219 "metop": ["EUMETSAT"], 220 "himawari": ["JMA"], 221 "fy": ["CMA"], "fengyun": ["CMA"], 222 "insat": ["ISRO", "Indian Space Research Organisation"], 223 "gsat": ["ISRO", "Indian Space Research Organisation"], 224 225 // Science & agencies (broad catch) 226 "esa": ["European Space Agency"], 227 "cnsa": ["CNSA", "China National Space Administration"], 228 "cnes": ["CNES"], 229 "dlr": ["DLR"], 230 "jaxa": ["JAXA"], 231 "roscosmos": ["Roscosmos", "Russian Federation"], 232 "nasa": ["National Aeronautics and Space Administration"], 233 "usgs": ["USGS"], 234 235 // Defence / intel (broad) 236 "nro": ["National Reconnaissance Office"], 237 "dod usa": ["United States Air Force", "US Space Force", "National Reconnaissance Office"], 238 "military": ["United States Air Force", "US Space Force", "National Reconnaissance Office", "Roscosmos", "Russian Federation", "Ministry of Defence (UK)", "People's Liberation Army", "CNSA", "China National Space Administration"], 239 240 // Misc popular programs 241 "skynet": ["UK Ministry of Defence", "Ministry of Defence (UK)"], 242 "sicral": ["Italian Ministry of Defence"], 243 "aeolus": ["European Space Agency"], 244 "ocean sat": ["ISRO", "Indian Space Research Organisation"], 245 "hubble": ["National Aeronautics and Space Administration", "European Space Agency"] 246}; 247 248 249// Build a robust reverse alias index that tolerates name variants 250function buildAliasIndex(allOps) { 251 const list = Array.isArray(allOps) ? allOps : []; 252 253 // Support list of strings OR objects with .name 254 const names = list 255 .map(o => (typeof o === 'string' ? o : (o && o.name) ? o.name : '')) 256 .filter(Boolean); 257 258 const normNames = names.map(n => ({ raw: n, norm: normalize(n) })); 259 260 // Helper: does opName ~ target by equality OR token containment? 261 function matchesTarget(opNorm, targetRaw) { 262 const t = normalize(targetRaw); 263 if (!t) return false; 264 if (opNorm === t) return true; 265 // token containment: every word in t must exist in opNorm 266 const words = t.split(' '); 267 return words.every(w => opNorm.includes(w)); 268 } 269 270 const reverse = new Map(); // alias_normalized -> Set(visibleOperatorName) 271 272 for (const [alias, targets] of Object.entries(OPERATOR_ALIASES_RAW)) { 273 const aliasKey = normalize(alias); 274 for (const target of targets) { 275 for (const { raw, norm } of normNames) { 276 if (matchesTarget(norm, target)) {
277 if (!reverse.has(aliasKey)) reverse.set(aliasKey, new Set()); 278 reverse.get(aliasKey).add(raw); 279 } 280 } 281 } 282 } 283 return reverse; 284} 285 286 287function buildPanel(topOps, allOps) { 288 if (!Array.isArray(topOps)) topOps = []; 289 if (!Array.isArray(allOps)) allOps = []; 290 291 injectOperatorPanelStyles(); 292 293 operatorList = allOps; 294 295 panel = document.getElementById('operator-panel'); 296 if (!panel) { 297 panel = document.createElement('div'); 298 panel.id = 'operator-panel'; 299 panel.classList.add('panel-main'); 300 document.body.appendChild(panel); 301 } else { 302 panel.classList.add('panel-main'); 303 } 304 305panel.innerHTML = ` 306 <div class="op-header"> 307 <span>Operators</span> 308 <button id="op-close-btn" title="Close panel">â</button> 309 </div> 310 311 <div id="op-impact" class="op-impact"></div> 312 313 <div class="op-search-wrap"> 314 <input type="text" id="op-search" placeholder="Search operatorsâ¦" /> 315 </div> 316 <div id="op-search-count" class="op-search-count"></div> 317 318 <div class="op-actions"> 319 <button id="op-clear-filter" class="op-action-clear" title="Clear operator filter (show all)">Clear filter</button> 320 </div> 321 322 <div id="op-presets" class="op-presets"></div> 323 <div class="op-hint">Tip: Shift+Click adds/removes operators. Press <span class="op-hint-key">ESC</span> to clear search.</div> 324 325 326 <div id="op-top-list"></div> 327 <div id="op-list"></div> 328 <div class="op-intersite-links" style="margin-top:14px;padding:10px 8px 4px;border-top:1px solid rgba(255,224,96,0.1);"> 329 <a href="/satellites-by-operator" class="op-site-link" style="display:block;color:#60eafe;font-size:12.5px;text-decoration:none;margin-bottom:6px;font-family:'JetBrains Mono',monospace;">ð Full Operator Directory â</a> 330 <a href="/satellites-by-country" class="op-site-link" style="display:block;color:#60eafe;font-size:12.5px;text-decoration:none;margin-bottom:6px;font-family:'JetBrains Mono',monospace;">ð Satellites by Country â</a> 331 <a href="/how-many-satellites-in-orbit" class="op-site-link" style="display:block;color:#60eafe;font-size:12.5px;text-decoration:none;font-family:'JetBrains Mono',monospace;">ð°ï¸ How Many Satellites in Orbit? â</a> 332 </div> 333`; 334 335 336 panel.style.display = ''; 337 338 // ---- Close button ---- 339 panel.querySelector('#op-close-btn').addEventListener('click', () => { 340 panel.classList.remove('visible'); 341 const tb = document.getElementById('show-operator-panel-btn'); 342 if (tb) tb.classList.remove('glow'); 343 }); 344 345 // ---- Top operators (now interactive) ---- 346 const topList = panel.querySelector('#op-top-list'); 347 topList.innerHTML = ` 348 <div class="top-operators">Top 7 Operators</div> 349 <div class="top-operator-list"></div> 350 `; 351 352 const topOperatorList = topList.querySelector('.top-operator-list'); 353 354 topOps.forEach(op => { 355 const btn = document.createElement('button'); 356 btn.type = 'button'; 357 btn.className = 'top-operator op-chip'; 358 btn.setAttribute('data-operator', op.name); 359 btn.title = 'Click: Solo ⢠Shift+Click: Add/Remove'; 360 361 // Mini donut ring showing fleet share 362 const totalSats = topOps.reduce((s, o) => s + (o.count || 0), 0) || 1; 363 const share = Math.max(0.04, Math.min(1, (op.count || 0) / totalSats)); 364 const r = 16, stroke = 3.5; 365 const circ = 2 * Math.PI * r; 366 const dashLen = share * circ; 367 const donutSvg = `<svg class="op-donut-ring" width="38" height="38" viewBox="0 0 38 38"> 368 <circle cx="19" cy="19" r="${r}" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="${stroke}"/> 369 <circle cx="19" cy="19" r="${r}" fill="none" stroke="#60eafe" stroke-width="${stroke}" 370 stroke-dasharray="${dashLen.toFixed(1)} ${(circ - dashLen).toFixed(1)}" 371 stroke-dashoffset="${(circ * 0.25).toFixed(1)}" stroke-linecap="round" 372 style="filter:drop-shadow(0 0 3px rgba(96,234,254,0.4));transition:stroke-dasharray 0.5s ease;"/> 373 <text x="19" y="20.5" text-anchor="middle" fill="#ffe060" font-family="Orbitron,JetBrains Mono,monospace" font-size="7.5" font-weight="800">${Math.round(share * 100)}%</text> 374 </svg>`; 375 376 btn.innerHTML = ` 377 ${donutSvg}
378 <span class="top-operator-name">${op.name}</span> 379 <span class="badge">${op.count}</span> 380 `; 381 382 btn.addEventListener('click', (ev) => { 383 const name = op.name; 384 if (!name) return; 385 386 if (ev.shiftKey) { 387 if (selected.has(name)) selected.delete(name); 388 else selected.add(name); 389 } else { 390 if (selected.size === 1 && selected.has(name)) { 391 selected.clear(); // clicking same solo clears back to ALL 392 } else { 393 selected.clear(); 394 selected.add(name); 395 } 396 } 397 398 renderList(); 399 renderImpactStrip(); 400 updateUrlSelectedOperators(Array.from(selected)); 401 onChange(Array.from(selected)); 402 }); 403 404 btn.addEventListener('mouseenter', () => showIntelCard(op.name, btn)); 405 btn.addEventListener('mouseleave', hideIntelCard); 406 407 topOperatorList.appendChild(btn); 408 }); 409 410 // ---- Presets ---- 411 const presetsEl = panel.querySelector('#op-presets'); 412 if (presetsEl) { 413 const PRESETS = [ 414 { key: 'starlink', label: 'Starlink' }, 415 { key: 'oneweb', label: 'OneWeb' }, 416 { key: 'kuiper', label: 'Amazon LEO' }, 417 { key: 'gnss', label: 'GNSS' }, 418 { key: 'weather', label: 'Weather' }, 419 { key: 'eo', label: 'Earth Obs' }, 420 ]; 421 422 presetsEl.innerHTML = PRESETS.map(p => 423 `<button type="button" class="op-preset-btn" data-preset="${p.key}">${p.label}</button>` 424 ).join(''); 425 426 presetsEl.querySelectorAll('.op-preset-btn').forEach(btn => { 427 btn.addEventListener('click', (ev) => { 428 const preset = btn.getAttribute('data-preset'); 429 430 const ops = getPresetOperators(preset, operatorList); 431 if (ev.shiftKey) ops.forEach(o => selected.add(o)); 432 else { 433 selected.clear(); 434 ops.forEach(o => selected.add(o)); 435 } 436 437 renderList(); 438 renderImpactStrip(); 439 updateUrlSelectedOperators(Array.from(selected)); 440 onChange(Array.from(selected)); 441}); 442 443 }); 444 } 445 446 // ---- Search ---- 447 const searchInput = panel.querySelector('#op-search'); 448 searchInput.oninput = filterList; 449 450 // ESC clears search and restores collapse state snapshot 451searchInput.addEventListener('keydown', (ev) => { 452 if (ev.key !== 'Escape') return; 453 if (!searchInput.value) return; 454 ev.preventDefault(); 455 searchInput.value = ''; 456 filterList({ target: { value: '' } }); 457}); 458 459 460 // Buttons 461panel.querySelector('#op-clear-filter').onclick = resetAll; 462 463 464 // Intel card singleton 465 ensureIntelCard(); 466 467 renderList(); 468 renderImpactStrip(); 469} 470 471let OPERATOR_ALIAS_INDEX = new Map(); // filled in init() 472 473// --- Intel Card (hover tooltip) --- 474let _opIntelCardEl = null; 475let _opIntelHideTimer = null; 476 477function ensureIntelCard() { 478 if (_opIntelCardEl && document.body.contains(_opIntelCardEl)) return; 479 480 const el = document.createElement('div'); 481 el.id = 'operatorIntelCard'; 482 el.style.display = 'none'; 483 el.style.position = 'fixed'; 484 el.style.zIndex = '3000'; 485 el.style.width = '330px'; 486 el.style.maxWidth = '88vw'; 487 el.style.pointerEvents = 'auto'; 488 489 // Theme-consistent styling (matches Orbital Radar HUD) 490 el.style.background = 'linear-gradient(120deg, rgba(24,25,41,0.96) 88%, rgba(96,234,254,0.10) 100%)'; 491 el.style.border = '1.5px solid rgba(255,224,96,0.55)'; 492 el.style.borderRadius = '14px'; 493 el.style.boxShadow = '0 10px 34px rgba(0,0,0,0.55), 0 0 0 1px rgba(96,234,254,0.10) inset'; 494 el.style.backdropFilter = 'blur(6px)'; 495 496 el.innerHTML = ` 497 <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:12px 12px 8px 12px;"> 498 <div style="min-width:0;"> 499 <div id="opIntelTitle" style="font-family:Orbitron,JetBrains Mono,monospace;font-weight:900;letter-spacing:1px;color:#ffe060;font-size:15px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"> 500 Operator Intel 501 </div> 502 <div id="opIntelSub" style="margin-top:4px;font-family:JetBrains Mono,monospace;font-size:12px;color:rgba(255,255,255,0.70);"> 503 Hover an operator to preview fleet + orbits 504 </div> 505 </div> 506 <button id="opIntelClose" title="Close" style="border:none;background:transparent;color:#ffe060;font-size:18px;line-height:1;cursor:pointer;padding:2px 6px;">Ã</button> 507 </div> 508 509 <div style="padding:0 12px 12px 12px;"> 510 <div id="opIntelStats" style="display:grid;grid-template-columns:1fr;gap:10px;"></div> 511 <div id="opIntelHint" style="margin-top:10px;font-family:JetBrains Mono,monospace;font-size:11.5px;color:rgba(255,224,96,0.72);letter-spacing:0.4px;"> 512 Tip: hover top operators for fast intel. 513 </div> 514 </div> 515 `; 516 517 // Donât hide when moving from the button into the card 518 el.addEventListener('mouseenter', () => { 519 if (_opIntelHideTimer) clearTimeout(_opIntelHideTimer); 520 _opIntelHideTimer = null; 521 }); 522 el.addEventListener('mouseleave', () => hideIntelCard()); 523 524 el.querySelector('#opIntelClose').onclick = () => { 525 el.style.display = 'none'; 526 }; 527 528 document.body.appendChild(el); 529 _opIntelCardEl = el; 530} 531
532function showIntelCard(operatorName, anchorEl) { 533 ensureIntelCard(); 534 if (!_opIntelCardEl) return; 535 536 if (_opIntelHideTimer) clearTimeout(_opIntelHideTimer); 537 _opIntelHideTimer = null; 538 539 const op = operatorName || 'Unknown'; 540 541 // Fleet count (whatâs currently in Cesium, not total catalog) 542 const byProvider = window.cesiumSatellitesByProvider || {}; 543 const fleet = Array.isArray(byProvider[op]) ? byProvider[op].length : 0; 544 545 // Orbit breakdown if available 546 const orbitCountsByProvider = window.operatorOrbitCountsByProvider || {}; 547 const orbit = orbitCountsByProvider[op] || {}; 548 const LEO = Number(orbit.LEO || 0); 549 const MEO = Number(orbit.MEO || 0); 550 const GEO = Number(orbit.GEO || 0); 551 const HEO = Number(orbit.HEO || 0); 552 const totalOrbit = LEO + MEO + GEO + HEO; 553 554 const titleEl = _opIntelCardEl.querySelector('#opIntelTitle'); 555 const subEl = _opIntelCardEl.querySelector('#opIntelSub'); 556 const statsEl = _opIntelCardEl.querySelector('#opIntelStats'); 557 558 titleEl.textContent = op; 559 subEl.textContent = `${fleet.toLocaleString()} objects currently rendered`; 560 561 const pct = (n) => totalOrbit ? Math.round((n / totalOrbit) * 100) : 0; 562 563 // Build orbit chips (only show non-zero, but still render an empty state) 564 const orbitRows = [ 565 { k: 'LEO', v: LEO }, 566 { k: 'MEO', v: MEO }, 567 { k: 'GEO', v: GEO }, 568 { k: 'HEO', v: HEO }, 569 ].filter(x => x.v > 0); 570 571 const orbitHtml = orbitRows.length ? orbitRows.map(x => ` 572 <div style="display:flex;align-items:center;justify-content:space-between;gap:10px;"> 573 <div style="font-family:JetBrains Mono,monospace;font-size:12px;color:rgba(255,255,255,0.85);">${x.k}</div> 574 <div style="display:flex;align-items:center;gap:10px;"> 575 <div style="width:120px;height:8px;border-radius:999px;background:rgba(255,255,255,0.10);overflow:hidden;"> 576 <div style="height:100%;width:${pct(x.v)}%;background:rgba(96,234,254,0.85);box-shadow:0 0 10px rgba(96,234,254,0.25);"></div> 577 </div> 578 <div style="font-family:JetBrains Mono,monospace;font-size:12px;color:#ffe060;min-width:58px;text-align:right;"> 579 ${x.v.toLocaleString()} (${pct(x.v)}%) 580 </div> 581 </div> 582 </div> 583 `).join('') : ` 584 <div style="font-family:JetBrains Mono,monospace;font-size:12px;color:rgba(255,255,255,0.55);"> 585 Orbit breakdown unavailable for this operator. 586 </div> 587 `; 588 589 statsEl.innerHTML = ` 590 <div style="padding:10px 10px;border-radius:12px;border:1px solid rgba(255,224,96,0.18);background:rgba(10,14,28,0.55);"> 591 <div style="font-family:Orbitron,JetBrains Mono,monospace;font-weight:900;letter-spacing:0.9px;color:#60eafe;font-size:12px;margin-bottom:8px;"> 592 ORBIT DISTRIBUTION 593 </div> 594 <div style="display:flex;flex-direction:column;gap:8px;"> 595 ${orbitHtml} 596 </div> 597 </div> 598 `; 599 600 // Position the card near the hovered operator, biased to the LEFT of the panel 601 const cardW = 330; 602 const margin = 12; 603 604 const a = anchorEl?.getBoundingClientRect?.() || { top: 160, left: window.innerWidth - 360, right: window.innerWidth - 40, width: 320, height: 28 }; 605 const p = panel?.getBoundingClientRect?.() || { left: window.innerWidth - 420, right: window.innerWidth - 20, top: 110, bottom: window.innerHeight - 20 }; 606 607 let left = (p.left - cardW - margin); 608 if (left < 8) left = (p.right + margin); // fallback to right side if no room 609 610 let top = Math.min(Math.max(a.top - 10, 10), window.innerHeight - 240); 611 _opIntelCardEl.style.left = `${Math.round(left)}px`; 612 _opIntelCardEl.style.top = `${Math.round(top)}px`; 613 _opIntelCardEl.style.display = 'block'; 614} 615 616function hideIntelCard() { 617 if (!_opIntelCardEl) return; 618 if (_opIntelHideTimer) clearTimeout(_opIntelHideTimer); 619 620 _opIntelHideTimer = setTimeout(() => { 621 if (_opIntelCardEl) _opIntelCardEl.style.display = 'none'; 622 }, 120); 623} 624 625 626 627// --- URL share + presets + impact helpers --- 628function getUrlSelectedOperators(allOps) { 629 try { 630 const url = new URL(window.location.href); 631 const ops = (url.searchParams.get('ops') || '').trim(); 632 if (!ops) return null; 633 634 const decoded = ops.split(',').map(s =>
634 decodeURIComponent(s.trim())).filter(Boolean); 635 const valid = decoded.filter(op => allOps.includes(op)); 636 return valid.length ? valid : null; 637 } catch (e) { 638 return null; 639 } 640} 641 642function updateUrlSelectedOperators(selectedOps) { 643 try { 644 const url = new URL(window.location.href); 645 if (!selectedOps || !selectedOps.length) { 646 url.searchParams.delete('ops'); 647 } else { 648 const sorted = [...selectedOps].sort((a, b) => a.localeCompare(b)); 649 url.searchParams.set('ops', sorted.map(s => encodeURIComponent(s)).join(',')); 650 } 651 window.history.replaceState({}, '', url.toString()); 652 } catch (e) {} 653} 654 655function getPresetOperators(presetKey, allOps) { 656 const key = String(presetKey || '').toLowerCase().trim(); 657 const out = new Set(); 658 659const aliasKeys = { 660 starlink: ['starlink', 'spacex'], 661 oneweb: ['oneweb'], 662kuiper: ['amazon leo', 'kuiper'], 663 'guowang-qianfan': ['qianfan', 'spacesail', 'guowang', 'china satnet', 'hulianwang'], 664 qianfan: ['qianfan', 'spacesail'], 665 guowang: ['guowang', 'china satnet', 'hulianwang'], 666 china: ['qianfan', 'spacesail', 'guowang', 'china satnet', 'hulianwang', 'beidou', 'gaofen', 'yaogan', 'jilin', 'zhongxing', 'chinasat', 'apstar', 'casc', 'cnsa', 'fengyun', 'shijian', 'shiyan', 'tianhui', 'yunhai', 'haiyang', 'tianlian', 'tiantong', 'geespace', 'geely', 'galaxy space', 'centispace', 'piesat'], 667 gnss: ['gnss', 'gps', 'galileo', 'glonass', 'beidou', 'navic', 'qzss'], 668 weather: ['weather', 'noaa', 'eumetsat', 'jma', 'cma', 'kma'], 669 eo: ['eo', 'earth observation', 'imaging', 'sar', 'copernicus', 'sentinel', 'landsat', 'planet', 'maxar', 'blacksky', 'iceye', 'capella', 'spire'], 670 gps: ['gps', 'navstar'], 671 galileo: ['galileo'], 672 beidou: ['beidou', 'compass'], 673}; 674 675 676 const tryAliases = (aliases) => { 677 (aliases || []).forEach(a => { 678 const aNorm = normalize(a); 679 for (const [aliasKey, targets] of OPERATOR_ALIAS_INDEX.entries()) { 680 if (aliasKey === aNorm || aliasKey.startsWith(aNorm) || aNorm.startsWith(aliasKey)) { 681 targets.forEach(t => out.add(t)); 682 } 683 } 684 }); 685 }; 686 687 if (aliasKeys[key]) tryAliases(aliasKeys[key]); 688 689 // fallback (keeps presets working even if alias index is imperfect) 690 if (key === 'starlink') allOps.filter(o => /spacex|starlink/i.test(o)).forEach(o => out.add(o)); 691 if (key === 'oneweb') allOps.filter(o => /oneweb/i.test(o)).forEach(o => out.add(o)); 692 if (key === 'kuiper') { 693 allOps 694 .filter(o => /\bkuiper\b|\bproject kuiper\b|\bamazon leo\b/i.test(o)) 695 .forEach(o => out.add(o)); 696} 697 if (key === 'gps') allOps.filter(o => /navstar|\bgps\b/i.test(o)).forEach(o => out.add(o)); 698 if (key === 'beidou') allOps.filter(o => /beidou|compass/i.test(o)).forEach(o => out.add(o)); 699 if (key === 'guowang-qianfan') allOps.filter(o => /qianfan|spacesail|guowang|satnet|hulianwang/i.test(o)).forEach(o => out.add(o)); 700 if (key === 'qianfan') allOps.filter(o => /qianfan|spacesail/i.test(o)).forEach(o => out.add(o)); 701 if (key === 'guowang') allOps.filter(o => /guowang|satnet|hulianwang/i.test(o)).forEach(o => out.add(o)); 702 // 'china' preset (/china hub globe CTA): every Chinese operator / name-family 703 // bucket the panel derives â mirrors the server-side programme matchers. 704 if (key === 'china') allOps.filter(o => /qianfan|spacesail|guowang|satnet|hulianwang|beidou|gaofen|yaogan|jilin|zhongxing|chinasat|apstar|\bcasc\b|cnsa|fengyun|shijian|shiyan|tianhui|yunhai|haiyang|tianlian|tiantong|geespace|geely|galaxy space|centispace|piesat|china/i.test(o)).forEach(o => out.add(o)); 705 706 707 return Array.from(out).filter(o => allOps.includes(o)); 708} 709 710function computeImpact(selectedOps) { 711 const sel = (selectedOps && selectedOps.length) ? new Set(selectedOps) : null; 712 713 const byProvider = window.cesiumSatellitesByProvider || {}; 714 const orbitCountsByProvider = window.operatorOrbitCountsByProvider || null; 715 716 let satsVisible = 0; 717 let satsTotal = 0; 718 719 const orbitMix = { LEO: 0, MEO: 0, GEO: 0, HEO: 0 }; 720 721 for (const [provider, arr] of Object.entries(byProvider)) { 722 const n = Array.isArray(arr) ? arr.length : 0; 723 satsTotal += n; 724 725 const included = !sel || sel.has(provider); 726 if (!included) continue; 727 728 satsVisible += n; 729 730 if (orbitCountsByProvider && orbitCountsByProvider[provider]) { 731 const oc = orbitCountsByProvider[provider]; 732 for (const k of Object.keys(orbitMix)) orbitMix[k] += (oc[k] || 0); 733 } else { 734 const topOrbit = (window.operatorOrbitMap && window.operatorOrbitMap[provider]) ? window.operatorOrbitMap[provider] : 'LEO'; 735 orbitMix[topOrbit] = (orbitMix[topOrbit] || 0) + n; 736 } 737 } 738
739 // ââ Use OBJECT_TYPE breakdown from server (SATCAT classification) ââ 740 // This properly counts PAY vs DEB vs R/B from the authoritative SATCAT data 741 let debrisCount = 0; 742 let rocketBodyCount = 0; 743 const satOnlyVisible = satsVisible; 744 745 if (_typeBreakdown) { 746 debrisCount = (_typeBreakdown.debris || 0); 747 rocketBodyCount = (_typeBreakdown.rocketBody || 0); 748 if (window.trackingTotalFromDB > 0) { 749 satsTotal = window.trackingTotalFromDB; 750 satsVisible = satsTotal; // all are visible when no filter 751 } 752 } else if (window.trackingTotalFromDB > 0) { 753 // Fallback: old subtraction method if type breakdown not yet loaded 754 satsTotal = window.trackingTotalFromDB; 755 const totalSatCount = Object.values(byProvider).reduce((s, a) => s + (Array.isArray(a) ? a.length : 0), 0); 756 debrisCount = Math.max(0, satsTotal - totalSatCount); 757 satsVisible += debrisCount; 758 } 759 const pct = satsTotal ? Math.round((satsVisible / satsTotal) * 100) : 0; 760 return { satsVisible, satsTotal, pct, orbitMix, debrisCount, rocketBodyCount, satOnlyVisible }; 761} 762 763let _lastImpactVisible = -1; 764 765function animateCountUp(el, target, duration = 600) { 766 const start = parseInt(el.textContent.replace(/,/g, ''), 10) || 0; 767 if (start === target) return; 768 el.classList.add('op-counting'); 769 const startTime = performance.now(); 770 function tick(now) { 771 const t = Math.min(1, (now - startTime) / duration); 772 const eased = 1 - Math.pow(1 - t, 3); // ease-out cubic 773 const cur = Math.round(start + (target - start) * eased); 774 el.textContent = cur.toLocaleString(); 775 if (t < 1) requestAnimationFrame(tick); 776 else { 777 el.classList.remove('op-counting'); 778 el.classList.add('op-glow'); 779 setTimeout(() => el.classList.remove('op-glow'), 650); 780 } 781 } 782 requestAnimationFrame(tick); 783} 784 785function renderImpactStrip() { 786 const el = panel && panel.querySelector('#op-impact'); 787 if (!el) return; 788 789 const selectedOps = Array.from(selected || []); 790 const impact = computeImpact(selectedOps); 791 792 const totalMix = (impact.orbitMix.LEO + impact.orbitMix.MEO + impact.orbitMix.GEO + impact.orbitMix.HEO) || 0; 793 const barSeg = (k) => { 794 const v = impact.orbitMix[k] || 0; 795 const w = totalMix ? Math.max(2, Math.round((v / totalMix) * 100)) : 0; 796 return `<span class="op-impact-bar-seg op-impact-${k.toLowerCase()}" style="width:${w}%" title="${k}: ${v.toLocaleString()}"></span>`; 797 }; 798 799 // Build type breakdown line (satellites · rocket bodies · debris) 800 const hasBrk = _typeBreakdown && (impact.debrisCount > 0 || impact.rocketBodyCount > 0); 801 let debrisLine = ''; 802 if (hasBrk) { 803 const payCount = (_typeBreakdown.payload || 0) + (_typeBreakdown.unknown || 0); 804 const parts = [ 805 `<span class="op-brk-sats">${payCount.toLocaleString()} satellites</span>` 806 ]; 807 if (impact.rocketBodyCount > 0) { 808 parts.push(`<span class="op-brk-sep">·</span><span class="op-brk-rb">${impact.rocketBodyCount.toLocaleString()} rocket bodies</span>`); 809 } 810 if (impact.debrisCount > 0) { 811 parts.push(`<span class="op-brk-sep">·</span><span class="op-brk-debris">${impact.debrisCount.toLocaleString()} debris</span>`); 812 } 813 debrisLine = `<div class="op-impact-breakdown">${parts.join('')}</div>`; 814 } else if (impact.debrisCount > 0) { 815 // Fallback: old style if type breakdown not loaded 816 debrisLine = `<div class="op-impact-breakdown"> 817 <span class="op-brk-sats">${impact.satOnlyVisible.toLocaleString()} satellites</span> 818 <span class="op-brk-sep">·</span> 819 <span class="op-brk-debris">${impact.debrisCount.toLocaleString()} debris</span> 820 </div>`; 821 } 822 823 el.innerHTML = ` 824 <div class="op-impact-row"> 825 <span class="op-impact-label">Tracking</span> 826 <span class="op-impact-value" id="op-impact-count">${_lastImpactVisible >= 0 ? _lastImpactVisible.toLocaleString() : impact.satsVisible.toLocaleString()}</span> 827 <span class="op-impact-sub">of ${impact.satsTotal.toLocaleString()} objects</span> 828 </div> 829 ${debrisLine} 830 831 <div class="op-impact-mix"> 832 <div class="op-impact-mix-label">Orbit mix</div> 833 <div class="op-impact-bar"> 834 ${barSeg('LEO')}${barSeg('MEO')}${barSeg('GEO')}${barSeg('HEO')} 835 </div> 836 <div class="op-impact-legend">
837 <span><i class="op-dot leo"></i>LEO</span> 838 <span><i class="op-dot meo"></i>MEO</span> 839 <span><i class="op-dot geo"></i>GEO</span> 840 <span><i class="op-dot heo"></i>HEO</span> 841 </div> 842 </div> 843 `; 844 845 // Animate the count 846 const countEl = el.querySelector('#op-impact-count'); 847 if (countEl && impact.satsVisible !== _lastImpactVisible) { 848 animateCountUp(countEl, impact.satsVisible); 849 } 850 _lastImpactVisible = impact.satsVisible; 851 852 // Keep badge & chip glow in sync 853 updateFilterBadge(); 854 updateChipGlow(); 855} 856 857 858 859// --- Active filter badge on sidebar button --- 860function updateFilterBadge() { 861 const btn = document.getElementById('show-operator-panel-btn'); 862 if (!btn) return; 863 // Ensure button can host the badge 864 if (getComputedStyle(btn).position === 'static') btn.style.position = 'relative'; 865 let badge = btn.querySelector('.op-filter-badge'); 866 if (selected.size > 0) { 867 if (!badge) { 868 badge = document.createElement('span'); 869 badge.className = 'op-filter-badge'; 870 btn.appendChild(badge); 871 } 872 badge.textContent = String(selected.size); 873 } else if (badge) { 874 badge.remove(); 875 } 876} 877 878// --- Top operator chip selected glow --- 879function updateChipGlow() { 880 if (!panel) return; 881 panel.querySelectorAll('.top-operator.op-chip').forEach(btn => { 882 const name = btn.getAttribute('data-operator'); 883 if (name && selected.has(name)) { 884 btn.classList.add('op-selected'); 885 } else { 886 btn.classList.remove('op-selected'); 887 } 888 }); 889} 890 891function renderList() { 892 const container = panel.querySelector('#op-list'); 893 container.innerHTML = ''; 894 895 // Always render these orbit buckets (even if empty) 896 const ORBIT_ORDER = ['LEO', 'MEO', 'GEO', 'HEO']; 897 898 // Build grouped operators (default empty arrays so headings always render) 899 const grouped = Object.fromEntries(ORBIT_ORDER.map(k => [k, []])); 900 901 operatorList.forEach(op => { 902 const orbit = window.operatorOrbitMap?.[op] || 'LEO'; // Fallback LEO 903 const key = grouped[orbit] ? orbit : 'LEO'; // Safety fallback 904 grouped[key].push(op); 905 }); 906 907 // Render groups in fixed order 908 ORBIT_ORDER.forEach(orbit => { 909 const section = document.createElement('div'); 910 section.className = 'op-group'; 911 912 // Compute counts for this orbit group 913 const opsInGroup = grouped[orbit] || []; 914 const opCount = opsInGroup.length; 915 const byProvider = window.cesiumSatellitesByProvider || {}; 916 const orbitObjCount = opsInGroup.reduce((sum, opName) => { 917 const arr = byProvider[opName]; 918 return sum + (Array.isArray(arr) ? arr.length : 0); 919 }, 0); 920 921 section.innerHTML = ` 922 <div class="op-group-header" data-orbit="${orbit}"> 923 <span>${orbit} â ${ORBIT_TYPE_LABELS[orbit] || orbit}<span class="op-group-stats">${opCount} operators · ${orbitObjCount.toLocaleString()} objects</span></span> 924 <button class="op-group-toggle" data-orbit="${orbit}">â¼</button> 925 </div> 926 <div class="op-group-list" data-orbit="${orbit}"></div> 927 `; 928 929 const list = section.querySelector('.op-group-list'); 930 const ops = grouped[orbit] || []; 931 932 if (!ops.length) { 933 const empty = document.createElement('div'); 934 empty.style.cssText = 'padding:6px 10px;color:rgba(255,255,255,0.55);font-size:13px;'; 935 empty.textContent = `No operators currently classified as ${orbit}.`; 936 list.appendChild(empty); 937 } else { 938 ops.forEach(op => { 939 const checked = selected.has(op) ? 'checked' : ''; 940 const item = document.createElement('label'); 941 item.style.display = 'block'; 942 item.setAttribute('data-operator', op.toLowerCase()); 943 item.innerHTML = ` 944 <input type="checkbox" value="${op}" ${checked} /> 945 ${op} 946 `; 947 item.querySelector('input').onchange = onCheckbox; 948 list.appendChild(item); 949 }); 950 } 951 952 container.appendChild(section); 953 954 // Collapse by default except LEO 955 if (orbit !== 'LEO') list.style.display = 'none'; 956 section.querySelector('.op-group-toggle').onclick = function() { 957 list.style.display = list.style.display === 'none' ? '' : 'none'; 958 }; 959 }); 960 961 // OPTIONAL: re-apply current search term after rendering 962 const searchTerm = panel.querySelector('#op-search').value.toLowerCase(); 963 if (searchTerm) filterList({ target: { value: searchTerm } }); 964} 965 966 967let _searchWasActive = false;
968let _collapseSnapshot = {}; 969 970function filterList(e) { 971 const raw = (e && e.target && e.target.value) || ""; 972 const query = normalize(raw); 973 const terms = query.split(" ").filter(Boolean); 974 975 const groupLists = Array.from(panel.querySelectorAll('.op-group-list')); 976 const groupHeaders = Array.from(panel.querySelectorAll('.op-group-header')); 977 978 if (terms.length && !_searchWasActive) { 979 _searchWasActive = true; 980 _collapseSnapshot = {}; 981 groupLists.forEach(gl => { 982 const orbit = gl.getAttribute('data-orbit'); 983 _collapseSnapshot[orbit] = (gl.style.display === 'none') ? 'none' : ''; 984 }); 985 } 986 987 if (!terms.length) { 988 panel.querySelectorAll('#op-list label').forEach(lbl => lbl.style.display = ''); 989 990 if (_searchWasActive) { 991 groupLists.forEach(gl => { 992 const orbit = gl.getAttribute('data-orbit'); 993 if (orbit && orbit in _collapseSnapshot) gl.style.display = _collapseSnapshot[orbit]; 994 }); 995 996 groupHeaders.forEach(h => { 997 const badge = h.querySelector('.op-group-match'); 998 if (badge) badge.remove(); 999 }); 1000 1001 _searchWasActive = false; 1002 _collapseSnapshot = {}; 1003 } 1004 updateSearchCount(); 1005 return; 1006 } 1007 1008 panel.querySelectorAll('#op-list label').forEach(lbl => { 1009 const opName = lbl.querySelector('.op-item-name')?.textContent?.trim() || lbl.textContent.trim(); 1010 const opNorm = normalize(opName); 1011 const tokens = new Set(opNorm.split(" ")); 1012 1013 const matchesAll = terms.every(t => 1014 opNorm.includes(t) || 1015 Array.from(tokens).some(tok => tok.startsWith(t)) || 1016 (() => { 1017 for (const [aliasKey, targets] of OPERATOR_ALIAS_INDEX.entries()) { 1018 if (aliasKey === t || aliasKey.startsWith(t)) { 1019 if (targets.has(opName)) return true; 1020 } 1021 } 1022 return false; 1023 })() 1024 ); 1025 1026 lbl.style.display = matchesAll ? '' : 'none'; 1027 }); 1028 1029 groupLists.forEach(gl => { 1030 const orbit = gl.getAttribute('data-orbit'); 1031 const visibleCount = Array.from(gl.querySelectorAll('label')).filter(l => l.style.display !== 'none').length; 1032 1033 const header = panel.querySelector(`.op-group-header[data-orbit="${orbit}"]`); 1034 if (header) { 1035 let badge = header.querySelector('.op-group-match'); 1036 if (!badge) { 1037 badge = document.createElement('span'); 1038 badge.className = 'op-group-match'; 1039 header.appendChild(badge); 1040 } 1041 badge.textContent = String(visibleCount); 1042 } 1043 1044 gl.style.display = visibleCount > 0 ? '' : 'none'; 1045 }); 1046 1047 // Update search results count 1048 updateSearchCount(); 1049} 1050 1051function updateSearchCount() { 1052 const countEl = panel && panel.querySelector('#op-search-count'); 1053 if (!countEl) return; 1054 const searchVal = (panel.querySelector('#op-search')?.value || '').trim(); 1055 if (!searchVal) { 1056 countEl.textContent = ''; 1057 return; 1058 } 1059 const allLabels = panel.querySelectorAll('#op-list label'); 1060 const visible = Array.from(allLabels).filter(l => l.style.display !== 'none').length; 1061 const total = allLabels.length; 1062 countEl.textContent = `${visible} of ${total} operators match`; 1063} 1064 1065 1066 function onCheckbox(e) { 1067 const val = e.target.value; 1068 if (e.target.checked) selected.add(val); 1069 else selected.delete(val); 1070 1071 renderImpactStrip(); 1072 updateUrlSelectedOperators(Array.from(selected)); 1073 onChange(Array.from(selected)); 1074} 1075 1076function selectAll() { 1077 operatorList.forEach(op => selected.add(op)); 1078 renderList(); 1079 renderImpactStrip(); 1080 updateUrlSelectedOperators(Array.from(selected)); 1081 onChange(Array.from(selected)); 1082} 1083 1084function resetAll() { 1085 selected.clear(); 1086 renderList(); 1087 renderImpactStrip(); 1088 updateUrlSelectedOperators([]); 1089 onChange([]); 1090} 1091 1092// --- Public helpers for legend / external UI --- 1093function setSelectedOperators(nextSelected) { 1094 const arr = Array.isArray(nextSelected) ? nextSelected : []; 1095 const valid = arr.filter(o => operatorList.includes(o)); 1096 selected = new Set(valid); 1097 renderList(); 1098 renderImpactStrip(); 1099 updateUrlSelectedOperators(Array.from(selected)); 1100 onChange(Array.from(selected)); 1101} 1102 1103function applyPreset(presetKey) { 1104 const ops = getPresetOperators(presetKey, operatorList); 1105 setSelectedOperators(ops); 1106 return ops; 1107} 1108 1109function isPresetActive(presetKey) { 1110 const ops = getPresetOperators(presetKey, operatorList); 1111 if (!ops || !ops.length) return false;
1112 if (selected.size !== ops.length) return false; 1113 return ops.every(o => selected.has(o)); 1114} 1115 1116function togglePreset(presetKey) { 1117 if (isPresetActive(presetKey)) { 1118 resetAll(); 1119 return false; 1120 } 1121 applyPreset(presetKey); 1122 return true; 1123} 1124 1125 1126 1127 // No need for show/hide as public methods anymore (handled by toggle) 1128 1129function init({ allOps, topOps, initialSelected }, changeCb) { 1130 onChange = changeCb; 1131 1132 operatorList = Array.isArray(allOps) ? allOps : []; // â guard 1133 OPERATOR_ALIAS_INDEX = buildAliasIndex(operatorList); // â pass guarded list 1134const urlSelected = getUrlSelectedOperators(operatorList); 1135selected = new Set((urlSelected && urlSelected.length) ? urlSelected : (initialSelected || [])); 1136buildPanel(topOps, operatorList); 1137updateUrlSelectedOperators(Array.from(selected)); 1138 1139 1140 // Poll for toggle button until available 1141 const interval = setInterval(() => { 1142 toggleBtn = document.getElementById('show-operator-panel-btn'); 1143 if (toggleBtn) { 1144 toggleBtn.onclick = function () { 1145 if (panel.classList.contains('visible')) { 1146 panel.classList.remove('visible'); 1147 toggleBtn.classList.remove('glow'); 1148 } else { 1149 if (typeof window.closeAllPanels === 'function') window.closeAllPanels(); 1150 panel.classList.add('visible'); 1151 toggleBtn.classList.add('glow'); 1152 } 1153 }; 1154 clearInterval(interval); 1155 } 1156 }, 100); 1157 } 1158 1159 return { 1160 init, 1161 setSelectedOperators, 1162 applyPreset, 1163 togglePreset, 1164 isPresetActive 1165}; 1166 1167})(); 1168 1169// Click-outside-to-close now handled by unified handler in filterNav.js
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.