├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── icons └── uilevel.png ├── img ├── node-red-dashboard-ui-level-shapes.JPG └── node-red-dashboard-ui-level.JPG ├── lib └── js │ ├── gsap.min.js │ └── gsap.min.js.map ├── package-lock.json ├── package.json ├── ui-level.html └── ui-level.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Environment and versions (please complete the following information):** 23 | - OS: [e.g. iOS] 24 | - Browser [e.g. chrome, safari] 25 | - Node-RED version 26 | - Dashboard version 27 | - ui-level version 28 | 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | 64 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at nipnippel@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 hotNipi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-red-contrib-ui-level 2 | 3 | [![NodeRed](https://img.shields.io/badge/Node--Red-0.20.5+-red.svg)](http://nodered.org) [![NPM version][npm-image]][npm-url] [![CodeFactor](https://www.codefactor.io/repository/github/hotnipi/node-red-contrib-ui-level/badge)](https://www.codefactor.io/repository/github/hotnipi/node-red-contrib-ui-level) ![npm](https://img.shields.io/npm/dm/node-red-contrib-ui-level) 4 | 5 | [npm-image]: http://img.shields.io/npm/v/node-red-contrib-ui-level.svg 6 | [npm-url]: https://npmjs.org/package/node-red-contrib-ui-level 7 | 8 | Adds a linear Level type widget to the user interface 9 | 10 | ![Node-RED dashboard widget node-red-contrib-ui-level](img/node-red-dashboard-ui-level.JPG) 11 | 12 | # Requirements 13 | 14 | Node-Red v0.20.5 or greater. Node-Red-dashboard v2.15.0 or greater. 15 | 16 | # Configuration and behavior 17 | 18 | Widget has 3 different layouts: `Single Horizontal`, `Pair Horizontal` and `Single Vertical`. 19 | 20 | Widget takes the configured input property and displays it's value at the top of the level graphics. For `Pair Horizontal` layout the input is expected to be an `array`. The value of input is validated to find a numeric value. So you can send number `msg.payload = 15` or string `msg.payload = "15"` 21 | 22 | The node's `Label` is displayed near the value. Leave the label field empty to show value only. The Label can be changed on fly by using `msg.control = {label:"New Label"}`. For `Pair Horizontal` layout the `Label` is not displayed but both `Channels` can be labelled independently. 23 | 24 | All four colors for stripes can be customized. In addition, there is three options to modify the colors of the level bar. `Multiple segments` - Colors tied to stripes according to sector values. `Single color bar` - Single value-based color for all active stripes. `Interpolated colors` - Colors interpolated from normal to high color. If you choose `Multiple segments` for your stripe colors, you can optionally use the `Peak mode`. 25 | 26 | By selecting the `Peak mode`, you can adjust peak hold time (in milliseconds, validated to be in between `800 ... 10000)` or `infinity` to hold peak until node receives `msg.peakreset = true` 27 | 28 | The node's `Unit` will be displayed near the current value. Exact position of the unit depends on layout. The unit can be any `string`, for example: `lbs psi F°` Set the unit to empty string if you don't need to display it. 29 | 30 | The `min` and `max` values are customizable within the configuration or you can change them on fly by sending new values with `msg.control` property. The `segments` values are also customizable within the configuration and with `msg.control` 31 | 32 | You can choose between 3 different stripe resolutions. `Superfine`, `Fine` and `Normal` 33 | 34 | You have option to show intermediate `tick values`. By choosing `Segments`, the `tick values` placed at segment positions, with option `Auto`, the tick values will be spread evenly. 35 | 36 | You can choose behavior of animations to be `Soft`, `Reactive` or `Rocket` representing speed of animations (800ms, 300ms and 100ms). Animations can be turned off completely. Animation of value text is turned off by default. You can turn it on but be aware, text animation affects performance significantly! 37 | 38 | Texts sizes and color in widget can be customized within the configuration only. Color applies for all texts in widget. There is 3 different sizes for text elements in use. Values represent font relative size with unit `"em"` 39 | 40 | ## Limitations 41 | 42 | Widget layout is optimized to look best with default `1x1` unit size `(48px x 48px)`. However, this is not a restriction. Smallest possible `1x1` unit size what is supported by dashboard `(24px x 24px)` is supported by widget also. But you cannot have intermediate tick values together with value field. Playing with text size options you can finetune the layout to fit better into your dashboard design. 43 | 44 | ## Examples of msg.control usage 45 | 46 | `msg.control = {min:10, max:80}` to change min and max values. `msg.control = {min:10, max:80, seg1:30, seg2:60}` to change min, max and segments all together. `msg.control = {seg2:60}` to change high segment value only. 47 | 48 | ## Performance alert 49 | 50 | This widget is not perfect choice to display high-frequent data changes like audio or similar. If you are using a lot of Level widgets on your dashboard and you are feeling performance loss, consider turning off animations of value text (if used) or turn off all animations. 51 | 52 | ### Licence 53 | 54 | This node uses GreenSock animation library GSAP licenced with Standard GreenSock License for non-commercial use https://greensock.com/standard-license/ 55 | -------------------------------------------------------------------------------- /icons/uilevel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotNipi/node-red-contrib-ui-level/061f28474de9179eaab8cd922e8f283de2defa64/icons/uilevel.png -------------------------------------------------------------------------------- /img/node-red-dashboard-ui-level-shapes.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotNipi/node-red-contrib-ui-level/061f28474de9179eaab8cd922e8f283de2defa64/img/node-red-dashboard-ui-level-shapes.JPG -------------------------------------------------------------------------------- /img/node-red-dashboard-ui-level.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotNipi/node-red-contrib-ui-level/061f28474de9179eaab8cd922e8f283de2defa64/img/node-red-dashboard-ui-level.JPG -------------------------------------------------------------------------------- /lib/js/gsap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * GSAP 3.6.0 3 | * https://greensock.com 4 | * 5 | * @license Copyright 2021, GreenSock. All rights reserved. 6 | * Subject to the terms at https://greensock.com/standard-license or for Club GreenSock members, the agreement issued with that membership. 7 | * @author: Jack Doyle, jack@greensock.com 8 | */ 9 | 10 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).window=t.window||{})}(this,function(e){"use strict";function _inheritsLoose(t,e){t.prototype=Object.create(e.prototype),(t.prototype.constructor=t).__proto__=e}function _assertThisInitialized(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function o(t){return"string"==typeof t}function p(t){return"function"==typeof t}function q(t){return"number"==typeof t}function r(t){return void 0===t}function s(t){return"object"==typeof t}function t(t){return!1!==t}function u(){return"undefined"!=typeof window}function v(t){return p(t)||o(t)}function M(t){return(h=mt(t,ot))&&ae}function N(t,e){return console.warn("Invalid property",t,"set to",e,"Missing plugin? gsap.registerPlugin()")}function O(t,e){return!e&&console.warn(t)}function P(t,e){return t&&(ot[t]=e)&&h&&(h[t]=e)||ot}function Q(){return 0}function $(t){var e,r,i=t[0];if(s(i)||p(i)||(t=[t]),!(e=(i._gsap||{}).harness)){for(r=pt.length;r--&&!pt[r].targetTest(i););e=pt[r]}for(r=t.length;r--;)t[r]&&(t[r]._gsap||(t[r]._gsap=new Lt(t[r],e)))||t.splice(r,1);return t}function _(t){return t._gsap||$(Tt(t))[0]._gsap}function aa(t,e,i){return(i=t[e])&&p(i)?t[e]():r(i)&&t.getAttribute&&t.getAttribute(e)||i}function ba(t,e){return(t=t.split(",")).forEach(e)||t}function ca(t){return Math.round(1e5*t)/1e5||0}function da(t,e){for(var r=e.length,i=0;t.indexOf(e[i])<0&&++it._dur||e._start<0))for(var r=t;r;)r._dirty=1,r=r.parent;return t}function wa(t){return t._repeat?gt(t._tTime,t=t.duration()+t._rDelay)*t:0}function ya(t,e){return(t-e._start)*e._ts+(0<=e._ts?0:e._dirty?e.totalDuration():e._tDur)}function za(t){return t._end=ca(t._start+(t._tDur/Math.abs(t._ts||t._rts||j)||0))}function Aa(t,e){var r=t._dp;return r&&r.smoothChildTiming&&t._ts&&(t._start=ca(r._time-(0j)&&e.render(r,!0)),ta(t,e)._dp&&t._initted&&t._time>=t._dur&&t._ts){if(t._dura;)s=s._prev;s?(e._next=s._next,s._next=e):(e._next=t[r],t[r]=e),e._next?e._next._prev=e:t[i]=e,e._prev=s,e.parent=e._dp=t}(t,e,"_first","_last",t._sort?"_start":0),t._recent=e,i||Ba(t,e),t}function Da(t,e){return(ot.ScrollTrigger||N("scrollTrigger",e))&&ot.ScrollTrigger.create(e,t)}function Ea(t,e,r,i){return Nt(t,e),t._initted?!r&&t._pt&&(t._dur&&!1!==t.vars.lazy||!t._dur&&t.vars.lazy)&&f!==Pt.frame?(ht.push(t),t._lazy=[e,i],1):void 0:1}function Ia(t,e,r,i){var n=t._repeat,a=ca(e)||0,s=t._tTime/t._tDur;return s&&!i&&(t._time*=a/t._dur),t._dur=a,t._tDur=n?n<0?1e10:ca(a*(n+1)+t._rDelay*n):a,s&&!i?Aa(t,t._tTime=t._tDur*s):t.parent&&za(t),r||ta(t.parent,t),t}function Ja(t){return t instanceof Bt?ta(t):Ia(t,t._dur)}function La(t,e){var r,i,n=t.labels,a=t._recent||vt,s=t.duration()>=U?a.endTime(!1):t._dur;return o(e)&&(isNaN(e)||e in n)?"<"===(r=e.charAt(0))||">"===r?("<"===r?a._start:a.endTime(0<=a._repeat))+(parseFloat(e.substr(1))||0):(r=e.indexOf("="))<0?(e in n||(n[e]=s),n[e]):(i=+(e.charAt(r-1)+e.substr(r+1)),1(n=Math.abs(n))&&(a=i,o=n);return a}function ib(t){return sa(t),t.progress()<1&&xt(t,"onInterrupt"),t}function nb(t,e,r){return(6*(t=t<0?t+1:1>16,t>>8&Ot,t&Ot]:0:Mt.black;if(!c){if(","===t.substr(-1)&&(t=t.substr(0,t.length-1)),Mt[t])c=Mt[t];else if("#"===t.charAt(0)){if(t.length<6&&(t="#"+(i=t.charAt(1))+i+(n=t.charAt(2))+n+(a=t.charAt(3))+a+(5===t.length?t.charAt(4)+t.charAt(4):"")),9===t.length)return[(c=parseInt(t.substr(1,6),16))>>16,c>>8&Ot,c&Ot,parseInt(t.substr(7),16)/255];c=[(t=parseInt(t.substr(1),16))>>16,t>>8&Ot,t&Ot]}else if("hsl"===t.substr(0,3))if(c=d=t.match(tt),e){if(~t.indexOf("="))return c=t.match(et),r&&c.length<4&&(c[3]=1),c}else s=+c[0]%360/360,o=c[1]/100,i=2*(u=c[2]/100)-(n=u<=.5?u*(o+1):u+o-u*o),3=r&&te)return i;i=i._next}else for(i=t._last;i&&i._start>=r;){if(!i._dur&&"isPause"===i.data&&i._start=n._start)&&n._ts&&h!==n){if(n.parent!==this)return this.render(t,e,r);if(n.render(0=this.totalDuration()||!v&&_)&&(f!==this._start&&Math.abs(l)===Math.abs(this._ts)||this._lock||(!t&&g||!(v===m&&0=i&&(a instanceof Gt?e&&n.push(a):(r&&n.push(a),t&&n.push.apply(n,a.getChildren(!0,e,r)))),a=a._next;return n},e.getById=function getById(t){for(var e=this.getChildren(1,1,1),r=e.length;r--;)if(e[r].vars.id===t)return e[r]},e.remove=function remove(t){return o(t)?this.removeLabel(t):p(t)?this.killTweensOf(t):(ra(this,t),t===this._recent&&(this._recent=this._last),ta(this))},e.totalTime=function totalTime(t,e){return arguments.length?(this._forcing=1,!this._dp&&this._ts&&(this._start=ca(Pt.time-(0e:!e||a.isActive())&&i.push(a):(r=a.getTweensOf(n,e)).length&&i.push.apply(i,r),a=a._next;return i},e.tweenTo=function tweenTo(t,e){e=e||{};var r=this,i=La(r,t),n=e.startAt,a=e.onStart,s=e.onStartParams,o=e.immediateRender,u=Gt.to(r,ja({ease:"none",lazy:!1,immediateRender:!1,time:i,overwrite:"auto",duration:e.duration||Math.abs((i-(n&&"time"in n?n.time:r._time))/r.timeScale())||j,onStart:function onStart(){r.pause();var t=e.duration||Math.abs((i-r._time)/r.timeScale());u._dur!==t&&Ia(u,t,0,1).render(u._time,!0,!0),a&&a.apply(u,s||[])}},e));return o?u.render(0):u},e.tweenFromTo=function tweenFromTo(t,e,r){return this.tweenTo(e,ja({startAt:{time:La(this,t)}},r))},e.recent=function recent(){return this._recent},e.nextLabel=function nextLabel(t){return void 0===t&&(t=this._time),gb(this,La(this,t))},e.previousLabel=function previousLabel(t){return void 0===t&&(t=this._time),gb(this,La(this,t),1)},e.currentLabel=function currentLabel(t){return arguments.length?this.seek(t,!0):this.previousLabel(this._time+j)},e.shiftChildren=function shiftChildren(t,e,r){void 0===r&&(r=0);for(var i,n=this._first,a=this.labels;n;)n._start>=r&&(n._start+=t,n._end+=t),n=n._next;if(e)for(i in a)a[i]>=r&&(a[i]+=t);return ta(this)},e.invalidate=function invalidate(){var t=this._first;for(this._lock=0;t;)t.invalidate(),t=t._next;return n.prototype.invalidate.call(this)},e.clear=function clear(t){void 0===t&&(t=!0);for(var e,r=this._first;r;)e=r._next,this.remove(r),r=e;return this._dp&&(this._time=this._tTime=this._pTime=0),t&&(this.labels={}),ta(this)},e.totalDuration=function totalDuration(t){var e,r,i,n=0,a=this,s=a._last,o=U;if(arguments.length)return a.timeScale((a._repeat<0?a.duration():a.totalDuration())/(a.reversed()?-t:t));if(a._dirty){for(i=a.parent;s;)e=s._prev,s._dirty&&s.totalDuration(),o<(r=s._start)&&a._sort&&s._ts&&!a._lock?(a._lock=1,Ca(a,s,r-s._delay,1)._lock=0):o=r,r<0&&s._ts&&(n-=r,(!i&&!a._dp||i&&i.smoothChildTiming)&&(a._start+=r/a._ts,a._time-=r,a._tTime-=r),a.shiftChildren(-r,!1,-Infinity),o=0),s._end>n&&s._ts&&(n=s._end),s=e;Ia(a,a===F&&a._time>n?a._time:n,1,1),a._dirty=0}return a._tDur},Timeline.updateRoot=function updateRoot(t){if(F._ts&&(ga(F,ya(t,F)),f=Pt.frame),Pt.frame>=ct){ct+=Y.autoSleep||120;var e=F._first;if((!e||!e._ts)&&Y.autoSleep&&Pt._listeners.length<2){for(;e&&!e._ts;)e=e._next;e||Pt.sleep()}}},Timeline}(Ft);ja(Bt.prototype,{_lock:0,_hasPause:0,_forcing:0});function Qb(t,e,r,i,n,a){var u,h,l,f;if(ft[t]&&!1!==(u=new ft[t]).init(n,u.rawVars?e[t]:function _processVars(t,e,r,i,n){if(p(t)&&(t=Ut(t,n,e,r,i)),!s(t)||t.style&&t.nodeType||K(t)||Z(t))return o(t)?Ut(t,n,e,r,i):t;var a,u={};for(a in t)u[a]=Ut(t[a],n,e,r,i);return u}(e[t],i,n,a,r),r,i,a)&&(r._pt=h=new ie(r._pt,n,t,0,1,u.render,u,0,u.priority),r!==d))for(l=r._ptLookup[r._targets.indexOf(n)],f=u._props.length;f--;)l[u._props[f]]=h;return u}var qt,Yt=function _addPropTween(t,e,r,i,n,a,s,u,h){p(i)&&(i=i(n||0,t,a));var l,f=t[e],d="get"!==r?r:p(f)?h?t[e.indexOf("set")||!p(t["get"+e.substr(3)])?e:"get"+e.substr(3)](h):t[e]():f,c=p(f)?h?Jt:Qt:Vt;if(o(i)&&(~i.indexOf("random(")&&(i=db(i)),"="===i.charAt(1)&&(i=parseFloat(d)+parseFloat(i.substr(2))*("-"===i.charAt(0)?-1:1)+(Oa(d)||0))),d!==i)return isNaN(d*i)?(f||e in t||N(e,i),function _addComplexStringPropTween(t,e,r,i,n,a,s){var o,u,h,l,f,d,c,p,_=new ie(this._pt,t,e,0,1,Zt,null,n),m=0,g=0;for(_.b=r,_.e=i,r+="",(c=~(i+="").indexOf("random("))&&(i=db(i)),a&&(a(p=[r,i],t,e),r=p[0],i=p[1]),u=r.match(it)||[];o=it.exec(i);)l=o[0],f=i.substring(m,o.index),h?h=(h+1)%5:"rgba("===f.substr(-5)&&(h=1),l!==u[g++]&&(d=parseFloat(u[g-1])||0,_._pt={_next:_._pt,p:f||1===g?f:",",s:d,c:"="===l.charAt(1)?parseFloat(l.substr(2))*("-"===l.charAt(0)?-1:1):parseFloat(l)-d,m:h&&h<4?Math.round:0},m=it.lastIndex);return _.c=m")});else{if(l=P.length,c=b?Va(b):Q,s(b))for(f in b)~jt.indexOf(f)&&((p=p||{})[f]=b[f]);for(u=0;u=t._tDur||e<0)&&t.ratio===u&&(u&&sa(t,1),r||(xt(t,u?"onComplete":"onReverseComplete",!0),t._prom&&t._prom()))}else t._zTime||(t._zTime=e)}(this,t,e,r);return this},e.targets=function targets(){return this._targets},e.invalidate=function invalidate(){return this._pt=this._op=this._startAt=this._onUpdate=this._lazy=this.ratio=0,this._ptLookup=[],this.timeline&&this.timeline.invalidate(),A.prototype.invalidate.call(this)},e.kill=function kill(t,e){if(void 0===e&&(e="all"),!(t||e&&"all"!==e))return this._lazy=this._pt=0,this.parent?ib(this):this;if(this.timeline){var r=this.timeline.totalDuration();return this.timeline.killTweensOf(t,e,qt&&!0!==qt.vars.overwrite)._first||ib(this),this.parent&&r!==this.timeline.totalDuration()&&Ia(this,this._dur*this.timeline._tDur/r,0,1),this}var i,n,a,s,u,h,l,f=this._targets,d=t?Tt(t):f,c=this._ptLookup,p=this._pt;if((!e||"all"===e)&&function _arraysMatch(t,e){for(var r=t.length,i=r===e.length;i&&r--&&t[r]===e[r];);return r<0}(f,d))return"all"===e&&(this._pt=0),ib(this);for(i=this._op=this._op||[],"all"!==e&&(o(e)&&(u={},ba(e,function(t){return u[t]=1}),e=u),e=function _addAliasesToVars(t,e){var r,i,n,a,s=t[0]?_(t[0]).harness:0,o=s&&s.aliases;if(!o)return e;for(i in r=mt({},e),o)if(i in r)for(n=(a=o[i].split(",")).length;n--;)r[a[n]]=r[i];return r}(f,e)),l=f.length;l--;)if(~d.indexOf(f[l]))for(u in n=c[l],"all"===e?(i[l]=e,s=n,a={}):(a=i[l]=i[l]||{},s=e),s)(h=n&&n[u])&&("kill"in h.d&&!0!==h.d.kill(u)||ra(this,h,"_pt"),delete n[u]),"all"!==a&&(a[u]=1);return this._initted&&!this._pt&&p&&ib(this),this},Tween.to=function to(t,e,r){return new Tween(t,e,r)},Tween.from=function from(t,e){return new Tween(t,ea(arguments,1))},Tween.delayedCall=function delayedCall(t,e,r,i){return new Tween(e,0,{immediateRender:!1,lazy:!1,overwrite:!1,delay:t,onComplete:e,onReverseComplete:e,onCompleteParams:r,onReverseCompleteParams:r,callbackScope:i})},Tween.fromTo=function fromTo(t,e,r){return new Tween(t,ea(arguments,2))},Tween.set=function set(t,e){return e.duration=0,e.repeatDelay||(e.repeat=0),new Tween(t,e)},Tween.killTweensOf=function killTweensOf(t,e,r){return F.killTweensOf(t,e,r)},Tween}(Ft);ja(Gt.prototype,{_targets:[],_lazy:0,_startAt:0,_op:0,_onInit:0}),ba("staggerTo,staggerFrom,staggerFromTo",function(r){Gt[r]=function(){var t=new Bt,e=bt.call(arguments,0);return e.splice("staggerFromTo"===r?5:4,0,0),t[r].apply(t,e)}});function _b(t,e,r){return t.setAttribute(e,r)}function hc(t,e,r,i){i.mSet(t,e,i.m.call(i.tween,r,i.mt),i)}var Vt=function _setterPlain(t,e,r){return t[e]=r},Qt=function _setterFunc(t,e,r){return t[e](r)},Jt=function _setterFuncWithParam(t,e,r,i){return t[e](i.fp,r)},Wt=function _getSetter(t,e){return p(t[e])?Qt:r(t[e])&&t.setAttribute?_b:Vt},Ht=function _renderPlain(t,e){return e.set(e.t,e.p,Math.round(1e4*(e.s+e.c*t))/1e4,e)},$t=function _renderBoolean(t,e){return e.set(e.t,e.p,!!(e.s+e.c*t),e)},Zt=function _renderComplexString(t,e){var r=e._pt,i="";if(!t&&e.b)i=e.b;else if(1===t&&e.e)i=e.e;else{for(;r;)i=r.p+(r.m?r.m(r.s+r.c*t):Math.round(1e4*(r.s+r.c*t))/1e4)+i,r=r._next;i+=e.c}e.set(e.t,e.p,i,e)},Kt=function _renderPropTweens(t,e){for(var r=e._pt;r;)r.r(t,r.d),r=r._next},te=function _addPluginModifier(t,e,r,i){for(var n,a=this._pt;a;)n=a._next,a.p===i&&a.modifier(t,e,r),a=n},ee=function _killPropTweensOf(t){for(var e,r,i=this._pt;i;)r=i._next,i.p===t&&!i.op||i.op===t?ra(this,i,"_pt"):i.dep||(e=1),i=r;return!e},re=function _sortPropTweensByPriority(t){for(var e,r,i,n,a=t._pt;a;){for(e=a._next,r=i;r&&r.pr>a.pr;)r=r._next;(a._prev=r?r._prev:n)?a._prev._next=a:i=a,(a._next=r)?r._prev=a:n=a,a=e}t._pt=i},ie=(PropTween.prototype.modifier=function modifier(t,e,r){this.mSet=this.mSet||this.set,this.set=hc,this.m=t,this.mt=r,this.tween=e},PropTween);function PropTween(t,e,r,i,n,a,s,o,u){this.t=e,this.s=i,this.c=n,this.p=r,this.r=a||Ht,this.d=s||this,this.set=o||Vt,this.pr=u||0,(this._next=t)&&(t._prev=this)}ba(_t+"parent,duration,ease,delay,overwrite,runBackwards,startAt,yoyo,immediateRender,repeat,repeatDelay,data,paused,reversed,lazy,callbackScope,stringFilter,id,yoyoEase,stagger,inherit,repeatRefresh,keyframes,autoRevert,scrollTrigger",function(t){return ut[t]=1}),ot.TweenMax=ot.TweenLite=Gt,ot.TimelineLite=ot.TimelineMax=Bt,F=new Bt({sortChildren:!1,defaults:B,autoRemoveChildren:!0,id:"root",smoothChildTiming:!0}),Y.stringFilter=tb;var ne={registerPlugin:function registerPlugin(){for(var t=arguments.length,e=new Array(t),r=0;r", 27 | "license": "MIT", 28 | "dependencies": {}, 29 | "devDependencies": {} 30 | } 31 | -------------------------------------------------------------------------------- /ui-level.html: -------------------------------------------------------------------------------- 1 | 24 | 233 | 387 | -------------------------------------------------------------------------------- /ui-level.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable eqeqeq */ 2 | /* eslint-disable no-tabs */ 3 | /* 4 | MIT License 5 | 6 | Copyright (c) 2019 hotNipi 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | module.exports = function (RED) { 27 | var path = require('path') 28 | 29 | function HTML(config) { 30 | var configAsJson = JSON.stringify(config) 31 | var styles = String.raw` 32 | ` 48 | var ipgradient = String.raw` 49 | 50 | 51 | 52 | 53 | ` 54 | var verticalipgradient = String.raw` 55 | 56 | 57 | 58 | 59 | ` 60 | var regulargradient = String.raw` 61 | 62 | 63 | 64 | 65 | 66 | ` 67 | var verticalgradient = String.raw` 68 | 69 | 70 | 71 | 72 | 73 | ` 74 | var gradienttype 75 | if (config.colorschema == 'rainbow') { 76 | gradienttype = config.layout == 'sv' ? verticalipgradient : ipgradient 77 | } else { 78 | if (config.colorschema == 'fixed') { 79 | gradienttype = config.layout == 'sv' ? verticalgradient : regulargradient 80 | } else { 81 | gradienttype = '' 82 | } 83 | } 84 | var filltype = config.colorschema == 'valuedriven' ? String.raw`fill="` + config.colorOff + '"' : String.raw`fill="url(#level_gradi_{{unique}})"` 85 | var level_single_h = String.raw` 86 | 87 | 88 | ${gradienttype} 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 105 | 106 | 112 | 113 | 119 | ` + config.label + ` 121 | {{msg.payload[0]}} 122 | 123 | 125 | ` + config.unit + ` 126 | 127 | 128 | ` + config.min + ` 130 | 134 | ` + config.max + ` 135 | ` 136 | var level_single_v = String.raw` 137 | 138 | 139 | ${gradienttype} 140 | 141 | 142 | 143 | 144 | 147 | 148 | 149 | 152 | 153 | 154 | 161 | 163 | 169 | 170 | 176 | 177 | 178 | ` + config.label + ` 179 | 180 | 182 | {{msg.payload[0]}} 183 | 184 | 186 | ` + config.unit + ` 187 | 188 | 189 | ` + config.label + ` 192 | {{msg.payload[0]}} 193 | 194 | 196 | ` + config.unit + ` 197 | 198 | 199 | ` + config.max + ` 200 | 204 | ' + config.min + ` 205 | ` 206 | var level_pair_h = String.raw` 207 | 208 | 209 | ${gradienttype} 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 232 | 233 | 239 | 240 | 246 | 252 | 253 | 259 | 260 | 266 | ` + config.channelA + ` 267 | 269 | ` + config.unit + ` 270 | 271 | 272 | 275 | {{msg.payload[0]}} 276 | 277 | 278 | ' + config.channelB + ` 279 | 281 | ` + config.unit + ` 282 | 283 | 284 | 287 | {{msg.payload[1]}} 288 | 289 | 290 | ` + config.min + ` 291 | 295 | ' + config.max + ` 296 | 297 | ` 298 | var layout 299 | if (config.layout === 'sh') { 300 | layout = level_single_h 301 | } 302 | if (config.layout === 'sv') { 303 | layout = level_single_v 304 | } 305 | if (config.layout === 'ph') { 306 | layout = level_pair_h 307 | } 308 | // var scripts = String.raw`` 309 | var html = String.raw` 310 | ${styles} 311 | ${layout}` 312 | return html 313 | } 314 | 315 | function checkConfig(node, conf) { 316 | if (!conf || !conf.hasOwnProperty('group')) { 317 | node.error(RED._('ui_level.error.no-group')) 318 | return false 319 | } 320 | return true 321 | } 322 | var ui = undefined 323 | 324 | function LevelNode(config) { 325 | try { 326 | var node = this 327 | if (ui === undefined) { 328 | ui = RED.require('node-red-dashboard')(RED) 329 | } 330 | RED.nodes.createNode(this, config) 331 | var done = null 332 | var range = null 333 | var stripecount = null 334 | var site = null 335 | var dimensions = null 336 | var updateControl = null 337 | var exactPosition = null 338 | var ensureNumber = null 339 | var getSiteProperties = null 340 | var msgFormat = null 341 | var difference = null 342 | var interTicks = null 343 | var evenly = null 344 | var validateEdges = null 345 | var initSectorValues = null 346 | var showStatus = null 347 | if (checkConfig(node, config)) { 348 | ensureNumber = function (input, dets) { 349 | if (input === undefined) { 350 | return config.min 351 | } 352 | if (typeof input !== 'number') { 353 | var inputString = input.toString() 354 | input = dets !== 0 ? parseFloat(inputString) : parseInt(inputString) 355 | if (isNaN(input)) { 356 | node.warn('msg.payload does not contain numeric value') 357 | return config.min 358 | } 359 | } 360 | if (dets > 0) { 361 | input = parseFloat(input.toFixed(dets)) 362 | } else { 363 | input = parseInt(input) 364 | } 365 | if (isNaN(input)) { 366 | node.warn('msg.payload does not contain numeric value') 367 | input = config.min 368 | } 369 | return input 370 | } 371 | getSiteProperties = function () { 372 | var opts = {} 373 | opts.sizes = { 374 | sx: 48, 375 | sy: 48, 376 | gx: 4, 377 | gy: 4, 378 | cx: 4, 379 | cy: 4, 380 | px: 4, 381 | py: 4 382 | } 383 | opts.theme = { 384 | 'widget-textColor': { 385 | value: '#eeeeee' 386 | } 387 | } 388 | if (typeof ui.getSizes === 'function') { 389 | if (ui.getSizes()) { 390 | opts.sizes = ui.getSizes() 391 | } 392 | if (ui.getTheme()) { 393 | opts.theme = ui.getTheme() 394 | } 395 | } 396 | return opts 397 | } 398 | range = function (n, p, r) { 399 | var divisor = p.maxin - p.minin 400 | n = n > p.maxin ? p.maxin - 0.00001 : n 401 | n = n < p.minin ? p.minin : n 402 | n = ((((n - p.minin) % divisor) + divisor) % divisor) + p.minin 403 | n = ((n - p.minin) / (p.maxin - p.minin)) * (p.maxout - p.minout) + p.minout 404 | if (!r) { 405 | return Math.round(n) 406 | } 407 | return n 408 | } 409 | showStatus = function (v) { 410 | var status = config.label+': '+v[0]+config.unit 411 | 412 | if (config.layout == 'ph') { 413 | status = config.channelA+': '+v[0]+' | '+config.channelB+': '+v[1] 414 | } 415 | node.status({ 416 | fill: 'green', 417 | shape: 'dot', 418 | text: status 419 | }); 420 | } 421 | stripecount = function () { 422 | var w = config.layout.indexOf('v') != -1 ? config.exactheight : config.exactwidth 423 | var cw = 0 424 | var c = 0 425 | while (cw <= w) { 426 | cw += config.stripe.width 427 | c += 0.5 428 | } 429 | c = Math.round(c) 430 | while (c * config.stripe.step > w) { 431 | c-- 432 | } 433 | if (c & (1 !== 1)) { 434 | c-- 435 | } 436 | return c 437 | } 438 | dimensions = function (direction) { 439 | var ret = 0 440 | switch (direction) { 441 | case 'w': 442 | { 443 | if (config.layout == 'sh') { 444 | ret = 6 445 | } 446 | if (config.layout == 'ph') { 447 | ret = 6 448 | } 449 | if (config.layout == 'sv') { 450 | ret = 3 451 | } 452 | break 453 | } 454 | case 'h': 455 | { 456 | if (config.layout == 'sh') { 457 | ret = 1 458 | } 459 | if (config.layout == 'ph') { 460 | ret = 2 461 | } 462 | if (config.layout == 'sv') { 463 | ret = 4 464 | } 465 | break 466 | } 467 | } 468 | return ret 469 | } 470 | updateControl = function (uicontrol) { 471 | var applies = false 472 | var updatesectors = false 473 | var updatetics = config.tickmode != 'off' 474 | sectorupdate = [] 475 | tickupdate = [] 476 | var mi = config.min 477 | var ma = config.max 478 | var input 479 | if (uicontrol.label != undefined) { 480 | config.label = uicontrol.label 481 | applies = true 482 | } 483 | if (uicontrol.min != undefined) { 484 | input = parseFloat(uicontrol.min) 485 | if (!isNaN(input)) { 486 | mi = input 487 | applies = true 488 | } 489 | } 490 | if (uicontrol.max != undefined) { 491 | input = parseFloat(uicontrol.max) 492 | if (!isNaN(input)) { 493 | ma = input 494 | applies = true 495 | } 496 | } 497 | if (uicontrol.seg1 != undefined) { 498 | input = parseFloat(uicontrol.seg1) 499 | if (!isNaN(input)) { 500 | config.sectorwarn = input 501 | applies = true 502 | updatesectors = true 503 | } 504 | } 505 | if (uicontrol.seg2 != undefined) { 506 | input = parseFloat(uicontrol.seg2) 507 | if (!isNaN(input)) { 508 | config.sectorhigh = input 509 | applies = true 510 | updatesectors = true 511 | } 512 | } 513 | config.min = mi 514 | config.max = ma 515 | config.reverse = mi > ma 516 | if (applies) { 517 | if (updatesectors) { 518 | if (config.colorschema == 'fixed' || config.colorschema == 'valuedriven') { 519 | var high = (config.gradient.high = exactPosition(config.sectorhigh, config.min, config.max, config.reverse, config.lastpos).p) 520 | var warn = (config.gradient.warn = exactPosition(config.sectorwarn, config.min, config.max, config.reverse, config.lastpos).p) 521 | if (config.layout == 'sv') { 522 | warn = 100 - warn 523 | high = 100 - high 524 | sectorupdate = [high, high, warn, warn] 525 | } else { 526 | sectorupdate = [warn, warn, high, high] 527 | } 528 | } 529 | 530 | } 531 | if (updatetics) { 532 | tickupdate = interTicks() 533 | } 534 | configsent = false 535 | } 536 | } 537 | difference = function (a, b) { 538 | return Math.abs(a - b) 539 | } 540 | evenly = function (len, fixed) { 541 | var a = Math.abs(config.max - config.min) / (len + 1) 542 | var b = config.reverse ? config.max : config.min 543 | var ret = [] 544 | var check = [] 545 | var legal = true 546 | var j 547 | for (j = 0; j < len; j++) { 548 | b = b + a 549 | ret.push(parseFloat(b.toFixed(fixed))) 550 | } 551 | for (j = 1; j < ret.length; j++) { 552 | check.push(difference(ret[j], ret[j - 1])) 553 | } 554 | for (j = 1; j < check.length; j++) { 555 | if (difference(check[j], check[j - 1]) > 0.2) { 556 | legal = false 557 | break 558 | } 559 | } 560 | if (legal == false) { 561 | return evenly(len, fixed + 1) 562 | } 563 | return ret 564 | } 565 | validateEdges = function (arr, vert) { 566 | if (arr.length == 0) { 567 | return arr 568 | } 569 | var mi = config.min.toString().length * config.stripe.step 570 | var ma = config.lastpos - config.max.toString().length * config.stripe.step 571 | if (vert) { 572 | mi = config.lastpos - 10 573 | ma = 10 574 | if (mi < parseFloat(arr[0].pos) || difference(mi, parseFloat(arr[0].pos)) < 10) { 575 | arr[0].val = '' 576 | } 577 | if (ma > parseFloat(arr[arr.length - 1].pos) || difference(ma, parseFloat(arr[arr.length - 1].pos)) < 10) { 578 | arr[arr.length - 1].val = '' 579 | } 580 | } else { 581 | if (mi > parseFloat(arr[0].pos) || difference(mi, parseFloat(arr[0].pos)) < 10) { 582 | arr[0].val = '' 583 | } 584 | if (ma < parseFloat(arr[arr.length - 1].pos) || difference(ma, parseFloat(arr[arr.length - 1].pos)) < 10) { 585 | arr[arr.length - 1].val = '' 586 | } 587 | } 588 | return arr 589 | } 590 | interTicks = function () { 591 | var ret = [] 592 | if (config.tickmode == 'off') { 593 | return ret 594 | } 595 | var vert = config.layout.indexOf('v') != -1 596 | var count = vert ? config.height : config.width - 1 597 | if (count <= 0) { 598 | return ret 599 | } 600 | var fixed = decimals.fixed > 0 ? 1 : 0 601 | if (Math.abs(config.max - config.min) > 10) { 602 | fixed = 0 603 | } 604 | var pos 605 | var calc 606 | if (config.tickmode == 'segments') { 607 | var w = parseFloat(config.sectorwarn.toFixed(1)) 608 | var h = parseFloat(config.sectorhigh.toFixed(1)) 609 | if (count == 1) { 610 | calc = [w] 611 | } else { 612 | calc = [w, h] 613 | } 614 | } else { 615 | calc = evenly(count, fixed) 616 | } 617 | for (var i = 0; i < calc.length; i++) { 618 | if (vert) { 619 | pos = exactPosition(calc[i], config.max, config.min, !config.reverse, config.lastpos + config.stripe.width, true) 620 | } else { 621 | pos = exactPosition(calc[i], config.min, config.max, config.reverse, config.lastpos, true) 622 | } 623 | ret.push({ 624 | val: calc[i], 625 | pos: pos.px + 'px' 626 | }) 627 | } 628 | ret = validateEdges(ret, vert) 629 | return ret 630 | } 631 | exactPosition = function (target, mi, ma, r, dir, noround) { 632 | var min = r ? ma : mi 633 | var max = r ? mi : ma 634 | var p = r ? { 635 | minin: min, 636 | maxin: max + 0.00001, 637 | minout: 100, 638 | maxout: 1 639 | } : { 640 | minin: min, 641 | maxin: max + 0.00001, 642 | minout: 1, 643 | maxout: 100 644 | } 645 | var c 646 | if (noround == true) { 647 | c = ((dir * range(target, p, true)) / 100 / config.stripe.width) * config.stripe.width 648 | } else { 649 | c = Math.round((dir * range(target, p)) / 100 / config.stripe.width) * config.stripe.width 650 | } 651 | var ret = c / dir 652 | var ep 653 | if (ret > 1) { 654 | ret = 1 655 | } 656 | if (ret < 0) { 657 | ret = 0 658 | } 659 | if (noround == true) { 660 | ep = ((ret * dir) / config.stripe.width) * config.stripe.width 661 | } else { 662 | ep = Math.round((ret * dir) / config.stripe.width) * config.stripe.width 663 | } 664 | return { 665 | px: ep, 666 | p: ret * 100 667 | } 668 | } 669 | initSectorValues = function () { 670 | var max = config.reverse ? config.min : config.max 671 | var def = config.reverse ? { 672 | sh: 0.7, 673 | sw: 0.85 674 | } : { 675 | sh: 0.85, 676 | sw: 0.7 677 | } 678 | config.sectorhigh = isNaN(parseFloat(config.segHigh)) ? parseFloat((max * def.sh).toFixed(decimals.fixed)) : parseFloat(config.segHigh) 679 | config.sectorwarn = isNaN(parseFloat(config.segWarn)) ? parseFloat((max * def.sw).toFixed(decimals.fixed)) : parseFloat(config.segWarn) 680 | } 681 | msgFormat = function (m, value) { 682 | if (value === undefined) { 683 | value = config.min 684 | } 685 | if (Array.isArray(value) === true) { 686 | if (value.length < 2) { 687 | value.push(null) 688 | } 689 | var v 690 | m.payload = [] 691 | for (var i = 0; i < 2; i++) { 692 | v = value[i] 693 | if (v !== null) { 694 | v = ensureNumber(v, decimals.fixed) 695 | } 696 | m.payload.push(v) 697 | } 698 | } else { 699 | value = ensureNumber(value, decimals.fixed) 700 | m.payload = [value, null] 701 | } 702 | return m 703 | } 704 | var group = RED.nodes.getNode(config.group) 705 | var site = getSiteProperties() 706 | if (config.width == 0) { 707 | config.width = parseInt(group.config.width) || dimensions('w') 708 | } 709 | if (config.height == 0) { 710 | config.height = parseInt(group.config.height) || dimensions('h') 711 | } 712 | config.width = parseInt(config.width) 713 | config.height = parseInt(config.height) 714 | config.exactwidth = parseInt(site.sizes.sx * config.width + site.sizes.cx * (config.width - 1)) - 12 715 | config.exactheight = parseInt(site.sizes.sy * config.height + site.sizes.cy * (config.height - 1)) - 12 716 | var strh = Math.floor(config.exactheight / 3) 717 | if (strh > 12) { 718 | strh = 12 719 | } 720 | if (strh < 4) { 721 | strh = 4 722 | } 723 | var y_0 = config.exactheight - strh + (12 - strh) 724 | var y_1 = 0 725 | if (config.layout === 'ph') { 726 | y_0 = config.exactheight / 3 - 6 727 | y_1 = -1 + (config.exactheight / 3) * 2 728 | } 729 | config.stripe = { 730 | step: parseInt(config.shape) * 2, 731 | width: parseInt(config.shape), 732 | height: strh, 733 | y0: y_0, 734 | y1: y_1 735 | } 736 | config.reverse = parseFloat(config.min) > parseFloat(config.max) 737 | config.min = parseFloat(config.min) 738 | config.max = parseFloat(config.max) 739 | config.peaktime = config.peaktime == 'infinity' ? -1 : isNaN(parseInt(config.peaktime)) ? 3000 : parseInt(config.peaktime) 740 | config.count = stripecount() 741 | config.hideValue = config.hideValue || false 742 | config.lastpos = config.count * config.stripe.step - config.stripe.width 743 | config.colorOff = config.colorOff || 'gray' 744 | config.colorNormal = config.colorNormal || 'green' 745 | config.colorWarn = config.colorWarn || 'orange' 746 | config.colorHi = config.colorHi || 'red' 747 | config.colorschema = config.colorschema || 'fixed' 748 | var opc = [ 749 | config.colorOff, 750 | config.colorNormal, 751 | config.colorWarn, 752 | config.colorHi 753 | ] 754 | var decimals = (config.decimals = isNaN(parseFloat(config.decimals)) ? { 755 | fixed: 1, 756 | mult: 0 757 | } : { 758 | fixed: parseInt(config.decimals), 759 | mult: Math.pow(10, parseInt(config.decimals)) 760 | }) 761 | initSectorValues() 762 | config.gradient = { 763 | warn: exactPosition(config.sectorwarn, config.min, config.max, config.reverse, config.lastpos).p, 764 | high: exactPosition(config.sectorhigh, config.min, config.max, config.reverse, config.lastpos).p 765 | } 766 | config.interticks = interTicks() 767 | var defaultFontOptions = { 768 | sh: { 769 | normal: 1, 770 | small: 0.65, 771 | big: 1.03, 772 | color: 'currentColor' 773 | }, 774 | sv: { 775 | normal: 1, 776 | small: 0.65, 777 | big: 2.5, 778 | color: 'currentColor' 779 | }, 780 | ph: { 781 | normal: 1, 782 | small: 0.65, 783 | big: 1.2, 784 | color: 'currentColor' 785 | } 786 | } 787 | config.fontoptions = defaultFontOptions[config.layout] 788 | if (config.textoptions !== 'default') { 789 | var opt = parseFloat(config.fontLabel) 790 | if (!isNaN(opt)) { 791 | config.fontoptions.normal = opt 792 | } 793 | opt = parseFloat(config.fontValue) 794 | if (!isNaN(opt)) { 795 | config.fontoptions.big = opt 796 | } 797 | opt = parseFloat(config.fontSmall) 798 | if (!isNaN(opt)) { 799 | config.fontoptions.small = opt 800 | } 801 | if (config.colorFromTheme == false) { 802 | opt = config.colorText 803 | if (opt != '') { 804 | config.fontoptions.color = opt 805 | } 806 | } 807 | } 808 | config.textpos = config.stripe.y0 - 2 809 | if (config.layout == 'sh' && config.tickmode != 'off') { 810 | config.textpos -= 16 * config.fontoptions.small 811 | } 812 | config.padding = { 813 | hor: '6px', 814 | vert: site.sizes.sy / 16 + 'px' 815 | } 816 | var tickupdate = [] 817 | var sectorupdate = [] 818 | config.property = config.property || 'payload' 819 | var configsent = false 820 | var html = HTML(config) 821 | done = ui.addWidget({ 822 | node: node, 823 | order: config.order, 824 | group: config.group, 825 | width: config.width, 826 | height: config.height, 827 | format: html, 828 | templateScope: 'local', 829 | emitOnlyNewValues: false, 830 | forwardInputMessages: true, 831 | storeFrontEndInputAsState: true, 832 | beforeEmit: function (msg) { 833 | if (msg.control) { 834 | updateControl(msg.control) 835 | } 836 | var fem = {} 837 | if (msg.peakreset) { 838 | fem.peakreset = msg.peakreset 839 | } 840 | if (!configsent) { 841 | fem.config = {} 842 | fem.config.min = config.min 843 | fem.config.max = config.max 844 | if (sectorupdate.length > 0) { 845 | fem.config.sectors = sectorupdate 846 | } 847 | if (tickupdate.length > 0) { 848 | fem.config.ticks = tickupdate 849 | } 850 | fem.config.label = config.label 851 | configsent = true 852 | } 853 | var val = RED.util.getMessageProperty(msg, config.property) 854 | if (val === undefined || val === null) { 855 | return { 856 | msg: fem 857 | } 858 | } 859 | fem = msgFormat(fem, val) 860 | if (config.layout === 'ph') { 861 | if (fem.payload[1] === null) { 862 | fem.payload[1] = config.min 863 | } 864 | } 865 | showStatus(fem.payload) 866 | var pos = [ 867 | exactPosition(fem.payload[0], config.min, config.max, config.reverse, config.lastpos, false), 868 | null 869 | ] 870 | if (config.layout === 'ph') { 871 | pos[1] = exactPosition(fem.payload[1], config.min, config.max, config.reverse, config.lastpos, false) 872 | } 873 | fem.position = [pos[0].px, null] 874 | if (config.layout === 'ph') { 875 | fem.position[1] = pos[1].px 876 | } 877 | if (config.colorschema == 'valuedriven' || config.peakmode == true) { 878 | var peakpos 879 | var col 880 | col = pos[0].p <= config.gradient.warn ? opc[1] : pos[0].p <= config.gradient.high ? opc[2] : opc[3] 881 | if (config.colorschema == 'valuedriven') { 882 | col = fem.payload[0] <= config.sectorwarn ? opc[1] : fem.payload[0] <= config.sectorhigh ? opc[2] : opc[3] 883 | fem.color = [col, null] 884 | } 885 | if (config.peakmode == true) { 886 | peakpos = Math.floor( 887 | (pos[0].px - config.stripe.width) / config.stripe.step) * config.stripe.step 888 | fem.peak = [{ 889 | px: peakpos, 890 | c: col 891 | }, null] 892 | } 893 | if (pos[1] != null) { 894 | col = pos[1].p <= config.gradient.warn ? opc[1] : pos[1].p <= config.gradient.high ? opc[2] : opc[3] 895 | if (config.colorschema == 'valuedriven') { 896 | col = fem.payload[1] <= config.sectorwarn ? opc[1] : fem.payload[1] <= config.sectorhigh ? opc[2] : opc[3] 897 | fem.color[1] = col 898 | } 899 | if (config.peakmode == true) { 900 | peakpos = Math.floor( 901 | (pos[1].px - config.stripe.width) / config.stripe.step) * config.stripe.step 902 | fem.peak[1] = { 903 | px: peakpos, 904 | c: col 905 | } 906 | } 907 | } 908 | } 909 | return { 910 | msg: fem 911 | } 912 | }, 913 | initController: function ($scope) { 914 | $scope.unique = $scope.$eval('$id') 915 | $scope.peaklock = [false, false] 916 | $scope.hold = [null, null] 917 | $scope.peaktoreset = [null, null] 918 | $scope.tickmode = false 919 | $scope.interticks = null 920 | $scope.padding = null 921 | $scope.waitingmessage = null 922 | $scope.inited = false 923 | $scope.timeout = null 924 | $scope.retried = 0 925 | $scope.init = function (config) { 926 | if (!document.getElementById('greensock-gsap-3')) { 927 | loadScript('greensock-gsap-3', 'ui-level/js/gsap.min.js') 928 | } 929 | $scope.padding = config.padding 930 | $scope.lastpeak = [{ 931 | px: 0, 932 | c: config.colorNormal 933 | }, { 934 | px: 0, 935 | c: config.colorNormal 936 | }] 937 | $scope.d = config.decimals 938 | $scope.prop = config.layout === 'sv' ? { 939 | dir: 'height', 940 | pos: 'y' 941 | } : { 942 | dir: 'width', 943 | pos: 'x' 944 | } 945 | $scope.len = config.layout === 'ph' ? 2 : 1 946 | $scope.animate = { 947 | g: config.animations, 948 | t: config.textAnimations, 949 | peak: config.peaktime 950 | } 951 | $scope.speed = $scope.animate.g == 'rocket' ? { 952 | ms: 100, 953 | s: 0.1 954 | } : $scope.animate.g == 'reactive' ? { 955 | ms: 300, 956 | s: 0.3 957 | } : { 958 | ms: 800, 959 | s: 0.8 960 | } 961 | if ($scope.animate.g == 'off') { 962 | $scope.speed = { 963 | ms: 20, 964 | s: 0 965 | } 966 | } 967 | if (config.tickmode != 'off') { 968 | $scope.interticks = config.interticks 969 | $scope.tickmode = config.tickmode 970 | setTicks() 971 | } 972 | updateContainerStyle() 973 | update(config) 974 | } 975 | var loadScript = function (id, path) { 976 | // console.log('loadscript',path) 977 | var head = document.getElementsByTagName('head')[0] 978 | var script = document.createElement('script') 979 | script.type = 'text/javascript' 980 | script.id = id 981 | script.src = path 982 | head.appendChild(script) 983 | script.onload = function () { 984 | try { 985 | gsap.config({ 986 | nullTargetWarn: false 987 | }) 988 | } catch (error) { 989 | // console.log('gsap configuration not changed') 990 | } 991 | } 992 | } 993 | var setTicks = function () { 994 | if ($scope.tickmode == false) { 995 | return 996 | } 997 | var j 998 | var tick 999 | $("[id*='level_tick_" + $scope.unique + "']").text('') 1000 | for (j = 0; j < $scope.interticks.length; j++) { 1001 | tick = document.getElementById('level_tick_' + $scope.unique + '_' + j) 1002 | if (tick) { 1003 | $(tick).text($scope.interticks[j].val) 1004 | $(tick).attr($scope.prop.pos, $scope.interticks[j].pos) 1005 | } 1006 | } 1007 | } 1008 | var peakpixel 1009 | var resetPeak = function () { 1010 | if ($scope.animate.peak != -1) { 1011 | return 1012 | } 1013 | try { 1014 | var j 1015 | for (j = 0; j < $scope.len; j++) { 1016 | peakpixel = document.getElementById('level_peak_' + j + '_' + $scope.unique) 1017 | if (peakpixel) { 1018 | var pixel = $(peakpixel) 1019 | if ($scope.peaktoreset[j] != null) { 1020 | gsap.to(pixel, { 1021 | [$scope.prop.pos]: $scope.peaktoreset[j].px, 1022 | duration: 0 1023 | }) 1024 | gsap.to(pixel, { 1025 | fill: $scope.peaktoreset[j].c, 1026 | duration: 0 1027 | }) 1028 | $scope.lastpeak[j] = $scope.peaktoreset[j] 1029 | } 1030 | } 1031 | } 1032 | } catch (error) { 1033 | // do nothing 1034 | } 1035 | } 1036 | var animatePeak = function (j, data) { 1037 | if (!data) { 1038 | return 1039 | } 1040 | try { 1041 | peakpixel = document.getElementById('level_peak_' + j + '_' + $scope.unique) 1042 | if (peakpixel) { 1043 | var pixel = $(peakpixel) 1044 | if (data.px > $scope.lastpeak[j].px) { 1045 | if ($scope.hold[j] != null) { 1046 | window.clearInterval($scope.hold[j]) 1047 | $scope.hold[j] = null 1048 | } 1049 | gsap.to(pixel, { 1050 | [$scope.prop.pos]: data.px, 1051 | duration: $scope.speed.s 1052 | }) 1053 | gsap.to(pixel, { 1054 | fill: data.c, 1055 | duration: $scope.speed.s 1056 | }) 1057 | pixel.mask = document.getElementById('url(#level_bgr_' + $scope.unique + '') 1058 | $scope.lastpeak[j] = data 1059 | $scope.peaklock[j] = false 1060 | } else { 1061 | if ($scope.peaktoreset[j] == null || $scope.peaktoreset[j].px > data.px) { 1062 | $scope.peaktoreset[j] = data 1063 | } 1064 | if ($scope.animate.peak == -1) { 1065 | $scope.peaklock[j] = true 1066 | return 1067 | } 1068 | if ($scope.peaklock[j] == false) { 1069 | $scope.peaklock[j] = true 1070 | var cb = function () { 1071 | $scope.peaklock[j] = false 1072 | gsap.to(pixel, { 1073 | [$scope.prop.pos]: $scope.peaktoreset[j].px, 1074 | duration: $scope.speed.s * 2 1075 | }) 1076 | gsap.to(pixel, { 1077 | fill: $scope.peaktoreset[j].c, 1078 | duration: $scope.speed.s * 2 1079 | }) 1080 | $scope.lastpeak[j] = $scope.peaktoreset[j] 1081 | } 1082 | $scope.hold[j] = window.setInterval(function () { 1083 | window.clearInterval($scope.hold[j]) 1084 | $scope.hold[j] = null 1085 | cb() 1086 | }, $scope.animate.peak) 1087 | } else { 1088 | $scope.peaktoreset[j] = data 1089 | } 1090 | } 1091 | } 1092 | } catch (error) { 1093 | // do nothing 1094 | } 1095 | } 1096 | var updateConfig = function (config) { 1097 | var txt = document.getElementById('level_min_' + $scope.unique) 1098 | $(txt).text(config.min) 1099 | txt = document.getElementById('level_max_' + $scope.unique) 1100 | $(txt).text(config.max) 1101 | txt = document.getElementById('level_title_' + $scope.unique) 1102 | $(txt).text(config.label) 1103 | if (config.sectors) { 1104 | var gradient = document.getElementById('level_gradi_' + $scope.unique) 1105 | if (gradient) { 1106 | for (var i = 0; i < config.sectors.length; i++) { 1107 | var stop = gradient.children[i] 1108 | if (stop) { 1109 | $(stop).attr({ 1110 | offset: config.sectors[i] + '%' 1111 | }) 1112 | } 1113 | } 1114 | } 1115 | } 1116 | if (config.ticks) { 1117 | $scope.interticks = config.ticks 1118 | setTicks() 1119 | } 1120 | 1121 | } 1122 | var updateLevel = function (data) { 1123 | var stripe 1124 | var mask 1125 | var j 1126 | var valfield 1127 | for (j = 0; j < $scope.len; j++) { 1128 | mask = document.getElementById('level_mask_' + j + '_' + $scope.unique) 1129 | if (mask) { 1130 | if (parseInt(mask.style[$scope.prop.dir]) != data.position[j]) { 1131 | if (data.peak) { 1132 | animatePeak(j, data.peak[j]) 1133 | } 1134 | var stripeUpdate = function () { 1135 | if (mask.style[$scope.prop.dir]) { 1136 | $(mask).attr( 1137 | [$scope.prop.dir], parseInt(mask.style[$scope.prop.dir])) 1138 | } 1139 | } 1140 | try { 1141 | if ($scope.animate.g !== 'off') { 1142 | gsap.to(mask, { 1143 | [$scope.prop.dir]: data.position[j], 1144 | duration: $scope.speed.s, 1145 | onUpdate: stripeUpdate 1146 | }) 1147 | } else { 1148 | mask.style[$scope.prop.dir] = data.position[j] + 'px' 1149 | } 1150 | } catch (error) { 1151 | //console.log('gsap fails to mask ', $scope.unique, mask, $scope.prop.dir, j, data) 1152 | $scope.waitingmessage = data 1153 | update(null) 1154 | } 1155 | } 1156 | } else { 1157 | // console.log('no mask found ',$scope.unique) 1158 | } 1159 | if (data.color) { 1160 | stripe = document.getElementById('level_stripe_' + j + '_' + $scope.unique) 1161 | if (stripe && data.color[j] != null) { 1162 | try { 1163 | if ($scope.animate.g !== 'off') { 1164 | if (stripe.style.fill != data.color[j]) { 1165 | gsap.to(stripe, { 1166 | fill: data.color[j], 1167 | duration: $scope.speed.s 1168 | }) 1169 | } 1170 | } else { 1171 | stripe.style.fill = data.color[j] 1172 | } 1173 | } catch (error) { 1174 | stripe.style.fill = data.color[j] 1175 | } 1176 | } 1177 | } 1178 | valfield = document.getElementById('level_value_channel_' + j + '_' + $scope.unique) 1179 | if (valfield) { 1180 | try { 1181 | if ($scope.animate.g !== 'off' && $scope.animate.t == true) { 1182 | var updateHandler = function (t) { 1183 | $(t.field).text( 1184 | (Math.ceil(t.val * $scope.d.mult) / $scope.d.mult).toFixed($scope.d.fixed)) 1185 | } 1186 | var nob = { 1187 | val: 0, 1188 | field: valfield, 1189 | from: parseFloat($(valfield).text()) 1190 | } 1191 | gsap.fromTo(nob, { 1192 | val: nob.from 1193 | }, { 1194 | val: data.payload[j], 1195 | duration: $scope.speed.s, 1196 | onUpdate: updateHandler, 1197 | onUpdateParams: [nob] 1198 | }) 1199 | } else { 1200 | $(valfield).text(data.payload[j].toFixed($scope.d.fixed)) 1201 | } 1202 | } catch (error) { 1203 | $(valfield).text(data.payload[j].toFixed($scope.d.fixed)) 1204 | } 1205 | } 1206 | } 1207 | } 1208 | var updateContainerStyle = function () { 1209 | var el = document.getElementById('level_svg_' + $scope.unique) 1210 | if (!el) { 1211 | setTimeout(updateContainerStyle, 40) 1212 | return 1213 | } 1214 | el = el.parentElement 1215 | if (el && el.classList.contains('nr-dashboard-template')) { 1216 | if ($(el).css('paddingLeft') == '0px') { 1217 | el.style.paddingLeft = el.style.paddingRight = $scope.padding.hor 1218 | el.style.paddingTop = el.style.paddingBottom = $scope.padding.vert 1219 | } 1220 | } 1221 | } 1222 | var update = function (data) { 1223 | //console.log('update', $scope.unique, 'R:', $scope.retried, $scope.waitingmessage != null, data) 1224 | if (data === null) { 1225 | if ($scope.retried > 5) { 1226 | $scope.waitingmessage = null 1227 | $scope.retried = 0 1228 | return 1229 | } 1230 | $scope.retried++ 1231 | } 1232 | $scope.inited = true 1233 | $scope.timeout = null 1234 | if ($scope.waitingmessage != null) { 1235 | var d = {} 1236 | Object.assign(d, $scope.waitingmessage) 1237 | $scope.waitingmessage = null 1238 | // console.log('ui-level ',$scope.unique,': reinit for waiting msg ') 1239 | $scope.timeout = setTimeout(() => { 1240 | update(d) 1241 | }, 50) 1242 | return 1243 | } 1244 | if (data === null) { 1245 | return 1246 | } 1247 | if (data.config) { 1248 | updateConfig(data.config) 1249 | } 1250 | if (data.peakreset) { 1251 | resetPeak() 1252 | } 1253 | if (data.payload) { 1254 | updateLevel(data) 1255 | setTicks() 1256 | } 1257 | } 1258 | $scope.$watch('msg', function (msg) { 1259 | if (!msg) { 1260 | return 1261 | } 1262 | var id = 'level_mask_0_' + $scope.unique 1263 | var stripe = document.getElementById(id) 1264 | if (!$scope.inited || stripe == null) { 1265 | $scope.waitingmessage = msg 1266 | } 1267 | update(msg) 1268 | }) 1269 | $scope.$on('$destroy', function () { 1270 | if ($scope.hold) { 1271 | for (var i = 0; i < 2; i++) { 1272 | if ($scope.hold[i] != null) { 1273 | clearInterval($scope.hold[i]) 1274 | } 1275 | } 1276 | } 1277 | }) 1278 | } 1279 | }) 1280 | } 1281 | } catch (e) { 1282 | console.log(e) 1283 | } 1284 | node.on('close', function () { 1285 | if (done) { 1286 | done() 1287 | } 1288 | }) 1289 | } 1290 | RED.nodes.registerType('ui_level', LevelNode) 1291 | var uipath = 'ui' 1292 | if (RED.settings.ui) { 1293 | uipath = RED.settings.ui.path 1294 | } 1295 | var fullPath = path.join('/', uipath, '/ui-level/*').replace(/\\/g, '/') 1296 | RED.httpNode.get(fullPath, function (req, res) { 1297 | var options = { 1298 | root: __dirname + '/lib/', 1299 | dotfiles: 'deny' 1300 | } 1301 | res.sendFile(req.params[0], options) 1302 | }) 1303 | } 1304 | --------------------------------------------------------------------------------