import * as i0 from '@angular/core';
import { InjectionToken, Injectable, Directive, EventEmitter, Output, Input, ContentChild, ViewChild, Inject, Component, SimpleChange, NgModule } from '@angular/core';
import * as i1$1 from '@c8y/ngx-components';
import { sortByPriority, ManagedObjectRealtimeService, globalAutoRefreshLoading, CountdownIntervalComponent, IconDirective, C8yTranslatePipe, CommonModule as CommonModule$1, FormsModule as FormsModule$1, RealtimeModule, CoreModule } from '@c8y/ngx-components';
import * as i3 from '@ngx-translate/core';
import * as i4 from 'rxjs';
import { of, combineLatest, defer, firstValueFrom, NEVER, BehaviorSubject, Subject, fromEvent, merge, from } from 'rxjs';
import { map, first, takeUntil, scan, filter, switchMap, tap, mergeMap, catchError, take, debounceTime, skip } from 'rxjs/operators';
import * as i1 from '@c8y/client';
import { QueriesUtil } from '@c8y/client';
import { latLng, latLngBounds } from 'leaflet';
import { get, remove, isUndefined, flatten, every, isNull, isEmpty, cloneDeep } from 'lodash-es';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { gettext } from '@c8y/ngx-components/gettext';
import { NgClass, AsyncPipe, CommonModule } from '@angular/common';
import { TooltipDirective, TooltipModule } from 'ngx-bootstrap/tooltip';
import { FormsModule } from '@angular/forms';

/**
 * Utility function to assign asset and event information to a Leaflet marker.
 * @param marker The Leaflet marker instance.
 * @param asset The managed object representing the asset (optional).
 * @param event The event associated with the marker (optional).
 * @returns The marker with asset and/or event information attached.
 */
function getC8yMarker(marker, asset, event) {
    marker.asset = asset;
    marker.event = event;
    return marker;
}
/**
 * Injection token for providing map tile layers as an observable.
 */
const MAP_TILE_LAYER = new InjectionToken('MAP_TILE_LAYER');
/**
 * Enum for supported cluster sizes on the map.
 */
var ClusterSize;
(function (ClusterSize) {
    /** No clustering. */
    ClusterSize[ClusterSize["NONE"] = 0] = "NONE";
    /** Cluster of 4. */
    ClusterSize[ClusterSize["FOUR"] = 1] = "FOUR";
    /** Cluster of 16. */
    ClusterSize[ClusterSize["SIXTEEN"] = 2] = "SIXTEEN";
})(ClusterSize || (ClusterSize = {}));
/**
 * Enum for map tenant option keys used in configuration.
 */
var MapTenantOptionKeys;
(function (MapTenantOptionKeys) {
    /** Map configuration key. */
    MapTenantOptionKeys["CONFIG"] = "map-config";
    /** Map layers key. */
    MapTenantOptionKeys["LAYERS"] = "map-layers";
})(MapTenantOptionKeys || (MapTenantOptionKeys = {}));
/**
 * Injection token for providing the default map configuration as an observable.
 */
const MAP_DEFAULT_CONFIG = new InjectionToken('MAP_DEFAULT_CONFIG');
/**
 * Default map tile layer configuration (OpenStreetMap).
 */
const defaultLayer = {
    layerUrl: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
    label: 'OpenStreetMap',
    priority: 1000,
    options: {
        maxZoom: 18,
        minZoom: 0,
        attribution: '&copy;<a href="http://www.openstreetmap.org/copyright" rel="noreferrer nofollow">OpenStreetMap</a>',
        noWrap: false,
        // Required for OSM tiles usage, possible values are documented here: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#referrerpolicy
        referrerPolicy: 'strict-origin-when-cross-origin'
    }
};
/**
 * Default map configuration (centered on Düsseldorf, zoom level 2).
 */
const defaultMapConfig = {
    center: [51.23544, 6.79599], // Düsseldorf
    zoomLevel: 2
};
/**
 * Default options for fitting map bounds (padding).
 */
const defaultFitBoundsOptions = {
    padding: [50, 50]
};
/**
 * Extension of QueriesUtil to use decimal literals instead of float literals.
 * Used for queries involving longitude and latitude to avoid precision issues.
 */
class QueriesUtilDecimalExtension extends QueriesUtil {
    useAsFloat(operand) {
        operand = this.quoteString(operand);
        if (typeof operand === 'number') {
            return `${operand}d`;
        }
        return operand;
    }
}

