From 7c273956a37fd82c0c3644d3c8ca39a3f7de9840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Genevi=C3=A8ve=20Bastien?= Date: Mon, 18 Dec 2023 14:51:40 -0500 Subject: [PATCH] deck.gl: Set styles for circle layers 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. --- .../src/config/defaultPreferences.config.ts | 6 +- .../src/services/map/MapLayerManager.ts | 9 +- .../services/map/layers/LayerDescription.ts | 64 ++- .../src/components/map/TransitionMainMap.tsx | 1 - .../map/layers/TransitionMapLayer.tsx | 150 ++++++- .../src/config/layers.config.ts | 366 +++++------------- .../map/events/NodeSectionMapEvents.ts | 4 +- 7 files changed, 311 insertions(+), 289 deletions(-) diff --git a/packages/chaire-lib-common/src/config/defaultPreferences.config.ts b/packages/chaire-lib-common/src/config/defaultPreferences.config.ts index 0967a8e9..23d35f3f 100644 --- a/packages/chaire-lib-common/src/config/defaultPreferences.config.ts +++ b/packages/chaire-lib-common/src/config/defaultPreferences.config.ts @@ -119,6 +119,8 @@ const defaultPreferences: PreferencesModel = { 'transitPathWaypointsErrors' ], nodes: [ + 'transitNodesSelected', + 'transitStationsSelected', 'aggregatedOD', 'transitNodes250mRadius', 'transitNodes500mRadius', @@ -128,9 +130,7 @@ const defaultPreferences: PreferencesModel = { 'transitNodesRoutingRadius', 'transitPaths', 'transitStations', - 'transitStationsSelected', - 'transitNodes', - 'transitNodesSelected' + 'transitNodes' ], scenarios: ['transitPathsForServices'], routing: [ diff --git a/packages/chaire-lib-frontend/src/services/map/MapLayerManager.ts b/packages/chaire-lib-frontend/src/services/map/MapLayerManager.ts index 142fecf3..94ccbea2 100644 --- a/packages/chaire-lib-frontend/src/services/map/MapLayerManager.ts +++ b/packages/chaire-lib-frontend/src/services/map/MapLayerManager.ts @@ -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); } @@ -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); diff --git a/packages/chaire-lib-frontend/src/services/map/layers/LayerDescription.ts b/packages/chaire-lib-frontend/src/services/map/layers/LayerDescription.ts index bb97bdf5..bcca467b 100644 --- a/packages/chaire-lib-frontend/src/services/map/layers/LayerDescription.ts +++ b/packages/chaire-lib-frontend/src/services/map/layers/LayerDescription.ts @@ -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; diff --git a/packages/transition-frontend/src/components/map/TransitionMainMap.tsx b/packages/transition-frontend/src/components/map/TransitionMainMap.tsx index c388cfbb..bce54264 100644 --- a/packages/transition-frontend/src/components/map/TransitionMainMap.tsx +++ b/packages/transition-frontend/src/components/map/TransitionMainMap.tsx @@ -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'; diff --git a/packages/transition-frontend/src/components/map/layers/TransitionMapLayer.tsx b/packages/transition-frontend/src/components/map/layers/TransitionMapLayer.tsx index d85dbac0..140798fa 100644 --- a/packages/transition-frontend/src/components/map/layers/TransitionMapLayer.tsx +++ b/packages/transition-frontend/src/components/map/layers/TransitionMapLayer.tsx @@ -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'; @@ -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; @@ -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, + 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, @@ -134,24 +205,71 @@ const getPolygonLayer = (props: TransitionMapLayerProps, eventsToAdd): GeoJsonLa ...eventsToAdd }); -const getScatterLayer = (props: TransitionMapLayerProps, eventsToAdd): ScatterplotLayer => - new ScatterplotLayer({ +const getScatterLayer = ( + props: TransitionMapLayerProps, + config: LayerDescription.PointLayerConfiguration, + eventsToAdd +): ScatterplotLayer | 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[] }, @@ -205,9 +323,9 @@ const getLayer = (props: TransitionMapLayerProps): Layer | 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') { diff --git a/packages/transition-frontend/src/config/layers.config.ts b/packages/transition-frontend/src/config/layers.config.ts index 18d4225d..be08bce5 100644 --- a/packages/transition-frontend/src/config/layers.config.ts +++ b/packages/transition-frontend/src/config/layers.config.ts @@ -4,68 +4,28 @@ * This file is licensed under the MIT License. * License text available at https://opensource.org/licenses/MIT */ +import serviceLocator from 'chaire-lib-common/lib/utils/ServiceLocator'; + const layersConfig = { routingPoints: { // for routing origin, destination and waypoints type: 'circle', - paint: { - 'circle-radius': { - base: 1, - stops: [ - [5, 1], - [10, 2], - [15, 10] - ] - }, - 'circle-color': { - property: 'color', - type: 'identity' - }, - 'circle-opacity': 1.0, - 'circle-stroke-width': { - base: 1, - stops: [ - [5, 1], - [11, 1], - [12, 2], - [14, 3], - [15, 4] - ] - }, - 'circle-stroke-opacity': 1.0, - 'circle-stroke-color': 'rgba(255,255,255,1.0)' - } + color: { type: 'property', property: 'color' }, + strokeColor: [255, 255, 255], + strokeWidth: 1, + radius: 5, + radiusScale: 3, + strokeWidthScale: 3 }, accessibilityMapPoints: { type: 'circle', - paint: { - 'circle-radius': { - base: 1, - stops: [ - [5, 1], - [10, 2], - [15, 10] - ] - }, - 'circle-color': { - property: 'color', - type: 'identity' - }, - 'circle-opacity': 1.0, - 'circle-stroke-width': { - base: 1, - stops: [ - [5, 1], - [11, 1], - [12, 2], - [14, 3], - [15, 4] - ] - }, - 'circle-stroke-opacity': 1.0, - 'circle-stroke-color': 'rgba(255,255,255,1.0)' - } + color: { type: 'property', property: 'color' }, + strokeColor: [255, 255, 255], + strokeWidth: 1, + radius: 5, + radiusScale: 3, + strokeWidthScale: 3 }, accessibilityMapPolygons: { @@ -375,85 +335,32 @@ const layersConfig = { transitPathWaypoints: { type: 'circle', - paint: { - 'circle-radius': { - base: 1, - stops: [ - [5, 1], - [10, 2], - [15, 5] - ] - }, - 'circle-color': 'rgba(0,0,0,1.0)', - 'circle-opacity': 0.5, - 'circle-stroke-width': { - base: 1, - stops: [ - [5, 1], - [11, 1], - [12, 2], - [14, 2], - [15, 3] - ] - }, - 'circle-stroke-opacity': 0.7, - 'circle-stroke-color': 'rgba(255,255,255,1.0)' - } + color: [0, 0, 0, 128], + strokeColor: [255, 255, 255, 180], + strokeWidth: 1, + radius: 4, + radiusScale: 3, + strokeWidthScale: 3 }, transitPathWaypointsSelected: { type: 'circle', - paint: { - 'circle-radius': { - base: 1, - stops: [ - [5, 1], - [10, 3], - [15, 6] - ] - }, - 'circle-color': 'rgba(0,0,0,1.0)', - 'circle-opacity': 0.5, - 'circle-stroke-width': { - base: 1, - stops: [ - [5, 1], - [11, 1], - [12, 2], - [14, 2], - [15, 3] - ] - }, - 'circle-stroke-opacity': 0.85, - 'circle-stroke-color': 'rgba(255,255,255,1.0)' - } + color: [0, 0, 0, 128], + strokeColor: [255, 255, 255, 220], + strokeWidth: 1, + radius: 4, + radiusScale: 3, + strokeWidthScale: 3 }, transitPathWaypointsErrors: { type: 'circle', - paint: { - 'circle-radius': { - base: 1, - stops: [ - [5, 1], - [10, 2], - [15, 5] - ] - }, - 'circle-opacity': 0, - 'circle-stroke-width': { - base: 1, - stops: [ - [5, 2], - [11, 2], - [12, 4], - [14, 4], - [15, 6] - ] - }, - 'circle-stroke-opacity': 0.7, - 'circle-stroke-color': 'rgba(255,0,0,1.0)' - } + color: [0, 0, 0, 128], + strokeColor: [255, 0, 0, 180], + strokeWidth: 1, + radius: 4, + radiusScale: 3, + strokeWidthScale: 3 }, transitPathsForServices: { @@ -637,138 +544,61 @@ const layersConfig = { transitNodes: { type: 'circle', - minzoom: 11, - paint: { - 'circle-radius': [ - 'interpolate', - ['exponential', 2], - ['zoom'], - 0, - ['*', ['number', ['feature-state', 'size'], 1], 0], - 10, - ['*', ['number', ['feature-state', 'size'], 1], 1.5], - 15, - ['*', ['number', ['feature-state', 'size'], 1], 8], - 20, - ['*', ['number', ['feature-state', 'size'], 1], 15] - ], - 'circle-color': { - property: 'color', - type: 'identity' - }, - 'circle-opacity': [ - 'interpolate', - ['linear'], - ['zoom'], - 10, - ['case', ['boolean', ['feature-state', 'hover'], false], 1.0, 0.1], - 15, - ['case', ['boolean', ['feature-state', 'hover'], false], 1.0, 0.8], - 20, - ['case', ['boolean', ['feature-state', 'hover'], false], 1.0, 0.9] - ], - 'circle-stroke-width': [ - 'interpolate', - ['exponential', 2], - ['zoom'], - 0, - ['*', ['number', ['feature-state', 'size'], 1], 0], - 10, - ['*', ['number', ['feature-state', 'size'], 1], 0.2], - 15, - ['*', ['number', ['feature-state', 'size'], 1], 3], - 20, - ['*', ['number', ['feature-state', 'size'], 1], 5] - ], - 'circle-stroke-opacity': [ - 'interpolate', - ['linear'], - ['zoom'], - 10, - ['case', ['boolean', ['feature-state', 'hover'], false], 1.0, 0.1], - 15, - ['case', ['boolean', ['feature-state', 'hover'], false], 1.0, 0.8], - 20, - ['case', ['boolean', ['feature-state', 'hover'], false], 1.0, 0.9] - ], - 'circle-stroke-color': 'rgba(255,255,255,1.0)' - } + minZoom: 11, + color: { type: 'property', property: 'color' }, + strokeColor: [255, 255, 255], + strokeWidth: 2, + radius: 5, + radiusScale: 3, + strokeWidthScale: 3, + pickable: () => serviceLocator.selectedObjectsManager.get('node') === undefined }, transitNodes250mRadius: { type: 'circle', - paint: { - 'circle-radius': [ - 'interpolate', - ['exponential', 2], - ['zoom'], - 0, - 0, - 20, - ['get', '_250mRadiusPixelsAtMaxZoom'] - ], - 'circle-color': 'hsla(93, 100%, 63%, 0.08)', - 'circle-stroke-width': 3, - 'circle-stroke-color': 'hsla(93, 100%, 63%, 0.10)' - } + color: [151, 255, 66, 20], + strokeColor: [151, 255, 66, 25], + strokeWidth: 3, + radius: 250, + pickable: false }, transitNodes500mRadius: { type: 'circle', - paint: { - 'circle-radius': [ - 'interpolate', - ['exponential', 2], - ['zoom'], - 0, - 0, - 20, - ['get', '_500mRadiusPixelsAtMaxZoom'] - ], - 'circle-color': 'hsla(74, 100%, 63%, 0.06)', - 'circle-stroke-width': 2, - 'circle-stroke-color': 'hsla(74, 100%, 63%, 0.075)' - } + color: [211, 255, 66, 16], + strokeColor: [211, 255, 66, 20], + strokeWidth: 3, + radius: 500, + pickable: false }, transitNodes750mRadius: { type: 'circle', - paint: { - 'circle-radius': [ - 'interpolate', - ['exponential', 2], - ['zoom'], - 0, - 0, - 20, - ['get', '_750mRadiusPixelsAtMaxZoom'] - ], - 'circle-color': 'hsla(49, 100%, 63%, 0.025)', - 'circle-stroke-width': 1, - 'circle-stroke-color': 'hsla(49, 100%, 63%, 0.075)' - } + color: [255, 220, 66, 6], + strokeColor: [255, 220, 66, 10], + strokeWidth: 3, + radius: 750, + pickable: false }, transitNodes1000mRadius: { type: 'circle', - paint: { - 'circle-radius': [ - 'interpolate', - ['exponential', 2], - ['zoom'], - 0, - 0, - 20, - ['get', '_1000mRadiusPixelsAtMaxZoom'] - ], - 'circle-color': 'hsla(6, 100%, 63%, 0.02)', - 'circle-stroke-width': 1, - 'circle-stroke-color': 'hsla(6, 100%, 63%, 0.075)' - } + color: [255, 85, 66, 16], + strokeColor: [255, 85, 66, 20], + strokeWidth: 3, + radius: 1000, + pickable: false }, transitNodesSelected: { type: 'circle', + minZoom: 11, + color: { type: 'property', property: 'color' }, + strokeColor: [255, 255, 255], + strokeWidth: 2, + radius: 7, + radiusScale: 3, + strokeWidthScale: 3, 'custom-shader': 'circleSpinner', repaint: true, paint: { @@ -801,32 +631,44 @@ const layersConfig = { transitNodesRoutingRadius: { type: 'circle', - paint: { - 'circle-radius': [ - 'interpolate', - ['exponential', 2], - ['zoom'], - 0, - 0, - 20, - ['get', '_routingRadiusPixelsAtMaxZoom'] - ], - 'circle-color': { - property: 'color', - type: 'identity' - }, - 'circle-opacity': 0.2, - //"circle-color" : { - // property: 'color', - // type: 'identity' - //}, - 'circle-stroke-width': 1, - 'circle-stroke-opacity': 0.3, - 'circle-stroke-color': { - property: 'color', - type: 'identity' + color: (node: GeoJSON.Feature) => { + const opacity = Math.floor(0.2 * 255); // 20% + const color = node.properties?.color; + if (typeof color === 'string' && color.startsWith('#')) { + return `${color.substring(0, 7)}${opacity.toString(16)}`; } - } + if (Array.isArray(color)) { + if (color.length === 3) { + color.push(opacity); + } else if (color.length > 3) { + color[3] = opacity; + } + return color; + } + return '#0086FF33'; + }, + strokeColor: (node: GeoJSON.Feature) => { + const opacity = Math.floor(0.3 * 255); // 30% + const color = node.properties?.color; + if (typeof color === 'string' && color.startsWith('#')) { + return `${color.substring(0, 7)}${opacity.toString(16)}`; + } + if (Array.isArray(color)) { + if (color.length === 3) { + color.push(opacity); + } else if (color.length > 3) { + color[3] = opacity; + } + return color; + } + return '#0086FF4C'; + }, + strokeWidth: 3, + radius: { + type: 'property', + property: 'routing_radius_meters' + }, + pickable: false }, transitNodesStationSelected: { diff --git a/packages/transition-frontend/src/services/map/events/NodeSectionMapEvents.ts b/packages/transition-frontend/src/services/map/events/NodeSectionMapEvents.ts index f576715e..e777cea0 100644 --- a/packages/transition-frontend/src/services/map/events/NodeSectionMapEvents.ts +++ b/packages/transition-frontend/src/services/map/events/NodeSectionMapEvents.ts @@ -158,8 +158,8 @@ const onMapClicked = (pointInfo: PointInfo, e: MjolnirEvent) => { // FIXME Migration to DeckGL: Reimplement // serviceLocator.eventManager.emit('selected.updateAutocompleteNameChoices.node', getRoadLabelAround(map, e)); selectedTransitNode.set('geography.coordinates', pointInfo.coordinates); - // FIXME Migration to DeckGL: Do we need to call this event? - // serviceLocator.eventManager.emit('selected.dragEnd.node', coordinates); + // This updates the position on the map. + serviceLocator.eventManager.emit('selected.drag.node', pointInfo.coordinates); serviceLocator.selectedObjectsManager.update('node', selectedNode); } }