diff options
| author | Georgios Andreadis <g.andreadis@student.tudelft.nl> | 2017-01-24 12:06:09 +0100 |
|---|---|---|
| committer | Georgios Andreadis <g.andreadis@student.tudelft.nl> | 2017-01-24 12:06:09 +0100 |
| commit | c96e6ffafb62bde1e08987b1fdf3c0786487f6ec (patch) | |
| tree | 37eaf4cf199ca77dc131b4212c526b707adf2e30 /src | |
Initial commit
Diffstat (limited to 'src')
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">×</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 & 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 Binary files differnew file mode 100644 index 00000000..c2f40a0d --- /dev/null +++ b/src/favicon.ico 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 Binary files differnew file mode 100644 index 00000000..16c18be0 --- /dev/null +++ b/src/img/app/coolingitem.png diff --git a/src/img/app/loading.gif b/src/img/app/loading.gif Binary files differnew file mode 100644 index 00000000..c6394822 --- /dev/null +++ b/src/img/app/loading.gif diff --git a/src/img/app/node-cpu.png b/src/img/app/node-cpu.png Binary files differnew file mode 100644 index 00000000..07cfbd31 --- /dev/null +++ b/src/img/app/node-cpu.png diff --git a/src/img/app/node-gpu.png b/src/img/app/node-gpu.png Binary files differnew file mode 100644 index 00000000..55d4fb05 --- /dev/null +++ b/src/img/app/node-gpu.png diff --git a/src/img/app/node-memory.png b/src/img/app/node-memory.png Binary files differnew file mode 100644 index 00000000..36e8a44e --- /dev/null +++ b/src/img/app/node-memory.png diff --git a/src/img/app/node-network.png b/src/img/app/node-network.png Binary files differnew file mode 100644 index 00000000..795e173b --- /dev/null +++ b/src/img/app/node-network.png diff --git a/src/img/app/node-storage.png b/src/img/app/node-storage.png Binary files differnew file mode 100644 index 00000000..7a39cb6f --- /dev/null +++ b/src/img/app/node-storage.png diff --git a/src/img/app/psu.png b/src/img/app/psu.png Binary files differnew file mode 100644 index 00000000..471af6ee --- /dev/null +++ b/src/img/app/psu.png diff --git a/src/img/app/rack-energy.png b/src/img/app/rack-energy.png Binary files differnew file mode 100644 index 00000000..1088c61b --- /dev/null +++ b/src/img/app/rack-energy.png diff --git a/src/img/app/rack-space.png b/src/img/app/rack-space.png Binary files differnew file mode 100644 index 00000000..387d7ea6 --- /dev/null +++ b/src/img/app/rack-space.png diff --git a/src/img/datacenter-drawing.png b/src/img/datacenter-drawing.png Binary files differnew file mode 100644 index 00000000..401168e3 --- /dev/null +++ b/src/img/datacenter-drawing.png diff --git a/src/img/email-icon.png b/src/img/email-icon.png Binary files differnew file mode 100644 index 00000000..ced9e175 --- /dev/null +++ b/src/img/email-icon.png diff --git a/src/img/favicon.png b/src/img/favicon.png Binary files differnew file mode 100644 index 00000000..85d74964 --- /dev/null +++ b/src/img/favicon.png diff --git a/src/img/github-icon.png b/src/img/github-icon.png Binary files differnew file mode 100644 index 00000000..1e221600 --- /dev/null +++ b/src/img/github-icon.png diff --git a/src/img/logo.png b/src/img/logo.png Binary files differnew file mode 100644 index 00000000..d743038b --- /dev/null +++ b/src/img/logo.png diff --git a/src/img/mockups/construction-node.png b/src/img/mockups/construction-node.png Binary files differnew file mode 100644 index 00000000..78ad81e8 --- /dev/null +++ b/src/img/mockups/construction-node.png diff --git a/src/img/mockups/simulation-node.png b/src/img/mockups/simulation-node.png Binary files differnew file mode 100644 index 00000000..fc56f44a --- /dev/null +++ b/src/img/mockups/simulation-node.png diff --git a/src/img/mockups/simulation-room.png b/src/img/mockups/simulation-room.png Binary files differnew file mode 100644 index 00000000..f8f80623 --- /dev/null +++ b/src/img/mockups/simulation-room.png diff --git a/src/img/opendc-splash.png b/src/img/opendc-splash.png Binary files differnew file mode 100644 index 00000000..99fd8658 --- /dev/null +++ b/src/img/opendc-splash.png diff --git a/src/img/portraits/aiosup.png b/src/img/portraits/aiosup.png Binary files differnew file mode 100644 index 00000000..30de349c --- /dev/null +++ b/src/img/portraits/aiosup.png diff --git a/src/img/portraits/gandreadis.png b/src/img/portraits/gandreadis.png Binary files differnew file mode 100644 index 00000000..403870fa --- /dev/null +++ b/src/img/portraits/gandreadis.png diff --git a/src/img/portraits/loverweel.png b/src/img/portraits/loverweel.png Binary files differnew file mode 100644 index 00000000..d12a9e86 --- /dev/null +++ b/src/img/portraits/loverweel.png diff --git a/src/img/portraits/mbijman.png b/src/img/portraits/mbijman.png Binary files differnew file mode 100644 index 00000000..decf9fdd --- /dev/null +++ b/src/img/portraits/mbijman.png diff --git a/src/img/stakeholders/Developer.png b/src/img/stakeholders/Developer.png Binary files differnew file mode 100644 index 00000000..d2638e6c --- /dev/null +++ b/src/img/stakeholders/Developer.png diff --git a/src/img/stakeholders/Manager.png b/src/img/stakeholders/Manager.png Binary files differnew file mode 100644 index 00000000..92db7459 --- /dev/null +++ b/src/img/stakeholders/Manager.png diff --git a/src/img/stakeholders/Researcher.png b/src/img/stakeholders/Researcher.png Binary files differnew file mode 100644 index 00000000..d87edd39 --- /dev/null +++ b/src/img/stakeholders/Researcher.png diff --git a/src/img/stakeholders/Sales.png b/src/img/stakeholders/Sales.png Binary files differnew file mode 100644 index 00000000..5b7c3a72 --- /dev/null +++ b/src/img/stakeholders/Sales.png diff --git a/src/img/stakeholders/Student.png b/src/img/stakeholders/Student.png Binary files differnew file mode 100644 index 00000000..a4900303 --- /dev/null +++ b/src/img/stakeholders/Student.png diff --git a/src/img/technologies/arrow.png b/src/img/technologies/arrow.png Binary files differnew file mode 100644 index 00000000..374f78bf --- /dev/null +++ b/src/img/technologies/arrow.png diff --git a/src/img/technologies/cogs-icon.png b/src/img/technologies/cogs-icon.png Binary files differnew file mode 100644 index 00000000..d19e1c20 --- /dev/null +++ b/src/img/technologies/cogs-icon.png diff --git a/src/img/technologies/database-icon.png b/src/img/technologies/database-icon.png Binary files differnew file mode 100644 index 00000000..26738e76 --- /dev/null +++ b/src/img/technologies/database-icon.png diff --git a/src/img/technologies/webserver-icon.png b/src/img/technologies/webserver-icon.png Binary files differnew file mode 100644 index 00000000..c627106e --- /dev/null +++ b/src/img/technologies/webserver-icon.png diff --git a/src/img/technologies/www-icon.png b/src/img/technologies/www-icon.png Binary files differnew file mode 100644 index 00000000..e69a54f2 --- /dev/null +++ b/src/img/technologies/www-icon.png diff --git a/src/img/tudelfticon.png b/src/img/tudelfticon.png Binary files differnew file mode 100644 index 00000000..a7a2d56a --- /dev/null +++ b/src/img/tudelfticon.png 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">×</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 |