class MapService {
    /**
     * Returns asset icon status for highest alarm severity found in device object.
     * @param device Device that contains alarms information.
     * @returns Status string according to alarm severity
     */
    static getStatus(device) {
        if (!device.c8y_ActiveAlarmsStatus) {
            return 'text-muted';
        }
        if (device.c8y_ActiveAlarmsStatus.critical) {
            return 'status critical';
        }
        if (device.c8y_ActiveAlarmsStatus.major) {
            return 'status major';
        }
        if (device.c8y_ActiveAlarmsStatus.minor) {
            return 'status minor';
        }
        if (device.c8y_ActiveAlarmsStatus.warning) {
            return 'status warning';
        }
        return 'text-muted';
    }
    /**
     * Returns the status of a group (highest severity among its devices).
     * @param group The group of devices.
     * @returns The status string representing the highest severity in the group.
     */
    static getGroupStatus(group) {
        let mostCriticalStatus = 'text-muted';
        let highestSeverity = 0;
        for (const device of group.items) {
            const status = this.getStatus(device);
            const severity = this.getStatusSeverity(status);
            if (severity > highestSeverity) {
                highestSeverity = severity;
                mostCriticalStatus = status;
                if (severity === 4) {
                    return status; // Critical is highest, return immediately
                }
            }
        }
        return mostCriticalStatus;
    }
    /**
     * Maps status strings to severity levels for comparison.
     * @param status The status string.
     * @returns Numeric severity level (higher = more severe).
     */
    static getStatusSeverity(status) {
        const severityMap = {
            'status critical': 4,
            'status major': 3,
            'status minor': 2,
            'status warning': 1,
            'text-muted': 0
        };
        return severityMap[status] ?? 0;
    }
    /**
     * @ignore: Only DI.
     */
    constructor(inventory, options, serviceRegistry, featureCacheService) {
        this.inventory = inventory;
        this.options = options;
        this.serviceRegistry = serviceRegistry;
        this.featureCacheService = featureCacheService;
        /**
         * The devices that are maximal displayed in one cluster.
         */
        this.MAX_DEVICE_PER_CLUSTER = 200;
        /**
         * The count until the cluster is sized. There are a maximum of
         * three clusters: 1, 4 or 16.
         */
        this.CLUSTER_LEVEL_THRESHOLD = 500;
        this.queriesUtil = new QueriesUtil();
    }
    /**
     * Returns the leaflet instance used by the cumulocity core.
     */
    async getLeaflet() {
        const originalLeflet = window.L;
        const c8yLeafletInstance = (await import('leaflet')).default;
        c8yLeafletInstance.noConflict();
        window.L = originalLeflet;
        return c8yLeafletInstance;
    }
    /**
     * Verifies if a given managed object is a device with a position fragment.
     * @param mo The given managed object.
     */
    isPositionedDevice(mo) {
        return !!(mo?.c8y_IsDevice && this.hasPosition(mo));
    }
    /**
     * Verifies if a given managed object has a position fragment.
     * @param mo The given managed object.
     */
    hasPosition(mo) {
        return mo?.c8y_Position;
    }
    getMapTileLayerProviders() {
        const layerProviders = this.serviceRegistry.get('mapTileLayerHook');
        return layerProviders;
    }
    getMapTileLayersFromHookedProviders$(layerProviders) {
        if (!layerProviders.length) {
            return of([]);
        }
        const layers = combineLatest(layerProviders.map(provider => provider.getMapTileLayers$()));
        return layers.pipe(map(layers => layers.flat()));
    }
    /**
     * Returns the layers available in this application.
     * Layers are taken from plugins installed to this application.
     * In case none of the plugins override the default layers, the default layers are also considered.
     * @returns The layers.
     */
    getMapTileLayers$() {
        return defer(() => {
            const layerProviders = this.getMapTileLayerProviders();
            const overridesDefaultLayer = layerProviders.some(provider => provider.overridesDefaultLayer?.());
            const layersFromProviders = defer(() => this.getMapTileLayersFromHookedProviders$(layerProviders));
            if (overridesDefaultLayer) {
                return layersFromProviders;
            }
            return combineLatest([this.getDefaultLayers(), layersFromProviders]).pipe(map(layers => {
                return layers.flat();
            }));
        });
    }
    /**
     * Returns the layers configured in the current platform via tenant options.
     * @returns The layers. If not set in tenant options the default layers.
     */
    getDefaultLayers() {
        return this.getMapOption(MapTenantOptionKeys.LAYERS, this.options.mapLayers || [defaultLayer]);
    }
    /**
     * Returns the map configuration configured on the tenant.
     * @returns The configuration. If not set in tenant options the default configuration.
     */
    getDefaultConfig() {
        return this.getMapOption(MapTenantOptionKeys.CONFIG, this.options.mapConfig || defaultMapConfig);
    }
    /**
     * Counts all managed objects in a given bound with a c8y_Position fragment.
     * @param bound The lat lng bound to request the managed objects for.
     * @param isInHierarchyOfMO The group managed object of which direct children should be searched for.
     * @returns The number of all position managed objects in the given bound (and group).
     */
    async getPositionMOsFromBoundCount(bound, isInHierarchyOfMO) {
        return this.getPositionMOsFromBound(bound, isInHierarchyOfMO, true);
    }
    async getPositionMOsFromBound(bound, isInHierarchyOfMO, count = false) {
        const { lat: latMin, lng: lngMinRaw } = bound.getSouthWest();
        const { lat: latMax, lng: lngMaxRaw } = bound.getNorthEast();
        const lngMin = lngMaxRaw - lngMinRaw > 360 ? -180 : this.normalizeLongitude(lngMinRaw);
        const lngMax = lngMaxRaw - lngMinRaw > 360 ? 180 : this.normalizeLongitude(lngMaxRaw);
        const filterObjWithCoordinates = {
            __and: [
                ...(isInHierarchyOfMO
                    ? [
                        {
                            __or: [
                                { id: isInHierarchyOfMO.id },
                                await this.childrenOfGroupFilter(isInHierarchyOfMO)
                            ]
                        }
                    ]
                    : []),
                {
                    __has: 'c8y_Position'
                },
                {
                    ['c8y_Position.lat']: {
                        __gt: latMin
                    }
                },
                {
                    ['c8y_Position.lat']: {
                        __lt: latMax
                    }
                },
                {
                    [lngMin < lngMax ? '__and' : '__or']: [
                        {
                            ['c8y_Position.lng']: {
                                __gt: lngMin
                            }
                        },
                        {
                            ['c8y_Position.lng']: {
                                __lt: lngMax
                            }
                        }
                    ]
                }
            ]
        };
        const query = this.queriesUtil.buildQuery(filterObjWithCoordinates);
        const { paging, data } = await this.inventory.list({
            pageSize: count ? 1 : this.MAX_DEVICE_PER_CLUSTER,
            withTotalPages: count,
            query
        });
        if (count) {
            return paging.totalPages;
        }
        return data.map((pmo) => bound.contains(latLng(pmo.c8y_Position.lat, pmo.c8y_Position.lng))
            ? pmo
            : this.denormalizePMO(pmo, bound));
    }
    /**
     * Returns all devices with c8y_Position.
     * @param pageSize The page size to return.
     * @param count Switches to counting only.
     * @returns All devices or the device count with a c8y_Position fragment.
     */
    async getPositionDevices(pageSize = this.MAX_DEVICE_PER_CLUSTER, count) {
        const query = this.queriesUtil.buildQuery({
            __and: [{ __has: 'c8y_Position' }, { __has: 'c8y_IsDevice' }]
        });
        const { paging, data } = await this.inventory.list({
            pageSize: count ? 1 : pageSize,
            withTotalPages: !!count,
            query
        });
        if (count) {
            return paging.totalPages;
        }
        return data;
    }
    /**
     * Returns all managed object with a c8y_Position fragment.
     * @param isInHierarchyOfMO The group managed object of which direct children should be searched for.
     * @param pageSize Defines how many results should be returned.
     * @returns The managed objects with position.
     */
    async getAllPositionMOs(isInHierarchyOfMO, pageSize = 500) {
        const childrenFilter = isInHierarchyOfMO
            ? await this.childrenOfGroupFilter(isInHierarchyOfMO)
            : undefined;
        const orValue = childrenFilter ? [{ id: isInHierarchyOfMO.id }, childrenFilter] : undefined;
        const queryObj = {
            __filter: {
                __and: [{ __has: 'c8y_Position' }],
                ...(orValue ? { __or: orValue } : {})
            }
        };
        const query = this.queriesUtil.buildQuery(queryObj);
        const filter = {
            pageSize,
            withTotalPages: true,
            query
        };
        const { paging, data, res } = await this.inventory.list(filter);
        return {
            res,
            paging: paging,
            data: data
        };
    }
    /**
     * Gets the bounds of all assets, optionally within a group.
     * @param isInHierarchyOfMO Is inhierachy of managed object to limit the assets to a group.
     * @returns The bounds of all assets.
     */
    async getAllBounds(isInHierarchyOfMO) {
        return this.getBounds('query', isInHierarchyOfMO);
    }
    /**
     * Determines a rectangular geographical area based on the positions of all devices.
     *
     * @returns A [[LatLngBounds]] object fitting all devices' geo positions.
     */
    async getAllDevicesBounds() {
        return this.getBounds('q');
    }
    /**
     * Calculates the bounding box that contains all given assets.
     * @param assets The assets to calculate the bounds for.
     * @returns The calculated bounds or undefined if no valid positions are found.
     */
    async getAssetsBounds(assets) {
        const leaflet = await this.getLeaflet();
        const bounds = leaflet.latLngBounds([]);
        let hasValidPositions = false;
        assets.forEach(asset => {
            const position = asset.c8y_Position;
            if (position && typeof position.lat === 'number' && typeof position.lng === 'number') {
                bounds.extend([position.lat, position.lng]);
                hasValidPositions = true;
            }
        });
        if (!hasValidPositions || !bounds.isValid()) {
            return;
        }
        return bounds;
    }
    /**
     * Returns the cluster size for clustered maps. Counting the position MOs in a bounding
     * and if it reach a threshold, returning a [[ClusterSize]].
     * @param bound The bounding to check for cluster size.
     * @returns The cluster size, can be NONE, FOUR or SIXTEEN.
     */
    async getClusterSize(bound) {
        const count = await this.getPositionMOsFromBoundCount(bound);
        let clusterSize = ClusterSize.NONE;
        if (count > this.CLUSTER_LEVEL_THRESHOLD) {
            clusterSize = ClusterSize.SIXTEEN;
        }
        else if (count > this.MAX_DEVICE_PER_CLUSTER) {
            clusterSize = ClusterSize.FOUR;
        }
        return clusterSize;
    }
    getMapOption(key, defaultValue) {
        return defer(() => this.options.getTenantOption('configuration', key, defaultValue)).pipe(map(config => {
            if (typeof config === 'string') {
                console.error(`The tenant option for maps 'configuration.${key}' is not a valid JSON structure.`);
                return defaultValue;
            }
            return config;
        }));
    }
    /**
     * Shifts longitudes received from Leaflet.js in the [-180 - k*360; 180 + k*360] rangewhen
     * `noWrap` is enabled to the [-180; 180] range expected for values of the c8y_Position fragment.
     *
     * @param lng Longitude to shift.
     * @returns  Longitude value in the [-180; 180] range
     */
    normalizeLongitude(lng) {
        return ((((lng + 180) % 360) + 360) % 360) - 180;
    }
    /**
     * Shifts longitudes in the [-180; 180] range expected for values of the c8y_Position fragment
     * the the [-180 - k*360; 180 + k*360] range expected from Leaflet.js when `noWrap` is enabled.
     *
     * The method naively adds/subtracts 360 degrees to the original value until the position fits in the expected bounds.
     *
     * @param pmo A managed object with a `c8y_Position` fragment
     * @param bounds The bounds where the position should fit
     * @returns A managed object whose `c8y_Position`'s `lng` values has been shifted to fit in bounds
     */
    denormalizePMO(pmo, bounds) {
        let { lng } = pmo.c8y_Position;
        const shiftFactor = lng > bounds.getEast() ? -1 : 1;
        while (!bounds.contains(latLng(pmo.c8y_Position.lat, lng))) {
            lng += shiftFactor * 360;
        }
        pmo.c8y_Position.lng = lng;
        return pmo;
    }
    async childrenOfGroupFilter(mo) {
        try {
            const useInHierarchyOf = await firstValueFrom(this.featureCacheService.getFeatureState('ui.map.isinhierarchyof'));
            if (useInHierarchyOf) {
                return { __isinhierarchyof: mo.id };
            }
        }
        catch (e) {
            console.warn('Error while determining isinhierarchyof feature toggle state:', e);
        }
        return { __bygroupid: mo.id };
    }
    /**
     * Internal method to calculate bounds based on position coordinates.
     * @param queryName The query parameter name to use ('q' or 'query'). Use q to limit to devices only.
     * @param isInHierarchyOfMO Optional managed object to limit the assets to a group.
     * @returns The calculated bounds
     */
    async getBounds(queryName, isInHierarchyOfMO) {
        const childrenFilter = isInHierarchyOfMO
            ? await this.childrenOfGroupFilter(isInHierarchyOfMO)
            : undefined;
        const filter = (coord, order) => ({
            pageSize: 1,
            [queryName]: new QueriesUtil().buildQuery({
                __filter: {
                    __and: [
                        childrenFilter,
                        {
                            __has: 'c8y_Position'
                        }
                    ].filter(Boolean)
                },
                __orderby: [
                    {
                        [`c8y_Position.${coord}`]: order === 'asc' ? 1 : -1
                    }
                ]
            })
        });
        const filterReverse = (op, order) => ({
            pageSize: 1,
            [queryName]: new QueriesUtilDecimalExtension().buildQuery({
                __filter: {
                    __and: [
                        childrenFilter,
                        {
                            __has: 'c8y_Position',
                            [`c8y_Position.lng`]: {
                                [`__${op}`]: 0
                            }
                        }
                    ].filter(Boolean)
                },
                __orderby: [
                    {
                        [`c8y_Position.lng`]: order === 'asc' ? 1 : -1
                    }
                ]
            })
        });
        const [latMin, latMax, lngMin, lngMax, lngRevMin, lngRevMax] = await Promise.all([
            this.inventory.list(filter('lat', 'asc')),
            this.inventory.list(filter('lat', 'desc')),
            this.inventory.list(filter('lng', 'asc')),
            this.inventory.list(filter('lng', 'desc')),
            this.inventory.list(filterReverse('gt', 'asc')),
            this.inventory.list(filterReverse('lt', 'desc'))
        ]).then(result => result.map(r => get(r.data, '[0].c8y_Position')));
        const shiftWorld = (lngRevMin?.lng ?? 0) - (lngRevMax?.lng ?? 0) > 180;
        return latLngBounds(latLng(latMin?.lat, shiftWorld ? lngRevMin?.lng : lngMin?.lng), latLng(latMax?.lat, shiftWorld ? lngRevMax?.lng + 360 : lngMax?.lng));
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: MapService, deps: [{ token: i1.InventoryService }, { token: i1$1.OptionsService }, { token: i1$1.ServiceRegistry }, { token: i1$1.FeatureCacheService }], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: MapService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: MapService, decorators: [{
            type: Injectable,
            args: [{
                    providedIn: 'root'
                }]
        }], ctorParameters: () => [{ type: i1.InventoryService }, { type: i1$1.OptionsService }, { type: i1$1.ServiceRegistry }, { type: i1$1.FeatureCacheService }] });

class ClusterMap {
    set clusterMarker(item) {
        this.removeClusterToBigMarker();
        this._clusterMarker = item;
    }
    get clusterMarker() {
        return this._clusterMarker;
    }
    set rect(item) {
        if (this._rect) {
            this._rect.remove();
        }
        this._rect = item;
    }
    get rect() {
        return this._rect;
    }
    constructor(iterable, addAssetCallback, translateService) {
        this.iterable = iterable;
        this.addAssetCallback = addAssetCallback;
        this.translateService = translateService;
        this.markers = [];
        this.positions = [];
        this.iterableDiffer = this.iterable.find(this.positions).create(this.trackBy);
    }
    render(map) {
        if (this._rect) {
            this._rect.addTo(map);
        }
        this.updateChanges(map);
        if (this._clusterMarker) {
            this._clusterMarker.addTo(map);
        }
    }
    clear(map) {
        this.removeClusterToBigMarker();
        this._rect.remove();
        this.positions = [];
        this.updateChanges(map);
    }
    removeClusterToBigMarker() {
        if (this._clusterMarker) {
            this._clusterMarker.remove();
            this._clusterMarker = null;
        }
    }
    addMarkerToMap(device, map) {
        const marker = this.addAssetCallback(device);
        this.markers.push(marker);
        marker.addTo(map);
    }
    setClusterToBigMarker(map, count, leaflet) {
        const bound = this.rect.getBounds();
        const divMarker = leaflet.divIcon({
            html: `<div><div class="popover top show"><span class="popover-content p-l-0 p-t-0 p-b-0 p-r-4 d-flex a-i-center"><i class="dlt-c8y-icon-marker"></i><span class="text-12 text-nowrap">${count}</span></span><span class="arrow" style="translate: -50% 0"></span></div></div>`
        });
        const labelIcon = leaflet.marker(bound.getCenter(), {
            icon: divMarker
        });
        labelIcon.addTo(map);
        labelIcon.on('click', () => {
            map.fitBounds(bound);
        });
        this.clusterMarker = labelIcon;
    }
    updateChanges(map) {
        const changes = this.iterableDiffer.diff(this.positions);
        if (changes) {
            changes.forEachRemovedItem((record) => {
                this.removeMarkerFromMap(record.item);
            });
            changes.forEachAddedItem((record) => {
                this.addMarkerToMap(record.item, map);
            });
        }
    }
    trackBy(index, item) {
        const trackItems = item.items.map(i => [
            i.id,
            i.c8y_Position.lat,
            i.c8y_Position.lng,
            MapService.getStatus(i)
        ]);
        return trackItems.join('');
    }
    removeMarkerFromMap(group) {
        group.items.forEach(device => {
            const markers = this.markers.filter((marker) => marker.asset?.id === device.id);
            markers.forEach(marker => marker.remove());
        });
    }
}

