UNPKG

@geogirafe/lib-geoportal

Version:

GeoGirafe is a flexible application to build online geoportals.

176 lines (175 loc) 6.42 kB
import VectorSource from 'ol/source/Vector.js'; import VectorLayer from 'ol/layer/Vector.js'; import { Circle, Fill, Stroke, Style } from 'ol/style.js'; import { unByKey } from 'ol/Observable.js'; import { asString } from 'ol/color.js'; import { getVectorContext } from 'ol/render.js'; import { easeOut } from 'ol/easing.js'; import { colorToRgbaArray } from '../../../tools/utils/utils.js'; /** * Helps to highlight features on the map by adding an animated * style around the features. */ export class FocusFeature { /** Duration of a flash animation in ms. */ flashDuration = 2000; olMap; configManager; flashStyleCache = {}; focusLayer; focusSource; focusAnimation = null; strokeColor; constructor(map, configManager) { this.olMap = map; this.configManager = configManager; this.createFocusLayer(); } setFocusedFeatures(features) { this.flash(features); } /** * Create animated layer for focussing feature. * @private */ createFocusLayer() { this.focusSource = new VectorSource(); this.focusLayer = new VectorLayer({ properties: { addToPrintedLayers: true }, source: this.focusSource }); this.setFocusLayerStyle(); this.olMap.addLayer(this.focusLayer); this.focusLayer.setZIndex(1004); this.focusLayer.set('altitudeMode', 'clampToGround'); } /** * Set the style of the focus layer. * @private */ setFocusLayerStyle() { const style = new Style({ stroke: new Stroke({ color: this.configManager.Config.selection.highlightStrokeColor, width: this.configManager.Config.selection.defaultStrokeWidth }), fill: new Fill({ color: this.configManager.Config.selection.highlightFillColor }), image: new Circle({ radius: 7, fill: new Fill({ color: this.configManager.Config.selection.highlightFillColor }), stroke: new Stroke({ color: this.configManager.Config.selection.highlightStrokeColor, width: this.configManager.Config.selection.defaultStrokeWidth }) }) }); this.focusLayer.setStyle(style); } /** * Activates the flash animation for the given features. * On null features or empty array, the animation ends. * @private */ flash(features) { // Remove earlier focus features from map this.focusSource?.clear(); // First deactivate the current animation // (We only want one animated object) if (this.focusAnimation !== null) { unByKey(this.focusAnimation); } // In case of deselection (feature is null), exit the loop. if (!features || !this.focusLayer) { return; } const geometries = features.map((feature) => feature.getGeometry()).filter((geometry) => geometry); // No geometry ? Exit the loop. if (!geometries.length) { return; } // Update features in map and their style this.focusSource?.addFeatures(features); this.setFocusLayerStyle(); this.strokeColor = colorToRgbaArray(this.configManager.Config.selection.highlightStrokeColor) || [255, 0, 0, 1]; this.focusAnimation = this.focusLayer.on('postrender', (renderEvent) => { geometries.forEach((geometry) => { const flashGeom = geometry.clone(); this.animate(renderEvent, flashGeom); }); // Tell OpenLayers to continue postrender animation (loop). this.focusLayer.changed(); }); } /** * Redraw the geometry. * @private */ animate(renderEvent, flashGeom) { const vectorContext = getVectorContext(renderEvent); vectorContext.setStyle(this.getFlashStyleCache(renderEvent.frameState)); vectorContext.drawGeometry(flashGeom); } /** * Get the "flash" style at the current frameState.time. * Use cache to not create new style object every N ms! * @returns The "Flash" style. * @private */ getFlashStyleCache(frameState) { const values = FocusFeature.getFlashDynamicValues(frameState?.time ?? 0, this.flashDuration); const cacheKeyName = values.join('-'); const cached = this.flashStyleCache[cacheKeyName]; if (cached) { return cached; } const style = FocusFeature.createFlashStyle(values, this.strokeColor); this.flashStyleCache[cacheKeyName] = style; return style; } /** * @param values - With [radius, opacity, offset] expected. * @param color - stroke color of flash animation * @returns A new "flash" style object based on the given values. * @static */ static createFlashStyle(values, color) { const [radius, opacity, offset] = values; return new Style({ image: new Circle({ radius: radius, stroke: new Stroke({ color: asString([...color.slice(0, 3), opacity]), width: 4 * opacity }) }), stroke: new Stroke({ color: asString(color), width: 12, lineDash: [16, 32], lineDashOffset: offset }) }); } /** * @returns Based on the given frameStateTime, a duration and a easeOut * function (quick to slow), returns values: * from [radius=5, opacity=1, offset=0] * to [radius=30, opacity=0, offset=48] * @static */ static getFlashDynamicValues(frameStateTime, duration) { const elapsed = frameStateTime; // ElapsedRatio, between 0 and 1. const elapsedRatio = (elapsed % duration) / duration; const roundedRatio = Math.round(elapsedRatio * 1000) / 1000; // Radius will be 5 at start and 30 at end. const radius = easeOut(roundedRatio) * 25 + 5; // Opacity will be between 0 and 1. const opacity = easeOut(1 - roundedRatio); // For lines, an offset between 0 and 48. const offset = Math.floor(elapsed / 100) % 48; return [radius, opacity, offset]; } }