<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Buien</title>
<script>
var stamps = null;
var currentIndex = -1;
var imageMap = new Map();  // stamp => (Image | false); false means 'still downloading'
var pendingImage = null;  // null | [idx, stamp]

var dbgpageload = new Date();

function dbgnow() {
	return new Date() - dbgpageload;
}

function renderStamp(s) {
	function zeropad2(num) {
		if (num < 10) return "0" + num;
		else return num.toString();
	}

	var date = new Date(Date.UTC(
		parseInt(s.slice(0, 4), 10),
		parseInt(s.slice(5, 7), 10) - 1,
		parseInt(s.slice(8, 10), 10),
		parseInt(s.slice(11, 13), 10),
		parseInt(s.slice(14, 16), 10),
		parseInt(s.slice(17, 19), 10)
	));
	return date.toLocaleString("nl-NL", {timeZone: "Europe/Amsterdam"});
	return date.getFullYear() + "-" +
		zeropad2(date.getMonth() + 1) + "-" +
		zeropad2(date.getDate()) + " " +
		zeropad2(date.getHours()) + ":" +
		zeropad2(date.getMinutes()) + ":" +
		zeropad2(date.getSeconds());
}

function getStamps() {
	var xhr = new XMLHttpRequest();
	xhr.onreadystatechange = function () {
		if (xhr.readyState == 4) {
			if (xhr.status != 200){ 
				alert("Error getting list of radar frames from server!");
			} else {
				stamps = xhr.response;
				if (currentIndex == -1) {
					doGotoLast();
				}
			}
		}
	};
	xhr.open("GET", "/api/list/json");
	xhr.responseType = "json";
	xhr.send();
}

// direction: -1 to download earlier frames first; 1 to download later frames first.
// Returns a list of indices in stamps.
function determineImageSetAround(idx, direction) {
	if (stamps.length == 0) return [];
	if (idx < 0) idx = 0;
	if (idx >= stamps.length) idx = stamps.length - 1;

	var res = [];

	var forwardPeek = 20;
	var backwardPeek = 10;
	var sizeLimit = 10;

	function loopcheck(start, end, incr) {
		for (var i = start; i != end; i += incr) {
			if (res.length >= sizeLimit) return;
			if (!imageMap.has(stamps[i])) res.push(i);
		}
	}

	switch (direction) {
		case -1:
			loopcheck(idx, Math.max(-1, idx - forwardPeek - 1), -1);
			loopcheck(Math.min(stamps.length, idx + 1), Math.min(stamps.length, idx + backwardPeek + 1), 1);
			break;

		case 1:
		default:
			loopcheck(idx, Math.min(stamps.length, idx + forwardPeek + 1), 1);
			loopcheck(Math.max(-1, idx - 1), Math.max(-1, idx - backwardPeek - 1), -1);
			break;
	}

	return res;
}

function readUint64LE(buffer, offset) {
	var arr = new DataView(buffer, offset, 8);
	var hi = arr.getUint32(4, true);
	if (hi >= (1 << (53 - 32))) {
		throw new Error("readUint64LE: Doesn't fit in Number");
	}
	return arr.getUint32(0, true) + (hi << 32);
}

// cb : (err) -> ()
function downloadImages(wantedindices, cb) {
	var indices = [];
	for (var i = 0; i < wantedindices.length; i++) {
		if (!imageMap.has(stamps[wantedindices[i]])) {
			indices.push(wantedindices[i]);
			imageMap.set(stamps[wantedindices[i]], false);
		}
	}

	// console.log(dbgnow(), "Downloading:", indices);

	var xhr = new XMLHttpRequest();
	xhr.onreadystatechange = function(){
		if (xhr.readyState == 4) {
			if (xhr.status != 200){ 
				cb("Server error while downloading images");
			} else {
				var buffer = xhr.response;

				var numLoaded = 0, numTotal = 0;

				var cursor = 0;
				for (var i = 0; cursor < buffer.byteLength; i++) {
					var pnglen = readUint64LE(buffer, cursor);
					if (!(imageMap.get(stamps[indices[i]]) instanceof Image)) {
						var arr = new Uint8Array(buffer, cursor + 8, pnglen);
						var blob = new Blob([arr], { type: "image/png" })
						var url = URL.createObjectURL(blob);
						var image = new Image();
						numTotal++;
						image.onload = function () {
							URL.revokeObjectURL(url);
							numLoaded++;
							if (numLoaded >= numTotal) {
								// console.log(dbgnow(), "Done:", indices);
								cb(null);
							}
						};
						image.src = url;
						imageMap.set(stamps[indices[i]], image);
					}
					cursor += 8 + pnglen;
				}
			}
		}
	};
	xhr.open("GET", "/api/images?s=" + indices.map(i => stamps[i]).join(","));
	xhr.responseType = "arraybuffer";
	xhr.send();
}

