@@ -33,12 +31,12 @@
-
+
- {{ 'debug-settings.all-messages' | translate: { time: (isDebugAllActive$ | async) && !allEnabled ? (allEnabledUntil | durationLeft) : (maxDebugModeDuration | milliSecondsToTimeString: true : true) } }}
+ {{ 'debug-settings.all-messages' | translate: { time: (isDebugAllActive$ | async) && !allEnabled && debugAllControl.untouched ? (allEnabledUntil | durationLeft) : (maxDebugModeDuration | milliSecondsToTimeString: true : true) } }}
-
-
+ @if (widgetActionFormGroup.get('actionSourceId').value === 'headerButton') {
+
+ } @else {
+
+ }
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts
index ab2b8870d0..b77b5c860f 100644
--- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts
@@ -14,7 +14,7 @@
/// limitations under the License.
///
-import { Component, Inject, OnDestroy, OnInit, SkipSelf, ViewChild } from '@angular/core';
+import { Component, DestroyRef, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
@@ -28,7 +28,6 @@ import {
ValidatorFn,
Validators
} from '@angular/forms';
-import { Subject } from 'rxjs';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import {
@@ -44,14 +43,17 @@ import {
defaultWidgetAction,
WidgetActionSource,
WidgetActionType,
+ WidgetHeaderActionButtonType,
+ WidgetHeaderActionButtonTypes,
+ widgetHeaderActionButtonTypeTranslationMap,
widgetType
} from '@shared/models/widget.models';
-import { takeUntil } from 'rxjs/operators';
import { CustomActionEditorCompleter } from '@home/components/widget/lib/settings/common/action/custom-action.models';
import { WidgetService } from '@core/http/widget.service';
import { isDefinedAndNotNull, isNotEmptyStr } from '@core/utils';
import { MatSelect } from '@angular/material/select';
import { TranslateService } from '@ngx-translate/core';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export interface WidgetActionDialogData {
isAdd: boolean;
@@ -69,9 +71,7 @@ export interface WidgetActionDialogData {
styleUrls: []
})
export class WidgetActionDialogComponent extends DialogComponent implements OnInit, OnDestroy, ErrorStateMatcher {
-
- private destroy$ = new Subject();
+ WidgetActionDescriptorInfo> implements OnInit, ErrorStateMatcher {
widgetActionFormGroup: FormGroup;
@@ -87,6 +87,10 @@ export class WidgetActionDialogComponent extends DialogComponent = [];
usedCellClickColumns: Array = [];
+ widgetHeaderActionButtonType = WidgetHeaderActionButtonType
+ widgetHeaderActionButtonTypes = WidgetHeaderActionButtonTypes;
+ widgetHeaderActionButtonTypeTranslationMap = widgetHeaderActionButtonTypeTranslationMap;
+
@ViewChild('columnIndexSelect') columnIndexSelect: MatSelect;
columnIndexPlaceholderText = this.translate.instant('widget-config.select-column-index');
@@ -98,7 +102,8 @@ export class WidgetActionDialogComponent extends DialogComponent,
public fb: FormBuilder,
- private translate: TranslateService) {
+ private translate: TranslateService,
+ private destroyRef: DestroyRef) {
super(store, router, dialogRef);
this.isAdd = data.isAdd;
if (this.isAdd) {
@@ -122,14 +127,25 @@ export class WidgetActionDialogComponent extends DialogComponent {
this.widgetActionFormGroup.get('name').updateValueAndValidity();
this.updateShowWidgetActionForm();
@@ -139,12 +155,26 @@ export class WidgetActionDialogComponent extends DialogComponent {
this.updateShowWidgetActionForm();
});
+ this.widgetActionFormGroup.get('buttonType').valueChanges.pipe(
+ takeUntilDestroyed(this.destroyRef)
+ ).subscribe(() => this.widgetHeaderButtonValidators());
setTimeout(() => {
if (this.action?.actionSourceId === 'cellClick') {
this.widgetActionFormGroup.get('columnIndex').enable();
@@ -156,10 +186,31 @@ export class WidgetActionDialogComponent extends DialogComponent {
this.latestChartOption = {
tooltip: {
trigger: this.settings.showTooltip ? 'item' : 'none',
- confine: false,
- appendTo: 'body',
+ confine: true,
formatter: (params: CallbackDataParams) =>
this.settings.showTooltip
? latestChartTooltipFormatter(this.renderer, this.settings, params, this.units, this.total, this.dataItems)
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts
index 9086bb1896..ad121a1eb0 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts
@@ -161,6 +161,8 @@ export class TbTimeSeriesChart {
private latestData: FormattedData[] = [];
+ private onParentScroll = this._onParentScroll.bind(this);
+
yMin$ = this.yMinSubject.asObservable();
yMax$ = this.yMaxSubject.asObservable();
@@ -358,6 +360,7 @@ export class TbTimeSeriesChart {
this.yMinSubject.complete();
this.yMaxSubject.complete();
this.darkModeObserver?.disconnect();
+ this.ctx.dashboard.gridster.el.removeEventListener('scroll', this.onParentScroll);
}
public resize(): void {
@@ -611,6 +614,7 @@ export class TbTimeSeriesChart {
this.timeSeriesChart = echarts.init(this.chartElement, null, {
renderer: 'svg'
});
+ this.ctx.dashboard.gridster.el.addEventListener('scroll', this.onParentScroll);
this.timeSeriesChartOptions = {
darkMode: this.darkMode,
backgroundColor: 'transparent',
@@ -837,6 +841,14 @@ export class TbTimeSeriesChart {
return this.settings.dataZoom ? 45 : 5;
}
+ private _onParentScroll() {
+ if (this.timeSeriesChart) {
+ this.timeSeriesChart.dispatchAction({
+ type: 'hideTip'
+ });
+ }
+ }
+
private onResize() {
const shapeWidth = this.chartElement.offsetWidth;
const shapeHeight = this.chartElement.offsetHeight;
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts
index 3ae61642ce..63d77c48b2 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts
@@ -15,15 +15,24 @@
///
import {
- DataLayerColorSettings, DataLayerColorType,
- DataLayerPatternSettings, DataLayerPatternType,
- MapDataLayerSettings, MapDataLayerType, mapDataSourceSettingsToDatasource,
- MapStringFunction, MapType,
+ DataLayerColorSettings,
+ DataLayerColorType,
+ DataLayerPatternSettings,
+ DataLayerPatternType,
+ MapDataLayerSettings,
+ MapDataLayerType,
+ mapDataSourceSettingsToDatasource,
+ MapStringFunction,
+ MapType,
TbMapDatasource
} from '@shared/models/widget/maps/map.models';
import {
createLabelFromPattern,
- guid, isDefined,
+ guid,
+ isDefined,
+ isDefinedAndNotNull,
+ isNumber,
+ isNumeric,
mergeDeepIgnoreArray,
parseTbFunction,
safeExecuteTbFunction
@@ -32,10 +41,11 @@ import L from 'leaflet';
import { CompiledTbFunction } from '@shared/models/js-function.models';
import { forkJoin, Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
-import { FormattedData } from '@shared/models/widget.models';
+import { DataKey, FormattedData } from '@shared/models/widget.models';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { TbMap } from '@home/components/widget/lib/maps/map';
import { WidgetContext } from '@home/models/widget-component.models';
+import { ColorRange } from '@shared/models/widget-settings.models';
export class DataLayerPatternProcessor {
@@ -77,22 +87,26 @@ export class DataLayerColorProcessor {
private colorFunction: CompiledTbFunction;
private color: string;
+ private rangeKey: DataKey;
+ private range: ColorRange[];
constructor(private dataLayer: TbMapDataLayer,
private settings: DataLayerColorSettings) {}
public setup(): Observable {
this.color = this.settings.color;
- if (this.settings.type === DataLayerColorType.function) {
+ if (this.settings.type === DataLayerColorType.range) {
+ this.rangeKey = this.settings.rangeKey;
+ this.range = this.settings.range;
+ } else if (this.settings.type === DataLayerColorType.function) {
return parseTbFunction(this.dataLayer.getCtx().http, this.settings.colorFunction, ['data', 'dsData']).pipe(
map((parsed) => {
this.colorFunction = parsed;
return null;
})
);
- } else {
- return of(null)
}
+ return of(null)
}
public processColor(data: FormattedData, dsData: FormattedData[]): string {
@@ -102,12 +116,33 @@ export class DataLayerColorProcessor {
if (!color) {
color = this.color;
}
+ } else if (this.settings.type === DataLayerColorType.range) {
+ color = this.color;
+ if (this.rangeKey && this.range?.length) {
+ const value = data[this.rangeKey.label];
+ if (isDefinedAndNotNull(value) && isNumeric(value)) {
+ const num = Number(value);
+ for (const range of this.range) {
+ if (DataLayerColorProcessor.constantRange(range) && range.from === num) {
+ color = range.color;
+ break;
+ } else if ((!isNumber(range.from) || num >= range.from) && (!isNumber(range.to) || num < range.to)) {
+ color = range.color;
+ break;
+ }
+ }
+ }
+ }
} else {
color = this.color;
}
return color;
}
+ static constantRange(range: ColorRange): boolean {
+ return isNumber(range.from) && isNumber(range.to) && range.from === range.to;
+ }
+
}
export abstract class TbDataLayerItem {
@@ -137,7 +172,7 @@ export abstract class TbMapDataLayer {
this.datasource = mapDataSourceSettingsToDatasource(this.settings);
this.datasource.dataKeys = this.settings.additionalDataKeys ? [...this.settings.additionalDataKeys] : [];
+ const colorRangeKeys = this.allColorSettings().filter(settings => settings.type === DataLayerColorType.range && settings.rangeKey)
+ .map(settings => settings.rangeKey);
+ this.datasource.dataKeys.push(...colorRangeKeys);
this.mapDataId = this.datasource.mapDataIds[0];
this.datasource = this.setupDatasource(this.datasource);
return forkJoin(
@@ -243,6 +281,10 @@ export abstract class TbMapDataLayer): boolean {
+ return data.$datasource.mapDataIds.includes(this.mapDataId);
+ }
+
protected createDataLayerContainer(): L.FeatureGroup {
return L.featureGroup([], {snapIgnore: true});
}
@@ -251,6 +293,10 @@ export abstract class TbMapDataLayer {
@@ -218,8 +221,7 @@ abstract class MarkerIconProcessor {
abstract class BaseColorMarkerShapeProcessor extends MarkerIconProcessor {
- private markerColorFunction: CompiledTbFunction;
-
+ private colorProcessor: DataLayerColorProcessor;
private defaultMarkerIconInfo: MarkerIconInfo;
protected constructor(protected dataProcessor: MarkerDataProcessor,
@@ -229,40 +231,28 @@ abstract class BaseColorMarkerShapeProcessor
public setup(): Observable {
const colorSettings = this.settings.color;
- if (colorSettings.type === DataLayerColorType.function) {
- return parseTbFunction(this.dataProcessor.dataLayer.getCtx().http, colorSettings.colorFunction, ['data', 'dsData']).pipe(
- map((parsed) => {
- this.markerColorFunction = parsed;
- return null;
- })
- );
- } else {
+ this.colorProcessor = new DataLayerColorProcessor(this.dataProcessor.dataLayer, colorSettings);
+ const setup$: Observable[] = [this.colorProcessor.setup()];
+ if (colorSettings.type === DataLayerColorType.constant) {
const color = tinycolor(colorSettings.color);
- return this.createMarkerShape(color, 0, this.settings.size).pipe(
- map((info) => {
- this.defaultMarkerIconInfo = info;
- return null;
- }
- ));
+ setup$.push(
+ this.createMarkerShape(color, 0, this.settings.size).pipe(
+ map((info) => {
+ this.defaultMarkerIconInfo = info;
+ return null;
+ }))
+ );
}
+ return forkJoin(setup$).pipe(map(() => null));
}
public createMarkerIcon(data: FormattedData, dsData: FormattedData[], rotationAngle = 0): Observable {
const colorSettings = this.settings.color;
- let color: tinycolor.Instance;
- if (colorSettings.type === DataLayerColorType.function) {
- const functionColor = safeExecuteTbFunction(this.markerColorFunction, [data, dsData]);
- if (isDefinedAndNotNull(functionColor)) {
- color = tinycolor(functionColor);
- } else {
- color = tinycolor(colorSettings.color);
- }
- return this.createMarkerShape(color, rotationAngle, this.settings.size);
- } else if (rotationAngle === 0) {
+ if (colorSettings.type === DataLayerColorType.constant && rotationAngle === 0) {
return of(this.defaultMarkerIconInfo);
} else {
- color = tinycolor(colorSettings.color);
- return this.createMarkerShape(color, rotationAngle, this.settings.size);
+ const color = this.colorProcessor.processColor(data, dsData);
+ return this.createMarkerShape(tinycolor(color), rotationAngle, this.settings.size);
}
}
@@ -639,6 +629,15 @@ export class TbMarkersDataLayer extends TbLatestMapDataLayer): Partial {
return defaultBaseMarkersDataLayerSettings(map.type());
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts
index ec6c5aba80..836b0e7947 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts
@@ -14,7 +14,7 @@
/// limitations under the License.
///
-import { ShapeDataLayerSettings, TbMapDatasource } from '@shared/models/widget/maps/map.models';
+import { DataLayerColorSettings, ShapeDataLayerSettings, TbMapDatasource } from '@shared/models/widget/maps/map.models';
import L from 'leaflet';
import { TbMap } from '@home/components/widget/lib/maps/map';
import { forkJoin, Observable } from 'rxjs';
@@ -45,6 +45,10 @@ export abstract class TbShapesDataLayer {
this.fillColorProcessor = new DataLayerColorProcessor(this, this.settings.fillColor);
this.strokeColorProcessor = new DataLayerColorProcessor(this, this.settings.strokeColor);
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts
index d6cbe5e4c9..e598685786 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts
@@ -16,16 +16,16 @@
import {
calculateInterpolationRatio,
- calculateLastPoints,
+ calculateLastPoints, DataLayerColorSettings, DataLayerColorType,
defaultBaseTripsDataLayerSettings,
findRotationAngle,
interpolateLineSegment,
- MapDataLayerType,
+ MapDataLayerType, MarkerType,
TbMapDatasource,
TripsDataLayerSettings
} from '@shared/models/widget/maps/map.models';
import { forkJoin, Observable } from 'rxjs';
-import { FormattedData, WidgetActionType } from '@shared/models/widget.models';
+import { DataKey, FormattedData, WidgetActionType } from '@shared/models/widget.models';
import { map } from 'rxjs/operators';
import L from 'leaflet';
import { deepClone, isDefined, isUndefined } from '@core/utils';
@@ -530,9 +530,14 @@ export class TbTripsDataLayer extends TbMapDataLayer settings.type === DataLayerColorType.range && settings.rangeKey)
+ .map(settings => settings.rangeKey);
if (this.settings.additionalDataKeys?.length) {
- const tsKeys = this.settings.additionalDataKeys.filter(key => key.type === DataKeyType.timeseries);
- const latestKeys = this.settings.additionalDataKeys.filter(key => key.type !== DataKeyType.timeseries);
+ additionalKeys.push(...this.settings.additionalDataKeys);
+ }
+ if (additionalKeys.length) {
+ const tsKeys = additionalKeys.filter(key => key.type === DataKeyType.timeseries);
+ const latestKeys = additionalKeys.filter(key => key.type !== DataKeyType.timeseries);
datasource.dataKeys.push(...tsKeys);
if (latestKeys.length) {
datasource.latestDataKeys = latestKeys;
@@ -541,6 +546,24 @@ export class TbTripsDataLayer extends TbMapDataLayer): Partial {
return defaultBaseTripsDataLayerSettings(map.type());
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts
index 06d49d0343..79e2f11ce6 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts
@@ -17,6 +17,7 @@
import L, { TB } from 'leaflet';
import { guid, isNotEmptyStr } from '@core/utils';
import 'leaflet-providers';
+import { Map as MapLibreGLMap, LngLat as MapLibreGLLngLat } from 'maplibre-gl';
import '@geoman-io/leaflet-geoman-free';
import 'leaflet.markercluster';
import { MatIconRegistry } from '@angular/material/icon';
@@ -26,7 +27,7 @@ import { of } from 'rxjs';
L.MarkerCluster = L.MarkerCluster.mergeOptions({ pmIgnore: true });
-class SidebarControl extends L.Control {
+class SidebarControl extends L.Control implements L.TB.SidebarControl {
private readonly sidebar: JQuery;
@@ -94,7 +95,7 @@ class SidebarControl extends L.Control {
}
}
-class SidebarPaneControl extends L.Control {
+class SidebarPaneControl extends L.Control implements L.TB.SidebarPaneControl {
private button: JQuery;
private $ui: JQuery;
@@ -154,7 +155,7 @@ class SidebarPaneControl extends L.Contr
}
}
-class LayersControl extends SidebarPaneControl {
+class LayersControl extends SidebarPaneControl implements L.TB.LayersControl {
constructor(options: TB.LayersControlOptions) {
super(options);
}
@@ -211,15 +212,16 @@ class LayersControl extends SidebarPaneControl {
input.on('click', (e: JQuery.MouseEventBase) => {
e.stopPropagation();
- layers.forEach((other) => {
- if (other.layer === layerData.layer) {
- map.addLayer(other.layer);
- map.attributionControl.setPrefix(other.attributionPrefix);
- } else {
- map.removeLayer(other.layer);
- }
- });
- map.fire('baselayerchange', { layer: layerData.layer });
+ if (!map.hasLayer(layerData.layer)) {
+ map.addLayer(layerData.layer);
+ map.attributionControl.setPrefix(layerData.attributionPrefix);
+ layers.forEach((other) => {
+ if (other.layer !== layerData.layer) {
+ map.removeLayer(other.layer);
+ }
+ });
+ map.fire('baselayerchange', { layer: layerData.layer });
+ }
});
item.on('dblclick', (e) => {
@@ -233,12 +235,12 @@ class LayersControl extends SidebarPaneControl