diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.scss
index 5dba13ec075..441391d95a1 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.scss
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.scss
@@ -13,145 +13,143 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-:host {
- .tb-battery-level-panel {
- width: 100%;
- height: 100%;
- position: relative;
+.tb-battery-level-panel {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 24px;
+ &.tb-battery-level-pointer {
+ cursor: pointer;
+ }
+ > div:not(.tb-battery-level-overlay) {
+ z-index: 1;
+ }
+ .tb-battery-level-overlay {
+ position: absolute;
+ top: 12px;
+ left: 12px;
+ bottom: 12px;
+ right: 12px;
+ }
+ .tb-battery-level-content {
+ min-height: 0;
+ flex: 1;
display: flex;
- flex-direction: column;
- gap: 16px;
- padding: 20px 24px 24px 24px;
- &.tb-battery-level-pointer {
- cursor: pointer;
- }
- > div:not(.tb-battery-level-overlay) {
- z-index: 1;
+ justify-content: center;
+ &.vertical {
+ flex-direction: row;
+ gap: 16px;
+ .tb-battery-level-value-box {
+ align-items: center;
+ .tb-battery-level-value {
+ padding: 8px 12px;
+ }
+ }
}
- .tb-battery-level-overlay {
- position: absolute;
- top: 12px;
- left: 12px;
- bottom: 12px;
- right: 12px;
+ &.horizontal {
+ flex-direction: column-reverse;
+ gap: 8px;
+ align-items: center;
+ .tb-battery-level-value-box {
+ .tb-battery-level-value {
+ padding: 4px 6px;
+ }
+ }
}
- .tb-battery-level-content {
- min-height: 0;
- flex: 1;
+ .tb-battery-level-box {
display: flex;
- justify-content: center;
- &.vertical {
- flex-direction: row;
- gap: 16px;
- .tb-battery-level-value-box {
- align-items: center;
- .tb-battery-level-value {
- padding: 8px 12px;
- }
+ align-items: center;
+ .tb-battery-level-rectangle {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ .tb-battery-level-shape {
+ position: absolute;
+ inset: 0;
+ mask-repeat: no-repeat;
+ mask-size: cover;
+ mask-position: center;
}
- }
- &.horizontal {
- flex-direction: column-reverse;
- gap: 8px;
- align-items: center;
- .tb-battery-level-value-box {
- .tb-battery-level-value {
- padding: 4px 6px;
- }
+ .tb-battery-level-container {
+ position: absolute;
+ display: flex;
+ gap: 3%;
}
- }
- .tb-battery-level-box {
- display: flex;
- align-items: center;
- .tb-battery-level-rectangle {
+ .tb-battery-level-indicator-box {
width: 100%;
height: 100%;
- position: relative;
+ &.solid {
+ background-repeat: no-repeat;
+ transition: background 0.2s ease-out;
+ }
+ &.divided {
+ transition: opacity 0.2s ease-out;
+ }
+ }
+ &.vertical {
.tb-battery-level-shape {
- position: absolute;
- inset: 0;
- mask-repeat: no-repeat;
- mask-size: cover;
- mask-position: center;
+ mask-image: url(/assets/widget/battery-level/battery-shape-vertical.svg);
}
.tb-battery-level-container {
- position: absolute;
- display: flex;
- gap: 3%;
+ flex-direction: column-reverse;
}
- .tb-battery-level-indicator-box {
- width: 100%;
- height: 100%;
- &.solid {
- background-repeat: no-repeat;
- transition: background 0.2s ease-out;
- }
- &.divided {
- transition: opacity 0.2s ease-out;
+ &.solid {
+ .tb-battery-level-container {
+ inset: 8.85% 6.25% 3.54% 6.25%;
}
}
- &.vertical {
- .tb-battery-level-shape {
- mask-image: url(/assets/widget/battery-level/battery-shape-vertical.svg);
- }
+ &.divided {
.tb-battery-level-container {
- flex-direction: column-reverse;
+ inset: 9.73% 7.81% 4.42% 7.81%;
}
+ }
+ .tb-battery-level-indicator-box {
&.solid {
- .tb-battery-level-container {
- inset: 8.85% 6.25% 3.54% 6.25%;
- }
+ border-radius: 10.7% / 6%;
+ background-position: 0 101%;
}
&.divided {
- .tb-battery-level-container {
- inset: 9.73% 7.81% 4.42% 7.81%;
- }
- }
- .tb-battery-level-indicator-box {
- &.solid {
- border-radius: 10.7% / 6%;
- background-position: 0 101%;
- }
- &.divided {
- border-radius: 7.14% / 17.8%;
- }
+ border-radius: 7.14% / 17.8%;
}
}
- &.horizontal {
- .tb-battery-level-shape {
- mask-image: url(/assets/widget/battery-level/battery-shape-horizontal.svg);
- }
+ }
+ &.horizontal {
+ .tb-battery-level-shape {
+ mask-image: url(/assets/widget/battery-level/battery-shape-horizontal.svg);
+ }
+ .tb-battery-level-container {
+ inset: 6.25% 8.85% 6.25% 3.54%;
+ flex-direction: row;
+ }
+ &.solid {
.tb-battery-level-container {
inset: 6.25% 8.85% 6.25% 3.54%;
- flex-direction: row;
}
+ }
+ &.divided {
+ .tb-battery-level-container {
+ inset: 7.81% 9.73% 7.81% 4.42%;
+ }
+ }
+ .tb-battery-level-indicator-box {
&.solid {
- .tb-battery-level-container {
- inset: 6.25% 8.85% 6.25% 3.54%;
- }
+ border-radius: 6% / 10.7%;
+ background-position: -1% 0%;
}
&.divided {
- .tb-battery-level-container {
- inset: 7.81% 9.73% 7.81% 4.42%;
- }
- }
- .tb-battery-level-indicator-box {
- &.solid {
- border-radius: 6% / 10.7%;
- background-position: -1% 0%;
- }
- &.divided {
- border-radius: 17.8% / 7.14%;
- }
+ border-radius: 17.8% / 7.14%;
}
}
}
}
- .tb-battery-level-value-box {
- display: flex;
- .tb-battery-level-value {
- white-space: nowrap;
- }
+ }
+ .tb-battery-level-value-box {
+ display: flex;
+ .tb-battery-level-value {
+ white-space: nowrap;
}
}
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts
index 442d207a5e2..6e9f8323be1 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts
@@ -24,7 +24,8 @@ import {
OnInit,
Renderer2,
TemplateRef,
- ViewChild
+ ViewChild,
+ ViewEncapsulation
} from '@angular/core';
import { WidgetContext } from '@home/models/widget-component.models';
import { formatValue, isDefinedAndNotNull, isNumeric } from '@core/utils';
@@ -74,7 +75,8 @@ const horizontalBatteryDimensions = {
@Component({
selector: 'tb-battery-level-widget',
templateUrl: './battery-level-widget.component.html',
- styleUrls: ['./battery-level-widget.component.scss']
+ styleUrls: ['./battery-level-widget.component.scss'],
+ encapsulation: ViewEncapsulation.None
})
export class BatteryLevelWidgetComponent implements OnInit, OnDestroy, AfterViewInit {
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.html
new file mode 100644
index 00000000000..e81519ff3c8
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.html
@@ -0,0 +1,102 @@
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.ts
new file mode 100644
index 00000000000..a9ae8b978a3
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.ts
@@ -0,0 +1,139 @@
+///
+/// Copyright © 2016-2023 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 { Component, Injector } from '@angular/core';
+import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
+import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { formatValue, isDefinedAndNotNull } from '@core/utils';
+import {
+ centerValueLabel,
+ windSpeedDirectionDefaultSettings,
+ WindSpeedDirectionLayout,
+ windSpeedDirectionLayoutImages,
+ windSpeedDirectionLayouts,
+ windSpeedDirectionLayoutTranslations
+} from '@home/components/widget/lib/weather/wind-speed-direction-widget.models';
+import { getDataKeyByLabel } from '@shared/models/widget-settings.models';
+
+@Component({
+ selector: 'tb-wind-speed-direction-widget-settings',
+ templateUrl: './wind-speed-direction-widget-settings.component.html',
+ styleUrls: []
+})
+export class WindSpeedDirectionWidgetSettingsComponent extends WidgetSettingsComponent {
+
+ get hasCenterValue(): boolean {
+ return !!getDataKeyByLabel(this.widgetConfig.config.datasources, centerValueLabel);
+ }
+
+ get majorTicksFontEnabled(): boolean {
+ const layout: WindSpeedDirectionLayout = this.windSpeedDirectionWidgetSettingsForm.get('layout').value;
+ return [ WindSpeedDirectionLayout.default, WindSpeedDirectionLayout.advanced ].includes(layout);
+ }
+
+ get minorTicksFontEnabled(): boolean {
+ const layout: WindSpeedDirectionLayout = this.windSpeedDirectionWidgetSettingsForm.get('layout').value;
+ return layout === WindSpeedDirectionLayout.advanced;
+ }
+
+ windSpeedDirectionLayouts = windSpeedDirectionLayouts;
+
+ windSpeedDirectionLayoutTranslationMap = windSpeedDirectionLayoutTranslations;
+ windSpeedDirectionLayoutImageMap = windSpeedDirectionLayoutImages;
+
+ windSpeedDirectionWidgetSettingsForm: UntypedFormGroup;
+
+ centerValuePreviewFn = this._centerValuePreviewFn.bind(this);
+
+ constructor(protected store: Store
,
+ private $injector: Injector,
+ private fb: UntypedFormBuilder) {
+ super(store);
+ }
+
+ protected settingsForm(): UntypedFormGroup {
+ return this.windSpeedDirectionWidgetSettingsForm;
+ }
+
+ protected defaultSettings(): WidgetSettings {
+ return {...windSpeedDirectionDefaultSettings};
+ }
+
+ protected onSettingsSet(settings: WidgetSettings) {
+ this.windSpeedDirectionWidgetSettingsForm = this.fb.group({
+ layout: [settings.layout, []],
+
+ centerValueFont: [settings.centerValueFont, []],
+ centerValueColor: [settings.centerValueColor, []],
+
+ ticksColor: [settings.ticksColor, []],
+ directionalNamesElseDegrees: [settings.directionalNamesElseDegrees, []],
+
+ majorTicksFont: [settings.majorTicksFont, []],
+ majorTicksColor: [settings.majorTicksColor, []],
+
+ minorTicksFont: [settings.minorTicksFont, []],
+ minorTicksColor: [settings.minorTicksColor, []],
+
+ arrowColor: [settings.arrowColor, []],
+
+ background: [settings.background, []]
+ });
+ }
+
+ protected validatorTriggers(): string[] {
+ return ['layout'];
+ }
+
+ protected updateValidators(emitEvent: boolean) {
+ const layout: WindSpeedDirectionLayout = this.windSpeedDirectionWidgetSettingsForm.get('layout').value;
+
+ const majorTicksFontEnabled = [ WindSpeedDirectionLayout.default, WindSpeedDirectionLayout.advanced ].includes(layout);
+ const minorTicksFontEnabled = layout === WindSpeedDirectionLayout.advanced;
+
+ if (majorTicksFontEnabled) {
+ this.windSpeedDirectionWidgetSettingsForm.get('majorTicksFont').enable();
+ } else {
+ this.windSpeedDirectionWidgetSettingsForm.get('majorTicksFont').disable();
+ }
+
+ if (minorTicksFontEnabled) {
+ this.windSpeedDirectionWidgetSettingsForm.get('minorTicksFont').enable();
+ } else {
+ this.windSpeedDirectionWidgetSettingsForm.get('minorTicksFont').disable();
+ }
+ }
+
+ private _centerValuePreviewFn(): string {
+ const centerValueDataKey = getDataKeyByLabel(this.widgetConfig.config.datasources, centerValueLabel);
+ if (centerValueDataKey) {
+ let units: string = this.widgetConfig.config.units;
+ let decimals: number = this.widgetConfig.config.decimals;
+ if (isDefinedAndNotNull(centerValueDataKey?.decimals)) {
+ decimals = centerValueDataKey.decimals;
+ }
+ if (centerValueDataKey?.units) {
+ units = centerValueDataKey.units;
+ }
+ return formatValue(25, decimals, units, true);
+ } else {
+ return '225°';
+ }
+ }
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
index 6593b82747f..c9175278723 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
@@ -288,6 +288,9 @@ import {
import {
BatteryLevelWidgetSettingsComponent
} from '@home/components/widget/lib/settings/indicator/battery-level-widget-settings.component';
+import {
+ WindSpeedDirectionWidgetSettingsComponent
+} from '@home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component';
@NgModule({
declarations: [
@@ -394,7 +397,8 @@ import {
AggregatedValueCardWidgetSettingsComponent,
AlarmCountWidgetSettingsComponent,
EntityCountWidgetSettingsComponent,
- BatteryLevelWidgetSettingsComponent
+ BatteryLevelWidgetSettingsComponent,
+ WindSpeedDirectionWidgetSettingsComponent
],
imports: [
CommonModule,
@@ -506,7 +510,8 @@ import {
AggregatedValueCardWidgetSettingsComponent,
AlarmCountWidgetSettingsComponent,
EntityCountWidgetSettingsComponent,
- BatteryLevelWidgetSettingsComponent
+ BatteryLevelWidgetSettingsComponent,
+ WindSpeedDirectionWidgetSettingsComponent
]
})
export class WidgetSettingsModule {
@@ -583,5 +588,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.scss
new file mode 100644
index 00000000000..53a36584538
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.scss
@@ -0,0 +1,49 @@
+/**
+ * Copyright © 2016-2023 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.
+ */
+.tb-wind-speed-direction-panel {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 24px;
+ &.tb-wind-speed-direction-pointer {
+ cursor: pointer;
+ }
+ > div:not(.tb-wind-speed-direction-overlay) {
+ z-index: 1;
+ }
+ .tb-wind-speed-direction-overlay {
+ position: absolute;
+ top: 12px;
+ left: 12px;
+ bottom: 12px;
+ right: 12px;
+ }
+ .tb-wind-speed-direction-content {
+ flex: 1;
+ min-width: 0;
+ min-height: 0;
+ .tb-wind-speed-direction-shape {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.ts
new file mode 100644
index 00000000000..d328e59acec
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.component.ts
@@ -0,0 +1,333 @@
+///
+/// Copyright © 2016-2023 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 {
+ AfterViewInit,
+ ChangeDetectorRef,
+ Component,
+ ElementRef,
+ Input,
+ OnDestroy,
+ OnInit,
+ Renderer2,
+ TemplateRef,
+ ViewChild, ViewEncapsulation
+} from '@angular/core';
+import { WidgetContext } from '@home/models/widget-component.models';
+import {
+ centerValueLabel,
+ windDirectionLabel,
+ windSpeedDirectionDefaultSettings,
+ WindSpeedDirectionLayout,
+ WindSpeedDirectionWidgetSettings
+} from '@home/components/widget/lib/weather/wind-speed-direction-widget.models';
+import {
+ backgroundStyle,
+ ColorProcessor,
+ ComponentStyle,
+ Font, getDataKey,
+ getDataKeyByLabel,
+ getSingleTsValueByDataKey,
+ overlayStyle
+} from '@shared/models/widget-settings.models';
+import { WidgetComponent } from '@home/components/widget/widget.component';
+import { formatValue, isDefinedAndNotNull, isNumeric } from '@core/utils';
+import { ResizeObserver } from '@juggle/resize-observer';
+import { Path, Svg, SVG, Text } from '@svgdotjs/svg.js';
+import { DataKey } from '@shared/models/widget.models';
+
+const shapeSize = 180;
+const cx = shapeSize / 2;
+const cy = shapeSize / 2;
+const ticksDiameter = 140;
+
+const ticksTextMap: {[angle: number]: string} = {
+ 0: 'N',
+ 45: 'NE',
+ 90: 'E',
+ 135: 'SE',
+ 180: 'S',
+ 225: 'SW',
+ 270: 'W',
+ 315: 'NW'
+};
+
+@Component({
+ selector: 'tb-wind-speed-direction-widget',
+ templateUrl: './wind-speed-direction-widget.component.html',
+ styleUrls: ['./wind-speed-direction-widget.component.scss'],
+ encapsulation: ViewEncapsulation.None
+})
+export class WindSpeedDirectionWidgetComponent implements OnInit, OnDestroy, AfterViewInit {
+
+ @ViewChild('windSpeedDirectionShape', {static: false})
+ windSpeedDirectionShape: ElementRef;
+
+ settings: WindSpeedDirectionWidgetSettings;
+
+ @Input()
+ ctx: WidgetContext;
+
+ @Input()
+ widgetTitlePanel: TemplateRef;
+
+ layout: WindSpeedDirectionLayout;
+
+ centerValueColor: ColorProcessor;
+
+ backgroundStyle: ComponentStyle = {};
+ overlayStyle: ComponentStyle = {};
+
+ shapeResize$: ResizeObserver;
+
+ hasCardClickAction = false;
+
+ private decimals = 0;
+ private units = '';
+
+ private drawSvgShapePending = false;
+ private svgShape: Svg;
+ private arrow: Path;
+ private centerValueTextNode: Text;
+
+ private windDirectionDataKey: DataKey;
+ private centerValueDataKey: DataKey;
+
+ private windDirection = 0;
+ private centerValueText = 'N/A';
+
+ constructor(private widgetComponent: WidgetComponent,
+ private renderer: Renderer2,
+ private cd: ChangeDetectorRef) {
+ }
+
+ ngOnInit(): void {
+ this.ctx.$scope.windSpeedDirectionWidget = this;
+ this.settings = {...windSpeedDirectionDefaultSettings, ...this.ctx.settings};
+
+ this.windDirectionDataKey = getDataKeyByLabel(this.ctx.datasources, windDirectionLabel);
+ if (!this.windDirectionDataKey) {
+ this.windDirectionDataKey = getDataKey(this.ctx.datasources);
+ }
+ this.centerValueDataKey = getDataKeyByLabel(this.ctx.datasources, centerValueLabel);
+
+ if (this.centerValueDataKey) {
+ this.decimals = this.ctx.decimals;
+ this.units = this.ctx.units;
+ if (isDefinedAndNotNull(this.centerValueDataKey.decimals)) {
+ this.decimals = this.centerValueDataKey.decimals;
+ }
+ if (this.centerValueDataKey.units) {
+ this.units = this.centerValueDataKey.units;
+ }
+ }
+
+ this.layout = this.settings.layout;
+
+ this.centerValueColor = ColorProcessor.fromSettings(this.settings.centerValueColor);
+
+ this.backgroundStyle = backgroundStyle(this.settings.background);
+ this.overlayStyle = overlayStyle(this.settings.background.overlay);
+
+ this.hasCardClickAction = this.ctx.actionsApi.getActionDescriptors('cardClick').length > 0;
+ }
+
+ ngAfterViewInit() {
+ if (this.drawSvgShapePending) {
+ this.drawSvg();
+ }
+ }
+
+ ngOnDestroy() {
+ if (this.shapeResize$) {
+ this.shapeResize$.disconnect();
+ }
+ }
+
+ public onInit() {
+ const borderRadius = this.ctx.$widgetElement.css('borderRadius');
+ this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
+ if (this.windSpeedDirectionShape) {
+ this.drawSvg();
+ } else {
+ this.drawSvgShapePending = true;
+ }
+ this.cd.detectChanges();
+ }
+
+ public onDataUpdated() {
+ let centerValue = 0;
+ this.windDirection = 0;
+ this.centerValueText = 'N/A';
+ const windDirectionTsValue = getSingleTsValueByDataKey(this.ctx.data, this.windDirectionDataKey);
+ if (windDirectionTsValue && isDefinedAndNotNull(windDirectionTsValue[1]) && isNumeric(windDirectionTsValue[1])) {
+ this.windDirection = windDirectionTsValue[1];
+ }
+ if (this.centerValueDataKey) {
+ const centerValueTsValue = getSingleTsValueByDataKey(this.ctx.data, this.centerValueDataKey);
+ if (centerValueTsValue && isDefinedAndNotNull(centerValueTsValue[1]) && isNumeric(centerValueTsValue[1])) {
+ centerValue = centerValueTsValue[1];
+ this.centerValueText = formatValue(centerValue, this.decimals, '', true);
+ }
+ }
+ this.centerValueColor.update(centerValue);
+ this.renderValues();
+ }
+
+ public cardClick($event: Event) {
+ this.ctx.actionsApi.cardClick($event);
+ }
+
+ private drawSvg() {
+ this.svgShape = SVG().addTo(this.windSpeedDirectionShape.nativeElement).size(shapeSize, shapeSize);
+ this.renderer.setStyle(this.svgShape.node, 'overflow', 'visible');
+ this.renderer.setStyle(this.svgShape.node, 'user-select', 'none');
+
+ // Draw ticks
+
+ const ticksYStart = (shapeSize - ticksDiameter) / 2;
+ for (let i = 0; i < 360; i += 3) {
+ if (i !== 0) {
+ let color: string;
+ let width: number;
+ let height: number;
+ if (i % 90 === 0) {
+ // Major ticks
+ color = this.settings.majorTicksColor;
+ width = 2;
+ height = 8;
+ } else if (i % 45 === 0) {
+ // Minor ticks
+ color = this.settings.minorTicksColor;
+ width = 2;
+ height = 8;
+ } else {
+ color = this.settings.ticksColor;
+ width = 1.2;
+ height = 3;
+ }
+ this.svgShape.line(cx, ticksYStart, cx, ticksYStart + height).attr({
+ 'stroke-width': width,
+ stroke: color
+ }).rotate(i, cx, cy);
+ }
+ }
+
+ // Draw pointer
+ this.svgShape.path('m 89.152,20.470002 c 0.3917,-0.626669 1.3043,-0.626669 1.696,0 l 3.1958,5.1132 ' +
+ 'c 0.4162,0.66605 -0.0626,1.53 -0.848,1.53 h -6.3916 c -0.7854,0 -1.2642,-0.86395 -0.848,-1.53 z')
+ .fill(this.settings.majorTicksColor);
+
+ let x: number;
+ let y: number;
+ let degree: number;
+
+ const drawMajorTicksText = [ WindSpeedDirectionLayout.default, WindSpeedDirectionLayout.advanced ].includes(this.settings.layout);
+ const drawMinorTicksText = this.settings.layout === WindSpeedDirectionLayout.advanced;
+
+ if (drawMajorTicksText) {
+ // Draw major ticks text
+ for (let i = 0; i < 4; i += 1) {
+ degree = i * 90;
+ if (i % 2 === 0) {
+ x = cx;
+ y = i === 0 ? 10 : shapeSize - 10;
+ } else {
+ y = cy;
+ x = i === 3 ? 10 : shapeSize - 10;
+ }
+ this.drawTickText(degree, this.settings.majorTicksFont, this.settings.majorTicksColor, x, y);
+ }
+ }
+
+ if (drawMinorTicksText) {
+ // Draw minor ticks text
+ for (let i = 0; i < 4; i += 1) {
+ degree = 45 + (i * 90);
+ if (i < 2) {
+ x = shapeSize - 30;
+ y = i === 0 ? 30 : shapeSize - 30;
+ } else {
+ x = 30;
+ y = i === 3 ? 30 : shapeSize - 30;
+ }
+ this.drawTickText(degree, this.settings.minorTicksFont, this.settings.minorTicksColor, x, y);
+ }
+ }
+
+ // Draw arrow
+ this.arrow = this.svgShape.path('m 89.263587,23.438382 c 0.388942,-0.392146 1.022181,-0.388549 1.414649,0 ' +
+ 'l 6.389758,6.389 c 0.392414,0.388462 0.394911,1.022828 0.0059,1.415 -0.388987,0.392109 -1.022226,0.383311 -1.41408,-0.006 ' +
+ 'l -4.6762,-4.676 v 28.417 h -2 v -28.417 l -4.637642,4.676 ' +
+ 'c -0.388878,0.392069 -1.022053,0.394895 -1.414202,0.006 -0.392082,-0.388967 -0.394683,-1.022852 -0.0057,-1.415 ' +
+ 'z M 88.983614,154.85438 h -2.217 v 2 h 6.434 v -2 h -2.217 v -29.939 h -2 z').fill(this.settings.arrowColor);
+
+ // Draw value
+ this.centerValueTextNode = this.svgShape.text('').font({
+ family: this.settings.centerValueFont.family,
+ weight: this.settings.centerValueFont.weight,
+ style: this.settings.centerValueFont.style
+ }).attr({x: '50%', y: '50%', 'text-anchor': 'middle'});
+ if (!this.units) {
+ this.centerValueTextNode.attr({'dominant-baseline': 'middle'});
+ }
+
+ this.shapeResize$ = new ResizeObserver(() => {
+ this.onResize();
+ });
+ this.shapeResize$.observe(this.windSpeedDirectionShape.nativeElement);
+ this.onResize();
+
+ this.renderValues();
+ }
+
+ private drawTickText(degree: number, font: Font, color: string, x: number, y: number) {
+ const tickText = this.settings.directionalNamesElseDegrees ? ticksTextMap[degree] : degree + '';
+ this.svgShape.text(tickText).font({
+ family: font.family,
+ weight: font.weight,
+ style: font.style,
+ size: this.settings.directionalNamesElseDegrees ? '14px' : '10px'
+ }).fill(color).center(x, y);
+ }
+
+ private renderValues() {
+ if (this.svgShape) {
+ this.arrow.timeline().finish();
+ this.arrow.animate(800).transform({rotate: this.windDirection});
+ this.renderCenterValueText();
+ }
+ }
+
+ private renderCenterValueText() {
+ const text = this.centerValueDataKey ? this.centerValueText : formatValue(this.windDirection, 0, '') + '°';
+ this.centerValueTextNode.text(add => {
+ add.tspan(text).font({size: '24px'});
+ if (this.units) {
+ add.tspan(this.units).newLine().font({size: '14px'});
+ }
+ }).fill(this.centerValueColor.color);
+ }
+
+ private onResize() {
+ const shapeWidth = this.windSpeedDirectionShape.nativeElement.getBoundingClientRect().width;
+ const shapeHeight = this.windSpeedDirectionShape.nativeElement.getBoundingClientRect().height;
+ const size = Math.min(shapeWidth, shapeHeight);
+ const scale = size / shapeSize;
+ this.renderer.setStyle(this.svgShape.node, 'transform', `scale(${scale})`);
+ }
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.models.ts
new file mode 100644
index 00000000000..c9fe2a84df4
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/weather/wind-speed-direction-widget.models.ts
@@ -0,0 +1,108 @@
+///
+/// Copyright © 2016-2023 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 { BatteryLevelLayout } from '@home/components/widget/lib/indicator/battery-level-widget.models';
+import {
+ BackgroundSettings,
+ BackgroundType,
+ ColorSettings,
+ constantColor,
+ Font
+} from '@shared/models/widget-settings.models';
+
+export enum WindSpeedDirectionLayout {
+ default = 'default',
+ advanced = 'advanced',
+ simplified = 'simplified'
+}
+
+export const windSpeedDirectionLayouts = Object.keys(WindSpeedDirectionLayout) as WindSpeedDirectionLayout[];
+
+export const windSpeedDirectionLayoutTranslations = new Map(
+ [
+ [WindSpeedDirectionLayout.default, 'widgets.wind-speed-direction.layout-default'],
+ [WindSpeedDirectionLayout.advanced, 'widgets.wind-speed-direction.layout-advanced'],
+ [WindSpeedDirectionLayout.simplified, 'widgets.wind-speed-direction.layout-simplified']
+ ]
+);
+
+export const windSpeedDirectionLayoutImages = new Map(
+ [
+ [WindSpeedDirectionLayout.default, 'assets/widget/wind-speed-direction/default-layout.svg'],
+ [WindSpeedDirectionLayout.advanced, 'assets/widget/wind-speed-direction/advanced-layout.svg'],
+ [WindSpeedDirectionLayout.simplified, 'assets/widget/wind-speed-direction/simplified-layout.svg']
+ ]
+);
+
+export interface WindSpeedDirectionWidgetSettings {
+ layout: WindSpeedDirectionLayout;
+ centerValueFont: Font;
+ centerValueColor: ColorSettings;
+ ticksColor: string;
+ arrowColor: string;
+ directionalNamesElseDegrees: boolean;
+ majorTicksColor: string;
+ majorTicksFont: Font;
+ minorTicksColor: string;
+ minorTicksFont: Font;
+ background: BackgroundSettings;
+}
+
+export const windSpeedDirectionDefaultSettings: WindSpeedDirectionWidgetSettings = {
+ layout: WindSpeedDirectionLayout.default,
+ centerValueFont: {
+ family: 'Roboto',
+ size: 24,
+ sizeUnit: 'px',
+ style: 'normal',
+ weight: '500',
+ lineHeight: '32px'
+ },
+ centerValueColor: constantColor('rgba(0, 0, 0, 0.87)'),
+ ticksColor: 'rgba(0, 0, 0, 0.12)',
+ arrowColor: 'rgba(0, 0, 0, 0.87)',
+ directionalNamesElseDegrees: true,
+ majorTicksColor: 'rgba(158, 158, 158, 1)',
+ majorTicksFont: {
+ family: 'Roboto',
+ size: 14,
+ sizeUnit: 'px',
+ style: 'normal',
+ weight: '500',
+ lineHeight: '20px'
+ },
+ minorTicksColor: 'rgba(0, 0, 0, 0.12)',
+ minorTicksFont: {
+ family: 'Roboto',
+ size: 14,
+ sizeUnit: 'px',
+ style: 'normal',
+ weight: '500',
+ lineHeight: '20px'
+ },
+ background: {
+ type: BackgroundType.color,
+ color: '#fff',
+ overlay: {
+ enabled: false,
+ color: 'rgba(255,255,255,0.72)',
+ blur: 3
+ }
+ }
+};
+
+export const windDirectionLabel = 'windDirection';
+export const centerValueLabel = 'centerValue';
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
index 30cc14aa36f..da1f42f1a2a 100644
--- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
@@ -58,6 +58,9 @@ import {
} from '@home/components/widget/lib/cards/aggregated-value-card-widget.component';
import { CountWidgetComponent } from '@home/components/widget/lib/count/count-widget.component';
import { BatteryLevelWidgetComponent } from '@home/components/widget/lib/indicator/battery-level-widget.component';
+import {
+ WindSpeedDirectionWidgetComponent
+} from '@home/components/widget/lib/weather/wind-speed-direction-widget.component';
@NgModule({
declarations:
@@ -92,7 +95,8 @@ import { BatteryLevelWidgetComponent } from '@home/components/widget/lib/indicat
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent,
CountWidgetComponent,
- BatteryLevelWidgetComponent
+ BatteryLevelWidgetComponent,
+ WindSpeedDirectionWidgetComponent
],
imports: [
CommonModule,
@@ -131,7 +135,8 @@ import { BatteryLevelWidgetComponent } from '@home/components/widget/lib/indicat
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent,
CountWidgetComponent,
- BatteryLevelWidgetComponent
+ BatteryLevelWidgetComponent,
+ WindSpeedDirectionWidgetComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }
diff --git a/ui-ngx/src/app/shared/models/widget-settings.models.ts b/ui-ngx/src/app/shared/models/widget-settings.models.ts
index 2769a635e40..d2d6aaa610f 100644
--- a/ui-ngx/src/app/shared/models/widget-settings.models.ts
+++ b/ui-ngx/src/app/shared/models/widget-settings.models.ts
@@ -426,6 +426,37 @@ export const getDataKey = (datasources?: Datasource[]): DataKey => {
return null;
};
+export const getDataKeyByLabel = (datasources: Datasource[], label: string): DataKey => {
+ if (datasources && datasources.length) {
+ const dataKeys = datasources[0].dataKeys;
+ if (dataKeys && dataKeys.length) {
+ return dataKeys.find(k => k.label === label);
+ }
+ }
+ return null;
+};
+
+export const updateDataKeyByLabel = (datasources: Datasource[], dataKey: DataKey, label: string): void => {
+ if (datasources && datasources.length) {
+ let dataKeys = datasources[0].dataKeys;
+ if (!dataKeys) {
+ dataKeys = [];
+ datasources[0].dataKeys = dataKeys;
+ }
+ const existingIndex = dataKeys.findIndex(k => k.label === label || k === dataKey);
+ if (dataKey) {
+ dataKey.label = label;
+ if (existingIndex > -1) {
+ dataKeys[existingIndex] = dataKey;
+ } else {
+ dataKeys.push(dataKey);
+ }
+ } else if (existingIndex > -1) {
+ dataKeys.splice(existingIndex, 1);
+ }
+ }
+};
+
export const getAlarmFilterConfig = (datasources?: Datasource[]): AlarmFilterConfig => {
if (datasources && datasources.length) {
const config = datasources[0].alarmFilterConfig;
@@ -467,6 +498,16 @@ export const getSingleTsValue = (data: Array): [number, any] =>
return null;
};
+export const getSingleTsValueByDataKey = (data: Array, dataKey: DataKey): [number, any] => {
+ if (data.length) {
+ const dsData = data.find(d => d.dataKey === dataKey);
+ if (dsData?.data?.length) {
+ return dsData.data[0];
+ }
+ }
+ return null;
+};
+
export const getLatestSingleTsValue = (data: Array): [number, any] => {
if (data.length) {
const dsData = data[0];
diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json
index 6342357da1c..a56ee4e4564 100644
--- a/ui-ngx/src/assets/locale/locale.constant-en_US.json
+++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json
@@ -6144,6 +6144,27 @@
"table-tabs": "Table tabs",
"show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode"
},
+ "wind-speed-direction": {
+ "layout": "Layout",
+ "layout-default": "Default",
+ "layout-advanced": "Advanced",
+ "layout-simplified": "Simplified",
+ "values": "Values",
+ "wind-direction": "Wind direction",
+ "center-value": "Center value",
+ "icon": "Icon",
+ "arrow": "Arrow",
+ "ticks": "Ticks",
+ "labels-type": "Labels type",
+ "directional-names": "Directional names",
+ "degrees": "Degrees",
+ "major-ticks": "Major ticks",
+ "minor-ticks": "Minor ticks",
+ "wind-speed-direction-card-style": "Wind speed and direction card style",
+ "ticks-color": "Ticks color",
+ "ticks-labels-type": "Ticks labels type",
+ "arrow-color": "Arrow color"
+ },
"value-source": {
"value-source": "Value source",
"predefined-value": "Predefined value",
diff --git a/ui-ngx/src/assets/widget/wind-speed-direction/advanced-layout.svg b/ui-ngx/src/assets/widget/wind-speed-direction/advanced-layout.svg
new file mode 100644
index 00000000000..701f0b0aa75
--- /dev/null
+++ b/ui-ngx/src/assets/widget/wind-speed-direction/advanced-layout.svg
@@ -0,0 +1,187 @@
+
+
diff --git a/ui-ngx/src/assets/widget/wind-speed-direction/default-layout.svg b/ui-ngx/src/assets/widget/wind-speed-direction/default-layout.svg
new file mode 100644
index 00000000000..86f6463b8a2
--- /dev/null
+++ b/ui-ngx/src/assets/widget/wind-speed-direction/default-layout.svg
@@ -0,0 +1,167 @@
+
+
diff --git a/ui-ngx/src/assets/widget/wind-speed-direction/simplified-layout.svg b/ui-ngx/src/assets/widget/wind-speed-direction/simplified-layout.svg
new file mode 100644
index 00000000000..e881c8f150b
--- /dev/null
+++ b/ui-ngx/src/assets/widget/wind-speed-direction/simplified-layout.svg
@@ -0,0 +1,153 @@
+
+
diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss
index 77a31eeb2ec..82794cbbc55 100644
--- a/ui-ngx/src/form.scss
+++ b/ui-ngx/src/form.scss
@@ -204,6 +204,12 @@
.mat-slide:only-child {
margin: 8px 0;
}
+ .tb-required::after {
+ font-size: 13px;
+ color: rgba(0, 0, 0, .54);
+ vertical-align: top;
+ content: " *";
+ }
}
.tb-form-panel, .tb-form-row {
@@ -227,8 +233,10 @@
&:before {
opacity: 0;
}
- .mdc-line-ripple::before {
- border-bottom-color: rgba(0, 0, 0, 0.12);
+ &:not(.mdc-text-field--invalid) {
+ .mdc-line-ripple::before {
+ border-bottom-color: rgba(0, 0, 0, 0.12);
+ }
}
}
.mat-mdc-form-field-focus-overlay {
diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock
index 33eb78ed02d..37ae52a5ecf 100644
--- a/ui-ngx/yarn.lock
+++ b/ui-ngx/yarn.lock
@@ -2747,6 +2747,11 @@
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
+"@svgdotjs/svg.js@^3.2.0":
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/@svgdotjs/svg.js/-/svg.js-3.2.0.tgz#6baa8cef6778a93818ac18faa2055222e60aa644"
+ integrity sha512-Tr8p+QVP7y+QT1GBlq1Tt57IvedVH8zCPoYxdHLX0Oof3a/PqnC/tXAkVufv1JQJfsDHlH/UrjcDfgxSofqSNA==
+
"@tinymce/tinymce-angular@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@tinymce/tinymce-angular/-/tinymce-angular-7.0.0.tgz#010de497d5774a8bdc5d5936bf4fb976adf05f56"