UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

443 lines (437 loc) 42.7 kB
import * as i6 from '@angular/common'; import { CommonModule } from '@angular/common'; import * as i0 from '@angular/core'; import { Input, Component, signal, computed, ViewChild, Inject, Optional } from '@angular/core'; import * as i1 from '@c8y/client'; import * as i2 from '@c8y/ngx-components'; import { C8yTranslateDirective, LoadingComponent, DatePipe, CommonModule as CommonModule$1, FormsModule as FormsModule$1, C8yTranslatePipe, DismissAlertStrategy, DynamicComponentAlert, AssetLinkPipe } from '@c8y/ngx-components'; import * as i5 from '@angular/forms'; import { NgForm, ControlContainer, FormsModule, ReactiveFormsModule } from '@angular/forms'; import * as i4$1 from '@c8y/ngx-components/context-dashboard'; import { gettext } from '@c8y/ngx-components/gettext'; import * as i5$1 from '@c8y/ngx-components/global-context'; import { GLOBAL_CONTEXT_DISPLAY_MODE, GlobalContextWidgetWrapperComponent } from '@c8y/ngx-components/global-context'; import * as i6$1 from '@c8y/ngx-components/icon-selector'; import { IconSelectorModule } from '@c8y/ngx-components/icon-selector'; import * as i4 from '@c8y/ngx-components/map'; import { defaultFitBoundsOptions, MAP_DEFAULT_CONFIG, MapModule, MapComponent, ClusterMapComponent } from '@c8y/ngx-components/map'; import * as i3 from '@ngx-translate/core'; import { isUndefined } from 'lodash'; import { PopoverModule } from 'ngx-bootstrap/popover'; import * as i7 from 'ngx-bootstrap/tooltip'; import { TooltipModule } from 'ngx-bootstrap/tooltip'; import * as i8 from 'rxjs'; import { Subject, fromEvent } from 'rxjs'; import { takeUntil, combineLatestWith, take, filter } from 'rxjs/operators'; import { RouterLink } from '@angular/router'; import { defaultWidgetIds } from '@c8y/ngx-components/widgets/definitions'; import { merge, debounce } from 'lodash-es'; class MapEventInfoComponent { constructor(eventService) { this.eventService = eventService; this.loading = true; } async ngOnChanges() { this.loading = true; const { data } = await this.eventService.list({ dateFrom: '1970-01-01', dateTo: new Date(Date.now()).toISOString(), fragmentType: 'c8y_Position', pageSize: 1, source: this.asset.id }); this.event = data[0]; this.loading = false; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: MapEventInfoComponent, deps: [{ token: i1.EventService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: MapEventInfoComponent, isStandalone: true, selector: "c8y-map-event-info", inputs: { asset: "asset" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"text-muted text-10 p-t-4 p-b-4\">\n <span translate>Last position update:</span>\n <br />\n <c8y-loading *ngIf=\"loading\"></c8y-loading>\n <ng-container *ngIf=\"!loading\">\n <i *ngIf=\"!event\" translate>No information found</i>\n <ng-container *ngIf=\"event\">\n <i>{{ event.time | c8yDate }}</i>\n <ng-content></ng-content>\n </ng-container>\n </ng-container>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i6.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "component", type: LoadingComponent, selector: "c8y-loading", inputs: ["layout", "progress", "message"] }, { kind: "pipe", type: DatePipe, name: "c8yDate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: MapEventInfoComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-map-event-info', standalone: true, imports: [CommonModule, C8yTranslateDirective, DatePipe, LoadingComponent], template: "<div class=\"text-muted text-10 p-t-4 p-b-4\">\n <span translate>Last position update:</span>\n <br />\n <c8y-loading *ngIf=\"loading\"></c8y-loading>\n <ng-container *ngIf=\"!loading\">\n <i *ngIf=\"!event\" translate>No information found</i>\n <ng-container *ngIf=\"event\">\n <i>{{ event.time | c8yDate }}</i>\n <ng-content></ng-content>\n </ng-container>\n </ng-container>\n</div>\n" }] }], ctorParameters: () => [{ type: i1.EventService }], propDecorators: { asset: [{ type: Input }] } }); class MapWidgetConfigComponent { set previewMapSet(template) { if (template) { this.widgetConfigService.setPreview(template); return; } this.widgetConfigService.setPreview(null); } constructor(mapService, alertService, translateService, defaultMapConfig, widgetConfigService) { this.mapService = mapService; this.alertService = alertService; this.translateService = translateService; this.defaultMapConfig = defaultMapConfig; this.widgetConfigService = widgetConfigService; this.config = { mapConfig: undefined }; this.device = signal(undefined, ...(ngDevMode ? [{ debugName: "device" }] : [])); this.assets = signal([], ...(ngDevMode ? [{ debugName: "assets" }] : [])); this.canAutoCenter = computed(() => { const assetsList = this.assets(); const deviceValue = this.device(); const isSingleAsset = assetsList?.length === 1; const idMatchesDevice = deviceValue?.id === assetsList[0]?.id; if (isSingleAsset && idMatchesDevice) { return this.mapService.hasPosition(assetsList[0]); } return false; }, ...(ngDevMode ? [{ debugName: "canAutoCenter" }] : [])); this.isPositionedDevice = computed(() => { const assetsList = this.assets(); const deviceValue = this.device(); const isSingleAsset = assetsList?.length === 1; const idMatchesDevice = deviceValue?.id === assetsList[0]?.id; if (isSingleAsset && idMatchesDevice) { return this.mapService.isPositionedDevice(assetsList[0]); } return false; }, ...(ngDevMode ? [{ debugName: "isPositionedDevice" }] : [])); this.refreshOption = 'live'; this.destroyed$ = new Subject(); // JS precision for floating point number is 15 for 64-bit and because of this provided to leaflet are recalculated // to values almost-like provided (e.g. 0 is recalculated to 1.46e-14; for 180 it's 180.00000000000003). // To ensure provided value has no floating point error, number needs to be rounded to fixed point 13. this.FIXED_POINT_DIGITS = 13; } ngOnDestroy() { this.destroyed$.next(); } ngOnInit() { this.device.set(this.config.device); this.defaultMapConfig.pipe(takeUntil(this.destroyed$)).subscribe(defaultConfig => { this.formConfig = { zoomLevel: defaultConfig.zoomLevel, center: defaultConfig.center }; this.initForm(); }); } async initForm() { if (!this.config.mapConfig) { this.config.mapConfig = this.formConfig; } this.formConfig = { ...this.formConfig, ...this.config.mapConfig, disablePan: false, displayMode: this.config.mapConfig?.displayMode ?? GLOBAL_CONTEXT_DISPLAY_MODE.DASHBOARD, // Keep both in sync realtime: this.config.mapConfig?.isRealtimeEnabled ?? this.config.mapConfig?.realtime ?? false, refreshOption: this.config.mapConfig?.refreshOption, refreshInterval: this.config.mapConfig?.refreshInterval ?? 30_000 }; await this.updateAsset(); } async ngOnChanges(changes) { if (changes.config) { this.device.set(this.config.device); if (changes.config.currentValue !== changes.config.previousValue && !changes.config.firstChange) { await this.updateAsset(); } } } previewMapInit(leaflet) { this.leaflet = leaflet; this.addCenterIcon(this.formConfig.center[0], this.formConfig.center[1]); } onBeforeSave() { this.config.mapConfig = this.formConfig; return true; } zoomLevelChanged() { this.config.mapConfig = { ...this.config.mapConfig, zoomLevel: this.formConfig.zoomLevel }; } changeCenter() { if (this.centerIcon) { this.centerIcon.remove(); } this.config.mapConfig = { ...this.formConfig, center: [...this.formConfig.center] }; this.addCenterIcon(this.formConfig.center[0], this.formConfig.center[1]); } changeCenterOnEnterKey(event) { event.preventDefault(); this.changeCenter(); } onPreviewZoomStart() { this.centerIcon.remove(); } onPreviewZoomEnd(event) { this.formConfig.zoomLevel = Math.floor(event.target.getZoom()); } onPreviewMapMove(event) { if (this.centerIcon) { this.centerIcon.remove(); } const { lat, lng } = event.target.getCenter(); const fixedLat = +lat.toFixed(this.FIXED_POINT_DIGITS); const fixedLng = +lng.toFixed(this.FIXED_POINT_DIGITS); this.addCenterIcon(fixedLat, fixedLng); this.formConfig.center = [fixedLat, fixedLng]; } useOwnPosition() { navigator.geolocation.getCurrentPosition(({ coords }) => { this.formConfig.center = [coords.latitude, coords.longitude]; this.changeCenter(); }); } centerToAsset() { this.formConfig.center = [ this.config.device.c8y_Position.lat, this.config.device.c8y_Position.lng ]; this.changeCenter(); } async fitToBounds() { const assetsList = this.assets(); if (!assetsList?.length || !this.previewMap?.map) { return; } const bounds = await this.mapService.getAssetsBounds(assetsList); // map.fitBounds causes zoom and move events, so get new center and zoom level only when these events are finished fromEvent(this.previewMap.map, 'moveend') .pipe(combineLatestWith(fromEvent(this.previewMap.map, 'zoomend')), take(1)) .subscribe(() => { const center = this.previewMap.map.getCenter(); const zoom = this.previewMap.map.getZoom(); const fixedLat = +center.lat.toFixed(this.FIXED_POINT_DIGITS); const fixedLng = +center.lng.toFixed(this.FIXED_POINT_DIGITS); this.formConfig.center = [fixedLat, fixedLng]; this.formConfig.zoomLevel = Math.floor(zoom); this.changeCenter(); }); this.previewMap.map.fitBounds(bounds, defaultFitBoundsOptions); } selectIcon(icon) { this.formConfig.icon = icon; this.config.mapConfig.icon = icon; this.previewMap.refreshMarkers(); } async updateAsset() { const { data, paging } = await this.mapService.getAllPositionMOs(this.device()); const onlyAssetsWithLatLng = data.filter(asset => !isUndefined(asset.c8y_Position?.lat) && !isUndefined(asset.c8y_Position?.lng)); this.assets.set(onlyAssetsWithLatLng); if (paging.totalPages > 1) { this.alertService.danger(gettext('It might be possible that assets are not shown in the preview, as the current selection has more than 500 assets and the preview only supports a maximum of 500 assets.')); } } addCenterIcon(lat, lng) { const titleText = this.translateService.instant(gettext('Map center')); const icon = this.leaflet.divIcon({ html: `<i style="pointer-events: none" class="c8y-map-marker-icon text-muted dlt-c8y-icon-target icon-2x" title="${titleText}" />` }); this.centerIcon = this.leaflet.marker([lat, lng], { icon }); this.previewMap.addMarkerToMap(this.centerIcon); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: MapWidgetConfigComponent, deps: [{ token: i4.MapService }, { token: i2.AlertService }, { token: i3.TranslateService }, { token: MAP_DEFAULT_CONFIG }, { token: i4$1.WidgetConfigService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: MapWidgetConfigComponent, isStandalone: true, selector: "c8y-map-widget-config", inputs: { config: "config" }, viewQueries: [{ propertyName: "previewMap", first: true, predicate: MapComponent, descendants: true }, { propertyName: "previewMapSet", first: true, predicate: ["previewMap"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "@if (formConfig) {\n <fieldset class=\"c8y-fieldset\">\n <legend>{{ 'Map' | translate }}</legend>\n <div class=\"row tight-grid\">\n <div class=\"col-xs-3\">\n <c8y-form-group class=\"m-b-16 text-center form-group-sm\">\n <label translate>Marker icon</label>\n <c8y-icon-selector-wrapper\n [canRemoveIcon]=\"true\"\n [selectedIcon]=\"formConfig.icon\"\n (onSelect)=\"selectIcon($event)\"\n ></c8y-icon-selector-wrapper>\n </c8y-form-group>\n </div>\n <div class=\"col-xs-9\">\n <div class=\"form-group form-group-sm\">\n <label translate>Zoom level</label>\n <c8y-range\n class=\"label-bottom\"\n name=\"zoomLevel\"\n #range\n [(ngModel)]=\"formConfig.zoomLevel\"\n (change)=\"zoomLevelChanged()\"\n >\n <input\n type=\"range\"\n min=\"0\"\n max=\"18\"\n step=\"1\"\n />\n </c8y-range>\n </div>\n </div>\n </div>\n <c8y-form-group class=\"form-group-sm\">\n <label translate>Center bound</label>\n <div class=\"input-group input-group-sm\">\n <input\n class=\"form-control\"\n placeholder=\"{{ 'lat.`latitude`' | translate }}\"\n name=\"centerLat\"\n type=\"number\"\n required\n [(ngModel)]=\"formConfig.center[0]\"\n (change)=\"changeCenter()\"\n (keydown.enter)=\"changeCenterOnEnterKey($event)\"\n min=\"-90\"\n max=\"90\"\n step=\"0.1\"\n />\n <input\n class=\"form-control\"\n placeholder=\"{{ 'lng.`longitude`' | translate }}\"\n name=\"centerLng\"\n type=\"number\"\n required\n min=\"-180\"\n max=\"180\"\n [(ngModel)]=\"formConfig.center[1]\"\n (change)=\"changeCenter()\"\n (keydown.enter)=\"changeCenterOnEnterKey($event)\"\n min=\"-180\"\n max=\"180\"\n step=\"0.1\"\n />\n <div class=\"input-group-btn\">\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Use your location' | translate\"\n [tooltip]=\"'Use your location' | translate\"\n placement=\"top\"\n container=\"body\"\n (click)=\"useOwnPosition()\"\n >\n <i\n class=\"icon-14\"\n c8yIcon=\"location-arrow\"\n ></i>\n </button>\n </div>\n <div class=\"input-group-btn\">\n @if (canAutoCenter()) {\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Use selected asset location' | translate\"\n [tooltip]=\"'Use selected asset location' | translate\"\n placement=\"top\"\n container=\"body\"\n (click)=\"centerToAsset()\"\n >\n <i\n class=\"icon-14\"\n c8yIcon=\"location\"\n ></i>\n </button>\n } @else {\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Fit to assets bounds' | translate\"\n [tooltip]=\"'Fit to assets bounds' | translate\"\n placement=\"top\"\n container=\"body\"\n (click)=\"fitToBounds()\"\n >\n <i\n class=\"icon-14\"\n c8yIcon=\"waypoint-map\"\n ></i>\n </button>\n }\n </div>\n </div>\n\n <c8y-messages\n [helpMessage]=\"'Drag the map to the desired position' | translate\"\n ></c8y-messages>\n </c8y-form-group>\n </fieldset>\n\n @if (isPositionedDevice()) {\n <fieldset class=\"c8y-fieldset\">\n <legend>{{ 'Follow' | translate }}</legend>\n <div class=\"row tight-grid d-flex a-i-center\">\n <div class=\"col-xs-12\">\n <div class=\"form-group form-group-sm m-b-16\">\n <label class=\"c8y-switch c8y-switch--inline\">\n <input\n name=\"follow\"\n type=\"checkbox\"\n [(ngModel)]=\"formConfig.follow\"\n />\n <span></span>\n <span\n class=\"text-12\"\n translate\n >\n Follow selected\n </span>\n </label>\n </div>\n </div>\n </div>\n </fieldset>\n }\n}\n\n<ng-template #previewMap>\n <div style=\"width: 100%; height: 100%\">\n @if (config.mapConfig) {\n <c8y-map\n [assets]=\"assets()\"\n [config]=\"config.mapConfig\"\n (onMove)=\"onPreviewMapMove($event)\"\n (onZoomStart)=\"onPreviewZoomStart()\"\n (onZoomEnd)=\"onPreviewZoomEnd($event)\"\n (onInit)=\"previewMapInit($event)\"\n ></c8y-map>\n }\n </div>\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i5.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i5.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i5.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i5.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i5.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i5.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i5.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i5.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: CommonModule$1 }, { kind: "directive", type: i2.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: i2.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "ngmodule", type: FormsModule$1 }, { kind: "directive", type: i2.MinValidationDirective, selector: "[min]", inputs: ["min"] }, { kind: "directive", type: i2.MaxValidationDirective, selector: "[max]", inputs: ["max"] }, { kind: "component", type: i2.FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "component", type: i2.MessagesComponent, selector: "c8y-messages", inputs: ["show", "defaults", "helpMessage"] }, { kind: "directive", type: i2.RequiredInputPlaceholderDirective, selector: "input[required], input[formControlName]" }, { kind: "directive", type: i2.RangeDirective, selector: "input[type=\"range\"]" }, { kind: "component", type: i2.RangeComponent, selector: "c8y-range", inputs: ["valueDisplayMode"] }, { kind: "ngmodule", type: IconSelectorModule }, { kind: "component", type: i6$1.IconSelectorWrapperComponent, selector: "c8y-icon-selector-wrapper", inputs: ["canRemoveIcon", "selectedIcon", "iconSize"], outputs: ["onSelect"] }, { kind: "ngmodule", type: MapModule }, { kind: "component", type: i4.MapComponent, selector: "c8y-map", inputs: ["config", "assets", "polyline$", "polylineOptions"], outputs: ["onRealtimeUpdate", "onMove", "onMoveEnd", "onZoomStart", "onZoomEnd", "onMap", "onInit"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i7.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: "ngmodule", type: ReactiveFormsModule }, { kind: "ngmodule", type: PopoverModule }, { kind: "pipe", type: i2.C8yTranslatePipe, name: "translate" }], viewProviders: [{ provide: ControlContainer, useExisting: NgForm }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: MapWidgetConfigComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-map-widget-config', viewProviders: [{ provide: ControlContainer, useExisting: NgForm }], standalone: true, imports: [ CommonModule, FormsModule, CommonModule$1, FormsModule$1, IconSelectorModule, MapModule, TooltipModule, ReactiveFormsModule, PopoverModule, C8yTranslatePipe ], template: "@if (formConfig) {\n <fieldset class=\"c8y-fieldset\">\n <legend>{{ 'Map' | translate }}</legend>\n <div class=\"row tight-grid\">\n <div class=\"col-xs-3\">\n <c8y-form-group class=\"m-b-16 text-center form-group-sm\">\n <label translate>Marker icon</label>\n <c8y-icon-selector-wrapper\n [canRemoveIcon]=\"true\"\n [selectedIcon]=\"formConfig.icon\"\n (onSelect)=\"selectIcon($event)\"\n ></c8y-icon-selector-wrapper>\n </c8y-form-group>\n </div>\n <div class=\"col-xs-9\">\n <div class=\"form-group form-group-sm\">\n <label translate>Zoom level</label>\n <c8y-range\n class=\"label-bottom\"\n name=\"zoomLevel\"\n #range\n [(ngModel)]=\"formConfig.zoomLevel\"\n (change)=\"zoomLevelChanged()\"\n >\n <input\n type=\"range\"\n min=\"0\"\n max=\"18\"\n step=\"1\"\n />\n </c8y-range>\n </div>\n </div>\n </div>\n <c8y-form-group class=\"form-group-sm\">\n <label translate>Center bound</label>\n <div class=\"input-group input-group-sm\">\n <input\n class=\"form-control\"\n placeholder=\"{{ 'lat.`latitude`' | translate }}\"\n name=\"centerLat\"\n type=\"number\"\n required\n [(ngModel)]=\"formConfig.center[0]\"\n (change)=\"changeCenter()\"\n (keydown.enter)=\"changeCenterOnEnterKey($event)\"\n min=\"-90\"\n max=\"90\"\n step=\"0.1\"\n />\n <input\n class=\"form-control\"\n placeholder=\"{{ 'lng.`longitude`' | translate }}\"\n name=\"centerLng\"\n type=\"number\"\n required\n min=\"-180\"\n max=\"180\"\n [(ngModel)]=\"formConfig.center[1]\"\n (change)=\"changeCenter()\"\n (keydown.enter)=\"changeCenterOnEnterKey($event)\"\n min=\"-180\"\n max=\"180\"\n step=\"0.1\"\n />\n <div class=\"input-group-btn\">\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Use your location' | translate\"\n [tooltip]=\"'Use your location' | translate\"\n placement=\"top\"\n container=\"body\"\n (click)=\"useOwnPosition()\"\n >\n <i\n class=\"icon-14\"\n c8yIcon=\"location-arrow\"\n ></i>\n </button>\n </div>\n <div class=\"input-group-btn\">\n @if (canAutoCenter()) {\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Use selected asset location' | translate\"\n [tooltip]=\"'Use selected asset location' | translate\"\n placement=\"top\"\n container=\"body\"\n (click)=\"centerToAsset()\"\n >\n <i\n class=\"icon-14\"\n c8yIcon=\"location\"\n ></i>\n </button>\n } @else {\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Fit to assets bounds' | translate\"\n [tooltip]=\"'Fit to assets bounds' | translate\"\n placement=\"top\"\n container=\"body\"\n (click)=\"fitToBounds()\"\n >\n <i\n class=\"icon-14\"\n c8yIcon=\"waypoint-map\"\n ></i>\n </button>\n }\n </div>\n </div>\n\n <c8y-messages\n [helpMessage]=\"'Drag the map to the desired position' | translate\"\n ></c8y-messages>\n </c8y-form-group>\n </fieldset>\n\n @if (isPositionedDevice()) {\n <fieldset class=\"c8y-fieldset\">\n <legend>{{ 'Follow' | translate }}</legend>\n <div class=\"row tight-grid d-flex a-i-center\">\n <div class=\"col-xs-12\">\n <div class=\"form-group form-group-sm m-b-16\">\n <label class=\"c8y-switch c8y-switch--inline\">\n <input\n name=\"follow\"\n type=\"checkbox\"\n [(ngModel)]=\"formConfig.follow\"\n />\n <span></span>\n <span\n class=\"text-12\"\n translate\n >\n Follow selected\n </span>\n </label>\n </div>\n </div>\n </div>\n </fieldset>\n }\n}\n\n<ng-template #previewMap>\n <div style=\"width: 100%; height: 100%\">\n @if (config.mapConfig) {\n <c8y-map\n [assets]=\"assets()\"\n [config]=\"config.mapConfig\"\n (onMove)=\"onPreviewMapMove($event)\"\n (onZoomStart)=\"onPreviewZoomStart()\"\n (onZoomEnd)=\"onPreviewZoomEnd($event)\"\n (onInit)=\"previewMapInit($event)\"\n ></c8y-map>\n }\n </div>\n</ng-template>\n" }] }], ctorParameters: () => [{ type: i4.MapService }, { type: i2.AlertService }, { type: i3.TranslateService }, { type: i8.Observable, decorators: [{ type: Inject, args: [MAP_DEFAULT_CONFIG] }] }, { type: i4$1.WidgetConfigService }], propDecorators: { config: [{ type: Input }], previewMap: [{ type: ViewChild, args: [MapComponent] }], previewMapSet: [{ type: ViewChild, args: ['previewMap'] }] } }); class MapWidgetComponent { constructor(dashboardChild, dashboardContextComponent, inventory, mapService, widgetsDashboardComponent, dynamicComponentService, widgetConfigMigrationService) { this.dashboardContextComponent = dashboardContextComponent; this.inventory = inventory; this.mapService = mapService; this.widgetsDashboardComponent = widgetsDashboardComponent; this.dynamicComponentService = dynamicComponentService; this.widgetConfigMigrationService = widgetConfigMigrationService; this.mapConfig = { center: [0, 0] }; this.TIMEOUT_ERROR_TEXT = gettext('The request is taking longer than usual. We apologize for the inconvenience.'); this.SERVER_ERROR_TEXT = gettext('Server error occurred.'); this.destroy$ = new Subject(); this.listenToWidgetResizeEvent(dashboardChild); } async ngOnInit() { this.alerts.setAlertGroupDismissStrategy('warning', DismissAlertStrategy.TEMPORARY_OR_PERMANENT); await this.applyDeviceTypeTarget(); if (this.config.device) { this.rootNode = this.config.device; } this.mapConfig = { ...this.config.mapConfig }; // Check for legacy widget BEFORE updateMapConfigRealtime() overwrites it const isLegacyWidgetWithRealtime = !this.config.hasOwnProperty('displayMode') && this.mapConfig.realtime; this.updateMapConfigRealtime(); this.savedNode = this.rootNode; await this.updateAssets(); const migratedConfig = this.widgetConfigMigrationService.migrateWidgetConfig(this.config); this.config = merge(this.config, migratedConfig); // For legacy widgets (with realtime), set displayMode to 'view_and_config' to maintain previous behavior to some degree // TODO: adjust widgetConfigMigrationService.migrateWidgetConfig or do nothing. // Map widget is a special case since it blocks auto-refresh in the config live mode. if (isLegacyWidgetWithRealtime) { this.config.displayMode = GLOBAL_CONTEXT_DISPLAY_MODE.VIEW_AND_CONFIG; } if (this.assets.length > 1) { this.mapConfig = { ...this.mapConfig, follow: false }; } this.controls = (await this.dynamicComponentService.getById(defaultWidgetIds.MAP)).data?.widgetControls || {}; } ngAfterViewInit() { this.subscribeToErrorsOccurred(); if (this.widgetsDashboardComponent) { const resizeCallback = debounce(() => { // initial size of map is assigned when left drawer is closed, but if it is opened after initialization // the map size is not updated, so we need to call invalidateSize so that map use the correct size this.clusterMap.map?.invalidateSize(false); }, 100); this.resizeObserver = new ResizeObserver(resizeCallback); this.resizeObserver.observe(this.widgetsDashboardComponent.nativeElement); } } startFollow(context) { if (context.id !== this.rootNode?.id) { this.rootNode = context; } this.mapConfig = { ...this.mapConfig, follow: true }; } stopFollow() { this.mapConfig = { ...this.mapConfig, follow: false }; this.rootNode = this.savedNode; } ngOnDestroy() { if (this.resizeObserver) { this.resizeObserver.disconnect(); } this.destroy$.next(); } async fitToBounds() { await this.updateAssets(); if (this.assets.length === 0 || !this.clusterMap?.map) { return; } const bounds = await this.mapService.getAssetsBounds(this.assets); this.clusterMap.map.fitBounds(bounds, defaultFitBoundsOptions); } async onGlobalContext(event) { const { context } = event; const isRealtime = this.mapConfig.realtime; if (!isRealtime && context.isAutoRefreshEnabled) { // If follow is enabled, fetch latest position and center map before reload if (this.mapConfig.follow && this.config.device?.c8y_Position) { await this.updateAssets(); if (this.assets?.length === 1) { // Update clusterMap's asset so marker can be rendered even if cluster query returns 0 positions this.clusterMap.assets = this.assets[0]; this.clusterMap?.moveToPositionOfMo(this.assets[0]); // Prevent cluster subscription from re-centering to same position const pos = this.assets[0].c8y_Position; this.clusterMap.lastFollowedPosition = { lat: pos.lat, lng: pos.lng }; } } this.clusterMap?.reload(); } } async updateAssets() { this.assets = (await this.mapService.getAllPositionMOs(this.config.device)).data; } listenToWidgetResizeEvent(dashboardChild) { dashboardChild.changeEnd .pipe(filter(child => child.lastChange === 'resize'), takeUntil(this.destroy$)) .subscribe(() => { this.clusterMap.reset(); }); } subscribeToErrorsOccurred() { this.clusterMap.errorNotifier .pipe(filter(Boolean), takeUntil(this.destroy$)) .subscribe(error => { this.alerts.addAlerts(new DynamicComponentAlert({ type: 'warning', text: error?.name === 'TimeoutError' ? this.TIMEOUT_ERROR_TEXT : (error?.message ?? this.SERVER_ERROR_TEXT) })); }); } updateMapConfigRealtime() { this.mapConfig.realtime = this.config.realtime || false; } async applyDeviceTypeTarget() { const hasDeviceTarget = !!this.config.device; // The map widget supports not selecting any target const isDeviceTypeDashboard = !!this.dashboardContextComponent?.dashboard?.deviceType; if (isDeviceTypeDashboard && hasDeviceTarget) { const context = this.dashboardContextComponent.context; if (context?.id) { const { id } = context; this.config.device = (await this.inventory.detail(id)).data; } } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: MapWidgetComponent, deps: [{ token: i2.DashboardChildComponent }, { token: i4$1.ContextDashboardComponent, optional: true }, { token: i1.InventoryService }, { token: i4.MapService }, { token: i2.WidgetsDashboardComponent, optional: true }, { token: i2.DynamicComponentService }, { token: i5$1.WidgetConfigMigrationService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: MapWidgetComponent, isStandalone: true, selector: "c8y-map-widget", inputs: { config: "config" }, viewQueries: [{ propertyName: "clusterMap", first: true, predicate: ClusterMapComponent, descendants: true }], ngImport: i0, template: "@if (mapConfig) {\n <c8y-map-status\n [clusterMap]=\"mapWidget\"\n [(config)]=\"mapConfig\"\n (onUnfollow)=\"stopFollow()\"\n (fitAssetsToBounds)=\"fitToBounds()\"\n [assets]=\"assets\"\n [buttonsConfig]=\"{\n autoRefresh: { show: false },\n refresh: { show: false },\n realtime: { show: false },\n fitToBounds: { show: true }\n }\"\n >\n <c8y-global-context-widget-wrapper\n *ngIf=\"controls\"\n [widgetControls]=\"controls\"\n [config]=\"config\"\n [isLoading]=\"mapWidget.isLoading$ | async\"\n (globalContextChange)=\"onGlobalContext($event)\"\n ></c8y-global-context-widget-wrapper>\n </c8y-map-status>\n\n <c8y-cluster-map\n #mapWidget\n [rootNode]=\"rootNode\"\n [config]=\"mapConfig\"\n [asset]=\"assets\"\n >\n <div\n class=\"map-marker\"\n *c8yMapPopup=\"let context\"\n >\n <a\n class=\"text-truncate deviceLink text-12\"\n routerLink=\"{{ context | assetLink }}\"\n >\n <strong>{{ context.name }}</strong>\n </a>\n <c8y-map-event-info [asset]=\"context\">\n <button\n class=\"btn btn-default btn-xs btn-block m-t-8\"\n [title]=\"'Activate realtime on this asset and follow it if it moves' | translate\"\n type=\"button\"\n (click)=\"startFollow(context)\"\n *ngIf=\"!mapConfig.follow\"\n translate\n >\n Follow\n </button>\n <button\n class=\"btn btn-default btn-xs btn-block m-t-8\"\n [title]=\"'Stop following this asset.' | translate\"\n type=\"button\"\n (click)=\"stopFollow()\"\n *ngIf=\"mapConfig.follow\"\n translate\n >\n Unfollow\n </button>\n </c8y-map-event-info>\n </div>\n </c8y-cluster-map>\n}\n", dependencies: [{ kind: "component", type: MapEventInfoComponent, selector: "c8y-map-event-info", inputs: ["asset"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i6.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: CommonModule$1 }, { kind: "directive", type: i2.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "ngmodule", type: FormsModule$1 }, { kind: "ngmodule", type: IconSelectorModule }, { kind: "ngmodule", type: MapModule }, { kind: "component", type: i4.MapStatusComponent, selector: "c8y-map-status", inputs: ["config", "assets", "clusterMap", "buttonsConfig"], outputs: ["configChange", "onUnfollow", "fitAssetsToBounds"] }, { kind: "component", type: i4.ClusterMapComponent, selector: "c8y-cluster-map", inputs: ["config", "rootNode", "asset", "showClusterColor"], outputs: ["mapChange"] }, { kind: "directive", type: i4.MapPopupDirective, selector: "[c8yMapPopup]" }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: GlobalContextWidgetWrapperComponent, selector: "c8y-global-context-widget-wrapper", inputs: ["isLoading", "displayMode", "widgetControls", "controlLinks", "dashboardChildForLegacy", "config"], outputs: ["globalContextChange"] }, { kind: "pipe", type: i6.AsyncPipe, name: "async" }, { kind: "pipe", type: i2.C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: i2.AssetLinkPipe, name: "assetLink" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: MapWidgetComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-map-widget', standalone: true, imports: [ MapEventInfoComponent, CommonModule, FormsModule, CommonModule$1, FormsModule$1, IconSelectorModule, MapModule, AssetLinkPipe, TooltipModule, RouterLink, GlobalContextWidgetWrapperComponent ], template: "@if (mapConfig) {\n <c8y-map-status\n [clusterMap]=\"mapWidget\"\n [(config)]=\"mapConfig\"\n (onUnfollow)=\"stopFollow()\"\n (fitAssetsToBounds)=\"fitToBounds()\"\n [assets]=\"assets\"\n [buttonsConfig]=\"{\n autoRefresh: { show: false },\n refresh: { show: false },\n realtime: { show: false },\n fitToBounds: { show: true }\n }\"\n >\n <c8y-global-context-widget-wrapper\n *ngIf=\"controls\"\n [widgetControls]=\"controls\"\n [config]=\"config\"\n [isLoading]=\"mapWidget.isLoading$ | async\"\n (globalContextChange)=\"onGlobalContext($event)\"\n ></c8y-global-context-widget-wrapper>\n </c8y-map-status>\n\n <c8y-cluster-map\n #mapWidget\n [rootNode]=\"rootNode\"\n [config]=\"mapConfig\"\n [asset]=\"assets\"\n >\n <div\n class=\"map-marker\"\n *c8yMapPopup=\"let context\"\n >\n <a\n class=\"text-truncate deviceLink text-12\"\n routerLink=\"{{ context | assetLink }}\"\n >\n <strong>{{ context.name }}</strong>\n </a>\n <c8y-map-event-info [asset]=\"context\">\n <button\n class=\"btn btn-default btn-xs btn-block m-t-8\"\n [title]=\"'Activate realtime on this asset and follow it if it moves' | translate\"\n type=\"button\"\n (click)=\"startFollow(context)\"\n *ngIf=\"!mapConfig.follow\"\n translate\n >\n Follow\n </button>\n <button\n class=\"btn btn-default btn-xs btn-block m-t-8\"\n [title]=\"'Stop following this asset.' | translate\"\n type=\"button\"\n (click)=\"stopFollow()\"\n *ngIf=\"mapConfig.follow\"\n translate\n >\n Unfollow\n </button>\n </c8y-map-event-info>\n </div>\n </c8y-cluster-map>\n}\n" }] }], ctorParameters: () => [{ type: i2.DashboardChildComponent }, { type: i4$1.ContextDashboardComponent, decorators: [{ type: Optional }] }, { type: i1.InventoryService }, { type: i4.MapService }, { type: i2.WidgetsDashboardComponent, decorators: [{ type: Optional }] }, { type: i2.DynamicComponentService }, { type: i5$1.WidgetConfigMigrationService }], propDecorators: { config: [{ type: Input }], clusterMap: [{ type: ViewChild, args: [ClusterMapComponent] }] } }); /** * Generated bundle index. Do not edit. */ export { MapEventInfoComponent, MapWidgetComponent, MapWidgetConfigComponent }; //# sourceMappingURL=c8y-ngx-components-widgets-implementations-map.mjs.map