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
+87
View File
@@ -0,0 +1,87 @@
// The Best Flat Map (femap/site/index.html): errors, overflow, controls clickable, score filled, at 8 viewports;
// then the solver: it reduces the broken count from the North-pole AE start, data toggles change the count,
// and the 3D release closes round.
// CH=<chromium> node femap.mjs [url] (default: the built file; LIVE=1 for https://fe-map.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://fe-map.aethercosmology.com/' : 'file://' + path.join(here, '../femap/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(2500);
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];
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) && !(el.tagName === 'INPUT' && hit && hit.closest('label') === el.closest('label')))
out.blocked.push((el.id || el.textContent.trim().slice(0, 20)) + ' <- ' + (hit && (hit.id || hit.className || hit.tagName)));
}
if (document.querySelectorAll('#score tr').length < 6) out.empty.push('score');
if (!document.querySelector('#bands tbody tr')) out.empty.push('bands');
for (const c of document.querySelectorAll('canvas')) if (!c.width || !c.height) out.empty.push('canvas ' + c.id);
for (const td of document.querySelectorAll('#score td')) if (td.scrollWidth > td.clientWidth + 1) out.clipped.push(td.textContent);
for (const el of document.querySelectorAll('.worst b')) if (el.scrollWidth > el.clientWidth + 1 && el.closest('li').getBoundingClientRect().width < 120) out.clipped.push(el.textContent);
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.slice(0, 4));
await p.screenshot({ path: path.join(here, `out/femap_${w}x${h}.png`) });
await p.close();
}
// behaviour
const p = await b.newPage({ viewport: { width: 1440, height: 900 } });
const errs = []; p.on('pageerror', e => errs.push(e.message));
await p.goto(url, { waitUntil: 'load' });
const broken = () => p.evaluate(() => +(document.getElementById('status').textContent.match(/broken ([\d,]+)/) || [0, 'NaN'])[1].replace(/,/g, ''));
await p.waitForTimeout(300);
const k0 = await broken();
await p.waitForFunction(() => /^Done/.test(document.getElementById('status').textContent), null, { timeout: 120000 });
const k1 = await broken();
console.log('AE start: broken', k0, '->', k1);
if (!(k1 < k0)) fail('solver did not reduce the broken count', k0, k1);
if (k1 > 60) fail('live solve far from the offline best', k1);
await p.screenshot({ path: path.join(here, 'out/femap_solved.png') });
// best of N: loads without solving and matches the build
await p.click('#starts button[data-start="best"]');
await p.waitForTimeout(400);
const kb = await broken();
const want = await p.evaluate(() => D.stats.best.broken);
console.log('Best start: broken', kb, 'build says', want);
if (kb !== want) fail('best layout count differs from the build', kb, want);
// flights off: the plane can satisfy limits alone by shrinking
await p.click('#d-fl');
await p.waitForFunction(() => /^Done/.test(document.getElementById('status').textContent), null, { timeout: 120000 });
const kf = await broken();
console.log('Limits only: broken', kf);
if (kf > 5) fail('limits alone should be (nearly) satisfiable on a plane', kf);
await p.click('#d-fl');
await p.click('#starts button[data-start="best"]');
await p.waitForTimeout(300);
// 3D release
await p.click('#lift');
await p.waitForFunction(() => document.getElementById('r3s').textContent === 'done', null, { timeout: 180000 });
const r3 = await p.evaluate(() => ({ b: document.getElementById('r3b').textContent, w: document.getElementById('r3w').textContent, r: document.getElementById('r3r').textContent, f: document.getElementById('r3f').textContent }));
console.log('3D', JSON.stringify(r3));
if (!(parseInt(r3.w) > 120)) fail('3D release did not wrap round', r3.w);
if (!(+r3.b.replace(/,/g, '') < +r3.f.replace(/,/g, ''))) fail('3D not better than the plane', r3);
await p.locator('.lift').screenshot({ path: path.join(here, 'out/femap_3d.png') });
if (errs.length) fail('errors', errs.slice(0, 3));
console.log(fails ? `femap: ${fails} FAILS` : 'femap OK');
await b.close();
process.exit(fails ? 1 : 0);
+7
View File
@@ -0,0 +1,7 @@
import { chromium } from 'playwright-core';
import { PAGE } from './paths.mjs';
const b = await chromium.launch({ executablePath: process.env.CH }); const errs = [];
for (const [w, h] of [[1440, 900], [390, 844]]) { const p = await b.newPage({ viewport: { width: w, height: h } }); p.on('pageerror', e => errs.push(e.message));
await p.goto(PAGE('site/index.html')); await p.waitForTimeout(1500); await p.screenshot({ path: `out/hero_${w}.png` });
const t = await p.evaluate(() => [document.querySelector('.tl').innerText.slice(0, 50), document.getElementById('step1').textContent.slice(0, 40), document.documentElement.scrollWidth]); console.log(w, JSON.stringify(t)); await p.close() }
console.log('ERRORS', JSON.stringify(errs)); await b.close();
+4 -12
View File
@@ -1,5 +1,4 @@
// New sections: no errors; flat panels at zero correction equal the page's straight-line counts for every source;
// the last slider stop leaves nothing inconsistent; screenshots of each section.
// Networks and code sections: no errors, no horizontal overflow, screenshots; the flat-fit section stays removed.
import { chromium } from 'playwright-core';
import { PAGE, HAND } from './paths.mjs';
const b = await chromium.launch({ executablePath: process.env.CH });
@@ -8,18 +7,11 @@ for (const [w, h] of [[1440, 900], [390, 844]]) {
const p = await b.newPage({ viewport: { width: w, height: h } });
p.on('pageerror', e => errs.push(`${w}: ${e.message}`)); p.on('console', m => { if (m.type() === 'error' && !/net::|ERR_/.test(m.text())) errs.push(`${w}: ${m.text()}`) });
await p.goto(PAGE('site/index.html')); await p.waitForTimeout(1500);
for (const src of ['all', 'ours', 'gp', 'home']) {
await p.selectOption('#srcsel', src); await p.waitForTimeout(150);
const r = await p.evaluate(() => { const out = {};
for (const [m, , k] of FFM) { const f = D.flat.fits[S.src][k]; out[m] = [f.budgets[0].inconsistent.length, liveBad(m, 'line'), f.budgets[f.budgets.length - 1].inconsistent.length, f.routes, LP.length] }
return out });
for (const [m, [b0, page, last, nr, lp]] of Object.entries(r)) if (b0 !== page || last !== 0 || nr !== lp) fails.push(`${w} ${src} ${m}: fit ${b0} page ${page} last ${last} routes ${nr}/${lp}`);
}
if (await p.locator('h2.sec', { hasText: 'flat arrangements' }).count()) fails.push(`${w}: flat-fit section present`);
await p.selectOption('#srcsel', 'all'); await p.waitForTimeout(200);
for (const [sec, name] of [['Networks on the measured paths', 'net'], ['Best attainable flat arrangements', 'ff'], ['Code and data', 'code']]) {
for (const [sec, name] of [['Networks on the measured paths', 'net'], ['Code and data', 'code']]) {
const el = p.locator('h2.sec', { hasText: sec }).locator('xpath=..');
if (name === 'ff') for (const v of [0, 4, 6]) { await p.fill('#ffb', String(v)); await p.dispatchEvent('#ffb', 'input'); await p.waitForTimeout(250); await el.screenshot({ path: `out/sec_${name}${v}_${w}.png` }) }
else await el.screenshot({ path: `out/sec_${name}_${w}.png` });
await el.screenshot({ path: `out/sec_${name}_${w}.png` });
}
const sw = await p.evaluate(() => document.documentElement.scrollWidth);
if (sw !== w) fails.push(`${w}: page scrollWidth ${sw}`);
+15
View File
@@ -0,0 +1,15 @@
// Clicking a red line where routes overlap selects the red route, not one hidden beneath it.
import { chromium } from 'playwright-core';
import { PAGE } from './paths.mjs';
const b = await chromium.launch({ executablePath: process.env.CH }); const p = await b.newPage({ viewport: { width: 1440, height: 900 } });
const errs = []; p.on('pageerror', e => errs.push(e.message));
await p.goto(PAGE('site/index.html')); await p.waitForTimeout(1800);
const pt = await p.evaluate(() => { const ia = LS.findIndex(s => s.name === 'Fortaleza'), ib = LS.findIndex(s => s.name === 'Luanda');
const q = LP.find(x => (x.a === ia && x.b === ib) || (x.a === ib && x.b === ia)); const segs = hits.filter(h => h[0] === q.idx).map(h => h[1]).sort((x, y) => y.length - x.length)[0];
const k = Math.floor(segs.length / 2), r = document.getElementById('cv').getBoundingClientRect(); return { x: r.left + segs[k][0], y: r.top + segs[k][1], want: LS[q.a].name + '–' + LS[q.b].name } });
await p.mouse.move(pt.x, pt.y); await p.waitForTimeout(250);
const tip = (await p.innerText('#tip')).replace(/\s+/g, ' ');
await p.mouse.click(pt.x, pt.y); await p.waitForTimeout(300);
const got = await p.evaluate(() => [LS[LP[S.sel].a].name + '–' + LS[LP[S.sel].b].name, rt(LP[S.sel]).tier]);
console.log('clicked SACS line; selected', got, 'expected', pt.want, '| tooltip:', tip.slice(0, 200));
console.log(got[1] === 2 ? 'OK: top (red) route selected' : 'FAIL', 'ERRORS', JSON.stringify(errs)); await b.close();
+5 -1
View File
@@ -13,6 +13,10 @@ node card_sel.mjs # every route x map x size: no
node trunc.mjs | tail -1 # no truncated text in the route card
node view_btns.mjs | tail -2 # zoom / reset buttons
node offline.mjs | tail -3 # site/index.html with the network blocked
node newsec.mjs | tail -2 # networks, flat arrangements, code: counts and layout
node newsec.mjs | tail -2 # networks and code sections: layout, no flat-fit section
node overlap.mjs | tail -1 # a click where routes overlap selects the top-drawn route
node hero.mjs | tail -1 # hero renders at desktop and phone width
node mode.mjs | cut -c1-60 # cable / straight toggle changes the counts
node sync.mjs | tail -1 # The Sync Test (satellites/): 8 viewports, readouts, controls
[ "${LIVE:-0}" = 1 ] && node live.mjs # LIVE=1: the public site
node femap.mjs | tail -1 # The Best Flat Map (femap/): 8 viewports, live solve, 3D release
+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);