31 changed files with 1468 additions and 69 deletions
@ -0,0 +1,170 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { |
|||
CirclesDataLayerSettings, |
|||
MapDataLayerSettings, mapDataSourceSettingsToDatasource, |
|||
MarkersDataLayerSettings, PolygonsDataLayerSettings, TbMapDatasource |
|||
} from '@home/components/widget/lib/maps/map.models'; |
|||
import { TbMap } from '@home/components/widget/lib/maps/map'; |
|||
import { FormattedData } from '@shared/models/widget.models'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { guid } from '@core/utils'; |
|||
import L from 'leaflet'; |
|||
|
|||
abstract class TbDataLayerItem<S extends MapDataLayerSettings, L extends TbMapDataLayer<S>> { |
|||
|
|||
protected layer: L.Layer; |
|||
|
|||
} |
|||
|
|||
export enum MapDataLayerType { |
|||
marker = 'marker', |
|||
polygon = 'polygon', |
|||
circle = 'circle' |
|||
} |
|||
|
|||
export abstract class TbMapDataLayer<S extends MapDataLayerSettings> { |
|||
|
|||
protected datasource: TbMapDatasource; |
|||
|
|||
protected mapDataId = guid(); |
|||
|
|||
protected constructor(protected map: TbMap<any>, |
|||
protected settings: S) { |
|||
} |
|||
|
|||
public setup(): Observable<void> { |
|||
this.datasource = mapDataSourceSettingsToDatasource(this.settings); |
|||
this.datasource.dataKeys = this.settings.additionalDataKeys ? [...this.settings.additionalDataKeys] : []; |
|||
this.mapDataId = this.datasource.mapDataIds[0]; |
|||
this.datasource = this.setupDatasource(this.datasource); |
|||
return this.doSetup(); |
|||
} |
|||
|
|||
public getDatasource(): TbMapDatasource { |
|||
return this.datasource; |
|||
} |
|||
|
|||
public updateData(dsData: FormattedData<TbMapDatasource>[]) { |
|||
const layerData = dsData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId)); |
|||
this.onData(layerData, dsData); |
|||
} |
|||
|
|||
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { |
|||
return datasource; |
|||
} |
|||
|
|||
public abstract dataLayerType(): MapDataLayerType; |
|||
|
|||
protected abstract doSetup(): Observable<void>; |
|||
|
|||
protected abstract onData(layerData: FormattedData<TbMapDatasource>[], dsData: FormattedData<TbMapDatasource>[]); |
|||
|
|||
} |
|||
|
|||
export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings> { |
|||
|
|||
constructor(protected map: TbMap<any>, |
|||
protected settings: MarkersDataLayerSettings) { |
|||
super(map, settings); |
|||
} |
|||
|
|||
public dataLayerType(): MapDataLayerType { |
|||
return MapDataLayerType.marker; |
|||
} |
|||
|
|||
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { |
|||
datasource.dataKeys.push(this.settings.xKey, this.settings.yKey); |
|||
return datasource; |
|||
} |
|||
|
|||
protected doSetup(): Observable<void> { |
|||
return of(null); |
|||
} |
|||
|
|||
protected onData(layerData: FormattedData<TbMapDatasource>[], dsData: FormattedData<TbMapDatasource>[]) { |
|||
layerData.forEach((data, index) => { |
|||
console.log(`[${this.mapDataId}][${index}]: Markers layer data updated!`); |
|||
console.log(data); |
|||
this.markerData(data, dsData); |
|||
}); |
|||
} |
|||
|
|||
private markerData(data: FormattedData, dsData: FormattedData[]) { |
|||
const xKeyVal = data[this.settings.xKey.label]; |
|||
const yKeyVal = data[this.settings.yKey.label]; |
|||
} |
|||
|
|||
} |
|||
|
|||
export class TbPolygonsDataLayer extends TbMapDataLayer<PolygonsDataLayerSettings> { |
|||
|
|||
constructor(protected map: TbMap<any>, |
|||
protected settings: PolygonsDataLayerSettings) { |
|||
super(map, settings); |
|||
} |
|||
|
|||
public dataLayerType(): MapDataLayerType { |
|||
return MapDataLayerType.polygon; |
|||
} |
|||
|
|||
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { |
|||
datasource.dataKeys.push(this.settings.polygonKey); |
|||
return datasource; |
|||
} |
|||
|
|||
protected doSetup(): Observable<void> { |
|||
return of(null); |
|||
} |
|||
|
|||
protected onData(layerData: FormattedData<TbMapDatasource>[], dsData: FormattedData<TbMapDatasource>[]) { |
|||
layerData.forEach((data, index) => { |
|||
console.log(`[${this.mapDataId}][${index}]: Polygons layer data updated!`); |
|||
console.log(data); |
|||
}); |
|||
} |
|||
|
|||
} |
|||
|
|||
export class TbCirclesDataLayer extends TbMapDataLayer<CirclesDataLayerSettings> { |
|||
|
|||
constructor(protected map: TbMap<any>, |
|||
protected settings: CirclesDataLayerSettings) { |
|||
super(map, settings); |
|||
} |
|||
|
|||
public dataLayerType(): MapDataLayerType { |
|||
return MapDataLayerType.circle; |
|||
} |
|||
|
|||
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { |
|||
datasource.dataKeys.push(this.settings.circleKey); |
|||
return datasource; |
|||
} |
|||
|
|||
protected doSetup(): Observable<void> { |
|||
return of(null); |
|||
} |
|||
|
|||
protected onData(layerData: FormattedData<TbMapDatasource>[], dsData: FormattedData<TbMapDatasource>[]) { |
|||
layerData.forEach((data, index) => { |
|||
console.log(`[${this.mapDataId}][${index}]: Circles layer data updated!`); |
|||
console.log(data); |
|||
}); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,129 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div [formGroup]="dataLayerFormGroup" class="tb-form-table-row tb-map-data-layer-row"> |
|||
<div class="tb-source-field"> |
|||
<mat-form-field class="tb-ds-type-field tb-inline-field" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="dsType"> |
|||
<mat-option *ngFor="let type of datasourceTypes" [value]="type"> |
|||
{{ datasourceTypesTranslations.get(type) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<tb-entity-autocomplete |
|||
class="tb-device-field" |
|||
*ngIf="dataLayerFormGroup.get('dsType').value === DatasourceType.device" |
|||
required |
|||
inlineField |
|||
placeholder="{{ 'device.select-device' | translate }}" |
|||
[entityType]="EntityType.DEVICE" |
|||
formControlName="dsDeviceId"> |
|||
</tb-entity-autocomplete> |
|||
<tb-entity-alias-select |
|||
class="tb-entity-alias-field" |
|||
*ngIf="dataLayerFormGroup.get('dsType').value === DatasourceType.entity" |
|||
inlineField |
|||
tbRequired |
|||
[aliasController]="aliasController" |
|||
formControlName="dsEntityAliasId" |
|||
[callbacks]="entityAliasSelectCallbacks"> |
|||
</tb-entity-alias-select> |
|||
</div> |
|||
<tb-data-key-input |
|||
class="tb-x-pos-field" |
|||
*ngIf="dataLayerType === 'markers'" |
|||
required |
|||
requiredText="{{ mapType === MapType.geoMap ? 'widgets.maps.data-layer.marker.latitude-key-required' : 'widgets.maps.data-layer.marker.x-pos-key-required' }}" |
|||
[datasourceType]="dataLayerFormGroup.get('dsType').value" |
|||
[entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value" |
|||
[deviceId]="dataLayerFormGroup.get('dsDeviceId').value" |
|||
[aliasController]="aliasController" |
|||
[dataKeyType]="functionsOnly ? DataKeyType.function : null" |
|||
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]" |
|||
[callbacks]="dataKeyCallbacks" |
|||
[generateKey]="generateDataKey" |
|||
(keyEdit)="editKey('xKey')" |
|||
formControlName="xKey"> |
|||
</tb-data-key-input> |
|||
<tb-data-key-input |
|||
class="tb-y-pos-field" |
|||
*ngIf="dataLayerType === 'markers'" |
|||
required |
|||
requiredText="{{ mapType === MapType.geoMap ? 'widgets.maps.data-layer.marker.longitude-key-required' : 'widgets.maps.data-layer.marker.y-pos-key-required' }}" |
|||
[datasourceType]="dataLayerFormGroup.get('dsType').value" |
|||
[entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value" |
|||
[deviceId]="dataLayerFormGroup.get('dsDeviceId').value" |
|||
[aliasController]="aliasController" |
|||
[dataKeyType]="functionsOnly ? DataKeyType.function : null" |
|||
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]" |
|||
[callbacks]="dataKeyCallbacks" |
|||
[generateKey]="generateDataKey" |
|||
(keyEdit)="editKey('yKey')" |
|||
formControlName="yKey"> |
|||
</tb-data-key-input> |
|||
<tb-data-key-input |
|||
class="tb-key-field" |
|||
*ngIf="dataLayerType === 'polygons'" |
|||
required |
|||
requiredText="widgets.maps.data-layer.polygon.polygon-key-required" |
|||
[datasourceType]="dataLayerFormGroup.get('dsType').value" |
|||
[entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value" |
|||
[deviceId]="dataLayerFormGroup.get('dsDeviceId').value" |
|||
[aliasController]="aliasController" |
|||
[dataKeyType]="functionsOnly ? DataKeyType.function : null" |
|||
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]" |
|||
[callbacks]="dataKeyCallbacks" |
|||
[generateKey]="generateDataKey" |
|||
(keyEdit)="editKey('polygonKey')" |
|||
formControlName="polygonKey"> |
|||
</tb-data-key-input> |
|||
<tb-data-key-input |
|||
class="tb-key-field" |
|||
*ngIf="dataLayerType === 'circles'" |
|||
required |
|||
requiredText="widgets.maps.data-layer.circle.circle-key-required" |
|||
[datasourceType]="dataLayerFormGroup.get('dsType').value" |
|||
[entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value" |
|||
[deviceId]="dataLayerFormGroup.get('dsDeviceId').value" |
|||
[aliasController]="aliasController" |
|||
[dataKeyType]="functionsOnly ? DataKeyType.function : null" |
|||
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]" |
|||
[callbacks]="dataKeyCallbacks" |
|||
[generateKey]="generateDataKey" |
|||
(keyEdit)="editKey('circleKey')" |
|||
formControlName="circleKey"> |
|||
</tb-data-key-input> |
|||
<div class="tb-form-table-row-cell-buttons"> |
|||
<button type="button" |
|||
mat-icon-button |
|||
#matButton |
|||
(click)="editDataLayer($event, matButton)" |
|||
matTooltip="{{ editDataLayerText | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>settings</mat-icon> |
|||
</button> |
|||
<div class="tb-remove-button"> |
|||
<button type="button" |
|||
mat-icon-button |
|||
(click)="dataLayerRemoved.emit()" |
|||
matTooltip="{{ removeDataLayerText | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>delete</mat-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,45 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
@import '../../../../../../../../../scss/constants'; |
|||
|
|||
.tb-form-table-row.tb-map-data-layer-row { |
|||
|
|||
.tb-source-field { |
|||
flex: 1 1 50%; |
|||
display: flex; |
|||
gap: 12px; |
|||
.tb-ds-type-field, .tb-device-field, .tb-entity-alias-field { |
|||
flex: 1; |
|||
} |
|||
} |
|||
|
|||
.tb-x-pos-field { |
|||
flex: 1 1 25%; |
|||
} |
|||
|
|||
.tb-y-pos-field { |
|||
flex: 1 1 25%; |
|||
} |
|||
|
|||
.tb-key-field { |
|||
flex: 1 1 50%; |
|||
} |
|||
|
|||
.tb-remove-button { |
|||
width: 40px; |
|||
min-width: 40px; |
|||
} |
|||
} |
|||
@ -0,0 +1,380 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { |
|||
ChangeDetectorRef, |
|||
Component, |
|||
DestroyRef, |
|||
EventEmitter, |
|||
forwardRef, |
|||
Input, |
|||
OnInit, |
|||
Output, |
|||
Renderer2, |
|||
ViewContainerRef, |
|||
ViewEncapsulation |
|||
} from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormBuilder, |
|||
UntypedFormGroup, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { MatButton } from '@angular/material/button'; |
|||
import { TbPopoverService } from '@shared/components/popover.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { |
|||
CirclesDataLayerSettings, |
|||
MapDataLayerSettings, |
|||
MapDataLayerType, |
|||
MapType, |
|||
MarkersDataLayerSettings, |
|||
PolygonsDataLayerSettings |
|||
} from '@home/components/widget/lib/maps/map.models'; |
|||
import { |
|||
DataKey, |
|||
DataKeyConfigMode, |
|||
DatasourceType, |
|||
datasourceTypeTranslationMap, |
|||
widgetType |
|||
} from '@shared/models/widget.models'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
import { MapSettingsComponent } from '@home/components/widget/lib/settings/common/map/map-settings.component'; |
|||
import { IAliasController } from '@core/api/widget-api.models'; |
|||
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; |
|||
import { |
|||
DataKeyConfigDialogComponent, |
|||
DataKeyConfigDialogData |
|||
} from '@home/components/widget/config/data-key-config-dialog.component'; |
|||
import { deepClone } from '@core/utils'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { |
|||
EntityAliasSelectCallbacks |
|||
} from '@home/components/widget/lib/settings/common/alias/entity-alias-select.component.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-map-data-layer-row', |
|||
templateUrl: './map-data-layer-row.component.html', |
|||
styleUrls: ['./map-data-layer-row.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => MapDataLayerRowComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
DatasourceType = DatasourceType; |
|||
DataKeyType = DataKeyType; |
|||
|
|||
EntityType = EntityType; |
|||
|
|||
MapType = MapType; |
|||
|
|||
datasourceTypes: Array<DatasourceType> = []; |
|||
datasourceTypesTranslations = datasourceTypeTranslationMap; |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
mapType: MapType = MapType.geoMap; |
|||
|
|||
@Input() |
|||
dataLayerType: MapDataLayerType = 'markers'; |
|||
|
|||
get functionsOnly(): boolean { |
|||
return this.mapSettingsComponent.functionsOnly; |
|||
} |
|||
|
|||
get aliasController(): IAliasController { |
|||
return this.mapSettingsComponent.aliasController; |
|||
} |
|||
|
|||
get dataKeyCallbacks(): DataKeysCallbacks { |
|||
return this.mapSettingsComponent.callbacks; |
|||
} |
|||
|
|||
public get entityAliasSelectCallbacks(): EntityAliasSelectCallbacks { |
|||
return this.mapSettingsComponent.callbacks; |
|||
} |
|||
|
|||
@Output() |
|||
dataLayerRemoved = new EventEmitter(); |
|||
|
|||
generateDataKey = this._generateDataKey.bind(this); |
|||
|
|||
dataLayerFormGroup: UntypedFormGroup; |
|||
|
|||
modelValue: MapDataLayerSettings; |
|||
|
|||
editDataLayerText: string; |
|||
|
|||
removeDataLayerText: string; |
|||
|
|||
private propagateChange = (_val: any) => {}; |
|||
|
|||
constructor(private mapSettingsComponent: MapSettingsComponent, |
|||
private fb: UntypedFormBuilder, |
|||
private dialog: MatDialog, |
|||
private translate: TranslateService, |
|||
private popoverService: TbPopoverService, |
|||
private renderer: Renderer2, |
|||
private viewContainerRef: ViewContainerRef, |
|||
private cd: ChangeDetectorRef, |
|||
private destroyRef: DestroyRef) { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
if (this.functionsOnly) { |
|||
this.datasourceTypes = [DatasourceType.function]; |
|||
} else { |
|||
this.datasourceTypes = [DatasourceType.function, DatasourceType.device, DatasourceType.entity]; |
|||
} |
|||
this.dataLayerFormGroup = this.fb.group({ |
|||
dsType: [null, [Validators.required]], |
|||
dsDeviceId: [null, [Validators.required]], |
|||
dsEntityAliasId: [null, [Validators.required]] |
|||
}); |
|||
switch (this.dataLayerType) { |
|||
case 'markers': |
|||
this.editDataLayerText = 'widgets.maps.data-layer.marker.marker-configuration'; |
|||
this.removeDataLayerText = 'widgets.maps.data-layer.marker.remove-marker'; |
|||
this.dataLayerFormGroup.addControl('xKey', this.fb.control(null, Validators.required)); |
|||
this.dataLayerFormGroup.addControl('yKey', this.fb.control(null, Validators.required)); |
|||
break; |
|||
case 'polygons': |
|||
this.editDataLayerText = 'widgets.maps.data-layer.polygon.polygon-configuration'; |
|||
this.removeDataLayerText = 'widgets.maps.data-layer.polygon.remove-polygon'; |
|||
this.dataLayerFormGroup.addControl('polygonKey', this.fb.control(null, Validators.required)); |
|||
break; |
|||
case 'circles': |
|||
this.editDataLayerText = 'widgets.maps.data-layer.circle.circle-configuration'; |
|||
this.removeDataLayerText = 'widgets.maps.data-layer.circle.remove-circle'; |
|||
this.dataLayerFormGroup.addControl('circleKey', this.fb.control(null, Validators.required)); |
|||
break; |
|||
} |
|||
this.dataLayerFormGroup.valueChanges.pipe( |
|||
takeUntilDestroyed(this.destroyRef) |
|||
).subscribe( |
|||
() => this.updateModel() |
|||
); |
|||
this.dataLayerFormGroup.get('dsType').valueChanges.pipe( |
|||
takeUntilDestroyed(this.destroyRef) |
|||
).subscribe( |
|||
(newDsType: DatasourceType) => this.onDsTypeChanged(newDsType) |
|||
); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(_fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.dataLayerFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.dataLayerFormGroup.enable({emitEvent: false}); |
|||
this.updateValidators(); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: MapDataLayerSettings): void { |
|||
this.modelValue = value; |
|||
this.dataLayerFormGroup.patchValue( |
|||
{ |
|||
dsType: value?.dsType, |
|||
dsDeviceId: value?.dsDeviceId, |
|||
dsEntityAliasId: value?.dsEntityAliasId |
|||
}, {emitEvent: false} |
|||
); |
|||
switch (this.dataLayerType) { |
|||
case 'markers': |
|||
const markersDataLayer = value as MarkersDataLayerSettings; |
|||
this.dataLayerFormGroup.patchValue( |
|||
{ |
|||
xKey: markersDataLayer?.xKey, |
|||
yKey: markersDataLayer?.yKey |
|||
}, {emitEvent: false} |
|||
); |
|||
break; |
|||
case 'polygons': |
|||
const polygonsDataLayer = value as PolygonsDataLayerSettings; |
|||
this.dataLayerFormGroup.patchValue( |
|||
{ |
|||
polygonKey: polygonsDataLayer?.polygonKey |
|||
}, {emitEvent: false} |
|||
); |
|||
break; |
|||
case 'circles': |
|||
const circlesDataLayer = value as CirclesDataLayerSettings; |
|||
this.dataLayerFormGroup.patchValue( |
|||
{ |
|||
circleKey: circlesDataLayer?.circleKey |
|||
}, {emitEvent: false} |
|||
); |
|||
break; |
|||
} |
|||
this.updateValidators(); |
|||
this.cd.markForCheck(); |
|||
} |
|||
|
|||
editKey(keyType: 'xKey' | 'yKey' | 'polygonKey' | 'circleKey') { |
|||
const targetDataKey: DataKey = this.dataLayerFormGroup.get(keyType).value; |
|||
this.dialog.open<DataKeyConfigDialogComponent, DataKeyConfigDialogData, DataKey>(DataKeyConfigDialogComponent, |
|||
{ |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
dataKey: deepClone(targetDataKey), |
|||
dataKeyConfigMode: DataKeyConfigMode.general, |
|||
aliasController: this.aliasController, |
|||
widgetType: widgetType.latest, |
|||
deviceId: this.dataLayerFormGroup.get('dsDeviceId').value, |
|||
entityAliasId: this.dataLayerFormGroup.get('dsEntityAliasId').value, |
|||
showPostProcessing: true, |
|||
callbacks: this.mapSettingsComponent.callbacks, |
|||
hideDataKeyColor: true, |
|||
hideDataKeyDecimals: true, |
|||
hideDataKeyUnits: true, |
|||
widget: this.mapSettingsComponent.widget, |
|||
dashboard: null, |
|||
dataKeySettingsForm: null, |
|||
dataKeySettingsDirective: null |
|||
} |
|||
}).afterClosed().subscribe((updatedDataKey) => { |
|||
if (updatedDataKey) { |
|||
this.dataLayerFormGroup.get(keyType).patchValue(updatedDataKey); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
editDataLayer($event: Event, matButton: MatButton) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
const trigger = matButton._elementRef.nativeElement; |
|||
if (this.popoverService.hasPopover(trigger)) { |
|||
this.popoverService.hidePopover(trigger); |
|||
} else { |
|||
/*const ctx: any = { |
|||
mapLayerSettings: deepClone(this.modelValue) |
|||
}; |
|||
const mapLayerSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, |
|||
this.viewContainerRef, MapLayerSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, |
|||
ctx, |
|||
{}, |
|||
{}, {}, true); |
|||
mapLayerSettingsPanelPopover.tbComponentRef.instance.popover = mapLayerSettingsPanelPopover; |
|||
mapLayerSettingsPanelPopover.tbComponentRef.instance.mapLayerSettingsApplied.subscribe((layer) => { |
|||
mapLayerSettingsPanelPopover.hide(); |
|||
this.layerFormGroup.patchValue( |
|||
layer, |
|||
{emitEvent: false}); |
|||
this.updateValidators(); |
|||
this.updateModel(); |
|||
});*/ |
|||
} |
|||
} |
|||
|
|||
private _generateDataKey(key: DataKey): DataKey { |
|||
key = this.dataKeyCallbacks.generateDataKey(key.name, key.type, null, false, |
|||
null); |
|||
return key; |
|||
} |
|||
|
|||
private onDsTypeChanged(newDsType: DatasourceType) { |
|||
let updateModel = false; |
|||
switch (this.dataLayerType) { |
|||
case 'markers': |
|||
const xKey: DataKey = this.dataLayerFormGroup.get('xKey').value; |
|||
if (this.updateDataKeyToNewDsType(xKey, newDsType)) { |
|||
this.dataLayerFormGroup.get('xKey').patchValue(xKey, {emitEvent: false}); |
|||
updateModel = true; |
|||
} |
|||
const yKey: DataKey = this.dataLayerFormGroup.get('yKey').value; |
|||
if (this.updateDataKeyToNewDsType(yKey, newDsType)) { |
|||
this.dataLayerFormGroup.get('yKey').patchValue(yKey, {emitEvent: false}); |
|||
updateModel = true; |
|||
} |
|||
break; |
|||
case 'polygons': |
|||
const polygonKey: DataKey = this.dataLayerFormGroup.get('polygonKey').value; |
|||
if (this.updateDataKeyToNewDsType(polygonKey, newDsType)) { |
|||
this.dataLayerFormGroup.get('polygonKey').patchValue(polygonKey, {emitEvent: false}); |
|||
updateModel = true; |
|||
} |
|||
break; |
|||
case 'circles': |
|||
const circleKey: DataKey = this.dataLayerFormGroup.get('circleKey').value; |
|||
if (this.updateDataKeyToNewDsType(circleKey, newDsType)) { |
|||
this.dataLayerFormGroup.get('circleKey').patchValue(circleKey, {emitEvent: false}); |
|||
updateModel = true; |
|||
} |
|||
break; |
|||
} |
|||
this.updateValidators(); |
|||
if (updateModel) { |
|||
this.updateModel(); |
|||
} |
|||
} |
|||
|
|||
private updateDataKeyToNewDsType(dataKey: DataKey, newDsType: DatasourceType): boolean { |
|||
if (newDsType === DatasourceType.function) { |
|||
if (dataKey.type !== DataKeyType.function) { |
|||
dataKey.type = DataKeyType.function; |
|||
return true; |
|||
} |
|||
} else { |
|||
if (dataKey.type === DataKeyType.function) { |
|||
dataKey.type = DataKeyType.attribute; |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
private updateValidators() { |
|||
const dsType: DatasourceType = this.dataLayerFormGroup.get('dsType').value; |
|||
if (dsType === DatasourceType.function) { |
|||
this.dataLayerFormGroup.get('dsDeviceId').disable({emitEvent: false}); |
|||
this.dataLayerFormGroup.get('dsEntityAliasId').disable({emitEvent: false}); |
|||
} else if (dsType === DatasourceType.device) { |
|||
this.dataLayerFormGroup.get('dsDeviceId').enable({emitEvent: false}); |
|||
this.dataLayerFormGroup.get('dsEntityAliasId').disable({emitEvent: false}); |
|||
} else { |
|||
this.dataLayerFormGroup.get('dsDeviceId').disable({emitEvent: false}); |
|||
this.dataLayerFormGroup.get('dsEntityAliasId').enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
private updateModel() { |
|||
this.modelValue = {...this.modelValue, ...this.dataLayerFormGroup.value}; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
|
|||
protected readonly datasourceType = DatasourceType; |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="tb-form-panel no-padding no-border"> |
|||
<div class="tb-form-table tb-map-data-layers"> |
|||
<div class="tb-form-table-header no-padding-right"> |
|||
<div class="tb-form-table-header-cell tb-source-header" translate>widgets.maps.data-layer.source</div> |
|||
<div *ngIf="dataLayerType === 'markers'" class="tb-form-table-header-cell tb-x-pos-header"> |
|||
{{ (mapType === MapType.geoMap ? 'widgets.maps.data-layer.marker.latitude-key' : 'widgets.maps.data-layer.marker.x-pos-key') | translate }} |
|||
</div> |
|||
<div *ngIf="dataLayerType === 'markers'" class="tb-form-table-header-cell tb-y-pos-header"> |
|||
{{ (mapType === MapType.geoMap ? 'widgets.maps.data-layer.marker.longitude-key' : 'widgets.maps.data-layer.marker.y-pos-key') | translate }} |
|||
</div> |
|||
<div *ngIf="dataLayerType === 'polygons'" class="tb-form-table-header-cell tb-key-header" translate>widgets.maps.data-layer.polygon.polygon-key</div> |
|||
<div *ngIf="dataLayerType === 'circles'" class="tb-form-table-header-cell tb-key-header" translate>widgets.maps.data-layer.circle.circle-key</div> |
|||
<div class="tb-form-table-header-cell tb-actions-header"></div> |
|||
</div> |
|||
<div *ngIf="dataLayersFormArray().controls.length; else noDataLayers" class="tb-form-table-body"> |
|||
<div *ngFor="let dataLayerControl of dataLayersFormArray().controls; trackBy: trackByDataLayer; let $index = index;"> |
|||
<tb-map-data-layer-row class="flex-1" |
|||
[mapType]="mapType" |
|||
[dataLayerType]="dataLayerType" |
|||
[formControl]="dataLayerControl" |
|||
(dataLayerRemoved)="removeDataLayer($index)"> |
|||
</tb-map-data-layer-row> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div> |
|||
<button type="button" mat-stroked-button color="primary" (click)="addDataLayer()"> |
|||
{{ addDataLayerText | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<ng-template #noDataLayers> |
|||
<span class="tb-prompt flex items-center justify-center">{{ noDataLayersText | translate }}</span> |
|||
</ng-template> |
|||
@ -0,0 +1,42 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
|
|||
.tb-map-data-layers { |
|||
.tb-form-table-header-cell { |
|||
&.tb-source-header { |
|||
flex: 1 1 50%; |
|||
} |
|||
&.tb-x-pos-header { |
|||
flex: 1 1 25%; |
|||
} |
|||
&.tb-y-pos-header { |
|||
flex: 1 1 25%; |
|||
} |
|||
&.tb-key-header { |
|||
flex: 1 1 50%; |
|||
} |
|||
&.tb-actions-header { |
|||
width: 80px; |
|||
min-width: 80px; |
|||
} |
|||
} |
|||
|
|||
.tb-form-table-body { |
|||
tb-map-data-layer-row { |
|||
overflow: hidden; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,177 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, DestroyRef, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; |
|||
import { |
|||
AbstractControl, |
|||
ControlValueAccessor, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormArray, |
|||
UntypedFormBuilder, |
|||
UntypedFormControl, |
|||
UntypedFormGroup, |
|||
Validator |
|||
} from '@angular/forms'; |
|||
import { mergeDeep } from '@core/utils'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { |
|||
defaultMapDataLayerSettings, |
|||
MapDataLayerSettings, |
|||
MapDataLayerType, |
|||
mapDataLayerValid, |
|||
mapDataLayerValidator, |
|||
MapType |
|||
} from '@home/components/widget/lib/maps/map.models'; |
|||
import { MapSettingsComponent } from '@home/components/widget/lib/settings/common/map/map-settings.component'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-map-data-layers', |
|||
templateUrl: './map-data-layers.component.html', |
|||
styleUrls: ['./map-data-layers.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => MapDataLayersComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => MapDataLayersComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class MapDataLayersComponent implements ControlValueAccessor, OnInit, Validator { |
|||
|
|||
MapType = MapType; |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
mapType: MapType = MapType.geoMap; |
|||
|
|||
@Input() |
|||
dataLayerType: MapDataLayerType = 'markers'; |
|||
|
|||
get functionsOnly(): boolean { |
|||
return this.mapSettingsComponent.functionsOnly; |
|||
} |
|||
|
|||
dataLayersFormGroup: UntypedFormGroup; |
|||
|
|||
addDataLayerText: string; |
|||
|
|||
noDataLayersText: string; |
|||
|
|||
private propagateChange = (_val: any) => {}; |
|||
|
|||
constructor(private mapSettingsComponent: MapSettingsComponent, |
|||
private fb: UntypedFormBuilder, |
|||
private destroyRef: DestroyRef) { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
switch (this.dataLayerType) { |
|||
case 'markers': |
|||
this.addDataLayerText = 'widgets.maps.data-layer.marker.add-marker'; |
|||
this.noDataLayersText = 'widgets.maps.data-layer.marker.no-markers'; |
|||
break; |
|||
case 'polygons': |
|||
this.addDataLayerText = 'widgets.maps.data-layer.polygon.add-polygon'; |
|||
this.noDataLayersText = 'widgets.maps.data-layer.polygon.no-polygons'; |
|||
break; |
|||
case 'circles': |
|||
this.addDataLayerText = 'widgets.maps.data-layer.circle.add-circle'; |
|||
this.noDataLayersText = 'widgets.maps.data-layer.circle.no-circles'; |
|||
break; |
|||
} |
|||
this.dataLayersFormGroup = this.fb.group({ |
|||
dataLayers: [this.fb.array([]), []] |
|||
}); |
|||
this.dataLayersFormGroup.valueChanges.pipe( |
|||
takeUntilDestroyed(this.destroyRef) |
|||
).subscribe( |
|||
() => { |
|||
let layers: MapDataLayerSettings[] = this.dataLayersFormGroup.get('dataLayers').value; |
|||
if (layers) { |
|||
layers = layers.filter(layer => mapDataLayerValid(layer, this.dataLayerType)); |
|||
} |
|||
this.propagateChange(layers); |
|||
} |
|||
); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.dataLayersFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.dataLayersFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: MapDataLayerSettings[] | undefined): void { |
|||
const dataLayers: MapDataLayerSettings[] = value || []; |
|||
this.dataLayersFormGroup.setControl('dataLayers', this.prepareDataLayersFormArray(dataLayers), {emitEvent: false}); |
|||
} |
|||
|
|||
public validate(c: UntypedFormControl) { |
|||
const valid = this.dataLayersFormGroup.valid; |
|||
return valid ? null : { |
|||
dataLayers: { |
|||
valid: false, |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
dataLayersFormArray(): UntypedFormArray { |
|||
return this.dataLayersFormGroup.get('dataLayers') as UntypedFormArray; |
|||
} |
|||
|
|||
trackByDataLayer(index: number, dataLayerControl: AbstractControl): any { |
|||
return dataLayerControl; |
|||
} |
|||
|
|||
removeDataLayer(index: number) { |
|||
(this.dataLayersFormGroup.get('dataLayers') as UntypedFormArray).removeAt(index); |
|||
} |
|||
|
|||
addDataLayer() { |
|||
const dataLayer = mergeDeep<MapDataLayerSettings>({} as MapDataLayerSettings, |
|||
defaultMapDataLayerSettings(this.mapType, this.dataLayerType, this.functionsOnly)); |
|||
const dataLayersArray = this.dataLayersFormGroup.get('dataLayers') as UntypedFormArray; |
|||
const dataLayerControl = this.fb.control(dataLayer, [mapDataLayerValidator(this.dataLayerType)]); |
|||
dataLayersArray.push(dataLayerControl); |
|||
} |
|||
|
|||
private prepareDataLayersFormArray(dataLayers: MapDataLayerSettings[]): UntypedFormArray { |
|||
const dataLayersControls: Array<AbstractControl> = []; |
|||
dataLayers.forEach((dataLayer) => { |
|||
dataLayersControls.push(this.fb.control(dataLayer, [mapDataLayerValidator(this.dataLayerType)])); |
|||
}); |
|||
return this.fb.array(dataLayersControls); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue