diff --git a/src/featureinfo.js b/src/featureinfo.js index 6908ef445..d8b835d45 100644 --- a/src/featureinfo.js +++ b/src/featureinfo.js @@ -38,6 +38,7 @@ const Featureinfo = function Featureinfo(options = {}) { let popup; let viewer; let selectionManager; + let textHtmlHandler; /** The featureinfo component itself */ let component; @@ -296,6 +297,10 @@ const Featureinfo = function Featureinfo(options = {}) { } }; + const addTextHtmlHandler = function addTextHtmlHandler(func) { + textHtmlHandler = func; + }; + /** * Creates temporary attributes on a feature in order for featureinfo to display attributes from related tables and * display attachments as links. Recursively adds attributes to related features in order to support multi level relations. @@ -653,7 +658,7 @@ const Featureinfo = function Featureinfo(options = {}) { coordinate, map, pixel - }, viewer) + }, viewer, textHtmlHandler) .then((data) => { const serverResult = data || []; const result = serverResult.concat(clientResult); @@ -696,6 +701,7 @@ const Featureinfo = function Featureinfo(options = {}) { getSelectionLayer, getSelection, addAttributeType, + addTextHtmlHandler, onAdd(e) { // Keep a reference to "ourselves" component = this; diff --git a/src/getfeatureinfo.js b/src/getfeatureinfo.js index ed51bb565..503d533e1 100644 --- a/src/getfeatureinfo.js +++ b/src/getfeatureinfo.js @@ -1,6 +1,7 @@ import EsriJSON from 'ol/format/EsriJSON'; import BaseTileLayer from 'ol/layer/BaseTile'; import ImageLayer from 'ol/layer/Image'; +import infoTemplates from './featureinfotemplates'; import maputils from './maputils'; import SelectedItem from './models/SelectedItem'; @@ -53,60 +54,135 @@ function createSelectedItem(feature, layer, map, groupLayers) { return new SelectedItem(feature, layer, map, selectionGroup, selectionGroupTitle); } -function getFeatureInfoUrl({ +async function getFeatureInfoUrl({ coordinate, resolution, projection -}, layer) { +}, layer, viewer, textHtmlHandler) { + if (layer.get('infoFormat') === 'text/html') { + const mapSource = viewer.getMapSource(); + const sourceName = layer.get('sourceName'); + const WMSServerType = mapSource[sourceName].type.toLowerCase(); + + const supportedWMSServerTypes = ['geoserver']; + + if ((!WMSServerType) || (!supportedWMSServerTypes.includes(WMSServerType))) { + return []; + } + // may be provided via featureinfo.js: addTextHtmlHandler(function) via viewer/api: getFeatureinfo() + const htmlHandler = textHtmlHandler || function htmlHandler({ vendor, lyr, htmlDOM }) { + if (vendor === 'geoserver') { + const handleTag = lyr.get('htmlSeparator')?.toUpperCase() || null; + if (handleTag) { + return Array.from(htmlDOM.body.children).filter(child => child.tagName === handleTag); + } + return [htmlDOM]; + } return []; + }; + + infoTemplates.addFeatureinfotemplate('textHtml', attributes => attributes.textHtml); + + let json; + if (!layer.get('htmlSeparator')) { + json = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: coordinate + }, + layerName: layer.get('name') + } + ] + }; + } else { + const jsonRequestParamObj = { + INFO_FORMAT: 'application/json', + FEATURE_COUNT: '20' + }; + + const jsonUrlString = layer.getSource().getFeatureInfoUrl(coordinate, resolution, projection, jsonRequestParamObj); + const jsonResponse = await fetch(jsonUrlString, { method: 'GET' }); + json = await jsonResponse.json(); + } + + const featureCollection = maputils.geojsonToFeature(json); + + const textFeatureInfoUrlString = layer.getSource().getFeatureInfoUrl(coordinate, resolution, projection, { + INFO_FORMAT: 'text/html', + FEATURE_COUNT: '20' + }); + + const htmlResponse = await fetch(textFeatureInfoUrlString, { method: 'GET' }); + const html = await htmlResponse.text(); + const htmlDOM = new DOMParser().parseFromString(html, 'text/html'); + + const elementArray = htmlHandler({ + vendor: WMSServerType.toLowerCase(), + lyr: layer, + htmlDOM + }); + + if (elementArray[0]?.body?.children?.length === 0) return []; + + const features = elementArray.map((element, index) => { + let feature; + let htmlfeat; + // case no htmlSeparator prop: show same dot geometry for all hits + // and put the documentElement of the response within + if (!layer.get('htmlSeparator')) { + feature = featureCollection[0]; + htmlfeat = ` ${element.documentElement.outerHTML} `; + } else { + feature = featureCollection[index]; + htmlfeat = ` ${htmlDOM.head.outerHTML} ${element.outerHTML} `; + } + feature.set('textHtml', htmlfeat); + return feature; + }); + layer.set('attributes', 'textHtml'); + return features; + } + if (layer.get('infoFormat') === 'application/geo+json' || layer.get('infoFormat') === 'application/geojson') { const url = layer.getSource().getFeatureInfoUrl(coordinate, resolution, projection, { INFO_FORMAT: layer.get('infoFormat'), FEATURE_COUNT: '20' }); - - return fetch(url, { type: 'GET' }) - .then((res) => { - if (res.error) { - return []; - } - return res.text(); - }) - .then(text => { - let json = {}; - try { - json = JSON.parse(text); - } catch (error) { - if (error instanceof SyntaxError) { - // Maybe bad escaped character, retry with escaping backslash - json = JSON.parse(text.replaceAll('\\', '\\\\')); - } else { - console.error(error); - } - } - if (json.features.length > 0) { - const copyJson = json; - copyJson.features.forEach((item, i) => { - if (!item.geometry) { - copyJson.features[i].geometry = { type: 'Point', coordinates: coordinate }; - } - }); - const feature = maputils.geojsonToFeature(copyJson); - return feature; + const res = await fetch(url, { method: 'GET' }); + const text = await res.text(); + let json = {}; + try { + json = JSON.parse(text); + } catch (error) { + if (error instanceof SyntaxError) { + json = JSON.parse(text.replaceAll('\\', '\\\\')); + } else { + console.error(error); + } + } + if (json.features.length > 0) { + const copyJson = json; + copyJson.features.forEach((item, i) => { + if (!item.geometry) { + copyJson.features[i].geometry = { type: 'Point', coordinates: coordinate }; } - return []; - }) - .catch(error => console.error(error)); + }); + const feature = maputils.geojsonToFeature(copyJson); + return feature; + } + return []; } + const url = layer.getSource().getFeatureInfoUrl(coordinate, resolution, projection, { INFO_FORMAT: 'application/json', FEATURE_COUNT: '20' }); - return fetch(url, { type: 'GET' }).then((res) => { - if (res.error) { - return []; - } - return res.json(); - }).then(json => maputils.geojsonToFeature(json)).catch(error => console.error(error)); + const res = await fetch(url, { method: 'GET' }); + const json = await res.json(); + return maputils.geojsonToFeature(json); } function getAGSIdentifyUrl({ layer, coordinate }, viewer) { @@ -148,7 +224,7 @@ function getAGSIdentifyUrl({ layer, coordinate }, viewer) { }).catch(error => console.error(error)); } -function getGetFeatureInfoRequest({ layer, coordinate }, viewer) { +function getGetFeatureInfoRequest({ layer, coordinate }, viewer, textHtmlHandler) { const layerType = layer.get('type'); const obj = {}; const projection = viewer.getProjection(); @@ -170,7 +246,7 @@ function getGetFeatureInfoRequest({ layer, coordinate }, viewer) { return getGetFeatureInfoRequest({ layer: featureinfoLayer, coordinate }, viewer); } obj.cb = 'GEOJSON'; - obj.fn = getFeatureInfoUrl({ coordinate, resolution, projection }, layer); + obj.fn = getFeatureInfoUrl({ coordinate, resolution, projection }, layer, viewer, textHtmlHandler); return obj; case 'AGS_TILE': if (layer.get('featureinfoLayer')) { @@ -191,7 +267,7 @@ function getFeatureInfoRequests({ coordinate, pixel, layers -}, viewer) { +}, viewer, textHtmlHandler) { const imageFeatureInfoMode = viewer.getViewerOptions().featureinfoOptions.imageFeatureInfoMode || 'pixel'; const requests = []; let queryableLayers; @@ -229,12 +305,12 @@ function getFeatureInfoRequests({ if (imageInfoMode === 'pixel') { const pixelVal = layer.getData(pixel); if (pixelVal instanceof Uint8ClampedArray && pixelVal[3] > 0) { - item = getGetFeatureInfoRequest({ layer, coordinate }, viewer); + item = getGetFeatureInfoRequest({ layer, coordinate }, viewer, textHtmlHandler); } } else if ((imageInfoMode === 'visible') && (layer.get('visible') === true)) { - item = getGetFeatureInfoRequest({ layer, coordinate }, viewer); + item = getGetFeatureInfoRequest({ layer, coordinate }, viewer, textHtmlHandler); } else if (imageInfoMode === 'always') { - item = getGetFeatureInfoRequest({ layer, coordinate }, viewer); + item = getGetFeatureInfoRequest({ layer, coordinate }, viewer, textHtmlHandler); } if (item) { requests.push(item); @@ -243,10 +319,9 @@ function getFeatureInfoRequests({ return requests; } -function getFeaturesFromRemote(requestOptions, viewer) { +function getFeaturesFromRemote(requestOptions, viewer, textHtmlHandler) { const requestResult = []; - - const requestPromises = getFeatureInfoRequests(requestOptions, viewer).map((request) => request.fn.then((features) => { + const requestPromises = getFeatureInfoRequests(requestOptions, viewer, textHtmlHandler).map((request) => request.fn.then((features) => { const layer = viewer.getLayer(request.layer); const groupLayers = viewer.getGroupLayers(); const map = viewer.getMap();