27 changed files with 1307 additions and 356 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,141 @@ |
|||
///
|
|||
/// 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 { |
|||
DEFAULT_ZOOM_LEVEL, |
|||
defaultGeoMapSettings, |
|||
GeoMapSettings, latLngPointToBounds, |
|||
MapZoomAction, TbCircleData |
|||
} from '@home/components/widget/lib/maps/models/map.models'; |
|||
import { WidgetContext } from '@home/models/widget-component.models'; |
|||
import { DeepPartial } from '@shared/models/common'; |
|||
import { forkJoin, Observable, of } from 'rxjs'; |
|||
import L, { LatLngBounds, LatLngTuple } from 'leaflet'; |
|||
import { map, tap } from 'rxjs/operators'; |
|||
import { TbMapLayer } from '@home/components/widget/lib/maps/map-layer'; |
|||
import { TbMap } from '@home/components/widget/lib/maps/map'; |
|||
|
|||
export class TbGeoMap extends TbMap<GeoMapSettings> { |
|||
|
|||
constructor(protected ctx: WidgetContext, |
|||
protected inputSettings: DeepPartial<GeoMapSettings>, |
|||
protected containerElement: HTMLElement) { |
|||
super(ctx, inputSettings, containerElement); |
|||
} |
|||
|
|||
protected defaultSettings(): GeoMapSettings { |
|||
return defaultGeoMapSettings; |
|||
} |
|||
|
|||
protected createMap(): Observable<L.Map> { |
|||
const map = L.map(this.mapElement, { |
|||
scrollWheelZoom: this.settings.zoomActions.includes(MapZoomAction.scroll), |
|||
doubleClickZoom: this.settings.zoomActions.includes(MapZoomAction.doubleClick), |
|||
zoomControl: this.settings.zoomActions.includes(MapZoomAction.controlButtons), |
|||
zoom: this.settings.defaultZoomLevel || DEFAULT_ZOOM_LEVEL, |
|||
center: this.defaultCenterPosition |
|||
}).setView(this.defaultCenterPosition, this.settings.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); |
|||
return of(map); |
|||
} |
|||
|
|||
protected onResize(): void {} |
|||
|
|||
protected fitBounds(bounds: LatLngBounds) { |
|||
if (bounds.isValid()) { |
|||
if (!this.settings.fitMapBounds && this.settings.defaultZoomLevel) { |
|||
this.map.setZoom(this.settings.defaultZoomLevel, { animate: false }); |
|||
if (this.settings.useDefaultCenterPosition) { |
|||
this.map.panTo(this.defaultCenterPosition, { animate: false }); |
|||
} |
|||
else { |
|||
this.map.panTo(bounds.getCenter()); |
|||
} |
|||
} else { |
|||
this.map.once('zoomend', () => { |
|||
let minZoom = this.settings.minZoomLevel; |
|||
if (this.settings.defaultZoomLevel) { |
|||
minZoom = Math.max(minZoom, this.settings.defaultZoomLevel); |
|||
} |
|||
if (this.map.getZoom() > minZoom) { |
|||
this.map.setZoom(minZoom, { animate: false }); |
|||
} |
|||
}); |
|||
if (this.settings.useDefaultCenterPosition) { |
|||
bounds = bounds.extend(this.defaultCenterPosition); |
|||
} |
|||
this.map.fitBounds(bounds, { padding: [50, 50], animate: false }); |
|||
this.map.invalidateSize(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected doSetupControls(): Observable<any> { |
|||
return this.loadLayers().pipe( |
|||
tap((layers: L.TB.LayerData[]) => { |
|||
if (layers.length) { |
|||
const layer = layers[0]; |
|||
layer.layer.addTo(this.map); |
|||
this.map.attributionControl.setPrefix(layer.attributionPrefix); |
|||
if (layers.length > 1) { |
|||
const sidebar = this.getSidebar(); |
|||
L.TB.layers({ |
|||
layers, |
|||
sidebar, |
|||
position: this.settings.controlsPosition, |
|||
uiClass: 'tb-layers', |
|||
paneTitle: this.ctx.translate.instant('widgets.maps.layer.map-layers'), |
|||
buttonTitle: this.ctx.translate.instant('widgets.maps.layer.layers'), |
|||
}).addTo(this.map); |
|||
} |
|||
} |
|||
}) |
|||
); |
|||
|
|||
} |
|||
|
|||
private loadLayers(): Observable<L.TB.LayerData[]> { |
|||
const layers = this.settings.layers.map(settings => TbMapLayer.fromSettings(this.ctx, settings)); |
|||
return forkJoin(layers.map(layer => layer.loadLayer(this.map))).pipe( |
|||
map((layersData) => { |
|||
return layersData.filter(l => l !== null); |
|||
}) |
|||
); |
|||
} |
|||
|
|||
public positionToLatLng(position: {x: number; y: number}): L.LatLng { |
|||
return L.latLng(position.x, position.y) as L.LatLng; |
|||
} |
|||
|
|||
public toPolygonCoordinates(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]): any { |
|||
return (expression).map((el) => { |
|||
if (!Array.isArray(el[0]) && el.length === 2) { |
|||
return el; |
|||
} else if (Array.isArray(el) && el.length) { |
|||
return this.toPolygonCoordinates(el as LatLngTuple[] | LatLngTuple[][]); |
|||
} else { |
|||
return null; |
|||
} |
|||
}).filter(el => !!el); |
|||
} |
|||
|
|||
public convertCircleData(circle: TbCircleData): TbCircleData { |
|||
const centerPoint = latLngPointToBounds(new L.LatLng(circle.latitude, circle.longitude), this.southWest, this.northEast); |
|||
circle.latitude = centerPoint.lat; |
|||
circle.longitude = centerPoint.lng; |
|||
return circle; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,293 @@ |
|||
///
|
|||
/// 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 { |
|||
defaultImageMapSettings, |
|||
defaultImageMapSourceSettings, |
|||
ImageMapSettings, |
|||
imageMapSourceSettingsToDatasource, |
|||
ImageSourceType, |
|||
loadImageWithAspect, |
|||
MapZoomAction, |
|||
TbCircleData |
|||
} from '@home/components/widget/lib/maps/models/map.models'; |
|||
import { WidgetContext } from '@home/models/widget-component.models'; |
|||
import { DeepPartial } from '@shared/models/common'; |
|||
import { Observable, of, ReplaySubject, switchMap } from 'rxjs'; |
|||
import L, { LatLngBounds, LatLngLiteral, LatLngTuple } from 'leaflet'; |
|||
import { TbMap } from '@home/components/widget/lib/maps/map'; |
|||
import { ImagePipe } from '@shared/pipe/image.pipe'; |
|||
import { catchError } from 'rxjs/operators'; |
|||
import { DataSet, widgetType } from '@shared/models/widget.models'; |
|||
import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; |
|||
import { isNotEmptyStr } from '@core/utils'; |
|||
import { EntityDataPageLink } from '@shared/models/query/query.models'; |
|||
|
|||
interface ImageLayerData { |
|||
imageUrl: string; |
|||
aspect: number; |
|||
update?: boolean; |
|||
} |
|||
|
|||
export class TbImageMap extends TbMap<ImageMapSettings> { |
|||
|
|||
private maxZoom: number; |
|||
private width: number; |
|||
private height: number; |
|||
private imageLayerData: ImageLayerData; |
|||
private initMapSubject: ReplaySubject<L.Map>; |
|||
|
|||
private imageOverlay: L.ImageOverlay; |
|||
|
|||
constructor(protected ctx: WidgetContext, |
|||
protected inputSettings: DeepPartial<ImageMapSettings>, |
|||
protected containerElement: HTMLElement) { |
|||
super(ctx, inputSettings, containerElement); |
|||
} |
|||
|
|||
protected defaultSettings(): ImageMapSettings { |
|||
return defaultImageMapSettings; |
|||
} |
|||
|
|||
protected createMap(): Observable<L.Map> { |
|||
this.maxZoom = 4; |
|||
this.width = 0; |
|||
this.height = 0; |
|||
this.imageLayerData = { |
|||
imageUrl: null, |
|||
aspect: 0 |
|||
}; |
|||
this.initMapSubject = new ReplaySubject<L.Map>(); |
|||
this.loadImageLayerData().subscribe((data) => { |
|||
this.imageLayerData = data; |
|||
if (this.imageLayerData.update) { |
|||
this.onResize(true); |
|||
} else { |
|||
this.onResize(); |
|||
this.initMapSubject.next(this.map); |
|||
this.initMapSubject.complete(); |
|||
} |
|||
}); |
|||
return this.initMapSubject.asObservable(); |
|||
} |
|||
|
|||
protected onResize(updateImage?: boolean): void { |
|||
let width = this.mapElement.clientWidth; |
|||
if (width > 0 && this.imageLayerData.aspect) { |
|||
let height = Math.round(width / this.imageLayerData.aspect); |
|||
const imageMapHeight = this.mapElement.clientHeight; |
|||
if (imageMapHeight > 0 && height > imageMapHeight) { |
|||
height = imageMapHeight; |
|||
width = Math.round(height * this.imageLayerData.aspect); |
|||
} |
|||
width *= this.maxZoom; |
|||
const prevWidth = this.width; |
|||
const prevHeight = this.height; |
|||
if (this.width !== width || updateImage) { |
|||
this.width = width; |
|||
this.height = Math.round(width / this.imageLayerData.aspect); |
|||
if (!this.map) { |
|||
this.doCreateMap(updateImage); |
|||
} else { |
|||
const lastCenterPos = this.latLngToPoint(this.map.getCenter()); |
|||
lastCenterPos.x /= prevWidth; |
|||
lastCenterPos.y /= prevHeight; |
|||
this.updateMaxBounds(updateImage, lastCenterPos); |
|||
(this.map as any)._enforcingBounds = true; |
|||
this.map.invalidateSize(false); |
|||
(this.map as any)._enforcingBounds = false; |
|||
this.invalidateDataLayersCoordinates(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected fitBounds(_bounds: LatLngBounds) {} |
|||
|
|||
public positionToLatLng(position: {x: number; y: number}): L.LatLng { |
|||
return this.pointToLatLng( |
|||
position.x * this.width, |
|||
position.y * this.height); |
|||
} |
|||
|
|||
public pointToLatLng(x: number, y: number): L.LatLng { |
|||
return L.CRS.Simple.pointToLatLng({ x, y } as L.PointExpression, this.maxZoom - 1); |
|||
} |
|||
|
|||
private latLngToPoint(latLng: LatLngLiteral): L.Point { |
|||
return L.CRS.Simple.latLngToPoint(latLng, this.maxZoom - 1); |
|||
} |
|||
|
|||
public toPolygonCoordinates(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]): any { |
|||
return (expression).map((el) => { |
|||
if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) { |
|||
return this.pointToLatLng( |
|||
el[0] * this.width, |
|||
el[1] * this.height |
|||
); |
|||
} else if (Array.isArray(el) && el.length) { |
|||
return this.toPolygonCoordinates(el as LatLngTuple[] | LatLngTuple[][]); |
|||
} else { |
|||
return null; |
|||
} |
|||
}).filter(el => !!el); |
|||
} |
|||
|
|||
public convertCircleData(circle: TbCircleData): TbCircleData { |
|||
const centerPoint = this.pointToLatLng(circle.latitude * this.width, circle.longitude * this.height); |
|||
circle.latitude = centerPoint.lat; |
|||
circle.longitude = centerPoint.lng; |
|||
circle.radius = circle.radius * this.width; |
|||
return circle; |
|||
} |
|||
|
|||
public getAspect(): number { |
|||
return this.imageLayerData.aspect; |
|||
} |
|||
|
|||
private doCreateMap(updateImage?: boolean) { |
|||
if (!this.map && this.imageLayerData.aspect > 0) { |
|||
const center = this.pointToLatLng(this.width / 2, this.height / 2); |
|||
this.map = L.map(this.mapElement, { |
|||
scrollWheelZoom: this.settings.zoomActions.includes(MapZoomAction.scroll), |
|||
doubleClickZoom: this.settings.zoomActions.includes(MapZoomAction.doubleClick), |
|||
zoomControl: this.settings.zoomActions.includes(MapZoomAction.controlButtons), |
|||
minZoom: 1, |
|||
maxZoom: this.maxZoom, |
|||
zoom: 1, |
|||
center, |
|||
crs: L.CRS.Simple, |
|||
attributionControl: false |
|||
}); |
|||
this.updateMaxBounds(updateImage); |
|||
} |
|||
} |
|||
|
|||
private updateMaxBounds(updateImage?: boolean, lastCenterPos?: L.Point) { |
|||
const w = this.width; |
|||
const h = this.height; |
|||
this.southWest = this.pointToLatLng(0, h); |
|||
this.northEast = this.pointToLatLng(w, 0); |
|||
const bounds = new L.LatLngBounds(this.southWest, this.northEast); |
|||
|
|||
if (updateImage && this.imageOverlay) { |
|||
this.imageOverlay.remove(); |
|||
this.imageOverlay = null; |
|||
} |
|||
|
|||
if (this.imageOverlay) { |
|||
this.imageOverlay.setBounds(bounds); |
|||
} else { |
|||
this.imageOverlay = L.imageOverlay(this.imageLayerData.imageUrl, bounds).addTo(this.map); |
|||
} |
|||
const padding = 200 * this.maxZoom; |
|||
const southWest = this.pointToLatLng(-padding, h + padding); |
|||
const northEast = this.pointToLatLng(w + padding, -padding); |
|||
const maxBounds = new L.LatLngBounds(southWest, northEast); |
|||
(this.map as any)._enforcingBounds = true; |
|||
this.map.setMaxBounds(maxBounds); |
|||
if (lastCenterPos) { |
|||
lastCenterPos.x *= w; |
|||
lastCenterPos.y *= h; |
|||
const center = this.pointToLatLng(lastCenterPos.x, lastCenterPos.y); |
|||
this.map.panTo(center, { animate: false }); |
|||
} |
|||
(this.map as any)._enforcingBounds = false; |
|||
} |
|||
|
|||
private loadImageLayerData(): Observable<ImageLayerData> { |
|||
const imageSource = this.settings.imageSource; |
|||
if (imageSource.sourceType === ImageSourceType.image) { |
|||
return this.imageFromUrl(imageSource.url); |
|||
} else { |
|||
const datasource = imageMapSourceSettingsToDatasource(imageSource); |
|||
const result = new ReplaySubject<[DataSet, boolean]>(); |
|||
let isUpdate = false; |
|||
const imageUrlSubscriptionOptions: WidgetSubscriptionOptions = { |
|||
datasources: [datasource], |
|||
hasDataPageLink: true, |
|||
singleEntity: true, |
|||
useDashboardTimewindow: false, |
|||
type: widgetType.latest, |
|||
callbacks: { |
|||
onDataUpdated: (subscription) => { |
|||
if (isNotEmptyStr(subscription.data[0]?.data[0]?.[1])) { |
|||
result.next([subscription.data[0].data, isUpdate]); |
|||
} else { |
|||
result.next([[[0, imageSource.url]], isUpdate]); |
|||
} |
|||
isUpdate = true; |
|||
} |
|||
} |
|||
}; |
|||
this.ctx.subscriptionApi.createSubscription(imageUrlSubscriptionOptions, true).subscribe((subscription) => { |
|||
const pageLink: EntityDataPageLink = { |
|||
page: 0, |
|||
pageSize: 1, |
|||
textSearch: null, |
|||
dynamic: true |
|||
}; |
|||
subscription.subscribeAllForPaginatedData(pageLink, null); |
|||
}); |
|||
return this.imageFromEntityData(result); |
|||
} |
|||
} |
|||
|
|||
private imageFromUrl(url: string, update = false): Observable<ImageLayerData> { |
|||
return loadImageWithAspect(this.ctx.$injector.get(ImagePipe), url).pipe( |
|||
switchMap( aspectImage => { |
|||
if (aspectImage) { |
|||
return of({ |
|||
imageUrl: aspectImage.url, |
|||
aspect: aspectImage.aspect, |
|||
update |
|||
}); |
|||
} else { |
|||
return this.imageFromUrl(defaultImageMapSourceSettings.url, update); |
|||
} |
|||
} |
|||
), |
|||
catchError(() => this.imageFromUrl(defaultImageMapSourceSettings.url, update)) |
|||
); |
|||
} |
|||
|
|||
private imageFromEntityData(entityData: Observable<[DataSet, boolean]>): Observable<ImageLayerData> { |
|||
return entityData.pipe( |
|||
switchMap(res => { |
|||
const update = res[1]; |
|||
const url = res[0][0][1]; |
|||
const layerData: ImageLayerData = { |
|||
imageUrl: null, |
|||
aspect: null, |
|||
update |
|||
}; |
|||
return loadImageWithAspect(this.ctx.$injector.get(ImagePipe), url).pipe( |
|||
switchMap((aspectImage) => { |
|||
if (aspectImage) { |
|||
layerData.aspect = aspectImage.aspect; |
|||
layerData.imageUrl = aspectImage.url; |
|||
return of(layerData); |
|||
} else { |
|||
return this.imageFromUrl(defaultImageMapSourceSettings.url, update); |
|||
} |
|||
}), |
|||
catchError(() => this.imageFromUrl(defaultImageMapSourceSettings.url, update)) |
|||
); |
|||
}) |
|||
); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
<!-- |
|||
|
|||
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-image-map-source-settings tb-form-panel stroked" [formGroup]="imageMapSourceFormGroup"> |
|||
<div class="flex flex-row items-center justify-between"> |
|||
<div class="tb-form-panel-title"> |
|||
{{ 'widgets.maps.image.image-source' | translate }} |
|||
</div> |
|||
<tb-toggle-select formControlName="sourceType"> |
|||
<tb-toggle-option [value]="ImageSourceType.image">{{ 'widgets.maps.image.image-source-image' | translate }}</tb-toggle-option> |
|||
<tb-toggle-option [value]="ImageSourceType.entityKey">{{ 'widgets.maps.image.image-source-entity-key' | translate }}</tb-toggle-option> |
|||
</tb-toggle-select> |
|||
</div> |
|||
<tb-gallery-image-input *ngIf="ImageSourceType.image === imageMapSourceFormGroup.get('sourceType').value" |
|||
required |
|||
formControlName="url"> |
|||
</tb-gallery-image-input> |
|||
<ng-container *ngIf="ImageSourceType.entityKey === imageMapSourceFormGroup.get('sourceType').value"> |
|||
<div class="tb-form-row column-xs"> |
|||
<div class="fixed-title-width" translate>widgets.maps.image.source-entity-alias</div> |
|||
<tb-entity-alias-select |
|||
class="flex-1" |
|||
tbRequired |
|||
inlineField |
|||
[aliasController]="context.aliasController" |
|||
formControlName="entityAliasId" |
|||
[callbacks]="context.callbacks"> |
|||
</tb-entity-alias-select> |
|||
</div> |
|||
<div class="tb-form-row tb-standard-fields column-xs"> |
|||
<div class="fixed-title-width" translate>widgets.maps.image.image-url-key</div> |
|||
<tb-data-key-input |
|||
class="flex-1" |
|||
required |
|||
requiredText="widgets.maps.image.image-url-key-required" |
|||
[datasourceType]="DatasourceType.entity" |
|||
[entityAliasId]="imageMapSourceFormGroup.get('entityAliasId').value" |
|||
[aliasController]="context.aliasController" |
|||
[widgetType]="widgetType.latest" |
|||
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]" |
|||
[callbacks]="context.callbacks" |
|||
[generateKey]="context.generateDataKey" |
|||
(keyEdit)="editKey()" |
|||
formControlName="entityKey"> |
|||
</tb-data-key-input> |
|||
</div> |
|||
</ng-container> |
|||
</div> |
|||
@ -0,0 +1,157 @@ |
|||
///
|
|||
/// 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 { |
|||
ControlValueAccessor, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormBuilder, |
|||
UntypedFormControl, |
|||
UntypedFormGroup, |
|||
Validator, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { ImageMapSourceSettings, ImageSourceType } from '@home/components/widget/lib/maps/models/map.models'; |
|||
import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models'; |
|||
import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; |
|||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-image-map-source-settings', |
|||
templateUrl: './image-map-source-settings.component.html', |
|||
styleUrls: [], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => ImageMapSourceSettingsComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => ImageMapSourceSettingsComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class ImageMapSourceSettingsComponent implements OnInit, ControlValueAccessor, Validator { |
|||
|
|||
ImageSourceType = ImageSourceType; |
|||
DatasourceType = DatasourceType; |
|||
widgetType = widgetType; |
|||
DataKeyType = DataKeyType; |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
context: MapSettingsContext; |
|||
|
|||
private modelValue: ImageMapSourceSettings; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
public imageMapSourceFormGroup: UntypedFormGroup; |
|||
|
|||
constructor(private fb: UntypedFormBuilder, |
|||
private destroyRef: DestroyRef) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.imageMapSourceFormGroup = this.fb.group({ |
|||
sourceType: [null, [Validators.required]], |
|||
url: [null, [Validators.required]], |
|||
entityAliasId: [null, [Validators.required]], |
|||
entityKey: [null, [Validators.required]] |
|||
}); |
|||
this.imageMapSourceFormGroup.valueChanges.pipe( |
|||
takeUntilDestroyed(this.destroyRef) |
|||
).subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
this.imageMapSourceFormGroup.get('sourceType').valueChanges.pipe( |
|||
takeUntilDestroyed(this.destroyRef) |
|||
).subscribe(() => { |
|||
this.updateValidators(); |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(_fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.imageMapSourceFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.imageMapSourceFormGroup.enable({emitEvent: false}); |
|||
this.updateValidators(); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: ImageMapSourceSettings): void { |
|||
this.modelValue = value; |
|||
this.imageMapSourceFormGroup.patchValue( |
|||
value, {emitEvent: false} |
|||
); |
|||
this.updateValidators(); |
|||
} |
|||
|
|||
editKey() { |
|||
const entityKey: DataKey = this.imageMapSourceFormGroup.get('entityKey').value; |
|||
this.context.editKey(entityKey, |
|||
null, this.imageMapSourceFormGroup.get('entityAliasId').value).subscribe( |
|||
(updatedDataKey) => { |
|||
if (updatedDataKey) { |
|||
this.imageMapSourceFormGroup.get('entityKey').patchValue(updatedDataKey); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
|
|||
public validate(c: UntypedFormControl) { |
|||
const valid = this.imageMapSourceFormGroup.valid; |
|||
return valid ? null : { |
|||
imageMapSource: { |
|||
valid: false, |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
|
|||
private updateValidators() { |
|||
const sourceType: ImageSourceType = this.imageMapSourceFormGroup.get('sourceType').value; |
|||
if (sourceType === ImageSourceType.image) { |
|||
this.imageMapSourceFormGroup.get('url').enable({emitEvent: false}); |
|||
this.imageMapSourceFormGroup.get('entityAliasId').disable({emitEvent: false}); |
|||
this.imageMapSourceFormGroup.get('entityKey').disable({emitEvent: false}); |
|||
} else { |
|||
this.imageMapSourceFormGroup.get('url').disable({emitEvent: false}); |
|||
this.imageMapSourceFormGroup.get('entityAliasId').enable({emitEvent: false}); |
|||
this.imageMapSourceFormGroup.get('entityKey').enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
private updateModel() { |
|||
this.modelValue = this.imageMapSourceFormGroup.getRawValue(); |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue