summaryrefslogtreecommitdiff
path: root/src/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'src/scripts')
-rw-r--r--src/scripts/colors.ts43
-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
-rw-r--r--src/scripts/definitions.ts318
-rw-r--r--src/scripts/error404.entry.ts26
-rw-r--r--src/scripts/main.entry.ts69
-rw-r--r--src/scripts/profile.entry.ts40
-rw-r--r--src/scripts/projects.entry.ts651
-rw-r--r--src/scripts/serverconnection.ts59
-rw-r--r--src/scripts/splash.entry.ts160
-rw-r--r--src/scripts/tests/util.spec.ts326
-rw-r--r--src/scripts/user.ts76
-rw-r--r--src/scripts/util.ts600
-rw-r--r--src/scripts/views/layers/dcobject.ts252
-rw-r--r--src/scripts/views/layers/dcprogressbar.ts99
-rw-r--r--src/scripts/views/layers/gray.ts145
-rw-r--r--src/scripts/views/layers/grid.ts59
-rw-r--r--src/scripts/views/layers/hover.ts129
-rw-r--r--src/scripts/views/layers/layer.ts8
-rw-r--r--src/scripts/views/layers/room.ts177
-rw-r--r--src/scripts/views/layers/roomtext.ts68
-rw-r--r--src/scripts/views/layers/wall.ts62
-rw-r--r--src/scripts/views/mapview.ts373
35 files changed, 8537 insertions, 0 deletions
diff --git a/src/scripts/colors.ts b/src/scripts/colors.ts
new file mode 100644
index 00000000..559b7ee3
--- /dev/null
+++ b/src/scripts/colors.ts
@@ -0,0 +1,43 @@
+/**
+ * Class serving as a color palette for the application.
+ */
+export class Colors {
+ public static GRID_COLOR = "rgba(0, 0, 0, 0.5)";
+
+ public static WALL_COLOR = "rgba(0, 0, 0, 1)";
+
+ public static ROOM_DEFAULT = "rgba(150, 150, 150, 1)";
+ public static ROOM_SELECTED = "rgba(51, 153, 255, 1)";
+ public static ROOM_HOVER_VALID = "rgba(51, 153, 255, 0.5)";
+ public static ROOM_HOVER_INVALID = "rgba(255, 102, 0, 0.5)";
+ public static ROOM_NAME_COLOR = "rgba(245, 245, 245, 1)";
+ public static ROOM_TYPE_COLOR = "rgba(245, 245, 245, 1)";
+
+ public static RACK_BACKGROUND = "rgba(170, 170, 170, 1)";
+ public static RACK_BORDER = "rgba(0, 0, 0, 1)";
+ public static RACK_SPACE_BAR_BACKGROUND = "rgba(222, 235, 247, 1)";
+ public static RACK_SPACE_BAR_FILL = "rgba(91, 155, 213, 1)";
+ public static RACK_ENERGY_BAR_BACKGROUND = "rgba(255, 242, 204, 1)";
+ public static RACK_ENERGY_BAR_FILL = "rgba(255, 192, 0, 1)";
+
+ public static COOLING_ITEM_BACKGROUND = "rgba(40, 50, 230, 1)";
+ public static COOLING_ITEM_BORDER = "rgba(0, 0, 0, 1)";
+
+ public static PSU_BACKGROUND = "rgba(230, 50, 60, 1)";
+ public static PSU_BORDER = "rgba(0, 0, 0, 1)";
+
+ public static GRAYED_OUT_AREA = "rgba(0, 0, 0, 0.6)";
+
+ public static INFO_BALLOON_INFO = "rgba(40, 50, 230, 1)";
+ public static INFO_BALLOON_WARNING = "rgba(230, 60, 70, 1)";
+
+ public static INFO_BALLOON_MAP = {
+ "info": Colors.INFO_BALLOON_INFO,
+ "warning": Colors.INFO_BALLOON_WARNING
+ };
+
+ public static SIM_LOW = "rgba(197, 224, 180, 1)";
+ public static SIM_MID_LOW = "rgba(255, 230, 153, 1)";
+ public static SIM_MID_HIGH = "rgba(248, 203, 173, 1)";
+ public static SIM_HIGH = "rgba(249, 165, 165, 1)";
+}
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());
+ });
+ }
+ });
+ }
+}
diff --git a/src/scripts/definitions.ts b/src/scripts/definitions.ts
new file mode 100644
index 00000000..a6893407
--- /dev/null
+++ b/src/scripts/definitions.ts
@@ -0,0 +1,318 @@
+/**
+ * JSON Specification of the data model.
+ *
+ * Represents the data model after populating it (based on the DB ideas retrieved from the backend). Unpopulated
+ * objects end with 'Stub'.
+ */
+
+// Webpack require declaration
+declare var require: {
+ <T>(path: string): T;
+ (paths: string[], callback: (...modules: any[]) => void): void;
+ ensure: (paths: string[], callback: (require: <T>(path: string) => T) => void) => void;
+};
+
+// Meta-constructs
+interface IDateTime {
+ year: number;
+ month: number;
+ day: number;
+ hour: number;
+ minute: number;
+ second: number;
+}
+
+interface IGridPosition {
+ x: number;
+ y: number;
+}
+
+interface IBounds {
+ min: number[];
+ center: number[];
+ max: number[];
+}
+
+interface IRoomNamePos {
+ topLeft: IGridPosition;
+ length: number;
+}
+
+interface IRoomWall {
+ startPos: number[];
+ horizontal: boolean;
+ length: number;
+}
+
+interface TilePositionObject {
+ position: IGridPosition;
+ tileObject: createjs.Shape;
+}
+
+type IRoomTypeMap = { [key: string]: string[]; };
+
+interface ITickState {
+ tick: number;
+ roomStates: IRoomState[];
+ rackStates: IRackState[];
+ machineStates: IMachineState[];
+ taskStates: ITaskState[];
+}
+
+// Communication
+interface IRequest {
+ id?: number;
+ path: string;
+ method: string;
+ parameters: {
+ body: any;
+ path: any;
+ query: any;
+ };
+ token?: string;
+}
+
+interface IResponse {
+ id?: number;
+ status: {
+ code: number;
+ description?: string;
+ };
+ content: any;
+}
+
+// Simulation
+interface ISimulation {
+ id: number;
+ name: string;
+ paths?: IPath[];
+ experiments?: IExperiment[];
+ datetimeCreated: string;
+ datetimeCreatedParsed?: IDateTime;
+ datetimeLastEdited: string;
+ datetimeLastEditedParsed?: IDateTime;
+}
+
+interface ISection {
+ id: number;
+ startTick: number;
+ simulationId: number;
+ datacenterId: number;
+ datacenter?: IDatacenter;
+}
+
+interface IPath {
+ id: number;
+ simulationId: number;
+ sections?: ISection[];
+ name: string;
+ datetimeCreated: string;
+}
+
+interface ITrace {
+ id: number;
+ name: string;
+ tasks?: ITask[];
+}
+
+interface IScheduler {
+ name: string;
+}
+
+interface ITask {
+ id: number;
+ traceId: number;
+ queueEntryTick: number;
+ startTick?: number;
+ finishedTick?: number;
+ totalFlopCount: number;
+}
+
+interface IExperiment {
+ id: number;
+ simulationId: number;
+ pathId: number;
+ traceId: number;
+ trace?: ITrace;
+ schedulerName: string;
+ name: string;
+}
+
+// Authorization
+interface IAuthorization {
+ userId: number;
+ user?: IUser;
+ simulationId: number;
+ simulation?: ISimulation;
+ authorizationLevel: string;
+}
+
+interface IUser {
+ id: number;
+ googleId: number;
+ email: string;
+ givenName: string;
+ familyName: string;
+}
+
+// DC Layout
+interface IDatacenter {
+ id: number;
+ rooms?: IRoom[];
+}
+
+interface IRoom {
+ id: number;
+ datacenterId: number;
+ name: string;
+ roomType: string;
+ tiles?: ITile[];
+}
+
+interface ITile {
+ id: number;
+ roomId: number;
+ objectId?: number;
+ objectType?: string;
+ object?: IDCObject;
+ position: IGridPosition;
+}
+
+// State
+interface IMachineState {
+ id: number;
+ machineId: number;
+ machine?: IMachine;
+ experimentId: number;
+ tick: number;
+ temperatureC: number;
+ inUseMemoryMb: number;
+ loadFraction: number;
+}
+
+interface IRackState {
+ id: number;
+ rackId: number;
+ rack?: IRack;
+ experimentId: number;
+ tick: number;
+ temperatureC: number;
+ inUseMemoryMb: number;
+ loadFraction: number;
+}
+
+interface IRoomState {
+ id: number;
+ roomId: number;
+ room?: IRoom;
+ experimentId: number;
+ tick: number;
+ temperatureC: number;
+ inUseMemoryMb: number;
+ loadFraction: number;
+}
+
+interface ITaskState {
+ id: number;
+ taskId: number;
+ task?: ITask;
+ experimentId: number;
+ tick: number;
+ flopsLeft: number;
+}
+
+// Generalization of a datacenter object
+type IDCObject = IRack | ICoolingItem | IPSU;
+
+interface IRack {
+ id: number;
+ objectType?: string;
+ name: string;
+ capacity: number;
+ powerCapacityW: number;
+ machines?: IMachine[];
+}
+
+interface ICoolingItem {
+ id: number;
+ objectType?: string;
+ energyConsumptionW: number;
+ type: string;
+ failureModelId: number;
+ failureModel?: IFailureModel;
+}
+
+interface IPSU {
+ id: number;
+ objectType?: string;
+ energyKwh: number;
+ type: string;
+ failureModelId: number;
+ failureModel?: IFailureModel;
+}
+
+// Machine
+interface IMachine {
+ id: number;
+ rackId: number;
+ position: number;
+ tags: string[];
+ cpuIds: number[];
+ cpus?: ICPU[];
+ gpuIds: number[];
+ gpus?: IGPU[];
+ memoryIds: number[];
+ memories?: IMemory[];
+ storageIds: number[];
+ storages?: IPermanentStorage[];
+}
+
+interface IProcessingUnit {
+ id: number;
+ manufacturer: string;
+ family: string;
+ generation: string;
+ model: string;
+ clockRateMhz: number;
+ numberOfCores: number;
+ energyConsumptionW: number;
+ failureModelId: number;
+ failureModel?: IFailureModel;
+}
+
+interface ICPU extends IProcessingUnit {
+
+}
+
+interface IGPU extends IProcessingUnit {
+
+}
+
+interface IStorageUnit {
+ id: number;
+ manufacturer: string;
+ family: string;
+ generation: string;
+ model: string;
+ speedMbPerS: number;
+ sizeMb: number;
+ energyConsumptionW: number;
+ failureModelId: number;
+ failureModel?: IFailureModel;
+}
+
+interface IMemory extends IStorageUnit {
+
+}
+
+interface IPermanentStorage extends IStorageUnit {
+
+}
+
+type INodeUnit = IProcessingUnit & IStorageUnit;
+
+interface IFailureModel {
+ id: number;
+ name: string;
+ rate: number;
+}
diff --git a/src/scripts/error404.entry.ts b/src/scripts/error404.entry.ts
new file mode 100644
index 00000000..07dc9ca0
--- /dev/null
+++ b/src/scripts/error404.entry.ts
@@ -0,0 +1,26 @@
+///<reference path="../../typings/globals/jquery/index.d.ts" />
+import * as $ from "jquery";
+
+
+$(document).ready(() => {
+ let text =
+ " oo oooo oo <br>" +
+ " oo oo oo oo <br>" +
+ " oo oo oo oo <br>" +
+ " oooooo oo oo oooooo <br>" +
+ " oo oo oo oo <br>" +
+ " oo oooo oo <br>";
+ let charList = text.split('');
+
+ let binary = "01001111011100000110010101101110010001000100001100100001";
+ let binaryIndex = 0;
+
+ for (let i = 0; i < charList.length; i++) {
+ if (charList[i] === "o") {
+ charList[i] = binary[binaryIndex];
+ binaryIndex++;
+ }
+ }
+
+ $(".code-block").html(charList.join(""));
+}); \ No newline at end of file
diff --git a/src/scripts/main.entry.ts b/src/scripts/main.entry.ts
new file mode 100644
index 00000000..c7d6ef90
--- /dev/null
+++ b/src/scripts/main.entry.ts
@@ -0,0 +1,69 @@
+///<reference path="../../typings/index.d.ts" />
+///<reference path="./views/mapview.ts" />
+import * as $ from "jquery";
+import {MapView} from "./views/mapview";
+import {APIController} from "./controllers/connection/api";
+window["$"] = $;
+require("jquery-mousewheel");
+window["jQuery"] = $;
+
+require("./user");
+
+
+$(document).ready(function () {
+ new Display(); //tslint:disable-line:no-unused-expression
+});
+
+
+/**
+ * Class responsible for launching the main view.
+ */
+class Display {
+ private stage: createjs.Stage;
+ private view: MapView;
+
+
+ /**
+ * Adjusts the canvas size to fit the window's initial dimensions (full expansion).
+ */
+ private static fitCanvasSize() {
+ let canvas = $("#main-canvas");
+ let parent = canvas.parent();
+ parent.height($(window).height() - 50);
+ canvas.attr("width", parent.width());
+ canvas.attr("height", parent.height());
+ }
+
+ constructor() {
+ // Check whether project has been selected before going to the app page
+ if (localStorage.getItem("simulationId") === null) {
+ window.location.replace("projects");
+ return;
+ }
+
+ Display.fitCanvasSize();
+ this.stage = new createjs.Stage("main-canvas");
+
+ new APIController((api: APIController) => {
+ api.getSimulation(parseInt(localStorage.getItem("simulationId")))
+ .then((simulationData: ISimulation) => {
+ if (simulationData.name !== "") {
+ document.title = simulationData.name + " | OpenDC";
+ }
+
+ api.getPathsBySimulation(simulationData.id)
+ .then((pathData: IPath[]) => {
+ simulationData.paths = pathData;
+ }).then(() => {
+ return api.getExperimentsBySimulation(simulationData.id);
+ }).then((experimentData: IExperiment[]) => {
+ $(".loading-overlay").hide();
+
+ simulationData.experiments = experimentData;
+ this.view = new MapView(simulationData, this.stage);
+ });
+ });
+ });
+
+ }
+}
diff --git a/src/scripts/profile.entry.ts b/src/scripts/profile.entry.ts
new file mode 100644
index 00000000..57c6b56c
--- /dev/null
+++ b/src/scripts/profile.entry.ts
@@ -0,0 +1,40 @@
+///<reference path="../../typings/index.d.ts" />
+import * as $ from "jquery";
+import {APIController} from "./controllers/connection/api";
+import {removeUserInfo} from "./user";
+window["jQuery"] = $;
+
+
+$(document).ready(() => {
+ let api = new APIController(() => {
+ });
+
+ $("#delete-account").on("click", () => {
+ let modalDialog = <any>$("#confirm-delete-account");
+
+ // Function called on delete confirmation
+ let callback = () => {
+ api.deleteUser(parseInt(localStorage.getItem("userId"))).then(() => {
+ removeUserInfo();
+ gapi.auth2.getAuthInstance().signOut().then(() => {
+ window.location.href = "/";
+ });
+ }, (reason: any) => {
+ modalDialog.find("button.confirm").off();
+ modalDialog.modal("hide");
+
+ let alert = $(".account-delete-alert");
+ alert.find("code").text(reason.code + ": " + reason.description);
+
+ alert.slideDown(200);
+
+ setTimeout(() => {
+ alert.slideUp(200);
+ }, 5000);
+ });
+ };
+
+ modalDialog.find("button.confirm").on("click", callback);
+ modalDialog.modal("show");
+ });
+});
diff --git a/src/scripts/projects.entry.ts b/src/scripts/projects.entry.ts
new file mode 100644
index 00000000..1ceb308b
--- /dev/null
+++ b/src/scripts/projects.entry.ts
@@ -0,0 +1,651 @@
+///<reference path="../../typings/globals/jquery/index.d.ts" />
+import * as $ from "jquery";
+import {APIController} from "./controllers/connection/api";
+import {Util} from "./util";
+window["jQuery"] = $;
+
+require("./user");
+
+
+$(document).ready(() => {
+ let api;
+ new APIController((apiInstance: APIController) => {
+ api = apiInstance;
+ api.getAuthorizationsByUser(parseInt(localStorage.getItem("userId"))).then((data: any) => {
+ let projectsController = new ProjectsController(data, api);
+ new WindowController(projectsController, api);
+ });
+ });
+});
+
+
+/**
+ * Controller class responsible for rendering the authorization list views and handling interactions with them.
+ */
+class ProjectsController {
+ public static authIconMap = {
+ "OWN": "glyphicon-home",
+ "EDIT": "glyphicon-pencil",
+ "VIEW": "glyphicon-eye-open"
+ };
+
+ public currentUserId: number;
+ public authorizations: IAuthorization[];
+ public authorizationsFiltered: IAuthorization[];
+ public windowController: WindowController;
+
+ private api: APIController;
+
+
+ /**
+ * 'Opens' a project, by putting the relevant simulation ID into local storage and referring to the app page.
+ *
+ * @param authorization The user's authorization belonging to the project to be opened
+ */
+ public static openProject(authorization: IAuthorization): void {
+ localStorage.setItem("simulationId", authorization.simulationId.toString());
+ localStorage.setItem("simulationAuthLevel", authorization.authorizationLevel);
+ window.location.href = "app";
+ }
+
+ /**
+ * Converts a list of authorizations into DOM objects, and adds them to the main list body of the page.
+ *
+ * @param list The list of authorizations to be displayed
+ */
+ public static populateList(list: IAuthorization[]): void {
+ let body = $(".project-list .list-body");
+ body.empty();
+
+ list.forEach((element: IAuthorization) => {
+ body.append(
+ $('<div class="project-row">').append(
+ $('<div>').text(element.simulation.name),
+ $('<div>').text(Util.formatDateTime(element.simulation.datetimeLastEditedParsed)),
+ $('<div>').append($('<span class="glyphicon">')
+ .addClass(this.authIconMap[element.authorizationLevel]),
+ Util.toSentenceCase(element.authorizationLevel)
+ )
+ ).attr("data-id", element.simulationId)
+ );
+ });
+ };
+
+ /**
+ * Filters an authorization list based on what authorization level is required.
+ *
+ * Leaves the original list intact.
+ *
+ * @param list The authorization list to be filtered
+ * @param ownedByUser Whether only authorizations should be included that are owned by the user, or whether only
+ * authorizations should be included that the user has no ownership over
+ * @returns {IAuthorization[]} A filtered list of authorizations
+ */
+ public static filterList(list: IAuthorization[], ownedByUser: boolean): IAuthorization[] {
+ let resultList: IAuthorization[] = [];
+
+ list.forEach((element: IAuthorization) => {
+ if (element.authorizationLevel === "OWN") {
+ if (ownedByUser) {
+ resultList.push(element);
+ }
+ } else {
+ if (!ownedByUser) {
+ resultList.push(element);
+ }
+ }
+ });
+
+ return resultList;
+ };
+
+ /**
+ * Activates a certain filter heading button, while deactivating the rest.
+ *
+ * @param target The event target to activate
+ */
+ private static activateFilterViewButton(target: JQuery): void {
+ target.addClass("active");
+ target.siblings().removeClass("active");
+ };
+
+ constructor(authorizations: IAuthorization[], api: APIController) {
+ this.currentUserId = parseInt(localStorage.getItem("userId"));
+ this.authorizations = authorizations;
+ this.authorizationsFiltered = authorizations;
+ this.api = api;
+
+ this.updateNoProjectsAlert();
+
+ this.handleFilterClick();
+
+ // Show a project view upon clicking on a simulation row
+ $("body").on("click", ".project-row", (event: JQueryEventObject) => {
+ this.displayProjectView($(event.target));
+ });
+ }
+
+ /**
+ * Update the list of authorizations, by fetching them from the server and reloading the list.
+ *
+ * Goes to the 'All Projects' page after this refresh.
+ */
+ public updateAuthorizations(): void {
+ this.api.getAuthorizationsByUser(this.currentUserId).then((data: any) => {
+ this.authorizations = data;
+ this.authorizationsFiltered = this.authorizations;
+
+ this.updateNoProjectsAlert();
+
+ this.goToAllProjects();
+ });
+ }
+
+ /**
+ * Show (or hide) the 'No projects here' alert in the list view, based on whether there are projects present.
+ */
+ private updateNoProjectsAlert(): void {
+ if (this.authorizationsFiltered.length === 0) {
+ $(".no-projects-alert").show();
+ $(".project-list").hide();
+ } else {
+ $(".no-projects-alert").hide();
+ $(".project-list").show();
+ }
+ }
+
+ /**
+ * Displays a project view with authorizations and entry buttons, inline within the table.
+ *
+ * @param target The element that was clicked on to launch this view
+ */
+ private displayProjectView(target: JQuery): void {
+ let closestRow = target.closest(".project-row");
+ let activeElement = $(".project-row.active");
+
+ // Disable previously selected row elements and remove any project-views, to have only one view open at a time
+ if (activeElement.length > 0) {
+ let view = $(".project-view").first();
+
+ view.slideUp(200, () => {
+ activeElement.removeClass("active");
+ view.remove();
+ });
+
+ if (closestRow.is(activeElement)) {
+ return;
+ }
+ }
+
+ let simulationId = parseInt(closestRow.attr("data-id"), 10);
+
+ // Generate a list of participants of this project
+ this.api.getAuthorizationsBySimulation(simulationId).then((data: any) => {
+ let simAuthorizations = data;
+ let participants = [];
+
+ Util.sortAuthorizations(simAuthorizations);
+
+ // For each participant of this simulation, include his/her name along with an icon of their authorization
+ // level in the list
+ simAuthorizations.forEach((authorization: IAuthorization) => {
+ let authorizationString = ' (<span class="glyphicon ' +
+ ProjectsController.authIconMap[authorization.authorizationLevel] + '"></span>)';
+ if (authorization.userId === this.currentUserId) {
+ participants.push(
+ 'You' + authorizationString
+ );
+ } else {
+ participants.push(
+ authorization.user.givenName + ' ' + authorization.user.familyName + authorizationString
+ );
+ }
+ });
+
+ // Generate a project view component with participants and relevant actions
+ let object = $('<div class="project-view">').append(
+ $('<div class="participants">').append(
+ $('<strong>').text("Participants"),
+ $('<div>').html(participants.join(", "))
+ ),
+ $('<div class="access-buttons">').append(
+ $('<div class="inline-btn edit">').text("Edit"),
+ $('<div class="inline-btn open">').text("Open")
+ )
+ ).hide();
+
+ closestRow.after(object);
+
+ // Hide the 'edit' button for non-owners and -editors
+ let currentAuth = this.authorizationsFiltered[closestRow.index(".project-row")];
+ if (currentAuth.authorizationLevel !== "OWN") {
+ $(".project-view .inline-btn.edit").hide();
+ }
+
+ object.find(".edit").click(() => {
+ this.windowController.showEditProjectWindow(simAuthorizations);
+ });
+
+ object.find(".open").click(() => {
+ ProjectsController.openProject(currentAuth);
+ });
+
+ closestRow.addClass("active");
+ object.slideDown(200);
+ });
+ }
+
+ /**
+ * Controls the filtered authorization list, based on clicks from the side menu.
+ */
+ private handleFilterClick(): void {
+ $(".all-projects").on("click", () => {
+ this.goToAllProjects();
+ });
+
+ $(".my-projects").on("click", () => {
+ this.goToMyProjects();
+ });
+
+ $(".shared-projects").on("click", () => {
+ this.goToSharedProjects();
+ });
+
+ this.goToAllProjects();
+ }
+
+ /**
+ * Show a list containing all projects (regardless of the authorization level the user has over them).
+ */
+ private goToAllProjects(): void {
+ this.authorizationsFiltered = this.authorizations;
+ ProjectsController.populateList(this.authorizations);
+ this.updateNoProjectsAlert();
+
+ ProjectsController.activateFilterViewButton($(".all-projects"));
+ }
+
+ /**
+ * Show a list containing only projects that the user owns.
+ */
+ private goToMyProjects(): void {
+ this.authorizationsFiltered = ProjectsController.filterList(this.authorizations, true);
+ ProjectsController.populateList(this.authorizationsFiltered);
+ this.updateNoProjectsAlert();
+
+ ProjectsController.activateFilterViewButton($(".my-projects"));
+ }
+
+ /**
+ * Show a list containing only projects that the user does not own (but can edit or view).
+ */
+ private goToSharedProjects(): void {
+ this.authorizationsFiltered = ProjectsController.filterList(this.authorizations, false);
+ ProjectsController.populateList(this.authorizationsFiltered);
+ this.updateNoProjectsAlert();
+
+ ProjectsController.activateFilterViewButton($(".shared-projects"));
+ }
+}
+
+
+/**
+ * Controller class responsible for rendering the project add/edit window and handle user interaction with it.
+ */
+class WindowController {
+ private projectsController: ProjectsController;
+ private windowOverlay: JQuery;
+ private window: JQuery;
+ private table: JQuery;
+ private closeCallback: () => any;
+ private simAuthorizations: IAuthorization[];
+ private simulationId: number;
+ private editMode: boolean;
+ private api: APIController;
+
+
+ constructor(projectsController: ProjectsController, api: APIController) {
+ this.projectsController = projectsController;
+ this.windowOverlay = $(".window-overlay");
+ this.window = $(".projects-window");
+ this.table = $(".participants-table");
+ this.projectsController.windowController = this;
+ this.simAuthorizations = [];
+ this.editMode = false;
+ this.api = api;
+
+ $(".window-footer .btn").hide();
+
+ $(".participant-add-form").submit((event: JQueryEventObject) => {
+ event.preventDefault();
+ $(".participant-add-form .btn").trigger("click");
+ });
+
+ $(".project-name-form").submit((event: JQueryEventObject) => {
+ event.preventDefault();
+ $(".project-name-form .btn").trigger("click");
+ });
+
+ // Clean-up actions to occur after every window-close
+ this.closeCallback = () => {
+ this.table.empty();
+ $(".project-name-form .btn").off();
+ $(".participant-add-form .btn").off();
+ $(".window-footer .btn").hide().off();
+ $(".participant-email-alert").hide();
+ $(".participant-level div").removeClass("active").off();
+ this.table.off("click", ".participant-remove div");
+
+ $(".project-name-form input").val("");
+ $(".participant-add-form input").val("");
+
+ if (this.editMode) {
+ this.projectsController.updateAuthorizations();
+ }
+ };
+
+ $(".new-project-btn").click(() => {
+ this.showAddProjectWindow();
+ });
+
+ // Stop click events on the window from closing it indirectly
+ this.window.click((event: JQueryEventObject) => {
+ event.stopPropagation();
+ });
+
+ $(".window-close, .window-overlay").click(() => {
+ this.closeWindow();
+ });
+ }
+
+ /**
+ * Displays a window for project edits (used for adding participants and changing the name).
+ *
+ * @param authorizations The authorizations of the simulation project to be edited.
+ */
+ public showEditProjectWindow(authorizations: IAuthorization[]): void {
+ this.editMode = true;
+ this.simAuthorizations = [];
+ this.simulationId = authorizations[0].simulation.id;
+
+ // Filter out the user's authorization from the authorization list (not to be displayed in the list)
+ authorizations.forEach((authorization: IAuthorization) => {
+ if (authorization.userId !== this.projectsController.currentUserId) {
+ this.simAuthorizations.push(authorization);
+ }
+ });
+
+ $(".window .window-heading").text("Edit project");
+
+ $(".project-name-form input").val(authorizations[0].simulation.name);
+
+ $(".project-name-form .btn").css("display", "inline-block").click(() => {
+ let nameInput = $(".project-name-form input").val();
+ if (nameInput !== "") {
+ authorizations[0].simulation.name = nameInput;
+ this.api.updateSimulation(authorizations[0].simulation);
+ }
+ });
+
+ $(".project-open-btn").show().click(() => {
+ ProjectsController.openProject({
+ userId: this.projectsController.currentUserId,
+ simulationId: this.simulationId,
+ authorizationLevel: "OWN"
+ });
+ });
+
+ $(".project-delete-btn").show().click(() => {
+ this.api.deleteSimulation(authorizations[0].simulationId).then(() => {
+ this.projectsController.updateAuthorizations();
+ this.closeWindow();
+ });
+ });
+
+ $(".participant-add-form .btn").click(() => {
+ this.handleParticipantAdd((userId: number) => {
+ this.api.addAuthorization({
+ userId: userId,
+ simulationId: authorizations[0].simulationId,
+ authorizationLevel: "VIEW"
+ });
+ });
+ });
+
+ this.table.on("click", ".participant-level div", (event: JQueryEventObject) => {
+ this.handleParticipantLevelChange(event, (authorization: IAuthorization) => {
+ this.api.updateAuthorization(authorization);
+ });
+ });
+
+ this.table.on("click", ".participant-remove div", (event: JQueryEventObject) => {
+ this.handleParticipantDelete(event, (authorization) => {
+ this.api.deleteAuthorization(authorization);
+ });
+ });
+
+ this.populateParticipantList();
+
+ this.windowOverlay.fadeIn(200);
+ }
+
+ /**
+ * Shows a window to be used for creating a new project.
+ */
+ public showAddProjectWindow(): void {
+ this.editMode = false;
+ this.simAuthorizations = [];
+
+ $(".project-name-form .btn").hide();
+
+ $(".window .window-heading").text("Create a project");
+
+ $(".project-create-open-btn").show().click(() => {
+ if ($(".project-name-form input").val() === "") {
+ this.showAlert(".project-name-alert");
+ return;
+ }
+ this.createSimulation((simulationId: number) => {
+ ProjectsController.openProject({
+ userId: this.projectsController.currentUserId,
+ simulationId,
+ authorizationLevel: "OWN"
+ });
+ });
+ });
+
+ $(".project-create-btn").show().click(() => {
+ if ($(".project-name-form input").val() === "") {
+ this.showAlert(".project-name-alert");
+ return;
+ }
+ this.createSimulation(() => {
+ this.projectsController.updateAuthorizations();
+ this.closeWindow();
+ });
+ });
+
+ $(".project-cancel-btn").show().click(() => {
+ this.closeWindow();
+ });
+
+ this.table.empty();
+
+ $(".project-name-form input").val("");
+ $(".participant-add-form input").val("");
+
+ $(".participant-add-form .btn").click(() => {
+ this.handleParticipantAdd(() => {
+ });
+ });
+
+ this.table.on("click", ".participant-level div", (event: JQueryEventObject) => {
+ this.handleParticipantLevelChange(event, () => {
+ });
+ });
+
+ this.table.on("click", ".participant-remove div", (event: JQueryEventObject) => {
+ this.handleParticipantDelete(event, () => {
+ });
+ });
+
+ this.windowOverlay.fadeIn(200);
+ }
+
+ /**
+ * Creates a new simulation with the current name input and all currently present authorizations added in the
+ * project 'add' window.
+ *
+ * @param callback The function to be called when this operation has succeeded
+ */
+ private createSimulation(callback: (simulationId: number) => any): void {
+ this.api.addSimulation({
+ id: -1,
+ name: $(".project-name-form input").val(),
+ datetimeCreated: Util.getCurrentDateTime(),
+ datetimeLastEdited: Util.getCurrentDateTime()
+ }).then((data: any) => {
+ let asyncCounter = this.simAuthorizations.length;
+ this.simAuthorizations.forEach((authorization: IAuthorization) => {
+ authorization.simulationId = data.id;
+ this.api.addAuthorization(authorization).then((data: any) => {
+ asyncCounter--;
+
+ if (asyncCounter <= 0) {
+ callback(data.id);
+ }
+ });
+ });
+ if (this.simAuthorizations.length === 0) {
+ callback(data.id);
+ }
+ });
+ }
+
+ /**
+ * Displays an alert of the given class name, to disappear again after a certain pre-defined timeout.
+ *
+ * @param name A selector that uniquely identifies the alert body to be shown.
+ */
+ private showAlert(name): void {
+ let alert = $(name);
+ alert.slideDown(200);
+
+ setTimeout(() => {
+ alert.slideUp(200);
+ }, 5000);
+ }
+
+ /**
+ * Closes the window with a transition, and calls the relevant callback after that transition has ended.
+ */
+ private closeWindow(): void {
+ this.windowOverlay.fadeOut(200, () => {
+ this.closeCallback();
+ });
+ }
+
+ /**
+ * Handles the click on an authorization icon in the project window authorization list.
+ *
+ * @param event The JQuery click event
+ * @param callback The function to be called after the authorization was changed
+ */
+ private handleParticipantLevelChange(event: JQueryEventObject,
+ callback: (authorization: IAuthorization) => any): void {
+ $(event.target).closest(".participant-level").find("div").removeClass("active");
+ $(event.target).addClass("active");
+
+ let affectedRow = $(event.target).closest(".participant-row");
+
+ for (let level in ProjectsController.authIconMap) {
+ if (!ProjectsController.authIconMap.hasOwnProperty(level)) {
+ continue;
+ }
+ if ($(event.target).is("." + ProjectsController.authIconMap[level])) {
+ this.simAuthorizations[affectedRow.index()].authorizationLevel = level;
+ callback(this.simAuthorizations[affectedRow.index()]);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handles the event where a user seeks to add a participant.
+ *
+ * @param callback The function to be called if the participant could be found and can be added.
+ */
+ private handleParticipantAdd(callback: (userId: number) => any): void {
+ let inputForm = $(".participant-add-form input");
+ this.api.getUserByEmail(inputForm.val()).then((data: any) => {
+ let insert = true;
+ for (let i = 0; i < this.simAuthorizations.length; i++) {
+ if (this.simAuthorizations[i].userId === data.id) {
+ insert = false;
+ }
+ }
+
+ let simulationId = this.editMode ? this.simulationId : -1;
+ if (data.id !== this.projectsController.currentUserId && insert) {
+ this.simAuthorizations.push({
+ userId: data.id,
+ user: data,
+ simulationId: simulationId,
+ authorizationLevel: "VIEW"
+ });
+ callback(data.id);
+ Util.sortAuthorizations(this.simAuthorizations);
+ this.populateParticipantList();
+ }
+
+ // Clear input field after submission
+ inputForm.val("");
+ }, (reason: any) => {
+ if (reason.code === 404) {
+ this.showAlert(".participant-email-alert");
+ }
+ });
+ }
+
+ /**
+ * Handles click events on the 'remove' icon next to each participant.
+ *
+ * @param event The JQuery click event
+ * @param callback The function to be executed on removal of the participant from the internal list
+ */
+ private handleParticipantDelete(event: JQueryEventObject, callback: (authorization: IAuthorization) => any): void {
+ let affectedRow = $(event.target).closest(".participant-row");
+ let index = affectedRow.index();
+ let authorization = this.simAuthorizations[index];
+ this.simAuthorizations.splice(index, 1);
+ this.populateParticipantList();
+ callback(authorization);
+ }
+
+ /**
+ * Populates the list of participants in the project edit window with all current authorizations.
+ */
+ private populateParticipantList(): void {
+ this.table.empty();
+
+ this.simAuthorizations.forEach((authorization: IAuthorization) => {
+ this.table.append(
+ '<div class="participant-row">' +
+ ' <div class="participant-name">' + authorization.user.givenName + ' ' +
+ authorization.user.familyName + '</div>' +
+ ' <div class="participant-level">' +
+ ' <div class="participant-level-view glyphicon glyphicon-eye-open ' +
+ (authorization.authorizationLevel === "VIEW" ? 'active' : '') + '"></div>' +
+ ' <div class="participant-level-edit glyphicon glyphicon-pencil ' +
+ (authorization.authorizationLevel === "EDIT" ? 'active' : '') + '"></div>' +
+ ' <div class="participant-level-own glyphicon glyphicon-home ' +
+ (authorization.authorizationLevel === "OWN" ? 'active' : '') + '"></div>' +
+ ' </div>' +
+ ' <div class="participant-remove">' +
+ ' <div class="glyphicon glyphicon-remove"></div>' +
+ ' </div>' +
+ '</div>'
+ );
+ });
+ }
+}
diff --git a/src/scripts/serverconnection.ts b/src/scripts/serverconnection.ts
new file mode 100644
index 00000000..c7f6e598
--- /dev/null
+++ b/src/scripts/serverconnection.ts
@@ -0,0 +1,59 @@
+import {SocketController} from "./controllers/connection/socket";
+
+
+export class ServerConnection {
+ private static _socketControllerInstance: SocketController;
+
+
+ public static connect(onConnect: () => any): void {
+ this._socketControllerInstance = new SocketController(onConnect);
+ }
+
+ public static send(request: IRequest): Promise<any> {
+ return new Promise((resolve, reject) => {
+ let checkUnimplemented = ServerConnection.interceptUnimplementedEndpoint(request);
+ if (checkUnimplemented) {
+ resolve(checkUnimplemented.content);
+ return;
+ }
+
+ this._socketControllerInstance.sendRequest(request, (response: IResponse) => {
+ if (response.status.code === 200) {
+ ServerConnection.convertFlatToNestedPositionData(response.content, resolve);
+ } else {
+ reject(response.status);
+ }
+ });
+ })
+ }
+
+ public static convertFlatToNestedPositionData(responseContent, resolve): void {
+ let nestPositionCoords = (content: any) => {
+ if (content["positionX"] !== undefined) {
+ content["position"] = {
+ x: content["positionX"],
+ y: content["positionY"]
+ };
+ }
+ };
+
+ if (responseContent instanceof Array) {
+ responseContent.forEach(nestPositionCoords);
+ } else {
+ nestPositionCoords(responseContent);
+ }
+
+ resolve(responseContent);
+ }
+
+ /**
+ * Intercepts endpoints that are still unimplemented and responds with mock data.
+ *
+ * @param request The request
+ * @returns {any} A response, or null if the endpoint is not on the list of unimplemented ones.
+ */
+ public static interceptUnimplementedEndpoint(request: IRequest): IResponse {
+ // Endpoints that are unimplemented can be intercepted here
+ return null;
+ }
+} \ No newline at end of file
diff --git a/src/scripts/splash.entry.ts b/src/scripts/splash.entry.ts
new file mode 100644
index 00000000..c1be1c28
--- /dev/null
+++ b/src/scripts/splash.entry.ts
@@ -0,0 +1,160 @@
+///<reference path="../../typings/index.d.ts" />
+///<reference path="./definitions.ts" />
+import * as $ from "jquery";
+import {APIController} from "./controllers/connection/api";
+window["jQuery"] = $;
+require("jquery.easing");
+
+
+// Variable to check whether user actively logged in by clicking the login button
+let hasClickedLogin = false;
+
+
+$(document).ready(() => {
+ /**
+ * jQuery for page scrolling feature
+ */
+ $('a.page-scroll').bind('click', function (event) {
+ let $anchor = $(this);
+ $('html, body').stop().animate({
+ scrollTop: $($anchor.attr('href')).offset().top
+ }, 1000, 'easeInOutExpo', () => {
+ if ($anchor.attr('href') === "#page-top") {
+ location.hash = '';
+ } else {
+ location.hash = $anchor.attr('href');
+ }
+ });
+ event.preventDefault();
+ });
+
+ let checkScrollState = () => {
+ const startY = 100;
+
+ if ($(window).scrollTop() > startY || window.innerWidth < 768) {
+ $('.navbar').removeClass("navbar-transparent");
+ } else {
+ $('.navbar').addClass("navbar-transparent");
+ }
+ };
+
+ $(window).on("scroll load resize", function () {
+ checkScrollState();
+ });
+
+ checkScrollState();
+
+ let googleSigninBtn = $("#google-signin");
+ googleSigninBtn.click(() => {
+ hasClickedLogin = true;
+ });
+
+ /**
+ * Display appropriate user buttons
+ */
+ if (localStorage.getItem("googleToken") !== null) {
+ googleSigninBtn.hide();
+ $(".navbar .logged-in").css("display", "inline-block");
+ $(".logged-in .sign-out").click(() => {
+ let auth2 = gapi.auth2.getAuthInstance();
+
+ auth2.signOut().then(() => {
+ // Remove session storage items
+ localStorage.removeItem("googleToken");
+ localStorage.removeItem("googleTokenExpiration");
+ localStorage.removeItem("googleName");
+ localStorage.removeItem("googleEmail");
+ localStorage.removeItem("userId");
+ localStorage.removeItem("simulationId");
+
+ location.reload();
+ });
+ });
+
+ // Check whether Google auth. token has expired and signin again if necessary
+ let currentTime = (new Date()).getTime();
+ if (parseInt(localStorage.getItem("googleTokenExpiration")) - currentTime <= 0) {
+ gapi.auth2.getAuthInstance().signIn().then(() => {
+ let authResponse = gapi.auth2.getAuthInstance().currentUser.get().getAuthResponse();
+ localStorage.setItem("googleToken", authResponse.id_token);
+ let expirationTime = (new Date()).getTime() / 1000 + parseInt(authResponse.expires_in) - 5;
+ localStorage.setItem("googleTokenExpiration", expirationTime.toString());
+ });
+ }
+ }
+});
+
+/**
+ * Google signin button
+ */
+window["renderButton"] = () => {
+ gapi.signin2.render('google-signin', {
+ 'scope': 'profile email',
+ 'width': 100,
+ 'height': 30,
+ 'longtitle': false,
+ 'theme': 'dark',
+ 'onsuccess': (googleUser) => {
+ let api;
+ new APIController((apiInstance: APIController) => {
+ api = apiInstance;
+ let email = googleUser.getBasicProfile().getEmail();
+
+ let getUser = (userId: number) => {
+ let reload = true;
+ if (localStorage.getItem("userId") !== null) {
+ reload = false;
+ }
+
+ localStorage.setItem("userId", userId.toString());
+
+ // Redirect to the projects page
+ if (hasClickedLogin) {
+ window.location.href = "projects";
+ } else if (reload) {
+ window.location.reload();
+ }
+
+ };
+
+ // Send the token to the server
+ let id_token = googleUser.getAuthResponse().id_token;
+ // Calculate token expiration time (in seconds since epoch)
+ let expirationTime = (new Date()).getTime() / 1000 + googleUser.getAuthResponse().expires_in - 5;
+
+ $.post('https://opendc.ewi.tudelft.nl/tokensignin', {
+ idtoken: id_token
+ }, (data: any) => {
+ // Save user information in session storage for later use on other pages
+ localStorage.setItem("googleToken", id_token);
+ localStorage.setItem("googleTokenExpiration", expirationTime.toString());
+ localStorage.setItem("googleName", googleUser.getBasicProfile().getGivenName() + " " +
+ googleUser.getBasicProfile().getFamilyName());
+ localStorage.setItem("googleEmail", email);
+
+ if (data.isNewUser === true) {
+ api.addUser({
+ id: -1,
+ email,
+ googleId: googleUser.getBasicProfile().getId(),
+ givenName: googleUser.getBasicProfile().getGivenName(),
+ familyName: googleUser.getBasicProfile().getFamilyName()
+ }).then((userData: any) => {
+ getUser(userData.id);
+ });
+ } else {
+ getUser(data.userId);
+ }
+ });
+ });
+ },
+ 'onfailure': () => {
+ console.log("Oops, something went wrong with your Google signin... Try again?")
+ }
+ });
+};
+
+// Set the language of the GAuth button to be English
+window["___gcfg"] = {
+ lang: 'en'
+};
diff --git a/src/scripts/tests/util.spec.ts b/src/scripts/tests/util.spec.ts
new file mode 100644
index 00000000..74d62dfa
--- /dev/null
+++ b/src/scripts/tests/util.spec.ts
@@ -0,0 +1,326 @@
+///<reference path="../util.ts" />
+///<reference path="../../../typings/globals/jasmine/index.d.ts" />
+import {Util} from "../util";
+
+
+class TestUtils {
+ /**
+ * Checks whether the two (three-dimensional) wall lists are equivalent in content.
+ *
+ * This is a set-compare method, meaning that the order of the elements does not matter, but that they are present
+ * in both arrays.
+ *
+ * Example of such a list: [[[1, 1], [2, 1]], [[3, 1], [0, 0]]]
+ *
+ * @param list1 The first list
+ * @param list2 The second list
+ */
+ public static wallListEquals(list1: IRoomWall[], list2: IRoomWall[]): void {
+ let current, found, counter;
+
+ counter = 0;
+ while (list1.length > 0) {
+ current = list1.pop();
+ found = false;
+ list2.forEach((e: IRoomWall) => {
+ if (current.startPos[0] === e.startPos[0] && current.startPos[1] === e.startPos[1] &&
+ current.horizontal === e.horizontal && current.length === e.length) {
+ counter++;
+ found = true;
+ }
+ });
+ if (!found) {
+ fail();
+ }
+ }
+ expect(list2.length).toEqual(counter);
+ }
+
+ /**
+ * Does the same as wallList3DEquals, only for two lists of tiles.
+ *
+ * @param expected
+ * @param actual
+ */
+ public static positionListEquals(expected: IGridPosition[], actual: IGridPosition[]): void {
+ let current, found;
+ let counter = 0;
+
+ while (expected.length > 0) {
+ current = expected.pop();
+ found = false;
+ actual.forEach((e) => {
+ if (current.x === e.x && current.y === e.y) {
+ counter++;
+ found = true;
+ }
+ });
+ if (!found) {
+ fail();
+ }
+ }
+
+ expect(actual.length).toEqual(counter);
+ }
+
+ public static boundsEquals(actual: IBounds, expected: IBounds): void {
+ expect(actual.min[0]).toBe(expected.min[0]);
+ expect(actual.min[1]).toBe(expected.min[1]);
+ expect(actual.center[0]).toBe(expected.center[0]);
+ expect(actual.center[1]).toBe(expected.center[1]);
+ expect(actual.max[0]).toBe(expected.max[0]);
+ expect(actual.max[1]).toBe(expected.max[1]);
+ }
+}
+
+describe("Deriving wall locations", () => {
+ it("should generate walls around a single tile", () => {
+ let room = {
+ id: -1,
+ datacenterId: -1,
+ name: "testroom",
+ roomType: "none",
+ tiles: [{
+ id: -1,
+ roomId: -1,
+ position: {x: 1, y: 1}
+ }]
+ };
+
+ let result = Util.deriveWallLocations([
+ room
+ ]);
+ let expected: IRoomWall[] = [
+ {
+ startPos: [1, 1],
+ horizontal: false,
+ length: 1
+ },
+ {
+ startPos: [2, 1],
+ horizontal: false,
+ length: 1
+ },
+ {
+ startPos: [1, 1],
+ horizontal: true,
+ length: 1
+ },
+ {
+ startPos: [1, 2],
+ horizontal: true,
+ length: 1
+ }
+ ];
+
+ TestUtils.wallListEquals(expected, result);
+ }
+ );
+
+ it("should generate walls around two tiles connected by an edge", () => {
+ let room = {
+ id: -1,
+ datacenterId: -1,
+ name: "testroom",
+ roomType: "none",
+ tiles: [
+ {
+ id: -1,
+ roomId: -1,
+ position: {x: 1, y: 1}
+ }, {
+ id: -1,
+ roomId: -1,
+ position: {x: 1, y: 2}
+ }
+ ]
+ };
+
+ let result = Util.deriveWallLocations([
+ room
+ ]);
+ let expected: IRoomWall[] = [
+ {
+ startPos: [1, 1],
+ horizontal: false,
+ length: 2
+ },
+ {
+ startPos: [2, 1],
+ horizontal: false,
+ length: 2
+ },
+ {
+ startPos: [1, 1],
+ horizontal: true,
+ length: 1
+ },
+ {
+ startPos: [1, 3],
+ horizontal: true,
+ length: 1
+ }
+ ];
+
+ TestUtils.wallListEquals(expected, result);
+ }
+ );
+
+ it("should generate walls around two independent rooms with one tile each", () => {
+ let room1 = {
+ id: -1,
+ datacenterId: -1,
+ name: "testroom",
+ roomType: "none",
+ tiles: [
+ {
+ id: -1,
+ roomId: -1,
+ position: {x: 1, y: 1}
+ }
+ ]
+ };
+
+ let room2 = {
+ id: -1,
+ datacenterId: -1,
+ name: "testroom",
+ roomType: "none",
+ tiles: [{
+ id: -1,
+ roomId: -1,
+ position: {x: 1, y: 3}
+ }
+ ]
+ };
+
+ let result = Util.deriveWallLocations([
+ room1, room2
+ ]);
+ let expected: IRoomWall[] = [
+ {
+ startPos: [1, 1],
+ horizontal: false,
+ length: 1
+ },
+ {
+ startPos: [1, 3],
+ horizontal: false,
+ length: 1
+ },
+ {
+ startPos: [2, 1],
+ horizontal: false,
+ length: 1
+ },
+ {
+ startPos: [2, 3],
+ horizontal: false,
+ length: 1
+ },
+ {
+ startPos: [1, 1],
+ horizontal: true,
+ length: 1
+ },
+ {
+ startPos: [1, 2],
+ horizontal: true,
+ length: 1
+ },
+ {
+ startPos: [1, 3],
+ horizontal: true,
+ length: 1
+ },
+ {
+ startPos: [1, 4],
+ horizontal: true,
+ length: 1
+ }
+ ];
+
+ TestUtils.wallListEquals(expected, result);
+ }
+ );
+});
+
+describe("Deriving valid next tile positions", () => {
+ it("should derive correctly 4 valid tile positions around 1 selected tile with no other rooms", () => {
+ let result = Util.deriveValidNextTilePositions([], [{
+ id: -1,
+ roomId: -1,
+ position: {x: 1, y: 1}
+ }]);
+ let expected = [
+ {x: 1, y: 0}, {x: 2, y: 1}, {x: 1, y: 2}, {x: 0, y: 1}
+ ];
+
+ TestUtils.positionListEquals(expected, result);
+ });
+
+ it("should derive correctly 6 valid tile positions around 2 selected tiles with no other rooms", () => {
+ let result = Util.deriveValidNextTilePositions([], [{
+ id: -1,
+ roomId: -1,
+ position: {x: 1, y: 1}
+ }, {
+ id: -1,
+ roomId: -1,
+ position: {x: 2, y: 1}
+ }]);
+ let expected = [
+ {x: 1, y: 0}, {x: 2, y: 0}, {x: 3, y: 1}, {x: 1, y: 2}, {x: 2, y: 2}, {x: 0, y: 1}
+ ];
+
+ TestUtils.positionListEquals(expected, result);
+ });
+
+ it("should derive correctly 3 valid tile positions around 1 selected tiles with 1 adjacent room", () => {
+ let room = {
+ id: -1,
+ datacenterId: -1,
+ name: "testroom",
+ roomType: "none",
+ tiles: [{
+ id: -1,
+ roomId: -1,
+ position: {x: 0, y: 1}
+ }]
+ };
+ let result = Util.deriveValidNextTilePositions([room], [{
+ id: -1,
+ roomId: -1,
+ position: {x: 1, y: 1}
+ }]);
+ let expected = [
+ {x: 1, y: 0}, {x: 2, y: 1}, {x: 1, y: 2}
+ ];
+
+ TestUtils.positionListEquals(expected, result);
+ });
+});
+
+describe("Calculating the bounds and average point of a list of rooms", () => {
+ it("should calculate correctly the bounds of a 1-tile room", () => {
+ let room = {
+ id: -1,
+ datacenterId: -1,
+ name: "testroom",
+ roomType: "none",
+ tiles: [{
+ id: -1,
+ roomId: -1,
+ position: {x: 1, y: 1}
+ }]
+ };
+ let result = Util.calculateRoomListBounds([room]);
+ let expected = {
+ min: [1, 1],
+ center: [1.5, 1.5],
+ max: [2, 2]
+ };
+
+ TestUtils.boundsEquals(result, expected);
+ });
+});
diff --git a/src/scripts/user.ts b/src/scripts/user.ts
new file mode 100644
index 00000000..dda2dcab
--- /dev/null
+++ b/src/scripts/user.ts
@@ -0,0 +1,76 @@
+///<reference path="../../typings/index.d.ts" />
+import * as $ from "jquery";
+
+
+const LOCAL_MODE = (document.location.hostname === "localhost");
+
+// Redirect the user to the splash page, if not signed in
+if (!LOCAL_MODE && localStorage.getItem("googleToken") === null) {
+ window.location.replace("/");
+}
+
+// Fill session storage with mock data during LOCAL_MODE
+if (LOCAL_MODE) {
+ localStorage.setItem("googleToken", "");
+ localStorage.setItem("googleTokenExpiration", "2000000000");
+ localStorage.setItem("googleName", "John Doe");
+ localStorage.setItem("googleEmail", "john@doe.com");
+ localStorage.setItem("userId", "2");
+ localStorage.setItem("simulationId", "1");
+ localStorage.setItem("simulationAuthLevel", "OWN");
+}
+
+// Set the username in the navbar
+$("nav .user .username").text(localStorage.getItem("googleName"));
+
+
+// Set the language of the GAuth button to be English
+window["___gcfg"] = {
+ lang: 'en'
+};
+
+/**
+ * Google signin button
+ */
+window["gapiSigninButton"] = () => {
+ gapi.signin2.render('google-signin', {
+ 'scope': 'profile email',
+ 'onsuccess': (googleUser) => {
+ let auth2 = gapi.auth2.getAuthInstance();
+
+ // Handle signout click
+ $("nav .user .sign-out").click(() => {
+ removeUserInfo();
+ auth2.signOut().then(() => {
+ window.location.href = "/";
+ });
+ });
+
+ // Check if the token has expired
+ let currentTime = (new Date()).getTime() / 1000;
+
+ if (parseInt(localStorage.getItem("googleTokenExpiration")) - currentTime <= 0) {
+ auth2.signIn().then(() => {
+ localStorage.setItem("googleToken", googleUser.getAuthResponse().id_token);
+ let expirationTime = (new Date()).getTime() / 1000 + parseInt(googleUser.getAuthResponse().expires_in) - 5;
+ localStorage.setItem("googleTokenExpiration", expirationTime.toString());
+ });
+ }
+ },
+ 'onfailure': () => {
+ window.location.href = "/";
+ console.log("Oops, something went wrong with your Google signin... Try again?")
+ }
+ });
+};
+
+
+export function removeUserInfo() {
+ // Remove session storage items
+ localStorage.removeItem("googleToken");
+ localStorage.removeItem("googleTokenExpiration");
+ localStorage.removeItem("googleName");
+ localStorage.removeItem("googleEmail");
+ localStorage.removeItem("userId");
+ localStorage.removeItem("simulationId");
+} \ No newline at end of file
diff --git a/src/scripts/util.ts b/src/scripts/util.ts
new file mode 100644
index 00000000..74bdb710
--- /dev/null
+++ b/src/scripts/util.ts
@@ -0,0 +1,600 @@
+///<reference path="definitions.ts" />
+import {Colors} from "./colors";
+
+
+export enum IntensityLevel {
+ LOW,
+ MID_LOW,
+ MID_HIGH,
+ HIGH
+}
+
+
+export class Util {
+ private static authorizationLevels = [
+ "OWN", "EDIT", "VIEW"
+ ];
+
+
+ /**
+ * Derives the wall locations given a list of rooms.
+ *
+ * Does so by computing an outline around all tiles in the rooms.
+ */
+ public static deriveWallLocations(rooms: IRoom[]): IRoomWall[] {
+ let verticalWalls = {};
+ let horizontalWalls = {};
+ let doInsert;
+ rooms.forEach((room: IRoom) => {
+ room.tiles.forEach((tile: ITile) => {
+ let x = tile.position.x, y = tile.position.y;
+ for (let dX = -1; dX <= 1; dX++) {
+ for (let dY = -1; dY <= 1; dY++) {
+ if (Math.abs(dX) === Math.abs(dY)) {
+ continue;
+ }
+
+ doInsert = true;
+ room.tiles.forEach((otherTile: ITile) => {
+ if (otherTile.position.x === x + dX && otherTile.position.y === y + dY) {
+ doInsert = false;
+ }
+ });
+
+ if (doInsert) {
+ if (dX === -1) {
+ if (verticalWalls[x] === undefined) {
+ verticalWalls[x] = [];
+ }
+ if (verticalWalls[x].indexOf(y) === -1) {
+ verticalWalls[x].push(y);
+ }
+ } else if (dX === 1) {
+ if (verticalWalls[x + 1] === undefined) {
+ verticalWalls[x + 1] = [];
+ }
+ if (verticalWalls[x + 1].indexOf(y) === -1) {
+ verticalWalls[x + 1].push(y);
+ }
+ } else if (dY === -1) {
+ if (horizontalWalls[y] === undefined) {
+ horizontalWalls[y] = [];
+ }
+ if (horizontalWalls[y].indexOf(x) === -1) {
+ horizontalWalls[y].push(x);
+ }
+ } else if (dY === 1) {
+ if (horizontalWalls[y + 1] === undefined) {
+ horizontalWalls[y + 1] = [];
+ }
+ if (horizontalWalls[y + 1].indexOf(x) === -1) {
+ horizontalWalls[y + 1].push(x);
+ }
+ }
+ }
+ }
+ }
+ });
+ });
+
+ let result: IRoomWall[] = [];
+ let walls = [verticalWalls, horizontalWalls];
+ for (let i = 0; i < 2; i++) {
+ let wallList = walls[i];
+ for (let a in wallList) {
+ if (!wallList.hasOwnProperty(a)) {
+ return;
+ }
+
+ wallList[a].sort((a: number, b: number) => {
+ return a - b;
+ });
+
+ let startPos = wallList[a][0];
+ let positionArray = (i === 1 ? <number[]>[startPos, parseInt(a)] : <number[]>[parseInt(a), startPos]);
+
+ if (wallList[a].length === 1) {
+ result.push({
+ startPos: positionArray,
+ horizontal: i === 1,
+ length: 1
+ });
+ } else {
+ let consecutiveCount = 1;
+ for (let b = 0; b < wallList[a].length - 1; b++) {
+ if (b + 1 === wallList[a].length - 1) {
+ if (wallList[a][b + 1] - wallList[a][b] > 1) {
+ result.push({
+ startPos: (i === 1 ? <number[]>[startPos, parseInt(a)] : <number[]>[parseInt(a), startPos]),
+ horizontal: i === 1,
+ length: consecutiveCount
+ });
+ consecutiveCount = 0;
+ startPos = wallList[a][b + 1];
+ }
+ result.push({
+ startPos: (i === 1 ? <number[]>[startPos, parseInt(a)] : <number[]>[parseInt(a), startPos]),
+ horizontal: i === 1,
+ length: consecutiveCount + 1
+ });
+ break;
+ } else if (wallList[a][b + 1] - wallList[a][b] > 1) {
+ result.push({
+ startPos: (i === 1 ? <number[]>[startPos, parseInt(a)] : <number[]>[parseInt(a), startPos]),
+ horizontal: i === 1,
+ length: consecutiveCount
+ });
+ startPos = wallList[a][b + 1];
+ consecutiveCount = 0;
+ }
+ consecutiveCount++;
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Generates a list of all valid tile positions around the currently selected room under construction.
+ *
+ * @param rooms The rooms that already exist in the model
+ * @param selectedTiles The tiles that the user has already selected to form a new room
+ * @returns {Array} A 2D list of tile positions that are valid next tile choices.
+ */
+ public static deriveValidNextTilePositions(rooms: IRoom[], selectedTiles: ITile[]): IGridPosition[] {
+ let result = [], newPosition = {x: 0, y: 0};
+ let isSurroundingTile;
+
+ selectedTiles.forEach((tile: ITile) => {
+ let x = tile.position.x, y = tile.position.y;
+ for (let dX = -1; dX <= 1; dX++) {
+ for (let dY = -1; dY <= 1; dY++) {
+ if (Math.abs(dX) === Math.abs(dY)) {
+ continue;
+ }
+
+ newPosition.x = x + dX;
+ newPosition.y = y + dY;
+
+ isSurroundingTile = true;
+ selectedTiles.forEach((otherTile: ITile) => {
+ if (otherTile.position.x === newPosition.x && otherTile.position.y === newPosition.y) {
+ isSurroundingTile = false;
+ }
+ });
+
+ if (isSurroundingTile && !Util.checkRoomCollision(rooms, newPosition)) {
+ result.push({x: newPosition.x, y: newPosition.y});
+ }
+ }
+ }
+ });
+
+ return result;
+ }
+
+ /**
+ * Determines whether a position is contained in a list of tiles.
+ *
+ * @param list A list of tiles
+ * @param position A position
+ * @returns {boolean} Whether the list contains the position
+ */
+ public static tileListContainsPosition(list: ITile[], position: IGridPosition): boolean {
+ return Util.tileListPositionIndexOf(list, position) !== -1;
+ }
+
+ /**
+ * Determines the index of a position in a list of tiles.
+ *
+ * @param list A list of tiles
+ * @param position A position
+ * @returns {number} Index of the position in the list of tiles, -1 if not found
+ */
+ public static tileListPositionIndexOf(list: ITile[], position: IGridPosition): number {
+ let index = -1;
+ let element;
+
+ for (let i = 0; i < list.length; i++) {
+ element = list[i];
+ if (position.x === element.position.x && position.y === element.position.y) {
+ index = i;
+ break;
+ }
+ }
+
+ return index;
+ }
+
+ /**
+ * Determines whether a position is contained in a list of positions.
+ *
+ * @param list A list of positions
+ * @param position A position
+ * @returns {boolean} Whether the list contains the position
+ */
+ public static positionListContainsPosition(list: IGridPosition[], position: IGridPosition): boolean {
+ return Util.positionListPositionIndexOf(list, position) !== -1;
+ }
+
+ /**
+ * Determines the index of a position in a list of positions.
+ *
+ * @param list A list of positions
+ * @param position A position
+ * @returns {number} Index of the position in the list of tiles, -1 if not found
+ */
+ public static positionListPositionIndexOf(list: IGridPosition[], position: IGridPosition): number {
+ let index = -1;
+ let element;
+
+ for (let i = 0; i < list.length; i++) {
+ element = list[i];
+ if (position.x === element.x && position.y === element.y) {
+ index = i;
+ break;
+ }
+ }
+
+ return index;
+ }
+
+ /**
+ * Determines the index of a room that is colliding with a given grid tile.
+ *
+ * Returns -1 if no collision is found.
+ *
+ * @param rooms An array of Room objects that should be checked for collisions
+ * @param position A position
+ * @returns {number} The index of the room in the rooms list if found, else -1
+ */
+ public static roomCollisionIndexOf(rooms: IRoom[], position: IGridPosition): number {
+ let index = -1;
+ let room;
+
+ for (let i = 0; i < rooms.length; i++) {
+ room = rooms[i];
+ if (Util.tileListContainsPosition(room.tiles, position)) {
+ index = i;
+ break;
+ }
+ }
+
+ return index;
+ }
+
+ /**
+ * Checks whether a tile location collides with an existing room.
+ *
+ * @param rooms A list of rooms to be analyzed
+ * @param position A position
+ * @returns {boolean} Whether the tile lies in an existing room
+ */
+ public static checkRoomCollision(rooms: IRoom[], position: IGridPosition): boolean {
+ return Util.roomCollisionIndexOf(rooms, position) !== -1;
+ }
+
+ /**
+ * Calculates the minimum, center, and maximum of a list of rooms in stage coordinates.
+ *
+ * This center is calculated by averaging the most outlying tiles of all rooms.
+ *
+ * @param rooms The rooms to be analyzed
+ * @returns {IBounds} The coordinates of the minimum, center, and maximum
+ */
+ public static calculateRoomListBounds(rooms: IRoom[]): IBounds {
+ let min = [Number.MAX_VALUE, Number.MAX_VALUE];
+ let max = [-1, -1];
+
+ rooms.forEach((room: IRoom) => {
+ room.tiles.forEach((tile: ITile) => {
+ if (tile.position.x < min[0]) {
+ min[0] = tile.position.x;
+ }
+ if (tile.position.y < min[1]) {
+ min[1] = tile.position.y;
+ }
+
+ if (tile.position.x > max[0]) {
+ max[0] = tile.position.x;
+ }
+ if (tile.position.y > max[1]) {
+ max[1] = tile.position.y;
+ }
+ });
+ });
+
+ max[0]++;
+ max[1]++;
+
+ let gridCenter = [min[0] + (max[0] - min[0]) / 2.0, min[1] + (max[1] - min[1]) / 2.0];
+
+ return {
+ min: min,
+ center: gridCenter,
+ max: max
+ };
+ }
+
+ /**
+ * Does the same as 'calculateRoomListBounds', only for one room.
+ *
+ * @param room The room to be analyzed
+ * @returns {IBounds} The coordinates of the minimum, center, and maximum
+ */
+ public static calculateRoomBounds(room: IRoom): IBounds {
+ return Util.calculateRoomListBounds([room]);
+ }
+
+ public static calculateRoomNamePosition(room: IRoom): IRoomNamePos {
+ let result: IRoomNamePos = {
+ topLeft: {x: 0, y: 0},
+ length: 0
+ };
+
+ // Look for the top-most tile y-coordinate
+ let topMin = Number.MAX_VALUE;
+ room.tiles.forEach((tile: ITile) => {
+ if (tile.position.y < topMin) {
+ topMin = tile.position.y;
+ }
+ });
+
+ // If there is no tile at the top, meaning that the room has no tiles, exit
+ if (topMin === Number.MAX_VALUE) {
+ return null;
+ }
+
+ // Find the left-most tile at the top and the length of its adjacent tiles to the right
+ let topTilePositions: number[] = [];
+ room.tiles.forEach((tile: ITile) => {
+ if (tile.position.y === topMin) {
+ topTilePositions.push(tile.position.x);
+ }
+ });
+ topTilePositions.sort();
+ let leftMin = topTilePositions[0];
+ let length = 0;
+
+ while (length < topTilePositions.length && topTilePositions[length] - leftMin === length) {
+ length++;
+ }
+
+ result.topLeft.x = leftMin;
+ result.topLeft.y = topMin;
+ result.length = length;
+
+ return result;
+ }
+
+ /**
+ * Analyzes an array of objects and calculates its fill ratio, by looking at the number of elements != null and
+ * comparing that number to the array length.
+ *
+ * @param inputList The list to be analyzed
+ * @returns {number} A fill ratio (between 0 and 1), representing the relative amount of objects != null in the list
+ */
+ public static getFillRatio(inputList: any[]): number {
+ let numNulls = 0;
+
+ if (inputList.length === 0) {
+ return 0;
+ }
+
+ inputList.forEach((element: any) => {
+ if (element == null) {
+ numNulls++;
+ }
+ });
+
+ return (inputList.length - numNulls) / inputList.length;
+ }
+
+ /**
+ * Calculates the energy consumption ration of the given rack.
+ *
+ * @param rack The rack of which the power consumption should be analyzed
+ * @returns {number} The energy consumption ratio
+ */
+ public static getEnergyRatio(rack: IRack): number {
+ let energySum = 0;
+
+ rack.machines.forEach((machine: IMachine) => {
+ if (machine === null) {
+ return;
+ }
+
+ let machineConsumption = 0;
+
+ let nodeUnitList: INodeUnit[] = <INodeUnit[]>machine.cpus.concat(machine.gpus);
+ nodeUnitList = nodeUnitList.concat(<INodeUnit[]>machine.memories);
+ nodeUnitList = nodeUnitList.concat(<INodeUnit[]>machine.storages);
+ nodeUnitList.forEach((unit: INodeUnit) => {
+ machineConsumption += unit.energyConsumptionW;
+ });
+
+ energySum += machineConsumption;
+ });
+
+ return energySum / rack.powerCapacityW;
+ }
+
+ /**
+ * Parses date-time expresses of the form YYYY-MM-DDTHH:MM:SS and returns a parsed object.
+ *
+ * @param input A string expressing a date and a time, in the above mentioned format
+ * @returns {IDateTime} A DateTime object with the parsed date and time information as content
+ */
+ public static parseDateTime(input: string): IDateTime {
+ let output: IDateTime = {
+ year: 0,
+ month: 0,
+ day: 0,
+ hour: 0,
+ minute: 0,
+ second: 0
+ };
+
+ let dateAndTime = input.split("T");
+ let dateComponents = dateAndTime[0].split("-");
+ output.year = parseInt(dateComponents[0], 10);
+ output.month = parseInt(dateComponents[1], 10);
+ output.day = parseInt(dateComponents[2], 10);
+
+ let timeComponents = dateAndTime[1].split(":");
+ output.hour = parseInt(timeComponents[0], 10);
+ output.minute = parseInt(timeComponents[1], 10);
+ output.second = parseInt(timeComponents[2], 10);
+
+ return output;
+ }
+
+ public static formatDateTime(input: IDateTime) {
+ let date;
+ let currentDate = new Date();
+
+ date = Util.addPaddingToTwo(input.day) + "/" +
+ Util.addPaddingToTwo(input.month) + "/" +
+ Util.addPaddingToTwo(input.year);
+
+ if (input.year === currentDate.getFullYear() &&
+ input.month === currentDate.getMonth() + 1) {
+ if (input.day === currentDate.getDate()) {
+ date = "Today";
+ } else if (input.day === currentDate.getDate() - 1) {
+ date = "Yesterday";
+ }
+ }
+
+ return date + ", " +
+ Util.addPaddingToTwo(input.hour) + ":" +
+ Util.addPaddingToTwo(input.minute);
+ }
+
+ public static getCurrentDateTime(): string {
+ let date = new Date();
+ return date.getFullYear() + "-" + Util.addPaddingToTwo(date.getMonth() + 1) + "-" +
+ Util.addPaddingToTwo(date.getDate()) + "T" + Util.addPaddingToTwo(date.getHours()) + ":" +
+ Util.addPaddingToTwo(date.getMinutes()) + ":" + Util.addPaddingToTwo(date.getSeconds());
+ }
+
+ /**
+ * Removes all populated object properties from a given object, and returns a copy without them.
+ *
+ * An exception of such an object property is made in the case of a position object (of type GridPosition), which
+ * is copied over as well.
+ *
+ * Does not manipulate the original object in any way, except if your object has quantum-like properties, which
+ * change upon inspection. In such a case, I'm afraid that this method can do little for you.
+ *
+ * @param object The input object
+ * @returns {any} A copy of the object without any populated properties (of type object).
+ */
+ public static packageForSending(object: any) {
+ let result: any = {};
+ for (let prop in object) {
+ if (object.hasOwnProperty(prop)) {
+ if (typeof object[prop] !== "object") {
+ result[prop] = object[prop];
+ } else {
+ if (object[prop] instanceof Array) {
+ if (object[prop].length === 0 || !(object[prop][0] instanceof Object)) {
+ result[prop] = [];
+ for (let i = 0; i < object[prop].length; i++) {
+ result[prop][i] = object[prop][i];
+ }
+ }
+ }
+ if (object[prop] != null && object[prop].hasOwnProperty("x") && object[prop].hasOwnProperty("y")) {
+ result["positionX"] = object[prop].x;
+ result["positionY"] = object[prop].y;
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ public static addPaddingToTwo(integer: number): string {
+ if (integer < 10) {
+ return "0" + integer.toString();
+ } else {
+ return integer.toString();
+ }
+ }
+
+ public static convertSecondsToFormattedTime(seconds: number): string {
+ let hour = Math.floor(seconds / 3600);
+ let minute = Math.floor(seconds / 60) % 60;
+ let second = seconds % 60;
+ return this.addPaddingToTwo(hour) + ":" +
+ this.addPaddingToTwo(minute) + ":" +
+ this.addPaddingToTwo(second);
+ }
+
+ public static determineLoadIntensityLevel(loadFraction: number): IntensityLevel {
+ if (loadFraction < 0.25) {
+ return IntensityLevel.LOW;
+ } else if (loadFraction < 0.5) {
+ return IntensityLevel.MID_LOW;
+ } else if (loadFraction < 0.75) {
+ return IntensityLevel.MID_HIGH;
+ } else {
+ return IntensityLevel.HIGH;
+ }
+ }
+
+ public static convertIntensityToColor(intensityLevel: IntensityLevel): string {
+ if (intensityLevel === IntensityLevel.LOW) {
+ return Colors.SIM_LOW;
+ } else if (intensityLevel === IntensityLevel.MID_LOW) {
+ return Colors.SIM_MID_LOW;
+ } else if (intensityLevel === IntensityLevel.MID_HIGH) {
+ return Colors.SIM_MID_HIGH;
+ } else if (intensityLevel === IntensityLevel.HIGH) {
+ return Colors.SIM_HIGH;
+ }
+ }
+
+ /**
+ * Gives the sentence-cased alternative for a given string.
+ *
+ * @example Input: TEST, Output: Test
+ *
+ * @param input The input string
+ * @returns {any} The sentence-cased string
+ */
+ public static toSentenceCase(input: string): string {
+ if (input === undefined || input === null) {
+ return undefined;
+ }
+ if (input.length === 0) {
+ return "";
+ }
+
+ return input[0].toUpperCase() + input.substr(1).toLowerCase();
+ }
+
+ /**
+ * Sort a list of authorizations based on the levels of authorizations.
+ *
+ * @param list The list to be sorted (in-place)
+ */
+ public static sortAuthorizations(list: IAuthorization[]): void {
+ list.sort((a: IAuthorization, b: IAuthorization): number => {
+ return this.authorizationLevels.indexOf(a.authorizationLevel) -
+ this.authorizationLevels.indexOf(b.authorizationLevel);
+ });
+ }
+
+ /**
+ * Returns an array containing all numbers of a range from 0 to x (including x).
+ */
+ public static range(x: number): number[] {
+ return Array.apply(null, Array(x + 1)).map((_, i) => {
+ return i.toString();
+ })
+ }
+}
diff --git a/src/scripts/views/layers/dcobject.ts b/src/scripts/views/layers/dcobject.ts
new file mode 100644
index 00000000..6cec1f7e
--- /dev/null
+++ b/src/scripts/views/layers/dcobject.ts
@@ -0,0 +1,252 @@
+import {Colors} from "../../colors";
+import {Util, IntensityLevel} from "../../util";
+import {MapView} from "../mapview";
+import {DCProgressBar} from "./dcprogressbar";
+import {Layer} from "./layer";
+import {CELL_SIZE} from "../../controllers/mapcontroller";
+
+
+export class DCObjectLayer implements Layer {
+ public static ITEM_MARGIN = CELL_SIZE / 7.0;
+ public static ITEM_PADDING = CELL_SIZE / 10.0;
+ public static STROKE_WIDTH = CELL_SIZE / 20.0;
+ public static PROGRESS_BAR_DISTANCE = CELL_SIZE / 17.0;
+ public static CONTENT_SIZE = CELL_SIZE - DCObjectLayer.ITEM_MARGIN * 2 - DCObjectLayer.ITEM_PADDING * 3;
+
+ public container: createjs.Container;
+ public detailedMode: boolean;
+ public coloringMode: boolean;
+ public intensityLevels: { [key: number]: IntensityLevel; } = {};
+
+ private mapView: MapView;
+ private preload: createjs.LoadQueue;
+ private rackSpaceBitmap: createjs.Bitmap;
+ private rackEnergyBitmap: createjs.Bitmap;
+ private psuBitmap: createjs.Bitmap;
+ private coolingItemBitmap: createjs.Bitmap;
+
+ // This associative lookup object keeps all DC display objects with as property name the index of the global map
+ // array that they are located in.
+ private dcObjectMap: { [key: number]: any; };
+
+
+ public static drawHoverRack(position: IGridPosition): createjs.Container {
+ let result = new createjs.Container();
+
+ DCObjectLayer.drawItemRectangle(
+ position, Colors.RACK_BACKGROUND, Colors.RACK_BORDER, result
+ );
+ DCProgressBar.drawItemProgressRectangle(
+ position, Colors.RACK_SPACE_BAR_BACKGROUND, result, 0, 1
+ );
+ DCProgressBar.drawItemProgressRectangle(
+ position, Colors.RACK_ENERGY_BAR_BACKGROUND, result, 1, 1
+ );
+
+ return result;
+ }
+
+ public static drawHoverPSU(position: IGridPosition): createjs.Container {
+ let result = new createjs.Container();
+
+ DCObjectLayer.drawItemRectangle(
+ position, Colors.PSU_BACKGROUND, Colors.PSU_BORDER, result
+ );
+
+ return result;
+ }
+
+ public static drawHoverCoolingItem(position: IGridPosition): createjs.Container {
+ let result = new createjs.Container();
+
+ DCObjectLayer.drawItemRectangle(
+ position, Colors.COOLING_ITEM_BACKGROUND, Colors.COOLING_ITEM_BORDER, result
+ );
+
+ return result;
+ }
+
+ /**
+ * Draws an object rectangle in a given grid cell, with margin around its border.
+ *
+ * @param position The coordinates of the grid cell in which it should be located
+ * @param color The background color of the item
+ * @param borderColor The border color
+ * @param container The container to which it should be drawn
+ * @returns {createjs.Shape} The drawn shape
+ */
+ private static drawItemRectangle(position: IGridPosition, color: string, borderColor: string,
+ container: createjs.Container): createjs.Shape {
+ let shape = new createjs.Shape();
+ shape.graphics.beginStroke(borderColor);
+ shape.graphics.setStrokeStyle(DCObjectLayer.STROKE_WIDTH);
+ shape.graphics.beginFill(color);
+ shape.graphics.drawRect(
+ position.x * CELL_SIZE + DCObjectLayer.ITEM_MARGIN,
+ position.y * CELL_SIZE + DCObjectLayer.ITEM_MARGIN,
+ CELL_SIZE - DCObjectLayer.ITEM_MARGIN * 2,
+ CELL_SIZE - DCObjectLayer.ITEM_MARGIN * 2
+ );
+ container.addChild(shape);
+ return shape;
+ }
+
+ /**
+ * Draws an bitmap in item format.
+ *
+ * @param position The coordinates of the grid cell in which it should be located
+ * @param container The container to which it should be drawn
+ * @param originBitmap The bitmap that should be drawn
+ * @returns {createjs.Bitmap} The drawn bitmap
+ */
+ private static drawItemIcon(position: IGridPosition, container: createjs.Container,
+ originBitmap: createjs.Bitmap): createjs.Bitmap {
+ let bitmap = originBitmap.clone();
+ container.addChild(bitmap);
+ bitmap.x = position.x * CELL_SIZE + DCObjectLayer.ITEM_MARGIN + DCObjectLayer.ITEM_PADDING * 1.5;
+ bitmap.y = position.y * CELL_SIZE + DCObjectLayer.ITEM_MARGIN + DCObjectLayer.ITEM_PADDING * 1.5;
+ return bitmap;
+ }
+
+ constructor(mapView: MapView) {
+ this.mapView = mapView;
+ this.container = new createjs.Container();
+
+ this.detailedMode = true;
+ this.coloringMode = false;
+
+ this.preload = new createjs.LoadQueue();
+ this.preload.addEventListener("complete", () => {
+ this.rackSpaceBitmap = new createjs.Bitmap(<HTMLImageElement>this.preload.getResult("rack-space"));
+ this.rackEnergyBitmap = new createjs.Bitmap(<HTMLImageElement>this.preload.getResult("rack-energy"));
+ this.psuBitmap = new createjs.Bitmap(<HTMLImageElement>this.preload.getResult("psu"));
+ this.coolingItemBitmap = new createjs.Bitmap(<HTMLImageElement>this.preload.getResult("coolingitem"));
+
+ // Scale the images
+ this.rackSpaceBitmap.scaleX = DCProgressBar.PROGRESS_BAR_WIDTH / this.rackSpaceBitmap.image.width;
+ this.rackSpaceBitmap.scaleY = DCProgressBar.PROGRESS_BAR_WIDTH / this.rackSpaceBitmap.image.height;
+
+ this.rackEnergyBitmap.scaleX = DCProgressBar.PROGRESS_BAR_WIDTH / this.rackEnergyBitmap.image.width;
+ this.rackEnergyBitmap.scaleY = DCProgressBar.PROGRESS_BAR_WIDTH / this.rackEnergyBitmap.image.height;
+
+ this.psuBitmap.scaleX = DCObjectLayer.CONTENT_SIZE / this.psuBitmap.image.width;
+ this.psuBitmap.scaleY = DCObjectLayer.CONTENT_SIZE / this.psuBitmap.image.height;
+
+ this.coolingItemBitmap.scaleX = DCObjectLayer.CONTENT_SIZE / this.coolingItemBitmap.image.width;
+ this.coolingItemBitmap.scaleY = DCObjectLayer.CONTENT_SIZE / this.coolingItemBitmap.image.height;
+
+
+ this.populateObjectList();
+ this.draw();
+
+ this.mapView.updateScene = true;
+ });
+
+ this.preload.loadFile({id: "rack-space", src: 'img/app/rack-space.png'});
+ this.preload.loadFile({id: "rack-energy", src: 'img/app/rack-energy.png'});
+ this.preload.loadFile({id: "psu", src: 'img/app/psu.png'});
+ this.preload.loadFile({id: "coolingitem", src: 'img/app/coolingitem.png'});
+ }
+
+ /**
+ * Generates a list of DC objects with their associated display objects.
+ */
+ public populateObjectList(): void {
+ this.dcObjectMap = {};
+
+ this.mapView.currentDatacenter.rooms.forEach((room: IRoom) => {
+ room.tiles.forEach((tile: ITile) => {
+ if (tile.object !== undefined) {
+ let index = tile.position.y * MapView.MAP_SIZE + tile.position.x;
+
+ switch (tile.objectType) {
+ case "RACK":
+ this.dcObjectMap[index] = {
+ spaceBar: new DCProgressBar(this.container,
+ Colors.RACK_SPACE_BAR_BACKGROUND, Colors.RACK_SPACE_BAR_FILL,
+ this.rackSpaceBitmap, tile.position, 0,
+ Util.getFillRatio((<IRack>tile.object).machines)),
+ energyBar: new DCProgressBar(this.container,
+ Colors.RACK_ENERGY_BAR_BACKGROUND, Colors.RACK_ENERGY_BAR_FILL,
+ this.rackEnergyBitmap, tile.position, 1,
+ Util.getFillRatio((<IRack>tile.object).machines)),
+ itemRect: createjs.Shape,
+ tile: tile, model: tile.object, position: tile.position, type: tile.objectType
+ };
+
+ break;
+ case "COOLING_ITEM":
+ this.dcObjectMap[index] = {
+ itemRect: createjs.Shape, batteryIcon: createjs.Bitmap,
+ tile: tile, model: tile.object, position: tile.position, type: tile.objectType
+ };
+ break;
+ case "PSU":
+ this.dcObjectMap[index] = {
+ itemRect: createjs.Shape, freezeIcon: createjs.Bitmap,
+ tile: tile, model: tile.object, position: tile.position, type: tile.objectType
+ };
+ break;
+ }
+ }
+ });
+ });
+ }
+
+ public draw(): void {
+ let currentObject;
+
+ this.container.removeAllChildren();
+
+ this.container.cursor = "pointer";
+
+ for (let property in this.dcObjectMap) {
+ if (this.dcObjectMap.hasOwnProperty(property)) {
+ currentObject = this.dcObjectMap[property];
+
+ switch (currentObject.type) {
+ case "RACK":
+ let color = Colors.RACK_BACKGROUND;
+
+ if (this.coloringMode && currentObject.tile.roomId ===
+ this.mapView.mapController.roomModeController.currentRoom.id) {
+ color = Util.convertIntensityToColor(this.intensityLevels[currentObject.model.id]);
+ }
+
+ currentObject.itemRect = DCObjectLayer.drawItemRectangle(
+ currentObject.position, color, Colors.RACK_BORDER, this.container
+ );
+
+ if (this.detailedMode) {
+ currentObject.spaceBar.fillRatio = Util.getFillRatio(currentObject.model.machines);
+ currentObject.energyBar.fillRatio = Util.getEnergyRatio(currentObject.model);
+
+ currentObject.spaceBar.draw();
+ currentObject.energyBar.draw();
+ }
+ break;
+ case "COOLING_ITEM":
+ currentObject.itemRect = DCObjectLayer.drawItemRectangle(
+ currentObject.position, Colors.COOLING_ITEM_BACKGROUND, Colors.COOLING_ITEM_BORDER,
+ this.container
+ );
+
+ currentObject.freezeIcon = DCObjectLayer.drawItemIcon(currentObject.position, this.container,
+ this.coolingItemBitmap);
+ break;
+ case "PSU":
+ currentObject.itemRect = DCObjectLayer.drawItemRectangle(
+ currentObject.position, Colors.PSU_BACKGROUND, Colors.PSU_BORDER,
+ this.container
+ );
+
+ currentObject.batteryIcon = DCObjectLayer.drawItemIcon(currentObject.position, this.container,
+ this.psuBitmap);
+ break;
+ }
+ }
+ }
+
+ this.mapView.updateScene = true;
+ }
+} \ No newline at end of file
diff --git a/src/scripts/views/layers/dcprogressbar.ts b/src/scripts/views/layers/dcprogressbar.ts
new file mode 100644
index 00000000..d0ec4397
--- /dev/null
+++ b/src/scripts/views/layers/dcprogressbar.ts
@@ -0,0 +1,99 @@
+import {DCObjectLayer} from "./dcobject";
+import {CELL_SIZE} from "../../controllers/mapcontroller";
+
+
+export class DCProgressBar {
+ public static PROGRESS_BAR_WIDTH = CELL_SIZE / 7.0;
+
+ public container: createjs.Container;
+ public fillRatio: number;
+
+ private backgroundRect: createjs.Shape;
+ private backgroundColor: string;
+ private fillRect: createjs.Shape;
+ private fillColor: string;
+ private bitmap: createjs.Bitmap;
+ private position: IGridPosition;
+ private distanceFromBottom: number;
+
+
+ /**
+ * Draws a progress rectangle with rounded ends.
+ *
+ * @param position The coordinates of the grid cell in which it should be located
+ * @param color The background color of the item
+ * @param container The container to which it should be drawn
+ * @param distanceFromBottom The index of its vertical position, counted from the bottom (0 is the lowest position)
+ * @param fractionFilled The fraction of the available horizontal space that the progress bar should take up
+ * @returns {createjs.Shape} The drawn shape
+ */
+ public static drawItemProgressRectangle(position: IGridPosition, color: string,
+ container: createjs.Container, distanceFromBottom: number,
+ fractionFilled: number): createjs.Shape {
+ let shape = new createjs.Shape();
+ shape.graphics.beginFill(color);
+ let x = position.x * CELL_SIZE + DCObjectLayer.ITEM_MARGIN + DCObjectLayer.ITEM_PADDING;
+ let y = (position.y + 1) * CELL_SIZE - DCObjectLayer.ITEM_MARGIN - DCObjectLayer.ITEM_PADDING -
+ DCProgressBar.PROGRESS_BAR_WIDTH - distanceFromBottom *
+ (DCProgressBar.PROGRESS_BAR_WIDTH + DCObjectLayer.PROGRESS_BAR_DISTANCE);
+ let width = (CELL_SIZE - (DCObjectLayer.ITEM_MARGIN + DCObjectLayer.ITEM_PADDING) * 2) * fractionFilled;
+ let height;
+ let radius;
+
+ if (width < DCProgressBar.PROGRESS_BAR_WIDTH) {
+ height = width;
+ radius = width / 2;
+ y += (DCProgressBar.PROGRESS_BAR_WIDTH - height) / 2;
+ } else {
+ height = DCProgressBar.PROGRESS_BAR_WIDTH;
+ radius = DCProgressBar.PROGRESS_BAR_WIDTH / 2;
+ }
+
+ shape.graphics.drawRoundRect(
+ x, y, width, height, radius
+ );
+ container.addChild(shape);
+ return shape;
+ }
+
+ /**
+ * Draws an bitmap in progressbar format.
+ *
+ * @param position The coordinates of the grid cell in which it should be located
+ * @param container The container to which it should be drawn
+ * @param originBitmap The bitmap that should be drawn
+ * @param distanceFromBottom The index of its vertical position, counted from the bottom (0 is the lowest position)
+ * @returns {createjs.Bitmap} The drawn bitmap
+ */
+ public static drawProgressbarIcon(position: IGridPosition, container: createjs.Container, originBitmap: createjs.Bitmap,
+ distanceFromBottom: number): createjs.Bitmap {
+ let bitmap = originBitmap.clone();
+ container.addChild(bitmap);
+ bitmap.x = (position.x + 0.5) * CELL_SIZE - DCProgressBar.PROGRESS_BAR_WIDTH * 0.5;
+ bitmap.y = (position.y + 1) * CELL_SIZE - DCObjectLayer.ITEM_MARGIN - DCObjectLayer.ITEM_PADDING -
+ DCProgressBar.PROGRESS_BAR_WIDTH - distanceFromBottom *
+ (DCProgressBar.PROGRESS_BAR_WIDTH + DCObjectLayer.PROGRESS_BAR_DISTANCE);
+ return bitmap;
+ }
+
+ constructor(container: createjs.Container, backgroundColor: string,
+ fillColor: string, bitmap: createjs.Bitmap, position: IGridPosition,
+ indexFromBottom: number, fillRatio: number) {
+ this.container = container;
+ this.backgroundColor = backgroundColor;
+ this.fillColor = fillColor;
+ this.bitmap = bitmap;
+ this.position = position;
+ this.distanceFromBottom = indexFromBottom;
+ this.fillRatio = fillRatio;
+ }
+
+ public draw() {
+ this.backgroundRect = DCProgressBar.drawItemProgressRectangle(this.position, this.backgroundColor,
+ this.container, this.distanceFromBottom, 1);
+ this.fillRect = DCProgressBar.drawItemProgressRectangle(this.position, this.fillColor, this.container,
+ this.distanceFromBottom, this.fillRatio);
+
+ DCProgressBar.drawProgressbarIcon(this.position, this.container, this.bitmap, this.distanceFromBottom);
+ }
+} \ No newline at end of file
diff --git a/src/scripts/views/layers/gray.ts b/src/scripts/views/layers/gray.ts
new file mode 100644
index 00000000..ed3c9429
--- /dev/null
+++ b/src/scripts/views/layers/gray.ts
@@ -0,0 +1,145 @@
+import {MapView} from "../mapview";
+import {Colors} from "../../colors";
+import {Util} from "../../util";
+import {Layer} from "./layer";
+
+
+/**
+ * Class responsible for graying out non-active UI elements.
+ */
+export class GrayLayer implements Layer {
+ public container: createjs.Container;
+ public currentRoom: IRoom;
+ public currentObjectTile: ITile;
+
+ private mapView: MapView;
+ private grayRoomShape: createjs.Shape;
+
+
+ constructor(mapView: MapView) {
+ this.mapView = mapView;
+ this.container = new createjs.Container();
+ }
+
+ /**
+ * Draws grayed out areas around a currently selected room.
+ *
+ * @param redraw Whether this is a redraw, or an initial draw action
+ */
+ public draw(redraw?: boolean): void {
+ if (this.currentRoom === undefined) {
+ return;
+ }
+
+ this.container.removeAllChildren();
+
+ let roomBounds = Util.calculateRoomBounds(this.currentRoom);
+
+ let shape = new createjs.Shape();
+ shape.graphics.beginFill(Colors.GRAYED_OUT_AREA);
+ shape.cursor = "pointer";
+
+ this.drawLargeRects(shape, roomBounds);
+ this.drawFineGrainedRects(shape, roomBounds);
+
+ this.container.addChild(shape);
+ if (redraw === true) {
+ shape.alpha = 1;
+ } else {
+ shape.alpha = 0;
+ this.mapView.animate(shape, {alpha: 1});
+ }
+
+ if (this.grayRoomShape !== undefined && !this.grayRoomShape.visible) {
+ this.grayRoomShape = undefined;
+ this.drawRackLevel(redraw);
+ }
+
+ this.mapView.updateScene = true;
+ }
+
+ private drawLargeRects(shape: createjs.Shape, roomBounds: IBounds): void {
+ if (roomBounds.min[0] > 0) {
+ MapView.drawRectangleToShape({x: 0, y: 0}, shape, roomBounds.min[0], MapView.MAP_SIZE);
+ }
+ if (roomBounds.min[1] > 0) {
+ MapView.drawRectangleToShape({x: roomBounds.min[0], y: 0}, shape, roomBounds.max[0] - roomBounds.min[0],
+ roomBounds.min[1]);
+ }
+ if (roomBounds.max[0] < MapView.MAP_SIZE - 1) {
+ MapView.drawRectangleToShape({x: roomBounds.max[0], y: 0}, shape, MapView.MAP_SIZE - roomBounds.max[0],
+ MapView.MAP_SIZE);
+ }
+ if (roomBounds.max[1] < MapView.MAP_SIZE - 1) {
+ MapView.drawRectangleToShape({x: roomBounds.min[0], y: roomBounds.max[1]}, shape,
+ roomBounds.max[0] - roomBounds.min[0], MapView.MAP_SIZE - roomBounds.max[1]);
+ }
+ }
+
+ private drawFineGrainedRects(shape: createjs.Shape, roomBounds: IBounds): void {
+ for (let x = roomBounds.min[0]; x < roomBounds.max[0]; x++) {
+ for (let y = roomBounds.min[1]; y < roomBounds.max[1]; y++) {
+ if (!Util.tileListContainsPosition(this.currentRoom.tiles, {x: x, y: y})) {
+ MapView.drawRectangleToShape({x: x, y: y}, shape);
+ }
+ }
+ }
+ }
+
+ public drawRackLevel(redraw?: boolean): void {
+ if (this.currentObjectTile === undefined) {
+ return;
+ }
+
+ this.grayRoomShape = new createjs.Shape();
+ this.grayRoomShape.graphics.beginFill(Colors.GRAYED_OUT_AREA);
+ this.grayRoomShape.cursor = "pointer";
+ this.grayRoomShape.alpha = 0;
+
+ this.currentRoom.tiles.forEach((tile: ITile) => {
+ if (this.currentObjectTile.position.x !== tile.position.x ||
+ this.currentObjectTile.position.y !== tile.position.y) {
+ MapView.drawRectangleToShape({x: tile.position.x, y: tile.position.y}, this.grayRoomShape);
+ }
+ });
+
+ this.container.addChild(this.grayRoomShape);
+ if (redraw === true) {
+ this.grayRoomShape.alpha = 1;
+ } else {
+ this.grayRoomShape.alpha = 0;
+ this.mapView.animate(this.grayRoomShape, {alpha: 1});
+ }
+ }
+
+ public hideRackLevel(): void {
+ if (this.currentObjectTile === undefined) {
+ return;
+ }
+
+ this.mapView.animate(this.grayRoomShape, {
+ alpha: 0, visible: false
+ });
+ }
+
+ /**
+ * Clears the container.
+ */
+ public clear(): void {
+ this.mapView.animate(this.container, {alpha: 0}, () => {
+ this.container.removeAllChildren();
+ this.container.alpha = 1;
+ });
+ this.grayRoomShape = undefined;
+ this.currentRoom = undefined;
+ }
+
+ /**
+ * Checks whether there is already an active room with grayed out areas around it.
+ *
+ * @returns {boolean} Whether the room is grayed out
+ */
+ public isGrayedOut(): boolean {
+ return this.currentRoom !== undefined;
+ }
+} \ No newline at end of file
diff --git a/src/scripts/views/layers/grid.ts b/src/scripts/views/layers/grid.ts
new file mode 100644
index 00000000..9a52b2af
--- /dev/null
+++ b/src/scripts/views/layers/grid.ts
@@ -0,0 +1,59 @@
+import {Layer} from "./layer";
+import {MapView} from "../mapview";
+import {Colors} from "../../colors";
+import {CELL_SIZE} from "../../controllers/mapcontroller";
+
+
+/**
+ * Class responsible for rendering the grid.
+ */
+export class GridLayer implements Layer {
+ public container: createjs.Container;
+ public gridPixelSize: number;
+
+ private mapView: MapView;
+ private gridLineWidth: number;
+
+
+ constructor(mapView: MapView) {
+ this.mapView = mapView;
+ this.container = new createjs.Container();
+
+ this.gridLineWidth = 0.5;
+ this.gridPixelSize = MapView.MAP_SIZE * CELL_SIZE;
+
+ this.draw();
+ }
+
+ /**
+ * Draws the entire grid (later to be navigated around with offsets).
+ */
+ public draw(): void {
+ this.container.removeAllChildren();
+
+ let currentCellX = 0;
+ let currentCellY = 0;
+
+ while (currentCellX <= MapView.MAP_SIZE) {
+ MapView.drawLine(
+ currentCellX * CELL_SIZE, 0,
+ currentCellX * CELL_SIZE, MapView.MAP_SIZE * CELL_SIZE,
+ this.gridLineWidth, Colors.GRID_COLOR, this.container);
+
+ currentCellX++;
+ }
+
+ while (currentCellY <= MapView.MAP_SIZE) {
+ MapView.drawLine(
+ 0, currentCellY * CELL_SIZE,
+ MapView.MAP_SIZE * CELL_SIZE, currentCellY * CELL_SIZE,
+ this.gridLineWidth, Colors.GRID_COLOR, this.container);
+
+ currentCellY++;
+ }
+ }
+
+ public setVisibility(value: boolean): void {
+ this.container.visible = value;
+ }
+} \ No newline at end of file
diff --git a/src/scripts/views/layers/hover.ts b/src/scripts/views/layers/hover.ts
new file mode 100644
index 00000000..b9f5509c
--- /dev/null
+++ b/src/scripts/views/layers/hover.ts
@@ -0,0 +1,129 @@
+import {Layer} from "./layer";
+import {MapView} from "../mapview";
+import {Colors} from "../../colors";
+import {DCObjectLayer} from "./dcobject";
+import {CELL_SIZE} from "../../controllers/mapcontroller";
+
+
+/**
+ * Class responsible for rendering the hover layer.
+ */
+export class HoverLayer implements Layer {
+ public container: createjs.Container;
+ public hoverTilePosition: IGridPosition;
+
+ private mapView: MapView;
+ private hoverTile: createjs.Shape;
+ private hoverRack: createjs.Container;
+ private hoverPSU: createjs.Container;
+ private hoverCoolingItem: createjs.Container;
+
+
+ constructor(mapView: MapView) {
+ this.mapView = mapView;
+ this.container = new createjs.Container();
+
+ this.initialDraw();
+ }
+
+ /**
+ * Draws the hover tile to the container at its current location and with its current color.
+ */
+ public draw(): void {
+ let color;
+
+ if (this.mapView.roomLayer.checkHoverTileValidity(this.hoverTilePosition)) {
+ color = Colors.ROOM_HOVER_VALID;
+ } else {
+ color = Colors.ROOM_HOVER_INVALID;
+ }
+
+ this.hoverTile.graphics.clear().beginFill(color)
+ .drawRect(this.hoverTilePosition.x * CELL_SIZE, this.hoverTilePosition.y * CELL_SIZE,
+ CELL_SIZE, CELL_SIZE)
+ .endFill();
+ if (this.hoverRack.visible) {
+ this.hoverRack.x = this.hoverTilePosition.x * CELL_SIZE;
+ this.hoverRack.y = this.hoverTilePosition.y * CELL_SIZE;
+ } else if (this.hoverPSU.visible) {
+ this.hoverPSU.x = this.hoverTilePosition.x * CELL_SIZE;
+ this.hoverPSU.y = this.hoverTilePosition.y * CELL_SIZE;
+ } else if (this.hoverCoolingItem.visible) {
+ this.hoverCoolingItem.x = this.hoverTilePosition.x * CELL_SIZE;
+ this.hoverCoolingItem.y = this.hoverTilePosition.y * CELL_SIZE;
+ }
+ }
+
+ /**
+ * Performs the initial drawing action.
+ */
+ public initialDraw(): void {
+ this.container.removeAllChildren();
+
+ this.hoverTile = new createjs.Shape();
+
+ this.hoverTilePosition = {x: 0, y: 0};
+
+ this.hoverTile = MapView.drawRectangle(this.hoverTilePosition, Colors.ROOM_HOVER_VALID, this.container);
+ this.hoverTile.visible = false;
+
+ this.hoverRack = DCObjectLayer.drawHoverRack(this.hoverTilePosition);
+ this.hoverPSU = DCObjectLayer.drawHoverPSU(this.hoverTilePosition);
+ this.hoverCoolingItem = DCObjectLayer.drawHoverCoolingItem(this.hoverTilePosition);
+
+ this.container.addChild(this.hoverRack);
+ this.container.addChild(this.hoverPSU);
+ this.container.addChild(this.hoverCoolingItem);
+
+ this.hoverRack.visible = false;
+ this.hoverPSU.visible = false;
+ this.hoverCoolingItem.visible = false;
+ }
+
+ /**
+ * Sets the hover tile visibility to true/false.
+ *
+ * @param value The visibility value
+ */
+ public setHoverTileVisibility(value: boolean): void {
+ this.hoverTile.visible = value;
+ this.mapView.updateScene = true;
+ }
+
+ /**
+ * Sets the hover item visibility to true/false.
+ *
+ * @param value The visibility value
+ * @param type The type of the object to be shown
+ */
+ public setHoverItemVisibility(value: boolean, type?: string): void {
+ if (value === true) {
+ this.hoverTile.visible = true;
+
+ this.setHoverItemVisibilities(type);
+ } else {
+ this.hoverTile.visible = false;
+ this.hoverRack.visible = false;
+ this.hoverPSU.visible = false;
+ this.hoverCoolingItem.visible = false;
+ }
+
+ this.mapView.updateScene = true;
+ }
+
+ private setHoverItemVisibilities(type: string): void {
+ if (type === "RACK") {
+ this.hoverRack.visible = true;
+ this.hoverPSU.visible = false;
+ this.hoverCoolingItem.visible = false;
+ } else if (type === "PSU") {
+ this.hoverRack.visible = false;
+ this.hoverPSU.visible = true;
+ this.hoverCoolingItem.visible = false;
+ } else if (type === "COOLING_ITEM") {
+ this.hoverRack.visible = false;
+ this.hoverPSU.visible = false;
+ this.hoverCoolingItem.visible = true;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/scripts/views/layers/layer.ts b/src/scripts/views/layers/layer.ts
new file mode 100644
index 00000000..5e5295ac
--- /dev/null
+++ b/src/scripts/views/layers/layer.ts
@@ -0,0 +1,8 @@
+/**
+ * Interface for a subview, representing a layer of the map view.
+ */
+export interface Layer {
+ container: createjs.Container;
+
+ draw(): void;
+}
diff --git a/src/scripts/views/layers/room.ts b/src/scripts/views/layers/room.ts
new file mode 100644
index 00000000..0e31fee0
--- /dev/null
+++ b/src/scripts/views/layers/room.ts
@@ -0,0 +1,177 @@
+import {InteractionLevel} from "../../controllers/mapcontroller";
+import {Util, IntensityLevel} from "../../util";
+import {Colors} from "../../colors";
+import {MapView} from "../mapview";
+import {Layer} from "./layer";
+
+
+/**
+ * Class responsible for rendering the rooms.
+ */
+export class RoomLayer implements Layer {
+ public container: createjs.Container;
+ public coloringMode: boolean;
+ public selectedTiles: ITile[];
+ public selectedTileObjects: TilePositionObject[];
+ public intensityLevels: { [key: number]: IntensityLevel; } = {};
+
+ private mapView: MapView;
+ private allRoomTileObjects: TilePositionObject[];
+ private validNextTilePositions: IGridPosition[];
+
+
+ constructor(mapView: MapView) {
+ this.mapView = mapView;
+ this.container = new createjs.Container();
+
+ this.allRoomTileObjects = [];
+ this.selectedTiles = [];
+ this.validNextTilePositions = [];
+ this.selectedTileObjects = [];
+ this.coloringMode = false;
+
+ this.draw();
+ }
+
+ /**
+ * Draws all rooms to the canvas.
+ */
+ public draw() {
+ this.container.removeAllChildren();
+
+ this.mapView.currentDatacenter.rooms.forEach((room: IRoom) => {
+ let color = Colors.ROOM_DEFAULT;
+
+ if (this.coloringMode && room.roomType === "SERVER" && this.intensityLevels[room.id] !== undefined) {
+ color = Util.convertIntensityToColor(this.intensityLevels[room.id]);
+ }
+
+ room.tiles.forEach((tile: ITile) => {
+ this.allRoomTileObjects.push({
+ position: tile.position,
+ tileObject: MapView.drawRectangle(tile.position, color, this.container)
+ });
+ });
+ });
+ }
+
+ /**
+ * Adds a newly selected tile to the list of selected tiles.
+ *
+ * If the tile was already selected beforehand, it is removed.
+ *
+ * @param tile The tile to be added
+ */
+ public addSelectedTile(tile: ITile): void {
+ this.selectedTiles.push(tile);
+
+ let tileObject = MapView.drawRectangle(tile.position, Colors.ROOM_SELECTED, this.container);
+ this.selectedTileObjects.push({
+ position: {x: tile.position.x, y: tile.position.y},
+ tileObject: tileObject
+ });
+
+ this.validNextTilePositions = Util.deriveValidNextTilePositions(
+ this.mapView.currentDatacenter.rooms, this.selectedTiles);
+
+ this.mapView.updateScene = true;
+ }
+
+ /**
+ * Removes a selected tile (upon being clicked on again).
+ *
+ * @param position The position at which a selected tile should be removed
+ * @param objectIndex The index of the tile in the selectedTileObjects array
+ */
+ public removeSelectedTile(position: IGridPosition, objectIndex: number): void {
+ let index = Util.tileListPositionIndexOf(this.selectedTiles, position);
+
+ // Check whether the given position doesn't belong to an already removed tile
+ if (index === -1) {
+ return;
+ }
+
+ this.selectedTiles.splice(index, 1);
+
+ this.container.removeChild(this.selectedTileObjects[objectIndex].tileObject);
+ this.selectedTileObjects.splice(objectIndex, 1);
+
+ this.validNextTilePositions = Util.deriveValidNextTilePositions(
+ this.mapView.currentDatacenter.rooms, this.selectedTiles);
+
+ this.mapView.updateScene = true;
+ }
+
+ /**
+ * Checks whether a hovered tile is in a valid location.
+ *
+ * @param position The tile location to be checked
+ * @returns {boolean} Whether it is a valid location
+ */
+ public checkHoverTileValidity(position: IGridPosition): boolean {
+ if (this.mapView.mapController.interactionLevel === InteractionLevel.BUILDING) {
+ if (this.selectedTiles.length === 0) {
+ return !Util.checkRoomCollision(this.mapView.currentDatacenter.rooms, position);
+ }
+ return Util.positionListContainsPosition(this.validNextTilePositions, position);
+ } else if (this.mapView.mapController.interactionLevel === InteractionLevel.ROOM) {
+ let valid = false;
+ this.mapView.mapController.roomModeController.currentRoom.tiles.forEach((element: ITile) => {
+ if (position.x === element.position.x && position.y === element.position.y &&
+ element.object === undefined) {
+ valid = true;
+ }
+ });
+ return valid;
+ }
+ }
+
+ /**
+ * Cancels room tile selection by removing all selected tiles from the scene.
+ */
+ public cancelRoomConstruction(): void {
+ if (this.selectedTiles.length === 0) {
+ return;
+ }
+
+ this.selectedTileObjects.forEach((tileObject: TilePositionObject) => {
+ this.container.removeChild(tileObject.tileObject);
+ });
+
+ this.resetTileLists();
+
+ this.mapView.updateScene = true;
+ }
+
+ /**
+ * Finalizes the selected room tiles into a standard room.
+ */
+ public finalizeRoom(room: IRoom): void {
+ if (this.selectedTiles.length === 0) {
+ return;
+ }
+
+ this.mapView.currentDatacenter.rooms.push(room);
+
+ this.resetTileLists();
+
+ // Trigger a redraw
+ this.draw();
+ this.mapView.wallLayer.generateWalls();
+ this.mapView.wallLayer.draw();
+
+ this.mapView.updateScene = true;
+ }
+
+ private resetTileLists(): void {
+ this.selectedTiles = [];
+ this.validNextTilePositions = [];
+ this.selectedTileObjects = [];
+ }
+
+ public setClickable(value: boolean): void {
+ this.allRoomTileObjects.forEach((tileObj: TilePositionObject) => {
+ tileObj.tileObject.cursor = value ? "pointer" : "default";
+ });
+ }
+} \ No newline at end of file
diff --git a/src/scripts/views/layers/roomtext.ts b/src/scripts/views/layers/roomtext.ts
new file mode 100644
index 00000000..65ea0735
--- /dev/null
+++ b/src/scripts/views/layers/roomtext.ts
@@ -0,0 +1,68 @@
+import {MapView} from "../mapview";
+import {Colors} from "../../colors";
+import {Util} from "../../util";
+import {Layer} from "./layer";
+import {CELL_SIZE} from "../../controllers/mapcontroller";
+
+
+export class RoomTextLayer implements Layer {
+ private static TEXT_PADDING = 4;
+
+ public container: createjs.Container;
+
+ private mapView: MapView;
+
+
+ constructor(mapView: MapView) {
+ this.mapView = mapView;
+ this.container = new createjs.Container();
+
+ this.draw();
+ }
+
+ public draw(): void {
+ this.container.removeAllChildren();
+
+ this.mapView.currentDatacenter.rooms.forEach((room: IRoom) => {
+ if (room.name !== "" && room.roomType !== "") {
+ this.renderTextOverlay(room);
+ }
+ });
+ }
+
+ public setVisibility(value: boolean): void {
+ this.mapView.animate(this.container, {alpha: value === true ? 1 : 0});
+ }
+
+ /**
+ * Draws a name and type overlay over the given room.
+ */
+ private renderTextOverlay(room: IRoom): void {
+ if (room.name === null || room.tiles.length === 0) {
+ return;
+ }
+
+ let textPos = Util.calculateRoomNamePosition(room);
+
+ let bottomY = this.renderText(room.name, "12px Arial", textPos,
+ textPos.topLeft.y * CELL_SIZE + RoomTextLayer.TEXT_PADDING);
+ this.renderText("Type: " + Util.toSentenceCase(room.roomType), "10px Arial", textPos, bottomY + 5);
+ }
+
+ private renderText(text: string, font: string, textPos: IRoomNamePos, startY: number): number {
+ let name = new createjs.Text(text, font, Colors.ROOM_NAME_COLOR);
+
+ if (name.getMeasuredWidth() > textPos.length * CELL_SIZE - RoomTextLayer.TEXT_PADDING * 2) {
+ name.scaleX = name.scaleY = (textPos.length * CELL_SIZE - RoomTextLayer.TEXT_PADDING * 2) /
+ name.getMeasuredWidth();
+ }
+
+ // Position the text to the top left of the selected tile
+ name.x = textPos.topLeft.x * CELL_SIZE + RoomTextLayer.TEXT_PADDING;
+ name.y = startY;
+
+ this.container.addChild(name);
+
+ return name.y + name.getMeasuredHeight() * name.scaleY;
+ }
+}
diff --git a/src/scripts/views/layers/wall.ts b/src/scripts/views/layers/wall.ts
new file mode 100644
index 00000000..06ba4675
--- /dev/null
+++ b/src/scripts/views/layers/wall.ts
@@ -0,0 +1,62 @@
+import {Colors} from "../../colors";
+import {MapView} from "../mapview";
+import {Util} from "../../util";
+import {Layer} from "./layer";
+import {CELL_SIZE} from "../../controllers/mapcontroller";
+
+
+/**
+ * Class responsible for rendering the walls.
+ */
+export class WallLayer implements Layer {
+ public container: createjs.Container;
+
+ private mapView: MapView;
+ private walls: IRoomWall[];
+ private wallLineWidth: number;
+
+
+ constructor(mapView: MapView) {
+ this.mapView = mapView;
+ this.container = new createjs.Container();
+ this.wallLineWidth = CELL_SIZE / 20.0;
+
+ this.generateWalls();
+ this.draw();
+ }
+
+ /**
+ * Calls the Util.deriveWallLocations function to generate the wall locations.
+ */
+ public generateWalls(): void {
+ this.walls = Util.deriveWallLocations(this.mapView.currentDatacenter.rooms);
+ }
+
+ /**
+ * Draws all walls to the canvas.
+ */
+ public draw(): void {
+ this.container.removeAllChildren();
+
+ // Draw walls
+ this.walls.forEach((element: IRoomWall) => {
+ if (element.horizontal) {
+ MapView.drawLine(
+ CELL_SIZE * element.startPos[0] - this.wallLineWidth / 2.0,
+ CELL_SIZE * element.startPos[1],
+ CELL_SIZE * (element.startPos[0] + element.length) + this.wallLineWidth / 2.0,
+ CELL_SIZE * element.startPos[1],
+ this.wallLineWidth, Colors.WALL_COLOR, this.container
+ );
+ } else {
+ MapView.drawLine(
+ CELL_SIZE * element.startPos[0],
+ CELL_SIZE * element.startPos[1] - this.wallLineWidth / 2.0,
+ CELL_SIZE * element.startPos[0],
+ CELL_SIZE * (element.startPos[1] + element.length) + this.wallLineWidth / 2.0,
+ this.wallLineWidth, Colors.WALL_COLOR, this.container
+ );
+ }
+ });
+ }
+} \ No newline at end of file
diff --git a/src/scripts/views/mapview.ts b/src/scripts/views/mapview.ts
new file mode 100644
index 00000000..ae7fd5cb
--- /dev/null
+++ b/src/scripts/views/mapview.ts
@@ -0,0 +1,373 @@
+///<reference path="../../../typings/globals/createjs-lib/index.d.ts" />
+///<reference path="../../../typings/globals/easeljs/index.d.ts" />
+///<reference path="../../../typings/globals/tweenjs/index.d.ts" />
+///<reference path="../../../typings/globals/preloadjs/index.d.ts" />
+///<reference path="../definitions.ts" />
+///<reference path="../controllers/mapcontroller.ts" />
+import * as $ from "jquery";
+import {Util} from "../util";
+import {MapController, CELL_SIZE} from "../controllers/mapcontroller";
+import {GridLayer} from "./layers/grid";
+import {RoomLayer} from "./layers/room";
+import {HoverLayer} from "./layers/hover";
+import {WallLayer} from "./layers/wall";
+import {DCObjectLayer} from "./layers/dcobject";
+import {GrayLayer} from "./layers/gray";
+import {RoomTextLayer} from "./layers/roomtext";
+
+
+/**
+ * Class responsible for rendering the map, by delegating the rendering tasks to appropriate instances.
+ */
+export class MapView {
+ public static MAP_SIZE = 100;
+ public static CELL_SIZE_METERS = 0.5;
+ public static MIN_ZOOM = 0.5;
+ public static DEFAULT_ZOOM = 2;
+ public static MAX_ZOOM = 6;
+ public static GAP_CORRECTION_DELTA = 0.2;
+ public static ANIMATION_LENGTH = 250;
+
+ // Models
+ public simulation: ISimulation;
+ public currentDatacenter: IDatacenter;
+
+ // Controllers
+ public mapController: MapController;
+
+ // Canvas objects
+ public stage: createjs.Stage;
+ public mapContainer: createjs.Container;
+
+ // Flag indicating whether the scene should be redrawn
+ public updateScene: boolean;
+ public animating: boolean;
+
+ // Subviews
+ public gridLayer: GridLayer;
+ public roomLayer: RoomLayer;
+ public dcObjectLayer: DCObjectLayer;
+ public roomTextLayer: RoomTextLayer;
+ public hoverLayer: HoverLayer;
+ public wallLayer: WallLayer;
+ public grayLayer: GrayLayer;
+
+ // Dynamic canvas attributes
+ public canvasWidth: number;
+ public canvasHeight: number;
+
+
+ /**
+ * Draws a line from (x1, y1) to (x2, y2).
+ *
+ * @param x1 The x coord. of start point
+ * @param y1 The y coord. of start point
+ * @param x2 The x coord. of end point
+ * @param y2 The y coord. of end point
+ * @param lineWidth The width of the line to be drawn
+ * @param color The color to be used
+ * @param container The container to be drawn to
+ */
+ public static drawLine(x1: number, y1: number, x2: number, y2: number,
+ lineWidth: number, color: string, container: createjs.Container): createjs.Shape {
+ let line = new createjs.Shape();
+ line.graphics.setStrokeStyle(lineWidth).beginStroke(color);
+ line.graphics.moveTo(x1, y1);
+ line.graphics.lineTo(x2, y2);
+ container.addChild(line);
+ return line;
+ }
+
+ /**
+ * Draws a tile at the given location with the given color.
+ *
+ * @param position The grid coordinates of the tile
+ * @param color The color with which the rectangle should be drawn
+ * @param container The container to be drawn to
+ * @param sizeX Optional parameter specifying the width of the tile to be drawn (in grid units)
+ * @param sizeY Optional parameter specifying the height of the tile to be drawn (in grid units)
+ */
+ public static drawRectangle(position: IGridPosition, color: string, container: createjs.Container,
+ sizeX?: number, sizeY?: number): createjs.Shape {
+ let tile = new createjs.Shape();
+ tile.graphics.setStrokeStyle(0);
+ tile.graphics.beginFill(color);
+ tile.graphics.drawRect(
+ position.x * CELL_SIZE - MapView.GAP_CORRECTION_DELTA,
+ position.y * CELL_SIZE - MapView.GAP_CORRECTION_DELTA,
+ CELL_SIZE * (sizeX === undefined ? 1 : sizeX) + MapView.GAP_CORRECTION_DELTA * 2,
+ CELL_SIZE * (sizeY === undefined ? 1 : sizeY) + MapView.GAP_CORRECTION_DELTA * 2
+ );
+ container.addChild(tile);
+ return tile;
+ }
+
+ /**
+ * Draws a tile at the given location with the given color, and add it to the given shape object.
+ *
+ * The fill color must be set beforehand, in order to not set it repeatedly and produce unwanted transparent overlap
+ * artifacts.
+ *
+ * @param position The grid coordinates of the tile
+ * @param shape The shape to be drawn to
+ * @param sizeX Optional parameter specifying the width of the tile to be drawn (in grid units)
+ * @param sizeY Optional parameter specifying the height of the tile to be drawn (in grid units)
+ */
+ public static drawRectangleToShape(position: IGridPosition, shape: createjs.Shape,
+ sizeX?: number, sizeY?: number) {
+ shape.graphics.drawRect(
+ position.x * CELL_SIZE - MapView.GAP_CORRECTION_DELTA,
+ position.y * CELL_SIZE - MapView.GAP_CORRECTION_DELTA,
+ CELL_SIZE * (sizeX === undefined ? 1 : sizeX) + MapView.GAP_CORRECTION_DELTA * 2,
+ CELL_SIZE * (sizeY === undefined ? 1 : sizeY) + MapView.GAP_CORRECTION_DELTA * 2
+ );
+ }
+
+ constructor(simulation: ISimulation, stage: createjs.Stage) {
+ this.simulation = simulation;
+ let path = this.simulation.paths[this.simulation.paths.length - 1];
+ this.currentDatacenter = path.sections[path.sections.length - 1].datacenter;
+
+ this.stage = stage;
+
+ console.log("THE DATA", simulation);
+
+ let canvas = $("#main-canvas");
+ this.canvasWidth = canvas.width();
+ this.canvasHeight = canvas.height();
+
+ this.mapContainer = new createjs.Container();
+
+ this.initializeLayers();
+
+ this.drawMap();
+ this.updateScene = true;
+ this.animating = false;
+
+ this.mapController = new MapController(this);
+
+ // Zoom DC to fit, if rooms are present
+ if (this.currentDatacenter.rooms.length > 0) {
+ this.zoomOutOnDC();
+ }
+
+ // Checks at every rendering tick whether the scene has changed, and updates accordingly
+ createjs.Ticker.addEventListener("tick", (event: createjs.TickerEvent) => {
+ if (this.updateScene || this.animating) {
+ if (this.mapController.isInHoverMode()) {
+ this.hoverLayer.draw();
+ }
+
+ this.updateScene = false;
+ this.stage.update(event);
+ }
+ });
+ }
+
+ private initializeLayers(): void {
+ this.gridLayer = new GridLayer(this);
+ this.roomLayer = new RoomLayer(this);
+ this.dcObjectLayer = new DCObjectLayer(this);
+ this.roomTextLayer = new RoomTextLayer(this);
+ this.hoverLayer = new HoverLayer(this);
+ this.wallLayer = new WallLayer(this);
+ this.grayLayer = new GrayLayer(this);
+ }
+
+ /**
+ * Triggers a redraw and re-population action on all layers.
+ */
+ public redrawMap(): void {
+ this.gridLayer.draw();
+ this.roomLayer.draw();
+ this.dcObjectLayer.populateObjectList();
+ this.dcObjectLayer.draw();
+ this.roomTextLayer.draw();
+ this.hoverLayer.initialDraw();
+ this.wallLayer.generateWalls();
+ this.wallLayer.draw();
+ this.grayLayer.draw(true);
+ this.updateScene = true;
+ }
+
+ /**
+ * Zooms in on a given position with a given amount.
+ *
+ * @param position The position that should appear centered after the zoom action
+ * @param amount The amount of zooming that should be performed
+ */
+ public zoom(position: number[], amount: number): void {
+ const newZoom = this.mapContainer.scaleX + 0.01 * amount;
+
+ // Check whether zooming too far in / out
+ if (newZoom > MapView.MAX_ZOOM ||
+ newZoom < MapView.MIN_ZOOM) {
+ return;
+ }
+
+ // Calculate position difference if zoomed, in order to later compensate for this
+ // unwanted movement
+ let oldPosition = [
+ position[0] - this.mapContainer.x, position[1] - this.mapContainer.y
+ ];
+ let newPosition = [
+ (oldPosition[0] / this.mapContainer.scaleX) * newZoom,
+ (oldPosition[1] / this.mapContainer.scaleX) * newZoom
+ ];
+ let positionDelta = [
+ newPosition[0] - oldPosition[0], newPosition[1] - oldPosition[1]
+ ];
+
+ // Apply the transformation operation to keep the selected position static
+ let newX = this.mapContainer.x - positionDelta[0];
+ let newY = this.mapContainer.y - positionDelta[1];
+
+ let finalPos = this.mapController.checkCanvasMovement(newX, newY, newZoom);
+
+ if (!this.animating) {
+ this.animate(this.mapContainer, {
+ scaleX: newZoom, scaleY: newZoom,
+ x: finalPos.x, y: finalPos.y
+ });
+ }
+ }
+
+ /**
+ * Adjusts the viewing scale to fully display a selected room and center it in view.
+ *
+ * @param room The room to be centered
+ * @param redraw Optional argument specifying whether this is a scene redraw
+ */
+ public zoomInOnRoom(room: IRoom, redraw?: boolean): void {
+ this.zoomInOnRooms([room]);
+
+ if (redraw === undefined || redraw === false) {
+ if (!this.grayLayer.isGrayedOut()) {
+ this.grayLayer.currentRoom = room;
+ this.grayLayer.draw();
+ }
+ }
+
+ this.updateScene = true;
+ }
+
+ /**
+ * Zooms out to global building view.
+ */
+ public zoomOutOnDC(): void {
+ this.grayLayer.clear();
+
+ if (this.currentDatacenter.rooms.length > 0) {
+ this.zoomInOnRooms(this.currentDatacenter.rooms);
+ }
+
+ this.updateScene = true;
+ }
+
+ /**
+ * Fits a given list of rooms to view, by scaling the viewport appropriately and moving the mapContainer.
+ *
+ * @param rooms The array of rooms to be viewed
+ */
+ private zoomInOnRooms(rooms: IRoom[]): void {
+ let bounds = Util.calculateRoomListBounds(rooms);
+ let newScale = this.calculateNewScale(bounds);
+
+ // Coordinates of the center of the room, relative to the global origin of the map
+ let roomCenterCoords = [
+ bounds.center[0] * CELL_SIZE * newScale,
+ bounds.center[1] * CELL_SIZE * newScale
+ ];
+ // Coordinates of the center of the stage (the visible part of the canvas), relative to the global map origin
+ let stageCenterCoords = [
+ -this.mapContainer.x + this.canvasWidth / 2,
+ -this.mapContainer.y + this.canvasHeight / 2
+ ];
+
+ let newX = this.mapContainer.x - roomCenterCoords[0] + stageCenterCoords[0];
+ let newY = this.mapContainer.y - roomCenterCoords[1] + stageCenterCoords[1];
+
+ let newPosition = this.mapController.checkCanvasMovement(newX, newY, newScale);
+
+ this.animate(this.mapContainer, {
+ scaleX: newScale, scaleY: newScale,
+ x: newPosition.x, y: newPosition.y
+ });
+ }
+
+ private calculateNewScale(bounds: IBounds): number {
+ const viewPadding = 30;
+ const sideMenuWidth = 350;
+
+ let width = bounds.max[0] - bounds.min[0];
+ let height = bounds.max[1] - bounds.min[1];
+
+ let scaleX = (this.canvasWidth - 2 * sideMenuWidth) / (width * CELL_SIZE + 2 * viewPadding);
+ let scaleY = this.canvasHeight / (height * CELL_SIZE + 2 * viewPadding);
+
+ let newScale = Math.min(scaleX, scaleY);
+
+ if (this.mapContainer.scaleX > MapView.MAX_ZOOM) {
+ newScale = MapView.MAX_ZOOM;
+ } else if (this.mapContainer.scaleX < MapView.MIN_ZOOM) {
+ newScale = MapView.MIN_ZOOM;
+ }
+
+ return newScale;
+ }
+
+ /**
+ * Draws all tiles contained in the MapModel.
+ */
+ private drawMap(): void {
+ // Create and draw the container for the entire map
+ let gridPixelSize = CELL_SIZE * MapView.MAP_SIZE;
+
+ // Add a white background to the entire container
+ let background = new createjs.Shape();
+ background.graphics.beginFill("#fff");
+ background.graphics.drawRect(0, 0,
+ gridPixelSize, gridPixelSize);
+ this.mapContainer.addChild(background);
+
+ this.stage.addChild(this.mapContainer);
+
+ // Set the map container to a default offset and zoom state (overridden if rooms are present)
+ this.mapContainer.x = -50;
+ this.mapContainer.y = -50;
+ this.mapContainer.scaleX = this.mapContainer.scaleY = MapView.DEFAULT_ZOOM;
+
+ this.addLayerContainers();
+ }
+
+ private addLayerContainers(): void {
+ this.mapContainer.addChild(this.gridLayer.container);
+ this.mapContainer.addChild(this.roomLayer.container);
+ this.mapContainer.addChild(this.dcObjectLayer.container);
+ this.mapContainer.addChild(this.roomTextLayer.container);
+ this.mapContainer.addChild(this.hoverLayer.container);
+ this.mapContainer.addChild(this.wallLayer.container);
+ this.mapContainer.addChild(this.grayLayer.container);
+ }
+
+ /**
+ * Wrapper function for TweenJS animate functionality.
+ *
+ * @param target What to animate
+ * @param properties Properties to be passed on to TweenJS
+ * @param callback To be called when animation ready
+ */
+ public animate(target: any, properties: any, callback?: () => any): void {
+ this.animating = true;
+ createjs.Tween.get(target)
+ .to(properties, MapView.ANIMATION_LENGTH, createjs.Ease.getPowInOut(4))
+ .call(() => {
+ this.animating = false;
+ this.updateScene = true;
+
+ if (callback !== undefined) {
+ callback();
+ }
+ });
+ }
+} \ No newline at end of file