diff options
-rw-r--r-- | modules-available/locations/page.inc.php | 2 | ||||
-rw-r--r-- | modules-available/roomplanner/api.inc.php | 3 | ||||
-rw-r--r-- | modules-available/roomplanner/inc/composedroom.inc.php | 77 | ||||
-rw-r--r-- | modules-available/roomplanner/inc/pvsgenerator.inc.php | 7 | ||||
-rw-r--r-- | modules-available/roomplanner/page.inc.php | 169 | ||||
-rw-r--r-- | modules-available/roomplanner/style.css | 35 | ||||
-rw-r--r-- | modules-available/roomplanner/templates/edit-composed-room.html | 150 | ||||
-rw-r--r-- | modules-available/roomplanner/templates/svg-plan.html | 2 |
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&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&show=svg&locationid={{locationid}}&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" /> |