Skip to content

Tooltips: sld to ol for 3.9 #5905

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: release_3_9
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions assets/src/components/Tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ export default class Tooltip extends HTMLElement {

this._template = () => html`
<select @change=${ event => { mainLizmap.tooltip.activate(event.target.value) }}>
<option value="">---</option>
<option value="" .selected=${mainLizmap.tooltip.activeLayerOrder === null}>---</option>
${this._tooltipLayersCfgs.map(tooltipLayerCfg =>
html`<option ?selected=${this._tooltipLayersCfgs.length === 1} value="${tooltipLayerCfg.order}">${mainLizmap.state.layersAndGroupsCollection.getLayerByName(tooltipLayerCfg.name).title}</option>`
html`<option
.selected=${tooltipLayerCfg.order === mainLizmap.tooltip.activeLayerOrder}
value="${tooltipLayerCfg.order}">
${mainLizmap.state.layersAndGroupsCollection.getLayerByName(tooltipLayerCfg.name).title}
</option>`
)}
</select>
`;
Expand All @@ -29,12 +33,6 @@ export default class Tooltip extends HTMLElement {
// Activate last selected tooltip layer
mainLizmap.tooltip.activate(this.querySelector('select').value);
}
},
minidockclosed: event => {
if ( event.id === 'tooltip-layer' ) {
// Deactivate tooltip on close
mainLizmap.tooltip.deactivate();
}
}
});

Expand All @@ -52,6 +50,13 @@ export default class Tooltip extends HTMLElement {
['tooltip.loaded']
);

mainEventDispatcher.addListener(
() => {
render(this._template(), this);
},
['tooltip.activated', 'tooltip.deactivated']
);

render(this._template(), this);
}

Expand Down
129 changes: 128 additions & 1 deletion assets/src/modules/Tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
* @copyright 2024 3Liz
* @license MPL-2.0
*/
import { mainEventDispatcher } from '../modules/Globals.js';
import { mainEventDispatcher, mainLizmap } from '../modules/Globals.js';
import { TooltipLayersConfig } from './config/Tooltip.js';
import WMS from '../modules/WMS.js';
import GeoJSON from 'ol/format/GeoJSON.js';
import VectorLayer from 'ol/layer/Vector.js';
import VectorSource from 'ol/source/Vector.js';
import { Circle, Fill, Stroke, Style } from 'ol/style.js';
import { Reader, createOlStyleFunction, getLayer, getStyle } from '@nieuwlandgeo/sldreader/src/index.js';

