(function () {
var box = document.getElementById("map-data");
var panel = document.getElementById("detail");
if (!box || !panel) return;
var data = JSON.parse(box.textContent);
var D = data.stops;
var project = data.project;
var map = document.querySelector(".map");
var stations = [].slice.call(document.querySelectorAll("svg .station"));
var gutters = [].slice.call(document.querySelectorAll("svg.rails"));
var rows = {};
[].slice.call(document.querySelectorAll("li.stop")).forEach(function (e) {
rows[+e.dataset.stop] = e;
});
var drawn = Object.keys(rows).map(Number);
function eachRow(fn) {
drawn.forEach(function (k) {
fn(rows[k], k);
});
}
var resting = panel.innerHTML;
var at = null;
function esc(s) {
return String(s == null ? "" : s).replace(/[&<>"]/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """ }[c];
});
}
function routeTo(i) {
var out = [];
while (i !== null && i !== undefined) {
out.unshift(i);
i = D[i].p;
}
return out;
}
function drawRoute(route) {
gutters.forEach(function (svg) {
var g = svg.querySelector(".route");
g.innerHTML = "";
if (!route.length) return;
var step = +svg.dataset.step,
origin = +svg.dataset.origin,
row = +svg.dataset.row;
var x = function (i) {
return origin + D[i].d * step;
};
var y = function (i) {
return D[i].r * row + row / 2;
};
var d = "";
route.forEach(function (n, k) {
if (k === 0) {
d += "M" + x(n) + " " + y(n);
return;
}
var px = x(route[k - 1]);
d +=
" V" + (y(n) - 7) + " Q" + px + " " + y(n) + " " + (px + 7) +
" " + y(n) + " H" + x(n);
});
g.insertAdjacentHTML("beforeend", '<path d="' + d + '"/>');
route.forEach(function (n) {
g.insertAdjacentHTML(
"beforeend",
'<circle cx="' + x(n) + '" cy="' + y(n) + '" r="4.2"/>'
);
});
});
}
function section(head, body) {
return body ? "<h3>" + head + "</h3><p class=\"prose\">" + esc(body) + "</p>" : "";
}
function links(head, list) {
if (!list.length) return "";
return (
"<h3>" + head + " · " + list.length + "</h3><ul class=\"plain\">" +
list
.map(function (k) {
return (
'<li><a class="go" href="#" data-go="' + k + '">' + esc(D[k].a) +
"</a>" + esc(D[k].t) + "</li>"
);
})
.join("") +
"</ul>"
);
}
function waiting(n) {
if (!n.bn) return "";
var out = links("Does not close until these close", n.bl);
if (!out) out = "<h3>Does not close until these close · " + n.bn + "</h3>";
var away = n.bn - n.bl.length;
if (away > 0) {
out +=
'<p class="prose">' + away + " of them " + (away === 1 ? "is" : "are") +
" folded away.</p>";
}
return out;
}
function notes(n) {
if (!n.nt.length) return "";
var many = n.nt.length > 1;
return (
"<h3>Notes · " + n.nt.length + "</h3>" +
n.nt
.map(function (x, k) {
var last = k === n.nt.length - 1;
return (
"<details" + (last || !many ? " open" : "") + "><summary>" +
(many ? "note " + (k + 1) + " · " : "") +
'<span class="when">' + esc(x.at.slice(0, 10)) + "</span>" +
"</summary><p class=\"prose\">" + esc(x.n) + "</p></details>"
);
})
.join("")
);
}
function around(i) {
var n = D[i];
var beside = [];
var under = [];
D.forEach(function (o, k) {
if (k !== i && o.p === n.p && n.p !== null) beside.push(k);
if (o.p === i) under.push(k);
});
return links("Beside it", beside) + links("Under it", under);
}
function card(n) {
var pairs = [
["line", n.ln || "a short branch"],
["under", n.p === null ? "a root" : D[n.p].a],
["children", n.c],
["below", n.tb + " nodes, " + n.ob + " open"],
["opened", n.op || "--"],
];
if (n.cl) pairs.push(["closed", n.cl]);
if (n.rf.length) pairs.push(["refs", n.rf.join("<br>")]);
if (n.gv.length) pairs.push(["governs", n.gv.join(", ")]);
return (
"<h3>Card</h3><dl>" +
pairs
.map(function (p) {
return "<dt>" + p[0] + "</dt><dd>" + p[1] + "</dd>";
})
.join("") +
"</dl>"
);
}
function show(i, quiet) {
var n = D[i];
if (!n || n.r === null) return;
var route = routeTo(i);
var onRoute = {};
route.forEach(function (k) {
onRoute[k] = true;
});
eachRow(function (e, k) {
e.classList.toggle("on", k === i);
e.classList.toggle("onroute", !!onRoute[k]);
});
stations.forEach(function (e) {
e.classList.toggle("on", !!onRoute[+e.dataset.stop]);
});
map.classList.add("routing");
drawRoute(route);
panel.innerHTML =
'<div class="sheet"><span class="grab"></span>' +
'<span class="code">' + esc(n.a) + "</span>" +
'<button class="close" type="button">Close</button></div>' +
'<p class="code">' + esc(n.a) + " · " + esc(n.k) + " · " + esc(n.s) +
(n.b ? " · blocks its parent" : "") +
(n.fc ? " · FALSE CLOSE" : "") + "</p>" +
"<h2>" + esc(n.t) + "</h2>" +
section("Why it was born", n.w) +
section("Outcome", n.o) +
waiting(n) +
notes(n) +
around(i) +
"<h3>The route here · " + route.length + " stops</h3>" +
'<ol class="plain route-list">' +
route
.map(function (k, j) {
return (
'<li class="' + (j === route.length - 1 ? "last" : "") + '">' +
'<a class="go" href="#" data-go="' + k + '">' + esc(D[k].a) +
"</a>" + esc(D[k].t) + "</li>"
);
})
.join("") +
"</ol>" +
card(n) +
'<p class="onward"><a href="/p/' + encodeURIComponent(project) +
"/why/" + encodeURIComponent(n.a) + '">The full lineage, on its own page</a></p>';
if (!quiet) panel.classList.add("open");
at = i;
try {
history.replaceState(null, "", "#" + encodeURIComponent(n.a));
} catch (e) {
}
}
function jump(i, straight) {
var e = rows[i];
if (!e) return;
e.scrollIntoView({ block: "center", behavior: straight ? "auto" : "smooth" });
}
function rest() {
panel.classList.remove("open");
map.classList.remove("routing");
panel.innerHTML = resting;
eachRow(function (e) {
e.classList.remove("on", "onroute");
});
stations.forEach(function (e) {
e.classList.remove("on");
});
drawRoute([]);
at = null;
}
panel.addEventListener("click", function (e) {
var go = e.target.closest("[data-go]");
if (go) {
e.preventDefault();
var to = +go.dataset.go;
show(to);
jump(to);
return;
}
if (e.target.closest(".close")) rest();
});
eachRow(function (e) {
e.addEventListener("click", function (ev) {
var link = ev.target.closest("a");
if (link && link.classList.contains("fold")) return;
if (link) ev.preventDefault();
show(+e.dataset.stop);
});
});
stations.forEach(function (e) {
e.addEventListener("click", function () {
var i = +e.dataset.stop;
show(i);
jump(i);
});
});
var whereAmI = document.getElementById("here");
if (whereAmI) {
whereAmI.addEventListener("click", function () {
var i = +whereAmI.dataset.stop;
show(i);
jump(i);
});
}
document.querySelectorAll(".legend .line").forEach(function (b) {
b.addEventListener("click", function () {
var on = b.classList.toggle("on");
document.querySelectorAll(".legend .line").forEach(function (o) {
if (o !== b) o.classList.remove("on");
});
eachRow(function (e, k) {
e.classList.toggle("faded", on && D[k].ln !== b.dataset.line);
});
});
});
function reach(list) {
var open = {};
var need = 0;
list.forEach(function (i) {
if (D[i].r !== null) return;
need = Math.max(need, D[i].d + 1);
var k = D[i].p;
while (k !== null && k !== undefined) {
if (folded[D[k].a]) open[D[k].a] = true;
k = D[k].p;
}
});
var keep = (data.fold || []).filter(function (a) {
return !open[a];
});
var depth = data.depth;
if (depth && need > depth) depth = need;
var parts = [];
if (depth) parts.push("depth=" + depth);
if (keep.length) parts.push("fold=" + keep.join(","));
return "?" + parts.join("&");
}
var folded = {};
(data.fold || []).forEach(function (a) {
folded[a] = true;
});
var find = document.getElementById("find");
var hits = document.getElementById("hits");
var found = [];
var here = [];
var cursor = -1;
if (find) {
find.addEventListener("input", function () {
var v = find.value.trim().toLowerCase();
eachRow(function (e) {
e.classList.remove("hit");
});
found = [];
cursor = -1;
if (v.length > 1) {
D.forEach(function (n, i) {
if ((n.a + " " + n.t + " " + n.w).toLowerCase().indexOf(v) >= 0) {
found.push(i);
if (rows[i]) rows[i].classList.add("hit");
}
});
}
here = found.filter(function (i) {
return D[i].r !== null;
});
var away = found.length - here.length;
if (v.length < 2) {
hits.textContent = "";
} else if (!found.length) {
hits.textContent = "nothing found";
} else {
hits.textContent =
found.length + " found" + (here.length ? " · Enter walks them" : "");
if (away) {
hits.insertAdjacentHTML(
"beforeend",
' · <a href="' + reach(found) + '">' + away + " folded away</a>"
);
}
}
});
find.addEventListener("keydown", function (e) {
if (e.key !== "Enter" || !here.length) return;
e.preventDefault();
cursor = (cursor + (e.shiftKey ? -1 : 1) + here.length) % here.length;
var i = here[cursor];
show(i, true);
jump(i);
hits.textContent = cursor + 1 + " of " + here.length + " on the page";
});
}
document.addEventListener("keydown", function (e) {
if (e.target === find) return;
if (e.key === "Escape") return rest();
if (e.key === "/") {
e.preventDefault();
if (find) find.focus();
return;
}
if (e.key === "ArrowDown" || e.key === "j" || e.key === "ArrowUp" || e.key === "k") {
e.preventDefault();
var back = e.key === "ArrowUp" || e.key === "k";
var seat = at === null ? -1 : drawn.indexOf(at);
var to =
seat < 0
? drawn[0]
: drawn[Math.min(Math.max(seat + (back ? -1 : 1), 0), drawn.length - 1)];
show(to, true);
jump(to);
}
});
function fromHash() {
var alias = decodeURIComponent(location.hash.replace(/^#/, ""));
if (!alias) return;
for (var i = 0; i < D.length; i++) {
if (D[i].a === alias) {
jump(i, true);
return;
}
}
}
window.addEventListener("hashchange", fromHash);
fromHash();
})();