Browse Source

TB-70: Add knob control widget.

pull/211/head
Igor Kulikov 9 years ago
parent
commit
c4e4f8953a
  1. 16
      application/src/main/data/json/system/widget_bundles/control_widgets.json
  2. 4
      ui/src/app/api/widget.service.js
  3. 16
      ui/src/app/widget/lib/CanvasDigitalGauge.js
  4. 21
      ui/src/app/widget/lib/rpc/index.js
  5. BIN
      ui/src/app/widget/lib/rpc/knob.png
  6. 198
      ui/src/app/widget/lib/rpc/shiny-knob.directive.js
  7. 80
      ui/src/app/widget/lib/rpc/shiny-knob.scss
  8. 30
      ui/src/app/widget/lib/rpc/shiny-knob.tpl.html

16
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\":{}}"
}
}
]
}

4
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;

16
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) {

21
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;

BIN
ui/src/app/widget/lib/rpc/knob.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

198
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;
});
}
}

80
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;
}
}
}

30
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>
Loading…
Cancel
Save