d4ec85add4
Add independent map profiles, offline city lookup, bundled world artwork and collision-free portrait placement with optional person details. Deploy with updated requirements, migrations 0029-0031 and collectstatic.
63 lines
2.7 KiB
JavaScript
63 lines
2.7 KiB
JavaScript
/* Equirectangular coordinates; all layout measurements are in CSS pixels. */
|
|
(() => {
|
|
'use strict';
|
|
function layout(canvas) {
|
|
const pins = [...canvas.querySelectorAll('[data-lat]')];
|
|
const width = canvas.clientWidth;
|
|
const baseHeight = width / 2;
|
|
const placed = [];
|
|
const lines = canvas.querySelector('svg');
|
|
lines.replaceChildren();
|
|
// A fixed minimum width preserves readable labels on phones. The scroll
|
|
// container exposes the entire map without shrinking portraits or hiding people.
|
|
let height = baseHeight;
|
|
pins.forEach(pin => {
|
|
const anchor = {
|
|
x: (Number(pin.dataset.lon) + 180) / 360 * width,
|
|
y: (90 - Number(pin.dataset.lat)) / 180 * baseHeight,
|
|
};
|
|
const w = pin.offsetWidth, h = pin.offsetHeight;
|
|
let point;
|
|
// Deterministic expanding rings. Test complete label+portrait rectangles,
|
|
// and grow the canvas for arbitrarily dense maps instead of accepting overlap.
|
|
for (let ring = 0; !point; ring++) {
|
|
const radius = ring * 12;
|
|
const count = Math.max(1, Math.ceil(2 * Math.PI * radius / 12));
|
|
for (let step = 0; step < count; step++) {
|
|
const angle = step / count * 2 * Math.PI;
|
|
const x = Math.max(w / 2 + 8, Math.min(width - w / 2 - 8, anchor.x + Math.cos(angle) * radius));
|
|
const y = Math.max(h / 2 + 8, anchor.y + Math.sin(angle) * radius);
|
|
if (!placed.some(p => Math.abs(p.x - x) < (p.w + w) / 2 + 8 && Math.abs(p.y - y) < (p.h + h) / 2 + 8)) {
|
|
point = {x, y, w, h}; break;
|
|
}
|
|
}
|
|
}
|
|
placed.push(point);
|
|
height = Math.max(height, point.y + h / 2 + 12);
|
|
pin.style.left = `${point.x - w / 2}px`;
|
|
pin.style.top = `${point.y - h / 2}px`;
|
|
pin.style.visibility = "visible";
|
|
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
line.setAttribute('x1', anchor.x); line.setAttribute('y1', anchor.y);
|
|
line.setAttribute('x2', point.x); line.setAttribute('y2', point.y - h / 2 + 28);
|
|
lines.append(line);
|
|
const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
dot.setAttribute('cx', anchor.x); dot.setAttribute('cy', anchor.y); dot.setAttribute('r', 3);
|
|
lines.append(dot);
|
|
});
|
|
canvas.style.height = `${height}px`;
|
|
lines.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
}
|
|
const maps = [...document.querySelectorAll('[data-people-map]')];
|
|
for (const canvas of maps) {
|
|
layout(canvas);
|
|
let previousWidth = canvas.clientWidth;
|
|
new ResizeObserver(() => {
|
|
if (previousWidth !== canvas.clientWidth) {
|
|
previousWidth = canvas.clientWidth; layout(canvas);
|
|
}
|
|
}).observe(canvas);
|
|
}
|
|
document.fonts.ready.then(() => maps.forEach(layout));
|
|
})();
|