UNPKG

ednl-liftstatus-web-components

Version:
688 lines (677 loc) 23.8 kB
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