diff --git a/multi-group.html b/multi-group.html new file mode 100644 index 0000000..07e2b75 --- /dev/null +++ b/multi-group.html @@ -0,0 +1,88 @@ + + + + + \ No newline at end of file diff --git a/multi-group.js b/multi-group.js new file mode 100644 index 0000000..a1d8b2a --- /dev/null +++ b/multi-group.js @@ -0,0 +1,384 @@ +const { create } = require('domain'); +var path = require('path'); + +module.exports = function (RED) { + var counter = 0; + + function MultiGroupNode(config) { + const iid = ++counter; + var ui = undefined; + try { + var node = this; + if (ui === undefined) { + ui = RED.require("node-red-dashboard")(RED); + } + RED.nodes.createNode(this, config); + + var html = ` + +
+ `; + + var done = ui.addWidget({ + node: node, + order: config.order, + group: config.group, + width: config.width, + height: config.height, + format: html, + templateScope: 'local', + emitOnlyNewValues: false, + forwardInputMessages: false, + storeFrontEndInputAsState: false, + convertBack: function (value) { + return value; + }, + beforeEmit: function (msg, value) { + return { + msg: { iid: iid, width: config.width, height: config.height, sizes: ui.getSizes(), items: value, socketid: msg.socketid } + }; + }, + beforeSend: function (msg, orig) { + if (orig) { + orig.msg.topic = config.topic; + return orig.msg; + } + }, + initController: function ($scope, events) { + let sizes; + let lastIid = -1; + let superStorage = {}; + + + function isSameObject(obj1, obj2) { + for (var p in obj1) { + if (p === 'action' || p === 'internal') { + continue; + } + + if (obj1.hasOwnProperty(p) !== obj2.hasOwnProperty(p)) { + return false; + } + + switch (typeof (obj1[p])) { + // Deep compare objects + case 'object': + if (!isSameObject(obj1[p], obj2[p])) { + return false; + } + break; + // Compare function code + case 'function': + if (typeof (obj2[p]) == 'undefined' || (obj1[p].toString() != obj2[p].toString())) { + return false; + } + break; + // Compare array elements + case 'array': + if (typeof (obj2[p]) == 'undefined' || obj1[p].length != obj2[p].length) { + return false; + } + for (let i = 0; i < obj1[p].length; i++) { + if (!isSameObject(obj1[p][i], obj2[p][i])) { + return false; + } + } + // Compare values + default: + if (obj1[p] != obj2[p]) { + return false; + } + } + } + + // Check object 2 for any extra properties + for (var p in obj2) { + if (typeof (obj1[p]) == 'undefined') { + return false; + } + } + + return true; + } + + function widthToPx(width, includeMargin = true) { + let widthPx = (width * sizes.sx); + + if (includeMargin) { + widthPx = widthPx - (sizes.gx * 2); + } + + return widthPx; + } + + function heightToPx(height, includeMargin = true) { + let heightPx = (height * sizes.sy); + + if (includeMargin) { + heightPx = heightPx - (sizes.gy * 2); + } + + return heightPx; + } + + + + function checkIntegrity(items, parentId) { + let id = 0; + + // Check if parentId ends with a number (It is the root node then and does not need to be checked) + if (!(/\d$/.test(parentId))) { + const parentFromStorage = superStorage[parentId]; + + if (!parentFromStorage) { + return false; + } + + if (items.length !== parentFromStorage.items.length) { + return false; + } + } + + for (const item of items) { + id = id + 1; + const itemId = `${parentId}-${id}-${item.type}`; + const itemFromStorage = superStorage[itemId]; + + if (!itemFromStorage) { + return false; + } + + if (item.width !== itemFromStorage.width || item.height !== itemFromStorage.height) { + return false; + } + + if (isSameObject(item, itemFromStorage)) { + item.action = 'skip'; + } else { + item.action = 'update'; + } + + if (item.type === 'container') { + if (!checkIntegrity(item.items, itemId)) { + return false; + } + item.action = 'update'; + } + } + + return true; + } + + function createContainer(parent, config, containerId, includeMargin = false) { + + const widthPx = widthToPx(config.width, includeMargin); + const heightPx = heightToPx(config.height, includeMargin); + + const container = $(``); + + container.css("width", `${widthPx}px`); + container.css("height", `${heightPx}px`); + if (includeMargin) { + container.css("margin", `${sizes.gx}px ${sizes.gy}px`); + } else { + container.css("margin", `0px 0px`); + } + + parent.append(container); + + updateContainer(container, config); + } + + function updateContainer(container, config) { + let id = 0; + for (const item of config.items) { + id = id + 1; + switch (item.action) { + case 'skip': + break; + case 'update': + switch (item.type) { + case 'container': + const containerId = `${container.attr('id')}-${id}-container`; + updateContainer(container.children(`#${containerId}`), item); + break; + case 'button': + const buttonId = `${container.attr('id')}-${id}-button`; + updateButton(container.children(`#${buttonId}`), item); + break; + case 'gauge': + const gaugeId = `${container.attr('id')}-${id}-gauge`; + updateGauge(container.children(`#${gaugeId}`), item); + break; + } + break; + default: + switch (item.type) { + case 'container': + const containerId = `${container.attr('id')}-${id}-container`; + createContainer(container, item, containerId); + break; + case 'button': + const buttonId = `${container.attr('id')}-${id}-button`; + createButton(container, item, buttonId); + break; + case 'gauge': + const gaugeId = `${container.attr('id')}-${id}-gauge`; + createGauge(container, item, gaugeId); + break; + default: + console.log(`Unknown msg.items[i] type: ${msg.items[i].type}`); + break; + } + break; + } + } + + superStorage[container.attr('id')] = config; + } + + function createButton(parent, config, buttonId) { + + const widthPx = widthToPx(config.width); + const heightPx = heightToPx(config.height); + + const button = $(` + + `); + + button.css("width", `${widthPx}px`); + button.css("height", `${heightPx}px`); + button.css("margin", `${sizes.gx}px ${sizes.gy}px`); + + button.click(function (e) { + if (config.payload) { + $scope.send({ "payload": config.payload }); + } + }); + + parent.append(button); + + updateButton(button, config); + } + + function updateButton(button, config) { + button.text(config.label); + button.css("background-color", config.color ? config.color : ""); + button.attr("disabled", config.disabled || false); + + superStorage[button.attr('id')] = config; + } + + function createGauge(parent, config, gaugeId) { + const canvasId = `${gaugeId}-canvas`; + const labelId = `${gaugeId}-label`; + + const widthPx = widthToPx(config.width); + const heightPx = heightToPx(config.height); + + const gaugeContainer = $(``); + + gaugeContainer.css("width", `${widthPx}px`); + gaugeContainer.css("height", `${heightPx}px`); + // gaugeContainer.css("margin", `${sizes.gx}px ${sizes.gy}px`); + + const gaugeLabel = $(``); + gaugeContainer.append(gaugeLabel); + + const gaugeCanvas = $(``); + gaugeContainer.append(gaugeCanvas); + + parent.append(gaugeContainer); + + let opts = { + angle: 0, // The span of the gauge arc + lineWidth: 0.4, // The line thickness + radiusScale: 1, // Relative radius + pointer: { + length: 0.6, // // Relative to gauge radius + strokeWidth: 0.07, // The thickness + color: '#000000' // Fill color + }, + limitMax: true, // If false, max value increases automatically if value > maxValue + limitMin: true, // If true, the min value of the gauge will be fixed + colorStart: '#6FADCF', // Colors + colorStop: '#8FC0DA', // just experiment with them + strokeColor: '#E0E0E0', // to see which ones work best for you + generateGradient: true, + highDpiSupport: true, // High resolution support + percentColors: config.smoothColors, + }; + + const gauge = new Gauge(gaugeCanvas[0]).setOptions(opts); + gauge.setMinValue(0); + + superStorage[`${gaugeId}-internal`] = gauge; + + updateGauge(gaugeContainer, config) + } + + function updateGauge(gauge, config) { + const gaugeId = gauge.attr('id'); + const gaugeLabel = $(`#${gaugeId}-label`); + const gaugeObj = superStorage[`${gaugeId}-internal`]; + gaugeObj.maxValue = config.max; + gaugeObj.set(config.value); + gaugeLabel.html(config.label); + + superStorage[gaugeId] = config; + } + + $scope.$watch('msg', function (msg) { + if (!msg) { + return; + } + const iid = msg.iid; + sizes = msg.sizes; + + var root = $(`#multi-${iid}`); + + const rootItems = [{ type: "container", width: msg.width, height: msg.height, items: msg.items }]; + + if (!checkIntegrity(rootItems, `${root.attr('id')}`) || lastIid !== iid) { + // Clear data + root.children().remove(); + superStorage = {}; + + // Set new iid + lastIid = iid; + + // Reinitialize + root = $(`#multi-${iid}`); + checkIntegrity(rootItems, `${root.attr('id')}`); + + if (!root.length) { + return; + } + + const rootContainerId = `${root.attr('id')}-1-container`; + createContainer(root, rootItems[0], rootContainerId); + } else { + updateContainer(root.children(), rootItems[0]); + } + }); + } + }); + } catch (e) { + console.log(e) + } + node.on('close', done); + } + + RED.nodes.registerType("ui_multi_group", MultiGroupNode); + + var uipath = 'ui'; + if (RED.settings.ui) { uipath = RED.settings.ui.path; } + var fullPath = path.join('/', uipath, '/ui-multi-group/*').replace(/\\/g, '/'); + RED.httpNode.get(fullPath, function (req, res) { + var options = { + root: __dirname + '/lib/', + dotfiles: 'deny' + }; + res.sendFile(req.params[0], options) + }); +} \ No newline at end of file diff --git a/package.json b/package.json index 3d4a11f..703fbdc 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "node-red": { "nodes": { "ui_button_group": "button-group.js", - "ui_gauge_group": "gauge-group.js" + "ui_gauge_group": "gauge-group.js", + "ui_multi_group": "multi-group.js" } } } \ No newline at end of file