///
///
///
///
///
///
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();
}
});
}
}