diff options
| author | Georgios Andreadis <g.andreadis@student.tudelft.nl> | 2017-01-24 12:06:09 +0100 |
|---|---|---|
| committer | Georgios Andreadis <g.andreadis@student.tudelft.nl> | 2017-01-24 12:06:09 +0100 |
| commit | c96e6ffafb62bde1e08987b1fdf3c0786487f6ec (patch) | |
| tree | 37eaf4cf199ca77dc131b4212c526b707adf2e30 /src/scripts/controllers/mapcontroller.ts | |
Initial commit
Diffstat (limited to 'src/scripts/controllers/mapcontroller.ts')
| -rw-r--r-- | src/scripts/controllers/mapcontroller.ts | 520 |
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"; + } +} |
