thingsboard-aplcache

TB-70: Add knob control widget.

7/18/2017 3:12:40 PM

Details

diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json
index ff920b1..b06c979 100644
--- a/application/src/main/data/json/system/widget_bundles/control_widgets.json
+++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json
@@ -36,6 +36,22 @@
         "dataKeySettingsSchema": "{}\n",
         "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#010101\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n   \\\"pin\\\": \\\"{$pin}\\\",\\n   \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#b71c1c\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"_uniqueKey\":2}]},\"title\":\"RPC remote shell\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
       }
+    },
+    {
+      "alias": "shiny_knob_control",
+      "name": "Shiny Knob Control",
+      "descriptor": {
+        "type": "rpc",
+        "sizeX": 2.5,
+        "sizeY": 3,
+        "resources": [],
+        "templateHtml": "<tb-shiny-knob ctx='ctx'></tb-shiny-knob>",
+        "templateCss": ".error {\n    font-size: 14px !important;\n    color: maroon;/*rgb(250,250,250);*/\n    background-color: transparent;\n    padding: 6px;\n}\n\n.error span {\n    margin: auto;\n}\n\n.gpio-panel {\n    padding-top: 10px;\n    white-space: nowrap;\n}\n\n.switch-panel {\n    margin: 0;\n    height: 32px;\n    width: 66px;\n    min-width: 66px;\n}\n\n.switch-panel md-switch {\n    margin: 0;\n    width: 36px;\n    min-width: 36px;\n}\n\n.switch-panel md-switch > div.md-container {\n    margin: 0;\n}\n\n.switch-panel.col-0 md-switch {\n    padding-left: 8px;\n    padding-right: 4px;\n}\n\n.switch-panel.col-1 md-switch {\n    padding-left: 4px;\n    padding-right: 8px;\n}\n\n.gpio-row {\n    height: 32px;\n}\n\n.pin {\n    margin-top: auto;\n    margin-bottom: auto;\n    color: white;\n    font-size: 12px;\n    width: 16px;\n    min-width: 16px;\n}\n\n.switch-panel.col-0 .pin {\n    margin-left: auto;\n    padding-left: 2px;\n    text-align: right;\n}\n\n.switch-panel.col-1 .pin {\n    margin-right: auto;\n    \n    text-align: left;\n}\n\n.gpio-left-label {\n    margin-right: 8px;\n}\n\n.gpio-right-label {\n    margin-left: 8px;\n}",
+        "controllerScript": "self.onInit = function() {\n    var scope = self.ctx.$scope;\n    scope.ctx = self.ctx;\n}\n\nself.onResize = function() {\n    if (self.ctx.resize) {\n        self.ctx.resize();\n    }\n}\n\nself.onDestroy = function() {\n}\n",
+        "settingsSchema": "{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"Settings\",\n        \"properties\": {\n            \"minValue\": {\n                \"title\": \"Minimum value\",\n                \"type\": \"number\",\n                \"default\": 0\n            },\n            \"maxValue\": {\n                \"title\": \"Maximum value\",\n                \"type\": \"number\",\n                \"default\": 100\n            },\n            \"initialValue\": {\n                \"title\": \"Initial value\",\n                \"type\": \"number\",\n                \"default\": 50\n            },\n            \"theme\": {\n                \"title\": \"Knob theme\",\n                \"type\": \"string\",\n                \"default\": \"light\"\n            },            \n            \"requestTimeout\": {\n                \"title\": \"RPC request timeout\",\n                \"type\": \"number\",\n                \"default\": 500\n            }\n        },\n        \"required\": [\"minValue\", \"maxValue\", \"requestTimeout\"]\n    },\n    \"form\": [\n        \"minValue\",\n        \"maxValue\",\n        \"initialValue\",\n        {\n            \"key\": \"theme\",\n            \"type\": \"rc-select\",\n            \"multiple\": false,\n            \"items\": [\n                {\n                    \"value\": \"light\",\n                    \"label\": \"Light\"\n                },\n                {\n                    \"value\": \"dark\",\n                    \"label\": \"Dark\"\n                }\n            ]\n        },\n        \"requestTimeout\"\n    ]\n}",
+        "dataKeySettingsSchema": "{}\n",
+        "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"maxValue\":100,\"theme\":\"light\",\"initialValue\":50},\"title\":\"Shiny Knob Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
+      }
     }
   ]
 }
\ No newline at end of file
diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js
index 8580b80..842b976 100644
--- a/ui/src/app/api/widget.service.js
+++ b/ui/src/app/api/widget.service.js
@@ -22,6 +22,8 @@ import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-wid
 import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget';
 import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget';
 
