56 changed files with 866 additions and 46 deletions
@ -0,0 +1,90 @@ |
|||||
|
import { |
||||
|
defaultOpenStreetMapLayerSettings, |
||||
|
MapLayerSettings, |
||||
|
MapProvider, OpenStreetLayerType, |
||||
|
OpenStreetMapLayerSettings, openStreetMapLayerTranslationMap |
||||
|
} from '@home/components/widget/lib/maps/map.models'; |
||||
|
import { WidgetContext } from '@home/models/widget-component.models'; |
||||
|
import { DeepPartial } from '@shared/models/common'; |
||||
|
import { mergeDeep } from '@core/utils'; |
||||
|
import { Observable, of } from 'rxjs'; |
||||
|
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; |
||||
|
import L from 'leaflet'; |
||||
|
import { map } from 'rxjs/operators'; |
||||
|
|
||||
|
export abstract class TbMapLayer<S extends MapLayerSettings> { |
||||
|
|
||||
|
static fromSettings(ctx: WidgetContext, |
||||
|
inputSettings: DeepPartial<MapLayerSettings>) { |
||||
|
|
||||
|
switch (inputSettings.provider) { |
||||
|
case MapProvider.google: |
||||
|
break; |
||||
|
case MapProvider.openstreet: |
||||
|
return new TbOpenStreetMapLayer(ctx, inputSettings); |
||||
|
case MapProvider.tencent: |
||||
|
break; |
||||
|
case MapProvider.here: |
||||
|
break; |
||||
|
case MapProvider.custom: |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected settings: S; |
||||
|
|
||||
|
protected constructor(protected ctx: WidgetContext, |
||||
|
protected inputSettings: DeepPartial<MapLayerSettings>) { |
||||
|
this.settings = mergeDeep({} as S, this.defaultSettings(), this.inputSettings as S); |
||||
|
} |
||||
|
|
||||
|
protected abstract defaultSettings(): S; |
||||
|
|
||||
|
protected title(): string { |
||||
|
const customTranslate = this.ctx.$injector.get(CustomTranslatePipe); |
||||
|
if (this.settings.label) { |
||||
|
return customTranslate.transform(this.settings.label); |
||||
|
} else { |
||||
|
return this.generateTitle(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected abstract generateTitle(): string; |
||||
|
|
||||
|
protected abstract createLayer(): Observable<L.Layer>; |
||||
|
|
||||
|
public loadLayer(): Observable<{title: string, layer: L.Layer}> { |
||||
|
return this.createLayer().pipe( |
||||
|
map((layer) => { |
||||
|
return { |
||||
|
title: this.title(), |
||||
|
layer |
||||
|
}; |
||||
|
}) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
class TbOpenStreetMapLayer extends TbMapLayer<OpenStreetMapLayerSettings> { |
||||
|
|
||||
|
constructor(protected ctx: WidgetContext, |
||||
|
protected inputSettings: DeepPartial<MapLayerSettings>) { |
||||
|
super(ctx, inputSettings); |
||||
|
} |
||||
|
|
||||
|
protected defaultSettings(): OpenStreetMapLayerSettings { |
||||
|
return defaultOpenStreetMapLayerSettings; |
||||
|
} |
||||
|
|
||||
|
protected generateTitle(): string { |
||||
|
const layerType = OpenStreetLayerType[this.settings.layerType]; |
||||
|
return this.ctx.translate.instant(openStreetMapLayerTranslationMap.get(layerType)); |
||||
|
} |
||||
|
|
||||
|
protected createLayer(): Observable<L.Layer> { |
||||
|
const layer = L.tileLayer.provider(OpenStreetLayerType[this.settings.layerType]); |
||||
|
return of(layer); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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-map-panel" [style.padding]="padding" [style]="backgroundStyle$ | async"> |
||||
|
<div class="tb-map-overlay" [style]="overlayStyle"></div> |
||||
|
<ng-container *ngIf="widgetComponent.dashboardWidget.showWidgetTitlePanel"> |
||||
|
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container> |
||||
|
</ng-container> |
||||
|
<div #mapElement> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,38 @@ |
|||||
|
/** |
||||
|
* 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-panel { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 8px; |
||||
|
padding: 20px 24px 24px 24px; |
||||
|
> div:not(.tb-map-overlay) { |
||||
|
z-index: 1; |
||||
|
} |
||||
|
.tb-map-overlay { |
||||
|
position: absolute; |
||||
|
top: 12px; |
||||
|
left: 12px; |
||||
|
bottom: 12px; |
||||
|
right: 12px; |
||||
|
} |
||||
|
div.tb-widget-title { |
||||
|
padding: 0; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,97 @@ |
|||||
|
///
|
||||
|
/// 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 { |
||||
|
AfterViewInit, |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
ElementRef, |
||||
|
Input, |
||||
|
OnDestroy, |
||||
|
OnInit, |
||||
|
Renderer2, |
||||
|
TemplateRef, |
||||
|
ViewChild, |
||||
|
ViewEncapsulation |
||||
|
} from '@angular/core'; |
||||
|
import { mapWidgetDefaultSettings, MapWidgetSettings } from '@home/components/widget/lib/maps/map-widget.models'; |
||||
|
import { WidgetContext } from '@home/models/widget-component.models'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models'; |
||||
|
import { TbMap } from '@home/components/widget/lib/maps/map'; |
||||
|
import { MapSetting } from '@home/components/widget/lib/maps/map.models'; |
||||
|
import { WidgetComponent } from '@home/components/widget/widget.component'; |
||||
|
import { ImagePipe } from '@shared/pipe/image.pipe'; |
||||
|
import { DomSanitizer } from '@angular/platform-browser'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-map-widget', |
||||
|
templateUrl: './map-widget.component.html', |
||||
|
styleUrls: ['./map-widget.component.scss'], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class MapWidgetComponent implements OnInit, OnDestroy, AfterViewInit { |
||||
|
|
||||
|
@ViewChild('mapElement', {static: false}) |
||||
|
mapElement: ElementRef<HTMLElement>; |
||||
|
|
||||
|
settings: MapWidgetSettings; |
||||
|
|
||||
|
@Input() |
||||
|
ctx: WidgetContext; |
||||
|
|
||||
|
@Input() |
||||
|
widgetTitlePanel: TemplateRef<any>; |
||||
|
|
||||
|
backgroundStyle$: Observable<ComponentStyle>; |
||||
|
overlayStyle: ComponentStyle = {}; |
||||
|
padding: string; |
||||
|
|
||||
|
private map: TbMap<MapSetting>; |
||||
|
|
||||
|
constructor(public widgetComponent: WidgetComponent, |
||||
|
private imagePipe: ImagePipe, |
||||
|
private sanitizer: DomSanitizer, |
||||
|
private renderer: Renderer2, |
||||
|
private cd: ChangeDetectorRef) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.ctx.$scope.mapWidget = this; |
||||
|
this.settings = {...mapWidgetDefaultSettings, ...this.ctx.settings}; |
||||
|
|
||||
|
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); |
||||
|
this.overlayStyle = overlayStyle(this.settings.background.overlay); |
||||
|
this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding; |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit() { |
||||
|
this.map = TbMap.fromSettings(this.ctx, this.settings, this.mapElement.nativeElement); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
if (this.map) { |
||||
|
this.map.destroy(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public onInit() { |
||||
|
const borderRadius = this.ctx.$widgetElement.css('borderRadius'); |
||||
|
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; |
||||
|
this.cd.detectChanges(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
///
|
||||
|
/// 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 { defaultMapSettings, MapSetting } from '@home/components/widget/lib/maps/map.models'; |
||||
|
import { BackgroundSettings, BackgroundType } from '@shared/models/widget-settings.models'; |
||||
|
import { mergeDeep } from '@core/utils'; |
||||
|
|
||||
|
export interface MapWidgetSettings extends MapSetting { |
||||
|
background: BackgroundSettings; |
||||
|
padding: string; |
||||
|
} |
||||
|
|
||||
|
export const mapWidgetDefaultSettings: MapWidgetSettings = |
||||
|
mergeDeep({} as MapWidgetSettings, defaultMapSettings as MapWidgetSettings, { |
||||
|
background: { |
||||
|
type: BackgroundType.color, |
||||
|
color: '#fff', |
||||
|
overlay: { |
||||
|
enabled: false, |
||||
|
color: 'rgba(255,255,255,0.72)', |
||||
|
blur: 3 |
||||
|
} |
||||
|
}, |
||||
|
padding: '12px' |
||||
|
} as MapWidgetSettings); |
||||
@ -0,0 +1,310 @@ |
|||||
|
///
|
||||
|
/// 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 { DataKey, DatasourceType } from '@shared/models/widget.models'; |
||||
|
import { EntityType } from '@shared/models/entity-type.models'; |
||||
|
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { mergeDeep } from '@core/utils'; |
||||
|
|
||||
|
export enum MapType { |
||||
|
geoMap = 'geoMap', |
||||
|
image = 'image' |
||||
|
} |
||||
|
|
||||
|
export interface MapDataSourceSettings { |
||||
|
dsType: DatasourceType; |
||||
|
dsEntityType?: EntityType; |
||||
|
dsEntityId?: string; |
||||
|
dsEntityAliasId?: string; |
||||
|
dsFilterId?: string; |
||||
|
} |
||||
|
|
||||
|
export interface MapDataLayerSettings extends MapDataSourceSettings { |
||||
|
additionalDataKeys?: DataKey[]; |
||||
|
group?: string; |
||||
|
} |
||||
|
|
||||
|
export interface MarkersDataLayerSettings extends MapDataLayerSettings { |
||||
|
xKey: DataKey; |
||||
|
yKey: DataKey; |
||||
|
} |
||||
|
|
||||
|
export const defaultMarkersDataLayerSettings = (mapType: MapType): MarkersDataLayerSettings => ({ |
||||
|
dsType: DatasourceType.entity, |
||||
|
xKey: { |
||||
|
name: MapType.geoMap === mapType ? 'latitude' : 'xPos', |
||||
|
label: MapType.geoMap === mapType ? 'latitude' : 'xPos', |
||||
|
type: DataKeyType.attribute |
||||
|
}, |
||||
|
yKey: { |
||||
|
name: MapType.geoMap === mapType ? 'longitude' : 'yPos', |
||||
|
label: MapType.geoMap === mapType ? 'longitude' : 'yPos', |
||||
|
type: DataKeyType.attribute |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
export interface PolygonsDataLayerSettings extends MapDataLayerSettings { |
||||
|
polygonKey: DataKey; |
||||
|
} |
||||
|
|
||||
|
export const defaultPolygonsDataLayerSettings: PolygonsDataLayerSettings = { |
||||
|
dsType: DatasourceType.entity, |
||||
|
polygonKey: { |
||||
|
name: 'perimeter', |
||||
|
label: 'perimeter', |
||||
|
type: DataKeyType.attribute |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export interface CirclesDataLayerSettings extends MapDataLayerSettings { |
||||
|
circleKey: DataKey; |
||||
|
} |
||||
|
|
||||
|
export const defaultCirclesDataLayerSettings: CirclesDataLayerSettings = { |
||||
|
dsType: DatasourceType.entity, |
||||
|
circleKey: { |
||||
|
name: 'perimeter', |
||||
|
label: 'perimeter', |
||||
|
type: DataKeyType.attribute |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export interface AdditionalMapDataSourceSettings extends MapDataSourceSettings { |
||||
|
dataKeys: DataKey[]; |
||||
|
} |
||||
|
|
||||
|
export enum MapControlsPosition { |
||||
|
topleft = 'topleft', |
||||
|
topright = 'topright', |
||||
|
bottomleft = 'bottomleft', |
||||
|
bottomright = 'bottomright' |
||||
|
} |
||||
|
|
||||
|
export enum MapZoomAction { |
||||
|
scroll = 'scroll', |
||||
|
doubleClick = 'doubleClick', |
||||
|
controlButtons = 'controlButtons' |
||||
|
} |
||||
|
|
||||
|
export interface BaseMapSettings { |
||||
|
mapType: MapType; |
||||
|
markers: MarkersDataLayerSettings[]; |
||||
|
polygons: PolygonsDataLayerSettings[]; |
||||
|
circles: CirclesDataLayerSettings[]; |
||||
|
additionalDataSources: AdditionalMapDataSourceSettings[]; |
||||
|
controlsPosition: MapControlsPosition; |
||||
|
zoomActions: MapZoomAction[]; |
||||
|
fitMapBounds: boolean; |
||||
|
useDefaultCenterPosition: boolean; |
||||
|
defaultCenterPosition?: string; |
||||
|
defaultZoomLevel: number; |
||||
|
mapPageSize: number; |
||||
|
} |
||||
|
|
||||
|
export const DEFAULT_MAP_PAGE_SIZE = 16384; |
||||
|
export const DEFAULT_ZOOM_LEVEL = 8; |
||||
|
|
||||
|
export const defaultBaseMapSettings: BaseMapSettings = { |
||||
|
mapType: MapType.geoMap, |
||||
|
markers: [], |
||||
|
polygons: [], |
||||
|
circles: [], |
||||
|
additionalDataSources: [], |
||||
|
controlsPosition: MapControlsPosition.topleft, |
||||
|
zoomActions: [MapZoomAction.scroll, MapZoomAction.doubleClick, MapZoomAction.controlButtons], |
||||
|
fitMapBounds: true, |
||||
|
useDefaultCenterPosition: false, |
||||
|
defaultCenterPosition: '0,0', |
||||
|
defaultZoomLevel: null, |
||||
|
mapPageSize: DEFAULT_MAP_PAGE_SIZE |
||||
|
}; |
||||
|
|
||||
|
export enum MapProvider { |
||||
|
google = 'google-map', |
||||
|
openstreet = 'openstreet-map', |
||||
|
here = 'here', |
||||
|
tencent = 'tencent-map', |
||||
|
custom = 'custom' |
||||
|
} |
||||
|
|
||||
|
export interface MapLayerSettings { |
||||
|
label?: string; |
||||
|
provider: MapProvider; |
||||
|
} |
||||
|
|
||||
|
export enum GoogleLayerType { |
||||
|
roadmap = 'roadmap', |
||||
|
satellite = 'satellite', |
||||
|
hybrid = 'hybrid', |
||||
|
terrain = 'terrain' |
||||
|
} |
||||
|
|
||||
|
export interface GoogleMapLayerSettings extends MapLayerSettings { |
||||
|
provider: MapProvider.google; |
||||
|
layerType: GoogleLayerType; |
||||
|
apiKey: string; |
||||
|
} |
||||
|
|
||||
|
export const defaultGoogleMapLayerSettings: GoogleMapLayerSettings = { |
||||
|
provider: MapProvider.google, |
||||
|
layerType: GoogleLayerType.roadmap, |
||||
|
apiKey: 'AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q' |
||||
|
}; |
||||
|
|
||||
|
export enum OpenStreetLayerType { |
||||
|
openStreetMapnik = 'OpenStreetMap.Mapnik', |
||||
|
openStreetHot = 'OpenStreetMap.HOT', |
||||
|
esriWorldStreetMap = 'Esri.WorldStreetMap', |
||||
|
esriWorldTopoMap = 'Esri.WorldTopoMap', |
||||
|
esriWorldImagery = 'Esri.WorldImagery', |
||||
|
cartoDbPositron = 'CartoDB.Positron', |
||||
|
cartoDbDarkMatter = 'CartoDB.DarkMatter' |
||||
|
} |
||||
|
|
||||
|
export const openStreetMapLayerTranslationMap = new Map<OpenStreetLayerType, string>( |
||||
|
[ |
||||
|
[OpenStreetLayerType.openStreetMapnik, 'widgets.maps.openstreet-provider-mapnik'], |
||||
|
[OpenStreetLayerType.openStreetHot, 'widgets.maps.openstreet-provider-hot'], |
||||
|
[OpenStreetLayerType.esriWorldStreetMap, 'widgets.maps.openstreet-provider-esri-street'], |
||||
|
[OpenStreetLayerType.esriWorldTopoMap, 'widgets.maps.openstreet-provider-esri-topo'], |
||||
|
[OpenStreetLayerType.esriWorldImagery, 'widgets.maps.openstreet-provider-esri-imagery'], |
||||
|
[OpenStreetLayerType.cartoDbPositron, 'widgets.maps.openstreet-provider-cartodb-positron'], |
||||
|
[OpenStreetLayerType.cartoDbDarkMatter, 'widgets.maps.openstreet-provider-cartodb-dark-matter'] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export interface OpenStreetMapLayerSettings extends MapLayerSettings { |
||||
|
provider: MapProvider.openstreet; |
||||
|
layerType: OpenStreetLayerType; |
||||
|
} |
||||
|
|
||||
|
export const defaultOpenStreetMapLayerSettings: OpenStreetMapLayerSettings = { |
||||
|
provider: MapProvider.openstreet, |
||||
|
layerType: OpenStreetLayerType.openStreetMapnik |
||||
|
} |
||||
|
|
||||
|
export enum HereLayerType { |
||||
|
hereNormalDay = 'HERE.normalDay', |
||||
|
hereNormalNight = 'HERE.normalNight', |
||||
|
hereHybridDay = 'HERE.hybridDay', |
||||
|
hereTerrainDay = 'HERE.terrainDay' |
||||
|
} |
||||
|
|
||||
|
export interface HereMapLayerSettings extends MapLayerSettings { |
||||
|
provider: MapProvider.here; |
||||
|
layerType: HereLayerType; |
||||
|
apiKey: string; |
||||
|
} |
||||
|
|
||||
|
export const defaultHereMapLayerSettings: HereMapLayerSettings = { |
||||
|
provider: MapProvider.here, |
||||
|
layerType: HereLayerType.hereNormalDay, |
||||
|
apiKey: 'kVXykxAfZ6LS4EbCTO02soFVfjA7HoBzNVVH9u7nzoE' |
||||
|
} |
||||
|
|
||||
|
export enum TencentLayerType { |
||||
|
roadmap = 'roadmap', |
||||
|
satellite = 'satellite', |
||||
|
hybrid = 'hybrid' |
||||
|
} |
||||
|
|
||||
|
export interface TencentMapLayerSettings extends MapLayerSettings { |
||||
|
provider: MapProvider.tencent; |
||||
|
layerType: TencentLayerType |
||||
|
apiKey: string; |
||||
|
} |
||||
|
|
||||
|
export const defaultTencentMapLayerSettings: TencentMapLayerSettings = { |
||||
|
provider: MapProvider.tencent, |
||||
|
layerType: TencentLayerType.roadmap, |
||||
|
apiKey: '84d6d83e0e51e481e50454ccbe8986b' |
||||
|
} |
||||
|
|
||||
|
export interface CustomMapLayerSettings extends MapLayerSettings { |
||||
|
provider: MapProvider.custom; |
||||
|
tileUrl: string; |
||||
|
} |
||||
|
|
||||
|
export const defaultCustomMapLayerSettings: CustomMapLayerSettings = { |
||||
|
provider: MapProvider.custom, |
||||
|
tileUrl: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' |
||||
|
} |
||||
|
|
||||
|
export const defaultMapLayerSettings = (provider: MapProvider): MapLayerSettings => { |
||||
|
switch (provider) { |
||||
|
case MapProvider.google: |
||||
|
return defaultGoogleMapLayerSettings; |
||||
|
case MapProvider.openstreet: |
||||
|
return defaultOpenStreetMapLayerSettings; |
||||
|
case MapProvider.here: |
||||
|
return defaultHereMapLayerSettings; |
||||
|
case MapProvider.tencent: |
||||
|
return defaultTencentMapLayerSettings; |
||||
|
case MapProvider.custom: |
||||
|
return defaultCustomMapLayerSettings; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export const defaultMapLayers: MapLayerSettings[] = (Object.keys(OpenStreetLayerType) as OpenStreetLayerType[]).map(type => ({ |
||||
|
provider: MapProvider.openstreet, |
||||
|
layerType: type |
||||
|
})); |
||||
|
|
||||
|
export interface GeoMapSettings extends BaseMapSettings { |
||||
|
layers?: MapLayerSettings[]; |
||||
|
} |
||||
|
|
||||
|
export const defaultGeoMapSettings: GeoMapSettings = { |
||||
|
mapType: MapType.geoMap, |
||||
|
layers: mergeDeep([], defaultMapLayers), |
||||
|
...mergeDeep({} as BaseMapSettings, defaultBaseMapSettings) |
||||
|
}; |
||||
|
|
||||
|
export enum ImageSourceType { |
||||
|
image = 'image', |
||||
|
attribute = 'attribute' |
||||
|
} |
||||
|
|
||||
|
export interface ImageMapSettings extends BaseMapSettings { |
||||
|
imageSourceType?: ImageSourceType; |
||||
|
imageUrl?: string; |
||||
|
imageEntityAlias?: string; |
||||
|
imageUrlAttribute?: string; |
||||
|
} |
||||
|
|
||||
|
export const defaultImageMapSettings: ImageMapSettings = { |
||||
|
mapType: MapType.image, |
||||
|
imageSourceType: ImageSourceType.image, |
||||
|
imageUrl: 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==', |
||||
|
...mergeDeep({} as BaseMapSettings, defaultBaseMapSettings) |
||||
|
} |
||||
|
|
||||
|
export type MapSetting = GeoMapSettings & ImageMapSettings; |
||||
|
|
||||
|
export const defaultMapSettings: MapSetting = defaultGeoMapSettings; |
||||
|
|
||||
|
export function parseCenterPosition(position: string | [number, number]): [number, number] { |
||||
|
if (typeof (position) === 'string') { |
||||
|
const parts = position.split(','); |
||||
|
if (parts.length === 2) { |
||||
|
return [Number(parts[0]), Number(parts[1])]; |
||||
|
} |
||||
|
} |
||||
|
if (typeof (position) === 'object') { |
||||
|
return position; |
||||
|
} |
||||
|
return [0, 0]; |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
.tb-map-layout { |
||||
|
display: flex; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
min-width: 0; |
||||
|
min-height: 0; |
||||
|
flex: 1; |
||||
|
&.tb-sidebar-left { |
||||
|
flex-direction: row-reverse; |
||||
|
} |
||||
|
&.tb-sidebar-right { |
||||
|
flex-direction: row; |
||||
|
} |
||||
|
.tb-map { |
||||
|
flex: 1; |
||||
|
} |
||||
|
.tb-map-sidebar { |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,196 @@ |
|||||
|
///
|
||||
|
/// 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 { |
||||
|
BaseMapSettings, |
||||
|
DEFAULT_ZOOM_LEVEL, |
||||
|
defaultGeoMapSettings, |
||||
|
defaultImageMapSettings, |
||||
|
GeoMapSettings, |
||||
|
ImageMapSettings, |
||||
|
MapSetting, |
||||
|
MapType, |
||||
|
MapZoomAction, |
||||
|
parseCenterPosition |
||||
|
} from '@home/components/widget/lib/maps/map.models'; |
||||
|
import { WidgetContext } from '@home/models/widget-component.models'; |
||||
|
import { mergeDeep } from '@core/utils'; |
||||
|
import { DeepPartial } from '@shared/models/common'; |
||||
|
import L from 'leaflet'; |
||||
|
import { forkJoin, Observable, of } from 'rxjs'; |
||||
|
import { TbMapLayer } from '@home/components/widget/lib/maps/map-layer'; |
||||
|
import { map } from 'rxjs/operators'; |
||||
|
|
||||
|
export abstract class TbMap<S extends BaseMapSettings> { |
||||
|
|
||||
|
static fromSettings(ctx: WidgetContext, |
||||
|
inputSettings: DeepPartial<MapSetting>, |
||||
|
mapElement: HTMLElement): TbMap<MapSetting> { |
||||
|
switch (inputSettings.mapType) { |
||||
|
case MapType.geoMap: |
||||
|
return new TbGeoMap(ctx, inputSettings, mapElement); |
||||
|
case MapType.image: |
||||
|
return new TbImageMap(ctx, inputSettings, mapElement); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected settings: S; |
||||
|
protected map: L.Map; |
||||
|
|
||||
|
protected defaultCenterPosition: [number, number]; |
||||
|
protected bounds: L.LatLngBounds; |
||||
|
protected layerControl: L.Control.Layers; |
||||
|
|
||||
|
protected mapElement: HTMLElement; |
||||
|
protected sidebarElement: HTMLElement; |
||||
|
|
||||
|
protected constructor(protected ctx: WidgetContext, |
||||
|
protected inputSettings: DeepPartial<S>, |
||||
|
protected containerElement: HTMLElement) { |
||||
|
this.settings = mergeDeep({} as S, this.defaultSettings(), this.inputSettings as S); |
||||
|
$(containerElement).empty(); |
||||
|
$(containerElement).addClass('tb-map-layout'); |
||||
|
if (this.settings.controlsPosition.endsWith('left')) { |
||||
|
$(containerElement).addClass('tb-sidebar-left'); |
||||
|
} else { |
||||
|
$(containerElement).addClass('tb-sidebar-right'); |
||||
|
} |
||||
|
const mapElement = $('<div class="tb-map"></div>'); |
||||
|
const sidebarElement = $('<div class="tb-map-sidebar"></div>'); |
||||
|
$(containerElement).append(mapElement); |
||||
|
$(containerElement).append(sidebarElement); |
||||
|
|
||||
|
this.mapElement = mapElement[0]; |
||||
|
this.sidebarElement = sidebarElement[0]; |
||||
|
|
||||
|
this.defaultCenterPosition = parseCenterPosition(this.settings.defaultCenterPosition); |
||||
|
|
||||
|
this.layerControl = L.control.layers({}, {}, {position: this.settings.controlsPosition, collapsed: true}); |
||||
|
this.createMap().subscribe((map) => { |
||||
|
this.map = map; |
||||
|
this.initMap(); |
||||
|
}); |
||||
|
L.TB = { |
||||
|
sidebar: (s) => { return null;} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private initMap() { |
||||
|
this.map.zoomControl.setPosition(this.settings.controlsPosition); |
||||
|
this.layerControl.addTo(this.map); |
||||
|
this.map.on('move', () => { |
||||
|
this.ctx.updatePopoverPositions(); |
||||
|
}); |
||||
|
this.map.on('zoomstart', () => { |
||||
|
this.ctx.setPopoversHidden(true); |
||||
|
}); |
||||
|
this.map.on('zoomend', () => { |
||||
|
this.ctx.setPopoversHidden(false); |
||||
|
this.ctx.updatePopoverPositions(); |
||||
|
setTimeout(() => { |
||||
|
this.ctx.updatePopoverPositions(); |
||||
|
}); |
||||
|
}); |
||||
|
if (this.settings.useDefaultCenterPosition) { |
||||
|
this.map.panTo(this.defaultCenterPosition); |
||||
|
this.bounds = this.map.getBounds(); |
||||
|
} else { |
||||
|
this.bounds = new L.LatLngBounds(null, null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected abstract defaultSettings(): S; |
||||
|
|
||||
|
protected abstract createMap(): Observable<L.Map>; |
||||
|
|
||||
|
public destroy() { |
||||
|
if (this.map) { |
||||
|
this.map.remove(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
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 theMap = 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) |
||||
|
}).setView(this.defaultCenterPosition, this.settings.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); |
||||
|
return this.loadLayers().pipe( |
||||
|
map((layers) => { |
||||
|
if (layers.length) { |
||||
|
const layer = layers[0]; |
||||
|
layer.layer.addTo(theMap); |
||||
|
if (layers.length > 1) { |
||||
|
layers.forEach(l => { |
||||
|
this.layerControl.addBaseLayer(l.layer, l.title); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
return theMap; |
||||
|
}) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private loadLayers(): Observable<{title: string, layer: L.Layer}[]> { |
||||
|
const layers = this.settings.layers.map(settings => TbMapLayer.fromSettings(this.ctx, settings)); |
||||
|
return forkJoin(layers.map(layer => layer.loadLayer())); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
|
|
||||
|
class TbImageMap extends TbMap<ImageMapSettings> { |
||||
|
|
||||
|
private maxZoom = 4; |
||||
|
|
||||
|
constructor(protected ctx: WidgetContext, |
||||
|
protected inputSettings: DeepPartial<ImageMapSettings>, |
||||
|
protected mapElement: HTMLElement) { |
||||
|
super(ctx, inputSettings, mapElement); |
||||
|
} |
||||
|
|
||||
|
protected defaultSettings(): ImageMapSettings { |
||||
|
return defaultImageMapSettings; |
||||
|
} |
||||
|
|
||||
|
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), |
||||
|
minZoom: 1, |
||||
|
maxZoom: this.maxZoom, |
||||
|
zoom: 1, |
||||
|
crs: L.CRS.Simple, |
||||
|
attributionControl: false |
||||
|
}).setView(this.defaultCenterPosition, this.settings.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); |
||||
|
return of(map); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue