ednl-liftstatus-web-components
Version:
The EDNL LiftStatus web components
688 lines (677 loc) • 23.8 kB
JavaScript
import { proxyCustomElement, HTMLElement, getAssetPath, h, Host } from '@stencil/core/internal/client';
import { A as identity, c as arrayEach, B as baseEach, m as debounce, n as select, o as time, p as axisBottom, r as timeFormat, x as sortBy, y as last, w as filter, v as get } from './time.js';
import { l as isArray, s as isObjectLike, z as baseGetTag, d as dayjs_min, i as isEmpty, g as getStore, o as isObject } from './store.js';
import { s as sensorInfo } from './sensorInfo.js';
import { t as toInteger, b as baseIndexOf, i as includes } from './includes.js';
import { k as keys, v as values } from './values.js';
import { i as isNil } from './isNil.js';
/**
* Casts `value` to `identity` if it's not a function.
*
* @private
* @param {*} value The value to inspect.
* @returns {Function} Returns cast function.
*/
function castFunction(value) {
return typeof value == 'function' ? value : identity;
}
/**
* Iterates over elements of `collection` and invokes `iteratee` for each element.
* The iteratee is invoked with three arguments: (value, index|key, collection).
* Iteratee functions may exit iteration early by explicitly returning `false`.
*
* **Note:** As with other "Collections" methods, objects with a "length"
* property are iterated like arrays. To avoid this behavior use `_.forIn`
* or `_.forOwn` for object iteration.
*
* @static
* @memberOf _
* @since 0.1.0
* @alias each
* @category Collection
* @param {Array|Object} collection The collection to iterate over.
* @param {Function} [iteratee=_.identity] The function invoked per iteration.
* @returns {Array|Object} Returns `collection`.
* @see _.forEachRight
* @example
*
* _.forEach([1, 2], function(value) {
* console.log(value);
* });
* // => Logs `1` then `2`.
*
* _.forEach({ 'a': 1, 'b': 2 }, function(value, key) {
* console.log(key);
* });
* // => Logs 'a' then 'b' (iteration order is not guaranteed).
*/
function forEach(collection, iteratee) {
var func = isArray(collection) ? arrayEach : baseEach;
return func(collection, castFunction(iteratee));
}
/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeMax = Math.max;
/**
* Gets the index at which the first occurrence of `value` is found in `array`
* using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* for equality comparisons. If `fromIndex` is negative, it's used as the
* offset from the end of `array`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Array
* @param {Array} array The array to inspect.
* @param {*} value The value to search for.
* @param {number} [fromIndex=0] The index to search from.
* @returns {number} Returns the index of the matched value, else `-1`.
* @example
*
* _.indexOf([1, 2, 1, 2], 2);
* // => 1
*
* // Search from the `fromIndex`.
* _.indexOf([1, 2, 1, 2], 2, 2);
* // => 3
*/
function indexOf(array, value, fromIndex) {
var length = array == null ? 0 : array.length;
if (!length) {
return -1;
}
var index = fromIndex == null ? 0 : toInteger(fromIndex);
if (index < 0) {
index = nativeMax(length + index, 0);
}
return baseIndexOf(array, value, index);
}
/** `Object#toString` result references. */
var numberTag = '[object Number]';
/**
* Checks if `value` is classified as a `Number` primitive or object.
*
* **Note:** To exclude `Infinity`, `-Infinity`, and `NaN`, which are
* classified as numbers, use the `_.isFinite` method.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a number, else `false`.
* @example
*
* _.isNumber(3);
* // => true
*
* _.isNumber(Number.MIN_VALUE);
* // => true
*
* _.isNumber(Infinity);
* // => true
*
* _.isNumber('3');
* // => false
*/
function isNumber(value) {
return typeof value == 'number' ||
(isObjectLike(value) && baseGetTag(value) == numberTag);
}
const lsSwimlaneCss = ":host{font-family:var(--ls-font-family);position:relative;display:block;min-width:800px}.loading,.error{font-size:var(--ls-text-base);line-height:var(--ls-line-base);font-family:var(--ls-font-family);color:var(--ls-font-primary-color);position:absolute;top:0;width:100%;height:100%;display:grid;align-items:center;justify-items:center;background-color:white}.disconnected svg{display:none}.disconnected::after{content:\"Disconnected\";position:absolute;width:100%;padding-top:20px;box-sizing:border-box;height:100%;font-size:2.5rem;color:#71717a;text-transform:uppercase;top:0;text-align:center;border:1px solid #71717a;background-color:rgba(255, 255, 255, 1)}.sensors-labels rect{fill:white}.sensors-labels text{fill:var(--ls-font-primary-color);font-size:var(--ls-text-xs)}.swim-lanes text{fill:var(--ls-font-primary-color)}";
const LsSwimlane$1 = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement {
constructor() {
super();
this.__registerHost();
this.__attachShadow();
this.DEBOUNCEDELAY = 350;
// Using a debounce here to save resources
this.handleResize = debounce(() => {
// Redraw the chart
this.drawChart();
this.updateTimeline();
}, this.DEBOUNCEDELAY,
// Enables the first trigger i.e. for when the screen gets maximized or media queries are triggered
{ leading: true });
/**
* Loading indicator
*/
this.chartLoading = true;
this.maxWidth = 0;
this.swimLineWidth = 0;
this.sensorLabelsWidth = 0;
this.dateRange = { from: null, to: null };
this.storedAndOrderedSensorData = {};
/**
* Create fixed colors for sensor ID's
* @param value the value of the sensor
* @param sensorType the type of sensor
*/
this.sensorValueColorCode = (value, sensorType) => {
let result = "#ffffff";
const switchValue = `${value}`;
switch (switchValue) {
case "+":
result = "#FEF08A";
break;
case "-":
result = "#E9D5FF";
break;
case "U":
result = "#FEF08A";
break;
case "D":
result = "#E9D5FF";
break;
case "O":
result = "#BBF7D0";
break;
case "C":
result = "#FECACA";
break;
case "X":
result = "#E4E4E7";
break;
}
const number = parseInt(value, 10);
if (!isNaN(number) && isNumber(number)) {
if ((!sensorType || sensorType !== "514") && isNumber(value)) {
result = "#C7D2FE";
}
else if (number % 2 === 0) {
result = "#CFFAFE";
}
else {
result = "#A5F3FC";
}
}
return result;
};
/**
* Settings object with some values
*/
this.swimLaneSettings = {
minHeight: 200,
laneHeight: 25,
timer: null,
subscriptions: [],
swimlanes: {},
labels: [],
scale: {
units: "minute",
value: 1,
},
};
this.clearChart = () => {
// clear out the html of the chart so that we have a clear chart
select(this.canvas).html("");
};
/**
* Create initial chart layout
*/
this.drawChart = () => {
// lets first remove the current chart before we draw a new one to prevent overlapping
this.clearChart();
// Calculate and apply chart width
this.maxWidth = this.canvas.clientWidth <= 0 ? 1 : this.canvas.clientWidth;
this.sensorLabelsWidth =
this.canvas.clientWidth < 800
? this.canvas.clientWidth * 0.3
: this.canvas.clientWidth * 0.25;
this.swimLineWidth =
this.canvas.clientWidth < 800
? this.canvas.clientWidth * 0.7
: this.canvas.clientWidth * 0.75;
// Chart
const chart = select(this.canvas)
.append("svg")
.attr("width", this.maxWidth)
.attr("height", this.swimLaneSettings.minHeight)
.attr("class", "chart");
// Swimlanes
const swimlanes = chart
.append("g")
.attr("width", this.swimLineWidth)
.attr("transform", `translate(${Math.floor(this.sensorLabelsWidth)},0)`)
.attr("class", "swimlanes");
// Sensor
const sensorLabels = chart
.append("g")
.attr("width", this.sensorLabelsWidth)
.attr("class", "sensors-labels");
// Sensor background
sensorLabels
.append("rect")
.attr("width", this.sensorLabelsWidth)
.attr("height", "100%");
// Main group
const main = chart
.append("g")
.attr("width", this.swimLineWidth)
.attr("transform", `translate(${Math.floor(this.sensorLabelsWidth)},0)`)
.attr("class", "main");
// Horizontal and vertical scales
const endDate = dayjs_min().toDate();
const startDate = dayjs_min()
.subtract(this.swimLaneSettings.scale.value, this.swimLaneSettings.scale.units)
.toDate();
const horizontalScale = time()
.domain([startDate, endDate])
.range([0, this.swimLineWidth]);
this.xAxisScale = horizontalScale;
this.dateRange.from = startDate;
this.dateRange.to = endDate;
// Add the x-axis
const horizontalAxis = axisBottom(horizontalScale)
.ticks(5, "s")
.tickFormat(timeFormat("%H:%M:%S"))
.tickSize(6);
// Append the labels to the x axis
main
.append("g")
.attr("transform", `translate(0,${this.sensorIdsArray.length * this.swimLaneSettings.laneHeight})`)
.attr("class", "main axis time")
.call(horizontalAxis)
.selectAll("text")
.attr("dx", 5)
.attr("dy", 12);
this.xAxis = horizontalAxis;
swimlanes.append("g").attr("class", "paths");
// Add swimlane for each sensor
forEach(this.sensorIdsArray, (sensorId) => {
this.addSwimlane(sensorId);
});
};
/**
* Add swimlane to chart
*/
this.addSwimlane = (sensorId) => {
if (this.xAxis !== undefined) {
// Only add swimlanes for sensors that we support
if (indexOf(this.sensorIdsArray, sensorId) > -1) {
// The height of the swimlanes is based on the number of existing swimlanes
const totalHeight = this.swimLaneSettings.laneHeight *
(keys(this.swimLaneSettings.swimlanes).length + 1) +
23;
const indexHeight = this.swimLaneSettings.laneHeight *
this.sensorIdsArray.indexOf(sensorId);
// Put the ticks (date and time) at the bottom
const chart = select(this.canvas);
// Increase the height of the canvas tot the total number of lanes + 23px for the bottom axis
chart.select("svg").attr("height", totalHeight);
// Add swimlane group
const swimlaneGroup = chart
.select(".swimlanes")
.append("g")
.attr("width", this.swimLineWidth)
.attr("height", this.swimLaneSettings.laneHeight)
.attr("class", "swimlane")
.attr("transform", `translate(0,${indexHeight})`);
// Add label group
const labelGroup = chart
.select(".sensors-labels")
.append("g")
.attr("width", this.sensorLabelsWidth)
.attr("height", this.swimLaneSettings.laneHeight)
.attr("class", "sensor")
.attr("transform", `translate(0,${indexHeight})`);
// Add text label
labelGroup
.append("text")
.text(sensorInfo.getLabel(sensorId))
.attr("transform", `translate(0,${this.swimLaneSettings.laneHeight - 9})`)
.attr("font-size", "10")
.attr("class", "sensor-title");
// Add lines
swimlaneGroup
.append("line")
.attr("x1", 0)
.attr("y1", this.swimLaneSettings.laneHeight)
.attr("x2", this.swimLineWidth)
.attr("y2", this.swimLaneSettings.laneHeight)
.attr("stroke", "lightgray");
labelGroup
.append("line")
.attr("x1", 0)
.attr("y1", this.swimLaneSettings.laneHeight)
.attr("x2", this.sensorLabelsWidth)
.attr("y2", this.swimLaneSettings.laneHeight)
.attr("stroke", "lightgray");
// Add the swimlane
this.swimLaneSettings.swimlanes[sensorId] = swimlaneGroup;
this.swimLaneSettings.labels.push({
element: labelGroup,
index: indexOf(this.sensorIdsArray, sensorId),
});
}
}
};
/**
* Add data to state
*/
this.addData = (sensorId, value, timestamp) => {
if (isNil(sensorId))
return;
if (!this.sensorIdsArray.includes(sensorId))
return;
let sensorDataCollection;
if (!isArray(this.storedAndOrderedSensorData[`${sensorId}`])) {
sensorDataCollection = [];
}
else {
sensorDataCollection = this.storedAndOrderedSensorData[`${sensorId}`];
}
const lastValue = last(sensorDataCollection);
// Previous entry found with other value than current
if (!isNil(lastValue) && lastValue.value !== value) {
// Remove previous
sensorDataCollection.pop();
// Add previous back with end time
sensorDataCollection.push({
value: lastValue.value,
start: lastValue.start,
color: "#eee",
end: timestamp, // Start time of next is end time of previous
});
}
// Add new entry
sensorDataCollection.push({
value,
start: timestamp,
color: "#eee", // Color will be overwritten
});
// Update all entries
this.storedAndOrderedSensorData[`${sensorId}`] = sortBy(sensorDataCollection, "start");
};
this.updateTimeline = () => {
const now = dayjs_min();
const results = [];
const theLimit = now
.clone()
.subtract(this.swimLaneSettings.scale.value, this.swimLaneSettings.scale.units);
// Remove 'old' items first
// These are the items that would otherwise overflow on left side
forEach(this.storedAndOrderedSensorData, (data, sensorId) => {
const intSensorId = parseInt(sensorId, 10);
if (indexOf(this.sensorIdsArray, intSensorId) > -1) {
this.storedAndOrderedSensorData[`${sensorId}`] = filter(data, (sensorUpdate) => {
const end = get(sensorUpdate, "end", +new Date());
return theLimit.isBefore(dayjs_min(end));
});
}
});
forEach(this.storedAndOrderedSensorData, (data, sensorId) => {
const intSensorId = parseInt(sensorId, 10);
if (indexOf(this.sensorIdsArray, intSensorId) > -1) {
const swimlaneIndex = indexOf(this.sensorIdsArray, intSensorId);
forEach(data, (sensorUpdate) => {
if (isObject(sensorUpdate)) {
const start = get(sensorUpdate, "start");
let value = get(sensorUpdate, "value", "?");
const end = get(sensorUpdate, "end", +new Date());
// The values of sensor 501, 514 and 515 are zero based, but we need to show them one based
if (includes([501, 514, 515], intSensorId) &&
!isNaN(parseInt(value, 10))) {
value = parseInt(value, 10) + 1;
}
results.push({
start: new Date(start),
end: new Date(end),
index: swimlaneIndex,
color: this.sensorValueColorCode(value, sensorId),
value: value,
});
}
});
}
});
const chart = select(this.canvas);
// Remove all references
const swimlaneGroup = chart.select(".swimlanes");
swimlaneGroup.selectAll(".mainItemGroup").remove();
// Add the rectangles
const rectangles = swimlaneGroup.selectAll("rect").data(results).enter();
rectangles
.append("g")
.attr("height", this.swimLaneSettings.laneHeight)
.attr("class", "mainItemGroup")
.attr("transform", (d) => {
let theBegin = new Date(d.start);
if (theLimit.isAfter(theBegin)) {
theBegin = theLimit.toDate();
}
return `translate( ${this.xAxisScale(theBegin)},${this.swimLaneSettings.laneHeight * d.index + 2})`;
})
.append("rect")
.attr("width", (d) => {
const theEnd = dayjs_min(d.end);
let theBegin = new Date(d.start);
if (theLimit.isAfter(theBegin)) {
theBegin = theLimit.toDate();
}
if (theLimit.isAfter(theEnd)) {
return 0;
}
else {
let result = this.xAxisScale(theEnd) - this.xAxisScale(theBegin);
// We need a minimum size for the rectangle
//
if (result < 1) {
result = 5;
}
return result;
}
})
.attr("height", () => 20)
.style("fill", (d) => d.color)
.select(function () {
return this.parentNode;
})
.append("text")
.text((d) => {
let value = d.value;
switch (value) {
case "D":
value = "↓";
break;
case "U":
value = "↑";
break;
case "I":
case "O":
case "C":
case "X":
case "-":
case "+":
value = "";
break;
}
return value;
})
.attr("text-anchor", "middle")
.attr("font-size", "9")
.attr("font-weight", "bold")
.attr("class", "sensor-value")
.attr("transform", (d) => {
const theEnd = dayjs_min(d.end);
let theBegin = new Date(d.start);
if (theLimit.isAfter(theBegin)) {
theBegin = theLimit.toDate();
}
if (theLimit.isAfter(theEnd)) {
return "translate(0,0)";
}
else {
const width = this.xAxisScale(theEnd) - this.xAxisScale(theBegin);
return `translate(${width / 2},13)`;
}
})
.filter((d) => d.value === "O" ||
d.value === "C" ||
d.value === "X" ||
d.value === "-" ||
d.value === "+")
.select(function () {
return this.parentNode;
})
.append("image")
.attr("href", (d) => {
let source = getAssetPath(`./assets/elevator.png`);
switch (d.value) {
case "O":
source = getAssetPath(`./assets/elevator-open.png`);
break;
case "C":
source = getAssetPath(`./assets/elevator-closed.png`);
break;
case "X":
source = getAssetPath(`./assets/elevator-locked.png`);
break;
case "-":
source = getAssetPath(`./assets/elevator-closing.png`);
break;
case "+":
source = getAssetPath(`./assets/elevator-opening.png`);
break;
}
return source;
})
.attr("x", (d) => {
const theEnd = dayjs_min(d.end);
let theBegin = new Date(d.start);
if (theLimit.isAfter(theBegin)) {
theBegin = theLimit.toDate();
}
if (theLimit.isAfter(theEnd)) {
return 0;
}
else {
const width = this.xAxisScale(theEnd) - this.xAxisScale(theBegin);
return Math.floor(width / 2 - 6);
}
})
.attr("y", 3)
.attr("width", 12)
.attr("height", 12);
this.xAxisScale.domain([theLimit.toDate(), now]);
// update the axis the D3.js way
chart.select(".main.axis.time").call(this.xAxis);
};
/**
* This method handles every incoming event from any sensor
*/
this.handleSensorUpdate = (data, timestamp) => {
if (!isNil(data)) {
const sensorId = parseInt(keys(data)[0], 10);
const value = values(data)[0];
const ts = timestamp || +new Date();
this.addData(sensorId, value, ts);
}
};
this.processHistoricalData = (sensorValues) => {
if (!isNil(values)) {
for (const value of sensorValues) {
const { rows, sensor } = value;
if (!isEmpty(rows)) {
rows.forEach((row) => {
const update = {};
update[sensor.id] = row.val;
this.handleSensorUpdate(update, +new Date(row.ts));
});
}
}
}
};
this.processLiveData = (sensorData) => {
const dataValues = sensorData.values;
const timestamps = sensorData.updated;
for (let i = 0; i < keys(dataValues).length; i++) {
const formattedDataValues = {};
formattedDataValues[keys(dataValues)[i]] = values(dataValues)[i];
const timestamp = timestamps[keys(dataValues)[i]];
this.handleSensorUpdate(formattedDataValues, timestamp);
}
};
/**
* Start (and stop) the animation interval
*/
this.timelineTimer = () => {
clearInterval(this.interval);
this.interval = setInterval(() => {
this.updateTimeline();
}, 500);
};
this.sensorIds = "500,501,502,503,514,515";
this.idKey = undefined;
this.sensorIdsArray = this.sensorIds.split(",").map(Number);
}
// Listen for resize events and recalculate
resizeWindow() {
this.handleResize();
}
async componentWillLoad() {
this.store = await getStore(this.idKey);
// Remove back door sensor if installation does not have one
if (!this.store.state.hasBackDoor) {
this.sensorIdsArray = this.sensorIdsArray.filter((value) => {
return value !== 503;
});
}
this.chartLoading = false;
}
componentDidLoad() {
this.drawChart();
// Fill chart with historical data
this.processHistoricalData(this.store.state.historicalSensorData);
// Start the timer to update the swim lane at regular intervals
this.timelineTimer();
// Subscribe to future sensor value updates
this.store.onChange("currentSensorData", (sensorData) => {
this.processLiveData(sensorData);
});
// Remove the loading indicator
this.chartLoading = false;
}
render() {
let errorMessage = "";
if (!isEmpty(this.store.state.error.type)) {
if (this.store.state.error.type === "Token") {
errorMessage = this.store.state.error.message;
}
else {
errorMessage = "Geen verbinding";
}
clearInterval(this.interval);
this.interval = null;
}
return (h(Host, null, h("div", { ref: (el) => {
this.canvas = el;
} }, h("div", { class: "chart" })), this.chartLoading && errorMessage === "" ? (h("div", { class: "loading" }, "Laden...")) : (errorMessage !== "" && h("div", { class: "error" }, errorMessage)), h("slot", null)));
}
disconnectedCallback() {
window.clearInterval(this.interval);
this.interval = null;
}
static get assetsDirs() { return ["assets"]; }
static get style() { return lsSwimlaneCss; }
}, [1, "ls-swimlane", {
"sensorIds": [1, "sensor-ids"],
"idKey": [1, "id-key"],
"sensorIdsArray": [32]
}, [[9, "resize", "resizeWindow"]]]);
function defineCustomElement$1() {
if (typeof customElements === "undefined") {
return;
}
const components = ["ls-swimlane"];
components.forEach(tagName => { switch (tagName) {
case "ls-swimlane":
if (!customElements.get(tagName)) {
customElements.define(tagName, LsSwimlane$1);
}
break;
} });
}
const LsSwimlane = LsSwimlane$1;
const defineCustomElement = defineCustomElement$1;
export { LsSwimlane, defineCustomElement };
//# sourceMappingURL=ls-swimlane.js.map