+import thingsboardRpcWidgets from '../widget/lib/rpc';
+
 import TbFlot from '../widget/lib/flot-widget';
 import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge';
 import TbAnalogueRadialGauge from '../widget/lib/analogue-radial-gauge';
@@ -39,7 +41,7 @@ import thingsboardTypes from '../common/types.constant';
 import thingsboardUtils from '../common/utils.service';
 
 export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
-    thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardTypes, thingsboardUtils])
+    thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils])
     .factory('widgetService', WidgetService)
     .name;
 
diff --git a/ui/src/app/widget/lib/CanvasDigitalGauge.js b/ui/src/app/widget/lib/CanvasDigitalGauge.js
index e51c558..e6c2e27 100644
--- a/ui/src/app/widget/lib/CanvasDigitalGauge.js
+++ b/ui/src/app/widget/lib/CanvasDigitalGauge.js
@@ -95,6 +95,12 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
             options.value = options.minValue;
         }
 
+        if (options.gaugeType === 'donut') {
+            if (!options.donutStartAngle) {
+                options.donutStartAngle = 1.5 * Math.PI;
+            }
+        }
+
         var colorsCount = options.levelColors.length;
         var inc = colorsCount > 1 ? (1 / (colorsCount - 1)) : 1;
         options.colorsRange = [];
@@ -473,7 +479,7 @@ function drawBackground(context, options) {
         context.lineCap = 'round';
     }
     if (options.gaugeType === 'donut') {
-        context.arc(context.barDimensions.Cx, context.barDimensions.Cy, context.barDimensions.Rm, 1.5 * Math.PI, 3.5 * Math.PI);
+        context.arc(context.barDimensions.Cx, context.barDimensions.Cy, context.barDimensions.Rm, options.donutStartAngle, options.donutStartAngle + 2 * Math.PI);
         context.stroke();
     } else if (options.gaugeType === 'arc') {
         context.arc(context.barDimensions.Cx, context.barDimensions.Cy, context.barDimensions.Rm, Math.PI, 2*Math.PI);
@@ -605,7 +611,7 @@ function getProgressColor(progress, colorsRange) {
     }
 }
 
