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