UI: Maps - add color range type to color settings.

This commit is contained in:
Igor Kulikov 2025-03-13 12:45:00 +02:00
parent 480e6a4935
commit 2d86e816f2
14 changed files with 314 additions and 65 deletions

View File

@ -15,15 +15,24 @@
/// ///
import { import {
DataLayerColorSettings, DataLayerColorType, DataLayerColorSettings,
DataLayerPatternSettings, DataLayerPatternType, DataLayerColorType,
MapDataLayerSettings, MapDataLayerType, mapDataSourceSettingsToDatasource, DataLayerPatternSettings,
MapStringFunction, MapType, DataLayerPatternType,
MapDataLayerSettings,
MapDataLayerType,
mapDataSourceSettingsToDatasource,
MapStringFunction,
MapType,
TbMapDatasource TbMapDatasource
} from '@shared/models/widget/maps/map.models'; } from '@shared/models/widget/maps/map.models';
import { import {
createLabelFromPattern, createLabelFromPattern,
guid, isDefined, guid,
isDefined,
isDefinedAndNotNull,
isNumber,
isNumeric,
mergeDeepIgnoreArray, mergeDeepIgnoreArray,
parseTbFunction, parseTbFunction,
safeExecuteTbFunction safeExecuteTbFunction
@ -32,10 +41,11 @@ import L from 'leaflet';
import { CompiledTbFunction } from '@shared/models/js-function.models'; import { CompiledTbFunction } from '@shared/models/js-function.models';
import { forkJoin, Observable, of } from 'rxjs'; import { forkJoin, Observable, of } from 'rxjs';
import { map } from 'rxjs/operators'; 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 { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { TbMap } from '@home/components/widget/lib/maps/map'; import { TbMap } from '@home/components/widget/lib/maps/map';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { ColorRange } from '@shared/models/widget-settings.models';
export class DataLayerPatternProcessor { export class DataLayerPatternProcessor {
@ -77,22 +87,26 @@ export class DataLayerColorProcessor {
private colorFunction: CompiledTbFunction<MapStringFunction>; private colorFunction: CompiledTbFunction<MapStringFunction>;
private color: string; private color: string;
private rangeKey: DataKey;
private range: ColorRange[];
constructor(private dataLayer: TbMapDataLayer, constructor(private dataLayer: TbMapDataLayer,
private settings: DataLayerColorSettings) {} private settings: DataLayerColorSettings) {}
public setup(): Observable<void> { public setup(): Observable<void> {
this.color = this.settings.color; 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<MapStringFunction>(this.dataLayer.getCtx().http, this.settings.colorFunction, ['data', 'dsData']).pipe( return parseTbFunction<MapStringFunction>(this.dataLayer.getCtx().http, this.settings.colorFunction, ['data', 'dsData']).pipe(
map((parsed) => { map((parsed) => {
this.colorFunction = parsed; this.colorFunction = parsed;
return null; return null;
}) })
); );
} else {
return of(null)
} }
return of(null)
} }
public processColor(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): string { public processColor(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): string {
@ -102,12 +116,33 @@ export class DataLayerColorProcessor {
if (!color) { if (!color) {
color = this.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 { } else {
color = this.color; color = this.color;
} }
return color; return color;
} }
static constantRange(range: ColorRange): boolean {
return isNumber(range.from) && isNumber(range.to) && range.from === range.to;
}
} }
export abstract class TbDataLayerItem<S extends MapDataLayerSettings = MapDataLayerSettings, D extends TbMapDataLayer = TbMapDataLayer, L extends L.Layer = L.Layer> { export abstract class TbDataLayerItem<S extends MapDataLayerSettings = MapDataLayerSettings, D extends TbMapDataLayer = TbMapDataLayer, L extends L.Layer = L.Layer> {
@ -169,6 +204,9 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings = MapDataLay
public setup(): Observable<any> { public setup(): Observable<any> {
this.datasource = mapDataSourceSettingsToDatasource(this.settings); this.datasource = mapDataSourceSettingsToDatasource(this.settings);
this.datasource.dataKeys = this.settings.additionalDataKeys ? [...this.settings.additionalDataKeys] : []; 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.mapDataId = this.datasource.mapDataIds[0];
this.datasource = this.setupDatasource(this.datasource); this.datasource = this.setupDatasource(this.datasource);
return forkJoin( return forkJoin(
@ -251,6 +289,10 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings = MapDataLay
return datasource; return datasource;
} }
protected allColorSettings(): DataLayerColorSettings[] {
return [];
}
protected onDataLayerEnabled(): void {} protected onDataLayerEnabled(): void {}
protected onDataLayerDisabled(): void {} protected onDataLayerDisabled(): void {}

View File

@ -17,11 +17,12 @@
import { import {
BaseMarkerShapeSettings, BaseMarkerShapeSettings,
ClusterMarkerColorFunction, ClusterMarkerColorFunction,
DataLayerColorSettings,
DataLayerColorType, DataLayerColorType,
defaultBaseMarkersDataLayerSettings, defaultBaseMarkersDataLayerSettings,
isValidLatLng, isValidLatLng,
loadImageWithAspect, MapDataLayerType, loadImageWithAspect,
MapStringFunction, MapDataLayerType,
MapType, MapType,
MarkerIconInfo, MarkerIconInfo,
MarkerIconSettings, MarkerIconSettings,
@ -53,17 +54,19 @@ import { ImagePipe } from '@shared/pipe/image.pipe';
import { TbMap } from '@home/components/widget/lib/maps/map'; import { TbMap } from '@home/components/widget/lib/maps/map';
import { import {
createColorMarkerIconElement, createColorMarkerIconElement,
createColorMarkerShapeURI, MarkerIconContainer, createColorMarkerShapeURI,
MarkerIconContainer,
MarkerShape MarkerShape
} from '@shared/models/widget/maps/marker-shape.models'; } from '@shared/models/widget/maps/marker-shape.models';
import { MatIconRegistry } from '@angular/material/icon'; import { MatIconRegistry } from '@angular/material/icon';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { import {
TbLatestDataLayerItem, TbLatestDataLayerItem,
TbLatestMapDataLayer, UnplacedMapDataItem TbLatestMapDataLayer,
UnplacedMapDataItem
} from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer'; } from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer';
import { TbImageMap } from '@home/components/widget/lib/maps/image-map'; import { TbImageMap } from '@home/components/widget/lib/maps/image-map';
import { TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; import { DataLayerColorProcessor, TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-data-layer';
export class MarkerDataProcessor<S extends MarkersDataLayerSettings = MarkersDataLayerSettings> { export class MarkerDataProcessor<S extends MarkersDataLayerSettings = MarkersDataLayerSettings> {
@ -218,8 +221,7 @@ abstract class MarkerIconProcessor<S> {
abstract class BaseColorMarkerShapeProcessor<S extends BaseMarkerShapeSettings> extends MarkerIconProcessor<S> { abstract class BaseColorMarkerShapeProcessor<S extends BaseMarkerShapeSettings> extends MarkerIconProcessor<S> {
private markerColorFunction: CompiledTbFunction<MapStringFunction>; private colorProcessor: DataLayerColorProcessor;
private defaultMarkerIconInfo: MarkerIconInfo; private defaultMarkerIconInfo: MarkerIconInfo;
protected constructor(protected dataProcessor: MarkerDataProcessor, protected constructor(protected dataProcessor: MarkerDataProcessor,
@ -229,40 +231,28 @@ abstract class BaseColorMarkerShapeProcessor<S extends BaseMarkerShapeSettings>
public setup(): Observable<void> { public setup(): Observable<void> {
const colorSettings = this.settings.color; const colorSettings = this.settings.color;
if (colorSettings.type === DataLayerColorType.function) { this.colorProcessor = new DataLayerColorProcessor(this.dataProcessor.dataLayer, colorSettings);
return parseTbFunction<MapStringFunction>(this.dataProcessor.dataLayer.getCtx().http, colorSettings.colorFunction, ['data', 'dsData']).pipe( const setup$: Observable<void>[] = [this.colorProcessor.setup()];
map((parsed) => { if (colorSettings.type === DataLayerColorType.constant) {
this.markerColorFunction = parsed;
return null;
})
);
} else {
const color = tinycolor(colorSettings.color); const color = tinycolor(colorSettings.color);
return this.createMarkerShape(color, 0, this.settings.size).pipe( setup$.push(
this.createMarkerShape(color, 0, this.settings.size).pipe(
map((info) => { map((info) => {
this.defaultMarkerIconInfo = info; this.defaultMarkerIconInfo = info;
return null; return null;
}))
);
} }
)); return forkJoin(setup$).pipe(map(() => null));
}
} }
public createMarkerIcon(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[], rotationAngle = 0): Observable<MarkerIconInfo> { public createMarkerIcon(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[], rotationAngle = 0): Observable<MarkerIconInfo> {
const colorSettings = this.settings.color; const colorSettings = this.settings.color;
let color: tinycolor.Instance; if (colorSettings.type === DataLayerColorType.constant && rotationAngle === 0) {
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) {
return of(this.defaultMarkerIconInfo); return of(this.defaultMarkerIconInfo);
} else { } else {
color = tinycolor(colorSettings.color); const color = this.colorProcessor.processColor(data, dsData);
return this.createMarkerShape(color, rotationAngle, this.settings.size); return this.createMarkerShape(tinycolor(color), rotationAngle, this.settings.size);
} }
} }
@ -639,6 +629,15 @@ export class TbMarkersDataLayer extends TbLatestMapDataLayer<MarkersDataLayerSet
return datasource; return datasource;
} }
protected allColorSettings(): DataLayerColorSettings[] {
if (this.settings.markerType === MarkerType.shape) {
return [this.settings.markerShape.color];
} else if (this.settings.markerType === MarkerType.icon) {
return [this.settings.markerIcon.color];
}
return [];
}
protected defaultBaseSettings(map: TbMap<any>): Partial<MarkersDataLayerSettings> { protected defaultBaseSettings(map: TbMap<any>): Partial<MarkersDataLayerSettings> {
return defaultBaseMarkersDataLayerSettings(map.type()); return defaultBaseMarkersDataLayerSettings(map.type());
} }

View File

@ -14,7 +14,7 @@
/// limitations under the License. /// 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 L from 'leaflet';
import { TbMap } from '@home/components/widget/lib/maps/map'; import { TbMap } from '@home/components/widget/lib/maps/map';
import { forkJoin, Observable } from 'rxjs'; import { forkJoin, Observable } from 'rxjs';
@ -45,6 +45,10 @@ export abstract class TbShapesDataLayer<S extends ShapeDataLayerSettings, L exte
}; };
} }
protected allColorSettings(): DataLayerColorSettings[] {
return [this.settings.fillColor, this.settings.strokeColor];
}
protected doSetup(): Observable<any> { protected doSetup(): Observable<any> {
this.fillColorProcessor = new DataLayerColorProcessor(this, this.settings.fillColor); this.fillColorProcessor = new DataLayerColorProcessor(this, this.settings.fillColor);
this.strokeColorProcessor = new DataLayerColorProcessor(this, this.settings.strokeColor); this.strokeColorProcessor = new DataLayerColorProcessor(this, this.settings.strokeColor);

View File

@ -16,16 +16,16 @@
import { import {
calculateInterpolationRatio, calculateInterpolationRatio,
calculateLastPoints, calculateLastPoints, DataLayerColorSettings, DataLayerColorType,
defaultBaseTripsDataLayerSettings, defaultBaseTripsDataLayerSettings,
findRotationAngle, findRotationAngle,
interpolateLineSegment, interpolateLineSegment,
MapDataLayerType, MapDataLayerType, MarkerType,
TbMapDatasource, TbMapDatasource,
TripsDataLayerSettings TripsDataLayerSettings
} from '@shared/models/widget/maps/map.models'; } from '@shared/models/widget/maps/map.models';
import { forkJoin, Observable } from 'rxjs'; 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 { map } from 'rxjs/operators';
import L from 'leaflet'; import L from 'leaflet';
import { deepClone, isDefined, isUndefined } from '@core/utils'; import { deepClone, isDefined, isUndefined } from '@core/utils';
@ -530,9 +530,14 @@ export class TbTripsDataLayer extends TbMapDataLayer<TripsDataLayerSettings, TbT
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
datasource.dataKeys = [this.settings.xKey, this.settings.yKey]; datasource.dataKeys = [this.settings.xKey, this.settings.yKey];
const additionalKeys = this.allColorSettings().filter(settings => settings.type === DataLayerColorType.range && settings.rangeKey)
.map(settings => settings.rangeKey);
if (this.settings.additionalDataKeys?.length) { if (this.settings.additionalDataKeys?.length) {
const tsKeys = this.settings.additionalDataKeys.filter(key => key.type === DataKeyType.timeseries); additionalKeys.push(...this.settings.additionalDataKeys);
const latestKeys = this.settings.additionalDataKeys.filter(key => key.type !== DataKeyType.timeseries); }
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); datasource.dataKeys.push(...tsKeys);
if (latestKeys.length) { if (latestKeys.length) {
datasource.latestDataKeys = latestKeys; datasource.latestDataKeys = latestKeys;
@ -541,6 +546,24 @@ export class TbTripsDataLayer extends TbMapDataLayer<TripsDataLayerSettings, TbT
return datasource; return datasource;
} }
protected allColorSettings(): DataLayerColorSettings[] {
const colorSettings: DataLayerColorSettings[] = [];
if (this.settings.showMarker) {
if (this.settings.markerType === MarkerType.shape) {
colorSettings.push(this.settings.markerShape.color);
} else if (this.settings.markerType === MarkerType.icon) {
colorSettings.push(this.settings.markerIcon.color);
}
}
if (this.settings.showPath) {
colorSettings.push(this.settings.pathStrokeColor);
}
if (this.settings.showPoints) {
colorSettings.push(this.settings.pointColor);
}
return colorSettings;
}
protected defaultBaseSettings(map: TbMap<any>): Partial<TripsDataLayerSettings> { protected defaultBaseSettings(map: TbMap<any>): Partial<TripsDataLayerSettings> {
return defaultBaseTripsDataLayerSettings(map.type()); return defaultBaseTripsDataLayerSettings(map.type());
} }

View File

@ -85,6 +85,10 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
@Input() @Input()
datasource: Datasource; datasource: Datasource;
@Input()
@coerceBoolean()
simpleRange = false;
@Input() @Input()
@coerceBoolean() @coerceBoolean()
advancedMode = false; advancedMode = false;
@ -133,7 +137,7 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
writeValue(value: any): void { writeValue(value: any): void {
if (value) { if (value) {
let rangeList: ColorRangeSettings = {}; let rangeList: ColorRangeSettings = {};
if (isUndefined(value?.advancedMode) && value?.length) { if (this.simpleRange || (isUndefined(value?.advancedMode) && value?.length)) {
rangeList.advancedMode = false; rangeList.advancedMode = false;
rangeList.range = value; rangeList.range = value;
} else { } else {
@ -229,7 +233,11 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
} }
updateModel() { updateModel() {
if (this.simpleRange) {
this.propagateChange(this.colorRangeListFormGroup.get('range').value);
} else {
this.propagateChange(this.colorRangeListFormGroup.value); this.propagateChange(this.colorRangeListFormGroup.value);
} }
}
} }

View File

@ -22,6 +22,9 @@
<tb-toggle-option [value]="DataLayerColorType.constant"> <tb-toggle-option [value]="DataLayerColorType.constant">
{{ 'widgets.maps.data-layer.color-type-constant' | translate }} {{ 'widgets.maps.data-layer.color-type-constant' | translate }}
</tb-toggle-option> </tb-toggle-option>
<tb-toggle-option [value]="DataLayerColorType.range">
{{ 'widgets.maps.data-layer.color-type-range' | translate }}
</tb-toggle-option>
<tb-toggle-option [value]="DataLayerColorType.function"> <tb-toggle-option [value]="DataLayerColorType.function">
{{ 'widgets.maps.data-layer.color-type-function' | translate }} {{ 'widgets.maps.data-layer.color-type-function' | translate }}
</tb-toggle-option> </tb-toggle-option>
@ -35,6 +38,35 @@
</div> </div>
<div class="tb-data-layer-color-settings-panel-body" [class.!hidden]="colorSettingsFormGroup.get('type').value !== DataLayerColorType.constant"> <div class="tb-data-layer-color-settings-panel-body" [class.!hidden]="colorSettingsFormGroup.get('type').value !== DataLayerColorType.constant">
</div> </div>
<div class="tb-data-layer-color-settings-panel-body" [class.!hidden]="colorSettingsFormGroup.get('type').value !== DataLayerColorType.range">
<tb-data-key-input
inlineField="false"
appearance="outline"
subscriptSizing="fixed"
label="{{ 'widgets.maps.data-layer.color-range-source-key' | translate }}"
required
requiredText="{{ 'widgets.maps.data-layer.color-range-source-key-required' }}"
[datasourceType]="dsType"
[entityAliasId]="dsEntityAliasId"
[deviceId]="dsDeviceId"
[aliasController]="context.aliasController"
[widgetType]="widgetType.latest"
[dataKeyType]="context.functionsOnly ? DataKeyType.function : null"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="context.callbacks"
[generateKey]="context.generateDataKey"
(keyEdit)="editRangeKey()"
formControlName="rangeKey">
</tb-data-key-input>
<div class="tb-form-panel stroked">
<div translate>widgets.maps.data-layer.color-range</div>
<tb-color-range-list class="tb-color-ranges-panel"
simpleRange
formControlName="range"
[popover]="popover">
</tb-color-range-list>
</div>
</div>
<div class="tb-data-layer-color-settings-panel-body" [class.!hidden]="colorSettingsFormGroup.get('type').value !== DataLayerColorType.function"> <div class="tb-data-layer-color-settings-panel-body" [class.!hidden]="colorSettingsFormGroup.get('type').value !== DataLayerColorType.function">
<ng-container *ngTemplateOutlet="function"></ng-container> <ng-container *ngTemplateOutlet="function"></ng-container>
</div> </div>

View File

@ -16,6 +16,8 @@
@import '../../../../../../../../../scss/constants'; @import '../../../../../../../../../scss/constants';
.tb-data-layer-color-settings-panel { .tb-data-layer-color-settings-panel {
--mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12);
--mat-form-field-trailing-icon-color: rgba(0,0,0,0.38);
width: 700px; width: 700px;
max-width: 90vw; max-width: 90vw;
min-height: 300px; min-height: 300px;
@ -50,4 +52,14 @@
justify-content: flex-end; justify-content: flex-end;
align-items: flex-end; align-items: flex-end;
} }
.tb-color-ranges-panel {
flex: 1;
min-height: 0;
gap: 16px;
display: flex;
flex-direction: column;
.tb-color-ranges {
--mat-icon-color: rgba(0, 0, 0, 0.38);
}
}
} }

View File

@ -17,12 +17,15 @@
import { Component, DestroyRef, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { Component, DestroyRef, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DataLayerColorSettings, DataLayerColorType } from '@shared/models/widget/maps/map.models'; import { DataLayerColorSettings, DataLayerColorType, MapType } from '@shared/models/widget/maps/map.models';
import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models';
@Component({ @Component({
selector: 'tb-data-layer-color-settings-panel', selector: 'tb-data-layer-color-settings-panel',
@ -33,9 +36,25 @@ import { DataLayerColorSettings, DataLayerColorType } from '@shared/models/widge
}) })
export class DataLayerColorSettingsPanelComponent extends PageComponent implements OnInit { export class DataLayerColorSettingsPanelComponent extends PageComponent implements OnInit {
widgetType = widgetType;
DataKeyType = DataKeyType;
@Input() @Input()
colorSettings: DataLayerColorSettings; colorSettings: DataLayerColorSettings;
@Input()
context: MapSettingsContext;
@Input()
dsType: DatasourceType;
@Input()
dsEntityAliasId: string;
@Input()
dsDeviceId: string;
@Input() @Input()
helpId = 'widget/lib/map/color_fn'; helpId = 'widget/lib/map/color_fn';
@ -63,14 +82,18 @@ export class DataLayerColorSettingsPanelComponent extends PageComponent implemen
{ {
type: [this.colorSettings?.type || DataLayerColorType.constant, []], type: [this.colorSettings?.type || DataLayerColorType.constant, []],
color: [this.colorSettings?.color, []], color: [this.colorSettings?.color, []],
rangeKey: [this.colorSettings?.rangeKey, [Validators.required]],
range: [this.colorSettings?.range, []],
colorFunction: [this.colorSettings?.colorFunction, []] colorFunction: [this.colorSettings?.colorFunction, []]
} }
); );
this.colorSettingsFormGroup.get('type').valueChanges.pipe( this.colorSettingsFormGroup.get('type').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
).subscribe(() => { ).subscribe(() => {
this.updateValidators();
setTimeout(() => {this.popover?.updatePosition();}, 0); setTimeout(() => {this.popover?.updatePosition();}, 0);
}); });
this.updateValidators();
} }
cancel() { cancel() {
@ -82,4 +105,25 @@ export class DataLayerColorSettingsPanelComponent extends PageComponent implemen
this.colorSettingsApplied.emit(colorSettings); this.colorSettingsApplied.emit(colorSettings);
} }
public editRangeKey() {
const targetDataKey: DataKey = this.colorSettingsFormGroup.get('rangeKey').value;
this.context.editKey(targetDataKey,
this.dsDeviceId, this.dsEntityAliasId, widgetType.latest).subscribe(
(updatedDataKey) => {
if (updatedDataKey) {
this.colorSettingsFormGroup.get('rangeKey').patchValue(updatedDataKey);
this.colorSettingsFormGroup.markAsDirty();
}
}
);
}
private updateValidators() {
const type: DataLayerColorType = this.colorSettingsFormGroup.get('type').value;
if (type === DataLayerColorType.range) {
this.colorSettingsFormGroup.get('rangeKey').enable({emitEvent: false});
} else {
this.colorSettingsFormGroup.get('rangeKey').disable({emitEvent: false});
}
}
} }

View File

@ -16,13 +16,15 @@
import { Component, forwardRef, Input, Renderer2, ViewContainerRef } from '@angular/core'; import { Component, forwardRef, Input, Renderer2, ViewContainerRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ComponentStyle } from '@shared/models/widget-settings.models'; import { ColorType, ComponentStyle } from '@shared/models/widget-settings.models';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { DataLayerColorSettings, DataLayerColorType } from '@shared/models/widget/maps/map.models'; import { DataLayerColorSettings, DataLayerColorType } from '@shared/models/widget/maps/map.models';
import { import {
DataLayerColorSettingsPanelComponent DataLayerColorSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component'; } from '@home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component';
import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models';
import { DatasourceType } from '@shared/models/widget.models';
@Component({ @Component({
selector: 'tb-data-layer-color-settings', selector: 'tb-data-layer-color-settings',
@ -41,6 +43,18 @@ export class DataLayerColorSettingsComponent implements ControlValueAccessor {
@Input() @Input()
disabled: boolean; disabled: boolean;
@Input()
context: MapSettingsContext;
@Input()
dsType: DatasourceType;
@Input()
dsEntityAliasId: string;
@Input()
dsDeviceId: string;
@Input() @Input()
helpId = 'widget/lib/map/color_fn'; helpId = 'widget/lib/map/color_fn';
@ -85,6 +99,10 @@ export class DataLayerColorSettingsComponent implements ControlValueAccessor {
} else { } else {
const ctx: any = { const ctx: any = {
colorSettings: this.modelValue, colorSettings: this.modelValue,
context: this.context,
dsType: this.dsType,
dsEntityAliasId: this.dsEntityAliasId,
dsDeviceId: this.dsDeviceId,
helpId: this.helpId helpId: this.helpId
}; };
const colorSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, const colorSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
@ -103,11 +121,23 @@ export class DataLayerColorSettingsComponent implements ControlValueAccessor {
} }
private updateColorStyle() { private updateColorStyle() {
if (!this.disabled && this.modelValue) { if (!this.disabled && this.modelValue && this.modelValue.type !== DataLayerColorType.function) {
if (this.modelValue.type === DataLayerColorType.constant) { let colors: string[] = [this.modelValue.color];
this.colorStyle = {backgroundColor: this.modelValue.color}; const rangeList = this.modelValue.range;
if (this.modelValue.type === DataLayerColorType.range && rangeList?.length) {
const rangeColors = rangeList.slice(0, Math.min(2, rangeList.length)).map(r => r.color);
colors = colors.concat(rangeColors);
}
if (colors.length === 1) {
this.colorStyle = {backgroundColor: colors[0]};
} else { } else {
this.colorStyle = {}; const gradientValues: string[] = [];
const step = 100 / colors.length;
for (let i = 0; i < colors.length; i++) {
gradientValues.push(`${colors[i]} ${step*i}%`);
gradientValues.push(`${colors[i]} ${step*(i+1)}%`);
}
this.colorStyle = {background: `linear-gradient(90deg, ${gradientValues.join(', ')})`};
} }
} else { } else {
this.colorStyle = {}; this.colorStyle = {};

View File

@ -200,11 +200,21 @@
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<div *ngIf="dataLayerFormGroup.get('markerType').value === MarkerType.shape" class="tb-form-row space-between column-xs"> <div *ngIf="dataLayerFormGroup.get('markerType').value === MarkerType.shape" class="tb-form-row space-between column-xs">
<div translate>widgets.maps.data-layer.marker.shape</div> <div translate>widgets.maps.data-layer.marker.shape</div>
<tb-marker-shape-settings formControlName="markerShape" [trip]="dataLayerType === 'trips'" [markerType]="MarkerType.shape"></tb-marker-shape-settings> <tb-marker-shape-settings formControlName="markerShape"
[context]="context"
[dsType]="dataLayerFormGroup.get('dsType').value"
[dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value"
[trip]="dataLayerType === 'trips'" [markerType]="MarkerType.shape"></tb-marker-shape-settings>
</div> </div>
<div *ngIf="dataLayerFormGroup.get('markerType').value === MarkerType.icon" class="tb-form-row space-between column-xs"> <div *ngIf="dataLayerFormGroup.get('markerType').value === MarkerType.icon" class="tb-form-row space-between column-xs">
<div translate>widgets.maps.data-layer.marker.icon</div> <div translate>widgets.maps.data-layer.marker.icon</div>
<tb-marker-shape-settings formControlName="markerIcon" [trip]="dataLayerType === 'trips'" [markerType]="MarkerType.icon"></tb-marker-shape-settings> <tb-marker-shape-settings formControlName="markerIcon"
[context]="context"
[dsType]="dataLayerFormGroup.get('dsType').value"
[dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value"
[trip]="dataLayerType === 'trips'" [markerType]="MarkerType.icon"></tb-marker-shape-settings>
</div> </div>
<div *ngIf="dataLayerFormGroup.get('markerType').value === MarkerType.image" class="tb-form-row space-between"> <div *ngIf="dataLayerFormGroup.get('markerType').value === MarkerType.image" class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.marker.image</div> <div translate>widgets.maps.data-layer.marker.image</div>
@ -275,7 +285,12 @@
<input matInput type="number" min="0" formControlName="pathStrokeWeight" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput type="number" min="0" formControlName="pathStrokeWeight" placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span> <span matSuffix>px</span>
</mat-form-field> </mat-form-field>
<tb-data-layer-color-settings helpId="widget/lib/map/path_color_fn" formControlName="pathStrokeColor"></tb-data-layer-color-settings> <tb-data-layer-color-settings
[context]="context"
[dsType]="dataLayerFormGroup.get('dsType').value"
[dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value"
helpId="widget/lib/map/path_color_fn" formControlName="pathStrokeColor"></tb-data-layer-color-settings>
</div> </div>
</div> </div>
<div class="tb-form-panel tb-slide-toggle stroked"> <div class="tb-form-panel tb-slide-toggle stroked">
@ -361,7 +376,12 @@
<input matInput type="number" min="0" formControlName="pointSize" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput type="number" min="0" formControlName="pointSize" placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span> <span matSuffix>px</span>
</mat-form-field> </mat-form-field>
<tb-data-layer-color-settings helpId="widget/lib/map/path_point_color_fn" formControlName="pointColor"></tb-data-layer-color-settings> <tb-data-layer-color-settings
[context]="context"
[dsType]="dataLayerFormGroup.get('dsType').value"
[dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value"
helpId="widget/lib/map/path_point_color_fn" formControlName="pointColor"></tb-data-layer-color-settings>
</div> </div>
</div> </div>
<tb-data-layer-pattern-settings <tb-data-layer-pattern-settings
@ -379,7 +399,12 @@
<ng-container *ngIf="['polygons', 'circles'].includes(dataLayerType)"> <ng-container *ngIf="['polygons', 'circles'].includes(dataLayerType)">
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.fill-color</div> <div translate>widgets.maps.data-layer.fill-color</div>
<tb-data-layer-color-settings helpId="{{ dataLayerType === 'polygons' ? 'widget/lib/map/polygon_fill_color_fn' : 'widget/lib/map/circle_fill_color_fn' }}" formControlName="fillColor"></tb-data-layer-color-settings> <tb-data-layer-color-settings
[context]="context"
[dsType]="dataLayerFormGroup.get('dsType').value"
[dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value"
helpId="{{ dataLayerType === 'polygons' ? 'widget/lib/map/polygon_fill_color_fn' : 'widget/lib/map/circle_fill_color_fn' }}" formControlName="fillColor"></tb-data-layer-color-settings>
</div> </div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.stroke</div> <div translate>widgets.maps.data-layer.stroke</div>
@ -388,7 +413,12 @@
<input matInput type="number" min="0" formControlName="strokeWeight" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput type="number" min="0" formControlName="strokeWeight" placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span> <span matSuffix>px</span>
</mat-form-field> </mat-form-field>
<tb-data-layer-color-settings helpId="{{ dataLayerType === 'polygons' ? 'widget/lib/map/polygon_stroke_color_fn' : 'widget/lib/map/circle_stroke_color_fn' }}" formControlName="strokeColor"></tb-data-layer-color-settings> <tb-data-layer-color-settings
[context]="context"
[dsType]="dataLayerFormGroup.get('dsType').value"
[dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value"
helpId="{{ dataLayerType === 'polygons' ? 'widget/lib/map/polygon_stroke_color_fn' : 'widget/lib/map/circle_stroke_color_fn' }}" formControlName="strokeColor"></tb-data-layer-color-settings>
</div> </div>
</div> </div>
</ng-container> </ng-container>

View File

@ -30,5 +30,9 @@
<div *ngIf="markerType === MarkerType.icon" matButtonIcon style="object-fit: contain; width: 24px; height: 24px;" [innerHTML]="iconPreview$ | async" [class.disabled]="disabled"> <div *ngIf="markerType === MarkerType.icon" matButtonIcon style="object-fit: contain; width: 24px; height: 24px;" [innerHTML]="iconPreview$ | async" [class.disabled]="disabled">
</div> </div>
</button> </button>
<tb-data-layer-color-settings helpId="widget/lib/map/color_fn" formControlName="color"></tb-data-layer-color-settings> <tb-data-layer-color-settings [context]="context"
[dsType]="dsType"
[dsEntityAliasId]="dsEntityAliasId"
[dsDeviceId]="dsDeviceId"
helpId="widget/lib/map/color_fn" formControlName="color"></tb-data-layer-color-settings>
</div> </div>

View File

@ -49,6 +49,8 @@ import { coerceBoolean } from '@shared/decorators/coercion';
import { import {
MarkerIconShapesComponent MarkerIconShapesComponent
} from '@home/components/widget/lib/settings/common/map/marker-icon-shapes.component'; } from '@home/components/widget/lib/settings/common/map/marker-icon-shapes.component';
import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models';
import { DatasourceType } from '@shared/models/widget.models';
@Component({ @Component({
selector: 'tb-marker-shape-settings', selector: 'tb-marker-shape-settings',
@ -69,6 +71,18 @@ export class MarkerShapeSettingsComponent implements ControlValueAccessor, OnIni
@Input() @Input()
disabled: boolean; disabled: boolean;
@Input()
context: MapSettingsContext;
@Input()
dsType: DatasourceType;
@Input()
dsEntityAliasId: string;
@Input()
dsDeviceId: string;
@Input() @Input()
markerType: MarkerType; markerType: MarkerType;

View File

@ -41,7 +41,7 @@ import { Observable, Observer, of, switchMap } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { ImagePipe } from '@shared/pipe/image.pipe'; import { ImagePipe } from '@shared/pipe/image.pipe';
import { MarkerIconContainer, MarkerShape } from '@shared/models/widget/maps/marker-shape.models'; import { MarkerIconContainer, MarkerShape } from '@shared/models/widget/maps/marker-shape.models';
import { DateFormatSettings, simpleDateFormat } from '@shared/models/widget-settings.models'; import { ColorRange, DateFormatSettings, simpleDateFormat } from '@shared/models/widget-settings.models';
export enum MapType { export enum MapType {
geoMap = 'geoMap', geoMap = 'geoMap',
@ -233,12 +233,15 @@ export enum MarkerType {
export enum DataLayerColorType { export enum DataLayerColorType {
constant = 'constant', constant = 'constant',
range = 'range',
function = 'function' function = 'function'
} }
export interface DataLayerColorSettings { export interface DataLayerColorSettings {
type: DataLayerColorType; type: DataLayerColorType;
color: string; color: string;
rangeKey?: DataKey;
range?: ColorRange[];
colorFunction?: TbFunction; colorFunction?: TbFunction;
} }

View File

@ -7979,7 +7979,11 @@
"stroke": "Stroke", "stroke": "Stroke",
"color-settings": "Color settings", "color-settings": "Color settings",
"color-type-constant": "Constant", "color-type-constant": "Constant",
"color-type-range": "Range",
"color-type-function": "Function", "color-type-function": "Function",
"color-range-source-key": "Color range source key",
"color-range-source-key-required": "Color range source key is required",
"color-range": "Color range",
"color-function": "Color function", "color-function": "Color function",
"label": "Label", "label": "Label",
"tooltip": "Tooltip", "tooltip": "Tooltip",