Add fe-map and iss sources, The Shape of Distance as published, contribution guide

- femap/: planar best-effort solver, build, result and page (fe-map.aethercosmology.com)
- satellites/: The Sync Test build, ISS elements, SatNOGS timings and page (iss.aethercosmology.com)
- src/handoff/The_Shape_of_Distance_PRESENTATION.html: inputs for both
- CONTRIBUTING.md; README and LICENSE-DATA cover all four sites; GitHub mirror for issues and pull requests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Toby
2026-10-30 18:42:34 -04:00
parent 2bf0bdccb5
commit ad8b889437
27 changed files with 4474 additions and 43 deletions
+99
View File
@@ -0,0 +1,99 @@
// The Sync Test (satellites/site/index.html): errors, overflow, controls clickable, readouts filled, at 8 viewports.
// CH=<chromium> node sync.mjs [url] (default: the built file; LIVE=1 for https://iss.aethercosmology.com)
import { chromium } from 'playwright-core';
import { fileURLToPath } from 'url';
import path from 'path';
const here = path.dirname(fileURLToPath(import.meta.url));
const url = process.argv[2] || (process.env.LIVE ? 'https://iss.aethercosmology.com/' : 'file://' + path.join(here, '../satellites/site/index.html'));
const VP = [[1920, 1080], [1680, 1050], [1440, 900], [1280, 720], [1024, 768], [820, 1180], [390, 844], [360, 740]];
const b = await chromium.launch({ executablePath: process.env.CH });
let fails = 0;
const fail = (...a) => { fails++; console.log('FAIL', ...a); };
for (const [w, h] of VP) {
const p = await b.newPage({ viewport: { width: w, height: h }, deviceScaleFactor: 1 });
const errs = [];
p.on('pageerror', e => errs.push(e.message));
p.on('console', m => m.type() === 'error' && errs.push(m.text()));
await p.goto(url, { waitUntil: 'load' });
await p.waitForTimeout(1500);
const r = await p.evaluate(() => {
const out = { overflow: document.documentElement.scrollWidth - innerWidth, blocked: [], empty: [], clipped: [] };
for (const el of document.querySelectorAll('button, input, a')) {
el.scrollIntoView({ block: 'center' });
const b = el.getClientRects()[0]; // first line box: inline links can wrap
if (!b || !b.width) continue;
const hit = document.elementFromPoint(b.left + b.width / 2, b.top + b.height / 2);
if (hit !== el && !el.contains(hit)) out.blocked.push((el.id || el.textContent.trim().slice(0, 20)) + ' <- ' + (hit && (hit.id || hit.className || hit.tagName)));
}
for (const dd of document.querySelectorAll('.ro dd')) {
if (/^—?$/.test(dd.textContent.trim())) out.empty.push(dd.closest('.panel').dataset.key + ':' + dd.dataset.f);
if (dd.scrollWidth > dd.clientWidth + 1) out.clipped.push(dd.closest('.panel').dataset.key + ':' + dd.dataset.f + ' "' + dd.textContent + '"');
}
for (const c of document.querySelectorAll('.map canvas')) if (!c.width) out.empty.push('canvas');
window.scrollTo(0, 0);
return out;
});
if (errs.length) fail(w, h, 'errors', errs.slice(0, 3));
if (r.overflow > 0) fail(w, h, 'horizontal overflow', r.overflow);
if (r.blocked.length) fail(w, h, 'blocked', r.blocked.slice(0, 5));
if (r.empty.length) fail(w, h, 'empty', r.empty);
if (r.clipped.length) fail(w, h, 'clipped', r.clipped);
await p.screenshot({ path: path.join(here, `out/sync_${w}x${h}.png`) });
await p.close();
}
// behaviour: pause, seek late in orbit 1, check readouts against the data
const p = await b.newPage({ viewport: { width: 1440, height: 900 } });
await p.goto(url, { waitUntil: 'load' });
await p.waitForTimeout(500);
const chk = await p.evaluate(async () => {
document.getElementById('play').click();
const r = document.getElementById('time'); r.value = '5000'; r.dispatchEvent(new Event('input'));
await new Promise(f => setTimeout(f, 400));
const g = k => [...document.querySelectorAll(`.panel[data-key="${k}"] .ro dd`)].map(d => d.textContent);
return { globe: g('globe'), ae: g('ae'), dp: g('dualpole'), me: g('mercator'), clock: document.getElementById('clock').textContent, orbit: document.getElementById('orbit').textContent, play: document.getElementById('play').getAttribute('aria-label') };
});
console.log(JSON.stringify(chk));
if (!/level/.test(chk.globe[2])) fail('globe signal not level', chk.globe[2]);
if (!/behind/.test(chk.ae[2])) fail('AE signal not behind at t=5000', chk.ae[2]);
if (chk.play !== 'Play') fail('pause did not toggle');
await p.screenshot({ path: path.join(here, 'out/sync_t5000.png') });
// "Line up signal now": level at once, drifts after, before the chosen moment level; "Each orbit" restores the default
const lu = await p.evaluate(async () => {
const r = document.getElementById('time'), wait = () => new Promise(f => setTimeout(f, 300));
const ae = () => document.querySelector('.panel[data-key="ae"] [data-f="s"]').textContent;
const go = v => { r.value = String(v); r.dispatchEvent(new Event('input')); };
go(20000); await wait();
document.getElementById('sync').click(); await wait();
const at0 = ae(), info = document.getElementById('syncinfo').textContent;
go(20600); await wait(); const after = ae();
go(19000); await wait(); const before = ae();
document.getElementById('syncauto').click(); go(20600); await wait();
return { at0, info, after, before, auto: ae(), infoAuto: document.getElementById('syncinfo').textContent, autoHidden: document.getElementById('syncauto').hidden };
});
console.log(JSON.stringify(lu));
if (!/level/.test(lu.at0)) fail('not level right after lining up', lu.at0);
if (!/behind|ahead/.test(lu.after)) fail('no drift 10 min after lining up', lu.after);
if (!/level/.test(lu.before)) fail('not level before the chosen moment', lu.before);
if (!/UTC/.test(lu.info) || !/equator/.test(lu.infoAuto) || !lu.autoHidden) fail('line-up status text', lu.info, lu.infoAuto);
if (lu.auto === lu.after) fail('Each orbit did not restore the default');
// "Real-time correction": the signal trails by the latency and moves at the map speed; its Doppler still says the measured speed
const rc = await p.evaluate(async () => {
const r = document.getElementById('time'), lat = document.getElementById('lat'), wait = () => new Promise(f => setTimeout(f, 300));
const q = (k, f) => document.querySelector(`.panel[data-key="${k}"] [data-f="${f}"]`).textContent;
r.value = '5000'; r.dispatchEvent(new Event('input'));
document.getElementById('corr').click(); await wait();
const l10 = q('ae', 's'), vs10 = q('ae', 'vs'), syncDisabled = document.getElementById('sync').disabled;
lat.value = '0'; lat.dispatchEvent(new Event('input')); await wait();
const l0 = q('ae', 's'), vs0 = q('ae', 'vs'), v0 = q('ae', 'v'), g0 = q('globe', 'vs');
document.getElementById('corr').click(); await wait();
return { l10, vs10, syncDisabled, l0, vs0, v0, g0, off: q('ae', 's'), offvs: q('ae', 'vs'), info: document.getElementById('syncinfo').textContent };
});
console.log(JSON.stringify(rc));
if (!/^10 s behind/.test(rc.l10)) fail('correction latency 10 s not shown', rc.l10);
if (!/level/.test(rc.l0) || rc.vs0 !== rc.v0) fail('zero latency: signal should be level and move at the map speed', rc.l0, rc.vs0, rc.v0);
if (/×1\.00/.test(rc.vs0)) fail('corrected signal speed should differ from its Doppler on the AE map', rc.vs0);
if (!/×1\.00/.test(rc.offvs) || !/×1\.00/.test(rc.g0)) fail('uncorrected / globe signal speed should equal its Doppler', rc.offvs, rc.g0);
if (!rc.syncDisabled || /Corrected/.test(rc.info)) fail('line-up controls not disabled / restored', rc.syncDisabled, rc.info);
await b.close();
console.log(fails ? `${fails} failures` : 'sync OK');
process.exit(fails ? 1 : 0);