class MapPopupDirective {
    constructor(template, elementRef, viewContainer) {
        this.template = template;
        this.elementRef = elementRef;
        this.viewContainer = viewContainer;
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: MapPopupDirective, deps: [{ token: i0.TemplateRef }, { token: i0.ElementRef }, { token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Directive }); }
    static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.25", type: MapPopupDirective, isStandalone: true, selector: "[c8yMapPopup]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: MapPopupDirective, decorators: [{
            type: Directive,
            args: [{ selector: '[c8yMapPopup]' }]
        }], ctorParameters: () => [{ type: i0.TemplateRef }, { type: i0.ElementRef }, { type: i0.ViewContainerRef }] });

class MapComponent {
    constructor(moRealtimeService, mapService, layers$, defaultConfig$, translateService, geo, datePipe, widgetGlobalAutoRefreshService) {
        this.moRealtimeService = moRealtimeService;
        this.mapService = mapService;
        this.layers$ = layers$;
        this.defaultConfig$ = defaultConfig$;
        this.translateService = translateService;
        this.geo = geo;
        this.datePipe = datePipe;
        this.widgetGlobalAutoRefreshService = widgetGlobalAutoRefreshService;
        /**
         * The markers currently placed on the map.
         */
        this.markers = [];
        /**
         * Indicates if the map was already initialized.
         */
        this.isInit = false;
        /**
         * Map configuration object (center, zoom, icon, color, etc).
         */
        this.config = {};
        /**
         * Observable for polyline coordinates to display on the map.
         */
        this.polyline$ = NEVER;
        /**
         * Emits when a tracked asset is updated in real-time.
         */
        this.onRealtimeUpdate = new EventEmitter();
        /**
         * Emits the Leaflet map instance when available.
         */
        this.onMap = new BehaviorSubject(null);
        /**
         * Emits when the map and Leaflet library are initialized.
         */
        this.onInit = new EventEmitter();
        this.unsubscribeTrigger$ = new Subject();
        this.destroy$ = new Subject();
        this.markerTitle = gettext('Marker at position {{lat}}, {{lng}}');
        this.markerGroupTitle = gettext('Group of {{count}} assets');
        this.initOutputs();
    }
    /**
     * Starts real-time updates for a single asset on the map.
     * Updates marker position and icon as new data arrives.
     */
    startRealtime() {
        if (this.realtimeSubscription) {
            return;
        }
        if (this.assets === null || (Array.isArray(this.assets) && this.assets.length > 1)) {
            this.config.realtime = false;
            this.stopRealtime();
            return;
        }
        const asset = Array.isArray(this.assets) ? this.assets[0] : this.assets;
        this.realtimeSubscription = this.moRealtimeService
            .onUpdate$(asset)
            .subscribe((asset) => {
            const marker = this.findMarker(asset.id);
            if (!marker) {
                return;
            }
            const icon = this.getAssetIcon(asset);
            const newLatLng = this.geo.getLatLong(asset);
            marker.setIcon(icon);
            marker.setLatLng(newLatLng);
            if (Array.isArray(this.assets)) {
                this.assets[0] = asset;
            }
            else {
                this.assets = asset;
            }
            this.moveToPositionOfMo(asset);
            this.onRealtimeUpdate.emit(asset);
        });
    }
    /**
     * Moves the map view to the position of the given asset(s) if follow is enabled.
     * @param positions The asset or array of assets to center the map on.
     */
    moveToPositionOfMo(positions) {
        if (!positions || !this.map) {
            return;
        }
        const position = Array.isArray(positions) ? positions[0] : positions;
        if (!position?.c8y_Position) {
            return;
        }
        if (this.config.follow) {
            this.map.setView([position.c8y_Position.lat, position.c8y_Position.lng]);
        }
    }
    /**
     * Stops real-time updates for the asset.
     */
    stopRealtime() {
        if (this.realtimeSubscription) {
            this.realtimeSubscription.unsubscribe();
            this.realtimeSubscription = null;
        }
    }
    /**
     * Finds a marker on the map by asset, event, or ID. (only if it is a C8yMarker)
     * @param moOrId Asset, event, or string ID to search for.
     * @returns The found marker or undefined.
     */
    findMarker(moOrId) {
        const getId = moOrId => (typeof moOrId === 'string' ? moOrId : moOrId?.id);
        return this.markers.find((marker) => marker.asset?.id === getId(moOrId) || marker.event?.id === getId(moOrId));
    }
    /**
     * Adds a marker to the map and internal marker list.
     * @param marker The marker to add.
     */
    addMarkerToMap(marker) {
        this.markers.push(marker);
        marker.addTo(this.map);
    }
    /**
     * Creates and returns a marker for the given asset, including icon and popup.
     * @param asset The asset to create a marker for.
     * @returns The created marker.
     */
    getAssetMarker(asset) {
        if (!asset || asset.items.length === 0) {
            return;
        }
        if (asset.items.length === 1) {
            return this.getAssetMarkerSingle(asset.items[0]);
        }
        else {
            return this.getGroupedMarker(asset);
        }
    }
    /**
     * Creates and returns a marker for a single asset, including icon and popup.
     * @param asset The asset to create a marker for.
     * @returns The created marker.
     */
    getAssetMarkerSingle(asset) {
        const icon = this.getAssetIcon(asset);
        const { lat, lng } = asset.c8y_Position;
        const leafletMarker = this.leaflet.marker(this.geo.getLatLong(asset), {
            icon,
            title: this.translateService.instant(this.markerTitle, { lat, lng })
        });
        const marker = getC8yMarker(leafletMarker, asset);
        this.bindPopup(marker, asset);
        return marker;
    }
    /**
     * Returns a marker with popup for a group of assets (clustered).
     * @param group The asset group.
     * @returns A marker for a group of assets (clustered).
     */
    getGroupedMarker(group) {
        const latLngs = group.items.map(item => this.leaflet.latLng(item.c8y_Position.lat, item.c8y_Position.lng));
        const bounds = this.leaflet.latLngBounds(latLngs);
        const center = bounds.getCenter();
        const icon = this.getGroupAssetIcon(group);
        const leafletMarker = this.leaflet.marker(center, {
            icon,
            title: this.translateService.instant(this.markerGroupTitle, { count: group.items.length })
        });
        const asset = group.items[0];
        const marker = getC8yMarker(leafletMarker, asset);
        marker.on('click', () => {
            this.map.fitBounds(bounds, { padding: [50, 50] });
            // Check if all markers are at the same position (within a small threshold)
            const areAllSamePosition = latLngs.every(ll => Math.abs(ll.lat - center.lat) < 0.0001 && Math.abs(ll.lng - center.lng) < 0.0001);
            // If all markers are at the same position and zoom is low, create spider layout
            if (areAllSamePosition && this.map && this.map.getZoom() >= 10) {
                this.createSpiderMarkers(group, center, marker);
            }
        });
        return marker;
    }
    /**
     * Creates spider markers for a group of assets at the same position.
     * @param group The group of assets to create spider markers for.
     * @param center The center position of the group.
     * @param originalMarker The marker which was replaced by spider markers.
     */
    createSpiderMarkers(group, center, originalMarker) {
        const markerGroup = this.leaflet.featureGroup();
        const items = group.items;
        const count = items.length;
        const radius = 0.0002; // Adjust based on your needs
        const angleStep = (2 * Math.PI) / count;
        originalMarker.setOpacity(0); // Hide the original marker
        items.forEach((item, index) => {
            const angle = angleStep * index;
            const offsetLat = center.lat + radius * Math.cos(angle);
            const offsetLng = center.lng + radius * Math.sin(angle);
            const markerPos = this.leaflet.latLng(offsetLat, offsetLng);
            // Create spider leg (line from center to marker)
            const polyline = this.leaflet.polyline([center, markerPos], {
                color: '#666',
                weight: 2,
                opacity: 1
            });
            polyline.addTo(markerGroup);
            // Create individual marker
            const icon = this.getAssetIcon(item, [6, -12]);
            const leafletMarker = this.leaflet.marker(markerPos, {
                icon,
                title: this.translateService.instant(this.markerTitle, {
                    lat: item.c8y_Position.lat,
                    lng: item.c8y_Position.lng
                })
            });
            const marker = getC8yMarker(leafletMarker, item);
            this.bindPopup(marker, item, new this.leaflet.Point(-2, -20));
            marker.addTo(markerGroup);
        });
        // Create red center dot
        const centerIcon = this.leaflet.divIcon({
            html: '<div style="background-color: red; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white;"></div>',
            className: 'spider-center-marker',
            iconSize: [12, 12],
            iconAnchor: [6, 6]
        });
        const centerMarker = this.leaflet.marker(center, {
            icon: centerIcon,
            title: this.translateService.instant(gettext('Click to collapse'))
        });
        const restoreOriginalMarker = () => {
            this.map.off('zoomstart', restoreOriginalMarker);
            this.map.off('movestart', restoreOriginalMarker);
            markerGroup.remove();
            originalMarker.setOpacity(1);
        };
        // Add click handler to collapse spider and remove it
        // if user navigates away
        centerMarker.on('click', restoreOriginalMarker);
        this.map.on('zoomstart', restoreOriginalMarker);
        this.map.on('movestart', restoreOriginalMarker);
        centerMarker.addTo(markerGroup);
        markerGroup.addTo(this.map);
    }
    /**
     * The icon for a group of assets (clustered).
     * It shows the count and is colored based on the highest severity status in the group.
     * @param group The group of assets.
     * @returns The marker icon for the group.
     */
    getGroupAssetIcon(group) {
        const status = MapService.getGroupStatus(group);
        const color = this.config.color ? `style='color: ${this.config.color};'` : '';
        const moreThan99 = group.items.length > 99 ? 'more-than-99' : '';
        const icon = this.leaflet.divIcon({
            html: `<div><div class="popover top show"><span class="popover-content p-l-0 p-t-0 p-b-0 p-r-4 d-flex a-i-center"><i class="dlt-c8y-icon-marker  ${status}" ${color}></i><span class="text-12 text-nowrap ${moreThan99}">${group.items.length > 99 ? '99+' : group.items.length}</span></span><span class="arrow" style="translate: -50% 0"></span></div></div>`,
            className: 'c8y-map-marker-icon',
            iconAnchor: [21, 40]
        });
        return icon;
    }
    /**
     * Creates and returns a marker for a tracking event, including icon and popup.
     * @param event The event to create a marker for.
     * @returns The created marker.
     */
    getTrackingMarker(event) {
        if (!event) {
            return;
        }
        const icon = this.getTrackingIcon();
        const { lat, lng } = event.c8y_Position;
        const leafletMarker = this.leaflet.marker(this.geo.getLatLong(event), {
            icon,
            title: this.translateService.instant(this.markerTitle, { lat, lng })
        });
        const marker = getC8yMarker(leafletMarker, null, event);
        this.bindPopup(marker, event);
        return marker;
    }
    /**
     * Returns a Leaflet icon for the given asset, using config or asset icon and color.
     * @param asset The asset to get the icon for.
     * @param anchor The icon anchor point.
     * @returns The Leaflet icon.
     */
    getAssetIcon(asset, anchor = [8, 8]) {
        const assetTypeIcon = this.config.icon || asset.icon?.name;
        const status = MapService.getStatus(asset);
        const color = this.config.color ? `style='color: ${this.config.color};'` : '';
        const icon = this.leaflet.divIcon({
            html: `<div class="dlt-c8y-icon-marker icon-3x ${status}" ${color}><i class="dlt-c8y-icon-${assetTypeIcon || 'data-transfer'}" /></div>`,
            className: 'c8y-map-marker-icon',
            // iconAnchor is used to set the marker accurately on click
            iconAnchor: anchor
        });
        return icon;
    }
    /**
     * Returns a Leaflet icon for a tracking event.
     * @returns The Leaflet icon.
     */
    getTrackingIcon() {
        const icon = this.leaflet.divIcon({
            html: `<div class="dlt-c8y-icon-marker icon-3x text-muted"></div>`,
            className: 'c8y-map-marker-icon',
            // iconAnchor is used to set the marker accurately on click
            iconAnchor: [8, 8]
        });
        return icon;
    }
    /**
     * Removes a marker from the map and internal marker list.
     * @param marker The marker to remove.
     */
    removeMarker(marker) {
        if (marker) {
            marker.remove();
            remove(this.markers, m => m === marker);
        }
    }
    /**
     * Removes all markers from the map, optionally filtered by marker attribute.
     * @param fragment Optional marker attribute to filter by.
     */
    clearMarkers(fragment) {
        const matchingMarkers = marker => !fragment || !!marker[fragment];
        this.markers.filter(matchingMarkers).forEach(marker => marker.remove());
        this.markers = this.markers.filter(marker => !matchingMarkers(marker));
    }
    /**
     * Refreshes all markers on the map based on the current assets.
     */
    refreshMarkers() {
        this.clearMarkers();
        let assets = [];
        if (!isUndefined(this.assets)) {
            assets = Array.isArray(this.assets) ? this.assets : [this.assets];
        }
        assets.forEach(asset => {
            const marker = this.getAssetMarkerSingle(asset);
            this.addMarkerToMap(marker);
        });
        if (!this.config.center) {
            this.zoomToBound(assets);
        }
        this.toggleControls();
    }
    /**
     * Centers the map on the configured center coordinates.
     */
    center() {
        this.map?.setView(this.config.center);
    }
    /**
     * Refreshes the map and markers if the map is initialized.
     */
    refresh() {
        if (this.isInit) {
            this.refreshMarkers();
        }
    }
    async ngAfterViewInit() {
        this.leaflet = await this.mapService.getLeaflet();
        const initialized$ = combineLatest([this.layers$, this.defaultConfig$]).pipe(first(), takeUntil(this.unsubscribeTrigger$));
        initialized$.subscribe(([layers, defaultConfig]) => {
            this.initMap(layers, defaultConfig);
            this.refreshMarkers();
        });
        combineLatest([this.polyline$, initialized$])
            .pipe(map(([expressions]) => this.leaflet.polyline(expressions, this.polylineOptions)), scan((oldPolyline, newPolyline) => {
            if (!!oldPolyline) {
                this.map.removeLayer(oldPolyline);
            }
            if (!!newPolyline) {
                newPolyline.addTo(this.map);
                this.fitBounds(newPolyline.getBounds());
            }
            return newPolyline;
        }, null), takeUntil(this.unsubscribeTrigger$))
            .subscribe();
    }
    ngOnChanges(changes) {
        if (!this.map) {
            return;
        }
        if (changes.assets?.currentValue && !changes.assets?.firstChange) {
            this.refreshMarkers();
        }
        if (changes.config?.currentValue && !changes.config?.firstChange) {
            this.changeConfig(changes.config);
        }
    }
    ngOnDestroy() {
        this.unsubscribeAllListeners();
        this.destroy$.next();
        this.destroy$.complete();
    }
    unsubscribeAllListeners() {
        this.unsubscribeTrigger$.next();
        this.stopRealtime();
    }
    initOutputs() {
        const getMapEventObservable = eventName => {
            return this.onMap.pipe(filter(map => !!map), switchMap(map => fromEvent(map, eventName)), takeUntilDestroyed());
        };
        const dragStart$ = getMapEventObservable('dragstart');
        const move$ = getMapEventObservable('move');
        const dragEnd$ = getMapEventObservable('dragend');
        this.onMove = dragStart$.pipe(switchMap(() => move$.pipe(takeUntil(dragEnd$))), takeUntil(this.unsubscribeTrigger$));
        this.onMoveEnd = getMapEventObservable('moveend');
        this.onZoomEnd = getMapEventObservable('zoomend');
        this.onZoomStart = getMapEventObservable('zoomstart');
    }
    initMap(layers, defaultConfig) {
        const defaultOptions = {
            center: this.config.center || defaultConfig.center,
            zoomSnap: 0,
            zoom: this.config.zoomLevel || defaultConfig.zoomLevel,
            worldCopyJump: true
        };
        if (this.map) {
            this.map.remove();
        }
        this.map = this.leaflet.map(this.mapElement.nativeElement, defaultOptions);
        this.map.attributionControl.setPrefix('');
        this.fitBounds(this.config.bounds);
        this.addLayers(layers);
        this.handleMobile();
        this.onMap.next(this.map);
        if (this.config.realtime) {
            this.startRealtime();
        }
        this.isInit = true;
        this.onInit.emit(this.leaflet);
    }
    handleMobile() {
        // adding event listener to do mobile 2 finger scrolling
        if (this.leaflet.Browser.mobile) {
            const touchMsg = this.translateService.instant(gettext('Use two fingers to move the map.'));
            this.map.dragging.disable();
            const container = this.map.getContainer();
            container.setAttribute('data-touch-warning-content', touchMsg);
            container.addEventListener('touchstart', event => this.handleTouch(event));
            container.addEventListener('touchmove', event => this.handleTouch(event));
            container.addEventListener('touchend', event => this.handleTouch(event));
            container.addEventListener('touchcancel', event => this.handleTouch(event));
            container.addEventListener('click', event => this.handleTouch(event));
        }
    }
    addLayers(layers) {
        const flattenLayers = flatten(layers);
        const baseLayers = {};
        const overlays = {};
        let firstLayer = true;
        for (const layer of sortByPriority(flattenLayers)) {
            const objectToAddTo = layer.isOverlay ? overlays : baseLayers;
            if (objectToAddTo[layer.label]) {
                continue;
            }
            const tiles = this.leaflet.tileLayer(layer.layerUrl, layer.options);
            if (!layer.isOverlay && firstLayer) {
                firstLayer = false;
                tiles.addTo(this.map);
            }
            objectToAddTo[layer.label] = tiles;
        }
        if (flattenLayers.length > 1) {
            this.leaflet.control.layers(baseLayers, overlays, { position: 'bottomleft' }).addTo(this.map);
        }
    }
    changeConfig(change) {
        if (this.hasChanged(change, 'zoomLevel')) {
            this.map.setZoom(this.config.zoomLevel);
        }
        if (this.hasChanged(change, 'center') && every(change.currentValue.center, p => !isNull(p))) {
            this.map.setView(change.currentValue.center);
        }
        if (this.hasChanged(change, 'icon') || this.hasChanged(change, 'color')) {
            this.refreshMarkers();
        }
        if (this.hasChanged(change, 'realtime') && change.currentValue.realtime) {
            this.startRealtime();
        }
        if (change.currentValue.realtime === false) {
            this.stopRealtime();
        }
        if (this.hasChanged(change, 'follow')) {
            this.moveToPositionOfMo(this.assets);
        }
        if (this.hasChanged(change, 'bounds')) {
            this.fitBounds(change.currentValue.bounds);
        }
        if (this.hasChanged(change, 'disablePan') || this.hasChanged(change, 'disableZoom')) {
            this.toggleControls();
        }
    }
    hasChanged(change, prop) {
        return change.currentValue[prop] !== change.previousValue[prop];
    }
    toggleControls() {
        if (this.config.disableZoom) {
            this.map.removeControl(this.map.zoomControl);
            this.map.scrollWheelZoom.disable();
        }
        else {
            this.map.addControl(this.map.zoomControl);
            this.map.scrollWheelZoom.enable();
        }
        if (this.config.disablePan) {
            this.map.dragging.disable();
        }
        else {
            this.map.dragging.enable();
        }
    }
    handleTouch(e) {
        // Disregard touch events on the minimap if present
        const ignoreList = [
            'leaflet-control-minimap',
            'leaflet-interactive',
            'leaflet-popup-content',
            'leaflet-popup-content-wrapper',
            'leaflet-popup-close-button',
            'leaflet-control-zoom-in',
            'leaflet-control-zoom-out'
        ];
        let ignoreElement = false;
        for (let i = 0; i < ignoreList.length; i++) {
            if (this.leaflet.DomUtil.hasClass(e.target, ignoreList[i])) {
                ignoreElement = true;
            }
        }
        const container = this.map.getContainer();
        if (ignoreElement) {
            if (this.leaflet.DomUtil.hasClass(e.target, 'leaflet-interactive') &&
                e.type === 'touchmove' &&
                e.touches.length === 1) {
                this.leaflet.DomUtil.addClass(container, 'touch-warning');
                this.map.dragging.disable();
            }
            else {
                this.leaflet.DomUtil.removeClass(container, 'touch-warning');
            }
            return;
        }
        if (e.type !== 'touchmove' && e.type !== 'touchstart') {
            this.leaflet.DomUtil.removeClass(container, 'touch-warning');
            return;
        }
        if (e.touches.length === 1) {
            this.leaflet.DomUtil.addClass(container, 'touch-warning');
            this.map.dragging.disable();
        }
        else {
            this.map.dragging.enable();
            this.leaflet.DomUtil.removeClass(container, 'touch-warning');
        }
    }
    zoomToBound(assets) {
        if (isEmpty(assets))
            return;
        const bounds = assets.map(asset => [
            asset.c8y_Position.lat,
            asset.c8y_Position.lng
        ]);
        if (assets.length > 1) {
            this.map.flyToBounds(bounds, { animate: false });
            return;
        }
        this.map.flyTo([assets[0].c8y_Position.lat, assets[0].c8y_Position.lng], this.map.options.zoom, {
            animate: false
        });
    }
    fitBounds(bounds) {
        if (bounds?.isValid()) {
            this.map.fitBounds(bounds, this.config.fitBoundsOptions);
        }
    }
    bindPopup(marker, context, offset = new this.leaflet.Point(-3, -40)) {
        if (this.popup) {
            marker.on('click', () => {
                this.popup.viewContainer.clear();
                const view = this.popup.viewContainer.createEmbeddedView(this.popup.template, {
                    $implicit: context
                });
                view.detectChanges();
                marker
                    .unbindPopup()
                    .bindPopup(this.popup.elementRef.nativeElement.previousSibling, {
                    offset,
                    maxWidth: 140,
                    autoPan: true,
                    closeButton: false
                })
                    .openPopup();
            });
        }
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: MapComponent, deps: [{ token: i1$1.ManagedObjectRealtimeService }, { token: MapService }, { token: MAP_TILE_LAYER }, { token: MAP_DEFAULT_CONFIG }, { token: i3.TranslateService }, { token: i1$1.GeoService }, { token: i1$1.DatePipe }, { token: i1$1.WidgetGlobalAutoRefreshService }], target: i0.ɵɵFactoryTarget.Component }); }
    static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.25", type: MapComponent, isStandalone: true, selector: "c8y-map", inputs: { config: "config", assets: "assets", polyline$: "polyline$", polylineOptions: "polylineOptions" }, outputs: { onRealtimeUpdate: "onRealtimeUpdate", onMove: "onMove", onMoveEnd: "onMoveEnd", onZoomStart: "onZoomStart", onZoomEnd: "onZoomEnd", onMap: "onMap", onInit: "onInit" }, providers: [ManagedObjectRealtimeService], queries: [{ propertyName: "popup", first: true, predicate: MapPopupDirective, descendants: true }], viewQueries: [{ propertyName: "mapElement", first: true, predicate: ["map"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"c8y-map\">\n  <div #map></div>\n</div>\n<ng-content></ng-content>\n" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: MapComponent, decorators: [{
            type: Component,
            args: [{ selector: 'c8y-map', providers: [ManagedObjectRealtimeService], template: "<div class=\"c8y-map\">\n  <div #map></div>\n</div>\n<ng-content></ng-content>\n" }]
        }], ctorParameters: () => [{ type: i1$1.ManagedObjectRealtimeService }, { type: MapService }, { type: i4.Observable, decorators: [{
                    type: Inject,
                    args: [MAP_TILE_LAYER]
                }] }, { type: i4.Observable, decorators: [{
                    type: Inject,
                    args: [MAP_DEFAULT_CONFIG]
                }] }, { type: i3.TranslateService }, { type: i1$1.GeoService }, { type: i1$1.DatePipe }, { type: i1$1.WidgetGlobalAutoRefreshService }], propDecorators: { mapElement: [{
                type: ViewChild,
                args: ['map']
            }], popup: [{
                type: ContentChild,
                args: [MapPopupDirective]
            }], config: [{
                type: Input
            }], assets: [{
                type: Input
            }], polyline$: [{
                type: Input
            }], polylineOptions: [{
                type: Input
            }], onRealtimeUpdate: [{
                type: Output
            }], onMove: [{
                type: Output
            }], onMoveEnd: [{
                type: Output
            }], onZoomStart: [{
                type: Output
            }], onZoomEnd: [{
                type: Output
            }], onMap: [{
                type: Output
            }], onInit: [{
                type: Output
            }] } });

