Skip to content

Commit

Permalink
deck.gl: Set styles for circle layers
Browse files Browse the repository at this point in the history
Bring back all mapbox styles for circle-type layers.
Type the layer description for circle layers, with fields being either a
number/color, a function receiving the feature in parameter, or a
property getter to retrieve the value from a geojson property.
  • Loading branch information
tahini committed Dec 22, 2023
1 parent 3577542 commit 7c27395
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 289 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ const defaultPreferences: PreferencesModel = {
'transitPathWaypointsErrors'
],
nodes: [
'transitNodesSelected',
'transitStationsSelected',
'aggregatedOD',
'transitNodes250mRadius',
'transitNodes500mRadius',
Expand All @@ -128,9 +130,7 @@ const defaultPreferences: PreferencesModel = {
'transitNodesRoutingRadius',
'transitPaths',
'transitStations',
'transitStationsSelected',
'transitNodes',
'transitNodesSelected'
'transitNodes'
],
scenarios: ['transitPathsForServices'],
routing: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ class MapboxLayerManager {
}

updateEnabledLayers(enabledLayers: string[] = []) {
this._enabledLayers = _uniq(enabledLayers).filter((layerName) => this._layersByName[layerName] !== undefined); // make sure we do not have the same layer twice (can happen with user prefs not replaced correctly after updates)
this._enabledLayers = _uniq(enabledLayers)
.filter((layerName) => this._layersByName[layerName] !== undefined)
.sort((layerName1, layerName2) => {
return (layerName1.includes('Selected') ? 0 : 1) - (layerName2.includes('Selected') ? 0 : 1);
}); // make sure we do not have the same layer twice (can happen with user prefs not replaced correctly after updates)
serviceLocator.eventManager.emit('map.updatedEnabledLayers', this._enabledLayers);
}

Expand Down Expand Up @@ -171,7 +175,8 @@ class MapboxLayerManager {
) {
// FIXME: In original code, geojson can be a function. Do we need to support this? It took the source data as parameter
if (this._layersByName[layerName] !== undefined) {
const geojsonData = typeof geojson === 'function' ? geojson(this._layersByName[layerName].layerData) : geojson;
const geojsonData =
typeof geojson === 'function' ? geojson(this._layersByName[layerName].layerData) : geojson;
this._layersByName[layerName].layerData = geojsonData;
} else {
console.log('layer does not exist', layerName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,69 @@
* License text available at https://opensource.org/licenses/MIT
*/

export type LayerConfiguration = {
// TODO Type this properly. When the data in layers.config.ts is used by the new API, add it here
[key: string]: any;
/**
* A type to describe how to get the value of some configuration. `property` is
* the name of the property in a geojson feature
*/
export type ConfigurationGetter = { type: 'property'; property: string };
/**
* A color for map features, either a string starting with `#` with hexadecimal
* colors, or an array of rgb or rgba numbers
*/
export type FeatureColor =
| string
| [number, number, number]
| [number, number, number, number]
| ConfigurationGetter
| ((feature: GeoJSON.Feature) => string | [number, number, number] | [number, number, number, number]);
export type FeatureNumber = number | ConfigurationGetter | ((feature: GeoJSON.Feature) => number);

export type CommonLayerConfiguration = {
/**
* Color of the feature
*/
color?: FeatureColor;
pickable?: boolean | (() => boolean);
};

export type PointLayerConfiguration = CommonLayerConfiguration & {
type: 'circle';
/**
* Radius of the feature
*/
radius?: FeatureNumber;
radiusScale?: FeatureNumber;
/**
* Color of the contour of the feature
*/
strokeColor?: FeatureColor;
/**
* Width of the contour of the feature
*/
strokeWidth?: FeatureNumber;
strokeWidthScale?: FeatureNumber;
minRadiusPixels?: FeatureNumber;
maxRadiusPixels?: FeatureNumber;
/**
* Minimal zoom level at which a feature should be displayed
*/
minZoom?: FeatureNumber;
/**
* Maximum zoom level at which a feature should be displayed
*/
maxZoom?: FeatureNumber;
};
export const layerIsCircle = (layer: LayerConfiguration): layer is PointLayerConfiguration => {
return layer.type === 'circle';
};

export type LayerConfiguration =
| PointLayerConfiguration
| {
// TODO Type this properly. When the data in layers.config.ts is used by the new API, add it here
[key: string]: any;
};

export type MapLayer = {
/** Unique identifier for this layer */
id: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import Node from 'transition-common/lib/services/nodes/Node';
import _cloneDeep from 'lodash/cloneDeep';
import { featureCollection as turfFeatureCollection } from '@turf/turf';
import { LayoutSectionProps } from 'chaire-lib-frontend/lib/services/dashboard/DashboardContribution';
import { MapEventHandlerDescription } from 'chaire-lib-frontend/lib/services/map/IMapEventHandler';
import { deleteUnusedNodes } from '../../services/transitNodes/transitNodesUtils';
import { MapUpdateLayerEventType } from 'chaire-lib-frontend/lib/services/map/events/MapEventsCallbacks';
import { EventManager } from 'chaire-lib-common/lib/services/events/EventManager';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
* License text available at https://opensource.org/licenses/MIT
*/
import { Layer, LayerProps } from '@deck.gl/core/typed';
import { propertiesContainsFilter } from '@turf/turf';
import {
layerEventNames,
MapCallbacks,
MapLayerEventHandlerDescriptor
} from 'chaire-lib-frontend/lib/services/map/IMapEventHandler';
import { MapLayer } from 'chaire-lib-frontend/lib/services/map/layers/LayerDescription';
import * as LayerDescription from 'chaire-lib-frontend/lib/services/map/layers/LayerDescription';
import { ScatterplotLayer, PathLayer, GeoJsonLayer, PickingInfo, Deck } from 'deck.gl/typed';
import { MjolnirEvent, MjolnirGestureEvent } from 'mjolnir.js';
import AnimatedArrowPathLayer from './AnimatedArrowPathLayer';
Expand All @@ -24,10 +25,19 @@ const defaultRGBA = [
255
] as [number, number, number, number];

// FIXME Deck.gl types the viewState as `any`, as if it could change depending
// on... what?. Here we just type the parameters that we know are available to
// our map, but maybe it is wrong to do so?
export type ViewState = {
zoom: number;
latitude: number;
longitude: number;
[key: string]: any;
};

type TransitionMapLayerProps = {
layerDescription: MapLayer;
// TODO Find the right type for this
viewState;
layerDescription: LayerDescription.MapLayer;
viewState: ViewState;
events?: { [evtName in layerEventNames]?: MapLayerEventHandlerDescriptor[] };
activeSection: string;
setDragging: (dragging: boolean) => void;
Expand Down Expand Up @@ -65,6 +75,67 @@ const propertyToColor = (
: defaultRGBA;
};

const layerColorGetter = (
getter: LayerDescription.FeatureColor | undefined,
defaultValue: string
):
| undefined
| [number, number, number]
| [number, number, number, number]
| ((feature: GeoJSON.Feature) => [number, number, number] | [number, number, number, number]) => {
if (getter === undefined) {
return stringToColor(defaultValue);
}
if (typeof getter === 'string') {
return stringToColor(getter);
}
if (Array.isArray(getter)) {
return getter;
}
if (typeof getter === 'function') {
return (feature: GeoJSON.Feature) => {
const color = getter(feature);
return typeof color === 'string' ? stringToColor(color) : color;
};
}
if (getter.type === 'property') {
return (feature: GeoJSON.Feature) => propertyToColor(feature, getter.property, defaultValue);
}
return undefined;
};

const propertytoNumber = (
feature: GeoJSON.Feature<GeoJSON.Geometry>,
property: string,
defaultNumber = 0
): number => {
if (!feature.properties || !feature.properties[property]) {
return 0;
}
const numberValue = feature.properties[property];
return typeof numberValue === 'number'
? numberValue
: typeof numberValue === 'string'
? parseInt(numberValue)
: defaultNumber;
};

const layerNumberGetter = (
getter: LayerDescription.FeatureNumber | undefined,
defaultValue: number | undefined
): undefined | number | ((feature: GeoJSON.Feature) => number) => {
if (getter === undefined) {
return defaultValue;
}
if (typeof getter === 'number' || typeof getter === 'function') {
return getter;
}
if (getter.type === 'property') {
return (feature: GeoJSON.Feature) => propertytoNumber(feature, getter.property, defaultValue);
}
return undefined;
};

const getLineLayer = (props: TransitionMapLayerProps, eventsToAdd): PathLayer =>
new PathLayer({
id: props.layerDescription.id,
Expand Down Expand Up @@ -134,24 +205,71 @@ const getPolygonLayer = (props: TransitionMapLayerProps, eventsToAdd): GeoJsonLa
...eventsToAdd
});

const getScatterLayer = (props: TransitionMapLayerProps, eventsToAdd): ScatterplotLayer<any> =>
new ScatterplotLayer({
const getScatterLayer = (
props: TransitionMapLayerProps,
config: LayerDescription.PointLayerConfiguration,
eventsToAdd
): ScatterplotLayer<any> | undefined => {
const layerProperties: any = {};
const minZoom = config.minZoom === undefined ? undefined : layerNumberGetter(config.minZoom, undefined);
if (typeof minZoom === 'number' && props.viewState.zoom <= minZoom) {
return undefined;
} else if (typeof minZoom === 'function') {
console.log('Function for minZoom level not supported yet');
}
const maxZoom = config.maxZoom === undefined ? undefined : layerNumberGetter(config.maxZoom, undefined);
if (typeof maxZoom === 'number' && props.viewState.zoom >= maxZoom) {
return undefined;
} else if (typeof maxZoom === 'function') {
console.log('Function for maxZoom level not supported yet');
}
const contourWidth =
config.strokeWidth === undefined ? undefined : layerNumberGetter(config.strokeWidth, undefined);
if (contourWidth !== undefined) {
layerProperties.getLineWidth = contourWidth;
}
const circleRadius = config.radius === undefined ? undefined : layerNumberGetter(config.radius, 10);
if (circleRadius !== undefined) {
layerProperties.getRadius = circleRadius;
}
const color = config.color === undefined ? undefined : layerColorGetter(config.color, '#ffffff');
if (color !== undefined) {
layerProperties.getFillColor = color;
}
const contourColor = config.strokeColor === undefined ? undefined : layerColorGetter(config.strokeColor, '#ffffff');
if (contourColor !== undefined) {
layerProperties.getLineColor = contourColor;
}
const radiusScale = config.radiusScale === undefined ? undefined : layerNumberGetter(config.radiusScale, 1);
if (radiusScale !== undefined) {
layerProperties.radiusScale = radiusScale;
}
const lineWidthScale =
config.strokeWidthScale === undefined ? undefined : layerNumberGetter(config.strokeWidthScale, 1);
if (lineWidthScale !== undefined) {
layerProperties.lineWidthScale = lineWidthScale;
}
const pickable =
config.pickable === undefined
? true
: typeof config.pickable === 'function'
? config.pickable()
: config.pickable;
return new ScatterplotLayer({
id: props.layerDescription.id,
data: props.layerDescription.layerData.features,
filled: true,
stroked: true,
filled: color !== undefined,
stroked: contourColor !== undefined || contourWidth !== undefined,
getPosition: (d) => d.geometry.coordinates,
getFillColor: (d) => propertyToColor(d, 'color'),
getLineColor: [255, 255, 255, 255],
getRadius: (d, i) => 10,
radiusScale: 6,
updateTriggers: {
getPosition: props.updateCount,
getFillColor: props.updateCount
},
pickable: true,
...eventsToAdd
pickable,
...eventsToAdd,
...layerProperties
});
};

const addEvents = (
events: { [evtName in layerEventNames]?: MapLayerEventHandlerDescriptor[] },
Expand Down Expand Up @@ -205,9 +323,9 @@ const getLayer = (props: TransitionMapLayerProps): Layer<LayerProps> | undefined
return undefined;
}
const eventsToAdd = props.events !== undefined ? addEvents(props.events, props) : {};
if (props.layerDescription.configuration.type === 'circle') {
if (LayerDescription.layerIsCircle(props.layerDescription.configuration)) {
// FIXME Try not to type as any
return getScatterLayer(props, eventsToAdd) as any;
return getScatterLayer(props, props.layerDescription.configuration, eventsToAdd) as any;
} else if (props.layerDescription.configuration.type === 'line') {
return getLineLayer(props, eventsToAdd) as any;
} else if (props.layerDescription.configuration.type === 'fill') {
Expand Down
Loading

0 comments on commit 7c27395

Please sign in to comment.