/** Map CoT / ADS-B entity display: icons and popups. */ export const COT_COLORS = { air: '#60a5fa', helicopter: '#fbbf24', surface: '#38bdf8', ground: '#f59e0b', } export function cotCategory(type) { const t = typeof type === 'string' ? type : '' if (t.startsWith('a-f-A-')) return 'air' if (t.startsWith('a-f-S-')) return 'surface' return 'ground' } /** Whether the entity is a helicopter or fixed-wing aircraft. @returns {'helicopter' | 'fixedWing'} */ export function cotAirIconKind(entity) { const type = entity?.type ?? '' if (type.endsWith('-C-H') || type.endsWith('-M-H')) return 'helicopter' return 'fixedWing' } function iconWrap(heading, inner) { const rotate = Number.isFinite(heading) ? ` style="transform:rotate(${heading}deg)"` : '' return `${inner}` } const PLANE_SVG = color => `` const HELI_SVG = color => `` const SHIP_SVG = color => `` const GROUND_SVG = color => `` export function getCotIconHtml(entity) { const category = cotCategory(entity?.type) const heading = Number(entity?.heading) if (category === 'air') { const kind = cotAirIconKind(entity) const color = kind === 'helicopter' ? COT_COLORS.helicopter : COT_COLORS.air const svg = kind === 'helicopter' ? HELI_SVG(color) : PLANE_SVG(color) return { html: iconWrap(heading, svg), className: `cot-entity-${kind}` } } if (category === 'surface') { return { html: iconWrap(heading, SHIP_SVG(COT_COLORS.surface)), className: 'cot-entity-surface' } } return { html: iconWrap(undefined, GROUND_SVG(COT_COLORS.ground)), className: 'cot-entity-ground' } } function msToKnots(ms) { return Number.isFinite(ms) ? Math.round(ms * 1.94384) : null } function metersToFeet(m) { return Number.isFinite(m) ? Math.round(m * 3.28084) : null } function fmtHeading(deg) { return Number.isFinite(deg) ? `${Math.round(deg)}°` : null } function fmtVerticalFpm(ms) { if (!Number.isFinite(ms) || ms === 0) return null const fpm = Math.round(ms * 196.85) return `${fpm > 0 ? '+' : ''}${fpm} fpm` } function icaoFromEntity(entity) { if (entity?.icao) return String(entity.icao).toUpperCase() if (typeof entity?.id === 'string' && entity.id.startsWith('ICAO.')) { return entity.id.slice(5).toUpperCase() } return null } function mmsiFromEntity(entity) { if (entity?.mmsi) return String(entity.mmsi) if (typeof entity?.id === 'string' && entity.id.startsWith('MMSI.')) return entity.id.slice(5) return null } function popupLine(escape, parts) { const line = parts.filter(Boolean).join(' · ') return line ? `
${line}
` : '' } /** * @param {Record} entity * @param {(s: string) => string} escape */ export function formatCotPopup(entity, escape) { const category = cotCategory(entity?.type) const label = escape(entity?.label || entity?.id || 'Unknown') if (entity?.source === 'adsb' || category === 'air') { const tag = cotAirIconKind(entity) === 'helicopter' ? 'Helicopter' : 'Aircraft' const icao = icaoFromEntity(entity) const meta = [ icao ? `ICAO ${icao}` : null, entity?.originCountry ? escape(String(entity.originCountry)) : null, ].filter(Boolean).join(' · ') const alt = metersToFeet(entity?.altitude) const stats = [ alt != null ? `${alt.toLocaleString()} ft` : null, entity?.onGround ? 'On ground' : null, msToKnots(entity?.speed) != null ? `${msToKnots(entity.speed)} kt` : null, fmtHeading(entity?.heading), fmtVerticalFpm(entity?.verticalRate), entity?.squawk ? `Squawk ${escape(String(entity.squawk))}` : null, ] return `
${label} ${tag}${meta ? `
${meta}
` : ''}${popupLine(escape, stats)}
` } if (entity?.source === 'ais' || category === 'surface') { const mmsi = mmsiFromEntity(entity) const meta = mmsi ? `MMSI ${escape(mmsi)}` : '' const stats = [ Number.isFinite(entity?.speed) ? `${Number(entity.speed).toFixed(1)} kt` : null, fmtHeading(entity?.heading), ] return `
${label} Vessel${meta ? `
${meta}
` : ''}${popupLine(escape, stats)}
` } return `
${label} Team
` }