Browse Source

UI: Refactoring.

pull/11391/head
Igor Kulikov 2 years ago
parent
commit
ebce8698e3
  1. 3
      ui-ngx/src/app/core/api/widget-api.models.ts
  2. 10
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  3. 26
      ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.html
  4. 48
      ui-ngx/src/app/modules/home/components/widget/config/basic/svg/iot-svg-basic-config.component.html
  5. 55
      ui-ngx/src/app/modules/home/components/widget/config/basic/svg/iot-svg-basic-config.component.ts
  6. 797
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts
  7. 15
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html
  8. 34
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts
  9. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts
  10. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  11. 9
      ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.html
  12. 34
      ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.scss
  13. 59
      ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts
  14. 23
      ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.models.ts
  15. 634
      ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts
  16. 6
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  17. 35
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  18. 382
      ui-ngx/src/assets/widget/svg/drawing.svg

3
ui-ngx/src/app/core/api/widget-api.models.ts

@ -23,7 +23,7 @@ import {
DatasourceType,
KeyInfo,
LegendConfig,
LegendData, TargetDevice,
LegendData, TargetDevice, WidgetAction,
WidgetActionDescriptor,
widgetType
} from '@shared/models/widget.models';
@ -95,6 +95,7 @@ export interface WidgetActionsApi {
getActionDescriptors: (actionSourceId: string) => Array<WidgetActionDescriptor>;
handleWidgetAction: ($event: Event, descriptor: WidgetActionDescriptor,
entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) => void;
onWidgetAction: ($event: Event, action: WidgetAction) => void;
elementClick: ($event: Event) => void;
cardClick: ($event: Event) => void;
click: ($event: Event) => void;

10
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -129,9 +129,7 @@ import {
import {
RadarChartBasicConfigComponent
} from '@home/components/widget/config/basic/chart/radar-chart-basic-config.component';
import {
ScadaTestBasicConfigComponent
} from '@home/components/widget/config/basic/scada/scada-test-basic-config.component';
import { IotSvgBasicConfigComponent } from '@home/components/widget/config/basic/svg/iot-svg-basic-config.component';
@NgModule({
declarations: [
@ -175,7 +173,7 @@ import {
BarChartBasicConfigComponent,
PolarAreaChartBasicConfigComponent,
RadarChartBasicConfigComponent,
ScadaTestBasicConfigComponent
IotSvgBasicConfigComponent
],
imports: [
CommonModule,
@ -221,7 +219,7 @@ import {
BarChartBasicConfigComponent,
PolarAreaChartBasicConfigComponent,
RadarChartBasicConfigComponent,
ScadaTestBasicConfigComponent
IotSvgBasicConfigComponent
]
})
export class BasicWidgetConfigModule {
@ -261,5 +259,5 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-bar-chart-basic-config': BarChartBasicConfigComponent,
'tb-polar-area-chart-basic-config': PolarAreaChartBasicConfigComponent,
'tb-radar-chart-basic-config': RadarChartBasicConfigComponent,
'tb-scada-test-basic-config': ScadaTestBasicConfigComponent
'tb-iot-svg-basic-config': IotSvgBasicConfigComponent
};

26
ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.html

@ -1,26 +0,0 @@
<!--
Copyright © 2016-2024 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.
-->
<ng-container [formGroup]="scadaTestWidgetConfigForm">
<tb-target-device formControlName="targetDevice"></tb-target-device>
<tb-scada-object-settings
formControlName="scadaObject"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType">
</tb-scada-object-settings>
</ng-container>

48
ui-ngx/src/app/modules/home/components/widget/config/basic/svg/iot-svg-basic-config.component.html

@ -0,0 +1,48 @@
<!--
Copyright © 2016-2024 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.
-->
<ng-container [formGroup]="iotSvgWidgetConfigForm">
<tb-target-device formControlName="targetDevice"></tb-target-device>
<tb-iot-svg-object-settings
formControlName="iotSvgObject"
[svgPath]="iotSvgWidgetConfigForm.get('iotSvg').value"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
[callbacks]="callbacks">
</tb-iot-svg-object-settings>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
<div class="tb-form-row space-between column-lt-md">
<div translate>widget-config.show-card-buttons</div>
<mat-chip-listbox multiple formControlName="cardButtons">
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-border-radius' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
</ng-container>

55
ui-ngx/src/app/modules/home/components/widget/config/basic/scada/scada-test-basic-config.component.ts → ui-ngx/src/app/modules/home/components/widget/config/basic/svg/iot-svg-basic-config.component.ts

@ -20,22 +20,26 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { TargetDevice, } from '@shared/models/widget.models';
import { TargetDevice, WidgetConfig, } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { ScadaTestWidgetSettings } from '@home/components/widget/lib/scada/scada-test-widget.models';
import {
iotSvgWidgetDefaultSettings,
IotSvgWidgetSettings
} from '@home/components/widget/lib/svg/iot-svg-widget.models';
import { isUndefined } from '@core/utils';
@Component({
selector: 'tb-scada-test-basic-config',
templateUrl: './scada-test-basic-config.component.html',
selector: 'tb-iot-svg-basic-config',
templateUrl: './iot-svg-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class ScadaTestBasicConfigComponent extends BasicWidgetConfigComponent {
export class IotSvgBasicConfigComponent extends BasicWidgetConfigComponent {
get targetDevice(): TargetDevice {
return this.scadaTestWidgetConfigForm.get('targetDevice').value;
return this.iotSvgWidgetConfigForm.get('targetDevice').value;
}
scadaTestWidgetConfigForm: UntypedFormGroup;
iotSvgWidgetConfigForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
@ -44,21 +48,48 @@ export class ScadaTestBasicConfigComponent extends BasicWidgetConfigComponent {
}
protected configForm(): UntypedFormGroup {
return this.scadaTestWidgetConfigForm;
return this.iotSvgWidgetConfigForm;
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: ScadaTestWidgetSettings = {...(configData.config.settings as ScadaTestWidgetSettings || { scadaObject: {}})};
this.scadaTestWidgetConfigForm = this.fb.group({
const settings: IotSvgWidgetSettings = {...iotSvgWidgetDefaultSettings, ...(configData.config.settings || {})};
this.iotSvgWidgetConfigForm = this.fb.group({
targetDevice: [configData.config.targetDevice, []],
scadaObject: [settings.scadaObject, []]
iotSvg: [settings.iotSvg, []],
iotSvgObject: [settings.iotSvgObject, []],
background: [settings.background, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.targetDevice = config.targetDevice;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.scadaObject = config.scadaObject;
this.widgetConfig.config.settings.iotSvg = config.iotSvg;
this.widgetConfig.config.settings.iotSvgObject = config.iotSvgObject;
this.widgetConfig.config.settings.background = config.background;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen');
}
return buttons;
}
private setCardButtons(buttons: string[], config: WidgetConfig) {
config.enableFullscreen = buttons.includes('fullscreen');
}
}

797
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts

@ -1,797 +0,0 @@
///
/// Copyright © 2016-2024 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 { ValueType } from '@shared/models/constants';
import { Box, Element, Runner, Svg, SVG, Timeline, Text } from '@svgdotjs/svg.js';
import {
DataToValueType,
GetValueAction,
GetValueSettings, SetValueAction,
SetValueSettings, ValueToDataType
} from '@shared/models/action-widget-settings.models';
import {
formatValue,
insertVariable,
isDefinedAndNotNull,
isNumber,
isNumeric,
isUndefinedOrNull,
mergeDeep,
parseFunction
} from '@core/utils';
import { BehaviorSubject, forkJoin, Observable } from 'rxjs';
import { map, share } from 'rxjs/operators';
import { ValueAction, ValueGetter, ValueSetter } from '@home/components/widget/lib/action/action-widget.models';
import { WidgetContext } from '@home/models/widget-component.models';
import { ColorProcessor, constantColor, Font } from '@shared/models/widget-settings.models';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
export type ScadaObjectValueType = 'input' | 'constant' | 'property' | 'function' | 'valueFormat';
export interface ScadaObjectValueBase {
type: ScadaObjectValueType;
}
export interface ScadaObjectValueConstant extends ScadaObjectValueBase {
constantValue?: any;
}
export interface ScadaObjectValueProperty extends ScadaObjectValueBase {
propertyId?: string;
computedPropertyValue?: any;
}
export interface ScadaObjectValueFunction extends ScadaObjectValueBase {
valueConvertFunction?: string;
valueConverter?: (val: any) => any;
}
export interface ScadaObjectValueFormat extends ScadaObjectValueBase {
units?: ScadaObjectValue;
decimals?: ScadaObjectValue;
computedUnits?: string;
computedDecimals?: number;
}
export type ScadaObjectValue = ScadaObjectValueProperty & ScadaObjectValueConstant & ScadaObjectValueFunction & ScadaObjectValueFormat;
export interface ScadaObjectAttribute {
name: string;
value: ScadaObjectValue;
}
export interface ScadaObjectText {
content?: ScadaObjectValue;
font?: ScadaObjectValue;
color?: ScadaObjectValue;
}
export interface ScadaObjectElementState {
tag: string;
inputValue?: string;
show?: ScadaObjectValue;
text?: ScadaObjectText;
attributes?: ScadaObjectAttribute[];
animate?: number;
addClass?: string;
removeClass?: string;
animationTimeline?: Timeline;
}
export interface ScadaObjectState {
initial?: boolean;
triggerValues?: string[];
conditionFunction?: string;
condition?: (values: {[key: string]: any}) => boolean;
state: ScadaObjectElementState[];
}
export interface ScadaObjectUpdateState {
updateValue: string;
}
export enum ScadaObjectBehaviorType {
setValue = 'setValue',
getValue = 'getValue'
}
export interface ScadaObjectBehaviorBase {
id: string;
name: string;
type: ScadaObjectBehaviorType;
}
export interface ScadaObjectBehaviorGet extends ScadaObjectBehaviorBase {
valueType: ValueType;
defaultValue: any;
onUpdate: ScadaObjectUpdateState[];
}
export interface ScadaObjectBehaviorSet extends ScadaObjectBehaviorBase {
valueToDataType: ValueToDataType;
constantValue: any;
valueToDataFunction: string;
}
export type ScadaObjectBehavior = ScadaObjectBehaviorGet | ScadaObjectBehaviorSet;
export type ScadaObjectPropertyType = 'string' | 'number' | 'color' | 'color-settings' | 'font' | 'units' | 'switch';
export interface ScadaObjectPropertyBase {
id: string;
name: string;
type: ScadaObjectPropertyType;
default: any;
required?: boolean;
subLabel?: string;
divider?: boolean;
fieldSuffix?: string;
disableOnProperty?: string;
rowClass?: string;
fieldClass?: string;
}
export interface ScadaObjectNumberProperty extends ScadaObjectPropertyBase {
min?: number;
max?: number;
step?: number;
}
export type ScadaObjectProperty = ScadaObjectPropertyBase & ScadaObjectNumberProperty;
export interface ScadaObjectCallBehavior {
behaviorId: string;
conditionFunction?: string;
condition?: (value: any) => boolean;
}
export interface ScadaObjectStateValue {
initialValue: any;
callBehavior?: ScadaObjectCallBehavior[];
}
export type ScadaObjectElementActionTrigger = 'click';
export type ScadaObjectActionType = 'updateValue';
export type ScadaObjectActionUpdateValueType = 'toggle' | 'increment' | 'constant';
export interface ScadaObjectElementAction {
trigger: ScadaObjectElementActionTrigger;
enabledTriggerValues?: string[];
enabledConditionFunction?: string;
enabledCondition?: (values: {[key: string]: any}) => boolean;
actionType: ScadaObjectActionType;
updateValueId?: string;
updateValueType?: ScadaObjectActionUpdateValueType;
updateValueConstant?: any;
updateValueInc?: number;
}
export interface ScadaObjectMetadata {
title: string;
stateValues: {[id: string]: ScadaObjectStateValue};
actions: {[tag: string]: ScadaObjectElementAction};
states: {[id: string]: ScadaObjectState};
behavior: ScadaObjectBehavior[];
properties: ScadaObjectProperty[];
}
export const emptyMetadata: ScadaObjectMetadata = {
title: '',
stateValues: {},
actions: {},
states: {},
behavior: [],
properties: []
};
export const parseScadaObjectMetadataFromContent = (svgContent: string): ScadaObjectMetadata => {
try {
const svgDoc = new DOMParser().parseFromString(svgContent, 'image/svg+xml');
return parseScadaObjectMetadataFromDom(svgDoc);
} catch (_e) {
return emptyMetadata;
}
};
const parseScadaObjectMetadataFromDom = (svgDoc: Document): ScadaObjectMetadata => {
try {
const elements = svgDoc.getElementsByTagName('tb:metadata');
if (elements.length) {
return JSON.parse(elements[0].innerHTML);
} else {
return emptyMetadata;
}
} catch (_e) {
console.error(_e);
return emptyMetadata;
}
};
const defaultGetValueSettings = (get: ScadaObjectBehaviorGet): GetValueSettings<any> => ({
action: GetValueAction.DO_NOTHING,
defaultValue: get.defaultValue,
executeRpc: {
method: 'getState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
getAttribute: {
key: 'state',
scope: null
},
getTimeSeries: {
key: 'state'
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
});
const defaultSetValueSettings = (set: ScadaObjectBehaviorSet): SetValueSettings => ({
action: SetValueAction.EXECUTE_RPC,
executeRpc: {
method: 'setState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
setAttribute: {
key: 'state',
scope: AttributeScope.SERVER_SCOPE
},
putTimeSeries: {
key: 'state'
},
valueToData: {
type: set.valueToDataType,
constantValue: set.constantValue,
valueToDataFunction: set.valueToDataFunction ? set.valueToDataFunction :
'/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;'
}
});
export const defaultScadaObjectSettings = (metadata: ScadaObjectMetadata): ScadaObjectSettings => {
const settings: ScadaObjectSettings = {};
for (const behavior of metadata.behavior) {
if (behavior.type === ScadaObjectBehaviorType.getValue) {
settings[behavior.id] = defaultGetValueSettings(behavior as ScadaObjectBehaviorGet);
} else if (behavior.type === ScadaObjectBehaviorType.setValue) {
settings[behavior.id] = defaultSetValueSettings(behavior as ScadaObjectBehaviorSet);
}
}
for (const property of metadata.properties) {
settings[property.id] = property.default;
}
return settings;
};
export type ScadaObjectSettings = {[id: string]: any};
export class ScadaObject {
private metadata: ScadaObjectMetadata;
private settings: ScadaObjectSettings;
private rootElement: HTMLElement;
private svgShape: Svg;
private box: Box;
private targetWidth: number;
private targetHeight: number;
private loadingSubject = new BehaviorSubject(false);
private valueGetters: ValueGetter<any>[] = [];
private valueActions: ValueAction[] = [];
private valueSetters: {[behaviorId: string]: ValueSetter<any>} = {};
private stateValueSubjects: {[id: string]: BehaviorSubject<any>} = {};
private stateValues: {[id: string]: any} = {};
loading$ = this.loadingSubject.asObservable().pipe(share());
constructor(private ctx: WidgetContext,
private svgPath: string,
private inputSettings: ScadaObjectSettings) {}
public init(): Observable<any> {
return this.ctx.http.get(this.svgPath, {responseType: 'text'}).pipe(
map((inputSvgContent) => {
const doc: XMLDocument = new DOMParser().parseFromString(inputSvgContent, 'image/svg+xml');
this.metadata = parseScadaObjectMetadataFromDom(doc);
const defaults = defaultScadaObjectSettings(this.metadata);
this.settings = mergeDeep<ScadaObjectSettings>({}, defaults, this.inputSettings || {});
this.prepareMetadata();
this.prepareSvgShape(doc);
this.initStates();
})
);
}
public addTo(element: HTMLElement) {
this.rootElement = element;
if (this.svgShape) {
this.svgShape.addTo(element);
}
}
public destroy() {
for (const stateValueId of Object.keys(this.stateValueSubjects)) {
this.stateValueSubjects[stateValueId].complete();
this.stateValueSubjects[stateValueId].unsubscribe();
}
this.valueActions.forEach(v => v.destroy());
this.loadingSubject.complete();
this.loadingSubject.unsubscribe();
}
public setSize(targetWidth: number, targetHeight: number) {
this.targetWidth = targetWidth;
this.targetHeight = targetHeight;
if (this.svgShape) {
this.resize();
}
}
private prepareMetadata() {
for (const stateValueId of Object.keys(this.metadata.stateValues)) {
const stateValue = this.metadata.stateValues[stateValueId];
if (stateValue.callBehavior) {
stateValue.callBehavior.forEach(callBehavior => {
let condition = () => true;
if (callBehavior.conditionFunction) {
try {
condition = parseFunction(this.insertVariables(callBehavior.conditionFunction), ['value']);
} catch (e) {
condition = () => false;
}
}
callBehavior.condition = condition;
});
}
}
for (const stateId of Object.keys(this.metadata.states)) {
const state = this.metadata.states[stateId];
let condition = () => true;
if (state.conditionFunction) {
try {
condition = parseFunction(this.insertVariables(state.conditionFunction), ['values']);
} catch (e) {
condition = () => false;
}
}
state.condition = condition;
for (const elementState of state.state) {
this.prepareValue(elementState.show);
this.prepareValue(elementState.text?.content);
this.prepareValue(elementState.text?.font);
this.prepareValue(elementState.text?.color);
if (elementState.attributes) {
for (const attribute of elementState.attributes) {
this.prepareValue(attribute.value);
}
}
}
}
}
private prepareValue(value: ScadaObjectValue) {
if (value) {
if (value.type === 'function' && value.valueConvertFunction) {
try {
value.valueConverter = parseFunction(this.insertVariables(value.valueConvertFunction), ['value']);
} catch (e) {
value.valueConverter = (v) => v;
}
} else if (value.type === 'property') {
value.computedPropertyValue = this.getPropertyValue(value.propertyId);
} else if (value.type === 'valueFormat') {
if (value.units) {
this.prepareValue(value.units);
}
if (value.decimals) {
this.prepareValue(value.decimals);
}
}
}
}
private insertVariables(content: string): string {
for (const property of this.metadata.properties) {
const value = this.getPropertyValue(property.id);
content = insertVariable(content, property.id, value);
}
return content;
}
private prepareSvgShape(doc: XMLDocument) {
const elements = doc.getElementsByTagName('tb:metadata');
for (let i=0;i<elements.length;i++) {
elements.item(i).remove();
}
this.svgShape = SVG().svg(doc.documentElement.innerHTML);
this.svgShape.node.style.overflow = 'visible';
this.svgShape.node.style['user-select'] = 'none';
this.box = this.svgShape.bbox();
this.svgShape.size(this.box.width, this.box.height);
if (this.rootElement) {
this.svgShape.addTo(this.rootElement);
}
if (this.targetWidth && this.targetHeight) {
this.resize();
}
}
private initStates() {
for (const stateValueId of Object.keys(this.metadata.stateValues)) {
const stateValue = this.metadata.stateValues[stateValueId];
this.stateValueSubjects[stateValueId] = new BehaviorSubject<any>(stateValue.initialValue);
this.stateValues[stateValueId] = stateValue.initialValue;
this.stateValueSubjects[stateValueId].subscribe(val => {
this.stateValues[stateValueId] = val;
const states = Object.values(this.metadata.states);
const triggerStates = states.filter(s => s.triggerValues && s.triggerValues.includes(stateValueId));
for (const state of triggerStates) {
if (state.condition(this.stateValues)) {
this.updateState(state);
}
}
});
}
for (const tag of Object.keys(this.metadata.actions)) {
const action = this.metadata.actions[tag];
const elements = this.svgShape.find(`[tb\\:tag="${tag}"]`);
switch (action.trigger) {
case 'click':
elements.forEach(e => {
e.attr('cursor', 'pointer');
e.on('click', () => {
this.triggerAction(action);
});
});
break;
}
}
for (const behavior of this.metadata.behavior) {
if (behavior.type === ScadaObjectBehaviorType.getValue) {
const getBehavior = behavior as ScadaObjectBehaviorGet;
let getValueSettings: GetValueSettings<any> = this.settings[getBehavior.id];
getValueSettings = {...getValueSettings, actionLabel: getBehavior.name};
const valueGetter =
ValueGetter.fromSettings(this.ctx, getValueSettings, getBehavior.valueType, {
next: (val) => {this.onValue(getBehavior.id, val);},
error: (e) => {}
});
this.valueGetters.push(valueGetter);
this.valueActions.push(valueGetter);
} else if (behavior.type === ScadaObjectBehaviorType.setValue) {
const setBehavior = behavior as ScadaObjectBehaviorSet;
let setValueSettings: SetValueSettings = this.settings[setBehavior.id];
setValueSettings = {...setValueSettings, actionLabel: setBehavior.name};
const valueSetter = ValueSetter.fromSettings<any>(this.ctx, setValueSettings);
this.valueSetters[setBehavior.id] = valueSetter;
this.valueActions.push(valueSetter);
}
}
const initialState = Object.values(this.metadata.states).find(s => s.initial);
if (initialState) {
this.updateState(initialState);
}
if (this.valueGetters.length) {
const getValueObservables: Array<Observable<any>> = [];
this.valueGetters.forEach(valueGetter => {
getValueObservables.push(valueGetter.getValue());
});
this.loadingSubject.next(true);
forkJoin(getValueObservables).subscribe(
{
next: () => {
this.loadingSubject.next(false);
},
error: () => {
this.loadingSubject.next(false);
}
}
);
}
}
private triggerAction(action: ScadaObjectElementAction) {
switch (action.actionType) {
case 'updateValue':
const targetValue = action.updateValueId;
const valueSubject = this.stateValueSubjects[targetValue];
if (valueSubject) {
const currentVal = valueSubject.value;
let newValue: any;
switch (action.updateValueType) {
case 'toggle':
newValue = !currentVal;
break;
case 'increment':
if (isNumber(currentVal)) {
newValue = currentVal + action.updateValueInc;
}
break;
case 'constant':
newValue = action.updateValueConstant;
break;
}
if (isDefinedAndNotNull(newValue)) {
valueSubject.next(newValue);
const stateValue = this.metadata.stateValues[targetValue];
if (stateValue.callBehavior) {
const observables: Observable<any>[] = [];
for (const behavior of stateValue.callBehavior) {
if (behavior.condition(newValue)) {
const valueSetter = this.valueSetters[behavior.behaviorId];
observables.push(valueSetter.setValue(newValue));
}
}
if (observables.length) {
this.loadingSubject.next(true);
forkJoin(observables).subscribe(
{
next: () => {
this.loadingSubject.next(false);
},
error: (err) => {
this.loadingSubject.next(false);
valueSubject.next(currentVal);
}
}
);
}
}
}
}
break;
}
}
private resize() {
let scale: number;
if (this.targetWidth < this.targetHeight) {
scale = this.targetWidth / this.box.width;
} else {
scale = this.targetHeight / this.box.height;
}
this.svgShape.node.style.transform = `scale(${scale})`;
}
private onValue(id: string, value: any) {
const getBehavior = this.metadata.behavior.find(b => b.id === id) as ScadaObjectBehaviorGet;
value = this.normalizeValue(value, getBehavior.valueType);
for (const onUpdate of getBehavior.onUpdate) {
const targetStateValueId = onUpdate.updateValue;
this.stateValueSubjects[targetStateValueId].next(value);
}
}
private updateState(state: ScadaObjectState) {
if (state) {
for (const elementState of state.state) {
const tag = elementState.tag;
let value;
if (elementState.inputValue) {
value = this.stateValues[elementState.inputValue];
}
if (elementState.animationTimeline) {
elementState.animationTimeline.finish();
}
const elements = this.svgShape.find(`[tb\\:tag="${tag}"]`);
if (elements.length) {
if (elementState.show) {
const show: boolean = this.computeValue(elementState.show, value);
elements.forEach(e => {
if (show) {
e.show();
} else {
e.hide();
}
});
}
if (elementState.addClass) {
elements.forEach(e => {
e.addClass(elementState.addClass);
});
}
if (elementState.removeClass) {
elements.forEach(e => {
e.removeClass(elementState.removeClass);
});
}
if (elementState.attributes) {
const attrs = this.computeAttributes(elementState.attributes, value);
elements.forEach(e => {
this.setElementAttributes(elementState, e, attrs, elementState.animate);
});
}
if (elementState.text) {
if (elementState.text.content) {
const text: string = this.computeValue(elementState.text.content, value);
elements.forEach(e => {
this.setElementText(e, text);
});
}
if (elementState.text.font || elementState.text.color) {
let font: Font = this.computeValue(elementState.text.font, value);
if (typeof font !== 'object') {
font = undefined;
}
let color: string = this.computeValue(elementState.text.color, value);
if (typeof color !== 'string') {
color = undefined;
}
elements.forEach(e => {
this.setElementFont(e, font, color);
});
}
}
}
}
}
}
private normalizeValue(value: any, type: ValueType): any {
if (isUndefinedOrNull(value)) {
switch (type) {
case ValueType.STRING:
return '';
case ValueType.INTEGER:
case ValueType.DOUBLE:
return 0;
case ValueType.BOOLEAN:
return false;
case ValueType.JSON:
return {};
}
} else {
return value;
}
}
private computeAttributes(attributes: ScadaObjectAttribute[], value: any): {[attr: string]: any} {
const res: {[attr: string]: any} = {};
for (const attribute of attributes) {
const attr = attribute.name;
res[attr] = this.computeValue(attribute.value, value);
}
return res;
}
private setElementAttributes(elementState: ScadaObjectElementState, element: Element, attrs: {[attr: string]: any}, animate?: number) {
if (isDefinedAndNotNull(animate)) {
this.animation(elementState, element, animate).attr(attrs);
} else {
element.attr(attrs);
}
}
private setElementText(element: Element, text: string) {
let textElement: Text;
if (element.type === 'text') {
const children = element.children();
if (children.length && children[0].type === 'tspan') {
textElement = children[0] as Text;
} else {
textElement = element as Text;
}
} else if (element.type === 'tspan') {
textElement = element as Text;
}
if (textElement) {
textElement.text(text);
}
}
private setElementFont(element: Element, font: Font, color: string) {
if (element.type === 'text') {
const textElement = element as Text;
if (font) {
textElement.font({
family: font.family,
size: (isDefinedAndNotNull(font.size) && isDefinedAndNotNull(font.sizeUnit)) ?
font.size + font.sizeUnit : null,
weight: font.weight,
style: font.style
});
}
if (color) {
textElement.fill(color);
}
}
}
private animation(elementState: ScadaObjectElementState, element: Element, duration: number): Runner {
if (!elementState.animationTimeline) {
elementState.animationTimeline = new Timeline();
}
element.timeline(elementState.animationTimeline);
return element.animate(duration, 0, 'now');
}
private computeValue(objectValue: ScadaObjectValue, value: any): any {
if (objectValue) {
switch (objectValue.type) {
case 'input':
return value;
case 'constant':
return objectValue.constantValue;
case 'property':
const property = this.getProperty(objectValue.propertyId);
if (property.type === 'color-settings') {
const colorProcessor: ColorProcessor = objectValue.computedPropertyValue;
colorProcessor.update(value);
return colorProcessor.color;
} else {
return objectValue.computedPropertyValue;
}
case 'function':
try {
return objectValue.valueConverter(value);
} catch (_e) {
return value;
}
case 'valueFormat':
let units = '';
let decimals = 0;
if (objectValue.units) {
units = this.computeValue(objectValue.units, value);
}
if (objectValue.decimals) {
decimals = this.computeValue(objectValue.decimals, value);
}
return formatValue(value, decimals, units, false);
}
} else {
return '';
}
}
private getProperty(id: string): ScadaObjectProperty {
return this.metadata.properties.find(p => p.id === id);
}
private getPropertyValue(id: string): any {
const property = this.getProperty(id);
if (property) {
const value = this.settings[id];
if (isDefinedAndNotNull(value)) {
if (property.type === 'color-settings') {
return ColorProcessor.fromSettings(value);
}
return value;
} else {
switch (property.type) {
case 'string':
return '';
case 'number':
return 0;
case 'color':
return '#000';
case 'color-settings':
return constantColor('#000');
}
}
} else {
return '';
}
}
}

15
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html

@ -21,7 +21,7 @@
<div class="tb-form-panel-title" translate>widgets.slider.behavior</div>
<div *ngFor="let behaviour of metadata.behavior" class="tb-form-row">
<div class="fixed-title-width">{{ behaviour.name }}</div>
<tb-get-value-action-settings *ngIf="behaviour.type === ScadaObjectBehaviorType.getValue"
<tb-get-value-action-settings *ngIf="behaviour.type === IotSvgBehaviorType.value"
fxFlex
panelTitle="{{ behaviour.name }}"
[valueType]="behaviour.valueType"
@ -30,13 +30,22 @@
[widgetType]="widgetType"
formControlName="{{ behaviour.id }}">
</tb-get-value-action-settings>
<tb-set-value-action-settings *ngIf="behaviour.type === ScadaObjectBehaviorType.setValue"
<tb-set-value-action-settings *ngIf="behaviour.type === IotSvgBehaviorType.action"
fxFlex
panelTitle="{{ behaviour.name }}"
[valueType]="behaviour.valueType"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="{{ behaviour.id }}"></tb-set-value-action-settings>
formControlName="{{ behaviour.id }}">
</tb-set-value-action-settings>
<tb-widget-action-settings *ngIf="behaviour.type === IotSvgBehaviorType.widgetAction"
fxFlex
panelTitle="{{ behaviour.name }}"
[callbacks]="callbacks"
[widgetType]="widgetType"
formControlName="{{ behaviour.id }}">
</tb-widget-action-settings>
</div>
</div>
<div *ngIf="propertyRows?.length" class="tb-form-panel">

34
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts

@ -31,10 +31,10 @@ import { AppState } from '@core/core.state';
import {
defaultScadaObjectSettings,
parseScadaObjectMetadataFromContent,
ScadaObjectBehaviorType,
ScadaObjectMetadata,
ScadaObjectSettings
} from '@home/components/widget/lib/scada/scada.models';
IotSvgBehaviorType,
IotSvgMetadata,
IotSvgObjectSettings
} from '@home/components/widget/lib/svg/iot-svg.models';
import { HttpClient } from '@angular/common/http';
import { ValueType } from '@shared/models/constants';
import { IAliasController } from '@core/api/widget-api.models';
@ -43,35 +43,36 @@ import { isDefinedAndNotNull } from '@core/utils';
import {
ScadaPropertyRow,
toPropertyRows
} from '@home/components/widget/lib/settings/common/scada/scada-object-settings.models';
} from '@home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models';
import { merge, Observable, Subscription } from 'rxjs';
import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models';
@Component({
selector: 'tb-scada-object-settings',
templateUrl: './scada-object-settings.component.html',
selector: 'tb-iot-svg-object-settings',
templateUrl: './iot-svg-object-settings.component.html',
styleUrls: ['./../../widget-settings.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ScadaObjectSettingsComponent),
useExisting: forwardRef(() => IotSvgObjectSettingsComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ScadaObjectSettingsComponent),
useExisting: forwardRef(() => IotSvgObjectSettingsComponent),
multi: true
}
]
})
export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlValueAccessor, Validator {
export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, ControlValueAccessor, Validator {
ScadaObjectBehaviorType = ScadaObjectBehaviorType;
IotSvgBehaviorType = IotSvgBehaviorType;
@Input()
disabled: boolean;
@Input()
svgPath = '/assets/widget/scada/drawing.svg';
svgPath = 'drawing.svg';
@Input()
aliasController: IAliasController;
@ -79,10 +80,13 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV
@Input()
targetDevice: TargetDevice;
@Input()
callbacks: WidgetActionCallbacks;
@Input()
widgetType: widgetType;
private modelValue: ScadaObjectSettings;
private modelValue: IotSvgObjectSettings;
private propagateChange = null;
@ -91,7 +95,7 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV
public scadaObjectSettingsFormGroup: UntypedFormGroup;
metadata: ScadaObjectMetadata;
metadata: IotSvgMetadata;
propertyRows: ScadaPropertyRow[];
constructor(protected store: Store<AppState>,
@ -136,7 +140,7 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV
}
}
writeValue(value: ScadaObjectSettings): void {
writeValue(value: IotSvgObjectSettings): void {
this.modelValue = value || {};
this.setupValue();
}

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts

@ -14,16 +14,16 @@
/// limitations under the License.
///
import { ScadaObjectProperty } from '@home/components/widget/lib/scada/scada.models';
import { IotSvgProperty } from '@home/components/widget/lib/svg/iot-svg.models';
export interface ScadaPropertyRow {
label: string;
properties: ScadaObjectProperty[];
switch?: ScadaObjectProperty;
properties: IotSvgProperty[];
switch?: IotSvgProperty;
rowClass?: string;
}
export const toPropertyRows = (properties: ScadaObjectProperty[]): ScadaPropertyRow[] => {
export const toPropertyRows = (properties: IotSvgProperty[]): ScadaPropertyRow[] => {
const result: ScadaPropertyRow[] = [];
for (const property of properties) {
let propertyRow = result.find(r => r.label === property.name);

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts

@ -150,8 +150,8 @@ import {
} from '@home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component';
import { ChartBarSettingsComponent } from '@home/components/widget/lib/settings/common/chart/chart-bar-settings.component';
import {
ScadaObjectSettingsComponent
} from '@home/components/widget/lib/settings/common/scada/scada-object-settings.component';
IotSvgObjectSettingsComponent
} from '@home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component';
@NgModule({
declarations: [
@ -207,7 +207,7 @@ import {
TimeSeriesChartStateRowComponent,
TimeSeriesChartGridSettingsComponent,
StatusWidgetStateSettingsComponent,
ScadaObjectSettingsComponent,
IotSvgObjectSettingsComponent,
DataKeyInputComponent,
EntityAliasInputComponent
],
@ -269,7 +269,7 @@ import {
TimeSeriesChartStateRowComponent,
TimeSeriesChartGridSettingsComponent,
StatusWidgetStateSettingsComponent,
ScadaObjectSettingsComponent,
IotSvgObjectSettingsComponent,
DataKeyInputComponent,
EntityAliasInputComponent
],

9
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.html → ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.html

@ -15,5 +15,12 @@
limitations under the License.
-->
<div #scadaShape class="tb-scada-shape">
<div class="tb-iot-svg-panel" [style]="backgroundStyle$ | async">
<div class="tb-iot-svg-overlay" [style]="overlayStyle"></div>
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
<div class="tb-iot-svg-content">
<div #iotSvgShape class="tb-iot-svg-shape">
</div>
</div>
<mat-progress-bar class="tb-action-widget-progress" style="height: 4px;" color="accent" mode="indeterminate" *ngIf="iotSvgObject.loading$ | async"></mat-progress-bar>
</div>

34
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.scss

@ -13,10 +13,38 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-scada-shape {
.tb-iot-svg-panel {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 16px;
padding: 20px 24px 24px 24px;
> div:not(.tb-iot-svg-overlay) {
z-index: 1;
}
.tb-iot-svg-overlay {
position: absolute;
top: 12px;
left: 12px;
bottom: 12px;
right: 12px;
}
div.tb-widget-title {
padding: 0;
}
.tb-iot-svg-content {
flex: 1;
min-width: 0;
min-height: 0;
.tb-iot-svg-shape {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
}

59
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts

@ -28,28 +28,35 @@ import {
import { BasicActionWidgetComponent } from '@home/components/widget/lib/action/action-widget.models';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { SVG, Svg } from '@svgdotjs/svg.js';
import { HttpClient } from '@angular/common/http';
import { ScadaObject, ScadaObjectSettings } from '@home/components/widget/lib/scada/scada.models';
import { IotSvgObject } from '@home/components/widget/lib/svg/iot-svg.models';
import { ResizeObserver } from '@juggle/resize-observer';
import { ScadaTestWidgetSettings } from '@home/components/widget/lib/scada/scada-test-widget.models';
import {
iotSvgWidgetDefaultSettings,
IotSvgWidgetSettings
} from '@home/components/widget/lib/svg/iot-svg-widget.models';
import { Observable } from 'rxjs';
import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models';
@Component({
selector: 'tb-scada-test-widget',
templateUrl: './scada-test-widget.component.html',
styleUrls: ['../action/action-widget.scss', './scada-test-widget.component.scss'],
selector: 'tb-iot-svg-widget',
templateUrl: './iot-svg-widget.component.html',
styleUrls: ['../action/action-widget.scss', './iot-svg-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ScadaTestWidgetComponent extends
export class IotSvgWidgetComponent extends
BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('scadaShape', {static: false})
scadaShape: ElementRef<HTMLElement>;
@ViewChild('iotSvgShape', {static: false})
iotSvgShape: ElementRef<HTMLElement>;
private settings: IotSvgWidgetSettings;
private settings: ScadaTestWidgetSettings;
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
iotSvgObject: IotSvgObject;
private autoScale = true;
private scadaObject: ScadaObject;
private shapeResize$: ResizeObserver;
@ -63,18 +70,25 @@ export class ScadaTestWidgetComponent extends
ngOnInit(): void {
super.ngOnInit();
this.settings = {...this.ctx.settings};
this.scadaObject = new ScadaObject(this.ctx, '/assets/widget/scada/drawing.svg', this.settings.scadaObject);
this.scadaObject.init().subscribe();
this.settings = {...iotSvgWidgetDefaultSettings, ...this.ctx.settings};
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
this.iotSvgObject = new IotSvgObject(this.ctx, this.settings.iotSvg, this.settings.iotSvgObject);
this.iotSvgObject.onError((error) => {
this.ctx.showErrorToast(error, 'bottom', 'center', this.ctx.toastTargetId, true);
});
this.iotSvgObject.init().subscribe();
}
ngAfterViewInit(): void {
this.scadaObject.addTo(this.scadaShape.nativeElement);
this.iotSvgObject.addTo(this.iotSvgShape.nativeElement);
if (this.autoScale) {
this.shapeResize$ = new ResizeObserver(() => {
this.onResize();
});
this.shapeResize$.observe(this.scadaShape.nativeElement);
this.shapeResize$.observe(this.iotSvgShape.nativeElement);
this.onResize();
}
super.ngAfterViewInit();
@ -84,19 +98,22 @@ export class ScadaTestWidgetComponent extends
if (this.shapeResize$) {
this.shapeResize$.disconnect();
}
if (this.scadaObject) {
this.scadaObject.destroy();
if (this.iotSvgObject) {
this.iotSvgObject.destroy();
}
super.ngOnDestroy();
}
public onInit() {
super.onInit();
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
this.cd.detectChanges();
}
private onResize() {
const shapeWidth = this.scadaShape.nativeElement.getBoundingClientRect().width;
const shapeHeight = this.scadaShape.nativeElement.getBoundingClientRect().height;
this.scadaObject.setSize(shapeWidth, shapeHeight);
const shapeWidth = this.iotSvgShape.nativeElement.getBoundingClientRect().width;
const shapeHeight = this.iotSvgShape.nativeElement.getBoundingClientRect().height;
this.iotSvgObject.setSize(shapeWidth, shapeHeight);
}
}

23
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-test-widget.models.ts → ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.models.ts

@ -14,8 +14,25 @@
/// limitations under the License.
///
import { ScadaObjectSettings } from '@home/components/widget/lib/scada/scada.models';
import { IotSvgObjectSettings } from '@home/components/widget/lib/svg/iot-svg.models';
import { BackgroundSettings, BackgroundType } from '@shared/models/widget-settings.models';
export interface ScadaTestWidgetSettings {
scadaObject: ScadaObjectSettings;
export interface IotSvgWidgetSettings {
iotSvg: string;
iotSvgObject: IotSvgObjectSettings;
background: BackgroundSettings;
}
export const iotSvgWidgetDefaultSettings: IotSvgWidgetSettings = {
iotSvg: '/assets/widget/svg/drawing.svg',
iotSvgObject: {},
background: {
type: BackgroundType.color,
color: '#fff',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
}
};

634
ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts

@ -0,0 +1,634 @@
///
/// Copyright © 2016-2024 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 { ValueType } from '@shared/models/constants';
import { Box, Element, Runner, Svg, SVG, Text } from '@svgdotjs/svg.js';
import {
DataToValueType,
GetValueAction,
GetValueSettings,
SetValueAction,
SetValueSettings,
ValueToDataType
} from '@shared/models/action-widget-settings.models';
import { formatValue, isDefinedAndNotNull, isUndefinedOrNull, mergeDeep, parseFunction } from '@core/utils';
import { BehaviorSubject, forkJoin, Observable, Observer } from 'rxjs';
import { map, share } from 'rxjs/operators';
import { ValueAction, ValueGetter, ValueSetter } from '@home/components/widget/lib/action/action-widget.models';
import { WidgetContext } from '@home/models/widget-component.models';
import { ColorProcessor, constantColor, Font } from '@shared/models/widget-settings.models';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service';
import { WidgetAction, WidgetActionType } from '@shared/models/widget.models';
export interface IotSvgApi {
formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined;
text: (element: Element | Element[], text: string) => void;
font: (element: Element | Element[], font: Font, color: string) => void;
animate: (element: Element, duration: number) => Runner;
disable: (element: Element | Element[]) => void;
enable: (element: Element | Element[]) => void;
callAction: (event: Event, behaviorId: string, value?: any, observer?: Partial<Observer<void>>) => void;
setValue: (valueId: string, value: any) => void;
}
export interface IotSvgContext {
api: IotSvgApi;
tags: {[id: string]: Element[]};
values: {[id: string]: any};
properties: {[id: string]: any};
}
export type IotSvgStateRenderFunction = (svg: Svg, ctx: IotSvgContext) => void;
export type IotSvgTagStateRenderFunction = (element: Element, ctx: IotSvgContext) => void;
export type IotSvgActionTrigger = 'click';
export type IotSvgActionFunction = (event: Event, ctx: IotSvgContext) => void;
export interface IotSvgAction {
actionFunction?: string;
action?: IotSvgActionFunction;
}
export interface IotSvgTag {
tag: string;
stateRenderFunction?: string;
stateRender?: IotSvgTagStateRenderFunction;
actions?: {[trigger: string]: IotSvgAction};
}
export enum IotSvgBehaviorType {
value = 'value',
action = 'action',
widgetAction = 'widgetAction'
}
export interface IotSvgBehaviorBase {
id: string;
name: string;
type: IotSvgBehaviorType;
}
export interface IotSvgBehaviorValue extends IotSvgBehaviorBase {
valueType: ValueType;
defaultValue: any;
valueId: string;
}
export interface IotSvgBehaviorAction extends IotSvgBehaviorBase {
valueToDataType: ValueToDataType;
constantValue: any;
valueToDataFunction: string;
}
export type IotSvgBehavior = IotSvgBehaviorValue | IotSvgBehaviorAction;
export type IotSvgPropertyType = 'string' | 'number' | 'color' | 'color-settings' | 'font' | 'units' | 'switch';
export interface IotSvgPropertyBase {
id: string;
name: string;
type: IotSvgPropertyType;
default: any;
required?: boolean;
subLabel?: string;
divider?: boolean;
fieldSuffix?: string;
disableOnProperty?: string;
rowClass?: string;
fieldClass?: string;
}
export interface IotSvgNumberProperty extends IotSvgPropertyBase {
min?: number;
max?: number;
step?: number;
}
export type IotSvgProperty = IotSvgPropertyBase & IotSvgNumberProperty;
export type ScadaObjectElementActionTrigger = 'click';
export type ScadaObjectActionType = 'updateValue';
export type ScadaObjectActionUpdateValueType = 'toggle' | 'increment' | 'constant';
export interface ScadaObjectElementAction {
trigger: ScadaObjectElementActionTrigger;
enabledTriggerValues?: string[];
enabledConditionFunction?: string;
enabledCondition?: (values: {[key: string]: any}) => boolean;
actionType: ScadaObjectActionType;
updateValueId?: string;
updateValueType?: ScadaObjectActionUpdateValueType;
updateValueConstant?: any;
updateValueInc?: number;
}
export interface IotSvgMetadata {
title: string;
stateRenderFunction?: string;
stateRender?: IotSvgStateRenderFunction;
tags: IotSvgTag[];
behavior: IotSvgBehavior[];
properties: IotSvgProperty[];
}
export const emptyMetadata: IotSvgMetadata = {
title: '',
tags: [],
behavior: [],
properties: []
};
export const parseScadaObjectMetadataFromContent = (svgContent: string): IotSvgMetadata => {
try {
const svgDoc = new DOMParser().parseFromString(svgContent, 'image/svg+xml');
return parseScadaObjectMetadataFromDom(svgDoc);
} catch (_e) {
return emptyMetadata;
}
};
const parseScadaObjectMetadataFromDom = (svgDoc: Document): IotSvgMetadata => {
try {
const elements = svgDoc.getElementsByTagName('tb:metadata');
if (elements.length) {
return JSON.parse(elements[0].textContent);
} else {
return emptyMetadata;
}
} catch (_e) {
console.error(_e);
return emptyMetadata;
}
};
const defaultGetValueSettings = (get: IotSvgBehaviorValue): GetValueSettings<any> => ({
action: GetValueAction.DO_NOTHING,
defaultValue: get.defaultValue,
executeRpc: {
method: 'getState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
getAttribute: {
key: 'state',
scope: null
},
getTimeSeries: {
key: 'state'
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
});
const defaultSetValueSettings = (set: IotSvgBehaviorAction): SetValueSettings => ({
action: SetValueAction.EXECUTE_RPC,
executeRpc: {
method: 'setState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
setAttribute: {
key: 'state',
scope: AttributeScope.SERVER_SCOPE
},
putTimeSeries: {
key: 'state'
},
valueToData: {
type: set.valueToDataType,
constantValue: set.constantValue,
valueToDataFunction: set.valueToDataFunction ? set.valueToDataFunction :
'/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;'
}
});
const defaultWidgetActionSettings = (widgetAction: IotSvgBehavior): WidgetAction => ({
type: WidgetActionType.updateDashboardState,
targetDashboardStateId: null,
openRightLayout: false,
setEntityId: true,
stateEntityParamName: null
});
export const defaultScadaObjectSettings = (metadata: IotSvgMetadata): IotSvgObjectSettings => {
const settings: IotSvgObjectSettings = {};
for (const behavior of metadata.behavior) {
if (behavior.type === IotSvgBehaviorType.value) {
settings[behavior.id] = defaultGetValueSettings(behavior as IotSvgBehaviorValue);
} else if (behavior.type === IotSvgBehaviorType.action) {
settings[behavior.id] = defaultSetValueSettings(behavior as IotSvgBehaviorAction);
} else if (behavior.type === IotSvgBehaviorType.widgetAction) {
settings[behavior.id] = defaultWidgetActionSettings(behavior);
}
}
for (const property of metadata.properties) {
settings[property.id] = property.default;
}
return settings;
};
export type IotSvgObjectSettings = {[id: string]: any};
const parseError = (ctx: WidgetContext, err: any): string =>
ctx.$injector.get(UtilsService).parseException(err).message || 'Unknown Error';
export class IotSvgObject {
private metadata: IotSvgMetadata;
private settings: IotSvgObjectSettings;
private context: IotSvgContext;
private rootElement: HTMLElement;
private svgShape: Svg;
private box: Box;
private targetWidth: number;
private targetHeight: number;
private loadingSubject = new BehaviorSubject(false);
private valueGetters: ValueGetter<any>[] = [];
private valueActions: ValueAction[] = [];
private valueSetters: {[behaviorId: string]: ValueSetter<any>} = {};
private stateValueSubjects: {[id: string]: BehaviorSubject<any>} = {};
loading$ = this.loadingSubject.asObservable().pipe(share());
private _onError: (error: string) => void = () => {};
constructor(private ctx: WidgetContext,
private svgPath: string,
private inputSettings: IotSvgObjectSettings) {}
public init(): Observable<any> {
return this.ctx.http.get(this.svgPath, {responseType: 'text'}).pipe(
map((inputSvgContent) => {
const doc: XMLDocument = new DOMParser().parseFromString(inputSvgContent, 'image/svg+xml');
this.metadata = parseScadaObjectMetadataFromDom(doc);
const defaults = defaultScadaObjectSettings(this.metadata);
this.settings = mergeDeep<IotSvgObjectSettings>({}, defaults, this.inputSettings || {});
this.prepareMetadata();
this.prepareSvgShape(doc);
this.initialize();
})
);
}
public onError(onError: (error: string) => void) {
this._onError = onError;
}
public addTo(element: HTMLElement) {
this.rootElement = element;
if (this.svgShape) {
this.svgShape.addTo(element);
}
}
public destroy() {
for (const stateValueId of Object.keys(this.stateValueSubjects)) {
this.stateValueSubjects[stateValueId].complete();
this.stateValueSubjects[stateValueId].unsubscribe();
}
this.valueActions.forEach(v => v.destroy());
this.loadingSubject.complete();
this.loadingSubject.unsubscribe();
}
public setSize(targetWidth: number, targetHeight: number) {
this.targetWidth = targetWidth;
this.targetHeight = targetHeight;
if (this.svgShape) {
this.resize();
}
}
private prepareMetadata() {
this.metadata.stateRender = parseFunction(this.metadata.stateRenderFunction, ['svg', 'ctx']) || (() => {});
for (const tag of this.metadata.tags) {
tag.stateRender = parseFunction(tag.stateRenderFunction, ['element', 'ctx']) || (() => {});
if (tag.actions) {
for (const trigger of Object.keys(tag.actions)) {
const action = tag.actions[trigger];
action.action = parseFunction(action.actionFunction, ['event', 'ctx']) || (() => {});
}
}
}
}
private prepareSvgShape(doc: XMLDocument) {
const elements = doc.getElementsByTagName('tb:metadata');
for (let i=0;i<elements.length;i++) {
elements.item(i).remove();
}
this.svgShape = SVG().svg(doc.documentElement.innerHTML);
this.svgShape.node.style.overflow = 'visible';
this.svgShape.node.style['user-select'] = 'none';
this.box = this.svgShape.bbox();
this.svgShape.size(this.box.width, this.box.height);
if (this.rootElement) {
this.svgShape.addTo(this.rootElement);
}
if (this.targetWidth && this.targetHeight) {
this.resize();
}
}
private initialize() {
this.context = {
api: {
formatValue,
text: this.setElementText.bind(this),
font: this.setElementFont.bind(this),
animate: this.animate.bind(this),
disable: this.disableElement.bind(this),
enable: this.enableElement.bind(this),
callAction: this.callAction.bind(this),
setValue: this.setValue.bind(this)
},
tags: {},
properties: {},
values: {}
};
const taggedElements = this.svgShape.find(`[tb\\:tag]`);
for (const element of taggedElements) {
const tag = element.attr('tb:tag');
let elements = this.context.tags[tag];
if (!elements) {
elements = [];
this.context.tags[tag] = elements;
}
elements.push(element);
}
for (const property of this.metadata.properties) {
this.context.properties[property.id] = this.getPropertyValue(property.id);
}
for (const tag of this.metadata.tags) {
if (tag.actions) {
const elements = this.svgShape.find(`[tb\\:tag="${tag.tag}"]`);
for (const trigger of Object.keys(tag.actions)) {
const action = tag.actions[trigger];
elements.forEach(element => {
element.attr('cursor', 'pointer');
element.on(trigger, (event) => {
action.action(event, this.context);
});
});
}
}
}
for (const behavior of this.metadata.behavior) {
if (behavior.type === IotSvgBehaviorType.value) {
const getBehavior = behavior as IotSvgBehaviorValue;
let getValueSettings: GetValueSettings<any> = this.settings[getBehavior.id];
getValueSettings = {...getValueSettings, actionLabel: getBehavior.name};
const stateValueSubject = new BehaviorSubject<any>(getValueSettings.defaultValue);
this.stateValueSubjects[getBehavior.valueId] = stateValueSubject;
this.context.values[getBehavior.valueId] = getValueSettings.defaultValue;
stateValueSubject.subscribe((value) => {
this.onStateValueChanged(getBehavior.valueId, value);
});
const valueGetter =
ValueGetter.fromSettings(this.ctx, getValueSettings, getBehavior.valueType, {
next: (val) => {this.onValue(getBehavior.id, val);},
error: (err) => {
const message = parseError(this.ctx, err);
this._onError(message);
}
});
this.valueGetters.push(valueGetter);
this.valueActions.push(valueGetter);
} else if (behavior.type === IotSvgBehaviorType.action) {
const setBehavior = behavior as IotSvgBehaviorAction;
let setValueSettings: SetValueSettings = this.settings[setBehavior.id];
setValueSettings = {...setValueSettings, actionLabel: setBehavior.name};
const valueSetter = ValueSetter.fromSettings<any>(this.ctx, setValueSettings);
this.valueSetters[setBehavior.id] = valueSetter;
this.valueActions.push(valueSetter);
} else if (behavior.type === IotSvgBehaviorType.widgetAction) {
// TODO:
}
}
this.renderState();
if (this.valueGetters.length) {
const getValueObservables: Array<Observable<any>> = [];
this.valueGetters.forEach(valueGetter => {
getValueObservables.push(valueGetter.getValue());
});
this.loadingSubject.next(true);
forkJoin(getValueObservables).subscribe(
{
next: () => {
this.loadingSubject.next(false);
},
error: () => {
this.loadingSubject.next(false);
}
}
);
}
}
private callAction(event: Event, behaviorId: string, value?: any, observer?: Partial<Observer<void>>) {
const behavior = this.metadata.behavior.find(b => b.id === behaviorId);
if (behavior) {
if (behavior.type === IotSvgBehaviorType.action) {
const valueSetter = this.valueSetters[behaviorId];
if (valueSetter) {
this.loadingSubject.next(true);
valueSetter.setValue(value).subscribe(
{
next: () => {
if (observer?.next) {
observer.next();
}
this.loadingSubject.next(false);
},
error: (err) => {
this.loadingSubject.next(false);
if (observer?.error) {
observer.error(err);
}
const message = parseError(this.ctx, err);
this._onError(message);
}
}
);
}
} else if (behavior.type === IotSvgBehaviorType.widgetAction) {
const widgetAction: WidgetAction = this.settings[behavior.id];
this.ctx.actionsApi.onWidgetAction(event, widgetAction);
}
}
}
private resize() {
let scale: number;
if (this.targetWidth < this.targetHeight) {
scale = this.targetWidth / this.box.width;
} else {
scale = this.targetHeight / this.box.height;
}
this.svgShape.node.style.transform = `scale(${scale})`;
}
private onValue(id: string, value: any) {
const valueBehavior = this.metadata.behavior.find(b => b.id === id) as IotSvgBehaviorValue;
value = this.normalizeValue(value, valueBehavior.valueType);
const valueId = valueBehavior.valueId;
this.setValue(valueId, value);
}
private setValue(valueId: string, value: any) {
const stateValueSubject = this.stateValueSubjects[valueId];
if (stateValueSubject && stateValueSubject.value !== value) {
stateValueSubject.next(value);
}
}
private onStateValueChanged(id: string, value: any) {
if (this.context.values[id] !== value) {
this.context.values[id] = value;
this.renderState();
}
}
private renderState(): void {
this.metadata.stateRender(this.svgShape, this.context);
for (const tag of this.metadata.tags) {
const elements = this.svgShape.find(`[tb\\:tag="${tag.tag}"]`);
elements.forEach(element => {
tag.stateRender(element, this.context);
});
}
}
private normalizeValue(value: any, type: ValueType): any {
if (isUndefinedOrNull(value)) {
switch (type) {
case ValueType.STRING:
return '';
case ValueType.INTEGER:
case ValueType.DOUBLE:
return 0;
case ValueType.BOOLEAN:
return false;
case ValueType.JSON:
return {};
}
} else {
return value;
}
}
private setElementText(e: Element | Element[], text: string) {
this.elements(e).forEach(element => {
let textElement: Text;
if (element.type === 'text') {
const children = element.children();
if (children.length && children[0].type === 'tspan') {
textElement = children[0] as Text;
} else {
textElement = element as Text;
}
} else if (element.type === 'tspan') {
textElement = element as Text;
}
if (textElement) {
textElement.text(text);
}
});
}
private setElementFont(e: Element | Element[], font: Font, color: string) {
this.elements(e).forEach(element => {
if (element.type === 'text') {
const textElement = element as Text;
if (font) {
textElement.font({
family: font.family,
size: (isDefinedAndNotNull(font.size) && isDefinedAndNotNull(font.sizeUnit)) ?
font.size + font.sizeUnit : null,
weight: font.weight,
style: font.style
});
}
if (color) {
textElement.fill(color);
}
}
});
}
private animate(element: Element, duration: number): Runner {
element.timeline().finish();
return element.animate(duration, 0, 'now');
}
private disableElement(e: Element | Element[]) {
this.elements(e).forEach(element => {
element.attr({'pointer-events': 'none'});
});
}
private enableElement(e: Element | Element[]) {
this.elements(e).forEach(element => {
element.attr({'pointer-events': null});
});
}
private elements(element: Element | Element[]): Element[] {
return Array.isArray(element) ? element : [element];
}
private getProperty(id: string): IotSvgProperty {
return this.metadata.properties.find(p => p.id === id);
}
private getPropertyValue(id: string): any {
const property = this.getProperty(id);
if (property) {
const value = this.settings[id];
if (isDefinedAndNotNull(value)) {
if (property.type === 'color-settings') {
return ColorProcessor.fromSettings(value);
}
return value;
} else {
switch (property.type) {
case 'string':
return '';
case 'number':
return 0;
case 'color':
return '#000';
case 'color-settings':
return constantColor('#000');
}
}
} else {
return '';
}
}
}

6
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -92,7 +92,7 @@ import { PieChartWidgetComponent } from '@home/components/widget/lib/chart/pie-c
import { BarChartWidgetComponent } from '@home/components/widget/lib/chart/bar-chart-widget.component';
import { PolarAreaWidgetComponent } from '@home/components/widget/lib/chart/polar-area-widget.component';
import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/radar-chart-widget.component';
import { ScadaTestWidgetComponent } from '@home/components/widget/lib/scada/scada-test-widget.component';
import { IotSvgWidgetComponent } from '@home/components/widget/lib/svg/iot-svg-widget.component';
@NgModule({
declarations:
@ -152,7 +152,7 @@ import { ScadaTestWidgetComponent } from '@home/components/widget/lib/scada/scad
BarChartWidgetComponent,
PolarAreaWidgetComponent,
RadarChartWidgetComponent,
ScadaTestWidgetComponent
IotSvgWidgetComponent
],
imports: [
CommonModule,
@ -215,7 +215,7 @@ import { ScadaTestWidgetComponent } from '@home/components/widget/lib/scada/scad
BarChartWidgetComponent,
PolarAreaWidgetComponent,
RadarChartWidgetComponent,
ScadaTestWidgetComponent
IotSvgWidgetComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }

35
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -39,7 +39,7 @@ import {
} from '@angular/core';
import { DashboardWidget } from '@home/models/dashboard-component.models';
import {
Widget,
Widget, WidgetAction,
WidgetActionDescriptor,
widgetActionSources,
WidgetActionType,
@ -57,7 +57,7 @@ import { WidgetService } from '@core/http/widget.service';
import { UtilsService } from '@core/services/utils.service';
import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs';
import {
deepClone,
deepClone, guid,
insertVariable,
isDefined,
isNotEmptyStr,
@ -254,6 +254,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
actionDescriptorsBySourceId,
getActionDescriptors: this.getActionDescriptors.bind(this),
handleWidgetAction: this.handleWidgetAction.bind(this),
onWidgetAction: this.onWidgetAction.bind(this),
elementClick: this.elementClick.bind(this),
cardClick: this.cardClick.bind(this),
click: this.click.bind(this),
@ -1039,7 +1040,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
return result;
}
private handleWidgetAction($event: Event, descriptor: WidgetActionDescriptor,
private handleWidgetAction($event: Event, descriptor: WidgetAction,
entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string): void {
const type = descriptor.type;
const targetEntityParamName = descriptor.stateEntityParamName;
@ -1112,7 +1113,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
const customHtml = descriptor.customHtml;
const customCss = descriptor.customCss;
const customResources = descriptor.customResources;
const actionNamespace = `custom-action-pretty-${descriptor.name.toLowerCase()}`;
const actionNamespace = `custom-action-pretty-${guid()}`;
let htmlTemplate = '';
if (isDefined(customHtml) && customHtml.length > 0) {
htmlTemplate = customHtml;
@ -1420,13 +1421,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
const idsList = descriptors.map(descriptor => `#${descriptor.name}`).join(',');
const targetElement = $(elementClicked).closest(idsList, this.widgetContext.$container[0]);
if (targetElement.length && targetElement[0].id) {
$event.stopPropagation();
const descriptor = descriptors.find(descriptorInfo => descriptorInfo.name === targetElement[0].id);
const entityInfo = this.getActiveEntityInfo();
const entityId = entityInfo ? entityInfo.entityId : null;
const entityName = entityInfo ? entityInfo.entityName : null;
const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null;
this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel);
this.onWidgetAction($event, descriptor);
}
}
}
@ -1442,18 +1438,23 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
private onClick($event: Event, sourceId: string) {
const descriptors = this.getActionDescriptors(sourceId);
if (descriptors.length) {
this.onWidgetAction($event, descriptors[0]);
}
}
private onWidgetAction($event: Event, action: WidgetAction) {
if ($event) {
$event.stopPropagation();
const descriptor = descriptors[0];
const entityInfo = this.getActiveEntityInfo();
const entityId = entityInfo ? entityInfo.entityId : null;
const entityName = entityInfo ? entityInfo.entityName : null;
const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null;
this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel);
}
const entityInfo = this.getActiveEntityInfo();
const entityId = entityInfo ? entityInfo.entityId : null;
const entityName = entityInfo ? entityInfo.entityName : null;
const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null;
this.handleWidgetAction($event, action, entityId, entityName, null, entityLabel);
}
private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array<WidgetResource>,
actionDescriptor: WidgetActionDescriptor): Observable<any> {
actionDescriptor: WidgetAction): Observable<any> {
const resourceTasks: Observable<string>[] = [];
const modulesTasks: Observable<ModulesWithFactories | string>[] = [];

382
ui-ngx/src/assets/widget/scada/drawing.svg → ui-ngx/src/assets/widget/svg/drawing.svg

@ -1,341 +1,115 @@
<svg width="100" height="100" version="1.1" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg">
<tb:metadata>{
"title": "My first SCADA Object",
"stateValues": {
"level": {
"initialValue": 0
},
"disabled": {
"initialValue": false
<tb:metadata><![CDATA[{
"title": "My first IoT SVG Object",
"stateRenderFunction": "var showMinMaxLevel = ctx.properties.showMinMaxLevel;\nvar minLevelElement = ctx.tags.minLevel[0];\nvar maxLevelElement = ctx.tags.maxLevel[0];\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nif (showMinMaxLevel) {\n \n var minMaxLevelFont = ctx.properties.minMaxLevelFont;\n var minMaxLevelColor = ctx.properties.minMaxLevelColor;\n \n ctx.api.text(minLevelElement, minLevel);\n ctx.api.text(maxLevelElement, maxLevel);\n \n ctx.api.font(minLevelElement, minMaxLevelFont, minMaxLevelColor);\n ctx.api.font(maxLevelElement, minMaxLevelFont, minMaxLevelColor);\n \n} else {\n minLevelElement.hide();\n maxLevelElement.hide();\n}\n\nvar disabled = ctx.values.disabled;\nvar on = ctx.values.on;\nvar level = ctx.values.level;\n\nvar onButton = ctx.tags.onButton;\nvar offButton = ctx.tags.offButton;\nvar levelUpButton = ctx.tags.levelUpButton;\nvar levelDownButton = ctx.tags.levelDownButton;\n\nvar onButtonEnabled = !disabled && !on;\nvar offButtonEnabled = !disabled && on;\nvar levelUpEnabled = !disabled && level < maxLevel;\nvar levelDownEnabled = !disabled && level > minLevel;\n\nif (onButtonEnabled) {\n ctx.api.enable(onButton);\n onButton[0].findOne('rect').attr({fill: '#12ed19'});\n} else {\n ctx.api.disable(onButton);\n onButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (offButtonEnabled) {\n ctx.api.enable(offButton);\n offButton[0].findOne('rect').attr({fill: '#ed121f'});\n} else {\n ctx.api.disable(offButton);\n offButton[0].findOne('rect').attr({fill: '#777'});\n} \n\n\nif (levelUpEnabled) {\n ctx.api.enable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (levelDownEnabled) {\n ctx.api.enable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#777'});\n}",
"tags": [
{
"tag": "level",
"stateRenderFunction": "var level = ctx.values.level; \nvar disabled = ctx.values.disabled;\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar height = (level - minLevel) / (maxLevel - minLevel);\nheight = Math.max(0, Math.min(1, height))*80; \n\nctx.api.animate(element, 200).attr({height: height});\n\nvar fill;\nif (disabled) {\n fill = ctx.properties.disabledLevelBackground;\n} else {\n var colorProcessor = ctx.properties.levelBackground;\n colorProcessor.update(level);\n fill = colorProcessor.color;\n}\n\nelement.attr({fill: fill});"
},
"on": {
"initialValue": false,
"callBehavior": [
{
"behaviorId": "onUpdateState",
"conditionFunction": "return value === true;"
},
{
"behaviorId": "offUpdateState",
"conditionFunction": "return value === false;"
}
]
}
},
"actions": {
"onButton": {
"trigger": "click",
"actionType": "updateValue",
"updateValueId": "on",
"updateValueType": "constant",
"updateValueConstant": true
{
"tag": "levelTitle",
"stateRenderFunction": "var showLevelTitle = ctx.properties.showLevelTitle;\n\nif (showLevelTitle) {\n var levelTitle = ctx.properties.levelTitle;\n var levelTitleFont = ctx.properties.levelTitleFont;\n var levelTitleColor = ctx.properties.levelTitleColor;\n \n ctx.api.text(element, levelTitle);\n ctx.api.font(element, levelTitleFont, levelTitleColor);\n \n} else {\n element.hide();\n}"
},
"offButton": {
"trigger": "click",
"actionType": "updateValue",
"updateValueId": "on",
"updateValueType": "constant",
"updateValueConstant": false
}
},
"states": {
"initialState": {
"initial": true,
"state": [
{
"tag": "levelTitle",
"show": {
"type": "property",
"propertyId": "showLevelTitle"
},
"text": {
"content": {
"type": "property",
"propertyId": "levelTitle"
},
"font": {
"type": "property",
"propertyId": "levelTitleFont"
},
"color": {
"type": "property",
"propertyId": "levelTitleColor"
}
}
},
{
"tag": "level",
"inputValue": "level",
"attributes": [
{
"name": "fill",
"value": {
"type": "property",
"propertyId": "levelBackground"
}
}
]
},
{
"tag": "levelValueBackground",
"show": {
"type": "property",
"propertyId": "showValue"
}
},
{
"tag": "levelValue",
"show": {
"type": "property",
"propertyId": "showValue"
},
"text": {
"font": {
"type": "property",
"propertyId": "valueFont"
},
"color": {
"type": "property",
"propertyId": "valueColor"
}
}
},
{
"tag": "minLevel",
"show": {
"type": "property",
"propertyId": "showMinMaxLevel"
},
"text": {
"content": {
"type": "property",
"propertyId": "minLevel"
},
"font": {
"type": "property",
"propertyId": "minMaxLevelFont"
},
"color": {
"type": "property",
"propertyId": "minMaxLevelColor"
}
}
},
{
"tag": "maxLevel",
"show": {
"type": "property",
"propertyId": "showMinMaxLevel"
},
"text": {
"content": {
"type": "property",
"propertyId": "maxLevel"
},
"font": {
"type": "property",
"propertyId": "minMaxLevelFont"
},
"color": {
"type": "property",
"propertyId": "minMaxLevelColor"
}
}
}
]
{
"tag": "levelValue",
"stateRenderFunction": "var showValue = ctx.properties.showValue;\n\nif (showValue) {\n var valueFont = ctx.properties.valueFont;\n var valueColor = ctx.properties.valueColor;\n var level = ctx.values.level; \n \n var levelText = ctx.api.formatValue(level, ctx.properties.valueDecimals, ctx.properties.valueUnits, false);\n\n ctx.api.font(element, valueFont, valueColor);\n ctx.api.text(element, levelText);\n\n} else {\n element.hide();\n ctx.tags.levelValueBackground[0].hide();\n}",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'levelValueClick');"
}
}
},
"updateLevelState": {
"triggerValues": ["level"],
"state": [
{
"tag": "level",
"inputValue": "level",
"animate": 200,
"attributes": [
{
"name": "height",
"value": {
"type": "function",
"valueConvertFunction": "var level = (value - ${minLevel}) / (${maxLevel} - ${minLevel});\nlevel = Math.max(0, Math.min(1, level));\nreturn level*80;"
}
}
]
},
{
"tag": "levelValue",
"inputValue": "level",
"text": {
"content": {
"type": "valueFormat",
"units": {
"type": "property",
"propertyId": "valueUnits"
},
"decimals": {
"type": "property",
"propertyId": "valueDecimals"
}
}
}
}
]
{
"tag": "fan",
"stateRenderFunction": "var on = ctx.values.on;\nif (on) {\n element.addClass('fan-rotate');\n} else {\n element.removeClass('fan-rotate');\n}\n"
},
"updateLevelBackgroundState": {
"triggerValues": ["disabled", "level"],
"conditionFunction": "return values.disabled === false;",
"state": [
{
"tag": "level",
"inputValue": "level",
"attributes": [
{
"name": "fill",
"value": {
"type": "property",
"propertyId": "levelBackground"
}
}
]
}
]
{
"tag": "onButton",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'onUpdateState');"
}
}
},
"disabledState": {
"triggerValues": ["disabled"],
"conditionFunction": "return values.disabled === true;",
"state": [
{
"tag": "level",
"attributes": [
{
"name": "fill",
"value": {
"type": "property",
"propertyId": "disabledLevelBackground"
}
}
]
}
]
{
"tag": "offButton",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'offUpdateState');"
}
}
},
"onState": {
"triggerValues": ["on"],
"conditionFunction": "return values.on === true;",
"state": [
{
"tag": "fan",
"addClass": "fan-rotate"
},
{
"tag": "onButtonBackground",
"attributes": [
{
"name": "fill",
"value": {
"type": "constant",
"constantValue": "#777"
}
}
]
},
{
"tag": "offButtonBackground",
"attributes": [
{
"name": "fill",
"value": {
"type": "constant",
"constantValue": "#ed121f"
}
}
]
}
]
{
"tag": "levelUpButton",
"actions": {
"click": {
"actionFunction": "var level = ctx.values.level; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar newLevel = Math.min(maxLevel, level + 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});"
}
}
},
"offState": {
"triggerValues": ["on"],
"conditionFunction": "return values.on === false;",
"state": [
{
"tag": "fan",
"removeClass": "fan-rotate"
},
{
"tag": "onButtonBackground",
"attributes": [
{
"name": "fill",
"value": {
"type": "constant",
"constantValue": "#12ed19"
}
}
]
},
{
"tag": "offButtonBackground",
"attributes": [
{
"name": "fill",
"value": {
"type": "constant",
"constantValue": "#777"
}
}
]
}
]
{
"tag": "levelDownButton",
"actions": {
"click": {
"actionFunction": "var level = ctx.values.level; \nvar minLevel = ctx.properties.minLevel;\n\nvar newLevel = Math.max(minLevel, level - 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});"
}
}
}
},
],
"behavior": [
{
"id": "levelState",
"name": "Level",
"type": "getValue",
"type": "value",
"valueType": "DOUBLE",
"defaultValue": 0,
"onUpdate": [
{
"updateValue": "level"
}
]
"valueId": "level"
},
{
"id": "disabledState",
"name": "Disabled state",
"type": "getValue",
"type": "value",
"valueType": "BOOLEAN",
"defaultValue": false,
"onUpdate": [
{
"updateValue": "disabled"
}
]
"valueId": "disabled"
},
{
"id": "onState",
"name": "On/Off state",
"type": "getValue",
"type": "value",
"valueType": "BOOLEAN",
"defaultValue": false,
"onUpdate": [
{
"updateValue": "on"
}
]
"valueId": "on"
},
{
"id": "onUpdateState",
"name": "Power On",
"type": "setValue",
"type": "action",
"valueType": "BOOLEAN",
"valueToDataType": "CONSTANT",
"constantValue": true
},
{
"id": "offUpdateState",
"name": "Power Off",
"type": "setValue",
"type": "action",
"valueType": "BOOLEAN",
"valueToDataType": "CONSTANT",
"constantValue": false
},
{
"id": "levelUpdateState",
"name": "Update level",
"type": "action",
"valueType": "DOUBLE",
"valueToDataType": "VALUE",
"constantValue": 0
},
{
"id": "levelValueClick",
"name": "On level value click",
"type": "widgetAction"
}
],
"properties": [
@ -489,7 +263,7 @@
"default": "#ccc"
}
]
}</tb:metadata>
}]]></tb:metadata>
<style>.fan-rotate {
animation: rotate 2s linear infinite;
transform-origin: 71.894px 39.787px;
@ -518,4 +292,12 @@
<rect x="74.367" y="60.311" width="14.263" height="7.4261" rx="1.5" fill="#ed121f" stroke="#000" tb:tag="offButtonBackground"/>
<text x="81.365944" y="64.518349" dominant-baseline="middle" fill="#000000" font-family="Roboto" font-size="4.4461px" stroke-width=".74101" text-anchor="middle" xml:space="preserve"><tspan stroke-width=".74101">Off</tspan></text>
</g>
<g stroke="#000" stroke-miterlimit="1" tb:tag="levelUpButton">
<rect x="34.97" y="39.377" width="9.5005" height="12.584" rx=".75" fill="none"/>
<path transform="matrix(.4753 -.2938 .2938 .4753 4.8821 31.04)" d="m41.544 60.24-7.893-4.9762 8.256-4.3474-0.18153 4.6618z" fill="#12ed19"/>
</g>
<g stroke="#000" stroke-miterlimit="1" tb:tag="levelDownButton">
<rect x="34.97" y="59.039" width="9.5005" height="12.584" rx=".75" fill="none"/>
<path transform="matrix(-.47514 .29405 -.29405 -.47514 74.568 79.771)" d="m41.544 60.24-7.893-4.9762 8.256-4.3474-0.18153 4.6618z" fill="#12ed19"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Loading…
Cancel
Save