UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1 lines • 104 kB
{"version":3,"file":"c8y-ngx-components-map.mjs","sources":["../../map/map.model.ts","../../map/map.service.ts","../../map/cluster-map.ts","../../map/map-popup.directive.ts","../../map/map.component.ts","../../map/map.component.html","../../map/cluster-map.component.ts","../../map/cluster-map.component.html","../../map/map-status.component.ts","../../map/map-status.component.html","../../map/map.module.ts","../../map/c8y-ngx-components-map.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\nimport { IEvent, IManagedObject } from '@c8y/client';\nimport type { MapDefaultConfig, MapTileLayer } from '@c8y/options';\nimport type * as L from 'leaflet';\nimport { Observable } from 'rxjs';\nimport { GlobalAutoRefreshWidgetConfig } from '@c8y/ngx-components';\n\nexport function getC8yMarker(marker: L.Marker, asset?: PositionManagedObject, event?: IEvent) {\n (marker as C8yMarker).asset = asset;\n (marker as C8yMarker).event = event;\n return marker as C8yMarker;\n}\n\nexport const MAP_TILE_LAYER = new InjectionToken<Observable<MapTileLayer[]>>('MAP_TILE_LAYER');\n\ntype RequireOnlyOne<T, Keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, Keys>> &\n { [K in Keys]-?: Required<Pick<T, K>> & Partial<Record<Exclude<Keys, K>, undefined>> }[Keys];\n\ninterface C8yMarkerAttr {\n asset: PositionManagedObject;\n event: IEvent;\n}\n\nexport type C8yMarkerAttributes = keyof C8yMarkerAttr;\n\nexport type C8yMarker = L.Marker & RequireOnlyOne<C8yMarkerAttr, 'asset' | 'event'>;\n\nexport enum ClusterSize {\n NONE = 0,\n FOUR = 1,\n SIXTEEN = 2\n}\n\nexport enum MapTenantOptionKeys {\n CONFIG = 'map-config',\n LAYERS = 'map-layers'\n}\n\nexport interface PositionManagedObject extends IManagedObject {\n c8y_Position: {\n lat: number;\n lng: number;\n alt?: number;\n };\n\n c8y_ActiveAlarmsStatus?: {\n minor: number;\n major: number;\n warning: number;\n critical: number;\n };\n\n icon?: {\n name: string;\n };\n}\n\nexport type ClusterMapConfig = MapConfig & {\n center: [number, number];\n refreshInterval?: number | null;\n};\n\nexport type MapConfig = MapDefaultConfig & {\n follow?: boolean;\n realtime?: boolean;\n icon?: string;\n color?: string;\n disableZoom?: boolean;\n disablePan?: boolean;\n bounds?: L.LatLngBounds;\n fitBoundsOptions?: L.FitBoundsOptions;\n} & GlobalAutoRefreshWidgetConfig;\n\nexport const MAP_DEFAULT_CONFIG = new InjectionToken<Observable<MapDefaultConfig>>(\n 'MAP_DEFAULT_CONFIG'\n);\n\nexport const defaultLayer: MapTileLayer = {\n layerUrl: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',\n label: 'OpenStreetMap',\n priority: 1000,\n options: {\n maxZoom: 18,\n minZoom: 2,\n attribution:\n '&copy;<a href=\"http://www.openstreetmap.org/copyright\" rel=\"noreferrer nofollow\">OpenStreetMap</a>',\n noWrap: false\n }\n};\n\nexport const defaultMapConfig: MapDefaultConfig = {\n center: [51.23544, 6.79599], // Düsseldorf\n zoomLevel: 2\n};\n\nexport type MapStatusButtonsConfig = Record<'realtime', { show: boolean; disabled?: boolean }>;\n","import { Injectable } from '@angular/core';\nimport { IManagedObject, InventoryService, IResultList, Paging } from '@c8y/client';\nimport { OptionsService, ServiceRegistry } from '@c8y/ngx-components';\nimport type { MapDefaultConfig, MapTileLayer } from '@c8y/options';\nimport type * as L from 'leaflet';\nimport { latLng, latLngBounds } from 'leaflet';\nimport { get } from 'lodash-es';\nimport { combineLatest, defer, Observable, of } from 'rxjs';\nimport { map } from 'rxjs/operators';\nimport {\n ClusterSize,\n defaultLayer,\n defaultMapConfig,\n MapTenantOptionKeys,\n PositionManagedObject\n} from './map.model';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class MapService {\n /**\n * Returns asset icon status for highest alarm severity found in device object.\n * @param device Device that contains alarms information.\n * @returns Status string according to alarm severity\n */\n static getStatus(device: PositionManagedObject) {\n if (!device.c8y_ActiveAlarmsStatus) {\n return 'text-muted';\n }\n if (device.c8y_ActiveAlarmsStatus.critical) {\n return 'status critical';\n }\n if (device.c8y_ActiveAlarmsStatus.major) {\n return 'status major';\n }\n if (device.c8y_ActiveAlarmsStatus.minor) {\n return 'status minor';\n }\n if (device.c8y_ActiveAlarmsStatus.warning) {\n return 'status warning';\n }\n return 'text-muted';\n }\n /**\n * The devices that are maximal displayed in one cluster.\n */\n MAX_DEVICE_PER_CLUSTER = 200;\n /**\n * The count until the cluster is sized. There are a maximum of\n * three clusters: 1, 4 or 16.\n */\n CLUSTER_LEVEL_THRESHOLD = 500;\n\n /**\n * @ignore: Only DI.\n */\n constructor(\n private inventory: InventoryService,\n private options: OptionsService,\n private serviceRegistry: ServiceRegistry\n ) {}\n\n /**\n * Returns the leaflet instance used by the cumulocity core.\n */\n async getLeaflet() {\n const originalLeflet = window.L;\n const c8yLeafletInstance = (await import('leaflet')).default;\n c8yLeafletInstance.noConflict();\n window.L = originalLeflet;\n return c8yLeafletInstance;\n }\n\n /**\n * Verifies if a given managed object is a device with a position fragment.\n * @param mo The given managed object.\n */\n isPositionedDevice(mo: IManagedObject) {\n return !!(mo?.c8y_IsDevice && this.hasPosition(mo));\n }\n\n /**\n * Verifies if a given managed object has a position fragment.\n * @param mo The given managed object.\n */\n hasPosition(mo: IManagedObject) {\n return mo?.c8y_Position;\n }\n\n getMapTileLayerProviders(): CumulocityServiceRegistry.MapTileLayerProvider[] {\n const layerProviders = this.serviceRegistry.get('mapTileLayerHook');\n\n return layerProviders;\n }\n\n getMapTileLayersFromHookedProviders$(\n layerProviders: CumulocityServiceRegistry.MapTileLayerProvider[]\n ): Observable<MapTileLayer[]> {\n if (!layerProviders.length) {\n return of([]);\n }\n const layers = combineLatest(layerProviders.map(provider => provider.getMapTileLayers$()));\n return layers.pipe(map(layers => layers.flat()));\n }\n\n /**\n * Returns the layers available in this application.\n * Layers are taken from plugins installed to this application.\n * In case none of the plugins override the default layers, the default layers are also considered.\n * @returns The layers.\n */\n getMapTileLayers$(): Observable<MapTileLayer[]> {\n return defer(() => {\n const layerProviders = this.getMapTileLayerProviders();\n const overridesDefaultLayer = layerProviders.some(provider =>\n provider.overridesDefaultLayer?.()\n );\n const layersFromProviders = defer(() =>\n this.getMapTileLayersFromHookedProviders$(layerProviders)\n );\n if (overridesDefaultLayer) {\n return layersFromProviders;\n }\n\n return combineLatest([this.getDefaultLayers(), layersFromProviders]).pipe(\n map(layers => {\n return layers.flat();\n })\n );\n });\n }\n\n /**\n * Returns the layers configured in the current platform via tenant options.\n * @returns The layers. If not set in tenant options the default layers.\n */\n getDefaultLayers(): Observable<MapTileLayer[]> {\n return this.getMapOption<MapTileLayer[]>(\n MapTenantOptionKeys.LAYERS,\n this.options.mapLayers || [defaultLayer]\n );\n }\n\n /**\n * Returns the map configuration configured on the tenant.\n * @returns The configuration. If not set in tenant options the default configuration.\n */\n getDefaultConfig(): Observable<MapDefaultConfig> {\n return this.getMapOption<MapDefaultConfig>(\n MapTenantOptionKeys.CONFIG,\n this.options.mapConfig || defaultMapConfig\n );\n }\n\n /**\n * Counts all managed objects in a given bound with a c8y_Position fragment.\n * @param bound The lat lng bound to request the managed objects for.\n * @param byGroupIdMO The group managed object of which direct children should be searched for.\n * @returns The number of all position managed objects in the given bound (and group).\n */\n async getPositionMOsFromBoundCount(\n bound: L.LatLngBounds,\n byGroupIdMO?: IManagedObject\n ): Promise<number> {\n return this.getPositionMOsFromBound(bound, byGroupIdMO, true) as Promise<number>;\n }\n\n /**\n * Returns all managed objects with a c8y_Position fragment in a certain boundary.\n * @param bound The lat lng bound to request the managed objects for.\n * @returns All position managed objects in the given bound.\n */\n async getPositionMOsFromBound(bound: L.LatLngBounds): Promise<PositionManagedObject[]>;\n /**\n * Returns all managed objects with a c8y_Position fragment in a certain boundary that belongs to a certain group.\n * @param bound The lat lng bound to request the managed objects for.\n * @param byGroupIdMO The group managed object of which direct children should be searched for.\n * @returns All position managed objects in the given bound that are children of the given group.\n */\n async getPositionMOsFromBound(\n bound: L.LatLngBounds,\n byGroupIdMO: IManagedObject\n ): Promise<PositionManagedObject[]>;\n /**\n * Counts the managed objects in a certain boundary belonging to a group.\n * @param bound The lat lng bound to request the managed objects for.\n * @param byGroupIdMO The group managed object of which direct children should be searched for.\n * @return The count of the managed objects.\n */\n async getPositionMOsFromBound(\n bound: L.LatLngBounds,\n byGroupIdMO: IManagedObject,\n count: true\n ): Promise<number>;\n async getPositionMOsFromBound(\n bound: L.LatLngBounds,\n byGroupIdMO?: IManagedObject,\n count = false\n ): Promise<PositionManagedObject[] | number> {\n const { lat: latMin, lng: lngMinRaw } = bound.getSouthWest();\n const { lat: latMax, lng: lngMaxRaw } = bound.getNorthEast();\n\n const lngMin = lngMaxRaw - lngMinRaw > 360 ? -180 : this.normalizeLongitude(lngMinRaw);\n const lngMax = lngMaxRaw - lngMinRaw > 360 ? 180 : this.normalizeLongitude(lngMaxRaw);\n\n const byGroupIdFilter = byGroupIdMO\n ? `(bygroupid(${byGroupIdMO.id}) or id eq '${byGroupIdMO.id}') and `\n : '';\n let boundFilter = `$filter=${byGroupIdFilter}has(c8y_Position) and c8y_Position.lat gt ${latMin}d and c8y_Position.lat lt ${latMax}d`;\n\n if (lngMin < lngMax) {\n boundFilter = `${boundFilter} and c8y_Position.lng gt ${lngMin}d and c8y_Position.lng lt ${lngMax}d`;\n } else {\n boundFilter = `${boundFilter} and (c8y_Position.lng gt ${lngMin}d or c8y_Position.lng lt ${lngMax}d)`;\n }\n\n const { paging, data } = await this.inventory.list({\n pageSize: count ? 1 : this.MAX_DEVICE_PER_CLUSTER,\n withTotalPages: count,\n query: boundFilter\n });\n if (count) {\n return paging.totalPages;\n }\n return data.map((pmo: PositionManagedObject) =>\n bound.contains(latLng(pmo.c8y_Position.lat, pmo.c8y_Position.lng))\n ? pmo\n : this.denormalizePMO(pmo, bound)\n ) as PositionManagedObject[];\n }\n\n /**\n * Returns all devices with c8y_Position.\n */\n async getPositionDevices(): Promise<PositionManagedObject[]>;\n /**\n * Returns all devices with c8y_Position.\n * @param pageSize The page size to return.\n */\n async getPositionDevices(pageSize: number): Promise<PositionManagedObject[]>;\n /**\n * Returns all devices with c8y_Position.\n * @param pageSize The page size to return.\n * @param count Counting is disabled\n */\n async getPositionDevices(pageSize: number, count: false): Promise<PositionManagedObject[]>;\n /**\n * Returns the number of all devices with c8y_Position.\n * @param pageSize The page size to return.\n * @param count Counting is enabled\n */\n async getPositionDevices(pageSize: number, count: true): Promise<number>;\n /**\n * Returns all devices with c8y_Position.\n * @param pageSize The page size to return.\n * @param count Switches to counting only.\n * @returns All devices or the device count with a c8y_Position fragment.\n */\n async getPositionDevices(\n pageSize = this.MAX_DEVICE_PER_CLUSTER,\n count?: boolean\n ): Promise<number | PositionManagedObject[]> {\n const { paging, data } = await this.inventory.list({\n pageSize: count ? 1 : pageSize,\n withTotalPages: !!count,\n query: '$filter=has(c8y_Position) and has(c8y_IsDevice)'\n });\n if (count) {\n return paging.totalPages;\n }\n return data as PositionManagedObject[];\n }\n\n /**\n * Returns all managed object with a c8y_Position fragment.\n * @param byGroupIdMO The group managed object of which direct children should be searched for.\n * @param pageSize Defines how many results should be returned.\n * @returns The managed objects with position.\n */\n async getAllPositionMOs(\n byGroupIdMO?: IManagedObject,\n pageSize = 500\n ): Promise<IResultList<PositionManagedObject>> {\n const filter: { pageSize: number; withTotalPages: boolean; query: string } = {\n pageSize,\n withTotalPages: true,\n query: 'has(c8y_Position)'\n };\n\n if (byGroupIdMO) {\n filter.query = `$filter=(bygroupid(${byGroupIdMO.id}) or id eq '${byGroupIdMO.id}') and has(c8y_Position)`;\n }\n\n const { paging, data, res } = await this.inventory.list(filter);\n\n return {\n res,\n paging: paging as Paging<PositionManagedObject>,\n data: data as PositionManagedObject[]\n };\n }\n\n /**\n * Determines a rectangular geographical area based on the positions of all devices.\n *\n * @returns A [[LatLngBounds]] object fitting all devices' geo positions.\n */\n async getAllDevicesBounds(): Promise<L.LatLngBounds> {\n const filter = (coord: 'lat' | 'lng', order: 'asc' | 'desc') => ({\n pageSize: 1,\n q: `$filter=has(c8y_Position) $orderby=c8y_Position.${coord} ${order}`\n });\n\n const filterReverse = (op: 'lt' | 'gt', order: 'asc' | 'desc') => ({\n pageSize: 1,\n q: `$filter=has(c8y_Position) and c8y_Position.lng ${op} 0d $orderby=c8y_Position.lng ${order}`\n });\n\n const [latMin, latMax, lngMin, lngMax, lngRevMin, lngRevMax] = await Promise.all([\n this.inventory.list(filter('lat', 'asc')),\n this.inventory.list(filter('lat', 'desc')),\n this.inventory.list(filter('lng', 'asc')),\n this.inventory.list(filter('lng', 'desc')),\n this.inventory.list(filterReverse('gt', 'asc')),\n this.inventory.list(filterReverse('lt', 'desc'))\n ]).then(result => result.map(r => get(r.data, '[0].c8y_Position')));\n\n const shiftWorld = (lngRevMin?.lng ?? 0) - (lngRevMax?.lng ?? 0) > 180;\n\n return latLngBounds(\n latLng(latMin?.lat, shiftWorld ? lngRevMin?.lng : lngMin?.lng),\n latLng(latMax?.lat, shiftWorld ? lngRevMax?.lng + 360 : lngMax?.lng)\n );\n }\n\n /**\n * Returns the cluster size for clustered maps. Counting the position MOs in a bounding\n * and if it reach a threshold, returning a [[ClusterSize]].\n * @param bound The bounding to check for cluster size.\n * @returns The cluster size, can be NONE, FOUR or SIXTEEN.\n */\n async getClusterSize(bound: L.LatLngBounds) {\n const count = await this.getPositionMOsFromBoundCount(bound);\n let clusterSize = ClusterSize.NONE;\n if (count > this.CLUSTER_LEVEL_THRESHOLD) {\n clusterSize = ClusterSize.SIXTEEN;\n } else if (count > this.MAX_DEVICE_PER_CLUSTER) {\n clusterSize = ClusterSize.FOUR;\n }\n return clusterSize;\n }\n\n private getMapOption<T>(key: MapTenantOptionKeys, defaultValue: T) {\n return defer(() =>\n this.options.getTenantOption<T | string>('configuration', key, defaultValue)\n ).pipe(\n map(config => {\n if (typeof config === 'string') {\n console.error(\n `The tenant option for maps 'configuration.${key}' is not a valid JSON structure.`\n );\n return defaultValue;\n }\n return config;\n })\n );\n }\n\n /**\n * Shifts longitudes received from Leaflet.js in the [-180 - k*360; 180 + k*360] rangewhen\n * `noWrap` is enabled to the [-180; 180] range expected for values of the c8y_Position fragment.\n *\n * @param lng Longitude to shift.\n * @returns Longitude value in the [-180; 180] range\n */\n private normalizeLongitude(lng: number): number {\n return ((((lng + 180) % 360) + 360) % 360) - 180;\n }\n\n /**\n * Shifts longitudes in the [-180; 180] range expected for values of the c8y_Position fragment\n * the the [-180 - k*360; 180 + k*360] range expected from Leaflet.js when `noWrap` is enabled.\n *\n * The method naively adds/subtracts 360 degrees to the original value until the position fits in the expected bounds.\n *\n * @param pmo A managed object with a `c8y_Position` fragment\n * @param bounds The bounds where the position should fit\n * @returns A managed object whose `c8y_Position`'s `lng` values has been shifted to fit in bounds\n */\n private denormalizePMO(\n pmo: PositionManagedObject,\n bounds: L.LatLngBounds\n ): PositionManagedObject {\n let { lng } = pmo.c8y_Position;\n const shiftFactor = lng > bounds.getEast() ? -1 : 1;\n\n while (!bounds.contains(latLng(pmo.c8y_Position.lat, lng))) {\n lng += shiftFactor * 360;\n }\n\n pmo.c8y_Position.lng = lng;\n return pmo;\n }\n}\n","import { IterableChangeRecord, IterableDiffer, IterableDiffers } from '@angular/core';\nimport { TranslateService } from '@ngx-translate/core';\nimport type * as L from 'leaflet';\nimport { gettext } from '@c8y/ngx-components';\nimport { C8yMarker, PositionManagedObject } from './map.model';\nimport { MapService } from './map.service';\n\nexport class ClusterMap {\n markers: C8yMarker[] = [];\n positions: PositionManagedObject[] = [];\n\n set clusterMarker(item: L.Layer) {\n this.removeClusterToBigMarker();\n this._clusterMarker = item;\n }\n\n get clusterMarker() {\n return this._clusterMarker;\n }\n\n set rect(item: L.Rectangle) {\n if (this._rect) {\n this._rect.remove();\n }\n this._rect = item;\n }\n\n get rect() {\n return this._rect;\n }\n\n private _clusterMarker: L.Layer;\n private _rect: L.Rectangle;\n private iterableDiffer: IterableDiffer<PositionManagedObject> | null;\n\n constructor(\n private iterable: IterableDiffers,\n private addAssetCallback: (asset: PositionManagedObject) => C8yMarker,\n private translateService: TranslateService\n ) {\n this.iterableDiffer = this.iterable.find(this.positions).create(this.trackBy);\n }\n\n render(map: L.Map) {\n if (this._rect) {\n this._rect.addTo(map);\n }\n this.updateChanges(map);\n if (this._clusterMarker) {\n this._clusterMarker.addTo(map);\n }\n }\n\n clear(map: L.Map) {\n this.removeClusterToBigMarker();\n this._rect.remove();\n this.positions = [];\n this.updateChanges(map);\n }\n\n removeClusterToBigMarker() {\n if (this._clusterMarker) {\n this._clusterMarker.remove();\n this._clusterMarker = null;\n }\n }\n\n addMarkerToMap(device: PositionManagedObject, map: L.Map) {\n const marker = this.addAssetCallback(device);\n this.markers.push(marker);\n marker.addTo(map);\n }\n\n setClusterToBigMarker(map: L.Map, count, leaflet: typeof L) {\n const bound = this.rect.getBounds();\n const text = this.translateService.instant(gettext('Zoom in'));\n const divMarker = leaflet.divIcon({\n html: `<div class=\"c8y-map-marker-count\" data-count=\"${count}\" title=\"${text}\"></div>`\n });\n const labelIcon = leaflet.marker(bound.getCenter(), {\n icon: divMarker\n });\n labelIcon.addTo(map);\n labelIcon.on('click', () => {\n map.fitBounds(bound);\n });\n this.clusterMarker = labelIcon;\n }\n\n private updateChanges(map: L.Map) {\n const changes = this.iterableDiffer.diff(this.positions);\n if (changes) {\n changes.forEachRemovedItem((record: IterableChangeRecord<PositionManagedObject>) => {\n this.removeMarkerFromMap(record.item);\n });\n\n changes.forEachAddedItem((record: IterableChangeRecord<PositionManagedObject>) => {\n this.addMarkerToMap(record.item, map);\n });\n }\n }\n\n private trackBy(index: number, item: PositionManagedObject) {\n const trackItems = [\n item.id,\n item.c8y_Position.lat,\n item.c8y_Position.lng,\n MapService.getStatus(item)\n ];\n return trackItems.join('');\n }\n\n private removeMarkerFromMap(device: PositionManagedObject) {\n const markers = this.markers.filter((marker: C8yMarker) => marker.asset?.id === device.id);\n markers.forEach(marker => marker.remove());\n }\n}\n","import { Directive, ElementRef, TemplateRef, ViewContainerRef } from '@angular/core';\n\n@Directive({\n selector: '[c8yMapPopup]'\n})\nexport class MapPopupDirective {\n constructor(\n public template: TemplateRef<unknown>,\n public elementRef: ElementRef,\n public viewContainer: ViewContainerRef\n ) {}\n}\n","import {\n Component,\n ContentChild,\n ElementRef,\n EventEmitter,\n Inject,\n Input,\n Output,\n SimpleChange,\n SimpleChanges,\n ViewChild\n} from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { IEvent } from '@c8y/client';\nimport {\n DatePipe,\n GeoService,\n gettext,\n ManagedObjectRealtimeService,\n sortByPriority,\n WidgetGlobalAutoRefreshService\n} from '@c8y/ngx-components';\nimport type { MapDefaultConfig, MapTileLayer } from '@c8y/options';\nimport { TranslateService } from '@ngx-translate/core';\nimport type * as L from 'leaflet';\nimport { every, flatten, isEmpty, isNull, isUndefined, remove } from 'lodash-es';\nimport {\n BehaviorSubject,\n combineLatest,\n fromEvent,\n NEVER,\n Observable,\n Subject,\n Subscription\n} from 'rxjs';\nimport { filter, first, map, scan, switchMap, takeUntil } from 'rxjs/operators';\nimport { MapPopupDirective } from './map-popup.directive';\nimport {\n C8yMarker,\n C8yMarkerAttributes,\n getC8yMarker,\n MAP_DEFAULT_CONFIG,\n MAP_TILE_LAYER,\n MapConfig,\n PositionManagedObject\n} from './map.model';\nimport { MapService } from './map.service';\n\n@Component({\n selector: 'c8y-map',\n templateUrl: './map.component.html',\n providers: [ManagedObjectRealtimeService]\n})\nexport class MapComponent {\n /**\n * The leaflet map object.\n */\n map: L.Map;\n /**\n * The markers placed on the map.\n */\n markers: Array<C8yMarker | L.Marker> = [];\n /**\n * The leaflet representation used.\n */\n leaflet: typeof L;\n /**\n * Tells if the map was already initialized.\n */\n isInit = false;\n\n @ViewChild('map')\n mapElement: ElementRef;\n\n @ContentChild(MapPopupDirective)\n popup: MapPopupDirective;\n\n @Input()\n config: MapConfig = {};\n\n @Input()\n assets: PositionManagedObject | PositionManagedObject[];\n\n @Input()\n polyline$: Observable<L.LatLngExpression[] | L.LatLngExpression[][]> = NEVER;\n\n @Input()\n polylineOptions: L.PolylineOptions;\n\n @Output()\n onRealtimeUpdate = new EventEmitter<PositionManagedObject>();\n\n @Output()\n onMove: Observable<L.LeafletEvent>;\n\n @Output()\n onMoveEnd: Observable<L.LeafletEvent>;\n\n @Output()\n onZoomStart: Observable<L.LeafletEvent>;\n\n @Output()\n onZoomEnd: Observable<L.LeafletEvent>;\n\n @Output()\n onMap = new BehaviorSubject<L.Map | null>(null);\n\n @Output()\n onInit = new EventEmitter<typeof L>();\n\n protected realtimeSubscription: Subscription;\n protected unsubscribeTrigger$ = new Subject<void>();\n protected destroy$ = new Subject<void>();\n\n private markerTitle = gettext('Marker at position {{lat}}, {{lng}}');\n\n constructor(\n protected moRealtimeService: ManagedObjectRealtimeService,\n protected mapService: MapService,\n @Inject(MAP_TILE_LAYER) protected layers$: Observable<MapTileLayer[]>,\n @Inject(MAP_DEFAULT_CONFIG)\n protected defaultConfig$: Observable<MapDefaultConfig>,\n protected translateService: TranslateService,\n protected geo: GeoService,\n protected datePipe: DatePipe,\n protected widgetGlobalAutoRefreshService: WidgetGlobalAutoRefreshService\n ) {\n this.initOutputs();\n }\n\n startRealtime() {\n if (!this.assets || (Array.isArray(this.assets) && this.assets.length > 1)) {\n this.config.realtime = false;\n this.stopRealtime();\n return;\n }\n const asset = Array.isArray(this.assets) ? this.assets[0] : this.assets;\n this.realtimeSubscription = this.moRealtimeService\n .onUpdate$(asset)\n .subscribe((asset: PositionManagedObject) => {\n const marker = this.findMarker(asset.id);\n const icon = this.getAssetIcon(asset);\n marker.setIcon(icon);\n marker.setLatLng(this.geo.getLatLong(asset));\n if (Array.isArray(this.assets)) {\n this.assets[0] = asset;\n } else {\n this.assets = asset;\n }\n this.moveToPositionOfMo(asset);\n this.onRealtimeUpdate.emit(asset);\n });\n }\n\n moveToPositionOfMo(positions: PositionManagedObject | PositionManagedObject[]) {\n const position = Array.isArray(positions) ? positions[0] : positions;\n if (this.config.follow) {\n this.map.setView([position.c8y_Position.lat, position.c8y_Position.lng]);\n }\n }\n\n stopRealtime() {\n if (this.realtimeSubscription) {\n this.realtimeSubscription.unsubscribe();\n }\n }\n\n findMarker(moOrId: string | IEvent | PositionManagedObject) {\n const getId = moOrId => (typeof moOrId === 'string' ? moOrId : moOrId?.id);\n return this.markers.find(\n (marker: C8yMarker) =>\n marker.asset?.id === getId(moOrId) || marker.event?.id === getId(moOrId)\n );\n }\n\n addMarkerToMap(marker: C8yMarker | L.Marker) {\n this.markers.push(marker);\n marker.addTo(this.map);\n }\n\n getAssetMarker(asset: PositionManagedObject) {\n if (!asset) {\n return;\n }\n\n const icon = this.getAssetIcon(asset);\n const { lat, lng } = asset.c8y_Position;\n const leafletMarker = this.leaflet.marker(this.geo.getLatLong(asset), {\n icon,\n title: this.translateService.instant(this.markerTitle, { lat, lng })\n });\n const marker = getC8yMarker(leafletMarker, asset);\n this.bindPopup(marker, asset);\n\n return marker;\n }\n\n getTrackingMarker(event: IEvent) {\n if (!event) {\n return;\n }\n\n const icon = this.getTrackingIcon();\n const { lat, lng } = event.c8y_Position;\n const leafletMarker = this.leaflet.marker(this.geo.getLatLong(event), {\n icon,\n title: this.translateService.instant(this.markerTitle, { lat, lng })\n });\n const marker = getC8yMarker(leafletMarker, null, event);\n this.bindPopup(marker, event);\n\n return marker;\n }\n\n getAssetIcon(asset: PositionManagedObject) {\n const assetTypeIcon = this.config.icon || asset.icon?.name;\n const status = MapService.getStatus(asset);\n const color = this.config.color ? `style='color: ${this.config.color};'` : '';\n const icon = this.leaflet.divIcon({\n html: `<div class=\"dlt-c8y-icon-marker icon-3x ${status}\" ${color}><i class=\"dlt-c8y-icon-${\n assetTypeIcon || 'data-transfer'\n }\" /></div>`,\n className: 'c8y-map-marker-icon',\n // iconAnchor is used to set the marker accurately on click\n iconAnchor: [8, 8]\n });\n return icon;\n }\n\n getTrackingIcon() {\n const icon = this.leaflet.divIcon({\n html: `<div class=\"dlt-c8y-icon-marker icon-3x text-muted\"></div>`,\n className: 'c8y-map-marker-icon',\n // iconAnchor is used to set the marker accurately on click\n iconAnchor: [8, 8]\n });\n return icon;\n }\n\n removeMarker(marker: C8yMarker | L.Marker) {\n if (marker) {\n marker.remove();\n remove(this.markers, m => m === marker);\n }\n }\n\n clearMarkers(fragment?: C8yMarkerAttributes) {\n const matchingMarkers = marker => !fragment || !!marker[fragment];\n this.markers.filter(matchingMarkers).forEach(marker => marker.remove());\n this.markers = this.markers.filter(marker => !matchingMarkers(marker));\n }\n\n refreshMarkers() {\n this.clearMarkers();\n let assets: PositionManagedObject[] = [];\n if (!isUndefined(this.assets)) {\n assets = Array.isArray(this.assets) ? this.assets : [this.assets];\n }\n assets.forEach(asset => {\n const marker = this.getAssetMarker(asset);\n this.addMarkerToMap(marker);\n });\n if (!this.config.center) {\n this.zoomToBound(assets);\n }\n this.toggleControls();\n }\n\n center() {\n this.map?.setView(this.config.center);\n }\n\n refresh() {\n if (this.isInit) {\n this.refreshMarkers();\n }\n }\n\n protected async ngAfterViewInit() {\n this.leaflet = await this.mapService.getLeaflet();\n const initialized$ = combineLatest([this.layers$, this.defaultConfig$]).pipe(\n first(),\n takeUntil(this.unsubscribeTrigger$)\n );\n\n initialized$.subscribe(([layers, defaultConfig]) => {\n this.initMap(layers, defaultConfig);\n this.refreshMarkers();\n });\n\n combineLatest([this.polyline$, initialized$])\n .pipe(\n map(([expressions]) => this.leaflet.polyline(expressions, this.polylineOptions)),\n scan((oldPolyline, newPolyline) => {\n if (!!oldPolyline) {\n this.map.removeLayer(oldPolyline);\n }\n if (!!newPolyline) {\n newPolyline.addTo(this.map);\n this.fitBounds(newPolyline.getBounds());\n }\n\n return newPolyline;\n }, null),\n takeUntil(this.unsubscribeTrigger$)\n )\n .subscribe();\n }\n\n protected ngOnChanges(changes: SimpleChanges): void {\n if (!this.map) {\n return;\n }\n if (changes.assets?.currentValue && !changes.assets?.firstChange) {\n this.refreshMarkers();\n }\n\n if (changes.config?.currentValue && !changes.config?.firstChange) {\n this.changeConfig(changes.config);\n }\n }\n\n protected ngOnDestroy(): void {\n this.unsubscribeAllListeners();\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n protected unsubscribeAllListeners() {\n this.unsubscribeTrigger$.next();\n this.stopRealtime();\n }\n\n protected initOutputs() {\n const getMapEventObservable = eventName => {\n return this.onMap.pipe(\n filter(map => !!map),\n switchMap(map => fromEvent<L.LeafletEvent>(map, eventName)),\n takeUntilDestroyed()\n );\n };\n const dragStart$ = getMapEventObservable('dragstart');\n const move$ = getMapEventObservable('move');\n const dragEnd$ = getMapEventObservable('dragend');\n\n this.onMove = dragStart$.pipe(\n switchMap(() => move$.pipe(takeUntil(dragEnd$))),\n takeUntil(this.unsubscribeTrigger$)\n );\n\n this.onMoveEnd = getMapEventObservable('moveend');\n\n this.onZoomEnd = getMapEventObservable('zoomend');\n\n this.onZoomStart = getMapEventObservable('zoomstart');\n }\n\n protected initMap(layers: MapTileLayer[], defaultConfig: MapConfig): void {\n const defaultOptions = {\n center: this.config.center || defaultConfig.center,\n zoomSnap: 0,\n zoom: this.config.zoomLevel || defaultConfig.zoomLevel,\n worldCopyJump: true\n };\n\n if (this.map) {\n this.map.remove();\n }\n this.map = this.leaflet.map(this.mapElement.nativeElement, defaultOptions);\n\n this.map.attributionControl.setPrefix('');\n\n this.fitBounds(this.config.bounds);\n\n this.addLayers(layers);\n\n this.handleMobile();\n\n this.onMap.next(this.map);\n\n if (this.config.realtime) {\n this.startRealtime();\n }\n\n this.isInit = true;\n this.onInit.emit(this.leaflet);\n }\n\n protected handleMobile() {\n // adding event listener to do mobile 2 finger scrolling\n if (this.leaflet.Browser.mobile) {\n const touchMsg = this.translateService.instant(gettext('Use two fingers to move the map.'));\n this.map.dragging.disable();\n const container = this.map.getContainer();\n container.setAttribute('data-touch-warning-content', touchMsg);\n container.addEventListener('touchstart', event => this.handleTouch(event));\n container.addEventListener('touchmove', event => this.handleTouch(event));\n container.addEventListener('touchend', event => this.handleTouch(event));\n container.addEventListener('touchcancel', event => this.handleTouch(event));\n container.addEventListener('click', event => this.handleTouch(event));\n }\n }\n\n protected addLayers(layers: MapTileLayer[]) {\n const flattenLayers: MapTileLayer[] = flatten(layers);\n const baseLayers: L.Control.LayersObject = {};\n const overlays: L.Control.LayersObject = {};\n let firstLayer = true;\n for (const layer of sortByPriority(flattenLayers)) {\n const objectToAddTo = layer.isOverlay ? overlays : baseLayers;\n if (objectToAddTo[layer.label]) {\n continue;\n }\n\n const tiles = this.leaflet.tileLayer(layer.layerUrl, layer.options);\n if (!layer.isOverlay && firstLayer) {\n firstLayer = false;\n tiles.addTo(this.map);\n }\n objectToAddTo[layer.label] = tiles;\n }\n if (flattenLayers.length > 1) {\n this.leaflet.control.layers(baseLayers, overlays, { position: 'bottomleft' }).addTo(this.map);\n }\n }\n\n protected changeConfig(change: SimpleChange) {\n if (this.hasChanged(change, 'zoomLevel')) {\n this.map.setZoom(this.config.zoomLevel);\n }\n\n if (this.hasChanged(change, 'center') && every(change.currentValue.center, p => !isNull(p))) {\n this.map.setView(change.currentValue.center);\n }\n\n if (this.hasChanged(change, 'icon') || this.hasChanged(change, 'color')) {\n this.refreshMarkers();\n }\n\n if (this.hasChanged(change, 'realtime') && change.currentValue.realtime) {\n this.startRealtime();\n }\n\n if (change.currentValue.realtime === false) {\n this.stopRealtime();\n }\n\n if (this.hasChanged(change, 'follow')) {\n this.moveToPositionOfMo(this.assets);\n }\n\n if (this.hasChanged(change, 'disablePan') || this.hasChanged(change, 'disableZoom')) {\n this.toggleControls();\n }\n }\n\n protected hasChanged(change: SimpleChange, prop: keyof MapConfig) {\n return change.currentValue[prop] !== change.previousValue[prop];\n }\n\n protected toggleControls() {\n if (this.config.disableZoom) {\n this.map.removeControl(this.map.zoomControl);\n this.map.scrollWheelZoom.disable();\n } else {\n this.map.addControl(this.map.zoomControl);\n this.map.scrollWheelZoom.enable();\n }\n if (this.config.disablePan) {\n this.map.dragging.disable();\n } else {\n this.map.dragging.enable();\n }\n }\n\n private handleTouch(e: Event) {\n // Disregard touch events on the minimap if present\n const ignoreList = [\n 'leaflet-control-minimap',\n 'leaflet-interactive',\n 'leaflet-popup-content',\n 'leaflet-popup-content-wrapper',\n 'leaflet-popup-close-button',\n 'leaflet-control-zoom-in',\n 'leaflet-control-zoom-out'\n ];\n\n let ignoreElement = false;\n for (let i = 0; i < ignoreList.length; i++) {\n if (this.leaflet.DomUtil.hasClass(e.target as HTMLElement, ignoreList[i])) {\n ignoreElement = true;\n }\n }\n\n const container = this.map.getContainer();\n if (ignoreElement) {\n if (\n this.leaflet.DomUtil.hasClass(e.target as HTMLElement, 'leaflet-interactive') &&\n e.type === 'touchmove' &&\n (e as TouchEvent).touches.length === 1\n ) {\n this.leaflet.DomUtil.addClass(container, 'touch-warning');\n this.map.dragging.disable();\n } else {\n this.leaflet.DomUtil.removeClass(container, 'touch-warning');\n }\n return;\n }\n\n if (e.type !== 'touchmove' && e.type !== 'touchstart') {\n this.leaflet.DomUtil.removeClass(container, 'touch-warning');\n return;\n }\n if ((e as TouchEvent).touches.length === 1) {\n this.leaflet.DomUtil.addClass(container, 'touch-warning');\n this.map.dragging.disable();\n } else {\n this.map.dragging.enable();\n this.leaflet.DomUtil.removeClass(container, 'touch-warning');\n }\n }\n\n private zoomToBound(assets: PositionManagedObject[]) {\n if (isEmpty(assets)) return;\n\n const bounds: [number, number][] = assets.map(asset => [\n asset.c8y_Position.lat,\n asset.c8y_Position.lng\n ]);\n if (assets.length > 1) {\n this.map.flyToBounds(bounds, { animate: false });\n return;\n }\n this.map.flyTo(\n [assets[0].c8y_Position.lat, assets[0].c8y_Position.lng],\n this.map.options.zoom,\n {\n animate: false\n }\n );\n }\n\n private fitBounds(bounds: L.LatLngBounds) {\n if (bounds?.isValid()) {\n this.map.fitBounds(bounds, this.config.fitBoundsOptions);\n }\n }\n\n private bindPopup(marker: C8yMarker, context: PositionManagedObject | IEvent) {\n if (this.popup) {\n marker.on('click', () => {\n this.popup.viewContainer.clear();\n const view = this.popup.viewContainer.createEmbeddedView(this.popup.template, {\n $implicit: context\n });\n view.detectChanges();\n marker\n .unbindPopup()\n .bindPopup(this.popup.elementRef.nativeElement.previousSibling, {\n offset: [-3, -40],\n maxWidth: 140,\n autoPan: true,\n closeButton: false\n })\n .openPopup();\n });\n }\n }\n}\n","<div class=\"c8y-map\">\n <div #map></div>\n</div>\n<ng-content></ng-content>\n","import {\n Component,\n EventEmitter,\n Inject,\n Input,\n IterableDiffers,\n Output,\n SimpleChange,\n SimpleChanges\n} from '@angular/core';\nimport { IManagedObject } from '@c8y/client';\nimport type { MapDefaultConfig, MapTileLayer } from '@c8y/options';\nimport {\n ColorService,\n CountdownIntervalComponent,\n DatePipe,\n GeoService,\n ManagedObjectRealtimeService,\n WidgetGlobalAutoRefreshService,\n globalAutoRefreshLoading,\n DynamicComponent,\n DynamicComponentAlertAggregator\n} from '@c8y/ngx-components';\nimport { TranslateService } from '@ngx-translate/core';\nimport type * as L from 'leaflet';\nimport {\n BehaviorSubject,\n EMPTY,\n Observable,\n combineLatest,\n from,\n fromEvent,\n merge,\n of\n} from 'rxjs';\nimport {\n catchError,\n debounceTime,\n mergeMap,\n skip,\n switchMap,\n takeUntil,\n tap\n} from 'rxjs/operators';\nimport { ClusterMap } from './cluster-map';\nimport { MapComponent } from './map.component';\nimport {\n ClusterMapConfig,\n ClusterSize,\n MAP_DEFAULT_CONFIG,\n MAP_TILE_LAYER,\n PositionManagedObject\n} from './map.model';\nimport { MapService } from './map.service';\n\n@Component({\n selector: 'c8y-cluster-map',\n templateUrl: './cluster-map.component.html',\n providers: [ManagedObjectRealtimeService]\n})\nexport class ClusterMapComponent extends MapComponent implements DynamicComponent {\n isLoading$ = new BehaviorSubject(false);\n countdownIntervalComp: CountdownIntervalComponent;\n\n @Input()\n config: ClusterMapConfig;\n\n @Input()\n rootNode: IManagedObject;\n\n @Input('asset')\n assets: PositionManagedObject;\n\n @Input()\n showClusterColor = false;\n\n @Output()\n mapChange = new EventEmitter<L.LeafletEvent>();\n\n alerts: DynamicComponentAlertAggregator;\n\n errorNotifier = new BehaviorSubject(null);\n private reloadTrigger$ = new BehaviorSubject(false);\n private clusters: ClusterMap[] = [];\n private readonly EVENT_THROTTLE_TIME = 750;\n\n constructor(\n protected moRealtimeService: ManagedObjectRealtimeService,\n protected mapService: MapService,\n @Inject(MAP_TILE_LAYER) protected layers$: Observable<MapTileLayer[]>,\n @Inject(MAP_DEFAULT_CONFIG)\n protected defaultConfig$: Observable<MapDefaultConfig>,\n protected translateService: TranslateService,\n protected widgetGlobalAutoRefreshService: WidgetGlobalAutoRefreshService,\n private iterable: IterableDiffers,\n private colorService: ColorService,\n geo: GeoService,\n datePipe: DatePipe\n ) {\n super(\n moRealtimeService,\n mapService,\n layers$,\n defaultConfig$,\n translateService,\n geo,\n datePipe,\n widgetGlobalAutoRefreshService\n );\n }\n\n async ngOnChanges(changes: SimpleChanges) {\n if (changes.config?.firstChange) {\n return;\n }\n\n if (changes.rootNode?.previousValue !== changes.rootNode?.currentValue) {\n this.changeRootNode(changes.rootNode.currentValue);\n }\n\n if (changes.config?.currentValue) {\n this.changeConfig(changes.config);\n }\n }\n\n changeConfig(change: SimpleChange) {\n // on following, cancel reload to avoid stale state\n if (change.currentValue.follow === true) {\n this.cancelReload();\n this.isLoading$.next(false);\n }\n\n if (change.currentValue.refreshInterval !== change.previousValue.refreshInterval) {\n this.reload();\n }\n super.changeConfig(change);\n }\n\n async ngAfterViewInit() {\n if (!this.leaflet) {\n this.leaflet = await this.mapService.getLeaflet();\n }\n\n if (this.config.widgetInstanceGlobalAutoRefreshContext) {\n this.handleGlobalRefreshLoading();\n }\n combineLatest([this.layers$, this.defaultConfig$])\n .pipe(takeUntil(this.unsubscribeTrigger$))\n .subscribe(([layers, defaultConfig]) => {\n this.initMap(layers, defaultConfig);\n this.changeRootNode(this.rootNode);\n this.changeConfig(new SimpleChange({}, this.config, false));\n });\n }\n\n async reset() {\n this.ngOnDestroy();\n await this.ngAfterViewInit();\n }\n\n reload() {\n this.reloadTrigger$.next(true);\n }\n\n cancelReload() {\n this.reloadTrigger$.next(false);\n }\n\n listenToClusterAndIntervalChanges() {\n const countdownEnded$ = this.config.widgetInstanceGlobalAutoRefreshContext\n ? this.widgetGlobalAutoRefreshService.countdownActions.countdownEnded$.pipe(\n takeUntil(this.destroy$)\n )\n : this.countdownIntervalComp\n ? this.countdownIntervalComp.countdownEnded.pipe(takeUntil(this.unsubscribeTrigger$))\n : EMPTY;\n\n const mapChange$ = this.getMapChangeObservable();\n merge(this.reloadTrigger$, mapChange$, countdownEnded$)\n .pipe(\n tap(value => {\n if (this.config.widgetInstanceGlobalAutoRefreshContext) {\n !this.isLeafletEventInterface(value) && this.isLoading$.next(true);\n return;\n }\n this.isLoading$.next(true);\n this.countdownIntervalComp?.stop(true);\n }),\n switchMap(value =>\n value === false\n ? of([])\n : from(this.mapService.getClusterSize(this.map.getBounds())).pipe(\n mergeMap((clusterSize: ClusterSize) =>\n this.getClusterRects(clusterSize, this.map.getBounds())\n ),\n mergeMap(rects => this.createOrUpdateCluster(rects)),\n catchError(error => {\n this.errorNotifier.next(error);\n\n return of([]);\n })\n )\n ),\n takeUntil(this.unsubscribeTrigger$)\n )\n .subscribe((clusters: ClusterMap[]) => {\n clusters.forEach(cluster => cluster.render(this.map));\n this.isLoading$.next(false);\n if (this.config.widgetInstanceGlobalAutoRefreshContext) {\n return;\n }\n this.countdownIntervalComp?.start();\n });\n }\n\n listenToClusterMapChanges() {\n this.getMapChangeObservable().subscribe();\n }\n\n refreshMarkers() {\n if (this.assets) {\n super.refreshMarkers();\n return;\n }\n this.clusters.forEach(cluster => {\n cluster.clear(this.map);\n });\n this.reload();\n }\n\n private changeRootNode(mo: IManagedObject) {\n this.unsubscribeAllListeners();\n this.clearMarkers();\n this.clearClusters();\n\n const isPositionDevice = mo?.c8y_Position && mo?.c8y_IsDevice;\n if (isPositionDevice) {\n this.assets = mo as PositionManagedObject;\n this.refreshMarkers();\n this.listenToClusterMapChanges();\n } else {\n this.assets = null;\n this.listenToClusterAndIntervalChanges();\n this.reload();\n }\n }\n\n private async getClusterRects(\n levelThreshold: ClusterSize = ClusterSize.FOUR,\n viewBounds: L.LatLngBounds,\n level = 0\n ): Promise<L.Rectangle[]> {\n let rects = [];\n\n if (levelThreshold === ClusterSize.NONE) {\n const rect = await this.getRect(viewBounds);\n rects.push(rect);\n return rects;\n }\n\n if (level >= levelThreshold) {\n return rects;\n }\n level++;\n\n const { lat: x1, lng: y1 } = viewBounds.getSouthWest();\n const { lat: x2, lng: y2 } = viewBounds.getNorthEast();\n const newX2 = (x1 + x2) / 2;\n const newY2 = (y1 + y2) / 2;\n\n const bounds: [[number, number], [number, number]][] = [\n [\n [x1, y1],\n [newX2, newY2]\n ],\n [\n [newX2, newY2],\n [x2, y2]\n ],\n [\n [x1, newY2],\n [newX2, y2]\n ],\n [\n [newX2, y1],\n [x2, newY2]\n ]\n ];\n for (const bound of bounds) {\n const latLngBound = this.leaflet.latLngBounds(bound);\n const rect = await this.getRect(latLngBound);\n rects = [...rects, ...(await this.getClusterRects(levelThreshold, latLngBound, level))];\n\n if (level === levelThreshold) {\n rects.push(rect);\n }\n }\n\n return rects;\n }\n\n private async getRect(latLngBound: L.LatLngBounds) {\n let color = 'none';\n if (this.showClusterColor) {\n color = await this.colorService.generateColor(latLngBound.toBBoxString());\n }\n const rect = this.leaflet.rectangle(latLngBound, {\n color,\n weight: color === 'none' ? 0 : 1,\n interactive: false\n });\n return rect;\n }\n\n private clearClusters() {\n this.clusters.forEach(cluster => {\n cluster.clear(this.map);\n });\n this.clusters = [];\n }\n\n private async updateCluster(cluster: ClusterMap) {\n const clusterCount = await this.mapService.getPositionMOsFromBoundCount(\n cluster.rect.getBounds(),\n this.rootNode\n );\n if (clusterCount > this.mapService.MAX_DEVICE_PER_CLUSTER) {\n cluster.setClusterToBigMarker(this.map, clusterCount, this.leaflet);\n cluster.positions = [];\n return cluster;\n }\n\n cluster.removeClusterToBigMarker();\n cluster.positions = await this.mapService.getPositionMOsFromBound(\n cluster.rect.getBounds(),\n this.rootNode\n );\n return cluster;\n }\n\n private createOrUpdateCluster(rects: L.Rectangle<unknown>[]) {\n const isNew = rects.length !== this.clusters.length;\n if (isNew) {\n this.clearClusters();\n }\n const updatePromise = rects.map((rect, index) => {\n if (isNew) {\n const cluster = new ClusterMap(\n this.iterable,\n asset => this.getAssetMarker(asset),\n this.translateService\n );\n this.clusters.push(cluster);\n }\n this.clusters[index].rect = rect;\n return this.updateCluster(this.clusters[index]);\n });\n\n return Promise.all(updatePromise);\n }\n\n private getMapChangeObservable() {\n return merge(\n fromEvent<L.LeafletEvent>(this.map, 'move'),\n fromEvent<L.LeafletEvent>(this.map, 'moveend')\n ).pipe(\n debounceTime(this.EVENT_THROTTLE_TIME),\n tap(event => this.mapChange.emit(event)),\n takeUntil(this.unsubscribeTrigger$)\n );\n }\n\n private isLeafletEventInterface(\n LeafletEventObject: L.LeafletEvent | boolean | void\n ): LeafletEventObject is L.LeafletEvent {\n return (LeafletEventObject as L.LeafletEvent)?.type !== undefined;\n }\n\n private handleGlobalRefreshLoading(): void {\n this.isLoading$\n .pipe(\n skip(1),\n globalAutoRefreshLoading(this.widgetGlobalAutoRefreshService),\n takeUntil(this.destroy$)\n )\n .subscribe();\n\n this.destroy$.subscribe({\n complete: () =>\n this.isLoading$.value && this.widgetGlobalAutoRefreshService.decrementLoading()\n });\n }\n}\n","<div class=\"c8y-map\">\n <div #map></div>\n</div>\n<ng-content></ng-content>\n","import { Component, EventEmitter, Input, Output, SimpleChanges, ViewChild } from '@angular/core';\nimport { CountdownIntervalComponent } from '@c8y/ngx-components';\nimport { cloneDeep } from 'lodash-es';\nimport { Subject } from 'rxjs';\nimport { takeUntil } from 'rxjs/operators';\nimport { ClusterMapComponent } from './cluster-map.component';\nimport { ClusterMapConfig, MapStatusButtonsConfig } from './map.model';\n\n@Component({\n selector: 'c8y-map-status',\n templateUrl: './map-status.component.html'\n})\nexport class MapStatusComponent {\n @Input()\n config: ClusterMapConfig;\n\n @Output()\n configChange = new EventEmitter<ClusterMapConfig>();\n\n @Output()\n onUnfollow = new EventEmitter<ClusterMapConfig>();\n\n @Input()\n clusterMap: ClusterMapComponent;\n\n @Input() buttonsConfig: Partial<MapStatusButtonsConfig> = {};\n\n centerMapButtonDisabled = true;\n @ViewChild(CountdownIntervalComponent) countdownIn