summaryrefslogtreecommitdiff
path: root/src
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
Initial commit
Diffstat (limited to 'src')
-rw-r--r--src/404.html26
-rw-r--r--src/app.html457
-rw-r--r--src/favicon.icobin0 -> 99678 bytes
-rw-r--r--src/humans.txt27
-rw-r--r--src/img/app/coolingitem.pngbin0 -> 2853 bytes
-rw-r--r--src/img/app/loading.gifbin0 -> 36550 bytes
-rw-r--r--src/img/app/node-cpu.pngbin0 -> 4062 bytes
-rw-r--r--src/img/app/node-gpu.pngbin0 -> 2227 bytes
-rw-r--r--src/img/app/node-memory.pngbin0 -> 1980 bytes
-rw-r--r--src/img/app/node-network.pngbin0 -> 3058 bytes
-rw-r--r--src/img/app/node-storage.pngbin0 -> 4038 bytes
-rw-r--r--src/img/app/psu.pngbin0 -> 1523 bytes
-rw-r--r--src/img/app/rack-energy.pngbin0 -> 893 bytes
-rw-r--r--src/img/app/rack-space.pngbin0 -> 957 bytes
-rw-r--r--src/img/datacenter-drawing.pngbin0 -> 219576 bytes
-rw-r--r--src/img/email-icon.pngbin0 -> 14761 bytes
-rw-r--r--src/img/favicon.pngbin0 -> 2788 bytes
-rw-r--r--src/img/github-icon.pngbin0 -> 6441 bytes
-rw-r--r--src/img/logo.pngbin0 -> 2825 bytes
-rw-r--r--src/img/mockups/construction-node.pngbin0 -> 71201 bytes
-rw-r--r--src/img/mockups/simulation-node.pngbin0 -> 63291 bytes
-rw-r--r--src/img/mockups/simulation-room.pngbin0 -> 45941 bytes
-rw-r--r--src/img/opendc-splash.pngbin0 -> 304805 bytes
-rw-r--r--src/img/portraits/aiosup.pngbin0 -> 111629 bytes
-rw-r--r--src/img/portraits/gandreadis.pngbin0 -> 118477 bytes
-rw-r--r--src/img/portraits/loverweel.pngbin0 -> 107768 bytes
-rw-r--r--src/img/portraits/mbijman.pngbin0 -> 111670 bytes
-rw-r--r--src/img/stakeholders/Developer.pngbin0 -> 11411 bytes
-rw-r--r--src/img/stakeholders/Manager.pngbin0 -> 9946 bytes
-rw-r--r--src/img/stakeholders/Researcher.pngbin0 -> 10984 bytes
-rw-r--r--src/img/stakeholders/Sales.pngbin0 -> 10074 bytes
-rw-r--r--src/img/stakeholders/Student.pngbin0 -> 12960 bytes
-rw-r--r--src/img/technologies/arrow.pngbin0 -> 2153 bytes
-rw-r--r--src/img/technologies/cogs-icon.pngbin0 -> 11500 bytes
-rw-r--r--src/img/technologies/database-icon.pngbin0 -> 7848 bytes
-rw-r--r--src/img/technologies/webserver-icon.pngbin0 -> 5762 bytes
-rw-r--r--src/img/technologies/www-icon.pngbin0 -> 11205 bytes
-rw-r--r--src/img/tudelfticon.pngbin0 -> 4387 bytes
-rw-r--r--src/index.html411
-rw-r--r--src/navbar.html18
-rw-r--r--src/profile.html63
-rw-r--r--src/projects.html94
-rw-r--r--src/robots.txt4
-rw-r--r--src/scripts/colors.ts43
-rw-r--r--src/scripts/controllers/connection/api.ts1724
-rw-r--r--src/scripts/controllers/connection/cache.ts85
-rw-r--r--src/scripts/controllers/connection/socket.ts76
-rw-r--r--src/scripts/controllers/mapcontroller.ts520
-rw-r--r--src/scripts/controllers/modes/building.ts114
-rw-r--r--src/scripts/controllers/modes/node.ts297
-rw-r--r--src/scripts/controllers/modes/object.ts297
-rw-r--r--src/scripts/controllers/modes/room.ts382
-rw-r--r--src/scripts/controllers/scaleindicator.ts45
-rw-r--r--src/scripts/controllers/simulation/chart.ts241
-rw-r--r--src/scripts/controllers/simulation/statecache.ts205
-rw-r--r--src/scripts/controllers/simulation/taskview.ts64
-rw-r--r--src/scripts/controllers/simulation/timeline.ts161
-rw-r--r--src/scripts/controllers/simulationcontroller.ts586
-rw-r--r--src/scripts/definitions.ts318
-rw-r--r--src/scripts/error404.entry.ts26
-rw-r--r--src/scripts/main.entry.ts69
-rw-r--r--src/scripts/profile.entry.ts40
-rw-r--r--src/scripts/projects.entry.ts651
-rw-r--r--src/scripts/serverconnection.ts59
-rw-r--r--src/scripts/splash.entry.ts160
-rw-r--r--src/scripts/tests/util.spec.ts326
-rw-r--r--src/scripts/user.ts76
-rw-r--r--src/scripts/util.ts600
-rw-r--r--src/scripts/views/layers/dcobject.ts252
-rw-r--r--src/scripts/views/layers/dcprogressbar.ts99
-rw-r--r--src/scripts/views/layers/gray.ts145
-rw-r--r--src/scripts/views/layers/grid.ts59
-rw-r--r--src/scripts/views/layers/hover.ts129
-rw-r--r--src/scripts/views/layers/layer.ts8
-rw-r--r--src/scripts/views/layers/room.ts177
-rw-r--r--src/scripts/views/layers/roomtext.ts68
-rw-r--r--src/scripts/views/layers/wall.ts62
-rw-r--r--src/scripts/views/mapview.ts373
-rw-r--r--src/styles/404.less147
-rw-r--r--src/styles/main.less1190
-rw-r--r--src/styles/navbar.less158
-rw-r--r--src/styles/profile.less22
-rw-r--r--src/styles/projects.less391
-rw-r--r--src/styles/splash.less436
-rw-r--r--src/unit-tests.html15
85 files changed, 11996 insertions, 0 deletions
diff --git a/src/404.html b/src/404.html
new file mode 100644
index 00000000..a7c0771d
--- /dev/null
+++ b/src/404.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>404 Error | OpenDC</title>
+
+ <link href="styles/404.css" rel="stylesheet" type="text/css">
+</head>
+<body>
+<div class="terminal-window">
+ <div class="terminal-header">Terminal -- bash</div>
+ <div class="terminal-body">
+ <div class="segfault">$ status <br>
+ opendc[4264]: segfault at 0000051497be459d1 err 12 in libopendc.9.0.4<br>
+ opendc[4269]: segfault at 000004234855fc2db err 3 in libopendc.9.0.4<br>
+ opendc[4270]: STDERR Page does not exist
+ </div>
+ <div class="code-block"></div>
+ <div class="sub-title">Got lost?<span class="cursor">_</span></div>
+ <a class="home-btn" href="https://opendc.ewi.tudelft.nl">GET ME BACK TO OPENDC</a>
+ </div>
+</div>
+
+<script src="scripts/error404.entry.js"></script>
+</body>
+</html> \ No newline at end of file
diff --git a/src/app.html b/src/app.html
new file mode 100644
index 00000000..c267a354
--- /dev/null
+++ b/src/app.html
@@ -0,0 +1,457 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="google-signin-client_id"
+ content="184853849394-v89e96desio4dub3360vg32p1l4r3jqd.apps.googleusercontent.com">
+
+ <title>OpenDC</title>
+
+ <link href="bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
+ <link href="bower_components/c3/c3.min.css" rel="stylesheet">
+
+ <link href="styles/navbar.css" rel="stylesheet" type="text/css">
+ <link href="styles/main.css" rel="stylesheet" type="text/css">
+
+ <link href="img/logo.png" rel="icon">
+</head>
+<body>
+<!-- build:include navbar.html -->
+<!-- /build -->
+<div class="app-content">
+ <!-- The main map canvas (the dimensions given are fallbacks, for when dynamic resizing doesn't work)-->
+ <canvas id="main-canvas" width="800" height="600">
+ Sorry, but it seems your browser does not support the HTML5 canvas.
+ </canvas>
+
+ <div class="side-menu-container right-middle-side">
+ <div class="menu-container level-menu hidden" id="node-menu">
+ <div class="menu-header-bar">
+ Node
+ <button class="menu-exit btn btn-default btn-circle">
+ <i class="glyphicon glyphicon-remove"></i>
+ </button>
+ <button class="menu-collapse btn btn-default btn-circle">
+ <i class="glyphicon glyphicon-minus"></i>
+ </button>
+ </div>
+ <div class="menu-body construction">
+ <ul class="nav nav-tabs">
+ <li class="active">
+ <a data-toggle="tab" href="#cpu-tab">
+ <img src="img/app/node-cpu.png" alt="CPU tab">
+ </a>
+ </li>
+ <li>
+ <a data-toggle="tab" href="#gpu-tab">
+ <img src="img/app/node-gpu.png" alt="GPU tab">
+ </a>
+ </li>
+ <li>
+ <a data-toggle="tab" href="#memory-tab">
+ <img src="img/app/node-memory.png" alt="Memory unit tab">
+ </a>
+ </li>
+ <li>
+ <a data-toggle="tab" href="#storage-tab">
+ <img src="img/app/node-storage.png" alt="Storage unit tab">
+ </a>
+ </li>
+ <li>
+ <a data-toggle="tab" href="#network-tab">
+ <img src="img/app/node-network.png" alt="Network unit tab">
+ </a>
+ </li>
+ </ul>
+ <div class="tab-content">
+ <div id="cpu-tab" class="tab-pane fade in active">
+ <h3>CPUs</h3>
+ <h4>Add a new CPU</h4>
+ <div class="unit-add-input input-group" id="add-cpu-form">
+ <select class="form-control" title="Generation">
+ <!-- Populated at runtime with a list of available units -->
+ </select>
+ <span class="input-group-btn"><button class="btn btn-default add-unit">Add</button></span>
+ </div>
+ <div class="panel-group" id="cpu-accordion">
+ <!-- Populated at runtime with CPU info -->
+ </div>
+ </div>
+ <div id="gpu-tab" class="tab-pane fade">
+ <h3>GPUs</h3>
+ <h4>Add a new GPU</h4>
+ <div class="unit-add-input input-group" id="add-gpu-form">
+ <select class="form-control" title="Generation">
+ <!-- Populated at runtime with a list of available units -->
+ </select>
+ <span class="input-group-btn"><button class="btn btn-default add-unit">Add</button></span>
+ </div>
+ <div class="panel-group" id="gpu-accordion">
+ <!-- Populated at runtime with GPU info -->
+ </div>
+ </div>
+ <div id="memory-tab" class="tab-pane fade">
+ <h3>Memory Units</h3>
+ <h4>Add a new memory unit</h4>
+ <div class="unit-add-input input-group" id="add-memory-form">
+ <select class="form-control" title="Generation">
+ <!-- Populated at runtime with a list of available units -->
+ </select>
+ <span class="input-group-btn"><button class="btn btn-default add-unit">Add</button></span>
+ </div>
+ <div class="panel-group" id="memory-accordion">
+ <!-- Populated at runtime with memory info -->
+ </div>
+ </div>
+ <div id="storage-tab" class="tab-pane fade">
+ <h3>Storage Units</h3>
+ <h4>Add a new storage unit</h4>
+ <div class="unit-add-input input-group" id="add-storage-form">
+ <select class="form-control" title="Generation">
+ <!-- Populated at runtime with a list of available units -->
+ </select>
+ <span class="input-group-btn"><button class="btn btn-default add-unit">Add</button></span>
+ </div>
+ <div class="panel-group" id="storage-accordion">
+ <!-- Populated at runtime with storage info -->
+ </div>
+ </div>
+ <div id="network-tab" class="tab-pane fade">
+ <h3>Network cards</h3>
+ <div>This machine has a standard, industry-grade network unit</div>
+ </div>
+ </div>
+ </div>
+ <div class="menu-body simulation">
+ <div id="machine-statistics-chart"></div>
+ </div>
+ </div>
+ </div>
+
+ <div class="side-menu-container right-side">
+ <div class="menu-container level-menu" id="building-menu">
+ <div class="menu-header-bar">
+ Building
+ <button type="button" class="menu-collapse btn btn-default btn-circle">
+ <i class="glyphicon glyphicon-minus"></i>
+ </button>
+ </div>
+ <div class="menu-body construction">
+ <div class="btn btn-primary btn-block" id="room-construction">
+ Construct new room
+ </div>
+ <div class="btn btn-default btn-block" id="room-construction-cancel">
+ Cancel
+ </div>
+ </div>
+ <div class="menu-body simulation">
+ <div class="building-stats-list">
+ <!-- Populated at runtime with dynamic simulation stats -->
+ </div>
+ </div>
+ </div>
+
+ <div class="menu-container level-menu hidden" id="room-menu">
+ <div class="menu-header-bar">
+ Room
+ <button type="button" class="menu-collapse btn btn-default btn-circle">
+ <i class="glyphicon glyphicon-minus"></i>
+ </button>
+ </div>
+ <div class="menu-body construction">
+ <div class="input-group">
+ <input type="text" class="form-control" id="room-name-input" placeholder="Room name...">
+ <span class="input-group-btn"><button class="btn btn-default" id="room-name-save"
+ type="button">Save</button></span>
+ </div>
+ <div class="input-group">
+ <div class="input-group-addon">Room type:</div>
+ <select class="form-control" title="Room Type" id="roomtype-select">
+ <!-- Populated at runtime with all available room types -->
+ </select>
+ </div>
+ <label id="add-objects-label">Add objects:</label>
+ <div class="dc-component-container hidden" id="add-rack-btn" data-active="false">
+ <div class="dc-component dc-rack"></div>
+ <div class="dc-component-label">Rack</div>
+ </div>
+ <div class="dc-component-container hidden" id="add-psu-btn" data-active="false">
+ <div class="dc-component dc-psu"></div>
+ <div class="dc-component-label">Power Supply Unit</div>
+ </div>
+ <div class="dc-component-container hidden" id="add-cooling-item-btn" data-active="false">
+ <div class="dc-component dc-cooling-item"></div>
+ <div class="dc-component-label">Cooling Item</div>
+ </div>
+ <label class="hidden" id="no-objects-info">
+ No objects are allowed in this type of room. <br>
+ Change the room type to be able to add elements.
+ </label>
+ <div class="btn btn-danger btn-block" id="room-deletion">
+ Delete this room
+ </div>
+ </div>
+ <div class="menu-body simulation">
+ <h3 id="room-name-field"></h3>
+ <h5 id="room-type-field"></h5>
+ <div class="room-stats-list">
+ <!-- Populated at runtime, with simulation stats per rack -->
+ </div>
+ </div>
+ </div>
+
+ <div class="menu-container level-menu hidden" id="object-menu">
+ <div id="rack-sub-menu" class="object-sub-menu">
+ <div class="menu-header-bar">
+ Rack
+ <button class="menu-collapse btn btn-default btn-circle">
+ <i class="glyphicon glyphicon-minus"></i>
+ </button>
+ </div>
+ <div class="menu-body construction">
+ <div class="input-group">
+ <input type="text" class="form-control" id="rack-name-input" placeholder="Rack name...">
+ <span class="input-group-btn"><button class="btn btn-default" id="rack-name-save"
+ type="button">Save</button></span>
+ </div>
+ <div class="node-list-container">
+ <!-- Populated at runtime with node data -->
+ </div>
+ <div class="btn btn-danger btn-block" id="rack-deletion">
+ Delete this rack
+ </div>
+ </div>
+ <div class="menu-body simulation">
+ <div class="node-list-container">
+ <!-- Populated at runtime with node simulation stats -->
+ </div>
+ </div>
+ </div>
+ <div id="psu-sub-menu" class="object-sub-menu">
+ <div class="menu-header-bar">
+ Power Supply Unit
+ <button class="menu-collapse btn btn-default btn-circle">
+ <i class="glyphicon glyphicon-minus"></i>
+ </button>
+ </div>
+ <div class="menu-body construction">
+ <div class="btn btn-danger btn-block" id="psu-deletion">
+ Delete this PSU
+ </div>
+ </div>
+ <div class="menu-body simulation">
+ </div>
+ </div>
+ <div id="cooling-item-sub-menu" class="object-sub-menu">
+ <div class="menu-header-bar">
+ Cooling Item
+ <button class="menu-collapse btn btn-default btn-circle">
+ <i class="glyphicon glyphicon-minus"></i>
+ </button>
+ </div>
+ <div class="menu-body construction">
+ <div class="btn btn-danger btn-block" id="cooling-item-deletion">
+ Delete this cooling item
+ </div>
+ </div>
+ <div class="menu-body simulation">
+ </div>
+ </div>
+ </div>
+ <div class="menu-container hidden" id="statistics-menu">
+ <div class="menu-header-bar">
+ Statistics
+ <button type="button" class="menu-collapse btn btn-default btn-circle">
+ <i class="glyphicon glyphicon-minus"></i>
+ </button>
+ </div>
+ <div class="menu-body simulation">
+ <div id="statistics-chart"></div>
+ </div>
+ </div>
+ </div>
+
+ <div class="side-menu-container left-side">
+ <div class="mode-switch" data-selected="construction" role="group" aria-label="Mode switch">
+ <div id="construction-mode-switch">Construction</div>
+ <div id="simulation-mode-switch">Simulation</div>
+ </div>
+ <div id="save-version-btn" data-saved="true">Save version</div>
+ <div class="menu-container hidden" id="experiment-menu">
+ <div class="menu-header-bar">
+ Experiment
+ <button type="button" class="menu-collapse btn btn-default btn-circle">
+ <i class="glyphicon glyphicon-minus"></i>
+ </button>
+ </div>
+ <div class="menu-body simulation">
+ <p><strong>Name:</strong> <span id="experiment-menu-name">Name</span></p>
+ <p><strong>Path:</strong> <span id="experiment-menu-path">Path</span></p>
+ <p><strong>Trace:</strong> <span id="experiment-menu-trace">Trace</span></p>
+ <p><strong>Scheduler:</strong> <span id="experiment-menu-scheduler">Scheduler</span></p>
+ <div id="change-experiment-btn" class="btn btn-default btn-block">Change experiment</div>
+ </div>
+ </div>
+ <div class="menu-container hidden" id="tasks-menu">
+ <div class="menu-header-bar">
+ Tasks
+ <button type="button" class="menu-collapse btn btn-default btn-circle">
+ <i class="glyphicon glyphicon-minus"></i>
+ </button>
+ </div>
+ <div class="menu-body simulation">
+ <div class="task-list">
+ <!-- Populated at runtime with a number of task elements -->
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="tool-panel">
+ <button type="button" class="btn btn-default btn-circle" id="zoom-plus" title="Zoom in">
+ <i class="glyphicon glyphicon-plus"></i>
+ </button>
+ <button type="button" class="btn btn-default btn-circle" id="zoom-minus" title="Zoom out">
+ <i class="glyphicon glyphicon-minus"></i>
+ </button>
+ <button type="button" class="btn btn-success btn-circle export-canvas" title="Export Canvas to PNG Image">
+ <i class="glyphicon glyphicon-camera"></i>
+ </button>
+ </div>
+
+ <!-- Map indicators -->
+ <div class="indicators">
+ <div class="scale-indicator">
+ 0.5m
+ </div>
+ <div class="color-indicator hidden">
+ <div class="intensity-labels">
+ <div>0</div>
+ <div>25</div>
+ <div>50</div>
+ <div>75</div>
+ <div>100</div>
+ </div>
+ <div class="intensity-colors">
+ <div class="intensity-low"></div>
+ <div class="intensity-mid-low"></div>
+ <div class="intensity-mid-high"></div>
+ <div class="intensity-high"></div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Timeline bar, for simulation playback -->
+ <div class="timeline-bar">
+ <div class="timeline-container hidden">
+ <div class="labels">
+ <div class="start-time-label">00:00:00</div>
+ <div class="end-time-label">00:00:00</div>
+ </div>
+ <div class="controls">
+ <div class="play-btn glyphicon glyphicon-play"><img src="img/app/loading.gif"></div>
+ <div class="timeline">
+ <div class="cache-section"></div>
+ <div class="time-marker"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Informational Balloon Popup -->
+ <div class="info-balloon">
+ <span></span>Test Info
+ </div>
+
+ <!-- Overlay for DC loading process -->
+ <div class="loading-overlay">
+ <div class="loading-overlay-content">
+ <img src="img/logo.png">
+ <div class="loading-text">
+ <h3>Loading your project...</h3>
+ <p class="muted">Fetching the DC</p>
+ </div>
+ </div>
+ </div>
+</div>
+
+<div class="modal fade" id="confirm-delete" tabindex="-1" role="dialog" aria-labelledby="confirm-delete-header">
+ <div class="modal-dialog" role="document">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
+ aria-hidden="true">&times;</span></button>
+ <h4 class="modal-title" id="confirm-delete-header">Confirm delete</h4>
+ </div>
+ <div class="modal-body">
+ Are you sure you want to delete this item?
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
+ <button type="button" class="btn btn-danger confirm">Delete</button>
+ </div>
+ </div>
+ </div>
+</div>
+
+<div class="window-overlay">
+ <div class="experiments-window" id="experiments-window">
+ <div class="window-close glyphicon glyphicon-remove"></div>
+ <div class="window-body">
+ <div class="window-heading">Experiments</div>
+ <form class="form-inline experiment-add-form">
+ <div class="form-group">
+ <label for="new-experiment-name-input">Name</label>
+ <input type="text" class="form-control" id="new-experiment-name-input"
+ placeholder="Experiment name">
+ <label for="new-experiment-path-select">Path</label>
+ <select class="form-control" id="new-experiment-path-select">
+ </select>
+ </div>
+ <div class="form-group">
+ <label for="new-experiment-trace-select">Trace</label>
+ <select class="form-control" id="new-experiment-trace-select">
+ </select>
+ <label for="new-experiment-scheduler-select">Scheduler</label>
+ <select class="form-control" id="new-experiment-scheduler-select">
+ </select>
+ <div class="btn btn-default" id="new-experiment-btn">Save &amp; Launch</div>
+ </div>
+ </form>
+ <strong class="experiments-table-label">Previous Experiments</strong>
+ <div class="experiment-list">
+ <div class="list-head">
+ <div>Name</div>
+ <div>Path</div>
+ <div>Trace</div>
+ <div>Scheduler</div>
+ <div></div>
+ </div>
+ <div class="list-body">
+ </div>
+ </div>
+ <div class="no-experiments-alert alert alert-info">
+ <span class="glyphicon glyphicon-info-sign"></span>
+ <strong>No experiments here yet...</strong> Add some with the form above!
+ </div>
+ <div class="experiment-name-alert alert alert-danger" role="alert">
+ <strong>Warning:</strong> Your experiment needs a name!
+ </div>
+ </div>
+ </div>
+</div>
+
+<!-- Bower dependencies that cannot be included via the module system -->
+<script src="bower_components/EaselJS/lib/easeljs-0.8.2.min.js"></script>
+<script src="bower_components/TweenJS/lib/tweenjs-0.6.2.min.js"></script>
+<script src="bower_components/PreloadJS/lib/preloadjs-0.6.2.min.js"></script>
+
+<script src="scripts/main.entry.js"></script>
+
+<script src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
+
+<!-- Google API -->
+<script src="https://apis.google.com/js/platform.js?onload=gapiSigninButton" async defer></script>
+
+</body>
+</html>
diff --git a/src/favicon.ico b/src/favicon.ico
new file mode 100644
index 00000000..c2f40a0d
--- /dev/null
+++ b/src/favicon.ico
Binary files differ
diff --git a/src/humans.txt b/src/humans.txt
new file mode 100644
index 00000000..652f9cd2
--- /dev/null
+++ b/src/humans.txt
@@ -0,0 +1,27 @@
+/* TEAM */
+Benevolent Dictator for Life: Alexandru Iosup.
+Site: http://www.ds.ewi.tudelft.nl/~iosup/
+Twitter: aiosup.
+Location: Delft, Netherlands.
+
+Backend Engineer: Leon Overweel.
+Site: http://leonoverweel.com/
+Twitter: layon_overwhale.
+Location: Delft, Netherlands.
+
+Frontend Engineer: Georgios Andreadis.
+Site: https://github.com/gandreadis
+Location: Delft, Netherlands.
+
+Simulation Engineer: Matthijs Bijman.
+Site: https://github.com/MDBijman
+Location: Delft, Netherlands.
+
+/* THANKS */
+Executive Producer: Vincent van Beek.
+Executive Producer: Tim Hegeman.
+
+/* SITE */
+Standards: HTML5, CSS3, ES5
+Components: jQuery, Bootstrap, CreateJS, c3.js
+Software: WebStorm, Vim, Visual Studio \ No newline at end of file
diff --git a/src/img/app/coolingitem.png b/src/img/app/coolingitem.png
new file mode 100644
index 00000000..16c18be0
--- /dev/null
+++ b/src/img/app/coolingitem.png
Binary files differ
diff --git a/src/img/app/loading.gif b/src/img/app/loading.gif
new file mode 100644
index 00000000..c6394822
--- /dev/null
+++ b/src/img/app/loading.gif
Binary files differ
diff --git a/src/img/app/node-cpu.png b/src/img/app/node-cpu.png
new file mode 100644
index 00000000..07cfbd31
--- /dev/null
+++ b/src/img/app/node-cpu.png
Binary files differ
diff --git a/src/img/app/node-gpu.png b/src/img/app/node-gpu.png
new file mode 100644
index 00000000..55d4fb05
--- /dev/null
+++ b/src/img/app/node-gpu.png
Binary files differ
diff --git a/src/img/app/node-memory.png b/src/img/app/node-memory.png
new file mode 100644
index 00000000..36e8a44e
--- /dev/null
+++ b/src/img/app/node-memory.png
Binary files differ
diff --git a/src/img/app/node-network.png b/src/img/app/node-network.png
new file mode 100644
index 00000000..795e173b
--- /dev/null
+++ b/src/img/app/node-network.png
Binary files differ
diff --git a/src/img/app/node-storage.png b/src/img/app/node-storage.png
new file mode 100644
index 00000000..7a39cb6f
--- /dev/null
+++ b/src/img/app/node-storage.png
Binary files differ
diff --git a/src/img/app/psu.png b/src/img/app/psu.png
new file mode 100644
index 00000000..471af6ee
--- /dev/null
+++ b/src/img/app/psu.png
Binary files differ
diff --git a/src/img/app/rack-energy.png b/src/img/app/rack-energy.png
new file mode 100644
index 00000000..1088c61b
--- /dev/null
+++ b/src/img/app/rack-energy.png
Binary files differ
diff --git a/src/img/app/rack-space.png b/src/img/app/rack-space.png
new file mode 100644
index 00000000..387d7ea6
--- /dev/null
+++ b/src/img/app/rack-space.png
Binary files differ
diff --git a/src/img/datacenter-drawing.png b/src/img/datacenter-drawing.png
new file mode 100644
index 00000000..401168e3
--- /dev/null
+++ b/src/img/datacenter-drawing.png
Binary files differ
diff --git a/src/img/email-icon.png b/src/img/email-icon.png
new file mode 100644
index 00000000..ced9e175
--- /dev/null
+++ b/src/img/email-icon.png
Binary files differ
diff --git a/src/img/favicon.png b/src/img/favicon.png
new file mode 100644
index 00000000..85d74964
--- /dev/null
+++ b/src/img/favicon.png
Binary files differ
diff --git a/src/img/github-icon.png b/src/img/github-icon.png
new file mode 100644
index 00000000..1e221600
--- /dev/null
+++ b/src/img/github-icon.png
Binary files differ
diff --git a/src/img/logo.png b/src/img/logo.png
new file mode 100644
index 00000000..d743038b
--- /dev/null
+++ b/src/img/logo.png
Binary files differ
diff --git a/src/img/mockups/construction-node.png b/src/img/mockups/construction-node.png
new file mode 100644
index 00000000..78ad81e8
--- /dev/null
+++ b/src/img/mockups/construction-node.png
Binary files differ
diff --git a/src/img/mockups/simulation-node.png b/src/img/mockups/simulation-node.png
new file mode 100644
index 00000000..fc56f44a
--- /dev/null
+++ b/src/img/mockups/simulation-node.png
Binary files differ
diff --git a/src/img/mockups/simulation-room.png b/src/img/mockups/simulation-room.png
new file mode 100644
index 00000000..f8f80623
--- /dev/null
+++ b/src/img/mockups/simulation-room.png
Binary files differ
diff --git a/src/img/opendc-splash.png b/src/img/opendc-splash.png
new file mode 100644
index 00000000..99fd8658
--- /dev/null
+++ b/src/img/opendc-splash.png
Binary files differ
diff --git a/src/img/portraits/aiosup.png b/src/img/portraits/aiosup.png
new file mode 100644
index 00000000..30de349c
--- /dev/null
+++ b/src/img/portraits/aiosup.png
Binary files differ
diff --git a/src/img/portraits/gandreadis.png b/src/img/portraits/gandreadis.png
new file mode 100644
index 00000000..403870fa
--- /dev/null
+++ b/src/img/portraits/gandreadis.png
Binary files differ
diff --git a/src/img/portraits/loverweel.png b/src/img/portraits/loverweel.png
new file mode 100644
index 00000000..d12a9e86
--- /dev/null
+++ b/src/img/portraits/loverweel.png
Binary files differ
diff --git a/src/img/portraits/mbijman.png b/src/img/portraits/mbijman.png
new file mode 100644
index 00000000..decf9fdd
--- /dev/null
+++ b/src/img/portraits/mbijman.png
Binary files differ
diff --git a/src/img/stakeholders/Developer.png b/src/img/stakeholders/Developer.png
new file mode 100644
index 00000000..d2638e6c
--- /dev/null
+++ b/src/img/stakeholders/Developer.png
Binary files differ
diff --git a/src/img/stakeholders/Manager.png b/src/img/stakeholders/Manager.png
new file mode 100644
index 00000000..92db7459
--- /dev/null
+++ b/src/img/stakeholders/Manager.png
Binary files differ
diff --git a/src/img/stakeholders/Researcher.png b/src/img/stakeholders/Researcher.png
new file mode 100644
index 00000000..d87edd39
--- /dev/null
+++ b/src/img/stakeholders/Researcher.png
Binary files differ
diff --git a/src/img/stakeholders/Sales.png b/src/img/stakeholders/Sales.png
new file mode 100644
index 00000000..5b7c3a72
--- /dev/null
+++ b/src/img/stakeholders/Sales.png
Binary files differ
diff --git a/src/img/stakeholders/Student.png b/src/img/stakeholders/Student.png
new file mode 100644
index 00000000..a4900303
--- /dev/null
+++ b/src/img/stakeholders/Student.png
Binary files differ
diff --git a/src/img/technologies/arrow.png b/src/img/technologies/arrow.png
new file mode 100644
index 00000000..374f78bf
--- /dev/null
+++ b/src/img/technologies/arrow.png
Binary files differ
diff --git a/src/img/technologies/cogs-icon.png b/src/img/technologies/cogs-icon.png
new file mode 100644
index 00000000..d19e1c20
--- /dev/null
+++ b/src/img/technologies/cogs-icon.png
Binary files differ
diff --git a/src/img/technologies/database-icon.png b/src/img/technologies/database-icon.png
new file mode 100644
index 00000000..26738e76
--- /dev/null
+++ b/src/img/technologies/database-icon.png
Binary files differ
diff --git a/src/img/technologies/webserver-icon.png b/src/img/technologies/webserver-icon.png
new file mode 100644
index 00000000..c627106e
--- /dev/null
+++ b/src/img/technologies/webserver-icon.png
Binary files differ
diff --git a/src/img/technologies/www-icon.png b/src/img/technologies/www-icon.png
new file mode 100644
index 00000000..e69a54f2
--- /dev/null
+++ b/src/img/technologies/www-icon.png
Binary files differ
diff --git a/src/img/tudelfticon.png b/src/img/tudelfticon.png
new file mode 100644
index 00000000..a7a2d56a
--- /dev/null
+++ b/src/img/tudelfticon.png
Binary files differ
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 00000000..554a21a1
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,411 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>OpenDC</title>
+
+ <meta name="description" content="Collaborative Datacenter Simulation and Exploration for Everybody">
+ <meta name="author" content="Alexandru Iosup, Leon Overweel, Georgios Andreadis, Matthijs Bijman">
+ <meta name="keywords" content="OpenDC, Datacenter, Simulation, Simulator, Collaborative, Distributed, Cluster">
+
+ <!-- OpenGraph meta tags -->
+ <meta property="og:title" content="OpenDC: Collaborative Datacenter Simulation and Exploration for Everybody">
+ <meta property="og:type" content="website">
+ <meta property="og:image" content="https://opendc.ewi.tudelft.nl/img/opendc-splash.png">
+ <meta property="og:url" content="https://opendc.ewi.tudelft.nl">
+ <meta property="og:description"
+ content="OpenDC provides collaborative online datacenter modeling, diverse and effective datacenter
+ simulation, and exploratory datacenter performance feedback.">
+ <meta property="og:locale" content="en_US">
+
+ <!-- Google Sign-in -->
+ <meta name="google-signin-client_id"
+ content="184853849394-v89e96desio4dub3360vg32p1l4r3jqd.apps.googleusercontent.com">
+
+ <!-- Set viewport -->
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+
+ <!-- Style sheets -->
+ <link href="bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
+ <link href="styles/splash.css" rel="stylesheet">
+
+ <!-- Favicon -->
+ <link href="img/logo.png" rel="icon">
+
+ <!-- humans.txt -->
+ <link type="text/plain" rel="author" href="humans.txt">
+</head>
+<body id="page-top" data-spy="scroll" data-target=".navbar-fixed-top">
+
+<div class="body-wrapper">
+
+ <nav class="navbar navbar-inverse navbar-fixed-top navbar-transparent" role="navigation">
+ <div class="container">
+ <div class="navbar-header page-scroll">
+ <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
+ <span class="sr-only">Toggle navigation</span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </button>
+ <a class="navbar-brand page-scroll" href="#page-top">
+ <img src="img/logo.png" alt="OpenDC Logo">
+ </a>
+ </div>
+
+ <div class="collapse navbar-collapse">
+ <ul class="nav navbar-nav">
+ <li class="hidden">
+ <a class="page-scroll" href="#page-top"></a>
+ </li>
+ <li>
+ <a class="page-scroll" href="#stakeholders">Stakeholders</a>
+ </li>
+ <li>
+ <a class="page-scroll" href="#modeling">Modeling</a>
+ </li>
+ <li>
+ <a class="page-scroll" href="#simulation">Simulation</a>
+ </li>
+ <li>
+ <a class="page-scroll" href="#technologies">Technologies</a>
+ </li>
+ <li>
+ <a class="page-scroll" href="#team">Team</a>
+ </li>
+ <li>
+ <a class="page-scroll" href="#contact">Contact</a>
+ </li>
+ </ul>
+ <div id="google-signin" class="navbar-right"></div>
+ <div class="logged-in navbar-right">
+ <a class="projects-btn" href="projects">My Projects</a>
+ <a class="sign-out glyphicon glyphicon-off" title="Sign out" href="javascript:void(0)"></a>
+ </div>
+ </div>
+
+ </div>
+ </nav>
+
+ <section class="header-section">
+ <div class="container">
+ <div class="jumbotron">
+ <h1>Open<span class="dc">DC</span></h1>
+ <h2>
+ Collaborative Datacenter Simulation and Exploration for Everybody
+ </h2>
+ </div>
+ </div>
+ </section>
+
+ <section id="intro" class="intro-section">
+ <div class="container">
+ <div class="row">
+ <div class="row pitch-container">
+ <div class="col-lg-4 col-md-4 col-sm-4 col-xs-8
+ col-lg-offset-0 col-md-offset-0 col-sm-offset-0 col-xs-offset-2 pitch-column">
+ <h3>The datacenter (DC) industry...</h3>
+ <ul class="info-points">
+ <li>Is worth over $15 bn, and growing</li>
+ <li>Has many hard-to-grasp concepts</li>
+ <li>Needs to become accessible to many</li>
+ </ul>
+ </div>
+ <div class="col-lg-4 col-md-4 col-sm-4 col-xs-8
+ col-lg-offset-0 col-md-offset-0 col-sm-offset-0 col-xs-offset-2">
+ <img src="img/datacenter-drawing.png" class="col-lg-12 col-md-12 col-sm-12 col-xs-12 dc-image"
+ alt="Schematic top-down view on a datacenter">
+ <p class="col-lg-12 col-md-12 col-sm-12 col-xs-12 img-source">Image source:
+ <a href="http://www.dolphinhosts.co.uk/wp-content/uploads/2013/07/data-centers.gif">
+ http://www.dolphinhosts.co.uk/wp-content/uploads/2013/07/data-centers.gif
+ </a>
+ </p>
+ </div>
+ <div class="col-lg-4 col-md-4 col-sm-4 col-xs-8
+ col-lg-offset-0 col-md-offset-0 col-sm-offset-0 col-xs-offset-2 pitch-column">
+ <h3>OpenDC provides...</h3>
+ <ul class="info-points">
+ <li>Collaborative online DC modeling</li>
+ <li>Diverse and effective DC simulation</li>
+ <li>Exploratory DC performance feedback</li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section id="stakeholders" class="stakeholder-section content-section">
+ <div class="container">
+ <div class="row">
+ <div class="col-lg-10 col-md-10 col-sm-10 col-xs-12 col-lg-offset-1 col-md-offset-1 col-sm-offset-1 col-xs-offset-0">
+ <h1>Stakeholders</h1>
+ <div class="row stakeholder-container">
+ <div class="col-lg-4 col-md-4 col-sm-6 col-xs-12">
+ <img src="img/stakeholders/Manager.png" class="col-lg-4 col-md-4 col-sm-4 col-xs-2"
+ alt="Managers">
+ <div class="col-lg-8 col-md-8 col-sm-8 col-xs-10">
+ <h3>Managers</h3>
+ <p>Seeing is deciding</p>
+ </div>
+ </div>
+ <div class="col-lg-4 col-md-4 col-sm-6 col-xs-12">
+ <img src="img/stakeholders/Sales.png" class="col-lg-4 col-md-4 col-sm-4 col-xs-2"
+ alt="Sales">
+ <div class="col-lg-8 col-md-8 col-sm-8 col-xs-10">
+ <h3>Sales</h3>
+ <p>Demo concepts</p>
+ </div>
+ </div>
+ <div class="col-lg-4 col-md-4 col-sm-6 col-xs-12">
+ <img src="img/stakeholders/Developer.png" class="col-lg-4 col-md-4 col-sm-4 col-xs-2"
+ alt="DevOps">
+ <div class="col-lg-8 col-md-8 col-sm-8 col-xs-10">
+ <h3>DevOps</h3>
+ <p>Develop & tune</p>
+ </div>
+ </div>
+ <div class="col-lg-4 col-md-4 col-sm-6 col-xs-12 col-lg-offset-2 col-md-offset-2 col-sm-offset-0 col-xs-offset-0">
+ <img src="img/stakeholders/Researcher.png" class="col-lg-4 col-md-4 col-sm-4 col-xs-2"
+ alt="Researchers">
+ <div class="col-lg-8 col-md-8 col-sm-8 col-xs-10">
+ <h3>Researchers</h3>
+ <p>Understand & design</p>
+ </div>
+ </div>
+ <div class="col-lg-4 col-md-4 col-sm-6 col-xs-12 col-md-offset-0 col-sm-offset-3 col-xs-offset-0">
+ <img src="img/stakeholders/Student.png" class="col-lg-4 col-md-4 col-sm-4 col-xs-2"
+ alt="Students">
+ <div class="col-lg-8 col-md-8 col-sm-8 col-xs-10">
+ <h3>Students</h3>
+ <p>Grasp complex concepts</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section id="modeling" class="modeling-section content-section">
+ <div class="container">
+ <div class="row">
+ <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
+ <h1>Datacenter Modeling</h1>
+ <div class="row">
+ <div class="col-lg-5 col-md-5 col-sm-5 col-xs-12 text-left">
+ <strong class="info-points">Collaboratively...</strong>
+ <ul class="info-points key-points">
+ <li>Model DC layout, and room locations and types</li>
+ <li>Place racks in rooms and nodes in racks</li>
+ <li>Add real-world CPU, GPU, memory, storage and network units to each node</li>
+ <li>Select from diverse scheduling policies</li>
+ </ul>
+ </div>
+
+
+ <div class="col-lg-7 col-md-7 col-sm-7 col-xs-12">
+ <img src="img/mockups/construction-node.png" class="col-lg-12 col-md-12 col-sm-12 col-xs-10
+ col-lg-offset-0 col-md-offset-0 col-sm-offset-0 col-xs-offset-1"
+ alt="Mockup of the datacenter construction interface">
+ <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 img-caption">
+ Mockup of the datacenter construction interface
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section id="simulation" class="simulation-section content-section">
+ <div class="container">
+ <div class="row">
+ <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
+ <h1>Datacenter Simulation</h1>
+ <div class="row simulation-building-row">
+ <div class="col-lg-5 col-md-5 col-sm-5 col-xs-12 text-left">
+ <strong class="info-points">Working with OpenDC:</strong>
+ <ul class="info-points key-points">
+ <li>Seamlessly switch between construction and simulation modes
+ <li>Choose one of several predefined workloads (Big Data, Bag of Tasks,
+ Hadoop, etc.)
+ <li>Play, pause, and skip around the informative simulation timeline
+ <li>Visualize and demo live
+ </ul>
+ </div>
+ <div class="col-lg-7 col-md-7 col-sm-7 col-xs-12">
+ <img src="img/mockups/simulation-room.png" class="col-lg-12 col-md-12 col-sm-12 col-xs-10
+ col-lg-offset-0 col-md-offset-0 col-sm-offset-0 col-xs-offset-1"
+ alt="Mockup of the datacenter simulation interface at room level">
+ <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 img-caption">
+ Mockup of the datacenter simulation interface at room level
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-5 col-md-5 col-sm-5 col-xs-12 text-left">
+ <strong class="info-points">Key features:</strong>
+ <ul class="info-points key-points">
+ <li>Live load or power use metrics on building, room, and rack levels
+ <li>Diverse scenarios from common operation to model-based failures
+ <li>Retrospective performance review of datacenter simulations
+ <li>Compare resource management practices
+ </ul>
+ </div>
+ <div class="col-lg-7 col-md-7 col-sm-7 col-xs-12">
+ <img src="img/mockups/simulation-node.png" class="col-lg-12 col-md-12 col-sm-12 col-xs-10
+ col-lg-offset-0 col-md-offset-0 col-sm-offset-0 col-xs-offset-1"
+ alt="Mockup of the same simulation at node level">
+ <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 img-caption">
+ Mockup of the same simulation at node level
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section id="technologies" class="technologies-section content-section">
+ <div class="container">
+ <div class="row">
+ <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
+ <h1>Technologies</h1>
+ <div class="tech-rows row col-lg-6 col-md-6 col-sm-12 col-xs-12
+ col-lg-offset-3 col-md-offset-3 col-sm-offset-0 col-xs-offset-0">
+ <div class="browser-tech tech-row row">
+ <img src="img/technologies/www-icon.png" class="col-lg-2 col-md-2 col-sm-2 col-xs-2"
+ alt="Web browser">
+ <div class="col-lg-10 col-md-10 col-sm-10 col-xs-10 text-left">
+ <h3>Browser</h3>
+ <p class="info-points">HTML5 canvas, CreateJS, TypeScript, SocketIO</p>
+ </div>
+ </div>
+ <div class="server-tech tech-row row">
+ <img src="img/technologies/webserver-icon.png" class="col-lg-2 col-md-2 col-sm-2 col-xs-2"
+ alt="Web Server">
+ <div class="col-lg-10 col-md-10 col-sm-10 col-xs-10 text-left">
+ <h3>Web Server</h3>
+ <p class="info-points">Python, Flask, FlaskSocketIO, OpenAPI</p>
+ </div>
+ </div>
+ <div class="database-tech tech-row row">
+ <img src="img/technologies/database-icon.png" class="col-lg-2 col-md-2 col-sm-2 col-xs-2"
+ alt="Database">
+ <div class="col-lg-10 col-md-10 col-sm-10 col-xs-10 text-left">
+ <h3>Database</h3>
+ <p class="info-points">SQLite</p>
+ </div>
+ </div>
+ <div class="simulator-tech tech-row row">
+ <img src="img/technologies/cogs-icon.png" class="col-lg-2 col-md-2 col-sm-2 col-xs-2"
+ alt="Simulator">
+ <div class="col-lg-10 col-md-10 col-sm-10 col-xs-10 text-left">
+ <h3>Simulator</h3>
+ <p class="info-points">C++</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section id="team" class="team-section content-section">
+ <div class="container">
+ <div class="row">
+ <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
+ <h1 class="col-lg-12 col-md-12 col-sm-12 col-xs-12 row">The Team</h1>
+ <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 row">
+ <div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
+ <img src="img/portraits/aiosup.png" class="col-lg-12 col-md-12 col-sm-12 col-xs-6
+ col-lg-offset-0 col-md-offset-0 col-sm-offset-0 col-xs-offset-3">
+ <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
+ <h3>Prof. dr. ir. Alexandru Iosup</h3>
+ <div class="team-member-description">
+ Project Lead
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
+ <img src="img/portraits/loverweel.png" class="col-lg-12 col-md-12 col-sm-12 col-xs-6
+ col-lg-offset-0 col-md-offset-0 col-sm-offset-0 col-xs-offset-3">
+ <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
+ <h3>Leon Overweel</h3>
+ <div class="team-member-description">
+ Project Manager and Software Engineer responsible for the web server, database, and
+ API
+ specification
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
+ <img src="img/portraits/gandreadis.png" class="col-lg-12 col-md-12 col-sm-12 col-xs-6
+ col-lg-offset-0 col-md-offset-0 col-sm-offset-0 col-xs-offset-3">
+ <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
+ <h3>Georgios Andreadis</h3>
+ <div class="team-member-description">
+ Software Engineer responsible for the frontend web application and splash page
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
+ <img src="img/portraits/mbijman.png" class="col-lg-12 col-md-12 col-sm-12 col-xs-6
+ col-lg-offset-0 col-md-offset-0 col-sm-offset-0 col-xs-offset-3">
+ <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
+ <h3>Matthijs Bijman</h3>
+ <div class="team-member-description">
+ Software Engineer responsible for the datacenter simulator
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section id="contact" class="contact-section content-section">
+ <div class="container">
+ <div class="row">
+ <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
+ <h1>Contact</h1>
+ <div class="row">
+ <img src="img/tudelfticon.png" class="col-lg-2 col-md-2 col-sm-3 col-xs-6
+ col-lg-offset-4 col-md-offset-4 col-sm-offset-3 col-xs-offset-3 tudelft-icon"
+ alt="TU Delft Logo">
+ <div class="col-lg-4 col-md-5 col-sm-6 col-xs-10 col-lg-offset-0 col-md-offset-0 col-sm-offset-0 col-xs-offset-1 text-left">
+ <div class="row vcenter">
+ <img src="img/email-icon.png" class="col-lg-2 col-md-2 col-sm-2 col-xs-2"
+ alt="Email Icon">
+ <div class="info-points col-lg-10 col-md-10 col-sm-10 col-xs-10">
+ <a href="mailto:opendc.tudelft@gmail.com">opendc.tudelft@gmail.com</a>
+ </div>
+ </div>
+ <div class="row vcenter">
+ <img src="img/github-icon.png" class="col-lg-2 col-md-2 col-sm-2 col-xs-2"
+ alt="Github Icon">
+ <div class="info-points col-lg-10 col-md-10 col-sm-10 col-xs-10">
+ <a href="#">Coming Soon</a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <script src="scripts/splash.entry.js"></script>
+
+ <!-- Bower dependencies -->
+ <script src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
+
+ <!-- Google API -->
+ <script src="https://apis.google.com/js/platform.js?onload=renderButton" async defer></script>
+
+</div>
+
+</body>
+</html>
diff --git a/src/navbar.html b/src/navbar.html
new file mode 100644
index 00000000..92c79000
--- /dev/null
+++ b/src/navbar.html
@@ -0,0 +1,18 @@
+<nav class="top-navbar">
+ <a class="opendc-brand" href="/">
+ <img src="img/logo.png" alt="OpenDC Logo">
+ <div class="opendc-title">Open<strong>DC</strong></div>
+ </a>
+ <div class="navigation navbar-button-group">
+ <a class="projects" title="Projects" href="projects">Projects</a>
+ </div>
+ <div class="user navbar-button-group">
+ <a class="support glyphicon glyphicon-question-sign" title="Support"
+ href="mailto:opendc.tudelft@gmail.com?Subject=OpenDC%20Support"></a>
+ <a class="username" title="My Profile" href="profile">Profile</a>
+ <a class="sign-out glyphicon glyphicon-off" title="Sign out" href="javascript:void(0)"></a>
+ </div>
+
+ <!--Hidden Google signin button for authentication-->
+ <div id="google-signin" class="navbar-right"></div>
+</nav> \ No newline at end of file
diff --git a/src/profile.html b/src/profile.html
new file mode 100644
index 00000000..23a1f5a3
--- /dev/null
+++ b/src/profile.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="google-signin-client_id"
+ content="184853849394-v89e96desio4dub3360vg32p1l4r3jqd.apps.googleusercontent.com">
+
+ <title>OpenDC - Profile</title>
+
+ <link href="bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
+
+ <link href="styles/navbar.css" rel="stylesheet">
+ <link href="styles/profile.css" rel="stylesheet">
+
+ <link href="img/logo.png" rel="icon">
+</head>
+<body>
+<!-- build:include navbar.html -->
+<!-- /build -->
+<div class="content">
+ <div class="main-body profile-page">
+ <h2>Profile Settings</h2>
+ <div id="delete-account" class="btn btn-danger">Delete my account on OpenDC</div>
+ <div class="delete-info">This does not delete your Google account, it simply disconnects it from the OpenDC app
+ and deletes any datacenter info that is associated with you (simulation projects you own, and any
+ authorizations you may have on other projects).
+ </div>
+ <div class="account-delete-alert alert alert-danger" role="alert">
+ <strong>Oops!</strong> Something went wrong while attempting to delete your account, here is the message:
+ <code></code>
+ </div>
+ </div>
+</div>
+
+<div class="modal fade" id="confirm-delete-account" tabindex="-1" role="dialog" aria-labelledby="confirm-delete-header">
+ <div class="modal-dialog" role="document">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
+ aria-hidden="true">&times;</span></button>
+ <h4 class="modal-title" id="confirm-delete-header">Confirm account deletion</h4>
+ </div>
+ <div class="modal-body">
+ Are you really sure you want us to delete your account? This action <strong>can not</strong> be undone.
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
+ <button type="button" class="btn btn-danger confirm">Delete my account</button>
+ </div>
+ </div>
+ </div>
+</div>
+
+<script src="scripts/profile.entry.js"></script>
+
+<!-- Bower dependencies -->
+<script src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
+
+<!-- Google API -->
+<script src="https://apis.google.com/js/platform.js?onload=gapiSigninButton" async defer></script>
+
+</body>
+</html> \ No newline at end of file
diff --git a/src/projects.html b/src/projects.html
new file mode 100644
index 00000000..c829ae00
--- /dev/null
+++ b/src/projects.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="google-signin-client_id"
+ content="184853849394-v89e96desio4dub3360vg32p1l4r3jqd.apps.googleusercontent.com">
+
+ <title>OpenDC - Projects</title>
+
+ <link href="bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
+
+ <link href="styles/navbar.css" rel="stylesheet">
+ <link href="styles/projects.css" rel="stylesheet">
+
+ <link href="img/logo.png" rel="icon">
+</head>
+<body>
+
+<!-- build:include navbar.html -->
+<!-- /build -->
+
+<div class="content">
+ <div class="main-body">
+ <div class="filter-menu">
+ <div class="project-filters">
+ <div class="all-projects active">All Projects</div>
+ <div class="my-projects">My Projects</div>
+ <div class="shared-projects">Projects shared with me</div>
+ </div>
+ </div>
+ <div class="no-projects-alert alert alert-info">
+ <span class="glyphicon glyphicon-info-sign"></span>
+ <strong>No projects here yet...</strong> Add some with the 'New Project' button!
+ </div>
+ <div class="project-list">
+ <div class="list-head">
+ <div>Project name</div>
+ <div>Last edited</div>
+ <div>Access rights</div>
+ </div>
+ <div class="list-body">
+ </div>
+ </div>
+ <div class="new-project-btn"><span class="glyphicon glyphicon-plus"></span> New Project</div>
+ </div>
+</div>
+
+<div class="window-overlay">
+ <div class="projects-window">
+ <div class="window-close glyphicon glyphicon-remove"></div>
+ <div class="window-body">
+ <div class="window-heading">Edit project</div>
+ <form class="form-inline project-name-form">
+ <div class="form-group">
+ <label for="newProjectNameInput">Name</label>
+ <input type="text" class="form-control" id="newProjectNameInput" placeholder="Project name">
+ <div class="btn btn-default">Save</div>
+ </div>
+ </form>
+ <strong class="participants-table-label">Participants</strong>
+ <div class="participants-table">
+ </div>
+ <form class="form-inline participant-add-form">
+ <div class="form-group">
+ <label for="participantAddInput" class="glyphicon glyphicon-plus"></label>
+ <input type="email" class="form-control" id="participantAddInput"
+ placeholder="Add a participant (by email)">
+ <div class="btn btn-default">Add</div>
+ </div>
+ </form>
+ <div class="participant-email-alert alert alert-danger" role="alert">
+ <strong>Warning:</strong> We couldn't find that email in our database. Misspelled something?
+ </div>
+ <div class="project-name-alert alert alert-danger" role="alert">
+ <strong>Warning:</strong> Your project needs a name!
+ </div>
+ </div>
+ <div class="window-footer">
+ <div class="project-open-btn btn btn-primary pull-left">Open</div>
+ <div class="project-create-open-btn btn btn-primary pull-left">Create & Open</div>
+ <div class="project-create-btn btn btn-info pull-left">Create</div>
+ <div class="project-delete-btn btn btn-danger pull-right">Delete</div>
+ <div class="project-cancel-btn btn btn-default pull-right">Cancel</div>
+ </div>
+ </div>
+</div>
+
+<script src="scripts/projects.entry.js"></script>
+
+<!-- Google API -->
+<script src="https://apis.google.com/js/platform.js?onload=gapiSigninButton" async defer></script>
+
+</body>
+</html> \ No newline at end of file
diff --git a/src/robots.txt b/src/robots.txt
new file mode 100644
index 00000000..2329e38e
--- /dev/null
+++ b/src/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Disallow: /app.html
+Disallow: /profile.html
+Disallow: /projects.html \ No newline at end of file
diff --git a/src/scripts/colors.ts b/src/scripts/colors.ts
new file mode 100644
index 00000000..559b7ee3
--- /dev/null
+++ b/src/scripts/colors.ts
@@ -0,0 +1,43 @@
+/**
+ * Class serving as a color palette for the application.
+ */
+export class Colors {
+ public static GRID_COLOR = "rgba(0, 0, 0, 0.5)";
+
+ public static WALL_COLOR = "rgba(0, 0, 0, 1)";
+
+ public static ROOM_DEFAULT = "rgba(150, 150, 150, 1)";
+ public static ROOM_SELECTED = "rgba(51, 153, 255, 1)";
+ public static ROOM_HOVER_VALID = "rgba(51, 153, 255, 0.5)";
+ public static ROOM_HOVER_INVALID = "rgba(255, 102, 0, 0.5)";
+ public static ROOM_NAME_COLOR = "rgba(245, 245, 245, 1)";
+ public static ROOM_TYPE_COLOR = "rgba(245, 245, 245, 1)";
+
+ public static RACK_BACKGROUND = "rgba(170, 170, 170, 1)";
+ public static RACK_BORDER = "rgba(0, 0, 0, 1)";
+ public static RACK_SPACE_BAR_BACKGROUND = "rgba(222, 235, 247, 1)";
+ public static RACK_SPACE_BAR_FILL = "rgba(91, 155, 213, 1)";
+ public static RACK_ENERGY_BAR_BACKGROUND = "rgba(255, 242, 204, 1)";
+ public static RACK_ENERGY_BAR_FILL = "rgba(255, 192, 0, 1)";
+
+ public static COOLING_ITEM_BACKGROUND = "rgba(40, 50, 230, 1)";
+ public static COOLING_ITEM_BORDER = "rgba(0, 0, 0, 1)";
+
+ public static PSU_BACKGROUND = "rgba(230, 50, 60, 1)";
+ public static PSU_BORDER = "rgba(0, 0, 0, 1)";
+
+ public static GRAYED_OUT_AREA = "rgba(0, 0, 0, 0.6)";
+
+ public static INFO_BALLOON_INFO = "rgba(40, 50, 230, 1)";
+ public static INFO_BALLOON_WARNING = "rgba(230, 60, 70, 1)";
+
+ public static INFO_BALLOON_MAP = {
+ "info": Colors.INFO_BALLOON_INFO,
+ "warning": Colors.INFO_BALLOON_WARNING
+ };
+
+ public static SIM_LOW = "rgba(197, 224, 180, 1)";
+ public static SIM_MID_LOW = "rgba(255, 230, 153, 1)";
+ public static SIM_MID_HIGH = "rgba(248, 203, 173, 1)";
+ public static SIM_HIGH = "rgba(249, 165, 165, 1)";
+}
diff --git a/src/scripts/controllers/connection/api.ts b/src/scripts/controllers/connection/api.ts
new file mode 100644
index 00000000..067e3ca0
--- /dev/null
+++ b/src/scripts/controllers/connection/api.ts
@@ -0,0 +1,1724 @@
+///<reference path="../../definitions.ts" />
+///<reference path="../../../../typings/index.d.ts" />
+import {Util} from "../../util";
+import {ServerConnection} from "../../serverconnection";
+
+
+export class APIController {
+ constructor(onConnect: (api: APIController) => any) {
+ ServerConnection.connect(() => {
+ onConnect(this);
+ });
+ }
+
+
+ ///
+ // PATH: /users
+ ///
+
+ // METHOD: GET
+ public getUserByEmail(email: string): Promise<IUser> {
+ return ServerConnection.send({
+ path: "/users",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {},
+ query: {
+ email
+ }
+ }
+ });
+ }
+
+ // METHOD: POST
+ public addUser(user: IUser): Promise<IUser> {
+ return ServerConnection.send({
+ path: "/users",
+ method: "POST",
+ parameters: {
+ body: {
+ user: user
+ },
+ path: {},
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /users/{id}
+ ///
+
+ // METHOD: GET
+ public getUser(userId: number): Promise<IUser> {
+ return ServerConnection.send({
+ path: "/users/{userId}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ userId
+ },
+ query: {}
+ }
+ });
+ }
+
+ // METHOD: PUT
+ public updateUser(userId: number, user: IUser): Promise<IUser> {
+ return ServerConnection.send({
+ path: "/users/{userId}",
+ method: "PUT",
+ parameters: {
+ body: {
+ user: {
+ givenName: user.givenName,
+ familyName: user.familyName
+ }
+ },
+ path: {
+ userId
+ },
+ query: {}
+ }
+ });
+ }
+
+ // METHOD: DELETE
+ public deleteUser(userId: number): Promise<IUser> {
+ return ServerConnection.send({
+ path: "/users/{userId}",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ userId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /users/{userId}/authorizations
+ ///
+
+ // METHOD: GET
+ public getAuthorizationsByUser(userId: number): Promise<IAuthorization[]> {
+ let authorizations = [];
+ return ServerConnection.send({
+ path: "/users/{userId}/authorizations",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ userId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ authorizations = data;
+ return this.getUser(userId);
+ }).then((userData: any) => {
+ let promises = [];
+ authorizations.forEach((authorization: IAuthorization) => {
+ authorization.user = userData;
+ promises.push(this.getSimulation(authorization.simulationId).then((simulationData: any) => {
+ authorization.simulation = simulationData;
+ }));
+ });
+ return Promise.all(promises);
+ }).then((data: any) => {
+ return authorizations;
+ });
+ }
+
+ ///
+ // PATH: /simulations
+ ///
+
+ // METHOD: POST
+ public addSimulation(simulation: ISimulation): Promise<ISimulation> {
+ return ServerConnection.send({
+ path: "/simulations",
+ method: "POST",
+ parameters: {
+ body: {
+ simulation: Util.packageForSending(simulation)
+ },
+ path: {},
+ query: {}
+ }
+ }).then((data: any) => {
+ this.parseSimulationTimestamps(data);
+ return data;
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}
+ ///
+
+ // METHOD: GET
+ public getSimulation(simulationId: number): Promise<ISimulation> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ this.parseSimulationTimestamps(data);
+ return data;
+ });
+ }
+
+ // METHOD: PUT
+ public updateSimulation(simulation: ISimulation): Promise<ISimulation> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}",
+ method: "PUT",
+ parameters: {
+ body: {
+ simulation: Util.packageForSending(simulation)
+ },
+ path: {
+ simulationId: simulation.id
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ this.parseSimulationTimestamps(data);
+ return data;
+ });
+ }
+
+ // METHOD: DELETE
+ public deleteSimulation(simulationId: number): Promise<ISimulation> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ simulationId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/authorizations
+ ///
+
+ // METHOD: GET
+ public getAuthorizationsBySimulation(simulationId: number): Promise<IAuthorization[]> {
+ let authorizations = [];
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/authorizations",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ authorizations = data;
+ return this.getSimulation(simulationId);
+ }).then((simulationData: any) => {
+ let promises = [];
+ authorizations.forEach((authorization: IAuthorization) => {
+ authorization.simulation = simulationData;
+ promises.push(this.getUser(authorization.userId).then((userData: any) => {
+ authorization.user = userData;
+ }));
+ });
+ return Promise.all(promises);
+ }).then((data: any) => {
+ return authorizations;
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/authorizations/{userId}
+ ///
+
+ // METHOD: GET
+ // Not needed
+
+ // METHOD: POST
+ public addAuthorization(authorization: IAuthorization): Promise<IAuthorization> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/authorizations/{userId}",
+ method: "POST",
+ parameters: {
+ body: {
+ authorization: {
+ authorizationLevel: authorization.authorizationLevel
+ }
+ },
+ path: {
+ simulationId: authorization.simulationId,
+ userId: authorization.userId
+ },
+ query: {}
+ }
+ });
+ }
+
+ // METHOD: PUT
+ public updateAuthorization(authorization: IAuthorization): Promise<IAuthorization> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/authorizations/{userId}",
+ method: "PUT",
+ parameters: {
+ body: {
+ authorization: {
+ authorizationLevel: authorization.authorizationLevel
+ }
+ },
+ path: {
+ simulationId: authorization.simulationId,
+ userId: authorization.userId
+ },
+ query: {}
+ }
+ });
+ }
+
+ // METHOD: DELETE
+ public deleteAuthorization(authorization: IAuthorization): Promise<IAuthorization> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/authorizations/{userId}",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ simulationId: authorization.simulationId,
+ userId: authorization.userId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}
+ ///
+
+ // METHOD: GET
+ public getDatacenter(simulationId: number, datacenterId: number): Promise<IDatacenter> {
+ let datacenter;
+
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ datacenter = data;
+
+ return this.getRoomsByDatacenter(simulationId, datacenterId);
+ }).then((data: any) => {
+ datacenter.rooms = data;
+ return datacenter;
+ });
+ }
+
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}/rooms
+ ///
+
+ // METHOD: GET
+ public getRoomsByDatacenter(simulationId: number, datacenterId: number): Promise<IRoom[]> {
+ let rooms;
+
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ rooms = data;
+
+ let promises = [];
+ rooms.forEach((room: IRoom) => {
+ promises.push(this.loadRoomTiles(simulationId, datacenterId, room));
+ });
+ return Promise.all(promises).then((data: any) => {
+ return rooms;
+ });
+ });
+ }
+
+ // METHOD: POST
+ public addRoomToDatacenter(simulationId: number, datacenterId: number): Promise<IRoom> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms",
+ method: "POST",
+ parameters: {
+ body: {
+ room: {
+ id: -1,
+ datacenterId,
+ roomType: "SERVER"
+ }
+ },
+ path: {
+ simulationId,
+ datacenterId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ data.tiles = [];
+ return data;
+ });
+ }
+
+ ///
+ // PATH: /room-types
+ ///
+
+ // METHOD: GET
+ public getAllRoomTypes(): Promise<string[]> {
+ return ServerConnection.send({
+ path: "/room-types",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {},
+ query: {}
+ }
+ }).then((data: any) => {
+ let result = [];
+ data.forEach((roomType: any) => {
+ result.push(roomType.name);
+ });
+ return result;
+ });
+ }
+
+ ///
+ // PATH: /room-types/{name}/allowed-objects
+ ///
+
+ // METHOD: GET
+ public getAllowedObjectsByRoomType(name: string): Promise<string[]> {
+ return ServerConnection.send({
+ path: "/room-types/{name}/allowed-objects",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ name
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}
+ ///
+
+ // METHOD: GET
+ public getRoom(simulationId: number, datacenterId: number, roomId: number): Promise<IRoom> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.loadRoomTiles(simulationId, datacenterId, data);
+ });
+ }
+
+ // METHOD: PUT
+ public updateRoom(simulationId: number, datacenterId: number, room: IRoom): Promise<IRoom> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}",
+ method: "PUT",
+ parameters: {
+ body: {
+ room: Util.packageForSending(room)
+ },
+ path: {
+ simulationId,
+ datacenterId,
+ roomId: room.id
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.loadRoomTiles(simulationId, datacenterId, data);
+ });
+ }
+
+ // METHOD: DELETE
+ public deleteRoom(simulationId: number, datacenterId: number, roomId: number): Promise<IRoom> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles
+ ///
+
+ // METHOD: GET
+ public getTilesByRoom(simulationId: number, datacenterId: number, roomId: number): Promise<ITile[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ let promises = data.map((item) => {
+ return this.loadTileObject(simulationId, datacenterId, roomId, item);
+ });
+
+ return Promise.all(promises).then(() => {
+ return data;
+ })
+ });
+ }
+
+ // METHOD: POST
+ public addTileToRoom(simulationId: number, datacenterId: number, roomId: number, tile: ITile): Promise<ITile> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles",
+ method: "POST",
+ parameters: {
+ body: {
+ tile: Util.packageForSending(tile)
+ },
+ path: {
+ simulationId,
+ datacenterId,
+ roomId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.loadTileObject(simulationId, datacenterId, roomId, data);
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}
+ ///
+
+ // METHOD: GET
+ // Not needed (yet)
+
+ // METHOD: DELETE
+ public deleteTile(simulationId: number, datacenterId: number, roomId: number, tileId: number): Promise<ITile> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/cooling-item
+ ///
+
+ // METHOD: GET
+ public getCoolingItem(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number): Promise<ICoolingItem> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/cooling-item",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.loadFailureModel(data);
+ });
+ }
+
+ // METHOD: POST
+ public addCoolingItem(simulationId: number, datacenterId: number, roomId: number, tileId: number,
+ coolingItem: ICoolingItem): Promise<ICoolingItem> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/cooling-item",
+ method: "POST",
+ parameters: {
+ body: {
+ coolingItem: Util.packageForSending(coolingItem)
+ },
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.loadFailureModel(data);
+ });
+ }
+
+ // METHOD: PUT
+ // Not needed (yet)
+
+ // METHOD: DELETE
+ public deleteCoolingItem(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number): Promise<ICoolingItem> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/cooling-item",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/psu
+ ///
+
+ // METHOD: GET
+ public getPSU(simulationId: number, datacenterId: number, roomId: number, tileId: number): Promise<IPSU> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/psu",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.loadFailureModel(data);
+ });
+ }
+
+ // METHOD: POST
+ public addPSU(simulationId: number, datacenterId: number, roomId: number, tileId: number,
+ psu: IPSU): Promise<IPSU> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/psu",
+ method: "POST",
+ parameters: {
+ body: {
+ psu: Util.packageForSending(psu)
+ },
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.loadFailureModel(data);
+ });
+ }
+
+ // METHOD: PUT
+ // Not needed (yet)
+
+ // METHOD: DELETE
+ public deletePSU(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number): Promise<IPSU> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/psu",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack
+ ///
+
+ // METHOD: GET
+ public getRack(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number): Promise<IRack> {
+ let rack = {};
+
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ rack = data;
+ return this.getMachinesByRack(simulationId, datacenterId, roomId, tileId);
+ }).then((machines: any) => {
+ let promises = machines.map((machine) => {
+ return this.loadMachineUnits(machine);
+ });
+
+
+ return Promise.all(promises).then(() => {
+ rack["machines"] = [];
+
+ machines.forEach((machine: IMachine) => {
+ rack["machines"][machine.position] = machine;
+ });
+
+ for (let i = 0; i < rack["capacity"]; i++) {
+ if (rack["machines"][i] === undefined) {
+ rack["machines"][i] = null;
+ }
+ }
+
+ return rack;
+ });
+ });
+ }
+
+ // METHOD: POST
+ public addRack(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number, rack: IRack): Promise<IRack> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack",
+ method: "POST",
+ parameters: {
+ body: {
+ rack: Util.packageForSending(rack)
+ },
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ data.machines = [];
+
+ for (let i = 0; i < data.capacity; i++) {
+ data.machines.push(null);
+ }
+
+ return data;
+ });
+ }
+
+ // METHOD: PUT
+ public updateRack(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number, rack: IRack): Promise<IRack> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack",
+ method: "PUT",
+ parameters: {
+ body: {
+ rack
+ },
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ data.machines = rack.machines;
+
+ return data;
+ });
+ }
+
+ // METHOD: DELETE
+ public deleteRack(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number): Promise<IRack> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack/machines
+ ///
+
+ // METHOD: GET
+ public getMachinesByRack(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number): Promise<IMachine[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack/machines",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ let promises = data.map((machine) => {
+ return this.loadMachineUnits(machine);
+ });
+
+ return Promise.all(promises).then(() => {
+ return data;
+ });
+ });
+ }
+
+ // METHOD: POST
+ public addMachineToRack(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number, machine: IMachine): Promise<IMachine> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack/machines",
+ method: "POST",
+ parameters: {
+ body: {
+ machine: Util.packageForSending(machine)
+ },
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.loadMachineUnits(data);
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack/machines/{position}
+ ///
+
+ // METHOD: GET
+ // Not needed (yet)
+
+ // METHOD: PUT
+ public updateMachine(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number, machine: IMachine): Promise<IMachine> {
+ machine["tags"] = [];
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack/machines/{position}",
+ method: "PUT",
+ parameters: {
+ body: {
+ machine: Util.packageForSending(machine)
+ },
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId,
+ position: machine.position
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.loadMachineUnits(data);
+ });
+ }
+
+ // METHOD: DELETE
+ public deleteMachine(simulationId: number, datacenterId: number, roomId: number,
+ tileId: number, position: number): Promise<any> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/datacenters/{datacenterId}/rooms/{roomId}/tiles/{tileId}/rack/machines/{position}",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ datacenterId,
+ roomId,
+ tileId,
+ position
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/experiments
+ ///
+
+ // METHOD: GET
+ public getExperimentsBySimulation(simulationId: number): Promise<IExperiment[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/experiments",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ let promises = data.map((item: any) => {
+ return this.getTrace(item.traceId).then((traceData: any) => {
+ item.trace = traceData;
+ });
+ });
+ return Promise.all(promises).then(() => {
+ return data;
+ });
+ });
+ }
+
+ // METHOD: POST
+ public addExperimentToSimulation(simulationId: number, experiment: IExperiment): Promise<IExperiment> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/experiments",
+ method: "POST",
+ parameters: {
+ body: {
+ experiment: Util.packageForSending(experiment)
+ },
+ path: {
+ simulationId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.getTrace(data.traceId).then((traceData: any) => {
+ data.trace = traceData;
+
+ return data;
+ });
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/experiments/{experimentId}
+ ///
+
+ // METHOD: GET
+ // Not needed (yet)
+
+ // METHOD: PUT
+ public updateExperiment(experiment: IExperiment): Promise<IExperiment> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/experiments/{experimentId}",
+ method: "PUT",
+ parameters: {
+ body: {
+ experiment: Util.packageForSending(experiment)
+ },
+ path: {
+ experimentId: experiment.id,
+ simulationId: experiment.simulationId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.getTrace(data.traceId).then((traceData: any) => {
+ data.trace = traceData;
+
+ return data;
+ });
+ });
+ }
+
+ // METHOD: DELETE
+ public deleteExperiment(simulationId: number, experimentId: number): Promise<any> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/experiments/{experimentId}",
+ method: "DELETE",
+ parameters: {
+ body: {},
+ path: {
+ experimentId,
+ simulationId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/experiments/{experimentId}/last-simulated-tick
+ ///
+
+ // METHOD: GET
+ public getLastSimulatedTickByExperiment(simulationId: number, experimentId: number): Promise<number> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/experiments/{experimentId}/last-simulated-tick",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ experimentId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return data.lastSimulatedTick;
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/experiments/{experimentId}/machine-states
+ ///
+
+ // METHOD: GET
+ public getMachineStatesByTick(simulationId: number, experimentId: number, tick: number,
+ machines: {[keys: number]: IMachine}): Promise<IMachineState[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/experiments/{experimentId}/machine-states",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ experimentId
+ },
+ query: {
+ tick
+ }
+ }
+ }).then((data: any) => {
+ data.forEach((item: any) => {
+ item.machine = machines[item.machineId];
+ });
+
+ return data;
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/experiments/{experimentId}/rack-states
+ ///
+
+ // METHOD: GET
+ public getRackStatesByTick(simulationId: number, experimentId: number, tick: number,
+ racks: {[keys: number]: IRack}): Promise<IRackState[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/experiments/{experimentId}/rack-states",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ experimentId
+ },
+ query: {
+ tick
+ }
+ }
+ }).then((data: any) => {
+ let promises = data.map((item: any) => {
+ item.rack = racks[item.rackId];
+ });
+
+ return Promise.all(promises).then(() => {
+ return data;
+ });
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/experiments/{experimentId}/room-states
+ ///
+
+ // METHOD: GET
+ public getRoomStatesByTick(simulationId: number, experimentId: number, tick: number,
+ rooms: {[keys: number]: IRoom}): Promise<IRoomState[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/experiments/{experimentId}/room-states",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ experimentId
+ },
+ query: {
+ tick
+ }
+ }
+ }).then((data: any) => {
+ let promises = data.map((item: any) => {
+ item.room = rooms[item.roomId];
+ });
+
+ return Promise.all(promises).then(() => {
+ return data;
+ });
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/experiments/{experimentId}/task-states
+ ///
+
+ // METHOD: GET
+ public getTaskStatesByTick(simulationId: number, experimentId: number, tick: number,
+ tasks: {[keys: number]: ITask}): Promise<ITaskState[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/experiments/{experimentId}/task-states",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ experimentId
+ },
+ query: {
+ tick
+ }
+ }
+ }).then((data: any) => {
+ let promises = data.map((item: any) => {
+ item.task = tasks[item.taskId];
+ });
+
+ return Promise.all(promises).then(() => {
+ return data;
+ });
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/paths
+ ///
+
+ // METHOD: GET
+ public getPathsBySimulation(simulationId: number): Promise<IPath[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/paths",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ let promises = data.map((item: any) => {
+ return this.getSectionsByPath(simulationId, item.id).then((sectionsData: any) => {
+ item.sections = sectionsData;
+ });
+ });
+ return Promise.all(promises).then(() => {
+ return data;
+ });
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/paths/{pathId}
+ ///
+
+ // METHOD: GET
+ public getPath(simulationId: number, pathId: number): Promise<IPath> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/paths/{pathId}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ pathId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.getSectionsByPath(simulationId, pathId).then((sectionsData: any) => {
+ data.sections = sectionsData;
+ return data;
+ });
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/paths/{pathId}/branches
+ ///
+
+ // METHOD: GET
+ public getBranchesByPath(simulationId: number, pathId: number): Promise<IPath[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/paths/{pathId}/branches",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ pathId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ let promises = data.map((item: any) => {
+ return this.getSectionsByPath(simulationId, item.id).then((sectionsData: any) => {
+ item.sections = sectionsData;
+ });
+ });
+ return Promise.all(promises).then(() => {
+ return data;
+ });
+ });
+ }
+
+ // METHOD: POST
+ public branchFromPath(simulationId: number, pathId: number, startTick: number): Promise<IPath> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/paths/{pathId}/branches",
+ method: "POST",
+ parameters: {
+ body: {
+ section: {
+ startTick
+ }
+ },
+ path: {
+ simulationId,
+ pathId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.getSectionsByPath(simulationId, data.id).then((sectionsData: any) => {
+ data.sections = sectionsData;
+ return data;
+ });
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/paths/{pathId}/sections
+ ///
+
+ // METHOD: GET
+ public getSectionsByPath(simulationId: number, pathId: number): Promise<IPath[]> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/paths/{pathId}/sections",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ pathId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ let promises = data.map((path: ISection) => {
+ return this.getDatacenter(simulationId, path.datacenterId).then((datacenter: any) => {
+ path.datacenter = datacenter;
+ });
+ });
+ return Promise.all(promises).then(() => {
+ return data;
+ });
+ });
+ }
+
+ ///
+ // PATH: /simulations/{simulationId}/paths/{pathId}/sections/{sectionId}
+ ///
+
+ // METHOD: GET
+ public getSection(simulationId: number, pathId: number, sectionId: number): Promise<ISection> {
+ return ServerConnection.send({
+ path: "/simulations/{simulationId}/paths/{pathId}/sections/{sectionId}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ simulationId,
+ pathId,
+ sectionId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ return this.getDatacenter(simulationId, data.datacenterId).then((datacenter: any) => {
+ data.datacenter = datacenter;
+ return data;
+ });
+ });
+ }
+
+ ///
+ // PATH: /specifications/psus
+ ///
+
+ // METHOD: GET
+ public getAllPSUSpecs(): Promise<IPSU[]> {
+ let psus;
+ return ServerConnection.send({
+ path: "/specifications/psus",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {},
+ query: {}
+ }
+ }).then((data: any) => {
+ psus = data;
+
+ let promises = [];
+ data.forEach((psu: IPSU) => {
+ promises.push(this.getFailureModel(psu.failureModelId));
+ });
+ return Promise.all(promises);
+ }).then((data: any) => {
+ return psus;
+ });
+ }
+
+ ///
+ // PATH: /specifications/psus/{id}
+ ///
+
+ // METHOD: GET
+ public getPSUSpec(id: number): Promise<IPSU> {
+ let psu;
+
+ return ServerConnection.send({
+ path: "/specifications/psus/{id}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ id
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ psu = data;
+ return this.getFailureModel(data.failureModelId);
+ }).then((data: any) => {
+ psu.failureModel = data;
+ return psu;
+ });
+ }
+
+ ///
+ // PATH: /specifications/cooling-items
+ ///
+
+ // METHOD: GET
+ public getAllCoolingItemSpecs(): Promise<ICoolingItem[]> {
+ let coolingItems;
+
+ return ServerConnection.send({
+ path: "/specifications/cooling-items",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {},
+ query: {}
+ }
+ }).then((data: any) => {
+ coolingItems = data;
+
+ let promises = [];
+ data.forEach((item: ICoolingItem) => {
+ promises.push(this.getFailureModel(item.failureModelId));
+ });
+ return Promise.all(promises);
+ }).then((data: any) => {
+ return coolingItems;
+ });
+ }
+
+ ///
+ // PATH: /specifications/cooling-items/{id}
+ ///
+
+ // METHOD: GET
+ public getCoolingItemSpec(id: number): Promise<IPSU> {
+ let coolingItem;
+
+ return ServerConnection.send({
+ path: "/specifications/cooling-items/{id}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ id
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ coolingItem = data;
+ return this.getFailureModel(data.failureModelId);
+ }).then((data: any) => {
+ coolingItem.failureModel = data;
+ return coolingItem;
+ });
+ }
+
+ ///
+ // PATH: /schedulers
+ ///
+
+ // METHOD: GET
+ public getAllSchedulers(): Promise<IScheduler[]> {
+ return ServerConnection.send({
+ path: "/schedulers",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {},
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /traces
+ ///
+
+ // METHOD: GET
+ public getAllTraces(): Promise<ITrace[]> {
+ return ServerConnection.send({
+ path: "/traces",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {},
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /traces/{traceId}
+ ///
+
+ // METHOD: GET
+ public getTrace(traceId: number): Promise<ITrace> {
+ let trace;
+
+ return ServerConnection.send({
+ path: "/traces/{traceId}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ traceId
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ trace = data;
+ return this.getTasksByTrace(traceId);
+ }).then((data: any) => {
+ trace.tasks = data;
+ return trace;
+ });
+ }
+
+ ///
+ // PATH: /traces/{traceId}/tasks
+ ///
+
+ // METHOD: GET
+ public getTasksByTrace(traceId: number): Promise<ITask[]> {
+ return ServerConnection.send({
+ path: "/traces/{traceId}/tasks",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ traceId
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /specifications/failure-models
+ ///
+
+ // METHOD: GET
+ public getAllFailureModels(): Promise<IFailureModel[]> {
+ return ServerConnection.send({
+ path: "/specifications/failure-models",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {},
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /specifications/failure-models/{id}
+ ///
+
+ // METHOD: GET
+ public getFailureModel(id: number): Promise<IFailureModel> {
+ return ServerConnection.send({
+ path: "/specifications/failure-models/{id}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ id
+ },
+ query: {}
+ }
+ });
+ }
+
+ ///
+ // PATH: /specifications/[units]
+ ///
+
+ // METHOD: GET
+ public getAllSpecificationsOfType(typePlural: string): Promise<INodeUnit> {
+ let specs: any;
+ return ServerConnection.send({
+ path: "/specifications/" + typePlural,
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {},
+ query: {}
+ }
+ }).then((data: any) => {
+ specs = data;
+
+ let promises = [];
+ data.forEach((unit: INodeUnit) => {
+ promises.push(this.getFailureModel(unit.failureModelId));
+ });
+ return Promise.all(promises);
+ }).then((data: any) => {
+ return specs;
+ });
+ }
+
+ ///
+ // PATH: /specifications/[units]/{id}
+ ///
+
+ // METHOD: GET
+ public getSpecificationOfType(typePlural: string, id: number): Promise<INodeUnit> {
+ let spec;
+
+ return ServerConnection.send({
+ path: "/specifications/" + typePlural + "/{id}",
+ method: "GET",
+ parameters: {
+ body: {},
+ path: {
+ id
+ },
+ query: {}
+ }
+ }).then((data: any) => {
+ spec = data;
+ return this.getFailureModel(data.failureModelId);
+ }).then((data: any) => {
+ spec.failureModel = data;
+ return spec;
+ });
+ }
+
+
+ ///
+ // HELPER METHODS
+ ///
+
+ private loadRoomTiles(simulationId: number, datacenterId: number, room: IRoom): Promise<IRoom> {
+ return this.getTilesByRoom(simulationId, datacenterId, room.id).then((data: any) => {
+ room.tiles = data;
+ return room;
+ });
+ }
+
+ private loadTileObject(simulationId: number, datacenterId: number, roomId: number, tile: ITile): Promise<ITile> {
+ let promise;
+
+ switch (tile.objectType) {
+ case "RACK":
+ promise = this.getRack(simulationId, datacenterId, roomId, tile.id).then((data: IRack) => {
+ tile.object = data;
+ });
+ break;
+ case "PSU":
+ promise = this.getPSU(simulationId, datacenterId, roomId, tile.id).then((data: IPSU) => {
+ tile.object = data;
+ });
+ break;
+ case "COOLING_ITEM":
+ promise = this.getCoolingItem(simulationId, datacenterId, roomId, tile.id).then((data: ICoolingItem) => {
+ tile.object = data;
+ });
+ break;
+ default:
+ promise = new Promise((resolve, reject) => {
+ resolve(undefined);
+ });
+ }
+
+ return promise.then(() => {
+ return tile;
+ })
+ }
+
+ private parseSimulationTimestamps(simulation: ISimulation): void {
+ simulation.datetimeCreatedParsed = Util.parseDateTime(simulation.datetimeCreated);
+ simulation.datetimeLastEditedParsed = Util.parseDateTime(simulation.datetimeLastEdited);
+ }
+
+ private loadFailureModel(data: any): Promise<any> {
+ return this.getFailureModel(data.failureModelId).then((failureModel: IFailureModel) => {
+ data.failureModel = failureModel;
+ return data;
+ });
+ }
+
+ private loadUnitsOfType(idListName: string, objectListName: string, machine: IMachine): Promise<IMachine> {
+ machine[objectListName] = [];
+
+ let promises = machine[idListName].map((item) => {
+ return this.getSpecificationOfType(objectListName, item).then((data) => {
+ machine[objectListName].push(data);
+ });
+ });
+
+ return Promise.all(promises).then(() => {
+ return machine;
+ })
+ }
+
+ private loadMachineUnits(machine: IMachine): Promise<IMachine> {
+ let listNames = [
+ {
+ idListName: "cpuIds",
+ objectListName: "cpus"
+ }, {
+ idListName: "gpuIds",
+ objectListName: "gpus"
+ }, {
+ idListName: "memoryIds",
+ objectListName: "memories"
+ }, {
+ idListName: "storageIds",
+ objectListName: "storages"
+ }
+ ];
+
+ let promises = listNames.map((item: any) => {
+ return this.loadUnitsOfType(item.idListName, item.objectListName, machine);
+ });
+
+ return Promise.all(promises).then(() => {
+ return machine;
+ });
+ }
+} \ No newline at end of file
diff --git a/src/scripts/controllers/connection/cache.ts b/src/scripts/controllers/connection/cache.ts
new file mode 100644
index 00000000..15517519
--- /dev/null
+++ b/src/scripts/controllers/connection/cache.ts
@@ -0,0 +1,85 @@
+export enum CacheStatus {
+ MISS,
+ FETCHING,
+ HIT,
+ NOT_CACHABLE
+}
+
+
+interface ICachableObject {
+ status: CacheStatus;
+ object: any;
+ callbacks: any[];
+}
+
+
+export class CacheController {
+ private static CACHABLE_ROUTES = [
+ "/specifications/psus/{id}",
+ "/specifications/cooling-items/{id}",
+ "/specifications/cpus/{id}",
+ "/specifications/gpus/{id}",
+ "/specifications/memories/{id}",
+ "/specifications/storages/{id}",
+ "/specifications/failure-models/{id}",
+ ];
+
+ // Maps every route name to a map of IDs => objects
+ private routeCaches: { [keys: string]: { [keys: number]: ICachableObject } };
+
+
+ constructor() {
+ this.routeCaches = {};
+
+ CacheController.CACHABLE_ROUTES.forEach((routeName: string) => {
+ this.routeCaches[routeName] = {};
+ })
+ }
+
+ public checkCache(request: IRequest): CacheStatus {
+ if (request.method === "GET" && CacheController.CACHABLE_ROUTES.indexOf(request.path) !== -1) {
+ if (this.routeCaches[request.path][request.parameters.path["id"]] === undefined) {
+ this.routeCaches[request.path][request.parameters.path["id"]] = {
+ status: CacheStatus.MISS,
+ object: null,
+ callbacks: []
+ };
+ return CacheStatus.MISS;
+ } else {
+ return this.routeCaches[request.path][request.parameters.path["id"]].status;
+ }
+ } else {
+ return CacheStatus.NOT_CACHABLE;
+ }
+ }
+
+ public fetchFromCache(request: IRequest): any {
+ return this.routeCaches[request.path][request.parameters.path["id"]].object;
+ }
+
+ public setToFetching(request: IRequest): void {
+ this.routeCaches[request.path][request.parameters.path["id"]].status = CacheStatus.FETCHING;
+ }
+
+ public onFetch(request: IRequest, response: IResponse): any {
+ let pathWithoutVersion = request.path.replace(/\/v\d+/, "");
+ this.routeCaches[pathWithoutVersion][request.parameters.path["id"]].status = CacheStatus.HIT;
+ this.routeCaches[pathWithoutVersion][request.parameters.path["id"]].object = response.content;
+
+ this.routeCaches[pathWithoutVersion][request.parameters.path["id"]].callbacks.forEach((callback) => {
+ callback({
+ status: {
+ code: 200
+ },
+ content: response.content,
+ id: request.id
+ });
+ });
+
+ this.routeCaches[pathWithoutVersion][request.parameters.path["id"]].callbacks = [];
+ }
+
+ public registerCallback(request: IRequest, callback): any {
+ this.routeCaches[request.path][request.parameters.path["id"]].callbacks.push(callback);
+ }
+}
diff --git a/src/scripts/controllers/connection/socket.ts b/src/scripts/controllers/connection/socket.ts
new file mode 100644
index 00000000..b38c303f
--- /dev/null
+++ b/src/scripts/controllers/connection/socket.ts
@@ -0,0 +1,76 @@
+import {CacheController, CacheStatus} from "./cache";
+import * as io from "socket.io-client";
+
+
+export class SocketController {
+ private static id = 1;
+ private _socket: SocketIOClient.Socket;
+ private _cacheController: CacheController;
+
+ // Mapping from request IDs to their registered callbacks
+ private callbacks: { [keys: number]: (response: IResponse) => any };
+
+
+ constructor(onConnect: () => any) {
+ this.callbacks = {};
+ this._cacheController = new CacheController();
+
+ this._socket = io.connect('https://opendc.ewi.tudelft.nl:443');
+ this._socket.on('connect', onConnect);
+
+ this._socket.on('response', (jsonResponse: string) => {
+ let response: IResponse = JSON.parse(jsonResponse);
+ console.log("Response, ID:", response.id, response);
+ this.callbacks[response.id](response);
+ delete this.callbacks[response.id];
+ });
+ }
+
+ /**
+ * Sends a request to the server socket and registers the callback to be triggered on response.
+ *
+ * @param request The request instance to be sent
+ * @param callback A function to be called with the response object once the socket has received a response
+ */
+ public sendRequest(request: IRequest, callback: (response: IResponse) => any): void {
+ // Check local cache, in case request is for cachable GET route
+ let cacheStatus = this._cacheController.checkCache(request);
+
+ if (cacheStatus === CacheStatus.HIT) {
+ callback({
+ status: {
+ code: 200
+ },
+ content: this._cacheController.fetchFromCache(request),
+ id: -1
+ });
+ } else if (cacheStatus === CacheStatus.FETCHING) {
+ this._cacheController.registerCallback(request, callback);
+ } else if (cacheStatus === CacheStatus.MISS || cacheStatus === CacheStatus.NOT_CACHABLE) {
+ if (!this._socket.connected) {
+ console.error("Socket not connected, sending request failed");
+ }
+
+ if (cacheStatus === CacheStatus.MISS) {
+ this._cacheController.setToFetching(request);
+
+ this.callbacks[SocketController.id] = (response: IResponse) => {
+ this._cacheController.onFetch(request, response);
+ callback(response);
+ };
+ } else {
+ this.callbacks[SocketController.id] = callback;
+ }
+
+ // Setup request object
+ request.id = SocketController.id;
+ request.token = localStorage.getItem("googleToken");
+ request.path = "/v1" + request.path;
+
+ console.log("Request, ID:", request.id, request);
+ this._socket.emit("request", request);
+
+ SocketController.id++;
+ }
+ }
+} \ No newline at end of file
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";
+ }
+}
diff --git a/src/scripts/controllers/modes/building.ts b/src/scripts/controllers/modes/building.ts
new file mode 100644
index 00000000..4d82f090
--- /dev/null
+++ b/src/scripts/controllers/modes/building.ts
@@ -0,0 +1,114 @@
+import {InteractionMode, MapController} from "../mapcontroller";
+import {MapView} from "../../views/mapview";
+import * as $ from "jquery";
+
+
+/**
+ * Class responsible for handling building mode interactions.
+ */
+export class BuildingModeController {
+ public newRoomId: number;
+
+ private mapController: MapController;
+ private mapView: MapView;
+
+
+ constructor(mapController: MapController) {
+ this.mapController = mapController;
+ this.mapView = this.mapController.mapView;
+ }
+
+ /**
+ * Connects all DOM event listeners to their respective element targets.
+ */
+ public setupEventListeners() {
+ let resetConstructionButtons = () => {
+ this.mapController.interactionMode = InteractionMode.DEFAULT;
+ this.mapView.hoverLayer.setHoverTileVisibility(false);
+ $("#room-construction").text("Construct new room");
+ $("#room-construction-cancel").slideToggle(300);
+ };
+
+ // Room construction button
+ $("#room-construction").on("click", (event: JQueryEventObject) => {
+ if (this.mapController.interactionMode === InteractionMode.DEFAULT) {
+ this.mapController.interactionMode = InteractionMode.SELECT_ROOM;
+ this.mapView.hoverLayer.setHoverTileVisibility(true);
+ this.mapController.api.addRoomToDatacenter(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id).then((room: IRoom) => {
+ this.newRoomId = room.id;
+ });
+ $(event.target).text("Finalize room");
+ $("#room-construction-cancel").slideToggle(300);
+ } else if (this.mapController.interactionMode === InteractionMode.SELECT_ROOM) {
+ resetConstructionButtons();
+ this.finalizeRoom();
+ }
+ });
+
+ // Cancel button for room construction
+ $("#room-construction-cancel").on("click", () => {
+ resetConstructionButtons();
+ this.cancelRoomConstruction();
+ });
+ }
+
+ /**
+ * Cancels room construction and deletes the temporary room created previously.
+ */
+ public cancelRoomConstruction() {
+ this.mapController.api.deleteRoom(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.newRoomId).then(() => {
+ this.mapView.roomLayer.cancelRoomConstruction();
+ });
+ }
+
+ /**
+ * Finalizes room construction by triggering a redraw of the room layer with the new room added.
+ */
+ public finalizeRoom() {
+ this.mapController.api.getRoom(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.newRoomId).then((room: IRoom) => {
+ this.mapView.roomLayer.finalizeRoom(room);
+ });
+ }
+
+ /**
+ * Adds a newly selected tile to the list of selected tiles.
+ *
+ * @param position The new tile position to be added
+ */
+ public addSelectedTile(position: IGridPosition): void {
+ let tile = {
+ id: -1,
+ roomId: this.newRoomId,
+ position: {x: position.x, y: position.y}
+ };
+ this.mapController.api.addTileToRoom(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.newRoomId, tile).then((tile: ITile) => {
+ this.mapView.roomLayer.addSelectedTile(tile);
+ });
+ }
+
+ /**
+ * Removes a previously selected tile.
+ *
+ * @param position The position of the tile to be removed
+ */
+ public removeSelectedTile(position: IGridPosition): void {
+ let tile;
+ let objectIndex = -1;
+
+ for (let i = 0; i < this.mapView.roomLayer.selectedTileObjects.length; i++) {
+ tile = this.mapView.roomLayer.selectedTileObjects[i];
+ if (tile.position.x === position.x && tile.position.y === position.y) {
+ objectIndex = i;
+ }
+ }
+ this.mapController.api.deleteTile(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.newRoomId,
+ this.mapView.roomLayer.selectedTileObjects[objectIndex].tileObject.id).then(() => {
+ this.mapView.roomLayer.removeSelectedTile(position, objectIndex);
+ });
+ }
+} \ No newline at end of file
diff --git a/src/scripts/controllers/modes/node.ts b/src/scripts/controllers/modes/node.ts
new file mode 100644
index 00000000..3b3f8a32
--- /dev/null
+++ b/src/scripts/controllers/modes/node.ts
@@ -0,0 +1,297 @@
+import {MapController, AppMode, InteractionLevel} from "../mapcontroller";
+import {MapView} from "../../views/mapview";
+import * as $ from "jquery";
+
+
+/**
+ * Class responsible for rendering node mode and handling UI interactions within it.
+ */
+export class NodeModeController {
+ public currentMachine: IMachine;
+
+ private mapController: MapController;
+ private mapView: MapView;
+
+
+ constructor(mapController: MapController) {
+ this.mapController = mapController;
+ this.mapView = this.mapController.mapView;
+
+ this.loadAddDropdowns();
+ }
+
+ /**
+ * Moves the UI model into node mode.
+ *
+ * @param machine The machine that was selected in rack mode
+ */
+ public enterMode(machine: IMachine): void {
+ this.currentMachine = machine;
+ this.populateUnitLists();
+ $("#node-menu").removeClass("hidden");
+
+ if (this.mapController.appMode === AppMode.SIMULATION) {
+ this.mapController.simulationController.transitionFromRackToNode();
+ }
+ }
+
+ /**
+ * Performs cleanup and closing actions before allowing transferal to rack mode.
+ */
+ public goToObjectMode(): void {
+ $("#node-menu").addClass("hidden");
+ $(".node-element-overlay").addClass("hidden");
+ this.currentMachine = undefined;
+ this.mapController.interactionLevel = InteractionLevel.OBJECT;
+
+ if (this.mapController.appMode === AppMode.SIMULATION) {
+ this.mapController.simulationController.transitionFromNodeToRack();
+ }
+ }
+
+ /**
+ * Connects all DOM event listeners to their respective element targets.
+ */
+ public setupEventListeners(): void {
+ let nodeMenu = $("#node-menu");
+
+ nodeMenu.find(".panel-group").on("click", ".remove-unit", (event: JQueryEventObject) => {
+ MapController.showConfirmDeleteDialog("unit", () => {
+ let index = $(event.target).closest(".panel").index();
+
+ if (index === -1) {
+ return;
+ }
+
+ let closestTabPane = $(event.target).closest(".panel-group");
+
+ let objectList, idList;
+ if (closestTabPane.is("#cpu-accordion")) {
+ objectList = this.currentMachine.cpus;
+ idList = this.currentMachine.cpuIds;
+ } else if (closestTabPane.is("#gpu-accordion")) {
+ objectList = this.currentMachine.gpus;
+ idList = this.currentMachine.gpuIds;
+ } else if (closestTabPane.is("#memory-accordion")) {
+ objectList = this.currentMachine.memories;
+ idList = this.currentMachine.memoryIds;
+ } else if (closestTabPane.is("#storage-accordion")) {
+ objectList = this.currentMachine.storages;
+ idList = this.currentMachine.storageIds;
+ }
+
+ idList.splice(idList.indexOf(objectList[index]).id, 1);
+ objectList.splice(index, 1);
+
+ this.mapController.api.updateMachine(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id,
+ this.mapController.objectModeController.currentObjectTile.id, this.currentMachine).then(
+ () => {
+ this.populateUnitLists();
+ this.mapController.objectModeController.updateNodeComponentOverlays();
+ });
+ });
+ });
+
+ nodeMenu.find(".add-unit").on("click", (event: JQueryEventObject) => {
+ let dropdown = $(event.target).closest(".input-group-btn").siblings("select").first();
+
+ let closestTabPane = $(event.target).closest(".input-group").siblings(".panel-group").first();
+ let objectList, idList, typePlural;
+ if (closestTabPane.is("#cpu-accordion")) {
+ objectList = this.currentMachine.cpus;
+ idList = this.currentMachine.cpuIds;
+ typePlural = "cpus";
+ } else if (closestTabPane.is("#gpu-accordion")) {
+ objectList = this.currentMachine.gpus;
+ idList = this.currentMachine.gpuIds;
+ typePlural = "gpus";
+ } else if (closestTabPane.is("#memory-accordion")) {
+ objectList = this.currentMachine.memories;
+ idList = this.currentMachine.memoryIds;
+ typePlural = "memories";
+ } else if (closestTabPane.is("#storage-accordion")) {
+ objectList = this.currentMachine.storages;
+ idList = this.currentMachine.storageIds;
+ typePlural = "storages";
+ }
+
+ if (idList.length + 1 > 4) {
+ this.mapController.showInfoBalloon("Machine has only 4 slots", "warning");
+ return;
+ }
+
+ let id = parseInt(dropdown.val(), 10);
+ idList.push(id);
+ this.mapController.api.getSpecificationOfType(typePlural, id).then((spec: INodeUnit) => {
+ objectList.push(spec);
+
+ this.mapController.api.updateMachine(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id,
+ this.mapController.objectModeController.currentObjectTile.id, this.currentMachine).then(
+ () => {
+ this.populateUnitLists();
+ this.mapController.objectModeController.updateNodeComponentOverlays();
+ });
+ });
+ });
+ }
+
+ /**
+ * Populates the "add" dropdowns with all available unit options.
+ */
+ private loadAddDropdowns(): void {
+ let unitTypes = [
+ "cpus", "gpus", "memories", "storages"
+ ];
+ let dropdowns = [
+ $("#add-cpu-form").find("select"),
+ $("#add-gpu-form").find("select"),
+ $("#add-memory-form").find("select"),
+ $("#add-storage-form").find("select"),
+ ];
+
+ unitTypes.forEach((type: string, index: number) => {
+ this.mapController.api.getAllSpecificationsOfType(type).then((data: any) => {
+ data.forEach((option: INodeUnit) => {
+ dropdowns[index].append($("<option>").val(option.id).text(option.manufacturer + " " + option.family +
+ " " + option.model + " (" + option.generation + ")"));
+ });
+ });
+ });
+ }
+
+ /**
+ * Generates and inserts dynamically HTML code concerning all units of a machine.
+ */
+ private populateUnitLists(): void {
+ // Contains the skeleton of a unit element and inserts the given data into it
+ let generatePanel = (type: string, index: number, list: any, specSection: string): string => {
+ return '<div class="panel panel-default">' +
+ ' <div class="panel-heading">' +
+ ' <h4 class="panel-title">' +
+ ' <a class="glyphicon glyphicon-remove remove-unit" href="javascript:void(0)"></a>' +
+ ' <a class="accordion-toggle collapsed" data-toggle="collapse" data-parent="#' + type + '-accordion"' +
+ ' href="#' + type + '-' + index + '">' +
+ list[index].manufacturer + ' ' + list[index].family + ' ' + list[index].model +
+ ' </a>' +
+ ' </h4>' +
+ ' </div>' +
+ ' <div id="' + type + '-' + index + '" class="panel-collapse collapse">' +
+ ' <table class="spec-table">' +
+ ' <tbody>' +
+ specSection +
+ ' </tbody>' +
+ ' </table>' +
+ ' </div>' +
+ '</div>';
+ };
+
+ // Generates the structure of the specification list of a processing unit
+ let generateProcessingUnitHtml = (element: IProcessingUnit) => {
+ return ' <tr>' +
+ ' <td class="glyphicon glyphicon-tasks"></td>' +
+ ' <td>Number of Cores</td>' +
+ ' <td>' + element.numberOfCores + '</td>' +
+ ' </tr>' +
+ ' <tr>' +
+ ' <td class="glyphicon glyphicon-dashboard"></td>' +
+ ' <td>Clockspeed (MHz)</td>' +
+ ' <td>' + element.clockRateMhz + '</td>' +
+ ' </tr>' +
+ ' <tr>' +
+ ' <td class="glyphicon glyphicon-flash"></td>' +
+ ' <td>Energy Consumption (W)</td>' +
+ ' <td>' + element.energyConsumptionW + '</td>' +
+ ' </tr>' +
+ ' <tr>' +
+ ' <td class="glyphicon glyphicon-alert"></td>' +
+ ' <td>Failure Rate (%)</td>' +
+ ' <td>' + element.failureModel.rate + '</td>' +
+ ' </tr>';
+ };
+
+ // Generates the structure of the spec list of a storage unit
+ let generateStorageUnitHtml = (element: IStorageUnit) => {
+ return ' <tr>' +
+ ' <td class="glyphicon glyphicon-floppy-disk"></td>' +
+ ' <td>Size (Mb)</td>' +
+ ' <td>' + element.sizeMb + '</td>' +
+ ' </tr>' +
+ ' <tr>' +
+ ' <td class="glyphicon glyphicon-dashboard"></td>' +
+ ' <td>Speed (Mb/s)</td>' +
+ ' <td>' + element.speedMbPerS + '</td>' +
+ ' </tr>' +
+ ' <tr>' +
+ ' <td class="glyphicon glyphicon-flash"></td>' +
+ ' <td>Energy Consumption (W)</td>' +
+ ' <td>' + element.energyConsumptionW + '</td>' +
+ ' </tr>' +
+ ' <tr>' +
+ ' <td class="glyphicon glyphicon-alert"></td>' +
+ ' <td>Failure Rate (%)</td>' +
+ ' <td>' + element.failureModel.rate + '</td>' +
+ ' </tr>';
+ };
+
+ // Inserts a "No units" message into the container of the given unit type
+ let addNoUnitsMessage = (type: string) => {
+ $("#" + type + "-accordion").append("<p>There are currently no units present here. " +
+ "<em>Add some with the dropdown below!</em></p>");
+ };
+
+ let container = $("#cpu-accordion");
+ container.children().remove(".panel");
+ container.children().remove("p");
+
+ if (this.currentMachine.cpus.length === 0) {
+ addNoUnitsMessage("cpu");
+ } else {
+ this.currentMachine.cpus.forEach((element: ICPU, i: number) => {
+ let specSection = generateProcessingUnitHtml(element);
+ let content = generatePanel("cpu", i, this.currentMachine.cpus, specSection);
+ container.append(content);
+ });
+ }
+
+ container = $("#gpu-accordion");
+ container.children().remove(".panel");
+ container.children().remove("p");
+ if (this.currentMachine.gpus.length === 0) {
+ addNoUnitsMessage("gpu");
+ } else {
+ this.currentMachine.gpus.forEach((element: IGPU, i: number) => {
+ let specSection = generateProcessingUnitHtml(element);
+ let content = generatePanel("gpu", i, this.currentMachine.gpus, specSection);
+ container.append(content);
+ });
+ }
+
+ container = $("#memory-accordion");
+ container.children().remove(".panel");
+ container.children().remove("p");
+ if (this.currentMachine.memories.length === 0) {
+ addNoUnitsMessage("memory");
+ } else {
+ this.currentMachine.memories.forEach((element: IMemory, i: number) => {
+ let specSection = generateStorageUnitHtml(element);
+ let content = generatePanel("memory", i, this.currentMachine.memories, specSection);
+ container.append(content);
+ });
+ }
+
+ container = $("#storage-accordion");
+ container.children().remove(".panel");
+ container.children().remove("p");
+ if (this.currentMachine.storages.length === 0) {
+ addNoUnitsMessage("storage");
+ } else {
+ this.currentMachine.storages.forEach((element: IMemory, i: number) => {
+ let specSection = generateStorageUnitHtml(element);
+ let content = generatePanel("storage", i, this.currentMachine.storages, specSection);
+ container.append(content);
+ });
+ }
+ }
+} \ No newline at end of file
diff --git a/src/scripts/controllers/modes/object.ts b/src/scripts/controllers/modes/object.ts
new file mode 100644
index 00000000..e922433e
--- /dev/null
+++ b/src/scripts/controllers/modes/object.ts
@@ -0,0 +1,297 @@
+import {AppMode, MapController, InteractionLevel} from "../mapcontroller";
+import {MapView} from "../../views/mapview";
+import * as $ from "jquery";
+
+
+/**
+ * Class responsible for rendering object mode and handling its UI interactions.
+ */
+export class ObjectModeController {
+ public currentObject: IDCObject;
+ public objectType: string;
+ public currentRack: IRack;
+ public currentPSU: IPSU;
+ public currentCoolingItem: ICoolingItem;
+ public currentObjectTile: ITile;
+
+ private mapController: MapController;
+ private mapView: MapView;
+
+
+ constructor(mapController: MapController) {
+ this.mapController = mapController;
+ this.mapView = this.mapController.mapView;
+ }
+
+ /**
+ * Performs the necessary setup actions and enters object mode.
+ *
+ * @param tile A reference to the tile containing the rack that was selected.
+ */
+ public enterMode(tile: ITile) {
+ this.currentObjectTile = tile;
+ this.mapView.grayLayer.currentObjectTile = tile;
+ this.currentObject = tile.object;
+ this.objectType = tile.objectType;
+
+ // Show the corresponding sub-menu of object mode
+ $(".object-sub-menu").hide();
+
+ switch (this.objectType) {
+ case "RACK":
+ $("#rack-sub-menu").show();
+ this.currentRack = <IRack>this.currentObject;
+ $("#rack-name-input").val(this.currentRack.name);
+ this.populateNodeList();
+
+ break;
+
+ case "PSU":
+ $("#psu-sub-menu").show();
+ this.currentPSU = <IPSU>this.currentObject;
+
+ break;
+
+ case "COOLING_ITEM":
+ $("#cooling-item-sub-menu").show();
+ this.currentCoolingItem = <ICoolingItem>this.currentObject;
+
+ break;
+ }
+
+ this.mapView.grayLayer.drawRackLevel();
+ MapController.hideAndShowMenus("#object-menu");
+ this.scrollToBottom();
+
+ if (this.mapController.appMode === AppMode.SIMULATION) {
+ this.mapController.simulationController.transitionFromRoomToRack();
+ }
+ }
+
+ /**
+ * Leaves object mode and transfers to room mode.
+ */
+ public goToRoomMode() {
+ this.mapController.interactionLevel = InteractionLevel.ROOM;
+ this.mapView.grayLayer.hideRackLevel();
+ MapController.hideAndShowMenus("#room-menu");
+ this.mapController.roomModeController.enterMode(this.mapController.roomModeController.currentRoom);
+
+ if (this.mapController.appMode === AppMode.SIMULATION) {
+ this.mapController.simulationController.transitionFromRackToRoom();
+ }
+ }
+
+ /**
+ * Connects all DOM event listeners to their respective element targets.
+ */
+ public setupEventListeners() {
+ // Handler for saving a new rack name
+ $("#rack-name-save").on("click", () => {
+ this.currentRack.name = $("#rack-name-input").val();
+ this.mapController.api.updateRack(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id,
+ this.mapController.objectModeController.currentObjectTile.id, this.currentRack).then(
+ () => {
+ this.mapController.showInfoBalloon("Rack name saved", "info");
+ });
+ });
+
+ let nodeListContainer = $(".node-list-container");
+
+ // Handler for the 'add' button of each machine slot of the rack
+ nodeListContainer.on("click", ".add-node", (event: JQueryEventObject) => {
+ // Convert the DOM element index to a JS array index
+ let index = this.currentRack.machines.length - $(event.target).closest(".node-element").index() - 1;
+
+ // Insert an empty machine at the selected position
+ this.mapController.api.addMachineToRack(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id,
+ this.mapController.objectModeController.currentObjectTile.id, {
+ id: -1,
+ rackId: this.currentRack.id,
+ position: index,
+ tags: [],
+ cpuIds: [],
+ gpuIds: [],
+ memoryIds: [],
+ storageIds: []
+ }).then((data: IMachine) => {
+ this.currentRack.machines[index] = data;
+ this.populateNodeList();
+ this.mapView.dcObjectLayer.draw();
+ });
+
+ event.stopPropagation();
+ });
+
+ // Handler for the 'remove' button of each machine slot of the rack
+ nodeListContainer.on("click", ".remove-node", (event: JQueryEventObject) => {
+ let target = $(event.target);
+ MapController.showConfirmDeleteDialog("machine", () => {
+ // Convert the DOM element index to a JS array index
+ let index = this.currentRack.machines.length - target.closest(".node-element").index() - 1;
+
+ this.mapController.api.deleteMachine(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id,
+ this.mapController.objectModeController.currentObjectTile.id,
+ index).then(() => {
+ this.currentRack.machines[index] = null;
+ this.populateNodeList();
+ this.mapView.dcObjectLayer.draw();
+ });
+ });
+ event.stopPropagation();
+ });
+
+ // Handler for every node element, triggering node mode
+ nodeListContainer.on("click", ".node-element", (event: JQueryEventObject) => {
+ let domIndex = $(event.target).closest(".node-element").index();
+ let index = this.currentRack.machines.length - domIndex - 1;
+ let machine = this.currentRack.machines[index];
+
+ if (machine != null) {
+ this.mapController.interactionLevel = InteractionLevel.NODE;
+
+ // Gray out the other nodes
+ $(event.target).closest(".node-list-container").children(".node-element").each((nodeIndex: number, element: Element) => {
+ if (nodeIndex !== domIndex) {
+ $(element).children(".node-element-overlay").removeClass("hidden");
+ } else {
+ $(element).children(".node-element-overlay").addClass("hidden");
+ }
+ });
+
+ this.mapController.nodeModeController.enterMode(machine);
+ }
+ });
+
+ // Handler for rack deletion button
+ $("#rack-deletion").on("click", () => {
+ MapController.showConfirmDeleteDialog("rack", () => {
+ this.mapController.api.deleteRack(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id,
+ this.mapController.objectModeController.currentObjectTile.id).then(() => {
+ this.currentObjectTile.object = undefined;
+ this.currentObjectTile.objectType = undefined;
+ this.currentObjectTile.objectId = undefined;
+ this.mapView.redrawMap();
+ this.goToRoomMode();
+ });
+ });
+ });
+
+ // Handler for PSU deletion button
+ $("#psu-deletion").on("click", () => {
+ MapController.showConfirmDeleteDialog("PSU", () => {
+ this.mapController.api.deletePSU(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id,
+ this.mapController.objectModeController.currentObjectTile.id).then(() => {
+ this.mapView.redrawMap();
+ this.goToRoomMode();
+ });
+ this.currentObjectTile.object = undefined;
+ this.currentObjectTile.objectType = undefined;
+ this.currentObjectTile.objectId = undefined;
+ });
+ });
+
+ // Handler for Cooling Item deletion button
+ $("#cooling-item-deletion").on("click", () => {
+ MapController.showConfirmDeleteDialog("cooling item", () => {
+ this.mapController.api.deleteCoolingItem(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.mapController.roomModeController.currentRoom.id,
+ this.mapController.objectModeController.currentObjectTile.id).then(() => {
+ this.mapView.redrawMap();
+ this.goToRoomMode();
+ });
+ this.currentObjectTile.object = undefined;
+ this.currentObjectTile.objectType = undefined;
+ this.currentObjectTile.objectId = undefined;
+ });
+ });
+ }
+
+ public updateNodeComponentOverlays(): void {
+ if (this.currentRack === undefined || this.currentRack.machines === undefined) {
+ return;
+ }
+
+ for (let i = 0; i < this.currentRack.machines.length; i++) {
+ if (this.currentRack.machines[i] === null) {
+ continue;
+ }
+
+ let container = this.mapController.appMode === AppMode.CONSTRUCTION ? ".construction" : ".simulation";
+ let element = $(container + " .node-element").eq(this.currentRack.machines.length - i - 1);
+ if (this.currentRack.machines[i].cpus.length !== 0) {
+ element.find(".overlay-cpu").addClass("hidden");
+ } else {
+ element.find(".overlay-cpu").removeClass("hidden");
+ }
+ if (this.currentRack.machines[i].gpus.length !== 0) {
+ element.find(".overlay-gpu").addClass("hidden");
+ } else {
+ element.find(".overlay-gpu").removeClass("hidden");
+ }
+ if (this.currentRack.machines[i].memories.length !== 0) {
+ element.find(".overlay-memory").addClass("hidden");
+ } else {
+ element.find(".overlay-memory").removeClass("hidden");
+ }
+ if (this.currentRack.machines[i].storages.length !== 0) {
+ element.find(".overlay-storage").addClass("hidden");
+ } else {
+ element.find(".overlay-storage").removeClass("hidden");
+ }
+ }
+ }
+
+ /**
+ * Dynamically generates and inserts HTML code for every node in the current rack.
+ */
+ private populateNodeList(): void {
+ let type, content;
+ let container = $(".node-list-container");
+
+ // Remove any previously present node elements
+ container.children().remove(".node-element");
+
+ for (let i = 0; i < this.currentRack.machines.length; i++) {
+ // Depending on whether the current machine slot is filled, allow removing or adding a new machine by adding
+ // the appropriate button next to the machine slot
+ type = (this.currentRack.machines[i] == null ? "glyphicon-plus add-node" : "glyphicon-remove remove-node");
+ content =
+ '<div class="node-element" data-id="' + (this.currentRack.machines[i] === null ?
+ "" : this.currentRack.machines[i].id) + '">' +
+ ' <div class="node-element-overlay hidden"></div>' +
+ ' <a class="node-element-btn glyphicon ' + type + '" href="javascript:void(0)"></a>' +
+ ' <div class="node-element-number">' + (i + 1) + '</div>';
+ if (this.currentRack.machines[i] !== null) {
+ content +=
+ '<div class="node-element-content">' +
+ ' <img src="img/app/node-cpu.png">' +
+ ' <img src="img/app/node-gpu.png">' +
+ ' <img src="img/app/node-memory.png">' +
+ ' <img src="img/app/node-storage.png">' +
+ ' <img src="img/app/node-network.png">' +
+ ' <div class="icon-overlay overlay-cpu hidden"></div>' +
+ ' <div class="icon-overlay overlay-gpu hidden"></div>' +
+ ' <div class="icon-overlay overlay-memory hidden"></div>' +
+ ' <div class="icon-overlay overlay-storage hidden"></div>' +
+ ' <div class="icon-overlay overlay-network"></div>' +
+ '</div>';
+ }
+ content += '</div>';
+ // Insert the generated machine slot into the DOM
+ container.prepend(content);
+ }
+
+ this.updateNodeComponentOverlays();
+ }
+
+ private scrollToBottom(): void {
+ let scrollContainer = $('.node-list-container');
+ scrollContainer.scrollTop(scrollContainer[0].scrollHeight);
+ }
+} \ No newline at end of file
diff --git a/src/scripts/controllers/modes/room.ts b/src/scripts/controllers/modes/room.ts
new file mode 100644
index 00000000..a858af5a
--- /dev/null
+++ b/src/scripts/controllers/modes/room.ts
@@ -0,0 +1,382 @@
+import {Util} from "../../util";
+import {InteractionLevel, MapController, AppMode} from "../mapcontroller";
+import {MapView} from "../../views/mapview";
+import * as $ from "jquery";
+
+
+export enum RoomInteractionMode {
+ DEFAULT,
+ ADD_RACK,
+ ADD_PSU,
+ ADD_COOLING_ITEM
+}
+
+
+export class RoomModeController {
+ public currentRoom: IRoom;
+ public roomInteractionMode: RoomInteractionMode;
+
+ private mapController: MapController;
+ private mapView: MapView;
+ private roomTypes: string[];
+ private roomTypeMap: IRoomTypeMap;
+ private availablePSUs: IPSU[];
+ private availableCoolingItems: ICoolingItem[];
+
+
+ constructor(mapController: MapController) {
+ this.mapController = mapController;
+ this.mapView = this.mapController.mapView;
+
+ this.mapController.api.getAllRoomTypes().then((roomTypes: string[]) => {
+ this.roomTypes = roomTypes;
+ this.roomTypeMap = {};
+
+ this.roomTypes.forEach((type: string) => {
+ this.mapController.api.getAllowedObjectsByRoomType(type).then((objects: string[]) => {
+ this.roomTypeMap[type] = objects;
+ });
+ });
+
+ this.populateRoomTypeDropdown();
+ });
+
+ // this.mapController.api.getAllPSUSpecs().then((specs: IPSU[]) => {
+ // this.availablePSUs = specs;
+ // });
+ //
+ // this.mapController.api.getAllCoolingItemSpecs().then((specs: ICoolingItem[]) => {
+ // this.availableCoolingItems = specs;
+ // });
+
+ this.roomInteractionMode = RoomInteractionMode.DEFAULT;
+ }
+
+ public enterMode(room: IRoom) {
+ this.currentRoom = room;
+ this.roomInteractionMode = RoomInteractionMode.DEFAULT;
+
+ this.mapView.roomTextLayer.setVisibility(false);
+
+ this.mapView.zoomInOnRoom(this.currentRoom);
+ $("#room-name-input").val(this.currentRoom.name);
+ MapController.hideAndShowMenus("#room-menu");
+
+ // Pre-select the type of the current room in the dropdown
+ let roomTypeDropdown = $("#roomtype-select");
+ roomTypeDropdown.find('option').prop("selected", "false");
+ let roomTypeIndex = this.roomTypes.indexOf(this.currentRoom.roomType);
+ if (roomTypeIndex !== -1) {
+ roomTypeDropdown.find('option[value="' + roomTypeIndex + '"]').prop("selected", "true");
+ } else {
+ roomTypeDropdown.val([]);
+ }
+
+ this.populateAllowedObjectTypes();
+
+ this.mapView.roomLayer.setClickable(false);
+
+ if (this.mapController.appMode === AppMode.SIMULATION) {
+ this.mapController.simulationController.transitionFromBuildingToRoom();
+ }
+ }
+
+ public goToBuildingMode() {
+ this.mapController.interactionLevel = InteractionLevel.BUILDING;
+
+ if (this.roomInteractionMode !== RoomInteractionMode.DEFAULT) {
+ this.roomInteractionMode = RoomInteractionMode.DEFAULT;
+ this.mapView.hoverLayer.setHoverItemVisibility(false);
+ $("#add-rack-btn").attr("data-active", "false");
+ $("#add-psu-btn").attr("data-active", "false");
+ $("#add-cooling-item-btn").attr("data-active", "false");
+ }
+
+ this.mapView.roomTextLayer.setVisibility(true);
+
+ this.mapView.zoomOutOnDC();
+ MapController.hideAndShowMenus("#building-menu");
+
+ this.mapView.roomLayer.setClickable(true);
+
+ if (this.mapController.appMode === AppMode.SIMULATION) {
+ this.mapController.simulationController.transitionFromRoomToBuilding();
+ }
+ }
+
+ public setupEventListeners(): void {
+ // Component buttons
+ let addRackBtn = $("#add-rack-btn");
+ let addPSUBtn = $("#add-psu-btn");
+ let addCoolingItemBtn = $("#add-cooling-item-btn");
+
+ let roomTypeDropdown = $("#roomtype-select");
+
+ addRackBtn.on("click", () => {
+ this.handleItemClick("RACK");
+ });
+ addPSUBtn.on("click", () => {
+ this.handleItemClick("PSU");
+ });
+ addCoolingItemBtn.on("click", () => {
+ this.handleItemClick("COOLING_ITEM");
+ });
+
+ // Handler for saving a new room name
+ $("#room-name-save").on("click", () => {
+ this.currentRoom.name = $("#room-name-input").val();
+ this.mapController.api.updateRoom(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.currentRoom).then(() => {
+ this.mapView.roomTextLayer.draw();
+ this.mapController.showInfoBalloon("Room name saved", "info");
+ });
+ });
+
+ // Handler for room deletion button
+ $("#room-deletion").on("click", () => {
+ MapController.showConfirmDeleteDialog("room", () => {
+ this.mapController.api.deleteRoom(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.currentRoom.id).then(() => {
+ let roomIndex = this.mapView.currentDatacenter.rooms.indexOf(this.currentRoom);
+ this.mapView.currentDatacenter.rooms.splice(roomIndex, 1);
+
+ this.mapView.redrawMap();
+ this.goToBuildingMode();
+ });
+ });
+ });
+
+ // Handler for the room type dropdown component
+ roomTypeDropdown.on("change", () => {
+ let newRoomType = this.roomTypes[roomTypeDropdown.val()];
+ if (!this.checkRoomTypeLegality(newRoomType)) {
+ roomTypeDropdown.val(this.roomTypes.indexOf(this.currentRoom.roomType));
+ this.mapController.showInfoBalloon("Room type couldn't be changed, illegal objects", "warning");
+ return;
+ }
+
+ this.currentRoom.roomType = newRoomType;
+ this.mapController.api.updateRoom(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.currentRoom).then(() => {
+ this.populateAllowedObjectTypes();
+ this.mapView.roomTextLayer.draw();
+ this.mapController.showInfoBalloon("Room type changed", "info");
+ });
+ });
+ }
+
+ public handleCanvasMouseClick(gridPos: IGridPosition): void {
+ if (this.roomInteractionMode === RoomInteractionMode.DEFAULT) {
+ let tileIndex = Util.tileListPositionIndexOf(this.currentRoom.tiles, gridPos);
+
+ if (tileIndex !== -1) {
+ let tile = this.currentRoom.tiles[tileIndex];
+
+ if (tile.object !== undefined) {
+ this.mapController.interactionLevel = InteractionLevel.OBJECT;
+ this.mapController.objectModeController.enterMode(tile);
+ }
+ } else {
+ this.goToBuildingMode();
+ }
+ } else if (this.roomInteractionMode === RoomInteractionMode.ADD_RACK) {
+ this.addObject(this.mapView.hoverLayer.hoverTilePosition, "RACK");
+
+ } else if (this.roomInteractionMode === RoomInteractionMode.ADD_PSU) {
+ this.addObject(this.mapView.hoverLayer.hoverTilePosition, "PSU");
+
+ } else if (this.roomInteractionMode === RoomInteractionMode.ADD_COOLING_ITEM) {
+ this.addObject(this.mapView.hoverLayer.hoverTilePosition, "COOLING_ITEM");
+
+ }
+ }
+
+ private handleItemClick(type: string): void {
+ let addRackBtn = $("#add-rack-btn");
+ let addPSUBtn = $("#add-psu-btn");
+ let addCoolingItemBtn = $("#add-cooling-item-btn");
+ let allObjectContainers = $(".dc-component-container");
+ let objectTypes = [
+ {
+ type: "RACK",
+ mode: RoomInteractionMode.ADD_RACK,
+ btn: addRackBtn
+ },
+ {
+ type: "PSU",
+ mode: RoomInteractionMode.ADD_PSU,
+ btn: addPSUBtn
+ },
+ {
+ type: "COOLING_ITEM",
+ mode: RoomInteractionMode.ADD_COOLING_ITEM,
+ btn: addCoolingItemBtn
+ }
+ ];
+
+ allObjectContainers.attr("data-active", "false");
+
+ if (this.roomInteractionMode === RoomInteractionMode.DEFAULT) {
+ this.mapView.hoverLayer.setHoverItemVisibility(true, type);
+
+ if (type === "RACK") {
+ this.roomInteractionMode = RoomInteractionMode.ADD_RACK;
+ addRackBtn.attr("data-active", "true");
+ } else if (type === "PSU") {
+ this.roomInteractionMode = RoomInteractionMode.ADD_PSU;
+ addPSUBtn.attr("data-active", "true");
+ } else if (type === "COOLING_ITEM") {
+ this.roomInteractionMode = RoomInteractionMode.ADD_COOLING_ITEM;
+ addCoolingItemBtn.attr("data-active", "true");
+ }
+
+ return;
+ }
+
+ let changed = false;
+ objectTypes.forEach((objectType: any, index: number) => {
+ if (this.roomInteractionMode === objectType.mode) {
+ if (changed) {
+ return;
+ }
+ if (type === objectType.type) {
+ this.roomInteractionMode = RoomInteractionMode.DEFAULT;
+ this.mapView.hoverLayer.setHoverItemVisibility(false);
+ objectType.btn.attr("data-active", "false");
+ } else {
+ objectTypes.forEach((otherObjectType, otherIndex: number) => {
+ if (index !== otherIndex) {
+ if (type === otherObjectType.type) {
+ this.mapView.hoverLayer.setHoverItemVisibility(true, type);
+ otherObjectType.btn.attr("data-active", "true");
+ this.roomInteractionMode = otherObjectType.mode;
+ }
+ }
+ });
+ }
+ changed = true;
+ }
+ });
+ }
+
+ private addObject(position: IGridPosition, type: string): void {
+ if (!this.mapView.roomLayer.checkHoverTileValidity(position)) {
+ return;
+ }
+
+ let tileList = this.mapView.mapController.roomModeController.currentRoom.tiles;
+
+ for (let i = 0; i < tileList.length; i++) {
+ if (tileList[i].position.x === position.x && tileList[i].position.y === position.y) {
+ if (type === "RACK") {
+ this.mapController.api.addRack(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.currentRoom.id, tileList[i].id, {
+ id: -1,
+ objectType: "RACK",
+ name: "",
+ capacity: 42,
+ powerCapacityW: 5000
+ }).then((rack: IRack) => {
+ tileList[i].object = rack;
+ tileList[i].objectId = rack.id;
+ tileList[i].objectType = type;
+ this.mapView.dcObjectLayer.populateObjectList();
+ this.mapView.dcObjectLayer.draw();
+
+ this.mapView.updateScene = true;
+ });
+ } else if (type === "PSU") {
+ this.mapController.api.addPSU(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.currentRoom.id, tileList[i].id, this.availablePSUs[0])
+ .then((psu: IPSU) => {
+ tileList[i].object = psu;
+ tileList[i].objectId = psu.id;
+ tileList[i].objectType = type;
+ this.mapView.dcObjectLayer.populateObjectList();
+ this.mapView.dcObjectLayer.draw();
+
+ this.mapView.updateScene = true;
+ });
+ } else if (type === "COOLING_ITEM") {
+ this.mapController.api.addCoolingItem(this.mapView.simulation.id,
+ this.mapView.currentDatacenter.id, this.currentRoom.id, tileList[i].id,
+ this.availableCoolingItems[0]).then((coolingItem: ICoolingItem) => {
+ tileList[i].object = coolingItem;
+ tileList[i].objectId = coolingItem.id;
+ tileList[i].objectType = type;
+ this.mapView.dcObjectLayer.populateObjectList();
+ this.mapView.dcObjectLayer.draw();
+
+ this.mapView.updateScene = true;
+ });
+ }
+
+ break;
+ }
+ }
+ }
+
+ /**
+ * Populates the room-type dropdown element with all available room types
+ */
+ private populateRoomTypeDropdown(): void {
+ let dropdown = $("#roomtype-select");
+
+ this.roomTypes.forEach((type: string, index: number) => {
+ dropdown.append($('<option>').text(Util.toSentenceCase(type)).val(index));
+ });
+ }
+
+ /**
+ * Loads all object types that are allowed in the current room into the menu.
+ */
+ private populateAllowedObjectTypes(): void {
+ let addObjectsLabel = $("#add-objects-label");
+ let noObjectsInfo = $("#no-objects-info");
+ let allowedObjectTypes = this.roomTypeMap[this.currentRoom.roomType];
+
+ $(".dc-component-container").addClass("hidden");
+
+ if (allowedObjectTypes === undefined || allowedObjectTypes === null || allowedObjectTypes.length === 0) {
+ addObjectsLabel.addClass("hidden");
+ noObjectsInfo.removeClass("hidden");
+
+ return;
+ }
+
+ addObjectsLabel.removeClass("hidden");
+ noObjectsInfo.addClass("hidden");
+ allowedObjectTypes.forEach((type: string) => {
+ switch (type) {
+ case "RACK":
+ $("#add-rack-btn").removeClass("hidden");
+ break;
+ case "PSU":
+ $("#add-psu-btn").removeClass("hidden");
+ break;
+ case "COOLING_ITEM":
+ $("#add-cooling-item-btn").removeClass("hidden");
+ break;
+ }
+ });
+ }
+
+ /**
+ * Checks whether a given room type can be assigned to the current room based on units already present.
+ *
+ * @param newRoomType The new room type to be validated
+ * @returns {boolean} Whether it is allowed to change the room's type to the new type
+ */
+ private checkRoomTypeLegality(newRoomType: string): boolean {
+ let legality = true;
+
+ this.currentRoom.tiles.forEach((tile: ITile) => {
+ if (tile.objectType !== undefined && tile.objectType !== null && tile.objectType !== "" &&
+ this.roomTypeMap[newRoomType].indexOf(tile.objectType) === -1) {
+ legality = false;
+ }
+ });
+
+ return legality;
+ }
+} \ No newline at end of file
diff --git a/src/scripts/controllers/scaleindicator.ts b/src/scripts/controllers/scaleindicator.ts
new file mode 100644
index 00000000..0ff83486
--- /dev/null
+++ b/src/scripts/controllers/scaleindicator.ts
@@ -0,0 +1,45 @@
+import {MapController, CELL_SIZE} from "./mapcontroller";
+import {MapView} from "../views/mapview";
+
+
+export class ScaleIndicatorController {
+ private static MIN_WIDTH = 50;
+ private static MAX_WIDTH = 100;
+
+ private mapController: MapController;
+ private mapView: MapView;
+
+ private jqueryObject: JQuery;
+ private currentDivisor: number;
+
+
+ constructor(mapController: MapController) {
+ this.mapController = mapController;
+ this.mapView = mapController.mapView;
+ }
+
+ public init(jqueryObject: JQuery): void {
+ this.jqueryObject = jqueryObject;
+ this.currentDivisor = 1;
+ }
+
+ public update(): void {
+ let currentZoom = this.mapView.mapContainer.scaleX;
+ let newWidth;
+ do {
+ newWidth = (currentZoom * CELL_SIZE) / this.currentDivisor;
+
+ if (newWidth < ScaleIndicatorController.MIN_WIDTH) {
+ this.currentDivisor /= 2;
+ } else if (newWidth > ScaleIndicatorController.MAX_WIDTH) {
+ this.currentDivisor *= 2;
+ } else {
+ break;
+ }
+ } while (true);
+
+
+ this.jqueryObject.text(MapView.CELL_SIZE_METERS / this.currentDivisor + "m");
+ this.jqueryObject.width(newWidth);
+ }
+}
diff --git a/src/scripts/controllers/simulation/chart.ts b/src/scripts/controllers/simulation/chart.ts
new file mode 100644
index 00000000..84009622
--- /dev/null
+++ b/src/scripts/controllers/simulation/chart.ts
@@ -0,0 +1,241 @@
+import * as c3 from "c3";
+import {InteractionLevel, MapController} from "../mapcontroller";
+import {ColorRepresentation, SimulationController} from "../simulationcontroller";
+import {Util} from "../../util";
+
+
+export interface IStateColumn {
+ loadFractions: string[] | number[];
+ inUseMemoryMb: string[] | number[];
+ temperatureC: string[] | number[];
+}
+
+
+export class ChartController {
+ public roomSeries: { [key: number]: IStateColumn };
+ public rackSeries: { [key: number]: IStateColumn };
+ public machineSeries: { [key: number]: IStateColumn };
+ public chart: c3.ChartAPI;
+ public machineChart: c3.ChartAPI;
+
+ private simulationController: SimulationController;
+ private mapController: MapController;
+ private chartData: (string | number)[][];
+ private xSeries: (string | number)[];
+ private names: { [key: string]: string };
+
+
+ constructor(simulationController: SimulationController) {
+ this.simulationController = simulationController;
+ this.mapController = simulationController.mapController;
+ }
+
+ public setup(): void {
+ this.names = {};
+
+ this.roomSeries = {};
+ this.rackSeries = {};
+ this.machineSeries = {};
+
+ this.simulationController.sections.forEach((simulationSection: ISection) => {
+ simulationSection.datacenter.rooms.forEach((room: IRoom) => {
+ if (room.roomType === "SERVER" && this.roomSeries[room.id] === undefined) {
+ this.names["ro" + room.id] = (room.name === "" || room.name === undefined) ?
+ "Unnamed room" : room.name;
+
+ this.roomSeries[room.id] = {
+ loadFractions: ["ro" + room.id],
+ inUseMemoryMb: ["ro" + room.id],
+ temperatureC: ["ro" + room.id]
+ };
+ }
+
+ room.tiles.forEach((tile: ITile) => {
+ if (tile.object !== undefined && tile.objectType === "RACK" && this.rackSeries[tile.objectId] === undefined) {
+ let objectName = (<IRack>tile.object).name;
+ this.names["ra" + tile.objectId] = objectName === "" || objectName === undefined ?
+ "Unnamed rack" : objectName;
+
+ this.rackSeries[tile.objectId] = {
+ loadFractions: ["ra" + tile.objectId],
+ inUseMemoryMb: ["ra" + tile.objectId],
+ temperatureC: ["ra" + tile.objectId]
+ };
+
+ (<IRack>tile.object).machines.forEach((machine: IMachine) => {
+ if (machine === null || this.machineSeries[machine.id] !== undefined) {
+ return;
+ }
+
+ this.names["ma" + machine.id] = "Machine at position " + (machine.position + 1).toString();
+
+ this.machineSeries[machine.id] = {
+ loadFractions: ["ma" + machine.id],
+ inUseMemoryMb: ["ma" + machine.id],
+ temperatureC: ["ma" + machine.id]
+ };
+ });
+ }
+ });
+ });
+ });
+
+
+ this.xSeries = ["time"];
+ this.chartData = [this.xSeries];
+
+ this.chart = this.chartSetup("#statistics-chart");
+ this.machineChart = this.chartSetup("#machine-statistics-chart");
+ }
+
+ public chartSetup(chartId: string): c3.ChartAPI {
+ return c3.generate({
+ bindto: chartId,
+ data: {
+ xFormat: '%S',
+ x: "time",
+ columns: this.chartData,
+ names: this.names
+ },
+ axis: {
+ x: {
+ type: "timeseries",
+ tick: {
+ format: function (time: Date) {
+ let formattedTime = time.getSeconds() + "s";
+
+ if (time.getMinutes() > 0) {
+ formattedTime = time.getMinutes() + "m" + formattedTime;
+ }
+ if (time.getHours() > 0) {
+ formattedTime = time.getHours() + "h" + formattedTime;
+ }
+
+ return formattedTime;
+ },
+ culling: {
+ max: 5
+ },
+ count: 8
+ },
+ padding: {
+ left: 0,
+ right: 10
+ }
+ },
+ y: {
+ min: 0,
+ max: 1,
+ padding: {
+ top: 0,
+ bottom: 0
+ },
+ tick: {
+ format: function (d) {
+ return (Math.round(d * 100) / 100).toString();
+ }
+ }
+ }
+ }
+ });
+ }
+
+ public update(): void {
+ this.xSeries = (<(number|string)[]>["time"]).concat(Util.range(this.simulationController.currentTick));
+
+ this.chartData = [this.xSeries];
+
+ let prefix = "";
+ let machineId = -1;
+ if (this.mapController.interactionLevel === InteractionLevel.BUILDING) {
+ for (let roomId in this.roomSeries) {
+ if (this.roomSeries.hasOwnProperty(roomId)) {
+ if (this.simulationController.colorRepresentation === ColorRepresentation.LOAD) {
+ this.chartData.push(this.roomSeries[roomId].loadFractions);
+ }
+ }
+ }
+ prefix = "ro";
+ } else if (this.mapController.interactionLevel === InteractionLevel.ROOM) {
+ for (let rackId in this.rackSeries) {
+ if (this.rackSeries.hasOwnProperty(rackId) &&
+ this.simulationController.rackToRoomMap[rackId] ===
+ this.mapController.roomModeController.currentRoom.id) {
+ if (this.simulationController.colorRepresentation === ColorRepresentation.LOAD) {
+ this.chartData.push(this.rackSeries[rackId].loadFractions);
+ }
+ }
+ }
+ prefix = "ra";
+ } else if (this.mapController.interactionLevel === InteractionLevel.NODE) {
+ if (this.simulationController.colorRepresentation === ColorRepresentation.LOAD) {
+ this.chartData.push(
+ this.machineSeries[this.mapController.nodeModeController.currentMachine.id].loadFractions
+ );
+ }
+ prefix = "ma";
+ machineId = this.mapController.nodeModeController.currentMachine.id;
+ }
+
+ let unloads: string[] = [];
+ for (let id in this.names) {
+ if (this.names.hasOwnProperty(id)) {
+ if (machineId === -1) {
+ if (id.substr(0, 2) !== prefix ||
+ (this.mapController.interactionLevel === InteractionLevel.ROOM &&
+ this.simulationController.rackToRoomMap[parseInt(id.substr(2))] !==
+ this.mapController.roomModeController.currentRoom.id)) {
+ unloads.push(id);
+ }
+ }
+ else {
+ if (id !== prefix + machineId) {
+ unloads.push(id);
+ }
+ }
+ }
+ }
+
+ let targetChart: c3.ChartAPI;
+ if (this.mapController.interactionLevel === InteractionLevel.NODE) {
+ targetChart = this.machineChart;
+ } else {
+ targetChart = this.chart;
+ }
+
+ targetChart.load({
+ columns: this.chartData,
+ unload: unloads
+ });
+
+ }
+
+ public tickUpdated(tick: number): void {
+ let roomStates: IRoomState[] = this.simulationController.stateCache.stateList[tick].roomStates;
+ roomStates.forEach((roomState: IRoomState) => {
+ ChartController.insertAtIndex(this.roomSeries[roomState.roomId].loadFractions, tick + 1, roomState.loadFraction);
+ });
+
+ let rackStates: IRackState[] = this.simulationController.stateCache.stateList[tick].rackStates;
+ rackStates.forEach((rackState: IRackState) => {
+ ChartController.insertAtIndex(this.rackSeries[rackState.rackId].loadFractions, tick + 1, rackState.loadFraction);
+ });
+
+ let machineStates: IMachineState[] = this.simulationController.stateCache.stateList[tick].machineStates;
+ machineStates.forEach((machineState: IMachineState) => {
+ ChartController.insertAtIndex(this.machineSeries[machineState.machineId].loadFractions, tick + 1, machineState.loadFraction);
+ });
+ }
+
+ private static insertAtIndex(list: any[], index: number, data: any): void {
+ if (index > list.length) {
+ let i = list.length;
+ while (i < index) {
+ list[i] = null;
+ i++;
+ }
+ }
+
+ list[index] = data;
+ }
+} \ No newline at end of file
diff --git a/src/scripts/controllers/simulation/statecache.ts b/src/scripts/controllers/simulation/statecache.ts
new file mode 100644
index 00000000..32f8f4e4
--- /dev/null
+++ b/src/scripts/controllers/simulation/statecache.ts
@@ -0,0 +1,205 @@
+import {SimulationController} from "../simulationcontroller";
+
+
+export class StateCache {
+ public static CACHE_INTERVAL = 3000;
+ private static PREFERRED_CACHE_ADVANCE = 5;
+
+ public stateList: {[key: number]: ITickState};
+ public lastCachedTick: number;
+ public cacheBlock: boolean;
+
+ private simulationController: SimulationController;
+ private intervalId: number;
+ private caching: boolean;
+
+ // Item caches
+ private machineCache: {[keys: number]: IMachine};
+ private rackCache: {[keys: number]: IRack};
+ private roomCache: {[keys: number]: IRoom};
+ private taskCache: {[keys: number]: ITask};
+
+
+ constructor(simulationController: SimulationController) {
+ this.stateList = {};
+ this.lastCachedTick = -1;
+ this.cacheBlock = true;
+ this.simulationController = simulationController;
+ this.caching = false;
+ }
+
+ public startCaching(): void {
+ this.machineCache = {};
+ this.rackCache = {};
+ this.roomCache = {};
+ this.taskCache = {};
+
+ this.simulationController.mapView.currentDatacenter.rooms.forEach((room: IRoom) => {
+ this.addRoomToCache(room);
+ });
+ this.simulationController.currentExperiment.trace.tasks.forEach((task: ITask) => {
+ this.taskCache[task.id] = task;
+ });
+
+ this.caching = true;
+
+ this.cache();
+ this.intervalId = setInterval(() => {
+ this.cache();
+ }, StateCache.CACHE_INTERVAL);
+ }
+
+ private addRoomToCache(room: IRoom) {
+ this.roomCache[room.id] = room;
+
+ room.tiles.forEach((tile: ITile) => {
+ if (tile.objectType === "RACK") {
+ this.rackCache[tile.objectId] = <IRack>tile.object;
+
+ (<IRack> tile.object).machines.forEach((machine: IMachine) => {
+ if (machine !== null) {
+ this.machineCache[machine.id] = machine;
+ }
+ });
+ }
+ });
+ }
+
+ public stopCaching(): void {
+ if (this.caching) {
+ this.caching = false;
+ clearInterval(this.intervalId);
+ }
+ }
+
+ private cache(): void {
+ let tick = this.lastCachedTick + 1;
+
+ this.updateLastTick().then(() => {
+ // Check if end of simulated region has been reached
+ if (this.lastCachedTick > this.simulationController.lastSimulatedTick) {
+ return;
+ }
+
+ this.fetchAllStatesOfTick(tick).then((data: ITickState) => {
+ this.stateList[tick] = data;
+
+ this.updateTasks(tick);
+
+ // Update chart cache
+ this.simulationController.chartController.tickUpdated(tick);
+
+ this.lastCachedTick++;
+
+ if (!this.cacheBlock && this.lastCachedTick - this.simulationController.currentTick <= 0) {
+ this.cacheBlock = true;
+ return;
+ }
+
+ if (this.cacheBlock) {
+ if (this.lastCachedTick - this.simulationController.currentTick >= StateCache.PREFERRED_CACHE_ADVANCE) {
+ this.cacheBlock = false;
+ }
+ }
+ });
+ });
+ }
+
+ private updateTasks(tick: number): void {
+ const taskIDsInTick = [];
+
+ this.stateList[tick].taskStates.forEach((taskState: ITaskState) => {
+ taskIDsInTick.push(taskState.taskId);
+ if (this.stateList[tick - 1] !== undefined) {
+ let previousFlops = 0;
+ const previousStates = this.stateList[tick - 1].taskStates;
+
+ for (let i = 0; i < previousStates.length; i++) {
+ if (previousStates[i].taskId === taskState.taskId) {
+ previousFlops = previousStates[i].flopsLeft;
+ break;
+ }
+ }
+
+ if (previousFlops > 0 && taskState.flopsLeft === 0) {
+ taskState.task.finishedTick = tick;
+ }
+ }
+ });
+
+ // Generate pseudo-task-states for tasks that haven't started yet or have already finished
+ const traceTasks = this.simulationController.currentExperiment.trace.tasks;
+ if (taskIDsInTick.length !== traceTasks.length) {
+ traceTasks
+ .filter((task: ITask) => {
+ return taskIDsInTick.indexOf(task.id) === -1;
+ })
+ .forEach((task: ITask) => {
+ const flopStateCount = task.startTick >= tick ? task.totalFlopCount : 0;
+
+ this.stateList[tick].taskStates.push({
+ id: -1,
+ taskId: task.id,
+ task: task,
+ experimentId: this.simulationController.currentExperiment.id,
+ tick,
+ flopsLeft: flopStateCount
+ });
+ });
+ }
+
+ this.stateList[tick].taskStates.sort((a: ITaskState, b: ITaskState) => {
+ return a.task.startTick - b.task.startTick;
+ });
+ }
+
+ private updateLastTick(): Promise<void> {
+ return this.simulationController.mapController.api.getLastSimulatedTickByExperiment(
+ this.simulationController.simulation.id, this.simulationController.currentExperiment.id).then((data) => {
+ this.simulationController.lastSimulatedTick = data;
+ });
+ }
+
+ private fetchAllStatesOfTick(tick: number): Promise<ITickState> {
+ let tickState: ITickState = {
+ tick,
+ machineStates: [],
+ rackStates: [],
+ roomStates: [],
+ taskStates: []
+ };
+ const promises = [];
+
+ promises.push(this.simulationController.mapController.api.getMachineStatesByTick(
+ this.simulationController.mapView.simulation.id, this.simulationController.currentExperiment.id,
+ tick, this.machineCache
+ ).then((states: IMachineState[]) => {
+ tickState.machineStates = states;
+ }));
+
+ promises.push(this.simulationController.mapController.api.getRackStatesByTick(
+ this.simulationController.mapView.simulation.id, this.simulationController.currentExperiment.id,
+ tick, this.rackCache
+ ).then((states: IRackState[]) => {
+ tickState.rackStates = states;
+ }));
+
+ promises.push(this.simulationController.mapController.api.getRoomStatesByTick(
+ this.simulationController.mapView.simulation.id, this.simulationController.currentExperiment.id,
+ tick, this.roomCache
+ ).then((states: IRoomState[]) => {
+ tickState.roomStates = states;
+ }));
+
+ promises.push(this.simulationController.mapController.api.getTaskStatesByTick(
+ this.simulationController.mapView.simulation.id, this.simulationController.currentExperiment.id,
+ tick, this.taskCache
+ ).then((states: ITaskState[]) => {
+ tickState.taskStates = states;
+ }));
+
+ return Promise.all(promises).then(() => {
+ return tickState;
+ });
+ }
+}
diff --git a/src/scripts/controllers/simulation/taskview.ts b/src/scripts/controllers/simulation/taskview.ts
new file mode 100644
index 00000000..d989e103
--- /dev/null
+++ b/src/scripts/controllers/simulation/taskview.ts
@@ -0,0 +1,64 @@
+import * as $ from "jquery";
+import {SimulationController} from "../simulationcontroller";
+import {Util} from "../../util";
+
+
+export class TaskViewController {
+ private simulationController: SimulationController;
+
+
+ constructor(simulationController: SimulationController) {
+ this.simulationController = simulationController;
+ }
+
+ /**
+ * Populates and displays the list of tasks with their current state.
+ */
+ public update() {
+ const container = $(".task-list");
+ container.children().remove(".task-element");
+
+ this.simulationController.stateCache.stateList[this.simulationController.currentTick].taskStates
+ .forEach((taskState: ITaskState) => {
+ const html = this.generateTaskElementHTML(taskState);
+ container.append(html);
+ });
+ }
+
+ private generateTaskElementHTML(taskState: ITaskState) {
+ let iconType, timeInfo;
+
+ if (taskState.task.startTick > this.simulationController.currentTick) {
+ iconType = "glyphicon-time";
+ timeInfo = "Not started yet";
+ } else if (taskState.task.startTick <= this.simulationController.currentTick && taskState.flopsLeft > 0) {
+ iconType = "glyphicon-refresh";
+ timeInfo = "Started at " + Util.convertSecondsToFormattedTime(taskState.task.startTick);
+ } else if (taskState.flopsLeft === 0) {
+ iconType = "glyphicon-ok";
+ timeInfo = "Started at " + Util.convertSecondsToFormattedTime(taskState.task.startTick);
+ }
+
+ // Calculate progression ratio
+ const progress = 1 - (taskState.flopsLeft / taskState.task.totalFlopCount);
+
+ // Generate completion text
+ const flopsCompleted = taskState.task.totalFlopCount - taskState.flopsLeft;
+ const completionInfo = "Completed: " + flopsCompleted + " / " + taskState.task.totalFlopCount + " FLOPS";
+
+ return '<div class="task-element">' +
+ ' <div class="task-icon glyphicon ' + iconType + '"></div>' +
+ ' <div class="task-info">' +
+ ' <div class="task-time">' + timeInfo +
+ ' </div>' +
+ ' <div class="progress">' +
+ ' <div class="progress-bar progress-bar-striped" role="progressbar" aria-valuenow="' +
+ progress * 100 + '%"' +
+ ' aria-valuemin="0" aria-valuemax="100" style="width: ' + progress * 100 + '%">' +
+ ' </div>' +
+ ' </div>' +
+ ' <div class="task-flops">' + completionInfo + '</div>' +
+ ' </div>' +
+ '</div>';
+ }
+}
diff --git a/src/scripts/controllers/simulation/timeline.ts b/src/scripts/controllers/simulation/timeline.ts
new file mode 100644
index 00000000..a558afe1
--- /dev/null
+++ b/src/scripts/controllers/simulation/timeline.ts
@@ -0,0 +1,161 @@
+import {SimulationController} from "../simulationcontroller";
+import {Util} from "../../util";
+import * as $ from "jquery";
+
+
+export class TimelineController {
+ private simulationController: SimulationController;
+ private startLabel: JQuery;
+ private endLabel: JQuery;
+ private playButton: JQuery;
+ private loadingIcon: JQuery;
+ private cacheSection: JQuery;
+ private timeMarker: JQuery;
+ private timeline: JQuery;
+ private timeUnitFraction: number;
+ private timeMarkerWidth: number;
+ private timelineWidth: number;
+
+
+ constructor(simulationController: SimulationController) {
+ this.simulationController = simulationController;
+ this.startLabel = $(".timeline-container .labels .start-time-label");
+ this.endLabel = $(".timeline-container .labels .end-time-label");
+ this.playButton = $(".timeline-container .play-btn");
+ this.loadingIcon = this.playButton.find("img");
+ this.cacheSection = $(".timeline-container .timeline .cache-section");
+ this.timeMarker = $(".timeline-container .timeline .time-marker");
+ this.timeline = $(".timeline-container .timeline");
+ this.timeMarkerWidth = this.timeMarker.width();
+ this.timelineWidth = this.timeline.width();
+ }
+
+ public togglePlayback(): void {
+ if (this.simulationController.stateCache.cacheBlock) {
+ this.simulationController.playing = false;
+ return;
+ }
+ this.simulationController.playing = !this.simulationController.playing;
+ this.setButtonIcon();
+ }
+
+ public setupListeners(): void {
+ this.playButton.on("click", () => {
+ this.togglePlayback();
+ });
+
+ $(".timeline-container .timeline").on("click", (event: JQueryEventObject) => {
+ let parentOffset = $(event.target).closest(".timeline").offset();
+ let clickX = event.pageX - parentOffset.left;
+
+ let newTick = Math.round(clickX / (this.timelineWidth * this.timeUnitFraction));
+
+ if (newTick > this.simulationController.stateCache.lastCachedTick) {
+ newTick = this.simulationController.stateCache.lastCachedTick;
+ }
+ this.simulationController.currentTick = newTick;
+ this.simulationController.checkCurrentSimulationSection();
+ this.simulationController.update();
+ });
+ }
+
+ public setButtonIcon(): void {
+ if (this.simulationController.playing && !this.playButton.hasClass("glyphicon-pause")) {
+ this.playButton.removeClass("glyphicon-play").addClass("glyphicon-pause");
+ } else if (!this.simulationController.playing && !this.playButton.hasClass("glyphicon-play")) {
+ this.playButton.removeClass("glyphicon-pause").addClass("glyphicon-play");
+ }
+ }
+
+ public update(): void {
+ this.timeUnitFraction = 1 / (this.simulationController.lastSimulatedTick + 1);
+ this.timelineWidth = $(".timeline-container .timeline").width();
+
+ this.updateTimeLabels();
+
+ this.cacheSection.css("width", this.calculateTickPosition(this.simulationController.stateCache.lastCachedTick));
+ this.timeMarker.css("left", this.calculateTickPosition(this.simulationController.currentTick));
+
+ this.updateTaskIndicators();
+ this.updateSectionMarkers();
+
+ if (this.simulationController.stateCache.cacheBlock) {
+ this.playButton.removeClass("glyphicon-pause").removeClass("glyphicon-play");
+ this.loadingIcon.show();
+ } else {
+ this.loadingIcon.hide();
+ this.setButtonIcon();
+ }
+ }
+
+ private updateTimeLabels(): void {
+ this.startLabel.text(Util.convertSecondsToFormattedTime(this.simulationController.currentTick));
+ this.endLabel.text(Util.convertSecondsToFormattedTime(this.simulationController.lastSimulatedTick));
+ }
+
+ private updateSectionMarkers(): void {
+ $(".section-marker").remove();
+
+ this.simulationController.sections.forEach((simulationSection: ISection) => {
+ if (simulationSection.startTick === 0) {
+ return;
+ }
+
+ this.timeline.append(
+ $('<div class="section-marker">')
+ .css("left", this.calculateTickPosition(simulationSection.startTick))
+ );
+ });
+ }
+
+ private updateTaskIndicators(): void {
+ $(".task-indicator").remove();
+
+ let tickStateTypes = {
+ "queueEntryTick": "task-queued",
+ "startTick": "task-started",
+ "finishedTick": "task-finished"
+ };
+
+ if (this.simulationController.stateCache.lastCachedTick === -1) {
+ return;
+ }
+
+ let indicatorCountList = new Array(this.simulationController.stateCache.lastCachedTick);
+ let indicator;
+ this.simulationController.currentExperiment.trace.tasks.forEach((task: ITask) => {
+ for (let tickStateType in tickStateTypes) {
+ if (!tickStateTypes.hasOwnProperty(tickStateType)) {
+ continue;
+ }
+
+ if (task[tickStateType] !== undefined &&
+ task[tickStateType] <= this.simulationController.stateCache.lastCachedTick) {
+
+ let bottomOffset;
+ if (indicatorCountList[task[tickStateType]] === undefined) {
+ indicatorCountList[task[tickStateType]] = 1;
+ bottomOffset = 0;
+ } else {
+ bottomOffset = indicatorCountList[task[tickStateType]] * 10;
+ indicatorCountList[task[tickStateType]]++;
+ }
+ indicator = $('<div class="task-indicator ' + tickStateTypes[tickStateType] + '">')
+ .css("left", this.calculateTickPosition(task[tickStateType]))
+ .css("bottom", bottomOffset);
+ this.timeline.append(indicator);
+ }
+ }
+ });
+ }
+
+ private calculateTickPosition(tick: number): string {
+ let correction = 0;
+ if (this.timeUnitFraction * this.timelineWidth > this.timeMarkerWidth) {
+ correction = (this.timeUnitFraction * this.timelineWidth - this.timeMarkerWidth) *
+ (tick / this.simulationController.lastSimulatedTick);
+ }
+
+ return (100 * (this.timeUnitFraction * tick + correction / this.timelineWidth)) + "%";
+ }
+} \ No newline at end of file
diff --git a/src/scripts/controllers/simulationcontroller.ts b/src/scripts/controllers/simulationcontroller.ts
new file mode 100644
index 00000000..8d9553e9
--- /dev/null
+++ b/src/scripts/controllers/simulationcontroller.ts
@@ -0,0 +1,586 @@
+///<reference path="../../../typings/index.d.ts" />
+///<reference path="mapcontroller.ts" />
+import * as $ from "jquery";
+import {MapView} from "../views/mapview";
+import {MapController, InteractionLevel, AppMode} from "./mapcontroller";
+import {Util} from "../util";
+import {StateCache} from "./simulation/statecache";
+import {ChartController} from "./simulation/chart";
+import {TaskViewController} from "./simulation/taskview";
+import {TimelineController} from "./simulation/timeline";
+
+
+export enum ColorRepresentation {
+ LOAD,
+ TEMPERATURE,
+ MEMORY
+}
+
+
+export class SimulationController {
+ public mapView: MapView;
+ public mapController: MapController;
+
+ public playing: boolean;
+ public currentTick: number;
+ public stateCache: StateCache;
+ public lastSimulatedTick: number;
+ public simulation: ISimulation;
+ public experiments: IExperiment[];
+ public currentExperiment: IExperiment;
+ public currentPath: IPath;
+ public sections: ISection[];
+ public currentSection: ISection;
+ public experimentSelectionMode: boolean;
+ public traces: ITrace[];
+ public schedulers: IScheduler[];
+ public sectionIndex: number;
+ public chartController: ChartController;
+ public timelineController: TimelineController;
+
+ public colorRepresentation: ColorRepresentation;
+ public rackToRoomMap: {[key: number]: number;};
+
+ private taskViewController: TaskViewController;
+ private tickerId: number;
+
+
+ public static showOrHideSimComponents(visibility: boolean): void {
+ if (visibility) {
+ $("#statistics-menu").removeClass("hidden");
+ $("#experiment-menu").removeClass("hidden");
+ $("#tasks-menu").removeClass("hidden");
+ $(".timeline-container").removeClass("hidden");
+ } else {
+ $("#statistics-menu").addClass("hidden");
+ $("#experiment-menu").addClass("hidden");
+ $("#tasks-menu").addClass("hidden");
+ $(".timeline-container").addClass("hidden");
+ }
+ }
+
+ constructor(mapController: MapController) {
+ this.mapController = mapController;
+ this.mapView = this.mapController.mapView;
+ this.simulation = this.mapController.mapView.simulation;
+ this.experiments = this.simulation.experiments;
+ this.taskViewController = new TaskViewController(this);
+ this.timelineController = new TimelineController(this);
+ this.chartController = new ChartController(this);
+
+ this.timelineController.setupListeners();
+ this.experimentSelectionMode = true;
+ this.sectionIndex = 0;
+
+ this.currentTick = 0;
+ this.playing = false;
+ this.stateCache = new StateCache(this);
+ this.colorRepresentation = ColorRepresentation.LOAD;
+
+ this.traces = [];
+ this.schedulers = [];
+
+ this.mapController.api.getAllTraces().then((data) => {
+ this.traces = data;
+ });
+
+ this.mapController.api.getAllSchedulers().then((data) => {
+ this.schedulers = data;
+ });
+ }
+
+ public enterMode() {
+ this.experimentSelectionMode = true;
+
+ if (this.mapController.interactionLevel === InteractionLevel.BUILDING) {
+ this.mapView.roomLayer.coloringMode = true;
+ this.mapView.dcObjectLayer.coloringMode = false;
+ } else if (this.mapController.interactionLevel === InteractionLevel.ROOM ||
+ this.mapController.interactionLevel === InteractionLevel.OBJECT) {
+ this.mapView.roomLayer.coloringMode = false;
+ this.mapView.dcObjectLayer.coloringMode = true;
+ } else if (this.mapController.interactionLevel === InteractionLevel.NODE) {
+ this.mapController.nodeModeController.goToObjectMode();
+ }
+
+ this.mapController.appMode = AppMode.SIMULATION;
+ this.mapView.dcObjectLayer.detailedMode = false;
+ this.mapView.gridLayer.setVisibility(false);
+ this.mapView.updateScene = true;
+
+ this.mapController.setAllMenuModes();
+ SimulationController.showOrHideSimComponents(true);
+ $(".mode-switch").attr("data-selected", "simulation");
+ $("#save-version-btn").hide();
+ $(".color-indicator").removeClass("hidden");
+
+ $("#change-experiment-btn").click(() => {
+ this.playing = false;
+ this.stateCache.stopCaching();
+ this.timelineController.update();
+ this.showExperimentsDialog();
+ });
+
+ this.setupColorMenu();
+ this.showExperimentsDialog();
+ }
+
+ private launchSimulation(): void {
+ this.onSimulationSectionChange();
+
+ this.chartController.setup();
+
+ this.stateCache.startCaching();
+
+ this.tickerId = setInterval(() => {
+ this.simulationTick();
+ }, 1000);
+ }
+
+ private onSimulationSectionChange(): void {
+ this.currentSection = this.currentPath.sections[this.sectionIndex];
+ this.mapView.currentDatacenter = this.currentSection.datacenter;
+
+ // Generate a map of all rack IDs in relation to their room IDs for use in room stats
+ this.rackToRoomMap = {};
+ this.currentSection.datacenter.rooms.forEach((room: IRoom) => {
+ room.tiles.forEach((tile: ITile) => {
+ if (tile.object !== undefined && tile.objectType === "RACK") {
+ this.rackToRoomMap[tile.objectId] = room.id;
+ }
+ });
+ });
+
+ if (this.mapController.interactionLevel === InteractionLevel.NODE) {
+ this.mapController.nodeModeController.goToObjectMode();
+ }
+ if (this.mapController.interactionLevel === InteractionLevel.OBJECT) {
+ this.mapController.objectModeController.goToRoomMode();
+ }
+ if (this.mapController.interactionLevel === InteractionLevel.ROOM) {
+ this.mapController.roomModeController.goToBuildingMode();
+ }
+
+ this.mapView.redrawMap();
+
+ this.mapView.zoomOutOnDC();
+ }
+
+ public exitMode() {
+ this.closeExperimentsDialog();
+
+ this.mapController.appMode = AppMode.CONSTRUCTION;
+ this.mapView.dcObjectLayer.detailedMode = true;
+ this.mapView.gridLayer.setVisibility(true);
+ this.mapView.redrawMap();
+
+ this.stateCache.stopCaching();
+ this.playing = false;
+
+ this.mapController.setAllMenuModes();
+ SimulationController.showOrHideSimComponents(false);
+
+ this.setColors();
+ $(".color-indicator").addClass("hidden")["popover"]("hide").off();
+ $(".mode-switch").attr("data-selected", "construction");
+ $("#save-version-btn").show();
+
+ clearInterval(this.tickerId);
+ }
+
+ public update() {
+ if (this.stateCache.cacheBlock) {
+ return;
+ }
+
+ this.setColors();
+ this.updateBuildingStats();
+ this.updateRoomStats();
+ this.chartController.update();
+ this.taskViewController.update();
+ }
+
+ public simulationTick(): void {
+ this.timelineController.update();
+
+ if (this.currentTick > this.lastSimulatedTick) {
+ this.currentTick = this.lastSimulatedTick;
+ this.playing = false;
+ this.timelineController.setButtonIcon();
+ }
+
+ if (this.playing) {
+ this.checkCurrentSimulationSection();
+ this.update();
+
+ if (!this.stateCache.cacheBlock) {
+ this.currentTick++;
+ }
+ }
+ }
+
+ public checkCurrentSimulationSection(): void {
+ for (let i = this.sections.length - 1; i >= 0; i--) {
+ if (this.currentTick >= this.sections[i].startTick) {
+ if (this.sectionIndex !== i) {
+ this.sectionIndex = i;
+ this.onSimulationSectionChange();
+ }
+ break;
+ }
+ }
+ }
+
+ public transitionFromBuildingToRoom(): void {
+ this.mapView.roomLayer.coloringMode = false;
+ this.mapView.dcObjectLayer.coloringMode = true;
+
+ this.setColors();
+ this.updateRoomStats();
+ this.chartController.update();
+ }
+
+ public transitionFromRoomToBuilding(): void {
+ this.mapView.roomLayer.coloringMode = true;
+ this.mapView.dcObjectLayer.coloringMode = false;
+
+ this.setColors();
+ this.updateBuildingStats();
+ this.chartController.update();
+ }
+
+ public transitionFromRoomToRack(): void {
+ this.setColors();
+ $("#statistics-menu").addClass("hidden");
+ this.chartController.update();
+ }
+
+ public transitionFromRackToRoom(): void {
+ this.setColors();
+ $("#statistics-menu").removeClass("hidden");
+ }
+
+ public transitionFromRackToNode(): void {
+ this.chartController.update();
+ }
+
+ public transitionFromNodeToRack(): void {
+ }
+
+ private showExperimentsDialog(): void {
+ $(".experiment-name-alert").hide();
+
+ this.populateExperimentsList();
+ this.populateDropdowns();
+
+ $(".experiment-row").click((event: JQueryEventObject) => {
+ if ($(event.target).hasClass("remove-experiment")) {
+ return;
+ }
+
+ let row = $(event.target).closest(".experiment-row");
+ this.prepareAndLaunchExperiment(this.experiments[row.index()]);
+ });
+
+ $(".experiment-list .list-body").on("click", ".remove-experiment", (event: JQueryEventObject) => {
+ event.stopPropagation();
+ let affectedRow = $(event.target).closest(".experiment-row");
+ let index = affectedRow.index();
+ let affectedExperiment = this.experiments[index];
+
+ MapController.showConfirmDeleteDialog("experiment", () => {
+ this.mapController.api.deleteExperiment(affectedExperiment.simulationId, affectedExperiment.id)
+ .then(() => {
+ this.experiments.splice(index, 1);
+ this.populateExperimentsList();
+ });
+ });
+ });
+
+ $("#new-experiment-btn").click(() => {
+ let nameInput = $("#new-experiment-name-input");
+ if (nameInput.val() === "") {
+ $(".experiment-name-alert").show();
+ return;
+ } else {
+ $(".experiment-name-alert").hide();
+ }
+
+ let newExperiment: IExperiment = {
+ id: -1,
+ name: nameInput.val(),
+ pathId: parseInt($("#new-experiment-path-select").val()),
+ schedulerName: $("#new-experiment-scheduler-select").val(),
+ traceId: parseInt($("#new-experiment-trace-select").val()),
+ simulationId: this.simulation.id
+ };
+
+ this.mapController.api.addExperimentToSimulation(this.simulation.id, newExperiment)
+ .then((data: IExperiment) => {
+ this.simulation.experiments.push(data);
+ this.prepareAndLaunchExperiment(data);
+ });
+ });
+
+ $(".window-close").click(() => {
+ this.exitMode();
+ });
+
+ $(".window-overlay").fadeIn(200);
+ }
+
+ private prepareAndLaunchExperiment(experiment: IExperiment): void {
+ this.prepareSimulationData(experiment);
+ this.launchSimulation();
+ this.closeExperimentsDialog();
+ }
+
+ private prepareSimulationData(experiment: IExperiment): void {
+ this.currentExperiment = experiment;
+ this.currentPath = this.getPathById(this.currentExperiment.pathId);
+ this.sections = this.currentPath.sections;
+ this.sectionIndex = 0;
+ this.currentTick = 0;
+ this.playing = false;
+ this.stateCache = new StateCache(this);
+ this.colorRepresentation = ColorRepresentation.LOAD;
+
+ this.sections.sort((a: ISection, b: ISection) => {
+ return a.startTick - b.startTick;
+ });
+
+ $("#experiment-menu-name").text(experiment.name);
+ $("#experiment-menu-path").text(SimulationController.getPathName(this.currentPath));
+ $("#experiment-menu-scheduler").text(experiment.schedulerName);
+ $("#experiment-menu-trace").text(experiment.trace.name);
+ }
+
+ private closeExperimentsDialog(): void {
+ $(".window-overlay").fadeOut(200);
+ $(".window-overlay input").val("");
+ }
+
+ private populateDropdowns(): void {
+ let pathDropdown = $("#new-experiment-path-select");
+ let traceDropdown = $("#new-experiment-trace-select");
+ let schedulerDropdown = $("#new-experiment-scheduler-select");
+
+ pathDropdown.empty();
+ for (let i = 0; i < this.simulation.paths.length; i++) {
+ pathDropdown.append(
+ $("<option>").text(SimulationController.getPathName(this.simulation.paths[i]))
+ .val(this.simulation.paths[i].id)
+ );
+ }
+
+ traceDropdown.empty();
+ for (let i = 0; i < this.traces.length; i++) {
+ traceDropdown.append(
+ $("<option>").text(this.traces[i].name)
+ .val(this.traces[i].id)
+ );
+ }
+
+ schedulerDropdown.empty();
+ for (let i = 0; i < this.schedulers.length; i++) {
+ schedulerDropdown.append(
+ $("<option>").text(this.schedulers[i].name)
+ .val(this.schedulers[i].name)
+ );
+ }
+ }
+
+ /**
+ * Populates the list of experiments.
+ */
+ private populateExperimentsList(): void {
+ let table = $(".experiment-list .list-body");
+ table.empty();
+
+ console.log("EXPERIMENT", this.experiments);
+ console.log("SIMULATION", this.simulation);
+
+ if (this.experiments.length === 0) {
+ $(".experiment-list").hide();
+ $(".no-experiments-alert").show();
+ } else {
+ $(".no-experiments-alert").hide();
+ this.experiments.forEach((experiment: IExperiment) => {
+ table.append(
+ '<div class="experiment-row">' +
+ ' <div>' + experiment.name + '</div>' +
+ ' <div>' + this.getPathNameById(experiment.pathId) + '</div>' +
+ ' <div>' + experiment.trace.name + '</div>' +
+ ' <div>' + experiment.schedulerName + '</div>' +
+ ' <div class="remove-experiment glyphicon glyphicon-remove"></div>' +
+ '</div>'
+ );
+ });
+ }
+ }
+
+ private getPathNameById(id: number): string {
+ for (let i = 0; i < this.simulation.paths.length; i++) {
+ if (id === this.simulation.paths[i].id) {
+ return SimulationController.getPathName(this.simulation.paths[i]);
+ }
+ }
+ }
+
+ private getPathById(id: number): IPath {
+ for (let i = 0; i < this.simulation.paths.length; i++) {
+ if (id === this.simulation.paths[i].id) {
+ return this.simulation.paths[i];
+ }
+ }
+ }
+
+ private static getPathName(path: IPath): string {
+ if (path.name === null) {
+ return "Path " + path.id;
+ } else {
+ return path.name;
+ }
+ }
+
+ private setColors() {
+ if (this.mapController.appMode === AppMode.SIMULATION) {
+ if (this.mapController.interactionLevel === InteractionLevel.BUILDING) {
+ this.mapView.roomLayer.intensityLevels = {};
+
+ this.stateCache.stateList[this.currentTick].roomStates.forEach((roomState: IRoomState) => {
+ if (this.colorRepresentation === ColorRepresentation.LOAD) {
+ this.mapView.roomLayer.intensityLevels[roomState.roomId] =
+ Util.determineLoadIntensityLevel(roomState.loadFraction);
+ }
+ });
+
+ this.mapView.roomLayer.draw();
+ this.mapView.dcObjectLayer.draw();
+ } else if (this.mapController.interactionLevel === InteractionLevel.ROOM ||
+ this.mapController.interactionLevel === InteractionLevel.OBJECT) {
+ this.mapView.dcObjectLayer.intensityLevels = {};
+
+ this.stateCache.stateList[this.currentTick].rackStates.forEach((rackState: IRackState) => {
+ if (this.colorRepresentation === ColorRepresentation.LOAD) {
+ this.mapView.dcObjectLayer.intensityLevels[rackState.rackId] =
+ Util.determineLoadIntensityLevel(rackState.loadFraction);
+ }
+ });
+
+ this.mapView.roomLayer.draw();
+ this.mapView.dcObjectLayer.draw();
+ }
+
+ if (this.mapController.interactionLevel === InteractionLevel.OBJECT ||
+ this.mapController.interactionLevel === InteractionLevel.NODE) {
+ this.stateCache.stateList[this.currentTick].machineStates.forEach((machineState: IMachineState) => {
+ let element = $('.node-element[data-id="' + machineState.machineId + '"] .node-element-content');
+ element.css("background-color", Util.convertIntensityToColor(
+ Util.determineLoadIntensityLevel(machineState.loadFraction)
+ ));
+
+ // Color all transparent icon overlays, as well
+ element = $('.node-element[data-id="' + machineState.machineId + '"] .icon-overlay');
+ element.css("background-color", Util.convertIntensityToColor(
+ Util.determineLoadIntensityLevel(machineState.loadFraction)
+ ));
+ });
+ }
+ } else {
+ this.mapView.roomLayer.coloringMode = false;
+ this.mapView.dcObjectLayer.coloringMode = false;
+
+ this.mapView.roomLayer.draw();
+ this.mapView.dcObjectLayer.draw();
+ }
+ }
+
+ /**
+ * Populates the building simulation menu with dynamic statistics concerning the state of all rooms in the building.
+ */
+ private updateBuildingStats(): void {
+ if (this.mapController.interactionLevel !== InteractionLevel.BUILDING) {
+ return;
+ }
+
+ console.log(this.stateCache);
+
+ let html;
+ let container = $(".building-stats-list");
+
+ container.children().remove("div");
+
+ this.stateCache.stateList[this.currentTick].roomStates.forEach((roomState: IRoomState) => {
+ if (this.colorRepresentation === ColorRepresentation.LOAD && roomState.room !== undefined) {
+ html = '<div>' +
+ ' <h4>' + roomState.room.name + '</h4>' +
+ ' <p>Load: ' + Math.round(roomState.loadFraction * 100) + '%</p>' +
+ '</div>';
+ container.append(html);
+ }
+ });
+
+ }
+
+ /**
+ * Populates the room simulation menu with dynamic statistics concerning the state of all racks in the room.
+ */
+ private updateRoomStats(): void {
+ if (this.mapController.interactionLevel !== InteractionLevel.ROOM) {
+ return;
+ }
+
+ $("#room-name-field").text(this.mapController.roomModeController.currentRoom.name);
+ $("#room-type-field").text(this.mapController.roomModeController.currentRoom.roomType);
+
+ let html;
+ let container = $(".room-stats-list");
+
+ container.children().remove("div");
+
+ this.stateCache.stateList[this.currentTick].rackStates.forEach((rackState: IRackState) => {
+ if (this.rackToRoomMap[rackState.rackId] !== this.mapController.roomModeController.currentRoom.id) {
+ return;
+ }
+ if (this.colorRepresentation === ColorRepresentation.LOAD) {
+ html = '<div>' +
+ ' <h4>' + rackState.rack.name + '</h4>' +
+ ' <p>Load: ' + Math.round(rackState.loadFraction * 100) + '%</p>' +
+ '</div>';
+ container.append(html);
+ }
+ });
+ }
+
+ private setupColorMenu(): void {
+ let html =
+ '<select class="form-control" title="Color Representation" id="color-representation-select">' +
+ ' <option value="1" selected>Load</option>' +
+ ' <option value="2">Power use</option>' +
+ '</select>';
+
+ let indicator = $(".color-indicator");
+ indicator["popover"]({
+ animation: true,
+ content: html,
+ html: true,
+ placement: "top",
+ title: "Colors represent:",
+ trigger: "manual"
+ });
+ indicator.click(() => {
+ //noinspection JSJQueryEfficiency // suppressed for dynamic element insertion
+ if ($("#color-representation-select").length) {
+ indicator["popover"]("hide");
+ } else {
+ indicator["popover"]("show");
+
+ let selectElement = $("#color-representation-select");
+ selectElement.change(() => {
+ console.log(selectElement.val());
+ });
+ }
+ });
+ }
+}
diff --git a/src/scripts/definitions.ts b/src/scripts/definitions.ts
new file mode 100644
index 00000000..a6893407
--- /dev/null
+++ b/src/scripts/definitions.ts
@@ -0,0 +1,318 @@
+/**
+ * JSON Specification of the data model.
+ *
+ * Represents the data model after populating it (based on the DB ideas retrieved from the backend). Unpopulated
+ * objects end with 'Stub'.
+ */
+
+// Webpack require declaration
+declare var require: {
+ <T>(path: string): T;
+ (paths: string[], callback: (...modules: any[]) => void): void;
+ ensure: (paths: string[], callback: (require: <T>(path: string) => T) => void) => void;
+};
+
+// Meta-constructs
+interface IDateTime {
+ year: number;
+ month: number;
+ day: number;
+ hour: number;
+ minute: number;
+ second: number;
+}
+
+interface IGridPosition {
+ x: number;
+ y: number;
+}
+
+interface IBounds {
+ min: number[];
+ center: number[];
+ max: number[];
+}
+
+interface IRoomNamePos {
+ topLeft: IGridPosition;
+ length: number;
+}
+
+interface IRoomWall {
+ startPos: number[];
+ horizontal: boolean;
+ length: number;
+}
+
+interface TilePositionObject {
+ position: IGridPosition;
+ tileObject: createjs.Shape;
+}
+
+type IRoomTypeMap = { [key: string]: string[]; };
+
+interface ITickState {
+ tick: number;
+ roomStates: IRoomState[];
+ rackStates: IRackState[];
+ machineStates: IMachineState[];
+ taskStates: ITaskState[];
+}
+
+// Communication
+interface IRequest {
+ id?: number;
+ path: string;
+ method: string;
+ parameters: {
+ body: any;
+ path: any;
+ query: any;
+ };
+ token?: string;
+}
+
+interface IResponse {
+ id?: number;
+ status: {
+ code: number;
+ description?: string;
+ };
+ content: any;
+}
+
+// Simulation
+interface ISimulation {
+ id: number;
+ name: string;
+ paths?: IPath[];
+ experiments?: IExperiment[];
+ datetimeCreated: string;
+ datetimeCreatedParsed?: IDateTime;
+ datetimeLastEdited: string;
+ datetimeLastEditedParsed?: IDateTime;
+}
+
+interface ISection {
+ id: number;
+ startTick: number;
+ simulationId: number;
+ datacenterId: number;
+ datacenter?: IDatacenter;
+}
+
+interface IPath {
+ id: number;
+ simulationId: number;
+ sections?: ISection[];
+ name: string;
+ datetimeCreated: string;
+}
+
+interface ITrace {
+ id: number;
+ name: string;
+ tasks?: ITask[];
+}
+
+interface IScheduler {
+ name: string;
+}
+
+interface ITask {
+ id: number;
+ traceId: number;
+ queueEntryTick: number;
+ startTick?: number;
+ finishedTick?: number;
+ totalFlopCount: number;
+}
+
+interface IExperiment {
+ id: number;
+ simulationId: number;
+ pathId: number;
+ traceId: number;
+ trace?: ITrace;
+ schedulerName: string;
+ name: string;
+}
+
+// Authorization
+interface IAuthorization {
+ userId: number;
+ user?: IUser;
+ simulationId: number;
+ simulation?: ISimulation;
+ authorizationLevel: string;
+}
+
+interface IUser {
+ id: number;
+ googleId: number;
+ email: string;
+ givenName: string;
+ familyName: string;
+}
+
+// DC Layout
+interface IDatacenter {
+ id: number;
+ rooms?: IRoom[];
+}
+
+interface IRoom {
+ id: number;
+ datacenterId: number;
+ name: string;
+ roomType: string;
+ tiles?: ITile[];
+}
+
+interface ITile {
+ id: number;
+ roomId: number;
+ objectId?: number;
+ objectType?: string;
+ object?: IDCObject;
+ position: IGridPosition;
+}
+
+// State
+interface IMachineState {
+ id: number;
+ machineId: number;
+ machine?: IMachine;
+ experimentId: number;
+ tick: number;
+ temperatureC: number;
+ inUseMemoryMb: number;
+ loadFraction: number;
+}
+
+interface IRackState {
+ id: number;
+ rackId: number;
+ rack?: IRack;
+ experimentId: number;
+ tick: number;
+ temperatureC: number;
+ inUseMemoryMb: number;
+ loadFraction: number;
+}
+
+interface IRoomState {
+ id: number;
+ roomId: number;
+ room?: IRoom;
+ experimentId: number;
+ tick: number;
+ temperatureC: number;
+ inUseMemoryMb: number;
+ loadFraction: number;
+}
+
+interface ITaskState {
+ id: number;
+ taskId: number;
+ task?: ITask;
+ experimentId: number;
+ tick: number;
+ flopsLeft: number;
+}
+
+// Generalization of a datacenter object
+type IDCObject = IRack | ICoolingItem | IPSU;
+
+interface IRack {
+ id: number;
+ objectType?: string;
+ name: string;
+ capacity: number;
+ powerCapacityW: number;
+ machines?: IMachine[];
+}
+
+interface ICoolingItem {
+ id: number;
+ objectType?: string;
+ energyConsumptionW: number;
+ type: string;
+ failureModelId: number;
+ failureModel?: IFailureModel;
+}
+
+interface IPSU {
+ id: number;
+ objectType?: string;
+ energyKwh: number;
+ type: string;
+ failureModelId: number;
+ failureModel?: IFailureModel;
+}
+
+// Machine
+interface IMachine {
+ id: number;
+ rackId: number;
+ position: number;
+ tags: string[];
+ cpuIds: number[];
+ cpus?: ICPU[];
+ gpuIds: number[];
+ gpus?: IGPU[];
+ memoryIds: number[];
+ memories?: IMemory[];
+ storageIds: number[];
+ storages?: IPermanentStorage[];
+}
+
+interface IProcessingUnit {
+ id: number;
+ manufacturer: string;
+ family: string;
+ generation: string;
+ model: string;
+ clockRateMhz: number;
+ numberOfCores: number;
+ energyConsumptionW: number;
+ failureModelId: number;
+ failureModel?: IFailureModel;
+}
+
+interface ICPU extends IProcessingUnit {
+
+}
+
+interface IGPU extends IProcessingUnit {
+
+}
+
+interface IStorageUnit {
+ id: number;
+ manufacturer: string;
+ family: string;
+ generation: string;
+ model: string;
+ speedMbPerS: number;
+ sizeMb: number;
+ energyConsumptionW: number;
+ failureModelId: number;
+ failureModel?: IFailureModel;
+}
+
+interface IMemory extends IStorageUnit {
+
+}
+
+interface IPermanentStorage extends IStorageUnit {
+
+}
+
+type INodeUnit = IProcessingUnit & IStorageUnit;
+
+interface IFailureModel {
+ id: number;
+ name: string;
+ rate: number;
+}
diff --git a/src/scripts/error404.entry.ts b/src/scripts/error404.entry.ts
new file mode 100644
index 00000000..07dc9ca0
--- /dev/null
+++ b/src/scripts/error404.entry.ts
@@ -0,0 +1,26 @@
+///<reference path="../../typings/globals/jquery/index.d.ts" />
+import * as $ from "jquery";
+
+
+$(document).ready(() => {
+ let text =
+ " oo oooo oo <br>" +
+ " oo oo oo oo <br>" +
+ " oo oo oo oo <br>" +
+ " oooooo oo oo oooooo <br>" +
+ " oo oo oo oo <br>" +
+ " oo oooo oo <br>";
+ let charList = text.split('');
+
+ let binary = "01001111011100000110010101101110010001000100001100100001";
+ let binaryIndex = 0;
+
+ for (let i = 0; i < charList.length; i++) {
+ if (charList[i] === "o") {
+ charList[i] = binary[binaryIndex];
+ binaryIndex++;
+ }
+ }
+
+ $(".code-block").html(charList.join(""));
+}); \ No newline at end of file
diff --git a/src/scripts/main.entry.ts b/src/scripts/main.entry.ts
new file mode 100644
index 00000000..c7d6ef90
--- /dev/null
+++ b/src/scripts/main.entry.ts
@@ -0,0 +1,69 @@
+///<reference path="../../typings/index.d.ts" />
+///<reference path="./views/mapview.ts" />
+import * as $ from "jquery";
+import {MapView} from "./views/mapview";
+import {APIController} from "./controllers/connection/api";
+window["$"] = $;
+require("jquery-mousewheel");
+window["jQuery"] = $;
+
+require("./user");
+
+
+$(document).ready(function () {
+ new Display(); //tslint:disable-line:no-unused-expression
+});
+
+
+/**
+ * Class responsible for launching the main view.
+ */
+class Display {
+ private stage: createjs.Stage;
+ private view: MapView;
+
+
+ /**
+ * Adjusts the canvas size to fit the window's initial dimensions (full expansion).
+ */
+ private static fitCanvasSize() {
+ let canvas = $("#main-canvas");
+ let parent = canvas.parent();
+ parent.height($(window).height() - 50);
+ canvas.attr("width", parent.width());
+ canvas.attr("height", parent.height());
+ }
+
+ constructor() {
+ // Check whether project has been selected before going to the app page
+ if (localStorage.getItem("simulationId") === null) {
+ window.location.replace("projects");
+ return;
+ }
+
+ Display.fitCanvasSize();
+ this.stage = new createjs.Stage("main-canvas");
+
+ new APIController((api: APIController) => {
+ api.getSimulation(parseInt(localStorage.getItem("simulationId")))
+ .then((simulationData: ISimulation) => {
+ if (simulationData.name !== "") {
+ document.title = simulationData.name + " | OpenDC";
+ }
+
+ api.getPathsBySimulation(simulationData.id)
+ .then((pathData: IPath[]) => {
+ simulationData.paths = pathData;
+ }).then(() => {
+ return api.getExperimentsBySimulation(simulationData.id);
+ }).then((experimentData: IExperiment[]) => {
+ $(".loading-overlay").hide();
+
+ simulationData.experiments = experimentData;
+ this.view = new MapView(simulationData, this.stage);
+ });
+ });
+ });
+
+ }
+}
diff --git a/src/scripts/profile.entry.ts b/src/scripts/profile.entry.ts
new file mode 100644
index 00000000..57c6b56c
--- /dev/null
+++ b/src/scripts/profile.entry.ts
@@ -0,0 +1,40 @@
+///<reference path="../../typings/index.d.ts" />
+import * as $ from "jquery";
+import {APIController} from "./controllers/connection/api";
+import {removeUserInfo} from "./user";
+window["jQuery"] = $;
+
+
+$(document).ready(() => {
+ let api = new APIController(() => {
+ });
+
+ $("#delete-account").on("click", () => {
+ let modalDialog = <any>$("#confirm-delete-account");
+
+ // Function called on delete confirmation
+ let callback = () => {
+ api.deleteUser(parseInt(localStorage.getItem("userId"))).then(() => {
+ removeUserInfo();
+ gapi.auth2.getAuthInstance().signOut().then(() => {
+ window.location.href = "/";
+ });
+ }, (reason: any) => {
+ modalDialog.find("button.confirm").off();
+ modalDialog.modal("hide");
+
+ let alert = $(".account-delete-alert");
+ alert.find("code").text(reason.code + ": " + reason.description);
+
+ alert.slideDown(200);
+
+ setTimeout(() => {
+ alert.slideUp(200);
+ }, 5000);
+ });
+ };
+
+ modalDialog.find("button.confirm").on("click", callback);
+ modalDialog.modal("show");
+ });
+});
diff --git a/src/scripts/projects.entry.ts b/src/scripts/projects.entry.ts
new file mode 100644
index 00000000..1ceb308b
--- /dev/null
+++ b/src/scripts/projects.entry.ts
@@ -0,0 +1,651 @@
+///<reference path="../../typings/globals/jquery/index.d.ts" />
+import * as $ from "jquery";
+import {APIController} from "./controllers/connection/api";
+import {Util} from "./util";
+window["jQuery"] = $;
+
+require("./user");
+
+
+$(document).ready(() => {
+ let api;
+ new APIController((apiInstance: APIController) => {
+ api = apiInstance;
+ api.getAuthorizationsByUser(parseInt(localStorage.getItem("userId"))).then((data: any) => {
+ let projectsController = new ProjectsController(data, api);
+ new WindowController(projectsController, api);
+ });
+ });
+});
+
+
+/**
+ * Controller class responsible for rendering the authorization list views and handling interactions with them.
+ */
+class ProjectsController {
+ public static authIconMap = {
+ "OWN": "glyphicon-home",
+ "EDIT": "glyphicon-pencil",
+ "VIEW": "glyphicon-eye-open"
+ };
+
+ public currentUserId: number;
+ public authorizations: IAuthorization[];
+ public authorizationsFiltered: IAuthorization[];
+ public windowController: WindowController;
+
+ private api: APIController;
+
+
+ /**
+ * 'Opens' a project, by putting the relevant simulation ID into local storage and referring to the app page.
+ *
+ * @param authorization The user's authorization belonging to the project to be opened
+ */
+ public static openProject(authorization: IAuthorization): void {
+ localStorage.setItem("simulationId", authorization.simulationId.toString());
+ localStorage.setItem("simulationAuthLevel", authorization.authorizationLevel);
+ window.location.href = "app";
+ }
+
+ /**
+ * Converts a list of authorizations into DOM objects, and adds them to the main list body of the page.
+ *
+ * @param list The list of authorizations to be displayed
+ */
+ public static populateList(list: IAuthorization[]): void {
+ let body = $(".project-list .list-body");
+ body.empty();
+
+ list.forEach((element: IAuthorization) => {
+ body.append(
+ $('<div class="project-row">').append(
+ $('<div>').text(element.simulation.name),
+ $('<div>').text(Util.formatDateTime(element.simulation.datetimeLastEditedParsed)),
+ $('<div>').append($('<span class="glyphicon">')
+ .addClass(this.authIconMap[element.authorizationLevel]),
+ Util.toSentenceCase(element.authorizationLevel)
+ )
+ ).attr("data-id", element.simulationId)
+ );
+ });
+ };
+
+ /**
+ * Filters an authorization list based on what authorization level is required.
+ *
+ * Leaves the original list intact.
+ *
+ * @param list The authorization list to be filtered
+ * @param ownedByUser Whether only authorizations should be included that are owned by the user, or whether only
+ * authorizations should be included that the user has no ownership over
+ * @returns {IAuthorization[]} A filtered list of authorizations
+ */
+ public static filterList(list: IAuthorization[], ownedByUser: boolean): IAuthorization[] {
+ let resultList: IAuthorization[] = [];
+
+ list.forEach((element: IAuthorization) => {
+ if (element.authorizationLevel === "OWN") {
+ if (ownedByUser) {
+ resultList.push(element);
+ }
+ } else {
+ if (!ownedByUser) {
+ resultList.push(element);
+ }
+ }
+ });
+
+ return resultList;
+ };
+
+ /**
+ * Activates a certain filter heading button, while deactivating the rest.
+ *
+ * @param target The event target to activate
+ */
+ private static activateFilterViewButton(target: JQuery): void {
+ target.addClass("active");
+ target.siblings().removeClass("active");
+ };
+
+ constructor(authorizations: IAuthorization[], api: APIController) {
+ this.currentUserId = parseInt(localStorage.getItem("userId"));
+ this.authorizations = authorizations;
+ this.authorizationsFiltered = authorizations;
+ this.api = api;
+
+ this.updateNoProjectsAlert();
+
+ this.handleFilterClick();
+
+ // Show a project view upon clicking on a simulation row
+ $("body").on("click", ".project-row", (event: JQueryEventObject) => {
+ this.displayProjectView($(event.target));
+ });
+ }
+
+ /**
+ * Update the list of authorizations, by fetching them from the server and reloading the list.
+ *
+ * Goes to the 'All Projects' page after this refresh.
+ */
+ public updateAuthorizations(): void {
+ this.api.getAuthorizationsByUser(this.currentUserId).then((data: any) => {
+ this.authorizations = data;
+ this.authorizationsFiltered = this.authorizations;
+
+ this.updateNoProjectsAlert();
+
+ this.goToAllProjects();
+ });
+ }
+
+ /**
+ * Show (or hide) the 'No projects here' alert in the list view, based on whether there are projects present.
+ */
+ private updateNoProjectsAlert(): void {
+ if (this.authorizationsFiltered.length === 0) {
+ $(".no-projects-alert").show();
+ $(".project-list").hide();
+ } else {
+ $(".no-projects-alert").hide();
+ $(".project-list").show();
+ }
+ }
+
+ /**
+ * Displays a project view with authorizations and entry buttons, inline within the table.
+ *
+ * @param target The element that was clicked on to launch this view
+ */
+ private displayProjectView(target: JQuery): void {
+ let closestRow = target.closest(".project-row");
+ let activeElement = $(".project-row.active");
+
+ // Disable previously selected row elements and remove any project-views, to have only one view open at a time
+ if (activeElement.length > 0) {
+ let view = $(".project-view").first();
+
+ view.slideUp(200, () => {
+ activeElement.removeClass("active");
+ view.remove();
+ });
+
+ if (closestRow.is(activeElement)) {
+ return;
+ }
+ }
+
+ let simulationId = parseInt(closestRow.attr("data-id"), 10);
+
+ // Generate a list of participants of this project
+ this.api.getAuthorizationsBySimulation(simulationId).then((data: any) => {
+ let simAuthorizations = data;
+ let participants = [];
+
+ Util.sortAuthorizations(simAuthorizations);
+
+ // For each participant of this simulation, include his/her name along with an icon of their authorization
+ // level in the list
+ simAuthorizations.forEach((authorization: IAuthorization) => {
+ let authorizationString = ' (<span class="glyphicon ' +
+ ProjectsController.authIconMap[authorization.authorizationLevel] + '"></span>)';
+ if (authorization.userId === this.currentUserId) {
+ participants.push(
+ 'You' + authorizationString
+ );
+ } else {
+ participants.push(
+ authorization.user.givenName + ' ' + authorization.user.familyName + authorizationString
+ );
+ }
+ });
+
+ // Generate a project view component with participants and relevant actions
+ let object = $('<div class="project-view">').append(
+ $('<div class="participants">').append(
+ $('<strong>').text("Participants"),
+ $('<div>').html(participants.join(", "))
+ ),
+ $('<div class="access-buttons">').append(
+ $('<div class="inline-btn edit">').text("Edit"),
+ $('<div class="inline-btn open">').text("Open")
+ )
+ ).hide();
+
+ closestRow.after(object);
+
+ // Hide the 'edit' button for non-owners and -editors
+ let currentAuth = this.authorizationsFiltered[closestRow.index(".project-row")];
+ if (currentAuth.authorizationLevel !== "OWN") {
+ $(".project-view .inline-btn.edit").hide();
+ }
+
+ object.find(".edit").click(() => {
+ this.windowController.showEditProjectWindow(simAuthorizations);
+ });
+
+ object.find(".open").click(() => {
+ ProjectsController.openProject(currentAuth);
+ });
+
+ closestRow.addClass("active");
+ object.slideDown(200);
+ });
+ }
+
+ /**
+ * Controls the filtered authorization list, based on clicks from the side menu.
+ */
+ private handleFilterClick(): void {
+ $(".all-projects").on("click", () => {
+ this.goToAllProjects();
+ });
+
+ $(".my-projects").on("click", () => {
+ this.goToMyProjects();
+ });
+
+ $(".shared-projects").on("click", () => {
+ this.goToSharedProjects();
+ });
+
+ this.goToAllProjects();
+ }
+
+ /**
+ * Show a list containing all projects (regardless of the authorization level the user has over them).
+ */
+ private goToAllProjects(): void {
+ this.authorizationsFiltered = this.authorizations;
+ ProjectsController.populateList(this.authorizations);
+ this.updateNoProjectsAlert();
+
+ ProjectsController.activateFilterViewButton($(".all-projects"));
+ }
+
+ /**
+ * Show a list containing only projects that the user owns.
+ */
+ private goToMyProjects(): void {
+ this.authorizationsFiltered = ProjectsController.filterList(this.authorizations, true);
+ ProjectsController.populateList(this.authorizationsFiltered);
+ this.updateNoProjectsAlert();
+
+ ProjectsController.activateFilterViewButton($(".my-projects"));
+ }
+
+ /**
+ * Show a list containing only projects that the user does not own (but can edit or view).
+ */
+ private goToSharedProjects(): void {
+ this.authorizationsFiltered = ProjectsController.filterList(this.authorizations, false);
+ ProjectsController.populateList(this.authorizationsFiltered);
+ this.updateNoProjectsAlert();
+
+ ProjectsController.activateFilterViewButton($(".shared-projects"));
+ }
+}
+
+
+/**
+ * Controller class responsible for rendering the project add/edit window and handle user interaction with it.
+ */
+class WindowController {
+ private projectsController: ProjectsController;
+ private windowOverlay: JQuery;
+ private window: JQuery;
+ private table: JQuery;
+ private closeCallback: () => any;
+ private simAuthorizations: IAuthorization[];
+ private simulationId: number;
+ private editMode: boolean;
+ private api: APIController;
+
+
+ constructor(projectsController: ProjectsController, api: APIController) {
+ this.projectsController = projectsController;
+ this.windowOverlay = $(".window-overlay");
+ this.window = $(".projects-window");
+ this.table = $(".participants-table");
+ this.projectsController.windowController = this;
+ this.simAuthorizations = [];
+ this.editMode = false;
+ this.api = api;
+
+ $(".window-footer .btn").hide();
+
+ $(".participant-add-form").submit((event: JQueryEventObject) => {
+ event.preventDefault();
+ $(".participant-add-form .btn").trigger("click");
+ });
+
+ $(".project-name-form").submit((event: JQueryEventObject) => {
+ event.preventDefault();
+ $(".project-name-form .btn").trigger("click");
+ });
+
+ // Clean-up actions to occur after every window-close
+ this.closeCallback = () => {
+ this.table.empty();
+ $(".project-name-form .btn").off();
+ $(".participant-add-form .btn").off();
+ $(".window-footer .btn").hide().off();
+ $(".participant-email-alert").hide();
+ $(".participant-level div").removeClass("active").off();
+ this.table.off("click", ".participant-remove div");
+
+ $(".project-name-form input").val("");
+ $(".participant-add-form input").val("");
+
+ if (this.editMode) {
+ this.projectsController.updateAuthorizations();
+ }
+ };
+
+ $(".new-project-btn").click(() => {
+ this.showAddProjectWindow();
+ });
+
+ // Stop click events on the window from closing it indirectly
+ this.window.click((event: JQueryEventObject) => {
+ event.stopPropagation();
+ });
+
+ $(".window-close, .window-overlay").click(() => {
+ this.closeWindow();
+ });
+ }
+
+ /**
+ * Displays a window for project edits (used for adding participants and changing the name).
+ *
+ * @param authorizations The authorizations of the simulation project to be edited.
+ */
+ public showEditProjectWindow(authorizations: IAuthorization[]): void {
+ this.editMode = true;
+ this.simAuthorizations = [];
+ this.simulationId = authorizations[0].simulation.id;
+
+ // Filter out the user's authorization from the authorization list (not to be displayed in the list)
+ authorizations.forEach((authorization: IAuthorization) => {
+ if (authorization.userId !== this.projectsController.currentUserId) {
+ this.simAuthorizations.push(authorization);
+ }
+ });
+
+ $(".window .window-heading").text("Edit project");
+
+ $(".project-name-form input").val(authorizations[0].simulation.name);
+
+ $(".project-name-form .btn").css("display", "inline-block").click(() => {
+ let nameInput = $(".project-name-form input").val();
+ if (nameInput !== "") {
+ authorizations[0].simulation.name = nameInput;
+ this.api.updateSimulation(authorizations[0].simulation);
+ }
+ });
+
+ $(".project-open-btn").show().click(() => {
+ ProjectsController.openProject({
+ userId: this.projectsController.currentUserId,
+ simulationId: this.simulationId,
+ authorizationLevel: "OWN"
+ });
+ });
+
+ $(".project-delete-btn").show().click(() => {
+ this.api.deleteSimulation(authorizations[0].simulationId).then(() => {
+ this.projectsController.updateAuthorizations();
+ this.closeWindow();
+ });
+ });
+
+ $(".participant-add-form .btn").click(() => {
+ this.handleParticipantAdd((userId: number) => {
+ this.api.addAuthorization({
+ userId: userId,
+ simulationId: authorizations[0].simulationId,
+ authorizationLevel: "VIEW"
+ });
+ });
+ });
+
+ this.table.on("click", ".participant-level div", (event: JQueryEventObject) => {
+ this.handleParticipantLevelChange(event, (authorization: IAuthorization) => {
+ this.api.updateAuthorization(authorization);
+ });
+ });
+
+ this.table.on("click", ".participant-remove div", (event: JQueryEventObject) => {
+ this.handleParticipantDelete(event, (authorization) => {
+ this.api.deleteAuthorization(authorization);
+ });
+ });
+
+ this.populateParticipantList();
+
+ this.windowOverlay.fadeIn(200);
+ }
+
+ /**
+ * Shows a window to be used for creating a new project.
+ */
+ public showAddProjectWindow(): void {
+ this.editMode = false;
+ this.simAuthorizations = [];
+
+ $(".project-name-form .btn").hide();
+
+ $(".window .window-heading").text("Create a project");
+
+ $(".project-create-open-btn").show().click(() => {
+ if ($(".project-name-form input").val() === "") {
+ this.showAlert(".project-name-alert");
+ return;
+ }
+ this.createSimulation((simulationId: number) => {
+ ProjectsController.openProject({
+ userId: this.projectsController.currentUserId,
+ simulationId,
+ authorizationLevel: "OWN"
+ });
+ });
+ });
+
+ $(".project-create-btn").show().click(() => {
+ if ($(".project-name-form input").val() === "") {
+ this.showAlert(".project-name-alert");
+ return;
+ }
+ this.createSimulation(() => {
+ this.projectsController.updateAuthorizations();
+ this.closeWindow();
+ });
+ });
+
+ $(".project-cancel-btn").show().click(() => {
+ this.closeWindow();
+ });
+
+ this.table.empty();
+
+ $(".project-name-form input").val("");
+ $(".participant-add-form input").val("");
+
+ $(".participant-add-form .btn").click(() => {
+ this.handleParticipantAdd(() => {
+ });
+ });
+
+ this.table.on("click", ".participant-level div", (event: JQueryEventObject) => {
+ this.handleParticipantLevelChange(event, () => {
+ });
+ });
+
+ this.table.on("click", ".participant-remove div", (event: JQueryEventObject) => {
+ this.handleParticipantDelete(event, () => {
+ });
+ });
+
+ this.windowOverlay.fadeIn(200);
+ }
+
+ /**
+ * Creates a new simulation with the current name input and all currently present authorizations added in the
+ * project 'add' window.
+ *
+ * @param callback The function to be called when this operation has succeeded
+ */
+ private createSimulation(callback: (simulationId: number) => any): void {
+ this.api.addSimulation({
+ id: -1,
+ name: $(".project-name-form input").val(),
+ datetimeCreated: Util.getCurrentDateTime(),
+ datetimeLastEdited: Util.getCurrentDateTime()
+ }).then((data: any) => {
+ let asyncCounter = this.simAuthorizations.length;
+ this.simAuthorizations.forEach((authorization: IAuthorization) => {
+ authorization.simulationId = data.id;
+ this.api.addAuthorization(authorization).then((data: any) => {
+ asyncCounter--;
+
+ if (asyncCounter <= 0) {
+ callback(data.id);
+ }
+ });
+ });
+ if (this.simAuthorizations.length === 0) {
+ callback(data.id);
+ }
+ });
+ }
+
+ /**
+ * Displays an alert of the given class name, to disappear again after a certain pre-defined timeout.
+ *
+ * @param name A selector that uniquely identifies the alert body to be shown.
+ */
+ private showAlert(name): void {
+ let alert = $(name);
+ alert.slideDown(200);
+
+ setTimeout(() => {
+ alert.slideUp(200);
+ }, 5000);
+ }
+
+ /**
+ * Closes the window with a transition, and calls the relevant callback after that transition has ended.
+ */
+ private closeWindow(): void {
+ this.windowOverlay.fadeOut(200, () => {
+ this.closeCallback();
+ });
+ }
+
+ /**
+ * Handles the click on an authorization icon in the project window authorization list.
+ *
+ * @param event The JQuery click event
+ * @param callback The function to be called after the authorization was changed
+ */
+ private handleParticipantLevelChange(event: JQueryEventObject,
+ callback: (authorization: IAuthorization) => any): void {
+ $(event.target).closest(".participant-level").find("div").removeClass("active");
+ $(event.target).addClass("active");
+
+ let affectedRow = $(event.target).closest(".participant-row");
+
+ for (let level in ProjectsController.authIconMap) {
+ if (!ProjectsController.authIconMap.hasOwnProperty(level)) {
+ continue;
+ }
+ if ($(event.target).is("." + ProjectsController.authIconMap[level])) {
+ this.simAuthorizations[affectedRow.index()].authorizationLevel = level;
+ callback(this.simAuthorizations[affectedRow.index()]);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handles the event where a user seeks to add a participant.
+ *
+ * @param callback The function to be called if the participant could be found and can be added.
+ */
+ private handleParticipantAdd(callback: (userId: number) => any): void {
+ let inputForm = $(".participant-add-form input");
+ this.api.getUserByEmail(inputForm.val()).then((data: any) => {
+ let insert = true;
+ for (let i = 0; i < this.simAuthorizations.length; i++) {
+ if (this.simAuthorizations[i].userId === data.id) {
+ insert = false;
+ }
+ }
+
+ let simulationId = this.editMode ? this.simulationId : -1;
+ if (data.id !== this.projectsController.currentUserId && insert) {
+ this.simAuthorizations.push({
+ userId: data.id,
+ user: data,
+ simulationId: simulationId,
+ authorizationLevel: "VIEW"
+ });
+ callback(data.id);
+ Util.sortAuthorizations(this.simAuthorizations);
+ this.populateParticipantList();
+ }
+
+ // Clear input field after submission
+ inputForm.val("");
+ }, (reason: any) => {
+ if (reason.code === 404) {
+ this.showAlert(".participant-email-alert");
+ }
+ });
+ }
+
+ /**
+ * Handles click events on the 'remove' icon next to each participant.
+ *
+ * @param event The JQuery click event
+ * @param callback The function to be executed on removal of the participant from the internal list
+ */
+ private handleParticipantDelete(event: JQueryEventObject, callback: (authorization: IAuthorization) => any): void {
+ let affectedRow = $(event.target).closest(".participant-row");
+ let index = affectedRow.index();
+ let authorization = this.simAuthorizations[index];
+ this.simAuthorizations.splice(index, 1);
+ this.populateParticipantList();
+ callback(authorization);
+ }
+
+ /**
+ * Populates the list of participants in the project edit window with all current authorizations.
+ */
+ private populateParticipantList(): void {
+ this.table.empty();
+
+ this.simAuthorizations.forEach((authorization: IAuthorization) => {
+ this.table.append(
+ '<div class="participant-row">' +
+ ' <div class="participant-name">' + authorization.user.givenName + ' ' +
+ authorization.user.familyName + '</div>' +
+ ' <div class="participant-level">' +
+ ' <div class="participant-level-view glyphicon glyphicon-eye-open ' +
+ (authorization.authorizationLevel === "VIEW" ? 'active' : '') + '"></div>' +
+ ' <div class="participant-level-edit glyphicon glyphicon-pencil ' +
+ (authorization.authorizationLevel === "EDIT" ? 'active' : '') + '"></div>' +
+ ' <div class="participant-level-own glyphicon glyphicon-home ' +
+ (authorization.authorizationLevel === "OWN" ? 'active' : '') + '"></div>' +
+ ' </div>' +
+ ' <div class="participant-remove">' +
+ ' <div class="glyphicon glyphicon-remove"></div>' +
+ ' </div>' +
+ '</div>'
+ );
+ });
+ }
+}
diff --git a/src/scripts/serverconnection.ts b/src/scripts/serverconnection.ts
new file mode 100644
index 00000000..c7f6e598
--- /dev/null
+++ b/src/scripts/serverconnection.ts
@@ -0,0 +1,59 @@
+import {SocketController} from "./controllers/connection/socket";
+
+
+export class ServerConnection {
+ private static _socketControllerInstance: SocketController;
+
+
+ public static connect(onConnect: () => any): void {
+ this._socketControllerInstance = new SocketController(onConnect);
+ }
+
+ public static send(request: IRequest): Promise<any> {
+ return new Promise((resolve, reject) => {
+ let checkUnimplemented = ServerConnection.interceptUnimplementedEndpoint(request);
+ if (checkUnimplemented) {
+ resolve(checkUnimplemented.content);
+ return;
+ }
+
+ this._socketControllerInstance.sendRequest(request, (response: IResponse) => {
+ if (response.status.code === 200) {
+ ServerConnection.convertFlatToNestedPositionData(response.content, resolve);
+ } else {
+ reject(response.status);
+ }
+ });
+ })
+ }
+
+ public static convertFlatToNestedPositionData(responseContent, resolve): void {
+ let nestPositionCoords = (content: any) => {
+ if (content["positionX"] !== undefined) {
+ content["position"] = {
+ x: content["positionX"],
+ y: content["positionY"]
+ };
+ }
+ };
+
+ if (responseContent instanceof Array) {
+ responseContent.forEach(nestPositionCoords);
+ } else {
+ nestPositionCoords(responseContent);
+ }
+
+ resolve(responseContent);
+ }
+
+ /**
+ * Intercepts endpoints that are still unimplemented and responds with mock data.
+ *
+ * @param request The request
+ * @returns {any} A response, or null if the endpoint is not on the list of unimplemented ones.
+ */
+ public static interceptUnimplementedEndpoint(request: IRequest): IResponse {
+ // Endpoints that are unimplemented can be intercepted here
+ return null;
+ }
+} \ No newline at end of file
diff --git a/src/scripts/splash.entry.ts b/src/scripts/splash.entry.ts
new file mode 100644
index 00000000..c1be1c28
--- /dev/null
+++ b/src/scripts/splash.entry.ts
@@ -0,0 +1,160 @@
+///<reference path="../../typings/index.d.ts" />
+///<reference path="./definitions.ts" />
+import * as $ from "jquery";
+import {APIController} from "./controllers/connection/api";
+window["jQuery"] = $;
+require("jquery.easing");
+
+
+// Variable to check whether user actively logged in by clicking the login button
+let hasClickedLogin = false;
+
+
+$(document).ready(() => {
+ /**
+ * jQuery for page scrolling feature
+ */
+ $('a.page-scroll').bind('click', function (event) {
+ let $anchor = $(this);
+ $('html, body').stop().animate({
+ scrollTop: $($anchor.attr('href')).offset().top
+ }, 1000, 'easeInOutExpo', () => {
+ if ($anchor.attr('href') === "#page-top") {
+ location.hash = '';
+ } else {
+ location.hash = $anchor.attr('href');
+ }
+ });
+ event.preventDefault();
+ });
+
+ let checkScrollState = () => {
+ const startY = 100;
+
+ if ($(window).scrollTop() > startY || window.innerWidth < 768) {
+ $('.navbar').removeClass("navbar-transparent");
+ } else {
+ $('.navbar').addClass("navbar-transparent");
+ }
+ };
+
+ $(window).on("scroll load resize", function () {
+ checkScrollState();
+ });
+
+ checkScrollState();
+
+ let googleSigninBtn = $("#google-signin");
+ googleSigninBtn.click(() => {
+ hasClickedLogin = true;
+ });
+
+ /**
+ * Display appropriate user buttons
+ */
+ if (localStorage.getItem("googleToken") !== null) {
+ googleSigninBtn.hide();
+ $(".navbar .logged-in").css("display", "inline-block");
+ $(".logged-in .sign-out").click(() => {
+ let auth2 = gapi.auth2.getAuthInstance();
+
+ auth2.signOut().then(() => {
+ // Remove session storage items
+ localStorage.removeItem("googleToken");
+ localStorage.removeItem("googleTokenExpiration");
+ localStorage.removeItem("googleName");
+ localStorage.removeItem("googleEmail");
+ localStorage.removeItem("userId");
+ localStorage.removeItem("simulationId");
+
+ location.reload();
+ });
+ });
+
+ // Check whether Google auth. token has expired and signin again if necessary
+ let currentTime = (new Date()).getTime();
+ if (parseInt(localStorage.getItem("googleTokenExpiration")) - currentTime <= 0) {
+ gapi.auth2.getAuthInstance().signIn().then(() => {
+ let authResponse = gapi.auth2.getAuthInstance().currentUser.get().getAuthResponse();
+ localStorage.setItem("googleToken", authResponse.id_token);
+ let expirationTime = (new Date()).getTime() / 1000 + parseInt(authResponse.expires_in) - 5;
+ localStorage.setItem("googleTokenExpiration", expirationTime.toString());
+ });
+ }
+ }
+});
+
+/**
+ * Google signin button
+ */
+window["renderButton"] = () => {
+ gapi.signin2.render('google-signin', {
+ 'scope': 'profile email',
+ 'width': 100,
+ 'height': 30,
+ 'longtitle': false,
+ 'theme': 'dark',
+ 'onsuccess': (googleUser) => {
+ let api;
+ new APIController((apiInstance: APIController) => {
+ api = apiInstance;
+ let email = googleUser.getBasicProfile().getEmail();
+
+ let getUser = (userId: number) => {
+ let reload = true;
+ if (localStorage.getItem("userId") !== null) {
+ reload = false;
+ }
+
+ localStorage.setItem("userId", userId.toString());
+
+ // Redirect to the projects page
+ if (hasClickedLogin) {
+ window.location.href = "projects";
+ } else if (reload) {
+ window.location.reload();
+ }
+
+ };
+
+ // Send the token to the server
+ let id_token = googleUser.getAuthResponse().id_token;
+ // Calculate token expiration time (in seconds since epoch)
+ let expirationTime = (new Date()).getTime() / 1000 + googleUser.getAuthResponse().expires_in - 5;
+
+ $.post('https://opendc.ewi.tudelft.nl/tokensignin', {
+ idtoken: id_token
+ }, (data: any) => {
+ // Save user information in session storage for later use on other pages
+ localStorage.setItem("googleToken", id_token);
+ localStorage.setItem("googleTokenExpiration", expirationTime.toString());
+ localStorage.setItem("googleName", googleUser.getBasicProfile().getGivenName() + " " +
+ googleUser.getBasicProfile().getFamilyName());
+ localStorage.setItem("googleEmail", email);
+
+ if (data.isNewUser === true) {
+ api.addUser({
+ id: -1,
+ email,
+ googleId: googleUser.getBasicProfile().getId(),
+ givenName: googleUser.getBasicProfile().getGivenName(),
+ familyName: googleUser.getBasicProfile().getFamilyName()
+ }).then((userData: any) => {
+ getUser(userData.id);
+ });
+ } else {
+ getUser(data.userId);
+ }
+ });
+ });
+ },
+ 'onfailure': () => {
+ console.log("Oops, something went wrong with your Google signin... Try again?")
+ }
+ });
+};
+
+// Set the language of the GAuth button to be English
+window["___gcfg"] = {
+ lang: 'en'
+};
diff --git a/src/scripts/tests/util.spec.ts b/src/scripts/tests/util.spec.ts
new file mode 100644
index 00000000..74d62dfa
--- /dev/null
+++ b/src/scripts/tests/util.spec.ts
@@ -0,0 +1,326 @@
+///<reference path="../util.ts" />
+///<reference path="../../../typings/globals/jasmine/index.d.ts" />
+import {Util} from "../util";
+
+
+class TestUtils {
+ /**
+ * Checks whether the two (three-dimensional) wall lists are equivalent in content.
+ *
+ * This is a set-compare method, meaning that the order of the elements does not matter, but that they are present
+ * in both arrays.
+ *
+ * Example of such a list: [[[1, 1], [2, 1]], [[3, 1], [0, 0]]]
+ *
+ * @param list1 The first list
+ * @param list2 The second list
+ */
+ public static wallListEquals(list1: IRoomWall[], list2: IRoomWall[]): void {
+ let current, found, counter;
+
+ counter = 0;
+ while (list1.length > 0) {
+ current = list1.pop();
+ found = false;
+ list2.forEach((e: IRoomWall) => {
+ if (current.startPos[0] === e.startPos[0] && current.startPos[1] === e.startPos[1] &&
+ current.horizontal === e.horizontal && current.length === e.length) {
+ counter++;
+ found = true;
+ }
+ });
+ if (!found) {
+ fail();
+ }
+ }
+ expect(list2.length).toEqual(counter);
+ }
+
+ /**
+ * Does the same as wallList3DEquals, only for two lists of tiles.
+ *
+ * @param expected
+ * @param actual
+ */
+ public static positionListEquals(expected: IGridPosition[], actual: IGridPosition[]): void {
+ let current, found;
+ let counter = 0;
+
+ while (expected.length > 0) {
+ current = expected.pop();
+ found = false;
+ actual.forEach((e) => {
+ if (current.x === e.x && current.y === e.y) {
+ counter++;
+ found = true;
+ }
+ });
+ if (!found) {
+ fail();
+ }
+ }
+
+ expect(actual.length).toEqual(counter);
+ }
+
+ public static boundsEquals(actual: IBounds, expected: IBounds): void {
+ expect(actual.min[0]).toBe(expected.min[0]);
+ expect(actual.min[1]).toBe(expected.min[1]);
+ expect(actual.center[0]).toBe(expected.center[0]);
+ expect(actual.center[1]).toBe(expected.center[1]);
+ expect(actual.max[0]).toBe(expected.max[0]);
+ expect(actual.max[1]).toBe(expected.max[1]);
+ }
+}
+
+describe("Deriving wall locations", () => {
+ it("should generate walls around a single tile", () => {
+ let room = {
+ id: -1,
+ datacenterId: -1,
+ name: "testroom",
+ roomType: "none",
+ tiles: [{
+ id: -1,
+ roomId: -1,
+ position: {x: 1, y: 1}
+ }]
+ };
+
+ let result = Util.deriveWallLocations([
+ room
+ ]);
+ let expected: IRoomWall[] = [
+ {
+ startPos: [1, 1],
+ horizontal: false,
+ length: 1
+ },
+ {
+ startPos: [2, 1],
+ horizontal: false,
+ length: 1
+ },
+ {
+ startPos: [1, 1],
+ horizontal: true,
+ length: 1
+ },
+ {
+ startPos: [1, 2],
+ horizontal: true,
+ length: 1
+ }
+ ];
+
+ TestUtils.wallListEquals(expected, result);
+ }
+ );
+
+ it("should generate walls around two tiles connected by an edge", () => {
+ let room = {
+ id: -1,
+ datacenterId: -1,
+ name: "testroom",
+ roomType: "none",
+ tiles: [
+ {
+ id: -1,
+ roomId: -1,
+ position: {x: 1, y: 1}
+ }, {
+ id: -1,
+ roomId: -1,
+ position: {x: 1, y: 2}
+ }
+ ]
+ };
+
+ let result = Util.deriveWallLocations([
+ room
+ ]);
+ let expected: IRoomWall[] = [
+ {
+ startPos: [1, 1],
+ horizontal: false,
+ length: 2
+ },
+ {
+ startPos: [2, 1],
+ horizontal: false,
+ length: 2
+ },
+ {
+ startPos: [1, 1],
+ horizontal: true,
+ length: 1
+ },
+ {
+ startPos: [1, 3],
+ horizontal: true,
+ length: 1
+ }
+ ];
+
+ TestUtils.wallListEquals(expected, result);
+ }
+ );
+
+ it("should generate walls around two independent rooms with one tile each", () => {
+ let room1 = {
+ id: -1,
+ datacenterId: -1,
+ name: "testroom",
+ roomType: "none",
+ tiles: [
+ {
+ id: -1,
+ roomId: -1,
+ position: {x: 1, y: 1}
+ }
+ ]
+ };
+
+ let room2 = {
+ id: -1,
+ datacenterId: -1,
+ name: "testroom",
+ roomType: "none",
+ tiles: [{
+ id: -1,
+ roomId: -1,
+ position: {x: 1, y: 3}
+ }
+ ]
+ };
+
+ let result = Util.deriveWallLocations([
+ room1, room2
+ ]);
+ let expected: IRoomWall[] = [
+ {
+ startPos: [1, 1],
+ horizontal: false,
+ length: 1
+ },
+ {
+ startPos: [1, 3],
+ horizontal: false,
+ length: 1
+ },
+ {
+ startPos: [2, 1],
+ horizontal: false,
+ length: 1
+ },
+ {
+ startPos: [2, 3],
+ horizontal: false,
+ length: 1
+ },
+ {
+ startPos: [1, 1],
+ horizontal: true,
+ length: 1
+ },
+ {
+ startPos: [1, 2],
+ horizontal: true,
+ length: 1
+ },
+ {
+ startPos: [1, 3],
+ horizontal: true,
+ length: 1
+ },
+ {
+ startPos: [1, 4],
+ horizontal: true,
+ length: 1
+ }
+ ];
+
+ TestUtils.wallListEquals(expected, result);
+ }
+ );
+});
+
+describe("Deriving valid next tile positions", () => {
+ it("should derive correctly 4 valid tile positions around 1 selected tile with no other rooms", () => {
+ let result = Util.deriveValidNextTilePositions([], [{
+ id: -1,
+ roomId: -1,
+ position: {x: 1, y: 1}
+ }]);
+ let expected = [
+ {x: 1, y: 0}, {x: 2, y: 1}, {x: 1, y: 2}, {x: 0, y: 1}
+ ];
+
+ TestUtils.positionListEquals(expected, result);
+ });
+
+ it("should derive correctly 6 valid tile positions around 2 selected tiles with no other rooms", () => {
+ let result = Util.deriveValidNextTilePositions([], [{
+ id: -1,
+ roomId: -1,
+ position: {x: 1, y: 1}
+ }, {
+ id: -1,
+ roomId: -1,
+ position: {x: 2, y: 1}
+ }]);
+ let expected = [
+ {x: 1, y: 0}, {x: 2, y: 0}, {x: 3, y: 1}, {x: 1, y: 2}, {x: 2, y: 2}, {x: 0, y: 1}
+ ];
+
+ TestUtils.positionListEquals(expected, result);
+ });
+
+ it("should derive correctly 3 valid tile positions around 1 selected tiles with 1 adjacent room", () => {
+ let room = {
+ id: -1,
+ datacenterId: -1,
+ name: "testroom",
+ roomType: "none",
+ tiles: [{
+ id: -1,
+ roomId: -1,
+ position: {x: 0, y: 1}
+ }]
+ };
+ let result = Util.deriveValidNextTilePositions([room], [{
+ id: -1,
+ roomId: -1,
+ position: {x: 1, y: 1}
+ }]);
+ let expected = [
+ {x: 1, y: 0}, {x: 2, y: 1}, {x: 1, y: 2}
+ ];
+
+ TestUtils.positionListEquals(expected, result);
+ });
+});
+
+describe("Calculating the bounds and average point of a list of rooms", () => {
+ it("should calculate correctly the bounds of a 1-tile room", () => {
+ let room = {
+ id: -1,
+ datacenterId: -1,
+ name: "testroom",
+ roomType: "none",
+ tiles: [{
+ id: -1,
+ roomId: -1,
+ position: {x: 1, y: 1}
+ }]
+ };
+ let result = Util.calculateRoomListBounds([room]);
+ let expected = {
+ min: [1, 1],
+ center: [1.5, 1.5],
+ max: [2, 2]
+ };
+
+ TestUtils.boundsEquals(result, expected);
+ });
+});
diff --git a/src/scripts/user.ts b/src/scripts/user.ts
new file mode 100644
index 00000000..dda2dcab
--- /dev/null
+++ b/src/scripts/user.ts
@@ -0,0 +1,76 @@
+///<reference path="../../typings/index.d.ts" />
+import * as $ from "jquery";
+
+
+const LOCAL_MODE = (document.location.hostname === "localhost");
+
+// Redirect the user to the splash page, if not signed in
+if (!LOCAL_MODE && localStorage.getItem("googleToken") === null) {
+ window.location.replace("/");
+}
+
+// Fill session storage with mock data during LOCAL_MODE
+if (LOCAL_MODE) {
+ localStorage.setItem("googleToken", "");
+ localStorage.setItem("googleTokenExpiration", "2000000000");
+ localStorage.setItem("googleName", "John Doe");
+ localStorage.setItem("googleEmail", "john@doe.com");
+ localStorage.setItem("userId", "2");
+ localStorage.setItem("simulationId", "1");
+ localStorage.setItem("simulationAuthLevel", "OWN");
+}
+
+// Set the username in the navbar
+$("nav .user .username").text(localStorage.getItem("googleName"));
+
+
+// Set the language of the GAuth button to be English
+window["___gcfg"] = {
+ lang: 'en'
+};
+
+/**
+ * Google signin button
+ */
+window["gapiSigninButton"] = () => {
+ gapi.signin2.render('google-signin', {
+ 'scope': 'profile email',
+ 'onsuccess': (googleUser) => {
+ let auth2 = gapi.auth2.getAuthInstance();
+
+ // Handle signout click
+ $("nav .user .sign-out").click(() => {
+ removeUserInfo();
+ auth2.signOut().then(() => {
+ window.location.href = "/";
+ });
+ });
+
+ // Check if the token has expired
+ let currentTime = (new Date()).getTime() / 1000;
+
+ if (parseInt(localStorage.getItem("googleTokenExpiration")) - currentTime <= 0) {
+ auth2.signIn().then(() => {
+ localStorage.setItem("googleToken", googleUser.getAuthResponse().id_token);
+ let expirationTime = (new Date()).getTime() / 1000 + parseInt(googleUser.getAuthResponse().expires_in) - 5;
+ localStorage.setItem("googleTokenExpiration", expirationTime.toString());
+ });
+ }
+ },
+ 'onfailure': () => {
+ window.location.href = "/";
+ console.log("Oops, something went wrong with your Google signin... Try again?")
+ }
+ });
+};
+
+
+export function removeUserInfo() {
+ // Remove session storage items
+ localStorage.removeItem("googleToken");
+ localStorage.removeItem("googleTokenExpiration");
+ localStorage.removeItem("googleName");
+ localStorage.removeItem("googleEmail");
+ localStorage.removeItem("userId");
+ localStorage.removeItem("simulationId");
+} \ No newline at end of file
diff --git a/src/scripts/util.ts b/src/scripts/util.ts
new file mode 100644
index 00000000..74bdb710
--- /dev/null
+++ b/src/scripts/util.ts
@@ -0,0 +1,600 @@
+///<reference path="definitions.ts" />
+import {Colors} from "./colors";
+
+
+export enum IntensityLevel {
+ LOW,
+ MID_LOW,
+ MID_HIGH,
+ HIGH
+}
+
+
+export class Util {
+ private static authorizationLevels = [
+ "OWN", "EDIT", "VIEW"
+ ];
+
+
+ /**
+ * Derives the wall locations given a list of rooms.
+ *
+ * Does so by computing an outline around all tiles in the rooms.
+ */
+ public static deriveWallLocations(rooms: IRoom[]): IRoomWall[] {
+ let verticalWalls = {};
+ let horizontalWalls = {};
+ let doInsert;
+ rooms.forEach((room: IRoom) => {
+ room.tiles.forEach((tile: ITile) => {
+ let x = tile.position.x, y = tile.position.y;
+ for (let dX = -1; dX <= 1; dX++) {
+ for (let dY = -1; dY <= 1; dY++) {
+ if (Math.abs(dX) === Math.abs(dY)) {
+ continue;
+ }
+
+ doInsert = true;
+ room.tiles.forEach((otherTile: ITile) => {
+ if (otherTile.position.x === x + dX && otherTile.position.y === y + dY) {
+ doInsert = false;
+ }
+ });
+
+ if (doInsert) {
+ if (dX === -1) {
+ if (verticalWalls[x] === undefined) {
+ verticalWalls[x] = [];
+ }
+ if (verticalWalls[x].indexOf(y) === -1) {
+ verticalWalls[x].push(y);
+ }
+ } else if (dX === 1) {
+ if (verticalWalls[x + 1] === undefined) {
+ verticalWalls[x + 1] = [];
+ }
+ if (verticalWalls[x + 1].indexOf(y) === -1) {
+ verticalWalls[x + 1].push(y);
+ }
+ } else if (dY === -1) {
+ if (horizontalWalls[y] === undefined) {
+ horizontalWalls[y] = [];
+ }
+ if (horizontalWalls[y].indexOf(x) === -1) {
+ horizontalWalls[y].push(x);
+ }
+ } else if (dY === 1) {
+ if (horizontalWalls[y + 1] === undefined) {
+ horizontalWalls[y + 1] = [];
+ }
+ if (horizontalWalls[y + 1].indexOf(x) === -1) {
+ horizontalWalls[y + 1].push(x);
+ }
+ }
+ }
+ }
+ }
+ });
+ });
+
+ let result: IRoomWall[] = [];
+ let walls = [verticalWalls, horizontalWalls];
+ for (let i = 0; i < 2; i++) {
+ let wallList = walls[i];
+ for (let a in wallList) {
+ if (!wallList.hasOwnProperty(a)) {
+ return;
+ }
+
+ wallList[a].sort((a: number, b: number) => {
+ return a - b;
+ });
+
+ let startPos = wallList[a][0];
+ let positionArray = (i === 1 ? <number[]>[startPos, parseInt(a)] : <number[]>[parseInt(a), startPos]);
+
+ if (wallList[a].length === 1) {
+ result.push({
+ startPos: positionArray,
+ horizontal: i === 1,
+ length: 1
+ });
+ } else {
+ let consecutiveCount = 1;
+ for (let b = 0; b < wallList[a].length - 1; b++) {
+ if (b + 1 === wallList[a].length - 1) {
+ if (wallList[a][b + 1] - wallList[a][b] > 1) {
+ result.push({
+ startPos: (i === 1 ? <number[]>[startPos, parseInt(a)] : <number[]>[parseInt(a), startPos]),
+ horizontal: i === 1,
+ length: consecutiveCount
+ });
+ consecutiveCount = 0;
+ startPos = wallList[a][b + 1];
+ }
+ result.push({
+ startPos: (i === 1 ? <number[]>[startPos, parseInt(a)] : <number[]>[parseInt(a), startPos]),
+ horizontal: i === 1,
+ length: consecutiveCount + 1
+ });
+ break;
+ } else if (wallList[a][b + 1] - wallList[a][b] > 1) {
+ result.push({
+ startPos: (i === 1 ? <number[]>[startPos, parseInt(a)] : <number[]>[parseInt(a), startPos]),
+ horizontal: i === 1,
+ length: consecutiveCount
+ });
+ startPos = wallList[a][b + 1];
+ consecutiveCount = 0;
+ }
+ consecutiveCount++;
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Generates a list of all valid tile positions around the currently selected room under construction.
+ *
+ * @param rooms The rooms that already exist in the model
+ * @param selectedTiles The tiles that the user has already selected to form a new room
+ * @returns {Array} A 2D list of tile positions that are valid next tile choices.
+ */
+ public static deriveValidNextTilePositions(rooms: IRoom[], selectedTiles: ITile[]): IGridPosition[] {
+ let result = [], newPosition = {x: 0, y: 0};
+ let isSurroundingTile;
+
+ selectedTiles.forEach((tile: ITile) => {
+ let x = tile.position.x, y = tile.position.y;
+ for (let dX = -1; dX <= 1; dX++) {
+ for (let dY = -1; dY <= 1; dY++) {
+ if (Math.abs(dX) === Math.abs(dY)) {
+ continue;
+ }
+
+ newPosition.x = x + dX;
+ newPosition.y = y + dY;
+
+ isSurroundingTile = true;
+ selectedTiles.forEach((otherTile: ITile) => {
+ if (otherTile.position.x === newPosition.x && otherTile.position.y === newPosition.y) {
+ isSurroundingTile = false;
+ }
+ });
+
+ if (isSurroundingTile && !Util.checkRoomCollision(rooms, newPosition)) {
+ result.push({x: newPosition.x, y: newPosition.y});
+ }
+ }
+ }
+ });
+
+ return result;
+ }
+
+ /**
+ * Determines whether a position is contained in a list of tiles.
+ *
+ * @param list A list of tiles
+ * @param position A position
+ * @returns {boolean} Whether the list contains the position
+ */
+ public static tileListContainsPosition(list: ITile[], position: IGridPosition): boolean {
+ return Util.tileListPositionIndexOf(list, position) !== -1;
+ }
+
+ /**
+ * Determines the index of a position in a list of tiles.
+ *
+ * @param list A list of tiles
+ * @param position A position
+ * @returns {number} Index of the position in the list of tiles, -1 if not found
+ */
+ public static tileListPositionIndexOf(list: ITile[], position: IGridPosition): number {
+ let index = -1;
+ let element;
+
+ for (let i = 0; i < list.length; i++) {
+ element = list[i];
+ if (position.x === element.position.x && position.y === element.position.y) {
+ index = i;
+ break;
+ }
+ }
+
+ return index;
+ }
+
+ /**
+ * Determines whether a position is contained in a list of positions.
+ *
+ * @param list A list of positions
+ * @param position A position
+ * @returns {boolean} Whether the list contains the position
+ */
+ public static positionListContainsPosition(list: IGridPosition[], position: IGridPosition): boolean {
+ return Util.positionListPositionIndexOf(list, position) !== -1;
+ }
+
+ /**
+ * Determines the index of a position in a list of positions.
+ *
+ * @param list A list of positions
+ * @param position A position
+ * @returns {number} Index of the position in the list of tiles, -1 if not found
+ */
+ public static positionListPositionIndexOf(list: IGridPosition[], position: IGridPosition): number {
+ let index = -1;
+ let element;
+
+ for (let i = 0; i < list.length; i++) {
+ element = list[i];
+ if (position.x === element.x && position.y === element.y) {
+ index = i;
+ break;
+ }
+ }
+
+ return index;
+ }
+
+ /**
+ * Determines the index of a room that is colliding with a given grid tile.
+ *
+ * Returns -1 if no collision is found.
+ *
+ * @param rooms An array of Room objects that should be checked for collisions
+ * @param position A position
+ * @returns {number} The index of the room in the rooms list if found, else -1
+ */
+ public static roomCollisionIndexOf(rooms: IRoom[], position: IGridPosition): number {
+ let index = -1;
+ let room;
+
+ for (let i = 0; i < rooms.length; i++) {
+ room = rooms[i];
+ if (Util.tileListContainsPosition(room.tiles, position)) {
+ index = i;
+ break;
+ }
+ }
+
+ return index;
+ }
+
+ /**
+ * Checks whether a tile location collides with an existing room.
+ *
+ * @param rooms A list of rooms to be analyzed
+ * @param position A position
+ * @returns {boolean} Whether the tile lies in an existing room
+ */
+ public static checkRoomCollision(rooms: IRoom[], position: IGridPosition): boolean {
+ return Util.roomCollisionIndexOf(rooms, position) !== -1;
+ }
+
+ /**
+ * Calculates the minimum, center, and maximum of a list of rooms in stage coordinates.
+ *
+ * This center is calculated by averaging the most outlying tiles of all rooms.
+ *
+ * @param rooms The rooms to be analyzed
+ * @returns {IBounds} The coordinates of the minimum, center, and maximum
+ */
+ public static calculateRoomListBounds(rooms: IRoom[]): IBounds {
+ let min = [Number.MAX_VALUE, Number.MAX_VALUE];
+ let max = [-1, -1];
+
+ rooms.forEach((room: IRoom) => {
+ room.tiles.forEach((tile: ITile) => {
+ if (tile.position.x < min[0]) {
+ min[0] = tile.position.x;
+ }
+ if (tile.position.y < min[1]) {
+ min[1] = tile.position.y;
+ }
+
+ if (tile.position.x > max[0]) {
+ max[0] = tile.position.x;
+ }
+ if (tile.position.y > max[1]) {
+ max[1] = tile.position.y;
+ }
+ });
+ });
+
+ max[0]++;
+ max[1]++;
+
+ let gridCenter = [min[0] + (max[0] - min[0]) / 2.0, min[1] + (max[1] - min[1]) / 2.0];
+
+ return {
+ min: min,
+ center: gridCenter,
+ max: max
+ };
+ }
+
+ /**
+ * Does the same as 'calculateRoomListBounds', only for one room.
+ *
+ * @param room The room to be analyzed
+ * @returns {IBounds} The coordinates of the minimum, center, and maximum
+ */
+ public static calculateRoomBounds(room: IRoom): IBounds {
+ return Util.calculateRoomListBounds([room]);
+ }
+
+ public static calculateRoomNamePosition(room: IRoom): IRoomNamePos {
+ let result: IRoomNamePos = {
+ topLeft: {x: 0, y: 0},
+ length: 0
+ };
+
+ // Look for the top-most tile y-coordinate
+ let topMin = Number.MAX_VALUE;
+ room.tiles.forEach((tile: ITile) => {
+ if (tile.position.y < topMin) {
+ topMin = tile.position.y;
+ }
+ });
+
+ // If there is no tile at the top, meaning that the room has no tiles, exit
+ if (topMin === Number.MAX_VALUE) {
+ return null;
+ }
+
+ // Find the left-most tile at the top and the length of its adjacent tiles to the right
+ let topTilePositions: number[] = [];
+ room.tiles.forEach((tile: ITile) => {
+ if (tile.position.y === topMin) {
+ topTilePositions.push(tile.position.x);
+ }
+ });
+ topTilePositions.sort();
+ let leftMin = topTilePositions[0];
+ let length = 0;
+
+ while (length < topTilePositions.length && topTilePositions[length] - leftMin === length) {
+ length++;
+ }
+
+ result.topLeft.x = leftMin;
+ result.topLeft.y = topMin;
+ result.length = length;
+
+ return result;
+ }
+
+ /**
+ * Analyzes an array of objects and calculates its fill ratio, by looking at the number of elements != null and
+ * comparing that number to the array length.
+ *
+ * @param inputList The list to be analyzed
+ * @returns {number} A fill ratio (between 0 and 1), representing the relative amount of objects != null in the list
+ */
+ public static getFillRatio(inputList: any[]): number {
+ let numNulls = 0;
+
+ if (inputList.length === 0) {
+ return 0;
+ }
+
+ inputList.forEach((element: any) => {
+ if (element == null) {
+ numNulls++;
+ }
+ });
+
+ return (inputList.length - numNulls) / inputList.length;
+ }
+
+ /**
+ * Calculates the energy consumption ration of the given rack.
+ *
+ * @param rack The rack of which the power consumption should be analyzed
+ * @returns {number} The energy consumption ratio
+ */
+ public static getEnergyRatio(rack: IRack): number {
+ let energySum = 0;
+
+ rack.machines.forEach((machine: IMachine) => {
+ if (machine === null) {
+ return;
+ }
+
+ let machineConsumption = 0;
+
+ let nodeUnitList: INodeUnit[] = <INodeUnit[]>machine.cpus.concat(machine.gpus);
+ nodeUnitList = nodeUnitList.concat(<INodeUnit[]>machine.memories);
+ nodeUnitList = nodeUnitList.concat(<INodeUnit[]>machine.storages);
+ nodeUnitList.forEach((unit: INodeUnit) => {
+ machineConsumption += unit.energyConsumptionW;
+ });
+
+ energySum += machineConsumption;
+ });
+
+ return energySum / rack.powerCapacityW;
+ }
+
+ /**
+ * Parses date-time expresses of the form YYYY-MM-DDTHH:MM:SS and returns a parsed object.
+ *
+ * @param input A string expressing a date and a time, in the above mentioned format
+ * @returns {IDateTime} A DateTime object with the parsed date and time information as content
+ */
+ public static parseDateTime(input: string): IDateTime {
+ let output: IDateTime = {
+ year: 0,
+ month: 0,
+ day: 0,
+ hour: 0,
+ minute: 0,
+ second: 0
+ };
+
+ let dateAndTime = input.split("T");
+ let dateComponents = dateAndTime[0].split("-");
+ output.year = parseInt(dateComponents[0], 10);
+ output.month = parseInt(dateComponents[1], 10);
+ output.day = parseInt(dateComponents[2], 10);
+
+ let timeComponents = dateAndTime[1].split(":");
+ output.hour = parseInt(timeComponents[0], 10);
+ output.minute = parseInt(timeComponents[1], 10);
+ output.second = parseInt(timeComponents[2], 10);
+
+ return output;
+ }
+
+ public static formatDateTime(input: IDateTime) {
+ let date;
+ let currentDate = new Date();
+
+ date = Util.addPaddingToTwo(input.day) + "/" +
+ Util.addPaddingToTwo(input.month) + "/" +
+ Util.addPaddingToTwo(input.year);
+
+ if (input.year === currentDate.getFullYear() &&
+ input.month === currentDate.getMonth() + 1) {
+ if (input.day === currentDate.getDate()) {
+ date = "Today";
+ } else if (input.day === currentDate.getDate() - 1) {
+ date = "Yesterday";
+ }
+ }
+
+ return date + ", " +
+ Util.addPaddingToTwo(input.hour) + ":" +
+ Util.addPaddingToTwo(input.minute);
+ }
+
+ public static getCurrentDateTime(): string {
+ let date = new Date();
+ return date.getFullYear() + "-" + Util.addPaddingToTwo(date.getMonth() + 1) + "-" +
+ Util.addPaddingToTwo(date.getDate()) + "T" + Util.addPaddingToTwo(date.getHours()) + ":" +
+ Util.addPaddingToTwo(date.getMinutes()) + ":" + Util.addPaddingToTwo(date.getSeconds());
+ }
+
+ /**
+ * Removes all populated object properties from a given object, and returns a copy without them.
+ *
+ * An exception of such an object property is made in the case of a position object (of type GridPosition), which
+ * is copied over as well.
+ *
+ * Does not manipulate the original object in any way, except if your object has quantum-like properties, which
+ * change upon inspection. In such a case, I'm afraid that this method can do little for you.
+ *
+ * @param object The input object
+ * @returns {any} A copy of the object without any populated properties (of type object).
+ */
+ public static packageForSending(object: any) {
+ let result: any = {};
+ for (let prop in object) {
+ if (object.hasOwnProperty(prop)) {
+ if (typeof object[prop] !== "object") {
+ result[prop] = object[prop];
+ } else {
+ if (object[prop] instanceof Array) {
+ if (object[prop].length === 0 || !(object[prop][0] instanceof Object)) {
+ result[prop] = [];
+ for (let i = 0; i < object[prop].length; i++) {
+ result[prop][i] = object[prop][i];
+ }
+ }
+ }
+ if (object[prop] != null && object[prop].hasOwnProperty("x") && object[prop].hasOwnProperty("y")) {
+ result["positionX"] = object[prop].x;
+ result["positionY"] = object[prop].y;
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ public static addPaddingToTwo(integer: number): string {
+ if (integer < 10) {
+ return "0" + integer.toString();
+ } else {
+ return integer.toString();
+ }
+ }
+
+ public static convertSecondsToFormattedTime(seconds: number): string {
+ let hour = Math.floor(seconds / 3600);
+ let minute = Math.floor(seconds / 60) % 60;
+ let second = seconds % 60;
+ return this.addPaddingToTwo(hour) + ":" +
+ this.addPaddingToTwo(minute) + ":" +
+ this.addPaddingToTwo(second);
+ }
+
+ public static determineLoadIntensityLevel(loadFraction: number): IntensityLevel {
+ if (loadFraction < 0.25) {
+ return IntensityLevel.LOW;
+ } else if (loadFraction < 0.5) {
+ return IntensityLevel.MID_LOW;
+ } else if (loadFraction < 0.75) {
+ return IntensityLevel.MID_HIGH;
+ } else {
+ return IntensityLevel.HIGH;
+ }
+ }
+
+ public static convertIntensityToColor(intensityLevel: IntensityLevel): string {
+ if (intensityLevel === IntensityLevel.LOW) {
+ return Colors.SIM_LOW;
+ } else if (intensityLevel === IntensityLevel.MID_LOW) {
+ return Colors.SIM_MID_LOW;
+ } else if (intensityLevel === IntensityLevel.MID_HIGH) {
+ return Colors.SIM_MID_HIGH;
+ } else if (intensityLevel === IntensityLevel.HIGH) {
+ return Colors.SIM_HIGH;
+ }
+ }
+
+ /**
+ * Gives the sentence-cased alternative for a given string.
+ *
+ * @example Input: TEST, Output: Test
+ *
+ * @param input The input string
+ * @returns {any} The sentence-cased string
+ */
+ public static toSentenceCase(input: string): string {
+ if (input === undefined || input === null) {
+ return undefined;
+ }
+ if (input.length === 0) {
+ return "";
+ }
+
+ return input[0].toUpperCase() + input.substr(1).toLowerCase();
+ }
+
+ /**
+ * Sort a list of authorizations based on the levels of authorizations.
+ *
+ * @param list The list to be sorted (in-place)
+ */
+ public static sortAuthorizations(list: IAuthorization[]): void {
+ list.sort((a: IAuthorization, b: IAuthorization): number => {
+ return this.authorizationLevels.indexOf(a.authorizationLevel) -
+ this.authorizationLevels.indexOf(b.authorizationLevel);
+ });
+ }
+
+ /**
+ * Returns an array containing all numbers of a range from 0 to x (including x).
+ */
+ public static range(x: number): number[] {
+ return Array.apply(null, Array(x + 1)).map((_, i) => {
+ return i.toString();
+ })
+ }
+}
diff --git a/src/scripts/views/layers/dcobject.ts b/src/scripts/views/layers/dcobject.ts
new file mode 100644
index 00000000..6cec1f7e
--- /dev/null
+++ b/src/scripts/views/layers/dcobject.ts
@@ -0,0 +1,252 @@
+import {Colors} from "../../colors";
+import {Util, IntensityLevel} from "../../util";
+import {MapView} from "../mapview";
+import {DCProgressBar} from "./dcprogressbar";
+import {Layer} from "./layer";
+import {CELL_SIZE} from "../../controllers/mapcontroller";
+
+
+export class DCObjectLayer implements Layer {
+ public static ITEM_MARGIN = CELL_SIZE / 7.0;
+ public static ITEM_PADDING = CELL_SIZE / 10.0;
+ public static STROKE_WIDTH = CELL_SIZE / 20.0;
+ public static PROGRESS_BAR_DISTANCE = CELL_SIZE / 17.0;
+ public static CONTENT_SIZE = CELL_SIZE - DCObjectLayer.ITEM_MARGIN * 2 - DCObjectLayer.ITEM_PADDING * 3;
+
+ public container: createjs.Container;
+ public detailedMode: boolean;
+ public coloringMode: boolean;
+ public intensityLevels: { [key: number]: IntensityLevel; } = {};
+
+ private mapView: MapView;
+ private preload: createjs.LoadQueue;
+ private rackSpaceBitmap: createjs.Bitmap;
+ private rackEnergyBitmap: createjs.Bitmap;
+ private psuBitmap: createjs.Bitmap;
+ private coolingItemBitmap: createjs.Bitmap;
+
+ // This associative lookup object keeps all DC display objects with as property name the index of the global map
+ // array that they are located in.
+ private dcObjectMap: { [key: number]: any; };
+
+
+ public static drawHoverRack(position: IGridPosition): createjs.Container {
+ let result = new createjs.Container();
+
+ DCObjectLayer.drawItemRectangle(
+ position, Colors.RACK_BACKGROUND, Colors.RACK_BORDER, result
+ );
+ DCProgressBar.drawItemProgressRectangle(
+ position, Colors.RACK_SPACE_BAR_BACKGROUND, result, 0, 1
+ );
+ DCProgressBar.drawItemProgressRectangle(
+ position, Colors.RACK_ENERGY_BAR_BACKGROUND, result, 1, 1
+ );
+
+ return result;
+ }
+
+ public static drawHoverPSU(position: IGridPosition): createjs.Container {
+ let result = new createjs.Container();
+
+ DCObjectLayer.drawItemRectangle(
+ position, Colors.PSU_BACKGROUND, Colors.PSU_BORDER, result
+ );
+
+ return result;
+ }
+
+ public static drawHoverCoolingItem(position: IGridPosition): createjs.Container {
+ let result = new createjs.Container();
+
+ DCObjectLayer.drawItemRectangle(
+ position, Colors.COOLING_ITEM_BACKGROUND, Colors.COOLING_ITEM_BORDER, result
+ );
+
+ return result;
+ }
+
+ /**
+ * Draws an object rectangle in a given grid cell, with margin around its border.
+ *
+ * @param position The coordinates of the grid cell in which it should be located
+ * @param color The background color of the item
+ * @param borderColor The border color
+ * @param container The container to which it should be drawn
+ * @returns {createjs.Shape} The drawn shape
+ */
+ private static drawItemRectangle(position: IGridPosition, color: string, borderColor: string,
+ container: createjs.Container): createjs.Shape {
+ let shape = new createjs.Shape();
+ shape.graphics.beginStroke(borderColor);
+ shape.graphics.setStrokeStyle(DCObjectLayer.STROKE_WIDTH);
+ shape.graphics.beginFill(color);
+ shape.graphics.drawRect(
+ position.x * CELL_SIZE + DCObjectLayer.ITEM_MARGIN,
+ position.y * CELL_SIZE + DCObjectLayer.ITEM_MARGIN,
+ CELL_SIZE - DCObjectLayer.ITEM_MARGIN * 2,
+ CELL_SIZE - DCObjectLayer.ITEM_MARGIN * 2
+ );
+ container.addChild(shape);
+ return shape;
+ }
+
+ /**
+ * Draws an bitmap in item format.
+ *
+ * @param position The coordinates of the grid cell in which it should be located
+ * @param container The container to which it should be drawn
+ * @param originBitmap The bitmap that should be drawn
+ * @returns {createjs.Bitmap} The drawn bitmap
+ */
+ private static drawItemIcon(position: IGridPosition, container: createjs.Container,
+ originBitmap: createjs.Bitmap): createjs.Bitmap {
+ let bitmap = originBitmap.clone();
+ container.addChild(bitmap);
+ bitmap.x = position.x * CELL_SIZE + DCObjectLayer.ITEM_MARGIN + DCObjectLayer.ITEM_PADDING * 1.5;
+ bitmap.y = position.y * CELL_SIZE + DCObjectLayer.ITEM_MARGIN + DCObjectLayer.ITEM_PADDING * 1.5;
+ return bitmap;
+ }
+
+ constructor(mapView: MapView) {
+ this.mapView = mapView;
+ this.container = new createjs.Container();
+
+ this.detailedMode = true;
+ this.coloringMode = false;
+
+ this.preload = new createjs.LoadQueue();
+ this.preload.addEventListener("complete", () => {
+ this.rackSpaceBitmap = new createjs.Bitmap(<HTMLImageElement>this.preload.getResult("rack-space"));
+ this.rackEnergyBitmap = new createjs.Bitmap(<HTMLImageElement>this.preload.getResult("rack-energy"));
+ this.psuBitmap = new createjs.Bitmap(<HTMLImageElement>this.preload.getResult("psu"));
+ this.coolingItemBitmap = new createjs.Bitmap(<HTMLImageElement>this.preload.getResult("coolingitem"));
+
+ // Scale the images
+ this.rackSpaceBitmap.scaleX = DCProgressBar.PROGRESS_BAR_WIDTH / this.rackSpaceBitmap.image.width;
+ this.rackSpaceBitmap.scaleY = DCProgressBar.PROGRESS_BAR_WIDTH / this.rackSpaceBitmap.image.height;
+
+ this.rackEnergyBitmap.scaleX = DCProgressBar.PROGRESS_BAR_WIDTH / this.rackEnergyBitmap.image.width;
+ this.rackEnergyBitmap.scaleY = DCProgressBar.PROGRESS_BAR_WIDTH / this.rackEnergyBitmap.image.height;
+
+ this.psuBitmap.scaleX = DCObjectLayer.CONTENT_SIZE / this.psuBitmap.image.width;
+ this.psuBitmap.scaleY = DCObjectLayer.CONTENT_SIZE / this.psuBitmap.image.height;
+
+ this.coolingItemBitmap.scaleX = DCObjectLayer.CONTENT_SIZE / this.coolingItemBitmap.image.width;
+ this.coolingItemBitmap.scaleY = DCObjectLayer.CONTENT_SIZE / this.coolingItemBitmap.image.height;
+
+
+ this.populateObjectList();
+ this.draw();
+
+ this.mapView.updateScene = true;
+ });
+
+ this.preload.loadFile({id: "rack-space", src: 'img/app/rack-space.png'});
+ this.preload.loadFile({id: "rack-energy", src: 'img/app/rack-energy.png'});
+ this.preload.loadFile({id: "psu", src: 'img/app/psu.png'});
+ this.preload.loadFile({id: "coolingitem", src: 'img/app/coolingitem.png'});
+ }
+
+ /**
+ * Generates a list of DC objects with their associated display objects.
+ */
+ public populateObjectList(): void {
+ this.dcObjectMap = {};
+
+ this.mapView.currentDatacenter.rooms.forEach((room: IRoom) => {
+ room.tiles.forEach((tile: ITile) => {
+ if (tile.object !== undefined) {
+ let index = tile.position.y * MapView.MAP_SIZE + tile.position.x;
+
+ switch (tile.objectType) {
+ case "RACK":
+ this.dcObjectMap[index] = {
+ spaceBar: new DCProgressBar(this.container,
+ Colors.RACK_SPACE_BAR_BACKGROUND, Colors.RACK_SPACE_BAR_FILL,
+ this.rackSpaceBitmap, tile.position, 0,
+ Util.getFillRatio((<IRack>tile.object).machines)),
+ energyBar: new DCProgressBar(this.container,
+ Colors.RACK_ENERGY_BAR_BACKGROUND, Colors.RACK_ENERGY_BAR_FILL,
+ this.rackEnergyBitmap, tile.position, 1,
+ Util.getFillRatio((<IRack>tile.object).machines)),
+ itemRect: createjs.Shape,
+ tile: tile, model: tile.object, position: tile.position, type: tile.objectType
+ };
+
+ break;
+ case "COOLING_ITEM":
+ this.dcObjectMap[index] = {
+ itemRect: createjs.Shape, batteryIcon: createjs.Bitmap,
+ tile: tile, model: tile.object, position: tile.position, type: tile.objectType
+ };
+ break;
+ case "PSU":
+ this.dcObjectMap[index] = {
+ itemRect: createjs.Shape, freezeIcon: createjs.Bitmap,
+ tile: tile, model: tile.object, position: tile.position, type: tile.objectType
+ };
+ break;
+ }
+ }
+ });
+ });
+ }
+
+ public draw(): void {
+ let currentObject;
+
+ this.container.removeAllChildren();
+
+ this.container.cursor = "pointer";
+
+ for (let property in this.dcObjectMap) {
+ if (this.dcObjectMap.hasOwnProperty(property)) {
+ currentObject = this.dcObjectMap[property];
+
+ switch (currentObject.type) {
+ case "RACK":
+ let color = Colors.RACK_BACKGROUND;
+
+ if (this.coloringMode && currentObject.tile.roomId ===
+ this.mapView.mapController.roomModeController.currentRoom.id) {
+ color = Util.convertIntensityToColor(this.intensityLevels[currentObject.model.id]);
+ }
+
+ currentObject.itemRect = DCObjectLayer.drawItemRectangle(
+ currentObject.position, color, Colors.RACK_BORDER, this.container
+ );
+
+ if (this.detailedMode) {
+ currentObject.spaceBar.fillRatio = Util.getFillRatio(currentObject.model.machines);
+ currentObject.energyBar.fillRatio = Util.getEnergyRatio(currentObject.model);
+
+ currentObject.spaceBar.draw();
+ currentObject.energyBar.draw();
+ }
+ break;
+ case "COOLING_ITEM":
+ currentObject.itemRect = DCObjectLayer.drawItemRectangle(
+ currentObject.position, Colors.COOLING_ITEM_BACKGROUND, Colors.COOLING_ITEM_BORDER,
+ this.container
+ );
+
+ currentObject.freezeIcon = DCObjectLayer.drawItemIcon(currentObject.position, this.container,
+ this.coolingItemBitmap);
+ break;
+ case "PSU":
+ currentObject.itemRect = DCObjectLayer.drawItemRectangle(
+ currentObject.position, Colors.PSU_BACKGROUND, Colors.PSU_BORDER,
+ this.container
+ );
+
+ currentObject.batteryIcon = DCObjectLayer.drawItemIcon(currentObject.position, this.container,
+ this.psuBitmap);
+ break;
+ }
+ }
+ }
+
+ this.mapView.updateScene = true;
+ }
+} \ No newline at end of file
diff --git a/src/scripts/views/layers/dcprogressbar.ts b/src/scripts/views/layers/dcprogressbar.ts
new file mode 100644
index 00000000..d0ec4397
--- /dev/null
+++ b/src/scripts/views/layers/dcprogressbar.ts
@@ -0,0 +1,99 @@
+import {DCObjectLayer} from "./dcobject";
+import {CELL_SIZE} from "../../controllers/mapcontroller";
+
+
+export class DCProgressBar {
+ public static PROGRESS_BAR_WIDTH = CELL_SIZE / 7.0;
+
+ public container: createjs.Container;
+ public fillRatio: number;
+
+ private backgroundRect: createjs.Shape;
+ private backgroundColor: string;
+ private fillRect: createjs.Shape;
+ private fillColor: string;
+ private bitmap: createjs.Bitmap;
+ private position: IGridPosition;
+ private distanceFromBottom: number;
+
+
+ /**
+ * Draws a progress rectangle with rounded ends.
+ *
+ * @param position The coordinates of the grid cell in which it should be located
+ * @param color The background color of the item
+ * @param container The container to which it should be drawn
+ * @param distanceFromBottom The index of its vertical position, counted from the bottom (0 is the lowest position)
+ * @param fractionFilled The fraction of the available horizontal space that the progress bar should take up
+ * @returns {createjs.Shape} The drawn shape
+ */
+ public static drawItemProgressRectangle(position: IGridPosition, color: string,
+ container: createjs.Container, distanceFromBottom: number,
+ fractionFilled: number): createjs.Shape {
+ let shape = new createjs.Shape();
+ shape.graphics.beginFill(color);
+ let x = position.x * CELL_SIZE + DCObjectLayer.ITEM_MARGIN + DCObjectLayer.ITEM_PADDING;
+ let y = (position.y + 1) * CELL_SIZE - DCObjectLayer.ITEM_MARGIN - DCObjectLayer.ITEM_PADDING -
+ DCProgressBar.PROGRESS_BAR_WIDTH - distanceFromBottom *
+ (DCProgressBar.PROGRESS_BAR_WIDTH + DCObjectLayer.PROGRESS_BAR_DISTANCE);
+ let width = (CELL_SIZE - (DCObjectLayer.ITEM_MARGIN + DCObjectLayer.ITEM_PADDING) * 2) * fractionFilled;
+ let height;
+ let radius;
+
+ if (width < DCProgressBar.PROGRESS_BAR_WIDTH) {
+ height = width;
+ radius = width / 2;
+ y += (DCProgressBar.PROGRESS_BAR_WIDTH - height) / 2;
+ } else {
+ height = DCProgressBar.PROGRESS_BAR_WIDTH;
+ radius = DCProgressBar.PROGRESS_BAR_WIDTH / 2;
+ }
+
+ shape.graphics.drawRoundRect(
+ x, y, width, height, radius
+ );
+ container.addChild(shape);
+ return shape;
+ }
+
+ /**
+ * Draws an bitmap in progressbar format.
+ *
+ * @param position The coordinates of the grid cell in which it should be located
+ * @param container The container to which it should be drawn
+ * @param originBitmap The bitmap that should be drawn
+ * @param distanceFromBottom The index of its vertical position, counted from the bottom (0 is the lowest position)
+ * @returns {createjs.Bitmap} The drawn bitmap
+ */
+ public static drawProgressbarIcon(position: IGridPosition, container: createjs.Container, originBitmap: createjs.Bitmap,
+ distanceFromBottom: number): createjs.Bitmap {
+ let bitmap = originBitmap.clone();
+ container.addChild(bitmap);
+ bitmap.x = (position.x + 0.5) * CELL_SIZE - DCProgressBar.PROGRESS_BAR_WIDTH * 0.5;
+ bitmap.y = (position.y + 1) * CELL_SIZE - DCObjectLayer.ITEM_MARGIN - DCObjectLayer.ITEM_PADDING -
+ DCProgressBar.PROGRESS_BAR_WIDTH - distanceFromBottom *
+ (DCProgressBar.PROGRESS_BAR_WIDTH + DCObjectLayer.PROGRESS_BAR_DISTANCE);
+ return bitmap;
+ }
+
+ constructor(container: createjs.Container, backgroundColor: string,
+ fillColor: string, bitmap: createjs.Bitmap, position: IGridPosition,
+ indexFromBottom: number, fillRatio: number) {
+ this.container = container;
+ this.backgroundColor = backgroundColor;
+ this.fillColor = fillColor;
+ this.bitmap = bitmap;
+ this.position = position;
+ this.distanceFromBottom = indexFromBottom;
+ this.fillRatio = fillRatio;
+ }
+
+ public draw() {
+ this.backgroundRect = DCProgressBar.drawItemProgressRectangle(this.position, this.backgroundColor,
+ this.container, this.distanceFromBottom, 1);
+ this.fillRect = DCProgressBar.drawItemProgressRectangle(this.position, this.fillColor, this.container,
+ this.distanceFromBottom, this.fillRatio);
+
+ DCProgressBar.drawProgressbarIcon(this.position, this.container, this.bitmap, this.distanceFromBottom);
+ }
+} \ No newline at end of file
diff --git a/src/scripts/views/layers/gray.ts b/src/scripts/views/layers/gray.ts
new file mode 100644
index 00000000..ed3c9429
--- /dev/null
+++ b/src/scripts/views/layers/gray.ts
@@ -0,0 +1,145 @@
+import {MapView} from "../mapview";
+import {Colors} from "../../colors";
+import {Util} from "../../util";
+import {Layer} from "./layer";
+
+
+/**
+ * Class responsible for graying out non-active UI elements.
+ */
+export class GrayLayer implements Layer {
+ public container: createjs.Container;
+ public currentRoom: IRoom;
+ public currentObjectTile: ITile;
+
+ private mapView: MapView;
+ private grayRoomShape: createjs.Shape;
+
+
+ constructor(mapView: MapView) {
+ this.mapView = mapView;
+ this.container = new createjs.Container();
+ }
+
+ /**
+ * Draws grayed out areas around a currently selected room.
+ *
+ * @param redraw Whether this is a redraw, or an initial draw action
+ */
+ public draw(redraw?: boolean): void {
+ if (this.currentRoom === undefined) {
+ return;
+ }
+
+ this.container.removeAllChildren();
+
+ let roomBounds = Util.calculateRoomBounds(this.currentRoom);
+
+ let shape = new createjs.Shape();
+ shape.graphics.beginFill(Colors.GRAYED_OUT_AREA);
+ shape.cursor = "pointer";
+
+ this.drawLargeRects(shape, roomBounds);
+ this.drawFineGrainedRects(shape, roomBounds);
+
+ this.container.addChild(shape);
+ if (redraw === true) {
+ shape.alpha = 1;
+ } else {
+ shape.alpha = 0;
+ this.mapView.animate(shape, {alpha: 1});
+ }
+
+ if (this.grayRoomShape !== undefined && !this.grayRoomShape.visible) {
+ this.grayRoomShape = undefined;
+ this.drawRackLevel(redraw);
+ }
+
+ this.mapView.updateScene = true;
+ }
+
+ private drawLargeRects(shape: createjs.Shape, roomBounds: IBounds): void {
+ if (roomBounds.min[0] > 0) {
+ MapView.drawRectangleToShape({x: 0, y: 0}, shape, roomBounds.min[0], MapView.MAP_SIZE);
+ }
+ if (roomBounds.min[1] > 0) {
+ MapView.drawRectangleToShape({x: roomBounds.min[0], y: 0}, shape, roomBounds.max[0] - roomBounds.min[0],
+ roomBounds.min[1]);
+ }
+ if (roomBounds.max[0] < MapView.MAP_SIZE - 1) {
+ MapView.drawRectangleToShape({x: roomBounds.max[0], y: 0}, shape, MapView.MAP_SIZE - roomBounds.max[0],
+ MapView.MAP_SIZE);
+ }
+ if (roomBounds.max[1] < MapView.MAP_SIZE - 1) {
+ MapView.drawRectangleToShape({x: roomBounds.min[0], y: roomBounds.max[1]}, shape,
+ roomBounds.max[0] - roomBounds.min[0], MapView.MAP_SIZE - roomBounds.max[1]);
+ }
+ }
+
+ private drawFineGrainedRects(shape: createjs.Shape, roomBounds: IBounds): void {
+ for (let x = roomBounds.min[0]; x < roomBounds.max[0]; x++) {
+ for (let y = roomBounds.min[1]; y < roomBounds.max[1]; y++) {
+ if (!Util.tileListContainsPosition(this.currentRoom.tiles, {x: x, y: y})) {
+ MapView.drawRectangleToShape({x: x, y: y}, shape);
+ }
+ }
+ }
+ }
+
+ public drawRackLevel(redraw?: boolean): void {
+ if (this.currentObjectTile === undefined) {
+ return;
+ }
+
+ this.grayRoomShape = new createjs.Shape();
+ this.grayRoomShape.graphics.beginFill(Colors.GRAYED_OUT_AREA);
+ this.grayRoomShape.cursor = "pointer";
+ this.grayRoomShape.alpha = 0;
+
+ this.currentRoom.tiles.forEach((tile: ITile) => {
+ if (this.currentObjectTile.position.x !== tile.position.x ||
+ this.currentObjectTile.position.y !== tile.position.y) {
+ MapView.drawRectangleToShape({x: tile.position.x, y: tile.position.y}, this.grayRoomShape);
+ }
+ });
+
+ this.container.addChild(this.grayRoomShape);
+ if (redraw === true) {
+ this.grayRoomShape.alpha = 1;
+ } else {
+ this.grayRoomShape.alpha = 0;
+ this.mapView.animate(this.grayRoomShape, {alpha: 1});
+ }
+ }
+
+ public hideRackLevel(): void {
+ if (this.currentObjectTile === undefined) {
+ return;
+ }
+
+ this.mapView.animate(this.grayRoomShape, {
+ alpha: 0, visible: false
+ });
+ }
+
+ /**
+ * Clears the container.
+ */
+ public clear(): void {
+ this.mapView.animate(this.container, {alpha: 0}, () => {
+ this.container.removeAllChildren();
+ this.container.alpha = 1;
+ });
+ this.grayRoomShape = undefined;
+ this.currentRoom = undefined;
+ }
+
+ /**
+ * Checks whether there is already an active room with grayed out areas around it.
+ *
+ * @returns {boolean} Whether the room is grayed out
+ */
+ public isGrayedOut(): boolean {
+ return this.currentRoom !== undefined;
+ }
+} \ No newline at end of file
diff --git a/src/scripts/views/layers/grid.ts b/src/scripts/views/layers/grid.ts
new file mode 100644
index 00000000..9a52b2af
--- /dev/null
+++ b/src/scripts/views/layers/grid.ts
@@ -0,0 +1,59 @@
+import {Layer} from "./layer";
+import {MapView} from "../mapview";
+import {Colors} from "../../colors";
+import {CELL_SIZE} from "../../controllers/mapcontroller";
+
+
+/**
+ * Class responsible for rendering the grid.
+ */
+export class GridLayer implements Layer {
+ public container: createjs.Container;
+ public gridPixelSize: number;
+
+ private mapView: MapView;
+ private gridLineWidth: number;
+
+
+ constructor(mapView: MapView) {
+ this.mapView = mapView;
+ this.container = new createjs.Container();
+
+ this.gridLineWidth = 0.5;
+ this.gridPixelSize = MapView.MAP_SIZE * CELL_SIZE;
+
+ this.draw();
+ }
+
+ /**
+ * Draws the entire grid (later to be navigated around with offsets).
+ */
+ public draw(): void {
+ this.container.removeAllChildren();
+
+ let currentCellX = 0;
+ let currentCellY = 0;
+
+ while (currentCellX <= MapView.MAP_SIZE) {
+ MapView.drawLine(
+ currentCellX * CELL_SIZE, 0,
+ currentCellX * CELL_SIZE, MapView.MAP_SIZE * CELL_SIZE,
+ this.gridLineWidth, Colors.GRID_COLOR, this.container);
+
+ currentCellX++;
+ }
+
+ while (currentCellY <= MapView.MAP_SIZE) {
+ MapView.drawLine(
+ 0, currentCellY * CELL_SIZE,
+ MapView.MAP_SIZE * CELL_SIZE, currentCellY * CELL_SIZE,
+ this.gridLineWidth, Colors.GRID_COLOR, this.container);
+
+ currentCellY++;
+ }
+ }
+
+ public setVisibility(value: boolean): void {
+ this.container.visible = value;
+ }
+} \ No newline at end of file
diff --git a/src/scripts/views/layers/hover.ts b/src/scripts/views/layers/hover.ts
new file mode 100644
index 00000000..b9f5509c
--- /dev/null
+++ b/src/scripts/views/layers/hover.ts
@@ -0,0 +1,129 @@
+import {Layer} from "./layer";
+import {MapView} from "../mapview";
+import {Colors} from "../../colors";
+import {DCObjectLayer} from "./dcobject";
+import {CELL_SIZE} from "../../controllers/mapcontroller";
+
+
+/**
+ * Class responsible for rendering the hover layer.
+ */
+export class HoverLayer implements Layer {
+ public container: createjs.Container;
+ public hoverTilePosition: IGridPosition;
+
+ private mapView: MapView;
+ private hoverTile: createjs.Shape;
+ private hoverRack: createjs.Container;
+ private hoverPSU: createjs.Container;
+ private hoverCoolingItem: createjs.Container;
+
+
+ constructor(mapView: MapView) {
+ this.mapView = mapView;
+ this.container = new createjs.Container();
+
+ this.initialDraw();
+ }
+
+ /**
+ * Draws the hover tile to the container at its current location and with its current color.
+ */
+ public draw(): void {
+ let color;
+
+ if (this.mapView.roomLayer.checkHoverTileValidity(this.hoverTilePosition)) {
+ color = Colors.ROOM_HOVER_VALID;
+ } else {
+ color = Colors.ROOM_HOVER_INVALID;
+ }
+
+ this.hoverTile.graphics.clear().beginFill(color)
+ .drawRect(this.hoverTilePosition.x * CELL_SIZE, this.hoverTilePosition.y * CELL_SIZE,
+ CELL_SIZE, CELL_SIZE)
+ .endFill();
+ if (this.hoverRack.visible) {
+ this.hoverRack.x = this.hoverTilePosition.x * CELL_SIZE;
+ this.hoverRack.y = this.hoverTilePosition.y * CELL_SIZE;
+ } else if (this.hoverPSU.visible) {
+ this.hoverPSU.x = this.hoverTilePosition.x * CELL_SIZE;
+ this.hoverPSU.y = this.hoverTilePosition.y * CELL_SIZE;
+ } else if (this.hoverCoolingItem.visible) {
+ this.hoverCoolingItem.x = this.hoverTilePosition.x * CELL_SIZE;
+ this.hoverCoolingItem.y = this.hoverTilePosition.y * CELL_SIZE;
+ }
+ }
+
+ /**
+ * Performs the initial drawing action.
+ */
+ public initialDraw(): void {
+ this.container.removeAllChildren();
+
+ this.hoverTile = new createjs.Shape();
+
+ this.hoverTilePosition = {x: 0, y: 0};
+
+ this.hoverTile = MapView.drawRectangle(this.hoverTilePosition, Colors.ROOM_HOVER_VALID, this.container);
+ this.hoverTile.visible = false;
+
+ this.hoverRack = DCObjectLayer.drawHoverRack(this.hoverTilePosition);
+ this.hoverPSU = DCObjectLayer.drawHoverPSU(this.hoverTilePosition);
+ this.hoverCoolingItem = DCObjectLayer.drawHoverCoolingItem(this.hoverTilePosition);
+
+ this.container.addChild(this.hoverRack);
+ this.container.addChild(this.hoverPSU);
+ this.container.addChild(this.hoverCoolingItem);
+
+ this.hoverRack.visible = false;
+ this.hoverPSU.visible = false;
+ this.hoverCoolingItem.visible = false;
+ }
+
+ /**
+ * Sets the hover tile visibility to true/false.
+ *
+ * @param value The visibility value
+ */
+ public setHoverTileVisibility(value: boolean): void {
+ this.hoverTile.visible = value;
+ this.mapView.updateScene = true;
+ }
+
+ /**
+ * Sets the hover item visibility to true/false.
+ *
+ * @param value The visibility value
+ * @param type The type of the object to be shown
+ */
+ public setHoverItemVisibility(value: boolean, type?: string): void {
+ if (value === true) {
+ this.hoverTile.visible = true;
+
+ this.setHoverItemVisibilities(type);
+ } else {
+ this.hoverTile.visible = false;
+ this.hoverRack.visible = false;
+ this.hoverPSU.visible = false;
+ this.hoverCoolingItem.visible = false;
+ }
+
+ this.mapView.updateScene = true;
+ }
+
+ private setHoverItemVisibilities(type: string): void {
+ if (type === "RACK") {
+ this.hoverRack.visible = true;
+ this.hoverPSU.visible = false;
+ this.hoverCoolingItem.visible = false;
+ } else if (type === "PSU") {
+ this.hoverRack.visible = false;
+ this.hoverPSU.visible = true;
+ this.hoverCoolingItem.visible = false;
+ } else if (type === "COOLING_ITEM") {
+ this.hoverRack.visible = false;
+ this.hoverPSU.visible = false;
+ this.hoverCoolingItem.visible = true;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/scripts/views/layers/layer.ts b/src/scripts/views/layers/layer.ts
new file mode 100644
index 00000000..5e5295ac
--- /dev/null
+++ b/src/scripts/views/layers/layer.ts
@@ -0,0 +1,8 @@
+/**
+ * Interface for a subview, representing a layer of the map view.
+ */
+export interface Layer {
+ container: createjs.Container;
+
+ draw(): void;
+}
diff --git a/src/scripts/views/layers/room.ts b/src/scripts/views/layers/room.ts
new file mode 100644
index 00000000..0e31fee0
--- /dev/null
+++ b/src/scripts/views/layers/room.ts
@@ -0,0 +1,177 @@
+import {InteractionLevel} from "../../controllers/mapcontroller";
+import {Util, IntensityLevel} from "../../util";
+import {Colors} from "../../colors";
+import {MapView} from "../mapview";
+import {Layer} from "./layer";
+
+
+/**
+ * Class responsible for rendering the rooms.
+ */
+export class RoomLayer implements Layer {
+ public container: createjs.Container;
+ public coloringMode: boolean;
+ public selectedTiles: ITile[];
+ public selectedTileObjects: TilePositionObject[];
+ public intensityLevels: { [key: number]: IntensityLevel; } = {};
+
+ private mapView: MapView;
+ private allRoomTileObjects: TilePositionObject[];
+ private validNextTilePositions: IGridPosition[];
+
+
+ constructor(mapView: MapView) {
+ this.mapView = mapView;
+ this.container = new createjs.Container();
+
+ this.allRoomTileObjects = [];
+ this.selectedTiles = [];
+ this.validNextTilePositions = [];
+ this.selectedTileObjects = [];
+ this.coloringMode = false;
+
+ this.draw();
+ }
+
+ /**
+ * Draws all rooms to the canvas.
+ */
+ public draw() {
+ this.container.removeAllChildren();
+
+ this.mapView.currentDatacenter.rooms.forEach((room: IRoom) => {
+ let color = Colors.ROOM_DEFAULT;
+
+ if (this.coloringMode && room.roomType === "SERVER" && this.intensityLevels[room.id] !== undefined) {
+ color = Util.convertIntensityToColor(this.intensityLevels[room.id]);
+ }
+
+ room.tiles.forEach((tile: ITile) => {
+ this.allRoomTileObjects.push({
+ position: tile.position,
+ tileObject: MapView.drawRectangle(tile.position, color, this.container)
+ });
+ });
+ });
+ }
+
+ /**
+ * Adds a newly selected tile to the list of selected tiles.
+ *
+ * If the tile was already selected beforehand, it is removed.
+ *
+ * @param tile The tile to be added
+ */
+ public addSelectedTile(tile: ITile): void {
+ this.selectedTiles.push(tile);
+
+ let tileObject = MapView.drawRectangle(tile.position, Colors.ROOM_SELECTED, this.container);
+ this.selectedTileObjects.push({
+ position: {x: tile.position.x, y: tile.position.y},
+ tileObject: tileObject
+ });
+
+ this.validNextTilePositions = Util.deriveValidNextTilePositions(
+ this.mapView.currentDatacenter.rooms, this.selectedTiles);
+
+ this.mapView.updateScene = true;
+ }
+
+ /**
+ * Removes a selected tile (upon being clicked on again).
+ *
+ * @param position The position at which a selected tile should be removed
+ * @param objectIndex The index of the tile in the selectedTileObjects array
+ */
+ public removeSelectedTile(position: IGridPosition, objectIndex: number): void {
+ let index = Util.tileListPositionIndexOf(this.selectedTiles, position);
+
+ // Check whether the given position doesn't belong to an already removed tile
+ if (index === -1) {
+ return;
+ }
+
+ this.selectedTiles.splice(index, 1);
+
+ this.container.removeChild(this.selectedTileObjects[objectIndex].tileObject);
+ this.selectedTileObjects.splice(objectIndex, 1);
+
+ this.validNextTilePositions = Util.deriveValidNextTilePositions(
+ this.mapView.currentDatacenter.rooms, this.selectedTiles);
+
+ this.mapView.updateScene = true;
+ }
+
+ /**
+ * Checks whether a hovered tile is in a valid location.
+ *
+ * @param position The tile location to be checked
+ * @returns {boolean} Whether it is a valid location
+ */
+ public checkHoverTileValidity(position: IGridPosition): boolean {
+ if (this.mapView.mapController.interactionLevel === InteractionLevel.BUILDING) {
+ if (this.selectedTiles.length === 0) {
+ return !Util.checkRoomCollision(this.mapView.currentDatacenter.rooms, position);
+ }
+ return Util.positionListContainsPosition(this.validNextTilePositions, position);
+ } else if (this.mapView.mapController.interactionLevel === InteractionLevel.ROOM) {
+ let valid = false;
+ this.mapView.mapController.roomModeController.currentRoom.tiles.forEach((element: ITile) => {
+ if (position.x === element.position.x && position.y === element.position.y &&
+ element.object === undefined) {
+ valid = true;
+ }
+ });
+ return valid;
+ }
+ }
+
+ /**
+ * Cancels room tile selection by removing all selected tiles from the scene.
+ */
+ public cancelRoomConstruction(): void {
+ if (this.selectedTiles.length === 0) {
+ return;
+ }
+
+ this.selectedTileObjects.forEach((tileObject: TilePositionObject) => {
+ this.container.removeChild(tileObject.tileObject);
+ });
+
+ this.resetTileLists();
+
+ this.mapView.updateScene = true;
+ }
+
+ /**
+ * Finalizes the selected room tiles into a standard room.
+ */
+ public finalizeRoom(room: IRoom): void {
+ if (this.selectedTiles.length === 0) {
+ return;
+ }
+
+ this.mapView.currentDatacenter.rooms.push(room);
+
+ this.resetTileLists();
+
+ // Trigger a redraw
+ this.draw();
+ this.mapView.wallLayer.generateWalls();
+ this.mapView.wallLayer.draw();
+
+ this.mapView.updateScene = true;
+ }
+
+ private resetTileLists(): void {
+ this.selectedTiles = [];
+ this.validNextTilePositions = [];
+ this.selectedTileObjects = [];
+ }
+
+ public setClickable(value: boolean): void {
+ this.allRoomTileObjects.forEach((tileObj: TilePositionObject) => {
+ tileObj.tileObject.cursor = value ? "pointer" : "default";
+ });
+ }
+} \ No newline at end of file
diff --git a/src/scripts/views/layers/roomtext.ts b/src/scripts/views/layers/roomtext.ts
new file mode 100644
index 00000000..65ea0735
--- /dev/null
+++ b/src/scripts/views/layers/roomtext.ts
@@ -0,0 +1,68 @@
+import {MapView} from "../mapview";
+import {Colors} from "../../colors";
+import {Util} from "../../util";
+import {Layer} from "./layer";
+import {CELL_SIZE} from "../../controllers/mapcontroller";
+
+
+export class RoomTextLayer implements Layer {
+ private static TEXT_PADDING = 4;
+
+ public container: createjs.Container;
+
+ private mapView: MapView;
+
+
+ constructor(mapView: MapView) {
+ this.mapView = mapView;
+ this.container = new createjs.Container();
+
+ this.draw();
+ }
+
+ public draw(): void {
+ this.container.removeAllChildren();
+
+ this.mapView.currentDatacenter.rooms.forEach((room: IRoom) => {
+ if (room.name !== "" && room.roomType !== "") {
+ this.renderTextOverlay(room);
+ }
+ });
+ }
+
+ public setVisibility(value: boolean): void {
+ this.mapView.animate(this.container, {alpha: value === true ? 1 : 0});
+ }
+
+ /**
+ * Draws a name and type overlay over the given room.
+ */
+ private renderTextOverlay(room: IRoom): void {
+ if (room.name === null || room.tiles.length === 0) {
+ return;
+ }
+
+ let textPos = Util.calculateRoomNamePosition(room);
+
+ let bottomY = this.renderText(room.name, "12px Arial", textPos,
+ textPos.topLeft.y * CELL_SIZE + RoomTextLayer.TEXT_PADDING);
+ this.renderText("Type: " + Util.toSentenceCase(room.roomType), "10px Arial", textPos, bottomY + 5);
+ }
+
+ private renderText(text: string, font: string, textPos: IRoomNamePos, startY: number): number {
+ let name = new createjs.Text(text, font, Colors.ROOM_NAME_COLOR);
+
+ if (name.getMeasuredWidth() > textPos.length * CELL_SIZE - RoomTextLayer.TEXT_PADDING * 2) {
+ name.scaleX = name.scaleY = (textPos.length * CELL_SIZE - RoomTextLayer.TEXT_PADDING * 2) /
+ name.getMeasuredWidth();
+ }
+
+ // Position the text to the top left of the selected tile
+ name.x = textPos.topLeft.x * CELL_SIZE + RoomTextLayer.TEXT_PADDING;
+ name.y = startY;
+
+ this.container.addChild(name);
+
+ return name.y + name.getMeasuredHeight() * name.scaleY;
+ }
+}
diff --git a/src/scripts/views/layers/wall.ts b/src/scripts/views/layers/wall.ts
new file mode 100644
index 00000000..06ba4675
--- /dev/null
+++ b/src/scripts/views/layers/wall.ts
@@ -0,0 +1,62 @@
+import {Colors} from "../../colors";
+import {MapView} from "../mapview";
+import {Util} from "../../util";
+import {Layer} from "./layer";
+import {CELL_SIZE} from "../../controllers/mapcontroller";
+
+
+/**
+ * Class responsible for rendering the walls.
+ */
+export class WallLayer implements Layer {
+ public container: createjs.Container;
+
+ private mapView: MapView;
+ private walls: IRoomWall[];
+ private wallLineWidth: number;
+
+
+ constructor(mapView: MapView) {
+ this.mapView = mapView;
+ this.container = new createjs.Container();
+ this.wallLineWidth = CELL_SIZE / 20.0;
+
+ this.generateWalls();
+ this.draw();
+ }
+
+ /**
+ * Calls the Util.deriveWallLocations function to generate the wall locations.
+ */
+ public generateWalls(): void {
+ this.walls = Util.deriveWallLocations(this.mapView.currentDatacenter.rooms);
+ }
+
+ /**
+ * Draws all walls to the canvas.
+ */
+ public draw(): void {
+ this.container.removeAllChildren();
+
+ // Draw walls
+ this.walls.forEach((element: IRoomWall) => {
+ if (element.horizontal) {
+ MapView.drawLine(
+ CELL_SIZE * element.startPos[0] - this.wallLineWidth / 2.0,
+ CELL_SIZE * element.startPos[1],
+ CELL_SIZE * (element.startPos[0] + element.length) + this.wallLineWidth / 2.0,
+ CELL_SIZE * element.startPos[1],
+ this.wallLineWidth, Colors.WALL_COLOR, this.container
+ );
+ } else {
+ MapView.drawLine(
+ CELL_SIZE * element.startPos[0],
+ CELL_SIZE * element.startPos[1] - this.wallLineWidth / 2.0,
+ CELL_SIZE * element.startPos[0],
+ CELL_SIZE * (element.startPos[1] + element.length) + this.wallLineWidth / 2.0,
+ this.wallLineWidth, Colors.WALL_COLOR, this.container
+ );
+ }
+ });
+ }
+} \ No newline at end of file
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
diff --git a/src/styles/404.less b/src/styles/404.less
new file mode 100644
index 00000000..8842b621
--- /dev/null
+++ b/src/styles/404.less
@@ -0,0 +1,147 @@
+html, body {
+ padding: 0;
+ margin: 0;
+ width: 100%;
+ height: 100%;
+
+ background-image: linear-gradient(135deg, #00678a, #008fbf, #00A6D6);
+}
+
+.terminal-window {
+ width: 600px;
+ height: 400px;
+ display: block;
+
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+
+ margin: auto;
+
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ cursor: default;
+
+ overflow: hidden;
+
+ box-shadow: 5px 5px 20px #444444;
+}
+
+.terminal-header {
+ font-family: monospace;
+ background: #cccccc;
+ color: #444444;
+ height: 30px;
+ line-height: 30px;
+ padding-left: 10px;
+
+ border-top-left-radius: 7px;
+ border-top-right-radius: 7px;
+}
+
+.terminal-body {
+ font-family: monospace;
+ text-align: center;
+ background-color: #333333;
+ color: #eeeeee;
+ padding: 10px;
+
+ height: 100%;
+}
+
+.segfault {
+ text-align: left;
+}
+
+.cursor {
+ -webkit-animation: 1s blink step-end infinite;
+ -moz-animation: 1s blink step-end infinite;
+ -ms-animation: 1s blink step-end infinite;
+ -o-animation: 1s blink step-end infinite;
+ animation: 1s blink step-end infinite;
+}
+
+.code-block {
+ white-space: pre-wrap;
+
+ margin-top: 60px;
+}
+
+.sub-title {
+ margin-top: 20px;
+}
+
+.home-btn {
+ margin-top: 10px;
+ padding: 5px;
+ display: inline-block;
+ border: 1px solid #eeeeee;
+ color: #eeeeee;
+ text-decoration: none;
+ cursor: pointer;
+
+ -webkit-transition: all 200ms;
+ -moz-transition: all 200ms;
+ -ms-transition: all 200ms;
+ -o-transition: all 200ms;
+ transition: all 200ms;
+}
+
+.home-btn:hover {
+ background: #eeeeee;
+ color: #333333;
+}
+
+.home-btn:active {
+ background: #333333;
+ color: #eeeeee;
+}
+
+@keyframes blink {
+ from, to {
+ color: #eeeeee;
+ }
+ 50% {
+ color: #333333;
+ }
+}
+
+@-moz-keyframes blink {
+ from, to {
+ color: #eeeeee;
+ }
+ 50% {
+ color: #333333;
+ }
+}
+
+@-webkit-keyframes blink {
+ from, to {
+ color: #eeeeee;
+ }
+ 50% {
+ color: #333333;
+ }
+}
+
+@-ms-keyframes blink {
+ from, to {
+ color: #eeeeee;
+ }
+ 50% {
+ color: #333333;
+ }
+}
+
+@-o-keyframes blink {
+ from, to {
+ color: #eeeeee;
+ }
+ 50% {
+ color: #333333;
+ }
+} \ No newline at end of file
diff --git a/src/styles/main.less b/src/styles/main.less
new file mode 100644
index 00000000..bb560c5b
--- /dev/null
+++ b/src/styles/main.less
@@ -0,0 +1,1190 @@
+/* Colors */
+@gray-dark: #aaa;
+@gray-semi-dark: #bbb;
+@gray-semi-light: #ccc;
+@gray-light: #ddd;
+@gray-very-light: #eee;
+@blue: #00A6D6;
+@blue-dark: #0087b5;
+@blue-very-dark: #006182;
+@blue-light: #deebf7;
+
+/* Sizes, Margins and Paddings*/
+@document-padding: 20px;
+@inter-element-margin: 5px;
+@standard-border-radius: 5px;
+@side-menu-width: 350px;
+@color-indicator-width: 140px;
+
+@global-padding: 30px;
+@transition-length: 150ms;
+@side-bar-width: 250px;
+@navbar-height: 50px;
+
+html, body {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+
+ font-family: Helvetica, Verdana, sans-serif;
+
+ overflow: hidden;
+
+ background: #eee;
+}
+
+a:hover {
+ text-decoration: none;
+}
+
+#main-canvas {
+ float: left;
+}
+
+.app-content {
+ position: relative;
+ width: 100%;
+ height: 100%;
+
+ z-index: 1;
+}
+
+/* Mixin for cross-platform prevention of user-text-selection */
+.user-select-def {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+/* Mixin for cross-platform transitions */
+.transition-def(@property, @time) {
+ -webkit-transition: @property @time;
+ -moz-transition: @property @time;
+ -ms-transition: @property @time;
+ -o-transition: @property @time;
+ transition: @property @time;
+}
+
+/* Mixin for cross-platform border-radius properties */
+.border-radius-def(@length) {
+ -webkit-border-radius: @length;
+ -moz-border-radius: @length;
+ border-radius: @length;
+}
+
+/* General Button Abstractions */
+.clickable {
+ cursor: pointer;
+ .user-select-def;
+}
+
+.button {
+ text-align: center;
+ padding: 5px;
+
+ background-color: @gray-semi-dark;
+
+ .border-radius-def(@standard-border-radius);
+ .clickable;
+
+ .transition-def(all, 200ms);
+}
+
+.button:hover {
+ background-color: @gray-semi-light;
+}
+
+.button:active {
+ background-color: @gray-dark;
+}
+
+/* Container for menu panels */
+.side-menu-container {
+ display: inline-block;
+ position: absolute;
+ top: @document-padding;
+ width: @side-menu-width;
+ height: calc(100% - @document-padding);
+ margin-top: 10px;
+
+ pointer-events: none;
+}
+
+.left-side {
+ left: @document-padding;
+}
+
+.right-side {
+ right: @document-padding;
+}
+
+.right-middle-side {
+ right: @document-padding * 2 + @side-menu-width;
+}
+
+/* Collapsible menu panel */
+.menu-container {
+ margin-bottom: 10px;
+ border: 1px solid @gray-dark;
+
+ -webkit-border-radius: @standard-border-radius;
+ -moz-border-radius: @standard-border-radius;
+ border-radius: @standard-border-radius;
+
+ overflow: hidden;
+ pointer-events: all;
+}
+
+.menu-header-bar {
+ display: block;
+ padding: 5px;
+
+ font-weight: bold;
+
+ background-color: @gray-semi-light;
+ border-bottom-width: 0;
+}
+
+.menu-body {
+ display: block;
+ padding: 10px 5px;
+
+ background-color: @gray-very-light;
+
+ .dropdown {
+ margin: @inter-element-margin 0;
+ display: inline-block;
+ }
+
+ .dropdown-label {
+ display: inline-block;
+ margin-right: 10px;
+ }
+}
+
+.menu-body.simulation {
+ display: none;
+}
+
+.side-menu-container .menu-collapse, .side-menu-container .menu-exit {
+ display: inline-block;
+ float: right;
+ width: 20px;
+ height: 20px;
+
+ text-align: center;
+ line-height: 20px;
+ padding: 0;
+ font-size: 9pt;
+
+ color: #777;
+}
+
+#room-menu {
+ .input-group {
+ margin-bottom: 5px;
+ }
+
+ label {
+ margin-top: 10px;
+ }
+}
+
+#room-construction-cancel {
+ display: none;
+}
+
+/* DC components */
+.dc-component-container {
+ .border-radius-def(@standard-border-radius);
+
+ margin-bottom: @inter-element-margin;
+ padding: 10px;
+
+ cursor: pointer;
+
+ .transition-def(background-color, 150ms);
+
+ .dc-component {
+ display: inline-block;
+ width: 50px;
+ height: 50px;
+ float: left;
+
+ border: 4px solid #000;
+ }
+
+ .dc-component-label {
+ display: inline-block;
+ padding-left: 20px;
+ line-height: 50px;
+ font-size: 1.3em;
+ }
+}
+
+.dc-component-container:hover {
+ background-color: @gray-semi-dark;
+}
+
+.dc-component-container:active {
+ background-color: @blue-dark;
+}
+
+.dc-component-container[data-active="true"] {
+ background-color: @blue;
+}
+
+/* DC Object colors */
+.dc-rack {
+ background-color: rgb(170, 170, 170);
+}
+
+.dc-psu {
+ background-color: rgb(230, 50, 60);
+}
+
+.dc-cooling-item {
+ background-color: rgb(40, 50, 230);
+}
+
+/* Rack menu */
+.node-list-container {
+ border: 2px solid #000000;
+ overflow: auto;
+ max-height: 300px;
+
+ margin: @inter-element-margin 0;
+ padding: 0;
+ @node-height: 50px;
+
+ .node-element {
+ position: relative;
+
+ @img-size: 34px;
+ height: @node-height;
+ cursor: hand;
+
+ background-color: @gray-semi-light;
+
+ .node-element-btn {
+ display: inline-block;
+ float: left;
+ width: 40px;
+ height: @node-height - 1px;
+
+ color: #000;
+
+ line-height: @node-height - 1px;
+ text-align: center;
+ }
+
+ .node-element-btn:hover {
+ text-decoration: none;
+ }
+
+ .node-element-content {
+ @element-padding-top: 2px;
+ @element-padding-left: 12px;
+ @img-margin: 7px;
+
+ position: relative;
+
+ display: inline-flex;
+ overflow: hidden;
+ margin: 5px;
+ padding: @element-padding-top @element-padding-left;
+
+ border: 1px solid #000;
+ background: #eeeeee;
+
+ img {
+ width: @img-size;
+ height: @img-size;
+ }
+
+ img:not(:last-of-type) {
+ margin-right: @img-margin;
+ }
+
+ .icon-overlay {
+ position: absolute;
+ top: @element-padding-top;
+ width: @img-size;
+ height: @img-size;
+
+ background: #eeeeee;
+ opacity: 0.6;
+ }
+
+ .overlay-cpu {
+ left: @element-padding-left;
+ }
+
+ .overlay-gpu {
+ left: @element-padding-left + @img-size + @img-margin;
+ }
+
+ .overlay-memory {
+ left: @element-padding-left + (@img-size + @img-margin) * 2;
+ }
+
+ .overlay-storage {
+ left: @element-padding-left + (@img-size + @img-margin) * 3;
+ }
+
+ .overlay-network {
+ left: @element-padding-left + (@img-size + @img-margin) * 4;
+ }
+ }
+
+ .node-element-number {
+ display: inline-block;
+ float: right;
+ width: 30px;
+ height: 100%;
+
+ line-height: 50px;
+ text-align: right;
+ padding-right: 10px;
+ }
+ }
+
+ .node-element:not(:last-of-type) {
+ border-bottom: 1px solid #666;
+ }
+
+ .node-element:last-of-type .node-element-content {
+ margin-bottom: 0;
+ }
+
+ .node-element-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+
+ width: 100%;
+ height: @node-height;
+
+ background: #eeeeee;
+ opacity: 0.6;
+
+ z-index: 100;
+ }
+}
+
+#node-menu {
+ .nav-tabs {
+ li.active, li.active a {
+ background-color: @gray-very-light;
+ }
+
+ img {
+ width: 30px;
+ height: 30px;
+ }
+ }
+
+ .tab-content {
+ overflow: hidden;
+
+ .panel-heading .accordion-toggle:after {
+ //noinspection CssNoGenericFontName
+ font-family: 'Glyphicons Halflings';
+ content: "\e114";
+ float: right;
+ color: grey;
+ }
+
+ .panel-heading .accordion-toggle.collapsed:after {
+ content: "\e080";
+ }
+ }
+
+ .remove-unit:hover {
+ text-decoration: none;
+ }
+
+ .unit-add-input {
+ margin-bottom: 10px;
+ }
+
+ .spec-table td {
+ height: 40px;
+ line-height: 40px;
+ padding: 0 10px;
+ }
+
+ .spec-table tr td:first-child {
+ width: 50px;
+ text-align: right;
+ }
+
+ .spec-table tr td:nth-child(2) {
+ width: 200px;
+ }
+
+ .spec-table tr td:nth-child(3) {
+ width: 50px;
+ }
+}
+
+/* Tasks menu */
+#tasks-menu .menu-body {
+ overflow-y: auto;
+ max-height: 350px;
+}
+
+#tasks-menu .menu-body .task-element {
+ height: 70px;
+
+ .task-icon {
+ @icon-size: 35px;
+
+ display: inline-block;
+ position: relative;
+ float: left;
+
+ top: 13px;
+ left: 10px;
+
+ width: @icon-size;
+ height: @icon-size;
+ font-size: @icon-size;
+
+ }
+
+ .task-info {
+ display: inline-block;
+ width: 270px;
+ float: right;
+
+ .task-time {
+ display: block;
+ }
+
+ .progress {
+ margin-bottom: 0;
+ }
+
+ .task-flops {
+ display: block;
+ }
+ }
+
+ &:not(:last-child) {
+ border-bottom: 1px solid #bbb;
+ margin-bottom: 10px;
+ }
+}
+
+#statistics-chart, #machine-statistics-chart {
+ display: block;
+ height: 200px;
+ margin-right: 15px;
+}
+
+/* Container for Zooming buttons */
+.tool-panel {
+ display: inline-block;
+ position: absolute;
+ bottom: @document-padding;
+ left: @document-padding;
+
+ z-index: 1000;
+}
+
+.btn-circle {
+ width: 30px;
+ height: 30px;
+ text-align: center;
+ padding: 6px 0;
+ font-size: 12px;
+ line-height: 1.428571429;
+ border-radius: 15px;
+
+ border-color: @gray-dark;
+}
+
+/* Indicators*/
+.indicators {
+ display: inline-block;
+ position: absolute;
+ bottom: @document-padding;
+ right: @document-padding;
+}
+
+/* Scale indicator */
+.scale-indicator {
+ display: inline-block;
+ width: 100px;
+ height: 18px;
+ line-height: 18px;
+
+ text-align: right;
+
+ border-top-width: 0;
+ border-right-width: 0;
+ border-bottom: 2px solid #000;
+ border-left: 2px solid #000;
+
+ .user-select-def;
+}
+
+/* Color indicator */
+.color-indicator {
+ @color-indicator-padding: 7px;
+ @element-width: (@color-indicator-width - @color-indicator-padding * 2) / 4;
+
+ display: inline-block;
+ position: relative;
+ top: 5px;
+ margin-left: 15px;
+ cursor: pointer;
+ padding: @color-indicator-padding;
+ .border-radius-def(@standard-border-radius);
+ .transition-def(background, @transition-length);
+ z-index: 100;
+
+ width: @color-indicator-width;
+
+ &:hover {
+ background: rgba(127, 127, 127, 0.5);
+ }
+
+ .intensity-labels {
+ height: 20px;
+ line-height: 20px;
+
+ font-size: 8pt;
+
+ div {
+ display: inline-block;
+ width: @element-width;
+ margin-right: -3px;
+
+ text-align: center;
+ }
+
+ div:first-of-type {
+ width: @element-width / 2;
+ text-align: left;
+ }
+
+ div:last-of-type {
+ width: @element-width / 2;
+ text-align: right;
+ }
+ }
+
+ .intensity-colors {
+ height: 15px;
+
+ div {
+ display: inline-block;
+ width: @element-width + 1;
+ height: 100%;
+ margin-right: -5px;
+ border: 1px solid #444;
+ }
+
+ .intensity-low {
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+ background: rgba(197, 224, 180, 1);
+ }
+
+ .intensity-mid-low {
+ background: rgba(255, 230, 153, 1);
+ }
+
+ .intensity-mid-high {
+ background: rgba(248, 203, 173, 1);
+ }
+
+ .intensity-high {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+ background: rgba(249, 165, 165, 1);
+ }
+ }
+}
+
+@global-control-height: 40px;
+
+/* Mode switch */
+.mode-switch {
+ display: block;
+ width: 100%;
+ height: @global-control-height;
+ line-height: @global-control-height;
+
+ margin-bottom: 10px;
+ pointer-events: all;
+
+ background-color: #fff;
+ border: 1px solid @gray-dark;
+ .border-radius-def(@standard-border-radius);
+ overflow: hidden;
+
+ div {
+ display: inline-block;
+ width: 50%;
+ height: 100%;
+ text-align: center;
+
+ font-size: 1.2em;
+ .clickable;
+ }
+
+ div:first-child {
+ float: left;
+ }
+
+ div:last-child {
+ float: right;
+ }
+}
+
+#save-version-btn {
+ width: 50%;
+ height: @global-control-height;
+ line-height: @global-control-height;
+ text-align: center;
+ margin-bottom: 10px;
+ font-size: 1.2em;
+ color: #fff;
+
+ .clickable;
+ pointer-events: all;
+ .border-radius-def(@standard-border-radius);
+}
+
+#save-version-btn[data-saved="true"] {
+ background-color: #4dba31;
+
+ &:hover {
+ background: #3b8e25;
+ }
+}
+
+#save-version-btn[data-saved="false"] {
+ background-color: #d69931;
+
+ &:hover {
+ background: #af7b2d;
+ }
+}
+
+.mode-switch[data-selected="construction"] {
+ #construction-mode-switch {
+ background: @blue;
+ color: #fff;
+ }
+
+ #construction-mode-switch:hover {
+ background: @blue-dark;
+ }
+
+ #simulation-mode-switch {
+ background: #fff;
+ color: #000;
+ }
+
+ #simulation-mode-switch:hover {
+ background: #eee;
+ }
+}
+
+.mode-switch[data-selected="simulation"] {
+ #construction-mode-switch {
+ background: #fff;
+ color: #000;
+ }
+
+ #construction-mode-switch:hover {
+ background: #eee;
+ }
+
+ #simulation-mode-switch {
+ background: @blue;
+ color: #fff;
+ }
+
+ #simulation-mode-switch:hover {
+ background: @blue-dark;
+ }
+}
+
+#experiment-menu h2 {
+ margin-top: 0;
+ font-size: 12pt;
+}
+
+/* Timeline controls */
+.timeline-bar {
+ display: block;
+ position: absolute;
+ left: 0;
+ bottom: @document-padding;
+ width: 100%;
+ text-align: center;
+}
+
+.timeline-container {
+ @container-size: 500px;
+ @play-btn-size: 40px;
+ @border-width: 1px;
+ @timeline-border: @border-width solid @gray-semi-dark;
+
+ display: inline-block;
+ margin: 0 auto;
+ text-align: left;
+
+ width: @container-size;
+
+ .labels {
+ display: block;
+ height: 25px;
+ line-height: 25px;
+
+ div {
+ display: inline-block;
+ }
+
+ .start-time-label {
+ margin-left: @play-btn-size - @border-width;
+ padding-left: 4px;
+ }
+
+ .end-time-label {
+ padding-right: 4px;
+ float: right;
+ }
+ }
+
+ .controls {
+ display: flex;
+ border: @timeline-border;
+ overflow: hidden;
+
+ // Fix for border-radius overflow in Chrome/Webkit
+ -webkit-mask-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC);
+
+ .border-radius-def(@standard-border-radius);
+
+ .play-btn {
+ width: @play-btn-size;
+ height: @play-btn-size + @border-width;
+ line-height: @play-btn-size + @border-width;
+ text-align: center;
+ float: left;
+
+ margin-top: -@border-width;
+
+ font-size: 16pt;
+ background: #333;
+ color: #eee;
+
+ .transition-def(background, @transition-length);
+
+ .user-select-def;
+ .clickable;
+
+ // Loading icon
+ img {
+ display: none;
+ position: relative;
+ width: 70%;
+ height: 70%;
+ margin-top: -10px;
+ }
+ }
+
+ .play-btn::before {
+ position: relative;
+ top: -1px;
+ left: 1px;
+ }
+
+ .play-btn:hover {
+ background: #656565;
+ }
+
+ .play-btn:active {
+ background: #000;
+ }
+
+ .timeline {
+ position: relative;
+ flex: 1;
+ height: @play-btn-size;
+ line-height: @play-btn-size;
+ float: right;
+
+ background: @blue-light;
+
+ z-index: 500;
+
+ div {
+ .transition-def(all, @transition-length);
+ }
+
+ .time-marker {
+ position: absolute;
+ top: 0;
+ left: 0;
+
+ width: 6px;
+ height: 100%;
+
+ background: @blue-very-dark;
+
+ .border-radius-def(2px);
+
+ z-index: 503;
+ }
+
+ .section-marker {
+ position: absolute;
+ top: 0;
+ left: 0;
+
+ width: 3px;
+ height: 100%;
+
+ background: #222222;
+
+ z-index: 504;
+ }
+
+ .cache-section {
+ position: absolute;
+ top: 0;
+ left: 0;
+
+ width: 0;
+ height: 100%;
+
+ background: @blue;
+
+ z-index: 501;
+ }
+
+ .task-indicator {
+ display: inline-block;
+ position: absolute;
+ bottom: 0;
+ width: 10px;
+ height: 10px;
+
+ border: 1px solid #000;
+
+ z-index: 502;
+ }
+
+ .task-queued {
+ background: #fff340;
+ }
+
+ .task-started {
+ background: #ff72a0;
+ }
+
+ .task-finished {
+ background: #c036ff;
+ }
+ }
+ }
+}
+
+/* Informational message container, for communicating events to the user */
+.info-balloon {
+ display: none;
+ position: absolute;
+ bottom: @document-padding;
+ right: @document-padding;
+
+ padding-left: 15px;
+ padding-right: 15px;
+
+ height: 30px;
+ line-height: 30px;
+
+ background: @blue;
+ color: #fff;
+
+ .border-radius-def(15px);
+
+ span {
+ line-height: 30px;
+ margin-right: 10px;
+ }
+}
+
+/* Loading overlay, shown during setup */
+.loading-overlay {
+ z-index: 1000;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+
+ background: rgba(0, 124, 159, 0.7);
+
+ .loading-overlay-content {
+ text-align: center;
+ display: inline-block;
+
+ width: 300px;
+ height: 200px;
+
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+
+ margin: auto;
+
+ img {
+ position: relative;
+ bottom: -10px;
+
+ animation: bounce 3s ease infinite;
+ }
+
+ .loading-text {
+ padding: 3px 10px;
+ background-color: #eeeeee;
+ .border-radius-def(@standard-border-radius);
+ }
+ }
+}
+
+@keyframes bounce {
+ 0% {
+ bottom: -10px;
+ }
+ 50% {
+ bottom: 40px;
+ }
+ 100% {
+ bottom: -10px;
+ }
+}
+
+/* Experiments */
+.experiment-list {
+ display: block;
+ font-size: 12pt;
+ border: 0;
+
+ .list-head, .list-body .experiment-row {
+ display: block;
+ position: relative;
+ }
+
+ .list-head div, .list-body .project-row div {
+ padding: 0 10px;
+ display: inline-block;
+ }
+
+ .list-head {
+ font-weight: bold;
+ }
+
+ .experiment-row {
+ background: #f8f8f8;
+ border: 1px solid #b6b6b6;
+ height: 40px;
+ line-height: 40px;
+ clear: both;
+
+ .transition-def(background, @transition-length);
+ .clickable;
+ }
+
+ .experiment-row:hover {
+ background: #fff;
+ }
+
+ .experiment-row:active {
+ background: #cccccc;
+ }
+
+ .experiment-row:not(:first-of-type) {
+ margin-top: -1px;
+ }
+
+ // Sizing of table columns
+ .experiment-row, .list-head {
+ div {
+ display: inline-block;
+ width: 20%;
+
+ padding-left: 5px;
+ margin-right: -4px; // Address default margin between inline-blocks
+ }
+
+ div:last-of-type {
+ text-align: right;
+ padding-right: 10px;
+ }
+ }
+}
+
+.no-experiments-alert {
+ display: none;
+ position: relative;
+ padding-left: 50px;
+ span {
+ position: absolute;
+ top: 11px;
+ left: 10px;
+ bottom: 10px;
+ font-size: 20pt;
+ }
+}
+
+.window-overlay {
+ display: none;
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+
+ background: rgba(0, 0, 0, 0.5);
+
+ z-index: 5000;
+}
+
+.experiments-window {
+ width: 80%;
+ height: 80%;
+
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ margin: auto;
+
+ overflow: hidden;
+
+ .window-body {
+ width: 100%;
+ height: 340px;
+
+ padding: 10px;
+
+ background: rgba(230, 230, 230, 0.9);
+
+ .border-radius-def(5px);
+
+ .window-heading {
+ font-size: 20pt;
+ color: #000000;
+
+ .user-select-def;
+
+ font-weight: bold;
+
+ padding-left: 5px;
+ }
+
+ .experiment-add-form {
+ margin: 20px;
+
+ input, select {
+ width: 200px;
+ }
+
+ label {
+ width: 80px;
+ text-align: right;
+
+ .user-select-def;
+ }
+ }
+
+ .experiments-table-label {
+ display: block;
+ padding-left: 20px;
+ margin-bottom: 5px;
+
+ font-size: 18px;
+
+ .user-select-def;
+ }
+
+ .experiment-name-alert {
+ position: absolute;
+ bottom: 0;
+ left: 20px;
+ display: none;
+ }
+ }
+
+ .window-footer {
+ width: 100%;
+ height: 60px;
+
+ background: rgba(56, 56, 56, 0.9);
+
+ padding: 12px 10px;
+
+ .btn.pull-left {
+ margin-right: 5px;
+ }
+
+ .btn.pull-right {
+ margin-left: 5px;
+ }
+ }
+}
+
+.window-close {
+ display: inline-block;
+ font-size: 16pt;
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ cursor: pointer;
+ color: #888888;
+}
+
+/* Internal page */
+/* Main content body (side-menu and main-body) */
+.content {
+ position: relative;
+ top: -@navbar-height;
+ width: 100%;
+ height: 100%;
+ padding-top: @navbar-height;
+ z-index: 1;
+}
+
+/* Content body */
+.main-body {
+ position: relative;
+ display: block;
+ margin: auto;
+ width: 900px;
+ max-width: 900px;
+ height: 100%;
+ padding: 40px;
+
+ border-left: 1px solid #bbb;
+ border-right: 1px solid #bbb;
+ background: #f7f7f7;
+
+ h2 {
+ font-weight: bold;
+ margin: 10px 0 20px 0;
+ }
+}
+
+.modal {
+ z-index: 10000;
+}
diff --git a/src/styles/navbar.less b/src/styles/navbar.less
new file mode 100644
index 00000000..3eba0982
--- /dev/null
+++ b/src/styles/navbar.less
@@ -0,0 +1,158 @@
+@import "main.less";
+
+/* Navbar */
+.top-navbar {
+ @navbar-padding: 10px;
+
+ position: relative;
+ display: block;
+ width: 100%;
+ height: @navbar-height;
+
+ color: #eee;
+ background: #343434;
+
+ z-index: 100;
+
+ -webkit-box-shadow: -10px 8px 3px 12px #000;
+ -moz-box-shadow: -10px 8px 3px 12px #000;
+ box-shadow: -10px -10px 3px 12px #000;
+
+ .opendc-brand {
+ display: inline-block;
+ float: left;
+ margin-left: @global-padding;
+
+ padding: 0 10px;
+
+ cursor: pointer;
+ color: #eee;
+ height: 100%;
+
+ .transition-def(background, @transition-length);
+
+ img {
+ display: inline-block;
+ float: left;
+ margin: @navbar-padding 0;
+
+ width: @navbar-height - @navbar-padding * 2;
+ height: @navbar-height - @navbar-padding * 2;
+ vertical-align: middle;
+ }
+
+ .opendc-title {
+ display: inline-block;
+ float: right;
+ margin-left: 20px;
+
+ font-size: 16pt;
+ line-height: @navbar-height;
+ }
+ }
+
+ .opendc-brand:hover {
+ background: @blue;
+ }
+
+ .opendc-brand:active {
+ background: @blue-dark;
+ }
+
+ .navbar-button-group {
+ display: inline-block;
+ height: 100%;
+
+ a {
+ display: inline-block;
+ line-height: @navbar-height;
+ text-align: center;
+ color: #eee;
+
+ border-left: 1px solid #464646;
+
+ .clickable;
+ .transition-def(background, @transition-length);
+ }
+
+ a:last-of-type {
+ border-right: 1px solid #464646;
+ }
+ }
+
+ .navigation {
+ margin-left: 30px;
+
+ .projects {
+ float: left;
+ padding: 0 20px;
+
+ font-size: 12pt;
+ }
+
+ .projects:hover {
+ background: #606060;
+ }
+
+ .projects:active {
+ background: #161616;
+ }
+ }
+
+ .user {
+ float: right;
+ margin-right: @global-padding;
+
+ .support {
+ float: left;
+ margin-top: -1px;
+
+ width: @navbar-height;
+
+ font-size: 14pt;
+ }
+
+ .support:hover {
+ background: #41a0cd;
+ }
+
+ .support:active {
+ background: #307798;
+ }
+
+ .username {
+ float: left;
+ padding: 0 20px;
+
+ font-size: 12pt;
+ }
+
+ .username:hover {
+ background: #4eae44;
+ }
+
+ .username:active {
+ background: #2d6527;
+ }
+
+ .sign-out {
+ float: right;
+ margin-top: -1px;
+ width: @navbar-height;
+
+ font-size: 14pt;
+ }
+
+ .sign-out:hover {
+ background: #e3474d;
+ }
+
+ .sign-out:active {
+ background: #a73438;
+ }
+ }
+}
+
+#google-signin {
+ display: none;
+}
diff --git a/src/styles/profile.less b/src/styles/profile.less
new file mode 100644
index 00000000..40bf49f8
--- /dev/null
+++ b/src/styles/profile.less
@@ -0,0 +1,22 @@
+@import "main.less";
+
+.main-body.profile-page {
+ text-align: center;
+
+ :not(.btn) {
+ text-align: left;
+ }
+}
+
+#delete-account {
+ margin: 10px 0;
+}
+
+.delete-info {
+ color: #999999;
+ padding: 0 80px;
+}
+
+.account-delete-alert {
+ display: none;
+} \ No newline at end of file
diff --git a/src/styles/projects.less b/src/styles/projects.less
new file mode 100644
index 00000000..926ea8ec
--- /dev/null
+++ b/src/styles/projects.less
@@ -0,0 +1,391 @@
+@import "main.less";
+
+/* Buttons */
+.new-project-btn {
+ @button-height: 35px;
+
+ display: inline-block;
+ position: absolute;
+ bottom: 40px;
+ right: 40px;
+ padding: 0 20px;
+ height: @button-height;
+ line-height: @button-height;
+ font-size: 12pt;
+
+ background: #679436;
+ color: #eee;
+ border: 1px solid #507830;
+
+ .border-radius-def(@standard-border-radius);
+ .clickable;
+ .transition-def(all, @transition-length);
+}
+
+.new-project-btn:hover {
+ background: #73ac45;
+}
+
+.new-project-btn:active {
+ background: #5c8835;
+}
+
+/* Side menu */
+.filter-menu {
+ display: block;
+
+ background: #0761b1;
+ border: 1px solid #06326b;
+ color: #eee;
+
+ text-align: center;
+
+ .border-radius-def(@standard-border-radius);
+ overflow: hidden;
+
+ margin-bottom: 20px;
+
+ .project-filters {
+ display: block;
+ overflow: hidden;
+ margin-left: -2px;
+
+ div {
+ display: inline-block;
+ width: 33.3%;
+ margin-right: -4px;
+ padding: 10px @global-padding;
+
+ font-size: 12pt;
+ border-right: 1px solid #06326b;
+
+ .clickable;
+ .transition-def(background, @transition-length);
+ }
+
+ div:last-of-type {
+ border: 0;
+ }
+
+ div:hover {
+ background: #0c60bf;
+ }
+
+ div:active, div.active {
+ background: #073d7d;
+ }
+ }
+}
+
+/* Message shown when no projects present */
+.no-projects-alert {
+ display: none;
+ position: relative;
+ padding-left: 50px;
+ span {
+ position: absolute;
+ top: 11px;
+ left: 10px;
+ bottom: 10px;
+ font-size: 20pt;
+ }
+}
+
+/* List of simulation projects */
+.project-list {
+ display: block;
+ font-size: 12pt;
+ border: 0;
+
+ .list-head, .list-body .project-row {
+ display: block;
+ position: relative;
+ }
+
+ .list-head div, .list-body .project-row div {
+ padding: 0 10px;
+ display: inline-block;
+ }
+
+ .list-head {
+ font-weight: bold;
+
+ div {
+ margin-right: -4px; // Address default margin between inline-blocks
+ }
+ }
+
+ .project-row {
+ background: #f8f8f8;
+ border: 1px solid #b6b6b6;
+ height: 40px;
+ line-height: 40px;
+ clear: both;
+
+ .transition-def(background, @transition-length);
+ .clickable;
+ }
+
+ .project-row:hover {
+ background: #fff;
+ }
+
+ .project-row:active {
+ background: #cccccc;
+ }
+
+ .project-row:not(:first-of-type) {
+ margin-top: -1px;
+ }
+
+ // Sizing of table columns
+ .project-row, .list-head {
+ div:first-of-type {
+ width: 50%;
+ }
+
+ div:nth-of-type(2) {
+ width: 30%;
+ }
+
+ div:last-of-type {
+ width: 20%;
+
+ span {
+ margin-right: 10px;
+ }
+ }
+ }
+
+ .project-row.active {
+ border-bottom: 0;
+ background: #3442b1;
+ color: #eee;
+ }
+
+ .project-row.active::before {
+ //noinspection CssNoGenericFontName
+ font-family: 'Glyphicons Halflings';
+ content: "\2212";
+ font-size: 14pt;
+ position: absolute;
+ top: 0;
+ right: 10px;
+ }
+
+ .project-view {
+ padding: 10px;
+ overflow: hidden;
+ border: 1px solid #b6b6b6;
+ border-top: 0;
+
+ background: #3442b1;
+ color: #eee;
+
+ .participants {
+ display: inline-block;
+ float: left;
+ }
+
+ .access-buttons {
+ display: inline-block;
+ float: right;
+
+ .inline-btn {
+ margin-left: 10px;
+ }
+
+ .open {
+ background: #e38829;
+ }
+
+ .open:hover {
+ background: #ff992e;
+ }
+
+ .open:active {
+ background: #ba6f21;
+ }
+
+ .edit {
+ background: #2c3897;
+ }
+
+ .edit:hover {
+ background: #3a4ac8;
+ }
+
+ .edit:active {
+ background: #242d7a;
+ }
+ }
+ }
+}
+
+.inline-btn {
+ display: inline-block;
+ height: 30px;
+ line-height: 30px;
+ font-size: 10pt;
+ width: 70px;
+
+ text-transform: uppercase;
+ text-align: center;
+
+ color: #eee;
+
+ .clickable;
+ .transition-def(background, @transition-length);
+}
+
+.projects-window {
+ width: 600px;
+ height: 400px;
+
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ margin: auto;
+
+ .border-radius-def(5px);
+ overflow: hidden;
+
+ .window-body {
+ width: 100%;
+ height: 340px;
+
+ padding: 10px;
+
+ background: rgba(230, 230, 230, 0.9);
+
+ .window-heading {
+ font-size: 20pt;
+ color: #000000;
+
+ .user-select-def;
+
+ font-weight: bold;
+
+ padding-left: 5px;
+ }
+
+ .project-name-form, .participant-add-form {
+ margin: 20px;
+
+ input {
+ width: 300px;
+ }
+
+ label {
+ padding-right: 5px;
+ width: 40px;
+ text-align: right;
+
+ .user-select-def;
+ }
+ }
+
+ .participants-table-label {
+ display: block;
+ padding-left: 20px;
+ margin-bottom: 5px;
+
+ .user-select-def;
+ }
+
+ .participants-table {
+ background: #ffffff;
+ .border-radius-def(5px);
+
+ margin: 0 20px;
+
+ max-height: 135px;
+ overflow: auto;
+
+ .participant-row {
+ height: 40px;
+ line-height: 40px;
+
+ div {
+ display: inline-block;
+ margin-right: -4px; // Address default margin between inline-blocks
+ }
+
+ .participant-name {
+ padding-left: 10px;
+ width: 60%;
+ }
+
+ .participant-level {
+ width: 30%;
+
+ .user-select-def;
+
+ div {
+ display: inline-block;
+
+ width: 25px;
+ height: 25px;
+
+ padding: 5px;
+
+ margin-right: 3px;
+
+ cursor: pointer;
+
+ .border-radius-def(5px);
+ .transition-def(all, @transition-length);
+ }
+
+ div.active, div:hover {
+ background-color: #415973;
+ color: #eeeeee;
+ }
+ }
+
+ .participant-remove {
+ position: relative;
+ width: 10%;
+ text-align: right;
+ padding-right: 10px;
+ }
+
+ .participant-remove div {
+ top: 2px;
+ right: 5px;
+ cursor: pointer;
+ }
+ }
+ }
+
+ .participant-add-form {
+ margin-top: 10px;
+ }
+
+ .participant-email-alert, .project-name-alert {
+ position: absolute;
+ bottom: 0;
+ left: 20px;
+ display: none;
+ }
+ }
+
+ .window-footer {
+ width: 100%;
+ height: 60px;
+
+ background: rgba(56, 56, 56, 0.9);
+
+ padding: 12px 10px;
+
+ .btn.pull-left {
+ margin-right: 5px;
+ }
+
+ .btn.pull-right {
+ margin-left: 5px;
+ }
+ }
+}
diff --git a/src/styles/splash.less b/src/styles/splash.less
new file mode 100644
index 00000000..7a88dd33
--- /dev/null
+++ b/src/styles/splash.less
@@ -0,0 +1,436 @@
+@screen-sm: 768px;
+@screen-md: 992px;
+@screen-lg: 1200px;
+
+html, body {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+}
+
+/* Fix for the white space appearing otherwise on the right of the page, on mobile */
+.body-wrapper {
+ overflow-x: hidden;
+ overflow-y: hidden;
+}
+
+/* NAVBAR */
+@media screen and (min-width: @screen-sm) {
+ .navbar {
+ padding: 20px 0;
+ -webkit-transition: background 200ms ease-in-out, padding 200ms ease-in-out;
+ -moz-transition: background 200ms ease-in-out, padding 200ms ease-in-out;
+ transition: background 200ms ease-in-out, padding 200ms ease-in-out;
+ }
+
+ .top-nav-collapse {
+ padding: 0;
+ }
+}
+
+.navbar {
+ text-transform: uppercase;
+}
+
+.navbar-transparent {
+ background: transparent;
+ border: none;
+
+ .collapse li a, .projects-btn {
+ color: #eee;
+ }
+}
+
+.navbar-toggle {
+ margin-right: 30px;
+}
+
+.navbar-fixed-top {
+ padding: 0;
+}
+
+.navbar .logged-in {
+ display: none;
+}
+
+.sign-out {
+ padding: 10px;
+ width: 40px;
+ margin-top: 5px;
+ margin-left: 5px;
+
+ font-size: 14pt;
+ color: #eeeeee;
+
+ -webkit-border-radius: 25px;
+ -moz-border-radius: 25px;
+ border-radius: 25px;
+
+ -webkit-transition: background 200ms;
+ -moz-transition: background 200ms;
+ -ms-transition: background 200ms;
+ -o-transition: background 200ms;
+ transition: background 200ms;
+}
+
+.sign-out:hover {
+ background: #e3474d;
+ color: #eeeeee;
+}
+
+.sign-out:active {
+ background: #a73438;
+}
+
+.projects-btn {
+ position: relative;
+ top: -4px;
+ color: #eee;
+}
+
+.projects-btn:hover {
+ color: #fff;
+}
+
+a.navbar-brand {
+ width: 40px;
+ height: 40px;
+ padding: 0;
+ margin-right: 5px;
+ margin-top: 5px;
+
+ -webkit-border-radius: 20px;
+ -moz-border-radius: 20px;
+ border-radius: 20px;
+
+ -webkit-transition: background-color 200ms ease-in-out;
+ -moz-transition: background-color 200ms ease-in-out;
+ -ms-transition: background-color 200ms ease-in-out;
+ -o-transition: background-color 200ms ease-in-out;
+ transition: background-color 200ms ease-in-out;
+}
+
+a.navbar-brand:hover {
+ background: #eee !important;
+}
+
+a.navbar-brand:active, a.navbar-brand:visited, .active a.navbar-brand {
+ background: #ccc !important;
+}
+
+.navbar-brand > img {
+ padding: 5px;
+ margin: 0;
+ width: auto;
+ height: 100%;
+}
+
+@media screen and (max-width: @screen-sm) {
+ .navbar-brand {
+ margin-left: 15px;
+ }
+}
+
+#google-signin {
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
+
+/* GENERAL CONTENT RULES */
+a {
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: none;
+}
+
+.content-section {
+ padding-top: 50px;
+ padding-bottom: 150px;
+ text-align: center;
+}
+
+.img-caption {
+ margin-top: 10px;
+ color: #666;
+}
+
+@media screen and (min-width: @screen-sm) and (max-width: @screen-md) {
+ .content-section h1 {
+ font-size: 2em;
+ margin-bottom: 40px;
+ }
+
+ .content-section h3 {
+ font-size: 1.5em;
+ }
+}
+
+@media screen and (min-width: @screen-md) and (max-width: @screen-lg) {
+ .content-section h1 {
+ font-size: 3em;
+ margin-bottom: 40px;
+ }
+
+ .content-section h3 {
+ font-size: 1.8em;
+ }
+}
+
+@media screen and (min-width: @screen-lg) {
+ .content-section h1 {
+ font-size: 3em;
+ margin-bottom: 40px;
+ }
+}
+
+.info-points {
+ font-size: 1.2em;
+}
+
+@media screen and (min-width: @screen-sm) and (max-width: @screen-md) {
+ .info-points {
+ font-size: 1em;
+ }
+}
+
+@media screen and (min-width: @screen-md) and (max-width: @screen-lg) {
+ .info-points {
+ font-size: 1.1em;
+ }
+}
+
+.pitch-column {
+ text-align: left;
+}
+
+.jumbotron ul, .content-section ul {
+ list-style: none;
+ padding-left: 25px;
+}
+
+.jumbotron ul li, .content-section ul li {
+ display: block;
+}
+
+.jumbotron ul li:before, .content-section ul li:before {
+ content: "\e080";
+ font-family: 'Glyphicons Halflings', monospace;
+ font-size: 10px;
+ float: left;
+ margin-top: 6px;
+ margin-left: -17px;
+ color: #aaa;
+}
+
+/* CONTENT SECTION COLORS */
+.intro-section {
+ background-color: #fff;
+}
+
+.stakeholder-section {
+ background-color: #f2f2f2;
+}
+
+.modeling-section {
+ background-color: #fff;
+}
+
+.simulation-section {
+ background-color: #f2f2f2;
+}
+
+.technologies-section {
+ background-color: #fff;
+}
+
+.team-section {
+ background-color: #f2f2f2;
+}
+
+.contact-section {
+ background-color: #444;
+}
+
+/* HEADING SECTION */
+.header-section {
+ background-image: linear-gradient(135deg, #1c2e48, #005bbf, #00c5d6);
+}
+
+.jumbotron {
+ margin: 140px 0 120px 0;
+ background-color: inherit;
+}
+
+.jumbotron h1 {
+ color: #eee;
+}
+
+.jumbotron h1 .dc {
+ color: #eee;
+ font-weight: bold;
+}
+
+.jumbotron h2 {
+ margin-top: 50px;
+ color: #eee;
+
+}
+
+/* INTRO SECTION*/
+.intro-section {
+ padding-top: 0;
+ padding-bottom: 50px;
+ text-align: center;
+}
+
+.pitch-container {
+ margin-top: 50px;
+}
+
+p.img-source {
+ font-size: 0.6em;
+ position: relative;
+ top: 20px;
+}
+
+@media screen and (max-width: @screen-sm) {
+ p.img-source {
+ top: 0;
+ }
+}
+
+/* STAKEHOLDERS */
+@media screen and (max-width: @screen-sm) {
+ .stakeholder-section img {
+ position: relative;
+ top: 15px;
+ }
+}
+
+@media screen and (min-width: @screen-sm) and (max-width: @screen-md) {
+ .stakeholder-section img {
+ position: relative;
+ top: 5px;
+ }
+
+ .stakeholder-section h3 {
+ font-size: 1.5em;
+ }
+}
+
+@media screen and (min-width: @screen-md) and (max-width: @screen-lg) {
+ .stakeholder-section img {
+ position: relative;
+ top: 15px;
+ }
+
+ .stakeholder-section h3 {
+ font-size: 1.7em;
+ }
+}
+
+.stakeholder-container div {
+ text-align: left;
+}
+
+/* MOCKUPS */
+.construction-container {
+ margin-top: 20px;
+ margin-bottom: 60px;
+}
+
+.simulation-container > div:first-child {
+ margin-bottom: 30px;
+}
+
+.img-construction-building {
+ margin-top: 10px;
+}
+
+.modeling-section img, .simulation-section img {
+ outline: 2px solid #3f3f3f;
+ padding-right: 0;
+ padding-left: 0;
+}
+
+.simulation-building-row {
+ margin-bottom: 40px;
+}
+
+.key-points {
+ padding-right: 50px;
+}
+
+/* TECHNOLOGIES */
+.web-flow {
+ font-size: 4em;
+}
+
+.technologies-section {
+ h3 {
+ margin-top: 0;
+ }
+
+ .tech-row {
+ padding: 15px;
+
+ -webkit-border-radius: 10px;
+ -moz-border-radius: 10px;
+ border-radius: 10px;
+
+ margin-bottom: 10px;
+ }
+
+ .browser-tech {
+ background-color: #82d0e7;
+ }
+
+ .server-tech {
+ background-color: #edd667;
+ }
+
+ .database-tech {
+ background-color: #ed9c67;
+ }
+
+ .simulator-tech {
+ background-color: #49d65f;
+ }
+}
+
+.technology-arrow {
+ margin-bottom: 20px;
+ margin-top: 15px;
+}
+
+/* TEAM */
+.team-member-description {
+ margin-bottom: 30px;
+}
+
+/* CONTACT */
+.contact-section {
+ margin-bottom: -5px; // Fixes an unwanted margin that appeared when adding the sign-in button to the nav
+
+ h1 {
+ color: #ddd;
+ }
+
+ .names {
+ color: #ddd;
+ }
+
+ a {
+ color: #ddd;
+ }
+
+ a:hover {
+ color: #fff;
+ }
+}
+
+.tudelft-icon {
+ margin-bottom: 10px;
+} \ No newline at end of file
diff --git a/src/unit-tests.html b/src/unit-tests.html
new file mode 100644
index 00000000..877bda6d
--- /dev/null
+++ b/src/unit-tests.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<!--suppress HtmlUnknownTarget -->
+<html>
+<head>
+ <meta http-equiv="content-type" content="text/html;charset=utf-8">
+ <title>OpenDC Unit Tests</title>
+ <link rel="stylesheet" href="../node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
+ <script src="../node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
+ <script src="../node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
+ <script src="../node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
+</head>
+<body>
+<script src="scripts/test.entry.js"></script>
+</body>
+</html> \ No newline at end of file