committed by
GitHub
9 changed files with 703 additions and 10 deletions
@ -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 tbKnob from './knob.directive'; |
|||
|
|||
export default angular.module('thingsboard.widgets.rpc', [ |
|||
tbKnob |
|||
]).name; |
|||
@ -0,0 +1,388 @@ |
|||
/* |
|||
* 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 './knob.scss'; |
|||
|
|||
import CanvasDigitalGauge from './../CanvasDigitalGauge'; |
|||
import tinycolor from 'tinycolor2'; |
|||
|
|||
import { isNumber } from '../widget-utils'; |
|||
|
|||
/* eslint-disable import/no-unresolved, import/default */ |
|||
|
|||
import knobTemplate from './knob.tpl.html'; |
|||
|
|||
/* eslint-enable import/no-unresolved, import/default */ |
|||
|
|||
export default angular.module('thingsboard.widgets.rpc.knob', []) |
|||
.directive('tbKnob', Knob) |
|||
.name; |
|||
|
|||
/*@ngInject*/ |
|||
function Knob() { |
|||
return { |
|||
restrict: "E", |
|||
scope: true, |
|||
bindToController: { |
|||
ctx: '=' |
|||
}, |
|||
controller: KnobController, |
|||
controllerAs: 'vm', |
|||
templateUrl: knobTemplate |
|||
}; |
|||
} |
|||
|
|||
/*@ngInject*/ |
|||
function KnobController($element, $scope, $document) { |
|||
let vm = this; |
|||
|
|||
vm.value = 0; |
|||
vm.error = ''; |
|||
|
|||
var knob = angular.element('.knob', $element), |
|||
knobContainer = angular.element('#knob-container', $element), |
|||
knobTopPointerContainer = knob.find('.top-pointer-container'), |
|||
knobTopPointer = knob.find('.top-pointer'), |
|||
knobValueContainer = knob.find('.value-container'), |
|||
knobValue = knob.find('.knob-value'), |
|||
knobErrorContainer = knob.find('.error-container'), |
|||
knobError = knob.find('.knob-error'), |
|||
knobTitleContainer = knob.find('.title-container'), |
|||
knobTitle = knob.find('.knob-title'), |
|||
knobMinmaxContainer = knob.find('.minmax-container'), |
|||
minmaxLanel = knob.find('.minmax-label'), |
|||
textMeasure = knob.find('#text-measure'), |
|||
startDeg = -1, |
|||
currentDeg = 0, |
|||
rotation = 0, |
|||
lastDeg = 0, |
|||
moving = false; |
|||
|
|||
var minDeg = -45; |
|||
var maxDeg = 225; |
|||
|
|||
var canvasBarElement = angular.element('#canvasBar', $element); |
|||
|
|||
var levelColors = ['#19ff4b', '#ffff19', '#ff3232']; |
|||
|
|||
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.title = angular.isDefined(vm.ctx.settings.title) ? vm.ctx.settings.title : ''; |
|||
|
|||
vm.darkTheme = vm.ctx.settings.theme == 'dark'; |
|||
|
|||
var canvasBarData = { |
|||
renderTo: canvasBarElement[0], |
|||
hideValue: true, |
|||
neonGlowBrightness: 0, |
|||
gaugeWidthScale: 0.4, |
|||
gaugeColor: 'rgba(0, 0, 0, 0)', |
|||
levelColors: levelColors, |
|||
minValue: vm.minValue, |
|||
maxValue: vm.maxValue, |
|||
gaugeType: 'donut', |
|||
dashThickness: 2, |
|||
donutStartAngle: 3/4*Math.PI, |
|||
donutEndAngle: 9/4*Math.PI, |
|||
animation: false |
|||
}; |
|||
|
|||
canvasBar = new CanvasDigitalGauge(canvasBarData).draw(); |
|||
|
|||
knob.on('click', (e) => { |
|||
if (moving) { |
|||
moving = false; |
|||
return false; |
|||
} |
|||
e.preventDefault(); |
|||
|
|||
var offset = knob.offset(); |
|||
var center = { |
|||
y : offset.top + knob.height()/2, |
|||
x: offset.left + knob.width()/2 |
|||
}; |
|||
var a, b, deg, |
|||
rad2deg = 180/Math.PI; |
|||
|
|||
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 (deg > maxDeg) { |
|||
if (deg - 360 > minDeg) { |
|||
deg = deg - 360; |
|||
} else { |
|||
return false; |
|||
} |
|||
} |
|||
currentDeg = deg; |
|||
lastDeg = deg; |
|||
knobTopPointerContainer.css('transform','rotate('+(currentDeg)+'deg)'); |
|||
turn(degreeToRatio(currentDeg)); |
|||
rotation = currentDeg; |
|||
startDeg = -1; |
|||
}); |
|||
|
|||
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) => { |
|||
moving = true; |
|||
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 (tmp > maxDeg) { |
|||
if (tmp - 360 > minDeg) { |
|||
tmp = tmp - 360; |
|||
} else { |
|||
var deltaMax = Math.abs(maxDeg - lastDeg); |
|||
var deltaMin = Math.abs(minDeg - lastDeg); |
|||
if (deltaMax < deltaMin) { |
|||
tmp = maxDeg; |
|||
} else { |
|||
tmp = minDeg; |
|||
} |
|||
} |
|||
} |
|||
if(Math.abs(tmp - lastDeg) > 180){ |
|||
startDeg = deg; |
|||
rotation = currentDeg; |
|||
return false; |
|||
} |
|||
|
|||
currentDeg = tmp; |
|||
lastDeg = tmp; |
|||
|
|||
knobTopPointerContainer.css('transform','rotate('+(currentDeg)+'deg)'); |
|||
turn(degreeToRatio(currentDeg)); |
|||
}); |
|||
|
|||
$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); |
|||
|
|||
var subscription = vm.ctx.defaultSubscription; |
|||
var rpcEnabled = subscription.rpcEnabled; |
|||
|
|||
vm.isSimulated = $scope.widgetEditMode; |
|||
|
|||
vm.requestTimeout = 500; |
|||
if (vm.ctx.settings.requestTimeout) { |
|||
vm.requestTimeout = vm.ctx.settings.requestTimeout; |
|||
} |
|||
vm.getValueMethod = 'getValue'; |
|||
if (vm.ctx.settings.getValueMethod && vm.ctx.settings.getValueMethod.length) { |
|||
vm.getValueMethod = vm.ctx.settings.getValueMethod; |
|||
} |
|||
vm.setValueMethod = 'setValue'; |
|||
if (vm.ctx.settings.setValueMethod && vm.ctx.settings.setValueMethod.length) { |
|||
vm.setValueMethod = vm.ctx.settings.setValueMethod; |
|||
} |
|||
if (!rpcEnabled) { |
|||
onError('Target device is not set!'); |
|||
} else { |
|||
if (!vm.isSimulated) { |
|||
rpcRequestValue(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
function ratioToDegree(ratio) { |
|||
return minDeg + ratio*(maxDeg-minDeg); |
|||
} |
|||
|
|||
function degreeToRatio(degree) { |
|||
return (degree-minDeg)/(maxDeg-minDeg); |
|||
} |
|||
|
|||
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}); |
|||
setFontSize(knobTitle, vm.title, knobTitleContainer.height(), knobTitleContainer.width()); |
|||
setFontSize(knobError, vm.error, knobErrorContainer.height(), knobErrorContainer.width()); |
|||
var minmaxHeight = knobMinmaxContainer.height(); |
|||
minmaxLanel.css({'fontSize': minmaxHeight+'px', 'lineHeight': minmaxHeight+'px'}); |
|||
checkValueSize(); |
|||
} |
|||
|
|||
function turn(ratio) { |
|||
var value = (vm.minValue + (vm.maxValue - vm.minValue)*ratio).toFixed(vm.ctx.decimals); |
|||
if (canvasBar.value != value) { |
|||
canvasBar.value = value; |
|||
} |
|||
updateColor(canvasBar.getValueColor()); |
|||
onValue(value); |
|||
} |
|||
|
|||
function setValue(value) { |
|||
var ratio = (value-vm.minValue) / (vm.maxValue - vm.minValue); |
|||
rotation = lastDeg = currentDeg = ratioToDegree(ratio); |
|||
knobTopPointerContainer.css('transform','rotate('+(currentDeg)+'deg)'); |
|||
if (canvasBar.value != value) { |
|||
canvasBar.value = value; |
|||
} |
|||
updateColor(canvasBar.getValueColor()); |
|||
vm.value = formatValue(value); |
|||
checkValueSize(); |
|||
} |
|||
|
|||
function updateColor(color) { |
|||
var glowColor = tinycolor(color).brighten(30).toHexString(); |
|||
knobValue.css({'color': glowColor}); |
|||
var textShadow = `${color} 1px 1px 10px, ${glowColor} 1px 1px 10px`; |
|||
knobValue.css({'textShadow': textShadow}); |
|||
knobTopPointer.css({'backgroundColor': glowColor}); |
|||
var boxShadow = `inset 1px 0 2px #040404, 1px 1px 8px 2px ${glowColor}`; |
|||
knobTopPointer.css({'boxShadow': boxShadow}); |
|||
} |
|||
|
|||
function onValue(value) { |
|||
$scope.$applyAsync(() => { |
|||
vm.value = formatValue(value); |
|||
checkValueSize(); |
|||
rpcUpdateValue(value); |
|||
}); |
|||
} |
|||
|
|||
function onError(error) { |
|||
$scope.$applyAsync(() => { |
|||
vm.error = error; |
|||
setFontSize(knobError, vm.error, knobErrorContainer.height(), knobErrorContainer.width()); |
|||
}); |
|||
} |
|||
|
|||
function formatValue(value) { |
|||
return vm.ctx.utils.formatValue(value, vm.ctx.decimals, vm.ctx.units, true); |
|||
} |
|||
|
|||
function checkValueSize() { |
|||
var fontSize = knobValueContainer.height()/3.3; |
|||
var containerWidth = knobValueContainer.width(); |
|||
setFontSize(knobValue, vm.value, fontSize, containerWidth); |
|||
} |
|||
|
|||
function setFontSize(element, text, fontSize, maxWidth) { |
|||
var textWidth = measureTextWidth(text, fontSize); |
|||
while (textWidth > maxWidth) { |
|||
fontSize--; |
|||
textWidth = measureTextWidth(text, fontSize); |
|||
} |
|||
element.css({'fontSize': fontSize+'px', 'lineHeight': fontSize+'px'}); |
|||
} |
|||
|
|||
function measureTextWidth(text, fontSize) { |
|||
textMeasure.css({'fontSize': fontSize+'px', 'lineHeight': fontSize+'px'}); |
|||
textMeasure.text(text); |
|||
return textMeasure.width(); |
|||
} |
|||
|
|||
function rpcRequestValue() { |
|||
vm.error = ''; |
|||
vm.ctx.controlApi.sendTwoWayCommand(vm.getValueMethod, null, vm.requestTimeout).then( |
|||
(responseBody) => { |
|||
if (isNumber(responseBody)) { |
|||
var numValue = Number(responseBody).toFixed(vm.ctx.decimals); |
|||
setValue(numValue); |
|||
} else { |
|||
var errorText = `Unable to parse response: ${responseBody}`; |
|||
onError(errorText); |
|||
} |
|||
}, |
|||
() => { |
|||
var errorText = vm.ctx.defaultSubscription.rpcErrorText; |
|||
onError(errorText); |
|||
} |
|||
); |
|||
} |
|||
|
|||
function rpcUpdateValue(value) { |
|||
if (vm.executingUpdateValue) { |
|||
vm.scheduledValue = value; |
|||
return; |
|||
} else { |
|||
vm.scheduledValue = null; |
|||
vm.rpcValue = value; |
|||
vm.executingUpdateValue = true; |
|||
} |
|||
vm.error = ''; |
|||
vm.ctx.controlApi.sendOneWayCommand(vm.setValueMethod, value, vm.requestTimeout).then( |
|||
() => { |
|||
vm.executingUpdateValue = false; |
|||
if (vm.scheduledValue != null && vm.scheduledValue != vm.rpcValue) { |
|||
rpcUpdateValue(vm.scheduledValue); |
|||
} |
|||
}, |
|||
() => { |
|||
vm.executingUpdateValue = false; |
|||
var errorText = vm.ctx.defaultSubscription.rpcErrorText; |
|||
onError(errorText); |
|||
} |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,157 @@ |
|||
/** |
|||
* 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.svg'); |
|||
|
|||
$bars-margin-pct: percentage(0.033); |
|||
$background-margin-pct: percentage(0.05); |
|||
$value-container-margin-pct: percentage(0.35); |
|||
$error-height: percentage(0.05); |
|||
$title-height: percentage(0.066); |
|||
$title-container-margin-pct: percentage(0.2); |
|||
$title-container-margin-bottom-pct: percentage(0.05); |
|||
$minmax-height: percentage(0.04); |
|||
$minmax-container-margin-pct: percentage(0.18); |
|||
$minmax-container-margin-bottom-pct: percentage(0.12); |
|||
|
|||
$background-color: #e6e7e8; |
|||
|
|||
.tb-knob { |
|||
width:100%; |
|||
height:100%; |
|||
background: $background-color; |
|||
&.dark { |
|||
background: #000; |
|||
} |
|||
|
|||
.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; |
|||
z-index: 2; |
|||
} |
|||
.canvas-background { |
|||
position:absolute; |
|||
top: $background-margin-pct; |
|||
left: $background-margin-pct; |
|||
bottom: $background-margin-pct; |
|||
right: $background-margin-pct; |
|||
border-radius: 50%; |
|||
background: #3f4346; |
|||
z-index:2; |
|||
} |
|||
.value-container { |
|||
position:absolute; |
|||
top: $value-container-margin-pct; |
|||
left: $value-container-margin-pct; |
|||
bottom: $value-container-margin-pct; |
|||
right: $value-container-margin-pct; |
|||
z-index:4; |
|||
.knob-value { |
|||
color: #fff; |
|||
font-weight: 500; |
|||
white-space: nowrap; |
|||
} |
|||
} |
|||
.error-container { |
|||
position:absolute; |
|||
top: 1%; |
|||
left: 0; |
|||
right: 0; |
|||
z-index:4; |
|||
height: $error-height; |
|||
.knob-error { |
|||
color: #ff3315; |
|||
white-space: nowrap; |
|||
} |
|||
} |
|||
.title-container { |
|||
position:absolute; |
|||
left: $title-container-margin-pct; |
|||
bottom: $title-container-margin-bottom-pct; |
|||
right: $title-container-margin-pct; |
|||
z-index:4; |
|||
height: $title-height; |
|||
.knob-title { |
|||
color: #757575; |
|||
font-weight: 500; |
|||
white-space: nowrap; |
|||
} |
|||
} |
|||
.minmax-container { |
|||
position:absolute; |
|||
left: $minmax-container-margin-pct; |
|||
bottom: $minmax-container-margin-bottom-pct; |
|||
right: $minmax-container-margin-pct; |
|||
z-index:4; |
|||
height: $minmax-height; |
|||
.minmax-label { |
|||
color: #757575; |
|||
font-weight: 500; |
|||
white-space: nowrap; |
|||
} |
|||
} |
|||
.top-pointer-container { |
|||
position:absolute; |
|||
top: $bars-margin-pct; |
|||
left: $bars-margin-pct; |
|||
bottom: $bars-margin-pct; |
|||
right: $bars-margin-pct; |
|||
z-index:3; |
|||
cursor:pointer !important; |
|||
.top-pointer { |
|||
content:''; |
|||
width:5%; |
|||
height:5%; |
|||
background-color:#b5b5b5; |
|||
position:absolute; |
|||
top:50%; |
|||
left:22%; |
|||
margin-top:-2.5%; |
|||
border-radius: 50%; |
|||
cursor:pointer !important; |
|||
box-shadow: 1px 0 2px #040404; |
|||
} |
|||
} |
|||
.top{ |
|||
position:absolute; |
|||
top: $bars-margin-pct; |
|||
left: $bars-margin-pct; |
|||
bottom: $bars-margin-pct; |
|||
right: $bars-margin-pct; |
|||
background:$knob-img no-repeat; |
|||
background-size: contain; |
|||
z-index:2; |
|||
cursor:pointer !important; |
|||
} |
|||
#text-measure { |
|||
position: absolute; |
|||
visibility: hidden; |
|||
height: auto; |
|||
width: auto; |
|||
white-space: nowrap; |
|||
} |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,46 @@ |
|||
<!-- |
|||
|
|||
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-knob" layout="column" ng-class="{'dark': vm.darkTheme}" ng-style="{'pointerEvents': vm.ctx.isEdit ? 'none' : 'all'}"> |
|||
<div id="knob-container" flex layout="column" layout-align="center center"> |
|||
<div class="knob"> |
|||
<div class="value-container" layout="row" layout-align="center center"> |
|||
<span class="knob-value">{{ vm.value }}</span> |
|||
</div> |
|||
<div class="canvas-background"></div> |
|||
<canvas id="canvasBar"></canvas> |
|||
<div class="top-pointer-container"> |
|||
<div class="top-pointer"></div> |
|||
</div> |
|||
<div class="top"></div> |
|||
<div class="error-container" ng-style="{'background': vm.error.length ? 'rgba(255,255,255,0.25)' : 'none'}" |
|||
layout="row" layout-align="center center"> |
|||
<span class="knob-error">{{ vm.error }}</span> |
|||
</div> |
|||
<div class="title-container" layout="row" layout-align="center center"> |
|||
<span class="knob-title">{{ vm.title }}</span> |
|||
</div> |
|||
<div class="minmax-container" layout="row" layout-align="start center"> |
|||
<span class="minmax-label">min</span> |
|||
<span flex></span> |
|||
<span class="minmax-label">max</span> |
|||
</div> |
|||
<div id="text-measure"></div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
Loading…
Reference in new issue