/**
 * Smart map component for that clusters devices together when there are too many to display individually.
 * Unlike the basic map, this component loads device data dynamically and manages cluster rendering and updates.
 * Extends the base MapComponent with clustering, dynamic data loading, and advanced refresh logic.
 */
class ClusterMapComponent extends MapComponent {
    /**
     * Constructs the ClusterMapComponent, injecting required services and initializing the base map.
     */
    constructor(moRealtimeService, mapService, layers$, defaultConfig$, translateService, widgetGlobalAutoRefreshService, iterable, colorService, geo, datePipe) {
        super(moRealtimeService, mapService, layers$, defaultConfig$, translateService, geo, datePipe, widgetGlobalAutoRefreshService);
        this.moRealtimeService = moRealtimeService;
        this.mapService = mapService;
        this.layers$ = layers$;
        this.defaultConfig$ = defaultConfig$;
        this.translateService = translateService;
        this.widgetGlobalAutoRefreshService = widgetGlobalAutoRefreshService;
        this.iterable = iterable;
        this.colorService = colorService;
        /** Emits true while the map is loading or refreshing clusters. */
        this.isLoading$ = new BehaviorSubject(false);
        /**
         * Whether to show a color overlay for each cluster rectangle. This can be useful for debugging or visualizing clusters.
         */
        this.showClusterColor = false;
        /**
         * Emits Leaflet map change events (move, moveend) for external listeners.
         */
        this.mapChange = new EventEmitter();
        /** @ignore */
        this.errorNotifier = new BehaviorSubject(null);
        /**
         * Indicates if the map was manually changed by the user (e.g., panned or zoomed).
         */
        this.isDirty = false;
        this.reloadTrigger$ = new BehaviorSubject(false);
        this.clusters = [];
        this.EVENT_THROTTLE_TIME = 750;
        /** Tracks the last followed position to prevent unnecessary map movements. */
        this.lastFollowedPosition = null;
        /** Flag to skip first map movement after switching from realtime (prevents jump to stale cluster data). */
        this.skipNextFollowMove = false;
    }
    /**
     * @ignore
     */
    async ngOnChanges(changes) {
        if (changes.config?.firstChange || !this.map) {
            return;
        }
        if (changes.rootNode?.previousValue !== changes.rootNode?.currentValue) {
            this.changeRootNode(changes.rootNode.currentValue);
        }
        if (changes.config?.currentValue) {
            this.changeConfig(changes.config);
        }
    }
    /**
     * Handles changes to the map configuration using a state machine approach.
     * @param change The config change object.
     */
    changeConfig(change) {
        if (!this.map)
            return;
        const prevState = this.getMapState(change.previousValue);
        const currState = this.getMapState(change.currentValue);
        if (prevState !== currState) {
            this.handleStateTransition(prevState, currState);
        }
        this.handleFollowChanges(change);
        this.handleRefreshIntervalChanges(change);
        super.changeConfig(change);
    }
    /**
     * @ignore
     */
    async ngAfterViewInit() {
        if (!this.leaflet) {
            this.leaflet = await this.mapService.getLeaflet();
        }
        if (this.config.widgetInstanceGlobalAutoRefreshContext) {
            this.handleGlobalRefreshLoading();
        }
        combineLatest([this.layers$, this.defaultConfig$])
            .pipe(takeUntil(this.unsubscribeTrigger$))
            .subscribe(([layers, defaultConfig]) => {
            this.initMap(layers, defaultConfig);
            this.changeRootNode(this.rootNode);
            this.changeConfig(new SimpleChange({}, this.config, false));
        });
    }
    /**
     * Resets the map and clusters, re-initializing the component.
     */
    async reset() {
        this.ngOnDestroy();
        await this.ngAfterViewInit();
    }
    /**
     * Triggers a reload of cluster data.
     */
    reload() {
        this.reloadTrigger$.next(true);
    }
    /**
     * Cancels an ongoing reload operation.
     */
    cancelReload() {
        this.reloadTrigger$.next(false);
    }
    /**
     * Subscribes to cluster and interval changes, updating clusters as needed.
     * Handles auto-refresh and global refresh loading.
     */
    listenToClusterAndIntervalChanges() {
        const mapChange$ = this.getMapChangeObservable();
        merge(this.reloadTrigger$, mapChange$)
            .pipe(tap(() => {
            this.isLoading$.next(true);
        }), filter(value => {
            if (value === false) {
                this.isLoading$.next(false);
                return false;
            }
            const isMapChangeEvent = typeof value !== 'boolean';
            const shouldSkip = isMapChangeEvent && this.config.follow;
            if (shouldSkip) {
                this.isLoading$.next(false);
            }
            return !shouldSkip;
        }), switchMap(value => value === false
            ? of([])
            : from(this.mapService.getClusterSize(this.map.getBounds())).pipe(mergeMap((clusterSize) => this.getClusterRects(clusterSize, this.map.getBounds())), mergeMap(rects => this.createOrUpdateCluster(rects)), catchError(error => {
                this.errorNotifier.next(error);
                return of([]);
            }))), takeUntil(this.unsubscribeTrigger$))
            .subscribe((clusters) => {
            // Don't render clusters if in realtime mode (single asset with markers)
            if (this.config.realtime && this.assets) {
                this.isLoading$.next(false);
                return;
            }
            // If follow mode with asset but cluster has no positions, render marker from asset
            const hasNoClusterPositions = clusters.length === 1 && clusters[0].positions?.length === 0;
            if (this.config.follow && this.assets && hasNoClusterPositions) {
                this.refreshMarkers();
            }
            clusters.forEach(cluster => cluster.render(this.map));
            if (clusters.length === 1 && clusters[0].positions?.length === 1 && this.config.follow) {
                const position = clusters[0].positions[0].items[0];
                const newPos = { lat: position.c8y_Position.lat, lng: position.c8y_Position.lng };
                if (this.skipNextFollowMove) {
                    this.skipNextFollowMove = false;
                    this.lastFollowedPosition = newPos;
                }
                else {
                    // Only move map if position changed significantly (tolerance: 0.00001 degrees ≈ 1 meter)
                    const hasSignificantChange = !this.lastFollowedPosition ||
                        Math.abs(this.lastFollowedPosition.lat - newPos.lat) > 0.00001 ||
                        Math.abs(this.lastFollowedPosition.lng - newPos.lng) > 0.00001;
                    if (hasSignificantChange) {
                        this.lastFollowedPosition = newPos;
                        this.moveToPositionOfMo(position);
                    }
                }
            }
            this.isLoading$.next(false);
        });
    }
    /**
     * Subscribes to map change events for cluster updates.
     */
    listenToClusterMapChanges() {
        this.getMapChangeObservable().subscribe();
    }
    /**
     * Refreshes markers or clusters, depending on whether a single asset or clusters are shown.
     */
    refreshMarkers() {
        if (this.assets) {
            super.refreshMarkers();
            return;
        }
        this.clusters.forEach(cluster => {
            cluster.clear(this.map);
        });
        this.reload();
    }
    /**
     * Overrides base startRealtime to ensure assets and markers are set for single device realtime mode.
     * Also switches from cluster subscription to marker-only mode.
     */
    startRealtime() {
        // Ensure assets is set for realtime mode
        if (!this.assets) {
            // Try to use current position from clusters if available (prevents jumping to old position)
            const hasClusterWithPosition = this.clusters?.length === 1 && this.clusters[0].positions?.length === 1;
            if (hasClusterWithPosition) {
                this.assets = this.clusters[0].positions[0].items[0];
            }
            else if (this.rootNode?.c8y_Position && this.rootNode?.c8y_IsDevice) {
                this.assets = this.rootNode;
            }
        }
        // Switch from cluster mode to single asset mode
        if (this.map && this.clusters?.length > 0) {
            this.unsubscribeAllListeners();
            this.clearClusters();
        }
        // Ensure markers exist BEFORE starting realtime (so updates can find them)
        if (this.map && this.assets && (!this.markers || this.markers.length === 0)) {
            this.refreshMarkers();
        }
        // Call parent startRealtime to start realtime subscription
        super.startRealtime();
    }
    /**
     * Determines the current map state based on config.
     */
    getMapState(config) {
        if (!config)
            return 'initial';
        const isRealtime = config.realtime === true;
        const isAutoRefreshEnabled = config.isAutoRefreshEnabled ?? true;
        if (isRealtime && isAutoRefreshEnabled) {
            return 'realtime';
        }
        else if (!isRealtime && !isAutoRefreshEnabled) {
            return 'paused';
        }
        else {
            return 'auto-refresh';
        }
    }
    /**
     * Handles state transitions using a clear state machine pattern.
     */
    handleStateTransition(from, to) {
        const transition = `${from}->${to}`;
        switch (transition) {
            case 'realtime->paused':
                this.transitionRealtimeToPaused();
                break;
            case 'realtime->auto-refresh':
                this.transitionRealtimeToAutoRefresh();
                break;
            case 'paused->realtime':
                this.transitionPausedToRealtime();
                break;
            case 'paused->auto-refresh':
                this.transitionPausedToAutoRefresh();
                break;
            case 'auto-refresh->realtime':
                this.transitionAutoRefreshToRealtime();
                break;
            case 'auto-refresh->paused':
                this.transitionAutoRefreshToPaused();
                break;
            case 'initial->realtime':
            case 'initial->auto-refresh':
            case 'initial->paused':
                break;
            default:
            // No action needed for unhandled transitions
        }
    }
    /** Realtime → Paused: Keep markers, stop subscriptions */
    transitionRealtimeToPaused() {
        this.unsubscribeAllListeners();
    }
    /** Realtime → Auto-refresh: Switch to cluster mode */
    transitionRealtimeToAutoRefresh() {
        this.unsubscribeAllListeners();
        // Preserve marker position to prevent jump
        this.preserveMarkerPosition();
        this.clearMarkers();
        this.assets = null;
        this.listenToClusterAndIntervalChanges();
    }
    /** Paused → Realtime: Resume with existing markers */
    transitionPausedToRealtime() {
        this.cancelReload();
        this.isLoading$.next(false);
        this.unsubscribeAllListeners();
        // If assets are null (paused from cluster mode), set up for realtime
        if (!this.assets && this.rootNode) {
            this.changeRootNode(this.rootNode);
            return; // changeRootNode handles the rest
        }
        // Keep this.assets (already has current position)
        this.clearClusters();
        this.listenToClusterMapChanges();
    }
    /** Paused → Auto-refresh: Switch to cluster mode */
    transitionPausedToAutoRefresh() {
        this.unsubscribeAllListeners();
        // Preserve marker position to prevent jump
        this.preserveMarkerPosition();
        this.clearMarkers();
        this.assets = null;
        this.listenToClusterAndIntervalChanges();
    }
    /** Auto-refresh → Realtime: Use cluster position, switch to marker mode */
    transitionAutoRefreshToRealtime() {
        this.cancelReload();
        this.isLoading$.next(false);
        this.unsubscribeAllListeners();
        // Use current cluster position if available
        const hasClusterWithPosition = this.clusters?.length === 1 && this.clusters[0].positions?.length === 1;
        if (hasClusterWithPosition) {
            this.assets = this.clusters[0].positions[0].items[0];
        }
        else if (this.rootNode?.c8y_Position && this.rootNode?.c8y_IsDevice) {
            this.assets = this.rootNode;
        }
        this.clearClusters();
        this.listenToClusterMapChanges();
    }
    /** Auto-refresh → Paused: Keep clusters visible, stop listeners, preserve position */
    transitionAutoRefreshToPaused() {
        this.cancelReload();
        this.isLoading$.next(false);
        this.unsubscribeAllListeners();
        // Extract and preserve position from cluster for later use
        const hasClusterWithPosition = this.clusters?.length === 1 && this.clusters[0].positions?.length === 1;
        if (hasClusterWithPosition) {
            const position = this.clusters[0].positions[0].items[0];
            this.assets = position; // Store for later use when resuming to realtime
        }
    }
    /** Helper to preserve marker position before transitions */
    preserveMarkerPosition() {
        if (this.markers?.length === 1 && this.markers[0] && this.config.follow) {
            const marker = this.markers[0];
            const latLng = marker.getLatLng();
            this.lastFollowedPosition = { lat: latLng.lat, lng: latLng.lng };
            this.skipNextFollowMove = true;
        }
    }
    /** Handle follow mode changes */
    handleFollowChanges(change) {
        const followChangedToTrue = change.previousValue?.follow === false && change.currentValue?.follow === true;
        if (followChangedToTrue) {
            this.cancelReload();
            this.isLoading$.next(false);
        }
        const followChangedToFalse = change.previousValue?.follow === true && change.currentValue?.follow === false;
        if (followChangedToFalse) {
            this.lastFollowedPosition = null;
        }
    }
    /** Handle refresh interval changes */
    handleRefreshIntervalChanges(change) {
        if (change.currentValue?.refreshInterval !== change.previousValue?.refreshInterval) {
            this.reload();
        }
    }
    changeRootNode(mo) {
        this.unsubscribeAllListeners();
        this.clearMarkers();
        this.clearClusters();
        const isPositionDevice = mo?.c8y_Position && mo?.c8y_IsDevice;
        if (isPositionDevice && this.config.realtime) {
            this.assets = mo;
            this.refreshMarkers();
            this.listenToClusterMapChanges();
        }
        else {
            this.assets = null;
            this.listenToClusterAndIntervalChanges();
            this.reload();
        }
    }
    async getClusterRects(levelThreshold = ClusterSize.FOUR, viewBounds, level = 0) {
        let rects = [];
        if (levelThreshold === ClusterSize.NONE) {
            const rect = await this.getRect(viewBounds);
            rects.push(rect);
            return rects;
        }
        if (level >= levelThreshold) {
            return rects;
        }
        level++;
        const { lat: x1, lng: y1 } = viewBounds.getSouthWest();
        const { lat: x2, lng: y2 } = viewBounds.getNorthEast();
        const newX2 = (x1 + x2) / 2;
        const newY2 = (y1 + y2) / 2;
        const bounds = [
            [
                [x1, y1],
                [newX2, newY2]
            ],
            [
                [newX2, newY2],
                [x2, y2]
            ],
            [
                [x1, newY2],
                [newX2, y2]
            ],
            [
                [newX2, y1],
                [x2, newY2]
            ]
        ];
        for (const bound of bounds) {
            const latLngBound = this.leaflet.latLngBounds(bound);
            const rect = await this.getRect(latLngBound);
            rects = [...rects, ...(await this.getClusterRects(levelThreshold, latLngBound, level))];
            if (level === levelThreshold) {
                rects.push(rect);
            }
        }
        return rects;
    }
    async getRect(latLngBound) {
        let color = 'none';
        if (this.showClusterColor) {
            color = await this.colorService.generateColor(latLngBound.toBBoxString());
        }
        const rect = this.leaflet.rectangle(latLngBound, {
            color,
            weight: color === 'none' ? 0 : 1,
            interactive: false
        });
        return rect;
    }
    clearClusters() {
        this.clusters.forEach(cluster => {
            cluster.clear(this.map);
        });
        this.clusters = [];
    }
    async updateCluster(cluster) {
        const clusterCount = await this.mapService.getPositionMOsFromBoundCount(cluster.rect.getBounds(), this.rootNode);
        if (clusterCount > this.mapService.MAX_DEVICE_PER_CLUSTER) {
            cluster.setClusterToBigMarker(this.map, clusterCount, this.leaflet);
            cluster.positions = [];
            return cluster;
        }
        cluster.removeClusterToBigMarker();
        const positionMOs = await this.mapService.getPositionMOsFromBound(cluster.rect.getBounds(), this.rootNode);
        const NEARBY_GROUPING_THRESHOLD_PX = 25;
        const groupedPositions = this.groupNearbyAssets(positionMOs, NEARBY_GROUPING_THRESHOLD_PX);
        cluster.positions = groupedPositions;
        return cluster;
    }
    /**
     * Groups nearby assets based on pixel distance threshold.
     * @param assets Array of assets to group.
     * @param thresholdPixels Distance threshold in pixels.
     * @returns Array of asset groups.
     */
    groupNearbyAssets(assets, thresholdPixels) {
        // Get current zoom level and adjust threshold accordingly
        const currentZoom = this.map.getZoom();
        const zoomThreshold = 15; // Zoom level above which grouping is disabled
        const zoomFactor = 1 - currentZoom / zoomThreshold;
        const NEARBY_GROUPING_MIN_ZOOM_FACTOR = 0.3;
        const adjustedThreshold = thresholdPixels * Math.max(NEARBY_GROUPING_MIN_ZOOM_FACTOR, zoomFactor);
        // Sort assets by latitude first, then longitude for efficient proximity checks
        const sortedAssets = [...assets].sort((a, b) => {
            const latDiff = a.c8y_Position.lat - b.c8y_Position.lat;
            return latDiff !== 0 ? latDiff : a.c8y_Position.lng - b.c8y_Position.lng;
        });
        const groups = [];
        const processed = new Set();
        sortedAssets.forEach(asset => {
            if (processed.has(asset.id)) {
                return;
            }
            const group = [asset];
            processed.add(asset.id);
            sortedAssets.forEach(otherAsset => {
                if (processed.has(otherAsset.id)) {
                    return;
                }
                const isClose = group.some(groupAsset => this.areMarkersClose(groupAsset, otherAsset, adjustedThreshold));
                if (isClose) {
                    group.push(otherAsset);
                    processed.add(otherAsset.id);
                }
            });
            groups.push({ items: group });
        });
        return groups;
    }
    /**
     * Checks if two assets are within the specified pixel distance.
     * @param asset1 First asset.
     * @param asset2 Second asset.
     * @param thresholdPixels Distance threshold in pixels.
     * @returns True if assets are close, false otherwise.
     */
    areMarkersClose(asset1, asset2, thresholdPixels) {
        const latLng1 = this.leaflet.latLng(asset1.c8y_Position.lat, asset1.c8y_Position.lng);
        const latLng2 = this.leaflet.latLng(asset2.c8y_Position.lat, asset2.c8y_Position.lng);
        const point1 = this.map.latLngToContainerPoint(latLng1);
        const point2 = this.map.latLngToContainerPoint(latLng2);
        const distance = point1.distanceTo(point2);
        return distance <= thresholdPixels;
    }
    createOrUpdateCluster(rects) {
        const isNew = rects.length !== this.clusters.length;
        if (isNew) {
            this.clearClusters();
        }
        const updatePromise = rects.map((rect, index) => {
            if (isNew) {
                const cluster = new ClusterMap(this.iterable, asset => this.getAssetMarker(asset), this.translateService);
                this.clusters.push(cluster);
            }
            this.clusters[index].rect = rect;
            return this.updateCluster(this.clusters[index]);
        });
        return Promise.all(updatePromise);
    }
    getMapChangeObservable() {
        // Set isDirty flag on user interactions
        merge(fromEvent(this.map, 'mousedown'), fromEvent(this.mapElement?.nativeElement, 'wheel'), fromEvent(this.map, 'touchstart'))
            .pipe(take(1), takeUntil(this.unsubscribeTrigger$))
            .subscribe(() => {
            this.isDirty = true;
        });
        let skipMoveEvent = false;
        return merge(fromEvent(this.map, 'move'), fromEvent(this.map, 'moveend')).pipe(debounceTime(this.EVENT_THROTTLE_TIME), switchMap(async (event) => {
            // If the map wasn't changed by user and we have autoFitToBounds, fit the bounds
            // Skip this event and let the fitBounds-triggered event be processed instead
            if (this.config.autoFitToBounds && !this.isDirty && !skipMoveEvent) {
                const bounds = await this.mapService.getAllBounds(this.rootNode);
                skipMoveEvent = true;
                this.map.fitBounds(bounds, this.config.fitBoundsOptions || defaultFitBoundsOptions);
                return null; // Skip current event
            }
            skipMoveEvent = false;
            return event;
        }), filter(event => event !== null), tap(event => {
            this.mapChange.emit(event);
        }), takeUntil(this.unsubscribeTrigger$));
    }
    handleGlobalRefreshLoading() {
        this.isLoading$
            .pipe(skip(1), globalAutoRefreshLoading(this.widgetGlobalAutoRefreshService), takeUntil(this.destroy$))
            .subscribe();
        this.destroy$.subscribe({
            complete: () => this.isLoading$.value && this.widgetGlobalAutoRefreshService.decrementLoading()
        });
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: ClusterMapComponent, deps: [{ token: i1$1.ManagedObjectRealtimeService }, { token: MapService }, { token: MAP_TILE_LAYER }, { token: MAP_DEFAULT_CONFIG }, { token: i3.TranslateService }, { token: i1$1.WidgetGlobalAutoRefreshService }, { token: i0.IterableDiffers }, { token: i1$1.ColorService }, { token: i1$1.GeoService }, { token: i1$1.DatePipe }], target: i0.ɵɵFactoryTarget.Component }); }
    static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.25", type: ClusterMapComponent, isStandalone: true, selector: "c8y-cluster-map", inputs: { config: "config", rootNode: "rootNode", assets: ["asset", "assets"], showClusterColor: "showClusterColor" }, outputs: { mapChange: "mapChange" }, providers: [ManagedObjectRealtimeService], viewQueries: [{ propertyName: "mapElement", first: true, predicate: ["map"], descendants: true }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: "<div class=\"c8y-map\">\n  <div #map></div>\n</div>\n<ng-content></ng-content>\n" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: ClusterMapComponent, decorators: [{
            type: Component,
            args: [{ selector: 'c8y-cluster-map', providers: [ManagedObjectRealtimeService], template: "<div class=\"c8y-map\">\n  <div #map></div>\n</div>\n<ng-content></ng-content>\n" }]
        }], ctorParameters: () => [{ type: i1$1.ManagedObjectRealtimeService }, { type: MapService }, { type: i4.Observable, decorators: [{
                    type: Inject,
                    args: [MAP_TILE_LAYER]
                }] }, { type: i4.Observable, decorators: [{
                    type: Inject,
                    args: [MAP_DEFAULT_CONFIG]
                }] }, { type: i3.TranslateService }, { type: i1$1.WidgetGlobalAutoRefreshService }, { type: i0.IterableDiffers }, { type: i1$1.ColorService }, { type: i1$1.GeoService }, { type: i1$1.DatePipe }], propDecorators: { config: [{
                type: Input
            }], rootNode: [{
                type: Input
            }], assets: [{
                type: Input,
                args: ['asset']
            }], showClusterColor: [{
                type: Input
            }], mapChange: [{
                type: Output
            }], mapElement: [{
                type: ViewChild,
                args: ['map']
            }] } });