-function drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, color, progress, isDonut) {
+function drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, color, progress, isDonut, donutStartAngle) {
     context.setLineDash([]);
     var strokeWidth = Ro - Ri;
     var blur = 0.55;
@@ -623,7 +629,7 @@ function drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, color, progress, isDonut) {
     context.beginPath();
     var e = 0.01 * Math.PI;
     if (isDonut) {
-        context.arc(Cx, Cy, Rm, 1.5 * Math.PI - e, 1.5 * Math.PI + 2 * Math.PI * progress + e);
+        context.arc(Cx, Cy, Rm, donutStartAngle - e, donutStartAngle + 2 * Math.PI * progress + e);
     } else {
         context.arc(Cx, Cy, Rm, Math.PI - e, Math.PI + Math.PI * progress + e);
     }
@@ -682,10 +688,10 @@ function drawProgress(context, options, progress) {
             context.strokeStyle = neonColor;
         }
         context.beginPath();
-        context.arc(Cx, Cy, Rm, 1.5 * Math.PI, 1.5 * Math.PI + 2 * Math.PI * progress);
+        context.arc(Cx, Cy, Rm, options.donutStartAngle, options.donutStartAngle + 2 * Math.PI * progress);
         context.stroke();
         if (options.neonGlowBrightness && !options.isMobile) {
-            drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, neonColor, progress, true);
+            drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, neonColor, progress, true, options.donutStartAngle);
         }
     } else if (options.gaugeType === 'arc') {
         if (options.neonGlowBrightness) {
diff --git a/ui/src/app/widget/lib/rpc/index.js b/ui/src/app/widget/lib/rpc/index.js
new file mode 100644
index 0000000..9f27989
--- /dev/null
+++ b/ui/src/app/widget/lib/rpc/index.js
@@ -0,0 +1,21 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * 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.
+ */
+
+import tbShinyKnob from './shiny-knob.directive';
+
+export default angular.module('thingsboard.widgets.rpc', [
+    tbShinyKnob
+]).name;
diff --git a/ui/src/app/widget/lib/rpc/knob.png b/ui/src/app/widget/lib/rpc/knob.png
new file mode 100644
index 0000000..24675b2
Binary files /dev/null and b/ui/src/app/widget/lib/rpc/knob.png differ
diff --git a/ui/src/app/widget/lib/rpc/shiny-knob.directive.js b/ui/src/app/widget/lib/rpc/shiny-knob.directive.js
new file mode 100644
index 0000000..b186799
--- /dev/null
+++ b/ui/src/app/widget/lib/rpc/shiny-knob.directive.js
@@ -0,0 +1,198 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * 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.
+ */
+
+import './shiny-knob.scss';
+
+import CanvasDigitalGauge from './../CanvasDigitalGauge';
+//import tinycolor from 'tinycolor2';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import shinyKnobTemplate from './shiny-knob.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.widgets.rpc.shinyKnob', [])
+    .directive('tbShinyKnob', ShinyKnob)
+    .name;
+
+/*@ngInject*/
+function ShinyKnob() {
+    return {
+        restrict: "E",
+        scope: true,
+        bindToController: {
+            ctx: '='
+        },
+        controller: ShinyKnobController,
+        controllerAs: 'vm',
+        templateUrl: shinyKnobTemplate
+    };
+}
+
+/*@ngInject*/
+function ShinyKnobController($element, $scope, $document) {
+    let vm = this;
+
+    vm.value = 0;
+
+    var snap = 0;
+
+    var knob = angular.element('.knob', $element),
+        knobContainer = angular.element('#knob-container', $element),
+        knobTop = knob.find('.top'),
+        startDeg = -1,
+        currentDeg = 0,
+        rotation = 0,
+        lastDeg = 0;
+
+    var canvasBarElement = angular.element('#canvasBar', $element);
+
+    var levelColors = ['rgb(0, 128, 0)', 'rgb(251, 192, 45)', 'rgb(244, 67, 54)'];
+    var canvasBar;
+
+    $scope.$watch('vm.ctx', () => {
+        if (vm.ctx) {
+            init();
+        }
+    });
+
+    function init() {
+
+        vm.minValue = angular.isDefined(vm.ctx.settings.minValue) ? vm.ctx.settings.minValue : 0;
+        vm.maxValue = angular.isDefined(vm.ctx.settings.maxValue) ? vm.ctx.settings.maxValue : 100;
+
+        vm.darkTheme = vm.ctx.settings.theme == 'dark';
+
+        var canvasBarData = {
+            renderTo: canvasBarElement[0],
+            hideValue: true,
+            neonGlowBrightness: vm.darkTheme ? 40 : 0,
+            gaugeWidthScale: 0.5,
+            gaugeColor: vm.darkTheme ? 'rgb(23, 26, 28)' : 'rgba(0,0,0,0)',
+            levelColors: levelColors,
+            minValue: vm.minValue,
+            maxValue: vm.maxValue,
+            gaugeType: 'donut',
+            dashThickness: 1.5,
+            donutStartAngle: Math.PI,
+            animation: false,
+            animationDuration: 250,
+            animationRule: 'linear'
+        };
+
+        canvasBar = new CanvasDigitalGauge(canvasBarData).draw();
+
+        knob.on('mousedown touchstart', (e) => {
+            e.preventDefault();
+            var offset = knob.offset();
+            var center = {
+                y : offset.top + knob.height()/2,
+                x: offset.left + knob.width()/2
+            };
+
+            var a, b, deg, tmp,
+                rad2deg = 180/Math.PI;
+
+            knob.on('mousemove.rem touchmove.rem', (e) => {
+
+                e = (e.originalEvent.touches) ? e.originalEvent.touches[0] : e;
+
+                a = center.y - e.pageY;
+                b = center.x - e.pageX;
+                deg = Math.atan2(a,b)*rad2deg;
+
+                if(deg<0){
+                    deg = 360 + deg;
+                }
+
+                if(startDeg == -1){
+                    startDeg = deg;
+                }
+
+                tmp = Math.floor((deg-startDeg) + rotation);
+
+                if(tmp < 0){
+                    tmp = 360 + tmp;
+                }
+                else if(tmp > 359){
+                    tmp = tmp % 360;
+                }
+
+                if(snap && tmp < snap){
+                    tmp = 0;
+                }
+                if(Math.abs(tmp - lastDeg) > 180){
+                    return false;
+                }
+                currentDeg = tmp;
+                lastDeg = tmp;
+
+                knobTop.css('transform','rotate('+(currentDeg)+'deg)');
+                turn(currentDeg/359);
+            });
+
+            $document.on('mouseup.rem  touchend.rem',() => {
+                knob.off('.rem');
+                $document.off('.rem');
+                rotation = currentDeg;
+                startDeg = -1;
+            });
+
+        });
+
+        vm.ctx.resize = resize;
+        resize();
+
+        var initialValue = angular.isDefined(vm.ctx.settings.initialValue) ? vm.ctx.settings.initialValue : vm.minValue;
+
+        setValue(initialValue);
+    }
+
+    function resize() {
+        var width = knobContainer.width();
+        var height = knobContainer.height();
+        var size = Math.min(width, height);
+        knob.css({width: size, height: size});
+        canvasBar.update({width: size, height: size});
+    }
+
+    function turn(ratio) {
+        var value = (vm.minValue + (vm.maxValue - vm.minValue)*ratio).toFixed(2);
+        if (canvasBar.value != value) {
+            canvasBar.value = value;
+        }
+        onValue(value);
+    }
+
+    function setValue(value) {
+        var ratio = (value-vm.minValue) / (vm.maxValue - vm.minValue);
+        rotation = lastDeg = currentDeg = ratio*360;
+        knobTop.css('transform','rotate('+(currentDeg)+'deg)');
+        if (canvasBar.value != value) {
+            canvasBar.value = value;
+        }
+        vm.value = value;
+    }
+
+    function onValue(value) {
+        console.log(`onValue ${value}`); //eslint-disable-line
+        $scope.$applyAsync(() => {
+            vm.value = value;
+        });
+    }
+
+}
\ No newline at end of file
diff --git a/ui/src/app/widget/lib/rpc/shiny-knob.scss b/ui/src/app/widget/lib/rpc/shiny-knob.scss
new file mode 100644
index 0000000..7ff204c
--- /dev/null
+++ b/ui/src/app/widget/lib/rpc/shiny-knob.scss
@@ -0,0 +1,80 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * 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.
+ */
+
+$knob-img: url('./knob.png');
+
+$bars-margin-pct: percentage(0.2);
+$shadow-size: 5;
+$shadow-size-px: $shadow-size+px;
+$shadow-offset-px: $shadow-size/2+px;
+
+.tb-shiny-knob {
+  width:100%;
+  height:100%;
+  &.dark {
+    background: #000;//$dark-bg-img #1f2129;
+  }
+
+  .knob {
+    position: relative;
+    &[draggable] {
+      -moz-user-select: none;
+      -webkit-user-select: none;
+      user-select: none;
+    }
+    #canvasBar {
+      position:absolute;
+      top:0;
+      left:0;
+      bottom: 0;
+      right: 0;
+    }
+    .top{
+      position:absolute;
+      top: calc(#{$bars-margin-pct} - #{$shadow-offset-px});
+      left: $bars-margin-pct;
+      bottom: calc(#{$bars-margin-pct} + #{$shadow-offset-px});
+      right: $bars-margin-pct;
+      background:$knob-img no-repeat;
+      background-size: contain;
+      z-index:10;
+      cursor:default !important;
+      &:after {
+        content:'';
+        width:10px;
+        height:10px;
+        background-color:#666;
+        position:absolute;
+        top:50%;
+        left:10px;
+        margin-top:-5px;
+        border-radius: 50%;
+        cursor:default !important;
+        box-shadow: 0 0 1px #5a5a5a inset;
+      }
+    }
+    .base{
+      top: calc(#{$bars-margin-pct} - #{$shadow-offset-px});
+      left: $bars-margin-pct;
+      bottom: calc(#{$bars-margin-pct} + #{$shadow-offset-px});
+      right: $bars-margin-pct;
+      border-radius:50%;
+      box-shadow:0 $shadow-size-px 0 #4a5056,$shadow-size-px $shadow-size-px $shadow-size-px #000;
+      position:absolute;
+      z-index:1;
+    }
+  }
+}
diff --git a/ui/src/app/widget/lib/rpc/shiny-knob.tpl.html b/ui/src/app/widget/lib/rpc/shiny-knob.tpl.html
new file mode 100644
index 0000000..18094b3
--- /dev/null
+++ b/ui/src/app/widget/lib/rpc/shiny-knob.tpl.html
@@ -0,0 +1,30 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    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.
+
+-->
+
+<div class="tb-shiny-knob" layout="column" ng-class="{'dark': vm.darkTheme}">
+    <div layout="row" layout-align="center start" class="md-padding">
+        <span>{{ vm.value }}</span>
+    </div>
+    <div id="knob-container" flex layout="column" layout-align="center center">
+        <div class="knob">
+            <canvas id="canvasBar"></canvas>
+            <div class="top"></div>
+            <div class="base"></div>
+        </div>
+    </div>
+</div>
\ No newline at end of file