sourcehypertextpublicvinylplotmap.js

/* https://github.com/timohausmann/quadtree-js.git v1.2.5 */
!function () { function o(t, e, s, i) { this.max_objects = e || 10, this.max_levels = s || 4, this.level = i || 0, this.bounds = t, this.objects = [], this.nodes = [] } o.prototype.split = function () { var t = this.level + 1, e = this.bounds.width / 2, s = this.bounds.height / 2, i = this.bounds.x, h = this.bounds.y; this.nodes[0] = new o({ x: i + e, y: h, width: e, height: s }, this.max_objects, this.max_levels, t), this.nodes[1] = new o({ x: i, y: h, width: e, height: s }, this.max_objects, this.max_levels, t), this.nodes[2] = new o({ x: i, y: h + s, width: e, height: s }, this.max_objects, this.max_levels, t), this.nodes[3] = new o({ x: i + e, y: h + s, width: e, height: s }, this.max_objects, this.max_levels, t) }, o.prototype.getIndex = function (t) { var e = [], s = this.bounds.x + this.bounds.width / 2, i = this.bounds.y + this.bounds.height / 2, h = t.y < i, o = t.x < s, n = t.x + t.width > s, d = t.y + t.height > i; return h && n && e.push(0), o && h && e.push(1), o && d && e.push(2), n && d && e.push(3), e }, o.prototype.insert = function (t) { var e, s = 0; if (this.nodes.length) for (e = this.getIndex(t), s = 0; s < e.length; s++)this.nodes[e[s]].insert(t); else if (this.objects.push(t), this.objects.length > this.max_objects && this.level < this.max_levels) { for (this.nodes.length || this.split(), s = 0; s < this.objects.length; s++) { e = this.getIndex(this.objects[s]); for (var i = 0; i < e.length; i++)this.nodes[e[i]].insert(this.objects[s]) } this.objects = [] } }, o.prototype.retrieve = function (t) { var e = this.getIndex(t), s = this.objects; if (this.nodes.length) for (var i = 0; i < e.length; i++)s = s.concat(this.nodes[e[i]].retrieve(t)); return s = s.filter(function (t, e) { return s.indexOf(t) >= e }) }, o.prototype.clear = function () { this.objects = []; for (var t = 0; t < this.nodes.length; t++)this.nodes.length && this.nodes[t].clear(); this.nodes = [] }, "undefined" != typeof module && void 0 !== module.exports ? module.exports = o : window.Quadtree = o }();

function plotMap() {
	let dots = [];
	const mapCanvas = $("#map-canvas");

	const params = {
		height: mapCanvas.getBoundingClientRect().height,
		width: mapCanvas.getBoundingClientRect().width,
		dia: Math.min(12, 0.01667 * mapCanvas.getBoundingClientRect().width),
		strength: 0.1
	};

	let tree = new Quadtree({
		x: 0,
		y: 0,
		width: params.width,
		height: params.height
	});

	function tooClose(a, b, tolerance) {
		px = Math.abs(a.x - b.x);
		py = Math.abs(a.y - b.y);
		pd = params.dia * tolerance;

		return px < pd && py < pd;
	}

	function clamp(lowest, input, highest) {
		return max(min(input, highest), lowest);
	}

	function isBetween(lowest, input, highest) {
		return input > lowest && input < highest;
	}

	function equalEarth(x, y) {
		var A1 = 1.340264,
			A2 = -0.081106,
			A3 = 0.000893,
			A4 = 0.003796,
			M = sqrt(3) / 2;

		wrapX = x < -168.75 ? 180 + 168.75 - x : x - 11.25;
		x_ = (wrapX * π) / 180;
		y_ = (y * π) / 180;

		var l = asin(M * sin(y_)),
			l2 = l * l,
			l6 = l2 * l2 * l2;
		return [
			0.5 +
				(x_ * (0.5 / 2.7066299836960748) * cos(l)) /
					(M * (A1 + 3 * A2 * l2 + l6 * (7 * A3 + 9 * A4 * l2))),
			0.5 -
				l *
					(0.5 / 1.3173627591574133) *
					(A1 + A2 * l2 + l6 * (A3 + A4 * l2))
		];
	}

	function ukpc(x, y) {
		return [((x + 6) * (332 / 1802)) / 8.5, 0.5 + ((y - 57) * 0.5) / -7.5];
	}

	for (const tr of $$("tbody tr")) {
		if (tr != $(`tr[data-artist="${tr.dataset.artist}"]`)) {
			continue;
		}
		const album = JSON.parse(tr.dataset.info);

		proj = equalEarth(album.lon, album.lat);
		if (
			isBetween(-6, album.lon, 2.5) &&
			isBetween(49.5, album.lat, 57) &&
			album.country != "fr" &&
			album.country != "gb/nir"
		) {
			proj = ukpc(album.lon, album.lat);
		}

		tx = params.width * proj[0];
		ty = params.height * proj[1];

		dots.push({
			sprite: album.sprite,
			artist: album.artist,
			alt: `${album.artist} - ${album.title} (${album.city})`,
			x: tx,
			y: ty,
			vx: 0,
			vy: 0,
			height: params.dia,
			width: params.dia
		});
	}

	function loop() {
		tree.clear();

		for (let d of dots) {
			d.collides = false;
			d.x = clamp(0, d.x + d.vx, params.width);
			d.y = clamp(0, d.y + d.vy, params.height);
			d.vx = 0;
			d.vy = 0;
			tree.insert(d);
		}

		newMap = "";

		for (let a of dots) {
			let candidates = tree.retrieve(a);
			const aIndex = dots.indexOf(a);

			for (let b of candidates) {
				if (aIndex >= dots.indexOf(b)) continue;
				const tct = a.artist == b.artist ? 0.95 : 1.17;
				if (tooClose(a, b, tct)) {
					a.collides = true;
					b.collides = true;
					const px = a.x - b.x || random() * 1e-6;
					const py = a.y - b.y || random() * 1e-6;
					const δ =
						(params.strength * params.dia) /
						sqrt(px * px + py * py);

					a.vx += δ * px;
					b.vx -= δ * px;
					a.vy += δ * py;
					b.vy -= δ * py;
				}
			}

			newMap += `<div class="dot album-cover" alt="${a.alt}" title="${a.alt}" style="--x: ${a.sprite.x}; --y: ${a.sprite.y}; top: ${a.y}px; left: ${a.x}px;"></div>`;
		}

		mapCanvas.innerHTML = newMap;
	}

	for (i = 0; i < 25 || (i > 0 && dots.some(d => d.collides == true)); i++) {
		loop();
	}
}

document.on("DOMContentLoaded", plotMap);