From f01f99e738c8e2d3b2e21dd5cb8770949ee0dc45 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 3 May 2024 19:21:07 +0300 Subject: [PATCH] UI: Scada metadata and configuration improvements. --- .../widget/lib/scada/scada.models.ts | 329 +++++++++++++--- .../scada-object-settings.component.html | 47 ++- .../scada/scada-object-settings.component.ts | 75 +++- .../scada/scada-object-settings.models.ts | 43 +++ ui-ngx/src/assets/widget/scada/drawing.svg | 352 +++++++++++++----- 5 files changed, 675 insertions(+), 171 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts index 7088fe6131..542e24844e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada.models.ts @@ -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({}, 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 s.initial); + if (initialState) { + this.updateState(initialState); } if (this.valueGetters.length) { const getValueObservables: Array> = []; @@ -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 ''; } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html index d5a8cba6df..2907a951dd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.html @@ -32,17 +32,42 @@ -
-
-
{{ property.name }}
- - - - - +
+
+ + {{ propertyRow.label }} + +
{{ propertyRow.label }}
+
+ +
{{ property.subLabel }}
+ + +
{{ property.fieldSuffix }}
+
+ + + + +
{{ property.fieldSuffix }}
+
+ + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts index ad10d681a7..3e0bd7409d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.component.ts @@ -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, 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[] = []; + 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); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts new file mode 100644 index 0000000000..2426b13a0b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-object-settings.models.ts @@ -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; +}; diff --git a/ui-ngx/src/assets/widget/scada/drawing.svg b/ui-ngx/src/assets/widget/scada/drawing.svg index 972ca550cd..03849049f3 100644 --- a/ui-ngx/src/assets/widget/scada/drawing.svg +++ b/ui-ngx/src/assets/widget/scada/drawing.svg @@ -1,39 +1,160 @@ - - { + { "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" + } ] - } - - + } + + + + min + + N/A + max