/**
 * Component to display controls and status information for a map rendered on an IoT Dashboard.
 * Provides UI for centering, fitting bounds, toggling real-time updates, auto-refresh, and following assets.
 */
class MapStatusComponent {
    constructor() {
        /**
         * Emits when the map configuration changes (e.g., toggling real-time, follow, etc.).
         */
        this.configChange = new EventEmitter();
        /**
         * Emits when the user unfollows an asset on the map.
         */
        this.onUnfollow = new EventEmitter();
        /**
         * Emits when the user requests to fit all assets within the map bounds.
         */
        this.fitAssetsToBounds = new EventEmitter();
        /**
         * Configuration for which map status buttons are shown/enabled.
         * By default the "center" button is shown and realtime is disabled.
         * See {@link MapStatusButtonsConfig} for available options.
         */
        this.buttonsConfig = {};
        /**
         * @ignore
         */
        this.refreshPaused = false;
        /**
         * @ignore
         */
        this.autoRefreshPausedLabel = gettext('Auto refresh paused');
        /**
         * @ignore
         */
        this.autoRefreshEnabledLabel = gettext('Auto refresh enabled');
        this.destroy$ = new Subject();
    }
    /**
     * @ignore
     */
    ngOnInit() {
        this.initConfig = cloneDeep(this.config);
        this.buttonsConfig = Object.assign(this.defaultButtonsConfig(), this.buttonsConfig);
    }
    /**
     * @ignore
     */
    ngAfterViewInit() {
        if (!this.config.widgetInstanceGlobalAutoRefreshContext &&
            this.buttonsConfig.autoRefresh.show) {
            this.clusterMap.countdownIntervalComp = this.countdownIntervalComp;
        }
    }
    /**
     * @ignore
     */
    ngOnChanges(changes) {
        if (changes.clusterMap?.previousValue !== changes.clusterMap?.currentValue) {
            this.checkIfMapIsAlreadyCentered();
        }
    }
    /**
     * Centers the map on the configured center coordinates.
     */
    center() {
        this.clusterMap.center();
    }
    /**
     * Emits an event to fit all assets within the map bounds and disables the button until the next change.
     */
    fitToBounds() {
        this.buttonsConfig.fitToBounds.disabled = true;
        this.fitAssetsToBounds.emit();
    }
    /**
     * Reloads the map data.
     */
    reload() {
        this.clusterMap.reload();
    }
    /**
     * Cancels the map reload operation.
     */
    cancelReload() {
        this.clusterMap.cancelReload();
    }
    /**
     * Toggles real-time updates for the map.
     */
    toggleRealtime() {
        this.config = {
            ...this.config,
            realtime: !this.config.realtime
        };
        this.configChange.emit(this.config);
    }
    /**
     * Toggles the auto-refresh state for the map, pausing or resuming the countdown.
     * @param $event Mouse event to prevent default and stop propagation.
     */
    toggleAutoRefresh($event) {
        $event.preventDefault();
        $event.stopPropagation();
        this.refreshPaused = !this.refreshPaused;
        if (this.refreshPaused) {
            this.countdownIntervalComp.stop(true);
        }
        else {
            this.countdownIntervalComp.start();
        }
    }
    /**
     * Disables following an asset on the map and emits the change.
     */
    unfollow() {
        this.config = {
            ...this.clusterMap.config,
            follow: false
        };
        this.configChange.emit(this.config);
        this.onUnfollow.emit(this.config);
    }
    /**
     * Enables following an asset on the map and emits the change.
     */
    follow() {
        this.config = {
            ...this.config,
            follow: true
        };
        this.configChange.emit(this.config);
    }
    /**
     * @ignore
     */
    ngOnDestroy() {
        this.destroy$.next();
    }
    /**
     * Subscribes to map changes and updates button states based on map bounds and center.
     */
    checkIfMapIsAlreadyCentered() {
        this.clusterMap.mapChange.pipe(takeUntil(this.destroy$)).subscribe((event) => {
            if (event.sourceTarget?.getBounds) {
                const bounds = event.sourceTarget.getBounds();
                this.buttonsConfig.fitToBounds.disabled = this.shouldDisableFitToBoundsButton(bounds);
                if (this.config?.center) {
                    this.buttonsConfig.center.disabled = this.shouldDisableCenterButton(bounds);
                }
            }
        });
    }
    /**
     * Checks if Center button should be disabled according to provided bounds.
     * Provided bounds contain coordinates of current map rectangle corners.
     * Center button should be disabled if distance between center coordinates (from config) and center of current bounds
     * is less than 1/4 of bounds dimensions.
     * To achieve it we just need to check if center coordinates (from config) are contained in the boundaries of
     * current bounds shrunk by 25%.
     * @param bounds Current map view boundaries.
     * @returns True if distance between config center and current boundaries center is bigger than 1/4 of boundaries dimensions
     */
    shouldDisableCenterButton(bounds) {
        const shrunkBounds = bounds.pad(-0.25);
        return shrunkBounds.contains(this.config.center);
    }
    /**
     * Checks if Fit to Bounds button should be disabled based on whether all assets are within the current bounds.
     * @param bounds Current map view boundaries.
     * @returns True if all assets are already within bounds, false otherwise.
     */
    shouldDisableFitToBoundsButton(bounds) {
        if (!this.assets) {
            return true;
        }
        return this.assets.every(({ c8y_Position }) => bounds.contains([c8y_Position.lat, c8y_Position.lng]));
    }
    /**
     * Returns the default configuration for map status buttons.
     * @returns Default MapStatusButtonsConfig object.
     */
    defaultButtonsConfig() {
        return {
            realtime: { show: this.config.realtime || this.clusterMap.config.follow, disabled: false },
            autoRefresh: { show: true, disabled: false },
            refresh: { show: true, disabled: false },
            fitToBounds: { show: false, disabled: false },
            center: { show: true, disabled: false }
        };
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: MapStatusComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
    static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: MapStatusComponent, isStandalone: true, selector: "c8y-map-status", inputs: { config: "config", assets: "assets", clusterMap: "clusterMap", buttonsConfig: "buttonsConfig" }, outputs: { configChange: "configChange", onUnfollow: "onUnfollow", fitAssetsToBounds: "fitAssetsToBounds" }, viewQueries: [{ propertyName: "countdownIntervalComp", first: true, predicate: CountdownIntervalComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"c8y-map-status\">\n  <div class=\"leaflet-touch\">\n    <div\n      class=\"leaflet-bar\"\n      role=\"group\"\n    >\n      @if (buttonsConfig.realtime.show) {\n        <button\n          class=\"c8y-realtime\"\n          title=\"{{ 'Realtime' | translate }}\"\n          type=\"button\"\n          (click)=\"toggleRealtime()\"\n          [disabled]=\"buttonsConfig.realtime.disabled\"\n        >\n          <span\n            class=\"c8y-pulse\"\n            [ngClass]=\"{\n              active: clusterMap?.config.realtime,\n              inactive: !clusterMap?.config.realtime\n            }\"\n          ></span>\n        </button>\n      }\n      @if (\n        buttonsConfig.autoRefresh.show &&\n        !config.widgetInstanceGlobalAutoRefreshContext &&\n        clusterMap?.config.refreshInterval &&\n        !clusterMap?.config.follow\n      ) {\n        <label\n          class=\"toggle-countdown vertical\"\n          [attr.aria-label]=\"\n            (refreshPaused ? autoRefreshPausedLabel : autoRefreshEnabledLabel) | translate\n          \"\n          [tooltip]=\"(refreshPaused ? autoRefreshPausedLabel : autoRefreshEnabledLabel) | translate\"\n          placement=\"left\"\n          [adaptivePosition]=\"false\"\n          [container]=\"'body'\"\n          [delay]=\"1000\"\n        >\n          <input\n            type=\"checkbox\"\n            (click)=\"toggleAutoRefresh($event)\"\n          />\n          <c8y-countdown-interval\n            [hidden]=\"refreshPaused\"\n            [countdownInterval]=\"clusterMap?.config.refreshInterval\"\n          ></c8y-countdown-interval>\n          @if (refreshPaused) {\n            <i c8yIcon=\"pause\"></i>\n          }\n        </label>\n      }\n      @if (\n        buttonsConfig.refresh.show &&\n        (clusterMap?.isLoading$ | async) &&\n        !config.widgetInstanceGlobalAutoRefreshContext\n      ) {\n        <button\n          title=\"{{ 'Cancel reload' | translate }}\"\n          type=\"button\"\n          (click)=\"cancelReload()\"\n        >\n          <i\n            class=\"icon-spin\"\n            c8yIcon=\"refresh\"\n          ></i>\n        </button>\n      }\n      @if (\n        buttonsConfig.refresh.show &&\n        !clusterMap?.config.realtime &&\n        !clusterMap?.assets &&\n        !(clusterMap?.isLoading$ | async) &&\n        !config.widgetInstanceGlobalAutoRefreshContext\n      ) {\n        <button\n          [title]=\"'Reload' | translate\"\n          type=\"button\"\n          (click)=\"reload()\"\n        >\n          <i c8yIcon=\"refresh\"></i>\n        </button>\n      }\n      <ng-content></ng-content>\n      @if (buttonsConfig.center.show) {\n        <button\n          title=\"{{ 'Center map' | translate }}\"\n          type=\"button\"\n          (click)=\"center()\"\n          [disabled]=\"buttonsConfig.center.disabled || clusterMap?.config.follow\"\n        >\n          <i c8yIcon=\"target1\"></i>\n        </button>\n      }\n      @if (buttonsConfig.fitToBounds.show) {\n        <button\n          title=\"{{ 'Fit to assets bounds' | translate }}\"\n          type=\"button\"\n          (click)=\"fitToBounds()\"\n          [disabled]=\"buttonsConfig.fitToBounds.disabled || clusterMap?.config.follow\"\n        >\n          <i c8yIcon=\"waypoint-map\"></i>\n        </button>\n      }\n      @if (clusterMap?.config.follow) {\n        <button\n          title=\"{{ 'Unfollow' | translate }}\"\n          type=\"button\"\n          (click)=\"unfollow()\"\n        >\n          <i c8yIcon=\"marker-off\"></i>\n        </button>\n      }\n      @if (initConfig.follow && !clusterMap?.config.follow) {\n        <button\n          title=\"{{ 'Follow' | translate }}\"\n          type=\"button\"\n          (click)=\"follow()\"\n        >\n          <i c8yIcon=\"marker\"></i>\n        </button>\n      }\n    </div>\n  </div>\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: TooltipDirective, selector: "[tooltip], [tooltipHtml]", inputs: ["adaptivePosition", "tooltip", "placement", "triggers", "container", "containerClass", "boundariesElement", "isOpen", "isDisabled", "delay", "tooltipHtml", "tooltipPlacement", "tooltipIsOpen", "tooltipEnable", "tooltipAppendToBody", "tooltipAnimation", "tooltipClass", "tooltipContext", "tooltipPopupDelay", "tooltipFadeDuration", "tooltipTrigger"], outputs: ["tooltipChange", "onShown", "onHidden", "tooltipStateChanged"], exportAs: ["bs-tooltip"] }, { kind: "component", type: CountdownIntervalComponent, selector: "c8y-countdown-interval", inputs: ["countdownInterval", "config"], outputs: ["countdownEnded"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: MapStatusComponent, decorators: [{
            type: Component,
            args: [{ selector: 'c8y-map-status', imports: [
                        NgClass,
                        TooltipDirective,
                        CountdownIntervalComponent,
                        IconDirective,
                        AsyncPipe,
                        C8yTranslatePipe
                    ], template: "<div class=\"c8y-map-status\">\n  <div class=\"leaflet-touch\">\n    <div\n      class=\"leaflet-bar\"\n      role=\"group\"\n    >\n      @if (buttonsConfig.realtime.show) {\n        <button\n          class=\"c8y-realtime\"\n          title=\"{{ 'Realtime' | translate }}\"\n          type=\"button\"\n          (click)=\"toggleRealtime()\"\n          [disabled]=\"buttonsConfig.realtime.disabled\"\n        >\n          <span\n            class=\"c8y-pulse\"\n            [ngClass]=\"{\n              active: clusterMap?.config.realtime,\n              inactive: !clusterMap?.config.realtime\n            }\"\n          ></span>\n        </button>\n      }\n      @if (\n        buttonsConfig.autoRefresh.show &&\n        !config.widgetInstanceGlobalAutoRefreshContext &&\n        clusterMap?.config.refreshInterval &&\n        !clusterMap?.config.follow\n      ) {\n        <label\n          class=\"toggle-countdown vertical\"\n          [attr.aria-label]=\"\n            (refreshPaused ? autoRefreshPausedLabel : autoRefreshEnabledLabel) | translate\n          \"\n          [tooltip]=\"(refreshPaused ? autoRefreshPausedLabel : autoRefreshEnabledLabel) | translate\"\n          placement=\"left\"\n          [adaptivePosition]=\"false\"\n          [container]=\"'body'\"\n          [delay]=\"1000\"\n        >\n          <input\n            type=\"checkbox\"\n            (click)=\"toggleAutoRefresh($event)\"\n          />\n          <c8y-countdown-interval\n            [hidden]=\"refreshPaused\"\n            [countdownInterval]=\"clusterMap?.config.refreshInterval\"\n          ></c8y-countdown-interval>\n          @if (refreshPaused) {\n            <i c8yIcon=\"pause\"></i>\n          }\n        </label>\n      }\n      @if (\n        buttonsConfig.refresh.show &&\n        (clusterMap?.isLoading$ | async) &&\n        !config.widgetInstanceGlobalAutoRefreshContext\n      ) {\n        <button\n          title=\"{{ 'Cancel reload' | translate }}\"\n          type=\"button\"\n          (click)=\"cancelReload()\"\n        >\n          <i\n            class=\"icon-spin\"\n            c8yIcon=\"refresh\"\n          ></i>\n        </button>\n      }\n      @if (\n        buttonsConfig.refresh.show &&\n        !clusterMap?.config.realtime &&\n        !clusterMap?.assets &&\n        !(clusterMap?.isLoading$ | async) &&\n        !config.widgetInstanceGlobalAutoRefreshContext\n      ) {\n        <button\n          [title]=\"'Reload' | translate\"\n          type=\"button\"\n          (click)=\"reload()\"\n        >\n          <i c8yIcon=\"refresh\"></i>\n        </button>\n      }\n      <ng-content></ng-content>\n      @if (buttonsConfig.center.show) {\n        <button\n          title=\"{{ 'Center map' | translate }}\"\n          type=\"button\"\n          (click)=\"center()\"\n          [disabled]=\"buttonsConfig.center.disabled || clusterMap?.config.follow\"\n        >\n          <i c8yIcon=\"target1\"></i>\n        </button>\n      }\n      @if (buttonsConfig.fitToBounds.show) {\n        <button\n          title=\"{{ 'Fit to assets bounds' | translate }}\"\n          type=\"button\"\n          (click)=\"fitToBounds()\"\n          [disabled]=\"buttonsConfig.fitToBounds.disabled || clusterMap?.config.follow\"\n        >\n          <i c8yIcon=\"waypoint-map\"></i>\n        </button>\n      }\n      @if (clusterMap?.config.follow) {\n        <button\n          title=\"{{ 'Unfollow' | translate }}\"\n          type=\"button\"\n          (click)=\"unfollow()\"\n        >\n          <i c8yIcon=\"marker-off\"></i>\n        </button>\n      }\n      @if (initConfig.follow && !clusterMap?.config.follow) {\n        <button\n          title=\"{{ 'Follow' | translate }}\"\n          type=\"button\"\n          (click)=\"follow()\"\n        >\n          <i c8yIcon=\"marker\"></i>\n        </button>\n      }\n    </div>\n  </div>\n</div>\n" }]
        }], propDecorators: { config: [{
                type: Input
            }], assets: [{
                type: Input
            }], configChange: [{
                type: Output
            }], onUnfollow: [{
                type: Output
            }], fitAssetsToBounds: [{
                type: Output
            }], clusterMap: [{
                type: Input
            }], buttonsConfig: [{
                type: Input
            }], countdownIntervalComp: [{
                type: ViewChild,
                args: [CountdownIntervalComponent]
            }] } });

