170 lines
6.8 KiB
Plaintext
170 lines
6.8 KiB
Plaintext
@page
|
|
@model IndexModel
|
|
@{
|
|
Layout = null;
|
|
// ViewData["Title"] = "Home page";
|
|
}
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<title>Traffic Light Simulation Frontend</title>
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto+Condensed:normal,bold,italic" />
|
|
<style>
|
|
body {
|
|
font-family: 'Roboto Condensed';
|
|
}
|
|
.wrapper {
|
|
display: flex;
|
|
max-width: 600px;
|
|
font-family: 'Roboto Condensed';
|
|
}
|
|
.wrapper > div {
|
|
margin: .1em;
|
|
align-self: center;
|
|
flex: 1;
|
|
text-align: center;
|
|
}
|
|
div.light {
|
|
background-size: 100px;
|
|
background-repeat: no-repeat;
|
|
min-height: 230px;
|
|
min-width: 100px;
|
|
width: 100px;
|
|
}
|
|
div.north {
|
|
background-image: url(/images/red.png);
|
|
}
|
|
span#state-data {
|
|
color: #666666;
|
|
font-family: monospace;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Traffic light Simulation Demo</h1>
|
|
<p><strong>Period: </strong><span id="period">Awaiting first update...</span></p>
|
|
<p><strong>Simulation time: </strong><span id="time">Awaiting first update...</span></p>
|
|
<p><strong>Current state index: </strong><span id="state-index">Awaiting first update...</span></p>
|
|
<p><strong>Current state data: </strong><span id="state-data">Awaiting first update...</span></p>
|
|
<hr />
|
|
<p><strong>Time until next update: </strong><span id="time-remaining">Awaiting first update...</span> second(s)</p>
|
|
<p><strong>Time factor: </strong><span id="time-factor">Awaiting first update...</span></p>
|
|
<hr />
|
|
<div class="wrapper">
|
|
<div></div>
|
|
<div>NORTH</div>
|
|
<div id="north" class="light north"></div>
|
|
<div id="north-right" class="light"></div>
|
|
<div></div>
|
|
<div></div>
|
|
</div>
|
|
<div class="wrapper">
|
|
<div>WEST</div>
|
|
<div id="west" class="light"></div>
|
|
<div></div>
|
|
<div></div>
|
|
<div></div>
|
|
<div id="east" class="light"></div>
|
|
<div>EAST</div>
|
|
</div>
|
|
<div class="wrapper">
|
|
<div></div>
|
|
<div>SOUTH</div>
|
|
<div id="south" class="light"></div>
|
|
<div></div>
|
|
<div></div>
|
|
<div></div>
|
|
</div>
|
|
<script>
|
|
function sleep(ms)
|
|
{
|
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
}
|
|
|
|
async function ready()
|
|
{
|
|
const req = new XMLHttpRequest();
|
|
const MIN_UPDATE_TIME_MS = 100;
|
|
|
|
let isSending = false;
|
|
let nextUpdateTime = Date.now() + 1;
|
|
|
|
let timeFactor = null;
|
|
let lastSimulationTimestamp = null;
|
|
let lastUpdateTimestamp = null;
|
|
|
|
req.onload = function()
|
|
{
|
|
if (this.status !== 200)
|
|
{
|
|
console.log("Failed update: status " + this.status);
|
|
isSending = false;
|
|
return;
|
|
}
|
|
let static_path_prefix = "/images/";
|
|
let now = Date.now()
|
|
|
|
let light_status = JSON.parse(this.responseText);
|
|
|
|
lastSimulationTimestamp = light_status.currentSimulationTimestampMs;
|
|
lastUpdateTimestamp = now;
|
|
timeFactor = light_status.timeFactor;
|
|
|
|
document.getElementById("period").innerHTML = light_status.currentPeriod['verbose-name'];
|
|
|
|
document.getElementById("state-index").innerHTML = light_status.currentStateIndex;
|
|
document.getElementById("state-data").innerHTML = JSON.stringify(light_status.currentState);
|
|
|
|
document.getElementById("north").style.backgroundImage = "url(" + static_path_prefix + light_status.currentState.north + ".png)";
|
|
document.getElementById("north-right").style.backgroundImage = "url(" + static_path_prefix + light_status.currentState["north-right"] + "-right.png)";
|
|
document.getElementById("west").style.backgroundImage = "url(" + static_path_prefix + light_status.currentState.west + ".png)";
|
|
document.getElementById("east").style.backgroundImage = "url(" + static_path_prefix + light_status.currentState.east + ".png)";
|
|
document.getElementById("south").style.backgroundImage = "url(" + static_path_prefix + light_status.currentState.south + ".png)";
|
|
document.getElementById("time-factor").innerHTML = light_status.timeFactor.toFixed(2);
|
|
|
|
nextUpdateTime = light_status.nextChangeoverTimestampMs;
|
|
isSending = false;
|
|
};
|
|
|
|
|
|
while (true)
|
|
{
|
|
let now = Date.now();
|
|
|
|
if (!isSending && now >= nextUpdateTime)
|
|
{
|
|
isSending = true;
|
|
req.open("GET", "/GetStatus", false);
|
|
|
|
// Note try/catch is the only way to account for net::ERR_CONNECTION_REFUSED - this is NOT dealt with by XMLHttpRequest event hooks.
|
|
// In the case of net::ERR_CONNECTION_REFUSED should throw DOMException with name "NetworkError".
|
|
try
|
|
{
|
|
req.send();
|
|
}
|
|
catch (err)
|
|
{
|
|
console.dir(err);
|
|
isSending = false;
|
|
}
|
|
}
|
|
// Important note: Due to marginal overshoot of scheduled timing, it is possible for time-remaining to be very slightly negative
|
|
// It is undesirable to present this on the frontend, and therefore zero is the lowest number displayed
|
|
document.getElementById("time-remaining").innerHTML = Math.max(0, (nextUpdateTime - now) / 1000).toFixed(3);
|
|
document.getElementById("")
|
|
|
|
if (lastSimulationTimestamp !== null && lastUpdateTimestamp !== null)
|
|
{
|
|
let simulationDate = new Date(lastSimulationTimestamp + parseInt((now - lastUpdateTimestamp) * timeFactor));
|
|
document.getElementById("time").innerHTML = simulationDate.toUTCString().slice(17, 25); // Time section of date string only
|
|
}
|
|
|
|
await sleep(Math.min(MIN_UPDATE_TIME_MS, nextUpdateTime - now));
|
|
}
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", ready);
|
|
</script>
|
|
</body>
|
|
</html>
|