summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--modules-available/locations/page.inc.php2
-rw-r--r--modules-available/roomplanner/api.inc.php3
-rw-r--r--modules-available/roomplanner/inc/composedroom.inc.php77
-rw-r--r--modules-available/roomplanner/inc/pvsgenerator.inc.php7
-rw-r--r--modules-available/roomplanner/page.inc.php169
-rw-r--r--modules-available/roomplanner/style.css35
-rw-r--r--modules-available/roomplanner/templates/edit-composed-room.html150
-rw-r--r--modules-available/roomplanner/templates/svg-plan.html2
8 files changed, 402 insertions, 43 deletions
diff --git a/modules-available/locations/page.inc.php b/modules-available/locations/page.inc.php
index 5b3d7ff0..658b4b18 100644
--- a/modules-available/locations/page.inc.php
+++ b/modules-available/locations/page.inc.php
@@ -545,7 +545,7 @@ class Page_Locations extends Page
'locationid' => $loc['locationid'],
'locationname' => $loc['locationname'],
'list' => $rows,
- 'roomplanner' => Module::get('roomplanner') !== false && Location::isLeaf($locationId),
+ 'roomplanner' => Module::get('roomplanner') !== false,
'parents' => Location::getLocations($loc['parentlocationid'], $locationId, true)
);
diff --git a/modules-available/roomplanner/api.inc.php b/modules-available/roomplanner/api.inc.php
index 055c6b2e..f964bea1 100644
--- a/modules-available/roomplanner/api.inc.php
+++ b/modules-available/roomplanner/api.inc.php
@@ -3,7 +3,8 @@
if (Request::any('show') === 'svg') {
$ret = PvsGenerator::generateSvg(Request::any('locationid', false, 'int'),
Request::any('machineuuid', false, 'string'),
- Request::any('rotate', 0, 'int'));
+ Request::any('rotate', 0, 'int'),
+ Request::any('scale', 1, 'float'));
if ($ret === false) {
Header('HTTP/1.1 404 Not Found');
exit;
diff --git a/modules-available/roomplanner/inc/composedroom.inc.php b/modules-available/roomplanner/inc/composedroom.inc.php
new file mode 100644
index 00000000..11e8455e
--- /dev/null
+++ b/modules-available/roomplanner/inc/composedroom.inc.php
@@ -0,0 +1,77 @@
+<?php
+
+class ComposedRoom
+{
+ /**
+ * @var string How to compose contained rooms. Value is either horizontal or vertical.
+ */
+ public $orientation = 'horizontal';
+
+ /**
+ * @var int[] Order in which contained rooms are composed. List of locationid.
+ */
+ public $list;
+
+ /**
+ * @var bool Whether composed room is active, ie. visible in PVS.
+ */
+ public $enabled;
+
+ /**
+ * @var int locationid of contained room that is the controlling room;
+ */
+ public $controlRoom;
+
+ public function __construct($data)
+ {
+ if ($data instanceof ComposedRoom) {
+ foreach ($data as $k => $v) {
+ $this->{$k} = $v;
+ }
+ } else {
+ if (is_array($data) && isset($data['roomplan'])) {
+ // From DB
+ $data = json_decode($data['roomplan'], true);
+ } elseif (is_string($data)) {
+ // Just JSON
+ $data = json_decode($data, true);
+ }
+ if (is_array($data)) {
+ foreach ($this as $k => $v) {
+ if (isset($data[$k])) {
+ $this->{$k} = $data[$k];
+ }
+ }
+ }
+ }
+ $this->sanitize();
+ }
+
+ /**
+ * Make sure all member vars have the proper type
+ */
+ private function sanitize()
+ {
+ $this->orientation = ($this->orientation === 'horizontal' ? 'horizontal' : 'vertical');
+ settype($this->enabled, 'bool');
+ settype($this->list, 'array');
+ settype($this->controlRoom, 'int');
+ foreach ($this->list as &$v) {
+ settype($v, 'int');
+ }
+ $this->list = array_values($this->list);
+ if (!empty($this->list) && !in_array($this->controlRoom, $this->list)) {
+ $this->controlRoom = $this->list[0];
+ }
+ }
+
+ /**
+ * @return false|string JSON
+ */
+ public function serialize()
+ {
+ $this->sanitize();
+ return json_encode($this);
+ }
+
+}
diff --git a/modules-available/roomplanner/inc/pvsgenerator.inc.php b/modules-available/roomplanner/inc/pvsgenerator.inc.php
index 2a4e2972..cfb38fd2 100644
--- a/modules-available/roomplanner/inc/pvsgenerator.inc.php
+++ b/modules-available/roomplanner/inc/pvsgenerator.inc.php
@@ -156,7 +156,7 @@ class PvsGenerator
* @param int $rotate rotate plan (0-3 for N E S W up, -1 for "auto" if highlightUuid is given)
* @return string SVG
*/
- public static function generateSvg($locationId = false, $highlightUuid = false, $rotate = 0)
+ public static function generateSvg($locationId = false, $highlightUuid = false, $rotate = 0, $scale = 1)
{
if ($locationId === false) {
$locationId = Database::queryFirst('SELECT fixedlocationid FROM machine
@@ -215,8 +215,9 @@ class PvsGenerator
self::swap($sizeX, $sizeY);
}
return Render::parse('svg-plan', [
- 'width' => $sizeX,
- 'height' => $sizeY,
+ 'scale' => $scale,
+ 'width' => $sizeX * $scale,
+ 'height' => $sizeY * $scale,
'centerX' => $centerX,
'centerY' => $centerY,
'rotate' => $rotate * 90,
diff --git a/modules-available/roomplanner/page.inc.php b/modules-available/roomplanner/page.inc.php
index 8b75499b..d1543a9e 100644
--- a/modules-available/roomplanner/page.inc.php
+++ b/modules-available/roomplanner/page.inc.php
@@ -18,11 +18,20 @@ class Page_Roomplanner extends Page
*/
private $action = false;
+ /**
+ * @var bool is this a leaf node, with a real room plan, or something in between with a composed plan
+ */
+ private $isLeaf;
+
private function loadRequestedLocation()
{
$this->locationid = Request::get('locationid', false, 'integer');
if ($this->locationid !== false) {
- $this->location = Location::get($this->locationid);
+ $locs = Location::getLocationsAssoc();
+ if (isset($locs[$this->locationid])) {
+ $this->location = $locs[$this->locationid];
+ $this->isLeaf = empty($this->location['children']);
+ }
}
}
@@ -58,48 +67,93 @@ class Page_Roomplanner extends Page
if ($this->action === 'show') {
/* do nothing */
Dashboard::disable();
- $config = Database::queryFirst('SELECT roomplan, managerip, tutoruuid FROM location_roomplan WHERE locationid = :locationid', ['locationid' => $this->locationid]);
- $runmode = RunMode::getForMode(Page::getModule(), $this->locationid, true);
- if (empty($runmode)) {
- $config['dedicatedmgr'] = false;
- } else {
- $runmode = array_pop($runmode);
- $config['managerip'] = $runmode['clientip'];
- $config['manageruuid'] = $runmode['machineuuid'];
- $data = json_decode($runmode['modedata'], true);
- $config['dedicatedmgr'] = (isset($data['dedicatedmgr']) && $data['dedicatedmgr']);
- }
- if ($config !== false) {
- $managerIp = $config['managerip'];
- $dediMgr = $config['dedicatedmgr'] ? 'checked' : '';
+ if ($this->isLeaf) {
+ $this->showLeafEditor();
} else {
- $dediMgr = $managerIp = '';
+ $this->showComposedEditor();
}
- $furniture = $this->getFurniture($config);
- $subnetMachines = $this->getPotentialMachines();
- $machinesOnPlan = $this->getMachinesOnPlan($config['tutoruuid']);
- $roomConfig = array_merge($furniture, $machinesOnPlan);
- $canEdit = User::hasPermission('edit', $this->locationid);
- $params = [
- 'location' => $this->location,
- 'managerip' => $managerIp,
- 'dediMgrChecked' => $dediMgr,
- 'subnetMachines' => json_encode($subnetMachines),
- 'locationid' => $this->locationid,
- 'roomConfiguration' => json_encode($roomConfig),
- 'edit_disabled' => $canEdit ? '' : 'disabled',
- 'statistics_disabled' => Module::get('statistics') !== false && User::hasPermission('.statistics.machine.view-details') ? '' : 'disabled',
- ];
- Render::addTemplate('header', $params);
- if ($canEdit) {
- Render::addTemplate('item-selector', $params);
- }
- Render::addTemplate('main-roomplan', $params);
- Render::addTemplate('footer', $params);
} else {
Message::addError('main.invalid-action', $this->action);
}
+ }
+
+ private function showLeafEditor()
+ {
+ $config = Database::queryFirst('SELECT roomplan, managerip, tutoruuid FROM location_roomplan WHERE locationid = :locationid', ['locationid' => $this->locationid]);
+ $runmode = RunMode::getForMode(Page::getModule(), $this->locationid, true);
+ if (empty($runmode)) {
+ $config['dedicatedmgr'] = false;
+ } else {
+ $runmode = array_pop($runmode);
+ $config['managerip'] = $runmode['clientip'];
+ $config['manageruuid'] = $runmode['machineuuid'];
+ $data = json_decode($runmode['modedata'], true);
+ $config['dedicatedmgr'] = (isset($data['dedicatedmgr']) && $data['dedicatedmgr']);
+ }
+ if ($config !== false) {
+ $managerIp = $config['managerip'];
+ $dediMgr = $config['dedicatedmgr'] ? 'checked' : '';
+ } else {
+ $dediMgr = $managerIp = '';
+ }
+ $furniture = $this->getFurniture($config);
+ $subnetMachines = $this->getPotentialMachines();
+ $machinesOnPlan = $this->getMachinesOnPlan($config['tutoruuid']);
+ $roomConfig = array_merge($furniture, $machinesOnPlan);
+ $canEdit = User::hasPermission('edit', $this->locationid);
+ $params = [
+ 'location' => $this->location,
+ 'managerip' => $managerIp,
+ 'dediMgrChecked' => $dediMgr,
+ 'subnetMachines' => json_encode($subnetMachines),
+ 'locationid' => $this->locationid,
+ 'roomConfiguration' => json_encode($roomConfig),
+ 'edit_disabled' => $canEdit ? '' : 'disabled',
+ 'statistics_disabled' => Module::get('statistics') !== false && User::hasPermission('.statistics.machine.view-details') ? '' : 'disabled',
+ ];
+ Render::addTemplate('header', $params);
+ if ($canEdit) {
+ Render::addTemplate('item-selector', $params);
+ }
+ Render::addTemplate('main-roomplan', $params);
+ Render::addTemplate('footer', $params);
+ }
+ private function showComposedEditor()
+ {
+ // Load settings
+ $row = Database::queryFirst("SELECT roomplan FROM location_roomplan WHERE locationid = :lid", [
+ 'lid' => $this->locationid,
+ ]);
+ $room = new ComposedRoom($row);
+ $params = [
+ 'location' => $this->location,
+ 'locations' => [],
+ $room->orientation . '_checked' => 'checked',
+ ];
+ if ($room->enabled) {
+ $params['enabled_checked'] = 'checked';
+ }
+ $inverseList = array_flip($room->list);
+ $sortList = [];
+ // Load locations
+ $locs = Location::getLocationsAssoc();
+ foreach ($this->location['children'] as $loc) {
+ if (isset($locs[$loc])) {
+ $data = $locs[$loc];
+ if (isset($inverseList[$loc])) {
+ $sortList[] = $inverseList[$loc];
+ } else {
+ $sortList[] = 1000 + $loc;
+ }
+ if ($loc === $room->controlRoom) {
+ $data['checked'] = 'checked';
+ }
+ $params['locations'][] = $data;
+ }
+ }
+ array_multisort($sortList, SORT_ASC | SORT_NUMERIC, $params['locations']);
+ Render::addTemplate('edit-composed-room', $params);
}
protected function doAjax()
@@ -160,6 +214,25 @@ class Page_Roomplanner extends Page
private function handleSaveRequest($isAjax)
{
User::assertPermission('edit', $this->locationid);
+ $leaf = (bool)Request::post('isleaf', 1, 'int');
+ if ($leaf !== $this->isLeaf) {
+ if ($isAjax) {
+ die('Leaf mode mismatch. Did you restructure locations while editing this room?');
+ } else {
+ Message::addError('leaf-mode-mismatch');
+ Util::redirect("?do=roomplanner&locationid={$this->locationid}&action=show");
+ }
+ return;
+ }
+ if ($this->isLeaf) {
+ $this->saveLeafRoom($isAjax);
+ } else {
+ $this->saveComposedRoom($isAjax);
+ }
+ }
+
+ private function saveLeafRoom($isAjax)
+ {
$machinesOnPlan = $this->getMachinesOnPlan('invalid');
$config = Request::post('serializedRoom', null, 'string');
$config = json_decode($config, true);
@@ -189,6 +262,28 @@ class Page_Roomplanner extends Page
$this->saveComputerConfig($config['computers'], $machinesOnPlan);
}
+ private function saveComposedRoom($isAjax)
+ {
+ $room = new ComposedRoom(null);
+ $room->orientation = Request::post('orientation', 'horizontal', 'string');
+ $room->enabled = (bool)Request::post('enabled', 0, 'int');
+ $room->controlRoom = Request::post('controlroom', 0, 'int');
+ $vals = Request::post('sort', [], 'array');
+ asort($vals, SORT_ASC | SORT_NUMERIC);
+ $room->list = array_keys($vals);
+ $res = Database::exec('INSERT INTO location_roomplan (locationid, roomplan)
+ VALUES (:lid, :plan) ON DUPLICATE KEY UPDATE roomplan = VALUES(roomplan)',
+ ['lid' => $this->locationid, 'plan' => $room->serialize()]);
+ if (!$res) {
+ if ($isAjax) {
+ die('Error writing config to DB');
+ } else {
+ Message::addError('db-error');
+ Util::redirect("?do=roomplanner&locationid={$this->locationid}&action=show");
+ }
+ }
+ }
+
private function sanitizeNumber(&$number, $lower, $upper)
{
if (!is_numeric($number) || $number < $lower) {
diff --git a/modules-available/roomplanner/style.css b/modules-available/roomplanner/style.css
index f1dd994a..8f516465 100644
--- a/modules-available/roomplanner/style.css
+++ b/modules-available/roomplanner/style.css
@@ -23,6 +23,41 @@ body {
/* end full screen changes */
+/* sort mode for composed rooms */
+
+#roomsort div.img {
+ border: none;
+}
+
+div.box1 {
+ padding: 2px;
+}
+
+div.horizontal div.box1 {
+ float: left;
+ display: inline-table;
+}
+
+div.horizontal div.name {
+ width: 5px;
+ overflow: visible;
+ height: 1em;
+}
+
+div.vertical div.box2 {
+ float: left;
+}
+
+div.vertical div.name {
+ float: right;
+}
+
+div.vertical div.img {
+ float: left;
+}
+
+/* end sort mode for composed rooms */
+
#drawpanel {
position:relative;}
diff --git a/modules-available/roomplanner/templates/edit-composed-room.html b/modules-available/roomplanner/templates/edit-composed-room.html
new file mode 100644
index 00000000..64a02d61
--- /dev/null
+++ b/modules-available/roomplanner/templates/edit-composed-room.html
@@ -0,0 +1,150 @@
+<h1>{{lang_editComposedRoom}}</h1>
+<h2>{{location.locationname}}</h2>
+
+<form id="main-form" method="post" action="?do=roomplanner&amp;locationid={{location.locationid}}">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="isleaf" value="0">
+ <input type="hidden" name="action" value="save">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <div class="checkbox">
+ <input id="check-enable" type="checkbox" name="enabled" value="1" {{enabled_checked}}>
+ <label for="check-enable">{{lang_exposeAsComposedRoom}}</label>
+ </div>
+ </div>
+ <div class="panel-body">
+ <div id="main-controls">
+ <h4>{{lang_composedLayout}}</h4>
+ <div class="radio radio-inline">
+ <input id="type-horz" type="radio" name="orientation" value="horizontal" {{horizontal_checked}}>
+ <label for="type-horz">{{lang_horizontal}}</label>
+ </div>
+ <div class="radio radio-inline">
+ <input id="type-vert" type="radio" name="orientation" value="vertical" {{vertical_checked}}>
+ <label for="type-vert">{{lang_vertical}}</label>
+ </div>
+ <br><br>
+ <div id="roomsort">
+ {{#locations}}
+ <div class="box1">
+ <div class="box2">
+ <div class="name text-nowrap small">{{locationname}}</div>
+ <div class="img">
+ <input type="hidden" class="sort-val" name="sort[{{locationid}}]">
+ <img src="api.php?do=roomplanner&amp;show=svg&amp;locationid={{locationid}}&amp;scale=2.2">
+ </div>
+ <div class="clearfix"></div>
+ </div>
+ <div class="clearfix"></div>
+ </div>
+ {{/locations}}
+ </div>
+ <div class="clearfix"></div>
+ <br>
+ <h4>{{lang_controllingRoom}}</h4>
+ <p>{{lang_controlRoomDesc}}</p>
+ {{#locations}}
+ <div>
+ <div class="radio">
+ <input id="control-{{locationid}}" type="radio" name="controlroom"
+ value="{{locationid}}" {{checked}}>
+ <label for="control-{{locationid}}">{{locationname}}</label>
+ </div>
+ </div>
+ {{/locations}}
+ </div>
+ <div class="buttonbar pull-right">
+ <button type="button" class="btn btn-default" id="btn-cancel">
+ {{lang_cancel}}
+ </button>
+ <button id="btn-save" type="submit" class="btn btn-primary">
+ <span class="glyphicon glyphicon-floppy-disk"></span>
+ {{lang_save}}
+ </button>
+ </div>
+ <div class="clearfix"></div>
+ <div class="alert alert-danger" style="display:none" id="error-msg"></div>
+ <div class="alert alert-success" style="display:none" id="success-msg">{{lang_planSuccessfullySaved}}</div>
+ <div class="alert alert-info" style="display:none" id="saving-msg">{{lang_planBeingSaved}}</div>
+ </div>
+ </div>
+</form>
+
+<script>
+ document.addEventListener('DOMContentLoaded', function () {
+
+ var reassignSortValues = function () {
+ var startValue = 1;
+ $('.sort-val').each(function (index, element) {
+ element.value = startValue * 10;
+ startValue++;
+ });
+ };
+
+ var $rs = $('#roomsort');
+ var $mc = $('#main-controls');
+
+ $rs.disableSelection().sortable({
+ opacity: 0.8,
+ start: function (evt, ui) {
+ ui.placeholder.css("visibility", "visible");
+ ui.placeholder.css("opacity", "0.352");
+ ui.placeholder.css("background-color", "#ddd");
+ },
+ stop: reassignSortValues
+ });
+
+ var setLayout = function () {
+ $rs.removeClass('horizontal vertical').addClass($('input[name=orientation]:checked').val());
+ };
+ $('input[name=orientation]').change(setLayout);
+
+ $('#btn-cancel').click(function () {
+ window.close();
+ });
+
+ var $ce = $('#check-enable');
+
+ var checkEnable = function () {
+ if ($ce.is(':checked')) {
+ $mc.show();
+ } else {
+ $mc.hide();
+ }
+ };
+
+ $ce.change(checkEnable);
+
+ var $mf = $('#main-form');
+ var $sb = $('#btn-save');
+ var success = false;
+ $sb.click(function(e) {
+ $sb.prop('disabled', true);
+ $('#error-msg').hide();
+ $('#success-msg').hide();
+ $('#saving-msg').show();
+ var str = $mf.serialize();
+ $.post($mf.attr('action'), str).done(function (data) {
+ if (data.indexOf('SUCCESS') !== -1) {
+ window.close();
+ // If window.close() failed, we give some feedback and remember the state as saved
+ $('#success-msg').show();
+ success = true;
+ return;
+ }
+ $('#error-msg').text('Error: ' + data).show();
+ }).fail(function (jq, textStatus, errorThrown) {
+ $('#error-msg').text('AJAX save call failed: ' + textStatus + ' (' + errorThrown + ')').show();
+ }).always(function() {
+ $sb.prop('disabled', success);
+ $('#saving-msg').hide();
+ });
+ e.preventDefault();
+ });
+
+ setLayout();
+ reassignSortValues();
+ checkEnable();
+
+ });
+</script> \ No newline at end of file
diff --git a/modules-available/roomplanner/templates/svg-plan.html b/modules-available/roomplanner/templates/svg-plan.html
index 072efbff..16899e5c 100644
--- a/modules-available/roomplanner/templates/svg-plan.html
+++ b/modules-available/roomplanner/templates/svg-plan.html
@@ -33,7 +33,7 @@
<stop offset="100%" stop-color="#074" />
</radialGradient>
</defs>
- <g transform="rotate({{rotate}} {{centerX}} {{centerY}}) translate({{shiftX}} {{shiftY}})">
+ <g transform="scale({{scale}}) rotate({{rotate}} {{centerX}} {{centerY}}) translate({{shiftX}} {{shiftY}})">
<line x1="{{line.x1}}" y1="{{line.y1}}"
x2="{{line.x2}}" y2="{{line.y2}}"
style="stroke:#555;stroke-width:.2;opacity:.5" />