/**
* Your sky now (DESIGN.md §12.3): what is above you this minute, on a sky
* dome (zenith in the centre, horizon at the rim, north up) and as a list.
* Live from globe.skyNow(); refreshes every 2 s while open.
*/
import { icon } from './icons.js';
import { $, esc, fmtNum, haptic } from './dom.js';
import { createSheet } from './sheet.js';
import { cardinal, utc } from './bands.js';
/** Sky dome SVG. size in px; items: [{ el, az, kind, id, label? }]. */
export function domeSvg(sky, { size = 300, labels = true, sel = null } = {}) {
const c = size / 2;
const R = c - (labels ? 18 : 4);
const pos = (el, az) => {
const r = R * (1 - Math.max(0, el) / 90);
const a = ((az - 90) * Math.PI) / 180;
return [c + r * Math.cos(a), c + r * Math.sin(a)];
};
const dots = [];
for (const s of sky?.satellites || []) {
const [x, y] = pos(s.elevationDeg, s.azimuthDeg);
dots.push(
` `,
);
}
for (const a of sky?.aircraft || []) {
if (a.elevationDeg < 0) continue;
const [x, y] = pos(a.elevationDeg, a.azimuthDeg);
dots.push(
` `,
);
}
const iss = sky?.iss;
if (iss && Number.isFinite(iss.elevationDeg) && iss.elevationDeg > 0) {
const [x, y] = pos(iss.elevationDeg, iss.azimuthDeg);
dots.push(
` ${labels ? `ISS ` : ''} `,
);
}
return `
${[30, 60].map((e) => ` `).join('')}
${labels ? `N E S W 60° 30° ` : ''}
${dots.join('')}
`;
}
export function createSkySheet({ host, globe, getHome, onOpenEntity }) {
const sheet = createSheet({
host,
label: 'Your sky now',
detents: ['medium', 'large'],
modal: true,
dismissible: true,
closeButton: true,
className: 'sky-sheet',
heights: {
medium: (H) => Math.round(H * 0.62),
large: (H) => Math.round(H * 0.93),
},
});
let timer = 0;
let sky = null;
sheet.on('close', () => clearInterval(timer));
function rowAc(a) {
return `${icon(a.kind === 'helicopter' ? 'heli' : 'sky')}
${esc(a.title)} ${esc(fmtNum(Math.round((a.altM * 3.28084) / 100) * 100))} ft · ${esc(fmtNum(a.distanceKm, 1))} km ${esc(cardinal(a.azimuthDeg))}
${Math.round(a.elevationDeg)}° UP `;
}
function rowSat(s) {
return `${icon('satellite')}
${esc(s.title)} ${esc(fmtNum(s.altKm))} km up · ${esc(fmtNum(s.rangeKm))} km away ${esc(cardinal(s.azimuthDeg))}
${Math.round(s.elevationDeg)}° UP `;
}
async function refresh() {
const home = getHome();
sky = await globe.skyNow({ lon: home.lon, lat: home.lat });
if (!sky) {
sheet.body.innerHTML =
'
';
return;
}
const ac = sky.aircraft || [];
const sats = sky.satellites || [];
const iss = sky.iss || {};
const np = iss.nextPass;
const hm = (t) =>
new Date(t).toLocaleTimeString([], {
hour: 'numeric',
minute: '2-digit',
});
$('.sky-dome', sheet.body).innerHTML = domeSvg(sky, { size: 300 });
$('.sky-count', sheet.body).innerHTML =
`${ac.length} aircraft
${sats.length} satellites
${iss.visibleNow ? 'Now' : np ? hm(np.start) : '—'} ${iss.visibleNow ? 'ISS overhead' : 'next ISS pass'}
`;
$('.sky-lists', sheet.body).innerHTML = `
${np || iss.visibleNow ? `${icon('satellite')}
${iss.visibleNow ? 'The ISS is above you right now' : `ISS passes over at ${hm(np.start)}`} ${np ? `Rises ${hm(np.start)} · highest ${np.maxElevationDeg ?? '—'}° at ${hm(np.max)} · sets ${np.end ? hm(np.end) : '—'}` : `${Math.round(iss.elevationDeg)}° up, ${cardinal(iss.azimuthDeg)}`}
` : ''}
Aircraft${ac.length} within 50 km
${ac.slice(0, 12).map(rowAc).join('') || '
No aircraft overhead right now. A quiet sky.
'}
Satellites${sats.length} above the horizon
${sats.slice(0, 12).map(rowSat).join('') || '
None above the horizon.
'}
`;
}
sheet.body.addEventListener('click', (e) => {
const r = e.target.closest('[data-ent]');
if (r) {
haptic('light');
sheet.close();
onOpenEntity?.(r.dataset.ent);
}
});
return {
async open() {
const home = getHome();
sheet.head.innerHTML = ` LIVE · ${esc(utc(new Date(), { seconds: false }))}
Your sky now Above ${esc(home.label || home.name)}${home.demo ? ' · demo location' : ''}, this minute
`;
sheet.body.innerHTML = `
`;
sheet.open('medium');
await refresh();
clearInterval(timer);
timer = setInterval(() => sheet.isOpen && refresh(), 2000);
},
close: () => sheet.close(),
get isOpen() {
return sheet.isOpen;
},
};
}