function provideMapConfig() {
    return [
        {
            provide: MAP_TILE_LAYER,
            useFactory: (mapService) => {
                return mapService.getMapTileLayers$();
            },
            deps: [MapService]
        },
        {
            provide: MAP_DEFAULT_CONFIG,
            useFactory: (mapService) => {
                return mapService.getDefaultConfig();
            },
            deps: [MapService]
        }
    ];
}
class MapModule {
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: MapModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
    static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.25", ngImport: i0, type: MapModule, imports: [CommonModule,
            FormsModule,
            CommonModule$1,
            FormsModule$1,
            RealtimeModule,
            CoreModule,
            TooltipModule,
            MapComponent,
            MapStatusComponent,
            ClusterMapComponent,
            MapPopupDirective], exports: [MapComponent, MapStatusComponent, ClusterMapComponent, MapPopupDirective] }); }
    static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: MapModule, providers: [provideMapConfig()], imports: [CommonModule,
            FormsModule,
            CommonModule$1,
            FormsModule$1,
            RealtimeModule,
            CoreModule,
            TooltipModule,
            MapStatusComponent] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: MapModule, decorators: [{
            type: NgModule,
            args: [{
                    imports: [
                        CommonModule,
                        FormsModule,
                        CommonModule$1,
                        FormsModule$1,
                        RealtimeModule,
                        CoreModule,
                        TooltipModule,
                        MapComponent,
                        MapStatusComponent,
                        ClusterMapComponent,
                        MapPopupDirective
                    ],
                    exports: [MapComponent, MapStatusComponent, ClusterMapComponent, MapPopupDirective],
                    providers: [provideMapConfig()]
                }]
        }] });

/**
 * Generated bundle index. Do not edit.
 */

export { ClusterMap, ClusterMapComponent, ClusterSize, MAP_DEFAULT_CONFIG, MAP_TILE_LAYER, MapComponent, MapModule, MapPopupDirective, MapService, MapStatusComponent, MapTenantOptionKeys, QueriesUtilDecimalExtension, defaultFitBoundsOptions, defaultLayer, defaultMapConfig, getC8yMarker, provideMapConfig };
//# sourceMappingURL=c8y-ngx-components-map.mjs.map
