@geogirafe/lib-geoportal
Version:
GeoGirafe is a flexible application to build online geoportals.
176 lines (175 loc) • 6.42 kB
JavaScript
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];
}
}