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