Skip to content

Commit

Permalink
feat: add text/html support for geoserver (#1955)
Browse files Browse the repository at this point in the history
* feat: add prospective text/html support for qgis- and geoserver

* revise: remove qgis support and make clearer where to extend if in future other vendors are to be supported

* Update lint.yml

* fix: update eslint rules to reflect current master

* fix: make text/html handling halfway user configurable
  • Loading branch information
Grammostola authored Feb 15, 2024
1 parent 4dae0b9 commit 1009814
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 50 deletions.
8 changes: 7 additions & 1 deletion src/featureinfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const Featureinfo = function Featureinfo(options = {}) {
let popup;
let viewer;
let selectionManager;
let textHtmlHandler;
/** The featureinfo component itself */
let component;

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -696,6 +701,7 @@ const Featureinfo = function Featureinfo(options = {}) {
getSelectionLayer,
getSelection,
addAttributeType,
addTextHtmlHandler,
onAdd(e) {
// Keep a reference to "ourselves"
component = this;
Expand Down
173 changes: 124 additions & 49 deletions src/getfeatureinfo.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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 <html>
if (!layer.get('htmlSeparator')) {
feature = featureCollection[0];
htmlfeat = `<html> ${element.documentElement.outerHTML} </html>`;
} else {
feature = featureCollection[index];
htmlfeat = `<html> ${htmlDOM.head.outerHTML} <body> ${element.outerHTML} </body> </html>`;
}
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) {
Expand Down Expand Up @@ -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();
Expand All @@ -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')) {
Expand All @@ -191,7 +267,7 @@ function getFeatureInfoRequests({
coordinate,
pixel,
layers
}, viewer) {
}, viewer, textHtmlHandler) {
const imageFeatureInfoMode = viewer.getViewerOptions().featureinfoOptions.imageFeatureInfoMode || 'pixel';
const requests = [];
let queryableLayers;
Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand Down

0 comments on commit 1009814

Please sign in to comment.