fe-map: data and tolerance changes re-score without moving cities; caption for the maximum-only case
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+21
-10
@@ -203,7 +203,6 @@ footer{padding-block:18px 30px;color:var(--faint);font-size:13px}
|
||||
</table>
|
||||
<div class="hist"><span>Map distance ÷ timed length, every flight</span><canvas id="hist" aria-label="Histogram of map distance divided by timed length"></canvas></div>
|
||||
<div><h2 style="margin-bottom:6px">Farthest out</h2><ul class="worst" id="worst"></ul></div>
|
||||
<p class="note" id="flnote" hidden>Flight times are off, so every remaining measurement is a maximum distance. Any flat map drawn small enough meets them all, so a score of 0 here says nothing about the shape. The triangles do not depend on the layout.</p>
|
||||
<p class="note" id="globenote">Globe: the same rules applied to great-circle distances on a sphere of radius 6,371 km.</p>
|
||||
</aside>
|
||||
</section>
|
||||
@@ -472,7 +471,16 @@ function restart() {
|
||||
$("#run").textContent = "Solve";
|
||||
update(true);
|
||||
}
|
||||
function continueSolve() { // data or tolerance changed: keep the current layout and solve again
|
||||
function dataChanged() { // data or tolerance changed: re-score the current layout; only Solve moves the cities
|
||||
if (!sv) return restart();
|
||||
buildActive();
|
||||
const asDrawn = sv.total === 0 && !sv.done && !sv.changed, watch = sv.watch, X = Float64Array.from(sv.X);
|
||||
sv = makeSolver(2, X);
|
||||
if (running) sv.watch = watch; // mid-solve: carry on with the new set
|
||||
else { sv.watch = asDrawn; sv.changed = !asDrawn; $("#run").textContent = "Solve"; }
|
||||
update(true);
|
||||
}
|
||||
function continueSolve() { // Solve pressed on a finished layout: solve again from it
|
||||
if (!sv) return restart();
|
||||
buildActive(); const X = Float64Array.from(sv.X); sv = makeSolver(2, X); sv.watch = false; running = true; $("#run").textContent = "Pause"; update(true);
|
||||
}
|
||||
@@ -499,7 +507,6 @@ function globeScore() {
|
||||
let SC = null, GS = null;
|
||||
function update(full) {
|
||||
SC = score(sv.X, 2); if (full) GS = globeScore();
|
||||
$("#flnote").hidden = S.fl;
|
||||
const nAct = { rt: act.l.filter(l => l.kind === "rt").length, rr: act.l.filter(l => l.kind === "rail" || l.kind === "road").length, sat: act.l.filter(l => l.kind === "sat").length };
|
||||
const row = (label, a, b, n, cls) => `<tr><th scope="row">${label}</th><td class="${a ? cls : "ok"}">${fmt(a)}<span class="of"> / ${fmt(n)}</span></td><td class="${b ? cls : "ok"}">${fmt(b)}</td></tr>`;
|
||||
const nf = S.fl ? FL.length : 0;
|
||||
@@ -699,13 +706,13 @@ $("#run").addEventListener("click", () => {
|
||||
running = !running; $("#run").textContent = running ? "Pause" : "Solve";
|
||||
});
|
||||
$("#reset").addEventListener("click", () => restart());
|
||||
for (const [id, key] of [["#d-fl", "fl"], ["#d-rt", "rt"], ["#d-rr", "rr"], ["#d-sat", "sat"]]) $(id).addEventListener("change", e => { S[key] = e.target.checked; continueSolve(); });
|
||||
for (const [id, key] of [["#d-fl", "fl"], ["#d-rt", "rt"], ["#d-rr", "rr"], ["#d-sat", "sat"]]) $(id).addEventListener("change", e => { S[key] = e.target.checked; dataChanged(); });
|
||||
$("#lo").addEventListener("input", e => { $("#lov").textContent = e.target.value + "%"; });
|
||||
$("#hi").addEventListener("input", e => { $("#hiv").textContent = e.target.value + "%"; });
|
||||
$("#lo").addEventListener("change", e => { S.lo = 1 - e.target.value / 100; continueSolve(); });
|
||||
$("#hi").addEventListener("change", e => { S.hi = e.target.value / 100 - 1; continueSolve(); });
|
||||
$("#lo").addEventListener("change", e => { S.lo = 1 - e.target.value / 100; dataChanged(); });
|
||||
$("#hi").addEventListener("change", e => { S.hi = e.target.value / 100 - 1; dataChanged(); });
|
||||
for (const b of document.querySelectorAll("#light button")) b.addEventListener("click", () => {
|
||||
S.vac = b.dataset.v === "vacuum"; for (const x of document.querySelectorAll("#light button")) x.setAttribute("aria-pressed", String(x === b)); continueSolve();
|
||||
S.vac = b.dataset.v === "vacuum"; for (const x of document.querySelectorAll("#light button")) x.setAttribute("aria-pressed", String(x === b)); dataChanged();
|
||||
});
|
||||
$("#dl").addEventListener("click", () => {
|
||||
const blob = new Blob([JSON.stringify(D)], { type: "application/json" }); const a = document.createElement("a");
|
||||
@@ -790,10 +797,14 @@ const pace = sv => sv.total < 100 ? 0.35 : sv.stage === 0 ? (sv.total < 200 ? 1
|
||||
let frame = 0, capText = "";
|
||||
function caption(k) {
|
||||
let t;
|
||||
if (!sv.done && sv.total === 0 && !running)
|
||||
if (sv.changed && sv.total === 0 && !running)
|
||||
t = `<b>Measurements changed: ${fmt(k)} broken on this layout.</b> It was solved for the previous set. Press <b>Solve</b> to fit it to these.`;
|
||||
else if (!sv.done && sv.total === 0 && !running)
|
||||
t = `<b>${START_LABEL[S.start]}, as drawn: ${fmt(k)} measurements broken.</b> Red: a flight longer than its aircraft can fly, or (dashed) a ping faster than light. Amber: a flight far shorter than it takes to fly. Press <b>Solve</b> to let the cities move.`;
|
||||
else if (!sv.done)
|
||||
t = `${running ? "Moving" : "Paused moving"} the cities to fit the measurements. Grey lines: each city's move from the starting map.`;
|
||||
else if (!S.fl)
|
||||
t = `<b>Done: ${fmt(k)} measurements broken.</b> Without flight times, every measurement left is a maximum distance (no farther apart than this), and pulling cities closer always meets a maximum. These measurements can show that a map as drawn is wrong, but no flat map can fail them.`;
|
||||
else if (sel >= 0) {
|
||||
let n = 0, kept = 0, long = 0, short = 0;
|
||||
for (const f of act.f) if (f.a === sel || f.b === sel) { n++; const r = SC.ratio[f.i]; if (r > 1 + S.hi) long++; else if (r < 1 - S.lo) short++; else kept++; }
|
||||
@@ -805,12 +816,12 @@ function loop() {
|
||||
frame++;
|
||||
if (sv && running && !sv.done) {
|
||||
if (sv.watch) { sv.acc += pace(sv); const n = Math.floor(sv.acc); sv.acc -= n; if (n) work(sv, 9, n); } else work(sv, 9, 400);
|
||||
if (sv.done) { running = false; $("#run").textContent = "Solve"; if (sv.watch && sel < 0) sel = SANTIAGO; }
|
||||
if (sv.done) { running = false; $("#run").textContent = "Solve"; if (sv.watch && sel < 0 && S.fl) sel = SANTIAGO; }
|
||||
}
|
||||
if (sv && (running || frame % 6 === 0)) update(frame % 30 === 0);
|
||||
if (sv) {
|
||||
const k = SC.long.length + SC.short.length + SC.rt.length + SC.rr.length + SC.sat.length;
|
||||
const head = sv.done ? (sv.isBest ? `Best of ${D.stats.n_starts}` : "Done") : sv.total === 0 && !running ? `${START_LABEL[S.start]}, as drawn` : (running ? "Solving, " : "Paused, ") + STAGE(sv.stage);
|
||||
const head = sv.done ? (sv.isBest ? `Best of ${D.stats.n_starts}` : "Done") : sv.total === 0 && !running ? (sv.changed ? "Measurements changed" : `${START_LABEL[S.start]}, as drawn`) : (running ? "Solving, " : "Paused, ") + STAGE(sv.stage);
|
||||
$("#status").innerHTML = `${head} · broken <b>${fmt(k)}</b>`;
|
||||
caption(k);
|
||||
}
|
||||
|
||||
+21
-10
@@ -203,7 +203,6 @@ footer{padding-block:18px 30px;color:var(--faint);font-size:13px}
|
||||
</table>
|
||||
<div class="hist"><span>Map distance ÷ timed length, every flight</span><canvas id="hist" aria-label="Histogram of map distance divided by timed length"></canvas></div>
|
||||
<div><h2 style="margin-bottom:6px">Farthest out</h2><ul class="worst" id="worst"></ul></div>
|
||||
<p class="note" id="flnote" hidden>Flight times are off, so every remaining measurement is a maximum distance. Any flat map drawn small enough meets them all, so a score of 0 here says nothing about the shape. The triangles do not depend on the layout.</p>
|
||||
<p class="note" id="globenote">Globe: the same rules applied to great-circle distances on a sphere of radius 6,371 km.</p>
|
||||
</aside>
|
||||
</section>
|
||||
@@ -472,7 +471,16 @@ function restart() {
|
||||
$("#run").textContent = "Solve";
|
||||
update(true);
|
||||
}
|
||||
function continueSolve() { // data or tolerance changed: keep the current layout and solve again
|
||||
function dataChanged() { // data or tolerance changed: re-score the current layout; only Solve moves the cities
|
||||
if (!sv) return restart();
|
||||
buildActive();
|
||||
const asDrawn = sv.total === 0 && !sv.done && !sv.changed, watch = sv.watch, X = Float64Array.from(sv.X);
|
||||
sv = makeSolver(2, X);
|
||||
if (running) sv.watch = watch; // mid-solve: carry on with the new set
|
||||
else { sv.watch = asDrawn; sv.changed = !asDrawn; $("#run").textContent = "Solve"; }
|
||||
update(true);
|
||||
}
|
||||
function continueSolve() { // Solve pressed on a finished layout: solve again from it
|
||||
if (!sv) return restart();
|
||||
buildActive(); const X = Float64Array.from(sv.X); sv = makeSolver(2, X); sv.watch = false; running = true; $("#run").textContent = "Pause"; update(true);
|
||||
}
|
||||
@@ -499,7 +507,6 @@ function globeScore() {
|
||||
let SC = null, GS = null;
|
||||
function update(full) {
|
||||
SC = score(sv.X, 2); if (full) GS = globeScore();
|
||||
$("#flnote").hidden = S.fl;
|
||||
const nAct = { rt: act.l.filter(l => l.kind === "rt").length, rr: act.l.filter(l => l.kind === "rail" || l.kind === "road").length, sat: act.l.filter(l => l.kind === "sat").length };
|
||||
const row = (label, a, b, n, cls) => `<tr><th scope="row">${label}</th><td class="${a ? cls : "ok"}">${fmt(a)}<span class="of"> / ${fmt(n)}</span></td><td class="${b ? cls : "ok"}">${fmt(b)}</td></tr>`;
|
||||
const nf = S.fl ? FL.length : 0;
|
||||
@@ -699,13 +706,13 @@ $("#run").addEventListener("click", () => {
|
||||
running = !running; $("#run").textContent = running ? "Pause" : "Solve";
|
||||
});
|
||||
$("#reset").addEventListener("click", () => restart());
|
||||
for (const [id, key] of [["#d-fl", "fl"], ["#d-rt", "rt"], ["#d-rr", "rr"], ["#d-sat", "sat"]]) $(id).addEventListener("change", e => { S[key] = e.target.checked; continueSolve(); });
|
||||
for (const [id, key] of [["#d-fl", "fl"], ["#d-rt", "rt"], ["#d-rr", "rr"], ["#d-sat", "sat"]]) $(id).addEventListener("change", e => { S[key] = e.target.checked; dataChanged(); });
|
||||
$("#lo").addEventListener("input", e => { $("#lov").textContent = e.target.value + "%"; });
|
||||
$("#hi").addEventListener("input", e => { $("#hiv").textContent = e.target.value + "%"; });
|
||||
$("#lo").addEventListener("change", e => { S.lo = 1 - e.target.value / 100; continueSolve(); });
|
||||
$("#hi").addEventListener("change", e => { S.hi = e.target.value / 100 - 1; continueSolve(); });
|
||||
$("#lo").addEventListener("change", e => { S.lo = 1 - e.target.value / 100; dataChanged(); });
|
||||
$("#hi").addEventListener("change", e => { S.hi = e.target.value / 100 - 1; dataChanged(); });
|
||||
for (const b of document.querySelectorAll("#light button")) b.addEventListener("click", () => {
|
||||
S.vac = b.dataset.v === "vacuum"; for (const x of document.querySelectorAll("#light button")) x.setAttribute("aria-pressed", String(x === b)); continueSolve();
|
||||
S.vac = b.dataset.v === "vacuum"; for (const x of document.querySelectorAll("#light button")) x.setAttribute("aria-pressed", String(x === b)); dataChanged();
|
||||
});
|
||||
$("#dl").addEventListener("click", () => {
|
||||
const blob = new Blob([JSON.stringify(D)], { type: "application/json" }); const a = document.createElement("a");
|
||||
@@ -790,10 +797,14 @@ const pace = sv => sv.total < 100 ? 0.35 : sv.stage === 0 ? (sv.total < 200 ? 1
|
||||
let frame = 0, capText = "";
|
||||
function caption(k) {
|
||||
let t;
|
||||
if (!sv.done && sv.total === 0 && !running)
|
||||
if (sv.changed && sv.total === 0 && !running)
|
||||
t = `<b>Measurements changed: ${fmt(k)} broken on this layout.</b> It was solved for the previous set. Press <b>Solve</b> to fit it to these.`;
|
||||
else if (!sv.done && sv.total === 0 && !running)
|
||||
t = `<b>${START_LABEL[S.start]}, as drawn: ${fmt(k)} measurements broken.</b> Red: a flight longer than its aircraft can fly, or (dashed) a ping faster than light. Amber: a flight far shorter than it takes to fly. Press <b>Solve</b> to let the cities move.`;
|
||||
else if (!sv.done)
|
||||
t = `${running ? "Moving" : "Paused moving"} the cities to fit the measurements. Grey lines: each city's move from the starting map.`;
|
||||
else if (!S.fl)
|
||||
t = `<b>Done: ${fmt(k)} measurements broken.</b> Without flight times, every measurement left is a maximum distance (no farther apart than this), and pulling cities closer always meets a maximum. These measurements can show that a map as drawn is wrong, but no flat map can fail them.`;
|
||||
else if (sel >= 0) {
|
||||
let n = 0, kept = 0, long = 0, short = 0;
|
||||
for (const f of act.f) if (f.a === sel || f.b === sel) { n++; const r = SC.ratio[f.i]; if (r > 1 + S.hi) long++; else if (r < 1 - S.lo) short++; else kept++; }
|
||||
@@ -805,12 +816,12 @@ function loop() {
|
||||
frame++;
|
||||
if (sv && running && !sv.done) {
|
||||
if (sv.watch) { sv.acc += pace(sv); const n = Math.floor(sv.acc); sv.acc -= n; if (n) work(sv, 9, n); } else work(sv, 9, 400);
|
||||
if (sv.done) { running = false; $("#run").textContent = "Solve"; if (sv.watch && sel < 0) sel = SANTIAGO; }
|
||||
if (sv.done) { running = false; $("#run").textContent = "Solve"; if (sv.watch && sel < 0 && S.fl) sel = SANTIAGO; }
|
||||
}
|
||||
if (sv && (running || frame % 6 === 0)) update(frame % 30 === 0);
|
||||
if (sv) {
|
||||
const k = SC.long.length + SC.short.length + SC.rt.length + SC.rr.length + SC.sat.length;
|
||||
const head = sv.done ? (sv.isBest ? `Best of ${D.stats.n_starts}` : "Done") : sv.total === 0 && !running ? `${START_LABEL[S.start]}, as drawn` : (running ? "Solving, " : "Paused, ") + STAGE(sv.stage);
|
||||
const head = sv.done ? (sv.isBest ? `Best of ${D.stats.n_starts}` : "Done") : sv.total === 0 && !running ? (sv.changed ? "Measurements changed" : `${START_LABEL[S.start]}, as drawn`) : (running ? "Solving, " : "Paused, ") + STAGE(sv.stage);
|
||||
$("#status").innerHTML = `${head} · broken <b>${fmt(k)}</b>`;
|
||||
caption(k);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user