summaryrefslogtreecommitdiff
path: root/src/scripts/controllers/modes
diff options
context:
space:
mode:
Diffstat (limited to 'src/scripts/controllers/modes')
-rw-r--r--src/scripts/controllers/modes/building.ts114
-rw-r--r--src/scripts/controllers/modes/node.ts297
-rw-r--r--src/scripts/controllers/modes/object.ts297
-rw-r--r--src/scripts/controllers/modes/room.ts382
4 files changed, 1090 insertions, 0 deletions
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