function showPendingImage() {
	if (pendingImage != null) {
		var idx = pendingImage[0], stamp = pendingImage[1];
		pendingImage = null;
		showImage(idx, stamp);
	}
}

function showImage(idx, stamp) {
	// console.log("showImage(" + idx + ", '" + stamp + "') -> " + imageMap.has(stamp));
	if (imageMap.has(stamp)) {
		var image = imageMap.get(stamp);
		if (image === false) {
			pendingImage = [idx, stamp];
			return;
		}

		var cvs = document.getElementById("imgcvs");
		var ctx = cvs.getContext("2d");
		ctx.drawImage(image, 0, 0);

		var dateelem = document.getElementById("imgdate");
		dateelem.innerHTML = "";
		dateelem.appendChild(document.createTextNode(renderStamp(stamp)));

		pendingImage = null;
	} else {
		pendingImage = [idx, stamp];

		downloadImages(determineImageSetAround(idx), function (err) {
			if (err) {
				alert("Error getting images from server:\n" + err);
				pendingImage = null;
			} else {
				showPendingImage();
			}
		});
	}
}

function gotoIndex(idx) {
	if (stamps == null || stamps.length == 0) { currentIndex = -1; return; }
	if (idx < 0) idx = 0;
	if (idx >= stamps.length) idx = stamps.length - 1;

	var dir = idx == currentIndex - 1 ? -1
				: idx == currentIndex + 1 ? 1
				: 0;

	currentIndex = idx;

	// console.log(dbgnow(), "Goto " + currentIndex);

	if (dir != 0) {
		var missing = false;
		for (var i = 1; i <= 8; i++) {
			if (currentIndex + i * dir < 0 || currentIndex + i * dir >= stamps.length) break;
			if (!imageMap.has(stamps[currentIndex + i * dir])) { missing = true; break; }
		}

		if (missing) {
			downloadImages(determineImageSetAround(currentIndex, dir), function () {
				showPendingImage();
			});
		}
	}

	showImage(currentIndex, stamps[currentIndex]);
}

function doGotoFirst() {
	if (stamps == null) return;
	gotoIndex(0);
}

function doGotoOffset(off) {
	if (stamps == null) return;
	var newIndex = Math.min(stamps.length - 1, Math.max(0, currentIndex + off));
	gotoIndex(newIndex);
}

function doGotoLast() {
	if (stamps == null) return;
	gotoIndex(stamps.length > 0 ? stamps.length - 1 : 0);
}

window.addEventListener("load", function () {
	getStamps();
});

window.addEventListener("keydown", function (ev) {
	if (ev.key == "ArrowLeft") { doGotoOffset(-1); ev.preventDefault(); }
	else if (ev.key == "ArrowRight") { doGotoOffset(1); ev.preventDefault(); }
});
</script>
<style>
button.wide {
	width: 40px;
	margin-right: 20px;
}
</style>
</head>
<body>
<noscript><b>This page requires JavaScript to do anything.</b></noscript>

<h1>Buien</h1>

<p>Warning: This page can load a lot of images; one frame is between 30KB and 70KB.
One day (288 frames) is about 14MB.</p>

<canvas id="imgcvs" width="550" height="512">Your browser does not support the <code>&lt;canvas&gt;</code> element. Get a newer/different browser.</canvas>
<br>
<span id="imgdate"></span><br>
<button class="wide" onclick="doGotoFirst()">&lt;&lt;</button>
<button class="wide" onclick="doGotoOffset(-288)">-1d</button>
<button class="wide" onclick="doGotoOffset(-12)">-1h</button>
<button class="wide" onclick="doGotoOffset(-1)">&lt;</button>
<button class="wide" onclick="doGotoOffset(1)">&gt;</button>
<button class="wide" onclick="doGotoOffset(12)">+1h</button>
<button class="wide" onclick="doGotoOffset(288)">+1d</button>
<button class="wide" onclick="doGotoLast()">&gt;&gt;</button>
</body>
</html>