| import type { AppContext, AppModule } from '@/app/app-context'; |
| import type { SearchResult } from '@/components/SearchModal'; |
| import type { NewsItem, MapLayers, MilitaryBase, MilitaryFlight } from '@/types'; |
| import type { MapView, TimeRange } from '@/components/MapContainer'; |
| import type { Command } from '@/config/commands'; |
| import { SearchModal } from '@/components/SearchModal'; |
| import type { CIIPanel } from '@/components/CIIPanel'; |
| import { SITE_VARIANT, STORAGE_KEYS, ALL_PANELS, getEffectivePanelConfig, isPanelEntitled } from '@/config'; |
| import { getAllowedLayerKeys, isLayerExecutable } from '@/config/map-layer-definitions'; |
| import type { MapRenderer } from '@/config/map-layer-definitions'; |
| import type { MapVariant } from '@/config/map-layer-definitions'; |
| import { LAYER_PRESETS, LAYER_KEY_MAP } from '@/config/commands'; |
| import { TIER1_COUNTRIES } from '@/services/country-instability'; |
| import { getCachedCountryScores } from '@/services/cached-risk-scores'; |
| import { CURATED_COUNTRIES } from '@/config/countries'; |
| import { getCountryBbox } from '@/services/country-geometry'; |
| import { INTEL_HOTSPOTS, CONFLICT_ZONES } from '@/config/geo'; |
| import { getCachedMilitaryBases, preloadMilitaryBases } from '@/services/military-base-config'; |
| import { UNDERSEA_CABLES, NUCLEAR_FACILITIES } from '@/config/geo-map'; |
| import { PIPELINES } from '@/config/pipelines'; |
| import { AI_DATA_CENTERS } from '@/config/ai-datacenters'; |
| import { GAMMA_IRRADIATORS } from '@/config/irradiators'; |
| import { TECH_COMPANIES } from '@/config/tech-companies'; |
| import { AI_RESEARCH_LABS } from '@/config/ai-research-labs'; |
| import { STARTUP_ECOSYSTEMS } from '@/config/startup-ecosystems'; |
| import { TECH_HQS, ACCELERATORS } from '@/config/tech-geo'; |
| import { STOCK_EXCHANGES, FINANCIAL_CENTERS, CENTRAL_BANKS, COMMODITY_HUBS } from '@/config/finance-geo'; |
| import { trackSearchResultSelected, trackCountrySelected } from '@/services/analytics'; |
| import { t } from '@/services/i18n'; |
| import { saveToStorage, setTheme } from '@/utils'; |
| import { CountryIntelManager } from '@/app/country-intel'; |
| import type { PositionSample } from '@/services/aviation'; |
| import { fetchAircraftPositions } from '@/services/aviation'; |
| import { isProUser } from '@/services/widget-store'; |
| import { getAuthState } from '@/services/auth-state'; |
|
|
| export interface SearchManagerCallbacks { |
| openCountryBriefByCode: (code: string, country: string) => void; |
| |
| enablePanel: (panelId: string) => boolean; |
| } |
|
|
| export class SearchManager implements AppModule { |
| private ctx: AppContext; |
| private callbacks: SearchManagerCallbacks; |
| private highlightTimers = new WeakMap<Element, ReturnType<typeof setTimeout>>(); |
|
|
| constructor(ctx: AppContext, callbacks: SearchManagerCallbacks) { |
| this.ctx = ctx; |
| this.callbacks = callbacks; |
| } |
|
|
| init(): void { |
| this.setupSearchModal(); |
| } |
|
|
| destroy(): void {} |
|
|
| private setupSearchModal(): void { |
| const searchOptions = SITE_VARIANT === 'tech' |
| ? { placeholder: t('modals.search.placeholderTech') } |
| : SITE_VARIANT === 'happy' |
| ? { placeholder: 'Search or type a command...' } |
| : SITE_VARIANT === 'finance' |
| ? { placeholder: t('modals.search.placeholderFinance') } |
| : { placeholder: t('modals.search.placeholder') }; |
| this.ctx.searchModal = new SearchModal(this.ctx.container, searchOptions); |
|
|
| if (SITE_VARIANT === 'happy') { |
| |
| } else if (SITE_VARIANT === 'tech') { |
| this.ctx.searchModal.registerSource('techcompany', TECH_COMPANIES.map(c => ({ |
| id: c.id, |
| title: c.name, |
| subtitle: `${c.sector} ${c.city} ${c.keyProducts?.join(' ') || ''}`.trim(), |
| data: c, |
| }))); |
|
|
| this.ctx.searchModal.registerSource('ailab', AI_RESEARCH_LABS.map(l => ({ |
| id: l.id, |
| title: l.name, |
| subtitle: `${l.type} ${l.city} ${l.focusAreas?.join(' ') || ''}`.trim(), |
| data: l, |
| }))); |
|
|
| this.ctx.searchModal.registerSource('startup', STARTUP_ECOSYSTEMS.map(s => ({ |
| id: s.id, |
| title: s.name, |
| subtitle: `${s.ecosystemTier} ${s.topSectors?.join(' ') || ''} ${s.notableStartups?.join(' ') || ''}`.trim(), |
| data: s, |
| }))); |
|
|
| this.ctx.searchModal.registerSource('datacenter', AI_DATA_CENTERS.map(d => ({ |
| id: d.id, |
| title: d.name, |
| subtitle: `${d.owner} ${d.chipType || ''}`.trim(), |
| data: d, |
| }))); |
|
|
| this.ctx.searchModal.registerSource('cable', UNDERSEA_CABLES.map(c => ({ |
| id: c.id, |
| title: c.name, |
| subtitle: c.major ? 'Major internet backbone' : 'Undersea cable', |
| data: c, |
| }))); |
|
|
| this.ctx.searchModal.registerSource('techhq', TECH_HQS.map(h => ({ |
| id: h.id, |
| title: h.company, |
| subtitle: `${h.type === 'faang' ? 'Big Tech' : h.type === 'unicorn' ? 'Unicorn' : 'Public'} • ${h.city}, ${h.country}`, |
| data: h, |
| }))); |
|
|
| this.ctx.searchModal.registerSource('accelerator', ACCELERATORS.map(a => ({ |
| id: a.id, |
| title: a.name, |
| subtitle: `${a.type} • ${a.city}, ${a.country}${a.notable ? ` • ${a.notable.slice(0, 2).join(', ')}` : ''}`, |
| data: a, |
| }))); |
| } else { |
| this.ctx.searchModal.registerSource('hotspot', INTEL_HOTSPOTS.map(h => ({ |
| id: h.id, |
| title: h.name, |
| subtitle: `${h.subtext || ''} ${h.keywords?.join(' ') || ''} ${h.description || ''}`.trim(), |
| data: h, |
| }))); |
|
|
| this.ctx.searchModal.registerSource('conflict', CONFLICT_ZONES.map(c => ({ |
| id: c.id, |
| title: c.name, |
| subtitle: `${c.parties?.join(' ') || ''} ${c.keywords?.join(' ') || ''} ${c.description || ''}`.trim(), |
| data: c, |
| }))); |
|
|
| this.registerBaseSearchSource(); |
|
|
| this.ctx.searchModal.registerSource('pipeline', PIPELINES.map(p => ({ |
| id: p.id, |
| title: p.name, |
| subtitle: `${p.type} ${p.operator || ''} ${p.countries?.join(' ') || ''}`.trim(), |
| data: p, |
| }))); |
|
|
| this.ctx.searchModal.registerSource('cable', UNDERSEA_CABLES.map(c => ({ |
| id: c.id, |
| title: c.name, |
| subtitle: c.major ? 'Major cable' : '', |
| data: c, |
| }))); |
|
|
| this.ctx.searchModal.registerSource('datacenter', AI_DATA_CENTERS.map(d => ({ |
| id: d.id, |
| title: d.name, |
| subtitle: `${d.owner} ${d.chipType || ''}`.trim(), |
| data: d, |
| }))); |
|
|
| this.ctx.searchModal.registerSource('nuclear', NUCLEAR_FACILITIES.map(n => ({ |
| id: n.id, |
| title: n.name, |
| subtitle: `${n.type} ${n.operator || ''}`.trim(), |
| data: n, |
| }))); |
|
|
| this.ctx.searchModal.registerSource('irradiator', GAMMA_IRRADIATORS.map(g => ({ |
| id: g.id, |
| title: `${g.city}, ${g.country}`, |
| subtitle: g.organization || '', |
| data: g, |
| }))); |
| } |
|
|
| if (SITE_VARIANT === 'finance') { |
| this.ctx.searchModal.registerSource('exchange', STOCK_EXCHANGES.map(e => ({ |
| id: e.id, |
| title: `${e.shortName} - ${e.name}`, |
| subtitle: `${e.tier} • ${e.city}, ${e.country}${e.marketCap ? ` • $${e.marketCap}T` : ''}`, |
| data: e, |
| }))); |
|
|
| this.ctx.searchModal.registerSource('financialcenter', FINANCIAL_CENTERS.map(f => ({ |
| id: f.id, |
| title: f.name, |
| subtitle: `${f.type} financial center${f.gfciRank ? ` • GFCI #${f.gfciRank}` : ''}${f.specialties ? ` • ${f.specialties.slice(0, 3).join(', ')}` : ''}`, |
| data: f, |
| }))); |
|
|
| this.ctx.searchModal.registerSource('centralbank', CENTRAL_BANKS.map(b => ({ |
| id: b.id, |
| title: `${b.shortName} - ${b.name}`, |
| subtitle: `${b.type}${b.currency ? ` • ${b.currency}` : ''} • ${b.city}, ${b.country}`, |
| data: b, |
| }))); |
|
|
| this.ctx.searchModal.registerSource('commodityhub', COMMODITY_HUBS.map(h => ({ |
| id: h.id, |
| title: h.name, |
| subtitle: `${h.type} • ${h.city}, ${h.country}${h.commodities ? ` • ${h.commodities.slice(0, 3).join(', ')}` : ''}`, |
| data: h, |
| }))); |
| } |
|
|
| this.ctx.searchModal.registerSource('country', this.buildCountrySearchItems()); |
|
|
| this.syncPanelSearchIndex(); |
| |
| |
| |
| |
| |
| |
| |
| this.ctx.searchModal.setLayerExecutableFn((layerKey) => { |
| const key = (LAYER_KEY_MAP[layerKey] || layerKey) as keyof MapLayers; |
| if (!(key in this.ctx.mapLayers)) return false; |
| const variantAllowed = getAllowedLayerKeys((SITE_VARIANT || 'full') as MapVariant); |
| if (!variantAllowed.has(key)) return false; |
| const renderer: MapRenderer = this.ctx.map?.isGlobeMode?.() ? 'globe' : 'flat'; |
| const isDeckGL = this.ctx.map?.isDeckGLActive?.() ?? false; |
| return isLayerExecutable(key, renderer, isDeckGL); |
| }); |
| this.ctx.searchModal.setOnSelect((result) => this.handleSearchResult(result)); |
| this.ctx.searchModal.setOnCommand((cmd) => this.handleCommand(cmd)); |
| |
| |
| this.ctx.searchModal.setOnFlightSearch((callsign) => { |
| if (!isProUser() && getAuthState().user?.role !== 'pro') return; |
| fetchAircraftPositions({ callsign }).then((positions) => { |
| if (!this.ctx.searchModal) return; |
| |
| const seen = new Map<string, PositionSample>(); |
| for (const p of positions) { |
| const key = (p.callsign || p.icao24).trim().toUpperCase(); |
| const existing = seen.get(key); |
| if (!existing || p.observedAt > existing.observedAt) { |
| seen.set(key, p); |
| } |
| } |
| const items = [...seen.values()].map(p => { |
| const fl = Number.isFinite(p.altitudeFt) ? Math.round(p.altitudeFt / 100) : null; |
| const kts = Number.isFinite(p.groundSpeedKts) ? Math.round(p.groundSpeedKts) : null; |
| return { |
| id: p.icao24, |
| title: (p.callsign || p.icao24).trim().toUpperCase(), |
| subtitle: p.onGround |
| ? t('modals.search.flightOnGround') |
| : fl !== null && kts !== null |
| ? t('modals.search.flightAirborne', { fl: String(fl), kts: String(kts) }) |
| : fl !== null ? `FL${fl}` : t('modals.search.flightOnGround'), |
| data: { kind: 'adsb' as const, lat: p.lat, lon: p.lon, layer: 'flights' as const }, |
| }; |
| }); |
| this.ctx.searchModal.registerSource('flight', items); |
| this.ctx.searchModal.refreshSearch(); |
| }).catch(() => {}); |
| }); |
|
|
| } |
|
|
| private registerBaseSearchSource(): void { |
| const register = (bases: MilitaryBase[]) => { |
| this.ctx.searchModal?.registerSource('base', bases.map(b => ({ |
| id: b.id, |
| title: b.name, |
| subtitle: `${b.type} ${b.description || ''}`.trim(), |
| data: b, |
| }))); |
| }; |
|
|
| const cached = getCachedMilitaryBases(); |
| if (cached.length > 0) register(cached); |
| void preloadMilitaryBases().then(register).catch(() => {}); |
| } |
|
|
| private handleSearchResult(result: SearchResult): void { |
| trackSearchResultSelected(result.type); |
| switch (result.type) { |
| case 'news': { |
| const item = result.data as NewsItem; |
| |
| let targetPanelId = 'politics'; |
| let targetPanel = this.ctx.newsPanels['politics'] ?? null; |
| for (const [panelId, panel] of Object.entries(this.ctx.newsPanels)) { |
| if (panel.hasNewsItem(item.link)) { |
| targetPanelId = panelId; |
| targetPanel = panel; |
| break; |
| } |
| } |
| this.scrollToPanel(targetPanelId); |
| if (targetPanel) { |
| setTimeout(() => targetPanel!.scrollToNewsItem(item.link), 300); |
| } |
| break; |
| } |
| case 'hotspot': { |
| const hotspot = result.data as typeof INTEL_HOTSPOTS[0]; |
| this.ctx.map?.setView('global'); |
| setTimeout(() => { this.ctx.map?.triggerHotspotClick(hotspot.id); }, 300); |
| break; |
| } |
| case 'conflict': { |
| const conflict = result.data as typeof CONFLICT_ZONES[0]; |
| this.ctx.map?.setView('global'); |
| setTimeout(() => { this.ctx.map?.triggerConflictClick(conflict.id); }, 300); |
| break; |
| } |
| case 'market': { |
| this.scrollToPanel('markets'); |
| break; |
| } |
| case 'prediction': { |
| this.scrollToPanel('polymarket'); |
| break; |
| } |
| case 'base': { |
| const base = result.data as MilitaryBase; |
| this.ctx.map?.setView('global'); |
| setTimeout(() => { this.ctx.map?.triggerBaseClick(base.id); }, 300); |
| break; |
| } |
| case 'pipeline': { |
| const pipeline = result.data as typeof PIPELINES[0]; |
| this.ctx.map?.setView('global'); |
| this.ctx.map?.enableLayer('pipelines'); |
| this.ctx.mapLayers.pipelines = true; |
| setTimeout(() => { this.ctx.map?.triggerPipelineClick(pipeline.id); }, 300); |
| break; |
| } |
| case 'cable': { |
| const cable = result.data as typeof UNDERSEA_CABLES[0]; |
| this.ctx.map?.setView('global'); |
| this.ctx.map?.enableLayer('cables'); |
| this.ctx.mapLayers.cables = true; |
| setTimeout(() => { this.ctx.map?.triggerCableClick(cable.id); }, 300); |
| break; |
| } |
| case 'datacenter': { |
| const dc = result.data as typeof AI_DATA_CENTERS[0]; |
| this.ctx.map?.setView('global'); |
| this.ctx.map?.enableLayer('datacenters'); |
| this.ctx.mapLayers.datacenters = true; |
| setTimeout(() => { this.ctx.map?.triggerDatacenterClick(dc.id); }, 300); |
| break; |
| } |
| case 'nuclear': { |
| const nuc = result.data as typeof NUCLEAR_FACILITIES[0]; |
| this.ctx.map?.setView('global'); |
| this.ctx.map?.enableLayer('nuclear'); |
| this.ctx.mapLayers.nuclear = true; |
| setTimeout(() => { this.ctx.map?.triggerNuclearClick(nuc.id); }, 300); |
| break; |
| } |
| case 'irradiator': { |
| const irr = result.data as typeof GAMMA_IRRADIATORS[0]; |
| this.ctx.map?.setView('global'); |
| this.ctx.map?.enableLayer('irradiators'); |
| this.ctx.mapLayers.irradiators = true; |
| setTimeout(() => { this.ctx.map?.triggerIrradiatorClick(irr.id); }, 300); |
| break; |
| } |
| case 'earthquake': |
| case 'outage': |
| this.ctx.map?.setView('global'); |
| break; |
| case 'techcompany': { |
| const company = result.data as typeof TECH_COMPANIES[0]; |
| this.ctx.map?.setView('global'); |
| this.ctx.map?.enableLayer('techHQs'); |
| this.ctx.mapLayers.techHQs = true; |
| setTimeout(() => { this.ctx.map?.setCenter(company.lat, company.lon, 4); }, 300); |
| break; |
| } |
| case 'ailab': { |
| const lab = result.data as typeof AI_RESEARCH_LABS[0]; |
| this.ctx.map?.setView('global'); |
| setTimeout(() => { this.ctx.map?.setCenter(lab.lat, lab.lon, 4); }, 300); |
| break; |
| } |
| case 'startup': { |
| const ecosystem = result.data as typeof STARTUP_ECOSYSTEMS[0]; |
| this.ctx.map?.setView('global'); |
| this.ctx.map?.enableLayer('startupHubs'); |
| this.ctx.mapLayers.startupHubs = true; |
| setTimeout(() => { this.ctx.map?.setCenter(ecosystem.lat, ecosystem.lon, 4); }, 300); |
| break; |
| } |
| case 'techevent': |
| this.ctx.map?.setView('global'); |
| this.ctx.map?.enableLayer('techEvents'); |
| this.ctx.mapLayers.techEvents = true; |
| break; |
| case 'techhq': { |
| const hq = result.data as typeof TECH_HQS[0]; |
| this.ctx.map?.setView('global'); |
| this.ctx.map?.enableLayer('techHQs'); |
| this.ctx.mapLayers.techHQs = true; |
| setTimeout(() => { this.ctx.map?.setCenter(hq.lat, hq.lon, 4); }, 300); |
| break; |
| } |
| case 'accelerator': { |
| const acc = result.data as typeof ACCELERATORS[0]; |
| this.ctx.map?.setView('global'); |
| this.ctx.map?.enableLayer('accelerators'); |
| this.ctx.mapLayers.accelerators = true; |
| setTimeout(() => { this.ctx.map?.setCenter(acc.lat, acc.lon, 4); }, 300); |
| break; |
| } |
| case 'exchange': { |
| const exchange = result.data as typeof STOCK_EXCHANGES[0]; |
| this.ctx.map?.setView('global'); |
| this.ctx.map?.enableLayer('stockExchanges'); |
| this.ctx.mapLayers.stockExchanges = true; |
| setTimeout(() => { this.ctx.map?.setCenter(exchange.lat, exchange.lon, 4); }, 300); |
| break; |
| } |
| case 'financialcenter': { |
| const fc = result.data as typeof FINANCIAL_CENTERS[0]; |
| this.ctx.map?.setView('global'); |
| this.ctx.map?.enableLayer('financialCenters'); |
| this.ctx.mapLayers.financialCenters = true; |
| setTimeout(() => { this.ctx.map?.setCenter(fc.lat, fc.lon, 4); }, 300); |
| break; |
| } |
| case 'centralbank': { |
| const bank = result.data as typeof CENTRAL_BANKS[0]; |
| this.ctx.map?.setView('global'); |
| this.ctx.map?.enableLayer('centralBanks'); |
| this.ctx.mapLayers.centralBanks = true; |
| setTimeout(() => { this.ctx.map?.setCenter(bank.lat, bank.lon, 4); }, 300); |
| break; |
| } |
| case 'commodityhub': { |
| const hub = result.data as typeof COMMODITY_HUBS[0]; |
| this.ctx.map?.setView('global'); |
| this.ctx.map?.enableLayer('commodityHubs'); |
| this.ctx.mapLayers.commodityHubs = true; |
| setTimeout(() => { this.ctx.map?.setCenter(hub.lat, hub.lon, 4); }, 300); |
| break; |
| } |
| case 'country': { |
| const { code, name } = result.data as { code: string; name: string }; |
| trackCountrySelected(code, name, 'search'); |
| this.callbacks.openCountryBriefByCode(code, name); |
| break; |
| } |
| case 'flight': { |
| const { lat, lon, layer } = result.data as { kind: string; lat: number; lon: number; layer: keyof MapLayers }; |
| this.ctx.map?.enableLayer(layer); |
| this.ctx.mapLayers[layer] = true; |
| setTimeout(() => { this.ctx.map?.setCenter(lat, lon, 9); }, 300); |
| break; |
| } |
| } |
| } |
|
|
| private handleCommand(cmd: Command): void { |
| const colonIdx = cmd.id.indexOf(':'); |
| if (colonIdx === -1) return; |
| const category = cmd.id.slice(0, colonIdx); |
| const action = cmd.id.slice(colonIdx + 1); |
|
|
| switch (category) { |
| case 'nav': |
| this.ctx.map?.setView(action as MapView); |
| { |
| const sel = document.getElementById('regionSelect') as HTMLSelectElement; |
| if (sel) sel.value = action; |
| } |
| break; |
|
|
| case 'layers': { |
| const allowed = getAllowedLayerKeys((SITE_VARIANT || 'full') as MapVariant); |
| |
| |
| |
| |
| |
| |
| |
| |
| const renderer: MapRenderer = this.ctx.map?.isGlobeMode?.() ? 'globe' : 'flat'; |
| const isDeckGL = this.ctx.map?.isDeckGLActive?.() ?? false; |
| const executable = (k: keyof MapLayers): boolean => |
| allowed.has(k) && isLayerExecutable(k, renderer, isDeckGL); |
| if (action === 'all') { |
| for (const key of Object.keys(this.ctx.mapLayers)) { |
| this.ctx.mapLayers[key as keyof MapLayers] = executable(key as keyof MapLayers); |
| } |
| } else if (action === 'none') { |
| for (const key of Object.keys(this.ctx.mapLayers)) |
| this.ctx.mapLayers[key as keyof MapLayers] = false; |
| } else { |
| const preset = LAYER_PRESETS[action]; |
| if (preset) { |
| for (const key of Object.keys(this.ctx.mapLayers)) |
| this.ctx.mapLayers[key as keyof MapLayers] = false; |
| for (const layer of preset) { |
| if (executable(layer)) this.ctx.mapLayers[layer] = true; |
| } |
| } |
| } |
| saveToStorage(STORAGE_KEYS.mapLayers, this.ctx.mapLayers); |
| this.ctx.map?.setLayers(this.ctx.mapLayers); |
| break; |
| } |
|
|
| case 'layer': { |
| const layerKey = (LAYER_KEY_MAP[action] || action) as keyof MapLayers; |
| if (!(layerKey in this.ctx.mapLayers)) return; |
| const variantAllowed = getAllowedLayerKeys((SITE_VARIANT || 'full') as MapVariant); |
| if (!variantAllowed.has(layerKey)) return; |
| |
| |
| |
| const renderer: MapRenderer = this.ctx.map?.isGlobeMode?.() ? 'globe' : 'flat'; |
| const isDeckGL = this.ctx.map?.isDeckGLActive?.() ?? false; |
| if (!isLayerExecutable(layerKey, renderer, isDeckGL)) return; |
| let newValue = !this.ctx.mapLayers[layerKey]; |
| if (newValue && layerKey === 'resilienceScore' && !this.ctx.map?.isDeckGLActive?.()) { |
| newValue = false; |
| } |
| this.ctx.mapLayers[layerKey] = newValue; |
| saveToStorage(STORAGE_KEYS.mapLayers, this.ctx.mapLayers); |
| if (newValue) { |
| this.ctx.map?.enableLayer(layerKey); |
| } else { |
| this.ctx.map?.setLayers(this.ctx.mapLayers); |
| } |
| break; |
| } |
|
|
| case 'panel': { |
| |
| |
| |
| |
| const [panelId, subTab] = action.split('@'); |
| if (!panelId) break; |
| const cfg = this.ctx.panelSettings[panelId]; |
| if (cfg && !cfg.enabled) { |
| if (this.callbacks.enablePanel(panelId)) { |
| this.scrollToPanelWhenReady(panelId); |
| if (subTab) this.dispatchPanelTab(panelId, subTab); |
| break; |
| } |
| } |
| this.scrollToPanel(panelId); |
| if (subTab) this.dispatchPanelTab(panelId, subTab); |
| break; |
| } |
|
|
| case 'view': |
| if (action === 'dark' || action === 'light') { |
| setTheme(action); |
| } else if (action === 'fullscreen') { |
| if (document.fullscreenElement) { |
| try { void document.exitFullscreen()?.catch(() => {}); } catch {} |
| } else { |
| const el = document.documentElement as HTMLElement & { webkitRequestFullscreen?: () => void }; |
| if (el.requestFullscreen) { |
| try { void el.requestFullscreen()?.catch(() => {}); } catch {} |
| } else if (el.webkitRequestFullscreen) { |
| try { el.webkitRequestFullscreen(); } catch {} |
| } |
| } |
| } else if (action === 'settings') { |
| this.ctx.unifiedSettings?.open(); |
| } else if (action === 'refresh') { |
| window.location.reload(); |
| } else if (action === 'resilience') { |
| const layerKey = 'resilienceScore' as keyof MapLayers; |
| const variantAllowed = getAllowedLayerKeys((SITE_VARIANT || 'full') as MapVariant); |
| if (!variantAllowed.has(layerKey)) break; |
| let newValue = !this.ctx.mapLayers[layerKey]; |
| if (newValue && !this.ctx.map?.isDeckGLActive?.()) newValue = false; |
| this.ctx.mapLayers[layerKey] = newValue; |
| saveToStorage(STORAGE_KEYS.mapLayers, this.ctx.mapLayers); |
| if (newValue) { |
| this.ctx.map?.enableLayer(layerKey); |
| } else { |
| this.ctx.map?.setLayers(this.ctx.mapLayers); |
| } |
| } else if (action === 'route-explorer') { |
| void import('@/components/RouteExplorer/RouteExplorer').then((m) => { |
| const explorer = m.getRouteExplorer(); |
| explorer.setMap(this.ctx.map); |
| explorer.open(); |
| }); |
| } |
| break; |
|
|
| case 'time': |
| this.ctx.map?.setTimeRange(action as TimeRange); |
| break; |
|
|
| case 'country': { |
| const name = TIER1_COUNTRIES[action] |
| || CURATED_COUNTRIES[action]?.name |
| || new Intl.DisplayNames(['en'], { type: 'region' }).of(action) |
| || action; |
| trackCountrySelected(action, name, 'command'); |
| this.callbacks.openCountryBriefByCode(action, name); |
| break; |
| } |
|
|
| case 'country-map': { |
| const bbox = getCountryBbox(action); |
| if (bbox) { |
| const [minLon, minLat, maxLon, maxLat] = bbox; |
| const lat = (minLat + maxLat) / 2; |
| const lon = (minLon + maxLon) / 2; |
| const span = Math.max(maxLat - minLat, maxLon - minLon); |
| const zoom = span > 40 ? 3 : span > 15 ? 4 : span > 5 ? 5 : 6; |
| this.ctx.map?.setView('global'); |
| setTimeout(() => { this.ctx.map?.setCenter(lat, lon, zoom); }, 300); |
| } |
| break; |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| private scrollToPanelWhenReady(panelId: string, attemptsLeft = 12): void { |
| if (document.querySelector(`[data-panel="${panelId}"]`)) { |
| this.scrollToPanel(panelId); |
| return; |
| } |
| if (attemptsLeft <= 0) return; |
| setTimeout(() => this.scrollToPanelWhenReady(panelId, attemptsLeft - 1), 80); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| private dispatchPanelTab(panelId: string, tab: string, attemptsLeft = 12): void { |
| |
| if (panelId !== 'consumer-prices') return; |
| if (document.querySelector(`[data-panel="${panelId}"]:not([data-deferred-panel])`)) { |
| window.dispatchEvent(new CustomEvent('wm-consumer-prices-open-tab', { detail: { tab } })); |
| return; |
| } |
| if (attemptsLeft <= 0) return; |
| setTimeout(() => this.dispatchPanelTab(panelId, tab, attemptsLeft - 1), 80); |
| } |
|
|
| private scrollToPanel(panelId: string): void { |
| const panel = document.querySelector(`[data-panel="${panelId}"]`); |
| if (panel) { |
| panel.scrollIntoView({ behavior: 'smooth', block: 'center' }); |
| this.applyHighlight(panel); |
| } |
| } |
|
|
| private applyHighlight(el: Element): void { |
| const prev = this.highlightTimers.get(el); |
| if (prev) clearTimeout(prev); |
| el.classList.remove('search-highlight'); |
| void (el as HTMLElement).offsetWidth; |
| el.classList.add('search-highlight'); |
| this.highlightTimers.set(el, setTimeout(() => { |
| el.classList.remove('search-highlight'); |
| this.highlightTimers.delete(el); |
| }, 3100)); |
| } |
|
|
| updateFlightSource(adsb: PositionSample[], military: MilitaryFlight[]): void { |
| if (!this.ctx.searchModal || !isProUser()) return; |
| const items = [ |
| ...adsb.map(p => { |
| const fl = Number.isFinite(p.altitudeFt) ? Math.round(p.altitudeFt / 100) : null; |
| const kts = Number.isFinite(p.groundSpeedKts) ? Math.round(p.groundSpeedKts) : null; |
| return { |
| id: p.icao24, |
| title: (p.callsign || p.icao24).trim().toUpperCase(), |
| subtitle: p.onGround |
| ? t('modals.search.flightOnGround') |
| : fl !== null && kts !== null |
| ? t('modals.search.flightAirborne', { fl: String(fl), kts: String(kts) }) |
| : fl !== null |
| ? `FL${fl}` |
| : t('modals.search.flightOnGround'), |
| data: { kind: 'adsb' as const, lat: p.lat, lon: p.lon, layer: 'flights' as const }, |
| }; |
| }), |
| ...military.map(f => { |
| const fl = Number.isFinite(f.altitude) ? Math.round(f.altitude / 100) : null; |
| return { |
| id: f.hexCode, |
| title: (f.callsign || f.hexCode).trim().toUpperCase(), |
| subtitle: f.onGround |
| ? t('modals.search.flightMilitaryOnGround', { type: f.aircraftType }) |
| : fl !== null |
| ? t('modals.search.flightMilitary', { type: f.aircraftType, fl: String(fl) }) |
| : t('modals.search.flightMilitaryOnGround', { type: f.aircraftType }), |
| data: { kind: 'military' as const, lat: f.lat, lon: f.lon, layer: 'military' as const }, |
| }; |
| }), |
| ]; |
| this.ctx.searchModal.registerSource('flight', items); |
| } |
|
|
| updateSearchIndex(): void { |
| if (!this.ctx.searchModal) return; |
|
|
| this.syncPanelSearchIndex(); |
| this.ctx.searchModal.registerSource('country', this.buildCountrySearchItems()); |
|
|
| const newsItems = this.ctx.allNews.slice(0, 500).map(n => ({ |
| id: n.link, |
| title: n.title, |
| subtitle: n.source, |
| data: n, |
| })); |
| console.log(`[Search] Indexing ${newsItems.length} news items (allNews total: ${this.ctx.allNews.length})`); |
| this.ctx.searchModal.registerSource('news', newsItems); |
|
|
| if (this.ctx.latestPredictions.length > 0) { |
| this.ctx.searchModal.registerSource('prediction', this.ctx.latestPredictions.map(p => ({ |
| id: p.title, |
| title: p.title, |
| subtitle: `${Math.round(p.yesPrice)}% probability`, |
| data: p, |
| }))); |
| } |
|
|
| if (this.ctx.latestMarkets.length > 0) { |
| this.ctx.searchModal.registerSource('market', this.ctx.latestMarkets.map(m => ({ |
| id: m.symbol, |
| title: `${m.symbol} - ${m.name}`, |
| subtitle: `$${m.price?.toFixed(2) || 'N/A'}`, |
| data: m, |
| }))); |
| } |
|
|
| if (SITE_VARIANT === 'tech') { |
| this.ctx.searchModal.registerSource('techevent', this.ctx.latestTechEvents.map((e) => ({ |
| id: e.id, |
| title: e.title, |
| subtitle: `${e.location} • ${new Date(e.startDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}`, |
| data: e, |
| }))); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| private syncPanelSearchIndex(): void { |
| if (!this.ctx.searchModal) return; |
| |
| const isPro = isProUser(); |
| this.ctx.searchModal.setActivePanels( |
| Object.entries(this.ctx.panelSettings).filter(([, v]) => v.enabled).map(([k]) => k) |
| ); |
| this.ctx.searchModal.setAvailablePanels( |
| Object.keys(this.ctx.panelSettings).filter((k) => { |
| |
| |
| const cfg = ALL_PANELS[k] ? getEffectivePanelConfig(k, SITE_VARIANT) : undefined; |
| return cfg ? isPanelEntitled(k, cfg, isPro) : false; |
| }) |
| ); |
| } |
|
|
| private buildCountrySearchItems(): { id: string; title: string; subtitle: string; data: { code: string; name: string } }[] { |
| const cachedScores = getCachedCountryScores(); |
| const panelScores = (this.ctx.panels.cii as CIIPanel | undefined)?.getScores() ?? []; |
| const scores = cachedScores.length > 0 |
| ? cachedScores |
| : panelScores; |
| const ciiByCode = new Map(scores.map((score) => [score.code, score])); |
| return Object.entries(TIER1_COUNTRIES).map(([code, name]) => { |
| const score = ciiByCode.get(code); |
| return { |
| id: code, |
| title: `${CountryIntelManager.toFlagEmoji(code)} ${name}`, |
| subtitle: score ? `CII: ${score.score}/100 • ${score.level}` : 'Country Brief', |
| data: { code, name }, |
| }; |
| }); |
| } |
| } |
|
|