UNPKG

hslayers-ng

Version:
1 lines 82.4 kB
{"version":3,"file":"hslayers-ng-components-query.mjs","sources":["../../../projects/hslayers/components/query/attribute-row/attribute-row.component.ts","../../../projects/hslayers/components/query/attribute-row/attribute-row.component.html","../../../projects/hslayers/components/query/feature-common.service.ts","../../../projects/hslayers/components/query/feature/feature.component.ts","../../../projects/hslayers/components/query/feature/feature.component.html","../../../projects/hslayers/components/query/feature-list/feature-list.component.ts","../../../projects/hslayers/components/query/feature-list/feature-list.component.html","../../../projects/hslayers/components/query/default-info-panel-body/default-info-panel-body.component.ts","../../../projects/hslayers/components/query/default-info-panel-body/default-info-panel-body.component.html","../../../projects/hslayers/components/query/query-wmts.service.ts","../../../projects/hslayers/components/query/query-wms.service.ts","../../../projects/hslayers/components/query/query.tokens.ts","../../../projects/hslayers/components/query/query.component.ts","../../../projects/hslayers/components/query/query.component.html","../../../projects/hslayers/components/query/hslayers-ng-components-query.ts"],"sourcesContent":["import {Component, Input, OnInit} from '@angular/core';\nimport {FormsModule} from '@angular/forms';\n\n@Component({\n selector: 'hs-query-attribute-row',\n templateUrl: './attribute-row.component.html',\n imports: [FormsModule],\n})\nexport class HsQueryAttributeRowComponent implements OnInit {\n isObject = false;\n @Input() attribute;\n @Input() feature;\n @Input() readonly: boolean;\n @Input() template;\n tmpObjectValue: any;\n\n ngOnInit(): void {\n this.checkAttributeValue();\n }\n\n /**\n * Act on feature attribute changes\n */\n change(): void {\n if (this.feature?.feature) {\n const feature = this.feature.feature;\n if (this.isObject) {\n feature.set(this.attribute.name, JSON.parse(this.tmpObjectValue));\n } else {\n feature.set(this.attribute.name, this.attribute.value);\n }\n }\n }\n\n /**\n * Check if attribute value is object and stringify it if needed\n */\n checkAttributeValue(): void {\n if (\n typeof this.attribute.value == 'object' &&\n !Array.isArray(this.attribute.value)\n ) {\n this.isObject = true;\n this.tmpObjectValue = JSON.stringify(this.attribute.value);\n }\n }\n}\n","<div class=\"row\">\n <div class=\"input-group m-1\">\n <span class=\"input-group-text\">{{attribute.name}}</span>\n @if (!isObject) {\n <input class=\"form-control\" [(ngModel)]=\"attribute.value\" (change)=\"change()\"\n [readonly]=\"readonly\" name=\"hs-query-attribute-row-input\">\n }\n @if (isObject) {\n <textarea class=\"form-control\" name=\"attributeValue\" [(ngModel)]=\"tmpObjectValue\"\n [readonly]=\"readonly\" (change)=\"change()\" name=\"hs-query-attribute-row-textarea\">\n </textarea>\n }\n </div>\n</div>","import {BehaviorSubject, Observable} from 'rxjs';\nimport {Injectable, inject} from '@angular/core';\n\nimport {Feature} from 'ol';\nimport {Geometry} from 'ol/geom';\nimport {Layer} from 'ol/layer';\nimport {Source, Vector as VectorSource} from 'ol/source';\n\nimport {HsLanguageService} from 'hslayers-ng/services/language';\nimport {HsMapService} from 'hslayers-ng/services/map';\nimport {HsQueryVectorService} from 'hslayers-ng/services/query';\nimport {HsToastService} from 'hslayers-ng/common/toast';\nimport {getTitle} from 'hslayers-ng/common/extensions';\nimport {isLayerDrawable} from 'hslayers-ng/services/utils';\n\nexport interface exportFormats {\n name: 'WKT' | 'GeoJSON';\n ext: string; //File extension\n serializedData?: string; //Features as string according to WKT or GeoJSON\n mimeType: string;\n downloadData?: any; //Serialized/sanitized data suitable for href\n}\n\n@Injectable({\n providedIn: 'root',\n})\nexport class HsFeatureCommonService {\n private hsQueryVectorService = inject(HsQueryVectorService);\n private hsToastService = inject(HsToastService);\n private hsLanguageService = inject(HsLanguageService);\n private hsMapService = inject(HsMapService);\n\n listSubject = new BehaviorSubject<Layer<Source>[]>([] as Layer<Source>[]);\n\n availableLayer$: Observable<Layer<Source>[]> =\n this.listSubject.asObservable();\n\n constructor() {\n this.hsMapService.loaded().then((map) => {\n map.getLayers().on('change:length', () => {\n this.updateLayerList();\n });\n this.updateLayerList();\n });\n }\n\n /**\n * Translate string value to the selected UI language\n * @param module - Locales json key\n * @param text - Locales json key value\n * @returns Translated text\n */\n translateString(module: string, text: string): string {\n return this.hsLanguageService.getTranslationIgnoreNonExisting(\n module,\n text,\n undefined,\n );\n }\n\n /**\n * Update layer list from the current app map\n */\n updateLayerList(): void {\n const layers = this.hsMapService\n .getLayersArray()\n .filter((layer: Layer<Source>) => {\n return isLayerDrawable(layer);\n });\n this.listSubject.next(layers);\n }\n\n /**\n * Prepare features for export\n * @param exportFormats - Export formats selected\n * @param features - Features to export\n */\n toggleExportMenu(\n exportFormats: exportFormats[],\n features: Feature<Geometry>[] | Feature<Geometry>,\n ): void {\n for (const format of exportFormats) {\n format.serializedData = this.hsQueryVectorService.exportData(\n format.name,\n features,\n );\n }\n }\n\n /**\n * Move or copy feature/s\n * @param type - Action type ('move' or 'copy')\n * @param features - Features to interact with\n * @param toLayer - Target layer\n */\n moveOrCopyFeature(\n type: 'move' | 'copy',\n features: Feature<Geometry>[],\n toLayer: Layer<VectorSource>,\n ): void {\n features.forEach((feature) => {\n feature.setStyle(null); //To prevent feature from getting individual style\n toLayer.getSource().addFeature(feature.clone());\n if (type == 'move') {\n this.hsQueryVectorService.removeFeature(feature);\n }\n });\n this.hsToastService.createToastPopupMessage(\n this.hsLanguageService.getTranslation('QUERY.feature.featureEdited'),\n this.hsLanguageService.getTranslation(\n `QUERY.feature.feature${type}Succ`,\n undefined,\n ) + getTitle(toLayer),\n {\n type: 'success',\n serviceCalledFrom: 'HsFeatureCommonService',\n },\n );\n }\n}\n","import {AsyncPipe, NgClass} from '@angular/common';\nimport {\n Component,\n DestroyRef,\n OnInit,\n computed,\n input,\n inject,\n} from '@angular/core';\nimport {FormsModule} from '@angular/forms';\nimport {NgbDropdownModule} from '@ng-bootstrap/ng-bootstrap';\nimport {Observable, map} from 'rxjs';\nimport {takeUntilDestroyed} from '@angular/core/rxjs-interop';\nimport {TranslatePipe} from '@ngx-translate/core';\n\nimport {Layer} from 'ol/layer';\n\nimport {HsDownloadDirective} from 'hslayers-ng/common/download';\nimport {HsFeatureCommonService, exportFormats} from '../feature-common.service';\nimport {HsMapService} from 'hslayers-ng/services/map';\nimport {HsQueryAttributeRowComponent} from '../attribute-row/attribute-row.component';\nimport {HsQueryVectorService} from 'hslayers-ng/services/query';\nimport {getTitle} from 'hslayers-ng/common/extensions';\nimport {HsFeatureDescriptor} from 'hslayers-ng/types';\nimport {isLayerEditable} from 'hslayers-ng/services/utils';\n@Component({\n selector: 'hs-query-feature',\n templateUrl: './feature.component.html',\n imports: [\n TranslatePipe,\n AsyncPipe,\n FormsModule,\n NgbDropdownModule,\n HsDownloadDirective,\n NgClass,\n HsQueryAttributeRowComponent,\n ],\n})\nexport class HsQueryFeatureComponent implements OnInit {\n private hsMapService = inject(HsMapService);\n private hsQueryVectorService = inject(HsQueryVectorService);\n private hsFeatureCommonService = inject(HsFeatureCommonService);\n private destroyRef = inject(DestroyRef);\n\n feature = input<HsFeatureDescriptor>();\n\n olFeature = computed(() => {\n return this.feature().feature;\n });\n\n layer = computed(() => {\n return this.hsMapService.getLayerForFeature(this.olFeature());\n });\n\n isFeatureRemovable = computed(() => {\n const layer = this.layer();\n if (layer) {\n return isLayerEditable(layer);\n }\n return false;\n });\n\n attributeName = '';\n attributeValue = '';\n newAttribVisible = false;\n exportFormats: exportFormats[] = [\n {name: 'WKT', ext: 'wkt', mimeType: 'text/plain', downloadData: ''},\n {\n name: 'GeoJSON',\n ext: 'geojson',\n mimeType: 'application/json',\n downloadData: '',\n },\n ];\n exportMenuVisible = false;\n editMenuVisible = false;\n selectedLayer = null;\n editType: 'move' | 'copy';\n getTitle = getTitle;\n availableLayers: Observable<Layer[]>;\n\n ngOnInit(): void {\n this.availableLayers = this.hsFeatureCommonService.availableLayer$.pipe(\n takeUntilDestroyed(this.destroyRef),\n map((layers) => {\n if (!this.olFeature()) {\n //Feature from WMS getFeatureInfo\n return [];\n }\n const featureLayer = this.layer();\n return layers.filter((layer) => layer != featureLayer);\n }),\n );\n }\n\n /**\n * Set new feature attribute\n * @param attributeName - New attribute name\n * @param attributeValue - New attribute value\n */\n saveNewAttribute(attributeName: string, attributeValue): void {\n const feature = this.olFeature();\n if (feature) {\n const getDuplicates = this.feature().attributes.filter(\n (duplicate) => duplicate.name == attributeName,\n );\n if (getDuplicates.length == 0) {\n const obj = {name: attributeName, value: attributeValue};\n this.feature().attributes.push(obj);\n feature.set(attributeName, attributeValue);\n }\n }\n this.newAttribVisible = !this.newAttribVisible;\n this.attributeName = '';\n this.attributeValue = '';\n }\n\n /**\n * Remove this feature\n */\n removeFeature(): void {\n this.hsQueryVectorService.removeFeature(this.olFeature());\n }\n\n /**\n * Zoom to this feature\n */\n zoomToFeature(): void {\n const extent = this.olFeature().getGeometry().getExtent();\n this.hsMapService.fitExtent(extent);\n }\n\n /**\n * Toggle dropdown menus\n * @param beingToggled - Menu name that is being toggled\n * @param other - Other menu name to be closed if opened\n */\n toggleMenus(beingToggled: string, other: string): void {\n this[other] = this[other] ? !this[other] : this[other];\n this[beingToggled] = !this[beingToggled];\n }\n\n /**\n * Toggle export menus\n */\n toggleExportMenu(): void {\n this.hsFeatureCommonService.toggleExportMenu(\n this.exportFormats,\n this.olFeature(),\n );\n this.toggleMenus('exportMenuVisible', 'editMenuVisible');\n }\n\n /**\n * Set edit type (move or copy)\n * @param type - Type selected\n */\n editTypeSelected(type: 'move' | 'copy'): void {\n this.editType = type;\n this.editMenuVisible = !this.editMenuVisible;\n }\n\n /**\n * Show or hide edit menu\n */\n toggleEditMenu(): void {\n if (this.editType) {\n this.editType = null;\n return;\n }\n this.toggleMenus('editMenuVisible', 'exportMenuVisible');\n }\n\n /**\n * Move or copy feature\n */\n moveOrCopyFeature(): void {\n this.hsFeatureCommonService.moveOrCopyFeature(\n this.editType,\n [this.olFeature()],\n this.selectedLayer,\n );\n }\n\n /**\n * Translate string value to the selected UI language\n * @param module - Locales json key\n * @param text - Locales json key value\n * @returns Translated text\n */\n translateString(module: string, text: string): string {\n return this.hsFeatureCommonService.translateString(module, text);\n }\n}\n","@let f = feature();\n<div class=\"container-fluid\">\n <div class=\"row\">\n <div class=\"col-12 m-2\"><strong>{{f.name}} @if (f.layer) {\n <span>{{'QUERY.feature.inLayer' |\n translate }}</span>\n }\n {{f.layer}}</strong></div>\n</div>\n@for (attribute of f.attributes; track attribute) {\n <hs-query-attribute-row [template]=\"f.hstemplate\"\n [feature]=\"feature\" [attribute]=\"attribute\" [readonly]=\"isFeatureRemovable()\">\n </hs-query-attribute-row>\n}\n@for (stat of f.stats; track stat) {\n <hs-query-attribute-row [feature]=\"feature\" [attribute]=\"stat\" [readonly]=\"true\">\n </hs-query-attribute-row>\n}\n\n@if (newAttribVisible) {\n <div class=\"bg-light p-2 my-3\">\n <div class=\"form-floating mb-3\">\n <input class=\"form-control\" [placeholder]=\"attributeName\" [(ngModel)]=\"attributeName\"\n name=\"hs-add-feature-attribute-name\">\n <label for=\"hs-add-feature-attribute-name\">{{'QUERY.feature.attributeName' | translate }}</label>\n </div>\n <div class=\"form-floating mb-3\" [hidden]=\"!(attributeName && newAttribVisible)\">\n <input class=\"form-control\" [placeholder]=\"attributeValue\" [(ngModel)]=\"attributeValue\"\n name=\"hs-add-feature-attribute-value\">\n <label for=\"hs-add-feature-attribute-value\">{{'QUERY.feature.attributeValue' | translate }}</label>\n </div>\n <div class=\"row justify-content-end\" [hidden]=\"!(attributeName && attributeValue && newAttribVisible)\">\n <button class=\"btn btn-primary btn-sm w-auto d-flex gap-2 mx-2\"\n (click)=\"saveNewAttribute(attributeName,attributeValue)\">{{'COMMON.save'\n | translate}}<i class=\"fa-solid fa-floppy-disk\"></i></button>\n </div>\n </div>\n }\n\n\n\n <div [hidden]=\"!editType\" class=\"row p-2 \">\n <div class=\"d-flex w-100 input-group input-group-sm align-items-baseline\">\n <label for=\"selectedLayer\" class=\"px-2\">{{translateString('QUERY.feature',editType+'Feature')}}\n {{'COMMON.into' | translate }}:</label>\n <select class=\"form-control text-truncate\" [title]=\"'ADDLAYERS.Vector.chooseLayer' | translate \"\n [(ngModel)]=\"selectedLayer\" name=\"selectedLayer\">\n <option [ngValue]=\"null\" [disabled]=\"true\" selected hidden> {{'ADDLAYERS.Vector.chooseLayer' |\n translate }}</option>\n @for (layer of availableLayers | async; track layer) {\n <option [ngValue]=\"layer\">\n {{'LAYERS.' + getTitle(layer) | translate : {fallbackValue: getTitle(layer)} }}</option>\n }\n </select>\n <button class=\"btn btn-primary btn-sm\" (click)=\"moveOrCopyFeature()\" [disabled]=\"!selectedLayer\"><i\n class=\"fa-solid fa-floppy-disk\"></i></button>\n </div>\n </div>\n</div>\n@if (olFeature() !== undefined) {\n <div class=\"justify-content-end\" style=\"padding-left: 10px\">\n <div class=\"mx-1 mb-2 text-end\">\n <div class=\"btn-group\">\n <button [disabled]=\"isFeatureRemovable()\" class=\"btn btn-secondary btn-sm\"\n (click)=\"newAttribVisible = !newAttribVisible\"><i class=\"fa-solid fa-square-plus\"></i></button>\n <button class=\"btn btn-secondary btn-sm\" (click)=\"zoomToFeature()\"><i class=\"fa-solid fa-magnifying-glass\"></i></button>\n <div class=\"btn-group\" ngbDropdown placement=\"bottom\" display=\"dynamic\">\n <button ngbDropdownToggle class=\"btn btn-secondary btn-sm\" type=\"button\" (click)=\"toggleEditMenu()\"><i\n class=\"fa-solid fa-gears\"></i></button>\n <div ngbDropdownMenu [ngClass]=\"{'show': editMenuVisible}\">\n <a class=\"dropdown-item\" (click)=\"editTypeSelected('copy')\">{{'QUERY.feature.copyFeature' |\n translate }}</a>\n <a class=\"dropdown-item\" (click)=\"editTypeSelected('move')\">{{'QUERY.feature.moveFeature' |\n translate }}</a>\n </div>\n </div>\n <div class=\"btn-group\" ngbDropdown placement=\"bottom\" display=\"dynamic\">\n <button ngbDropdownToggle class=\"btn btn-secondary btn-sm rounded-0\" type=\"button\"\n (click)=\"toggleExportMenu()\" [title]=\"'QUERY.downloadAs' | translate \"><i\n class=\"fa-solid fa-download\"></i></button>\n <div ngbDropdownMenu [ngClass]=\"{'show': exportMenuVisible}\">\n @for (format of exportFormats; track format) {\n <a class=\"dropdown-item\" [download]=\"format.name + '_file.' + format.ext\"\n [hsDownload]=\"format.serializedData\" [mimeType]=\"format.mimeType\" [href]=\"format.downloadData\"\n (downloadPrepared)=\"format.downloadData = $event\"\n >{{format.name}}</a>\n }\n </div>\n </div>\n @if (isFeatureRemovable()) {\n <button class=\"btn btn-danger btn-sm\" (click)=\"removeFeature()\"><i\n class=\"fa-solid fa-trash\"></i></button>\n }\n </div>\n </div>\n </div>\n }\n","import {AsyncPipe, NgClass, NgStyle, SlicePipe} from '@angular/common';\nimport {Component, inject} from '@angular/core';\nimport {FormsModule} from '@angular/forms';\nimport {NgbDropdownModule} from '@ng-bootstrap/ng-bootstrap';\nimport {TranslatePipe} from '@ngx-translate/core';\n\nimport {Feature, getUid} from 'ol';\nimport {Geometry} from 'ol/geom';\n\nimport {HsConfirmDialogComponent} from 'hslayers-ng/common/confirm';\nimport {HsDialogContainerService} from 'hslayers-ng/common/dialogs';\nimport {HsDownloadDirective} from 'hslayers-ng/common/download';\nimport {HsFeatureCommonService, exportFormats} from '../feature-common.service';\nimport {\n HsQueryBaseService,\n HsQueryVectorService,\n} from 'hslayers-ng/services/query';\nimport {HsQueryFeatureComponent} from '../feature/feature.component';\nimport {getTitle} from 'hslayers-ng/common/extensions';\n\n@Component({\n selector: 'hs-query-feature-list',\n templateUrl: './feature-list.component.html',\n imports: [\n TranslatePipe,\n AsyncPipe,\n FormsModule,\n NgbDropdownModule,\n HsDownloadDirective,\n NgClass,\n NgStyle,\n HsQueryFeatureComponent,\n SlicePipe,\n ],\n})\nexport class HsQueryFeatureListComponent {\n private hsQueryVectorService = inject(HsQueryVectorService);\n private hsDialogContainerService = inject(HsDialogContainerService);\n hsFeatureCommonService = inject(HsFeatureCommonService);\n hsQueryBaseService = inject(HsQueryBaseService);\n\n exportMenuVisible;\n selectedFeaturesVisible = true;\n exportFormats: exportFormats[] = [\n {name: 'WKT', ext: 'wkt', mimeType: 'text/plain', downloadData: ''},\n {\n name: 'GeoJSON',\n ext: 'geojson',\n mimeType: 'application/json',\n downloadData: '',\n },\n ];\n editType = null;\n editMenuVisible = false;\n selectedLayer = null;\n getTitle = getTitle;\n\n /**\n * Track item by OpenLayers feature, ol_uid value\n * @param index - Index\n * @param item - Item provided\n */\n trackById(index, item) {\n if (item.feature) {\n return getUid(item.feature);\n }\n return JSON.stringify(item);\n }\n\n /**\n * Get OL feature array\n * @returns Feature array\n */\n olFeatureArray(): Feature<Geometry>[] {\n return this.hsQueryBaseService.features\n .map((feature) => feature.feature)\n .filter((f) => f);\n }\n\n /**\n * Toggle dropdown menus\n * @param beingToggled - Menu name that is being toggled\n * @param other - Other menu name to be closed if opened\n */\n toggleMenus(beingToggled: string, other: string): void {\n this[other] = this[other] ? !this[other] : this[other];\n this[beingToggled] = !this[beingToggled];\n }\n\n /**\n * Toggle export menu\n */\n toggleExportMenu(): void {\n this.hsFeatureCommonService.toggleExportMenu(\n this.exportFormats,\n this.olFeatureArray(),\n );\n this.toggleMenus('exportMenuVisible', 'editMenuVisible');\n }\n\n /**\n * Toggle edit menu\n */\n toggleEditMenu(): void {\n if (this.editType) {\n this.editType = null;\n return;\n }\n this.toggleMenus('editMenuVisible', 'exportMenuVisible');\n }\n\n /**\n * Set edit type\n * @param type - Type selected\n */\n editTypeSelected(type: string): void {\n this.editType = type;\n this.editMenuVisible = !this.editMenuVisible;\n }\n\n /**\n * Move or copy feature\n */\n moveOrCopyFeature(): void {\n this.hsFeatureCommonService.moveOrCopyFeature(\n this.editType,\n this.olFeatureArray(),\n this.selectedLayer,\n );\n }\n\n /**\n * Remove all selected features\n */\n async removeAllSelectedFeatures(): Promise<void> {\n const dialog = this.hsDialogContainerService.create(\n HsConfirmDialogComponent,\n {\n message: 'QUERY.reallyDeleteAllSelectedLayers',\n title: 'COMMON.confirmDelete',\n },\n );\n const confirmed = await dialog.waitResult();\n if (confirmed == 'yes') {\n for (const feature of this.hsQueryBaseService.features.filter((f) =>\n this.hsQueryVectorService.isFeatureRemovable(f.feature),\n )) {\n //Give HsQueryVectorService.featureRemovals time to splice QueryBase.data.features\n setTimeout(() => {\n this.hsQueryVectorService.removeFeature(feature.feature);\n }, 250);\n }\n }\n }\n\n /**\n * Translate string value to the selected UI language\n * @param module - Locales json key\n * @param text - Locales json key value\n * @returns Translated text\n */\n translateString(module: string, text: string): string {\n return this.hsFeatureCommonService.translateString(module, text);\n }\n}\n","<ng-container>\n @if (hsQueryBaseService.features.length > 0) {\n <div class=\"navbar navbar-light bg-light px-1\">\n <div class=\"d-flex align-items-baseline w-100\">\n <div class=\"d-flex align-items-baseline\" style=\"flex: 1;\">\n <p class=\"m-0\" [ngStyle]=\"{'flex' : hsQueryBaseService.features.length > 1 ? '' : '1'}\">\n {{'QUERY.featuresSelected' | translate }}{{hsQueryBaseService.features.length}}\n </p>\n <button type=\"button\" class=\"btn dropdown-toggle border-0 ms-2\"\n [title]=\"'HISTORY.toggleDropdown' | translate \"\n (click)=\"selectedFeaturesVisible = !selectedFeaturesVisible\">\n </button>\n </div>\n @if (olFeatureArray().length > 0) {\n <div class=\"btn-group\">\n <div class=\"btn-group\" ngbDropdown placement=\"bottom\" display=\"dynamic\">\n <button ngbDropdownToggle class=\"btn btn-secondary btn-sm\" type=\"button\"\n (click)=\"toggleEditMenu()\"><i class=\"fa-solid fa-gears\"></i></button>\n <div ngbDropdownMenu [ngClass]=\"{'show': editMenuVisible}\">\n <a class=\"dropdown-item\" (click)=\"editTypeSelected('copy')\">{{'QUERY.feature.copyFeatures' |\n translate }}</a>\n <a class=\"dropdown-item\" (click)=\"editTypeSelected('move')\">{{'QUERY.feature.moveFeatures' |\n translate }}</a>\n </div>\n </div>\n <div class=\"btn-group\" ngbDropdown placement=\"bottom\" display=\"dynamic\">\n <button ngbDropdownToggle class=\"btn btn-secondary btn-sm rounded-0\" type=\"button\"\n [title]=\"'QUERY.downloadAs' | translate \" (click)=\"toggleExportMenu()\"><i\n class=\"fa-solid fa-download\"></i></button>\n <div ngbDropdownMenu [ngClass]=\"{'show': exportMenuVisible}\">\n @for (format of exportFormats; track format) {\n <a class=\"dropdown-item\"\n [download]=\"format.name + '_file.' + format.ext\" [hsDownload]=\"format.serializedData\"\n [mimeType]=\"format.mimeType\" [href]=\"format.downloadData\"\n (downloadPrepared)=\"format.downloadData = $event\">\n {{format.name}}</a>\n }\n </div>\n </div>\n <button type=\"button\" class=\"btn-danger btn btn-sm border-0\"\n [title]=\"'QUERY.deleteAllSelected' | translate \" (click)=\"removeAllSelectedFeatures()\">\n <i class=\"fa-solid fa-trash\"></i>\n </button>\n </div>\n }\n </div>\n @if (editType) {\n <div class=\"d-flex p-2 w-100\">\n <div class=\"d-flex w-100 input-group input-group-sm align-items-baseline\">\n <label for=\"selectedLayer\" class=\"px-2\">{{translateString('QUERY.feature',editType+'Features')}}\n {{'COMMON.into' | translate }}:</label>\n <select class=\"form-control text-truncate\" [(ngModel)]=\"selectedLayer\" name=\"selectedLayer\"\n [title]=\"'ADDLAYERS.Vector.chooseLayer' | translate \">\n <option [ngValue]=\"null\" [disabled]=\"true\" selected hidden> {{'ADDLAYERS.Vector.chooseLayer' |\n translate }}</option>\n @for (layer of hsFeatureCommonService.availableLayer$ | async; track layer) {\n <option [ngValue]=\"layer\">\n {{'LAYERS.' + getTitle(layer) | translate : {fallbackValue: getTitle(layer)} }}</option>\n }\n </select>\n <button class=\"btn btn-primary btn-sm\" (click)=\"moveOrCopyFeature()\" [disabled]=\"!selectedLayer\"><i\n class=\"fa-solid fa-floppy-disk\"></i></button>\n </div>\n </div>\n }\n </div>\n }\n @if (selectedFeaturesVisible) {\n <div>\n @for (item of hsQueryBaseService.features | slice:0:50; track trackById($index, item)) {\n <hs-query-feature\n [feature]=\"item\">\n </hs-query-feature>\n }\n </div>\n }\n</ng-container>\n","import {Component, OnInit, inject} from '@angular/core';\nimport {NgbDropdownModule} from '@ng-bootstrap/ng-bootstrap';\nimport {TranslatePipe} from '@ngx-translate/core';\n\nimport {HsQueryBaseService} from 'hslayers-ng/services/query';\nimport {HsQueryFeatureListComponent} from '../feature-list/feature-list.component';\n\n@Component({\n selector: 'hs-query-default-info-panel-body',\n templateUrl: './default-info-panel-body.component.html',\n imports: [HsQueryFeatureListComponent, NgbDropdownModule, TranslatePipe],\n})\nexport class HsQueryDefaultInfoPanelBodyComponent implements OnInit {\n hsQueryBaseService = inject(HsQueryBaseService);\n\n featureInfoExpanded: boolean;\n ngOnInit(): void {\n this.featureInfoExpanded = true;\n }\n}\n","@if ((hsQueryBaseService.features.length > 0) || (hsQueryBaseService.coordinates.length > 0)) {\n<div class=\"card hs-main-panel\">\n <div class=\"card-body\">\n @if(hsQueryBaseService.featureInfoHtmls.length > 0) {\n <div ngbDropdown>\n <button ngbDropdownToggle class=\"btn btn-light w-100 rounded-0\">\n {{'QUERY.featureInfo' | translate }}\n </button>\n @for (html of hsQueryBaseService.featureInfoHtmls; track html) {\n <div ngbDropdownMenu class=\"position-relative w-100 px-1 mt-1\" [innerHtml]=\"html\"></div>\n }\n </div>\n }\n @else if(hsQueryBaseService.wmsFeatureInfoLoading) {\n <div class=\"d-flex justify-content-center p-2\" style=\"min-height: 38px;\"><span\n class=\"hs-loader hs-loader-dark ml-auto\"></span></div>\n }\n <hs-query-feature-list></hs-query-feature-list>\n <table class=\"table table-striped\" style=\"table-layout:fixed\"\n [hidden]=\"!(hsQueryBaseService.attributes.length > 0)\">\n <tbody>\n @for (attribute of hsQueryBaseService.attributes; track attribute) {\n <tr>\n <td class=\"first-col\">{{attribute.name}}</td>\n <td class=\"second-col\" [innerHtml]=\"attribute.value\"></td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n <iframe id=\"invisible_popup\" style=\"display:block; z-index:12122;left:-10000px; position:absolute\"></iframe>\n</div>\n}\n","import {inject, Injectable} from '@angular/core';\n\nimport WMTSTileGrid from 'ol/tilegrid/WMTS';\nimport {Layer} from 'ol/layer';\nimport {WMTS} from 'ol/source';\nimport {get as getProjection, transform} from 'ol/proj';\n\nimport {HsMapService} from 'hslayers-ng/services/map';\nimport {getInfoFormat} from 'hslayers-ng/common/extensions';\nimport {paramsToURLWoEncode} from 'hslayers-ng/services/utils';\n\n@Injectable({\n providedIn: 'root',\n})\nexport class HsQueryWmtsService {\n hsMapService = inject(HsMapService);\n\n /**\n * Parse request URL\n * @param layer - Layer to Query\n * @param coordinate - Clicked coordinates\n * @returns Request URL and format\n */\n async parseRequestURL(\n layer: Layer<WMTS>,\n coordinate: number[],\n ): Promise<{url: string; format: string}> {\n const source = layer.getSource();\n\n if (getProjection(source.getProjection())) {\n coordinate = transform(\n coordinate,\n this.hsMapService.getCurrentProj(),\n source.getProjection(),\n );\n }\n\n const tileGrid = source.getTileGrid() as WMTSTileGrid;\n const tileCoord = tileGrid.getTileCoordForCoordAndResolution(\n coordinate,\n this.hsMapService.getMap().getView().getResolution(),\n );\n\n const tileExtent = tileGrid.getTileCoordExtent(tileCoord);\n\n const tileResolution = tileGrid.getResolution(tileCoord[0]);\n const tileMatrix = tileGrid.getMatrixIds()[tileCoord[0]];\n\n // I.J params (clicked pixel position relative to tile)\n const x = Math.floor(\n (coordinate[0] - tileExtent[0]) / (tileResolution / 1),\n ); //pixelRatio\n const y = Math.floor(\n (tileExtent[3] - coordinate[1]) / (tileResolution / 1),\n ); //pixelRatio\n\n const urls = source.getUrls()[0];\n\n const params = {\n LAYER: source.getLayer(),\n service: 'WMTS',\n INFOFORMAT: getInfoFormat(layer),\n REQUEST: 'GetFeatureInfo',\n TileCol: tileCoord[1],\n TileRow: tileCoord[2],\n I: x,\n J: y,\n TILEMATRIXSET: source.getMatrixSet(),\n TileMatrix: tileMatrix,\n feature_count: source.getLayer().length,\n };\n\n const url = [urls, paramsToURLWoEncode(params)].join('');\n return {\n url: url,\n format: params.INFOFORMAT,\n };\n }\n}\n","import {HttpClient, HttpHeaders} from '@angular/common/http';\nimport {Injectable, inject} from '@angular/core';\n\nimport {Feature} from 'ol';\nimport {Geometry} from 'ol/geom';\nimport {Image as ImageLayer, Layer, Tile} from 'ol/layer';\nimport {ImageWMS, Source, TileWMS, WMTS} from 'ol/source';\nimport {WMSGetFeatureInfo} from 'ol/format';\nimport {lastValueFrom} from 'rxjs';\n\nimport {HsLanguageService} from 'hslayers-ng/services/language';\nimport {getLayerName, HsProxyService, instOf} from 'hslayers-ng/services/utils';\nimport {HsLogService} from 'hslayers-ng/services/log';\nimport {HsMapService} from 'hslayers-ng/services/map';\nimport {HsQueryBaseService} from 'hslayers-ng/services/query';\nimport {HsQueryWmtsService} from './query-wmts.service';\nimport {\n getBase,\n getFeatureInfoLang,\n getFeatureInfoTarget,\n getInfoFormat,\n getPopupClass,\n getQueryFilter,\n} from 'hslayers-ng/common/extensions';\nimport {jsonGetFeatureInfo} from 'hslayers-ng/common/get-feature-info';\n\n@Injectable({\n providedIn: 'root',\n})\nexport class HsQueryWmsService {\n private hsMapService = inject(HsMapService);\n private hsLanguageService = inject(HsLanguageService);\n private httpClient = inject(HttpClient);\n private hsLogService = inject(HsLogService);\n private hsQueryWmtsService = inject(HsQueryWmtsService);\n private hsQueryBaseService = inject(HsQueryBaseService);\n private hsProxyService = inject(HsProxyService);\n\n infoCounter = 0;\n\n constructor() {\n this.hsQueryBaseService.getFeatureInfoStarted.subscribe((evt) => {\n this.infoCounter = 0;\n this.hsMapService.getLayersArray().forEach((layer: Layer<Source>) => {\n if (getBase(layer) == true || layer.get('queryable') == false) {\n return;\n }\n if (getQueryFilter(layer) != undefined) {\n const filter = getQueryFilter(layer);\n if (!filter(this.hsMapService.getMap(), layer, evt.pixel)) {\n return;\n }\n }\n this.queryWmsLayer(\n instOf(layer, Tile)\n ? (layer as Layer<TileWMS>)\n : (layer as Layer<ImageWMS>),\n evt.coordinate,\n );\n });\n });\n }\n\n /**\n * Request information about clicked WMS layer coordinates\n * @param url - Request URL\n * @param infoFormat - Request information format\n * @param coordinate - Clicked coordinates\n * @param layer - Target layer\n */\n async request(\n url: string,\n infoFormat: string,\n coordinate: number[],\n layer: Layer<Source>,\n ): Promise<void> {\n this.infoCounter++;\n if (this.infoCounter > 1) {\n this.hsQueryBaseService.multiWmsQuery = true;\n }\n\n const req_url = this.hsProxyService.proxify(url);\n const reqHash = this.hsQueryBaseService.currentQuery;\n try {\n const headers = new Headers({'Content-Type': 'text'});\n headers.set('Accept', 'text');\n const response = await lastValueFrom(\n this.httpClient.get(req_url, {\n headers: new HttpHeaders().set('Content-Type', 'text'),\n responseType: 'text',\n }),\n );\n\n if (reqHash != this.hsQueryBaseService.currentQuery) {\n return;\n }\n this.featureInfoReceived(response, infoFormat, coordinate, layer);\n } catch (exception) {\n if (reqHash != this.hsQueryBaseService.currentQuery) {\n return;\n }\n this.featureInfoError(coordinate, exception);\n }\n }\n\n /**\n * Error callback to decrease infoCounter\n * @param coordinate - Clicked coordinates\n * @param exception - Error caught\n */\n featureInfoError(coordinate: number[], exception): void {\n this.infoCounter--;\n this.hsLogService.warn(exception);\n if (this.infoCounter === 0) {\n this.queriesCollected(coordinate);\n }\n }\n\n /**\n * Parse Information from GetFeatureInfo request. If result came in XML format, Infopanel data are updated. If response is in HTML, popup window is updated and shown.\n * @param response - Response of GetFeatureInfoRequest\n * @param infoFormat - Format of GetFeatureInfoResponse\n * @param coordinate - Coordinate of request\n * @param layer - Target layer\n */\n featureInfoReceived(\n response: string,\n infoFormat: string,\n coordinate: number[],\n layer: Layer<Source>,\n ): void {\n if (infoFormat.includes('xml') || infoFormat.includes('gml')) {\n const parser = new WMSGetFeatureInfo();\n const features = parser.readFeatures(response);\n this.parseXmlResponse(features, layer);\n }\n if (infoFormat.includes('html')) {\n if (response.length <= 1) {\n this.infoCounter--;\n if (this.infoCounter === 0) {\n this.queriesCollected(coordinate);\n }\n return;\n }\n if (getFeatureInfoTarget(layer) === 'info-panel') {\n this.hsQueryBaseService.pushFeatureInfoHtml(response);\n } else {\n this.hsQueryBaseService.fillIframeAndResize(\n response,\n this.hsQueryBaseService.multiWmsQuery,\n );\n if (getPopupClass(layer) != undefined) {\n this.hsQueryBaseService.popupClassname =\n 'ol-popup ' + getPopupClass(layer);\n }\n }\n }\n if (infoFormat.includes('json')) {\n this.parseJSONResponse(JSON.parse(response), layer);\n }\n this.infoCounter--;\n if (this.infoCounter === 0) {\n this.queriesCollected(coordinate);\n }\n }\n\n /**\n * Parse Information from JSON based GetFeatureInfo response.\n * @param response - jsonGetFeatureInfo\n * @param layer - Target layer\n */\n parseJSONResponse(response: jsonGetFeatureInfo, layer: Layer<Source>) {\n for (const feature of response.features) {\n const group = {\n name: 'Feature',\n layer: getLayerName(layer),\n attributes: Object.entries(feature.properties).map(([key, value]) => {\n return {\n 'name': key,\n 'value': value,\n };\n }),\n feature: feature,\n stats: [],\n };\n this.hsQueryBaseService.setFeatures(group);\n }\n }\n\n /**\n * Parse Information from XML based GetFeatureInfo response.\n * @param features - Parsed features\n * @param layer - Target layer\n */\n parseXmlResponse(features: Feature<Geometry>[], layer: Layer<Source>): void {\n let updated = false;\n features.forEach((feature) => {\n /**\n * TODO: Layered response need to be refactored as well but I haven't found an example yet\n * so I don't really know how to handle those. Multiple layers in one request are handled by loop\n */\n //const layerName = getTitle(layer) || getName(layer);\n // const layers = feature.getElementsByTagName('Layer');\n // for (const fioLayer of Array.from(layers)) {\n // const featureName = fioLayer.attributes[0].nodeValue;\n // const attrs = fioLayer.getElementsByTagName('Attribute');\n // const attributes = [];\n // for (const attr of Array.from(attrs)) {\n // attributes.push({\n // 'name': attr.attributes[0].nodeValue,\n // 'value': attr.innerHTML,\n // });\n // }\n // const group = {\n // layer: layerName,\n // name: featureName,\n // attributes,\n // customInfoTemplate,\n // };\n // this.updateFeatureList(updated, group);\n // }\n const geometryName = feature.getGeometryName();\n const group = {\n name: 'Feature',\n layer: getLayerName(layer),\n attributes: Object.entries(feature.getProperties())\n .filter((attr) => attr[0] !== geometryName)\n .map(([key, value]) => {\n updated = true;\n return {\n 'name': key,\n 'value': value,\n };\n }),\n feature: feature,\n stats: [],\n };\n if (updated) {\n this.hsQueryBaseService.setFeatures(group);\n }\n });\n }\n\n /**\n * Acknowledge that queries for clicked coordinates have been collected\n * @param coordinate - Clicked coordinates\n */\n queriesCollected(coordinate: number[]): void {\n this.hsQueryBaseService.multiWmsQuery = false;\n this.hsQueryBaseService.wmsFeatureInfoLoading = false;\n\n const invisiblePopup: any = this.hsQueryBaseService.getInvisiblePopup();\n if (\n this.hsQueryBaseService.features.length > 0 ||\n invisiblePopup.contentDocument.body.innerHTML.length > 30\n ) {\n this.hsQueryBaseService.getFeatureInfoCollected.next(coordinate);\n }\n }\n\n /**\n * Get FeatureInfo from WMS queryable layer (only if format of response is XML/GML/HTML). Use hs.query.service_getwmsfeatureinfo service for request and parsing response.\n * @param layer - Layer to Query\n * @param coordinate - Clicked coordinates\n */\n queryWmsLayer(\n layer: Layer<TileWMS | ImageWMS | WMTS>,\n coordinate: number[],\n ): void {\n if (this.isLayerWmsQueryable(layer)) {\n /*\n * Reset info panel before new request/set of requests.\n * To prevent appending to previous query results\n */\n if (this.infoCounter === 0) {\n const invisiblePopup = this.hsQueryBaseService.getInvisiblePopup();\n if (invisiblePopup) {\n invisiblePopup.contentDocument.body.innerHTML = '';\n }\n if (getFeatureInfoTarget(layer) === 'info-panel') {\n this.hsQueryBaseService.wmsFeatureInfoLoading = true;\n }\n this.hsQueryBaseService.featureInfoHtmls = [];\n }\n if (instOf(layer.getSource(), WMTS)) {\n this.hsQueryWmtsService\n .parseRequestURL(layer as Layer<WMTS>, coordinate)\n .then((res) => {\n this.request(res.url, res.format, coordinate, layer);\n });\n return;\n }\n\n let source: ImageWMS | TileWMS;\n if (instOf(layer.getSource(), ImageWMS)) {\n source = layer.getSource() as ImageWMS;\n } else if (instOf(layer.getSource(), TileWMS)) {\n source = layer.getSource() as TileWMS;\n }\n\n const info_format = source.getParams().INFO_FORMAT;\n const map = this.hsMapService.getMap();\n const viewResolution = map.getView().getResolution();\n let url = source.getFeatureInfoUrl(\n coordinate,\n viewResolution,\n source.getProjection()\n ? source.getProjection()\n : this.hsMapService.getCurrentProj(),\n {\n INFO_FORMAT: info_format,\n /**\n * FIXME: Might return multiple results for the same layer not always 1 of each\n */\n feature_count: source.getParams().LAYERS.split(',').length || 1,\n },\n );\n if (\n getFeatureInfoLang(layer) &&\n getFeatureInfoLang(layer)[this.hsLanguageService.language]\n ) {\n if (instOf(source, TileWMS)) {\n url = url.replace(\n (source as TileWMS).getUrls()[0],\n getFeatureInfoLang(layer)[this.hsLanguageService.language],\n );\n } else {\n url = url.replace(\n (source as ImageWMS).getUrl(),\n getFeatureInfoLang(layer)[this.hsLanguageService.language],\n );\n }\n }\n if (url) {\n //this.hsLogService.log(url);\n if (\n ['xml', 'html', 'json', 'gml'].some((o) => info_format.includes(o))\n ) {\n this.request(url, info_format, coordinate, layer);\n }\n }\n }\n }\n\n /**\n * Check if the selected layer is queryable\n * @param layer - Layer selected\n * @returns True if the layer is queryable, false otherwise\n */\n isLayerWmsQueryable(layer: Layer<ImageWMS | TileWMS | WMTS>): boolean {\n if (!layer.getVisible()) {\n return false;\n }\n if (instOf(layer, Tile)) {\n if (\n instOf(layer.getSource(), TileWMS) &&\n (layer.getSource() as TileWMS).getParams().INFO_FORMAT\n ) {\n return true;\n }\n if (instOf(layer.getSource(), WMTS) && getInfoFormat(layer)) {\n return true;\n }\n }\n if (\n instOf(layer, ImageLayer) &&\n instOf(layer.getSource(), ImageWMS) &&\n (layer.getSource() as ImageWMS).getParams().INFO_FORMAT\n ) {\n return true;\n }\n return false;\n }\n}\n","import {InjectionToken, Type} from '@angular/core';\n\nexport const QUERY_INFO_PANEL = new InjectionToken<Type<any>>(\n 'QUERY_INFO_PANEL',\n);\n","//TODO: Check if this import is still needed. Breaks production though\n//import 'ol-popup/src/ol-popup.css';\nimport {AsyncPipe, NgClass, NgComponentOutlet} from '@angular/common';\nimport {Component, Injector, OnInit, inject} from '@angular/core';\nimport {FormsModule} from '@angular/forms';\nimport {Subject} from 'rxjs';\nimport {debounceTime, takeUntil} from 'rxjs/operators';\nimport {takeUntilDestroyed} from '@angular/core/rxjs-interop';\nimport {TranslatePipe} from '@ngx-translate/core';\n\nimport Popup from 'ol-popup';\n\nimport {HsDrawService} from 'hslayers-ng/services/draw';\nimport {HsLogService} from 'hslayers-ng/services/log';\nimport {HsMapService} from 'hslayers-ng/services/map';\nimport {\n HsPanelBaseComponent,\n HsPanelHeaderComponent,\n} from 'hslayers-ng/common/panels';\nimport {\n HsQueryBaseService,\n HsQueryVectorService,\n} from 'hslayers-ng/services/query';\nimport {HsQueryDefaultInfoPanelBodyComponent} from './default-info-panel-body/default-info-panel-body.component';\nimport {HsQueryWmsService} from './query-wms.service';\nimport {QUERY_INFO_PANEL} from './query.tokens';\n\n@Component({\n selector: 'hs-query',\n templateUrl: './query.component.html',\n imports: [\n TranslatePipe,\n HsPanelHeaderComponent,\n AsyncPipe,\n NgClass,\n FormsModule,\n NgComponentOutlet,\n ],\n})\nexport class HsQueryComponent extends HsPanelBaseComponent implements OnInit {\n hsQueryBaseService = inject(HsQueryBaseService);\n private hsMapService = inject(HsMapService);\n private hsLog = inject(HsLogService);\n private hsQueryVectorService = inject(HsQueryVectorService);\n private hsDrawService = inject(HsDrawService);\n private hsQueryWmsService = inject(HsQueryWmsService);\n // Inject QUERY_INFO_PANEL if available, otherwise fallback to HsQueryDefaultInfoPanelBodyComponent\n infoPanelComponent =\n inject(QUERY_INFO_PANEL, {optional: true}) ||\n HsQueryDefaultInfoPanelBodyComponent;\n injector = inject(Injector);\n\n popup = new Popup();\n popupOpens: Subject<any> = new Subject();\n name = 'query';\n //To deactivate queries (unsubscribe subscribers) per app\n queryDeactivator = new Subject<void>();\n\n async ngOnInit() {\n super.ngOnInit();\n this.popupOpens\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe((source) => {\n if (source && source != 'hs.query' && this.popup !== undefined) {\n this.popup.hide();\n }\n });\n\n this.hsQueryVectorService.featureRemovals\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe((feature) => {\n const featureIdxInQuery = this.hsQueryBaseService.features\n .map((fDescription) => fDescription.feature)\n .filter((f) => f)\n .indexOf(feature);\n if (featureIdxInQuery < 0) {\n // should not happen as the hsQueryBaseService.features array can't be manipulated from other place in the GUI,\n // but it could be done programmatically\n return;\n }\n this.hsQueryBaseService.features.splice(featureIdxInQuery, 1);\n });\n this.hsMapService.loaded().then((map) => {\n map.addOverlay(this.popup);\n });\n //add current panel queryable - activate/deactivate\n this.hsLayoutService.mainpanel$\n .pipe(debounceTime(250), takeUntilDestroyed(this.destroyRef))\n .subscribe((which) => {\n if (this.hsQueryBaseService.currentPanelQueryable()) {\n if (\n !this.hsQueryBaseService.queryActive &&\n !this.hsDrawService.drawActive\n ) {\n this.hsQueryBaseService.activateQueries();\n }\n const panelSpace = this.hsLayoutService.contentWrapper.querySelector(\n '.hs-panelspace',\n ) as HTMLElement;\n if (panelSpace) {\n panelSpace.classList.remove('panel-collapsed');\n }\n } else {\n if (this.hsQueryBaseService.queryActive) {\n this.hsQueryBaseService.deactivateQueries();\n }\n }\n });\n this.hsQueryBaseService.queryStatusChanges\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe((status) => {\n this.queryStatusChanged(status);\n });\n const active = this.hsQueryBaseService.queryActive;\n if (active) {\n this.queryStatusChanged(active);\n }\n }\n\n /**\n * Act on query status changes\n * @param active - Query status state\n */\n queryStatusChanged(active: boolean): void {\n if (!active) {\n this.queryDeactivator.next();\n return;\n }\n this.hsQueryBaseService.getFeatureInfoStarted\n .pipe(takeUntilDestroyed(this.destroyRef))\n .pipe(takeUntil(this.queryDeactivator))\n .subscribe((evt) => {\n this.popup.hide();\n if (\n this.hsQueryBaseService.currentPanelQueryable() &&\n this.hsLayoutService.mainpanel != 'draw'\n ) {\n this.hsLayoutService.setMainPanel('query');\n }\n });\n\n this.hsQueryBaseService.getFeatureInfoCollected\n .pipe(takeUntilDestroyed(this.destroyRef))\n .pipe(takeUntil(this.queryDeactivator))\n .subscribe((coordinate) => {\n const invisiblePopup: HTMLIFrameElement =\n this.hsQueryBaseService.getInvisiblePopup();\n if (!invisiblePopup) {\n return;\n }\n const bodyElementsFound = this.checkForBodyElements(\n invisiblePopup.contentDocument.body.children,\n );\n if (!bodyElementsFound) {\n return;\n }\n //TODO: don't count style, title, meta towards length\n if (this.hsQueryBaseService.popupClassname.length > 0) {\n this.popup.getElement().className =\n this.hsQueryBaseService.popupClassname;\n } else {\n this.popup.getElement().className = 'ol-popup';\n }\n if (!coordinate) {\n //FIXME: why setting empty coordinates for pop-up?\n this.hsLog.log('empty coordinates for', this.popup);\n } else {\n this.popup.show(\n coordinate,\n invisiblePopup.contentDocument.body.innerHTML,\n );\n this.popupOpens.next('hs.query');\n }\n });\n }\n\n /**\n * Check if popup HTML body contains valid elements\n * @param docChildren - Popup HTML collection\n * @returns True or false\n */\n checkForBodyElements(docChildren: HTMLCollection): boolean {\n return Array.from(docChildren).some((ch: any) => {\n if (ch.tagName == 'TITLE' && ch.title == '') {\n return false;\n }\n return (\n ch.tagName != 'SERVICEEXCEPTIONREPORT' &&\n ch.tagName != 'META' &&\n ch.tagName != 'STYLE'\n );\n });\n }\n\n /**\n * Ch