/* 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)); })();