diff options
Diffstat (limited to 'src/scripts/views/mapview.ts')
| -rw-r--r-- | src/scripts/views/mapview.ts | 373 |
1 files changed, 373 insertions, 0 deletions
diff --git a/src/scripts/views/mapview.ts b/src/scripts/views/mapview.ts new file mode 100644 index 00000000..ae7fd5cb --- /dev/null +++ b/src/scripts/views/mapview.ts @@ -0,0 +1,373 @@ +///<reference path="../../../typings/globals/createjs-lib/index.d.ts" /> +///<reference path="../../../typings/globals/easeljs/index.d.ts" /> +///<reference path="../../../typings/globals/tweenjs/index.d.ts" /> +///<reference path="../../../typings/globals/preloadjs/index.d.ts" /> +///<reference path="../definitions.ts" /> +///<reference path="../controllers/mapcontroller.ts" /> +import * as $ from "jquery"; +import {Util} from "../util"; +import {MapController, CELL_SIZE} from "../controllers/mapcontroller"; +import {GridLayer} from "./layers/grid"; +import {RoomLayer} from "./layers/room"; +import {HoverLayer} from "./layers/hover"; +import {WallLayer} from "./layers/wall"; +import {DCObjectLayer} from "./layers/dcobject"; +import {GrayLayer} from "./layers/gray"; +import {RoomTextLayer} from "./layers/roomtext"; + + +/** + * Class responsible for rendering the map, by delegating the rendering tasks to appropriate instances. + */ +export class MapView { + public static MAP_SIZE = 100; + public static CELL_SIZE_METERS = 0.5; + public static MIN_ZOOM = 0.5; + public static DEFAULT_ZOOM = 2; + public static MAX_ZOOM = 6; + public static GAP_CORRECTION_DELTA = 0.2; + public static ANIMATION_LENGTH = 250; + + // Models + public simulation: ISimulation; + public currentDatacenter: IDatacenter; + + // Controllers + public mapController: MapController; + + // Canvas objects + public stage: createjs.Stage; + public mapContainer: createjs.Container; + + // Flag indicating whether the scene should be redrawn + public updateScene: boolean; + public animating: boolean; + + // Subviews + public gridLayer: GridLayer; + public roomLayer: RoomLayer; + public dcObjectLayer: DCObjectLayer; + public roomTextLayer: RoomTextLayer; + public hoverLayer: HoverLayer; + public wallLayer: WallLayer; + public grayLayer: GrayLayer; + + // Dynamic canvas attributes + public canvasWidth: number; + public canvasHeight: number; + + + /** + * Draws a line from (x1, y1) to (x2, y2). + * + * @param x1 The x coord. of start point + * @param y1 The y coord. of start point + * @param x2 The x coord. of end point + * @param y2 The y coord. of end point + * @param lineWidth The width of the line to be drawn + * @param color The color to be used + * @param container The container to be drawn to + */ + public static drawLine(x1: number, y1: number, x2: number, y2: number, + lineWidth: number, color: string, container: createjs.Container): createjs.Shape { + let line = new createjs.Shape(); + line.graphics.setStrokeStyle(lineWidth).beginStroke(color); + line.graphics.moveTo(x1, y1); + line.graphics.lineTo(x2, y2); + container.addChild(line); + return line; + } + + /** + * Draws a tile at the given location with the given color. + * + * @param position The grid coordinates of the tile + * @param color The color with which the rectangle should be drawn + * @param container The container to be drawn to + * @param sizeX Optional parameter specifying the width of the tile to be drawn (in grid units) + * @param sizeY Optional parameter specifying the height of the tile to be drawn (in grid units) + */ + public static drawRectangle(position: IGridPosition, color: string, container: createjs.Container, + sizeX?: number, sizeY?: number): createjs.Shape { + let tile = new createjs.Shape(); + tile.graphics.setStrokeStyle(0); + tile.graphics.beginFill(color); + tile.graphics.drawRect( + position.x * CELL_SIZE - MapView.GAP_CORRECTION_DELTA, + position.y * CELL_SIZE - MapView.GAP_CORRECTION_DELTA, + CELL_SIZE * (sizeX === undefined ? 1 : sizeX) + MapView.GAP_CORRECTION_DELTA * 2, + CELL_SIZE * (sizeY === undefined ? 1 : sizeY) + MapView.GAP_CORRECTION_DELTA * 2 + ); + container.addChild(tile); + return tile; + } + + /** + * Draws a tile at the given location with the given color, and add it to the given shape object. + * + * The fill color must be set beforehand, in order to not set it repeatedly and produce unwanted transparent overlap + * artifacts. + * + * @param position The grid coordinates of the tile + * @param shape The shape to be drawn to + * @param sizeX Optional parameter specifying the width of the tile to be drawn (in grid units) + * @param sizeY Optional parameter specifying the height of the tile to be drawn (in grid units) + */ + public static drawRectangleToShape(position: IGridPosition, shape: createjs.Shape, + sizeX?: number, sizeY?: number) { + shape.graphics.drawRect( + position.x * CELL_SIZE - MapView.GAP_CORRECTION_DELTA, + position.y * CELL_SIZE - MapView.GAP_CORRECTION_DELTA, + CELL_SIZE * (sizeX === undefined ? 1 : sizeX) + MapView.GAP_CORRECTION_DELTA * 2, + CELL_SIZE * (sizeY === undefined ? 1 : sizeY) + MapView.GAP_CORRECTION_DELTA * 2 + ); + } + + constructor(simulation: ISimulation, stage: createjs.Stage) { + this.simulation = simulation; + let path = this.simulation.paths[this.simulation.paths.length - 1]; + this.currentDatacenter = path.sections[path.sections.length - 1].datacenter; + + this.stage = stage; + + console.log("THE DATA", simulation); + + let canvas = $("#main-canvas"); + this.canvasWidth = canvas.width(); + this.canvasHeight = canvas.height(); + + this.mapContainer = new createjs.Container(); + + this.initializeLayers(); + + this.drawMap(); + this.updateScene = true; + this.animating = false; + + this.mapController = new MapController(this); + + // Zoom DC to fit, if rooms are present + if (this.currentDatacenter.rooms.length > 0) { + this.zoomOutOnDC(); + } + + // Checks at every rendering tick whether the scene has changed, and updates accordingly + createjs.Ticker.addEventListener("tick", (event: createjs.TickerEvent) => { + if (this.updateScene || this.animating) { + if (this.mapController.isInHoverMode()) { + this.hoverLayer.draw(); + } + + this.updateScene = false; + this.stage.update(event); + } + }); + } + + private initializeLayers(): void { + this.gridLayer = new GridLayer(this); + this.roomLayer = new RoomLayer(this); + this.dcObjectLayer = new DCObjectLayer(this); + this.roomTextLayer = new RoomTextLayer(this); + this.hoverLayer = new HoverLayer(this); + this.wallLayer = new WallLayer(this); + this.grayLayer = new GrayLayer(this); + } + + /** + * Triggers a redraw and re-population action on all layers. + */ + public redrawMap(): void { + this.gridLayer.draw(); + this.roomLayer.draw(); + this.dcObjectLayer.populateObjectList(); + this.dcObjectLayer.draw(); + this.roomTextLayer.draw(); + this.hoverLayer.initialDraw(); + this.wallLayer.generateWalls(); + this.wallLayer.draw(); + this.grayLayer.draw(true); + this.updateScene = true; + } + + /** + * Zooms in on a given position with a given amount. + * + * @param position The position that should appear centered after the zoom action + * @param amount The amount of zooming that should be performed + */ + public zoom(position: number[], amount: number): void { + const newZoom = this.mapContainer.scaleX + 0.01 * amount; + + // Check whether zooming too far in / out + if (newZoom > MapView.MAX_ZOOM || + newZoom < MapView.MIN_ZOOM) { + return; + } + + // Calculate position difference if zoomed, in order to later compensate for this + // unwanted movement + let oldPosition = [ + position[0] - this.mapContainer.x, position[1] - this.mapContainer.y + ]; + let newPosition = [ + (oldPosition[0] / this.mapContainer.scaleX) * newZoom, + (oldPosition[1] / this.mapContainer.scaleX) * newZoom + ]; + let positionDelta = [ + newPosition[0] - oldPosition[0], newPosition[1] - oldPosition[1] + ]; + + // Apply the transformation operation to keep the selected position static + let newX = this.mapContainer.x - positionDelta[0]; + let newY = this.mapContainer.y - positionDelta[1]; + + let finalPos = this.mapController.checkCanvasMovement(newX, newY, newZoom); + + if (!this.animating) { + this.animate(this.mapContainer, { + scaleX: newZoom, scaleY: newZoom, + x: finalPos.x, y: finalPos.y + }); + } + } + + /** + * Adjusts the viewing scale to fully display a selected room and center it in view. + * + * @param room The room to be centered + * @param redraw Optional argument specifying whether this is a scene redraw + */ + public zoomInOnRoom(room: IRoom, redraw?: boolean): void { + this.zoomInOnRooms([room]); + + if (redraw === undefined || redraw === false) { + if (!this.grayLayer.isGrayedOut()) { + this.grayLayer.currentRoom = room; + this.grayLayer.draw(); + } + } + + this.updateScene = true; + } + + /** + * Zooms out to global building view. + */ + public zoomOutOnDC(): void { + this.grayLayer.clear(); + + if (this.currentDatacenter.rooms.length > 0) { + this.zoomInOnRooms(this.currentDatacenter.rooms); + } + + this.updateScene = true; + } + + /** + * Fits a given list of rooms to view, by scaling the viewport appropriately and moving the mapContainer. + * + * @param rooms The array of rooms to be viewed + */ + private zoomInOnRooms(rooms: IRoom[]): void { + let bounds = Util.calculateRoomListBounds(rooms); + let newScale = this.calculateNewScale(bounds); + + // Coordinates of the center of the room, relative to the global origin of the map + let roomCenterCoords = [ + bounds.center[0] * CELL_SIZE * newScale, + bounds.center[1] * CELL_SIZE * newScale + ]; + // Coordinates of the center of the stage (the visible part of the canvas), relative to the global map origin + let stageCenterCoords = [ + -this.mapContainer.x + this.canvasWidth / 2, + -this.mapContainer.y + this.canvasHeight / 2 + ]; + + let newX = this.mapContainer.x - roomCenterCoords[0] + stageCenterCoords[0]; + let newY = this.mapContainer.y - roomCenterCoords[1] + stageCenterCoords[1]; + + let newPosition = this.mapController.checkCanvasMovement(newX, newY, newScale); + + this.animate(this.mapContainer, { + scaleX: newScale, scaleY: newScale, + x: newPosition.x, y: newPosition.y + }); + } + + private calculateNewScale(bounds: IBounds): number { + const viewPadding = 30; + const sideMenuWidth = 350; + + let width = bounds.max[0] - bounds.min[0]; + let height = bounds.max[1] - bounds.min[1]; + + let scaleX = (this.canvasWidth - 2 * sideMenuWidth) / (width * CELL_SIZE + 2 * viewPadding); + let scaleY = this.canvasHeight / (height * CELL_SIZE + 2 * viewPadding); + + let newScale = Math.min(scaleX, scaleY); + + if (this.mapContainer.scaleX > MapView.MAX_ZOOM) { + newScale = MapView.MAX_ZOOM; + } else if (this.mapContainer.scaleX < MapView.MIN_ZOOM) { + newScale = MapView.MIN_ZOOM; + } + + return newScale; + } + + /** + * Draws all tiles contained in the MapModel. + */ + private drawMap(): void { + // Create and draw the container for the entire map + let gridPixelSize = CELL_SIZE * MapView.MAP_SIZE; + + // Add a white background to the entire container + let background = new createjs.Shape(); + background.graphics.beginFill("#fff"); + background.graphics.drawRect(0, 0, + gridPixelSize, gridPixelSize); + this.mapContainer.addChild(background); + + this.stage.addChild(this.mapContainer); + + // Set the map container to a default offset and zoom state (overridden if rooms are present) + this.mapContainer.x = -50; + this.mapContainer.y = -50; + this.mapContainer.scaleX = this.mapContainer.scaleY = MapView.DEFAULT_ZOOM; + + this.addLayerContainers(); + } + + private addLayerContainers(): void { + this.mapContainer.addChild(this.gridLayer.container); + this.mapContainer.addChild(this.roomLayer.container); + this.mapContainer.addChild(this.dcObjectLayer.container); + this.mapContainer.addChild(this.roomTextLayer.container); + this.mapContainer.addChild(this.hoverLayer.container); + this.mapContainer.addChild(this.wallLayer.container); + this.mapContainer.addChild(this.grayLayer.container); + } + + /** + * Wrapper function for TweenJS animate functionality. + * + * @param target What to animate + * @param properties Properties to be passed on to TweenJS + * @param callback To be called when animation ready + */ + public animate(target: any, properties: any, callback?: () => any): void { + this.animating = true; + createjs.Tween.get(target) + .to(properties, MapView.ANIMATION_LENGTH, createjs.Ease.getPowInOut(4)) + .call(() => { + this.animating = false; + this.updateScene = true; + + if (callback !== undefined) { + callback(); + } + }); + } +}
\ No newline at end of file |
