mobility-toolbox-js
Version:
Toolbox for JavaScript applications in the domains of mobility and logistics.
512 lines (511 loc) • 20.2 kB
JavaScript
import debounce from 'lodash.debounce';
import { Layer } from 'ol/layer';
import { unByKey } from 'ol/Observable';
import { Source } from 'ol/source';
import { VECTOR_TILE_FEATURE_PROPERTY } from '../../common';
import MaplibreStyleLayerRenderer from '../renderers/MaplibreStyleLayerRenderer';
import defineDeprecatedProperties from '../utils/defineDeprecatedProperties';
let deprecated = () => { };
if (typeof window !== 'undefined' &&
new URLSearchParams(window.location.search).get('deprecated')) {
deprecated = debounce((...messages) => {
// eslint-disable-next-line no-console
console.warn(...messages);
}, 1000);
}
/**
* Layer that helps show/hide a specific subset of style layers of a [MaplibreLayer](./MaplibreLayer.js~MaplibreLayer.html).
*
* @example
* import { MaplibreLayer, MocoLayer } from 'mobility-toolbox-js/ol';
*
* const maplibreLayer = new MaplibreLayer({
* apiKey: 'yourApiKey',
* });
*
* const layer = new MocoLayer({
* maplibreLayer: maplibreLayer,
* layersFilter: (layer) => {
* // show/hide only style layers related to stations
* return /station/.test(layer.id);
* },
* });
*
* @extends {ol/layer/Layer~Layer}
* @public
*/
class MaplibreStyleLayer extends Layer {
get beforeId() {
return this.get('beforeId');
}
set beforeId(newValue) {
this.set('beforeId', newValue);
}
get layers() {
return this.get('layers') || [];
}
set layers(newValue) {
this.set('layers', newValue);
}
get layersFilter() {
return this.get('layersFilter');
}
set layersFilter(newValue) {
this.set('layersFilter', newValue);
}
/**
* @deprecated Use MaplibreStyleLayer.maplibreLayer instead.
*/
get mapboxLayer() {
deprecated('Deprecated. Use maplibreLayer instead.');
return this.get('maplibreLayer');
}
get maplibreLayer() {
return this.get('maplibreLayer');
}
set maplibreLayer(newValue) {
this.set('maplibreLayer', newValue);
}
get queryRenderedLayersFilter() {
return this.get('queryRenderedLayersFilter');
}
set queryRenderedLayersFilter(newValue) {
this.set('queryRenderedLayersFilter', newValue);
}
get sources() {
return this.get('sources');
}
set sources(newValue) {
this.set('sources', newValue);
}
/**
* @deprecated Use MaplibreStyleLayer.layer instead.
*/
get styleLayer() {
deprecated('Deprecated. Use MaplibreStyleLayer.layer instead.');
return this.layers[0];
}
/**
* @deprecated
*/
set styleLayer(newValue) {
deprecated('MaplibreStyleLayer.styleLayer is deprecated. Use MaplibreStyleLayer.layer instead.');
this.layers = [newValue];
}
/**
* Apply visibility to style layers that fits the styleLayersFilter function.
*/
/**
* @deprecated
*/
get styleLayers() {
deprecated('MaplibreStyleLayer.styleLayers is deprecated. Use MaplibreStyleLayer.layers instead.');
return this.layers;
}
/**
* @deprecated
*/
set styleLayers(newValue) {
deprecated('MaplibreStyleLayer.styleLayers is deprecated. Use MaplibreStyleLayer.layers instead.');
this.layers = newValue;
}
/**
* Constructor.
*
* @param {Object} options
* @param {string} [options.beforeId] The style layer id to use when the options.layers property is defined, unsused otherwise.
* @param {maplibregl.AddLayerObject[]} [options.layers] The layers to add to the style on load.
* @param {FilterFunction} [options.layersFilter] Filter function to decide which style layer to apply visiblity on. If not provided, the 'layers' property is used.
* @param {MaplibreLayer} [options.maplibreLayer] The MaplibreLayer to use.
* @param {FilterFunction} [options.queryRenderedLayersFilter] Filter function to decide which style layer are available for query.
* @param {{[id: string]:maplibregl.SourceSpecification}} [options.sources] The sources to add to the style on load.
* @public
*/
constructor(options = {
mapLibreOptions: { style: { layers: [], sources: {}, version: 8 } },
}) {
/** Manage renamed property for backward compatibility with v2 */
if (options.mapboxLayer) {
deprecated('options.mapboxLayer is deprecated. Use options.maplibreLayer instead.');
// @ts-expect-error - mapboxLayer is deprecated
options.maplibreLayer = options.mapboxLayer;
delete options.mapboxLayer;
}
if (options.styleLayer) {
deprecated('options.styleLayer is deprecated. Use options.layers instead.');
options.layers = [options.styleLayer];
delete options.styleLayer;
}
if (options.styleLayers) {
deprecated('options.styleLayers is deprecated. Use options.layers instead.');
options.layers = options.styleLayers;
delete options.styleLayers;
}
if (options.styleLayersFilter) {
deprecated('options.styleLayersFilter is deprecated. Use options.layersFilter instead.');
options.layersFilter = options.styleLayersFilter;
delete options.styleLayersFilter;
}
super(Object.assign({ source: new Source({}) }, options));
this.highlightedFeatures = [];
this.olEventsKeys = [];
this.selectedFeatures = [];
// For backward compatibility with v2
defineDeprecatedProperties(this, options);
// For cloning
this.set('options', options);
this.beforeId = options.beforeId;
this.onLoad = this.onLoad.bind(this);
if (!this.layersFilter && this.layers) {
this.layersFilter = (layer) => {
return !!this.layers.find((l) => {
return layer.id === l.id;
});
};
}
}
addLayers() {
var _a;
if (!((_a = this.maplibreLayer) === null || _a === void 0 ? void 0 : _a.mapLibreMap) || !Array.isArray(this.layers)) {
return;
}
const { mapLibreMap } = this.maplibreLayer;
if (mapLibreMap) {
this.layers.forEach((layer) => {
// @ts-expect-error source is optional but exists in TS definition
const { id, source } = layer;
if ((!source || (source && mapLibreMap.getSource(source))) &&
id &&
!mapLibreMap.getLayer(id)) {
mapLibreMap.addLayer(layer, this.beforeId);
}
});
this.applyLayoutVisibility();
}
}
addSources() {
var _a;
if (!((_a = this.maplibreLayer) === null || _a === void 0 ? void 0 : _a.mapLibreMap) || !this.sources) {
return;
}
const { mapLibreMap } = this.maplibreLayer;
if (mapLibreMap) {
Object.entries(this.sources).forEach(([id, source]) => {
if (!mapLibreMap.getSource(id)) {
mapLibreMap.addSource(id, source);
}
});
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
applyLayoutVisibility(evt) {
var _a, _b;
if (!((_b = (_a = this.maplibreLayer) === null || _a === void 0 ? void 0 : _a.mapLibreMap) === null || _b === void 0 ? void 0 : _b.getStyle()) || !this.layersFilter) {
return;
}
const { mapLibreMap } = this.maplibreLayer;
const style = mapLibreMap.getStyle();
const visibilityValue = this.getVisible() ? 'visible' : 'none';
const layers = style.layers || [];
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < layers.length; i += 1) {
const layer = layers[i];
if (this.layersFilter(layer)) {
const { id } = layer;
if (mapLibreMap.getLayer(id)) {
mapLibreMap.setLayoutProperty(id, 'visibility', visibilityValue);
// OL sets -Infinity, Infinity for minZoom and maxZoom.
if (Number.isFinite(this.getMinZoom()) ||
Number.isFinite(this.getMaxZoom())) {
mapLibreMap.setLayerZoomRange(id, Number.isFinite(this.getMinZoom()) ? this.getMinZoom() - 1 : 0, // Maplibre zoom = ol zoom - 1
Number.isFinite(this.getMaxZoom()) ? this.getMaxZoom() - 1 : 24);
}
}
}
}
}
/**
* Initialize the layer.
* @param {ol/Map~Map} map the Maplibre map.
* @override
*/
attachToMap(map) {
const mapInternal = this.getMapInternal();
if (!mapInternal || !this.maplibreLayer) {
return;
}
// Apply the initial visibility if possible otherwise we wait for the load event of the layer
const { mapLibreMap } = this.maplibreLayer;
if (mapLibreMap) {
// mapLibreMap.loaded() and mapLibreMap.isStyleLoaded() are reliable only on the first call of init.
// On the next call (when a topic change for example), these functions returns false because
// the style is being modified.
// That's why we rely on a property instead for the next calls.
if (mapLibreMap.loaded()) {
this.onLoad();
}
else {
if (mapLibreMap.isStyleLoaded()) {
this.onLoad();
}
else {
void mapLibreMap.once('load', this.onLoad);
}
}
}
// Apply the visibiltity when layer's visibility change.
this.olEventsKeys.push(
// @ts-expect-error 'load' is a custom event
this.maplibreLayer.on('load', this.onLoad.bind(this)), this.on('change:visible', (evt) => {
// Once the map is loaded we can apply visiblity without waiting
// the style. Maplibre take care of the application of style changes.
this.applyLayoutVisibility(evt);
}), this.on('propertychange', (evt) => {
if (/(sources|layers|layersFilter|maplibreLayer|beforeId)/.test(evt.key)) {
this.detachFromMap();
this.attachToMap(map);
}
}),
// When the style changes we wait that it is loaded to relaunch the onLoad
this.maplibreLayer.on('propertychange', (evt) => {
if (evt.key === 'style') {
const mbMap = evt.target.mapLibreMap;
void (mbMap === null || mbMap === void 0 ? void 0 : mbMap.once('styledata', () => {
void (mbMap === null || mbMap === void 0 ? void 0 : mbMap.once('idle', () => {
this.onLoad();
}));
}));
}
}));
}
/**
* Create a copy of the MaplibreStyleLayer.
*
* @param {Object} newOptions Options to override. See constructor.
* @return {MapboxStyleLayer} A MaplibreStyleLayer.
* @public
*/
clone(newOptions) {
return new MaplibreStyleLayer(Object.assign(Object.assign({}, (this.get('options') || {})), (newOptions || {})));
}
createRenderer() {
return new MaplibreStyleLayerRenderer(this);
}
/**
* Terminate the layer.
* @override
*/
detachFromMap() {
var _a;
unByKey(this.olEventsKeys);
if ((_a = this.maplibreLayer) === null || _a === void 0 ? void 0 : _a.mapLibreMap) {
this.maplibreLayer.mapLibreMap.off('load', this.onLoad);
this.removeLayers();
this.removeSources();
}
}
// /**
// * Request feature information for a given coordinate.
// * @param {ol/coordinate~Coordinate} coordinate Coordinate to request the information at.
// * @return {Promise<FeatureInfo>} Promise with features, layer and coordinate.
// * @deprecated Use getFeatureInfoAtCoordinate([layer], coordinate) from mobility-toolbox-ol package instead.
// */
// getFeatureInfoAtCoordinate(
// coordinate: Coordinate,
// ): Promise<LayerGetFeatureInfoResponse> {
// deprecated(
// `Deprecated. getFeatureInfoAtCoordinate([layer], coordinate) from ol package instead.`,
// );
// if (!this.maplibreLayer?.mapLibreMap) {
// return Promise.resolve({ coordinate, features: [], layer: this });
// }
// const { mapLibreMap } = this.maplibreLayer;
// // Ignore the getFeatureInfo until the Maplibre map is loaded
// if (!mapLibreMap.isStyleLoaded()) {
// return Promise.resolve({ coordinate, features: [], layer: this });
// }
// // We query features only on style layers used by this layer.
// let layers = this.layers || [];
// if (this.layersFilter) {
// layers = mapLibreMap.getStyle().layers.filter(this.layersFilter);
// }
// if (this.queryRenderedLayersFilter) {
// layers = mapLibreMap
// .getStyle()
// .layers.filter(this.queryRenderedLayersFilter);
// }
// return Promise.resolve({
// coordinate,
// features: [],
// layer: this,
// });
// // this.maplibreLayer
// // .getFeatureInfoAtCoordinate(coordinate, {
// // layers: layers.map((layer) => layer && layer.id),
// // validate: false,
// // })
// // .then((featureInfo: LayerGetFeatureInfoResponse) => {
// // const features: Feature[] = featureInfo.features.filter(
// // (feature: Feature) => {
// // // @ts-expect-error
// // return this.featureInfoFilter(
// // feature,
// // this.map?.getView().getResolution(),
// // ) as Feature[];
// // },
// // );
// // this.highlight(features);
// // return { ...featureInfo, features, layer: this };
// // });
// }
/**
* Highlight a list of features.
* @param {Array<ol/Feature~Feature>} [features=[]] Features to highlight.
* @deprecated Use layer.setFeatureState(features, {hover: true|false}) instead.
*/
highlight(features = []) {
var _a;
deprecated(`Deprecated. Use layer.setFeatureState(features, {hover: true}) instead.`);
// Filter out selected features
const filtered = ((_a = this.highlightedFeatures) === null || _a === void 0 ? void 0 : _a.filter((feature) => {
return !(this.selectedFeatures || [])
.map((feat) => {
return feat.getId();
})
.includes(feature.getId());
})) || [];
// Remove previous highlight
this.setHoverState(filtered, false);
this.highlightedFeatures = features;
// Add highlight
this.setHoverState(this.highlightedFeatures, true);
}
/**
* On Maplibre map load callback function. Add style layers and dynaimc filters.
*/
onLoad() {
var _a;
if (!((_a = this.maplibreLayer) === null || _a === void 0 ? void 0 : _a.mapLibreMap)) {
return;
}
this.addSources();
this.addLayers();
const { mapLibreMap } = this.maplibreLayer;
const style = mapLibreMap.getStyle();
if ((style === null || style === void 0 ? void 0 : style.layers) && this.layersFilter) {
const styles = style.layers.filter(this.layersFilter);
this.set('disabled', !styles.length);
}
this.applyLayoutVisibility();
}
removeLayers() {
var _a;
if (!((_a = this.maplibreLayer) === null || _a === void 0 ? void 0 : _a.mapLibreMap) || !Array.isArray(this.layers)) {
return;
}
const { mapLibreMap } = this.maplibreLayer;
if (mapLibreMap) {
this.layers.forEach((styleLayer) => {
const { id } = styleLayer;
if (id && (mapLibreMap === null || mapLibreMap === void 0 ? void 0 : mapLibreMap.getLayer(id))) {
mapLibreMap.removeLayer(id);
}
});
}
}
// /**
// * Set filter that determines which features should be rendered in a style layer.
// * @param {maplibregl.filter} filter Determines which features should be rendered in a style layer.
// */
// setFilter(filter: { [key: string]: any }) {
// if (!this.maplibreLayer?.mapLibreMap) {
// return;
// }
// const { mapLibreMap } = this.maplibreLayer;
// this.styleLayers.forEach(({ id }) => {
// if (id && filter && mapLibreMap.getLayer(id)) {
// // @ts-expect-error
// mapLibreMap.setFilter(id, filter);
// }
// });
// }
removeSources() {
var _a;
if (!((_a = this.maplibreLayer) === null || _a === void 0 ? void 0 : _a.mapLibreMap) || !this.sources) {
return;
}
const { mapLibreMap } = this.maplibreLayer;
if (mapLibreMap) {
Object.keys(this.sources).forEach((id) => {
if (mapLibreMap.getSource(id)) {
mapLibreMap.removeSource(id);
}
});
}
}
/**
* Select a list of features.
* @param {Array<ol/Feature~Feature>} [features=[]] Features to select.
* @deprecated Use layer.setFeatureState(features, {selected: true|false}) instead.
*/
select(features = []) {
deprecated(`Deprecated. Use layer.setFeatureState(features, {selected: true}) instead.`);
this.setHoverState(this.selectedFeatures || [], false);
this.selectedFeatures = features;
this.setHoverState(this.selectedFeatures || [], true);
}
/**
* Set the [feature state](https://maplibre.org/maplibre-style-spec/expressions/#feature-state) of the features.
*
* @param {ol/Feature~Feature[]} features
* @param {{[key: string]: any}} state The feature state
* @public
*/
setFeatureState(features, state) {
var _a;
if (!((_a = this.maplibreLayer) === null || _a === void 0 ? void 0 : _a.mapLibreMap) || !features.length) {
return;
}
const mapLibreMap = this.maplibreLayer.mapLibreMap;
features.forEach((feature) => {
const { source, sourceLayer } = (feature.get(VECTOR_TILE_FEATURE_PROPERTY) || {});
if ((!source && !sourceLayer) || !feature.getId()) {
if (!feature.getId()) {
deprecated("No feature's id found. To use the feature state functionnality, tiles must be generated with --generate-ids. See https://github.com/Maplibre/tippecanoe#adding-calculated-attributes.", feature.getProperties());
}
return;
}
if (source) {
mapLibreMap.setFeatureState({
id: feature.getId(),
source,
sourceLayer,
}, state);
}
else {
deprecated('No source found for the feature. To use the feature state functionnality, a source must be defined.', feature.getProperties());
}
});
}
/**
* Set if features are hovered or not.
* @param {Array<ol/Feature~Feature>} features
* @param {boolean} state Is the feature hovered
* @deprecated Use layer.setFeatureState(features, {hover: true|false}) instead.
*/
setHoverState(features, state) {
deprecated(`Deprecated. Use layer.setFeatureState(features, {hover: ${state}}) instead.`);
this.setFeatureState(features, { hover: state });
}
setMapInternal(map) {
if (map) {
super.setMapInternal(map);
this.attachToMap(map);
}
else {
this.detachFromMap();
super.setMapInternal(map);
}
}
}
export default MaplibreStyleLayer;