<!DOCTYPE html>
<!--
parameter
required:
uuid: [integer] panel id, see in admin panel
optional:
lang:[en,de] set the language
mode:[1,2,3,4] sets the displaying
1: Calendar & Room
2: only Calendar
3: only Room
4: Calendar & Room alternately
daystoshow:[1,2,3,4,5,6,7] sets how many days the calendar shows
scale:[10-90] scales the calendar and Roomplan in mode 1
switchtime:[1-120] sets the time between switchen in mode 4 (in seconds)
calupdate: Time the calender querys for updates,in minutes.
roomupdate: Time the PCs in the room gets updated,in seconds.
rotation:[0-3] rotation of the roomplan
vertical:[true] only mode 1, sets the calendar above the roomplan
scaledaysauto: [true] if true it finds automatically the daystoshow parameter depending on display size
-->
<html lang="de">
<meta name="viewport" content="width=device-width, initial-scale=1.0" charset="utf-8">
<head>
<title>DoorSign</title>
<link rel='stylesheet' type='text/css' href='../../js_jqueryui/style.css'/>
<link rel='stylesheet' type='text/css' href='jquery-week-calendar/jquery.weekcalendar.css'/>
<style type='text/css'>
body {
font-family: "Lucida Grande", Helvetica, Arial, Verdana, sans-serif;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background-color: #cacaca;
overflow: hidden;
position: absolute;
display: table;
}
.row {
background-color: #444;
box-shadow: 0 0.1875rem 0.375rem rgba(0, 0, 0, 0.25);
margin-bottom: 4px;
width: 100%;
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
}
.col {
padding: 0 4px;
color: white;
overflow: hidden;
flex: 1 1 auto;
text-overflow: ellipsis;
}
.col-square {
width: 4vw;
height: 4vw;
font-size: 2.5vw;
flex: 0 0 auto;
text-align: center;
padding: 5px 0 0 0;
}
.count-1 .col-square {
width: 8vw;
height: 8vw;
font-size: 6vw;
}
.count-2 .col-square {
width: 6vw;
height: 6vw;
font-size: 4.5vw;
}
.progressbar {
width: 0;
height: 2px;
position: absolute;
background-color: red;
bottom: 0;
z-index: 100;
}
.header-font {
font-size: 3vw;
font-weight: bold;
}
.nowrap {
white-space: nowrap;
overflow: hidden;
}
.timer {
color: #ddd;
}
.count-2 .header-font {
font-size: 1.8vw;
}
.count-3 .header-font {
font-size: 1.2vw;
}
.count-1 .header-font {
font-size: 1.5vw;
}
.seats-counter {
color: white;
margin: auto;
padding: 0;
font-weight: bold;
overflow: visible;
height: 100%;
text-shadow: #000 2px 2px;
}
.center {
text-align: center;
}
.room-layout {
position: relative;
float: left;
}
.location-container {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
border: 1px solid darkgrey;
background: linear-gradient(#ddd, white);
}
.calendar {
float: left;
padding: 0;
box-sizing: border-box;
}
.free-busy-busy {
background: rgba(0,0,0,.25);
}
.ui-widget-content {
color: white;
}
.wc-header {
background-color: #404040;
font-weight: bold;
}
.ui-state-default {
text-shadow: none;
}
.BROKEN {
opacity: 0.4;
}
.pc-container {
position: absolute;
left: 0;
bottom: 0;
display: inline-block;
padding: 0;
margin: 0;
overflow: hidden;
}
.pc-container div {
box-sizing: border-box;
}
.screen-frame {
position: relative;
background: black;
border-radius: 15%;
width: 100%;
height: 80%;
padding: 10%;
}
.screen-inner {
width: 100%;
height: 100%;
transition: background 2s;
}
.BROKEN .screen-inner {
background: #000;
}
.OFF .screen-inner {
background: #0c9;
}
.IDLE .screen-inner {
background: #3e4;
}
.OCCUPIED .screen-inner {
background: #800;
}
.screen-foot1 {
margin: 0 auto;
width: 10%;
height: 10%;
background: black;
}
.screen-foot2 {
width: 100%;
height: 10%;
background: black;
border-radius: 30% 30% 0 0;
}
.pc-overlay-container {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 100%;
display: table;
}
.pc-img {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.overlay {
position: relative;
width: 50%;
height: 50%;
opacity: 0.5;
float: left;
z-index: 5;
}
.overlay-rollstuhl {
width: 25%;
height: 50%;
background-color: white;
opacity: 0.5;
float: left;
}
.ui-widget-content .ui-state-active {
font-weight: bold;
color: black;
}
.wc-container {
font-weight: bold;
}
.wc-today {
background-color: rgba(255,255,255,.66);
}
.wc-time-header-cell {
background-color: #eeeeee;
border: none;
}
.ui-corner-all {
border-radius: 0;
}
.wc-scrollable-grid {
transition: height 500ms;
background: rgba(0,0,0,0);
}
.wc-scrollable-grid .wc-day-column-first {
border-style: solid;
}
[class|="wc-day"] {
border-color: grey;
}
</style>
<script type='text/javascript' src='../../../script/jquery.js'></script>
<script type='text/javascript' src='../../js_jqueryui/clientscript.js'></script>
<script type='text/javascript' src="jquery-week-calendar/jquery.weekcalendar.js"></script>
</head>
<body>
</body>
<script type='text/javascript'>
var rooms = {};
var lastRoomUpdate = 0;
var lastCalendarUpdate = 0;
var lastSwitchTime = 0;
var hasMode4 = false;
var globalConfig = {};
var roomIds = [];
var panelUuid = false;
var list;
if (typeof(SVGRect) !== "undefined") {
list = [".svg", ".png", ".jpg", ".gif"];
} else {
list = [".png", ".jpg", ".gif"];
}
const IMG_FORMAT_LIST = list;
delete list;
// TODO: Get languages from slx-admin
var translation = {
"en": {
"room": "Room",
"closed": "Closed",
"free": "Free",
"shortDays": ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
"longDays": ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
"to": "to"
},
"de": {
"room": "Raum",
"closed": "Geschlossen",
"free": "Frei",
"shortDays": ["Son", "Mon", "Die", "Mit", "Don", "Frei", "Sam"],
"longDays": ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"],
"to": "bis"
},
"pt": {
"room": "Quarto",
"closed": "Fechado",
"free": "Livre",
"shortDays": ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Se', 'Sáb'],
"longDays": ['Domingo', 'Segunda-feira', 'Terça-feira', 'Quarta-feira', 'Quinta-feira',
'Sexta-feira', 'Sábado'],
"to": "para"
}
};
$(document).ready(function () {
var uuid = getUrlParameter("uuid");
if (!uuid) {
console.log('No panel uuid given');
$("body").empty().append($('<h1>').text('No panel UUID given.'));
return;
}
getConfig(uuid);
});
/**
* Display given error message and try reloading page once a minute
*/
function fatalError(message) {
$('body').empty().append($('<h1>').text(message));
window.setInterval(function () {
$.ajax('/').done(function () {
window.location.reload(true);
}).fail(function () {
$('body').append('...');
});
}, 60000);
}
/**
* Downloads the config of a room, also reloads the page if the config hase changed over time
* @param uuid ID of the room
*/
function getConfig(uuid) {
$.ajax({
url: "../../../api.php?do=locationinfo&get=config&uuid=" + uuid,
dataType: 'json',
cache: false,
timeout: 30000,
success: function (result) {
if (!result.locations || result.locations.constructor !== Array) {
fatalError("Requested panel doesn't contain locations / not array");
return;
}
var fetchedRooms = result.locations.filter(function (x) {
// filter out if no numeric id, or id already present, or already got 4 locations
if (typeof(x.id) !== 'number' || x.id <= 0 || roomIds.indexOf(x.id) !== -1 || roomIds.length >= 4)
return false;
roomIds.push(x.id);
return true;
});
if (roomIds.length === 0) {
fatalError("List of location ids is empty");
return;
}
var time = new Date(result.time);
if (isNaN(time.getTime()) || time.getYear() < 2010) {
time = new Date();
}
SetUpDate(time);
delete result.time;
delete result.locations;
globalConfig = result;
sanitizeGlobalConfig();
lastRoomUpdate = MyDate().getTime();
for (var i = 0; i < fetchedRooms.length; ++i) {
addRoom(fetchedRooms[i]);
}
panelUuid = uuid;
initRooms();
// TODO: Check for changed config using get=timestamp and reload whole page on change
}, error: function () {
fatalError('Could not fetch panel config');
}
})
}
const PARAM_STRING = 1;
const PARAM_INT = 2;
const PARAM_BOOL = 3;
/**
* Read given parameter from URL, replacing it in the config object if present.
* @param config object config object
* @param property string name of property in object, URL param of same name is being checked
* @param paramType int one of PARAM_STRING, PARAM_INT, PARAM_BOOL
* @param intScaleFactor int optional scale factor that will be applied if paramType == PARAM_INT
*/
function setRoomConfigFromUrl(config, property, paramType, intScaleFactor) {
var val = getUrlParameter(property);
if (val === true || val === false)
return;
if (paramType === PARAM_STRING) {
config[property] = val;
} else if (paramType === PARAM_INT) {
config[property] = parseInt(val);
if (intScaleFactor) {
config[property] *= intScaleFactor;
}
} else if (paramType === PARAM_BOOL) {
val = val.toLowerCase();
config[property] = val.length > 0 && val !== 'false' && val !== 'off' && val !== '0';
} else {
console.log('Invalid paramType: ' + paramType);
}
}
/**
* Put given numeric config property in range min..max (both inclusive),
* if not in range, set to default.
* @param config - object config object
* @param property - string config property
* @param min int - min allowed value (inclusive)
* @param max int - max allowed value (inclusive)
* @param defaultval - default value to use if out of range
* @param scaleFactor int - optional scale factor to apply
*/
function putInRange(config, property, min, max, defaultval, scaleFactor) {
var v = config[property];
if (!scaleFactor) {
scaleFactor = 1;
}
if (!v || !isFinite(v) || isNaN(v) || v < min * scaleFactor || v > max * scaleFactor) {
config[property] = defaultval * scaleFactor;
}
}
/**
* gets Additional Parameters from the URL, and from the
* downloaded json.
* also makes sure parameters are in a given range
*/
function sanitizeGlobalConfig() {
sanitizeConfig(globalConfig);
$('html').attr('lang', globalConfig.language);
}
function sanitizeConfig(config) {
if (config) {
config.switchtime = config.switchtime * 1000;
config.calupdate = config.calupdate * 60 * 1000;
config.roomupdate = config.roomupdate * 1000;
}
setRoomConfigFromUrl(config, 'calupdate', PARAM_INT, 60 * 1000);
setRoomConfigFromUrl(config, 'roomupdate', PARAM_INT, 1000);
setRoomConfigFromUrl(config, 'daystoshow', PARAM_INT);
setRoomConfigFromUrl(config, 'scaledaysauto', PARAM_BOOL);
setRoomConfigFromUrl(config, 'vertical', PARAM_BOOL);
setRoomConfigFromUrl(config, 'eco', PARAM_BOOL);
setRoomConfigFromUrl(config, 'scale', PARAM_INT);
setRoomConfigFromUrl(config, 'rotation', PARAM_INT);
setRoomConfigFromUrl(config, 'switchtime', PARAM_INT, 1000);
// parameter validation
putInRange(config, 'switchtime', 5, 120, 6, 1000);
putInRange(config, 'scale', 10, 90, 50);
putInRange(config, 'daystoshow', 1, 7, 7);
putInRange(config, 'roomupdate', 15, 5 * 60, 60, 1000);
putInRange(config, 'calupdate', 1, 60, 30, 60 * 1000);
putInRange(config, 'mode', 1, 4, 1);
putInRange(config, 'rotation', 0, 3, 0);
}
/**
* generates the Room divs and calls the needed functions depending on the rooms mode
*/
function initRooms() {
var width = "100%";
var height = "100%";
var top, left;
hasMode4 = false;
if (roomIds.length === 2 || roomIds.length === 4) {
width = "50%";
}
if (roomIds.length === 3) {
width = "33%";
}
if (roomIds.length === 4) {
height = "50%";
}
for (var t = 0; t < roomIds.length; t++) {
var rid = roomIds[t];
var room = rooms[rid];
if (roomIds.length === 3) {
top = 0;
left = (t * 33) + '%';
} else {
top = (Math.floor(t / 2) * 50) + '%';
left = ((t % 2) * 50) + '%';
}
var $loc = $("<div>").addClass('location-container');
$loc.css({top: top, left: left, width: width, height: height});
$("body").append($loc);
room.$.container = $loc;
room.$.locationName = $('<div>').addClass('col').addClass('header-font');
room.$.currentEvent = $("<span>").addClass('nowrap');
room.$.currentRemain = $("<span>").addClass('nowrap').addClass('timer');
room.$.seatsCounter = $('<span>').addClass('seats-counter');
room.$.seatsBackground = $('<div>').addClass('col col-square').append(room.$.seatsCounter);
var $header = $('<div>').addClass('row').addClass('count-' + roomIds.length);
$header.append(room.$.locationName);
$header.append($('<div>').addClass('col header-font center').append(room.$.currentEvent).append(' ').append(room.$.currentRemain));
$header.append(room.$.seatsBackground);
room.$.header = $header;
$loc.append($header);
if (room.name !== null) {
room.$.locationName.text(room.name);
}
if (room.config.mode !== 3) {
setUpCalendar(room);
}
if (room.config.mode !== 2) {
initRoomLayout(room);
}
if (room.config.mode === 4) {
hasMode4 = true;
}
SetOpeningTimes(room);
UpdateRoomHeader(room);
(function (room) {
setTimeout(function () {
resizeIfRequired(room);
}, 800);
})(room);
}
if (hasMode4) {
generateProgressBar();
}
mainUpdateLoop();
setInterval(mainUpdateLoop, 10000);
setInterval(updateHeaders, globalConfig.eco ? 10000 : 1000);
}
var lastDate = false;
/**
* Main Update loop, this loop runs every 10 seconds
*/
function mainUpdateLoop() {
var date = MyDate();
var now = date.getTime();
if (lastCalendarUpdate + globalConfig.calupdate < now) {
lastCalendarUpdate = now;
queryCalendars();
}
if (lastRoomUpdate + globalConfig.roomupdate < now) {
lastRoomUpdate = now;
queryRooms();
}
$('.calendar').weekCalendar("scrollToHour");
// reload site at midnight
var today = date.getDate();
if (lastDate !== false) {
if (lastDate !== today) {
location.reload(true);
}
} else {
lastDate = today;
}
}
/**
* Update all location headers.
* Runs ever second (normal) or every 10 seconds (eco)
*/
function updateHeaders() {
for (var property in rooms) {
if (rooms[property].state.end) {
// Updating All room Headers
UpdateRoomHeader(rooms[property]);
}
}
}
/**
* Generates a room Object and adds it to the rooms array
* @param roomData Config Json of the room
*/
function addRoom(roomData) {
var mergedConfig = {};
if (roomData.config && typeof(roomData.config.constructor) === 'object') {
mergedConfig = roomData.config;
sanitizeConfig(mergedConfig);
}
for (var k in globalConfig) {
if (typeof mergedConfig[k] === 'undefined') {
mergedConfig[k] = globalConfig[k];
}
}
var now = MyDate().getTime();
var room = {
id: roomData.id,
name: roomData.name,
config: mergedConfig,
timetable: null,
currentEvent: null,
nextEventEnd: null,
timeTilFree: null,
state: null,
rawOpeningTimes: roomData.openingtime || null,
openingTimes: null,
openTimes: 24,
currentfreePcs: 0,
layout: roomData.machines || null,
freePcs: 0,
resizeRoom: true,
resizeCalendar: true,
lastCalendarUpdate: now,
lastRoomUpdate: now,
$: {},
getState: function () {
if (this.state === null) {
ComputeCurrentState(this);
return this.state;
}
if (this.state.end) {
if (this.state.end < MyDate()) {
ComputeCurrentState(this);
}
}
return this.state;
}
};
rooms[roomData.id] = room;
return room;
}
/**
* inilizes the Calendar for an room
* @param room Room Object
*/
function setUpCalendar(room) {
var daysToShow = room.config.daystoshow;
generateCalendarDiv(room);
room.$.calendar.weekCalendar({
timeslotsPerHour: 1,
timeslotHeight: 30,
daysToShow: daysToShow,
height: function () {
if (room.config.mode === 1 && room.config.vertical && (!room.timetable || !room.timetable.length)) return 20;
var height = $(window).height();
if (roomIds.length === 4) {
height /= 2;
}
height -= room.$.header.height() - 5;
if (room.config.mode === 1 && room.config.vertical) {
height *= (room.config.scale / 100);
}
return height;
},
eventRender: function (calEvent, $event) {
if (calEvent.end.getTime() < MyDate().getTime()) {
$event.css("backgroundColor", "#aaa");
$event.find(".time").css({"backgroundColor": "#999", "border": "1px solid #888"});
} else if (calEvent.end.getTime() > MyDate().getTime() && calEvent.start.getTime() < MyDate().getTime()) {
$event.css("backgroundColor", "#25B002");
$event.find(".time").css({"backgroundColor": "#25B002", "border": "1px solid #888"});
}
},
date: MyDate(),
dateFormat: "j.n",
timeFormat: "G:i",
scrollToHourMillis: 500,
use24Hour: true,
readonly: true,
showHeader: false,
hourLine: true,
shortDays: t("shortDays", room.config.language),
longDays: t("longDays", room.config.language),
buttons: false,
timeSeparator: " " + t("to", room.config.language) + " ",
startOnFirstDayOfWeek: false,
displayFreeBusys: true,
defaultFreeBusy: {free: false}
});
}
/**
* Generates the Calendar Div, depending on it's width
* @param width width of the Calendar Div
* @param room Room Object
*/
function generateCalendarDiv(room) {
var width = 100;
if (room.config.mode === 1 && !room.config.vertical) {
width = room.config.scale;
}
var $cal = $('<div>').addClass('calendar');
if (room.config.mode === 1 && room.config.vertical) {
$cal.css('float', "none");
}
$cal.width(width + '%');
room.$.container.append($cal);
room.$.calendar = $cal;
}
const OT_DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const OT_KEYS = ['HourOpen', 'HourClose', 'MinutesOpen', 'MinutesClose'];
/**
* sets the opening Time in the calendar plugin and saves it in the room object
* @param room Room Object
*/
function SetOpeningTimes(room) {
var opening = 24;
var close = 0;
var i;
if (room.rawOpeningTimes && typeof(room.rawOpeningTimes) === 'object') {
// TODO: wtf! we have three(!) formats for storing the opening times (DB, API, now this one) - WHY!?
var parsedOpenings = room.rawOpeningTimes;
room.state = null;
room.openingTimesCalendar = [];
room.openingTimes = [];
for (i = 0; i < OT_DAYS.length; ++i) {
room.openingTimes.push(filterOpeningTimesDay(parsedOpenings[OT_DAYS[i]]));
}
delete room.rawOpeningTimes;
}
if (!room.openingTimes) {
scaleCalendar(room);
return;
}
if (room.config.mode === 3) {
// Calendar is not displayed, don't need to do additional work
return;
}
var now = MyDate();
for (i = 0; i < 7; i++) {
var tmp = room.openingTimes[i];
for (var d = 0; d < tmp.length; d++) {
var day = getNextDayOfWeek(now, i);
if (room.openingTimesCalendar) {
room.openingTimesCalendar.push({
"start": new Date(day.getFullYear(), day.getMonth(), day.getDate(),
tmp[d]['HourOpen'], tmp[d]['MinutesOpen']),
"end": new Date(day.getFullYear(), day.getMonth(),
day.getDate(), tmp[d]['HourClose'], tmp[d]['MinutesClose']),
"free": true
});
}
if (tmp[d]['HourOpen'] < opening) {
opening = tmp[d]['HourOpen'];
}
if (tmp[d]['HourClose'] >= close) {
close = tmp[d]['HourClose'];
if (tmp[d]['MinutesClose'] !== 0) {
close++;
}
}
}
}
if (opening === 24 && close === 0) {
opening = 0;
close = 24;
}
room.openTimes = close - opening;
scaleCalendar(room);
room.$.calendar.weekCalendar("option", "businessHours", {
start: opening,
end: close,
limitDisplay: true
});
}
/**
* Filter out invalid opening time entries from given array,
* also make sure all the values are of type number (int)
*
* @param {Array} arr
* @return {Array} list of valid opening times
*/
function filterOpeningTimesDay(arr) {
if (!arr || arr.constructor !== Array) return [];
return arr.map(function (el) {
if (!el || typeof el !== 'object') return null;
for (var i = 0; i < OT_KEYS.length; ++i) {
el[OT_KEYS[i]] = toInt(el[OT_KEYS[i]]);
if (isNaN(el[OT_KEYS[i]])) return null;
}
return el;
}).filter(function (el) {
if (!el) return false;
if (el.HourOpen < 0 || el.HourOpen > 23) return false;
if (el.HourClose < 0 || el.HourClose > 23) return false;
if (el.HourClose < el.HourOpen) return false;
if (el.MinutesOpen < 0 || el.MinutesOpen > 59) return false;
if (el.MinutesClose < 0 || el.MinutesClose > 59) return false;
if (el.HourOpen === el.HourClose && el.MinutesClose < el.MinutesOpen) return false;
return true;
});
}
/**
* querys the Calendar data
*/
function queryCalendars() {
if (!panelUuid) return;
var url = "../../../api.php?do=locationinfo&get=calendar&uuid=" + panelUuid;
$.ajax({
url: url,
dataType: 'json',
cache: false,
timeout: 30000,
success: function (result) {
if (result && result.constructor === Array) {
var l = result.length;
for (var i = 0; i < l; i++) {
updateCalendar(result[i].calendar, rooms[result[i].id]);
}
}
}, error: function () {
// Retry in 5 minutes (300 seconds)
lastCalendarUpdate = MyDate().getTime() + globalConfig.calupdate + 300000;
}
});
}
const SEVEN_DAYS = 7 * 86400 * 1000;
/**
* applays new calendar data to the calendar plugin and also saves it to the room object
* @param {Array} json Calendar data
* @param room Room Object
*/
function updateCalendar(json, room) {
if (!room) {
console.log("Error: No room for calendar data");
return;
}
if (!json || json.constructor !== Array) {
console.log("Error: Calendar data was empty or malformed.");
return;
}
var now = MyDate().getTime();
json = json.filter(function (el) {
if (!el.title || !el.start || !el.end) return false;
var s = new Date(el.start).getTime();
var e = new Date(el.end).getTime();
return !(isNaN(s) || isNaN(e) || Math.abs(s - now) > SEVEN_DAYS || Math.abs(e - now) > SEVEN_DAYS);
});
if (json.length === 0) {
console.log('Notice: Calendar has no current events for ' + room.name);
}
try {
room.timetable = json;
if (room.config.mode !== 3) {
// TODO: Check if they're the same
var cal = room.$.calendar;
cal.weekCalendar('option', 'data', {events: json});
cal.weekCalendar("refresh");
cal.weekCalendar("option", "defaultFreeBusy", {free: !room.openingTimesCalendar});
cal.weekCalendar("updateFreeBusy", room.openingTimesCalendar);
cal.weekCalendar("resizeCalendar");
setTimeout(function() {
scaleRoom(room);
}, 550);
}
room.state = null;
UpdateRoomHeader(room);
} catch (e) {
console.log("Error: Couldnt add calendar data");
console.log(e);
}
}
/**
* scales calendar, called once on create and on window resize
* @param room Room Object
*/
function scaleCalendar(room) {
if (room.config.mode === 3) {
return;
}
var $cal = room.$.calendar;
if (!$cal.is(':visible')) return;
room.resizeCalendar = false;
var columnWidth = $cal.find(".wc-day-1").width();
if (room.config.scaledaysauto) {
var result = ($cal.weekCalendar("option", "daysToShow") * columnWidth) / 100;
result = parseInt(Math.min(Math.max(Math.abs(result), 1), 7));
if (result !== $cal.weekCalendar("option", "daysToShow")) {
$cal.weekCalendar("option", "daysToShow", result);
}
}
if (((!room.config.scaledaysauto) || $cal.weekCalendar("option", "daysToShow") === 1) && columnWidth < 85) {
$cal.weekCalendar("option", "useShortDayNames", true);
} else {
$cal.weekCalendar("option", "useShortDayNames", false);
}
var clientHeight = $(window).height();
if (roomIds.length === 4) {
clientHeight = clientHeight / 2;
}
clientHeight = clientHeight - room.$.header.height()
- room.$.calendar.find(".wc-time-column-header").height() - 2;
if (room.config.mode === 1 && room.config.vertical) {
clientHeight = clientHeight * (room.config.scale / 100);
clientHeight -= 22;
}
clientHeight -= 6;
var height = clientHeight / (room.openTimes * $cal.weekCalendar("option", "timeslotsPerHour"));
if (height < 30) {
height = 30;
}
// Scale calendar font
if (height > 120) {
$cal.weekCalendar("option", "textSize", 28);
}
else if (height > 100) {
$cal.weekCalendar("option", "textSize", 24);
} else if (height > 80) {
$cal.weekCalendar("option", "textSize", 22);
} else if (height > 70) {
$cal.weekCalendar("option", "textSize", 20);
} else if (height > 60) {
$cal.weekCalendar("option", "textSize", 14);
} else {
$cal.weekCalendar("option", "textSize", 13);
}
$cal.weekCalendar("option", "timeslotHeight", height);
if (room.timetable) {
$cal.weekCalendar("option", "data", {events: room.timetable});
$cal.weekCalendar('refresh');
}
$cal.weekCalendar("option", "defaultFreeBusy", {free: !room.openingTimesCalendar});
if (room.openingTimesCalendar) {
$cal.weekCalendar("updateFreeBusy", room.openingTimesCalendar);
}
$cal.weekCalendar("resizeCalendar");
$cal.weekCalendar("option", "hourLine", true);
}
/**
* used for countdown
* computes the time difference between 2 Date objects
* @param {Date} a
* @param {Date} b
* @param room Room Object
* @returns {string} printable time
*/
function GetTimeDiferenceAsString(a, b, room) {
if (!a || !b) {
return "";
}
var milliseconds = a.getTime() - b.getTime();
var seconds = Math.floor((milliseconds / 1000) % 60);
milliseconds -= seconds * 1000;
var minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
milliseconds -= minutes * 1000 * 60;
var hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
var days = Math.floor((milliseconds / (1000 * 60 * 60 * 24)) % 31);
if (seconds < 10) {
seconds = "0" + seconds;
}
if (minutes < 10) {
minutes = "0" + minutes;
}
if (days !== 0) {
// don't show?
return "";
}
if (globalConfig.eco) {
return hours + ":" + minutes;
}
return hours + ":" + minutes + ":" + seconds;
}
/**
* returns next closing time of a given room
* @param room
* @returns {Date} Object of next closing
*/
function GetNextClosing(room) {
if (!room.openingTimes || room.openingTimes.length === 0) return null;
var now = MyDate();
var day = now.getDay();
var bestdate = null;
for (var a = 0; a < 7; a++) {
var tmp = room.openingTimes[(day + a) % 7];
if (!tmp) continue;
for (var i = 0; i < tmp.length; i++) {
var closeDate = getNextDayOfWeek(now, (day + a) % 7);
closeDate.setHours(tmp[i].HourClose);
closeDate.setMinutes(tmp[i].MinutesClose);
closeDate.setSeconds(0);
if (closeDate > now) {
if (!IsOpen(new Date(closeDate.getTime() + 1800000), room)) {
if (!bestdate || bestdate > closeDate) {
bestdate = closeDate;
}
}
}
}
if (bestdate) return bestdate;
}
return null;
}
/**
* checks if a room is on a given date/time open
* @param date Date Object
* @param room Room object
* @returns {Boolean} for open or not
*/
function IsOpen(date, room) {
if (!room.openingTimes || room.openingTimes.length === 0) return true;
var tmp = room.openingTimes[date.getDay()];
if (!tmp) return false;
var openDate = new Date(date.getTime());
var closeDate = new Date(date.getTime());
for (var i = 0; i < tmp.length; i++) {
openDate.setHours(tmp[i].HourOpen);
openDate.setMinutes(tmp[i].MinutesOpen);
closeDate.setHours(tmp[i].HourClose);
closeDate.setMinutes(tmp[i].MinutesClose);
if (openDate < date && closeDate > date) {
return true;
}
}
return false;
}
/**
* Returns next Opening
* @param room Room Object
* @returns {Date} Object of next opening
*/
function GetNextOpening(room) {
if (!room.openingTimes) return null;
var now = MyDate();
var day = now.getDay();
var bestdate = null;
for (var dow = 0; dow < 7; dow++) {
var tmp = room.openingTimes[(day + dow) % 7];
if (!tmp) continue;
for (var i = 0; i < tmp.length; i++) {
var openDate = getNextDayOfWeek(now, (day + dow) % 7);
openDate.setHours(tmp[i].HourOpen);
openDate.setMinutes(tmp[i].MinutesOpen);
if (openDate > now) {
if (!IsOpen(new Date(openDate.getTime() - 1800000), room)) {
if (!bestdate || bestdate > openDate) {
bestdate = openDate;
}
}
}
}
if (bestdate) return bestdate;
}
return null;
}
/**
* Sets the free PCs number in the right corner and updates the square color accordingly
* @param room Room
* @param seats Number of free PC's in the room
*/
function SetFreeSeats(room, seats) {
room.$.seatsCounter.text(seats >= 0 ? seats : '');
if (seats > 0) {
room.$.seatsBackground.css('background-color', '#00dd10');
} else if (seats === -1) {
room.$.seatsBackground.css('background-color', 'red');
} else {
room.$.seatsBackground.css('background-color', 'red');
}
}
/**
* Updates the Header of an Room
* @param room Room Object
*/
function UpdateRoomHeader(room) {
var tmp = room.getState();
var same = (tmp === room.lastHeaderState);
if (!same) {
room.lastHeaderState = tmp;
}
var newText = false, newTime = false;
var seats = room.freePcs;
if (tmp.state === 'closed' || tmp.state === 'CalendarEvent' || tmp.state === 'Free') {
newTime = GetTimeDiferenceAsString(tmp.end, MyDate(), room);
} else if (!same) {
newTime = '';
}
if (tmp.state === "closed") {
if (!same) newText = t("closed", globalConfig.language);
} else if (tmp.state === "CalendarEvent") {
if (!same) newText = tmp.title;
seats = -1;
} else if (tmp.state === "Free") {
if (!same) newText = t("free", room.config.language);
} else if (tmp.state === "FreeNoEnd") {
if (!same) newText = t("free", room.config.language);
}
if (newText !== false) {
room.$.currentEvent.text(newText);
}
if (newTime !== false) {
room.$.currentRemain.text(newTime);
}
if (room.lastFreeSeats !== seats) {
SetFreeSeats(room, room.freePcs);
room.lastFreeSeats = seats;
}
}
/**
* computes state of a room, states are:
* closed, FreeNoEnd, Free, CalendarEvent.
* @param room Object
*/
function ComputeCurrentState(room) {
if (!IsOpen(MyDate(), room)) {
room.state = {state: "closed", end: GetNextOpening(room), title: "", next: ""};
return;
}
var closing = GetNextClosing(room);
var event = getNextEvent(room.timetable);
// no event and no closing
if (!closing && !event) {
room.state = {state: "FreeNoEnd", end: "", title: "", next: ""};
return;
}
// no event so closing is next
if (!event) {
room.state = {state: "Free", end: closing, title: "", next: "closing"};
return;
}
// event is at the moment
if ((!closing || event.start.getTime() < closing.getTime()) && event.start.getTime() < MyDate()) {
room.state = {
state: "CalendarEvent",
end: event.end,
title: event.title,
next: ""
};
return;
}
// no closing so event is next
if (!closing) {
room.state = {state: "Free", end: event.start, title: "", next: "event"};
return;
}
// event sooner then closing
if (event.start.getTime() < closing) {
room.state = {state: "Free", end: event.start, title: "", next: "event"};
} else {
room.state = {state: "Free", end: closing, title: "", next: "closing"};
}
}
/**
* returns next event from a given json of events
* @param calEvents Json which contains the calendar data.
* @returns event next Calendar Event
*/
function getNextEvent(calEvents) {
if (!calEvents) return null;
if (calEvents.constructor !== Array) {
console.log('getNextEvent called with something not array: ' + typeof(calEvents));
return null;
}
var event;
var now = MyDate();
for (var i = 0; i < calEvents.length; i++) {
//event is now active
if (calEvents[i].start.getTime() < now.getTime() && calEvents[i].end.getTime() > now.getTime()) {
return calEvents[i];
}
//first element to consider
if (!event) {
if (calEvents[i].start.getTime() > now.getTime()) {
event = calEvents[i];
}
} else if (calEvents[i].start.getTime() > now.getTime() && event.start.getTime() > calEvents[i].start.getTime()) {
event = calEvents[i];
}
}
return event;
}
/**
* Skip to next upcoming day matching the given day of week.
* @return {Date}
*/
function getNextDayOfWeek(date, dayOfWeek) {
var resultDate = new Date(date.getTime());
resultDate.setDate(date.getDate() + (7 + dayOfWeek - date.getDay()) % 7);
return resultDate;
}
/*
/========================================== Room Layout =============================================
*/
const picSizeX = 3.8;
const picSizeY = 3;
/**
* Generates the RoomLayout Div
* @param width The width the RoomLayout should have (in percent).
* @param room Room Object
*/
function generateRoomLayoutDiv(width, room) {
if ((room.config.vertical && room.config.mode === 1) || (room.config.mode === 3) || (room.config.mode === 4)) {
width = 100 + "%";
}
var $div = $('<div>').prop('id', 'roomLayout_' + room.id).addClass("room-layout").css('width', width);
if (room.config.mode === 4) {
$div.hide();
}
room.$.container.append($div);
room.$.layout = $div;
}
/**
* Main function for generating the Room Layout
* @param room Room Object
*/
function initRoomLayout(room) {
var maxX = false, maxY = false;
var minX = false, minY = false;
var xDifference, yDifference;
var x, y;
generateRoomLayoutDiv((100 - room.config.scale) + "%", room);
var layout = room.layout;
if (layout === null || !layout.length) {
return;
}
rotateRoom(room.config.rotation, layout);
for (var i = 0; i < layout.length; i++) {
x = layout[i].x = parseInt(layout[i].x);
y = layout[i].y = parseInt(layout[i].y);
if (isNaN(x) || isNaN(y)) continue;
if (minX === false || x < minX) {
minX = x;
}
if (minY === false || y < minY) {
minY = y;
}
if (maxX === false || x > maxX) {
maxX = x;
}
if (maxY === false || y > maxY) {
maxY = y;
}
}
xDifference = maxX - minX;
yDifference = maxY - minY;
room.xDifference = xDifference;
room.yDifference = yDifference;
room.minX = minX;
room.minY = minY;
room.maxX = maxX;
room.maxY = maxY;
setUpRoom(room, layout);
scaleRoom(room);
UpdatePc(layout, room);
}
/**
* Computes offsets and scaling's for the RoomLayout
* @param room Room Object
*/
function generateOffsetAndScale(room) {
var clientHeight;
if (room.config.vertical && room.config.mode === 1) {
clientHeight = room.$.container.height() - (room.$.calendar.position().top + room.$.calendar.height());
} else {
clientHeight = room.$.container.height() - (room.$.header.height() + 5);
}
var clientWidth = room.$.layout.width();
var scaleX;
if (room.xDifference !== 0) {
scaleX = clientWidth / room.xDifference;
} else {
scaleX = clientWidth;
}
var scaleY;
if (room.yDifference !== 0) {
scaleY = clientHeight / room.yDifference;
} else {
scaleY = clientHeight;
}
var scaleYs = (clientHeight - (picSizeY * scaleY)) / room.yDifference;
var scaleXs = (clientWidth - (picSizeX * scaleX)) / room.xDifference;
if (scaleYs <= 0) {
scaleYs = 9999;
}
if (scaleXs <= 0) {
scaleXs = 9999;
}
room.scale = Math.min(scaleYs, scaleY, scaleXs, scaleX, (clientHeight * 0.9) / picSizeY, (clientWidth * 0.9) / picSizeX);
room.xOffset = 0 - room.minX;
room.yOffset = 0 - room.minY;
room.xOffset += ((1 / 2 * (clientWidth - (((room.maxX + room.xOffset) * room.scale) + picSizeX * room.scale))) / room.scale);
room.yOffset += ((1 / 2 * (clientHeight - (((room.maxY + room.yOffset) * room.scale) + picSizeY * room.scale))) / room.scale);
}
/**
* adds images for each pc to Room Layout
* @param room Room Object
* @param layout Layout json
*/
function setUpRoom(room, layout) {
for (var i = 0; i < layout.length; i++) {
if (!isNaN(layout[i].y) && !isNaN(layout[i].x)) {
//var $img = $('<img>').prop('id', "pc-img_" + room.id + "_" + layout[i].id).addClass('pc-img');
var $overlays = $('<div>').addClass('pc-overlay-container');
layout[i].$div = $('<div>').prop('id', "pc_" + room.id + "_" + layout[i].id).addClass('pc-container');
layout[i].$div.append($('<div>').addClass('screen-frame').append($('<div>').addClass('screen-inner')));
layout[i].$div.append($('<div>').addClass('screen-foot1'));
layout[i].$div.append($('<div>').addClass('screen-foot2'));
//layout[i].$div.append($overlays).append($img);
room.$.layout.append(layout[i].$div);
if (layout[i].overlay && layout[i].overlay.constructor === Array) {
for (var a = 0; a < layout[i].overlay.length; a++) {
addOverlay($overlays, layout[i].overlay[a]);
}
}
}
}
}
/**
* Generate overlay with given image name.
* @param $container container to put overlay into
* @param overlayName name of the overlay (image name without ending)
*/
function addOverlay($container, overlayName) {
var imgname;
for (var i = 0; i < IMG_FORMAT_LIST.length; ++i) {
if (imageExists("img/overlay/" + overlayName + IMG_FORMAT_LIST[i])) {
imgname = "img/overlay/" + overlayName + IMG_FORMAT_LIST[i];
break;
}
}
if (!imgname) return; // No image found on server, do nothing TODO just div with class
var $overlay = $("<img>");
$overlay.addClass('overlay').attr('src', imgname).addClass("overlay-" + overlayName);
$container.append($overlay);
}
var imgExists = {};
/**
* checks if images exists on the web server.
* result will be cached after fist call.
*
* @param {String} image_url URL of image to check
* @return {Boolean} true iff image exists
*/
function imageExists(image_url) {
if (!imgExists.hasOwnProperty(image_url)) {
var http = new XMLHttpRequest();
http.open('HEAD', image_url, false);
http.send();
imgExists[image_url] = http.status === 200;
}
return imgExists[image_url];
}
/**
* Queries Pc states
*/
function queryRooms() {
$.ajax({
url: "../../../api.php?do=locationinfo&get=machines&uuid=" + panelUuid,
dataType: 'json',
cache: false,
timeout: 30000,
success: function (result) {
if (!result || result.constructor !== Array) {
console.log('Warning: get=machines didnt return array');
return;
}
for (var i = 0; i < result.length; i++) {
UpdatePc(result[i].machines, rooms[result[i].id]);
}
}
})
}
/**
* Updates the PC's (images) in the room layout. Also Updates how many pc's are free.
* @param update Update Json from query for one(!) room
* @param room Room object
*/
function UpdatePc(update, room) {
if (!room) {
console.log('Got room update for unknown room, ignored.');
return;
}
if (!update || update.constructor !== Array) {
console.log('Update data is not array for room ' + room.name);
console.log(update);
return;
}
var freePcs = 0;
for (var i = 0; i < update.length; i++) {
var $div = $("#pc_" + room.id + "_" + update[i].id);
// Pc free
if (update[i].pcState === "IDLE" || update[i].pcState === "OFF") {
freePcs++;
}
$div.removeClass('BROKEN OFF IDLE OCCUPIED'.replace(update[i].pcState, '')).addClass(update[i].pcState);
}
room.freePcs = freePcs;
UpdateRoomHeader(room);
}
/**
* Adjust pc coordinate depending on room rotation
* @param r Rotation, from 0 - 3 (int)
* @param layout Layout json
*/
function rotateRoom(r, layout) {
for (var z = 0; z < r; z++) {
for (var i = 0; i < layout.length; i++) {
var x = parseInt(layout[i].x);
var y = parseInt(layout[i].y);
layout[i].x = y;
layout[i].y = -x;
}
}
}
/**
* Positions the computer images in the roomLayout div according to their position and div size
* @param room Room object
*/
function scaleRoom(room) {
if (!room.$.layout.is(':visible')) return;
room.resizeRoom = false;
generateOffsetAndScale(room);
for (var i = 0; i < room.layout.length; i++) {
var pcWidth = (picSizeX * room.scale) + "px";
var pcHeight = (picSizeY * room.scale) + "px";
if (room.layout[i].$div && !isNaN(room.layout[i].y) && !isNaN(room.layout[i].x)) {
room.layout[i].$div.css({
width: pcWidth,
height: pcHeight,
top: ((room.layout[i].y + room.yOffset) * room.scale) + "px",
left: ((room.layout[i].x + room.xOffset) * room.scale) + "px"
});
}
}
}
/*
/========================================== Misc =============================================
*/
var resizeTimeout = false;
// called when browser window changes size
// scales calendar and room layout accordingly
$(window).resize(function () {
if (resizeTimeout !== false) clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(function () {
resizeTimeout = false;
for (var property in rooms) {
rooms[property].resizeCalendar = true;
rooms[property].resizeRoom = true;
scaleCalendar(rooms[property]);
scaleRoom(rooms[property]);
}
SetProgressBarSpeed();
}, 200);
});
/**
* returns parameter value from the url
* @param sParam
* @returns boolean|string for given parameter
*/
function getUrlParameter(sParam) {
var sPageURL = decodeURIComponent(window.location.search.substring(1)),
sURLVariables = sPageURL.split('&'),
sParameterName,
i;
for (i = 0; i < sURLVariables.length; i++) {
sParameterName = sURLVariables[i].split('=', 2);
if (sParameterName[0] === sParam) {
if (sParameterName.length === 1) return true;
return sParameterName[1];
}
}
return false;
}
/**
* Function for translation
* @param toTranslate key which we wan't to translate
* @param lang languages in which should be translated
* @returns r translated string
*/
function t(toTranslate, lang) {
var r;
if (!lang || !translation[lang] || !translation[lang][toTranslate]) {
r = translation['en'][toTranslate] || 'missing';
} else {
r = translation[lang][toTranslate];
}
return r;
}
function resizeIfRequired(room) {
if (room.resizeCalendar) {
scaleCalendar(room);
}
if (room.resizeRoom) {
scaleRoom(room);
}
}
/**
* Used in Mode 4, switches given room from Timetable to room layout and vice versa
*/
function switchLayouts() {
for (var roomKey in rooms) {
var room = rooms[roomKey];
if (room.config.mode !== 4) continue;
if (room.$.layout.is(':visible')) {
room.$.layout.hide();
room.$.calendar.show();
} else {
room.$.layout.show();
room.$.calendar.hide();
}
resizeIfRequired(room);
}
lastSwitchTime = MyDate().getTime();
}
var $pbar = false;
var pbarTimer = false;
const PX_PER_SEC_TARGET = 10;
/**
* adds a progressbar (id) used in mode 4
*/
function generateProgressBar() {
if ($pbar) return;
$pbar = $('<div class="progressbar">');
$('body').append($pbar);
SetProgressBarSpeed();
}
function SetProgressBarSpeed() {
if (!$pbar || !globalConfig.switchtime) return;
if (pbarTimer) clearInterval(pbarTimer);
var interval = 1000;
if (!globalConfig.eco) {
var pxPerMSec = $('body').width() / globalConfig.switchtime;
interval = Math.max(1 / (pxPerMSec / PX_PER_SEC_TARGET), 100);
}
pbarTimer = setInterval(function () {
var width = ((MyDate().getTime() - lastSwitchTime) / globalConfig.switchtime) * 100;
if (width < 0) width = 0;
if (width >= 100) {
width = 100;
switchLayouts();
}
$pbar.width(width + '%');
}, interval);
}
/**
* Convert passed argument to integer if possible, return NaN otherwise.
* The difference to parseInt() is that leading zeros are ignored and not
* interpreted as octal representation.
*
* @param str string or already a number
* @return {number} str converted to number, or NaN
*/
function toInt(str) {
var t = typeof str;
if (t === 'number') return str | 0;
if (t === 'string') return parseInt(str.replace(/^0+([^0])/, '$1'));
return NaN;
}
</script>
</html>