521 lines
18 KiB
JavaScript
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;
|
|
}
|
|
};
|