UI: Scada metadata and configuration improvements.
This commit is contained in:
parent
eb50048c50
commit
f01f99e738
@ -15,13 +15,23 @@
|
||||
///
|
||||
|
||||
import { ValueType } from '@shared/models/constants';
|
||||
import { Box, Element, Runner, Svg, SVG, Timeline } from '@svgdotjs/svg.js';
|
||||
import { Box, Element, Runner, Svg, SVG, Timeline, Text } from '@svgdotjs/svg.js';
|
||||
import { DataToValueType, GetValueAction, GetValueSettings } from '@shared/models/action-widget-settings.models';
|
||||
import { insertVariable, isDefinedAndNotNull, isNumber, isNumeric, isUndefinedOrNull, mergeDeep } from '@core/utils';
|
||||
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 } from '@home/components/widget/lib/action/action-widget.models';
|
||||
import { WidgetContext } from '@home/models/widget-component.models';
|
||||
import { Font } from '@shared/models/widget-settings.models';
|
||||
|
||||
export type ValueMatcherType = 'any' | 'constant' | 'range';
|
||||
|
||||
@ -31,29 +41,85 @@ export interface ValueMatcher {
|
||||
range?: {from: number; to: number};
|
||||
}
|
||||
|
||||
export type ScadaObjectAttributeValueType = 'input' | 'property';
|
||||
const valueMatches = (matcher: ValueMatcher, val: any): boolean => {
|
||||
switch (matcher.type) {
|
||||
case 'any':
|
||||
return true;
|
||||
case 'constant':
|
||||
return matcher.value === val;
|
||||
case 'range':
|
||||
if (isDefinedAndNotNull(val) && isNumeric(val)) {
|
||||
const num = Number(val);
|
||||
const range = matcher.range;
|
||||
return ((!isNumber(range.from) || num >= range.from) && (!isNumber(range.to) || num < range.to));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export interface ScadaObjectAttributeValue {
|
||||
type: ScadaObjectAttributeValueType;
|
||||
propertyId?: string;
|
||||
export type ScadaObjectValueType = 'input' | 'constant' | 'property' | 'function' | 'valueFormat';
|
||||
|
||||
export interface ScadaObjectValueBase {
|
||||
type: ScadaObjectValueType;
|
||||
}
|
||||
|
||||
export interface ScadaObjectAttributeState {
|
||||
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: ScadaObjectAttributeValue;
|
||||
value: ScadaObjectValue;
|
||||
}
|
||||
|
||||
export interface ScadaObjectText {
|
||||
content?: ScadaObjectValue;
|
||||
font?: ScadaObjectValue;
|
||||
color?: ScadaObjectValue;
|
||||
}
|
||||
|
||||
export interface ScadaObjectElementState {
|
||||
tag: string;
|
||||
show?: ScadaObjectValue;
|
||||
text?: ScadaObjectText;
|
||||
attributes?: ScadaObjectAttribute[];
|
||||
animate?: number;
|
||||
}
|
||||
|
||||
export interface ScadaObjectState {
|
||||
tag: string;
|
||||
attributes: ScadaObjectAttributeState[];
|
||||
animate?: number;
|
||||
initial?: boolean;
|
||||
value?: any;
|
||||
state: ScadaObjectElementState[];
|
||||
}
|
||||
|
||||
export interface ScadaObjectUpdateState {
|
||||
matcher: ValueMatcher;
|
||||
state: ScadaObjectState[];
|
||||
stateId: string;
|
||||
}
|
||||
|
||||
const filterUpdateStates = (states: ScadaObjectUpdateState[], val: any): ScadaObjectUpdateState[] =>
|
||||
states.filter(s => valueMatches(s.matcher, val));
|
||||
|
||||
export enum ScadaObjectBehaviorType {
|
||||
setValue = 'setValue',
|
||||
getValue = 'getValue'
|
||||
@ -77,32 +143,38 @@ export interface ScadaObjectBehaviorSet extends ScadaObjectBehaviorBase {
|
||||
|
||||
export type ScadaObjectBehavior = ScadaObjectBehaviorGet | ScadaObjectBehaviorSet;
|
||||
|
||||
export type ScadaObjectPropertyType = 'string' | 'number' | 'color';
|
||||
export type ScadaObjectPropertyType = 'string' | 'number' | 'color' | 'font' | 'units' | 'switch';
|
||||
|
||||
export interface ScadaObjectPropertyBase {
|
||||
id: string;
|
||||
name: string;
|
||||
type: ScadaObjectPropertyType;
|
||||
default: any;
|
||||
required?: boolean;
|
||||
subLabel?: string;
|
||||
divider?: boolean;
|
||||
fieldSuffix?: string;
|
||||
disableOnProperty?: string;
|
||||
}
|
||||
|
||||
export interface ScadaObjectNumberProperty extends ScadaObjectPropertyBase {
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
export type ScadaObjectProperty = ScadaObjectPropertyBase & ScadaObjectNumberProperty;
|
||||
|
||||
export interface ScadaObjectMetadata {
|
||||
title: string;
|
||||
initial: ScadaObjectState[];
|
||||
states: {[id: string]: ScadaObjectState};
|
||||
behavior: ScadaObjectBehavior[];
|
||||
properties: ScadaObjectProperty[];
|
||||
}
|
||||
|
||||
export const emptyMetadata: ScadaObjectMetadata = {
|
||||
title: '',
|
||||
initial: [],
|
||||
states: {},
|
||||
behavior: [],
|
||||
properties: []
|
||||
};
|
||||
@ -126,6 +198,7 @@ const parseScadaObjectMetadataFromDom = (svgDoc: Document): ScadaObjectMetadata
|
||||
return emptyMetadata;
|
||||
}
|
||||
} catch (_e) {
|
||||
console.error(_e);
|
||||
return emptyMetadata;
|
||||
}
|
||||
};
|
||||
@ -201,8 +274,9 @@ export class ScadaObject {
|
||||
this.metadata = parseScadaObjectMetadataFromDom(doc);
|
||||
const defaults = defaultScadaObjectSettings(this.metadata);
|
||||
this.settings = mergeDeep<ScadaObjectSettings>({}, defaults, this.inputSettings || {});
|
||||
this.prepareMetadata();
|
||||
this.prepareSvgShape(doc);
|
||||
this.prepareStates();
|
||||
this.initStates();
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -228,17 +302,58 @@ export class ScadaObject {
|
||||
}
|
||||
}
|
||||
|
||||
private prepareMetadata() {
|
||||
for (const stateId of Object.keys(this.metadata.states)) {
|
||||
const state = this.metadata.states[stateId];
|
||||
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();
|
||||
}
|
||||
let svgContent = doc.documentElement.innerHTML;
|
||||
for (const property of this.metadata.properties) {
|
||||
const value = this.settings[property.id] || '';
|
||||
svgContent = insertVariable(svgContent, property.id, value);
|
||||
}
|
||||
this.svgShape = SVG().svg(svgContent);
|
||||
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();
|
||||
@ -251,7 +366,7 @@ export class ScadaObject {
|
||||
}
|
||||
}
|
||||
|
||||
private prepareStates() {
|
||||
private initStates() {
|
||||
for (const behavior of this.metadata.behavior) {
|
||||
if (behavior.type === ScadaObjectBehaviorType.getValue) {
|
||||
const getBehavior = behavior as ScadaObjectBehaviorGet;
|
||||
@ -266,8 +381,9 @@ export class ScadaObject {
|
||||
this.valueActions.push(valueGetter);
|
||||
}
|
||||
}
|
||||
if (this.metadata.initial) {
|
||||
this.updateState(this.metadata.initial);
|
||||
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>> = [];
|
||||
@ -301,23 +417,61 @@ export class ScadaObject {
|
||||
private onValue(id: string, value: any) {
|
||||
const getBehavior = this.metadata.behavior.find(b => b.id === id) as ScadaObjectBehaviorGet;
|
||||
value = this.normalizeValue(value, getBehavior.valueType);
|
||||
const updateStates = this.filterUpdateStates(getBehavior.onUpdate, value);
|
||||
const updateStates = filterUpdateStates(getBehavior.onUpdate, value);
|
||||
if (this.animationTimeline) {
|
||||
this.animationTimeline.finish();
|
||||
}
|
||||
for (const updateState of updateStates) {
|
||||
this.updateState(updateState.state, value);
|
||||
const state = this.metadata.states[updateState.stateId];
|
||||
this.updateState(state, value);
|
||||
}
|
||||
}
|
||||
|
||||
private updateState(state: ScadaObjectState[], value?: any) {
|
||||
for (const stateEntry of state) {
|
||||
const tag = stateEntry.tag;
|
||||
const elements = this.svgShape.find(`[tb\\:tag="${tag}"]`);
|
||||
const attrs = this.computeAttributes(stateEntry.attributes, value);
|
||||
elements.forEach(e => {
|
||||
this.setElementAttributes(e, attrs, stateEntry.animate);
|
||||
});
|
||||
private updateState(state: ScadaObjectState, value?: any) {
|
||||
if (state) {
|
||||
for (const elementState of state.state) {
|
||||
const tag = elementState.tag;
|
||||
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.attributes) {
|
||||
const attrs = this.computeAttributes(elementState.attributes, value);
|
||||
elements.forEach(e => {
|
||||
this.setElementAttributes(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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -339,11 +493,11 @@ export class ScadaObject {
|
||||
}
|
||||
}
|
||||
|
||||
private computeAttributes(attributes: ScadaObjectAttributeState[], value: any): {[attr: string]: any} {
|
||||
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.getAttributeValue(attribute, value);
|
||||
res[attr] = this.computeValue(attribute.value, value);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@ -356,6 +510,41 @@ export class ScadaObject {
|
||||
}
|
||||
}
|
||||
|
||||
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(element: Element, duration: number): Runner {
|
||||
if (!this.animationTimeline) {
|
||||
this.animationTimeline = new Timeline();
|
||||
@ -364,35 +553,55 @@ export class ScadaObject {
|
||||
return element.animate(duration, 0, 'now');
|
||||
}
|
||||
|
||||
private getAttributeValue(attribute: ScadaObjectAttributeState, value: any): any {
|
||||
if (attribute.value.type === 'input') {
|
||||
return value;
|
||||
} else if (attribute.value.type === 'property') {
|
||||
const id = attribute.value.propertyId;
|
||||
return this.settings[id] || '';
|
||||
private computeValue(objectValue: ScadaObjectValue, value: any): any {
|
||||
if (objectValue) {
|
||||
switch (objectValue.type) {
|
||||
case 'input':
|
||||
return value;
|
||||
case 'constant':
|
||||
return objectValue.constantValue;
|
||||
case 'property':
|
||||
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 filterUpdateStates(states: ScadaObjectUpdateState[], val: any): ScadaObjectUpdateState[] {
|
||||
return states.filter(s => this.valueMatches(s.matcher, val));
|
||||
}
|
||||
|
||||
private valueMatches(matcher: ValueMatcher, val: any): boolean {
|
||||
switch (matcher.type) {
|
||||
case 'any':
|
||||
return true;
|
||||
case 'constant':
|
||||
return matcher.value === val;
|
||||
case 'range':
|
||||
if (isDefinedAndNotNull(val) && isNumeric(val)) {
|
||||
const num = Number(val);
|
||||
const range = matcher.range;
|
||||
return ((!isNumber(range.from) || num >= range.from) && (!isNumber(range.to) || num < range.to));
|
||||
} else {
|
||||
return false;
|
||||
private getPropertyValue(id: string): any {
|
||||
const property = this.metadata.properties.find(p => p.id === id);
|
||||
if (property) {
|
||||
const value = this.settings[id];
|
||||
if (isDefinedAndNotNull(value)) {
|
||||
return value;
|
||||
} else {
|
||||
switch (property.type) {
|
||||
case 'string':
|
||||
return '';
|
||||
case 'number':
|
||||
return 0;
|
||||
case 'color':
|
||||
return '#000';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,17 +32,42 @@
|
||||
</tb-get-value-action-settings>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="metadata?.properties?.length" class="tb-form-panel">
|
||||
<div *ngFor="let property of metadata.properties" class="tb-form-row space-between">
|
||||
<div class="fixed-title-width">{{ property.name }}</div>
|
||||
<tb-color-input *ngIf="property.type === 'color'"
|
||||
asBoxInput
|
||||
colorClearButton
|
||||
formControlName="{{ property.id }}">
|
||||
</tb-color-input>
|
||||
<mat-form-field *ngIf="property.type === 'number'" appearance="outline" class="number" subscriptSizing="dynamic">
|
||||
<input matInput formControlName="{{ property.id }}" [min]="property.min" [max]="property.max" type="number" placeholder="{{ 'widget-config.set' | translate }}">
|
||||
</mat-form-field>
|
||||
<div *ngIf="propertyRows?.length" class="tb-form-panel">
|
||||
<div *ngFor="let propertyRow of propertyRows" class="tb-form-row space-between">
|
||||
<mat-slide-toggle *ngIf="propertyRow.switch" class="mat-slide fixed-title-width" formControlName="{{ propertyRow.switch.id }}">
|
||||
{{ propertyRow.label }}
|
||||
</mat-slide-toggle>
|
||||
<div *ngIf="!propertyRow.switch" class="fixed-title-width">{{ propertyRow.label }}</div>
|
||||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
|
||||
<ng-container *ngFor="let property of propertyRow.properties">
|
||||
<div *ngIf="property.subLabel" class="tb-small-label">{{ property.subLabel }}</div>
|
||||
<mat-form-field *ngIf="property.type === 'string'" appearance="outline" subscriptSizing="dynamic">
|
||||
<input matInput formControlName="{{ property.id }}" [required]="property.required" placeholder="{{ 'widget-config.set' | translate }}">
|
||||
<div matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix }}</div>
|
||||
</mat-form-field>
|
||||
<tb-color-input *ngIf="property.type === 'color'"
|
||||
[required]="property.required"
|
||||
asBoxInput
|
||||
colorClearButton
|
||||
formControlName="{{ property.id }}">
|
||||
</tb-color-input>
|
||||
<mat-form-field *ngIf="property.type === 'number'" appearance="outline" class="number" subscriptSizing="dynamic">
|
||||
<input matInput formControlName="{{ property.id }}" [required]="property.required"
|
||||
[min]="property.min" [max]="property.max" [step]="property.step"
|
||||
type="number" placeholder="{{ 'widget-config.set' | translate }}">
|
||||
<div matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix }}</div>
|
||||
</mat-form-field>
|
||||
<tb-font-settings *ngIf="property.type === 'font'"
|
||||
formControlName="{{ property.id }}"
|
||||
clearButton
|
||||
disabledLineHeight>
|
||||
</tb-font-settings>
|
||||
<tb-unit-input *ngIf="property.type === 'units'"
|
||||
[required]="property.required"
|
||||
formControlName="{{ property.id }}"></tb-unit-input>
|
||||
<mat-divider *ngIf="property.divider" vertical></mat-divider>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@ -14,12 +14,15 @@
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
||||
import {
|
||||
ControlValueAccessor,
|
||||
NG_VALIDATORS,
|
||||
NG_VALUE_ACCESSOR,
|
||||
UntypedFormBuilder,
|
||||
UntypedFormControl,
|
||||
UntypedFormGroup,
|
||||
Validator,
|
||||
ValidatorFn,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
@ -37,6 +40,11 @@ import { ValueType } from '@shared/models/constants';
|
||||
import { IAliasController } from '@core/api/widget-api.models';
|
||||
import { TargetDevice, widgetType } from '@shared/models/widget.models';
|
||||
import { isDefinedAndNotNull } from '@core/utils';
|
||||
import {
|
||||
ScadaPropertyRow,
|
||||
toPropertyRows
|
||||
} from '@home/components/widget/lib/settings/common/scada/scada-object-settings.models';
|
||||
import { merge, Observable, Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-scada-object-settings',
|
||||
@ -47,10 +55,15 @@ import { isDefinedAndNotNull } from '@core/utils';
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => ScadaObjectSettingsComponent),
|
||||
multi: true
|
||||
},
|
||||
{
|
||||
provide: NG_VALIDATORS,
|
||||
useExisting: forwardRef(() => ScadaObjectSettingsComponent),
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlValueAccessor {
|
||||
export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlValueAccessor, Validator {
|
||||
|
||||
ScadaObjectBehaviorType = ScadaObjectBehaviorType;
|
||||
|
||||
@ -73,13 +86,18 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV
|
||||
|
||||
private propagateChange = null;
|
||||
|
||||
private validatorTriggers: string[];
|
||||
private validatorSubscription: Subscription;
|
||||
|
||||
public scadaObjectSettingsFormGroup: UntypedFormGroup;
|
||||
|
||||
metadata: ScadaObjectMetadata;
|
||||
propertyRows: ScadaPropertyRow[];
|
||||
|
||||
constructor(protected store: Store<AppState>,
|
||||
private fb: UntypedFormBuilder,
|
||||
private http: HttpClient) {
|
||||
private http: HttpClient,
|
||||
private cd: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -114,6 +132,7 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV
|
||||
this.scadaObjectSettingsFormGroup.disable({emitEvent: false});
|
||||
} else {
|
||||
this.scadaObjectSettingsFormGroup.enable({emitEvent: false});
|
||||
this.updateValidators();
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,10 +141,25 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV
|
||||
this.setupValue();
|
||||
}
|
||||
|
||||
validate(c: UntypedFormControl) {
|
||||
const valid = this.scadaObjectSettingsFormGroup.valid;
|
||||
return valid ? null : {
|
||||
scadaObject: {
|
||||
valid: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private loadMetadata() {
|
||||
if (this.validatorSubscription) {
|
||||
this.validatorSubscription.unsubscribe();
|
||||
this.validatorSubscription = null;
|
||||
}
|
||||
this.validatorTriggers = [];
|
||||
this.http.get(this.svgPath, {responseType: 'text'}).subscribe(
|
||||
(svgContent) => {
|
||||
this.metadata = parseScadaObjectMetadataFromContent(svgContent);
|
||||
this.propertyRows = toPropertyRows(this.metadata.properties);
|
||||
for (const control of Object.keys(this.scadaObjectSettingsFormGroup.controls)) {
|
||||
this.scadaObjectSettingsFormGroup.removeControl(control, {emitEvent: false});
|
||||
}
|
||||
@ -133,7 +167,15 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV
|
||||
this.scadaObjectSettingsFormGroup.addControl(behaviour.id, this.fb.control(null, []), {emitEvent: false});
|
||||
}
|
||||
for (const property of this.metadata.properties) {
|
||||
if (property.disableOnProperty) {
|
||||
if (!this.validatorTriggers.includes(property.disableOnProperty)) {
|
||||
this.validatorTriggers.push(property.disableOnProperty);
|
||||
}
|
||||
}
|
||||
const validators: ValidatorFn[] = [];
|
||||
if (property.required) {
|
||||
validators.push(Validators.required);
|
||||
}
|
||||
if (property.type === 'number') {
|
||||
if (isDefinedAndNotNull(property.min)) {
|
||||
validators.push(Validators.min(property.min));
|
||||
@ -144,11 +186,37 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV
|
||||
}
|
||||
this.scadaObjectSettingsFormGroup.addControl(property.id, this.fb.control(null, validators), {emitEvent: false});
|
||||
}
|
||||
if (this.validatorTriggers.length) {
|
||||
const observables: Observable<any>[] = [];
|
||||
for (const trigger of this.validatorTriggers) {
|
||||
observables.push(this.scadaObjectSettingsFormGroup.get(trigger).valueChanges);
|
||||
}
|
||||
this.validatorSubscription = merge(...observables).subscribe(() => {
|
||||
this.updateValidators();
|
||||
});
|
||||
}
|
||||
this.setupValue();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private updateValidators() {
|
||||
for (const trigger of this.validatorTriggers) {
|
||||
const value: boolean = this.scadaObjectSettingsFormGroup.get(trigger).value;
|
||||
this.metadata.properties.filter(p => p.disableOnProperty === trigger).forEach(
|
||||
(p) => {
|
||||
const control = this.scadaObjectSettingsFormGroup.get(p.id);
|
||||
if (value) {
|
||||
control.enable({emitEvent: false});
|
||||
} else {
|
||||
control.disable({emitEvent: false});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private setupValue() {
|
||||
if (this.metadata) {
|
||||
const defaults = defaultScadaObjectSettings(this.metadata);
|
||||
@ -156,6 +224,7 @@ export class ScadaObjectSettingsComponent implements OnInit, OnChanges, ControlV
|
||||
this.scadaObjectSettingsFormGroup.patchValue(
|
||||
this.modelValue, {emitEvent: false}
|
||||
);
|
||||
this.setDisabledState(this.disabled);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,43 @@
|
||||
///
|
||||
/// 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 { ScadaObjectProperty } from '@home/components/widget/lib/scada/scada.models';
|
||||
|
||||
export interface ScadaPropertyRow {
|
||||
label: string;
|
||||
properties: ScadaObjectProperty[];
|
||||
switch?: ScadaObjectProperty;
|
||||
}
|
||||
|
||||
export const toPropertyRows = (properties: ScadaObjectProperty[]): ScadaPropertyRow[] => {
|
||||
const result: ScadaPropertyRow[] = [];
|
||||
for (const property of properties) {
|
||||
let propertyRow = result.find(r => r.label === property.name);
|
||||
if (!propertyRow) {
|
||||
propertyRow = {
|
||||
label: property.name,
|
||||
properties: []
|
||||
};
|
||||
result.push(propertyRow);
|
||||
}
|
||||
if (property.type === 'switch') {
|
||||
propertyRow.switch = property;
|
||||
} else {
|
||||
propertyRow.properties.push(property);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@ -1,39 +1,160 @@
|
||||
<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>
|
||||
{
|
||||
<tb:metadata>{
|
||||
"title": "My first SCADA Object",
|
||||
"initial": [
|
||||
{
|
||||
"tag": "RECT",
|
||||
"attributes": [
|
||||
{
|
||||
"name": "fill",
|
||||
"value": {
|
||||
"type": "property",
|
||||
"propertyId": "background"
|
||||
"states": {
|
||||
"initialState": {
|
||||
"initial": true,
|
||||
"state": [
|
||||
{
|
||||
"tag": "level",
|
||||
"attributes": [
|
||||
{
|
||||
"name": "fill",
|
||||
"value": {
|
||||
"type": "property",
|
||||
"propertyId": "levelBackground"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "levelValueBackground",
|
||||
"show": {
|
||||
"type": "property",
|
||||
"propertyId": "showValue"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stroke",
|
||||
"value": {
|
||||
"type": "property",
|
||||
"propertyId": "strokeColor"
|
||||
},
|
||||
{
|
||||
"tag": "levelValue",
|
||||
"show": {
|
||||
"type": "property",
|
||||
"propertyId": "showValue"
|
||||
},
|
||||
"text": {
|
||||
"font": {
|
||||
"type": "property",
|
||||
"propertyId": "valueFont"
|
||||
},
|
||||
"color": {
|
||||
"type": "property",
|
||||
"propertyId": "valueColor"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stroke-width",
|
||||
"value": {
|
||||
"type": "property",
|
||||
"propertyId": "strokeWidth"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"updateLevelState": {
|
||||
"state": [
|
||||
{
|
||||
"tag": "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",
|
||||
"text": {
|
||||
"content": {
|
||||
"type": "valueFormat",
|
||||
"units": {
|
||||
"type": "property",
|
||||
"propertyId": "valueUnits"
|
||||
},
|
||||
"decimals": {
|
||||
"type": "property",
|
||||
"propertyId": "valueDecimals"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"disabledState": {
|
||||
"state": [
|
||||
{
|
||||
"tag": "level",
|
||||
"attributes": [
|
||||
{
|
||||
"name": "fill",
|
||||
"value": {
|
||||
"type": "property",
|
||||
"propertyId": "disabledLevelBackground"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"enabledState": {
|
||||
"state": [
|
||||
{
|
||||
"tag": "level",
|
||||
"attributes": [
|
||||
{
|
||||
"name": "fill",
|
||||
"value": {
|
||||
"type": "property",
|
||||
"propertyId": "levelBackground"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
"behavior": [
|
||||
{
|
||||
"id": "initialState",
|
||||
"name": "Initial state",
|
||||
"id": "levelState",
|
||||
"name": "Level",
|
||||
"type": "getValue",
|
||||
"valueType": "DOUBLE",
|
||||
"defaultValue": 0,
|
||||
@ -42,26 +163,7 @@
|
||||
"matcher": {
|
||||
"type": "any"
|
||||
},
|
||||
"state": [
|
||||
{
|
||||
"tag": "RECT",
|
||||
"animate": 200,
|
||||
"attributes": [
|
||||
{
|
||||
"name": "height",
|
||||
"value": {
|
||||
"type": "input"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "width",
|
||||
"value": {
|
||||
"type": "input"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
"stateId": "updateLevelState"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -77,73 +179,129 @@
|
||||
"type": "constant",
|
||||
"value": true
|
||||
},
|
||||
"state": [
|
||||
{
|
||||
"tag": "RECT",
|
||||
"attributes": [
|
||||
{
|
||||
"name": "fill",
|
||||
"value": {
|
||||
"type": "property",
|
||||
"propertyId": "disabledBackground"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
"stateId": "disabledState"
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"type": "constant",
|
||||
"value": false
|
||||
},
|
||||
"state": [
|
||||
{
|
||||
"tag": "RECT",
|
||||
"attributes": [
|
||||
{
|
||||
"name": "fill",
|
||||
"value": {
|
||||
"type": "property",
|
||||
"propertyId": "background"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
"stateId": "enabledState"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": [
|
||||
{
|
||||
"id": "strokeColor",
|
||||
"name": "Stroke color",
|
||||
"type": "color",
|
||||
"default": "#aaa"
|
||||
"id": "showValue",
|
||||
"name": "Value",
|
||||
"type": "switch",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"id": "strokeWidth",
|
||||
"name": "Stroke width",
|
||||
"id": "valueUnits",
|
||||
"name": "Value",
|
||||
"type": "units",
|
||||
"default": "",
|
||||
"disableOnProperty": "showValue"
|
||||
},
|
||||
{
|
||||
"id": "valueDecimals",
|
||||
"name": "Value",
|
||||
"type": "number",
|
||||
"default": 5,
|
||||
"default": 2,
|
||||
"min": 0,
|
||||
"max": 20
|
||||
"max": 15,
|
||||
"step": 1,
|
||||
"fieldSuffix": "decimals",
|
||||
"disableOnProperty": "showValue"
|
||||
},
|
||||
{
|
||||
"id": "background",
|
||||
"name": "Background",
|
||||
"id": "valueFont",
|
||||
"name": "Value",
|
||||
"type": "font",
|
||||
"default": {
|
||||
"size": 6,
|
||||
"sizeUnit": "px",
|
||||
"family": "Roboto",
|
||||
"weight": "normal",
|
||||
"style": "normal"
|
||||
},
|
||||
"disableOnProperty": "showValue"
|
||||
},
|
||||
{
|
||||
"id": "valueColor",
|
||||
"name": "Value",
|
||||
"type": "color",
|
||||
"default": "#000000",
|
||||
"disableOnProperty": "showValue"
|
||||
},
|
||||
{
|
||||
"id": "minLevel",
|
||||
"name": "Level range",
|
||||
"subLabel": "min",
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"default": 0,
|
||||
"min": 0
|
||||
},
|
||||
{
|
||||
"id": "maxLevel",
|
||||
"name": "Level range",
|
||||
"subLabel": "max",
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"default": 100,
|
||||
"min": 0
|
||||
},
|
||||
{
|
||||
"id": "showMinMaxLevel",
|
||||
"name": "Min/Max label",
|
||||
"type": "switch",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"id": "minMaxLevelFont",
|
||||
"name": "Min/Max label",
|
||||
"type": "font",
|
||||
"default": {
|
||||
"size": 6,
|
||||
"sizeUnit": "px",
|
||||
"family": "Roboto",
|
||||
"weight": "normal",
|
||||
"style": "normal"
|
||||
},
|
||||
"disableOnProperty": "showMinMaxLevel"
|
||||
},
|
||||
{
|
||||
"id": "minMaxLevelColor",
|
||||
"name": "Min/Max label",
|
||||
"type": "color",
|
||||
"default": "#666",
|
||||
"disableOnProperty": "showMinMaxLevel"
|
||||
},
|
||||
{
|
||||
"id": "levelBackground",
|
||||
"name": "Level background",
|
||||
"subLabel": "Enabled",
|
||||
"type": "color",
|
||||
"default": "green"
|
||||
},
|
||||
{
|
||||
"id": "disabledBackground",
|
||||
"name": "Disabled background",
|
||||
"type": "color",
|
||||
"default": "#ccc"
|
||||
}
|
||||
"default": "#1abb48",
|
||||
"divider": true
|
||||
},
|
||||
{
|
||||
"id": "disabledLevelBackground",
|
||||
"name": "Level background",
|
||||
"subLabel": "Disabled",
|
||||
"type": "color",
|
||||
"default": "#ccc"
|
||||
}
|
||||
]
|
||||
}
|
||||
</tb:metadata>
|
||||
<rect tb:tag="RECT" width="100" height="100" rx="0" fill="none" stroke="#ccc" stroke-width="2"/>
|
||||
}</tb:metadata>
|
||||
<rect width="100" height="100" rx="0" fill="none" stroke="#ccc" stroke-width="2"/>
|
||||
<rect x="8" y="15" width="20" height="81" rx="2" fill="#ececec" stroke="#000" stroke-width="1.0359"/>
|
||||
<rect transform="scale(1 -1)" x="8.5" y="-95.5" width="19" height="40" rx="1.5" fill="#1abb48" tb:tag="level"/>
|
||||
<text x="32" y="95" fill="#666" font-family="Roboto" font-size="6px" tb:tag="minLevel" xml:space="preserve"><tspan x="31.354307" y="96.626251">min</tspan></text>
|
||||
<rect x="10.941" y="49.544" width="14.155" height="12" rx="2.8668" fill="#fff" fill-opacity=".45148" tb:tag="levelValueBackground"/>
|
||||
<text x="18" y="56" dominant-baseline="middle" fill="#000000" font-family="Roboto" font-size="6px" text-anchor="middle" tb:tag="levelValue" xml:space="preserve"><tspan>N/A</tspan></text>
|
||||
<text x="32" y="20" fill="#666" font-family="Roboto" font-size="6px" tb:tag="maxLevel" xml:space="preserve"><tspan x="29.865496" y="18.716127">max</tspan></text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 9.3 KiB |
Loading…
x
Reference in New Issue
Block a user