1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
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 {
const 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 {
const 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;
const 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);
const 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
const oldPosition = [
position[0] - this.mapContainer.x, position[1] - this.mapContainer.y
];
const newPosition = [
(oldPosition[0] / this.mapContainer.scaleX) * newZoom,
(oldPosition[1] / this.mapContainer.scaleX) * newZoom
];
const positionDelta = [
newPosition[0] - oldPosition[0], newPosition[1] - oldPosition[1]
];
// Apply the transformation operation to keep the selected position static
const newX = this.mapContainer.x - positionDelta[0];
const newY = this.mapContainer.y - positionDelta[1];
const 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 {
const bounds = Util.calculateRoomListBounds(rooms);
const newScale = this.calculateNewScale(bounds);
// Coordinates of the center of the room, relative to the global origin of the map
const 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
const stageCenterCoords = [
-this.mapContainer.x + this.canvasWidth / 2,
-this.mapContainer.y + this.canvasHeight / 2
];
const newX = this.mapContainer.x - roomCenterCoords[0] + stageCenterCoords[0];
const newY = this.mapContainer.y - roomCenterCoords[1] + stageCenterCoords[1];
const 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;
const width = bounds.max[0] - bounds.min[0];
const height = bounds.max[1] - bounds.min[1];
const scaleX = (this.canvasWidth - 2 * sideMenuWidth) / (width * CELL_SIZE + 2 * viewPadding);
const 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
const gridPixelSize = CELL_SIZE * MapView.MAP_SIZE;
// Add a white background to the entire container
const 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();
}
});
}
}
|