@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
443 lines (437 loc) • 42.7 kB
JavaScript
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