UNPKG

hslayers-ng

Version:
605 lines (596 loc) 214 kB
import * as i0 from '@angular/core'; import { inject, DestroyRef, Component, Pipe, ViewChild, Input, Injectable, input, computed, ViewChildren, ChangeDetectionStrategy, signal, model, viewChild, ViewContainerRef, CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { BehaviorSubject, filter, merge, Subject, map, fromEvent, debounceTime, tap, startWith, timer, throwError, of } from 'rxjs'; import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop'; import { HsDimensionDescriptor } from 'hslayers-ng/common/dimensions'; import { HsDimensionService, HsDimensionTimeService, HsWmsGetCapabilitiesService } from 'hslayers-ng/services/get-capabilities'; import { HsEventBusService } from 'hslayers-ng/services/event-bus'; import { HsLayerSelectorService, HsLayerManagerVisibilityService, HsLayerEditorVectorLayerService, HsLayerManagerMetadataService, HsLayerManagerService, HsLayerManagerFolderService, HsLayerManagerLoadingProgressService, HsLayerManagerUtilsService, HsLayerManagerCopyLayerService } from 'hslayers-ng/services/layer-manager'; import { HsMapService } from 'hslayers-ng/services/map'; import { isFunction, getLayerParams, isLayerArcgis, updateLayerParams, isLayerWMS, getURL, isLayerVectorLayer, instOf, listNumericAttributes, isLayerIDW, calculateResolutionFromScale, layerIsZoomable, layerIsStyleable, isLayerQueryable, layerInvalid } from 'hslayers-ng/services/utils'; import { getDimensions, getCachedCapabilities, getQueryCapabilities, getWmsOriginalExtent, setCluster, getCluster, getInlineLegend, setWmsOriginalExtent, getPath, getBase, setPath, setBase, getAttribution, setAbstract, getAbstract, getGreyscale, getRemovable, getWfsUrl, getWorkspace, getTitle, setTitle, getExclusive, getHsLaymanSynchronizing, getShowInLayerManager, getActive, getThumbnail } from 'hslayers-ng/common/extensions'; import * as i1 from '@angular/forms'; import { FormsModule, FormControl, Validators, ReactiveFormsModule } from '@angular/forms'; import * as i2 from '@ng-bootstrap/ng-bootstrap'; import { NgbTooltipModule, NgbDropdownModule, NgbProgressbarModule, NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; import * as i2$1 from '@ngx-translate/core'; import { TranslatePipe } from '@ngx-translate/core'; import * as i1$1 from '@angular/common'; import { CommonModule, NgClass, AsyncPipe } from '@angular/common'; import { HsConfig } from 'hslayers-ng/config'; import { HsLayoutService } from 'hslayers-ng/services/layout'; export * from 'hslayers-ng/types'; import { HsConfirmDialogComponent } from 'hslayers-ng/common/confirm'; import { HsDialogContainerService } from 'hslayers-ng/common/dialogs'; import * as i2$3 from 'hslayers-ng/common/panels'; import { HsPanelHelpersModule, HsGuiOverlayBaseComponent, HsPanelBaseComponent, HsPanelHeaderComponent } from 'hslayers-ng/common/panels'; import { HsStylerService } from 'hslayers-ng/services/styler'; import { WMSCapabilities } from 'ol/format'; import { transformExtent, METERS_PER_UNIT } from 'ol/proj'; import { HsShareUrlService, HS_PRMS } from 'hslayers-ng/services/share'; import * as i1$2 from 'hslayers-ng/components/legend'; import { HsLegendService, HsLegendModule } from 'hslayers-ng/components/legend'; import { HsPanelContainerService } from 'hslayers-ng/services/panels'; import { HsDrawService } from 'hslayers-ng/services/draw'; import colorScales from 'colormap/colorScale'; import { HsLanguageService } from 'hslayers-ng/services/language'; import { InterpolatedSource } from 'hslayers-ng/common/layers'; import * as i2$2 from 'hslayers-ng/common/color-map-picker'; import { HsColormapPickerModule } from 'hslayers-ng/common/color-map-picker'; import { OSM, ImageWMS, TileWMS } from 'ol/source'; import * as i1$3 from 'hslayers-ng/common/clipboard-text'; import { HsClipboardTextComponent } from 'hslayers-ng/common/clipboard-text'; import { Image, Tile } from 'ol/layer'; import { HsLayerShiftingService } from 'hslayers-ng/services/layer-shifting'; import { startWith as startWith$1, combineLatestWith, filter as filter$1, map as map$1, debounceTime as debounceTime$1, take, switchMap, debounce, distinctUntilChanged, share, retry, catchError } from 'rxjs/operators'; import * as i2$4 from '@angular/cdk/drag-drop'; import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop'; import { HsCommonLaymanService } from 'hslayers-ng/common/layman'; import { HsCompositionsParserService } from 'hslayers-ng/services/compositions'; import { HsRemoveLayerDialogService } from 'hslayers-ng/common/remove-multiple'; import { HslayersService } from 'hslayers-ng/core'; import { FilterPipe } from 'hslayers-ng/common/pipes'; class HsLayerEditorWidgetBaseComponent { constructor() { this.hsLayerSelectorService = inject(HsLayerSelectorService); this.layerDescriptor = new BehaviorSubject(null); this.isVisible$ = new BehaviorSubject(true); this.baseComponentInitRun = false; this.destroyRef = inject(DestroyRef); this.layerDescriptor.subscribe((descriptor) => { this.olLayer = descriptor?.layer; }); setTimeout(() => { if (!this.baseComponentInitRun) { console.warn(`${this.name || this.constructor.name} implements ngOnInit lifecycle hook without calling HsLayerEditorWidgetBaseComponent ngOnInit. Make sure it is executed by calling super.ngOnInit() from component's ngOnInit manually`); } }, 3000); } ngOnInit() { this.baseComponentInitRun = true; this.layerDescriptor.next(this.hsLayerSelectorService.currentLayer); this.hsLayerSelectorService.layerSelected .pipe(filter((layer) => !!layer), takeUntilDestroyed(this.destroyRef)) .subscribe((layer) => { this.layerDescriptor.next(layer); }); } isVisible() { return true; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsLayerEditorWidgetBaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.2.3", type: HsLayerEditorWidgetBaseComponent, isStandalone: false, selector: "ng-component", ngImport: i0, template: '<div></div>', isInline: true }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsLayerEditorWidgetBaseComponent, decorators: [{ type: Component, args: [{ template: '<div></div>', standalone: false, }] }], ctorParameters: () => [] }); class HsLayerEditorDimensionsComponent extends HsLayerEditorWidgetBaseComponent { constructor() { super(); this.hsDimensionService = inject(HsDimensionService); this.hsDimensionTimeService = inject(HsDimensionTimeService); this.hsMapService = inject(HsMapService); this.hsEventBusService = inject(HsEventBusService); this.name = 'dimensions'; this.dimensions = []; merge(this.hsEventBusService.layerDimensionDefinitionChanges.pipe(filter((layer) => layer === this.olLayer)), this.layerDescriptor) .pipe(filter((data) => !!data), takeUntilDestroyed()) .subscribe(() => { this.updateDimensions(); }); } updateDimensions() { const layer = this.olLayer; if (!layer) { return; } this.dimensions = []; const dimensions = getDimensions(layer); if (dimensions && Object.entries(dimensions)) { for (const [key, dimension] of Object.entries(dimensions)) { let available = true; if (isFunction(dimension.availability)) { available = dimension.availability(layer); } if (available) { if (typeof dimension.values === 'string') { dimension.values = this.hsDimensionTimeService.parseTimePoints(dimension.values); } this.dimensions.push(new HsDimensionDescriptor(key, dimension)); } } } } dimensionIsTime(dimension) { const dimensions = getDimensions(this.olLayer); const type = Object.keys(dimensions).find((key) => dimensions[key] === dimension); // value of time.onlyInEditor used inversely here intentionally // ( => replacement for inline time-editor) return type === 'time' && dimensions.time?.onlyInEditor; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsLayerEditorDimensionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.3", type: HsLayerEditorDimensionsComponent, isStandalone: false, selector: "hs-layer-editor-dimensions", usesInheritance: true, ngImport: i0, template: "@if (hsDimensionService.isLayerWithDimensions(olLayer)) {\n <div>\n <!-- TODO: Remove function call from template -->\n <p style=\"text-align: center; font-weight: bold\">\n {{'LAYERMANAGER.dimensions' | translate }}\n </p>\n @for (dimension of dimensions; track $index) {\n <div class=\"form-inline\">\n <label class=\"control-label\" style=\"width: 100%; justify-content: left\">{{dimension.label ||\n dimension.name}}:</label>\n @if (!dimension.type) {\n <select class=\"form-control form-select\" style=\"width: 100%\" [(ngModel)]=\"dimension.modelValue\"\n [ngModelOptions]=\"{standalone: true}\"\n (change)=\"hsDimensionService.dimensionChanged(dimension)\">\n @for (dimension_value of dimension.values; track dimension_value) {\n <option [ngValue]=\"dimension_value\">\n {{dimension_value}}\n </option>\n }\n </select>\n }\n <!-- Time slider replaced by layermanger-time-editor component -->\n <div class=\"form-group\" [hidden]=\"!dimensionIsTime(dimension)\">\n <label>{{'COMMON.date' | translate }}</label>\n </div>\n @if (dimension.type === 'datetime' || dimension.type === 'date') {\n <div class=\"input-group flex-fill\">\n <input type=\"text\" style=\"width: 6.1em\" ngbDatepicker placeholder=\"YYYY-MM-DD\" #d=\"ngbDatepicker\"\n [(ngModel)]=\"dimension.modelValue\" (dateSelect)=\"hsDimensionService.dimensionChanged(dimension)\"\n [ngModelOptions]=\"{standalone: true}\" />\n <button class=\"btn btn-outline-secondary\" (click)=\"d.toggle()\" type=\"button\">\n <i class=\"fa-solid fa-calendar\"></i>\n </button>\n </div>\n }\n @if (dimension.type === 'datetime') {\n <ngb-timepicker size=\"small\" [(ngModel)]=\"dimension.modelTimeValue\"\n (ngModelChange)=\"hsDimensionService.dimensionChanged(dimension)\" [ngModelOptions]=\"{standalone: true}\">\n </ngb-timepicker>\n }\n </div>\n }\n </div>\n}\n", dependencies: [{ kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.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: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgbInputDatepicker, selector: "input[ngbDatepicker]", inputs: ["autoClose", "contentTemplate", "datepickerClass", "dayTemplate", "dayTemplateData", "displayMonths", "firstDayOfWeek", "footerTemplate", "markDisabled", "minDate", "maxDate", "navigation", "outsideDays", "placement", "popperOptions", "restoreFocus", "showWeekNumbers", "startDate", "container", "positionTarget", "weekdays", "disabled"], outputs: ["dateSelect", "navigate", "closed"], exportAs: ["ngbDatepicker"] }, { kind: "pipe", type: i2$1.TranslatePipe, name: "translate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsLayerEditorDimensionsComponent, decorators: [{ type: Component, args: [{ selector: 'hs-layer-editor-dimensions', standalone: false, template: "@if (hsDimensionService.isLayerWithDimensions(olLayer)) {\n <div>\n <!-- TODO: Remove function call from template -->\n <p style=\"text-align: center; font-weight: bold\">\n {{'LAYERMANAGER.dimensions' | translate }}\n </p>\n @for (dimension of dimensions; track $index) {\n <div class=\"form-inline\">\n <label class=\"control-label\" style=\"width: 100%; justify-content: left\">{{dimension.label ||\n dimension.name}}:</label>\n @if (!dimension.type) {\n <select class=\"form-control form-select\" style=\"width: 100%\" [(ngModel)]=\"dimension.modelValue\"\n [ngModelOptions]=\"{standalone: true}\"\n (change)=\"hsDimensionService.dimensionChanged(dimension)\">\n @for (dimension_value of dimension.values; track dimension_value) {\n <option [ngValue]=\"dimension_value\">\n {{dimension_value}}\n </option>\n }\n </select>\n }\n <!-- Time slider replaced by layermanger-time-editor component -->\n <div class=\"form-group\" [hidden]=\"!dimensionIsTime(dimension)\">\n <label>{{'COMMON.date' | translate }}</label>\n </div>\n @if (dimension.type === 'datetime' || dimension.type === 'date') {\n <div class=\"input-group flex-fill\">\n <input type=\"text\" style=\"width: 6.1em\" ngbDatepicker placeholder=\"YYYY-MM-DD\" #d=\"ngbDatepicker\"\n [(ngModel)]=\"dimension.modelValue\" (dateSelect)=\"hsDimensionService.dimensionChanged(dimension)\"\n [ngModelOptions]=\"{standalone: true}\" />\n <button class=\"btn btn-outline-secondary\" (click)=\"d.toggle()\" type=\"button\">\n <i class=\"fa-solid fa-calendar\"></i>\n </button>\n </div>\n }\n @if (dimension.type === 'datetime') {\n <ngb-timepicker size=\"small\" [(ngModel)]=\"dimension.modelTimeValue\"\n (ngModelChange)=\"hsDimensionService.dimensionChanged(dimension)\" [ngModelOptions]=\"{standalone: true}\">\n </ngb-timepicker>\n }\n </div>\n }\n </div>\n}\n" }] }], ctorParameters: () => [] }); /** * Formats a shorthand date in a form YYYYMMDD into a full ISO form YYYY-MM-DD * If not used, the shorthand date is parsed as milliseconds in the DatePipe */ class DatePreformatPipe { transform(value) { if (!value.includes('-') && value.length === 8) { return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`; } return value; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: DatePreformatPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.2.3", ngImport: i0, type: DatePreformatPipe, isStandalone: true, name: "datePreformat" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: DatePreformatPipe, decorators: [{ type: Pipe, args: [{ name: 'datePreformat', standalone: true, }] }] }); class HsLayerManagerTimeEditorComponent { constructor() { this.hsEventBusService = inject(HsEventBusService); this.hsDimensionTimeService = inject(HsDimensionTimeService); this.hsLayoutService = inject(HsLayoutService); this.hsConfig = inject(HsConfig); this.availableTimesFetched = false; this.hasPreviousTime = false; this.hasFollowingTime = false; this.timeDisplayFormat = 'yyyy-MM-dd HH:mm:ss z'; this.hsDimensionTimeService.layerTimeChanges .pipe(takeUntilDestroyed()) .subscribe(({ layer: layerDescriptor, time }) => { if (this.layer.uid !== layerDescriptor.uid) { return; } if (!this.availableTimes) { this.fillAvailableTimes(layerDescriptor, time); } this.setCurrentTimeIfAvailable(time); }); this.hsEventBusService.layerTimeSynchronizations .pipe(takeUntilDestroyed()) .subscribe(({ sync, time }) => { this.timesInSync = sync; if (sync) { this.hideTimeSelect(); this.setCurrentTimeIfAvailable(time); if (this.currentTime) { this.setLayerTime(); } } }); } ngOnInit() { this.selectVisible = false; this.timesInSync = false; if (this.layer.time) { this.fillAvailableTimes(this.layer, undefined); } } /** * Sets hasPreviousTime and hasFollowingTime properties */ checkPrevFollowTimesAvailability() { if (!this.availableTimes) { return; } if (this.currentTimeIdx > 0) { this.hasPreviousTime = true; } else { this.hasPreviousTime = false; } if (this.currentTimeIdx < this.availableTimes.length - 1) { this.hasFollowingTime = true; } else { this.hasFollowingTime = false; } } /** * Handler for the "prev" button click */ previousTime() { if (!this.hasPreviousTime) { return; } this.currentTime = this.availableTimes[--this.currentTimeIdx]; if (this.timesInSync) { this.hsEventBusService.layerTimeSynchronizations.next({ sync: this.timesInSync, time: this.currentTime, }); } this.setLayerTime(); this.checkPrevFollowTimesAvailability(); } /** * Handler for the "next" button click */ followingTime() { if (!this.hasFollowingTime) { return; } this.currentTime = this.availableTimes[++this.currentTimeIdx]; if (this.timesInSync) { this.hsEventBusService.layerTimeSynchronizations.next({ sync: this.timesInSync, time: this.currentTime, }); } this.setLayerTime(); this.checkPrevFollowTimesAvailability(); } /** * Handler for a time selection from the SELECT element */ selectTime() { this.currentTimeIdx = this.availableTimes.indexOf(this.currentTime); if (this.timesInSync) { this.hsEventBusService.layerTimeSynchronizations.next({ sync: this.timesInSync, time: this.currentTime, }); } this.setLayerTime(); this.checkPrevFollowTimesAvailability(); } setCurrentTimeIfAvailable(time) { if (this.availableTimes.includes(time)) { this.currentTime = time; this.currentTimeIdx = this.availableTimes.indexOf(time); } else { this.currentTime = null; this.currentTimeIdx = -1; } this.checkPrevFollowTimesAvailability(); } /** * Set selected 'time' on the layer object */ setLayerTime() { setTimeout(() => { this.hsDimensionTimeService.setLayerTime(this.layer, this.currentTime); }, 100); } showTimeSelect() { this.selectVisible = true; this.selectElement.nativeElement.focus(); //FIXME: this just refuse to work... } hideTimeSelect() { this.selectVisible = false; } synchronizeTimes() { this.timesInSync = !this.timesInSync; this.hsEventBusService.layerTimeSynchronizations.next({ sync: this.timesInSync, time: this.currentTime, }); } /** * This gets called from subscriber and also OnInit because * subscriber could have been set up after the event was broadcasted */ fillAvailableTimes(layer, defaultTime) { this.availableTimes = layer.time.timePoints; this.setDateTimeFormatting(); this.setCurrentTimeIfAvailable(defaultTime ?? this.layer.time.default); if (!this.currentTime && this.availableTimes.length > 0) { this.currentTime = this.availableTimes[0]; this.currentTimeIdx = this.availableTimes.indexOf(this.currentTime); } this.availableTimesFetched = true; this.checkPrevFollowTimesAvailability(); } setDateTimeFormatting() { if (this.hsConfig.timeDisplayFormat) { this.timeDisplayFormat = this.hsConfig.timeDisplayFormat; } else if (this.availableTimes.every((time) => time.endsWith('00-00T00:00:00.000Z'))) { this.timeDisplayFormat = 'yyyy'; } else if (this.availableTimes.every((time) => time.endsWith('00T00:00:00.000Z'))) { this.timeDisplayFormat = 'yyyy-MM'; } else if (this.availableTimes.every((time) => time.endsWith('00:00:00.000Z'))) { this.timeDisplayFormat = 'yyyy-MM-dd'; } else if (this.availableTimes.every((time) => time.endsWith('00.000Z'))) { this.timeDisplayFormat = 'y-MM-dd HH:mm'; } else if (this.availableTimes.every((time) => time.endsWith('000Z'))) { this.timeDisplayFormat = 'y-MM-dd HH:mm:ss'; } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsLayerManagerTimeEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.3", type: HsLayerManagerTimeEditorComponent, isStandalone: true, selector: "hs-layer-manager-time-editor", inputs: { layer: "layer" }, viewQueries: [{ propertyName: "selectElement", first: true, predicate: ["hstimeselector"], descendants: true }], ngImport: i0, template: "<div class=\"p-0 flex-grow-1\">\n <button type=\"button\" class=\"btn btn-sm text-info border-0\" (click)=\"previousTime()\" [disabled]=\"!hasPreviousTime\">\n <i class=\"fa-solid fa-chevron-left\"></i>\n </button>\n @if (availableTimesFetched) {\n <button class=\"hs-lm-time-editor-currentTime btn btn-sm text-info px-0\"\n (click)=\"$event.preventDefault();showTimeSelect()\" [hidden]=\"selectVisible\"\n >\n @if (currentTime) {\n <span>\n {{currentTime | datePreformat | date:timeDisplayFormat:timeDisplayFormat}}\n </span>\n } @else {\n <span class=\"bg-warning text-dark\" data-toggle=\"tooltip\" data-container=\"body\" data-placement=\"auto\"\n [ngbTooltip]=\"'LAYERMANAGER.time.outOfRangeDescription' | translate \"\n [closeDelay]=\"hsConfig.layerTooltipDelay || 0\">\n {{'LAYERMANAGER.time.outOfRange' | translate }}\n </span>\n }\n </button>\n } @else {\n <div class=\"spinner-border spinner-border-sm text-info\" role=\"status\">\n <span class=\"visually-hidden\">{{'COMMON.loading' | translate }}</span>\n </div>&nbsp;<span class=\"text-muted\">{{'LAYERMANAGER.time.loading'| translate }}</span>\n }\n <select #hstimeselector [hidden]=\"!selectVisible\" [(ngModel)]=\"currentTime\" (change)=\"selectTime();hideTimeSelect();\"\n (blur)=\"hideTimeSelect()\">\n @for (time of availableTimes; track time) {\n <option [ngValue]=\"time\">\n {{time | datePreformat | date:timeDisplayFormat:timeDisplayFormat}}\n </option>\n }\n </select>\n <button type=\"button\" class=\"btn btn-sm text-info border-0\" (click)=\"followingTime()\" [disabled]=\"!hasFollowingTime\">\n <i class=\"fa-solid fa-chevron-right\"></i>\n </button>\n</div>\n<div class=\"p-0\">\n <button type=\"button\" class=\"hs-timeSync-toggle btn px-0 py-md-1 py-2 me-1\"\n [ngClass]=\"{'btn-info text-white': timesInSync, 'text-secondary': !timesInSync}\" (click)=\"synchronizeTimes()\"\n [title]=\"'LAYERMANAGER.time.syncTimesTooltip' | translate \">\n <i class=\"fa-solid fa-clock-rotate-left\"></i>\n <span class=\"visually-hidden\">{{'LAYERMANAGER.time.syncTimes' | translate }}</span>\n </button>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: NgbTooltipModule }, { kind: "directive", type: i2.NgbTooltip, selector: "[ngbTooltip]", inputs: ["animation", "autoClose", "placement", "popperOptions", "triggers", "positionTarget", "container", "disableTooltip", "tooltipClass", "tooltipContext", "openDelay", "closeDelay", "ngbTooltip"], outputs: ["shown", "hidden"], exportAs: ["ngbTooltip"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: i1$1.DatePipe, name: "date" }, { kind: "pipe", type: TranslatePipe, name: "translate" }, { kind: "pipe", type: DatePreformatPipe, name: "datePreformat" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsLayerManagerTimeEditorComponent, decorators: [{ type: Component, args: [{ selector: 'hs-layer-manager-time-editor', imports: [ CommonModule, TranslatePipe, NgClass, DatePreformatPipe, NgbTooltipModule, FormsModule, ], template: "<div class=\"p-0 flex-grow-1\">\n <button type=\"button\" class=\"btn btn-sm text-info border-0\" (click)=\"previousTime()\" [disabled]=\"!hasPreviousTime\">\n <i class=\"fa-solid fa-chevron-left\"></i>\n </button>\n @if (availableTimesFetched) {\n <button class=\"hs-lm-time-editor-currentTime btn btn-sm text-info px-0\"\n (click)=\"$event.preventDefault();showTimeSelect()\" [hidden]=\"selectVisible\"\n >\n @if (currentTime) {\n <span>\n {{currentTime | datePreformat | date:timeDisplayFormat:timeDisplayFormat}}\n </span>\n } @else {\n <span class=\"bg-warning text-dark\" data-toggle=\"tooltip\" data-container=\"body\" data-placement=\"auto\"\n [ngbTooltip]=\"'LAYERMANAGER.time.outOfRangeDescription' | translate \"\n [closeDelay]=\"hsConfig.layerTooltipDelay || 0\">\n {{'LAYERMANAGER.time.outOfRange' | translate }}\n </span>\n }\n </button>\n } @else {\n <div class=\"spinner-border spinner-border-sm text-info\" role=\"status\">\n <span class=\"visually-hidden\">{{'COMMON.loading' | translate }}</span>\n </div>&nbsp;<span class=\"text-muted\">{{'LAYERMANAGER.time.loading'| translate }}</span>\n }\n <select #hstimeselector [hidden]=\"!selectVisible\" [(ngModel)]=\"currentTime\" (change)=\"selectTime();hideTimeSelect();\"\n (blur)=\"hideTimeSelect()\">\n @for (time of availableTimes; track time) {\n <option [ngValue]=\"time\">\n {{time | datePreformat | date:timeDisplayFormat:timeDisplayFormat}}\n </option>\n }\n </select>\n <button type=\"button\" class=\"btn btn-sm text-info border-0\" (click)=\"followingTime()\" [disabled]=\"!hasFollowingTime\">\n <i class=\"fa-solid fa-chevron-right\"></i>\n </button>\n</div>\n<div class=\"p-0\">\n <button type=\"button\" class=\"hs-timeSync-toggle btn px-0 py-md-1 py-2 me-1\"\n [ngClass]=\"{'btn-info text-white': timesInSync, 'text-secondary': !timesInSync}\" (click)=\"synchronizeTimes()\"\n [title]=\"'LAYERMANAGER.time.syncTimesTooltip' | translate \">\n <i class=\"fa-solid fa-clock-rotate-left\"></i>\n <span class=\"visually-hidden\">{{'LAYERMANAGER.time.syncTimes' | translate }}</span>\n </button>\n</div>\n" }] }], ctorParameters: () => [], propDecorators: { layer: [{ type: Input }], selectElement: [{ type: ViewChild, args: ['hstimeselector'] }] } }); class HsCopyLayerDialogComponent { constructor() { this.hsDialogContainerService = inject(HsDialogContainerService); } yes() { this.hsDialogContainerService.destroy(this); this.dialogItem.resolve({ confirmed: 'yes', layerTitle: this.data.layerTitle, }); } no() { this.hsDialogContainerService.destroy(this); this.dialogItem.resolve('no'); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsCopyLayerDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.2.3", type: HsCopyLayerDialogComponent, isStandalone: false, selector: "hs-copy-layer-dialog", ngImport: i0, template: "<div class=\"modal in\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n <div class=\"modal-dialog\">\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <h4 class=\"modal-title\">\n {{data.title | translate }} ?\n </h4>\n <button type=\"button\" (click)=\"no()\" class=\"btn-close\" data-dismiss=\"modal\"\n [attr.aria-label]=\"'COMMON.close' | translate\"></button>\n </div>\n <div class=\"modal-body\" style=\"overflow-y:auto\">\n <p class=\"fw-bold h6\">{{data.message | translate }}</p>\n <div class=\"form-floating m-2\">\n <input class=\"form-control\" name=\"title\" [(ngModel)]=\"data.layerTitle\"\n [placeholder]=\"'COMMON.title' | translate\" />\n <label for=\"title\" class=\"capabilities_label control-label\">{{'COMMON.title' | translate\n }}</label>\n </div>\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"yes()\" [title]=\"'COMMON.yes' | translate\"\n data-dismiss=\"modal\">{{'COMMON.yes' |\n translate}}</button>\n <button type=\"button\" class=\"btn btn-secondary compositions-btn-cancel\"\n [title]=\"'COMMON.no' | translate\" (click)=\"no()\" data-dismiss=\"modal\">{{'COMMON.no' |\n translate }}</button>\n </div>\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "directive", type: i1.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: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: i2$1.TranslatePipe, name: "translate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsCopyLayerDialogComponent, decorators: [{ type: Component, args: [{ selector: 'hs-copy-layer-dialog', standalone: false, template: "<div class=\"modal in\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n <div class=\"modal-dialog\">\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <h4 class=\"modal-title\">\n {{data.title | translate }} ?\n </h4>\n <button type=\"button\" (click)=\"no()\" class=\"btn-close\" data-dismiss=\"modal\"\n [attr.aria-label]=\"'COMMON.close' | translate\"></button>\n </div>\n <div class=\"modal-body\" style=\"overflow-y:auto\">\n <p class=\"fw-bold h6\">{{data.message | translate }}</p>\n <div class=\"form-floating m-2\">\n <input class=\"form-control\" name=\"title\" [(ngModel)]=\"data.layerTitle\"\n [placeholder]=\"'COMMON.title' | translate\" />\n <label for=\"title\" class=\"capabilities_label control-label\">{{'COMMON.title' | translate\n }}</label>\n </div>\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"yes()\" [title]=\"'COMMON.yes' | translate\"\n data-dismiss=\"modal\">{{'COMMON.yes' |\n translate}}</button>\n <button type=\"button\" class=\"btn btn-secondary compositions-btn-cancel\"\n [title]=\"'COMMON.no' | translate\" (click)=\"no()\" data-dismiss=\"modal\">{{'COMMON.no' |\n translate }}</button>\n </div>\n </div>\n </div>\n</div>\n" }] }] }); class HsLayerEditorSublayerService { constructor() { this.hsLayerManagerVisibilityService = inject(HsLayerManagerVisibilityService); this.populatedLayers = []; this.sublayers = []; } getSubLayers(layer) { return layer ? this.populateSubLayers(layer) : []; } /** * Converts HsWmsLayer to HsSublayer */ mapWMSToLayerVisibility(layers, visible) { return layers.map((layer) => { const mappedLayer = { name: layer.Name, title: layer.Title, visible: visible, previousVisible: undefined, maxResolution: layer.maxResolution, // Recursively map sublayers if they exist sublayers: layer.Layer ? this.mapWMSToLayerVisibility(layer.Layer, visible) : undefined, }; return mappedLayer; }); } /** * Populates the sublayers of a layer with the given layer descriptor. */ populateSubLayers(lyr) { if (this.populatedLayers.includes(lyr.uid)) { return lyr._sublayers; } const layer = lyr.layer; const subLayers = getCachedCapabilities(layer)?.Layer; if (subLayers?.length > 0) { const parsed = this.mapWMSToLayerVisibility(subLayers, lyr.visible); this.populatedLayers.push(lyr.uid); lyr._sublayers = parsed; return parsed; } } /** * Constructs LAYERS param for layer visibility */ constructLayersParam(layers) { const getVisibleLayers = (layer) => { if (!layer.visible) { return []; } if (layer.sublayers?.length) { const visibleSublayers = layer.sublayers.flatMap(getVisibleLayers); return visibleSublayers.length === layer.sublayers.length ? [layer.name] : visibleSublayers; } return [layer.name]; }; return layers.flatMap(getVisibleLayers).join(','); } /** * Handles the selection of a sublayer, updating the layer's visibility and parameters. */ subLayerSelected() { const layer = this.layer; if (!layer) { console.error('Trying to update sublayer on undefined layer'); return; } const params = getLayerParams(layer.layer); params.LAYERS = this.constructLayersParam(layer._sublayers); // Special handling for ArcGIS layers if (isLayerArcgis(layer.layer)) { params.LAYERS = `show:${params.LAYERS}`; } // If no sublayers are visible, update the overall layer visibility if (params.LAYERS == '' || params.LAYERS == 'show:') { this.hsLayerManagerVisibilityService.changeLayerVisibility(!layer.visible, layer); return; } // If the layer was previously invisible, make it visible if (layer.visible == false) { this.hsLayerManagerVisibilityService.changeLayerVisibility(!layer.visible, layer); } // Update the layer parameters updateLayerParams(layer.layer, params); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsLayerEditorSublayerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsLayerEditorSublayerService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsLayerEditorSublayerService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); class HsLayerEditorSubLayerCheckboxesComponent { constructor() { this.hsLayerEditorSublayerService = inject(HsLayerEditorSublayerService); this.hsLayerManagerVisibilityService = inject(HsLayerManagerVisibilityService); this.hsConfig = inject(HsConfig); this.parent = input(...(ngDevMode ? [undefined, { debugName: "parent" }] : [])); this.subLayer = input.required(...(ngDevMode ? [{ debugName: "subLayer" }] : [])); this.getNestedLayers = computed(() => { const wmsLayer = this.subLayer(); if (!wmsLayer || !wmsLayer.sublayers) { return []; } return Array.isArray(wmsLayer.sublayers) ? wmsLayer.sublayers : [wmsLayer.sublayers]; }, ...(ngDevMode ? [{ debugName: "getNestedLayers" }] : [])); this.expanded = false; } ngOnInit() { this.app = this.hsConfig.id; } subLayerIsString(subLayer) { return typeof subLayer == 'string'; } toggleExpanded() { this.expanded = !this.expanded; } /** * Controls state of layer´s sublayers manipulated by input checkboxes * @param sublayer - Selected sublayer * @param state - New state of sublayer */ subLayerSelected(sublayer, parent) { if (parent) { parent.visible = sublayer.visible || parent.sublayers.some((sl) => sl.visible); } if (sublayer.sublayers) { sublayer.sublayers.forEach((sl) => { sl.visible = sublayer.visible; }); } return this.hsLayerEditorSublayerService.subLayerSelected(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsLayerEditorSubLayerCheckboxesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.3", type: HsLayerEditorSubLayerCheckboxesComponent, isStandalone: true, selector: "hs-layer-editor-sub-layer-checkbox", inputs: { parent: { classPropertyName: "parent", publicName: "parent", isSignal: true, isRequired: false, transformFunction: null }, subLayer: { classPropertyName: "subLayer", publicName: "subLayer", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "@let sl = subLayer();\n<div class=\"d-flex m-auto sublayerContainer\">\n <div class=\"w-100\">\n <div class=\"d-flex\">\n <div class=\"p-0 form-check form-check-inline\">\n @if (!sl.sublayers) {\n <input class=\"form-check-input\" type=\"checkbox\" [(ngModel)]=\"sl.visible\" name=\"sublayerWithoutNestedSublayers\"\n (change)=\"subLayerSelected(sl, parent())\" [attr.id]=\"'hs-sublayers-' + sl.name + '-' + app \">\n <div>\n <label class=\"form-check-label m-0\" [ngClass]=\"{\n 'hs-checkmark':sl.visible,\n 'hs-uncheckmark':!sl.visible,\n 'grayed': (hsLayerManagerVisibilityService.currentResolution > sl.maxResolution) }\"\n [attr.for]=\"'hs-sublayers-' + sl.name + '-' + app \">{{sl.title ||sl.name}}</label>\n </div>\n }\n @else {\n <input class=\"form-check-input\" type=\"checkbox\" [(ngModel)]=\"sl.visible\" name=\"sublayerWithNestedSublayers\"\n (change)=\"subLayerSelected(sl, parent())\" [attr.id]=\"'hs-sublayers-' + sl.name + '-' + app \">\n <div class=\"p-0 d-inline-flex flex-grow\">\n <label class=\"form-check-label m-0\" [ngClass]=\"{'hs-checkmark':sl.visible,'hs-uncheckmark':!sl.visible}\"\n [attr.for]=\"'hs-sublayers-' + sl.name + '-' + app \"></label>\n <div (click)=\"toggleExpanded()\" style=\"cursor:pointer\"\n [ngClass]=\"{'grayed': hsLayerManagerVisibilityService.currentResolution >= sl.maxResolution}\">\n {{sl.title || sl.name}}\n <button type=\"button\" class=\"btn btn-sm p-0\" style=\"font-size: 0.6rem;\">\n <i class=\"fa-solid\" [ngClass]=\"expanded ? 'fa-chevron-down' : 'fa-chevron-right'\"></i>\n </button>\n </div>\n </div>\n <div class=\"mt-2 ms-3\" [hidden]=\"!expanded\">\n @for (nestedLayer of getNestedLayers(); track nestedLayer) {\n <hs-layer-editor-sub-layer-checkbox [subLayer]=\"nestedLayer\" [parent]=\"sl\">\n </hs-layer-editor-sub-layer-checkbox>\n }\n </div>\n }\n </div>\n </div>\n </div>\n</div>", dependencies: [{ kind: "component", type: HsLayerEditorSubLayerCheckboxesComponent, selector: "hs-layer-editor-sub-layer-checkbox", inputs: ["parent", "subLayer"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsLayerEditorSubLayerCheckboxesComponent, decorators: [{ type: Component, args: [{ selector: 'hs-layer-editor-sub-layer-checkbox', imports: [FormsModule, NgClass], template: "@let sl = subLayer();\n<div class=\"d-flex m-auto sublayerContainer\">\n <div class=\"w-100\">\n <div class=\"d-flex\">\n <div class=\"p-0 form-check form-check-inline\">\n @if (!sl.sublayers) {\n <input class=\"form-check-input\" type=\"checkbox\" [(ngModel)]=\"sl.visible\" name=\"sublayerWithoutNestedSublayers\"\n (change)=\"subLayerSelected(sl, parent())\" [attr.id]=\"'hs-sublayers-' + sl.name + '-' + app \">\n <div>\n <label class=\"form-check-label m-0\" [ngClass]=\"{\n 'hs-checkmark':sl.visible,\n 'hs-uncheckmark':!sl.visible,\n 'grayed': (hsLayerManagerVisibilityService.currentResolution > sl.maxResolution) }\"\n [attr.for]=\"'hs-sublayers-' + sl.name + '-' + app \">{{sl.title ||sl.name}}</label>\n </div>\n }\n @else {\n <input class=\"form-check-input\" type=\"checkbox\" [(ngModel)]=\"sl.visible\" name=\"sublayerWithNestedSublayers\"\n (change)=\"subLayerSelected(sl, parent())\" [attr.id]=\"'hs-sublayers-' + sl.name + '-' + app \">\n <div class=\"p-0 d-inline-flex flex-grow\">\n <label class=\"form-check-label m-0\" [ngClass]=\"{'hs-checkmark':sl.visible,'hs-uncheckmark':!sl.visible}\"\n [attr.for]=\"'hs-sublayers-' + sl.name + '-' + app \"></label>\n <div (click)=\"toggleExpanded()\" style=\"cursor:pointer\"\n [ngClass]=\"{'grayed': hsLayerManagerVisibilityService.currentResolution >= sl.maxResolution}\">\n {{sl.title || sl.name}}\n <button type=\"button\" class=\"btn btn-sm p-0\" style=\"font-size: 0.6rem;\">\n <i class=\"fa-solid\" [ngClass]=\"expanded ? 'fa-chevron-down' : 'fa-chevron-right'\"></i>\n </button>\n </div>\n </div>\n <div class=\"mt-2 ms-3\" [hidden]=\"!expanded\">\n @for (nestedLayer of getNestedLayers(); track nestedLayer) {\n <hs-layer-editor-sub-layer-checkbox [subLayer]=\"nestedLayer\" [parent]=\"sl\">\n </hs-layer-editor-sub-layer-checkbox>\n }\n </div>\n }\n </div>\n </div>\n </div>\n</div>" }] }] }); class HsLayerEditorSublayersComponent { constructor() { this.hsLayerEditorSublayerService = inject(HsLayerEditorSublayerService); this.layer = input.required(...(ngDevMode ? [{ debugName: "layer" }] : [])); this.subLayers = computed(() => this.hsLayerEditorSublayerService.getSubLayers(this.layer()), ...(ngDevMode ? [{ debugName: "subLayers" }] : [])); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsLayerEditorSublayersComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.3", type: HsLayerEditorSublayersComponent, isStandalone: true, selector: "hs-layer-editor-sublayers", inputs: { layer: { classPropertyName: "layer", publicName: "layer", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: ` <div class="card-body py-2"> @for (subLayer of subLayers(); track subLayer) { <hs-layer-editor-sub-layer-checkbox [subLayer]="subLayer"> </hs-layer-editor-sub-layer-checkbox> } </div> `, isInline: true, dependencies: [{ kind: "component", type: HsLayerEditorSubLayerCheckboxesComponent, selector: "hs-layer-editor-sub-layer-checkbox", inputs: ["parent", "subLayer"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsLayerEditorSublayersComponent, decorators: [{ type: Component, args: [{ selector: 'hs-layer-editor-sublayers', template: ` <div class="card-body py-2"> @for (subLayer of subLayers(); track subLayer) { <hs-layer-editor-sub-layer-checkbox [subLayer]="subLayer"> </hs-layer-editor-sub-layer-checkbox> } </div> `, imports: [HsLayerEditorSubLayerCheckboxesComponent], }] }] }); class HsLayerEditorService { constructor() { this.hsMapService = inject(HsMapService); this.hsWmsGetCapabilitiesService = inject(HsWmsGetCapabilitiesService); this.hsLayerEditorVectorLayerService = inject(HsLayerEditorVectorLayerService); this.hsEventBusService = inject(HsEventBusService); this.hsLayoutService = inject(HsLayoutService); this.hsLegendService = inject(HsLegendService); this.hsLayerSelectorService = inject(HsLayerSelectorService); this.hsLayerManagerMetadataService = inject(HsLayerManagerMetadataService); this.hsLayerEditorSublayerService = inject(HsLayerEditorSublayerService); this.hsShareUrlService = inject(HsShareUrlService); this.layerTitleChange = new Subject(); this.hsLayerSelectorService.layerSelected .pipe(filter((layer) => !!layer)) .subscribe(async (layer) => { this.legendDescriptor = await this.hsLegendService.getLayerLegendDescriptor(layer.layer); }); } /** * Create layer editor component for settings * @param vcr - View container reference * @param layer - Layer */ createLayerEditor(vcr, layer) { return this.createEditor(vcr, layer, 'settings', HsLayerEditorComponent); } /** * Create layer editor component for sublayers * @param vcr - View container reference * @param layer - Layer */ createSublayerEditor(vcr, layer) { return th