/**
* Copyright 2021 Christian Meinert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var path = require('path');
module.exports = function(RED) {
function HTML(config) {
var configAsJson = JSON.stringify(config).replace(/[\/\(\)\']/g, "'");
var html;
html = String.raw`
`;
return html;
}
function checkConfig(node, conf) {
if (!conf || !conf.hasOwnProperty("group")) {
node.error(RED._("ui_uplot-charts.error.no-group"));
return false;
}
return true;
}
var ui = undefined;
function uPlotUINode(config) {
try {
var node = this;
var _dataMap = {}; // data lookup topic vs array rows
if(ui === undefined) {
ui = RED.require("node-red-dashboard")(RED);
}
RED.nodes.createNode(this, config);
if (checkConfig(node, config)) {
// Add default values to older nodes (version 1.0.0)
config.stateField = config.stateField || 'payload';
config.enableField = config.enableField || 'enable';
config.width=parseInt(config.width);
config.height=parseInt(config.height);
var group = RED.nodes.getNode(config.group);
config.groupId = group.id;
getSiteProperties = function () {
var opts = {}
opts.sizes = { sx: 48, sy: 48, gx: 4, gy: 4, cx: 4, cy: 4, px: 4, py: 4 }
opts.theme = {
'group-borderColor': {
value: "#097479"
}
}
if (typeof ui.getSizes === "function") {
if (ui.getSizes()) {
opts.sizes = ui.getSizes();
}
if (ui.getTheme()) {
opts.theme = ui.getTheme();
}
}
return opts
}
config.site = getSiteProperties();
var getUiControl = function () {
return {
}
}
config.ui_control = getUiControl();
/**
* calculate horizontal dimension in pixel out of grid units
* @param {number} gridX width in number of grids
* @return {number} dimension in pixel
**/
var getX = function(gridX) {
return ((gridX===0) ? 0 : parseInt(config.site.sizes.sx * gridX + config.site.sizes.cx * (gridX - 1)));
}
/**
* calculate vertical dimension in pixel out of grid units
* @param {number} gridY height in number of grids
* @return {number} dimension in pixel
**/
var getY = function(gridY) {
return ((gridY===0) ? 0 : parseInt(config.site.sizes.sy * gridY + config.site.sizes.cy * (gridY - 1)));
}
config.widgetProperties = {
x: getX((config.width!==0) ? config.width : group.config.width)-12,
y: getY((config.height!==0) ? config.height : group.config.width)-12, // square as default
};
node.on("input", function(msg) {
node.topi = msg.topic;
});
var sizes = ui.getSizes();
var html = HTML(config); // *REQUIRED* !!DO NOT EDIT!!
var done = ui.addWidget({ // *REQUIRED* !!DO NOT EDIT!!
type: 'chart',
label: config.label,
tooltip: config.tooltip,
node: node, // *REQUIRED* !!DO NOT EDIT!!
order: config.order, // *REQUIRED* !!DO NOT EDIT!!
group: config.group, // *REQUIRED* !!DO NOT EDIT!!
width: config.width, // *REQUIRED* !!DO NOT EDIT!!
height: config.height, // *REQUIRED* !!DO NOT EDIT!!
format: html, // *REQUIRED* !!DO NOT EDIT!!
templateScope: "local", // *REQUIRED* !!DO NOT EDIT!!
emitOnlyNewValues: false, // *REQUIRED* Edit this if you would like your node to only emit new values.
forwardInputMessages: false, // *REQUIRED* Edit this if you would like your node to forward the input message to it's ouput.
storeFrontEndInputAsState: false, // *REQUIRED* If the widget accepts user input - should it update the backend stored state ?
convertBack: function (value) {
console.log('convertBack',value);
return value;
},
convert: function(value,fullDataset,msg,step) {
console.log('convert',fullDataset,msg,step);
var conversion = {
updatedValues: [],
newPoint: {},
}
if (fullDataset) conversion.updatedValues = fullDataset;
if (msg.topic=='') msg.topic = "unknown";
if (node._dataMap === undefined) node._dataMap = {};
if (!node._dataMap.hasOwnProperty(msg.topic)) {
node._dataMap[msg.topic] = {index: Object.keys(node._dataMap).length+1};
// newMsg._dataMap = node._dataMap;
}
node._dataMap[msg.topic].lastUpdate = Math.floor((msg.timestamp) ? msg.timestamp : (Date.now()/1000));
// Try to get the specified message fields, and copy those to predefined fixed message fields.
// This way the message has been flattened, so the client doesn't need to access the nested msg properties.
// See https://discourse.nodered.org/t/red-in-ui-nodes/29824/2
try {
// Get the new state value from the specified message field
conversion.newPoint.state = RED.util.getMessageProperty(msg, config.stateField || "payload");
}
catch(err) {
// No problem because the state field is optional ...
}
conversion.newPoint.topic = msg.topic;
conversion.newPoint.dataId = node._dataMap[msg.topic];
conversion.updatedValues.push(conversion.newPoint);
conversion.update = true;
console.log('conversion done:', conversion);
return conversion;
},
beforeEmit: function(msg, fullDataset) {
if (Array.isArray(fullDataset)) { return fullDataset };
console.log(`beforeEmit:`, msg, fullDataset);
var newMsg = fullDataset;
if (msg) {
// Copy the socket id from the original input message.
if (msg.socketid) newMsg.socketid = msg.socketid;
console.log(`beforeEmit: (${msg.socketid})`, newMsg);
} else {
console.log('no Message',msg,fullDataset)
}
return { msg: newMsg };
},
beforeSend: function (msg, orig) {
//console.log('beforeSend:',msg,orig,config.outFormat);
if (orig) {
var newMsg = {};
// Store the switch state in the specified msg state field
RED.util.setMessageProperty(newMsg, config.stateField, orig.msg.state, true)
//orig.msg = newMsg;
node.status({fill:"red", shape:'dot', text:"ok?"});
var topic = RED.util.evaluateNodeProperty(config.topic,config.topicType || "str",node,msg) || node.topi;
if (topic) { newMsg.topic = topic; }
return newMsg;
}
},
initController: function($scope, events) {
var _data = [];
var widgetDiv;
$scope.flag = true; // not sure if this is needed?
/**
* creates an instance of the uPlot.js widget
* - destroys existing instance
* - register callback functions
* - `input:start` sets the `$scope.inputStarted` flag and btn an modal references
* - `input:move`
* - `input:end`
* @return {void}
*/
var createUPlot = function () {
console.log("create uPlot",$scope.config);
if (!document.querySelector(widgetDiv)) {
//
var uplotDiv = document.createElement("div");
uplotDiv.setAttribute("class", 'uplot-charts-widget');
uplotDiv.setAttribute("style", `width:${$scope.config.widgetProperties.x}px;`);
uplotDiv.setAttribute("id", 'ui_uplot_chart-' + $scope.$eval('$id'));
document.getElementById(`uplot-charts-container-${$scope.config.id}`).appendChild(uplotDiv);
}
if($scope.uPlot !== undefined) {
$scope.uPlot.destroy();
}
$scope.uPlot = uPlot($scope.opts,_data,document.querySelector(widgetDiv));
}
$scope.init = function (config) {
console.log('uPlot init',config,_data);
$scope.config = config;
widgetDiv = '#ui_uplot_chart-' + $scope.$eval('$id');
if (_data===undefined) _data = [];
$scope.opts = {
title: "My Chart",
id: widgetDiv,
width: $scope.config.widgetProperties.x,
height: $scope.config.widgetProperties.y,
series: [
{},
{
// initial toggled state (optional)
show: true,
spanGaps: true,
// in-legend display
label: "Value 1",
value: (self, rawValue) => (Number(rawValue)) ? rawValue.toFixed(1)+"hPa" : "",
// series style
stroke: "red",
width: 1,
fill: "rgba(255, 0, 0, 0.3)",
dash: [10, 5],
}
],
};
createUPlot();
};
$scope.$watch('msg', function(msg) {
if (!msg) { return; } // Ignore undefined msg
console.log('uPlot Message received:',msg,_data);
if (msg.hasOwnProperty('state') && msg.dataId && msg.dataId.lastUpdate){
if (_data[0]===undefined) _data[0]=[];
let index = _data[0].push(msg.dataId.lastUpdate);
switch (typeof msg.state) {
case "string":
msg.state = Number (msg.state);
case "number":
if (_data[msg.dataId.index]===undefined) _data[msg.dataId.index]=[];
_data[msg.dataId.index][index-1]=msg.state;
break;
}
console.log('Data:',_data);
$scope.uPlot.setData(_data);
}
});
$scope.colorButtonPress = function(e){
if(document.getElementById(`colorModal-${$scope.config.id}`)) {
document.getElementsByTagName("body")[0].removeChild(document.getElementById(`colorModal-${$scope.config.id}`));
};
var modal = document.getElementsByTagName("body")[0].appendChild(document.createElement("div"));
};
}
});
}
}
catch (e) {
console.warn(e); // catch any errors that may occur and display them in the web browsers console
}
node.on("close", function() {
console.log("uPlot close");
done();
})
}
setImmediate(function() {
RED.nodes.registerType("ui_uplot-charts", uPlotUINode);
var uipath = 'ui';
if (RED.settings.ui) { uipath = RED.settings.ui.path; }
var fullPath = path.join('/', uipath, '/ui-uplot-charts/*').replace(/\\/g, '/');
RED.httpNode.get(fullPath, function (req, res) {
var options = {
root: __dirname + '/lib/',
dotfiles: 'deny'
};
res.sendFile(req.params[0], options)
});
})
}