/**
* @class
Expand All @@ -29,13 +31,22 @@ export default class Tooltip {
this._lizmap3 = lizmap3;
this._activeTooltipLayer;
this._tooltipLayers = new Map();
this.activeLayerOrder = null;

mainLizmap.state.rootMapGroup.addListener(
evt => {
this._applyFilter(evt.name);
},
'layer.filter.token.changed'
);
}

/**
* Activate tooltip for a layer order
* @param {number} layerOrder a layer order
*/
activate(layerOrder) {
// If the layer order is empty, deactivate the tooltip
if (layerOrder === "") {
this.deactivate();
return;
Expand All @@ -48,6 +59,7 @@ export default class Tooltip {
const layerName = layerTooltipCfg.name;
const tooltipLayer = this._tooltipLayers.get(layerName);
this._displayGeom = layerTooltipCfg.displayGeom;
this._displayLayerStyle = true;

// Styles
const fill = new Fill({
Expand Down Expand Up @@ -98,6 +110,7 @@ export default class Tooltip {

if (tooltipLayer) {
this._activeTooltipLayer = tooltipLayer;
this._applyFilter(layerName);
} else {
const url = `${lizUrls.service.replace('service?','features/tooltips?')}&layerId=${layerTooltipCfg.id}`;

Expand All @@ -111,16 +124,48 @@ export default class Tooltip {
stroke: stroke,
});

// Initially hidden, will be set to 1 when features are loaded and filter is applied
// to avoid visual flickering
// Using the visible property of the layer does not work
this._activeTooltipLayer = new VectorLayer({
opacity: 0,
source: new VectorSource({
url: url,
format: new GeoJSON(),
}),
style: vectorStyle
});

// Handle points layers with QGIS style
if (this._displayLayerStyle) {
const wmsParams = {
LAYERS: layerName
};

const wms = new WMS();

wms.getStyles(wmsParams).then((response) => {
const sldObject = Reader(response);

const sldLayer = getLayer(sldObject);
const style = getStyle(sldLayer);
const featureTypeStyle = style.featuretypestyles[0];

const olStyleFunction = createOlStyleFunction(featureTypeStyle, {
imageLoadedCallback: () => {
// Signal OpenLayers to redraw the layer when an image icon has loaded.
// On redraw, the updated symbolizer with the correct image scale will be used to draw the icon.
this._activeTooltipLayer.changed();
},
});

this._activeTooltipLayer.setStyle(olStyleFunction);
});
}

// Load tooltip layer
this._activeTooltipLayer.once('sourceready', () => {
this._applyFilter(layerName);
mainEventDispatcher.dispatch('tooltip.loaded');
});

Expand Down Expand Up @@ -168,6 +213,30 @@ export default class Tooltip {
if (this._displayGeom){
feature.setStyle(hoverStyle);
}
// Increase point size on hover
else if (this._displayLayerStyle){
const olStyleFunction = this._activeTooltipLayer.getStyleFunction();
const mapResolution = this._map.getView().getResolution();
const olStyle = olStyleFunction(feature, mapResolution);

const newStyle = [];
for (const style of olStyle) {
const clonedStyle = style.clone();
// If the style is a Circle, increase its radius
// We could increase the scale but pixels are blurry
const newRadius = clonedStyle.getImage().getRadius?.() * 1.5;
if (newRadius) {
clonedStyle.getImage().setRadius(newRadius);
} else {
// If the style is not a Circle, we can still increase the scale
const newScale = clonedStyle.getImage().getScale() * 1.5;
clonedStyle.getImage().setScale(newScale);
}
newStyle.push(clonedStyle);
}

feature.setStyle(newStyle);
}

// Display tooltip
tooltip.style.left = pixel[0] + 'px';
Expand All @@ -191,6 +260,10 @@ export default class Tooltip {
};

this._map.getTargetElement().addEventListener('pointerleave', this._onPointerLeave);

// Dispatch event to notify that the tooltip is activated
this.activeLayerOrder = layerOrder;
mainEventDispatcher.dispatch('tooltip.activated', { layerOrder: layerOrder });
}

/**
Expand All @@ -204,5 +277,59 @@ export default class Tooltip {
if (this._onPointerLeave) {
this._map.getTargetElement().removeEventListener('pointerleave', this._onPointerLeave);
}

// Dispatch event to notify that the tooltip is deactivated
this.activeLayerOrder = null;
mainEventDispatcher.dispatch('tooltip.deactivated');
}

_applyFilter(layerName) {
const tooltipLayer = this._tooltipLayers.get(layerName);

if (!tooltipLayer) {
// No tooltip layer for this feature type
return;
}

const expFilter = mainLizmap.state.rootMapGroup.getMapLayerByName(layerName).itemState.expressionFilter;
let featureIds = [];

const hideFilteredFeatures = () => {
for (const feature of tooltipLayer.getSource().getFeatures()) {
// If the feature id is not in the list, hide it
if (featureIds.length === 0 || featureIds.includes(feature.getId())) {
feature.setStyle(null);
} else {
feature.setStyle(new Style({}));
}
}
// Display the layer now all styles are applied
tooltipLayer.setOpacity(1);
};

if(!expFilter) {
hideFilteredFeatures();
return;
}

if (expFilter.startsWith('$id IN ')) {
const re = /[() ]/g;
featureIds = expFilter.replace('$id IN ', '').replace(re, '').split(',').map(Number);
hideFilteredFeatures();
} else {
const wfsParams = {
TYPENAME: layerName,
// No geometry needed
GEOMETRYNAME: 'none',
// Force to return only the featureId
PROPERTYNAME: 'no_feature_properties',
// Filter
EXP_FILTER: expFilter
};
mainLizmap.wfs.getFeature(wfsParams).then(result => {
featureIds = result.features.map(f => parseInt(f.id.split('.')[1]));
hideFilteredFeatures();
});
}
}
}
26 changes: 26 additions & 0 deletions assets/src/modules/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,32 @@ export class Utils {
}).catch(error => {return Promise.reject(error)});
}

/**
* Fetching a resource from the network, which is XML, returning a promise that resolves with a text representation of the response body.
* @static
* @param {string} resource - This defines the resource that you wish to fetch. A string or any other object with a stringifier — including a URL object — that provides the URL of the resource you want to fetch.
* @param {object} options - An object containing any custom settings you want to apply to the request.
* @returns {Promise} A Promise that resolves with a text representation of the response body.
* @throws {ResponseError} In case of invalid content type (not text/xml)
* @throws {HttpError} In case of not successful response (status not in the range 200 – 299)
* @throws {NetworkError} In case of catch exceptions
* @see https://developer.mozilla.org/en-US/docs/Web/API/fetch
* @see https://developer.mozilla.org/en-US/docs/Web/API/Response
*/
static fetchXML(resource, options) {
return Utils.fetch(resource, options).then(response => {
const contentType = response.headers.get('Content-Type') || '';

if (contentType.includes('text/xml')) {
return response.text().catch(error => {
return Promise.reject(new ResponseError('XML error: ' + error.message, response, resource, options));
});
}

return Promise.reject(new ResponseError('Invalid content type: ' + contentType, response, resource, options));
}).catch(error => {return Promise.reject(error)});
}

/**
* Get the corresponding resolution for the scale with meters per unit
* @static
Expand Down
24 changes: 24 additions & 0 deletions assets/src/modules/WMS.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ export default class WMS {
VERSION: '1.3.0',
FORMAT: 'application/json',
};

this._defaultGetStylesParameters = {
repository: globalThis['lizUrls'].params.repository,
project: globalThis['lizUrls'].params.project,
SERVICE: 'WMS',
REQUEST: 'GetStyles',
VERSION: '1.1.1',
};
}

/**
Expand Down Expand Up @@ -86,4 +94,20 @@ export default class WMS {
body: params,
});
}

/**
* Get styles from WMS
* @param {object} options - optional parameters which can override this._defaultGetStylesParameters
* @returns {Promise} Promise object represents data
* @memberof WMS
*/
async getStyles(options) {
return Utils.fetchXML(globalThis['lizUrls'].wms, {
method: "POST",
body: new URLSearchParams({
...this._defaultGetStylesParameters,
...options
})
});
}
}
9 changes: 9 additions & 0 deletions assets/src/modules/config/Tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { BaseObjectLayerConfig, BaseObjectLayersConfig } from './BaseObject.js';
const requiredProperties = {
'layerId': {type: 'string'},
'displayGeom': {type: 'boolean'},
'displayLayerStyle': {type: 'boolean'},
'order': {type: 'number'},
};

Expand Down Expand Up @@ -51,6 +52,14 @@ export class TooltipLayerConfig extends BaseObjectLayerConfig {
return this._displayGeom;
}

/**
* The layer style will be displayed
* @type {boolean}
*/
get displayLayerStyle() {
return this._displayLayerStyle;
}

/**
* The feature's geometry color
* @type {string}
Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
},
"devDependencies": {
"@eslint/js": "^9.30.x",
"@nieuwlandgeo/sldreader": "^0.6.2",
"@playwright/test": "^1.53.x",
"@rspack/cli": "^1.4.1",
"@rspack/core": "^1.4.1",
Expand Down
Loading
Loading