summaryrefslogtreecommitdiff
path: root/src/scripts/controllers
diff options
context:
space:
mode:
Diffstat (limited to 'src/scripts/controllers')
-rw-r--r--src/scripts/controllers/connection/api.ts1724
-rw-r--r--src/scripts/controllers/connection/cache.ts85
-rw-r--r--src/scripts/controllers/connection/socket.ts76
-rw-r--r--src/scripts/controllers/mapcontroller.ts520
-rw-r--r--src/scripts/controllers/modes/building.ts114
-rw-r--r--src/scripts/controllers/modes/node.ts297
-rw-r--r--src/scripts/controllers/modes/object.ts297
-rw-r--r--src/scripts/controllers/modes/room.ts382
-rw-r--r--src/scripts/controllers/scaleindicator.ts45
-rw-r--r--src/scripts/controllers/simulation/chart.ts241
-rw-r--r--src/scripts/controllers/simulation/statecache.ts205
-rw-r--r--src/scripts/controllers/simulation/taskview.ts64
-rw-r--r--src/scripts/controllers/simulation/timeline.ts161
-rw-r--r--src/scripts/controllers/simulationcontroller.ts586
14 files changed, 4797 insertions, 0 deletions
diff --git a/src/scripts/controllers/connection/api.ts b/src/scripts/controllers/connection/api.ts
new file mode 100644
index 00000000..067e3ca0
--- /dev/null
+++ b/src/scripts/controllers/connection/api.ts
@@ -0,0 +1,1724 @@
+///<reference path="../../definitions.ts" />
+///<reference path="../../../../typings/index.d.ts" />
+import {Util} from "../../util";
+import {ServerConnection} from "../../serverconnection";
+
+
+export class APIController {
+ constructor(onConnect: (api: APIController) => any) {
+ ServerConnection.connect(() => {
+ onConnect(this);
+ });
+ }
+
+
+ ///
+ // PATH: /users
+ ///
+
+ // METHOD: GET
+ public getUserByEmail(email: string): Promise<IUser> {
+ return ServerConnection.send({
+ path: "/users",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {},
+ query: {
+ email
+ }
+ }
+ });
+ }
+
+ // METHOD: POST
+ public addUser(user: IUser): Promise<IUser> {
+ return ServerConnection.send({
+ path: "/users",
+ method: "POST",
+ parameters: {
+ body: {
+ user: user
+ },
+ path: {},
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /users/{id}
+ ///
+
+ // METHOD: GET
+ public getUser(userId: number): Promise<IUser> {
+ return ServerConnection.send({
+ path: "/users/{userId}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ userId
+ },
+ query: {}
+ }
+ });
+ }
+
+ // METHOD: PUT
+ public updateUser(userId: number, user: IUser): Promise<IUser> {
+ return ServerConnection.send({
+ path: "/users/{userId}",
+ method: "PUT",
+ parameters: {
+ body: {
+ user: {
+ givenName: user.givenName,
+ familyName: user.familyName
+ }
+ },
+ path: {
+ userId
+ },
+ query: {}
+ }
+ });
+ }
+
+ // METHOD: DELETE
+ public deleteUser(userId: number): Promise<IUser> {
+ return ServerConnection.send({
+ path: "/users/{userId}",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ userId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /users/{userId}/authorizations
+ ///
+
+ // METHOD: GET
+ public getAuthorizationsByUser(userId: number): Promise<IAuthorization[]> {
+ let authorizations = [];
+ return ServerConnection.send({
+ path: "/users/{userId}/authorizations",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ userId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ authorizations = data;
+ return this.getUser(userId);
+ }).then((userData: any) => {
+ let promises = [];
+ authorizations.forEach((authorization: IAuthorization) => {
+ authorization.user = userData;
+ promises.push(this.getSimulation(authorization.simulationId).then((simulationData: any) => {
+ authorization.simulation = simulationData;
+ }));
+ });
+ return Promise.all(promises);
+ }).then((data: any) => {
+ return authorizations;
+ });
+ }
+
+ ///
+ // PATH: /simulations
+ ///
+
+ // METHOD: POST
+ public addSimulation(simulation: ISimulation): Promise<ISimulation> {
+ return ServerConnection.send({
+ path: "/simulations",
+ method: "POST",
+ parameters: {
+ body: {
+ simulation: Util.packageForSending(simulation)
+ },
+ path: {},
+ query: {}
+ }
+ }).then((data: any) => {
+ this.parseSimulationTimestamps(data);
+ return data;
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}
+ ///
+
+ // METHOD: GET
+ public getSimulation(simulationId: number): Promise<ISimulation> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ this.parseSimulationTimestamps(data);
+ return data;
+ });
+ }
+
+ // METHOD: PUT
+ public updateSimulation(simulation: ISimulation): Promise<ISimulation> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}",
+ method: "PUT",
+ parameters: {
+ body: {
+ simulation: Util.packageForSending(simulation)
+ },
+ path: {
+ simulationId: simulation.id
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ this.parseSimulationTimestamps(data);
+ return data;
+ });
+ }
+
+ // METHOD: DELETE
+ public deleteSimulation(simulationId: number): Promise<ISimulation> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ simulationId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/authorizations
+ ///
+
+ // METHOD: GET
+ public getAuthorizationsBySimulation(simulationId: number): Promise<IAuthorization[]> {
+ let authorizations = [];
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/authorizations",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ authorizations = data;
+ return this.getSimulation(simulationId);
+ }).then((simulationData: any) => {
+ let promises = [];
+ authorizations.forEach((authorization: IAuthorization) => {
+ authorization.simulation = simulationData;
+ promises.push(this.getUser(authorization.userId).then((userData: any) => {
+ authorization.user = userData;
+ }));
+ });
+ return Promise.all(promises);
+ }).then((data: any) => {
+ return authorizations;
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/authorizations/{userId}
+ ///
+
+ // METHOD: GET
+ // Not needed
+
+ // METHOD: POST
+ public addAuthorization(authorization: IAuthorization): Promise<IAuthorization> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/authorizations/{userId}",
+ method: "POST",
+ parameters: {
+ body: {
+ authorization: {
+ authorizationLevel: authorization.authorizationLevel
+ }
+ },
+ path: {
+ simulationId: authorization.simulationId,
+ userId: authorization.userId
+ },
+ query: {}
+ }
+ });
+ }
+
+ // METHOD: PUT
+ public updateAuthorization(authorization: IAuthorization): Promise<IAuthorization> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/authorizations/{userId}",
+ method: "PUT",
+ parameters: {
+ body: {
+ authorization: {
+ authorizationLevel: authorization.authorizationLevel
+ }
+ },
+ path: {
+ simulationId: authorization.simulationId,
+ userId: authorization.userId
+ },
+ query: {}
+ }
+ });
+ }
+
+ // METHOD: DELETE
+ public deleteAuthorization(authorization: IAuthorization): Promise<IAuthorization> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/authorizations/{userId}",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ simulationId: authorization.simulationId,
+ userId: authorization.userId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}
+ ///
+
+ // METHOD: GET
+ public getDatacenter(simulationId: number, datacenterId: number): Promise<IDatacenter> {
+ let datacenter;
+
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ datacenter = data;
+
+ return this.getRoomsByDatacenter(simulationId, datacenterId);
+ }).then((data: any) => {
+ datacenter.rooms = data;
+ return datacenter;
+ });
+ }
+
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}/rooms
+ ///
+
+ // METHOD: GET
+ public getRoomsByDatacenter(simulationId: number, datacenterId: number): Promise<IRoom[]> {
+ let rooms;
+
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ rooms = data;
+
+ let promises = [];
+ rooms.forEach((room: IRoom) => {
+ promises.push(this.loadRoomTiles(simulationId, datacenterId, room));
+ });
+ return Promise.all(promises).then((data: any) => {
+ return rooms;
+ });
+ });
+ }
+
+ // METHOD: POST
+ public addRoomToDatacenter(simulationId: number, datacenterId: number): Promise<IRoom> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms",
+ method: "POST",
+ parameters: {
+ body: {
+ room: {
+ id: -1,
+ datacenterId,
+ roomType: "SERVER"
+ }
+ },
+ path: {
+ simulationId,
+ datacenterId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ data.tiles = [];
+ return data;
+ });
+ }
+
+ ///
+ // PATH: /room-types
+ ///
+
+ // METHOD: GET
+ public getAllRoomTypes(): Promise<string[]> {
+ return ServerConnection.send({
+ path: "/room-types",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {},
+ query: {}
+ }
+ }).then((data: any) => {
+ let result = [];
+ data.forEach((roomType: any) => {
+ result.push(roomType.name);
+ });
+ return result;
+ });
+ }
+
+ ///
+ // PATH: /room-types/{name}/allowed-objects
+ ///
+
+ // METHOD: GET
+ public getAllowedObjectsByRoomType(name: string): Promise<string[]> {
+ return ServerConnection.send({
+ path: "/room-types/{name}/allowed-objects",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ name
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}
+ ///
+
+ // METHOD: GET
+ public getRoom(simulationId: number, datacenterId: number, roomId: number): Promise<IRoom> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.loadRoomTiles(simulationId, datacenterId, data);
+ });
+ }
+
+ // METHOD: PUT
+ public updateRoom(simulationId: number, datacenterId: number, room: IRoom): Promise<IRoom> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}",
+ method: "PUT",
+ parameters: {
+ body: {
+ room: Util.packageForSending(room)
+ },
+ path: {
+ simulationId,
+ datacenterId,
+ roomId: room.id
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.loadRoomTiles(simulationId, datacenterId, data);
+ });
+ }
+
+ // METHOD: DELETE
+ public deleteRoom(simulationId: number, datacenterId: number, roomId: number): Promise<IRoom> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles
+ ///
+
+ // METHOD: GET
+ public getTilesByRoom(simulationId: number, datacenterId: number, roomId: number): Promise<ITile[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ let promises = data.map((item) => {
+ return this.loadTileObject(simulationId, datacenterId, roomId, item);
+ });
+
+ return Promise.all(promises).then(() => {
+ return data;
+ })
+ });
+ }
+
+ // METHOD: POST
+ public addTileToRoom(simulationId: number, datacenterId: number, roomId: number, tile: ITile): Promise<ITile> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles",
+ method: "POST",
+ parameters: {
+ body: {
+ tile: Util.packageForSending(tile)
+ },
+ path: {
+ simulationId,
+ datacenterId,
+ roomId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.loadTileObject(simulationId, datacenterId, roomId, data);
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}
+ ///
+
+ // METHOD: GET
+ // Not needed (yet)
+
+ // METHOD: DELETE
+ public deleteTile(simulationId: number, datacenterId: number, roomId: number, tileId: number): Promise<ITile> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/cooling-item
+ ///
+
+ // METHOD: GET
+ public getCoolingItem(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number): Promise<ICoolingItem> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/cooling-item",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.loadFailureModel(data);
+ });
+ }
+
+ // METHOD: POST
+ public addCoolingItem(simulationId: number, datacenterId: number, roomId: number, tileId: number,
+ coolingItem: ICoolingItem): Promise<ICoolingItem> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/cooling-item",
+ method: "POST",
+ parameters: {
+ body: {
+ coolingItem: Util.packageForSending(coolingItem)
+ },
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.loadFailureModel(data);
+ });
+ }
+
+ // METHOD: PUT
+ // Not needed (yet)
+
+ // METHOD: DELETE
+ public deleteCoolingItem(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number): Promise<ICoolingItem> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/cooling-item",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/psu
+ ///
+
+ // METHOD: GET
+ public getPSU(simulationId: number, datacenterId: number, roomId: number, tileId: number): Promise<IPSU> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/psu",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.loadFailureModel(data);
+ });
+ }
+
+ // METHOD: POST
+ public addPSU(simulationId: number, datacenterId: number, roomId: number, tileId: number,
+ psu: IPSU): Promise<IPSU> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/psu",
+ method: "POST",
+ parameters: {
+ body: {
+ psu: Util.packageForSending(psu)
+ },
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.loadFailureModel(data);
+ });
+ }
+
+ // METHOD: PUT
+ // Not needed (yet)
+
+ // METHOD: DELETE
+ public deletePSU(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number): Promise<IPSU> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/psu",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack
+ ///
+
+ // METHOD: GET
+ public getRack(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number): Promise<IRack> {
+ let rack = {};
+
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ rack = data;
+ return this.getMachinesByRack(simulationId, datacenterId, roomId, tileId);
+ }).then((machines: any) => {
+ let promises = machines.map((machine) => {
+ return this.loadMachineUnits(machine);
+ });
+
+
+ return Promise.all(promises).then(() => {
+ rack["machines"] = [];
+
+ machines.forEach((machine: IMachine) => {
+ rack["machines"][machine.position] = machine;
+ });
+
+ for (let i = 0; i < rack["capacity"]; i++) {
+ if (rack["machines"][i] === undefined) {
+ rack["machines"][i] = null;
+ }
+ }
+
+ return rack;
+ });
+ });
+ }
+
+ // METHOD: POST
+ public addRack(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number, rack: IRack): Promise<IRack> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack",
+ method: "POST",
+ parameters: {
+ body: {
+ rack: Util.packageForSending(rack)
+ },
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ data.machines = [];
+
+ for (let i = 0; i < data.capacity; i++) {
+ data.machines.push(null);
+ }
+
+ return data;
+ });
+ }
+
+ // METHOD: PUT
+ public updateRack(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number, rack: IRack): Promise<IRack> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack",
+ method: "PUT",
+ parameters: {
+ body: {
+ rack
+ },
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ data.machines = rack.machines;
+
+ return data;
+ });
+ }
+
+ // METHOD: DELETE
+ public deleteRack(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number): Promise<IRack> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack/machines
+ ///
+
+ // METHOD: GET
+ public getMachinesByRack(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number): Promise<IMachine[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack/machines",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ let promises = data.map((machine) => {
+ return this.loadMachineUnits(machine);
+ });
+
+ return Promise.all(promises).then(() => {
+ return data;
+ });
+ });
+ }
+
+ // METHOD: POST
+ public addMachineToRack(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number, machine: IMachine): Promise<IMachine> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack/machines",
+ method: "POST",
+ parameters: {
+ body: {
+ machine: Util.packageForSending(machine)
+ },
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.loadMachineUnits(data);
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack/machines/{position}
+ ///
+
+ // METHOD: GET
+ // Not needed (yet)
+
+ // METHOD: PUT
+ public updateMachine(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number, machine: IMachine): Promise<IMachine> {
+ machine["tags"] = [];
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack/machines/{position}",
+ method: "PUT",
+ parameters: {
+ body: {
+ machine: Util.packageForSending(machine)
+ },
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId,
+ position: machine.position
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.loadMachineUnits(data);
+ });
+ }
+
+ // METHOD: DELETE
+ public deleteMachine(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number, position: number): Promise<any> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack/machines/{position}",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId,
+ position
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/experiments
+ ///
+
+ // METHOD: GET
+ public getExperimentsBySimulation(simulationId: number): Promise<IExperiment[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/experiments",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ let promises = data.map((item: any) => {
+ return this.getTrace(item.traceId).then((traceData: any) => {
+ item.trace = traceData;
+ });
+ });
+ return Promise.all(promises).then(() => {
+ return data;
+ });
+ });
+ }
+
+ // METHOD: POST
+ public addExperimentToSimulation(simulationId: number, experiment: IExperiment): Promise<IExperiment> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/experiments",
+ method: "POST",
+ parameters: {
+ body: {
+ experiment: Util.packageForSending(experiment)
+ },
+ path: {
+ simulationId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.getTrace(data.traceId).then((traceData: any) => {
+ data.trace = traceData;
+
+ return data;
+ });
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/experiments/{experimentId}
+ ///
+
+ // METHOD: GET
+ // Not needed (yet)
+
+ // METHOD: PUT
+ public updateExperiment(experiment: IExperiment): Promise<IExperiment> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/experiments/{experimentId}",
+ method: "PUT",
+ parameters: {
+ body: {
+ experiment: Util.packageForSending(experiment)
+ },
+ path: {
+ experimentId: experiment.id,
+ simulationId: experiment.simulationId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.getTrace(data.traceId).then((traceData: any) => {
+ data.trace = traceData;
+
+ return data;
+ });
+ });
+ }
+
+ // METHOD: DELETE
+ public deleteExperiment(simulationId: number, experimentId: number): Promise<any> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/experiments/{experimentId}",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ experimentId,
+ simulationId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/experiments/{experimentId}/last-simulated-tick
+ ///
+
+ // METHOD: GET
+ public getLastSimulatedTickByExperiment(simulationId: number, experimentId: number): Promise<number> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/experiments/{experimentId}/last-simulated-tick",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ experimentId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return data.lastSimulatedTick;
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/experiments/{experimentId}/machine-states
+ ///
+
+ // METHOD: GET
+ public getMachineStatesByTick(simulationId: number, experimentId: number, tick: number,
+ machines: {[keys: number]: IMachine}): Promise<IMachineState[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/experiments/{experimentId}/machine-states",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ experimentId
+ },
+ query: {
+ tick
+ }
+ }
+ }).then((data: any) => {
+ data.forEach((item: any) => {
+ item.machine = machines[item.machineId];
+ });
+
+ return data;
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/experiments/{experimentId}/rack-states
+ ///
+
+ // METHOD: GET
+ public getRackStatesByTick(simulationId: number, experimentId: number, tick: number,
+ racks: {[keys: number]: IRack}): Promise<IRackState[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/experiments/{experimentId}/rack-states",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ experimentId
+ },
+ query: {
+ tick
+ }
+ }
+ }).then((data: any) => {
+ let promises = data.map((item: any) => {
+ item.rack = racks[item.rackId];
+ });
+
+ return Promise.all(promises).then(() => {
+ return data;
+ });
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/experiments/{experimentId}/room-states
+ ///
+
+ // METHOD: GET
+ public getRoomStatesByTick(simulationId: number, experimentId: number, tick: number,
+ rooms: {[keys: number]: IRoom}): Promise<IRoomState[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/experiments/{experimentId}/room-states",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ experimentId
+ },
+ query: {
+ tick
+ }
+ }
+ }).then((data: any) => {
+ let promises = data.map((item: any) => {
+ item.room = rooms[item.roomId];
+ });
+
+ return Promise.all(promises).then(() => {
+ return data;
+ });
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/experiments/{experimentId}/task-states
+ ///
+
+ // METHOD: GET
+ public getTaskStatesByTick(simulationId: number, experimentId: number, tick: number,
+ tasks: {[keys: number]: ITask}): Promise<ITaskState[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/experiments/{experimentId}/task-states",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ experimentId
+ },
+ query: {
+ tick
+ }
+ }
+ }).then((data: any) => {
+ let promises = data.map((item: any) => {
+ item.task = tasks[item.taskId];
+ });
+
+ return Promise.all(promises).then(() => {
+ return data;
+ });
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/paths
+ ///
+
+ // METHOD: GET
+ public getPathsBySimulation(simulationId: number): Promise<IPath[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/paths",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ let promises = data.map((item: any) => {
+ return this.getSectionsByPath(simulationId, item.id).then((sectionsData: any) => {
+ item.sections = sectionsData;
+ });
+ });
+ return Promise.all(promises).then(() => {
+ return data;
+ });
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/paths/{pathId}
+ ///
+
+ // METHOD: GET
+ public getPath(simulationId: number, pathId: number): Promise<IPath> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/paths/{pathId}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ pathId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.getSectionsByPath(simulationId, pathId).then((sectionsData: any) => {
+ data.sections = sectionsData;
+ return data;
+ });
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/paths/{pathId}/branches
+ ///
+
+ // METHOD: GET
+ public getBranchesByPath(simulationId: number, pathId: number): Promise<IPath[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/paths/{pathId}/branches",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ pathId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ let promises = data.map((item: any) => {
+ return this.getSectionsByPath(simulationId, item.id).then((sectionsData: any) => {
+ item.sections = sectionsData;
+ });
+ });
+ return Promise.all(promises).then(() => {
+ return data;
+ });
+ });
+ }
+
+ // METHOD: POST
+ public branchFromPath(simulationId: number, pathId: number, startTick: number): Promise<IPath> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/paths/{pathId}/branches",
+ method: "POST",
+ parameters: {
+ body: {
+ section: {
+ startTick
+ }
+ },
+ path: {
+ simulationId,
+ pathId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.getSectionsByPath(simulationId, data.id).then((sectionsData: any) => {
+ data.sections = sectionsData;
+ return data;
+ });
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/paths/{pathId}/sections
+ ///
+
+ // METHOD: GET
+ public getSectionsByPath(simulationId: number, pathId: number): Promise<IPath[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/paths/{pathId}/sections",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ pathId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ let promises = data.map((path: ISection) => {
+ return this.getDatacenter(simulationId, path.datacenterId).then((datacenter: any) => {
+ path.datacenter = datacenter;
+ });
+ });
+ return Promise.all(promises).then(() => {
+ return data;
+ });
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/paths/{pathId}/sections/{sectionId}
+ ///
+
+ // METHOD: GET
+ public getSection(simulationId: number, pathId: number, sectionId: number): Promise<ISection> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/paths/{pathId}/sections/{sectionId}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ pathId,
+ sectionId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.getDatacenter(simulationId, data.datacenterId).then((datacenter: any) => {
+ data.datacenter = datacenter;
+ return data;
+ });
+ });
+ }
+
+ ///
+ // PATH: /specifications/psus
+ ///
+
+ // METHOD: GET
+ public getAllPSUSpecs(): Promise<IPSU[]> {
+ let psus;
+ return ServerConnection.send({
+ path: "/specifications/psus",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {},
+ query: {}
+ }
+ }).then((data: any) => {
+ psus = data;
+
+ let promises = [];
+ data.forEach((psu: IPSU) => {
+ promises.push(this.getFailureModel(psu.failureModelId));
+ });
+ return Promise.all(promises);
+ }).then((data: any) => {
+ return psus;
+ });
+ }
+
+ ///
+ // PATH: /specifications/psus/{id}
+ ///
+
+ // METHOD: GET
+ public getPSUSpec(id: number): Promise<IPSU> {
+ let psu;
+
+ return ServerConnection.send({
+ path: "/specifications/psus/{id}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ id
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ psu = data;
+ return this.getFailureModel(data.failureModelId);
+ }).then((data: any) => {
+ psu.failureModel = data;
+ return psu;
+ });
+ }
+
+ ///
+ // PATH: /specifications/cooling-items
+ ///
+
+ // METHOD: GET
+ public getAllCoolingItemSpecs(): Promise<ICoolingItem[]> {
+ let coolingItems;
+
+ return ServerConnection.send({
+ path: "/specifications/cooling-items",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {},
+ query: {}
+ }
+ }).then((data: any) => {
+ coolingItems = data;
+
+ let promises = [];
+ data.forEach((item: ICoolingItem) => {
+ promises.push(this.getFailureModel(item.failureModelId));
+ });
+ return Promise.all(promises);
+ }).then((data: any) => {
+ return coolingItems;
+ });
+ }
+
+ ///
+ // PATH: /specifications/cooling-items/{id}
+ ///
+
+ // METHOD: GET
+ public getCoolingItemSpec(id: number): Promise<IPSU> {
+ let coolingItem;
+
+ return ServerConnection.send({
+ path: "/specifications/cooling-items/{id}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ id
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ coolingItem = data;
+ return this.getFailureModel(data.failureModelId);
+ }).then((data: any) => {
+ coolingItem.failureModel = data;
+ return coolingItem;
+ });
+ }
+
+ ///
+ // PATH: /schedulers
+ ///
+
+ // METHOD: GET
+ public getAllSchedulers(): Promise<IScheduler[]> {
+ return ServerConnection.send({
+ path: "/schedulers",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {},
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /traces
+ ///
+
+ // METHOD: GET
+ public getAllTraces(): Promise<ITrace[]> {
+ return ServerConnection.send({
+ path: "/traces",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {},
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /traces/{traceId}
+ ///
+
+ // METHOD: GET
+ public getTrace(traceId: number): Promise<ITrace> {
+ let trace;
+
+ return ServerConnection.send({
+ path: "/traces/{traceId}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ traceId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ trace = data;
+ return this.getTasksByTrace(traceId);
+ }).then((data: any) => {
+ trace.tasks = data;
+ return trace;
+ });
+ }
+
+ ///
+ // PATH: /traces/{traceId}/tasks
+ ///
+
+ // METHOD: GET
+ public getTasksByTrace(traceId: number): Promise<ITask[]> {
+ return ServerConnection.send({
+ path: "/traces/{traceId}/tasks",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ traceId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /specifications/failure-models
+ ///
+
+ // METHOD: GET
+ public getAllFailureModels(): Promise<IFailureModel[]> {
+ return ServerConnection.send({
+ path: "/specifications/failure-models",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {},
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /specifications/failure-models/{id}
+ ///
+
+ // METHOD: GET
+ public getFailureModel(id: number): Promise<IFailureModel> {
+ return ServerConnection.send({
+ path: "/specifications/failure-models/{id}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ id
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /specifications/[units]
+ ///
+
+ // METHOD: GET
+ public getAllSpecificationsOfType(typePlural: string): Promise<INodeUnit> {
+ let specs: any;
+ return ServerConnection.send({
+ path: "/specifications/" + typePlural,
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {},
+ query: {}
+ }
+ }).then((data: any) => {
+ specs = data;
+
+ let promises = [];
+ data.forEach((unit: INodeUnit) => {
+ promises.push(this.getFailureModel(unit.failureModelId));
+ });
+ return Promise.all(promises);
+ }).then((data: any) => {
+ return specs;
+ });
+ }
+
+ ///
+ // PATH: /specifications/[units]/{id}
+ ///
+
+ // METHOD: GET
+ public getSpecificationOfType(typePlural: string, id: number): Promise<INodeUnit> {
+ let spec;
+
+ return ServerConnection.send({
+ path: "/specifications/" + typePlural + "/{id}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ id
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ spec = data;
+ return this.getFailureModel(data.failureModelId);
+ }).then((data: any) => {
+ spec.failureModel = data;
+ return spec;
+ });
+ }
+
+
+ ///
+ // HELPER METHODS
+ ///
+
+ private loadRoomTiles(simulationId: number, datacenterId: number, room: IRoom): Promise<IRoom> {
+ return this.getTilesByRoom(simulationId, datacenterId, room.id).then((data: any) => {
+ room.tiles = data;
+ return room;
+ });
+ }
+
+ private loadTileObject(simulationId: number, datacenterId: number, roomId: number, tile: ITile): Promise<ITile> {
+ let promise;
+
+ switch (tile.objectType) {
+ case "RACK":
+ promise = this.getRack(simulationId, datacenterId, roomId, tile.id).then((data: IRack) => {
+ tile.object = data;
+ });
+ break;
+ case "PSU":
+ promise = this.getPSU(simulationId, datacenterId, roomId, tile.id).then((data: IPSU) => {
+ tile.object = data;
+ });
+ break;
+ case "COOLING_ITEM":
+ promise = this.getCoolingItem(simulationId, datacenterId, roomId, tile.id).then((data: ICoolingItem) => {
+ tile.object = data;
+ });
+ break;
+ default:
+ promise = new Promise((resolve, reject) => {
+ resolve(undefined);
+ });
+ }
+
+ return promise.then(() => {
+ return tile;
+ })
+ }
+
+ private parseSimulationTimestamps(simulation: ISimulation): void {
+ simulation.datetimeCreatedParsed = Util.parseDateTime(simulation.datetimeCreated);
+ simulation.datetimeLastEditedParsed = Util.parseDateTime(simulation.datetimeLastEdited);
+ }
+
+ private loadFailureModel(data: any): Promise<any> {
+ return this.getFailureModel(data.failureModelId).then((failureModel: IFailureModel) => {
+ data.failureModel = failureModel;
+ return data;
+ });
+ }
+
+ private loadUnitsOfType(idListName: string, objectListName: string, machine: IMachine): Promise<IMachine> {
+ machine[objectListName] = [];
+
+ let promises = machine[idListName].map((item) => {
+ return this.getSpecificationOfType(objectListName, item).then((data) => {
+ machine[objectListName].push(data);
+ });
+ });
+
+ return Promise.all(promises).then(() => {
+ return machine;
+ })
+ }
+
+ private loadMachineUnits(machine: IMachine): Promise<IMachine> {
+ let listNames = [
+ {
+ idListName: "cpuIds",
+ objectListName: "cpus"
+ }, {
+ idListName: "gpuIds",
+ objectListName: "gpus"
+ }, {
+ idListName: "memoryIds",
+ objectListName: "memories"
+ }, {
+ idListName: "storageIds",
+ objectListName: "storages"
+ }
+ ];
+
+ let promises = listNames.map((item: any) => {
+ return this.loadUnitsOfType(item.idListName, item.objectListName, machine);
+ });
+
+ return Promise.all(promises).then(() => {
+ return machine;
+ });
+ }
+} \ No newline at end of file
diff --git a/src/scripts/controllers/connection/cache.ts b/src/scripts/controllers/connection/cache.ts
new file mode 100644
index 00000000..15517519
--- /dev/null
+++ b/src/scripts/controllers/connection/cache.ts
@@ -0,0 +1,85 @@
+export enum CacheStatus {
+ MISS,
+ FETCHING,
+ HIT,
+ NOT_CACHABLE
+}
+
+
+interface ICachableObject {
+ status: CacheStatus;
+ object: any;
+ callbacks: any[];
+}
+
+
+export class CacheController {
+ private static CACHABLE_ROUTES = [
+ "/specifications/psus/{id}",
+ "/specifications/cooling-items/{id}",
+ "/specifications/cpus/{id}",
+ "/specifications/gpus/{id}",
+ "/specifications/memories/{id}",
+ "/specifications/storages/{id}",
+ "/specifications/failure-models/{id}",
+ ];
+
+ // Maps every route name to a map of IDs => objects
+ private routeCaches: { [keys: string]: { [keys: number]: ICachableObject } };
+
+
+ constructor() {
+ this.routeCaches = {};
+
+ CacheController.CACHABLE_ROUTES.forEach((routeName: string) => {
+ this.routeCaches[routeName] = {};
+ })
+ }
+
+ public checkCache(request: IRequest): CacheStatus {
+ if (request.method === "GET" && CacheController.CACHABLE_ROUTES.indexOf(request.path) !== -1) {
+ if (this.routeCaches[request.path][request.parameters.path["id"]] === undefined) {
+ this.routeCaches[request.path][request.parameters.path["id"]] = {
+ status: CacheStatus.MISS,
+ object: null,
+ callbacks: []
+ };
+ return CacheStatus.MISS;
+ } else {
+ return this.routeCaches[request.path][request.parameters.path["id"]].status;
+ }
+ } else {
+ return CacheStatus.NOT_CACHABLE;
+ }
+ }
+
+ public fetchFromCache(request: IRequest): any {
+ return this.routeCaches[request.path][request.parameters.path["id"]].object;
+ }
+
+ public setToFetching(request: IRequest): void {
+ this.routeCaches[request.path][request.parameters.path["id"]].status = CacheStatus.FETCHING;
+ }
+
+ public onFetch(request: IRequest, response: IResponse): any {
+ let pathWithoutVersion = request.path.replace(/\/v\d+/, "");
+ this.routeCaches[pathWithoutVersion][request.parameters.path["id"]].status = CacheStatus.HIT;
+ this.routeCaches[pathWithoutVersion][request.parameters.path["id"]].object = response.content;
+
+ this.routeCaches[pathWithoutVersion][request.parameters.path["id"]].callbacks.forEach((callback) => {
+ callback({
+ status: {
+ code: 200
+ },
+ content: response.content,
+ id: request.id
+ });
+ });
+
+ this.routeCaches[pathWithoutVersion][request.parameters.path["id"]].callbacks = [];
+ }
+
+ public registerCallback(request: IRequest, callback): any {
+ this.routeCaches[request.path][request.parameters.path["id"]].callbacks.push(callback);
+ }
+}
diff --git a/src/scripts/controllers/connection/socket.ts b/src/scripts/controllers/connection/socket.ts
new file mode 100644
index 00000000..b38c303f
--- /dev/null
+++ b/src/scripts/controllers/connection/socket.ts
@@ -0,0 +1,76 @@
+import {CacheController, CacheStatus} from "./cache";
+import * as io from "socket.io-client";
+
+
+export class SocketController {
+ private static id = 1;
+ private _socket: SocketIOClient.Socket;
+ private _cacheController: CacheController;
+
+ // Mapping from request IDs to their registered callbacks
+ private callbacks: { [keys: number]: (response: IResponse) => any };
+
+
+ constructor(onConnect: () => any) {
+ this.callbacks = {};
+ this._cacheController = new CacheController();
+
+ this._socket = io.connect('https://opendc.ewi.tudelft.nl:443');
+ this._socket.on('connect', onConnect);
+
+ this._socket.on('response', (jsonResponse: string) => {
+ let response: IResponse = JSON.parse(jsonResponse);
+ console.log("Response, ID:", response.id, response);
+ this.callbacks[response.id](response);
+ delete this.callbacks[response.id];
+ });
+ }
+
+ /**
+ * Sends a request to the server socket and registers the callback to be triggered on response.
+ *
+ * @param request The request instance to be sent
+ * @param callback A function to be called with the response object once the socket has received a response
+ */
+ public sendRequest(request: IRequest, callback: (response: IResponse) => any): void {
+ // Check local cache, in case request is for cachable GET route
+ let cacheStatus = this._cacheController.checkCache(request);
+
+ if (cacheStatus === CacheStatus.HIT) {
+ callback({
+ status: {
+ code: 200
+ },
+ content: this._cacheController.fetchFromCache(request),
+ id: -1
+ });
+ } else if (cacheStatus === CacheStatus.FETCHING) {
+ this._cacheController.registerCallback(request, callback);
+ } else if (cacheStatus === CacheStatus.MISS || cacheStatus === CacheStatus.NOT_CACHABLE) {
+ if (!this._socket.connected) {
+ console.error("Socket not connected, sending request failed");
+ }
+
+ if (cacheStatus === CacheStatus.MISS) {
+ this._cacheController.setToFetching(request);
+
+ this.callbacks[SocketController.id] = (response: IResponse) => {
+ this._cacheController.onFetch(request, response);
+ callback(response);
+ };
+ } else {
+ this.callbacks[SocketController.id] = callback;
+ }
+
+ // Setup request object
+ request.id = SocketController.id;
+ request.token = localStorage.getItem("googleToken");
+ request.path = "/v1" + request.path;
+
+ console.log("Request, ID:", request.id, request);
+ this._socket.emit("request", request);
+
+ SocketController.id++;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/scripts/controllers/mapcontroller.ts b/src/scripts/controllers/mapcontroller.ts
new file mode 100644
index 00000000..d7458852
--- /dev/null
+++ b/src/scripts/controllers/mapcontroller.ts
@@ -0,0 +1,520 @@
+///<reference path="../../../typings/index.d.ts" />
+///<reference path="../views/mapview.ts" />
+import * as $ from "jquery";
+import {Colors} from "../colors";
+import {Util} from "../util";
+import {SimulationController} from "./simulationcontroller";
+import {MapView} from "../views/mapview";
+import {APIController} from "./connection/api";
+import {BuildingModeController} from "./modes/building";
+import {RoomModeController, RoomInteractionMode} from "./modes/room";
+import {ObjectModeController} from "./modes/object";
+import {NodeModeController} from "./modes/node";
+import {ScaleIndicatorController} from "./scaleindicator";
+
+export var CELL_SIZE = 50;
+
+
+export enum AppMode {
+ CONSTRUCTION,
+ SIMULATION
+}
+
+
+/**
+ * The current level of datacenter hierarchy that is selected
+ */
+export enum InteractionLevel {
+ BUILDING,
+ ROOM,
+ OBJECT,
+ NODE
+}
+
+
+/**
+ * Possible states that the application can be in, in terms of interaction
+ */
+export enum InteractionMode {
+ DEFAULT,
+ SELECT_ROOM
+}
+
+
+/**
+ * Class responsible for handling user input in the map.
+ */
+export class MapController {
+ public stage: createjs.Stage;
+ public mapView: MapView;
+
+ public appMode: AppMode;
+ public interactionLevel: InteractionLevel;
+ public interactionMode: InteractionMode;
+
+ public buildingModeController: BuildingModeController;
+ public roomModeController: RoomModeController;
+ public objectModeController: ObjectModeController;
+ public nodeModeController: NodeModeController;
+
+ public simulationController: SimulationController;
+ public api: APIController;
+ private scaleIndicatorController: ScaleIndicatorController;
+
+ private canvas: JQuery;
+ private gridDragging: boolean;
+
+ private infoTimeOut: any;
+ // Current mouse coordinates on the stage canvas (mainly for zooming purposes)
+ private currentStageMouseX: number;
+
+ private currentStageMouseY: number;
+ // Keep start coordinates relative to the grid to compute dragging offset later
+ private gridDragBeginX: number;
+
+ private gridDragBeginY: number;
+ // Keep start coordinates on stage to compute delta values
+ private stageDragBeginX: number;
+ private stageDragBeginY: number;
+
+ private MAX_DELTA = 5;
+
+
+ /**
+ * Hides all side menus except for the active one.
+ *
+ * @param activeMenu An identifier (e.g. #room-menu) for the menu container
+ */
+ public static hideAndShowMenus(activeMenu: string): void {
+ $(".menu-container.level-menu").each((index: number, elem: Element) => {
+ if ($(elem).is(activeMenu)) {
+ $(elem).removeClass("hidden");
+ } else {
+ $(elem).addClass("hidden");
+ }
+ });
+ }
+
+ constructor(mapView: MapView) {
+ this.mapView = mapView;
+ this.stage = this.mapView.stage;
+
+ new APIController((apiInstance: APIController) => {
+ this.api = apiInstance;
+
+ this.buildingModeController = new BuildingModeController(this);
+ this.roomModeController = new RoomModeController(this);
+ this.objectModeController = new ObjectModeController(this);
+ this.nodeModeController = new NodeModeController(this);
+ this.simulationController = new SimulationController(this);
+
+ this.scaleIndicatorController = new ScaleIndicatorController(this);
+
+ this.canvas = $("#main-canvas");
+
+ $(window).on("resize", () => {
+ this.onWindowResize();
+ });
+
+ this.gridDragging = false;
+
+ this.appMode = AppMode.CONSTRUCTION;
+ this.interactionLevel = InteractionLevel.BUILDING;
+ this.interactionMode = InteractionMode.DEFAULT;
+
+ this.setAllMenuModes();
+
+ this.setupMapInteractionHandlers();
+ this.setupEventListeners();
+ this.buildingModeController.setupEventListeners();
+ this.roomModeController.setupEventListeners();
+ this.objectModeController.setupEventListeners();
+ this.nodeModeController.setupEventListeners();
+
+ this.scaleIndicatorController.init($(".scale-indicator"));
+ this.scaleIndicatorController.update();
+
+ this.mapView.roomLayer.setClickable(true);
+
+ this.matchUserAuthLevel();
+ });
+ }
+
+ /**
+ * Hides and shows the menu bodies corresponding to the current mode (construction or simulation).
+ */
+ public setAllMenuModes(): void {
+ $(".menu-body" + (this.appMode === AppMode.CONSTRUCTION ? ".construction" : ".simulation")).show();
+ $(".menu-body" + (this.appMode === AppMode.CONSTRUCTION ? ".simulation" : ".construction")).hide();
+ }
+
+ /**
+ * Checks whether the mapContainer is still within its legal bounds.
+ *
+ * Resets, if necessary, to the most similar still legal position.
+ */
+ public checkAndResetCanvasMovement(): void {
+ if (this.mapView.mapContainer.x + this.mapView.gridLayer.gridPixelSize *
+ this.mapView.mapContainer.scaleX < this.mapView.canvasWidth) {
+ this.mapView.mapContainer.x = this.mapView.canvasWidth - this.mapView.gridLayer.gridPixelSize *
+ this.mapView.mapContainer.scaleX;
+ }
+ if (this.mapView.mapContainer.x > 0) {
+ this.mapView.mapContainer.x = 0;
+ }
+ if (this.mapView.mapContainer.y + this.mapView.gridLayer.gridPixelSize *
+ this.mapView.mapContainer.scaleX < this.mapView.canvasHeight) {
+ this.mapView.mapContainer.y = this.mapView.canvasHeight - this.mapView.gridLayer.gridPixelSize *
+ this.mapView.mapContainer.scaleX;
+ }
+ if (this.mapView.mapContainer.y > 0) {
+ this.mapView.mapContainer.y = 0;
+ }
+ }
+
+ /**
+ * Checks whether the mapContainer is still within its legal bounds and generates corrections if needed.
+ *
+ * Does not change the x and y coordinates, only returns.
+ */
+ public checkCanvasMovement(x: number, y: number, scale: number): IGridPosition {
+ let result: IGridPosition = {x: x, y: y};
+ if (x + this.mapView.gridLayer.gridPixelSize * scale < this.mapView.canvasWidth) {
+ result.x = this.mapView.canvasWidth - this.mapView.gridLayer.gridPixelSize *
+ this.mapView.mapContainer.scaleX;
+ }
+ if (x > 0) {
+ result.x = 0;
+ }
+ if (y + this.mapView.gridLayer.gridPixelSize * scale < this.mapView.canvasHeight) {
+ result.y = this.mapView.canvasHeight - this.mapView.gridLayer.gridPixelSize *
+ this.mapView.mapContainer.scaleX;
+ }
+ if (y > 0) {
+ result.y = 0;
+ }
+
+ return result;
+ }
+
+ /**
+ * Checks whether the current interaction mode is a hover mode (meaning that there is a hover item present).
+ *
+ * @returns {boolean} Whether it is in hover mode.
+ */
+ public isInHoverMode(): boolean {
+ return this.roomModeController !== undefined &&
+ (this.interactionMode === InteractionMode.SELECT_ROOM ||
+ this.roomModeController.roomInteractionMode === RoomInteractionMode.ADD_RACK ||
+ this.roomModeController.roomInteractionMode === RoomInteractionMode.ADD_PSU ||
+ this.roomModeController.roomInteractionMode === RoomInteractionMode.ADD_COOLING_ITEM);
+ }
+
+ public static showConfirmDeleteDialog(itemType: string, onConfirm: () => void): void {
+ let modalDialog = <any>$("#confirm-delete");
+ modalDialog.find(".modal-body").text("Are you sure you want to delete this " + itemType + "?");
+
+ let callback = () => {
+ onConfirm();
+ modalDialog.modal("hide");
+ modalDialog.find("button.confirm").first().off("click");
+ $(document).off("keypress");
+ };
+
+ $(document).on("keypress", (event: JQueryEventObject) => {
+ if (event.which === 13) {
+ callback();
+ } else if (event.which === 27) {
+ modalDialog.modal("hide");
+ $(document).off("keypress");
+ modalDialog.find("button.confirm").first().off("click");
+ }
+ });
+ modalDialog.find("button.confirm").first().on("click", callback);
+ modalDialog.modal("show");
+ }
+
+ /**
+ * Shows an informational popup in a corner of the screen, communicating a certain event.
+ *
+ * @param message The message to be displayed in the body of the popup
+ * @param type The severity of the message; Currently supported: "info" and "warning"
+ */
+ public showInfoBalloon(message: string, type: string): void {
+ let balloon = $(".info-balloon");
+ balloon.html('<span></span>' + message);
+ let callback = () => {
+ balloon.fadeOut(300);
+
+ this.infoTimeOut = undefined;
+ };
+ const DISPLAY_TIME = 3000;
+
+ let balloonIcon = balloon.find("span").first();
+ balloonIcon.removeClass();
+
+ balloon.css("background", Colors.INFO_BALLOON_MAP[type]);
+ balloonIcon.addClass("glyphicon");
+ if (type === "info") {
+ balloonIcon.addClass("glyphicon-info-sign");
+ } else if (type === "warning") {
+ balloonIcon.addClass("glyphicon-exclamation-sign");
+ }
+
+ if (this.infoTimeOut === undefined) {
+ balloon.fadeIn(300);
+ this.infoTimeOut = setTimeout(callback, DISPLAY_TIME);
+ } else {
+ clearTimeout(this.infoTimeOut);
+ this.infoTimeOut = setTimeout(callback, DISPLAY_TIME);
+ }
+ }
+
+ private setupMapInteractionHandlers(): void {
+ this.stage.enableMouseOver(20);
+
+ // Listen for mouse movement events to update hover positions
+ this.stage.on("stagemousemove", (event: createjs.MouseEvent) => {
+ this.currentStageMouseX = event.stageX;
+ this.currentStageMouseY = event.stageY;
+
+ let gridPos = this.convertScreenCoordsToGridCoords([event.stageX, event.stageY]);
+ let tileX = gridPos.x;
+ let tileY = gridPos.y;
+
+ // Check whether the coordinates of the hover location have changed since the last draw
+ if (this.mapView.hoverLayer.hoverTilePosition.x !== tileX) {
+ this.mapView.hoverLayer.hoverTilePosition.x = tileX;
+ this.mapView.updateScene = true;
+ }
+ if (this.mapView.hoverLayer.hoverTilePosition.y !== tileY) {
+ this.mapView.hoverLayer.hoverTilePosition.y = tileY;
+ this.mapView.updateScene = true;
+ }
+ });
+
+ // Handle mousedown interaction
+ this.stage.on("mousedown", (e: createjs.MouseEvent) => {
+ this.stageDragBeginX = e.stageX;
+ this.stageDragBeginY = e.stageY;
+ });
+
+ // Handle map dragging interaction
+ // Drag begin and progress handlers
+ this.mapView.mapContainer.on("pressmove", (e: createjs.MouseEvent) => {
+ if (!this.gridDragging) {
+ this.gridDragBeginX = e.stageX - this.mapView.mapContainer.x;
+ this.gridDragBeginY = e.stageY - this.mapView.mapContainer.y;
+ this.stageDragBeginX = e.stageX;
+ this.stageDragBeginY = e.stageY;
+ this.gridDragging = true;
+ } else {
+ this.mapView.mapContainer.x = e.stageX - this.gridDragBeginX;
+ this.mapView.mapContainer.y = e.stageY - this.gridDragBeginY;
+
+ this.checkAndResetCanvasMovement();
+
+ this.mapView.updateScene = true;
+ }
+ });
+
+ // Drag exit handlers
+ this.mapView.mapContainer.on("pressup", (e: createjs.MouseEvent) => {
+ if (this.gridDragging) {
+ this.gridDragging = false;
+ }
+
+ if (Math.abs(e.stageX - this.stageDragBeginX) < this.MAX_DELTA &&
+ Math.abs(e.stageY - this.stageDragBeginY) < this.MAX_DELTA) {
+ this.handleCanvasMouseClick(e.stageX, e.stageY);
+ }
+ });
+
+ // Disable an ongoing drag action if the mouse leaves the canvas
+ this.mapView.stage.on("mouseleave", () => {
+ if (this.gridDragging) {
+ this.gridDragging = false;
+ }
+ });
+
+ // Relay scroll events to the MapView zoom handler
+ $("#main-canvas").on("mousewheel", (event: JQueryEventObject) => {
+ let originalEvent = (<any>event.originalEvent);
+ this.mapView.zoom([this.currentStageMouseX, this.currentStageMouseY], -0.7 * originalEvent.deltaY);
+ this.scaleIndicatorController.update();
+ });
+ }
+
+ /**
+ * Connects clickable UI elements to their respective event listeners.
+ */
+ private setupEventListeners(): void {
+ // Zooming elements
+ $("#zoom-plus").on("click", () => {
+ this.mapView.zoom([
+ this.mapView.canvasWidth / 2,
+ this.mapView.canvasHeight / 2
+ ], 20);
+ });
+ $("#zoom-minus").on("click", () => {
+ this.mapView.zoom([
+ this.mapView.canvasWidth / 2,
+ this.mapView.canvasHeight / 2
+ ], -20);
+ });
+
+ $(".export-canvas").click(() => {
+ this.exportCanvasToImage();
+ });
+
+ // Menu panels
+ $(".menu-header-bar .menu-collapse").on("click", (event: JQueryEventObject) => {
+ let container = $(event.target).closest(".menu-container");
+ if (this.appMode === AppMode.CONSTRUCTION) {
+ container.children(".menu-body.construction").first().slideToggle(300);
+ } else if (this.appMode === AppMode.SIMULATION) {
+ container.children(".menu-body.simulation").first().slideToggle(300);
+ }
+
+ });
+
+ // Menu close button
+ $(".menu-header-bar .menu-exit").on("click", (event: JQueryEventObject) => {
+ let nearestMenuContainer = $(event.target).closest(".menu-container");
+ if (nearestMenuContainer.is("#node-menu")) {
+ this.interactionLevel = InteractionLevel.OBJECT;
+ $(".node-element-overlay").addClass("hidden");
+ }
+ nearestMenuContainer.addClass("hidden");
+ });
+
+ // Handler for the construction mode switch
+ $("#construction-mode-switch").on("click", () => {
+ this.simulationController.exitMode();
+ });
+
+ // Handler for the simulation mode switch
+ $("#simulation-mode-switch").on("click", () => {
+ this.simulationController.enterMode();
+ });
+
+ // Handler for the version-save button
+ $("#save-version-btn").on("click", (event: JQueryEventObject) => {
+ let target = $(event.target);
+
+ target.attr("data-saved", "false");
+ let lastPath = this.mapView.simulation.paths[this.mapView.simulation.paths.length - 1];
+ this.api.branchFromPath(
+ this.mapView.simulation.id, lastPath.id, lastPath.sections[lastPath.sections.length - 1].startTick + 1
+ ).then((data: IPath) => {
+ this.mapView.simulation.paths.push(data);
+ this.mapView.currentDatacenter = data.sections[data.sections.length - 1].datacenter;
+ target.attr("data-saved", "true");
+ });
+ });
+
+ $(document).on("keydown", (event: JQueryKeyEventObject) => {
+ if ($(event.target).is('input')) {
+ return;
+ }
+
+ if (event.which === 83) {
+ this.simulationController.enterMode();
+ } else if (event.which === 67) {
+ this.simulationController.exitMode();
+ } else if (event.which == 32) {
+ if (this.appMode === AppMode.SIMULATION) {
+ this.simulationController.timelineController.togglePlayback();
+ }
+ }
+ });
+ }
+
+ /**
+ * Handles a simple mouse click (without drag) on the canvas.
+ *
+ * @param stageX The x coordinate of the location in pixels on the stage
+ * @param stageY The y coordinate of the location in pixels on the stage
+ */
+ private handleCanvasMouseClick(stageX: number, stageY: number): void {
+ let gridPos = this.convertScreenCoordsToGridCoords([stageX, stageY]);
+
+ if (this.interactionLevel === InteractionLevel.BUILDING) {
+ if (this.interactionMode === InteractionMode.DEFAULT) {
+ let roomIndex = Util.roomCollisionIndexOf(this.mapView.currentDatacenter.rooms, gridPos);
+
+ if (roomIndex !== -1) {
+ this.interactionLevel = InteractionLevel.ROOM;
+ this.roomModeController.enterMode(this.mapView.currentDatacenter.rooms[roomIndex]);
+ }
+ } else if (this.interactionMode === InteractionMode.SELECT_ROOM) {
+ if (this.mapView.roomLayer.checkHoverTileValidity(gridPos)) {
+ this.buildingModeController.addSelectedTile(this.mapView.hoverLayer.hoverTilePosition);
+ } else if (Util.tileListContainsPosition(this.mapView.roomLayer.selectedTiles, gridPos)) {
+ this.buildingModeController.removeSelectedTile(this.mapView.hoverLayer.hoverTilePosition);
+ }
+ }
+ } else if (this.interactionLevel === InteractionLevel.ROOM) {
+ this.roomModeController.handleCanvasMouseClick(gridPos);
+ } else if (this.interactionLevel === InteractionLevel.OBJECT) {
+ if (gridPos.x !== this.mapView.grayLayer.currentObjectTile.position.x ||
+ gridPos.y !== this.mapView.grayLayer.currentObjectTile.position.y) {
+ this.objectModeController.goToRoomMode();
+ }
+ } else if (this.interactionLevel === InteractionLevel.NODE) {
+ this.interactionLevel = InteractionLevel.OBJECT;
+ this.nodeModeController.goToObjectMode();
+ }
+ }
+
+ /**
+ * Takes screen (stage) coordinates and returns the grid cell position they belong to.
+ *
+ * @param stagePosition The raw x and y coordinates of the wanted position
+ * @returns {Array} The corresponding grid cell coordinates
+ */
+ private convertScreenCoordsToGridCoords(stagePosition: number[]): IGridPosition {
+ let result = {x: 0, y: 0};
+ result.x = Math.floor((stagePosition[0] - this.mapView.mapContainer.x) /
+ (this.mapView.mapContainer.scaleX * CELL_SIZE));
+ result.y = Math.floor((stagePosition[1] - this.mapView.mapContainer.y) /
+ (this.mapView.mapContainer.scaleY * CELL_SIZE));
+ return result;
+ }
+
+ /**
+ * Adjusts the canvas size to fit the window perfectly.
+ */
+ private onWindowResize() {
+ let parent = this.canvas.parent(".app-content");
+ parent.height($(window).height() - 50);
+ this.canvas.attr("width", parent.width());
+ this.canvas.attr("height", parent.height());
+ this.mapView.canvasWidth = parent.width();
+ this.mapView.canvasHeight = parent.height();
+
+ if (this.interactionLevel === InteractionLevel.BUILDING) {
+ this.mapView.zoomOutOnDC();
+ } else if (this.interactionLevel === InteractionLevel.ROOM) {
+ this.mapView.zoomInOnRoom(this.roomModeController.currentRoom);
+ } else {
+ this.mapView.zoomInOnRoom(this.roomModeController.currentRoom, true);
+ }
+
+ this.mapView.updateScene = true;
+ }
+
+ private matchUserAuthLevel() {
+ let authLevel = localStorage.getItem("simulationAuthLevel");
+ if (authLevel === "VIEW") {
+ $(".side-menu-container.right-middle-side, .side-menu-container.right-side").hide();
+ }
+ }
+
+ private exportCanvasToImage() {
+ let canvasData = (<HTMLCanvasElement>this.canvas.get(0)).toDataURL("image/png");
+ let newWindow = window.open('about:blank', 'OpenDC Canvas Export');
+ newWindow.document.write("<img src='" + canvasData + "' alt='Canvas Image Export'/>");
+ newWindow.document.title = "OpenDC Canvas Export";
+ }
+}
diff --git a/src/scripts/controllers/modes/building.ts b/src/scripts/controllers/modes/building.ts
new file mode 100644
index 00000000..4d82f090
--- /dev/null
+++ b/src/scripts/controllers/modes/building.ts
@@ -0,0 +1,114 @@
+import {InteractionMode, MapController} from "../mapcontroller";
+import {MapView} from "../../views/mapview";
+import * as $ from "jquery";
+
+
+/**
+ * Class responsible for handling building mode interactions.
+ */
+export class BuildingModeController {
+ public newRoomId: number;
+
+ private mapController: MapController;
+ private mapView: MapView;
+
+
+ constructor(mapController: MapController) {
+ this.mapController = mapController;
+ this.mapView = this.mapController.mapView;
+ }
+
+ /**
+ * Connects all DOM event listeners to their respective element targets.
+ */
+ public setupEventListeners() {
+ let resetConstructionButtons = () => {
+ this.mapController.interactionMode = InteractionMode.DEFAULT;
+ this.mapView.hoverLayer.setHoverTileVisibility(false);
+ $("#room-construction").text("Construct new room");
+ $("#room-construction-cancel").slideToggle(300);
+ };
+
+ // Room construction button
+ $("#room-construction").on("click", (event: JQueryEventObject) => {
+ if (this.mapController.interactionMode === InteractionMode.DEFAULT) {
+ this.mapController.interactionMode = InteractionMode.SELECT_ROOM;
+ this.mapView.hoverLayer.setHoverTileVisibility(true);
+ this.mapController.api.addRoomToDatacenter(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id).then((room: IRoom) => {
+ this.newRoomId = room.id;
+ });
+ $(event.target).text("Finalize room");
+ $("#room-construction-cancel").slideToggle(300);
+ } else if (this.mapController.interactionMode === InteractionMode.SELECT_ROOM) {
+ resetConstructionButtons();
+ this.finalizeRoom();
+ }
+ });
+
+ // Cancel button for room construction
+ $("#room-construction-cancel").on("click", () => {
+ resetConstructionButtons();
+ this.cancelRoomConstruction();
+ });
+ }
+
+ /**
+ * Cancels room construction and deletes the temporary room created previously.
+ */
+ public cancelRoomConstruction() {
+ this.mapController.api.deleteRoom(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.newRoomId).then(() => {
+ this.mapView.roomLayer.cancelRoomConstruction();
+ });
+ }
+
+ /**
+ * Finalizes room construction by triggering a redraw of the room layer with the new room added.
+ */
+ public finalizeRoom() {
+ this.mapController.api.getRoom(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.newRoomId).then((room: IRoom) => {
+ this.mapView.roomLayer.finalizeRoom(room);
+ });
+ }
+
+ /**
+ * Adds a newly selected tile to the list of selected tiles.
+ *
+ * @param position The new tile position to be added
+ */
+ public addSelectedTile(position: IGridPosition): void {
+ let tile = {
+ id: -1,
+ roomId: this.newRoomId,
+ position: {x: position.x, y: position.y}
+ };
+ this.mapController.api.addTileToRoom(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.newRoomId, tile).then((tile: ITile) => {
+ this.mapView.roomLayer.addSelectedTile(tile);
+ });
+ }
+
+ /**
+ * Removes a previously selected tile.
+ *
+ * @param position The position of the tile to be removed
+ */
+ public removeSelectedTile(position: IGridPosition): void {
+ let tile;
+ let objectIndex = -1;
+
+ for (let i = 0; i < this.mapView.roomLayer.selectedTileObjects.length; i++) {
+ tile = this.mapView.roomLayer.selectedTileObjects[i];
+ if (tile.position.x === position.x && tile.position.y === position.y) {
+ objectIndex = i;
+ }
+ }
+ this.mapController.api.deleteTile(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.newRoomId,
+ this.mapView.roomLayer.selectedTileObjects[objectIndex].tileObject.id).then(() => {
+ this.mapView.roomLayer.removeSelectedTile(position, objectIndex);
+ });
+ }
+} \ No newline at end of file
diff --git a/src/scripts/controllers/modes/node.ts b/src/scripts/controllers/modes/node.ts
new file mode 100644
index 00000000..3b3f8a32
--- /dev/null
+++ b/src/scripts/controllers/modes/node.ts
@@ -0,0 +1,297 @@
+import {MapController, AppMode, InteractionLevel} from "../mapcontroller";
+import {MapView} from "../../views/mapview";
+import * as $ from "jquery";
+
+
+/**
+ * Class responsible for rendering node mode and handling UI interactions within it.
+ */
+export class NodeModeController {
+ public currentMachine: IMachine;
+
+ private mapController: MapController;
+ private mapView: MapView;
+
+
+ constructor(mapController: MapController) {
+ this.mapController = mapController;
+ this.mapView = this.mapController.mapView;
+
+ this.loadAddDropdowns();
+ }
+
+ /**
+ * Moves the UI model into node mode.
+ *
+ * @param machine The machine that was selected in rack mode
+ */
+ public enterMode(machine: IMachine): void {
+ this.currentMachine = machine;
+ this.populateUnitLists();
+ $("#node-menu").removeClass("hidden");
+
+ if (this.mapController.appMode === AppMode.SIMULATION) {
+ this.mapController.simulationController.transitionFromRackToNode();
+ }
+ }
+
+ /**
+ * Performs cleanup and closing actions before allowing transferal to rack mode.
+ */
+ public goToObjectMode(): void {
+ $("#node-menu").addClass("hidden");
+ $(".node-element-overlay").addClass("hidden");
+ this.currentMachine = undefined;
+ this.mapController.interactionLevel = InteractionLevel.OBJECT;
+
+ if (this.mapController.appMode === AppMode.SIMULATION) {
+ this.mapController.simulationController.transitionFromNodeToRack();
+ }
+ }
+
+ /**
+ * Connects all DOM event listeners to their respective element targets.
+ */
+ public setupEventListeners(): void {
+ let nodeMenu = $("#node-menu");
+
+ nodeMenu.find(".panel-group").on("click", ".remove-unit", (event: JQueryEventObject) => {
+ MapController.showConfirmDeleteDialog("unit", () => {
+ let index = $(event.target).closest(".panel").index();
+
+ if (index === -1) {
+ return;
+ }
+
+ let closestTabPane = $(event.target).closest(".panel-group");
+
+ let objectList, idList;
+ if (closestTabPane.is("#cpu-accordion")) {
+ objectList = this.currentMachine.cpus;
+ idList = this.currentMachine.cpuIds;
+ } else if (closestTabPane.is("#gpu-accordion")) {
+ objectList = this.currentMachine.gpus;
+ idList = this.currentMachine.gpuIds;
+ } else if (closestTabPane.is("#memory-accordion")) {
+ objectList = this.currentMachine.memories;
+ idList = this.currentMachine.memoryIds;
+ } else if (closestTabPane.is("#storage-accordion")) {
+ objectList = this.currentMachine.storages;
+ idList = this.currentMachine.storageIds;
+ }
+
+ idList.splice(idList.indexOf(objectList[index]).id, 1);
+ objectList.splice(index, 1);
+
+ this.mapController.api.updateMachine(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id,
+ this.mapController.objectModeController.currentObjectTile.id, this.currentMachine).then(
+ () => {
+ this.populateUnitLists();
+ this.mapController.objectModeController.updateNodeComponentOverlays();
+ });
+ });
+ });
+
+ nodeMenu.find(".add-unit").on("click", (event: JQueryEventObject) => {
+ let dropdown = $(event.target).closest(".input-group-btn").siblings("select").first();
+
+ let closestTabPane = $(event.target).closest(".input-group").siblings(".panel-group").first();
+ let objectList, idList, typePlural;
+ if (closestTabPane.is("#cpu-accordion")) {
+ objectList = this.currentMachine.cpus;
+ idList = this.currentMachine.cpuIds;
+ typePlural = "cpus";
+ } else if (closestTabPane.is("#gpu-accordion")) {
+ objectList = this.currentMachine.gpus;
+ idList = this.currentMachine.gpuIds;
+ typePlural = "gpus";
+ } else if (closestTabPane.is("#memory-accordion")) {
+ objectList = this.currentMachine.memories;
+ idList = this.currentMachine.memoryIds;
+ typePlural = "memories";
+ } else if (closestTabPane.is("#storage-accordion")) {
+ objectList = this.currentMachine.storages;
+ idList = this.currentMachine.storageIds;
+ typePlural = "storages";
+ }
+
+ if (idList.length + 1 > 4) {
+ this.mapController.showInfoBalloon("Machine has only 4 slots", "warning");
+ return;
+ }
+
+ let id = parseInt(dropdown.val(), 10);
+ idList.push(id);
+ this.mapController.api.getSpecificationOfType(typePlural, id).then((spec: INodeUnit) => {
+ objectList.push(spec);
+
+ this.mapController.api.updateMachine(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id,
+ this.mapController.objectModeController.currentObjectTile.id, this.currentMachine).then(
+ () => {
+ this.populateUnitLists();
+ this.mapController.objectModeController.updateNodeComponentOverlays();
+ });
+ });
+ });
+ }
+
+ /**
+ * Populates the "add" dropdowns with all available unit options.
+ */
+ private loadAddDropdowns(): void {
+ let unitTypes = [
+ "cpus", "gpus", "memories", "storages"
+ ];
+ let dropdowns = [
+ $("#add-cpu-form").find("select"),
+ $("#add-gpu-form").find("select"),
+ $("#add-memory-form").find("select"),
+ $("#add-storage-form").find("select"),
+ ];
+
+ unitTypes.forEach((type: string, index: number) => {
+ this.mapController.api.getAllSpecificationsOfType(type).then((data: any) => {
+ data.forEach((option: INodeUnit) => {
+ dropdowns[index].append($("<option>").val(option.id).text(option.manufacturer + " " + option.family +
+ " " + option.model + " (" + option.generation + ")"));
+ });
+ });
+ });
+ }
+
+ /**
+ * Generates and inserts dynamically HTML code concerning all units of a machine.
+ */
+ private populateUnitLists(): void {
+ // Contains the skeleton of a unit element and inserts the given data into it
+ let generatePanel = (type: string, index: number, list: any, specSection: string): string => {
+ return '<div class="panel panel-default">' +
+ ' <div class="panel-heading">' +
+ ' <h4 class="panel-title">' +
+ ' <a class="glyphicon glyphicon-remove remove-unit" href="javascript:void(0)"></a>' +
+ ' <a class="accordion-toggle collapsed" data-toggle="collapse" data-parent="#' + type + '-accordion"' +
+ ' href="#' + type + '-' + index + '">' +
+ list[index].manufacturer + ' ' + list[index].family + ' ' + list[index].model +
+ ' </a>' +
+ ' </h4>' +
+ ' </div>' +
+ ' <div id="' + type + '-' + index + '" class="panel-collapse collapse">' +
+ ' <table class="spec-table">' +
+ ' <tbody>' +
+ specSection +
+ ' </tbody>' +
+ ' </table>' +
+ ' </div>' +
+ '</div>';
+ };
+
+ // Generates the structure of the specification list of a processing unit
+ let generateProcessingUnitHtml = (element: IProcessingUnit) => {
+ return ' <tr>' +
+ ' <td class="glyphicon glyphicon-tasks"></td>' +
+ ' <td>Number of Cores</td>' +
+ ' <td>' + element.numberOfCores + '</td>' +
+ ' </tr>' +
+ ' <tr>' +
+ ' <td class="glyphicon glyphicon-dashboard"></td>' +
+ ' <td>Clockspeed (MHz)</td>' +
+ ' <td>' + element.clockRateMhz + '</td>' +
+ ' </tr>' +
+ ' <tr>' +
+ ' <td class="glyphicon glyphicon-flash"></td>' +
+ ' <td>Energy Consumption (W)</td>' +
+ ' <td>' + element.energyConsumptionW + '</td>' +
+ ' </tr>' +
+ ' <tr>' +
+ ' <td class="glyphicon glyphicon-alert"></td>' +
+ ' <td>Failure Rate (%)</td>' +
+ ' <td>' + element.failureModel.rate + '</td>' +
+ ' </tr>';
+ };
+
+ // Generates the structure of the spec list of a storage unit
+ let generateStorageUnitHtml = (element: IStorageUnit) => {
+ return ' <tr>' +
+ ' <td class="glyphicon glyphicon-floppy-disk"></td>' +
+ ' <td>Size (Mb)</td>' +
+ ' <td>' + element.sizeMb + '</td>' +
+ ' </tr>' +
+ ' <tr>' +
+ ' <td class="glyphicon glyphicon-dashboard"></td>' +
+ ' <td>Speed (Mb/s)</td>' +
+ ' <td>' + element.speedMbPerS + '</td>' +
+ ' </tr>' +
+ ' <tr>' +
+ ' <td class="glyphicon glyphicon-flash"></td>' +
+ ' <td>Energy Consumption (W)</td>' +
+ ' <td>' + element.energyConsumptionW + '</td>' +
+ ' </tr>' +
+ ' <tr>' +
+ ' <td class="glyphicon glyphicon-alert"></td>' +
+ ' <td>Failure Rate (%)</td>' +
+ ' <td>' + element.failureModel.rate + '</td>' +
+ ' </tr>';
+ };
+
+ // Inserts a "No units" message into the container of the given unit type
+ let addNoUnitsMessage = (type: string) => {
+ $("#" + type + "-accordion").append("<p>There are currently no units present here. " +
+ "<em>Add some with the dropdown below!</em></p>");
+ };
+
+ let container = $("#cpu-accordion");
+ container.children().remove(".panel");
+ container.children().remove("p");
+
+ if (this.currentMachine.cpus.length === 0) {
+ addNoUnitsMessage("cpu");
+ } else {
+ this.currentMachine.cpus.forEach((element: ICPU, i: number) => {
+ let specSection = generateProcessingUnitHtml(element);
+ let content = generatePanel("cpu", i, this.currentMachine.cpus, specSection);
+ container.append(content);
+ });
+ }
+
+ container = $("#gpu-accordion");
+ container.children().remove(".panel");
+ container.children().remove("p");
+ if (this.currentMachine.gpus.length === 0) {
+ addNoUnitsMessage("gpu");
+ } else {
+ this.currentMachine.gpus.forEach((element: IGPU, i: number) => {
+ let specSection = generateProcessingUnitHtml(element);
+ let content = generatePanel("gpu", i, this.currentMachine.gpus, specSection);
+ container.append(content);
+ });
+ }
+
+ container = $("#memory-accordion");
+ container.children().remove(".panel");
+ container.children().remove("p");
+ if (this.currentMachine.memories.length === 0) {
+ addNoUnitsMessage("memory");
+ } else {
+ this.currentMachine.memories.forEach((element: IMemory, i: number) => {
+ let specSection = generateStorageUnitHtml(element);
+ let content = generatePanel("memory", i, this.currentMachine.memories, specSection);
+ container.append(content);
+ });
+ }
+
+ container = $("#storage-accordion");
+ container.children().remove(".panel");
+ container.children().remove("p");
+ if (this.currentMachine.storages.length === 0) {
+ addNoUnitsMessage("storage");
+ } else {
+ this.currentMachine.storages.forEach((element: IMemory, i: number) => {
+ let specSection = generateStorageUnitHtml(element);
+ let content = generatePanel("storage", i, this.currentMachine.storages, specSection);
+ container.append(content);
+ });
+ }
+ }
+} \ No newline at end of file
diff --git a/src/scripts/controllers/modes/object.ts b/src/scripts/controllers/modes/object.ts
new file mode 100644
index 00000000..e922433e
--- /dev/null
+++ b/src/scripts/controllers/modes/object.ts
@@ -0,0 +1,297 @@
+import {AppMode, MapController, InteractionLevel} from "../mapcontroller";
+import {MapView} from "../../views/mapview";
+import * as $ from "jquery";
+
+
+/**
+ * Class responsible for rendering object mode and handling its UI interactions.
+ */
+export class ObjectModeController {
+ public currentObject: IDCObject;
+ public objectType: string;
+ public currentRack: IRack;
+ public currentPSU: IPSU;
+ public currentCoolingItem: ICoolingItem;
+ public currentObjectTile: ITile;
+
+ private mapController: MapController;
+ private mapView: MapView;
+
+
+ constructor(mapController: MapController) {
+ this.mapController = mapController;
+ this.mapView = this.mapController.mapView;
+ }
+
+ /**
+ * Performs the necessary setup actions and enters object mode.
+ *
+ * @param tile A reference to the tile containing the rack that was selected.
+ */
+ public enterMode(tile: ITile) {
+ this.currentObjectTile = tile;
+ this.mapView.grayLayer.currentObjectTile = tile;
+ this.currentObject = tile.object;
+ this.objectType = tile.objectType;
+
+ // Show the corresponding sub-menu of object mode
+ $(".object-sub-menu").hide();
+
+ switch (this.objectType) {
+ case "RACK":
+ $("#rack-sub-menu").show();
+ this.currentRack = <IRack>this.currentObject;
+ $("#rack-name-input").val(this.currentRack.name);
+ this.populateNodeList();
+
+ break;
+
+ case "PSU":
+ $("#psu-sub-menu").show();
+ this.currentPSU = <IPSU>this.currentObject;
+
+ break;
+
+ case "COOLING_ITEM":
+ $("#cooling-item-sub-menu").show();
+ this.currentCoolingItem = <ICoolingItem>this.currentObject;
+
+ break;
+ }
+
+ this.mapView.grayLayer.drawRackLevel();
+ MapController.hideAndShowMenus("#object-menu");
+ this.scrollToBottom();
+
+ if (this.mapController.appMode === AppMode.SIMULATION) {
+ this.mapController.simulationController.transitionFromRoomToRack();
+ }
+ }
+
+ /**
+ * Leaves object mode and transfers to room mode.
+ */
+ public goToRoomMode() {
+ this.mapController.interactionLevel = InteractionLevel.ROOM;
+ this.mapView.grayLayer.hideRackLevel();
+ MapController.hideAndShowMenus("#room-menu");
+ this.mapController.roomModeController.enterMode(this.mapController.roomModeController.currentRoom);
+
+ if (this.mapController.appMode === AppMode.SIMULATION) {
+ this.mapController.simulationController.transitionFromRackToRoom();
+ }
+ }
+
+ /**
+ * Connects all DOM event listeners to their respective element targets.
+ */
+ public setupEventListeners() {
+ // Handler for saving a new rack name
+ $("#rack-name-save").on("click", () => {
+ this.currentRack.name = $("#rack-name-input").val();
+ this.mapController.api.updateRack(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id,
+ this.mapController.objectModeController.currentObjectTile.id, this.currentRack).then(
+ () => {
+ this.mapController.showInfoBalloon("Rack name saved", "info");
+ });
+ });
+
+ let nodeListContainer = $(".node-list-container");
+
+ // Handler for the 'add' button of each machine slot of the rack
+ nodeListContainer.on("click", ".add-node", (event: JQueryEventObject) => {
+ // Convert the DOM element index to a JS array index
+ let index = this.currentRack.machines.length - $(event.target).closest(".node-element").index() - 1;
+
+ // Insert an empty machine at the selected position
+ this.mapController.api.addMachineToRack(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id,
+ this.mapController.objectModeController.currentObjectTile.id, {
+ id: -1,
+ rackId: this.currentRack.id,
+ position: index,
+ tags: [],
+ cpuIds: [],
+ gpuIds: [],
+ memoryIds: [],
+ storageIds: []
+ }).then((data: IMachine) => {
+ this.currentRack.machines[index] = data;
+ this.populateNodeList();
+ this.mapView.dcObjectLayer.draw();
+ });
+
+ event.stopPropagation();
+ });
+
+ // Handler for the 'remove' button of each machine slot of the rack
+ nodeListContainer.on("click", ".remove-node", (event: JQueryEventObject) => {
+ let target = $(event.target);
+ MapController.showConfirmDeleteDialog("machine", () => {
+ // Convert the DOM element index to a JS array index
+ let index = this.currentRack.machines.length - target.closest(".node-element").index() - 1;
+
+ this.mapController.api.deleteMachine(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id,
+ this.mapController.objectModeController.currentObjectTile.id,
+ index).then(() => {
+ this.currentRack.machines[index] = null;
+ this.populateNodeList();
+ this.mapView.dcObjectLayer.draw();
+ });
+ });
+ event.stopPropagation();
+ });
+
+ // Handler for every node element, triggering node mode
+ nodeListContainer.on("click", ".node-element", (event: JQueryEventObject) => {
+ let domIndex = $(event.target).closest(".node-element").index();
+ let index = this.currentRack.machines.length - domIndex - 1;
+ let machine = this.currentRack.machines[index];
+
+ if (machine != null) {
+ this.mapController.interactionLevel = InteractionLevel.NODE;
+
+ // Gray out the other nodes
+ $(event.target).closest(".node-list-container").children(".node-element").each((nodeIndex: number, element: Element) => {
+ if (nodeIndex !== domIndex) {
+ $(element).children(".node-element-overlay").removeClass("hidden");
+ } else {
+ $(element).children(".node-element-overlay").addClass("hidden");
+ }
+ });
+
+ this.mapController.nodeModeController.enterMode(machine);
+ }
+ });
+
+ // Handler for rack deletion button
+ $("#rack-deletion").on("click", () => {
+ MapController.showConfirmDeleteDialog("rack", () => {
+ this.mapController.api.deleteRack(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id,
+ this.mapController.objectModeController.currentObjectTile.id).then(() => {
+ this.currentObjectTile.object = undefined;
+ this.currentObjectTile.objectType = undefined;
+ this.currentObjectTile.objectId = undefined;
+ this.mapView.redrawMap();
+ this.goToRoomMode();
+ });
+ });
+ });
+
+ // Handler for PSU deletion button
+ $("#psu-deletion").on("click", () => {
+ MapController.showConfirmDeleteDialog("PSU", () => {
+ this.mapController.api.deletePSU(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id,
+ this.mapController.objectModeController.currentObjectTile.id).then(() => {
+ this.mapView.redrawMap();
+ this.goToRoomMode();
+ });
+ this.currentObjectTile.object = undefined;
+ this.currentObjectTile.objectType = undefined;
+ this.currentObjectTile.objectId = undefined;
+ });
+ });
+
+ // Handler for Cooling Item deletion button
+ $("#cooling-item-deletion").on("click", () => {
+ MapController.showConfirmDeleteDialog("cooling item", () => {
+ this.mapController.api.deleteCoolingItem(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id,
+ this.mapController.objectModeController.currentObjectTile.id).then(() => {
+ this.mapView.redrawMap();
+ this.goToRoomMode();
+ });
+ this.currentObjectTile.object = undefined;
+ this.currentObjectTile.objectType = undefined;
+ this.currentObjectTile.objectId = undefined;
+ });
+ });
+ }
+
+ public updateNodeComponentOverlays(): void {
+ if (this.currentRack === undefined || this.currentRack.machines === undefined) {
+ return;
+ }
+
+ for (let i = 0; i < this.currentRack.machines.length; i++) {
+ if (this.currentRack.machines[i] === null) {
+ continue;
+ }
+
+ let container = this.mapController.appMode === AppMode.CONSTRUCTION ? ".construction" : ".simulation";
+ let element = $(container + " .node-element").eq(this.currentRack.machines.length - i - 1);
+ if (this.currentRack.machines[i].cpus.length !== 0) {
+ element.find(".overlay-cpu").addClass("hidden");
+ } else {
+ element.find(".overlay-cpu").removeClass("hidden");
+ }
+ if (this.currentRack.machines[i].gpus.length !== 0) {
+ element.find(".overlay-gpu").addClass("hidden");
+ } else {
+ element.find(".overlay-gpu").removeClass("hidden");
+ }
+ if (this.currentRack.machines[i].memories.length !== 0) {
+ element.find(".overlay-memory").addClass("hidden");
+ } else {
+ element.find(".overlay-memory").removeClass("hidden");
+ }
+ if (this.currentRack.machines[i].storages.length !== 0) {
+ element.find(".overlay-storage").addClass("hidden");
+ } else {
+ element.find(".overlay-storage").removeClass("hidden");
+ }
+ }
+ }
+
+ /**
+ * Dynamically generates and inserts HTML code for every node in the current rack.
+ */
+ private populateNodeList(): void {
+ let type, content;
+ let container = $(".node-list-container");
+
+ // Remove any previously present node elements
+ container.children().remove(".node-element");
+
+ for (let i = 0; i < this.currentRack.machines.length; i++) {
+ // Depending on whether the current machine slot is filled, allow removing or adding a new machine by adding
+ // the appropriate button next to the machine slot
+ type = (this.currentRack.machines[i] == null ? "glyphicon-plus add-node" : "glyphicon-remove remove-node");
+ content =
+ '<div class="node-element" data-id="' + (this.currentRack.machines[i] === null ?
+ "" : this.currentRack.machines[i].id) + '">' +
+ ' <div class="node-element-overlay hidden"></div>' +
+ ' <a class="node-element-btn glyphicon ' + type + '" href="javascript:void(0)"></a>' +
+ ' <div class="node-element-number">' + (i + 1) + '</div>';
+ if (this.currentRack.machines[i] !== null) {
+ content +=
+ '<div class="node-element-content">' +
+ ' <img src="img/app/node-cpu.png">' +
+ ' <img src="img/app/node-gpu.png">' +
+ ' <img src="img/app/node-memory.png">' +
+ ' <img src="img/app/node-storage.png">' +
+ ' <img src="img/app/node-network.png">' +
+ ' <div class="icon-overlay overlay-cpu hidden"></div>' +
+ ' <div class="icon-overlay overlay-gpu hidden"></div>' +
+ ' <div class="icon-overlay overlay-memory hidden"></div>' +
+ ' <div class="icon-overlay overlay-storage hidden"></div>' +
+ ' <div class="icon-overlay overlay-network"></div>' +
+ '</div>';
+ }
+ content += '</div>';
+ // Insert the generated machine slot into the DOM
+ container.prepend(content);
+ }
+
+ this.updateNodeComponentOverlays();
+ }
+
+ private scrollToBottom(): void {
+ let scrollContainer = $('.node-list-container');
+ scrollContainer.scrollTop(scrollContainer[0].scrollHeight);
+ }
+} \ No newline at end of file
diff --git a/src/scripts/controllers/modes/room.ts b/src/scripts/controllers/modes/room.ts
new file mode 100644
index 00000000..a858af5a
--- /dev/null
+++ b/src/scripts/controllers/modes/room.ts
@@ -0,0 +1,382 @@
+import {Util} from "../../util";
+import {InteractionLevel, MapController, AppMode} from "../mapcontroller";
+import {MapView} from "../../views/mapview";
+import * as $ from "jquery";
+
+
+export enum RoomInteractionMode {
+ DEFAULT,
+ ADD_RACK,
+ ADD_PSU,
+ ADD_COOLING_ITEM
+}
+
+
+export class RoomModeController {
+ public currentRoom: IRoom;
+ public roomInteractionMode: RoomInteractionMode;
+
+ private mapController: MapController;
+ private mapView: MapView;
+ private roomTypes: string[];
+ private roomTypeMap: IRoomTypeMap;
+ private availablePSUs: IPSU[];
+ private availableCoolingItems: ICoolingItem[];
+
+
+ constructor(mapController: MapController) {
+ this.mapController = mapController;
+ this.mapView = this.mapController.mapView;
+
+ this.mapController.api.getAllRoomTypes().then((roomTypes: string[]) => {
+ this.roomTypes = roomTypes;
+ this.roomTypeMap = {};
+
+ this.roomTypes.forEach((type: string) => {
+ this.mapController.api.getAllowedObjectsByRoomType(type).then((objects: string[]) => {
+ this.roomTypeMap[type] = objects;
+ });
+ });
+
+ this.populateRoomTypeDropdown();
+ });
+
+ // this.mapController.api.getAllPSUSpecs().then((specs: IPSU[]) => {
+ // this.availablePSUs = specs;
+ // });
+ //
+ // this.mapController.api.getAllCoolingItemSpecs().then((specs: ICoolingItem[]) => {
+ // this.availableCoolingItems = specs;
+ // });
+
+ this.roomInteractionMode = RoomInteractionMode.DEFAULT;
+ }
+
+ public enterMode(room: IRoom) {
+ this.currentRoom = room;
+ this.roomInteractionMode = RoomInteractionMode.DEFAULT;
+
+ this.mapView.roomTextLayer.setVisibility(false);
+
+ this.mapView.zoomInOnRoom(this.currentRoom);
+ $("#room-name-input").val(this.currentRoom.name);
+ MapController.hideAndShowMenus("#room-menu");
+
+ // Pre-select the type of the current room in the dropdown
+ let roomTypeDropdown = $("#roomtype-select");
+ roomTypeDropdown.find('option').prop("selected", "false");
+ let roomTypeIndex = this.roomTypes.indexOf(this.currentRoom.roomType);
+ if (roomTypeIndex !== -1) {
+ roomTypeDropdown.find('option[value="' + roomTypeIndex + '"]').prop("selected", "true");
+ } else {
+ roomTypeDropdown.val([]);
+ }
+
+ this.populateAllowedObjectTypes();
+
+ this.mapView.roomLayer.setClickable(false);
+
+ if (this.mapController.appMode === AppMode.SIMULATION) {
+ this.mapController.simulationController.transitionFromBuildingToRoom();
+ }
+ }
+
+ public goToBuildingMode() {
+ this.mapController.interactionLevel = InteractionLevel.BUILDING;
+
+ if (this.roomInteractionMode !== RoomInteractionMode.DEFAULT) {
+ this.roomInteractionMode = RoomInteractionMode.DEFAULT;
+ this.mapView.hoverLayer.setHoverItemVisibility(false);
+ $("#add-rack-btn").attr("data-active", "false");
+ $("#add-psu-btn").attr("data-active", "false");
+ $("#add-cooling-item-btn").attr("data-active", "false");
+ }
+
+ this.mapView.roomTextLayer.setVisibility(true);
+
+ this.mapView.zoomOutOnDC();
+ MapController.hideAndShowMenus("#building-menu");
+
+ this.mapView.roomLayer.setClickable(true);
+
+ if (this.mapController.appMode === AppMode.SIMULATION) {
+ this.mapController.simulationController.transitionFromRoomToBuilding();
+ }
+ }
+
+ public setupEventListeners(): void {
+ // Component buttons
+ let addRackBtn = $("#add-rack-btn");
+ let addPSUBtn = $("#add-psu-btn");
+ let addCoolingItemBtn = $("#add-cooling-item-btn");
+
+ let roomTypeDropdown = $("#roomtype-select");
+
+ addRackBtn.on("click", () => {
+ this.handleItemClick("RACK");
+ });
+ addPSUBtn.on("click", () => {
+ this.handleItemClick("PSU");
+ });
+ addCoolingItemBtn.on("click", () => {
+ this.handleItemClick("COOLING_ITEM");
+ });
+
+ // Handler for saving a new room name
+ $("#room-name-save").on("click", () => {
+ this.currentRoom.name = $("#room-name-input").val();
+ this.mapController.api.updateRoom(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.currentRoom).then(() => {
+ this.mapView.roomTextLayer.draw();
+ this.mapController.showInfoBalloon("Room name saved", "info");
+ });
+ });
+
+ // Handler for room deletion button
+ $("#room-deletion").on("click", () => {
+ MapController.showConfirmDeleteDialog("room", () => {
+ this.mapController.api.deleteRoom(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.currentRoom.id).then(() => {
+ let roomIndex = this.mapView.currentDatacenter.rooms.indexOf(this.currentRoom);
+ this.mapView.currentDatacenter.rooms.splice(roomIndex, 1);
+
+ this.mapView.redrawMap();
+ this.goToBuildingMode();
+ });
+ });
+ });
+
+ // Handler for the room type dropdown component
+ roomTypeDropdown.on("change", () => {
+ let newRoomType = this.roomTypes[roomTypeDropdown.val()];
+ if (!this.checkRoomTypeLegality(newRoomType)) {
+ roomTypeDropdown.val(this.roomTypes.indexOf(this.currentRoom.roomType));
+ this.mapController.showInfoBalloon("Room type couldn't be changed, illegal objects", "warning");
+ return;
+ }
+
+ this.currentRoom.roomType = newRoomType;
+ this.mapController.api.updateRoom(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.currentRoom).then(() => {
+ this.populateAllowedObjectTypes();
+ this.mapView.roomTextLayer.draw();
+ this.mapController.showInfoBalloon("Room type changed", "info");
+ });
+ });
+ }
+
+ public handleCanvasMouseClick(gridPos: IGridPosition): void {
+ if (this.roomInteractionMode === RoomInteractionMode.DEFAULT) {
+ let tileIndex = Util.tileListPositionIndexOf(this.currentRoom.tiles, gridPos);
+
+ if (tileIndex !== -1) {
+ let tile = this.currentRoom.tiles[tileIndex];
+
+ if (tile.object !== undefined) {
+ this.mapController.interactionLevel = InteractionLevel.OBJECT;
+ this.mapController.objectModeController.enterMode(tile);
+ }
+ } else {
+ this.goToBuildingMode();
+ }
+ } else if (this.roomInteractionMode === RoomInteractionMode.ADD_RACK) {
+ this.addObject(this.mapView.hoverLayer.hoverTilePosition, "RACK");
+
+ } else if (this.roomInteractionMode === RoomInteractionMode.ADD_PSU) {
+ this.addObject(this.mapView.hoverLayer.hoverTilePosition, "PSU");
+
+ } else if (this.roomInteractionMode === RoomInteractionMode.ADD_COOLING_ITEM) {
+ this.addObject(this.mapView.hoverLayer.hoverTilePosition, "COOLING_ITEM");
+
+ }
+ }
+
+ private handleItemClick(type: string): void {
+ let addRackBtn = $("#add-rack-btn");
+ let addPSUBtn = $("#add-psu-btn");
+ let addCoolingItemBtn = $("#add-cooling-item-btn");
+ let allObjectContainers = $(".dc-component-container");
+ let objectTypes = [
+ {
+ type: "RACK",
+ mode: RoomInteractionMode.ADD_RACK,
+ btn: addRackBtn
+ },
+ {
+ type: "PSU",
+ mode: RoomInteractionMode.ADD_PSU,
+ btn: addPSUBtn
+ },
+ {
+ type: "COOLING_ITEM",
+ mode: RoomInteractionMode.ADD_COOLING_ITEM,
+ btn: addCoolingItemBtn
+ }
+ ];
+
+ allObjectContainers.attr("data-active", "false");
+
+ if (this.roomInteractionMode === RoomInteractionMode.DEFAULT) {
+ this.mapView.hoverLayer.setHoverItemVisibility(true, type);
+
+ if (type === "RACK") {
+ this.roomInteractionMode = RoomInteractionMode.ADD_RACK;
+ addRackBtn.attr("data-active", "true");
+ } else if (type === "PSU") {
+ this.roomInteractionMode = RoomInteractionMode.ADD_PSU;
+ addPSUBtn.attr("data-active", "true");
+ } else if (type === "COOLING_ITEM") {
+ this.roomInteractionMode = RoomInteractionMode.ADD_COOLING_ITEM;
+ addCoolingItemBtn.attr("data-active", "true");
+ }
+
+ return;
+ }
+
+ let changed = false;
+ objectTypes.forEach((objectType: any, index: number) => {
+ if (this.roomInteractionMode === objectType.mode) {
+ if (changed) {
+ return;
+ }
+ if (type === objectType.type) {
+ this.roomInteractionMode = RoomInteractionMode.DEFAULT;
+ this.mapView.hoverLayer.setHoverItemVisibility(false);
+ objectType.btn.attr("data-active", "false");
+ } else {
+ objectTypes.forEach((otherObjectType, otherIndex: number) => {
+ if (index !== otherIndex) {
+ if (type === otherObjectType.type) {
+ this.mapView.hoverLayer.setHoverItemVisibility(true, type);
+ otherObjectType.btn.attr("data-active", "true");
+ this.roomInteractionMode = otherObjectType.mode;
+ }
+ }
+ });
+ }
+ changed = true;
+ }
+ });
+ }
+
+ private addObject(position: IGridPosition, type: string): void {
+ if (!this.mapView.roomLayer.checkHoverTileValidity(position)) {
+ return;
+ }
+
+ let tileList = this.mapView.mapController.roomModeController.currentRoom.tiles;
+
+ for (let i = 0; i < tileList.length; i++) {
+ if (tileList[i].position.x === position.x && tileList[i].position.y === position.y) {
+ if (type === "RACK") {
+ this.mapController.api.addRack(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.currentRoom.id, tileList[i].id, {
+ id: -1,
+ objectType: "RACK",
+ name: "",
+ capacity: 42,
+ powerCapacityW: 5000
+ }).then((rack: IRack) => {
+ tileList[i].object = rack;
+ tileList[i].objectId = rack.id;
+ tileList[i].objectType = type;
+ this.mapView.dcObjectLayer.populateObjectList();
+ this.mapView.dcObjectLayer.draw();
+
+ this.mapView.updateScene = true;
+ });
+ } else if (type === "PSU") {
+ this.mapController.api.addPSU(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.currentRoom.id, tileList[i].id, this.availablePSUs[0])
+ .then((psu: IPSU) => {
+ tileList[i].object = psu;
+ tileList[i].objectId = psu.id;
+ tileList[i].objectType = type;
+ this.mapView.dcObjectLayer.populateObjectList();
+ this.mapView.dcObjectLayer.draw();
+
+ this.mapView.updateScene = true;
+ });
+ } else if (type === "COOLING_ITEM") {
+ this.mapController.api.addCoolingItem(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.currentRoom.id, tileList[i].id,
+ this.availableCoolingItems[0]).then((coolingItem: ICoolingItem) => {
+ tileList[i].object = coolingItem;
+ tileList[i].objectId = coolingItem.id;
+ tileList[i].objectType = type;
+ this.mapView.dcObjectLayer.populateObjectList();
+ this.mapView.dcObjectLayer.draw();
+
+ this.mapView.updateScene = true;
+ });
+ }
+
+ break;
+ }
+ }
+ }
+
+ /**
+ * Populates the room-type dropdown element with all available room types
+ */
+ private populateRoomTypeDropdown(): void {
+ let dropdown = $("#roomtype-select");
+
+ this.roomTypes.forEach((type: string, index: number) => {
+ dropdown.append($('<option>').text(Util.toSentenceCase(type)).val(index));
+ });
+ }
+
+ /**
+ * Loads all object types that are allowed in the current room into the menu.
+ */
+ private populateAllowedObjectTypes(): void {
+ let addObjectsLabel = $("#add-objects-label");
+ let noObjectsInfo = $("#no-objects-info");
+ let allowedObjectTypes = this.roomTypeMap[this.currentRoom.roomType];
+
+ $(".dc-component-container").addClass("hidden");
+
+ if (allowedObjectTypes === undefined || allowedObjectTypes === null || allowedObjectTypes.length === 0) {
+ addObjectsLabel.addClass("hidden");
+ noObjectsInfo.removeClass("hidden");
+
+ return;
+ }
+
+ addObjectsLabel.removeClass("hidden");
+ noObjectsInfo.addClass("hidden");
+ allowedObjectTypes.forEach((type: string) => {
+ switch (type) {
+ case "RACK":
+ $("#add-rack-btn").removeClass("hidden");
+ break;
+ case "PSU":
+ $("#add-psu-btn").removeClass("hidden");
+ break;
+ case "COOLING_ITEM":
+ $("#add-cooling-item-btn").removeClass("hidden");
+ break;
+ }
+ });
+ }
+
+ /**
+ * Checks whether a given room type can be assigned to the current room based on units already present.
+ *
+ * @param newRoomType The new room type to be validated
+ * @returns {boolean} Whether it is allowed to change the room's type to the new type
+ */
+ private checkRoomTypeLegality(newRoomType: string): boolean {
+ let legality = true;
+
+ this.currentRoom.tiles.forEach((tile: ITile) => {
+ if (tile.objectType !== undefined && tile.objectType !== null && tile.objectType !== "" &&
+ this.roomTypeMap[newRoomType].indexOf(tile.objectType) === -1) {
+ legality = false;
+ }
+ });
+
+ return legality;
+ }
+} \ No newline at end of file
diff --git a/src/scripts/controllers/scaleindicator.ts b/src/scripts/controllers/scaleindicator.ts
new file mode 100644
index 00000000..0ff83486
--- /dev/null
+++ b/src/scripts/controllers/scaleindicator.ts
@@ -0,0 +1,45 @@
+import {MapController, CELL_SIZE} from "./mapcontroller";
+import {MapView} from "../views/mapview";
+
+
+export class ScaleIndicatorController {
+ private static MIN_WIDTH = 50;
+ private static MAX_WIDTH = 100;
+
+ private mapController: MapController;
+ private mapView: MapView;
+
+ private jqueryObject: JQuery;
+ private currentDivisor: number;
+
+
+ constructor(mapController: MapController) {
+ this.mapController = mapController;
+ this.mapView = mapController.mapView;
+ }
+
+ public init(jqueryObject: JQuery): void {
+ this.jqueryObject = jqueryObject;
+ this.currentDivisor = 1;
+ }
+
+ public update(): void {
+ let currentZoom = this.mapView.mapContainer.scaleX;
+ let newWidth;
+ do {
+ newWidth = (currentZoom * CELL_SIZE) / this.currentDivisor;
+
+ if (newWidth < ScaleIndicatorController.MIN_WIDTH) {
+ this.currentDivisor /= 2;
+ } else if (newWidth > ScaleIndicatorController.MAX_WIDTH) {
+ this.currentDivisor *= 2;
+ } else {
+ break;
+ }
+ } while (true);
+
+
+ this.jqueryObject.text(MapView.CELL_SIZE_METERS / this.currentDivisor + "m");
+ this.jqueryObject.width(newWidth);
+ }
+}
diff --git a/src/scripts/controllers/simulation/chart.ts b/src/scripts/controllers/simulation/chart.ts
new file mode 100644
index 00000000..84009622
--- /dev/null
+++ b/src/scripts/controllers/simulation/chart.ts
@@ -0,0 +1,241 @@
+import * as c3 from "c3";
+import {InteractionLevel, MapController} from "../mapcontroller";
+import {ColorRepresentation, SimulationController} from "../simulationcontroller";
+import {Util} from "../../util";
+
+
+export interface IStateColumn {
+ loadFractions: string[] | number[];
+ inUseMemoryMb: string[] | number[];
+ temperatureC: string[] | number[];
+}
+
+
+export class ChartController {
+ public roomSeries: { [key: number]: IStateColumn };
+ public rackSeries: { [key: number]: IStateColumn };
+ public machineSeries: { [key: number]: IStateColumn };
+ public chart: c3.ChartAPI;
+ public machineChart: c3.ChartAPI;
+
+ private simulationController: SimulationController;
+ private mapController: MapController;
+ private chartData: (string | number)[][];
+ private xSeries: (string | number)[];
+ private names: { [key: string]: string };
+
+
+ constructor(simulationController: SimulationController) {
+ this.simulationController = simulationController;
+ this.mapController = simulationController.mapController;
+ }
+
+ public setup(): void {
+ this.names = {};
+
+ this.roomSeries = {};
+ this.rackSeries = {};
+ this.machineSeries = {};
+
+ this.simulationController.sections.forEach((simulationSection: ISection) => {
+ simulationSection.datacenter.rooms.forEach((room: IRoom) => {
+ if (room.roomType === "SERVER" && this.roomSeries[room.id] === undefined) {
+ this.names["ro" + room.id] = (room.name === "" || room.name === undefined) ?
+ "Unnamed room" : room.name;
+
+ this.roomSeries[room.id] = {
+ loadFractions: ["ro" + room.id],
+ inUseMemoryMb: ["ro" + room.id],
+ temperatureC: ["ro" + room.id]
+ };
+ }
+
+ room.tiles.forEach((tile: ITile) => {
+ if (tile.object !== undefined && tile.objectType === "RACK" && this.rackSeries[tile.objectId] === undefined) {
+ let objectName = (<IRack>tile.object).name;
+ this.names["ra" + tile.objectId] = objectName === "" || objectName === undefined ?
+ "Unnamed rack" : objectName;
+
+ this.rackSeries[tile.objectId] = {
+ loadFractions: ["ra" + tile.objectId],
+ inUseMemoryMb: ["ra" + tile.objectId],
+ temperatureC: ["ra" + tile.objectId]
+ };
+
+ (<IRack>tile.object).machines.forEach((machine: IMachine) => {
+ if (machine === null || this.machineSeries[machine.id] !== undefined) {
+ return;
+ }
+
+ this.names["ma" + machine.id] = "Machine at position " + (machine.position + 1).toString();
+
+ this.machineSeries[machine.id] = {
+ loadFractions: ["ma" + machine.id],
+ inUseMemoryMb: ["ma" + machine.id],
+ temperatureC: ["ma" + machine.id]
+ };
+ });
+ }
+ });
+ });
+ });
+
+
+ this.xSeries = ["time"];
+ this.chartData = [this.xSeries];
+
+ this.chart = this.chartSetup("#statistics-chart");
+ this.machineChart = this.chartSetup("#machine-statistics-chart");
+ }
+
+ public chartSetup(chartId: string): c3.ChartAPI {
+ return c3.generate({
+ bindto: chartId,
+ data: {
+ xFormat: '%S',
+ x: "time",
+ columns: this.chartData,
+ names: this.names
+ },
+ axis: {
+ x: {
+ type: "timeseries",
+ tick: {
+ format: function (time: Date) {
+ let formattedTime = time.getSeconds() + "s";
+
+ if (time.getMinutes() > 0) {
+ formattedTime = time.getMinutes() + "m" + formattedTime;
+ }
+ if (time.getHours() > 0) {
+ formattedTime = time.getHours() + "h" + formattedTime;
+ }
+
+ return formattedTime;
+ },
+ culling: {
+ max: 5
+ },
+ count: 8
+ },
+ padding: {
+ left: 0,
+ right: 10
+ }
+ },
+ y: {
+ min: 0,
+ max: 1,
+ padding: {
+ top: 0,
+ bottom: 0
+ },
+ tick: {
+ format: function (d) {
+ return (Math.round(d * 100) / 100).toString();
+ }
+ }
+ }
+ }
+ });
+ }
+
+ public update(): void {
+ this.xSeries = (<(number|string)[]>["time"]).concat(Util.range(this.simulationController.currentTick));
+
+ this.chartData = [this.xSeries];
+
+ let prefix = "";
+ let machineId = -1;
+ if (this.mapController.interactionLevel === InteractionLevel.BUILDING) {
+ for (let roomId in this.roomSeries) {
+ if (this.roomSeries.hasOwnProperty(roomId)) {
+ if (this.simulationController.colorRepresentation === ColorRepresentation.LOAD) {
+ this.chartData.push(this.roomSeries[roomId].loadFractions);
+ }
+ }
+ }
+ prefix = "ro";
+ } else if (this.mapController.interactionLevel === InteractionLevel.ROOM) {
+ for (let rackId in this.rackSeries) {
+ if (this.rackSeries.hasOwnProperty(rackId) &&
+ this.simulationController.rackToRoomMap[rackId] ===
+ this.mapController.roomModeController.currentRoom.id) {
+ if (this.simulationController.colorRepresentation === ColorRepresentation.LOAD) {
+ this.chartData.push(this.rackSeries[rackId].loadFractions);
+ }
+ }
+ }
+ prefix = "ra";
+ } else if (this.mapController.interactionLevel === InteractionLevel.NODE) {
+ if (this.simulationController.colorRepresentation === ColorRepresentation.LOAD) {
+ this.chartData.push(
+ this.machineSeries[this.mapController.nodeModeController.currentMachine.id].loadFractions
+ );
+ }
+ prefix = "ma";
+ machineId = this.mapController.nodeModeController.currentMachine.id;
+ }
+
+ let unloads: string[] = [];
+ for (let id in this.names) {
+ if (this.names.hasOwnProperty(id)) {
+ if (machineId === -1) {
+ if (id.substr(0, 2) !== prefix ||
+ (this.mapController.interactionLevel === InteractionLevel.ROOM &&
+ this.simulationController.rackToRoomMap[parseInt(id.substr(2))] !==
+ this.mapController.roomModeController.currentRoom.id)) {
+ unloads.push(id);
+ }
+ }
+ else {
+ if (id !== prefix + machineId) {
+ unloads.push(id);
+ }
+ }
+ }
+ }
+
+ let targetChart: c3.ChartAPI;
+ if (this.mapController.interactionLevel === InteractionLevel.NODE) {
+ targetChart = this.machineChart;
+ } else {
+ targetChart = this.chart;
+ }
+
+ targetChart.load({
+ columns: this.chartData,
+ unload: unloads
+ });
+
+ }
+
+ public tickUpdated(tick: number): void {
+ let roomStates: IRoomState[] = this.simulationController.stateCache.stateList[tick].roomStates;
+ roomStates.forEach((roomState: IRoomState) => {
+ ChartController.insertAtIndex(this.roomSeries[roomState.roomId].loadFractions, tick + 1, roomState.loadFraction);
+ });
+
+ let rackStates: IRackState[] = this.simulationController.stateCache.stateList[tick].rackStates;
+ rackStates.forEach((rackState: IRackState) => {
+ ChartController.insertAtIndex(this.rackSeries[rackState.rackId].loadFractions, tick + 1, rackState.loadFraction);
+ });
+
+ let machineStates: IMachineState[] = this.simulationController.stateCache.stateList[tick].machineStates;
+ machineStates.forEach((machineState: IMachineState) => {
+ ChartController.insertAtIndex(this.machineSeries[machineState.machineId].loadFractions, tick + 1, machineState.loadFraction);
+ });
+ }
+
+ private static insertAtIndex(list: any[], index: number, data: any): void {
+ if (index > list.length) {
+ let i = list.length;
+ while (i < index) {
+ list[i] = null;
+ i++;
+ }
+ }
+
+ list[index] = data;
+ }
+} \ No newline at end of file
diff --git a/src/scripts/controllers/simulation/statecache.ts b/src/scripts/controllers/simulation/statecache.ts
new file mode 100644
index 00000000..32f8f4e4
--- /dev/null
+++ b/src/scripts/controllers/simulation/statecache.ts
@@ -0,0 +1,205 @@
+import {SimulationController} from "../simulationcontroller";
+
+
+export class StateCache {
+ public static CACHE_INTERVAL = 3000;
+ private static PREFERRED_CACHE_ADVANCE = 5;
+
+ public stateList: {[key: number]: ITickState};
+ public lastCachedTick: number;
+ public cacheBlock: boolean;
+
+ private simulationController: SimulationController;
+ private intervalId: number;
+ private caching: boolean;
+
+ // Item caches
+ private machineCache: {[keys: number]: IMachine};
+ private rackCache: {[keys: number]: IRack};
+ private roomCache: {[keys: number]: IRoom};
+ private taskCache: {[keys: number]: ITask};
+
+
+ constructor(simulationController: SimulationController) {
+ this.stateList = {};
+ this.lastCachedTick = -1;
+ this.cacheBlock = true;
+ this.simulationController = simulationController;
+ this.caching = false;
+ }
+
+ public startCaching(): void {
+ this.machineCache = {};
+ this.rackCache = {};
+ this.roomCache = {};
+ this.taskCache = {};
+
+ this.simulationController.mapView.currentDatacenter.rooms.forEach((room: IRoom) => {
+ this.addRoomToCache(room);
+ });
+ this.simulationController.currentExperiment.trace.tasks.forEach((task: ITask) => {
+ this.taskCache[task.id] = task;
+ });
+
+ this.caching = true;
+
+ this.cache();
+ this.intervalId = setInterval(() => {
+ this.cache();
+ }, StateCache.CACHE_INTERVAL);
+ }
+
+ private addRoomToCache(room: IRoom) {
+ this.roomCache[room.id] = room;
+
+ room.tiles.forEach((tile: ITile) => {
+ if (tile.objectType === "RACK") {
+ this.rackCache[tile.objectId] = <IRack>tile.object;
+
+ (<IRack> tile.object).machines.forEach((machine: IMachine) => {
+ if (machine !== null) {
+ this.machineCache[machine.id] = machine;
+ }
+ });
+ }
+ });
+ }
+
+ public stopCaching(): void {
+ if (this.caching) {
+ this.caching = false;
+ clearInterval(this.intervalId);
+ }
+ }
+
+ private cache(): void {
+ let tick = this.lastCachedTick + 1;
+
+ this.updateLastTick().then(() => {
+ // Check if end of simulated region has been reached
+ if (this.lastCachedTick > this.simulationController.lastSimulatedTick) {
+ return;
+ }
+
+ this.fetchAllStatesOfTick(tick).then((data: ITickState) => {
+ this.stateList[tick] = data;
+
+ this.updateTasks(tick);
+
+ // Update chart cache
+ this.simulationController.chartController.tickUpdated(tick);
+
+ this.lastCachedTick++;
+
+ if (!this.cacheBlock && this.lastCachedTick - this.simulationController.currentTick <= 0) {
+ this.cacheBlock = true;
+ return;
+ }
+
+ if (this.cacheBlock) {
+ if (this.lastCachedTick - this.simulationController.currentTick >= StateCache.PREFERRED_CACHE_ADVANCE) {
+ this.cacheBlock = false;
+ }
+ }
+ });
+ });
+ }
+
+ private updateTasks(tick: number): void {
+ const taskIDsInTick = [];
+
+ this.stateList[tick].taskStates.forEach((taskState: ITaskState) => {
+ taskIDsInTick.push(taskState.taskId);
+ if (this.stateList[tick - 1] !== undefined) {
+ let previousFlops = 0;
+ const previousStates = this.stateList[tick - 1].taskStates;
+
+ for (let i = 0; i < previousStates.length; i++) {
+ if (previousStates[i].taskId === taskState.taskId) {
+ previousFlops = previousStates[i].flopsLeft;
+ break;
+ }
+ }
+
+ if (previousFlops > 0 && taskState.flopsLeft === 0) {
+ taskState.task.finishedTick = tick;
+ }
+ }
+ });
+
+ // Generate pseudo-task-states for tasks that haven't started yet or have already finished
+ const traceTasks = this.simulationController.currentExperiment.trace.tasks;
+ if (taskIDsInTick.length !== traceTasks.length) {
+ traceTasks
+ .filter((task: ITask) => {
+ return taskIDsInTick.indexOf(task.id) === -1;
+ })
+ .forEach((task: ITask) => {
+ const flopStateCount = task.startTick >= tick ? task.totalFlopCount : 0;
+
+ this.stateList[tick].taskStates.push({
+ id: -1,
+ taskId: task.id,
+ task: task,
+ experimentId: this.simulationController.currentExperiment.id,
+ tick,
+ flopsLeft: flopStateCount
+ });
+ });
+ }
+
+ this.stateList[tick].taskStates.sort((a: ITaskState, b: ITaskState) => {
+ return a.task.startTick - b.task.startTick;
+ });
+ }
+
+ private updateLastTick(): Promise<void> {
+ return this.simulationController.mapController.api.getLastSimulatedTickByExperiment(
+ this.simulationController.simulation.id, this.simulationController.currentExperiment.id).then((data) => {
+ this.simulationController.lastSimulatedTick = data;
+ });
+ }
+
+ private fetchAllStatesOfTick(tick: number): Promise<ITickState> {
+ let tickState: ITickState = {
+ tick,
+ machineStates: [],
+ rackStates: [],
+ roomStates: [],
+ taskStates: []
+ };
+ const promises = [];
+
+ promises.push(this.simulationController.mapController.api.getMachineStatesByTick(
+ this.simulationController.mapView.simulation.id, this.simulationController.currentExperiment.id,
+ tick, this.machineCache
+ ).then((states: IMachineState[]) => {
+ tickState.machineStates = states;
+ }));
+
+ promises.push(this.simulationController.mapController.api.getRackStatesByTick(
+ this.simulationController.mapView.simulation.id, this.simulationController.currentExperiment.id,
+ tick, this.rackCache
+ ).then((states: IRackState[]) => {
+ tickState.rackStates = states;
+ }));
+
+ promises.push(this.simulationController.mapController.api.getRoomStatesByTick(
+ this.simulationController.mapView.simulation.id, this.simulationController.currentExperiment.id,
+ tick, this.roomCache
+ ).then((states: IRoomState[]) => {
+ tickState.roomStates = states;
+ }));
+
+ promises.push(this.simulationController.mapController.api.getTaskStatesByTick(
+ this.simulationController.mapView.simulation.id, this.simulationController.currentExperiment.id,
+ tick, this.taskCache
+ ).then((states: ITaskState[]) => {
+ tickState.taskStates = states;
+ }));
+
+ return Promise.all(promises).then(() => {
+ return tickState;
+ });
+ }
+}
diff --git a/src/scripts/controllers/simulation/taskview.ts b/src/scripts/controllers/simulation/taskview.ts
new file mode 100644
index 00000000..d989e103
--- /dev/null
+++ b/src/scripts/controllers/simulation/taskview.ts
@@ -0,0 +1,64 @@
+import * as $ from "jquery";
+import {SimulationController} from "../simulationcontroller";
+import {Util} from "../../util";
+
+
+export class TaskViewController {
+ private simulationController: SimulationController;
+
+
+ constructor(simulationController: SimulationController) {
+ this.simulationController = simulationController;
+ }
+
+ /**
+ * Populates and displays the list of tasks with their current state.
+ */
+ public update() {
+ const container = $(".task-list");
+ container.children().remove(".task-element");
+
+ this.simulationController.stateCache.stateList[this.simulationController.currentTick].taskStates
+ .forEach((taskState: ITaskState) => {
+ const html = this.generateTaskElementHTML(taskState);
+ container.append(html);
+ });
+ }
+
+ private generateTaskElementHTML(taskState: ITaskState) {
+ let iconType, timeInfo;
+
+ if (taskState.task.startTick > this.simulationController.currentTick) {
+ iconType = "glyphicon-time";
+ timeInfo = "Not started yet";
+ } else if (taskState.task.startTick <= this.simulationController.currentTick && taskState.flopsLeft > 0) {
+ iconType = "glyphicon-refresh";
+ timeInfo = "Started at " + Util.convertSecondsToFormattedTime(taskState.task.startTick);
+ } else if (taskState.flopsLeft === 0) {
+ iconType = "glyphicon-ok";
+ timeInfo = "Started at " + Util.convertSecondsToFormattedTime(taskState.task.startTick);
+ }
+
+ // Calculate progression ratio
+ const progress = 1 - (taskState.flopsLeft / taskState.task.totalFlopCount);
+
+ // Generate completion text
+ const flopsCompleted = taskState.task.totalFlopCount - taskState.flopsLeft;
+ const completionInfo = "Completed: " + flopsCompleted + " / " + taskState.task.totalFlopCount + " FLOPS";
+
+ return '<div class="task-element">' +
+ ' <div class="task-icon glyphicon ' + iconType + '"></div>' +
+ ' <div class="task-info">' +
+ ' <div class="task-time">' + timeInfo +
+ ' </div>' +
+ ' <div class="progress">' +
+ ' <div class="progress-bar progress-bar-striped" role="progressbar" aria-valuenow="' +
+ progress * 100 + '%"' +
+ ' aria-valuemin="0" aria-valuemax="100" style="width: ' + progress * 100 + '%">' +
+ ' </div>' +
+ ' </div>' +
+ ' <div class="task-flops">' + completionInfo + '</div>' +
+ ' </div>' +
+ '</div>';
+ }
+}
diff --git a/src/scripts/controllers/simulation/timeline.ts b/src/scripts/controllers/simulation/timeline.ts
new file mode 100644
index 00000000..a558afe1
--- /dev/null
+++ b/src/scripts/controllers/simulation/timeline.ts
@@ -0,0 +1,161 @@
+import {SimulationController} from "../simulationcontroller";
+import {Util} from "../../util";
+import * as $ from "jquery";
+
+
+export class TimelineController {
+ private simulationController: SimulationController;
+ private startLabel: JQuery;
+ private endLabel: JQuery;
+ private playButton: JQuery;
+ private loadingIcon: JQuery;
+ private cacheSection: JQuery;
+ private timeMarker: JQuery;
+ private timeline: JQuery;
+ private timeUnitFraction: number;
+ private timeMarkerWidth: number;
+ private timelineWidth: number;
+
+
+ constructor(simulationController: SimulationController) {
+ this.simulationController = simulationController;
+ this.startLabel = $(".timeline-container .labels .start-time-label");
+ this.endLabel = $(".timeline-container .labels .end-time-label");
+ this.playButton = $(".timeline-container .play-btn");
+ this.loadingIcon = this.playButton.find("img");
+ this.cacheSection = $(".timeline-container .timeline .cache-section");
+ this.timeMarker = $(".timeline-container .timeline .time-marker");
+ this.timeline = $(".timeline-container .timeline");
+ this.timeMarkerWidth = this.timeMarker.width();
+ this.timelineWidth = this.timeline.width();
+ }
+
+ public togglePlayback(): void {
+ if (this.simulationController.stateCache.cacheBlock) {
+ this.simulationController.playing = false;
+ return;
+ }
+ this.simulationController.playing = !this.simulationController.playing;
+ this.setButtonIcon();
+ }
+
+ public setupListeners(): void {
+ this.playButton.on("click", () => {
+ this.togglePlayback();
+ });
+
+ $(".timeline-container .timeline").on("click", (event: JQueryEventObject) => {
+ let parentOffset = $(event.target).closest(".timeline").offset();
+ let clickX = event.pageX - parentOffset.left;
+
+ let newTick = Math.round(clickX / (this.timelineWidth * this.timeUnitFraction));
+
+ if (newTick > this.simulationController.stateCache.lastCachedTick) {
+ newTick = this.simulationController.stateCache.lastCachedTick;
+ }
+ this.simulationController.currentTick = newTick;
+ this.simulationController.checkCurrentSimulationSection();
+ this.simulationController.update();
+ });
+ }
+
+ public setButtonIcon(): void {
+ if (this.simulationController.playing && !this.playButton.hasClass("glyphicon-pause")) {
+ this.playButton.removeClass("glyphicon-play").addClass("glyphicon-pause");
+ } else if (!this.simulationController.playing && !this.playButton.hasClass("glyphicon-play")) {
+ this.playButton.removeClass("glyphicon-pause").addClass("glyphicon-play");
+ }
+ }
+
+ public update(): void {
+ this.timeUnitFraction = 1 / (this.simulationController.lastSimulatedTick + 1);
+ this.timelineWidth = $(".timeline-container .timeline").width();
+
+ this.updateTimeLabels();
+
+ this.cacheSection.css("width", this.calculateTickPosition(this.simulationController.stateCache.lastCachedTick));
+ this.timeMarker.css("left", this.calculateTickPosition(this.simulationController.currentTick));
+
+ this.updateTaskIndicators();
+ this.updateSectionMarkers();
+
+ if (this.simulationController.stateCache.cacheBlock) {
+ this.playButton.removeClass("glyphicon-pause").removeClass("glyphicon-play");
+ this.loadingIcon.show();
+ } else {
+ this.loadingIcon.hide();
+ this.setButtonIcon();
+ }
+ }
+
+ private updateTimeLabels(): void {
+ this.startLabel.text(Util.convertSecondsToFormattedTime(this.simulationController.currentTick));
+ this.endLabel.text(Util.convertSecondsToFormattedTime(this.simulationController.lastSimulatedTick));
+ }
+
+ private updateSectionMarkers(): void {
+ $(".section-marker").remove();
+
+ this.simulationController.sections.forEach((simulationSection: ISection) => {
+ if (simulationSection.startTick === 0) {
+ return;
+ }
+
+ this.timeline.append(
+ $('<div class="section-marker">')
+ .css("left", this.calculateTickPosition(simulationSection.startTick))
+ );
+ });
+ }
+
+ private updateTaskIndicators(): void {
+ $(".task-indicator").remove();
+
+ let tickStateTypes = {
+ "queueEntryTick": "task-queued",
+ "startTick": "task-started",
+ "finishedTick": "task-finished"
+ };
+
+ if (this.simulationController.stateCache.lastCachedTick === -1) {
+ return;
+ }
+
+ let indicatorCountList = new Array(this.simulationController.stateCache.lastCachedTick);
+ let indicator;
+ this.simulationController.currentExperiment.trace.tasks.forEach((task: ITask) => {
+ for (let tickStateType in tickStateTypes) {
+ if (!tickStateTypes.hasOwnProperty(tickStateType)) {
+ continue;
+ }
+
+ if (task[tickStateType] !== undefined &&
+ task[tickStateType] <= this.simulationController.stateCache.lastCachedTick) {
+
+ let bottomOffset;
+ if (indicatorCountList[task[tickStateType]] === undefined) {
+ indicatorCountList[task[tickStateType]] = 1;
+ bottomOffset = 0;
+ } else {
+ bottomOffset = indicatorCountList[task[tickStateType]] * 10;
+ indicatorCountList[task[tickStateType]]++;
+ }
+ indicator = $('<div class="task-indicator ' + tickStateTypes[tickStateType] + '">')
+ .css("left", this.calculateTickPosition(task[tickStateType]))
+ .css("bottom", bottomOffset);
+ this.timeline.append(indicator);
+ }
+ }
+ });
+ }
+
+ private calculateTickPosition(tick: number): string {
+ let correction = 0;
+ if (this.timeUnitFraction * this.timelineWidth > this.timeMarkerWidth) {
+ correction = (this.timeUnitFraction * this.timelineWidth - this.timeMarkerWidth) *
+ (tick / this.simulationController.lastSimulatedTick);
+ }
+
+ return (100 * (this.timeUnitFraction * tick + correction / this.timelineWidth)) + "%";
+ }
+} \ No newline at end of file
diff --git a/src/scripts/controllers/simulationcontroller.ts b/src/scripts/controllers/simulationcontroller.ts
new file mode 100644
index 00000000..8d9553e9
--- /dev/null
+++ b/src/scripts/controllers/simulationcontroller.ts
@@ -0,0 +1,586 @@
+///<reference path="../../../typings/index.d.ts" />
+///<reference path="mapcontroller.ts" />
+import * as $ from "jquery";
+import {MapView} from "../views/mapview";
+import {MapController, InteractionLevel, AppMode} from "./mapcontroller";
+import {Util} from "../util";
+import {StateCache} from "./simulation/statecache";
+import {ChartController} from "./simulation/chart";
+import {TaskViewController} from "./simulation/taskview";
+import {TimelineController} from "./simulation/timeline";
+
+
+export enum ColorRepresentation {
+ LOAD,
+ TEMPERATURE,
+ MEMORY
+}
+
+
+export class SimulationController {
+ public mapView: MapView;
+ public mapController: MapController;
+
+ public playing: boolean;
+ public currentTick: number;
+ public stateCache: StateCache;
+ public lastSimulatedTick: number;
+ public simulation: ISimulation;
+ public experiments: IExperiment[];
+ public currentExperiment: IExperiment;
+ public currentPath: IPath;
+ public sections: ISection[];
+ public currentSection: ISection;
+ public experimentSelectionMode: boolean;
+ public traces: ITrace[];
+ public schedulers: IScheduler[];
+ public sectionIndex: number;
+ public chartController: ChartController;
+ public timelineController: TimelineController;
+
+ public colorRepresentation: ColorRepresentation;
+ public rackToRoomMap: {[key: number]: number;};
+
+ private taskViewController: TaskViewController;
+ private tickerId: number;
+
+
+ public static showOrHideSimComponents(visibility: boolean): void {
+ if (visibility) {
+ $("#statistics-menu").removeClass("hidden");
+ $("#experiment-menu").removeClass("hidden");
+ $("#tasks-menu").removeClass("hidden");
+ $(".timeline-container").removeClass("hidden");
+ } else {
+ $("#statistics-menu").addClass("hidden");
+ $("#experiment-menu").addClass("hidden");
+ $("#tasks-menu").addClass("hidden");
+ $(".timeline-container").addClass("hidden");
+ }
+ }
+
+ constructor(mapController: MapController) {
+ this.mapController = mapController;
+ this.mapView = this.mapController.mapView;
+ this.simulation = this.mapController.mapView.simulation;
+ this.experiments = this.simulation.experiments;
+ this.taskViewController = new TaskViewController(this);
+ this.timelineController = new TimelineController(this);
+ this.chartController = new ChartController(this);
+
+ this.timelineController.setupListeners();
+ this.experimentSelectionMode = true;
+ this.sectionIndex = 0;
+
+ this.currentTick = 0;
+ this.playing = false;
+ this.stateCache = new StateCache(this);
+ this.colorRepresentation = ColorRepresentation.LOAD;
+
+ this.traces = [];
+ this.schedulers = [];
+
+ this.mapController.api.getAllTraces().then((data) => {
+ this.traces = data;
+ });
+
+ this.mapController.api.getAllSchedulers().then((data) => {
+ this.schedulers = data;
+ });
+ }
+
+ public enterMode() {
+ this.experimentSelectionMode = true;
+
+ if (this.mapController.interactionLevel === InteractionLevel.BUILDING) {
+ this.mapView.roomLayer.coloringMode = true;
+ this.mapView.dcObjectLayer.coloringMode = false;
+ } else if (this.mapController.interactionLevel === InteractionLevel.ROOM ||
+ this.mapController.interactionLevel === InteractionLevel.OBJECT) {
+ this.mapView.roomLayer.coloringMode = false;
+ this.mapView.dcObjectLayer.coloringMode = true;
+ } else if (this.mapController.interactionLevel === InteractionLevel.NODE) {
+ this.mapController.nodeModeController.goToObjectMode();
+ }
+
+ this.mapController.appMode = AppMode.SIMULATION;
+ this.mapView.dcObjectLayer.detailedMode = false;
+ this.mapView.gridLayer.setVisibility(false);
+ this.mapView.updateScene = true;
+
+ this.mapController.setAllMenuModes();
+ SimulationController.showOrHideSimComponents(true);
+ $(".mode-switch").attr("data-selected", "simulation");
+ $("#save-version-btn").hide();
+ $(".color-indicator").removeClass("hidden");
+
+ $("#change-experiment-btn").click(() => {
+ this.playing = false;
+ this.stateCache.stopCaching();
+ this.timelineController.update();
+ this.showExperimentsDialog();
+ });
+
+ this.setupColorMenu();
+ this.showExperimentsDialog();
+ }
+
+ private launchSimulation(): void {
+ this.onSimulationSectionChange();
+
+ this.chartController.setup();
+
+ this.stateCache.startCaching();
+
+ this.tickerId = setInterval(() => {
+ this.simulationTick();
+ }, 1000);
+ }
+
+ private onSimulationSectionChange(): void {
+ this.currentSection = this.currentPath.sections[this.sectionIndex];
+ this.mapView.currentDatacenter = this.currentSection.datacenter;
+
+ // Generate a map of all rack IDs in relation to their room IDs for use in room stats
+ this.rackToRoomMap = {};
+ this.currentSection.datacenter.rooms.forEach((room: IRoom) => {
+ room.tiles.forEach((tile: ITile) => {
+ if (tile.object !== undefined && tile.objectType === "RACK") {
+ this.rackToRoomMap[tile.objectId] = room.id;
+ }
+ });
+ });
+
+ if (this.mapController.interactionLevel === InteractionLevel.NODE) {
+ this.mapController.nodeModeController.goToObjectMode();
+ }
+ if (this.mapController.interactionLevel === InteractionLevel.OBJECT) {
+ this.mapController.objectModeController.goToRoomMode();
+ }
+ if (this.mapController.interactionLevel === InteractionLevel.ROOM) {
+ this.mapController.roomModeController.goToBuildingMode();
+ }
+
+ this.mapView.redrawMap();
+
+ this.mapView.zoomOutOnDC();
+ }
+
+ public exitMode() {
+ this.closeExperimentsDialog();
+
+ this.mapController.appMode = AppMode.CONSTRUCTION;
+ this.mapView.dcObjectLayer.detailedMode = true;
+ this.mapView.gridLayer.setVisibility(true);
+ this.mapView.redrawMap();
+
+ this.stateCache.stopCaching();
+ this.playing = false;
+
+ this.mapController.setAllMenuModes();
+ SimulationController.showOrHideSimComponents(false);
+
+ this.setColors();
+ $(".color-indicator").addClass("hidden")["popover"]("hide").off();
+ $(".mode-switch").attr("data-selected", "construction");
+ $("#save-version-btn").show();
+
+ clearInterval(this.tickerId);
+ }
+
+ public update() {
+ if (this.stateCache.cacheBlock) {
+ return;
+ }
+
+ this.setColors();
+ this.updateBuildingStats();
+ this.updateRoomStats();
+ this.chartController.update();
+ this.taskViewController.update();
+ }
+
+ public simulationTick(): void {
+ this.timelineController.update();
+
+ if (this.currentTick > this.lastSimulatedTick) {
+ this.currentTick = this.lastSimulatedTick;
+ this.playing = false;
+ this.timelineController.setButtonIcon();
+ }
+
+ if (this.playing) {
+ this.checkCurrentSimulationSection();
+ this.update();
+
+ if (!this.stateCache.cacheBlock) {
+ this.currentTick++;
+ }
+ }
+ }
+
+ public checkCurrentSimulationSection(): void {
+ for (let i = this.sections.length - 1; i >= 0; i--) {
+ if (this.currentTick >= this.sections[i].startTick) {
+ if (this.sectionIndex !== i) {
+ this.sectionIndex = i;
+ this.onSimulationSectionChange();
+ }
+ break;
+ }
+ }
+ }
+
+ public transitionFromBuildingToRoom(): void {
+ this.mapView.roomLayer.coloringMode = false;
+ this.mapView.dcObjectLayer.coloringMode = true;
+
+ this.setColors();
+ this.updateRoomStats();
+ this.chartController.update();
+ }
+
+ public transitionFromRoomToBuilding(): void {
+ this.mapView.roomLayer.coloringMode = true;
+ this.mapView.dcObjectLayer.coloringMode = false;
+
+ this.setColors();
+ this.updateBuildingStats();
+ this.chartController.update();
+ }
+
+ public transitionFromRoomToRack(): void {
+ this.setColors();
+ $("#statistics-menu").addClass("hidden");
+ this.chartController.update();
+ }
+
+ public transitionFromRackToRoom(): void {
+ this.setColors();
+ $("#statistics-menu").removeClass("hidden");
+ }
+
+ public transitionFromRackToNode(): void {
+ this.chartController.update();
+ }
+
+ public transitionFromNodeToRack(): void {
+ }
+
+ private showExperimentsDialog(): void {
+ $(".experiment-name-alert").hide();
+
+ this.populateExperimentsList();
+ this.populateDropdowns();
+
+ $(".experiment-row").click((event: JQueryEventObject) => {
+ if ($(event.target).hasClass("remove-experiment")) {
+ return;
+ }
+
+ let row = $(event.target).closest(".experiment-row");
+ this.prepareAndLaunchExperiment(this.experiments[row.index()]);
+ });
+
+ $(".experiment-list .list-body").on("click", ".remove-experiment", (event: JQueryEventObject) => {
+ event.stopPropagation();
+ let affectedRow = $(event.target).closest(".experiment-row");
+ let index = affectedRow.index();
+ let affectedExperiment = this.experiments[index];
+
+ MapController.showConfirmDeleteDialog("experiment", () => {
+ this.mapController.api.deleteExperiment(affectedExperiment.simulationId, affectedExperiment.id)
+ .then(() => {
+ this.experiments.splice(index, 1);
+ this.populateExperimentsList();
+ });
+ });
+ });
+
+ $("#new-experiment-btn").click(() => {
+ let nameInput = $("#new-experiment-name-input");
+ if (nameInput.val() === "") {
+ $(".experiment-name-alert").show();
+ return;
+ } else {
+ $(".experiment-name-alert").hide();
+ }
+
+ let newExperiment: IExperiment = {
+ id: -1,
+ name: nameInput.val(),
+ pathId: parseInt($("#new-experiment-path-select").val()),
+ schedulerName: $("#new-experiment-scheduler-select").val(),
+ traceId: parseInt($("#new-experiment-trace-select").val()),
+ simulationId: this.simulation.id
+ };
+
+ this.mapController.api.addExperimentToSimulation(this.simulation.id, newExperiment)
+ .then((data: IExperiment) => {
+ this.simulation.experiments.push(data);
+ this.prepareAndLaunchExperiment(data);
+ });
+ });
+
+ $(".window-close").click(() => {
+ this.exitMode();
+ });
+
+ $(".window-overlay").fadeIn(200);
+ }
+
+ private prepareAndLaunchExperiment(experiment: IExperiment): void {
+ this.prepareSimulationData(experiment);
+ this.launchSimulation();
+ this.closeExperimentsDialog();
+ }
+
+ private prepareSimulationData(experiment: IExperiment): void {
+ this.currentExperiment = experiment;
+ this.currentPath = this.getPathById(this.currentExperiment.pathId);
+ this.sections = this.currentPath.sections;
+ this.sectionIndex = 0;
+ this.currentTick = 0;
+ this.playing = false;
+ this.stateCache = new StateCache(this);
+ this.colorRepresentation = ColorRepresentation.LOAD;
+
+ this.sections.sort((a: ISection, b: ISection) => {
+ return a.startTick - b.startTick;
+ });
+
+ $("#experiment-menu-name").text(experiment.name);
+ $("#experiment-menu-path").text(SimulationController.getPathName(this.currentPath));
+ $("#experiment-menu-scheduler").text(experiment.schedulerName);
+ $("#experiment-menu-trace").text(experiment.trace.name);
+ }
+
+ private closeExperimentsDialog(): void {
+ $(".window-overlay").fadeOut(200);
+ $(".window-overlay input").val("");
+ }
+
+ private populateDropdowns(): void {
+ let pathDropdown = $("#new-experiment-path-select");
+ let traceDropdown = $("#new-experiment-trace-select");
+ let schedulerDropdown = $("#new-experiment-scheduler-select");
+
+ pathDropdown.empty();
+ for (let i = 0; i < this.simulation.paths.length; i++) {
+ pathDropdown.append(
+ $("<option>").text(SimulationController.getPathName(this.simulation.paths[i]))
+ .val(this.simulation.paths[i].id)
+ );
+ }
+
+ traceDropdown.empty();
+ for (let i = 0; i < this.traces.length; i++) {
+ traceDropdown.append(
+ $("<option>").text(this.traces[i].name)
+ .val(this.traces[i].id)
+ );
+ }
+
+ schedulerDropdown.empty();
+ for (let i = 0; i < this.schedulers.length; i++) {
+ schedulerDropdown.append(
+ $("<option>").text(this.schedulers[i].name)
+ .val(this.schedulers[i].name)
+ );
+ }
+ }
+
+ /**
+ * Populates the list of experiments.
+ */
+ private populateExperimentsList(): void {
+ let table = $(".experiment-list .list-body");
+ table.empty();
+
+ console.log("EXPERIMENT", this.experiments);
+ console.log("SIMULATION", this.simulation);
+
+ if (this.experiments.length === 0) {
+ $(".experiment-list").hide();
+ $(".no-experiments-alert").show();
+ } else {
+ $(".no-experiments-alert").hide();
+ this.experiments.forEach((experiment: IExperiment) => {
+ table.append(
+ '<div class="experiment-row">' +
+ ' <div>' + experiment.name + '</div>' +
+ ' <div>' + this.getPathNameById(experiment.pathId) + '</div>' +
+ ' <div>' + experiment.trace.name + '</div>' +
+ ' <div>' + experiment.schedulerName + '</div>' +
+ ' <div class="remove-experiment glyphicon glyphicon-remove"></div>' +
+ '</div>'
+ );
+ });
+ }
+ }
+
+ private getPathNameById(id: number): string {
+ for (let i = 0; i < this.simulation.paths.length; i++) {
+ if (id === this.simulation.paths[i].id) {
+ return SimulationController.getPathName(this.simulation.paths[i]);
+ }
+ }
+ }
+
+ private getPathById(id: number): IPath {
+ for (let i = 0; i < this.simulation.paths.length; i++) {
+ if (id === this.simulation.paths[i].id) {
+ return this.simulation.paths[i];
+ }
+ }
+ }
+
+ private static getPathName(path: IPath): string {
+ if (path.name === null) {
+ return "Path " + path.id;
+ } else {
+ return path.name;
+ }
+ }
+
+ private setColors() {
+ if (this.mapController.appMode === AppMode.SIMULATION) {
+ if (this.mapController.interactionLevel === InteractionLevel.BUILDING) {
+ this.mapView.roomLayer.intensityLevels = {};
+
+ this.stateCache.stateList[this.currentTick].roomStates.forEach((roomState: IRoomState) => {
+ if (this.colorRepresentation === ColorRepresentation.LOAD) {
+ this.mapView.roomLayer.intensityLevels[roomState.roomId] =
+ Util.determineLoadIntensityLevel(roomState.loadFraction);
+ }
+ });
+
+ this.mapView.roomLayer.draw();
+ this.mapView.dcObjectLayer.draw();
+ } else if (this.mapController.interactionLevel === InteractionLevel.ROOM ||
+ this.mapController.interactionLevel === InteractionLevel.OBJECT) {
+ this.mapView.dcObjectLayer.intensityLevels = {};
+
+ this.stateCache.stateList[this.currentTick].rackStates.forEach((rackState: IRackState) => {
+ if (this.colorRepresentation === ColorRepresentation.LOAD) {
+ this.mapView.dcObjectLayer.intensityLevels[rackState.rackId] =
+ Util.determineLoadIntensityLevel(rackState.loadFraction);
+ }
+ });
+
+ this.mapView.roomLayer.draw();
+ this.mapView.dcObjectLayer.draw();
+ }
+
+ if (this.mapController.interactionLevel === InteractionLevel.OBJECT ||
+ this.mapController.interactionLevel === InteractionLevel.NODE) {
+ this.stateCache.stateList[this.currentTick].machineStates.forEach((machineState: IMachineState) => {
+ let element = $('.node-element[data-id="' + machineState.machineId + '"] .node-element-content');
+ element.css("background-color", Util.convertIntensityToColor(
+ Util.determineLoadIntensityLevel(machineState.loadFraction)
+ ));
+
+ // Color all transparent icon overlays, as well
+ element = $('.node-element[data-id="' + machineState.machineId + '"] .icon-overlay');
+ element.css("background-color", Util.convertIntensityToColor(
+ Util.determineLoadIntensityLevel(machineState.loadFraction)
+ ));
+ });
+ }
+ } else {
+ this.mapView.roomLayer.coloringMode = false;
+ this.mapView.dcObjectLayer.coloringMode = false;
+
+ this.mapView.roomLayer.draw();
+ this.mapView.dcObjectLayer.draw();
+ }
+ }
+
+ /**
+ * Populates the building simulation menu with dynamic statistics concerning the state of all rooms in the building.
+ */
+ private updateBuildingStats(): void {
+ if (this.mapController.interactionLevel !== InteractionLevel.BUILDING) {
+ return;
+ }
+
+ console.log(this.stateCache);
+
+ let html;
+ let container = $(".building-stats-list");
+
+ container.children().remove("div");
+
+ this.stateCache.stateList[this.currentTick].roomStates.forEach((roomState: IRoomState) => {
+ if (this.colorRepresentation === ColorRepresentation.LOAD && roomState.room !== undefined) {
+ html = '<div>' +
+ ' <h4>' + roomState.room.name + '</h4>' +
+ ' <p>Load: ' + Math.round(roomState.loadFraction * 100) + '%</p>' +
+ '</div>';
+ container.append(html);
+ }
+ });
+
+ }
+
+ /**
+ * Populates the room simulation menu with dynamic statistics concerning the state of all racks in the room.
+ */
+ private updateRoomStats(): void {
+ if (this.mapController.interactionLevel !== InteractionLevel.ROOM) {
+ return;
+ }
+
+ $("#room-name-field").text(this.mapController.roomModeController.currentRoom.name);
+ $("#room-type-field").text(this.mapController.roomModeController.currentRoom.roomType);
+
+ let html;
+ let container = $(".room-stats-list");
+
+ container.children().remove("div");
+
+ this.stateCache.stateList[this.currentTick].rackStates.forEach((rackState: IRackState) => {
+ if (this.rackToRoomMap[rackState.rackId] !== this.mapController.roomModeController.currentRoom.id) {
+ return;
+ }
+ if (this.colorRepresentation === ColorRepresentation.LOAD) {
+ html = '<div>' +
+ ' <h4>' + rackState.rack.name + '</h4>' +
+ ' <p>Load: ' + Math.round(rackState.loadFraction * 100) + '%</p>' +
+ '</div>';
+ container.append(html);
+ }
+ });
+ }
+
+ private setupColorMenu(): void {
+ let html =
+ '<select class="form-control" title="Color Representation" id="color-representation-select">' +
+ ' <option value="1" selected>Load</option>' +
+ ' <option value="2">Power use</option>' +
+ '</select>';
+
+ let indicator = $(".color-indicator");
+ indicator["popover"]({
+ animation: true,
+ content: html,
+ html: true,
+ placement: "top",
+ title: "Colors represent:",
+ trigger: "manual"
+ });
+ indicator.click(() => {
+ //noinspection JSJQueryEfficiency // suppressed for dynamic element insertion
+ if ($("#color-representation-select").length) {
+ indicator["popover"]("hide");
+ } else {
+ indicator["popover"]("show");
+
+ let selectElement = $("#color-representation-select");
+ selectElement.change(() => {
+ console.log(selectElement.val());
+ });
+ }
+ });
+ }
+}