summaryrefslogtreecommitdiff
path: root/src/scripts/controllers/mapcontroller.ts
diff options
context:
space:
mode:
authorGeorgios Andreadis <g.andreadis@student.tudelft.nl>2017-01-24 12:06:09 +0100
committerGeorgios Andreadis <g.andreadis@student.tudelft.nl>2017-01-24 12:06:09 +0100
commitc96e6ffafb62bde1e08987b1fdf3c0786487f6ec (patch)
tree37eaf4cf199ca77dc131b4212c526b707adf2e30 /src/scripts/controllers/mapcontroller.ts
Initial commit
Diffstat (limited to 'src/scripts/controllers/mapcontroller.ts')
-rw-r--r--src/scripts/controllers/mapcontroller.ts520
1 files changed, 520 insertions, 0 deletions
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";
+ }
+}