UnityTrafficSimulation/Visualisation/js/visualiser.js
2026-09-13 20:53:11 +01:00

521 lines
18 KiB
JavaScript

/*
2D vizualizáció a szimuláció állapotáról
Az utakon lévő autók számából találja ki, hogy milyen szélesek legyenek az utak a grafikán.
(Ezeket a határértékeket az intToVisual methodban lehet állítani.)
A normálisan müködő dolgok kék színűek, a nem jó dolgok piros színűek.
Ha egy úton túl sok autó van, azaz dugó alakult ki, akkor az út kék helyett piros színű lesz.
A pulzáló körök a jelzőlámpákat jelképezik, ha valid állapotban van egy lámpa (egyik irányba piros, másikba zöld, vagy mindegyik sárga),
akkor kék színű, ha invalid (minden más, pl összes piros, összes zöld), akkor piros színű lesz.
Minél pirosabb a megjelenítés annál rosszabb a helyzet.
A RestAPI-t használja, azon belül is a GET /all-t.
Szépen animált, nem túl erőforrásigényes. Ha mégis az lenne, akkor a shouldPulse = false értékkel célszerű inicializálni, ez nagy mértékben felgyorsítja.
A hibákat tűri, ha elveszti a kapcsolatot, amint lehet helyreállítja.
A resize-t szépen kezeli, animált átmenettel mérteződik újra.
Használható metódusok:
//Célszerű width = height-tal dolgozni, különben széthúzza, és az nem szép.
constructor(int width, int height, string canvasID, string textID, string serverAddress, bool shouldPulse, bool shouldDrawGrid);
//Egyszer kell meghívni az indításhoz, 2 setInterval van benne. Stop nincs.
void start();
//A canvas méretét kizárólag ezen keresztül változtassuk.
void resize(int width, int height);
Részletek:
Kifejezetten izgalmas része nincsen, az olvashatóságra törekedtem írás közben. Szóval előbbiek következménye, hogy szükség esetén
könnyű továbbfejleszteni.
A beállításokat a konstruktor kódjában lehet változtatni.
*/
class Visualiser {
constructor(width, height, canvasName, canvasTextName, serverAddress, shouldPulse, shouldDrawGrid) {
/* Külső referenciák */
this.canvas = document.getElementById(canvasName);
this.text = document.getElementById(canvasTextName);
this.serverAddress = serverAddress;
this.ctx = this.canvas.getContext("2d");
/**/
/* Beállítások, amiket könnyen lehet változtatni */
this.goodColor = "#4242FF";
this.badColor = "#9E1414";
this.bgColor = "rgb(55, 55, 55)";
this.strokeColor = "#EEEEEE";
//Egy négyzetrács hányad részét foglalja el egy adott szélességű útelem?
this.smallWidthQ = 0.25;
this.mediumWidthQ = 0.75;
this.largeWidthQ = 1.33; //lógjon túl rajta
//A lámpa pulzálásának méretei, szintén a cellák oldalhosszúságához mérve.
this.trafficlightRadiusMinQ = 0.75;
this.trafficlightRadiusMaxQ = 0.85;
this.trafficlightSpeedQ = 0.025;
//A lámpa körvonalának (és a gridnek) a vastagsága.
this.lineWidthQ = 0.073;
//s-ben, célszerű kissebnek lennie, mint this.ajaxUpdateInterval
this.roadGrowSpeed = 0.7;
/**/
/* Beállítások, amiket nem nagyon kell változtatni */
//Az utak topológiája ettől függ, velük együtt lehet csak megváltoztatni.
this.horizontalLineCount = 11;
this.verticalLineCount = 11;
this.FPS = 30;
//s-ben, az egész projektben 1s-es intervallumok vannak updatekre, szóval ez ahhoz illeszkedik.
this.ajaxUpdateInterval = 1;
this.shouldPulse = shouldPulse;
this.shouldDrawGrid = shouldDrawGrid;
/**/
/* Belső */
this.resize(width, height);
this.trafficlightRadiusCurrent = this.trafficlightRadiusMin;
if(!shouldPulse) this.canvas.style["animation"] = "none";
//Az útelemek animációjához kell, kezdő és végállapotokat tárol.
//startWidth: az útelem először rajzolt oldala, endWith: az utoljára rajzolt
this.roadElemWidths = [];
for(let x = 0; x < this.verticalLineCount; x++) {
this.roadElemWidths[x] = [];
for(let y = 0; y < this.horizontalLineCount; y++) {
this.roadElemWidths[x][y] = [];
for(let layer = 0; layer < 2; layer++)
this.roadElemWidths[x][y][layer] = {
//Lehetne külön class ehhez, de nem szépítene annyit, hogy megérje
startWidth: {
start: 0,
end: 0
},
endWith: {
start: 0,
end: 0
}
};
}
}
this.deltaTimeSinceAjax = 0;
/**/
}
//Az úton lévő autók számából kitalálja, hogy milyen széles legyen az út a grafikán
//A visual egy nem túl intuitív nevű struct, ami tartalmazza, hogy egy út milyen fajta elemekből áll, visual(szélesElemekSzáma, közepes, vékony);
intToVisual(int) {
if(int >= 5) this.ctx.fillStyle = this.badColor;
if(int <= 0)
return new visual(0, 0, 3);
if(int <= 1)
return new visual(0, 1, 2);
if(int <= 2)
return new visual(0, 2, 1);
if(int <= 3)
return new visual(0, 3, 0);
if(int <= 5)
return new visual(1, 2, 0);
if(int <= 7)
return new visual(2, 1, 0);
else
return new visual(3, 0, 0);
}
resize(width, height) {
this.width = width;
this.height = height;
this.canvas.width = this.width
this.canvas.height = this.height
this.deltaX = this.width / this.verticalLineCount;
this.deltaY = this.height / this.horizontalLineCount;
this.smallWidth = 0.5 * this.deltaX * this.smallWidthQ;
this.mediumWidth = 0.5 * this.deltaX * this.mediumWidthQ;
this.largeWidth = 0.5 * this.deltaX * this.largeWidthQ;
this.trafficlightRadiusMin = this.deltaX * this.trafficlightRadiusMinQ;
this.trafficlightRadiusMax = this.deltaX * this.trafficlightRadiusMaxQ;
this.trafficlightSpeed = this.deltaX * this.trafficlightSpeedQ;
this.ctx.lineWidth = this.deltaX * this.lineWidthQ;
this.ctx.strokeStyle = this.strokeColor;
}
start() {
this.getRoadStates();
let that = this;
setInterval(function() {
that.getRoadStates();
}, this.ajaxUpdateInterval * 1000);
setInterval(function() {
that.drawTraffic(false);
}, 1000 / this.FPS);
}
//Index vektor által jelölt cella bal felső sarkának koordinátáit adja vissza.
getOriginOfVec(indexVec) {
return new Vec2 (
this.deltaX * indexVec.x,
this.deltaY * indexVec.y
);
}
drawGrid() {
this.ctx.beginPath();
for(let i = 1; i < this.horizontalLineCount; i++) {
this.ctx.moveTo(0, i * this.deltaY);
this.ctx.lineTo(this.width, i * this.deltaY);
}
for(let i = 1; i < this.verticalLineCount; i++) {
this.ctx.moveTo(i * this.deltaX, 0);
this.ctx.lineTo(i * this.deltaX, this.height);
}
this.ctx.stroke();
}
//Egy cellát tölt fel
drawRoadElem(newStartWidth, newEndWith, isHorizontal, indexVec, isKey, isJamAtStart) {
//A kereszteződések közti utak duplán vannak rajzolva (egyszer egyik irány, aztán a másik), ezért külön rétegen tárolom az állapotukat
const layerIndex = isJamAtStart ? 0 : 1;
//Nincs külön tárolva, mert egységes mindegyik útra nézve
const currentStartWidth = this.deltaTimeSinceAjax < this.roadGrowSpeed ?
this.roadElemWidths[indexVec.x][indexVec.y][layerIndex].startWidth.start +
(this.roadElemWidths[indexVec.x][indexVec.y][layerIndex].startWidth.end -
this.roadElemWidths[indexVec.x][indexVec.y][layerIndex].startWidth.start)
/ this.roadGrowSpeed * this.deltaTimeSinceAjax :
this.roadElemWidths[indexVec.x][indexVec.y][layerIndex].startWidth.end;
const currentEndWith = this.deltaTimeSinceAjax < this.roadGrowSpeed ?
this.roadElemWidths[indexVec.x][indexVec.y][layerIndex].endWith.start +
(this.roadElemWidths[indexVec.x][indexVec.y][layerIndex].endWith.end -
this.roadElemWidths[indexVec.x][indexVec.y][layerIndex].endWith.start)
/ this.roadGrowSpeed * this.deltaTimeSinceAjax :
this.roadElemWidths[indexVec.x][indexVec.y][layerIndex].endWith.end;
//isKey"frame", csak ha igaz, akkor veszi figyelembe a start/endWidth-et, onnan folytatja, ahol most tart
if(isKey) {
this.roadElemWidths[indexVec.x][indexVec.y][layerIndex] = {
startWidth: {
start: currentStartWidth,
end: newStartWidth
},
endWith: {
start: currentEndWith,
end: newEndWith
}
};
}
let origin = this.getOriginOfVec(indexVec);
//szebb anti-aliasing, picit nagyobbra rajzolja, mint kéne, így nem lesznek indokolatlan vonalak
let d = 0.5;
this.ctx.beginPath();
if(isHorizontal) {
this.ctx.moveTo(origin.x - d, origin.y + this.deltaY / 2 - currentStartWidth);
this.ctx.lineTo(origin.x + this.deltaX + d, origin.y + this.deltaY / 2 - currentEndWith);
this.ctx.lineTo(origin.x + this.deltaX + d, origin.y + this.deltaY / 2 + currentEndWith);
this.ctx.lineTo(origin.x - d, origin.y + this.deltaY / 2 + currentStartWidth);
}
else {
this.ctx.moveTo(origin.x + this.deltaX / 2 + currentStartWidth, origin.y - d);
this.ctx.lineTo(origin.x + this.deltaX / 2 + currentEndWith, origin.y + this.deltaY + d);
this.ctx.lineTo(origin.x + this.deltaX / 2 - currentEndWith, origin.y + this.deltaY + d);
this.ctx.lineTo(origin.x + this.deltaX / 2 - currentStartWidth, origin.y - d);
}
this.ctx.fill();
}
//Egy egész utat lerajzol
drawRoad(road, visual, isKey) {
const startVec = road.startVec;
const isHorizontal = road.isHorizontal;
const isJamAtStart = road.isJamAtStart;
const smallLength = visual.small;
const mediumLength = visual.medium;
const largeLength = visual.large;
const length = largeLength + mediumLength + smallLength;
if(isJamAtStart) {
for(let i = 0; i < length; i++) {
if(i < largeLength)
this.drawRoadElem(this.largeWidth, this.largeWidth, isHorizontal, startVec, isKey, isJamAtStart);
else if(i == largeLength && i > 0)
this.drawRoadElem(this.largeWidth, this.mediumWidth, isHorizontal, startVec, isKey, isJamAtStart);
else if(i < largeLength + mediumLength)
this.drawRoadElem(this.mediumWidth, this.mediumWidth, isHorizontal, startVec, isKey, isJamAtStart);
else if(i == largeLength + mediumLength && i > 0)
this.drawRoadElem(this.mediumWidth, this.smallWidth, isHorizontal, startVec, isKey, isJamAtStart);
else
this.drawRoadElem(this.smallWidth, this.smallWidth, isHorizontal, startVec, isKey, isJamAtStart);
if(isHorizontal) startVec.x++;
else startVec.y++;
}
}
else {
if(isHorizontal) startVec.x += length - 1;
else startVec.y += length - 1;
for(let i = 0; i < length; i++) {
if(i < largeLength)
this.drawRoadElem(this.largeWidth, this.largeWidth, isHorizontal, startVec, isKey, isJamAtStart);
else if(i == largeLength && i > 0)
this.drawRoadElem(this.mediumWidth, this.largeWidth, isHorizontal, startVec, isKey, isJamAtStart);
else if(i < largeLength + mediumLength)
this.drawRoadElem(this.mediumWidth, this.mediumWidth, isHorizontal, startVec, isKey, isJamAtStart);
else if(i == largeLength + mediumLength && i > 0)
this.drawRoadElem(this.smallWidth, this.mediumWidth, isHorizontal, startVec, isKey, isJamAtStart);
else
this.drawRoadElem(this.smallWidth, this.smallWidth, isHorizontal, startVec, isKey, isJamAtStart);
if(isHorizontal) startVec.x--;
else startVec.y--;
}
}
}
drawTrafficlight(indexVec, isValid) {
const origin = this.getOriginOfVec(indexVec);
origin.x += this.deltaX / 2;
origin.y += this.deltaY / 2;
this.ctx.fillStyle = isValid ? this.goodColor : this.badColor;
this.trafficlightRadiusCurrent += this.trafficlightSpeed / this.FPS;
if(this.trafficlightRadiusCurrent > this.trafficlightRadiusMax) {
this.trafficlightRadiusCurrent = this.trafficlightRadiusMax;
this.trafficlightSpeed *= -1;
}
else if(this.trafficlightRadiusCurrent < this.trafficlightRadiusMin) {
this.trafficlightRadiusCurrent = this.trafficlightRadiusMin;
this.trafficlightSpeed *= -1;
}
this.ctx.beginPath();
this.ctx.arc(origin.x, origin.y, this.trafficlightRadiusCurrent, 0, Math.PI * 2);
this.ctx.fill();
this.ctx.stroke();
}
drawTraffic(isKey) {
this.deltaTimeSinceAjax += 1 / this.FPS;
this.ctx.fillStyle = this.bgColor;
this.ctx.fillRect(0, 0, this.width, this.height);
this.ctx.fillStyle = this.goodColor;
if(this.shouldDrawGrid)
this.drawGrid();
if(this.roads) {
/* Út topológia:
* (megjelenítésben ez balra van "forgatva" 45°-kal)
*
* 1 5
* V V
* 0 > < 2 4 > < 6
* ^ ^
* 3 7
*
* 13 9
* V V
* 12 > < 14 8 > < 10
* ^ ^
* 15 11
*/
//Az indexek nem az id-k
const roads = [
new road(new Vec2(0, 3), true, false, "NW", "W"),
new road(new Vec2(3, 0), false, false, "NW", "N"),
new road(new Vec2(4, 3), true, true, "NW", "E", true),
new road(new Vec2(4, 3), true, false, "NE", "W"),
new road(new Vec2(7, 0), false, false, "NE", "N"),
new road(new Vec2(8, 3), true, true, "NE", "E"),
new road(new Vec2(7, 4), false, true, "NE", "S", true),
new road(new Vec2(7, 4), false, false, "SE", "N"),
new road(new Vec2(8, 7), true, true, "SE", "E"),
new road(new Vec2(7, 8), false, true, "SE", "S"),
new road(new Vec2(0, 7), true, false, "SW", "W"),
new road(new Vec2(3, 4), false, true, "NW", "S", true),
new road(new Vec2(3, 4), false, false, "SW", "N"),
new road(new Vec2(4, 7), true, false, "SE", "W", true),
new road(new Vec2(4, 7), true, true, "SW", "E"),
new road(new Vec2(3, 8), false, true, "SW", "S"),
];
//Mivel a kereszteződések közt 2 utat rajzol egymásra, fontos, hogy azok azonos színűek legyenek
let shouldNotResetColor = false;
for(let i = 0; i < roads.length; i++) {
if(!shouldNotResetColor)
this.ctx.fillStyle = this.goodColor;
this.drawRoad(roads[i], this.intToVisual(this.roads[roads[i].secondaryDirection][roads[i].primaryDirection]), isKey);
shouldNotResetColor = roads[i].shouldNotResetColor;
}
//Lámpák rajzolása
const tlVecs = [new Vec2(3, 3), new Vec2(7, 3), new Vec2(7, 7), new Vec2(3, 7)];
const directions = ["NW", "NE", "SE", "SW"];
for(let i = 0; i < 4; i++) {
//Ezeket tartom valid jelzőlámpa helyzeteknek
this.drawTrafficlight(
tlVecs[i],
(
this.trafficLights[directions[i]]["W"] == "red" &&
this.trafficLights[directions[i]]["E"] == "red" &&
this.trafficLights[directions[i]]["N"] == "green" &&
this.trafficLights[directions[i]]["S"] == "green"
) ||
(
this.trafficLights[directions[i]]["W"] == "green" &&
this.trafficLights[directions[i]]["E"] == "green" &&
this.trafficLights[directions[i]]["N"] == "red" &&
this.trafficLights[directions[i]]["S"] == "red"
) ||
(
this.trafficLights[directions[i]]["W"] == "yellow" &&
this.trafficLights[directions[i]]["E"] == "yellow" &&
this.trafficLights[directions[i]]["N"] == "yellow" &&
this.trafficLights[directions[i]]["S"] == "yellow"
)
);
}
}
//Minimálisan CPU igényes, inkább GPU-val kéne ilyeneket csinálni, de ez még belefér-
if(this.shouldPulse) {
let pixels = this.ctx.getImageData(0, 0, this.width, this.height);
for(let y = 0; y < this.height; y++) {
for(let x = 0; x < this.width; x++) {
if(
!( pixels.data[(this.width * y + x) * 4 + 0] == 55 &&
pixels.data[(this.width * y + x) * 4 + 1] == 55 &&
pixels.data[(this.width * y + x) * 4 + 2] == 55 )
) {
pixels.data[(this.width * y + x) * 4 + 3] = 200;
}
}
}
this.ctx.putImageData(pixels, 0, 0);
}
//Itt nullázom le a számlálót, az animáció és amiatt van erre szükség, hogy a jelenlegi szélességét nem tárolom az utaknak, ezért amikor update érkezik,
//tudnia kell, hogy jelenleg hol tart, azt eltárolja kezdetinek, és csak utána lehet lenullázni.
if(isKey)
this.deltaTimeSinceAjax = 0;
}
//a restApi /all-ját használja
//Átírja a szöveget is, ezt értelemszerűen lehet továbbbővíteni, más infókkal
getRoadStates() {
let xhttp = new XMLHttpRequest();
let that = this;
xhttp.onreadystatechange = function() {
//4-szer hívódik meg egy kérés alatt
if(this.readyState == 4) {
if(this.status == 200) {
const parsed = JSON.parse(this.responseText);
that.text.innerHTML = parsed.passedCarCount;
that.roads = parsed.roads;
that.trafficLights = parsed.trafficLights;
that.drawTraffic(true);
}
//Megfelelő mértékű hibakezelés
else if(this.status)
that.text.innerHTML = "Error: " + this.status;
else
that.text.innerHTML = "Timeout";
}
};
xhttp.open("GET", this.serverAddress + "all", true);
xhttp.send();
}
}; //Nem kell pontosvessző, de azért így jobban néz ki.
//Így gyorsabb létrehozni új vektorokat.
class Vec2 {
constructor(x, y) {
this.x = x;
this.y = y;
}
};
//A visual egy nem túl intuitív nevű struct, ami tartalmazza, hogy egy út milyen fajta elemekből áll.
class visual {
constructor(large, medium, small) {
this.large = large;
this.medium = medium;
this.small = small;
}
};
//Struct az utak tárolására, hossz nincs benne, az egységesen három, ez az intToVisualban van "megadva".
//Csak az utak tárolásában van igazi szerepe, később nem lesz nagyon kihasználva
class road {
constructor(startVec, isHorizontal, isJamAtStart, secondaryDirection, primaryDirection, shouldNotResetColor) {
this.startVec = startVec;
this.isHorizontal = isHorizontal;
this.isJamAtStart = isJamAtStart;
this.secondaryDirection = secondaryDirection;
this.primaryDirection = primaryDirection;
this.shouldNotResetColor = shouldNotResetColor;
}
};