Skip to content

Commit

Permalink
deck.gl: Support filtering lines and agencies
Browse files Browse the repository at this point in the history
Add the `canFilter` layer property. Because we need to know from the
start the number of filters to enable for a layer, as it is sent to the
shader and not updatable, even with the updateTrigger, this property
allows to set a placeholder filter for all features when no other filter
is set. See if this can be prevented somehow.

The PathMapLayerManager is renamed to TransitPathFilterManager because
it does not involve layers, but manages the filters. This class emits
the filter update events. The event is caught by the map, which updates
the layer accordingly.
  • Loading branch information
tahini committed Jan 23, 2024
1 parent d034a61 commit 62ad89b
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 113 deletions.
86 changes: 13 additions & 73 deletions packages/chaire-lib-frontend/src/services/map/MapLayerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,14 @@ const defaultGeojson = turfFeatureCollection([]) as GeoJSON.FeatureCollection<Ge
/**
* Layer manager for Mapbox-gl maps
*
* TODO See how filters are used and type them properly, make them map implementation independant ideally
*
* TODO: If we want to support multiple map implementation, this layer management will have to be updated
*/
class MapboxLayerManager {
class MapLayerManager {
private _layersByName: { [key: string]: MapLayer } = {};
private _enabledLayers: string[] = [];
private _defaultFilterByLayer = {};
private _filtersByLayer = {};
private _filtersByLayer: {
[layerName: string]: ((feature: GeoJSON.Feature) => 0 | 1) | undefined;
} = {};

constructor(layersConfig: any) {
for (const layerName in layersConfig) {
Expand All @@ -49,26 +48,20 @@ class MapboxLayerManager {
}
}

getFilter(layerName: string) {
// TODO Re-implement
return this._filtersByLayer[layerName] || null;
getFilter(layerName: string): ((feature: GeoJSON.Feature) => 0 | 1) | undefined {
return this._filtersByLayer[layerName];
}

updateFilter(layerName: string, filter: boolean | any[] | null | undefined) {
if (this._defaultFilterByLayer[layerName]) {
filter = ['all', this._defaultFilterByLayer[layerName], filter];
}
this._filtersByLayer[layerName] = filter;
if (this.layerIsEnabled(layerName)) {
// this._map?.setFilter(layerName, this._filtersByLayer[layerName]);
updateFilter(layerName: string, filter: ((feature: GeoJSON.Feature) => 0 | 1) | undefined) {
if (filter === undefined) {
delete this._filtersByLayer[layerName];
} else {
this._filtersByLayer[layerName] = filter;
}
}

clearFilter(layerName: string) {
this._filtersByLayer[layerName] = this._defaultFilterByLayer[layerName];
if (this.layerIsEnabled(layerName)) {
//this._map?.setFilter(layerName, this._defaultFilterByLayer[layerName]);
}
delete this._filtersByLayer[layerName];
}

updateEnabledLayers(enabledLayers: string[] = []) {
Expand All @@ -80,59 +73,6 @@ class MapboxLayerManager {
serviceLocator.eventManager.emit('map.updatedEnabledLayers', this._enabledLayers);
}

showLayerObjectByAttribute(layerName: string, attribute: string, value: any) {
// TODO Reimplement
const existingFilter = this.getFilter(layerName);
let values: any[] = [];
if (
existingFilter &&
existingFilter[0] === 'match' &&
existingFilter[1] &&
existingFilter[1][0] === 'get' &&
existingFilter[1][1] === attribute
) {
if (existingFilter[2] && !existingFilter[2].includes(value)) {
values = existingFilter[2];
values.push(value);
} else {
values = [value];
}
existingFilter[2] = values;
// TODO Map needs updating at this point
// this._map?.setFilter(layerName, existingFilter);
} else {
// this._map?.setFilter(layerName, ['match', ['get', attribute], values, true, false]);
}
}

hideLayerObjectByAttribute(layerName, attribute, value) {
// TODO Reimplement
const existingFilter = this.getFilter(layerName);
let values: any[] = [];
if (
existingFilter &&
existingFilter[0] === 'match' &&
existingFilter[1] &&
existingFilter[1][0] === 'get' &&
existingFilter[1][1] === attribute
) {
if (existingFilter[2]) {
values = existingFilter[2];
const valueIndex = values.indexOf(value);
if (valueIndex < 0) {
values.push(value);
}
} else {
values = [value];
}
existingFilter[2] = values;
// TODO Map needs updating at this point
// this._map?.setFilter(layerName, existingFilter);
} else {
// this._map?.setFilter(layerName, ['match', ['get', attribute], values, true, false]);
}
}

getLayer(layerName: string) {
return this._layersByName[layerName];
}
Expand Down Expand Up @@ -220,4 +160,4 @@ class MapboxLayerManager {
}
}

export default MapboxLayerManager;
export default MapLayerManager;
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ export type MapUpdateLayerEventType = {
data: GeoJSON.FeatureCollection | ((original: GeoJSON.FeatureCollection) => GeoJSON.FeatureCollection);
};
};

export type MapFilterLayerEventType = {
name: 'map.layers.updateFilter';
arguments: {
layerName: string;
filter: ((feature: GeoJSON.Feature) => 0 | 1) | undefined;
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export type CommonLayerConfiguration = {
maxZoom?: FeatureNumber;
autoHighlight?: boolean;
featureMinZoom?: FeatureNumber;
/**
* Whether this layer can be filtered by additional filters.
* @default false
*/
canFilter?: boolean;
};

export type PointLayerConfiguration = CommonLayerConfiguration & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import React, { createRef } from 'react';
import ReactDom from 'react-dom';
import { withTranslation } from 'react-i18next';
import DeckGL from '@deck.gl/react/typed';
import { FilterContext, Layer, Deck } from '@deck.gl/core/typed';
import { Layer, Deck } from '@deck.gl/core/typed';

import { Map as MapLibreMap } from 'react-map-gl/maplibre';
import MapboxGL from 'mapbox-gl';
Expand All @@ -21,7 +21,7 @@ import globalMapEvents from 'chaire-lib-frontend/lib/services/map/events/GlobalM
import transitionMapEvents from '../../services/map/events';
import mapCustomEvents from '../../services/map/events/MapRelatedCustomEvents';
import MapLayerManager from 'chaire-lib-frontend/lib/services/map/MapLayerManager';
import PathMapLayerManager from '../../services/map/PathMapLayerManager';
import TransitPathFilterManager from '../../services/map/TransitPathFilterManager';
import MapPopupManager from 'chaire-lib-frontend/lib/services/map/MapPopupManager';
import serviceLocator from 'chaire-lib-common/lib/utils/ServiceLocator';
import { getMapBoxDraw, removeMapBoxDraw } from 'chaire-lib-frontend/lib/services/map/MapPolygonService';
Expand All @@ -32,7 +32,10 @@ import _cloneDeep from 'lodash/cloneDeep';
import { featureCollection as turfFeatureCollection } from '@turf/turf';
import { LayoutSectionProps } from 'chaire-lib-frontend/lib/services/dashboard/DashboardContribution';
import { deleteUnusedNodes } from '../../services/transitNodes/transitNodesUtils';
import { MapUpdateLayerEventType } from 'chaire-lib-frontend/lib/services/map/events/MapEventsCallbacks';
import {
MapUpdateLayerEventType,
MapFilterLayerEventType
} from 'chaire-lib-frontend/lib/services/map/events/MapEventsCallbacks';
import { EventManager } from 'chaire-lib-common/lib/services/events/EventManager';
import {
layerEventNames,
Expand Down Expand Up @@ -83,9 +86,7 @@ interface MainMapState {
*/
class MainMap extends React.Component<MainMapProps, MainMapState> {
private layerManager: MapLayerManager;
private pathLayerManager: PathMapLayerManager;
private defaultZoomArray: [number];
private defaultCenter: [number, number];
private pathFilterManager: TransitPathFilterManager;
private mapEvents: {
map: { [evtName in mapEventNames]?: MapEventHandlerDescriptor[] };
layers: {
Expand Down Expand Up @@ -154,11 +155,9 @@ class MainMap extends React.Component<MainMapProps, MainMapState> {
isDragging: false
};

this.defaultZoomArray = [props.zoom];
this.defaultCenter = props.center;
this.layerManager = new MapLayerManager(layersConfig);

this.pathLayerManager = new PathMapLayerManager(this.layerManager);
this.pathFilterManager = new TransitPathFilterManager();

this.popupManager = new MapPopupManager();
this.mapContainer = createRef<HTMLDivElement>();
Expand Down Expand Up @@ -218,28 +217,28 @@ class MainMap extends React.Component<MainMapProps, MainMapState> {
showPathsByAttribute = (attribute: string, value: any) => {
// attribute must be agency_id or line_id
if (attribute === 'agency_id') {
this.pathLayerManager.showAgencyId(value);
this.pathFilterManager.showAgencyId(value);
} else if (attribute === 'line_id') {
this.pathLayerManager.showLineId(value);
this.pathFilterManager.showLineId(value);
}
};

hidePathsByAttribute = (attribute: string, value: any) => {
// attribute must be agency_id or line_id
if (attribute === 'agency_id') {
this.pathLayerManager.hideAgencyId(value);
this.pathFilterManager.hideAgencyId(value);
} else if (attribute === 'line_id') {
this.pathLayerManager.hideLineId(value);
this.pathFilterManager.hideLineId(value);
}
};

clearPathsFilter = () => {
this.pathLayerManager.clearFilter();
this.pathFilterManager.clearFilter();
};

componentDidMount = () => {
serviceLocator.addService('layerManager', this.layerManager);
serviceLocator.addService('pathLayerManager', this.pathLayerManager);
serviceLocator.addService('pathLayerManager', this.pathFilterManager);
this.layerManager.updateEnabledLayers(Preferences.current.map.layers[this.props.activeSection]);
mapCustomEvents.addEvents(serviceLocator.eventManager);
//elementResizedEvent(this.mapContainer, this.onResizeContainer);
Expand All @@ -249,10 +248,13 @@ class MainMap extends React.Component<MainMapProps, MainMapState> {
'map.updateLayer',
this.updateLayer
);
(serviceLocator.eventManager as EventManager).onEvent<MapFilterLayerEventType>(
'map.layers.updateFilter',
this.updateFilter
);
serviceLocator.eventManager.on('map.updateLayers', this.updateLayers);
serviceLocator.eventManager.on('map.addPopup', this.addPopup);
serviceLocator.eventManager.on('map.removePopup', this.removePopup);
serviceLocator.eventManager.on('map.updateFilter', this.updateFilter);
serviceLocator.eventManager.on('map.clearFilter', this.clearFilter);
serviceLocator.eventManager.on('map.showLayer', this.showLayer);
serviceLocator.eventManager.on('map.hideLayer', this.hideLayer);
Expand Down Expand Up @@ -286,7 +288,7 @@ class MainMap extends React.Component<MainMapProps, MainMapState> {
serviceLocator.eventManager.off('map.updateLayers', this.updateLayers);
serviceLocator.eventManager.off('map.addPopup', this.addPopup);
serviceLocator.eventManager.off('map.removePopup', this.removePopup);
serviceLocator.eventManager.off('map.updateFilter', this.updateFilter);
serviceLocator.eventManager.off('map.layers.updateFilter', this.updateFilter);
serviceLocator.eventManager.off('map.clearFilter', this.clearFilter);
serviceLocator.eventManager.off('map.showLayer', this.showLayer);
serviceLocator.eventManager.off('map.hideLayer', this.hideLayer);
Expand Down Expand Up @@ -354,8 +356,10 @@ class MainMap extends React.Component<MainMapProps, MainMapState> {
this.layerManager.clearFilter(layerName);
};

updateFilter = (layerName: string, filter) => {
this.layerManager.updateFilter(layerName, filter);
updateFilter = (args: { layerName: string; filter: ((feature: GeoJSON.Feature) => 0 | 1) | undefined }) => {
this.layerManager.updateFilter(args.layerName, args.filter);
this.updateCounts[args.layerName] = (this.updateCounts[args.layerName] || 0) + 1;
this.setState({ enabledLayers: this.layerManager.getEnabledLayers().map((layer) => layer.id) });
};

setRef = (ref) => {
Expand Down Expand Up @@ -569,10 +573,6 @@ class MainMap extends React.Component<MainMapProps, MainMapState> {
ReactDom.render(<React.Fragment></React.Fragment>, contextMenu);
};

onLayerFilter = (context: FilterContext): boolean => {
return true;
};

private updateUserPrefs = _debounce((viewStateChange) => {
// Save map zoom and center to user preferences
Preferences.update(
Expand Down Expand Up @@ -683,7 +683,7 @@ class MainMap extends React.Component<MainMapProps, MainMapState> {
}): PickingInfo[] => (this.mapContainer.current as Deck).pickMultipleObjects(opts);

render() {
// TODO: Deck.gl Migration: Should this be a state? To avoid recalculating for every render? See how often we render when the migration is complete
// TODO: Deck.gl Migration: Should this be a state or a local field (equivalent of useMemo)? To avoid recalculating for every render? See how often we render when the migration is complete
const enabledLayers = this.layerManager.getEnabledLayers();
const layers: Layer[] = enabledLayers
.map((layer) =>
Expand All @@ -694,7 +694,8 @@ class MainMap extends React.Component<MainMapProps, MainMapState> {
activeSection: this.props.activeSection,
setDragging: this.setDragging,
mapCallbacks: this.mapCallbacks,
updateCount: this.updateCounts[layer.id] || 0
updateCount: this.updateCounts[layer.id] || 0,
filter: this.layerManager.getFilter(layer.id)
})
)
.filter((layer) => layer !== undefined) as Layer[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type TransitionMapLayerProps = {
setDragging: (dragging: boolean) => void;
mapCallbacks: MapCallbacks;
updateCount: number;
filter?: (feature: GeoJSON.Feature) => 0 | 1;
};

const stringToColor = (hexStringColor: string): [number, number, number] | [number, number, number, number] => [
Expand Down Expand Up @@ -133,15 +134,39 @@ const layerNumberGetter = (
return undefined;
};

// Get the filter extension for this layer
const getLayerFeatureFilter = (props: TransitionMapLayerProps, config: LayerDescription.CommonLayerConfiguration) => {
// FIXME Is it possible to change the number of filters during execution? We
// tried for the transitPaths layer, but apparently if we dynamically change
// the number of filters from 1 to 2 and vice versa, it fails. The filter
// range is sent to the gl shader and it may not be updated. We tried with
// the updateTrigger, without success.
const layerFilter: any = {};
const getFilterFcts: (number | ((feature: GeoJSON.Feature) => number))[] = [];
const filterRanges: [number, number][] = [];
const featureMinZoom =
config.featureMinZoom === undefined ? undefined : layerNumberGetter(config.featureMinZoom, 1);
if (featureMinZoom !== undefined) {
layerFilter.getFilterValue = featureMinZoom;
layerFilter.extensions = [new DataFilterExtension({ filterSize: 1 })];
getFilterFcts.push(featureMinZoom);
// Display the feature if the min zoom is less than the current zoom
layerFilter.filterRange = [0, Math.floor(props.viewState.zoom)];
filterRanges.push([0, Math.floor(props.viewState.zoom)]);
}
if (config.canFilter === true) {
getFilterFcts.push(props.filter !== undefined ? props.filter : (feature) => 1);
// Display the feature if the function's return value is above 1
filterRanges.push([1, 10]);
}

// Prepare the layer properties depending on the number of filtering functions
if (getFilterFcts.length === 1) {
layerFilter.getFilterValue = getFilterFcts[0];
layerFilter.extensions = [new DataFilterExtension({ filterSize: 1 })];
layerFilter.filterRange = filterRanges[0];
} else if (getFilterFcts.length > 1) {
layerFilter.getFilterValue = (feature: GeoJSON.Feature) =>
getFilterFcts.map((fct) => (typeof fct === 'function' ? fct(feature) : fct));
layerFilter.extensions = [new DataFilterExtension({ filterSize: getFilterFcts.length })];
layerFilter.filterRange = filterRanges;
}
return layerFilter;
};
Expand Down
1 change: 1 addition & 0 deletions packages/transition-frontend/src/config/layers.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const layersConfig = {
? 10
: 11;
},
canFilter: true,
color: { type: 'property', property: 'color' },
opacity: 0.8,
widthScale: 4,
Expand Down
Loading

0 comments on commit 62ad89b

Please sign in to comment.